}
+ */
+const fetchPresets = async () => {
+ const response = await fetch('option-presets');
+ presets = await response.json();
+ const presetSelect = document.getElementById('game-options-preset');
+ presetSelect.removeAttribute('disabled');
+
+ const game = document.getElementById('player-options').getAttribute('data-game');
+ const presetToApply = localStorage.getItem(`${game}-preset`);
+ const playerName = localStorage.getItem(`${game}-player`);
+ if (presetToApply) {
+ localStorage.removeItem(`${game}-preset`);
+ presetSelect.value = presetToApply;
+ applyPresets(presetToApply);
+ }
+
+ if (playerName) {
+ document.getElementById('player-name').value = playerName;
+ localStorage.removeItem(`${game}-player`);
+ }
+};
+
+/**
+ * Clear the localStorage for this game and set a preset to be loaded upon page reload
+ * @param evt
+ */
+const choosePreset = (evt) => {
+ if (evt.target.value === 'custom') { return; }
+
+ const game = document.getElementById('player-options').getAttribute('data-game');
+ localStorage.removeItem(game);
+
+ localStorage.setItem(`${game}-player`, document.getElementById('player-name').value);
+ if (evt.target.value !== 'default') {
+ localStorage.setItem(`${game}-preset`, evt.target.value);
+ }
+
+ document.querySelectorAll('#options-form input, #options-form select').forEach((input) => {
+ if (input.id === 'player-name') { return; }
+ input.removeAttribute('value');
+ });
+
+ window.location.replace(window.location.href);
+};
+
+const applyPresets = (presetName) => {
+ // Ignore the "default" preset, because it gets set automatically by Jinja
+ if (presetName === 'default') {
+ saveSettings();
+ return;
+ }
+
+ if (!presets[presetName]) {
+ console.error(`Unknown preset ${presetName} chosen`);
+ return;
+ }
+
+ const preset = presets[presetName];
+ Object.keys(preset).forEach((optionName) => {
+ const optionValue = preset[optionName];
+
+ // Handle List and Set options
+ if (Array.isArray(optionValue)) {
+ document.querySelectorAll(`input[type=checkbox][name=${optionName}]`).forEach((checkbox) => {
+ if (optionValue.includes(checkbox.value)) {
+ checkbox.setAttribute('checked', '1');
+ } else {
+ checkbox.removeAttribute('checked');
+ }
+ });
+ return;
+ }
+
+ // Handle Dict options
+ if (typeof(optionValue) === 'object' && optionValue !== null) {
+ const itemNames = Object.keys(optionValue);
+ document.querySelectorAll(`input[type=number][data-option-name=${optionName}]`).forEach((input) => {
+ const itemName = input.getAttribute('data-item-name');
+ input.value = (itemNames.includes(itemName)) ? optionValue[itemName] : 0
+ });
+ return;
+ }
+
+ // Identify all possible elements
+ const normalInput = document.getElementById(optionName);
+ const customInput = document.getElementById(`${optionName}-custom`);
+ const rangeValue = document.getElementById(`${optionName}-value`);
+ const randomizeInput = document.getElementById(`random-${optionName}`);
+ const namedRangeSelect = document.getElementById(`${optionName}-select`);
+
+ // It is possible for named ranges to use name of a value rather than the value itself. This is accounted for here
+ let trueValue = optionValue;
+ if (namedRangeSelect) {
+ namedRangeSelect.querySelectorAll('option').forEach((opt) => {
+ if (opt.innerText.startsWith(optionValue)) {
+ trueValue = opt.value;
+ }
+ });
+ namedRangeSelect.value = trueValue;
+ }
+
+ // Handle options whose presets are "random"
+ if (optionValue === 'random') {
+ normalInput.setAttribute('disabled', '1');
+ randomizeInput.setAttribute('checked', '1');
+ if (customInput) {
+ customInput.setAttribute('disabled', '1');
+ }
+ if (rangeValue) {
+ rangeValue.innerText = normalInput.value;
+ }
+ if (namedRangeSelect) {
+ namedRangeSelect.setAttribute('disabled', '1');
+ }
+ return;
+ }
+
+ // Handle normal (text, number, select, etc.) and custom inputs (custom inputs exist with TextChoice only)
+ normalInput.value = trueValue;
+ normalInput.removeAttribute('disabled');
+ randomizeInput.removeAttribute('checked');
+ if (customInput) {
+ document.getElementById(`${optionName}-custom`).removeAttribute('disabled');
+ }
+ if (rangeValue) {
+ rangeValue.innerText = trueValue;
+ }
+ });
+
+ saveSettings();
+};
+
+const showUserMessage = (text) => {
+ const userMessage = document.getElementById('user-message');
+ userMessage.innerText = text;
+ userMessage.addEventListener('click', hideUserMessage);
+ userMessage.style.display = 'block';
+};
+
+const hideUserMessage = () => {
+ const userMessage = document.getElementById('user-message');
+ userMessage.removeEventListener('click', hideUserMessage);
+ userMessage.style.display = 'none';
+};
diff --git a/WebHostLib/static/assets/sc2Tracker.js b/WebHostLib/static/assets/sc2Tracker.js
new file mode 100644
index 000000000000..30d4acd60b7e
--- /dev/null
+++ b/WebHostLib/static/assets/sc2Tracker.js
@@ -0,0 +1,49 @@
+window.addEventListener('load', () => {
+ // Reload tracker every 15 seconds
+ const url = window.location;
+ setInterval(() => {
+ const ajax = new XMLHttpRequest();
+ ajax.onreadystatechange = () => {
+ if (ajax.readyState !== 4) { return; }
+
+ // Create a fake DOM using the returned HTML
+ const domParser = new DOMParser();
+ const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
+
+ // Update item tracker
+ document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
+ // Update only counters in the location-table
+ let counters = document.getElementsByClassName('counter');
+ const fakeCounters = fakeDOM.getElementsByClassName('counter');
+ for (let i = 0; i < counters.length; i++) {
+ counters[i].innerHTML = fakeCounters[i].innerHTML;
+ }
+ };
+ ajax.open('GET', url);
+ ajax.send();
+ }, 15000)
+
+ // Collapsible advancement sections
+ const categories = document.getElementsByClassName("location-category");
+ for (let category of categories) {
+ let hide_id = category.id.split('_')[0];
+ if (hide_id === 'Total') {
+ continue;
+ }
+ category.addEventListener('click', function() {
+ // Toggle the advancement list
+ document.getElementById(hide_id).classList.toggle("hide");
+ // Change text of the header
+ const tab_header = document.getElementById(hide_id+'_header').children[0];
+ const orig_text = tab_header.innerHTML;
+ let new_text;
+ if (orig_text.includes("â–¼")) {
+ new_text = orig_text.replace("â–¼", "â–²");
+ }
+ else {
+ new_text = orig_text.replace("â–²", "â–¼");
+ }
+ tab_header.innerHTML = new_text;
+ });
+ }
+});
diff --git a/WebHostLib/static/assets/sc2wolTracker.js b/WebHostLib/static/assets/sc2wolTracker.js
deleted file mode 100644
index a698214b8dd6..000000000000
--- a/WebHostLib/static/assets/sc2wolTracker.js
+++ /dev/null
@@ -1,49 +0,0 @@
-window.addEventListener('load', () => {
- // Reload tracker every 15 seconds
- const url = window.location;
- setInterval(() => {
- const ajax = new XMLHttpRequest();
- ajax.onreadystatechange = () => {
- if (ajax.readyState !== 4) { return; }
-
- // Create a fake DOM using the returned HTML
- const domParser = new DOMParser();
- const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
-
- // Update item tracker
- document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
- // Update only counters in the location-table
- let counters = document.getElementsByClassName('counter');
- const fakeCounters = fakeDOM.getElementsByClassName('counter');
- for (let i = 0; i < counters.length; i++) {
- counters[i].innerHTML = fakeCounters[i].innerHTML;
- }
- };
- ajax.open('GET', url);
- ajax.send();
- }, 15000)
-
- // Collapsible advancement sections
- const categories = document.getElementsByClassName("location-category");
- for (let i = 0; i < categories.length; i++) {
- let hide_id = categories[i].id.split('-')[0];
- if (hide_id == 'Total') {
- continue;
- }
- categories[i].addEventListener('click', function() {
- // Toggle the advancement list
- document.getElementById(hide_id).classList.toggle("hide");
- // Change text of the header
- const tab_header = document.getElementById(hide_id+'-header').children[0];
- const orig_text = tab_header.innerHTML;
- let new_text;
- if (orig_text.includes("â–¼")) {
- new_text = orig_text.replace("â–¼", "â–²");
- }
- else {
- new_text = orig_text.replace("â–²", "â–¼");
- }
- tab_header.innerHTML = new_text;
- });
- }
-});
diff --git a/WebHostLib/static/assets/supportedGames.js b/WebHostLib/static/assets/supportedGames.js
new file mode 100644
index 000000000000..b692db9283d2
--- /dev/null
+++ b/WebHostLib/static/assets/supportedGames.js
@@ -0,0 +1,44 @@
+window.addEventListener('load', () => {
+ // Add toggle listener to all elements with .collapse-toggle
+ const toggleButtons = document.querySelectorAll('details');
+
+ // 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 games as collapsed
+ return toggleButtons.forEach((header) => {
+ header.style.display = null;
+ header.removeAttribute('open');
+ });
+ }
+
+ // Loop over all the games
+ toggleButtons.forEach((header) => {
+ // If the game name includes the search string, display the game. If not, hide it
+ if (header.getAttribute('data-game').toLowerCase().includes(evt.target.value.toLowerCase())) {
+ header.style.display = null;
+ header.setAttribute('open', '1');
+ } else {
+ header.style.display = 'none';
+ header.removeAttribute('open');
+ }
+ });
+ });
+
+ document.getElementById('expand-all').addEventListener('click', expandAll);
+ document.getElementById('collapse-all').addEventListener('click', collapseAll);
+});
+
+const expandAll = () => {
+ document.querySelectorAll('details').forEach((detail) => {
+ detail.setAttribute('open', '1');
+ });
+};
+
+const collapseAll = () => {
+ document.querySelectorAll('details').forEach((detail) => {
+ detail.removeAttribute('open');
+ });
+};
diff --git a/WebHostLib/static/assets/trackerCommon.js b/WebHostLib/static/assets/trackerCommon.js
index 41c4020dace8..6324837b2816 100644
--- a/WebHostLib/static/assets/trackerCommon.js
+++ b/WebHostLib/static/assets/trackerCommon.js
@@ -4,13 +4,20 @@ const adjustTableHeight = () => {
return;
const upperDistance = tablesContainer.getBoundingClientRect().top;
- const containerHeight = window.innerHeight - upperDistance;
- tablesContainer.style.maxHeight = `calc(${containerHeight}px - 1rem)`;
-
const tableWrappers = document.getElementsByClassName('table-wrapper');
- for(let i=0; i < tableWrappers.length; i++){
- const maxHeight = (window.innerHeight - upperDistance) / 2;
- tableWrappers[i].style.maxHeight = `calc(${maxHeight}px - 1rem)`;
+ for (let i = 0; i < tableWrappers.length; i++) {
+ // Ensure we are starting from maximum size prior to calculation.
+ tableWrappers[i].style.height = null;
+ tableWrappers[i].style.maxHeight = null;
+
+ // Set as a reasonable height, but still allows the user to resize element if they desire.
+ const currentHeight = tableWrappers[i].offsetHeight;
+ const maxHeight = (window.innerHeight - upperDistance) / Math.min(tableWrappers.length, 4);
+ if (currentHeight > maxHeight) {
+ tableWrappers[i].style.height = `calc(${maxHeight}px - 1rem)`;
+ }
+
+ tableWrappers[i].style.maxHeight = `${currentHeight}px`;
}
};
@@ -20,7 +27,7 @@ const adjustTableHeight = () => {
* @returns {string}
*/
const secondsToHours = (seconds) => {
- let hours = Math.floor(seconds / 3600);
+ let hours = Math.floor(seconds / 3600);
let minutes = Math.floor((seconds - (hours * 3600)) / 60).toString().padStart(2, '0');
return `${hours}:${minutes}`;
};
@@ -31,18 +38,18 @@ window.addEventListener('load', () => {
info: false,
dom: "t",
stateSave: true,
- stateSaveCallback: function(settings, data) {
+ stateSaveCallback: function (settings, data) {
delete data.search;
localStorage.setItem(`DataTables_${settings.sInstance}_/tracker`, JSON.stringify(data));
},
- stateLoadCallback: function(settings) {
+ stateLoadCallback: function (settings) {
return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`));
},
- footerCallback: function(tfoot, data, start, end, display) {
+ footerCallback: function (tfoot, data, start, end, display) {
if (tfoot) {
const activityData = this.api().column('lastActivity:name').data().toArray().filter(x => !isNaN(x));
Array.from(tfoot?.children).find(td => td.classList.contains('last-activity')).innerText =
- (activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None';
+ (activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None';
}
},
columnDefs: [
@@ -55,7 +62,7 @@ window.addEventListener('load', () => {
render: function (data, type, row) {
if (type === "sort" || type === 'type') {
if (data === "None")
- return -1;
+ return Number.MAX_VALUE;
return parseInt(data);
}
@@ -116,49 +123,64 @@ window.addEventListener('load', () => {
event.preventDefault();
}
});
- const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
- const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3;
+ const target_second = parseInt(document.getElementById('tracker-wrapper').getAttribute('data-second')) + 3;
+ console.log("Target second of refresh: " + target_second);
- function getSleepTimeSeconds(){
+ function getSleepTimeSeconds() {
// -40 % 60 is -40, which is absolutely wrong and should burn
var sleepSeconds = (((target_second - new Date().getSeconds()) % 60) + 60) % 60;
return sleepSeconds || 60;
}
+ let update_on_view = false;
const update = () => {
- const target = $("
");
- console.log("Updating Tracker...");
- target.load(location.href, function (response, status) {
- if (status === "success") {
- target.find(".table").each(function (i, new_table) {
- const new_trs = $(new_table).find("tbody>tr");
- const footer_tr = $(new_table).find("tfoot>tr");
- const old_table = tables.eq(i);
- const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
- const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
- old_table.clear();
- if (footer_tr.length) {
- $(old_table.table).find("tfoot").html(footer_tr);
- }
- old_table.rows.add(new_trs);
- old_table.draw();
- $(old_table.settings()[0].nScrollBody).scrollTop(topscroll);
- $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
- });
- $("#multi-stream-link").replaceWith(target.find("#multi-stream-link"));
- } else {
- console.log("Failed to connect to Server, in order to update Table Data.");
- console.log(response);
- }
- })
- setTimeout(update, getSleepTimeSeconds()*1000);
+ if (document.hidden) {
+ console.log("Document reporting as not visible, not updating Tracker...");
+ update_on_view = true;
+ } else {
+ update_on_view = false;
+ const target = $("
");
+ console.log("Updating Tracker...");
+ target.load(location.href, function (response, status) {
+ if (status === "success") {
+ target.find(".table").each(function (i, new_table) {
+ const new_trs = $(new_table).find("tbody>tr");
+ const footer_tr = $(new_table).find("tfoot>tr");
+ const old_table = tables.eq(i);
+ const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
+ const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
+ old_table.clear();
+ if (footer_tr.length) {
+ $(old_table.table).find("tfoot").html(footer_tr);
+ }
+ old_table.rows.add(new_trs);
+ old_table.draw();
+ $(old_table.settings()[0].nScrollBody).scrollTop(topscroll);
+ $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
+ });
+ $("#multi-stream-link").replaceWith(target.find("#multi-stream-link"));
+ } else {
+ console.log("Failed to connect to Server, in order to update Table Data.");
+ console.log(response);
+ }
+ })
+ }
+ updater = setTimeout(update, getSleepTimeSeconds() * 1000);
}
- setTimeout(update, getSleepTimeSeconds()*1000);
+ let updater = setTimeout(update, getSleepTimeSeconds() * 1000);
window.addEventListener('resize', () => {
adjustTableHeight();
tables.draw();
});
+ window.addEventListener('visibilitychange', () => {
+ if (!document.hidden && update_on_view) {
+ console.log("Page became visible, tracker should be refreshed.");
+ clearTimeout(updater);
+ update();
+ }
+ });
+
adjustTableHeight();
});
diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js
deleted file mode 100644
index 07157cb57915..000000000000
--- a/WebHostLib/static/assets/weighted-settings.js
+++ /dev/null
@@ -1,1250 +0,0 @@
-window.addEventListener('load', () => {
- fetchSettingData().then((results) => {
- let settingHash = localStorage.getItem('weighted-settings-hash');
- if (!settingHash) {
- // If no hash data has been set before, set it now
- settingHash = md5(JSON.stringify(results));
- localStorage.setItem('weighted-settings-hash', settingHash);
- localStorage.removeItem('weighted-settings');
- }
-
- if (settingHash !== md5(JSON.stringify(results))) {
- const userMessage = document.getElementById('user-message');
- userMessage.innerText = "Your settings are out of date! Click here to update them! Be aware this will reset " +
- "them all to default.";
- userMessage.classList.add('visible');
- userMessage.addEventListener('click', resetSettings);
- }
-
- // Page setup
- createDefaultSettings(results);
- buildUI(results);
- updateVisibleGames();
- adjustHeaderWidth();
-
- // Event listeners
- document.getElementById('export-settings').addEventListener('click', () => exportSettings());
- document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
- document.getElementById('generate-game').addEventListener('click', () => generateGame());
-
- // Name input field
- const weightedSettings = JSON.parse(localStorage.getItem('weighted-settings'));
- const nameInput = document.getElementById('player-name');
- nameInput.setAttribute('data-type', 'data');
- nameInput.setAttribute('data-setting', 'name');
- nameInput.addEventListener('keyup', updateBaseSetting);
- nameInput.value = weightedSettings.name;
- });
-});
-
-const resetSettings = () => {
- localStorage.removeItem('weighted-settings');
- localStorage.removeItem('weighted-settings-hash')
- window.location.reload();
-};
-
-const fetchSettingData = () => new Promise((resolve, reject) => {
- fetch(new Request(`${window.location.origin}/static/generated/weighted-settings.json`)).then((response) => {
- try{ response.json().then((jsonObj) => resolve(jsonObj)); }
- catch(error){ reject(error); }
- });
-});
-
-const createDefaultSettings = (settingData) => {
- if (!localStorage.getItem('weighted-settings')) {
- const newSettings = {};
-
- // Transfer base options directly
- for (let baseOption of Object.keys(settingData.baseOptions)){
- newSettings[baseOption] = settingData.baseOptions[baseOption];
- }
-
- // Set options per game
- for (let game of Object.keys(settingData.games)) {
- // Initialize game object
- newSettings[game] = {};
-
- // Transfer game settings
- for (let gameSetting of Object.keys(settingData.games[game].gameSettings)){
- newSettings[game][gameSetting] = {};
-
- const setting = settingData.games[game].gameSettings[gameSetting];
- switch(setting.type){
- case 'select':
- setting.options.forEach((option) => {
- newSettings[game][gameSetting][option.value] =
- (setting.hasOwnProperty('defaultValue') && setting.defaultValue === option.value) ? 25 : 0;
- });
- break;
- case 'range':
- case 'special_range':
- newSettings[game][gameSetting]['random'] = 0;
- newSettings[game][gameSetting]['random-low'] = 0;
- newSettings[game][gameSetting]['random-high'] = 0;
- if (setting.hasOwnProperty('defaultValue')) {
- newSettings[game][gameSetting][setting.defaultValue] = 25;
- } else {
- newSettings[game][gameSetting][setting.min] = 25;
- }
- break;
-
- case 'items-list':
- case 'locations-list':
- case 'custom-list':
- newSettings[game][gameSetting] = setting.defaultValue;
- break;
-
- default:
- console.error(`Unknown setting type for ${game} setting ${gameSetting}: ${setting.type}`);
- }
- }
-
- 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 = [];
- }
-
- localStorage.setItem('weighted-settings', JSON.stringify(newSettings));
- }
-};
-
-const buildUI = (settingData) => {
- // Build the game-choice div
- buildGameChoice(settingData.games);
-
- const gamesWrapper = document.getElementById('games-wrapper');
- Object.keys(settingData.games).forEach((game) => {
- // Create game div, invisible by default
- const gameDiv = document.createElement('div');
- gameDiv.setAttribute('id', `${game}-div`);
- gameDiv.classList.add('game-div');
- gameDiv.classList.add('invisible');
-
- const gameHeader = document.createElement('h2');
- gameHeader.innerText = game;
- gameDiv.appendChild(gameHeader);
-
- const collapseButton = document.createElement('a');
- collapseButton.innerText = '(Collapse)';
- gameDiv.appendChild(collapseButton);
-
- const expandButton = document.createElement('a');
- expandButton.innerText = '(Expand)';
- expandButton.classList.add('invisible');
- gameDiv.appendChild(expandButton);
-
- 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)));
-
- const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings,
- settingData.games[game].gameItems, settingData.games[game].gameLocations);
- gameDiv.appendChild(weightedSettingsDiv);
-
- const itemPoolDiv = buildItemsDiv(game, settingData.games[game].gameItems);
- gameDiv.appendChild(itemPoolDiv);
-
- const hintsDiv = buildHintsDiv(game, settingData.games[game].gameItems, settingData.games[game].gameLocations);
- gameDiv.appendChild(hintsDiv);
-
- const locationsDiv = buildLocationsDiv(game, settingData.games[game].gameLocations);
- gameDiv.appendChild(locationsDiv);
-
- gamesWrapper.appendChild(gameDiv);
-
- collapseButton.addEventListener('click', () => {
- collapseButton.classList.add('invisible');
- weightedSettingsDiv.classList.add('invisible');
- itemPoolDiv.classList.add('invisible');
- hintsDiv.classList.add('invisible');
- expandButton.classList.remove('invisible');
- });
-
- expandButton.addEventListener('click', () => {
- collapseButton.classList.remove('invisible');
- weightedSettingsDiv.classList.remove('invisible');
- itemPoolDiv.classList.remove('invisible');
- hintsDiv.classList.remove('invisible');
- expandButton.classList.add('invisible');
- });
- });
-};
-
-const buildGameChoice = (games) => {
- const settings = JSON.parse(localStorage.getItem('weighted-settings'));
- const gameChoiceDiv = document.getElementById('game-choice');
- const h2 = document.createElement('h2');
- h2.innerText = 'Game Select';
- gameChoiceDiv.appendChild(h2);
-
- const gameSelectDescription = document.createElement('p');
- gameSelectDescription.classList.add('setting-description');
- gameSelectDescription.innerText = 'Choose which games you might be required to play.';
- gameChoiceDiv.appendChild(gameSelectDescription);
-
- const hintText = document.createElement('p');
- hintText.classList.add('hint-text');
- hintText.innerText = 'If a game\'s value is greater than zero, you can click it\'s name to jump ' +
- 'to that section.'
- gameChoiceDiv.appendChild(hintText);
-
- // Build the game choice table
- const table = document.createElement('table');
- const tbody = document.createElement('tbody');
-
- Object.keys(games).forEach((game) => {
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- const span = document.createElement('span');
- span.innerText = game;
- span.setAttribute('id', `${game}-game-option`)
- tdLeft.appendChild(span);
- tr.appendChild(tdLeft);
-
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('min', 0);
- range.setAttribute('max', 50);
- range.setAttribute('data-type', 'weight');
- range.setAttribute('data-setting', 'game');
- range.setAttribute('data-option', game);
- range.value = settings.game[game];
- range.addEventListener('change', (evt) => {
- updateBaseSetting(evt);
- updateVisibleGames(); // Show or hide games based on the new settings
- });
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `game-${game}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
- tbody.appendChild(tr);
- });
-
- table.appendChild(tbody);
- gameChoiceDiv.appendChild(table);
-};
-
-const 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);
- });
-
- optionTable.appendChild(tbody);
- settingWrapper.appendChild(optionTable);
- break;
-
- case 'range':
- case 'special_range':
- const rangeTable = document.createElement('table');
- const rangeTbody = document.createElement('tbody');
-
- if (((setting.max - setting.min) + 1) < 11) {
- for (let i=setting.min; i <= setting.max; ++i) {
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- tdLeft.innerText = i;
- tr.appendChild(tdLeft);
-
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('id', `${game}-${settingName}-${i}-range`);
- range.setAttribute('data-game', game);
- range.setAttribute('data-setting', settingName);
- range.setAttribute('data-option', i);
- range.setAttribute('min', 0);
- range.setAttribute('max', 50);
- range.addEventListener('change', updateRangeSetting);
- range.value = currentSettings[game][settingName][i] || 0;
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `${game}-${settingName}-${i}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
-
- rangeTbody.appendChild(tr);
- }
- } else {
- 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')); }
- });
-
- addOptionButton.addEventListener('click', () => {
- const optionInput = document.getElementById(`${game}-${settingName}-option`);
- let option = optionInput.value;
- if (!option || !option.trim()) { return; }
- option = parseInt(option, 10);
- if ((option < setting.min) || (option > setting.max)) { return; }
- optionInput.value = '';
- if (document.getElementById(`${game}-${settingName}-${option}-range`)) { return; }
-
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- tdLeft.innerText = option;
- tr.appendChild(tdLeft);
-
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('id', `${game}-${settingName}-${option}-range`);
- range.setAttribute('data-game', game);
- range.setAttribute('data-setting', settingName);
- range.setAttribute('data-option', option);
- range.setAttribute('min', 0);
- range.setAttribute('max', 50);
- range.addEventListener('change', updateRangeSetting);
- range.value = currentSettings[game][settingName][parseInt(option, 10)];
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `${game}-${settingName}-${option}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
-
- const tdDelete = document.createElement('td');
- tdDelete.classList.add('td-delete');
- const deleteButton = document.createElement('span');
- deleteButton.classList.add('range-option-delete');
- deleteButton.innerText = 'âŒ';
- deleteButton.addEventListener('click', () => {
- range.value = 0;
- range.dispatchEvent(new Event('change'));
- rangeTbody.removeChild(tr);
- });
- tdDelete.appendChild(deleteButton);
- tr.appendChild(tdDelete);
-
- rangeTbody.appendChild(tr);
-
- // Save new option to settings
- range.dispatchEvent(new Event('change'));
- });
-
- 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; }
-
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- tdLeft.innerText = option;
- tr.appendChild(tdLeft);
-
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('id', `${game}-${settingName}-${option}-range`);
- range.setAttribute('data-game', game);
- range.setAttribute('data-setting', settingName);
- range.setAttribute('data-option', option);
- range.setAttribute('min', 0);
- range.setAttribute('max', 50);
- range.addEventListener('change', updateRangeSetting);
- range.value = currentSettings[game][settingName][parseInt(option, 10)];
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `${game}-${settingName}-${option}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
-
- const tdDelete = document.createElement('td');
- tdDelete.classList.add('td-delete');
- const deleteButton = document.createElement('span');
- deleteButton.classList.add('range-option-delete');
- deleteButton.innerText = 'âŒ';
- deleteButton.addEventListener('click', () => {
- range.value = 0;
- const changeEvent = new Event('change');
- changeEvent.action = 'rangeDelete';
- range.dispatchEvent(changeEvent);
- rangeTbody.removeChild(tr);
- });
- tdDelete.appendChild(deleteButton);
- tr.appendChild(tdDelete);
-
- rangeTbody.appendChild(tr);
- });
- }
-
- ['random', 'random-low', 'random-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;
-
- 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');
- }
-
- const itemName = document.createElement('span');
- itemName.innerText = item.toString();
-
- itemLabel.appendChild(itemCheckbox);
- itemLabel.appendChild(itemName);
-
- itemRow.appendChild(itemLabel);
- itemsList.appendChild((itemRow));
- });
-
- settingWrapper.appendChild(itemsList);
- break;
-
- 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 locationName = document.createElement('span');
- locationName.innerText = location.toString();
-
- locationLabel.appendChild(locationCheckbox);
- locationLabel.appendChild(locationName);
-
- locationRow.appendChild(locationLabel);
- locationsList.appendChild((locationRow));
- });
-
- settingWrapper.appendChild(locationsList);
- break;
-
- 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');
- }
-
- const customItemName = document.createElement('span');
- customItemName.innerText = listItem.toString();
-
- customItemLabel.appendChild(customItemCheckbox);
- customItemLabel.appendChild(customItemName);
-
- customListRow.appendChild(customItemLabel);
- customList.appendChild((customListRow));
- });
-
- settingWrapper.appendChild(customList);
- break;
-
- default:
- console.error(`Unknown setting type for ${game} setting ${settingName}: ${setting.type}`);
- return;
- }
-
- settingsWrapper.appendChild(settingWrapper);
- });
-
- return settingsWrapper;
-};
-
-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);
- }
- });
-
- 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;
-};
-
-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;
-};
-
-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);
-
- itemQtyDiv.addEventListener('dragstart', (evt) => {
- evt.dataTransfer.setData('text/plain', itemQtyDiv.getAttribute('id'));
- });
- return itemQtyDiv;
-};
-
-const itemDragoverHandler = (evt) => {
- evt.preventDefault();
-};
-
-const itemDropHandler = (evt) => {
- evt.preventDefault();
- const sourceId = evt.dataTransfer.getData('text/plain');
- const sourceDiv = document.getElementById(sourceId);
-
- const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
- const game = sourceDiv.getAttribute('data-game');
- const item = sourceDiv.getAttribute('data-item');
-
- const oldSetting = sourceDiv.hasAttribute('data-setting') ? sourceDiv.getAttribute('data-setting') : null;
- const newSetting = evt.target.hasAttribute('data-setting') ? evt.target.getAttribute('data-setting') : null;
-
- const itemDiv = newSetting === 'start_inventory' ? buildItemQtyDiv(game, item) : buildItemDiv(game, item);
-
- 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);
- }
- }
- }
-
- 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);
- }
- }
- } 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);
-
- // Save the updated settings
- localStorage.setItem('weighted-settings', JSON.stringify(currentSettings));
-};
-
-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);
-
- const itemName = document.createElement('span');
- itemName.innerText = item;
- itemLabel.appendChild(itemName);
-
- 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);
-
- const locationName = document.createElement('span');
- locationName.innerText = location;
- locationLabel.appendChild(locationName);
-
- locationRow.appendChild(locationLabel);
- locationHintsDiv.appendChild(locationRow);
- });
-
- locationHintsWrapper.appendChild(locationHintsDiv);
- itemHintsContainer.appendChild(locationHintsWrapper);
-
- 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);
-
- const locationName = document.createElement('span');
- locationName.innerText = location;
- locationLabel.appendChild(locationName);
-
- 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);
-
- const locationName = document.createElement('span');
- locationName.innerText = location;
- locationLabel.appendChild(locationName);
-
- locationRow.appendChild(locationLabel);
- excludeLocationsDiv.appendChild(locationRow);
- });
-
- excludeLocationsWrapper.appendChild(excludeLocationsDiv);
- locationsContainer.appendChild(excludeLocationsWrapper);
-
- 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',
- });
- });
- } else {
- gameDiv.classList.add('invisible');
- gameOption.classList.remove('jump-link');
-
- }
- });
-};
-
-const updateBaseSetting = (event) => {
- const settings = JSON.parse(localStorage.getItem('weighted-settings'));
- const setting = event.target.getAttribute('data-setting');
- const option = event.target.getAttribute('data-option');
- const type = event.target.getAttribute('data-type');
-
- switch(type){
- case 'weight':
- settings[setting][option] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10);
- document.getElementById(`${setting}-${option}`).innerText = event.target.value;
- break;
- case 'data':
- settings[setting] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10);
- break;
- }
-
- localStorage.setItem('weighted-settings', JSON.stringify(settings));
-};
-
-const 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; }
-
- 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; }
-
- 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;
- }
-
- // 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;
-};
-
-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) => {
- const downloadLink = document.createElement('a');
- downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
- downloadLink.setAttribute('download', filename);
- downloadLink.style.display = 'none';
- document.body.appendChild(downloadLink);
- downloadLink.click();
- document.body.removeChild(downloadLink);
-};
-
-const generateGame = (raceMode = false) => {
- 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/assets/weightedOptions.js b/WebHostLib/static/assets/weightedOptions.js
new file mode 100644
index 000000000000..0417ab174b0e
--- /dev/null
+++ b/WebHostLib/static/assets/weightedOptions.js
@@ -0,0 +1,223 @@
+let deletedOptions = {};
+
+window.addEventListener('load', () => {
+ const worldName = document.querySelector('#weighted-options').getAttribute('data-game');
+
+ // Generic change listener. Detecting unique qualities and acting on them here reduces initial JS initialisation time
+ // and handles dynamically created elements
+ document.addEventListener('change', (evt) => {
+ // Handle updates to range inputs
+ if (evt.target.type === 'range') {
+ // Update span containing range value. All ranges have a corresponding `{rangeId}-value` span
+ document.getElementById(`${evt.target.id}-value`).innerText = evt.target.value;
+
+ // If the changed option was the name of a game, determine whether to show or hide that game's div
+ if (evt.target.id.startsWith('game||')) {
+ const gameName = evt.target.id.split('||')[1];
+ const gameDiv = document.getElementById(`${gameName}-container`);
+ if (evt.target.value > 0) {
+ gameDiv.classList.remove('hidden');
+ } else {
+ gameDiv.classList.add('hidden');
+ }
+ }
+ }
+ });
+
+ // Generic click listener
+ document.addEventListener('click', (evt) => {
+ // Handle creating new rows for Range options
+ if (evt.target.classList.contains('add-range-option-button')) {
+ const optionName = evt.target.getAttribute('data-option');
+ addRangeRow(optionName);
+ }
+
+ // Handle deleting range rows
+ if (evt.target.classList.contains('range-option-delete')) {
+ const targetRow = document.querySelector(`tr[data-row="${evt.target.getAttribute('data-target')}"]`);
+ setDeletedOption(
+ targetRow.getAttribute('data-option-name'),
+ targetRow.getAttribute('data-value'),
+ );
+ targetRow.parentElement.removeChild(targetRow);
+ }
+ });
+
+ // Listen for enter presses on inputs intended to add range rows
+ document.addEventListener('keydown', (evt) => {
+ if (evt.key === 'Enter') {
+ evt.preventDefault();
+ }
+
+ if (evt.key === 'Enter' && evt.target.classList.contains('range-option-value')) {
+ const optionName = evt.target.getAttribute('data-option');
+ addRangeRow(optionName);
+ }
+ });
+
+ // Detect form submission
+ document.getElementById('weighted-options-form').addEventListener('submit', (evt) => {
+ // Save data to localStorage
+ const weightedOptions = {};
+ document.querySelectorAll('input[name]').forEach((input) => {
+ const keys = input.getAttribute('name').split('||');
+
+ // Determine keys
+ const optionName = keys[0] ?? null;
+ const subOption = keys[1] ?? null;
+
+ // Ensure keys exist
+ if (!weightedOptions[optionName]) { weightedOptions[optionName] = {}; }
+ if (subOption && !weightedOptions[optionName][subOption]) {
+ weightedOptions[optionName][subOption] = null;
+ }
+
+ if (subOption) { return weightedOptions[optionName][subOption] = determineValue(input); }
+ if (optionName) { return weightedOptions[optionName] = determineValue(input); }
+ });
+
+ localStorage.setItem(`${worldName}-weights`, JSON.stringify(weightedOptions));
+ localStorage.setItem(`${worldName}-deletedOptions`, JSON.stringify(deletedOptions));
+ });
+
+ // Remove all deleted values as specified by localStorage
+ deletedOptions = JSON.parse(localStorage.getItem(`${worldName}-deletedOptions`) || '{}');
+ Object.keys(deletedOptions).forEach((optionName) => {
+ deletedOptions[optionName].forEach((value) => {
+ const targetRow = document.querySelector(`tr[data-row="${value}-row"]`);
+ targetRow.parentElement.removeChild(targetRow);
+ });
+ });
+
+ // Populate all settings from localStorage on page initialisation
+ const previousSettingsJson = localStorage.getItem(`${worldName}-weights`);
+ if (previousSettingsJson) {
+ const previousSettings = JSON.parse(previousSettingsJson);
+ Object.keys(previousSettings).forEach((option) => {
+ if (typeof previousSettings[option] === 'string') {
+ return document.querySelector(`input[name="${option}"]`).value = previousSettings[option];
+ }
+
+ Object.keys(previousSettings[option]).forEach((value) => {
+ const input = document.querySelector(`input[name="${option}||${value}"]`);
+ if (!input?.type) {
+ return console.error(`Unable to populate option with name ${option}||${value}.`);
+ }
+
+ switch (input.type) {
+ case 'checkbox':
+ input.checked = (parseInt(previousSettings[option][value], 10) === 1);
+ break;
+ case 'range':
+ input.value = parseInt(previousSettings[option][value], 10);
+ break;
+ case 'number':
+ input.value = previousSettings[option][value].toString();
+ break;
+ default:
+ console.error(`Found unsupported input type: ${input.type}`);
+ }
+ });
+ });
+ }
+});
+
+const addRangeRow = (optionName) => {
+ const inputQuery = `input[type=number][data-option="${optionName}"].range-option-value`;
+ const inputTarget = document.querySelector(inputQuery);
+ const newValue = inputTarget.value;
+ if (!/^-?\d+$/.test(newValue)) {
+ alert('Range values must be a positive or negative integer!');
+ return;
+ }
+ inputTarget.value = '';
+ const tBody = document.querySelector(`table[data-option="${optionName}"].range-rows tbody`);
+ const tr = document.createElement('tr');
+ tr.setAttribute('data-row', `${optionName}-${newValue}-row`);
+ tr.setAttribute('data-option-name', optionName);
+ tr.setAttribute('data-value', newValue);
+ const tdLeft = document.createElement('td');
+ tdLeft.classList.add('td-left');
+ const label = document.createElement('label');
+ label.setAttribute('for', `${optionName}||${newValue}`);
+ label.innerText = newValue.toString();
+ tdLeft.appendChild(label);
+ 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('value', '0');
+ range.setAttribute('id', `${optionName}||${newValue}`);
+ range.setAttribute('name', `${optionName}||${newValue}`);
+ tdMiddle.appendChild(range);
+ tr.appendChild(tdMiddle);
+ const tdRight = document.createElement('td');
+ tdRight.classList.add('td-right');
+ const valueSpan = document.createElement('span');
+ valueSpan.setAttribute('id', `${optionName}||${newValue}-value`);
+ valueSpan.innerText = '0';
+ tdRight.appendChild(valueSpan);
+ tr.appendChild(tdRight);
+ const tdDelete = document.createElement('td');
+ const deleteSpan = document.createElement('span');
+ deleteSpan.classList.add('range-option-delete');
+ deleteSpan.classList.add('js-required');
+ deleteSpan.setAttribute('data-target', `${optionName}-${newValue}-row`);
+ deleteSpan.innerText = 'âŒ';
+ tdDelete.appendChild(deleteSpan);
+ tr.appendChild(tdDelete);
+ tBody.appendChild(tr);
+
+ // Remove this option from the set of deleted options if it exists
+ unsetDeletedOption(optionName, newValue);
+};
+
+/**
+ * Determines the value of an input element, or returns a 1 or 0 if the element is a checkbox
+ *
+ * @param {object} input - The input element.
+ * @returns {number} The value of the input element.
+ */
+const determineValue = (input) => {
+ switch (input.type) {
+ case 'checkbox':
+ return (input.checked ? 1 : 0);
+ case 'range':
+ return parseInt(input.value, 10);
+ default:
+ return input.value;
+ }
+};
+
+/**
+ * Sets the deleted option value for a given world and option name.
+ * If the world or option does not exist, it creates the necessary entries.
+ *
+ * @param {string} optionName - The name of the option.
+ * @param {*} value - The value to be set for the deleted option.
+ * @returns {void}
+ */
+const setDeletedOption = (optionName, value) => {
+ deletedOptions[optionName] = deletedOptions[optionName] || [];
+ deletedOptions[optionName].push(`${optionName}-${value}`);
+};
+
+/**
+ * Removes a specific value from the deletedOptions object.
+ *
+ * @param {string} optionName - The name of the option.
+ * @param {*} value - The value to be removed
+ * @returns {void}
+ */
+const unsetDeletedOption = (optionName, value) => {
+ if (!deletedOptions.hasOwnProperty(optionName)) { return; }
+ if (deletedOptions[optionName].includes(`${optionName}-${value}`)) {
+ deletedOptions[optionName].splice(deletedOptions[optionName].indexOf(`${optionName}-${value}`), 1);
+ }
+ if (deletedOptions[optionName].length === 0) {
+ delete deletedOptions[optionName];
+ }
+};
diff --git a/WebHostLib/static/robots_file.txt b/WebHostLib/static/robots_file.txt
new file mode 100644
index 000000000000..770ae26c1985
--- /dev/null
+++ b/WebHostLib/static/robots_file.txt
@@ -0,0 +1,20 @@
+User-agent: Googlebot
+Disallow: /
+
+User-agent: APIs-Google
+Disallow: /
+
+User-agent: AdsBot-Google-Mobile
+Disallow: /
+
+User-agent: AdsBot-Google-Mobile
+Disallow: /
+
+User-agent: Mediapartners-Google
+Disallow: /
+
+User-agent: Google-Safety
+Disallow: /
+
+User-agent: *
+Disallow: /
diff --git a/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png b/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png
deleted file mode 100644
index 8fb366b93ff0..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png b/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png
deleted file mode 100644
index 336dc5f77af2..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/advanceballistics.png b/WebHostLib/static/static/icons/sc2/advanceballistics.png
deleted file mode 100644
index 1bf7df9fb74c..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/advanceballistics.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/autoturretblackops.png b/WebHostLib/static/static/icons/sc2/autoturretblackops.png
deleted file mode 100644
index 552707831a00..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/autoturretblackops.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/biomechanicaldrone.png b/WebHostLib/static/static/icons/sc2/biomechanicaldrone.png
deleted file mode 100644
index e7ebf4031619..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/biomechanicaldrone.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/burstcapacitors.png b/WebHostLib/static/static/icons/sc2/burstcapacitors.png
deleted file mode 100644
index 3af9b20a1698..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/burstcapacitors.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/crossspectrumdampeners.png b/WebHostLib/static/static/icons/sc2/crossspectrumdampeners.png
deleted file mode 100644
index d1c0c6c9a010..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/crossspectrumdampeners.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/cyclone.png b/WebHostLib/static/static/icons/sc2/cyclone.png
deleted file mode 100644
index d2016116ea3b..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/cyclone.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/cyclonerangeupgrade.png b/WebHostLib/static/static/icons/sc2/cyclonerangeupgrade.png
deleted file mode 100644
index 351be570d11b..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/cyclonerangeupgrade.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/drillingclaws.png b/WebHostLib/static/static/icons/sc2/drillingclaws.png
deleted file mode 100644
index 2b067a6e44d4..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/drillingclaws.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/emergencythrusters.png b/WebHostLib/static/static/icons/sc2/emergencythrusters.png
deleted file mode 100644
index 159fba37c903..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/emergencythrusters.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/hellionbattlemode.png b/WebHostLib/static/static/icons/sc2/hellionbattlemode.png
deleted file mode 100644
index 56bfd98c924c..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/hellionbattlemode.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/high-explosive-spidermine.png b/WebHostLib/static/static/icons/sc2/high-explosive-spidermine.png
deleted file mode 100644
index 40a5991ebb80..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/high-explosive-spidermine.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/hyperflightrotors.png b/WebHostLib/static/static/icons/sc2/hyperflightrotors.png
deleted file mode 100644
index 375325845876..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/hyperflightrotors.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/hyperfluxor.png b/WebHostLib/static/static/icons/sc2/hyperfluxor.png
deleted file mode 100644
index cdd95bb515be..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/hyperfluxor.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/impalerrounds.png b/WebHostLib/static/static/icons/sc2/impalerrounds.png
deleted file mode 100644
index b00e0c475827..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/impalerrounds.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/improvedburstlaser.png b/WebHostLib/static/static/icons/sc2/improvedburstlaser.png
deleted file mode 100644
index 8a48e38e874d..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/improvedburstlaser.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/improvedsiegemode.png b/WebHostLib/static/static/icons/sc2/improvedsiegemode.png
deleted file mode 100644
index f19dad952bb5..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/improvedsiegemode.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/interferencematrix.png b/WebHostLib/static/static/icons/sc2/interferencematrix.png
deleted file mode 100644
index ced928aa57a9..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/interferencematrix.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/internalizedtechmodule.png b/WebHostLib/static/static/icons/sc2/internalizedtechmodule.png
deleted file mode 100644
index e97f3db0d29a..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/internalizedtechmodule.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/jotunboosters.png b/WebHostLib/static/static/icons/sc2/jotunboosters.png
deleted file mode 100644
index 25720306e5c2..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/jotunboosters.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/jumpjets.png b/WebHostLib/static/static/icons/sc2/jumpjets.png
deleted file mode 100644
index dfdfef4052ca..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/jumpjets.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/lasertargetingsystem.png b/WebHostLib/static/static/icons/sc2/lasertargetingsystem.png
deleted file mode 100644
index c57899b270ff..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/lasertargetingsystem.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/liberator.png b/WebHostLib/static/static/icons/sc2/liberator.png
deleted file mode 100644
index 31507be5fe68..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/liberator.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/lockdown.png b/WebHostLib/static/static/icons/sc2/lockdown.png
deleted file mode 100644
index a2e7f5dc3e3f..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/lockdown.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/magfieldaccelerator.png b/WebHostLib/static/static/icons/sc2/magfieldaccelerator.png
deleted file mode 100644
index 0272b4b73892..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/magfieldaccelerator.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/magrailmunitions.png b/WebHostLib/static/static/icons/sc2/magrailmunitions.png
deleted file mode 100644
index ec303498ccdb..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/magrailmunitions.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/medivacemergencythrusters.png b/WebHostLib/static/static/icons/sc2/medivacemergencythrusters.png
deleted file mode 100644
index 1c7ce9d6ab1a..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/medivacemergencythrusters.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/neosteelfortifiedarmor.png b/WebHostLib/static/static/icons/sc2/neosteelfortifiedarmor.png
deleted file mode 100644
index 04d68d35dc46..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/neosteelfortifiedarmor.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/opticalflare.png b/WebHostLib/static/static/icons/sc2/opticalflare.png
deleted file mode 100644
index f888fd518b99..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/opticalflare.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/optimizedlogistics.png b/WebHostLib/static/static/icons/sc2/optimizedlogistics.png
deleted file mode 100644
index dcf5fd72da86..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/optimizedlogistics.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/reapercombatdrugs.png b/WebHostLib/static/static/icons/sc2/reapercombatdrugs.png
deleted file mode 100644
index b9f2f055c265..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/reapercombatdrugs.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/restoration.png b/WebHostLib/static/static/icons/sc2/restoration.png
deleted file mode 100644
index f5c94e1aeefd..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/restoration.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/ripwavemissiles.png b/WebHostLib/static/static/icons/sc2/ripwavemissiles.png
deleted file mode 100644
index f68e82039765..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/ripwavemissiles.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/shreddermissile.png b/WebHostLib/static/static/icons/sc2/shreddermissile.png
deleted file mode 100644
index 40899095fe3a..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/shreddermissile.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/siegetank-spidermines.png b/WebHostLib/static/static/icons/sc2/siegetank-spidermines.png
deleted file mode 100644
index 1b9f8cf06097..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/siegetank-spidermines.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/siegetankrange.png b/WebHostLib/static/static/icons/sc2/siegetankrange.png
deleted file mode 100644
index 5aef00a656c9..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/siegetankrange.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/specialordance.png b/WebHostLib/static/static/icons/sc2/specialordance.png
deleted file mode 100644
index 4f7410d7ca9e..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/specialordance.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/spidermine.png b/WebHostLib/static/static/icons/sc2/spidermine.png
deleted file mode 100644
index bb39cf0bf8ce..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/spidermine.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/staticempblast.png b/WebHostLib/static/static/icons/sc2/staticempblast.png
deleted file mode 100644
index 38f361510775..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/staticempblast.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/superstimpack.png b/WebHostLib/static/static/icons/sc2/superstimpack.png
deleted file mode 100644
index 0fba8ce5749a..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/superstimpack.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/targetingoptics.png b/WebHostLib/static/static/icons/sc2/targetingoptics.png
deleted file mode 100644
index 057a40f08e30..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/targetingoptics.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/terran-cloak-color.png b/WebHostLib/static/static/icons/sc2/terran-cloak-color.png
deleted file mode 100644
index 44d1bb9541fb..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/terran-cloak-color.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/terran-emp-color.png b/WebHostLib/static/static/icons/sc2/terran-emp-color.png
deleted file mode 100644
index 972b828c75e2..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/terran-emp-color.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/terrandefendermodestructureattack.png b/WebHostLib/static/static/icons/sc2/terrandefendermodestructureattack.png
deleted file mode 100644
index 9d5982655183..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/terrandefendermodestructureattack.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/thorsiegemode.png b/WebHostLib/static/static/icons/sc2/thorsiegemode.png
deleted file mode 100644
index a298fb57de5a..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/thorsiegemode.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/transformationservos.png b/WebHostLib/static/static/icons/sc2/transformationservos.png
deleted file mode 100644
index f7f0524ac15c..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/transformationservos.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/valkyrie.png b/WebHostLib/static/static/icons/sc2/valkyrie.png
deleted file mode 100644
index 9cbf339b10db..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/valkyrie.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/warpjump.png b/WebHostLib/static/static/icons/sc2/warpjump.png
deleted file mode 100644
index ff0a7b1af4aa..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/warpjump.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/widowmine-attackrange.png b/WebHostLib/static/static/icons/sc2/widowmine-attackrange.png
deleted file mode 100644
index 8f5e09c6a593..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/widowmine-attackrange.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/widowmine-deathblossom.png b/WebHostLib/static/static/icons/sc2/widowmine-deathblossom.png
deleted file mode 100644
index 7097db05e6c0..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/widowmine-deathblossom.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/widowmine.png b/WebHostLib/static/static/icons/sc2/widowmine.png
deleted file mode 100644
index 802c49a83d88..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/widowmine.png and /dev/null differ
diff --git a/WebHostLib/static/static/icons/sc2/widowminehidden.png b/WebHostLib/static/static/icons/sc2/widowminehidden.png
deleted file mode 100644
index e568742e8a50..000000000000
Binary files a/WebHostLib/static/static/icons/sc2/widowminehidden.png and /dev/null differ
diff --git a/WebHostLib/static/styles/globalStyles.css b/WebHostLib/static/styles/globalStyles.css
index a787b0c6570a..1a0144830e7c 100644
--- a/WebHostLib/static/styles/globalStyles.css
+++ b/WebHostLib/static/styles/globalStyles.css
@@ -44,7 +44,7 @@ a{
font-family: LexendDeca-Regular, sans-serif;
}
-button{
+button, input[type=submit]{
font-weight: 500;
font-size: 0.9rem;
padding: 10px 17px 11px 16px; /* top right bottom left */
@@ -57,7 +57,7 @@ button{
cursor: pointer;
}
-button:active{
+button:active, input[type=submit]:active{
border-right: 1px solid rgba(0, 0, 0, 0.5);
border-bottom: 1px solid rgba(0, 0, 0, 0.5);
padding-right: 16px;
@@ -66,11 +66,11 @@ button:active{
margin-bottom: 2px;
}
-button.button-grass{
+button.button-grass, input[type=submit].button-grass{
border: 1px solid black;
}
-button.button-dirt{
+button.button-dirt, input[type=submit].button-dirt{
border: 1px solid black;
}
@@ -111,4 +111,4 @@ h5, h6{
.interactive{
color: #ffef00;
-}
\ No newline at end of file
+}
diff --git a/WebHostLib/static/styles/landing.css b/WebHostLib/static/styles/landing.css
index 202c43badd5f..96975553c142 100644
--- a/WebHostLib/static/styles/landing.css
+++ b/WebHostLib/static/styles/landing.css
@@ -235,9 +235,6 @@ html{
line-height: 30px;
}
-#landing .variable{
- color: #ffff00;
-}
.landing-deco{
position: absolute;
diff --git a/WebHostLib/static/styles/lttp-tracker.css b/WebHostLib/static/styles/lttp-tracker.css
deleted file mode 100644
index 899a8f695925..000000000000
--- a/WebHostLib/static/styles/lttp-tracker.css
+++ /dev/null
@@ -1,75 +0,0 @@
-#player-tracker-wrapper{
- margin: 0;
- font-family: LexendDeca-Light, sans-serif;
- color: white;
- font-size: 14px;
-}
-
-#inventory-table{
- border-top: 2px solid #000000;
- border-left: 2px solid #000000;
- border-right: 2px solid #000000;
- border-top-left-radius: 4px;
- border-top-right-radius: 4px;
- padding: 3px 3px 10px;
- width: 284px;
- background-color: #42b149;
-}
-
-#inventory-table td{
- width: 40px;
- height: 40px;
- text-align: center;
- vertical-align: middle;
-}
-
-#inventory-table img{
- height: 100%;
- max-width: 40px;
- max-height: 40px;
- filter: grayscale(100%) contrast(75%) brightness(75%);
-}
-
-#inventory-table img.acquired{
- filter: none;
-}
-
-#inventory-table img.powder-fix{
- width: 35px;
- height: 35px;
-}
-
-#location-table{
- width: 284px;
- border-left: 2px solid #000000;
- border-right: 2px solid #000000;
- border-bottom: 2px solid #000000;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
- background-color: #42b149;
- padding: 0 3px 3px;
-}
-
-#location-table th{
- vertical-align: middle;
- text-align: center;
- padding-right: 10px;
-}
-
-#location-table td{
- padding-top: 2px;
- padding-bottom: 2px;
- padding-right: 5px;
- line-height: 20px;
-}
-
-#location-table td.counter{
- padding-right: 8px;
- text-align: right;
-}
-
-#location-table img{
- height: 100%;
- max-width: 30px;
- max-height: 30px;
-}
diff --git a/WebHostLib/static/styles/markdown.css b/WebHostLib/static/styles/markdown.css
index dce135588e5f..e0165b7489ef 100644
--- a/WebHostLib/static/styles/markdown.css
+++ b/WebHostLib/static/styles/markdown.css
@@ -23,7 +23,7 @@
.markdown a{}
-.markdown h1{
+.markdown h1, .markdown details summary.h1{
font-size: 52px;
font-weight: normal;
font-family: LondrinaSolid-Regular, sans-serif;
@@ -33,7 +33,7 @@
text-shadow: 1px 1px 4px #000000;
}
-.markdown h2{
+.markdown h2, .markdown details summary.h2{
font-size: 38px;
font-weight: normal;
font-family: LondrinaSolid-Light, sans-serif;
@@ -45,7 +45,7 @@
text-shadow: 1px 1px 2px #000000;
}
-.markdown h3{
+.markdown h3, .markdown details summary.h3{
font-size: 26px;
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
@@ -55,7 +55,7 @@
margin-bottom: 0.5rem;
}
-.markdown h4{
+.markdown h4, .markdown details summary.h4{
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
font-size: 24px;
@@ -63,21 +63,21 @@
margin-bottom: 24px;
}
-.markdown h5{
+.markdown h5, .markdown details summary.h5{
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
font-size: 22px;
cursor: pointer;
}
-.markdown h6{
+.markdown h6, .markdown details summary.h6{
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
font-size: 20px;
cursor: pointer;;
}
-.markdown h4, .markdown h5,.markdown h6{
+.markdown h4, .markdown h5, .markdown h6{
margin-bottom: 0.5rem;
}
diff --git a/WebHostLib/static/styles/player-settings.css b/WebHostLib/static/styles/player-settings.css
deleted file mode 100644
index e6e0c292922a..000000000000
--- a/WebHostLib/static/styles/player-settings.css
+++ /dev/null
@@ -1,213 +0,0 @@
-html{
- background-image: url('../static/backgrounds/grass.png');
- background-repeat: repeat;
- background-size: 650px 650px;
-}
-
-#player-settings{
- box-sizing: border-box;
- max-width: 1024px;
- margin-left: auto;
- margin-right: auto;
- background-color: rgba(0, 0, 0, 0.15);
- border-radius: 8px;
- padding: 1rem;
- color: #eeffeb;
-}
-
-#player-settings #player-settings-button-row{
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- margin-top: 15px;
-}
-
-#player-settings code{
- background-color: #d9cd8e;
- border-radius: 4px;
- padding-left: 0.25rem;
- padding-right: 0.25rem;
- color: #000000;
-}
-
-#player-settings #user-message{
- display: none;
- width: calc(100% - 8px);
- background-color: #ffe86b;
- border-radius: 4px;
- color: #000000;
- padding: 4px;
- text-align: center;
-}
-
-#player-settings #user-message.visible{
- display: block;
- cursor: pointer;
-}
-
-#player-settings h1{
- font-size: 2.5rem;
- font-weight: normal;
- width: 100%;
- margin-bottom: 0.5rem;
- text-shadow: 1px 1px 4px #000000;
-}
-
-#player-settings h2{
- font-size: 40px;
- font-weight: normal;
- width: 100%;
- margin-bottom: 0.5rem;
- text-transform: lowercase;
- text-shadow: 1px 1px 2px #000000;
-}
-
-#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{
- text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
-}
-
-#player-settings input:not([type]){
- border: 1px solid #000000;
- padding: 3px;
- border-radius: 3px;
- min-width: 150px;
-}
-
-#player-settings input:not([type]):focus{
- border: 1px solid #ffffff;
-}
-
-#player-settings select{
- border: 1px solid #000000;
- padding: 3px;
- border-radius: 3px;
- min-width: 150px;
- background-color: #ffffff;
-}
-
-#player-settings #game-options, #player-settings #rom-options{
- display: flex;
- flex-direction: row;
-}
-
-#player-settings .left, #player-settings .right{
- flex-grow: 1;
-}
-
-#player-settings .left{
- margin-right: 10px;
-}
-
-#player-settings .right{
- margin-left: 10px;
-}
-
-#player-settings table{
- margin-bottom: 30px;
- width: 100%;
-}
-
-#player-settings table .select-container{
- display: flex;
- flex-direction: row;
-}
-
-#player-settings table .select-container select{
- min-width: 200px;
- flex-grow: 1;
-}
-
-#player-settings table select:disabled{
- background-color: lightgray;
-}
-
-#player-settings table .range-container{
- display: flex;
- flex-direction: row;
-}
-
-#player-settings table .range-container input[type=range]{
- flex-grow: 1;
-}
-
-#player-settings table .range-value{
- min-width: 20px;
- margin-left: 0.25rem;
-}
-
-#player-settings table .special-range-container{
- display: flex;
- flex-direction: column;
-}
-
-#player-settings table .special-range-wrapper{
- display: flex;
- flex-direction: row;
- margin-top: 0.25rem;
-}
-
-#player-settings table .special-range-wrapper input[type=range]{
- flex-grow: 1;
-}
-
-#player-settings table .randomize-button {
- max-height: 24px;
- line-height: 16px;
- padding: 2px 8px;
- margin: 0 0 0 0.25rem;
- font-size: 12px;
- border: 1px solid black;
- border-radius: 3px;
-}
-
-#player-settings table .randomize-button.active {
- background-color: #ffef00; /* Same as .interactive in globalStyles.css */
-}
-
-#player-settings table .randomize-button[data-tooltip]::after {
- left: unset;
- right: 0;
-}
-
-#player-settings table label{
- display: block;
- min-width: 200px;
- margin-right: 4px;
- cursor: default;
-}
-
-#player-settings th, #player-settings td{
- border: none;
- padding: 3px;
- font-size: 17px;
- vertical-align: top;
-}
-
-@media all and (max-width: 1024px) {
- #player-settings {
- border-radius: 0;
- }
-
- #player-settings #game-options{
- justify-content: flex-start;
- flex-wrap: wrap;
- }
-
- #player-settings .left,
- #player-settings .right {
- margin: 0;
- }
-
- #game-options table {
- margin-bottom: 0;
- }
-
- #game-options table label{
- display: block;
- min-width: 200px;
- }
-
- #game-options table tr td {
- width: 50%;
- }
-}
diff --git a/WebHostLib/static/styles/playerOptions/playerOptions.css b/WebHostLib/static/styles/playerOptions/playerOptions.css
new file mode 100644
index 000000000000..56c9263d3330
--- /dev/null
+++ b/WebHostLib/static/styles/playerOptions/playerOptions.css
@@ -0,0 +1,310 @@
+@import "../markdown.css";
+html {
+ background-image: url("../../static/backgrounds/grass.png");
+ background-repeat: repeat;
+ background-size: 650px 650px;
+ overflow-x: hidden;
+}
+
+#player-options {
+ box-sizing: border-box;
+ max-width: 1024px;
+ margin-left: auto;
+ margin-right: auto;
+ background-color: rgba(0, 0, 0, 0.15);
+ border-radius: 8px;
+ padding: 1rem;
+ color: #eeffeb;
+ word-break: break-word;
+}
+#player-options #player-options-header h1 {
+ margin-bottom: 0;
+ padding-bottom: 0;
+}
+#player-options #player-options-header h1:nth-child(2) {
+ font-size: 1.4rem;
+ margin-top: -8px;
+ margin-bottom: 0.5rem;
+}
+#player-options .js-warning-banner {
+ width: calc(100% - 1rem);
+ padding: 0.5rem;
+ border-radius: 4px;
+ background-color: #f3f309;
+ color: #000000;
+ margin-bottom: 0.5rem;
+ text-align: center;
+}
+#player-options .group-container {
+ padding: 0;
+ margin: 0;
+}
+#player-options .group-container h2 {
+ user-select: none;
+ cursor: unset;
+}
+#player-options .group-container h2 label {
+ cursor: pointer;
+}
+#player-options #player-options-button-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ margin-top: 15px;
+}
+#player-options #user-message {
+ display: none;
+ width: calc(100% - 8px);
+ background-color: #ffe86b;
+ border-radius: 4px;
+ color: #000000;
+ padding: 4px;
+ text-align: center;
+ cursor: pointer;
+}
+#player-options h1 {
+ font-size: 2.5rem;
+ font-weight: normal;
+ width: 100%;
+ margin-bottom: 0.5rem;
+ text-shadow: 1px 1px 4px #000000;
+}
+#player-options h2 {
+ font-size: 40px;
+ font-weight: normal;
+ width: 100%;
+ margin-bottom: 0.5rem;
+ text-transform: lowercase;
+ text-shadow: 1px 1px 2px #000000;
+}
+#player-options h3, #player-options h4, #player-options h5, #player-options h6 {
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
+}
+#player-options input:not([type]) {
+ border: 1px solid #000000;
+ padding: 3px;
+ border-radius: 3px;
+ min-width: 150px;
+}
+#player-options input:not([type]):focus {
+ border: 1px solid #ffffff;
+}
+#player-options select {
+ border: 1px solid #000000;
+ padding: 3px;
+ border-radius: 3px;
+ min-width: 150px;
+ background-color: #ffffff;
+ text-overflow: ellipsis;
+}
+#player-options .game-options {
+ display: flex;
+ flex-direction: row;
+}
+#player-options .game-options .left, #player-options .game-options .right {
+ display: grid;
+ grid-template-columns: 12rem auto;
+ grid-row-gap: 0.5rem;
+ grid-auto-rows: min-content;
+ align-items: start;
+ min-width: 480px;
+ width: 50%;
+}
+#player-options #meta-options {
+ display: flex;
+ justify-content: space-between;
+ gap: 20px;
+ padding: 3px;
+}
+#player-options #meta-options input, #player-options #meta-options select {
+ box-sizing: border-box;
+ width: 200px;
+}
+#player-options .left, #player-options .right {
+ flex-grow: 1;
+ margin-bottom: 0.5rem;
+}
+#player-options .left {
+ margin-right: 20px;
+}
+#player-options .select-container {
+ display: flex;
+ flex-direction: row;
+ max-width: 270px;
+}
+#player-options .select-container select {
+ min-width: 200px;
+ flex-grow: 1;
+}
+#player-options .select-container select:disabled {
+ background-color: lightgray;
+}
+#player-options .range-container {
+ display: flex;
+ flex-direction: row;
+ max-width: 270px;
+}
+#player-options .range-container input[type=range] {
+ flex-grow: 1;
+}
+#player-options .range-container .range-value {
+ min-width: 20px;
+ margin-left: 0.25rem;
+}
+#player-options .named-range-container {
+ display: flex;
+ flex-direction: column;
+ max-width: 270px;
+}
+#player-options .named-range-container .named-range-wrapper {
+ display: flex;
+ flex-direction: row;
+ margin-top: 0.25rem;
+}
+#player-options .named-range-container .named-range-wrapper input[type=range] {
+ flex-grow: 1;
+}
+#player-options .free-text-container {
+ display: flex;
+ flex-direction: column;
+ max-width: 270px;
+}
+#player-options .free-text-container input[type=text] {
+ flex-grow: 1;
+}
+#player-options .text-choice-container {
+ display: flex;
+ flex-direction: column;
+ max-width: 270px;
+}
+#player-options .text-choice-container .text-choice-wrapper {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 0.25rem;
+}
+#player-options .text-choice-container .text-choice-wrapper select {
+ flex-grow: 1;
+}
+#player-options .option-container {
+ display: flex;
+ flex-direction: column;
+ background-color: rgba(0, 0, 0, 0.25);
+ border: 1px solid rgba(20, 20, 20, 0.25);
+ border-radius: 3px;
+ color: #ffffff;
+ max-height: 10rem;
+ min-width: 14.5rem;
+ overflow-y: auto;
+ padding-right: 0.25rem;
+ padding-left: 0.25rem;
+}
+#player-options .option-container .option-divider {
+ width: 100%;
+ height: 2px;
+ background-color: rgba(20, 20, 20, 0.25);
+ margin-top: 0.125rem;
+ margin-bottom: 0.125rem;
+}
+#player-options .option-container .option-entry {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ margin-bottom: 0.125rem;
+ margin-top: 0.125rem;
+ user-select: none;
+}
+#player-options .option-container .option-entry:hover {
+ background-color: rgba(20, 20, 20, 0.25);
+}
+#player-options .option-container .option-entry input[type=checkbox] {
+ margin-right: 0.25rem;
+}
+#player-options .option-container .option-entry input[type=number] {
+ max-width: 1.5rem;
+ max-height: 1rem;
+ margin-left: 0.125rem;
+ text-align: center;
+ /* Hide arrows on input[type=number] fields */
+ -moz-appearance: textfield;
+}
+#player-options .option-container .option-entry input[type=number]::-webkit-outer-spin-button, #player-options .option-container .option-entry input[type=number]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+#player-options .option-container .option-entry label {
+ flex-grow: 1;
+ margin-right: 0;
+ min-width: unset;
+ display: unset;
+}
+#player-options .randomize-button {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ height: 22px;
+ max-width: 30px;
+ margin: 0 0 0 0.25rem;
+ font-size: 14px;
+ border: 1px solid black;
+ border-radius: 3px;
+ background-color: #d3d3d3;
+ user-select: none;
+}
+#player-options .randomize-button:hover {
+ background-color: #c0c0c0;
+ cursor: pointer;
+}
+#player-options .randomize-button label {
+ line-height: 22px;
+ padding-left: 5px;
+ padding-right: 2px;
+ margin-right: 4px;
+ width: 100%;
+ height: 100%;
+ min-width: unset;
+}
+#player-options .randomize-button label:hover {
+ cursor: pointer;
+}
+#player-options .randomize-button input[type=checkbox] {
+ display: none;
+}
+#player-options .randomize-button:has(input[type=checkbox]:checked) {
+ background-color: #ffef00; /* Same as .interactive in globalStyles.css */
+}
+#player-options .randomize-button:has(input[type=checkbox]:checked):hover {
+ background-color: #eedd27;
+}
+#player-options .randomize-button[data-tooltip]::after {
+ left: unset;
+ right: 0;
+}
+#player-options label {
+ display: block;
+ margin-right: 4px;
+ cursor: default;
+ word-break: break-word;
+}
+#player-options th, #player-options td {
+ border: none;
+ padding: 3px;
+ font-size: 17px;
+ vertical-align: top;
+}
+
+@media all and (max-width: 1024px) {
+ #player-options {
+ border-radius: 0;
+ }
+ #player-options #meta-options {
+ flex-direction: column;
+ justify-content: flex-start;
+ gap: 6px;
+ }
+ #player-options .game-options {
+ justify-content: flex-start;
+ flex-wrap: wrap;
+ }
+}
+
+/*# sourceMappingURL=playerOptions.css.map */
diff --git a/WebHostLib/static/styles/playerOptions/playerOptions.css.map b/WebHostLib/static/styles/playerOptions/playerOptions.css.map
new file mode 100644
index 000000000000..6797b88c7bfe
--- /dev/null
+++ b/WebHostLib/static/styles/playerOptions/playerOptions.css.map
@@ -0,0 +1 @@
+{"version":3,"sourceRoot":"","sources":["playerOptions.scss"],"names":[],"mappings":"AAAQ;AAER;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAEA;EACI;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;AAEA;EACA;;AACA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACI;;AAIR;EACI;;AAGJ;EACI;;AAEA;EACI;;AAIR;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;;AAIR;EACI;IACI;;EAEA;IACI;IACA;IACA;;EAGJ;IACI;IACA","file":"playerOptions.css"}
\ No newline at end of file
diff --git a/WebHostLib/static/styles/playerOptions/playerOptions.scss b/WebHostLib/static/styles/playerOptions/playerOptions.scss
new file mode 100644
index 000000000000..06bde759d263
--- /dev/null
+++ b/WebHostLib/static/styles/playerOptions/playerOptions.scss
@@ -0,0 +1,364 @@
+@import "../markdown.css";
+
+html{
+ background-image: url('../../static/backgrounds/grass.png');
+ background-repeat: repeat;
+ background-size: 650px 650px;
+ overflow-x: hidden;
+}
+
+#player-options{
+ box-sizing: border-box;
+ max-width: 1024px;
+ margin-left: auto;
+ margin-right: auto;
+ background-color: rgba(0, 0, 0, 0.15);
+ border-radius: 8px;
+ padding: 1rem;
+ color: #eeffeb;
+ word-break: break-word;
+
+ #player-options-header{
+ h1{
+ margin-bottom: 0;
+ padding-bottom: 0;
+ }
+
+ h1:nth-child(2){
+ font-size: 1.4rem;
+ margin-top: -8px;
+ margin-bottom: 0.5rem;
+ }
+ }
+
+ .js-warning-banner{
+ width: calc(100% - 1rem);
+ padding: 0.5rem;
+ border-radius: 4px;
+ background-color: #f3f309;
+ color: #000000;
+ margin-bottom: 0.5rem;
+ text-align: center;
+ }
+
+ .group-container{
+ padding: 0;
+ margin: 0;
+
+ h2{
+ user-select: none;
+ cursor: unset;
+
+ label{
+ cursor: pointer;
+ }
+ }
+ }
+
+ #player-options-button-row{
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ margin-top: 15px;
+ }
+
+ #user-message{
+ display: none;
+ width: calc(100% - 8px);
+ background-color: #ffe86b;
+ border-radius: 4px;
+ color: #000000;
+ padding: 4px;
+ text-align: center;
+ cursor: pointer;
+ }
+
+ h1{
+ font-size: 2.5rem;
+ font-weight: normal;
+ width: 100%;
+ margin-bottom: 0.5rem;
+ text-shadow: 1px 1px 4px #000000;
+ }
+
+ h2{
+ font-size: 40px;
+ font-weight: normal;
+ width: 100%;
+ margin-bottom: 0.5rem;
+ text-transform: lowercase;
+ text-shadow: 1px 1px 2px #000000;
+ }
+
+ h3, h4, h5, h6{
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
+ }
+
+ input:not([type]){
+ border: 1px solid #000000;
+ padding: 3px;
+ border-radius: 3px;
+ min-width: 150px;
+
+ &:focus{
+ border: 1px solid #ffffff;
+ }
+ }
+
+ select{
+ border: 1px solid #000000;
+ padding: 3px;
+ border-radius: 3px;
+ min-width: 150px;
+ background-color: #ffffff;
+ text-overflow: ellipsis;
+ }
+
+ .game-options{
+ display: flex;
+ flex-direction: row;
+
+ .left, .right{
+ display: grid;
+ grid-template-columns: 12rem auto;
+ grid-row-gap: 0.5rem;
+ grid-auto-rows: min-content;
+ align-items: start;
+ min-width: 480px;
+ width: 50%;
+ }
+ }
+
+ #meta-options{
+ display: flex;
+ justify-content: space-between;
+ gap: 20px;
+ padding: 3px;
+
+ input, select{
+ box-sizing: border-box;
+ width: 200px;
+ }
+ }
+
+ .left, .right{
+ flex-grow: 1;
+ margin-bottom: 0.5rem;
+ }
+
+ .left{
+ margin-right: 20px;
+ }
+
+ .select-container{
+ display: flex;
+ flex-direction: row;
+ max-width: 270px;
+
+ select{
+ min-width: 200px;
+ flex-grow: 1;
+
+ &:disabled{
+ background-color: lightgray;
+ }
+ }
+ }
+
+ .range-container{
+ display: flex;
+ flex-direction: row;
+ max-width: 270px;
+
+ input[type=range]{
+ flex-grow: 1;
+ }
+
+ .range-value{
+ min-width: 20px;
+ margin-left: 0.25rem;
+ }
+ }
+
+ .named-range-container{
+ display: flex;
+ flex-direction: column;
+ max-width: 270px;
+
+ .named-range-wrapper{
+ display: flex;
+ flex-direction: row;
+ margin-top: 0.25rem;
+
+ input[type=range]{
+ flex-grow: 1;
+ }
+ }
+ }
+
+ .free-text-container{
+ display: flex;
+ flex-direction: column;
+ max-width: 270px;
+
+ input[type=text]{
+ flex-grow: 1;
+ }
+ }
+
+ .text-choice-container{
+ display: flex;
+ flex-direction: column;
+ max-width: 270px;
+
+ .text-choice-wrapper{
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 0.25rem;
+
+ select{
+ flex-grow: 1;
+ }
+ }
+ }
+
+ .option-container{
+ display: flex;
+ flex-direction: column;
+ background-color: rgba(0, 0, 0, 0.25);
+ border: 1px solid rgba(20, 20, 20, 0.25);
+ border-radius: 3px;
+ color: #ffffff;
+ max-height: 10rem;
+ min-width: 14.5rem;
+ overflow-y: auto;
+ padding-right: 0.25rem;
+ padding-left: 0.25rem;
+
+ .option-divider{
+ width: 100%;
+ height: 2px;
+ background-color: rgba(20, 20, 20, 0.25);
+ margin-top: 0.125rem;
+ margin-bottom: 0.125rem;
+ }
+
+ .option-entry{
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ margin-bottom: 0.125rem;
+ margin-top: 0.125rem;
+ user-select: none;
+
+ &:hover{
+ background-color: rgba(20, 20, 20, 0.25);
+ }
+
+ input[type=checkbox]{
+ margin-right: 0.25rem;
+ }
+
+ input[type=number]{
+ max-width: 1.5rem;
+ max-height: 1rem;
+ margin-left: 0.125rem;
+ text-align: center;
+
+ /* Hide arrows on input[type=number] fields */
+ -moz-appearance: textfield;
+ &::-webkit-outer-spin-button, &::-webkit-inner-spin-button{
+ -webkit-appearance: none;
+ margin: 0;
+ }
+ }
+
+ label{
+ flex-grow: 1;
+ margin-right: 0;
+ min-width: unset;
+ display: unset;
+ }
+ }
+ }
+
+ .randomize-button{
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ height: 22px;
+ max-width: 30px;
+ margin: 0 0 0 0.25rem;
+ font-size: 14px;
+ border: 1px solid black;
+ border-radius: 3px;
+ background-color: #d3d3d3;
+ user-select: none;
+
+ &:hover{
+ background-color: #c0c0c0;
+ cursor: pointer;
+ }
+
+ label{
+ line-height: 22px;
+ padding-left: 5px;
+ padding-right: 2px;
+ margin-right: 4px;
+ width: 100%;
+ height: 100%;
+ min-width: unset;
+ &:hover{
+ cursor: pointer;
+ }
+ }
+
+ input[type=checkbox]{
+ display: none;
+ }
+
+ &:has(input[type=checkbox]:checked){
+ background-color: #ffef00; /* Same as .interactive in globalStyles.css */
+
+ &:hover{
+ background-color: #eedd27;
+ }
+ }
+
+ &[data-tooltip]::after{
+ left: unset;
+ right: 0;
+ }
+ }
+
+ label{
+ display: block;
+ margin-right: 4px;
+ cursor: default;
+ word-break: break-word;
+ }
+
+ th, td{
+ border: none;
+ padding: 3px;
+ font-size: 17px;
+ vertical-align: top;
+ }
+}
+
+@media all and (max-width: 1024px) {
+ #player-options {
+ border-radius: 0;
+
+ #meta-options {
+ flex-direction: column;
+ justify-content: flex-start;
+ gap: 6px;
+ }
+
+ .game-options{
+ justify-content: flex-start;
+ flex-wrap: wrap;
+ }
+ }
+}
diff --git a/WebHostLib/static/styles/sc2Tracker.css b/WebHostLib/static/styles/sc2Tracker.css
new file mode 100644
index 000000000000..29a719a110c8
--- /dev/null
+++ b/WebHostLib/static/styles/sc2Tracker.css
@@ -0,0 +1,160 @@
+#player-tracker-wrapper{
+ margin: 0;
+}
+
+#tracker-table td {
+ vertical-align: top;
+}
+
+.inventory-table-area{
+ border: 2px solid #000000;
+ border-radius: 4px;
+ padding: 3px 10px 3px 10px;
+}
+
+.inventory-table-area:has(.inventory-table-terran) {
+ width: 690px;
+ background-color: #525494;
+}
+
+.inventory-table-area:has(.inventory-table-zerg) {
+ width: 360px;
+ background-color: #9d60d2;
+}
+
+.inventory-table-area:has(.inventory-table-protoss) {
+ width: 400px;
+ background-color: #d2b260;
+}
+
+#tracker-table .inventory-table td{
+ width: 40px;
+ height: 40px;
+ text-align: center;
+ vertical-align: middle;
+}
+
+.inventory-table td.title{
+ padding-top: 10px;
+ height: 20px;
+ font-family: "JuraBook", monospace;
+ font-size: 16px;
+ font-weight: bold;
+}
+
+.inventory-table img{
+ height: 100%;
+ max-width: 40px;
+ max-height: 40px;
+ border: 1px solid #000000;
+ filter: grayscale(100%) contrast(75%) brightness(20%);
+ background-color: black;
+}
+
+.inventory-table img.acquired{
+ filter: none;
+ background-color: black;
+}
+
+.inventory-table .tint-terran img.acquired {
+ filter: sepia(100%) saturate(300%) brightness(130%) hue-rotate(120deg)
+}
+
+.inventory-table .tint-protoss img.acquired {
+ filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(180deg)
+}
+
+.inventory-table .tint-level-1 img.acquired {
+ filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg)
+}
+
+.inventory-table .tint-level-2 img.acquired {
+ filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg) hue-rotate(120deg)
+}
+
+.inventory-table .tint-level-3 img.acquired {
+ filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg) hue-rotate(240deg)
+}
+
+.inventory-table div.counted-item {
+ position: relative;
+}
+
+.inventory-table div.item-count {
+ width: 160px;
+ text-align: left;
+ color: black;
+ font-family: "JuraBook", monospace;
+ font-weight: bold;
+}
+
+#location-table{
+ border: 2px solid #000000;
+ border-radius: 4px;
+ background-color: #87b678;
+ padding: 10px 3px 3px;
+ font-family: "JuraBook", monospace;
+ font-size: 16px;
+ font-weight: bold;
+ cursor: default;
+}
+
+#location-table table{
+ width: 100%;
+}
+
+#location-table th{
+ vertical-align: middle;
+ text-align: left;
+ padding-right: 10px;
+}
+
+#location-table td{
+ padding-top: 2px;
+ padding-bottom: 2px;
+ line-height: 20px;
+}
+
+#location-table td.counter {
+ text-align: right;
+ font-size: 14px;
+}
+
+#location-table td.toggle-arrow {
+ text-align: right;
+}
+
+#location-table tr#Total-header {
+ font-weight: bold;
+}
+
+#location-table img{
+ height: 100%;
+ max-width: 30px;
+ max-height: 30px;
+}
+
+#location-table tbody.locations {
+ font-size: 16px;
+}
+
+#location-table td.location-name {
+ padding-left: 16px;
+}
+
+#location-table td:has(.location-column) {
+ vertical-align: top;
+}
+
+#location-table .location-column {
+ width: 100%;
+ height: 100%;
+}
+
+#location-table .location-column .spacer {
+ min-height: 24px;
+}
+
+.hide {
+ display: none;
+}
diff --git a/WebHostLib/static/styles/sc2wolTracker.css b/WebHostLib/static/styles/sc2wolTracker.css
deleted file mode 100644
index a7d8bd28c4f8..000000000000
--- a/WebHostLib/static/styles/sc2wolTracker.css
+++ /dev/null
@@ -1,112 +0,0 @@
-#player-tracker-wrapper{
- margin: 0;
-}
-
-#inventory-table{
- border-top: 2px solid #000000;
- border-left: 2px solid #000000;
- border-right: 2px solid #000000;
- border-top-left-radius: 4px;
- border-top-right-radius: 4px;
- padding: 3px 3px 10px;
- width: 710px;
- background-color: #525494;
-}
-
-#inventory-table td{
- width: 40px;
- height: 40px;
- text-align: center;
- vertical-align: middle;
-}
-
-#inventory-table td.title{
- padding-top: 10px;
- height: 20px;
- font-family: "JuraBook", monospace;
- font-size: 16px;
- font-weight: bold;
-}
-
-#inventory-table img{
- height: 100%;
- max-width: 40px;
- max-height: 40px;
- border: 1px solid #000000;
- filter: grayscale(100%) contrast(75%) brightness(20%);
- background-color: black;
-}
-
-#inventory-table img.acquired{
- filter: none;
- background-color: black;
-}
-
-#inventory-table div.counted-item {
- position: relative;
-}
-
-#inventory-table div.item-count {
- text-align: left;
- color: black;
- font-family: "JuraBook", monospace;
- font-weight: bold;
-}
-
-#location-table{
- width: 710px;
- border-left: 2px solid #000000;
- border-right: 2px solid #000000;
- border-bottom: 2px solid #000000;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
- background-color: #525494;
- padding: 10px 3px 3px;
- font-family: "JuraBook", monospace;
- font-size: 16px;
- font-weight: bold;
- cursor: default;
-}
-
-#location-table th{
- vertical-align: middle;
- text-align: left;
- padding-right: 10px;
-}
-
-#location-table td{
- padding-top: 2px;
- padding-bottom: 2px;
- line-height: 20px;
-}
-
-#location-table td.counter {
- text-align: right;
- font-size: 14px;
-}
-
-#location-table td.toggle-arrow {
- text-align: right;
-}
-
-#location-table tr#Total-header {
- font-weight: bold;
-}
-
-#location-table img{
- height: 100%;
- max-width: 30px;
- max-height: 30px;
-}
-
-#location-table tbody.locations {
- font-size: 16px;
-}
-
-#location-table td.location-name {
- padding-left: 16px;
-}
-
-.hide {
- display: none;
-}
diff --git a/WebHostLib/static/styles/supportedGames.css b/WebHostLib/static/styles/supportedGames.css
index f86ab581ca47..ab12f320716b 100644
--- a/WebHostLib/static/styles/supportedGames.css
+++ b/WebHostLib/static/styles/supportedGames.css
@@ -8,14 +8,15 @@
cursor: unset;
}
-#games h1{
+#games h1, #games details summary.h1{
font-size: 60px;
cursor: unset;
}
-#games h2{
+#games h2, #games details summary.h2{
color: #93dcff;
margin-bottom: 2px;
+ text-transform: none;
}
#games a{
@@ -31,3 +32,13 @@
line-height: 25px;
margin-bottom: 7px;
}
+
+#games .page-controls{
+ display: flex;
+ flex-direction: row;
+ margin-top: 0.25rem;
+}
+
+#games .page-controls button{
+ margin-left: 0.5rem;
+}
diff --git a/WebHostLib/static/styles/tooltip.css b/WebHostLib/static/styles/tooltip.css
index 7cd8463f64a4..dc9026ce6c3d 100644
--- a/WebHostLib/static/styles/tooltip.css
+++ b/WebHostLib/static/styles/tooltip.css
@@ -12,12 +12,12 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
*/
/* Base styles for the element that has a tooltip */
-[data-tooltip], .tooltip {
+[data-tooltip], .tooltip-container {
position: relative;
}
/* Base styles for the entire tooltip */
-[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after {
+[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip {
position: absolute;
visibility: hidden;
opacity: 0;
@@ -39,13 +39,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
pointer-events: none;
}
-[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after{
+[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before,
+.tooltip-container:hover .tooltip {
visibility: visible;
opacity: 1;
+ word-break: break-word;
}
/** Directional arrow styles */
-.tooltip:before, [data-tooltip]:before {
+[data-tooltip]:before, .tooltip-container:before {
z-index: 10000;
border: 6px solid transparent;
background: transparent;
@@ -53,7 +55,7 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
}
/** Content styles */
-.tooltip:after, [data-tooltip]:after {
+[data-tooltip]:after, .tooltip {
width: 260px;
z-index: 10000;
padding: 8px;
@@ -62,24 +64,26 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
background-color: hsla(0, 0%, 20%, 0.9);
color: #fff;
content: attr(data-tooltip);
- white-space: pre-wrap;
font-size: 14px;
line-height: 1.2;
}
-[data-tooltip]:before, [data-tooltip]:after{
+[data-tooltip]:after {
+ white-space: pre-wrap;
+}
+
+[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip {
visibility: hidden;
opacity: 0;
pointer-events: none;
}
-[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after,
-.tooltip-top:before, .tooltip-top:after {
+[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip {
bottom: 100%;
left: 50%;
}
-[data-tooltip]:before, .tooltip:before, .tooltip-top:before {
+[data-tooltip]:before, .tooltip-container:before {
margin-left: -6px;
margin-bottom: -12px;
border-top-color: #000;
@@ -87,19 +91,19 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
}
/** Horizontally align tooltips on the top and bottom */
-[data-tooltip]:after, .tooltip:after, .tooltip-top:after {
+[data-tooltip]:after, .tooltip {
margin-left: -80px;
}
-[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after,
-.tooltip-top:hover:before, .tooltip-top:hover:after {
+[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before,
+.tooltip-container:hover .tooltip {
-webkit-transform: translateY(-12px);
-moz-transform: translateY(-12px);
transform: translateY(-12px);
}
/** Tooltips on the left */
-.tooltip-left:before, .tooltip-left:after {
+.tooltip-left:before, [data-tooltip].tooltip-left:after, .tooltip-left .tooltip {
right: 100%;
bottom: 50%;
left: auto;
@@ -114,14 +118,14 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
border-left-color: hsla(0, 0%, 20%, 0.9);
}
-.tooltip-left:hover:before, .tooltip-left:hover:after {
+.tooltip-left:hover:before, [data-tooltip].tooltip-left:hover:after, .tooltip-left:hover .tooltip {
-webkit-transform: translateX(-12px);
-moz-transform: translateX(-12px);
transform: translateX(-12px);
}
/** Tooltips on the bottom */
-.tooltip-bottom:before, .tooltip-bottom:after {
+.tooltip-bottom:before, [data-tooltip].tooltip-bottom:after, .tooltip-bottom .tooltip {
top: 100%;
bottom: auto;
left: 50%;
@@ -135,14 +139,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
border-bottom-color: hsla(0, 0%, 20%, 0.9);
}
-.tooltip-bottom:hover:before, .tooltip-bottom:hover:after {
+.tooltip-bottom:hover:before, [data-tooltip].tooltip-bottom:hover:after,
+.tooltip-bottom:hover .tooltip {
-webkit-transform: translateY(12px);
-moz-transform: translateY(12px);
transform: translateY(12px);
}
/** Tooltips on the right */
-.tooltip-right:before, .tooltip-right:after {
+.tooltip-right:before, [data-tooltip].tooltip-right:after, .tooltip-right .tooltip {
bottom: 50%;
left: 100%;
}
@@ -155,7 +160,8 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
border-right-color: hsla(0, 0%, 20%, 0.9);
}
-.tooltip-right:hover:before, .tooltip-right:hover:after {
+.tooltip-right:hover:before, [data-tooltip].tooltip-right:hover:after,
+.tooltip-right:hover .tooltip {
-webkit-transform: translateX(12px);
-moz-transform: translateX(12px);
transform: translateX(12px);
@@ -167,7 +173,16 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
}
/** Center content vertically for tooltips ont he left and right */
-.tooltip-left:after, .tooltip-right:after {
+[data-tooltip].tooltip-left:after, [data-tooltip].tooltip-right:after,
+.tooltip-left .tooltip, .tooltip-right .tooltip {
margin-left: 0;
margin-bottom: -16px;
}
+
+.tooltip ul, .tooltip ol {
+ padding-left: 1rem;
+}
+
+.tooltip :last-child {
+ margin-bottom: 0;
+}
diff --git a/WebHostLib/static/styles/tracker.css b/WebHostLib/static/styles/tracker.css
index 0cc2ede59fe3..8fcb0c923012 100644
--- a/WebHostLib/static/styles/tracker.css
+++ b/WebHostLib/static/styles/tracker.css
@@ -7,81 +7,119 @@
width: calc(100% - 1rem);
}
-#tracker-wrapper a{
+#tracker-wrapper a {
color: #234ae4;
text-decoration: none;
cursor: pointer;
}
-.table-wrapper{
- overflow-y: auto;
- overflow-x: auto;
- margin-bottom: 1rem;
-}
-
-#tracker-header-bar{
+#tracker-header-bar {
display: flex;
flex-direction: row;
justify-content: flex-start;
+ align-content: center;
line-height: 20px;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
}
-#tracker-header-bar .info{
+#tracker-header-bar .info {
color: #ffffff;
+ padding: 2px;
+ flex-grow: 1;
+ align-self: center;
+ text-align: justify;
+}
+
+#tracker-navigation {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 0 0.5rem 0.5rem 0.5rem;
+ user-select: none;
+ height: 2rem;
+}
+
+.tracker-navigation-bar {
+ display: flex;
+ background-color: #b0a77d;
+ border-radius: 4px;
+}
+
+.tracker-navigation-button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: 4px;
+ padding-left: 12px;
+ padding-right: 12px;
+ border-radius: 4px;
+ text-align: center;
+ font-size: 14px;
+ color: black !important;
+ font-weight: lighter;
+}
+
+.tracker-navigation-button:hover {
+ background-color: #e2eabb !important;
+}
+
+.tracker-navigation-button.selected {
+ background-color: rgb(220, 226, 189);
+}
+
+.table-wrapper {
+ overflow-y: auto;
+ overflow-x: auto;
+ margin-bottom: 1rem;
+ resize: vertical;
}
-#search{
+#search {
border: 1px solid #000000;
border-radius: 3px;
padding: 3px;
width: 200px;
- margin-bottom: 0.5rem;
- margin-right: 1rem;
-}
-
-#multi-stream-link{
- margin-right: 1rem;
}
-div.dataTables_wrapper.no-footer .dataTables_scrollBody{
+div.dataTables_wrapper.no-footer .dataTables_scrollBody {
border: none;
}
-table.dataTable{
+table.dataTable {
color: #000000;
}
-table.dataTable thead{
+table.dataTable thead {
font-family: LexendDeca-Regular, sans-serif;
}
-table.dataTable tbody, table.dataTable tfoot{
+table.dataTable tbody, table.dataTable tfoot {
background-color: #dce2bd;
font-family: LexendDeca-Light, sans-serif;
}
-table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover{
+table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover {
background-color: #e2eabb;
}
-table.dataTable tbody td, table.dataTable tfoot td{
+table.dataTable tbody td, table.dataTable tfoot td {
padding: 4px 6px;
}
-table.dataTable, table.dataTable.no-footer{
+table.dataTable, table.dataTable.no-footer {
border-left: 1px solid #bba967;
width: calc(100% - 2px) !important;
font-size: 1rem;
}
-table.dataTable thead th{
+table.dataTable thead th {
position: -webkit-sticky;
position: sticky;
background-color: #b0a77d;
top: 0;
}
-table.dataTable thead th.upper-row{
+table.dataTable thead th.upper-row {
position: -webkit-sticky;
position: sticky;
background-color: #b0a77d;
@@ -89,7 +127,7 @@ table.dataTable thead th.upper-row{
top: 0;
}
-table.dataTable thead th.lower-row{
+table.dataTable thead th.lower-row {
position: -webkit-sticky;
position: sticky;
background-color: #b0a77d;
@@ -97,59 +135,32 @@ table.dataTable thead th.lower-row{
top: 46px;
}
-table.dataTable tbody td, table.dataTable tfoot td{
+table.dataTable tbody td, table.dataTable tfoot td {
border: 1px solid #bba967;
}
-table.dataTable tfoot td{
+table.dataTable tfoot td {
font-weight: bold;
}
-div.dataTables_scrollBody{
+div.dataTables_scrollBody {
background-color: inherit !important;
}
-table.dataTable .center-column{
+table.dataTable .center-column {
text-align: center;
}
-img.alttp-sprite {
+img.icon-sprite {
height: auto;
max-height: 32px;
min-height: 14px;
}
-.item-acquired{
+.item-acquired {
background-color: #d3c97d;
}
-#tracker-navigation {
- display: inline-flex;
- background-color: #b0a77d;
- margin: 0.5rem;
- border-radius: 4px;
-}
-
-.tracker-navigation-button {
- display: block;
- margin: 4px;
- padding-left: 12px;
- padding-right: 12px;
- border-radius: 4px;
- text-align: center;
- font-size: 14px;
- color: #000;
- font-weight: lighter;
-}
-
-.tracker-navigation-button:hover {
- background-color: #e2eabb !important;
-}
-
-.tracker-navigation-button.selected {
- background-color: rgb(220, 226, 189);
-}
-
@media all and (max-width: 1700px) {
table.dataTable thead th.upper-row{
position: -webkit-sticky;
@@ -159,7 +170,7 @@ img.alttp-sprite {
top: 0;
}
- table.dataTable thead th.lower-row{
+ table.dataTable thead th.lower-row {
position: -webkit-sticky;
position: sticky;
background-color: #b0a77d;
@@ -167,11 +178,11 @@ img.alttp-sprite {
top: 37px;
}
- table.dataTable, table.dataTable.no-footer{
+ table.dataTable, table.dataTable.no-footer {
font-size: 0.8rem;
}
- img.alttp-sprite {
+ img.icon-sprite {
height: auto;
max-height: 24px;
min-height: 10px;
@@ -187,7 +198,7 @@ img.alttp-sprite {
top: 0;
}
- table.dataTable thead th.lower-row{
+ table.dataTable thead th.lower-row {
position: -webkit-sticky;
position: sticky;
background-color: #b0a77d;
@@ -195,11 +206,11 @@ img.alttp-sprite {
top: 32px;
}
- table.dataTable, table.dataTable.no-footer{
+ table.dataTable, table.dataTable.no-footer {
font-size: 0.6rem;
}
- img.alttp-sprite {
+ img.icon-sprite {
height: auto;
max-height: 20px;
min-height: 10px;
diff --git a/WebHostLib/static/styles/tracker__ALinkToThePast.css b/WebHostLib/static/styles/tracker__ALinkToThePast.css
new file mode 100644
index 000000000000..db5dfcbdfed7
--- /dev/null
+++ b/WebHostLib/static/styles/tracker__ALinkToThePast.css
@@ -0,0 +1,142 @@
+@import url('https://fonts.googleapis.com/css2?family=Lexend+Deca:wght@100..900&display=swap');
+
+.tracker-container {
+ width: 440px;
+ box-sizing: border-box;
+ font-family: "Lexend Deca", Arial, Helvetica, sans-serif;
+ border: 2px solid black;
+ border-radius: 4px;
+ resize: both;
+
+ background-color: #42b149;
+ color: white;
+}
+
+.hidden {
+ visibility: hidden;
+}
+
+/** Inventory Grid ****************************************************************************************************/
+.inventory-grid {
+ display: grid;
+ grid-template-columns: repeat(6, minmax(0, 1fr));
+ padding: 1rem;
+ gap: 1rem;
+}
+
+.inventory-grid .item {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ height: 48px;
+}
+
+.inventory-grid .dual-item {
+ display: flex;
+ justify-content: center;
+}
+
+.inventory-grid .missing {
+ /* Missing items will be in full grayscale to signify "uncollected". */
+ filter: grayscale(100%) contrast(75%) brightness(75%);
+}
+
+.inventory-grid .item img,
+.inventory-grid .dual-item img {
+ display: flex;
+ align-items: center;
+ text-align: center;
+ font-size: 0.8rem;
+ text-shadow: 0 1px 2px black;
+ font-weight: bold;
+ image-rendering: crisp-edges;
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+.inventory-grid .dual-item img {
+ height: 48px;
+ margin: 0 -4px;
+}
+
+.inventory-grid .dual-item img:first-child {
+ align-self: flex-end;
+}
+
+.inventory-grid .item .quantity {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ text-align: right;
+ font-weight: 600;
+ font-size: 1.75rem;
+ line-height: 1.75rem;
+ text-shadow:
+ -1px -1px 0 #000,
+ 1px -1px 0 #000,
+ -1px 1px 0 #000,
+ 1px 1px 0 #000;
+ user-select: none;
+}
+
+/** Regions List ******************************************************************************************************/
+.regions-list {
+ padding: 1rem;
+}
+
+.regions-list summary {
+ list-style: none;
+ display: flex;
+ gap: 0.5rem;
+ cursor: pointer;
+}
+
+.regions-list summary::before {
+ content: "⯈";
+ width: 1em;
+ flex-shrink: 0;
+}
+
+.regions-list details {
+ font-weight: 300;
+}
+
+.regions-list details[open] > summary::before {
+ content: "⯆";
+}
+
+.regions-list .region {
+ width: 100%;
+ display: grid;
+ grid-template-columns: 20fr 8fr 2fr 2fr;
+ align-items: center;
+ gap: 4px;
+ text-align: center;
+ font-weight: 300;
+ box-sizing: border-box;
+}
+
+.regions-list .region :first-child {
+ text-align: left;
+ font-weight: 500;
+}
+
+.regions-list .region.region-header {
+ margin-left: 24px;
+ width: calc(100% - 24px);
+ padding: 2px;
+}
+
+.regions-list .location-rows {
+ border-top: 1px solid white;
+ display: grid;
+ grid-template-columns: auto 32px;
+ font-weight: 300;
+ padding: 2px 8px;
+ margin-top: 4px;
+ font-size: 0.8rem;
+}
+
+.regions-list .location-rows :nth-child(even) {
+ text-align: right;
+}
diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-settings.css
deleted file mode 100644
index cc5231634e5b..000000000000
--- a/WebHostLib/static/styles/weighted-settings.css
+++ /dev/null
@@ -1,309 +0,0 @@
-html{
- background-image: url('../static/backgrounds/grass.png');
- background-repeat: repeat;
- background-size: 650px 650px;
- scroll-padding-top: 90px;
-}
-
-#weighted-settings{
- max-width: 1000px;
- margin-left: auto;
- margin-right: auto;
- background-color: rgba(0, 0, 0, 0.15);
- border-radius: 8px;
- padding: 1rem;
- color: #eeffeb;
-}
-
-#weighted-settings #games-wrapper{
- width: 100%;
-}
-
-#weighted-settings .setting-wrapper{
- width: 100%;
- margin-bottom: 2rem;
-}
-
-#weighted-settings .setting-wrapper .add-option-div{
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- margin-bottom: 1rem;
-}
-
-#weighted-settings .setting-wrapper .add-option-div button{
- width: auto;
- height: auto;
- margin: 0 0 0 0.15rem;
- padding: 0 0.25rem;
- border-radius: 4px;
- cursor: default;
-}
-
-#weighted-settings .setting-wrapper .add-option-div button:active{
- margin-bottom: 1px;
-}
-
-#weighted-settings p.setting-description{
- margin: 0 0 1rem;
-}
-
-#weighted-settings p.hint-text{
- margin: 0 0 1rem;
- font-style: italic;
-}
-
-#weighted-settings .jump-link{
- color: #ffef00;
- cursor: pointer;
- text-decoration: underline;
-}
-
-#weighted-settings table{
- width: 100%;
-}
-
-#weighted-settings table th, #weighted-settings table td{
- border: none;
-}
-
-#weighted-settings table td{
- padding: 5px;
-}
-
-#weighted-settings table .td-left{
- font-family: LexendDeca-Regular, sans-serif;
- padding-right: 1rem;
- width: 200px;
-}
-
-#weighted-settings table .td-middle{
- display: flex;
- flex-direction: column;
- justify-content: space-evenly;
- padding-right: 1rem;
-}
-
-#weighted-settings table .td-right{
- width: 4rem;
- text-align: right;
-}
-
-#weighted-settings table .td-delete{
- width: 50px;
- text-align: right;
-}
-
-#weighted-settings table .range-option-delete{
- cursor: pointer;
-}
-
-#weighted-settings .items-wrapper{
- display: flex;
- flex-direction: row;
- justify-content: space-between;
-}
-
-#weighted-settings .items-div h3{
- margin-bottom: 0.5rem;
-}
-
-#weighted-settings .items-wrapper .item-set-wrapper{
- width: 24%;
- font-weight: bold;
-}
-
-#weighted-settings .item-container{
- border: 1px solid #ffffff;
- border-radius: 2px;
- width: 100%;
- height: 300px;
- overflow-y: auto;
- overflow-x: hidden;
- margin-top: 0.125rem;
- font-weight: normal;
-}
-
-#weighted-settings .item-container .item-div{
- padding: 0.125rem 0.5rem;
- cursor: pointer;
-}
-
-#weighted-settings .item-container .item-div:hover{
- background-color: rgba(0, 0, 0, 0.1);
-}
-
-#weighted-settings .item-container .item-qty-div{
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- padding: 0.125rem 0.5rem;
- cursor: pointer;
-}
-
-#weighted-settings .item-container .item-qty-div .item-qty-input-wrapper{
- display: flex;
- flex-direction: column;
- justify-content: space-around;
-}
-
-#weighted-settings .item-container .item-qty-div input{
- min-width: unset;
- width: 1.5rem;
- text-align: center;
-}
-
-#weighted-settings .item-container .item-qty-div:hover{
- background-color: rgba(0, 0, 0, 0.1);
-}
-
-#weighted-settings .hints-div, #weighted-settings .locations-div{
- margin-top: 2rem;
-}
-
-#weighted-settings .hints-div h3, #weighted-settings .locations-div h3{
- margin-bottom: 0.5rem;
-}
-
-#weighted-settings .hints-container, #weighted-settings .locations-container{
- display: flex;
- flex-direction: row;
- justify-content: space-between;
-}
-
-#weighted-settings .hints-wrapper, #weighted-settings .locations-wrapper{
- width: calc(50% - 0.5rem);
- font-weight: bold;
-}
-
-#weighted-settings .hints-wrapper .simple-list, #weighted-settings .locations-wrapper .simple-list{
- margin-top: 0.25rem;
- height: 300px;
- font-weight: normal;
-}
-
-#weighted-settings #weighted-settings-button-row{
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- margin-top: 15px;
-}
-
-#weighted-settings code{
- background-color: #d9cd8e;
- border-radius: 4px;
- padding-left: 0.25rem;
- padding-right: 0.25rem;
- color: #000000;
-}
-
-#weighted-settings #user-message{
- display: none;
- width: calc(100% - 8px);
- background-color: #ffe86b;
- border-radius: 4px;
- color: #000000;
- padding: 4px;
- text-align: center;
-}
-
-#weighted-settings #user-message.visible{
- display: block;
- cursor: pointer;
-}
-
-#weighted-settings h1{
- font-size: 2.5rem;
- font-weight: normal;
- border-bottom: 1px solid #ffffff;
- width: 100%;
- margin-bottom: 0.5rem;
- color: #ffffff;
- text-shadow: 1px 1px 4px #000000;
-}
-
-#weighted-settings h2{
- font-size: 2rem;
- font-weight: normal;
- border-bottom: 1px solid #ffffff;
- width: 100%;
- margin-bottom: 0.5rem;
- color: #ffe993;
- text-transform: none;
- text-shadow: 1px 1px 2px #000000;
-}
-
-#weighted-settings h3, #weighted-settings h4, #weighted-settings h5, #weighted-settings h6{
- color: #ffffff;
- text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
- text-transform: none;
-}
-
-#weighted-settings a{
- color: #ffef00;
- cursor: pointer;
-}
-
-#weighted-settings input:not([type]){
- border: 1px solid #000000;
- padding: 3px;
- border-radius: 3px;
- min-width: 150px;
-}
-
-#weighted-settings input:not([type]):focus{
- border: 1px solid #ffffff;
-}
-
-#weighted-settings select{
- border: 1px solid #000000;
- padding: 3px;
- border-radius: 3px;
- min-width: 150px;
- background-color: #ffffff;
-}
-
-#weighted-settings .game-options, #weighted-settings .rom-options{
- display: flex;
- flex-direction: column;
-}
-
-#weighted-settings .simple-list{
- display: flex;
- flex-direction: column;
-
- max-height: 300px;
- overflow-y: auto;
- border: 1px solid #ffffff;
- border-radius: 4px;
-}
-
-#weighted-settings .simple-list .list-row label{
- display: block;
- width: calc(100% - 0.5rem);
- padding: 0.0625rem 0.25rem;
-}
-
-#weighted-settings .simple-list .list-row label:hover{
- background-color: rgba(0, 0, 0, 0.1);
-}
-
-#weighted-settings .simple-list .list-row label input[type=checkbox]{
- margin-right: 0.5rem;
-}
-
-#weighted-settings .invisible{
- display: none;
-}
-
-@media all and (max-width: 1000px), all and (orientation: portrait){
- #weighted-settings .game-options{
- justify-content: flex-start;
- flex-wrap: wrap;
- }
-
- #game-options table label{
- display: block;
- min-width: 200px;
- }
-}
diff --git a/WebHostLib/static/styles/weightedOptions/weightedOptions.css b/WebHostLib/static/styles/weightedOptions/weightedOptions.css
new file mode 100644
index 000000000000..3cfc6d24992d
--- /dev/null
+++ b/WebHostLib/static/styles/weightedOptions/weightedOptions.css
@@ -0,0 +1,232 @@
+html {
+ background-image: url("../../static/backgrounds/grass.png");
+ background-repeat: repeat;
+ background-size: 650px 650px;
+ scroll-padding-top: 90px;
+}
+
+#weighted-options {
+ max-width: 1000px;
+ margin-left: auto;
+ margin-right: auto;
+ background-color: rgba(0, 0, 0, 0.15);
+ border-radius: 8px;
+ padding: 1rem;
+ color: #eeffeb;
+}
+#weighted-options #weighted-options-header h1 {
+ margin-bottom: 0;
+ padding-bottom: 0;
+}
+#weighted-options #weighted-options-header h1:nth-child(2) {
+ font-size: 1.4rem;
+ margin-top: -8px;
+ margin-bottom: 0.5rem;
+}
+#weighted-options .js-warning-banner {
+ width: calc(100% - 1rem);
+ padding: 0.5rem;
+ border-radius: 4px;
+ background-color: #f3f309;
+ color: #000000;
+ margin-bottom: 0.5rem;
+ text-align: center;
+}
+#weighted-options .option-wrapper {
+ width: 100%;
+ margin-bottom: 2rem;
+}
+#weighted-options .option-wrapper .add-option-div {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ margin-bottom: 1rem;
+}
+#weighted-options .option-wrapper .add-option-div button {
+ width: auto;
+ height: auto;
+ margin: 0 0 0 0.15rem;
+ padding: 0 0.25rem;
+ border-radius: 4px;
+ cursor: default;
+}
+#weighted-options .option-wrapper .add-option-div button:active {
+ margin-bottom: 1px;
+}
+#weighted-options p.option-description {
+ margin: 0 0 1rem;
+}
+#weighted-options p.hint-text {
+ margin: 0 0 1rem;
+ font-style: italic;
+}
+#weighted-options table {
+ width: 100%;
+ margin-top: 0.5rem;
+ margin-bottom: 1.5rem;
+}
+#weighted-options table th, #weighted-options table td {
+ border: none;
+}
+#weighted-options table td {
+ padding: 5px;
+}
+#weighted-options table .td-left {
+ font-family: LexendDeca-Regular, sans-serif;
+ padding-right: 1rem;
+ width: 200px;
+}
+#weighted-options table .td-middle {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-evenly;
+ padding-right: 1rem;
+}
+#weighted-options table .td-right {
+ width: 4rem;
+ text-align: right;
+}
+#weighted-options table .td-delete {
+ width: 50px;
+ text-align: right;
+}
+#weighted-options table .range-option-delete {
+ cursor: pointer;
+}
+#weighted-options #weighted-options-button-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ margin-top: 15px;
+}
+#weighted-options #user-message {
+ display: none;
+ width: calc(100% - 8px);
+ background-color: #ffe86b;
+ border-radius: 4px;
+ color: #000000;
+ padding: 4px;
+ text-align: center;
+}
+#weighted-options #user-message.visible {
+ display: block;
+ cursor: pointer;
+}
+#weighted-options h1 {
+ font-size: 2.5rem;
+ font-weight: normal;
+ width: 100%;
+ margin-bottom: 0.5rem;
+ color: #ffffff;
+ text-shadow: 1px 1px 4px #000000;
+}
+#weighted-options h2, #weighted-options details summary.h2 {
+ font-size: 2rem;
+ font-weight: normal;
+ border-bottom: 1px solid #ffffff;
+ width: 100%;
+ margin-bottom: 0.5rem;
+ color: #ffe993;
+ text-transform: none;
+ text-shadow: 1px 1px 2px #000000;
+}
+#weighted-options h3, #weighted-options h4, #weighted-options h5, #weighted-options h6 {
+ color: #ffffff;
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
+ text-transform: none;
+ cursor: unset;
+}
+#weighted-options h3.option-group-header {
+ margin-top: 0.75rem;
+ font-weight: bold;
+}
+#weighted-options a {
+ color: #ffef00;
+ cursor: pointer;
+}
+#weighted-options input:not([type]) {
+ border: 1px solid #000000;
+ padding: 3px;
+ border-radius: 3px;
+ min-width: 150px;
+}
+#weighted-options input:not([type]):focus {
+ border: 1px solid #ffffff;
+}
+#weighted-options .invisible {
+ display: none;
+}
+#weighted-options .unsupported-option {
+ margin-top: 0.5rem;
+}
+#weighted-options .set-container, #weighted-options .dict-container, #weighted-options .list-container {
+ display: flex;
+ flex-direction: column;
+ background-color: rgba(0, 0, 0, 0.25);
+ border: 1px solid rgba(20, 20, 20, 0.25);
+ border-radius: 3px;
+ color: #ffffff;
+ max-height: 15rem;
+ min-width: 14.5rem;
+ overflow-y: auto;
+ padding-right: 0.25rem;
+ padding-left: 0.25rem;
+ margin-top: 0.5rem;
+}
+#weighted-options .set-container .divider, #weighted-options .dict-container .divider, #weighted-options .list-container .divider {
+ width: 100%;
+ height: 2px;
+ background-color: rgba(20, 20, 20, 0.25);
+ margin-top: 0.125rem;
+ margin-bottom: 0.125rem;
+}
+#weighted-options .set-container .set-entry, #weighted-options .set-container .dict-entry, #weighted-options .set-container .list-entry, #weighted-options .dict-container .set-entry, #weighted-options .dict-container .dict-entry, #weighted-options .dict-container .list-entry, #weighted-options .list-container .set-entry, #weighted-options .list-container .dict-entry, #weighted-options .list-container .list-entry {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ padding-bottom: 0.25rem;
+ padding-top: 0.25rem;
+ user-select: none;
+ line-height: 1rem;
+}
+#weighted-options .set-container .set-entry:hover, #weighted-options .set-container .dict-entry:hover, #weighted-options .set-container .list-entry:hover, #weighted-options .dict-container .set-entry:hover, #weighted-options .dict-container .dict-entry:hover, #weighted-options .dict-container .list-entry:hover, #weighted-options .list-container .set-entry:hover, #weighted-options .list-container .dict-entry:hover, #weighted-options .list-container .list-entry:hover {
+ background-color: rgba(20, 20, 20, 0.25);
+}
+#weighted-options .set-container .set-entry input[type=checkbox], #weighted-options .set-container .dict-entry input[type=checkbox], #weighted-options .set-container .list-entry input[type=checkbox], #weighted-options .dict-container .set-entry input[type=checkbox], #weighted-options .dict-container .dict-entry input[type=checkbox], #weighted-options .dict-container .list-entry input[type=checkbox], #weighted-options .list-container .set-entry input[type=checkbox], #weighted-options .list-container .dict-entry input[type=checkbox], #weighted-options .list-container .list-entry input[type=checkbox] {
+ margin-right: 0.25rem;
+}
+#weighted-options .set-container .set-entry input[type=number], #weighted-options .set-container .dict-entry input[type=number], #weighted-options .set-container .list-entry input[type=number], #weighted-options .dict-container .set-entry input[type=number], #weighted-options .dict-container .dict-entry input[type=number], #weighted-options .dict-container .list-entry input[type=number], #weighted-options .list-container .set-entry input[type=number], #weighted-options .list-container .dict-entry input[type=number], #weighted-options .list-container .list-entry input[type=number] {
+ max-width: 1.5rem;
+ max-height: 1rem;
+ margin-left: 0.125rem;
+ text-align: center;
+ /* Hide arrows on input[type=number] fields */
+ -moz-appearance: textfield;
+}
+#weighted-options .set-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .set-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .set-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .list-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .list-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .list-entry input[type=number]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+#weighted-options .set-container .set-entry label, #weighted-options .set-container .dict-entry label, #weighted-options .set-container .list-entry label, #weighted-options .dict-container .set-entry label, #weighted-options .dict-container .dict-entry label, #weighted-options .dict-container .list-entry label, #weighted-options .list-container .set-entry label, #weighted-options .list-container .dict-entry label, #weighted-options .list-container .list-entry label {
+ flex-grow: 1;
+ margin-right: 0;
+ min-width: unset;
+ display: unset;
+}
+
+.hidden {
+ display: none;
+}
+
+@media all and (max-width: 1000px), all and (orientation: portrait) {
+ #weighted-options .game-options {
+ justify-content: flex-start;
+ flex-wrap: wrap;
+ }
+ #game-options table label {
+ display: block;
+ min-width: 200px;
+ }
+}
+
+/*# sourceMappingURL=weightedOptions.css.map */
diff --git a/WebHostLib/static/styles/weightedOptions/weightedOptions.css.map b/WebHostLib/static/styles/weightedOptions/weightedOptions.css.map
new file mode 100644
index 000000000000..7c57cde01506
--- /dev/null
+++ b/WebHostLib/static/styles/weightedOptions/weightedOptions.css.map
@@ -0,0 +1 @@
+{"version":3,"sourceRoot":"","sources":["weightedOptions.scss"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAOZ;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAIR;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;AAEA;EACA;;AACA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;;AAMhB;EACI;;;AAGJ;EACI;IACI;IACA;;EAGJ;IACI;IACA","file":"weightedOptions.css"}
\ No newline at end of file
diff --git a/WebHostLib/static/styles/weightedOptions/weightedOptions.scss b/WebHostLib/static/styles/weightedOptions/weightedOptions.scss
new file mode 100644
index 000000000000..7ff3a2c3722e
--- /dev/null
+++ b/WebHostLib/static/styles/weightedOptions/weightedOptions.scss
@@ -0,0 +1,274 @@
+html{
+ background-image: url('../../static/backgrounds/grass.png');
+ background-repeat: repeat;
+ background-size: 650px 650px;
+ scroll-padding-top: 90px;
+}
+
+#weighted-options{
+ max-width: 1000px;
+ margin-left: auto;
+ margin-right: auto;
+ background-color: rgba(0, 0, 0, 0.15);
+ border-radius: 8px;
+ padding: 1rem;
+ color: #eeffeb;
+
+ #weighted-options-header{
+ h1{
+ margin-bottom: 0;
+ padding-bottom: 0;
+ }
+
+ h1:nth-child(2){
+ font-size: 1.4rem;
+ margin-top: -8px;
+ margin-bottom: 0.5rem;
+ }
+ }
+
+ .js-warning-banner{
+ width: calc(100% - 1rem);
+ padding: 0.5rem;
+ border-radius: 4px;
+ background-color: #f3f309;
+ color: #000000;
+ margin-bottom: 0.5rem;
+ text-align: center;
+ }
+
+ .option-wrapper{
+ width: 100%;
+ margin-bottom: 2rem;
+
+ .add-option-div{
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ margin-bottom: 1rem;
+
+ button{
+ width: auto;
+ height: auto;
+ margin: 0 0 0 0.15rem;
+ padding: 0 0.25rem;
+ border-radius: 4px;
+ cursor: default;
+
+ &:active{
+ margin-bottom: 1px;
+ }
+ }
+ }
+ }
+
+ p{
+ &.option-description{
+ margin: 0 0 1rem;
+ }
+
+ &.hint-text{
+ margin: 0 0 1rem;
+ font-style: italic;
+ };
+ }
+
+ table{
+ width: 100%;
+ margin-top: 0.5rem;
+ margin-bottom: 1.5rem;
+
+ th, td{
+ border: none;
+ }
+
+ td{
+ padding: 5px;
+ }
+
+ .td-left{
+ font-family: LexendDeca-Regular, sans-serif;
+ padding-right: 1rem;
+ width: 200px;
+ }
+
+ .td-middle{
+ display: flex;
+ flex-direction: column;
+ justify-content: space-evenly;
+ padding-right: 1rem;
+ }
+
+ .td-right{
+ width: 4rem;
+ text-align: right;
+ }
+
+ .td-delete{
+ width: 50px;
+ text-align: right;
+ }
+
+ .range-option-delete{
+ cursor: pointer;
+ }
+ }
+
+ #weighted-options-button-row{
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ margin-top: 15px;
+ }
+
+ #user-message{
+ display: none;
+ width: calc(100% - 8px);
+ background-color: #ffe86b;
+ border-radius: 4px;
+ color: #000000;
+ padding: 4px;
+ text-align: center;
+
+ &.visible{
+ display: block;
+ cursor: pointer;
+ }
+ }
+
+ h1{
+ font-size: 2.5rem;
+ font-weight: normal;
+ width: 100%;
+ margin-bottom: 0.5rem;
+ color: #ffffff;
+ text-shadow: 1px 1px 4px #000000;
+ }
+
+ h2, details summary.h2{
+ font-size: 2rem;
+ font-weight: normal;
+ border-bottom: 1px solid #ffffff;
+ width: 100%;
+ margin-bottom: 0.5rem;
+ color: #ffe993;
+ text-transform: none;
+ text-shadow: 1px 1px 2px #000000;
+ }
+
+ h3, h4, h5, h6{
+ color: #ffffff;
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
+ text-transform: none;
+ cursor: unset;
+ }
+
+ h3{
+ &.option-group-header{
+ margin-top: 0.75rem;
+ font-weight: bold;
+ }
+ }
+
+ a{
+ color: #ffef00;
+ cursor: pointer;
+ }
+
+ input:not([type]){
+ border: 1px solid #000000;
+ padding: 3px;
+ border-radius: 3px;
+ min-width: 150px;
+
+ &:focus{
+ border: 1px solid #ffffff;
+ }
+ }
+
+ .invisible{
+ display: none;
+ }
+
+ .unsupported-option{
+ margin-top: 0.5rem;
+ }
+
+ .set-container, .dict-container, .list-container{
+ display: flex;
+ flex-direction: column;
+ background-color: rgba(0, 0, 0, 0.25);
+ border: 1px solid rgba(20, 20, 20, 0.25);
+ border-radius: 3px;
+ color: #ffffff;
+ max-height: 15rem;
+ min-width: 14.5rem;
+ overflow-y: auto;
+ padding-right: 0.25rem;
+ padding-left: 0.25rem;
+ margin-top: 0.5rem;
+
+ .divider{
+ width: 100%;
+ height: 2px;
+ background-color: rgba(20, 20, 20, 0.25);
+ margin-top: 0.125rem;
+ margin-bottom: 0.125rem;
+ }
+
+ .set-entry, .dict-entry, .list-entry{
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ padding-bottom: 0.25rem;
+ padding-top: 0.25rem;
+ user-select: none;
+ line-height: 1rem;
+
+ &:hover{
+ background-color: rgba(20, 20, 20, 0.25);
+ }
+
+ input[type=checkbox]{
+ margin-right: 0.25rem;
+ }
+
+ input[type=number]{
+ max-width: 1.5rem;
+ max-height: 1rem;
+ margin-left: 0.125rem;
+ text-align: center;
+
+ /* Hide arrows on input[type=number] fields */
+ -moz-appearance: textfield;
+ &::-webkit-outer-spin-button, &::-webkit-inner-spin-button{
+ -webkit-appearance: none;
+ margin: 0;
+ }
+ }
+
+ label{
+ flex-grow: 1;
+ margin-right: 0;
+ min-width: unset;
+ display: unset;
+ }
+ }
+ }
+}
+
+.hidden{
+ display: none;
+}
+
+@media all and (max-width: 1000px), all and (orientation: portrait){
+ #weighted-options .game-options{
+ justify-content: flex-start;
+ flex-wrap: wrap;
+ }
+
+ #game-options table label{
+ display: block;
+ min-width: 200px;
+ }
+}
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 @@ Verification Results
{% endfor %}
+ {% if combined_yaml %}
+ Combined File Download
+ Download
+ {% endif %}
{% endblock %}
diff --git a/WebHostLib/templates/checksfinderTracker.html b/WebHostLib/templates/checksfinderTracker.html
deleted file mode 100644
index 5df77f5e74d0..000000000000
--- a/WebHostLib/templates/checksfinderTracker.html
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
- {{ player_name }}'s Tracker
-
-
-
-
-
-
-
-
diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html
index 33f8dbc09e6c..53d98dfae6ba 100644
--- a/WebHostLib/templates/generate.html
+++ b/WebHostLib/templates/generate.html
@@ -69,8 +69,8 @@ Generate Game{% if race %} (Race Mode){% endif %}
- Allow !collect after goal completion
Automatic on goal completion
+ Allow !collect after goal completion
Automatic on goal completion and manual !collect
@@ -93,9 +93,9 @@ Generate Game{% if race %} (Race Mode){% endif %}
{% if race -%}
Disabled in Race mode
{%- else -%}
- Disabled
Allow !remaining after goal completion
Manual !remaining
+ Disabled
{%- endif -%}
@@ -185,12 +185,12 @@ Generate Game{% if race %} (Race Mode){% endif %}
+
+ Items
+
Bosses
-
- Items
-
Connections
diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/genericTracker.html
index 1c2fcd44c0dd..5a533204083b 100644
--- a/WebHostLib/templates/genericTracker.html
+++ b/WebHostLib/templates/genericTracker.html
@@ -1,36 +1,57 @@
-{% extends 'tablepage.html' %}
+{% extends "tablepage.html" %}
{% block head %}
{{ super() }}
{{ player_name }}'s Tracker
-
-
-
+
+
+
{% endblock %}
{% block body %}
- {% include 'header/dirtHeader.html' %}
-
+ {% include "header/dirtHeader.html" %}
+
+
+
+
+
Item
Amount
- Order Received
+ Last Order Received
- {% for id, count in inventory.items() %}
-
- {{ id | item_name }}
- {{ count }}
- {{received_items[id]}}
-
+ {% for id, count in inventory.items() if count > 0 %}
+
+ {{ item_id_to_name[game][id] }}
+ {{ count }}
+ {{ received_items[id] }}
+
{%- endfor -%}
@@ -39,24 +60,62 @@
-
- Location
- Checked
-
+
+ Location
+ Checked
+
- {% for name in checked_locations %}
+
+ {%- for location in locations -%}
+
+ {{ location_id_to_name[game][location] }}
+
+ {% if location in checked_locations %}✔{% endif %}
+
+
+ {%- endfor -%}
+
+
+
+
+
+
+
- {{ name | location_name}}
- ✔
+ Finder
+ Receiver
+ Item
+ Location
+ Game
+ Entrance
+ Found
- {%- endfor -%}
- {% for name in not_checked_locations %}
+
+
+ {%- for hint in hints -%}
- {{ name | location_name}}
-
+
+ {% if hint.finding_player == player %}
+ {{ player_names_with_alias[(team, hint.finding_player)] }}
+ {% else %}
+ {{ player_names_with_alias[(team, hint.finding_player)] }}
+ {% endif %}
+
+
+ {% if hint.receiving_player == player %}
+ {{ player_names_with_alias[(team, hint.receiving_player)] }}
+ {% else %}
+ {{ player_names_with_alias[(team, hint.receiving_player)] }}
+ {% endif %}
+
+ {{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}
+ {{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}
+ {{ games[(team, hint.finding_player)] }}
+ {% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}
+ {% if hint.found %}✔{% endif %}
- {%- endfor -%}
+ {%- endfor -%}
diff --git a/WebHostLib/templates/hintTable.html b/WebHostLib/templates/hintTable.html
deleted file mode 100644
index 00b74111ea51..000000000000
--- a/WebHostLib/templates/hintTable.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% for team, hints in hints.items() %}
-
-
-
-
- Finder
- Receiver
- Item
- Location
- Entrance
- Found
-
-
-
- {%- for hint in hints -%}
-
- {{ long_player_names[team, hint.finding_player] }}
- {{ long_player_names[team, hint.receiving_player] }}
- {{ hint.item|item_name }}
- {{ hint.location|location_name }}
- {% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}
- {% if hint.found %}✔{% endif %}
-
- {%- endfor -%}
-
-
-
-{% endfor %}
\ No newline at end of file
diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html
index ba15d64acac1..fa8e26c2cbf8 100644
--- a/WebHostLib/templates/hostRoom.html
+++ b/WebHostLib/templates/hostRoom.html
@@ -3,6 +3,16 @@
{% block head %}
Multiworld {{ room.id|suuid }}
{% if should_refresh %} {% endif %}
+
+
+
+ {% if room.seed.slots|length < 2 %}
+
+ {% else %}
+
+ {% endif %}
{% endblock %}
@@ -14,7 +24,8 @@
{% endif %}
{% if room.tracker %}
- This room has a Multiworld Tracker enabled.
+ This room has a Multiworld Tracker
+ and a Sphere Tracker enabled.
{% endif %}
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
@@ -33,7 +44,7 @@
{{ macros.list_patches_room(room) }}
{% if room.owner == session["_id"] %}
diff --git a/WebHostLib/templates/islandFooter.html b/WebHostLib/templates/islandFooter.html
index 7b89c4a9e079..08cf227990b8 100644
--- a/WebHostLib/templates/islandFooter.html
+++ b/WebHostLib/templates/islandFooter.html
@@ -1,6 +1,6 @@
{% block footer %}
- Copyright 2023 Archipelago
+ Copyright 2024 Archipelago
Site Map
-
diff --git a/WebHostLib/templates/landing.html b/WebHostLib/templates/landing.html
index fd45b78cfb74..b489ef18ac91 100644
--- a/WebHostLib/templates/landing.html
+++ b/WebHostLib/templates/landing.html
@@ -49,9 +49,9 @@
multiworld multi-game randomizer
our crazy idea into a reality.
- {{ seeds }}
+ {{ seeds }}
games were generated and
- {{ rooms }}
+ {{ rooms }}
were hosted in the last 7 days.
diff --git a/WebHostLib/templates/lttpMultiTracker.html b/WebHostLib/templates/lttpMultiTracker.html
deleted file mode 100644
index 2b943a22b0cb..000000000000
--- a/WebHostLib/templates/lttpMultiTracker.html
+++ /dev/null
@@ -1,171 +0,0 @@
-{% extends 'tablepage.html' %}
-{% block head %}
- {{ super() }}
- ALttP Multiworld Tracker
-
-
-
-
-{% endblock %}
-
-{% block body %}
- {% include 'header/dirtHeader.html' %}
- {% include 'multiTrackerNavigation.html' %}
-
-
-
- {% for team, players in inventory.items() %}
-
-
-
-
- #
- Name
- {%- for name in tracking_names -%}
- {%- if name in icons -%}
-
-
-
- {%- else -%}
- {{ name|e }}
- {%- endif -%}
- {%- endfor -%}
-
-
-
- {%- for player, items in players.items() -%}
-
- {{ loop.index }}
- {%- if (team, loop.index) in video -%}
- {%- if video[(team, loop.index)][0] == "Twitch" -%}
-
-
- {{ player_names[(team, loop.index)] }}
- â–¶ï¸
- {%- elif video[(team, loop.index)][0] == "Youtube" -%}
-
-
- {{ player_names[(team, loop.index)] }}
- â–¶ï¸
- {%- endif -%}
- {%- else -%}
- {{ player_names[(team, loop.index)] }}
- {%- endif -%}
- {%- for id in tracking_ids -%}
- {%- if items[id] -%}
-
- {% if id in multi_items %}{{ items[id] }}{% else %}✔ï¸{% endif %}
- {%- else -%}
-
- {%- endif -%}
- {% endfor %}
-
- {%- endfor -%}
-
-
-
- {% endfor %}
-
- {% for team, players in checks_done.items() %}
-
- {% endfor %}
- {% include "hintTable.html" with context %}
-
-
-{% endblock %}
diff --git a/WebHostLib/templates/lttpTracker.html b/WebHostLib/templates/lttpTracker.html
deleted file mode 100644
index 3f1c35793eeb..000000000000
--- a/WebHostLib/templates/lttpTracker.html
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
-
- {{ player_name }}'s Tracker
-
-
-
-
-
-
-
-
-
diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html
index 746399da74a6..7bbb894de090 100644
--- a/WebHostLib/templates/macros.html
+++ b/WebHostLib/templates/macros.html
@@ -22,7 +22,7 @@
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
{{ patch.player_id }}
- {{ patch.player_name }}
+ {{ patch.player_name }}
{{ patch.game }}
{% if patch.data %}
@@ -47,9 +47,9 @@
{% elif patch.game | supports_apdeltapatch %}
Download Patch File...
- {% elif patch.game == "Dark Souls III" %}
+ {% elif patch.game == "Final Fantasy Mystic Quest" %}
- Download JSON File...
+ Download APMQ File...
{% else %}
No file to download for this game.
{% endif %}
diff --git a/WebHostLib/templates/minecraftTracker.html b/WebHostLib/templates/minecraftTracker.html
deleted file mode 100644
index 9f5022b4cc43..000000000000
--- a/WebHostLib/templates/minecraftTracker.html
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
- {{ player_name }}'s Tracker
-
-
-
-
-
-
-
-
-
- {% for area in checks_done %}
-
-
- {% for location in location_info[area] %}
-
- {{ location }}
- {{ '✔' if location_info[area][location] else '' }}
-
- {% endfor %}
-
- {% endfor %}
-
-
-
-
diff --git a/WebHostLib/templates/multiFactorioTracker.html b/WebHostLib/templates/multiFactorioTracker.html
deleted file mode 100644
index faca756ee975..000000000000
--- a/WebHostLib/templates/multiFactorioTracker.html
+++ /dev/null
@@ -1,46 +0,0 @@
-{% extends "multiTracker.html" %}
-{% block custom_table_headers %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{% endblock %}
-{% block custom_table_row scoped %}
-{% if games[player] == "Factorio" %}
-{% set player_inventory = named_inventory[team][player] %}
-{% set prog_science = player_inventory["progressive-science-pack"] %}
-{% if player_inventory["logistic-science-pack"] or prog_science %}✔{% endif %}
-{% if player_inventory["military-science-pack"] or prog_science > 1%}✔{% endif %}
-{% if player_inventory["chemical-science-pack"] or prog_science > 2%}✔{% endif %}
-{% if player_inventory["production-science-pack"] or prog_science > 3%}✔{% endif %}
-{% if player_inventory["utility-science-pack"] or prog_science > 4%}✔{% endif %}
-{% if player_inventory["space-science-pack"] or prog_science > 5%}✔{% endif %}
-{% else %}
-âŒ
-âŒ
-âŒ
-âŒ
-âŒ
-âŒ
-{% endif %}
-{% endblock%}
diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html
deleted file mode 100644
index 40d89eb4c6cc..000000000000
--- a/WebHostLib/templates/multiTracker.html
+++ /dev/null
@@ -1,86 +0,0 @@
-{% extends 'tablepage.html' %}
-{% block head %}
- {{ super() }}
- Multiworld Tracker
-
-
-{% endblock %}
-
-{% block body %}
- {% include 'header/dirtHeader.html' %}
- {% include 'multiTrackerNavigation.html' %}
-
-
-
- {% for team, players in checks_done.items() %}
-
-
-
-
- #
- Name
- Game
- Status
- {% block custom_table_headers %}
- {# implement this block in game-specific multi trackers #}
- {% endblock %}
- Checks
- %
- Last Activity
-
-
-
- {%- for player, checks in players.items() -%}
-
- {{ loop.index }}
- {{ player_names[(team, loop.index)]|e }}
- {{ games[player] }}
- {{ {0: "Disconnected", 5: "Connected", 10: "Ready", 20: "Playing",
- 30: "Goal Completed"}.get(states[team, player], "Unknown State") }}
- {% block custom_table_row scoped %}
- {# implement this block in game-specific multi trackers #}
- {% endblock %}
-
- {{ checks["Total"] }}/{{ locations[player] | length }}
-
- {{ percent_total_checks_done[team][player] }}
- {%- if activity_timers[team, player] -%}
- {{ activity_timers[team, player].total_seconds() }}
- {%- else -%}
- None
- {%- endif -%}
-
- {%- endfor -%}
-
- {% if not self.custom_table_headers() | trim %}
-
-
-
- Total
- All Games
- {{ completed_worlds }}/{{ players|length }} Complete
- {{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }}
- {{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }}
-
-
-
- {% endif %}
-
-
- {% endfor %}
- {% include "hintTable.html" with context %}
-
-
-{% endblock %}
diff --git a/WebHostLib/templates/multiTrackerNavigation.html b/WebHostLib/templates/multiTrackerNavigation.html
deleted file mode 100644
index 7fc405b6fbd2..000000000000
--- a/WebHostLib/templates/multiTrackerNavigation.html
+++ /dev/null
@@ -1,9 +0,0 @@
-{%- if enabled_multiworld_trackers|length > 1 -%}
-
- {% for enabled_tracker in enabled_multiworld_trackers %}
- {% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %}
-
{{ enabled_tracker.name }}
- {% endfor %}
-
-{%- endif -%}
diff --git a/WebHostLib/templates/multispheretracker.html b/WebHostLib/templates/multispheretracker.html
new file mode 100644
index 000000000000..a86697498396
--- /dev/null
+++ b/WebHostLib/templates/multispheretracker.html
@@ -0,0 +1,72 @@
+{% extends "tablepage.html" %}
+{% block head %}
+ {{ super() }}
+ Multiworld Sphere Tracker
+
+
+{% endblock %}
+
+{% block body %}
+ {% include "header/dirtHeader.html" %}
+
+
+
+
+
+ {%- for team, players in tracker_data.get_all_players().items() %}
+
+
+
+
+ Sphere
+ {#- Mimicking hint table header for familiarity. #}
+ Finder
+ Receiver
+ Item
+ Location
+ Game
+
+
+
+ {%- for sphere in tracker_data.get_spheres() %}
+ {%- set current_sphere = loop.index %}
+ {%- for player, sphere_location_ids in sphere.items() %}
+ {%- set checked_locations = tracker_data.get_player_checked_locations(team, player) %}
+ {%- set finder_game = tracker_data.get_player_game(team, player) %}
+ {%- set player_location_data = tracker_data.get_player_locations(team, player) %}
+ {%- for location_id in sphere_location_ids.intersection(checked_locations) %}
+
+ {%- set item_id, receiver, item_flags = player_location_data[location_id] %}
+ {%- set receiver_game = tracker_data.get_player_game(team, receiver) %}
+ {{ current_sphere }}
+ {{ tracker_data.get_player_name(team, player) }}
+ {{ tracker_data.get_player_name(team, receiver) }}
+ {{ tracker_data.item_id_to_name[receiver_game][item_id] }}
+ {{ tracker_data.location_id_to_name[finder_game][location_id] }}
+ {{ finder_game }}
+
+ {%- endfor %}
+
+ {%- endfor %}
+ {%- endfor %}
+
+
+
+
+ {%- endfor -%}
+
+
+{% endblock %}
diff --git a/WebHostLib/templates/multitracker.html b/WebHostLib/templates/multitracker.html
new file mode 100644
index 000000000000..1b371b1229e5
--- /dev/null
+++ b/WebHostLib/templates/multitracker.html
@@ -0,0 +1,144 @@
+{% extends "tablepage.html" %}
+{% block head %}
+ {{ super() }}
+ Multiworld Tracker
+
+
+{% endblock %}
+
+{% block body %}
+ {% include "header/dirtHeader.html" %}
+ {% include "multitrackerNavigation.html" %}
+
+
+
+
+
+ {%- for team, players in room_players.items() -%}
+
+
+
+
+ #
+ Name
+ {% if current_tracker == "Generic" %}Game {% endif %}
+ Status
+ {% block custom_table_headers %}
+ {# Implement this block in game-specific multi-trackers. #}
+ {% endblock %}
+ Checks
+ %
+ Last Activity
+
+
+
+ {%- for player in players -%}
+ {%- if current_tracker == "Generic" or games[(team, player)] == current_tracker -%}
+
+
+
+ {{ player }}
+
+
+ {{ player_names_with_alias[(team, player)] | e }}
+ {%- if current_tracker == "Generic" -%}
+ {{ games[(team, player)] }}
+ {%- endif -%}
+
+ {{
+ {
+ 0: "Disconnected",
+ 5: "Connected",
+ 10: "Ready",
+ 20: "Playing",
+ 30: "Goal Completed"
+ }.get(states[(team, player)], "Unknown State")
+ }}
+
+
+ {% block custom_table_row scoped %}
+ {# Implement this block in game-specific multi-trackers. #}
+ {% endblock %}
+
+ {% set location_count = locations[(team, player)] | length %}
+
+ {{ locations_complete[(team, player)] }}/{{ location_count }}
+
+
+
+ {%- if locations[(team, player)] | length > 0 -%}
+ {% set percentage_of_completion = locations_complete[(team, player)] / location_count * 100 %}
+ {{ "{0:.2f}".format(percentage_of_completion) }}
+ {%- else -%}
+ 100.00
+ {%- endif -%}
+
+
+ {%- if activity_timers[(team, player)] -%}
+ {{ activity_timers[(team, player)].total_seconds() }}
+ {%- else -%}
+ None
+ {%- endif -%}
+
+ {%- endif -%}
+ {%- endfor -%}
+
+
+ {%- if not self.custom_table_headers() | trim -%}
+
+
+ Total
+ All Games
+ {{ completed_worlds[team] }}/{{ players | length }} Complete
+
+ {{ total_team_locations_complete[team] }}/{{ total_team_locations[team] }}
+
+
+ {%- if total_team_locations[team] == 0 -%}
+ 100
+ {%- else -%}
+ {{ "{0:.2f}".format(total_team_locations_complete[team] / total_team_locations[team] * 100) }}
+ {%- endif -%}
+
+
+
+
+ {%- endif -%}
+
+
+
+ {%- endfor -%}
+
+ {% block custom_tables %}
+ {# Implement this block to create custom tables in game-specific multi-trackers. #}
+ {% endblock %}
+
+ {% include "multitrackerHintTable.html" with context %}
+
+
+{% endblock %}
diff --git a/WebHostLib/templates/multitrackerHintTable.html b/WebHostLib/templates/multitrackerHintTable.html
new file mode 100644
index 000000000000..a931e9b04845
--- /dev/null
+++ b/WebHostLib/templates/multitrackerHintTable.html
@@ -0,0 +1,37 @@
+{% for team, hints in hints.items() %}
+
+
+
+
+ Finder
+ Receiver
+ Item
+ Location
+ Game
+ Entrance
+ Found
+
+
+
+ {%- for hint in hints -%}
+ {%-
+ if current_tracker == "Generic" or (
+ games[(team, hint.finding_player)] == current_tracker or
+ games[(team, hint.receiving_player)] == current_tracker
+ )
+ -%}
+
+ {{ player_names_with_alias[(team, hint.finding_player)] }}
+ {{ player_names_with_alias[(team, hint.receiving_player)] }}
+ {{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}
+ {{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}
+ {{ games[(team, hint.finding_player)] }}
+ {% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}
+ {% if hint.found %}✔{% endif %}
+
+ {% endif %}
+ {%- endfor -%}
+
+
+
+{% endfor %}
diff --git a/WebHostLib/templates/multitrackerNavigation.html b/WebHostLib/templates/multitrackerNavigation.html
new file mode 100644
index 000000000000..1256181b27d3
--- /dev/null
+++ b/WebHostLib/templates/multitrackerNavigation.html
@@ -0,0 +1,16 @@
+{% if enabled_trackers | length > 1 %}
+
+ {# Multitracker game navigation. #}
+
+ {%- for game_tracker in enabled_trackers -%}
+ {%- set tracker_url = url_for("get_multiworld_tracker", tracker=room.tracker, game=game_tracker) -%}
+
+ {{ game_tracker }}
+
+ {%- endfor -%}
+
+
+{% endif %}
diff --git a/WebHostLib/templates/multitracker__ALinkToThePast.html b/WebHostLib/templates/multitracker__ALinkToThePast.html
new file mode 100644
index 000000000000..9b8f460c4cc3
--- /dev/null
+++ b/WebHostLib/templates/multitracker__ALinkToThePast.html
@@ -0,0 +1,248 @@
+{% extends "multitracker.html" %}
+{% block head %}
+ {{ super() }}
+
+
+{% endblock %}
+
+{# List all tracker-relevant icons. Format: (Name, Image URL) #}
+{% set icons = {
+ "Blue Shield": "https://www.zeldadungeon.net/wiki/images/thumb/c/c3/FightersShield-ALttP-Sprite.png/100px-FightersShield-ALttP-Sprite.png",
+ "Red Shield": "https://www.zeldadungeon.net/wiki/images/thumb/9/9e/FireShield-ALttP-Sprite.png/111px-FireShield-ALttP-Sprite.png",
+ "Mirror Shield": "https://www.zeldadungeon.net/wiki/images/thumb/e/e3/MirrorShield-ALttP-Sprite.png/105px-MirrorShield-ALttP-Sprite.png",
+ "Progressive Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/c/cc/ALttP_Master_Sword_Sprite.png",
+ "Progressive Bow": "https://www.zeldadungeon.net/wiki/images/thumb/8/8c/BowArrows-ALttP-Sprite.png/120px-BowArrows-ALttP-Sprite.png",
+ "Progressive Glove": "https://www.zeldadungeon.net/wiki/images/thumb/4/41/PowerGlove-ALttP-Sprite.png/105px-PowerGlove-ALttP-Sprite.png",
+ "Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png",
+ "Flippers": "https://www.zeldadungeon.net/wiki/images/thumb/b/bc/ZoraFlippers-ALttP-Sprite.png/112px-ZoraFlippers-ALttP-Sprite.png",
+ "Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png",
+ "Blue Boomerang": "https://www.zeldadungeon.net/wiki/images/thumb/f/f0/Boomerang-ALttP-Sprite.png/86px-Boomerang-ALttP-Sprite.png",
+ "Red Boomerang": "https://www.zeldadungeon.net/wiki/images/thumb/3/3c/MagicalBoomerang-ALttP-Sprite.png/86px-MagicalBoomerang-ALttP-Sprite.png",
+ "Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png",
+ "Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png",
+ "Magic Powder": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png",
+ "Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png",
+ "Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png",
+ "Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png",
+ "Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png",
+ "Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png",
+ "Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png",
+ "Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png",
+ "Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png",
+ "Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png",
+ "Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png",
+ "Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png",
+ "Bottles": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png",
+ "Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png",
+ "Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png",
+ "Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png",
+ "Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png",
+ "Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png",
+ "Triforce Piece": "https://www.zeldadungeon.net/wiki/images/thumb/5/54/Triforce_Fragment_-_BS_Zelda.png/62px-Triforce_Fragment_-_BS_Zelda.png",
+ "Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/38/ALttP_Bomb_Sprite.png",
+ "Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png",
+ "Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png",
+ "Chest": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda",
+ "Light World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6",
+ "Dark World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc",
+ "Hyrule Castle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be",
+ "Agahnims Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5",
+ "Desert Palace": "https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png",
+ "Eastern Palace": "https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png",
+ "Tower of Hera": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7",
+ "Palace of Darkness": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022",
+ "Swamp Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5",
+ "Skull Woods": "https://alttp-wiki.net/images/6/6a/Mothula.png",
+ "Thieves Town": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222",
+ "Ice Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0",
+ "Misery Mire": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8",
+ "Turtle Rock": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be",
+ "Ganons Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74",
+} %}
+
+{% set inventory_order = [
+ "Progressive Sword",
+ "Progressive Bow",
+ "Blue Boomerang",
+ "Red Boomerang",
+ "Hookshot",
+ "Bombs",
+ "Mushroom",
+ "Magic Powder",
+ "Fire Rod",
+ "Ice Rod",
+ "Bombos",
+ "Ether",
+ "Quake",
+ "Lamp",
+ "Hammer",
+ "Flute",
+ "Bug Catching Net",
+ "Book of Mudora",
+ "Cane of Somaria",
+ "Cane of Byrna",
+ "Cape",
+ "Magic Mirror",
+ "Shovel",
+ "Pegasus Boots",
+ "Flippers",
+ "Progressive Glove",
+ "Moon Pearl",
+ "Bottles",
+ "Triforce Piece",
+ "Triforce",
+] %}
+
+{% set dungeon_keys = {
+ "Hyrule Castle": ("Small Key (Hyrule Castle)", "Big Key (Hyrule Castle)"),
+ "Agahnims Tower": ("Small Key (Agahnims Tower)", "Big Key (Agahnims Tower)"),
+ "Eastern Palace": ("Small Key (Eastern Palace)", "Big Key (Eastern Palace)"),
+ "Desert Palace": ("Small Key (Desert Palace)", "Big Key (Desert Palace)"),
+ "Tower of Hera": ("Small Key (Tower of Hera)", "Big Key (Tower of Hera)"),
+ "Palace of Darkness": ("Small Key (Palace of Darkness)", "Big Key (Palace of Darkness)"),
+ "Thieves Town": ("Small Key (Thieves Town)", "Big Key (Thieves Town)"),
+ "Skull Woods": ("Small Key (Skull Woods)", "Big Key (Skull Woods)"),
+ "Swamp Palace": ("Small Key (Swamp Palace)", "Big Key (Swamp Palace)"),
+ "Ice Palace": ("Small Key (Ice Palace)", "Big Key (Ice Palace)"),
+ "Misery Mire": ("Small Key (Misery Mire)", "Big Key (Misery Mire)"),
+ "Turtle Rock": ("Small Key (Turtle Rock)", "Big Key (Turtle Rock)"),
+ "Ganons Tower": ("Small Key (Ganons Tower)", "Big Key (Ganons Tower)"),
+} %}
+
+{% set multi_items = [
+ "Progressive Sword",
+ "Progressive Glove",
+ "Progressive Bow",
+ "Bottles",
+ "Triforce Piece",
+] %}
+
+{%- block custom_table_headers %}
+ {#- macro that creates a table header with display name and image -#}
+ {%- macro make_header(name, img_src) %}
+
+
+
+ {% endmacro -%}
+
+ {#- call the macro to build the table header -#}
+ {%- for item in inventory_order %}
+ {%- if item in icons -%}
+
+
+
+ {%- endif %}
+ {% endfor -%}
+{% endblock %}
+
+{# build each row of custom entries #}
+{% block custom_table_row scoped %}
+ {%- for item in inventory_order -%}
+ {%- if inventories[(team, player)][item] -%}
+
+ {% if item in multi_items %}
+ {{ inventories[(team, player)][item] }}
+ {% else %}
+ ✔ï¸
+ {% endif %}
+
+ {%- else -%}
+
+ {%- endif -%}
+ {% endfor %}
+{% endblock %}
+
+{% block custom_tables %}
+
+{% for team in total_team_locations %}
+
+{% endfor %}
+
+{% endblock %}
diff --git a/WebHostLib/templates/multitracker__Factorio.html b/WebHostLib/templates/multitracker__Factorio.html
new file mode 100644
index 000000000000..a7ad824db41f
--- /dev/null
+++ b/WebHostLib/templates/multitracker__Factorio.html
@@ -0,0 +1,41 @@
+{% extends "multitracker.html" %}
+{# establish the to be tracked data. Display Name, factorio/AP internal name, display image #}
+{%- set science_packs = [
+ ("Logistic Science Pack", "logistic-science-pack",
+ "https://wiki.factorio.com/images/thumb/Logistic_science_pack.png/32px-Logistic_science_pack.png"),
+ ("Military Science Pack", "military-science-pack",
+ "https://wiki.factorio.com/images/thumb/Military_science_pack.png/32px-Military_science_pack.png"),
+ ("Chemical Science Pack", "chemical-science-pack",
+ "https://wiki.factorio.com/images/thumb/Chemical_science_pack.png/32px-Chemical_science_pack.png"),
+ ("Production Science Pack", "production-science-pack",
+ "https://wiki.factorio.com/images/thumb/Production_science_pack.png/32px-Production_science_pack.png"),
+ ("Utility Science Pack", "utility-science-pack",
+ "https://wiki.factorio.com/images/thumb/Utility_science_pack.png/32px-Utility_science_pack.png"),
+ ("Space Science Pack", "space-science-pack",
+ "https://wiki.factorio.com/images/thumb/Space_science_pack.png/32px-Space_science_pack.png"),
+] -%}
+
+{%- block custom_table_headers %}
+{#- macro that creates a table header with display name and image -#}
+{%- macro make_header(name, img_src) %}
+
+
+
+{% endmacro -%}
+{#- call the macro to build the table header -#}
+{%- for name, internal_name, img_src in science_packs %}
+ {{ make_header(name, img_src) }}
+{% endfor -%}
+{% endblock %}
+
+{% block custom_table_row scoped %}
+ {%- set player_inventory = inventories[(team, player)] -%}
+ {%- set prog_science = player_inventory["progressive-science-pack"] -%}
+ {%- for name, internal_name, img_src in science_packs %}
+ {% if player_inventory[internal_name] or prog_science > loop.index0 %}
+ ✔ï¸
+ {% else %}
+
+ {% endif %}
+ {% endfor -%}
+{% endblock%}
diff --git a/WebHostLib/templates/ootTracker.html b/WebHostLib/templates/ootTracker.html
deleted file mode 100644
index ea7a6d5a4c30..000000000000
--- a/WebHostLib/templates/ootTracker.html
+++ /dev/null
@@ -1,180 +0,0 @@
-
-
-
- {{ player_name }}'s Tracker
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ hookshot_length }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ bottle_count if bottle_count > 0 else '' }}
-
-
-
-
-
-
-
-
-
{{ wallet_size }}
-
-
-
-
-
-
-
-
-
-
Zelda
-
-
-
-
-
-
Epona
-
-
-
-
-
-
Saria
-
-
-
-
-
-
Sun
-
-
-
-
-
-
Time
-
-
-
-
-
-
Storms
-
-
-
-
-
-
{{ token_count }}
-
-
-
-
-
-
-
-
Min
-
-
-
-
-
-
Bol
-
-
-
-
-
-
Ser
-
-
-
-
-
-
Req
-
-
-
-
-
-
Noc
-
-
-
-
-
-
Pre
-
-
-
-
-
-
{{ piece_count if piece_count > 0 else '' }}
-
-
-
-
-
-
-
-
-
- Items
-
- {% for area in checks_done %}
-
-
- {% for location in location_info[area] %}
-
- {{ location }}
-
-
- {{ '✔' if location_info[area][location] else '' }}
-
- {% endfor %}
-
- {% endfor %}
-
-
-
-
diff --git a/WebHostLib/templates/pageWrapper.html b/WebHostLib/templates/pageWrapper.html
index ec7888ac7317..c7dda523ef4e 100644
--- a/WebHostLib/templates/pageWrapper.html
+++ b/WebHostLib/templates/pageWrapper.html
@@ -16,7 +16,7 @@
{% with messages = get_flashed_messages() %}
{% if messages %}
- {% for message in messages %}
+ {% for message in messages | unique %}
{{ message }}
{% endfor %}
diff --git a/WebHostLib/templates/player-settings.html b/WebHostLib/templates/player-settings.html
deleted file mode 100644
index 50b9e3cbb1a2..000000000000
--- a/WebHostLib/templates/player-settings.html
+++ /dev/null
@@ -1,48 +0,0 @@
-{% extends 'pageWrapper.html' %}
-
-{% block head %}
- {{ game }} Settings
-
-
-
-
-
-
-{% endblock %}
-
-{% block body %}
- {% include 'header/'+theme+'Header.html' %}
-
-
-
Player Settings
-
Choose the options you would like to play with! You may generate a single-player game from this page,
- or download a settings file you can use to participate in a MultiWorld.
-
-
- A more advanced settings configuration for all games can be found on the
- Weighted Settings page.
-
- A list of all games you have generated can be found on the User Content Page .
-
- You may also download the
- template file for this game .
-
-
-
Please enter your player name. This will appear in-game as you send and receive
- items if you are playing in a MultiWorld.
-
-
-
-
Game Options
-
-
-
- Export Settings
- Generate Game
- Generate Race
-
-
-{% endblock %}
diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html
new file mode 100644
index 000000000000..30a4fc78dff3
--- /dev/null
+++ b/WebHostLib/templates/playerOptions/macros.html
@@ -0,0 +1,221 @@
+{% macro Toggle(option_name, option) %}
+ {{ OptionTitle(option_name, option) }}
+
+
+ {% if option.default == 1 %}
+ No
+ Yes
+ {% else %}
+ No
+ Yes
+ {% endif %}
+
+ {{ RandomizeButton(option_name, option) }}
+
+{% endmacro %}
+
+{% macro Choice(option_name, option) %}
+ {{ OptionTitle(option_name, option) }}
+
+
+ {% for id, name in option.name_lookup.items() %}
+ {% if name != "random" %}
+ {% if option.default == id %}
+ {{ option.get_option_name(id) }}
+ {% else %}
+ {{ option.get_option_name(id) }}
+ {% endif %}
+ {% endif %}
+ {% endfor %}
+
+ {{ RandomizeButton(option_name, option) }}
+
+{% endmacro %}
+
+{% macro Range(option_name, option) %}
+ {{ OptionTitle(option_name, option) }}
+
+
+
+ {{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
+
+ {{ RandomizeButton(option_name, option) }}
+
+{% endmacro %}
+
+{% macro NamedRange(option_name, option) %}
+ {{ OptionTitle(option_name, option) }}
+
+
+ {% for key, val in option.special_range_names.items() %}
+ {% if option.default == val %}
+ {{ key|replace("_", " ")|title }} ({{ val }})
+ {% else %}
+ {{ key|replace("_", " ")|title }} ({{ val }})
+ {% endif %}
+ {% endfor %}
+ Custom
+
+
+
+
+ {{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
+
+ {{ RandomizeButton(option_name, option) }}
+
+
+{% endmacro %}
+
+{% macro FreeText(option_name, option) %}
+ {{ OptionTitle(option_name, option) }}
+
+
+
+{% endmacro %}
+
+{% macro TextChoice(option_name, option) %}
+ {{ OptionTitle(option_name, option) }}
+
+
+
+ {% for id, name in option.name_lookup.items()|sort %}
+ {% if name != "random" %}
+ {% if option.default == id %}
+ {{ option.get_option_name(id) }}
+ {% else %}
+ {{ option.get_option_name(id) }}
+ {% endif %}
+ {% endif %}
+ {% endfor %}
+ Custom
+
+ {{ RandomizeButton(option_name, option) }}
+
+
+
+{% endmacro %}
+
+{% macro ItemDict(option_name, option) %}
+ {{ OptionTitle(option_name, option) }}
+
+ {% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
+
+ {{ item_name }}
+
+
+ {% endfor %}
+
+{% endmacro %}
+
+{% macro OptionList(option_name, option) %}
+ {{ OptionTitle(option_name, option) }}
+
+ {% for key in (option.valid_keys if option.valid_keys is ordered else option.valid_keys|sort) %}
+
+
+ {{ key }}
+
+ {% endfor %}
+
+{% endmacro %}
+
+{% macro LocationSet(option_name, option) %}
+ {{ OptionTitle(option_name, option) }}
+
+ {% for group_name in world.location_name_groups.keys()|sort %}
+ {% if group_name != "Everywhere" %}
+
+
+ {{ group_name }}
+
+ {% endif %}
+ {% endfor %}
+ {% if world.location_name_groups.keys()|length > 1 %}
+
+ {% endif %}
+ {% for location_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.location_names|sort) %}
+
+
+ {{ location_name }}
+
+ {% endfor %}
+
+{% endmacro %}
+
+{% macro ItemSet(option_name, option) %}
+ {{ OptionTitle(option_name, option) }}
+
+ {% for group_name in world.item_name_groups.keys()|sort %}
+ {% if group_name != "Everything" %}
+
+
+ {{ group_name }}
+
+ {% endif %}
+ {% endfor %}
+ {% if world.item_name_groups.keys()|length > 1 %}
+
+ {% endif %}
+ {% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
+
+
+ {{ item_name }}
+
+ {% endfor %}
+
+{% endmacro %}
+
+{% macro OptionSet(option_name, option) %}
+ {{ OptionTitle(option_name, option) }}
+
+ {% for key in (option.valid_keys if option.valid_keys is ordered else option.valid_keys|sort) %}
+
+
+ {{ key }}
+
+ {% endfor %}
+
+{% endmacro %}
+
+{% macro OptionTitle(option_name, option) %}
+
+ {{ option.display_name|default(option_name) }}:
+
+ (?)
+ {% if option.rich_text_doc | default(world.web.rich_text_options_doc, true) %}
+
+ {{ option.__doc__ | default("**Please document me!**") | rst_to_html | safe }}
+
+ {% endif %}
+
+
+{% endmacro %}
+
+{% macro RandomizeButton(option_name, option) %}
+
+
+
+ 🎲
+
+
+{% endmacro %}
diff --git a/WebHostLib/templates/playerOptions/playerOptions.html b/WebHostLib/templates/playerOptions/playerOptions.html
new file mode 100644
index 000000000000..73de5d56eb20
--- /dev/null
+++ b/WebHostLib/templates/playerOptions/playerOptions.html
@@ -0,0 +1,166 @@
+{% extends 'pageWrapper.html' %}
+{% import 'playerOptions/macros.html' as inputs with context %}
+
+{% block head %}
+ {{ world_name }} Options
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block body %}
+ {% include 'header/'+theme+'Header.html' %}
+
+
+
+ This page has reduced functionality without JavaScript.
+
+
+
+
{{ message }}
+
+
+
Choose the options you would like to play with! You may generate a single-player game from this page,
+ or download an options file you can use to participate in a MultiWorld.
+
+
+ A more advanced options configuration for all games can be found on the
+ Weighted options page.
+
+ A list of all games you have generated can be found on the User Content Page .
+
+ You may also download the
+ template file for this game .
+
+
+
+
+{% endblock %}
diff --git a/WebHostLib/templates/sc2wolTracker.html b/WebHostLib/templates/sc2wolTracker.html
deleted file mode 100644
index 49c31a579544..000000000000
--- a/WebHostLib/templates/sc2wolTracker.html
+++ /dev/null
@@ -1,361 +0,0 @@
-
-
-
- {{ player_name }}'s Tracker
-
-
-
-
-
-
-
-
-
- {% for area in checks_in_area %}
- {% if checks_in_area[area] > 0 %}
-
-
- {% for location in location_info[area] %}
-
- {{ location }}
- {{ '✔' if location_info[area][location] else '' }}
-
- {% endfor %}
-
- {% endif %}
- {% endfor %}
-
-
-
-
diff --git a/WebHostLib/templates/siteMap.html b/WebHostLib/templates/siteMap.html
index 562dd3b71bc3..cdd6ad45eb27 100644
--- a/WebHostLib/templates/siteMap.html
+++ b/WebHostLib/templates/siteMap.html
@@ -24,7 +24,6 @@ Base Pages
Supported Games Page
Tutorials Page
User Content
- Weighted Settings Page
Game Statistics
Glossary
@@ -46,12 +45,16 @@ Game Info Pages
{% endfor %}
- Game Settings Pages
+ Game Options Pages
{% for game in games | title_sorted %}
{% if game['has_settings'] %}
- {{ game['title'] }}
- {% endif %}
+ {{ game['title'] }}
+
+ {% endif %}
{% endfor %}
diff --git a/WebHostLib/templates/startPlaying.html b/WebHostLib/templates/startPlaying.html
index 436af3df07e8..ab2f021d61d2 100644
--- a/WebHostLib/templates/startPlaying.html
+++ b/WebHostLib/templates/startPlaying.html
@@ -18,7 +18,7 @@ Start Playing
To start playing a game, you'll first need to generate a randomized game .
- You'll need to upload either a config file or a zip file containing one more config files.
+ You'll need to upload one or more config files (YAMLs) or a zip file containing one or more config files.
If you have already generated a game and just need to host it, this site can
diff --git a/WebHostLib/templates/supermetroidTracker.html b/WebHostLib/templates/supermetroidTracker.html
deleted file mode 100644
index 342f75642fcc..000000000000
--- a/WebHostLib/templates/supermetroidTracker.html
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
-
- {{ player_name }}'s Tracker
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ energy_count }}
-
-
-
-
-
-
{{ reserve_count }}
-
-
-
-
-
-
{{ missile_count }}
-
-
-
-
-
-
{{ super_count }}
-
-
-
-
-
-
{{ power_count }}
-
-
-
-
-
-
-
- {% for area in checks_done %}
-
-
- {% for location in location_info[area] %}
-
- {{ location }}
- {{ '✔' if location_info[area][location] else '' }}
-
- {% endfor %}
-
- {% endfor %}
-
-
-
-
diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html
index 82f6348db2e9..b3f20d293543 100644
--- a/WebHostLib/templates/supportedGames.html
+++ b/WebHostLib/templates/supportedGames.html
@@ -4,34 +4,65 @@
Supported Games
+
+
+
+
{% endblock %}
{% block body %}
{% include 'header/oceanHeader.html' %}
Currently Supported Games
+
+
Search for your game below!
+
+
+ Expand All
+ Collapse All
+
+
{% for game_name in worlds | title_sorted %}
{% set world = worlds[game_name] %}
-
{{ game_name }}
-
+
+ {{ game_name }}
{{ world.__doc__ | default("No description provided.", true) }}
Game Page
{% if world.web.tutorials %}
|
- Setup Guides
+ Setup Guides
{% endif %}
- {% if world.web.settings_page is string %}
+ {% if world.web.options_page is string %}
+ |
+ Options Page (External Link)
+ {% elif world.web.options_page %}
|
- Settings Page
- {% elif world.web.settings_page %}
+ Options Page
|
- Settings Page
+ Advanced Options
{% endif %}
{% if world.web.bug_report_page %}
|
Report a Bug
{% endif %}
-
+
{% endfor %}
{% endblock %}
diff --git a/WebHostLib/templates/timespinnerTracker.html b/WebHostLib/templates/timespinnerTracker.html
deleted file mode 100644
index f02ec6daab77..000000000000
--- a/WebHostLib/templates/timespinnerTracker.html
+++ /dev/null
@@ -1,117 +0,0 @@
-
-
-
- {{ player_name }}'s Tracker
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% if 'UnchainedKeys' in options %}
- {% if 'EnterSandman' in options %}
-
-
-
- {% endif %}
-
-
-
-
-
-
- {% endif %}
-
-
-
-
-
-
-
-
-
- {% if 'DownloadableItems' in options %}
-
- {% endif %}
-
-
- {% if 'DownloadableItems' in options %}
-
- {% endif %}
-
- {% if 'EyeSpy' in options %}
-
- {% endif %}
-
-
-
-
- {% if 'GyreArchives' in options %}
-
-
- {% endif %}
-
- {% if 'Djinn Inferno' in acquired_items %}
-
- {% elif 'Pyro Ring' in acquired_items %}
-
- {% elif 'Fire Orb' in acquired_items %}
-
- {% elif 'Infernal Flames' in acquired_items %}
-
- {% else %}
-
- {% endif %}
-
-
- {% if 'Royal Ring' in acquired_items %}
-
- {% elif 'Plasma Geyser' in acquired_items %}
-
- {% elif 'Plasma Orb' in acquired_items %}
-
- {% else %}
-
- {% endif %}
-
-
-
-
-
- {% for area in checks_done %}
-
-
- {% for location in location_info[area] %}
-
- {{ location }}
- {{ '✔' if location_info[area][location] else '' }}
-
- {% endfor %}
-
- {% endfor %}
-
-
-
-
diff --git a/WebHostLib/templates/tracker__ALinkToThePast.html b/WebHostLib/templates/tracker__ALinkToThePast.html
new file mode 100644
index 000000000000..99179797f443
--- /dev/null
+++ b/WebHostLib/templates/tracker__ALinkToThePast.html
@@ -0,0 +1,219 @@
+{% set icons = {
+ "Blue Shield": "https://www.zeldadungeon.net/wiki/images/thumb/c/c3/FightersShield-ALttP-Sprite.png/100px-FightersShield-ALttP-Sprite.png",
+ "Red Shield": "https://www.zeldadungeon.net/wiki/images/thumb/9/9e/FireShield-ALttP-Sprite.png/111px-FireShield-ALttP-Sprite.png",
+ "Mirror Shield": "https://www.zeldadungeon.net/wiki/images/thumb/e/e3/MirrorShield-ALttP-Sprite.png/105px-MirrorShield-ALttP-Sprite.png",
+ "Fighter Sword": "https://upload.wikimedia.org/wikibooks/en/8/8e/Zelda_ALttP_item_L-1_Sword.png",
+ "Master Sword": "https://upload.wikimedia.org/wikibooks/en/8/87/BS_Zelda_AST_item_L-2_Sword.png",
+ "Tempered Sword": "https://upload.wikimedia.org/wikibooks/en/c/cc/BS_Zelda_AST_item_L-3_Sword.png",
+ "Golden Sword": "https://upload.wikimedia.org/wikibooks/en/4/40/BS_Zelda_AST_item_L-4_Sword.png",
+ "Bow": "https://www.zeldadungeon.net/wiki/images/thumb/8/8c/BowArrows-ALttP-Sprite.png/120px-BowArrows-ALttP-Sprite.png",
+ "Silver Bow": "https://upload.wikimedia.org/wikibooks/en/6/69/Zelda_ALttP_item_Silver_Arrows.png",
+ "Green Mail": "https://upload.wikimedia.org/wikibooks/en/d/dd/Zelda_ALttP_item_Green_Mail.png",
+ "Blue Mail": "https://upload.wikimedia.org/wikibooks/en/b/b5/Zelda_ALttP_item_Blue_Mail.png",
+ "Red Mail": "https://upload.wikimedia.org/wikibooks/en/d/db/Zelda_ALttP_item_Red_Mail.png",
+ "Power Glove": "https://www.zeldadungeon.net/wiki/images/thumb/4/41/PowerGlove-ALttP-Sprite.png/105px-PowerGlove-ALttP-Sprite.png",
+ "Titan Mitts": "https://www.zeldadungeon.net/wiki/images/thumb/7/75/TitanMitt-ALttP-Sprite.png/105px-TitanMitt-ALttP-Sprite.png",
+ "Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png",
+ "Flippers": "https://www.zeldadungeon.net/wiki/images/thumb/b/bc/ZoraFlippers-ALttP-Sprite.png/112px-ZoraFlippers-ALttP-Sprite.png",
+ "Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png",
+ "Blue Boomerang": "https://www.zeldadungeon.net/wiki/images/thumb/f/f0/Boomerang-ALttP-Sprite.png/86px-Boomerang-ALttP-Sprite.png",
+ "Red Boomerang": "https://www.zeldadungeon.net/wiki/images/thumb/3/3c/MagicalBoomerang-ALttP-Sprite.png/86px-MagicalBoomerang-ALttP-Sprite.png",
+ "Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png",
+ "Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png",
+ "Magic Powder": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png",
+ "Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png",
+ "Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png",
+ "Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png",
+ "Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png",
+ "Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png",
+ "Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png",
+ "Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png",
+ "Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png",
+ "Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png",
+ "Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png",
+ "Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png",
+ "Bottles": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png",
+ "Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png",
+ "Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png",
+ "Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png",
+ "Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png",
+ "Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png",
+ "Triforce Piece": "https://www.zeldadungeon.net/wiki/images/thumb/5/54/Triforce_Fragment_-_BS_Zelda.png/62px-Triforce_Fragment_-_BS_Zelda.png",
+ "Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/38/ALttP_Bomb_Sprite.png",
+ "Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png",
+ "Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png",
+} %}
+
+{% set inventory_order = [
+ "Progressive Bow", "Boomerangs", "Hookshot", "Bombs", "Mushroom", "Magic Powder",
+ "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", "Progressive Mail",
+ "Lamp", "Hammer", "Flute", "Bug Catching Net", "Book of Mudora", "Progressive Shield",
+ "Bottles", "Cane of Somaria", "Cane of Byrna", "Cape", "Magic Mirror", "Progressive Sword",
+ "Shovel", "Pegasus Boots", "Progressive Glove", "Flippers", "Moon Pearl", "Triforce Piece",
+] %}
+
+{# Most have a duplicated 0th entry for when we have none of that item to still load the correct icon/name. #}
+{% set progressive_order = {
+ "Progressive Bow": ["Bow", "Bow", "Silver Bow"],
+ "Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
+ "Progressive Shield": ["Blue Shield", "Blue Shield", "Red Shield", "Mirror Shield"],
+ "Progressive Sword": ["Fighter Sword", "Fighter Sword", "Master Sword", "Tempered Sword", "Golden Sword"],
+ "Progressive Glove": ["Power Glove", "Power Glove", "Titan Mitts"],
+} %}
+
+{% set dungeon_keys = {
+ "Hyrule Castle": ("Small Key (Hyrule Castle)", "Big Key (Hyrule Castle)"),
+ "Agahnims Tower": ("Small Key (Agahnims Tower)", "Big Key (Agahnims Tower)"),
+ "Eastern Palace": ("Small Key (Eastern Palace)", "Big Key (Eastern Palace)"),
+ "Desert Palace": ("Small Key (Desert Palace)", "Big Key (Desert Palace)"),
+ "Tower of Hera": ("Small Key (Tower of Hera)", "Big Key (Tower of Hera)"),
+ "Palace of Darkness": ("Small Key (Palace of Darkness)", "Big Key (Palace of Darkness)"),
+ "Swamp Palace": ("Small Key (Swamp Palace)", "Big Key (Swamp Palace)"),
+ "Thieves Town": ("Small Key (Thieves Town)", "Big Key (Thieves Town)"),
+ "Skull Woods": ("Small Key (Skull Woods)", "Big Key (Skull Woods)"),
+ "Ice Palace": ("Small Key (Ice Palace)", "Big Key (Ice Palace)"),
+ "Misery Mire": ("Small Key (Misery Mire)", "Big Key (Misery Mire)"),
+ "Turtle Rock": ("Small Key (Turtle Rock)", "Big Key (Turtle Rock)"),
+ "Ganons Tower": ("Small Key (Ganons Tower)", "Big Key (Ganons Tower)"),
+} %}
+
+
+
+
+
+
+ {{ player_name }}'s Tracker
+
+
+
+
+ {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
+
+
+
+ {# Inventory Grid #}
+
+ {% for item in inventory_order %}
+ {% if item in progressive_order %}
+ {% set non_prog_item = progressive_order[item][inventory[item]] %}
+
+
+
+ {% elif item == "Boomerangs" %}
+
+
+
+
+ {% else %}
+
+
+ {% if item == "Bottles" or item == "Triforce Piece" %}
+
{{ inventory[item] }}
+ {% endif %}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+ {% for region_name in known_regions %}
+ {% set region_data = regions[region_name] %}
+ {% if region_data["locations"] | length > 0 %}
+
+
+ {% if region_name in dungeon_keys %}
+
+ {{ region_name }}
+ {{ region_data["checked"] }} / {{ region_data["locations"] | length }}
+ {{ inventory[dungeon_keys[region_name][0]] }}
+
+ {% if region_name == "Agahnims Tower" %}
+ —
+ {% elif inventory[dungeon_keys[region_name][1]] %}
+ ✔
+ {% endif %}
+
+
+ {% else %}
+
+ {{ region_name }}
+ {{ region_data["checked"] }} / {{ region_data["locations"] | length }}
+ —
+ —
+
+ {% endif %}
+
+
+
+ {% for location, checked in region_data["locations"] %}
+
{{ location }}
+
{% if checked %}✔{% endif %}
+ {% endfor %}
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+
diff --git a/WebHostLib/templates/tracker__ChecksFinder.html b/WebHostLib/templates/tracker__ChecksFinder.html
new file mode 100644
index 000000000000..f0995c854838
--- /dev/null
+++ b/WebHostLib/templates/tracker__ChecksFinder.html
@@ -0,0 +1,40 @@
+
+
+
+ {{ player_name }}'s Tracker
+
+
+
+
+
+ {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
+
+
+
+
+
diff --git a/WebHostLib/templates/tracker__Minecraft.html b/WebHostLib/templates/tracker__Minecraft.html
new file mode 100644
index 000000000000..248f2778bda1
--- /dev/null
+++ b/WebHostLib/templates/tracker__Minecraft.html
@@ -0,0 +1,84 @@
+
+
+
+ {{ player_name }}'s Tracker
+
+
+
+
+
+
+ {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
+
+
+
+
+
+ {% for area in checks_done %}
+
+
+ {% for location in location_info[area] %}
+
+ {{ location }}
+ {{ '✔' if location_info[area][location] else '' }}
+
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
diff --git a/WebHostLib/templates/tracker__OcarinaOfTime.html b/WebHostLib/templates/tracker__OcarinaOfTime.html
new file mode 100644
index 000000000000..41b76816cfca
--- /dev/null
+++ b/WebHostLib/templates/tracker__OcarinaOfTime.html
@@ -0,0 +1,185 @@
+
+
+
+ {{ player_name }}'s Tracker
+
+
+
+
+
+ {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ hookshot_length }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ bottle_count if bottle_count > 0 else '' }}
+
+
+
+
+
+
+
+
+
{{ wallet_size }}
+
+
+
+
+
+
+
+
+
+
Zelda
+
+
+
+
+
+
Epona
+
+
+
+
+
+
Saria
+
+
+
+
+
+
Sun
+
+
+
+
+
+
Time
+
+
+
+
+
+
Storms
+
+
+
+
+
+
{{ token_count }}
+
+
+
+
+
+
+
+
Min
+
+
+
+
+
+
Bol
+
+
+
+
+
+
Ser
+
+
+
+
+
+
Req
+
+
+
+
+
+
Noc
+
+
+
+
+
+
Pre
+
+
+
+
+
+
{{ piece_count if piece_count > 0 else '' }}
+
+
+
+
+
+
+
+
+
+ Items
+
+ {% for area in checks_done %}
+
+
+ {% for location in location_info[area] %}
+
+ {{ location }}
+
+
+ {{ '✔' if location_info[area][location] else '' }}
+
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
diff --git a/WebHostLib/templates/tracker__Starcraft2.html b/WebHostLib/templates/tracker__Starcraft2.html
new file mode 100644
index 000000000000..d365d126338d
--- /dev/null
+++ b/WebHostLib/templates/tracker__Starcraft2.html
@@ -0,0 +1,1092 @@
+
+{% macro sc2_icon(name) -%}
+
+{% endmacro -%}
+{% macro sc2_progressive_icon(name, url, level) -%}
+
+{% endmacro -%}
+{% macro sc2_progressive_icon_with_custom_name(item_name, url, title) -%}
+
+{% endmacro -%}
+{%+ macro sc2_tint_level(level) %}
+ tint-level-{{ level }}
+{%+ endmacro %}
+{% macro sc2_render_area(area) %}
+
+
+ {% for location in location_info[area] %}
+
+ {{ location }}
+ {{ '✔' if location_info[area][location] else '' }}
+
+ {% endfor %}
+
+{% endmacro -%}
+{% macro sc2_loop_areas(column_index, column_count) %}
+ {% for area in checks_in_area if checks_in_area[area] > 0 and area != 'Total' %}
+ {% if loop.index0 < (loop.length / column_count) * (column_index + 1)
+ and loop.index0 >= (loop.length / column_count) * (column_index) %}
+ {{ sc2_render_area(area) }}
+ {% endif %}
+ {% endfor %}
+{% endmacro -%}
+
+
+ {{ player_name }}'s Tracker
+
+
+
+
+
+
+ {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
+
+
+
+
+
+
+
+
+
+
+ {{ player_name }}'s Starcraft 2 Tracker
+ Starting Resources
+
+
+
+
+ +{{ minerals_count }}
+
+ +{{ vespene_count }}
+
+ +{{ supply_count }}
+
+
+
+
+
+
+
+
+
+ Terran
+
+
+
+
+ Weapon & Armor Upgrades
+
+
+
+ {{ sc2_progressive_icon('Progressive Terran Infantry Weapon', terran_infantry_weapon_url, terran_infantry_weapon_level) }}
+ {{ sc2_progressive_icon('Progressive Terran Infantry Armor', terran_infantry_armor_url, terran_infantry_armor_level) }}
+ {{ sc2_progressive_icon('Progressive Terran Vehicle Weapon', terran_vehicle_weapon_url, terran_vehicle_weapon_level) }}
+ {{ sc2_progressive_icon('Progressive Terran Vehicle Armor', terran_vehicle_armor_url, terran_vehicle_armor_level) }}
+ {{ sc2_progressive_icon('Progressive Terran Ship Weapon', terran_ship_weapon_url, terran_ship_weapon_level) }}
+ {{ sc2_progressive_icon('Progressive Terran Ship Armor', terran_ship_armor_url, terran_ship_armor_level) }}
+
+
+ {{ sc2_icon('Ultra-Capacitors') }}
+ {{ sc2_icon('Vanadium Plating') }}
+
+
+
+ Base
+
+
+
+ {{ sc2_icon('Bunker') }}
+ {{ sc2_icon('Projectile Accelerator (Bunker)') }}
+ {{ sc2_icon('Neosteel Bunker (Bunker)') }}
+ {{ sc2_icon('Shrike Turret (Bunker)') }}
+ {{ sc2_icon('Fortified Bunker (Bunker)') }}
+
+ {{ sc2_icon('Missile Turret') }}
+ {{ sc2_icon('Titanium Housing (Missile Turret)') }}
+ {{ sc2_icon('Hellstorm Batteries (Missile Turret)') }}
+
+ {{ sc2_icon('Tech Reactor') }}
+
+ {{ sc2_icon('Orbital Depots') }}
+
+
+ {{ sc2_icon('Command Center Reactor') }}
+ {{ sc2_progressive_icon_with_custom_name('Progressive Orbital Command', orbital_command_url, orbital_command_name) }}
+ {{ sc2_icon('Planetary Fortress') }}
+ {{ sc2_progressive_icon_with_custom_name('Progressive Augmented Thrusters (Planetary Fortress)', augmented_thrusters_planetary_fortress_url, augmented_thrusters_planetary_fortress_name) }}
+ {{ sc2_icon('Advanced Targeting (Planetary Fortress)') }}
+
+ {{ sc2_icon('Micro-Filtering') }}
+ {{ sc2_icon('Automated Refinery') }}
+
+ {{ sc2_icon('Advanced Construction (SCV)') }}
+ {{ sc2_icon('Dual-Fusion Welders (SCV)') }}
+ {{ sc2_icon('Hostile Environment Adaptation (SCV)') }}
+
+
+ {{ sc2_icon('Sensor Tower') }}
+
+ {{ sc2_icon('Perdition Turret') }}
+
+ {{ sc2_icon('Hive Mind Emulator') }}
+
+ {{ sc2_icon('Psi Disrupter') }}
+
+
+
+ Infantry
+
+
+
+ Vehicles
+
+
+
+ {{ sc2_icon('Marine') }}
+ {{ sc2_progressive_icon_with_custom_name('Progressive Stimpack (Marine)', stimpack_marine_url, stimpack_marine_name) }}
+ {{ sc2_icon('Combat Shield (Marine)') }}
+ {{ sc2_icon('Laser Targeting System (Marine)') }}
+ {{ sc2_icon('Magrail Munitions (Marine)') }}
+ {{ sc2_icon('Optimized Logistics (Marine)') }}
+
+ {{ sc2_icon('Hellion') }}
+ {{ sc2_icon('Twin-Linked Flamethrower (Hellion)') }}
+ {{ sc2_icon('Thermite Filaments (Hellion)') }}
+ {{ sc2_icon('Hellbat Aspect (Hellion)') }}
+ {{ sc2_icon('Smart Servos (Hellion)') }}
+ {{ sc2_icon('Optimized Logistics (Hellion)') }}
+ {{ sc2_icon('Jump Jets (Hellion)') }}
+
+
+ {{ sc2_icon('Medic') }}
+ {{ sc2_icon('Advanced Medic Facilities (Medic)') }}
+ {{ sc2_icon('Stabilizer Medpacks (Medic)') }}
+ {{ sc2_icon('Restoration (Medic)') }}
+ {{ sc2_icon('Optical Flare (Medic)') }}
+ {{ sc2_icon('Resource Efficiency (Medic)') }}
+ {{ sc2_icon('Adaptive Medpacks (Medic)') }}
+
+
+ {{ sc2_progressive_icon_with_custom_name('Progressive Stimpack (Hellion)', stimpack_hellion_url, stimpack_hellion_name) }}
+ {{ sc2_icon('Infernal Plating (Hellion)') }}
+
+
+
+ {{ sc2_icon('Nano Projector (Medic)') }}
+
+ {{ sc2_icon('Vulture') }}
+ {{ sc2_progressive_icon_with_custom_name('Progressive Replenishable Magazine (Vulture)', replenishable_magazine_vulture_url, replenishable_magazine_vulture_name) }}
+ {{ sc2_icon('Ion Thrusters (Vulture)') }}
+ {{ sc2_icon('Auto Launchers (Vulture)') }}
+ {{ sc2_icon('Auto-Repair (Vulture)') }}
+
+
+ {{ sc2_icon('Firebat') }}
+ {{ sc2_icon('Incinerator Gauntlets (Firebat)') }}
+ {{ sc2_icon('Juggernaut Plating (Firebat)') }}
+ {{ sc2_progressive_icon_with_custom_name('Progressive Stimpack (Firebat)', stimpack_firebat_url, stimpack_firebat_name) }}
+ {{ sc2_icon('Resource Efficiency (Firebat)') }}
+ {{ sc2_icon('Infernal Pre-Igniter (Firebat)') }}
+ {{ sc2_icon('Kinetic Foam (Firebat)') }}
+
+ {{ sc2_icon('Cerberus Mine (Spider Mine)') }}
+ {{ sc2_icon('High Explosive Munition (Spider Mine)') }}
+
+
+
+ {{ sc2_icon('Nano Projectors (Firebat)') }}
+
+ {{ sc2_icon('Goliath') }}
+ {{ sc2_icon('Multi-Lock Weapons System (Goliath)') }}
+ {{ sc2_icon('Ares-Class Targeting System (Goliath)') }}
+ {{ sc2_icon('Jump Jets (Goliath)') }}
+ {{ sc2_icon('Shaped Hull (Goliath)') }}
+ {{ sc2_icon('Optimized Logistics (Goliath)') }}
+ {{ sc2_icon('Resource Efficiency (Goliath)') }}
+
+
+ {{ sc2_icon('Marauder') }}
+ {{ sc2_icon('Concussive Shells (Marauder)') }}
+ {{ sc2_icon('Kinetic Foam (Marauder)') }}
+ {{ sc2_progressive_icon_with_custom_name('Progressive Stimpack (Marauder)', stimpack_marauder_url, stimpack_marauder_name) }}
+ {{ sc2_icon('Laser Targeting System (Marauder)') }}
+ {{ sc2_icon('Magrail Munitions (Marauder)') }}
+ {{ sc2_icon('Internal Tech Module (Marauder)') }}
+
+ {{ sc2_icon('Internal Tech Module (Goliath)') }}
+
+
+
+ {{ sc2_icon('Juggernaut Plating (Marauder)') }}
+
+ {{ sc2_icon('Diamondback') }}
+ {{ sc2_progressive_icon_with_custom_name('Progressive Tri-Lithium Power Cell (Diamondback)', trilithium_power_cell_diamondback_url, trilithium_power_cell_diamondback_name) }}
+ {{ sc2_icon('Shaped Hull (Diamondback)') }}
+ {{ sc2_icon('Hyperfluxor (Diamondback)') }}
+ {{ sc2_icon('Burst Capacitors (Diamondback)') }}
+ {{ sc2_icon('Ion Thrusters (Diamondback)') }}
+ {{ sc2_icon('Resource Efficiency (Diamondback)') }}
+
+
+ {{ sc2_icon('Reaper') }}
+ {{ sc2_icon('U-238 Rounds (Reaper)') }}
+ {{ sc2_icon('G-4 Clusterbomb (Reaper)') }}
+ {{ sc2_progressive_icon_with_custom_name('Progressive Stimpack (Reaper)', stimpack_reaper_url, stimpack_reaper_name) }}
+ {{ sc2_icon('Laser Targeting System (Reaper)') }}
+ {{ sc2_icon('Advanced Cloaking Field (Reaper)') }}
+ {{ sc2_icon('Spider Mines (Reaper)') }}
+
+ {{ sc2_icon('Siege Tank') }}
+ {{ sc2_icon('Maelstrom Rounds (Siege Tank)') }}
+ {{ sc2_icon('Shaped Blast (Siege Tank)') }}
+ {{ sc2_icon('Jump Jets (Siege Tank)') }}
+ {{ sc2_icon('Spider Mines (Siege Tank)') }}
+ {{ sc2_icon('Smart Servos (Siege Tank)') }}
+ {{ sc2_icon('Graduating Range (Siege Tank)') }}
+
+
+
+ {{ sc2_icon('Combat Drugs (Reaper)') }}
+ {{ sc2_icon('Jet Pack Overdrive (Reaper)') }}
+
+ {{ sc2_icon('Laser Targeting System (Siege Tank)') }}
+ {{ sc2_icon('Advanced Siege Tech (Siege Tank)') }}
+ {{ sc2_icon('Internal Tech Module (Siege Tank)') }}
+ {{ sc2_icon('Shaped Hull (Siege Tank)') }}
+ {{ sc2_icon('Resource Efficiency (Siege Tank)') }}
+
+
+ {{ sc2_icon('Ghost') }}
+ {{ sc2_icon('Ocular Implants (Ghost)') }}
+ {{ sc2_icon('Crius Suit (Ghost)') }}
+ {{ sc2_icon('EMP Rounds (Ghost)') }}
+ {{ sc2_icon('Lockdown (Ghost)') }}
+ {{ sc2_icon('Resource Efficiency (Ghost)') }}
+
+ {{ sc2_icon('Thor') }}
+ {{ sc2_icon('330mm Barrage Cannon (Thor)') }}
+ {{ sc2_progressive_icon_with_custom_name('Progressive Immortality Protocol (Thor)', immortality_protocol_thor_url, immortality_protocol_thor_name) }}
+ {{ sc2_progressive_icon_with_custom_name('Progressive High Impact Payload (Thor)', high_impact_payload_thor_url, high_impact_payload_thor_name) }}
+ {{ sc2_icon('Button With a Skull on It (Thor)') }}
+ {{ sc2_icon('Laser Targeting System (Thor)') }}
+ {{ sc2_icon('Large Scale Field Construction (Thor)') }}
+
+
+ {{ sc2_icon('Spectre') }}
+ {{ sc2_icon('Psionic Lash (Spectre)') }}
+ {{ sc2_icon('Nyx-Class Cloaking Module (Spectre)') }}
+ {{ sc2_icon('Impaler Rounds (Spectre)') }}
+ {{ sc2_icon('Resource Efficiency (Spectre)') }}
+
+ {{ sc2_icon('Predator') }}
+ {{ sc2_icon('Resource Efficiency (Predator)') }}
+ {{ sc2_icon('Cloak (Predator)') }}
+ {{ sc2_icon('Charge (Predator)') }}
+ {{ sc2_icon('Predator\'s Fury (Predator)') }}
+
+
+ {{ sc2_icon('HERC') }}
+ {{ sc2_icon('Juggernaut Plating (HERC)') }}
+ {{ sc2_icon('Kinetic Foam (HERC)') }}
+ {{ sc2_icon('Resource Efficiency (HERC)') }}
+
+ {{ sc2_icon('Widow Mine') }}
+ {{ sc2_icon('Drilling Claws (Widow Mine)') }}
+ {{ sc2_icon('Concealment (Widow Mine)') }}
+ {{ sc2_icon('Black Market Launchers (Widow Mine)') }}
+ {{ sc2_icon('Executioner Missiles (Widow Mine)') }}
+
+
+
+ {{ sc2_icon('Cyclone') }}
+ {{ sc2_icon('Mag-Field Accelerators (Cyclone)') }}
+ {{ sc2_icon('Mag-Field Launchers (Cyclone)') }}
+ {{ sc2_icon('Targeting Optics (Cyclone)') }}
+ {{ sc2_icon('Rapid Fire Launchers (Cyclone)') }}
+ {{ sc2_icon('Resource Efficiency (Cyclone)') }}
+ {{ sc2_icon('Internal Tech Module (Cyclone)') }}
+
+
+
+ {{ sc2_icon('Warhound') }}
+ {{ sc2_icon('Resource Efficiency (Warhound)') }}
+ {{ sc2_icon('Reinforced Plating (Warhound)') }}
+
+
+
+ Starships
+
+
+
+ {{ sc2_icon('Medivac') }}
+ {{ sc2_icon('Rapid Deployment Tube (Medivac)') }}
+ {{ sc2_icon('Advanced Healing AI (Medivac)') }}
+ {{ sc2_icon('Expanded Hull (Medivac)') }}
+ {{ sc2_icon('Afterburners (Medivac)') }}
+ {{ sc2_icon('Scatter Veil (Medivac)') }}
+ {{ sc2_icon('Advanced Cloaking Field (Medivac)') }}
+
+ {{ sc2_icon('Raven') }}
+ {{ sc2_icon('Bio Mechanical Repair Drone (Raven)') }}
+ {{ sc2_icon('Spider Mines (Raven)') }}
+ {{ sc2_icon('Railgun Turret (Raven)') }}
+ {{ sc2_icon('Hunter-Seeker Weapon (Raven)') }}
+ {{ sc2_icon('Interference Matrix (Raven)') }}
+ {{ sc2_icon('Anti-Armor Missile (Raven)') }}
+
+
+ {{ sc2_icon('Wraith') }}
+ {{ sc2_progressive_icon_with_custom_name('Progressive Tomahawk Power Cells (Wraith)', tomahawk_power_cells_wraith_url, tomahawk_power_cells_wraith_name) }}
+ {{ sc2_icon('Displacement Field (Wraith)') }}
+ {{ sc2_icon('Advanced Laser Technology (Wraith)') }}
+ {{ sc2_icon('Trigger Override (Wraith)') }}
+ {{ sc2_icon('Internal Tech Module (Wraith)') }}
+ {{ sc2_icon('Resource Efficiency (Wraith)') }}
+
+
+ {{ sc2_icon('Internal Tech Module (Raven)') }}
+ {{ sc2_icon('Resource Efficiency (Raven)') }}
+ {{ sc2_icon('Durable Materials (Raven)') }}
+
+
+ {{ sc2_icon('Viking') }}
+ {{ sc2_icon('Ripwave Missiles (Viking)') }}
+ {{ sc2_icon('Phobos-Class Weapons System (Viking)') }}
+ {{ sc2_icon('Smart Servos (Viking)') }}
+ {{ sc2_icon('Anti-Mechanical Munition (Viking)') }}
+ {{ sc2_icon('Shredder Rounds (Viking)') }}
+ {{ sc2_icon('W.I.L.D. Missiles (Viking)') }}
+
+ {{ sc2_icon('Science Vessel') }}
+ {{ sc2_icon('EMP Shockwave (Science Vessel)') }}
+ {{ sc2_icon('Defensive Matrix (Science Vessel)') }}
+ {{ sc2_icon('Improved Nano-Repair (Science Vessel)') }}
+ {{ sc2_icon('Advanced AI Systems (Science Vessel)') }}
+
+
+ {{ sc2_icon('Banshee') }}
+ {{ sc2_progressive_icon_with_custom_name('Progressive Cross-Spectrum Dampeners (Banshee)', crossspectrum_dampeners_banshee_url, crossspectrum_dampeners_banshee_name) }}
+ {{ sc2_icon('Shockwave Missile Battery (Banshee)') }}
+ {{ sc2_icon('Hyperflight Rotors (Banshee)') }}
+ {{ sc2_icon('Laser Targeting System (Banshee)') }}
+ {{ sc2_icon('Internal Tech Module (Banshee)') }}
+ {{ sc2_icon('Shaped Hull (Banshee)') }}
+
+ {{ sc2_icon('Hercules') }}
+ {{ sc2_icon('Internal Fusion Module (Hercules)') }}
+ {{ sc2_icon('Tactical Jump (Hercules)') }}
+
+
+
+ {{ sc2_icon('Advanced Targeting Optics (Banshee)') }}
+ {{ sc2_icon('Distortion Blasters (Banshee)') }}
+ {{ sc2_icon('Rocket Barrage (Banshee)') }}
+
+ {{ sc2_icon('Liberator') }}
+ {{ sc2_icon('Advanced Ballistics (Liberator)') }}
+ {{ sc2_icon('Raid Artillery (Liberator)') }}
+ {{ sc2_icon('Cloak (Liberator)') }}
+ {{ sc2_icon('Laser Targeting System (Liberator)') }}
+ {{ sc2_icon('Optimized Logistics (Liberator)') }}
+ {{ sc2_icon('Smart Servos (Liberator)') }}
+
+
+ {{ sc2_icon('Battlecruiser') }}
+ {{ sc2_progressive_icon('Progressive Missile Pods (Battlecruiser)', missile_pods_battlecruiser_url, missile_pods_battlecruiser_level) }}
+ {{ sc2_progressive_icon_with_custom_name('Progressive Defensive Matrix (Battlecruiser)', defensive_matrix_battlecruiser_url, defensive_matrix_battlecruiser_name) }}
+ {{ sc2_icon('Tactical Jump (Battlecruiser)') }}
+ {{ sc2_icon('Cloak (Battlecruiser)') }}
+ {{ sc2_icon('ATX Laser Battery (Battlecruiser)') }}
+ {{ sc2_icon('Optimized Logistics (Battlecruiser)') }}
+
+ {{ sc2_icon('Resource Efficiency (Liberator)') }}
+
+
+
+ {{ sc2_icon('Internal Tech Module (Battlecruiser)') }}
+ {{ sc2_icon('Behemoth Plating (Battlecruiser)') }}
+ {{ sc2_icon('Covert Ops Engines (Battlecruiser)') }}
+
+ {{ sc2_icon('Valkyrie') }}
+ {{ sc2_icon('Enhanced Cluster Launchers (Valkyrie)') }}
+ {{ sc2_icon('Shaped Hull (Valkyrie)') }}
+ {{ sc2_icon('Flechette Missiles (Valkyrie)') }}
+ {{ sc2_icon('Afterburners (Valkyrie)') }}
+ {{ sc2_icon('Launching Vector Compensator (Valkyrie)') }}
+ {{ sc2_icon('Resource Efficiency (Valkyrie)') }}
+
+
+
+ Mercenaries
+
+
+
+ {{ sc2_icon('War Pigs') }}
+ {{ sc2_icon('Devil Dogs') }}
+ {{ sc2_icon('Hammer Securities') }}
+ {{ sc2_icon('Spartan Company') }}
+ {{ sc2_icon('Siege Breakers') }}
+ {{ sc2_icon('Hel\'s Angels') }}
+ {{ sc2_icon('Dusk Wings') }}
+ {{ sc2_icon('Jackson\'s Revenge') }}
+ {{ sc2_icon('Skibi\'s Angels') }}
+ {{ sc2_icon('Death Heads') }}
+ {{ sc2_icon('Winged Nightmares') }}
+ {{ sc2_icon('Midnight Riders') }}
+ {{ sc2_icon('Brynhilds') }}
+ {{ sc2_icon('Jotun') }}
+
+
+
+ General Upgrades
+
+
+
+ {{ sc2_progressive_icon('Progressive Fire-Suppression System', firesuppression_system_url, firesuppression_system_level) }}
+ {{ sc2_icon('Orbital Strike') }}
+ {{ sc2_icon('Cellular Reactor') }}
+ {{ sc2_progressive_icon('Progressive Regenerative Bio-Steel', regenerative_biosteel_url, regenerative_biosteel_level) }}
+ {{ sc2_icon('Structure Armor') }}
+ {{ sc2_icon('Hi-Sec Auto Tracking') }}
+ {{ sc2_icon('Advanced Optics') }}
+ {{ sc2_icon('Rogue Forces') }}
+
+
+
+ Nova Equipment
+
+
+
+ {{ sc2_icon('C20A Canister Rifle (Nova Weapon)') }}
+ {{ sc2_icon('Hellfire Shotgun (Nova Weapon)') }}
+ {{ sc2_icon('Plasma Rifle (Nova Weapon)') }}
+ {{ sc2_icon('Monomolecular Blade (Nova Weapon)') }}
+ {{ sc2_icon('Blazefire Gunblade (Nova Weapon)') }}
+
+ {{ sc2_icon('Stim Infusion (Nova Gadget)') }}
+ {{ sc2_icon('Pulse Grenades (Nova Gadget)') }}
+ {{ sc2_icon('Flashbang Grenades (Nova Gadget)') }}
+ {{ sc2_icon('Ionic Force Field (Nova Gadget)') }}
+ {{ sc2_icon('Holo Decoy (Nova Gadget)') }}
+
+
+ {{ sc2_progressive_icon_with_custom_name('Progressive Stealth Suit Module (Nova Suit Module)', stealth_suit_module_nova_suit_module_url, stealth_suit_module_nova_suit_module_name) }}
+ {{ sc2_icon('Energy Suit Module (Nova Suit Module)') }}
+ {{ sc2_icon('Armored Suit Module (Nova Suit Module)') }}
+ {{ sc2_icon('Jump Suit Module (Nova Suit Module)') }}
+
+ {{ sc2_icon('Ghost Visor (Nova Equipment)') }}
+ {{ sc2_icon('Rangefinder Oculus (Nova Equipment)') }}
+ {{ sc2_icon('Domination (Nova Ability)') }}
+ {{ sc2_icon('Blink (Nova Ability)') }}
+ {{ sc2_icon('Tac Nuke Strike (Nova Ability)') }}
+
+
+
+
+
+
+
+ Zerg
+
+
+
+
+ Weapon & Armor Upgrades
+
+
+
+ {{ sc2_progressive_icon('Progressive Zerg Melee Attack', zerg_melee_attack_url, zerg_melee_attack_level) }}
+ {{ sc2_progressive_icon('Progressive Zerg Missile Attack', zerg_missile_attack_url, zerg_missile_attack_level) }}
+ {{ sc2_progressive_icon('Progressive Zerg Ground Carapace', zerg_ground_carapace_url, zerg_ground_carapace_level) }}
+
+ {{ sc2_progressive_icon('Progressive Zerg Flyer Attack', zerg_flyer_attack_url, zerg_flyer_attack_level) }}
+ {{ sc2_progressive_icon('Progressive Zerg Flyer Carapace', zerg_flyer_carapace_url, zerg_flyer_carapace_level) }}
+
+
+
+ Base
+
+
+
+ {{ sc2_icon('Automated Extractors (Kerrigan Tier 3)') }}
+ {{ sc2_icon('Vespene Efficiency (Kerrigan Tier 5)') }}
+
+ {{ sc2_icon('Twin Drones (Kerrigan Tier 5)') }}
+
+ {{ sc2_icon('Improved Overlords (Kerrigan Tier 3)') }}
+ {{ sc2_icon('Ventral Sacs (Overlord)') }}
+
+
+ {{ sc2_icon('Malignant Creep (Kerrigan Tier 5)') }}
+
+ {{ sc2_icon('Spine Crawler') }}
+
+ {{ sc2_icon('Spore Crawler') }}
+
+
+
+ Units
+
+
+
+ {{ sc2_icon('Zergling') }}
+ {{ sc2_icon('Raptor Strain (Zergling)') }}
+ {{ sc2_icon('Swarmling Strain (Zergling)') }}
+ {{ sc2_icon('Hardened Carapace (Zergling)') }}
+ {{ sc2_icon('Adrenal Overload (Zergling)') }}
+ {{ sc2_icon('Metabolic Boost (Zergling)') }}
+ {{ sc2_icon('Shredding Claws (Zergling)') }}
+ {{ sc2_icon('Zergling Reconstitution (Kerrigan Tier 3)') }}
+
+
+
+ {{ sc2_icon('Baneling Aspect (Zergling)') }}
+ {{ sc2_icon('Splitter Strain (Baneling)') }}
+ {{ sc2_icon('Hunter Strain (Baneling)') }}
+ {{ sc2_icon('Corrosive Acid (Baneling)') }}
+ {{ sc2_icon('Rupture (Baneling)') }}
+ {{ sc2_icon('Regenerative Acid (Baneling)') }}
+ {{ sc2_icon('Centrifugal Hooks (Baneling)') }}
+
+
+
+ {{ sc2_icon('Tunneling Jaws (Baneling)') }}
+ {{ sc2_icon('Rapid Metamorph (Baneling)') }}
+
+
+ {{ sc2_icon('Swarm Queen') }}
+ {{ sc2_icon('Spawn Larvae (Swarm Queen)') }}
+ {{ sc2_icon('Deep Tunnel (Swarm Queen)') }}
+ {{ sc2_icon('Organic Carapace (Swarm Queen)') }}
+ {{ sc2_icon('Bio-Mechanical Transfusion (Swarm Queen)') }}
+ {{ sc2_icon('Resource Efficiency (Swarm Queen)') }}
+ {{ sc2_icon('Incubator Chamber (Swarm Queen)') }}
+
+
+ {{ sc2_icon('Roach') }}
+ {{ sc2_icon('Vile Strain (Roach)') }}
+ {{ sc2_icon('Corpser Strain (Roach)') }}
+ {{ sc2_icon('Hydriodic Bile (Roach)') }}
+ {{ sc2_icon('Adaptive Plating (Roach)') }}
+ {{ sc2_icon('Tunneling Claws (Roach)') }}
+ {{ sc2_icon('Glial Reconstitution (Roach)') }}
+ {{ sc2_icon('Organic Carapace (Roach)') }}
+
+
+
+ {{ sc2_icon('Ravager Aspect (Roach)') }}
+ {{ sc2_icon('Potent Bile (Ravager)') }}
+ {{ sc2_icon('Bloated Bile Ducts (Ravager)') }}
+ {{ sc2_icon('Deep Tunnel (Ravager)') }}
+
+
+ {{ sc2_icon('Hydralisk') }}
+ {{ sc2_icon('Frenzy (Hydralisk)') }}
+ {{ sc2_icon('Ancillary Carapace (Hydralisk)') }}
+ {{ sc2_icon('Grooved Spines (Hydralisk)') }}
+ {{ sc2_icon('Muscular Augments (Hydralisk)') }}
+ {{ sc2_icon('Resource Efficiency (Hydralisk)') }}
+
+
+
+ {{ sc2_icon('Impaler Aspect (Hydralisk)') }}
+ {{ sc2_icon('Adaptive Talons (Impaler)') }}
+ {{ sc2_icon('Secretion Glands (Impaler)') }}
+ {{ sc2_icon('Hardened Tentacle Spines (Impaler)') }}
+
+
+
+ {{ sc2_icon('Lurker Aspect (Hydralisk)') }}
+ {{ sc2_icon('Seismic Spines (Lurker)') }}
+ {{ sc2_icon('Adapted Spines (Lurker)') }}
+
+
+ {{ sc2_icon('Aberration') }}
+
+
+ {{ sc2_icon('Swarm Host') }}
+ {{ sc2_icon('Carrion Strain (Swarm Host)') }}
+ {{ sc2_icon('Creeper Strain (Swarm Host)') }}
+ {{ sc2_icon('Burrow (Swarm Host)') }}
+ {{ sc2_icon('Rapid Incubation (Swarm Host)') }}
+ {{ sc2_icon('Pressurized Glands (Swarm Host)') }}
+ {{ sc2_icon('Locust Metabolic Boost (Swarm Host)') }}
+ {{ sc2_icon('Enduring Locusts (Swarm Host)') }}
+
+
+
+ {{ sc2_icon('Organic Carapace (Swarm Host)') }}
+ {{ sc2_icon('Resource Efficiency (Swarm Host)') }}
+
+
+ {{ sc2_icon('Infestor') }}
+ {{ sc2_icon('Infested Terran (Infestor)') }}
+ {{ sc2_icon('Microbial Shroud (Infestor)') }}
+
+
+ {{ sc2_icon('Defiler') }}
+
+
+ {{ sc2_icon('Ultralisk') }}
+ {{ sc2_icon('Noxious Strain (Ultralisk)') }}
+ {{ sc2_icon('Torrasque Strain (Ultralisk)') }}
+ {{ sc2_icon('Burrow Charge (Ultralisk)') }}
+ {{ sc2_icon('Tissue Assimilation (Ultralisk)') }}
+ {{ sc2_icon('Monarch Blades (Ultralisk)') }}
+ {{ sc2_icon('Anabolic Synthesis (Ultralisk)') }}
+ {{ sc2_icon('Chitinous Plating (Ultralisk)') }}
+
+
+
+ {{ sc2_icon('Organic Carapace (Ultralisk)') }}
+ {{ sc2_icon('Resource Efficiency (Ultralisk)') }}
+
+
+ {{ sc2_icon('Mutalisk') }}
+ {{ sc2_icon('Rapid Regeneration (Mutalisk)') }}
+ {{ sc2_icon('Sundering Glaive (Mutalisk)') }}
+ {{ sc2_icon('Vicious Glaive (Mutalisk)') }}
+ {{ sc2_icon('Severing Glaive (Mutalisk)') }}
+ {{ sc2_icon('Aerodynamic Glaive Shape (Mutalisk)') }}
+
+
+ {{ sc2_icon('Corruptor') }}
+ {{ sc2_icon('Corruption (Corruptor)') }}
+ {{ sc2_icon('Caustic Spray (Corruptor)') }}
+
+
+
+ {{ sc2_icon('Brood Lord Aspect (Mutalisk/Corruptor)') }}
+ {{ sc2_icon('Porous Cartilage (Brood Lord)') }}
+ {{ sc2_icon('Evolved Carapace (Brood Lord)') }}
+ {{ sc2_icon('Splitter Mitosis (Brood Lord)') }}
+ {{ sc2_icon('Resource Efficiency (Brood Lord)') }}
+
+
+
+ {{ sc2_icon('Viper Aspect (Mutalisk/Corruptor)') }}
+ {{ sc2_icon('Parasitic Bomb (Viper)') }}
+ {{ sc2_icon('Paralytic Barbs (Viper)') }}
+ {{ sc2_icon('Virulent Microbes (Viper)') }}
+
+
+
+ {{ sc2_icon('Guardian Aspect (Mutalisk/Corruptor)') }}
+ {{ sc2_icon('Prolonged Dispersion (Guardian)') }}
+ {{ sc2_icon('Primal Adaptation (Guardian)') }}
+ {{ sc2_icon('Soronan Acid (Guardian)') }}
+
+
+
+ {{ sc2_icon('Devourer Aspect (Mutalisk/Corruptor)') }}
+ {{ sc2_icon('Corrosive Spray (Devourer)') }}
+ {{ sc2_icon('Gaping Maw (Devourer)') }}
+ {{ sc2_icon('Improved Osmosis (Devourer)') }}
+ {{ sc2_icon('Prescient Spores (Devourer)') }}
+
+
+ {{ sc2_icon('Brood Queen') }}
+ {{ sc2_icon('Fungal Growth (Brood Queen)') }}
+ {{ sc2_icon('Ensnare (Brood Queen)') }}
+ {{ sc2_icon('Enhanced Mitochondria (Brood Queen)') }}
+
+
+ {{ sc2_icon('Scourge') }}
+ {{ sc2_icon('Virulent Spores (Scourge)') }}
+ {{ sc2_icon('Resource Efficiency (Scourge)') }}
+ {{ sc2_icon('Swarm Scourge (Scourge)') }}
+
+
+
+ Mercenaries
+
+
+
+ {{ sc2_icon('Infested Medics') }}
+ {{ sc2_icon('Infested Siege Tanks') }}
+ {{ sc2_icon('Infested Banshees') }}
+
+
+
+ Kerrigan
+
+
+
+ Level: {{ kerrigan_level }}
+ {{ sc2_icon('Primal Form (Kerrigan)') }}
+
+
+ {{ sc2_icon('Kinetic Blast (Kerrigan Tier 1)') }}
+ {{ sc2_icon('Heroic Fortitude (Kerrigan Tier 1)') }}
+ {{ sc2_icon('Leaping Strike (Kerrigan Tier 1)') }}
+
+ {{ sc2_icon('Crushing Grip (Kerrigan Tier 2)') }}
+ {{ sc2_icon('Chain Reaction (Kerrigan Tier 2)') }}
+ {{ sc2_icon('Psionic Shift (Kerrigan Tier 2)') }}
+
+
+ {{ sc2_icon('Wild Mutation (Kerrigan Tier 4)') }}
+ {{ sc2_icon('Spawn Banelings (Kerrigan Tier 4)') }}
+ {{ sc2_icon('Mend (Kerrigan Tier 4)') }}
+
+ {{ sc2_icon('Infest Broodlings (Kerrigan Tier 6)') }}
+ {{ sc2_icon('Fury (Kerrigan Tier 6)') }}
+ {{ sc2_icon('Ability Efficiency (Kerrigan Tier 6)') }}
+
+
+ {{ sc2_icon('Apocalypse (Kerrigan Tier 7)') }}
+ {{ sc2_icon('Spawn Leviathan (Kerrigan Tier 7)') }}
+ {{ sc2_icon('Drop-Pods (Kerrigan Tier 7)') }}
+
+
+
+
+
+
+
+ Protoss
+
+
+
+
+ Weapon & Armor Upgrades
+
+
+
+ {{ sc2_progressive_icon('Progressive Protoss Ground Weapon', protoss_ground_weapon_url, protoss_ground_weapon_level) }}
+ {{ sc2_progressive_icon('Progressive Protoss Ground Armor', protoss_ground_armor_url, protoss_ground_armor_level) }}
+
+ {{ sc2_progressive_icon('Progressive Protoss Air Weapon', protoss_air_weapon_url, protoss_air_weapon_level) }}
+ {{ sc2_progressive_icon('Progressive Protoss Air Armor', protoss_air_armor_url, protoss_air_armor_level) }}
+
+ {{ sc2_progressive_icon('Progressive Protoss Shields', protoss_shields_url, protoss_shields_level) }}
+
+ {{ sc2_icon('Quatro') }}
+
+
+
+ Base
+
+
+
+ {{ sc2_icon('Photon Cannon') }}
+ {{ sc2_icon('Khaydarin Monolith') }}
+ {{ sc2_icon('Shield Battery') }}
+ {{ sc2_icon('Enhanced Targeting') }}
+ {{ sc2_icon('Optimized Ordnance') }}
+ {{ sc2_icon('Khalai Ingenuity') }}
+
+ {{ sc2_icon('Orbital Assimilators') }}
+ {{ sc2_icon('Amplified Assimilators') }}
+
+
+ {{ sc2_icon('Warp Harmonization') }}
+ {{ sc2_icon('Superior Warp Gates') }}
+
+ {{ sc2_icon('Nexus Overcharge') }}
+
+
+
+ Gateway
+
+
+
+
+ {{ sc2_icon('Zealot') }}
+ {{ sc2_icon('Centurion') }}
+ {{ sc2_icon('Sentinel') }}
+ {{ sc2_icon('Leg Enhancements (Zealot/Sentinel/Centurion)') }}
+ {{ sc2_icon('Shield Capacity (Zealot/Sentinel/Centurion)') }}
+
+
+
+ {{ sc2_icon('Supplicant') }}
+ {{ sc2_icon('Blood Shield (Supplicant)') }}
+ {{ sc2_icon('Soul Augmentation (Supplicant)') }}
+ {{ sc2_icon('Shield Regeneration (Supplicant)') }}
+
+
+
+ {{ sc2_icon('Sentry') }}
+ {{ sc2_icon('Force Field (Sentry)') }}
+ {{ sc2_icon('Hallucination (Sentry)') }}
+
+
+
+ {{ sc2_icon('Energizer') }}
+ {{ sc2_icon('Reclamation (Energizer)') }}
+ {{ sc2_icon('Forged Chassis (Energizer)') }}
+
+ {{ sc2_icon('Cloaking Module (Sentry/Energizer/Havoc)') }}
+ {{ sc2_icon('Rapid Recharging (Sentry/Energizer/Havoc/Shield Battery)') }}
+
+
+
+ {{ sc2_icon('Havoc') }}
+ {{ sc2_icon('Detect Weakness (Havoc)') }}
+ {{ sc2_icon('Bloodshard Resonance (Havoc)') }}
+
+
+
+ {{ sc2_icon('Stalker') }}
+ {{ sc2_icon('Instigator') }}
+ {{ sc2_icon('Slayer') }}
+ {{ sc2_icon('Disintegrating Particles (Stalker/Instigator/Slayer)') }}
+ {{ sc2_icon('Particle Reflection (Stalker/Instigator/Slayer)') }}
+
+
+
+ {{ sc2_icon('Dragoon') }}
+ {{ sc2_icon('High Impact Phase Disruptor (Dragoon)') }}
+ {{ sc2_icon('Trillic Compression System (Dragoon)') }}
+ {{ sc2_icon('Singularity Charge (Dragoon)') }}
+ {{ sc2_icon('Enhanced Strider Servos (Dragoon)') }}
+
+
+
+ {{ sc2_icon('Adept') }}
+ {{ sc2_icon('Shockwave (Adept)') }}
+ {{ sc2_icon('Resonating Glaives (Adept)') }}
+ {{ sc2_icon('Phase Bulwark (Adept)') }}
+
+
+
+ {{ sc2_icon('High Templar') }}
+ {{ sc2_icon('Signifier') }}
+ {{ sc2_icon('Unshackled Psionic Storm (High Templar/Signifier)') }}
+ {{ sc2_icon('Hallucination (High Templar/Signifier)') }}
+ {{ sc2_icon('Khaydarin Amulet (High Templar/Signifier)') }}
+
+ {{ sc2_icon('High Archon (Archon)') }}
+
+
+
+ {{ sc2_icon('Ascendant') }}
+ {{ sc2_icon('Power Overwhelming (Ascendant)') }}
+ {{ sc2_icon('Chaotic Attunement (Ascendant)') }}
+ {{ sc2_icon('Blood Amulet (Ascendant)') }}
+
+
+
+ {{ sc2_icon('Dark Archon') }}
+ {{ sc2_icon('Feedback (Dark Archon)') }}
+ {{ sc2_icon('Maelstrom (Dark Archon)') }}
+ {{ sc2_icon('Argus Talisman (Dark Archon)') }}
+
+
+
+ {{ sc2_icon('Dark Templar') }}
+ {{ sc2_icon('Dark Archon Meld (Dark Templar)') }}
+
+
+
+ {{ sc2_icon('Avenger') }}
+ {{ sc2_icon('Blood Hunter') }}
+
+ {{ sc2_icon('Shroud of Adun (Dark Templar/Avenger/Blood Hunter)') }}
+ {{ sc2_icon('Shadow Guard Training (Dark Templar/Avenger/Blood Hunter)') }}
+ {{ sc2_icon('Blink (Dark Templar/Avenger/Blood Hunter)') }}
+ {{ sc2_icon('Resource Efficiency (Dark Templar/Avenger/Blood Hunter)') }}
+
+
+
+ Robotics Facility
+
+
+
+
+ {{ sc2_icon('Warp Prism') }}
+ {{ sc2_icon('Gravitic Drive (Warp Prism)') }}
+ {{ sc2_icon('Phase Blaster (Warp Prism)') }}
+ {{ sc2_icon('War Configuration (Warp Prism)') }}
+
+
+
+ {{ sc2_icon('Immortal') }}
+ {{ sc2_icon('Annihilator') }}
+ {{ sc2_icon('Singularity Charge (Immortal/Annihilator)') }}
+ {{ sc2_icon('Advanced Targeting Mechanics (Immortal/Annihilator)') }}
+
+
+
+ {{ sc2_icon('Vanguard') }}
+ {{ sc2_icon('Agony Launchers (Vanguard)') }}
+ {{ sc2_icon('Matter Dispersion (Vanguard)') }}
+
+
+
+ {{ sc2_icon('Colossus') }}
+ {{ sc2_icon('Pacification Protocol (Colossus)') }}
+
+
+
+ {{ sc2_icon('Wrathwalker') }}
+ {{ sc2_icon('Rapid Power Cycling (Wrathwalker)') }}
+ {{ sc2_icon('Eye of Wrath (Wrathwalker)') }}
+
+
+
+ {{ sc2_icon('Observer') }}
+ {{ sc2_icon('Gravitic Boosters (Observer)') }}
+ {{ sc2_icon('Sensor Array (Observer)') }}
+
+
+
+ {{ sc2_icon('Reaver') }}
+ {{ sc2_icon('Scarab Damage (Reaver)') }}
+ {{ sc2_icon('Solarite Payload (Reaver)') }}
+ {{ sc2_icon('Reaver Capacity (Reaver)') }}
+ {{ sc2_icon('Resource Efficiency (Reaver)') }}
+
+
+
+ {{ sc2_icon('Disruptor') }}
+
+
+
+ Stargate
+
+
+
+
+ {{ sc2_icon('Phoenix') }}
+ {{ sc2_icon('Mirage') }}
+ {{ sc2_icon('Ionic Wavelength Flux (Phoenix/Mirage)') }}
+ {{ sc2_icon('Anion Pulse-Crystals (Phoenix/Mirage)') }}
+
+
+
+ {{ sc2_icon('Corsair') }}
+ {{ sc2_icon('Stealth Drive (Corsair)') }}
+ {{ sc2_icon('Argus Jewel (Corsair)') }}
+ {{ sc2_icon('Sustaining Disruption (Corsair)') }}
+ {{ sc2_icon('Neutron Shields (Corsair)') }}
+
+
+
+ {{ sc2_icon('Destroyer') }}
+ {{ sc2_icon('Reforged Bloodshard Core (Destroyer)') }}
+
+
+
+ {{ sc2_icon('Void Ray') }}
+
+ {{ sc2_icon('Flux Vanes (Void Ray/Destroyer)') }}
+
+
+
+ {{ sc2_icon('Carrier') }}
+ {{ sc2_icon('Graviton Catapult (Carrier)') }}
+ {{ sc2_icon('Hull of Past Glories (Carrier)') }}
+
+
+
+ {{ sc2_icon('Scout') }}
+ {{ sc2_icon('Combat Sensor Array (Scout)') }}
+ {{ sc2_icon('Apial Sensors (Scout)') }}
+ {{ sc2_icon('Gravitic Thrusters (Scout)') }}
+ {{ sc2_icon('Advanced Photon Blasters (Scout)') }}
+
+
+
+ {{ sc2_icon('Tempest') }}
+ {{ sc2_icon('Tectonic Destabilizers (Tempest)') }}
+ {{ sc2_icon('Quantic Reactor (Tempest)') }}
+ {{ sc2_icon('Gravity Sling (Tempest)') }}
+
+
+
+ {{ sc2_icon('Mothership') }}
+
+
+
+ {{ sc2_icon('Arbiter') }}
+ {{ sc2_icon('Chronostatic Reinforcement (Arbiter)') }}
+ {{ sc2_icon('Khaydarin Core (Arbiter)') }}
+ {{ sc2_icon('Spacetime Anchor (Arbiter)') }}
+ {{ sc2_icon('Resource Efficiency (Arbiter)') }}
+ {{ sc2_icon('Enhanced Cloak Field (Arbiter)') }}
+
+
+
+ {{ sc2_icon('Oracle') }}
+ {{ sc2_icon('Stealth Drive (Oracle)') }}
+ {{ sc2_icon('Stasis Calibration (Oracle)') }}
+ {{ sc2_icon('Temporal Acceleration Beam (Oracle)') }}
+
+
+
+ General Upgrades
+
+
+
+ {{ sc2_icon('Matrix Overload') }}
+ {{ sc2_icon('Guardian Shell') }}
+
+
+
+ Spear of Adun
+
+
+
+ {{ sc2_icon('Chrono Surge (Spear of Adun Calldown)') }}
+ {{ sc2_progressive_icon_with_custom_name('Progressive Proxy Pylon (Spear of Adun Calldown)', proxy_pylon_spear_of_adun_calldown_url, proxy_pylon_spear_of_adun_calldown_name) }}
+ {{ sc2_icon('Pylon Overcharge (Spear of Adun Calldown)') }}
+
+ {{ sc2_icon('Mass Recall (Spear of Adun Calldown)') }}
+ {{ sc2_icon('Shield Overcharge (Spear of Adun Calldown)') }}
+ {{ sc2_icon('Deploy Fenix (Spear of Adun Calldown)') }}
+
+ {{ sc2_icon('Reconstruction Beam (Spear of Adun Auto-Cast)') }}
+
+
+ {{ sc2_icon('Orbital Strike (Spear of Adun Calldown)') }}
+ {{ sc2_icon('Temporal Field (Spear of Adun Calldown)') }}
+ {{ sc2_icon('Solar Lance (Spear of Adun Calldown)') }}
+
+ {{ sc2_icon('Purifier Beam (Spear of Adun Calldown)') }}
+ {{ sc2_icon('Time Stop (Spear of Adun Calldown)') }}
+ {{ sc2_icon('Solar Bombardment (Spear of Adun Calldown)') }}
+
+ {{ sc2_icon('Overwatch (Spear of Adun Auto-Cast)') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ sc2_loop_areas(0, 3) }}
+
+
+
+
+ {{ sc2_loop_areas(1, 3) }}
+
+
+
+
+ {{ sc2_loop_areas(2, 3) }}
+
+ {{ sc2_render_area('Total') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/WebHostLib/templates/tracker__SuperMetroid.html b/WebHostLib/templates/tracker__SuperMetroid.html
new file mode 100644
index 000000000000..0c648176513f
--- /dev/null
+++ b/WebHostLib/templates/tracker__SuperMetroid.html
@@ -0,0 +1,90 @@
+
+
+
+ {{ player_name }}'s Tracker
+
+
+
+
+
+ {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ energy_count }}
+
+
+
+
+
+
{{ reserve_count }}
+
+
+
+
+
+
{{ missile_count }}
+
+
+
+
+
+
{{ super_count }}
+
+
+
+
+
+
{{ power_count }}
+
+
+
+
+
+
+
+ {% for area in checks_done %}
+
+
+ {% for location in location_info[area] %}
+
+ {{ location }}
+ {{ '✔' if location_info[area][location] else '' }}
+
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
diff --git a/WebHostLib/templates/tracker__Timespinner.html b/WebHostLib/templates/tracker__Timespinner.html
new file mode 100644
index 000000000000..b118c3383344
--- /dev/null
+++ b/WebHostLib/templates/tracker__Timespinner.html
@@ -0,0 +1,122 @@
+
+
+
+ {{ player_name }}'s Tracker
+
+
+
+
+
+ {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if 'UnchainedKeys' in options %}
+ {% if 'EnterSandman' in options %}
+
+
+
+ {% endif %}
+
+
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+ {% if 'DownloadableItems' in options %}
+
+ {% endif %}
+
+
+ {% if 'DownloadableItems' in options %}
+
+ {% endif %}
+
+ {% if 'EyeSpy' in options %}
+
+ {% endif %}
+
+
+
+
+ {% if 'GyreArchives' in options %}
+
+
+ {% endif %}
+
+ {% if 'Djinn Inferno' in acquired_items %}
+
+ {% elif 'Pyro Ring' in acquired_items %}
+
+ {% elif 'Fire Orb' in acquired_items %}
+
+ {% elif 'Infernal Flames' in acquired_items %}
+
+ {% else %}
+
+ {% endif %}
+
+
+ {% if 'Royal Ring' in acquired_items %}
+
+ {% elif 'Plasma Geyser' in acquired_items %}
+
+ {% elif 'Plasma Orb' in acquired_items %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
+
+
+ {% for area in checks_done %}
+
+
+ {% for location in location_info[area] %}
+
+ {{ location }}
+ {{ '✔' if location_info[area][location] else '' }}
+
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
diff --git a/WebHostLib/templates/userContent.html b/WebHostLib/templates/userContent.html
index c20d39f46d82..3603d4112d20 100644
--- a/WebHostLib/templates/userContent.html
+++ b/WebHostLib/templates/userContent.html
@@ -25,6 +25,7 @@ Your Rooms
Players
Created (UTC)
Last Activity (UTC)
+ Mark for deletion
@@ -35,6 +36,7 @@ Your Rooms
{{ room.seed.slots|length }}
{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}
{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}
+ Delete next maintenance.
{% endfor %}
@@ -51,6 +53,7 @@ Your Seeds
Seed
Players
Created (UTC)
+ Mark for deletion
@@ -60,6 +63,7 @@ Your Seeds
{% if seed.multidata %}{{ seed.slots|length }}{% else %}1{% endif %}
{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}
+ Delete next maintenance.
{% endfor %}
diff --git a/WebHostLib/templates/viewSeed.html b/WebHostLib/templates/viewSeed.html
index e252fb06a28b..a8478c95c30d 100644
--- a/WebHostLib/templates/viewSeed.html
+++ b/WebHostLib/templates/viewSeed.html
@@ -34,7 +34,7 @@ Seed Info
{% endif %}
Rooms:
-
+
{% call macros.list_rooms(seed.rooms | selectattr("owner", "eq", session["_id"])) %}
Create New Room
diff --git a/WebHostLib/templates/weighted-settings.html b/WebHostLib/templates/weighted-settings.html
deleted file mode 100644
index 9ce097c37fb5..000000000000
--- a/WebHostLib/templates/weighted-settings.html
+++ /dev/null
@@ -1,48 +0,0 @@
-{% extends 'pageWrapper.html' %}
-
-{% block head %}
- {{ game }} Settings
-
-
-
-
-
-
-{% endblock %}
-
-{% block body %}
- {% include 'header/grassHeader.html' %}
-
-
-
Weighted Settings
-
Weighted Settings allows you to choose how likely a particular option is to be used in game generation.
- The higher an option is weighted, the more likely the option will be chosen. Think of them like
- entries in a raffle.
-
-
Choose the games and options you would like to play with! You may generate a single-player game from
- this page, or download a settings file you can use to participate in a MultiWorld.
-
-
A list of all games you have generated can be found on the User Content
- page.
-
-
Please enter your player name. This will appear in-game as you send and receive
- items if you are playing in a MultiWorld.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Export Settings
- Generate Game
- Generate Race
-
-
-{% endblock %}
diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html
new file mode 100644
index 000000000000..a1d319697154
--- /dev/null
+++ b/WebHostLib/templates/weightedOptions/macros.html
@@ -0,0 +1,264 @@
+{% macro Toggle(option_name, option) %}
+
+
+ {{ RangeRow(option_name, option, "No", "false", False, "true" if option.default else "false") }}
+ {{ RangeRow(option_name, option, "Yes", "true", False, "true" if option.default else "false") }}
+ {{ RandomRow(option_name, option) }}
+
+
+{% endmacro %}
+
+{% macro DefaultOnToggle(option_name, option) %}
+
+ {{ Toggle(option_name, option) }}
+{% endmacro %}
+
+{% macro Choice(option_name, option) %}
+
+
+ {% for id, name in option.name_lookup.items() %}
+ {% if name != 'random' %}
+ {% if option.default != 'random' %}
+ {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.default == id else None) }}
+ {% else %}
+ {{ RangeRow(option_name, option, option.get_option_name(id), name) }}
+ {% endif %}
+ {% endif %}
+ {% endfor %}
+ {{ RandomRow(option_name, option) }}
+
+
+{% endmacro %}
+
+{% macro Range(option_name, option) %}
+
+ This is a range option.
+
+ Accepted values:
+ Normal range: {{ option.range_start }} - {{ option.range_end }}
+ {% if option.special_range_names %}
+
+ The following values have special meanings, and may fall outside the normal range.
+
+ {% for name, value in option.special_range_names.items() %}
+ {{ value }}: {{ name|replace("_", " ")|title }}
+ {% endfor %}
+
+ {% endif %}
+
+
+ Add
+
+
+
+
+ {{ RangeRow(option_name, option, option.range_start, option.range_start, True) }}
+ {% if option.range_start < option.default < option.range_end %}
+ {{ RangeRow(option_name, option, option.default, option.default, True) }}
+ {% endif %}
+ {{ RangeRow(option_name, option, option.range_end, option.range_end, True) }}
+ {{ RandomRows(option_name, option) }}
+
+
+{% endmacro %}
+
+{% macro NamedRange(option_name, option) %}
+
+ {{ Range(option_name, option) }}
+{% endmacro %}
+
+{% macro FreeText(option_name, option) %}
+
+ This option allows custom values only. Please enter your desired values below.
+
+
+ Add
+
+
+
+ {% if option.default %}
+ {{ RangeRow(option_name, option, option.default, option.default) }}
+ {% endif %}
+
+
+
+{% endmacro %}
+
+{% macro TextChoice(option_name, option) %}
+
+ Custom values are also allowed for this option. To create one, enter it into the input box below.
+
+
+ Add
+
+
+
+
+ {% for id, name in option.name_lookup.items() %}
+ {% if name != 'random' %}
+ {% if option.default != 'random' %}
+ {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.default == id else None) }}
+ {% else %}
+ {{ RangeRow(option_name, option, option.get_option_name(id), name) }}
+ {% endif %}
+ {% endif %}
+ {% endfor %}
+ {{ RandomRow(option_name, option) }}
+
+
+{% endmacro %}
+
+{% macro PlandoBosses(option_name, option) %}
+
+ {{ TextChoice(option_name, option) }}
+{% endmacro %}
+
+{% macro ItemDict(option_name, option, world) %}
+
+ {% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
+
+ {{ item_name }}
+
+
+ {% endfor %}
+
+{% endmacro %}
+
+{% macro OptionList(option_name, option) %}
+
+ {% for key in (option.valid_keys if option.valid_keys is ordered else option.valid_keys|sort) %}
+
+
+
+ {{ key }}
+
+
+ {% endfor %}
+
+{% endmacro %}
+
+{% macro LocationSet(option_name, option, world) %}
+
+ {% for group_name in world.location_name_groups.keys()|sort %}
+ {% if group_name != "Everywhere" %}
+
+
+ {{ group_name }}
+
+ {% endif %}
+ {% endfor %}
+ {% if world.location_name_groups.keys()|length > 1 %}
+
+ {% endif %}
+ {% for location_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.location_names|sort) %}
+
+
+ {{ location_name }}
+
+ {% endfor %}
+
+{% endmacro %}
+
+{% macro ItemSet(option_name, option, world) %}
+
+ {% for group_name in world.item_name_groups.keys()|sort %}
+ {% if group_name != "Everything" %}
+
+
+ {{ group_name }}
+
+ {% endif %}
+ {% endfor %}
+ {% if world.item_name_groups.keys()|length > 1 %}
+
+ {% endif %}
+ {% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
+
+
+ {{ item_name }}
+
+ {% endfor %}
+
+{% endmacro %}
+
+{% macro OptionSet(option_name, option) %}
+
+ {% for key in (option.valid_keys if option.valid_keys is ordered else option.valid_keys|sort) %}
+
+
+ {{ key }}
+
+ {% endfor %}
+
+{% endmacro %}
+
+{% macro OptionTitleTd(option_name, value) %}
+
+
+ {{ option.display_name|default(option_name) }}
+
+
+{% endmacro %}
+
+{% macro RandomRow(option_name, option, extra_column=False) %}
+ {{ RangeRow(option_name, option, "Random", "random") }}
+{% endmacro %}
+
+{% macro RandomRows(option_name, option, extra_column=False) %}
+ {% for key, value in {"Random": "random", "Random (Low)": "random-low", "Random (Middle)": "random-middle", "Random (High)": "random-high"}.items() %}
+ {{ RangeRow(option_name, option, key, value) }}
+ {% endfor %}
+{% endmacro %}
+
+{% macro RangeRow(option_name, option, display_value, value, can_delete=False, default_override=None) %}
+
+
+
+ {{ display_value }}
+
+
+
+
+
+
+
+ {% if option.default == value or default_override == value %}
+ 25
+ {% else %}
+ 0
+ {% endif %}
+
+
+ {% if can_delete %}
+
+
+ âŒ
+
+
+ {% else %}
+
+ {% endif %}
+
+{% endmacro %}
diff --git a/WebHostLib/templates/weightedOptions/weightedOptions.html b/WebHostLib/templates/weightedOptions/weightedOptions.html
new file mode 100644
index 000000000000..b3aefd483535
--- /dev/null
+++ b/WebHostLib/templates/weightedOptions/weightedOptions.html
@@ -0,0 +1,119 @@
+{% extends 'pageWrapper.html' %}
+{% import 'weightedOptions/macros.html' as inputs %}
+
+{% block head %}
+ {{ world_name }} Weighted Options
+
+
+
+
+
+
+{% endblock %}
+
+{% block body %}
+ {% include 'header/'+theme+'Header.html' %}
+
+
+
+ This page has reduced functionality without JavaScript.
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py
index 4261c27e096f..75b5fb0202d9 100644
--- a/WebHostLib/tracker.py
+++ b/WebHostLib/tracker.py
@@ -1,1768 +1,2461 @@
-import collections
import datetime
-import typing
-from typing import Counter, Optional, Dict, Any, Tuple, List
+import collections
+from dataclasses import dataclass
+from typing import Any, Callable, Dict, List, Optional, Set, Tuple, NamedTuple, Counter
from uuid import UUID
+from email.utils import parsedate_to_datetime
-from flask import render_template
-from jinja2 import pass_context, runtime
+from flask import render_template, make_response, Response, request
from werkzeug.exceptions import abort
from MultiServer import Context, get_saving_second
-from NetUtils import SlotType, NetworkSlot
-from Utils import restricted_loads
-from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package, games
-from worlds.alttp import Items
+from NetUtils import ClientStatus, Hint, NetworkItem, NetworkSlot, SlotType
+from Utils import restricted_loads, KeyedDefaultDict
from . import app, cache
from .models import GameDataPackage, Room
-alttp_icons = {
- "Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
- "Red Shield": r"https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
- "Mirror Shield": r"https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png",
- "Fighter Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920",
- "Master Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920",
- "Tempered Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920",
- "Golden Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920",
- "Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c",
- "Silver Bow": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920",
- "Green Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920",
- "Blue Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920",
- "Red Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920",
- "Power Glove": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920",
- "Titan Mitts": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
- "Progressive Sword": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725",
- "Pegasus Boots": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9",
- "Progressive Glove": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
- "Flippers": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920",
- "Moon Pearl": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e",
- "Progressive Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed",
- "Blue Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e",
- "Red Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400",
- "Hookshot": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b",
- "Mushroom": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59",
- "Magic Powder": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=c24e38effbd4f80496d35830ce8ff4ec",
- "Fire Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0",
- "Ice Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc",
- "Bombos": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26",
- "Ether": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5",
- "Quake": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879",
- "Lamp": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce",
- "Hammer": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500",
- "Shovel": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05",
- "Flute": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390",
- "Bug Catching Net": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6",
- "Book of Mudora": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744",
- "Bottle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b",
- "Cane of Somaria": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943",
- "Cane of Byrna": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54",
- "Cape": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832",
- "Magic Mirror": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc",
- "Triforce": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48",
- "Small Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e",
- "Big Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d",
- "Chest": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda",
- "Light World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6",
- "Dark World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc",
- "Hyrule Castle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be",
- "Agahnims Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5",
- "Desert Palace": r"https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png",
- "Eastern Palace": r"https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png",
- "Tower of Hera": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7",
- "Palace of Darkness": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022",
- "Swamp Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5",
- "Skull Woods": r"https://alttp-wiki.net/images/6/6a/Mothula.png",
- "Thieves Town": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222",
- "Ice Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0",
- "Misery Mire": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8",
- "Turtle Rock": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be",
- "Ganons Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74"
-}
-
-
-def get_alttp_id(item_name):
- return Items.item_table[item_name][2]
-
-
-links = {"Bow": "Progressive Bow",
- "Silver Arrows": "Progressive Bow",
- "Silver Bow": "Progressive Bow",
- "Progressive Bow (Alt)": "Progressive Bow",
- "Bottle (Red Potion)": "Bottle",
- "Bottle (Green Potion)": "Bottle",
- "Bottle (Blue Potion)": "Bottle",
- "Bottle (Fairy)": "Bottle",
- "Bottle (Bee)": "Bottle",
- "Bottle (Good Bee)": "Bottle",
- "Fighter Sword": "Progressive Sword",
- "Master Sword": "Progressive Sword",
- "Tempered Sword": "Progressive Sword",
- "Golden Sword": "Progressive Sword",
- "Power Glove": "Progressive Glove",
- "Titans Mitts": "Progressive Glove"
- }
-
-levels = {"Fighter Sword": 1,
- "Master Sword": 2,
- "Tempered Sword": 3,
- "Golden Sword": 4,
- "Power Glove": 1,
- "Titans Mitts": 2,
- "Bow": 1,
- "Silver Bow": 2}
-
-multi_items = {get_alttp_id(name) for name in ("Progressive Sword", "Progressive Bow", "Bottle", "Progressive Glove")}
-links = {get_alttp_id(key): get_alttp_id(value) for key, value in links.items()}
-levels = {get_alttp_id(key): value for key, value in levels.items()}
-
-tracking_names = ["Progressive Sword", "Progressive Bow", "Book of Mudora", "Hammer",
- "Hookshot", "Magic Mirror", "Flute",
- "Pegasus Boots", "Progressive Glove", "Flippers", "Moon Pearl", "Blue Boomerang",
- "Red Boomerang", "Bug Catching Net", "Cape", "Shovel", "Lamp",
- "Mushroom", "Magic Powder",
- "Cane of Somaria", "Cane of Byrna", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake",
- "Bottle", "Triforce"]
-
-default_locations = {
- 'Light World': {1572864, 1572865, 60034, 1572867, 1572868, 60037, 1572869, 1572866, 60040, 59788, 60046, 60175,
- 1572880, 60049, 60178, 1572883, 60052, 60181, 1572885, 60055, 60184, 191256, 60058, 60187, 1572884,
- 1572886, 1572887, 1572906, 60202, 60205, 59824, 166320, 1010170, 60208, 60211, 60214, 60217, 59836,
- 60220, 60223, 59839, 1573184, 60226, 975299, 1573188, 1573189, 188229, 60229, 60232, 1573193,
- 1573194, 60235, 1573187, 59845, 59854, 211407, 60238, 59857, 1573185, 1573186, 1572882, 212328,
- 59881, 59761, 59890, 59770, 193020, 212605},
- 'Dark World': {59776, 59779, 975237, 1572870, 60043, 1572881, 60190, 60193, 60196, 60199, 60840, 1573190, 209095,
- 1573192, 1573191, 60241, 60244, 60247, 60250, 59884, 59887, 60019, 60022, 60028, 60031},
- 'Desert Palace': {1573216, 59842, 59851, 59791, 1573201, 59830},
- 'Eastern Palace': {1573200, 59827, 59893, 59767, 59833, 59773},
- 'Hyrule Castle': {60256, 60259, 60169, 60172, 59758, 59764, 60025, 60253},
- 'Agahnims Tower': {60082, 60085},
- 'Tower of Hera': {1573218, 59878, 59821, 1573202, 59896, 59899},
- 'Swamp Palace': {60064, 60067, 60070, 59782, 59785, 60073, 60076, 60079, 1573204, 60061},
- 'Thieves Town': {59905, 59908, 59911, 59914, 59917, 59920, 59923, 1573206},
- 'Skull Woods': {59809, 59902, 59848, 59794, 1573205, 59800, 59803, 59806},
- 'Ice Palace': {59872, 59875, 59812, 59818, 59860, 59797, 1573207, 59869},
- 'Misery Mire': {60001, 60004, 60007, 60010, 60013, 1573208, 59866, 59998},
- 'Turtle Rock': {59938, 59941, 59944, 1573209, 59947, 59950, 59953, 59956, 59926, 59929, 59932, 59935},
- 'Palace of Darkness': {59968, 59971, 59974, 59977, 59980, 59983, 59986, 1573203, 59989, 59959, 59992, 59962, 59995,
- 59965},
- 'Ganons Tower': {60160, 60163, 60166, 60088, 60091, 60094, 60097, 60100, 60103, 60106, 60109, 60112, 60115, 60118,
- 60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157},
- 'Total': set()}
-
-key_only_locations = {
- 'Light World': set(),
- 'Dark World': set(),
- 'Desert Palace': {0x140031, 0x14002b, 0x140061, 0x140028},
- 'Eastern Palace': {0x14005b, 0x140049},
- 'Hyrule Castle': {0x140037, 0x140034, 0x14000d, 0x14003d},
- 'Agahnims Tower': {0x140061, 0x140052},
- 'Tower of Hera': set(),
- 'Swamp Palace': {0x140019, 0x140016, 0x140013, 0x140010, 0x14000a},
- 'Thieves Town': {0x14005e, 0x14004f},
- 'Skull Woods': {0x14002e, 0x14001c},
- 'Ice Palace': {0x140004, 0x140022, 0x140025, 0x140046},
- 'Misery Mire': {0x140055, 0x14004c, 0x140064},
- 'Turtle Rock': {0x140058, 0x140007},
- 'Palace of Darkness': set(),
- 'Ganons Tower': {0x140040, 0x140043, 0x14003a, 0x14001f},
- 'Total': set()
-}
-
-location_to_area = {}
-for area, locations in default_locations.items():
- for location in locations:
- location_to_area[location] = area
-
-for area, locations in key_only_locations.items():
- for location in locations:
- location_to_area[location] = area
-
-checks_in_area = {area: len(checks) for area, checks in default_locations.items()}
-checks_in_area["Total"] = 216
-
-ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
- 'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
- 'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
-
-tracking_ids = []
-
-for item in tracking_names:
- tracking_ids.append(get_alttp_id(item))
-
-small_key_ids = {}
-big_key_ids = {}
-ids_small_key = {}
-ids_big_key = {}
-
-for item_name, data in Items.item_table.items():
- if "Key" in item_name:
- area = item_name.split("(")[1][:-1]
- if "Small" in item_name:
- small_key_ids[area] = data[2]
- ids_small_key[data[2]] = area
- else:
- big_key_ids[area] = data[2]
- ids_big_key[data[2]] = area
+# Multisave is currently updated, at most, every minute.
+TRACKER_CACHE_TIMEOUT_IN_SECONDS = 60
-# cleanup global namespace
-del item_name
-del data
-del item
+_multidata_cache = {}
+_multiworld_trackers: Dict[str, Callable] = {}
+_player_trackers: Dict[str, Callable] = {}
+TeamPlayer = Tuple[int, int]
+ItemMetadata = Tuple[int, int, int]
-def attribute_item_solo(inventory, item):
- """Adds item to inventory counter, converts everything to progressive."""
- target_item = links.get(item, item)
- if item in levels: # non-progressive
- inventory[target_item] = max(inventory[target_item], levels[item])
- else:
- inventory[target_item] += 1
+def _cache_results(func: Callable) -> Callable:
+ """Stores the results of any computationally expensive methods after the initial call in TrackerData.
+ If called again, returns the cached result instead, as results will not change for the lifetime of TrackerData.
+ """
+ def method_wrapper(self: "TrackerData", *args):
+ cache_key = f"{func.__name__}{''.join(f'_[{arg.__repr__()}]' for arg in args)}"
+ if cache_key in self._tracker_cache:
+ return self._tracker_cache[cache_key]
-@app.template_filter()
-def render_timedelta(delta: datetime.timedelta):
- hours, minutes = divmod(delta.total_seconds() / 60, 60)
- hours = str(int(hours))
- minutes = str(int(minutes)).zfill(2)
- return f"{hours}:{minutes}"
+ result = func(self, *args)
+ self._tracker_cache[cache_key] = result
+ return result
+ return method_wrapper
+
+
+@dataclass
+class TrackerData:
+ """A helper dataclass that is instantiated each time an HTTP request comes in for tracker data.
+
+ Provides helper methods to lazily load necessary data that each tracker require and caches any results so any
+ subsequent helper method calls do not need to recompute results during the lifetime of this instance.
+ """
+ room: Room
+ _multidata: Dict[str, Any]
+ _multisave: Dict[str, Any]
+ _tracker_cache: Dict[str, Any]
+
+ def __init__(self, room: Room):
+ """Initialize a new RoomMultidata object for the current room."""
+ self.room = room
+ self._multidata = Context.decompress(room.seed.multidata)
+ self._multisave = restricted_loads(room.multisave) if room.multisave else {}
+ self._tracker_cache = {}
+
+ self.item_name_to_id: Dict[str, Dict[str, int]] = {}
+ self.location_name_to_id: Dict[str, Dict[str, int]] = {}
+
+ # Generate inverse lookup tables from data package, useful for trackers.
+ self.item_id_to_name: Dict[str, Dict[int, str]] = KeyedDefaultDict(lambda game_name: {
+ game_name: KeyedDefaultDict(lambda code: f"Unknown Game {game_name} - Item (ID: {code})")
+ })
+ self.location_id_to_name: Dict[str, Dict[int, str]] = KeyedDefaultDict(lambda game_name: {
+ game_name: KeyedDefaultDict(lambda code: f"Unknown Game {game_name} - Location (ID: {code})")
+ })
+ for game, game_package in self._multidata["datapackage"].items():
+ game_package = restricted_loads(GameDataPackage.get(checksum=game_package["checksum"]).data)
+ self.item_id_to_name[game] = KeyedDefaultDict(lambda code: f"Unknown Item (ID: {code})", {
+ id: name for name, id in game_package["item_name_to_id"].items()})
+ self.location_id_to_name[game] = KeyedDefaultDict(lambda code: f"Unknown Location (ID: {code})", {
+ id: name for name, id in game_package["location_name_to_id"].items()})
+
+ # Normal lookup tables as well.
+ self.item_name_to_id[game] = game_package["item_name_to_id"]
+ self.location_name_to_id[game] = game_package["location_name_to_id"]
+
+ def get_seed_name(self) -> str:
+ """Retrieves the seed name."""
+ return self._multidata["seed_name"]
+
+ def get_slot_data(self, team: int, player: int) -> Dict[str, Any]:
+ """Retrieves the slot data for a given player."""
+ return self._multidata["slot_data"][player]
+
+ def get_slot_info(self, team: int, player: int) -> NetworkSlot:
+ """Retrieves the NetworkSlot data for a given player."""
+ return self._multidata["slot_info"][player]
+
+ def get_player_name(self, team: int, player: int) -> str:
+ """Retrieves the slot name for a given player."""
+ return self.get_slot_info(team, player).name
+
+ def get_player_game(self, team: int, player: int) -> str:
+ """Retrieves the game for a given player."""
+ return self.get_slot_info(team, player).game
+
+ def get_player_locations(self, team: int, player: int) -> Dict[int, ItemMetadata]:
+ """Retrieves all locations with their containing item's metadata for a given player."""
+ return self._multidata["locations"][player]
+
+ def get_player_starting_inventory(self, team: int, player: int) -> List[int]:
+ """Retrieves a list of all item codes a given slot starts with."""
+ return self._multidata["precollected_items"][player]
+
+ def get_player_checked_locations(self, team: int, player: int) -> Set[int]:
+ """Retrieves the set of all locations marked complete by this player."""
+ return self._multisave.get("location_checks", {}).get((team, player), set())
+
+ @_cache_results
+ def get_player_missing_locations(self, team: int, player: int) -> Set[int]:
+ """Retrieves the set of all locations not marked complete by this player."""
+ return set(self.get_player_locations(team, player)) - self.get_player_checked_locations(team, player)
+
+ def get_player_received_items(self, team: int, player: int) -> List[NetworkItem]:
+ """Returns all items received to this player in order of received."""
+ return self._multisave.get("received_items", {}).get((team, player, True), [])
+
+ @_cache_results
+ def get_player_inventory_counts(self, team: int, player: int) -> collections.Counter:
+ """Retrieves a dictionary of all items received by their id and their received count."""
+ received_items = self.get_player_received_items(team, player)
+ starting_items = self.get_player_starting_inventory(team, player)
+ inventory = collections.Counter()
+ for item in received_items:
+ inventory[item.item] += 1
+ for item in starting_items:
+ inventory[item] += 1
+
+ return inventory
+
+ @_cache_results
+ def get_player_hints(self, team: int, player: int) -> Set[Hint]:
+ """Retrieves a set of all hints relevant for a particular player."""
+ return self._multisave.get("hints", {}).get((team, player), set())
+
+ @_cache_results
+ def get_player_last_activity(self, team: int, player: int) -> Optional[datetime.timedelta]:
+ """Retrieves the relative timedelta for when a particular player was last active.
+ Returns None if no activity was ever recorded.
+ """
+ return self.get_room_last_activity().get((team, player), None)
+
+ def get_player_client_status(self, team: int, player: int) -> ClientStatus:
+ """Retrieves the ClientStatus of a particular player."""
+ return self._multisave.get("client_game_state", {}).get((team, player), ClientStatus.CLIENT_UNKNOWN)
+
+ def get_player_alias(self, team: int, player: int) -> Optional[str]:
+ """Returns the alias of a particular player, if any."""
+ return self._multisave.get("name_aliases", {}).get((team, player), None)
+
+ @_cache_results
+ def get_team_completed_worlds_count(self) -> Dict[int, int]:
+ """Retrieves a dictionary of number of completed worlds per team."""
+ return {
+ team: sum(
+ self.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL for player in players
+ ) for team, players in self.get_all_players().items()
+ }
-@pass_context
-def get_location_name(context: runtime.Context, loc: int) -> str:
- # once all rooms embed data package, the chain lookup can be dropped
- context_locations = context.get("custom_locations", {})
- return collections.ChainMap(context_locations, lookup_any_location_id_to_name).get(loc, loc)
+ @_cache_results
+ def get_team_hints(self) -> Dict[int, Set[Hint]]:
+ """Retrieves a dictionary of all hints per team."""
+ hints = {}
+ for team, players in self.get_all_slots().items():
+ hints[team] = set()
+ for player in players:
+ hints[team] |= self.get_player_hints(team, player)
+
+ return hints
+
+ @_cache_results
+ def get_team_locations_total_count(self) -> Dict[int, int]:
+ """Retrieves a dictionary of total player locations each team has."""
+ return {
+ team: sum(len(self.get_player_locations(team, player)) for player in players)
+ for team, players in self.get_all_players().items()
+ }
+ @_cache_results
+ def get_team_locations_checked_count(self) -> Dict[int, int]:
+ """Retrieves a dictionary of checked player locations each team has."""
+ return {
+ team: sum(len(self.get_player_checked_locations(team, player)) for player in players)
+ for team, players in self.get_all_players().items()
+ }
-@pass_context
-def get_item_name(context: runtime.Context, item: int) -> str:
- context_items = context.get("custom_items", {})
- return collections.ChainMap(context_items, lookup_any_item_id_to_name).get(item, item)
+ # TODO: Change this method to properly build for each team once teams are properly implemented, as they don't
+ # currently exist in multidata to easily look up, so these are all assuming only 1 team: Team #0
+ @_cache_results
+ def get_all_slots(self) -> Dict[int, List[int]]:
+ """Retrieves a dictionary of all players ids on each team."""
+ return {
+ 0: [
+ player for player, slot_info in self._multidata["slot_info"].items()
+ ]
+ }
+ # TODO: Change this method to properly build for each team once teams are properly implemented, as they don't
+ # currently exist in multidata to easily look up, so these are all assuming only 1 team: Team #0
+ @_cache_results
+ def get_all_players(self) -> Dict[int, List[int]]:
+ """Retrieves a dictionary of all player slot-type players ids on each team."""
+ return {
+ 0: [
+ player for player, slot_info in self._multidata["slot_info"].items()
+ if self.get_slot_info(0, player).type == SlotType.player
+ ]
+ }
-app.jinja_env.filters["location_name"] = get_location_name
-app.jinja_env.filters["item_name"] = get_item_name
+ @_cache_results
+ def get_room_saving_second(self) -> int:
+ """Retrieves the saving second value for this seed.
+ Useful for knowing when the multisave gets updated so trackers can attempt to update.
+ """
+ return get_saving_second(self.get_seed_name())
-_multidata_cache = {}
+ @_cache_results
+ def get_room_locations(self) -> Dict[TeamPlayer, Dict[int, ItemMetadata]]:
+ """Retrieves a dictionary of all locations and their associated item metadata per player."""
+ return {
+ (team, player): self.get_player_locations(team, player)
+ for team, players in self.get_all_players().items() for player in players
+ }
+ @_cache_results
+ def get_room_games(self) -> Dict[TeamPlayer, str]:
+ """Retrieves a dictionary of games for each player."""
+ return {
+ (team, player): self.get_player_game(team, player)
+ for team, players in self.get_all_slots().items() for player in players
+ }
-def get_location_table(checks_table: dict) -> dict:
- loc_to_area = {}
- for area, locations in checks_table.items():
- if area == "Total":
- continue
- for location in locations:
- loc_to_area[location] = area
- return loc_to_area
+ @_cache_results
+ def get_room_locations_complete(self) -> Dict[TeamPlayer, int]:
+ """Retrieves a dictionary of all locations complete per player."""
+ return {
+ (team, player): len(self.get_player_checked_locations(team, player))
+ for team, players in self.get_all_players().items() for player in players
+ }
+ @_cache_results
+ def get_room_client_statuses(self) -> Dict[TeamPlayer, ClientStatus]:
+ """Retrieves a dictionary of all ClientStatus values per player."""
+ return {
+ (team, player): self.get_player_client_status(team, player)
+ for team, players in self.get_all_players().items() for player in players
+ }
-def get_static_room_data(room: Room):
- result = _multidata_cache.get(room.seed.id, None)
- if result:
- return result
- multidata = Context.decompress(room.seed.multidata)
- # in > 100 players this can take a bit of time and is the main reason for the cache
- locations: Dict[int, Dict[int, Tuple[int, int, int]]] = multidata['locations']
- names: List[List[str]] = multidata.get("names", [])
- games = multidata.get("games", {})
- groups = {}
- custom_locations = {}
- custom_items = {}
- if "slot_info" in multidata:
- slot_info_dict: Dict[int, NetworkSlot] = multidata["slot_info"]
- games = {slot: slot_info.game for slot, slot_info in slot_info_dict.items()}
- groups = {slot: slot_info.group_members for slot, slot_info in slot_info_dict.items()
- if slot_info.type == SlotType.group}
- names = [[slot_info.name for slot, slot_info in sorted(slot_info_dict.items())]]
- for game in games.values():
- if game not in multidata["datapackage"]:
- continue
- game_data = multidata["datapackage"][game]
- if "checksum" in game_data:
- if network_data_package["games"].get(game, {}).get("checksum") == game_data["checksum"]:
- # non-custom. remove from multidata
- # network_data_package import could be skipped once all rooms embed data package
- del multidata["datapackage"][game]
- continue
+ @_cache_results
+ def get_room_long_player_names(self) -> Dict[TeamPlayer, str]:
+ """Retrieves a dictionary of names with aliases for each player."""
+ long_player_names = {}
+ for team, players in self.get_all_slots().items():
+ for player in players:
+ alias = self.get_player_alias(team, player)
+ if alias:
+ long_player_names[team, player] = f"{alias} ({self.get_player_name(team, player)})"
else:
- game_data = restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data)
- custom_locations.update(
- {id_: name for name, id_ in game_data["location_name_to_id"].items()})
- custom_items.update(
- {id_: name for name, id_ in game_data["item_name_to_id"].items()})
-
- seed_checks_in_area = checks_in_area.copy()
-
- use_door_tracker = False
- if "tags" in multidata:
- use_door_tracker = "DR" in multidata["tags"]
- if use_door_tracker:
- for area, checks in key_only_locations.items():
- seed_checks_in_area[area] += len(checks)
- seed_checks_in_area["Total"] = 249
-
- player_checks_in_area = {
- playernumber: {
- areaname: len(multidata["checks_in_area"][playernumber][areaname]) if areaname != "Total" else
- multidata["checks_in_area"][playernumber]["Total"]
- for areaname in ordered_areas
- }
- for playernumber in multidata["checks_in_area"]
- }
+ long_player_names[team, player] = self.get_player_name(team, player)
- player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber])
- for playernumber in multidata["checks_in_area"]}
- saving_second = get_saving_second(multidata["seed_name"])
- result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
- multidata["precollected_items"], games, multidata["slot_data"], groups, saving_second, \
- custom_locations, custom_items
- _multidata_cache[room.seed.id] = result
- return result
-
-
-@app.route('/tracker///')
-def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False):
- key = f"{tracker}_{tracked_team}_{tracked_player}_{want_generic}"
- tracker_page = cache.get(key)
- if tracker_page:
- return tracker_page
- timeout, tracker_page = _get_player_tracker(tracker, tracked_team, tracked_player, want_generic)
- cache.set(key, tracker_page, timeout)
- return tracker_page
-
-
-def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool):
- # Team and player must be positive and greater than zero
- if tracked_team < 0 or tracked_player < 1:
- abort(404)
+ return long_player_names
+
+ @_cache_results
+ def get_room_last_activity(self) -> Dict[TeamPlayer, datetime.timedelta]:
+ """Retrieves a dictionary of all players and the timedelta from now to their last activity.
+ Does not include players who have no activity recorded.
+ """
+ last_activity: Dict[TeamPlayer, datetime.timedelta] = {}
+ now = datetime.datetime.utcnow()
+ for (team, player), timestamp in self._multisave.get("client_activity_timers", []):
+ last_activity[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
+
+ return last_activity
+
+ @_cache_results
+ def get_room_videos(self) -> Dict[TeamPlayer, Tuple[str, str]]:
+ """Retrieves a dictionary of any players who have video streaming enabled and their feeds.
+
+ Only supported platforms are Twitch and YouTube.
+ """
+ video_feeds = {}
+ for (team, player), video_data in self._multisave.get("video", []):
+ video_feeds[team, player] = video_data
+
+ return video_feeds
+
+ @_cache_results
+ def get_spheres(self) -> List[List[int]]:
+ """ each sphere is { player: { location_id, ... } } """
+ return self._multidata.get("spheres", [])
- room: Optional[Room] = Room.get(tracker=tracker)
+
+def _process_if_request_valid(incoming_request, room: Optional[Room]) -> Optional[Response]:
if not room:
abort(404)
- # Collect seed information and pare it down to a single player
- locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
- precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
- get_static_room_data(room)
- player_name = names[tracked_team][tracked_player - 1]
- location_to_area = player_location_to_area.get(tracked_player, {})
- inventory = collections.Counter()
- checks_done = {loc_name: 0 for loc_name in default_locations}
-
- # Add starting items to inventory
- starting_items = precollected_items[tracked_player]
- if starting_items:
- for item_id in starting_items:
- attribute_item_solo(inventory, item_id)
-
- if room.multisave:
- multisave: Dict[str, Any] = restricted_loads(room.multisave)
- else:
- multisave: Dict[str, Any] = {}
-
- slots_aimed_at_player = {tracked_player}
- for group_id, group_members in groups.items():
- if tracked_player in group_members:
- slots_aimed_at_player.add(group_id)
-
- # Add items to player inventory
- for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}).items():
- # Skip teams and players not matching the request
- player_locations = locations[ms_player]
- if ms_team == tracked_team:
- # If the player does not have the item, do nothing
- for location in locations_checked:
- if location in player_locations:
- item, recipient, flags = player_locations[location]
- if recipient in slots_aimed_at_player: # a check done for the tracked player
- attribute_item_solo(inventory, item)
- if ms_player == tracked_player: # a check done by the tracked player
- area_name = location_to_area.get(location, None)
- if area_name:
- checks_done[area_name] += 1
- checks_done["Total"] += 1
- specific_tracker = game_specific_trackers.get(games[tracked_player], None)
- if specific_tracker and not want_generic:
- tracker = specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
- seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second)
+ if_modified = incoming_request.headers.get("If-Modified-Since", None)
+ if if_modified:
+ if_modified = parsedate_to_datetime(if_modified)
+ # if_modified has less precision than last_activity, so we bring them to same precision
+ if if_modified >= room.last_activity.replace(microsecond=0):
+ return make_response("", 304)
+
+
+@app.route("/tracker///")
+def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> Response:
+ key = f"{tracker}_{tracked_team}_{tracked_player}_{generic}"
+ response: Optional[Response] = cache.get(key)
+ if response:
+ return response
+
+ # Room must exist.
+ room = Room.get(tracker=tracker)
+
+ response = _process_if_request_valid(request, room)
+ if response:
+ return response
+
+ timeout, last_modified, tracker_page = get_timeout_and_player_tracker(room, tracked_team, tracked_player, generic)
+ response = make_response(tracker_page)
+ response.last_modified = last_modified
+ cache.set(key, response, timeout)
+ return response
+
+
+def get_timeout_and_player_tracker(room: Room, tracked_team: int, tracked_player: int, generic: bool)\
+ -> Tuple[int, datetime.datetime, str]:
+ tracker_data = TrackerData(room)
+
+ # Load and render the game-specific player tracker, or fallback to generic tracker if none exists.
+ game_specific_tracker = _player_trackers.get(tracker_data.get_player_game(tracked_team, tracked_player), None)
+ if game_specific_tracker and not generic:
+ tracker = game_specific_tracker(tracker_data, tracked_team, tracked_player)
else:
- tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player,
- player_name, seed_checks_in_area, checks_done, saving_second,
- custom_locations, custom_items)
+ tracker = render_generic_tracker(tracker_data, tracked_team, tracked_player)
- return (saving_second - datetime.datetime.now().second) % 60 or 60, tracker
+ return ((tracker_data.get_room_saving_second() - datetime.datetime.now().second)
+ % TRACKER_CACHE_TIMEOUT_IN_SECONDS or TRACKER_CACHE_TIMEOUT_IN_SECONDS, room.last_activity, tracker)
-@app.route('/generic_tracker///')
-def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
+@app.route("/generic_tracker///")
+def get_generic_game_tracker(tracker: UUID, tracked_team: int, tracked_player: int) -> Response:
return get_player_tracker(tracker, tracked_team, tracked_player, True)
-def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
- inventory: Counter, team: int, player: int, player_name: str,
- seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict,
- saving_second: int) -> str:
+@app.route("/tracker/", defaults={"game": "Generic"})
+@app.route("/tracker//")
+def get_multiworld_tracker(tracker: UUID, game: str) -> Response:
+ key = f"{tracker}_{game}"
+ response: Optional[Response] = cache.get(key)
+ if response:
+ return response
- # Note the presence of the triforce item
- game_state = multisave.get("client_game_state", {}).get((team, player), 0)
- if game_state == 30:
- inventory[106] = 1 # Triforce
+ # Room must exist.
+ room = Room.get(tracker=tracker)
- # Progressive items need special handling for icons and class
- progressive_items = {
- "Progressive Sword": 94,
- "Progressive Glove": 97,
- "Progressive Bow": 100,
- "Progressive Mail": 96,
- "Progressive Shield": 95,
- }
- progressive_names = {
- "Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
- "Progressive Glove": [None, 'Power Glove', 'Titan Mitts'],
- "Progressive Bow": [None, "Bow", "Silver Bow"],
- "Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
- "Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"]
- }
+ response = _process_if_request_valid(request, room)
+ if response:
+ return response
- # Determine which icon to use
- display_data = {}
- for item_name, item_id in progressive_items.items():
- level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
- display_name = progressive_names[item_name][level]
- acquired = True
- if not display_name:
- acquired = False
- display_name = progressive_names[item_name][level + 1]
- base_name = item_name.split(maxsplit=1)[1].lower()
- display_data[base_name + "_acquired"] = acquired
- display_data[base_name + "_url"] = alttp_icons[display_name]
-
- # The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
- sp_areas = ordered_areas[2:15]
-
- player_big_key_locations = set()
- player_small_key_locations = set()
- for loc_data in locations.values():
- for values in loc_data.values():
- item_id, item_player, flags = values
- if item_player == player:
- if item_id in ids_big_key:
- player_big_key_locations.add(ids_big_key[item_id])
- elif item_id in ids_small_key:
- player_small_key_locations.add(ids_small_key[item_id])
-
- return render_template("lttpTracker.html", inventory=inventory,
- player_name=player_name, room=room, icons=alttp_icons, checks_done=checks_done,
- checks_in_area=seed_checks_in_area[player],
- acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
- small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
- key_locations=player_small_key_locations,
- big_key_locations=player_big_key_locations,
- **display_data)
-
-
-def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
- inventory: Counter, team: int, player: int, playerName: str,
- seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict,
- saving_second: int) -> str:
-
- icons = {
- "Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
- "Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png",
- "Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png",
- "Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png",
- "Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png",
- "Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png",
- "Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png",
- "Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png",
- "Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png",
- "Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png",
- "Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png",
- "Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png",
- "Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png",
- "Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png",
- "Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png",
- "Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
- "Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
- "Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
- "Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png",
- "Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
- "Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
- "Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
- "Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png",
- "Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
- "Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
- "Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
- "Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png",
- "Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png",
- "Saddle": "https://i.imgur.com/2QtDyR0.png",
- "Channeling Book": "https://i.imgur.com/J3WsYZw.png",
- "Silk Touch Book": "https://i.imgur.com/iqERxHQ.png",
- "Piercing IV Book": "https://i.imgur.com/OzJptGz.png",
- }
+ timeout, last_modified, tracker_page = get_timeout_and_multiworld_tracker(room, game)
+ response = make_response(tracker_page)
+ response.last_modified = last_modified
+ cache.set(key, response, timeout)
+ return response
- minecraft_location_ids = {
- "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070,
- 42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077],
- "Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
- 42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014],
- "The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046],
- "Adventure": [42047, 42050, 42096, 42097, 42098, 42059, 42055, 42072, 42003, 42109, 42035, 42016, 42020,
- 42048, 42054, 42068, 42043, 42106, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42105, 42099, 42103, 42110, 42100],
- "Husbandry": [42065, 42067, 42078, 42022, 42113, 42107, 42007, 42079, 42013, 42028, 42036, 42108, 42111, 42112,
- 42057, 42063, 42053, 42102, 42101, 42092, 42093, 42094, 42095],
- "Archipelago": [42080, 42081, 42082, 42083, 42084, 42085, 42086, 42087, 42088, 42089, 42090, 42091],
- }
- display_data = {}
+def get_timeout_and_multiworld_tracker(room: Room, game: str)\
+ -> Tuple[int, datetime.datetime, str]:
+ tracker_data = TrackerData(room)
+ enabled_trackers = list(get_enabled_multiworld_trackers(room).keys())
+ if game in _multiworld_trackers:
+ tracker = _multiworld_trackers[game](tracker_data, enabled_trackers)
+ else:
+ tracker = render_generic_multiworld_tracker(tracker_data, enabled_trackers)
- # Determine display for progressive items
- progressive_items = {
- "Progressive Tools": 45013,
- "Progressive Weapons": 45012,
- "Progressive Armor": 45014,
- "Progressive Resource Crafting": 45001
- }
- progressive_names = {
- "Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"],
- "Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"],
- "Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"],
- "Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"]
- }
- for item_name, item_id in progressive_items.items():
- level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
- display_name = progressive_names[item_name][level]
- base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
- display_data[base_name + "_url"] = icons[display_name]
-
- # Multi-items
- multi_items = {
- "3 Ender Pearls": 45029,
- "8 Netherite Scrap": 45015,
- "Dragon Egg Shard": 45043
- }
- for item_name, item_id in multi_items.items():
- base_name = item_name.split()[-1].lower()
- count = inventory[item_id]
- if count >= 0:
- display_data[base_name + "_count"] = count
+ return ((tracker_data.get_room_saving_second() - datetime.datetime.now().second)
+ % TRACKER_CACHE_TIMEOUT_IN_SECONDS or TRACKER_CACHE_TIMEOUT_IN_SECONDS, room.last_activity, tracker)
- # Victory condition
- game_state = multisave.get("client_game_state", {}).get((team, player), 0)
- display_data['game_finished'] = game_state == 30
-
- # Turn location IDs into advancement tab counts
- checked_locations = multisave.get("location_checks", {}).get((team, player), set())
- lookup_name = lambda id: lookup_any_location_id_to_name[id]
- location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
- for tab_name, tab_locations in minecraft_location_ids.items()}
- checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
- for tab_name, tab_locations in minecraft_location_ids.items()}
- checks_done['Total'] = len(checked_locations)
- checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()}
- checks_in_area['Total'] = sum(checks_in_area.values())
-
- return render_template("minecraftTracker.html",
- inventory=inventory, icons=icons,
- acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
- id in lookup_any_item_id_to_name},
- player=player, team=team, room=room, player_name=playerName, saving_second = saving_second,
- checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
- **display_data)
-
-
-def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
- inventory: Counter, team: int, player: int, playerName: str,
- seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict,
- saving_second: int) -> str:
-
- icons = {
- "Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
- "Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png",
- "Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png",
- "Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png",
- "Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png",
- "Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png",
- "Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png",
- "Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png",
- "Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png",
- "Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png",
- "Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png",
- "Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png",
- "Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png",
- "Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png",
- "Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png",
- "Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png",
- "Dins Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png",
- "Farores Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png",
- "Nayrus Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png",
- "Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png",
- "Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png",
- "Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png",
- "Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png",
- "Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png",
- "Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png",
- "Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png",
- "Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png",
- "Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png",
- "Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png",
- "Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png",
- "Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png",
- "Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png",
- "Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png",
- "Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png",
- "Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png",
- "Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png",
- "Gold Skulltula Token": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png",
- "Triforce Piece": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png",
- "Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png",
- "Zeldas Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
- "Eponas Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
- "Sarias Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
- "Suns Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
- "Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
- "Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
- "Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png",
- "Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png",
- "Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png",
- "Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png",
- "Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png",
- "Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png",
- "Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png",
- "Boss Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png",
- }
- display_data = {}
+def get_enabled_multiworld_trackers(room: Room) -> Dict[str, Callable]:
+ # Render the multitracker for any games that exist in the current room if they are defined.
+ enabled_trackers = {}
+ for game_name, endpoint in _multiworld_trackers.items():
+ if any(slot.game == game_name for slot in room.seed.slots):
+ enabled_trackers[game_name] = endpoint
- # Determine display for progressive items
- progressive_items = {
- "Progressive Hookshot": 66128,
- "Progressive Strength Upgrade": 66129,
- "Progressive Wallet": 66133,
- "Progressive Scale": 66134,
- "Magic Meter": 66138,
- "Ocarina": 66139,
+ # We resort the tracker to have Generic first, then lexicographically each enabled game.
+ return {
+ "Generic": render_generic_multiworld_tracker,
+ **{key: enabled_trackers[key] for key in sorted(enabled_trackers.keys())},
}
- progressive_names = {
- "Progressive Hookshot": ["Hookshot", "Hookshot", "Longshot"],
- "Progressive Strength Upgrade": ["Goron Bracelet", "Goron Bracelet", "Silver Gauntlets", "Golden Gauntlets"],
- "Progressive Wallet": ["Adults Wallet", "Adults Wallet", "Giants Wallet", "Giants Wallet"],
- "Progressive Scale": ["Silver Scale", "Silver Scale", "Gold Scale"],
- "Magic Meter": ["Small Magic", "Small Magic", "Large Magic"],
- "Ocarina": ["Fairy Ocarina", "Fairy Ocarina", "Ocarina of Time"]
- }
- for item_name, item_id in progressive_items.items():
- level = min(inventory[item_id], len(progressive_names[item_name])-1)
- display_name = progressive_names[item_name][level]
- if item_name.startswith("Progressive"):
- base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
- else:
- base_name = item_name.lower().replace(' ', '_')
- display_data[base_name+"_url"] = icons[display_name]
-
- if base_name == "hookshot":
- display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level)
- if base_name == "wallet":
- display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level)
-
- # Determine display for bottles. Show letter if it's obtained, determine bottle count
- bottle_ids = [66015, 66020, 66021, 66140, 66141, 66142, 66143, 66144, 66145, 66146, 66147, 66148]
- display_data['bottle_count'] = min(sum(map(lambda item_id: inventory[item_id], bottle_ids)), 4)
- display_data['bottle_url'] = icons['Rutos Letter'] if inventory[66021] > 0 else icons['Bottle']
-
- # Determine bombchu display
- display_data['has_bombchus'] = any(map(lambda item_id: inventory[item_id] > 0, [66003, 66106, 66107, 66137]))
-
- # Multi-items
- multi_items = {
- "Gold Skulltula Token": 66091,
- "Triforce Piece": 66202,
- }
- for item_name, item_id in multi_items.items():
- base_name = item_name.split()[-1].lower()
- display_data[base_name+"_count"] = inventory[item_id]
-
- # Gather dungeon locations
- area_id_ranges = {
- "Overworld": ((67000, 67263), (67269, 67280), (67747, 68024), (68054, 68062)),
- "Deku Tree": ((67281, 67303), (68063, 68077)),
- "Dodongo's Cavern": ((67304, 67334), (68078, 68160)),
- "Jabu Jabu's Belly": ((67335, 67359), (68161, 68188)),
- "Bottom of the Well": ((67360, 67384), (68189, 68230)),
- "Forest Temple": ((67385, 67420), (68231, 68281)),
- "Fire Temple": ((67421, 67457), (68282, 68350)),
- "Water Temple": ((67458, 67484), (68351, 68483)),
- "Shadow Temple": ((67485, 67532), (68484, 68565)),
- "Spirit Temple": ((67533, 67582), (68566, 68625)),
- "Ice Cavern": ((67583, 67596), (68626, 68649)),
- "Gerudo Training Ground": ((67597, 67635), (68650, 68656)),
- "Thieves' Hideout": ((67264, 67268), (68025, 68053)),
- "Ganon's Castle": ((67636, 67673), (68657, 68705)),
- }
+def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
+ game = tracker_data.get_player_game(team, player)
+
+ received_items_in_order = {}
+ starting_inventory = tracker_data.get_player_starting_inventory(team, player)
+ for index, item in enumerate(starting_inventory):
+ received_items_in_order[item] = index
+ for index, network_item in enumerate(tracker_data.get_player_received_items(team, player),
+ start=len(starting_inventory)):
+ received_items_in_order[network_item.item] = index
+
+ return render_template(
+ template_name_or_list="genericTracker.html",
+ game_specific_tracker=game in _player_trackers,
+ room=tracker_data.room,
+ team=team,
+ player=player,
+ player_name=tracker_data.get_room_long_player_names()[team, player],
+ inventory=tracker_data.get_player_inventory_counts(team, player),
+ locations=tracker_data.get_player_locations(team, player),
+ checked_locations=tracker_data.get_player_checked_locations(team, player),
+ received_items=received_items_in_order,
+ saving_second=tracker_data.get_room_saving_second(),
+ game=game,
+ games=tracker_data.get_room_games(),
+ player_names_with_alias=tracker_data.get_room_long_player_names(),
+ location_id_to_name=tracker_data.location_id_to_name,
+ item_id_to_name=tracker_data.item_id_to_name,
+ hints=tracker_data.get_player_hints(team, player),
+ )
- def lookup_and_trim(id, area):
- full_name = lookup_any_location_id_to_name[id]
- if 'Ganons Tower' in full_name:
- return full_name
- if area not in ["Overworld", "Thieves' Hideout"]:
- # trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC
- return full_name[len(area):]
- return full_name
-
- checked_locations = multisave.get("location_checks", {}).get((team, player), set()).intersection(set(locations[player]))
- location_info = {}
- checks_done = {}
- checks_in_area = {}
- for area, ranges in area_id_ranges.items():
- location_info[area] = {}
- checks_done[area] = 0
- checks_in_area[area] = 0
- for r in ranges:
- min_id, max_id = r
- for id in range(min_id, max_id+1):
- if id in locations[player]:
- checked = id in checked_locations
- location_info[area][lookup_and_trim(id, area)] = checked
- checks_in_area[area] += 1
- checks_done[area] += checked
-
- checks_done['Total'] = sum(checks_done.values())
- checks_in_area['Total'] = sum(checks_in_area.values())
-
- # Give skulltulas on non-tracked locations
- non_tracked_locations = multisave.get("location_checks", {}).get((team, player), set()).difference(set(locations[player]))
- for id in non_tracked_locations:
- if "GS" in lookup_and_trim(id, ''):
- display_data["token_count"] += 1
-
- oot_y = '✔'
- oot_x = '✕'
-
- # Gather small and boss key info
- small_key_counts = {
- "Forest Temple": oot_y if inventory[66203] else inventory[66175],
- "Fire Temple": oot_y if inventory[66204] else inventory[66176],
- "Water Temple": oot_y if inventory[66205] else inventory[66177],
- "Spirit Temple": oot_y if inventory[66206] else inventory[66178],
- "Shadow Temple": oot_y if inventory[66207] else inventory[66179],
- "Bottom of the Well": oot_y if inventory[66208] else inventory[66180],
- "Gerudo Training Ground": oot_y if inventory[66209] else inventory[66181],
- "Thieves' Hideout": oot_y if inventory[66210] else inventory[66182],
- "Ganon's Castle": oot_y if inventory[66211] else inventory[66183],
- }
- boss_key_counts = {
- "Forest Temple": oot_y if inventory[66149] else oot_x,
- "Fire Temple": oot_y if inventory[66150] else oot_x,
- "Water Temple": oot_y if inventory[66151] else oot_x,
- "Spirit Temple": oot_y if inventory[66152] else oot_x,
- "Shadow Temple": oot_y if inventory[66153] else oot_x,
- "Ganon's Castle": oot_y if inventory[66154] else oot_x,
- }
- # Victory condition
- game_state = multisave.get("client_game_state", {}).get((team, player), 0)
- display_data['game_finished'] = game_state == 30
-
- return render_template("ootTracker.html",
- inventory=inventory, player=player, team=team, room=room, player_name=playerName,
- icons=icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
- checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
- small_key_counts=small_key_counts, boss_key_counts=boss_key_counts,
- **display_data)
-
-
-def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
- inventory: Counter, team: int, player: int, playerName: str,
- seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int],
- slot_data: Dict[str, Any], saving_second: int) -> str:
-
- icons = {
- "Timespinner Wheel": "https://timespinnerwiki.com/mediawiki/images/7/76/Timespinner_Wheel.png",
- "Timespinner Spindle": "https://timespinnerwiki.com/mediawiki/images/1/1a/Timespinner_Spindle.png",
- "Timespinner Gear 1": "https://timespinnerwiki.com/mediawiki/images/3/3c/Timespinner_Gear_1.png",
- "Timespinner Gear 2": "https://timespinnerwiki.com/mediawiki/images/e/e9/Timespinner_Gear_2.png",
- "Timespinner Gear 3": "https://timespinnerwiki.com/mediawiki/images/2/22/Timespinner_Gear_3.png",
- "Talaria Attachment": "https://timespinnerwiki.com/mediawiki/images/6/61/Talaria_Attachment.png",
- "Succubus Hairpin": "https://timespinnerwiki.com/mediawiki/images/4/49/Succubus_Hairpin.png",
- "Lightwall": "https://timespinnerwiki.com/mediawiki/images/0/03/Lightwall.png",
- "Celestial Sash": "https://timespinnerwiki.com/mediawiki/images/f/f1/Celestial_Sash.png",
- "Twin Pyramid Key": "https://timespinnerwiki.com/mediawiki/images/4/49/Twin_Pyramid_Key.png",
- "Security Keycard D": "https://timespinnerwiki.com/mediawiki/images/1/1b/Security_Keycard_D.png",
- "Security Keycard C": "https://timespinnerwiki.com/mediawiki/images/e/e5/Security_Keycard_C.png",
- "Security Keycard B": "https://timespinnerwiki.com/mediawiki/images/f/f6/Security_Keycard_B.png",
- "Security Keycard A": "https://timespinnerwiki.com/mediawiki/images/b/b9/Security_Keycard_A.png",
- "Library Keycard V": "https://timespinnerwiki.com/mediawiki/images/5/50/Library_Keycard_V.png",
- "Tablet": "https://timespinnerwiki.com/mediawiki/images/a/a0/Tablet.png",
- "Elevator Keycard": "https://timespinnerwiki.com/mediawiki/images/5/55/Elevator_Keycard.png",
- "Oculus Ring": "https://timespinnerwiki.com/mediawiki/images/8/8d/Oculus_Ring.png",
- "Water Mask": "https://timespinnerwiki.com/mediawiki/images/0/04/Water_Mask.png",
- "Gas Mask": "https://timespinnerwiki.com/mediawiki/images/2/2e/Gas_Mask.png",
- "Djinn Inferno": "https://timespinnerwiki.com/mediawiki/images/f/f6/Djinn_Inferno.png",
- "Pyro Ring": "https://timespinnerwiki.com/mediawiki/images/2/2c/Pyro_Ring.png",
- "Infernal Flames": "https://timespinnerwiki.com/mediawiki/images/1/1f/Infernal_Flames.png",
- "Fire Orb": "https://timespinnerwiki.com/mediawiki/images/3/3e/Fire_Orb.png",
- "Royal Ring": "https://timespinnerwiki.com/mediawiki/images/f/f3/Royal_Ring.png",
- "Plasma Geyser": "https://timespinnerwiki.com/mediawiki/images/1/12/Plasma_Geyser.png",
- "Plasma Orb": "https://timespinnerwiki.com/mediawiki/images/4/44/Plasma_Orb.png",
- "Kobo": "https://timespinnerwiki.com/mediawiki/images/c/c6/Familiar_Kobo.png",
- "Merchant Crow": "https://timespinnerwiki.com/mediawiki/images/4/4e/Familiar_Crow.png",
- }
+def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_trackers: List[str]) -> str:
+ return render_template(
+ "multitracker.html",
+ enabled_trackers=enabled_trackers,
+ current_tracker="Generic",
+ room=tracker_data.room,
+ all_slots=tracker_data.get_all_slots(),
+ room_players=tracker_data.get_all_players(),
+ locations=tracker_data.get_room_locations(),
+ locations_complete=tracker_data.get_room_locations_complete(),
+ total_team_locations=tracker_data.get_team_locations_total_count(),
+ total_team_locations_complete=tracker_data.get_team_locations_checked_count(),
+ player_names_with_alias=tracker_data.get_room_long_player_names(),
+ completed_worlds=tracker_data.get_team_completed_worlds_count(),
+ games=tracker_data.get_room_games(),
+ states=tracker_data.get_room_client_statuses(),
+ hints=tracker_data.get_team_hints(),
+ activity_timers=tracker_data.get_room_last_activity(),
+ videos=tracker_data.get_room_videos(),
+ item_id_to_name=tracker_data.item_id_to_name,
+ location_id_to_name=tracker_data.location_id_to_name,
+ saving_second=tracker_data.get_room_saving_second(),
+ )
- timespinner_location_ids = {
- "Present": [
- 1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009,
- 1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019,
- 1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029,
- 1337030, 1337031, 1337032, 1337033, 1337034, 1337035, 1337036, 1337037, 1337038, 1337039,
- 1337040, 1337041, 1337042, 1337043, 1337044, 1337045, 1337046, 1337047, 1337048, 1337049,
- 1337050, 1337051, 1337052, 1337053, 1337054, 1337055, 1337056, 1337057, 1337058, 1337059,
- 1337060, 1337061, 1337062, 1337063, 1337064, 1337065, 1337066, 1337067, 1337068, 1337069,
- 1337070, 1337071, 1337072, 1337073, 1337074, 1337075, 1337076, 1337077, 1337078, 1337079,
- 1337080, 1337081, 1337082, 1337083, 1337084, 1337085],
- "Past": [
- 1337086, 1337087, 1337088, 1337089,
- 1337090, 1337091, 1337092, 1337093, 1337094, 1337095, 1337096, 1337097, 1337098, 1337099,
- 1337100, 1337101, 1337102, 1337103, 1337104, 1337105, 1337106, 1337107, 1337108, 1337109,
- 1337110, 1337111, 1337112, 1337113, 1337114, 1337115, 1337116, 1337117, 1337118, 1337119,
- 1337120, 1337121, 1337122, 1337123, 1337124, 1337125, 1337126, 1337127, 1337128, 1337129,
- 1337130, 1337131, 1337132, 1337133, 1337134, 1337135, 1337136, 1337137, 1337138, 1337139,
- 1337140, 1337141, 1337142, 1337143, 1337144, 1337145, 1337146, 1337147, 1337148, 1337149,
- 1337150, 1337151, 1337152, 1337153, 1337154, 1337155,
- 1337171, 1337172, 1337173, 1337174, 1337175],
- "Ancient Pyramid": [
- 1337236,
- 1337246, 1337247, 1337248, 1337249]
- }
- if(slot_data["DownloadableItems"]):
- timespinner_location_ids["Present"] += [
- 1337156, 1337157, 1337159,
- 1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
- 1337170]
- if(slot_data["Cantoran"]):
- timespinner_location_ids["Past"].append(1337176)
- if(slot_data["LoreChecks"]):
- timespinner_location_ids["Present"] += [
- 1337177, 1337178, 1337179,
- 1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187]
- timespinner_location_ids["Past"] += [
- 1337188, 1337189,
- 1337190, 1337191, 1337192, 1337193, 1337194, 1337195, 1337196, 1337197, 1337198]
- if(slot_data["GyreArchives"]):
- timespinner_location_ids["Ancient Pyramid"] += [
- 1337237, 1337238, 1337239,
- 1337240, 1337241, 1337242, 1337243, 1337244, 1337245]
-
- display_data = {}
-
- # Victory condition
- game_state = multisave.get("client_game_state", {}).get((team, player), 0)
- display_data['game_finished'] = game_state == 30
-
- # Turn location IDs into advancement tab counts
- checked_locations = multisave.get("location_checks", {}).get((team, player), set())
- lookup_name = lambda id: lookup_any_location_id_to_name[id]
- location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
- for tab_name, tab_locations in timespinner_location_ids.items()}
- checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
- for tab_name, tab_locations in timespinner_location_ids.items()}
- checks_done['Total'] = len(checked_locations)
- checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in timespinner_location_ids.items()}
- checks_in_area['Total'] = sum(checks_in_area.values())
- acquired_items = {lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name}
- options = {k for k, v in slot_data.items() if v}
-
- return render_template("timespinnerTracker.html",
- inventory=inventory, icons=icons, acquired_items=acquired_items,
- player=player, team=team, room=room, player_name=playerName,
- checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
- options=options, **display_data)
-
-def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
- inventory: Counter, team: int, player: int, playerName: str,
- seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict,
- saving_second: int) -> str:
-
- icons = {
- "Energy Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ETank.png",
- "Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Missile.png",
- "Super Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Super.png",
- "Power Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/PowerBomb.png",
- "Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Bomb.png",
- "Charge Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Charge.png",
- "Ice Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Ice.png",
- "Hi-Jump Boots": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/HiJump.png",
- "Speed Booster": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpeedBooster.png",
- "Wave Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Wave.png",
- "Spazer": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Spazer.png",
- "Spring Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpringBall.png",
- "Varia Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Varia.png",
- "Plasma Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Plasma.png",
- "Grappling Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Grapple.png",
- "Morph Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Morph.png",
- "Reserve Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Reserve.png",
- "Gravity Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Gravity.png",
- "X-Ray Scope": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/XRayScope.png",
- "Space Jump": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpaceJump.png",
- "Screw Attack": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ScrewAttack.png",
- "Nothing": "",
- "No Energy": "",
- "Kraid": "",
- "Phantoon": "",
- "Draygon": "",
- "Ridley": "",
- "Mother Brain": "",
- }
+def render_generic_multiworld_sphere_tracker(tracker_data: TrackerData) -> str:
+ return render_template(
+ "multispheretracker.html",
+ room=tracker_data.room,
+ tracker_data=tracker_data,
+ )
- multi_items = {
- "Energy Tank": 83000,
- "Missile": 83001,
- "Super Missile": 83002,
- "Power Bomb": 83003,
- "Reserve Tank": 83020,
- }
- supermetroid_location_ids = {
- 'Crateria/Blue Brinstar': [82005, 82007, 82008, 82026, 82029,
- 82000, 82004, 82006, 82009, 82010,
- 82011, 82012, 82027, 82028, 82034,
- 82036, 82037],
- 'Green/Pink Brinstar': [82017, 82023, 82030, 82033, 82035,
- 82013, 82014, 82015, 82016, 82018,
- 82019, 82021, 82022, 82024, 82025,
- 82031],
- 'Red Brinstar': [82038, 82042, 82039, 82040, 82041],
- 'Kraid': [82043, 82048, 82044],
- 'Norfair': [82050, 82053, 82061, 82066, 82068,
- 82049, 82051, 82054, 82055, 82056,
- 82062, 82063, 82064, 82065, 82067],
- 'Lower Norfair': [82078, 82079, 82080, 82070, 82071,
- 82073, 82074, 82075, 82076, 82077],
- 'Crocomire': [82052, 82060, 82057, 82058, 82059],
- 'Wrecked Ship': [82129, 82132, 82134, 82135, 82001,
- 82002, 82003, 82128, 82130, 82131,
- 82133],
- 'West Maridia': [82138, 82136, 82137, 82139, 82140,
- 82141, 82142],
- 'East Maridia': [82143, 82145, 82150, 82152, 82154,
- 82144, 82146, 82147, 82148, 82149,
- 82151],
- }
+@app.route("/sphere_tracker/")
+@cache.memoize(timeout=TRACKER_CACHE_TIMEOUT_IN_SECONDS)
+def get_multiworld_sphere_tracker(tracker: UUID):
+ # Room must exist.
+ room = Room.get(tracker=tracker)
+ if not room:
+ abort(404)
- display_data = {}
-
-
- for item_name, item_id in multi_items.items():
- base_name = item_name.split()[0].lower()
- display_data[base_name+"_count"] = inventory[item_id]
-
- # Victory condition
- game_state = multisave.get("client_game_state", {}).get((team, player), 0)
- display_data['game_finished'] = game_state == 30
-
- # Turn location IDs into advancement tab counts
- checked_locations = multisave.get("location_checks", {}).get((team, player), set())
- lookup_name = lambda id: lookup_any_location_id_to_name[id]
- location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
- for tab_name, tab_locations in supermetroid_location_ids.items()}
- checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
- for tab_name, tab_locations in supermetroid_location_ids.items()}
- checks_done['Total'] = len(checked_locations)
- checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in supermetroid_location_ids.items()}
- checks_in_area['Total'] = sum(checks_in_area.values())
-
- return render_template("supermetroidTracker.html",
- inventory=inventory, icons=icons,
- acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
- id in lookup_any_item_id_to_name},
- player=player, team=team, room=room, player_name=playerName,
- checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
- **display_data)
-
-def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
- inventory: Counter, team: int, player: int, playerName: str,
- seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int],
- slot_data: Dict, saving_second: int) -> str:
-
- SC2WOL_LOC_ID_OFFSET = 1000
- SC2WOL_ITEM_ID_OFFSET = 1000
-
-
- icons = {
- "Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png",
- "Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png",
- "Starting Supply": "https://static.wikia.nocookie.net/starcraft/images/d/d3/TerranSupply_SC2_Icon1.gif",
-
- "Infantry Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel1.png",
- "Infantry Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel2.png",
- "Infantry Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel3.png",
- "Infantry Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel1.png",
- "Infantry Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel2.png",
- "Infantry Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel3.png",
- "Vehicle Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel1.png",
- "Vehicle Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel2.png",
- "Vehicle Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel3.png",
- "Vehicle Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel1.png",
- "Vehicle Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel2.png",
- "Vehicle Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel3.png",
- "Ship Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel1.png",
- "Ship Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel2.png",
- "Ship Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel3.png",
- "Ship Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel1.png",
- "Ship Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel2.png",
- "Ship Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel3.png",
-
- "Bunker": "https://static.wikia.nocookie.net/starcraft/images/c/c5/Bunker_SC2_Icon1.jpg",
- "Missile Turret": "https://static.wikia.nocookie.net/starcraft/images/5/5f/MissileTurret_SC2_Icon1.jpg",
- "Sensor Tower": "https://static.wikia.nocookie.net/starcraft/images/d/d2/SensorTower_SC2_Icon1.jpg",
-
- "Projectile Accelerator (Bunker)": "https://0rganics.org/archipelago/sc2wol/ProjectileAccelerator.png",
- "Neosteel Bunker (Bunker)": "https://0rganics.org/archipelago/sc2wol/NeosteelBunker.png",
- "Titanium Housing (Missile Turret)": "https://0rganics.org/archipelago/sc2wol/TitaniumHousing.png",
- "Hellstorm Batteries (Missile Turret)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png",
- "Advanced Construction (SCV)": "https://0rganics.org/archipelago/sc2wol/AdvancedConstruction.png",
- "Dual-Fusion Welders (SCV)": "https://0rganics.org/archipelago/sc2wol/Dual-FusionWelders.png",
- "Fire-Suppression System (Building)": "https://0rganics.org/archipelago/sc2wol/Fire-SuppressionSystem.png",
- "Orbital Command (Building)": "https://0rganics.org/archipelago/sc2wol/OrbitalCommandCampaign.png",
-
- "Marine": "https://static.wikia.nocookie.net/starcraft/images/4/47/Marine_SC2_Icon1.jpg",
- "Medic": "https://static.wikia.nocookie.net/starcraft/images/7/74/Medic_SC2_Rend1.jpg",
- "Firebat": "https://static.wikia.nocookie.net/starcraft/images/3/3c/Firebat_SC2_Rend1.jpg",
- "Marauder": "https://static.wikia.nocookie.net/starcraft/images/b/ba/Marauder_SC2_Icon1.jpg",
- "Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg",
-
- "Stimpack (Marine)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
- "Super Stimpack (Marine)": "/static/static/icons/sc2/superstimpack.png",
- "Combat Shield (Marine)": "https://0rganics.org/archipelago/sc2wol/CombatShieldCampaign.png",
- "Laser Targeting System (Marine)": "/static/static/icons/sc2/lasertargetingsystem.png",
- "Magrail Munitions (Marine)": "/static/static/icons/sc2/magrailmunitions.png",
- "Optimized Logistics (Marine)": "/static/static/icons/sc2/optimizedlogistics.png",
- "Advanced Medic Facilities (Medic)": "https://0rganics.org/archipelago/sc2wol/AdvancedMedicFacilities.png",
- "Stabilizer Medpacks (Medic)": "https://0rganics.org/archipelago/sc2wol/StabilizerMedpacks.png",
- "Restoration (Medic)": "/static/static/icons/sc2/restoration.png",
- "Optical Flare (Medic)": "/static/static/icons/sc2/opticalflare.png",
- "Optimized Logistics (Medic)": "/static/static/icons/sc2/optimizedlogistics.png",
- "Incinerator Gauntlets (Firebat)": "https://0rganics.org/archipelago/sc2wol/IncineratorGauntlets.png",
- "Juggernaut Plating (Firebat)": "https://0rganics.org/archipelago/sc2wol/JuggernautPlating.png",
- "Stimpack (Firebat)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
- "Super Stimpack (Firebat)": "/static/static/icons/sc2/superstimpack.png",
- "Optimized Logistics (Firebat)": "/static/static/icons/sc2/optimizedlogistics.png",
- "Concussive Shells (Marauder)": "https://0rganics.org/archipelago/sc2wol/ConcussiveShellsCampaign.png",
- "Kinetic Foam (Marauder)": "https://0rganics.org/archipelago/sc2wol/KineticFoam.png",
- "Stimpack (Marauder)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
- "Super Stimpack (Marauder)": "/static/static/icons/sc2/superstimpack.png",
- "Laser Targeting System (Marauder)": "/static/static/icons/sc2/lasertargetingsystem.png",
- "Magrail Munitions (Marauder)": "/static/static/icons/sc2/magrailmunitions.png",
- "Internal Tech Module (Marauder)": "/static/static/icons/sc2/internalizedtechmodule.png",
- "U-238 Rounds (Reaper)": "https://0rganics.org/archipelago/sc2wol/U-238Rounds.png",
- "G-4 Clusterbomb (Reaper)": "https://0rganics.org/archipelago/sc2wol/G-4Clusterbomb.png",
- "Stimpack (Reaper)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
- "Super Stimpack (Reaper)": "/static/static/icons/sc2/superstimpack.png",
- "Laser Targeting System (Reaper)": "/static/static/icons/sc2/lasertargetingsystem.png",
- "Advanced Cloaking Field (Reaper)": "/static/static/icons/sc2/terran-cloak-color.png",
- "Spider Mines (Reaper)": "/static/static/icons/sc2/spidermine.png",
- "Combat Drugs (Reaper)": "/static/static/icons/sc2/reapercombatdrugs.png",
-
- "Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg",
- "Vulture": "https://static.wikia.nocookie.net/starcraft/images/d/da/Vulture_WoL.jpg",
- "Goliath": "https://static.wikia.nocookie.net/starcraft/images/e/eb/Goliath_WoL.jpg",
- "Diamondback": "https://static.wikia.nocookie.net/starcraft/images/a/a6/Diamondback_WoL.jpg",
- "Siege Tank": "https://static.wikia.nocookie.net/starcraft/images/5/57/SiegeTank_SC2_Icon1.jpg",
-
- "Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png",
- "Thermite Filaments (Hellion)": "https://0rganics.org/archipelago/sc2wol/ThermiteFilaments.png",
- "Hellbat Aspect (Hellion)": "/static/static/icons/sc2/hellionbattlemode.png",
- "Smart Servos (Hellion)": "/static/static/icons/sc2/transformationservos.png",
- "Optimized Logistics (Hellion)": "/static/static/icons/sc2/optimizedlogistics.png",
- "Jump Jets (Hellion)": "/static/static/icons/sc2/jumpjets.png",
- "Stimpack (Hellion)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
- "Super Stimpack (Hellion)": "/static/static/icons/sc2/superstimpack.png",
- "Cerberus Mine (Spider Mine)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png",
- "High Explosive Munition (Spider Mine)": "/static/static/icons/sc2/high-explosive-spidermine.png",
- "Replenishable Magazine (Vulture)": "https://0rganics.org/archipelago/sc2wol/ReplenishableMagazine.png",
- "Ion Thrusters (Vulture)": "/static/static/icons/sc2/emergencythrusters.png",
- "Auto Launchers (Vulture)": "/static/static/icons/sc2/jotunboosters.png",
- "Multi-Lock Weapons System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Multi-LockWeaponsSystem.png",
- "Ares-Class Targeting System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Ares-ClassTargetingSystem.png",
- "Jump Jets (Goliath)": "/static/static/icons/sc2/jumpjets.png",
- "Optimized Logistics (Goliath)": "/static/static/icons/sc2/optimizedlogistics.png",
- "Tri-Lithium Power Cell (Diamondback)": "https://0rganics.org/archipelago/sc2wol/Tri-LithiumPowerCell.png",
- "Shaped Hull (Diamondback)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png",
- "Hyperfluxor (Diamondback)": "/static/static/icons/sc2/hyperfluxor.png",
- "Burst Capacitors (Diamondback)": "/static/static/icons/sc2/burstcapacitors.png",
- "Optimized Logistics (Diamondback)": "/static/static/icons/sc2/optimizedlogistics.png",
- "Maelstrom Rounds (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/MaelstromRounds.png",
- "Shaped Blast (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/ShapedBlast.png",
- "Jump Jets (Siege Tank)": "/static/static/icons/sc2/jumpjets.png",
- "Spider Mines (Siege Tank)": "/static/static/icons/sc2/siegetank-spidermines.png",
- "Smart Servos (Siege Tank)": "/static/static/icons/sc2/transformationservos.png",
- "Graduating Range (Siege Tank)": "/static/static/icons/sc2/siegetankrange.png",
- "Laser Targeting System (Siege Tank)": "/static/static/icons/sc2/lasertargetingsystem.png",
- "Advanced Siege Tech (Siege Tank)": "/static/static/icons/sc2/improvedsiegemode.png",
- "Internal Tech Module (Siege Tank)": "/static/static/icons/sc2/internalizedtechmodule.png",
-
- "Medivac": "https://static.wikia.nocookie.net/starcraft/images/d/db/Medivac_SC2_Icon1.jpg",
- "Wraith": "https://static.wikia.nocookie.net/starcraft/images/7/75/Wraith_WoL.jpg",
- "Viking": "https://static.wikia.nocookie.net/starcraft/images/2/2a/Viking_SC2_Icon1.jpg",
- "Banshee": "https://static.wikia.nocookie.net/starcraft/images/3/32/Banshee_SC2_Icon1.jpg",
- "Battlecruiser": "https://static.wikia.nocookie.net/starcraft/images/f/f5/Battlecruiser_SC2_Icon1.jpg",
-
- "Rapid Deployment Tube (Medivac)": "https://0rganics.org/archipelago/sc2wol/RapidDeploymentTube.png",
- "Advanced Healing AI (Medivac)": "https://0rganics.org/archipelago/sc2wol/AdvancedHealingAI.png",
- "Expanded Hull (Medivac)": "/static/static/icons/sc2/neosteelfortifiedarmor.png",
- "Afterburners (Medivac)": "/static/static/icons/sc2/medivacemergencythrusters.png",
- "Tomahawk Power Cells (Wraith)": "https://0rganics.org/archipelago/sc2wol/TomahawkPowerCells.png",
- "Displacement Field (Wraith)": "https://0rganics.org/archipelago/sc2wol/DisplacementField.png",
- "Advanced Laser Technology (Wraith)": "/static/static/icons/sc2/improvedburstlaser.png",
- "Ripwave Missiles (Viking)": "https://0rganics.org/archipelago/sc2wol/RipwaveMissiles.png",
- "Phobos-Class Weapons System (Viking)": "https://0rganics.org/archipelago/sc2wol/Phobos-ClassWeaponsSystem.png",
- "Smart Servos (Viking)": "/static/static/icons/sc2/transformationservos.png",
- "Magrail Munitions (Viking)": "/static/static/icons/sc2/magrailmunitions.png",
- "Cross-Spectrum Dampeners (Banshee)": "/static/static/icons/sc2/crossspectrumdampeners.png",
- "Advanced Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png",
- "Shockwave Missile Battery (Banshee)": "https://0rganics.org/archipelago/sc2wol/ShockwaveMissileBattery.png",
- "Hyperflight Rotors (Banshee)": "/static/static/icons/sc2/hyperflightrotors.png",
- "Laser Targeting System (Banshee)": "/static/static/icons/sc2/lasertargetingsystem.png",
- "Internal Tech Module (Banshee)": "/static/static/icons/sc2/internalizedtechmodule.png",
- "Missile Pods (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/MissilePods.png",
- "Defensive Matrix (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png",
- "Tactical Jump (Battlecruiser)": "/static/static/icons/sc2/warpjump.png",
- "Cloak (Battlecruiser)": "/static/static/icons/sc2/terran-cloak-color.png",
- "ATX Laser Battery (Battlecruiser)": "/static/static/icons/sc2/specialordance.png",
- "Optimized Logistics (Battlecruiser)": "/static/static/icons/sc2/optimizedlogistics.png",
- "Internal Tech Module (Battlecruiser)": "/static/static/icons/sc2/internalizedtechmodule.png",
-
- "Ghost": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Ghost_SC2_Icon1.jpg",
- "Spectre": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Spectre_WoL.jpg",
- "Thor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/Thor_SC2_Icon1.jpg",
-
- "Widow Mine": "/static/static/icons/sc2/widowmine.png",
- "Cyclone": "/static/static/icons/sc2/cyclone.png",
- "Liberator": "/static/static/icons/sc2/liberator.png",
- "Valkyrie": "/static/static/icons/sc2/valkyrie.png",
-
- "Ocular Implants (Ghost)": "https://0rganics.org/archipelago/sc2wol/OcularImplants.png",
- "Crius Suit (Ghost)": "https://0rganics.org/archipelago/sc2wol/CriusSuit.png",
- "EMP Rounds (Ghost)": "/static/static/icons/sc2/terran-emp-color.png",
- "Lockdown (Ghost)": "/static/static/icons/sc2/lockdown.png",
- "Psionic Lash (Spectre)": "https://0rganics.org/archipelago/sc2wol/PsionicLash.png",
- "Nyx-Class Cloaking Module (Spectre)": "https://0rganics.org/archipelago/sc2wol/Nyx-ClassCloakingModule.png",
- "Impaler Rounds (Spectre)": "/static/static/icons/sc2/impalerrounds.png",
- "330mm Barrage Cannon (Thor)": "https://0rganics.org/archipelago/sc2wol/330mmBarrageCannon.png",
- "Immortality Protocol (Thor)": "https://0rganics.org/archipelago/sc2wol/ImmortalityProtocol.png",
- "High Impact Payload (Thor)": "/static/static/icons/sc2/thorsiegemode.png",
- "Smart Servos (Thor)": "/static/static/icons/sc2/transformationservos.png",
-
- "Optimized Logistics (Predator)": "/static/static/icons/sc2/optimizedlogistics.png",
- "Drilling Claws (Widow Mine)": "/static/static/icons/sc2/drillingclaws.png",
- "Concealment (Widow Mine)": "/static/static/icons/sc2/widowminehidden.png",
- "Black Market Launchers (Widow Mine)": "/static/static/icons/sc2/widowmine-attackrange.png",
- "Executioner Missiles (Widow Mine)": "/static/static/icons/sc2/widowmine-deathblossom.png",
- "Mag-Field Accelerators (Cyclone)": "/static/static/icons/sc2/magfieldaccelerator.png",
- "Mag-Field Launchers (Cyclone)": "/static/static/icons/sc2/cyclonerangeupgrade.png",
- "Targeting Optics (Cyclone)": "/static/static/icons/sc2/targetingoptics.png",
- "Rapid Fire Launchers (Cyclone)": "/static/static/icons/sc2/ripwavemissiles.png",
- "Bio Mechanical Repair Drone (Raven)": "/static/static/icons/sc2/biomechanicaldrone.png",
- "Spider Mines (Raven)": "/static/static/icons/sc2/siegetank-spidermines.png",
- "Railgun Turret (Raven)": "/static/static/icons/sc2/autoturretblackops.png",
- "Hunter-Seeker Weapon (Raven)": "/static/static/icons/sc2/specialordance.png",
- "Interference Matrix (Raven)": "/static/static/icons/sc2/interferencematrix.png",
- "Anti-Armor Missile (Raven)": "/static/static/icons/sc2/shreddermissile.png",
- "Internal Tech Module (Raven)": "/static/static/icons/sc2/internalizedtechmodule.png",
- "EMP Shockwave (Science Vessel)": "/static/static/icons/sc2/staticempblast.png",
- "Defensive Matrix (Science Vessel)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png",
- "Advanced Ballistics (Liberator)": "/static/static/icons/sc2/advanceballistics.png",
- "Raid Artillery (Liberator)": "/static/static/icons/sc2/terrandefendermodestructureattack.png",
- "Cloak (Liberator)": "/static/static/icons/sc2/terran-cloak-color.png",
- "Laser Targeting System (Liberator)": "/static/static/icons/sc2/lasertargetingsystem.png",
- "Optimized Logistics (Liberator)": "/static/static/icons/sc2/optimizedlogistics.png",
- "Enhanced Cluster Launchers (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png",
- "Shaped Hull (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png",
- "Burst Lasers (Valkyrie)": "/static/static/icons/sc2/improvedburstlaser.png",
- "Afterburners (Valkyrie)": "/static/static/icons/sc2/medivacemergencythrusters.png",
-
- "War Pigs": "https://static.wikia.nocookie.net/starcraft/images/e/ed/WarPigs_SC2_Icon1.jpg",
- "Devil Dogs": "https://static.wikia.nocookie.net/starcraft/images/3/33/DevilDogs_SC2_Icon1.jpg",
- "Hammer Securities": "https://static.wikia.nocookie.net/starcraft/images/3/3b/HammerSecurity_SC2_Icon1.jpg",
- "Spartan Company": "https://static.wikia.nocookie.net/starcraft/images/b/be/SpartanCompany_SC2_Icon1.jpg",
- "Siege Breakers": "https://static.wikia.nocookie.net/starcraft/images/3/31/SiegeBreakers_SC2_Icon1.jpg",
- "Hel's Angel": "https://static.wikia.nocookie.net/starcraft/images/6/63/HelsAngels_SC2_Icon1.jpg",
- "Dusk Wings": "https://static.wikia.nocookie.net/starcraft/images/5/52/DuskWings_SC2_Icon1.jpg",
- "Jackson's Revenge": "https://static.wikia.nocookie.net/starcraft/images/9/95/JacksonsRevenge_SC2_Icon1.jpg",
-
- "Ultra-Capacitors": "https://static.wikia.nocookie.net/starcraft/images/2/23/SC2_Lab_Ultra_Capacitors_Icon.png",
- "Vanadium Plating": "https://static.wikia.nocookie.net/starcraft/images/6/67/SC2_Lab_VanPlating_Icon.png",
- "Orbital Depots": "https://static.wikia.nocookie.net/starcraft/images/0/01/SC2_Lab_Orbital_Depot_Icon.png",
- "Micro-Filtering": "https://static.wikia.nocookie.net/starcraft/images/2/20/SC2_Lab_MicroFilter_Icon.png",
- "Automated Refinery": "https://static.wikia.nocookie.net/starcraft/images/7/71/SC2_Lab_Auto_Refinery_Icon.png",
- "Command Center Reactor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/SC2_Lab_CC_Reactor_Icon.png",
- "Raven": "https://static.wikia.nocookie.net/starcraft/images/1/19/SC2_Lab_Raven_Icon.png",
- "Science Vessel": "https://static.wikia.nocookie.net/starcraft/images/c/c3/SC2_Lab_SciVes_Icon.png",
- "Tech Reactor": "https://static.wikia.nocookie.net/starcraft/images/c/c5/SC2_Lab_Tech_Reactor_Icon.png",
- "Orbital Strike": "https://static.wikia.nocookie.net/starcraft/images/d/df/SC2_Lab_Orb_Strike_Icon.png",
-
- "Shrike Turret (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png",
- "Fortified Bunker (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png",
- "Planetary Fortress": "https://static.wikia.nocookie.net/starcraft/images/0/0b/SC2_Lab_PlanetFortress_Icon.png",
- "Perdition Turret": "https://static.wikia.nocookie.net/starcraft/images/a/af/SC2_Lab_PerdTurret_Icon.png",
- "Predator": "https://static.wikia.nocookie.net/starcraft/images/8/83/SC2_Lab_Predator_Icon.png",
- "Hercules": "https://static.wikia.nocookie.net/starcraft/images/4/40/SC2_Lab_Hercules_Icon.png",
- "Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png",
- "Regenerative Bio-Steel Level 1": "/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png",
- "Regenerative Bio-Steel Level 2": "/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png",
- "Hive Mind Emulator": "https://static.wikia.nocookie.net/starcraft/images/b/bc/SC2_Lab_Hive_Emulator_Icon.png",
- "Psi Disrupter": "https://static.wikia.nocookie.net/starcraft/images/c/cf/SC2_Lab_Psi_Disruptor_Icon.png",
-
- "Zealot": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Icon_Protoss_Zealot.jpg",
- "Stalker": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Icon_Protoss_Stalker.jpg",
- "High Templar": "https://static.wikia.nocookie.net/starcraft/images/a/a0/Icon_Protoss_High_Templar.jpg",
- "Dark Templar": "https://static.wikia.nocookie.net/starcraft/images/9/90/Icon_Protoss_Dark_Templar.jpg",
- "Immortal": "https://static.wikia.nocookie.net/starcraft/images/c/c1/Icon_Protoss_Immortal.jpg",
- "Colossus": "https://static.wikia.nocookie.net/starcraft/images/4/40/Icon_Protoss_Colossus.jpg",
- "Phoenix": "https://static.wikia.nocookie.net/starcraft/images/b/b1/Icon_Protoss_Phoenix.jpg",
- "Void Ray": "https://static.wikia.nocookie.net/starcraft/images/1/1d/VoidRay_SC2_Rend1.jpg",
- "Carrier": "https://static.wikia.nocookie.net/starcraft/images/2/2c/Icon_Protoss_Carrier.jpg",
-
- "Nothing": "",
- }
- sc2wol_location_ids = {
- "Liberation Day": range(SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 200),
- "The Outlaws": range(SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 300),
- "Zero Hour": range(SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 400),
- "Evacuation": range(SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 500),
- "Outbreak": range(SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 600),
- "Safe Haven": range(SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 700),
- "Haven's Fall": range(SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 800),
- "Smash and Grab": range(SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 900),
- "The Dig": range(SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 1000),
- "The Moebius Factor": range(SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1100),
- "Supernova": range(SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1200),
- "Maw of the Void": range(SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1300),
- "Devil's Playground": range(SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1400),
- "Welcome to the Jungle": range(SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1500),
- "Breakout": range(SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1600),
- "Ghost of a Chance": range(SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1700),
- "The Great Train Robbery": range(SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1800),
- "Cutthroat": range(SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1900),
- "Engine of Destruction": range(SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 2000),
- "Media Blitz": range(SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2100),
- "Piercing the Shroud": range(SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2200),
- "Whispers of Doom": range(SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2300),
- "A Sinister Turn": range(SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2400),
- "Echoes of the Future": range(SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2500),
- "In Utter Darkness": range(SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2600),
- "Gates of Hell": range(SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2700),
- "Belly of the Beast": range(SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2800),
- "Shatter the Sky": range(SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2900),
- }
+ tracker_data = TrackerData(room)
+ return render_generic_multiworld_sphere_tracker(tracker_data)
- display_data = {}
- # Grouped Items
- grouped_item_ids = {
- "Progressive Weapon Upgrade": 107 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Armor Upgrade": 108 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Infantry Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Vehicle Upgrade": 110 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Ship Upgrade": 111 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Weapon/Armor Upgrade": 112 + SC2WOL_ITEM_ID_OFFSET
- }
- grouped_item_replacements = {
- "Progressive Weapon Upgrade": ["Progressive Infantry Weapon", "Progressive Vehicle Weapon", "Progressive Ship Weapon"],
- "Progressive Armor Upgrade": ["Progressive Infantry Armor", "Progressive Vehicle Armor", "Progressive Ship Armor"],
- "Progressive Infantry Upgrade": ["Progressive Infantry Weapon", "Progressive Infantry Armor"],
- "Progressive Vehicle Upgrade": ["Progressive Vehicle Weapon", "Progressive Vehicle Armor"],
- "Progressive Ship Upgrade": ["Progressive Ship Weapon", "Progressive Ship Armor"]
- }
- grouped_item_replacements["Progressive Weapon/Armor Upgrade"] = grouped_item_replacements["Progressive Weapon Upgrade"] + grouped_item_replacements["Progressive Armor Upgrade"]
- replacement_item_ids = {
- "Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET,
- }
- for grouped_item_name, grouped_item_id in grouped_item_ids.items():
- count: int = inventory[grouped_item_id]
- if count > 0:
- for replacement_item in grouped_item_replacements[grouped_item_name]:
- replacement_id: int = replacement_item_ids[replacement_item]
- inventory[replacement_id] = count
-
- # Determine display for progressive items
- progressive_items = {
- "Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Stimpack (Marine)": 208 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Stimpack (Firebat)": 226 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Stimpack (Marauder)": 228 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Stimpack (Reaper)": 250 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Stimpack (Hellion)": 259 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive High Impact Payload (Thor)": 361 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Cross-Spectrum Dampeners (Banshee)": 316 + SC2WOL_ITEM_ID_OFFSET,
- "Progressive Regenerative Bio-Steel": 617 + SC2WOL_ITEM_ID_OFFSET
- }
- progressive_names = {
- "Progressive Infantry Weapon": ["Infantry Weapons Level 1", "Infantry Weapons Level 1", "Infantry Weapons Level 2", "Infantry Weapons Level 3"],
- "Progressive Infantry Armor": ["Infantry Armor Level 1", "Infantry Armor Level 1", "Infantry Armor Level 2", "Infantry Armor Level 3"],
- "Progressive Vehicle Weapon": ["Vehicle Weapons Level 1", "Vehicle Weapons Level 1", "Vehicle Weapons Level 2", "Vehicle Weapons Level 3"],
- "Progressive Vehicle Armor": ["Vehicle Armor Level 1", "Vehicle Armor Level 1", "Vehicle Armor Level 2", "Vehicle Armor Level 3"],
- "Progressive Ship Weapon": ["Ship Weapons Level 1", "Ship Weapons Level 1", "Ship Weapons Level 2", "Ship Weapons Level 3"],
- "Progressive Ship Armor": ["Ship Armor Level 1", "Ship Armor Level 1", "Ship Armor Level 2", "Ship Armor Level 3"],
- "Progressive Stimpack (Marine)": ["Stimpack (Marine)", "Stimpack (Marine)", "Super Stimpack (Marine)"],
- "Progressive Stimpack (Firebat)": ["Stimpack (Firebat)", "Stimpack (Firebat)", "Super Stimpack (Firebat)"],
- "Progressive Stimpack (Marauder)": ["Stimpack (Marauder)", "Stimpack (Marauder)", "Super Stimpack (Marauder)"],
- "Progressive Stimpack (Reaper)": ["Stimpack (Reaper)", "Stimpack (Reaper)", "Super Stimpack (Reaper)"],
- "Progressive Stimpack (Hellion)": ["Stimpack (Hellion)", "Stimpack (Hellion)", "Super Stimpack (Hellion)"],
- "Progressive High Impact Payload (Thor)": ["High Impact Payload (Thor)", "High Impact Payload (Thor)", "Smart Servos (Thor)"],
- "Progressive Cross-Spectrum Dampeners (Banshee)": ["Cross-Spectrum Dampeners (Banshee)", "Cross-Spectrum Dampeners (Banshee)", "Advanced Cross-Spectrum Dampeners (Banshee)"],
- "Progressive Regenerative Bio-Steel": ["Regenerative Bio-Steel Level 1", "Regenerative Bio-Steel Level 1", "Regenerative Bio-Steel Level 2"]
- }
- for item_name, item_id in progressive_items.items():
- level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
- display_name = progressive_names[item_name][level]
- base_name = (item_name.split(maxsplit=1)[1].lower()
- .replace(' ', '_')
- .replace("-", "")
- .replace("(", "")
- .replace(")", ""))
- display_data[base_name + "_level"] = level
- display_data[base_name + "_url"] = icons[display_name]
- display_data[base_name + "_name"] = display_name
-
- # Multi-items
- multi_items = {
- "+15 Starting Minerals": 800 + SC2WOL_ITEM_ID_OFFSET,
- "+15 Starting Vespene": 801 + SC2WOL_ITEM_ID_OFFSET,
- "+2 Starting Supply": 802 + SC2WOL_ITEM_ID_OFFSET
- }
- for item_name, item_id in multi_items.items():
- base_name = item_name.split()[-1].lower()
- count = inventory[item_id]
- if base_name == "supply":
- count = count * 2
- display_data[base_name + "_count"] = count
- else:
- count = count * 15
- display_data[base_name + "_count"] = count
+# TODO: This is a temporary solution until a proper Tracker API can be implemented for tracker templates and data to
+# live in their respective world folders.
+
+from worlds import network_data_package
- # Victory condition
- game_state = multisave.get("client_game_state", {}).get((team, player), 0)
- display_data['game_finished'] = game_state == 30
-
- # Turn location IDs into mission objective counts
- checked_locations = multisave.get("location_checks", {}).get((team, player), set())
- lookup_name = lambda id: lookup_any_location_id_to_name[id]
- location_info = {mission_name: {lookup_name(id): (id in checked_locations) for id in mission_locations if id in set(locations[player])} for mission_name, mission_locations in sc2wol_location_ids.items()}
- checks_done = {mission_name: len([id for id in mission_locations if id in checked_locations and id in set(locations[player])]) for mission_name, mission_locations in sc2wol_location_ids.items()}
- checks_done['Total'] = len(checked_locations)
- checks_in_area = {mission_name: len([id for id in mission_locations if id in set(locations[player])]) for mission_name, mission_locations in sc2wol_location_ids.items()}
- checks_in_area['Total'] = sum(checks_in_area.values())
-
- return render_template("sc2wolTracker.html",
- inventory=inventory, icons=icons,
- acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
- id in lookup_any_item_id_to_name},
- player=player, team=team, room=room, player_name=playerName,
- checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
- **display_data)
-
-def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
- inventory: Counter, team: int, player: int, playerName: str,
- seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, saving_second: int) -> str:
-
- icons = {
- "Checks Available": "https://0rganics.org/archipelago/cf/spr_tiles_3.png",
- "Map Width": "https://0rganics.org/archipelago/cf/spr_tiles_4.png",
- "Map Height": "https://0rganics.org/archipelago/cf/spr_tiles_5.png",
- "Map Bombs": "https://0rganics.org/archipelago/cf/spr_tiles_6.png",
-
- "Nothing": "",
+
+if "Factorio" in network_data_package["games"]:
+ def render_Factorio_multiworld_tracker(tracker_data: TrackerData, enabled_trackers: List[str]):
+ inventories: Dict[TeamPlayer, collections.Counter[str]] = {
+ (team, player): collections.Counter({
+ tracker_data.item_id_to_name["Factorio"][item_id]: count
+ for item_id, count in tracker_data.get_player_inventory_counts(team, player).items()
+ }) for team, players in tracker_data.get_all_slots().items() for player in players
+ if tracker_data.get_player_game(team, player) == "Factorio"
+ }
+
+ return render_template(
+ "multitracker__Factorio.html",
+ enabled_trackers=enabled_trackers,
+ current_tracker="Factorio",
+ room=tracker_data.room,
+ all_slots=tracker_data.get_all_slots(),
+ room_players=tracker_data.get_all_players(),
+ locations=tracker_data.get_room_locations(),
+ locations_complete=tracker_data.get_room_locations_complete(),
+ total_team_locations=tracker_data.get_team_locations_total_count(),
+ total_team_locations_complete=tracker_data.get_team_locations_checked_count(),
+ player_names_with_alias=tracker_data.get_room_long_player_names(),
+ completed_worlds=tracker_data.get_team_completed_worlds_count(),
+ games=tracker_data.get_room_games(),
+ states=tracker_data.get_room_client_statuses(),
+ hints=tracker_data.get_team_hints(),
+ activity_timers=tracker_data.get_room_last_activity(),
+ videos=tracker_data.get_room_videos(),
+ item_id_to_name=tracker_data.item_id_to_name,
+ location_id_to_name=tracker_data.location_id_to_name,
+ inventories=inventories,
+ )
+
+ _multiworld_trackers["Factorio"] = render_Factorio_multiworld_tracker
+
+if "A Link to the Past" in network_data_package["games"]:
+ # Mapping from non-progressive item to progressive name and max level.
+ non_progressive_items = {
+ "Fighter Sword": ("Progressive Sword", 1),
+ "Master Sword": ("Progressive Sword", 2),
+ "Tempered Sword": ("Progressive Sword", 3),
+ "Golden Sword": ("Progressive Sword", 4),
+ "Power Glove": ("Progressive Glove", 1),
+ "Titans Mitts": ("Progressive Glove", 2),
+ "Bow": ("Progressive Bow", 1),
+ "Silver Bow": ("Progressive Bow", 2),
+ "Blue Mail": ("Progressive Mail", 1),
+ "Red Mail": ("Progressive Mail", 2),
+ "Blue Shield": ("Progressive Shield", 1),
+ "Red Shield": ("Progressive Shield", 2),
+ "Mirror Shield": ("Progressive Shield", 3),
}
- checksfinder_location_ids = {
- "Tile 1": 81000,
- "Tile 2": 81001,
- "Tile 3": 81002,
- "Tile 4": 81003,
- "Tile 5": 81004,
- "Tile 6": 81005,
- "Tile 7": 81006,
- "Tile 8": 81007,
- "Tile 9": 81008,
- "Tile 10": 81009,
- "Tile 11": 81010,
- "Tile 12": 81011,
- "Tile 13": 81012,
- "Tile 14": 81013,
- "Tile 15": 81014,
- "Tile 16": 81015,
- "Tile 17": 81016,
- "Tile 18": 81017,
- "Tile 19": 81018,
- "Tile 20": 81019,
- "Tile 21": 81020,
- "Tile 22": 81021,
- "Tile 23": 81022,
- "Tile 24": 81023,
- "Tile 25": 81024,
+ progressive_item_max = {
+ "Progressive Sword": 4,
+ "Progressive Glove": 2,
+ "Progressive Bow": 2,
+ "Progressive Mail": 2,
+ "Progressive Shield": 3,
}
- display_data = {}
+ bottle_items = [
+ "Bottle",
+ "Bottle (Bee)",
+ "Bottle (Blue Potion)",
+ "Bottle (Fairy)",
+ "Bottle (Good Bee)",
+ "Bottle (Green Potion)",
+ "Bottle (Red Potion)",
+ ]
- # Multi-items
- multi_items = {
- "Map Width": 80000,
- "Map Height": 80001,
- "Map Bombs": 80002
- }
- for item_name, item_id in multi_items.items():
- base_name = item_name.split()[-1].lower()
- count = inventory[item_id]
- display_data[base_name + "_count"] = count
- display_data[base_name + "_display"] = count + 5
-
- # Get location info
- checked_locations = multisave.get("location_checks", {}).get((team, player), set())
- lookup_name = lambda id: lookup_any_location_id_to_name[id]
- location_info = {tile_name: {lookup_name(tile_location): (tile_location in checked_locations)} for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in set(locations[player])}
- checks_done = {tile_name: len([tile_location]) for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in checked_locations and tile_location in set(locations[player])}
- checks_done['Total'] = len(checked_locations)
- checks_in_area = checks_done
-
- # Calculate checks available
- display_data["checks_unlocked"] = min(display_data["width_count"] + display_data["height_count"] + display_data["bombs_count"] + 5, 25)
- display_data["checks_available"] = max(display_data["checks_unlocked"] - len(checked_locations), 0)
-
- # Victory condition
- game_state = multisave.get("client_game_state", {}).get((team, player), 0)
- display_data['game_finished'] = game_state == 30
-
- return render_template("checksfinderTracker.html",
- inventory=inventory, icons=icons,
- acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
- id in lookup_any_item_id_to_name},
- player=player, team=team, room=room, player_name=playerName,
- checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
- **display_data)
-
-def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
- inventory: Counter, team: int, player: int, playerName: str,
- seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int],
- saving_second: int, custom_locations: Dict[int, str], custom_items: Dict[int, str]) -> str:
-
- checked_locations = multisave.get("location_checks", {}).get((team, player), set())
- player_received_items = {}
- if multisave.get('version', 0) > 0:
- ordered_items = multisave.get('received_items', {}).get((team, player, True), [])
- else:
- ordered_items = multisave.get('received_items', {}).get((team, player), [])
-
- # add numbering to all items but starter_inventory
- for order_index, networkItem in enumerate(ordered_items, start=1):
- player_received_items[networkItem.item] = order_index
-
- return render_template("genericTracker.html",
- inventory=inventory,
- player=player, team=team, room=room, player_name=playerName,
- checked_locations=checked_locations,
- not_checked_locations=set(locations[player]) - checked_locations,
- received_items=player_received_items, saving_second=saving_second,
- custom_items=custom_items, custom_locations=custom_locations)
-
-
-def get_enabled_multiworld_trackers(room: Room, current: str):
- enabled = [
- {
- "name": "Generic",
- "endpoint": "get_multiworld_tracker",
- "current": current == "Generic"
- }
+ known_regions = [
+ "Light World", "Dark World", "Hyrule Castle", "Agahnims Tower", "Eastern Palace", "Desert Palace",
+ "Tower of Hera", "Palace of Darkness", "Swamp Palace", "Thieves Town", "Skull Woods", "Ice Palace",
+ "Misery Mire", "Turtle Rock", "Ganons Tower"
]
- for game_name, endpoint in multi_trackers.items():
- if any(slot.game == game_name for slot in room.seed.slots) or current == game_name:
- enabled.append({
- "name": game_name,
- "endpoint": endpoint.__name__,
- "current": current == game_name}
- )
- return enabled
-
-
-def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]:
- room: Room = Room.get(tracker=tracker)
- if not room:
- return None
- locations, names, use_door_tracker, checks_in_area, player_location_to_area, \
- precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
- get_static_room_data(room)
+ class RegionCounts(NamedTuple):
+ total: int
+ checked: int
- checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
- for playernumber in range(1, len(team) + 1) if playernumber not in groups}
- for teamnumber, team in enumerate(names)}
+ def prepare_inventories(team: int, player: int, inventory: Counter[str], tracker_data: TrackerData):
+ for item, (prog_item, level) in non_progressive_items.items():
+ if item in inventory:
+ inventory[prog_item] = min(max(inventory[prog_item], level), progressive_item_max[prog_item])
- percent_total_checks_done = {teamnumber: {playernumber: 0
- for playernumber in range(1, len(team) + 1) if playernumber not in groups}
- for teamnumber, team in enumerate(names)}
+ for bottle in bottle_items:
+ inventory["Bottles"] = min(inventory["Bottles"] + inventory[bottle], 4)
- total_locations = {teamnumber: sum(len(locations[playernumber])
- for playernumber in range(1, len(team) + 1) if playernumber not in groups)
- for teamnumber, team in enumerate(names)}
+ if "Progressive Bow (Alt)" in inventory:
+ inventory["Progressive Bow"] += inventory["Progressive Bow (Alt)"]
+ inventory["Progressive Bow"] = min(inventory["Progressive Bow"], progressive_item_max["Progressive Bow"])
- hints = {team: set() for team in range(len(names))}
- if room.multisave:
- multisave = restricted_loads(room.multisave)
- else:
- multisave = {}
- if "hints" in multisave:
- for (team, slot), slot_hints in multisave["hints"].items():
- hints[team] |= set(slot_hints)
-
- for (team, player), locations_checked in multisave.get("location_checks", {}).items():
- if player in groups:
- continue
- player_locations = locations[player]
- checks_done[team][player]["Total"] = len(locations_checked)
- percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] /
- len(player_locations) * 100) \
- if player_locations else 100
-
- activity_timers = {}
- now = datetime.datetime.utcnow()
- for (team, player), timestamp in multisave.get("client_activity_timers", []):
- activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
-
- player_names = {}
- completed_worlds = 0
- states: typing.Dict[typing.Tuple[int, int], int] = {}
- for team, names in enumerate(names):
- for player, name in enumerate(names, 1):
- player_names[team, player] = name
- states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0)
- if states[team, player] == 30: # Goal Completed
- completed_worlds += 1
- long_player_names = player_names.copy()
- for (team, player), alias in multisave.get("name_aliases", {}).items():
- player_names[team, player] = alias
- long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})"
-
- video = {}
- for (team, player), data in multisave.get("video", []):
- video[team, player] = data
-
- return dict(
- player_names=player_names, room=room, checks_done=checks_done,
- percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area,
- activity_timers=activity_timers, video=video, hints=hints,
- long_player_names=long_player_names,
- multisave=multisave, precollected_items=precollected_items, groups=groups,
- locations=locations, total_locations=total_locations, games=games, states=states,
- completed_worlds=completed_worlds,
- custom_locations=custom_locations, custom_items=custom_items,
- )
+ # Highlight 'bombs' if we received any bomb upgrades in bombless start.
+ # In race mode, we'll just assume bombless start for simplicity.
+ if tracker_data.get_slot_data(team, player).get("bombless_start", True):
+ inventory["Bombs"] = sum(count for item, count in inventory.items() if item.startswith("Bomb Upgrade"))
+ else:
+ inventory["Bombs"] = 1
+
+ # Triforce item if we meet goal.
+ if tracker_data.get_room_client_statuses()[team, player] == ClientStatus.CLIENT_GOAL:
+ inventory["Triforce"] = 1
+
+ def render_ALinkToThePast_multiworld_tracker(tracker_data: TrackerData, enabled_trackers: List[str]):
+ inventories: Dict[Tuple[int, int], Counter[str]] = {
+ (team, player): collections.Counter({
+ tracker_data.item_id_to_name["A Link to the Past"][code]: count
+ for code, count in tracker_data.get_player_inventory_counts(team, player).items()
+ })
+ for team, players in tracker_data.get_all_players().items()
+ for player in players if tracker_data.get_slot_info(team, player).game == "A Link to the Past"
+ }
+ # Translate non-progression items to progression items for tracker simplicity.
+ for (team, player), inventory in inventories.items():
+ prepare_inventories(team, player, inventory, tracker_data)
+
+ regions: Dict[Tuple[int, int], Dict[str, RegionCounts]] = {
+ (team, player): {
+ region_name: RegionCounts(
+ total=len(tracker_data._multidata["checks_in_area"][player][region_name]),
+ checked=sum(
+ 1 for location in tracker_data._multidata["checks_in_area"][player][region_name]
+ if location in tracker_data.get_player_checked_locations(team, player)
+ ),
+ )
+ for region_name in known_regions
+ }
+ for team, players in tracker_data.get_all_players().items()
+ for player in players if tracker_data.get_slot_info(team, player).game == "A Link to the Past"
+ }
-def _get_inventory_data(data: typing.Dict[str, typing.Any]) \
- -> typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]]:
- inventory: typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]] = {
- teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
- for teamnumber, team_data in data["checks_done"].items()
- }
+ # Get a totals count.
+ for player, player_regions in regions.items():
+ total = 0
+ checked = 0
+ for region, region_counts in player_regions.items():
+ total += region_counts.total
+ checked += region_counts.checked
+ regions[player]["Total"] = RegionCounts(total, checked)
+
+ return render_template(
+ "multitracker__ALinkToThePast.html",
+ enabled_trackers=enabled_trackers,
+ current_tracker="A Link to the Past",
+ room=tracker_data.room,
+ all_slots=tracker_data.get_all_slots(),
+ room_players=tracker_data.get_all_players(),
+ locations=tracker_data.get_room_locations(),
+ locations_complete=tracker_data.get_room_locations_complete(),
+ total_team_locations=tracker_data.get_team_locations_total_count(),
+ total_team_locations_complete=tracker_data.get_team_locations_checked_count(),
+ player_names_with_alias=tracker_data.get_room_long_player_names(),
+ completed_worlds=tracker_data.get_team_completed_worlds_count(),
+ games=tracker_data.get_room_games(),
+ states=tracker_data.get_room_client_statuses(),
+ hints=tracker_data.get_team_hints(),
+ activity_timers=tracker_data.get_room_last_activity(),
+ videos=tracker_data.get_room_videos(),
+ item_id_to_name=tracker_data.item_id_to_name,
+ location_id_to_name=tracker_data.location_id_to_name,
+ inventories=inventories,
+ regions=regions,
+ known_regions=known_regions,
+ )
+
+ def render_ALinkToThePast_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
+ inventory = collections.Counter({
+ tracker_data.item_id_to_name["A Link to the Past"][code]: count
+ for code, count in tracker_data.get_player_inventory_counts(team, player).items()
+ })
+
+ # Translate non-progression items to progression items for tracker simplicity.
+ prepare_inventories(team, player, inventory, tracker_data)
+
+ regions = {
+ region_name: {
+ "checked": sum(
+ 1 for location in tracker_data._multidata["checks_in_area"][player][region_name]
+ if location in tracker_data.get_player_checked_locations(team, player)
+ ),
+ "locations": [
+ (
+ tracker_data.location_id_to_name["A Link to the Past"][location],
+ location in tracker_data.get_player_checked_locations(team, player)
+ )
+ for location in tracker_data._multidata["checks_in_area"][player][region_name]
+ ],
+ }
+ for region_name in known_regions
+ }
- groups = data["groups"]
-
- for (team, player), locations_checked in data["multisave"].get("location_checks", {}).items():
- if player in data["groups"]:
- continue
- player_locations = data["locations"][player]
- precollected = data["precollected_items"][player]
- for item_id in precollected:
- inventory[team][player][item_id] += 1
- for location in locations_checked:
- item_id, recipient, flags = player_locations[location]
- recipients = groups.get(recipient, [recipient])
- for recipient in recipients:
- inventory[team][recipient][item_id] += 1
- return inventory
-
-
-def _get_named_inventory(inventory: typing.Dict[int, int], custom_items: typing.Dict[int, str] = None) \
- -> typing.Dict[str, int]:
- """slow"""
- if custom_items:
- mapping = collections.ChainMap(custom_items, lookup_any_item_id_to_name)
- else:
- mapping = lookup_any_item_id_to_name
+ # Sort locations in regions by name
+ for region in regions:
+ regions[region]["locations"].sort()
+
+ return render_template(
+ template_name_or_list="tracker__ALinkToThePast.html",
+ room=tracker_data.room,
+ team=team,
+ player=player,
+ inventory=inventory,
+ player_name=tracker_data.get_player_name(team, player),
+ regions=regions,
+ known_regions=known_regions,
+ )
+
+ _multiworld_trackers["A Link to the Past"] = render_ALinkToThePast_multiworld_tracker
+ _player_trackers["A Link to the Past"] = render_ALinkToThePast_tracker
+
+if "Minecraft" in network_data_package["games"]:
+ def render_Minecraft_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
+ icons = {
+ "Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
+ "Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png",
+ "Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png",
+ "Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png",
+ "Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png",
+ "Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png",
+ "Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png",
+ "Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png",
+ "Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png",
+ "Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png",
+ "Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png",
+ "Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png",
+ "Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png",
+ "Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png",
+ "Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png",
+ "Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
+ "Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
+ "Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
+ "Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png",
+ "Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
+ "Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
+ "Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
+ "Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png",
+ "Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
+ "Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
+ "Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
+ "Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png",
+ "Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png",
+ "Saddle": "https://i.imgur.com/2QtDyR0.png",
+ "Channeling Book": "https://i.imgur.com/J3WsYZw.png",
+ "Silk Touch Book": "https://i.imgur.com/iqERxHQ.png",
+ "Piercing IV Book": "https://i.imgur.com/OzJptGz.png",
+ }
- return collections.Counter({mapping.get(item_id, None): count for item_id, count in inventory.items()})
+ minecraft_location_ids = {
+ "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070,
+ 42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077],
+ "Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
+ 42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014],
+ "The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046],
+ "Adventure": [42047, 42050, 42096, 42097, 42098, 42059, 42055, 42072, 42003, 42109, 42035, 42016, 42020,
+ 42048, 42054, 42068, 42043, 42106, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42105,
+ 42099, 42103, 42110, 42100],
+ "Husbandry": [42065, 42067, 42078, 42022, 42113, 42107, 42007, 42079, 42013, 42028, 42036, 42108, 42111,
+ 42112,
+ 42057, 42063, 42053, 42102, 42101, 42092, 42093, 42094, 42095],
+ "Archipelago": [42080, 42081, 42082, 42083, 42084, 42085, 42086, 42087, 42088, 42089, 42090, 42091],
+ }
+ display_data = {}
-@app.route('/tracker/')
-@cache.memoize(timeout=60) # multisave is currently created at most every minute
-def get_multiworld_tracker(tracker: UUID):
- data = _get_multiworld_tracker_data(tracker)
- if not data:
- abort(404)
+ # Determine display for progressive items
+ progressive_items = {
+ "Progressive Tools": 45013,
+ "Progressive Weapons": 45012,
+ "Progressive Armor": 45014,
+ "Progressive Resource Crafting": 45001
+ }
+ progressive_names = {
+ "Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"],
+ "Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"],
+ "Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"],
+ "Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"]
+ }
- data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic")
+ inventory = tracker_data.get_player_inventory_counts(team, player)
+ for item_name, item_id in progressive_items.items():
+ level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
+ display_name = progressive_names[item_name][level]
+ base_name = item_name.split(maxsplit=1)[1].lower().replace(" ", "_")
+ display_data[base_name + "_url"] = icons[display_name]
+
+ # Multi-items
+ multi_items = {
+ "3 Ender Pearls": 45029,
+ "8 Netherite Scrap": 45015,
+ "Dragon Egg Shard": 45043
+ }
+ for item_name, item_id in multi_items.items():
+ base_name = item_name.split()[-1].lower()
+ count = inventory[item_id]
+ if count >= 0:
+ display_data[base_name + "_count"] = count
+
+ # Victory condition
+ game_state = tracker_data.get_player_client_status(team, player)
+ display_data["game_finished"] = game_state == 30
+
+ # Turn location IDs into advancement tab counts
+ checked_locations = tracker_data.get_player_checked_locations(team, player)
+ lookup_name = lambda id: tracker_data.location_id_to_name["Minecraft"][id]
+ location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
+ for tab_name, tab_locations in minecraft_location_ids.items()}
+ checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
+ for tab_name, tab_locations in minecraft_location_ids.items()}
+ checks_done["Total"] = len(checked_locations)
+ checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()}
+ checks_in_area["Total"] = sum(checks_in_area.values())
+
+ lookup_any_item_id_to_name = tracker_data.item_id_to_name["Minecraft"]
+ return render_template(
+ "tracker__Minecraft.html",
+ inventory=inventory,
+ icons=icons,
+ acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0},
+ player=player,
+ team=team,
+ room=tracker_data.room,
+ player_name=tracker_data.get_player_name(team, player),
+ saving_second=tracker_data.get_room_saving_second(),
+ checks_done=checks_done,
+ checks_in_area=checks_in_area,
+ location_info=location_info,
+ **display_data,
+ )
+
+ _player_trackers["Minecraft"] = render_Minecraft_tracker
+
+if "Ocarina of Time" in network_data_package["games"]:
+ def render_OcarinaOfTime_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
+ icons = {
+ "Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
+ "Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png",
+ "Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png",
+ "Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png",
+ "Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png",
+ "Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png",
+ "Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png",
+ "Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png",
+ "Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png",
+ "Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png",
+ "Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png",
+ "Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png",
+ "Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png",
+ "Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png",
+ "Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png",
+ "Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png",
+ "Dins Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png",
+ "Farores Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png",
+ "Nayrus Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png",
+ "Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png",
+ "Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png",
+ "Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png",
+ "Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png",
+ "Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png",
+ "Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png",
+ "Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png",
+ "Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png",
+ "Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png",
+ "Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png",
+ "Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png",
+ "Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png",
+ "Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png",
+ "Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png",
+ "Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png",
+ "Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png",
+ "Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png",
+ "Gold Skulltula Token": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png",
+ "Triforce Piece": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png",
+ "Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png",
+ "Zeldas Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
+ "Eponas Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
+ "Sarias Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
+ "Suns Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
+ "Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
+ "Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
+ "Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png",
+ "Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png",
+ "Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png",
+ "Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png",
+ "Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png",
+ "Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png",
+ "Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png",
+ "Boss Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png",
+ }
- return render_template("multiTracker.html", **data)
+ display_data = {}
-if "Factorio" in games:
- @app.route('/tracker//Factorio')
- @cache.memoize(timeout=60) # multisave is currently created at most every minute
- def get_Factorio_multiworld_tracker(tracker: UUID):
- data = _get_multiworld_tracker_data(tracker)
- if not data:
- abort(404)
+ # Determine display for progressive items
+ progressive_items = {
+ "Progressive Hookshot": 66128,
+ "Progressive Strength Upgrade": 66129,
+ "Progressive Wallet": 66133,
+ "Progressive Scale": 66134,
+ "Magic Meter": 66138,
+ "Ocarina": 66139,
+ }
- data["inventory"] = _get_inventory_data(data)
- data["named_inventory"] = {team_id : {
- player_id: _get_named_inventory(inventory, data["custom_items"])
- for player_id, inventory in team_inventory.items()
- } for team_id, team_inventory in data["inventory"].items()}
- data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio")
+ progressive_names = {
+ "Progressive Hookshot": ["Hookshot", "Hookshot", "Longshot"],
+ "Progressive Strength Upgrade": ["Goron Bracelet", "Goron Bracelet", "Silver Gauntlets",
+ "Golden Gauntlets"],
+ "Progressive Wallet": ["Adults Wallet", "Adults Wallet", "Giants Wallet", "Giants Wallet"],
+ "Progressive Scale": ["Silver Scale", "Silver Scale", "Gold Scale"],
+ "Magic Meter": ["Small Magic", "Small Magic", "Large Magic"],
+ "Ocarina": ["Fairy Ocarina", "Fairy Ocarina", "Ocarina of Time"]
+ }
- return render_template("multiFactorioTracker.html", **data)
+ inventory = tracker_data.get_player_inventory_counts(team, player)
+ for item_name, item_id in progressive_items.items():
+ level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
+ display_name = progressive_names[item_name][level]
+ if item_name.startswith("Progressive"):
+ base_name = item_name.split(maxsplit=1)[1].lower().replace(" ", "_")
+ else:
+ base_name = item_name.lower().replace(" ", "_")
+ display_data[base_name + "_url"] = icons[display_name]
+
+ if base_name == "hookshot":
+ display_data["hookshot_length"] = {0: "", 1: "H", 2: "L"}.get(level)
+ if base_name == "wallet":
+ display_data["wallet_size"] = {0: "99", 1: "200", 2: "500", 3: "999"}.get(level)
+
+ # Determine display for bottles. Show letter if it's obtained, determine bottle count
+ bottle_ids = [66015, 66020, 66021, 66140, 66141, 66142, 66143, 66144, 66145, 66146, 66147, 66148]
+ display_data["bottle_count"] = min(sum(map(lambda item_id: inventory[item_id], bottle_ids)), 4)
+ display_data["bottle_url"] = icons["Rutos Letter"] if inventory[66021] > 0 else icons["Bottle"]
+
+ # Determine bombchu display
+ display_data["has_bombchus"] = any(map(lambda item_id: inventory[item_id] > 0, [66003, 66106, 66107, 66137]))
+
+ # Multi-items
+ multi_items = {
+ "Gold Skulltula Token": 66091,
+ "Triforce Piece": 66202,
+ }
+ for item_name, item_id in multi_items.items():
+ base_name = item_name.split()[-1].lower()
+ display_data[base_name + "_count"] = inventory[item_id]
+
+ # Gather dungeon locations
+ area_id_ranges = {
+ "Overworld": ((67000, 67263), (67269, 67280), (67747, 68024), (68054, 68062)),
+ "Deku Tree": ((67281, 67303), (68063, 68077)),
+ "Dodongo's Cavern": ((67304, 67334), (68078, 68160)),
+ "Jabu Jabu's Belly": ((67335, 67359), (68161, 68188)),
+ "Bottom of the Well": ((67360, 67384), (68189, 68230)),
+ "Forest Temple": ((67385, 67420), (68231, 68281)),
+ "Fire Temple": ((67421, 67457), (68282, 68350)),
+ "Water Temple": ((67458, 67484), (68351, 68483)),
+ "Shadow Temple": ((67485, 67532), (68484, 68565)),
+ "Spirit Temple": ((67533, 67582), (68566, 68625)),
+ "Ice Cavern": ((67583, 67596), (68626, 68649)),
+ "Gerudo Training Ground": ((67597, 67635), (68650, 68656)),
+ "Thieves' Hideout": ((67264, 67268), (68025, 68053)),
+ "Ganon's Castle": ((67636, 67673), (68657, 68705)),
+ }
+ def lookup_and_trim(id, area):
+ full_name = tracker_data.location_id_to_name["Ocarina of Time"][id]
+ if "Ganons Tower" in full_name:
+ return full_name
+ if area not in ["Overworld", "Thieves' Hideout"]:
+ # trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC
+ return full_name[len(area):]
+ return full_name
-@app.route('/tracker//A Link to the Past')
-@cache.memoize(timeout=60) # multisave is currently created at most every minute
-def get_LttP_multiworld_tracker(tracker: UUID):
- room: Room = Room.get(tracker=tracker)
- if not room:
- abort(404)
- locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
- precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
- get_static_room_data(room)
+ locations = tracker_data.get_player_locations(team, player)
+ checked_locations = tracker_data.get_player_checked_locations(team, player).intersection(set(locations))
+ location_info = {}
+ checks_done = {}
+ checks_in_area = {}
+ for area, ranges in area_id_ranges.items():
+ location_info[area] = {}
+ checks_done[area] = 0
+ checks_in_area[area] = 0
+ for r in ranges:
+ min_id, max_id = r
+ for id in range(min_id, max_id + 1):
+ if id in locations:
+ checked = id in checked_locations
+ location_info[area][lookup_and_trim(id, area)] = checked
+ checks_in_area[area] += 1
+ checks_done[area] += checked
+
+ checks_done["Total"] = sum(checks_done.values())
+ checks_in_area["Total"] = sum(checks_in_area.values())
+
+ # Give skulltulas on non-tracked locations
+ non_tracked_locations = tracker_data.get_player_checked_locations(team, player).difference(set(locations))
+ for id in non_tracked_locations:
+ if "GS" in lookup_and_trim(id, ""):
+ display_data["token_count"] += 1
+
+ oot_y = "✔"
+ oot_x = "✕"
+
+ # Gather small and boss key info
+ small_key_counts = {
+ "Forest Temple": oot_y if inventory[66203] else inventory[66175],
+ "Fire Temple": oot_y if inventory[66204] else inventory[66176],
+ "Water Temple": oot_y if inventory[66205] else inventory[66177],
+ "Spirit Temple": oot_y if inventory[66206] else inventory[66178],
+ "Shadow Temple": oot_y if inventory[66207] else inventory[66179],
+ "Bottom of the Well": oot_y if inventory[66208] else inventory[66180],
+ "Gerudo Training Ground": oot_y if inventory[66209] else inventory[66181],
+ "Thieves' Hideout": oot_y if inventory[66210] else inventory[66182],
+ "Ganon's Castle": oot_y if inventory[66211] else inventory[66183],
+ }
+ boss_key_counts = {
+ "Forest Temple": oot_y if inventory[66149] else oot_x,
+ "Fire Temple": oot_y if inventory[66150] else oot_x,
+ "Water Temple": oot_y if inventory[66151] else oot_x,
+ "Spirit Temple": oot_y if inventory[66152] else oot_x,
+ "Shadow Temple": oot_y if inventory[66153] else oot_x,
+ "Ganon's Castle": oot_y if inventory[66154] else oot_x,
+ }
- inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if
- playernumber not in groups}
- for teamnumber, team in enumerate(names)}
+ # Victory condition
+ game_state = tracker_data.get_player_client_status(team, player)
+ display_data["game_finished"] = game_state == 30
+
+ lookup_any_item_id_to_name = tracker_data.item_id_to_name["Ocarina of Time"]
+ return render_template(
+ "tracker__OcarinaOfTime.html",
+ inventory=inventory,
+ player=player,
+ team=team,
+ room=tracker_data.room,
+ player_name=tracker_data.get_player_name(team, player),
+ icons=icons,
+ acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0},
+ checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
+ small_key_counts=small_key_counts,
+ boss_key_counts=boss_key_counts,
+ **display_data,
+ )
+
+ _player_trackers["Ocarina of Time"] = render_OcarinaOfTime_tracker
+
+if "Timespinner" in network_data_package["games"]:
+ def render_Timespinner_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
+ icons = {
+ "Timespinner Wheel": "https://timespinnerwiki.com/mediawiki/images/7/76/Timespinner_Wheel.png",
+ "Timespinner Spindle": "https://timespinnerwiki.com/mediawiki/images/1/1a/Timespinner_Spindle.png",
+ "Timespinner Gear 1": "https://timespinnerwiki.com/mediawiki/images/3/3c/Timespinner_Gear_1.png",
+ "Timespinner Gear 2": "https://timespinnerwiki.com/mediawiki/images/e/e9/Timespinner_Gear_2.png",
+ "Timespinner Gear 3": "https://timespinnerwiki.com/mediawiki/images/2/22/Timespinner_Gear_3.png",
+ "Talaria Attachment": "https://timespinnerwiki.com/mediawiki/images/6/61/Talaria_Attachment.png",
+ "Succubus Hairpin": "https://timespinnerwiki.com/mediawiki/images/4/49/Succubus_Hairpin.png",
+ "Lightwall": "https://timespinnerwiki.com/mediawiki/images/0/03/Lightwall.png",
+ "Celestial Sash": "https://timespinnerwiki.com/mediawiki/images/f/f1/Celestial_Sash.png",
+ "Twin Pyramid Key": "https://timespinnerwiki.com/mediawiki/images/4/49/Twin_Pyramid_Key.png",
+ "Security Keycard D": "https://timespinnerwiki.com/mediawiki/images/1/1b/Security_Keycard_D.png",
+ "Security Keycard C": "https://timespinnerwiki.com/mediawiki/images/e/e5/Security_Keycard_C.png",
+ "Security Keycard B": "https://timespinnerwiki.com/mediawiki/images/f/f6/Security_Keycard_B.png",
+ "Security Keycard A": "https://timespinnerwiki.com/mediawiki/images/b/b9/Security_Keycard_A.png",
+ "Library Keycard V": "https://timespinnerwiki.com/mediawiki/images/5/50/Library_Keycard_V.png",
+ "Tablet": "https://timespinnerwiki.com/mediawiki/images/a/a0/Tablet.png",
+ "Elevator Keycard": "https://timespinnerwiki.com/mediawiki/images/5/55/Elevator_Keycard.png",
+ "Oculus Ring": "https://timespinnerwiki.com/mediawiki/images/8/8d/Oculus_Ring.png",
+ "Water Mask": "https://timespinnerwiki.com/mediawiki/images/0/04/Water_Mask.png",
+ "Gas Mask": "https://timespinnerwiki.com/mediawiki/images/2/2e/Gas_Mask.png",
+ "Djinn Inferno": "https://timespinnerwiki.com/mediawiki/images/f/f6/Djinn_Inferno.png",
+ "Pyro Ring": "https://timespinnerwiki.com/mediawiki/images/2/2c/Pyro_Ring.png",
+ "Infernal Flames": "https://timespinnerwiki.com/mediawiki/images/1/1f/Infernal_Flames.png",
+ "Fire Orb": "https://timespinnerwiki.com/mediawiki/images/3/3e/Fire_Orb.png",
+ "Royal Ring": "https://timespinnerwiki.com/mediawiki/images/f/f3/Royal_Ring.png",
+ "Plasma Geyser": "https://timespinnerwiki.com/mediawiki/images/1/12/Plasma_Geyser.png",
+ "Plasma Orb": "https://timespinnerwiki.com/mediawiki/images/4/44/Plasma_Orb.png",
+ "Kobo": "https://timespinnerwiki.com/mediawiki/images/c/c6/Familiar_Kobo.png",
+ "Merchant Crow": "https://timespinnerwiki.com/mediawiki/images/4/4e/Familiar_Crow.png",
+ }
- checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
- for playernumber in range(1, len(team) + 1) if playernumber not in groups}
- for teamnumber, team in enumerate(names)}
+ timespinner_location_ids = {
+ "Present": [
+ 1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009,
+ 1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019,
+ 1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029,
+ 1337030, 1337031, 1337032, 1337033, 1337034, 1337035, 1337036, 1337037, 1337038, 1337039,
+ 1337040, 1337041, 1337042, 1337043, 1337044, 1337045, 1337046, 1337047, 1337048, 1337049,
+ 1337050, 1337051, 1337052, 1337053, 1337054, 1337055, 1337056, 1337057, 1337058, 1337059,
+ 1337060, 1337061, 1337062, 1337063, 1337064, 1337065, 1337066, 1337067, 1337068, 1337069,
+ 1337070, 1337071, 1337072, 1337073, 1337074, 1337075, 1337076, 1337077, 1337078, 1337079,
+ 1337080, 1337081, 1337082, 1337083, 1337084, 1337085],
+ "Past": [
+ 1337086, 1337087, 1337088, 1337089,
+ 1337090, 1337091, 1337092, 1337093, 1337094, 1337095, 1337096, 1337097, 1337098, 1337099,
+ 1337100, 1337101, 1337102, 1337103, 1337104, 1337105, 1337106, 1337107, 1337108, 1337109,
+ 1337110, 1337111, 1337112, 1337113, 1337114, 1337115, 1337116, 1337117, 1337118, 1337119,
+ 1337120, 1337121, 1337122, 1337123, 1337124, 1337125, 1337126, 1337127, 1337128, 1337129,
+ 1337130, 1337131, 1337132, 1337133, 1337134, 1337135, 1337136, 1337137, 1337138, 1337139,
+ 1337140, 1337141, 1337142, 1337143, 1337144, 1337145, 1337146, 1337147, 1337148, 1337149,
+ 1337150, 1337151, 1337152, 1337153, 1337154, 1337155,
+ 1337171, 1337172, 1337173, 1337174, 1337175],
+ "Ancient Pyramid": [
+ 1337236,
+ 1337246, 1337247, 1337248, 1337249]
+ }
- percent_total_checks_done = {teamnumber: {playernumber: 0
- for playernumber in range(1, len(team) + 1) if playernumber not in groups}
- for teamnumber, team in enumerate(names)}
+ slot_data = tracker_data.get_slot_data(team, player)
+ if (slot_data["DownloadableItems"]):
+ timespinner_location_ids["Present"] += [
+ 1337156, 1337157, 1337159,
+ 1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
+ 1337170]
+ if (slot_data["Cantoran"]):
+ timespinner_location_ids["Past"].append(1337176)
+ if (slot_data["LoreChecks"]):
+ timespinner_location_ids["Present"] += [
+ 1337177, 1337178, 1337179,
+ 1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187]
+ timespinner_location_ids["Past"] += [
+ 1337188, 1337189,
+ 1337190, 1337191, 1337192, 1337193, 1337194, 1337195, 1337196, 1337197, 1337198]
+ if (slot_data["GyreArchives"]):
+ timespinner_location_ids["Ancient Pyramid"] += [
+ 1337237, 1337238, 1337239,
+ 1337240, 1337241, 1337242, 1337243, 1337244, 1337245]
+
+ display_data = {}
+
+ # Victory condition
+ game_state = tracker_data.get_player_client_status(team, player)
+ display_data["game_finished"] = game_state == 30
+
+ inventory = tracker_data.get_player_inventory_counts(team, player)
+
+ # Turn location IDs into advancement tab counts
+ checked_locations = tracker_data.get_player_checked_locations(team, player)
+ lookup_name = lambda id: tracker_data.location_id_to_name["Timespinner"][id]
+ location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
+ for tab_name, tab_locations in timespinner_location_ids.items()}
+ checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
+ for tab_name, tab_locations in timespinner_location_ids.items()}
+ checks_done["Total"] = len(checked_locations)
+ checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in timespinner_location_ids.items()}
+ checks_in_area["Total"] = sum(checks_in_area.values())
+ options = {k for k, v in slot_data.items() if v}
+
+ lookup_any_item_id_to_name = tracker_data.item_id_to_name["Timespinner"]
+ return render_template(
+ "tracker__Timespinner.html",
+ inventory=inventory,
+ icons=icons,
+ acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0},
+ player=player,
+ team=team,
+ room=tracker_data.room,
+ player_name=tracker_data.get_player_name(team, player),
+ checks_done=checks_done,
+ checks_in_area=checks_in_area,
+ location_info=location_info,
+ options=options,
+ **display_data,
+ )
+
+ _player_trackers["Timespinner"] = render_Timespinner_tracker
+
+if "Super Metroid" in network_data_package["games"]:
+ def render_SuperMetroid_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
+ icons = {
+ "Energy Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ETank.png",
+ "Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Missile.png",
+ "Super Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Super.png",
+ "Power Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/PowerBomb.png",
+ "Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Bomb.png",
+ "Charge Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Charge.png",
+ "Ice Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Ice.png",
+ "Hi-Jump Boots": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/HiJump.png",
+ "Speed Booster": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpeedBooster.png",
+ "Wave Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Wave.png",
+ "Spazer": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Spazer.png",
+ "Spring Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpringBall.png",
+ "Varia Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Varia.png",
+ "Plasma Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Plasma.png",
+ "Grappling Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Grapple.png",
+ "Morph Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Morph.png",
+ "Reserve Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Reserve.png",
+ "Gravity Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Gravity.png",
+ "X-Ray Scope": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/XRayScope.png",
+ "Space Jump": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpaceJump.png",
+ "Screw Attack": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ScrewAttack.png",
+ "Nothing": "",
+ "No Energy": "",
+ "Kraid": "",
+ "Phantoon": "",
+ "Draygon": "",
+ "Ridley": "",
+ "Mother Brain": "",
+ }
- hints = {team: set() for team in range(len(names))}
- if room.multisave:
- multisave = restricted_loads(room.multisave)
- else:
- multisave = {}
- if "hints" in multisave:
- for (team, slot), slot_hints in multisave["hints"].items():
- hints[team] |= set(slot_hints)
-
- def attribute_item(team: int, recipient: int, item: int):
- nonlocal inventory
- target_item = links.get(item, item)
- if item in levels: # non-progressive
- inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item])
- else:
- inventory[team][recipient][target_item] += 1
-
- for (team, player), locations_checked in multisave.get("location_checks", {}).items():
- if player in groups:
- continue
- player_locations = locations[player]
- if precollected_items:
- precollected = precollected_items[player]
- for item_id in precollected:
- attribute_item(team, player, item_id)
- for location in locations_checked:
- if location not in player_locations or location not in player_location_to_area[player]:
- continue
- item, recipient, flags = player_locations[location]
- recipients = groups.get(recipient, [recipient])
- for recipient in recipients:
- attribute_item(team, recipient, item)
- checks_done[team][player][player_location_to_area[player][location]] += 1
- checks_done[team][player]["Total"] += 1
- percent_total_checks_done[team][player] = int(
- checks_done[team][player]["Total"] / len(player_locations) * 100) if \
- player_locations else 100
-
- for (team, player), game_state in multisave.get("client_game_state", {}).items():
- if player in groups:
- continue
- if game_state == 30:
- inventory[team][player][106] = 1 # Triforce
-
- player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
- player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
- for loc_data in locations.values():
- for values in loc_data.values():
- item_id, item_player, flags = values
-
- if item_id in ids_big_key:
- player_big_key_locations[item_player].add(ids_big_key[item_id])
- elif item_id in ids_small_key:
- player_small_key_locations[item_player].add(ids_small_key[item_id])
- group_big_key_locations = set()
- group_key_locations = set()
- for player in [player for player in range(1, len(names[0]) + 1) if player not in groups]:
- group_key_locations |= player_small_key_locations[player]
- group_big_key_locations |= player_big_key_locations[player]
-
- activity_timers = {}
- now = datetime.datetime.utcnow()
- for (team, player), timestamp in multisave.get("client_activity_timers", []):
- activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
-
- player_names = {}
- for team, names in enumerate(names):
- for player, name in enumerate(names, 1):
- player_names[(team, player)] = name
- long_player_names = player_names.copy()
- for (team, player), alias in multisave.get("name_aliases", {}).items():
- player_names[(team, player)] = alias
- long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})"
-
- video = {}
- for (team, player), data in multisave.get("video", []):
- video[(team, player)] = data
-
- enabled_multiworld_trackers = get_enabled_multiworld_trackers(room, "A Link to the Past")
-
- return render_template("lttpMultiTracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
- lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
- tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
- multi_items=multi_items, checks_done=checks_done,
- percent_total_checks_done=percent_total_checks_done,
- ordered_areas=ordered_areas, checks_in_area=seed_checks_in_area,
- activity_timers=activity_timers,
- key_locations=group_key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids,
- video=video, big_key_locations=group_big_key_locations,
- hints=hints, long_player_names=long_player_names,
- enabled_multiworld_trackers=enabled_multiworld_trackers)
-
-
-game_specific_trackers: typing.Dict[str, typing.Callable] = {
- "Minecraft": __renderMinecraftTracker,
- "Ocarina of Time": __renderOoTTracker,
- "Timespinner": __renderTimespinnerTracker,
- "A Link to the Past": __renderAlttpTracker,
- "ChecksFinder": __renderChecksfinder,
- "Super Metroid": __renderSuperMetroidTracker,
- "Starcraft 2 Wings of Liberty": __renderSC2WoLTracker
-}
-
-multi_trackers: typing.Dict[str, typing.Callable] = {
- "A Link to the Past": get_LttP_multiworld_tracker,
-}
-
-if "Factorio" in games:
- multi_trackers["Factorio"] = get_Factorio_multiworld_tracker
+ multi_items = {
+ "Energy Tank": 83000,
+ "Missile": 83001,
+ "Super Missile": 83002,
+ "Power Bomb": 83003,
+ "Reserve Tank": 83020,
+ }
+
+ supermetroid_location_ids = {
+ 'Crateria/Blue Brinstar': [82005, 82007, 82008, 82026, 82029,
+ 82000, 82004, 82006, 82009, 82010,
+ 82011, 82012, 82027, 82028, 82034,
+ 82036, 82037],
+ 'Green/Pink Brinstar': [82017, 82023, 82030, 82033, 82035,
+ 82013, 82014, 82015, 82016, 82018,
+ 82019, 82021, 82022, 82024, 82025,
+ 82031],
+ 'Red Brinstar': [82038, 82042, 82039, 82040, 82041],
+ 'Kraid': [82043, 82048, 82044],
+ 'Norfair': [82050, 82053, 82061, 82066, 82068,
+ 82049, 82051, 82054, 82055, 82056,
+ 82062, 82063, 82064, 82065, 82067],
+ 'Lower Norfair': [82078, 82079, 82080, 82070, 82071,
+ 82073, 82074, 82075, 82076, 82077],
+ 'Crocomire': [82052, 82060, 82057, 82058, 82059],
+ 'Wrecked Ship': [82129, 82132, 82134, 82135, 82001,
+ 82002, 82003, 82128, 82130, 82131,
+ 82133],
+ 'West Maridia': [82138, 82136, 82137, 82139, 82140,
+ 82141, 82142],
+ 'East Maridia': [82143, 82145, 82150, 82152, 82154,
+ 82144, 82146, 82147, 82148, 82149,
+ 82151],
+ }
+
+ display_data = {}
+ inventory = tracker_data.get_player_inventory_counts(team, player)
+
+ for item_name, item_id in multi_items.items():
+ base_name = item_name.split()[0].lower()
+ display_data[base_name + "_count"] = inventory[item_id]
+
+ # Victory condition
+ game_state = tracker_data.get_player_client_status(team, player)
+ display_data["game_finished"] = game_state == 30
+
+ # Turn location IDs into advancement tab counts
+ checked_locations = tracker_data.get_player_checked_locations(team, player)
+ lookup_name = lambda id: tracker_data.location_id_to_name["Super Metroid"][id]
+ location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
+ for tab_name, tab_locations in supermetroid_location_ids.items()}
+ checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
+ for tab_name, tab_locations in supermetroid_location_ids.items()}
+ checks_done['Total'] = len(checked_locations)
+ checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in supermetroid_location_ids.items()}
+ checks_in_area['Total'] = sum(checks_in_area.values())
+
+ lookup_any_item_id_to_name = tracker_data.item_id_to_name["Super Metroid"]
+ return render_template(
+ "tracker__SuperMetroid.html",
+ inventory=inventory,
+ icons=icons,
+ acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0},
+ player=player,
+ team=team,
+ room=tracker_data.room,
+ player_name=tracker_data.get_player_name(team, player),
+ checks_done=checks_done,
+ checks_in_area=checks_in_area,
+ location_info=location_info,
+ **display_data,
+ )
+
+ _player_trackers["Super Metroid"] = render_SuperMetroid_tracker
+
+if "ChecksFinder" in network_data_package["games"]:
+ def render_ChecksFinder_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
+ icons = {
+ "Checks Available": "https://0rganics.org/archipelago/cf/spr_tiles_3.png",
+ "Map Width": "https://0rganics.org/archipelago/cf/spr_tiles_4.png",
+ "Map Height": "https://0rganics.org/archipelago/cf/spr_tiles_5.png",
+ "Map Bombs": "https://0rganics.org/archipelago/cf/spr_tiles_6.png",
+
+ "Nothing": "",
+ }
+
+ checksfinder_location_ids = {
+ "Tile 1": 81000,
+ "Tile 2": 81001,
+ "Tile 3": 81002,
+ "Tile 4": 81003,
+ "Tile 5": 81004,
+ "Tile 6": 81005,
+ "Tile 7": 81006,
+ "Tile 8": 81007,
+ "Tile 9": 81008,
+ "Tile 10": 81009,
+ "Tile 11": 81010,
+ "Tile 12": 81011,
+ "Tile 13": 81012,
+ "Tile 14": 81013,
+ "Tile 15": 81014,
+ "Tile 16": 81015,
+ "Tile 17": 81016,
+ "Tile 18": 81017,
+ "Tile 19": 81018,
+ "Tile 20": 81019,
+ "Tile 21": 81020,
+ "Tile 22": 81021,
+ "Tile 23": 81022,
+ "Tile 24": 81023,
+ "Tile 25": 81024,
+ }
+
+ display_data = {}
+ inventory = tracker_data.get_player_inventory_counts(team, player)
+ locations = tracker_data.get_player_locations(team, player)
+
+ # Multi-items
+ multi_items = {
+ "Map Width": 80000,
+ "Map Height": 80001,
+ "Map Bombs": 80002
+ }
+ for item_name, item_id in multi_items.items():
+ base_name = item_name.split()[-1].lower()
+ count = inventory[item_id]
+ display_data[base_name + "_count"] = count
+ display_data[base_name + "_display"] = count + 5
+
+ # Get location info
+ checked_locations = tracker_data.get_player_checked_locations(team, player)
+ lookup_name = lambda id: tracker_data.location_id_to_name["ChecksFinder"][id]
+ location_info = {tile_name: {lookup_name(tile_location): (tile_location in checked_locations)} for
+ tile_name, tile_location in checksfinder_location_ids.items() if
+ tile_location in set(locations)}
+ checks_done = {tile_name: len([tile_location]) for tile_name, tile_location in checksfinder_location_ids.items()
+ if tile_location in checked_locations and tile_location in set(locations)}
+ checks_done['Total'] = len(checked_locations)
+ checks_in_area = checks_done
+
+ # Calculate checks available
+ display_data["checks_unlocked"] = min(
+ display_data["width_count"] + display_data["height_count"] + display_data["bombs_count"] + 5, 25)
+ display_data["checks_available"] = max(display_data["checks_unlocked"] - len(checked_locations), 0)
+
+ # Victory condition
+ game_state = tracker_data.get_player_client_status(team, player)
+ display_data["game_finished"] = game_state == 30
+
+ lookup_any_item_id_to_name = tracker_data.item_id_to_name["ChecksFinder"]
+ return render_template(
+ "tracker__ChecksFinder.html",
+ inventory=inventory, icons=icons,
+ acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0},
+ player=player,
+ team=team,
+ room=tracker_data.room,
+ player_name=tracker_data.get_player_name(team, player),
+ checks_done=checks_done,
+ checks_in_area=checks_in_area,
+ location_info=location_info,
+ **display_data,
+ )
+
+ _player_trackers["ChecksFinder"] = render_ChecksFinder_tracker
+
+if "Starcraft 2" in network_data_package["games"]:
+ def render_Starcraft2_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
+ SC2WOL_LOC_ID_OFFSET = 1000
+ SC2HOTS_LOC_ID_OFFSET = 20000000 # Avoid clashes with The Legend of Zelda
+ SC2LOTV_LOC_ID_OFFSET = SC2HOTS_LOC_ID_OFFSET + 2000
+ SC2NCO_LOC_ID_OFFSET = SC2LOTV_LOC_ID_OFFSET + 2500
+
+ SC2WOL_ITEM_ID_OFFSET = 1000
+ SC2HOTS_ITEM_ID_OFFSET = SC2WOL_ITEM_ID_OFFSET + 1000
+ SC2LOTV_ITEM_ID_OFFSET = SC2HOTS_ITEM_ID_OFFSET + 1000
+
+ slot_data = tracker_data.get_slot_data(team, player)
+ minerals_per_item = slot_data.get("minerals_per_item", 15)
+ vespene_per_item = slot_data.get("vespene_per_item", 15)
+ starting_supply_per_item = slot_data.get("starting_supply_per_item", 2)
+
+ github_icon_base_url = "https://matthewmarinets.github.io/ap_sc2_icons/icons/"
+ organics_icon_base_url = "https://0rganics.org/archipelago/sc2wol/"
+
+ icons = {
+ "Starting Minerals": github_icon_base_url + "blizzard/icon-mineral-nobg.png",
+ "Starting Vespene": github_icon_base_url + "blizzard/icon-gas-terran-nobg.png",
+ "Starting Supply": github_icon_base_url + "blizzard/icon-supply-terran_nobg.png",
+
+ "Terran Infantry Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel1.png",
+ "Terran Infantry Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel2.png",
+ "Terran Infantry Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel3.png",
+ "Terran Infantry Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel1.png",
+ "Terran Infantry Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel2.png",
+ "Terran Infantry Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel3.png",
+ "Terran Vehicle Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel1.png",
+ "Terran Vehicle Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel2.png",
+ "Terran Vehicle Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel3.png",
+ "Terran Vehicle Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel1.png",
+ "Terran Vehicle Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel2.png",
+ "Terran Vehicle Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel3.png",
+ "Terran Ship Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel1.png",
+ "Terran Ship Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel2.png",
+ "Terran Ship Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel3.png",
+ "Terran Ship Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel1.png",
+ "Terran Ship Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel2.png",
+ "Terran Ship Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel3.png",
+
+ "Bunker": "https://static.wikia.nocookie.net/starcraft/images/c/c5/Bunker_SC2_Icon1.jpg",
+ "Missile Turret": "https://static.wikia.nocookie.net/starcraft/images/5/5f/MissileTurret_SC2_Icon1.jpg",
+ "Sensor Tower": "https://static.wikia.nocookie.net/starcraft/images/d/d2/SensorTower_SC2_Icon1.jpg",
+
+ "Projectile Accelerator (Bunker)": github_icon_base_url + "blizzard/btn-upgrade-zerg-stukov-bunkerresearchbundle_05.png",
+ "Neosteel Bunker (Bunker)": organics_icon_base_url + "NeosteelBunker.png",
+ "Titanium Housing (Missile Turret)": organics_icon_base_url + "TitaniumHousing.png",
+ "Hellstorm Batteries (Missile Turret)": github_icon_base_url + "blizzard/btn-ability-stetmann-corruptormissilebarrage.png",
+ "Advanced Construction (SCV)": github_icon_base_url + "blizzard/btn-ability-mengsk-trooper-advancedconstruction.png",
+ "Dual-Fusion Welders (SCV)": github_icon_base_url + "blizzard/btn-upgrade-swann-scvdoublerepair.png",
+ "Hostile Environment Adaptation (SCV)": github_icon_base_url + "blizzard/btn-upgrade-swann-hellarmor.png",
+ "Fire-Suppression System Level 1": organics_icon_base_url + "Fire-SuppressionSystem.png",
+ "Fire-Suppression System Level 2": github_icon_base_url + "blizzard/btn-upgrade-swann-firesuppressionsystem.png",
+
+ "Orbital Command": organics_icon_base_url + "OrbitalCommandCampaign.png",
+ "Planetary Command Module": github_icon_base_url + "original/btn-orbital-fortress.png",
+ "Lift Off (Planetary Fortress)": github_icon_base_url + "blizzard/btn-ability-terran-liftoff.png",
+ "Armament Stabilizers (Planetary Fortress)": github_icon_base_url + "blizzard/btn-ability-mengsk-siegetank-flyingtankarmament.png",
+ "Advanced Targeting (Planetary Fortress)": github_icon_base_url + "blizzard/btn-ability-terran-detectionconedebuff.png",
+
+ "Marine": "https://static.wikia.nocookie.net/starcraft/images/4/47/Marine_SC2_Icon1.jpg",
+ "Medic": github_icon_base_url + "blizzard/btn-unit-terran-medic.png",
+ "Firebat": github_icon_base_url + "blizzard/btn-unit-terran-firebat.png",
+ "Marauder": "https://static.wikia.nocookie.net/starcraft/images/b/ba/Marauder_SC2_Icon1.jpg",
+ "Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg",
+ "Ghost": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Ghost_SC2_Icon1.jpg",
+ "Spectre": github_icon_base_url + "original/btn-unit-terran-spectre.png",
+ "HERC": github_icon_base_url + "blizzard/btn-unit-terran-herc.png",
+
+ "Stimpack (Marine)": github_icon_base_url + "blizzard/btn-ability-terran-stimpack-color.png",
+ "Super Stimpack (Marine)": github_icon_base_url + "blizzard/btn-upgrade-terran-superstimppack.png",
+ "Combat Shield (Marine)": github_icon_base_url + "blizzard/btn-techupgrade-terran-combatshield-color.png",
+ "Laser Targeting System (Marine)": github_icon_base_url + "blizzard/btn-upgrade-terran-lazertargetingsystem.png",
+ "Magrail Munitions (Marine)": github_icon_base_url + "blizzard/btn-upgrade-terran-magrailmunitions.png",
+ "Optimized Logistics (Marine)": github_icon_base_url + "blizzard/btn-upgrade-terran-optimizedlogistics.png",
+ "Advanced Medic Facilities (Medic)": organics_icon_base_url + "AdvancedMedicFacilities.png",
+ "Stabilizer Medpacks (Medic)": github_icon_base_url + "blizzard/btn-upgrade-raynor-stabilizermedpacks.png",
+ "Restoration (Medic)": github_icon_base_url + "original/btn-ability-terran-restoration@scbw.png",
+ "Optical Flare (Medic)": github_icon_base_url + "blizzard/btn-upgrade-protoss-fenix-dragoonsolariteflare.png",
+ "Resource Efficiency (Medic)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Adaptive Medpacks (Medic)": github_icon_base_url + "blizzard/btn-ability-terran-heal-color.png",
+ "Nano Projector (Medic)": github_icon_base_url + "blizzard/talent-raynor-level03-firebatmedicrange.png",
+ "Incinerator Gauntlets (Firebat)": github_icon_base_url + "blizzard/btn-upgrade-raynor-incineratorgauntlets.png",
+ "Juggernaut Plating (Firebat)": github_icon_base_url + "blizzard/btn-upgrade-raynor-juggernautplating.png",
+ "Stimpack (Firebat)": github_icon_base_url + "blizzard/btn-ability-terran-stimpack-color.png",
+ "Super Stimpack (Firebat)": github_icon_base_url + "blizzard/btn-upgrade-terran-superstimppack.png",
+ "Resource Efficiency (Firebat)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Infernal Pre-Igniter (Firebat)": github_icon_base_url + "blizzard/btn-upgrade-terran-infernalpreigniter.png",
+ "Kinetic Foam (Firebat)": organics_icon_base_url + "KineticFoam.png",
+ "Nano Projectors (Firebat)": github_icon_base_url + "blizzard/talent-raynor-level03-firebatmedicrange.png",
+ "Concussive Shells (Marauder)": github_icon_base_url + "blizzard/btn-ability-terran-punishergrenade-color.png",
+ "Kinetic Foam (Marauder)": organics_icon_base_url + "KineticFoam.png",
+ "Stimpack (Marauder)": github_icon_base_url + "blizzard/btn-ability-terran-stimpack-color.png",
+ "Super Stimpack (Marauder)": github_icon_base_url + "blizzard/btn-upgrade-terran-superstimppack.png",
+ "Laser Targeting System (Marauder)": github_icon_base_url + "blizzard/btn-upgrade-terran-lazertargetingsystem.png",
+ "Magrail Munitions (Marauder)": github_icon_base_url + "blizzard/btn-upgrade-terran-magrailmunitions.png",
+ "Internal Tech Module (Marauder)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png",
+ "Juggernaut Plating (Marauder)": organics_icon_base_url + "JuggernautPlating.png",
+ "U-238 Rounds (Reaper)": organics_icon_base_url + "U-238Rounds.png",
+ "G-4 Clusterbomb (Reaper)": github_icon_base_url + "blizzard/btn-upgrade-terran-kd8chargeex3.png",
+ "Stimpack (Reaper)": github_icon_base_url + "blizzard/btn-ability-terran-stimpack-color.png",
+ "Super Stimpack (Reaper)": github_icon_base_url + "blizzard/btn-upgrade-terran-superstimppack.png",
+ "Laser Targeting System (Reaper)": github_icon_base_url + "blizzard/btn-upgrade-terran-lazertargetingsystem.png",
+ "Advanced Cloaking Field (Reaper)": github_icon_base_url + "original/btn-permacloak-reaper.png",
+ "Spider Mines (Reaper)": github_icon_base_url + "original/btn-ability-terran-spidermine.png",
+ "Combat Drugs (Reaper)": github_icon_base_url + "blizzard/btn-upgrade-terran-reapercombatdrugs.png",
+ "Jet Pack Overdrive (Reaper)": github_icon_base_url + "blizzard/btn-ability-hornerhan-reaper-flightmode.png",
+ "Ocular Implants (Ghost)": organics_icon_base_url + "OcularImplants.png",
+ "Crius Suit (Ghost)": github_icon_base_url + "original/btn-permacloak-ghost.png",
+ "EMP Rounds (Ghost)": github_icon_base_url + "blizzard/btn-ability-terran-emp-color.png",
+ "Lockdown (Ghost)": github_icon_base_url + "original/btn-abilty-terran-lockdown@scbw.png",
+ "Resource Efficiency (Ghost)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Psionic Lash (Spectre)": organics_icon_base_url + "PsionicLash.png",
+ "Nyx-Class Cloaking Module (Spectre)": github_icon_base_url + "original/btn-permacloak-spectre.png",
+ "Impaler Rounds (Spectre)": github_icon_base_url + "blizzard/btn-techupgrade-terran-impalerrounds.png",
+ "Resource Efficiency (Spectre)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Juggernaut Plating (HERC)": organics_icon_base_url + "JuggernautPlating.png",
+ "Kinetic Foam (HERC)": organics_icon_base_url + "KineticFoam.png",
+ "Resource Efficiency (HERC)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+
+ "Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg",
+ "Vulture": github_icon_base_url + "blizzard/btn-unit-terran-vulture.png",
+ "Goliath": github_icon_base_url + "blizzard/btn-unit-terran-goliath.png",
+ "Diamondback": github_icon_base_url + "blizzard/btn-unit-terran-cobra.png",
+ "Siege Tank": "https://static.wikia.nocookie.net/starcraft/images/5/57/SiegeTank_SC2_Icon1.jpg",
+ "Thor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/Thor_SC2_Icon1.jpg",
+ "Predator": github_icon_base_url + "original/btn-unit-terran-predator.png",
+ "Widow Mine": github_icon_base_url + "blizzard/btn-unit-terran-widowmine.png",
+ "Cyclone": github_icon_base_url + "blizzard/btn-unit-terran-cyclone.png",
+ "Warhound": github_icon_base_url + "blizzard/btn-unit-terran-warhound.png",
+
+ "Twin-Linked Flamethrower (Hellion)": github_icon_base_url + "blizzard/btn-upgrade-mengsk-trooper-flamethrower.png",
+ "Thermite Filaments (Hellion)": github_icon_base_url + "blizzard/btn-upgrade-terran-infernalpreigniter.png",
+ "Hellbat Aspect (Hellion)": github_icon_base_url + "blizzard/btn-unit-terran-hellionbattlemode.png",
+ "Smart Servos (Hellion)": github_icon_base_url + "blizzard/btn-upgrade-terran-transformationservos.png",
+ "Optimized Logistics (Hellion)": github_icon_base_url + "blizzard/btn-upgrade-terran-optimizedlogistics.png",
+ "Jump Jets (Hellion)": github_icon_base_url + "blizzard/btn-upgrade-terran-jumpjets.png",
+ "Stimpack (Hellion)": github_icon_base_url + "blizzard/btn-ability-terran-stimpack-color.png",
+ "Super Stimpack (Hellion)": github_icon_base_url + "blizzard/btn-upgrade-terran-superstimppack.png",
+ "Infernal Plating (Hellion)": github_icon_base_url + "blizzard/btn-upgrade-swann-hellarmor.png",
+ "Cerberus Mine (Spider Mine)": github_icon_base_url + "blizzard/btn-upgrade-raynor-cerberusmines.png",
+ "High Explosive Munition (Spider Mine)": github_icon_base_url + "original/btn-ability-terran-spidermine.png",
+ "Replenishable Magazine (Vulture)": github_icon_base_url + "blizzard/btn-upgrade-raynor-replenishablemagazine.png",
+ "Replenishable Magazine (Free) (Vulture)": github_icon_base_url + "blizzard/btn-upgrade-raynor-replenishablemagazine.png",
+ "Ion Thrusters (Vulture)": github_icon_base_url + "blizzard/btn-ability-terran-emergencythrusters.png",
+ "Auto Launchers (Vulture)": github_icon_base_url + "blizzard/btn-upgrade-terran-jotunboosters.png",
+ "Auto-Repair (Vulture)": github_icon_base_url + "blizzard/ui_tipicon_campaign_space01-repair.png",
+ "Multi-Lock Weapons System (Goliath)": github_icon_base_url + "blizzard/btn-upgrade-swann-multilockweaponsystem.png",
+ "Ares-Class Targeting System (Goliath)": github_icon_base_url + "blizzard/btn-upgrade-swann-aresclasstargetingsystem.png",
+ "Jump Jets (Goliath)": github_icon_base_url + "blizzard/btn-upgrade-terran-jumpjets.png",
+ "Optimized Logistics (Goliath)": github_icon_base_url + "blizzard/btn-upgrade-terran-optimizedlogistics.png",
+ "Shaped Hull (Goliath)": organics_icon_base_url + "ShapedHull.png",
+ "Resource Efficiency (Goliath)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Internal Tech Module (Goliath)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png",
+ "Tri-Lithium Power Cell (Diamondback)": github_icon_base_url + "original/btn-upgrade-terran-trilithium-power-cell.png",
+ "Tungsten Spikes (Diamondback)": github_icon_base_url + "original/btn-upgrade-terran-tungsten-spikes.png",
+ "Shaped Hull (Diamondback)": organics_icon_base_url + "ShapedHull.png",
+ "Hyperfluxor (Diamondback)": github_icon_base_url + "blizzard/btn-upgrade-mengsk-engineeringbay-orbitaldrop.png",
+ "Burst Capacitors (Diamondback)": github_icon_base_url + "blizzard/btn-ability-terran-electricfield.png",
+ "Ion Thrusters (Diamondback)": github_icon_base_url + "blizzard/btn-ability-terran-emergencythrusters.png",
+ "Resource Efficiency (Diamondback)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Maelstrom Rounds (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-raynor-maelstromrounds.png",
+ "Shaped Blast (Siege Tank)": organics_icon_base_url + "ShapedBlast.png",
+ "Jump Jets (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-terran-jumpjets.png",
+ "Spider Mines (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-siegetank-spidermines.png",
+ "Smart Servos (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-terran-transformationservos.png",
+ "Graduating Range (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-terran-nova-siegetankrange.png",
+ "Laser Targeting System (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-terran-lazertargetingsystem.png",
+ "Advanced Siege Tech (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-raynor-improvedsiegemode.png",
+ "Internal Tech Module (Siege Tank)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png",
+ "Shaped Hull (Siege Tank)": organics_icon_base_url + "ShapedHull.png",
+ "Resource Efficiency (Siege Tank)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "330mm Barrage Cannon (Thor)": github_icon_base_url + "original/btn-ability-thor-330mm.png",
+ "Immortality Protocol (Thor)": github_icon_base_url + "blizzard/btn-techupgrade-terran-immortalityprotocol.png",
+ "Immortality Protocol (Free) (Thor)": github_icon_base_url + "blizzard/btn-techupgrade-terran-immortalityprotocol.png",
+ "High Impact Payload (Thor)": github_icon_base_url + "blizzard/btn-unit-terran-thorsiegemode.png",
+ "Smart Servos (Thor)": github_icon_base_url + "blizzard/btn-upgrade-terran-transformationservos.png",
+ "Button With a Skull on It (Thor)": github_icon_base_url + "blizzard/btn-ability-terran-nuclearstrike-color.png",
+ "Laser Targeting System (Thor)": github_icon_base_url + "blizzard/btn-upgrade-terran-lazertargetingsystem.png",
+ "Large Scale Field Construction (Thor)": github_icon_base_url + "blizzard/talent-swann-level12-immortalityprotocol.png",
+ "Resource Efficiency (Predator)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Cloak (Predator)": github_icon_base_url + "blizzard/btn-ability-terran-cloak-color.png",
+ "Charge (Predator)": github_icon_base_url + "blizzard/btn-ability-protoss-charge-color.png",
+ "Predator's Fury (Predator)": github_icon_base_url + "blizzard/btn-ability-protoss-shadowfury.png",
+ "Drilling Claws (Widow Mine)": github_icon_base_url + "blizzard/btn-upgrade-terran-researchdrillingclaws.png",
+ "Concealment (Widow Mine)": github_icon_base_url + "blizzard/btn-ability-terran-widowminehidden.png",
+ "Black Market Launchers (Widow Mine)": github_icon_base_url + "blizzard/btn-ability-hornerhan-widowmine-attackrange.png",
+ "Executioner Missiles (Widow Mine)": github_icon_base_url + "blizzard/btn-ability-hornerhan-widowmine-deathblossom.png",
+ "Mag-Field Accelerators (Cyclone)": github_icon_base_url + "blizzard/btn-upgrade-terran-magfieldaccelerator.png",
+ "Mag-Field Launchers (Cyclone)": github_icon_base_url + "blizzard/btn-upgrade-terran-cyclonerangeupgrade.png",
+ "Targeting Optics (Cyclone)": github_icon_base_url + "blizzard/btn-upgrade-swann-targetingoptics.png",
+ "Rapid Fire Launchers (Cyclone)": github_icon_base_url + "blizzard/btn-upgrade-raynor-ripwavemissiles.png",
+ "Resource Efficiency (Cyclone)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Internal Tech Module (Cyclone)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png",
+ "Resource Efficiency (Warhound)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Reinforced Plating (Warhound)": github_icon_base_url + "original/btn-research-zerg-fortifiedbunker.png",
+
+ "Medivac": "https://static.wikia.nocookie.net/starcraft/images/d/db/Medivac_SC2_Icon1.jpg",
+ "Wraith": github_icon_base_url + "blizzard/btn-unit-terran-wraith.png",
+ "Viking": "https://static.wikia.nocookie.net/starcraft/images/2/2a/Viking_SC2_Icon1.jpg",
+ "Banshee": "https://static.wikia.nocookie.net/starcraft/images/3/32/Banshee_SC2_Icon1.jpg",
+ "Battlecruiser": "https://static.wikia.nocookie.net/starcraft/images/f/f5/Battlecruiser_SC2_Icon1.jpg",
+ "Raven": "https://static.wikia.nocookie.net/starcraft/images/1/19/SC2_Lab_Raven_Icon.png",
+ "Science Vessel": "https://static.wikia.nocookie.net/starcraft/images/c/c3/SC2_Lab_SciVes_Icon.png",
+ "Hercules": "https://static.wikia.nocookie.net/starcraft/images/4/40/SC2_Lab_Hercules_Icon.png",
+ "Liberator": github_icon_base_url + "blizzard/btn-unit-terran-liberator.png",
+ "Valkyrie": github_icon_base_url + "original/btn-unit-terran-valkyrie@scbw.png",
+
+ "Rapid Deployment Tube (Medivac)": organics_icon_base_url + "RapidDeploymentTube.png",
+ "Advanced Healing AI (Medivac)": github_icon_base_url + "blizzard/btn-ability-mengsk-medivac-doublehealbeam.png",
+ "Expanded Hull (Medivac)": github_icon_base_url + "blizzard/btn-upgrade-mengsk-engineeringbay-neosteelfortifiedarmor.png",
+ "Afterburners (Medivac)": github_icon_base_url + "blizzard/btn-upgrade-terran-medivacemergencythrusters.png",
+ "Scatter Veil (Medivac)": github_icon_base_url + "blizzard/btn-upgrade-swann-defensivematrix.png",
+ "Advanced Cloaking Field (Medivac)": github_icon_base_url + "original/btn-permacloak-medivac.png",
+ "Tomahawk Power Cells (Wraith)": organics_icon_base_url + "TomahawkPowerCells.png",
+ "Unregistered Cloaking Module (Wraith)": github_icon_base_url + "original/btn-permacloak-wraith.png",
+ "Trigger Override (Wraith)": github_icon_base_url + "blizzard/btn-ability-hornerhan-wraith-attackspeed.png",
+ "Internal Tech Module (Wraith)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png",
+ "Resource Efficiency (Wraith)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Displacement Field (Wraith)": github_icon_base_url + "blizzard/btn-upgrade-swann-displacementfield.png",
+ "Advanced Laser Technology (Wraith)": github_icon_base_url + "blizzard/btn-upgrade-swann-improvedburstlaser.png",
+ "Ripwave Missiles (Viking)": github_icon_base_url + "blizzard/btn-upgrade-raynor-ripwavemissiles.png",
+ "Phobos-Class Weapons System (Viking)": github_icon_base_url + "blizzard/btn-upgrade-raynor-phobosclassweaponssystem.png",
+ "Smart Servos (Viking)": github_icon_base_url + "blizzard/btn-upgrade-terran-transformationservos.png",
+ "Anti-Mechanical Munition (Viking)": github_icon_base_url + "blizzard/btn-ability-terran-ignorearmor.png",
+ "Shredder Rounds (Viking)": github_icon_base_url + "blizzard/btn-ability-hornerhan-viking-piercingattacks.png",
+ "W.I.L.D. Missiles (Viking)": github_icon_base_url + "blizzard/btn-ability-hornerhan-viking-missileupgrade.png",
+ "Cross-Spectrum Dampeners (Banshee)": github_icon_base_url + "original/btn-banshee-cross-spectrum-dampeners.png",
+ "Advanced Cross-Spectrum Dampeners (Banshee)": github_icon_base_url + "original/btn-permacloak-banshee.png",
+ "Shockwave Missile Battery (Banshee)": github_icon_base_url + "blizzard/btn-upgrade-raynor-shockwavemissilebattery.png",
+ "Hyperflight Rotors (Banshee)": github_icon_base_url + "blizzard/btn-upgrade-terran-hyperflightrotors.png",
+ "Laser Targeting System (Banshee)": github_icon_base_url + "blizzard/btn-upgrade-terran-lazertargetingsystem.png",
+ "Internal Tech Module (Banshee)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png",
+ "Shaped Hull (Banshee)": organics_icon_base_url + "ShapedHull.png",
+ "Advanced Targeting Optics (Banshee)": github_icon_base_url + "blizzard/btn-ability-terran-detectionconedebuff.png",
+ "Distortion Blasters (Banshee)": github_icon_base_url + "blizzard/btn-techupgrade-terran-cloakdistortionfield.png",
+ "Rocket Barrage (Banshee)": github_icon_base_url + "blizzard/btn-upgrade-terran-nova-bansheemissilestrik.png",
+ "Missile Pods (Battlecruiser) Level 1": organics_icon_base_url + "MissilePods.png",
+ "Missile Pods (Battlecruiser) Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-nova-bansheemissilestrik.png",
+ "Defensive Matrix (Battlecruiser)": github_icon_base_url + "blizzard/btn-upgrade-swann-defensivematrix.png",
+ "Advanced Defensive Matrix (Battlecruiser)": github_icon_base_url + "blizzard/btn-upgrade-swann-defensivematrix.png",
+ "Tactical Jump (Battlecruiser)": github_icon_base_url + "blizzard/btn-ability-terran-warpjump.png",
+ "Cloak (Battlecruiser)": github_icon_base_url + "blizzard/btn-ability-terran-cloak-color.png",
+ "ATX Laser Battery (Battlecruiser)": github_icon_base_url + "blizzard/btn-upgrade-terran-nova-specialordance.png",
+ "Optimized Logistics (Battlecruiser)": github_icon_base_url + "blizzard/btn-upgrade-terran-optimizedlogistics.png",
+ "Internal Tech Module (Battlecruiser)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png",
+ "Behemoth Plating (Battlecruiser)": github_icon_base_url + "original/btn-research-zerg-fortifiedbunker.png",
+ "Covert Ops Engines (Battlecruiser)": github_icon_base_url + "blizzard/btn-ability-terran-emergencythrusters.png",
+ "Bio Mechanical Repair Drone (Raven)": github_icon_base_url + "blizzard/btn-unit-biomechanicaldrone.png",
+ "Spider Mines (Raven)": github_icon_base_url + "blizzard/btn-upgrade-siegetank-spidermines.png",
+ "Railgun Turret (Raven)": github_icon_base_url + "blizzard/btn-unit-terran-autoturretblackops.png",
+ "Hunter-Seeker Weapon (Raven)": github_icon_base_url + "blizzard/btn-upgrade-terran-nova-specialordance.png",
+ "Interference Matrix (Raven)": github_icon_base_url + "blizzard/btn-upgrade-terran-interferencematrix.png",
+ "Anti-Armor Missile (Raven)": github_icon_base_url + "blizzard/btn-ability-terran-shreddermissile-color.png",
+ "Internal Tech Module (Raven)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png",
+ "Resource Efficiency (Raven)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Durable Materials (Raven)": github_icon_base_url + "blizzard/btn-upgrade-terran-durablematerials.png",
+ "EMP Shockwave (Science Vessel)": github_icon_base_url + "blizzard/btn-ability-mengsk-ghost-staticempblast.png",
+ "Defensive Matrix (Science Vessel)": github_icon_base_url + "blizzard/btn-upgrade-swann-defensivematrix.png",
+ "Improved Nano-Repair (Science Vessel)": github_icon_base_url + "blizzard/btn-upgrade-swann-improvednanorepair.png",
+ "Advanced AI Systems (Science Vessel)": github_icon_base_url + "blizzard/btn-ability-mengsk-medivac-doublehealbeam.png",
+ "Internal Fusion Module (Hercules)": github_icon_base_url + "blizzard/btn-upgrade-terran-internalizedtechmodule.png",
+ "Tactical Jump (Hercules)": github_icon_base_url + "blizzard/btn-ability-terran-hercules-tacticaljump.png",
+ "Advanced Ballistics (Liberator)": github_icon_base_url + "blizzard/btn-upgrade-terran-advanceballistics.png",
+ "Raid Artillery (Liberator)": github_icon_base_url + "blizzard/btn-upgrade-terran-nova-terrandefendermodestructureattack.png",
+ "Cloak (Liberator)": github_icon_base_url + "blizzard/btn-ability-terran-cloak-color.png",
+ "Laser Targeting System (Liberator)": github_icon_base_url + "blizzard/btn-upgrade-terran-lazertargetingsystem.png",
+ "Optimized Logistics (Liberator)": github_icon_base_url + "blizzard/btn-upgrade-terran-optimizedlogistics.png",
+ "Smart Servos (Liberator)": github_icon_base_url + "blizzard/btn-upgrade-terran-transformationservos.png",
+ "Resource Efficiency (Liberator)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Enhanced Cluster Launchers (Valkyrie)": github_icon_base_url + "blizzard/btn-ability-stetmann-corruptormissilebarrage.png",
+ "Shaped Hull (Valkyrie)": organics_icon_base_url + "ShapedHull.png",
+ "Flechette Missiles (Valkyrie)": github_icon_base_url + "blizzard/btn-ability-hornerhan-viking-missileupgrade.png",
+ "Afterburners (Valkyrie)": github_icon_base_url + "blizzard/btn-upgrade-terran-medivacemergencythrusters.png",
+ "Launching Vector Compensator (Valkyrie)": github_icon_base_url + "blizzard/btn-ability-terran-emergencythrusters.png",
+ "Resource Efficiency (Valkyrie)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+
+ "War Pigs": "https://static.wikia.nocookie.net/starcraft/images/e/ed/WarPigs_SC2_Icon1.jpg",
+ "Devil Dogs": "https://static.wikia.nocookie.net/starcraft/images/3/33/DevilDogs_SC2_Icon1.jpg",
+ "Hammer Securities": "https://static.wikia.nocookie.net/starcraft/images/3/3b/HammerSecurity_SC2_Icon1.jpg",
+ "Spartan Company": "https://static.wikia.nocookie.net/starcraft/images/b/be/SpartanCompany_SC2_Icon1.jpg",
+ "Siege Breakers": "https://static.wikia.nocookie.net/starcraft/images/3/31/SiegeBreakers_SC2_Icon1.jpg",
+ "Hel's Angels": "https://static.wikia.nocookie.net/starcraft/images/6/63/HelsAngels_SC2_Icon1.jpg",
+ "Dusk Wings": "https://static.wikia.nocookie.net/starcraft/images/5/52/DuskWings_SC2_Icon1.jpg",
+ "Jackson's Revenge": "https://static.wikia.nocookie.net/starcraft/images/9/95/JacksonsRevenge_SC2_Icon1.jpg",
+ "Skibi's Angels": github_icon_base_url + "blizzard/btn-unit-terran-medicelite.png",
+ "Death Heads": github_icon_base_url + "blizzard/btn-unit-terran-deathhead.png",
+ "Winged Nightmares": github_icon_base_url + "blizzard/btn-unit-collection-wraith-junker.png",
+ "Midnight Riders": github_icon_base_url + "blizzard/btn-unit-terran-liberatorblackops.png",
+ "Brynhilds": github_icon_base_url + "blizzard/btn-unit-collection-vikingfighter-covertops.png",
+ "Jotun": github_icon_base_url + "blizzard/btn-unit-terran-thormengsk.png",
+
+ "Ultra-Capacitors": "https://static.wikia.nocookie.net/starcraft/images/2/23/SC2_Lab_Ultra_Capacitors_Icon.png",
+ "Vanadium Plating": "https://static.wikia.nocookie.net/starcraft/images/6/67/SC2_Lab_VanPlating_Icon.png",
+ "Orbital Depots": "https://static.wikia.nocookie.net/starcraft/images/0/01/SC2_Lab_Orbital_Depot_Icon.png",
+ "Micro-Filtering": "https://static.wikia.nocookie.net/starcraft/images/2/20/SC2_Lab_MicroFilter_Icon.png",
+ "Automated Refinery": "https://static.wikia.nocookie.net/starcraft/images/7/71/SC2_Lab_Auto_Refinery_Icon.png",
+ "Command Center Reactor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/SC2_Lab_CC_Reactor_Icon.png",
+ "Tech Reactor": "https://static.wikia.nocookie.net/starcraft/images/c/c5/SC2_Lab_Tech_Reactor_Icon.png",
+ "Orbital Strike": "https://static.wikia.nocookie.net/starcraft/images/d/df/SC2_Lab_Orb_Strike_Icon.png",
+
+ "Shrike Turret (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png",
+ "Fortified Bunker (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png",
+ "Planetary Fortress": "https://static.wikia.nocookie.net/starcraft/images/0/0b/SC2_Lab_PlanetFortress_Icon.png",
+ "Perdition Turret": "https://static.wikia.nocookie.net/starcraft/images/a/af/SC2_Lab_PerdTurret_Icon.png",
+ "Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png",
+ "Regenerative Bio-Steel Level 1": github_icon_base_url + "original/btn-regenerativebiosteel-green.png",
+ "Regenerative Bio-Steel Level 2": github_icon_base_url + "original/btn-regenerativebiosteel-blue.png",
+ "Regenerative Bio-Steel Level 3": github_icon_base_url + "blizzard/btn-research-zerg-regenerativebio-steel.png",
+ "Hive Mind Emulator": "https://static.wikia.nocookie.net/starcraft/images/b/bc/SC2_Lab_Hive_Emulator_Icon.png",
+ "Psi Disrupter": "https://static.wikia.nocookie.net/starcraft/images/c/cf/SC2_Lab_Psi_Disruptor_Icon.png",
+
+ "Structure Armor": github_icon_base_url + "blizzard/btn-upgrade-terran-buildingarmor.png",
+ "Hi-Sec Auto Tracking": github_icon_base_url + "blizzard/btn-upgrade-terran-hisecautotracking.png",
+ "Advanced Optics": github_icon_base_url + "blizzard/btn-upgrade-swann-vehiclerangeincrease.png",
+ "Rogue Forces": github_icon_base_url + "blizzard/btn-unit-terran-tosh.png",
+
+ "Ghost Visor (Nova Equipment)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-ghostvisor.png",
+ "Rangefinder Oculus (Nova Equipment)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-rangefinderoculus.png",
+ "Domination (Nova Ability)": github_icon_base_url + "blizzard/btn-ability-nova-domination.png",
+ "Blink (Nova Ability)": github_icon_base_url + "blizzard/btn-upgrade-nova-blink.png",
+ "Stealth Suit Module (Nova Suit Module)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-stealthsuit.png",
+ "Cloak (Nova Suit Module)": github_icon_base_url + "blizzard/btn-ability-terran-cloak-color.png",
+ "Permanently Cloaked (Nova Suit Module)": github_icon_base_url + "blizzard/btn-upgrade-nova-tacticalstealthsuit.png",
+ "Energy Suit Module (Nova Suit Module)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-apolloinfantrysuit.png",
+ "Armored Suit Module (Nova Suit Module)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-blinksuit.png",
+ "Jump Suit Module (Nova Suit Module)": github_icon_base_url + "blizzard/btn-upgrade-nova-jetpack.png",
+ "C20A Canister Rifle (Nova Weapon)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-canisterrifle.png",
+ "Hellfire Shotgun (Nova Weapon)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-shotgun.png",
+ "Plasma Rifle (Nova Weapon)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-plasmagun.png",
+ "Monomolecular Blade (Nova Weapon)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-monomolecularblade.png",
+ "Blazefire Gunblade (Nova Weapon)": github_icon_base_url + "blizzard/btn-upgrade-nova-equipment-gunblade_sword.png",
+ "Stim Infusion (Nova Gadget)": github_icon_base_url + "blizzard/btn-upgrade-terran-superstimppack.png",
+ "Pulse Grenades (Nova Gadget)": github_icon_base_url + "blizzard/btn-upgrade-nova-btn-upgrade-nova-pulsegrenade.png",
+ "Flashbang Grenades (Nova Gadget)": github_icon_base_url + "blizzard/btn-upgrade-nova-btn-upgrade-nova-flashgrenade.png",
+ "Ionic Force Field (Nova Gadget)": github_icon_base_url + "blizzard/btn-upgrade-terran-nova-personaldefensivematrix.png",
+ "Holo Decoy (Nova Gadget)": github_icon_base_url + "blizzard/btn-upgrade-nova-holographicdecoy.png",
+ "Tac Nuke Strike (Nova Ability)": github_icon_base_url + "blizzard/btn-ability-terran-nuclearstrike-color.png",
+
+ "Zerg Melee Attack Level 1": github_icon_base_url + "blizzard/btn-upgrade-zerg-meleeattacks-level1.png",
+ "Zerg Melee Attack Level 2": github_icon_base_url + "blizzard/btn-upgrade-zerg-meleeattacks-level2.png",
+ "Zerg Melee Attack Level 3": github_icon_base_url + "blizzard/btn-upgrade-zerg-meleeattacks-level3.png",
+ "Zerg Missile Attack Level 1": github_icon_base_url + "blizzard/btn-upgrade-zerg-missileattacks-level1.png",
+ "Zerg Missile Attack Level 2": github_icon_base_url + "blizzard/btn-upgrade-zerg-missileattacks-level2.png",
+ "Zerg Missile Attack Level 3": github_icon_base_url + "blizzard/btn-upgrade-zerg-missileattacks-level3.png",
+ "Zerg Ground Carapace Level 1": github_icon_base_url + "blizzard/btn-upgrade-zerg-groundcarapace-level1.png",
+ "Zerg Ground Carapace Level 2": github_icon_base_url + "blizzard/btn-upgrade-zerg-groundcarapace-level2.png",
+ "Zerg Ground Carapace Level 3": github_icon_base_url + "blizzard/btn-upgrade-zerg-groundcarapace-level3.png",
+ "Zerg Flyer Attack Level 1": github_icon_base_url + "blizzard/btn-upgrade-zerg-airattacks-level1.png",
+ "Zerg Flyer Attack Level 2": github_icon_base_url + "blizzard/btn-upgrade-zerg-airattacks-level2.png",
+ "Zerg Flyer Attack Level 3": github_icon_base_url + "blizzard/btn-upgrade-zerg-airattacks-level3.png",
+ "Zerg Flyer Carapace Level 1": github_icon_base_url + "blizzard/btn-upgrade-zerg-flyercarapace-level1.png",
+ "Zerg Flyer Carapace Level 2": github_icon_base_url + "blizzard/btn-upgrade-zerg-flyercarapace-level2.png",
+ "Zerg Flyer Carapace Level 3": github_icon_base_url + "blizzard/btn-upgrade-zerg-flyercarapace-level3.png",
+
+ "Automated Extractors (Kerrigan Tier 3)": github_icon_base_url + "blizzard/btn-ability-kerrigan-automatedextractors.png",
+ "Vespene Efficiency (Kerrigan Tier 5)": github_icon_base_url + "blizzard/btn-ability-kerrigan-vespeneefficiency.png",
+ "Twin Drones (Kerrigan Tier 5)": github_icon_base_url + "blizzard/btn-ability-kerrigan-twindrones.png",
+ "Improved Overlords (Kerrigan Tier 3)": github_icon_base_url + "blizzard/btn-ability-kerrigan-improvedoverlords.png",
+ "Ventral Sacs (Overlord)": github_icon_base_url + "blizzard/btn-upgrade-zerg-ventralsacs.png",
+ "Malignant Creep (Kerrigan Tier 5)": github_icon_base_url + "blizzard/btn-ability-kerrigan-malignantcreep.png",
+
+ "Spine Crawler": github_icon_base_url + "blizzard/btn-building-zerg-spinecrawler.png",
+ "Spore Crawler": github_icon_base_url + "blizzard/btn-building-zerg-sporecrawler.png",
+
+ "Zergling": github_icon_base_url + "blizzard/btn-unit-zerg-zergling.png",
+ "Swarm Queen": github_icon_base_url + "blizzard/btn-unit-zerg-broodqueen.png",
+ "Roach": github_icon_base_url + "blizzard/btn-unit-zerg-roach.png",
+ "Hydralisk": github_icon_base_url + "blizzard/btn-unit-zerg-hydralisk.png",
+ "Aberration": github_icon_base_url + "blizzard/btn-unit-zerg-aberration.png",
+ "Mutalisk": github_icon_base_url + "blizzard/btn-unit-zerg-mutalisk.png",
+ "Corruptor": github_icon_base_url + "blizzard/btn-unit-zerg-corruptor.png",
+ "Swarm Host": github_icon_base_url + "blizzard/btn-unit-zerg-swarmhost.png",
+ "Infestor": github_icon_base_url + "blizzard/btn-unit-zerg-infestor.png",
+ "Defiler": github_icon_base_url + "original/btn-unit-zerg-defiler@scbw.png",
+ "Ultralisk": github_icon_base_url + "blizzard/btn-unit-zerg-ultralisk.png",
+ "Brood Queen": github_icon_base_url + "blizzard/btn-unit-zerg-classicqueen.png",
+ "Scourge": github_icon_base_url + "blizzard/btn-unit-zerg-scourge.png",
+
+ "Baneling Aspect (Zergling)": github_icon_base_url + "blizzard/btn-unit-zerg-baneling.png",
+ "Ravager Aspect (Roach)": github_icon_base_url + "blizzard/btn-unit-zerg-ravager.png",
+ "Impaler Aspect (Hydralisk)": github_icon_base_url + "blizzard/btn-unit-zerg-impaler.png",
+ "Lurker Aspect (Hydralisk)": github_icon_base_url + "blizzard/btn-unit-zerg-lurker.png",
+ "Brood Lord Aspect (Mutalisk/Corruptor)": github_icon_base_url + "blizzard/btn-unit-zerg-broodlord.png",
+ "Viper Aspect (Mutalisk/Corruptor)": github_icon_base_url + "blizzard/btn-unit-zerg-viper.png",
+ "Guardian Aspect (Mutalisk/Corruptor)": github_icon_base_url + "blizzard/btn-unit-zerg-primalguardian.png",
+ "Devourer Aspect (Mutalisk/Corruptor)": github_icon_base_url + "blizzard/btn-unit-zerg-devourerex3.png",
+
+ "Raptor Strain (Zergling)": github_icon_base_url + "blizzard/btn-unit-zerg-zergling-raptor.png",
+ "Swarmling Strain (Zergling)": github_icon_base_url + "blizzard/btn-unit-zerg-zergling-swarmling.png",
+ "Hardened Carapace (Zergling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-hardenedcarapace.png",
+ "Adrenal Overload (Zergling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-adrenaloverload.png",
+ "Metabolic Boost (Zergling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-hotsmetabolicboost.png",
+ "Shredding Claws (Zergling)": github_icon_base_url + "blizzard/btn-upgrade-zergling-armorshredding.png",
+ "Zergling Reconstitution (Kerrigan Tier 3)": github_icon_base_url + "blizzard/btn-ability-kerrigan-zerglingreconstitution.png",
+ "Splitter Strain (Baneling)": github_icon_base_url + "blizzard/talent-zagara-level14-unlocksplitterling.png",
+ "Hunter Strain (Baneling)": github_icon_base_url + "blizzard/btn-ability-zerg-cliffjump-baneling.png",
+ "Corrosive Acid (Baneling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-corrosiveacid.png",
+ "Rupture (Baneling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-rupture.png",
+ "Regenerative Acid (Baneling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-regenerativebile.png",
+ "Centrifugal Hooks (Baneling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-centrifugalhooks.png",
+ "Tunneling Jaws (Baneling)": github_icon_base_url + "blizzard/btn-upgrade-zerg-tunnelingjaws.png",
+ "Rapid Metamorph (Baneling)": github_icon_base_url + "blizzard/btn-upgrade-terran-optimizedlogistics.png",
+ "Spawn Larvae (Swarm Queen)": github_icon_base_url + "blizzard/btn-unit-zerg-larva.png",
+ "Deep Tunnel (Swarm Queen)": github_icon_base_url + "blizzard/btn-ability-zerg-deeptunnel.png",
+ "Organic Carapace (Swarm Queen)": github_icon_base_url + "blizzard/btn-upgrade-zerg-organiccarapace.png",
+ "Bio-Mechanical Transfusion (Swarm Queen)": github_icon_base_url + "blizzard/btn-upgrade-zerg-abathur-biomechanicaltransfusion.png",
+ "Resource Efficiency (Swarm Queen)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Incubator Chamber (Swarm Queen)": github_icon_base_url + "blizzard/btn-upgrade-zerg-abathur-incubationchamber.png",
+ "Vile Strain (Roach)": github_icon_base_url + "blizzard/btn-unit-zerg-roach-vile.png",
+ "Corpser Strain (Roach)": github_icon_base_url + "blizzard/btn-unit-zerg-roach-corpser.png",
+ "Hydriodic Bile (Roach)": github_icon_base_url + "blizzard/btn-upgrade-zerg-hydriaticacid.png",
+ "Adaptive Plating (Roach)": github_icon_base_url + "blizzard/btn-upgrade-zerg-adaptivecarapace.png",
+ "Tunneling Claws (Roach)": github_icon_base_url + "blizzard/btn-upgrade-zerg-hotstunnelingclaws.png",
+ "Glial Reconstitution (Roach)": github_icon_base_url + "blizzard/btn-upgrade-zerg-glialreconstitution.png",
+ "Organic Carapace (Roach)": github_icon_base_url + "blizzard/btn-upgrade-zerg-organiccarapace.png",
+ "Potent Bile (Ravager)": github_icon_base_url + "blizzard/potentbile_coop.png",
+ "Bloated Bile Ducts (Ravager)": github_icon_base_url + "blizzard/btn-ability-zerg-abathur-corrosivebilelarge.png",
+ "Deep Tunnel (Ravager)": github_icon_base_url + "blizzard/btn-ability-zerg-deeptunnel.png",
+ "Frenzy (Hydralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-frenzy.png",
+ "Ancillary Carapace (Hydralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-ancillaryarmor.png",
+ "Grooved Spines (Hydralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-hotsgroovedspines.png",
+ "Muscular Augments (Hydralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-evolvemuscularaugments.png",
+ "Resource Efficiency (Hydralisk)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Adaptive Talons (Impaler)": github_icon_base_url + "blizzard/btn-upgrade-zerg-adaptivetalons.png",
+ "Secretion Glands (Impaler)": github_icon_base_url + "blizzard/btn-ability-zerg-creepspread.png",
+ "Hardened Tentacle Spines (Impaler)": github_icon_base_url + "blizzard/btn-ability-zerg-dehaka-impaler-tenderize.png",
+ "Seismic Spines (Lurker)": github_icon_base_url + "blizzard/btn-upgrade-kerrigan-seismicspines.png",
+ "Adapted Spines (Lurker)": github_icon_base_url + "blizzard/btn-upgrade-zerg-groovedspines.png",
+ "Vicious Glaive (Mutalisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-viciousglaive.png",
+ "Rapid Regeneration (Mutalisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-rapidregeneration.png",
+ "Sundering Glaive (Mutalisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-explosiveglaive.png",
+ "Severing Glaive (Mutalisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-explosiveglaive.png",
+ "Aerodynamic Glaive Shape (Mutalisk)": github_icon_base_url + "blizzard/btn-ability-dehaka-airbonusdamage.png",
+ "Corruption (Corruptor)": github_icon_base_url + "blizzard/btn-ability-zerg-causticspray.png",
+ "Caustic Spray (Corruptor)": github_icon_base_url + "blizzard/btn-ability-zerg-corruption-color.png",
+ "Porous Cartilage (Brood Lord)": github_icon_base_url + "blizzard/btn-upgrade-kerrigan-broodlordspeed.png",
+ "Evolved Carapace (Brood Lord)": github_icon_base_url + "blizzard/btn-upgrade-zerg-chitinousplating.png",
+ "Splitter Mitosis (Brood Lord)": github_icon_base_url + "blizzard/abilityicon_spawnbroodlings_square.png",
+ "Resource Efficiency (Brood Lord)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Parasitic Bomb (Viper)": github_icon_base_url + "blizzard/btn-ability-zerg-parasiticbomb.png",
+ "Paralytic Barbs (Viper)": github_icon_base_url + "blizzard/btn-upgrade-zerg-abathur-abduct.png",
+ "Virulent Microbes (Viper)": github_icon_base_url + "blizzard/btn-upgrade-zerg-abathur-castrange.png",
+ "Prolonged Dispersion (Guardian)": github_icon_base_url + "blizzard/btn-upgrade-zerg-abathur-prolongeddispersion.png",
+ "Primal Adaptation (Guardian)": github_icon_base_url + "blizzard/biomassrecovery_coop.png",
+ "Soronan Acid (Guardian)": github_icon_base_url + "blizzard/btn-upgrade-zerg-abathur-biomass.png",
+ "Corrosive Spray (Devourer)": github_icon_base_url + "blizzard/btn-upgrade-zerg-abathur-devourer-corrosivespray.png",
+ "Gaping Maw (Devourer)": github_icon_base_url + "blizzard/btn-ability-zerg-explode-color.png",
+ "Improved Osmosis (Devourer)": github_icon_base_url + "blizzard/btn-upgrade-zerg-pneumatizedcarapace.png",
+ "Prescient Spores (Devourer)": github_icon_base_url + "blizzard/btn-upgrade-zerg-airattacks-level2.png",
+ "Carrion Strain (Swarm Host)": github_icon_base_url + "blizzard/btn-unit-zerg-swarmhost-carrion.png",
+ "Creeper Strain (Swarm Host)": github_icon_base_url + "blizzard/btn-unit-zerg-swarmhost-creeper.png",
+ "Burrow (Swarm Host)": github_icon_base_url + "blizzard/btn-ability-zerg-burrow-color.png",
+ "Rapid Incubation (Swarm Host)": github_icon_base_url + "blizzard/btn-upgrade-zerg-rapidincubation.png",
+ "Pressurized Glands (Swarm Host)": github_icon_base_url + "blizzard/btn-upgrade-zerg-pressurizedglands.png",
+ "Locust Metabolic Boost (Swarm Host)": github_icon_base_url + "blizzard/btn-upgrade-zerg-glialreconstitution.png",
+ "Enduring Locusts (Swarm Host)": github_icon_base_url + "blizzard/btn-upgrade-zerg-evolveincreasedlocustlifetime.png",
+ "Organic Carapace (Swarm Host)": github_icon_base_url + "blizzard/btn-upgrade-zerg-organiccarapace.png",
+ "Resource Efficiency (Swarm Host)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Infested Terran (Infestor)": github_icon_base_url + "blizzard/btn-unit-zerg-infestedmarine.png",
+ "Microbial Shroud (Infestor)": github_icon_base_url + "blizzard/btn-ability-zerg-darkswarm.png",
+ "Noxious Strain (Ultralisk)": github_icon_base_url + "blizzard/btn-unit-zerg-ultralisk-noxious.png",
+ "Torrasque Strain (Ultralisk)": github_icon_base_url + "blizzard/btn-unit-zerg-ultralisk-torrasque.png",
+ "Burrow Charge (Ultralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-burrowcharge.png",
+ "Tissue Assimilation (Ultralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-tissueassimilation.png",
+ "Monarch Blades (Ultralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-monarchblades.png",
+ "Anabolic Synthesis (Ultralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-anabolicsynthesis.png",
+ "Chitinous Plating (Ultralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-chitinousplating.png",
+ "Organic Carapace (Ultralisk)": github_icon_base_url + "blizzard/btn-upgrade-zerg-organiccarapace.png",
+ "Resource Efficiency (Ultralisk)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Fungal Growth (Brood Queen)": github_icon_base_url + "blizzard/btn-upgrade-zerg-stukov-researchqueenfungalgrowth.png",
+ "Ensnare (Brood Queen)": github_icon_base_url + "blizzard/btn-ability-zerg-fungalgrowth-color.png",
+ "Enhanced Mitochondria (Brood Queen)": github_icon_base_url + "blizzard/btn-upgrade-zerg-stukov-queenenergyregen.png",
+ "Virulent Spores (Scourge)": github_icon_base_url + "blizzard/btn-upgrade-zagara-scourgesplashdamage.png",
+ "Resource Efficiency (Scourge)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Swarm Scourge (Scourge)": github_icon_base_url + "original/btn-upgrade-custom-triple-scourge.png",
+
+ "Infested Medics": github_icon_base_url + "blizzard/btn-unit-terran-medicelite.png",
+ "Infested Siege Tanks": github_icon_base_url + "original/btn-unit-terran-siegetankmercenary-tank.png",
+ "Infested Banshees": github_icon_base_url + "original/btn-unit-terran-bansheemercenary.png",
+
+ "Primal Form (Kerrigan)": github_icon_base_url + "blizzard/btn-unit-zerg-kerriganinfested.png",
+ "Kinetic Blast (Kerrigan Tier 1)": github_icon_base_url + "blizzard/btn-ability-kerrigan-kineticblast.png",
+ "Heroic Fortitude (Kerrigan Tier 1)": github_icon_base_url + "blizzard/btn-ability-kerrigan-heroicfortitude.png",
+ "Leaping Strike (Kerrigan Tier 1)": github_icon_base_url + "blizzard/btn-ability-kerrigan-leapingstrike.png",
+ "Crushing Grip (Kerrigan Tier 2)": github_icon_base_url + "blizzard/btn-ability-swarm-kerrigan-crushinggrip.png",
+ "Chain Reaction (Kerrigan Tier 2)": github_icon_base_url + "blizzard/btn-ability-swarm-kerrigan-chainreaction.png",
+ "Psionic Shift (Kerrigan Tier 2)": github_icon_base_url + "blizzard/btn-ability-kerrigan-psychicshift.png",
+ "Wild Mutation (Kerrigan Tier 4)": github_icon_base_url + "blizzard/btn-ability-kerrigan-wildmutation.png",
+ "Spawn Banelings (Kerrigan Tier 4)": github_icon_base_url + "blizzard/abilityicon_spawnbanelings_square.png",
+ "Mend (Kerrigan Tier 4)": github_icon_base_url + "blizzard/btn-ability-zerg-transfusion-color.png",
+ "Infest Broodlings (Kerrigan Tier 6)": github_icon_base_url + "blizzard/abilityicon_spawnbroodlings_square.png",
+ "Fury (Kerrigan Tier 6)": github_icon_base_url + "blizzard/btn-ability-kerrigan-fury.png",
+ "Ability Efficiency (Kerrigan Tier 6)": github_icon_base_url + "blizzard/btn-ability-kerrigan-abilityefficiency.png",
+ "Apocalypse (Kerrigan Tier 7)": github_icon_base_url + "blizzard/btn-ability-kerrigan-apocalypse.png",
+ "Spawn Leviathan (Kerrigan Tier 7)": github_icon_base_url + "blizzard/btn-unit-zerg-leviathan.png",
+ "Drop-Pods (Kerrigan Tier 7)": github_icon_base_url + "blizzard/btn-ability-kerrigan-droppods.png",
+
+ "Protoss Ground Weapon Level 1": github_icon_base_url + "blizzard/btn-upgrade-protoss-groundweaponslevel1.png",
+ "Protoss Ground Weapon Level 2": github_icon_base_url + "blizzard/btn-upgrade-protoss-groundweaponslevel2.png",
+ "Protoss Ground Weapon Level 3": github_icon_base_url + "blizzard/btn-upgrade-protoss-groundweaponslevel3.png",
+ "Protoss Ground Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-protoss-groundarmorlevel1.png",
+ "Protoss Ground Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-protoss-groundarmorlevel2.png",
+ "Protoss Ground Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-protoss-groundarmorlevel3.png",
+ "Protoss Shields Level 1": github_icon_base_url + "blizzard/btn-upgrade-protoss-shieldslevel1.png",
+ "Protoss Shields Level 2": github_icon_base_url + "blizzard/btn-upgrade-protoss-shieldslevel2.png",
+ "Protoss Shields Level 3": github_icon_base_url + "blizzard/btn-upgrade-protoss-shieldslevel3.png",
+ "Protoss Air Weapon Level 1": github_icon_base_url + "blizzard/btn-upgrade-protoss-airweaponslevel1.png",
+ "Protoss Air Weapon Level 2": github_icon_base_url + "blizzard/btn-upgrade-protoss-airweaponslevel2.png",
+ "Protoss Air Weapon Level 3": github_icon_base_url + "blizzard/btn-upgrade-protoss-airweaponslevel3.png",
+ "Protoss Air Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-protoss-airarmorlevel1.png",
+ "Protoss Air Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-protoss-airarmorlevel2.png",
+ "Protoss Air Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-protoss-airarmorlevel3.png",
+
+ "Quatro": github_icon_base_url + "blizzard/btn-progression-protoss-fenix-6-forgeresearch.png",
+
+ "Photon Cannon": github_icon_base_url + "blizzard/btn-building-protoss-photoncannon.png",
+ "Khaydarin Monolith": github_icon_base_url + "blizzard/btn-unit-protoss-khaydarinmonolith.png",
+ "Shield Battery": github_icon_base_url + "blizzard/btn-building-protoss-shieldbattery.png",
+
+ "Enhanced Targeting": github_icon_base_url + "blizzard/btn-upgrade-karax-turretrange.png",
+ "Optimized Ordnance": github_icon_base_url + "blizzard/btn-upgrade-karax-turretattackspeed.png",
+ "Khalai Ingenuity": github_icon_base_url + "blizzard/btn-upgrade-karax-pylonwarpininstantly.png",
+ "Orbital Assimilators": github_icon_base_url + "blizzard/btn-ability-spearofadun-orbitalassimilator.png",
+ "Amplified Assimilators": github_icon_base_url + "original/btn-research-terran-microfiltering.png",
+ "Warp Harmonization": github_icon_base_url + "blizzard/btn-ability-spearofadun-warpharmonization.png",
+ "Superior Warp Gates": github_icon_base_url + "blizzard/talent-artanis-level03-warpgatecharges.png",
+ "Nexus Overcharge": github_icon_base_url + "blizzard/btn-ability-spearofadun-nexusovercharge.png",
+
+ "Zealot": github_icon_base_url + "blizzard/btn-unit-protoss-zealot-aiur.png",
+ "Centurion": github_icon_base_url + "blizzard/btn-unit-protoss-zealot-nerazim.png",
+ "Sentinel": github_icon_base_url + "blizzard/btn-unit-protoss-zealot-purifier.png",
+ "Supplicant": github_icon_base_url + "blizzard/btn-unit-protoss-alarak-taldarim-supplicant.png",
+ "Sentry": github_icon_base_url + "blizzard/btn-unit-protoss-sentry.png",
+ "Energizer": github_icon_base_url + "blizzard/btn-unit-protoss-sentry-purifier.png",
+ "Havoc": github_icon_base_url + "blizzard/btn-unit-protoss-sentry-taldarim.png",
+ "Stalker": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Icon_Protoss_Stalker.jpg",
+ "Instigator": github_icon_base_url + "blizzard/btn-unit-protoss-stalker-purifier.png",
+ "Slayer": github_icon_base_url + "blizzard/btn-unit-protoss-alarak-taldarim-stalker.png",
+ "Dragoon": github_icon_base_url + "blizzard/btn-unit-protoss-dragoon-void.png",
+ "Adept": github_icon_base_url + "blizzard/btn-unit-protoss-adept-purifier.png",
+ "High Templar": "https://static.wikia.nocookie.net/starcraft/images/a/a0/Icon_Protoss_High_Templar.jpg",
+ "Signifier": github_icon_base_url + "original/btn-unit-protoss-hightemplar-nerazim.png",
+ "Ascendant": github_icon_base_url + "blizzard/btn-unit-protoss-hightemplar-taldarim.png",
+ "Dark Archon": github_icon_base_url + "blizzard/talent-vorazun-level05-unlockdarkarchon.png",
+ "Dark Templar": "https://static.wikia.nocookie.net/starcraft/images/9/90/Icon_Protoss_Dark_Templar.jpg",
+ "Avenger": github_icon_base_url + "blizzard/btn-unit-protoss-darktemplar-aiur.png",
+ "Blood Hunter": github_icon_base_url + "blizzard/btn-unit-protoss-darktemplar-taldarim.png",
+
+ "Leg Enhancements (Zealot/Sentinel/Centurion)": github_icon_base_url + "blizzard/btn-ability-protoss-charge-color.png",
+ "Shield Capacity (Zealot/Sentinel/Centurion)": github_icon_base_url + "blizzard/btn-upgrade-protoss-shieldslevel1.png",
+ "Blood Shield (Supplicant)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-supplicantarmor.png",
+ "Soul Augmentation (Supplicant)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-supplicantextrashields.png",
+ "Shield Regeneration (Supplicant)": github_icon_base_url + "blizzard/btn-ability-protoss-voidarmor.png",
+ "Force Field (Sentry)": github_icon_base_url + "blizzard/btn-ability-protoss-forcefield-color.png",
+ "Hallucination (Sentry)": github_icon_base_url + "blizzard/btn-ability-protoss-hallucination-color.png",
+ "Reclamation (Energizer)": github_icon_base_url + "blizzard/btn-ability-protoss-reclamation.png",
+ "Forged Chassis (Energizer)": github_icon_base_url + "blizzard/btn-upgrade-protoss-groundarmorlevel0.png",
+ "Detect Weakness (Havoc)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-havoctargetlockbuffed.png",
+ "Bloodshard Resonance (Havoc)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-rangeincrease.png",
+ "Cloaking Module (Sentry/Energizer/Havoc)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-permanentcloak.png",
+ "Rapid Recharging (Sentry/Energizer/Havoc/Shield Battery)": github_icon_base_url + "blizzard/btn-upgrade-karax-energyregen200.png",
+ "Disintegrating Particles (Stalker/Instigator/Slayer)": github_icon_base_url + "blizzard/btn-ability-protoss-phasedisruptor.png",
+ "Particle Reflection (Stalker/Instigator/Slayer)": github_icon_base_url + "blizzard/btn-upgrade-protoss-fenix-adeptchampionbounceattack.png",
+ "High Impact Phase Disruptor (Dragoon)": github_icon_base_url + "blizzard/btn-ability-protoss-phasedisruptor.png",
+ "Trillic Compression System (Dragoon)": github_icon_base_url + "blizzard/btn-ability-protoss-dragoonchassis.png",
+ "Singularity Charge (Dragoon)": github_icon_base_url + "blizzard/btn-upgrade-artanis-singularitycharge.png",
+ "Enhanced Strider Servos (Dragoon)": github_icon_base_url + "blizzard/btn-upgrade-terran-transformationservos.png",
+ "Shockwave (Adept)": github_icon_base_url + "blizzard/btn-upgrade-protoss-fenix-adept-recochetglaiveupgraded.png",
+ "Resonating Glaives (Adept)": github_icon_base_url + "blizzard/btn-upgrade-protoss-resonatingglaives.png",
+ "Phase Bulwark (Adept)": github_icon_base_url + "blizzard/btn-upgrade-protoss-adeptshieldupgrade.png",
+ "Unshackled Psionic Storm (High Templar/Signifier)": github_icon_base_url + "blizzard/btn-ability-protoss-psistorm.png",
+ "Hallucination (High Templar/Signifier)": github_icon_base_url + "blizzard/btn-ability-protoss-hallucination-color.png",
+ "Khaydarin Amulet (High Templar/Signifier)": github_icon_base_url + "blizzard/btn-upgrade-protoss-khaydarinamulet.png",
+ "High Archon (Archon)": github_icon_base_url + "blizzard/btn-upgrade-artanis-healingpsionicstorm.png",
+ "Power Overwhelming (Ascendant)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-ascendantspermanentlybetter.png",
+ "Chaotic Attunement (Ascendant)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-ascendant'spsiorbtravelsfurther.png",
+ "Blood Amulet (Ascendant)": github_icon_base_url + "blizzard/btn-upgrade-protoss-wrathwalker-chargetimeimproved.png",
+ "Feedback (Dark Archon)": github_icon_base_url + "blizzard/btn-ability-protoss-feedback-color.png",
+ "Maelstrom (Dark Archon)": github_icon_base_url + "blizzard/btn-ability-protoss-voidstasis.png",
+ "Argus Talisman (Dark Archon)": github_icon_base_url + "original/btn-upgrade-protoss-argustalisman@scbw.png",
+ "Dark Archon Meld (Dark Templar)": github_icon_base_url + "blizzard/talent-vorazun-level05-unlockdarkarchon.png",
+ "Shroud of Adun (Dark Templar/Avenger/Blood Hunter)": github_icon_base_url + "blizzard/talent-vorazun-level01-shadowstalk.png",
+ "Shadow Guard Training (Dark Templar/Avenger/Blood Hunter)": github_icon_base_url + "blizzard/btn-ability-terran-heal-color.png",
+ "Blink (Dark Templar/Avenger/Blood Hunter)": github_icon_base_url + "blizzard/btn-ability-protoss-shadowdash.png",
+ "Resource Efficiency (Dark Templar/Avenger/Blood Hunter)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+
+ "Warp Prism": github_icon_base_url + "blizzard/btn-unit-protoss-warpprism.png",
+ "Immortal": "https://static.wikia.nocookie.net/starcraft/images/c/c1/Icon_Protoss_Immortal.jpg",
+ "Annihilator": github_icon_base_url + "blizzard/btn-unit-protoss-immortal-nerazim.png",
+ "Vanguard": github_icon_base_url + "blizzard/btn-unit-protoss-immortal-taldarim.png",
+ "Colossus": github_icon_base_url + "blizzard/btn-unit-protoss-colossus-purifier.png",
+ "Wrathwalker": github_icon_base_url + "blizzard/btn-unit-protoss-colossus-taldarim.png",
+ "Observer": github_icon_base_url + "blizzard/btn-unit-protoss-observer.png",
+ "Reaver": github_icon_base_url + "blizzard/btn-unit-protoss-reaver.png",
+ "Disruptor": github_icon_base_url + "blizzard/btn-unit-protoss-disruptor.png",
+
+ "Gravitic Drive (Warp Prism)": github_icon_base_url + "blizzard/btn-upgrade-protoss-graviticdrive.png",
+ "Phase Blaster (Warp Prism)": github_icon_base_url + "blizzard/btn-upgrade-protoss-airweaponslevel0.png",
+ "War Configuration (Warp Prism)": github_icon_base_url + "blizzard/btn-upgrade-protoss-alarak-graviticdrive.png",
+ "Singularity Charge (Immortal/Annihilator)": github_icon_base_url + "blizzard/btn-upgrade-artanis-singularitycharge.png",
+ "Advanced Targeting Mechanics (Immortal/Annihilator)": github_icon_base_url + "blizzard/btn-ability-terran-detectionconedebuff.png",
+ "Agony Launchers (Vanguard)": github_icon_base_url + "blizzard/btn-upgrade-protoss-vanguard-aoeradiusincreased.png",
+ "Matter Dispersion (Vanguard)": github_icon_base_url + "blizzard/btn-ability-terran-detectionconedebuff.png",
+ "Pacification Protocol (Colossus)": github_icon_base_url + "blizzard/btn-ability-protoss-chargedblast.png",
+ "Rapid Power Cycling (Wrathwalker)": github_icon_base_url + "blizzard/btn-upgrade-protoss-wrathwalker-chargetimeimproved.png",
+ "Eye of Wrath (Wrathwalker)": github_icon_base_url + "blizzard/btn-upgrade-protoss-extendedthermallance.png",
+ "Gravitic Boosters (Observer)": github_icon_base_url + "blizzard/btn-upgrade-protoss-graviticbooster.png",
+ "Sensor Array (Observer)": github_icon_base_url + "blizzard/btn-ability-zeratul-observer-sensorarray.png",
+ "Scarab Damage (Reaver)": github_icon_base_url + "blizzard/btn-ability-protoss-scarabshot.png",
+ "Solarite Payload (Reaver)": github_icon_base_url + "blizzard/btn-upgrade-artanis-scarabsplashradius.png",
+ "Reaver Capacity (Reaver)": github_icon_base_url + "original/btn-upgrade-protoss-increasedscarabcapacity@scbw.png",
+ "Resource Efficiency (Reaver)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+
+ "Phoenix": "https://static.wikia.nocookie.net/starcraft/images/b/b1/Icon_Protoss_Phoenix.jpg",
+ "Mirage": github_icon_base_url + "blizzard/btn-unit-protoss-phoenix-purifier.png",
+ "Corsair": github_icon_base_url + "blizzard/btn-unit-protoss-corsair.png",
+ "Destroyer": github_icon_base_url + "blizzard/btn-unit-protoss-voidray-taldarim.png",
+ "Void Ray": github_icon_base_url + "blizzard/btn-unit-protoss-voidray-nerazim.png",
+ "Carrier": "https://static.wikia.nocookie.net/starcraft/images/2/2c/Icon_Protoss_Carrier.jpg",
+ "Scout": github_icon_base_url + "original/btn-unit-protoss-scout.png",
+ "Tempest": github_icon_base_url + "blizzard/btn-unit-protoss-tempest-purifier.png",
+ "Mothership": github_icon_base_url + "blizzard/btn-unit-protoss-mothership-taldarim.png",
+ "Arbiter": github_icon_base_url + "blizzard/btn-unit-protoss-arbiter.png",
+ "Oracle": github_icon_base_url + "blizzard/btn-unit-protoss-oracle.png",
+
+ "Ionic Wavelength Flux (Phoenix/Mirage)": github_icon_base_url + "blizzard/btn-upgrade-protoss-airweaponslevel0.png",
+ "Anion Pulse-Crystals (Phoenix/Mirage)": github_icon_base_url + "blizzard/btn-upgrade-protoss-phoenixrange.png",
+ "Stealth Drive (Corsair)": github_icon_base_url + "blizzard/btn-upgrade-vorazun-corsairpermanentlycloaked.png",
+ "Argus Jewel (Corsair)": github_icon_base_url + "blizzard/btn-ability-protoss-stasistrap.png",
+ "Sustaining Disruption (Corsair)": github_icon_base_url + "blizzard/btn-ability-protoss-disruptionweb.png",
+ "Neutron Shields (Corsair)": github_icon_base_url + "blizzard/btn-upgrade-protoss-shieldslevel1.png",
+ "Reforged Bloodshard Core (Destroyer)": github_icon_base_url + "blizzard/btn-amonshardsarmor.png",
+ "Flux Vanes (Void Ray/Destroyer)": github_icon_base_url + "blizzard/btn-upgrade-protoss-fluxvanes.png",
+ "Graviton Catapult (Carrier)": github_icon_base_url + "blizzard/btn-upgrade-protoss-gravitoncatapult.png",
+ "Hull of Past Glories (Carrier)": github_icon_base_url + "blizzard/btn-progression-protoss-fenix-14-colossusandcarrierchampionsresearch.png",
+ "Combat Sensor Array (Scout)": github_icon_base_url + "blizzard/btn-upgrade-protoss-fenix-scoutchampionrange.png",
+ "Apial Sensors (Scout)": github_icon_base_url + "blizzard/btn-upgrade-tychus-detection.png",
+ "Gravitic Thrusters (Scout)": github_icon_base_url + "blizzard/btn-upgrade-protoss-graviticbooster.png",
+ "Advanced Photon Blasters (Scout)": github_icon_base_url + "blizzard/btn-upgrade-protoss-airweaponslevel3.png",
+ "Tectonic Destabilizers (Tempest)": github_icon_base_url + "blizzard/btn-ability-protoss-disruptionblast.png",
+ "Quantic Reactor (Tempest)": github_icon_base_url + "blizzard/btn-upgrade-protoss-researchgravitysling.png",
+ "Gravity Sling (Tempest)": github_icon_base_url + "blizzard/btn-upgrade-protoss-tectonicdisruptors.png",
+ "Chronostatic Reinforcement (Arbiter)": github_icon_base_url + "blizzard/btn-upgrade-protoss-airarmorlevel2.png",
+ "Khaydarin Core (Arbiter)": github_icon_base_url + "blizzard/btn-upgrade-protoss-adeptshieldupgrade.png",
+ "Spacetime Anchor (Arbiter)": github_icon_base_url + "blizzard/btn-ability-protoss-stasisfield.png",
+ "Resource Efficiency (Arbiter)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
+ "Enhanced Cloak Field (Arbiter)": github_icon_base_url + "blizzard/btn-ability-stetmann-stetzonegenerator-speed.png",
+ "Stealth Drive (Oracle)": github_icon_base_url + "blizzard/btn-upgrade-vorazun-oraclepermanentlycloaked.png",
+ "Stasis Calibration (Oracle)": github_icon_base_url + "blizzard/btn-ability-protoss-oracle-stasiscalibration.png",
+ "Temporal Acceleration Beam (Oracle)": github_icon_base_url + "blizzard/btn-ability-protoss-oraclepulsarcannonon.png",
+
+ "Matrix Overload": github_icon_base_url + "blizzard/btn-ability-spearofadun-matrixoverload.png",
+ "Guardian Shell": github_icon_base_url + "blizzard/btn-ability-spearofadun-guardianshell.png",
+
+ "Chrono Surge (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-chronosurge.png",
+ "Proxy Pylon (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-deploypylon.png",
+ "Warp In Reinforcements (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-warpinreinforcements.png",
+ "Pylon Overcharge (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-protoss-purify.png",
+ "Orbital Strike (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-orbitalstrike.png",
+ "Temporal Field (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-temporalfield.png",
+ "Solar Lance (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-solarlance.png",
+ "Mass Recall (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-massrecall.png",
+ "Shield Overcharge (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-shieldovercharge.png",
+ "Deploy Fenix (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-unit-protoss-fenix.png",
+ "Purifier Beam (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-purifierbeam.png",
+ "Time Stop (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-timestop.png",
+ "Solar Bombardment (Spear of Adun Calldown)": github_icon_base_url + "blizzard/btn-ability-spearofadun-solarbombardment.png",
+
+ "Reconstruction Beam (Spear of Adun Auto-Cast)": github_icon_base_url + "blizzard/btn-ability-spearofadun-reconstructionbeam.png",
+ "Overwatch (Spear of Adun Auto-Cast)": github_icon_base_url + "blizzard/btn-ability-zeratul-chargedcrystal-psionicwinds.png",
+
+ "Nothing": "",
+ }
+ sc2wol_location_ids = {
+ "Liberation Day": range(SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 200),
+ "The Outlaws": range(SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 300),
+ "Zero Hour": range(SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 400),
+ "Evacuation": range(SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 500),
+ "Outbreak": range(SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 600),
+ "Safe Haven": range(SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 700),
+ "Haven's Fall": range(SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 800),
+ "Smash and Grab": range(SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 900),
+ "The Dig": range(SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 1000),
+ "The Moebius Factor": range(SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1100),
+ "Supernova": range(SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1200),
+ "Maw of the Void": range(SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1300),
+ "Devil's Playground": range(SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1400),
+ "Welcome to the Jungle": range(SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1500),
+ "Breakout": range(SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1600),
+ "Ghost of a Chance": range(SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1700),
+ "The Great Train Robbery": range(SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1800),
+ "Cutthroat": range(SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1900),
+ "Engine of Destruction": range(SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 2000),
+ "Media Blitz": range(SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2100),
+ "Piercing the Shroud": range(SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2200),
+ "Whispers of Doom": range(SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2300),
+ "A Sinister Turn": range(SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2400),
+ "Echoes of the Future": range(SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2500),
+ "In Utter Darkness": range(SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2600),
+ "Gates of Hell": range(SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2700),
+ "Belly of the Beast": range(SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2800),
+ "Shatter the Sky": range(SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2900),
+ "All-In": range(SC2WOL_LOC_ID_OFFSET + 2900, SC2WOL_LOC_ID_OFFSET + 3000),
+
+ "Lab Rat": range(SC2HOTS_LOC_ID_OFFSET + 100, SC2HOTS_LOC_ID_OFFSET + 200),
+ "Back in the Saddle": range(SC2HOTS_LOC_ID_OFFSET + 200, SC2HOTS_LOC_ID_OFFSET + 300),
+ "Rendezvous": range(SC2HOTS_LOC_ID_OFFSET + 300, SC2HOTS_LOC_ID_OFFSET + 400),
+ "Harvest of Screams": range(SC2HOTS_LOC_ID_OFFSET + 400, SC2HOTS_LOC_ID_OFFSET + 500),
+ "Shoot the Messenger": range(SC2HOTS_LOC_ID_OFFSET + 500, SC2HOTS_LOC_ID_OFFSET + 600),
+ "Enemy Within": range(SC2HOTS_LOC_ID_OFFSET + 600, SC2HOTS_LOC_ID_OFFSET + 700),
+ "Domination": range(SC2HOTS_LOC_ID_OFFSET + 700, SC2HOTS_LOC_ID_OFFSET + 800),
+ "Fire in the Sky": range(SC2HOTS_LOC_ID_OFFSET + 800, SC2HOTS_LOC_ID_OFFSET + 900),
+ "Old Soldiers": range(SC2HOTS_LOC_ID_OFFSET + 900, SC2HOTS_LOC_ID_OFFSET + 1000),
+ "Waking the Ancient": range(SC2HOTS_LOC_ID_OFFSET + 1000, SC2HOTS_LOC_ID_OFFSET + 1100),
+ "The Crucible": range(SC2HOTS_LOC_ID_OFFSET + 1100, SC2HOTS_LOC_ID_OFFSET + 1200),
+ "Supreme": range(SC2HOTS_LOC_ID_OFFSET + 1200, SC2HOTS_LOC_ID_OFFSET + 1300),
+ "Infested": range(SC2HOTS_LOC_ID_OFFSET + 1300, SC2HOTS_LOC_ID_OFFSET + 1400),
+ "Hand of Darkness": range(SC2HOTS_LOC_ID_OFFSET + 1400, SC2HOTS_LOC_ID_OFFSET + 1500),
+ "Phantoms of the Void": range(SC2HOTS_LOC_ID_OFFSET + 1500, SC2HOTS_LOC_ID_OFFSET + 1600),
+ "With Friends Like These": range(SC2HOTS_LOC_ID_OFFSET + 1600, SC2HOTS_LOC_ID_OFFSET + 1700),
+ "Conviction": range(SC2HOTS_LOC_ID_OFFSET + 1700, SC2HOTS_LOC_ID_OFFSET + 1800),
+ "Planetfall": range(SC2HOTS_LOC_ID_OFFSET + 1800, SC2HOTS_LOC_ID_OFFSET + 1900),
+ "Death From Above": range(SC2HOTS_LOC_ID_OFFSET + 1900, SC2HOTS_LOC_ID_OFFSET + 2000),
+ "The Reckoning": range(SC2HOTS_LOC_ID_OFFSET + 2000, SC2HOTS_LOC_ID_OFFSET + 2100),
+
+ "Dark Whispers": range(SC2LOTV_LOC_ID_OFFSET + 100, SC2LOTV_LOC_ID_OFFSET + 200),
+ "Ghosts in the Fog": range(SC2LOTV_LOC_ID_OFFSET + 200, SC2LOTV_LOC_ID_OFFSET + 300),
+ "Evil Awoken": range(SC2LOTV_LOC_ID_OFFSET + 300, SC2LOTV_LOC_ID_OFFSET + 400),
+
+ "For Aiur!": range(SC2LOTV_LOC_ID_OFFSET + 400, SC2LOTV_LOC_ID_OFFSET + 500),
+ "The Growing Shadow": range(SC2LOTV_LOC_ID_OFFSET + 500, SC2LOTV_LOC_ID_OFFSET + 600),
+ "The Spear of Adun": range(SC2LOTV_LOC_ID_OFFSET + 600, SC2LOTV_LOC_ID_OFFSET + 700),
+ "Sky Shield": range(SC2LOTV_LOC_ID_OFFSET + 700, SC2LOTV_LOC_ID_OFFSET + 800),
+ "Brothers in Arms": range(SC2LOTV_LOC_ID_OFFSET + 800, SC2LOTV_LOC_ID_OFFSET + 900),
+ "Amon's Reach": range(SC2LOTV_LOC_ID_OFFSET + 900, SC2LOTV_LOC_ID_OFFSET + 1000),
+ "Last Stand": range(SC2LOTV_LOC_ID_OFFSET + 1000, SC2LOTV_LOC_ID_OFFSET + 1100),
+ "Forbidden Weapon": range(SC2LOTV_LOC_ID_OFFSET + 1100, SC2LOTV_LOC_ID_OFFSET + 1200),
+ "Temple of Unification": range(SC2LOTV_LOC_ID_OFFSET + 1200, SC2LOTV_LOC_ID_OFFSET + 1300),
+ "The Infinite Cycle": range(SC2LOTV_LOC_ID_OFFSET + 1300, SC2LOTV_LOC_ID_OFFSET + 1400),
+ "Harbinger of Oblivion": range(SC2LOTV_LOC_ID_OFFSET + 1400, SC2LOTV_LOC_ID_OFFSET + 1500),
+ "Unsealing the Past": range(SC2LOTV_LOC_ID_OFFSET + 1500, SC2LOTV_LOC_ID_OFFSET + 1600),
+ "Purification": range(SC2LOTV_LOC_ID_OFFSET + 1600, SC2LOTV_LOC_ID_OFFSET + 1700),
+ "Steps of the Rite": range(SC2LOTV_LOC_ID_OFFSET + 1700, SC2LOTV_LOC_ID_OFFSET + 1800),
+ "Rak'Shir": range(SC2LOTV_LOC_ID_OFFSET + 1800, SC2LOTV_LOC_ID_OFFSET + 1900),
+ "Templar's Charge": range(SC2LOTV_LOC_ID_OFFSET + 1900, SC2LOTV_LOC_ID_OFFSET + 2000),
+ "Templar's Return": range(SC2LOTV_LOC_ID_OFFSET + 2000, SC2LOTV_LOC_ID_OFFSET + 2100),
+ "The Host": range(SC2LOTV_LOC_ID_OFFSET + 2100, SC2LOTV_LOC_ID_OFFSET + 2200),
+ "Salvation": range(SC2LOTV_LOC_ID_OFFSET + 2200, SC2LOTV_LOC_ID_OFFSET + 2300),
+
+ "Into the Void": range(SC2LOTV_LOC_ID_OFFSET + 2300, SC2LOTV_LOC_ID_OFFSET + 2400),
+ "The Essence of Eternity": range(SC2LOTV_LOC_ID_OFFSET + 2400, SC2LOTV_LOC_ID_OFFSET + 2500),
+ "Amon's Fall": range(SC2LOTV_LOC_ID_OFFSET + 2500, SC2LOTV_LOC_ID_OFFSET + 2600),
+
+ "The Escape": range(SC2NCO_LOC_ID_OFFSET + 100, SC2NCO_LOC_ID_OFFSET + 200),
+ "Sudden Strike": range(SC2NCO_LOC_ID_OFFSET + 200, SC2NCO_LOC_ID_OFFSET + 300),
+ "Enemy Intelligence": range(SC2NCO_LOC_ID_OFFSET + 300, SC2NCO_LOC_ID_OFFSET + 400),
+ "Trouble In Paradise": range(SC2NCO_LOC_ID_OFFSET + 400, SC2NCO_LOC_ID_OFFSET + 500),
+ "Night Terrors": range(SC2NCO_LOC_ID_OFFSET + 500, SC2NCO_LOC_ID_OFFSET + 600),
+ "Flashpoint": range(SC2NCO_LOC_ID_OFFSET + 600, SC2NCO_LOC_ID_OFFSET + 700),
+ "In the Enemy's Shadow": range(SC2NCO_LOC_ID_OFFSET + 700, SC2NCO_LOC_ID_OFFSET + 800),
+ "Dark Skies": range(SC2NCO_LOC_ID_OFFSET + 800, SC2NCO_LOC_ID_OFFSET + 900),
+ "End Game": range(SC2NCO_LOC_ID_OFFSET + 900, SC2NCO_LOC_ID_OFFSET + 1000),
+ }
+
+ display_data = {}
+
+ # Grouped Items
+ grouped_item_ids = {
+ "Progressive Terran Weapon Upgrade": 107 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Armor Upgrade": 108 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Infantry Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Vehicle Upgrade": 110 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Ship Upgrade": 111 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Weapon/Armor Upgrade": 112 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Zerg Weapon Upgrade": 105 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Zerg Armor Upgrade": 106 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Zerg Ground Upgrade": 107 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Zerg Flyer Upgrade": 108 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Zerg Weapon/Armor Upgrade": 109 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Protoss Weapon Upgrade": 105 + SC2LOTV_ITEM_ID_OFFSET,
+ "Progressive Protoss Armor Upgrade": 106 + SC2LOTV_ITEM_ID_OFFSET,
+ "Progressive Protoss Ground Upgrade": 107 + SC2LOTV_ITEM_ID_OFFSET,
+ "Progressive Protoss Air Upgrade": 108 + SC2LOTV_ITEM_ID_OFFSET,
+ "Progressive Protoss Weapon/Armor Upgrade": 109 + SC2LOTV_ITEM_ID_OFFSET,
+ }
+ grouped_item_replacements = {
+ "Progressive Terran Weapon Upgrade": ["Progressive Terran Infantry Weapon",
+ "Progressive Terran Vehicle Weapon",
+ "Progressive Terran Ship Weapon"],
+ "Progressive Terran Armor Upgrade": ["Progressive Terran Infantry Armor",
+ "Progressive Terran Vehicle Armor",
+ "Progressive Terran Ship Armor"],
+ "Progressive Terran Infantry Upgrade": ["Progressive Terran Infantry Weapon",
+ "Progressive Terran Infantry Armor"],
+ "Progressive Terran Vehicle Upgrade": ["Progressive Terran Vehicle Weapon",
+ "Progressive Terran Vehicle Armor"],
+ "Progressive Terran Ship Upgrade": ["Progressive Terran Ship Weapon", "Progressive Terran Ship Armor"],
+ "Progressive Zerg Weapon Upgrade": ["Progressive Zerg Melee Attack", "Progressive Zerg Missile Attack",
+ "Progressive Zerg Flyer Attack"],
+ "Progressive Zerg Armor Upgrade": ["Progressive Zerg Ground Carapace",
+ "Progressive Zerg Flyer Carapace"],
+ "Progressive Zerg Ground Upgrade": ["Progressive Zerg Melee Attack", "Progressive Zerg Missile Attack",
+ "Progressive Zerg Ground Carapace"],
+ "Progressive Zerg Flyer Upgrade": ["Progressive Zerg Flyer Attack", "Progressive Zerg Flyer Carapace"],
+ "Progressive Protoss Weapon Upgrade": ["Progressive Protoss Ground Weapon",
+ "Progressive Protoss Air Weapon"],
+ "Progressive Protoss Armor Upgrade": ["Progressive Protoss Ground Armor", "Progressive Protoss Shields",
+ "Progressive Protoss Air Armor"],
+ "Progressive Protoss Ground Upgrade": ["Progressive Protoss Ground Weapon",
+ "Progressive Protoss Ground Armor",
+ "Progressive Protoss Shields"],
+ "Progressive Protoss Air Upgrade": ["Progressive Protoss Air Weapon", "Progressive Protoss Air Armor",
+ "Progressive Protoss Shields"]
+ }
+ grouped_item_replacements["Progressive Terran Weapon/Armor Upgrade"] = \
+ grouped_item_replacements["Progressive Terran Weapon Upgrade"] \
+ + grouped_item_replacements["Progressive Terran Armor Upgrade"]
+ grouped_item_replacements["Progressive Zerg Weapon/Armor Upgrade"] = \
+ grouped_item_replacements["Progressive Zerg Weapon Upgrade"] \
+ + grouped_item_replacements["Progressive Zerg Armor Upgrade"]
+ grouped_item_replacements["Progressive Protoss Weapon/Armor Upgrade"] = \
+ grouped_item_replacements["Progressive Protoss Weapon Upgrade"] \
+ + grouped_item_replacements["Progressive Protoss Armor Upgrade"]
+ replacement_item_ids = {
+ "Progressive Terran Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Zerg Melee Attack": 100 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Zerg Missile Attack": 101 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Zerg Ground Carapace": 102 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Zerg Flyer Attack": 103 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Zerg Flyer Carapace": 104 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Protoss Ground Weapon": 100 + SC2LOTV_ITEM_ID_OFFSET,
+ "Progressive Protoss Ground Armor": 101 + SC2LOTV_ITEM_ID_OFFSET,
+ "Progressive Protoss Shields": 102 + SC2LOTV_ITEM_ID_OFFSET,
+ "Progressive Protoss Air Weapon": 103 + SC2LOTV_ITEM_ID_OFFSET,
+ "Progressive Protoss Air Armor": 104 + SC2LOTV_ITEM_ID_OFFSET,
+ }
+
+ inventory: collections.Counter = tracker_data.get_player_inventory_counts(team, player)
+ for grouped_item_name, grouped_item_id in grouped_item_ids.items():
+ count: int = inventory[grouped_item_id]
+ if count > 0:
+ for replacement_item in grouped_item_replacements[grouped_item_name]:
+ replacement_id: int = replacement_item_ids[replacement_item]
+ if replacement_id not in inventory or count > inventory[replacement_id]:
+ # If two groups provide the same individual item, maximum is used
+ # (this behavior is used for Protoss Shields)
+ inventory[replacement_id] = count
+
+ # Determine display for progressive items
+ progressive_items = {
+ "Progressive Terran Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Terran Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Fire-Suppression System": 206 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Orbital Command": 207 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Stimpack (Marine)": 208 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Stimpack (Firebat)": 226 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Stimpack (Marauder)": 228 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Stimpack (Reaper)": 250 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Stimpack (Hellion)": 259 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Replenishable Magazine (Vulture)": 303 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Tri-Lithium Power Cell (Diamondback)": 306 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Tomahawk Power Cells (Wraith)": 312 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Cross-Spectrum Dampeners (Banshee)": 316 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Missile Pods (Battlecruiser)": 318 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Defensive Matrix (Battlecruiser)": 319 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Immortality Protocol (Thor)": 325 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive High Impact Payload (Thor)": 361 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Augmented Thrusters (Planetary Fortress)": 388 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Regenerative Bio-Steel": 617 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Stealth Suit Module (Nova Suit Module)": 904 + SC2WOL_ITEM_ID_OFFSET,
+ "Progressive Zerg Melee Attack": 100 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Zerg Missile Attack": 101 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Zerg Ground Carapace": 102 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Zerg Flyer Attack": 103 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Zerg Flyer Carapace": 104 + SC2HOTS_ITEM_ID_OFFSET,
+ "Progressive Protoss Ground Weapon": 100 + SC2LOTV_ITEM_ID_OFFSET,
+ "Progressive Protoss Ground Armor": 101 + SC2LOTV_ITEM_ID_OFFSET,
+ "Progressive Protoss Shields": 102 + SC2LOTV_ITEM_ID_OFFSET,
+ "Progressive Protoss Air Weapon": 103 + SC2LOTV_ITEM_ID_OFFSET,
+ "Progressive Protoss Air Armor": 104 + SC2LOTV_ITEM_ID_OFFSET,
+ "Progressive Proxy Pylon (Spear of Adun Calldown)": 701 + SC2LOTV_ITEM_ID_OFFSET,
+ }
+ # Format: L0, L1, L2, L3
+ progressive_names = {
+ "Progressive Terran Infantry Weapon": ["Terran Infantry Weapons Level 1",
+ "Terran Infantry Weapons Level 1",
+ "Terran Infantry Weapons Level 2",
+ "Terran Infantry Weapons Level 3"],
+ "Progressive Terran Infantry Armor": ["Terran Infantry Armor Level 1",
+ "Terran Infantry Armor Level 1",
+ "Terran Infantry Armor Level 2",
+ "Terran Infantry Armor Level 3"],
+ "Progressive Terran Vehicle Weapon": ["Terran Vehicle Weapons Level 1",
+ "Terran Vehicle Weapons Level 1",
+ "Terran Vehicle Weapons Level 2",
+ "Terran Vehicle Weapons Level 3"],
+ "Progressive Terran Vehicle Armor": ["Terran Vehicle Armor Level 1",
+ "Terran Vehicle Armor Level 1",
+ "Terran Vehicle Armor Level 2",
+ "Terran Vehicle Armor Level 3"],
+ "Progressive Terran Ship Weapon": ["Terran Ship Weapons Level 1",
+ "Terran Ship Weapons Level 1",
+ "Terran Ship Weapons Level 2",
+ "Terran Ship Weapons Level 3"],
+ "Progressive Terran Ship Armor": ["Terran Ship Armor Level 1",
+ "Terran Ship Armor Level 1",
+ "Terran Ship Armor Level 2",
+ "Terran Ship Armor Level 3"],
+ "Progressive Fire-Suppression System": ["Fire-Suppression System Level 1",
+ "Fire-Suppression System Level 1",
+ "Fire-Suppression System Level 2"],
+ "Progressive Orbital Command": ["Orbital Command", "Orbital Command",
+ "Planetary Command Module"],
+ "Progressive Stimpack (Marine)": ["Stimpack (Marine)", "Stimpack (Marine)",
+ "Super Stimpack (Marine)"],
+ "Progressive Stimpack (Firebat)": ["Stimpack (Firebat)", "Stimpack (Firebat)",
+ "Super Stimpack (Firebat)"],
+ "Progressive Stimpack (Marauder)": ["Stimpack (Marauder)", "Stimpack (Marauder)",
+ "Super Stimpack (Marauder)"],
+ "Progressive Stimpack (Reaper)": ["Stimpack (Reaper)", "Stimpack (Reaper)",
+ "Super Stimpack (Reaper)"],
+ "Progressive Stimpack (Hellion)": ["Stimpack (Hellion)", "Stimpack (Hellion)",
+ "Super Stimpack (Hellion)"],
+ "Progressive Replenishable Magazine (Vulture)": ["Replenishable Magazine (Vulture)",
+ "Replenishable Magazine (Vulture)",
+ "Replenishable Magazine (Free) (Vulture)"],
+ "Progressive Tri-Lithium Power Cell (Diamondback)": ["Tri-Lithium Power Cell (Diamondback)",
+ "Tri-Lithium Power Cell (Diamondback)",
+ "Tungsten Spikes (Diamondback)"],
+ "Progressive Tomahawk Power Cells (Wraith)": ["Tomahawk Power Cells (Wraith)",
+ "Tomahawk Power Cells (Wraith)",
+ "Unregistered Cloaking Module (Wraith)"],
+ "Progressive Cross-Spectrum Dampeners (Banshee)": ["Cross-Spectrum Dampeners (Banshee)",
+ "Cross-Spectrum Dampeners (Banshee)",
+ "Advanced Cross-Spectrum Dampeners (Banshee)"],
+ "Progressive Missile Pods (Battlecruiser)": ["Missile Pods (Battlecruiser) Level 1",
+ "Missile Pods (Battlecruiser) Level 1",
+ "Missile Pods (Battlecruiser) Level 2"],
+ "Progressive Defensive Matrix (Battlecruiser)": ["Defensive Matrix (Battlecruiser)",
+ "Defensive Matrix (Battlecruiser)",
+ "Advanced Defensive Matrix (Battlecruiser)"],
+ "Progressive Immortality Protocol (Thor)": ["Immortality Protocol (Thor)",
+ "Immortality Protocol (Thor)",
+ "Immortality Protocol (Free) (Thor)"],
+ "Progressive High Impact Payload (Thor)": ["High Impact Payload (Thor)",
+ "High Impact Payload (Thor)", "Smart Servos (Thor)"],
+ "Progressive Augmented Thrusters (Planetary Fortress)": ["Lift Off (Planetary Fortress)",
+ "Lift Off (Planetary Fortress)",
+ "Armament Stabilizers (Planetary Fortress)"],
+ "Progressive Regenerative Bio-Steel": ["Regenerative Bio-Steel Level 1",
+ "Regenerative Bio-Steel Level 1",
+ "Regenerative Bio-Steel Level 2",
+ "Regenerative Bio-Steel Level 3"],
+ "Progressive Stealth Suit Module (Nova Suit Module)": ["Stealth Suit Module (Nova Suit Module)",
+ "Cloak (Nova Suit Module)",
+ "Permanently Cloaked (Nova Suit Module)"],
+ "Progressive Zerg Melee Attack": ["Zerg Melee Attack Level 1",
+ "Zerg Melee Attack Level 1",
+ "Zerg Melee Attack Level 2",
+ "Zerg Melee Attack Level 3"],
+ "Progressive Zerg Missile Attack": ["Zerg Missile Attack Level 1",
+ "Zerg Missile Attack Level 1",
+ "Zerg Missile Attack Level 2",
+ "Zerg Missile Attack Level 3"],
+ "Progressive Zerg Ground Carapace": ["Zerg Ground Carapace Level 1",
+ "Zerg Ground Carapace Level 1",
+ "Zerg Ground Carapace Level 2",
+ "Zerg Ground Carapace Level 3"],
+ "Progressive Zerg Flyer Attack": ["Zerg Flyer Attack Level 1",
+ "Zerg Flyer Attack Level 1",
+ "Zerg Flyer Attack Level 2",
+ "Zerg Flyer Attack Level 3"],
+ "Progressive Zerg Flyer Carapace": ["Zerg Flyer Carapace Level 1",
+ "Zerg Flyer Carapace Level 1",
+ "Zerg Flyer Carapace Level 2",
+ "Zerg Flyer Carapace Level 3"],
+ "Progressive Protoss Ground Weapon": ["Protoss Ground Weapon Level 1",
+ "Protoss Ground Weapon Level 1",
+ "Protoss Ground Weapon Level 2",
+ "Protoss Ground Weapon Level 3"],
+ "Progressive Protoss Ground Armor": ["Protoss Ground Armor Level 1",
+ "Protoss Ground Armor Level 1",
+ "Protoss Ground Armor Level 2",
+ "Protoss Ground Armor Level 3"],
+ "Progressive Protoss Shields": ["Protoss Shields Level 1", "Protoss Shields Level 1",
+ "Protoss Shields Level 2", "Protoss Shields Level 3"],
+ "Progressive Protoss Air Weapon": ["Protoss Air Weapon Level 1",
+ "Protoss Air Weapon Level 1",
+ "Protoss Air Weapon Level 2",
+ "Protoss Air Weapon Level 3"],
+ "Progressive Protoss Air Armor": ["Protoss Air Armor Level 1",
+ "Protoss Air Armor Level 1",
+ "Protoss Air Armor Level 2",
+ "Protoss Air Armor Level 3"],
+ "Progressive Proxy Pylon (Spear of Adun Calldown)": ["Proxy Pylon (Spear of Adun Calldown)",
+ "Proxy Pylon (Spear of Adun Calldown)",
+ "Warp In Reinforcements (Spear of Adun Calldown)"]
+ }
+ for item_name, item_id in progressive_items.items():
+ level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
+ display_name = progressive_names[item_name][level]
+ base_name = (item_name.split(maxsplit=1)[1].lower()
+ .replace(' ', '_')
+ .replace("-", "")
+ .replace("(", "")
+ .replace(")", ""))
+ display_data[base_name + "_level"] = level
+ display_data[base_name + "_url"] = icons[display_name] if display_name in icons else "FIXME"
+ display_data[base_name + "_name"] = display_name
+
+ # Multi-items
+ multi_items = {
+ "Additional Starting Minerals": 800 + SC2WOL_ITEM_ID_OFFSET,
+ "Additional Starting Vespene": 801 + SC2WOL_ITEM_ID_OFFSET,
+ "Additional Starting Supply": 802 + SC2WOL_ITEM_ID_OFFSET
+ }
+ for item_name, item_id in multi_items.items():
+ base_name = item_name.split()[-1].lower()
+ count = inventory[item_id]
+ if base_name == "supply":
+ count = count * starting_supply_per_item
+ elif base_name == "minerals":
+ count = count * minerals_per_item
+ elif base_name == "vespene":
+ count = count * vespene_per_item
+ display_data[base_name + "_count"] = count
+ # Kerrigan level
+ level_items = {
+ "1 Kerrigan Level": 509 + SC2HOTS_ITEM_ID_OFFSET,
+ "2 Kerrigan Levels": 508 + SC2HOTS_ITEM_ID_OFFSET,
+ "3 Kerrigan Levels": 507 + SC2HOTS_ITEM_ID_OFFSET,
+ "4 Kerrigan Levels": 506 + SC2HOTS_ITEM_ID_OFFSET,
+ "5 Kerrigan Levels": 505 + SC2HOTS_ITEM_ID_OFFSET,
+ "6 Kerrigan Levels": 504 + SC2HOTS_ITEM_ID_OFFSET,
+ "7 Kerrigan Levels": 503 + SC2HOTS_ITEM_ID_OFFSET,
+ "8 Kerrigan Levels": 502 + SC2HOTS_ITEM_ID_OFFSET,
+ "9 Kerrigan Levels": 501 + SC2HOTS_ITEM_ID_OFFSET,
+ "10 Kerrigan Levels": 500 + SC2HOTS_ITEM_ID_OFFSET,
+ "14 Kerrigan Levels": 510 + SC2HOTS_ITEM_ID_OFFSET,
+ "35 Kerrigan Levels": 511 + SC2HOTS_ITEM_ID_OFFSET,
+ "70 Kerrigan Levels": 512 + SC2HOTS_ITEM_ID_OFFSET,
+ }
+ level_amounts = {
+ "1 Kerrigan Level": 1,
+ "2 Kerrigan Levels": 2,
+ "3 Kerrigan Levels": 3,
+ "4 Kerrigan Levels": 4,
+ "5 Kerrigan Levels": 5,
+ "6 Kerrigan Levels": 6,
+ "7 Kerrigan Levels": 7,
+ "8 Kerrigan Levels": 8,
+ "9 Kerrigan Levels": 9,
+ "10 Kerrigan Levels": 10,
+ "14 Kerrigan Levels": 14,
+ "35 Kerrigan Levels": 35,
+ "70 Kerrigan Levels": 70,
+ }
+ kerrigan_level = 0
+ for item_name, item_id in level_items.items():
+ count = inventory[item_id]
+ amount = level_amounts[item_name]
+ kerrigan_level += count * amount
+ display_data["kerrigan_level"] = kerrigan_level
+
+ # Victory condition
+ game_state = tracker_data.get_player_client_status(team, player)
+ display_data["game_finished"] = game_state == 30
+
+ # Turn location IDs into mission objective counts
+ locations = tracker_data.get_player_locations(team, player)
+ checked_locations = tracker_data.get_player_checked_locations(team, player)
+ lookup_name = lambda id: tracker_data.location_id_to_name["Starcraft 2"][id]
+ location_info = {mission_name: {lookup_name(id): (id in checked_locations) for id in mission_locations if
+ id in set(locations)} for mission_name, mission_locations in
+ sc2wol_location_ids.items()}
+ checks_done = {mission_name: len(
+ [id for id in mission_locations if id in checked_locations and id in set(locations)]) for
+ mission_name, mission_locations in sc2wol_location_ids.items()}
+ checks_done['Total'] = len(checked_locations)
+ checks_in_area = {mission_name: len([id for id in mission_locations if id in set(locations)]) for
+ mission_name, mission_locations in sc2wol_location_ids.items()}
+ checks_in_area['Total'] = sum(checks_in_area.values())
+
+ lookup_any_item_id_to_name = tracker_data.item_id_to_name["Starcraft 2"]
+ return render_template(
+ "tracker__Starcraft2.html",
+ inventory=inventory,
+ icons=icons,
+ acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0},
+ player=player,
+ team=team,
+ room=tracker_data.room,
+ player_name=tracker_data.get_player_name(team, player),
+ checks_done=checks_done,
+ checks_in_area=checks_in_area,
+ location_info=location_info,
+ **display_data,
+ )
+
+ _player_trackers["Starcraft 2"] = render_Starcraft2_tracker
diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py
index 89a839cfa364..45b26b175eb3 100644
--- a/WebHostLib/upload.py
+++ b/WebHostLib/upload.py
@@ -7,21 +7,50 @@
import zlib
from io import BytesIO
-from flask import request, flash, redirect, url_for, session, render_template
+from flask import request, flash, redirect, url_for, session, render_template, abort
from markupsafe import Markup
from pony.orm import commit, flush, select, rollback
from pony.orm.core import TransactionIntegrityError
+import schema
import MultiServer
from NetUtils import SlotType
from Utils import VersionException, __version__
+from worlds import GamesPackage
from worlds.Files import AutoPatchRegister
+from worlds.AutoWorld import data_package_checksum
from . import app
from .models import Seed, Room, Slot, GameDataPackage
-banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb")
+banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gbc", ".gba")
+allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip")
+allowed_generation_extensions = (".archipelago", ".zip")
+
+games_package_schema = schema.Schema({
+ "item_name_groups": {str: [str]},
+ "item_name_to_id": {str: int},
+ "location_name_groups": {str: [str]},
+ "location_name_to_id": {str: int},
+ schema.Optional("checksum"): str,
+ schema.Optional("version"): int,
+})
+
+
+def allowed_options(filename: str) -> bool:
+ return filename.endswith(allowed_options_extensions)
+
+
+def allowed_generation(filename: str) -> bool:
+ return filename.endswith(allowed_generation_extensions)
+
+
+def banned_file(filename: str) -> bool:
+ return filename.endswith(banned_extensions)
+
def process_multidata(compressed_multidata, files={}):
+ game_data: GamesPackage
+
decompressed_multidata = MultiServer.Context.decompress(compressed_multidata)
slots: typing.Set[Slot] = set()
@@ -30,11 +59,20 @@ def process_multidata(compressed_multidata, files={}):
game_data_packages: typing.List[GameDataPackage] = []
for game, game_data in decompressed_multidata["datapackage"].items():
if game_data.get("checksum"):
+ original_checksum = game_data.pop("checksum")
+ game_data = games_package_schema.validate(game_data)
+ game_data = {key: value for key, value in sorted(game_data.items())}
+ game_data["checksum"] = data_package_checksum(game_data)
+ if original_checksum != game_data["checksum"]:
+ raise Exception(f"Original checksum {original_checksum} != "
+ f"calculated checksum {game_data['checksum']} "
+ f"for game {game}.")
+
game_data_package = GameDataPackage(checksum=game_data["checksum"],
data=pickle.dumps(game_data))
decompressed_multidata["datapackage"][game] = {
"version": game_data.get("version", 0),
- "checksum": game_data["checksum"]
+ "checksum": game_data["checksum"],
}
try:
commit() # commit game data package
@@ -49,20 +87,21 @@ def process_multidata(compressed_multidata, files={}):
if slot_info.type == SlotType.group:
continue
slots.add(Slot(data=files.get(slot, None),
- player_name=slot_info.name,
- player_id=slot,
- game=slot_info.game))
+ player_name=slot_info.name,
+ player_id=slot,
+ game=slot_info.game))
flush() # commit slots
compressed_multidata = compressed_multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9)
return slots, compressed_multidata
+
def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None):
if not owner:
owner = session["_id"]
infolist = zfile.infolist()
- if all(file.filename.endswith((".yaml", ".yml")) or file.is_dir() for file in infolist):
- flash(Markup("Error: Your .zip file only contains .yaml files. "
+ if all(allowed_options(file.filename) or file.is_dir() for file in infolist):
+ flash(Markup("Error: Your .zip file only contains options files. "
'Did you mean to generate a game ?'))
return
@@ -73,7 +112,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
# Load files.
for file in infolist:
handler = AutoPatchRegister.get_handler(file.filename)
- if file.filename.endswith(banned_zip_contents):
+ if banned_file(file.filename):
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
"Your file was deleted."
@@ -104,13 +143,21 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
# Factorio
elif file.filename.endswith(".zip"):
- _, _, slot_id, *_ = file.filename.split('_')[0].split('-', 3)
+ try:
+ _, _, slot_id, *_ = file.filename.split('_')[0].split('-', 3)
+ except ValueError:
+ flash("Error: Unexpected file found in .zip: " + file.filename)
+ return
data = zfile.open(file, "r").read()
files[int(slot_id[1:])] = data
# All other files using the standard MultiWorld.get_out_file_name_base method
else:
- _, _, slot_id, *_ = file.filename.split('.')[0].split('_', 3)
+ try:
+ _, _, slot_id, *_ = file.filename.split('.')[0].split('_', 3)
+ except ValueError:
+ flash("Error: Unexpected file found in .zip: " + file.filename)
+ return
data = zfile.open(file, "r").read()
files[int(slot_id[1:])] = data
@@ -128,35 +175,36 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
flash("No multidata was found in the zip file, which is required.")
-@app.route('/uploads', methods=['GET', 'POST'])
+@app.route("/uploads", methods=["GET", "POST"])
def uploads():
- if request.method == 'POST':
- # check if the post request has the file part
- if 'file' not in request.files:
- flash('No file part')
+ if request.method == "POST":
+ # check if the POST request has a file part.
+ if "file" not in request.files:
+ flash("No file part in POST request.")
else:
- file = request.files['file']
- # if user does not select file, browser also
- # submit an empty part without filename
- if file.filename == '':
- flash('No selected file')
- elif file and allowed_file(file.filename):
- if zipfile.is_zipfile(file):
- with zipfile.ZipFile(file, 'r') as zfile:
+ uploaded_file = request.files["file"]
+ # If the user does not select file, the browser will still submit an empty string without a file name.
+ if uploaded_file.filename == "":
+ flash("No selected file.")
+ elif uploaded_file and allowed_generation(uploaded_file.filename):
+ if zipfile.is_zipfile(uploaded_file):
+ with zipfile.ZipFile(uploaded_file, "r") as zfile:
try:
res = upload_zip_to_db(zfile)
except VersionException:
flash(f"Could not load multidata. Wrong Version detected.")
+ except Exception as e:
+ flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
else:
- if type(res) == str:
+ if res is str:
return res
elif res:
return redirect(url_for("view_seed", seed=res.id))
else:
- file.seek(0) # offset from is_zipfile check
+ uploaded_file.seek(0) # offset from is_zipfile check
# noinspection PyBroadException
try:
- multidata = file.read()
+ multidata = uploaded_file.read()
slots, multidata = process_multidata(multidata)
except Exception as e:
flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
@@ -176,5 +224,27 @@ def user_content():
return render_template("userContent.html", rooms=rooms, seeds=seeds)
-def allowed_file(filename):
- return filename.endswith(('.archipelago', ".zip"))
+@app.route("/disown_seed/", methods=["GET"])
+def disown_seed(seed):
+ seed = Seed.get(id=seed)
+ if not seed:
+ return abort(404)
+ if seed.owner != session["_id"]:
+ return abort(403)
+
+ seed.owner = 0
+
+ return redirect(url_for("user_content"))
+
+
+@app.route("/disown_room/", methods=["GET"])
+def disown_room(room):
+ room = Room.get(id=room)
+ if not room:
+ return abort(404)
+ if room.owner != session["_id"]:
+ return abort(403)
+
+ room.owner = 0
+
+ return redirect(url_for("user_content"))
diff --git a/Zelda1Client.py b/Zelda1Client.py
index db3d3519aa60..1154804fbf56 100644
--- a/Zelda1Client.py
+++ b/Zelda1Client.py
@@ -13,7 +13,6 @@
import Utils
from Utils import async_start
-from worlds import lookup_any_location_id_to_name
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
get_base_parser
@@ -153,7 +152,7 @@ def get_payload(ctx: ZeldaContext):
def reconcile_shops(ctx: ZeldaContext):
- checked_location_names = [lookup_any_location_id_to_name[location] for location in ctx.checked_locations]
+ checked_location_names = [ctx.location_names.lookup_in_game(location) for location in ctx.checked_locations]
shops = [location for location in checked_location_names if "Shop" in location]
left_slots = [shop for shop in shops if "Left" in shop]
middle_slots = [shop for shop in shops if "Middle" in shop]
@@ -191,7 +190,7 @@ async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone=
locations_checked = []
location = None
for location in ctx.missing_locations:
- location_name = lookup_any_location_id_to_name[location]
+ location_name = ctx.location_names.lookup_in_game(location)
if location_name in Locations.overworld_locations and zone == "overworld":
status = locations_array[Locations.major_location_offsets[location_name]]
diff --git a/ZillionClient.py b/ZillionClient.py
index 7d32a722615e..ef96edab045e 100644
--- a/ZillionClient.py
+++ b/ZillionClient.py
@@ -1,505 +1,10 @@
-import asyncio
-import base64
-import platform
-from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, Type, cast
+import ModuleUpdate
+ModuleUpdate.update()
-# CommonClient import first to trigger ModuleUpdater
-from CommonClient import CommonContext, server_loop, gui_enabled, \
- ClientCommandProcessor, logger, get_base_parser
-from NetUtils import ClientStatus
-import Utils
-from Utils import async_start
-
-import colorama # type: ignore
-
-from zilliandomizer.zri.memory import Memory
-from zilliandomizer.zri import events
-from zilliandomizer.utils.loc_name_maps import id_to_loc
-from zilliandomizer.options import Chars
-from zilliandomizer.patch import RescueInfo
-
-from worlds.zillion.id_maps import make_id_to_others
-from worlds.zillion.config import base_id, zillion_map
-
-
-class ZillionCommandProcessor(ClientCommandProcessor):
- ctx: "ZillionContext"
-
- def _cmd_sms(self) -> None:
- """ Tell the client that Zillion is running in RetroArch. """
- logger.info("ready to look for game")
- self.ctx.look_for_retroarch.set()
-
- def _cmd_map(self) -> None:
- """ Toggle view of the map tracker. """
- self.ctx.ui_toggle_map()
-
-
-class ToggleCallback(Protocol):
- def __call__(self) -> None: ...
-
-
-class SetRoomCallback(Protocol):
- def __call__(self, rooms: List[List[int]]) -> None: ...
-
-
-class ZillionContext(CommonContext):
- game = "Zillion"
- command_processor: Type[ClientCommandProcessor] = ZillionCommandProcessor
- items_handling = 1 # receive items from other players
-
- known_name: Optional[str]
- """ This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """
-
- from_game: "asyncio.Queue[events.EventFromGame]"
- to_game: "asyncio.Queue[events.EventToGame]"
- ap_local_count: int
- """ local checks watched by server """
- next_item: int
- """ index in `items_received` """
- ap_id_to_name: Dict[int, str]
- ap_id_to_zz_id: Dict[int, int]
- start_char: Chars = "JJ"
- rescues: Dict[int, RescueInfo] = {}
- loc_mem_to_id: Dict[int, int] = {}
- got_room_info: asyncio.Event
- """ flag for connected to server """
- got_slot_data: asyncio.Event
- """ serves as a flag for whether I am logged in to the server """
-
- look_for_retroarch: asyncio.Event
- """
- There is a bug in Python in Windows
- https://github.com/python/cpython/issues/91227
- that makes it so if I look for RetroArch before it's ready,
- it breaks the asyncio udp transport system.
-
- As a workaround, we don't look for RetroArch until this event is set.
- """
-
- ui_toggle_map: ToggleCallback
- ui_set_rooms: SetRoomCallback
- """ parameter is y 16 x 8 numbers to show in each room """
-
- def __init__(self,
- server_address: str,
- password: str) -> None:
- super().__init__(server_address, password)
- self.known_name = None
- self.from_game = asyncio.Queue()
- self.to_game = asyncio.Queue()
- self.got_room_info = asyncio.Event()
- self.got_slot_data = asyncio.Event()
- self.ui_toggle_map = lambda: None
- self.ui_set_rooms = lambda rooms: None
-
- self.look_for_retroarch = asyncio.Event()
- if platform.system() != "Windows":
- # asyncio udp bug is only on Windows
- self.look_for_retroarch.set()
-
- self.reset_game_state()
-
- def reset_game_state(self) -> None:
- for _ in range(self.from_game.qsize()):
- self.from_game.get_nowait()
- for _ in range(self.to_game.qsize()):
- self.to_game.get_nowait()
- self.got_slot_data.clear()
-
- self.ap_local_count = 0
- self.next_item = 0
- self.ap_id_to_name = {}
- self.ap_id_to_zz_id = {}
- self.rescues = {}
- self.loc_mem_to_id = {}
-
- self.locations_checked.clear()
- self.missing_locations.clear()
- self.checked_locations.clear()
- self.finished_game = False
- self.items_received.clear()
-
- # override
- def on_deathlink(self, data: Dict[str, Any]) -> None:
- self.to_game.put_nowait(events.DeathEventToGame())
- return super().on_deathlink(data)
-
- # override
- async def server_auth(self, password_requested: bool = False) -> None:
- if password_requested and not self.password:
- await super().server_auth(password_requested)
- if not self.auth:
- logger.info('waiting for connection to game...')
- return
- logger.info("logging in to server...")
- await self.send_connect()
-
- # override
- def run_gui(self) -> None:
- from kvui import GameManager
- from kivy.core.text import Label as CoreLabel
- from kivy.graphics import Ellipse, Color, Rectangle
- from kivy.uix.layout import Layout
- from kivy.uix.widget import Widget
-
- class ZillionManager(GameManager):
- logging_pairs = [
- ("Client", "Archipelago")
- ]
- base_title = "Archipelago Zillion Client"
-
- class MapPanel(Widget):
- MAP_WIDTH: ClassVar[int] = 281
-
- _number_textures: List[Any] = []
- rooms: List[List[int]] = []
-
- def __init__(self, **kwargs: Any) -> None:
- super().__init__(**kwargs)
-
- self.rooms = [[0 for _ in range(8)] for _ in range(16)]
-
- self._make_numbers()
- self.update_map()
-
- self.bind(pos=self.update_map)
- # self.bind(size=self.update_bg)
-
- def _make_numbers(self) -> None:
- self._number_textures = []
- for n in range(10):
- label = CoreLabel(text=str(n), font_size=22, color=(0.1, 0.9, 0, 1))
- label.refresh()
- self._number_textures.append(label.texture)
-
- def update_map(self, *args: Any) -> None:
- self.canvas.clear()
-
- with self.canvas:
- Color(1, 1, 1, 1)
- Rectangle(source=zillion_map,
- pos=self.pos,
- size=(ZillionManager.MapPanel.MAP_WIDTH,
- int(ZillionManager.MapPanel.MAP_WIDTH * 1.456))) # aspect ratio of that image
- for y in range(16):
- for x in range(8):
- num = self.rooms[15 - y][x]
- if num > 0:
- Color(0, 0, 0, 0.4)
- pos = [self.pos[0] + 17 + x * 32, self.pos[1] + 14 + y * 24]
- Ellipse(size=[22, 22], pos=pos)
- Color(1, 1, 1, 1)
- pos = [self.pos[0] + 22 + x * 32, self.pos[1] + 12 + y * 24]
- num_texture = self._number_textures[num]
- Rectangle(texture=num_texture, size=num_texture.size, pos=pos)
-
- def build(self) -> Layout:
- container = super().build()
- self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=0)
- self.main_area_container.add_widget(self.map_widget)
- return container
-
- def toggle_map_width(self) -> None:
- if self.map_widget.width == 0:
- self.map_widget.width = ZillionManager.MapPanel.MAP_WIDTH
- else:
- self.map_widget.width = 0
- self.container.do_layout()
-
- def set_rooms(self, rooms: List[List[int]]) -> None:
- self.map_widget.rooms = rooms
- self.map_widget.update_map()
-
- self.ui = ZillionManager(self)
- self.ui_toggle_map = lambda: self.ui.toggle_map_width()
- self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms)
- run_co: Coroutine[Any, Any, None] = self.ui.async_run()
- self.ui_task = asyncio.create_task(run_co, name="UI")
-
- def on_package(self, cmd: str, args: Dict[str, Any]) -> None:
- self.room_item_numbers_to_ui()
- if cmd == "Connected":
- logger.info("logged in to Archipelago server")
- if "slot_data" not in args:
- logger.warn("`Connected` packet missing `slot_data`")
- return
- slot_data = args["slot_data"]
-
- if "start_char" not in slot_data:
- logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`")
- return
- self.start_char = slot_data['start_char']
- if self.start_char not in {"Apple", "Champ", "JJ"}:
- logger.warn("invalid Zillion `Connected` packet, "
- f"`slot_data` `start_char` has invalid value: {self.start_char}")
-
- if "rescues" not in slot_data:
- logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`")
- return
- rescues = slot_data["rescues"]
- self.rescues = {}
- for rescue_id, json_info in rescues.items():
- assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}"
- # TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch?
- assert json_info["start_char"] == self.start_char, \
- f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}'
- ri = RescueInfo(json_info["start_char"],
- json_info["room_code"],
- json_info["mask"])
- self.rescues[0 if rescue_id == "0" else 1] = ri
-
- if "loc_mem_to_id" not in slot_data:
- logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`")
- return
- loc_mem_to_id = slot_data["loc_mem_to_id"]
- self.loc_mem_to_id = {}
- for mem_str, id_str in loc_mem_to_id.items():
- mem = int(mem_str)
- id_ = int(id_str)
- room_i = mem // 256
- assert 0 <= room_i < 74
- assert id_ in id_to_loc
- self.loc_mem_to_id[mem] = id_
-
- if len(self.loc_mem_to_id) != 394:
- logger.warn("invalid Zillion `Connected` packet, "
- f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}")
-
- self.got_slot_data.set()
-
- payload = {
- "cmd": "Get",
- "keys": [f"zillion-{self.auth}-doors"]
- }
- async_start(self.send_msgs([payload]))
- elif cmd == "Retrieved":
- if "keys" not in args:
- logger.warning(f"invalid Retrieved packet to ZillionClient: {args}")
- return
- keys = cast(Dict[str, Optional[str]], args["keys"])
- doors_b64 = keys[f"zillion-{self.auth}-doors"]
- if doors_b64:
- logger.info("received door data from server")
- doors = base64.b64decode(doors_b64)
- self.to_game.put_nowait(events.DoorEventToGame(doors))
- elif cmd == "RoomInfo":
- self.seed_name = args["seed_name"]
- self.got_room_info.set()
-
- def room_item_numbers_to_ui(self) -> None:
- rooms = [[0 for _ in range(8)] for _ in range(16)]
- for loc_id in self.missing_locations:
- loc_id_small = loc_id - base_id
- loc_name = id_to_loc[loc_id_small]
- y = ord(loc_name[0]) - 65
- x = ord(loc_name[2]) - 49
- if y == 9 and x == 5:
- # don't show main computer in numbers
- continue
- assert (0 <= y < 16) and (0 <= x < 8), f"invalid index from location name {loc_name}"
- rooms[y][x] += 1
- # TODO: also add locations with locals lost from loading save state or reset
- self.ui_set_rooms(rooms)
-
- def process_from_game_queue(self) -> None:
- if self.from_game.qsize():
- event_from_game = self.from_game.get_nowait()
- if isinstance(event_from_game, events.AcquireLocationEventFromGame):
- server_id = event_from_game.id + base_id
- loc_name = id_to_loc[event_from_game.id]
- self.locations_checked.add(server_id)
- if server_id in self.missing_locations:
- self.ap_local_count += 1
- n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win
- logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})')
- async_start(self.send_msgs([
- {"cmd": 'LocationChecks', "locations": [server_id]}
- ]))
- else:
- # This will happen a lot in Zillion,
- # because all the key words are local and unwatched by the server.
- logger.debug(f"DEBUG: {loc_name} not in missing")
- elif isinstance(event_from_game, events.DeathEventFromGame):
- async_start(self.send_death())
- elif isinstance(event_from_game, events.WinEventFromGame):
- if not self.finished_game:
- async_start(self.send_msgs([
- {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}
- ]))
- self.finished_game = True
- elif isinstance(event_from_game, events.DoorEventFromGame):
- if self.auth:
- doors_b64 = base64.b64encode(event_from_game.doors).decode()
- payload = {
- "cmd": "Set",
- "key": f"zillion-{self.auth}-doors",
- "operations": [{"operation": "replace", "value": doors_b64}]
- }
- async_start(self.send_msgs([payload]))
- else:
- logger.warning(f"WARNING: unhandled event from game {event_from_game}")
-
- def process_items_received(self) -> None:
- if len(self.items_received) > self.next_item:
- zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received]
- for index in range(self.next_item, len(self.items_received)):
- ap_id = self.items_received[index].item
- from_name = self.player_names[self.items_received[index].player]
- # TODO: colors in this text, like sni client?
- logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}')
- self.to_game.put_nowait(
- events.ItemEventToGame(zz_item_ids)
- )
- self.next_item = len(self.items_received)
-
-
-def name_seed_from_ram(data: bytes) -> Tuple[str, str]:
- """ returns player name, and end of seed string """
- if len(data) == 0:
- # no connection to game
- return "", "xxx"
- null_index = data.find(b'\x00')
- if null_index == -1:
- logger.warning(f"invalid game id in rom {repr(data)}")
- null_index = len(data)
- name = data[:null_index].decode()
- null_index_2 = data.find(b'\x00', null_index + 1)
- if null_index_2 == -1:
- null_index_2 = len(data)
- seed_name = data[null_index + 1:null_index_2].decode()
-
- return name, seed_name
-
-
-async def zillion_sync_task(ctx: ZillionContext) -> None:
- logger.info("started zillion sync task")
-
- # to work around the Python bug where we can't check for RetroArch
- if not ctx.look_for_retroarch.is_set():
- logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.")
- await asyncio.wait((
- asyncio.create_task(ctx.look_for_retroarch.wait()),
- asyncio.create_task(ctx.exit_event.wait())
- ), return_when=asyncio.FIRST_COMPLETED)
-
- last_log = ""
-
- def log_no_spam(msg: str) -> None:
- nonlocal last_log
- if msg != last_log:
- last_log = msg
- logger.info(msg)
-
- # to only show this message once per client run
- help_message_shown = False
-
- with Memory(ctx.from_game, ctx.to_game) as memory:
- while not ctx.exit_event.is_set():
- ram = await memory.read()
- game_id = memory.get_rom_to_ram_data(ram)
- name, seed_end = name_seed_from_ram(game_id)
- if len(name):
- if name == ctx.known_name:
- ctx.auth = name
- # this is the name we know
- if ctx.server and ctx.server.socket: # type: ignore
- if ctx.got_room_info.is_set():
- if ctx.seed_name and ctx.seed_name.endswith(seed_end):
- # correct seed
- if memory.have_generation_info():
- log_no_spam("everything connected")
- await memory.process_ram(ram)
- ctx.process_from_game_queue()
- ctx.process_items_received()
- else: # no generation info
- if ctx.got_slot_data.is_set():
- memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id)
- ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \
- make_id_to_others(ctx.start_char)
- ctx.next_item = 0
- ctx.ap_local_count = len(ctx.checked_locations)
- else: # no slot data yet
- async_start(ctx.send_connect())
- log_no_spam("logging in to server...")
- await asyncio.wait((
- asyncio.create_task(ctx.got_slot_data.wait()),
- asyncio.create_task(ctx.exit_event.wait()),
- asyncio.create_task(asyncio.sleep(6))
- ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets
- else: # not correct seed name
- log_no_spam("incorrect seed - did you mix up roms?")
- else: # no room info
- # If we get here, it looks like `RoomInfo` packet got lost
- log_no_spam("waiting for room info from server...")
- else: # server not connected
- log_no_spam("waiting for server connection...")
- else: # new game
- log_no_spam("connected to new game")
- await ctx.disconnect()
- ctx.reset_server_state()
- ctx.seed_name = None
- ctx.got_room_info.clear()
- ctx.reset_game_state()
- memory.reset_game_state()
-
- ctx.auth = name
- ctx.known_name = name
- async_start(ctx.connect())
- await asyncio.wait((
- asyncio.create_task(ctx.got_room_info.wait()),
- asyncio.create_task(ctx.exit_event.wait()),
- asyncio.create_task(asyncio.sleep(6))
- ), return_when=asyncio.FIRST_COMPLETED)
- else: # no name found in game
- if not help_message_shown:
- logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.')
- help_message_shown = True
- log_no_spam("looking for connection to game...")
- await asyncio.sleep(0.3)
-
- await asyncio.sleep(0.09375)
- logger.info("zillion sync task ending")
-
-
-async def main() -> None:
- parser = get_base_parser()
- parser.add_argument('diff_file', default="", type=str, nargs="?",
- help='Path to a .apzl Archipelago Binary Patch file')
- # SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
- args = parser.parse_args()
- print(args)
-
- if args.diff_file:
- import Patch
- logger.info("patch file was supplied - creating sms rom...")
- meta, rom_file = Patch.create_rom_file(args.diff_file)
- if "server" in meta:
- args.connect = meta["server"]
- logger.info(f"wrote rom file to {rom_file}")
-
- ctx = ZillionContext(args.connect, args.password)
- if ctx.server_task is None:
- ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
-
- if gui_enabled:
- ctx.run_gui()
- ctx.run_cli()
-
- sync_task = asyncio.create_task(zillion_sync_task(ctx))
-
- await ctx.exit_event.wait()
-
- ctx.server_address = None
- logger.debug("waiting for sync task to end")
- await sync_task
- logger.debug("sync task ended")
- await ctx.shutdown()
+import Utils # noqa: E402
+from worlds.zillion.client import launch # noqa: E402
if __name__ == "__main__":
Utils.init_logging("ZillionClient", exception_logger="Client")
-
- colorama.init()
- asyncio.run(main())
- colorama.deinit()
+ launch()
diff --git a/_speedups.pyx b/_speedups.pyx
index 9bf25cce2984..4b083c2f9aef 100644
--- a/_speedups.pyx
+++ b/_speedups.pyx
@@ -1,5 +1,6 @@
#cython: language_level=3
-#distutils: language = c++
+#distutils: language = c
+#distutils: depends = intset.h
"""
Provides faster implementation of some core parts.
@@ -13,7 +14,6 @@ from cpython cimport PyObject
from typing import Any, Dict, Iterable, Iterator, Generator, Sequence, Tuple, TypeVar, Union, Set, List, TYPE_CHECKING
from cymem.cymem cimport Pool
from libc.stdint cimport int64_t, uint32_t
-from libcpp.set cimport set as std_set
from collections import defaultdict
cdef extern from *:
@@ -31,6 +31,27 @@ ctypedef int64_t ap_id_t
cdef ap_player_t MAX_PLAYER_ID = 1000000 # limit the size of indexing array
cdef size_t INVALID_SIZE = (-1) # this is all 0xff... adding 1 results in 0, but it's not negative
+# configure INTSET for player
+cdef extern from *:
+ """
+ #define INTSET_NAME ap_player_set
+ #define INTSET_TYPE uint32_t // has to match ap_player_t
+ """
+
+# create INTSET for player
+cdef extern from "intset.h":
+ """
+ #undef INTSET_NAME
+ #undef INTSET_TYPE
+ """
+ ctypedef struct ap_player_set:
+ pass
+
+ ap_player_set* ap_player_set_new(size_t bucket_count) nogil
+ void ap_player_set_free(ap_player_set* set) nogil
+ bint ap_player_set_add(ap_player_set* set, ap_player_t val) nogil
+ bint ap_player_set_contains(ap_player_set* set, ap_player_t val) nogil
+
cdef struct LocationEntry:
# layout is so that
@@ -48,6 +69,7 @@ cdef struct IndexEntry:
size_t count
+@cython.auto_pickle(False)
cdef class LocationStore:
"""Compact store for locations and their items in a MultiServer"""
# The original implementation uses Dict[int, Dict[int, Tuple(int, int, int]]
@@ -78,18 +100,6 @@ cdef class LocationStore:
size += sizeof(self._raw_proxies[0]) * self.sender_index_size
return size
- def __cinit__(self, locations_dict: Dict[int, Dict[int, Sequence[int]]]) -> None:
- self._mem = None
- self._keys = None
- self._items = None
- self._proxies = None
- self._len = 0
- self.entries = NULL
- self.entry_count = 0
- self.sender_index = NULL
- self.sender_index_size = 0
- self._raw_proxies = NULL
-
def __init__(self, locations_dict: Dict[int, Dict[int, Sequence[int]]]) -> None:
self._mem = Pool()
cdef object key
@@ -196,7 +206,7 @@ cdef class LocationStore:
def find_item(self, slots: Set[int], seeked_item_id: int) -> Generator[Tuple[int, int, int, int, int], None, None]:
cdef ap_id_t item = seeked_item_id
cdef ap_player_t receiver
- cdef std_set[ap_player_t] receivers
+ cdef ap_player_set* receivers
cdef size_t slot_count = len(slots)
if slot_count == 1:
# specialized implementation for single slot
@@ -208,13 +218,20 @@ cdef class LocationStore:
yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags
elif slot_count:
# generic implementation with lookup in set
- for receiver in slots:
- receivers.insert(receiver)
- with nogil:
- for entry in self.entries[:self.entry_count]:
- if entry.item == item and receivers.count(entry.receiver):
- with gil:
- yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags
+ receivers = ap_player_set_new(min(1023, slot_count)) # limit top level struct to 16KB
+ if not receivers:
+ raise MemoryError()
+ try:
+ for receiver in slots:
+ if not ap_player_set_add(receivers, receiver):
+ raise MemoryError()
+ with nogil:
+ for entry in self.entries[:self.entry_count]:
+ if entry.item == item and ap_player_set_contains(receivers, entry.receiver):
+ with gil:
+ yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags
+ finally:
+ ap_player_set_free(receivers)
def get_for_player(self, slot: int) -> Dict[int, Set[int]]:
cdef ap_player_t receiver = slot
@@ -281,6 +298,7 @@ cdef class LocationStore:
entry.location not in checked])
+@cython.auto_pickle(False)
@cython.internal # unsafe. disable direct import
cdef class PlayerLocationProxy:
cdef LocationStore _store
diff --git a/_speedups.pyxbld b/_speedups.pyxbld
index e1fe19b2efc6..974eaed03b6a 100644
--- a/_speedups.pyxbld
+++ b/_speedups.pyxbld
@@ -1,8 +1,10 @@
-# This file is required to get pyximport to work with C++.
-# Switching from std::set to a pure C implementation is still on the table to simplify everything.
+# This file is used when doing pyximport
+import os
def make_ext(modname, pyxfilename):
from distutils.extension import Extension
return Extension(name=modname,
sources=[pyxfilename],
- language='c++')
+ depends=["intset.h"],
+ include_dirs=[os.getcwd()],
+ language="c")
diff --git a/data/basepatch.bsdiff4 b/data/basepatch.bsdiff4
index a578b248f577..379eee80c6c0 100644
Binary files a/data/basepatch.bsdiff4 and b/data/basepatch.bsdiff4 differ
diff --git a/data/client.kv b/data/client.kv
index f0e36169002a..dc8a5c9c9d72 100644
--- a/data/client.kv
+++ b/data/client.kv
@@ -13,10 +13,17 @@
plum: "AF99EF" # typically progression item
salmon: "FA8072" # typically trap item
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
+ orange: "FF7700" # Used for command echo
:
color: "FFFFFF"
:
tab_width: root.width / app.tab_count
+:
+ text_size: self.width, None
+ size_hint_y: None
+ height: self.texture_size[1]
+ font_size: dp(20)
+ markup: True
:
canvas.before:
Color:
@@ -24,11 +31,6 @@
Rectangle:
size: self.size
pos: self.pos
- text_size: self.width, None
- size_hint_y: None
- height: self.texture_size[1]
- font_size: dp(20)
- markup: True
:
messages: 1000 # amount of messages stored in client logs.
cols: 1
@@ -44,6 +46,76 @@
height: self.minimum_height
orientation: 'vertical'
spacing: dp(3)
+:
+ canvas.before:
+ Color:
+ rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1) if self.striped else (0.18, 0.18, 0.18, 1)
+ Rectangle:
+ size: self.size
+ pos: self.pos
+ height: self.minimum_height
+ receiving_text: "Receiving Player"
+ item_text: "Item"
+ finding_text: "Finding Player"
+ location_text: "Location"
+ entrance_text: "Entrance"
+ found_text: "Found?"
+ TooltipLabel:
+ id: receiving
+ sort_key: 'receiving'
+ text: root.receiving_text
+ halign: 'center'
+ valign: 'center'
+ pos_hint: {"center_y": 0.5}
+ TooltipLabel:
+ id: item
+ sort_key: 'item'
+ text: root.item_text
+ halign: 'center'
+ valign: 'center'
+ pos_hint: {"center_y": 0.5}
+ TooltipLabel:
+ id: finding
+ sort_key: 'finding'
+ text: root.finding_text
+ halign: 'center'
+ valign: 'center'
+ pos_hint: {"center_y": 0.5}
+ TooltipLabel:
+ id: location
+ sort_key: 'location'
+ text: root.location_text
+ halign: 'center'
+ valign: 'center'
+ pos_hint: {"center_y": 0.5}
+ TooltipLabel:
+ id: entrance
+ sort_key: 'entrance'
+ text: root.entrance_text
+ halign: 'center'
+ valign: 'center'
+ pos_hint: {"center_y": 0.5}
+ TooltipLabel:
+ id: found
+ sort_key: 'found'
+ text: root.found_text
+ halign: 'center'
+ valign: 'center'
+ pos_hint: {"center_y": 0.5}
+:
+ cols: 1
+ viewclass: 'HintLabel'
+ scroll_y: self.height
+ scroll_type: ["content", "bars"]
+ bar_width: dp(12)
+ effect_cls: "ScrollEffect"
+ SelectableRecycleBoxLayout:
+ default_size: None, dp(20)
+ default_size_hint: 1, None
+ size_hint_y: None
+ height: self.minimum_height
+ orientation: 'vertical'
+ spacing: dp(3)
:
text: "Server:"
size_hint_x: None
diff --git a/data/lua/base64.lua b/data/lua/base64.lua
new file mode 100644
index 000000000000..ebe80643531b
--- /dev/null
+++ b/data/lua/base64.lua
@@ -0,0 +1,119 @@
+-- This file originates from this repository: https://github.com/iskolbin/lbase64
+-- It was modified to translate between base64 strings and lists of bytes instead of base64 strings and strings.
+
+local base64 = {}
+
+local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode
+if not extract then
+ if _G._VERSION == "Lua 5.4" then
+ extract = load[[return function( v, from, width )
+ return ( v >> from ) & ((1 << width) - 1)
+ end]]()
+ elseif _G.bit then -- LuaJIT
+ local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band
+ extract = function( v, from, width )
+ return band( shr( v, from ), shl( 1, width ) - 1 )
+ end
+ elseif _G._VERSION == "Lua 5.1" then
+ extract = function( v, from, width )
+ local w = 0
+ local flag = 2^from
+ for i = 0, width-1 do
+ local flag2 = flag + flag
+ if v % flag2 >= flag then
+ w = w + 2^i
+ end
+ flag = flag2
+ end
+ return w
+ end
+ end
+end
+
+
+function base64.makeencoder( s62, s63, spad )
+ local encoder = {}
+ for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J',
+ 'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y',
+ 'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n',
+ 'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2',
+ '3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do
+ encoder[b64code] = char:byte()
+ end
+ return encoder
+end
+
+function base64.makedecoder( s62, s63, spad )
+ local decoder = {}
+ for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do
+ decoder[charcode] = b64code
+ end
+ return decoder
+end
+
+local DEFAULT_ENCODER = base64.makeencoder()
+local DEFAULT_DECODER = base64.makedecoder()
+
+local char, concat = string.char, table.concat
+
+function base64.encode( arr, encoder )
+ encoder = encoder or DEFAULT_ENCODER
+ local t, k, n = {}, 1, #arr
+ local lastn = n % 3
+ for i = 1, n-lastn, 3 do
+ local a, b, c = arr[i], arr[i + 1], arr[i + 2]
+ local v = a*0x10000 + b*0x100 + c
+ local s
+ s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
+ t[k] = s
+ k = k + 1
+ end
+ if lastn == 2 then
+ local a, b = arr[n-1], arr[n]
+ local v = a*0x10000 + b*0x100
+ t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64])
+ elseif lastn == 1 then
+ local v = arr[n]*0x10000
+ t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64])
+ end
+ return concat( t )
+end
+
+function base64.decode( b64, decoder )
+ decoder = decoder or DEFAULT_DECODER
+ local pattern = '[^%w%+%/%=]'
+ if decoder then
+ local s62, s63
+ for charcode, b64code in pairs( decoder ) do
+ if b64code == 62 then s62 = charcode
+ elseif b64code == 63 then s63 = charcode
+ end
+ end
+ pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) )
+ end
+ b64 = b64:gsub( pattern, '' )
+ local t, k = {}, 1
+ local n = #b64
+ local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0
+ for i = 1, padding > 0 and n-4 or n, 4 do
+ local a, b, c, d = b64:byte( i, i+3 )
+ local s
+ local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
+ table.insert(t,extract(v,16,8))
+ table.insert(t,extract(v,8,8))
+ table.insert(t,extract(v,0,8))
+ end
+ if padding == 1 then
+ local a, b, c = b64:byte( n-3, n-1 )
+ local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40
+ table.insert(t,extract(v,16,8))
+ table.insert(t,extract(v,8,8))
+ elseif padding == 2 then
+ local a, b = b64:byte( n-3, n-2 )
+ local v = decoder[a]*0x40000 + decoder[b]*0x1000
+ table.insert(t,extract(v,16,8))
+ end
+ return t
+end
+
+return base64
diff --git a/data/lua/connector_bizhawk_generic.lua b/data/lua/connector_bizhawk_generic.lua
new file mode 100644
index 000000000000..00021b241f9a
--- /dev/null
+++ b/data/lua/connector_bizhawk_generic.lua
@@ -0,0 +1,659 @@
+--[[
+Copyright (c) 2023 Zunawe
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+]]
+
+local SCRIPT_VERSION = 1
+
+-- Set to log incoming requests
+-- Will cause lag due to large console output
+local DEBUG = false
+
+--[[
+This script expects to receive JSON and will send JSON back. A message should
+be a list of 1 or more requests which will be executed in order. Each request
+will have a corresponding response in the same order.
+
+Every individual request and response is a JSON object with at minimum one
+field `type`. The value of `type` determines what other fields may exist.
+
+To get the script version, instead of JSON, send "VERSION" to get the script
+version directly (e.g. "2").
+
+#### Ex. 1
+
+Request: `[{"type": "PING"}]`
+
+Response: `[{"type": "PONG"}]`
+
+---
+
+#### Ex. 2
+
+Request: `[{"type": "LOCK"}, {"type": "HASH"}]`
+
+Response: `[{"type": "LOCKED"}, {"type": "HASH_RESPONSE", "value": "F7D18982"}]`
+
+---
+
+#### Ex. 3
+
+Request:
+
+```json
+[
+ {"type": "GUARD", "address": 100, "expected_data": "aGVsbG8=", "domain": "System Bus"},
+ {"type": "READ", "address": 500, "size": 4, "domain": "ROM"}
+]
+```
+
+Response:
+
+```json
+[
+ {"type": "GUARD_RESPONSE", "address": 100, "value": true},
+ {"type": "READ_RESPONSE", "value": "dGVzdA=="}
+]
+```
+
+---
+
+#### Ex. 4
+
+Request:
+
+```json
+[
+ {"type": "GUARD", "address": 100, "expected_data": "aGVsbG8=", "domain": "System Bus"},
+ {"type": "READ", "address": 500, "size": 4, "domain": "ROM"}
+]
+```
+
+Response:
+
+```json
+[
+ {"type": "GUARD_RESPONSE", "address": 100, "value": false},
+ {"type": "GUARD_RESPONSE", "address": 100, "value": false}
+]
+```
+
+---
+
+### Supported Request Types
+
+- `PING`
+ Does nothing; resets timeout.
+
+ Expected Response Type: `PONG`
+
+- `SYSTEM`
+ Returns the system of the currently loaded ROM (N64, GBA, etc...).
+
+ Expected Response Type: `SYSTEM_RESPONSE`
+
+- `PREFERRED_CORES`
+ Returns the user's default cores for systems with multiple cores. If the
+ current ROM's system has multiple cores, the one that is currently
+ running is very probably the preferred core.
+
+ Expected Response Type: `PREFERRED_CORES_RESPONSE`
+
+- `HASH`
+ Returns the hash of the currently loaded ROM calculated by BizHawk.
+
+ Expected Response Type: `HASH_RESPONSE`
+
+- `GUARD`
+ Checks a section of memory against `expected_data`. If the bytes starting
+ at `address` do not match `expected_data`, the response will have `value`
+ set to `false`, and all subsequent requests will not be executed and
+ receive the same `GUARD_RESPONSE`.
+
+ Expected Response Type: `GUARD_RESPONSE`
+
+ Additional Fields:
+ - `address` (`int`): The address of the memory to check
+ - `expected_data` (string): A base64 string of contiguous data
+ - `domain` (`string`): The name of the memory domain the address
+ corresponds to
+
+- `LOCK`
+ Halts emulation and blocks on incoming requests until an `UNLOCK` request
+ is received or the client times out. All requests processed while locked
+ will happen on the same frame.
+
+ Expected Response Type: `LOCKED`
+
+- `UNLOCK`
+ Resumes emulation after the current list of requests is done being
+ executed.
+
+ Expected Response Type: `UNLOCKED`
+
+- `READ`
+ Reads an array of bytes at the provided address.
+
+ Expected Response Type: `READ_RESPONSE`
+
+ Additional Fields:
+ - `address` (`int`): The address of the memory to read
+ - `size` (`int`): The number of bytes to read
+ - `domain` (`string`): The name of the memory domain the address
+ corresponds to
+
+- `WRITE`
+ Writes an array of bytes to the provided address.
+
+ Expected Response Type: `WRITE_RESPONSE`
+
+ Additional Fields:
+ - `address` (`int`): The address of the memory to write to
+ - `value` (`string`): A base64 string representing the data to write
+ - `domain` (`string`): The name of the memory domain the address
+ corresponds to
+
+- `DISPLAY_MESSAGE`
+ Adds a message to the message queue which will be displayed using
+ `gui.addmessage` according to the message interval.
+
+ Expected Response Type: `DISPLAY_MESSAGE_RESPONSE`
+
+ Additional Fields:
+ - `message` (`string`): The string to display
+
+- `SET_MESSAGE_INTERVAL`
+ Sets the minimum amount of time to wait between displaying messages.
+ Potentially useful if you add many messages quickly but want players
+ to be able to read each of them.
+
+ Expected Response Type: `SET_MESSAGE_INTERVAL_RESPONSE`
+
+ Additional Fields:
+ - `value` (`number`): The number of seconds to set the interval to
+
+
+### Response Types
+
+- `PONG`
+ Acknowledges `PING`.
+
+- `SYSTEM_RESPONSE`
+ Contains the name of the system for currently running ROM.
+
+ Additional Fields:
+ - `value` (`string`): The returned system name
+
+- `PREFERRED_CORES_RESPONSE`
+ Contains the user's preferred cores for systems with multiple supported
+ cores. Currently includes NES, SNES, GB, GBC, DGB, SGB, PCE, PCECD, and
+ SGX.
+
+ Additional Fields:
+ - `value` (`{[string]: [string]}`): A dictionary map from system name to
+ core name
+
+- `HASH_RESPONSE`
+ Contains the hash of the currently loaded ROM calculated by BizHawk.
+
+ Additional Fields:
+ - `value` (`string`): The returned hash
+
+- `GUARD_RESPONSE`
+ The result of an attempted `GUARD` request.
+
+ Additional Fields:
+ - `value` (`boolean`): true if the memory was validated, false if not
+ - `address` (`int`): The address of the memory that was invalid (the same
+ address provided by the `GUARD`, not the address of the individual invalid
+ byte)
+
+- `LOCKED`
+ Acknowledges `LOCK`.
+
+- `UNLOCKED`
+ Acknowledges `UNLOCK`.
+
+- `READ_RESPONSE`
+ Contains the result of a `READ` request.
+
+ Additional Fields:
+ - `value` (`string`): A base64 string representing the read data
+
+- `WRITE_RESPONSE`
+ Acknowledges `WRITE`.
+
+- `DISPLAY_MESSAGE_RESPONSE`
+ Acknowledges `DISPLAY_MESSAGE`.
+
+- `SET_MESSAGE_INTERVAL_RESPONSE`
+ Acknowledges `SET_MESSAGE_INTERVAL`.
+
+- `ERROR`
+ Signifies that something has gone wrong while processing a request.
+
+ Additional Fields:
+ - `err` (`string`): A description of the problem
+]]
+
+local bizhawk_version = client.getversion()
+local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)")
+bizhawk_major = tonumber(bizhawk_major)
+bizhawk_minor = tonumber(bizhawk_minor)
+if bizhawk_patch == "" then
+ bizhawk_patch = 0
+else
+ bizhawk_patch = tonumber(bizhawk_patch)
+end
+
+local lua_major, lua_minor = _VERSION:match("Lua (%d+)%.(%d+)")
+lua_major = tonumber(lua_major)
+lua_minor = tonumber(lua_minor)
+
+if lua_major > 5 or (lua_major == 5 and lua_minor >= 3) then
+ require("lua_5_3_compat")
+end
+
+local base64 = require("base64")
+local socket = require("socket")
+local json = require("json")
+
+local SOCKET_PORT_FIRST = 43055
+local SOCKET_PORT_RANGE_SIZE = 5
+local SOCKET_PORT_LAST = SOCKET_PORT_FIRST + SOCKET_PORT_RANGE_SIZE
+
+local STATE_NOT_CONNECTED = 0
+local STATE_CONNECTED = 1
+
+local server = nil
+local client_socket = nil
+
+local current_state = STATE_NOT_CONNECTED
+
+local timeout_timer = 0
+local message_timer = 0
+local message_interval = 0
+local prev_time = 0
+local current_time = 0
+
+local locked = false
+
+local rom_hash = nil
+
+function queue_push (self, value)
+ self[self.right] = value
+ self.right = self.right + 1
+end
+
+function queue_is_empty (self)
+ return self.right == self.left
+end
+
+function queue_shift (self)
+ value = self[self.left]
+ self[self.left] = nil
+ self.left = self.left + 1
+ return value
+end
+
+function new_queue ()
+ local queue = {left = 1, right = 1}
+ return setmetatable(queue, {__index = {is_empty = queue_is_empty, push = queue_push, shift = queue_shift}})
+end
+
+local message_queue = new_queue()
+
+function lock ()
+ locked = true
+ client_socket:settimeout(2)
+end
+
+function unlock ()
+ locked = false
+ client_socket:settimeout(0)
+end
+
+request_handlers = {
+ ["PING"] = function (req)
+ local res = {}
+
+ res["type"] = "PONG"
+
+ return res
+ end,
+
+ ["SYSTEM"] = function (req)
+ local res = {}
+
+ res["type"] = "SYSTEM_RESPONSE"
+ res["value"] = emu.getsystemid()
+
+ return res
+ end,
+
+ ["PREFERRED_CORES"] = function (req)
+ local res = {}
+ local preferred_cores = client.getconfig().PreferredCores
+
+ res["type"] = "PREFERRED_CORES_RESPONSE"
+ res["value"] = {}
+ res["value"]["NES"] = preferred_cores.NES
+ res["value"]["SNES"] = preferred_cores.SNES
+ res["value"]["GB"] = preferred_cores.GB
+ res["value"]["GBC"] = preferred_cores.GBC
+ res["value"]["DGB"] = preferred_cores.DGB
+ res["value"]["SGB"] = preferred_cores.SGB
+ res["value"]["PCE"] = preferred_cores.PCE
+ res["value"]["PCECD"] = preferred_cores.PCECD
+ res["value"]["SGX"] = preferred_cores.SGX
+
+ return res
+ end,
+
+ ["HASH"] = function (req)
+ local res = {}
+
+ res["type"] = "HASH_RESPONSE"
+ res["value"] = rom_hash
+
+ return res
+ end,
+
+ ["GUARD"] = function (req)
+ local res = {}
+ local expected_data = base64.decode(req["expected_data"])
+ local actual_data = memory.read_bytes_as_array(req["address"], #expected_data, req["domain"])
+
+ local data_is_validated = true
+ for i, byte in ipairs(actual_data) do
+ if byte ~= expected_data[i] then
+ data_is_validated = false
+ break
+ end
+ end
+
+ res["type"] = "GUARD_RESPONSE"
+ res["value"] = data_is_validated
+ res["address"] = req["address"]
+
+ return res
+ end,
+
+ ["LOCK"] = function (req)
+ local res = {}
+
+ res["type"] = "LOCKED"
+ lock()
+
+ return res
+ end,
+
+ ["UNLOCK"] = function (req)
+ local res = {}
+
+ res["type"] = "UNLOCKED"
+ unlock()
+
+ return res
+ end,
+
+ ["READ"] = function (req)
+ local res = {}
+
+ res["type"] = "READ_RESPONSE"
+ res["value"] = base64.encode(memory.read_bytes_as_array(req["address"], req["size"], req["domain"]))
+
+ return res
+ end,
+
+ ["WRITE"] = function (req)
+ local res = {}
+
+ res["type"] = "WRITE_RESPONSE"
+ memory.write_bytes_as_array(req["address"], base64.decode(req["value"]), req["domain"])
+
+ return res
+ end,
+
+ ["DISPLAY_MESSAGE"] = function (req)
+ local res = {}
+
+ res["type"] = "DISPLAY_MESSAGE_RESPONSE"
+ message_queue:push(req["message"])
+
+ return res
+ end,
+
+ ["SET_MESSAGE_INTERVAL"] = function (req)
+ local res = {}
+
+ res["type"] = "SET_MESSAGE_INTERVAL_RESPONSE"
+ message_interval = req["value"]
+
+ return res
+ end,
+
+ ["default"] = function (req)
+ local res = {}
+
+ res["type"] = "ERROR"
+ res["err"] = "Unknown command: "..req["type"]
+
+ return res
+ end,
+}
+
+function process_request (req)
+ if request_handlers[req["type"]] then
+ return request_handlers[req["type"]](req)
+ else
+ return request_handlers["default"](req)
+ end
+end
+
+-- Receive data from AP client and send message back
+function send_receive ()
+ local message, err = client_socket:receive()
+
+ -- Handle errors
+ if err == "closed" then
+ if current_state == STATE_CONNECTED then
+ print("Connection to client closed")
+ end
+ current_state = STATE_NOT_CONNECTED
+ return
+ elseif err == "timeout" then
+ unlock()
+ return
+ elseif err ~= nil then
+ print(err)
+ current_state = STATE_NOT_CONNECTED
+ unlock()
+ return
+ end
+
+ -- Reset timeout timer
+ timeout_timer = 5
+
+ -- Process received data
+ if DEBUG then
+ print("Received Message ["..emu.framecount().."]: "..'"'..message..'"')
+ end
+
+ if message == "VERSION" then
+ client_socket:send(tostring(SCRIPT_VERSION).."\n")
+ else
+ local res = {}
+ local data = json.decode(message)
+ local failed_guard_response = nil
+ for i, req in ipairs(data) do
+ if failed_guard_response ~= nil then
+ res[i] = failed_guard_response
+ else
+ -- An error is more likely to cause an NLua exception than to return an error here
+ local status, response = pcall(process_request, req)
+ if status then
+ res[i] = response
+
+ -- If the GUARD validation failed, skip the remaining commands
+ if response["type"] == "GUARD_RESPONSE" and not response["value"] then
+ failed_guard_response = response
+ end
+ else
+ if type(response) ~= "string" then response = "Unknown error" end
+ res[i] = {type = "ERROR", err = response}
+ end
+ end
+ end
+
+ client_socket:send(json.encode(res).."\n")
+ end
+end
+
+function initialize_server ()
+ local err
+ local port = SOCKET_PORT_FIRST
+ local res = nil
+
+ server, err = socket.socket.tcp4()
+ while res == nil and port <= SOCKET_PORT_LAST do
+ res, err = server:bind("localhost", port)
+ if res == nil and err ~= "address already in use" then
+ print(err)
+ return
+ end
+
+ if res == nil then
+ port = port + 1
+ end
+ end
+
+ if port > SOCKET_PORT_LAST then
+ print("Too many instances of connector script already running. Exiting.")
+ return
+ end
+
+ res, err = server:listen(0)
+
+ if err ~= nil then
+ print(err)
+ return
+ end
+
+ server:settimeout(0)
+end
+
+function main ()
+ while true do
+ if server == nil then
+ initialize_server()
+ end
+
+ current_time = socket.socket.gettime()
+ timeout_timer = timeout_timer - (current_time - prev_time)
+ message_timer = message_timer - (current_time - prev_time)
+ prev_time = current_time
+
+ if message_timer <= 0 and not message_queue:is_empty() then
+ gui.addmessage(message_queue:shift())
+ message_timer = message_interval
+ end
+
+ if current_state == STATE_NOT_CONNECTED then
+ if emu.framecount() % 30 == 0 then
+ print("Looking for client...")
+ local client, timeout = server:accept()
+ if timeout == nil then
+ print("Client connected")
+ current_state = STATE_CONNECTED
+ client_socket = client
+ server:close()
+ server = nil
+ client_socket:settimeout(0)
+ end
+ end
+ else
+ repeat
+ send_receive()
+ until not locked
+
+ if timeout_timer <= 0 then
+ print("Client timed out")
+ current_state = STATE_NOT_CONNECTED
+ end
+ end
+
+ coroutine.yield()
+ end
+end
+
+event.onexit(function ()
+ print("\n-- Restarting Script --\n")
+ if server ~= nil then
+ server:close()
+ end
+end)
+
+if bizhawk_major < 2 or (bizhawk_major == 2 and bizhawk_minor < 7) then
+ print("Must use BizHawk 2.7.0 or newer")
+elseif bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor > 9) then
+ print("Warning: This version of BizHawk is newer than this script. If it doesn't work, consider downgrading to 2.9.")
+else
+ if emu.getsystemid() == "NULL" then
+ print("No ROM is loaded. Please load a ROM.")
+ while emu.getsystemid() == "NULL" do
+ emu.frameadvance()
+ end
+ end
+
+ rom_hash = gameinfo.getromhash()
+
+ print("Waiting for client to connect. This may take longer the more instances of this script you have open at once.\n")
+
+ local co = coroutine.create(main)
+ function tick ()
+ local status, err = coroutine.resume(co)
+
+ if not status and err ~= "cannot resume dead coroutine" then
+ print("\nERROR: "..err)
+ print("Consider reporting this crash.\n")
+
+ if server ~= nil then
+ server:close()
+ end
+
+ co = coroutine.create(main)
+ end
+ end
+
+ -- Gambatte has a setting which can cause script execution to become
+ -- misaligned, so for GB and GBC we explicitly set the callback on
+ -- vblank instead.
+ -- https://github.com/TASEmulators/BizHawk/issues/3711
+ if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" or emu.getsystemid() == "SGB" then
+ event.onmemoryexecute(tick, 0x40, "tick", "System Bus")
+ else
+ event.onframeend(tick)
+ end
+
+ while true do
+ emu.frameadvance()
+ end
+end
diff --git a/data/lua/connector_ff1.lua b/data/lua/connector_ff1.lua
index 455b046961f9..afae5d3c81dc 100644
--- a/data/lua/connector_ff1.lua
+++ b/data/lua/connector_ff1.lua
@@ -322,7 +322,7 @@ function processBlock(block)
end
end
end
- if #itemsBlock ~= itemIndex then
+ if #itemsBlock > itemIndex then
wU8(ITEM_INDEX, #itemsBlock)
end
diff --git a/data/lua/connector_mmbn3.lua b/data/lua/connector_mmbn3.lua
index 8482bf85b1a8..fce38a4c1102 100644
--- a/data/lua/connector_mmbn3.lua
+++ b/data/lua/connector_mmbn3.lua
@@ -27,14 +27,9 @@ local mmbn3Socket = nil
local frame = 0
-- States
-local ITEMSTATE_NONINITIALIZED = "Game Not Yet Started" -- Game has not yet started
local ITEMSTATE_NONITEM = "Non-Itemable State" -- Do not send item now. RAM is not capable of holding
local ITEMSTATE_IDLE = "Item State Ready" -- Ready for the next item if there are any
-local ITEMSTATE_SENT = "Item Sent Not Claimed" -- The ItemBit is set, but the dialog has not been closed yet
-local itemState = ITEMSTATE_NONINITIALIZED
-
-local itemQueued = nil
-local itemQueueCounter = 120
+local itemState = ITEMSTATE_NONITEM
local debugEnabled = false
local game_complete = false
@@ -104,21 +99,24 @@ end
local IsInBattle = function()
return memory.read_u8(0x020097F8) == 0x08
end
-local IsItemQueued = function()
- return memory.read_u8(0x2000224) == 0x01
-end
-
-- This function actually determines when you're on ANY full-screen menu (navi cust, link battle, etc.) but we
-- don't want to check any locations there either so it's fine.
local IsOnTitle = function()
return bit.band(memory.read_u8(0x020097F8),0x04) == 0
end
+
local IsItemable = function()
- return not IsInMenu() and not IsInTransition() and not IsInDialog() and not IsInBattle() and not IsOnTitle() and not IsItemQueued()
+ return not IsInMenu() and not IsInTransition() and not IsInDialog() and not IsInBattle() and not IsOnTitle()
end
local is_game_complete = function()
- if IsOnTitle() or itemState == ITEMSTATE_NONINITIALIZED then return game_complete end
+ -- If the Cannary Byte is 0xFF, then the save RAM is untrustworthy
+ if memory.read_u8(canary_byte) == 0xFF then
+ return game_complete
+ end
+
+ -- If on the title screen don't read RAM, RAM can't be trusted yet
+ if IsOnTitle() then return game_complete end
-- If the game is already marked complete, do not read memory
if game_complete then return true end
@@ -177,14 +175,6 @@ local Check_Progressive_Undernet_ID = function()
end
return 9
end
-local GenerateTextBytes = function(message)
- bytes = {}
- for i = 1, #message do
- local c = message:sub(i,i)
- table.insert(bytes, charDict[c])
- end
- return bytes
-end
-- Item Message Generation functions
local Next_Progressive_Undernet_ID = function(index)
@@ -196,150 +186,6 @@ local Next_Progressive_Undernet_ID = function(index)
item_index=ordered_IDs[index]
return item_index
end
-local Extra_Progressive_Undernet = function()
- fragBytes = int32ToByteList_le(20)
- bytes = {
- 0xF6, 0x50, fragBytes[1], fragBytes[2], fragBytes[3], fragBytes[4], 0xFF, 0xFF, 0xFF
- }
- bytes = TableConcat(bytes, GenerateTextBytes("The extra data\ndecompiles into:\n\"20 BugFrags\"!!"))
- return bytes
-end
-
-local GenerateChipGet = function(chip, code, amt)
- chipBytes = int16ToByteList_le(chip)
- bytes = {
- 0xF6, 0x10, chipBytes[1], chipBytes[2], code, amt,
- charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict[' '], charDict['c'], charDict['h'], charDict['i'], charDict['p'], charDict[' '], charDict['f'], charDict['o'], charDict['r'], charDict['\n'],
-
- }
- if chip < 256 then
- bytes = TableConcat(bytes, {
- charDict['\"'], 0xF9,0x00,chipBytes[1],0x01,0x00,0xF9,0x00,code,0x03, charDict['\"'],charDict['!'],charDict['!']
- })
- else
- bytes = TableConcat(bytes, {
- charDict['\"'], 0xF9,0x00,chipBytes[1],0x02,0x00,0xF9,0x00,code,0x03, charDict['\"'],charDict['!'],charDict['!']
- })
- end
- return bytes
-end
-local GenerateKeyItemGet = function(item, amt)
- bytes = {
- 0xF6, 0x00, item, amt,
- charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'],
- charDict['\"'], 0xF9, 0x00, item, 0x00, charDict['\"'],charDict['!'],charDict['!']
- }
- return bytes
-end
-local GenerateSubChipGet = function(subchip, amt)
- -- SubChips have an extra bit of trouble. If you have too many, they're supposed to skip to another text bank that doesn't give you the item
- -- Instead, I'm going to just let it get eaten
- bytes = {
- 0xF6, 0x20, subchip, amt, 0xFF, 0xFF, 0xFF,
- charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'],
- charDict['S'], charDict['u'], charDict['b'], charDict['C'], charDict['h'], charDict['i'], charDict['p'], charDict[' '], charDict['f'], charDict['o'], charDict['r'], charDict['\n'],
- charDict['\"'], 0xF9, 0x00, subchip, 0x00, charDict['\"'],charDict['!'],charDict['!']
- }
- return bytes
-end
-local GenerateZennyGet = function(amt)
- zennyBytes = int32ToByteList_le(amt)
- bytes = {
- 0xF6, 0x30, zennyBytes[1], zennyBytes[2], zennyBytes[3], zennyBytes[4], 0xFF, 0xFF, 0xFF,
- charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'], charDict['\"']
- }
- -- The text needs to be added one char at a time, so we need to convert the number to a string then iterate through it
- zennyStr = tostring(amt)
- for i = 1, #zennyStr do
- local c = zennyStr:sub(i,i)
- table.insert(bytes, charDict[c])
- end
- bytes = TableConcat(bytes, {
- charDict[' '], charDict['Z'], charDict['e'], charDict['n'], charDict['n'], charDict['y'], charDict['s'], charDict['\"'],charDict['!'],charDict['!']
- })
- return bytes
-end
-local GenerateProgramGet = function(program, color, amt)
- bytes = {
- 0xF6, 0x40, (program * 4), amt, color,
- charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict[' '], charDict['N'], charDict['a'], charDict['v'], charDict['i'], charDict['\n'],
- charDict['C'], charDict['u'], charDict['s'], charDict['t'], charDict['o'], charDict['m'], charDict['i'], charDict['z'], charDict['e'], charDict['r'], charDict[' '], charDict['P'], charDict['r'], charDict['o'], charDict['g'], charDict['r'], charDict['a'], charDict['m'], charDict[':'], charDict['\n'],
- charDict['\"'], 0xF9, 0x00, program, 0x05, charDict['\"'],charDict['!'],charDict['!']
- }
-
- return bytes
-end
-local GenerateBugfragGet = function(amt)
- fragBytes = int32ToByteList_le(amt)
- bytes = {
- 0xF6, 0x50, fragBytes[1], fragBytes[2], fragBytes[3], fragBytes[4], 0xFF, 0xFF, 0xFF,
- charDict['G'], charDict['o'], charDict['t'], charDict[':'], charDict['\n'], charDict['\"']
- }
- -- The text needs to be added one char at a time, so we need to convert the number to a string then iterate through it
- bugFragStr = tostring(amt)
- for i = 1, #bugFragStr do
- local c = bugFragStr:sub(i,i)
- table.insert(bytes, charDict[c])
- end
- bytes = TableConcat(bytes, {
- charDict[' '], charDict['B'], charDict['u'], charDict['g'], charDict['F'], charDict['r'], charDict['a'], charDict['g'], charDict['s'], charDict['\"'],charDict['!'],charDict['!']
- })
- return bytes
-end
-local GenerateGetMessageFromItem = function(item)
- --Special case for progressive undernet
- if item["type"] == "undernet" then
- undernet_id = Check_Progressive_Undernet_ID()
- if undernet_id > 8 then
- return Extra_Progressive_Undernet()
- end
- return GenerateKeyItemGet(Next_Progressive_Undernet_ID(undernet_id),1)
- elseif item["type"] == "chip" then
- return GenerateChipGet(item["itemID"], item["subItemID"], item["count"])
- elseif item["type"] == "key" then
- return GenerateKeyItemGet(item["itemID"], item["count"])
- elseif item["type"] == "subchip" then
- return GenerateSubChipGet(item["itemID"], item["count"])
- elseif item["type"] == "zenny" then
- return GenerateZennyGet(item["count"])
- elseif item["type"] == "program" then
- return GenerateProgramGet(item["itemID"], item["subItemID"], item["count"])
- elseif item["type"] == "bugfrag" then
- return GenerateBugfragGet(item["count"])
- end
-
- return GenerateTextBytes("Empty Message")
-end
-
-local GetMessage = function(item)
- startBytes = {0x02, 0x00}
- playerLockBytes = {0xF8,0x00, 0xF8, 0x10}
- msgOpenBytes = {0xF1, 0x02}
- textBytes = GenerateTextBytes("Receiving\ndata from\n"..item["sender"]..".")
- dotdotWaitBytes = {0xEA,0x00,0x0A,0x00,0x4D,0xEA,0x00,0x0A,0x00,0x4D}
- continueBytes = {0xEB, 0xE9}
- -- continueBytes = {0xE9}
- playReceiveAnimationBytes = {0xF8,0x04,0x18}
- chipGiveBytes = GenerateGetMessageFromItem(item)
- playerFinishBytes = {0xF8, 0x0C}
- playerUnlockBytes={0xEB, 0xF8, 0x08}
- -- playerUnlockBytes={0xF8, 0x08}
- endMessageBytes = {0xF8, 0x10, 0xE7}
-
- bytes = {}
- bytes = TableConcat(bytes,startBytes)
- bytes = TableConcat(bytes,playerLockBytes)
- bytes = TableConcat(bytes,msgOpenBytes)
- bytes = TableConcat(bytes,textBytes)
- bytes = TableConcat(bytes,dotdotWaitBytes)
- bytes = TableConcat(bytes,continueBytes)
- bytes = TableConcat(bytes,playReceiveAnimationBytes)
- bytes = TableConcat(bytes,chipGiveBytes)
- bytes = TableConcat(bytes,playerFinishBytes)
- bytes = TableConcat(bytes,playerUnlockBytes)
- bytes = TableConcat(bytes,endMessageBytes)
- return bytes
-end
local getChipCodeIndex = function(chip_id, chip_code)
chipCodeArrayStartAddress = 0x8011510 + (0x20 * chip_id)
@@ -353,6 +199,10 @@ local getChipCodeIndex = function(chip_id, chip_code)
end
local getProgramColorIndex = function(program_id, program_color)
+ -- For whatever reason, OilBody (ID 24) does not follow the rules and should be color index 3
+ if program_id == 24 then
+ return 3
+ end
-- The general case, most programs use white pink or yellow. This is the values the enums already have
if program_id >= 20 and program_id <= 47 then
return program_color-1
@@ -401,11 +251,11 @@ local changeZenny = function(val)
return 0
end
if memory.read_u32_le(0x20018F4) <= math.abs(tonumber(val)) and tonumber(val) < 0 then
- memory.write_u32_le(0x20018f4, 0)
+ memory.write_u32_le(0x20018F4, 0)
val = 0
return "empty"
end
- memory.write_u32_le(0x20018f4, memory.read_u32_le(0x20018F4) + tonumber(val))
+ memory.write_u32_le(0x20018F4, memory.read_u32_le(0x20018F4) + tonumber(val))
if memory.read_u32_le(0x20018F4) > 999999 then
memory.write_u32_le(0x20018F4, 999999)
end
@@ -417,30 +267,17 @@ local changeFrags = function(val)
return 0
end
if memory.read_u16_le(0x20018F8) <= math.abs(tonumber(val)) and tonumber(val) < 0 then
- memory.write_u16_le(0x20018f8, 0)
+ memory.write_u16_le(0x20018F8, 0)
val = 0
return "empty"
end
- memory.write_u16_le(0x20018f8, memory.read_u16_le(0x20018F8) + tonumber(val))
+ memory.write_u16_le(0x20018F8, memory.read_u16_le(0x20018F8) + tonumber(val))
if memory.read_u16_le(0x20018F8) > 9999 then
memory.write_u16_le(0x20018F8, 9999)
end
return val
end
--- Fix Health Pools
-local fix_hp = function()
- -- Current Health fix
- if IsInBattle() and not (memory.read_u16_le(0x20018A0) == memory.read_u16_le(0x2037294)) then
- memory.write_u16_le(0x20018A0, memory.read_u16_le(0x2037294))
- end
-
- -- Max Health Fix
- if IsInBattle() and not (memory.read_u16_le(0x20018A2) == memory.read_u16_le(0x2037296)) then
- memory.write_u16_le(0x20018A2, memory.read_u16_le(0x2037296))
- end
-end
-
local changeRegMemory = function(amt)
regMemoryAddress = 0x02001897
currentRegMem = memory.read_u8(regMemoryAddress)
@@ -448,34 +285,18 @@ local changeRegMemory = function(amt)
end
local changeMaxHealth = function(val)
- fix_hp()
- if val == nil then
- fix_hp()
+ if val == nil then
return 0
end
- if math.abs(tonumber(val)) >= memory.read_u16_le(0x20018A2) and tonumber(val) < 0 then
- memory.write_u16_le(0x20018A2, 0)
- if IsInBattle() then
- memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2))
- if memory.read_u16_le(0x2037296) >= memory.read_u16_le(0x20018A2) then
- memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2))
- end
- end
- fix_hp()
- return "lethal"
- end
+
memory.write_u16_le(0x20018A2, memory.read_u16_le(0x20018A2) + tonumber(val))
if memory.read_u16_le(0x20018A2) > 9999 then
memory.write_u16_le(0x20018A2, 9999)
end
- if IsInBattle() then
- memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2))
- end
- fix_hp()
return val
end
-local SendItem = function(item)
+local SendItemToGame = function(item)
if item["type"] == "undernet" then
undernet_id = Check_Progressive_Undernet_ID()
if undernet_id > 8 then
@@ -553,13 +374,6 @@ local OpenShortcuts = function()
end
end
-local RestoreItemRam = function()
- if backup_bytes ~= nil then
- memory.write_bytes_as_array(0x203fe10, backup_bytes)
- end
- backup_bytes = nil
-end
-
local process_block = function(block)
-- Sometimes the block is nothing, if this is the case then quietly stop processing
if block == nil then
@@ -574,14 +388,7 @@ local process_block = function(block)
end
local itemStateMachineProcess = function()
- if itemState == ITEMSTATE_NONINITIALIZED then
- itemQueueCounter = 120
- -- Only exit this state the first time a dialog window pops up. This way we know for sure that we're ready to receive
- if not IsInMenu() and (IsInDialog() or IsInTransition()) then
- itemState = ITEMSTATE_NONITEM
- end
- elseif itemState == ITEMSTATE_NONITEM then
- itemQueueCounter = 120
+ if itemState == ITEMSTATE_NONITEM then
-- Always attempt to restore the previously stored memory in this state
-- Exit this state whenever the game is in an itemable status
if IsItemable() then
@@ -592,26 +399,11 @@ local itemStateMachineProcess = function()
if not IsItemable() then
itemState = ITEMSTATE_NONITEM
end
- if itemQueueCounter == 0 then
- if #itemsReceived > loadItemIndexFromRAM() and not IsItemQueued() then
- itemQueued = itemsReceived[loadItemIndexFromRAM()+1]
- SendItem(itemQueued)
- itemState = ITEMSTATE_SENT
- end
- else
- itemQueueCounter = itemQueueCounter - 1
- end
- elseif itemState == ITEMSTATE_SENT then
- -- Once the item is sent, wait for the dialog to close. Then clear the item bit and be ready for the next item.
- if IsInTransition() or IsInMenu() or IsOnTitle() then
- itemState = ITEMSTATE_NONITEM
- itemQueued = nil
- RestoreItemRam()
- elseif not IsInDialog() then
- itemState = ITEMSTATE_IDLE
+ if #itemsReceived > loadItemIndexFromRAM() then
+ itemQueued = itemsReceived[loadItemIndexFromRAM()+1]
+ SendItemToGame(itemQueued)
saveItemIndexToRAM(itemQueued["itemIndex"])
- itemQueued = nil
- RestoreItemRam()
+ itemState = ITEMSTATE_NONITEM
end
end
end
@@ -702,18 +494,8 @@ function main()
-- Handle the debug data display
gui.cleartext()
if debugEnabled then
- -- gui.text(0,0,"Item Queued: "..tostring(IsItemQueued()))
- -- gui.text(0,16,"In Battle: "..tostring(IsInBattle()))
- -- gui.text(0,32,"In Dialog: "..tostring(IsInDialog()))
- -- gui.text(0,48,"In Menu: "..tostring(IsInMenu()))
- gui.text(0,48,"Item Wait Time: "..tostring(itemQueueCounter))
- gui.text(0,64,itemState)
- if itemQueued == nil then
- gui.text(0,80,"No item queued")
- else
- gui.text(0,80,itemQueued["type"].." "..itemQueued["itemID"])
- end
- gui.text(0,96,"Item Index: "..loadItemIndexFromRAM())
+ gui.text(0,0,itemState)
+ gui.text(0,16,"Item Index: "..loadItemIndexFromRAM())
end
emu.frameadvance()
diff --git a/data/lua/connector_pkmn_rb.lua b/data/lua/connector_pkmn_rb.lua
deleted file mode 100644
index 3f56435bdbee..000000000000
--- a/data/lua/connector_pkmn_rb.lua
+++ /dev/null
@@ -1,224 +0,0 @@
-local socket = require("socket")
-local json = require('json')
-local math = require('math')
-require("common")
-local STATE_OK = "Ok"
-local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
-local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
-local STATE_UNINITIALIZED = "Uninitialized"
-
-local SCRIPT_VERSION = 3
-
-local APIndex = 0x1A6E
-local APDeathLinkAddress = 0x00FD
-local APItemAddress = 0x00FF
-local EventFlagAddress = 0x1735
-local MissableAddress = 0x161A
-local HiddenItemsAddress = 0x16DE
-local RodAddress = 0x1716
-local DexSanityAddress = 0x1A71
-local InGameAddress = 0x1A84
-local ClientCompatibilityAddress = 0xFF00
-
-local ItemsReceived = nil
-local playerName = nil
-local seedName = nil
-
-local deathlink_rec = nil
-local deathlink_send = false
-
-local prevstate = ""
-local curstate = STATE_UNINITIALIZED
-local gbSocket = nil
-local frame = 0
-
-local compat = nil
-
-local function defineMemoryFunctions()
- local memDomain = {}
- local domains = memory.getmemorydomainlist()
- memDomain["rom"] = function() memory.usememorydomain("ROM") end
- memDomain["wram"] = function() memory.usememorydomain("WRAM") end
- return memDomain
-end
-
-local memDomain = defineMemoryFunctions()
-u8 = memory.read_u8
-wU8 = memory.write_u8
-u16 = memory.read_u16_le
-function uRange(address, bytes)
- data = memory.readbyterange(address - 1, bytes + 1)
- data[0] = nil
- return data
-end
-
-function generateLocationsChecked()
- memDomain.wram()
- events = uRange(EventFlagAddress, 0x140)
- missables = uRange(MissableAddress, 0x20)
- hiddenitems = uRange(HiddenItemsAddress, 0x0E)
- rod = {u8(RodAddress)}
- dexsanity = uRange(DexSanityAddress, 19)
-
-
- data = {}
-
- categories = {events, missables, hiddenitems, rod}
- if compat > 1 then
- table.insert(categories, dexsanity)
- end
- for _, category in ipairs(categories) do
- for _, v in ipairs(category) do
- table.insert(data, v)
- end
- end
-
- return data
-end
-
-local function arrayEqual(a1, a2)
- if #a1 ~= #a2 then
- return false
- end
-
- for i, v in ipairs(a1) do
- if v ~= a2[i] then
- return false
- end
- end
-
- return true
-end
-
-function receive()
- l, e = gbSocket:receive()
- if e == 'closed' then
- if curstate == STATE_OK then
- print("Connection closed")
- end
- curstate = STATE_UNINITIALIZED
- return
- elseif e == 'timeout' then
- return
- elseif e ~= nil then
- print(e)
- curstate = STATE_UNINITIALIZED
- return
- end
- if l ~= nil then
- block = json.decode(l)
- if block ~= nil then
- local itemsBlock = block["items"]
- if itemsBlock ~= nil then
- ItemsReceived = itemsBlock
- end
- deathlink_rec = block["deathlink"]
-
- end
- end
- -- Determine Message to send back
- memDomain.rom()
- newPlayerName = uRange(0xFFF0, 0x10)
- newSeedName = uRange(0xFFDB, 21)
- if (playerName ~= nil and not arrayEqual(playerName, newPlayerName)) or (seedName ~= nil and not arrayEqual(seedName, newSeedName)) then
- print("ROM changed, quitting")
- curstate = STATE_UNINITIALIZED
- return
- end
- playerName = newPlayerName
- seedName = newSeedName
- local retTable = {}
- retTable["scriptVersion"] = SCRIPT_VERSION
-
- if compat == nil then
- compat = u8(ClientCompatibilityAddress)
- if compat < 2 then
- InGameAddress = 0x1A71
- end
- end
-
- retTable["clientCompatibilityVersion"] = compat
- retTable["playerName"] = playerName
- retTable["seedName"] = seedName
- memDomain.wram()
-
- in_game = u8(InGameAddress)
- if in_game == 0x2A or in_game == 0xAC then
- retTable["locations"] = generateLocationsChecked()
- elseif in_game ~= 0 then
- print("Game may have crashed")
- curstate = STATE_UNINITIALIZED
- return
- end
-
- retTable["deathLink"] = deathlink_send
- deathlink_send = false
-
- msg = json.encode(retTable).."\n"
- local ret, error = gbSocket:send(msg)
- if ret == nil then
- print(error)
- elseif curstate == STATE_INITIAL_CONNECTION_MADE then
- curstate = STATE_TENTATIVELY_CONNECTED
- elseif curstate == STATE_TENTATIVELY_CONNECTED then
- print("Connected!")
- curstate = STATE_OK
- end
-end
-
-function main()
- if not checkBizHawkVersion() then
- return
- end
- server, error = socket.bind('localhost', 17242)
-
- while true do
- frame = frame + 1
- if not (curstate == prevstate) then
- print("Current state: "..curstate)
- prevstate = curstate
- end
- if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
- if (frame % 5 == 0) then
- receive()
- in_game = u8(InGameAddress)
- if in_game == 0x2A or in_game == 0xAC then
- if u8(APItemAddress) == 0x00 then
- ItemIndex = u16(APIndex)
- if deathlink_rec == true then
- wU8(APDeathLinkAddress, 1)
- elseif u8(APDeathLinkAddress) == 3 then
- wU8(APDeathLinkAddress, 0)
- deathlink_send = true
- end
- if ItemsReceived[ItemIndex + 1] ~= nil then
- item_id = ItemsReceived[ItemIndex + 1] - 172000000
- if item_id > 255 then
- item_id = item_id - 256
- end
- wU8(APItemAddress, item_id)
- end
- end
- end
- end
- elseif (curstate == STATE_UNINITIALIZED) then
- if (frame % 60 == 0) then
-
- print("Waiting for client.")
-
- emu.frameadvance()
- server:settimeout(2)
- print("Attempting to connect")
- local client, timeout = server:accept()
- if timeout == nil then
- curstate = STATE_INITIAL_CONNECTION_MADE
- gbSocket = client
- gbSocket:settimeout(0)
- end
- end
- end
- emu.frameadvance()
- end
-end
-
-main()
diff --git a/data/lua/connector_tloz.lua b/data/lua/connector_tloz.lua
index f48e4dfac1f2..4a2d2f25bf66 100644
--- a/data/lua/connector_tloz.lua
+++ b/data/lua/connector_tloz.lua
@@ -67,6 +67,7 @@ local itemsObtained = 0x0677
local takeAnyCavesChecked = 0x0678
local localTriforce = 0x0679
local bonusItemsObtained = 0x067A
+local itemsObtainedHigh = 0x067B
itemAPids = {
["Boomerang"] = 7100,
@@ -173,11 +174,18 @@ for key, value in pairs(itemAPids) do
itemIDNames[value] = key
end
+local function getItemsObtained()
+ return bit.bor(bit.lshift(u8(itemsObtainedHigh), 8), u8(itemsObtained))
+end
+local function setItemsObtained(value)
+ wU8(itemsObtainedHigh, bit.rshift(value, 8))
+ wU8(itemsObtained, bit.band(value, 0xFF))
+end
local function determineItem(array)
memdomain.ram()
- currentItemsObtained = u8(itemsObtained)
+ currentItemsObtained = getItemsObtained()
end
@@ -364,8 +372,8 @@ local function gotItem(item)
wU8(0x505, itemCode)
wU8(0x506, 128)
wU8(0x602, 4)
- numberObtained = u8(itemsObtained) + 1
- wU8(itemsObtained, numberObtained)
+ numberObtained = getItemsObtained() + 1
+ setItemsObtained(numberObtained)
if itemName == "Boomerang" then gotBoomerang() end
if itemName == "Bow" then gotBow() end
if itemName == "Magical Boomerang" then gotMagicalBoomerang() end
@@ -476,7 +484,7 @@ function processBlock(block)
if i > u8(bonusItemsObtained) then
if u8(0x505) == 0 then
gotItem(item)
- wU8(itemsObtained, u8(itemsObtained) - 1)
+ setItemsObtained(getItemsObtained() - 1)
wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1)
end
end
@@ -494,7 +502,7 @@ function processBlock(block)
for i, item in ipairs(itemsBlock) do
memDomain.ram()
if u8(0x505) == 0 then
- if i > u8(itemsObtained) then
+ if i > getItemsObtained() then
gotItem(item)
end
end
@@ -546,7 +554,7 @@ function receive()
retTable["gameMode"] = gameMode
retTable["overworldHC"] = getHCLocation()
retTable["overworldPB"] = getPBLocation()
- retTable["itemsObtained"] = u8(itemsObtained)
+ retTable["itemsObtained"] = getItemsObtained()
msg = json.encode(retTable).."\n"
local ret, error = zeldaSocket:send(msg)
if ret == nil then
@@ -606,4 +614,4 @@ function main()
end
end
-main()
\ No newline at end of file
+main()
diff --git a/data/options.yaml b/data/options.yaml
index b9bacaa0d103..ee8866627d52 100644
--- a/data/options.yaml
+++ b/data/options.yaml
@@ -17,10 +17,10 @@
# A. This is a .yaml file. You are allowed to use most characters.
# To test if your yaml is valid or not, you can use this website:
# http://www.yamllint.com/
-# You can also verify your Archipelago settings are valid at this site:
+# You can also verify that your Archipelago options are valid at this site:
# https://archipelago.gg/check
-# Your name in-game. Spaces will be replaced with underscores and there is a 16-character limit.
+# Your name in-game, limited to 16 characters.
# {player} will be replaced with the player's slot number.
# {PLAYER} will be replaced with the player's slot number, if that slot number is greater than 1.
# {number} will be replaced with the counter value of the name.
@@ -45,7 +45,10 @@ requires:
{% endmacro %}
{{ game }}:
- {%- for option_key, option in options.items() %}
+ {%- for group_name, group_options in option_groups.items() %}
+ # {{ group_name }}
+
+ {%- for option_key, option in group_options.items() %}
{{ option_key }}:
{%- if option.__doc__ %}
# {{ option.__doc__
@@ -65,21 +68,22 @@ requires:
{%- elif option.options -%}
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
- {{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
+ {{ yaml_dump(sub_option_name) }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
{%- endfor -%}
{%- if option.name_lookup[option.default] not in option.options %}
- {{ option.default }}: 50
+ {{ yaml_dump(option.default) }}: 50
{%- endif -%}
{%- elif option.default is string %}
- {{ option.default }}: 50
+ {{ yaml_dump(option.default) }}: 50
{%- elif option.default is iterable and option.default is not mapping %}
{{ option.default | list }}
{%- else %}
- {{ yaml_dump(option.default) | trim | indent(4, first=false) }}
+ {{ yaml_dump(option.default) | indent(4, first=false) }}
{%- endif -%}
{{ "\n" }}
{%- endfor %}
+ {%- endfor %}
diff --git a/data/yatta.ico b/data/yatta.ico
new file mode 100644
index 000000000000..f87a0980f49c
Binary files /dev/null and b/data/yatta.ico differ
diff --git a/data/yatta.png b/data/yatta.png
new file mode 100644
index 000000000000..4f230c86c727
Binary files /dev/null and b/data/yatta.png differ
diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS
index e92bfa42b628..ab841e65ee4c 100644
--- a/docs/CODEOWNERS
+++ b/docs/CODEOWNERS
@@ -1,35 +1,46 @@
# Archipelago World Code Owners / Maintainers Document
#
-# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder. For any pull
-# requests that modify these worlds, a code owner must approve the PR in addition to a core maintainer. This is not to
-# be used for files/folders outside the /worlds folder, those will always need sign off from a core maintainer.
+# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder as well as
+# certain documentation. For any pull requests that modify these worlds/docs, a code owner must approve the PR in
+# addition to a core maintainer. All other files and folders are owned and maintained by core maintainers directly.
#
# All usernames must be GitHub usernames (and are case sensitive).
-###################
-## Active Worlds ##
-###################
-
# Adventure
/worlds/adventure/ @JusticePS
+# A Hat in Time
+/worlds/ahit/ @CookieCat45
+
# A Link to the Past
/worlds/alttp/ @Berserker66
+# Sudoku (APSudoku)
+/worlds/apsudoku/ @EmilyV99
+
+# Aquaria
+/worlds/aquaria/ @tioui
+
# ArchipIDLE
/worlds/archipidle/ @LegendaryLinux
-# Sudoku (BK Sudoku)
-/worlds/bk_sudoku/ @Jarno458
-
# Blasphemous
/worlds/blasphemous/ @TRPG0
+# Bomb Rush Cyberfunk
+/worlds/bomb_rush_cyberfunk/ @TRPG0
+
# Bumper Stickers
/worlds/bumpstik/ @FelicitusNeko
+# Castlevania 64
+/worlds/cv64/ @LiquidCat64
+
+# Celeste 64
+/worlds/celeste64/ @PoryGone
+
# ChecksFinder
-/worlds/checksfinder/ @jonloveslegos
+/worlds/checksfinder/ @SunCatMC
# Clique
/worlds/clique/ @ThePhar
@@ -46,28 +57,43 @@
# DOOM 1993
/worlds/doom_1993/ @Daivuk
+# DOOM II
+/worlds/doom_ii/ @Daivuk
+
# Factorio
/worlds/factorio/ @Berserker66
-# Final Fantasy
-/worlds/ff1/ @jtoyoda
+# Final Fantasy Mystic Quest
+/worlds/ffmq/ @Alchav @wildham0
+
+# Heretic
+/worlds/heretic/ @Daivuk
# Hollow Knight
-/worlds/hk/ @BadMagic100 @ThePhar
+/worlds/hk/ @BadMagic100 @qwint
# Hylics 2
/worlds/hylics2/ @TRPG0
+# Kirby's Dream Land 3
+/worlds/kdl3/ @Silvris
+
# Kingdom Hearts 2
/worlds/kh2/ @JaredWeakStrike
-# Links Awakening DX
-/worlds/ladx/ @zig-for
+# Landstalker: The Treasures of King Nole
+/worlds/landstalker/ @Dinopony
+
+# Lingo
+/worlds/lingo/ @hatkirby
# Lufia II Ancient Cave
/worlds/lufia2ac/ @el-u
/worlds/lufia2ac/docs/ @wordfcuk @el-u
+# Mario & Luigi: Superstar Saga
+/worlds/mlss/ @jamesbrq
+
# Meritous
/worlds/meritous/ @FelicitusNeko
@@ -92,6 +118,9 @@
# Overcooked! 2
/worlds/overcooked2/ @toasterparty
+# Pokemon Emerald
+/worlds/pokemon_emerald/ @Zunawe
+
# Pokemon Red and Blue
/worlds/pokemon_rb/ @Alchav
@@ -104,11 +133,17 @@
# Risk of Rain 2
/worlds/ror2/ @kindasneaki
+# Shivers
+/worlds/shivers/ @GodlFire
+
+# A Short Hike
+/worlds/shorthike/ @chandler05
+
# Sonic Adventure 2 Battle
/worlds/sa2b/ @PoryGone @RaspberrySpace
-# Starcraft 2 Wings of Liberty
-/worlds/sc2wol/ @Ziktofel
+# Starcraft 2
+/worlds/sc2/ @Ziktofel
# Super Metroid
/worlds/sm/ @lordlou
@@ -143,6 +178,9 @@
# The Legend of Zelda (1)
/worlds/tloz/ @Rosalie-A @t3hf1gm3nt
+# TUNIC
+/worlds/tunic/ @silent-destroyer @ScipioWright
+
# Undertale
/worlds/undertale/ @jonloveslegos
@@ -155,12 +193,44 @@
# The Witness
/worlds/witness/ @NewSoupVi @blastron
+# Yoshi's Island
+/worlds/yoshisisland/ @PinkSwitch
+
+#Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
+/worlds/yugioh06/ @Rensen3
+
# Zillion
/worlds/zillion/ @beauxq
-##################################
-## Disabled Unmaintained Worlds ##
-##################################
+# Zork Grand Inquisitor
+/worlds/zork_grand_inquisitor/ @nbrochu
+
+
+## Active Unmaintained Worlds
+
+# The following worlds in this repo are currently unmaintained, but currently still work in core. If any update breaks
+# compatibility, these worlds may be moved to `worlds_disabled`. If you are interested in stepping up as maintainer for
+# any of these worlds, please review `/docs/world maintainer.md` documentation.
+
+# Final Fantasy (1)
+# /worlds/ff1/
+
+# Links Awakening DX
+# /worlds/ladx/
+
+## Disabled Unmaintained Worlds
+
+# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are
+# interested in stepping up as maintainer for any of these worlds, please review `/docs/world maintainer.md`
+# documentation.
# Ori and the Blind Forest
-# /worlds_disabled/oribf/
+# /worlds_disabled/oribf/
+
+###################
+## Documentation ##
+###################
+
+# Apworld Dev Faq
+/docs/apworld_dev_faq.md @qwint @ScipioWright
+
diff --git a/docs/adding games.md b/docs/adding games.md
index 24d9e499cd98..9d2860b4a196 100644
--- a/docs/adding games.md
+++ b/docs/adding games.md
@@ -1,344 +1,78 @@
-
-
-# How do I add a game to Archipelago?
-This guide is going to try and be a broad summary of how you can do just that.
-There are two key steps to incorporating a game into Archipelago:
-- Game Modification
-- Archipelago Server Integration
-
-Refer to the following documents as well:
-- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md) for network communication between client and server.
-- [world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md) for documentation on server side code and creating a world package.
-
-
-# Game Modification
-One half of the work required to integrate a game into Archipelago is the development of the game client. This is
-typically done through a modding API or other modification process, described further down.
-
-As an example, modifications to a game typically include (more on this later):
-- Hooking into when a 'location check' is completed.
-- Networking with the Archipelago server.
-- Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection.
-
-In order to determine how to modify a game, refer to the following sections.
-
-## Engine Identification
-This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
-Examples are provided below.
-
-### Creepy Castle
-![Creepy Castle Root Directory in Window's Explorer](./img/creepy-castle-directory.png)
-
-This is the delightful title Creepy Castle, which is a fantastic game that I highly recommend. It’s also your worst-case
-scenario as a modder. All that’s present here is an executable file and some meta-information that Steam uses. You have
-basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty nasty
-disassembly and reverse engineering work, which is outside the scope of this tutorial. Let’s look at some other examples
-of game releases.
-
-### Heavy Bullets
-![Heavy Bullets Root Directory in Window's Explorer](./img/heavy-bullets-directory.png)
-
-Here’s the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files.
-“hello.txt†is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually
-with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing
-information, credits, and general info about the game. You usually won’t find anything too helpful here, but it never
-hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important.
-“steam_api.dll†is a file you can safely ignore, it’s just some code used to interface with Steam.
-The directory “HEAVY_BULLETS_Dataâ€, however, has some good news.
-
-![Heavy Bullets Data Directory in Window's Explorer](./img/heavy-bullets-data-directory.png)
-
-Jackpot! It might not be obvious what you’re looking at here, but I can instantly tell from this folder’s contents that
-what we have is a game made in the Unity Engine. If you look in the sub-folders, you’ll seem some .dll files which affirm
-our suspicions. Telltale signs for this are directories titled “Managed†and “Monoâ€, as well as the numbered, extension-less
-level files and the sharedassets files. We’ll tell you a bit about why seeing a Unity game is such good news later,
-but for now, this is what one looks like. Also keep your eyes out for an executable with a name like UnityCrashHandler,
-that’s another dead giveaway.
-
-### Stardew Valley
-![Stardew Valley Root Directory in Window's Explorer](./img/stardew-valley-directory.png)
-
-This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways.
-Notice the .dll files which include “CSharp†in their name. This tells us that the game was made in C#, which is good news.
-More on that later.
-
-### Gato Roboto
-![Gato Roboto Root Directory in Window's Explorer](./img/gato-roboto-directory.png)
-
-Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for.
-The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker.
-
-This isn't all you'll ever see looking at game files, but it's a good place to start.
-As a general rule, the more files a game has out in plain sight, the more you'll be able to change.
-This especially applies in the case of code or script files - always keep a lookout for anything you can use to your
-advantage!
-
-## Open or Leaked Source Games
-As a side note, many games have either been made open source, or have had source files leaked at some point.
-This can be a boon to any would-be modder, for obvious reasons.
-Always be sure to check - a quick internet search for "(Game) Source Code" might not give results often, but when it
-does you're going to have a much better time.
-
-Be sure never to distribute source code for games that you decompile or find if you do not have express permission to do
-so, or to redistribute any materials obtained through similar methods, as this is illegal and unethical.
-
-## Modifying Release Versions of Games
-However, for now we'll assume you haven't been so lucky, and have to work with only what’s sitting in your install directory.
-Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools,
-but these are often not geared to the kind of work you'll be doing and may not help much.
-
-As a general rule, any modding tool that lets you write actual code is something worth using.
-
-### Research
-The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification,
-it's possible other motivated parties have concocted useful tools for your game already.
-Always be sure to search the Internet for the efforts of other modders.
-
-### Analysis Tools
-Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to existing game tools.
-
-#### [dnSpy](https://github.com/dnSpy/dnSpy/releases)
-The first tool in your toolbox is dnSpy.
-dnSpy is useful for opening and modifying code files, like .exe and .dll files, that were made in C#.
-This won't work for executable files made by other means, and obfuscated code (code which was deliberately made
-difficult to reverse engineer) will thwart it, but 9 times out of 10 this is exactly what you need.
-You'll want to avoid opening common library files in dnSpy, as these are unlikely to contain the data you're looking to
-modify.
-
-For Unity games, the file you’ll want to open will be the file (Data Folder)/Managed/Assembly-CSharp.dll, as pictured below:
-
-![Heavy Bullets Managed Directory in Window's Explorer](./img/heavy-bullets-managed-directory.png)
-
-This file will contain the data of the actual game.
-For other C# games, the file you want is usually just the executable itself.
-
-With dnSpy, you can view the game’s C# code, but the tool isn’t perfect.
-Although the names of classes, methods, variables, and more will be preserved, code structures may not remain entirely intact. This is because compilers will often subtly rewrite code to be more optimal, so that it works the same as the original code but uses fewer resources. Compiled C# files also lose comments and other documentation.
-
-#### [UndertaleModTool](https://github.com/krzys-h/UndertaleModTool/releases)
-This is currently the best tool for modifying games made in GameMaker, and supports games made in both GMS 1 and 2.
-It allows you to modify code in GML, if the game wasn't made with the wrong compiler (usually something you don't have
-to worry about).
-
-You'll want to open the data.win file, as this is where all the goods are kept.
-Like dnSpy, you won’t be able to see comments.
-In addition, you will be able to see and modify many hidden fields on items that GameMaker itself will often hide from
-creators.
-
-Fonts in particular are notoriously complex, and to add new sprites you may need to modify existing sprite sheets.
-
-#### [CheatEngine](https://cheatengine.org/)
-CheatEngine is a tool with a very long and storied history.
-Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as
-malware (because this behavior is most commonly found in malware and rarely used by other programs).
-If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level,
-including binary data formats, addressing, and assembly language programming.
-
-The tool itself is highly complex and even I have not yet charted its expanses.
-However, it can also be a very powerful tool in the right hands, allowing you to query and modify gamestate without ever
-modifying the actual game itself.
-In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do
-anything with it.
-
-### What Modifications You Should Make to the Game
-We talked about this briefly in [Game Modification](#game-modification) section.
-The next step is to know what you need to make the game do now that you can modify it. Here are your key goals:
-- Modify the game so that checks are shuffled
-- Know when the player has completed a check, and react accordingly
-- Listen for messages from the Archipelago server
-- Modify the game to display messages from the Archipelago server
-- Add interface for connecting to the Archipelago server with passwords and sessions
-- Add commands for manually rewarding, re-syncing, releasing, and other actions
-
-To elaborate, you need to be able to inform the server whenever you check locations, print out messages that you receive
-from the server in-game so players can read them, award items when the server tells you to, sync and re-sync when necessary,
-avoid double-awarding items while still maintaining game file integrity, and allow players to manually enter commands in
-case the client or server make mistakes.
-
-Refer to the [Network Protocol documentation](./network%20protocol.md) for how to communicate with Archipelago's servers.
-
-## But my Game is a console game. Can I still add it?
-That depends – what console?
-
-### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc
-Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised
-that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright
-holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console games.
-
-### My Game isn’t that old, it’s for the Wii/PS2/360/etc
-This is very complex, but doable.
-If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it.
-There exist many disassembly and debugging tools, but more recent content may have lackluster support.
-
-### My Game is a classic for the SNES/Sega Genesis/etc
-That’s a lot more feasible.
-There are many good tools available for understanding and modifying games on these older consoles, and the emulation
-community will have figured out the bulk of the console’s secrets.
-Look for debugging tools, but be ready to learn assembly.
-Old consoles usually have their own unique dialects of ASM you’ll need to get used to.
-
-Also make sure there’s a good way to interface with a running emulator, since that’s the only way you can connect these
-older consoles to the Internet.
-There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a computer,
-but these will require the same sort of interface software to be written in order to work properly - from your perspective
-the two won't really look any different.
-
-### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that-
-Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no.
-Obscurity is your enemy – there will likely be little to no emulator or modding information, and you’d essentially be
-working from scratch.
-
-## How to Distribute Game Modifications
-**NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!**
-
-This is a good way to get any project you're working on sued out from under you.
-The right way to distribute modified versions of a game's binaries, assuming that the licensing terms do not allow you
-to copy them wholesale, is as patches.
-
-There are many patch formats, which I'll cover in brief. The common theme is that you can’t distribute anything that
-wasn't made by you. Patches are files that describe how your modified file differs from the original one, thus avoiding
-the issue of distributing someone else’s original work.
-
-Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play.
-
-### Patches
-
-#### IPS
-IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode
-moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's
-fine.
-
-#### UPS, BPS, VCDIFF (xdelta), bsdiff
-Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is
-possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes
-compression, so this format is used by APBP.
-
-Only a bsdiff module is integrated into AP. If the final patch requires or is based on any other patch, convert them to
-bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp".
-
-#### APBP Archipelago Binary Patch
-Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional
-files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the
-bsdiff between the original and the randomized ROM.
-
-To make using APBP easy, they can be generated by inheriting from `worlds.Files.APDeltaPatch`.
-
-### Mod files
-Games which support modding will usually just let you drag and drop the mod’s files into a folder somewhere.
-Mod files come in many forms, but the rules about not distributing other people's content remain the same.
-They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be
-generated per seed.
-
-If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy
-integration into the Webhost by inheriting from `worlds.Files.APContainer`.
-
-
-## Archipelago Integration
-Integrating a randomizer into Archipelago involves a few steps.
-There are several things that may need to be done, but the most important is to create an implementation of the
-`World` class specific to your game. This implementation should exist as a Python module within the `worlds` folder
-in the Archipelago file structure.
-
-This encompasses most of the data for your game – the items available, what checks you have, the logic for reaching those
-checks, what options to offer for the player’s yaml file, and the code to initialize all this data.
-
-Here’s an example of what your world module can look like:
-
-![Example world module directory open in Window's Explorer](./img/archipelago-world-directory-example.png)
-
-The minimum requirements for a new archipelago world are the package itself (the world folder containing a file named `__init__.py`),
-which must define a `World` class object for the game with a game name, create an equal number of items and locations with rules,
-a win condition, and at least one `Region` object.
-
-Let's give a quick breakdown of what the contents for these files look like.
-This is just one example of an Archipelago world - the way things are done below is not an immutable property of Archipelago.
-
-### Items.py
-This file is used to define the items which exist in a given game.
-
-![Example Items.py file open in Notepad++](./img/example-items-py-file.png)
-
-Some important things to note here. The center of our Items.py file is the item_table, which individually lists every
-item in the game and associates them with an ItemData.
-
-This file is rather skeletal - most of the actual data has been stripped out for simplicity.
-Each ItemData gives a numeric ID to associate with the item and a boolean telling us whether the item might allow the
-player to do more than they would have been able to before.
-
-Next there's the item_frequencies. This simply tells Archipelago how many times each item appears in the pool.
-Items that appear exactly once need not be listed - Archipelago will interpret absence from this dictionary as meaning
-that the item appears once.
-
-Lastly, note the `lookup_id_to_name` dictionary, which is typically imported and used in your Archipelago `World`
-implementation. This is how Archipelago is told about the items in your world.
-
-### Locations.py
-This file lists all locations in the game.
-
-![Example Locations.py file open in Notepad++](./img/example-locations-py-file.png)
-
-First is the achievement_table. It lists each location, the region that it can be found in (more on regions later),
-and a numeric ID to associate with each location.
-
-The exclusion table is a series of dictionaries which are used to exclude certain checks from the pool of progression
-locations based on user settings, and the events table associates certain specific checks with specific items.
-
-`lookup_id_to_name` is also present for locations, though this is a separate dictionary, to be clear.
-
-### Options.py
-This file details options to be searched for in a player's YAML settings file.
-
-![Example Options.py file open in Notepad++](./img/example-options-py-file.png)
-
-There are several types of option Archipelago has support for.
-In our case, we have three separate choices a player can toggle, either On or Off.
-You can also have players choose between a number of predefined values, or have them provide a numeric value within a
-specified range.
-
-### Regions.py
-This file contains data which defines the world's topology.
-In other words, it details how different regions of the game connect to each other.
-
-![Example Regions.py file open in Notepad++](./img/example-regions-py-file.png)
-
-`terraria_regions` contains a list of tuples.
-The first element of the tuple is the name of the region, and the second is a list of connections that lead out of the region.
-
-`mandatory_connections` describe where the connection leads.
-
-Above this data is a function called `link_terraria_structures` which uses our defined regions and connections to create
-something more usable for Archipelago, but this has been left out for clarity.
-
-### Rules.py
-This is the file that details rules for what players can and cannot logically be required to do, based on items and settings.
-
-![Example Rules.py file open in Notepad++](./img/example-rules-py-file.png)
-
-This is the most complicated part of the job, and is one part of Archipelago that is likely to see some changes in the future.
-The first class, called `TerrariaLogic`, is an extension of the `LogicMixin` class.
-This is where you would want to define methods for evaluating certain conditions, which would then return a boolean to
-indicate whether conditions have been met. Your rule definitions should start with some sort of identifier to delineate it
-from other games, as all rules are mixed together due to `LogicMixin`. In our case, `_terraria_rule` would be a better name.
-
-The method below, `set_rules()`, is where you would assign these functions as "rules", using lambdas to associate these
-functions or combinations of them (or any other code that evaluates to a boolean, in my case just the placeholder `True`)
-to certain tasks, like checking locations or using entrances.
-
-### \_\_init\_\_.py
-This is the file that actually extends the `World` class, and is where you expose functionality and data to Archipelago.
-
-![Example \_\_init\_\_.py file open in Notepad++](./img/example-init-py-file.png)
-
-This is the most important file for the implementation, and technically the only one you need, but it's best to keep this
-file as short as possible and use other script files to do most of the heavy lifting.
-If you've done things well, this will just be where you assign everything you set up in the other files to their associated
-fields in the class being extended.
-
-This is also a good place to put game-specific quirky behavior that needs to be managed, as it tends to make things a bit
-cluttered if you put these things elsewhere.
-
-The various methods and attributes are documented in `/worlds/AutoWorld.py[World]` and
-[world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md),
-though it is also recommended to look at existing implementations to see how all this works first-hand.
-Once you get all that, all that remains to do is test the game and publish your work.
-Make sure to check out [world maintainer.md](./world%20maintainer.md) before publishing.
+# Adding Games
+
+Adding a new game to Archipelago has two major parts:
+
+* Game Modification to communicate with Archipelago server (hereafter referred to as "client")
+* Archipelago Generation and Server integration plugin (hereafter referred to as "world")
+
+This document will attempt to illustrate the bare minimum requirements and expectations of both parts of a new world
+integration. As game modification wildly varies by system and engine, and has no bearing on the Archipelago protocol,
+it will not be detailed here.
+
+## Client
+
+The client is an intermediary program between the game and the Archipelago server. This can either be a direct
+modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
+must fulfill a few requirements in order to function as expected. The specific requirements the game client must follow
+to behave as expected are:
+
+* Handle both secure and unsecure websocket connections
+* Detect and react when a location has been "checked" by the player by sending a network packet to the server
+* Receive and parse network packets when the player receives an item from the server, and reward it to the player on
+demand
+ * **Any** of your items can be received any number of times, up to and far surpassing those that the game might
+normally expect from features such as starting inventory, item link replacement, or item cheating
+ * Players and the admin can cheat items to the player at any time with a server command, and these items may not have
+a player or location attributed to them
+* Be able to change the port for saved connection info
+ * Rooms hosted on the website attempt to reserve their port, but since there are a limited number of ports, this
+privilege can be lost, requiring the room to be moved to a new port
+* Reconnect if the connection is unstable and lost while playing
+* Keep an index for items received in order to resync. The ItemsReceived Packets are a single list with guaranteed
+order.
+* Receive items that were sent to the player while they were not connected to the server
+ * The player being able to complete checks while offline and sending them when reconnecting is a good bonus, but not
+strictly required
+* Send a status update packet alerting the server that the player has completed their goal
+
+Libraries for most modern languages and the spec for various packets can be found in the
+[network protocol](/docs/network%20protocol.md) API reference document.
+
+## World
+
+The world is your game integration for the Archipelago generator, webhost, and multiworld server. It contains all the
+information necessary for creating the items and locations to be randomized, the logic for item placement, the
+datapackage information so other game clients can recognize your game data, and documentation. Your world must be
+written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
+repository and creating a new world package in `/worlds/`. A bare minimum world implementation must satisfy the
+following requirements:
+
+* A folder within `/worlds/` that contains an `__init__.py`
+* A `World` subclass where you create your world and define all of its rules
+* A unique game name
+* For webhost documentation and behaviors, a `WebWorld` subclass that must be instantiated in the `World` class
+definition
+ * The game_info doc must follow the format `{language_code}_{game_name}.md`
+* A mapping for items and locations defining their names and ids for clients to be able to identify them. These are
+`item_name_to_id` and `location_name_to_id`, respectively.
+* Create an item when `create_item` is called both by your code and externally
+* An `options_dataclass` defining the options players have available to them
+* A `Region` for your player with the name "Menu" to start from
+* Create a non-zero number of locations and add them to your regions
+* Create a non-zero number of items **equal** to the number of locations and add them to the multiworld itempool
+* All items submitted to the multiworld itempool must not be manually placed by the World. If you need to place specific
+items, there are multiple ways to do so, but they should not be added to the multiworld itempool.
+
+Notable caveats:
+* The "Menu" region will always be considered the "start" for the player
+* The "Menu" region is *always* considered accessible; i.e. the player is expected to always be able to return to the
+start of the game from anywhere
+* When submitting regions or items to the multiworld (multiworld.regions and multiworld.itempool respectively), use
+`append`, `extend`, or `+=`. **Do not use `=`**
+* Regions are simply containers for locations that share similar access rules. They do not have to map to
+concrete, physical areas within your game and can be more abstract like tech trees or a questline.
+
+The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call during
+generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
+regarding the API can be found in the [world api doc](/docs/world%20api.md).
+Before publishing, make sure to also check out [world maintainer.md](/docs/world%20maintainer.md).
diff --git a/docs/apworld specification.md b/docs/apworld specification.md
index 98cd25a73032..ed2e8b1c8ecb 100644
--- a/docs/apworld specification.md
+++ b/docs/apworld specification.md
@@ -29,6 +29,7 @@ The zip can contain arbitrary files in addition what was specified above.
## Caveats
-Imports from other files inside the apworld have to use relative imports.
+Imports from other files inside the apworld have to use relative imports. e.g. `from .options import MyGameOptions`
-Imports from AP base have to use absolute imports, e.g. Options.py and worlds/AutoWorld.py.
+Imports from AP base have to use absolute imports, e.g. `from Options import Toggle` or
+`from worlds.AutoWorld import World`
diff --git a/docs/apworld_dev_faq.md b/docs/apworld_dev_faq.md
new file mode 100644
index 000000000000..8d9429afa321
--- /dev/null
+++ b/docs/apworld_dev_faq.md
@@ -0,0 +1,45 @@
+# APWorld Dev FAQ
+
+This document is meant as a reference tool to show solutions to common problems when developing an apworld.
+It is not intended to answer every question about Archipelago and it assumes you have read the other docs,
+including [Contributing](contributing.md), [Adding Games](), and [World API]().
+
+---
+
+### My game has a restrictive start that leads to fill errors
+
+Hint to the Generator that an item needs to be in sphere one with local_early_items. Here, `1` represents the number of "Sword" items to attempt to place in sphere one.
+```py
+early_item_name = "Sword"
+self.multiworld.local_early_items[self.player][early_item_name] = 1
+```
+
+Some alternative ways to try to fix this problem are:
+* Add more locations to sphere one of your world, potentially only when there would be a restrictive start
+* Pre-place items yourself, such as during `create_items`
+* Put items into the player's starting inventory using `push_precollected`
+* Raise an exception, such as an `OptionError` during `generate_early`, to disallow options that would lead to a restrictive start
+
+---
+
+### I have multiple settings that change the item/location pool counts and need to balance them out
+
+In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be unbalanced. But in real, complex situations, that might be unfeasible.
+
+If that's the case, you can create extra filler based on the difference between your unfilled locations and your itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations) to your list of items to submit
+
+Note: to use self.create_filler(), self.get_filler_item_name() should be defined to only return valid filler item names
+```py
+total_locations = len(self.multiworld.get_unfilled_locations(self.player))
+item_pool = self.create_non_filler_items()
+
+for _ in range(total_locations - len(item_pool)):
+ item_pool.append(self.create_filler())
+
+self.multiworld.itempool += item_pool
+```
+
+A faster alternative to the `for` loop would be to use a [list comprehension](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions):
+```py
+item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))]
+```
diff --git a/docs/contributing.md b/docs/contributing.md
index 899c06b92279..9fd21408eb7b 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -1,14 +1,49 @@
# Contributing
-Contributions are welcome. We have a few requests of any new contributors.
-* Ensure that all changes which affect logic are covered by unit tests.
-* Do not introduce any unit test failures/regressions.
-* Follow styling as designated in our [styling documentation](/docs/style.md).
+All contributions are welcome, though we have a few requests of contributors, whether they be for core, webhost, or new
+game contributions:
-Otherwise, we tend to judge code on a case to case basis.
+* **Follow styling guidelines.**
+ Please take a look at the [code style documentation](/docs/style.md)
+ to ensure ease of communication and uniformity.
-For adding a new game to Archipelago and other documentation on how Archipelago functions, please see
-[the docs folder](/docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev
-channel in our [Discord](https://archipelago.gg/discord).
-If you want to merge a new game, please make sure to read the responsibilities as
+* **Ensure that critical changes are covered by tests.**
+ It is strongly recommended that unit tests are used to avoid regression and to ensure everything is still working.
+ If you wish to contribute by adding a new game, please take a look at
+ the [logic unit test documentation](/docs/tests.md).
+ If you wish to contribute to the website, please take a look at [these tests](/test/webhost).
+
+* **Do not introduce unit test failures/regressions.**
+ Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test
+ your changes. Currently, the oldest supported version
+ is [Python 3.8](https://www.python.org/downloads/release/python-380/).
+ It is recommended that automated github actions are turned on in your fork to have github run unit tests after
+ pushing.
+ You can turn them on here:
+ ![Github actions example](./img/github-actions-example.png)
+
+* **When reviewing PRs, please leave a message about what was done.**
+ We don't have full test coverage, so manual testing can help.
+ For code changes that could affect multiple worlds or that could have changes in unexpected code paths, manual testing
+ or checking if all code paths are covered by automated tests is desired. The original author may not have been able
+ to test all possibly affected worlds, or didn't know it would affect another world. In such cases, it is helpful to
+ state which games or settings were rolled, if any.
+ Please also tell us if you looked at code, just did functional testing, did both, or did neither.
+ If testing the PR depends on other PRs, please state what you merged into what for testing.
+ We cannot determine what "LGTM" means without additional context, so that should not be the norm.
+
+Other than these requests, we tend to judge code on a case-by-case basis.
+
+For contribution to the website, please refer to the [WebHost README](/WebHostLib/README.md).
+
+If you want to contribute to the core, you will be subject to stricter review on your pull requests. It is recommended
+that you get in touch with other core maintainers via the [Discord](https://archipelago.gg/discord).
+
+If you want to add Archipelago support for a new game, please take a look at
+the [adding games documentation](/docs/adding%20games.md)
+which details what is required to implement support for a game, and has tips on to get started.
+If you want to merge a new game into the main Archipelago repo, please make sure to read the responsibilities as a
[world maintainer](/docs/world%20maintainer.md).
+
+For other questions, feel free to explore the [main documentation folder](/docs), and ask us questions in the
+#ap-world-dev channel of the [Discord](https://archipelago.gg/discord).
diff --git a/docs/img/archipelago-world-directory-example.png b/docs/img/archipelago-world-directory-example.png
deleted file mode 100644
index ba720f3319b9..000000000000
Binary files a/docs/img/archipelago-world-directory-example.png and /dev/null differ
diff --git a/docs/img/creepy-castle-directory.png b/docs/img/creepy-castle-directory.png
deleted file mode 100644
index af4fdc584dc0..000000000000
Binary files a/docs/img/creepy-castle-directory.png and /dev/null differ
diff --git a/docs/img/example-init-py-file.png b/docs/img/example-init-py-file.png
deleted file mode 100644
index 6dd5c3c9380b..000000000000
Binary files a/docs/img/example-init-py-file.png and /dev/null differ
diff --git a/docs/img/example-items-py-file.png b/docs/img/example-items-py-file.png
deleted file mode 100644
index e114a78d2110..000000000000
Binary files a/docs/img/example-items-py-file.png and /dev/null differ
diff --git a/docs/img/example-locations-py-file.png b/docs/img/example-locations-py-file.png
deleted file mode 100644
index 53c4bc1e2905..000000000000
Binary files a/docs/img/example-locations-py-file.png and /dev/null differ
diff --git a/docs/img/example-options-py-file.png b/docs/img/example-options-py-file.png
deleted file mode 100644
index 5811f5400060..000000000000
Binary files a/docs/img/example-options-py-file.png and /dev/null differ
diff --git a/docs/img/example-regions-py-file.png b/docs/img/example-regions-py-file.png
deleted file mode 100644
index a9d05c53fcbf..000000000000
Binary files a/docs/img/example-regions-py-file.png and /dev/null differ
diff --git a/docs/img/example-rules-py-file.png b/docs/img/example-rules-py-file.png
deleted file mode 100644
index b76e78b40622..000000000000
Binary files a/docs/img/example-rules-py-file.png and /dev/null differ
diff --git a/docs/img/gato-roboto-directory.png b/docs/img/gato-roboto-directory.png
deleted file mode 100644
index fda0eb5ff992..000000000000
Binary files a/docs/img/gato-roboto-directory.png and /dev/null differ
diff --git a/docs/img/github-actions-example.png b/docs/img/github-actions-example.png
new file mode 100644
index 000000000000..2363a3ed4c56
Binary files /dev/null and b/docs/img/github-actions-example.png differ
diff --git a/docs/img/heavy-bullets-data-directory.png b/docs/img/heavy-bullets-data-directory.png
deleted file mode 100644
index 9fdcba1ca02a..000000000000
Binary files a/docs/img/heavy-bullets-data-directory.png and /dev/null differ
diff --git a/docs/img/heavy-bullets-directory.png b/docs/img/heavy-bullets-directory.png
deleted file mode 100644
index 8bc14836c601..000000000000
Binary files a/docs/img/heavy-bullets-directory.png and /dev/null differ
diff --git a/docs/img/heavy-bullets-managed-directory.png b/docs/img/heavy-bullets-managed-directory.png
deleted file mode 100644
index 73017f6dc96d..000000000000
Binary files a/docs/img/heavy-bullets-managed-directory.png and /dev/null differ
diff --git a/docs/img/stardew-valley-directory.png b/docs/img/stardew-valley-directory.png
deleted file mode 100644
index 64bbf66ec299..000000000000
Binary files a/docs/img/stardew-valley-directory.png and /dev/null differ
diff --git a/docs/network protocol.md b/docs/network protocol.md
index d461cebce1ec..da5c41431501 100644
--- a/docs/network protocol.md
+++ b/docs/network protocol.md
@@ -27,8 +27,13 @@ There are also a number of community-supported libraries available that implemen
| Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | |
| Rust | [ArchipelagoRS](https://github.com/ryanisaacg/archipelago_rs) | |
| Lua | [lua-apclientpp](https://github.com/black-sliver/lua-apclientpp) | |
+| Game Maker + Studio 1.x | [gm-apclientpp](https://github.com/black-sliver/gm-apclientpp) | For GM7, GM8 and GMS1.x, maybe older |
+| GameMaker: Studio 2.x+ | [see Discord](https://discord.com/channels/731205301247803413/1166418532519653396) | |
## Synchronizing Items
+After a client connects, it will receive all previously collected items for its associated slot in a [ReceivedItems](#ReceivedItems) packet. This will include items the client may have already processed in a previous play session.
+To ensure the client is able to reject those items if it needs to, each item in the packet has an associated `index` argument. You will need to find a way to save the "last processed item index" to the player's local savegame, a local file, or something to that effect. Before connecting, you should load that "last processed item index" value and compare against it in your received items handling.
+
When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet.
Even if the client detects a desync, it can still accept the items provided in this packet to prevent gameplay interruption.
@@ -48,7 +53,7 @@ Example:
```
## (Server -> Client)
-These packets are are sent from the multiworld server to the client. They are not messages which the server accepts.
+These packets are sent from the multiworld server to the client. They are not messages which the server accepts.
* [RoomInfo](#RoomInfo)
* [ConnectionRefused](#ConnectionRefused)
* [Connected](#Connected)
@@ -75,7 +80,6 @@ Sent to clients when they connect to an Archipelago server.
| hint_cost | int | The percentage of total locations that need to be checked to receive a hint from the server. |
| location_check_points | int | The amount of hint points you receive per item/location check completed. |
| games | list\[str\] | List of games present in this multiworld. |
-| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). **Deprecated. Use `datapackage_checksums` instead.** |
| datapackage_checksums | dict[str, str] | Checksum hash of the individual games' data packages the server will send. Used by newer clients to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents) for more information. |
| seed_name | str | Uniquely identifying name of this generation |
| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. |
@@ -325,7 +329,11 @@ Sent to server to inform it of locations that the client has checked. Used to in
| locations | list\[int\] | The ids of the locations checked by the client. May contain any number of checks, even ones sent before; duplicates do not cause issues with the Archipelago server. |
### LocationScouts
-Sent to the server to inform it of locations the client has seen, but not checked. Useful in cases in which the item may appear in the game world, such as 'ledge items' in A Link to the Past. The server will always respond with a [LocationInfo](#LocationInfo) packet with the items located in the scouted location.
+Sent to the server to retrieve the items that are on a specified list of locations. The server will respond with a [LocationInfo](#LocationInfo) packet containing the items located in the scouted locations.
+Fully remote clients without a patch file may use this to "place" items onto their in-game locations, most commonly to display their names or item classifications before/upon pickup.
+
+LocationScouts can also be used to inform the server of locations the client has seen, but not checked. This creates a hint as if the player had run `!hint_location` on a location, but without deducting hint points.
+This is useful in cases where an item appears in the game world, such as 'ledge items' in _A Link to the Past_. To do this, set the `create_as_hint` parameter to a non-zero value.
#### Arguments
| Name | Type | Notes |
@@ -339,7 +347,7 @@ Sent to the server to update on the sender's status. Examples include readiness
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
-| status | ClientStatus\[int\] | One of [Client States](#Client-States). Send as int. Follow the link for more information. |
+| status | ClientStatus\[int\] | One of [Client States](#ClientStatus). Send as int. Follow the link for more information. |
### Say
Basic chat command which sends text to the server to be distributed to other clients.
@@ -380,11 +388,13 @@ Additional arguments sent in this package will also be added to the [Retrieved](
Some special keys exist with specific return data, all of them have the prefix `_read_`, so `hints_{team}_{slot}` is `_read_hints_{team}_{slot}`.
-| Name | Type | Notes |
-|-------------------------------|--------------------------|---------------------------------------------------|
-| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. |
-| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. |
-| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
+| Name | Type | Notes |
+|----------------------------------|-------------------------------|-------------------------------------------------------|
+| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. |
+| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. |
+| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
+| location_name_groups_{game_name} | dict\[str, list\[str\]\] | location_name_groups belonging to the requested game. |
+| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. |
### Set
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package.
@@ -415,6 +425,8 @@ The following operations can be applied to a datastorage key
| mul | Multiplies the current value of the key by `value`. |
| pow | Multiplies the current value of the key to the power of `value`. |
| mod | Sets the current value of the key to the remainder after division by `value`. |
+| floor | Floors the current value (`value` is ignored). |
+| ceil | Ceils the current value (`value` is ignored). |
| max | Sets the current value of the key to `value` if `value` is bigger. |
| min | Sets the current value of the key to `value` if `value` is lower. |
| and | Applies a bitwise AND to the current value of the key with `value`. |
@@ -487,9 +499,9 @@ In JSON this may look like:
{"item": 3, "location": 3, "player": 3, "flags": 0}
]
```
-`item` is the item id of the item. Item ids are in the range of ± 253 -1.
+`item` is the item id of the item. Item ids are only supported in the range of [-253 , 253 - 1], with anything ≤ 0 reserved for Archipelago use.
-`location` is the location id of the item inside the world. Location ids are in the range of ± 253 -1.
+`location` is the location id of the item inside the world. Location ids are only supported in the range of [-253 , 253 - 1], with anything ≤ 0 reserved for Archipelago use.
`player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item
@@ -556,7 +568,7 @@ Color options:
`player` marks owning player id for location/item,
`flags` contains the [NetworkItem](#NetworkItem) flags that belong to the item
-### Client States
+### ClientStatus
An enumeration containing the possible client states that may be used to inform
the server in [StatusUpdate](#StatusUpdate). The MultiServer automatically sets
the client state to `ClientStatus.CLIENT_CONNECTED` on the first active connection
@@ -633,15 +645,47 @@ class Hint(typing.NamedTuple):
```
### Data Package Contents
-A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings.
-
-We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different version. A special case is datapackage version 0, where it is expected the package is custom and should not be cached.
-
-Note:
- * Any ID is unique to its type across AP: Item 56 only exists once and Location 56 only exists once.
- * Any Name is unique to its type across its own Game only: Single Arrow can exist in two games.
- * The IDs from the game "Archipelago" may be used in any other game.
- Especially Location ID -1: Cheat Console and -2: Server (typically Remote Start Inventory)
+A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago
+server most easily and not maintain their own mappings. Some contents include:
+
+ - Name to ID mappings for items and locations.
+ - A checksum of each game's data package for clients to tell if a cached package is invalid.
+
+We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know
+when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different checksum
+than any locally cached ones.
+
+**Important Notes about IDs and Names**:
+
+* IDs ≤ 0 are reserved for "Archipelago" and should not be used by other world implementations.
+* The IDs from the game "Archipelago" (in `worlds/generic`) may be used in any world.
+ * Especially Location ID `-1`: `Cheat Console` and `-2`: `Server` (typically Remote Start Inventory)
+* Any names and IDs are only unique in its own world data package, but different games may reuse these names or IDs.
+ * At runtime, you will need to look up the game of the player to know which item or location ID/Name to lookup in the
+ data package. This can be easily achieved by reviewing the `slot_info` for a particular player ID prior to lookup.
+ * For example, a data package like this is valid (Some properties such as `checksum` were omitted):
+ ```json
+ {
+ "games": {
+ "Game A": {
+ "location_name_to_id": {
+ "Boss Chest": 40
+ },
+ "item_name_to_id": {
+ "Item X": 12
+ }
+ },
+ "Game B": {
+ "location_name_to_id": {
+ "Minigame Prize": 40
+ },
+ "item_name_to_id": {
+ "Item X": 40
+ }
+ }
+ }
+ }
+ ```
#### Contents
| Name | Type | Notes |
@@ -655,7 +699,6 @@ GameData is a **dict** but contains these keys and values. It's broken out into
|---------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------|
| item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. |
| location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. |
-| version | int | Version number of this game's data. Deprecated. Used by older clients to request an updated datapackage if cache is outdated. |
| checksum | str | A checksum hash of this game's data. |
### Tags
@@ -671,8 +714,8 @@ Tags are represented as a list of strings, the common Client tags follow:
### DeathLink
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
-| Name | Type | Notes |
-| ---- | ---- | ---- |
-| time | float | Unix Time Stamp of time of death. |
-| cause | str | Optional. Text to explain the cause of death, ex. "Berserker was run over by a train." |
-| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
+| Name | Type | Notes |
+|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
+| time | float | Unix Time Stamp of time of death. |
+| cause | str | Optional. Text to explain the cause of death. When provided, or checked, this should contain the player name, ex. "Berserker was run over by a train." |
+| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
diff --git a/docs/options api.md b/docs/options api.md
index fdabd9facd8a..7e479809ee6a 100644
--- a/docs/options api.md
+++ b/docs/options api.md
@@ -10,10 +10,9 @@ Archipelago will be abbreviated as "AP" from now on.
## Option Definitions
Option parsing in AP is done using different Option classes. For each option you would like to have in your game, you
need to create:
-- A new option class with a docstring detailing what the option will do to your user.
-- A `display_name` to be displayed on the webhost.
-- A new entry in the `option_definitions` dict for your World.
-By style and convention, the internal names should be snake_case.
+- A new option class, with a docstring detailing what the option does, to be exposed to the user.
+- A new entry in the `options_dataclass` definition for your World.
+By style and convention, the dataclass attributes should be `snake_case`.
### Option Creation
- If the option supports having multiple sub_options, such as Choice options, these can be defined with
@@ -24,53 +23,202 @@ display as `Value1` on the webhost.
(i.e. `alias_value_1 = option_value1`) which will allow users to use either `value_1` or `value1` in their yaml
files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a
Choice, and defining `alias_true = option_full`.
-- All options support `random` as a generic option. `random` chooses from any of the available values for that option,
-and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
+- All options with a fixed set of possible values (i.e. those which inherit from `Toggle`, `(Text)Choice` or
+`(Named/Special)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
+option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
+However, you can override `from_text` and handle `text == "random"` to customize its behavior or
+implement it for additional option types.
-As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's
-create our option class (with a docstring), give it a `display_name`, and add it to a dictionary that keeps track of our
-options:
+As an example, suppose we want an option that lets the user start their game with a sword in their inventory, an option
+to let the player choose the difficulty, and an option to choose how much health the final boss has. Let's create our
+option classes (with a docstring), give them a `display_name`, and add them to our game's options dataclass:
```python
-# Options.py
+# options.py
+from dataclasses import dataclass
+
+from Options import Toggle, Range, Choice, PerGameCommonOptions
+
+
class StartingSword(Toggle):
"""Adds a sword to your starting inventory."""
- display_name = "Start With Sword"
+ display_name = "Start With Sword" # this is the option name as it's displayed to the user on the webhost and in the spoiler log
-example_options = {
- "starting_sword": StartingSword
-}
+class Difficulty(Choice):
+ """Sets overall game difficulty."""
+ display_name = "Difficulty"
+ option_easy = 0
+ option_normal = 1
+ option_hard = 2
+ alias_beginner = 0 # same as easy but allows the player to use beginner as an alternative for easy in the result in their options
+ alias_expert = 2 # same as hard
+ default = 1 # default to normal
+
+
+class FinalBossHP(Range):
+ """Sets the HP of the final boss"""
+ display_name = "Final Boss HP"
+ range_start = 100
+ range_end = 10000
+ default = 2000
+
+
+@dataclass
+class ExampleGameOptions(PerGameCommonOptions):
+ starting_sword: StartingSword
+ difficulty: Difficulty
+ final_boss_health: FinalBossHP
```
-This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it
-to our world's `__init__.py`:
+To then submit this to the multiworld, we add it to our world's `__init__.py`:
```python
from worlds.AutoWorld import World
-from .Options import options
+from .Options import ExampleGameOptions
class ExampleWorld(World):
- option_definitions = options
+ # this gives the generator all the definitions for our options
+ options_dataclass = ExampleGameOptions
+ # this gives us typing hints for all the options we defined
+ options: ExampleGameOptions
+```
+
+### Option Documentation
+
+Options' [docstrings] are used as their user-facing documentation. They're displayed on the WebHost setup page when a
+user hovers over the yellow "(?)" icon, and included in the YAML templates generated for each game.
+
+[docstrings]: /docs/world%20api.md#docstrings
+
+The WebHost can display Option documentation either as plain text with all whitespace preserved (other than the base
+indentation), or as HTML generated from the standard Python [reStructuredText] format. Although plain text is the
+default for backwards compatibility, world authors are encouraged to write their Option documentation as
+reStructuredText and enable rich text rendering by setting `World.rich_text_options_doc = True`.
+
+[reStructuredText]: https://docutils.sourceforge.io/rst.html
+
+```python
+from worlds.AutoWorld import WebWorld
+
+
+class ExampleWebWorld(WebWorld):
+ # Render all this world's options as rich text.
+ rich_text_options_doc = True
+```
+
+You can set a single option to use rich or plain text by setting
+`Option.rich_text_doc`.
+
+```python
+from Options import Toggle, Range, Choice, PerGameCommonOptions
+
+
+class Difficulty(Choice):
+ """Sets overall game difficulty.
+
+ - **Easy:** All enemies die in one hit.
+ - **Normal:** Enemies and the player both have normal health bars.
+ - **Hard:** The player dies in one hit."""
+ display_name = "Difficulty"
+ rich_text_doc = True
+ option_easy = 0
+ option_normal = 1
+ option_hard = 2
+ default = 1
+```
+
+### Option Groups
+Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified
+by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment
+with the group name at the beginning of each group of options. The `start_collapsed` Boolean only affects how the groups
+appear on the WebHost, with the grouping being collapsed when this is `True`.
+
+Options without a group name are categorized into a generic "Game Options" group, which is always the first group. If
+every option for your world is in a group, this group will be removed. There is also an "Items & Location Options"
+group, which is automatically created using certain specified `item_and_loc_options`. These specified options cannot be
+removed from this group.
+
+Both the "Game Options" and "Item & Location Options" groups can be overridden by creating your own groups with
+those names, letting you add options to them and change whether they start collapsed. The "Item &
+Location Options" group can also be moved to a different position in the group ordering, but "Game Options" will always
+be first, regardless of where it is in your list.
+
+```python
+from worlds.AutoWorld import WebWorld
+from Options import OptionGroup
+from . import Options
+
+class MyWorldWeb(WebWorld):
+ option_groups = [
+ OptionGroup("Color Options", [
+ Options.ColorblindMode,
+ Options.FlashReduction,
+ Options.UIColors,
+ ]),
+ ]
```
### Option Checking
Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after
world instantiation. These are created as attributes on the MultiWorld and can be accessed with
-`self.multiworld.my_option_name[self.player]`. This is the option class, which supports direct comparison methods to
+`self.options.my_option_name`. This is an instance of the option class, which supports direct comparison methods to
relevant objects (like comparing a Toggle class to a `bool`). If you need to access the option result directly, this is
the option class's `value` attribute. For our example above we can do a simple check:
```python
-if self.multiworld.starting_sword[self.player]:
+if self.options.starting_sword:
do_some_things()
```
or if I need a boolean object, such as in my slot_data I can access it as:
```python
-start_with_sword = bool(self.multiworld.starting_sword[self.player].value)
+start_with_sword = bool(self.options.starting_sword.value)
+```
+All numeric options (i.e. Toggle, Choice, Range) can be compared to integers, strings that match their attributes,
+strings that match the option attributes after "option_" is stripped, and the attributes themselves. The option can
+also be checked to see if it exists within a collection, but this will fail for a set of strings due to hashing.
+```python
+# options.py
+class Logic(Choice):
+ option_normal = 0
+ option_hard = 1
+ option_challenging = 2
+ option_extreme = 3
+ option_insane = 4
+ alias_extra_hard = 2
+ crazy = 4 # won't be listed as an option and only exists as an attribute on the class
+
+class Weapon(Choice):
+ option_none = 0
+ option_sword = 1
+ option_bow = 2
+ option_hammer = 3
+
+# __init__.py
+from .options import Logic
+
+if self.options.logic:
+ do_things_for_all_non_normal_logic()
+if self.options.logic == 1:
+ do_hard_things()
+elif self.options.logic == "challenging":
+ do_challenging_things()
+elif self.options.logic == Logic.option_extreme:
+ do_extreme_things()
+elif self.options.logic == "crazy":
+ do_insane_things()
+
+# check if the current option is in a collection of integers using the class attributes
+if self.options.weapon in {Weapon.option_bow, Weapon.option_sword}:
+ do_stuff()
+# in order to make a set of strings work, we have to compare against current_key
+elif self.options.weapon.current_key in {"none", "hammer"}:
+ do_something_else()
+# though it's usually better to just use a tuple instead
+elif self.options.weapon in ("none", "hammer"):
+ do_something_else()
```
-
## Generic Option Classes
These options are generically available to every game automatically, but can be overridden for slightly different
behavior, if desired. See `worlds/soe/Options.py` for an example.
@@ -99,10 +247,12 @@ Gives the player starting hints for where the items defined here are.
Gives the player starting hints for the items on locations defined here.
### ExcludeLocations
-Marks locations given here as `LocationProgressType.Excluded` so that progression items can't be placed on them.
+Marks locations given here as `LocationProgressType.Excluded` so that neither progression nor useful items can be
+placed on them.
### PriorityLocations
-Marks locations given here as `LocationProgressType.Priority` forcing progression items on them.
+Marks locations given here as `LocationProgressType.Priority` forcing progression items on them if any are available in
+the pool.
### ItemLinks
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between
@@ -120,7 +270,7 @@ Like Toggle, but 1 (true) is the default value.
A numeric option allowing you to define different sub options. Values are stored as integers, but you can also do
comparison methods with the class and strings, so if you have an `option_early_sword`, this can be compared with:
```python
-if self.multiworld.sword_availability[self.player] == "early_sword":
+if self.options.sword_availability == "early_sword":
do_early_sword_things()
```
@@ -128,7 +278,7 @@ or:
```python
from .Options import SwordAvailability
-if self.multiworld.sword_availability[self.player] == SwordAvailability.option_early_sword:
+if self.options.sword_availability == SwordAvailability.option_early_sword:
do_early_sword_things()
```
@@ -137,13 +287,20 @@ A numeric option allowing a variety of integers including the endpoints. Has a d
`range_end` of 1. Allows for negative values as well. This will always be an integer and has no methods for string
comparisons.
-### SpecialRange
+### NamedRange
Like range but also allows you to define a dictionary of special names the user can use to equate to a specific value.
+`special_range_names` can be used to
+- give descriptive names to certain values from within the range
+- add option values above or below the regular range, to be associated with a special meaning
+
For example:
```python
-special_range_names: {
+range_start = 1
+range_end = 99
+special_range_names = {
"normal": 20,
"extreme": 99,
+ "unlimited": -1,
}
```
@@ -160,7 +317,7 @@ within the world.
Like choice allows you to predetermine options and has all of the same comparison methods and handling. Also accepts any
user defined string as a valid option, so will either need to be validated by adding a validation step to the option
class or within world, if necessary. Value for this class is `Union[str, int]` so if you need the value at a specified
-point, `self.multiworld.my_option[self.player].current_key` will always return a string.
+point, `self.options.my_option.current_key` will always return a string.
### PlandoBosses
An option specifically built for handling boss rando, if your game can use it. Is a subclass of TextChoice so supports
diff --git a/docs/running from source.md b/docs/running from source.md
index c0f4bf580227..34083a603d1b 100644
--- a/docs/running from source.md
+++ b/docs/running from source.md
@@ -8,7 +8,7 @@ use that version. These steps are for developers or platforms without compiled r
What you'll need:
* [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
- * **Python 3.11 does not work currently**
+ * **Python 3.12 is currently unsupported**
* pip: included in downloads from python.org, separate in many Linux distributions
* Matching C compiler
* possibly optional, read operating system specific sections
@@ -17,20 +17,21 @@ Then run any of the starting point scripts, like Generate.py, and the included M
required modules and after pressing enter proceed to install everything automatically.
After this, you should be able to run the programs.
+ * `Launcher.py` gives access to many components, including clients registered in `worlds/LauncherComponents.py`.
+ * The Launcher button "Generate Template Options" will generate default yamls for all worlds.
* With yaml(s) in the `Players` folder, `Generate.py` will generate the multiworld archive.
* `MultiServer.py`, with the filename of the generated archive as a command line parameter, will host the multiworld locally.
* `--log_network` is a command line parameter useful for debugging.
* `WebHost.py` will host the website on your computer.
* You can copy `docs/webhost configuration sample.yaml` to `config.yaml`
to change WebHost options (like the web hosting port number).
- * As a side effect, `WebHost.py` creates the template yamls for all the games in `WebHostLib/static/generated`.
## Windows
Recommended steps
* Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads)
- * **Python 3.11 does not work currently**
+ * **Python 3.12 is currently unsupported**
* **Optional**: Download and install Visual Studio Build Tools from
[Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/).
diff --git a/docs/settings api.md b/docs/settings api.md
index f9cbe5e021cc..bfc642d4b50c 100644
--- a/docs/settings api.md
+++ b/docs/settings api.md
@@ -1,7 +1,7 @@
# Archipelago Settings API
The settings API describes how to use installation-wide config and let the user configure them, like paths, etc. using
-host.yaml. For the player settings / player yamls see [options api.md](options api.md).
+host.yaml. For the player options / player yamls see [options api.md](options api.md).
The settings API replaces `Utils.get_options()` and `Utils.get_default_options()`
as well as the predefined `host.yaml` in the repository.
@@ -121,6 +121,10 @@ Path to a single file. Automatically resolves as user_path:
Source folder or AP install path on Windows. ~/Archipelago for the AppImage.
Will open a file browser if the file is missing when in GUI mode.
+If the file is used in the world's `generate_output`, make sure to add a `stage_assert_generate` that checks if the
+file is available, otherwise generation may fail at the very end.
+See also [world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md#generation).
+
#### class method validate(cls, path: str)
Override this and raise ValueError if validation fails.
diff --git a/docs/style.md b/docs/style.md
index 4cc8111425e3..81853f41725b 100644
--- a/docs/style.md
+++ b/docs/style.md
@@ -6,7 +6,6 @@
* 120 character per line for all source files.
* Avoid white space errors like trailing spaces.
-
## Python Code
* We mostly follow [PEP8](https://peps.python.org/pep-0008/). Read below to see the differences.
@@ -18,9 +17,19 @@
* Use type annotations where possible for function signatures and class members.
* Use type annotations where appropriate for local variables (e.g. `var: List[int] = []`, or when the
type is hard or impossible to deduce.) Clear annotations help developers look up and validate API calls.
+* If a line ends with an open bracket/brace/parentheses, the matching closing bracket should be at the
+ beginning of a line at the same indentation as the beginning of the line with the open bracket.
+ ```python
+ stuff = {
+ x: y
+ for x, y in thing
+ if y > 2
+ }
+ ```
+* New classes, attributes, and methods in core code should have docstrings that follow
+ [reST style](https://peps.python.org/pep-0287/).
* Worlds that do not follow PEP8 should still have a consistent style across its files to make reading easier.
-
## Markdown
* We almost follow [Google's styleguide](https://google.github.io/styleguide/docguide/style.html).
@@ -30,20 +39,17 @@
* One space between bullet/number and text.
* No lazy numbering.
-
## HTML
* Indent with 2 spaces for new code.
* kebab-case for ids and classes.
-
## CSS
* Indent with 2 spaces for new code.
* `{` on the same line as the selector.
* No space between selector and `{`.
-
## JS
* Indent with 2 spaces.
diff --git a/docs/tests.md b/docs/tests.md
new file mode 100644
index 000000000000..7a3531f0f84f
--- /dev/null
+++ b/docs/tests.md
@@ -0,0 +1,90 @@
+# Archipelago Unit Testing API
+
+This document covers some of the generic tests available using Archipelago's unit testing system, as well as some basic
+steps on how to write your own.
+
+## Generic Tests
+
+Some generic tests are run on every World to ensure basic functionality with default options. These basic tests can be
+found in the [general test directory](/test/general).
+
+## Defining World Tests
+
+In order to run tests from your world, you will need to create a `test` package within your world package. This can be
+done by creating a `test` directory with a file named `__init__.py` inside it inside your world. By convention, a base
+for your world tests can be created in this file that you can then import into other modules.
+
+### WorldTestBase
+
+In order to test basic functionality of varying options, as well as to test specific edge cases or that certain
+interactions in the world interact as expected, you will want to use the [WorldTestBase](/test/bases.py). This class
+comes with the basics for test setup as well as a few preloaded tests that most worlds might want to check on varying
+options combinations.
+
+Example `/worlds//test/__init__.py`:
+
+```python
+from test.bases import WorldTestBase
+
+
+class MyGameTestBase(WorldTestBase):
+ game = "My Game"
+```
+
+The basic tests that WorldTestBase comes with include `test_all_state_can_reach_everything`,
+`test_empty_state_can_reach_something`, and `test_fill`. These test that with all collected items everything is
+reachable, with no collected items at least something is reachable, and that a valid multiworld can be completed with
+all steps being called, respectively.
+
+### Writing Tests
+
+#### Using WorldTestBase
+
+Adding runs for the basic tests for a different option combination is as easy as making a new module in the test
+package, creating a class that inherits from your game's TestBase, and defining the options in a dict as a field on the
+class. The new module should be named `test_.py` and have at least one class inheriting from the base, or
+define its own testing methods. Newly defined test methods should follow standard PEP8 snake_case format and also start
+with `test_`.
+
+Example `/worlds//test/test_chest_access.py`:
+
+```python
+from . import MyGameTestBase
+
+
+class TestChestAccess(MyGameTestBase):
+ options = {
+ "difficulty": "easy",
+ "final_boss_hp": 4000,
+ }
+
+ def test_sword_chests(self) -> None:
+ """Test locations that require a sword"""
+ locations = ["Chest1", "Chest2"]
+ items = [["Sword"]]
+ # This tests that the provided locations aren't accessible without the provided items, but can be accessed once
+ # the items are obtained.
+ # This will also check that any locations not provided don't have the same dependency requirement.
+ # Optionally, passing only_check_listed=True to the method will only check the locations provided.
+ self.assertAccessDependency(locations, items)
+```
+
+When tests are run, this class will create a multiworld with a single player having the provided options, and run the
+generic tests, as well as the new custom test. Each test method definition will create its own separate solo multiworld
+that will be cleaned up after. If you don't want to run the generic tests on a base, `run_default_tests` can be
+overridden. For more information on what methods are available to your class, check the
+[WorldTestBase definition](/test/bases.py#L104).
+
+#### Alternatives to WorldTestBase
+
+Unit tests can also be created using [TestBase](/test/bases.py#L14) or
+[unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) depending on your use case. These
+may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for
+testing portions of your code that can be tested without relying on a multiworld to be created first.
+
+## Running Tests
+
+In PyCharm, running all tests can be done by right-clicking the root `test` directory and selecting `run Python tests`.
+If you do not have pytest installed, you may get import failures. To solve this, edit the run configuration, and set the
+working directory of the run to the Archipelago directory. If you only want to run your world's defined tests, repeat
+the steps for the test directory within your world.
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/docs/webhost configuration sample.yaml b/docs/webhost configuration sample.yaml
index 70050b0590d6..afb87b399643 100644
--- a/docs/webhost configuration sample.yaml
+++ b/docs/webhost configuration sample.yaml
@@ -1,4 +1,4 @@
-# This is a sample configuration for the Web host.
+# This is a sample configuration for the Web host.
# If you wish to change any of these, rename this file to config.yaml
# Default values are shown here. Uncomment and change the values as desired.
@@ -25,7 +25,7 @@
# Secret key used to determine important things like cookie authentication of room/seed page ownership.
# If you wish to deploy, uncomment the following line and set it to something not easily guessable.
-# SECRET_KEY: "Your secret key here"
+# SECRET_KEY: "Your secret key here"
# TODO
#JOB_THRESHOLD: 2
@@ -38,15 +38,16 @@
# provider: "sqlite"
# filename: "ap.db3" # This MUST be the ABSOLUTE PATH to the file.
# create_db: true
-
+
# Maximum number of players that are allowed to be rolled on the server. After this limit, one should roll locally and upload the results.
#MAX_ROLL: 20
# TODO
#CACHE_TYPE: "simple"
-# TODO
-#JSON_AS_ASCII: false
-
# Host Address. This is the address encoded into the patch that will be used for client auto-connect.
#HOST_ADDRESS: archipelago.gg
+
+# Asset redistribution rights. If true, the host affirms they have been given explicit permission to redistribute
+# the proprietary assets in WebHostLib
+#ASSET_RIGHTS: false
diff --git a/docs/world api.md b/docs/world api.md
index 7a7f37b17ce4..6551f2260416 100644
--- a/docs/world api.md
+++ b/docs/world api.md
@@ -1,119 +1,231 @@
# Archipelago API
-This document tries to explain some internals required to implement a game for
-Archipelago's generation and server. Once a seed is generated, a client or mod is
-required to send and receive items between the game and server.
+This document tries to explain some aspects of the Archipelago World API used when implementing the generation logic of
+a game.
-Client implementation is out of scope of this document. Please refer to an
-existing game that provides a similar API to yours.
-Refer to the following documents as well:
-- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md)
-- [adding games.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/adding%20games.md)
+Client implementation is out of scope of this document. Please refer to an existing game that provides a similar API to
+yours, and the following documents:
-Archipelago will be abbreviated as "AP" from now on.
+* [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md)
+* [adding games.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/adding%20games.md)
+Archipelago will be abbreviated as "AP" from now on.
## Language
AP worlds are written in python3.
-Clients that connect to the server to sync items can be in any language that
-allows using WebSockets.
-
+Clients that connect to the server to sync items can be in any language that allows using WebSockets.
## Coding style
-AP follows [style.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/style.md).
-When in doubt use an IDE with coding style linter, for example PyCharm Community Edition.
-
+AP follows a [style guide](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/style.md).
+When in doubt, use an IDE with a code-style linter, for example PyCharm Community Edition.
## Docstrings
-Docstrings are strings attached to an object in Python that describe what the
-object is supposed to be. Certain docstrings will be picked up and used by AP.
-They are assigned by writing a string without any assignment right below a
-definition. The string must be a triple-quoted string.
+Docstrings are strings attached to an object in Python that describe what the object is supposed to be. Certain
+docstrings will be picked up and used by AP. They are assigned by writing a string without any assignment right below a
+definition. The string must be a triple-quoted string, and should
+follow [reST style](https://peps.python.org/pep-0287/).
+
Example:
+
```python
from worlds.AutoWorld import World
+
+
class MyGameWorld(World):
- """This is the description of My Game that will be displayed on the AP
- website."""
+ """This is the description of My Game that will be displayed on the AP website."""
```
-
## Definitions
-This section will cover various classes and objects you can use for your world.
-While some of the attributes and methods are mentioned here, not all of them are,
-but you can find them in `BaseClasses.py`.
+This section covers various classes and objects you can use for your world. While some of the attributes and methods
+are mentioned here, not all of them are, but you can find them in
+[`BaseClasses.py`](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py).
### World Class
-A `World` class is the class with all the specifics of a certain game to be
-included. It will be instantiated for each player that rolls a seed for that
-game.
+A `World` is the class with all the specifics of a certain game that is to be included. A new instance will be created
+for each player of the game for any given generated multiworld.
### WebWorld Class
-A `WebWorld` class contains specific attributes and methods that can be modified
-for your world specifically on the webhost:
+A `WebWorld` class contains specific attributes and methods that can be modified for your world specifically on the
+webhost:
+
+* `options_page` can be changed to a link instead of an AP-generated options page.
+
+* `rich_text_options_doc` controls whether [Option documentation] uses plain text (`False`) or rich text (`True`). It
+ defaults to `False`, but world authors are encouraged to set it to `True` for nicer-looking documentation that looks
+ good on both the WebHost and the YAML template.
-`settings_page`, which can be changed to a link instead of an AP generated settings page.
+ [Option documentation]: /docs/options%20api.md#option-documentation
-`theme` to be used for your game specific AP pages. Available themes:
+* `theme` to be used for your game-specific AP pages. Available themes:
-| dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone |
-|---|---|---|---|---|---|---|---|
-| | | | | | | | |
+ | dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone |
+ |--------------------------------------------|---------------------------------------------|----------------------------------------------------|-------------------------------------------|----------------------------------------------|---------------------------------------------|-------------------------------------------------|---------------------------------------------|
+ | | | | | | | | |
-`bug_report_page` (optional) can be a link to a bug reporting page, most likely a GitHub issue page, that will be placed by the site to help direct users to report bugs.
+* `bug_report_page` (optional) can be a link to a bug reporting page, most likely a GitHub issue page, that will be
+ placed by the site to help users report bugs.
-`tutorials` list of `Tutorial` classes where each class represents a guide to be generated on the webhost.
+* `tutorials` list of `Tutorial` classes where each class represents a guide to be generated on the webhost.
+
+* `game_info_languages` (optional) list of strings for defining the existing game info pages your game supports. The
+ documents must be prefixed with the same string as defined here. Default already has 'en'.
+
+* `options_presets` (optional) `Dict[str, Dict[str, Any]]` where the keys are the names of the presets and the values
+ are the options to be set for that preset. The options are defined as a `Dict[str, Any]` where the keys are the names
+ of the options and the values are the values to be set for that option. These presets will be available for users to
+ select from on the game's options page.
+
+Note: The values must be a non-aliased value for the option type and can only include the following option types:
+
+* If you have a `Range`/`NamedRange` option, the value should be an `int` between the `range_start` and `range_end`
+ values.
+ * If you have a `NamedRange` option, the value can alternatively be a `str` that is one of the
+ `special_range_names` keys.
+* If you have a `Choice` option, the value should be a `str` that is one of the `option_` values.
+* If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`.
+* `random` is also a valid value for any of these option types.
+
+`OptionDict`, `OptionList`, `OptionSet`, `FreeText`, or custom `Option`-derived classes are not supported for presets on
+the webhost at this time.
+
+Here is an example of a defined preset:
+
+```python
+# presets.py
+options_presets = {
+ "Limited Potential": {
+ "progression_balancing": 0,
+ "fairy_chests_per_zone": 2,
+ "starting_class": "random",
+ "chests_per_zone": 30,
+ "vendors": "normal",
+ "architect": "disabled",
+ "gold_gain_multiplier": "half",
+ "number_of_children": 2,
+ "free_diary_on_generation": False,
+ "health_pool": 10,
+ "mana_pool": 10,
+ "attack_pool": 10,
+ "magic_damage_pool": 10,
+ "armor_pool": 5,
+ "equip_pool": 10,
+ "crit_chance_pool": 5,
+ "crit_damage_pool": 5,
+ }
+}
-`game_info_languages` (optional) List of strings for defining the existing gameinfo pages your game supports. The documents must be
-prefixed with the same string as defined here. Default already has 'en'.
+
+# __init__.py
+class RLWeb(WebWorld):
+ options_presets = options_presets
+ # ...
+```
+
+* `location_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of locations
+or location groups.
+
+ ```python
+ # locations.py
+ location_descriptions = {
+ "Red Potion #6": "In a secret destructible block under the second stairway",
+ "L2 Spaceship": """
+ The group of all items in the spaceship in Level 2.
+
+ This doesn't include the item on the spaceship door, since it can be
+ accessed without the Spaceship Key.
+ """
+ }
+
+ # __init__.py
+ from worlds.AutoWorld import WebWorld
+ from .locations import location_descriptions
+
+
+ class MyGameWeb(WebWorld):
+ location_descriptions = location_descriptions
+ ```
+
+* `item_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of items or item
+groups.
+
+ ```python
+ # items.py
+ item_descriptions = {
+ "Red Potion": "A standard health potion",
+ "Spaceship Key": """
+ The key to the spaceship in Level 2.
+
+ This is necessary to get to the Star Realm.
+ """,
+ }
+
+ # __init__.py
+ from worlds.AutoWorld import WebWorld
+ from .items import item_descriptions
+
+
+ class MyGameWeb(WebWorld):
+ item_descriptions = item_descriptions
+ ```
### MultiWorld Object
-The `MultiWorld` object references the whole multiworld (all items and locations
-for all players) and is accessible through `self.multiworld` inside a `World` object.
+The `MultiWorld` object references the whole multiworld (all items and locations for all players) and is accessible
+through `self.multiworld` from your `World` object.
### Player
-The player is just an integer in AP and is accessible through `self.player`
-inside a `World` object.
+The player is just an `int` in AP and is accessible through `self.player` from your `World` object.
### Player Options
-Players provide customized settings for their World in the form of yamls.
-Those are accessible through `self.multiworld.[self.player]`. A dict
-of valid options has to be provided in `self.option_definitions`. Options are automatically
-added to the `World` object for easy access.
+Options are provided by the user as part of the generation process, intended to affect how their randomizer experience
+should play out. These can control aspects such as what locations should be shuffled, what items are in the itempool,
+etc. Players provide the customized options for their World in the form of yamls.
+
+By convention, options are defined in `options.py` and will be used when parsing the players' yaml files. Each option
+has its own class, which inherits from a base option type, a docstring to describe it, and a `display_name` property
+shown on the website and in spoiler logs.
+
+The available options are defined by creating a `dataclass`, which must be a subclass of `PerGameCommonOptions`. It has
+defined fields for the option names used in the player yamls and used for options access, with their types matching the
+appropriate Option class. By convention, the strings that define your option names should be in `snake_case`. The
+`dataclass` is then assigned to your `World` by defining its `options_dataclass`. Option results are then automatically
+added to the `World` object for easy access, between `World` creation and `generate_early`. These are accessible through
+`self.options.`, and you can get a dictionary with option values
+via `self.options.as_dict()`,
+passing the desired option names as strings.
+
+Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, and `Range`.
+For more information, see the [options api doc](options%20api.md).
### World Settings
-Any AP installation can provide settings for a world, for example a ROM file, accessible through
-`self.settings.` or `cls.settings.` (new API)
-or `Utils.get_options()["_options"][""]` (deprecated).
+Settings are set by the user outside the generation process. They can be used for those settings that may affect
+generation or client behavior, but should remain static between generations, such as the path to a ROM file.
+These settings are accessible through `self.settings.` or `cls.settings.`.
-Users can set those in their `host.yaml` file. Some settings may automatically open a file browser if a file is missing.
+Users can set these in their `host.yaml` file. Some settings may automatically open a file browser if a file is missing.
-Refer to [settings api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/settings%20api.md)
-for details.
+Refer to [settings api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/settings%20api.md) for details.
### Locations
-Locations are places where items can be located in your game. This may be chests
-or boss drops for RPG-like games but could also be progress in a research tree.
+Locations are places where items can be located in your game. This may be chests or boss drops for RPG-like games, but
+could also be progress in a research tree, or even something more abstract like a level up.
-Each location has a `name` and an `id` (a.k.a. "code" or "address"), is placed
-in a Region, has access rules and a classification.
-The name needs to be unique in each game and must not be numeric (has to
-contain least 1 letter or symbol). The ID needs to be unique across all games
-and is best in the same range as the item IDs.
-World-specific IDs are 1 to 253 -1, IDs ≤ 0 are global and reserved.
+Each location has a `name` and an `address` (hereafter referred to as an `id`), is placed in a Region, has access rules,
+and has a classification. The name needs to be unique within each game and must not be numeric (must contain least 1
+letter or symbol). The ID needs to be unique across all games, and is best kept in the same range as the item IDs.
+Locations and items can share IDs, so typically a game's locations and items start at the same ID.
-Special locations with ID `None` can hold events.
+World-specific IDs must be in the range 1 to 253 -1; IDs ≤ 0 are global and reserved.
Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED`.
The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being
@@ -121,236 +233,147 @@ required, and will prevent progression and useful items from being placed at exc
### Items
-Items are all things that can "drop" for your game. This may be RPG items like
-weapons, could as well be technologies you normally research in a research tree.
+Items are all things that can "drop" for your game. This may be RPG items like weapons, or technologies you normally
+research in a research tree.
-Each item has a `name`, an `id` (can be known as "code"), and a classification.
-The most important classification is `progression` (formerly advancement).
-Progression items are items which a player may require to progress in
-their world. Progression items will be assigned to locations with higher
-priority and moved around to meet defined rules and accomplish progression
-balancing.
+Each item has a `name`, a `code` (hereafter referred to as `id`), and a classification.
+The most important classification is `progression`. Progression items are items which a player *may* require to progress
+in their world. If an item can possibly be considered for logic (it's referenced in a location's rules) it *must* be
+progression. Progression items will be assigned to locations with higher priority, and moved around to meet defined rules
+and satisfy progression balancing.
-The name needs to be unique in each game, meaning a duplicate item has the
-same ID. Name must not be numeric (has to contain at least 1 letter or symbol).
+The name needs to be unique within each game, meaning if you need to create multiple items with the same name, they
+will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol).
-Special items with ID `None` can mark events (read below).
+Other classifications include:
-Other classifications include
* `filler`: a regular item or trash item
-* `useful`: generally quite useful, but not required for anything logical
+* `useful`: generally quite useful, but not required for anything logical. Cannot be placed on excluded locations
* `trap`: negative impact on the player
* `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be
combined with `progression`; see below)
* `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that
- will not be moved around by progression balancing; used, e.g., for currency or tokens
+ will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres
### Events
-Events will mark some progress. You define an event location, an
-event item, strap some rules to the location (i.e. hold certain
-items) and manually place the event item at the event location.
+An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to
+track certain logic interactions, with the Event Item being required for access in other locations or regions, but not
+being "real". Since the item and location have no ID, they get dropped at the end of generation and so the server is
+never made aware of them and these locations can never be checked, nor can the items be received during play.
+They may also be used for making the spoiler log look nicer, i.e. by having a `"Victory"` Event Item, that
+is required to finish the game. This makes it very clear when the player finishes, rather than only seeing their last
+relevant Item. Events function just like any other Location, and can still have their own access rules, etc.
+By convention, the Event "pair" of Location and Item typically have the same name, though this is not a requirement.
+They must not exist in the `name_to_id` lookups, as they have no ID.
-Events can be used to either simplify the logic or to get better spoiler logs.
-Events will show up in the spoiler playthrough but they do not represent actual
-items or locations within the game.
+The most common way to create an Event pair is to create and place the Item on the Location as soon as it's created:
+
+```python
+from worlds.AutoWorld import World
+from BaseClasses import ItemClassification
+from .subclasses import MyGameLocation, MyGameItem
-There is one special case for events: Victory. To get the win condition to show
-up in the spoiler log, you create an event item and place it at an event
-location with the `access_rules` for game completion. Once that's done, the
-world's win condition can be as simple as checking for that item.
-By convention the victory event is called `"Victory"`. It can be placed at one
-or more event locations based on player options.
+class MyGameWorld(World):
+ victory_loc = MyGameLocation(self.player, "Victory", None)
+ victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player))
+```
### Regions
-Regions are logical groups of locations that share some common access rules. If
-location logic is written from scratch, using regions greatly simplifies the
-definition and allows to somewhat easily implement things like entrance
-randomizer in logic.
+Regions are logical containers that typically hold locations that share some common access rules. If location logic is
+written from scratch, using regions greatly simplifies the requirements and can help with implementing things
+like entrance randomization in logic.
-Regions have a list called `exits`, which are `Entrance` objects representing
-transitions to other regions.
+Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
-There has to be one special region "Menu" from which the logic unfolds. AP
-assumes that a player will always be able to return to the "Menu" region by
-resetting the game ("Save and quit").
+There must be one special region, "Menu", from which the logic unfolds. AP assumes that a player will always be able to
+return to the "Menu" region by resetting the game ("Save and quit").
### Entrances
-An `Entrance` connects to a region, is assigned to region's exits and has rules
-to define if it and thus the connected region is accessible.
-They can be static (regular logic) or be defined/connected during generation
-(entrance randomizer).
+An `Entrance` has a `parent_region` and `connected_region`, where it is in the `exits` of its parent, and the
+`entrances` of its connected region. The `Entrance` then has rules assigned to it to determine if it can be passed
+through, making the connected region accessible. They can be static (regular logic) or be defined/connected during
+generation (entrance randomization).
### Access Rules
-An access rule is a function that returns `True` or `False` for a `Location` or
-`Entrance` based on the current `state` (items that can be collected).
+An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state`
+(items that have been collected).
### Item Rules
-An item rule is a function that returns `True` or `False` for a `Location` based
-on a single item. It can be used to reject placement of an item there.
-
+An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
+reject the placement of an item there.
## Implementation
### Your World
-All code for your world implementation should be placed in a python package in
-the `/worlds` directory. The starting point for the package is `__init__.py`.
-Conventionally, your world class is placed in that file.
+All code for your world implementation should be placed in a python package in the `/worlds` directory. The starting
+point for the package is `__init__.py`. Conventionally, your `World` class is placed in that file.
-World classes must inherit from the `World` class in `/worlds/AutoWorld.py`,
-which can be imported as `from worlds.AutoWorld import World` from your package.
+World classes must inherit from the `World` class in `/worlds/AutoWorld.py`, which can be imported as
+`from worlds.AutoWorld import World` from your package.
AP will pick up your world automatically due to the `AutoWorld` implementation.
### Requirements
-If your world needs specific python packages, they can be listed in
-`worlds//requirements.txt`. ModuleUpdate.py will automatically
-pick up and install them.
+If your world needs specific python packages, they can be listed in `worlds//requirements.txt`.
+ModuleUpdate.py will automatically pick up and install them.
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format).
### Relative Imports
-AP will only import the `__init__.py`. Depending on code size it makes sense to
-use multiple files and use relative imports to access them.
+AP will only import the `__init__.py`. Depending on code size, it may make sense to use multiple files and use relative
+imports to access them.
-e.g. `from .Options import mygame_options` from your `__init__.py` will load
-`worlds//Options.py` and make its `mygame_options` accessible.
+e.g. `from .options import MyGameOptions` from your `__init__.py` will load `world/[world_name]/options.py` and make
+its `MyGameOptions` accessible.
-When imported names pile up it may be easier to use `from . import Options`
-and access the variable as `Options.mygame_options`.
+When imported names pile up, it may be easier to use `from . import options` and access the variable as
+`options.MyGameOptions`.
-Imports from directories outside your world should use absolute imports.
-Correct use of relative / absolute imports is required for zipped worlds to
-function, see [apworld specification.md](apworld%20specification.md).
+Imports from directories outside your world should use absolute imports. Correct use of relative / absolute imports is
+required for zipped worlds to function, see [apworld specification.md](apworld%20specification.md).
### Your Item Type
-Each world uses its own subclass of `BaseClasses.Item`. The constructor can be
-overridden to attach additional data to it, e.g. "price in shop".
-Since the constructor is only ever called from your code, you can add whatever
-arguments you like to the constructor.
+Each world uses its own subclass of `BaseClasses.Item`. The constructor can be overridden to attach additional data to
+it, e.g. "price in shop". Since the constructor is only ever called from your code, you can add whatever arguments you
+like to the constructor.
+
+In its simplest form, we only set the game name and use the default constructor:
-In its simplest form we only set the game name and use the default constructor
```python
from BaseClasses import Item
-class MyGameItem(Item):
- game: str = "My Game"
-```
-By convention this class definition will either be placed in your `__init__.py`
-or your `Items.py`. For a more elaborate example see `worlds/oot/Items.py`.
-
-### Your location type
-
-The same we have done for items above, we will do for locations
-```python
-from BaseClasses import Location
-class MyGameLocation(Location):
+class MyGameItem(Item):
game: str = "My Game"
-
- # override constructor to automatically mark event locations as such
- def __init__(self, player: int, name = "", code = None, parent = None):
- super(MyGameLocation, self).__init__(player, name, code, parent)
- self.event = code is None
```
-in your `__init__.py` or your `Locations.py`.
-
-### Options
-
-By convention options are defined in `Options.py` and will be used when parsing
-the players' yaml files.
-
-Each option has its own class, inherits from a base option type, has a docstring
-to describe it and a `display_name` property for display on the website and in
-spoiler logs.
-
-The actual name as used in the yaml is defined in a `Dict[str, AssembleOptions]`, that is
-assigned to the world under `self.option_definitions`.
-
-Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`.
-For more see `Options.py` in AP's base directory.
-
-#### Toggle, DefaultOnToggle
-Those don't need any additional properties defined. After parsing the option,
-its `value` will either be True or False.
+By convention, this class definition will either be placed in your `__init__.py` or your `items.py`. For a more
+elaborate example see
+[`worlds/oot/Items.py`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/Items.py).
-#### Range
+### Your Location Type
-Define properties `range_start`, `range_end` and `default`. Ranges will be
-displayed as sliders on the website and can be set to random in the yaml.
+The same thing we did for items above, we will now do for locations:
-#### Choice
-
-Choices are like toggles, but have more options than just True and False.
-Define a property `option_ = ` per selectable value and
-`default = ` to set the default selection. Aliases can be set by
-defining a property `alias_ = `.
-
-```python
-option_off = 0
-option_on = 1
-option_some = 2
-alias_disabled = 0
-alias_enabled = 1
-default = 0
-```
-
-#### Sample
```python
-# Options.py
+from BaseClasses import Location
-from Options import Toggle, Range, Choice, Option
-import typing
-class Difficulty(Choice):
- """Sets overall game difficulty."""
- display_name = "Difficulty"
- option_easy = 0
- option_normal = 1
- option_hard = 2
- alias_beginner = 0 # same as easy
- alias_expert = 2 # same as hard
- default = 1 # default to normal
-
-class FinalBossHP(Range):
- """Sets the HP of the final boss"""
- display_name = "Final Boss HP"
- range_start = 100
- range_end = 10000
- default = 2000
-
-class FixXYZGlitch(Toggle):
- """Fixes ABC when you do XYZ"""
- display_name = "Fix XYZ Glitch"
-
-# By convention we call the options dict variable `_options`.
-mygame_options: typing.Dict[str, AssembleOptions] = {
- "difficulty": Difficulty,
- "final_boss_hp": FinalBossHP,
- "fix_xyz_glitch": FixXYZGlitch,
-}
+class MyGameLocation(Location):
+ game: str = "My Game"
```
-```python
-# __init__.py
-from worlds.AutoWorld import World
-from .Options import mygame_options # import the options dict
-
-class MyGameWorld(World):
- #...
- option_definitions = mygame_options # assign the options dict to the world
- #...
-```
+in your `__init__.py` or your `locations.py`.
### A World Class Skeleton
@@ -359,9 +382,9 @@ class MyGameWorld(World):
import settings
import typing
-from .Options import mygame_options # the options we defined earlier
-from .Items import mygame_items # data used below to add items to the World
-from .Locations import mygame_locations # same as above
+from .options import MyGameOptions # the options we defined earlier
+from .items import mygame_items # data used below to add items to the World
+from .locations import mygame_locations # same as above
from worlds.AutoWorld import World
from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification
@@ -384,14 +407,15 @@ class MyGameSettings(settings.Group):
class MyGameWorld(World):
"""Insert description of the world/game here."""
game = "My Game" # name of the game/world
- option_definitions = mygame_options # options the player can set
+ options_dataclass = MyGameOptions # options the player can set
+ options: MyGameOptions # typing hints for option results
settings: typing.ClassVar[MyGameSettings] # will be automatically assigned from type hint
topology_present = True # show path to required location checks in spoiler
# ID of first item and location, could be hard-coded but code may be easier
# to read with this as a property.
base_id = 1234
- # Instead of dynamic numbering, IDs could be part of data.
+ # instead of dynamic numbering, IDs could be part of data
# The following two dicts are required for the generation to know which
# items exist. They could be generated from json or something else. They can
@@ -410,75 +434,109 @@ class MyGameWorld(World):
### Generation
-The world has to provide the following things for generation
+The world has to provide the following things for generation:
-* the properties mentioned above
+* the properties mentioned above
* additions to the item pool
* additions to the regions list: at least one called "Menu"
* locations placed inside those regions
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
-* applying `self.multiworld.push_precollected` for start inventory
-* `required_client_version: Tuple[int, int, int]`
- Optional client version as tuple of 3 ints to make sure the client is compatible to
- this world (e.g. implements all required features) when connecting.
+* applying `self.multiworld.push_precollected` for world-defined start inventory
-In addition, the following methods can be implemented and are called in this order during generation
+In addition, the following methods can be implemented and are called in this order during generation:
-* `stage_assert_generate(cls, multiworld)` is a class method called at the start of
- generation to check the existence of prerequisite files, usually a ROM for
+* `stage_assert_generate(cls, multiworld: MultiWorld)`
+ a class method called at the start of generation to check for the existence of prerequisite files, usually a ROM for
games which require one.
-* `def generate_early(self)`
- called per player before any items or locations are created. You can set
- properties on your world here. Already has access to player options and RNG.
-* `def create_regions(self)`
- called to place player's regions and their locations into the MultiWorld's regions list. If it's
- hard to separate, this can be done during `generate_early` or `create_items` as well.
-* `def create_items(self)`
- called to place player's items into the MultiWorld's itempool. After this step all regions and items have to be in
- the MultiWorld's regions and itempool, and these lists should not be modified afterwards.
-* `def set_rules(self)`
- called to set access and item rules on locations and entrances.
- Locations have to be defined before this, or rule application can miss them.
-* `def generate_basic(self)`
- called after the previous steps. Some placement and player specific
- randomizations can be done here.
-* `pre_fill`, `fill_hook` and `post_fill` are called to modify item placement
- before, during and after the regular fill process, before `generate_output`.
- If items need to be placed during pre_fill, these items can be determined
- and created using `get_prefill_items`
-* `def generate_output(self, output_directory: str)` that creates the output
- files if there is output to be generated. When this is
- called, `self.multiworld.get_locations(self.player)` has all locations for the player, with
- attribute `item` pointing to the item.
- `location.item.player` can be used to see if it's a local item.
-* `fill_slot_data` and `modify_multidata` can be used to modify the data that
+* `generate_early(self)`
+ called per player before any items or locations are created. You can set properties on your
+ world here. Already has access to player options and RNG. This is the earliest step where the world should start
+ setting up for the current multiworld, as the multiworld itself is still setting up before this point.
+* `create_regions(self)`
+ called to place player's regions and their locations into the MultiWorld's regions list.
+ If it's hard to separate, this can be done during `generate_early` or `create_items` as well.
+* `create_items(self)`
+ called to place player's items into the MultiWorld's itempool. By the end of this step all regions, locations and
+ items have to be in the MultiWorld's regions and itempool. You cannot add or remove items, locations, or regions
+ after this step. Locations cannot be moved to different regions after this step.
+* `set_rules(self)`
+ called to set access and item rules on locations and entrances.
+* `generate_basic(self)`
+ player-specific randomization that does not affect logic can be done here.
+* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)`
+ called to modify item placement before, during, and after the regular fill process; all finishing before
+ `generate_output`. Any items that need to be placed during `pre_fill` should not exist in the itempool, and if there
+ are any items that need to be filled this way, but need to be in state while you fill other items, they can be
+ returned from `get_prefill_items`.
+* `generate_output(self, output_directory: str)`
+ creates the output files if there is output to be generated. When this is called,
+ `self.multiworld.get_locations(self.player)` has all locations for the player, with attribute `item` pointing to the
+ item. `location.item.player` can be used to see if it's a local item.
+* `fill_slot_data(self)` and `modify_multidata(self, multidata: Dict[str, Any])` can be used to modify the data that
will be used by the server to host the MultiWorld.
+All instance methods can, optionally, have a class method defined which will be called after all instance methods are
+finished running, by defining a method with `stage_` in front of the method name. These class methods will have the
+args `(cls, multiworld: MultiWorld)`, followed by any other args that the relevant instance method has.
#### generate_early
```python
def generate_early(self) -> None:
- # read player settings to world instance
- self.final_boss_hp = self.multiworld.final_boss_hp[self.player].value
+ # read player options to world instance
+ self.final_boss_hp = self.options.final_boss_hp.value
+```
+
+#### create_regions
+
+```python
+def create_regions(self) -> None:
+ # Add regions to the multiworld. "Menu" is the required starting point.
+ # Arguments to Region() are name, player, multiworld, and optionally hint_text
+ menu_region = Region("Menu", self.player, self.multiworld)
+ self.multiworld.regions.append(menu_region) # or use += [menu_region...]
+
+ main_region = Region("Main Area", self.player, self.multiworld)
+ # add main area's locations to main area (all but final boss)
+ main_region.add_locations(main_region_locations, MyGameLocation)
+ # or
+ # main_region.locations = \
+ # [MyGameLocation(self.player, location_name, self.location_name_to_id[location_name], main_region]
+ self.multiworld.regions.append(main_region)
+
+ boss_region = Region("Boss Room", self.player, self.multiworld)
+ # add event to Boss Room
+ boss_region.locations.append(MyGameLocation(self.player, "Final Boss", None, boss_region))
+
+ # if entrances are not randomized, they should be connected here, otherwise they can also be connected at a later stage
+ # create Entrances and connect the Regions
+ menu_region.connect(main_region) # connects the "Menu" and "Main Area", can also pass a rule
+ # or
+ main_region.add_exits({"Boss Room": "Boss Door"}, {"Boss Room": lambda state: state.has("Sword", self.player)})
+ # connects the "Main Area" and "Boss Room" regions, and places a rule requiring the "Sword" item to traverse
+
+ # if setting location access rules from data is easier here, set_rules can possibly be omitted
```
#### create_item
```python
-# we need a way to know if an item provides progress in the game ("key item")
-# this can be part of the items definition, or depend on recipe randomization
-from .Items import is_progression # this is just a dummy
-
-def create_item(self, item: str):
- # This is called when AP wants to create an item by name (for plando) or
- # when you call it from your own code.
- classification = ItemClassification.progression if is_progression(item) else \
- ItemClassification.filler
- return MyGameItem(item, classification, self.item_name_to_id[item],
- self.player)
-
-def create_event(self, event: str):
+# we need a way to know if an item provides progress in the game ("key item") this can be part of the items definition,
+# or depend on recipe randomization
+from .items import is_progression # this is just a dummy
+
+
+def create_item(self, item: str) -> MyGameItem:
+ # this is called when AP wants to create an item by name (for plando) or when you call it from your own code
+ classification = ItemClassification.progression if is_progression(item) else
+ ItemClassification.filler
+
+
+return MyGameItem(item, classification, self.item_name_to_id[item],
+ self.player)
+
+
+def create_event(self, event: str) -> MyGameItem:
# while we are at it, we can also add a helper to create events
return MyGameItem(event, True, None, self.player)
```
@@ -489,8 +547,7 @@ def create_event(self, event: str):
def create_items(self) -> None:
# Add items to the Multiworld.
# If there are two of the same item, the item has to be twice in the pool.
- # Which items are added to the pool may depend on player settings,
- # e.g. custom win condition like triforce hunt.
+ # Which items are added to the pool may depend on player options, e.g. custom win condition like triforce hunt.
# Having an item in the start inventory won't remove it from the pool.
# If an item can't have duplicates it has to be excluded manually.
@@ -506,66 +563,15 @@ def create_items(self) -> None:
# itempool and number of locations should match up.
# If this is not the case we want to fill the itempool with junk.
- junk = 0 # calculate this based on player settings
+ junk = 0 # calculate this based on player options
self.multiworld.itempool += [self.create_item("nothing") for _ in range(junk)]
```
-#### create_regions
-
-```python
-def create_regions(self) -> None:
- # Add regions to the multiworld. "Menu" is the required starting point.
- # Arguments to Region() are name, player, world, and optionally hint_text
- menu_region = Region("Menu", self.player, self.multiworld)
- self.multiworld.regions.append(menu_region) # or use += [menu_region...]
-
- main_region = Region("Main Area", self.player, self.multiworld)
- # Add main area's locations to main area (all but final boss)
- main_region.add_locations(main_region_locations, MyGameLocation)
- # or
- # main_region.locations = \
- # [MyGameLocation(self.player, location_name, self.location_name_to_id[location_name], main_region]
- self.multiworld.regions.append(main_region)
-
- boss_region = Region("Boss Room", self.player, self.multiworld)
- # Add event to Boss Room
- boss_region.locations.append(MyGameLocation(self.player, "Final Boss", None, boss_region))
-
- # If entrances are not randomized, they should be connected here,
- # otherwise they can also be connected at a later stage.
- # Create Entrances and connect the Regions
- menu_region.connect(main_region) # connects the "Menu" and "Main Area", can also pass a rule
- # or
- main_region.add_exits({"Boss Room": "Boss Door"}, {"Boss Room": lambda state: state.has("Sword", self.player)})
- # Connects the "Main Area" and "Boss Room" regions, and places a rule requiring the "Sword" item to traverse
-
- # If setting location access rules from data is easier here, set_rules can
- # possibly omitted.
-```
-
-#### generate_basic
-
-```python
-def generate_basic(self) -> None:
- # place "Victory" at "Final Boss" and set collection as win condition
- self.multiworld.get_location("Final Boss", self.player)
- .place_locked_item(self.create_event("Victory"))
- self.multiworld.completion_condition[self.player] =
- lambda state: state.has("Victory", self.player)
-
- # place item Herb into location Chest1 for some reason
- item = self.create_item("Herb")
- self.multiworld.get_location("Chest1", self.player).place_locked_item(item)
- # in most cases it's better to do this at the same time the itempool is
- # filled to avoid accidental duplicates:
- # manually placed and still in the itempool
-```
-
### Setting Rules
```python
-from worlds.generic.Rules import add_rule, set_rule, forbid_item
-from Items import get_item_type
+from worlds.generic.Rules import add_rule, set_rule, forbid_item, add_item_rule
+from .items import get_item_type
def set_rules(self) -> None:
@@ -576,6 +582,7 @@ def set_rules(self) -> None:
# set a simple rule for an region
set_rule(self.multiworld.get_entrance("Boss Door", self.player),
lambda state: state.has("Boss Key", self.player))
+ # location.access_rule = ... is likely to be a bit faster
# combine rules to require two items
add_rule(self.multiworld.get_location("Chest2", self.player),
lambda state: state.has("Sword", self.player))
@@ -591,7 +598,7 @@ def set_rules(self) -> None:
# require one item from an item group
add_rule(self.multiworld.get_location("Chest3", self.player),
lambda state: state.has_group("weapons", self.player))
- # state also has .item_count() for items, .has_any() and .has_all() for sets
+ # state also has .count() for items, .has_any() and .has_all() for multiple
# and .count_group() for groups
# set_rule is likely to be a bit faster than add_rule
@@ -603,71 +610,92 @@ def set_rules(self) -> None:
# get_item_type needs to take player/world into account
# if MyGameItem has a type property, a more direct implementation would be
add_item_rule(self.multiworld.get_location("Chest5", self.player),
- lambda item: item.player != self.player or\
+ lambda item: item.player != self.player or
item.my_type == "weapon")
# location.item_rule = ... is likely to be a bit faster
-```
-### Logic Mixin
+ # place "Victory" at "Final Boss" and set collection as win condition
+ self.multiworld.get_location("Final Boss", self.player).place_locked_item(self.create_event("Victory"))
-While lambdas and events could do pretty much anything, by convention we
-implement more complex logic in logic mixins, even if there is no need to add
-properties to the `BaseClasses.CollectionState` state object.
-
-When importing a file that defines a class that inherits from
-`worlds.AutoWorld.LogicMixin` the state object's class is automatically extended by
-the mixin's members. These members should be prefixed with underscore following
-the name of the implementing world. This is due to sharing a namespace with all
-other logic mixins.
-
-Typical uses are defining methods that are used instead of `state.has`
-in lambdas, e.g.`state.mygame_has(custom, player)` or recurring checks
-like `state.mygame_can_do_something(player)` to simplify lambdas.
-Private members, only accessible from mixins, should start with `_mygame_`,
-public members with `mygame_`.
-
-More advanced uses could be to add additional variables to the state object,
-override `World.collect(self, state, item)` and `remove(self, state, item)`
-to update the state object, and check those added variables in added methods.
-Please do this with caution and only when necessary.
+ self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
-#### Sample
+# for debugging purposes, you may want to visualize the layout of your world. Uncomment the following code to
+# write a PlantUML diagram to the file "my_world.puml" that can help you see whether your regions and locations
+# are connected and placed as desired
+# from Utils import visualize_regions
+# visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
+```
+
+### Custom Logic Rules
+
+Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or
+Entrance should be
+a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L9).
+Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other
+functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly.
+For an example, see [The Messenger](/worlds/messenger/rules.py).
```python
-# Logic.py
+# logic.py
+
+from BaseClasses import CollectionState
-from worlds.AutoWorld import LogicMixin
-class MyGameLogic(LogicMixin):
- def mygame_has_key(self, player: int):
- # Arguments above are free to choose
- # MultiWorld can be accessed through self.multiworld, explicitly passing in
- # MyGameWorld instance for easy options access is also a valid approach
- return self.has("key", player) # or whatever
+def mygame_has_key(self, state: CollectionState, player: int) -> bool:
+ # More arguments above are free to choose, since you can expect this is only called in your world
+ # MultiWorld can be accessed through state.multiworld.
+ # Explicitly passing in MyGameWorld instance for easy options access is also a valid approach, but it's generally
+ # better to check options before rule assignment since the individual functions can be called thousands of times
+ return state.has("key", player) # or whatever
```
+
```python
# __init__.py
from worlds.generic.Rules import set_rule
-import .Logic # apply the mixin by importing its file
+from . import logic
+
class MyGameWorld(World):
# ...
- def set_rules(self):
+ def set_rules(self) -> None:
set_rule(self.multiworld.get_location("A Door", self.player),
- lambda state: state.mygame_has_key(self.player))
+ lambda state: logic.mygame_has_key(state, self.player))
+```
+
+### Logic Mixin
+
+While lambdas and events can do pretty much anything, more complex logic can be handled in logic mixins.
+
+When importing a file that defines a class that inherits from `worlds.AutoWorld.LogicMixin`, the `CollectionState` class
+is automatically extended by the mixin's members. These members should be prefixed with the name of the implementing
+world since the namespace is shared with all other logic mixins.
+
+Some uses could be to add additional variables to the state object, or to have a custom state machine that gets modified
+with the state.
+Please do this with caution and only when necessary.
+
+#### pre_fill
+
+```python
+def pre_fill(self) -> None:
+ # place item Herb into location Chest1 for some reason
+ item = self.create_item("Herb")
+ self.multiworld.get_location("Chest1", self.player).place_locked_item(item)
+ # in most cases it's better to do this at the same time the itempool is
+ # filled to avoid accidental duplicates, such as manually placed and still in the itempool
```
### Generate Output
```python
-from .Mod import generate_mod
+from .mod import generate_mod
-def generate_output(self, output_directory: str):
- # How to generate the mod or ROM highly depends on the game
- # if the mod is written in Lua, Jinja can be used to fill a template
- # if the mod reads a json file, `json.dump()` can be used to generate that
+def generate_output(self, output_directory: str) -> None:
+ # How to generate the mod or ROM highly depends on the game.
+ # If the mod is written in Lua, Jinja can be used to fill a template.
+ # If the mod reads a json file, `json.dump()` can be used to generate that.
# code below is a dummy
data = {
"seed": self.multiworld.seed_name, # to verify the server's multiworld
@@ -677,14 +705,11 @@ def generate_output(self, output_directory: str):
for location in self.multiworld.get_filled_locations(self.player)},
# store start_inventory from player's .yaml
# make sure to mark as not remote_start_inventory when connecting if stored in rom/mod
- "starter_items": [item.name for item
- in self.multiworld.precollected_items[self.player]],
- "final_boss_hp": self.final_boss_hp,
- # store option name "easy", "normal" or "hard" for difficuly
- "difficulty": self.multiworld.difficulty[self.player].current_key,
- # store option value True or False for fixing a glitch
- "fix_xyz_glitch": self.multiworld.fix_xyz_glitch[self.player].value,
+ "starter_items": [item.name for item in self.multiworld.precollected_items[self.player]],
}
+
+ # add needed option results to the dictionary
+ data.update(self.options.as_dict("final_boss_hp", "difficulty", "fix_xyz_glitch"))
# point to a ROM specified by the installation
src = self.settings.rom_file
# or point to worlds/mygame/data/mod_template
@@ -696,21 +721,44 @@ def generate_output(self, output_directory: str):
generate_mod(src, out_file, data)
```
+### Slot Data
+
+If the game client needs to know information about the generated seed, a preferred method of transferring the data
+is through the slot data. This is filled with the `fill_slot_data` method of your world by returning
+a `dict` with `str` keys that can be serialized with json.
+But, to not waste resources, it should be limited to data that is absolutely necessary. Slot data is sent to your client
+once it has successfully [connected](network%20protocol.md#connected).
+If you need to know information about locations in your world, instead of propagating the slot data, it is preferable
+to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. The most
+common usage of slot data is sending option results that the client needs to be aware of.
+
+```python
+def fill_slot_data(self) -> Dict[str, Any]:
+ # In order for our game client to handle the generated seed correctly we need to know what the user selected
+ # for their difficulty and final boss HP.
+ # A dictionary returned from this method gets set as the slot_data and will be sent to the client after connecting.
+ # The options dataclass has a method to return a `Dict[str, Any]` of each option name provided and the relevant
+ # option's value.
+ return self.options.as_dict("difficulty", "final_boss_hp")
+```
+
### Documentation
Each world implementation should have a tutorial and a game info page. These are both rendered on the website by reading
the `.md` files in your world's `/docs` directory.
#### Game Info
+
The game info page is for a short breakdown of what your game is and how it works in Archipelago. Any additional
information that may be useful to the player when learning your randomizer should also go here. The file name format
is `_.md`. While you can write these docs for multiple languages, currently only the english
version is displayed on the website.
#### Tutorials
+
Your game can have as many tutorials in as many languages as you like, with each one having a relevant `Tutorial`
-defined in the `WebWorld`. The file name you use aren't particularly important, but it should be descriptive of what
-the tutorial is covering, and the name of the file must match the relative URL provided in the `Tutorial`. Currently,
+defined in the `WebWorld`. The file name you use isn't particularly important, but it should be descriptive of what
+the tutorial covers, and the name of the file must match the relative URL provided in the `Tutorial`. Currently,
the JS that determines this ignores the provided file name and will search for `game/document_lang.md`, where
`game/document/lang` is the provided URL.
@@ -723,33 +771,37 @@ multiworld for each test written using it. Within subsequent modules, classes sh
TestBase, and can then define options to test in the class body, and run tests in each test method.
Example `__init__.py`
+
```python
-from test.TestBase import WorldTestBase
+from test.bases import WorldTestBase
class MyGameTestBase(WorldTestBase):
- game = "My Game"
+ game = "My Game"
```
-Next using the rules defined in the above `set_rules` we can test that the chests have the correct access rules.
+Next, using the rules defined in the above `set_rules` we can test that the chests have the correct access rules.
+
+Example `test_chest_access.py`
-Example `testChestAccess.py`
```python
from . import MyGameTestBase
class TestChestAccess(MyGameTestBase):
- def test_sword_chests(self):
+ def test_sword_chests(self) -> None:
"""Test locations that require a sword"""
locations = ["Chest1", "Chest2"]
items = [["Sword"]]
- # this will test that each location can't be accessed without the "Sword", but can be accessed once obtained.
+ # this will test that each location can't be accessed without the "Sword", but can be accessed once obtained
self.assertAccessDependency(locations, items)
- def test_any_weapon_chests(self):
+ def test_any_weapon_chests(self) -> None:
"""Test locations that require any weapon"""
locations = [f"Chest{i}" for i in range(3, 6)]
items = [["Sword"], ["Axe"], ["Spear"]]
- # this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them.
+ # this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them
self.assertAccessDependency(locations, items)
```
+
+For more information on tests, check the [tests doc](tests.md).
diff --git a/inno_setup.iss b/inno_setup.iss
index 147cd74dca07..f097500f7d7d 100644
--- a/inno_setup.iss
+++ b/inno_setup.iss
@@ -31,8 +31,11 @@ ArchitecturesAllowed=x64 arm64
AllowNoIcons=yes
SetupIconFile={#MyAppIcon}
UninstallDisplayIcon={app}\{#MyAppExeName}
-; you will likely have to remove the following signtool line when testing/debugging locally. Don't include that change in PRs.
+#ifndef NO_SIGNTOOL
+; You will likely have to remove the SignTool= line when testing/debugging locally or run with iscc.exe /DNO_SIGNTOOL.
+; Don't include that change in PRs.
SignTool= signtool
+#endif
LicenseFile= LICENSE
WizardStyle= modern
SetupLogging=yes
@@ -46,147 +49,33 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
[Types]
Name: "full"; Description: "Full installation"
-Name: "hosting"; Description: "Installation for hosting purposes"
-Name: "playing"; Description: "Installation for playing purposes"
+Name: "minimal"; Description: "Minimal installation"
Name: "custom"; Description: "Custom installation"; Flags: iscustom
[Components]
-Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
-Name: "generator"; Description: "Generator"; Types: full hosting
-Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
-Name: "generator/dkc3"; Description: "Donkey Kong Country 3 ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
-Name: "generator/smw"; Description: "Super Mario World ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
-Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
-Name: "generator/l2ac"; Description: "Lufia II Ancient Cave ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 2621440; Flags: disablenouninstallwarning
-Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
-Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
-Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning
-Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting
-Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting
-Name: "generator/mmbn3"; Description: "MegaMan Battle Network 3"; Types: full hosting; ExtraDiskSpaceRequired: 8388608; Flags: disablenouninstallwarning
-Name: "generator/ladx"; Description: "Link's Awakening DX ROM Setup"; Types: full hosting
-Name: "generator/tloz"; Description: "The Legend of Zelda ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 135168; Flags: disablenouninstallwarning
-Name: "server"; Description: "Server"; Types: full hosting
-Name: "client"; Description: "Clients"; Types: full playing
-Name: "client/sni"; Description: "SNI Client"; Types: full playing
-Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
-Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
-Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
-Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
-Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
-Name: "client/factorio"; Description: "Factorio"; Types: full playing
-Name: "client/kh2"; Description: "Kingdom Hearts 2"; Types: full playing
-Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
-Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
-Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
-Name: "client/pkmn"; Description: "Pokemon Client"
-Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
-Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
-Name: "client/mmbn3"; Description: "MegaMan Battle Network 3 Client"; Types: full playing;
-Name: "client/ladx"; Description: "Link's Awakening Client"; Types: full playing; ExtraDiskSpaceRequired: 1048576
-Name: "client/cf"; Description: "ChecksFinder"; Types: full playing
-Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing
-Name: "client/wargroove"; Description: "Wargroove"; Types: full playing
-Name: "client/zl"; Description: "Zillion"; Types: full playing
-Name: "client/tloz"; Description: "The Legend of Zelda"; Types: full playing
-Name: "client/advn"; Description: "Adventure"; Types: full playing
-Name: "client/ut"; Description: "Undertale"; Types: full playing
-Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
+Name: "core"; Description: "Archipelago"; Types: full minimal custom; Flags: fixed
+Name: "lttp_sprites"; Description: "Download ""A Link to the Past"" player sprites"; Types: full;
[Dirs]
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
[Files]
-Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp
-Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm
-Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"; Flags: external; Components: client/sni/dkc3 or generator/dkc3
-Source: "{code:GetSMWROMPath}"; DestDir: "{app}"; DestName: "Super Mario World (USA).sfc"; Flags: external; Components: client/sni/smw or generator/smw
-Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe
-Source: "{code:GetL2ACROMPath}"; DestDir: "{app}"; DestName: "Lufia II - Rise of the Sinistrals (USA).sfc"; Flags: external; Components: generator/l2ac
-Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot
-Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl
-Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r
-Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b
-Source: "{code:GetBN3ROMPath}"; DestDir: "{app}"; DestName: "Mega Man Battle Network 3 - Blue Version (USA).gba"; Flags: external; Components: client/mmbn3
-Source: "{code:GetLADXROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"; Flags: external; Components: client/ladx or generator/ladx
-Source: "{code:GetTLoZROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The (U) (PRG0) [!].nes"; Flags: external; Components: client/tloz or generator/tloz
-Source: "{code:GetAdvnROMPath}"; DestDir: "{app}"; DestName: "ADVNTURE.BIN"; Flags: external; Components: client/advn
-Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
-Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
-Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
-
-Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion;
-Source: "{#source_path}\ArchipelagoLauncher(DEBUG).exe"; DestDir: "{app}"; Flags: ignoreversion;
-Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
-Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
-Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
-Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
-Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
-Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ladx
-Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
-Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
-Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
-Source: "{#source_path}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
-Source: "{#source_path}\ArchipelagoZillionClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/zl
-Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1
-Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn
-Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf
-Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2
-Source: "{#source_path}\ArchipelagoMMBN3Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/mmbn3
-Source: "{#source_path}\ArchipelagoZelda1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/tloz
-Source: "{#source_path}\ArchipelagoWargrooveClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/wargroove
-Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2
-Source: "{#source_path}\ArchipelagoAdventureClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/advn
-Source: "{#source_path}\ArchipelagoUndertaleClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ut
+Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
+Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs;
+Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs;
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
[Icons]
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
Name: "{group}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"
-Name: "{group}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Components: server
-Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text
-Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni
-Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
-Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
-Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot
-Name: "{group}\{#MyAppName} Zillion Client"; Filename: "{app}\ArchipelagoZillionClient.exe"; Components: client/zl
-Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1
-Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn
-Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf
-Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2
-Name: "{group}\{#MyAppName} MegaMan Battle Network 3 Client"; Filename: "{app}\ArchipelagoMMBN3Client.exe"; Components: client/mmbn3
-Name: "{group}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Components: client/tloz
-Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2
-Name: "{group}\{#MyAppName} Link's Awakening Client"; Filename: "{app}\ArchipelagoLinksAwakeningClient.exe"; Components: client/ladx
-Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn
-Name: "{group}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Components: client/wargroove
-Name: "{group}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Components: client/ut
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"; Tasks: desktopicon
-Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Tasks: desktopicon; Components: server
-Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni
-Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio
-Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft
-Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot
-Name: "{commondesktop}\{#MyAppName} Zillion Client"; Filename: "{app}\ArchipelagoZillionClient.exe"; Tasks: desktopicon; Components: client/zl
-Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/ff1
-Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn
-Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf
-Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2
-Name: "{commondesktop}\{#MyAppName} MegaMan Battle Network 3 Client"; Filename: "{app}\ArchipelagoMMBN3Client.exe"; Tasks: desktopicon; Components: client/mmbn3
-Name: "{commondesktop}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Tasks: desktopicon; Components: client/tloz
-Name: "{commondesktop}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Tasks: desktopicon; Components: client/wargroove
-Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2
-Name: "{commondesktop}\{#MyAppName} Link's Awakening Client"; Filename: "{app}\ArchipelagoLinksAwakeningClient.exe"; Tasks: desktopicon; Components: client/ladx
-Name: "{commondesktop}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Tasks: desktopicon; Components: client/advn
-Name: "{commondesktop}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Tasks: desktopicon; Components: client/ut
[Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
-Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/sni/lttp or generator/lttp
-Filename: "{app}\ArchipelagoMinecraftClient.exe"; Parameters: "--install"; StatusMsg: "Installing Forge Server..."; Components: client/minecraft
+Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: lttp_sprites
Filename: "{app}\ArchipelagoLauncher"; Parameters: "--update_settings"; StatusMsg: "Updating host.yaml..."; Flags: runasoriginaluser runhidden
Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringChange('Launcher', '&', '&&')}}"; Flags: nowait postinstall skipifsilent
@@ -194,109 +83,150 @@ Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringC
Type: dirifempty; Name: "{app}"
[InstallDelete]
+Type: files; Name: "{app}\lib\worlds\_bizhawk.apworld"
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
-Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*"
+Type: files; Name: "{app}\ArchipelagoPokemonClient.exe"
+Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
+Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy"
+Type: dirifempty; Name: "{app}\lib\worlds\rogue-legacy"
+Type: files; Name: "{app}\lib\worlds\sc2wol.apworld"
+Type: filesandordirs; Name: "{app}\lib\worlds\sc2wol"
+Type: dirifempty; Name: "{app}\lib\worlds\sc2wol"
+Type: filesandordirs; Name: "{app}\lib\worlds\bk_sudoku"
+Type: dirifempty; Name: "{app}\lib\worlds\bk_sudoku"
+Type: files; Name: "{app}\ArchipelagoLauncher(DEBUG).exe"
Type: filesandordirs; Name: "{app}\SNI\lua*"
Type: filesandordirs; Name: "{app}\EnemizerCLI*"
#include "installdelete.iss"
[Registry]
-Root: HKCR; Subkey: ".aplttp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
-
-Root: HKCR; Subkey: ".apsm"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
-
-Root: HKCR; Subkey: ".apdkc3"; ValueData: "{#MyAppName}dkc3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Archipelago Donkey Kong Country 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}dkc3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}dkc3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
-
-Root: HKCR; Subkey: ".apsmw"; ValueData: "{#MyAppName}smwpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smwpatch"; ValueData: "Archipelago Super Mario World Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smwpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smwpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
-
-Root: HKCR; Subkey: ".apzl"; ValueData: "{#MyAppName}zlpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/zl
-Root: HKCR; Subkey: "{#MyAppName}zlpatch"; ValueData: "Archipelago Zillion Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/zl
-Root: HKCR; Subkey: "{#MyAppName}zlpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZillionClient.exe,0"; ValueType: string; ValueName: ""; Components: client/zl
-Root: HKCR; Subkey: "{#MyAppName}zlpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZillionClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/zl
-
-Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}smz3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
-
-Root: HKCR; Subkey: ".apsoe"; ValueData: "{#MyAppName}soepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Archipelago Secret of Evermore Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
-
-Root: HKCR; Subkey: ".apl2ac"; ValueData: "{#MyAppName}l2acpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}l2acpatch"; ValueData: "Archipelago Lufia II Ancient Cave Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}l2acpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
-Root: HKCR; Subkey: "{#MyAppName}l2acpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
-
-Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft
-Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft
-Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: ""; Components: client/minecraft
-Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/minecraft
-
-Root: HKCR; Subkey: ".apz5"; ValueData: "{#MyAppName}n64zpf"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/oot
-Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archipelago Ocarina of Time Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/oot
-Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: ""; Components: client/oot
-Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{app}\ArchipelagoOoTClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/oot
-
-Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn
-Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn
-Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn
-Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn
-
-Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn
-Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn
-Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn
-Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn
-
-Root: HKCR; Subkey: ".apbn3"; ValueData: "{#MyAppName}bn3bpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/mmbn3
-Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Archipelago MegaMan Battle Network 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/mmbn3
-Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoMMBN3Client.exe,0"; ValueType: string; ValueName: ""; Components: client/mmbn3
-Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\shell\open\command"; ValueData: """{app}\ArchipelagoMMBN3Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/mmbn3
-
-Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/ladx
-Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/ladx
-Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; Components: client/ladx
-Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/ladx
-
-Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/tloz
-Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/tloz
-Root: HKCR; Subkey: "{#MyAppName}tlozpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZelda1Client.exe,0"; ValueType: string; ValueName: ""; Components: client/tloz
-Root: HKCR; Subkey: "{#MyAppName}tlozpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZelda1Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/tloz
-
-Root: HKCR; Subkey: ".apadvn"; ValueData: "{#MyAppName}advnpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/advn
-Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Archipelago Adventure Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/advn
-Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: ""; Components: client/advn
-Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/advn
-
-Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
-Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
-Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server
-Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: server
-
-Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey; Components: client/text
-Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: ""; Components: client/text
-Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0"; Components: client/text
-Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1"""; Components: client/text
+Root: HKCR; Subkey: ".aplttp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apsm"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apdkc3"; ValueData: "{#MyAppName}dkc3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Archipelago Donkey Kong Country 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}dkc3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}dkc3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apsmw"; ValueData: "{#MyAppName}smwpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smwpatch"; ValueData: "Archipelago Super Mario World Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smwpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smwpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apzl"; ValueData: "{#MyAppName}zlpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}zlpatch"; ValueData: "Archipelago Zillion Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}zlpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZillionClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}zlpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZillionClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}smz3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apsoe"; ValueData: "{#MyAppName}soepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Archipelago Secret of Evermore Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apl2ac"; ValueData: "{#MyAppName}l2acpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}l2acpatch"; ValueData: "Archipelago Lufia II Ancient Cave Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}l2acpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}l2acpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apkdl3"; ValueData: "{#MyAppName}kdl3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}kdl3patch"; ValueData: "Archipelago Kirby's Dream Land 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}kdl3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}kdl3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apz5"; ValueData: "{#MyAppName}n64zpf"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archipelago Ocarina of Time Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{app}\ArchipelagoOoTClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apbn3"; ValueData: "{#MyAppName}bn3bpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Archipelago MegaMan Battle Network 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoMMBN3Client.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\shell\open\command"; ValueData: """{app}\ArchipelagoMMBN3Client.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apemerald"; ValueData: "{#MyAppName}pkmnepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnepatch"; ValueData: "Archipelago Pokemon Emerald Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apmlss"; ValueData: "{#MyAppName}mlsspatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}mlsspatch"; ValueData: "Archipelago Mario & Luigi Superstar Saga Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}mlsspatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}mlsspatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apcv64"; ValueData: "{#MyAppName}cv64patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Archipelago Castlevania 64 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}tlozpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZelda1Client.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}tlozpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZelda1Client.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apadvn"; ValueData: "{#MyAppName}advnpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Archipelago Adventure Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apyi"; ValueData: "{#MyAppName}yipatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}yipatch"; ValueData: "Archipelago Yoshi's Island Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}yipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}yipatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apygo06"; ValueData: "{#MyAppName}ygo06patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}ygo06patch"; ValueData: "Archipelago Yu-Gi-Oh 2006 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}ygo06patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}ygo06patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: ".apworld"; ValueData: "{#MyAppName}worlddata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}worlddata"; ValueData: "Archipelago World Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}worlddata\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
+
+Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
+Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
+Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0";
+Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1""";
[Code]
-const
- SHCONTCH_NOPROGRESSBOX = 4;
- SHCONTCH_RESPONDYESTOALL = 16;
-
// See: https://stackoverflow.com/a/51614652/2287576
function IsVCRedist64BitNeeded(): boolean;
var
@@ -307,7 +237,7 @@ begin
begin
// Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion);
- Result := (CompareStr(strVersion, 'v14.32.31332') < 0);
+ Result := (CompareStr(strVersion, 'v14.38.33130') < 0);
end
else
begin
@@ -316,594 +246,3 @@ begin
Result := True;
end;
end;
-
-var R : longint;
-
-var lttprom: string;
-var LttPROMFilePage: TInputFileWizardPage;
-
-var smrom: string;
-var SMRomFilePage: TInputFileWizardPage;
-
-var dkc3rom: string;
-var DKC3RomFilePage: TInputFileWizardPage;
-
-var smwrom: string;
-var SMWRomFilePage: TInputFileWizardPage;
-
-var soerom: string;
-var SoERomFilePage: TInputFileWizardPage;
-
-var l2acrom: string;
-var L2ACROMFilePage: TInputFileWizardPage;
-
-var ootrom: string;
-var OoTROMFilePage: TInputFileWizardPage;
-
-var zlrom: string;
-var ZlROMFilePage: TInputFileWizardPage;
-
-var redrom: string;
-var RedROMFilePage: TInputFileWizardPage;
-
-var bluerom: string;
-var BlueROMFilePage: TInputFileWizardPage;
-
-var bn3rom: string;
-var BN3ROMFilePage: TInputFileWizardPage;
-
-var ladxrom: string;
-var LADXROMFilePage: TInputFileWizardPage;
-
-var tlozrom: string;
-var TLoZROMFilePage: TInputFileWizardPage;
-
-var advnrom: string;
-var AdvnROMFilePage: TInputFileWizardPage;
-
-function GetSNESMD5OfFile(const rom: string): string;
-var data: AnsiString;
-begin
- if LoadStringFromFile(rom, data) then
- begin
- if Length(data) mod 1024 = 512 then
- begin
- data := copy(data, 513, Length(data)-512);
- end;
- Result := GetMD5OfString(data);
- end;
-end;
-
-function GetSMSMD5OfFile(const rom: string): string;
-var data: AnsiString;
-begin
- if LoadStringFromFile(rom, data) then
- begin
- Result := GetMD5OfString(data);
- end;
-end;
-
-function CheckRom(name: string; hash: string): string;
-var rom: string;
-begin
- log('Handling ' + name)
- rom := FileSearch(name, WizardDirValue());
- if Length(rom) > 0 then
- begin
- log('existing ROM found');
- log(IntToStr(CompareStr(GetSNESMD5OfFile(rom), hash)));
- if CompareStr(GetSNESMD5OfFile(rom), hash) = 0 then
- begin
- log('existing ROM verified');
- Result := rom;
- exit;
- end;
- log('existing ROM failed verification');
- end;
-end;
-
-function CheckSMSRom(name: string; hash: string): string;
-var rom: string;
-begin
- log('Handling ' + name)
- rom := FileSearch(name, WizardDirValue());
- if Length(rom) > 0 then
- begin
- log('existing ROM found');
- log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash)));
- if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then
- begin
- log('existing ROM verified');
- Result := rom;
- exit;
- end;
- log('existing ROM failed verification');
- end;
-end;
-
-function CheckNESRom(name: string; hash: string): string;
-var rom: string;
-begin
- log('Handling ' + name)
- rom := FileSearch(name, WizardDirValue());
- if Length(rom) > 0 then
- begin
- log('existing ROM found');
- log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash)));
- if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then
- begin
- log('existing ROM verified');
- Result := rom;
- exit;
- end;
- log('existing ROM failed verification');
- end;
-end;
-
-function AddRomPage(name: string): TInputFileWizardPage;
-begin
- Result :=
- CreateInputFilePage(
- wpSelectComponents,
- 'Select ROM File',
- 'Where is your ' + name + ' located?',
- 'Select the file, then click Next.');
-
- Result.Add(
- 'Location of ROM file:',
- 'SNES ROM files|*.sfc;*.smc|All files|*.*',
- '.sfc');
-end;
-
-
-function AddGBRomPage(name: string): TInputFileWizardPage;
-begin
- Result :=
- CreateInputFilePage(
- wpSelectComponents,
- 'Select ROM File',
- 'Where is your ' + name + ' located?',
- 'Select the file, then click Next.');
-
- Result.Add(
- 'Location of ROM file:',
- 'GB ROM files|*.gb;*.gbc|All files|*.*',
- '.gb');
-end;
-
-function AddGBARomPage(name: string): TInputFileWizardPage;
-begin
- Result :=
- CreateInputFilePage(
- wpSelectComponents,
- 'Select ROM File',
- 'Where is your ' + name + ' located?',
- 'Select the file, then click Next.');
- Result.Add(
- 'Location of ROM file:',
- 'GBA ROM files|*.gba|All files|*.*',
- '.gba');
-end;
-
-function AddSMSRomPage(name: string): TInputFileWizardPage;
-begin
- Result :=
- CreateInputFilePage(
- wpSelectComponents,
- 'Select ROM File',
- 'Where is your ' + name + ' located?',
- 'Select the file, then click Next.');
- Result.Add(
- 'Location of ROM file:',
- 'SMS ROM files|*.sms|All files|*.*',
- '.sms');
-end;
-
-function AddNESRomPage(name: string): TInputFileWizardPage;
-begin
- Result :=
- CreateInputFilePage(
- wpSelectComponents,
- 'Select ROM File',
- 'Where is your ' + name + ' located?',
- 'Select the file, then click Next.');
-
- Result.Add(
- 'Location of ROM file:',
- 'NES ROM files|*.nes|All files|*.*',
- '.nes');
-end;
-
-procedure AddOoTRomPage();
-begin
- ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue());
- if Length(ootrom) > 0 then
- begin
- log('existing ROM found');
- log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '5bd1fe107bf8106b2ab6650abecd54d6'))); // normal
- log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '6697768a7a7df2dd27a692a2638ea90b'))); // byteswapped
- log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '05f0f3ebacbc8df9243b6148ffe4792f'))); // decompressed
- if (CompareStr(GetMD5OfFile(ootrom), '5bd1fe107bf8106b2ab6650abecd54d6') = 0) or (CompareStr(GetMD5OfFile(ootrom), '6697768a7a7df2dd27a692a2638ea90b') = 0) or (CompareStr(GetMD5OfFile(ootrom), '05f0f3ebacbc8df9243b6148ffe4792f') = 0) then
- begin
- log('existing ROM verified');
- exit;
- end;
- log('existing ROM failed verification');
- end;
- ootrom := ''
- OoTROMFilePage :=
- CreateInputFilePage(
- wpSelectComponents,
- 'Select ROM File',
- 'Where is your OoT 1.0 ROM located?',
- 'Select the file, then click Next.');
-
- OoTROMFilePage.Add(
- 'Location of ROM file:',
- 'N64 ROM files (*.z64, *.n64)|*.z64;*.n64|All files|*.*',
- '.z64');
-end;
-
-function AddA26Page(name: string): TInputFileWizardPage;
-begin
- Result :=
- CreateInputFilePage(
- wpSelectComponents,
- 'Select ROM File',
- 'Where is your ' + name + ' located?',
- 'Select the file, then click Next.');
-
- Result.Add(
- 'Location of ROM file:',
- 'A2600 ROM files|*.BIN;*.a26|All files|*.*',
- '.BIN');
-end;
-
-function NextButtonClick(CurPageID: Integer): Boolean;
-begin
- if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
- Result := not (LttPROMFilePage.Values[0] = '')
- else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then
- Result := not (SMROMFilePage.Values[0] = '')
- else if (assigned(DKC3ROMFilePage)) and (CurPageID = DKC3ROMFilePage.ID) then
- Result := not (DKC3ROMFilePage.Values[0] = '')
- else if (assigned(SMWROMFilePage)) and (CurPageID = SMWROMFilePage.ID) then
- Result := not (SMWROMFilePage.Values[0] = '')
- else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then
- Result := not (SoEROMFilePage.Values[0] = '')
- else if (assigned(L2ACROMFilePage)) and (CurPageID = L2ACROMFilePage.ID) then
- Result := not (L2ACROMFilePage.Values[0] = '')
- else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
- Result := not (OoTROMFilePage.Values[0] = '')
- else if (assigned(BN3ROMFilePage)) and (CurPageID = BN3ROMFilePage.ID) then
- Result := not (BN3ROMFilePage.Values[0] = '')
- else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then
- Result := not (ZlROMFilePage.Values[0] = '')
- else if (assigned(RedROMFilePage)) and (CurPageID = RedROMFilePage.ID) then
- Result := not (RedROMFilePage.Values[0] = '')
- else if (assigned(BlueROMFilePage)) and (CurPageID = BlueROMFilePage.ID) then
- Result := not (BlueROMFilePage.Values[0] = '')
- else if (assigned(LADXROMFilePage)) and (CurPageID = LADXROMFilePage.ID) then
- Result := not (LADXROMFilePage.Values[0] = '')
- else if (assigned(TLoZROMFilePage)) and (CurPageID = TLoZROMFilePage.ID) then
- Result := not (TLoZROMFilePage.Values[0] = '')
- else if (assigned(AdvnROMFilePage)) and (CurPageID = AdvnROMFilePage.ID) then
- Result := not (AdvnROMFilePage.Values[0] = '')
- else
- Result := True;
-end;
-
-function GetROMPath(Param: string): string;
-begin
- if Length(lttprom) > 0 then
- Result := lttprom
- else if Assigned(LttPRomFilePage) then
- begin
- R := CompareStr(GetSNESMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
- if R <> 0 then
- MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := LttPROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetSMROMPath(Param: string): string;
-begin
- if Length(smrom) > 0 then
- Result := smrom
- else if Assigned(SMRomFilePage) then
- begin
- R := CompareStr(GetSNESMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
- if R <> 0 then
- MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := SMROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetDKC3ROMPath(Param: string): string;
-begin
- if Length(dkc3rom) > 0 then
- Result := dkc3rom
- else if Assigned(DKC3RomFilePage) then
- begin
- R := CompareStr(GetSNESMD5OfFile(DKC3ROMFilePage.Values[0]), '120abf304f0c40fe059f6a192ed4f947')
- if R <> 0 then
- MsgBox('Donkey Kong Country 3 ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := DKC3ROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetSMWROMPath(Param: string): string;
-begin
- if Length(smwrom) > 0 then
- Result := smwrom
- else if Assigned(SMWRomFilePage) then
- begin
- R := CompareStr(GetSNESMD5OfFile(SMWROMFilePage.Values[0]), 'cdd3c8c37322978ca8669b34bc89c804')
- if R <> 0 then
- MsgBox('Super Mario World ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := SMWROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetSoEROMPath(Param: string): string;
-begin
- if Length(soerom) > 0 then
- Result := soerom
- else if Assigned(SoERomFilePage) then
- begin
- R := CompareStr(GetSNESMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a')
- if R <> 0 then
- MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := SoEROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetOoTROMPath(Param: string): string;
-begin
- if Length(ootrom) > 0 then
- Result := ootrom
- else if Assigned(OoTROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '5bd1fe107bf8106b2ab6650abecd54d6') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '6697768a7a7df2dd27a692a2638ea90b') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '05f0f3ebacbc8df9243b6148ffe4792f');
- if R <> 0 then
- MsgBox('OoT ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := OoTROMFilePage.Values[0]
- end
- else
- Result := '';
-end;
-
-function GetL2ACROMPath(Param: string): string;
-begin
- if Length(l2acrom) > 0 then
- Result := l2acrom
- else if Assigned(L2ACROMFilePage) then
- begin
- R := CompareStr(GetSNESMD5OfFile(L2ACROMFilePage.Values[0]), '6efc477d6203ed2b3b9133c1cd9e9c5d')
- if R <> 0 then
- MsgBox('Lufia II ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := L2ACROMFilePage.Values[0]
- end
- else
- Result := '';
-end;
-
-function GetZlROMPath(Param: string): string;
-begin
- if Length(zlrom) > 0 then
- Result := zlrom
- else if Assigned(ZlROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(ZlROMFilePage.Values[0]), 'd4bf9e7bcf9a48da53785d2ae7bc4270');
- if R <> 0 then
- MsgBox('Zillion ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := ZlROMFilePage.Values[0]
- end
- else
- Result := '';
-end;
-
-function GetRedROMPath(Param: string): string;
-begin
- if Length(redrom) > 0 then
- Result := redrom
- else if Assigned(RedROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(RedROMFilePage.Values[0]), '3d45c1ee9abd5738df46d2bdda8b57dc')
- if R <> 0 then
- MsgBox('Pokemon Red ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := RedROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetBlueROMPath(Param: string): string;
-begin
- if Length(bluerom) > 0 then
- Result := bluerom
- else if Assigned(BlueROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(BlueROMFilePage.Values[0]), '50927e843568814f7ed45ec4f944bd8b')
- if R <> 0 then
- MsgBox('Pokemon Blue ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := BlueROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetTLoZROMPath(Param: string): string;
-begin
- if Length(tlozrom) > 0 then
- Result := tlozrom
- else if Assigned(TLoZROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(TLoZROMFilePage.Values[0]), '337bd6f1a1163df31bf2633665589ab0');
- if R <> 0 then
- MsgBox('The Legend of Zelda ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := TLoZROMFilePage.Values[0]
- end
- else
- Result := '';
-end;
-
-function GetLADXROMPath(Param: string): string;
-begin
- if Length(ladxrom) > 0 then
- Result := ladxrom
- else if Assigned(LADXROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(LADXROMFilePage.Values[0]), '07c211479386825042efb4ad31bb525f')
- if R <> 0 then
- MsgBox('Link''s Awakening DX ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := LADXROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-function GetAdvnROMPath(Param: string): string;
-begin
- if Length(advnrom) > 0 then
- Result := advnrom
- else if Assigned(AdvnROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(AdvnROMFilePage.Values[0]), '157bddb7192754a45372be196797f284');
- if R <> 0 then
- MsgBox('Adventure ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := AdvnROMFilePage.Values[0]
- end
- else
- Result := '';
-end;
-
-function GetBN3ROMPath(Param: string): string;
-begin
- if Length(bn3rom) > 0 then
- Result := bn3rom
- else if Assigned(BN3ROMFilePage) then
- begin
- R := CompareStr(GetMD5OfFile(BN3ROMFilePage.Values[0]), '6fe31df0144759b34ad666badaacc442')
- if R <> 0 then
- MsgBox('MegaMan Battle Network 3 Blue ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
-
- Result := BN3ROMFilePage.Values[0]
- end
- else
- Result := '';
- end;
-
-procedure InitializeWizard();
-begin
- AddOoTRomPage();
-
- lttprom := CheckRom('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', '03a63945398191337e896e5771f77173');
- if Length(lttprom) = 0 then
- LttPROMFilePage:= AddRomPage('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc');
-
- smrom := CheckRom('Super Metroid (JU).sfc', '21f3e98df4780ee1c667b84e57d88675');
- if Length(smrom) = 0 then
- SMRomFilePage:= AddRomPage('Super Metroid (JU).sfc');
-
- dkc3rom := CheckRom('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc', '120abf304f0c40fe059f6a192ed4f947');
- if Length(dkc3rom) = 0 then
- DKC3RomFilePage:= AddRomPage('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc');
-
- smwrom := CheckRom('Super Mario World (USA).sfc', 'cdd3c8c37322978ca8669b34bc89c804');
- if Length(smwrom) = 0 then
- SMWRomFilePage:= AddRomPage('Super Mario World (USA).sfc');
-
- soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a');
- if Length(soerom) = 0 then
- SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc');
-
- zlrom := CheckSMSRom('Zillion (UE) [!].sms', 'd4bf9e7bcf9a48da53785d2ae7bc4270');
- if Length(zlrom) = 0 then
- ZlROMFilePage:= AddSMSRomPage('Zillion (UE) [!].sms');
-
- redrom := CheckRom('Pokemon Red (UE) [S][!].gb','3d45c1ee9abd5738df46d2bdda8b57dc');
- if Length(redrom) = 0 then
- RedROMFilePage:= AddGBRomPage('Pokemon Red (UE) [S][!].gb');
-
- bluerom := CheckRom('Pokemon Blue (UE) [S][!].gb','50927e843568814f7ed45ec4f944bd8b');
- if Length(bluerom) = 0 then
- BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb');
-
- bn3rom := CheckRom('Mega Man Battle Network 3 - Blue Version (USA).gba','6fe31df0144759b34ad666badaacc442');
- if Length(bn3rom) = 0 then
- BN3ROMFilePage:= AddGBARomPage('Mega Man Battle Network 3 - Blue Version (USA).gba');
-
- ladxrom := CheckRom('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc','07c211479386825042efb4ad31bb525f');
- if Length(ladxrom) = 0 then
- LADXROMFilePage:= AddGBRomPage('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc');
-
- l2acrom := CheckRom('Lufia II - Rise of the Sinistrals (USA).sfc', '6efc477d6203ed2b3b9133c1cd9e9c5d');
- if Length(l2acrom) = 0 then
- L2ACROMFilePage:= AddRomPage('Lufia II - Rise of the Sinistrals (USA).sfc');
-
- tlozrom := CheckNESROM('Legend of Zelda, The (U) (PRG0) [!].nes', '337bd6f1a1163df31bf2633665589ab0');
- if Length(tlozrom) = 0 then
- TLoZROMFilePage:= AddNESRomPage('Legend of Zelda, The (U) (PRG0) [!].nes');
-
- advnrom := CheckSMSRom('ADVNTURE.BIN', '157bddb7192754a45372be196797f284');
- if Length(advnrom) = 0 then
- AdvnROMFilePage:= AddA26Page('ADVNTURE.BIN');
-end;
-
-
-function ShouldSkipPage(PageID: Integer): Boolean;
-begin
- Result := False;
- if (assigned(LttPROMFilePage)) and (PageID = LttPROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('client/sni/lttp') or WizardIsComponentSelected('generator/lttp'));
- if (assigned(SMROMFilePage)) and (PageID = SMROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm'));
- if (assigned(DKC3ROMFilePage)) and (PageID = DKC3ROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('client/sni/dkc3') or WizardIsComponentSelected('generator/dkc3'));
- if (assigned(SMWROMFilePage)) and (PageID = SMWROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('client/sni/smw') or WizardIsComponentSelected('generator/smw'));
- if (assigned(L2ACROMFilePage)) and (PageID = L2ACROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('client/sni/l2ac') or WizardIsComponentSelected('generator/l2ac'));
- if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/soe'));
- if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/oot') or WizardIsComponentSelected('client/oot'));
- if (assigned(ZlROMFilePage)) and (PageID = ZlROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/zl') or WizardIsComponentSelected('client/zl'));
- if (assigned(RedROMFilePage)) and (PageID = RedROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red'));
- if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue'));
- if (assigned(BN3ROMFilePage)) and (PageID = BN3ROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/mmbn3') or WizardIsComponentSelected('client/mmbn3'));
- if (assigned(LADXROMFilePage)) and (PageID = LADXROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/ladx') or WizardIsComponentSelected('client/ladx'));
- if (assigned(TLoZROMFilePage)) and (PageID = TLoZROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('generator/tloz') or WizardIsComponentSelected('client/tloz'));
- if (assigned(AdvnROMFilePage)) and (PageID = AdvnROMFilePage.ID) then
- Result := not (WizardIsComponentSelected('client/advn'));
-end;
diff --git a/intset.h b/intset.h
new file mode 100644
index 000000000000..fac84fb6f890
--- /dev/null
+++ b/intset.h
@@ -0,0 +1,135 @@
+/* A specialized unordered_set implementation for literals, where bucket_count
+ * is defined at initialization rather than increased automatically.
+ */
+#include
+#include
+#include
+#include
+
+#ifndef INTSET_NAME
+#error "Please #define INTSET_NAME ... before including intset.h"
+#endif
+
+#ifndef INTSET_TYPE
+#error "Please #define INTSET_TYPE ... before including intset.h"
+#endif
+
+/* macros to generate unique names from INTSET_NAME */
+#ifndef INTSET_CONCAT
+#define INTSET_CONCAT_(a, b) a ## b
+#define INTSET_CONCAT(a, b) INTSET_CONCAT_(a, b)
+#define INTSET_FUNC_(a, b) INTSET_CONCAT(a, _ ## b)
+#endif
+
+#define INTSET_FUNC(name) INTSET_FUNC_(INTSET_NAME, name)
+#define INTSET_BUCKET INTSET_CONCAT(INTSET_NAME, Bucket)
+#define INTSET_UNION INTSET_CONCAT(INTSET_NAME, Union)
+
+#if defined(_MSC_VER)
+#pragma warning(push)
+#pragma warning(disable : 4200)
+#endif
+
+
+typedef struct {
+ size_t count;
+ union INTSET_UNION {
+ INTSET_TYPE val;
+ INTSET_TYPE *data;
+ } v;
+} INTSET_BUCKET;
+
+typedef struct {
+ size_t bucket_count;
+ INTSET_BUCKET buckets[];
+} INTSET_NAME;
+
+static INTSET_NAME *INTSET_FUNC(new)(size_t buckets)
+{
+ size_t i, size;
+ INTSET_NAME *set;
+
+ if (buckets < 1)
+ buckets = 1;
+ if ((SIZE_MAX - sizeof(INTSET_NAME)) / sizeof(INTSET_BUCKET) < buckets)
+ return NULL;
+ size = sizeof(INTSET_NAME) + buckets * sizeof(INTSET_BUCKET);
+ set = (INTSET_NAME*)malloc(size);
+ if (!set)
+ return NULL;
+ memset(set, 0, size); /* gcc -fanalyzer does not understand this sets all buckets' count to 0 */
+ for (i = 0; i < buckets; i++) {
+ set->buckets[i].count = 0;
+ }
+ set->bucket_count = buckets;
+ return set;
+}
+
+static void INTSET_FUNC(free)(INTSET_NAME *set)
+{
+ size_t i;
+ if (!set)
+ return;
+ for (i = 0; i < set->bucket_count; i++) {
+ if (set->buckets[i].count > 1)
+ free(set->buckets[i].v.data);
+ }
+ free(set);
+}
+
+static bool INTSET_FUNC(contains)(INTSET_NAME *set, INTSET_TYPE val)
+{
+ size_t i;
+ INTSET_BUCKET* bucket = &set->buckets[(size_t)val % set->bucket_count];
+ if (bucket->count == 1)
+ return bucket->v.val == val;
+ for (i = 0; i < bucket->count; ++i) {
+ if (bucket->v.data[i] == val)
+ return true;
+ }
+ return false;
+}
+
+static bool INTSET_FUNC(add)(INTSET_NAME *set, INTSET_TYPE val)
+{
+ INTSET_BUCKET* bucket;
+
+ if (INTSET_FUNC(contains)(set, val))
+ return true; /* ok */
+
+ bucket = &set->buckets[(size_t)val % set->bucket_count];
+ if (bucket->count == 0) {
+ bucket->v.val = val;
+ bucket->count = 1;
+ } else if (bucket->count == 1) {
+ INTSET_TYPE old = bucket->v.val;
+ bucket->v.data = (INTSET_TYPE*)malloc(2 * sizeof(INTSET_TYPE));
+ if (!bucket->v.data) {
+ bucket->v.val = old;
+ return false; /* error */
+ }
+ bucket->v.data[0] = old;
+ bucket->v.data[1] = val;
+ bucket->count = 2;
+ } else {
+ size_t new_bucket_size;
+ INTSET_TYPE* new_bucket_data;
+
+ new_bucket_size = (bucket->count + 1) * sizeof(INTSET_TYPE);
+ new_bucket_data = (INTSET_TYPE*)realloc(bucket->v.data, new_bucket_size);
+ if (!new_bucket_data)
+ return false; /* error */
+ bucket->v.data = new_bucket_data;
+ bucket->v.data[bucket->count++] = val;
+ }
+ return true; /* success */
+}
+
+
+#if defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+
+#undef INTSET_FUNC
+#undef INTSET_BUCKET
+#undef INTSET_UNION
diff --git a/kvui.py b/kvui.py
index 77b96b896fd0..f83590a819d5 100644
--- a/kvui.py
+++ b/kvui.py
@@ -1,6 +1,19 @@
import os
import logging
+import sys
import typing
+import re
+from collections import deque
+
+if sys.platform == "win32":
+ import ctypes
+
+ # kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout
+ # by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's
+ try:
+ ctypes.windll.shcore.SetProcessDpiAwareness(0)
+ except FileNotFoundError: # shcore may not be found on <= Windows 7
+ pass # TODO: remove silent except when Python 3.8 is phased out.
os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1"
@@ -8,14 +21,15 @@
os.environ["KIVY_LOG_ENABLE"] = "0"
import Utils
+
if Utils.is_frozen():
os.environ["KIVY_DATA_DIR"] = Utils.local_path("data")
from kivy.config import Config
Config.set("input", "mouse", "mouse,disable_multitouch")
-Config.set('kivy', 'exit_on_escape', '0')
-Config.set('graphics', 'multisamples', '0') # multisamples crash old intel drivers
+Config.set("kivy", "exit_on_escape", "0")
+Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
from kivy.app import App
from kivy.core.window import Window
@@ -26,11 +40,13 @@
from kivy.factory import Factory
from kivy.properties import BooleanProperty, ObjectProperty
from kivy.metrics import dp
+from kivy.effects.scroll import ScrollEffect
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.layout import Layout
from kivy.uix.textinput import TextInput
+from kivy.uix.scrollview import ScrollView
from kivy.uix.recycleview import RecycleView
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
from kivy.uix.boxlayout import BoxLayout
@@ -48,9 +64,8 @@
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
-
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType
-from Utils import async_start
+from Utils import async_start, get_input_text_from_response
if typing.TYPE_CHECKING:
import CommonClient
@@ -59,6 +74,8 @@
else:
context_type = object
+remove_between_brackets = re.compile(r"\[.*?]")
+
# I was surprised to find this didn't already exist in kivy :(
class HoverBehavior(object):
@@ -67,8 +84,8 @@ class HoverBehavior(object):
border_point = ObjectProperty(None)
def __init__(self, **kwargs):
- self.register_event_type('on_enter')
- self.register_event_type('on_leave')
+ self.register_event_type("on_enter")
+ self.register_event_type("on_leave")
Window.bind(mouse_pos=self.on_mouse_pos)
Window.bind(on_cursor_leave=self.on_cursor_leave)
super(HoverBehavior, self).__init__(**kwargs)
@@ -96,7 +113,7 @@ def on_cursor_leave(self, *args):
self.dispatch("on_leave")
-Factory.register('HoverBehavior', HoverBehavior)
+Factory.register("HoverBehavior", HoverBehavior)
class ToolTip(Label):
@@ -107,10 +124,77 @@ class ServerToolTip(ToolTip):
pass
+class ScrollBox(ScrollView):
+ layout: BoxLayout
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.layout = BoxLayout(size_hint_y=None)
+ self.layout.bind(minimum_height=self.layout.setter("height"))
+ self.add_widget(self.layout)
+ self.effect_cls = ScrollEffect
+ self.bar_width = dp(12)
+ self.scroll_type = ["content", "bars"]
+
+
class HovererableLabel(HoverBehavior, Label):
pass
+class TooltipLabel(HovererableLabel):
+ tooltip = None
+
+ def create_tooltip(self, text, x, y):
+ text = text.replace(" ", "\n").replace("&", "&").replace("&bl;", "[").replace("&br;", "]")
+ if self.tooltip:
+ # update
+ self.tooltip.children[0].text = text
+ else:
+ self.tooltip = FloatLayout()
+ tooltip_label = ToolTip(text=text)
+ self.tooltip.add_widget(tooltip_label)
+ fade_in_animation.start(self.tooltip)
+ App.get_running_app().root.add_widget(self.tooltip)
+
+ # handle left-side boundary to not render off-screen
+ x = max(x, 3 + self.tooltip.children[0].texture_size[0] / 2)
+
+ # position float layout
+ self.tooltip.x = x - self.tooltip.width / 2
+ self.tooltip.y = y - self.tooltip.height / 2 + 48
+
+ def remove_tooltip(self):
+ if self.tooltip:
+ App.get_running_app().root.remove_widget(self.tooltip)
+ self.tooltip = None
+
+ def on_mouse_pos(self, window, pos):
+ if not self.get_root_window():
+ return # Abort if not displayed
+ super().on_mouse_pos(window, pos)
+ if self.refs and self.hovered:
+
+ tx, ty = self.to_widget(*pos, relative=True)
+ # Why TF is Y flipped *within* the texture?
+ ty = self.texture_size[1] - ty
+ hit = False
+ for uid, zones in self.refs.items():
+ for zone in zones:
+ x, y, w, h = zone
+ if x <= tx <= w and y <= ty <= h:
+ self.create_tooltip(uid.split("|", 1)[1], *pos)
+ hit = True
+ break
+ if not hit:
+ self.remove_tooltip()
+
+ def on_enter(self):
+ pass
+
+ def on_leave(self):
+ self.remove_tooltip()
+
+
class ServerLabel(HovererableLabel):
def __init__(self, *args, **kwargs):
super(HovererableLabel, self).__init__(*args, **kwargs)
@@ -179,11 +263,10 @@ class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
""" Adds selection and focus behaviour to the view. """
-class SelectableLabel(RecycleDataViewBehavior, HovererableLabel):
+class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
""" Add selection support to the Label """
index = None
selected = BooleanProperty(False)
- tooltip = None
def refresh_view_attrs(self, rv, index, data):
""" Catch and handle the view changes """
@@ -191,56 +274,6 @@ def refresh_view_attrs(self, rv, index, data):
return super(SelectableLabel, self).refresh_view_attrs(
rv, index, data)
- def create_tooltip(self, text, x, y):
- text = text.replace(" ", "\n").replace('&', '&').replace('&bl;', '[').replace('&br;', ']')
- if self.tooltip:
- # update
- self.tooltip.children[0].text = text
- else:
- self.tooltip = FloatLayout()
- tooltip_label = ToolTip(text=text)
- self.tooltip.add_widget(tooltip_label)
- fade_in_animation.start(self.tooltip)
- App.get_running_app().root.add_widget(self.tooltip)
-
- # handle left-side boundary to not render off-screen
- x = max(x, 3+self.tooltip.children[0].texture_size[0] / 2)
-
- # position float layout
- self.tooltip.x = x - self.tooltip.width / 2
- self.tooltip.y = y - self.tooltip.height / 2 + 48
-
- def remove_tooltip(self):
- if self.tooltip:
- App.get_running_app().root.remove_widget(self.tooltip)
- self.tooltip = None
-
- def on_mouse_pos(self, window, pos):
- if not self.get_root_window():
- return # Abort if not displayed
- super().on_mouse_pos(window, pos)
- if self.refs and self.hovered:
-
- tx, ty = self.to_widget(*pos, relative=True)
- # Why TF is Y flipped *within* the texture?
- ty = self.texture_size[1] - ty
- hit = False
- for uid, zones in self.refs.items():
- for zone in zones:
- x, y, w, h = zone
- if x <= tx <= w and y <= ty <= h:
- self.create_tooltip(uid.split("|", 1)[1], *pos)
- hit = True
- break
- if not hit:
- self.remove_tooltip()
-
- def on_enter(self):
- pass
-
- def on_leave(self):
- self.remove_tooltip()
-
def on_touch_down(self, touch):
""" Add selection on touch down """
if super(SelectableLabel, self).on_touch_down(touch):
@@ -253,18 +286,12 @@ def on_touch_down(self, touch):
temp = MarkupLabel(text=self.text).markup
text = "".join(part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
cmdinput = App.get_running_app().textinput
- if not cmdinput.text and " did you mean " in text:
- for question in ("Didn't find something that closely matches, did you mean ",
- "Too many close matches, did you mean "):
- if text.startswith(question):
- name = Utils.get_text_between(text, question,
- "? (")
- cmdinput.text = f"!{App.get_running_app().last_autofillable_command} {name}"
- break
- elif not cmdinput.text and text.startswith("Missing: "):
- cmdinput.text = text.replace("Missing: ", "!hint_location ")
-
- Clipboard.copy(text.replace('&', '&').replace('&bl;', '[').replace('&br;', ']'))
+ if not cmdinput.text:
+ input_text = get_input_text_from_response(text, App.get_running_app().last_autofillable_command)
+ if input_text is not None:
+ cmdinput.text = input_text
+
+ Clipboard.copy(text.replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
return self.parent.select_with_touch(self.index, touch)
def apply_selection(self, rv, index, is_selected):
@@ -272,12 +299,139 @@ def apply_selection(self, rv, index, is_selected):
self.selected = is_selected
+class HintLabel(RecycleDataViewBehavior, BoxLayout):
+ selected = BooleanProperty(False)
+ striped = BooleanProperty(False)
+ index = None
+
+ def __init__(self):
+ super(HintLabel, self).__init__()
+ self.receiving_text = ""
+ self.item_text = ""
+ self.finding_text = ""
+ self.location_text = ""
+ self.entrance_text = ""
+ self.found_text = ""
+ for child in self.children:
+ child.bind(texture_size=self.set_height)
+
+ def set_height(self, instance, value):
+ self.height = max([child.texture_size[1] for child in self.children])
+
+ def refresh_view_attrs(self, rv, index, data):
+ self.index = index
+ self.striped = data.get("striped", False)
+ self.receiving_text = data["receiving"]["text"]
+ self.item_text = data["item"]["text"]
+ self.finding_text = data["finding"]["text"]
+ self.location_text = data["location"]["text"]
+ self.entrance_text = data["entrance"]["text"]
+ self.found_text = data["found"]["text"]
+ self.height = self.minimum_height
+ return super(HintLabel, self).refresh_view_attrs(rv, index, data)
+
+ def on_touch_down(self, touch):
+ """ Add selection on touch down """
+ if super(HintLabel, self).on_touch_down(touch):
+ return True
+ if self.index: # skip header
+ if self.collide_point(*touch.pos):
+ if self.selected:
+ self.parent.clear_selection()
+ else:
+ text = "".join((self.receiving_text, "\'s ", self.item_text, " is at ", self.location_text, " in ",
+ self.finding_text, "\'s World", (" at " + self.entrance_text)
+ if self.entrance_text != "Vanilla"
+ else "", ". (", self.found_text.lower(), ")"))
+ temp = MarkupLabel(text).markup
+ text = "".join(
+ part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
+ Clipboard.copy(escape_markup(text).replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
+ return self.parent.select_with_touch(self.index, touch)
+ else:
+ parent = self.parent
+ parent.clear_selection()
+ parent: HintLog = parent.parent
+ # find correct column
+ for child in self.children:
+ if child.collide_point(*touch.pos):
+ key = child.sort_key
+ parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower()
+ if key == parent.sort_key:
+ # second click reverses order
+ parent.reversed = not parent.reversed
+ else:
+ parent.sort_key = key
+ parent.reversed = False
+ break
+ else:
+ logging.warning("Did not find clicked header for sorting.")
+
+ App.get_running_app().update_hints()
+
+ def apply_selection(self, rv, index, is_selected):
+ """ Respond to the selection of items in the view. """
+ if self.index:
+ self.selected = is_selected
+
+
class ConnectBarTextInput(TextInput):
def insert_text(self, substring, from_undo=False):
- s = substring.replace('\n', '').replace('\r', '')
+ s = substring.replace("\n", "").replace("\r", "")
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
+def is_command_input(string: str) -> bool:
+ return len(string) > 0 and string[0] in "/!"
+
+
+class CommandPromptTextInput(TextInput):
+ MAXIMUM_HISTORY_MESSAGES = 50
+
+ def __init__(self, **kwargs) -> None:
+ super().__init__(**kwargs)
+ self._command_history_index = -1
+ self._command_history: typing.Deque[str] = deque(maxlen=CommandPromptTextInput.MAXIMUM_HISTORY_MESSAGES)
+
+ def update_history(self, new_entry: str) -> None:
+ self._command_history_index = -1
+ if is_command_input(new_entry):
+ self._command_history.appendleft(new_entry)
+
+ def keyboard_on_key_down(
+ self,
+ window,
+ keycode: typing.Tuple[int, str],
+ text: typing.Optional[str],
+ modifiers: typing.List[str]
+ ) -> bool:
+ """
+ :param window: The kivy window object
+ :param keycode: A tuple of (keycode, keyname). Keynames are always lowercase
+ :param text: The text printed by this key, not accounting for modifiers, or `None` if no text.
+ Seems to pretty naively interpret the keycode as unicode, so numlock can return odd characters.
+ :param modifiers: A list of string modifiers, like `ctrl` or `numlock`
+ """
+ if keycode[1] == 'up':
+ self._change_to_history_text_if_available(self._command_history_index + 1)
+ return True
+ if keycode[1] == 'down':
+ self._change_to_history_text_if_available(self._command_history_index - 1)
+ return True
+ return super().keyboard_on_key_down(window, keycode, text, modifiers)
+
+ def _change_to_history_text_if_available(self, new_index: int) -> None:
+ if new_index < -1:
+ return
+ if new_index >= len(self._command_history):
+ return
+ self._command_history_index = new_index
+ if new_index == -1:
+ self.text = ""
+ return
+ self.text = self._command_history[self._command_history_index]
+
+
class MessageBox(Popup):
class MessageBoxLabel(Label):
def __init__(self, **kwargs):
@@ -292,7 +446,7 @@ def __init__(self, **kwargs):
def __init__(self, title, text, error=False, **kwargs):
label = MessageBox.MessageBoxLabel(text=text)
separator_color = [217 / 255, 129 / 255, 122 / 255, 1.] if error else [47 / 255., 167 / 255., 212 / 255, 1.]
- super().__init__(title=title, content=label, size_hint=(None, None), width=max(100, int(label.width)+40),
+ super().__init__(title=title, content=label, size_hint=(None, None), width=max(100, int(label.width) + 40),
separator_color=separator_color, **kwargs)
self.height += max(0, label.height - 18)
@@ -313,7 +467,7 @@ def __init__(self, ctx: context_type):
self.commandprocessor = ctx.command_processor(ctx)
self.icon = r"data/icon.png"
self.json_to_kivy_parser = KivyJSONtoTextParser(ctx)
- self.log_panels = {}
+ self.log_panels: typing.Dict[str, Widget] = {}
# keep track of last used command to autofill on click
self.last_autofillable_command = "hint"
@@ -348,11 +502,14 @@ def build(self) -> Layout:
# top part
server_label = ServerLabel()
self.connect_layout.add_widget(server_label)
- self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:", size_hint_y=None,
+ self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
+ size_hint_y=None,
height=dp(30), multiline=False, write_tab=False)
+
def connect_bar_validate(sender):
if not self.ctx.server:
self.connect_button_action(sender)
+
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
self.connect_layout.add_widget(self.server_connect_bar)
self.server_connect_button = Button(text="Connect", size=(dp(100), dp(30)), size_hint_y=None, size_hint_x=None)
@@ -373,26 +530,28 @@ def connect_bar_validate(sender):
bridge_logger = logging.getLogger(logger_name)
panel = TabbedPanelItem(text=display_name)
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
- self.tabs.add_widget(panel)
+ if len(self.logging_pairs) > 1:
+ # show Archipelago tab if other logging is present
+ self.tabs.add_widget(panel)
+
+ hint_panel = TabbedPanelItem(text="Hints")
+ self.log_panels["Hints"] = hint_panel.content = HintLog(self.json_to_kivy_parser)
+ self.tabs.add_widget(hint_panel)
+
+ if len(self.logging_pairs) == 1:
+ self.tabs.default_tab_text = "Archipelago"
self.main_area_container = GridLayout(size_hint_y=1, rows=1)
self.main_area_container.add_widget(self.tabs)
self.grid.add_widget(self.main_area_container)
- if len(self.logging_pairs) == 1:
- # Hide Tab selection if only one tab
- self.tabs.clear_tabs()
- self.tabs.do_default_tab = False
- self.tabs.current_tab.height = 0
- self.tabs.tab_height = 0
-
# bottom part
bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None)
info_button.bind(on_release=self.command_button_action)
bottom_layout.add_widget(info_button)
- self.textinput = TextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
+ self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
self.textinput.bind(on_text_validate=self.on_message)
self.textinput.text_validate_unfocus = False
bottom_layout.add_widget(self.textinput)
@@ -412,7 +571,7 @@ def connect_bar_validate(sender):
return self.container
def update_texts(self, dt):
- if hasattr(self.tabs.content.children[0], 'fix_heights'):
+ if hasattr(self.tabs.content.children[0], "fix_heights"):
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
if self.ctx.server:
self.title = self.base_title + " " + Utils.__version__ + \
@@ -436,8 +595,9 @@ def command_button_action(self, button):
"!help for server commands.")
def connect_button_action(self, button):
+ self.ctx.username = None
+ self.ctx.password = None
if self.ctx.server:
- self.ctx.username = None
async_start(self.ctx.disconnect())
else:
async_start(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", "")))
@@ -450,14 +610,18 @@ def on_stop(self):
self.ctx.exit_event.set()
- def on_message(self, textinput: TextInput):
+ def on_message(self, textinput: CommandPromptTextInput):
try:
input_text = textinput.text.strip()
textinput.text = ""
+ textinput.update_history(input_text)
if self.ctx.input_requests > 0:
self.ctx.input_requests -= 1
self.ctx.input_queue.put_nowait(input_text)
+ elif is_command_input(input_text):
+ self.ctx.on_ui_command(input_text)
+ self.commandprocessor(input_text)
elif input_text:
self.commandprocessor(input_text)
@@ -489,6 +653,10 @@ def set_new_energy_link_value(self):
if hasattr(self, "energy_link_label"):
self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J"
+ def update_hints(self):
+ hints = self.ctx.stored_data[f"_read_hints_{self.ctx.team}_{self.ctx.slot}"]
+ self.log_panels["Hints"].refresh_hints(hints)
+
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
def open_settings(self, *largs):
pass
@@ -503,12 +671,12 @@ def __init__(self, on_log):
def format_compact(record: logging.LogRecord) -> str:
if isinstance(record.msg, Exception):
return str(record.msg)
- return (f'{record.exc_info[1]}\n' if record.exc_info else '') + str(record.msg).split("\n")[0]
+ return (f"{record.exc_info[1]}\n" if record.exc_info else "") + str(record.msg).split("\n")[0]
def handle(self, record: logging.LogRecord) -> None:
- if getattr(record, 'skip_gui', False):
+ if getattr(record, "skip_gui", False):
pass # skip output
- elif getattr(record, 'compact_gui', False):
+ elif getattr(record, "compact_gui", False):
self.on_log(self.format_compact(record))
else:
self.on_log(self.format(record))
@@ -542,6 +710,67 @@ def fix_heights(self):
element.height = element.texture_size[1]
+class HintLog(RecycleView):
+ header = {
+ "receiving": {"text": "[u]Receiving Player[/u]"},
+ "item": {"text": "[u]Item[/u]"},
+ "finding": {"text": "[u]Finding Player[/u]"},
+ "location": {"text": "[u]Location[/u]"},
+ "entrance": {"text": "[u]Entrance[/u]"},
+ "found": {"text": "[u]Status[/u]"},
+ "striped": True,
+ }
+
+ sort_key: str = ""
+ reversed: bool = False
+
+ def __init__(self, parser):
+ super(HintLog, self).__init__()
+ self.data = [self.header]
+ self.parser = parser
+
+ def refresh_hints(self, hints):
+ data = []
+ for hint in hints:
+ data.append({
+ "receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})},
+ "item": {"text": self.parser.handle_node({
+ "type": "item_id",
+ "text": hint["item"],
+ "flags": hint["item_flags"],
+ "player": hint["receiving_player"],
+ })},
+ "finding": {"text": self.parser.handle_node({"type": "player_id", "text": hint["finding_player"]})},
+ "location": {"text": self.parser.handle_node({
+ "type": "location_id",
+ "text": hint["location"],
+ "player": hint["finding_player"],
+ })},
+ "entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text",
+ "color": "blue", "text": hint["entrance"]
+ if hint["entrance"] else "Vanilla"})},
+ "found": {
+ "text": self.parser.handle_node({"type": "color", "color": "green" if hint["found"] else "red",
+ "text": "Found" if hint["found"] else "Not Found"})},
+ })
+
+ data.sort(key=self.hint_sorter, reverse=self.reversed)
+ for i in range(0, len(data), 2):
+ data[i]["striped"] = True
+ data.insert(0, self.header)
+ self.data = data
+
+ @staticmethod
+ def hint_sorter(element: dict) -> str:
+ return ""
+
+ def fix_heights(self):
+ """Workaround fix for divergent texture and layout heights"""
+ for element in self.children[0].children:
+ max_height = max(child.texture_size[1] for child in element.children)
+ element.height = max_height
+
+
class E(ExceptionHandler):
logger = logging.getLogger("Client")
@@ -570,15 +799,17 @@ def __call__(self, *args, **kwargs):
def _handle_item_name(self, node: JSONMessagePart):
flags = node.get("flags", 0)
+ item_types = []
if flags & 0b001: # advancement
- itemtype = "progression"
- elif flags & 0b010: # useful
- itemtype = "useful"
- elif flags & 0b100: # trap
- itemtype = "trap"
- else:
- itemtype = "normal"
- node.setdefault("refs", []).append("Item Class: " + itemtype)
+ item_types.append("progression")
+ if flags & 0b010: # useful
+ item_types.append("useful")
+ if flags & 0b100: # trap
+ item_types.append("trap")
+ if not item_types:
+ item_types.append("normal")
+
+ node.setdefault("refs", []).append("Item Class: " + ", ".join(item_types))
return super(KivyJSONtoTextParser, self)._handle_item_name(node)
def _handle_player_id(self, node: JSONMessagePart):
@@ -588,8 +819,10 @@ def _handle_player_id(self, node: JSONMessagePart):
text = f"Game: {slot_info.game} " \
f"Type: {SlotType(slot_info.type).name}"
if slot_info.group_members:
- text += f" Members: " + \
- ' '.join(self.ctx.player_names[player] for player in slot_info.group_members)
+ text += f" Members: " + " ".join(
+ escape_markup(self.ctx.player_names[player])
+ for player in slot_info.group_members
+ )
node.setdefault("refs", []).append(text)
return super(KivyJSONtoTextParser, self)._handle_player_id(node)
@@ -604,6 +837,10 @@ def _handle_color(self, node: JSONMessagePart):
return self._handle_text(node)
def _handle_text(self, node: JSONMessagePart):
+ # All other text goes through _handle_color, and we don't want to escape markup twice,
+ # or mess up text that already has intentional markup applied to it
+ if node.get("type", "text") == "text":
+ node["text"] = escape_markup(node["text"])
for ref in node.get("refs", []):
node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]"
self.ref_count += 1
@@ -617,4 +854,3 @@ def _handle_text(self, node: JSONMessagePart):
if os.path.exists(user_file):
logging.info("Loading user.kv into builder.")
Builder.load_file(user_file)
-
diff --git a/playerSettings.yaml b/playerSettings.yaml
deleted file mode 100644
index e28963ddb340..000000000000
--- a/playerSettings.yaml
+++ /dev/null
@@ -1,588 +0,0 @@
-# What is this file?
-# This file contains options which allow you to configure your multiworld experience while allowing others
-# to play how they want as well.
-
-# How do I use it?
-# The options in this file are weighted. This means the higher number you assign to a value, the more
-# chances you have for that option to be chosen. For example, an option like this:
-#
-# map_shuffle:
-# on: 5
-# off: 15
-#
-# Means you have 5 chances for map shuffle to occur, and 15 chances for map shuffle to be turned off
-
-# I've never seen a file like this before. What characters am I allowed to use?
-# This is a .yaml file. You are allowed to use most characters.
-# To test if your yaml is valid or not, you can use this website:
-# http://www.yamllint.com/
-
-description: Template Name # Used to describe your yaml. Useful if you have multiple files
-name: YourName{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
-#{player} will be replaced with the player's slot number.
-#{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1.
-#{number} will be replaced with the counter value of the name.
-#{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1.
-game: # Pick a game to play
- A Link to the Past: 1
-requires:
- version: 0.3.3 # Version of Archipelago required for this yaml to work as expected.
-A Link to the Past:
- progression_balancing:
- # A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
- # A lower setting means more getting stuck. A higher setting means less getting stuck.
- #
- # You can define additional values between the minimum and maximum values.
- # Minimum value is 0
- # Maximum value is 99
- random: 0
- random-low: 0
- random-high: 0
- disabled: 0 # equivalent to 0
- normal: 50 # equivalent to 50
- extreme: 0 # equivalent to 99
-
- accessibility:
- # Set rules for reachability of your items/locations.
- # Locations: ensure everything can be reached and acquired.
- # Items: ensure all logically relevant items can be acquired.
- # Minimal: ensure what is needed to reach your goal can be acquired.
- locations: 0
- items: 50
- minimal: 0
-
- local_items:
- # Forces these items to be in their native world.
- [ ]
-
- non_local_items:
- # Forces these items to be outside their native world.
- [ ]
-
- start_inventory:
- # Start with these items.
- { }
-
- start_hints:
- # Start with these item's locations prefilled into the !hint command.
- [ ]
-
- start_location_hints:
- # Start with these locations and their item prefilled into the !hint command
- [ ]
-
- exclude_locations:
- # Prevent these locations from having an important item
- [ ]
-
- priority_locations:
- # Prevent these locations from having an unimportant item
- [ ]
-
- item_links:
- # Share part of your item pool with other players.
- [ ]
-
- ### Logic Section ###
- glitches_required: # Determine the logic required to complete the seed
- none: 50 # No glitches required
- minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic
- overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches
- hybrid_major_glitches: 0 # In addition to overworld glitches, also requires underworld clips between dungeons.
- no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx.
- # Other players items are placed into your world under HMG logic
- dark_room_logic: # Logic for unlit dark rooms
- lamp: 50 # require the Lamp for these rooms to be considered accessible.
- torches: 0 # in addition to lamp, allow the fire rod and presence of easily accessible torches for access
- none: 0 # all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness
- restrict_dungeon_item_on_boss: # aka ambrosia boss items
- on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
- off: 50
- ### End of Logic Section ###
- bigkey_shuffle: # Big Key Placement
- original_dungeon: 50
- own_dungeons: 0
- own_world: 0
- any_world: 0
- different_world: 0
- start_with: 0
- smallkey_shuffle: # Small Key Placement
- original_dungeon: 50
- own_dungeons: 0
- own_world: 0
- any_world: 0
- different_world: 0
- universal: 0
- start_with: 0
- compass_shuffle: # Compass Placement
- original_dungeon: 50
- own_dungeons: 0
- own_world: 0
- any_world: 0
- different_world: 0
- start_with: 0
- map_shuffle: # Map Placement
- original_dungeon: 50
- own_dungeons: 0
- own_world: 0
- any_world: 0
- different_world: 0
- start_with: 0
- dungeon_counters:
- on: 0 # Always display amount of items checked in a dungeon
- pickup: 50 # Show when compass is picked up
- default: 0 # Show when compass is picked up if the compass itself is shuffled
- off: 0 # Never show item count in dungeons
- progressive: # Enable or disable progressive items (swords, shields, bow)
- on: 50 # All items are progressive
- off: 0 # No items are progressive
- grouped_random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
- entrance_shuffle:
- none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
- dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
- dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons, but keep dungeons to a specific world
- dungeonscrossed: 0 # like dungeonsfull, but allow cross-world traversal through a dungeon. Warning: May force repeated dungeon traversal
- simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules
- restricted: 0 # Less strict than simple
- full: 0 # Less strict than restricted
- crossed: 0 # Less strict than full
- insanity: 0 # Very few grouping rules. Good luck
- # you can also define entrance shuffle seed, like so:
- crossed-1000: 0 # using this method, you can have the same layout as another player and share entrance information
- # however, many other settings like logic, world state, retro etc. may affect the shuffle result as well.
- crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed
- goals:
- ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon
- crystals: 0 # Only killing Ganon is required. However, items may still be placed in GT
- bosses: 0 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
- pedestal: 0 # Pull the Triforce from the Master Sword pedestal
- ganon_pedestal: 0 # Pull the Master Sword pedestal, then kill Ganon
- triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
- local_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
- ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
- local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
- ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock.
- open_pyramid:
- goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons
- auto: 0 # Same as Goal, but also is closed if holes are shuffled and ganon is part of the shuffle pool
- open: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt
- closed: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower
- triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
- extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
- percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
- available: 50 # available = triforce_pieces_available
- triforce_pieces_extra: # Set to how many extra triforces pieces are available to collect in the world.
- # Format "pieces: chance"
- 0: 0
- 5: 50
- 10: 50
- 15: 0
- 20: 0
- triforce_pieces_percentage: # Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world.
- # Format "pieces: chance"
- 100: 0 #No extra
- 150: 50 #Half the required will be added as extra
- 200: 0 #There are the double of the required ones available.
- triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1
- # Format "pieces: chance"
- 25: 0
- 30: 50
- 40: 0
- 50: 0
- triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1
- # Format "pieces: chance"
- 15: 0
- 20: 50
- 30: 0
- 40: 0
- 50: 0
- crystals_needed_for_gt: # Crystals required to open GT
- 0: 0
- 7: 50
- random: 0
- random-low: 0 # any valid number, weighted towards the lower end
- random-middle: 0 # any valid number, weighted towards the central range
- random-high: 0 # any valid number, weighted towards the higher end
- crystals_needed_for_ganon: # Crystals required to hurt Ganon
- 0: 0
- 7: 50
- random: 0
- random-low: 0
- random-middle: 0
- random-high: 0
- mode:
- standard: 0 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary
- open: 50 # Begin the game from your choice of Link's House or the Sanctuary
- inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered
- retro_bow:
- on: 0 # Zelda-1 like mode. You have to purchase a quiver to shoot arrows using rupees.
- off: 50
- retro_caves:
- on: 0 # Zelda-1 like mode. There are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion.
- off: 50
- hints: # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints.
- 'on': 50
- 'off': 0
- full: 0
- scams: # If on, these Merchants will no longer tell you what they're selling.
- 'off': 50
- 'king_zora': 0
- 'bottle_merchant': 0
- 'all': 0
- swordless:
- on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
- off: 1
- item_pool:
- easy: 0 # Doubled upgrades, progressives, and etc
- normal: 50 # Item availability remains unchanged from vanilla game
- hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless)
- expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless)
- item_functionality:
- easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
- normal: 50 # Vanilla item functionality
- hard: 0 # Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon)
- expert: 0 # Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon)
- tile_shuffle: # Randomize the tile layouts in flying tile rooms
- on: 0
- off: 50
- misery_mire_medallion: # required medallion to open Misery Mire front entrance
- random: 50
- Ether: 0
- Bombos: 0
- Quake: 0
- turtle_rock_medallion: # required medallion to open Turtle Rock front entrance
- random: 50
- Ether: 0
- Bombos: 0
- Quake: 0
- ### Enemizer Section ###
- boss_shuffle:
- none: 50 # Vanilla bosses
- basic: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons
- full: 0 # 3 bosses can occur twice
- chaos: 0 # Any boss can appear any amount of times
- singularity: 0 # Picks a boss, tries to put it everywhere that works, if there's spaces remaining it picks a boss to fill those
- enemy_shuffle: # Randomize enemy placement
- on: 0
- off: 50
- killable_thieves: # Make thieves killable
- on: 0 # Usually turned on together with enemy_shuffle to make annoying thief placement more manageable
- off: 50
- bush_shuffle: # Randomize the chance that bushes have enemies and the enemies under said bush
- on: 0
- off: 50
- enemy_damage:
- default: 50 # Vanilla enemy damage
- shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps
- chaos: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage
- enemy_health:
- default: 50 # Vanilla enemy HP
- easy: 0 # Enemies have reduced health
- hard: 0 # Enemies have increased health
- expert: 0 # Enemies have greatly increased health
- pot_shuffle:
- 'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile
- 'off': 50 # Default pot item locations
- ### End of Enemizer Section ###
- ### Beemizer ###
- # can add weights for any whole number between 0 and 100
- beemizer_total_chance: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps
- 0: 50 # No junk fill items are replaced (Beemizer is off)
- 25: 0 # 25% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
- 50: 0 # 50% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
- 75: 0 # 75% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
- 100: 0 # All junk fill items (rupees, bombs and arrows) are replaced with bees
- beemizer_trap_chance:
- 60: 50 # 60% chance for each beemizer replacement to be a trap, 40% chance to be a single bee
- 70: 0 # 70% chance for each beemizer replacement to be a trap, 30% chance to be a single bee
- 80: 0 # 80% chance for each beemizer replacement to be a trap, 20% chance to be a single bee
- 90: 0 # 90% chance for each beemizer replacement to be a trap, 10% chance to be a single bee
- 100: 0 # All beemizer replacements are traps
- ### Shop Settings ###
- shop_item_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
- 0: 50
- 5: 0
- 15: 0
- 30: 0
- random: 0 # 0 to 30 evenly distributed
- shop_price_modifier: # Percentage modifier for shuffled item prices in shops
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 400: 0 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- 100: 50
- shop_shuffle:
- none: 50
- g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
- f: 0 # Generate new default inventories for every shop independently
- i: 0 # Shuffle default inventories of the shops around
- p: 0 # Randomize the prices of the items in shop inventories
- u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
- w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
- P: 0 # Prices of the items in shop inventories cost hearts, arrow, or bombs instead of rupees
- ip: 0 # Shuffle inventories and randomize prices
- fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool
- uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool
- # You can add more combos
- ### End of Shop Section ###
- shuffle_prizes: # aka drops
- none: 0 # do not shuffle prize packs
- g: 50 # shuffle "general" prize packs, as in enemy, tree pull, dig etc.
- b: 0 # shuffle "bonk" prize packs
- bg: 0 # shuffle both
- timer:
- none: 50 # No timer will be displayed.
- timed: 0 # Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.
- timed_ohko: 0 # Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.
- ohko: 0 # Timer always at zero. Permanent OHKO.
- timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.
- display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool.
- countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with
- 0: 0 # For timed_ohko, starts in OHKO mode when starting the game
- 10: 50
- 20: 0
- 30: 0
- 60: 0
- red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock
- -2: 50
- 1: 0
- blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock
- 1: 0
- 2: 50
- green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock
- 4: 50
- 10: 0
- 15: 0
- glitch_boots:
- on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them
- off: 0
- # rom options section
- random_sprite_on_event: # An alternative to specifying randomonhit / randomonexit / etc... in sprite down below.
- enabled: # If enabled, sprite down below is ignored completely, (although it may become the sprite pool)
- on: 0
- off: 1
- on_hit: # Random sprite on hit. Being hit by things that cause 0 damage still counts.
- on: 1
- off: 0
- on_enter: # Random sprite on underworld entry. Note that entering hobo counts.
- on: 0
- off: 1
- on_exit: # Random sprite on underworld exit. Exiting hobo does not count.
- on: 0
- off: 1
- on_slash: # Random sprite on sword slash. Note, it still counts if you attempt to slash while swordless.
- on: 0
- off: 1
- on_item: # Random sprite on getting an item. Anything that causes you to hold an item above your head counts.
- on: 0
- off: 1
- on_bonk: # Random sprite on bonk.
- on: 0
- off: 1
- on_everything: # Random sprite on ALL currently implemented events, even if not documented at present time.
- on: 0
- off: 1
- use_weighted_sprite_pool: # Always on if no sprite_pool exists, otherwise it controls whether to use sprite as a weighted sprite pool
- on: 0
- off: 1
- #sprite_pool: # When specified, limits the pool of sprites used for randomon-event to the specified pool. Uncomment to use this.
- # - link
- # - pride link
- # - penguin link
- # - random # You can specify random multiple times for however many potentially unique random sprites you want in your pool.
- sprite: # Enter the name of your preferred sprite and weight it appropriately
- random: 0
- randomonhit: 0 # Random sprite on hit
- randomonenter: 0 # Random sprite on entering the underworld.
- randomonexit: 0 # Random sprite on exiting the underworld.
- randomonslash: 0 # Random sprite on sword slashes
- randomonitem: 0 # Random sprite on getting items.
- randomonbonk: 0 # Random sprite on bonk.
- # You can combine these events like this. randomonhit-enter-exit if you want it on hit, enter, exit.
- randomonall: 0 # Random sprite on any and all currently supported events. Refer to above for the supported events.
- Link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it
- music: # If "off", all in-game music will be disabled
- on: 50
- off: 0
- quickswap: # Enable switching items by pressing the L+R shoulder buttons
- on: 50
- off: 0
- triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahadala
- normal: 0 # original behavior (always visible)
- hide_goal: 50 # hide counter until a piece is collected or speaking to Murahadala
- hide_required: 0 # Always visible, but required amount is invisible until determined by Murahadala
- hide_both: 0 # Hide both under above circumstances
- reduceflashing: # Reduces instances of flashing such as lightning attacks, weather, ether and more.
- on: 50
- off: 0
- menuspeed: # Controls how fast the item menu opens and closes
- normal: 50
- instant: 0
- double: 0
- triple: 0
- quadruple: 0
- half: 0
- heartcolor: # Controls the color of your health hearts
- red: 50
- blue: 0
- green: 0
- yellow: 0
- random: 0
- heartbeep: # Controls the frequency of the low-health beeping
- double: 0
- normal: 50
- half: 0
- quarter: 0
- off: 0
- ow_palettes: # Change the colors of the overworld
- default: 50 # No changes
- good: 0 # Shuffle the colors, with harmony in mind
- blackout: 0 # everything black / blind mode
- grayscale: 0
- negative: 0
- classic: 0
- dizzy: 0
- sick: 0
- puke: 0
- uw_palettes: # Change the colors of caves and dungeons
- default: 50 # No changes
- good: 0 # Shuffle the colors, with harmony in mind
- blackout: 0 # everything black / blind mode
- grayscale: 0
- negative: 0
- classic: 0
- dizzy: 0
- sick: 0
- puke: 0
- hud_palettes: # Change the colors of the hud
- default: 50 # No changes
- good: 0 # Shuffle the colors, with harmony in mind
- blackout: 0 # everything black / blind mode
- grayscale: 0
- negative: 0
- classic: 0
- dizzy: 0
- sick: 0
- puke: 0
- sword_palettes: # Change the colors of swords
- default: 50 # No changes
- good: 0 # Shuffle the colors, with harmony in mind
- blackout: 0 # everything black / blind mode
- grayscale: 0
- negative: 0
- classic: 0
- dizzy: 0
- sick: 0
- puke: 0
- shield_palettes: # Change the colors of shields
- default: 50 # No changes
- good: 0 # Shuffle the colors, with harmony in mind
- blackout: 0 # everything black / blind mode
- grayscale: 0
- negative: 0
- classic: 0
- dizzy: 0
- sick: 0
- puke: 0
-
- # triggers that replace options upon rolling certain options
- legacy_weapons: # this is not an actual option, just a set of weights to trigger from
- trigger_disabled: 50
- randomized: 0 # Swords are placed randomly throughout the world
- assured: 0 # Begin with a sword, the rest are placed randomly throughout the world
- vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
- swordless: 0 # swordless mode
-
- death_link:
- false: 50
- true: 0
-
- allow_collect: # Allows for !collect / co-op to auto-open chests containing items for other players.
- # Off by default, because it currently crashes on real hardware.
- false: 50
- true: 0
-
-linked_options:
- - name: crosskeys
- options: # These overwrite earlier options if the percentage chance triggers
- A Link to the Past:
- entrance_shuffle: crossed
- bigkey_shuffle: true
- compass_shuffle: true
- map_shuffle: true
- smallkey_shuffle: true
- percentage: 0 # Set this to the percentage chance you want crosskeys
- - name: localcrosskeys
- options: # These overwrite earlier options if the percentage chance triggers
- A Link to the Past:
- entrance_shuffle: crossed
- bigkey_shuffle: true
- compass_shuffle: true
- map_shuffle: true
- smallkey_shuffle: true
- local_items: # Forces keys to be local to your own world
- - "Small Keys"
- - "Big Keys"
- percentage: 0 # Set this to the percentage chance you want local crosskeys
- - name: enemizer
- options:
- A Link to the Past:
- boss_shuffle: # Subchances can be injected too, which then get rolled
- basic: 1
- full: 1
- chaos: 1
- singularity: 1
- enemy_damage:
- shuffled: 1
- chaos: 1
- enemy_health:
- easy: 1
- hard: 1
- expert: 1
- percentage: 0 # Set this to the percentage chance you want enemizer
-triggers:
- # trigger block for legacy weapons mode, to enable these add weights to legacy_weapons
- - option_name: legacy_weapons
- option_result: randomized
- option_category: A Link to the Past
- options:
- A Link to the Past:
- swordless: off
- - option_name: legacy_weapons
- option_result: assured
- option_category: A Link to the Past
- options:
- A Link to the Past:
- swordless: off
- start_inventory:
- Progressive Sword: 1
- - option_name: legacy_weapons
- option_result: vanilla
- option_category: A Link to the Past
- options:
- A Link to the Past:
- swordless: off
- plando_items:
- - items:
- Progressive Sword: 4
- locations:
- - Master Sword Pedestal
- - Pyramid Fairy - Left
- - Blacksmith
- - Link's Uncle
- - option_name: legacy_weapons
- option_result: swordless
- option_category: A Link to the Past
- options:
- A Link to the Past:
- swordless: on
- # end of legacy weapons block
- - option_name: enemy_damage # targets enemy_damage
- option_category: A Link to the Past
- option_result: shuffled # if it rolls shuffled
- percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
- options: # then inserts these options
- A Link to the Past:
- swordless: off
diff --git a/pytest.ini b/pytest.ini
index 5599a3c90f3a..33e0bab8a98f 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,4 +1,4 @@
[pytest]
-python_files = Test*.py
+python_files = test_*.py Test*.py # TODO: remove Test* once all worlds have been ported
python_classes = Test
-python_functions = test
\ No newline at end of file
+python_functions = test
diff --git a/requirements.txt b/requirements.txt
index 610892848d15..db4f5445036a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,12 +1,14 @@
-colorama>=0.4.5
-websockets>=11.0.3
+colorama>=0.4.6
+websockets>=12.0
PyYAML>=6.0.1
-jellyfish>=1.0.0
-jinja2>=3.1.2
-schema>=0.7.5
-kivy>=2.2.0
-bsdiff4>=1.2.3
-platformdirs>=3.9.1
-certifi>=2023.7.22
-cython>=0.29.35
-cymem>=2.0.7
+jellyfish>=1.0.3
+jinja2>=3.1.4
+schema>=0.7.7
+kivy>=2.3.0
+bsdiff4>=1.2.4
+platformdirs>=4.2.2
+certifi>=2024.6.2
+cython>=3.0.10
+cymem>=2.0.8
+orjson>=3.10.3
+typing_extensions>=4.12.1
diff --git a/settings.py b/settings.py
index 4c9c1d1c3042..792770521459 100644
--- a/settings.py
+++ b/settings.py
@@ -1,8 +1,9 @@
"""
Application settings / host.yaml interface using type hints.
-This is different from player settings.
+This is different from player options.
"""
+import os
import os.path
import shutil
import sys
@@ -11,7 +12,6 @@
from enum import IntEnum
from threading import Lock
from typing import cast, Any, BinaryIO, ClassVar, Dict, Iterator, List, Optional, TextIO, Tuple, Union, TypeVar
-import os
__all__ = [
"get_settings", "fmt_doc", "no_gui",
@@ -118,7 +118,7 @@ def get_type_hints(cls) -> Dict[str, Any]:
cls._type_cache = typing.get_type_hints(cls, globalns=mod_dict, localns=cls.__dict__)
return cls._type_cache
- def get(self, key: str, default: Any) -> Any:
+ def get(self, key: str, default: Any = None) -> Any:
if key in self:
return self[key]
return default
@@ -200,7 +200,7 @@ def as_dict(self, *args: str, downcast: bool = True) -> Dict[str, Any]:
def _dump_value(cls, value: Any, f: TextIO, indent: str) -> None:
"""Write a single yaml line to f"""
from Utils import dump, Dumper as BaseDumper
- yaml_line: str = dump(value, Dumper=cast(BaseDumper, cls._dumper))
+ yaml_line: str = dump(value, Dumper=cast(BaseDumper, cls._dumper), width=2**31-1)
assert yaml_line.count("\n") == 1, f"Unexpected input for yaml dumper: {value}"
f.write(f"{indent}{yaml_line}")
@@ -597,8 +597,8 @@ class LogNetwork(IntEnum):
disable_item_cheat: Union[DisableItemCheat, bool] = False
location_check_points: LocationCheckPoints = LocationCheckPoints(1)
hint_cost: HintCost = HintCost(10)
- release_mode: ReleaseMode = ReleaseMode("goal")
- collect_mode: CollectMode = CollectMode("goal")
+ release_mode: ReleaseMode = ReleaseMode("auto")
+ collect_mode: CollectMode = CollectMode("auto")
remaining_mode: RemainingMode = RemainingMode("goal")
auto_shutdown: AutoShutdown = AutoShutdown(0)
compatibility: Compatibility = Compatibility(2)
@@ -643,17 +643,6 @@ class Spoiler(IntEnum):
PLAYTHROUGH = 2
FULL = 3
- class GlitchTriforceRoom(IntEnum):
- """
- Glitch to Triforce room from Ganon
- When disabled, you have to have a weapon that can hurt ganon (master sword or swordless/easy item functionality
- + hammer) and have completed the goal required for killing ganon to be able to access the triforce room.
- 1 -> Enabled.
- 0 -> Disabled (except in no-logic)
- """
- OFF = 0
- ON = 1
-
class PlandoOptions(str):
"""
List of options that can be plando'd. Can be combined, for example "bosses, items"
@@ -665,15 +654,23 @@ class Race(IntEnum):
OFF = 0
ON = 1
+ class PanicMethod(str):
+ """
+ What to do if the current item placements appear unsolvable.
+ raise -> Raise an exception and abort.
+ swap -> Attempt to fix it by swapping prior placements around. (Default)
+ start_inventory -> Move remaining items to start_inventory, generate additional filler items to fill locations.
+ """
+
enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows
player_files_path: PlayerFilesPath = PlayerFilesPath("Players")
players: Players = Players(0)
weights_file_path: WeightsFilePath = WeightsFilePath("weights.yaml")
meta_file_path: MetaFilePath = MetaFilePath("meta.yaml")
spoiler: Spoiler = Spoiler(3)
- glitch_triforce_room: GlitchTriforceRoom = GlitchTriforceRoom(1) # why is this here?
race: Race = Race(0)
- plando_options: PlandoOptions = PlandoOptions("bosses")
+ plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
+ panic_method: PanicMethod = PanicMethod("swap")
class SNIOptions(Group):
@@ -694,6 +691,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 +717,7 @@ class Settings(Group):
server_options: ServerOptions = ServerOptions()
generator: GeneratorOptions = GeneratorOptions()
sni_options: SNIOptions = SNIOptions()
+ bizhawkclient_options: BizHawkClientOptions = BizHawkClientOptions()
_filename: Optional[str] = None
@@ -781,6 +798,7 @@ def autosave() -> None:
atexit.register(autosave)
def save(self, location: Optional[str] = None) -> None: # as above
+ from Utils import parse_yaml
location = location or self._filename
assert location, "No file specified"
temp_location = location + ".tmp" # not using tempfile to test expected file access
@@ -790,10 +808,18 @@ def save(self, location: Optional[str] = None) -> None: # as above
# can't use utf-8-sig because it breaks backward compat: pyyaml on Windows with bytes does not strip the BOM
with open(temp_location, "w", encoding="utf-8") as f:
self.dump(f)
- # replace old with new
- if os.path.exists(location):
+ f.flush()
+ if hasattr(os, "fsync"):
+ os.fsync(f.fileno())
+ # validate new file is valid yaml
+ with open(temp_location, encoding="utf-8") as f:
+ parse_yaml(f.read())
+ # replace old with new, try atomic operation first
+ try:
+ os.rename(temp_location, location)
+ except (OSError, FileExistsError):
os.unlink(location)
- os.rename(temp_location, location)
+ os.rename(temp_location, location)
self._filename = location
def dump(self, f: TextIO, level: int = 0) -> None:
@@ -815,7 +841,6 @@ def get_settings() -> Settings:
with _lock: # make sure we only have one instance
res = getattr(get_settings, "_cache", None)
if not res:
- import os
from Utils import user_path, local_path
filenames = ("options.yaml", "host.yaml")
locations: List[str] = []
diff --git a/setup.py b/setup.py
index ce35c0f1cc5d..cb4d1a7511b6 100644
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,7 @@
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
try:
- requirement = 'cx-Freeze>=6.15.2'
+ requirement = 'cx-Freeze==7.2.0'
import pkg_resources
try:
pkg_resources.require(requirement)
@@ -54,7 +54,6 @@
# TODO: move stuff to not require this
import ModuleUpdate
ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv)
- ModuleUpdate.update_ran = False # restore for later
from worlds.LauncherComponents import components, icon_paths
from Utils import version_tuple, is_windows, is_linux
@@ -69,28 +68,22 @@
"Archipelago",
"ChecksFinder",
"Clique",
- "DLCQuest",
"Final Fantasy",
- "Hylics 2",
- "Kingdom Hearts 2",
"Lufia II Ancient Cave",
"Meritous",
"Ocarina of Time",
"Overcooked! 2",
"Raft",
- "Secret of Evermore",
"Slay the Spire",
"Sudoku",
"Super Mario 64",
"VVVVVV",
"Wargroove",
- "Zillion",
}
# LogicMixin is broken before 3.10 import revamp
if sys.version_info < (3,10):
non_apworlds.add("Hollow Knight")
- non_apworlds.add("Starcraft 2 Wings of Liberty")
def download_SNI():
print("Updating SNI")
@@ -197,7 +190,7 @@ def resolve_icon(icon_name: str):
c = next(component for component in components if component.script_name == "Launcher")
exes.append(cx_Freeze.Executable(
script=f"{c.script_name}.py",
- target_name=f"{c.frozen_name}(DEBUG).exe",
+ target_name=f"{c.frozen_name}Debug.exe",
icon=resolve_icon(c.icon),
))
@@ -235,8 +228,8 @@ def finalize_options(self):
# Override cx_Freeze's build_exe command for pre and post build steps
-class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
- user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [
+class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
+ user_options = cx_Freeze.command.build_exe.build_exe.user_options + [
('yes', 'y', 'Answer "yes" to all questions.'),
('extra-data=', None, 'Additional files to add.'),
]
@@ -307,7 +300,6 @@ def run(self):
print(f"Outputting to: {self.buildfolder}")
os.makedirs(self.buildfolder, exist_ok=True)
import ModuleUpdate
- ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
ModuleUpdate.update(yes=self.yes)
# auto-build cython modules
@@ -354,6 +346,18 @@ def run(self):
for folder in sdl2.dep_bins + glew.dep_bins:
shutil.copytree(folder, self.libfolder, dirs_exist_ok=True)
print(f"copying {folder} -> {self.libfolder}")
+ # windows needs Visual Studio C++ Redistributable
+ # Installer works for x64 and arm64
+ print("Downloading VC Redist")
+ import certifi
+ import ssl
+ context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
+ with urllib.request.urlopen(r"https://aka.ms/vs/17/release/vc_redist.x64.exe",
+ context=context) as download:
+ vc_redist = download.read()
+ print(f"Download complete, {len(vc_redist) / 1024 / 1024:.2f} MBytes downloaded.", )
+ with open("VC_redist.x64.exe", "wb") as vc_file:
+ vc_file.write(vc_redist)
for data in self.extra_data:
self.installfile(Path(data))
@@ -370,6 +374,10 @@ def run(self):
assert not non_apworlds - set(AutoWorldRegister.world_types), \
f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld"
folders_to_remove: typing.List[str] = []
+ disabled_worlds_folder = "worlds_disabled"
+ for entry in os.listdir(disabled_worlds_folder):
+ if os.path.isdir(os.path.join(disabled_worlds_folder, entry)):
+ folders_to_remove.append(entry)
generate_yaml_templates(self.buildfolder / "Players" / "Templates", False)
for worldname, worldtype in AutoWorldRegister.world_types.items():
if worldname not in non_apworlds:
@@ -385,8 +393,6 @@ def run(self):
folders_to_remove.append(file_name)
shutil.rmtree(world_directory)
shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml")
- # TODO: fix LttP options one day
- shutil.copyfile("playerSettings.yaml", self.buildfolder / "Players" / "Templates" / "A Link to the Past.yaml")
try:
from maseya import z3pr
except ImportError:
@@ -617,7 +623,7 @@ def find_lib(lib, arch, libc):
"excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas"],
"zip_include_packages": ["*"],
- "zip_exclude_packages": ["worlds", "sc2"],
+ "zip_exclude_packages": ["worlds", "sc2", "orjson"], # TODO: remove orjson here once we drop py3.8 support
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
"include_msvcr": False,
"replace_paths": ["*."],
diff --git a/test/TestBase.py b/test/TestBase.py
index dc79ad2855ed..bfd92346d301 100644
--- a/test/TestBase.py
+++ b/test/TestBase.py
@@ -1,254 +1,3 @@
-import pathlib
-import typing
-import unittest
-from argparse import Namespace
-
-import Utils
-from test.general import gen_steps
-from worlds import AutoWorld
-from worlds.AutoWorld import call_all
-
-file_path = pathlib.Path(__file__).parent.parent
-Utils.local_path.cached_path = file_path
-
-from BaseClasses import MultiWorld, CollectionState, ItemClassification, Item
-from worlds.alttp.Items import ItemFactory
-
-
-class TestBase(unittest.TestCase):
- multiworld: MultiWorld
- _state_cache = {}
-
- def get_state(self, items):
- if (self.multiworld, tuple(items)) in self._state_cache:
- return self._state_cache[self.multiworld, tuple(items)]
- state = CollectionState(self.multiworld)
- for item in items:
- item.classification = ItemClassification.progression
- state.collect(item)
- state.sweep_for_events()
- self._state_cache[self.multiworld, tuple(items)] = state
- return state
-
- def get_path(self, state, region):
- def flist_to_iter(node):
- while node:
- value, node = node
- yield value
-
- from itertools import zip_longest
- reversed_path_as_flist = state.path.get(region, (region, None))
- string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
- # Now we combine the flat string list into (region, exit) pairs
- pathsiter = iter(string_path_flat)
- pathpairs = zip_longest(pathsiter, pathsiter)
- return list(pathpairs)
-
- def run_location_tests(self, access_pool):
- for i, (location, access, *item_pool) in enumerate(access_pool):
- items = item_pool[0]
- all_except = item_pool[1] if len(item_pool) > 1 else None
- state = self._get_items(item_pool, all_except)
- path = self.get_path(state, self.multiworld.get_location(location, 1).parent_region)
- with self.subTest(msg="Reach Location", location=location, access=access, items=items,
- all_except=all_except, path=path, entry=i):
-
- self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access)
-
- # check for partial solution
- if not all_except and access: # we are not supposed to be able to reach location with partial inventory
- for missing_item in item_pool[0]:
- with self.subTest(msg="Location reachable without required item", location=location,
- items=item_pool[0], missing_item=missing_item, entry=i):
- state = self._get_items_partial(item_pool, missing_item)
- self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False)
-
- def run_entrance_tests(self, access_pool):
- for i, (entrance, access, *item_pool) in enumerate(access_pool):
- items = item_pool[0]
- all_except = item_pool[1] if len(item_pool) > 1 else None
- state = self._get_items(item_pool, all_except)
- path = self.get_path(state, self.multiworld.get_entrance(entrance, 1).parent_region)
- with self.subTest(msg="Reach Entrance", entrance=entrance, access=access, items=items,
- all_except=all_except, path=path, entry=i):
-
- self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), access)
-
- # check for partial solution
- if not all_except and access: # we are not supposed to be able to reach location with partial inventory
- for missing_item in item_pool[0]:
- with self.subTest(msg="Entrance reachable without required item", entrance=entrance,
- items=item_pool[0], missing_item=missing_item, entry=i):
- state = self._get_items_partial(item_pool, missing_item)
- self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False)
-
- def _get_items(self, item_pool, all_except):
- if all_except and len(all_except) > 0:
- items = self.multiworld.itempool[:]
- items = [item for item in items if
- item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
- items.extend(ItemFactory(item_pool[0], 1))
- else:
- items = ItemFactory(item_pool[0], 1)
- return self.get_state(items)
-
- def _get_items_partial(self, item_pool, missing_item):
- new_items = item_pool[0].copy()
- new_items.remove(missing_item)
- items = ItemFactory(new_items, 1)
- return self.get_state(items)
-
-
-class WorldTestBase(unittest.TestCase):
- options: typing.Dict[str, typing.Any] = {}
- multiworld: MultiWorld
-
- game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore"
- auto_construct: typing.ClassVar[bool] = True
- """ automatically set up a world for each test in this class """
-
- def setUp(self) -> None:
- if self.auto_construct:
- self.world_setup()
-
- def world_setup(self, seed: typing.Optional[int] = None) -> None:
- if type(self) is WorldTestBase or \
- (hasattr(WorldTestBase, self._testMethodName)
- and not self.run_default_tests and
- getattr(self, self._testMethodName).__code__ is
- getattr(WorldTestBase, self._testMethodName, None).__code__):
- return # setUp gets called for tests defined in the base class. We skip world_setup here.
- if not hasattr(self, "game"):
- raise NotImplementedError("didn't define game name")
- self.multiworld = MultiWorld(1)
- self.multiworld.game[1] = self.game
- self.multiworld.player_name = {1: "Tester"}
- self.multiworld.set_seed(seed)
- args = Namespace()
- for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].option_definitions.items():
- setattr(args, name, {
- 1: option.from_any(self.options.get(name, getattr(option, "default")))
- })
- self.multiworld.set_options(args)
- self.multiworld.set_default_common_options()
- for step in gen_steps:
- call_all(self.multiworld, step)
-
- # methods that can be called within tests
- def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]) -> None:
- """Collects all pre-placed items and items in the multiworld itempool except those provided"""
- if isinstance(item_names, str):
- item_names = (item_names,)
- for item in self.multiworld.get_items():
- if item.name not in item_names:
- self.multiworld.state.collect(item)
-
- def get_item_by_name(self, item_name: str) -> Item:
- """Returns the first item found in placed items, or in the itempool with the matching name"""
- for item in self.multiworld.get_items():
- if item.name == item_name:
- return item
- raise ValueError("No such item")
-
- def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
- """Returns actual items from the itempool that match the provided name(s)"""
- if isinstance(item_names, str):
- item_names = (item_names,)
- return [item for item in self.multiworld.itempool if item.name in item_names]
-
- def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
- """ collect all of the items in the item pool that have the given names """
- items = self.get_items_by_name(item_names)
- self.collect(items)
- return items
-
- def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
- """Collects the provided item(s) into state"""
- if isinstance(items, Item):
- items = (items,)
- for item in items:
- self.multiworld.state.collect(item)
-
- def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
- """Removes the provided item(s) from state"""
- if isinstance(items, Item):
- items = (items,)
- for item in items:
- if item.location and item.location.event and item.location in self.multiworld.state.events:
- self.multiworld.state.events.remove(item.location)
- self.multiworld.state.remove(item)
-
- def can_reach_location(self, location: str) -> bool:
- """Determines if the current state can reach the provide location name"""
- return self.multiworld.state.can_reach(location, "Location", 1)
-
- def can_reach_entrance(self, entrance: str) -> bool:
- """Determines if the current state can reach the provided entrance name"""
- return self.multiworld.state.can_reach(entrance, "Entrance", 1)
-
- def count(self, item_name: str) -> int:
- """Returns the amount of an item currently in state"""
- return self.multiworld.state.count(item_name, 1)
-
- def assertAccessDependency(self,
- locations: typing.List[str],
- possible_items: typing.Iterable[typing.Iterable[str]]) -> None:
- """Asserts that the provided locations can't be reached without the listed items but can be reached with any
- one of the provided combinations"""
- all_items = [item_name for item_names in possible_items for item_name in item_names]
-
- self.collect_all_but(all_items)
- for location in self.multiworld.get_locations():
- loc_reachable = self.multiworld.state.can_reach(location)
- self.assertEqual(loc_reachable, location.name not in locations,
- f"{location.name} is reachable without {all_items}" if loc_reachable
- else f"{location.name} is not reachable without {all_items}")
- for item_names in possible_items:
- items = self.collect_by_name(item_names)
- for location in locations:
- self.assertTrue(self.can_reach_location(location),
- f"{location} not reachable with {item_names}")
- self.remove(items)
-
- def assertBeatable(self, beatable: bool):
- """Asserts that the game can be beaten with the current state"""
- self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable)
-
- # following tests are automatically run
- @property
- def run_default_tests(self) -> bool:
- """Not possible or identical to the base test that's always being run already"""
- return (self.options
- or self.setUp.__code__ is not WorldTestBase.setUp.__code__
- or self.world_setup.__code__ is not WorldTestBase.world_setup.__code__)
-
- @property
- def constructed(self) -> bool:
- """A multiworld has been constructed by this point"""
- return hasattr(self, "game") and hasattr(self, "multiworld")
-
- def testAllStateCanReachEverything(self):
- """Ensure all state can reach everything and complete the game with the defined options"""
- if not (self.run_default_tests and self.constructed):
- return
- with self.subTest("Game", game=self.game):
- excluded = self.multiworld.exclude_locations[1].value
- state = self.multiworld.get_all_state(False)
- for location in self.multiworld.get_locations():
- if location.name not in excluded:
- with self.subTest("Location should be reached", location=location):
- reachable = location.can_reach(state)
- self.assertTrue(reachable, f"{location.name} unreachable")
- with self.subTest("Beatable"):
- self.multiworld.state = state
- self.assertBeatable(True)
-
- def testEmptyStateCanReachSomething(self):
- """Ensure empty state can reach at least one location with the defined options"""
- if not (self.run_default_tests and self.constructed):
- return
- with self.subTest("Game", game=self.game):
- state = CollectionState(self.multiworld)
- locations = self.multiworld.get_reachable_locations(state, 1)
- self.assertGreater(len(locations), 0,
- "Need to be able to reach at least one location to get started.")
+from .bases import TestBase, WorldTestBase
+from warnings import warn
+warn("TestBase was renamed to bases", DeprecationWarning)
diff --git a/test/__init__.py b/test/__init__.py
index 32622f65a927..37ebe3f62743 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -1,3 +1,4 @@
+import pathlib
import warnings
import settings
@@ -5,3 +6,13 @@
warnings.simplefilter("always")
settings.no_gui = True
settings.skip_autosave = True
+
+import ModuleUpdate
+
+ModuleUpdate.update_ran = True # don't upgrade
+
+import Utils
+
+file_path = pathlib.Path(__file__).parent.parent
+Utils.local_path.cached_path = file_path
+Utils.user_path() # initialize cached_path
diff --git a/test/bases.py b/test/bases.py
new file mode 100644
index 000000000000..5c2d241cbbfe
--- /dev/null
+++ b/test/bases.py
@@ -0,0 +1,347 @@
+import random
+import sys
+import typing
+import unittest
+from argparse import Namespace
+
+from Generate import get_seed_name
+from test.general import gen_steps
+from worlds import AutoWorld
+from worlds.AutoWorld import World, call_all
+
+from BaseClasses import Location, MultiWorld, CollectionState, ItemClassification, Item
+from worlds.alttp.Items import item_factory
+
+
+class TestBase(unittest.TestCase):
+ multiworld: MultiWorld
+ _state_cache = {}
+
+ def get_state(self, items):
+ if (self.multiworld, tuple(items)) in self._state_cache:
+ return self._state_cache[self.multiworld, tuple(items)]
+ state = CollectionState(self.multiworld)
+ for item in items:
+ item.classification = ItemClassification.progression
+ state.collect(item, event=True)
+ state.sweep_for_events()
+ state.update_reachable_regions(1)
+ self._state_cache[self.multiworld, tuple(items)] = state
+ return state
+
+ def get_path(self, state, region):
+ def flist_to_iter(node):
+ while node:
+ value, node = node
+ yield value
+
+ from itertools import zip_longest
+ reversed_path_as_flist = state.path.get(region, (region, None))
+ string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
+ # Now we combine the flat string list into (region, exit) pairs
+ pathsiter = iter(string_path_flat)
+ pathpairs = zip_longest(pathsiter, pathsiter)
+ return list(pathpairs)
+
+ def run_location_tests(self, access_pool):
+ for i, (location, access, *item_pool) in enumerate(access_pool):
+ items = item_pool[0]
+ all_except = item_pool[1] if len(item_pool) > 1 else None
+ state = self._get_items(item_pool, all_except)
+ path = self.get_path(state, self.multiworld.get_location(location, 1).parent_region)
+ with self.subTest(msg="Reach Location", location=location, access=access, items=items,
+ all_except=all_except, path=path, entry=i):
+
+ self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access,
+ f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
+
+ # check for partial solution
+ if not all_except and access: # we are not supposed to be able to reach location with partial inventory
+ for missing_item in item_pool[0]:
+ with self.subTest(msg="Location reachable without required item", location=location,
+ items=item_pool[0], missing_item=missing_item, entry=i):
+ state = self._get_items_partial(item_pool, missing_item)
+
+ self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False,
+ f"failed {self.multiworld.get_location(location, 1)}: succeeded with "
+ f"{missing_item} removed from: {item_pool}")
+
+ def run_entrance_tests(self, access_pool):
+ for i, (entrance, access, *item_pool) in enumerate(access_pool):
+ items = item_pool[0]
+ all_except = item_pool[1] if len(item_pool) > 1 else None
+ state = self._get_items(item_pool, all_except)
+ path = self.get_path(state, self.multiworld.get_entrance(entrance, 1).parent_region)
+ with self.subTest(msg="Reach Entrance", entrance=entrance, access=access, items=items,
+ all_except=all_except, path=path, entry=i):
+
+ self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), access)
+
+ # check for partial solution
+ if not all_except and access: # we are not supposed to be able to reach location with partial inventory
+ for missing_item in item_pool[0]:
+ with self.subTest(msg="Entrance reachable without required item", entrance=entrance,
+ items=item_pool[0], missing_item=missing_item, entry=i):
+ state = self._get_items_partial(item_pool, missing_item)
+ self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False,
+ f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}")
+
+ def _get_items(self, item_pool, all_except):
+ if all_except and len(all_except) > 0:
+ items = self.multiworld.itempool[:]
+ items = [item for item in items if
+ item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
+ items.extend(item_factory(item_pool[0], self.multiworld.worlds[1]))
+ else:
+ items = item_factory(item_pool[0], self.multiworld.worlds[1])
+ return self.get_state(items)
+
+ def _get_items_partial(self, item_pool, missing_item):
+ new_items = item_pool[0].copy()
+ new_items.remove(missing_item)
+ items = item_factory(new_items, self.multiworld.worlds[1])
+ return self.get_state(items)
+
+
+class WorldTestBase(unittest.TestCase):
+ options: typing.Dict[str, typing.Any] = {}
+ """Define options that should be used when setting up this TestBase."""
+ multiworld: MultiWorld
+ """The constructed MultiWorld instance after setup."""
+ world: World
+ """The constructed World instance after setup."""
+ player: typing.ClassVar[int] = 1
+
+ game: typing.ClassVar[str]
+ """Define game name in subclass, example "Secret of Evermore"."""
+ auto_construct: typing.ClassVar[bool] = True
+ """ automatically set up a world for each test in this class """
+ memory_leak_tested: typing.ClassVar[bool] = False
+ """ remember if memory leak test was already done for this class """
+
+ def setUp(self) -> None:
+ if self.auto_construct:
+ self.world_setup()
+
+ def tearDown(self) -> None:
+ if self.__class__.memory_leak_tested or not self.options or not self.constructed or \
+ sys.version_info < (3, 11, 0): # the leak check in tearDown fails in py<3.11 for an unknown reason
+ # only run memory leak test once per class, only for constructed with non-default options
+ # default options will be tested in test/general
+ super().tearDown()
+ return
+
+ import gc
+ import weakref
+ weak = weakref.ref(self.multiworld)
+ for attr_name in dir(self): # delete all direct references to MultiWorld and World
+ attr: object = typing.cast(object, getattr(self, attr_name))
+ if type(attr) is MultiWorld or isinstance(attr, AutoWorld.World):
+ delattr(self, attr_name)
+ state_cache: typing.Optional[typing.Dict[typing.Any, typing.Any]] = getattr(self, "_state_cache", None)
+ if state_cache is not None: # in case of multiple inheritance with TestBase, we need to clear its cache
+ state_cache.clear()
+ gc.collect()
+ self.__class__.memory_leak_tested = True
+ self.assertFalse(weak(), f"World {getattr(self, 'game', '')} leaked MultiWorld object")
+ super().tearDown()
+
+ def world_setup(self, seed: typing.Optional[int] = None) -> None:
+ if type(self) is WorldTestBase or \
+ (hasattr(WorldTestBase, self._testMethodName)
+ and not self.run_default_tests and
+ getattr(self, self._testMethodName).__code__ is
+ getattr(WorldTestBase, self._testMethodName, None).__code__):
+ return # setUp gets called for tests defined in the base class. We skip world_setup here.
+ if not hasattr(self, "game"):
+ raise NotImplementedError("didn't define game name")
+ self.multiworld = MultiWorld(1)
+ self.multiworld.game[self.player] = self.game
+ self.multiworld.player_name = {self.player: "Tester"}
+ self.multiworld.set_seed(seed)
+ self.multiworld.state = CollectionState(self.multiworld)
+ random.seed(self.multiworld.seed)
+ self.multiworld.seed_name = get_seed_name(random) # only called to get same RNG progression as Generate.py
+ args = Namespace()
+ for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
+ setattr(args, name, {
+ 1: option.from_any(self.options.get(name, option.default))
+ })
+ self.multiworld.set_options(args)
+ self.world = self.multiworld.worlds[self.player]
+ for step in gen_steps:
+ call_all(self.multiworld, step)
+
+ # methods that can be called within tests
+ def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]],
+ state: typing.Optional[CollectionState] = None) -> None:
+ """Collects all pre-placed items and items in the multiworld itempool except those provided"""
+ if isinstance(item_names, str):
+ item_names = (item_names,)
+ if not state:
+ state = self.multiworld.state
+ for item in self.multiworld.get_items():
+ if item.name not in item_names:
+ state.collect(item)
+
+ def get_item_by_name(self, item_name: str) -> Item:
+ """Returns the first item found in placed items, or in the itempool with the matching name"""
+ for item in self.multiworld.get_items():
+ if item.name == item_name:
+ return item
+ raise ValueError("No such item")
+
+ def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
+ """Returns actual items from the itempool that match the provided name(s)"""
+ if isinstance(item_names, str):
+ item_names = (item_names,)
+ return [item for item in self.multiworld.itempool if item.name in item_names]
+
+ def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
+ """ collect all of the items in the item pool that have the given names """
+ items = self.get_items_by_name(item_names)
+ self.collect(items)
+ return items
+
+ def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
+ """Collects the provided item(s) into state"""
+ if isinstance(items, Item):
+ items = (items,)
+ for item in items:
+ self.multiworld.state.collect(item)
+
+ def remove_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
+ """Remove all of the items in the item pool with the given names from state"""
+ items = self.get_items_by_name(item_names)
+ self.remove(items)
+ return items
+
+ def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
+ """Removes the provided item(s) from state"""
+ if isinstance(items, Item):
+ items = (items,)
+ for item in items:
+ if item.location and item.advancement and item.location in self.multiworld.state.events:
+ self.multiworld.state.events.remove(item.location)
+ self.multiworld.state.remove(item)
+
+ def can_reach_location(self, location: str) -> bool:
+ """Determines if the current state can reach the provided location name"""
+ return self.multiworld.state.can_reach(location, "Location", self.player)
+
+ def can_reach_entrance(self, entrance: str) -> bool:
+ """Determines if the current state can reach the provided entrance name"""
+ return self.multiworld.state.can_reach(entrance, "Entrance", self.player)
+
+ def can_reach_region(self, region: str) -> bool:
+ """Determines if the current state can reach the provided region name"""
+ return self.multiworld.state.can_reach(region, "Region", self.player)
+
+ def count(self, item_name: str) -> int:
+ """Returns the amount of an item currently in state"""
+ return self.multiworld.state.count(item_name, self.player)
+
+ def assertAccessDependency(self,
+ locations: typing.List[str],
+ possible_items: typing.Iterable[typing.Iterable[str]],
+ only_check_listed: bool = False) -> None:
+ """Asserts that the provided locations can't be reached without the listed items but can be reached with any
+ one of the provided combinations"""
+ all_items = [item_name for item_names in possible_items for item_name in item_names]
+
+ state = CollectionState(self.multiworld)
+ self.collect_all_but(all_items, state)
+ if only_check_listed:
+ for location in locations:
+ self.assertFalse(state.can_reach(location, "Location", self.player),
+ f"{location} is reachable without {all_items}")
+ else:
+ for location in self.multiworld.get_locations():
+ loc_reachable = state.can_reach(location, "Location", self.player)
+ self.assertEqual(loc_reachable, location.name not in locations,
+ f"{location.name} is reachable without {all_items}" if loc_reachable
+ else f"{location.name} is not reachable without {all_items}")
+ for item_names in possible_items:
+ items = self.get_items_by_name(item_names)
+ for item in items:
+ state.collect(item)
+ for location in locations:
+ self.assertTrue(state.can_reach(location, "Location", self.player),
+ f"{location} not reachable with {item_names}")
+ for item in items:
+ state.remove(item)
+
+ def assertBeatable(self, beatable: bool):
+ """Asserts that the game can be beaten with the current state"""
+ self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable)
+
+ # following tests are automatically run
+ @property
+ def run_default_tests(self) -> bool:
+ """Not possible or identical to the base test that's always being run already"""
+ return (self.options
+ or self.setUp.__code__ is not WorldTestBase.setUp.__code__
+ or self.world_setup.__code__ is not WorldTestBase.world_setup.__code__)
+
+ @property
+ def constructed(self) -> bool:
+ """A multiworld has been constructed by this point"""
+ return hasattr(self, "game") and hasattr(self, "multiworld")
+
+ def test_all_state_can_reach_everything(self):
+ """Ensure all state can reach everything and complete the game with the defined options"""
+ if not (self.run_default_tests and self.constructed):
+ return
+ with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
+ excluded = self.multiworld.worlds[self.player].options.exclude_locations.value
+ state = self.multiworld.get_all_state(False)
+ for location in self.multiworld.get_locations():
+ if location.name not in excluded:
+ with self.subTest("Location should be reached", location=location.name):
+ reachable = location.can_reach(state)
+ self.assertTrue(reachable, f"{location.name} unreachable")
+ with self.subTest("Beatable"):
+ self.multiworld.state = state
+ self.assertBeatable(True)
+
+ def test_empty_state_can_reach_something(self):
+ """Ensure empty state can reach at least one location with the defined options"""
+ if not (self.run_default_tests and self.constructed):
+ return
+ with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
+ state = CollectionState(self.multiworld)
+ locations = self.multiworld.get_reachable_locations(state, self.player)
+ self.assertGreater(len(locations), 0,
+ "Need to be able to reach at least one location to get started.")
+
+ def test_fill(self):
+ """Generates a multiworld and validates placements with the defined options"""
+ if not (self.run_default_tests and self.constructed):
+ return
+ from Fill import distribute_items_restrictive
+
+ # basically a shortened reimplementation of this method from core, in order to force the check is done
+ def fulfills_accessibility() -> bool:
+ locations = list(self.multiworld.get_locations(1))
+ state = CollectionState(self.multiworld)
+ while locations:
+ sphere: typing.List[Location] = []
+ for n in range(len(locations) - 1, -1, -1):
+ if locations[n].can_reach(state):
+ sphere.append(locations.pop(n))
+ self.assertTrue(sphere or self.multiworld.worlds[1].options.accessibility == "minimal",
+ f"Unreachable locations: {locations}")
+ if not sphere:
+ break
+ for location in sphere:
+ if location.item:
+ state.collect(location.item, True, location)
+ return self.multiworld.has_beaten_game(state, self.player)
+
+ with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
+ distribute_items_restrictive(self.multiworld)
+ call_all(self.multiworld, "post_fill")
+ self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.")
+ placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code]
+ self.assertLessEqual(len(self.multiworld.itempool), len(placed_items),
+ "Unplaced Items remaining in itempool")
diff --git a/test/benchmark/__init__.py b/test/benchmark/__init__.py
new file mode 100644
index 000000000000..6c80c60b89d7
--- /dev/null
+++ b/test/benchmark/__init__.py
@@ -0,0 +1,7 @@
+if __name__ == "__main__":
+ import path_change
+ path_change.change_home()
+ import load_worlds
+ load_worlds.run_load_worlds_benchmark()
+ import locations
+ locations.run_locations_benchmark()
diff --git a/test/benchmark/load_worlds.py b/test/benchmark/load_worlds.py
new file mode 100644
index 000000000000..3b001699f4cb
--- /dev/null
+++ b/test/benchmark/load_worlds.py
@@ -0,0 +1,27 @@
+def run_load_worlds_benchmark():
+ """List worlds and their load time.
+ Note that any first-time imports will be attributed to that world, as it is cached afterwards.
+ Likely best used with isolated worlds to measure their time alone."""
+ import logging
+
+ from Utils import init_logging
+
+ # get some general imports cached, to prevent it from being attributed to one world.
+ import orjson
+ orjson.loads("{}") # orjson runs initialization on first use
+
+ import BaseClasses, Launcher, Fill
+
+ from worlds import world_sources
+
+ init_logging("Benchmark Runner")
+ logger = logging.getLogger("Benchmark")
+
+ for module in world_sources:
+ logger.info(f"{module} took {module.time_taken:.4f} seconds.")
+
+
+if __name__ == "__main__":
+ from path_change import change_home
+ change_home()
+ run_load_worlds_benchmark()
diff --git a/test/benchmark/locations.py b/test/benchmark/locations.py
new file mode 100644
index 000000000000..f2209eb689e1
--- /dev/null
+++ b/test/benchmark/locations.py
@@ -0,0 +1,101 @@
+def run_locations_benchmark():
+ import argparse
+ import logging
+ import gc
+ import collections
+ import typing
+ import sys
+
+ from time_it import TimeIt
+
+ from Utils import init_logging
+ from BaseClasses import MultiWorld, CollectionState, Location
+ from worlds import AutoWorld
+ from worlds.AutoWorld import call_all
+
+ init_logging("Benchmark Runner")
+ logger = logging.getLogger("Benchmark")
+
+ class BenchmarkRunner:
+ gen_steps: typing.Tuple[str, ...] = (
+ "generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
+ rule_iterations: int = 100_000
+
+ if sys.version_info >= (3, 9):
+ @staticmethod
+ def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str:
+ return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
+ else:
+ @staticmethod
+ def format_times_from_counter(counter: collections.Counter, top: int = 5) -> str:
+ return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
+
+ def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float:
+ with TimeIt(f"{test_location.game} {self.rule_iterations} "
+ f"runs of {test_location}.access_rule({state_name})", logger) as t:
+ for _ in range(self.rule_iterations):
+ test_location.access_rule(state)
+ # if time is taken to disentangle complex ref chains,
+ # this time should be attributed to the rule.
+ gc.collect()
+ return t.dif
+
+ def main(self):
+ for game in sorted(AutoWorld.AutoWorldRegister.world_types):
+ summary_data: typing.Dict[str, collections.Counter[str]] = {
+ "empty_state": collections.Counter(),
+ "all_state": collections.Counter(),
+ }
+ try:
+ multiworld = MultiWorld(1)
+ multiworld.game[1] = game
+ multiworld.player_name = {1: "Tester"}
+ multiworld.set_seed(0)
+ multiworld.state = CollectionState(multiworld)
+ args = argparse.Namespace()
+ for name, option in AutoWorld.AutoWorldRegister.world_types[game].options_dataclass.type_hints.items():
+ setattr(args, name, {
+ 1: option.from_any(getattr(option, "default"))
+ })
+ multiworld.set_options(args)
+
+ gc.collect()
+ for step in self.gen_steps:
+ with TimeIt(f"{game} step {step}", logger):
+ call_all(multiworld, step)
+ gc.collect()
+
+ locations = sorted(multiworld.get_unfilled_locations())
+ if not locations:
+ continue
+
+ all_state = multiworld.get_all_state(False)
+ for location in locations:
+ time_taken = self.location_test(location, multiworld.state, "empty_state")
+ summary_data["empty_state"][location.name] = time_taken
+
+ time_taken = self.location_test(location, all_state, "all_state")
+ summary_data["all_state"][location.name] = time_taken
+
+ total_empty_state = sum(summary_data["empty_state"].values())
+ total_all_state = sum(summary_data["all_state"].values())
+
+ logger.info(f"{game} took {total_empty_state/len(locations):.4f} "
+ f"seconds per location in empty_state and {total_all_state/len(locations):.4f} "
+ f"in all_state. (all times summed for {self.rule_iterations} runs.)")
+ logger.info(f"Top times in empty_state:\n"
+ f"{self.format_times_from_counter(summary_data['empty_state'])}")
+ logger.info(f"Top times in all_state:\n"
+ f"{self.format_times_from_counter(summary_data['all_state'])}")
+
+ except Exception as e:
+ logger.exception(e)
+
+ runner = BenchmarkRunner()
+ runner.main()
+
+
+if __name__ == "__main__":
+ from path_change import change_home
+ change_home()
+ run_locations_benchmark()
diff --git a/test/benchmark/path_change.py b/test/benchmark/path_change.py
new file mode 100644
index 000000000000..2baa6273e11e
--- /dev/null
+++ b/test/benchmark/path_change.py
@@ -0,0 +1,16 @@
+import sys
+import os
+
+
+def change_home():
+ """Allow scripts to run from "this" folder."""
+ old_home = os.path.dirname(__file__)
+ sys.path.remove(old_home)
+ new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
+ os.chdir(new_home)
+ sys.path.append(new_home)
+ # fallback to local import
+ sys.path.append(old_home)
+
+ from Utils import local_path
+ local_path.cached_path = new_home
diff --git a/test/benchmark/time_it.py b/test/benchmark/time_it.py
new file mode 100644
index 000000000000..95c0314682f6
--- /dev/null
+++ b/test/benchmark/time_it.py
@@ -0,0 +1,23 @@
+import time
+
+
+class TimeIt:
+ def __init__(self, name: str, time_logger=None):
+ self.name = name
+ self.logger = time_logger
+ self.timer = None
+ self.end_timer = None
+
+ def __enter__(self):
+ self.timer = time.perf_counter()
+ return self
+
+ @property
+ def dif(self):
+ return self.end_timer - self.timer
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if not self.end_timer:
+ self.end_timer = time.perf_counter()
+ if self.logger:
+ self.logger.info(f"{self.dif:.4f} seconds in {self.name}.")
diff --git a/test/cpp/CMakeLists.txt b/test/cpp/CMakeLists.txt
new file mode 100644
index 000000000000..927b7494dac4
--- /dev/null
+++ b/test/cpp/CMakeLists.txt
@@ -0,0 +1,49 @@
+cmake_minimum_required(VERSION 3.5)
+project(ap-cpp-tests)
+
+enable_testing()
+
+find_package(GTest REQUIRED)
+
+if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
+ add_definitions("/source-charset:utf-8")
+ set(CMAKE_CXX_FLAGS_DEBUG "/MTd")
+ set(CMAKE_CXX_FLAGS_RELEASE "/MT")
+elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+ # enable static analysis for gcc
+ add_compile_options(-fanalyzer -Werror)
+ # disable stuff that gets triggered by googletest
+ add_compile_options(-Wno-analyzer-malloc-leak)
+ # enable asan for gcc
+ add_compile_options(-fsanitize=address)
+ add_link_options(-fsanitize=address)
+endif ()
+
+add_executable(test_default)
+
+target_include_directories(test_default
+ PRIVATE
+ ${GTEST_INCLUDE_DIRS}
+)
+
+target_link_libraries(test_default
+ ${GTEST_BOTH_LIBRARIES}
+)
+
+add_test(
+ NAME test_default
+ COMMAND test_default
+)
+
+set_property(
+ TEST test_default
+ PROPERTY ENVIRONMENT "ASAN_OPTIONS=allocator_may_return_null=1"
+)
+
+file(GLOB ITEMS *)
+foreach(item ${ITEMS})
+ if(IS_DIRECTORY ${item} AND EXISTS ${item}/CMakeLists.txt)
+ message(${item})
+ add_subdirectory(${item})
+ endif()
+endforeach()
diff --git a/test/cpp/README.md b/test/cpp/README.md
new file mode 100644
index 000000000000..792b9be77e72
--- /dev/null
+++ b/test/cpp/README.md
@@ -0,0 +1,32 @@
+# C++ tests
+
+Test framework for C and C++ code in AP.
+
+## Adding a Test
+
+### GoogleTest
+
+Adding GoogleTests is as simple as creating a directory with
+* one or more `test_*.cpp` files that define tests using
+ [GoogleTest API](https://google.github.io/googletest/)
+* a `CMakeLists.txt` that adds the .cpp files to `test_default` target using
+ [target_sources](https://cmake.org/cmake/help/latest/command/target_sources.html)
+
+### CTest
+
+If either GoogleTest is not suitable for the test or the build flags / sources / libraries are incompatible,
+you can add another CTest to the project using add_target and add_test, similar to how it's done for `test_default`.
+
+## Running Tests
+
+* Install [CMake](https://cmake.org/).
+* Build and/or install GoogleTest and make sure
+ [CMake can find it](https://cmake.org/cmake/help/latest/module/FindGTest.html), or
+ [create a parent `CMakeLists.txt` that fetches GoogleTest](https://google.github.io/googletest/quickstart-cmake.html).
+* Enter the directory with the top-most `CMakeLists.txt` and run
+ ```sh
+ mkdir build
+ cmake -S . -B build/ -DCMAKE_BUILD_TYPE=Release
+ cmake --build build/ --config Release && \
+ ctest --test-dir build/ -C Release --output-on-failure
+ ```
diff --git a/test/cpp/intset/CMakeLists.txt b/test/cpp/intset/CMakeLists.txt
new file mode 100644
index 000000000000..175e0bd0b9e8
--- /dev/null
+++ b/test/cpp/intset/CMakeLists.txt
@@ -0,0 +1,4 @@
+target_sources(test_default
+ PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/test_intset.cpp
+)
diff --git a/test/cpp/intset/test_intset.cpp b/test/cpp/intset/test_intset.cpp
new file mode 100644
index 000000000000..2f85bea960c4
--- /dev/null
+++ b/test/cpp/intset/test_intset.cpp
@@ -0,0 +1,105 @@
+#include
+#include
+#include
+
+// uint32Set
+#define INTSET_NAME uint32Set
+#define INTSET_TYPE uint32_t
+#include "../../../intset.h"
+#undef INTSET_NAME
+#undef INTSET_TYPE
+
+// int64Set
+#define INTSET_NAME int64Set
+#define INTSET_TYPE int64_t
+#include "../../../intset.h"
+
+
+TEST(IntsetTest, ZeroBuckets)
+{
+ // trying to allocate with zero buckets has to either fail or be functioning
+ uint32Set *set = uint32Set_new(0);
+ if (!set)
+ return; // failed -> OK
+
+ EXPECT_FALSE(uint32Set_contains(set, 1));
+ EXPECT_TRUE(uint32Set_add(set, 1));
+ EXPECT_TRUE(uint32Set_contains(set, 1));
+ uint32Set_free(set);
+}
+
+TEST(IntsetTest, Duplicate)
+{
+ // adding the same number again can't fail
+ uint32Set *set = uint32Set_new(2);
+ ASSERT_TRUE(set);
+ EXPECT_TRUE(uint32Set_add(set, 0));
+ EXPECT_TRUE(uint32Set_add(set, 0));
+ EXPECT_TRUE(uint32Set_contains(set, 0));
+ uint32Set_free(set);
+}
+
+TEST(IntsetTest, SetAllocFailure)
+{
+ // try to allocate 100TB of RAM, should fail and return NULL
+ if (sizeof(size_t) < 8)
+ GTEST_SKIP() << "Alloc error not testable on 32bit";
+ int64Set *set = int64Set_new(6250000000000ULL);
+ EXPECT_FALSE(set);
+ int64Set_free(set);
+}
+
+TEST(IntsetTest, SetAllocOverflow)
+{
+ // try to overflow argument passed to malloc
+ int64Set *set = int64Set_new(std::numeric_limits::max());
+ EXPECT_FALSE(set);
+ int64Set_free(set);
+}
+
+TEST(IntsetTest, NullFree)
+{
+ // free(NULL) should not try to free buckets
+ uint32Set_free(NULL);
+ int64Set_free(NULL);
+}
+
+TEST(IntsetTest, BucketRealloc)
+{
+ // add a couple of values to the same bucket to test growing the bucket
+ uint32Set* set = uint32Set_new(1);
+ ASSERT_TRUE(set);
+ EXPECT_FALSE(uint32Set_contains(set, 0));
+ EXPECT_TRUE(uint32Set_add(set, 0));
+ EXPECT_TRUE(uint32Set_contains(set, 0));
+ for (uint32_t i = 1; i < 32; ++i) {
+ EXPECT_TRUE(uint32Set_add(set, i));
+ EXPECT_TRUE(uint32Set_contains(set, i - 1));
+ EXPECT_TRUE(uint32Set_contains(set, i));
+ EXPECT_FALSE(uint32Set_contains(set, i + 1));
+ }
+ uint32Set_free(set);
+}
+
+TEST(IntSet, Max)
+{
+ constexpr auto n = std::numeric_limits::max();
+ uint32Set *set = uint32Set_new(1);
+ ASSERT_TRUE(set);
+ EXPECT_FALSE(uint32Set_contains(set, n));
+ EXPECT_TRUE(uint32Set_add(set, n));
+ EXPECT_TRUE(uint32Set_contains(set, n));
+ uint32Set_free(set);
+}
+
+TEST(InsetTest, Negative)
+{
+ constexpr auto n = std::numeric_limits::min();
+ static_assert(n < 0, "n not negative");
+ int64Set *set = int64Set_new(3);
+ ASSERT_TRUE(set);
+ EXPECT_FALSE(int64Set_contains(set, n));
+ EXPECT_TRUE(int64Set_add(set, n));
+ EXPECT_TRUE(int64Set_contains(set, n));
+ int64Set_free(set);
+}
diff --git a/test/general/TestFill.py b/test/general/TestFill.py
deleted file mode 100644
index 99f48cd0c70f..000000000000
--- a/test/general/TestFill.py
+++ /dev/null
@@ -1,827 +0,0 @@
-from typing import List, Iterable
-import unittest
-from worlds.AutoWorld import World
-from Fill import FillError, balance_multiworld_progression, fill_restrictive, \
- distribute_early_items, distribute_items_restrictive
-from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, Item, Location, \
- ItemClassification
-from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule
-
-
-def generate_multi_world(players: int = 1) -> MultiWorld:
- multi_world = MultiWorld(players)
- multi_world.player_name = {}
- for i in range(players):
- player_id = i+1
- world = World(multi_world, player_id)
- multi_world.game[player_id] = f"Game {player_id}"
- multi_world.worlds[player_id] = world
- multi_world.player_name[player_id] = "Test Player " + str(player_id)
- region = Region("Menu", player_id, multi_world, "Menu Region Hint")
- multi_world.regions.append(region)
-
- multi_world.set_seed(0)
- multi_world.set_default_common_options()
-
- return multi_world
-
-
-class PlayerDefinition(object):
- multiworld: MultiWorld
- id: int
- menu: Region
- locations: List[Location]
- prog_items: List[Item]
- basic_items: List[Item]
- regions: List[Region]
-
- def __init__(self, world: MultiWorld, id: int, menu: Region, locations: List[Location] = [], prog_items: List[Item] = [], basic_items: List[Item] = []):
- self.multiworld = world
- self.id = id
- self.menu = menu
- self.locations = locations
- self.prog_items = prog_items
- self.basic_items = basic_items
- self.regions = [menu]
-
- def generate_region(self, parent: Region, size: int, access_rule: CollectionRule = lambda state: True) -> Region:
- region_tag = "_region" + str(len(self.regions))
- region_name = "player" + str(self.id) + region_tag
- region = Region("player" + str(self.id) + region_tag, self.id, self.multiworld)
- self.locations += generate_locations(size, self.id, None, region, region_tag)
-
- entrance = Entrance(self.id, region_name + "_entrance", parent)
- parent.exits.append(entrance)
- entrance.connect(region)
- entrance.access_rule = access_rule
-
- self.regions.append(region)
- self.multiworld.regions.append(region)
-
- return region
-
-
-def fillRegion(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]:
- items = items.copy()
- while len(items) > 0:
- location = region.locations.pop(0)
- region.locations.append(location)
- if location.item:
- return items
- item = items.pop(0)
- world.push_item(location, item, False)
- location.event = item.advancement
-
- return items
-
-
-def regionContains(region: Region, item: Item) -> bool:
- for location in region.locations:
- if location.item == item:
- return True
-
- return False
-
-
-def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int = 0, prog_item_count: int = 0, basic_item_count: int = 0) -> PlayerDefinition:
- menu = multi_world.get_region("Menu", player_id)
- locations = generate_locations(location_count, player_id, None, menu)
- prog_items = generate_items(prog_item_count, player_id, True)
- multi_world.itempool += prog_items
- basic_items = generate_items(basic_item_count, player_id, False)
- multi_world.itempool += basic_items
-
- return PlayerDefinition(multi_world, player_id, menu, locations, prog_items, basic_items)
-
-
-def generate_locations(count: int, player_id: int, address: int = None, region: Region = None, tag: str = "") -> List[Location]:
- locations = []
- prefix = "player" + str(player_id) + tag + "_location"
- for i in range(count):
- name = prefix + str(i)
- location = Location(player_id, name, address, region)
- locations.append(location)
- region.locations.append(location)
- return locations
-
-
-def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]:
- items = []
- item_type = "prog" if advancement else ""
- for i in range(count):
- name = "player" + str(player_id) + "_" + item_type + "item" + str(i)
- items.append(Item(name,
- ItemClassification.progression if advancement else ItemClassification.filler,
- code, player_id))
- return items
-
-
-def names(objs: list) -> Iterable[str]:
- return map(lambda o: o.name, objs)
-
-
-class TestFillRestrictive(unittest.TestCase):
- def test_basic_fill(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(multi_world, 1, 2, 2)
-
- item0 = player1.prog_items[0]
- item1 = player1.prog_items[1]
- loc0 = player1.locations[0]
- loc1 = player1.locations[1]
-
- fill_restrictive(multi_world, multi_world.state,
- player1.locations, player1.prog_items)
-
- self.assertEqual(loc0.item, item1)
- self.assertEqual(loc1.item, item0)
- self.assertEqual([], player1.locations)
- self.assertEqual([], player1.prog_items)
-
- def test_ordered_fill(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(multi_world, 1, 2, 2)
- items = player1.prog_items
- locations = player1.locations
-
- multi_world.completion_condition[player1.id] = lambda state: state.has(
- items[0].name, player1.id) and state.has(items[1].name, player1.id)
- set_rule(locations[1], lambda state: state.has(
- items[0].name, player1.id))
- fill_restrictive(multi_world, multi_world.state,
- player1.locations.copy(), player1.prog_items.copy())
-
- self.assertEqual(locations[0].item, items[0])
- self.assertEqual(locations[1].item, items[1])
-
- def test_partial_fill(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(multi_world, 1, 3, 2)
-
- item0 = player1.prog_items[0]
- item1 = player1.prog_items[1]
- loc0 = player1.locations[0]
- loc1 = player1.locations[1]
- loc2 = player1.locations[2]
-
- multi_world.completion_condition[player1.id] = lambda state: state.has(
- item0.name, player1.id) and state.has(item1.name, player1.id)
- set_rule(loc1, lambda state: state.has(
- item0.name, player1.id))
- # forces a swap
- set_rule(loc2, lambda state: state.has(
- item0.name, player1.id))
- fill_restrictive(multi_world, multi_world.state,
- player1.locations, player1.prog_items)
-
- self.assertEqual(loc0.item, item0)
- self.assertEqual(loc1.item, item1)
- self.assertEqual(1, len(player1.locations))
- self.assertEqual(player1.locations[0], loc2)
-
- def test_minimal_fill(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(multi_world, 1, 2, 2)
-
- items = player1.prog_items
- locations = player1.locations
-
- multi_world.accessibility[player1.id].value = multi_world.accessibility[player1.id].option_minimal
- multi_world.completion_condition[player1.id] = lambda state: state.has(
- items[1].name, player1.id)
- set_rule(locations[1], lambda state: state.has(
- items[0].name, player1.id))
-
- fill_restrictive(multi_world, multi_world.state,
- player1.locations.copy(), player1.prog_items.copy())
-
- self.assertEqual(locations[0].item, items[1])
- # Unnecessary unreachable Item
- self.assertEqual(locations[1].item, items[0])
-
- def test_minimal_mixed_fill(self):
- """
- Test that fill for 1 minimal and 1 non-minimal player will correctly place items in a way that lets
- the non-minimal player get all items.
- """
-
- multi_world = generate_multi_world(2)
- player1 = generate_player_data(multi_world, 1, 3, 3)
- player2 = generate_player_data(multi_world, 2, 3, 3)
-
- multi_world.accessibility[player1.id].value = multi_world.accessibility[player1.id].option_minimal
- multi_world.accessibility[player2.id].value = multi_world.accessibility[player2.id].option_locations
-
- multi_world.completion_condition[player1.id] = lambda state: True
- multi_world.completion_condition[player2.id] = lambda state: state.has(player2.prog_items[2].name, player2.id)
-
- set_rule(player1.locations[1], lambda state: state.has(player1.prog_items[0].name, player1.id))
- set_rule(player1.locations[2], lambda state: state.has(player1.prog_items[1].name, player1.id))
- set_rule(player2.locations[1], lambda state: state.has(player2.prog_items[0].name, player2.id))
- set_rule(player2.locations[2], lambda state: state.has(player2.prog_items[1].name, player2.id))
-
- # force-place an item that makes it impossible to have all locations accessible
- player1.locations[0].place_locked_item(player1.prog_items[2])
-
- # fill remaining locations with remaining items
- location_pool = player1.locations[1:] + player2.locations
- item_pool = player1.prog_items[:-1] + player2.prog_items
- fill_restrictive(multi_world, multi_world.state, location_pool, item_pool)
- multi_world.state.sweep_for_events() # collect everything
-
- # all of player2's locations and items should be accessible (not all of player1's)
- for item in player2.prog_items:
- self.assertTrue(multi_world.state.has(item.name, player2.id),
- f'{item} is unreachable in {item.location}')
-
- def test_reversed_fill(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(multi_world, 1, 2, 2)
-
- item0 = player1.prog_items[0]
- item1 = player1.prog_items[1]
- loc0 = player1.locations[0]
- loc1 = player1.locations[1]
-
- multi_world.completion_condition[player1.id] = lambda state: state.has(
- item0.name, player1.id) and state.has(item1.name, player1.id)
- set_rule(loc1, lambda state: state.has(item1.name, player1.id))
- fill_restrictive(multi_world, multi_world.state,
- player1.locations, player1.prog_items)
-
- self.assertEqual(loc0.item, item1)
- self.assertEqual(loc1.item, item0)
-
- def test_multi_step_fill(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(multi_world, 1, 4, 4)
-
- items = player1.prog_items
- locations = player1.locations
-
- multi_world.completion_condition[player1.id] = lambda state: state.has(
- items[2].name, player1.id) and state.has(items[3].name, player1.id)
- set_rule(locations[1], lambda state: state.has(
- items[0].name, player1.id))
- set_rule(locations[2], lambda state: state.has(
- items[1].name, player1.id))
- set_rule(locations[3], lambda state: state.has(
- items[1].name, player1.id))
-
- fill_restrictive(multi_world, multi_world.state,
- player1.locations.copy(), player1.prog_items.copy())
-
- self.assertEqual(locations[0].item, items[1])
- self.assertEqual(locations[1].item, items[2])
- self.assertEqual(locations[2].item, items[0])
- self.assertEqual(locations[3].item, items[3])
-
- def test_impossible_fill(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(multi_world, 1, 2, 2)
- items = player1.prog_items
- locations = player1.locations
-
- multi_world.completion_condition[player1.id] = lambda state: state.has(
- items[0].name, player1.id) and state.has(items[1].name, player1.id)
- set_rule(locations[1], lambda state: state.has(
- items[1].name, player1.id))
- set_rule(locations[0], lambda state: state.has(
- items[0].name, player1.id))
-
- self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
- player1.locations.copy(), player1.prog_items.copy())
-
- def test_circular_fill(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(multi_world, 1, 3, 3)
-
- item0 = player1.prog_items[0]
- item1 = player1.prog_items[1]
- item2 = player1.prog_items[2]
- loc0 = player1.locations[0]
- loc1 = player1.locations[1]
- loc2 = player1.locations[2]
-
- multi_world.completion_condition[player1.id] = lambda state: state.has(
- item0.name, player1.id) and state.has(item1.name, player1.id) and state.has(item2.name, player1.id)
- set_rule(loc1, lambda state: state.has(item0.name, player1.id))
- set_rule(loc2, lambda state: state.has(item1.name, player1.id))
- set_rule(loc0, lambda state: state.has(item2.name, player1.id))
-
- self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
- player1.locations.copy(), player1.prog_items.copy())
-
- def test_competing_fill(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(multi_world, 1, 2, 2)
-
- item0 = player1.prog_items[0]
- item1 = player1.prog_items[1]
- loc1 = player1.locations[1]
-
- multi_world.completion_condition[player1.id] = lambda state: state.has(
- item0.name, player1.id) and state.has(item0.name, player1.id) and state.has(item1.name, player1.id)
- set_rule(loc1, lambda state: state.has(item0.name, player1.id)
- and state.has(item1.name, player1.id))
-
- self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
- player1.locations.copy(), player1.prog_items.copy())
-
- def test_multiplayer_fill(self):
- multi_world = generate_multi_world(2)
- player1 = generate_player_data(multi_world, 1, 2, 2)
- player2 = generate_player_data(multi_world, 2, 2, 2)
-
- multi_world.completion_condition[player1.id] = lambda state: state.has(
- player1.prog_items[0].name, player1.id) and state.has(
- player1.prog_items[1].name, player1.id)
- multi_world.completion_condition[player2.id] = lambda state: state.has(
- player2.prog_items[0].name, player2.id) and state.has(
- player2.prog_items[1].name, player2.id)
-
- fill_restrictive(multi_world, multi_world.state, player1.locations +
- player2.locations, player1.prog_items + player2.prog_items)
-
- self.assertEqual(player1.locations[0].item, player1.prog_items[1])
- self.assertEqual(player1.locations[1].item, player2.prog_items[1])
- self.assertEqual(player2.locations[0].item, player1.prog_items[0])
- self.assertEqual(player2.locations[1].item, player2.prog_items[0])
-
- def test_multiplayer_rules_fill(self):
- multi_world = generate_multi_world(2)
- player1 = generate_player_data(multi_world, 1, 2, 2)
- player2 = generate_player_data(multi_world, 2, 2, 2)
-
- multi_world.completion_condition[player1.id] = lambda state: state.has(
- player1.prog_items[0].name, player1.id) and state.has(
- player1.prog_items[1].name, player1.id)
- multi_world.completion_condition[player2.id] = lambda state: state.has(
- player2.prog_items[0].name, player2.id) and state.has(
- player2.prog_items[1].name, player2.id)
-
- set_rule(player2.locations[1], lambda state: state.has(
- player2.prog_items[0].name, player2.id))
-
- fill_restrictive(multi_world, multi_world.state, player1.locations +
- player2.locations, player1.prog_items + player2.prog_items)
-
- self.assertEqual(player1.locations[0].item, player2.prog_items[0])
- self.assertEqual(player1.locations[1].item, player2.prog_items[1])
- self.assertEqual(player2.locations[0].item, player1.prog_items[0])
- self.assertEqual(player2.locations[1].item, player1.prog_items[1])
-
- def test_restrictive_progress(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(multi_world, 1, prog_item_count=25)
- items = player1.prog_items.copy()
- multi_world.completion_condition[player1.id] = lambda state: state.has_all(
- names(player1.prog_items), player1.id)
-
- player1.generate_region(player1.menu, 5)
- player1.generate_region(player1.menu, 5, lambda state: state.has_all(
- names(items[2:7]), player1.id))
- player1.generate_region(player1.menu, 5, lambda state: state.has_all(
- names(items[7:12]), player1.id))
- player1.generate_region(player1.menu, 5, lambda state: state.has_all(
- names(items[12:17]), player1.id))
- player1.generate_region(player1.menu, 5, lambda state: state.has_all(
- names(items[17:22]), player1.id))
-
- locations = multi_world.get_unfilled_locations()
-
- fill_restrictive(multi_world, multi_world.state,
- locations, player1.prog_items)
-
- def test_swap_to_earlier_location_with_item_rule(self):
- # test for PR#1109
- multi_world = generate_multi_world(1)
- player1 = generate_player_data(multi_world, 1, 4, 4)
- locations = player1.locations[:] # copy required
- items = player1.prog_items[:] # copy required
- # for the test to work, item and location order is relevant: Sphere 1 last, allowed_item not last
- for location in locations[:-1]: # Sphere 2
- # any one provides access to Sphere 2
- set_rule(location, lambda state: any(state.has(item.name, player1.id) for item in items))
- # forbid all but 1 item in Sphere 1
- sphere1_loc = locations[-1]
- allowed_item = items[1]
- add_item_rule(sphere1_loc, lambda item_to_place: item_to_place == allowed_item)
- # test our rules
- self.assertTrue(location.can_fill(None, allowed_item, False), "Test is flawed")
- self.assertTrue(location.can_fill(None, items[2], False), "Test is flawed")
- self.assertTrue(sphere1_loc.can_fill(None, allowed_item, False), "Test is flawed")
- self.assertFalse(sphere1_loc.can_fill(None, items[2], False), "Test is flawed")
- # fill has to place items[1] in locations[0] which will result in a swap because of placement order
- fill_restrictive(multi_world, multi_world.state, player1.locations, player1.prog_items)
- # assert swap happened
- self.assertTrue(sphere1_loc.item, "Did not swap required item into Sphere 1")
- self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1")
-
- def test_double_sweep(self):
- # test for PR1114
- multi_world = generate_multi_world(1)
- player1 = generate_player_data(multi_world, 1, 1, 1)
- location = player1.locations[0]
- location.address = None
- location.event = True
- item = player1.prog_items[0]
- item.code = None
- location.place_locked_item(item)
- multi_world.state.sweep_for_events()
- multi_world.state.sweep_for_events()
- self.assertTrue(multi_world.state.prog_items[item.name, item.player], "Sweep did not collect - Test flawed")
- self.assertEqual(multi_world.state.prog_items[item.name, item.player], 1, "Sweep collected multiple times")
-
- def test_correct_item_instance_removed_from_pool(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(multi_world, 1, 2, 2)
-
- player1.prog_items[0].name = "Different_item_instance_but_same_item_name"
- player1.prog_items[1].name = "Different_item_instance_but_same_item_name"
- loc0 = player1.locations[0]
-
- fill_restrictive(multi_world, multi_world.state,
- [loc0], player1.prog_items)
-
- self.assertEqual(1, len(player1.prog_items))
- self.assertIsNot(loc0.item, player1.prog_items[0], "Filled item was still present in item pool")
-
-
-class TestDistributeItemsRestrictive(unittest.TestCase):
- def test_basic_distribute(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(
- multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
- locations = player1.locations
- prog_items = player1.prog_items
- basic_items = player1.basic_items
-
- distribute_items_restrictive(multi_world)
-
- self.assertEqual(locations[0].item, basic_items[1])
- self.assertFalse(locations[0].event)
- self.assertEqual(locations[1].item, prog_items[0])
- self.assertTrue(locations[1].event)
- self.assertEqual(locations[2].item, prog_items[1])
- self.assertTrue(locations[2].event)
- self.assertEqual(locations[3].item, basic_items[0])
- self.assertFalse(locations[3].event)
-
- def test_excluded_distribute(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(
- multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
- locations = player1.locations
-
- locations[1].progress_type = LocationProgressType.EXCLUDED
- locations[2].progress_type = LocationProgressType.EXCLUDED
-
- distribute_items_restrictive(multi_world)
-
- self.assertFalse(locations[1].item.advancement)
- self.assertFalse(locations[2].item.advancement)
-
- def test_non_excluded_item_distribute(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(
- multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
- locations = player1.locations
- basic_items = player1.basic_items
-
- locations[1].progress_type = LocationProgressType.EXCLUDED
- basic_items[1].classification = ItemClassification.useful
-
- distribute_items_restrictive(multi_world)
-
- self.assertEqual(locations[1].item, basic_items[0])
-
- def test_too_many_excluded_distribute(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(
- multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
- locations = player1.locations
-
- locations[0].progress_type = LocationProgressType.EXCLUDED
- locations[1].progress_type = LocationProgressType.EXCLUDED
- locations[2].progress_type = LocationProgressType.EXCLUDED
-
- self.assertRaises(FillError, distribute_items_restrictive, multi_world)
-
- def test_non_excluded_item_must_distribute(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(
- multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
- locations = player1.locations
- basic_items = player1.basic_items
-
- locations[1].progress_type = LocationProgressType.EXCLUDED
- locations[2].progress_type = LocationProgressType.EXCLUDED
- basic_items[0].classification = ItemClassification.useful
- basic_items[1].classification = ItemClassification.useful
-
- self.assertRaises(FillError, distribute_items_restrictive, multi_world)
-
- def test_priority_distribute(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(
- multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
- locations = player1.locations
-
- locations[0].progress_type = LocationProgressType.PRIORITY
- locations[3].progress_type = LocationProgressType.PRIORITY
-
- distribute_items_restrictive(multi_world)
-
- self.assertTrue(locations[0].item.advancement)
- self.assertTrue(locations[3].item.advancement)
-
- def test_excess_priority_distribute(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(
- multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
- locations = player1.locations
-
- locations[0].progress_type = LocationProgressType.PRIORITY
- locations[1].progress_type = LocationProgressType.PRIORITY
- locations[2].progress_type = LocationProgressType.PRIORITY
-
- distribute_items_restrictive(multi_world)
-
- self.assertFalse(locations[3].item.advancement)
-
- def test_multiple_world_priority_distribute(self):
- multi_world = generate_multi_world(3)
- player1 = generate_player_data(
- multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
- player2 = generate_player_data(
- multi_world, 2, 4, prog_item_count=1, basic_item_count=3)
- player3 = generate_player_data(
- multi_world, 3, 6, prog_item_count=4, basic_item_count=2)
-
- player1.locations[2].progress_type = LocationProgressType.PRIORITY
- player1.locations[3].progress_type = LocationProgressType.PRIORITY
-
- player2.locations[1].progress_type = LocationProgressType.PRIORITY
-
- player3.locations[0].progress_type = LocationProgressType.PRIORITY
- player3.locations[1].progress_type = LocationProgressType.PRIORITY
- player3.locations[2].progress_type = LocationProgressType.PRIORITY
- player3.locations[3].progress_type = LocationProgressType.PRIORITY
-
- distribute_items_restrictive(multi_world)
-
- self.assertTrue(player1.locations[2].item.advancement)
- self.assertTrue(player1.locations[3].item.advancement)
- self.assertTrue(player2.locations[1].item.advancement)
- self.assertTrue(player3.locations[0].item.advancement)
- self.assertTrue(player3.locations[1].item.advancement)
- self.assertTrue(player3.locations[2].item.advancement)
- self.assertTrue(player3.locations[3].item.advancement)
-
- def test_can_remove_locations_in_fill_hook(self):
-
- multi_world = generate_multi_world()
- player1 = generate_player_data(
- multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
-
- removed_item: list[Item] = []
- removed_location: list[Location] = []
-
- def fill_hook(progitempool, usefulitempool, filleritempool, fill_locations):
- removed_item.append(filleritempool.pop(0))
- removed_location.append(fill_locations.pop(0))
-
- multi_world.worlds[player1.id].fill_hook = fill_hook
-
- distribute_items_restrictive(multi_world)
-
- self.assertIsNone(removed_item[0].location)
- self.assertIsNone(removed_location[0].item)
-
- def test_seed_robust_to_item_order(self):
- mw1 = generate_multi_world()
- gen1 = generate_player_data(
- mw1, 1, 4, prog_item_count=2, basic_item_count=2)
- distribute_items_restrictive(mw1)
-
- mw2 = generate_multi_world()
- gen2 = generate_player_data(
- mw2, 1, 4, prog_item_count=2, basic_item_count=2)
- mw2.itempool.append(mw2.itempool.pop(0))
- distribute_items_restrictive(mw2)
-
- self.assertEqual(gen1.locations[0].item, gen2.locations[0].item)
- self.assertEqual(gen1.locations[1].item, gen2.locations[1].item)
- self.assertEqual(gen1.locations[2].item, gen2.locations[2].item)
- self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
-
- def test_seed_robust_to_location_order(self):
- mw1 = generate_multi_world()
- gen1 = generate_player_data(
- mw1, 1, 4, prog_item_count=2, basic_item_count=2)
- distribute_items_restrictive(mw1)
-
- mw2 = generate_multi_world()
- gen2 = generate_player_data(
- mw2, 1, 4, prog_item_count=2, basic_item_count=2)
- reg = mw2.get_region("Menu", gen2.id)
- reg.locations.append(reg.locations.pop(0))
- distribute_items_restrictive(mw2)
-
- self.assertEqual(gen1.locations[0].item, gen2.locations[0].item)
- self.assertEqual(gen1.locations[1].item, gen2.locations[1].item)
- self.assertEqual(gen1.locations[2].item, gen2.locations[2].item)
- self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
-
- def test_can_reserve_advancement_items_for_general_fill(self):
- multi_world = generate_multi_world()
- player1 = generate_player_data(
- multi_world, 1, location_count=5, prog_item_count=5)
- items = player1.prog_items
- multi_world.completion_condition[player1.id] = lambda state: state.has_all(
- names(items), player1.id)
-
- location = player1.locations[0]
- location.progress_type = LocationProgressType.PRIORITY
- location.item_rule = lambda item: item != items[
- 0] and item != items[1] and item != items[2] and item != items[3]
-
- distribute_items_restrictive(multi_world)
-
- self.assertEqual(location.item, items[4])
-
- def test_non_excluded_local_items(self):
- multi_world = generate_multi_world(2)
- player1 = generate_player_data(
- multi_world, 1, location_count=5, basic_item_count=5)
- player2 = generate_player_data(
- multi_world, 2, location_count=5, basic_item_count=5)
-
- for item in multi_world.get_items():
- item.classification = ItemClassification.useful
-
- multi_world.local_items[player1.id].value = set(names(player1.basic_items))
- multi_world.local_items[player2.id].value = set(names(player2.basic_items))
- locality_rules(multi_world)
-
- distribute_items_restrictive(multi_world)
-
- for item in multi_world.get_items():
- self.assertEqual(item.player, item.location.player)
- self.assertFalse(item.location.event, False)
-
- def test_early_items(self) -> None:
- mw = generate_multi_world(2)
- player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5)
- player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5)
- mw.early_items[1][player1.basic_items[0].name] = 1
- mw.early_items[2][player2.basic_items[2].name] = 1
- mw.early_items[2][player2.basic_items[3].name] = 1
-
- early_items = [
- player1.basic_items[0],
- player2.basic_items[2],
- player2.basic_items[3],
- ]
-
- # copied this code from the beginning of `distribute_items_restrictive`
- # before `distribute_early_items` is called
- fill_locations = sorted(mw.get_unfilled_locations())
- mw.random.shuffle(fill_locations)
- itempool = sorted(mw.itempool)
- mw.random.shuffle(itempool)
-
- fill_locations, itempool = distribute_early_items(mw, fill_locations, itempool)
-
- remaining_p1 = [item for item in itempool if item.player == 1]
- remaining_p2 = [item for item in itempool if item.player == 2]
-
- assert len(itempool) == 7, f"number of items remaining after early_items: {len(itempool)}"
- assert len(remaining_p1) == 4, f"number of p1 items after early_items: {len(remaining_p1)}"
- assert len(remaining_p2) == 3, f"number of p2 items after early_items: {len(remaining_p1)}"
- for i in range(5):
- if i != 0:
- assert player1.basic_items[i] in itempool, "non-early item to remain in itempool"
- if i not in {2, 3}:
- assert player2.basic_items[i] in itempool, "non-early item to remain in itempool"
- for item in early_items:
- assert item not in itempool, "early item to be taken out of itempool"
-
- assert len(fill_locations) == len(mw.get_locations()) - len(early_items), \
- f"early location count from {mw.get_locations()} to {len(fill_locations)} " \
- f"after {len(early_items)} early items"
-
- items_in_locations = {loc.item for loc in mw.get_locations() if loc.item}
-
- assert len(items_in_locations) == len(early_items), \
- f"{len(early_items)} early items in {len(items_in_locations)} locations"
-
- for item in early_items:
- assert item in items_in_locations, "early item to be placed in location"
-
-
-class TestBalanceMultiworldProgression(unittest.TestCase):
- def assertRegionContains(self, region: Region, item: Item) -> bool:
- for location in region.locations:
- if location.item and location.item == item:
- return True
-
- self.fail("Expected " + region.name + " to contain " + item.name +
- "\n Contains" + str(list(map(lambda location: location.item, region.locations))))
-
- def setUp(self) -> None:
- multi_world = generate_multi_world(2)
- self.multi_world = multi_world
- player1 = generate_player_data(
- multi_world, 1, prog_item_count=2, basic_item_count=40)
- self.player1 = player1
- player2 = generate_player_data(
- multi_world, 2, prog_item_count=2, basic_item_count=40)
- self.player2 = player2
-
- multi_world.completion_condition[player1.id] = lambda state: state.has(
- player1.prog_items[0].name, player1.id) and state.has(
- player1.prog_items[1].name, player1.id)
- multi_world.completion_condition[player2.id] = lambda state: state.has(
- player2.prog_items[0].name, player2.id) and state.has(
- player2.prog_items[1].name, player2.id)
-
- items = player1.basic_items + player2.basic_items
-
- # Sphere 1
- region = player1.generate_region(player1.menu, 20)
- items = fillRegion(multi_world, region, [
- player1.prog_items[0]] + items)
-
- # Sphere 2
- region = player1.generate_region(
- player1.regions[1], 20, lambda state: state.has(player1.prog_items[0].name, player1.id))
- items = fillRegion(
- multi_world, region, [player1.prog_items[1], player2.prog_items[0]] + items)
-
- # Sphere 3
- region = player2.generate_region(
- player2.menu, 20, lambda state: state.has(player2.prog_items[0].name, player2.id))
- fillRegion(multi_world, region, [player2.prog_items[1]] + items)
-
- def test_balances_progression(self) -> None:
- self.multi_world.progression_balancing[self.player1.id].value = 50
- self.multi_world.progression_balancing[self.player2.id].value = 50
-
- self.assertRegionContains(
- self.player1.regions[2], self.player2.prog_items[0])
-
- balance_multiworld_progression(self.multi_world)
-
- self.assertRegionContains(
- self.player1.regions[1], self.player2.prog_items[0])
-
- def test_balances_progression_light(self) -> None:
- self.multi_world.progression_balancing[self.player1.id].value = 1
- self.multi_world.progression_balancing[self.player2.id].value = 1
-
- self.assertRegionContains(
- self.player1.regions[2], self.player2.prog_items[0])
-
- balance_multiworld_progression(self.multi_world)
-
- # TODO: arrange for a result that's different from the default
- self.assertRegionContains(
- self.player1.regions[1], self.player2.prog_items[0])
-
- def test_balances_progression_heavy(self) -> None:
- self.multi_world.progression_balancing[self.player1.id].value = 99
- self.multi_world.progression_balancing[self.player2.id].value = 99
-
- self.assertRegionContains(
- self.player1.regions[2], self.player2.prog_items[0])
-
- balance_multiworld_progression(self.multi_world)
-
- # TODO: arrange for a result that's different from the default
- self.assertRegionContains(
- self.player1.regions[1], self.player2.prog_items[0])
-
- def test_skips_balancing_progression(self) -> None:
- self.multi_world.progression_balancing[self.player1.id].value = 0
- self.multi_world.progression_balancing[self.player2.id].value = 0
-
- self.assertRegionContains(
- self.player1.regions[2], self.player2.prog_items[0])
-
- balance_multiworld_progression(self.multi_world)
-
- self.assertRegionContains(
- self.player1.regions[2], self.player2.prog_items[0])
-
- def test_ignores_priority_locations(self) -> None:
- self.multi_world.progression_balancing[self.player1.id].value = 50
- self.multi_world.progression_balancing[self.player2.id].value = 50
-
- self.player2.prog_items[0].location.progress_type = LocationProgressType.PRIORITY
-
- balance_multiworld_progression(self.multi_world)
-
- self.assertRegionContains(
- self.player1.regions[2], self.player2.prog_items[0])
diff --git a/test/general/TestHelpers.py b/test/general/TestHelpers.py
deleted file mode 100644
index c0b560c7e4a6..000000000000
--- a/test/general/TestHelpers.py
+++ /dev/null
@@ -1,82 +0,0 @@
-from typing import Dict, Optional, Callable
-
-from BaseClasses import MultiWorld, CollectionState, Region
-import unittest
-
-
-class TestHelpers(unittest.TestCase):
- multiworld: MultiWorld
- player: int = 1
-
- def setUp(self) -> None:
- self.multiworld = MultiWorld(self.player)
- self.multiworld.game[self.player] = "helper_test_game"
- self.multiworld.player_name = {1: "Tester"}
- self.multiworld.set_seed()
- self.multiworld.set_default_common_options()
-
- def testRegionHelpers(self) -> None:
- regions: Dict[str, str] = {
- "TestRegion1": "I'm an apple",
- "TestRegion2": "I'm a banana",
- "TestRegion3": "Empty Region",
- }
-
- locations: Dict[str, Dict[str, Optional[int]]] = {
- "TestRegion1": {
- "loc_1": 123,
- "loc_2": 456,
- "event_loc": None,
- },
- "TestRegion2": {
- "loc_1": 321,
- "loc_2": 654,
- }
- }
-
- reg_exits: Dict[str, Dict[str, Optional[str]]] = {
- "TestRegion1": {"TestRegion2": "connection"},
- "TestRegion2": {"TestRegion1": None},
- }
-
- reg_exit_set: Dict[str, set[str]] = {
- "TestRegion1": {"TestRegion3"}
- }
-
- exit_rules: Dict[str, Callable[[CollectionState], bool]] = {
- "TestRegion1": lambda state: state.has("test_item", self.player)
- }
-
- self.multiworld.regions += [Region(region, self.player, self.multiworld, regions[region]) for region in regions]
-
- with self.subTest("Test Location Creation Helper"):
- for region, loc_pair in locations.items():
- self.multiworld.get_region(region, self.player).add_locations(loc_pair)
-
- created_location_names = [loc.name for loc in self.multiworld.get_locations()]
- for loc_pair in locations.values():
- for loc_name in loc_pair:
- self.assertTrue(loc_name in created_location_names)
-
- with self.subTest("Test Exit Creation Helper"):
- for region, exit_dict in reg_exits.items():
- self.multiworld.get_region(region, self.player).add_exits(exit_dict, exit_rules)
-
- created_exit_names = [exit.name for region in self.multiworld.get_regions() for exit in region.exits]
- for parent, exit_pair in reg_exits.items():
- for exit_reg, exit_name in exit_pair.items():
- if exit_name:
- self.assertTrue(exit_name in created_exit_names)
- else:
- self.assertTrue(f"{parent} -> {exit_reg}" in created_exit_names)
- if exit_reg in exit_rules:
- entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}"
- self.assertEqual(exit_rules[exit_reg],
- self.multiworld.get_entrance(entrance_name, self.player).access_rule)
-
- for region in reg_exit_set:
- current_region = self.multiworld.get_region(region, self.player)
- current_region.add_exits(reg_exit_set[region])
- exit_names = {_exit.name for _exit in current_region.exits}
- for reg_exit in reg_exit_set[region]:
- self.assertTrue(f"{region} -> {reg_exit}" in exit_names, f"{region} -> {reg_exit} not in {exit_names}")
diff --git a/test/general/TestHostYAML.py b/test/general/TestHostYAML.py
deleted file mode 100644
index f5fd406cac84..000000000000
--- a/test/general/TestHostYAML.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import os
-import unittest
-from tempfile import TemporaryFile
-
-from settings import Settings
-import Utils
-
-
-class TestIDs(unittest.TestCase):
- @classmethod
- def setUpClass(cls) -> None:
- with TemporaryFile("w+", encoding="utf-8") as f:
- Settings(None).dump(f)
- f.seek(0, os.SEEK_SET)
- cls.yaml_options = Utils.parse_yaml(f.read())
-
- def test_utils_in_yaml(self) -> None:
- for option_key, option_set in Utils.get_default_options().items():
- with self.subTest(option_key):
- self.assertIn(option_key, self.yaml_options)
- for sub_option_key in option_set:
- self.assertIn(sub_option_key, self.yaml_options[option_key])
-
- def test_yaml_in_utils(self) -> None:
- utils_options = Utils.get_default_options()
- for option_key, option_set in self.yaml_options.items():
- with self.subTest(option_key):
- self.assertIn(option_key, utils_options)
- for sub_option_key in option_set:
- self.assertIn(sub_option_key, utils_options[option_key])
diff --git a/test/general/TestIDs.py b/test/general/TestIDs.py
deleted file mode 100644
index db1c9461b91a..000000000000
--- a/test/general/TestIDs.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import unittest
-from worlds.AutoWorld import AutoWorldRegister
-
-
-class TestIDs(unittest.TestCase):
- def testUniqueItems(self):
- known_item_ids = set()
- for gamename, world_type in AutoWorldRegister.world_types.items():
- current = len(known_item_ids)
- known_item_ids |= set(world_type.item_id_to_name)
- self.assertEqual(len(known_item_ids) - len(world_type.item_id_to_name), current)
-
- def testUniqueLocations(self):
- known_location_ids = set()
- for gamename, world_type in AutoWorldRegister.world_types.items():
- current = len(known_location_ids)
- known_location_ids |= set(world_type.location_id_to_name)
- self.assertEqual(len(known_location_ids) - len(world_type.location_id_to_name), current)
-
- def testRangeItems(self):
- """There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
- for gamename, world_type in AutoWorldRegister.world_types.items():
- with self.subTest(game=gamename):
- for item_id in world_type.item_id_to_name:
- self.assertLess(item_id, 2**53)
-
- def testRangeLocations(self):
- """There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
- for gamename, world_type in AutoWorldRegister.world_types.items():
- with self.subTest(game=gamename):
- for location_id in world_type.location_id_to_name:
- self.assertLess(location_id, 2**53)
-
- def testReservedItems(self):
- """negative item IDs are reserved to the special "Archipelago" world."""
- for gamename, world_type in AutoWorldRegister.world_types.items():
- with self.subTest(game=gamename):
- if gamename == "Archipelago":
- for item_id in world_type.item_id_to_name:
- self.assertLess(item_id, 0)
- else:
- for item_id in world_type.item_id_to_name:
- self.assertGreater(item_id, 0)
-
- def testReservedLocations(self):
- """negative location IDs are reserved to the special "Archipelago" world."""
- for gamename, world_type in AutoWorldRegister.world_types.items():
- with self.subTest(game=gamename):
- if gamename == "Archipelago":
- for location_id in world_type.location_id_to_name:
- self.assertLess(location_id, 0)
- else:
- for location_id in world_type.location_id_to_name:
- self.assertGreater(location_id, 0)
-
- def testDuplicateItemIDs(self):
- for gamename, world_type in AutoWorldRegister.world_types.items():
- with self.subTest(game=gamename):
- self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id))
-
- def testDuplicateLocationIDs(self):
- for gamename, world_type in AutoWorldRegister.world_types.items():
- with self.subTest(game=gamename):
- self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))
diff --git a/test/general/TestImplemented.py b/test/general/TestImplemented.py
deleted file mode 100644
index 22c546eff18b..000000000000
--- a/test/general/TestImplemented.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import unittest
-from worlds.AutoWorld import AutoWorldRegister
-
-from . import setup_solo_multiworld
-
-
-class TestImplemented(unittest.TestCase):
- def testCompletionCondition(self):
- """Ensure a completion condition is set that has requirements."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
- if not world_type.hidden and game_name not in {"Sudoku"}:
- with self.subTest(game_name):
- multiworld = setup_solo_multiworld(world_type)
- self.assertFalse(multiworld.completion_condition[1](multiworld.state))
-
- def testEntranceParents(self):
- """Tests that the parents of created Entrances match the exiting Region."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
- if not world_type.hidden:
- with self.subTest(game_name):
- multiworld = setup_solo_multiworld(world_type)
- for region in multiworld.regions:
- for exit in region.exits:
- self.assertEqual(exit.parent_region, region)
-
- def testStageMethods(self):
- """Tests that worlds don't try to implement certain steps that are only ever called as stage."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
- if not world_type.hidden:
- with self.subTest(game_name):
- for method in ("assert_generate",):
- self.assertFalse(hasattr(world_type, method),
- f"{method} must be implemented as a @classmethod named stage_{method}.")
diff --git a/test/general/TestItems.py b/test/general/TestItems.py
deleted file mode 100644
index 11caa4faec01..000000000000
--- a/test/general/TestItems.py
+++ /dev/null
@@ -1,82 +0,0 @@
-import unittest
-from worlds.AutoWorld import AutoWorldRegister
-from . import setup_solo_multiworld
-
-
-class TestBase(unittest.TestCase):
- def testItem(self):
- for game_name, world_type in AutoWorldRegister.world_types.items():
- multiworld = setup_solo_multiworld(world_type, steps=("generate_early", "create_regions", "create_items"))
- proxy_world = multiworld.worlds[1]
- empty_prog_items = multiworld.state.prog_items.copy()
- for item_name in world_type.item_name_to_id:
- with self.subTest("Create Item", item_name=item_name, game_name=game_name):
- item = proxy_world.create_item(item_name)
-
- with self.subTest("Item Name", item_name=item_name, game_name=game_name):
- self.assertEqual(item.name, item_name)
-
- if item.advancement:
- with self.subTest("Item State Collect", item_name=item_name, game_name=game_name):
- multiworld.state.collect(item, True)
-
- with self.subTest("Item State Remove", item_name=item_name, game_name=game_name):
- multiworld.state.remove(item)
-
- self.assertEqual(multiworld.state.prog_items, empty_prog_items,
- "Item Collect -> Remove should restore empty state.")
- else:
- with self.subTest("Item State Collect No Change", item_name=item_name, game_name=game_name):
- # Non-Advancement should not modify state.
- base_state = multiworld.state.prog_items.copy()
- multiworld.state.collect(item)
- self.assertEqual(base_state, multiworld.state.prog_items)
-
- multiworld.state.prog_items = empty_prog_items
-
- def testItemNameGroupHasValidItem(self):
- """Test that all item name groups contain valid items. """
- # This cannot test for Event names that you may have declared for logic, only sendable Items.
- # In such a case, you can add your entries to this Exclusion dict. Game Name -> Group Names
- exclusion_dict = {
- "A Link to the Past":
- {"Pendants", "Crystals"},
- "Ocarina of Time":
- {"medallions", "stones", "rewards", "logic_bottles"},
- "Starcraft 2 Wings of Liberty":
- {"Missions"},
- }
- for game_name, world_type in AutoWorldRegister.world_types.items():
- with self.subTest(game_name, game_name=game_name):
- exclusions = exclusion_dict.get(game_name, frozenset())
- for group_name, items in world_type.item_name_groups.items():
- if group_name not in exclusions:
- with self.subTest(group_name, group_name=group_name):
- for item in items:
- self.assertIn(item, world_type.item_name_to_id)
-
- def testItemNameGroupConflict(self):
- """Test that all item name groups aren't also item names."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
- with self.subTest(game_name, game_name=game_name):
- for group_name in world_type.item_name_groups:
- with self.subTest(group_name, group_name=group_name):
- self.assertNotIn(group_name, world_type.item_name_to_id)
-
- def testItemCountGreaterEqualLocations(self):
- for game_name, world_type in AutoWorldRegister.world_types.items():
- with self.subTest("Game", game=game_name):
- multiworld = setup_solo_multiworld(world_type)
- self.assertGreaterEqual(
- len(multiworld.itempool),
- len(multiworld.get_unfilled_locations()),
- f"{game_name} Item count MUST meet or exceed the number of locations",
- )
-
- def testItemsInDatapackage(self):
- """Test that any created items in the itempool are in the datapackage"""
- for game_name, world_type in AutoWorldRegister.world_types.items():
- with self.subTest("Game", game=game_name):
- multiworld = setup_solo_multiworld(world_type)
- for item in multiworld.itempool:
- self.assertIn(item.name, world_type.item_name_to_id)
diff --git a/test/general/TestLocations.py b/test/general/TestLocations.py
deleted file mode 100644
index e77e7a6332bb..000000000000
--- a/test/general/TestLocations.py
+++ /dev/null
@@ -1,71 +0,0 @@
-import unittest
-from collections import Counter
-from worlds.AutoWorld import AutoWorldRegister, call_all
-from . import setup_solo_multiworld
-
-
-class TestBase(unittest.TestCase):
- def testCreateDuplicateLocations(self):
- """Tests that no two Locations share a name or ID."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
- multiworld = setup_solo_multiworld(world_type)
- locations = Counter(location.name for location in multiworld.get_locations())
- if locations:
- self.assertLessEqual(locations.most_common(1)[0][1], 1,
- f"{world_type.game} has duplicate of location name {locations.most_common(1)}")
-
- locations = Counter(location.address for location in multiworld.get_locations()
- if type(location.address) is int)
- if locations:
- self.assertLessEqual(locations.most_common(1)[0][1], 1,
- f"{world_type.game} has duplicate of location ID {locations.most_common(1)}")
-
- def testLocationsInDatapackage(self):
- """Tests that created locations not filled before fill starts exist in the datapackage."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
- with self.subTest("Game", game_name=game_name):
- multiworld = setup_solo_multiworld(world_type)
- locations = multiworld.get_unfilled_locations() # do unfilled locations to avoid Events
- for location in locations:
- self.assertIn(location.name, world_type.location_name_to_id)
- self.assertEqual(location.address, world_type.location_name_to_id[location.name])
-
- def testLocationCreationSteps(self):
- """Tests that Regions and Locations aren't created after `create_items`."""
- gen_steps = ("generate_early", "create_regions", "create_items")
- for game_name, world_type in AutoWorldRegister.world_types.items():
- with self.subTest("Game", game_name=game_name):
- multiworld = setup_solo_multiworld(world_type, gen_steps)
- multiworld._recache()
- region_count = len(multiworld.get_regions())
- location_count = len(multiworld.get_locations())
-
- call_all(multiworld, "set_rules")
- self.assertEqual(region_count, len(multiworld.get_regions()),
- f"{game_name} modified region count during rule creation")
- self.assertEqual(location_count, len(multiworld.get_locations()),
- f"{game_name} modified locations count during rule creation")
-
- multiworld._recache()
- call_all(multiworld, "generate_basic")
- self.assertEqual(region_count, len(multiworld.get_regions()),
- f"{game_name} modified region count during generate_basic")
- self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
- f"{game_name} modified locations count during generate_basic")
-
- multiworld._recache()
- call_all(multiworld, "pre_fill")
- self.assertEqual(region_count, len(multiworld.get_regions()),
- f"{game_name} modified region count during pre_fill")
- self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
- f"{game_name} modified locations count during pre_fill")
-
- def testLocationGroup(self):
- """Test that all location name groups contain valid locations and don't share names."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
- with self.subTest(game_name, game_name=game_name):
- for group_name, locations in world_type.location_name_groups.items():
- with self.subTest(group_name, group_name=group_name):
- for location in locations:
- self.assertIn(location, world_type.location_name_to_id)
- self.assertNotIn(group_name, world_type.location_name_to_id)
diff --git a/test/general/TestNames.py b/test/general/TestNames.py
deleted file mode 100644
index 6dae53240d10..000000000000
--- a/test/general/TestNames.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import unittest
-from worlds.AutoWorld import AutoWorldRegister
-
-
-class TestNames(unittest.TestCase):
- def testItemNamesFormat(self):
- """Item names must not be all numeric in order to differentiate between ID and name in !hint"""
- for gamename, world_type in AutoWorldRegister.world_types.items():
- with self.subTest(game=gamename):
- for item_name in world_type.item_name_to_id:
- self.assertFalse(item_name.isnumeric(),
- f"Item name \"{item_name}\" is invalid. It must not be numeric.")
-
- def testLocationNameFormat(self):
- """Location names must not be all numeric in order to differentiate between ID and name in !hint_location"""
- for gamename, world_type in AutoWorldRegister.world_types.items():
- with self.subTest(game=gamename):
- for location_name in world_type.location_name_to_id:
- self.assertFalse(location_name.isnumeric(),
- f"Location name \"{location_name}\" is invalid. It must not be numeric.")
diff --git a/test/general/TestOptions.py b/test/general/TestOptions.py
deleted file mode 100644
index b7058183e09c..000000000000
--- a/test/general/TestOptions.py
+++ /dev/null
@@ -1,11 +0,0 @@
-import unittest
-from worlds.AutoWorld import AutoWorldRegister
-
-
-class TestOptions(unittest.TestCase):
- def testOptionsHaveDocString(self):
- for gamename, world_type in AutoWorldRegister.world_types.items():
- if not world_type.hidden:
- for option_key, option in world_type.option_definitions.items():
- with self.subTest(game=gamename, option=option_key):
- self.assertTrue(option.__doc__)
diff --git a/test/general/TestReachability.py b/test/general/TestReachability.py
deleted file mode 100644
index dd786b8352f5..000000000000
--- a/test/general/TestReachability.py
+++ /dev/null
@@ -1,69 +0,0 @@
-import unittest
-
-from BaseClasses import CollectionState
-from worlds.AutoWorld import AutoWorldRegister
-from . import setup_solo_multiworld
-
-
-class TestBase(unittest.TestCase):
- gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
-
- default_settings_unreachable_regions = {
- "A Link to the Past": {
- "Chris Houlihan Room", # glitch room by definition
- "Desert Northern Cliffs", # on top of mountain, only reachable via OWG
- "Dark Death Mountain Bunny Descent Area" # OWG Mountain descent
- },
- "Ocarina of Time": {
- "Prelude of Light Warp", # Prelude is not progression by default
- "Serenade of Water Warp", # Serenade is not progression by default
- "Lost Woods Mushroom Timeout", # trade quest starts after this item
- "ZD Eyeball Frog Timeout", # trade quest starts after this item
- "ZR Top of Waterfall", # dummy region used for entrance shuffle
- },
- # The following SM regions are only used when the corresponding StartLocation option is selected (so not with
- # default settings). Also, those don't have any entrances as they serve as starting Region (that's why they
- # have to be excluded for testAllStateCanReachEverything).
- "Super Metroid": {
- "Ceres",
- "Gauntlet Top",
- "Mama Turtle"
- }
- }
-
- def testDefaultAllStateCanReachEverything(self):
- for game_name, world_type in AutoWorldRegister.world_types.items():
- unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
- with self.subTest("Game", game=game_name):
- world = setup_solo_multiworld(world_type)
- excluded = world.exclude_locations[1].value
- state = world.get_all_state(False)
- for location in world.get_locations():
- if location.name not in excluded:
- with self.subTest("Location should be reached", location=location):
- self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
-
- for region in world.get_regions():
- if region.name in unreachable_regions:
- with self.subTest("Region should be unreachable", region=region):
- self.assertFalse(region.can_reach(state))
- else:
- with self.subTest("Region should be reached", region=region):
- self.assertTrue(region.can_reach(state))
-
- with self.subTest("Completion Condition"):
- self.assertTrue(world.can_beat_game(state))
-
- def testDefaultEmptyStateCanReachSomething(self):
- for game_name, world_type in AutoWorldRegister.world_types.items():
- with self.subTest("Game", game=game_name):
- world = setup_solo_multiworld(world_type)
- state = CollectionState(world)
- all_locations = world.get_locations()
- if all_locations:
- locations = set()
- for location in all_locations:
- if location.can_reach(state):
- locations.add(location)
- self.assertGreater(len(locations), 0,
- msg="Need to be able to reach at least one location to get started.")
diff --git a/test/general/__init__.py b/test/general/__init__.py
index b0fb7ca32e76..8afd84976540 100644
--- a/test/general/__init__.py
+++ b/test/general/__init__.py
@@ -1,22 +1,114 @@
from argparse import Namespace
-from typing import Type, Tuple
+from typing import List, Optional, Tuple, Type, Union
-from BaseClasses import MultiWorld
-from worlds.AutoWorld import call_all, World
+from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region
+from worlds import network_data_package
+from worlds.AutoWorld import World, call_all
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
-def setup_solo_multiworld(world_type: Type[World], steps: Tuple[str, ...] = gen_steps) -> MultiWorld:
- multiworld = MultiWorld(1)
- multiworld.game[1] = world_type.game
- multiworld.player_name = {1: "Tester"}
- multiworld.set_seed()
+def setup_solo_multiworld(
+ world_type: Type[World], steps: Tuple[str, ...] = gen_steps, seed: Optional[int] = None
+) -> MultiWorld:
+ """
+ Creates a multiworld with a single player of `world_type`, sets default options, and calls provided gen steps.
+
+ :param world_type: Type of the world to generate a multiworld for
+ :param steps: The gen steps that should be called on the generated multiworld before returning. Default calls
+ steps through pre_fill
+ :param seed: The seed to be used when creating this multiworld
+ :return: The generated multiworld
+ """
+ return setup_multiworld(world_type, steps, seed)
+
+
+def setup_multiworld(worlds: Union[List[Type[World]], Type[World]], steps: Tuple[str, ...] = gen_steps,
+ seed: Optional[int] = None) -> MultiWorld:
+ """
+ Creates a multiworld with a player for each provided world type, allowing duplicates, setting default options, and
+ calling the provided gen steps.
+
+ :param worlds: Type/s of worlds to generate a multiworld for
+ :param steps: Gen steps that should be called before returning. Default calls through pre_fill
+ :param seed: The seed to be used when creating this multiworld
+ :return: The generated multiworld
+ """
+ if not isinstance(worlds, list):
+ worlds = [worlds]
+ players = len(worlds)
+ multiworld = MultiWorld(players)
+ multiworld.game = {player: world_type.game for player, world_type in enumerate(worlds, 1)}
+ multiworld.player_name = {player: f"Tester{player}" for player in multiworld.player_ids}
+ multiworld.set_seed(seed)
+ multiworld.state = CollectionState(multiworld)
args = Namespace()
- for name, option in world_type.option_definitions.items():
- setattr(args, name, {1: option.from_any(option.default)})
+ for player, world_type in enumerate(worlds, 1):
+ for key, option in world_type.options_dataclass.type_hints.items():
+ updated_options = getattr(args, key, {})
+ updated_options[player] = option.from_any(option.default)
+ setattr(args, key, updated_options)
multiworld.set_options(args)
- multiworld.set_default_common_options()
for step in steps:
call_all(multiworld, step)
return multiworld
+
+
+class TestWorld(World):
+ game = f"Test Game"
+ item_name_to_id = {}
+ location_name_to_id = {}
+ hidden = True
+
+
+# add our test world to the data package, so we can test it later
+network_data_package["games"][TestWorld.game] = TestWorld.get_data_package_data()
+
+
+def generate_test_multiworld(players: int = 1) -> MultiWorld:
+ """
+ Generates a multiworld using a special Test Case World class, and seed of 0.
+
+ :param players: Number of players to generate the multiworld for
+ :return: The generated test multiworld
+ """
+ multiworld = setup_multiworld([TestWorld] * players, seed=0)
+ multiworld.regions += [Region("Menu", player_id + 1, multiworld) for player_id in range(players)]
+
+ return multiworld
+
+
+def generate_locations(count: int, player_id: int, region: Region, address: Optional[int] = None,
+ tag: str = "") -> List[Location]:
+ """
+ Generates the specified amount of locations for the player and adds them to the specified region.
+
+ :param count: Number of locations to create
+ :param player_id: ID of the player to create the locations for
+ :param address: Address for the specified locations. They will all share the same address if multiple are created
+ :param region: Parent region to add these locations to
+ :param tag: Tag to add to the name of the generated locations
+ :return: List containing the created locations
+ """
+ prefix = f"player{player_id}{tag}_location"
+
+ locations = [Location(player_id, f"{prefix}{i}", address, region) for i in range(count)]
+ region.locations += locations
+ return locations
+
+
+def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]:
+ """
+ Generates the specified amount of items for the target player.
+
+ :param count: The amount of items to create
+ :param player_id: ID of the player to create the items for
+ :param advancement: Whether the created items should be advancement
+ :param code: The code the items should be created with
+ :return: List containing the created items
+ """
+ item_type = "prog" if advancement else ""
+ classification = ItemClassification.progression if advancement else ItemClassification.filler
+
+ items = [Item(f"player{player_id}_{item_type}item{i}", classification, code, player_id) for i in range(count)]
+ return items
diff --git a/test/general/test_client_server_interaction.py b/test/general/test_client_server_interaction.py
new file mode 100644
index 000000000000..17de91517409
--- /dev/null
+++ b/test/general/test_client_server_interaction.py
@@ -0,0 +1,23 @@
+import unittest
+
+from Utils import get_intended_text, get_input_text_from_response
+
+
+class TestClient(unittest.TestCase):
+ def test_autofill_hint_from_fuzzy_hint(self) -> None:
+ tests = (
+ ("item", ["item1", "item2"]), # Multiple close matches
+ ("itm", ["item1", "item21"]), # No close match, multiple option
+ ("item", ["item1"]), # No close match, single option
+ ("item", ["\"item\" 'item' (item)"]), # Testing different special characters
+ )
+
+ for input_text, possible_answers in tests:
+ item_name, usable, response = get_intended_text(input_text, possible_answers)
+ self.assertFalse(usable, "This test must be updated, it seems get_fuzzy_results behavior changed")
+
+ hint_command = get_input_text_from_response(response, "hint")
+ self.assertIsNotNone(hint_command,
+ "The response to fuzzy hints is no longer recognized by the hint autofill")
+ self.assertEqual(hint_command, f"!hint {item_name}",
+ "The hint command autofilled by the response is not correct")
diff --git a/test/general/test_fill.py b/test/general/test_fill.py
new file mode 100644
index 000000000000..db24b706918c
--- /dev/null
+++ b/test/general/test_fill.py
@@ -0,0 +1,860 @@
+from typing import List, Iterable
+import unittest
+
+from Options import Accessibility
+from test.general import generate_items, generate_locations, generate_test_multiworld
+from Fill import FillError, balance_multiworld_progression, fill_restrictive, \
+ distribute_early_items, distribute_items_restrictive
+from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, Item, Location, \
+ ItemClassification
+from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule
+
+
+class PlayerDefinition(object):
+ multiworld: MultiWorld
+ id: int
+ menu: Region
+ locations: List[Location]
+ prog_items: List[Item]
+ basic_items: List[Item]
+ regions: List[Region]
+
+ def __init__(self, multiworld: MultiWorld, id: int, menu: Region, locations: List[Location] = [], prog_items: List[Item] = [], basic_items: List[Item] = []):
+ self.multiworld = multiworld
+ self.id = id
+ self.menu = menu
+ self.locations = locations
+ self.prog_items = prog_items
+ self.basic_items = basic_items
+ self.regions = [menu]
+
+ def generate_region(self, parent: Region, size: int, access_rule: CollectionRule = lambda state: True) -> Region:
+ region_tag = f"_region{len(self.regions)}"
+ region_name = f"player{self.id}{region_tag}"
+ region = Region(f"player{self.id}{region_tag}", self.id, self.multiworld)
+ self.locations += generate_locations(size, self.id, region, None, region_tag)
+
+ entrance = Entrance(self.id, f"{region_name}_entrance", parent)
+ parent.exits.append(entrance)
+ entrance.connect(region)
+ entrance.access_rule = access_rule
+
+ self.regions.append(region)
+ self.multiworld.regions.append(region)
+
+ return region
+
+
+def fill_region(multiworld: MultiWorld, region: Region, items: List[Item]) -> List[Item]:
+ items = items.copy()
+ while len(items) > 0:
+ location = region.locations.pop(0)
+ region.locations.append(location)
+ if location.item:
+ return items
+ item = items.pop(0)
+ multiworld.push_item(location, item, False)
+
+ return items
+
+
+def region_contains(region: Region, item: Item) -> bool:
+ for location in region.locations:
+ if location.item == item:
+ return True
+
+ return False
+
+
+def generate_player_data(multiworld: MultiWorld, player_id: int, location_count: int = 0, prog_item_count: int = 0, basic_item_count: int = 0) -> PlayerDefinition:
+ menu = multiworld.get_region("Menu", player_id)
+ locations = generate_locations(location_count, player_id, menu, None)
+ prog_items = generate_items(prog_item_count, player_id, True)
+ multiworld.itempool += prog_items
+ basic_items = generate_items(basic_item_count, player_id, False)
+ multiworld.itempool += basic_items
+
+ return PlayerDefinition(multiworld, player_id, menu, locations, prog_items, basic_items)
+
+
+def names(objs: list) -> Iterable[str]:
+ return map(lambda o: o.name, objs)
+
+
+class TestFillRestrictive(unittest.TestCase):
+ def test_basic_fill(self):
+ """Tests `fill_restrictive` fills and removes the locations and items from their respective lists"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(multiworld, 1, 2, 2)
+
+ item0 = player1.prog_items[0]
+ item1 = player1.prog_items[1]
+ loc0 = player1.locations[0]
+ loc1 = player1.locations[1]
+
+ fill_restrictive(multiworld, multiworld.state,
+ player1.locations, player1.prog_items)
+
+ self.assertEqual(loc0.item, item1)
+ self.assertEqual(loc1.item, item0)
+ self.assertEqual([], player1.locations)
+ self.assertEqual([], player1.prog_items)
+
+ def test_ordered_fill(self):
+ """Tests `fill_restrictive` fulfills set rules"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(multiworld, 1, 2, 2)
+ items = player1.prog_items
+ locations = player1.locations
+
+ multiworld.completion_condition[player1.id] = lambda state: state.has(
+ items[0].name, player1.id) and state.has(items[1].name, player1.id)
+ set_rule(locations[1], lambda state: state.has(
+ items[0].name, player1.id))
+ fill_restrictive(multiworld, multiworld.state,
+ player1.locations.copy(), player1.prog_items.copy())
+
+ self.assertEqual(locations[0].item, items[0])
+ self.assertEqual(locations[1].item, items[1])
+
+ def test_partial_fill(self):
+ """Tests that `fill_restrictive` returns unfilled locations"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(multiworld, 1, 3, 2)
+
+ item0 = player1.prog_items[0]
+ item1 = player1.prog_items[1]
+ loc0 = player1.locations[0]
+ loc1 = player1.locations[1]
+ loc2 = player1.locations[2]
+
+ multiworld.completion_condition[player1.id] = lambda state: state.has(
+ item0.name, player1.id) and state.has(item1.name, player1.id)
+ set_rule(loc1, lambda state: state.has(
+ item0.name, player1.id))
+ # forces a swap
+ set_rule(loc2, lambda state: state.has(
+ item0.name, player1.id))
+ fill_restrictive(multiworld, multiworld.state,
+ player1.locations, player1.prog_items)
+
+ self.assertEqual(loc0.item, item0)
+ self.assertEqual(loc1.item, item1)
+ self.assertEqual(1, len(player1.locations))
+ self.assertEqual(player1.locations[0], loc2)
+
+ def test_minimal_fill(self):
+ """Test that fill for minimal player can have unreachable items"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(multiworld, 1, 2, 2)
+
+ items = player1.prog_items
+ locations = player1.locations
+
+ multiworld.worlds[player1.id].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
+ multiworld.completion_condition[player1.id] = lambda state: state.has(
+ items[1].name, player1.id)
+ set_rule(locations[1], lambda state: state.has(
+ items[0].name, player1.id))
+
+ fill_restrictive(multiworld, multiworld.state,
+ player1.locations.copy(), player1.prog_items.copy())
+
+ self.assertEqual(locations[0].item, items[1])
+ # Unnecessary unreachable Item
+ self.assertEqual(locations[1].item, items[0])
+
+ def test_minimal_mixed_fill(self):
+ """
+ Test that fill for 1 minimal and 1 non-minimal player will correctly place items in a way that lets
+ the non-minimal player get all items.
+ """
+
+ multiworld = generate_test_multiworld(2)
+ player1 = generate_player_data(multiworld, 1, 3, 3)
+ player2 = generate_player_data(multiworld, 2, 3, 3)
+
+ multiworld.worlds[player1.id].options.accessibility.value = Accessibility.option_minimal
+ multiworld.worlds[player2.id].options.accessibility.value = Accessibility.option_full
+
+ multiworld.completion_condition[player1.id] = lambda state: True
+ multiworld.completion_condition[player2.id] = lambda state: state.has(player2.prog_items[2].name, player2.id)
+
+ set_rule(player1.locations[1], lambda state: state.has(player1.prog_items[0].name, player1.id))
+ set_rule(player1.locations[2], lambda state: state.has(player1.prog_items[1].name, player1.id))
+ set_rule(player2.locations[1], lambda state: state.has(player2.prog_items[0].name, player2.id))
+ set_rule(player2.locations[2], lambda state: state.has(player2.prog_items[1].name, player2.id))
+
+ # force-place an item that makes it impossible to have all locations accessible
+ player1.locations[0].place_locked_item(player1.prog_items[2])
+
+ # fill remaining locations with remaining items
+ location_pool = player1.locations[1:] + player2.locations
+ item_pool = player1.prog_items[:-1] + player2.prog_items
+ fill_restrictive(multiworld, multiworld.state, location_pool, item_pool)
+ multiworld.state.sweep_for_events() # collect everything
+
+ # all of player2's locations and items should be accessible (not all of player1's)
+ for item in player2.prog_items:
+ self.assertTrue(multiworld.state.has(item.name, player2.id),
+ f"{item} is unreachable in {item.location}")
+
+ def test_reversed_fill(self):
+ """Test a different set of rules can be satisfied"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(multiworld, 1, 2, 2)
+
+ item0 = player1.prog_items[0]
+ item1 = player1.prog_items[1]
+ loc0 = player1.locations[0]
+ loc1 = player1.locations[1]
+
+ multiworld.completion_condition[player1.id] = lambda state: state.has(
+ item0.name, player1.id) and state.has(item1.name, player1.id)
+ set_rule(loc1, lambda state: state.has(item1.name, player1.id))
+ fill_restrictive(multiworld, multiworld.state,
+ player1.locations, player1.prog_items)
+
+ self.assertEqual(loc0.item, item1)
+ self.assertEqual(loc1.item, item0)
+
+ def test_multi_step_fill(self):
+ """Test that fill is able to satisfy multiple spheres"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(multiworld, 1, 4, 4)
+
+ items = player1.prog_items
+ locations = player1.locations
+
+ multiworld.completion_condition[player1.id] = lambda state: state.has(
+ items[2].name, player1.id) and state.has(items[3].name, player1.id)
+ set_rule(locations[1], lambda state: state.has(
+ items[0].name, player1.id))
+ set_rule(locations[2], lambda state: state.has(
+ items[1].name, player1.id))
+ set_rule(locations[3], lambda state: state.has(
+ items[1].name, player1.id))
+
+ fill_restrictive(multiworld, multiworld.state,
+ player1.locations.copy(), player1.prog_items.copy())
+
+ self.assertEqual(locations[0].item, items[1])
+ self.assertEqual(locations[1].item, items[2])
+ self.assertEqual(locations[2].item, items[0])
+ self.assertEqual(locations[3].item, items[3])
+
+ def test_impossible_fill(self):
+ """Test that fill raises an error when it can't place any items"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(multiworld, 1, 2, 2)
+ items = player1.prog_items
+ locations = player1.locations
+
+ multiworld.completion_condition[player1.id] = lambda state: state.has(
+ items[0].name, player1.id) and state.has(items[1].name, player1.id)
+ set_rule(locations[1], lambda state: state.has(
+ items[1].name, player1.id))
+ set_rule(locations[0], lambda state: state.has(
+ items[0].name, player1.id))
+
+ self.assertRaises(FillError, fill_restrictive, multiworld, multiworld.state,
+ player1.locations.copy(), player1.prog_items.copy())
+
+ def test_circular_fill(self):
+ """Test that fill raises an error when it can't place all items"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(multiworld, 1, 3, 3)
+
+ item0 = player1.prog_items[0]
+ item1 = player1.prog_items[1]
+ item2 = player1.prog_items[2]
+ loc0 = player1.locations[0]
+ loc1 = player1.locations[1]
+ loc2 = player1.locations[2]
+
+ multiworld.completion_condition[player1.id] = lambda state: state.has(
+ item0.name, player1.id) and state.has(item1.name, player1.id) and state.has(item2.name, player1.id)
+ set_rule(loc1, lambda state: state.has(item0.name, player1.id))
+ set_rule(loc2, lambda state: state.has(item1.name, player1.id))
+ set_rule(loc0, lambda state: state.has(item2.name, player1.id))
+
+ self.assertRaises(FillError, fill_restrictive, multiworld, multiworld.state,
+ player1.locations.copy(), player1.prog_items.copy())
+
+ def test_competing_fill(self):
+ """Test that fill raises an error when it can't place items in a way to satisfy the conditions"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(multiworld, 1, 2, 2)
+
+ item0 = player1.prog_items[0]
+ item1 = player1.prog_items[1]
+ loc1 = player1.locations[1]
+
+ multiworld.completion_condition[player1.id] = lambda state: state.has(
+ item0.name, player1.id) and state.has(item0.name, player1.id) and state.has(item1.name, player1.id)
+ set_rule(loc1, lambda state: state.has(item0.name, player1.id)
+ and state.has(item1.name, player1.id))
+
+ self.assertRaises(FillError, fill_restrictive, multiworld, multiworld.state,
+ player1.locations.copy(), player1.prog_items.copy())
+
+ def test_multiplayer_fill(self):
+ """Test that items can be placed across worlds"""
+ multiworld = generate_test_multiworld(2)
+ player1 = generate_player_data(multiworld, 1, 2, 2)
+ player2 = generate_player_data(multiworld, 2, 2, 2)
+
+ multiworld.completion_condition[player1.id] = lambda state: state.has(
+ player1.prog_items[0].name, player1.id) and state.has(
+ player1.prog_items[1].name, player1.id)
+ multiworld.completion_condition[player2.id] = lambda state: state.has(
+ player2.prog_items[0].name, player2.id) and state.has(
+ player2.prog_items[1].name, player2.id)
+
+ fill_restrictive(multiworld, multiworld.state, player1.locations +
+ player2.locations, player1.prog_items + player2.prog_items)
+
+ self.assertEqual(player1.locations[0].item, player1.prog_items[1])
+ self.assertEqual(player1.locations[1].item, player2.prog_items[1])
+ self.assertEqual(player2.locations[0].item, player1.prog_items[0])
+ self.assertEqual(player2.locations[1].item, player2.prog_items[0])
+
+ def test_multiplayer_rules_fill(self):
+ """Test that fill across worlds satisfies the rules"""
+ multiworld = generate_test_multiworld(2)
+ player1 = generate_player_data(multiworld, 1, 2, 2)
+ player2 = generate_player_data(multiworld, 2, 2, 2)
+
+ multiworld.completion_condition[player1.id] = lambda state: state.has(
+ player1.prog_items[0].name, player1.id) and state.has(
+ player1.prog_items[1].name, player1.id)
+ multiworld.completion_condition[player2.id] = lambda state: state.has(
+ player2.prog_items[0].name, player2.id) and state.has(
+ player2.prog_items[1].name, player2.id)
+
+ set_rule(player2.locations[1], lambda state: state.has(
+ player2.prog_items[0].name, player2.id))
+
+ fill_restrictive(multiworld, multiworld.state, player1.locations +
+ player2.locations, player1.prog_items + player2.prog_items)
+
+ self.assertEqual(player1.locations[0].item, player2.prog_items[0])
+ self.assertEqual(player1.locations[1].item, player2.prog_items[1])
+ self.assertEqual(player2.locations[0].item, player1.prog_items[0])
+ self.assertEqual(player2.locations[1].item, player1.prog_items[1])
+
+ def test_restrictive_progress(self):
+ """Test that various spheres with different requirements can be filled"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(multiworld, 1, prog_item_count=25)
+ items = player1.prog_items.copy()
+ multiworld.completion_condition[player1.id] = lambda state: state.has_all(
+ names(player1.prog_items), player1.id)
+
+ player1.generate_region(player1.menu, 5)
+ player1.generate_region(player1.menu, 5, lambda state: state.has_all(
+ names(items[2:7]), player1.id))
+ player1.generate_region(player1.menu, 5, lambda state: state.has_all(
+ names(items[7:12]), player1.id))
+ player1.generate_region(player1.menu, 5, lambda state: state.has_all(
+ names(items[12:17]), player1.id))
+ player1.generate_region(player1.menu, 5, lambda state: state.has_all(
+ names(items[17:22]), player1.id))
+
+ locations = multiworld.get_unfilled_locations()
+
+ fill_restrictive(multiworld, multiworld.state,
+ locations, player1.prog_items)
+
+ def test_swap_to_earlier_location_with_item_rule(self):
+ """Test that item swap happens and works as intended"""
+ # test for PR#1109
+ multiworld = generate_test_multiworld(1)
+ player1 = generate_player_data(multiworld, 1, 4, 4)
+ locations = player1.locations[:] # copy required
+ items = player1.prog_items[:] # copy required
+ # for the test to work, item and location order is relevant: Sphere 1 last, allowed_item not last
+ for location in locations[:-1]: # Sphere 2
+ # any one provides access to Sphere 2
+ set_rule(location, lambda state: any(state.has(item.name, player1.id) for item in items))
+ # forbid all but 1 item in Sphere 1
+ sphere1_loc = locations[-1]
+ allowed_item = items[1]
+ add_item_rule(sphere1_loc, lambda item_to_place: item_to_place == allowed_item)
+ # test our rules
+ self.assertTrue(location.can_fill(None, allowed_item, False), "Test is flawed")
+ self.assertTrue(location.can_fill(None, items[2], False), "Test is flawed")
+ self.assertTrue(sphere1_loc.can_fill(None, allowed_item, False), "Test is flawed")
+ self.assertFalse(sphere1_loc.can_fill(None, items[2], False), "Test is flawed")
+ # fill has to place items[1] in locations[0] which will result in a swap because of placement order
+ fill_restrictive(multiworld, multiworld.state, player1.locations, player1.prog_items)
+ # assert swap happened
+ self.assertTrue(sphere1_loc.item, "Did not swap required item into Sphere 1")
+ self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1")
+
+ def test_swap_to_earlier_location_with_item_rule2(self):
+ """Test that swap works before all items are placed"""
+ multiworld = generate_test_multiworld(1)
+ player1 = generate_player_data(multiworld, 1, 5, 5)
+ locations = player1.locations[:] # copy required
+ items = player1.prog_items[:] # copy required
+ # Two items provide access to sphere 2.
+ # One of them is forbidden in sphere 1, the other is first placed in sphere 4 because of placement order,
+ # requiring a swap.
+ # There are spheres in between, so for the swap to work, it'll have to assume all other items are collected.
+ one_to_two1 = items[4].name
+ one_to_two2 = items[3].name
+ three_to_four = items[2].name
+ two_to_three1 = items[1].name
+ two_to_three2 = items[0].name
+ # Sphere 4
+ set_rule(locations[0], lambda state: ((state.has(one_to_two1, player1.id) or state.has(one_to_two2, player1.id))
+ and state.has(two_to_three1, player1.id)
+ and state.has(two_to_three2, player1.id)
+ and state.has(three_to_four, player1.id)))
+ # Sphere 3
+ set_rule(locations[1], lambda state: ((state.has(one_to_two1, player1.id) or state.has(one_to_two2, player1.id))
+ and state.has(two_to_three1, player1.id)
+ and state.has(two_to_three2, player1.id)))
+ # Sphere 2
+ set_rule(locations[2], lambda state: state.has(one_to_two1, player1.id) or state.has(one_to_two2, player1.id))
+ # Sphere 1
+ sphere1_loc1 = locations[3]
+ sphere1_loc2 = locations[4]
+ # forbid one_to_two2 in sphere 1 to make the swap happen as described above
+ add_item_rule(sphere1_loc1, lambda item_to_place: item_to_place.name != one_to_two2)
+ add_item_rule(sphere1_loc2, lambda item_to_place: item_to_place.name != one_to_two2)
+
+ # Now fill should place one_to_two1 in sphere1_loc1 or sphere1_loc2 via swap,
+ # which it will attempt before two_to_three and three_to_four are placed, testing the behavior.
+ fill_restrictive(multiworld, multiworld.state, player1.locations, player1.prog_items)
+ # assert swap happened
+ self.assertTrue(sphere1_loc1.item and sphere1_loc2.item, "Did not swap required item into Sphere 1")
+ self.assertTrue(sphere1_loc1.item.name == one_to_two1 or
+ sphere1_loc2.item.name == one_to_two1, "Wrong item in Sphere 1")
+
+ def test_double_sweep(self):
+ """Test that sweep doesn't duplicate Event items when sweeping"""
+ # test for PR1114
+ multiworld = generate_test_multiworld(1)
+ player1 = generate_player_data(multiworld, 1, 1, 1)
+ location = player1.locations[0]
+ location.address = None
+ item = player1.prog_items[0]
+ item.code = None
+ location.place_locked_item(item)
+ multiworld.state.sweep_for_events()
+ multiworld.state.sweep_for_events()
+ self.assertTrue(multiworld.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed")
+ self.assertEqual(multiworld.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times")
+
+ def test_correct_item_instance_removed_from_pool(self):
+ """Test that a placed item gets removed from the submitted pool"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(multiworld, 1, 2, 2)
+
+ player1.prog_items[0].name = "Different_item_instance_but_same_item_name"
+ player1.prog_items[1].name = "Different_item_instance_but_same_item_name"
+ loc0 = player1.locations[0]
+
+ fill_restrictive(multiworld, multiworld.state,
+ [loc0], player1.prog_items)
+
+ self.assertEqual(1, len(player1.prog_items))
+ self.assertIsNot(loc0.item, player1.prog_items[0], "Filled item was still present in item pool")
+
+
+class TestDistributeItemsRestrictive(unittest.TestCase):
+ def test_basic_distribute(self):
+ """Test that distribute_items_restrictive is deterministic"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(
+ multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
+ locations = player1.locations
+ prog_items = player1.prog_items
+ basic_items = player1.basic_items
+
+ distribute_items_restrictive(multiworld)
+
+ self.assertEqual(locations[0].item, basic_items[1])
+ self.assertFalse(locations[0].advancement)
+ self.assertEqual(locations[1].item, prog_items[0])
+ self.assertTrue(locations[1].advancement)
+ self.assertEqual(locations[2].item, prog_items[1])
+ self.assertTrue(locations[2].advancement)
+ self.assertEqual(locations[3].item, basic_items[0])
+ self.assertFalse(locations[3].advancement)
+
+ def test_excluded_distribute(self):
+ """Test that distribute_items_restrictive doesn't put advancement items on excluded locations"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(
+ multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
+ locations = player1.locations
+
+ locations[1].progress_type = LocationProgressType.EXCLUDED
+ locations[2].progress_type = LocationProgressType.EXCLUDED
+
+ distribute_items_restrictive(multiworld)
+
+ self.assertFalse(locations[1].item.advancement)
+ self.assertFalse(locations[2].item.advancement)
+
+ def test_non_excluded_item_distribute(self):
+ """Test that useful items aren't placed on excluded locations"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(
+ multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
+ locations = player1.locations
+ basic_items = player1.basic_items
+
+ locations[1].progress_type = LocationProgressType.EXCLUDED
+ basic_items[1].classification = ItemClassification.useful
+
+ distribute_items_restrictive(multiworld)
+
+ self.assertEqual(locations[1].item, basic_items[0])
+
+ def test_too_many_excluded_distribute(self):
+ """Test that fill fails if it can't place all progression items due to too many excluded locations"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(
+ multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
+ locations = player1.locations
+
+ locations[0].progress_type = LocationProgressType.EXCLUDED
+ locations[1].progress_type = LocationProgressType.EXCLUDED
+ locations[2].progress_type = LocationProgressType.EXCLUDED
+
+ self.assertRaises(FillError, distribute_items_restrictive, multiworld)
+
+ def test_non_excluded_item_must_distribute(self):
+ """Test that fill fails if it can't place useful items due to too many excluded locations"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(
+ multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
+ locations = player1.locations
+ basic_items = player1.basic_items
+
+ locations[1].progress_type = LocationProgressType.EXCLUDED
+ locations[2].progress_type = LocationProgressType.EXCLUDED
+ basic_items[0].classification = ItemClassification.useful
+ basic_items[1].classification = ItemClassification.useful
+
+ self.assertRaises(FillError, distribute_items_restrictive, multiworld)
+
+ def test_priority_distribute(self):
+ """Test that priority locations receive advancement items"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(
+ multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
+ locations = player1.locations
+
+ locations[0].progress_type = LocationProgressType.PRIORITY
+ locations[3].progress_type = LocationProgressType.PRIORITY
+
+ distribute_items_restrictive(multiworld)
+
+ self.assertTrue(locations[0].item.advancement)
+ self.assertTrue(locations[3].item.advancement)
+
+ def test_excess_priority_distribute(self):
+ """Test that if there's more priority locations than advancement items, they can still fill"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(
+ multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
+ locations = player1.locations
+
+ locations[0].progress_type = LocationProgressType.PRIORITY
+ locations[1].progress_type = LocationProgressType.PRIORITY
+ locations[2].progress_type = LocationProgressType.PRIORITY
+
+ distribute_items_restrictive(multiworld)
+
+ self.assertFalse(locations[3].item.advancement)
+
+ def test_multiple_world_priority_distribute(self):
+ """Test that priority fill can be satisfied for multiple worlds"""
+ multiworld = generate_test_multiworld(3)
+ player1 = generate_player_data(
+ multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
+ player2 = generate_player_data(
+ multiworld, 2, 4, prog_item_count=1, basic_item_count=3)
+ player3 = generate_player_data(
+ multiworld, 3, 6, prog_item_count=4, basic_item_count=2)
+
+ player1.locations[2].progress_type = LocationProgressType.PRIORITY
+ player1.locations[3].progress_type = LocationProgressType.PRIORITY
+
+ player2.locations[1].progress_type = LocationProgressType.PRIORITY
+
+ player3.locations[0].progress_type = LocationProgressType.PRIORITY
+ player3.locations[1].progress_type = LocationProgressType.PRIORITY
+ player3.locations[2].progress_type = LocationProgressType.PRIORITY
+ player3.locations[3].progress_type = LocationProgressType.PRIORITY
+
+ distribute_items_restrictive(multiworld)
+
+ self.assertTrue(player1.locations[2].item.advancement)
+ self.assertTrue(player1.locations[3].item.advancement)
+ self.assertTrue(player2.locations[1].item.advancement)
+ self.assertTrue(player3.locations[0].item.advancement)
+ self.assertTrue(player3.locations[1].item.advancement)
+ self.assertTrue(player3.locations[2].item.advancement)
+ self.assertTrue(player3.locations[3].item.advancement)
+
+ def test_can_remove_locations_in_fill_hook(self):
+ """Test that distribute_items_restrictive calls the fill hook and allows for item and location removal"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(
+ multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
+
+ removed_item: list[Item] = []
+ removed_location: list[Location] = []
+
+ def fill_hook(progitempool, usefulitempool, filleritempool, fill_locations):
+ removed_item.append(filleritempool.pop(0))
+ removed_location.append(fill_locations.pop(0))
+
+ multiworld.worlds[player1.id].fill_hook = fill_hook
+
+ distribute_items_restrictive(multiworld)
+
+ self.assertIsNone(removed_item[0].location)
+ self.assertIsNone(removed_location[0].item)
+
+ def test_seed_robust_to_item_order(self):
+ """Test deterministic fill"""
+ mw1 = generate_test_multiworld()
+ gen1 = generate_player_data(
+ mw1, 1, 4, prog_item_count=2, basic_item_count=2)
+ distribute_items_restrictive(mw1)
+
+ mw2 = generate_test_multiworld()
+ gen2 = generate_player_data(
+ mw2, 1, 4, prog_item_count=2, basic_item_count=2)
+ mw2.itempool.append(mw2.itempool.pop(0))
+ distribute_items_restrictive(mw2)
+
+ self.assertEqual(gen1.locations[0].item, gen2.locations[0].item)
+ self.assertEqual(gen1.locations[1].item, gen2.locations[1].item)
+ self.assertEqual(gen1.locations[2].item, gen2.locations[2].item)
+ self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
+
+ def test_seed_robust_to_location_order(self):
+ """Test deterministic fill even if locations in a region are reordered"""
+ mw1 = generate_test_multiworld()
+ gen1 = generate_player_data(
+ mw1, 1, 4, prog_item_count=2, basic_item_count=2)
+ distribute_items_restrictive(mw1)
+
+ mw2 = generate_test_multiworld()
+ gen2 = generate_player_data(
+ mw2, 1, 4, prog_item_count=2, basic_item_count=2)
+ reg = mw2.get_region("Menu", gen2.id)
+ reg.locations.append(reg.locations.pop(0))
+ distribute_items_restrictive(mw2)
+
+ self.assertEqual(gen1.locations[0].item, gen2.locations[0].item)
+ self.assertEqual(gen1.locations[1].item, gen2.locations[1].item)
+ self.assertEqual(gen1.locations[2].item, gen2.locations[2].item)
+ self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
+
+ def test_can_reserve_advancement_items_for_general_fill(self):
+ """Test that priority locations fill still satisfies item rules"""
+ multiworld = generate_test_multiworld()
+ player1 = generate_player_data(
+ multiworld, 1, location_count=5, prog_item_count=5)
+ items = player1.prog_items
+ multiworld.completion_condition[player1.id] = lambda state: state.has_all(
+ names(items), player1.id)
+
+ location = player1.locations[0]
+ location.progress_type = LocationProgressType.PRIORITY
+ location.item_rule = lambda item: item not in items[:4]
+
+ distribute_items_restrictive(multiworld)
+
+ self.assertEqual(location.item, items[4])
+
+ def test_non_excluded_local_items(self):
+ """Test that local items get placed locally in a multiworld"""
+ multiworld = generate_test_multiworld(2)
+ player1 = generate_player_data(
+ multiworld, 1, location_count=5, basic_item_count=5)
+ player2 = generate_player_data(
+ multiworld, 2, location_count=5, basic_item_count=5)
+
+ for item in multiworld.get_items():
+ item.classification = ItemClassification.useful
+
+ multiworld.local_items[player1.id].value = set(names(player1.basic_items))
+ multiworld.local_items[player2.id].value = set(names(player2.basic_items))
+ locality_rules(multiworld)
+
+ distribute_items_restrictive(multiworld)
+
+ for item in multiworld.get_items():
+ self.assertEqual(item.player, item.location.player)
+ self.assertFalse(item.location.advancement, False)
+
+ def test_early_items(self) -> None:
+ """Test that the early items API successfully places items early"""
+ mw = generate_test_multiworld(2)
+ player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5)
+ player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5)
+ mw.early_items[1][player1.basic_items[0].name] = 1
+ mw.early_items[2][player2.basic_items[2].name] = 1
+ mw.early_items[2][player2.basic_items[3].name] = 1
+
+ early_items = [
+ player1.basic_items[0],
+ player2.basic_items[2],
+ player2.basic_items[3],
+ ]
+
+ # copied this code from the beginning of `distribute_items_restrictive`
+ # before `distribute_early_items` is called
+ fill_locations = sorted(mw.get_unfilled_locations())
+ mw.random.shuffle(fill_locations)
+ itempool = sorted(mw.itempool)
+ mw.random.shuffle(itempool)
+
+ fill_locations, itempool = distribute_early_items(mw, fill_locations, itempool)
+
+ remaining_p1 = [item for item in itempool if item.player == 1]
+ remaining_p2 = [item for item in itempool if item.player == 2]
+
+ assert len(itempool) == 7, f"number of items remaining after early_items: {len(itempool)}"
+ assert len(remaining_p1) == 4, f"number of p1 items after early_items: {len(remaining_p1)}"
+ assert len(remaining_p2) == 3, f"number of p2 items after early_items: {len(remaining_p1)}"
+ for i in range(5):
+ if i != 0:
+ assert player1.basic_items[i] in itempool, "non-early item to remain in itempool"
+ if i not in {2, 3}:
+ assert player2.basic_items[i] in itempool, "non-early item to remain in itempool"
+ for item in early_items:
+ assert item not in itempool, "early item to be taken out of itempool"
+
+ assert len(fill_locations) == len(mw.get_locations()) - len(early_items), \
+ f"early location count from {mw.get_locations()} to {len(fill_locations)} " \
+ f"after {len(early_items)} early items"
+
+ items_in_locations = {loc.item for loc in mw.get_locations() if loc.item}
+
+ assert len(items_in_locations) == len(early_items), \
+ f"{len(early_items)} early items in {len(items_in_locations)} locations"
+
+ for item in early_items:
+ assert item in items_in_locations, "early item to be placed in location"
+
+
+class TestBalanceMultiworldProgression(unittest.TestCase):
+ def assertRegionContains(self, region: Region, item: Item) -> bool:
+ for location in region.locations:
+ if location.item and location.item == item:
+ return True
+
+ self.fail(f"Expected {region.name} to contain {item.name}.\n"
+ f"Contains{list(map(lambda location: location.item, region.locations))}")
+
+ def setUp(self) -> None:
+ multiworld = generate_test_multiworld(2)
+ self.multiworld = multiworld
+ player1 = generate_player_data(
+ multiworld, 1, prog_item_count=2, basic_item_count=40)
+ self.player1 = player1
+ player2 = generate_player_data(
+ multiworld, 2, prog_item_count=2, basic_item_count=40)
+ self.player2 = player2
+
+ multiworld.completion_condition[player1.id] = lambda state: state.has(
+ player1.prog_items[0].name, player1.id) and state.has(
+ player1.prog_items[1].name, player1.id)
+ multiworld.completion_condition[player2.id] = lambda state: state.has(
+ player2.prog_items[0].name, player2.id) and state.has(
+ player2.prog_items[1].name, player2.id)
+
+ items = player1.basic_items + player2.basic_items
+
+ # Sphere 1
+ region = player1.generate_region(player1.menu, 20)
+ items = fill_region(multiworld, region, [
+ player1.prog_items[0]] + items)
+
+ # Sphere 2
+ region = player1.generate_region(
+ player1.regions[1], 20, lambda state: state.has(player1.prog_items[0].name, player1.id))
+ items = fill_region(
+ multiworld, region, [player1.prog_items[1], player2.prog_items[0]] + items)
+
+ # Sphere 3
+ region = player2.generate_region(
+ player2.menu, 20, lambda state: state.has(player2.prog_items[0].name, player2.id))
+ fill_region(multiworld, region, [player2.prog_items[1]] + items)
+
+ def test_balances_progression(self) -> None:
+ """Tests that progression balancing moves progression items earlier"""
+ self.multiworld.progression_balancing[self.player1.id].value = 50
+ self.multiworld.progression_balancing[self.player2.id].value = 50
+
+ self.assertRegionContains(
+ self.player1.regions[2], self.player2.prog_items[0])
+
+ balance_multiworld_progression(self.multiworld)
+
+ self.assertRegionContains(
+ self.player1.regions[1], self.player2.prog_items[0])
+
+ def test_balances_progression_light(self) -> None:
+ """Test that progression balancing still moves items earlier on minimum value"""
+ self.multiworld.progression_balancing[self.player1.id].value = 1
+ self.multiworld.progression_balancing[self.player2.id].value = 1
+
+ self.assertRegionContains(
+ self.player1.regions[2], self.player2.prog_items[0])
+
+ balance_multiworld_progression(self.multiworld)
+
+ # TODO: arrange for a result that's different from the default
+ self.assertRegionContains(
+ self.player1.regions[1], self.player2.prog_items[0])
+
+ def test_balances_progression_heavy(self) -> None:
+ """Test that progression balancing moves items earlier on maximum value"""
+ self.multiworld.progression_balancing[self.player1.id].value = 99
+ self.multiworld.progression_balancing[self.player2.id].value = 99
+
+ self.assertRegionContains(
+ self.player1.regions[2], self.player2.prog_items[0])
+
+ balance_multiworld_progression(self.multiworld)
+
+ # TODO: arrange for a result that's different from the default
+ self.assertRegionContains(
+ self.player1.regions[1], self.player2.prog_items[0])
+
+ def test_skips_balancing_progression(self) -> None:
+ """Test that progression balancing is skipped when players have it disabled"""
+ self.multiworld.progression_balancing[self.player1.id].value = 0
+ self.multiworld.progression_balancing[self.player2.id].value = 0
+
+ self.assertRegionContains(
+ self.player1.regions[2], self.player2.prog_items[0])
+
+ balance_multiworld_progression(self.multiworld)
+
+ self.assertRegionContains(
+ self.player1.regions[2], self.player2.prog_items[0])
+
+ def test_ignores_priority_locations(self) -> None:
+ """Test that progression items on priority locations don't get moved by balancing"""
+ self.multiworld.progression_balancing[self.player1.id].value = 50
+ self.multiworld.progression_balancing[self.player2.id].value = 50
+
+ self.player2.prog_items[0].location.progress_type = LocationProgressType.PRIORITY
+
+ balance_multiworld_progression(self.multiworld)
+
+ self.assertRegionContains(
+ self.player1.regions[2], self.player2.prog_items[0])
diff --git a/test/general/test_groups.py b/test/general/test_groups.py
new file mode 100644
index 000000000000..486d3311fa6b
--- /dev/null
+++ b/test/general/test_groups.py
@@ -0,0 +1,27 @@
+from unittest import TestCase
+
+from worlds.AutoWorld import AutoWorldRegister
+
+
+class TestNameGroups(TestCase):
+ def test_item_name_groups_not_empty(self) -> None:
+ """
+ Test that there are no empty item name groups, which is likely a bug.
+ """
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ if not world_type.item_id_to_name:
+ continue # ignore worlds without items
+ with self.subTest(game=game_name):
+ for name, group in world_type.item_name_groups.items():
+ self.assertTrue(group, f"Item name group \"{name}\" of \"{game_name}\" is empty")
+
+ def test_location_name_groups_not_empty(self) -> None:
+ """
+ Test that there are no empty location name groups, which is likely a bug.
+ """
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ if not world_type.location_id_to_name:
+ continue # ignore worlds without locations
+ with self.subTest(game=game_name):
+ for name, group in world_type.location_name_groups.items():
+ self.assertTrue(group, f"Location name group \"{name}\" of \"{game_name}\" is empty")
diff --git a/test/general/test_helpers.py b/test/general/test_helpers.py
new file mode 100644
index 000000000000..be8473975638
--- /dev/null
+++ b/test/general/test_helpers.py
@@ -0,0 +1,83 @@
+import unittest
+from typing import Callable, Dict, Optional
+
+from BaseClasses import CollectionState, MultiWorld, Region
+
+
+class TestHelpers(unittest.TestCase):
+ multiworld: MultiWorld
+ player: int = 1
+
+ def setUp(self) -> None:
+ self.multiworld = MultiWorld(self.player)
+ self.multiworld.game[self.player] = "helper_test_game"
+ self.multiworld.player_name = {1: "Tester"}
+ self.multiworld.set_seed()
+
+ def test_region_helpers(self) -> None:
+ """Tests `Region.add_locations()` and `Region.add_exits()` have correct behavior"""
+ regions: Dict[str, str] = {
+ "TestRegion1": "I'm an apple",
+ "TestRegion2": "I'm a banana",
+ "TestRegion3": "Empty Region",
+ }
+
+ locations: Dict[str, Dict[str, Optional[int]]] = {
+ "TestRegion1": {
+ "loc_1": 123,
+ "loc_2": 456,
+ "event_loc": None,
+ },
+ "TestRegion2": {
+ "loc_3": 321,
+ "loc_4": 654,
+ }
+ }
+
+ reg_exits: Dict[str, Dict[str, Optional[str]]] = {
+ "TestRegion1": {"TestRegion2": "connection"},
+ "TestRegion2": {"TestRegion1": None},
+ }
+
+ reg_exit_set: Dict[str, set[str]] = {
+ "TestRegion1": {"TestRegion3"}
+ }
+
+ exit_rules: Dict[str, Callable[[CollectionState], bool]] = {
+ "TestRegion1": lambda state: state.has("test_item", self.player)
+ }
+
+ self.multiworld.regions += [Region(region, self.player, self.multiworld, regions[region]) for region in regions]
+
+ with self.subTest("Test Location Creation Helper"):
+ for region, loc_pair in locations.items():
+ self.multiworld.get_region(region, self.player).add_locations(loc_pair)
+
+ created_location_names = [loc.name for loc in self.multiworld.get_locations()]
+ for loc_pair in locations.values():
+ for loc_name in loc_pair:
+ self.assertTrue(loc_name in created_location_names)
+
+ with self.subTest("Test Exit Creation Helper"):
+ for region, exit_dict in reg_exits.items():
+ self.multiworld.get_region(region, self.player).add_exits(exit_dict, exit_rules)
+
+ created_exit_names = [exit.name for region in self.multiworld.get_regions() for exit in region.exits]
+ for parent, exit_pair in reg_exits.items():
+ for exit_reg, exit_name in exit_pair.items():
+ if exit_name:
+ self.assertTrue(exit_name in created_exit_names)
+ else:
+ self.assertTrue(f"{parent} -> {exit_reg}" in created_exit_names)
+ if exit_reg in exit_rules:
+ entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}"
+ self.assertEqual(exit_rules[exit_reg],
+ self.multiworld.get_entrance(entrance_name, self.player).access_rule)
+
+ for region in reg_exit_set:
+ current_region = self.multiworld.get_region(region, self.player)
+ current_region.add_exits(reg_exit_set[region])
+ exit_names = {_exit.name for _exit in current_region.exits}
+ for reg_exit in reg_exit_set[region]:
+ self.assertTrue(f"{region} -> {reg_exit}" in exit_names,
+ f"{region} -> {reg_exit} not in {exit_names}")
diff --git a/test/general/test_host_yaml.py b/test/general/test_host_yaml.py
new file mode 100644
index 000000000000..3edbd34a51c5
--- /dev/null
+++ b/test/general/test_host_yaml.py
@@ -0,0 +1,107 @@
+import os
+import os.path
+import unittest
+from io import StringIO
+from tempfile import TemporaryDirectory, TemporaryFile
+from typing import Any, Dict, List, cast
+
+import Utils
+from settings import Group, Settings, ServerOptions
+
+
+class TestIDs(unittest.TestCase):
+ yaml_options: Dict[Any, Any]
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ with TemporaryFile("w+", encoding="utf-8") as f:
+ Settings(None).dump(f)
+ f.seek(0, os.SEEK_SET)
+ yaml_options = Utils.parse_yaml(f.read())
+ assert isinstance(yaml_options, dict)
+ cls.yaml_options = yaml_options
+
+ def test_utils_in_yaml(self) -> None:
+ """Tests that the auto generated host.yaml has default settings in it"""
+ for option_key, option_set in Settings(None).items():
+ with self.subTest(option_key):
+ self.assertIn(option_key, self.yaml_options)
+ for sub_option_key in option_set:
+ self.assertIn(sub_option_key, self.yaml_options[option_key])
+
+ def test_yaml_in_utils(self) -> None:
+ """Tests that the auto generated host.yaml shows up in reference calls"""
+ utils_options = Settings(None)
+ for option_key, option_set in self.yaml_options.items():
+ with self.subTest(option_key):
+ self.assertIn(option_key, utils_options)
+ for sub_option_key in option_set:
+ self.assertIn(sub_option_key, utils_options[option_key])
+
+
+class TestSettingsDumper(unittest.TestCase):
+ def test_string_format(self) -> None:
+ """Test that dumping a string will yield the expected output"""
+ # By default, pyyaml has automatic line breaks in strings and quoting is optional.
+ # What we want for consistency instead is single-line strings and always quote them.
+ # Line breaks have to become \n in that quoting style.
+ class AGroup(Group):
+ key: str = " ".join(["x"] * 60) + "\n" # more than 120 chars, contains spaces and a line break
+
+ with StringIO() as writer:
+ AGroup().dump(writer, 0)
+ expected_value = AGroup.key.replace("\n", "\\n")
+ self.assertEqual(writer.getvalue(), f"key: \"{expected_value}\"\n",
+ "dumped string has unexpected formatting")
+
+ def test_indentation(self) -> None:
+ """Test that dumping items will add indentation"""
+ # NOTE: we don't care how many spaces there are, but it has to be a multiple of level
+ class AList(List[Any]):
+ __doc__ = None # make sure we get no doc string
+
+ class AGroup(Group):
+ key: AList = cast(AList, ["a", "b", [1]])
+
+ for level in range(3):
+ with StringIO() as writer:
+ AGroup().dump(writer, level)
+ lines = writer.getvalue().split("\n", 5)
+ key_line = lines[0]
+ key_spaces = len(key_line) - len(key_line.lstrip(" "))
+ value_lines = lines[1:-1]
+ value_spaces = [len(value_line) - len(value_line.lstrip(" ")) for value_line in value_lines]
+ if level == 0:
+ self.assertEqual(key_spaces, 0)
+ else:
+ self.assertGreaterEqual(key_spaces, level)
+ self.assertEqual(key_spaces % level, 0)
+ self.assertGreaterEqual(value_spaces[0], key_spaces) # a
+ self.assertEqual(value_spaces[1], value_spaces[0]) # b
+ self.assertEqual(value_spaces[2], value_spaces[0]) # start of sub-list
+ self.assertGreater(value_spaces[3], value_spaces[0],
+ f"{value_lines[3]} should have more indentation than {value_lines[0]} in {lines}")
+
+
+class TestSettingsSave(unittest.TestCase):
+ def test_save(self) -> None:
+ """Test that saving and updating works"""
+ with TemporaryDirectory() as d:
+ filename = os.path.join(d, "host.yaml")
+ new_release_mode = ServerOptions.ReleaseMode("enabled")
+ # create default host.yaml
+ settings = Settings(None)
+ settings.save(filename)
+ self.assertTrue(os.path.exists(filename),
+ "Default settings could not be saved")
+ self.assertNotEqual(settings.server_options.release_mode, new_release_mode,
+ "Unexpected default release mode")
+ # update host.yaml
+ settings.server_options.release_mode = new_release_mode
+ settings.save(filename)
+ self.assertFalse(os.path.exists(filename + ".tmp"),
+ "Temp file was not removed during save")
+ # read back host.yaml
+ settings = Settings(filename)
+ self.assertEqual(settings.server_options.release_mode, new_release_mode,
+ "Settings were not overwritten")
diff --git a/test/general/test_ids.py b/test/general/test_ids.py
new file mode 100644
index 000000000000..e51a070c1fd7
--- /dev/null
+++ b/test/general/test_ids.py
@@ -0,0 +1,88 @@
+import unittest
+
+from Fill import distribute_items_restrictive
+from worlds import network_data_package
+from worlds.AutoWorld import AutoWorldRegister, call_all
+from . import setup_solo_multiworld
+
+
+class TestIDs(unittest.TestCase):
+ def test_range_items(self):
+ """There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
+ for gamename, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest(game=gamename):
+ for item_id in world_type.item_id_to_name:
+ self.assertLess(item_id, 2**53)
+
+ def test_range_locations(self):
+ """There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
+ for gamename, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest(game=gamename):
+ for location_id in world_type.location_id_to_name:
+ self.assertLess(location_id, 2**53)
+
+ def test_reserved_items(self):
+ """negative item IDs are reserved to the special "Archipelago" world."""
+ for gamename, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest(game=gamename):
+ if gamename == "Archipelago":
+ for item_id in world_type.item_id_to_name:
+ self.assertLess(item_id, 0)
+ else:
+ for item_id in world_type.item_id_to_name:
+ self.assertGreater(item_id, 0)
+
+ def test_reserved_locations(self):
+ """negative location IDs are reserved to the special "Archipelago" world."""
+ for gamename, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest(game=gamename):
+ if gamename == "Archipelago":
+ for location_id in world_type.location_id_to_name:
+ self.assertLess(location_id, 0)
+ else:
+ for location_id in world_type.location_id_to_name:
+ self.assertGreater(location_id, 0)
+
+ def test_duplicate_item_ids(self):
+ """Test that a game doesn't have item id overlap within its own datapackage"""
+ for gamename, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest(game=gamename):
+ self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id))
+
+ def test_duplicate_location_ids(self):
+ """Test that a game doesn't have location id overlap within its own datapackage"""
+ for gamename, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest(game=gamename):
+ self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))
+
+ def test_postgen_datapackage(self):
+ """Generates a solo multiworld and checks that the datapackage is still valid"""
+ for gamename, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest(game=gamename):
+ multiworld = setup_solo_multiworld(world_type)
+ distribute_items_restrictive(multiworld)
+ call_all(multiworld, "post_fill")
+ datapackage = world_type.get_data_package_data()
+ for item_group, item_names in datapackage["item_name_groups"].items():
+ self.assertIsInstance(item_group, str,
+ f"item_name_group names should be strings: {item_group}")
+ for item_name in item_names:
+ self.assertIsInstance(item_name, str,
+ f"{item_name}, in group {item_group} is not a string")
+ for loc_group, loc_names in datapackage["location_name_groups"].items():
+ self.assertIsInstance(loc_group, str,
+ f"location_name_group names should be strings: {loc_group}")
+ for loc_name in loc_names:
+ self.assertIsInstance(loc_name, str,
+ f"{loc_name}, in group {loc_group} is not a string")
+ for item_name, item_id in datapackage["item_name_to_id"].items():
+ self.assertIsInstance(item_name, str,
+ f"{item_name} is not a valid item name for item_name_to_id")
+ self.assertIsInstance(item_id, int,
+ f"{item_id} for {item_name} should be an int")
+ for loc_name, loc_id in datapackage["location_name_to_id"].items():
+ self.assertIsInstance(loc_name, str,
+ f"{loc_name} is not a valid item name for location_name_to_id")
+ self.assertIsInstance(loc_id, int,
+ f"{loc_id} for {loc_name} should be an int")
+ self.assertEqual(datapackage["checksum"], network_data_package["games"][gamename]["checksum"])
diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py
new file mode 100644
index 000000000000..e76d539451ea
--- /dev/null
+++ b/test/general/test_implemented.py
@@ -0,0 +1,54 @@
+import unittest
+
+from Fill import distribute_items_restrictive
+from NetUtils import encode
+from worlds.AutoWorld import AutoWorldRegister, call_all
+from worlds import failed_world_loads
+from . import setup_solo_multiworld
+
+
+class TestImplemented(unittest.TestCase):
+ def test_completion_condition(self):
+ """Ensure a completion condition is set that has requirements."""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ if not world_type.hidden and game_name not in {"Sudoku"}:
+ with self.subTest(game_name):
+ multiworld = setup_solo_multiworld(world_type)
+ self.assertFalse(multiworld.completion_condition[1](multiworld.state))
+
+ def test_entrance_parents(self):
+ """Tests that the parents of created Entrances match the exiting Region."""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ if not world_type.hidden:
+ with self.subTest(game_name):
+ multiworld = setup_solo_multiworld(world_type)
+ for region in multiworld.regions:
+ for exit in region.exits:
+ self.assertEqual(exit.parent_region, region)
+
+ def test_stage_methods(self):
+ """Tests that worlds don't try to implement certain steps that are only ever called as stage."""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ if not world_type.hidden:
+ with self.subTest(game_name):
+ for method in ("assert_generate",):
+ self.assertFalse(hasattr(world_type, method),
+ f"{method} must be implemented as a @classmethod named stage_{method}.")
+
+ def test_slot_data(self):
+ """Tests that if a world creates slot data, it's json serializable."""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ # has an await for generate_output which isn't being called
+ if game_name in {"Ocarina of Time", "Zillion"}:
+ continue
+ multiworld = setup_solo_multiworld(world_type)
+ with self.subTest(game=game_name, seed=multiworld.seed):
+ distribute_items_restrictive(multiworld)
+ call_all(multiworld, "post_fill")
+ for key, data in multiworld.worlds[1].fill_slot_data().items():
+ self.assertIsInstance(key, str, "keys in slot data must be a string")
+ self.assertIsInstance(encode(data), str, f"object {type(data).__name__} not serializable.")
+
+ def test_no_failed_world_loads(self):
+ if failed_world_loads:
+ self.fail(f"The following worlds failed to load: {failed_world_loads}")
diff --git a/test/general/test_items.py b/test/general/test_items.py
new file mode 100644
index 000000000000..0c132b74b2f7
--- /dev/null
+++ b/test/general/test_items.py
@@ -0,0 +1,105 @@
+import unittest
+
+from worlds.AutoWorld import AutoWorldRegister, call_all
+from . import setup_solo_multiworld
+
+
+class TestBase(unittest.TestCase):
+ def test_create_item(self):
+ """Test that a world can successfully create all items in its datapackage"""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ proxy_world = setup_solo_multiworld(world_type, ()).worlds[1]
+ multiworld = setup_solo_multiworld(world_type, steps=("generate_early", "create_regions", "create_items"))
+ proxy_world = multiworld.worlds[1]
+ empty_prog_items = multiworld.state.prog_items.copy()
+ for item_name in world_type.item_name_to_id:
+ with self.subTest("Create Item", item_name=item_name, game_name=game_name):
+ item = proxy_world.create_item(item_name)
+
+ with self.subTest("Item Name", item_name=item_name, game_name=game_name):
+ self.assertEqual(item.name, item_name)
+
+ if item.advancement:
+ with self.subTest("Item State Collect", item_name=item_name, game_name=game_name):
+ multiworld.state.collect(item, True)
+
+ with self.subTest("Item State Remove", item_name=item_name, game_name=game_name):
+ multiworld.state.remove(item)
+
+ self.assertEqual(multiworld.state.prog_items, empty_prog_items,
+ "Item Collect -> Remove should restore empty state.")
+ else:
+ with self.subTest("Item State Collect No Change", item_name=item_name, game_name=game_name):
+ # Non-Advancement should not modify state.
+ base_state = multiworld.state.prog_items.copy()
+ multiworld.state.collect(item)
+ self.assertEqual(base_state, multiworld.state.prog_items)
+
+ multiworld.state.prog_items = empty_prog_items
+
+ def test_item_name_group_has_valid_item(self):
+ """Test that all item name groups contain valid items. """
+ # This cannot test for Event names that you may have declared for logic, only sendable Items.
+ # In such a case, you can add your entries to this Exclusion dict. Game Name -> Group Names
+ exclusion_dict = {
+ "A Link to the Past":
+ {"Pendants", "Crystals"},
+ "Ocarina of Time":
+ {"medallions", "stones", "rewards", "logic_bottles"},
+ "Starcraft 2":
+ {"Missions", "WoL Missions"},
+ "Yu-Gi-Oh! 2006":
+ {"Campaign Boss Beaten"}
+ }
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest(game_name, game_name=game_name):
+ exclusions = exclusion_dict.get(game_name, frozenset())
+ for group_name, items in world_type.item_name_groups.items():
+ if group_name not in exclusions:
+ with self.subTest(group_name, group_name=group_name):
+ for item in items:
+ self.assertIn(item, world_type.item_name_to_id)
+
+ def test_item_name_group_conflict(self):
+ """Test that all item name groups aren't also item names."""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest(game_name, game_name=game_name):
+ for group_name in world_type.item_name_groups:
+ with self.subTest(group_name, group_name=group_name):
+ self.assertNotIn(group_name, world_type.item_name_to_id)
+
+ def test_item_count_equal_locations(self):
+ """Test that by the pre_fill step under default settings, each game submits items == locations"""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest("Game", game=game_name):
+ multiworld = setup_solo_multiworld(world_type)
+ self.assertEqual(
+ len(multiworld.itempool),
+ len(multiworld.get_unfilled_locations()),
+ f"{game_name} Item count MUST match the number of locations",
+ )
+
+ def test_items_in_datapackage(self):
+ """Test that any created items in the itempool are in the datapackage"""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest("Game", game=game_name):
+ multiworld = setup_solo_multiworld(world_type)
+ for item in multiworld.itempool:
+ self.assertIn(item.name, world_type.item_name_to_id)
+
+ def test_itempool_not_modified(self):
+ """Test that worlds don't modify the itempool after `create_items`"""
+ gen_steps = ("generate_early", "create_regions", "create_items")
+ additional_steps = ("set_rules", "generate_basic", "pre_fill")
+ excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3")
+ worlds_to_test = {game: world
+ for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games}
+ for game_name, world_type in worlds_to_test.items():
+ with self.subTest("Game", game=game_name):
+ multiworld = setup_solo_multiworld(world_type, gen_steps)
+ created_items = multiworld.itempool.copy()
+ for step in additional_steps:
+ with self.subTest("step", step=step):
+ call_all(multiworld, step)
+ self.assertEqual(created_items, multiworld.itempool,
+ f"{game_name} modified the itempool during {step}")
diff --git a/test/general/test_locations.py b/test/general/test_locations.py
new file mode 100644
index 000000000000..4b95ebd22c90
--- /dev/null
+++ b/test/general/test_locations.py
@@ -0,0 +1,68 @@
+import unittest
+from collections import Counter
+from worlds.AutoWorld import AutoWorldRegister, call_all
+from . import setup_solo_multiworld
+
+
+class TestBase(unittest.TestCase):
+ def test_create_duplicate_locations(self):
+ """Tests that no two Locations share a name or ID."""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ multiworld = setup_solo_multiworld(world_type)
+ locations = Counter(location.name for location in multiworld.get_locations())
+ if locations:
+ self.assertEqual(locations.most_common(1)[0][1], 1,
+ f"{world_type.game} has duplicate of location name {locations.most_common(1)}")
+
+ locations = Counter(location.address for location in multiworld.get_locations()
+ if type(location.address) is int)
+ if locations:
+ self.assertEqual(locations.most_common(1)[0][1], 1,
+ f"{world_type.game} has duplicate of location ID {locations.most_common(1)}")
+
+ def test_locations_in_datapackage(self):
+ """Tests that created locations not filled before fill starts exist in the datapackage."""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest("Game", game_name=game_name):
+ multiworld = setup_solo_multiworld(world_type)
+ locations = multiworld.get_unfilled_locations() # do unfilled locations to avoid Events
+ for location in locations:
+ self.assertIn(location.name, world_type.location_name_to_id)
+ self.assertEqual(location.address, world_type.location_name_to_id[location.name])
+
+ def test_location_creation_steps(self):
+ """Tests that Regions and Locations aren't created after `create_items`."""
+ gen_steps = ("generate_early", "create_regions", "create_items")
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest("Game", game_name=game_name):
+ multiworld = setup_solo_multiworld(world_type, gen_steps)
+ region_count = len(multiworld.get_regions())
+ location_count = len(multiworld.get_locations())
+
+ call_all(multiworld, "set_rules")
+ self.assertEqual(region_count, len(multiworld.get_regions()),
+ f"{game_name} modified region count during rule creation")
+ self.assertEqual(location_count, len(multiworld.get_locations()),
+ f"{game_name} modified locations count during rule creation")
+
+ call_all(multiworld, "generate_basic")
+ self.assertEqual(region_count, len(multiworld.get_regions()),
+ f"{game_name} modified region count during generate_basic")
+ self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
+ f"{game_name} modified locations count during generate_basic")
+
+ call_all(multiworld, "pre_fill")
+ self.assertEqual(region_count, len(multiworld.get_regions()),
+ f"{game_name} modified region count during pre_fill")
+ self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
+ f"{game_name} modified locations count during pre_fill")
+
+ def test_location_group(self):
+ """Test that all location name groups contain valid locations and don't share names."""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest(game_name, game_name=game_name):
+ for group_name, locations in world_type.location_name_groups.items():
+ with self.subTest(group_name, group_name=group_name):
+ for location in locations:
+ self.assertIn(location, world_type.location_name_to_id)
+ self.assertNotIn(group_name, world_type.location_name_to_id)
diff --git a/test/general/test_memory.py b/test/general/test_memory.py
new file mode 100644
index 000000000000..e352b9e8751a
--- /dev/null
+++ b/test/general/test_memory.py
@@ -0,0 +1,16 @@
+import unittest
+
+from worlds.AutoWorld import AutoWorldRegister
+from . import setup_solo_multiworld
+
+
+class TestWorldMemory(unittest.TestCase):
+ def test_leak(self):
+ """Tests that worlds don't leak references to MultiWorld or themselves with default options."""
+ import gc
+ import weakref
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest("Game", game_name=game_name):
+ weak = weakref.ref(setup_solo_multiworld(world_type))
+ gc.collect()
+ self.assertFalse(weak(), "World leaked a reference")
diff --git a/test/general/test_names.py b/test/general/test_names.py
new file mode 100644
index 000000000000..7be76eed4ba9
--- /dev/null
+++ b/test/general/test_names.py
@@ -0,0 +1,20 @@
+import unittest
+from worlds.AutoWorld import AutoWorldRegister
+
+
+class TestNames(unittest.TestCase):
+ def test_item_names_format(self):
+ """Item names must not be all numeric in order to differentiate between ID and name in !hint"""
+ for gamename, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest(game=gamename):
+ for item_name in world_type.item_name_to_id:
+ self.assertFalse(item_name.isnumeric(),
+ f"Item name \"{item_name}\" is invalid. It must not be numeric.")
+
+ def test_location_name_format(self):
+ """Location names must not be all numeric in order to differentiate between ID and name in !hint_location"""
+ for gamename, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest(game=gamename):
+ for location_name in world_type.location_name_to_id:
+ self.assertFalse(location_name.isnumeric(),
+ f"Location name \"{location_name}\" is invalid. It must not be numeric.")
diff --git a/test/general/test_options.py b/test/general/test_options.py
new file mode 100644
index 000000000000..2229b7ea7e66
--- /dev/null
+++ b/test/general/test_options.py
@@ -0,0 +1,61 @@
+import unittest
+
+from BaseClasses import MultiWorld, PlandoOptions
+from Options import ItemLinks
+from worlds.AutoWorld import AutoWorldRegister
+
+
+class TestOptions(unittest.TestCase):
+ def test_options_have_doc_string(self):
+ """Test that submitted options have their own specified docstring"""
+ for gamename, world_type in AutoWorldRegister.world_types.items():
+ if not world_type.hidden:
+ for option_key, option in world_type.options_dataclass.type_hints.items():
+ with self.subTest(game=gamename, option=option_key):
+ self.assertTrue(option.__doc__)
+
+ def test_options_are_not_set_by_world(self):
+ """Test that options attribute is not already set"""
+ for gamename, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest(game=gamename):
+ self.assertFalse(hasattr(world_type, "options"),
+ f"Unexpected assignment to {world_type.__name__}.options!")
+
+ def test_item_links_name_groups(self):
+ """Tests that item links successfully unfold item_name_groups"""
+ item_link_groups = [
+ [{
+ "name": "ItemLinkGroup",
+ "item_pool": ["Everything"],
+ "link_replacement": False,
+ "replacement_item": None,
+ }],
+ [{
+ "name": "ItemLinkGroup",
+ "item_pool": ["Hammer", "Bow"],
+ "link_replacement": False,
+ "replacement_item": None,
+ }]
+ ]
+ # we really need some sort of test world but generic doesn't have enough items for this
+ world = AutoWorldRegister.world_types["A Link to the Past"]
+ plando_options = PlandoOptions.from_option_string("bosses")
+ item_links = [ItemLinks.from_any(item_link_groups[0]), ItemLinks.from_any(item_link_groups[1])]
+ for link in item_links:
+ link.verify(world, "tester", plando_options)
+ self.assertIn("Hammer", link.value[0]["item_pool"])
+ self.assertIn("Bow", link.value[0]["item_pool"])
+
+ # TODO test that the group created using these options has the items
+
+ def test_item_links_resolve(self):
+ """Test item link option resolves correctly."""
+ item_link_group = [{
+ "name": "ItemLinkTest",
+ "item_pool": ["Everything"],
+ "link_replacement": False,
+ "replacement_item": None,
+ }]
+ item_links = {1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)}
+ for link in item_links.values():
+ self.assertEqual(link.value[0], item_link_group[0])
diff --git a/test/general/test_player_options.py b/test/general/test_player_options.py
new file mode 100644
index 000000000000..ea7f19e3d917
--- /dev/null
+++ b/test/general/test_player_options.py
@@ -0,0 +1,39 @@
+import unittest
+import Generate
+
+
+class TestPlayerOptions(unittest.TestCase):
+
+ def test_update_weights(self):
+ original_weights = {
+ "scalar_1": 50,
+ "scalar_2": 25,
+ "list_1": ["string"],
+ "dict_1": {"option_a": 50, "option_b": 50},
+ "dict_2": {"option_f": 50},
+ "set_1": {"option_c"}
+ }
+
+ # test that we don't allow +merge syntax on scalar variables
+ with self.assertRaises(BaseException):
+ Generate.update_weights(original_weights, {"+scalar_1": 0}, "Tested", "")
+
+ new_weights = Generate.update_weights(original_weights, {"scalar_2": 0,
+ "+list_1": ["string_2"],
+ "+dict_1": {"option_b": 0, "option_c": 50},
+ "+set_1": {"option_c", "option_d"},
+ "dict_2": {"option_g": 50},
+ "+list_2": ["string_3"]},
+ "Tested", "")
+
+ self.assertEqual(new_weights["scalar_1"], 50)
+ self.assertEqual(new_weights["scalar_2"], 0)
+ self.assertEqual(new_weights["list_2"], ["string_3"])
+ self.assertEqual(new_weights["list_1"], ["string", "string_2"])
+ self.assertEqual(new_weights["dict_1"]["option_a"], 50)
+ self.assertEqual(new_weights["dict_1"]["option_b"], 50)
+ self.assertEqual(new_weights["dict_1"]["option_c"], 50)
+ self.assertNotIn("option_f", new_weights["dict_2"])
+ self.assertEqual(new_weights["dict_2"]["option_g"], 50)
+ self.assertEqual(len(new_weights["set_1"]), 2)
+ self.assertIn("option_d", new_weights["set_1"])
diff --git a/test/general/test_reachability.py b/test/general/test_reachability.py
new file mode 100644
index 000000000000..4b71762f77fe
--- /dev/null
+++ b/test/general/test_reachability.py
@@ -0,0 +1,71 @@
+import unittest
+
+from BaseClasses import CollectionState
+from worlds.AutoWorld import AutoWorldRegister
+from . import setup_solo_multiworld
+
+
+class TestBase(unittest.TestCase):
+ gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
+
+ default_settings_unreachable_regions = {
+ "A Link to the Past": {
+ "Chris Houlihan Room", # glitch room by definition
+ "Desert Northern Cliffs", # on top of mountain, only reachable via OWG
+ "Dark Death Mountain Bunny Descent Area" # OWG Mountain descent
+ },
+ "Ocarina of Time": {
+ "Prelude of Light Warp", # Prelude is not progression by default
+ "Serenade of Water Warp", # Serenade is not progression by default
+ "Lost Woods Mushroom Timeout", # trade quest starts after this item
+ "ZD Eyeball Frog Timeout", # trade quest starts after this item
+ "ZR Top of Waterfall", # dummy region used for entrance shuffle
+ },
+ # The following SM regions are only used when the corresponding StartLocation option is selected (so not with
+ # default settings). Also, those don't have any entrances as they serve as starting Region (that's why they
+ # have to be excluded for testAllStateCanReachEverything).
+ "Super Metroid": {
+ "Ceres",
+ "Gauntlet Top",
+ "Mama Turtle"
+ }
+ }
+
+ def test_default_all_state_can_reach_everything(self):
+ """Ensure all state can reach everything and complete the game with the defined options"""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
+ with self.subTest("Game", game=game_name):
+ multiworld = setup_solo_multiworld(world_type)
+ excluded = multiworld.worlds[1].options.exclude_locations.value
+ state = multiworld.get_all_state(False)
+ for location in multiworld.get_locations():
+ if location.name not in excluded:
+ with self.subTest("Location should be reached", location=location.name):
+ self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
+
+ for region in multiworld.get_regions():
+ if region.name in unreachable_regions:
+ with self.subTest("Region should be unreachable", region=region.name):
+ self.assertFalse(region.can_reach(state))
+ else:
+ with self.subTest("Region should be reached", region=region.name):
+ self.assertTrue(region.can_reach(state))
+
+ with self.subTest("Completion Condition"):
+ self.assertTrue(multiworld.can_beat_game(state))
+
+ def test_default_empty_state_can_reach_something(self):
+ """Ensure empty state can reach at least one location with the defined options"""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ with self.subTest("Game", game=game_name):
+ multiworld = setup_solo_multiworld(world_type)
+ state = CollectionState(multiworld)
+ all_locations = multiworld.get_locations()
+ if all_locations:
+ locations = set()
+ for location in all_locations:
+ if location.can_reach(state):
+ locations.add(location)
+ self.assertGreater(len(locations), 0,
+ msg="Need to be able to reach at least one location to get started.")
diff --git a/worlds/stardew_valley/test/checks/__init__.py b/test/hosting/__init__.py
similarity index 100%
rename from worlds/stardew_valley/test/checks/__init__.py
rename to test/hosting/__init__.py
diff --git a/test/hosting/__main__.py b/test/hosting/__main__.py
new file mode 100644
index 000000000000..6640c637b5bd
--- /dev/null
+++ b/test/hosting/__main__.py
@@ -0,0 +1,191 @@
+# A bunch of tests to verify MultiServer and custom webhost server work as expected.
+# This spawns processes and may modify your local AP, so this is not run as part of unit testing.
+# Run with `python test/hosting` instead,
+import logging
+import traceback
+from tempfile import TemporaryDirectory
+from time import sleep
+from typing import Any
+
+from test.hosting.client import Client
+from test.hosting.generate import generate_local
+from test.hosting.serve import ServeGame, LocalServeGame, WebHostServeGame
+from test.hosting.webhost import (create_room, get_app, get_multidata_for_room, set_multidata_for_room, start_room,
+ stop_autohost, upload_multidata)
+from test.hosting.world import copy as copy_world, delete as delete_world
+
+failure = False
+fail_fast = True
+
+
+def assert_true(condition: Any, msg: str = "") -> None:
+ global failure
+ if not condition:
+ failure = True
+ msg = f": {msg}" if msg else ""
+ raise AssertionError(f"Assertion failed{msg}")
+
+
+def assert_equal(first: Any, second: Any, msg: str = "") -> None:
+ global failure
+ if first != second:
+ failure = True
+ msg = f": {msg}" if msg else ""
+ raise AssertionError(f"Assertion failed: {first} == {second}{msg}")
+
+
+if fail_fast:
+ expect_true = assert_true
+ expect_equal = assert_equal
+else:
+ def expect_true(condition: Any, msg: str = "") -> None:
+ global failure
+ if not condition:
+ failure = True
+ tb = "".join(traceback.format_stack()[:-1])
+ msg = f": {msg}" if msg else ""
+ logging.error(f"Expectation failed{msg}\n{tb}")
+
+ def expect_equal(first: Any, second: Any, msg: str = "") -> None:
+ global failure
+ if first != second:
+ failure = True
+ tb = "".join(traceback.format_stack()[:-1])
+ msg = f": {msg}" if msg else ""
+ logging.error(f"Expectation failed {first} == {second}{msg}\n{tb}")
+
+
+if __name__ == "__main__":
+ import warnings
+ warnings.simplefilter("ignore", ResourceWarning)
+ warnings.simplefilter("ignore", UserWarning)
+
+ spacer = '=' * 80
+
+ with TemporaryDirectory() as tempdir:
+ multis = [["Clique"], ["Temp World"], ["Clique", "Temp World"]]
+ p1_games = []
+ data_paths = []
+ rooms = []
+
+ copy_world("Clique", "Temp World")
+ try:
+ for n, games in enumerate(multis, 1):
+ print(f"Generating [{n}] {', '.join(games)}")
+ multidata = generate_local(games, tempdir)
+ print(f"Generated [{n}] {', '.join(games)} as {multidata}\n")
+ p1_games.append(games[0])
+ data_paths.append(multidata)
+ finally:
+ delete_world("Temp World")
+
+ webapp = get_app(tempdir)
+ webhost_client = webapp.test_client()
+ for n, multidata in enumerate(data_paths, 1):
+ seed = upload_multidata(webhost_client, multidata)
+ room = create_room(webhost_client, seed)
+ print(f"Uploaded [{n}] {multidata} as {room}\n")
+ rooms.append(room)
+
+ print("Starting autohost")
+ from WebHostLib.autolauncher import autohost
+ try:
+ autohost(webapp.config)
+
+ host: ServeGame
+ for n, (multidata, room, game, multi_games) in enumerate(zip(data_paths, rooms, p1_games, multis), 1):
+ involved_games = {"Archipelago"} | set(multi_games)
+ for collected_items in range(3):
+ print(f"\nTesting [{n}] {game} in {multidata} on MultiServer with {collected_items} items collected")
+ with LocalServeGame(multidata) as host:
+ with Client(host.address, game, "Player1") as client:
+ local_data_packages = client.games_packages
+ local_collected_items = len(client.checked_locations)
+ if collected_items < 2: # Clique only has 2 Locations
+ client.collect_any()
+ # TODO: Ctrl+C test here as well
+
+ for game_name in sorted(involved_games):
+ expect_true(game_name in local_data_packages,
+ f"{game_name} missing from MultiServer datap ackage")
+ expect_true("item_name_groups" not in local_data_packages.get(game_name, {}),
+ f"item_name_groups are not supposed to be in MultiServer data for {game_name}")
+ expect_true("location_name_groups" not in local_data_packages.get(game_name, {}),
+ f"location_name_groups are not supposed to be in MultiServer data for {game_name}")
+ for game_name in local_data_packages:
+ expect_true(game_name in involved_games,
+ f"Received unexpected extra data package for {game_name} from MultiServer")
+ assert_equal(local_collected_items, collected_items,
+ "MultiServer did not load or save correctly")
+
+ print(f"\nTesting [{n}] {game} in {multidata} on customserver with {collected_items} items collected")
+ prev_host_adr: str
+ with WebHostServeGame(webhost_client, room) as host:
+ prev_host_adr = host.address
+ with Client(host.address, game, "Player1") as client:
+ web_data_packages = client.games_packages
+ web_collected_items = len(client.checked_locations)
+ if collected_items < 2: # Clique only has 2 Locations
+ client.collect_any()
+ if collected_items == 1:
+ sleep(1) # wait for the server to collect the item
+ stop_autohost(True) # simulate Ctrl+C
+ sleep(3)
+ autohost(webapp.config) # this will spin the room right up again
+ sleep(1) # make log less annoying
+ # if saving failed, the next iteration will fail below
+
+ # verify server shut down
+ try:
+ with Client(prev_host_adr, game, "Player1") as client:
+ assert_true(False, "Server did not shut down")
+ except ConnectionError:
+ pass
+
+ for game_name in sorted(involved_games):
+ expect_true(game_name in web_data_packages,
+ f"{game_name} missing from customserver data package")
+ expect_true("item_name_groups" not in web_data_packages.get(game_name, {}),
+ f"item_name_groups are not supposed to be in customserver data for {game_name}")
+ expect_true("location_name_groups" not in web_data_packages.get(game_name, {}),
+ f"location_name_groups are not supposed to be in customserver data for {game_name}")
+ for game_name in web_data_packages:
+ expect_true(game_name in involved_games,
+ f"Received unexpected extra data package for {game_name} from customserver")
+ assert_equal(web_collected_items, collected_items,
+ "customserver did not load or save correctly during/after "
+ + ("Ctrl+C" if collected_items == 2 else "/exit"))
+
+ # compare customserver to MultiServer
+ expect_equal(local_data_packages, web_data_packages,
+ "customserver datapackage differs from MultiServer")
+
+ sleep(5.5) # make sure all tasks actually stopped
+
+ # raise an exception in customserver and verify the save doesn't get destroyed
+ # local variables room is the last room's id here
+ old_data = get_multidata_for_room(webhost_client, room)
+ print(f"Destroying multidata for {room}")
+ set_multidata_for_room(webhost_client, room, bytes([0]))
+ try:
+ start_room(webhost_client, room, timeout=7)
+ except TimeoutError:
+ pass
+ else:
+ assert_true(False, "Room started with destroyed multidata")
+ print(f"Restoring multidata for {room}")
+ set_multidata_for_room(webhost_client, room, old_data)
+ with WebHostServeGame(webhost_client, room) as host:
+ with Client(host.address, game, "Player1") as client:
+ assert_equal(len(client.checked_locations), 2,
+ "Save was destroyed during exception in customserver")
+ print("Save file is not busted 🥳")
+
+ finally:
+ print("Stopping autohost")
+ stop_autohost(False)
+
+ if failure:
+ print("Some tests failed")
+ exit(1)
+ exit(0)
diff --git a/test/hosting/client.py b/test/hosting/client.py
new file mode 100644
index 000000000000..b805bb6a2638
--- /dev/null
+++ b/test/hosting/client.py
@@ -0,0 +1,110 @@
+import json
+import sys
+from typing import Any, Collection, Dict, Iterable, Optional
+from websockets import ConnectionClosed
+from websockets.sync.client import connect, ClientConnection
+from threading import Thread
+
+
+__all__ = [
+ "Client"
+]
+
+
+class Client:
+ """Incomplete, minimalistic sync test client for AP network protocol"""
+
+ recv_timeout = 1.0
+
+ host: str
+ game: str
+ slot: str
+ password: Optional[str]
+
+ _ws: Optional[ClientConnection]
+
+ games: Iterable[str]
+ data_package_checksums: Dict[str, Any]
+ games_packages: Dict[str, Any]
+ missing_locations: Collection[int]
+ checked_locations: Collection[int]
+
+ def __init__(self, host: str, game: str, slot: str, password: Optional[str] = None) -> None:
+ self.host = host
+ self.game = game
+ self.slot = slot
+ self.password = password
+ self._ws = None
+ self.games = []
+ self.data_package_checksums = {}
+ self.games_packages = {}
+ self.missing_locations = []
+ self.checked_locations = []
+
+ def __enter__(self) -> "Client":
+ try:
+ self.connect()
+ except BaseException:
+ self.__exit__(*sys.exc_info())
+ raise
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore
+ self.close()
+
+ def _poll(self) -> None:
+ assert self._ws
+ try:
+ while True:
+ self._ws.recv()
+ except (TimeoutError, ConnectionClosed, KeyboardInterrupt, SystemExit):
+ pass
+
+ def connect(self) -> None:
+ self._ws = connect(f"ws://{self.host}")
+ room_info = json.loads(self._ws.recv(self.recv_timeout))[0]
+ self.games = sorted(room_info["games"])
+ self.data_package_checksums = room_info["datapackage_checksums"]
+ self._ws.send(json.dumps([{
+ "cmd": "GetDataPackage",
+ "games": list(self.games),
+ }]))
+ data_package_msg = json.loads(self._ws.recv(self.recv_timeout))[0]
+ self.games_packages = data_package_msg["data"]["games"]
+ self._ws.send(json.dumps([{
+ "cmd": "Connect",
+ "game": self.game,
+ "name": self.slot,
+ "password": self.password,
+ "uuid": "",
+ "version": {
+ "class": "Version",
+ "major": 0,
+ "minor": 4,
+ "build": 6,
+ },
+ "items_handling": 0,
+ "tags": [],
+ "slot_data": False,
+ }]))
+ connect_result_msg = json.loads(self._ws.recv(self.recv_timeout))[0]
+ if connect_result_msg["cmd"] != "Connected":
+ raise ConnectionError(", ".join(connect_result_msg.get("errors", [connect_result_msg["cmd"]])))
+ self.missing_locations = connect_result_msg["missing_locations"]
+ self.checked_locations = connect_result_msg["checked_locations"]
+
+ def close(self) -> None:
+ if self._ws:
+ Thread(target=self._poll).start()
+ self._ws.close()
+
+ def collect(self, locations: Iterable[int]) -> None:
+ if not self._ws:
+ raise ValueError("Not connected")
+ self._ws.send(json.dumps([{
+ "cmd": "LocationChecks",
+ "locations": locations,
+ }]))
+
+ def collect_any(self) -> None:
+ self.collect([next(iter(self.missing_locations))])
diff --git a/test/hosting/generate.py b/test/hosting/generate.py
new file mode 100644
index 000000000000..d5d39dc95ee0
--- /dev/null
+++ b/test/hosting/generate.py
@@ -0,0 +1,76 @@
+import json
+import sys
+import warnings
+from pathlib import Path
+from typing import Iterable, Union, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from multiprocessing.managers import ListProxy # noqa
+
+__all__ = [
+ "generate_local",
+]
+
+
+def _generate_local_inner(games: Iterable[str],
+ dest: Union[Path, str],
+ results: "ListProxy[Union[Path, BaseException]]") -> None:
+ original_argv = sys.argv
+ warnings.simplefilter("ignore")
+ try:
+ from tempfile import TemporaryDirectory
+
+ if not isinstance(dest, Path):
+ dest = Path(dest)
+
+ with TemporaryDirectory() as players_dir:
+ with TemporaryDirectory() as output_dir:
+ import Generate
+ import Main
+
+ for n, game in enumerate(games, 1):
+ player_path = Path(players_dir) / f"{n}.yaml"
+ with open(player_path, "w", encoding="utf-8") as f:
+ f.write(json.dumps({
+ "name": f"Player{n}",
+ "game": game,
+ game: {"hard_mode": "true"},
+ "description": f"generate_local slot {n} ('Player{n}'): {game}",
+ }))
+
+ # this is basically copied from test/programs/test_generate.py
+ # uses a reproducible seed that is different for each set of games
+ sys.argv = [sys.argv[0], "--seed", str(hash(tuple(games))),
+ "--player_files_path", players_dir,
+ "--outputpath", output_dir]
+ Main.main(*Generate.main())
+ output_files = list(Path(output_dir).glob('*.zip'))
+ assert len(output_files) == 1
+ final_file = dest / output_files[0].name
+ output_files[0].rename(final_file)
+ results.append(final_file)
+ except BaseException as e:
+ results.append(e)
+ raise e
+ finally:
+ sys.argv = original_argv
+
+
+def generate_local(games: Iterable[str], dest: Union[Path, str]) -> Path:
+ from multiprocessing import Manager, Process, set_start_method
+
+ try:
+ set_start_method("spawn")
+ except RuntimeError:
+ pass
+
+ manager = Manager()
+ results: "ListProxy[Union[Path, Exception]]" = manager.list()
+
+ p = Process(target=_generate_local_inner, args=(games, dest, results))
+ p.start()
+ p.join()
+ result = results[0]
+ if isinstance(result, BaseException):
+ raise Exception("Could not generate multiworld") from result
+ return result
diff --git a/test/hosting/serve.py b/test/hosting/serve.py
new file mode 100644
index 000000000000..c3eaac87cc08
--- /dev/null
+++ b/test/hosting/serve.py
@@ -0,0 +1,115 @@
+import sys
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from threading import Event
+ from werkzeug.test import Client as FlaskClient
+
+__all__ = [
+ "ServeGame",
+ "LocalServeGame",
+ "WebHostServeGame",
+]
+
+
+class ServeGame:
+ address: str
+
+
+def _launch_multiserver(multidata: Path, ready: "Event", stop: "Event") -> None:
+ import os
+ import warnings
+
+ original_argv = sys.argv
+ original_stdin = sys.stdin
+ warnings.simplefilter("ignore")
+ try:
+ import asyncio
+ from MultiServer import main, parse_args
+
+ sys.argv = [sys.argv[0], str(multidata), "--host", "127.0.0.1"]
+ r, w = os.pipe()
+ sys.stdin = os.fdopen(r, "r")
+
+ async def set_ready() -> None:
+ await asyncio.sleep(.01) # switch back to other task once more
+ ready.set() # server should be up, set ready state
+
+ async def wait_stop() -> None:
+ await asyncio.get_event_loop().run_in_executor(None, stop.wait)
+ os.fdopen(w, "w").write("/exit")
+
+ async def run() -> None:
+ # this will run main() until first await, then switch to set_ready()
+ await asyncio.gather(
+ main(parse_args()),
+ set_ready(),
+ wait_stop(),
+ )
+
+ asyncio.run(run())
+ finally:
+ sys.argv = original_argv
+ sys.stdin = original_stdin
+
+
+class LocalServeGame(ServeGame):
+ from multiprocessing import Process
+
+ _multidata: Path
+ _proc: Process
+ _stop: "Event"
+
+ def __init__(self, multidata: Path) -> None:
+ self.address = ""
+ self._multidata = multidata
+
+ def __enter__(self) -> "LocalServeGame":
+ from multiprocessing import Manager, Process, set_start_method
+
+ try:
+ set_start_method("spawn")
+ except RuntimeError:
+ pass
+
+ manager = Manager()
+ ready: "Event" = manager.Event()
+ self._stop = manager.Event()
+
+ self._proc = Process(target=_launch_multiserver, args=(self._multidata, ready, self._stop))
+ try:
+ self._proc.start()
+ ready.wait(30)
+ self.address = "localhost:38281"
+ return self
+ except BaseException:
+ self.__exit__(*sys.exc_info())
+ raise
+
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore
+ try:
+ self._stop.set()
+ self._proc.join(30)
+ except TimeoutError:
+ self._proc.terminate()
+ self._proc.join()
+
+
+class WebHostServeGame(ServeGame):
+ _client: "FlaskClient"
+ _room: str
+
+ def __init__(self, app_client: "FlaskClient", room: str) -> None:
+ self.address = ""
+ self._client = app_client
+ self._room = room
+
+ def __enter__(self) -> "WebHostServeGame":
+ from .webhost import start_room
+ self.address = start_room(self._client, self._room)
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore
+ from .webhost import stop_room
+ stop_room(self._client, self._room, timeout=30)
diff --git a/test/hosting/webhost.py b/test/hosting/webhost.py
new file mode 100644
index 000000000000..4db605e8c1ea
--- /dev/null
+++ b/test/hosting/webhost.py
@@ -0,0 +1,208 @@
+import re
+from pathlib import Path
+from typing import TYPE_CHECKING, Optional, cast
+
+if TYPE_CHECKING:
+ from flask import Flask
+ from werkzeug.test import Client as FlaskClient
+
+__all__ = [
+ "get_app",
+ "upload_multidata",
+ "create_room",
+ "start_room",
+ "stop_room",
+ "set_room_timeout",
+ "get_multidata_for_room",
+ "set_multidata_for_room",
+ "stop_autohost",
+]
+
+
+def get_app(tempdir: str) -> "Flask":
+ from WebHostLib import app as raw_app
+ from WebHost import get_app
+ raw_app.config["PONY"] = {
+ "provider": "sqlite",
+ "filename": str(Path(tempdir) / "host.db"),
+ "create_db": True,
+ }
+ raw_app.config.update({
+ "TESTING": True,
+ "HOST_ADDRESS": "localhost",
+ "HOSTERS": 1,
+ })
+ return get_app()
+
+
+def upload_multidata(app_client: "FlaskClient", multidata: Path) -> str:
+ response = app_client.post("/uploads", data={
+ "file": multidata.open("rb"),
+ })
+ assert response.status_code < 400, f"Upload of {multidata} failed: status {response.status_code}"
+ assert "Location" in response.headers, f"Upload of {multidata} failed: no redirect"
+ location = response.headers["Location"]
+ assert isinstance(location, str)
+ assert location.startswith("/seed/"), f"Upload of {multidata} failed: unexpected redirect"
+ return location[6:]
+
+
+def create_room(app_client: "FlaskClient", seed: str, auto_start: bool = False) -> str:
+ response = app_client.get(f"/new_room/{seed}")
+ assert response.status_code < 400, f"Creating room for {seed} failed: status {response.status_code}"
+ assert "Location" in response.headers, f"Creating room for {seed} failed: no redirect"
+ location = response.headers["Location"]
+ assert isinstance(location, str)
+ assert location.startswith("/room/"), f"Creating room for {seed} failed: unexpected redirect"
+ room_id = location[6:]
+
+ if not auto_start:
+ # by default, creating a room will auto-start it, so we update last activity here
+ stop_room(app_client, room_id, simulate_idle=False)
+
+ return room_id
+
+
+def start_room(app_client: "FlaskClient", room_id: str, timeout: float = 30) -> str:
+ from time import sleep
+
+ import pony.orm
+
+ poll_interval = .2
+
+ print(f"Starting room {room_id}")
+ no_timeout = timeout <= 0
+ while no_timeout or timeout > 0:
+ try:
+ response = app_client.get(f"/room/{room_id}")
+ except pony.orm.core.OptimisticCheckError:
+ # hoster wrote to room during our transaction
+ continue
+
+ assert response.status_code == 200, f"Starting room for {room_id} failed: status {response.status_code}"
+ match = re.search(r"/connect ([\w:.\-]+)", response.text)
+ if match:
+ return match[1]
+ timeout -= poll_interval
+ sleep(poll_interval)
+ raise TimeoutError("Room did not start")
+
+
+def stop_room(app_client: "FlaskClient",
+ room_id: str,
+ timeout: Optional[float] = None,
+ simulate_idle: bool = True) -> None:
+ from datetime import datetime, timedelta
+ from time import sleep
+
+ from pony.orm import db_session
+
+ from WebHostLib.models import Command, Room
+ from WebHostLib import app
+
+ poll_interval = 2
+
+ print(f"Stopping room {room_id}")
+ room_uuid = app.url_map.converters["suuid"].to_python(None, room_id) # type: ignore[arg-type]
+
+ if timeout is not None:
+ sleep(.1) # should not be required, but other things might use threading
+
+ with db_session:
+ room: Room = Room.get(id=room_uuid)
+ if simulate_idle:
+ new_last_activity = datetime.utcnow() - timedelta(seconds=room.timeout + 5)
+ else:
+ new_last_activity = datetime.utcnow() - timedelta(days=3)
+ room.last_activity = new_last_activity
+ address = f"localhost:{room.last_port}" if room.last_port > 0 else None
+ if address:
+ original_timeout = room.timeout
+ room.timeout = 1 # avoid spinning it up again
+ Command(room=room, commandtext="/exit")
+
+ try:
+ if address and timeout is not None:
+ print("waiting for shutdown")
+ import socket
+ host_str, port_str = tuple(address.split(":"))
+ address_tuple = host_str, int(port_str)
+
+ no_timeout = timeout <= 0
+ while no_timeout or timeout > 0:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ try:
+ s.connect(address_tuple)
+ s.close()
+ except ConnectionRefusedError:
+ return
+ sleep(poll_interval)
+ timeout -= poll_interval
+
+ raise TimeoutError("Room did not stop")
+ finally:
+ with db_session:
+ room = Room.get(id=room_uuid)
+ room.last_port = 0 # easier to detect when the host is up this way
+ if address:
+ room.timeout = original_timeout
+ room.last_activity = new_last_activity
+ print("timeout restored")
+
+
+def set_room_timeout(room_id: str, timeout: float) -> None:
+ from pony.orm import db_session
+
+ from WebHostLib.models import Room
+ from WebHostLib import app
+
+ room_uuid = app.url_map.converters["suuid"].to_python(None, room_id) # type: ignore[arg-type]
+ with db_session:
+ room: Room = Room.get(id=room_uuid)
+ room.timeout = timeout
+
+
+def get_multidata_for_room(webhost_client: "FlaskClient", room_id: str) -> bytes:
+ from pony.orm import db_session
+
+ from WebHostLib.models import Room
+ from WebHostLib import app
+
+ room_uuid = app.url_map.converters["suuid"].to_python(None, room_id) # type: ignore[arg-type]
+ with db_session:
+ room: Room = Room.get(id=room_uuid)
+ return cast(bytes, room.seed.multidata)
+
+
+def set_multidata_for_room(webhost_client: "FlaskClient", room_id: str, data: bytes) -> None:
+ from pony.orm import db_session
+
+ from WebHostLib.models import Room
+ from WebHostLib import app
+
+ room_uuid = app.url_map.converters["suuid"].to_python(None, room_id) # type: ignore[arg-type]
+ with db_session:
+ room: Room = Room.get(id=room_uuid)
+ room.seed.multidata = data
+
+
+def stop_autohost(graceful: bool = True) -> None:
+ import os
+ import signal
+
+ import multiprocessing
+
+ from WebHostLib.autolauncher import stop
+
+ stop()
+ proc: multiprocessing.process.BaseProcess
+ for proc in filter(lambda child: child.name.startswith("MultiHoster"), multiprocessing.active_children()):
+ if graceful and proc.pid:
+ os.kill(proc.pid, getattr(signal, "CTRL_C_EVENT", signal.SIGINT))
+ else:
+ proc.kill()
+ try:
+ proc.join(30)
+ except TimeoutError:
+ proc.kill()
+ proc.join()
diff --git a/test/hosting/world.py b/test/hosting/world.py
new file mode 100644
index 000000000000..e083e027fee1
--- /dev/null
+++ b/test/hosting/world.py
@@ -0,0 +1,42 @@
+import re
+import shutil
+from pathlib import Path
+from typing import Dict
+
+
+__all__ = ["copy", "delete"]
+
+
+_new_worlds: Dict[str, str] = {}
+
+
+def copy(src: str, dst: str) -> None:
+ from Utils import get_file_safe_name
+ from worlds import AutoWorldRegister
+
+ assert dst not in _new_worlds, "World already created"
+ if '"' in dst or "\\" in dst: # easier to reject than to escape
+ raise ValueError(f"Unsupported symbols in {dst}")
+ dst_folder_name = get_file_safe_name(dst.lower())
+ src_cls = AutoWorldRegister.world_types[src]
+ src_folder = Path(src_cls.__file__).parent
+ worlds_folder = src_folder.parent
+ if (not src_cls.__file__.endswith("__init__.py") or not src_folder.is_dir()
+ or not (worlds_folder / "generic").is_dir()):
+ raise ValueError(f"Unsupported layout for copy_world from {src}")
+ dst_folder = worlds_folder / dst_folder_name
+ if dst_folder.is_dir():
+ raise ValueError(f"Destination {dst_folder} already exists")
+ shutil.copytree(src_folder, dst_folder)
+ _new_worlds[dst] = str(dst_folder)
+ with open(dst_folder / "__init__.py", "r", encoding="utf-8-sig") as f:
+ contents = f.read()
+ contents = re.sub(r'game\s*=\s*[\'"]' + re.escape(src) + r'[\'"]', f'game = "{dst}"', contents)
+ with open(dst_folder / "__init__.py", "w", encoding="utf-8") as f:
+ f.write(contents)
+
+
+def delete(name: str) -> None:
+ assert name in _new_worlds, "World not created by this script"
+ shutil.rmtree(_new_worlds[name])
+ del _new_worlds[name]
diff --git a/test/multiworld/__init__.py b/test/multiworld/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/test/multiworld/test_multiworlds.py b/test/multiworld/test_multiworlds.py
new file mode 100644
index 000000000000..5289cac6c357
--- /dev/null
+++ b/test/multiworld/test_multiworlds.py
@@ -0,0 +1,77 @@
+import unittest
+from typing import List, Tuple
+from unittest import TestCase
+
+from BaseClasses import CollectionState, Location, MultiWorld
+from Fill import distribute_items_restrictive
+from Options import Accessibility
+from worlds.AutoWorld import AutoWorldRegister, call_all, call_single
+from ..general import gen_steps, setup_multiworld
+
+
+class MultiworldTestBase(TestCase):
+ multiworld: MultiWorld
+
+ # similar to the implementation in WorldTestBase.test_fill
+ # but for multiple players and doesn't allow minimal accessibility
+ def fulfills_accessibility(self) -> bool:
+ """
+ Checks that the multiworld satisfies locations accessibility requirements, failing if all locations are cleared
+ but not beatable, or some locations are unreachable.
+ """
+ locations = [loc for loc in self.multiworld.get_locations()]
+ state = CollectionState(self.multiworld)
+ while locations:
+ sphere: List[Location] = []
+ for n in range(len(locations) - 1, -1, -1):
+ if locations[n].can_reach(state):
+ sphere.append(locations.pop(n))
+ self.assertTrue(sphere, f"Unreachable locations: {locations}")
+ if not sphere:
+ return False
+ for location in sphere:
+ if location.item:
+ state.collect(location.item, True, location)
+ return self.multiworld.has_beaten_game(state, 1)
+
+ def assertSteps(self, steps: Tuple[str, ...]) -> None:
+ """Calls each step individually, continuing if a step for a specific world step fails."""
+ world_types = {world.__class__ for world in self.multiworld.worlds.values()}
+ for step in steps:
+ for player, world in self.multiworld.worlds.items():
+ with self.subTest(game=world.game, step=step):
+ call_single(self.multiworld, step, player)
+ for world_type in sorted(world_types, key=lambda world: world.__name__):
+ with self.subTest(game=world_type.game, step=f"stage_{step}"):
+ stage_callable = getattr(world_type, f"stage_{step}", None)
+ if stage_callable:
+ stage_callable(self.multiworld)
+
+
+@unittest.skip("too slow for main")
+class TestAllGamesMultiworld(MultiworldTestBase):
+ def test_fills(self) -> None:
+ """Tests that a multiworld with one of every registered game world can generate."""
+ all_worlds = list(AutoWorldRegister.world_types.values())
+ self.multiworld = setup_multiworld(all_worlds, ())
+ for world in self.multiworld.worlds.values():
+ world.options.accessibility.value = Accessibility.option_locations
+ self.assertSteps(gen_steps)
+ with self.subTest("filling multiworld", seed=self.multiworld.seed):
+ distribute_items_restrictive(self.multiworld)
+ call_all(self.multiworld, "post_fill")
+ self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
+
+
+class TestTwoPlayerMulti(MultiworldTestBase):
+ def test_two_player_single_game_fills(self) -> None:
+ """Tests that a multiworld of two players for each registered game world can generate."""
+ for world in AutoWorldRegister.world_types.values():
+ self.multiworld = setup_multiworld([world, world], ())
+ for world in self.multiworld.worlds.values():
+ world.options.accessibility.value = Accessibility.option_full
+ self.assertSteps(gen_steps)
+ with self.subTest("filling multiworld", seed=self.multiworld.seed):
+ distribute_items_restrictive(self.multiworld)
+ call_all(self.multiworld, "post_fill")
+ self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
diff --git a/test/netutils/TestLocationStore.py b/test/netutils/TestLocationStore.py
deleted file mode 100644
index a7f117255faa..000000000000
--- a/test/netutils/TestLocationStore.py
+++ /dev/null
@@ -1,238 +0,0 @@
-# Tests for _speedups.LocationStore and NetUtils._LocationStore
-import typing
-import unittest
-import warnings
-from NetUtils import LocationStore, _LocationStore
-
-State = typing.Dict[typing.Tuple[int, int], typing.Set[int]]
-RawLocations = typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
-
-sample_data: RawLocations = {
- 1: {
- 11: (21, 2, 7),
- 12: (22, 2, 0),
- 13: (13, 1, 0),
- },
- 2: {
- 23: (11, 1, 0),
- 22: (12, 1, 0),
- 21: (23, 2, 0),
- },
- 4: {
- 9: (99, 3, 0),
- },
- 3: {
- 9: (99, 4, 0),
- },
-}
-
-empty_state: State = {
- (0, slot): set() for slot in sample_data
-}
-
-full_state: State = {
- (0, slot): set(locations) for (slot, locations) in sample_data.items()
-}
-
-one_state: State = {
- (0, 1): {12}
-}
-
-
-class Base:
- class TestLocationStore(unittest.TestCase):
- """Test method calls on a loaded store."""
- store: typing.Union[LocationStore, _LocationStore]
-
- def test_len(self) -> None:
- self.assertEqual(len(self.store), 4)
- self.assertEqual(len(self.store[1]), 3)
-
- def test_key_error(self) -> None:
- with self.assertRaises(KeyError):
- _ = self.store[0]
- with self.assertRaises(KeyError):
- _ = self.store[5]
- locations = self.store[1] # no Exception
- with self.assertRaises(KeyError):
- _ = locations[7]
- _ = locations[11] # no Exception
-
- def test_getitem(self) -> None:
- self.assertEqual(self.store[1][11], (21, 2, 7))
- self.assertEqual(self.store[1][13], (13, 1, 0))
- self.assertEqual(self.store[2][22], (12, 1, 0))
- self.assertEqual(self.store[4][9], (99, 3, 0))
-
- def test_get(self) -> None:
- self.assertEqual(self.store.get(1, None), self.store[1])
- self.assertEqual(self.store.get(0, None), None)
- self.assertEqual(self.store[1].get(11, (None, None, None)), self.store[1][11])
- self.assertEqual(self.store[1].get(10, (None, None, None)), (None, None, None))
-
- def test_iter(self) -> None:
- self.assertEqual(sorted(self.store), [1, 2, 3, 4])
- self.assertEqual(len(self.store), len(sample_data))
- self.assertEqual(list(self.store[1]), [11, 12, 13])
- self.assertEqual(len(self.store[1]), len(sample_data[1]))
-
- def test_items(self) -> None:
- self.assertEqual(sorted(p for p, _ in self.store.items()), sorted(self.store))
- self.assertEqual(sorted(p for p, _ in self.store[1].items()), sorted(self.store[1]))
- self.assertEqual(sorted(self.store.items())[0][0], 1)
- self.assertEqual(sorted(self.store.items())[0][1], self.store[1])
- self.assertEqual(sorted(self.store[1].items())[0][0], 11)
- self.assertEqual(sorted(self.store[1].items())[0][1], self.store[1][11])
-
- def test_find_item(self) -> None:
- self.assertEqual(sorted(self.store.find_item(set(), 99)), [])
- self.assertEqual(sorted(self.store.find_item({3}, 1)), [])
- self.assertEqual(sorted(self.store.find_item({5}, 99)), [])
- self.assertEqual(sorted(self.store.find_item({3}, 99)),
- [(4, 9, 99, 3, 0)])
- self.assertEqual(sorted(self.store.find_item({3, 4}, 99)),
- [(3, 9, 99, 4, 0), (4, 9, 99, 3, 0)])
-
- def test_get_for_player(self) -> None:
- self.assertEqual(self.store.get_for_player(3), {4: {9}})
- self.assertEqual(self.store.get_for_player(1), {1: {13}, 2: {22, 23}})
-
- def test_get_checked(self) -> None:
- self.assertEqual(self.store.get_checked(full_state, 0, 1), [11, 12, 13])
- self.assertEqual(self.store.get_checked(one_state, 0, 1), [12])
- self.assertEqual(self.store.get_checked(empty_state, 0, 1), [])
- self.assertEqual(self.store.get_checked(full_state, 0, 3), [9])
-
- def test_get_missing(self) -> None:
- self.assertEqual(self.store.get_missing(full_state, 0, 1), [])
- self.assertEqual(self.store.get_missing(one_state, 0, 1), [11, 13])
- self.assertEqual(self.store.get_missing(empty_state, 0, 1), [11, 12, 13])
- self.assertEqual(self.store.get_missing(empty_state, 0, 3), [9])
-
- def test_get_remaining(self) -> None:
- self.assertEqual(self.store.get_remaining(full_state, 0, 1), [])
- self.assertEqual(self.store.get_remaining(one_state, 0, 1), [13, 21])
- self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [13, 21, 22])
- self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [99])
-
- def test_location_set_intersection(self) -> None:
- locations = {10, 11, 12}
- locations.intersection_update(self.store[1])
- self.assertEqual(locations, {11, 12})
-
- class TestLocationStoreConstructor(unittest.TestCase):
- """Test constructors for a given store type."""
- type: type
-
- def test_hole(self) -> None:
- with self.assertRaises(Exception):
- self.type({
- 1: {1: (1, 1, 1)},
- 3: {1: (1, 1, 1)},
- })
-
- def test_no_slot1(self) -> None:
- with self.assertRaises(Exception):
- self.type({
- 2: {1: (1, 1, 1)},
- 3: {1: (1, 1, 1)},
- })
-
- def test_slot0(self) -> None:
- with self.assertRaises(ValueError):
- self.type({
- 0: {1: (1, 1, 1)},
- 1: {1: (1, 1, 1)},
- })
- with self.assertRaises(ValueError):
- self.type({
- 0: {1: (1, 1, 1)},
- 2: {1: (1, 1, 1)},
- })
-
- def test_no_players(self) -> None:
- with self.assertRaises(Exception):
- _ = self.type({})
-
- def test_no_locations(self) -> None:
- with warnings.catch_warnings():
- warnings.simplefilter("ignore")
- store = self.type({
- 1: {},
- })
- self.assertEqual(len(store), 1)
- self.assertEqual(len(store[1]), 0)
-
- def test_no_locations_for_1(self) -> None:
- store = self.type({
- 1: {},
- 2: {1: (1, 2, 3)},
- })
- self.assertEqual(len(store), 2)
- self.assertEqual(len(store[1]), 0)
- self.assertEqual(len(store[2]), 1)
-
- def test_no_locations_for_last(self) -> None:
- store = self.type({
- 1: {1: (1, 2, 3)},
- 2: {},
- })
- self.assertEqual(len(store), 2)
- self.assertEqual(len(store[1]), 1)
- self.assertEqual(len(store[2]), 0)
-
-
-class TestPurePythonLocationStore(Base.TestLocationStore):
- """Run base method tests for pure python implementation."""
- def setUp(self) -> None:
- self.store = _LocationStore(sample_data)
- super().setUp()
-
-
-class TestPurePythonLocationStoreConstructor(Base.TestLocationStoreConstructor):
- """Run base constructor tests for the pure python implementation."""
- def setUp(self) -> None:
- self.type = _LocationStore
- super().setUp()
-
-
-@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
-class TestSpeedupsLocationStore(Base.TestLocationStore):
- """Run base method tests for cython implementation."""
- def setUp(self) -> None:
- self.store = LocationStore(sample_data)
- super().setUp()
-
-
-@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
-class TestSpeedupsLocationStoreConstructor(Base.TestLocationStoreConstructor):
- """Run base constructor tests and tests the additional constraints for cython implementation."""
- def setUp(self) -> None:
- self.type = LocationStore
- super().setUp()
-
- def test_float_key(self) -> None:
- with self.assertRaises(Exception):
- self.type({
- 1: {1: (1, 1, 1)},
- 1.1: {1: (1, 1, 1)},
- 3: {1: (1, 1, 1)}
- })
-
- def test_string_key(self) -> None:
- with self.assertRaises(Exception):
- self.type({
- "1": {1: (1, 1, 1)},
- })
-
- def test_high_player_number(self) -> None:
- with self.assertRaises(Exception):
- self.type({
- 1 << 32: {1: (1, 1, 1)},
- })
-
- def test_not_a_tuple(self) -> None:
- with self.assertRaises(Exception):
- self.type({
- 1: {1: None},
- })
diff --git a/test/netutils/test_location_store.py b/test/netutils/test_location_store.py
new file mode 100644
index 000000000000..f3e83989bea4
--- /dev/null
+++ b/test/netutils/test_location_store.py
@@ -0,0 +1,259 @@
+# Tests for _speedups.LocationStore and NetUtils._LocationStore
+import os
+import typing
+import unittest
+import warnings
+from NetUtils import LocationStore, _LocationStore
+
+State = typing.Dict[typing.Tuple[int, int], typing.Set[int]]
+RawLocations = typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
+
+ci = bool(os.environ.get("CI")) # always set in GitHub actions
+
+sample_data: RawLocations = {
+ 1: {
+ 11: (21, 2, 7),
+ 12: (22, 2, 0),
+ 13: (13, 1, 0),
+ },
+ 2: {
+ 23: (11, 1, 0),
+ 22: (12, 1, 0),
+ 21: (23, 2, 0),
+ },
+ 4: {
+ 9: (99, 3, 0),
+ },
+ 3: {
+ 9: (99, 4, 0),
+ },
+ 5: {
+ 9: (99, 5, 0),
+ }
+}
+
+empty_state: State = {
+ (0, slot): set() for slot in sample_data
+}
+
+full_state: State = {
+ (0, slot): set(locations) for (slot, locations) in sample_data.items()
+}
+
+one_state: State = {
+ (0, 1): {12}
+}
+
+
+class Base:
+ class TestLocationStore(unittest.TestCase):
+ """Test method calls on a loaded store."""
+ store: typing.Union[LocationStore, _LocationStore]
+
+ def test_len(self) -> None:
+ self.assertEqual(len(self.store), 5)
+ self.assertEqual(len(self.store[1]), 3)
+
+ def test_key_error(self) -> None:
+ with self.assertRaises(KeyError):
+ _ = self.store[0]
+ with self.assertRaises(KeyError):
+ _ = self.store[6]
+ locations = self.store[1] # no Exception
+ with self.assertRaises(KeyError):
+ _ = locations[7]
+ _ = locations[11] # no Exception
+
+ def test_getitem(self) -> None:
+ self.assertEqual(self.store[1][11], (21, 2, 7))
+ self.assertEqual(self.store[1][13], (13, 1, 0))
+ self.assertEqual(self.store[2][22], (12, 1, 0))
+ self.assertEqual(self.store[4][9], (99, 3, 0))
+
+ def test_get(self) -> None:
+ self.assertEqual(self.store.get(1, None), self.store[1])
+ self.assertEqual(self.store.get(0, None), None)
+ self.assertEqual(self.store[1].get(11, (None, None, None)), self.store[1][11])
+ self.assertEqual(self.store[1].get(10, (None, None, None)), (None, None, None))
+
+ def test_iter(self) -> None:
+ self.assertEqual(sorted(self.store), [1, 2, 3, 4, 5])
+ self.assertEqual(len(self.store), len(sample_data))
+ self.assertEqual(list(self.store[1]), [11, 12, 13])
+ self.assertEqual(len(self.store[1]), len(sample_data[1]))
+
+ def test_items(self) -> None:
+ self.assertEqual(sorted(p for p, _ in self.store.items()), sorted(self.store))
+ self.assertEqual(sorted(p for p, _ in self.store[1].items()), sorted(self.store[1]))
+ self.assertEqual(sorted(self.store.items())[0][0], 1)
+ self.assertEqual(sorted(self.store.items())[0][1], self.store[1])
+ self.assertEqual(sorted(self.store[1].items())[0][0], 11)
+ self.assertEqual(sorted(self.store[1].items())[0][1], self.store[1][11])
+
+ def test_find_item(self) -> None:
+ # empty player set
+ self.assertEqual(sorted(self.store.find_item(set(), 99)), [])
+ # no such player, single
+ self.assertEqual(sorted(self.store.find_item({6}, 99)), [])
+ # no such player, set
+ self.assertEqual(sorted(self.store.find_item({7, 8, 9}, 99)), [])
+ # no such item
+ self.assertEqual(sorted(self.store.find_item({3}, 1)), [])
+ # valid matches
+ self.assertEqual(sorted(self.store.find_item({3}, 99)),
+ [(4, 9, 99, 3, 0)])
+ self.assertEqual(sorted(self.store.find_item({3, 4}, 99)),
+ [(3, 9, 99, 4, 0), (4, 9, 99, 3, 0)])
+ self.assertEqual(sorted(self.store.find_item({2, 3, 4}, 99)),
+ [(3, 9, 99, 4, 0), (4, 9, 99, 3, 0)])
+ # test hash collision in set
+ self.assertEqual(sorted(self.store.find_item({3, 5}, 99)),
+ [(4, 9, 99, 3, 0), (5, 9, 99, 5, 0)])
+ self.assertEqual(sorted(self.store.find_item(set(range(2048)), 13)),
+ [(1, 13, 13, 1, 0)])
+
+ def test_get_for_player(self) -> None:
+ self.assertEqual(self.store.get_for_player(3), {4: {9}})
+ self.assertEqual(self.store.get_for_player(1), {1: {13}, 2: {22, 23}})
+
+ def test_get_checked(self) -> None:
+ self.assertEqual(self.store.get_checked(full_state, 0, 1), [11, 12, 13])
+ self.assertEqual(self.store.get_checked(one_state, 0, 1), [12])
+ self.assertEqual(self.store.get_checked(empty_state, 0, 1), [])
+ self.assertEqual(self.store.get_checked(full_state, 0, 3), [9])
+
+ def test_get_missing(self) -> None:
+ self.assertEqual(self.store.get_missing(full_state, 0, 1), [])
+ self.assertEqual(self.store.get_missing(one_state, 0, 1), [11, 13])
+ self.assertEqual(self.store.get_missing(empty_state, 0, 1), [11, 12, 13])
+ self.assertEqual(self.store.get_missing(empty_state, 0, 3), [9])
+
+ def test_get_remaining(self) -> None:
+ self.assertEqual(self.store.get_remaining(full_state, 0, 1), [])
+ self.assertEqual(self.store.get_remaining(one_state, 0, 1), [13, 21])
+ self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [13, 21, 22])
+ self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [99])
+
+ def test_location_set_intersection(self) -> None:
+ locations = {10, 11, 12}
+ locations.intersection_update(self.store[1])
+ self.assertEqual(locations, {11, 12})
+
+ class TestLocationStoreConstructor(unittest.TestCase):
+ """Test constructors for a given store type."""
+ type: type
+
+ def test_hole(self) -> None:
+ with self.assertRaises(Exception):
+ self.type({
+ 1: {1: (1, 1, 1)},
+ 3: {1: (1, 1, 1)},
+ })
+
+ def test_no_slot1(self) -> None:
+ with self.assertRaises(Exception):
+ self.type({
+ 2: {1: (1, 1, 1)},
+ 3: {1: (1, 1, 1)},
+ })
+
+ def test_slot0(self) -> None:
+ with self.assertRaises(ValueError):
+ self.type({
+ 0: {1: (1, 1, 1)},
+ 1: {1: (1, 1, 1)},
+ })
+ with self.assertRaises(ValueError):
+ self.type({
+ 0: {1: (1, 1, 1)},
+ 2: {1: (1, 1, 1)},
+ })
+
+ def test_no_players(self) -> None:
+ with self.assertRaises(Exception):
+ _ = self.type({})
+
+ def test_no_locations(self) -> None:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ store = self.type({
+ 1: {},
+ })
+ self.assertEqual(len(store), 1)
+ self.assertEqual(len(store[1]), 0)
+
+ def test_no_locations_for_1(self) -> None:
+ store = self.type({
+ 1: {},
+ 2: {1: (1, 2, 3)},
+ })
+ self.assertEqual(len(store), 2)
+ self.assertEqual(len(store[1]), 0)
+ self.assertEqual(len(store[2]), 1)
+
+ def test_no_locations_for_last(self) -> None:
+ store = self.type({
+ 1: {1: (1, 2, 3)},
+ 2: {},
+ })
+ self.assertEqual(len(store), 2)
+ self.assertEqual(len(store[1]), 1)
+ self.assertEqual(len(store[2]), 0)
+
+
+class TestPurePythonLocationStore(Base.TestLocationStore):
+ """Run base method tests for pure python implementation."""
+ def setUp(self) -> None:
+ self.store = _LocationStore(sample_data)
+ super().setUp()
+
+
+class TestPurePythonLocationStoreConstructor(Base.TestLocationStoreConstructor):
+ """Run base constructor tests for the pure python implementation."""
+ def setUp(self) -> None:
+ self.type = _LocationStore
+ super().setUp()
+
+
+@unittest.skipIf(LocationStore is _LocationStore and not ci, "_speedups not available")
+class TestSpeedupsLocationStore(Base.TestLocationStore):
+ """Run base method tests for cython implementation."""
+ def setUp(self) -> None:
+ self.assertFalse(LocationStore is _LocationStore, "Failed to load _speedups")
+ self.store = LocationStore(sample_data)
+ super().setUp()
+
+
+@unittest.skipIf(LocationStore is _LocationStore and not ci, "_speedups not available")
+class TestSpeedupsLocationStoreConstructor(Base.TestLocationStoreConstructor):
+ """Run base constructor tests and tests the additional constraints for cython implementation."""
+ def setUp(self) -> None:
+ self.assertFalse(LocationStore is _LocationStore, "Failed to load _speedups")
+ self.type = LocationStore
+ super().setUp()
+
+ def test_float_key(self) -> None:
+ with self.assertRaises(Exception):
+ self.type({
+ 1: {1: (1, 1, 1)},
+ 1.1: {1: (1, 1, 1)},
+ 3: {1: (1, 1, 1)}
+ })
+
+ def test_string_key(self) -> None:
+ with self.assertRaises(Exception):
+ self.type({
+ "1": {1: (1, 1, 1)},
+ })
+
+ def test_high_player_number(self) -> None:
+ with self.assertRaises(Exception):
+ self.type({
+ 1 << 32: {1: (1, 1, 1)},
+ })
+
+ def test_not_a_tuple(self) -> None:
+ with self.assertRaises(Exception):
+ self.type({
+ 1: {1: None},
+ })
diff --git a/test/options/__init__.py b/test/options/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/test/options/test_option_classes.py b/test/options/test_option_classes.py
new file mode 100644
index 000000000000..8e2c4702c380
--- /dev/null
+++ b/test/options/test_option_classes.py
@@ -0,0 +1,67 @@
+import unittest
+
+from Options import Choice, DefaultOnToggle, Toggle
+
+
+class TestNumericOptions(unittest.TestCase):
+ def test_numeric_option(self) -> None:
+ """Tests the initialization and equivalency comparisons of the base Numeric Option class."""
+ class TestChoice(Choice):
+ option_zero = 0
+ option_one = 1
+ option_two = 2
+ alias_three = 1
+ non_option_attr = 2
+
+ class TestToggle(Toggle):
+ pass
+
+ class TestDefaultOnToggle(DefaultOnToggle):
+ pass
+
+ with self.subTest("choice"):
+ choice_option_default = TestChoice.from_any(TestChoice.default)
+ choice_option_string = TestChoice.from_any("one")
+ choice_option_int = TestChoice.from_any(2)
+ choice_option_alias = TestChoice.from_any("three")
+ choice_option_attr = TestChoice.from_any(TestChoice.option_two)
+
+ self.assertEqual(choice_option_default, TestChoice.option_zero,
+ "assigning default didn't match default value")
+ self.assertEqual(choice_option_string, "one")
+ self.assertEqual(choice_option_int, 2)
+ self.assertEqual(choice_option_alias, TestChoice.alias_three)
+ self.assertEqual(choice_option_attr, TestChoice.non_option_attr)
+
+ self.assertRaises(KeyError, TestChoice.from_any, "four")
+
+ self.assertIn(choice_option_int, [1, 2, 3])
+ self.assertIn(choice_option_int, {2})
+ self.assertIn(choice_option_int, (2,))
+
+ self.assertIn(choice_option_string, ["one", "two", "three"])
+ # this fails since the hash is derived from the value
+ self.assertNotIn(choice_option_string, {"one"})
+ self.assertIn(choice_option_string, ("one",))
+
+ with self.subTest("toggle"):
+ toggle_default = TestToggle.from_any(TestToggle.default)
+ toggle_string = TestToggle.from_any("false")
+ toggle_int = TestToggle.from_any(0)
+ toggle_alias = TestToggle.from_any("off")
+
+ self.assertFalse(toggle_default)
+ self.assertFalse(toggle_string)
+ self.assertFalse(toggle_int)
+ self.assertFalse(toggle_alias)
+
+ with self.subTest("on toggle"):
+ toggle_default = TestDefaultOnToggle.from_any(TestDefaultOnToggle.default)
+ toggle_string = TestDefaultOnToggle.from_any("true")
+ toggle_int = TestDefaultOnToggle.from_any(1)
+ toggle_alias = TestDefaultOnToggle.from_any("on")
+
+ self.assertTrue(toggle_default)
+ self.assertTrue(toggle_string)
+ self.assertTrue(toggle_int)
+ self.assertTrue(toggle_alias)
diff --git a/test/programs/TestGenerate.py b/test/programs/TestGenerate.py
deleted file mode 100644
index d04e1f2c5bf4..000000000000
--- a/test/programs/TestGenerate.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# Tests for Generate.py (ArchipelagoGenerate.exe)
-
-import unittest
-import sys
-from pathlib import Path
-from tempfile import TemporaryDirectory
-import os.path
-import os
-import ModuleUpdate
-ModuleUpdate.update_ran = True # don't upgrade
-import Generate
-
-
-class TestGenerateMain(unittest.TestCase):
- """This tests Generate.py (ArchipelagoGenerate.exe) main"""
-
- generate_dir = Path(Generate.__file__).parent
- run_dir = generate_dir / "test" # reproducible cwd that's neither __file__ nor Generate.__file__
- abs_input_dir = Path(__file__).parent / 'data' / 'OnePlayer'
- rel_input_dir = abs_input_dir.relative_to(run_dir) # directly supplied relative paths are relative to cwd
- yaml_input_dir = abs_input_dir.relative_to(generate_dir) # yaml paths are relative to user_path
-
- def assertOutput(self, output_dir: str):
- output_path = Path(output_dir)
- output_files = list(output_path.glob('*.zip'))
- if len(output_files) == 1:
- return True
- self.fail(f"Expected {output_dir} to contain one zip, but has {len(output_files)}: "
- f"{list(output_path.glob('*'))}")
-
- def setUp(self):
- self.original_argv = sys.argv.copy()
- self.original_cwd = os.getcwd()
- self.original_local_path = Generate.Utils.local_path.cached_path
- self.original_user_path = Generate.Utils.user_path.cached_path
-
- # Force both user_path and local_path to a specific path. They have independent caches.
- Generate.Utils.user_path.cached_path = Generate.Utils.local_path.cached_path = str(self.generate_dir)
- os.chdir(self.run_dir)
- self.output_tempdir = TemporaryDirectory(prefix='AP_out_')
-
- def tearDown(self):
- self.output_tempdir.cleanup()
- os.chdir(self.original_cwd)
- sys.argv = self.original_argv
- Generate.Utils.local_path.cached_path = self.original_local_path
- Generate.Utils.user_path.cached_path = self.original_user_path
-
- def test_paths(self):
- self.assertTrue(os.path.exists(self.generate_dir))
- self.assertTrue(os.path.exists(self.run_dir))
- self.assertTrue(os.path.exists(self.abs_input_dir))
- self.assertTrue(os.path.exists(self.rel_input_dir))
- self.assertFalse(os.path.exists(self.yaml_input_dir)) # relative to user_path, not cwd
-
- def test_generate_absolute(self):
- sys.argv = [sys.argv[0], '--seed', '0',
- '--player_files_path', str(self.abs_input_dir),
- '--outputpath', self.output_tempdir.name]
- print(f'Testing Generate.py {sys.argv} in {os.getcwd()}')
- Generate.main()
-
- self.assertOutput(self.output_tempdir.name)
-
- def test_generate_relative(self):
- sys.argv = [sys.argv[0], '--seed', '0',
- '--player_files_path', str(self.rel_input_dir),
- '--outputpath', self.output_tempdir.name]
- print(f'Testing Generate.py {sys.argv} in {os.getcwd()}')
- Generate.main()
-
- self.assertOutput(self.output_tempdir.name)
-
- def test_generate_yaml(self):
- # override host.yaml
- from settings import get_settings
- from Utils import user_path, local_path
- settings = get_settings()
- # NOTE: until/unless we override settings.Group's setattr, we have to upcast the input dir here
- settings.generator.player_files_path = settings.generator.PlayerFilesPath(self.yaml_input_dir)
- settings.generator.players = 0
- settings._filename = None # don't write to disk
- user_path_backup = user_path.cached_path
- user_path.cached_path = local_path() # test yaml is actually in local_path
- try:
- sys.argv = [sys.argv[0], '--seed', '0',
- '--outputpath', self.output_tempdir.name]
- print(f'Testing Generate.py {sys.argv} in {os.getcwd()}, player_files_path={self.yaml_input_dir}')
- Generate.main()
- finally:
- user_path.cached_path = user_path_backup
-
- self.assertOutput(self.output_tempdir.name)
diff --git a/test/programs/data/OnePlayer/test.yaml b/test/programs/data/one_player/test.yaml
similarity index 100%
rename from test/programs/data/OnePlayer/test.yaml
rename to test/programs/data/one_player/test.yaml
diff --git a/test/programs/test_common_client.py b/test/programs/test_common_client.py
new file mode 100644
index 000000000000..9936240d17b9
--- /dev/null
+++ b/test/programs/test_common_client.py
@@ -0,0 +1,106 @@
+import unittest
+
+import NetUtils
+from CommonClient import CommonContext
+
+
+class TestCommonContext(unittest.IsolatedAsyncioTestCase):
+ async def asyncSetUp(self):
+ self.ctx = CommonContext()
+ self.ctx.slot = 1 # Pretend we're player 1 for this.
+ self.ctx.slot_info.update({
+ 1: NetUtils.NetworkSlot("Player 1", "__TestGame1", NetUtils.SlotType.player),
+ 2: NetUtils.NetworkSlot("Player 2", "__TestGame1", NetUtils.SlotType.player),
+ 3: NetUtils.NetworkSlot("Player 3", "__TestGame2", NetUtils.SlotType.player),
+ })
+ self.ctx.consume_players_package([
+ NetUtils.NetworkPlayer(1, 1, "Player 1", "Player 1"),
+ NetUtils.NetworkPlayer(1, 2, "Player 2", "Player 2"),
+ NetUtils.NetworkPlayer(1, 3, "Player 3", "Player 3"),
+ ])
+ # Using IDs outside the "safe range" for testing purposes only. If this fails unit tests, it's because
+ # another world is not following the spec for allowed ID ranges.
+ self.ctx.update_data_package({
+ "games": {
+ "__TestGame1": {
+ "location_name_to_id": {
+ "Test Location 1 - Safe": 2**54 + 1,
+ "Test Location 2 - Duplicate": 2**54 + 2,
+ },
+ "item_name_to_id": {
+ "Test Item 1 - Safe": 2**54 + 1,
+ "Test Item 2 - Duplicate": 2**54 + 2,
+ },
+ },
+ "__TestGame2": {
+ "location_name_to_id": {
+ "Test Location 3 - Duplicate": 2**54 + 2,
+ },
+ "item_name_to_id": {
+ "Test Item 3 - Duplicate": 2**54 + 2,
+ },
+ },
+ },
+ })
+
+ async def test_archipelago_datapackage_lookups_exist(self):
+ assert "Archipelago" in self.ctx.item_names, "Archipelago item names entry does not exist"
+ assert "Archipelago" in self.ctx.location_names, "Archipelago location names entry does not exist"
+
+ async def test_implicit_name_lookups(self):
+ # Items
+ assert self.ctx.item_names[2**54 + 1] == "Test Item 1 - Safe"
+ assert self.ctx.item_names[2**54 + 3] == f"Unknown item (ID: {2**54+3})"
+ assert self.ctx.item_names[-1] == "Nothing"
+
+ # Locations
+ assert self.ctx.location_names[2**54 + 1] == "Test Location 1 - Safe"
+ assert self.ctx.location_names[2**54 + 3] == f"Unknown location (ID: {2**54+3})"
+ assert self.ctx.location_names[-1] == "Cheat Console"
+
+ async def test_explicit_name_lookups(self):
+ # Items
+ assert self.ctx.item_names["__TestGame1"][2**54+1] == "Test Item 1 - Safe"
+ assert self.ctx.item_names["__TestGame1"][2**54+2] == "Test Item 2 - Duplicate"
+ assert self.ctx.item_names["__TestGame1"][2**54+3] == f"Unknown item (ID: {2**54+3})"
+ assert self.ctx.item_names["__TestGame1"][-1] == "Nothing"
+ assert self.ctx.item_names["__TestGame2"][2**54+1] == f"Unknown item (ID: {2**54+1})"
+ assert self.ctx.item_names["__TestGame2"][2**54+2] == "Test Item 3 - Duplicate"
+ assert self.ctx.item_names["__TestGame2"][2**54+3] == f"Unknown item (ID: {2**54+3})"
+ assert self.ctx.item_names["__TestGame2"][-1] == "Nothing"
+
+ # Locations
+ assert self.ctx.location_names["__TestGame1"][2**54+1] == "Test Location 1 - Safe"
+ assert self.ctx.location_names["__TestGame1"][2**54+2] == "Test Location 2 - Duplicate"
+ assert self.ctx.location_names["__TestGame1"][2**54+3] == f"Unknown location (ID: {2**54+3})"
+ assert self.ctx.location_names["__TestGame1"][-1] == "Cheat Console"
+ assert self.ctx.location_names["__TestGame2"][2**54+1] == f"Unknown location (ID: {2**54+1})"
+ assert self.ctx.location_names["__TestGame2"][2**54+2] == "Test Location 3 - Duplicate"
+ assert self.ctx.location_names["__TestGame2"][2**54+3] == f"Unknown location (ID: {2**54+3})"
+ assert self.ctx.location_names["__TestGame2"][-1] == "Cheat Console"
+
+ async def test_lookup_helper_functions(self):
+ # Checking own slot.
+ assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1) == "Test Item 1 - Safe"
+ assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2) == "Test Item 2 - Duplicate"
+ assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 3) == f"Unknown item (ID: {2 ** 54 + 3})"
+ assert self.ctx.item_names.lookup_in_slot(-1) == f"Nothing"
+
+ # Checking others' slots.
+ assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1, 2) == "Test Item 1 - Safe"
+ assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2, 2) == "Test Item 2 - Duplicate"
+ assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1, 3) == f"Unknown item (ID: {2 ** 54 + 1})"
+ assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2, 3) == "Test Item 3 - Duplicate"
+
+ # Checking by game.
+ assert self.ctx.item_names.lookup_in_game(2 ** 54 + 1, "__TestGame1") == "Test Item 1 - Safe"
+ assert self.ctx.item_names.lookup_in_game(2 ** 54 + 2, "__TestGame1") == "Test Item 2 - Duplicate"
+ assert self.ctx.item_names.lookup_in_game(2 ** 54 + 3, "__TestGame1") == f"Unknown item (ID: {2 ** 54 + 3})"
+ assert self.ctx.item_names.lookup_in_game(2 ** 54 + 1, "__TestGame2") == f"Unknown item (ID: {2 ** 54 + 1})"
+ assert self.ctx.item_names.lookup_in_game(2 ** 54 + 2, "__TestGame2") == "Test Item 3 - Duplicate"
+
+ # Checking with Archipelago ids are valid in any game package.
+ assert self.ctx.item_names.lookup_in_slot(-1, 2) == "Nothing"
+ assert self.ctx.item_names.lookup_in_slot(-1, 3) == "Nothing"
+ assert self.ctx.item_names.lookup_in_game(-1, "__TestGame1") == "Nothing"
+ assert self.ctx.item_names.lookup_in_game(-1, "__TestGame2") == "Nothing"
diff --git a/test/programs/test_generate.py b/test/programs/test_generate.py
new file mode 100644
index 000000000000..9281c9c753cd
--- /dev/null
+++ b/test/programs/test_generate.py
@@ -0,0 +1,94 @@
+# Tests for Generate.py (ArchipelagoGenerate.exe)
+
+import unittest
+import os
+import os.path
+import sys
+
+from pathlib import Path
+from tempfile import TemporaryDirectory
+
+import Generate
+import Main
+
+
+class TestGenerateMain(unittest.TestCase):
+ """This tests Generate.py (ArchipelagoGenerate.exe) main"""
+
+ generate_dir = Path(Generate.__file__).parent
+ run_dir = generate_dir / "test" # reproducible cwd that's neither __file__ nor Generate.__file__
+ abs_input_dir = Path(__file__).parent / 'data' / 'one_player'
+ rel_input_dir = abs_input_dir.relative_to(run_dir) # directly supplied relative paths are relative to cwd
+ yaml_input_dir = abs_input_dir.relative_to(generate_dir) # yaml paths are relative to user_path
+
+ def assertOutput(self, output_dir: str):
+ output_path = Path(output_dir)
+ output_files = list(output_path.glob('*.zip'))
+ if len(output_files) == 1:
+ return True
+ self.fail(f"Expected {output_dir} to contain one zip, but has {len(output_files)}: "
+ f"{list(output_path.glob('*'))}")
+
+ def setUp(self):
+ self.original_argv = sys.argv.copy()
+ self.original_cwd = os.getcwd()
+ self.original_local_path = Generate.Utils.local_path.cached_path
+ self.original_user_path = Generate.Utils.user_path.cached_path
+
+ # Force both user_path and local_path to a specific path. They have independent caches.
+ Generate.Utils.user_path.cached_path = Generate.Utils.local_path.cached_path = str(self.generate_dir)
+ os.chdir(self.run_dir)
+ self.output_tempdir = TemporaryDirectory(prefix='AP_out_')
+
+ def tearDown(self):
+ self.output_tempdir.cleanup()
+ os.chdir(self.original_cwd)
+ sys.argv = self.original_argv
+ Generate.Utils.local_path.cached_path = self.original_local_path
+ Generate.Utils.user_path.cached_path = self.original_user_path
+
+ def test_paths(self):
+ self.assertTrue(os.path.exists(self.generate_dir))
+ self.assertTrue(os.path.exists(self.run_dir))
+ self.assertTrue(os.path.exists(self.abs_input_dir))
+ self.assertTrue(os.path.exists(self.rel_input_dir))
+ self.assertFalse(os.path.exists(self.yaml_input_dir)) # relative to user_path, not cwd
+
+ def test_generate_absolute(self):
+ sys.argv = [sys.argv[0], '--seed', '0',
+ '--player_files_path', str(self.abs_input_dir),
+ '--outputpath', self.output_tempdir.name]
+ print(f'Testing Generate.py {sys.argv} in {os.getcwd()}')
+ Main.main(*Generate.main())
+
+ self.assertOutput(self.output_tempdir.name)
+
+ def test_generate_relative(self):
+ sys.argv = [sys.argv[0], '--seed', '0',
+ '--player_files_path', str(self.rel_input_dir),
+ '--outputpath', self.output_tempdir.name]
+ print(f'Testing Generate.py {sys.argv} in {os.getcwd()}')
+ Main.main(*Generate.main())
+
+ self.assertOutput(self.output_tempdir.name)
+
+ def test_generate_yaml(self):
+ # override host.yaml
+ from settings import get_settings
+ from Utils import user_path, local_path
+ settings = get_settings()
+ # NOTE: until/unless we override settings.Group's setattr, we have to upcast the input dir here
+ settings.generator.player_files_path = settings.generator.PlayerFilesPath(self.yaml_input_dir)
+ settings.generator.players = 0
+ settings._filename = None # don't write to disk
+ user_path_backup = user_path.cached_path
+ user_path.cached_path = local_path() # test yaml is actually in local_path
+ try:
+ sys.argv = [sys.argv[0], '--seed', '0',
+ '--outputpath', self.output_tempdir.name]
+ print(f'Testing Generate.py {sys.argv} in {os.getcwd()}, player_files_path={self.yaml_input_dir}')
+ Main.main(*Generate.main())
+ finally:
+ user_path.cached_path = user_path_backup
+
+ self.assertOutput(self.output_tempdir.name)
diff --git a/test/programs/TestMultiServer.py b/test/programs/test_multi_server.py
similarity index 100%
rename from test/programs/TestMultiServer.py
rename to test/programs/test_multi_server.py
diff --git a/test/utils/test_caches.py b/test/utils/test_caches.py
new file mode 100644
index 000000000000..fc681611f0cf
--- /dev/null
+++ b/test/utils/test_caches.py
@@ -0,0 +1,66 @@
+# Tests for caches in Utils.py
+
+import unittest
+from typing import Any
+
+from Utils import cache_argsless, cache_self1
+
+
+class TestCacheArgless(unittest.TestCase):
+ def test_cache(self) -> None:
+ @cache_argsless
+ def func_argless() -> object:
+ return object()
+
+ self.assertTrue(func_argless() is func_argless())
+
+ if __debug__: # assert only available with __debug__
+ def test_invalid_decorator(self) -> None:
+ with self.assertRaises(Exception):
+ @cache_argsless # type: ignore[arg-type]
+ def func_with_arg(_: Any) -> None:
+ pass
+
+
+class TestCacheSelf1(unittest.TestCase):
+ def test_cache(self) -> None:
+ class Cls:
+ @cache_self1
+ def func(self, _: Any) -> object:
+ return object()
+
+ o1 = Cls()
+ o2 = Cls()
+ self.assertTrue(o1.func(1) is o1.func(1))
+ self.assertFalse(o1.func(1) is o1.func(2))
+ self.assertFalse(o1.func(1) is o2.func(1))
+
+ def test_gc(self) -> None:
+ # verify that we don't keep a global reference
+ import gc
+ import weakref
+
+ class Cls:
+ @cache_self1
+ def func(self, _: Any) -> object:
+ return object()
+
+ o = Cls()
+ _ = o.func(o) # keep a hard ref to the result
+ r = weakref.ref(o) # keep weak ref to the cache
+ del o # remove hard ref to the cache
+ gc.collect()
+ self.assertFalse(r()) # weak ref should be dead now
+
+ if __debug__: # assert only available with __debug__
+ def test_no_self(self) -> None:
+ with self.assertRaises(Exception):
+ @cache_self1 # type: ignore[arg-type]
+ def func() -> Any:
+ pass
+
+ def test_too_many_args(self) -> None:
+ with self.assertRaises(Exception):
+ @cache_self1 # type: ignore[arg-type]
+ def func(_1: Any, _2: Any, _3: Any) -> Any:
+ pass
diff --git a/test/utils/TestSIPrefix.py b/test/utils/test_si_prefix.py
similarity index 100%
rename from test/utils/TestSIPrefix.py
rename to test/utils/test_si_prefix.py
diff --git a/test/utils/test_yaml.py b/test/utils/test_yaml.py
new file mode 100644
index 000000000000..4e23857eb07f
--- /dev/null
+++ b/test/utils/test_yaml.py
@@ -0,0 +1,68 @@
+# Tests that yaml wrappers in Utils.py do what they should
+
+import unittest
+from typing import cast, Any, ClassVar, Dict
+
+from Utils import dump, Dumper # type: ignore[attr-defined]
+from Utils import parse_yaml, parse_yamls, unsafe_parse_yaml
+
+
+class AClass:
+ def __eq__(self, other: Any) -> bool:
+ return isinstance(other, self.__class__)
+
+
+class TestYaml(unittest.TestCase):
+ safe_data: ClassVar[Dict[str, Any]] = {
+ "a": [1, 2, 3],
+ "b": None,
+ "c": True,
+ }
+ unsafe_data: ClassVar[Dict[str, Any]] = {
+ "a": AClass()
+ }
+
+ @property
+ def safe_str(self) -> str:
+ return cast(str, dump(self.safe_data, Dumper=Dumper))
+
+ @property
+ def unsafe_str(self) -> str:
+ return cast(str, dump(self.unsafe_data, Dumper=Dumper))
+
+ def assertIsNonEmptyString(self, string: str) -> None:
+ self.assertTrue(string)
+ self.assertIsInstance(string, str)
+
+ def test_dump(self) -> None:
+ self.assertIsNonEmptyString(self.safe_str)
+ self.assertIsNonEmptyString(self.unsafe_str)
+
+ def test_safe_parse(self) -> None:
+ self.assertEqual(self.safe_data, parse_yaml(self.safe_str))
+ with self.assertRaises(Exception):
+ parse_yaml(self.unsafe_str)
+ with self.assertRaises(Exception):
+ parse_yaml("1\n---\n2\n")
+
+ def test_unsafe_parse(self) -> None:
+ self.assertEqual(self.safe_data, unsafe_parse_yaml(self.safe_str))
+ self.assertEqual(self.unsafe_data, unsafe_parse_yaml(self.unsafe_str))
+ with self.assertRaises(Exception):
+ unsafe_parse_yaml("1\n---\n2\n")
+
+ def test_multi_parse(self) -> None:
+ self.assertEqual(self.safe_data, next(parse_yamls(self.safe_str)))
+ with self.assertRaises(Exception):
+ next(parse_yamls(self.unsafe_str))
+ self.assertEqual(2, len(list(parse_yamls("1\n---\n2\n"))))
+
+ def test_unique_key(self) -> None:
+ s = """
+ a: 1
+ a: 2
+ """
+ with self.assertRaises(Exception):
+ parse_yaml(s)
+ with self.assertRaises(Exception):
+ next(parse_yamls(s))
diff --git a/test/webhost/TestAPIGenerate.py b/test/webhost/TestAPIGenerate.py
deleted file mode 100644
index 5c14373a90e6..000000000000
--- a/test/webhost/TestAPIGenerate.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import unittest
-import json
-
-
-class TestDocs(unittest.TestCase):
- @classmethod
- def setUpClass(cls) -> None:
- from WebHost import get_app, raw_app
- raw_app.config["PONY"] = {
- "provider": "sqlite",
- "filename": ":memory:",
- "create_db": True,
- }
- raw_app.config.update({
- "TESTING": True,
- })
- app = get_app()
-
- cls.client = app.test_client()
-
- def testCorrectErrorEmptyRequest(self):
- response = self.client.post("/api/generate")
- self.assertIn("No options found. Expected file attachment or json weights.", response.text)
-
- def testGenerationQueued(self):
- options = {
- "Tester1":
- {
- "game": "Archipelago",
- "name": "Tester",
- "Archipelago": {}
- }
- }
- response = self.client.post(
- "/api/generate",
- data=json.dumps({"weights": options}),
- content_type='application/json'
- )
- json_data = response.get_json()
- self.assertTrue(json_data["text"].startswith("Generation of seed "))
- self.assertTrue(json_data["text"].endswith(" started successfully."))
diff --git a/test/webhost/TestDocs.py b/test/webhost/TestDocs.py
deleted file mode 100644
index f6ede1543e26..000000000000
--- a/test/webhost/TestDocs.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import unittest
-import Utils
-import os
-
-import WebHost
-from worlds.AutoWorld import AutoWorldRegister
-
-
-class TestDocs(unittest.TestCase):
- @classmethod
- def setUpClass(cls) -> None:
- cls.tutorials_data = WebHost.create_ordered_tutorials_file()
-
- def testHasTutorial(self):
- games_with_tutorial = set(entry["gameTitle"] for entry in self.tutorials_data)
- for game_name, world_type in AutoWorldRegister.world_types.items():
- if not world_type.hidden:
- with self.subTest(game_name):
- try:
- self.assertIn(game_name, games_with_tutorial)
- except AssertionError:
- # look for partial name in the tutorial name
- for game in games_with_tutorial:
- if game_name in game:
- break
- else:
- self.fail(f"{game_name} has no setup tutorial. "
- f"Games with Tutorial: {games_with_tutorial}")
-
- def testHasGameInfo(self):
- for game_name, world_type in AutoWorldRegister.world_types.items():
- if not world_type.hidden:
- target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", game_name)
- for game_info_lang in world_type.web.game_info_languages:
- with self.subTest(game_name):
- self.assertTrue(
- os.path.isfile(Utils.local_path(target_path, f'{game_info_lang}_{game_name}.md')),
- f'{game_name} missing game info file for "{game_info_lang}" language.'
- )
diff --git a/test/webhost/TestFileGeneration.py b/test/webhost/TestFileGeneration.py
deleted file mode 100644
index 6010202c4101..000000000000
--- a/test/webhost/TestFileGeneration.py
+++ /dev/null
@@ -1,35 +0,0 @@
-"""Tests for successful generation of WebHost cached files. Can catch some other deeper errors."""
-
-import os
-import unittest
-
-import WebHost
-
-
-class TestFileGeneration(unittest.TestCase):
- @classmethod
- def setUpClass(cls) -> None:
- cls.correct_path = os.path.join(os.path.dirname(WebHost.__file__), "WebHostLib")
- # should not create the folder *here*
- cls.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
-
- def testOptions(self):
- WebHost.create_options_files()
- target = os.path.join(self.correct_path, "static", "generated", "configs")
- self.assertTrue(os.path.exists(target))
- self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs")))
-
- # folder seems fine, so now we try to generate Options based on the default file
- from WebHostLib.check import roll_options
- file: os.DirEntry
- for file in os.scandir(target):
- if file.is_file() and file.name.endswith(".yaml"):
- with self.subTest(file=file.name):
- with open(file, encoding="utf-8-sig") as f:
- for value in roll_options({file.name: f.read()})[0].values():
- self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.")
-
- def testTutorial(self):
- WebHost.create_ordered_tutorials_file()
- self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json")))
- self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "tutorials.json")))
diff --git a/test/webhost/__init__.py b/test/webhost/__init__.py
index e69de29bb2d1..2eb340722a3a 100644
--- a/test/webhost/__init__.py
+++ b/test/webhost/__init__.py
@@ -0,0 +1,36 @@
+import unittest
+import typing
+from uuid import uuid4
+
+from flask import Flask
+from flask.testing import FlaskClient
+
+
+class TestBase(unittest.TestCase):
+ app: typing.ClassVar[Flask]
+ client: FlaskClient
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ from WebHostLib import app as raw_app
+ from WebHost import get_app
+
+ raw_app.config["PONY"] = {
+ "provider": "sqlite",
+ "filename": ":memory:",
+ "create_db": True,
+ }
+ raw_app.config.update({
+ "TESTING": True,
+ "DEBUG": True,
+ })
+ try:
+ cls.app = get_app()
+ except AssertionError as e:
+ # since we only have 1 global app object, this might fail, but luckily all tests use the same config
+ if "register_blueprint" not in e.args[0]:
+ raise
+ cls.app = raw_app
+
+ def setUp(self) -> None:
+ self.client = self.app.test_client()
diff --git a/test/webhost/test_api_generate.py b/test/webhost/test_api_generate.py
new file mode 100644
index 000000000000..591c61d74880
--- /dev/null
+++ b/test/webhost/test_api_generate.py
@@ -0,0 +1,45 @@
+import io
+import json
+import yaml
+
+from . import TestBase
+
+
+class TestAPIGenerate(TestBase):
+ def test_correct_error_empty_request(self) -> None:
+ response = self.client.post("/api/generate")
+ self.assertIn("No options found. Expected file attachment or json weights.", response.text)
+
+ def test_generation_queued_weights(self) -> None:
+ options = {
+ "Tester1":
+ {
+ "game": "Archipelago",
+ "name": "Tester",
+ "Archipelago": {}
+ }
+ }
+ response = self.client.post(
+ "/api/generate",
+ data=json.dumps({"weights": options}),
+ content_type='application/json'
+ )
+ json_data = response.get_json()
+ self.assertTrue(json_data["text"].startswith("Generation of seed "))
+ self.assertTrue(json_data["text"].endswith(" started successfully."))
+
+ def test_generation_queued_file(self) -> None:
+ options = {
+ "game": "Archipelago",
+ "name": "Tester",
+ "Archipelago": {}
+ }
+ response = self.client.post(
+ "/api/generate",
+ data={
+ 'file': (io.BytesIO(yaml.dump(options, encoding="utf-8")), "test.yaml")
+ },
+ )
+ json_data = response.get_json()
+ self.assertTrue(json_data["text"].startswith("Generation of seed "))
+ self.assertTrue(json_data["text"].endswith(" started successfully."))
diff --git a/test/webhost/test_descriptions.py b/test/webhost/test_descriptions.py
new file mode 100644
index 000000000000..70f375b51cf0
--- /dev/null
+++ b/test/webhost/test_descriptions.py
@@ -0,0 +1,23 @@
+import unittest
+
+from worlds.AutoWorld import AutoWorldRegister
+
+
+class TestWebDescriptions(unittest.TestCase):
+ def test_item_descriptions_have_valid_names(self) -> None:
+ """Ensure all item descriptions match an item name or item group name"""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ valid_names = world_type.item_names.union(world_type.item_name_groups)
+ for name in world_type.web.item_descriptions:
+ with self.subTest("Name should be valid", game=game_name, item=name):
+ self.assertIn(name, valid_names,
+ "All item descriptions must match defined item names")
+
+ def test_location_descriptions_have_valid_names(self) -> None:
+ """Ensure all location descriptions match a location name or location group name"""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ valid_names = world_type.location_names.union(world_type.location_name_groups)
+ for name in world_type.web.location_descriptions:
+ with self.subTest("Name should be valid", game=game_name, location=name):
+ self.assertIn(name, valid_names,
+ "All location descriptions must match defined location names")
diff --git a/test/webhost/test_docs.py b/test/webhost/test_docs.py
new file mode 100644
index 000000000000..68aba05f9dcc
--- /dev/null
+++ b/test/webhost/test_docs.py
@@ -0,0 +1,39 @@
+import unittest
+import Utils
+import os
+
+import WebHost
+from worlds.AutoWorld import AutoWorldRegister
+
+
+class TestDocs(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls) -> None:
+ cls.tutorials_data = WebHost.create_ordered_tutorials_file()
+
+ def test_has_tutorial(self):
+ games_with_tutorial = set(entry["gameTitle"] for entry in self.tutorials_data)
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ if not world_type.hidden:
+ with self.subTest(game_name):
+ try:
+ self.assertIn(game_name, games_with_tutorial)
+ except AssertionError:
+ # look for partial name in the tutorial name
+ for game in games_with_tutorial:
+ if game_name in game:
+ break
+ else:
+ self.fail(f"{game_name} has no setup tutorial. "
+ f"Games with Tutorial: {games_with_tutorial}")
+
+ def test_has_game_info(self):
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ if not world_type.hidden:
+ target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", game_name)
+ for game_info_lang in world_type.web.game_info_languages:
+ with self.subTest(game_name):
+ self.assertTrue(
+ os.path.isfile(Utils.local_path(target_path, f'{game_info_lang}_{game_name}.md')),
+ f'{game_name} missing game info file for "{game_info_lang}" language.'
+ )
diff --git a/test/webhost/test_file_generation.py b/test/webhost/test_file_generation.py
new file mode 100644
index 000000000000..059f6b49a1fd
--- /dev/null
+++ b/test/webhost/test_file_generation.py
@@ -0,0 +1,36 @@
+"""Tests for successful generation of WebHost cached files. Can catch some other deeper errors."""
+
+import os
+import unittest
+
+import WebHost
+
+
+class TestFileGeneration(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls) -> None:
+ cls.correct_path = os.path.join(os.path.dirname(WebHost.__file__), "WebHostLib")
+ # should not create the folder *here*
+ cls.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
+
+ def test_options(self):
+ from WebHostLib.options import create as create_options_files
+ create_options_files()
+ target = os.path.join(self.correct_path, "static", "generated", "configs")
+ self.assertTrue(os.path.exists(target))
+ self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs")))
+
+ # folder seems fine, so now we try to generate Options based on the default file
+ from WebHostLib.check import roll_options
+ file: os.DirEntry
+ for file in os.scandir(target):
+ if file.is_file() and file.name.endswith(".yaml"):
+ with self.subTest(file=file.name):
+ with open(file, encoding="utf-8-sig") as f:
+ for value in roll_options({file.name: f.read()})[0].values():
+ self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.")
+
+ def test_tutorial(self):
+ WebHost.create_ordered_tutorials_file()
+ self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json")))
+ self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "tutorials.json")))
diff --git a/test/webhost/test_host_room.py b/test/webhost/test_host_room.py
new file mode 100644
index 000000000000..e9dae41dd06f
--- /dev/null
+++ b/test/webhost/test_host_room.py
@@ -0,0 +1,192 @@
+import os
+from uuid import UUID, uuid4, uuid5
+
+from flask import url_for
+
+from . import TestBase
+
+
+class TestHostFakeRoom(TestBase):
+ room_id: UUID
+ log_filename: str
+
+ def setUp(self) -> None:
+ from pony.orm import db_session
+ from Utils import user_path
+ from WebHostLib.models import Room, Seed
+
+ super().setUp()
+
+ with self.client.session_transaction() as session:
+ session["_id"] = uuid4()
+ with db_session:
+ # create an empty seed and a room from it
+ seed = Seed(multidata=b"", owner=session["_id"])
+ room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
+ self.room_id = room.id
+ self.log_filename = user_path("logs", f"{self.room_id}.txt")
+
+ def tearDown(self) -> None:
+ from pony.orm import db_session, select
+ from WebHostLib.models import Command, Room
+
+ with db_session:
+ for command in select(command for command in Command if command.room.id == self.room_id): # type: ignore
+ command.delete()
+ room: Room = Room.get(id=self.room_id)
+ room.seed.delete()
+ room.delete()
+
+ try:
+ os.unlink(self.log_filename)
+ except FileNotFoundError:
+ pass
+
+ def test_display_log_missing_full(self) -> None:
+ """
+ Verify that we get a 200 response even if log is missing.
+ This is required to not get an error for fetch.
+ """
+ with self.app.app_context(), self.app.test_request_context():
+ response = self.client.get(url_for("display_log", room=self.room_id))
+ self.assertEqual(response.status_code, 200)
+
+ def test_display_log_missing_range(self) -> None:
+ """
+ Verify that we get a full response for missing log even if we asked for range.
+ This is required for the JS logic to differentiate between log update and log error message.
+ """
+ with self.app.app_context(), self.app.test_request_context():
+ response = self.client.get(url_for("display_log", room=self.room_id), headers={
+ "Range": "bytes=100-"
+ })
+ self.assertEqual(response.status_code, 200)
+
+ def test_display_log_denied(self) -> None:
+ """Verify that only the owner can see the log."""
+ other_client = self.app.test_client()
+ with self.app.app_context(), self.app.test_request_context():
+ response = other_client.get(url_for("display_log", room=self.room_id))
+ self.assertEqual(response.status_code, 403)
+
+ def test_display_log_missing_room(self) -> None:
+ """Verify log for missing room gives an error as opposed to missing log for existing room."""
+ missing_room_id = uuid5(uuid4(), "") # rooms are always uuid4, so this can't exist
+ other_client = self.app.test_client()
+ with self.app.app_context(), self.app.test_request_context():
+ response = other_client.get(url_for("display_log", room=missing_room_id))
+ self.assertEqual(response.status_code, 404)
+
+ def test_display_log_full(self) -> None:
+ """Verify full log response."""
+ with open(self.log_filename, "w", encoding="utf-8") as f:
+ text = "x" * 200
+ f.write(text)
+
+ with self.app.app_context(), self.app.test_request_context():
+ response = self.client.get(url_for("display_log", room=self.room_id))
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.get_data(True), text)
+
+ def test_display_log_range(self) -> None:
+ """Verify that Range header in request gives a range in response."""
+ with open(self.log_filename, "w", encoding="utf-8") as f:
+ f.write(" " * 100)
+ text = "x" * 100
+ f.write(text)
+
+ with self.app.app_context(), self.app.test_request_context():
+ response = self.client.get(url_for("display_log", room=self.room_id), headers={
+ "Range": "bytes=100-"
+ })
+ self.assertEqual(response.status_code, 206)
+ self.assertEqual(response.get_data(True), text)
+
+ def test_display_log_range_bom(self) -> None:
+ """Verify that a BOM in the log file is skipped for range."""
+ with open(self.log_filename, "w", encoding="utf-8-sig") as f:
+ f.write(" " * 100)
+ text = "x" * 100
+ f.write(text)
+ self.assertEqual(f.tell(), 203) # including BOM
+
+ with self.app.app_context(), self.app.test_request_context():
+ response = self.client.get(url_for("display_log", room=self.room_id), headers={
+ "Range": "bytes=100-"
+ })
+ self.assertEqual(response.status_code, 206)
+ self.assertEqual(response.get_data(True), text)
+
+ def test_host_room_missing(self) -> None:
+ """Verify that missing room gives a 404 response."""
+ missing_room_id = uuid5(uuid4(), "") # rooms are always uuid4, so this can't exist
+ with self.app.app_context(), self.app.test_request_context():
+ response = self.client.get(url_for("host_room", room=missing_room_id))
+ self.assertEqual(response.status_code, 404)
+
+ def test_host_room_own(self) -> None:
+ """Verify that own room gives the full output."""
+ with open(self.log_filename, "w", encoding="utf-8-sig") as f:
+ text = "* should be visible *"
+ f.write(text)
+
+ with self.app.app_context(), self.app.test_request_context():
+ response = self.client.get(url_for("host_room", room=self.room_id))
+ response_text = response.get_data(True)
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("href=\"/seed/", response_text)
+ self.assertIn(text, response_text)
+
+ def test_host_room_other(self) -> None:
+ """Verify that non-own room gives the reduced output."""
+ from pony.orm import db_session
+ from WebHostLib.models import Room
+
+ with db_session:
+ room: Room = Room.get(id=self.room_id)
+ room.last_port = 12345
+
+ with open(self.log_filename, "w", encoding="utf-8-sig") as f:
+ text = "* should not be visible *"
+ f.write(text)
+
+ other_client = self.app.test_client()
+ with self.app.app_context(), self.app.test_request_context():
+ response = other_client.get(url_for("host_room", room=self.room_id))
+ response_text = response.get_data(True)
+ self.assertEqual(response.status_code, 200)
+ self.assertNotIn("href=\"/seed/", response_text)
+ self.assertNotIn(text, response_text)
+ self.assertIn("/connect ", response_text)
+ self.assertIn(":12345", response_text)
+
+ def test_host_room_own_post(self) -> None:
+ """Verify command from owner gets queued for the server and response is redirect."""
+ from pony.orm import db_session, select
+ from WebHostLib.models import Command
+
+ with self.app.app_context(), self.app.test_request_context():
+ response = self.client.post(url_for("host_room", room=self.room_id), data={
+ "cmd": "/help"
+ })
+ self.assertEqual(response.status_code, 302, response.text)\
+
+ with db_session:
+ commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore
+ self.assertIn("/help", (command.commandtext for command in commands))
+
+ def test_host_room_other_post(self) -> None:
+ """Verify command from non-owner does not get queued for the server."""
+ from pony.orm import db_session, select
+ from WebHostLib.models import Command
+
+ other_client = self.app.test_client()
+ with self.app.app_context(), self.app.test_request_context():
+ response = other_client.post(url_for("host_room", room=self.room_id), data={
+ "cmd": "/help"
+ })
+ self.assertLess(response.status_code, 500)
+
+ with db_session:
+ commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore
+ self.assertNotIn("/help", (command.commandtext for command in commands))
diff --git a/test/webhost/test_option_presets.py b/test/webhost/test_option_presets.py
new file mode 100644
index 000000000000..b0af8a871183
--- /dev/null
+++ b/test/webhost/test_option_presets.py
@@ -0,0 +1,63 @@
+import unittest
+
+from worlds import AutoWorldRegister
+from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet
+
+
+class TestOptionPresets(unittest.TestCase):
+ def test_option_presets_have_valid_options(self):
+ """Test that all predefined option presets are valid options."""
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ presets = world_type.web.options_presets
+ for preset_name, preset in presets.items():
+ for option_name, option_value in preset.items():
+ with self.subTest(game=game_name, preset=preset_name, option=option_name):
+ try:
+ option = world_type.options_dataclass.type_hints[option_name].from_any(option_value)
+ supported_types = [NumericOption, OptionSet, OptionList, ItemDict]
+ if not any([issubclass(option.__class__, t) for t in supported_types]):
+ self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "
+ f"is not a supported type for webhost. "
+ f"Supported types: {', '.join([t.__name__ for t in supported_types])}")
+ except AssertionError as ex:
+ self.fail(f"Option '{option_name}': '{option_value}' in preset '{preset_name}' for game "
+ f"'{game_name}' is not valid. Error: {ex}")
+ except KeyError as ex:
+ self.fail(f"Option '{option_name}' in preset '{preset_name}' for game '{game_name}' is "
+ f"not a defined option. Error: {ex}")
+
+ def test_option_preset_values_are_explicitly_defined(self):
+ """Test that option preset values are not a special flavor of 'random' or use from_text to resolve another
+ value.
+ """
+ for game_name, world_type in AutoWorldRegister.world_types.items():
+ presets = world_type.web.options_presets
+ for preset_name, preset in presets.items():
+ for option_name, option_value in preset.items():
+ with self.subTest(game=game_name, preset=preset_name, option=option_name):
+ # Check for non-standard random values.
+ self.assertFalse(
+ str(option_value).startswith("random-"),
+ f"'{option_name}': '{option_value}' in preset '{preset_name}' for game '{game_name}' "
+ f"is not supported for webhost. Special random values are not supported for presets."
+ )
+
+ option = world_type.options_dataclass.type_hints[option_name].from_any(option_value)
+
+ # Check for from_text resolving to a different value. ("random" is allowed though.)
+ if option_value != "random" and isinstance(option_value, str):
+ # Allow special named values for NamedRange option presets.
+ if isinstance(option, NamedRange):
+ self.assertTrue(
+ option_value in option.special_range_names,
+ f"Invalid preset '{option_name}': '{option_value}' in preset '{preset_name}' "
+ f"for game '{game_name}'. Expected {option.special_range_names.keys()} or "
+ f"{option.range_start}-{option.range_end}."
+ )
+ else:
+ self.assertTrue(
+ option.name_lookup.get(option.value, None) == option_value,
+ f"'{option_name}': '{option_value}' in preset '{preset_name}' for game "
+ f"'{game_name}' is not supported for webhost. Values must not be resolved to a "
+ f"different option via option.from_text (or an alias)."
+ )
diff --git a/test/worlds/__init__.py b/test/worlds/__init__.py
index d1817cc67489..cf396111bfd3 100644
--- a/test/worlds/__init__.py
+++ b/test/worlds/__init__.py
@@ -1,7 +1,7 @@
def load_tests(loader, standard_tests, pattern):
import os
import unittest
- from ..TestBase import file_path
+ from .. import file_path
from worlds.AutoWorld import AutoWorldRegister
suite = unittest.TestSuite()
diff --git a/typings/kivy/core/window.pyi b/typings/kivy/core/window.pyi
new file mode 100644
index 000000000000..c133b986d6c9
--- /dev/null
+++ b/typings/kivy/core/window.pyi
@@ -0,0 +1,15 @@
+from typing import Callable, ClassVar
+
+from kivy.event import EventDispatcher
+
+
+class WindowBase(EventDispatcher):
+ width: ClassVar[int] # readonly AliasProperty
+ height: ClassVar[int] # readonly AliasProperty
+
+ @staticmethod
+ def bind(**kwargs: Callable[..., None]) -> None: ...
+
+
+class Window(WindowBase):
+ ...
diff --git a/typings/kivy/event.pyi b/typings/kivy/event.pyi
new file mode 100644
index 000000000000..2e76adab0baf
--- /dev/null
+++ b/typings/kivy/event.pyi
@@ -0,0 +1,2 @@
+class EventDispatcher:
+ ...
diff --git a/typings/kivy/graphics.pyi b/typings/kivy/graphics.pyi
deleted file mode 100644
index 1950910661f4..000000000000
--- a/typings/kivy/graphics.pyi
+++ /dev/null
@@ -1,40 +0,0 @@
-""" FillType_* is not a real kivy type - just something to fill unknown typing. """
-
-from typing import Sequence
-
-FillType_Vec = Sequence[int]
-
-
-class FillType_Drawable:
- def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ...
-
-
-class FillType_Texture(FillType_Drawable):
- pass
-
-
-class FillType_Shape(FillType_Drawable):
- texture: FillType_Texture
-
- def __init__(self,
- *,
- texture: FillType_Texture = ...,
- pos: FillType_Vec = ...,
- size: FillType_Vec = ...) -> None: ...
-
-
-class Ellipse(FillType_Shape):
- pass
-
-
-class Color:
- def __init__(self, r: float, g: float, b: float, a: float) -> None: ...
-
-
-class Rectangle(FillType_Shape):
- def __init__(self,
- *,
- source: str = ...,
- texture: FillType_Texture = ...,
- pos: FillType_Vec = ...,
- size: FillType_Vec = ...) -> None: ...
diff --git a/typings/kivy/graphics/__init__.pyi b/typings/kivy/graphics/__init__.pyi
new file mode 100644
index 000000000000..a1a5bc02f68e
--- /dev/null
+++ b/typings/kivy/graphics/__init__.pyi
@@ -0,0 +1,28 @@
+from .texture import FillType_Drawable, FillType_Vec, Texture
+
+
+class FillType_Shape(FillType_Drawable):
+ texture: Texture
+
+ def __init__(self,
+ *,
+ texture: Texture = ...,
+ pos: FillType_Vec = ...,
+ size: FillType_Vec = ...) -> None: ...
+
+
+class Ellipse(FillType_Shape):
+ pass
+
+
+class Color:
+ def __init__(self, r: float, g: float, b: float, a: float) -> None: ...
+
+
+class Rectangle(FillType_Shape):
+ def __init__(self,
+ *,
+ source: str = ...,
+ texture: Texture = ...,
+ pos: FillType_Vec = ...,
+ size: FillType_Vec = ...) -> None: ...
diff --git a/typings/kivy/graphics/texture.pyi b/typings/kivy/graphics/texture.pyi
new file mode 100644
index 000000000000..ca643b1cada5
--- /dev/null
+++ b/typings/kivy/graphics/texture.pyi
@@ -0,0 +1,13 @@
+""" FillType_* is not a real kivy type - just something to fill unknown typing. """
+
+from typing import Sequence
+
+FillType_Vec = Sequence[int]
+
+
+class FillType_Drawable:
+ def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ...
+
+
+class Texture:
+ size: FillType_Vec
diff --git a/typings/kivy/uix/boxlayout.pyi b/typings/kivy/uix/boxlayout.pyi
new file mode 100644
index 000000000000..c63d691debdd
--- /dev/null
+++ b/typings/kivy/uix/boxlayout.pyi
@@ -0,0 +1,6 @@
+from typing import Literal
+from .layout import Layout
+
+
+class BoxLayout(Layout):
+ orientation: Literal['horizontal', 'vertical']
diff --git a/typings/kivy/uix/image.pyi b/typings/kivy/uix/image.pyi
new file mode 100644
index 000000000000..fa014baec7c2
--- /dev/null
+++ b/typings/kivy/uix/image.pyi
@@ -0,0 +1,9 @@
+import io
+
+from kivy.graphics.texture import Texture
+
+
+class CoreImage:
+ texture: Texture
+
+ def __init__(self, data: io.BytesIO, ext: str) -> None: ...
diff --git a/typings/kivy/uix/layout.pyi b/typings/kivy/uix/layout.pyi
index 2a418a1d8b50..c27f89086306 100644
--- a/typings/kivy/uix/layout.pyi
+++ b/typings/kivy/uix/layout.pyi
@@ -1,8 +1,14 @@
-from typing import Any
+from typing import Any, Sequence
+
from .widget import Widget
class Layout(Widget):
+ @property
+ def children(self) -> Sequence[Widget]: ...
+
def add_widget(self, widget: Widget) -> None: ...
+ def remove_widget(self, widget: Widget) -> None: ...
+
def do_layout(self, *largs: Any, **kwargs: Any) -> None: ...
diff --git a/typings/kivy/uix/widget.pyi b/typings/kivy/uix/widget.pyi
index 54e3b781ea01..bf736fae72fc 100644
--- a/typings/kivy/uix/widget.pyi
+++ b/typings/kivy/uix/widget.pyi
@@ -1,7 +1,7 @@
""" FillType_* is not a real kivy type - just something to fill unknown typing. """
from typing import Any, Optional, Protocol
-from ..graphics import FillType_Drawable, FillType_Vec
+from ..graphics.texture import FillType_Drawable, FillType_Vec
class FillType_BindCallback(Protocol):
diff --git a/typings/schema/__init__.pyi b/typings/schema/__init__.pyi
new file mode 100644
index 000000000000..d993ec22745f
--- /dev/null
+++ b/typings/schema/__init__.pyi
@@ -0,0 +1,17 @@
+from typing import Any, Callable
+
+
+class And:
+ def __init__(self, __type: type, __func: Callable[[Any], bool]) -> None: ...
+
+
+class Or:
+ def __init__(self, *args: object) -> None: ...
+
+
+class Schema:
+ def __init__(self, __x: object) -> None: ...
+
+
+class Optional(Schema):
+ ...
diff --git a/worlds/AutoSNIClient.py b/worlds/AutoSNIClient.py
index a30dbbb46d1f..2b984d9c8846 100644
--- a/worlds/AutoSNIClient.py
+++ b/worlds/AutoSNIClient.py
@@ -1,11 +1,35 @@
from __future__ import annotations
import abc
-from typing import TYPE_CHECKING, ClassVar, Dict, Tuple, Any, Optional
+from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union
+
+from typing_extensions import TypeGuard
+
+from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
if TYPE_CHECKING:
from SNIClient import SNIContext
+component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"))
+components.append(component)
+
+
+def valid_patch_suffix(obj: object) -> TypeGuard[Union[str, Iterable[str]]]:
+ """ make sure this is a valid value for the class variable `patch_suffix` """
+
+ def valid_individual(one: object) -> TypeGuard[str]:
+ """ check an individual suffix """
+ # TODO: decide: len(one) > 3 and one.startswith(".ap") ?
+ # or keep it more general?
+ return isinstance(one, str) and len(one) > 1 and one.startswith(".")
+
+ if isinstance(obj, str):
+ return valid_individual(obj)
+ if not isinstance(obj, Iterable):
+ return False
+ obj_it: Iterable[object] = obj
+ return all(valid_individual(each) for each in obj_it)
+
class AutoSNIClientRegister(abc.ABCMeta):
game_handlers: ClassVar[Dict[str, SNIClient]] = {}
@@ -15,6 +39,22 @@ def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut
new_class = super().__new__(cls, name, bases, dct)
if "game" in dct:
AutoSNIClientRegister.game_handlers[dct["game"]] = new_class()
+
+ if "patch_suffix" in dct:
+ patch_suffix = dct["patch_suffix"]
+ assert valid_patch_suffix(patch_suffix), f"class {name} defining invalid {patch_suffix=}"
+
+ existing_identifier = component.file_identifier
+ assert isinstance(existing_identifier, SuffixIdentifier), f"{existing_identifier=}"
+ new_suffixes = [*existing_identifier.suffixes]
+
+ if isinstance(patch_suffix, str):
+ new_suffixes.append(patch_suffix)
+ else:
+ new_suffixes.extend(patch_suffix)
+
+ component.file_identifier = SuffixIdentifier(*new_suffixes)
+
return new_class
@staticmethod
@@ -27,6 +67,9 @@ async def get_handler(ctx: SNIContext) -> Optional[SNIClient]:
class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister):
+ patch_suffix: ClassVar[Union[str, Iterable[str]]] = ()
+ """The file extension(s) this client is meant to open and patch (e.g. ".aplttp")"""
+
@abc.abstractmethod
async def validate_rom(self, ctx: SNIContext) -> bool:
""" TODO: interface documentation here """
diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py
index 217269aa9927..af067e5cb8a6 100644
--- a/worlds/AutoWorld.py
+++ b/worlds/AutoWorld.py
@@ -4,18 +4,22 @@
import logging
import pathlib
import sys
-from typing import Any, Callable, ClassVar, Dict, FrozenSet, List, Optional, Set, TYPE_CHECKING, TextIO, Tuple, Type, \
- Union
+import time
+from random import Random
+from dataclasses import make_dataclass
+from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple,
+ TYPE_CHECKING, Type, Union)
+from Options import item_and_loc_options, OptionGroup, PerGameCommonOptions
from BaseClasses import CollectionState
-from Options import AssembleOptions
if TYPE_CHECKING:
- import random
- from BaseClasses import MultiWorld, Item, Location, Tutorial
+ from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
from . import GamesPackage
from settings import Group
+perf_logger = logging.getLogger("performance")
+
class AutoWorldRegister(type):
world_types: Dict[str, Type[World]] = {}
@@ -47,6 +51,7 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut
dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
in dct.get("item_name_groups", {}).items()}
dct["item_name_groups"]["Everything"] = dct["item_names"]
+
dct["location_names"] = frozenset(dct["location_name_to_id"])
dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
in dct.get("location_name_groups", {}).items()}
@@ -63,6 +68,16 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut
dct["required_client_version"] = max(dct["required_client_version"],
base.__dict__["required_client_version"])
+ # create missing options_dataclass from legacy option_definitions
+ # TODO - remove this once all worlds use options dataclasses
+ if "options_dataclass" not in dct and "option_definitions" in dct:
+ # TODO - switch to deprecate after a version
+ if __debug__:
+ logging.warning(f"{name} Assigned options through option_definitions which is now deprecated. "
+ "Please use options_dataclass instead.")
+ dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
+ bases=(PerGameCommonOptions,))
+
# construct class
new_class = super().__new__(mcs, name, bases, dct)
if "game" in dct:
@@ -96,10 +111,57 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut
return new_class
+class WebWorldRegister(type):
+ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> WebWorldRegister:
+ # don't allow an option to appear in multiple groups, allow "Item & Location Options" to appear anywhere by the
+ # dev, putting it at the end if they don't define options in it
+ option_groups: List[OptionGroup] = dct.get("option_groups", [])
+ prebuilt_options = ["Game Options", "Item & Location Options"]
+ seen_options = []
+ item_group_in_list = False
+ for group in option_groups:
+ assert group.options, "A custom defined Option Group must contain at least one Option."
+ # catch incorrectly titled versions of the prebuilt groups so they don't create extra groups
+ title_name = group.name.title()
+ assert title_name not in prebuilt_options or title_name == group.name, \
+ f"Prebuilt group name \"{group.name}\" must be \"{title_name}\""
+
+ if group.name == "Item & Location Options":
+ assert not any(option in item_and_loc_options for option in group.options), \
+ f"Item and Location Options cannot be specified multiple times"
+ group.options.extend(item_and_loc_options)
+ item_group_in_list = True
+ else:
+ for option in group.options:
+ assert option not in item_and_loc_options, \
+ f"{option} cannot be moved out of the \"Item & Location Options\" Group"
+ assert len(group.options) == len(set(group.options)), f"Duplicate options in option group {group.name}"
+ for option in group.options:
+ assert option not in seen_options, f"{option} found in two option groups"
+ seen_options.append(option)
+ if not item_group_in_list:
+ option_groups.append(OptionGroup("Item & Location Options", item_and_loc_options, True))
+ return super().__new__(mcs, name, bases, dct)
+
+
+def _timed_call(method: Callable[..., Any], *args: Any,
+ multiworld: Optional["MultiWorld"] = None, player: Optional[int] = None) -> Any:
+ start = time.perf_counter()
+ ret = method(*args)
+ taken = time.perf_counter() - start
+ if taken > 1.0:
+ if player and multiworld:
+ perf_logger.info(f"Took {taken:.4f} seconds in {method.__qualname__} for player {player}, "
+ f"named {multiworld.player_name[player]}.")
+ else:
+ perf_logger.info(f"Took {taken:.4f} seconds in {method.__qualname__}.")
+ return ret
+
+
def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
method = getattr(multiworld.worlds[player], method_name)
try:
- ret = method(*args)
+ ret = _timed_call(method, *args, multiworld=multiworld, player=player)
except Exception as e:
message = f"Exception in {method} for player {player}, named {multiworld.player_name[player]}."
if sys.version_info >= (3, 11, 0):
@@ -125,24 +187,21 @@ def call_all(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
f"Duplicate item reference of \"{item.name}\" in \"{multiworld.worlds[player].game}\" "
f"of player \"{multiworld.player_name[player]}\". Please make a copy instead.")
- for world_type in sorted(world_types, key=lambda world: world.__name__):
- stage_callable = getattr(world_type, f"stage_{method_name}", None)
- if stage_callable:
- stage_callable(multiworld, *args)
+ call_stage(multiworld, method_name, *args)
def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
world_types = {multiworld.worlds[player].__class__ for player in multiworld.player_ids}
- for world_type in world_types:
+ for world_type in sorted(world_types, key=lambda world: world.__name__):
stage_callable = getattr(world_type, f"stage_{method_name}", None)
if stage_callable:
- stage_callable(multiworld, *args)
+ _timed_call(stage_callable, multiworld, *args)
-class WebWorld:
+class WebWorld(metaclass=WebWorldRegister):
"""Webhost integration"""
- settings_page: Union[bool, str] = True
+ options_page: Union[bool, str] = True
"""display a settings page. Can be a link to a specific page or external tool."""
game_info_languages: List[str] = ['en']
@@ -158,17 +217,47 @@ class WebWorld:
bug_report_page: Optional[str]
"""display a link to a bug report page, most likely a link to a GitHub issue page."""
+ options_presets: Dict[str, Dict[str, Any]] = {}
+ """A dictionary containing a collection of developer-defined game option presets."""
+
+ option_groups: ClassVar[List[OptionGroup]] = []
+ """Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options"."""
+
+ rich_text_options_doc = False
+ """Whether the WebHost should render Options' docstrings as rich text.
+
+ If this is True, Options' docstrings are interpreted as reStructuredText_,
+ the standard Python markup format. In the WebHost, they're rendered to HTML
+ so that lists, emphasis, and other rich text features are displayed
+ properly.
+
+ If this is False, the docstrings are instead interpreted as plain text, and
+ displayed as-is on the WebHost with whitespace preserved. For backwards
+ compatibility, this is the default.
+
+ .. _reStructuredText: https://docutils.sourceforge.io/rst.html
+ """
+
+ location_descriptions: Dict[str, str] = {}
+ """An optional map from location names (or location group names) to brief descriptions for users."""
+
+ item_descriptions: Dict[str, str] = {}
+ """An optional map from item names (or item group names) to brief descriptions for users."""
+
class World(metaclass=AutoWorldRegister):
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
A Game should have its own subclass of World in which it defines the required data structures."""
- option_definitions: ClassVar[Dict[str, AssembleOptions]] = {}
+ options_dataclass: ClassVar[Type[PerGameCommonOptions]] = PerGameCommonOptions
"""link your Options mapping"""
+ options: PerGameCommonOptions
+ """resulting options for the player of this world"""
+
game: ClassVar[str]
"""name the game"""
- topology_present: ClassVar[bool] = False
- """indicate if world type has any meaningful layout/pathing"""
+ topology_present: bool = False
+ """indicate if this world has any meaningful layout/pathing"""
all_item_and_group_names: ClassVar[FrozenSet[str]] = frozenset()
"""gets automatically populated with all item and item group names"""
@@ -184,18 +273,6 @@ class World(metaclass=AutoWorldRegister):
location_name_groups: ClassVar[Dict[str, Set[str]]] = {}
"""maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}"""
- data_version: ClassVar[int] = 0
- """
- Increment this every time something in your world's names/id mappings changes.
-
- When this is set to 0, that world's DataPackage is considered in "testing mode", which signals to servers/clients
- that it should not be cached, and clients should request that world's DataPackage every connection. Not
- recommended for production-ready worlds.
-
- Deprecated. Clients should utilize `checksum` to determine if DataPackage has changed since last connection and
- request a new DataPackage, if necessary.
- """
-
required_client_version: Tuple[int, int, int] = (0, 1, 6)
"""
override this if changes to a world break forward-compatibility of the client
@@ -203,7 +280,7 @@ class World(metaclass=AutoWorldRegister):
future. Protocol level compatibility check moved to MultiServer.min_client_version.
"""
- required_server_version: Tuple[int, int, int] = (0, 2, 4)
+ required_server_version: Tuple[int, int, int] = (0, 5, 0)
"""update this if the resulting multidata breaks forward-compatibility of the server"""
hint_blacklist: ClassVar[FrozenSet[str]] = frozenset()
@@ -230,7 +307,7 @@ class World(metaclass=AutoWorldRegister):
location_names: ClassVar[Set[str]]
"""set of all potential location names"""
- random: random.Random
+ random: Random
"""This world's random object. Should be used for any randomization needed in world for this player slot."""
settings_key: ClassVar[str]
@@ -244,8 +321,11 @@ class World(metaclass=AutoWorldRegister):
"""path it was loaded from"""
def __init__(self, multiworld: "MultiWorld", player: int):
+ assert multiworld is not None
self.multiworld = multiworld
self.player = player
+ self.random = Random(multiworld.random.getrandbits(64))
+ multiworld.per_slot_randoms[player] = self.random
def __getattr__(self, item: str) -> Any:
if item == "settings":
@@ -254,13 +334,15 @@ def __getattr__(self, item: str) -> Any:
# overridable methods that get called by Main.py, sorted by execution order
# can also be implemented as a classmethod and called "stage_",
- # in that case the MultiWorld object is passed as an argument and it gets called once for the entire multiworld.
+ # in that case the MultiWorld object is passed as an argument, and it gets called once for the entire multiworld.
# An example of this can be found in alttp as stage_pre_fill
@classmethod
def stage_assert_generate(cls, multiworld: "MultiWorld") -> None:
- """Checks that a game is capable of generating, usually checks for some base file like a ROM.
- This gets called once per present world type. Not run for unittests since they don't produce output"""
+ """
+ Checks that a game is capable of generating, such as checking for some base file like a ROM.
+ This gets called once per present world type. Not run for unittests since they don't produce output.
+ """
pass
def generate_early(self) -> None:
@@ -276,7 +358,7 @@ def create_regions(self) -> None:
def create_items(self) -> None:
"""
- Method for creating and submitting items to the itempool. Items and Regions should *not* be created and submitted
+ Method for creating and submitting items to the itempool. Items and Regions must *not* be created and submitted
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_prefill_items`.
"""
pass
@@ -305,26 +387,40 @@ def fill_hook(self,
pass
def post_fill(self) -> None:
- """Optional Method that is called after regular fill. Can be used to do adjustments before output generation.
- This happens before progression balancing, so the items may not be in their final locations yet."""
+ """
+ Optional Method that is called after regular fill. Can be used to do adjustments before output generation.
+ This happens before progression balancing, so the items may not be in their final locations yet.
+ """
def generate_output(self, output_directory: str) -> None:
- """This method gets called from a threadpool, do not use multiworld.random here.
- If you need any last-second randomization, use self.random instead."""
+ """
+ This method gets called from a threadpool, do not use multiworld.random here.
+ If you need any last-second randomization, use self.random instead.
+ """
pass
- def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot
- """Fill in the `slot_data` field in the `Connected` network package.
+ def fill_slot_data(self) -> Mapping[str, Any]: # json of WebHostLib.models.Slot
+ """
+ What is returned from this function will be in the `slot_data` field
+ in the `Connected` network package.
+ It should be a `dict` with `str` keys, and should be serializable with json.
+
This is a way the generator can give custom data to the client.
The client will receive this as JSON in the `Connected` response.
The generation does not wait for `generate_output` to complete before calling this.
- `threading.Event` can be used if you need to wait for something from `generate_output`."""
+ `threading.Event` can be used if you need to wait for something from `generate_output`.
+ """
+ # The reason for the `Mapping` type annotation, rather than `dict`
+ # is so that type checkers won't worry about the mutability of `dict`,
+ # so you can have more specific typing in your world implementation.
return {}
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
- """Fill in additional entrance information text into locations, which is displayed when hinted.
- structure is {player_id: {location_id: text}} You will need to insert your own player_id."""
+ """
+ Fill in additional entrance information text into locations, which is displayed when hinted.
+ structure is {player_id: {location_id: text}} You will need to insert your own player_id.
+ """
pass
def modify_multidata(self, multidata: Dict[str, Any]) -> None: # TODO: TypedDict for multidata?
@@ -333,13 +429,17 @@ def modify_multidata(self, multidata: Dict[str, Any]) -> None: # TODO: TypedDic
# Spoiler writing is optional, these may not get called.
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
- """Write to the spoiler header. If individual it's right at the end of that player's options,
- if as stage it's right under the common header before per-player options."""
+ """
+ Write to the spoiler header. If individual it's right at the end of that player's options,
+ if as stage it's right under the common header before per-player options.
+ """
pass
def write_spoiler(self, spoiler_handle: TextIO) -> None:
- """Write to the spoiler "middle", this is after the per-player options and before locations,
- meant for useful or interesting info."""
+ """
+ Write to the spoiler "middle", this is after the per-player options and before locations,
+ meant for useful or interesting info.
+ """
pass
def write_spoiler_end(self, spoiler_handle: TextIO) -> None:
@@ -349,8 +449,10 @@ def write_spoiler_end(self, spoiler_handle: TextIO) -> None:
# end of ordered Main.py calls
def create_item(self, name: str) -> "Item":
- """Create an item for this world type and player.
- Warning: this may be called with self.world = None, for example by MultiServer"""
+ """
+ Create an item for this world type and player.
+ Warning: this may be called with self.world = None, for example by MultiServer
+ """
raise NotImplementedError
def get_filler_item_name(self) -> str:
@@ -358,41 +460,78 @@ def get_filler_item_name(self) -> str:
logging.warning(f"World {self} is generating a filler item without custom filler pool.")
return self.multiworld.random.choice(tuple(self.item_name_to_id.keys()))
+ @classmethod
+ def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World:
+ """
+ Creates a group, which is an instance of World that is responsible for multiple others.
+ An example case is ItemLinks creating these.
+ """
+ # TODO remove loop when worlds use options dataclass
+ for option_key, option in cls.options_dataclass.type_hints.items():
+ getattr(multiworld, option_key)[new_player_id] = option.from_any(option.default)
+ group = cls(multiworld, new_player_id)
+ group.options = cls.options_dataclass(**{option_key: option.from_any(option.default)
+ for option_key, option in cls.options_dataclass.type_hints.items()})
+
+ return group
+
# decent place to implement progressive items, in most cases can stay as-is
def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]:
- """Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
+ """
+ Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
Collect None to skip item.
:param state: CollectionState to collect into
:param item: Item to decide on if it should be collected into state
- :param remove: indicate if this is meant to remove from state instead of adding."""
+ :param remove: indicate if this is meant to remove from state instead of adding.
+ """
if item.advancement:
return item.name
return None
- # called to create all_state, return Items that are created during pre_fill
def get_pre_fill_items(self) -> List["Item"]:
+ """
+ Used to return items that need to be collected when creating a fresh all_state, but don't exist in the
+ multiworld itempool.
+ """
return []
- # following methods should not need to be overridden.
+ # these two methods can be extended for pseudo-items on state
def collect(self, state: "CollectionState", item: "Item") -> bool:
+ """Called when an item is collected in to state. Useful for things such as progressive items or currency."""
name = self.collect_item(state, item)
if name:
- state.prog_items[name, self.player] += 1
+ state.prog_items[self.player][name] += 1
return True
return False
def remove(self, state: "CollectionState", item: "Item") -> bool:
+ """Called when an item is removed from to state. Useful for things such as progressive items or currency."""
name = self.collect_item(state, item, True)
if name:
- state.prog_items[name, self.player] -= 1
- if state.prog_items[name, self.player] < 1:
- del (state.prog_items[name, self.player])
+ state.prog_items[self.player][name] -= 1
+ if state.prog_items[self.player][name] < 1:
+ del (state.prog_items[self.player][name])
return True
return False
+ # following methods should not need to be overridden.
def create_filler(self) -> "Item":
return self.create_item(self.get_filler_item_name())
+ # convenience methods
+ def get_location(self, location_name: str) -> "Location":
+ return self.multiworld.get_location(location_name, self.player)
+
+ def get_entrance(self, entrance_name: str) -> "Entrance":
+ return self.multiworld.get_entrance(entrance_name, self.player)
+
+ def get_region(self, region_name: str) -> "Region":
+ return self.multiworld.get_region(region_name, self.player)
+
+ @property
+ def player_name(self) -> str:
+ return self.multiworld.get_player_name(self.player)
+
@classmethod
def get_data_package_data(cls) -> "GamesPackage":
sorted_item_name_groups = {
@@ -407,7 +546,6 @@ def get_data_package_data(cls) -> "GamesPackage":
"item_name_to_id": cls.item_name_to_id,
"location_name_groups": sorted_location_name_groups,
"location_name_to_id": cls.location_name_to_id,
- "version": cls.data_version,
}
res["checksum"] = data_package_checksum(res)
return res
diff --git a/worlds/Files.py b/worlds/Files.py
index ac1acbf32248..69a88218efd4 100644
--- a/worlds/Files.py
+++ b/worlds/Files.py
@@ -1,14 +1,23 @@
from __future__ import annotations
+import abc
import json
import zipfile
+from enum import IntEnum
+import os
+import threading
-from typing import ClassVar, Dict, Tuple, Any, Optional, Union, BinaryIO
+from typing import ClassVar, Dict, List, Literal, Tuple, Any, Optional, Union, BinaryIO, overload, Sequence
import bsdiff4
+semaphore = threading.Semaphore(os.cpu_count() or 4)
-class AutoPatchRegister(type):
+del threading
+del os
+
+
+class AutoPatchRegister(abc.ABCMeta):
patch_types: ClassVar[Dict[str, AutoPatchRegister]] = {}
file_endings: ClassVar[Dict[str, AutoPatchRegister]] = {}
@@ -30,12 +39,47 @@ def get_handler(file: str) -> Optional[AutoPatchRegister]:
return None
-current_patch_version: int = 5
+class AutoPatchExtensionRegister(abc.ABCMeta):
+ extension_types: ClassVar[Dict[str, AutoPatchExtensionRegister]] = {}
+ required_extensions: Tuple[str, ...] = ()
+
+ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoPatchExtensionRegister:
+ # construct class
+ new_class = super().__new__(mcs, name, bases, dct)
+ if "game" in dct:
+ AutoPatchExtensionRegister.extension_types[dct["game"]] = new_class
+ return new_class
+
+ @staticmethod
+ def get_handler(game: Optional[str]) -> Union[AutoPatchExtensionRegister, List[AutoPatchExtensionRegister]]:
+ if not game:
+ return APPatchExtension
+ handler = AutoPatchExtensionRegister.extension_types.get(game, APPatchExtension)
+ if handler.required_extensions:
+ handlers = [handler]
+ for required in handler.required_extensions:
+ ext = AutoPatchExtensionRegister.extension_types.get(required)
+ if not ext:
+ raise NotImplementedError(f"No handler for {required}.")
+ handlers.append(ext)
+ return handlers
+ else:
+ return handler
+
+
+container_version: int = 6
+
+
+class InvalidDataError(Exception):
+ """
+ Since games can override `read_contents` in APContainer,
+ this is to report problems in that process.
+ """
class APContainer:
"""A zipfile containing at least archipelago.json"""
- version: int = current_patch_version
+ version: int = container_version
compression_level: int = 9
compression_method: int = zipfile.ZIP_DEFLATED
game: Optional[str] = None
@@ -57,11 +101,12 @@ def write(self, file: Optional[Union[str, BinaryIO]] = None) -> None:
zip_file = file if file else self.path
if not zip_file:
raise FileNotFoundError(f"Cannot write {self.__class__.__name__} due to no path provided.")
- with zipfile.ZipFile(zip_file, "w", self.compression_method, True, self.compression_level) \
- as zf:
- if file:
- self.path = zf.filename
- self.write_contents(zf)
+ with semaphore: # TODO: remove semaphore once generate_output has a thread limit
+ with zipfile.ZipFile(
+ zip_file, "w", self.compression_method, True, self.compression_level) as zf:
+ if file:
+ self.path = zf.filename
+ self.write_contents(zf)
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
manifest = self.get_manifest()
@@ -80,7 +125,15 @@ def read(self, file: Optional[Union[str, BinaryIO]] = None) -> None:
with zipfile.ZipFile(zip_file, "r") as zf:
if file:
self.path = zf.filename
- self.read_contents(zf)
+ try:
+ self.read_contents(zf)
+ except Exception as e:
+ message = ""
+ if len(e.args):
+ arg0 = e.args[0]
+ if isinstance(arg0, str):
+ message = f"{arg0} - "
+ raise InvalidDataError(f"{message}This might be the incorrect world version for this file") from e
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
with opened_zipfile.open("archipelago.json", "r") as f:
@@ -100,31 +153,48 @@ def get_manifest(self) -> Dict[str, Any]:
"game": self.game,
# minimum version of patch system expected for patching to be successful
"compatible_version": 5,
- "version": current_patch_version,
+ "version": container_version,
}
-class APDeltaPatch(APContainer, metaclass=AutoPatchRegister):
- """An APContainer that additionally has delta.bsdiff4
- containing a delta patch to get the desired file, often a rom."""
+class APPatch(APContainer):
+ """
+ An `APContainer` that represents a patch file.
+ It includes the `procedure` key in the manifest to indicate that it is a patch.
- hash: Optional[str] # base checksum of source file
- patch_file_ending: str = ""
- delta: Optional[bytes] = None
- result_file_ending: str = ".sfc"
- source_data: bytes
-
- def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None:
- self.patched_path = patched_path
- super(APDeltaPatch, self).__init__(*args, **kwargs)
+ Your implementation should inherit from this if your output file
+ represents a patch file, but will not be applied with AP's `Patch.py`
+ """
+ procedure: Union[Literal["custom"], List[Tuple[str, List[Any]]]] = "custom"
def get_manifest(self) -> Dict[str, Any]:
- manifest = super(APDeltaPatch, self).get_manifest()
- manifest["base_checksum"] = self.hash
- manifest["result_file_ending"] = self.result_file_ending
- manifest["patch_file_ending"] = self.patch_file_ending
+ manifest = super(APPatch, self).get_manifest()
+ manifest["procedure"] = self.procedure
+ manifest["compatible_version"] = 6
return manifest
+
+class APAutoPatchInterface(APPatch, abc.ABC, metaclass=AutoPatchRegister):
+ """
+ An abstract `APPatch` that defines the requirements for a patch
+ to be applied with AP's `Patch.py`
+ """
+ result_file_ending: str = ".sfc"
+
+ @abc.abstractmethod
+ def patch(self, target: str) -> None:
+ """ create the output file with the file name `target` """
+
+
+class APProcedurePatch(APAutoPatchInterface):
+ """
+ An APPatch that defines a procedure to produce the desired file.
+ """
+ hash: Optional[str] # base checksum of source file
+ source_data: bytes
+ patch_file_ending: str = ""
+ files: Dict[str, bytes]
+
@classmethod
def get_source_data(cls) -> bytes:
"""Get Base data"""
@@ -136,21 +206,223 @@ def get_source_data_with_cache(cls) -> bytes:
cls.source_data = cls.get_source_data()
return cls.source_data
- def write_contents(self, opened_zipfile: zipfile.ZipFile):
- super(APDeltaPatch, self).write_contents(opened_zipfile)
- # write Delta
- opened_zipfile.writestr("delta.bsdiff4",
- bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()),
- compress_type=zipfile.ZIP_STORED) # bsdiff4 is a format with integrated compression
-
- def read_contents(self, opened_zipfile: zipfile.ZipFile):
- super(APDeltaPatch, self).read_contents(opened_zipfile)
- self.delta = opened_zipfile.read("delta.bsdiff4")
-
- def patch(self, target: str):
- """Base + Delta -> Patched"""
- if not self.delta:
+ def __init__(self, *args: Any, **kwargs: Any):
+ super(APProcedurePatch, self).__init__(*args, **kwargs)
+ self.files = {}
+
+ def get_manifest(self) -> Dict[str, Any]:
+ manifest = super(APProcedurePatch, self).get_manifest()
+ manifest["base_checksum"] = self.hash
+ manifest["result_file_ending"] = self.result_file_ending
+ manifest["patch_file_ending"] = self.patch_file_ending
+ manifest["procedure"] = self.procedure
+ if self.procedure == APDeltaPatch.procedure:
+ manifest["compatible_version"] = 5
+ return manifest
+
+ def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
+ super(APProcedurePatch, self).read_contents(opened_zipfile)
+ with opened_zipfile.open("archipelago.json", "r") as f:
+ manifest = json.load(f)
+ if "procedure" not in manifest:
+ # support patching files made before moving to procedures
+ self.procedure = [("apply_bsdiff4", ["delta.bsdiff4"])]
+ else:
+ self.procedure = manifest["procedure"]
+ for file in opened_zipfile.namelist():
+ if file not in ["archipelago.json"]:
+ self.files[file] = opened_zipfile.read(file)
+
+ def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
+ super(APProcedurePatch, self).write_contents(opened_zipfile)
+ for file in self.files:
+ opened_zipfile.writestr(file, self.files[file],
+ compress_type=zipfile.ZIP_STORED if file.endswith(".bsdiff4") else None)
+
+ def get_file(self, file: str) -> bytes:
+ """ Retrieves a file from the patch container."""
+ if file not in self.files:
self.read()
- result = bsdiff4.patch(self.get_source_data_with_cache(), self.delta)
- with open(target, "wb") as f:
- f.write(result)
+ return self.files[file]
+
+ def write_file(self, file_name: str, file: bytes) -> None:
+ """ Writes a file to the patch container, to be retrieved upon patching. """
+ self.files[file_name] = file
+
+ def patch(self, target: str) -> None:
+ self.read()
+ base_data = self.get_source_data_with_cache()
+ patch_extender = AutoPatchExtensionRegister.get_handler(self.game)
+ assert not isinstance(self.procedure, str), f"{type(self)} must define procedures"
+ for step, args in self.procedure:
+ if isinstance(patch_extender, list):
+ extension = next((item for item in [getattr(extender, step, None) for extender in patch_extender]
+ if item is not None), None)
+ else:
+ extension = getattr(patch_extender, step, None)
+ if extension is not None:
+ base_data = extension(self, base_data, *args)
+ else:
+ raise NotImplementedError(f"Unknown procedure {step} for {self.game}.")
+ with open(target, 'wb') as f:
+ f.write(base_data)
+
+
+class APDeltaPatch(APProcedurePatch):
+ """An APProcedurePatch that additionally has delta.bsdiff4
+ containing a delta patch to get the desired file, often a rom."""
+
+ procedure = [
+ ("apply_bsdiff4", ["delta.bsdiff4"])
+ ]
+
+ def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None:
+ super(APDeltaPatch, self).__init__(*args, **kwargs)
+ self.patched_path = patched_path
+
+ def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
+ self.write_file("delta.bsdiff4",
+ bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()))
+ super(APDeltaPatch, self).write_contents(opened_zipfile)
+
+
+class APTokenTypes(IntEnum):
+ WRITE = 0
+ COPY = 1
+ RLE = 2
+ AND_8 = 3
+ OR_8 = 4
+ XOR_8 = 5
+
+
+class APTokenMixin:
+ """
+ A class that defines functions for generating a token binary, for use in patches.
+ """
+ _tokens: Sequence[
+ Tuple[APTokenTypes, int, Union[
+ bytes, # WRITE
+ Tuple[int, int], # COPY, RLE
+ int # AND_8, OR_8, XOR_8
+ ]]] = ()
+
+ def get_token_binary(self) -> bytes:
+ """
+ Returns the token binary created from stored tokens.
+ :return: A bytes object representing the token data.
+ """
+ data = bytearray()
+ data.extend(len(self._tokens).to_bytes(4, "little"))
+ for token_type, offset, args in self._tokens:
+ data.append(token_type)
+ data.extend(offset.to_bytes(4, "little"))
+ if token_type in [APTokenTypes.AND_8, APTokenTypes.OR_8, APTokenTypes.XOR_8]:
+ assert isinstance(args, int), f"Arguments to AND/OR/XOR must be of type int, not {type(args)}"
+ data.extend(int.to_bytes(1, 4, "little"))
+ data.append(args)
+ elif token_type in [APTokenTypes.COPY, APTokenTypes.RLE]:
+ assert isinstance(args, tuple), f"Arguments to COPY/RLE must be of type tuple, not {type(args)}"
+ data.extend(int.to_bytes(8, 4, "little"))
+ data.extend(args[0].to_bytes(4, "little"))
+ data.extend(args[1].to_bytes(4, "little"))
+ elif token_type == APTokenTypes.WRITE:
+ assert isinstance(args, bytes), f"Arguments to WRITE must be of type bytes, not {type(args)}"
+ data.extend(len(args).to_bytes(4, "little"))
+ data.extend(args)
+ else:
+ raise ValueError(f"Unknown token type {token_type}")
+ return bytes(data)
+
+ @overload
+ def write_token(self,
+ token_type: Literal[APTokenTypes.AND_8, APTokenTypes.OR_8, APTokenTypes.XOR_8],
+ offset: int,
+ data: int) -> None:
+ ...
+
+ @overload
+ def write_token(self,
+ token_type: Literal[APTokenTypes.COPY, APTokenTypes.RLE],
+ offset: int,
+ data: Tuple[int, int]) -> None:
+ ...
+
+ @overload
+ def write_token(self,
+ token_type: Literal[APTokenTypes.WRITE],
+ offset: int,
+ data: bytes) -> None:
+ ...
+
+ def write_token(self, token_type: APTokenTypes, offset: int, data: Union[bytes, Tuple[int, int], int]) -> None:
+ """
+ Stores a token to be used by patching.
+ """
+ if not isinstance(self._tokens, list):
+ assert len(self._tokens) == 0, f"{type(self)}._tokens was tampered with."
+ self._tokens = []
+ self._tokens.append((token_type, offset, data))
+
+
+class APPatchExtension(metaclass=AutoPatchExtensionRegister):
+ """Class that defines patch extension functions for a given game.
+ Patch extension functions must have the following two arguments in the following order:
+
+ caller: APProcedurePatch (used to retrieve files from the patch container)
+
+ rom: bytes (the data to patch)
+
+ Further arguments are passed in from the procedure as defined.
+
+ Patch extension functions must return the changed bytes.
+ """
+ game: str
+ required_extensions: ClassVar[Tuple[str, ...]] = ()
+
+ @staticmethod
+ def apply_bsdiff4(caller: APProcedurePatch, rom: bytes, patch: str) -> bytes:
+ """Applies the given bsdiff4 from the patch onto the current file."""
+ return bsdiff4.patch(rom, caller.get_file(patch))
+
+ @staticmethod
+ def apply_tokens(caller: APProcedurePatch, rom: bytes, token_file: str) -> bytes:
+ """Applies the given token file from the patch onto the current file."""
+ token_data = caller.get_file(token_file)
+ rom_data = bytearray(rom)
+ token_count = int.from_bytes(token_data[0:4], "little")
+ bpr = 4
+ for _ in range(token_count):
+ token_type = token_data[bpr:bpr + 1][0]
+ offset = int.from_bytes(token_data[bpr + 1:bpr + 5], "little")
+ size = int.from_bytes(token_data[bpr + 5:bpr + 9], "little")
+ data = token_data[bpr + 9:bpr + 9 + size]
+ if token_type in [APTokenTypes.AND_8, APTokenTypes.OR_8, APTokenTypes.XOR_8]:
+ arg = data[0]
+ if token_type == APTokenTypes.AND_8:
+ rom_data[offset] = rom_data[offset] & arg
+ elif token_type == APTokenTypes.OR_8:
+ rom_data[offset] = rom_data[offset] | arg
+ else:
+ rom_data[offset] = rom_data[offset] ^ arg
+ elif token_type in [APTokenTypes.COPY, APTokenTypes.RLE]:
+ length = int.from_bytes(data[:4], "little")
+ value = int.from_bytes(data[4:], "little")
+ if token_type == APTokenTypes.COPY:
+ rom_data[offset: offset + length] = rom_data[value: value + length]
+ else:
+ rom_data[offset: offset + length] = bytes([value] * length)
+ else:
+ rom_data[offset:offset + len(data)] = data
+ bpr += 9 + size
+ return bytes(rom_data)
+
+ @staticmethod
+ def calc_snes_crc(caller: APProcedurePatch, rom: bytes) -> bytes:
+ """Calculates and applies a valid CRC for the SNES rom header."""
+ rom_data = bytearray(rom)
+ if len(rom) < 0x8000:
+ raise Exception("Tried to calculate SNES CRC on file too small to be a SNES ROM.")
+ crc = (sum(rom_data[:0x7FDC] + rom_data[0x7FE0:]) + 0x01FE) & 0xFFFF
+ inv = crc ^ 0xFFFF
+ rom_data[0x7FDC:0x7FE0] = [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF]
+ return bytes(rom_data)
diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py
index c3ae2b0495b0..18c1a1661ef0 100644
--- a/worlds/LauncherComponents.py
+++ b/worlds/LauncherComponents.py
@@ -1,8 +1,11 @@
+import bisect
+import logging
+import pathlib
import weakref
from enum import Enum, auto
-from typing import Optional, Callable, List, Iterable
+from typing import Optional, Callable, List, Iterable, Tuple
-from Utils import local_path
+from Utils import local_path, open_filename
class Type(Enum):
@@ -49,8 +52,10 @@ def handles_file(self, path: str):
def __repr__(self):
return f"{self.__class__.__name__}({self.display_name})"
+
processes = weakref.WeakSet()
+
def launch_subprocess(func: Callable, name: str = None):
global processes
import multiprocessing
@@ -58,13 +63,14 @@ def launch_subprocess(func: Callable, name: str = None):
process.start()
processes.add(process)
+
class SuffixIdentifier:
suffixes: Iterable[str]
def __init__(self, *args: str):
self.suffixes = args
- def __call__(self, path: str):
+ def __call__(self, path: str) -> bool:
if isinstance(path, str):
for suffix in self.suffixes:
if path.endswith(suffix):
@@ -77,6 +83,80 @@ def launch_textclient():
launch_subprocess(CommonClient.run_as_textclient, name="TextClient")
+def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
+ if not apworld_src:
+ apworld_src = open_filename('Select APWorld file to install', (('APWorld', ('.apworld',)),))
+ if not apworld_src:
+ # user closed menu
+ return
+
+ if not apworld_src.endswith(".apworld"):
+ raise Exception(f"Wrong file format, looking for .apworld. File identified: {apworld_src}")
+
+ apworld_path = pathlib.Path(apworld_src)
+
+ module_name = pathlib.Path(apworld_path.name).stem
+ try:
+ import zipfile
+ zipfile.ZipFile(apworld_path).open(module_name + "/__init__.py")
+ except ValueError as e:
+ raise Exception("Archive appears invalid or damaged.") from e
+ except KeyError as e:
+ raise Exception("Archive appears to not be an apworld. (missing __init__.py)") from e
+
+ import worlds
+ if worlds.user_folder is None:
+ raise Exception("Custom Worlds directory appears to not be writable.")
+ for world_source in worlds.world_sources:
+ if apworld_path.samefile(world_source.resolved_path):
+ # Note that this doesn't check if the same world is already installed.
+ # It only checks if the user is trying to install the apworld file
+ # that comes from the installation location (worlds or custom_worlds)
+ raise Exception(f"APWorld is already installed at {world_source.resolved_path}.")
+
+ # TODO: run generic test suite over the apworld.
+ # TODO: have some kind of version system to tell from metadata if the apworld should be compatible.
+
+ target = pathlib.Path(worlds.user_folder) / apworld_path.name
+ import shutil
+ shutil.copyfile(apworld_path, target)
+
+ # If a module with this name is already loaded, then we can't load it now.
+ # TODO: We need to be able to unload a world module,
+ # so the user can update a world without restarting the application.
+ found_already_loaded = False
+ for loaded_world in worlds.world_sources:
+ loaded_name = pathlib.Path(loaded_world.path).stem
+ if module_name == loaded_name:
+ found_already_loaded = True
+ break
+ if found_already_loaded:
+ raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded,\n"
+ "so a Launcher restart is required to use the new installation.")
+ world_source = worlds.WorldSource(str(target), is_zip=True)
+ bisect.insort(worlds.world_sources, world_source)
+ world_source.load()
+
+ return apworld_path, target
+
+
+def install_apworld(apworld_path: str = "") -> None:
+ try:
+ res = _install_apworld(apworld_path)
+ if res is None:
+ logging.info("Aborting APWorld installation.")
+ return
+ source, target = res
+ except Exception as e:
+ import Utils
+ Utils.messagebox(e.__class__.__name__, str(e), error=True)
+ logging.exception(e)
+ else:
+ import Utils
+ logging.info(f"Installed APWorld successfully, copied {source} to {target}.")
+ Utils.messagebox("Install complete.", f"Installed APWorld from {source}.")
+
+
components: List[Component] = [
# Launcher
Component('Launcher', 'Launcher', component_type=Type.HIDDEN),
@@ -84,11 +164,8 @@ def launch_textclient():
Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
file_identifier=SuffixIdentifier('.archipelago', '.zip')),
Component('Generate', 'Generate', cli=True),
+ Component("Install APWorld", func=install_apworld, file_identifier=SuffixIdentifier(".apworld")),
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient),
- # SNI
- Component('SNI Client', 'SNIClient',
- file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3',
- '.apsmw', '.apl2ac')),
Component('Links Awakening DX Client', 'LinksAwakeningClient',
file_identifier=SuffixIdentifier('.apladx')),
Component('LttP Adjuster', 'LttPAdjuster'),
@@ -101,8 +178,6 @@ def launch_textclient():
Component('OoT Adjuster', 'OoTAdjuster'),
# FF1
Component('FF1 Client', 'FF1Client'),
- # Pokémon
- Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
# TLoZ
Component('Zelda 1 Client', 'Zelda1Client', file_identifier=SuffixIdentifier('.aptloz')),
# ChecksFinder
@@ -114,8 +189,6 @@ def launch_textclient():
# Zillion
Component('Zillion Client', 'ZillionClient',
file_identifier=SuffixIdentifier('.apzl')),
- # Kingdom Hearts 2
- Component('KH2 Client', "KH2Client"),
#MegaMan Battle Network 3
Component('MMBN3 Client', 'MMBN3Client', file_identifier=SuffixIdentifier('.apbn3'))
diff --git a/worlds/__init__.py b/worlds/__init__.py
index c6208fa9a159..bb2fe866d02d 100644
--- a/worlds/__init__.py
+++ b/worlds/__init__.py
@@ -1,61 +1,74 @@
import importlib
+import importlib.util
+import logging
import os
import sys
-import typing
import warnings
import zipimport
+import time
+import dataclasses
+from typing import Dict, List, TypedDict
-folder = os.path.dirname(__file__)
+from Utils import local_path, user_path
+
+local_folder = os.path.dirname(__file__)
+user_folder = user_path("worlds") if user_path() != local_path() else user_path("custom_worlds")
+try:
+ os.makedirs(user_folder, exist_ok=True)
+except OSError: # can't access/write?
+ user_folder = None
__all__ = {
- "lookup_any_item_id_to_name",
- "lookup_any_location_id_to_name",
"network_data_package",
"AutoWorldRegister",
"world_sources",
- "folder",
+ "local_folder",
+ "user_folder",
+ "GamesPackage",
+ "DataPackage",
+ "failed_world_loads",
}
-if typing.TYPE_CHECKING:
- from .AutoWorld import World
-
-class GamesData(typing.TypedDict):
- item_name_groups: typing.Dict[str, typing.List[str]]
- item_name_to_id: typing.Dict[str, int]
- location_name_groups: typing.Dict[str, typing.List[str]]
- location_name_to_id: typing.Dict[str, int]
- version: int
+failed_world_loads: List[str] = []
-class GamesPackage(GamesData, total=False):
+class GamesPackage(TypedDict, total=False):
+ item_name_groups: Dict[str, List[str]]
+ item_name_to_id: Dict[str, int]
+ location_name_groups: Dict[str, List[str]]
+ location_name_to_id: Dict[str, int]
checksum: str
-class DataPackage(typing.TypedDict):
- games: typing.Dict[str, GamesPackage]
+class DataPackage(TypedDict):
+ games: Dict[str, GamesPackage]
-class WorldSource(typing.NamedTuple):
+@dataclasses.dataclass(order=True)
+class WorldSource:
path: str # typically relative path from this module
is_zip: bool = False
relative: bool = True # relative to regular world import folder
+ time_taken: float = -1.0
- def __repr__(self):
+ def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip}, relative={self.relative})"
@property
def resolved_path(self) -> str:
if self.relative:
- return os.path.join(folder, self.path)
+ return os.path.join(local_folder, self.path)
return self.path
def load(self) -> bool:
try:
+ start = time.perf_counter()
if self.is_zip:
importer = zipimport.zipimporter(self.resolved_path)
if hasattr(importer, "find_spec"): # new in Python 3.10
spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0])
+ assert spec, f"{self.path} is not a loadable module"
mod = importlib.util.module_from_spec(spec)
else: # TODO: remove with 3.8 support
mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0])
@@ -70,9 +83,10 @@ def load(self) -> bool:
importer.exec_module(mod)
else:
importlib.import_module(f".{self.path}", "worlds")
+ self.time_taken = time.perf_counter()-start
return True
- except Exception as e:
+ except Exception:
# A single world failing can still mean enough is working for the user, log and carry on
import traceback
import io
@@ -80,46 +94,38 @@ def load(self) -> bool:
print(f"Could not load world {self}:", file=file_like)
traceback.print_exc(file=file_like)
file_like.seek(0)
- import logging
logging.exception(file_like.read())
+ failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0])
return False
# find potential world containers, currently folders and zip-importable .apworld's
-world_sources: typing.List[WorldSource] = []
-file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly
-for file in os.scandir(folder):
- # prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "."
- if not file.name.startswith(("_", ".")):
- if file.is_dir():
- world_sources.append(WorldSource(file.name))
- elif file.is_file() and file.name.endswith(".apworld"):
- world_sources.append(WorldSource(file.name, is_zip=True))
+world_sources: List[WorldSource] = []
+for folder in (folder for folder in (user_folder, local_folder) if folder):
+ relative = folder == local_folder
+ for entry in os.scandir(folder):
+ # prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "."
+ if not entry.name.startswith(("_", ".")):
+ file_name = entry.name if relative else os.path.join(folder, entry.name)
+ if entry.is_dir():
+ if os.path.isfile(os.path.join(entry.path, '__init__.py')):
+ world_sources.append(WorldSource(file_name, relative=relative))
+ elif os.path.isfile(os.path.join(entry.path, '__init__.pyc')):
+ world_sources.append(WorldSource(file_name, relative=relative))
+ else:
+ logging.warning(f"excluding {entry.name} from world sources because it has no __init__.py")
+ elif entry.is_file() and entry.name.endswith(".apworld"):
+ world_sources.append(WorldSource(file_name, is_zip=True, relative=relative))
# import all submodules to trigger AutoWorldRegister
world_sources.sort()
for world_source in world_sources:
world_source.load()
-lookup_any_item_id_to_name = {}
-lookup_any_location_id_to_name = {}
-games: typing.Dict[str, GamesPackage] = {}
-
-from .AutoWorld import AutoWorldRegister
-
# Build the data package for each game.
-for world_name, world in AutoWorldRegister.world_types.items():
- games[world_name] = world.get_data_package_data()
- lookup_any_item_id_to_name.update(world.item_id_to_name)
- lookup_any_location_id_to_name.update(world.location_id_to_name)
+from .AutoWorld import AutoWorldRegister
network_data_package: DataPackage = {
- "games": games,
+ "games": {world_name: world.get_data_package_data() for world_name, world in AutoWorldRegister.world_types.items()},
}
-# Set entire datapackage to version 0 if any of them are set to 0
-if any(not world.data_version for world in AutoWorldRegister.world_types.values()):
- import logging
-
- logging.warning(f"Datapackage is in custom mode. Custom Worlds: "
- f"{[world for world in AutoWorldRegister.world_types.values() if not world.data_version]}")
diff --git a/worlds/_bizhawk/README.md b/worlds/_bizhawk/README.md
new file mode 100644
index 000000000000..ddc70c3dd748
--- /dev/null
+++ b/worlds/_bizhawk/README.md
@@ -0,0 +1,279 @@
+# BizHawk Client
+
+`BizHawkClient` is an abstract base class for a client that can access the memory of a ROM running in BizHawk. It does
+the legwork of connecting Python to a Lua connector script, letting you focus on the loop of checking locations and
+making on-the-fly modifications based on updates from the server. It also provides the same experience to users across
+multiple games that use it, and was built in response to a growing number of similar but separate bespoke game clients
+which are/were largely exclusive to BizHawk anyway.
+
+It's similar to `SNIClient`, but where `SNIClient` is designed to work for specifically SNES games across different
+emulators/hardware, `BizHawkClient` is designed to work for specifically BizHawk across the different systems BizHawk
+supports.
+
+The idea is that `BizHawkClient` connects to and communicates with a Lua script running in BizHawk. It provides an API
+that will call BizHawk functions for you to do things like read and write memory. And on an interval, control will be
+handed to a function you write for your game (`game_watcher`) which should interact with the game's memory to check what
+locations have been checked, give the player items, detect and send deathlinks, etc...
+
+Table of Contents:
+- [Connector Requests](#connector-requests)
+ - [Requests that depend on other requests](#requests-that-depend-on-other-requests)
+- [Implementing a Client](#implementing-a-client)
+ - [Example](#example)
+- [Tips](#tips)
+
+## Connector Requests
+
+Communication with BizHawk is done through `connector_bizhawk_generic.lua`. The client sends requests to the Lua script
+via sockets; the Lua script processes the request and sends the corresponding responses.
+
+The Lua script includes its own documentation, but you probably don't need to worry about the specifics. Instead, you'll
+be using the functions in `worlds/_bizhawk/__init__.py`. If you do need more control over the specific requests being
+sent or their order, you can still use `send_requests` to directly communicate with the connector script.
+
+It's not necessary to use the UI or client context if you only want to interact with the connector script. You can
+import and use just `worlds/_bizhawk/__init__.py`, which only depends on default modules.
+
+Here's a list of the included classes and functions. I would highly recommend looking at the actual function signatures
+and docstrings to learn more about each function.
+
+```
+class ConnectionStatus
+class BizHawkContext
+
+class NotConnectedError
+class RequestFailedError
+class ConnectorError
+class SyncError
+
+async def read(ctx, read_list) -> list[bytes]
+async def write(ctx, write_list) -> None:
+async def guarded_read(ctx, read_list, guard_list) -> (list[bytes] | None)
+async def guarded_write(ctx, write_list, guard_list) -> bool
+
+async def lock(ctx) -> None
+async def unlock(ctx) -> None
+
+async def get_hash(ctx) -> str
+async def get_system(ctx) -> str
+async def get_cores(ctx) -> dict[str, str]
+async def ping(ctx) -> None
+
+async def display_message(ctx, message: str) -> None
+async def set_message_interval(ctx, value: float) -> None
+
+async def connect(ctx) -> bool
+def disconnect(ctx) -> None
+
+async def get_script_version(ctx) -> int
+async def send_requests(ctx, req_list) -> list[dict[str, Any]]
+```
+
+`send_requests` is what actually communicates with the connector, and any functions like `guarded_read` will build the
+requests and then call `send_requests` for you. You can call `send_requests` yourself for more direct control, but make
+sure to read the docs in `connector_bizhawk_generic.lua`.
+
+A bundle of requests sent by `send_requests` will all be executed on the same frame, and by extension, so will any
+helper that calls `send_requests`. For example, if you were to call `read` with 3 items on your `read_list`, all 3
+addresses will be read on the same frame and then sent back.
+
+It also means that, by default, the only way to run multiple requests on the same frame is for them to be included in
+the same `send_requests` call. As soon as the connector finishes responding to a list of requests, it will advance the
+frame before checking for the next batch.
+
+### Requests that depend on other requests
+
+The fact that you have to wait at least a frame to act on any response may raise concerns. For example, Pokemon
+Emerald's save data is at a dynamic location in memory; it moves around when you load a new map. There is a static
+variable that holds the address of the save data, so we want to read the static variable to get the save address, and
+then use that address in a `write` to send the player an item. But between the `read` that tells us the address of the
+save data and the `write` to save data itself, an arbitrary number of frames have been executed, and the player may have
+loaded a new map, meaning we've written data to who knows where.
+
+There are two solutions to this problem.
+
+1. Use `guarded_write` instead of `write`. We can include a guard against the address changing, and the script will only
+perform the write if the data in memory matches what's in the guard. In the below example, `write_result` will be `True`
+if the guard validated and the data was written, and `False` if the guard failed to validate.
+
+```py
+# Get the address of the save data
+read_result: bytes = (await _bizhawk.read(ctx, [(0x3001111, 4, "System Bus")]))[0]
+save_data_address = int.from_bytes(read_result, "little")
+
+# Write to `save_data_address` if it hasn't changed
+write_result: bool = await _bizhawk.guarded_write(
+ ctx,
+ [(save_data_address, [0xAA, 0xBB], "System Bus")],
+ [(0x3001111, read_result, "System Bus")]
+)
+
+if write_result:
+ # The data at 0x3001111 was still the same value as
+ # what was returned from the first `_bizhawk.read`,
+ # so the data was written.
+ ...
+else:
+ # The data at 0x3001111 has changed since the
+ # first `_bizhawk.read`, so the data was not written.
+ ...
+```
+
+2. Use `lock` and `unlock` (discouraged if not necessary). When you call `lock`, you tell the emulator to stop advancing
+frames and just process requests until it receives an unlock request. This means you can lock, read the address, write
+the data, and then unlock on a single frame. **However**, this is _slow_. If you can't get in and get out quickly
+enough, players will notice a stutter in the emulation.
+
+```py
+# Pause emulation
+await _bizhawk.lock(ctx)
+
+# Get the address of the save data
+read_result: bytes = (await _bizhawk.read(ctx, [(0x3001111, 4, "System Bus")]))[0]
+save_data_address = int.from_bytes(read_result, "little")
+
+# Write to `save_data_address`
+await _bizhawk.write(ctx, [(save_data_address, [0xAA, 0xBB], "System Bus")])
+
+# Resume emulation
+await _bizhawk.unlock(ctx)
+```
+
+You should always use `guarded_read` and `guarded_write` instead of locking the emulator if possible. It may be
+unreliable, but that's by design. Most of the time you should have no problem giving up and retrying. Data that is
+volatile but only changes occasionally is the perfect use case.
+
+If data is almost guaranteed to change between frames, locking may be the better solution. You can lower the time spent
+locked by using `send_requests` directly to include as many requests alongside the `LOCK` and `UNLOCK` requests as
+possible. But in general it's probably worth doing some extra asm hacking and designing to make guards work instead.
+
+## Implementing a Client
+
+`BizHawkClient` itself is built on `CommonClient` and inspired heavily by `SNIClient`. Your world's client should
+inherit from `BizHawkClient` in `worlds/_bizhawk/client.py`. It must implement `validate_rom` and `game_watcher`, and
+must define values for `system` and `game`.
+
+As with the functions and classes in the previous section, I would highly recommend looking at the types and docstrings
+of the code itself.
+
+`game` should be the same value you use for your world definition.
+
+`system` can either be a string or a tuple of strings. This is the system (or systems) that your client is intended to
+handle games on (SNES, GBA, etc.). It's used to prevent validators from running on unknown systems and crashing. The
+actual abbreviation corresponds to whatever BizHawk returns from `emu.getsystemid()`.
+
+`patch_suffix` is an optional `ClassVar` meant to specify the file extensions you want to register. It can be a string
+or tuple of strings. When a player clicks "Open Patch" in a launcher, the suffix(es) will be whitelisted in the file
+select dialog and they will be associated with BizHawkClient. This does not affect whether the user's computer will
+associate the file extension with Archipelago.
+
+`validate_rom` is called to figure out whether a given ROM belongs to your client. It will only be called when a ROM is
+running on a system you specified in your `system` class variable. In most cases, that will be a single system and you
+can be sure that you're not about to try to read from nonexistent domains or out of bounds. If you decide to claim this
+ROM as yours, this is where you should do setup for things like `items_handling`.
+
+`game_watcher` is the "main loop" of your client where you should be checking memory and sending new items to the ROM.
+`BizHawkClient` will make sure that your `game_watcher` only runs when your client has validated the ROM, and will do
+its best to make sure you're connected to the connector script before calling your watcher. It runs this loop either
+immediately once it receives a message from the server, or a specified amount of time after the last iteration of the
+loop finished.
+
+`validate_rom`, `game_watcher`, and other methods will be passed an instance of `BizHawkClientContext`, which is a
+subclass of `CommonContext`. It additionally includes `slot_data` (if you are connected and asked for slot data),
+`bizhawk_ctx` (the instance of `BizHawkContext` that you should be giving to functions like `guarded_read`), and
+`watcher_timeout` (the amount of time in seconds between iterations of the game watcher loop).
+
+### Example
+
+A very simple client might look like this. All addresses here are made up; you should instead be using addresses that
+make sense for your specific ROM. The `validate_rom` here tries to read the name of the ROM. If it gets the value it
+wanted, it sets a couple values on `ctx` and returns `True`. The `game_watcher` reads some data from memory and acts on
+it by sending messages to AP. You should be smarter than this example, which will send `LocationChecks` messages even if
+there's nothing new since the last loop.
+
+```py
+from typing import TYPE_CHECKING
+
+from NetUtils import ClientStatus
+
+import worlds._bizhawk as bizhawk
+from worlds._bizhawk.client import BizHawkClient
+
+if TYPE_CHECKING:
+ from worlds._bizhawk.context import BizHawkClientContext
+
+
+class MyGameClient(BizHawkClient):
+ game = "My Game"
+ system = "GBA"
+ patch_suffix = ".apextension"
+
+ async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
+ try:
+ # Check ROM name/patch version
+ rom_name = ((await bizhawk.read(ctx.bizhawk_ctx, [(0x100, 6, "ROM")]))[0]).decode("ascii")
+ if rom_name != "MYGAME":
+ return False # Not a MYGAME ROM
+ except bizhawk.RequestFailedError:
+ return False # Not able to get a response, say no for now
+
+ # This is a MYGAME ROM
+ ctx.game = self.game
+ ctx.items_handling = 0b001
+ ctx.want_slot_data = True
+
+ return True
+
+ async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
+ try:
+ # Read save data
+ save_data = await bizhawk.read(
+ ctx.bizhawk_ctx,
+ [(0x3000100, 20, "System Bus")]
+ )[0]
+
+ # Check locations
+ if save_data[2] & 0x04:
+ await ctx.send_msgs([{
+ "cmd": "LocationChecks",
+ "locations": [23]
+ }])
+
+ # Send game clear
+ if not ctx.finished_game and (save_data[5] & 0x01):
+ await ctx.send_msgs([{
+ "cmd": "StatusUpdate",
+ "status": ClientStatus.CLIENT_GOAL
+ }])
+
+ except bizhawk.RequestFailedError:
+ # The connector didn't respond. Exit handler and return to main loop to reconnect
+ pass
+```
+
+### Tips
+
+- Make sure your client gets imported when your world is imported. You probably don't need to actually use anything in
+your `client.py` elsewhere, but you still have to import the file for your client to register itself.
+- When it comes to performance, there are two directions to optimize:
+ 1. If you need to execute multiple commands on the same frame, do as little work as possible. Only read and write necessary data,
+ and if you have to use locks, unlock as soon as it's okay to advance frames. This is probably the obvious one.
+ 2. Multiple things that don't have to happen on the same frame should be split up if they're likely to be slow.
+ Remember, the game watcher runs only a few times per second. Extra function calls on the client aren't that big of a
+ deal; the player will not notice if your `game_watcher` is slow. But the emulator has to be done with any given set of
+ commands in 1/60th of a second to avoid hiccups (faster still if your players use speedup). Too many reads of too much
+ data at the same time is more likely to cause a bad user experience.
+- Your `game_watcher` will be called regardless of the status of the client's connection to the server. Double-check the
+server connection before trying to interact with it.
+- By default, the player will be asked to provide their slot name after connecting to the server and validating, and
+that input will be used to authenticate with the `Connect` command. You can override `set_auth` in your own client to
+set it automatically based on data in the ROM or on your client instance.
+- You can override `on_package` in your client to watch raw packages, but don't forget you also have access to a
+subclass of `CommonContext` and its API.
+- You can import `BizHawkClientContext` for type hints using `typing.TYPE_CHECKING`. Importing it without conditions at
+the top of the file will probably cause a circular dependency.
+- Your game's system may have multiple usable cores in BizHawk. You can use `get_cores` to try to determine which one is
+currently loaded (it's the best we can do). Some cores may differ in the names of memory domains. It's good to check all
+the available cores to find differences before your users do.
+- The connector script includes a DEBUG variable that you can use to log requests/responses. (Be aware that as the log
+grows in size in BizHawk, it begins to stutter while trying to print it.)
diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py
new file mode 100644
index 000000000000..74f2954b984b
--- /dev/null
+++ b/worlds/_bizhawk/__init__.py
@@ -0,0 +1,326 @@
+"""
+A module for interacting with BizHawk through `connector_bizhawk_generic.lua`.
+
+Any mention of `domain` in this module refers to the names BizHawk gives to memory domains in its own lua api. They are
+naively passed to BizHawk without validation or modification.
+"""
+
+import asyncio
+import base64
+import enum
+import json
+import sys
+import typing
+
+
+BIZHAWK_SOCKET_PORT_RANGE_START = 43055
+BIZHAWK_SOCKET_PORT_RANGE_SIZE = 5
+
+
+class ConnectionStatus(enum.IntEnum):
+ NOT_CONNECTED = 1
+ TENTATIVE = 2
+ CONNECTED = 3
+
+
+class NotConnectedError(Exception):
+ """Raised when something tries to make a request to the connector script before a connection has been established"""
+ pass
+
+
+class RequestFailedError(Exception):
+ """Raised when the connector script did not respond to a request"""
+ pass
+
+
+class ConnectorError(Exception):
+ """Raised when the connector script encounters an error while processing a request"""
+ pass
+
+
+class SyncError(Exception):
+ """Raised when the connector script responded with a mismatched response type"""
+ pass
+
+
+class BizHawkContext:
+ streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]]
+ connection_status: ConnectionStatus
+ _lock: asyncio.Lock
+ _port: typing.Optional[int]
+
+ def __init__(self) -> None:
+ self.streams = None
+ self.connection_status = ConnectionStatus.NOT_CONNECTED
+ self._lock = asyncio.Lock()
+ self._port = None
+
+ async def _send_message(self, message: str):
+ async with self._lock:
+ if self.streams is None:
+ raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
+
+ try:
+ reader, writer = self.streams
+ writer.write(message.encode("utf-8") + b"\n")
+ await asyncio.wait_for(writer.drain(), timeout=5)
+
+ res = await asyncio.wait_for(reader.readline(), timeout=5)
+
+ if res == b"":
+ writer.close()
+ self.streams = None
+ self.connection_status = ConnectionStatus.NOT_CONNECTED
+ raise RequestFailedError("Connection closed")
+
+ if self.connection_status == ConnectionStatus.TENTATIVE:
+ self.connection_status = ConnectionStatus.CONNECTED
+
+ return res.decode("utf-8")
+ except asyncio.TimeoutError as exc:
+ writer.close()
+ self.streams = None
+ self.connection_status = ConnectionStatus.NOT_CONNECTED
+ raise RequestFailedError("Connection timed out") from exc
+ except ConnectionResetError as exc:
+ writer.close()
+ self.streams = None
+ self.connection_status = ConnectionStatus.NOT_CONNECTED
+ raise RequestFailedError("Connection reset") from exc
+
+
+async def connect(ctx: BizHawkContext) -> bool:
+ """Attempts to establish a connection with a connector script. Returns True if successful."""
+ rotation_steps = 0 if ctx._port is None else ctx._port - BIZHAWK_SOCKET_PORT_RANGE_START
+ ports = [*range(BIZHAWK_SOCKET_PORT_RANGE_START, BIZHAWK_SOCKET_PORT_RANGE_START + BIZHAWK_SOCKET_PORT_RANGE_SIZE)]
+ ports = ports[rotation_steps:] + ports[:rotation_steps]
+
+ for port in ports:
+ try:
+ ctx.streams = await asyncio.open_connection("127.0.0.1", port)
+ ctx.connection_status = ConnectionStatus.TENTATIVE
+ ctx._port = port
+ return True
+ except (TimeoutError, ConnectionRefusedError):
+ continue
+
+ # No ports worked
+ ctx.streams = None
+ ctx.connection_status = ConnectionStatus.NOT_CONNECTED
+ return False
+
+
+def disconnect(ctx: BizHawkContext) -> None:
+ """Closes the connection to the connector script."""
+ if ctx.streams is not None:
+ ctx.streams[1].close()
+ ctx.streams = None
+ ctx.connection_status = ConnectionStatus.NOT_CONNECTED
+
+
+async def get_script_version(ctx: BizHawkContext) -> int:
+ return int(await ctx._send_message("VERSION"))
+
+
+async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]:
+ """Sends a list of requests to the BizHawk connector and returns their responses.
+
+ It's likely you want to use the wrapper functions instead of this."""
+ responses = json.loads(await ctx._send_message(json.dumps(req_list)))
+ errors: typing.List[ConnectorError] = []
+
+ for response in responses:
+ if response["type"] == "ERROR":
+ errors.append(ConnectorError(response["err"]))
+
+ if errors:
+ if sys.version_info >= (3, 11, 0):
+ raise ExceptionGroup("Connector script returned errors", errors) # noqa
+ else:
+ raise errors[0]
+
+ return responses
+
+
+async def ping(ctx: BizHawkContext) -> None:
+ """Sends a PING request and receives a PONG response."""
+ res = (await send_requests(ctx, [{"type": "PING"}]))[0]
+
+ if res["type"] != "PONG":
+ raise SyncError(f"Expected response of type PONG but got {res['type']}")
+
+
+async def get_hash(ctx: BizHawkContext) -> str:
+ """Gets the system name for the currently loaded ROM"""
+ res = (await send_requests(ctx, [{"type": "HASH"}]))[0]
+
+ if res["type"] != "HASH_RESPONSE":
+ raise SyncError(f"Expected response of type HASH_RESPONSE but got {res['type']}")
+
+ return res["value"]
+
+
+async def get_system(ctx: BizHawkContext) -> str:
+ """Gets the system name for the currently loaded ROM"""
+ res = (await send_requests(ctx, [{"type": "SYSTEM"}]))[0]
+
+ if res["type"] != "SYSTEM_RESPONSE":
+ raise SyncError(f"Expected response of type SYSTEM_RESPONSE but got {res['type']}")
+
+ return res["value"]
+
+
+async def get_cores(ctx: BizHawkContext) -> typing.Dict[str, str]:
+ """Gets the preferred cores for systems with multiple cores. Only systems with multiple available cores have
+ entries."""
+ res = (await send_requests(ctx, [{"type": "PREFERRED_CORES"}]))[0]
+
+ if res["type"] != "PREFERRED_CORES_RESPONSE":
+ raise SyncError(f"Expected response of type PREFERRED_CORES_RESPONSE but got {res['type']}")
+
+ return res["value"]
+
+
+async def lock(ctx: BizHawkContext) -> None:
+ """Locks BizHawk in anticipation of receiving more requests this frame.
+
+ Consider using guarded reads and writes instead of locks if possible.
+
+ While locked, emulation will halt and the connector will block on incoming requests until an `UNLOCK` request is
+ sent. Remember to unlock when you're done, or the emulator will appear to freeze.
+
+ Sending multiple lock commands is the same as sending one."""
+ res = (await send_requests(ctx, [{"type": "LOCK"}]))[0]
+
+ if res["type"] != "LOCKED":
+ raise SyncError(f"Expected response of type LOCKED but got {res['type']}")
+
+
+async def unlock(ctx: BizHawkContext) -> None:
+ """Unlocks BizHawk to allow it to resume emulation. See `lock` for more info.
+
+ Sending multiple unlock commands is the same as sending one."""
+ res = (await send_requests(ctx, [{"type": "UNLOCK"}]))[0]
+
+ if res["type"] != "UNLOCKED":
+ raise SyncError(f"Expected response of type UNLOCKED but got {res['type']}")
+
+
+async def display_message(ctx: BizHawkContext, message: str) -> None:
+ """Displays the provided message in BizHawk's message queue."""
+ res = (await send_requests(ctx, [{"type": "DISPLAY_MESSAGE", "message": message}]))[0]
+
+ if res["type"] != "DISPLAY_MESSAGE_RESPONSE":
+ raise SyncError(f"Expected response of type DISPLAY_MESSAGE_RESPONSE but got {res['type']}")
+
+
+async def set_message_interval(ctx: BizHawkContext, value: float) -> None:
+ """Sets the minimum amount of time in seconds to wait between queued messages. The default value of 0 will allow one
+ new message to display per frame."""
+ res = (await send_requests(ctx, [{"type": "SET_MESSAGE_INTERVAL", "value": value}]))[0]
+
+ if res["type"] != "SET_MESSAGE_INTERVAL_RESPONSE":
+ raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}")
+
+
+async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]],
+ guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> typing.Optional[typing.List[bytes]]:
+ """Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected
+ value.
+
+ Items in read_list should be organized (address, size, domain) where
+ - `address` is the address of the first byte of data
+ - `size` is the number of bytes to read
+ - `domain` is the name of the region of memory the address corresponds to
+
+ Items in `guard_list` should be organized `(address, expected_data, domain)` where
+ - `address` is the address of the first byte of data
+ - `expected_data` is the bytes that the data starting at this address is expected to match
+ - `domain` is the name of the region of memory the address corresponds to
+
+ Returns None if any item in guard_list failed to validate. Otherwise returns a list of bytes in the order they
+ were requested."""
+ res = await send_requests(ctx, [{
+ "type": "GUARD",
+ "address": address,
+ "expected_data": base64.b64encode(bytes(expected_data)).decode("ascii"),
+ "domain": domain
+ } for address, expected_data, domain in guard_list] + [{
+ "type": "READ",
+ "address": address,
+ "size": size,
+ "domain": domain
+ } for address, size, domain in read_list])
+
+ ret: typing.List[bytes] = []
+ for item in res:
+ if item["type"] == "GUARD_RESPONSE":
+ if not item["value"]:
+ return None
+ else:
+ if item["type"] != "READ_RESPONSE":
+ raise SyncError(f"Expected response of type READ_RESPONSE or GUARD_RESPONSE but got {item['type']}")
+
+ ret.append(base64.b64decode(item["value"]))
+
+ return ret
+
+
+async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]]) -> typing.List[bytes]:
+ """Reads data at 1 or more addresses.
+
+ Items in `read_list` should be organized `(address, size, domain)` where
+ - `address` is the address of the first byte of data
+ - `size` is the number of bytes to read
+ - `domain` is the name of the region of memory the address corresponds to
+
+ Returns a list of bytes in the order they were requested."""
+ return await guarded_read(ctx, read_list, [])
+
+
+async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]],
+ guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> bool:
+ """Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value.
+
+ Items in `write_list` should be organized `(address, value, domain)` where
+ - `address` is the address of the first byte of data
+ - `value` is a list of bytes to write, in order, starting at `address`
+ - `domain` is the name of the region of memory the address corresponds to
+
+ Items in `guard_list` should be organized `(address, expected_data, domain)` where
+ - `address` is the address of the first byte of data
+ - `expected_data` is the bytes that the data starting at this address is expected to match
+ - `domain` is the name of the region of memory the address corresponds to
+
+ Returns False if any item in guard_list failed to validate. Otherwise returns True."""
+ res = await send_requests(ctx, [{
+ "type": "GUARD",
+ "address": address,
+ "expected_data": base64.b64encode(bytes(expected_data)).decode("ascii"),
+ "domain": domain
+ } for address, expected_data, domain in guard_list] + [{
+ "type": "WRITE",
+ "address": address,
+ "value": base64.b64encode(bytes(value)).decode("ascii"),
+ "domain": domain
+ } for address, value, domain in write_list])
+
+ for item in res:
+ if item["type"] == "GUARD_RESPONSE":
+ if not item["value"]:
+ return False
+ else:
+ if item["type"] != "WRITE_RESPONSE":
+ raise SyncError(f"Expected response of type WRITE_RESPONSE or GUARD_RESPONSE but got {item['type']}")
+
+ return True
+
+
+async def write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> None:
+ """Writes data to 1 or more addresses.
+
+ Items in write_list should be organized `(address, value, domain)` where
+ - `address` is the address of the first byte of data
+ - `value` is a list of bytes to write, in order, starting at `address`
+ - `domain` is the name of the region of memory the address corresponds to"""
+ await guarded_write(ctx, write_list, [])
diff --git a/worlds/_bizhawk/client.py b/worlds/_bizhawk/client.py
new file mode 100644
index 000000000000..00370c277a17
--- /dev/null
+++ b/worlds/_bizhawk/client.py
@@ -0,0 +1,101 @@
+"""
+A module containing the BizHawkClient base class and metaclass
+"""
+
+from __future__ import annotations
+
+import abc
+from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Tuple, Union
+
+from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess
+
+if TYPE_CHECKING:
+ from .context import BizHawkClientContext
+
+
+def launch_client(*args) -> None:
+ from .context import launch
+ launch_subprocess(launch, name="BizHawkClient")
+
+
+component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
+ file_identifier=SuffixIdentifier())
+components.append(component)
+
+
+class AutoBizHawkClientRegister(abc.ABCMeta):
+ game_handlers: ClassVar[Dict[Tuple[str, ...], Dict[str, BizHawkClient]]] = {}
+
+ def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> AutoBizHawkClientRegister:
+ new_class = super().__new__(cls, name, bases, namespace)
+
+ # Register handler
+ if "system" in namespace:
+ systems = (namespace["system"],) if type(namespace["system"]) is str else tuple(sorted(namespace["system"]))
+ if systems not in AutoBizHawkClientRegister.game_handlers:
+ AutoBizHawkClientRegister.game_handlers[systems] = {}
+
+ if "game" in namespace:
+ AutoBizHawkClientRegister.game_handlers[systems][namespace["game"]] = new_class()
+
+ # Update launcher component's suffixes
+ if "patch_suffix" in namespace:
+ if namespace["patch_suffix"] is not None:
+ existing_identifier: SuffixIdentifier = component.file_identifier
+ new_suffixes = [*existing_identifier.suffixes]
+
+ if type(namespace["patch_suffix"]) is str:
+ new_suffixes.append(namespace["patch_suffix"])
+ else:
+ new_suffixes.extend(namespace["patch_suffix"])
+
+ component.file_identifier = SuffixIdentifier(*new_suffixes)
+
+ return new_class
+
+ @staticmethod
+ async def get_handler(ctx: "BizHawkClientContext", system: str) -> Optional[BizHawkClient]:
+ for systems, handlers in AutoBizHawkClientRegister.game_handlers.items():
+ if system in systems:
+ for handler in handlers.values():
+ if await handler.validate_rom(ctx):
+ return handler
+
+ return None
+
+
+class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
+ system: ClassVar[Union[str, Tuple[str, ...]]]
+ """The system(s) that the game this client is for runs on"""
+
+ game: ClassVar[str]
+ """The game this client is for"""
+
+ patch_suffix: ClassVar[Optional[Union[str, Tuple[str, ...]]]]
+ """The file extension(s) this client is meant to open and patch (e.g. ".apz3")"""
+
+ @abc.abstractmethod
+ async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
+ """Should return whether the currently loaded ROM should be handled by this client. You might read the game name
+ from the ROM header, for example. This function will only be asked to validate ROMs from the system set by the
+ client class, so you do not need to check the system yourself.
+
+ Once this function has determined that the ROM should be handled by this client, it should also modify `ctx`
+ as necessary (such as setting `ctx.game = self.game`, modifying `ctx.items_handling`, etc...)."""
+ ...
+
+ async def set_auth(self, ctx: "BizHawkClientContext") -> None:
+ """Should set ctx.auth in anticipation of sending a `Connected` packet. You may override this if you store slot
+ name in your patched ROM. If ctx.auth is not set after calling, the player will be prompted to enter their
+ username."""
+ pass
+
+ @abc.abstractmethod
+ async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
+ """Runs on a loop with the approximate interval `ctx.watcher_timeout`. The currently loaded ROM is guaranteed
+ to have passed your validator when this function is called, and the emulator is very likely to be connected."""
+ ...
+
+ def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None:
+ """For handling packages from the server. Called from `BizHawkClientContext.on_package`."""
+ pass
diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py
new file mode 100644
index 000000000000..234faf3b65cf
--- /dev/null
+++ b/worlds/_bizhawk/context.py
@@ -0,0 +1,276 @@
+"""
+A module containing context and functions relevant to running the client. This module should only be imported for type
+checking or launching the client, otherwise it will probably cause circular import issues.
+"""
+
+import asyncio
+import enum
+import subprocess
+from typing import Any, Dict, Optional
+
+from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser, server_loop, logger, gui_enabled
+import Patch
+import Utils
+
+from . import BizHawkContext, ConnectionStatus, NotConnectedError, RequestFailedError, connect, disconnect, get_hash, \
+ get_script_version, get_system, ping
+from .client import BizHawkClient, AutoBizHawkClientRegister
+
+
+EXPECTED_SCRIPT_VERSION = 1
+
+
+class AuthStatus(enum.IntEnum):
+ NOT_AUTHENTICATED = 0
+ NEED_INFO = 1
+ PENDING = 2
+ AUTHENTICATED = 3
+
+
+class BizHawkClientCommandProcessor(ClientCommandProcessor):
+ def _cmd_bh(self):
+ """Shows the current status of the client's connection to BizHawk"""
+ if isinstance(self.ctx, BizHawkClientContext):
+ if self.ctx.bizhawk_ctx.connection_status == ConnectionStatus.NOT_CONNECTED:
+ logger.info("BizHawk Connection Status: Not Connected")
+ elif self.ctx.bizhawk_ctx.connection_status == ConnectionStatus.TENTATIVE:
+ logger.info("BizHawk Connection Status: Tentatively Connected")
+ elif self.ctx.bizhawk_ctx.connection_status == ConnectionStatus.CONNECTED:
+ logger.info("BizHawk Connection Status: Connected")
+
+
+class BizHawkClientContext(CommonContext):
+ command_processor = BizHawkClientCommandProcessor
+ auth_status: AuthStatus
+ password_requested: bool
+ client_handler: Optional[BizHawkClient]
+ slot_data: Optional[Dict[str, Any]] = None
+ rom_hash: Optional[str] = None
+ bizhawk_ctx: BizHawkContext
+
+ watcher_timeout: float
+ """The maximum amount of time the game watcher loop will wait for an update from the server before executing"""
+
+ def __init__(self, server_address: Optional[str], password: Optional[str]):
+ super().__init__(server_address, password)
+ self.auth_status = AuthStatus.NOT_AUTHENTICATED
+ self.password_requested = False
+ self.client_handler = None
+ self.bizhawk_ctx = BizHawkContext()
+ self.watcher_timeout = 0.5
+
+ def run_gui(self):
+ from kvui import GameManager
+
+ class BizHawkManager(GameManager):
+ base_title = "Archipelago BizHawk Client"
+
+ self.ui = BizHawkManager(self)
+ self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
+
+ def on_package(self, cmd, args):
+ if cmd == "Connected":
+ self.slot_data = args.get("slot_data", None)
+ self.auth_status = AuthStatus.AUTHENTICATED
+
+ if self.client_handler is not None:
+ self.client_handler.on_package(self, cmd, args)
+
+ async def server_auth(self, password_requested: bool=False):
+ self.password_requested = password_requested
+
+ if self.bizhawk_ctx.connection_status != ConnectionStatus.CONNECTED:
+ logger.info("Awaiting connection to BizHawk before authenticating")
+ return
+
+ if self.client_handler is None:
+ return
+
+ # Ask handler to set auth
+ if self.auth is None:
+ self.auth_status = AuthStatus.NEED_INFO
+ await self.client_handler.set_auth(self)
+
+ # Handler didn't set auth, ask user for slot name
+ if self.auth is None:
+ await self.get_username()
+
+ if password_requested and not self.password:
+ self.auth_status = AuthStatus.NEED_INFO
+ await super(BizHawkClientContext, self).server_auth(password_requested)
+
+ await self.send_connect()
+ self.auth_status = AuthStatus.PENDING
+
+ async def disconnect(self, allow_autoreconnect: bool=False):
+ self.auth_status = AuthStatus.NOT_AUTHENTICATED
+ await super().disconnect(allow_autoreconnect)
+
+
+async def _game_watcher(ctx: BizHawkClientContext):
+ showed_connecting_message = False
+ showed_connected_message = False
+ showed_no_handler_message = False
+
+ while not ctx.exit_event.is_set():
+ try:
+ await asyncio.wait_for(ctx.watcher_event.wait(), ctx.watcher_timeout)
+ except asyncio.TimeoutError:
+ pass
+
+ ctx.watcher_event.clear()
+
+ try:
+ if ctx.bizhawk_ctx.connection_status == ConnectionStatus.NOT_CONNECTED:
+ showed_connected_message = False
+
+ if not showed_connecting_message:
+ logger.info("Waiting to connect to BizHawk...")
+ showed_connecting_message = True
+
+ # Since a call to `connect` can take a while to return, this will cancel connecting
+ # if the user has decided to close the client.
+ connect_task = asyncio.create_task(connect(ctx.bizhawk_ctx), name="BizHawkConnect")
+ exit_task = asyncio.create_task(ctx.exit_event.wait(), name="ExitWait")
+ await asyncio.wait([connect_task, exit_task], return_when=asyncio.FIRST_COMPLETED)
+
+ if exit_task.done():
+ connect_task.cancel()
+ return
+
+ if not connect_task.result():
+ # Failed to connect
+ continue
+
+ showed_no_handler_message = False
+
+ script_version = await get_script_version(ctx.bizhawk_ctx)
+
+ if script_version != EXPECTED_SCRIPT_VERSION:
+ logger.info(f"Connector script is incompatible. Expected version {EXPECTED_SCRIPT_VERSION} but "
+ f"got {script_version}. Disconnecting.")
+ disconnect(ctx.bizhawk_ctx)
+ continue
+
+ showed_connecting_message = False
+
+ await ping(ctx.bizhawk_ctx)
+
+ if not showed_connected_message:
+ showed_connected_message = True
+ logger.info("Connected to BizHawk")
+
+ rom_hash = await get_hash(ctx.bizhawk_ctx)
+ if ctx.rom_hash is not None and ctx.rom_hash != rom_hash:
+ if ctx.server is not None and not ctx.server.socket.closed:
+ logger.info(f"ROM changed. Disconnecting from server.")
+
+ ctx.auth = None
+ ctx.username = None
+ ctx.client_handler = None
+ ctx.finished_game = False
+ await ctx.disconnect(False)
+ ctx.rom_hash = rom_hash
+
+ if ctx.client_handler is None:
+ system = await get_system(ctx.bizhawk_ctx)
+ ctx.client_handler = await AutoBizHawkClientRegister.get_handler(ctx, system)
+
+ if ctx.client_handler is None:
+ if not showed_no_handler_message:
+ logger.info("No handler was found for this game. Double-check that the apworld is installed "
+ "correctly and that you loaded the right ROM file.")
+ showed_no_handler_message = True
+ continue
+ else:
+ showed_no_handler_message = False
+ logger.info(f"Running handler for {ctx.client_handler.game}")
+
+ except RequestFailedError as exc:
+ logger.info(f"Lost connection to BizHawk: {exc.args[0]}")
+ continue
+ except NotConnectedError:
+ continue
+
+ # Server auth
+ if ctx.server is not None and not ctx.server.socket.closed:
+ if ctx.auth_status == AuthStatus.NOT_AUTHENTICATED:
+ Utils.async_start(ctx.server_auth(ctx.password_requested))
+ else:
+ ctx.auth_status = AuthStatus.NOT_AUTHENTICATED
+
+ # Call the handler's game watcher
+ await ctx.client_handler.game_watcher(ctx)
+
+
+async def _run_game(rom: str):
+ 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,
+ f"--lua={Utils.local_path('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):
+ try:
+ metadata, output_file = Patch.create_rom_file(patch_file)
+ Utils.async_start(_run_game(output_file))
+ except Exception as exc:
+ logger.exception(exc)
+
+
+def launch() -> None:
+ async def main():
+ parser = get_base_parser()
+ parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file")
+ args = parser.parse_args()
+
+ ctx = BizHawkClientContext(args.connect, args.password)
+ ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
+
+ if gui_enabled:
+ ctx.run_gui()
+ ctx.run_cli()
+
+ if args.patch_file != "":
+ Utils.async_start(_patch_and_run_game(args.patch_file))
+
+ watcher_task = asyncio.create_task(_game_watcher(ctx), name="GameWatcher")
+
+ try:
+ await watcher_task
+ except Exception as e:
+ logger.exception(e)
+
+ await ctx.exit_event.wait()
+ await ctx.shutdown()
+
+ Utils.init_logging("BizHawkClient", exception_logger="Client")
+ import colorama
+ colorama.init()
+ asyncio.run(main())
+ colorama.deinit()
diff --git a/worlds/_sc2common/bot/bot_ai.py b/worlds/_sc2common/bot/bot_ai.py
index 79c11a5ad4a3..b08f8af29ac0 100644
--- a/worlds/_sc2common/bot/bot_ai.py
+++ b/worlds/_sc2common/bot/bot_ai.py
@@ -12,6 +12,8 @@
from .unit import Unit
from .units import Units
+from worlds._sc2common.bot import logger
+
if TYPE_CHECKING:
from .game_info import Ramp
@@ -310,6 +312,7 @@ async def chat_send(self, message: str, team_only: bool = False):
:param message:
:param team_only:"""
assert isinstance(message, str), f"{message} is not a string"
+ logger.debug("Sending message: " + message)
await self.client.chat_send(message, team_only)
def in_map_bounds(self, pos: Union[Point2, tuple, list]) -> bool:
diff --git a/worlds/_sc2common/bot/sc2process.py b/worlds/_sc2common/bot/sc2process.py
index e36632165979..f74ed9c18f9f 100644
--- a/worlds/_sc2common/bot/sc2process.py
+++ b/worlds/_sc2common/bot/sc2process.py
@@ -28,6 +28,11 @@ def add(cls, value):
logger.debug("kill_switch: Add switch")
cls._to_kill.append(value)
+ @classmethod
+ def kill(cls, value):
+ logger.info(f"kill_switch: Process cleanup for 1 process")
+ value._clean(verbose=False)
+
@classmethod
def kill_all(cls):
logger.info(f"kill_switch: Process cleanup for {len(cls._to_kill)} processes")
@@ -116,7 +121,7 @@ def signal_handler(*_args):
async def __aexit__(self, *args):
logger.exception("async exit")
await self._close_connection()
- kill_switch.kill_all()
+ kill_switch.kill(self)
signal.signal(signal.SIGINT, signal.SIG_DFL)
@property
diff --git a/worlds/adventure/Locations.py b/worlds/adventure/Locations.py
index 2ef561b1e3e1..27e504684cbf 100644
--- a/worlds/adventure/Locations.py
+++ b/worlds/adventure/Locations.py
@@ -19,9 +19,9 @@ def __init__(self, room_id: int, room_x: int = None, room_y: int = None):
def get_position(self, random):
if self.room_x is None or self.room_y is None:
- return random.choice(standard_positions)
+ return self.room_id, random.choice(standard_positions)
else:
- return self.room_x, self.room_y
+ return self.room_id, (self.room_x, self.room_y)
class LocationData:
@@ -46,24 +46,26 @@ def __init__(self, region, name, location_id, world_positions: [WorldPosition] =
self.needs_bat_logic: int = needs_bat_logic
self.local_item: int = None
- def get_position(self, random):
+ def get_random_position(self, random):
+ x: int = None
+ y: int = None
if self.world_positions is None or len(self.world_positions) == 0:
if self.room_id is None:
return None
- self.room_x, self.room_y = random.choice(standard_positions)
- if self.room_id is None:
+ x, y = random.choice(standard_positions)
+ return self.room_id, x, y
+ else:
selected_pos = random.choice(self.world_positions)
- self.room_id = selected_pos.room_id
- self.room_x, self.room_y = selected_pos.get_position(random)
- return self.room_x, self.room_y
+ room_id, (x, y) = selected_pos.get_position(random)
+ return self.get_random_room_id(random), x, y
- def get_room_id(self, random):
+ def get_random_room_id(self, random):
if self.world_positions is None or len(self.world_positions) == 0:
- return None
+ if self.room_id is None:
+ return None
if self.room_id is None:
selected_pos = random.choice(self.world_positions)
- self.room_id = selected_pos.room_id
- self.room_x, self.room_y = selected_pos.get_position(random)
+ return selected_pos.room_id
return self.room_id
@@ -97,7 +99,7 @@ def get_random_room_in_regions(regions: [str], random) -> int:
possible_rooms = {}
for locname in location_table:
if location_table[locname].region in regions:
- room = location_table[locname].get_room_id(random)
+ room = location_table[locname].get_random_room_id(random)
if room is not None:
possible_rooms[room] = location_table[locname].room_id
return random.choice(list(possible_rooms.keys()))
diff --git a/worlds/adventure/Options.py b/worlds/adventure/Options.py
index fb09e5329b82..e6a8e4c20200 100644
--- a/worlds/adventure/Options.py
+++ b/worlds/adventure/Options.py
@@ -2,7 +2,8 @@
from typing import Dict
-from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle
+from dataclasses import dataclass
+from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions
class FreeincarnateMax(Range):
@@ -223,22 +224,22 @@ class StartCastle(Choice):
option_white = 2
default = option_yellow
+@dataclass
+class AdventureOptions(PerGameCommonOptions):
+ dragon_slay_check: DragonSlayCheck
+ death_link: DeathLink
+ bat_logic: BatLogic
+ freeincarnate_max: FreeincarnateMax
+ dragon_rando_type: DragonRandoType
+ connector_multi_slot: ConnectorMultiSlot
+ yorgle_speed: YorgleStartingSpeed
+ yorgle_min_speed: YorgleMinimumSpeed
+ grundle_speed: GrundleStartingSpeed
+ grundle_min_speed: GrundleMinimumSpeed
+ rhindle_speed: RhindleStartingSpeed
+ rhindle_min_speed: RhindleMinimumSpeed
+ difficulty_switch_a: DifficultySwitchA
+ difficulty_switch_b: DifficultySwitchB
+ start_castle: StartCastle
+
-adventure_option_definitions: Dict[str, type(Option)] = {
- "dragon_slay_check": DragonSlayCheck,
- "death_link": DeathLink,
- "bat_logic": BatLogic,
- "freeincarnate_max": FreeincarnateMax,
- "dragon_rando_type": DragonRandoType,
- "connector_multi_slot": ConnectorMultiSlot,
- "yorgle_speed": YorgleStartingSpeed,
- "yorgle_min_speed": YorgleMinimumSpeed,
- "grundle_speed": GrundleStartingSpeed,
- "grundle_min_speed": GrundleMinimumSpeed,
- "rhindle_speed": RhindleStartingSpeed,
- "rhindle_min_speed": RhindleMinimumSpeed,
- "difficulty_switch_a": DifficultySwitchA,
- "difficulty_switch_b": DifficultySwitchB,
- "start_castle": StartCastle,
-
-}
\ No newline at end of file
diff --git a/worlds/adventure/Regions.py b/worlds/adventure/Regions.py
index 4a62518fbd36..e72806ca454f 100644
--- a/worlds/adventure/Regions.py
+++ b/worlds/adventure/Regions.py
@@ -1,4 +1,5 @@
from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType
+from Options import PerGameCommonOptions
from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region
@@ -24,9 +25,7 @@ def connect(world: MultiWorld, player: int, source: str, target: str, rule: call
connect(world, player, target, source, rule, True)
-def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
- for name, locdata in location_table.items():
- locdata.get_position(multiworld.random)
+def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
menu = Region("Menu", player, multiworld)
@@ -76,7 +75,7 @@ def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> Non
credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side))
multiworld.regions.append(credits_room_far_side)
- dragon_slay_check = multiworld.dragon_slay_check[player].value
+ dragon_slay_check = options.dragon_slay_check.value
priority_locations = determine_priority_locations(multiworld, dragon_slay_check)
for name, location_data in location_table.items():
diff --git a/worlds/adventure/Rom.py b/worlds/adventure/Rom.py
index 62c401971895..ca64e569716a 100644
--- a/worlds/adventure/Rom.py
+++ b/worlds/adventure/Rom.py
@@ -6,9 +6,8 @@
import Utils
from .Locations import AdventureLocation, LocationData
-from Utils import OptionsType
-from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer
-from itertools import chain
+from settings import get_settings
+from worlds.Files import APPatch, AutoPatchRegister
import bsdiff4
@@ -79,7 +78,7 @@ def get_dict(self):
return ret_dict
-class AdventureDeltaPatch(APContainer, metaclass=AutoPatchRegister):
+class AdventureDeltaPatch(APPatch, metaclass=AutoPatchRegister):
hash = ADVENTUREHASH
game = "Adventure"
patch_file_ending = ".apadvn"
@@ -313,9 +312,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
def get_base_rom_path(file_name: str = "") -> str:
- options: OptionsType = Utils.get_options()
if not file_name:
- file_name = options["adventure_options"]["rom_file"]
+ file_name = get_settings()["adventure_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.user_path(file_name)
return file_name
diff --git a/worlds/adventure/Rules.py b/worlds/adventure/Rules.py
index 6f4b53faa11b..53617b039d78 100644
--- a/worlds/adventure/Rules.py
+++ b/worlds/adventure/Rules.py
@@ -1,12 +1,10 @@
-from worlds.adventure import location_table
-from worlds.adventure.Options import BatLogic, DifficultySwitchB, DifficultySwitchA
+from .Options import BatLogic, DifficultySwitchB
from worlds.generic.Rules import add_rule, set_rule, forbid_item
-from BaseClasses import LocationProgressType
def set_rules(self) -> None:
world = self.multiworld
- use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic
+ use_bat_logic = self.options.bat_logic.value == BatLogic.option_use_logic
set_rule(world.get_entrance("YellowCastlePort", self.player),
lambda state: state.has("Yellow Key", self.player))
@@ -28,7 +26,7 @@ def set_rules(self) -> None:
lambda state: state.has("Bridge", self.player) or
state.has("Magnet", self.player))
- dragon_slay_check = world.dragon_slay_check[self.player].value
+ dragon_slay_check = self.options.dragon_slay_check.value
if dragon_slay_check:
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
set_rule(world.get_location("Slay Yorgle", self.player),
diff --git a/worlds/adventure/__init__.py b/worlds/adventure/__init__.py
index 105725bd053c..ed5ebbd3dc56 100644
--- a/worlds/adventure/__init__.py
+++ b/worlds/adventure/__init__.py
@@ -15,7 +15,8 @@
from worlds.AutoWorld import WebWorld, World
from Fill import fill_restrictive
from worlds.generic.Rules import add_rule, set_rule
-from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
+from .Options import DragonRandoType, DifficultySwitchA, DifficultySwitchB, \
+ AdventureOptions
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \
AdventureAutoCollectLocation
from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max
@@ -109,11 +110,10 @@ class AdventureWorld(World):
game: ClassVar[str] = "Adventure"
web: ClassVar[WebWorld] = AdventureWeb()
- option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions
+ options_dataclass = AdventureOptions
settings: ClassVar[AdventureSettings]
item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()}
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
- data_version: ClassVar[int] = 1
required_client_version: Tuple[int, int, int] = (0, 3, 9)
def __init__(self, world: MultiWorld, player: int):
@@ -150,18 +150,18 @@ def generate_early(self) -> None:
bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21]
self.rom_name.extend([0] * (21 - len(self.rom_name)))
- self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value
- self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value
- self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value
- self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value
- self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value
- self.grundle_speed = self.multiworld.grundle_speed[self.player].value
- self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value
- self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value
- self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value
- self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value
- self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value
- self.start_castle = self.multiworld.start_castle[self.player].value
+ self.dragon_rando_type = self.options.dragon_rando_type.value
+ self.dragon_slay_check = self.options.dragon_slay_check.value
+ self.connector_multi_slot = self.options.connector_multi_slot.value
+ self.yorgle_speed = self.options.yorgle_speed.value
+ self.yorgle_min_speed = self.options.yorgle_min_speed.value
+ self.grundle_speed = self.options.grundle_speed.value
+ self.grundle_min_speed = self.options.grundle_min_speed.value
+ self.rhindle_speed = self.options.rhindle_speed.value
+ self.rhindle_min_speed = self.options.rhindle_min_speed.value
+ self.difficulty_switch_a = self.options.difficulty_switch_a.value
+ self.difficulty_switch_b = self.options.difficulty_switch_b.value
+ self.start_castle = self.options.start_castle.value
self.created_items = 0
if self.dragon_slay_check == 0:
@@ -228,7 +228,7 @@ def create_items(self) -> None:
extra_filler_count = num_locations - self.created_items
# traps would probably go here, if enabled
- freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value
+ freeincarnate_max = self.options.freeincarnate_max.value
actual_freeincarnates = min(extra_filler_count, freeincarnate_max)
self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)]
self.created_items += actual_freeincarnates
@@ -248,7 +248,7 @@ def create_dragon_slow_items(self, min_speed: int, speed: int, item_name: str, m
self.created_items += 1
def create_regions(self) -> None:
- create_regions(self.multiworld, self.player, self.dragon_rooms)
+ create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
set_rules = set_rules
@@ -271,7 +271,7 @@ def pre_fill(self):
overworld_locations_copy = overworld.locations.copy()
all_locations = self.multiworld.get_locations(self.player)
- locations_copy = all_locations.copy()
+ locations_copy = list(all_locations)
for loc in all_locations:
if loc.item is not None or loc.progress_type != LocationProgressType.DEFAULT:
locations_copy.remove(loc)
@@ -355,7 +355,7 @@ def generate_output(self, output_directory: str) -> None:
auto_collect_locations: [AdventureAutoCollectLocation] = []
local_item_to_location: {int, int} = {}
bat_no_touch_locs: [LocationData] = []
- bat_logic: int = self.multiworld.bat_logic[self.player].value
+ bat_logic: int = self.options.bat_logic.value
try:
rom_deltas: { int, int } = {}
self.place_dragons(rom_deltas)
@@ -371,8 +371,9 @@ def generate_output(self, output_directory: str) -> None:
if location.item.player == self.player and \
location.item.name == "nothing":
location_data = location_table[location.name]
+ room_id = location_data.get_random_room_id(self.random)
auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id,
- location_data.room_id))
+ room_id))
# standard Adventure items, which are placed in the rom
elif location.item.player == self.player and \
location.item.name != "nothing" and \
@@ -383,14 +384,18 @@ def generate_output(self, output_directory: str) -> None:
item_ram_address = item_ram_addresses[item_table[location.item.name].table_index]
item_position_data_start = item_position_table + item_ram_address - items_ram_start
location_data = location_table[location.name]
- room_x, room_y = location_data.get_position(self.multiworld.per_slot_randoms[self.player])
+ (room_id, room_x, room_y) = \
+ location_data.get_random_position(self.random)
if location_data.needs_bat_logic and bat_logic == 0x0:
copied_location = copy.copy(location_data)
copied_location.local_item = item_ram_address
+ copied_location.room_id = room_id
+ copied_location.room_x = room_x
+ copied_location.room_y = room_y
bat_no_touch_locs.append(copied_location)
del unplaced_local_items[location.item.name]
- rom_deltas[item_position_data_start] = location_data.room_id
+ rom_deltas[item_position_data_start] = room_id
rom_deltas[item_position_data_start + 1] = room_x
rom_deltas[item_position_data_start + 2] = room_y
local_item_to_location[item_table_offset] = self.location_name_to_id[location.name] \
@@ -398,20 +403,26 @@ def generate_output(self, output_directory: str) -> None:
# items from other worlds, and non-standard Adventure items handled by script, like difficulty switches
elif location.item.code is not None:
if location.item.code != nothing_item_id:
- location_data = location_table[location.name]
+ location_data = copy.copy(location_table[location.name])
+ (room_id, room_x, room_y) = \
+ location_data.get_random_position(self.random)
+ location_data.room_id = room_id
+ location_data.room_x = room_x
+ location_data.room_y = room_y
foreign_item_locations.append(location_data)
if location_data.needs_bat_logic and bat_logic == 0x0:
bat_no_touch_locs.append(location_data)
else:
location_data = location_table[location.name]
+ room_id = location_data.get_random_room_id(self.random)
auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id,
- location_data.room_id))
+ room_id))
# Adventure items that are in another world get put in an invalid room until needed
for unplaced_item_name, unplaced_item in unplaced_local_items.items():
item_position_data_start = get_item_position_data_start(unplaced_item.table_index)
rom_deltas[item_position_data_start] = 0xff
- if self.multiworld.connector_multi_slot[self.player].value:
+ if self.options.connector_multi_slot.value:
rom_deltas[connector_port_offset] = (self.player & 0xff)
else:
rom_deltas[connector_port_offset] = 0
diff --git a/worlds/adventure/docs/en_Adventure.md b/worlds/adventure/docs/en_Adventure.md
index c39e0f7d919d..f5216e9145b2 100644
--- a/worlds/adventure/docs/en_Adventure.md
+++ b/worlds/adventure/docs/en_Adventure.md
@@ -1,11 +1,11 @@
# Adventure
-## Where is the settings page?
-The [player settings page for Adventure](../player-settings) contains all the options you need to configure and export a config file.
+## Where is the options page?
+The [player options page for Adventure](../player-options) contains all the options you need to configure and export a config file.
## What does randomization do to this game?
Adventure items may be distributed into additional locations not possible in the vanilla Adventure randomizer. All
-Adventure items are added to the multiworld item pool. Depending on the settings, dragon locations may be randomized,
+Adventure items are added to the multiworld item pool. Depending on the `dragon_rando_type` value, dragon locations may be randomized,
slaying dragons may award items, difficulty switches may require items to unlock, and limited use 'freeincarnates'
can allow reincarnation without resurrecting dragons. Dragon speeds may also be randomized, and items may exist
to reduce their speeds.
@@ -15,7 +15,7 @@ Same as vanilla; Find the Enchanted Chalice and return it to the Yellow Castle
## Which items can be in another player's world?
All three keys, the chalice, the sword, the magnet, and the bridge can be found in another player's world. Depending on
-settings, dragon slowdowns, difficulty switch unlocks, and freeincarnates may also be found.
+options, dragon slowdowns, difficulty switch unlocks, and freeincarnates may also be found.
## What is considered a location check in Adventure?
Most areas in Adventure have one or more locations which can contain an Adventure item or an Archipelago item.
@@ -41,7 +41,7 @@ A message is shown in the client log. While empty handed, the player can press
order they were received. Once an item is retrieved this way, it cannot be retrieved again until pressing select to
return to the 'GO' screen or doing a hard reset, either one of which will reset all items to their original positions.
-## What are recommended settings to tweak for beginners to the rando?
+## What are recommended options to tweak for beginners to the rando?
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or
the credits room.
diff --git a/worlds/adventure/docs/setup_en.md b/worlds/adventure/docs/setup_en.md
index 10fc564aba51..060225e3971a 100644
--- a/worlds/adventure/docs/setup_en.md
+++ b/worlds/adventure/docs/setup_en.md
@@ -10,8 +10,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
- 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)
- (select `Adventure Client` during installation).
+- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
- An Adventure NTSC ROM file. The Archipelago community cannot provide these.
## Configuring BizHawk
@@ -42,9 +41,9 @@ an experience customized for their taste, and different players in the same mult
### Where do I get a YAML file?
-You can generate a yaml or download a template by visiting the [Adventure Settings Page](/games/Adventure/player-settings)
+You can generate a yaml or download a template by visiting the [Adventure Options Page](/games/Adventure/player-options)
-### What are recommended settings to tweak for beginners to the rando?
+### What are recommended options to tweak for beginners to the rando?
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or
the credits room.
diff --git a/worlds/adventure/docs/setup_fr.md b/worlds/adventure/docs/setup_fr.md
index 07881ce94da4..e8346fe6f088 100644
--- a/worlds/adventure/docs/setup_fr.md
+++ b/worlds/adventure/docs/setup_fr.md
@@ -42,7 +42,7 @@ une expérience personnalisée à leur goût, et différents joueurs dans le mê
### Où puis-je obtenir un fichier YAML ?
-Vous pouvez générer un yaml ou télécharger un modèle en visitant la [page des paramètres d'aventure](/games/Adventure/player-settings)
+Vous pouvez générer un yaml ou télécharger un modèle en visitant la [page des paramètres d'aventure](/games/Adventure/player-options)
### Quels sont les paramètres recommandés pour s'initier à la rando ?
Régler la difficulty_switch_a et réduire la vitesse des dragons rend les dragons plus faciles à éviter. Ajouter Calice Ã
@@ -72,4 +72,4 @@ configuré pour le faire automatiquement.
Pour connecter le client au multiserveur, mettez simplement `:` dans le champ de texte en haut et appuyez sur Entrée (si le
le serveur utilise un mot de passe, saisissez dans le champ de texte inférieur `/connect  : [mot de passe]`)
-Appuyez sur Réinitialiser et commencez à jouer
\ No newline at end of file
+Appuyez sur Réinitialiser et commencez à jouer
diff --git a/worlds/ahit/Client.py b/worlds/ahit/Client.py
new file mode 100644
index 000000000000..2cd67e468294
--- /dev/null
+++ b/worlds/ahit/Client.py
@@ -0,0 +1,232 @@
+import asyncio
+import Utils
+import websockets
+import functools
+from copy import deepcopy
+from typing import List, Any, Iterable
+from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem
+from MultiServer import Endpoint
+from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser
+
+DEBUG = False
+
+
+class AHITJSONToTextParser(JSONtoTextParser):
+ def _handle_color(self, node: JSONMessagePart):
+ return self._handle_text(node) # No colors for the in-game text
+
+
+class AHITCommandProcessor(ClientCommandProcessor):
+ def _cmd_ahit(self):
+ """Check AHIT Connection State"""
+ if isinstance(self.ctx, AHITContext):
+ logger.info(f"AHIT Status: {self.ctx.get_ahit_status()}")
+
+
+class AHITContext(CommonContext):
+ command_processor = AHITCommandProcessor
+ game = "A Hat in Time"
+
+ def __init__(self, server_address, password):
+ super().__init__(server_address, password)
+ self.proxy = None
+ self.proxy_task = None
+ self.gamejsontotext = AHITJSONToTextParser(self)
+ self.autoreconnect_task = None
+ self.endpoint = None
+ self.items_handling = 0b111
+ self.room_info = None
+ self.connected_msg = None
+ self.game_connected = False
+ self.awaiting_info = False
+ self.full_inventory: List[Any] = []
+ self.server_msgs: List[Any] = []
+
+ async def server_auth(self, password_requested: bool = False):
+ if password_requested and not self.password:
+ await super(AHITContext, self).server_auth(password_requested)
+
+ await self.get_username()
+ await self.send_connect()
+
+ def get_ahit_status(self) -> str:
+ if not self.is_proxy_connected():
+ return "Not connected to A Hat in Time"
+
+ return "Connected to A Hat in Time"
+
+ async def send_msgs_proxy(self, msgs: Iterable[dict]) -> bool:
+ """ `msgs` JSON serializable """
+ if not self.endpoint or not self.endpoint.socket.open or self.endpoint.socket.closed:
+ return False
+
+ if DEBUG:
+ logger.info(f"Outgoing message: {msgs}")
+
+ await self.endpoint.socket.send(msgs)
+ return True
+
+ async def disconnect(self, allow_autoreconnect: bool = False):
+ await super().disconnect(allow_autoreconnect)
+
+ async def disconnect_proxy(self):
+ if self.endpoint and not self.endpoint.socket.closed:
+ await self.endpoint.socket.close()
+ if self.proxy_task is not None:
+ await self.proxy_task
+
+ def is_connected(self) -> bool:
+ return self.server and self.server.socket.open
+
+ def is_proxy_connected(self) -> bool:
+ return self.endpoint and self.endpoint.socket.open
+
+ def on_print_json(self, args: dict):
+ text = self.gamejsontotext(deepcopy(args["data"]))
+ msg = {"cmd": "PrintJSON", "data": [{"text": text}], "type": "Chat"}
+ self.server_msgs.append(encode([msg]))
+
+ if self.ui:
+ self.ui.print_json(args["data"])
+ else:
+ text = self.jsontotextparser(args["data"])
+ logger.info(text)
+
+ def update_items(self):
+ # just to be safe - we might still have an inventory from a different room
+ if not self.is_connected():
+ return
+
+ self.server_msgs.append(encode([{"cmd": "ReceivedItems", "index": 0, "items": self.full_inventory}]))
+
+ def on_package(self, cmd: str, args: dict):
+ if cmd == "Connected":
+ self.connected_msg = encode([args])
+ if self.awaiting_info:
+ self.server_msgs.append(self.room_info)
+ self.update_items()
+ self.awaiting_info = False
+
+ elif cmd == "ReceivedItems":
+ if args["index"] == 0:
+ self.full_inventory.clear()
+
+ for item in args["items"]:
+ self.full_inventory.append(NetworkItem(*item))
+
+ self.server_msgs.append(encode([args]))
+
+ elif cmd == "RoomInfo":
+ self.seed_name = args["seed_name"]
+ self.room_info = encode([args])
+
+ else:
+ if cmd != "PrintJSON":
+ self.server_msgs.append(encode([args]))
+
+ def run_gui(self):
+ from kvui import GameManager
+
+ class AHITManager(GameManager):
+ logging_pairs = [
+ ("Client", "Archipelago")
+ ]
+ base_title = "Archipelago A Hat in Time Client"
+
+ self.ui = AHITManager(self)
+ self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
+
+
+async def proxy(websocket, path: str = "/", ctx: AHITContext = None):
+ ctx.endpoint = Endpoint(websocket)
+ try:
+ await on_client_connected(ctx)
+
+ if ctx.is_proxy_connected():
+ async for data in websocket:
+ if DEBUG:
+ logger.info(f"Incoming message: {data}")
+
+ for msg in decode(data):
+ if msg["cmd"] == "Connect":
+ # Proxy is connecting, make sure it is valid
+ if msg["game"] != "A Hat in Time":
+ logger.info("Aborting proxy connection: game is not A Hat in Time")
+ await ctx.disconnect_proxy()
+ break
+
+ if ctx.seed_name:
+ seed_name = msg.get("seed_name", "")
+ if seed_name != "" and seed_name != ctx.seed_name:
+ logger.info("Aborting proxy connection: seed mismatch from save file")
+ logger.info(f"Expected: {ctx.seed_name}, got: {seed_name}")
+ text = encode([{"cmd": "PrintJSON",
+ "data": [{"text": "Connection aborted - save file to seed mismatch"}]}])
+ await ctx.send_msgs_proxy(text)
+ await ctx.disconnect_proxy()
+ break
+
+ if ctx.connected_msg and ctx.is_connected():
+ await ctx.send_msgs_proxy(ctx.connected_msg)
+ ctx.update_items()
+ continue
+
+ if not ctx.is_proxy_connected():
+ break
+
+ await ctx.send_msgs([msg])
+
+ except Exception as e:
+ if not isinstance(e, websockets.WebSocketException):
+ logger.exception(e)
+ finally:
+ await ctx.disconnect_proxy()
+
+
+async def on_client_connected(ctx: AHITContext):
+ if ctx.room_info and ctx.is_connected():
+ await ctx.send_msgs_proxy(ctx.room_info)
+ else:
+ ctx.awaiting_info = True
+
+
+async def proxy_loop(ctx: AHITContext):
+ try:
+ while not ctx.exit_event.is_set():
+ if len(ctx.server_msgs) > 0:
+ for msg in ctx.server_msgs:
+ await ctx.send_msgs_proxy(msg)
+
+ ctx.server_msgs.clear()
+ await asyncio.sleep(0.1)
+ except Exception as e:
+ logger.exception(e)
+ logger.info("Aborting AHIT Proxy Client due to errors")
+
+
+def launch():
+ async def main():
+ parser = get_base_parser()
+ args = parser.parse_args()
+
+ ctx = AHITContext(args.connect, args.password)
+ logger.info("Starting A Hat in Time proxy server")
+ ctx.proxy = websockets.serve(functools.partial(proxy, ctx=ctx),
+ host="localhost", port=11311, ping_timeout=999999, ping_interval=999999)
+ ctx.proxy_task = asyncio.create_task(proxy_loop(ctx), name="ProxyLoop")
+
+ if gui_enabled:
+ ctx.run_gui()
+ ctx.run_cli()
+
+ await ctx.proxy
+ await ctx.proxy_task
+ await ctx.exit_event.wait()
+
+ Utils.init_logging("AHITClient")
+ # options = Utils.get_options()
+
+ import colorama
+ colorama.init()
+ asyncio.run(main())
+ colorama.deinit()
diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py
new file mode 100644
index 000000000000..ef74cadcaa53
--- /dev/null
+++ b/worlds/ahit/DeathWishLocations.py
@@ -0,0 +1,243 @@
+from .Types import HatInTimeLocation, HatInTimeItem
+from .Regions import create_region
+from BaseClasses import Region, LocationProgressType, ItemClassification
+from worlds.generic.Rules import add_rule
+from typing import List, TYPE_CHECKING
+from .Locations import death_wishes
+from .Options import EndGoal
+
+if TYPE_CHECKING:
+ from . import HatInTimeWorld
+
+
+dw_prereqs = {
+ "So You're Back From Outer Space": ["Beat the Heat"],
+ "Snatcher's Hit List": ["Beat the Heat"],
+ "Snatcher Coins in Mafia Town": ["So You're Back From Outer Space"],
+ "Rift Collapse: Mafia of Cooks": ["So You're Back From Outer Space"],
+ "Collect-a-thon": ["So You're Back From Outer Space"],
+ "She Speedran from Outer Space": ["Rift Collapse: Mafia of Cooks"],
+ "Mafia's Jumps": ["She Speedran from Outer Space"],
+ "Vault Codes in the Wind": ["Collect-a-thon", "She Speedran from Outer Space"],
+ "Encore! Encore!": ["Collect-a-thon"],
+
+ "Security Breach": ["Beat the Heat"],
+ "Rift Collapse: Dead Bird Studio": ["Security Breach"],
+ "The Great Big Hootenanny": ["Security Breach"],
+ "10 Seconds until Self-Destruct": ["The Great Big Hootenanny"],
+ "Killing Two Birds": ["Rift Collapse: Dead Bird Studio", "10 Seconds until Self-Destruct"],
+ "Community Rift: Rhythm Jump Studio": ["10 Seconds until Self-Destruct"],
+ "Snatcher Coins in Battle of the Birds": ["The Great Big Hootenanny"],
+ "Zero Jumps": ["Rift Collapse: Dead Bird Studio"],
+ "Snatcher Coins in Nyakuza Metro": ["Killing Two Birds"],
+
+ "Speedrun Well": ["Beat the Heat"],
+ "Rift Collapse: Sleepy Subcon": ["Speedrun Well"],
+ "Boss Rush": ["Speedrun Well"],
+ "Quality Time with Snatcher": ["Rift Collapse: Sleepy Subcon"],
+ "Breaching the Contract": ["Boss Rush", "Quality Time with Snatcher"],
+ "Community Rift: Twilight Travels": ["Quality Time with Snatcher"],
+ "Snatcher Coins in Subcon Forest": ["Rift Collapse: Sleepy Subcon"],
+
+ "Bird Sanctuary": ["Beat the Heat"],
+ "Snatcher Coins in Alpine Skyline": ["Bird Sanctuary"],
+ "Wound-Up Windmill": ["Bird Sanctuary"],
+ "Rift Collapse: Alpine Skyline": ["Bird Sanctuary"],
+ "Camera Tourist": ["Rift Collapse: Alpine Skyline"],
+ "Community Rift: The Mountain Rift": ["Rift Collapse: Alpine Skyline"],
+ "The Illness has Speedrun": ["Rift Collapse: Alpine Skyline", "Wound-Up Windmill"],
+
+ "The Mustache Gauntlet": ["Wound-Up Windmill"],
+ "No More Bad Guys": ["The Mustache Gauntlet"],
+ "Seal the Deal": ["Encore! Encore!", "Killing Two Birds",
+ "Breaching the Contract", "No More Bad Guys"],
+
+ "Rift Collapse: Deep Sea": ["Rift Collapse: Mafia of Cooks", "Rift Collapse: Dead Bird Studio",
+ "Rift Collapse: Sleepy Subcon", "Rift Collapse: Alpine Skyline"],
+
+ "Cruisin' for a Bruisin'": ["Rift Collapse: Deep Sea"],
+}
+
+dw_candles = [
+ "Snatcher's Hit List",
+ "Zero Jumps",
+ "Camera Tourist",
+ "Snatcher Coins in Mafia Town",
+ "Snatcher Coins in Battle of the Birds",
+ "Snatcher Coins in Subcon Forest",
+ "Snatcher Coins in Alpine Skyline",
+ "Snatcher Coins in Nyakuza Metro",
+]
+
+annoying_dws = [
+ "Vault Codes in the Wind",
+ "Boss Rush",
+ "Camera Tourist",
+ "The Mustache Gauntlet",
+ "Rift Collapse: Deep Sea",
+ "Cruisin' for a Bruisin'",
+ "Seal the Deal", # Non-excluded if goal
+]
+
+# includes the above as well
+annoying_bonuses = [
+ "So You're Back From Outer Space",
+ "Encore! Encore!",
+ "Snatcher's Hit List",
+ "Vault Codes in the Wind",
+ "10 Seconds until Self-Destruct",
+ "Killing Two Birds",
+ "Zero Jumps",
+ "Boss Rush",
+ "Bird Sanctuary",
+ "The Mustache Gauntlet",
+ "Wound-Up Windmill",
+ "Camera Tourist",
+ "Rift Collapse: Deep Sea",
+ "Cruisin' for a Bruisin'",
+ "Seal the Deal",
+]
+
+dw_classes = {
+ "Beat the Heat": "Hat_SnatcherContract_DeathWish_HeatingUpHarder",
+ "So You're Back From Outer Space": "Hat_SnatcherContract_DeathWish_BackFromSpace",
+ "Snatcher's Hit List": "Hat_SnatcherContract_DeathWish_KillEverybody",
+ "Collect-a-thon": "Hat_SnatcherContract_DeathWish_PonFrenzy",
+ "Rift Collapse: Mafia of Cooks": "Hat_SnatcherContract_DeathWish_RiftCollapse_MafiaTown",
+ "Encore! Encore!": "Hat_SnatcherContract_DeathWish_MafiaBossEX",
+ "She Speedran from Outer Space": "Hat_SnatcherContract_DeathWish_Speedrun_MafiaAlien",
+ "Mafia's Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses_MafiaAlien",
+ "Vault Codes in the Wind": "Hat_SnatcherContract_DeathWish_MovingVault",
+ "Snatcher Coins in Mafia Town": "Hat_SnatcherContract_DeathWish_Tokens_MafiaTown",
+
+ "Security Breach": "Hat_SnatcherContract_DeathWish_DeadBirdStudioMoreGuards",
+ "The Great Big Hootenanny": "Hat_SnatcherContract_DeathWish_DifficultParade",
+ "Rift Collapse: Dead Bird Studio": "Hat_SnatcherContract_DeathWish_RiftCollapse_Birds",
+ "10 Seconds until Self-Destruct": "Hat_SnatcherContract_DeathWish_TrainRushShortTime",
+ "Killing Two Birds": "Hat_SnatcherContract_DeathWish_BirdBossEX",
+ "Snatcher Coins in Battle of the Birds": "Hat_SnatcherContract_DeathWish_Tokens_Birds",
+ "Zero Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses",
+
+ "Speedrun Well": "Hat_SnatcherContract_DeathWish_Speedrun_SubWell",
+ "Rift Collapse: Sleepy Subcon": "Hat_SnatcherContract_DeathWish_RiftCollapse_Subcon",
+ "Boss Rush": "Hat_SnatcherContract_DeathWish_BossRush",
+ "Quality Time with Snatcher": "Hat_SnatcherContract_DeathWish_SurvivalOfTheFittest",
+ "Breaching the Contract": "Hat_SnatcherContract_DeathWish_SnatcherEX",
+ "Snatcher Coins in Subcon Forest": "Hat_SnatcherContract_DeathWish_Tokens_Subcon",
+
+ "Bird Sanctuary": "Hat_SnatcherContract_DeathWish_NiceBirdhouse",
+ "Rift Collapse: Alpine Skyline": "Hat_SnatcherContract_DeathWish_RiftCollapse_Alps",
+ "Wound-Up Windmill": "Hat_SnatcherContract_DeathWish_FastWindmill",
+ "The Illness has Speedrun": "Hat_SnatcherContract_DeathWish_Speedrun_Illness",
+ "Snatcher Coins in Alpine Skyline": "Hat_SnatcherContract_DeathWish_Tokens_Alps",
+ "Camera Tourist": "Hat_SnatcherContract_DeathWish_CameraTourist_1",
+
+ "The Mustache Gauntlet": "Hat_SnatcherContract_DeathWish_HardCastle",
+ "No More Bad Guys": "Hat_SnatcherContract_DeathWish_MuGirlEX",
+
+ "Seal the Deal": "Hat_SnatcherContract_DeathWish_BossRushEX",
+ "Rift Collapse: Deep Sea": "Hat_SnatcherContract_DeathWish_RiftCollapse_Cruise",
+ "Cruisin' for a Bruisin'": "Hat_SnatcherContract_DeathWish_EndlessTasks",
+
+ "Community Rift: Rhythm Jump Studio": "Hat_SnatcherContract_DeathWish_CommunityRift_RhythmJump",
+ "Community Rift: Twilight Travels": "Hat_SnatcherContract_DeathWish_CommunityRift_TwilightTravels",
+ "Community Rift: The Mountain Rift": "Hat_SnatcherContract_DeathWish_CommunityRift_MountainRift",
+
+ "Snatcher Coins in Nyakuza Metro": "Hat_SnatcherContract_DeathWish_Tokens_Metro",
+}
+
+
+def create_dw_regions(world: "HatInTimeWorld"):
+ if world.options.DWExcludeAnnoyingContracts:
+ for name in annoying_dws:
+ world.excluded_dws.append(name)
+
+ if not world.options.DWEnableBonus or world.options.DWAutoCompleteBonuses:
+ for name in death_wishes:
+ world.excluded_bonuses.append(name)
+ elif world.options.DWExcludeAnnoyingBonuses:
+ for name in annoying_bonuses:
+ world.excluded_bonuses.append(name)
+
+ if world.options.DWExcludeCandles:
+ for name in dw_candles:
+ if name not in world.excluded_dws:
+ world.excluded_dws.append(name)
+
+ spaceship = world.multiworld.get_region("Spaceship", world.player)
+ dw_map: Region = create_region(world, "Death Wish Map")
+ entrance = spaceship.connect(dw_map, "-> Death Wish Map")
+ add_rule(entrance, lambda state: state.has("Time Piece", world.player, world.options.DWTimePieceRequirement))
+
+ if world.options.DWShuffle:
+ # Connect Death Wishes randomly to one another in a linear sequence
+ dw_list: List[str] = []
+ for name in death_wishes.keys():
+ # Don't shuffle excluded or invalid Death Wishes
+ if not world.is_dlc2() and name == "Snatcher Coins in Nyakuza Metro" or world.is_dw_excluded(name):
+ continue
+
+ dw_list.append(name)
+
+ world.random.shuffle(dw_list)
+ count = world.random.randint(world.options.DWShuffleCountMin.value, world.options.DWShuffleCountMax.value)
+ dw_shuffle: List[str] = []
+ total = min(len(dw_list), count)
+ for i in range(total):
+ dw_shuffle.append(dw_list[i])
+
+ # Seal the Deal is always last if it's the goal
+ if world.options.EndGoal == EndGoal.option_seal_the_deal:
+ if "Seal the Deal" in dw_shuffle:
+ dw_shuffle.remove("Seal the Deal")
+
+ dw_shuffle.append("Seal the Deal")
+
+ world.dw_shuffle = dw_shuffle
+ prev_dw = dw_map
+ for death_wish_name in dw_shuffle:
+ dw = create_region(world, death_wish_name)
+ prev_dw.connect(dw)
+ create_dw_locations(world, dw)
+ prev_dw = dw
+ else:
+ # DWShuffle is disabled, use vanilla connections
+ for key in death_wishes.keys():
+ if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2():
+ world.excluded_dws.append(key)
+ continue
+
+ dw = create_region(world, key)
+ if key == "Beat the Heat":
+ dw_map.connect(dw, f"{dw_map.name} -> Beat the Heat")
+ elif key in dw_prereqs.keys():
+ for name in dw_prereqs[key]:
+ parent = world.multiworld.get_region(name, world.player)
+ parent.connect(dw, f"{parent.name} -> {key}")
+
+ create_dw_locations(world, dw)
+
+
+def create_dw_locations(world: "HatInTimeWorld", dw: Region):
+ loc_id = death_wishes[dw.name]
+ main_objective = HatInTimeLocation(world.player, f"{dw.name} - Main Objective", loc_id, dw)
+ full_clear = HatInTimeLocation(world.player, f"{dw.name} - All Clear", loc_id + 1, dw)
+ main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {dw.name}", None, dw)
+ bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {dw.name}", None, dw)
+ main_stamp.show_in_spoiler = False
+ bonus_stamps.show_in_spoiler = False
+ dw.locations.append(main_stamp)
+ dw.locations.append(bonus_stamps)
+ main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {dw.name}",
+ ItemClassification.progression, None, world.player))
+ bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamp - {dw.name}",
+ ItemClassification.progression, None, world.player))
+
+ if dw.name in world.excluded_dws:
+ main_objective.progress_type = LocationProgressType.EXCLUDED
+ full_clear.progress_type = LocationProgressType.EXCLUDED
+ elif world.is_bonus_excluded(dw.name):
+ full_clear.progress_type = LocationProgressType.EXCLUDED
+
+ dw.locations.append(main_objective)
+ dw.locations.append(full_clear)
diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py
new file mode 100644
index 000000000000..1432ef5c0d75
--- /dev/null
+++ b/worlds/ahit/DeathWishRules.py
@@ -0,0 +1,462 @@
+from worlds.AutoWorld import CollectionState
+from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, get_difficulty, has_paintings
+from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData, HitType
+from .DeathWishLocations import dw_prereqs, dw_candles
+from BaseClasses import Entrance, Location, ItemClassification
+from worlds.generic.Rules import add_rule, set_rule
+from typing import List, Callable, TYPE_CHECKING
+from .Locations import death_wishes
+from .Options import EndGoal
+
+if TYPE_CHECKING:
+ from . import HatInTimeWorld
+
+
+# Any speedruns expect the player to have Sprint Hat
+dw_requirements = {
+ "Beat the Heat": LocData(hit_type=HitType.umbrella),
+ "So You're Back From Outer Space": LocData(hookshot=True),
+ "Mafia's Jumps": LocData(required_hats=[HatType.ICE]),
+ "Vault Codes in the Wind": LocData(required_hats=[HatType.SPRINT]),
+
+ "Security Breach": LocData(hit_type=HitType.umbrella_or_brewing),
+ "10 Seconds until Self-Destruct": LocData(hookshot=True),
+ "Community Rift: Rhythm Jump Studio": LocData(required_hats=[HatType.ICE]),
+
+ "Speedrun Well": LocData(hookshot=True, hit_type=HitType.umbrella_or_brewing),
+ "Boss Rush": LocData(hit_type=HitType.umbrella, hookshot=True),
+ "Community Rift: Twilight Travels": LocData(hookshot=True, required_hats=[HatType.DWELLER]),
+
+ "Bird Sanctuary": LocData(hookshot=True),
+ "Wound-Up Windmill": LocData(hookshot=True),
+ "The Illness has Speedrun": LocData(hookshot=True),
+ "Community Rift: The Mountain Rift": LocData(hookshot=True, required_hats=[HatType.DWELLER]),
+ "Camera Tourist": LocData(misc_required=["Camera Badge"]),
+
+ "The Mustache Gauntlet": LocData(hookshot=True, required_hats=[HatType.DWELLER]),
+
+ "Rift Collapse: Deep Sea": LocData(hookshot=True),
+}
+
+# Includes main objective requirements
+dw_bonus_requirements = {
+ # Some One-Hit Hero requirements need badge pins as well because of Hookshot
+ "So You're Back From Outer Space": LocData(required_hats=[HatType.SPRINT]),
+ "Encore! Encore!": LocData(misc_required=["One-Hit Hero Badge"]),
+
+ "10 Seconds until Self-Destruct": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]),
+
+ "Boss Rush": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]),
+ "Community Rift: Twilight Travels": LocData(required_hats=[HatType.BREWING]),
+
+ "Bird Sanctuary": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"], required_hats=[HatType.DWELLER]),
+ "Wound-Up Windmill": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]),
+ "The Illness has Speedrun": LocData(required_hats=[HatType.SPRINT]),
+
+ "The Mustache Gauntlet": LocData(required_hats=[HatType.ICE]),
+
+ "Rift Collapse: Deep Sea": LocData(required_hats=[HatType.DWELLER]),
+}
+
+dw_stamp_costs = {
+ "So You're Back From Outer Space": 2,
+ "Collect-a-thon": 5,
+ "She Speedran from Outer Space": 8,
+ "Encore! Encore!": 10,
+
+ "Security Breach": 4,
+ "The Great Big Hootenanny": 7,
+ "10 Seconds until Self-Destruct": 15,
+ "Killing Two Birds": 25,
+ "Snatcher Coins in Nyakuza Metro": 30,
+
+ "Speedrun Well": 10,
+ "Boss Rush": 15,
+ "Quality Time with Snatcher": 20,
+ "Breaching the Contract": 40,
+
+ "Bird Sanctuary": 15,
+ "Wound-Up Windmill": 30,
+ "The Illness has Speedrun": 35,
+
+ "The Mustache Gauntlet": 35,
+ "No More Bad Guys": 50,
+ "Seal the Deal": 70,
+}
+
+required_snatcher_coins = {
+ "Snatcher Coins in Mafia Town": ["Snatcher Coin - Top of HQ", "Snatcher Coin - Top of Tower",
+ "Snatcher Coin - Under Ruined Tower"],
+
+ "Snatcher Coins in Battle of the Birds": ["Snatcher Coin - Top of Red House", "Snatcher Coin - Train Rush",
+ "Snatcher Coin - Picture Perfect"],
+
+ "Snatcher Coins in Subcon Forest": ["Snatcher Coin - Swamp Tree", "Snatcher Coin - Manor Roof",
+ "Snatcher Coin - Giant Time Piece"],
+
+ "Snatcher Coins in Alpine Skyline": ["Snatcher Coin - Goat Village Top", "Snatcher Coin - Lava Cake",
+ "Snatcher Coin - Windmill"],
+
+ "Snatcher Coins in Nyakuza Metro": ["Snatcher Coin - Green Clean Tower", "Snatcher Coin - Bluefin Cat Train",
+ "Snatcher Coin - Pink Paw Fence"],
+}
+
+
+def set_dw_rules(world: "HatInTimeWorld"):
+ if "Snatcher's Hit List" not in world.excluded_dws or "Camera Tourist" not in world.excluded_dws:
+ set_enemy_rules(world)
+
+ dw_list: List[str] = []
+ if world.options.DWShuffle:
+ dw_list = world.dw_shuffle
+ else:
+ for name in death_wishes.keys():
+ dw_list.append(name)
+
+ for name in dw_list:
+ if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2():
+ continue
+
+ dw = world.multiworld.get_region(name, world.player)
+ if not world.options.DWShuffle and name in dw_stamp_costs.keys():
+ for entrance in dw.entrances:
+ add_rule(entrance, lambda state, n=name: state.has("Stamps", world.player, dw_stamp_costs[n]))
+
+ main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player)
+ all_clear = world.multiworld.get_location(f"{name} - All Clear", world.player)
+ main_stamp = world.multiworld.get_location(f"Main Stamp - {name}", world.player)
+ bonus_stamps = world.multiworld.get_location(f"Bonus Stamps - {name}", world.player)
+ if not world.options.DWEnableBonus:
+ # place nothing, but let the locations exist still, so we can use them for bonus stamp rules
+ all_clear.address = None
+ all_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, world.player))
+ all_clear.show_in_spoiler = False
+
+ # No need for rules if excluded - stamps will be auto-granted
+ if world.is_dw_excluded(name):
+ continue
+
+ modify_dw_rules(world, name)
+ add_dw_rules(world, main_objective)
+ add_dw_rules(world, all_clear)
+ add_rule(main_stamp, main_objective.access_rule)
+ add_rule(all_clear, main_objective.access_rule)
+ # Only set bonus stamp rules if we don't auto complete bonuses
+ if not world.options.DWAutoCompleteBonuses and not world.is_bonus_excluded(all_clear.name):
+ add_rule(bonus_stamps, all_clear.access_rule)
+
+ if world.options.DWShuffle:
+ for i in range(len(world.dw_shuffle)-1):
+ name = world.dw_shuffle[i+1]
+ prev_dw = world.multiworld.get_region(world.dw_shuffle[i], world.player)
+ entrance = world.multiworld.get_entrance(f"{prev_dw.name} -> {name}", world.player)
+ add_rule(entrance, lambda state, n=prev_dw.name: state.has(f"1 Stamp - {n}", world.player))
+ else:
+ for key, reqs in dw_prereqs.items():
+ if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2():
+ continue
+
+ access_rules: List[Callable[[CollectionState], bool]] = []
+ entrances: List[Entrance] = []
+
+ for parent in reqs:
+ entrance = world.multiworld.get_entrance(f"{parent} -> {key}", world.player)
+ entrances.append(entrance)
+
+ if not world.is_dw_excluded(parent):
+ access_rules.append(lambda state, n=parent: state.has(f"1 Stamp - {n}", world.player))
+
+ for entrance in entrances:
+ for rule in access_rules:
+ add_rule(entrance, rule)
+
+ if world.options.EndGoal == EndGoal.option_seal_the_deal:
+ world.multiworld.completion_condition[world.player] = lambda state: \
+ state.has("1 Stamp - Seal the Deal", world.player)
+
+
+def add_dw_rules(world: "HatInTimeWorld", loc: Location):
+ bonus: bool = "All Clear" in loc.name
+ if not bonus:
+ data = dw_requirements.get(loc.parent_region.name)
+ else:
+ data = dw_bonus_requirements.get(loc.parent_region.name)
+
+ if data is None:
+ return
+
+ if data.hookshot:
+ add_rule(loc, lambda state: can_use_hookshot(state, world))
+
+ for hat in data.required_hats:
+ add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h))
+
+ for misc in data.misc_required:
+ add_rule(loc, lambda state, item=misc: state.has(item, world.player))
+
+ if data.paintings > 0 and world.options.ShuffleSubconPaintings:
+ add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
+
+ if data.hit_type is not HitType.none and world.options.UmbrellaLogic:
+ if data.hit_type == HitType.umbrella:
+ add_rule(loc, lambda state: state.has("Umbrella", world.player))
+
+ elif data.hit_type == HitType.umbrella_or_brewing:
+ add_rule(loc, lambda state: state.has("Umbrella", world.player)
+ or can_use_hat(state, world, HatType.BREWING))
+
+ elif data.hit_type == HitType.dweller_bell:
+ add_rule(loc, lambda state: state.has("Umbrella", world.player)
+ or can_use_hat(state, world, HatType.BREWING)
+ or can_use_hat(state, world, HatType.DWELLER))
+
+
+def modify_dw_rules(world: "HatInTimeWorld", name: str):
+ difficulty: Difficulty = get_difficulty(world)
+ main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player)
+ full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player)
+
+ if name == "The Illness has Speedrun":
+ # All stamps with hookshot only in Expert
+ if difficulty >= Difficulty.EXPERT:
+ set_rule(full_clear, lambda state: True)
+ else:
+ add_rule(main_objective, lambda state: state.has("Umbrella", world.player))
+
+ elif name == "The Mustache Gauntlet":
+ add_rule(main_objective, lambda state: state.has("Umbrella", world.player)
+ or can_use_hat(state, world, HatType.ICE) or can_use_hat(state, world, HatType.BREWING))
+
+ elif name == "Vault Codes in the Wind":
+ # Sprint is normally expected here
+ if difficulty >= Difficulty.HARD:
+ set_rule(main_objective, lambda state: True)
+
+ elif name == "Speedrun Well":
+ # All stamps with nothing :)
+ if difficulty >= Difficulty.EXPERT:
+ set_rule(main_objective, lambda state: True)
+
+ elif name == "Mafia's Jumps":
+ if difficulty >= Difficulty.HARD:
+ set_rule(main_objective, lambda state: True)
+ set_rule(full_clear, lambda state: True)
+
+ elif name == "So You're Back from Outer Space":
+ # Without Hookshot
+ if difficulty >= Difficulty.HARD:
+ set_rule(main_objective, lambda state: True)
+
+ elif name == "Wound-Up Windmill":
+ # No badge pin required. Player can switch to One Hit Hero after the checkpoint and do level without it.
+ if difficulty >= Difficulty.MODERATE:
+ set_rule(full_clear, lambda state: can_use_hookshot(state, world)
+ and state.has("One-Hit Hero Badge", world.player))
+
+ if name in dw_candles:
+ set_candle_dw_rules(name, world)
+
+
+def set_candle_dw_rules(name: str, world: "HatInTimeWorld"):
+ main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player)
+ full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player)
+
+ if name == "Zero Jumps":
+ add_rule(main_objective, lambda state: state.has("Zero Jumps", world.player))
+ add_rule(full_clear, lambda state: state.has("Zero Jumps", world.player, 4)
+ and state.has("Train Rush (Zero Jumps)", world.player) and can_use_hat(state, world, HatType.ICE))
+
+ # No Ice Hat/painting required in Expert for Toilet Zero Jumps
+ # This painting wall can only be skipped via cherry hover.
+ if get_difficulty(world) < Difficulty.EXPERT or world.options.NoPaintingSkips:
+ set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player),
+ lambda state: can_use_hookshot(state, world) and can_hit(state, world)
+ and has_paintings(state, world, 1, False))
+ else:
+ set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player),
+ lambda state: can_use_hookshot(state, world) and can_hit(state, world))
+
+ set_rule(world.multiworld.get_location("Contractual Obligations (Zero Jumps)", world.player),
+ lambda state: has_paintings(state, world, 1, False))
+
+ elif name == "Snatcher's Hit List":
+ add_rule(main_objective, lambda state: state.has("Mafia Goon", world.player))
+ add_rule(full_clear, lambda state: state.has("Enemy", world.player, 12))
+
+ elif name == "Camera Tourist":
+ add_rule(main_objective, lambda state: state.has("Enemy", world.player, 8))
+ add_rule(full_clear, lambda state: state.has("Boss", world.player, 6)
+ and state.has("Triple Enemy Photo", world.player))
+
+ elif "Snatcher Coins" in name:
+ coins: List[str] = []
+ for coin in required_snatcher_coins[name]:
+ coins.append(coin)
+ add_rule(full_clear, lambda state, c=coin: state.has(c, world.player))
+
+ # any coin works for the main objective
+ add_rule(main_objective, lambda state: state.has(coins[0], world.player)
+ or state.has(coins[1], world.player)
+ or state.has(coins[2], world.player))
+
+
+def create_enemy_events(world: "HatInTimeWorld"):
+ no_tourist = "Camera Tourist" in world.excluded_dws
+ for enemy, regions in hit_list.items():
+ if no_tourist and enemy in bosses:
+ continue
+
+ for area in regions:
+ if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1():
+ continue
+
+ if area == "Time Rift - Tour" and (not world.is_dlc1() or world.options.ExcludeTour):
+ continue
+
+ if area == "Bluefin Tunnel" and not world.is_dlc2():
+ continue
+
+ if world.options.DWShuffle and area in death_wishes.keys() and area not in world.dw_shuffle:
+ continue
+
+ region = world.multiworld.get_region(area, world.player)
+ event = HatInTimeLocation(world.player, f"{enemy} - {area}", None, region)
+ event.place_locked_item(HatInTimeItem(enemy, ItemClassification.progression, None, world.player))
+ region.locations.append(event)
+ event.show_in_spoiler = False
+
+ for name in triple_enemy_locations:
+ if name == "Time Rift - Tour" and (not world.is_dlc1() or world.options.ExcludeTour):
+ continue
+
+ if world.options.DWShuffle and name in death_wishes.keys() and name not in world.dw_shuffle:
+ continue
+
+ region = world.multiworld.get_region(name, world.player)
+ event = HatInTimeLocation(world.player, f"Triple Enemy Photo - {name}", None, region)
+ event.place_locked_item(HatInTimeItem("Triple Enemy Photo", ItemClassification.progression, None, world.player))
+ region.locations.append(event)
+ event.show_in_spoiler = False
+ if name == "The Mustache Gauntlet":
+ add_rule(event, lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER))
+
+
+def set_enemy_rules(world: "HatInTimeWorld"):
+ no_tourist = "Camera Tourist" in world.excluded_dws or "Camera Tourist" in world.excluded_bonuses
+
+ for enemy, regions in hit_list.items():
+ if no_tourist and enemy in bosses:
+ continue
+
+ for area in regions:
+ if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1():
+ continue
+
+ if area == "Time Rift - Tour" and (not world.is_dlc1() or world.options.ExcludeTour):
+ continue
+
+ if area == "Bluefin Tunnel" and not world.is_dlc2():
+ continue
+
+ if world.options.DWShuffle and area in death_wishes and area not in world.dw_shuffle:
+ continue
+
+ event = world.multiworld.get_location(f"{enemy} - {area}", world.player)
+
+ if enemy == "Toxic Flower":
+ add_rule(event, lambda state: can_use_hookshot(state, world))
+
+ if area == "The Illness has Spread":
+ add_rule(event, lambda state: not zipline_logic(world) or
+ state.has("Zipline Unlock - The Birdhouse Path", world.player)
+ or state.has("Zipline Unlock - The Lava Cake Path", world.player)
+ or state.has("Zipline Unlock - The Windmill Path", world.player))
+
+ elif enemy == "Director":
+ if area == "Dead Bird Studio Basement":
+ add_rule(event, lambda state: can_use_hookshot(state, world))
+
+ elif enemy == "Snatcher" or enemy == "Mustache Girl":
+ if area == "Boss Rush":
+ # need to be able to kill toilet and snatcher
+ add_rule(event, lambda state: can_hit(state, world) and can_use_hookshot(state, world))
+ if enemy == "Mustache Girl":
+ add_rule(event, lambda state: can_hit(state, world, True) and can_use_hookshot(state, world))
+
+ elif area == "The Finale" and enemy == "Mustache Girl":
+ add_rule(event, lambda state: can_use_hookshot(state, world)
+ and can_use_hat(state, world, HatType.DWELLER))
+
+ elif enemy == "Shock Squid" or enemy == "Ninja Cat":
+ if area == "Time Rift - Deep Sea":
+ add_rule(event, lambda state: can_use_hookshot(state, world))
+
+
+# Enemies for Snatcher's Hit List/Camera Tourist, and where to find them
+hit_list = {
+ "Mafia Goon": ["Mafia Town Area", "Time Rift - Mafia of Cooks", "Time Rift - Tour",
+ "Bon Voyage!", "The Mustache Gauntlet", "Rift Collapse: Mafia of Cooks",
+ "So You're Back From Outer Space"],
+
+ "Sleepy Raccoon": ["She Came from Outer Space", "Down with the Mafia!", "The Twilight Bell",
+ "She Speedran from Outer Space", "Mafia's Jumps", "The Mustache Gauntlet",
+ "Time Rift - Sleepy Subcon", "Rift Collapse: Sleepy Subcon"],
+
+ "UFO": ["Picture Perfect", "So You're Back From Outer Space", "Community Rift: Rhythm Jump Studio"],
+
+ "Rat": ["Down with the Mafia!", "Bluefin Tunnel"],
+
+ "Shock Squid": ["Bon Voyage!", "Time Rift - Sleepy Subcon", "Time Rift - Deep Sea",
+ "Rift Collapse: Sleepy Subcon"],
+
+ "Shromb Egg": ["The Birdhouse", "Bird Sanctuary"],
+
+ "Spider": ["Subcon Forest Area", "The Mustache Gauntlet", "Speedrun Well",
+ "The Lava Cake", "The Windmill"],
+
+ "Crow": ["Mafia Town Area", "The Birdhouse", "Time Rift - Tour", "Bird Sanctuary",
+ "Time Rift - Alpine Skyline", "Rift Collapse: Alpine Skyline"],
+
+ "Pompous Crow": ["The Birdhouse", "Time Rift - The Lab", "Bird Sanctuary", "The Mustache Gauntlet"],
+
+ "Fiery Crow": ["The Finale", "The Lava Cake", "The Mustache Gauntlet"],
+
+ "Express Owl": ["The Finale", "Time Rift - The Owl Express", "Time Rift - Deep Sea"],
+
+ "Ninja Cat": ["The Birdhouse", "The Windmill", "Bluefin Tunnel", "The Mustache Gauntlet",
+ "Time Rift - Curly Tail Trail", "Time Rift - Alpine Skyline", "Time Rift - Deep Sea",
+ "Rift Collapse: Alpine Skyline"],
+
+ # Bosses
+ "Mafia Boss": ["Down with the Mafia!", "Encore! Encore!", "Boss Rush"],
+
+ "Conductor": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"],
+ "Toilet": ["Toilet of Doom", "Boss Rush"],
+
+ "Snatcher": ["Your Contract has Expired", "Breaching the Contract", "Boss Rush",
+ "Quality Time with Snatcher"],
+
+ "Toxic Flower": ["The Illness has Spread", "The Illness has Speedrun"],
+
+ "Mustache Girl": ["The Finale", "Boss Rush", "No More Bad Guys"],
+}
+
+# Camera Tourist has a bonus that requires getting three different types of enemies in one photo.
+triple_enemy_locations = [
+ "She Came from Outer Space",
+ "She Speedran from Outer Space",
+ "Mafia's Jumps",
+ "The Mustache Gauntlet",
+ "The Birdhouse",
+ "Bird Sanctuary",
+ "Time Rift - Tour",
+]
+
+bosses = [
+ "Mafia Boss",
+ "Conductor",
+ "Toilet",
+ "Snatcher",
+ "Toxic Flower",
+ "Mustache Girl",
+]
diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py
new file mode 100644
index 000000000000..54c6e6b5d392
--- /dev/null
+++ b/worlds/ahit/Items.py
@@ -0,0 +1,302 @@
+from BaseClasses import Item, ItemClassification
+from .Types import HatDLC, HatType, hat_type_to_item, Difficulty, ItemData, HatInTimeItem
+from .Locations import get_total_locations
+from .Rules import get_difficulty
+from .Options import get_total_time_pieces, CTRLogic
+from typing import List, Dict, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from . import HatInTimeWorld
+
+
+def create_itempool(world: "HatInTimeWorld") -> List[Item]:
+ itempool: List[Item] = []
+ if world.has_yarn():
+ yarn_pool: List[Item] = create_multiple_items(world, "Yarn",
+ world.options.YarnAvailable.value,
+ ItemClassification.progression_skip_balancing)
+
+ for i in range(int(len(yarn_pool) * (0.01 * world.options.YarnBalancePercent))):
+ yarn_pool[i].classification = ItemClassification.progression
+
+ itempool += yarn_pool
+
+ for name in item_table.keys():
+ if name == "Yarn":
+ continue
+
+ if not item_dlc_enabled(world, name):
+ continue
+
+ if not world.options.HatItems and name in hat_type_to_item.values():
+ continue
+
+ item_type: ItemClassification = item_table.get(name).classification
+
+ if world.is_dw_only():
+ if item_type is ItemClassification.progression \
+ or item_type is ItemClassification.progression_skip_balancing:
+ continue
+ else:
+ if name == "Scooter Badge":
+ if world.options.CTRLogic == CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE:
+ item_type = ItemClassification.progression
+ elif name == "No Bonk Badge" and world.is_dw():
+ item_type = ItemClassification.progression
+
+ # some death wish bonuses require one hit hero + hookshot
+ if world.is_dw() and name == "Badge Pin" and not world.is_dw_only():
+ item_type = ItemClassification.progression
+
+ if item_type is ItemClassification.filler or item_type is ItemClassification.trap:
+ continue
+
+ if name in act_contracts.keys() and not world.options.ShuffleActContracts:
+ continue
+
+ if name in alps_hooks.keys() and not world.options.ShuffleAlpineZiplines:
+ continue
+
+ if name == "Progressive Painting Unlock" and not world.options.ShuffleSubconPaintings:
+ continue
+
+ if world.options.StartWithCompassBadge and name == "Compass Badge":
+ continue
+
+ if name == "Time Piece":
+ tp_list: List[Item] = create_multiple_items(world, name, get_total_time_pieces(world), item_type)
+ for i in range(int(len(tp_list) * (0.01 * world.options.TimePieceBalancePercent))):
+ tp_list[i].classification = ItemClassification.progression
+
+ itempool += tp_list
+ continue
+
+ itempool += create_multiple_items(world, name, item_frequencies.get(name, 1), item_type)
+
+ itempool += create_junk_items(world, get_total_locations(world) - len(itempool))
+ return itempool
+
+
+def calculate_yarn_costs(world: "HatInTimeWorld"):
+ min_yarn_cost = int(min(world.options.YarnCostMin.value, world.options.YarnCostMax.value))
+ max_yarn_cost = int(max(world.options.YarnCostMin.value, world.options.YarnCostMax.value))
+
+ max_cost = 0
+ for i in range(5):
+ hat: HatType = HatType(i)
+ if not world.is_hat_precollected(hat):
+ cost: int = world.random.randint(min_yarn_cost, max_yarn_cost)
+ world.hat_yarn_costs[hat] = cost
+ max_cost += cost
+ else:
+ world.hat_yarn_costs[hat] = 0
+
+ available_yarn: int = world.options.YarnAvailable.value
+ if max_cost > available_yarn:
+ world.options.YarnAvailable.value = max_cost
+ available_yarn = max_cost
+
+ extra_yarn = max_cost + world.options.MinExtraYarn - available_yarn
+ if extra_yarn > 0:
+ world.options.YarnAvailable.value += extra_yarn
+
+
+def item_dlc_enabled(world: "HatInTimeWorld", name: str) -> bool:
+ data = item_table[name]
+
+ if data.dlc_flags == HatDLC.none:
+ return True
+ elif data.dlc_flags == HatDLC.dlc1 and world.is_dlc1():
+ return True
+ elif data.dlc_flags == HatDLC.dlc2 and world.is_dlc2():
+ return True
+ elif data.dlc_flags == HatDLC.death_wish and world.is_dw():
+ return True
+
+ return False
+
+
+def create_item(world: "HatInTimeWorld", name: str) -> Item:
+ data = item_table[name]
+ return HatInTimeItem(name, data.classification, data.code, world.player)
+
+
+def create_multiple_items(world: "HatInTimeWorld", name: str, count: int = 1,
+ item_type: ItemClassification = ItemClassification.progression) -> List[Item]:
+
+ data = item_table[name]
+ itemlist: List[Item] = []
+
+ for i in range(count):
+ itemlist += [HatInTimeItem(name, item_type, data.code, world.player)]
+
+ return itemlist
+
+
+def create_junk_items(world: "HatInTimeWorld", count: int) -> List[Item]:
+ trap_chance = world.options.TrapChance.value
+ junk_pool: List[Item] = []
+ junk_list: Dict[str, int] = {}
+ trap_list: Dict[str, int] = {}
+ ic: ItemClassification
+
+ for name in item_table.keys():
+ ic = item_table[name].classification
+ if ic == ItemClassification.filler:
+ if world.is_dw_only() and "Pons" in name:
+ continue
+
+ junk_list[name] = junk_weights.get(name)
+
+ elif trap_chance > 0 and ic == ItemClassification.trap:
+ if name == "Baby Trap":
+ trap_list[name] = world.options.BabyTrapWeight.value
+ elif name == "Laser Trap":
+ trap_list[name] = world.options.LaserTrapWeight.value
+ elif name == "Parade Trap":
+ trap_list[name] = world.options.ParadeTrapWeight.value
+
+ for i in range(count):
+ if trap_chance > 0 and world.random.randint(1, 100) <= trap_chance:
+ junk_pool.append(world.create_item(
+ world.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0]))
+ else:
+ junk_pool.append(world.create_item(
+ world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0]))
+
+ return junk_pool
+
+
+def get_shop_trap_name(world: "HatInTimeWorld") -> str:
+ rand = world.random.randint(1, 9)
+ name = ""
+ if rand == 1:
+ name = "Time Plece"
+ elif rand == 2:
+ name = "Time Piece (Trust me bro)"
+ elif rand == 3:
+ name = "TimePiece"
+ elif rand == 4:
+ name = "Time Piece?"
+ elif rand == 5:
+ name = "Time Pizza"
+ elif rand == 6:
+ name = "Time piece"
+ elif rand == 7:
+ name = "TIme Piece"
+ elif rand == 8:
+ name = "Time Piece (maybe)"
+ elif rand == 9:
+ name = "Time Piece ;)"
+
+ return name
+
+
+ahit_items = {
+ "Yarn": ItemData(2000300001, ItemClassification.progression_skip_balancing),
+ "Time Piece": ItemData(2000300002, ItemClassification.progression_skip_balancing),
+
+ # for HatItems option
+ "Sprint Hat": ItemData(2000300049, ItemClassification.progression),
+ "Brewing Hat": ItemData(2000300050, ItemClassification.progression),
+ "Ice Hat": ItemData(2000300051, ItemClassification.progression),
+ "Dweller Mask": ItemData(2000300052, ItemClassification.progression),
+ "Time Stop Hat": ItemData(2000300053, ItemClassification.progression),
+
+ # Badges
+ "Projectile Badge": ItemData(2000300024, ItemClassification.useful),
+ "Fast Hatter Badge": ItemData(2000300025, ItemClassification.useful),
+ "Hover Badge": ItemData(2000300026, ItemClassification.useful),
+ "Hookshot Badge": ItemData(2000300027, ItemClassification.progression),
+ "Item Magnet Badge": ItemData(2000300028, ItemClassification.useful),
+ "No Bonk Badge": ItemData(2000300029, ItemClassification.useful),
+ "Compass Badge": ItemData(2000300030, ItemClassification.useful),
+ "Scooter Badge": ItemData(2000300031, ItemClassification.useful),
+ "One-Hit Hero Badge": ItemData(2000300038, ItemClassification.progression, HatDLC.death_wish),
+ "Camera Badge": ItemData(2000300042, ItemClassification.progression, HatDLC.death_wish),
+
+ # Relics
+ "Relic (Burger Patty)": ItemData(2000300006, ItemClassification.progression),
+ "Relic (Burger Cushion)": ItemData(2000300007, ItemClassification.progression),
+ "Relic (Mountain Set)": ItemData(2000300008, ItemClassification.progression),
+ "Relic (Train)": ItemData(2000300009, ItemClassification.progression),
+ "Relic (UFO)": ItemData(2000300010, ItemClassification.progression),
+ "Relic (Cow)": ItemData(2000300011, ItemClassification.progression),
+ "Relic (Cool Cow)": ItemData(2000300012, ItemClassification.progression),
+ "Relic (Tin-foil Hat Cow)": ItemData(2000300013, ItemClassification.progression),
+ "Relic (Crayon Box)": ItemData(2000300014, ItemClassification.progression),
+ "Relic (Red Crayon)": ItemData(2000300015, ItemClassification.progression),
+ "Relic (Blue Crayon)": ItemData(2000300016, ItemClassification.progression),
+ "Relic (Green Crayon)": ItemData(2000300017, ItemClassification.progression),
+ # DLC
+ "Relic (Cake Stand)": ItemData(2000300018, ItemClassification.progression, HatDLC.dlc1),
+ "Relic (Shortcake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1),
+ "Relic (Chocolate Cake Slice)": ItemData(2000300020, ItemClassification.progression, HatDLC.dlc1),
+ "Relic (Chocolate Cake)": ItemData(2000300021, ItemClassification.progression, HatDLC.dlc1),
+ "Relic (Necklace Bust)": ItemData(2000300022, ItemClassification.progression, HatDLC.dlc2),
+ "Relic (Necklace)": ItemData(2000300023, ItemClassification.progression, HatDLC.dlc2),
+
+ # Garbage items
+ "25 Pons": ItemData(2000300034, ItemClassification.filler),
+ "50 Pons": ItemData(2000300035, ItemClassification.filler),
+ "100 Pons": ItemData(2000300036, ItemClassification.filler),
+ "Health Pon": ItemData(2000300037, ItemClassification.filler),
+ "Random Cosmetic": ItemData(2000300044, ItemClassification.filler),
+
+ # Traps
+ "Baby Trap": ItemData(2000300039, ItemClassification.trap),
+ "Laser Trap": ItemData(2000300040, ItemClassification.trap),
+ "Parade Trap": ItemData(2000300041, ItemClassification.trap),
+
+ # Other
+ "Badge Pin": ItemData(2000300043, ItemClassification.useful),
+ "Umbrella": ItemData(2000300033, ItemClassification.progression),
+ "Progressive Painting Unlock": ItemData(2000300003, ItemClassification.progression),
+ # DLC
+ "Metro Ticket - Yellow": ItemData(2000300045, ItemClassification.progression, HatDLC.dlc2),
+ "Metro Ticket - Green": ItemData(2000300046, ItemClassification.progression, HatDLC.dlc2),
+ "Metro Ticket - Blue": ItemData(2000300047, ItemClassification.progression, HatDLC.dlc2),
+ "Metro Ticket - Pink": ItemData(2000300048, ItemClassification.progression, HatDLC.dlc2),
+}
+
+act_contracts = {
+ "Snatcher's Contract - The Subcon Well": ItemData(2000300200, ItemClassification.progression),
+ "Snatcher's Contract - Toilet of Doom": ItemData(2000300201, ItemClassification.progression),
+ "Snatcher's Contract - Queen Vanessa's Manor": ItemData(2000300202, ItemClassification.progression),
+ "Snatcher's Contract - Mail Delivery Service": ItemData(2000300203, ItemClassification.progression),
+}
+
+alps_hooks = {
+ "Zipline Unlock - The Birdhouse Path": ItemData(2000300204, ItemClassification.progression),
+ "Zipline Unlock - The Lava Cake Path": ItemData(2000300205, ItemClassification.progression),
+ "Zipline Unlock - The Windmill Path": ItemData(2000300206, ItemClassification.progression),
+ "Zipline Unlock - The Twilight Bell Path": ItemData(2000300207, ItemClassification.progression),
+}
+
+relic_groups = {
+ "Burger": {"Relic (Burger Patty)", "Relic (Burger Cushion)"},
+ "Train": {"Relic (Mountain Set)", "Relic (Train)"},
+ "UFO": {"Relic (UFO)", "Relic (Cow)", "Relic (Cool Cow)", "Relic (Tin-foil Hat Cow)"},
+ "Crayon": {"Relic (Crayon Box)", "Relic (Red Crayon)", "Relic (Blue Crayon)", "Relic (Green Crayon)"},
+ "Cake": {"Relic (Cake Stand)", "Relic (Chocolate Cake)", "Relic (Chocolate Cake Slice)", "Relic (Shortcake)"},
+ "Necklace": {"Relic (Necklace Bust)", "Relic (Necklace)"},
+}
+
+item_frequencies = {
+ "Badge Pin": 2,
+ "Progressive Painting Unlock": 3,
+}
+
+junk_weights = {
+ "25 Pons": 50,
+ "50 Pons": 25,
+ "100 Pons": 10,
+ "Health Pon": 35,
+ "Random Cosmetic": 35,
+}
+
+item_table = {
+ **ahit_items,
+ **act_contracts,
+ **alps_hooks,
+}
diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py
new file mode 100644
index 000000000000..9954514e8f3b
--- /dev/null
+++ b/worlds/ahit/Locations.py
@@ -0,0 +1,1057 @@
+from .Types import HatDLC, HatType, LocData, Difficulty, HitType
+from typing import Dict, TYPE_CHECKING
+from .Options import TasksanityCheckCount
+
+if TYPE_CHECKING:
+ from . import HatInTimeWorld
+
+TASKSANITY_START_ID = 2000300204
+
+
+def get_total_locations(world: "HatInTimeWorld") -> int:
+ total = 0
+
+ if not world.is_dw_only():
+ for name in location_table.keys():
+ if is_location_valid(world, name):
+ total += 1
+
+ if world.is_dlc1() and world.options.Tasksanity:
+ total += world.options.TasksanityCheckCount
+
+ if world.is_dw():
+ if world.options.DWShuffle:
+ total += len(world.dw_shuffle)
+ if world.options.DWEnableBonus:
+ total += len(world.dw_shuffle)
+ else:
+ total += 37
+ if world.is_dlc2():
+ total += 1
+
+ if world.options.DWEnableBonus:
+ total += 37
+ if world.is_dlc2():
+ total += 1
+
+ return total
+
+
+def location_dlc_enabled(world: "HatInTimeWorld", location: str) -> bool:
+ data = location_table.get(location) or event_locs.get(location)
+
+ if data.dlc_flags == HatDLC.none:
+ return True
+ elif data.dlc_flags == HatDLC.dlc1 and world.is_dlc1():
+ return True
+ elif data.dlc_flags == HatDLC.dlc2 and world.is_dlc2():
+ return True
+ elif data.dlc_flags == HatDLC.death_wish and world.is_dw():
+ return True
+ elif data.dlc_flags == HatDLC.dlc1_dw and world.is_dlc1() and world.is_dw():
+ return True
+ elif data.dlc_flags == HatDLC.dlc2_dw and world.is_dlc2() and world.is_dw():
+ return True
+
+ return False
+
+
+def is_location_valid(world: "HatInTimeWorld", location: str) -> bool:
+ if not location_dlc_enabled(world, location):
+ return False
+
+ if not world.options.ShuffleStorybookPages and location in storybook_pages.keys():
+ return False
+
+ if not world.options.ShuffleActContracts and location in contract_locations.keys():
+ return False
+
+ if location not in world.shop_locs and location in shop_locations:
+ return False
+
+ data = location_table.get(location) or event_locs.get(location)
+ if world.options.ExcludeTour and data.region == "Time Rift - Tour":
+ return False
+
+ # No need for all those event items if we're not doing candles
+ if data.dlc_flags & HatDLC.death_wish:
+ if world.options.DWExcludeCandles and location in event_locs.keys():
+ return False
+
+ if world.options.DWShuffle and data.region in death_wishes and data.region not in world.dw_shuffle:
+ return False
+
+ if location in zero_jumps:
+ if world.options.DWShuffle and "Zero Jumps" not in world.dw_shuffle:
+ return False
+
+ difficulty: Difficulty = Difficulty(world.options.LogicDifficulty)
+ if location in zero_jumps_hard and difficulty < Difficulty.HARD:
+ return False
+
+ if location in zero_jumps_expert and difficulty < Difficulty.EXPERT:
+ return False
+
+ return True
+
+
+def get_location_names() -> Dict[str, int]:
+ names = {name: data.id for name, data in location_table.items()}
+ id_start: int = TASKSANITY_START_ID
+ for i in range(TasksanityCheckCount.range_end):
+ names.setdefault(f"Tasksanity Check {i+1}", id_start+i)
+
+ for (key, loc_id) in death_wishes.items():
+ names.setdefault(f"{key} - Main Objective", loc_id)
+ names.setdefault(f"{key} - All Clear", loc_id+1)
+
+ return names
+
+
+ahit_locations = {
+ "Spaceship - Rumbi Abuse": LocData(2000301000, "Spaceship", hit_type=HitType.umbrella_or_brewing),
+
+ # 300000 range - Mafia Town/Battle of the Birds
+ "Welcome to Mafia Town - Umbrella": LocData(2000301002, "Welcome to Mafia Town"),
+ "Mafia Town - Old Man (Seaside Spaghetti)": LocData(2000303833, "Mafia Town Area"),
+ "Mafia Town - Old Man (Steel Beams)": LocData(2000303832, "Mafia Town Area"),
+ "Mafia Town - Blue Vault": LocData(2000302850, "Mafia Town Area"),
+ "Mafia Town - Green Vault": LocData(2000302851, "Mafia Town Area"),
+ "Mafia Town - Red Vault": LocData(2000302848, "Mafia Town Area"),
+ "Mafia Town - Blue Vault Brewing Crate": LocData(2000305572, "Mafia Town Area", required_hats=[HatType.BREWING]),
+ "Mafia Town - Plaza Under Boxes": LocData(2000304458, "Mafia Town Area"),
+ "Mafia Town - Small Boat": LocData(2000304460, "Mafia Town Area"),
+ "Mafia Town - Staircase Pon Cluster": LocData(2000304611, "Mafia Town Area"),
+ "Mafia Town - Palm Tree": LocData(2000304609, "Mafia Town Area"),
+ "Mafia Town - Port": LocData(2000305219, "Mafia Town Area"),
+ "Mafia Town - Docks Chest": LocData(2000303534, "Mafia Town Area"),
+ "Mafia Town - Ice Hat Cage": LocData(2000304831, "Mafia Town Area", required_hats=[HatType.ICE]),
+ "Mafia Town - Hidden Buttons Chest": LocData(2000303483, "Mafia Town Area"),
+
+ # These can be accessed from HUMT, the above locations can't be
+ "Mafia Town - Dweller Boxes": LocData(2000304462, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Ledge Chest": LocData(2000303530, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Yellow Sphere Building Chest": LocData(2000303535, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Beneath Scaffolding": LocData(2000304456, "Mafia Town Area (HUMT)"),
+ "Mafia Town - On Scaffolding": LocData(2000304457, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Cargo Ship": LocData(2000304459, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Beach Alcove": LocData(2000304463, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Wood Cage": LocData(2000304606, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Beach Patio": LocData(2000304610, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Steel Beam Nest": LocData(2000304608, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Top of Ruined Tower": LocData(2000304607, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]),
+ "Mafia Town - Hot Air Balloon": LocData(2000304829, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]),
+ "Mafia Town - Camera Badge 1": LocData(2000302003, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Camera Badge 2": LocData(2000302004, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Chest Beneath Aqueduct": LocData(2000303489, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Secret Cave": LocData(2000305220, "Mafia Town Area (HUMT)", required_hats=[HatType.BREWING]),
+ "Mafia Town - Crow Chest": LocData(2000303532, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Above Boats": LocData(2000305218, "Mafia Town Area (HUMT)", hookshot=True),
+ "Mafia Town - Slip Slide Chest": LocData(2000303529, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Behind Faucet": LocData(2000304214, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Clock Tower Chest": LocData(2000303481, "Mafia Town Area (HUMT)", hookshot=True),
+ "Mafia Town - Top of Lighthouse": LocData(2000304213, "Mafia Town Area (HUMT)", hookshot=True),
+ "Mafia Town - Mafia Geek Platform": LocData(2000304212, "Mafia Town Area (HUMT)"),
+ "Mafia Town - Behind HQ Chest": LocData(2000303486, "Mafia Town Area (HUMT)"),
+
+ "Mafia HQ - Hallway Brewing Crate": LocData(2000305387, "Down with the Mafia!", required_hats=[HatType.BREWING]),
+ "Mafia HQ - Freezer Chest": LocData(2000303241, "Down with the Mafia!"),
+ "Mafia HQ - Secret Room": LocData(2000304979, "Down with the Mafia!", required_hats=[HatType.ICE]),
+ "Mafia HQ - Bathroom Stall Chest": LocData(2000303243, "Down with the Mafia!"),
+
+ "Dead Bird Studio - Up the Ladder": LocData(2000304874, "Dead Bird Studio - Elevator Area"),
+ "Dead Bird Studio - Red Building Top": LocData(2000305024, "Dead Bird Studio - Elevator Area"),
+ "Dead Bird Studio - Behind Water Tower": LocData(2000305248, "Dead Bird Studio - Elevator Area"),
+ "Dead Bird Studio - Side of House": LocData(2000305247, "Dead Bird Studio - Elevator Area"),
+
+ "Dead Bird Studio - DJ Grooves Sign Chest": LocData(2000303901, "Dead Bird Studio - Post Elevator Area",
+ hit_type=HitType.umbrella_or_brewing),
+
+ "Dead Bird Studio - Tightrope Chest": LocData(2000303898, "Dead Bird Studio - Post Elevator Area",
+ hit_type=HitType.umbrella_or_brewing),
+
+ "Dead Bird Studio - Tepee Chest": LocData(2000303899, "Dead Bird Studio - Post Elevator Area",
+ hit_type=HitType.umbrella_or_brewing),
+
+ "Dead Bird Studio - Conductor Chest": LocData(2000303900, "Dead Bird Studio - Post Elevator Area",
+ hit_type=HitType.umbrella_or_brewing),
+
+ "Murder on the Owl Express - Cafeteria": LocData(2000305313, "Murder on the Owl Express"),
+ "Murder on the Owl Express - Luggage Room Top": LocData(2000305090, "Murder on the Owl Express"),
+ "Murder on the Owl Express - Luggage Room Bottom": LocData(2000305091, "Murder on the Owl Express"),
+
+ "Murder on the Owl Express - Raven Suite Room": LocData(2000305701, "Murder on the Owl Express",
+ required_hats=[HatType.BREWING]),
+
+ "Murder on the Owl Express - Raven Suite Top": LocData(2000305312, "Murder on the Owl Express"),
+ "Murder on the Owl Express - Lounge Chest": LocData(2000303963, "Murder on the Owl Express"),
+
+ "Picture Perfect - Behind Badge Seller": LocData(2000304307, "Picture Perfect"),
+ "Picture Perfect - Hats Buy Building": LocData(2000304530, "Picture Perfect"),
+
+ "Dead Bird Studio Basement - Window Platform": LocData(2000305432, "Dead Bird Studio Basement", hookshot=True),
+ "Dead Bird Studio Basement - Cardboard Conductor": LocData(2000305059, "Dead Bird Studio Basement", hookshot=True),
+ "Dead Bird Studio Basement - Above Conductor Sign": LocData(2000305057, "Dead Bird Studio Basement", hookshot=True),
+ "Dead Bird Studio Basement - Logo Wall": LocData(2000305207, "Dead Bird Studio Basement"),
+ "Dead Bird Studio Basement - Disco Room": LocData(2000305061, "Dead Bird Studio Basement", hookshot=True),
+ "Dead Bird Studio Basement - Small Room": LocData(2000304813, "Dead Bird Studio Basement"),
+ "Dead Bird Studio Basement - Vent Pipe": LocData(2000305430, "Dead Bird Studio Basement"),
+ "Dead Bird Studio Basement - Tightrope": LocData(2000305058, "Dead Bird Studio Basement", hookshot=True),
+ "Dead Bird Studio Basement - Cameras": LocData(2000305431, "Dead Bird Studio Basement", hookshot=True),
+ "Dead Bird Studio Basement - Locked Room": LocData(2000305819, "Dead Bird Studio Basement", hookshot=True),
+
+ # Subcon Forest
+ "Contractual Obligations - Cherry Bomb Bone Cage": LocData(2000324761, "Contractual Obligations"),
+ "Subcon Village - Tree Top Ice Cube": LocData(2000325078, "Subcon Forest Area"),
+ "Subcon Village - Graveyard Ice Cube": LocData(2000325077, "Subcon Forest Area"),
+ "Subcon Village - House Top": LocData(2000325471, "Subcon Forest Area"),
+ "Subcon Village - Ice Cube House": LocData(2000325469, "Subcon Forest Area"),
+ "Subcon Village - Snatcher Statue Chest": LocData(2000323730, "Subcon Forest Area", paintings=1),
+ "Subcon Village - Stump Platform Chest": LocData(2000323729, "Subcon Forest Area"),
+ "Subcon Forest - Giant Tree Climb": LocData(2000325470, "Subcon Forest Area"),
+
+ "Subcon Forest - Ice Cube Shack": LocData(2000324465, "Subcon Forest Area", paintings=1),
+ "Subcon Forest - Swamp Gravestone": LocData(2000326296, "Subcon Forest Area",
+ required_hats=[HatType.BREWING], paintings=1),
+
+ "Subcon Forest - Swamp Near Well": LocData(2000324762, "Subcon Forest Area", paintings=1),
+ "Subcon Forest - Swamp Tree A": LocData(2000324763, "Subcon Forest Area", paintings=1),
+ "Subcon Forest - Swamp Tree B": LocData(2000324764, "Subcon Forest Area", paintings=1),
+ "Subcon Forest - Swamp Ice Wall": LocData(2000324706, "Subcon Forest Area", paintings=1),
+ "Subcon Forest - Swamp Treehouse": LocData(2000325468, "Subcon Forest Area", paintings=1),
+ "Subcon Forest - Swamp Tree Chest": LocData(2000323728, "Subcon Forest Area", paintings=1),
+
+ "Subcon Forest - Burning House": LocData(2000324710, "Subcon Forest Area", paintings=2),
+ "Subcon Forest - Burning Tree Climb": LocData(2000325079, "Subcon Forest Area", paintings=2),
+ "Subcon Forest - Burning Stump Chest": LocData(2000323731, "Subcon Forest Area", paintings=2),
+ "Subcon Forest - Burning Forest Treehouse": LocData(2000325467, "Subcon Forest Area", paintings=2),
+ "Subcon Forest - Spider Bone Cage A": LocData(2000324462, "Subcon Forest Area", paintings=2),
+ "Subcon Forest - Spider Bone Cage B": LocData(2000325080, "Subcon Forest Area", paintings=2),
+ "Subcon Forest - Triple Spider Bounce": LocData(2000324765, "Subcon Forest Area", paintings=2),
+ "Subcon Forest - Noose Treehouse": LocData(2000324856, "Subcon Forest Area", hookshot=True, paintings=2),
+
+ "Subcon Forest - Long Tree Climb Chest": LocData(2000323734, "Subcon Forest Area",
+ required_hats=[HatType.DWELLER], paintings=2),
+
+ "Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Area"),
+
+ "Subcon Forest - Manor Rooftop": LocData(2000325466, "Subcon Forest Area",
+ hit_type=HitType.dweller_bell, paintings=1),
+
+ "Subcon Forest - Infinite Yarn Bush": LocData(2000325478, "Subcon Forest Area",
+ required_hats=[HatType.BREWING], paintings=2),
+
+ "Subcon Forest - Magnet Badge Bush": LocData(2000325479, "Subcon Forest Area",
+ required_hats=[HatType.BREWING], paintings=3),
+
+ "Subcon Forest - Dweller Stump": LocData(2000324767, "Subcon Forest Area",
+ required_hats=[HatType.DWELLER], paintings=3),
+
+ "Subcon Forest - Dweller Floating Rocks": LocData(2000324464, "Subcon Forest Area",
+ required_hats=[HatType.DWELLER], paintings=3),
+
+ "Subcon Forest - Dweller Platforming Tree A": LocData(2000324709, "Subcon Forest Area", paintings=3),
+
+ "Subcon Forest - Dweller Platforming Tree B": LocData(2000324855, "Subcon Forest Area",
+ required_hats=[HatType.DWELLER], paintings=3),
+
+ "Subcon Forest - Giant Time Piece": LocData(2000325473, "Subcon Forest Area", paintings=3),
+ "Subcon Forest - Gallows": LocData(2000325472, "Subcon Forest Area", paintings=3),
+
+ "Subcon Forest - Green and Purple Dweller Rocks": LocData(2000325082, "Subcon Forest Area", paintings=3),
+
+ "Subcon Forest - Dweller Shack": LocData(2000324463, "Subcon Forest Area",
+ required_hats=[HatType.DWELLER], paintings=3),
+
+ "Subcon Forest - Tall Tree Hookshot Swing": LocData(2000324766, "Subcon Forest Area",
+ required_hats=[HatType.DWELLER],
+ hookshot=True,
+ paintings=3),
+
+ "Subcon Well - Hookshot Badge Chest": LocData(2000324114, "The Subcon Well",
+ hit_type=HitType.umbrella_or_brewing, paintings=1),
+
+ "Subcon Well - Above Chest": LocData(2000324612, "The Subcon Well",
+ hit_type=HitType.umbrella_or_brewing, paintings=1),
+
+ "Subcon Well - On Pipe": LocData(2000324311, "The Subcon Well", hookshot=True,
+ hit_type=HitType.umbrella_or_brewing, paintings=1),
+
+ "Subcon Well - Mushroom": LocData(2000325318, "The Subcon Well",
+ hit_type=HitType.umbrella_or_brewing, paintings=1),
+
+ "Queen Vanessa's Manor - Cellar": LocData(2000324841, "Queen Vanessa's Manor",
+ hit_type=HitType.dweller_bell, paintings=1),
+
+ "Queen Vanessa's Manor - Bedroom Chest": LocData(2000323808, "Queen Vanessa's Manor",
+ hit_type=HitType.dweller_bell, paintings=1),
+
+ "Queen Vanessa's Manor - Hall Chest": LocData(2000323896, "Queen Vanessa's Manor",
+ hit_type=HitType.dweller_bell, paintings=1),
+
+ "Queen Vanessa's Manor - Chandelier": LocData(2000325546, "Queen Vanessa's Manor",
+ hit_type=HitType.dweller_bell, paintings=1),
+
+ # Alpine Skyline
+ "Alpine Skyline - Goat Village: Below Hookpoint": LocData(2000334856, "Alpine Skyline Area (TIHS)"),
+ "Alpine Skyline - Goat Village: Hidden Branch": LocData(2000334855, "Alpine Skyline Area (TIHS)"),
+ "Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area (TIHS)", hookshot=True),
+ "Alpine Skyline - Bird Pass Fork": LocData(2000335911, "Alpine Skyline Area (TIHS)", hookshot=True),
+
+ "Alpine Skyline - Yellow Band Hills": LocData(2000335756, "Alpine Skyline Area (TIHS)", hookshot=True,
+ required_hats=[HatType.BREWING]),
+
+ "Alpine Skyline - The Purrloined Village: Horned Stone": LocData(2000335561, "Alpine Skyline Area"),
+ "Alpine Skyline - The Purrloined Village: Chest Reward": LocData(2000334831, "Alpine Skyline Area"),
+ "Alpine Skyline - The Birdhouse: Triple Crow Chest": LocData(2000334758, "The Birdhouse"),
+
+ "Alpine Skyline - The Birdhouse: Dweller Platforms Relic": LocData(2000336497, "The Birdhouse",
+ required_hats=[HatType.DWELLER]),
+
+ "Alpine Skyline - The Birdhouse: Brewing Crate House": LocData(2000336496, "The Birdhouse"),
+ "Alpine Skyline - The Birdhouse: Hay Bale": LocData(2000335885, "The Birdhouse"),
+ "Alpine Skyline - The Birdhouse: Alpine Crow Mini-Gauntlet": LocData(2000335886, "The Birdhouse"),
+ "Alpine Skyline - The Birdhouse: Outer Edge": LocData(2000335492, "The Birdhouse"),
+
+ "Alpine Skyline - Mystifying Time Mesa: Zipline": LocData(2000337058, "Alpine Skyline Area"),
+ "Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": LocData(2000336052, "Alpine Skyline Area"),
+ "Alpine Skyline - Ember Summit": LocData(2000336311, "Alpine Skyline Area (TIHS)", hookshot=True),
+ "Alpine Skyline - The Lava Cake: Center Fence Cage": LocData(2000335448, "The Lava Cake"),
+ "Alpine Skyline - The Lava Cake: Outer Island Chest": LocData(2000334291, "The Lava Cake"),
+ "Alpine Skyline - The Lava Cake: Dweller Pillars": LocData(2000335417, "The Lava Cake"),
+ "Alpine Skyline - The Lava Cake: Top Cake": LocData(2000335418, "The Lava Cake"),
+ "Alpine Skyline - The Twilight Path": LocData(2000334434, "Alpine Skyline Area", required_hats=[HatType.DWELLER]),
+ "Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(2000336478, "The Twilight Bell"),
+ "Alpine Skyline - The Twilight Bell: Ice Platform": LocData(2000335826, "The Twilight Bell"),
+ "Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area"),
+ "Alpine Skyline - Windy Passage": LocData(2000334776, "Alpine Skyline Area (TIHS)", hookshot=True),
+ "Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(2000336395, "The Windmill"),
+ "Alpine Skyline - The Windmill: Entrance": LocData(2000335783, "The Windmill"),
+ "Alpine Skyline - The Windmill: Dropdown": LocData(2000335815, "The Windmill"),
+ "Alpine Skyline - The Windmill: House Window": LocData(2000335389, "The Windmill"),
+
+ "The Finale - Frozen Item": LocData(2000304108, "The Finale"),
+
+ "Bon Voyage! - Lamp Post Top": LocData(2000305321, "Bon Voyage!", dlc_flags=HatDLC.dlc1),
+ "Bon Voyage! - Mafia Cargo Ship": LocData(2000304313, "Bon Voyage!", dlc_flags=HatDLC.dlc1),
+ "The Arctic Cruise - Toilet": LocData(2000305109, "Cruise Ship", dlc_flags=HatDLC.dlc1),
+ "The Arctic Cruise - Bar": LocData(2000304251, "Cruise Ship", dlc_flags=HatDLC.dlc1),
+ "The Arctic Cruise - Dive Board Ledge": LocData(2000304254, "Cruise Ship", dlc_flags=HatDLC.dlc1),
+ "The Arctic Cruise - Top Balcony": LocData(2000304255, "Cruise Ship", dlc_flags=HatDLC.dlc1),
+ "The Arctic Cruise - Octopus Room": LocData(2000305253, "Cruise Ship", dlc_flags=HatDLC.dlc1),
+ "The Arctic Cruise - Octopus Room Top": LocData(2000304249, "Cruise Ship", dlc_flags=HatDLC.dlc1),
+ "The Arctic Cruise - Laundry Room": LocData(2000304250, "Cruise Ship", dlc_flags=HatDLC.dlc1),
+ "The Arctic Cruise - Ship Side": LocData(2000304247, "Cruise Ship", dlc_flags=HatDLC.dlc1),
+ "The Arctic Cruise - Silver Ring": LocData(2000305252, "Cruise Ship", dlc_flags=HatDLC.dlc1),
+ "Rock the Boat - Reception Room - Suitcase": LocData(2000304045, "Rock the Boat", dlc_flags=HatDLC.dlc1),
+ "Rock the Boat - Reception Room - Under Desk": LocData(2000304047, "Rock the Boat", dlc_flags=HatDLC.dlc1),
+ "Rock the Boat - Lamp Post": LocData(2000304048, "Rock the Boat", dlc_flags=HatDLC.dlc1),
+ "Rock the Boat - Iceberg Top": LocData(2000304046, "Rock the Boat", dlc_flags=HatDLC.dlc1),
+ "Rock the Boat - Post Captain Rescue": LocData(2000304049, "Rock the Boat", dlc_flags=HatDLC.dlc1,
+ required_hats=[HatType.ICE]),
+
+ "Nyakuza Metro - Main Station Dining Area": LocData(2000304105, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2),
+ "Nyakuza Metro - Top of Ramen Shop": LocData(2000304104, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2),
+
+ "Yellow Overpass Station - Brewing Crate": LocData(2000305413, "Yellow Overpass Station",
+ dlc_flags=HatDLC.dlc2,
+ required_hats=[HatType.BREWING]),
+
+ "Bluefin Tunnel - Cat Vacuum": LocData(2000305111, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2),
+
+ "Pink Paw Station - Cat Vacuum": LocData(2000305110, "Pink Paw Station",
+ dlc_flags=HatDLC.dlc2,
+ hookshot=True,
+ required_hats=[HatType.DWELLER]),
+
+ "Pink Paw Station - Behind Fan": LocData(2000304106, "Pink Paw Station",
+ dlc_flags=HatDLC.dlc2,
+ hookshot=True,
+ required_hats=[HatType.TIME_STOP, HatType.DWELLER]),
+}
+
+act_completions = {
+ "Act Completion (Time Rift - Gallery)": LocData(2000312758, "Time Rift - Gallery", required_hats=[HatType.BREWING]),
+ "Act Completion (Time Rift - The Lab)": LocData(2000312838, "Time Rift - The Lab"),
+
+ "Act Completion (Welcome to Mafia Town)": LocData(2000311771, "Welcome to Mafia Town"),
+ "Act Completion (Barrel Battle)": LocData(2000311958, "Barrel Battle"),
+ "Act Completion (She Came from Outer Space)": LocData(2000312262, "She Came from Outer Space"),
+ "Act Completion (Down with the Mafia!)": LocData(2000311326, "Down with the Mafia!"),
+ "Act Completion (Cheating the Race)": LocData(2000312318, "Cheating the Race", required_hats=[HatType.TIME_STOP]),
+ "Act Completion (Heating Up Mafia Town)": LocData(2000311481, "Heating Up Mafia Town", hit_type=HitType.umbrella),
+ "Act Completion (The Golden Vault)": LocData(2000312250, "The Golden Vault"),
+ "Act Completion (Time Rift - Bazaar)": LocData(2000312465, "Time Rift - Bazaar"),
+ "Act Completion (Time Rift - Sewers)": LocData(2000312484, "Time Rift - Sewers"),
+ "Act Completion (Time Rift - Mafia of Cooks)": LocData(2000311855, "Time Rift - Mafia of Cooks"),
+
+ "Act Completion (Dead Bird Studio)": LocData(2000311383, "Dead Bird Studio",
+ hit_type=HitType.umbrella_or_brewing),
+
+ "Act Completion (Murder on the Owl Express)": LocData(2000311544, "Murder on the Owl Express"),
+ "Act Completion (Picture Perfect)": LocData(2000311587, "Picture Perfect"),
+ "Act Completion (Train Rush)": LocData(2000312481, "Train Rush", hookshot=True),
+ "Act Completion (The Big Parade)": LocData(2000311157, "The Big Parade", hit_type=HitType.umbrella),
+ "Act Completion (Award Ceremony)": LocData(2000311488, "Award Ceremony"),
+ "Act Completion (Dead Bird Studio Basement)": LocData(2000312253, "Dead Bird Studio Basement", hookshot=True),
+ "Act Completion (Time Rift - The Owl Express)": LocData(2000312807, "Time Rift - The Owl Express"),
+ "Act Completion (Time Rift - The Moon)": LocData(2000312785, "Time Rift - The Moon"),
+ "Act Completion (Time Rift - Dead Bird Studio)": LocData(2000312577, "Time Rift - Dead Bird Studio"),
+
+ "Act Completion (Contractual Obligations)": LocData(2000312317, "Contractual Obligations", paintings=1),
+
+ "Act Completion (The Subcon Well)": LocData(2000311160, "The Subcon Well",
+ hookshot=True, hit_type=HitType.umbrella_or_brewing, paintings=1),
+
+ "Act Completion (Toilet of Doom)": LocData(2000311984, "Toilet of Doom",
+ hit_type=HitType.umbrella_or_brewing, hookshot=True, paintings=1),
+
+ "Act Completion (Queen Vanessa's Manor)": LocData(2000312017, "Queen Vanessa's Manor",
+ hit_type=HitType.umbrella, paintings=1),
+
+ "Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service",
+ required_hats=[HatType.SPRINT]),
+
+ "Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired",
+ hit_type=HitType.umbrella),
+
+ "Act Completion (Time Rift - Pipe)": LocData(2000313069, "Time Rift - Pipe", hookshot=True),
+ "Act Completion (Time Rift - Village)": LocData(2000313056, "Time Rift - Village"),
+ "Act Completion (Time Rift - Sleepy Subcon)": LocData(2000312086, "Time Rift - Sleepy Subcon"),
+
+ "Act Completion (The Birdhouse)": LocData(2000311428, "The Birdhouse"),
+ "Act Completion (The Lava Cake)": LocData(2000312509, "The Lava Cake"),
+ "Act Completion (The Twilight Bell)": LocData(2000311540, "The Twilight Bell"),
+ "Act Completion (The Windmill)": LocData(2000312263, "The Windmill"),
+ "Act Completion (The Illness has Spread)": LocData(2000312022, "The Illness has Spread", hookshot=True),
+
+ "Act Completion (Time Rift - The Twilight Bell)": LocData(2000312399, "Time Rift - The Twilight Bell",
+ required_hats=[HatType.DWELLER]),
+
+ "Act Completion (Time Rift - Curly Tail Trail)": LocData(2000313335, "Time Rift - Curly Tail Trail",
+ required_hats=[HatType.ICE]),
+
+ "Act Completion (Time Rift - Alpine Skyline)": LocData(2000311777, "Time Rift - Alpine Skyline"),
+
+ "Act Completion (The Finale)": LocData(2000311872, "The Finale", hookshot=True, required_hats=[HatType.DWELLER]),
+ "Act Completion (Time Rift - Tour)": LocData(2000311803, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
+
+ "Act Completion (Bon Voyage!)": LocData(2000311520, "Bon Voyage!", dlc_flags=HatDLC.dlc1, hookshot=True),
+ "Act Completion (Ship Shape)": LocData(2000311451, "Ship Shape", dlc_flags=HatDLC.dlc1),
+
+ "Act Completion (Rock the Boat)": LocData(2000311437, "Rock the Boat", dlc_flags=HatDLC.dlc1,
+ required_hats=[HatType.ICE]),
+
+ "Act Completion (Time Rift - Balcony)": LocData(2000312226, "Time Rift - Balcony", dlc_flags=HatDLC.dlc1,
+ hookshot=True),
+
+ "Act Completion (Time Rift - Deep Sea)": LocData(2000312434, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1,
+ hookshot=True, required_hats=[HatType.DWELLER, HatType.ICE]),
+
+ "Act Completion (Nyakuza Metro Intro)": LocData(2000311138, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2),
+
+ "Act Completion (Yellow Overpass Station)": LocData(2000311206, "Yellow Overpass Station",
+ dlc_flags=HatDLC.dlc2,
+ hookshot=True),
+
+ "Act Completion (Yellow Overpass Manhole)": LocData(2000311387, "Yellow Overpass Manhole",
+ dlc_flags=HatDLC.dlc2,
+ required_hats=[HatType.ICE]),
+
+ "Act Completion (Green Clean Station)": LocData(2000311207, "Green Clean Station", dlc_flags=HatDLC.dlc2),
+
+ "Act Completion (Green Clean Manhole)": LocData(2000311388, "Green Clean Manhole",
+ dlc_flags=HatDLC.dlc2,
+ required_hats=[HatType.ICE, HatType.DWELLER]),
+
+ "Act Completion (Bluefin Tunnel)": LocData(2000311208, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2),
+
+ "Act Completion (Pink Paw Station)": LocData(2000311209, "Pink Paw Station",
+ dlc_flags=HatDLC.dlc2,
+ hookshot=True,
+ required_hats=[HatType.DWELLER]),
+
+ "Act Completion (Pink Paw Manhole)": LocData(2000311389, "Pink Paw Manhole",
+ dlc_flags=HatDLC.dlc2,
+ required_hats=[HatType.ICE]),
+
+ "Act Completion (Rush Hour)": LocData(2000311210, "Rush Hour",
+ dlc_flags=HatDLC.dlc2,
+ hookshot=True,
+ required_hats=[HatType.ICE, HatType.BREWING]),
+
+ "Act Completion (Time Rift - Rumbi Factory)": LocData(2000312736, "Time Rift - Rumbi Factory",
+ dlc_flags=HatDLC.dlc2),
+}
+
+storybook_pages = {
+ "Mafia of Cooks - Page: Fish Pile": LocData(2000345091, "Time Rift - Mafia of Cooks"),
+ "Mafia of Cooks - Page: Trash Mound": LocData(2000345090, "Time Rift - Mafia of Cooks"),
+ "Mafia of Cooks - Page: Beside Red Building": LocData(2000345092, "Time Rift - Mafia of Cooks"),
+ "Mafia of Cooks - Page: Behind Shipping Containers": LocData(2000345095, "Time Rift - Mafia of Cooks"),
+ "Mafia of Cooks - Page: Top of Boat": LocData(2000345093, "Time Rift - Mafia of Cooks"),
+ "Mafia of Cooks - Page: Below Dock": LocData(2000345094, "Time Rift - Mafia of Cooks"),
+
+ "Dead Bird Studio (Rift) - Page: Behind Cardboard Planet": LocData(2000345449, "Time Rift - Dead Bird Studio"),
+ "Dead Bird Studio (Rift) - Page: Near Time Rift Gate": LocData(2000345447, "Time Rift - Dead Bird Studio"),
+ "Dead Bird Studio (Rift) - Page: Top of Metal Bar": LocData(2000345448, "Time Rift - Dead Bird Studio"),
+ "Dead Bird Studio (Rift) - Page: Lava Lamp": LocData(2000345450, "Time Rift - Dead Bird Studio"),
+ "Dead Bird Studio (Rift) - Page: Above Horse Picture": LocData(2000345451, "Time Rift - Dead Bird Studio"),
+ "Dead Bird Studio (Rift) - Page: Green Screen": LocData(2000345452, "Time Rift - Dead Bird Studio"),
+ "Dead Bird Studio (Rift) - Page: In The Corner": LocData(2000345453, "Time Rift - Dead Bird Studio"),
+ "Dead Bird Studio (Rift) - Page: Above TV Room": LocData(2000345445, "Time Rift - Dead Bird Studio"),
+
+ "Sleepy Subcon - Page: Behind Entrance Area": LocData(2000345373, "Time Rift - Sleepy Subcon"),
+ "Sleepy Subcon - Page: Near Wrecking Ball": LocData(2000345327, "Time Rift - Sleepy Subcon"),
+ "Sleepy Subcon - Page: Behind Crane": LocData(2000345371, "Time Rift - Sleepy Subcon"),
+ "Sleepy Subcon - Page: Wrecked Treehouse": LocData(2000345326, "Time Rift - Sleepy Subcon"),
+ "Sleepy Subcon - Page: Behind 2nd Rift Gate": LocData(2000345372, "Time Rift - Sleepy Subcon"),
+ "Sleepy Subcon - Page: Rotating Platform": LocData(2000345328, "Time Rift - Sleepy Subcon"),
+ "Sleepy Subcon - Page: Behind 3rd Rift Gate": LocData(2000345329, "Time Rift - Sleepy Subcon"),
+ "Sleepy Subcon - Page: Frozen Tree": LocData(2000345330, "Time Rift - Sleepy Subcon"),
+ "Sleepy Subcon - Page: Secret Library": LocData(2000345370, "Time Rift - Sleepy Subcon"),
+
+ "Alpine Skyline (Rift) - Page: Entrance Area Hidden Ledge": LocData(2000345016, "Time Rift - Alpine Skyline"),
+ "Alpine Skyline (Rift) - Page: Windmill Island Ledge": LocData(2000345012, "Time Rift - Alpine Skyline"),
+ "Alpine Skyline (Rift) - Page: Waterfall Wooden Pillar": LocData(2000345015, "Time Rift - Alpine Skyline"),
+ "Alpine Skyline (Rift) - Page: Lonely Birdhouse Top": LocData(2000345014, "Time Rift - Alpine Skyline"),
+ "Alpine Skyline (Rift) - Page: Below Aqueduct": LocData(2000345013, "Time Rift - Alpine Skyline"),
+
+ "Deep Sea - Page: Starfish": LocData(2000346454, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1),
+ "Deep Sea - Page: Mini Castle": LocData(2000346452, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1),
+ "Deep Sea - Page: Urchins": LocData(2000346449, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1),
+
+ "Deep Sea - Page: Big Castle": LocData(2000346450, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1,
+ hookshot=True),
+
+ "Deep Sea - Page: Castle Top Chest": LocData(2000304850, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1,
+ hookshot=True),
+
+ "Deep Sea - Page: Urchin Ledge": LocData(2000346451, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1,
+ hookshot=True),
+
+ "Deep Sea - Page: Hidden Castle Chest": LocData(2000304849, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1,
+ hookshot=True),
+
+ "Deep Sea - Page: Falling Platform": LocData(2000346456, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1,
+ hookshot=True),
+
+ "Deep Sea - Page: Lava Starfish": LocData(2000346453, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1,
+ hookshot=True),
+
+ "Tour - Page: Mafia Town - Ledge": LocData(2000345038, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
+ "Tour - Page: Mafia Town - Beach": LocData(2000345039, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
+ "Tour - Page: Dead Bird Studio - C.A.W. Agents": LocData(2000345040, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
+ "Tour - Page: Dead Bird Studio - Fragile Box": LocData(2000345041, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
+ "Tour - Page: Subcon Forest - Giant Frozen Tree": LocData(2000345042, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
+ "Tour - Page: Subcon Forest - Top of Pillar": LocData(2000345043, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
+ "Tour - Page: Alpine Skyline - Birdhouse": LocData(2000345044, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
+ "Tour - Page: Alpine Skyline - Behind Lava Isle": LocData(2000345047, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
+ "Tour - Page: The Finale - Near Entrance": LocData(2000345087, "Time Rift - Tour", dlc_flags=HatDLC.dlc1),
+
+ "Rumbi Factory - Page: Manhole": LocData(2000345891, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
+ "Rumbi Factory - Page: Shutter Doors": LocData(2000345888, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
+
+ "Rumbi Factory - Page: Toxic Waste Dispenser": LocData(2000345892, "Time Rift - Rumbi Factory",
+ dlc_flags=HatDLC.dlc2),
+
+ "Rumbi Factory - Page: 3rd Area Ledge": LocData(2000345889, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
+
+ "Rumbi Factory - Page: Green Box Assembly Line": LocData(2000345884, "Time Rift - Rumbi Factory",
+ dlc_flags=HatDLC.dlc2),
+
+ "Rumbi Factory - Page: Broken Window": LocData(2000345885, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
+ "Rumbi Factory - Page: Money Vault": LocData(2000345890, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
+ "Rumbi Factory - Page: Warehouse Boxes": LocData(2000345887, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
+ "Rumbi Factory - Page: Glass Shelf": LocData(2000345886, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
+ "Rumbi Factory - Page: Last Area": LocData(2000345883, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
+}
+
+shop_locations = {
+ "Badge Seller - Item 1": LocData(2000301003, "Badge Seller"),
+ "Badge Seller - Item 2": LocData(2000301004, "Badge Seller"),
+ "Badge Seller - Item 3": LocData(2000301005, "Badge Seller"),
+ "Badge Seller - Item 4": LocData(2000301006, "Badge Seller"),
+ "Badge Seller - Item 5": LocData(2000301007, "Badge Seller"),
+ "Badge Seller - Item 6": LocData(2000301008, "Badge Seller"),
+ "Badge Seller - Item 7": LocData(2000301009, "Badge Seller"),
+ "Badge Seller - Item 8": LocData(2000301010, "Badge Seller"),
+ "Badge Seller - Item 9": LocData(2000301011, "Badge Seller"),
+ "Badge Seller - Item 10": LocData(2000301012, "Badge Seller"),
+ "Mafia Boss Shop Item": LocData(2000301013, "Spaceship"),
+
+ "Yellow Overpass Station - Yellow Ticket Booth": LocData(2000301014, "Yellow Overpass Station",
+ dlc_flags=HatDLC.dlc2),
+
+ "Green Clean Station - Green Ticket Booth": LocData(2000301015, "Green Clean Station", dlc_flags=HatDLC.dlc2),
+ "Bluefin Tunnel - Blue Ticket Booth": LocData(2000301016, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2),
+
+ "Pink Paw Station - Pink Ticket Booth": LocData(2000301017, "Pink Paw Station", dlc_flags=HatDLC.dlc2,
+ hookshot=True, required_hats=[HatType.DWELLER]),
+
+ "Main Station Thug A - Item 1": LocData(2000301048, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_0"),
+ "Main Station Thug A - Item 2": LocData(2000301049, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_0"),
+ "Main Station Thug A - Item 3": LocData(2000301050, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_0"),
+ "Main Station Thug A - Item 4": LocData(2000301051, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_0"),
+ "Main Station Thug A - Item 5": LocData(2000301052, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_0"),
+
+ "Main Station Thug B - Item 1": LocData(2000301053, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_1"),
+ "Main Station Thug B - Item 2": LocData(2000301054, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_1"),
+ "Main Station Thug B - Item 3": LocData(2000301055, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_1"),
+ "Main Station Thug B - Item 4": LocData(2000301056, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_1"),
+ "Main Station Thug B - Item 5": LocData(2000301057, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_1"),
+
+ "Main Station Thug C - Item 1": LocData(2000301058, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_2"),
+ "Main Station Thug C - Item 2": LocData(2000301059, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_2"),
+ "Main Station Thug C - Item 3": LocData(2000301060, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_2"),
+ "Main Station Thug C - Item 4": LocData(2000301061, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_2"),
+ "Main Station Thug C - Item 5": LocData(2000301062, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_2"),
+
+ "Yellow Overpass Thug A - Item 1": LocData(2000301018, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_13"),
+ "Yellow Overpass Thug A - Item 2": LocData(2000301019, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_13"),
+ "Yellow Overpass Thug A - Item 3": LocData(2000301020, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_13"),
+ "Yellow Overpass Thug A - Item 4": LocData(2000301021, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_13"),
+ "Yellow Overpass Thug A - Item 5": LocData(2000301022, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_13"),
+
+ "Yellow Overpass Thug B - Item 1": LocData(2000301043, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_5"),
+ "Yellow Overpass Thug B - Item 2": LocData(2000301044, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_5"),
+ "Yellow Overpass Thug B - Item 3": LocData(2000301045, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_5"),
+ "Yellow Overpass Thug B - Item 4": LocData(2000301046, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_5"),
+ "Yellow Overpass Thug B - Item 5": LocData(2000301047, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_5"),
+
+ "Yellow Overpass Thug C - Item 1": LocData(2000301063, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_14"),
+ "Yellow Overpass Thug C - Item 2": LocData(2000301064, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_14"),
+ "Yellow Overpass Thug C - Item 3": LocData(2000301065, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_14"),
+ "Yellow Overpass Thug C - Item 4": LocData(2000301066, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_14"),
+ "Yellow Overpass Thug C - Item 5": LocData(2000301067, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_14"),
+
+ "Green Clean Station Thug A - Item 1": LocData(2000301033, "Green Clean Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_4"),
+ "Green Clean Station Thug A - Item 2": LocData(2000301034, "Green Clean Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_4"),
+ "Green Clean Station Thug A - Item 3": LocData(2000301035, "Green Clean Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_4"),
+ "Green Clean Station Thug A - Item 4": LocData(2000301036, "Green Clean Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_4"),
+ "Green Clean Station Thug A - Item 5": LocData(2000301037, "Green Clean Station", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_4"),
+
+ # This guy requires either the yellow ticket or the Ice Hat
+ "Green Clean Station Thug B - Item 1": LocData(2000301028, "Green Clean Station", dlc_flags=HatDLC.dlc2,
+ required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"),
+ "Green Clean Station Thug B - Item 2": LocData(2000301029, "Green Clean Station", dlc_flags=HatDLC.dlc2,
+ required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"),
+ "Green Clean Station Thug B - Item 3": LocData(2000301030, "Green Clean Station", dlc_flags=HatDLC.dlc2,
+ required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"),
+ "Green Clean Station Thug B - Item 4": LocData(2000301031, "Green Clean Station", dlc_flags=HatDLC.dlc2,
+ required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"),
+ "Green Clean Station Thug B - Item 5": LocData(2000301032, "Green Clean Station", dlc_flags=HatDLC.dlc2,
+ required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"),
+
+ "Bluefin Tunnel Thug - Item 1": LocData(2000301023, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_7"),
+ "Bluefin Tunnel Thug - Item 2": LocData(2000301024, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_7"),
+ "Bluefin Tunnel Thug - Item 3": LocData(2000301025, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_7"),
+ "Bluefin Tunnel Thug - Item 4": LocData(2000301026, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_7"),
+ "Bluefin Tunnel Thug - Item 5": LocData(2000301027, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_7"),
+
+ "Pink Paw Station Thug - Item 1": LocData(2000301038, "Pink Paw Station", dlc_flags=HatDLC.dlc2,
+ required_hats=[HatType.DWELLER], hookshot=True,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_12"),
+ "Pink Paw Station Thug - Item 2": LocData(2000301039, "Pink Paw Station", dlc_flags=HatDLC.dlc2,
+ required_hats=[HatType.DWELLER], hookshot=True,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_12"),
+ "Pink Paw Station Thug - Item 3": LocData(2000301040, "Pink Paw Station", dlc_flags=HatDLC.dlc2,
+ required_hats=[HatType.DWELLER], hookshot=True,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_12"),
+ "Pink Paw Station Thug - Item 4": LocData(2000301041, "Pink Paw Station", dlc_flags=HatDLC.dlc2,
+ required_hats=[HatType.DWELLER], hookshot=True,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_12"),
+ "Pink Paw Station Thug - Item 5": LocData(2000301042, "Pink Paw Station", dlc_flags=HatDLC.dlc2,
+ required_hats=[HatType.DWELLER], hookshot=True,
+ nyakuza_thug="Hat_NPC_NyakuzaShop_12"),
+
+}
+
+contract_locations = {
+ "Snatcher's Contract - The Subcon Well": LocData(2000300200, "Contractual Obligations"),
+ "Snatcher's Contract - Toilet of Doom": LocData(2000300201, "Subcon Forest Area", paintings=1),
+ "Snatcher's Contract - Queen Vanessa's Manor": LocData(2000300202, "Subcon Forest Area", paintings=1),
+ "Snatcher's Contract - Mail Delivery Service": LocData(2000300203, "Subcon Forest Area", paintings=1),
+}
+
+# Don't put any of the locations from peaks here, the rules for their entrances are set already
+zipline_unlocks = {
+ "Alpine Skyline - Bird Pass Fork": "Zipline Unlock - The Birdhouse Path",
+ "Alpine Skyline - Yellow Band Hills": "Zipline Unlock - The Birdhouse Path",
+ "Alpine Skyline - The Purrloined Village: Horned Stone": "Zipline Unlock - The Birdhouse Path",
+ "Alpine Skyline - The Purrloined Village: Chest Reward": "Zipline Unlock - The Birdhouse Path",
+
+ "Alpine Skyline - Mystifying Time Mesa: Zipline": "Zipline Unlock - The Lava Cake Path",
+ "Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": "Zipline Unlock - The Lava Cake Path",
+ "Alpine Skyline - Ember Summit": "Zipline Unlock - The Lava Cake Path",
+
+ "Alpine Skyline - Goat Outpost Horn": "Zipline Unlock - The Windmill Path",
+ "Alpine Skyline - Windy Passage": "Zipline Unlock - The Windmill Path",
+
+ "Alpine Skyline - The Twilight Path": "Zipline Unlock - The Twilight Bell Path",
+}
+
+# act completion rules should be set automatically as these are all event items
+zero_jumps_hard = {
+ "Time Rift - Sewers (Zero Jumps)": LocData(0, "Time Rift - Sewers",
+ required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish),
+
+ "Time Rift - Bazaar (Zero Jumps)": LocData(0, "Time Rift - Bazaar",
+ required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish),
+
+ "The Big Parade (Zero Jumps)": LocData(0, "The Big Parade",
+ hit_type=HitType.umbrella,
+ required_hats=[HatType.ICE],
+ dlc_flags=HatDLC.death_wish),
+
+ "Time Rift - Pipe (Zero Jumps)": LocData(0, "Time Rift - Pipe", hookshot=True, dlc_flags=HatDLC.death_wish),
+
+ "Time Rift - Curly Tail Trail (Zero Jumps)": LocData(0, "Time Rift - Curly Tail Trail",
+ required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish),
+
+ "Time Rift - The Twilight Bell (Zero Jumps)": LocData(0, "Time Rift - The Twilight Bell",
+ required_hats=[HatType.ICE, HatType.DWELLER],
+ hit_type=HitType.umbrella_or_brewing,
+ dlc_flags=HatDLC.death_wish),
+
+ "The Illness has Spread (Zero Jumps)": LocData(0, "The Illness has Spread",
+ required_hats=[HatType.ICE], hookshot=True,
+ hit_type=HitType.umbrella_or_brewing, dlc_flags=HatDLC.death_wish),
+
+ "The Finale (Zero Jumps)": LocData(0, "The Finale",
+ required_hats=[HatType.ICE, HatType.DWELLER],
+ hookshot=True,
+ dlc_flags=HatDLC.death_wish),
+
+ "Pink Paw Station (Zero Jumps)": LocData(0, "Pink Paw Station",
+ required_hats=[HatType.ICE],
+ hookshot=True,
+ dlc_flags=HatDLC.dlc2_dw),
+}
+
+zero_jumps_expert = {
+ "The Birdhouse (Zero Jumps)": LocData(0, "The Birdhouse",
+ required_hats=[HatType.ICE],
+ dlc_flags=HatDLC.death_wish),
+
+ "The Lava Cake (Zero Jumps)": LocData(0, "The Lava Cake", dlc_flags=HatDLC.death_wish),
+
+ "The Windmill (Zero Jumps)": LocData(0, "The Windmill",
+ required_hats=[HatType.ICE],
+ misc_required=["No Bonk Badge"],
+ dlc_flags=HatDLC.death_wish),
+ "The Twilight Bell (Zero Jumps)": LocData(0, "The Twilight Bell",
+ required_hats=[HatType.ICE, HatType.DWELLER],
+ hit_type=HitType.umbrella_or_brewing,
+ misc_required=["No Bonk Badge"],
+ dlc_flags=HatDLC.death_wish),
+
+ "Sleepy Subcon (Zero Jumps)": LocData(0, "Time Rift - Sleepy Subcon", required_hats=[HatType.ICE],
+ dlc_flags=HatDLC.death_wish),
+
+ "Ship Shape (Zero Jumps)": LocData(0, "Ship Shape", required_hats=[HatType.ICE], dlc_flags=HatDLC.dlc1_dw),
+}
+
+zero_jumps = {
+ **zero_jumps_hard,
+ **zero_jumps_expert,
+ "Welcome to Mafia Town (Zero Jumps)": LocData(0, "Welcome to Mafia Town", dlc_flags=HatDLC.death_wish),
+
+ "Down with the Mafia! (Zero Jumps)": LocData(0, "Down with the Mafia!",
+ required_hats=[HatType.ICE],
+ dlc_flags=HatDLC.death_wish),
+
+ "Cheating the Race (Zero Jumps)": LocData(0, "Cheating the Race",
+ required_hats=[HatType.TIME_STOP],
+ dlc_flags=HatDLC.death_wish),
+
+ "The Golden Vault (Zero Jumps)": LocData(0, "The Golden Vault",
+ required_hats=[HatType.ICE],
+ dlc_flags=HatDLC.death_wish),
+
+ "Dead Bird Studio (Zero Jumps)": LocData(0, "Dead Bird Studio",
+ required_hats=[HatType.ICE],
+ hit_type=HitType.umbrella_or_brewing,
+ dlc_flags=HatDLC.death_wish),
+
+ "Murder on the Owl Express (Zero Jumps)": LocData(0, "Murder on the Owl Express",
+ required_hats=[HatType.ICE],
+ dlc_flags=HatDLC.death_wish),
+
+ "Picture Perfect (Zero Jumps)": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish),
+
+ "Train Rush (Zero Jumps)": LocData(0, "Train Rush",
+ required_hats=[HatType.ICE],
+ hookshot=True,
+ dlc_flags=HatDLC.death_wish),
+
+ "Contractual Obligations (Zero Jumps)": LocData(0, "Contractual Obligations",
+ paintings=1,
+ dlc_flags=HatDLC.death_wish),
+
+ "Your Contract has Expired (Zero Jumps)": LocData(0, "Your Contract has Expired",
+ hit_type=HitType.umbrella,
+ dlc_flags=HatDLC.death_wish),
+
+ # No ice hat/painting required in Expert
+ "Toilet of Doom (Zero Jumps)": LocData(0, "Toilet of Doom",
+ hookshot=True,
+ hit_type=HitType.umbrella_or_brewing,
+ required_hats=[HatType.ICE],
+ paintings=1,
+ dlc_flags=HatDLC.death_wish),
+
+ "Mail Delivery Service (Zero Jumps)": LocData(0, "Mail Delivery Service",
+ required_hats=[HatType.SPRINT],
+ dlc_flags=HatDLC.death_wish),
+
+ "Time Rift - Alpine Skyline (Zero Jumps)": LocData(0, "Time Rift - Alpine Skyline",
+ required_hats=[HatType.ICE],
+ hookshot=True,
+ dlc_flags=HatDLC.death_wish),
+
+ "Time Rift - The Lab (Zero Jumps)": LocData(0, "Time Rift - The Lab",
+ required_hats=[HatType.ICE],
+ dlc_flags=HatDLC.death_wish),
+
+ "Yellow Overpass Station (Zero Jumps)": LocData(0, "Yellow Overpass Station",
+ required_hats=[HatType.ICE],
+ hookshot=True,
+ dlc_flags=HatDLC.dlc2_dw),
+
+ "Green Clean Station (Zero Jumps)": LocData(0, "Green Clean Station",
+ required_hats=[HatType.ICE],
+ dlc_flags=HatDLC.dlc2_dw),
+}
+
+snatcher_coins = {
+ "Snatcher Coin - Top of HQ (DWTM)": LocData(0, "Down with the Mafia!", snatcher_coin="Snatcher Coin - Top of HQ",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Top of HQ (CTR)": LocData(0, "Cheating the Race", snatcher_coin="Snatcher Coin - Top of HQ",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Top of HQ (HUMT)": LocData(0, "Heating Up Mafia Town", snatcher_coin="Snatcher Coin - Top of HQ",
+ hit_type=HitType.umbrella, dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Top of HQ (TGV)": LocData(0, "The Golden Vault", snatcher_coin="Snatcher Coin - Top of HQ",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Top of HQ (DW: BTH)": LocData(0, "Beat the Heat", snatcher_coin="Snatcher Coin - Top of HQ",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Top of Tower": LocData(0, "Mafia Town Area (HUMT)", snatcher_coin="Snatcher Coin - Top of Tower",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Top of Tower (DW: BTH)": LocData(0, "Beat the Heat", snatcher_coin="Snatcher Coin - Top of Tower",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Top of Tower (DW: CAT)": LocData(0, "Collect-a-thon", snatcher_coin="Snatcher Coin - Top of Tower",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Top of Tower (SSFOS)": LocData(0, "She Speedran from Outer Space",
+ snatcher_coin="Snatcher Coin - Top of Tower",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Top of Tower (DW: MJ)": LocData(0, "Mafia's Jumps", snatcher_coin="Snatcher Coin - Top of Tower",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Under Ruined Tower": LocData(0, "Mafia Town Area",
+ snatcher_coin="Snatcher Coin - Under Ruined Tower",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Under Ruined Tower (DW: CAT)": LocData(0, "Collect-a-thon",
+ snatcher_coin="Snatcher Coin - Under Ruined Tower",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Under Ruined Tower (DW: SSFOS)": LocData(0, "She Speedran from Outer Space",
+ snatcher_coin="Snatcher Coin - Under Ruined Tower",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Top of Red House (DBS)": LocData(0, "Dead Bird Studio - Elevator Area",
+ snatcher_coin="Snatcher Coin - Top of Red House",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Top of Red House (DW: SB)": LocData(0, "Security Breach",
+ snatcher_coin="Snatcher Coin - Top of Red House",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Train Rush": LocData(0, "Train Rush", snatcher_coin="Snatcher Coin - Train Rush",
+ hookshot=True, dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Train Rush (10 Seconds)": LocData(0, "10 Seconds until Self-Destruct",
+ snatcher_coin="Snatcher Coin - Train Rush",
+ hookshot=True, dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Picture Perfect": LocData(0, "Picture Perfect", snatcher_coin="Snatcher Coin - Picture Perfect",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Swamp Tree": LocData(0, "Subcon Forest Area", snatcher_coin="Snatcher Coin - Swamp Tree",
+ hookshot=True, paintings=1,
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Swamp Tree (Speedrun Well)": LocData(0, "Speedrun Well",
+ snatcher_coin="Snatcher Coin - Swamp Tree",
+ hookshot=True, dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Manor Roof": LocData(0, "Subcon Forest Area", snatcher_coin="Snatcher Coin - Manor Roof",
+ hit_type=HitType.dweller_bell, paintings=1,
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Giant Time Piece": LocData(0, "Subcon Forest Area",
+ snatcher_coin="Snatcher Coin - Giant Time Piece",
+ paintings=3, dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Goat Village Top": LocData(0, "Alpine Skyline Area (TIHS)",
+ snatcher_coin="Snatcher Coin - Goat Village Top",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Goat Village Top (Illness Speedrun)": LocData(0, "The Illness has Speedrun",
+ snatcher_coin="Snatcher Coin - Goat Village Top",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Lava Cake": LocData(0, "The Lava Cake", snatcher_coin="Snatcher Coin - Lava Cake",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Windmill": LocData(0, "The Windmill", snatcher_coin="Snatcher Coin - Windmill",
+ dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Windmill (DW: WUW)": LocData(0, "Wound-Up Windmill", snatcher_coin="Snatcher Coin - Windmill",
+ hookshot=True, dlc_flags=HatDLC.death_wish),
+
+ "Snatcher Coin - Green Clean Tower": LocData(0, "Green Clean Station",
+ snatcher_coin="Snatcher Coin - Green Clean Tower",
+ dlc_flags=HatDLC.dlc2_dw),
+
+ "Snatcher Coin - Bluefin Cat Train": LocData(0, "Bluefin Tunnel",
+ snatcher_coin="Snatcher Coin - Bluefin Cat Train",
+ dlc_flags=HatDLC.dlc2_dw),
+
+ "Snatcher Coin - Pink Paw Fence": LocData(0, "Pink Paw Station",
+ snatcher_coin="Snatcher Coin - Pink Paw Fence",
+ dlc_flags=HatDLC.dlc2_dw),
+}
+
+event_locs = {
+ **zero_jumps,
+ **snatcher_coins,
+ "HUMT Access": LocData(0, "Heating Up Mafia Town"),
+ "TOD Access": LocData(0, "Toilet of Doom"),
+ "YCHE Access": LocData(0, "Your Contract has Expired"),
+ "AFR Access": LocData(0, "Alpine Free Roam"),
+ "TIHS Access": LocData(0, "The Illness has Spread"),
+
+ "Birdhouse Cleared": LocData(0, "The Birdhouse", act_event=True),
+ "Lava Cake Cleared": LocData(0, "The Lava Cake", act_event=True),
+ "Windmill Cleared": LocData(0, "The Windmill", act_event=True),
+ "Twilight Bell Cleared": LocData(0, "The Twilight Bell", act_event=True),
+ "Time Piece Cluster": LocData(0, "The Finale", act_event=True),
+
+ # not really an act
+ "Nyakuza Intro Cleared": LocData(0, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2),
+
+ "Yellow Overpass Station Cleared": LocData(0, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, act_event=True),
+ "Green Clean Station Cleared": LocData(0, "Green Clean Station", dlc_flags=HatDLC.dlc2, act_event=True),
+ "Bluefin Tunnel Cleared": LocData(0, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, act_event=True),
+ "Pink Paw Station Cleared": LocData(0, "Pink Paw Station", dlc_flags=HatDLC.dlc2, act_event=True),
+ "Yellow Overpass Manhole Cleared": LocData(0, "Yellow Overpass Manhole", dlc_flags=HatDLC.dlc2, act_event=True),
+ "Green Clean Manhole Cleared": LocData(0, "Green Clean Manhole", dlc_flags=HatDLC.dlc2, act_event=True),
+ "Pink Paw Manhole Cleared": LocData(0, "Pink Paw Manhole", dlc_flags=HatDLC.dlc2, act_event=True),
+ "Rush Hour Cleared": LocData(0, "Rush Hour", dlc_flags=HatDLC.dlc2, act_event=True),
+}
+
+# DO NOT ALTER THE ORDER OF THIS LIST
+death_wishes = {
+ "Beat the Heat": 2000350000,
+ "Snatcher's Hit List": 2000350002,
+ "So You're Back From Outer Space": 2000350004,
+ "Collect-a-thon": 2000350006,
+ "Rift Collapse: Mafia of Cooks": 2000350008,
+ "She Speedran from Outer Space": 2000350010,
+ "Mafia's Jumps": 2000350012,
+ "Vault Codes in the Wind": 2000350014,
+ "Encore! Encore!": 2000350016,
+ "Snatcher Coins in Mafia Town": 2000350018,
+
+ "Security Breach": 2000350020,
+ "The Great Big Hootenanny": 2000350022,
+ "Rift Collapse: Dead Bird Studio": 2000350024,
+ "10 Seconds until Self-Destruct": 2000350026,
+ "Killing Two Birds": 2000350028,
+ "Snatcher Coins in Battle of the Birds": 2000350030,
+ "Zero Jumps": 2000350032,
+
+ "Speedrun Well": 2000350034,
+ "Rift Collapse: Sleepy Subcon": 2000350036,
+ "Boss Rush": 2000350038,
+ "Quality Time with Snatcher": 2000350040,
+ "Breaching the Contract": 2000350042,
+ "Snatcher Coins in Subcon Forest": 2000350044,
+
+ "Bird Sanctuary": 2000350046,
+ "Rift Collapse: Alpine Skyline": 2000350048,
+ "Wound-Up Windmill": 2000350050,
+ "The Illness has Speedrun": 2000350052,
+ "Snatcher Coins in Alpine Skyline": 2000350054,
+ "Camera Tourist": 2000350056,
+
+ "The Mustache Gauntlet": 2000350058,
+ "No More Bad Guys": 2000350060,
+
+ "Seal the Deal": 2000350062,
+ "Rift Collapse: Deep Sea": 2000350064,
+ "Cruisin' for a Bruisin'": 2000350066,
+
+ "Community Rift: Rhythm Jump Studio": 2000350068,
+ "Community Rift: Twilight Travels": 2000350070,
+ "Community Rift: The Mountain Rift": 2000350072,
+ "Snatcher Coins in Nyakuza Metro": 2000350074,
+}
+
+location_table = {
+ **ahit_locations,
+ **act_completions,
+ **storybook_pages,
+ **contract_locations,
+ **shop_locations,
+}
diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py
new file mode 100644
index 000000000000..17c4b95efc7a
--- /dev/null
+++ b/worlds/ahit/Options.py
@@ -0,0 +1,770 @@
+from typing import List, TYPE_CHECKING, Dict, Any
+from schema import Schema, Optional
+from dataclasses import dataclass
+from worlds.AutoWorld import PerGameCommonOptions
+from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle, OptionGroup
+
+if TYPE_CHECKING:
+ from . import HatInTimeWorld
+
+
+def create_option_groups() -> List[OptionGroup]:
+ option_group_list: List[OptionGroup] = []
+ for name, options in ahit_option_groups.items():
+ option_group_list.append(OptionGroup(name=name, options=options))
+
+ return option_group_list
+
+
+def adjust_options(world: "HatInTimeWorld"):
+ if world.options.HighestChapterCost < world.options.LowestChapterCost:
+ world.options.HighestChapterCost.value, world.options.LowestChapterCost.value = \
+ world.options.LowestChapterCost.value, world.options.HighestChapterCost.value
+
+ if world.options.FinalChapterMaxCost < world.options.FinalChapterMinCost:
+ world.options.FinalChapterMaxCost.value, world.options.FinalChapterMinCost.value = \
+ world.options.FinalChapterMinCost.value, world.options.FinalChapterMaxCost.value
+
+ if world.options.BadgeSellerMaxItems < world.options.BadgeSellerMinItems:
+ world.options.BadgeSellerMaxItems.value, world.options.BadgeSellerMinItems.value = \
+ world.options.BadgeSellerMinItems.value, world.options.BadgeSellerMaxItems.value
+
+ if world.options.NyakuzaThugMaxShopItems < world.options.NyakuzaThugMinShopItems:
+ world.options.NyakuzaThugMaxShopItems.value, world.options.NyakuzaThugMinShopItems.value = \
+ world.options.NyakuzaThugMinShopItems.value, world.options.NyakuzaThugMaxShopItems.value
+
+ if world.options.DWShuffleCountMax < world.options.DWShuffleCountMin:
+ world.options.DWShuffleCountMax.value, world.options.DWShuffleCountMin.value = \
+ world.options.DWShuffleCountMin.value, world.options.DWShuffleCountMax.value
+
+ total_tps: int = get_total_time_pieces(world)
+ if world.options.HighestChapterCost > total_tps-5:
+ world.options.HighestChapterCost.value = min(45, total_tps-5)
+
+ if world.options.LowestChapterCost > total_tps-5:
+ world.options.LowestChapterCost.value = min(45, total_tps-5)
+
+ if world.options.FinalChapterMaxCost > total_tps:
+ world.options.FinalChapterMaxCost.value = min(50, total_tps)
+
+ if world.options.FinalChapterMinCost > total_tps:
+ world.options.FinalChapterMinCost.value = min(50, total_tps)
+
+ if world.is_dlc1() and world.options.ShipShapeCustomTaskGoal <= 0:
+ # automatically determine task count based on Tasksanity settings
+ if world.options.Tasksanity:
+ world.options.ShipShapeCustomTaskGoal.value = world.options.TasksanityCheckCount * world.options.TasksanityTaskStep
+ else:
+ world.options.ShipShapeCustomTaskGoal.value = 18
+
+ # Don't allow Rush Hour goal if DLC2 content is disabled
+ if world.options.EndGoal == EndGoal.option_rush_hour and not world.options.EnableDLC2:
+ world.options.EndGoal.value = EndGoal.option_finale
+
+ # Don't allow Seal the Deal goal if Death Wish content is disabled
+ if world.options.EndGoal == EndGoal.option_seal_the_deal and not world.is_dw():
+ world.options.EndGoal.value = EndGoal.option_finale
+
+ if world.options.DWEnableBonus:
+ world.options.DWAutoCompleteBonuses.value = 0
+
+ if world.is_dw_only():
+ world.options.EndGoal.value = EndGoal.option_seal_the_deal
+ world.options.ActRandomizer.value = 0
+ world.options.ShuffleAlpineZiplines.value = 0
+ world.options.ShuffleSubconPaintings.value = 0
+ world.options.ShuffleStorybookPages.value = 0
+ world.options.ShuffleActContracts.value = 0
+ world.options.EnableDLC1.value = 0
+ world.options.LogicDifficulty.value = LogicDifficulty.option_normal
+ world.options.DWTimePieceRequirement.value = 0
+
+
+def get_total_time_pieces(world: "HatInTimeWorld") -> int:
+ count: int = 40
+ if world.is_dlc1():
+ count += 6
+
+ if world.is_dlc2():
+ count += 10
+
+ return min(40+world.options.MaxExtraTimePieces, count)
+
+
+class EndGoal(Choice):
+ """The end goal required to beat the game.
+ Finale: Reach Time's End and beat Mustache Girl. The Finale will be in its vanilla location.
+
+ Rush Hour: Reach and complete Rush Hour. The level will be in its vanilla location and Chapter 7
+ will be the final chapter. You also must find Nyakuza Metro itself and complete all of its levels.
+ Requires DLC2 content to be enabled.
+
+ Seal the Deal: Reach and complete the Seal the Deal death wish main objective.
+ Requires Death Wish content to be enabled."""
+ display_name = "End Goal"
+ option_finale = 1
+ option_rush_hour = 2
+ option_seal_the_deal = 3
+ default = 1
+
+
+class ActRandomizer(Choice):
+ """If enabled, shuffle the game's Acts between each other.
+ Light will cause Time Rifts to only be shuffled amongst each other,
+ and Blue Time Rifts and Purple Time Rifts to be shuffled separately."""
+ display_name = "Shuffle Acts"
+ option_false = 0
+ option_light = 1
+ option_insanity = 2
+ default = 1
+
+
+class ActPlando(OptionDict):
+ """Plando acts onto other acts. For example, \"Train Rush\": \"Alpine Free Roam\" will place Alpine Free Roam
+ at Train Rush."""
+ display_name = "Act Plando"
+ schema = Schema({
+ Optional(str): str
+ })
+
+
+class ActBlacklist(OptionDict):
+ """Blacklist acts from being shuffled onto other acts. Multiple can be listed per act.
+ For example, \"Barrel Battle\": [\"The Big Parade\", \"Dead Bird Studio\"]
+ will prevent The Big Parade and Dead Bird Studio from being shuffled onto Barrel Battle."""
+ display_name = "Act Blacklist"
+ schema = Schema({
+ Optional(str): list
+ })
+
+
+class FinaleShuffle(Toggle):
+ """If enabled, chapter finales will only be shuffled amongst each other in act shuffle."""
+ display_name = "Finale Shuffle"
+
+
+class LogicDifficulty(Choice):
+ """Choose the difficulty setting for logic.
+ For an exhaustive list of all logic tricks for each difficulty, see this Google Doc:
+ https://docs.google.com/document/d/1x9VLSQ5davfx1KGamR9T0mD5h69_lDXJ6H7Gq7knJRI/edit?usp=sharing"""
+ display_name = "Logic Difficulty"
+ option_normal = -1
+ option_moderate = 0
+ option_hard = 1
+ option_expert = 2
+ default = -1
+
+
+class CTRLogic(Choice):
+ """Choose how you want to logically clear Cheating the Race."""
+ display_name = "Cheating the Race Logic"
+ option_time_stop_only = 0
+ option_scooter = 1
+ option_sprint = 2
+ option_nothing = 3
+ default = 0
+
+
+class RandomizeHatOrder(Choice):
+ """Randomize the order that hats are stitched in.
+ Time Stop Last will force Time Stop to be the last hat in the sequence."""
+ display_name = "Randomize Hat Order"
+ option_false = 0
+ option_true = 1
+ option_time_stop_last = 2
+ default = 1
+
+
+class YarnBalancePercent(Range):
+ """How much (in percentage) of the yarn in the pool that will be progression balanced."""
+ display_name = "Yarn Balance Percentage"
+ default = 20
+ range_start = 0
+ range_end = 100
+
+
+class TimePieceBalancePercent(Range):
+ """How much (in percentage) of time pieces in the pool that will be progression balanced."""
+ display_name = "Time Piece Balance Percentage"
+ default = 35
+ range_start = 0
+ range_end = 100
+
+
+class StartWithCompassBadge(DefaultOnToggle):
+ """If enabled, start with the Compass Badge. In Archipelago, the Compass Badge will track all items in the world
+ (instead of just Relics). Recommended if you're not familiar with where item locations are."""
+ display_name = "Start with Compass Badge"
+
+
+class CompassBadgeMode(Choice):
+ """closest - Compass Badge points to the closest item regardless of classification
+ important_only - Compass Badge points to progression/useful items only
+ important_first - Compass Badge points to progression/useful items first, then it will point to junk items"""
+ display_name = "Compass Badge Mode"
+ option_closest = 1
+ option_important_only = 2
+ option_important_first = 3
+ default = 1
+
+
+class UmbrellaLogic(Toggle):
+ """Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful"""
+ display_name = "Umbrella Logic"
+
+
+class ShuffleStorybookPages(DefaultOnToggle):
+ """If enabled, each storybook page in the purple Time Rifts is an item check.
+ The Compass Badge can track these down for you."""
+ display_name = "Shuffle Storybook Pages"
+
+
+class ShuffleActContracts(DefaultOnToggle):
+ """If enabled, shuffle Snatcher's act contracts into the pool as items"""
+ display_name = "Shuffle Contracts"
+
+
+class ShuffleAlpineZiplines(Toggle):
+ """If enabled, Alpine's zipline paths leading to the peaks will be locked behind items."""
+ display_name = "Shuffle Alpine Ziplines"
+
+
+class ShuffleSubconPaintings(Toggle):
+ """If enabled, shuffle items into the pool that unlock Subcon Forest fire spirit paintings.
+ These items are progressive, with the order of Village-Swamp-Courtyard."""
+ display_name = "Shuffle Subcon Paintings"
+
+
+class NoPaintingSkips(Toggle):
+ """If enabled, prevent Subcon fire wall skips from being in logic on higher difficulty settings."""
+ display_name = "No Subcon Fire Wall Skips"
+
+
+class StartingChapter(Choice):
+ """Determines which chapter you will be guaranteed to be able to enter at the beginning of the game."""
+ display_name = "Starting Chapter"
+ option_1 = 1
+ option_2 = 2
+ option_3 = 3
+ option_4 = 4
+ default = 1
+
+
+class ChapterCostIncrement(Range):
+ """Lower values mean chapter costs increase slower. Higher values make the cost differences more steep."""
+ display_name = "Chapter Cost Increment"
+ range_start = 1
+ range_end = 8
+ default = 4
+
+
+class ChapterCostMinDifference(Range):
+ """The minimum difference between chapter costs."""
+ display_name = "Minimum Chapter Cost Difference"
+ range_start = 1
+ range_end = 8
+ default = 4
+
+
+class LowestChapterCost(Range):
+ """Value determining the lowest possible cost for a chapter.
+ Chapter costs will, progressively, be calculated based on this value (except for the final chapter)."""
+ display_name = "Lowest Possible Chapter Cost"
+ range_start = 0
+ range_end = 10
+ default = 5
+
+
+class HighestChapterCost(Range):
+ """Value determining the highest possible cost for a chapter.
+ Chapter costs will, progressively, be calculated based on this value (except for the final chapter)."""
+ display_name = "Highest Possible Chapter Cost"
+ range_start = 15
+ range_end = 45
+ default = 25
+
+
+class FinalChapterMinCost(Range):
+ """Minimum Time Pieces required to enter the final chapter. This is part of your goal."""
+ display_name = "Final Chapter Minimum Time Piece Cost"
+ range_start = 0
+ range_end = 50
+ default = 30
+
+
+class FinalChapterMaxCost(Range):
+ """Maximum Time Pieces required to enter the final chapter. This is part of your goal."""
+ display_name = "Final Chapter Maximum Time Piece Cost"
+ range_start = 0
+ range_end = 50
+ default = 35
+
+
+class MaxExtraTimePieces(Range):
+ """Maximum number of extra Time Pieces from the DLCs.
+ Arctic Cruise will add up to 6. Nyakuza Metro will add up to 10. The absolute maximum is 56."""
+ display_name = "Max Extra Time Pieces"
+ range_start = 0
+ range_end = 16
+ default = 16
+
+
+class YarnCostMin(Range):
+ """The minimum possible yarn needed to stitch a hat."""
+ display_name = "Minimum Yarn Cost"
+ range_start = 1
+ range_end = 12
+ default = 4
+
+
+class YarnCostMax(Range):
+ """The maximum possible yarn needed to stitch a hat."""
+ display_name = "Maximum Yarn Cost"
+ range_start = 1
+ range_end = 12
+ default = 8
+
+
+class YarnAvailable(Range):
+ """How much yarn is available to collect in the item pool."""
+ display_name = "Yarn Available"
+ range_start = 30
+ range_end = 80
+ default = 50
+
+
+class MinExtraYarn(Range):
+ """The minimum number of extra yarn in the item pool.
+ There must be at least this much more yarn over the total number of yarn needed to craft all hats.
+ For example, if this option's value is 10, and the total yarn needed to craft all hats is 40,
+ there must be at least 50 yarn in the pool."""
+ display_name = "Max Extra Yarn"
+ range_start = 5
+ range_end = 15
+ default = 10
+
+
+class HatItems(Toggle):
+ """Removes all yarn from the pool and turns the hats into individual items instead."""
+ display_name = "Hat Items"
+
+
+class MinPonCost(Range):
+ """The minimum number of Pons that any item in the Badge Seller's shop can cost."""
+ display_name = "Minimum Shop Pon Cost"
+ range_start = 10
+ range_end = 800
+ default = 75
+
+
+class MaxPonCost(Range):
+ """The maximum number of Pons that any item in the Badge Seller's shop can cost."""
+ display_name = "Maximum Shop Pon Cost"
+ range_start = 10
+ range_end = 800
+ default = 300
+
+
+class BadgeSellerMinItems(Range):
+ """The smallest number of items that the Badge Seller can have for sale."""
+ display_name = "Badge Seller Minimum Items"
+ range_start = 0
+ range_end = 10
+ default = 4
+
+
+class BadgeSellerMaxItems(Range):
+ """The largest number of items that the Badge Seller can have for sale."""
+ display_name = "Badge Seller Maximum Items"
+ range_start = 0
+ range_end = 10
+ default = 8
+
+
+class EnableDLC1(Toggle):
+ """Shuffle content from The Arctic Cruise (Chapter 6) into the game. This also includes the Tour time rift.
+ DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!"""
+ display_name = "Shuffle Chapter 6"
+
+
+class Tasksanity(Toggle):
+ """If enabled, Ship Shape tasks will become checks. Requires DLC1 content to be enabled."""
+ display_name = "Tasksanity"
+
+
+class TasksanityTaskStep(Range):
+ """How many tasks the player must complete in Tasksanity to send a check."""
+ display_name = "Tasksanity Task Step"
+ range_start = 1
+ range_end = 3
+ default = 1
+
+
+class TasksanityCheckCount(Range):
+ """How many Tasksanity checks there will be in total."""
+ display_name = "Tasksanity Check Count"
+ range_start = 1
+ range_end = 30
+ default = 18
+
+
+class ExcludeTour(Toggle):
+ """Removes the Tour time rift from the game. This option is recommended if you don't want to deal with
+ important levels being shuffled onto the Tour time rift, or important items being shuffled onto Tour pages
+ when your goal is Time's End."""
+ display_name = "Exclude Tour Time Rift"
+
+
+class ShipShapeCustomTaskGoal(Range):
+ """Change the number of tasks required to complete Ship Shape. If this option's value is 0, the number of tasks
+ required will be TasksanityTaskStep x TasksanityCheckCount, if Tasksanity is enabled. If Tasksanity is disabled,
+ it will use the game's default of 18.
+ This option will not affect Cruisin' for a Bruisin'."""
+ display_name = "Ship Shape Custom Task Goal"
+ range_start = 0
+ range_end = 90
+ default = 0
+
+
+class EnableDLC2(Toggle):
+ """Shuffle content from Nyakuza Metro (Chapter 7) into the game.
+ DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE NYAKUZA METRO DLC INSTALLED!!!"""
+ display_name = "Shuffle Chapter 7"
+
+
+class MetroMinPonCost(Range):
+ """The cheapest an item can be in any Nyakuza Metro shop. Includes ticket booths."""
+ display_name = "Metro Shops Minimum Pon Cost"
+ range_start = 10
+ range_end = 800
+ default = 50
+
+
+class MetroMaxPonCost(Range):
+ """The most expensive an item can be in any Nyakuza Metro shop. Includes ticket booths."""
+ display_name = "Metro Shops Maximum Pon Cost"
+ range_start = 10
+ range_end = 800
+ default = 200
+
+
+class NyakuzaThugMinShopItems(Range):
+ """The smallest number of items that the thugs in Nyakuza Metro can have for sale."""
+ display_name = "Nyakuza Thug Minimum Shop Items"
+ range_start = 0
+ range_end = 5
+ default = 2
+
+
+class NyakuzaThugMaxShopItems(Range):
+ """The largest number of items that the thugs in Nyakuza Metro can have for sale."""
+ display_name = "Nyakuza Thug Maximum Shop Items"
+ range_start = 0
+ range_end = 5
+ default = 4
+
+
+class NoTicketSkips(Choice):
+ """Prevent metro gate skips from being in logic on higher difficulties.
+ Rush Hour option will only consider the ticket skips for Rush Hour in logic."""
+ display_name = "No Ticket Skips"
+ option_false = 0
+ option_true = 1
+ option_rush_hour = 2
+
+
+class BaseballBat(Toggle):
+ """Replace the Umbrella with the baseball bat from Nyakuza Metro.
+ DLC2 content does not have to be shuffled for this option but Nyakuza Metro still needs to be installed."""
+ display_name = "Baseball Bat"
+
+
+class EnableDeathWish(Toggle):
+ """Shuffle Death Wish contracts into the game. Each contract by default will have 1 check granted upon completion.
+ DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!"""
+ display_name = "Enable Death Wish"
+
+
+class DeathWishOnly(Toggle):
+ """An alternative gameplay mode that allows you to exclusively play Death Wish in a seed.
+ This has the following effects:
+ - Death Wish is instantly unlocked from the start
+ - All hats and other progression items are instantly given to you
+ - Useful items such as Fast Hatter Badge will still be in the item pool instead of in your inventory at the start
+ - All chapters and their levels are unlocked, act shuffle is forced off
+ - Any checks other than Death Wish contracts are completely removed
+ - All Pons in the item pool are replaced with Health Pons or random cosmetics
+ - The EndGoal option is forced to complete Seal the Deal"""
+ display_name = "Death Wish Only"
+
+
+class DWShuffle(Toggle):
+ """An alternative mode for Death Wish where each contract is unlocked one by one, in a random order.
+ Stamp requirements to unlock contracts is removed. Any excluded contracts will not be shuffled into the sequence.
+ If Seal the Deal is the end goal, it will always be the last Death Wish in the sequence.
+ Disabling candles is highly recommended."""
+ display_name = "Death Wish Shuffle"
+
+
+class DWShuffleCountMin(Range):
+ """The minimum number of Death Wishes that can be in the Death Wish shuffle sequence.
+ The final result is clamped at the number of non-excluded Death Wishes."""
+ display_name = "Death Wish Shuffle Minimum Count"
+ range_start = 5
+ range_end = 38
+ default = 18
+
+
+class DWShuffleCountMax(Range):
+ """The maximum number of Death Wishes that can be in the Death Wish shuffle sequence.
+ The final result is clamped at the number of non-excluded Death Wishes."""
+ display_name = "Death Wish Shuffle Maximum Count"
+ range_start = 5
+ range_end = 38
+ default = 25
+
+
+class DWEnableBonus(Toggle):
+ """In Death Wish, add a location for completing all of a DW contract's bonuses,
+ in addition to the location for completing the DW contract normally.
+ WARNING!! Only for the brave! This option can create VERY DIFFICULT SEEDS!
+ ONLY turn this on if you know what you are doing to yourself and everyone else in the multiworld!
+ Using Peace and Tranquility to auto-complete the bonuses will NOT count!"""
+ display_name = "Shuffle Death Wish Full Completions"
+
+
+class DWAutoCompleteBonuses(DefaultOnToggle):
+ """If enabled, auto complete all bonus stamps after completing the main objective in a Death Wish.
+ This option will have no effect if bonus checks (DWEnableBonus) are turned on."""
+ display_name = "Auto Complete Bonus Stamps"
+
+
+class DWExcludeAnnoyingContracts(DefaultOnToggle):
+ """Exclude Death Wish contracts from the pool that are particularly tedious or take a long time to reach/clear.
+ Excluded Death Wishes are automatically completed as soon as they are unlocked.
+ This option currently excludes the following contracts:
+ - Vault Codes in the Wind
+ - Boss Rush
+ - Camera Tourist
+ - The Mustache Gauntlet
+ - Rift Collapse: Deep Sea
+ - Cruisin' for a Bruisin'
+ - Seal the Deal (non-excluded if goal, but the checks are still excluded)"""
+ display_name = "Exclude Annoying Death Wish Contracts"
+
+
+class DWExcludeAnnoyingBonuses(DefaultOnToggle):
+ """If Death Wish full completions are shuffled in, exclude tedious Death Wish full completions from the pool.
+ Excluded bonus Death Wishes automatically reward their bonus stamps upon completion of the main objective.
+ This option currently excludes the following bonuses:
+ - So You're Back From Outer Space
+ - Encore! Encore!
+ - Snatcher's Hit List
+ - 10 Seconds until Self-Destruct
+ - Killing Two Birds
+ - Zero Jumps
+ - Bird Sanctuary
+ - Wound-Up Windmill
+ - Vault Codes in the Wind
+ - Boss Rush
+ - Camera Tourist
+ - The Mustache Gauntlet
+ - Rift Collapse: Deep Sea
+ - Cruisin' for a Bruisin'
+ - Seal the Deal"""
+ display_name = "Exclude Annoying Death Wish Full Completions"
+
+
+class DWExcludeCandles(DefaultOnToggle):
+ """If enabled, exclude all candle Death Wishes."""
+ display_name = "Exclude Candle Death Wishes"
+
+
+class DWTimePieceRequirement(Range):
+ """How many Time Pieces that will be required to unlock Death Wish."""
+ display_name = "Death Wish Time Piece Requirement"
+ range_start = 0
+ range_end = 35
+ default = 15
+
+
+class TrapChance(Range):
+ """The chance for any junk item in the pool to be replaced by a trap."""
+ display_name = "Trap Chance"
+ range_start = 0
+ range_end = 100
+ default = 0
+
+
+class BabyTrapWeight(Range):
+ """The weight of Baby Traps in the trap pool.
+ Baby Traps place a multitude of the Conductor's grandkids into Hat Kid's hands, causing her to lose her balance."""
+ display_name = "Baby Trap Weight"
+ range_start = 0
+ range_end = 100
+ default = 40
+
+
+class LaserTrapWeight(Range):
+ """The weight of Laser Traps in the trap pool.
+ Laser Traps will spawn multiple giant lasers (from Snatcher's boss fight) at Hat Kid's location."""
+ display_name = "Laser Trap Weight"
+ range_start = 0
+ range_end = 100
+ default = 40
+
+
+class ParadeTrapWeight(Range):
+ """The weight of Parade Traps in the trap pool.
+ Parade Traps will summon multiple Express Band owls with knives that chase Hat Kid by mimicking her movement."""
+ display_name = "Parade Trap Weight"
+ range_start = 0
+ range_end = 100
+ default = 20
+
+
+@dataclass
+class AHITOptions(PerGameCommonOptions):
+ EndGoal: EndGoal
+ ActRandomizer: ActRandomizer
+ ActPlando: ActPlando
+ ActBlacklist: ActBlacklist
+ ShuffleAlpineZiplines: ShuffleAlpineZiplines
+ FinaleShuffle: FinaleShuffle
+ LogicDifficulty: LogicDifficulty
+ YarnBalancePercent: YarnBalancePercent
+ TimePieceBalancePercent: TimePieceBalancePercent
+ RandomizeHatOrder: RandomizeHatOrder
+ UmbrellaLogic: UmbrellaLogic
+ StartWithCompassBadge: StartWithCompassBadge
+ CompassBadgeMode: CompassBadgeMode
+ ShuffleStorybookPages: ShuffleStorybookPages
+ ShuffleActContracts: ShuffleActContracts
+ ShuffleSubconPaintings: ShuffleSubconPaintings
+ NoPaintingSkips: NoPaintingSkips
+ StartingChapter: StartingChapter
+ CTRLogic: CTRLogic
+
+ EnableDLC1: EnableDLC1
+ Tasksanity: Tasksanity
+ TasksanityTaskStep: TasksanityTaskStep
+ TasksanityCheckCount: TasksanityCheckCount
+ ExcludeTour: ExcludeTour
+ ShipShapeCustomTaskGoal: ShipShapeCustomTaskGoal
+
+ EnableDeathWish: EnableDeathWish
+ DWShuffle: DWShuffle
+ DWShuffleCountMin: DWShuffleCountMin
+ DWShuffleCountMax: DWShuffleCountMax
+ DeathWishOnly: DeathWishOnly
+ DWEnableBonus: DWEnableBonus
+ DWAutoCompleteBonuses: DWAutoCompleteBonuses
+ DWExcludeAnnoyingContracts: DWExcludeAnnoyingContracts
+ DWExcludeAnnoyingBonuses: DWExcludeAnnoyingBonuses
+ DWExcludeCandles: DWExcludeCandles
+ DWTimePieceRequirement: DWTimePieceRequirement
+
+ EnableDLC2: EnableDLC2
+ BaseballBat: BaseballBat
+ MetroMinPonCost: MetroMinPonCost
+ MetroMaxPonCost: MetroMaxPonCost
+ NyakuzaThugMinShopItems: NyakuzaThugMinShopItems
+ NyakuzaThugMaxShopItems: NyakuzaThugMaxShopItems
+ NoTicketSkips: NoTicketSkips
+
+ LowestChapterCost: LowestChapterCost
+ HighestChapterCost: HighestChapterCost
+ ChapterCostIncrement: ChapterCostIncrement
+ ChapterCostMinDifference: ChapterCostMinDifference
+ MaxExtraTimePieces: MaxExtraTimePieces
+
+ FinalChapterMinCost: FinalChapterMinCost
+ FinalChapterMaxCost: FinalChapterMaxCost
+
+ YarnCostMin: YarnCostMin
+ YarnCostMax: YarnCostMax
+ YarnAvailable: YarnAvailable
+ MinExtraYarn: MinExtraYarn
+ HatItems: HatItems
+
+ MinPonCost: MinPonCost
+ MaxPonCost: MaxPonCost
+ BadgeSellerMinItems: BadgeSellerMinItems
+ BadgeSellerMaxItems: BadgeSellerMaxItems
+
+ TrapChance: TrapChance
+ BabyTrapWeight: BabyTrapWeight
+ LaserTrapWeight: LaserTrapWeight
+ ParadeTrapWeight: ParadeTrapWeight
+
+ death_link: DeathLink
+
+
+ahit_option_groups: Dict[str, List[Any]] = {
+ "General Options": [EndGoal, ShuffleStorybookPages, ShuffleAlpineZiplines, ShuffleSubconPaintings,
+ ShuffleActContracts, MinPonCost, MaxPonCost, BadgeSellerMinItems, BadgeSellerMaxItems,
+ LogicDifficulty, NoPaintingSkips, CTRLogic],
+
+ "Act Options": [ActRandomizer, StartingChapter, LowestChapterCost, HighestChapterCost,
+ ChapterCostIncrement, ChapterCostMinDifference, FinalChapterMinCost, FinalChapterMaxCost,
+ FinaleShuffle, ActPlando, ActBlacklist],
+
+ "Item Options": [StartWithCompassBadge, CompassBadgeMode, RandomizeHatOrder, YarnAvailable, YarnCostMin,
+ YarnCostMax, MinExtraYarn, HatItems, UmbrellaLogic, MaxExtraTimePieces, YarnBalancePercent,
+ TimePieceBalancePercent],
+
+ "Arctic Cruise Options": [EnableDLC1, Tasksanity, TasksanityTaskStep, TasksanityCheckCount,
+ ShipShapeCustomTaskGoal, ExcludeTour],
+
+ "Nyakuza Metro Options": [EnableDLC2, MetroMinPonCost, MetroMaxPonCost, NyakuzaThugMinShopItems,
+ NyakuzaThugMaxShopItems, BaseballBat, NoTicketSkips],
+
+ "Death Wish Options": [EnableDeathWish, DWTimePieceRequirement, DWShuffle, DWShuffleCountMin, DWShuffleCountMax,
+ DWEnableBonus, DWAutoCompleteBonuses, DWExcludeAnnoyingContracts, DWExcludeAnnoyingBonuses,
+ DWExcludeCandles, DeathWishOnly],
+
+ "Trap Options": [TrapChance, BabyTrapWeight, LaserTrapWeight, ParadeTrapWeight]
+}
+
+
+slot_data_options: List[str] = [
+ "EndGoal",
+ "ActRandomizer",
+ "ShuffleAlpineZiplines",
+ "LogicDifficulty",
+ "CTRLogic",
+ "RandomizeHatOrder",
+ "UmbrellaLogic",
+ "StartWithCompassBadge",
+ "CompassBadgeMode",
+ "ShuffleStorybookPages",
+ "ShuffleActContracts",
+ "ShuffleSubconPaintings",
+ "NoPaintingSkips",
+ "HatItems",
+
+ "EnableDLC1",
+ "Tasksanity",
+ "TasksanityTaskStep",
+ "TasksanityCheckCount",
+ "ShipShapeCustomTaskGoal",
+ "ExcludeTour",
+
+ "EnableDeathWish",
+ "DWShuffle",
+ "DeathWishOnly",
+ "DWEnableBonus",
+ "DWAutoCompleteBonuses",
+ "DWTimePieceRequirement",
+
+ "EnableDLC2",
+ "MetroMinPonCost",
+ "MetroMaxPonCost",
+ "BaseballBat",
+ "NoTicketSkips",
+
+ "MinPonCost",
+ "MaxPonCost",
+
+ "death_link",
+]
diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py
new file mode 100644
index 000000000000..8cb3782bdec6
--- /dev/null
+++ b/worlds/ahit/Regions.py
@@ -0,0 +1,1036 @@
+from BaseClasses import Region, Entrance, ItemClassification, Location, LocationProgressType
+from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem
+from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \
+ shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard
+from typing import TYPE_CHECKING, List, Dict, Optional
+from .Rules import set_rift_rules, get_difficulty
+from .Options import ActRandomizer, EndGoal
+
+if TYPE_CHECKING:
+ from . import HatInTimeWorld
+
+
+MIN_FIRST_SPHERE_LOCATIONS = 30
+
+
+# ChapterIndex: region
+chapter_regions = {
+ ChapterIndex.SPACESHIP: "Spaceship",
+ ChapterIndex.MAFIA: "Mafia Town",
+ ChapterIndex.BIRDS: "Battle of the Birds",
+ ChapterIndex.SUBCON: "Subcon Forest",
+ ChapterIndex.ALPINE: "Alpine Skyline",
+ ChapterIndex.FINALE: "Time's End",
+ ChapterIndex.CRUISE: "The Arctic Cruise",
+ ChapterIndex.METRO: "Nyakuza Metro",
+}
+
+# entrance: region
+act_entrances = {
+ "Welcome to Mafia Town": "Mafia Town - Act 1",
+ "Barrel Battle": "Mafia Town - Act 2",
+ "She Came from Outer Space": "Mafia Town - Act 3",
+ "Down with the Mafia!": "Mafia Town - Act 4",
+ "Cheating the Race": "Mafia Town - Act 5",
+ "Heating Up Mafia Town": "Mafia Town - Act 6",
+ "The Golden Vault": "Mafia Town - Act 7",
+
+ "Dead Bird Studio": "Battle of the Birds - Act 1",
+ "Murder on the Owl Express": "Battle of the Birds - Act 2",
+ "Picture Perfect": "Battle of the Birds - Act 3",
+ "Train Rush": "Battle of the Birds - Act 4",
+ "The Big Parade": "Battle of the Birds - Act 5",
+ "Award Ceremony": "Battle of the Birds - Finale A",
+ "Dead Bird Studio Basement": "Battle of the Birds - Finale B",
+
+ "Contractual Obligations": "Subcon Forest - Act 1",
+ "The Subcon Well": "Subcon Forest - Act 2",
+ "Toilet of Doom": "Subcon Forest - Act 3",
+ "Queen Vanessa's Manor": "Subcon Forest - Act 4",
+ "Mail Delivery Service": "Subcon Forest - Act 5",
+ "Your Contract has Expired": "Subcon Forest - Finale",
+
+ "Alpine Free Roam": "Alpine Skyline - Free Roam",
+ "The Illness has Spread": "Alpine Skyline - Finale",
+
+ "The Finale": "Time's End - Act 1",
+
+ "Bon Voyage!": "The Arctic Cruise - Act 1",
+ "Ship Shape": "The Arctic Cruise - Act 2",
+ "Rock the Boat": "The Arctic Cruise - Finale",
+
+ "Nyakuza Free Roam": "Nyakuza Metro - Free Roam",
+ "Rush Hour": "Nyakuza Metro - Finale",
+}
+
+act_chapters = {
+ "Time Rift - Gallery": "Spaceship",
+ "Time Rift - The Lab": "Spaceship",
+
+ "Welcome to Mafia Town": "Mafia Town",
+ "Barrel Battle": "Mafia Town",
+ "She Came from Outer Space": "Mafia Town",
+ "Down with the Mafia!": "Mafia Town",
+ "Cheating the Race": "Mafia Town",
+ "Heating Up Mafia Town": "Mafia Town",
+ "The Golden Vault": "Mafia Town",
+ "Time Rift - Mafia of Cooks": "Mafia Town",
+ "Time Rift - Sewers": "Mafia Town",
+ "Time Rift - Bazaar": "Mafia Town",
+
+ "Dead Bird Studio": "Battle of the Birds",
+ "Murder on the Owl Express": "Battle of the Birds",
+ "Picture Perfect": "Battle of the Birds",
+ "Train Rush": "Battle of the Birds",
+ "The Big Parade": "Battle of the Birds",
+ "Award Ceremony": "Battle of the Birds",
+ "Dead Bird Studio Basement": "Battle of the Birds",
+ "Time Rift - Dead Bird Studio": "Battle of the Birds",
+ "Time Rift - The Owl Express": "Battle of the Birds",
+ "Time Rift - The Moon": "Battle of the Birds",
+
+ "Contractual Obligations": "Subcon Forest",
+ "The Subcon Well": "Subcon Forest",
+ "Toilet of Doom": "Subcon Forest",
+ "Queen Vanessa's Manor": "Subcon Forest",
+ "Mail Delivery Service": "Subcon Forest",
+ "Your Contract has Expired": "Subcon Forest",
+ "Time Rift - Sleepy Subcon": "Subcon Forest",
+ "Time Rift - Pipe": "Subcon Forest",
+ "Time Rift - Village": "Subcon Forest",
+
+ "Alpine Free Roam": "Alpine Skyline",
+ "The Illness has Spread": "Alpine Skyline",
+ "Time Rift - Alpine Skyline": "Alpine Skyline",
+ "Time Rift - The Twilight Bell": "Alpine Skyline",
+ "Time Rift - Curly Tail Trail": "Alpine Skyline",
+
+ "The Finale": "Time's End",
+ "Time Rift - Tour": "Time's End",
+
+ "Bon Voyage!": "The Arctic Cruise",
+ "Ship Shape": "The Arctic Cruise",
+ "Rock the Boat": "The Arctic Cruise",
+ "Time Rift - Balcony": "The Arctic Cruise",
+ "Time Rift - Deep Sea": "The Arctic Cruise",
+
+ "Nyakuza Free Roam": "Nyakuza Metro",
+ "Rush Hour": "Nyakuza Metro",
+ "Time Rift - Rumbi Factory": "Nyakuza Metro",
+}
+
+# region: list[Region]
+rift_access_regions = {
+ "Time Rift - Gallery": ["Spaceship"],
+ "Time Rift - The Lab": ["Spaceship"],
+
+ "Time Rift - Sewers": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space",
+ "Down with the Mafia!", "Cheating the Race", "Heating Up Mafia Town",
+ "The Golden Vault"],
+
+ "Time Rift - Bazaar": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space",
+ "Down with the Mafia!", "Cheating the Race", "Heating Up Mafia Town",
+ "The Golden Vault"],
+
+ "Time Rift - Mafia of Cooks": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space",
+ "Down with the Mafia!", "Cheating the Race", "The Golden Vault"],
+
+ "Time Rift - The Owl Express": ["Murder on the Owl Express"],
+ "Time Rift - The Moon": ["Picture Perfect", "The Big Parade"],
+ "Time Rift - Dead Bird Studio": ["Dead Bird Studio", "Dead Bird Studio Basement"],
+
+ "Time Rift - Pipe": ["Contractual Obligations", "The Subcon Well",
+ "Toilet of Doom", "Queen Vanessa's Manor",
+ "Mail Delivery Service"],
+
+ "Time Rift - Village": ["Contractual Obligations", "The Subcon Well",
+ "Toilet of Doom", "Queen Vanessa's Manor",
+ "Mail Delivery Service"],
+
+ "Time Rift - Sleepy Subcon": ["Contractual Obligations", "The Subcon Well",
+ "Toilet of Doom", "Queen Vanessa's Manor",
+ "Mail Delivery Service"],
+
+ "Time Rift - The Twilight Bell": ["Alpine Free Roam"],
+ "Time Rift - Curly Tail Trail": ["Alpine Free Roam"],
+ "Time Rift - Alpine Skyline": ["Alpine Free Roam", "The Illness has Spread"],
+
+ "Time Rift - Tour": ["Time's End"],
+
+ "Time Rift - Balcony": ["Cruise Ship"],
+ "Time Rift - Deep Sea": ["Bon Voyage!"],
+
+ "Time Rift - Rumbi Factory": ["Nyakuza Free Roam"],
+}
+
+# Time piece identifiers to be used in act shuffle
+chapter_act_info = {
+ "Time Rift - Gallery": "Spaceship_WaterRift_Gallery",
+ "Time Rift - The Lab": "Spaceship_WaterRift_MailRoom",
+
+ "Welcome to Mafia Town": "chapter1_tutorial",
+ "Barrel Battle": "chapter1_barrelboss",
+ "She Came from Outer Space": "chapter1_cannon_repair",
+ "Down with the Mafia!": "chapter1_boss",
+ "Cheating the Race": "harbor_impossible_race",
+ "Heating Up Mafia Town": "mafiatown_lava",
+ "The Golden Vault": "mafiatown_goldenvault",
+ "Time Rift - Mafia of Cooks": "TimeRift_Cave_Mafia",
+ "Time Rift - Sewers": "TimeRift_Water_Mafia_Easy",
+ "Time Rift - Bazaar": "TimeRift_Water_Mafia_Hard",
+
+ "Dead Bird Studio": "DeadBirdStudio",
+ "Murder on the Owl Express": "chapter3_murder",
+ "Picture Perfect": "moon_camerasnap",
+ "Train Rush": "trainwreck_selfdestruct",
+ "The Big Parade": "moon_parade",
+ "Award Ceremony": "award_ceremony",
+ "Dead Bird Studio Basement": "chapter3_secret_finale",
+ "Time Rift - Dead Bird Studio": "TimeRift_Cave_BirdBasement",
+ "Time Rift - The Owl Express": "TimeRift_Water_TWreck_Panels",
+ "Time Rift - The Moon": "TimeRift_Water_TWreck_Parade",
+
+ "Contractual Obligations": "subcon_village_icewall",
+ "The Subcon Well": "subcon_cave",
+ "Toilet of Doom": "chapter2_toiletboss",
+ "Queen Vanessa's Manor": "vanessa_manor_attic",
+ "Mail Delivery Service": "subcon_maildelivery",
+ "Your Contract has Expired": "snatcher_boss",
+ "Time Rift - Sleepy Subcon": "TimeRift_Cave_Raccoon",
+ "Time Rift - Pipe": "TimeRift_Water_Subcon_Hookshot",
+ "Time Rift - Village": "TimeRift_Water_Subcon_Dwellers",
+
+ "Alpine Free Roam": "AlpineFreeRoam", # not an actual Time Piece
+ "The Illness has Spread": "AlpineSkyline_Finale",
+ "Time Rift - Alpine Skyline": "TimeRift_Cave_Alps",
+ "Time Rift - The Twilight Bell": "TimeRift_Water_Alp_Goats",
+ "Time Rift - Curly Tail Trail": "TimeRift_Water_AlpineSkyline_Cats",
+
+ "The Finale": "TheFinale_FinalBoss",
+ "Time Rift - Tour": "TimeRift_Cave_Tour",
+
+ "Bon Voyage!": "Cruise_Boarding",
+ "Ship Shape": "Cruise_Working",
+ "Rock the Boat": "Cruise_Sinking",
+ "Time Rift - Balcony": "Cruise_WaterRift_Slide",
+ "Time Rift - Deep Sea": "Cruise_CaveRift_Aquarium",
+
+ "Nyakuza Free Roam": "MetroFreeRoam", # not an actual Time Piece
+ "Rush Hour": "Metro_Escape",
+ "Time Rift - Rumbi Factory": "Metro_CaveRift_RumbiFactory"
+}
+
+# Some of these may vary depending on options. See is_valid_first_act()
+guaranteed_first_acts = [
+ "Welcome to Mafia Town",
+ "Barrel Battle",
+ "She Came from Outer Space",
+ "Down with the Mafia!",
+ "Heating Up Mafia Town",
+ "The Golden Vault",
+
+ "Dead Bird Studio",
+ "Murder on the Owl Express",
+ "Dead Bird Studio Basement",
+
+ "Contractual Obligations",
+ "The Subcon Well",
+ "Queen Vanessa's Manor",
+ "Your Contract has Expired",
+
+ "Rock the Boat",
+
+ "Time Rift - Mafia of Cooks",
+ "Time Rift - Dead Bird Studio",
+ "Time Rift - Sleepy Subcon",
+ "Time Rift - Alpine Skyline"
+ "Time Rift - Tour",
+ "Time Rift - Rumbi Factory",
+]
+
+purple_time_rifts = [
+ "Time Rift - Mafia of Cooks",
+ "Time Rift - Dead Bird Studio",
+ "Time Rift - Sleepy Subcon",
+ "Time Rift - Alpine Skyline",
+ "Time Rift - Deep Sea",
+ "Time Rift - Tour",
+ "Time Rift - Rumbi Factory",
+]
+
+chapter_finales = [
+ "Dead Bird Studio Basement",
+ "Your Contract has Expired",
+ "The Illness has Spread",
+ "Rock the Boat",
+ "Rush Hour",
+]
+
+# Acts blacklisted in act shuffle
+# entrance: region
+blacklisted_acts = {
+ "Battle of the Birds - Finale A": "Award Ceremony",
+}
+
+# Blacklisted act shuffle combinations to help prevent impossible layouts. Mostly for free roam acts.
+blacklisted_combos = {
+ "The Illness has Spread": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"],
+ "Rush Hour": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"],
+
+ # Bon Voyage is here to prevent the cycle: Owl Express -> Bon Voyage -> Deep Sea -> MOTOE -> Owl Express
+ # which would make them all inaccessible since those rifts have no other entrances
+ "Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam", "Bon Voyage!",
+ "Contractual Obligations"],
+
+ "Time Rift - The Moon": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"],
+ "Time Rift - Dead Bird Studio": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"],
+ "Time Rift - Curly Tail Trail": ["Nyakuza Free Roam", "Contractual Obligations"],
+ "Time Rift - The Twilight Bell": ["Nyakuza Free Roam", "Contractual Obligations"],
+ "Time Rift - Alpine Skyline": ["Nyakuza Free Roam", "Contractual Obligations"],
+ "Time Rift - Rumbi Factory": ["Alpine Free Roam", "Contractual Obligations"],
+
+ # See above comment
+ "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations",
+ "Murder on the Owl Express"],
+
+ # was causing test failures
+ "Time Rift - Balcony": ["Alpine Free Roam"],
+}
+
+
+def create_regions(world: "HatInTimeWorld"):
+ # ------------------------------------------- HUB -------------------------------------------------- #
+ menu = create_region(world, "Menu")
+ spaceship = create_region_and_connect(world, "Spaceship", "Save File -> Spaceship", menu)
+
+ # we only need the menu and the spaceship regions
+ if world.is_dw_only():
+ return
+
+ create_rift_connections(world, create_region(world, "Time Rift - Gallery"))
+ create_rift_connections(world, create_region(world, "Time Rift - The Lab"))
+
+ # ------------------------------------------- MAFIA TOWN ------------------------------------------- #
+ mafia_town = create_region_and_connect(world, "Mafia Town", "Telescope -> Mafia Town", spaceship)
+ mt_act1 = create_region_and_connect(world, "Welcome to Mafia Town", "Mafia Town - Act 1", mafia_town)
+ mt_act2 = create_region_and_connect(world, "Barrel Battle", "Mafia Town - Act 2", mafia_town)
+ mt_act3 = create_region_and_connect(world, "She Came from Outer Space", "Mafia Town - Act 3", mafia_town)
+ mt_act4 = create_region_and_connect(world, "Down with the Mafia!", "Mafia Town - Act 4", mafia_town)
+ mt_act6 = create_region_and_connect(world, "Heating Up Mafia Town", "Mafia Town - Act 6", mafia_town)
+ mt_act5 = create_region_and_connect(world, "Cheating the Race", "Mafia Town - Act 5", mafia_town)
+ mt_act7 = create_region_and_connect(world, "The Golden Vault", "Mafia Town - Act 7", mafia_town)
+
+ # ------------------------------------------- BOTB ------------------------------------------------- #
+ botb = create_region_and_connect(world, "Battle of the Birds", "Telescope -> Battle of the Birds", spaceship)
+ dbs = create_region_and_connect(world, "Dead Bird Studio", "Battle of the Birds - Act 1", botb)
+ create_region_and_connect(world, "Murder on the Owl Express", "Battle of the Birds - Act 2", botb)
+ pp = create_region_and_connect(world, "Picture Perfect", "Battle of the Birds - Act 3", botb)
+ tr = create_region_and_connect(world, "Train Rush", "Battle of the Birds - Act 4", botb)
+ create_region_and_connect(world, "The Big Parade", "Battle of the Birds - Act 5", botb)
+ create_region_and_connect(world, "Award Ceremony", "Battle of the Birds - Finale A", botb)
+ basement = create_region_and_connect(world, "Dead Bird Studio Basement", "Battle of the Birds - Finale B", botb)
+ create_rift_connections(world, create_region(world, "Time Rift - Dead Bird Studio"))
+ create_rift_connections(world, create_region(world, "Time Rift - The Owl Express"))
+ create_rift_connections(world, create_region(world, "Time Rift - The Moon"))
+
+ # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert
+ ev_area = create_region_and_connect(world, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs)
+ post_ev = create_region_and_connect(world, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs)
+ basement.connect(ev_area, "DBS Basement -> Elevator Area")
+ if world.options.LogicDifficulty >= int(Difficulty.EXPERT):
+ basement.connect(post_ev, "DBS Basement -> Post Elevator Area")
+
+ # ------------------------------------------- SUBCON FOREST --------------------------------------- #
+ subcon_forest = create_region_and_connect(world, "Subcon Forest", "Telescope -> Subcon Forest", spaceship)
+ sf_act1 = create_region_and_connect(world, "Contractual Obligations", "Subcon Forest - Act 1", subcon_forest)
+ sf_act2 = create_region_and_connect(world, "The Subcon Well", "Subcon Forest - Act 2", subcon_forest)
+ sf_act3 = create_region_and_connect(world, "Toilet of Doom", "Subcon Forest - Act 3", subcon_forest)
+ sf_act4 = create_region_and_connect(world, "Queen Vanessa's Manor", "Subcon Forest - Act 4", subcon_forest)
+ sf_act5 = create_region_and_connect(world, "Mail Delivery Service", "Subcon Forest - Act 5", subcon_forest)
+ create_region_and_connect(world, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest)
+
+ # ------------------------------------------- ALPINE SKYLINE ------------------------------------------ #
+ alpine_skyline = create_region_and_connect(world, "Alpine Skyline", "Telescope -> Alpine Skyline", spaceship)
+ alpine_freeroam = create_region_and_connect(world, "Alpine Free Roam", "Alpine Skyline - Free Roam", alpine_skyline)
+ alpine_area = create_region_and_connect(world, "Alpine Skyline Area", "AFR -> Alpine Skyline Area", alpine_freeroam)
+
+ # Needs to be separate because there are a lot of locations in Alpine that can't be accessed from Illness
+ alpine_area_tihs = create_region_and_connect(world, "Alpine Skyline Area (TIHS)", "-> Alpine Skyline Area (TIHS)",
+ alpine_area)
+
+ create_region_and_connect(world, "The Birdhouse", "-> The Birdhouse", alpine_area)
+ create_region_and_connect(world, "The Lava Cake", "-> The Lava Cake", alpine_area)
+ create_region_and_connect(world, "The Windmill", "-> The Windmill", alpine_area)
+ create_region_and_connect(world, "The Twilight Bell", "-> The Twilight Bell", alpine_area)
+
+ illness = create_region_and_connect(world, "The Illness has Spread", "Alpine Skyline - Finale", alpine_skyline)
+ illness.connect(alpine_area_tihs, "TIHS -> Alpine Skyline Area (TIHS)")
+ create_rift_connections(world, create_region(world, "Time Rift - Alpine Skyline"))
+ create_rift_connections(world, create_region(world, "Time Rift - The Twilight Bell"))
+ create_rift_connections(world, create_region(world, "Time Rift - Curly Tail Trail"))
+
+ # ------------------------------------------- OTHER -------------------------------------------------- #
+ mt_area: Region = create_region(world, "Mafia Town Area")
+ mt_area_humt: Region = create_region(world, "Mafia Town Area (HUMT)")
+ mt_area.connect(mt_area_humt, "MT Area -> MT Area (HUMT)")
+ mt_act1.connect(mt_area, "Mafia Town Entrance WTMT")
+ mt_act2.connect(mt_area, "Mafia Town Entrance BB")
+ mt_act3.connect(mt_area, "Mafia Town Entrance SCFOS")
+ mt_act4.connect(mt_area, "Mafia Town Entrance DWTM")
+ mt_act5.connect(mt_area, "Mafia Town Entrance CTR")
+ mt_act6.connect(mt_area_humt, "Mafia Town Entrance HUMT")
+ mt_act7.connect(mt_area, "Mafia Town Entrance TGV")
+
+ create_rift_connections(world, create_region(world, "Time Rift - Mafia of Cooks"))
+ create_rift_connections(world, create_region(world, "Time Rift - Sewers"))
+ create_rift_connections(world, create_region(world, "Time Rift - Bazaar"))
+
+ sf_area: Region = create_region(world, "Subcon Forest Area")
+ sf_act1.connect(sf_area, "Subcon Forest Entrance CO")
+ sf_act2.connect(sf_area, "Subcon Forest Entrance SW")
+ sf_act3.connect(sf_area, "Subcon Forest Entrance TOD")
+ sf_act4.connect(sf_area, "Subcon Forest Entrance QVM")
+ sf_act5.connect(sf_area, "Subcon Forest Entrance MDS")
+
+ create_rift_connections(world, create_region(world, "Time Rift - Sleepy Subcon"))
+ create_rift_connections(world, create_region(world, "Time Rift - Pipe"))
+ create_rift_connections(world, create_region(world, "Time Rift - Village"))
+
+ badge_seller = create_badge_seller(world)
+ mt_area.connect(badge_seller, "MT Area -> Badge Seller")
+ mt_area_humt.connect(badge_seller, "MT Area (HUMT) -> Badge Seller")
+ sf_area.connect(badge_seller, "SF Area -> Badge Seller")
+ dbs.connect(badge_seller, "DBS -> Badge Seller")
+ pp.connect(badge_seller, "PP -> Badge Seller")
+ tr.connect(badge_seller, "TR -> Badge Seller")
+ alpine_area_tihs.connect(badge_seller, "ASA -> Badge Seller")
+
+ times_end = create_region_and_connect(world, "Time's End", "Telescope -> Time's End", spaceship)
+ create_region_and_connect(world, "The Finale", "Time's End - Act 1", times_end)
+
+ # ------------------------------------------- DLC1 ------------------------------------------------- #
+ if world.is_dlc1():
+ arctic_cruise = create_region_and_connect(world, "The Arctic Cruise", "Telescope -> Arctic Cruise", spaceship)
+ cruise_ship = create_region(world, "Cruise Ship")
+
+ ac_act1 = create_region_and_connect(world, "Bon Voyage!", "The Arctic Cruise - Act 1", arctic_cruise)
+ ac_act2 = create_region_and_connect(world, "Ship Shape", "The Arctic Cruise - Act 2", arctic_cruise)
+ ac_act3 = create_region_and_connect(world, "Rock the Boat", "The Arctic Cruise - Finale", arctic_cruise)
+
+ ac_act1.connect(cruise_ship, "Cruise Ship Entrance BV")
+ ac_act2.connect(cruise_ship, "Cruise Ship Entrance SS")
+ ac_act3.connect(cruise_ship, "Cruise Ship Entrance RTB")
+ create_rift_connections(world, create_region(world, "Time Rift - Balcony"))
+ create_rift_connections(world, create_region(world, "Time Rift - Deep Sea"))
+
+ if not world.options.ExcludeTour:
+ create_rift_connections(world, create_region(world, "Time Rift - Tour"))
+
+ if world.options.Tasksanity:
+ create_tasksanity_locations(world)
+
+ cruise_ship.connect(badge_seller, "CS -> Badge Seller")
+
+ if world.is_dlc2():
+ nyakuza = create_region_and_connect(world, "Nyakuza Metro", "Telescope -> Nyakuza Metro", spaceship)
+ metro_freeroam = create_region_and_connect(world, "Nyakuza Free Roam", "Nyakuza Metro - Free Roam", nyakuza)
+ create_region_and_connect(world, "Rush Hour", "Nyakuza Metro - Finale", nyakuza)
+
+ yellow = create_region_and_connect(world, "Yellow Overpass Station", "-> Yellow Overpass Station", metro_freeroam)
+ green = create_region_and_connect(world, "Green Clean Station", "-> Green Clean Station", metro_freeroam)
+ pink = create_region_and_connect(world, "Pink Paw Station", "-> Pink Paw Station", metro_freeroam)
+ create_region_and_connect(world, "Bluefin Tunnel", "-> Bluefin Tunnel", metro_freeroam) # No manhole
+
+ create_region_and_connect(world, "Yellow Overpass Manhole", "-> Yellow Overpass Manhole", yellow)
+ create_region_and_connect(world, "Green Clean Manhole", "-> Green Clean Manhole", green)
+ create_region_and_connect(world, "Pink Paw Manhole", "-> Pink Paw Manhole", pink)
+
+ create_rift_connections(world, create_region(world, "Time Rift - Rumbi Factory"))
+ create_thug_shops(world)
+
+
+def create_rift_connections(world: "HatInTimeWorld", region: Region):
+ for i, name in enumerate(rift_access_regions[region.name]):
+ act_region = world.multiworld.get_region(name, world.player)
+ entrance_name = f"{region.name} Portal - Entrance {i+1}"
+ act_region.connect(region, entrance_name)
+
+
+def create_tasksanity_locations(world: "HatInTimeWorld"):
+ ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player)
+ id_start: int = TASKSANITY_START_ID
+ for i in range(world.options.TasksanityCheckCount):
+ location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape)
+ ship_shape.locations.append(location)
+
+
+def randomize_act_entrances(world: "HatInTimeWorld"):
+ region_list: List[Region] = get_shuffleable_act_regions(world)
+ world.random.shuffle(region_list)
+ region_list.sort(key=sort_acts)
+ candidate_list: List[Region] = region_list.copy()
+ rift_dict: Dict[str, Region] = {}
+
+ # Check if Plando's are valid, if so, map them
+ if world.options.ActPlando:
+ player_name = world.multiworld.get_player_name(world.player)
+ for (name1, name2) in world.options.ActPlando.items():
+ region: Region
+ act: Region
+ try:
+ region = world.multiworld.get_region(name1, world.player)
+ except KeyError:
+ print(f"ActPlando ({player_name}) - "
+ f"Act \"{name1}\" does not exist in the multiworld. "
+ f"Possible reasons are typos, case-sensitivity, or DLC options.")
+ continue
+
+ try:
+ act = world.multiworld.get_region(name2, world.player)
+ except KeyError:
+ print(f"ActPlando ({player_name}) - "
+ f"Act \"{name2}\" does not exist in the multiworld. "
+ f"Possible reasons are typos, case-sensitivity, or DLC options.")
+ continue
+
+ if is_valid_plando(world, region.name, act.name):
+ region_list.remove(region)
+ candidate_list.remove(act)
+ connect_acts(world, region, act, rift_dict)
+ else:
+ print(f"ActPlando "
+ f"({player_name}) - "
+ f"\"{name1}: {name2}\" "
+ f"is an invalid or disallowed act plando combination!")
+
+ # Decide what should be on the first few levels before randomizing the rest
+ first_acts: List[Region] = []
+ first_chapter_name = chapter_regions[ChapterIndex(world.options.StartingChapter)]
+ first_acts.append(get_act_by_number(world, first_chapter_name, 1))
+ # Chapter 3 and 4 only have one level accessible at the start
+ if first_chapter_name == "Mafia Town" or first_chapter_name == "Battle of the Birds":
+ first_acts.append(get_act_by_number(world, first_chapter_name, 2))
+ first_acts.append(get_act_by_number(world, first_chapter_name, 3))
+
+ valid_first_acts: List[Region] = []
+ for candidate in candidate_list:
+ if is_valid_first_act(world, candidate):
+ valid_first_acts.append(candidate)
+
+ total_locations = 0
+ for level in first_acts:
+ if level not in region_list: # make sure it hasn't been plando'd
+ continue
+
+ candidate = valid_first_acts[world.random.randint(0, len(valid_first_acts)-1)]
+ region_list.remove(level)
+ candidate_list.remove(candidate)
+ valid_first_acts.remove(candidate)
+ connect_acts(world, level, candidate, rift_dict)
+
+ # Only allow one purple rift
+ if candidate.name in purple_time_rifts:
+ for act in reversed(valid_first_acts):
+ if act.name in purple_time_rifts:
+ valid_first_acts.remove(act)
+
+ total_locations += get_region_location_count(world, candidate.name)
+ if "Time Rift" not in candidate.name:
+ chapter = act_chapters.get(candidate.name)
+ if chapter == "Mafia Town":
+ total_locations += get_region_location_count(world, "Mafia Town Area (HUMT)")
+ if candidate.name != "Heating Up Mafia Town":
+ total_locations += get_region_location_count(world, "Mafia Town Area")
+ elif chapter == "Subcon Forest":
+ total_locations += get_region_location_count(world, "Subcon Forest Area")
+ elif chapter == "The Arctic Cruise":
+ total_locations += get_region_location_count(world, "Cruise Ship")
+
+ # If we have enough Sphere 1 locations, we can allow the rest to be randomized
+ if total_locations >= MIN_FIRST_SPHERE_LOCATIONS:
+ break
+
+ ignore_certain_rules: bool = False
+ while len(region_list) > 0:
+ region = region_list[0]
+ candidate: Region
+ valid_candidates: List[Region] = []
+
+ # Look for candidates to map this act to
+ for c in candidate_list:
+ if is_valid_act_combo(world, region, c, ignore_certain_rules):
+ valid_candidates.append(c)
+
+ if len(valid_candidates) > 0:
+ candidate = valid_candidates[world.random.randint(0, len(valid_candidates)-1)]
+ else:
+ # If we fail here, try again with less shuffle rules. If we still somehow fail, there's an issue for sure
+ if ignore_certain_rules:
+ raise Exception(f"Failed to find act shuffle candidate for {region}"
+ f"\nRemaining acts to map to: {region_list}"
+ f"\nRemaining candidates: {candidate_list}")
+
+ ignore_certain_rules = True
+ continue
+
+ ignore_certain_rules = False
+ region_list.remove(region)
+ candidate_list.remove(candidate)
+ connect_acts(world, region, candidate, rift_dict)
+
+ for name in blacklisted_acts.values():
+ region: Region = world.multiworld.get_region(name, world.player)
+ update_chapter_act_info(world, region, region)
+
+ set_rift_rules(world, rift_dict)
+
+
+# Try to do levels that may have specific mapping rules first
+def sort_acts(act: Region) -> int:
+ if "Time Rift" in act.name:
+ return -5
+
+ if act.name in chapter_finales:
+ return -4
+
+ # Free Roam
+ if (act_chapters[act.name] == "Alpine Skyline" or act_chapters[act.name] == "Nyakuza Metro") \
+ and "Time Rift" not in act.name:
+ return -3
+
+ if act.name == "Contractual Obligations" or act.name == "The Subcon Well":
+ return -2
+
+ world = act.multiworld.worlds[act.player]
+ blacklist = world.options.ActBlacklist
+ if len(blacklist) > 0:
+ for name, act_list in blacklist.items():
+ if act.name == name or act.name in act_list:
+ return -1
+
+ return 0
+
+
+def connect_acts(world: "HatInTimeWorld", entrance_act: Region, exit_act: Region, rift_dict: Dict[str, Region]):
+ # Vanilla
+ if exit_act.name == entrance_act.name:
+ if entrance_act.name in rift_access_regions.keys():
+ rift_dict.setdefault(entrance_act.name, exit_act)
+
+ update_chapter_act_info(world, entrance_act, exit_act)
+ return
+
+ if entrance_act.name in rift_access_regions.keys():
+ connect_time_rift(world, entrance_act, exit_act)
+ rift_dict.setdefault(entrance_act.name, exit_act)
+ else:
+ if exit_act.name in rift_access_regions.keys():
+ for e in exit_act.entrances.copy():
+ e.parent_region.exits.remove(e)
+ e.connected_region.entrances.remove(e)
+
+ entrance = world.multiworld.get_entrance(act_entrances[entrance_act.name], world.player)
+ chapter = world.multiworld.get_region(act_chapters[entrance_act.name], world.player)
+ reconnect_regions(entrance, chapter, exit_act)
+
+ update_chapter_act_info(world, entrance_act, exit_act)
+
+
+def is_valid_act_combo(world: "HatInTimeWorld", entrance_act: Region,
+ exit_act: Region, ignore_certain_rules: bool = False) -> bool:
+
+ # Ignore certain rules that aren't to prevent impossible combos. This is needed for ActPlando.
+ if not ignore_certain_rules:
+ if world.options.ActRandomizer == ActRandomizer.option_light and not ignore_certain_rules:
+ # Don't map Time Rifts to normal acts
+ if "Time Rift" in entrance_act.name and "Time Rift" not in exit_act.name:
+ return False
+
+ # Don't map normal acts to Time Rifts
+ if "Time Rift" not in entrance_act.name and "Time Rift" in exit_act.name:
+ return False
+
+ # Separate purple rifts
+ if entrance_act.name in purple_time_rifts and exit_act.name not in purple_time_rifts \
+ or entrance_act.name not in purple_time_rifts and exit_act.name in purple_time_rifts:
+ return False
+
+ if world.options.FinaleShuffle and entrance_act.name in chapter_finales:
+ if exit_act.name not in chapter_finales:
+ return False
+
+ exit_chapter: str = act_chapters.get(exit_act.name)
+ # make sure that certain time rift combinations never happen
+ always_block: bool = exit_chapter != "Mafia Town" and exit_chapter != "Subcon Forest"
+ if not ignore_certain_rules or always_block:
+ if entrance_act.name in rift_access_regions and exit_act.name in rift_access_regions[entrance_act.name]:
+ return False
+
+ # Blacklisted?
+ if entrance_act.name in blacklisted_combos.keys() and exit_act.name in blacklisted_combos[entrance_act.name]:
+ return False
+
+ if world.options.ActBlacklist:
+ act_blacklist = world.options.ActBlacklist.get(entrance_act.name)
+ if act_blacklist is not None and exit_act.name in act_blacklist:
+ return False
+
+ # Prevent Contractual Obligations from being inaccessible if contracts are not shuffled
+ if not world.options.ShuffleActContracts:
+ if (entrance_act.name == "Your Contract has Expired" or entrance_act.name == "The Subcon Well") \
+ and exit_act.name == "Contractual Obligations":
+ return False
+
+ return True
+
+
+def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool:
+ if act.name not in guaranteed_first_acts:
+ return False
+
+ if world.options.ActRandomizer == ActRandomizer.option_light and "Time Rift" in act.name:
+ return False
+
+ # If there's only a single level in the starting chapter, only allow Mafia Town or Subcon Forest levels
+ start_chapter = world.options.StartingChapter
+ if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON:
+ if "Time Rift" in act.name:
+ return False
+
+ if act_chapters[act.name] != "Mafia Town" and act_chapters[act.name] != "Subcon Forest":
+ return False
+
+ if act.name in purple_time_rifts and not world.options.ShuffleStorybookPages:
+ return False
+
+ diff = get_difficulty(world)
+ # Not completable without Umbrella?
+ if world.options.UmbrellaLogic:
+ # Needs to be at least moderate to cross the big dweller wall
+ if act.name == "Queen Vanessa's Manor" and diff < Difficulty.MODERATE:
+ return False
+ elif act.name == "Heating Up Mafia Town": # Straight up impossible
+ return False
+
+ # Need to be able to hover
+ if act.name == "Your Contract has Expired":
+ if diff < Difficulty.EXPERT or world.options.ShuffleSubconPaintings and world.options.NoPaintingSkips:
+ return False
+
+ if act.name == "Dead Bird Studio":
+ # No umbrella logic = moderate, umbrella logic = expert.
+ if diff < Difficulty.MODERATE or world.options.UmbrellaLogic and diff < Difficulty.EXPERT:
+ return False
+ elif act.name == "Dead Bird Studio Basement" and (diff < Difficulty.EXPERT or world.options.FinaleShuffle):
+ return False
+ elif act.name == "Rock the Boat" and (diff < Difficulty.MODERATE or world.options.FinaleShuffle):
+ return False
+ elif act.name == "The Subcon Well" and diff < Difficulty.MODERATE:
+ return False
+ elif act.name == "Contractual Obligations" and world.options.ShuffleSubconPaintings:
+ return False
+
+ if world.options.ShuffleSubconPaintings and "Time Rift" not in act.name \
+ and act_chapters.get(act.name, "") == "Subcon Forest":
+ # Only allow Subcon levels if painting skips are allowed
+ if diff < Difficulty.MODERATE or world.options.NoPaintingSkips:
+ return False
+
+ return True
+
+
+def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region):
+ i = 1
+ while i <= len(rift_access_regions[time_rift.name]):
+ name = f"{time_rift.name} Portal - Entrance {i}"
+ entrance: Entrance
+ try:
+ entrance = world.multiworld.get_entrance(name, world.player)
+ reconnect_regions(entrance, entrance.parent_region, exit_region)
+ except KeyError:
+ time_rift.connect(exit_region, name)
+
+ i += 1
+
+
+def get_shuffleable_act_regions(world: "HatInTimeWorld") -> List[Region]:
+ act_list: List[Region] = []
+ for region in world.multiworld.get_regions(world.player):
+ if region.name in chapter_act_info.keys():
+ if not is_act_blacklisted(world, region.name):
+ act_list.append(region)
+
+ return act_list
+
+
+def is_act_blacklisted(world: "HatInTimeWorld", name: str) -> bool:
+ act_plando = world.options.ActPlando
+ plando: bool = name in act_plando.keys() and is_valid_plando(world, name, act_plando[name])
+ if not plando and name in act_plando.values():
+ for key in act_plando.keys():
+ if act_plando[key] == name and is_valid_plando(world, key, name):
+ plando = True
+ break
+
+ if name == "The Finale":
+ return not plando and world.options.EndGoal == EndGoal.option_finale
+
+ if name == "Rush Hour":
+ return not plando and world.options.EndGoal == EndGoal.option_rush_hour
+
+ if name == "Time Rift - Tour":
+ return bool(world.options.ExcludeTour)
+
+ return name in blacklisted_acts.values()
+
+
+def is_valid_plando(world: "HatInTimeWorld", region: str, act: str) -> bool:
+ # Duplicated keys will throw an exception for us, but we still need to check for duplicated values
+ found_count = 0
+ for val in world.options.ActPlando.values():
+ if val == act:
+ found_count += 1
+
+ if found_count > 1:
+ raise Exception(f"ActPlando ({world.multiworld.get_player_name(world.player)}) - "
+ f"Duplicated act plando mapping found for act: \"{act}\"")
+
+ if region in blacklisted_acts.values() or (region not in act_entrances.keys() and "Time Rift" not in region):
+ return False
+
+ if act in blacklisted_acts.values() or (act not in act_entrances.keys() and "Time Rift" not in act):
+ return False
+
+ # Don't allow plando-ing things onto the first act that aren't permitted
+ entrance_name = act_entrances.get(region, "")
+ if entrance_name != "":
+ is_first_act: bool = act_chapters.get(region) == get_first_chapter_region(world).name \
+ and ("Act 1" in entrance_name or "Free Roam" in entrance_name)
+
+ if is_first_act and not is_valid_first_act(world, world.multiworld.get_region(act, world.player)):
+ return False
+
+ # Don't allow straight up impossible mappings
+ if (region == "Time Rift - Curly Tail Trail"
+ or region == "Time Rift - The Twilight Bell"
+ or region == "The Illness has Spread") \
+ and act == "Alpine Free Roam":
+ return False
+
+ if (region == "Rush Hour" or region == "Time Rift - Rumbi Factory") and act == "Nyakuza Free Roam":
+ return False
+
+ if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express":
+ return False
+
+ if region == "Time Rift - Deep Sea" and act == "Bon Voyage!":
+ return False
+
+ return any(a.name == world.options.ActPlando.get(region) for a in world.multiworld.get_regions(world.player))
+
+
+def create_region(world: "HatInTimeWorld", name: str) -> Region:
+ reg = Region(name, world.player, world.multiworld)
+
+ for (key, data) in location_table.items():
+ if world.is_dw_only():
+ break
+
+ if data.nyakuza_thug != "":
+ continue
+
+ if data.region == name:
+ if key in storybook_pages.keys() and not world.options.ShuffleStorybookPages:
+ continue
+
+ location = HatInTimeLocation(world.player, key, data.id, reg)
+ reg.locations.append(location)
+ if location.name in shop_locations:
+ world.shop_locs.append(location.name)
+
+ world.multiworld.regions.append(reg)
+ return reg
+
+
+def create_badge_seller(world: "HatInTimeWorld") -> Region:
+ badge_seller = Region("Badge Seller", world.player, world.multiworld)
+ world.multiworld.regions.append(badge_seller)
+ count = 0
+ max_items = 0
+
+ if world.options.BadgeSellerMaxItems > 0:
+ max_items = world.random.randint(world.options.BadgeSellerMinItems.value,
+ world.options.BadgeSellerMaxItems.value)
+
+ if max_items <= 0:
+ world.badge_seller_count = 0
+ return badge_seller
+
+ for (key, data) in shop_locations.items():
+ if "Badge Seller" not in key:
+ continue
+
+ location = HatInTimeLocation(world.player, key, data.id, badge_seller)
+ badge_seller.locations.append(location)
+ world.shop_locs.append(location.name)
+
+ count += 1
+ if count >= max_items:
+ break
+
+ world.badge_seller_count = max_items
+ return badge_seller
+
+
+# Takes an entrance, removes its old connections, and reconnects it between the two regions specified.
+def reconnect_regions(entrance: Entrance, start_region: Region, exit_region: Region):
+ if entrance in entrance.connected_region.entrances:
+ entrance.connected_region.entrances.remove(entrance)
+
+ if entrance in entrance.parent_region.exits:
+ entrance.parent_region.exits.remove(entrance)
+
+ if entrance in start_region.exits:
+ start_region.exits.remove(entrance)
+
+ if entrance in exit_region.entrances:
+ exit_region.entrances.remove(entrance)
+
+ entrance.parent_region = start_region
+ start_region.exits.append(entrance)
+ entrance.connect(exit_region)
+
+
+def create_region_and_connect(world: "HatInTimeWorld",
+ name: str, entrancename: str, connected_region: Region, is_exit: bool = True) -> Region:
+
+ reg: Region = create_region(world, name)
+ entrance_region: Region
+ exit_region: Region
+
+ if is_exit:
+ entrance_region = connected_region
+ exit_region = reg
+ else:
+ entrance_region = reg
+ exit_region = connected_region
+
+ entrance_region.connect(exit_region, entrancename)
+ return reg
+
+
+def get_first_chapter_region(world: "HatInTimeWorld") -> Region:
+ start_chapter: ChapterIndex = ChapterIndex(world.options.StartingChapter)
+ return world.multiworld.get_region(chapter_regions.get(start_chapter), world.player)
+
+
+def get_act_original_chapter(world: "HatInTimeWorld", act_name: str) -> Region:
+ return world.multiworld.get_region(act_chapters[act_name], world.player)
+
+
+# Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game
+def update_chapter_act_info(world: "HatInTimeWorld", original_region: Region, new_region: Region):
+ original_act_info = chapter_act_info[original_region.name]
+ new_act_info = chapter_act_info[new_region.name]
+ world.act_connections[original_act_info] = new_act_info
+
+
+def get_shuffled_region(world: "HatInTimeWorld", region: str) -> str:
+ ci: str = chapter_act_info[region]
+ for key, val in world.act_connections.items():
+ if val == ci:
+ for name in chapter_act_info.keys():
+ if chapter_act_info[name] == key:
+ return name
+
+
+def get_region_location_count(world: "HatInTimeWorld", region_name: str, included_only: bool = True) -> int:
+ count = 0
+ region = world.multiworld.get_region(region_name, world.player)
+ for loc in region.locations:
+ if loc.address is not None and (not included_only or loc.progress_type is not LocationProgressType.EXCLUDED):
+ count += 1
+
+ return count
+
+
+def get_act_by_number(world: "HatInTimeWorld", chapter_name: str, num: int) -> Region:
+ chapter = world.multiworld.get_region(chapter_name, world.player)
+ act: Optional[Region] = None
+ for e in chapter.exits:
+ if f"Act {num}" in e.name or num == 1 and "Free Roam" in e.name:
+ act = e.connected_region
+ break
+
+ return act
+
+
+def create_thug_shops(world: "HatInTimeWorld"):
+ min_items: int = world.options.NyakuzaThugMinShopItems.value
+ max_items: int = world.options.NyakuzaThugMaxShopItems.value
+ count = -1
+ step = 0
+ old_name = ""
+
+ for key, data in shop_locations.items():
+ if data.nyakuza_thug == "":
+ continue
+
+ if old_name != "" and old_name == data.nyakuza_thug:
+ continue
+
+ try:
+ if world.nyakuza_thug_items[data.nyakuza_thug] <= 0:
+ continue
+ except KeyError:
+ pass
+
+ if count == -1:
+ count = world.random.randint(min_items, max_items)
+ world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count)
+ if count <= 0:
+ continue
+
+ if count >= 1:
+ region = world.multiworld.get_region(data.region, world.player)
+ loc = HatInTimeLocation(world.player, key, data.id, region)
+ region.locations.append(loc)
+ world.shop_locs.append(loc.name)
+
+ step += 1
+ if step >= count:
+ old_name = data.nyakuza_thug
+ step = 0
+ count = -1
+
+
+def create_events(world: "HatInTimeWorld") -> int:
+ count = 0
+
+ for (name, data) in event_locs.items():
+ if not is_location_valid(world, name):
+ continue
+
+ item_name: str = name
+ if world.is_dw():
+ if name in snatcher_coins.keys():
+ item_name = data.snatcher_coin
+ elif name in zero_jumps:
+ if get_difficulty(world) < Difficulty.HARD and name in zero_jumps_hard:
+ continue
+
+ if get_difficulty(world) < Difficulty.EXPERT and name in zero_jumps_expert:
+ continue
+
+ event: Location = create_event(name, item_name, world.multiworld.get_region(data.region, world.player), world)
+ event.show_in_spoiler = False
+ count += 1
+
+ return count
+
+
+def create_event(name: str, item_name: str, region: Region, world: "HatInTimeWorld") -> Location:
+ event = HatInTimeLocation(world.player, name, None, region)
+ region.locations.append(event)
+ event.place_locked_item(HatInTimeItem(item_name, ItemClassification.progression, None, world.player))
+ return event
diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py
new file mode 100644
index 000000000000..b716b793a797
--- /dev/null
+++ b/worlds/ahit/Rules.py
@@ -0,0 +1,958 @@
+from worlds.AutoWorld import CollectionState
+from worlds.generic.Rules import add_rule, set_rule
+from .Locations import location_table, zipline_unlocks, is_location_valid, shop_locations, event_locs
+from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HitType
+from BaseClasses import Location, Entrance, Region
+from typing import TYPE_CHECKING, List, Callable, Union, Dict
+from .Options import EndGoal, CTRLogic, NoTicketSkips
+
+if TYPE_CHECKING:
+ from . import HatInTimeWorld
+
+
+act_connections = {
+ "Mafia Town - Act 2": ["Mafia Town - Act 1"],
+ "Mafia Town - Act 3": ["Mafia Town - Act 1"],
+ "Mafia Town - Act 4": ["Mafia Town - Act 2", "Mafia Town - Act 3"],
+ "Mafia Town - Act 6": ["Mafia Town - Act 4"],
+ "Mafia Town - Act 7": ["Mafia Town - Act 4"],
+ "Mafia Town - Act 5": ["Mafia Town - Act 6", "Mafia Town - Act 7"],
+
+ "Battle of the Birds - Act 2": ["Battle of the Birds - Act 1"],
+ "Battle of the Birds - Act 3": ["Battle of the Birds - Act 1"],
+ "Battle of the Birds - Act 4": ["Battle of the Birds - Act 2", "Battle of the Birds - Act 3"],
+ "Battle of the Birds - Act 5": ["Battle of the Birds - Act 2", "Battle of the Birds - Act 3"],
+ "Battle of the Birds - Finale A": ["Battle of the Birds - Act 4", "Battle of the Birds - Act 5"],
+ "Battle of the Birds - Finale B": ["Battle of the Birds - Finale A"],
+
+ "Subcon Forest - Finale": ["Subcon Forest - Act 1", "Subcon Forest - Act 2",
+ "Subcon Forest - Act 3", "Subcon Forest - Act 4",
+ "Subcon Forest - Act 5"],
+
+ "The Arctic Cruise - Act 2": ["The Arctic Cruise - Act 1"],
+ "The Arctic Cruise - Finale": ["The Arctic Cruise - Act 2"],
+}
+
+
+def can_use_hat(state: CollectionState, world: "HatInTimeWorld", hat: HatType) -> bool:
+ if world.options.HatItems:
+ return state.has(hat_type_to_item[hat], world.player)
+
+ if world.hat_yarn_costs[hat] <= 0: # this means the hat was put into starting inventory
+ return True
+
+ return state.has("Yarn", world.player, get_hat_cost(world, hat))
+
+
+def get_hat_cost(world: "HatInTimeWorld", hat: HatType) -> int:
+ cost = 0
+ for h in world.hat_craft_order:
+ cost += world.hat_yarn_costs[h]
+ if h == hat:
+ break
+
+ return cost
+
+
+def painting_logic(world: "HatInTimeWorld") -> bool:
+ return bool(world.options.ShuffleSubconPaintings)
+
+
+# -1 = Normal, 0 = Moderate, 1 = Hard, 2 = Expert
+def get_difficulty(world: "HatInTimeWorld") -> Difficulty:
+ return Difficulty(world.options.LogicDifficulty)
+
+
+def has_paintings(state: CollectionState, world: "HatInTimeWorld", count: int, allow_skip: bool = True) -> bool:
+ if not painting_logic(world):
+ return True
+
+ if not world.options.NoPaintingSkips and allow_skip:
+ # In Moderate there is a very easy trick to skip all the walls, except for the one guarding the boss arena
+ if get_difficulty(world) >= Difficulty.MODERATE:
+ return True
+
+ return state.has("Progressive Painting Unlock", world.player, count)
+
+
+def zipline_logic(world: "HatInTimeWorld") -> bool:
+ return bool(world.options.ShuffleAlpineZiplines)
+
+
+def can_use_hookshot(state: CollectionState, world: "HatInTimeWorld"):
+ return state.has("Hookshot Badge", world.player)
+
+
+def can_hit(state: CollectionState, world: "HatInTimeWorld", umbrella_only: bool = False):
+ if not world.options.UmbrellaLogic:
+ return True
+
+ return state.has("Umbrella", world.player) or not umbrella_only and can_use_hat(state, world, HatType.BREWING)
+
+
+def has_relic_combo(state: CollectionState, world: "HatInTimeWorld", relic: str) -> bool:
+ return state.has_group(relic, world.player, len(world.item_name_groups[relic]))
+
+
+def get_relic_count(state: CollectionState, world: "HatInTimeWorld", relic: str) -> int:
+ return state.count_group(relic, world.player)
+
+
+# This is used to determine if the player can clear an act that's required to unlock a Time Rift
+def can_clear_required_act(state: CollectionState, world: "HatInTimeWorld", act_entrance: str) -> bool:
+ entrance: Entrance = world.multiworld.get_entrance(act_entrance, world.player)
+ if not state.can_reach(entrance.connected_region, "Region", world.player):
+ return False
+
+ if "Free Roam" in entrance.connected_region.name:
+ return True
+
+ name: str = f"Act Completion ({entrance.connected_region.name})"
+ return world.multiworld.get_location(name, world.player).access_rule(state)
+
+
+def can_clear_alpine(state: CollectionState, world: "HatInTimeWorld") -> bool:
+ return state.has("Birdhouse Cleared", world.player) and state.has("Lava Cake Cleared", world.player) \
+ and state.has("Windmill Cleared", world.player) and state.has("Twilight Bell Cleared", world.player)
+
+
+def can_clear_metro(state: CollectionState, world: "HatInTimeWorld") -> bool:
+ return state.has("Nyakuza Intro Cleared", world.player) \
+ and state.has("Yellow Overpass Station Cleared", world.player) \
+ and state.has("Yellow Overpass Manhole Cleared", world.player) \
+ and state.has("Green Clean Station Cleared", world.player) \
+ and state.has("Green Clean Manhole Cleared", world.player) \
+ and state.has("Bluefin Tunnel Cleared", world.player) \
+ and state.has("Pink Paw Station Cleared", world.player) \
+ and state.has("Pink Paw Manhole Cleared", world.player)
+
+
+def set_rules(world: "HatInTimeWorld"):
+ # First, chapter access
+ starting_chapter = ChapterIndex(world.options.StartingChapter)
+ world.chapter_timepiece_costs[starting_chapter] = 0
+
+ # Chapter costs increase progressively. Randomly decide the chapter order, except for Finale
+ chapter_list: List[ChapterIndex] = [ChapterIndex.MAFIA, ChapterIndex.BIRDS,
+ ChapterIndex.SUBCON, ChapterIndex.ALPINE]
+
+ final_chapter = ChapterIndex.FINALE
+ if world.options.EndGoal == EndGoal.option_rush_hour:
+ final_chapter = ChapterIndex.METRO
+ chapter_list.append(ChapterIndex.FINALE)
+ elif world.options.EndGoal == EndGoal.option_seal_the_deal:
+ final_chapter = None
+ chapter_list.append(ChapterIndex.FINALE)
+
+ if world.is_dlc1():
+ chapter_list.append(ChapterIndex.CRUISE)
+
+ if world.is_dlc2() and final_chapter != ChapterIndex.METRO:
+ chapter_list.append(ChapterIndex.METRO)
+
+ chapter_list.remove(starting_chapter)
+ world.random.shuffle(chapter_list)
+
+ # Make sure Alpine is unlocked before any DLC chapters are, as the Alpine door needs to be open to access them
+ if starting_chapter != ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()):
+ index1 = 69
+ index2 = 69
+ pos: int
+ lowest_index: int
+ chapter_list.remove(ChapterIndex.ALPINE)
+
+ if world.is_dlc1():
+ index1 = chapter_list.index(ChapterIndex.CRUISE)
+
+ if world.is_dlc2() and final_chapter != ChapterIndex.METRO:
+ index2 = chapter_list.index(ChapterIndex.METRO)
+
+ lowest_index = min(index1, index2)
+ if lowest_index == 0:
+ pos = 0
+ else:
+ pos = world.random.randint(0, lowest_index)
+
+ chapter_list.insert(pos, ChapterIndex.ALPINE)
+
+ lowest_cost: int = world.options.LowestChapterCost.value
+ highest_cost: int = world.options.HighestChapterCost.value
+ cost_increment: int = world.options.ChapterCostIncrement.value
+ min_difference: int = world.options.ChapterCostMinDifference.value
+ last_cost = 0
+
+ for i, chapter in enumerate(chapter_list):
+ min_range: int = lowest_cost + (cost_increment * i)
+ if min_range >= highest_cost:
+ min_range = highest_cost-1
+
+ value: int = world.random.randint(min_range, min(highest_cost, max(lowest_cost, last_cost + cost_increment)))
+ cost = world.random.randint(value, min(value + cost_increment, highest_cost))
+ if i >= 1:
+ if last_cost + min_difference > cost:
+ cost = last_cost + min_difference
+
+ cost = min(cost, highest_cost)
+ world.chapter_timepiece_costs[chapter] = cost
+ last_cost = cost
+
+ if final_chapter is not None:
+ final_chapter_cost: int
+ if world.options.FinalChapterMinCost == world.options.FinalChapterMaxCost:
+ final_chapter_cost = world.options.FinalChapterMaxCost.value
+ else:
+ final_chapter_cost = world.random.randint(world.options.FinalChapterMinCost.value,
+ world.options.FinalChapterMaxCost.value)
+
+ world.chapter_timepiece_costs[final_chapter] = final_chapter_cost
+
+ add_rule(world.multiworld.get_entrance("Telescope -> Mafia Town", world.player),
+ lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.MAFIA]))
+
+ add_rule(world.multiworld.get_entrance("Telescope -> Battle of the Birds", world.player),
+ lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.BIRDS]))
+
+ add_rule(world.multiworld.get_entrance("Telescope -> Subcon Forest", world.player),
+ lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.SUBCON]))
+
+ add_rule(world.multiworld.get_entrance("Telescope -> Alpine Skyline", world.player),
+ lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE]))
+
+ add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player),
+ lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.FINALE])
+ and can_use_hat(state, world, HatType.BREWING) and can_use_hat(state, world, HatType.DWELLER))
+
+ if world.is_dlc1():
+ add_rule(world.multiworld.get_entrance("Telescope -> Arctic Cruise", world.player),
+ lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE])
+ and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.CRUISE]))
+
+ if world.is_dlc2():
+ add_rule(world.multiworld.get_entrance("Telescope -> Nyakuza Metro", world.player),
+ lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE])
+ and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.METRO])
+ and can_use_hat(state, world, HatType.DWELLER) and can_use_hat(state, world, HatType.ICE))
+
+ if not world.options.ActRandomizer:
+ set_default_rift_rules(world)
+
+ table = {**location_table, **event_locs}
+ for (key, data) in table.items():
+ if not is_location_valid(world, key):
+ continue
+
+ loc = world.multiworld.get_location(key, world.player)
+
+ for hat in data.required_hats:
+ add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h))
+
+ if data.hookshot:
+ add_rule(loc, lambda state: can_use_hookshot(state, world))
+
+ if data.paintings > 0 and world.options.ShuffleSubconPaintings:
+ add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
+
+ if data.hit_type != HitType.none and world.options.UmbrellaLogic:
+ if data.hit_type == HitType.umbrella:
+ add_rule(loc, lambda state: state.has("Umbrella", world.player))
+
+ elif data.hit_type == HitType.umbrella_or_brewing:
+ add_rule(loc, lambda state: state.has("Umbrella", world.player)
+ or can_use_hat(state, world, HatType.BREWING))
+
+ elif data.hit_type == HitType.dweller_bell:
+ add_rule(loc, lambda state: state.has("Umbrella", world.player)
+ or can_use_hat(state, world, HatType.BREWING)
+ or can_use_hat(state, world, HatType.DWELLER))
+
+ for misc in data.misc_required:
+ add_rule(loc, lambda state, item=misc: state.has(item, world.player))
+
+ set_specific_rules(world)
+
+ # Putting all of this here, so it doesn't get overridden by anything
+ # Illness starts the player past the intro
+ alpine_entrance = world.multiworld.get_entrance("AFR -> Alpine Skyline Area", world.player)
+ add_rule(alpine_entrance, lambda state: can_use_hookshot(state, world))
+ if world.options.UmbrellaLogic:
+ add_rule(alpine_entrance, lambda state: state.has("Umbrella", world.player))
+
+ if zipline_logic(world):
+ add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player),
+ lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player))
+
+ add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player),
+ lambda state: state.has("Zipline Unlock - The Lava Cake Path", world.player))
+
+ add_rule(world.multiworld.get_entrance("-> The Windmill", world.player),
+ lambda state: state.has("Zipline Unlock - The Windmill Path", world.player))
+
+ add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player),
+ lambda state: state.has("Zipline Unlock - The Twilight Bell Path", world.player))
+
+ add_rule(world.multiworld.get_location("Act Completion (The Illness has Spread)", world.player),
+ lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player)
+ and state.has("Zipline Unlock - The Lava Cake Path", world.player)
+ and state.has("Zipline Unlock - The Windmill Path", world.player))
+
+ if zipline_logic(world):
+ for (loc, zipline) in zipline_unlocks.items():
+ add_rule(world.multiworld.get_location(loc, world.player),
+ lambda state, z=zipline: state.has(z, world.player))
+
+ dummy_entrances: List[Entrance] = []
+
+ for (key, acts) in act_connections.items():
+ if "Arctic Cruise" in key and not world.is_dlc1():
+ continue
+
+ entrance: Entrance = world.multiworld.get_entrance(key, world.player)
+ region: Region = entrance.connected_region
+ access_rules: List[Callable[[CollectionState], bool]] = []
+ dummy_entrances.append(entrance)
+
+ # Entrances to this act that we have to set access_rules on
+ entrances: List[Entrance] = []
+
+ for i, act in enumerate(acts, start=1):
+ act_entrance: Entrance = world.multiworld.get_entrance(act, world.player)
+ access_rules.append(act_entrance.access_rule)
+ required_region = act_entrance.connected_region
+ name: str = f"{key}: Connection {i}"
+ new_entrance: Entrance = required_region.connect(region, name)
+ entrances.append(new_entrance)
+
+ # Copy access rules from act completions
+ if "Free Roam" not in required_region.name:
+ rule: Callable[[CollectionState], bool]
+ name = f"Act Completion ({required_region.name})"
+ rule = world.multiworld.get_location(name, world.player).access_rule
+ access_rules.append(rule)
+
+ for e in entrances:
+ for rules in access_rules:
+ add_rule(e, rules)
+
+ for e in dummy_entrances:
+ set_rule(e, lambda state: False)
+
+ set_event_rules(world)
+
+ if world.options.EndGoal == EndGoal.option_finale:
+ world.multiworld.completion_condition[world.player] = lambda state: state.has("Time Piece Cluster", world.player)
+ elif world.options.EndGoal == EndGoal.option_rush_hour:
+ world.multiworld.completion_condition[world.player] = lambda state: state.has("Rush Hour Cleared", world.player)
+
+
+def set_specific_rules(world: "HatInTimeWorld"):
+ add_rule(world.multiworld.get_location("Mafia Boss Shop Item", world.player),
+ lambda state: state.has("Time Piece", world.player, 12)
+ and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.BIRDS]))
+
+ set_mafia_town_rules(world)
+ set_botb_rules(world)
+ set_subcon_rules(world)
+ set_alps_rules(world)
+
+ if world.is_dlc1():
+ set_dlc1_rules(world)
+
+ if world.is_dlc2():
+ set_dlc2_rules(world)
+
+ difficulty: Difficulty = get_difficulty(world)
+
+ if difficulty >= Difficulty.MODERATE:
+ set_moderate_rules(world)
+
+ if difficulty >= Difficulty.HARD:
+ set_hard_rules(world)
+
+ if difficulty >= Difficulty.EXPERT:
+ set_expert_rules(world)
+
+
+def set_moderate_rules(world: "HatInTimeWorld"):
+ # Moderate: Gallery without Brewing Hat
+ set_rule(world.multiworld.get_location("Act Completion (Time Rift - Gallery)", world.player), lambda state: True)
+
+ # Moderate: Above Boats via Ice Hat Sliding
+ add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player),
+ lambda state: can_use_hat(state, world, HatType.ICE), "or")
+
+ # Moderate: Clock Tower Chest + Ruined Tower with nothing
+ add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
+ add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
+
+ # Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell
+ for loc in world.multiworld.get_region("The Subcon Well", world.player).locations:
+ set_rule(loc, lambda state: has_paintings(state, world, 1))
+
+ # Moderate: Vanessa Manor with nothing
+ for loc in world.multiworld.get_region("Queen Vanessa's Manor", world.player).locations:
+ set_rule(loc, lambda state: has_paintings(state, world, 1))
+
+ set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player),
+ lambda state: has_paintings(state, world, 1))
+
+ # Moderate: Village Time Rift with nothing IF umbrella logic is off
+ if not world.options.UmbrellaLogic:
+ set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: True)
+
+ # Moderate: get to Birdhouse/Yellow Band Hills without Brewing Hat
+ set_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player),
+ lambda state: can_use_hookshot(state, world))
+ set_rule(world.multiworld.get_location("Alpine Skyline - Yellow Band Hills", world.player),
+ lambda state: can_use_hookshot(state, world))
+
+ # Moderate: The Birdhouse - Dweller Platforms Relic with only Birdhouse access
+ set_rule(world.multiworld.get_location("Alpine Skyline - The Birdhouse: Dweller Platforms Relic", world.player),
+ lambda state: True)
+
+ # Moderate: Twilight Path without Dweller Mask
+ set_rule(world.multiworld.get_location("Alpine Skyline - The Twilight Path", world.player), lambda state: True)
+
+ # Moderate: Mystifying Time Mesa time trial without hats
+ set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player),
+ lambda state: can_use_hookshot(state, world))
+
+ # Moderate: Goat Refinery from TIHS with Sprint only
+ add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player),
+ lambda state: state.has("TIHS Access", world.player)
+ and can_use_hat(state, world, HatType.SPRINT), "or")
+
+ # Moderate: Finale Telescope with only Ice Hat
+ add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player),
+ lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.FINALE])
+ and can_use_hat(state, world, HatType.ICE), "or")
+
+ # Moderate: Finale without Hookshot
+ set_rule(world.multiworld.get_location("Act Completion (The Finale)", world.player),
+ lambda state: can_use_hat(state, world, HatType.DWELLER))
+
+ if world.is_dlc1():
+ # Moderate: clear Rock the Boat without Ice Hat
+ add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
+ add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
+
+ # Moderate: clear Deep Sea without Ice Hat
+ set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player),
+ lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER))
+
+ # There is a glitched fall damage volume near the Yellow Overpass time piece that warps the player to Pink Paw.
+ # Yellow Overpass time piece can also be reached without Hookshot quite easily.
+ if world.is_dlc2():
+ # No Hookshot
+ set_rule(world.multiworld.get_location("Act Completion (Yellow Overpass Station)", world.player),
+ lambda state: True)
+
+ # No Dweller, Hookshot, or Time Stop for these
+ set_rule(world.multiworld.get_location("Pink Paw Station - Cat Vacuum", world.player), lambda state: True)
+ set_rule(world.multiworld.get_location("Pink Paw Station - Behind Fan", world.player), lambda state: True)
+ set_rule(world.multiworld.get_location("Pink Paw Station - Pink Ticket Booth", world.player), lambda state: True)
+ set_rule(world.multiworld.get_location("Act Completion (Pink Paw Station)", world.player), lambda state: True)
+ for key in shop_locations.keys():
+ if "Pink Paw Station Thug" in key and is_location_valid(world, key):
+ set_rule(world.multiworld.get_location(key, world.player), lambda state: True)
+
+ # Moderate: clear Rush Hour without Hookshot
+ set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
+ lambda state: state.has("Metro Ticket - Pink", world.player)
+ and state.has("Metro Ticket - Yellow", world.player)
+ and state.has("Metro Ticket - Blue", world.player)
+ and can_use_hat(state, world, HatType.ICE)
+ and can_use_hat(state, world, HatType.BREWING))
+
+ # Moderate: Bluefin Tunnel + Pink Paw Station without tickets
+ if not world.options.NoTicketSkips:
+ set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True)
+ set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True)
+
+
+def set_hard_rules(world: "HatInTimeWorld"):
+ # Hard: clear Time Rift - The Twilight Bell with Sprint+Scooter only
+ add_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player),
+ lambda state: can_use_hat(state, world, HatType.SPRINT)
+ and state.has("Scooter Badge", world.player), "or")
+
+ # No Dweller Mask required
+ set_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player),
+ lambda state: has_paintings(state, world, 3))
+ set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player),
+ lambda state: has_paintings(state, world, 3))
+
+ # Cherry bridge over boss arena gap (painting still expected)
+ set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
+ lambda state: has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player))
+
+ set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player),
+ lambda state: has_paintings(state, world, 2, True))
+ set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player),
+ lambda state: has_paintings(state, world, 2, True))
+ set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player),
+ lambda state: has_paintings(state, world, 3, True))
+
+ # SDJ
+ add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player),
+ lambda state: can_use_hat(state, world, HatType.SPRINT) and has_paintings(state, world, 2), "or")
+
+ add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player),
+ lambda state: can_use_hat(state, world, HatType.SPRINT), "or")
+
+ # Hard: Goat Refinery from TIHS with nothing
+ add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player),
+ lambda state: state.has("TIHS Access", world.player), "or")
+
+ if world.is_dlc1():
+ # Hard: clear Deep Sea without Dweller Mask
+ set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player),
+ lambda state: can_use_hookshot(state, world))
+
+ if world.is_dlc2():
+ # Hard: clear Green Clean Manhole without Dweller Mask
+ set_rule(world.multiworld.get_location("Act Completion (Green Clean Manhole)", world.player),
+ lambda state: can_use_hat(state, world, HatType.ICE))
+
+ # Hard: clear Rush Hour with Brewing Hat only
+ if world.options.NoTicketSkips != NoTicketSkips.option_true:
+ set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
+ lambda state: can_use_hat(state, world, HatType.BREWING))
+ else:
+ set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
+ lambda state: can_use_hat(state, world, HatType.BREWING)
+ and state.has("Metro Ticket - Yellow", world.player)
+ and state.has("Metro Ticket - Blue", world.player)
+ and state.has("Metro Ticket - Pink", world.player))
+
+
+def set_expert_rules(world: "HatInTimeWorld"):
+ # Finale Telescope with no hats
+ set_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player),
+ lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.FINALE]))
+
+ # Expert: Mafia Town - Above Boats, Top of Lighthouse, and Hot Air Balloon with nothing
+ set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True)
+ set_rule(world.multiworld.get_location("Mafia Town - Top of Lighthouse", world.player), lambda state: True)
+ set_rule(world.multiworld.get_location("Mafia Town - Hot Air Balloon", world.player), lambda state: True)
+
+ # Expert: Clear Dead Bird Studio with nothing
+ for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations:
+ set_rule(loc, lambda state: True)
+
+ set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player), lambda state: True)
+
+ # Expert: Clear Dead Bird Studio Basement without Hookshot
+ for loc in world.multiworld.get_region("Dead Bird Studio Basement", world.player).locations:
+ set_rule(loc, lambda state: True)
+
+ # Expert: get to and clear Twilight Bell without Dweller Mask.
+ # Dweller Mask OR Sprint Hat OR Brewing Hat OR Time Stop + Umbrella required to complete act.
+ add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player),
+ lambda state: can_use_hookshot(state, world), "or")
+
+ add_rule(world.multiworld.get_location("Act Completion (The Twilight Bell)", world.player),
+ lambda state: can_use_hat(state, world, HatType.BREWING)
+ or can_use_hat(state, world, HatType.DWELLER)
+ or can_use_hat(state, world, HatType.SPRINT)
+ or (can_use_hat(state, world, HatType.TIME_STOP) and state.has("Umbrella", world.player)))
+
+ # Expert: Time Rift - Curly Tail Trail with nothing
+ # Time Rift - Twilight Bell and Time Rift - Village with nothing
+ set_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player),
+ lambda state: True)
+
+ set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: True)
+ set_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player),
+ lambda state: True)
+
+ # Expert: Cherry Hovering
+ subcon_area = world.multiworld.get_region("Subcon Forest Area", world.player)
+ yche = world.multiworld.get_region("Your Contract has Expired", world.player)
+ entrance = yche.connect(subcon_area, "Subcon Forest Entrance YCHE")
+
+ if world.options.NoPaintingSkips:
+ add_rule(entrance, lambda state: has_paintings(state, world, 1))
+
+ set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
+ lambda state: can_use_hookshot(state, world) and can_hit(state, world)
+ and has_paintings(state, world, 1, True))
+
+ # Set painting rules only. Skipping paintings is determined in has_paintings
+ set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
+ lambda state: has_paintings(state, world, 1, True))
+ set_rule(world.multiworld.get_location("Subcon Forest - Magnet Badge Bush", world.player),
+ lambda state: has_paintings(state, world, 3, True))
+
+ # You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him
+ subcon_area.connect(yche, "Snatcher Hover")
+ set_rule(world.multiworld.get_location("Act Completion (Your Contract has Expired)", world.player),
+ lambda state: True)
+
+ if world.is_dlc2():
+ # Expert: clear Rush Hour with nothing
+ if not world.options.NoTicketSkips:
+ set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True)
+ else:
+ set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
+ lambda state: state.has("Metro Ticket - Yellow", world.player)
+ and state.has("Metro Ticket - Blue", world.player)
+ and state.has("Metro Ticket - Pink", world.player))
+
+ # Expert: Yellow/Green Manhole with nothing using a Boop Clip
+ set_rule(world.multiworld.get_location("Act Completion (Yellow Overpass Manhole)", world.player),
+ lambda state: True)
+ set_rule(world.multiworld.get_location("Act Completion (Green Clean Manhole)", world.player),
+ lambda state: True)
+
+
+def set_mafia_town_rules(world: "HatInTimeWorld"):
+ add_rule(world.multiworld.get_location("Mafia Town - Behind HQ Chest", world.player),
+ lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", world.player)
+ or state.can_reach("Down with the Mafia!", "Region", world.player)
+ or state.can_reach("Cheating the Race", "Region", world.player)
+ or state.can_reach("The Golden Vault", "Region", world.player))
+
+ # Old guys don't appear in SCFOS
+ add_rule(world.multiworld.get_location("Mafia Town - Old Man (Steel Beams)", world.player),
+ lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player)
+ or state.can_reach("Barrel Battle", "Region", world.player)
+ or state.can_reach("Cheating the Race", "Region", world.player)
+ or state.can_reach("The Golden Vault", "Region", world.player)
+ or state.can_reach("Down with the Mafia!", "Region", world.player))
+
+ add_rule(world.multiworld.get_location("Mafia Town - Old Man (Seaside Spaghetti)", world.player),
+ lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player)
+ or state.can_reach("Barrel Battle", "Region", world.player)
+ or state.can_reach("Cheating the Race", "Region", world.player)
+ or state.can_reach("The Golden Vault", "Region", world.player)
+ or state.can_reach("Down with the Mafia!", "Region", world.player))
+
+ # Only available outside She Came from Outer Space
+ add_rule(world.multiworld.get_location("Mafia Town - Mafia Geek Platform", world.player),
+ lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player)
+ or state.can_reach("Barrel Battle", "Region", world.player)
+ or state.can_reach("Down with the Mafia!", "Region", world.player)
+ or state.can_reach("Cheating the Race", "Region", world.player)
+ or state.can_reach("Heating Up Mafia Town", "Region", world.player)
+ or state.can_reach("The Golden Vault", "Region", world.player))
+
+ # Only available outside Down with the Mafia! (for some reason)
+ add_rule(world.multiworld.get_location("Mafia Town - On Scaffolding", world.player),
+ lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player)
+ or state.can_reach("Barrel Battle", "Region", world.player)
+ or state.can_reach("She Came from Outer Space", "Region", world.player)
+ or state.can_reach("Cheating the Race", "Region", world.player)
+ or state.can_reach("Heating Up Mafia Town", "Region", world.player)
+ or state.can_reach("The Golden Vault", "Region", world.player))
+
+ # For some reason, the brewing crate is removed in HUMT
+ add_rule(world.multiworld.get_location("Mafia Town - Secret Cave", world.player),
+ lambda state: state.has("HUMT Access", world.player), "or")
+
+ # Can bounce across the lava to get this without Hookshot (need to die though)
+ add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player),
+ lambda state: state.has("HUMT Access", world.player), "or")
+
+ if world.options.CTRLogic == CTRLogic.option_nothing:
+ set_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), lambda state: True)
+ elif world.options.CTRLogic == CTRLogic.option_sprint:
+ add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player),
+ lambda state: can_use_hat(state, world, HatType.SPRINT), "or")
+ elif world.options.CTRLogic == CTRLogic.option_scooter:
+ add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player),
+ lambda state: can_use_hat(state, world, HatType.SPRINT)
+ and state.has("Scooter Badge", world.player), "or")
+
+
+def set_botb_rules(world: "HatInTimeWorld"):
+ if not world.options.UmbrellaLogic and get_difficulty(world) < Difficulty.MODERATE:
+ set_rule(world.multiworld.get_location("Dead Bird Studio - DJ Grooves Sign Chest", world.player),
+ lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING))
+ set_rule(world.multiworld.get_location("Dead Bird Studio - Tepee Chest", world.player),
+ lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING))
+ set_rule(world.multiworld.get_location("Dead Bird Studio - Conductor Chest", world.player),
+ lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING))
+ set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player),
+ lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING))
+
+
+def set_subcon_rules(world: "HatInTimeWorld"):
+ set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player),
+ lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player)
+ or can_use_hat(state, world, HatType.DWELLER))
+
+ # You can't skip over the boss arena wall without cherry hover, so these two need to be set this way
+ set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
+ lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world)
+ and has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player))
+
+ # The painting wall can't be skipped without cherry hover, which is Expert
+ set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
+ lambda state: can_use_hookshot(state, world) and can_hit(state, world)
+ and has_paintings(state, world, 1, False))
+
+ add_rule(world.multiworld.get_entrance("Subcon Forest - Act 2", world.player),
+ lambda state: state.has("Snatcher's Contract - The Subcon Well", world.player))
+
+ add_rule(world.multiworld.get_entrance("Subcon Forest - Act 3", world.player),
+ lambda state: state.has("Snatcher's Contract - Toilet of Doom", world.player))
+
+ add_rule(world.multiworld.get_entrance("Subcon Forest - Act 4", world.player),
+ lambda state: state.has("Snatcher's Contract - Queen Vanessa's Manor", world.player))
+
+ add_rule(world.multiworld.get_entrance("Subcon Forest - Act 5", world.player),
+ lambda state: state.has("Snatcher's Contract - Mail Delivery Service", world.player))
+
+ if painting_logic(world):
+ add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player),
+ lambda state: has_paintings(state, world, 1, False))
+
+
+def set_alps_rules(world: "HatInTimeWorld"):
+ add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player),
+ lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.BREWING))
+
+ add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player),
+ lambda state: can_use_hookshot(state, world))
+
+ add_rule(world.multiworld.get_entrance("-> The Windmill", world.player),
+ lambda state: can_use_hookshot(state, world))
+
+ add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player),
+ lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER))
+
+ add_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player),
+ lambda state: can_use_hat(state, world, HatType.SPRINT) or can_use_hat(state, world, HatType.TIME_STOP))
+
+ add_rule(world.multiworld.get_entrance("Alpine Skyline - Finale", world.player),
+ lambda state: can_clear_alpine(state, world))
+
+ add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player),
+ lambda state: state.has("AFR Access", world.player)
+ and can_use_hookshot(state, world)
+ and can_hit(state, world, True))
+
+
+def set_dlc1_rules(world: "HatInTimeWorld"):
+ add_rule(world.multiworld.get_entrance("Cruise Ship Entrance BV", world.player),
+ lambda state: can_use_hookshot(state, world))
+
+ # This particular item isn't present in Act 3 for some reason, yes in vanilla too
+ add_rule(world.multiworld.get_location("The Arctic Cruise - Toilet", world.player),
+ lambda state: state.can_reach("Bon Voyage!", "Region", world.player)
+ or state.can_reach("Ship Shape", "Region", world.player))
+
+
+def set_dlc2_rules(world: "HatInTimeWorld"):
+ add_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player),
+ lambda state: state.has("Metro Ticket - Green", world.player)
+ or state.has("Metro Ticket - Blue", world.player))
+
+ add_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player),
+ lambda state: state.has("Metro Ticket - Pink", world.player)
+ or state.has("Metro Ticket - Yellow", world.player) and state.has("Metro Ticket - Blue", world.player))
+
+ add_rule(world.multiworld.get_entrance("Nyakuza Metro - Finale", world.player),
+ lambda state: can_clear_metro(state, world))
+
+ add_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
+ lambda state: state.has("Metro Ticket - Yellow", world.player)
+ and state.has("Metro Ticket - Blue", world.player)
+ and state.has("Metro Ticket - Pink", world.player))
+
+ for key in shop_locations.keys():
+ if "Green Clean Station Thug B" in key and is_location_valid(world, key):
+ add_rule(world.multiworld.get_location(key, world.player),
+ lambda state: state.has("Metro Ticket - Yellow", world.player), "or")
+
+
+def reg_act_connection(world: "HatInTimeWorld", region: Union[str, Region], unlocked_entrance: Union[str, Entrance]):
+ reg: Region
+ entrance: Entrance
+ if isinstance(region, str):
+ reg = world.multiworld.get_region(region, world.player)
+ else:
+ reg = region
+
+ if isinstance(unlocked_entrance, str):
+ entrance = world.multiworld.get_entrance(unlocked_entrance, world.player)
+ else:
+ entrance = unlocked_entrance
+
+ world.multiworld.register_indirect_condition(reg, entrance)
+
+
+# See randomize_act_entrances in Regions.py
+# Called before set_rules
+def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]):
+
+ # This is accessing the regions in place of these time rifts, so we can set the rules on all the entrances.
+ for entrance in regions["Time Rift - Gallery"].entrances:
+ add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING)
+ and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.BIRDS]))
+
+ for entrance in regions["Time Rift - The Lab"].entrances:
+ add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER)
+ and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE]))
+
+ for entrance in regions["Time Rift - Sewers"].entrances:
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Mafia Town - Act 4"))
+ reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 4",
+ world.player).connected_region, entrance)
+
+ for entrance in regions["Time Rift - Bazaar"].entrances:
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Mafia Town - Act 6"))
+ reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 6",
+ world.player).connected_region, entrance)
+
+ for entrance in regions["Time Rift - Mafia of Cooks"].entrances:
+ add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger"))
+
+ for entrance in regions["Time Rift - The Owl Express"].entrances:
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 2"))
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 3"))
+ reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 2",
+ world.player).connected_region, entrance)
+ reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 3",
+ world.player).connected_region, entrance)
+
+ for entrance in regions["Time Rift - The Moon"].entrances:
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 4"))
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 5"))
+ reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 4",
+ world.player).connected_region, entrance)
+ reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 5",
+ world.player).connected_region, entrance)
+
+ for entrance in regions["Time Rift - Dead Bird Studio"].entrances:
+ add_rule(entrance, lambda state: has_relic_combo(state, world, "Train"))
+
+ for entrance in regions["Time Rift - Pipe"].entrances:
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 2"))
+ reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 2",
+ world.player).connected_region, entrance)
+ if painting_logic(world):
+ add_rule(entrance, lambda state: has_paintings(state, world, 2))
+
+ for entrance in regions["Time Rift - Village"].entrances:
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 4"))
+ reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 4",
+ world.player).connected_region, entrance)
+
+ if painting_logic(world):
+ add_rule(entrance, lambda state: has_paintings(state, world, 2))
+
+ for entrance in regions["Time Rift - Sleepy Subcon"].entrances:
+ add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO"))
+ if painting_logic(world):
+ add_rule(entrance, lambda state: has_paintings(state, world, 3))
+
+ for entrance in regions["Time Rift - Curly Tail Trail"].entrances:
+ add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player))
+
+ for entrance in regions["Time Rift - The Twilight Bell"].entrances:
+ add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", world.player))
+
+ for entrance in regions["Time Rift - Alpine Skyline"].entrances:
+ add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
+
+ if world.is_dlc1():
+ for entrance in regions["Time Rift - Balcony"].entrances:
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale"))
+ reg_act_connection(world, world.multiworld.get_entrance("The Arctic Cruise - Finale",
+ world.player).connected_region, entrance)
+
+ for entrance in regions["Time Rift - Deep Sea"].entrances:
+ add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))
+
+ if world.is_dlc2():
+ for entrance in regions["Time Rift - Rumbi Factory"].entrances:
+ add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace"))
+
+
+# Basically the same as above, but without the need of the dict since we are just setting defaults
+# Called if Act Rando is disabled
+def set_default_rift_rules(world: "HatInTimeWorld"):
+
+ for entrance in world.multiworld.get_region("Time Rift - Gallery", world.player).entrances:
+ add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING)
+ and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.BIRDS]))
+
+ for entrance in world.multiworld.get_region("Time Rift - The Lab", world.player).entrances:
+ add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER)
+ and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE]))
+
+ for entrance in world.multiworld.get_region("Time Rift - Sewers", world.player).entrances:
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Mafia Town - Act 4"))
+ reg_act_connection(world, "Down with the Mafia!", entrance.name)
+
+ for entrance in world.multiworld.get_region("Time Rift - Bazaar", world.player).entrances:
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Mafia Town - Act 6"))
+ reg_act_connection(world, "Heating Up Mafia Town", entrance.name)
+
+ for entrance in world.multiworld.get_region("Time Rift - Mafia of Cooks", world.player).entrances:
+ add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger"))
+
+ for entrance in world.multiworld.get_region("Time Rift - The Owl Express", world.player).entrances:
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 2"))
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 3"))
+ reg_act_connection(world, "Murder on the Owl Express", entrance.name)
+ reg_act_connection(world, "Picture Perfect", entrance.name)
+
+ for entrance in world.multiworld.get_region("Time Rift - The Moon", world.player).entrances:
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 4"))
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 5"))
+ reg_act_connection(world, "Train Rush", entrance.name)
+ reg_act_connection(world, "The Big Parade", entrance.name)
+
+ for entrance in world.multiworld.get_region("Time Rift - Dead Bird Studio", world.player).entrances:
+ add_rule(entrance, lambda state: has_relic_combo(state, world, "Train"))
+
+ for entrance in world.multiworld.get_region("Time Rift - Pipe", world.player).entrances:
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 2"))
+ reg_act_connection(world, "The Subcon Well", entrance.name)
+ if painting_logic(world):
+ add_rule(entrance, lambda state: has_paintings(state, world, 2))
+
+ for entrance in world.multiworld.get_region("Time Rift - Village", world.player).entrances:
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 4"))
+ reg_act_connection(world, "Queen Vanessa's Manor", entrance.name)
+ if painting_logic(world):
+ add_rule(entrance, lambda state: has_paintings(state, world, 2))
+
+ for entrance in world.multiworld.get_region("Time Rift - Sleepy Subcon", world.player).entrances:
+ add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO"))
+ if painting_logic(world):
+ add_rule(entrance, lambda state: has_paintings(state, world, 3))
+
+ for entrance in world.multiworld.get_region("Time Rift - Curly Tail Trail", world.player).entrances:
+ add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player))
+
+ for entrance in world.multiworld.get_region("Time Rift - The Twilight Bell", world.player).entrances:
+ add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", world.player))
+
+ for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances:
+ add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
+
+ if world.is_dlc1():
+ for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances:
+ add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale"))
+ reg_act_connection(world, "Rock the Boat", entrance.name)
+
+ for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances:
+ add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))
+
+ if world.is_dlc2():
+ for entrance in world.multiworld.get_region("Time Rift - Rumbi Factory", world.player).entrances:
+ add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace"))
+
+
+def set_event_rules(world: "HatInTimeWorld"):
+ for (name, data) in event_locs.items():
+ if not is_location_valid(world, name):
+ continue
+
+ event: Location = world.multiworld.get_location(name, world.player)
+
+ if data.act_event:
+ add_rule(event, world.multiworld.get_location(f"Act Completion ({data.region})", world.player).access_rule)
diff --git a/worlds/ahit/Types.py b/worlds/ahit/Types.py
new file mode 100644
index 000000000000..468cfcb78ad3
--- /dev/null
+++ b/worlds/ahit/Types.py
@@ -0,0 +1,86 @@
+from enum import IntEnum, IntFlag
+from typing import NamedTuple, Optional, List
+from BaseClasses import Location, Item, ItemClassification
+
+
+class HatInTimeLocation(Location):
+ game = "A Hat in Time"
+
+
+class HatInTimeItem(Item):
+ game = "A Hat in Time"
+
+
+class HatType(IntEnum):
+ SPRINT = 0
+ BREWING = 1
+ ICE = 2
+ DWELLER = 3
+ TIME_STOP = 4
+
+
+class HitType(IntEnum):
+ none = 0
+ umbrella = 1
+ umbrella_or_brewing = 2
+ dweller_bell = 3
+
+
+class HatDLC(IntFlag):
+ none = 0b000
+ dlc1 = 0b001
+ dlc2 = 0b010
+ death_wish = 0b100
+ dlc1_dw = 0b101
+ dlc2_dw = 0b110
+
+
+class ChapterIndex(IntEnum):
+ SPACESHIP = 0
+ MAFIA = 1
+ BIRDS = 2
+ SUBCON = 3
+ ALPINE = 4
+ FINALE = 5
+ CRUISE = 6
+ METRO = 7
+
+
+class Difficulty(IntEnum):
+ NORMAL = -1
+ MODERATE = 0
+ HARD = 1
+ EXPERT = 2
+
+
+class LocData(NamedTuple):
+ id: int = 0
+ region: str = ""
+ required_hats: List[HatType] = []
+ hookshot: bool = False
+ dlc_flags: HatDLC = HatDLC.none
+ paintings: int = 0 # Paintings required for Subcon painting shuffle
+ misc_required: List[str] = []
+
+ # For UmbrellaLogic setting only.
+ hit_type: HitType = HitType.none
+
+ # Other
+ act_event: bool = False # Only used for event locations. Copy access rule from act completion
+ nyakuza_thug: str = "" # Name of Nyakuza thug NPC (for metro shops)
+ snatcher_coin: str = "" # Only for Snatcher Coin event locations, name of the Snatcher Coin item
+
+
+class ItemData(NamedTuple):
+ code: Optional[int]
+ classification: ItemClassification
+ dlc_flags: Optional[HatDLC] = HatDLC.none
+
+
+hat_type_to_item = {
+ HatType.SPRINT: "Sprint Hat",
+ HatType.BREWING: "Brewing Hat",
+ HatType.ICE: "Ice Hat",
+ HatType.DWELLER: "Dweller Mask",
+ HatType.TIME_STOP: "Time Stop Hat",
+}
diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py
new file mode 100644
index 000000000000..dd5e88abbc66
--- /dev/null
+++ b/worlds/ahit/__init__.py
@@ -0,0 +1,386 @@
+from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld
+from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \
+ calculate_yarn_costs, alps_hooks
+from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
+from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
+ get_total_locations
+from .Rules import set_rules, has_paintings
+from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal, create_option_groups
+from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item, Difficulty
+from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes
+from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses
+from worlds.AutoWorld import World, WebWorld, CollectionState
+from worlds.generic.Rules import add_rule
+from typing import List, Dict, TextIO
+from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type
+from Utils import local_path
+
+
+def launch_client():
+ from .Client import launch
+ launch_subprocess(launch, name="AHITClient")
+
+
+components.append(Component("A Hat in Time Client", "AHITClient", func=launch_client,
+ component_type=Type.CLIENT, icon='yatta'))
+
+icon_paths['yatta'] = local_path('data', 'yatta.png')
+
+
+class AWebInTime(WebWorld):
+ theme = "partyTime"
+ option_groups = create_option_groups()
+ tutorials = [Tutorial(
+ "Multiworld Setup Guide",
+ "A guide for setting up A Hat in Time to be played in Archipelago.",
+ "English",
+ "ahit_en.md",
+ "setup/en",
+ ["CookieCat"]
+ )]
+
+
+class HatInTimeWorld(World):
+ """
+ A Hat in Time is a cute-as-peck 3D platformer featuring a little girl who stitches hats for wicked powers!
+ Freely explore giant worlds and recover Time Pieces to travel to new heights!
+ """
+
+ game = "A Hat in Time"
+ item_name_to_id = {name: data.code for name, data in item_table.items()}
+ location_name_to_id = get_location_names()
+ options_dataclass = AHITOptions
+ options: AHITOptions
+ item_name_groups = relic_groups
+ web = AWebInTime()
+
+ def __init__(self, multiworld: "MultiWorld", player: int):
+ super().__init__(multiworld, player)
+ self.act_connections: Dict[str, str] = {}
+ self.shop_locs: List[str] = []
+
+ self.hat_craft_order: List[HatType] = [HatType.SPRINT, HatType.BREWING, HatType.ICE,
+ HatType.DWELLER, HatType.TIME_STOP]
+
+ self.hat_yarn_costs: Dict[HatType, int] = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1,
+ HatType.DWELLER: -1, HatType.TIME_STOP: -1}
+
+ self.chapter_timepiece_costs: Dict[ChapterIndex, int] = {ChapterIndex.MAFIA: -1,
+ ChapterIndex.BIRDS: -1,
+ ChapterIndex.SUBCON: -1,
+ ChapterIndex.ALPINE: -1,
+ ChapterIndex.FINALE: -1,
+ ChapterIndex.CRUISE: -1,
+ ChapterIndex.METRO: -1}
+ self.excluded_dws: List[str] = []
+ self.excluded_bonuses: List[str] = []
+ self.dw_shuffle: List[str] = []
+ self.nyakuza_thug_items: Dict[str, int] = {}
+ self.badge_seller_count: int = 0
+
+ def generate_early(self):
+ adjust_options(self)
+
+ if self.options.StartWithCompassBadge:
+ self.multiworld.push_precollected(self.create_item("Compass Badge"))
+
+ if self.is_dw_only():
+ return
+
+ # Take care of some extremely restrictive starts in other chapters with act shuffle off
+ if not self.options.ActRandomizer:
+ start_chapter = self.options.StartingChapter
+ if start_chapter == ChapterIndex.ALPINE:
+ self.multiworld.push_precollected(self.create_item("Hookshot Badge"))
+ if self.options.UmbrellaLogic:
+ self.multiworld.push_precollected(self.create_item("Umbrella"))
+
+ if self.options.ShuffleAlpineZiplines:
+ ziplines = list(alps_hooks.keys())
+ ziplines.remove("Zipline Unlock - The Twilight Bell Path") # not enough checks from this one
+ self.multiworld.push_precollected(self.create_item(self.random.choice(ziplines)))
+ elif start_chapter == ChapterIndex.SUBCON:
+ if self.options.ShuffleSubconPaintings:
+ self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock"))
+ elif start_chapter == ChapterIndex.BIRDS:
+ if self.options.UmbrellaLogic:
+ if self.options.LogicDifficulty < Difficulty.EXPERT:
+ self.multiworld.push_precollected(self.create_item("Umbrella"))
+ elif self.options.LogicDifficulty < Difficulty.MODERATE:
+ self.multiworld.push_precollected(self.create_item("Umbrella"))
+
+ def create_regions(self):
+ # noinspection PyClassVar
+ self.topology_present = bool(self.options.ActRandomizer)
+
+ create_regions(self)
+ if self.options.EnableDeathWish:
+ create_dw_regions(self)
+
+ if self.is_dw_only():
+ return
+
+ create_events(self)
+ if self.is_dw():
+ if "Snatcher's Hit List" not in self.excluded_dws or "Camera Tourist" not in self.excluded_dws:
+ create_enemy_events(self)
+
+ # place vanilla contract locations if contract shuffle is off
+ if not self.options.ShuffleActContracts:
+ for name in contract_locations.keys():
+ loc = self.get_location(name)
+ loc.place_locked_item(create_item(self, name))
+ if self.options.ShuffleSubconPaintings and loc.name != "Snatcher's Contract - The Subcon Well":
+ add_rule(loc, lambda state: has_paintings(state, self, 1))
+
+ def create_items(self):
+ if self.has_yarn():
+ calculate_yarn_costs(self)
+
+ if self.options.RandomizeHatOrder:
+ self.random.shuffle(self.hat_craft_order)
+ if self.options.RandomizeHatOrder == RandomizeHatOrder.option_time_stop_last:
+ self.hat_craft_order.remove(HatType.TIME_STOP)
+ self.hat_craft_order.append(HatType.TIME_STOP)
+
+ # move precollected hats to the start of the list
+ for i in range(5):
+ hat = HatType(i)
+ if self.is_hat_precollected(hat):
+ self.hat_craft_order.remove(hat)
+ self.hat_craft_order.insert(0, hat)
+
+ self.multiworld.itempool += create_itempool(self)
+
+ def set_rules(self):
+ if self.is_dw_only():
+ # we already have all items if this is the case, no need for rules
+ self.multiworld.push_precollected(HatInTimeItem("Death Wish Only Mode", ItemClassification.progression,
+ None, self.player))
+
+ self.multiworld.completion_condition[self.player] = lambda state: state.has("Death Wish Only Mode",
+ self.player)
+
+ if not self.options.DWEnableBonus:
+ for name in death_wishes:
+ if name == "Snatcher Coins in Nyakuza Metro" and not self.is_dlc2():
+ continue
+
+ if self.options.DWShuffle and name not in self.dw_shuffle:
+ continue
+
+ full_clear = self.multiworld.get_location(f"{name} - All Clear", self.player)
+ full_clear.address = None
+ full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, self.player))
+ full_clear.show_in_spoiler = False
+
+ return
+
+ if self.options.ActRandomizer:
+ randomize_act_entrances(self)
+
+ set_rules(self)
+
+ if self.is_dw():
+ set_dw_rules(self)
+
+ def create_item(self, name: str) -> Item:
+ return create_item(self, name)
+
+ def fill_slot_data(self) -> dict:
+ slot_data: dict = {"Chapter1Cost": self.chapter_timepiece_costs[ChapterIndex.MAFIA],
+ "Chapter2Cost": self.chapter_timepiece_costs[ChapterIndex.BIRDS],
+ "Chapter3Cost": self.chapter_timepiece_costs[ChapterIndex.SUBCON],
+ "Chapter4Cost": self.chapter_timepiece_costs[ChapterIndex.ALPINE],
+ "Chapter5Cost": self.chapter_timepiece_costs[ChapterIndex.FINALE],
+ "Chapter6Cost": self.chapter_timepiece_costs[ChapterIndex.CRUISE],
+ "Chapter7Cost": self.chapter_timepiece_costs[ChapterIndex.METRO],
+ "BadgeSellerItemCount": self.badge_seller_count,
+ "SeedNumber": str(self.multiworld.seed), # For shop prices
+ "SeedName": self.multiworld.seed_name,
+ "TotalLocations": get_total_locations(self)}
+
+ if self.has_yarn():
+ slot_data.setdefault("SprintYarnCost", self.hat_yarn_costs[HatType.SPRINT])
+ slot_data.setdefault("BrewingYarnCost", self.hat_yarn_costs[HatType.BREWING])
+ slot_data.setdefault("IceYarnCost", self.hat_yarn_costs[HatType.ICE])
+ slot_data.setdefault("DwellerYarnCost", self.hat_yarn_costs[HatType.DWELLER])
+ slot_data.setdefault("TimeStopYarnCost", self.hat_yarn_costs[HatType.TIME_STOP])
+ slot_data.setdefault("Hat1", int(self.hat_craft_order[0]))
+ slot_data.setdefault("Hat2", int(self.hat_craft_order[1]))
+ slot_data.setdefault("Hat3", int(self.hat_craft_order[2]))
+ slot_data.setdefault("Hat4", int(self.hat_craft_order[3]))
+ slot_data.setdefault("Hat5", int(self.hat_craft_order[4]))
+
+ if self.options.ActRandomizer:
+ for name in self.act_connections.keys():
+ slot_data[name] = self.act_connections[name]
+
+ if self.is_dlc2() and not self.is_dw_only():
+ for name in self.nyakuza_thug_items.keys():
+ slot_data[name] = self.nyakuza_thug_items[name]
+
+ if self.is_dw():
+ i = 0
+ for name in self.excluded_dws:
+ if self.options.EndGoal.value == EndGoal.option_seal_the_deal and name == "Seal the Deal":
+ continue
+
+ slot_data[f"excluded_dw{i}"] = dw_classes[name]
+ i += 1
+
+ i = 0
+ if not self.options.DWAutoCompleteBonuses:
+ for name in self.excluded_bonuses:
+ if name in self.excluded_dws:
+ continue
+
+ slot_data[f"excluded_bonus{i}"] = dw_classes[name]
+ i += 1
+
+ if self.options.DWShuffle:
+ shuffled_dws = self.dw_shuffle
+ for i in range(len(shuffled_dws)):
+ slot_data[f"dw_{i}"] = dw_classes[shuffled_dws[i]]
+
+ shop_item_names: Dict[str, str] = {}
+ for name in self.shop_locs:
+ loc: Location = self.multiworld.get_location(name, self.player)
+ assert loc.item
+ item_name: str
+ if loc.item.classification is ItemClassification.trap and loc.item.game == "A Hat in Time":
+ item_name = get_shop_trap_name(self)
+ else:
+ item_name = loc.item.name
+
+ shop_item_names.setdefault(str(loc.address), item_name)
+
+ slot_data["ShopItemNames"] = shop_item_names
+
+ for name, value in self.options.as_dict(*self.options_dataclass.type_hints).items():
+ if name in slot_data_options:
+ slot_data[name] = value
+
+ return slot_data
+
+ def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
+ if self.is_dw_only() or not self.options.ActRandomizer:
+ return
+
+ new_hint_data = {}
+ alpine_regions = ["The Birdhouse", "The Lava Cake", "The Windmill",
+ "The Twilight Bell", "Alpine Skyline Area", "Alpine Skyline Area (TIHS)"]
+
+ metro_regions = ["Yellow Overpass Station", "Green Clean Station", "Bluefin Tunnel", "Pink Paw Station"]
+
+ for key, data in location_table.items():
+ if not is_location_valid(self, key):
+ continue
+
+ location = self.multiworld.get_location(key, self.player)
+ region_name: str
+
+ if data.region in alpine_regions:
+ region_name = "Alpine Free Roam"
+ elif data.region in metro_regions:
+ region_name = "Nyakuza Free Roam"
+ elif "Dead Bird Studio - " in data.region:
+ region_name = "Dead Bird Studio"
+ elif data.region in chapter_act_info.keys():
+ region_name = location.parent_region.name
+ else:
+ continue
+
+ new_hint_data[location.address] = get_shuffled_region(self, region_name)
+
+ if self.is_dlc1() and self.options.Tasksanity:
+ ship_shape_region = get_shuffled_region(self, "Ship Shape")
+ id_start: int = TASKSANITY_START_ID
+ for i in range(self.options.TasksanityCheckCount):
+ new_hint_data[id_start+i] = ship_shape_region
+
+ hint_data[self.player] = new_hint_data
+
+ def write_spoiler_header(self, spoiler_handle: TextIO):
+ for i in self.chapter_timepiece_costs:
+ spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.chapter_timepiece_costs[ChapterIndex(i)]))
+
+ for hat in self.hat_craft_order:
+ spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, self.hat_yarn_costs[hat]))
+
+ def collect(self, state: "CollectionState", item: "Item") -> bool:
+ old_count: int = state.count(item.name, self.player)
+ change = super().collect(state, item)
+ if change and old_count == 0:
+ if "Stamp" in item.name:
+ if "2 Stamp" in item.name:
+ state.prog_items[self.player]["Stamps"] += 2
+ else:
+ state.prog_items[self.player]["Stamps"] += 1
+ elif "(Zero Jumps)" in item.name:
+ state.prog_items[self.player]["Zero Jumps"] += 1
+ elif item.name in hit_list.keys():
+ if item.name not in bosses:
+ state.prog_items[self.player]["Enemy"] += 1
+ else:
+ state.prog_items[self.player]["Boss"] += 1
+
+ return change
+
+ def remove(self, state: "CollectionState", item: "Item") -> bool:
+ old_count: int = state.count(item.name, self.player)
+ change = super().remove(state, item)
+ if change and old_count == 1:
+ if "Stamp" in item.name:
+ if "2 Stamp" in item.name:
+ state.prog_items[self.player]["Stamps"] -= 2
+ else:
+ state.prog_items[self.player]["Stamps"] -= 1
+ elif "(Zero Jumps)" in item.name:
+ state.prog_items[self.player]["Zero Jumps"] -= 1
+ elif item.name in hit_list.keys():
+ if item.name not in bosses:
+ state.prog_items[self.player]["Enemy"] -= 1
+ else:
+ state.prog_items[self.player]["Boss"] -= 1
+
+ return change
+
+ def has_yarn(self) -> bool:
+ return not self.is_dw_only() and not self.options.HatItems
+
+ def is_hat_precollected(self, hat: HatType) -> bool:
+ for item in self.multiworld.precollected_items[self.player]:
+ if item.name == hat_type_to_item[hat]:
+ return True
+
+ return False
+
+ def is_dlc1(self) -> bool:
+ return bool(self.options.EnableDLC1)
+
+ def is_dlc2(self) -> bool:
+ return bool(self.options.EnableDLC2)
+
+ def is_dw(self) -> bool:
+ return bool(self.options.EnableDeathWish)
+
+ def is_dw_only(self) -> bool:
+ return self.is_dw() and bool(self.options.DeathWishOnly)
+
+ def is_dw_excluded(self, name: str) -> bool:
+ # don't exclude Seal the Deal if it's our goal
+ if self.options.EndGoal.value == EndGoal.option_seal_the_deal and name == "Seal the Deal" \
+ and f"{name} - Main Objective" not in self.options.exclude_locations:
+ return False
+
+ if name in self.excluded_dws:
+ return True
+
+ return f"{name} - Main Objective" in self.options.exclude_locations
+
+ def is_bonus_excluded(self, name: str) -> bool:
+ if self.is_dw_excluded(name) or name in self.excluded_bonuses:
+ return True
+
+ return f"{name} - All Clear" in self.options.exclude_locations
diff --git a/worlds/ahit/docs/en_A Hat in Time.md b/worlds/ahit/docs/en_A Hat in Time.md
new file mode 100644
index 000000000000..9f1a593bbdd9
--- /dev/null
+++ b/worlds/ahit/docs/en_A Hat in Time.md
@@ -0,0 +1,53 @@
+# A Hat in Time
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
+config file.
+
+## What does randomization do to this game?
+
+Items which the player would normally acquire throughout the game have been moved around.
+Chapter costs are randomized in a progressive order based on your options,
+so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order.
+If act shuffle is turned on, the levels and Time Rifts in these chapters will be randomized as well.
+
+To unlock and access a chapter's Time Rift in act shuffle,
+the levels in place of the original acts required to unlock the Time Rift in the vanilla game must be completed,
+and then you must enter a level that allows you to access that Time Rift.
+For example, Time Rift: Bazaar requires Heating Up Mafia Town to be completed in the vanilla game.
+To unlock this Time Rift in act shuffle (and therefore the level it contains)
+you must complete the level that was shuffled in place of Heating Up Mafia Town
+and then enter the Time Rift through a Mafia Town level.
+
+## What items and locations get shuffled?
+
+Time Pieces, Relics, Yarn, Badges, and most other items are shuffled.
+Unlike in the vanilla game, yarn is typeless, and hats will be automatically stitched
+in a set order once you gather enough yarn for each hat.
+Hats can also optionally be shuffled as individual items instead.
+Any items in the world, shops, act completions,
+and optionally storybook pages or Death Wish contracts are locations.
+
+Any freestanding items that are considered to be progression or useful
+will have a rainbow streak particle attached to them.
+Filler items will have a white glow attached to them instead.
+
+## Which items can be in another player's world?
+
+Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit
+certain items to your own world.
+
+## What does another world's item look like in A Hat in Time?
+
+Items belonging to other worlds are represented by a badge with the Archipelago logo on it.
+
+## When the player receives an item, what happens?
+
+When the player receives an item, it will play the item collect effect and information about the item
+will be printed on the screen and in the in-game developer console.
+
+## Is the DLC required to play A Hat in Time in Archipelago?
+
+No, the DLC expansions are not required to play. Their content can be enabled through certain options
+that are disabled by default, but please don't turn them on if you don't own the respective DLC.
diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md
new file mode 100644
index 000000000000..23b34907071c
--- /dev/null
+++ b/worlds/ahit/docs/setup_en.md
@@ -0,0 +1,65 @@
+# Setup Guide for A Hat in Time in Archipelago
+
+## Required Software
+- [Steam release of A Hat in Time](https://store.steampowered.com/app/253230/A_Hat_in_Time/)
+
+- [Archipelago Workshop Mod for A Hat in Time](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601)
+
+
+## Optional Software
+- [A Hat in Time Archipelago Map Tracker](https://github.com/Mysteryem/ahit-poptracker/releases), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases)
+
+
+## Instructions
+
+1. **BACK UP YOUR SAVE FILES IN YOUR MAIN INSTALL IF YOU CARE ABOUT THEM!!!**
+ Go to `steamapps/common/HatinTime/HatinTimeGame/SaveData/` and copy everything inside that folder over to a safe place.
+ **This is important! Changing the game version CAN and WILL break your existing save files!!!**
+
+
+2. In your Steam library, right-click on **A Hat in Time** in the list of games and click on **Properties**.
+
+
+3. Click the **Betas** tab. In the **Beta Participation** dropdown, select `tcplink`.
+ While it downloads, you can subscribe to the [Archipelago workshop mod.]((https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601))
+
+
+4. Once the game finishes downloading, start it up.
+ In Game Settings, make sure **Enable Developer Console** is checked.
+
+
+5. You should now be good to go. See below for more details on how to use the mod and connect to an Archipelago game.
+
+
+## Connecting to the Archipelago server
+
+To connect to the multiworld server, simply run the **Archipelago AHIT Client** from the Launcher
+and connect it to the Archipelago server.
+The game will connect to the client automatically when you create a new save file.
+
+
+## Console Commands
+
+Commands will not work on the title screen, you must be in-game to use them. To use console commands,
+make sure ***Enable Developer Console*** is checked in Game Settings and press the tilde key or TAB while in-game.
+
+`ap_say ` - Send a chat message to the server. Supports commands, such as `!hint` or `!release`.
+
+`ap_deathlink` - Toggle Death Link.
+
+
+## FAQ/Common Issues
+
+### The game is not connecting when starting a new save!
+For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu
+(rocket icon) in-game, and re-enable the mod.
+
+### Why do relics disappear from the stands in the Spaceship after they're completed?
+This is intentional behaviour. Because of how randomizer logic works, there is no way to predict the order that
+a player will place their relics. Since there are a limited amount of relic stands in the Spaceship, relics are removed
+after being completed to allow for the placement of more relics without being potentially locked out.
+The level that the relic set unlocked will stay unlocked.
+
+### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work!
+There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly
+if you have too many save files. Delete them and it should fix the problem.
\ No newline at end of file
diff --git a/worlds/ahit/test/__init__.py b/worlds/ahit/test/__init__.py
new file mode 100644
index 000000000000..67b750a65c7d
--- /dev/null
+++ b/worlds/ahit/test/__init__.py
@@ -0,0 +1,5 @@
+from test.bases import WorldTestBase
+
+
+class HatInTimeTestBase(WorldTestBase):
+ game = "A Hat in Time"
diff --git a/worlds/ahit/test/test_acts.py b/worlds/ahit/test/test_acts.py
new file mode 100644
index 000000000000..6502db1d9e6b
--- /dev/null
+++ b/worlds/ahit/test/test_acts.py
@@ -0,0 +1,31 @@
+from ..Regions import act_chapters
+from ..Rules import act_connections
+from . import HatInTimeTestBase
+
+
+class TestActs(HatInTimeTestBase):
+ run_default_tests = False
+
+ options = {
+ "ActRandomizer": 2,
+ "EnableDLC1": 1,
+ "EnableDLC2": 1,
+ "ShuffleActContracts": 0,
+ }
+
+ def test_act_shuffle(self):
+ for i in range(300):
+ self.world_setup()
+ self.collect_all_but([""])
+
+ for name in act_chapters.keys():
+ region = self.multiworld.get_region(name, 1)
+ for entrance in region.entrances:
+ if entrance.name in act_connections.keys():
+ continue
+
+ self.assertTrue(self.can_reach_entrance(entrance.name),
+ f"Can't reach {name} from {entrance}\n"
+ f"{entrance.parent_region.entrances[0]} -> {entrance.parent_region} "
+ f"-> {entrance} -> {name}"
+ f" (expected method of access)")
diff --git a/worlds/alttp/Bosses.py b/worlds/alttp/Bosses.py
index 90ffe9dcf4b1..965a86db008a 100644
--- a/worlds/alttp/Bosses.py
+++ b/worlds/alttp/Bosses.py
@@ -6,7 +6,7 @@
from Fill import FillError
from .Options import LTTPBosses as Bosses
from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, \
- has_melee_weapon, has_fire_source
+ has_melee_weapon, has_fire_source, can_use_bombs
if TYPE_CHECKING:
from . import ALTTPWorld
@@ -62,7 +62,8 @@ def MoldormDefeatRule(state, player: int) -> bool:
def HelmasaurKingDefeatRule(state, player: int) -> bool:
# TODO: technically possible with the hammer
- return has_sword(state, player) or can_shoot_arrows(state, player)
+ return (can_use_bombs(state, player, 5) or state.has("Hammer", player)) and (has_sword(state, player)
+ or can_shoot_arrows(state, player))
def ArrghusDefeatRule(state, player: int) -> bool:
@@ -143,7 +144,7 @@ def GanonDefeatRule(state, player: int) -> bool:
can_hurt = has_beam_sword(state, player)
common = can_hurt and has_fire_source(state, player)
# silverless ganon may be needed in anything higher than no glitches
- if state.multiworld.logic[player] != 'noglitches':
+ if state.multiworld.glitches_required[player] != 'no_glitches':
# need to light torch a sufficient amount of times
return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (
state.has('Silver Bow', player) and can_shoot_arrows(state, player)) or
diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py
index 7ac24fde9f28..a0b28829f4bb 100644
--- a/worlds/alttp/Client.py
+++ b/worlds/alttp/Client.py
@@ -107,7 +107,7 @@
"Hyrule Castle - Zelda's Chest": (0x80, 0x10),
'Hyrule Castle - Big Key Drop': (0x80, 0x400),
'Sewers - Dark Cross': (0x32, 0x10),
- 'Hyrule Castle - Key Rat Key Drop': (0x21, 0x400),
+ 'Sewers - Key Rat Key Drop': (0x21, 0x400),
'Sewers - Secret Room - Left': (0x11, 0x10),
'Sewers - Secret Room - Middle': (0x11, 0x20),
'Sewers - Secret Room - Right': (0x11, 0x40),
@@ -339,7 +339,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool:
def new_check(location_id):
new_locations.append(location_id)
ctx.locations_checked.add(location_id)
- location = ctx.location_names[location_id]
+ location = ctx.location_names.lookup_in_game(location_id)
snes_logger.info(
f'New Check: {location} ' +
f'({len(ctx.checked_locations) + 1 if ctx.checked_locations else len(ctx.locations_checked)}/' +
@@ -471,6 +471,7 @@ def new_check(location_id):
class ALTTPSNIClient(SNIClient):
game = "A Link to the Past"
+ patch_suffix = [".aplttp", ".apz3"]
async def deathlink_kill_player(self, ctx):
from SNIClient import DeathState, snes_read, snes_buffered_write, snes_flush_writes
@@ -520,7 +521,8 @@ async def game_watcher(self, ctx):
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
currently_dead = gamemode[0] in DEATH_MODES
- await ctx.handle_deathlink_state(currently_dead)
+ await ctx.handle_deathlink_state(currently_dead,
+ ctx.player_names[ctx.slot] + " ran out of hearts." if ctx.slot else "")
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
@@ -550,9 +552,9 @@ async def game_watcher(self, ctx):
item = ctx.items_received[recv_index]
recv_index += 1
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
- color(ctx.item_names[item.item], 'red', 'bold'),
+ color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'),
color(ctx.player_names[item.player], 'yellow'),
- ctx.location_names[item.location], recv_index, len(ctx.items_received)))
+ ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received)))
snes_buffered_write(ctx, RECV_PROGRESS_ADDR,
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
@@ -680,7 +682,7 @@ def onButtonClick(answer: str = 'no'):
if 'yes' in choice:
import LttPAdjuster
- from worlds.alttp.Rom import get_base_rom_path
+ from .Rom import get_base_rom_path
last_settings.rom = romfile
last_settings.baserom = get_base_rom_path()
last_settings.world = None
diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py
index b789fd6db638..150d52cc6c58 100644
--- a/worlds/alttp/Dungeons.py
+++ b/worlds/alttp/Dungeons.py
@@ -7,9 +7,9 @@
from Fill import fill_restrictive
from .Bosses import BossFactory, Boss
-from .Items import ItemFactory
-from .Regions import lookup_boss_drops
-from .Options import smallkey_shuffle
+from .Items import item_factory
+from .Regions import lookup_boss_drops, key_drop_data
+from .Options import small_key_shuffle
if typing.TYPE_CHECKING:
from .SubClasses import ALttPLocation, ALttPItem
@@ -66,7 +66,7 @@ def create_dungeons(world: "ALTTPWorld"):
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
dungeon = Dungeon(name, dungeon_regions, big_key,
- [] if multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal else small_keys,
+ [] if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal else small_keys,
dungeon_items, player)
for item in dungeon.all_items:
item.dungeon = dungeon
@@ -81,85 +81,90 @@ def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dunge
return dungeon
ES = make_dungeon('Hyrule Castle', None, ['Hyrule Castle', 'Sewers', 'Sewer Drop', 'Sewers (Dark)', 'Sanctuary'],
- None, [ItemFactory('Small Key (Hyrule Castle)', player)],
- [ItemFactory('Map (Hyrule Castle)', player)])
+ item_factory('Big Key (Hyrule Castle)', world),
+ item_factory(['Small Key (Hyrule Castle)'] * 4, world),
+ [item_factory('Map (Hyrule Castle)', world)])
EP = make_dungeon('Eastern Palace', 'Armos Knights', ['Eastern Palace'],
- ItemFactory('Big Key (Eastern Palace)', player), [],
- ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player))
+ item_factory('Big Key (Eastern Palace)', world),
+ item_factory(['Small Key (Eastern Palace)'] * 2, world),
+ item_factory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], world))
DP = make_dungeon('Desert Palace', 'Lanmolas',
['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)',
- 'Desert Palace East'], ItemFactory('Big Key (Desert Palace)', player),
- [ItemFactory('Small Key (Desert Palace)', player)],
- ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player))
+ 'Desert Palace East'], item_factory('Big Key (Desert Palace)', world),
+ item_factory(['Small Key (Desert Palace)'] * 4, world),
+ item_factory(['Map (Desert Palace)', 'Compass (Desert Palace)'], world))
ToH = make_dungeon('Tower of Hera', 'Moldorm',
['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'],
- ItemFactory('Big Key (Tower of Hera)', player),
- [ItemFactory('Small Key (Tower of Hera)', player)],
- ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], player))
+ item_factory('Big Key (Tower of Hera)', world),
+ [item_factory('Small Key (Tower of Hera)', world)],
+ item_factory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], world))
PoD = make_dungeon('Palace of Darkness', 'Helmasaur King',
['Palace of Darkness (Entrance)', 'Palace of Darkness (Center)',
'Palace of Darkness (Big Key Chest)', 'Palace of Darkness (Bonk Section)',
'Palace of Darkness (North)', 'Palace of Darkness (Maze)',
'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness (Final Section)'],
- ItemFactory('Big Key (Palace of Darkness)', player),
- ItemFactory(['Small Key (Palace of Darkness)'] * 6, player),
- ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], player))
+ item_factory('Big Key (Palace of Darkness)', world),
+ item_factory(['Small Key (Palace of Darkness)'] * 6, world),
+ item_factory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], world))
TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'],
- ItemFactory('Big Key (Thieves Town)', player), [ItemFactory('Small Key (Thieves Town)', player)],
- ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)'], player))
+ item_factory('Big Key (Thieves Town)', world),
+ item_factory(['Small Key (Thieves Town)'] * 3, world),
+ item_factory(['Map (Thieves Town)', 'Compass (Thieves Town)'], world))
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section',
'Skull Woods Second Section', 'Skull Woods Second Section (Drop)',
'Skull Woods Final Section (Mothula)',
'Skull Woods First Section (Right)',
'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'],
- ItemFactory('Big Key (Skull Woods)', player),
- ItemFactory(['Small Key (Skull Woods)'] * 3, player),
- ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player))
+ item_factory('Big Key (Skull Woods)', world),
+ item_factory(['Small Key (Skull Woods)'] * 5, world),
+ item_factory(['Map (Skull Woods)', 'Compass (Skull Woods)'], world))
SP = make_dungeon('Swamp Palace', 'Arrghus',
['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)',
- 'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)', player),
- [ItemFactory('Small Key (Swamp Palace)', player)],
- ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], player))
+ 'Swamp Palace (West)', 'Swamp Palace (Center)', 'Swamp Palace (North)'],
+ item_factory('Big Key (Swamp Palace)', world),
+ item_factory(['Small Key (Swamp Palace)'] * 6, world),
+ item_factory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], world))
IP = make_dungeon('Ice Palace', 'Kholdstare',
- ['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)',
- 'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player),
- ItemFactory(['Small Key (Ice Palace)'] * 2, player),
- ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player))
+ ['Ice Palace (Entrance)', 'Ice Palace (Second Section)', 'Ice Palace (Main)', 'Ice Palace (East)',
+ 'Ice Palace (East Top)', 'Ice Palace (Kholdstare)'], item_factory('Big Key (Ice Palace)', world),
+ item_factory(['Small Key (Ice Palace)'] * 6, world),
+ item_factory(['Map (Ice Palace)', 'Compass (Ice Palace)'], world))
MM = make_dungeon('Misery Mire', 'Vitreous',
['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)',
- 'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)', player),
- ItemFactory(['Small Key (Misery Mire)'] * 3, player),
- ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
+ 'Misery Mire (Vitreous)'], item_factory('Big Key (Misery Mire)', world),
+ item_factory(['Small Key (Misery Mire)'] * 6, world),
+ item_factory(['Map (Misery Mire)', 'Compass (Misery Mire)'], world))
TR = make_dungeon('Turtle Rock', 'Trinexx',
['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)',
+ 'Turtle Rock (Pokey Room)',
'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)',
'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'],
- ItemFactory('Big Key (Turtle Rock)', player),
- ItemFactory(['Small Key (Turtle Rock)'] * 4, player),
- ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
+ item_factory('Big Key (Turtle Rock)', world),
+ item_factory(['Small Key (Turtle Rock)'] * 6, world),
+ item_factory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], world))
if multiworld.mode[player] != 'inverted':
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None,
- ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
+ item_factory(['Small Key (Agahnims Tower)'] * 4, world), [])
GT = make_dungeon('Ganons Tower', 'Agahnim2',
['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)',
'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)',
'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)',
'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'],
- ItemFactory('Big Key (Ganons Tower)', player),
- ItemFactory(['Small Key (Ganons Tower)'] * 4, player),
- ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
+ item_factory('Big Key (Ganons Tower)', world),
+ item_factory(['Small Key (Ganons Tower)'] * 8, world),
+ item_factory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], world))
else:
AT = make_dungeon('Inverted Agahnims Tower', 'Agahnim', ['Inverted Agahnims Tower', 'Agahnim 1'], None,
- ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
+ item_factory(['Small Key (Agahnims Tower)'] * 4, world), [])
GT = make_dungeon('Inverted Ganons Tower', 'Agahnim2',
['Inverted Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)',
'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)',
'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)',
'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)',
- 'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player),
- ItemFactory(['Small Key (Ganons Tower)'] * 4, player),
- ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
+ 'Agahnim 2'], item_factory('Big Key (Ganons Tower)', world),
+ item_factory(['Small Key (Ganons Tower)'] * 8, world),
+ item_factory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], world))
GT.bosses['bottom'] = BossFactory('Armos Knights', player)
GT.bosses['middle'] = BossFactory('Lanmolas', player)
@@ -195,10 +200,11 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
dungeon_specific: set = set()
for subworld in multiworld.get_game_worlds("A Link to the Past"):
player = subworld.player
- localized |= {(player, item_name) for item_name in
- subworld.dungeon_local_item_names}
- dungeon_specific |= {(player, item_name) for item_name in
- subworld.dungeon_specific_item_names}
+ if player not in multiworld.groups:
+ localized |= {(player, item_name) for item_name in
+ subworld.dungeon_local_item_names}
+ dungeon_specific |= {(player, item_name) for item_name in
+ subworld.dungeon_specific_item_names}
if localized:
in_dungeon_items = [item for item in get_dungeon_item_pool(multiworld) if (item.player, item.name) in localized]
@@ -249,7 +255,17 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
if all_state_base.has("Triforce", player):
all_state_base.remove(multiworld.worlds[player].create_item("Triforce"))
- fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True)
+ for (player, key_drop_shuffle) in multiworld.key_drop_shuffle.items():
+ if not key_drop_shuffle and player not in multiworld.groups:
+ for key_loc in key_drop_data:
+ key_data = key_drop_data[key_loc]
+ all_state_base.remove(item_factory(key_data[3], multiworld.worlds[player]))
+ loc = multiworld.get_location(key_loc, player)
+
+ if loc in all_state_base.events:
+ all_state_base.events.remove(loc)
+ fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, lock=True, allow_excluded=True,
+ name="LttP Dungeon Items")
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
diff --git a/worlds/alttp/EntranceRandomizer.py b/worlds/alttp/EntranceRandomizer.py
index 47c36b6cde33..e62088c1e05c 100644
--- a/worlds/alttp/EntranceRandomizer.py
+++ b/worlds/alttp/EntranceRandomizer.py
@@ -23,170 +23,7 @@ def defval(value):
multiargs, _ = parser.parse_known_args(argv)
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
- parser.add_argument('--logic', default=defval('noglitches'), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'owglitches', 'hybridglitches', 'nologic'],
- help='''\
- Select Enforcement of Item Requirements. (default: %(default)s)
- No Glitches:
- Minor Glitches: May require Fake Flippers, Bunny Revival
- and Dark Room Navigation.
- Overworld Glitches: May require overworld glitches.
- Hybrid Major Glitches: May require both overworld and underworld clipping.
- No Logic: Distribute items without regard for
- item requirements.
- ''')
- parser.add_argument('--glitch_triforce', help='Allow glitching to Triforce from Ganon\'s room', action='store_true')
- parser.add_argument('--mode', default=defval('open'), const='open', nargs='?', choices=['standard', 'open', 'inverted'],
- help='''\
- Select game mode. (default: %(default)s)
- Open: World starts with Zelda rescued.
- Standard: Fixes Hyrule Castle Secret Entrance and Front Door
- but may lead to weird rain state issues if you exit
- through the Hyrule Castle side exits before rescuing
- Zelda in a full shuffle.
- Inverted: Starting locations are Dark Sanctuary in West Dark
- World or at Link's House, which is shuffled freely.
- Requires the moon pearl to be Link in the Light World
- instead of a bunny.
- ''')
- parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?',
- choices=['ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'],
- help='''\
- Select completion goal. (default: %(default)s)
- Ganon: Collect all crystals, beat Agahnim 2 then
- defeat Ganon.
- Crystals: Collect all crystals then defeat Ganon.
- Pedestal: Places the Triforce at the Master Sword Pedestal.
- Ganon Pedestal: Pull the Master Sword Pedestal, then defeat Ganon.
- All Dungeons: Collect all crystals, pendants, beat both
- Agahnim fights and then defeat Ganon.
- Triforce Hunt: Places 30 Triforce Pieces in the world, collect
- 20 of them to beat the game.
- Local Triforce Hunt: Places 30 Triforce Pieces in your world, collect
- 20 of them to beat the game.
- Ganon Triforce Hunt: Places 30 Triforce Pieces in the world, collect
- 20 of them, then defeat Ganon.
- Local Ganon Triforce Hunt: Places 30 Triforce Pieces in your world,
- collect 20 of them, then defeat Ganon.
- ''')
- parser.add_argument('--triforce_pieces_available', default=defval(30),
- type=lambda value: min(max(int(value), 1), 90),
- help='''Set Triforce Pieces available in item pool.''')
- parser.add_argument('--triforce_pieces_required', default=defval(20),
- type=lambda value: min(max(int(value), 1), 90),
- help='''Set Triforce Pieces required to win a Triforce Hunt''')
- parser.add_argument('--difficulty', default=defval('normal'), const='normal', nargs='?',
- choices=['easy', 'normal', 'hard', 'expert'],
- help='''\
- Select game difficulty. Affects available itempool. (default: %(default)s)
- Easy: An easier setting with some equipment duplicated and increased health.
- Normal: Normal difficulty.
- Hard: A harder setting with less equipment and reduced health.
- Expert: A harder yet setting with minimum equipment and health.
- ''')
- parser.add_argument('--item_functionality', default=defval('normal'), const='normal', nargs='?',
- choices=['easy', 'normal', 'hard', 'expert'],
- help='''\
- Select limits on item functionality to increase difficulty. (default: %(default)s)
- Easy: Easy functionality. (Medallions usable without sword)
- Normal: Normal functionality.
- Hard: Reduced functionality.
- Expert: Greatly reduced functionality.
- ''')
- parser.add_argument('--timer', default=defval('none'), const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'],
- help='''\
- Select game timer setting. Affects available itempool. (default: %(default)s)
- None: No timer.
- Display: Displays a timer but does not affect
- the itempool.
- Timed: Starts with clock at zero. Green Clocks
- subtract 4 minutes (Total: 20), Blue Clocks
- subtract 2 minutes (Total: 10), Red Clocks add
- 2 minutes (Total: 10). Winner is player with
- lowest time at the end.
- Timed OHKO: Starts clock at 10 minutes. Green Clocks add
- 5 minutes (Total: 25). As long as clock is at 0,
- Link will die in one hit.
- OHKO: Like Timed OHKO, but no clock items are present
- and the clock is permenantly at zero.
- Timed Countdown: Starts with clock at 40 minutes. Same clocks as
- Timed mode. If time runs out, you lose (but can
- still keep playing).
- ''')
- parser.add_argument('--countdown_start_time', default=defval(10), type=int,
- help='''Set amount of time, in minutes, to start with in Timed Countdown and Timed OHKO modes''')
- parser.add_argument('--red_clock_time', default=defval(-2), type=int,
- help='''Set amount of time, in minutes, to add from picking up red clocks; negative removes time instead''')
- parser.add_argument('--blue_clock_time', default=defval(2), type=int,
- help='''Set amount of time, in minutes, to add from picking up blue clocks; negative removes time instead''')
- parser.add_argument('--green_clock_time', default=defval(4), type=int,
- help='''Set amount of time, in minutes, to add from picking up green clocks; negative removes time instead''')
- parser.add_argument('--dungeon_counters', default=defval('default'), const='default', nargs='?', choices=['default', 'on', 'pickup', 'off'],
- help='''\
- Select dungeon counter display settings. (default: %(default)s)
- (Note, since timer takes up the same space on the hud as dungeon
- counters, timer settings override dungeon counter settings.)
- Default: Dungeon counters only show when the compass is
- picked up, or otherwise sent, only when compass
- shuffle is turned on.
- On: Dungeon counters are always displayed.
- Pickup: Dungeon counters are shown when the compass is
- picked up, even when compass shuffle is turned
- off.
- Off: Dungeon counters are never shown.
- ''')
- parser.add_argument('--algorithm', default=defval('balanced'), const='balanced', nargs='?',
- choices=['freshness', 'flood', 'vt25', 'vt26', 'balanced'],
- help='''\
- Select item filling algorithm. (default: %(default)s
- balanced: vt26 derivitive that aims to strike a balance between
- the overworld heavy vt25 and the dungeon heavy vt26
- algorithm.
- vt26: Shuffle items and place them in a random location
- that it is not impossible to be in. This includes
- dungeon keys and items.
- vt25: Shuffle items and place them in a random location
- that it is not impossible to be in.
- Flood: Push out items starting from Link\'s House and
- slightly biased to placing progression items with
- less restrictions.
- ''')
- parser.add_argument('--shuffle', default=defval('vanilla'), const='vanilla', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple', 'dungeonscrossed'],
- help='''\
- Select Entrance Shuffling Algorithm. (default: %(default)s)
- Full: Mix cave and dungeon entrances freely while limiting
- multi-entrance caves to one world.
- Simple: Shuffle Dungeon Entrances/Exits between each other
- and keep all 4-entrance dungeons confined to one
- location. All caves outside of death mountain are
- shuffled in pairs and matched by original type.
- Restricted: Use Dungeons shuffling from Simple but freely
- connect remaining entrances.
- Crossed: Mix cave and dungeon entrances freely while allowing
- caves to cross between worlds.
- Insanity: Decouple entrances and exits from each other and
- shuffle them freely. Caves that used to be single
- entrance will still exit to the same location from
- which they are entered.
- Vanilla: All entrances are in the same locations they were
- in the base game.
- Legacy shuffles preserve behavior from older versions of the
- entrance randomizer including significant technical limitations.
- The dungeon variants only mix up dungeons and keep the rest of
- the overworld vanilla.
- ''')
- parser.add_argument('--open_pyramid', default=defval('auto'), help='''\
- Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it.
- Depending on goal, you might still need to beat Agahnim 2 in order to beat ganon.
- fast ganon goals are crystals, ganontriforcehunt, localganontriforcehunt, pedestalganon
- auto - Only opens pyramid hole if the goal specifies a fast ganon, and entrance shuffle
- is vanilla, dungeonssimple or dungeonsfull.
- goal - Opens pyramid hole if the goal specifies a fast ganon.
- yes - Always opens the pyramid hole.
- no - Never opens the pyramid hole.
- ''', choices=['auto', 'goal', 'yes', 'no'])
-
- parser.add_argument('--loglevel', default=defval('info'), const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--count', help='''\
Use to batch generate multiple seeds with same settings.
@@ -195,16 +32,6 @@ def defval(value):
--seed given will produce the same 10 (different) roms each
time).
''', type=int)
-
- parser.add_argument('--custom', default=defval(False), help='Not supported.')
- parser.add_argument('--customitemarray', default=defval(False), help='Not supported.')
- # included for backwards compatibility
- parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=defval(True))
- parser.add_argument('--no-shuffleganon', help='''\
- If set, the Pyramid Hole and Ganon's Tower are not
- included entrance shuffle pool.
- ''', action='store_false', dest='shuffleganon')
-
parser.add_argument('--sprite', help='''\
Path to a sprite sheet to use for Link. Needs to be in
binary format and have a length of 0x7000 (28672) bytes,
@@ -212,35 +39,12 @@ def defval(value):
Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted.
''')
-
- parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos',
- "singularity"])
-
- parser.add_argument('--enemy_health', default=defval('default'),
- choices=['default', 'easy', 'normal', 'hard', 'expert'])
- parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
- parser.add_argument('--beemizer_total_chance', default=defval(0), type=lambda value: min(max(int(value), 0), 100))
- parser.add_argument('--beemizer_trap_chance', default=defval(0), type=lambda value: min(max(int(value), 0), 100))
- parser.add_argument('--shop_shuffle', default='', help='''\
- combine letters for options:
- g: generate default inventories for light and dark world shops, and unique shops
- f: generate default inventories for each shop individually
- i: shuffle the default inventories of the shops around
- p: randomize the prices of the items in shop inventories
- u: shuffle capacity upgrades into the item pool
- w: consider witch's hut like any other shop and shuffle/randomize it too
- ''')
- parser.add_argument('--shuffle_prizes', default=defval('g'), choices=['', 'g', 'b', 'gb'])
parser.add_argument('--sprite_pool', help='''\
Specifies a colon separated list of sprites used for random/randomonevent. If not specified, the full sprite pool is used.''')
- parser.add_argument('--dark_room_logic', default=('Lamp'), choices=["lamp", "torches", "none"], help='''\
- For unlit dark rooms, require the Lamp to be considered in logic by default.
- Torches means additionally easily accessible Torches that can be lit with Fire Rod are considered doable.
- None means full traversal through dark rooms without tools is considered doable.''')
parser.add_argument('--multi', default=defval(1), type=lambda value: max(int(value), 1))
parser.add_argument('--names', default=defval(''))
parser.add_argument('--outputpath')
- parser.add_argument('--game', default="A Link to the Past")
+ parser.add_argument('--game', default="Archipelago")
parser.add_argument('--race', default=defval(False), action='store_true')
parser.add_argument('--outputname')
if multiargs.multi:
@@ -249,43 +53,21 @@ def defval(value):
ret = parser.parse_args(argv)
- # shuffle medallions
-
- ret.required_medallions = ("random", "random")
# cannot be set through CLI currently
ret.plando_items = []
ret.plando_texts = {}
ret.plando_connections = []
- if ret.timer == "none":
- ret.timer = False
- if ret.dungeon_counters == 'on':
- ret.dungeon_counters = True
- elif ret.dungeon_counters == 'off':
- ret.dungeon_counters = False
-
if multiargs.multi:
defaults = copy.deepcopy(ret)
for player in range(1, multiargs.multi + 1):
playerargs = parse_arguments(shlex.split(getattr(ret, f"p{player}")), True)
- for name in ['logic', 'mode', 'goal', 'difficulty', 'item_functionality',
- 'shuffle', 'open_pyramid', 'timer',
- 'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
- 'beemizer_total_chance', 'beemizer_trap_chance',
- 'shufflebosses', 'enemy_health', 'enemy_damage',
- 'sprite',
- "triforce_pieces_available",
- "triforce_pieces_required", "shop_shuffle",
- "required_medallions",
- "plando_items", "plando_texts", "plando_connections",
- 'dungeon_counters',
- 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
- 'game']:
+ for name in ["plando_items", "plando_texts", "plando_connections", "game", "sprite", "sprite_pool"]:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1:
setattr(ret, name, {1: value})
else:
getattr(ret, name)[player] = value
- return ret
\ No newline at end of file
+ return ret
diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py
index b7fe688431b7..f759b6309a0e 100644
--- a/worlds/alttp/EntranceShuffle.py
+++ b/worlds/alttp/EntranceShuffle.py
@@ -3,6 +3,8 @@
from .OverworldGlitchRules import overworld_glitch_connections
from .UnderworldGlitchRules import underworld_glitch_connections
+from .Regions import mark_light_world_regions
+from .InvertedRegions import mark_dark_world_regions
def link_entrances(world, player):
@@ -21,17 +23,17 @@ def link_entrances(world, player):
connect_simple(world, exitname, regionname, player)
# if we do not shuffle, set default connections
- if world.shuffle[player] == 'vanilla':
+ if world.entrance_shuffle[player] == 'vanilla':
for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player)
for exitname, regionname in default_dungeon_connections:
connect_simple(world, exitname, regionname, player)
- elif world.shuffle[player] == 'dungeonssimple':
+ elif world.entrance_shuffle[player] == 'dungeons_simple':
for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player)
simple_shuffle_dungeons(world, player)
- elif world.shuffle[player] == 'dungeonsfull':
+ elif world.entrance_shuffle[player] == 'dungeons_full':
for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player)
@@ -63,9 +65,9 @@ def link_entrances(world, player):
connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player)
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
- elif world.shuffle[player] == 'dungeonscrossed':
+ elif world.entrance_shuffle[player] == 'dungeons_crossed':
crossed_shuffle_dungeons(world, player)
- elif world.shuffle[player] == 'simple':
+ elif world.entrance_shuffle[player] == 'simple':
simple_shuffle_dungeons(world, player)
old_man_entrances = list(Old_Man_Entrances)
@@ -136,7 +138,7 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, single_doors, door_targets, player)
- elif world.shuffle[player] == 'restricted':
+ elif world.entrance_shuffle[player] == 'restricted':
simple_shuffle_dungeons(world, player)
lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
@@ -207,62 +209,8 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, doors, door_targets, player)
- elif world.shuffle[player] == 'restricted_legacy':
- simple_shuffle_dungeons(world, player)
-
- lw_entrances = list(LW_Entrances)
- dw_entrances = list(DW_Entrances)
- dw_must_exits = list(DW_Entrances_Must_Exit)
- old_man_entrances = list(Old_Man_Entrances)
- caves = list(Cave_Exits)
- three_exit_caves = list(Cave_Three_Exits)
- single_doors = list(Single_Cave_Doors)
- bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors)
- blacksmith_doors = list(Blacksmith_Single_Cave_Doors)
- door_targets = list(Single_Cave_Targets)
-
- # only use two exit caves to do mandatory dw connections
- connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player)
- # add three exit doors to pool for remainder
- caves.extend(three_exit_caves)
-
- # place old man, has limited options
- # exit has to come from specific set of doors, the entrance is free to move about
- world.random.shuffle(old_man_entrances)
- old_man_exit = old_man_entrances.pop()
- lw_entrances.extend(old_man_entrances)
- world.random.shuffle(lw_entrances)
- old_man_entrance = lw_entrances.pop()
- connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
- connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player)
-
- # place Old Man House in Light World
- connect_caves(world, lw_entrances, [], Old_Man_House, player)
- # connect rest. There's 2 dw entrances remaining, so we will not run into parity issue placing caves
- connect_caves(world, lw_entrances, dw_entrances, caves, player)
-
- # scramble holes
- scramble_holes(world, player)
-
- # place blacksmith, has limited options
- world.random.shuffle(blacksmith_doors)
- blacksmith_hut = blacksmith_doors.pop()
- connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
- bomb_shop_doors.extend(blacksmith_doors)
-
- # place dam and pyramid fairy, have limited options
- world.random.shuffle(bomb_shop_doors)
- bomb_shop = bomb_shop_doors.pop()
- connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
- single_doors.extend(bomb_shop_doors)
-
- # tavern back door cannot be shuffled yet
- connect_doors(world, ['Tavern North'], ['Tavern'], player)
-
- # place remaining doors
- connect_doors(world, single_doors, door_targets, player)
- elif world.shuffle[player] == 'full':
+ elif world.entrance_shuffle[player] == 'full':
skull_woods_shuffle(world, player)
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
@@ -368,7 +316,7 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, doors, door_targets, player)
- elif world.shuffle[player] == 'crossed':
+ elif world.entrance_shuffle[player] == 'crossed':
skull_woods_shuffle(world, player)
entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors)
@@ -445,337 +393,8 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, entrances, door_targets, player)
- elif world.shuffle[player] == 'full_legacy':
- skull_woods_shuffle(world, player)
-
- lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances)
- dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances)
- dw_must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit)
- lw_must_exits = list(LW_Dungeon_Entrances_Must_Exit)
- old_man_entrances = list(Old_Man_Entrances + ['Tower of Hera'])
- caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) # don't need to consider three exit caves, have one exit caves to avoid parity issues
- single_doors = list(Single_Cave_Doors)
- bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors)
- blacksmith_doors = list(Blacksmith_Single_Cave_Doors)
- door_targets = list(Single_Cave_Targets)
-
- if world.mode[player] == 'standard':
- # must connect front of hyrule castle to do escape
- connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
- else:
- caves.append(tuple(world.random.sample(
- ['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3)))
- lw_entrances.append('Hyrule Castle Entrance (South)')
-
- if not world.shuffle_ganon:
- connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
- else:
- dw_entrances.append('Ganons Tower')
- caves.append('Ganons Tower Exit')
-
- # we randomize which world requirements we fulfill first so we get better dungeon distribution
- if world.random.randint(0, 1) == 0:
- connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player)
- connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player)
- else:
- connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player)
- connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player)
- if world.mode[player] == 'standard':
- # rest of hyrule castle must be in light world
- connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
-
- # place old man, has limited options
- # exit has to come from specific set of doors, the entrance is free to move about
- old_man_entrances = [door for door in old_man_entrances if door in lw_entrances]
- world.random.shuffle(old_man_entrances)
- old_man_exit = old_man_entrances.pop()
- lw_entrances.remove(old_man_exit)
-
- world.random.shuffle(lw_entrances)
- old_man_entrance = lw_entrances.pop()
- connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
- connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player)
-
- # place Old Man House in Light World
- connect_caves(world, lw_entrances, [], list(Old_Man_House), player) #need this to avoid badness with multiple seeds
-
- # now scramble the rest
- connect_caves(world, lw_entrances, dw_entrances, caves, player)
-
- # scramble holes
- scramble_holes(world, player)
-
- # place blacksmith, has limited options
- world.random.shuffle(blacksmith_doors)
- blacksmith_hut = blacksmith_doors.pop()
- connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
- bomb_shop_doors.extend(blacksmith_doors)
-
- # place bomb shop, has limited options
- world.random.shuffle(bomb_shop_doors)
- bomb_shop = bomb_shop_doors.pop()
- connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
- single_doors.extend(bomb_shop_doors)
-
- # tavern back door cannot be shuffled yet
- connect_doors(world, ['Tavern North'], ['Tavern'], player)
-
- # place remaining doors
- connect_doors(world, single_doors, door_targets, player)
- elif world.shuffle[player] == 'madness_legacy':
- # here lie dragons, connections are no longer two way
- lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances)
- dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances)
- dw_entrances_must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit)
-
- lw_doors = list(LW_Entrances + LW_Dungeon_Entrances + LW_Dungeon_Entrances_Must_Exit) + ['Kakariko Well Cave',
- 'Bat Cave Cave',
- 'North Fairy Cave',
- 'Sanctuary',
- 'Lost Woods Hideout Stump',
- 'Lumberjack Tree Cave'] + list(
- Old_Man_Entrances)
- dw_doors = list(
- DW_Entrances + DW_Dungeon_Entrances + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) + [
- 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)',
- 'Skull Woods Second Section Door (West)']
-
- world.random.shuffle(lw_doors)
- world.random.shuffle(dw_doors)
-
- dw_entrances_must_exits.append('Skull Woods Second Section Door (West)')
- dw_entrances.append('Skull Woods Second Section Door (East)')
- dw_entrances.append('Skull Woods First Section Door')
-
- lw_entrances.extend(
- ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump',
- 'Lumberjack Tree Cave'])
-
- lw_entrances_must_exits = list(LW_Dungeon_Entrances_Must_Exit)
-
- old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera']
-
- mandatory_light_world = ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)']
- mandatory_dark_world = []
- caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits)
-
- # shuffle up holes
-
- lw_hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave']
- dw_hole_entrances = ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole']
-
- hole_targets = [('Kakariko Well Exit', 'Kakariko Well (top)'),
- ('Bat Cave Exit', 'Bat Cave (right)'),
- ('North Fairy Cave Exit', 'North Fairy Cave'),
- ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'),
- ('Lumberjack Tree Exit', 'Lumberjack Tree (top)'),
- (('Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'), 'Skull Woods Second Section (Drop)')]
-
- if world.mode[player] == 'standard':
- # cannot move uncle cave
- connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
- connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
- connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
- else:
- lw_hole_entrances.append('Hyrule Castle Secret Entrance Drop')
- hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
- lw_doors.append('Hyrule Castle Secret Entrance Stairs')
- lw_entrances.append('Hyrule Castle Secret Entrance Stairs')
-
- if not world.shuffle_ganon:
- connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
- connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player)
- connect_entrance(world, 'Pyramid Hole', 'Pyramid', player)
- else:
- dw_entrances.append('Ganons Tower')
- caves.append('Ganons Tower Exit')
- dw_hole_entrances.append('Pyramid Hole')
- hole_targets.append(('Pyramid Exit', 'Pyramid'))
- dw_entrances_must_exits.append('Pyramid Entrance')
- dw_doors.extend(['Ganons Tower', 'Pyramid Entrance'])
-
- world.random.shuffle(lw_hole_entrances)
- world.random.shuffle(dw_hole_entrances)
- world.random.shuffle(hole_targets)
-
- # decide if skull woods first section should be in light or dark world
- sw_light = world.random.randint(0, 1) == 0
- if sw_light:
- sw_hole_pool = lw_hole_entrances
- mandatory_light_world.append('Skull Woods First Section Exit')
- else:
- sw_hole_pool = dw_hole_entrances
- mandatory_dark_world.append('Skull Woods First Section Exit')
- for target in ['Skull Woods First Section (Left)', 'Skull Woods First Section (Right)',
- 'Skull Woods First Section (Top)']:
- connect_entrance(world, sw_hole_pool.pop(), target, player)
-
- # sanctuary has to be in light world
- connect_entrance(world, lw_hole_entrances.pop(), 'Sewer Drop', player)
- mandatory_light_world.append('Sanctuary Exit')
-
- # fill up remaining holes
- for hole in dw_hole_entrances:
- exits, target = hole_targets.pop()
- mandatory_dark_world.append(exits)
- connect_entrance(world, hole, target, player)
-
- for hole in lw_hole_entrances:
- exits, target = hole_targets.pop()
- mandatory_light_world.append(exits)
- connect_entrance(world, hole, target, player)
-
- # hyrule castle handling
- if world.mode[player] == 'standard':
- # must connect front of hyrule castle to do escape
- connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
- connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
- mandatory_light_world.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
- else:
- lw_doors.append('Hyrule Castle Entrance (South)')
- lw_entrances.append('Hyrule Castle Entrance (South)')
- caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
-
- # now let's deal with mandatory reachable stuff
- def extract_reachable_exit(cavelist):
- world.random.shuffle(cavelist)
- candidate = None
- for cave in cavelist:
- if isinstance(cave, tuple) and len(cave) > 1:
- # special handling: TRock and Spectracle Rock cave have two entries that we should consider entrance only
- # ToDo this should be handled in a more sensible manner
- if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2:
- continue
- candidate = cave
- break
- if candidate is None:
- raise KeyError('No suitable cave.')
- cavelist.remove(candidate)
- return candidate
-
- def connect_reachable_exit(entrance, general, worldspecific, worldoors):
- # select which one is the primary option
- if world.random.randint(0, 1) == 0:
- primary = general
- secondary = worldspecific
- else:
- primary = worldspecific
- secondary = general
-
- try:
- cave = extract_reachable_exit(primary)
- except KeyError:
- cave = extract_reachable_exit(secondary)
-
- exit = cave[-1]
- cave = cave[:-1]
- connect_exit(world, exit, entrance, player)
- connect_entrance(world, worldoors.pop(), exit, player)
- # rest of cave now is forced to be in this world
- worldspecific.append(cave)
-
- # we randomize which world requirements we fulfill first so we get better dungeon distribution
- if world.random.randint(0, 1) == 0:
- for entrance in lw_entrances_must_exits:
- connect_reachable_exit(entrance, caves, mandatory_light_world, lw_doors)
- for entrance in dw_entrances_must_exits:
- connect_reachable_exit(entrance, caves, mandatory_dark_world, dw_doors)
- else:
- for entrance in dw_entrances_must_exits:
- connect_reachable_exit(entrance, caves, mandatory_dark_world, dw_doors)
- for entrance in lw_entrances_must_exits:
- connect_reachable_exit(entrance, caves, mandatory_light_world, lw_doors)
-
- # place old man, has limited options
- # exit has to come from specific set of doors, the entrance is free to move about
- old_man_entrances = [entrance for entrance in old_man_entrances if entrance in lw_entrances]
- world.random.shuffle(old_man_entrances)
- old_man_exit = old_man_entrances.pop()
- lw_entrances.remove(old_man_exit)
-
- connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player)
- connect_entrance(world, lw_doors.pop(), 'Old Man Cave Exit (East)', player)
- mandatory_light_world.append('Old Man Cave Exit (West)')
-
- # we connect up the mandatory associations we have found
- for mandatory in mandatory_light_world:
- if not isinstance(mandatory, tuple):
- mandatory = (mandatory,)
- for exit in mandatory:
- # point out somewhere
- connect_exit(world, exit, lw_entrances.pop(), player)
- # point in from somewhere
- connect_entrance(world, lw_doors.pop(), exit, player)
-
- for mandatory in mandatory_dark_world:
- if not isinstance(mandatory, tuple):
- mandatory = (mandatory,)
- for exit in mandatory:
- # point out somewhere
- connect_exit(world, exit, dw_entrances.pop(), player)
- # point in from somewhere
- connect_entrance(world, dw_doors.pop(), exit, player)
-
- # handle remaining caves
- while caves:
- # connect highest exit count caves first, prevent issue where we have 2 or 3 exits accross worlds left to fill
- cave_candidate = (None, 0)
- for i, cave in enumerate(caves):
- if isinstance(cave, str):
- cave = (cave,)
- if len(cave) > cave_candidate[1]:
- cave_candidate = (i, len(cave))
- cave = caves.pop(cave_candidate[0])
-
- place_lightworld = world.random.randint(0, 1) == 0
- if place_lightworld:
- target_doors = lw_doors
- target_entrances = lw_entrances
- else:
- target_doors = dw_doors
- target_entrances = dw_entrances
-
- if isinstance(cave, str):
- cave = (cave,)
-
- # check if we can still fit the cave into our target group
- if len(target_doors) < len(cave):
- if not place_lightworld:
- target_doors = lw_doors
- target_entrances = lw_entrances
- else:
- target_doors = dw_doors
- target_entrances = dw_entrances
-
- for exit in cave:
- connect_exit(world, exit, target_entrances.pop(), player)
- connect_entrance(world, target_doors.pop(), exit, player)
-
- # handle simple doors
-
- single_doors = list(Single_Cave_Doors)
- bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors)
- blacksmith_doors = list(Blacksmith_Single_Cave_Doors)
- door_targets = list(Single_Cave_Targets)
-
- # place blacksmith, has limited options
- world.random.shuffle(blacksmith_doors)
- blacksmith_hut = blacksmith_doors.pop()
- connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
- bomb_shop_doors.extend(blacksmith_doors)
- # place dam and pyramid fairy, have limited options
- world.random.shuffle(bomb_shop_doors)
- bomb_shop = bomb_shop_doors.pop()
- connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
- single_doors.extend(bomb_shop_doors)
-
- # tavern back door cannot be shuffled yet
- connect_doors(world, ['Tavern North'], ['Tavern'], player)
-
- # place remaining doors
- connect_doors(world, single_doors, door_targets, player)
- elif world.shuffle[player] == 'insanity':
+ elif world.entrance_shuffle[player] == 'insanity':
# beware ye who enter here
entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']
@@ -922,174 +541,33 @@ def connect_reachable_exit(entrance, caves, doors):
# place remaining doors
connect_doors(world, doors, door_targets, player)
- elif world.shuffle[player] == 'insanity_legacy':
- world.fix_fake_world[player] = False
- # beware ye who enter here
- entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']
- entrances_must_exits = DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + LW_Dungeon_Entrances_Must_Exit + ['Skull Woods Second Section Door (West)']
-
- doors = LW_Entrances + LW_Dungeon_Entrances + LW_Dungeon_Entrances_Must_Exit + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] + Old_Man_Entrances +\
- DW_Entrances + DW_Dungeon_Entrances + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)']
-
- world.random.shuffle(doors)
-
- old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera']
-
- caves = Cave_Exits + Dungeon_Exits + Cave_Three_Exits + ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)', 'Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)',
- 'Kakariko Well Exit', 'Bat Cave Exit', 'North Fairy Cave Exit', 'Lost Woods Hideout Exit', 'Lumberjack Tree Exit', 'Sanctuary Exit']
-
- # shuffle up holes
-
- hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave',
- 'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole']
-
- hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Woods Second Section (Drop)',
- 'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)']
-
- if world.mode[player] == 'standard':
- # cannot move uncle cave
- connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
- connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
- connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
- else:
- hole_entrances.append('Hyrule Castle Secret Entrance Drop')
- hole_targets.append('Hyrule Castle Secret Entrance')
- doors.append('Hyrule Castle Secret Entrance Stairs')
- entrances.append('Hyrule Castle Secret Entrance Stairs')
- caves.append('Hyrule Castle Secret Entrance Exit')
-
- if not world.shuffle_ganon:
- connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
- connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player)
- connect_entrance(world, 'Pyramid Hole', 'Pyramid', player)
- else:
- entrances.append('Ganons Tower')
- caves.extend(['Ganons Tower Exit', 'Pyramid Exit'])
- hole_entrances.append('Pyramid Hole')
- hole_targets.append('Pyramid')
- entrances_must_exits.append('Pyramid Entrance')
- doors.extend(['Ganons Tower', 'Pyramid Entrance'])
-
- world.random.shuffle(hole_entrances)
- world.random.shuffle(hole_targets)
- world.random.shuffle(entrances)
-
- # fill up holes
- for hole in hole_entrances:
- connect_entrance(world, hole, hole_targets.pop(), player)
-
- # hyrule castle handling
- if world.mode[player] == 'standard':
- # must connect front of hyrule castle to do escape
- connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
- connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
- caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
- else:
- doors.append('Hyrule Castle Entrance (South)')
- entrances.append('Hyrule Castle Entrance (South)')
- caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
-
- # now let's deal with mandatory reachable stuff
- def extract_reachable_exit(cavelist):
- world.random.shuffle(cavelist)
- candidate = None
- for cave in cavelist:
- if isinstance(cave, tuple) and len(cave) > 1:
- # special handling: TRock has two entries that we should consider entrance only
- # ToDo this should be handled in a more sensible manner
- if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2:
- continue
- candidate = cave
- break
- if candidate is None:
- raise KeyError('No suitable cave.')
- cavelist.remove(candidate)
- return candidate
-
- def connect_reachable_exit(entrance, caves, doors):
- cave = extract_reachable_exit(caves)
-
- exit = cave[-1]
- cave = cave[:-1]
- connect_exit(world, exit, entrance, player)
- connect_entrance(world, doors.pop(), exit, player)
- # rest of cave now is forced to be in this world
- caves.append(cave)
-
- # connect mandatory exits
- for entrance in entrances_must_exits:
- connect_reachable_exit(entrance, caves, doors)
-
- # place old man, has limited options
- # exit has to come from specific set of doors, the entrance is free to move about
- old_man_entrances = [entrance for entrance in old_man_entrances if entrance in entrances]
- world.random.shuffle(old_man_entrances)
- old_man_exit = old_man_entrances.pop()
- entrances.remove(old_man_exit)
-
- connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player)
- connect_entrance(world, doors.pop(), 'Old Man Cave Exit (East)', player)
- caves.append('Old Man Cave Exit (West)')
-
- # handle remaining caves
- for cave in caves:
- if isinstance(cave, str):
- cave = (cave,)
-
- for exit in cave:
- connect_exit(world, exit, entrances.pop(), player)
- connect_entrance(world, doors.pop(), exit, player)
-
- # handle simple doors
-
- single_doors = list(Single_Cave_Doors)
- bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors)
- blacksmith_doors = list(Blacksmith_Single_Cave_Doors)
- door_targets = list(Single_Cave_Targets)
-
- # place blacksmith, has limited options
- world.random.shuffle(blacksmith_doors)
- blacksmith_hut = blacksmith_doors.pop()
- connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
- bomb_shop_doors.extend(blacksmith_doors)
-
- # place dam and pyramid fairy, have limited options
- world.random.shuffle(bomb_shop_doors)
- bomb_shop = bomb_shop_doors.pop()
- connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
- single_doors.extend(bomb_shop_doors)
-
- # tavern back door cannot be shuffled yet
- connect_doors(world, ['Tavern North'], ['Tavern'], player)
-
- # place remaining doors
- connect_doors(world, single_doors, door_targets, player)
else:
raise NotImplementedError(
- f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_name(player)}')
+ f'{world.entrance_shuffle[player]} Shuffling not supported yet. Player {world.get_player_name(player)}')
- if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']:
+ if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
overworld_glitch_connections(world, player)
# mandatory hybrid major glitches connections
- if world.logic[player] in ['hybridglitches', 'nologic']:
+ if world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']:
underworld_glitch_connections(world, player)
# check for swamp palace fix
if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':
- world.swamp_patch_required[player] = True
+ world.worlds[player].swamp_patch_required = True
# check for potion shop location
if world.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop':
- world.powder_patch_required[player] = True
+ world.worlds[player].powder_patch_required = True
# check for ganon location
if world.get_entrance('Pyramid Hole', player).connected_region.name != 'Pyramid':
- world.ganon_at_pyramid[player] = False
+ world.worlds[player].ganon_at_pyramid = False
# check for Ganon's Tower location
if world.get_entrance('Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)':
- world.ganonstower_vanilla[player] = False
+ world.worlds[player].ganonstower_vanilla = False
+
def link_inverted_entrances(world, player):
# Link's house shuffled freely, Houlihan set in mandatory_connections
@@ -1106,17 +584,17 @@ def link_inverted_entrances(world, player):
connect_simple(world, exitname, regionname, player)
# if we do not shuffle, set default connections
- if world.shuffle[player] == 'vanilla':
+ if world.entrance_shuffle[player] == 'vanilla':
for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player)
for exitname, regionname in inverted_default_dungeon_connections:
connect_simple(world, exitname, regionname, player)
- elif world.shuffle[player] == 'dungeonssimple':
+ elif world.entrance_shuffle[player] == 'dungeons_simple':
for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player)
simple_shuffle_dungeons(world, player)
- elif world.shuffle[player] == 'dungeonsfull':
+ elif world.entrance_shuffle[player] == 'dungeons_full':
for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player)
@@ -1171,9 +649,9 @@ def link_inverted_entrances(world, player):
connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player)
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
- elif world.shuffle[player] == 'dungeonscrossed':
+ elif world.entrance_shuffle[player] == 'dungeons_crossed':
inverted_crossed_shuffle_dungeons(world, player)
- elif world.shuffle[player] == 'simple':
+ elif world.entrance_shuffle[player] == 'simple':
simple_shuffle_dungeons(world, player)
old_man_entrances = list(Inverted_Old_Man_Entrances)
@@ -1270,7 +748,7 @@ def link_inverted_entrances(world, player):
# place remaining doors
connect_doors(world, single_doors, door_targets, player)
- elif world.shuffle[player] == 'restricted':
+ elif world.entrance_shuffle[player] == 'restricted':
simple_shuffle_dungeons(world, player)
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Single_Cave_Doors)
@@ -1355,7 +833,7 @@ def link_inverted_entrances(world, player):
doors = lw_entrances + dw_entrances
# place remaining doors
connect_doors(world, doors, door_targets, player)
- elif world.shuffle[player] == 'full':
+ elif world.entrance_shuffle[player] == 'full':
skull_woods_shuffle(world, player)
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors)
@@ -1506,7 +984,7 @@ def link_inverted_entrances(world, player):
# place remaining doors
connect_doors(world, doors, door_targets, player)
- elif world.shuffle[player] == 'crossed':
+ elif world.entrance_shuffle[player] == 'crossed':
skull_woods_shuffle(world, player)
entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors)
@@ -1617,7 +1095,7 @@ def link_inverted_entrances(world, player):
# place remaining doors
connect_doors(world, entrances, door_targets, player)
- elif world.shuffle[player] == 'insanity':
+ elif world.entrance_shuffle[player] == 'insanity':
# beware ye who enter here
entrances = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Entrance (South)']
@@ -1776,27 +1254,27 @@ def connect_reachable_exit(entrance, caves, doors):
else:
raise NotImplementedError('Shuffling not supported yet')
- if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']:
+ if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
overworld_glitch_connections(world, player)
# mandatory hybrid major glitches connections
- if world.logic[player] in ['hybridglitches', 'nologic']:
+ if world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']:
underworld_glitch_connections(world, player)
# patch swamp drain
if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':
- world.swamp_patch_required[player] = True
+ world.worlds[player].swamp_patch_required = True
# check for potion shop location
if world.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop':
- world.powder_patch_required[player] = True
+ world.worlds[player].powder_patch_required = True
# check for ganon location
if world.get_entrance('Inverted Pyramid Hole', player).connected_region.name != 'Pyramid':
- world.ganon_at_pyramid[player] = False
+ world.worlds[player].ganon_at_pyramid = False
# check for Ganon's Tower location
if world.get_entrance('Inverted Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)':
- world.ganonstower_vanilla[player] = False
+ world.worlds[player].ganonstower_vanilla = False
def connect_simple(world, exitname, regionname, player):
@@ -1880,14 +1358,14 @@ def scramble_holes(world, player):
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed
- if world.shuffle[player] == 'crossed':
+ if world.entrance_shuffle[player] == 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
if world.shuffle_ganon:
world.random.shuffle(hole_targets)
exit, target = hole_targets.pop()
connect_two_way(world, 'Pyramid Entrance', exit, player)
connect_entrance(world, 'Pyramid Hole', target, player)
- if world.shuffle[player] != 'crossed':
+ if world.entrance_shuffle[player] != 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
world.random.shuffle(hole_targets)
@@ -1922,14 +1400,14 @@ def scramble_inverted_holes(world, player):
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed
- if world.shuffle[player] == 'crossed':
+ if world.entrance_shuffle[player] == 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
if world.shuffle_ganon:
world.random.shuffle(hole_targets)
exit, target = hole_targets.pop()
connect_two_way(world, 'Inverted Pyramid Entrance', exit, player)
connect_entrance(world, 'Inverted Pyramid Hole', target, player)
- if world.shuffle[player] != 'crossed':
+ if world.entrance_shuffle[player] != 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
world.random.shuffle(hole_targets)
@@ -1958,8 +1436,8 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
invalid_connections = Must_Exit_Invalid_Connections.copy()
invalid_cave_connections = defaultdict(set)
- if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']:
- from worlds.alttp import OverworldGlitchRules
+ if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
+ from . import OverworldGlitchRules
for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'):
invalid_connections[entrance] = set()
if entrance in must_be_exits:
@@ -2352,6 +1830,10 @@ def plando_connect(world, player: int):
func(world, connection.entrance, connection.exit, player)
except Exception as e:
raise Exception(f"Could not connect using {connection}") from e
+ if world.mode[player] != 'inverted':
+ mark_light_world_regions(world, player)
+ else:
+ mark_dark_world_regions(world, player)
LW_Dungeon_Entrances = ['Desert Palace Entrance (South)',
@@ -3038,6 +2520,7 @@ def plando_connect(world, player: int):
('Sanctuary Push Door', 'Sanctuary'),
('Sewer Drop', 'Sewers'),
('Sewers Back Door', 'Sewers (Dark)'),
+ ('Sewers Secret Room', 'Sewers Secret Room'),
('Agahnim 1', 'Agahnim 1'),
('Flute Spot 1', 'Death Mountain'),
('Death Mountain Entrance Rock', 'Death Mountain Entrance'),
@@ -3053,6 +2536,8 @@ def plando_connect(world, player: int):
('Spiral Cave Ledge Access', 'Spiral Cave Ledge'),
('Spiral Cave Ledge Drop', 'East Death Mountain (Bottom)'),
('Spiral Cave (top to bottom)', 'Spiral Cave (Bottom)'),
+ ('Hookshot Cave Bomb Wall (South)', 'Hookshot Cave (Upper)'),
+ ('Hookshot Cave Bomb Wall (North)', 'Hookshot Cave'),
('East Death Mountain (Top)', 'East Death Mountain (Top)'),
('Death Mountain (Top)', 'Death Mountain (Top)'),
('Death Mountain Drop', 'Death Mountain'),
@@ -3134,6 +2619,7 @@ def plando_connect(world, player: int):
('Swamp Palace Moat', 'Swamp Palace (First Room)'),
('Swamp Palace Small Key Door', 'Swamp Palace (Starting Area)'),
('Swamp Palace (Center)', 'Swamp Palace (Center)'),
+ ('Swamp Palace (West)', 'Swamp Palace (West)'),
('Swamp Palace (North)', 'Swamp Palace (North)'),
('Thieves Town Big Key Door', 'Thieves Town (Deep)'),
('Skull Woods Torch Room', 'Skull Woods Final Section (Mothula)'),
@@ -3148,7 +2634,8 @@ def plando_connect(world, player: int):
('Blind Fight', 'Blind Fight'),
('Desert Palace Pots (Outer)', 'Desert Palace Main (Inner)'),
('Desert Palace Pots (Inner)', 'Desert Palace Main (Outer)'),
- ('Ice Palace Entrance Room', 'Ice Palace (Main)'),
+ ('Ice Palace (Main)', 'Ice Palace (Main)'),
+ ('Ice Palace (Second Section)', 'Ice Palace (Second Section)'),
('Ice Palace (East)', 'Ice Palace (East)'),
('Ice Palace (East Top)', 'Ice Palace (East Top)'),
('Ice Palace (Kholdstare)', 'Ice Palace (Kholdstare)'),
@@ -3158,9 +2645,11 @@ def plando_connect(world, player: int):
('Misery Mire (Vitreous)', 'Misery Mire (Vitreous)'),
('Turtle Rock Entrance Gap', 'Turtle Rock (First Section)'),
('Turtle Rock Entrance Gap Reverse', 'Turtle Rock (Entrance)'),
- ('Turtle Rock Pokey Room', 'Turtle Rock (Chain Chomp Room)'),
+ ('Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room)'),
+ ('Turtle Rock (Pokey Room) (South)', 'Turtle Rock (First Section)'),
+ ('Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Chain Chomp Room)'),
('Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Second Section)'),
- ('Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock (First Section)'),
+ ('Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock (Pokey Room)'),
('Turtle Rock Chain Chomp Staircase', 'Turtle Rock (Chain Chomp Room)'),
('Turtle Rock (Big Chest) (North)', 'Turtle Rock (Second Section)'),
('Turtle Rock Big Key Door', 'Turtle Rock (Crystaroller Room)'),
@@ -3169,6 +2658,10 @@ def plando_connect(world, player: int):
('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Crystaroller Room)'),
('Turtle Rock (Dark Room) (South)', 'Turtle Rock (Eye Bridge)'),
('Turtle Rock Dark Room (South)', 'Turtle Rock (Dark Room)'),
+ ('Turtle Rock Second Section Bomb Wall', 'Turtle Rock (Second Section Bomb Wall)'),
+ ('Turtle Rock Second Section from Bomb Wall', 'Turtle Rock (Second Section)'),
+ ('Turtle Rock Eye Bridge Bomb Wall', 'Turtle Rock (Eye Bridge Bomb Wall)'),
+ ('Turtle Rock Eye Bridge from Bomb Wall', 'Turtle Rock (Eye Bridge)'),
('Turtle Rock (Trinexx)', 'Turtle Rock (Trinexx)'),
('Palace of Darkness Bridge Room', 'Palace of Darkness (Center)'),
('Palace of Darkness Bonk Wall', 'Palace of Darkness (Bonk Section)'),
@@ -3223,6 +2716,7 @@ def plando_connect(world, player: int):
('Sanctuary Push Door', 'Sanctuary'),
('Sewer Drop', 'Sewers'),
('Sewers Back Door', 'Sewers (Dark)'),
+ ('Sewers Secret Room', 'Sewers Secret Room'),
('Agahnim 1', 'Agahnim 1'),
('Death Mountain Entrance Rock', 'Death Mountain Entrance'),
('Death Mountain Entrance Drop', 'Light World'),
@@ -3237,6 +2731,8 @@ def plando_connect(world, player: int):
('Spiral Cave Ledge Access', 'Spiral Cave Ledge'),
('Spiral Cave Ledge Drop', 'East Death Mountain (Bottom)'),
('Spiral Cave (top to bottom)', 'Spiral Cave (Bottom)'),
+ ('Hookshot Cave Bomb Wall (South)', 'Hookshot Cave (Upper)'),
+ ('Hookshot Cave Bomb Wall (North)', 'Hookshot Cave'),
('East Death Mountain (Top)', 'East Death Mountain (Top)'),
('Death Mountain (Top)', 'Death Mountain (Top)'),
('Death Mountain Drop', 'Death Mountain'),
@@ -3285,6 +2781,7 @@ def plando_connect(world, player: int):
('Swamp Palace Moat', 'Swamp Palace (First Room)'),
('Swamp Palace Small Key Door', 'Swamp Palace (Starting Area)'),
('Swamp Palace (Center)', 'Swamp Palace (Center)'),
+ ('Swamp Palace (West)', 'Swamp Palace (West)'),
('Swamp Palace (North)', 'Swamp Palace (North)'),
('Thieves Town Big Key Door', 'Thieves Town (Deep)'),
('Skull Woods Torch Room', 'Skull Woods Final Section (Mothula)'),
@@ -3299,7 +2796,8 @@ def plando_connect(world, player: int):
('Blind Fight', 'Blind Fight'),
('Desert Palace Pots (Outer)', 'Desert Palace Main (Inner)'),
('Desert Palace Pots (Inner)', 'Desert Palace Main (Outer)'),
- ('Ice Palace Entrance Room', 'Ice Palace (Main)'),
+ ('Ice Palace (Main)', 'Ice Palace (Main)'),
+ ('Ice Palace (Second Section)', 'Ice Palace (Second Section)'),
('Ice Palace (East)', 'Ice Palace (East)'),
('Ice Palace (East Top)', 'Ice Palace (East Top)'),
('Ice Palace (Kholdstare)', 'Ice Palace (Kholdstare)'),
@@ -3309,9 +2807,11 @@ def plando_connect(world, player: int):
('Misery Mire (Vitreous)', 'Misery Mire (Vitreous)'),
('Turtle Rock Entrance Gap', 'Turtle Rock (First Section)'),
('Turtle Rock Entrance Gap Reverse', 'Turtle Rock (Entrance)'),
- ('Turtle Rock Pokey Room', 'Turtle Rock (Chain Chomp Room)'),
+ ('Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room)'),
+ ('Turtle Rock (Pokey Room) (South)', 'Turtle Rock (First Section)'),
+ ('Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Chain Chomp Room)'),
('Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Second Section)'),
- ('Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock (First Section)'),
+ ('Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock (Pokey Room)'),
('Turtle Rock Chain Chomp Staircase', 'Turtle Rock (Chain Chomp Room)'),
('Turtle Rock (Big Chest) (North)', 'Turtle Rock (Second Section)'),
('Turtle Rock Big Key Door', 'Turtle Rock (Crystaroller Room)'),
@@ -3320,6 +2820,10 @@ def plando_connect(world, player: int):
('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Crystaroller Room)'),
('Turtle Rock (Dark Room) (South)', 'Turtle Rock (Eye Bridge)'),
('Turtle Rock Dark Room (South)', 'Turtle Rock (Dark Room)'),
+ ('Turtle Rock Second Section Bomb Wall', 'Turtle Rock (Second Section Bomb Wall)'),
+ ('Turtle Rock Second Section from Bomb Wall', 'Turtle Rock (Second Section)'),
+ ('Turtle Rock Eye Bridge Bomb Wall', 'Turtle Rock (Eye Bridge Bomb Wall)'),
+ ('Turtle Rock Eye Bridge from Bomb Wall', 'Turtle Rock (Eye Bridge)'),
('Turtle Rock (Trinexx)', 'Turtle Rock (Trinexx)'),
('Palace of Darkness Bridge Room', 'Palace of Darkness (Center)'),
('Palace of Darkness Bonk Wall', 'Palace of Darkness (Bonk Section)'),
@@ -3564,7 +3068,7 @@ def plando_connect(world, player: int):
('Superbunny Cave Exit (Bottom)', 'Dark Death Mountain (East Bottom)'),
('Hookshot Cave Exit (South)', 'Dark Death Mountain (Top)'),
('Hookshot Cave Exit (North)', 'Death Mountain Floating Island (Dark World)'),
- ('Hookshot Cave Back Entrance', 'Hookshot Cave'),
+ ('Hookshot Cave Back Entrance', 'Hookshot Cave (Upper)'),
('Mimic Cave', 'Mimic Cave'),
('Pyramid Hole', 'Pyramid'),
@@ -3695,7 +3199,7 @@ def plando_connect(world, player: int):
('Superbunny Cave (Bottom)', 'Superbunny Cave (Bottom)'),
('Superbunny Cave Exit (Bottom)', 'Dark Death Mountain (East Bottom)'),
('Hookshot Cave Exit (North)', 'Death Mountain Floating Island (Dark World)'),
- ('Hookshot Cave Back Entrance', 'Hookshot Cave'),
+ ('Hookshot Cave Back Entrance', 'Hookshot Cave (Upper)'),
('Mimic Cave', 'Mimic Cave'),
('Inverted Pyramid Hole', 'Pyramid'),
('Inverted Links House', 'Inverted Links House'),
diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py
index acec73bf33be..63a2d499e2d4 100644
--- a/worlds/alttp/InvertedRegions.py
+++ b/worlds/alttp/InvertedRegions.py
@@ -133,7 +133,7 @@ def create_inverted_regions(world, player):
create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'),
create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']),
- create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies'),
+ create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
create_cave_region(world, player, 'Two Brothers House', 'a connector', None,
['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'],
@@ -149,41 +149,38 @@ def create_inverted_regions(world, player):
create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None,
['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks',
'Desert Palace North Mirror Spot']),
- create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace',
- ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
- ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)',
- 'Desert Palace East Wing']),
- create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None,
- ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
- create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace',
- ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
+ create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
+ ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
+ create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
+ create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace',
- ['Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
+ ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key',
+ 'Desert Palace - Desert Tiles 2 Pot Key',
+ 'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace',
['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
'Eastern Palace - Cannonball Chest',
- 'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss',
- 'Eastern Palace - Prize'], ['Eastern Palace Exit']),
+ 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop',
+ 'Eastern Palace - Big Key Chest',
+ 'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'],
+ ['Eastern Palace Exit']),
create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'),
- create_lw_region(world, player, 'Hyrule Castle Ledge', None,
- ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower',
- 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']),
+ create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']),
create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle',
['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
- 'Hyrule Castle - Zelda\'s Chest'],
+ 'Hyrule Castle - Zelda\'s Chest',
+ 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
+ 'Hyrule Castle - Big Key Drop'],
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)',
'Throne Room']),
create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
- create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross'],
- ['Sewers Door']),
- create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit',
- ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
- 'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']),
+ create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
+ create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
+ create_dungeon_region(world, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
+ 'Sewers - Secret Room - Right']),
create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
- create_dungeon_region(world, player, 'Inverted Agahnims Tower', 'Castle Tower',
- ['Castle Tower - Room 03', 'Castle Tower - Dark Maze'],
- ['Agahnim 1', 'Inverted Agahnims Tower Exit']),
+ create_dungeon_region(world, player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']),
create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'],
['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
@@ -253,14 +250,9 @@ def create_inverted_regions(world, player):
'Death Mountain (Top) Mirror Spot']),
create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
['Bumper Cave Ledge Drop', 'Bumper Cave (Top)']),
- create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera',
- ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'],
- ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
- create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera',
- ['Tower of Hera - Big Key Chest']),
- create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera',
- ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss',
- 'Tower of Hera - Prize']),
+ create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
+ create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']),
+ create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']),
create_dw_region(world, player, 'East Dark World', ['Pyramid'],
['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness',
@@ -355,133 +347,91 @@ def create_inverted_regions(world, player):
create_cave_region(world, player, 'Hookshot Cave', 'a connector',
['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right',
'Hookshot Cave - Bottom Left'],
- ['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']),
+ ['Hookshot Cave Exit (South)', 'Hookshot Cave Bomb Wall (South)']),
+ create_cave_region(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
+ 'Hookshot Cave Bomb Wall (North)']),
create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None,
['Floating Island Drop', 'Hookshot Cave Back Entrance']),
create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
- create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None,
- ['Swamp Palace Moat', 'Swamp Palace Exit']),
- create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'],
- ['Swamp Palace Small Key Door']),
- create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace',
- ['Swamp Palace - Map Chest'], ['Swamp Palace (Center)']),
- create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace',
- ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest',
- 'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest'], ['Swamp Palace (North)']),
- create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace',
- ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
- 'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']),
- create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town',
- ['Thieves\' Town - Big Key Chest',
- 'Thieves\' Town - Map Chest',
- 'Thieves\' Town - Compass Chest',
- 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
+ create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
+ create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
+ create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
+ 'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']),
+ create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
+ 'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']),
+ create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
+ create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
+ 'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room',
+ 'Swamp Palace - Boss', 'Swamp Palace - Prize']),
+ create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
+ 'Thieves\' Town - Map Chest',
+ 'Thieves\' Town - Compass Chest',
+ 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
- 'Thieves\' Town - Big Chest',
- 'Thieves\' Town - Blind\'s Cell'],
+ 'Thieves\' Town - Big Chest',
+ 'Thieves\' Town - Hallway Pot Key',
+ 'Thieves\' Town - Spike Switch Pot Key',
+ 'Thieves\' Town - Blind\'s Cell'],
['Blind Fight']),
- create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town',
- ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
- create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'],
- ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump',
- 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods',
- ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods',
- ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'],
- ['Skull Woods First Section (Left) Door to Exit',
- 'Skull Woods First Section (Left) Door to Right']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods',
- ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
- create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None,
- ['Skull Woods Second Section (Drop)']),
- create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods',
- ['Skull Woods - Big Key Chest'],
- ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
- create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods',
- ['Skull Woods - Bridge Room'],
- ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
- create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods',
- ['Skull Woods - Boss', 'Skull Woods - Prize']),
- create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', None,
- ['Ice Palace Entrance Room', 'Ice Palace Exit']),
- create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace',
- ['Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest',
- 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'],
- ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
- create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'],
- ['Ice Palace (East Top)']),
- create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace',
- ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest']),
- create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace',
- ['Ice Palace - Boss', 'Ice Palace - Prize']),
- create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None,
- ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
- create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire',
- ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
- 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'],
- ['Misery Mire (West)', 'Misery Mire Big Key Door']),
- create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire',
- ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
- create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None,
- ['Misery Mire (Vitreous)']),
- create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire',
- ['Misery Mire - Boss', 'Misery Mire - Prize']),
- create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None,
- ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
- create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock',
- ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
- 'Turtle Rock - Roller Room - Right'],
- ['Turtle Rock Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
- create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock',
- ['Turtle Rock - Chain Chomps'],
+ create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
+ create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
+ create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
+ create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
+ create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
+ create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
+ create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
+ create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
+ create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
+ create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
+ create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
+ create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
+ 'Ice Palace - Many Pots Pot Key',
+ 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
+ create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
+ create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
+ create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
+ create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
+ create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
+ 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
+ 'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key',
+ 'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']),
+ create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
+ create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
+ create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
+ create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
+ create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
+ 'Turtle Rock - Roller Room - Right'],
+ ['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
+ create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
+ create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'],
['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
- ['Turtle Rock - Big Key Chest'],
- ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase',
- 'Turtle Rock Big Key Door']),
- create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'],
- ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
- create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock',
- ['Turtle Rock - Crystaroller Room'],
- ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
- create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None,
- ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
- create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock',
- ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
- 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
- ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)',
- 'Turtle Rock Isolated Ledge Exit']),
- create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock',
- ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
- create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness',
- ['Palace of Darkness - Shooter Room'],
- ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall',
- 'Palace of Darkness Exit']),
- create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness',
- ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
- ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)',
- 'Palace of Darkness Big Key Door']),
- create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness',
- ['Palace of Darkness - Big Key Chest']),
- create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness',
- ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'],
- ['Palace of Darkness Hammer Peg Drop']),
- create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness',
- ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left',
- 'Palace of Darkness - Dark Basement - Right'],
+ ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'],
+ ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door',
+ 'Turtle Rock Second Section Bomb Wall']),
+ create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
+ create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
+ create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
+ create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
+ create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
+ create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
+ 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
+ ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
+ create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
+ ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
+ create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
- create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness',
- ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom',
- 'Palace of Darkness - Big Chest']),
- create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness',
- ['Palace of Darkness - Harmless Hellway']),
- create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness',
- ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
create_dungeon_region(world, player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower',
['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
- 'Ganons Tower - Hope Room - Right'],
+ 'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'],
['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door',
'Inverted Ganons Tower Exit']),
create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'],
@@ -489,10 +439,13 @@ def create_inverted_regions(world, player):
create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower',
['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
'Ganons Tower - Compass Room - Bottom Left',
- 'Ganons Tower - Compass Room - Bottom Right'], ['Ganons Tower (Bottom) (East)']),
+ 'Ganons Tower - Compass Room - Bottom Right',
+ 'Ganons Tower - Conveyor Star Pits Pot Key'],
+ ['Ganons Tower (Bottom) (East)']),
create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower',
['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
- 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'],
+ 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
+ 'Ganons Tower - Double Switch Pot Key'],
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower',
@@ -501,21 +454,21 @@ def create_inverted_regions(world, player):
['Ganons Tower - Randomizer Room - Top Left',
'Ganons Tower - Randomizer Room - Top Right',
'Ganons Tower - Randomizer Room - Bottom Left',
- 'Ganons Tower - Randomizer Room - Bottom Right'], ['Ganons Tower (Bottom) (West)']),
+ 'Ganons Tower - Randomizer Room - Bottom Right'],
+ ['Ganons Tower (Bottom) (West)']),
create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower',
['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest',
'Ganons Tower - Big Key Room - Left',
'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
- create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None,
- ['Ganons Tower Torch Rooms']),
+ create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower',
['Ganons Tower - Mini Helmasaur Room - Left',
'Ganons Tower - Mini Helmasaur Room - Right',
- 'Ganons Tower - Pre-Moldorm Chest'], ['Ganons Tower Moldorm Door']),
- create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None,
- ['Ganons Tower Moldorm Gap']),
- create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower',
- ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
+ 'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'],
+ ['Ganons Tower Moldorm Door']),
+ create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
+
+ create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted
@@ -529,8 +482,6 @@ def create_inverted_regions(world, player):
create_lw_region(world, player, 'Death Mountain Bunny Descent Area')
]
- world.initialize_regions()
-
def mark_dark_world_regions(world, player):
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py
index 56eb355837d4..125475561bb2 100644
--- a/worlds/alttp/ItemPool.py
+++ b/worlds/alttp/ItemPool.py
@@ -5,13 +5,14 @@
from Fill import FillError
from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType
-from .Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops, create_dynamic_shop_locations
+from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType
from .Bosses import place_bosses
from .Dungeons import get_dungeon_item_pool_player
from .EntranceShuffle import connect_entrance
-from .Items import ItemFactory, GetBeemizerItem
-from .Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle, LTTPBosses
+from .Items import item_factory, GetBeemizerItem, trap_replaceable, item_name_groups
+from .Options import small_key_shuffle, compass_shuffle, big_key_shuffle, map_shuffle, TriforcePiecesMode, LTTPBosses
from .StateHelpers import has_triforce_pieces, has_melee_weapon
+from .Regions import key_drop_data
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
@@ -80,7 +81,7 @@
basicglove=basicgloves,
alwaysitems=alwaysitems,
legacyinsanity=legacyinsanity,
- universal_keys=['Small Key (Universal)'] * 28,
+ universal_keys=['Small Key (Universal)'] * 29,
extras=[easyfirst15extra, easysecond15extra, easythird10extra, easyfourth5extra, easyfinal25extra],
progressive_sword_limit=8,
progressive_shield_limit=6,
@@ -112,7 +113,7 @@
basicglove=basicgloves,
alwaysitems=alwaysitems,
legacyinsanity=legacyinsanity,
- universal_keys=['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10,
+ universal_keys=['Small Key (Universal)'] * 19 + ['Rupees (20)'] * 10,
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit=4,
progressive_shield_limit=3,
@@ -144,7 +145,7 @@
basicglove=basicgloves,
alwaysitems=alwaysitems,
legacyinsanity=legacyinsanity,
- universal_keys=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16,
+ universal_keys=['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 16,
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit=3,
progressive_shield_limit=2,
@@ -176,7 +177,7 @@
basicglove=basicgloves,
alwaysitems=alwaysitems,
legacyinsanity=legacyinsanity,
- universal_keys=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16,
+ universal_keys=['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 16,
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit=2,
progressive_shield_limit=1,
@@ -188,118 +189,72 @@
),
}
-ice_rod_hunt_difficulties = dict()
-for diff in {'easy', 'normal', 'hard', 'expert'}:
- ice_rod_hunt_difficulties[diff] = Difficulty(
- baseitems=['Nothing'] * 41,
- bottles=['Nothing'] * 4,
- bottle_count=difficulties[diff].bottle_count,
- same_bottle=difficulties[diff].same_bottle,
- progressiveshield=['Nothing'] * 3,
- basicshield=['Nothing'] * 3,
- progressivearmor=['Nothing'] * 2,
- basicarmor=['Nothing'] * 2,
- swordless=['Nothing'] * 4,
- progressivemagic=['Nothing'] * 2,
- basicmagic=['Nothing'] * 2,
- progressivesword=['Nothing'] * 4,
- basicsword=['Nothing'] * 4,
- progressivebow=['Nothing'] * 2,
- basicbow=['Nothing'] * 2,
- timedohko=difficulties[diff].timedohko,
- timedother=difficulties[diff].timedother,
- progressiveglove=['Nothing'] * 2,
- basicglove=['Nothing'] * 2,
- alwaysitems=['Ice Rod'] + ['Nothing'] * 19,
- legacyinsanity=['Nothing'] * 2,
- universal_keys=['Nothing'] * 28,
- extras=[['Nothing'] * 15, ['Nothing'] * 15, ['Nothing'] * 10, ['Nothing'] * 5, ['Nothing'] * 25],
- progressive_sword_limit=difficulties[diff].progressive_sword_limit,
- progressive_shield_limit=difficulties[diff].progressive_shield_limit,
- progressive_armor_limit=difficulties[diff].progressive_armor_limit,
- progressive_bow_limit=difficulties[diff].progressive_bow_limit,
- progressive_bottle_limit=difficulties[diff].progressive_bottle_limit,
- boss_heart_container_limit=difficulties[diff].boss_heart_container_limit,
- heart_piece_limit=difficulties[diff].heart_piece_limit,
- )
+
+items_reduction_table = (
+ ("Piece of Heart", "Boss Heart Container", 4, 1),
+ # the order of the upgrades is important
+ ("Arrow Upgrade (+5)", "Arrow Upgrade (+10)", 8, 4),
+ ("Arrow Upgrade (+5)", "Arrow Upgrade (+10)", 7, 4),
+ ("Arrow Upgrade (+5)", "Arrow Upgrade (+10)", 6, 3),
+ ("Arrow Upgrade (+10)", "Arrow Upgrade (70)", 4, 1),
+ ("Bomb Upgrade (+5)", "Bomb Upgrade (+10)", 8, 4),
+ ("Bomb Upgrade (+5)", "Bomb Upgrade (+10)", 7, 4),
+ ("Bomb Upgrade (+5)", "Bomb Upgrade (+10)", 6, 3),
+ ("Bomb Upgrade (+10)", "Bomb Upgrade (50)", 5, 1),
+ ("Bomb Upgrade (+10)", "Bomb Upgrade (50)", 4, 1),
+ ("Progressive Sword", 4),
+ ("Fighter Sword", 1),
+ ("Master Sword", 1),
+ ("Tempered Sword", 1),
+ ("Golden Sword", 1),
+ ("Progressive Shield", 3),
+ ("Blue Shield", 1),
+ ("Red Shield", 1),
+ ("Mirror Shield", 1),
+ ("Progressive Mail", 2),
+ ("Blue Mail", 1),
+ ("Red Mail", 1),
+ ("Progressive Bow", 2),
+ ("Bow", 1),
+ ("Silver Bow", 1),
+ ("Lamp", 1),
+ ("Bottles",)
+)
def generate_itempool(world):
player = world.player
multiworld = world.multiworld
- if multiworld.difficulty[player] not in difficulties:
- raise NotImplementedError(f"Diffulty {multiworld.difficulty[player]}")
- if multiworld.goal[player] not in {'ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'icerodhunt',
- 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'}:
+ if multiworld.item_pool[player].current_key not in difficulties:
+ raise NotImplementedError(f"Diffulty {multiworld.item_pool[player]}")
+ if multiworld.goal[player] not in ('ganon', 'pedestal', 'bosses', 'triforce_hunt', 'local_triforce_hunt',
+ 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'crystals',
+ 'ganon_pedestal'):
raise NotImplementedError(f"Goal {multiworld.goal[player]} for player {player}")
- if multiworld.mode[player] not in {'open', 'standard', 'inverted'}:
+ if multiworld.mode[player] not in ('open', 'standard', 'inverted'):
raise NotImplementedError(f"Mode {multiworld.mode[player]} for player {player}")
- if multiworld.timer[player] not in {False, 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'}:
- raise NotImplementedError(f"Timer {multiworld.mode[player]} for player {player}")
+ if multiworld.timer[player] not in (False, 'display', 'timed', 'timed_ohko', 'ohko', 'timed_countdown'):
+ raise NotImplementedError(f"Timer {multiworld.timer[player]} for player {player}")
- if multiworld.timer[player] in ['ohko', 'timed-ohko']:
- multiworld.can_take_damage[player] = False
- if multiworld.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt', 'icerodhunt']:
- multiworld.push_item(multiworld.get_location('Ganon', player), ItemFactory('Nothing', player), False)
+ if multiworld.timer[player] in ['ohko', 'timed_ohko']:
+ world.can_take_damage = False
+ if multiworld.goal[player] in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']:
+ multiworld.push_item(multiworld.get_location('Ganon', player), item_factory('Nothing', world), False)
else:
- multiworld.push_item(multiworld.get_location('Ganon', player), ItemFactory('Triforce', player), False)
-
- if multiworld.goal[player] == 'icerodhunt':
- multiworld.progression_balancing[player].value = 0
- loc = multiworld.get_location('Turtle Rock - Boss', player)
- multiworld.push_item(loc, ItemFactory('Triforce Piece', player), False)
- multiworld.treasure_hunt_count[player] = 1
- if multiworld.boss_shuffle[player] != 'none':
- if isinstance(multiworld.boss_shuffle[player].value, str) and 'turtle rock-' not in multiworld.boss_shuffle[player].value:
- multiworld.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{multiworld.boss_shuffle[player].current_key}')
- elif isinstance(multiworld.boss_shuffle[player].value, int):
- multiworld.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{multiworld.boss_shuffle[player].current_key}')
- else:
- logging.warning(f'Cannot guarantee that Trinexx is the boss of Turtle Rock for player {player}')
- loc.event = True
- loc.locked = True
- itemdiff = difficulties[multiworld.difficulty[player]]
- itempool = []
- itempool.extend(itemdiff.alwaysitems)
- itempool.remove('Ice Rod')
-
- itempool.extend(['Single Arrow', 'Sanctuary Heart Container'])
- itempool.extend(['Boss Heart Container'] * itemdiff.boss_heart_container_limit)
- itempool.extend(['Piece of Heart'] * itemdiff.heart_piece_limit)
- itempool.extend(itemdiff.bottles)
- itempool.extend(itemdiff.basicbow)
- itempool.extend(itemdiff.basicarmor)
- if not multiworld.swordless[player]:
- itempool.extend(itemdiff.basicsword)
- itempool.extend(itemdiff.basicmagic)
- itempool.extend(itemdiff.basicglove)
- itempool.extend(itemdiff.basicshield)
- itempool.extend(itemdiff.legacyinsanity)
- itempool.extend(['Rupees (300)'] * 34)
- itempool.extend(['Bombs (10)'] * 5)
- itempool.extend(['Arrows (10)'] * 7)
- if multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
- itempool.extend(itemdiff.universal_keys)
- itempool.append('Small Key (Universal)')
-
- for item in itempool:
- multiworld.push_precollected(ItemFactory(item, player))
-
- if multiworld.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']:
+ multiworld.push_item(multiworld.get_location('Ganon', player), item_factory('Triforce', world), False)
+
+ if multiworld.goal[player] in ['triforce_hunt', 'local_triforce_hunt']:
region = multiworld.get_region('Light World', player)
loc = ALttPLocation(player, "Murahdahla", parent=region)
loc.access_rule = lambda state: has_triforce_pieces(state, player)
region.locations.append(loc)
- multiworld.clear_location_cache()
- multiworld.push_item(loc, ItemFactory('Triforce', player), False)
- loc.event = True
+ multiworld.push_item(loc, item_factory('Triforce', world), False)
loc.locked = True
- multiworld.get_location('Ganon', player).event = True
multiworld.get_location('Ganon', player).locked = True
event_pairs = [
('Agahnim 1', 'Beat Agahnim 1'),
@@ -309,27 +264,29 @@ def generate_itempool(world):
('Missing Smith', 'Return Smith'),
('Floodgate', 'Open Floodgate'),
('Agahnim 1', 'Beat Agahnim 1'),
- ('Flute Activation Spot', 'Activated Flute')
+ ('Flute Activation Spot', 'Activated Flute'),
+ ('Capacity Upgrade Shop', 'Capacity Upgrade Shop')
]
for location_name, event_name in event_pairs:
location = multiworld.get_location(location_name, player)
- event = ItemFactory(event_name, player)
+ event = item_factory(event_name, world)
multiworld.push_item(location, event, False)
- location.event = location.locked = True
+ location.locked = True
# set up item pool
additional_triforce_pieces = 0
+ treasure_hunt_total = 0
if multiworld.custom:
- (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count,
- treasure_hunt_icon) = make_custom_item_pool(multiworld, player)
+ pool, placed_items, precollected_items, clock_mode, treasure_hunt_required = (
+ make_custom_item_pool(multiworld, player))
multiworld.rupoor_cost = min(multiworld.customitemarray[67], 9999)
else:
- pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, \
- treasure_hunt_icon, additional_triforce_pieces = get_pool_core(multiworld, player)
+ (pool, placed_items, precollected_items, clock_mode, treasure_hunt_required, treasure_hunt_total,
+ additional_triforce_pieces) = get_pool_core(multiworld, player)
for item in precollected_items:
- multiworld.push_precollected(ItemFactory(item, player))
+ multiworld.push_precollected(item_factory(item, world))
if multiworld.mode[player] == 'standard' and not has_melee_weapon(multiworld.state, player):
if "Link's Uncle" not in placed_items:
@@ -341,22 +298,36 @@ def generate_itempool(world):
if not found_sword:
found_sword = True
possible_weapons.append(item)
- if item in ['Progressive Bow', 'Bow'] and not found_bow:
+ elif item in ['Progressive Bow', 'Bow'] and not found_bow:
found_bow = True
possible_weapons.append(item)
- if item in ['Hammer', 'Bombs (10)', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
+ elif item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
if item not in possible_weapons:
possible_weapons.append(item)
+ elif (item == 'Bombs (10)' and (not multiworld.bombless_start[player]) and item not in
+ possible_weapons):
+ possible_weapons.append(item)
+ elif (item in ['Bomb Upgrade (+10)', 'Bomb Upgrade (50)'] and multiworld.bombless_start[player] and item
+ not in possible_weapons):
+ possible_weapons.append(item)
+
starting_weapon = multiworld.random.choice(possible_weapons)
placed_items["Link's Uncle"] = starting_weapon
pool.remove(starting_weapon)
- if placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Cane of Somaria', 'Cane of Byrna'] and multiworld.enemy_health[player] not in ['default', 'easy']:
- multiworld.escape_assist[player].append('bombs')
+ if (placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Bomb Upgrade (+10)',
+ 'Bomb Upgrade (50)', 'Cane of Somaria', 'Cane of Byrna'] and multiworld.enemy_health[player] not in ['default', 'easy']):
+ if multiworld.bombless_start[player] and "Bomb Upgrade" not in placed_items["Link's Uncle"]:
+ if 'Bow' in placed_items["Link's Uncle"]:
+ multiworld.worlds[player].escape_assist.append('arrows')
+ elif 'Cane' in placed_items["Link's Uncle"]:
+ multiworld.worlds[player].escape_assist.append('magic')
+ else:
+ multiworld.worlds[player].escape_assist.append('bombs')
for (location, item) in placed_items.items():
- multiworld.get_location(location, player).place_locked_item(ItemFactory(item, player))
+ multiworld.get_location(location, player).place_locked_item(item_factory(item, world))
- items = ItemFactory(pool, player)
+ items = item_factory(pool, world)
# convert one Progressive Bow into Progressive Bow (Alt), in ID only, for ganon silvers hint text
if multiworld.worlds[player].has_progressive_bows:
for item in items:
@@ -364,89 +335,178 @@ def generate_itempool(world):
item.code = 0x65 # Progressive Bow (Alt)
break
- if clock_mode is not None:
- multiworld.clock_mode[player] = clock_mode
+ if clock_mode:
+ world.clock_mode = clock_mode
- if treasure_hunt_count is not None:
- multiworld.treasure_hunt_count[player] = treasure_hunt_count % 999
- if treasure_hunt_icon is not None:
- multiworld.treasure_hunt_icon[player] = treasure_hunt_icon
+ multiworld.worlds[player].treasure_hunt_required = treasure_hunt_required % 999
+ multiworld.worlds[player].treasure_hunt_total = treasure_hunt_total
dungeon_items = [item for item in get_dungeon_item_pool_player(world)
if item.name not in multiworld.worlds[player].dungeon_local_item_names]
- dungeon_item_replacements = difficulties[multiworld.difficulty[player]].extras[0]\
- + difficulties[multiworld.difficulty[player]].extras[1]\
- + difficulties[multiworld.difficulty[player]].extras[2]\
- + difficulties[multiworld.difficulty[player]].extras[3]\
- + difficulties[multiworld.difficulty[player]].extras[4]
+
+ for key_loc in key_drop_data:
+ key_data = key_drop_data[key_loc]
+ drop_item = item_factory(key_data[3], world)
+ if not multiworld.key_drop_shuffle[player]:
+ if drop_item in dungeon_items:
+ dungeon_items.remove(drop_item)
+ else:
+ dungeon = drop_item.name.split("(")[1].split(")")[0]
+ if multiworld.mode[player] == 'inverted':
+ if dungeon == "Agahnims Tower":
+ dungeon = "Inverted Agahnims Tower"
+ if dungeon == "Ganons Tower":
+ dungeon = "Inverted Ganons Tower"
+ if drop_item in world.dungeons[dungeon].small_keys:
+ world.dungeons[dungeon].small_keys.remove(drop_item)
+ elif world.dungeons[dungeon].big_key is not None and world.dungeons[dungeon].big_key == drop_item:
+ world.dungeons[dungeon].big_key = None
+
+ loc = multiworld.get_location(key_loc, player)
+ loc.place_locked_item(drop_item)
+ loc.address = None
+ elif "Small" in key_data[3] and multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
+ # key drop shuffle and universal keys are on. Add universal keys in place of key drop keys.
+ multiworld.itempool.append(item_factory(GetBeemizerItem(multiworld, player, 'Small Key (Universal)'), world))
+ dungeon_item_replacements = sum(difficulties[world.options.item_pool.current_key].extras, []) * 2
multiworld.random.shuffle(dungeon_item_replacements)
- if multiworld.goal[player] == 'icerodhunt':
- for item in dungeon_items:
- multiworld.itempool.append(ItemFactory(GetBeemizerItem(multiworld, player, 'Nothing'), player))
+
+ for x in range(len(dungeon_items)-1, -1, -1):
+ item = dungeon_items[x]
+ if ((multiworld.small_key_shuffle[player] == small_key_shuffle.option_start_with and item.type == 'SmallKey')
+ or (multiworld.big_key_shuffle[player] == big_key_shuffle.option_start_with and item.type == 'BigKey')
+ or (multiworld.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass')
+ or (multiworld.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')):
+ dungeon_items.pop(x)
multiworld.push_precollected(item)
+ multiworld.itempool.append(item_factory(dungeon_item_replacements.pop(), world))
+ multiworld.itempool.extend([item for item in dungeon_items])
+
+ set_up_shops(multiworld, player)
+
+ if multiworld.retro_bow[player]:
+ shop_items = 0
+ shop_locations = [location for shop_locations in (shop.region.locations for shop in multiworld.shops if
+ shop.type == ShopType.Shop and shop.region.player == player) for location in shop_locations if
+ location.shop_slot is not None]
+ for location in shop_locations:
+ if location.shop.inventory[location.shop_slot]["item"] == "Single Arrow":
+ location.place_locked_item(item_factory("Single Arrow", world))
+ else:
+ shop_items += 1
else:
- for x in range(len(dungeon_items)-1, -1, -1):
- item = dungeon_items[x]
- if ((multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_start_with and item.type == 'SmallKey')
- or (multiworld.bigkey_shuffle[player] == bigkey_shuffle.option_start_with and item.type == 'BigKey')
- or (multiworld.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass')
- or (multiworld.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')):
- dungeon_items.remove(item)
- multiworld.push_precollected(item)
- multiworld.itempool.append(ItemFactory(dungeon_item_replacements.pop(), player))
- multiworld.itempool.extend([item for item in dungeon_items])
- # logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
- # rather than making all hearts/heart pieces progression items (which slows down generation considerably)
- # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
- if multiworld.goal[player] != 'icerodhunt' and multiworld.difficulty[player] in ['easy', 'normal', 'hard'] and not (multiworld.custom and multiworld.customitemarray[30] == 0):
- next(item for item in items if item.name == 'Boss Heart Container').classification = ItemClassification.progression
- elif multiworld.goal[player] != 'icerodhunt' and multiworld.difficulty[player] in ['expert'] and not (multiworld.custom and multiworld.customitemarray[29] < 4):
- adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
- for i in range(4):
- next(adv_heart_pieces).classification = ItemClassification.progression
-
-
- progressionitems = []
- nonprogressionitems = []
- for item in items:
- if item.advancement or item.type:
- progressionitems.append(item)
+ shop_items = min(multiworld.shop_item_slots[player], 30 if multiworld.include_witch_hut[player] else 27)
+
+ if multiworld.shuffle_capacity_upgrades[player]:
+ shop_items += 2
+ chance_100 = int(multiworld.retro_bow[player]) * 0.25 + int(
+ multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal) * 0.5
+ for _ in range(shop_items):
+ if multiworld.random.random() < chance_100:
+ items.append(item_factory(GetBeemizerItem(multiworld, player, "Rupees (100)"), world))
else:
- nonprogressionitems.append(GetBeemizerItem(multiworld, item.player, item))
- multiworld.random.shuffle(nonprogressionitems)
-
- if additional_triforce_pieces:
- if additional_triforce_pieces > len(nonprogressionitems):
- raise FillError(f"Not enough non-progression items to replace with Triforce pieces found for player "
- f"{multiworld.get_player_name(player)}.")
- progressionitems += [ItemFactory("Triforce Piece", player) for _ in range(additional_triforce_pieces)]
- nonprogressionitems.sort(key=lambda item: int("Heart" in item.name)) # try to keep hearts in the pool
- nonprogressionitems = nonprogressionitems[additional_triforce_pieces:]
- multiworld.random.shuffle(nonprogressionitems)
-
- # shuffle medallions
- if multiworld.required_medallions[player][0] == "random":
- mm_medallion = multiworld.random.choice(['Ether', 'Quake', 'Bombos'])
- else:
- mm_medallion = multiworld.required_medallions[player][0]
- if multiworld.required_medallions[player][1] == "random":
- tr_medallion = multiworld.random.choice(['Ether', 'Quake', 'Bombos'])
+ items.append(item_factory(GetBeemizerItem(multiworld, player, "Rupees (50)"), world))
+
+ multiworld.random.shuffle(items)
+ pool_count = len(items)
+ new_items = ["Triforce Piece" for _ in range(additional_triforce_pieces)]
+ if multiworld.shuffle_capacity_upgrades[player] or multiworld.bombless_start[player]:
+ progressive = multiworld.progressive[player]
+ progressive = multiworld.random.choice([True, False]) if progressive == 'grouped_random' else progressive == 'on'
+ if multiworld.shuffle_capacity_upgrades[player] == "on_combined":
+ new_items.append("Bomb Upgrade (50)")
+ elif multiworld.shuffle_capacity_upgrades[player] == "on":
+ new_items += ["Bomb Upgrade (+5)"] * 6
+ new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
+ if multiworld.shuffle_capacity_upgrades[player] != "on_combined" and multiworld.bombless_start[player]:
+ new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
+
+ if multiworld.shuffle_capacity_upgrades[player] and not multiworld.retro_bow[player]:
+ if multiworld.shuffle_capacity_upgrades[player] == "on_combined":
+ new_items += ["Arrow Upgrade (70)"]
+ else:
+ new_items += ["Arrow Upgrade (+5)"] * 6
+ new_items.append("Arrow Upgrade (+5)" if progressive else "Arrow Upgrade (+10)")
+
+ items += [item_factory(item, world) for item in new_items]
+ removed_filler = []
+
+ multiworld.random.shuffle(items) # Decide what gets tossed randomly.
+
+ while len(items) > pool_count:
+ for i, item in enumerate(items):
+ if item.classification in (ItemClassification.filler, ItemClassification.trap):
+ removed_filler.append(items.pop(i))
+ break
+ else:
+ # no more junk to remove, condense progressive items
+ def condense_items(items, small_item, big_item, rem, add):
+ small_item = item_factory(small_item, world)
+ # while (len(items) >= pool_count + rem - 1 # minus 1 to account for the replacement item
+ # and items.count(small_item) >= rem):
+ if items.count(small_item) >= rem:
+ for _ in range(rem):
+ items.remove(small_item)
+ removed_filler.append(item_factory(small_item.name, world))
+ items += [item_factory(big_item, world) for _ in range(add)]
+ return True
+ return False
+
+ def cut_item(items, item_to_cut, minimum_items):
+ item_to_cut = item_factory(item_to_cut, world)
+ if items.count(item_to_cut) > minimum_items:
+ items.remove(item_to_cut)
+ removed_filler.append(item_factory(item_to_cut.name, world))
+ return True
+ return False
+
+ while len(items) > pool_count:
+ items_were_cut = False
+ for reduce_item in items_reduction_table:
+ if len(reduce_item) == 2:
+ items_were_cut = items_were_cut or cut_item(items, *reduce_item)
+ elif len(reduce_item) == 4:
+ items_were_cut = items_were_cut or condense_items(items, *reduce_item)
+ elif len(reduce_item) == 1: # Bottles
+ bottles = [item for item in items if item.name in item_name_groups["Bottles"]]
+ if len(bottles) > 4:
+ bottle = multiworld.random.choice(bottles)
+ items.remove(bottle)
+ removed_filler.append(bottle)
+ items_were_cut = True
+ if items_were_cut:
+ break
+ else:
+ raise Exception(f"Failed to limit item pool size for player {player}")
+ if len(items) < pool_count:
+ items += removed_filler[len(items) - pool_count:]
+
+ if multiworld.randomize_cost_types[player]:
+ # Heart and Arrow costs require all Heart Container/Pieces and Arrow Upgrades to be advancement items for logic
+ for item in items:
+ if (item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart")
+ or "Arrow Upgrade" in item.name):
+ item.classification = ItemClassification.progression
else:
- tr_medallion = multiworld.required_medallions[player][1]
- multiworld.required_medallions[player] = (mm_medallion, tr_medallion)
+ # Otherwise, logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
+ # rather than making all hearts/heart pieces progression items (which slows down generation considerably)
+ # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
+ if multiworld.item_pool[player] in ['easy', 'normal', 'hard'] and not (multiworld.custom and multiworld.customitemarray[30] == 0):
+ next(item for item in items if item.name == 'Boss Heart Container').classification = ItemClassification.progression
+ elif multiworld.item_pool[player] in ['expert'] and not (multiworld.custom and multiworld.customitemarray[29] < 4):
+ adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
+ for i in range(4):
+ next(adv_heart_pieces).classification = ItemClassification.progression
+
+ world.required_medallions = (multiworld.misery_mire_medallion[player].current_key.title(),
+ multiworld.turtle_rock_medallion[player].current_key.title())
place_bosses(world)
- set_up_shops(multiworld, player)
- if multiworld.shop_shuffle[player]:
- shuffle_shops(multiworld, nonprogressionitems, player)
-
- multiworld.itempool += progressionitems + nonprogressionitems
+ multiworld.itempool += items
if multiworld.retro_caves[player]:
- set_up_take_anys(multiworld, player) # depends on world.itempool to be set
- # set_up_take_anys needs to run first
- create_dynamic_shop_locations(multiworld, player)
+ set_up_take_anys(multiworld, world, player) # depends on world.itempool to be set
take_any_locations = {
@@ -466,69 +526,76 @@ def generate_itempool(world):
take_any_locations.sort()
-def set_up_take_anys(world, player):
+def set_up_take_anys(multiworld, world, player):
# these are references, do not modify these lists in-place
- if world.mode[player] == 'inverted':
+ if multiworld.mode[player] == 'inverted':
take_any_locs = take_any_locations_inverted
else:
take_any_locs = take_any_locations
- regions = world.random.sample(take_any_locs, 5)
+ regions = multiworld.random.sample(take_any_locs, 5)
- old_man_take_any = LTTPRegion("Old Man Sword Cave", LTTPRegionType.Cave, 'the sword cave', player, world)
- world.regions.append(old_man_take_any)
+ old_man_take_any = LTTPRegion("Old Man Sword Cave", LTTPRegionType.Cave, 'the sword cave', player, multiworld)
+ multiworld.regions.append(old_man_take_any)
reg = regions.pop()
- entrance = world.get_region(reg, player).entrances[0]
- connect_entrance(world, entrance.name, old_man_take_any.name, player)
+ entrance = multiworld.get_region(reg, player).entrances[0]
+ connect_entrance(multiworld, entrance.name, old_man_take_any.name, player)
entrance.target = 0x58
old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots)
- world.shops.append(old_man_take_any.shop)
+ multiworld.shops.append(old_man_take_any.shop)
- swords = [item for item in world.itempool if item.player == player and item.type == 'Sword']
+ swords = [item for item in multiworld.itempool if item.player == player and item.type == 'Sword']
if swords:
- sword = world.random.choice(swords)
- world.itempool.remove(sword)
- world.itempool.append(ItemFactory('Rupees (20)', player))
- old_man_take_any.shop.add_inventory(0, sword.name, 0, 0, create_location=True)
+ sword = multiworld.random.choice(swords)
+ multiworld.itempool.remove(sword)
+ multiworld.itempool.append(item_factory('Rupees (20)', world))
+ old_man_take_any.shop.add_inventory(0, sword.name, 0, 0)
+ loc_name = "Old Man Sword Cave"
+ location = ALttPLocation(player, loc_name, shop_table_by_location[loc_name], parent=old_man_take_any)
+ location.shop_slot = 0
+ old_man_take_any.locations.append(location)
+ location.place_locked_item(sword)
else:
- old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0, create_location=True)
+ old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0)
for num in range(4):
- take_any = LTTPRegion("Take-Any #{}".format(num+1), LTTPRegionType.Cave, 'a cave of choice', player, world)
- world.regions.append(take_any)
+ take_any = LTTPRegion("Take-Any #{}".format(num+1), LTTPRegionType.Cave, 'a cave of choice', player, multiworld)
+ multiworld.regions.append(take_any)
- target, room_id = world.random.choice([(0x58, 0x0112), (0x60, 0x010F), (0x46, 0x011F)])
+ target, room_id = multiworld.random.choice([(0x58, 0x0112), (0x60, 0x010F), (0x46, 0x011F)])
reg = regions.pop()
- entrance = world.get_region(reg, player).entrances[0]
- connect_entrance(world, entrance.name, take_any.name, player)
+ entrance = multiworld.get_region(reg, player).entrances[0]
+ connect_entrance(multiworld, entrance.name, take_any.name, player)
entrance.target = target
take_any.shop = TakeAny(take_any, room_id, 0xE3, True, True, total_shop_slots + num + 1)
- world.shops.append(take_any.shop)
+ multiworld.shops.append(take_any.shop)
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
- take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0, create_location=True)
-
- world.initialize_regions()
+ take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0)
+ location = ALttPLocation(player, take_any.name, shop_table_by_location[take_any.name], parent=take_any)
+ location.shop_slot = 1
+ take_any.locations.append(location)
+ location.place_locked_item(item_factory("Boss Heart Container", world))
def get_pool_core(world, player: int):
- shuffle = world.shuffle[player]
- difficulty = world.difficulty[player]
- timer = world.timer[player]
- goal = world.goal[player]
- mode = world.mode[player]
+ shuffle = world.entrance_shuffle[player].current_key
+ difficulty = world.item_pool[player].current_key
+ timer = world.timer[player].current_key
+ goal = world.goal[player].current_key
+ mode = world.mode[player].current_key
swordless = world.swordless[player]
retro_bow = world.retro_bow[player]
- logic = world.logic[player]
+ logic = world.glitches_required[player]
pool = []
placed_items = {}
precollected_items = []
- clock_mode = None
- treasure_hunt_count = None
- treasure_hunt_icon = None
+ clock_mode: str = ""
+ treasure_hunt_required: int = 0
+ treasure_hunt_total: int = 0
- diff = ice_rod_hunt_difficulties[difficulty] if goal == 'icerodhunt' else difficulties[difficulty]
+ diff = difficulties[difficulty]
pool.extend(diff.alwaysitems)
def place_item(loc, item):
@@ -536,7 +603,7 @@ def place_item(loc, item):
placed_items[loc] = item
# provide boots to major glitch dependent seeds
- if logic in {'owglitches', 'hybridglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt':
+ if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.glitch_boots[player]:
precollected_items.append('Pegasus Boots')
pool.remove('Pegasus Boots')
pool.append('Rupees (20)')
@@ -587,7 +654,7 @@ def place_item(loc, item):
if want_progressives(world.random):
pool.extend(diff.progressivebow)
world.worlds[player].has_progressive_bows = True
- elif (swordless or logic == 'noglitches') and goal != 'icerodhunt':
+ elif (swordless or logic == 'no_glitches'):
swordless_bows = ['Bow', 'Silver Bow']
if difficulty == "easy":
swordless_bows *= 2
@@ -603,22 +670,33 @@ def place_item(loc, item):
extraitems = total_items_to_place - len(pool) - len(placed_items)
- if timer in ['timed', 'timed-countdown']:
+ if timer in ['timed', 'timed_countdown']:
pool.extend(diff.timedother)
extraitems -= len(diff.timedother)
clock_mode = 'stopwatch' if timer == 'timed' else 'countdown'
- elif timer == 'timed-ohko':
+ elif timer == 'timed_ohko':
pool.extend(diff.timedohko)
extraitems -= len(diff.timedohko)
clock_mode = 'countdown-ohko'
additional_pieces_to_place = 0
- if 'triforcehunt' in goal:
- pieces_in_core = min(extraitems, world.triforce_pieces_available[player])
- additional_pieces_to_place = world.triforce_pieces_available[player] - pieces_in_core
+ if 'triforce_hunt' in goal:
+
+ if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra:
+ treasure_hunt_total = (world.triforce_pieces_available[player].value
+ + world.triforce_pieces_extra[player].value)
+ elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage:
+ percentage = float(world.triforce_pieces_percentage[player].value) / 100
+ treasure_hunt_total = int(round(world.triforce_pieces_required[player].value * percentage, 0))
+ else: # available
+ treasure_hunt_total = world.triforce_pieces_available[player].value
+
+ triforce_pieces = min(90, max(treasure_hunt_total, world.triforce_pieces_required[player].value))
+
+ pieces_in_core = min(extraitems, triforce_pieces)
+ additional_pieces_to_place = triforce_pieces - pieces_in_core
pool.extend(["Triforce Piece"] * pieces_in_core)
extraitems -= pieces_in_core
- treasure_hunt_count = world.triforce_pieces_required[player]
- treasure_hunt_icon = 'Triforce Piece'
+ treasure_hunt_required = world.triforce_pieces_required[player].value
for extra in diff.extras:
if extraitems >= len(extra):
@@ -635,26 +713,37 @@ def place_item(loc, item):
pool.remove("Rupees (20)")
if retro_bow:
- replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)'}
+ replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (50)'}
pool = ['Rupees (5)' if item in replace else item for item in pool]
- if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
+ if world.small_key_shuffle[player] == small_key_shuffle.option_universal:
pool.extend(diff.universal_keys)
- item_to_place = 'Small Key (Universal)' if goal != 'icerodhunt' else 'Nothing'
if mode == 'standard':
- key_location = world.random.choice(
- ['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
- 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
- place_item(key_location, item_to_place)
- else:
- pool.extend([item_to_place])
-
- return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon,
+ if world.key_drop_shuffle[player]:
+ key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop']
+ key_location = world.random.choice(key_locations)
+ key_locations.remove(key_location)
+ place_item(key_location, "Small Key (Universal)")
+ key_locations += ['Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Boomerang Chest',
+ 'Hyrule Castle - Map Chest']
+ key_location = world.random.choice(key_locations)
+ key_locations.remove(key_location)
+ place_item(key_location, "Small Key (Universal)")
+ key_locations += ['Hyrule Castle - Big Key Drop', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']
+ key_location = world.random.choice(key_locations)
+ key_locations.remove(key_location)
+ place_item(key_location, "Small Key (Universal)")
+ key_locations += ['Sewers - Key Rat Key Drop']
+ key_location = world.random.choice(key_locations)
+ place_item(key_location, "Small Key (Universal)")
+ pool = pool[:-3]
+
+ return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_required, treasure_hunt_total,
additional_pieces_to_place)
def make_custom_item_pool(world, player):
- shuffle = world.shuffle[player]
- difficulty = world.difficulty[player]
+ shuffle = world.entrance_shuffle[player]
+ difficulty = world.item_pool[player]
timer = world.timer[player]
goal = world.goal[player]
mode = world.mode[player]
@@ -663,9 +752,9 @@ def make_custom_item_pool(world, player):
pool = []
placed_items = {}
precollected_items = []
- clock_mode = None
- treasure_hunt_count = None
- treasure_hunt_icon = None
+ clock_mode: str = ""
+ treasure_hunt_required: int = 0
+ treasure_hunt_total: int = 0
def place_item(loc, item):
assert loc not in placed_items, "cannot place item twice"
@@ -760,12 +849,11 @@ def place_item(loc, item):
if "triforce" in world.goal[player]:
pool.extend(["Triforce Piece"] * world.triforce_pieces_available[player])
itemtotal += world.triforce_pieces_available[player]
- treasure_hunt_count = world.triforce_pieces_required[player]
- treasure_hunt_icon = 'Triforce Piece'
+ treasure_hunt_required = world.triforce_pieces_required[player]
- if timer in ['display', 'timed', 'timed-countdown']:
- clock_mode = 'countdown' if timer == 'timed-countdown' else 'stopwatch'
- elif timer == 'timed-ohko':
+ if timer in ['display', 'timed', 'timed_countdown']:
+ clock_mode = 'countdown' if timer == 'timed_countdown' else 'stopwatch'
+ elif timer == 'timed_ohko':
clock_mode = 'countdown-ohko'
elif timer == 'ohko':
clock_mode = 'ohko'
@@ -775,7 +863,7 @@ def place_item(loc, item):
itemtotal = itemtotal + 1
if mode == 'standard':
- if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
+ if world.small_key_shuffle[player] == small_key_shuffle.option_universal:
key_location = world.random.choice(
['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
@@ -798,10 +886,12 @@ def place_item(loc, item):
pool.extend(['Magic Mirror'] * customitemarray[22])
pool.extend(['Moon Pearl'] * customitemarray[28])
- if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
- itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in universal mode
+ if world.small_key_shuffle[player] == small_key_shuffle.option_universal:
+ itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in universal Mode
+ if world.key_drop_shuffle[player]:
+ itemtotal = itemtotal - (len(key_drop_data) - 1)
if itemtotal < total_items_to_place:
pool.extend(['Nothing'] * (total_items_to_place - itemtotal))
logging.warning(f"Pool was filled up with {total_items_to_place - itemtotal} Nothing's for player {player}")
- return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon)
+ return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_required)
diff --git a/worlds/alttp/Items.py b/worlds/alttp/Items.py
index 40634de8daa3..cb44f35d58f1 100644
--- a/worlds/alttp/Items.py
+++ b/worlds/alttp/Items.py
@@ -1,6 +1,7 @@
import typing
from BaseClasses import ItemClassification as IC
+from worlds.AutoWorld import World
def GetBeemizerItem(world, player: int, item):
@@ -17,13 +18,10 @@ def GetBeemizerItem(world, player: int, item):
if not world.beemizer_trap_chance[player] or world.random.random() > (world.beemizer_trap_chance[player] / 100):
return "Bee" if isinstance(item, str) else world.create_item("Bee", player)
else:
- return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player)
+ return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player)
-# should be replaced with direct world.create_item(item) call in the future
-def ItemFactory(items: typing.Union[str, typing.Iterable[str]], player: int):
- from worlds.alttp import ALTTPWorld
- world = ALTTPWorld(None, player)
+def item_factory(items: typing.Union[str, typing.Iterable[str]], world: World):
ret = []
singleton = False
if isinstance(items, str):
@@ -102,7 +100,7 @@ def as_init_dict(self) -> typing.Dict[str, typing.Any]:
'Red Pendant': ItemData(IC.progression, 'Crystal', (0x01, 0x32, 0x60, 0x00, 0x69, 0x03), None, None, None, None, None, None, "the red pendant"),
'Triforce': ItemData(IC.progression, None, 0x6A, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'),
'Power Star': ItemData(IC.progression, None, 0x6B, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'),
- 'Triforce Piece': ItemData(IC.progression, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'),
+ 'Triforce Piece': ItemData(IC.progression_skip_balancing, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'),
'Crystal 1': ItemData(IC.progression, 'Crystal', (0x02, 0x34, 0x64, 0x40, 0x7F, 0x06), None, None, None, None, None, None, "a blue crystal"),
'Crystal 2': ItemData(IC.progression, 'Crystal', (0x10, 0x34, 0x64, 0x40, 0x79, 0x06), None, None, None, None, None, None, "a blue crystal"),
'Crystal 3': ItemData(IC.progression, 'Crystal', (0x40, 0x34, 0x64, 0x40, 0x6C, 0x06), None, None, None, None, None, None, "a blue crystal"),
@@ -112,13 +110,15 @@ def as_init_dict(self) -> typing.Dict[str, typing.Any]:
'Crystal 7': ItemData(IC.progression, 'Crystal', (0x08, 0x34, 0x64, 0x40, 0x7C, 0x06), None, None, None, None, None, None, "a blue crystal"),
'Single Arrow': ItemData(IC.filler, None, 0x43, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'),
'Arrows (10)': ItemData(IC.filler, None, 0x44, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack','stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again','ten arrows'),
- 'Arrow Upgrade (+10)': ItemData(IC.filler, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
- 'Arrow Upgrade (+5)': ItemData(IC.filler, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
+ 'Arrow Upgrade (+10)': ItemData(IC.useful, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
+ 'Arrow Upgrade (+5)': ItemData(IC.useful, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
+ 'Arrow Upgrade (70)': ItemData(IC.useful, None, 0x4D, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
'Single Bomb': ItemData(IC.filler, None, 0x27, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'),
'Bombs (3)': ItemData(IC.filler, None, 0x28, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'),
'Bombs (10)': ItemData(IC.filler, None, 0x31, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'),
- 'Bomb Upgrade (+10)': ItemData(IC.filler, None, 0x52, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'),
- 'Bomb Upgrade (+5)': ItemData(IC.filler, None, 0x51, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'),
+ 'Bomb Upgrade (+10)': ItemData(IC.progression, None, 0x52, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'),
+ 'Bomb Upgrade (+5)': ItemData(IC.progression, None, 0x51, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'),
+ 'Bomb Upgrade (50)': ItemData(IC.progression, None, 0x4C, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'),
'Blue Mail': ItemData(IC.useful, None, 0x22, 'Now you\'re a\nblue elf!', 'and the banana hat', 'the protected kid', 'banana hat for sale', 'the clothing store', 'tailor boy banana hatted again', 'the Blue Mail'),
'Red Mail': ItemData(IC.useful, None, 0x23, 'Now you\'re a\nred elf!', 'and the eggplant hat', 'well-protected kid', 'purple hat for sale', 'the nice clothing store', 'tailor boy fears nothing again', 'the Red Mail'),
'Progressive Mail': ItemData(IC.useful, None, 0x60, 'time for a\nchange of\nclothes?', 'and the unknown hat', 'the protected kid', 'new hat for sale', 'the clothing store', 'tailor boy has threads again', 'some armor'),
@@ -222,6 +222,7 @@ def as_init_dict(self) -> typing.Dict[str, typing.Any]:
'Return Smith': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None),
'Pick Up Purple Chest': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None),
'Open Floodgate': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None),
+ 'Capacity Upgrade Shop': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None),
}
item_init_table = {name: data.as_init_dict() for name, data in item_table.items()}
@@ -287,5 +288,5 @@ def as_init_dict(self) -> typing.Dict[str, typing.Any]:
item_table[name].classification in {IC.progression, IC.progression_skip_balancing}}
item_name_groups['Progression Items'] = progression_items
item_name_groups['Non Progression Items'] = everything - progression_items
-
+item_name_groups['Upgrades'] = {name for name in everything if 'Upgrade' in name}
trap_replaceable = item_name_groups['Rupees'] | {'Arrows (10)', 'Single Bomb', 'Bombs (3)', 'Bombs (10)', 'Nothing'}
diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py
index b4b0958ac23f..20dd18038a14 100644
--- a/worlds/alttp/Options.py
+++ b/worlds/alttp/Options.py
@@ -1,10 +1,21 @@
import typing
from BaseClasses import MultiWorld
-from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses
-
-
-class Logic(Choice):
+from Options import Choice, Range, DeathLink, DefaultOnToggle, FreeText, ItemsAccessibility, Option, \
+ PlandoBosses, PlandoConnections, PlandoTexts, Removed, StartInventoryPool, Toggle
+from .EntranceShuffle import default_connections, default_dungeon_connections, \
+ inverted_default_connections, inverted_default_dungeon_connections
+from .Text import TextTable
+
+
+class GlitchesRequired(Choice):
+ """Determine the logic required to complete the seed
+ None: No glitches required
+ Minor Glitches: Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic
+ Overworld Glitches: Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches
+ Hybrid Major Glitches: In addition to overworld glitches, also requires underworld clips between dungeons.
+ No Logic: Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx."""
+ display_name = "Glitches Required"
option_no_glitches = 0
option_minor_glitches = 1
option_overworld_glitches = 2
@@ -12,20 +23,121 @@ class Logic(Choice):
option_no_logic = 4
alias_owg = 2
alias_hmg = 3
+ alias_none = 0
-class Objective(Choice):
- option_crystals = 0
- # option_pendants = 1
- option_triforce_pieces = 2
- option_pedestal = 3
- option_bingo = 4
+class DarkRoomLogic(Choice):
+ """Logic for unlit dark rooms. Lamp: require the Lamp for these rooms to be considered accessible.
+ Torches: in addition to lamp, allow the fire rod and presence of easily accessible torches for access.
+ None: all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness."""
+ display_name = "Dark Room Logic"
+ option_lamp = 0
+ option_torches = 1
+ option_none = 2
+ default = 0
class Goal(Choice):
- option_kill_ganon = 0
- option_kill_ganon_and_gt_agahnim = 1
- option_hand_in = 2
+ """Ganon: Climb GT, defeat Agahnim 2, and then kill Ganon
+ Crystals: Only killing Ganon is required. However, items may still be placed in GT
+ Bosses: Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
+ Pedestal: Pull the Triforce from the Master Sword pedestal
+ Ganon Pedestal: Pull the Master Sword pedestal, then kill Ganon
+ Triforce Hunt: Collect Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
+ Local Triforce Hunt: Collect Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
+ Ganon Triforce Hunt: Collect Triforce pieces spread throughout the worlds, then kill Ganon
+ Local Ganon Triforce Hunt: Collect Triforce pieces spread throughout your world, then kill Ganon"""
+ display_name = "Goal"
+ default = 0
+ option_ganon = 0
+ option_crystals = 1
+ option_bosses = 2
+ option_pedestal = 3
+ option_ganon_pedestal = 4
+ option_triforce_hunt = 5
+ option_local_triforce_hunt = 6
+ option_ganon_triforce_hunt = 7
+ option_local_ganon_triforce_hunt = 8
+
+
+class EntranceShuffle(Choice):
+ """Dungeons Simple: Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon.
+ Dungeons Full: Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons, but keep dungeons to a specific world.
+ Dungeons Crossed: like dungeons_full, but allow cross-world traversal through a dungeon. Warning: May force repeated dungeon traversal.
+ Simple: Entrances are grouped together before being randomized. Interiors with two entrances are grouped shuffled together with each other,
+ and Death Mountain entrances are shuffled only on Death Mountain. Dungeons are swapped entirely.
+ Restricted: Like Simple, but single entrance interiors, multi entrance interiors, and Death Mountain interior entrances are all shuffled with each other.
+ Full: Like Restricted, but all Dungeon entrances are shuffled with all non-Dungeon entrances.
+ Crossed: Like Full, but interiors with multiple entrances are no longer confined to the same world, which may allow crossing worlds.
+ Insanity: Like Crossed, but entrances and exits may be decoupled from each other, so that leaving through an exit may not return you to the entrance you entered from."""
+ display_name = "Entrance Shuffle"
+ default = 0
+ alias_none = 0
+ option_vanilla = 0
+ option_dungeons_simple = 1
+ option_dungeons_full = 2
+ option_dungeons_crossed = 3
+ option_simple = 4
+ option_restricted = 5
+ option_full = 6
+ option_crossed = 7
+ option_insanity = 8
+ alias_dungeonssimple = 1
+ alias_dungeonsfull = 2
+ alias_dungeonscrossed = 3
+
+
+class EntranceShuffleSeed(FreeText):
+ """You can specify a number to use as an entrance shuffle seed, or a group name. Everyone with the same group name
+ will get the same entrance shuffle result as long as their Entrance Shuffle, Mode, Retro Caves, and Glitches
+ Required options are the same."""
+ default = "random"
+ display_name = "Entrance Shuffle Seed"
+
+
+class TriforcePiecesMode(Choice):
+ """Determine how to calculate the extra available triforce pieces.
+ Extra: available = triforce_pieces_extra + triforce_pieces_required
+ Percentage: available = (triforce_pieces_percentage /100) * triforce_pieces_required
+ Available: available = triforce_pieces_available"""
+ display_name = "Triforce Pieces Mode"
+ default = 2
+ option_extra = 0
+ option_percentage = 1
+ option_available = 2
+
+
+class TriforcePiecesPercentage(Range):
+ """Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world."""
+ display_name = "Triforce Pieces Percentage"
+ range_start = 100
+ range_end = 1000
+ default = 150
+
+
+class TriforcePiecesAvailable(Range):
+ """Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1"""
+ display_name = "Triforce Pieces Available"
+ range_start = 1
+ range_end = 90
+ default = 30
+
+
+class TriforcePiecesRequired(Range):
+ """Set to how many out of X triforce pieces you need to win the game in a triforce hunt.
+ Default is 20. Max is 90, Min is 1."""
+ display_name = "Triforce Pieces Required"
+ range_start = 1
+ range_end = 90
+ default = 20
+
+
+class TriforcePiecesExtra(Range):
+ """Set to how many extra triforces pieces are available to collect in the world."""
+ display_name = "Triforce Pieces Extra"
+ range_start = 0
+ range_end = 89
+ default = 10
class OpenPyramid(Choice):
@@ -44,10 +156,10 @@ class OpenPyramid(Choice):
def to_bool(self, world: MultiWorld, player: int) -> bool:
if self.value == self.option_goal:
- return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
+ return world.goal[player].current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
elif self.value == self.option_auto:
- return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} \
- and (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not
+ return world.goal[player].current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
+ and (world.entrance_shuffle[player].current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
world.shuffle_ganon)
elif self.value == self.option_open:
return True
@@ -76,13 +188,13 @@ def hints_useful(self):
return self.value in {1, 2, 3, 4}
-class bigkey_shuffle(DungeonItem):
+class big_key_shuffle(DungeonItem):
"""Big Key Placement"""
item_name_group = "Big Keys"
display_name = "Big Key Shuffle"
-class smallkey_shuffle(DungeonItem):
+class small_key_shuffle(DungeonItem):
"""Small Key Placement"""
option_universal = 5
item_name_group = "Small Keys"
@@ -101,6 +213,154 @@ class map_shuffle(DungeonItem):
display_name = "Map Shuffle"
+class key_drop_shuffle(DefaultOnToggle):
+ """Shuffle keys found in pots and dropped from killed enemies,
+ respects the small key and big key shuffle options."""
+ display_name = "Key Drop Shuffle"
+
+
+class DungeonCounters(Choice):
+ """On: Always display amount of items checked in a dungeon. Pickup: Show when compass is picked up.
+ Default: Show when compass is picked up if the compass itself is shuffled. Off: Never show item count in dungeons."""
+ display_name = "Dungeon Counters"
+ default = 1
+ option_on = 0
+ option_pickup = 1
+ option_default = 2
+ option_off = 4
+
+
+class Mode(Choice):
+ """Standard: Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary
+ Open: Begin the game from your choice of Link's House or the Sanctuary
+ Inverted: Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered"""
+ option_standard = 0
+ option_open = 1
+ option_inverted = 2
+ default = 1
+ display_name = "Mode"
+
+
+class ItemPool(Choice):
+ """Easy: Doubled upgrades, progressives, and etc. Normal: Item availability remains unchanged from vanilla game.
+ Hard: Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless).
+ Expert: Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless)."""
+ display_name = "Item Pool"
+ default = 1
+ option_easy = 0
+ option_normal = 1
+ option_hard = 2
+ option_expert = 3
+
+
+class ItemFunctionality(Choice):
+ """Easy: Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
+ Normal: Vanilla item functionality
+ Hard: Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon)
+ Expert: Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon)"""
+ display_name = "Item Functionality"
+ default = 1
+ option_easy = 0
+ option_normal = 1
+ option_hard = 2
+ option_expert = 3
+
+
+class EnemyHealth(Choice):
+ """Default: Vanilla enemy HP. Easy: Enemies have reduced health. Hard: Enemies have increased health.
+ Expert: Enemies have greatly increased health."""
+ display_name = "Enemy Health"
+ default = 1
+ option_easy = 0
+ option_default = 1
+ option_hard = 2
+ option_expert = 3
+
+
+class EnemyDamage(Choice):
+ """Default: Vanilla enemy damage. Shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps.
+ Chaos: Enemies deal 0 to 8 hearts and armor just reshuffles the damage."""
+ display_name = "Enemy Damage"
+ default = 0
+ option_default = 0
+ option_shuffled = 2
+ option_chaos = 3
+
+
+class ShufflePrizes(Choice):
+ """Shuffle "general" prize packs, as in enemy, tree pull, dig etc.; "bonk" prizes; or both."""
+ display_name = "Shuffle Prizes"
+ default = 1
+ option_off = 0
+ option_general = 1
+ option_bonk = 2
+ option_both = 3
+
+
+class Medallion(Choice):
+ default = "random"
+ option_ether = 0
+ option_bombos = 1
+ option_quake = 2
+
+
+class MiseryMireMedallion(Medallion):
+ """Required medallion to open Misery Mire front entrance."""
+ display_name = "Misery Mire Medallion"
+
+
+class TurtleRockMedallion(Medallion):
+ """Required medallion to open Turtle Rock front entrance."""
+ display_name = "Turtle Rock Medallion"
+
+
+class Timer(Choice):
+ """None: No timer will be displayed. OHKO: Timer always at zero. Permanent OHKO.
+ Timed: Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.
+ Timed OHKO: Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.
+ Timed Countdown: Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.
+ Display: Displays a timer, but otherwise does not affect gameplay or the item pool."""
+ display_name = "Timer"
+ option_none = 0
+ option_timed = 1
+ option_timed_ohko = 2
+ option_ohko = 3
+ option_timed_countdown = 4
+ option_display = 5
+ default = 0
+
+
+class CountdownStartTime(Range):
+ """For Timed OHKO and Timed Countdown timer modes, the amount of time in minutes to start with."""
+ display_name = "Countdown Start Time"
+ range_start = 0
+ range_end = 480
+ default = 10
+
+
+class ClockTime(Range):
+ range_start = -60
+ range_end = 60
+
+
+class RedClockTime(ClockTime):
+ """For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock."""
+ display_name = "Red Clock Time"
+ default = -2
+
+
+class BlueClockTime(ClockTime):
+ """For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock."""
+ display_name = "Blue Clock Time"
+ default = 2
+
+
+class GreenClockTime(ClockTime):
+ """For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock."""
+ display_name = "Green Clock Time"
+ default = 4
+
+
class Crystals(Range):
range_start = 0
range_end = 7
@@ -131,18 +391,52 @@ class ShopItemSlots(Range):
range_end = 30
+class RandomizeShopInventories(Choice):
+ """Generate new default inventories for overworld/underworld shops, and unique shops; or each shop independently"""
+ display_name = "Randomize Shop Inventories"
+ default = 0
+ option_default = 0
+ option_randomize_by_shop_type = 1
+ option_randomize_each = 2
+
+
+class ShuffleShopInventories(Toggle):
+ """Shuffle default inventories of the shops around"""
+ display_name = "Shuffle Shop Inventories"
+
+
+class RandomizeShopPrices(Toggle):
+ """Randomize the prices of the items in shop inventories"""
+ display_name = "Randomize Shop Prices"
+
+
+class RandomizeCostTypes(Toggle):
+ """Prices of the items in shop inventories may cost hearts, arrow, or bombs instead of rupees"""
+ display_name = "Randomize Cost Types"
+
+
class ShopPriceModifier(Range):
"""Percentage modifier for shuffled item prices in shops"""
- display_name = "Shop Price Cost Percent"
+ display_name = "Shop Price Modifier"
range_start = 0
default = 100
range_end = 400
-class WorldState(Choice):
- option_standard = 1
- option_open = 0
- option_inverted = 2
+class IncludeWitchHut(Toggle):
+ """Consider witch's hut like any other shop and shuffle/randomize it too"""
+ display_name = "Include Witch's Hut"
+
+
+class ShuffleCapacityUpgrades(Choice):
+ """Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld).
+ On Combined will shuffle only a single bomb upgrade and arrow upgrade each which bring you to the maximum capacity."""
+ display_name = "Shuffle Capacity Upgrades"
+ option_off = 0
+ option_on = 1
+ option_on_combined = 2
+ alias_false = 0
+ alias_true = 1
class LTTPBosses(PlandoBosses):
@@ -192,7 +486,7 @@ class LTTPBosses(PlandoBosses):
@classmethod
def can_place_boss(cls, boss: str, location: str) -> bool:
- from worlds.alttp.Bosses import can_place_boss
+ from .Bosses import can_place_boss
level = ''
words = location.split(" ")
if words[-1] in ("top", "middle", "bottom"):
@@ -230,6 +524,11 @@ class Swordless(Toggle):
display_name = "Swordless"
+class BomblessStart(Toggle):
+ """Start with a max of 0 bombs available, requiring Bomb Upgrade items in order to use bombs"""
+ display_name = "Bombless Start"
+
+
# Might be a decent idea to split "Bow" into its own option with choices of
# Defer to Progressive Option (default), Progressive, Non-Progressive, Bow + Silvers, Retro
class RetroBow(Toggle):
@@ -420,35 +719,93 @@ class BeemizerTrapChance(BeemizerRange):
display_name = "Beemizer Trap Chance"
-class AllowCollect(Toggle):
- """Allows for !collect / co-op to auto-open chests containing items for other players.
- Off by default, because it currently crashes on real hardware."""
+class AllowCollect(DefaultOnToggle):
+ """Allows for !collect / co-op to auto-open chests containing items for other players."""
display_name = "Allow Collection of checks for other players"
+class ALttPPlandoConnections(PlandoConnections):
+ entrances = set([connection[0] for connection in (
+ *default_connections, *default_dungeon_connections, *inverted_default_connections,
+ *inverted_default_dungeon_connections)])
+ exits = set([connection[1] for connection in (
+ *default_connections, *default_dungeon_connections, *inverted_default_connections,
+ *inverted_default_dungeon_connections)])
+
+
+class ALttPPlandoTexts(PlandoTexts):
+ """Text plando. Format is:
+ - text: 'This is your text'
+ at: text_key
+ percentage: 100
+ Percentage is an integer from 1 to 100, and defaults to 100 when omitted."""
+ valid_keys = TextTable.valid_keys
+
+
alttp_options: typing.Dict[str, type(Option)] = {
+ "accessibility": ItemsAccessibility,
+ "plando_connections": ALttPPlandoConnections,
+ "plando_texts": ALttPPlandoTexts,
+ "start_inventory_from_pool": StartInventoryPool,
+ "goal": Goal,
+ "mode": Mode,
+ "glitches_required": GlitchesRequired,
+ "dark_room_logic": DarkRoomLogic,
+ "open_pyramid": OpenPyramid,
"crystals_needed_for_gt": CrystalsTower,
"crystals_needed_for_ganon": CrystalsGanon,
- "open_pyramid": OpenPyramid,
- "bigkey_shuffle": bigkey_shuffle,
- "smallkey_shuffle": smallkey_shuffle,
+ "triforce_pieces_mode": TriforcePiecesMode,
+ "triforce_pieces_percentage": TriforcePiecesPercentage,
+ "triforce_pieces_required": TriforcePiecesRequired,
+ "triforce_pieces_available": TriforcePiecesAvailable,
+ "triforce_pieces_extra": TriforcePiecesExtra,
+ "entrance_shuffle": EntranceShuffle,
+ "entrance_shuffle_seed": EntranceShuffleSeed,
+ "big_key_shuffle": big_key_shuffle,
+ "small_key_shuffle": small_key_shuffle,
+ "key_drop_shuffle": key_drop_shuffle,
"compass_shuffle": compass_shuffle,
"map_shuffle": map_shuffle,
+ "restrict_dungeon_item_on_boss": RestrictBossItem,
+ "item_pool": ItemPool,
+ "item_functionality": ItemFunctionality,
+ "enemy_health": EnemyHealth,
+ "enemy_damage": EnemyDamage,
"progressive": Progressive,
"swordless": Swordless,
+ "dungeon_counters": DungeonCounters,
"retro_bow": RetroBow,
"retro_caves": RetroCaves,
"hints": Hints,
"scams": Scams,
- "restrict_dungeon_item_on_boss": RestrictBossItem,
"boss_shuffle": LTTPBosses,
"pot_shuffle": PotShuffle,
"enemy_shuffle": EnemyShuffle,
"killable_thieves": KillableThieves,
"bush_shuffle": BushShuffle,
"shop_item_slots": ShopItemSlots,
+ "randomize_shop_inventories": RandomizeShopInventories,
+ "shuffle_shop_inventories": ShuffleShopInventories,
+ "include_witch_hut": IncludeWitchHut,
+ "randomize_shop_prices": RandomizeShopPrices,
+ "randomize_cost_types": RandomizeCostTypes,
"shop_price_modifier": ShopPriceModifier,
+ "shuffle_capacity_upgrades": ShuffleCapacityUpgrades,
+ "bombless_start": BomblessStart,
+ "shuffle_prizes": ShufflePrizes,
"tile_shuffle": TileShuffle,
+ "misery_mire_medallion": MiseryMireMedallion,
+ "turtle_rock_medallion": TurtleRockMedallion,
+ "glitch_boots": GlitchBoots,
+ "beemizer_total_chance": BeemizerTotalChance,
+ "beemizer_trap_chance": BeemizerTrapChance,
+ "timer": Timer,
+ "countdown_start_time": CountdownStartTime,
+ "red_clock_time": RedClockTime,
+ "blue_clock_time": BlueClockTime,
+ "green_clock_time": GreenClockTime,
+ "death_link": DeathLink,
+ "allow_collect": AllowCollect,
"ow_palettes": OWPalette,
"uw_palettes": UWPalette,
"hud_palettes": HUDPalette,
@@ -462,10 +819,9 @@ class AllowCollect(Toggle):
"music": Music,
"reduceflashing": ReduceFlashing,
"triforcehud": TriforceHud,
- "glitch_boots": GlitchBoots,
- "beemizer_total_chance": BeemizerTotalChance,
- "beemizer_trap_chance": BeemizerTrapChance,
- "death_link": DeathLink,
- "allow_collect": AllowCollect,
- "start_inventory_from_pool": StartInventoryPool,
+
+ # removed:
+ "goals": Removed,
+ "smallkey_shuffle": Removed,
+ "bigkey_shuffle": Removed,
}
diff --git a/worlds/alttp/OverworldGlitchRules.py b/worlds/alttp/OverworldGlitchRules.py
index 146fc2f0cac9..2da76234bd40 100644
--- a/worlds/alttp/OverworldGlitchRules.py
+++ b/worlds/alttp/OverworldGlitchRules.py
@@ -220,26 +220,7 @@ def get_invalid_bunny_revival_dungeons():
yield 'Sanctuary'
-def no_logic_rules(world, player):
- """
- Add OWG transitions to no logic player's world
- """
- create_no_logic_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'))
- create_no_logic_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player))
-
- # Glitched speed drops.
- create_no_logic_connections(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'))
-
- # Mirror clip spots.
- if world.mode[player] != 'inverted':
- create_no_logic_connections(player, world, get_mirror_clip_spots_dw())
- create_no_logic_connections(player, world, get_mirror_offset_spots_dw())
- else:
- create_no_logic_connections(player, world, get_mirror_offset_spots_lw(player))
-
-
def overworld_glitch_connections(world, player):
-
# Boots-accessible locations.
create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'))
create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player))
diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py
index 9badbd877422..f3dbbdc059f1 100644
--- a/worlds/alttp/Regions.py
+++ b/worlds/alttp/Regions.py
@@ -14,42 +14,26 @@ def create_regions(world, player):
world.regions += [
create_lw_region(world, player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']),
create_lw_region(world, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure',
- 'Purple Chest', 'Flute Activation Spot'],
- ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River',
- 'Kings Grave Outer Rocks', 'Dam',
- 'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut',
- 'Kakariko Well Drop', 'Kakariko Well Cave',
- 'Blacksmiths Hut', 'Bat Cave Drop Ledge', 'Bat Cave Cave', 'Sick Kids House', 'Hobo Bridge',
- 'Lost Woods Hideout Drop', 'Lost Woods Hideout Stump',
- 'Lumberjack Tree Tree', 'Lumberjack Tree Cave', 'Mini Moldorm Cave', 'Ice Rod Cave',
- 'Lake Hylia Central Island Pier',
- 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Two Brothers House (East)',
- 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow',
- 'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Flute Spot 1',
- 'Dark Desert Teleporter', 'East Hyrule Teleporter', 'South Hyrule Teleporter',
- 'Kakariko Teleporter',
- 'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop',
- 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)',
- 'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave',
- 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing',
- 'Hyrule Castle Main Gate',
- 'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy',
- 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller',
- 'Kakariko Gamble Game', 'Top of Pyramid']),
- create_lw_region(world, player, 'Death Mountain Entrance', None,
- ['Old Man Cave (West)', 'Death Mountain Entrance Drop']),
- create_lw_region(world, player, 'Lake Hylia Central Island', None,
- ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']),
+ 'Purple Chest', 'Flute Activation Spot'],
+ ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam',
+ 'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave',
+ 'Blacksmiths Hut', 'Bat Cave Drop Ledge', 'Bat Cave Cave', 'Sick Kids House', 'Hobo Bridge', 'Lost Woods Hideout Drop', 'Lost Woods Hideout Stump',
+ 'Lumberjack Tree Tree', 'Lumberjack Tree Cave', 'Mini Moldorm Cave', 'Ice Rod Cave', 'Lake Hylia Central Island Pier',
+ 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Two Brothers House (East)', 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow',
+ 'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Flute Spot 1', 'Dark Desert Teleporter', 'East Hyrule Teleporter', 'South Hyrule Teleporter', 'Kakariko Teleporter',
+ 'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)',
+ 'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing', 'Hyrule Castle Main Gate',
+ 'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']),
+ create_lw_region(world, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']),
+ create_lw_region(world, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']),
create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
- "Blind\'s Hideout - Left",
- "Blind\'s Hideout - Right",
- "Blind\'s Hideout - Far Left",
- "Blind\'s Hideout - Far Right"]),
- create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit',
- ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
+ "Blind\'s Hideout - Left",
+ "Blind\'s Hideout - Right",
+ "Blind\'s Hideout - Far Left",
+ "Blind\'s Hideout - Far Right"]),
+ create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
- create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests',
- ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
+ create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
@@ -57,8 +41,7 @@ def create_regions(world, player):
create_cave_region(world, player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']),
create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
- create_cave_region(world, player, 'Elder House', 'a connector', None,
- ['Elder House Exit (East)', 'Elder House Exit (West)']),
+ create_cave_region(world, player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']),
create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'),
create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'),
create_cave_region(world, player, 'Bush Covered House', 'the grass man'),
@@ -79,12 +62,9 @@ def create_regions(world, player):
create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
- create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla',
- ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right',
- 'Sahasrahla']),
- create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit',
- ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
- 'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
+ create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']),
+ create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
+ 'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
@@ -92,12 +72,9 @@ def create_regions(world, player):
create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
create_lw_region(world, player, 'Hobo Bridge', ['Hobo']),
- create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'],
- ['Lost Woods Hideout (top to bottom)']),
- create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None,
- ['Lost Woods Hideout Exit']),
- create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'],
- ['Lumberjack Tree (top to bottom)']),
+ create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']),
+ create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']),
+ create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']),
create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
create_lw_region(world, player, 'Cave 45 Ledge', None, ['Cave 45']),
create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
@@ -105,9 +82,8 @@ def create_regions(world, player):
create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'),
- create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items',
- ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
- 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
+ create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
+ 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'),
create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'),
@@ -118,92 +94,58 @@ def create_regions(world, player):
create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'),
create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']),
- create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies'),
- create_cave_region(world, player, 'Two Brothers House', 'a connector', None,
- ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
+ create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
+ create_cave_region(world, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']),
create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'),
- create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'],
- ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']),
+ create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']),
create_lw_region(world, player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']),
create_lw_region(world, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']),
- create_lw_region(world, player, 'Desert Palace Lone Stairs', None,
- ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
- create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None,
- ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']),
- create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace',
- ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
- ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)',
- 'Desert Palace East Wing']),
- create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None,
- ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
- create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace',
- ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
- create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace',
- ['Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
- create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace',
- ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
- 'Eastern Palace - Cannonball Chest',
- 'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss',
- 'Eastern Palace - Prize'], ['Eastern Palace Exit']),
+ create_lw_region(world, player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
+ create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']),
+ create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
+ ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
+ create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
+ create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
+ create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
+ 'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
+ create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest',
+ 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', 'Eastern Palace - Big Key Chest',
+ 'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'], ['Eastern Palace Exit']),
create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'),
- create_lw_region(world, player, 'Hyrule Castle Courtyard', None,
- ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']),
- create_lw_region(world, player, 'Hyrule Castle Ledge', None,
- ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower',
- 'Hyrule Castle Ledge Courtyard Drop']),
- create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle',
- ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
- 'Hyrule Castle - Zelda\'s Chest'],
- ['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)',
- 'Throne Room']),
+ create_lw_region(world, player, 'Hyrule Castle Courtyard', None, ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']),
+ create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Hyrule Castle Ledge Courtyard Drop']),
+ create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest',
+ 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Big Key Drop'],
+ ['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']),
create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
- create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross'],
- ['Sewers Door']),
- create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit',
- ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
- 'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']),
+ create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
+ create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
+ create_dungeon_region(world, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
+ 'Sewers - Secret Room - Right']),
create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
- create_dungeon_region(world, player, 'Agahnims Tower', 'Castle Tower',
- ['Castle Tower - Room 03', 'Castle Tower - Dark Maze'],
- ['Agahnim 1', 'Agahnims Tower Exit']),
+ create_dungeon_region(world, player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Agahnims Tower Exit']),
create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
- create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'],
- ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
- create_cave_region(world, player, 'Old Man House', 'a connector', None,
- ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
- create_cave_region(world, player, 'Old Man House Back', 'a connector', None,
- ['Old Man House Exit (Top)', 'Old Man House Back to Front']),
- create_lw_region(world, player, 'Death Mountain', None,
- ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)',
- 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak',
- 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']),
- create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None,
- ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
- create_lw_region(world, player, 'Death Mountain Return Ledge', None,
- ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']),
- create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'],
- ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
- create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None,
- ['Spectacle Rock Cave Exit']),
- create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None,
- ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
- create_lw_region(world, player, 'East Death Mountain (Bottom)', None,
- ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)',
- 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks',
- 'Spiral Cave (Bottom)']),
+ create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
+ create_cave_region(world, player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
+ create_cave_region(world, player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']),
+ create_lw_region(world, player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']),
+ create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
+ create_lw_region(world, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']),
+ create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
+ create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']),
+ create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
+ create_lw_region(world, player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'),
- create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None,
- ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)',
- 'Light World Death Mountain Shop']),
+ create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']),
create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
- 'Paradox Cave Lower - Left',
- 'Paradox Cave Lower - Right',
- 'Paradox Cave Lower - Far Right',
- 'Paradox Cave Lower - Middle',
- 'Paradox Cave Upper - Left',
- 'Paradox Cave Upper - Right'],
+ 'Paradox Cave Lower - Left',
+ 'Paradox Cave Lower - Right',
+ 'Paradox Cave Lower - Far Right',
+ 'Paradox Cave Lower - Middle',
+ 'Paradox Cave Upper - Left',
+ 'Paradox Cave Upper - Right'],
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
create_cave_region(world, player, 'Paradox Cave', 'a connector', None,
['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
@@ -334,7 +276,9 @@ def create_regions(world, player):
create_cave_region(world, player, 'Hookshot Cave', 'a connector',
['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right',
'Hookshot Cave - Bottom Left'],
- ['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']),
+ ['Hookshot Cave Exit (South)', 'Hookshot Cave Bomb Wall (South)']),
+ create_cave_region(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
+ 'Hookshot Cave Bomb Wall (North)']),
create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None,
['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']),
create_lw_region(world, player, 'Death Mountain Floating Island (Light World)', ['Floating Island']),
@@ -342,162 +286,100 @@ def create_regions(world, player):
create_lw_region(world, player, 'Mimic Cave Ledge', None, ['Mimic Cave']),
create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
- create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None,
- ['Swamp Palace Moat', 'Swamp Palace Exit']),
- create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'],
- ['Swamp Palace Small Key Door']),
- create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace',
- ['Swamp Palace - Map Chest'], ['Swamp Palace (Center)']),
- create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace',
- ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest',
- 'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest'], ['Swamp Palace (North)']),
- create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace',
- ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
- 'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']),
- create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town',
- ['Thieves\' Town - Big Key Chest',
- 'Thieves\' Town - Map Chest',
- 'Thieves\' Town - Compass Chest',
- 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
+ create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
+ create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
+ create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
+ 'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']),
+ create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
+ 'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']),
+ create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
+ create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
+ 'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room',
+ 'Swamp Palace - Boss', 'Swamp Palace - Prize']),
+ create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
+ 'Thieves\' Town - Map Chest',
+ 'Thieves\' Town - Compass Chest',
+ 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
- 'Thieves\' Town - Big Chest',
- 'Thieves\' Town - Blind\'s Cell'],
- ['Blind Fight']),
- create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town',
- ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
- create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'],
- ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump',
- 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods',
- ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods',
- ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'],
- ['Skull Woods First Section (Left) Door to Exit',
- 'Skull Woods First Section (Left) Door to Right']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods',
- ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
- create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None,
- ['Skull Woods Second Section (Drop)']),
- create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods',
- ['Skull Woods - Big Key Chest'],
- ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
- create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods',
- ['Skull Woods - Bridge Room'],
- ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
- create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods',
- ['Skull Woods - Boss', 'Skull Woods - Prize']),
- create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', None,
- ['Ice Palace Entrance Room', 'Ice Palace Exit']),
- create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace',
- ['Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest',
- 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'],
- ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
- create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'],
- ['Ice Palace (East Top)']),
- create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace',
- ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest']),
- create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace',
- ['Ice Palace - Boss', 'Ice Palace - Prize']),
- create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None,
- ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
- create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire',
- ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
- 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'],
- ['Misery Mire (West)', 'Misery Mire Big Key Door']),
- create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire',
- ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
- create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None,
- ['Misery Mire (Vitreous)']),
- create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire',
- ['Misery Mire - Boss', 'Misery Mire - Prize']),
- create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None,
- ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
- create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock',
- ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
- 'Turtle Rock - Roller Room - Right'],
- ['Turtle Rock Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
- create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock',
- ['Turtle Rock - Chain Chomps'],
- ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
- create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
- ['Turtle Rock - Big Key Chest'],
- ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase',
- 'Turtle Rock Big Key Door']),
- create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'],
- ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
- create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock',
- ['Turtle Rock - Crystaroller Room'],
- ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
- create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None,
- ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
- create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock',
- ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
- 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
- ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)',
- 'Turtle Rock Isolated Ledge Exit']),
- create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock',
- ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
- create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness',
- ['Palace of Darkness - Shooter Room'],
- ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall',
- 'Palace of Darkness Exit']),
- create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness',
- ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
- ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)',
- 'Palace of Darkness Big Key Door']),
- create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness',
- ['Palace of Darkness - Big Key Chest']),
- create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness',
- ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'],
- ['Palace of Darkness Hammer Peg Drop']),
- create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness',
- ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left',
- 'Palace of Darkness - Dark Basement - Right'],
+ 'Thieves\' Town - Big Chest',
+ 'Thieves\' Town - Hallway Pot Key',
+ 'Thieves\' Town - Spike Switch Pot Key',
+ 'Thieves\' Town - Blind\'s Cell'], ['Blind Fight']),
+ create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
+ create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
+ create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
+ create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
+ create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
+ create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
+ create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
+ create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
+ create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
+ create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
+ create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
+ create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
+ 'Ice Palace - Many Pots Pot Key',
+ 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
+ create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
+ create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
+ create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
+ create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
+ create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
+ 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
+ 'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key',
+ 'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']),
+ create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
+ create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
+ create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
+ create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
+ create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
+ 'Turtle Rock - Roller Room - Right'],
+ ['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
+ create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
+ create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
+ create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']),
+ create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
+ create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
+ create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
+ create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
+ create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
+ create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
+ 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
+ ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
+ create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
+ ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
+ create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
- create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness',
- ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom',
- 'Palace of Darkness - Big Chest']),
- create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness',
- ['Palace of Darkness - Harmless Hellway']),
- create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness',
- ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
- create_dungeon_region(world, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower',
- ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
- 'Ganons Tower - Hope Room - Right'],
- ['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door',
- 'Ganons Tower Exit']),
- create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'],
- ['Ganons Tower (Tile Room) Key Door']),
- create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower',
- ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
- 'Ganons Tower - Compass Room - Bottom Left',
- 'Ganons Tower - Compass Room - Bottom Right'], ['Ganons Tower (Bottom) (East)']),
- create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower',
- ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
- 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'],
+ create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
+ create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
+ create_dungeon_region(world, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
+ 'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'],
+ ['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door', 'Ganons Tower Exit']),
+ create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']),
+ create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
+ 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right',
+ 'Ganons Tower - Conveyor Star Pits Pot Key'],
+ ['Ganons Tower (Bottom) (East)']),
+ create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
+ 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
+ 'Ganons Tower - Double Switch Pot Key'],
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
- create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower',
- ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
- create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower',
- ['Ganons Tower - Randomizer Room - Top Left',
- 'Ganons Tower - Randomizer Room - Top Right',
- 'Ganons Tower - Randomizer Room - Bottom Left',
- 'Ganons Tower - Randomizer Room - Bottom Right'], ['Ganons Tower (Bottom) (West)']),
- create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower',
- ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest',
- 'Ganons Tower - Big Key Room - Left',
- 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
- create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None,
- ['Ganons Tower Torch Rooms']),
- create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower',
- ['Ganons Tower - Mini Helmasaur Room - Left',
- 'Ganons Tower - Mini Helmasaur Room - Right',
- 'Ganons Tower - Pre-Moldorm Chest'], ['Ganons Tower Moldorm Door']),
- create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None,
- ['Ganons Tower Moldorm Gap']),
- create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower',
- ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
+ create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
+ create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right',
+ 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'],
+ ['Ganons Tower (Bottom) (West)']),
+ create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left',
+ 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
+ create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
+ create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right',
+ 'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'], ['Ganons Tower Moldorm Door']),
+ create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
+ create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']),
@@ -505,8 +387,6 @@ def create_regions(world, player):
create_dw_region(world, player, 'Dark Death Mountain Bunny Descent Area')
]
- world.initialize_regions()
-
def create_lw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
return _create_region(world, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits)
@@ -526,15 +406,19 @@ def create_dungeon_region(world: MultiWorld, player: int, name: str, hint: str,
def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None,
exits=None):
- from worlds.alttp.SubClasses import ALttPLocation
+ from .SubClasses import ALttPLocation
ret = LTTPRegion(name, type, hint, player, world)
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
if locations:
for location in locations:
- address, player_address, crystal, hint_text = location_table[location]
- ret.locations.append(ALttPLocation(player, location, address, crystal, hint_text, ret, player_address))
+ if location in key_drop_data:
+ ko_hint = key_drop_data[location][2]
+ ret.locations.append(ALttPLocation(player, location, key_drop_data[location][1], False, ko_hint, ret, key_drop_data[location][0]))
+ else:
+ address, player_address, crystal, hint_text = location_table[location]
+ ret.locations.append(ALttPLocation(player, location, address, crystal, hint_text, ret, player_address))
return ret
@@ -587,39 +471,39 @@ def mark_light_world_regions(world, player: int):
key_drop_data = {
- 'Hyrule Castle - Map Guard Key Drop': [0x140036, 0x140037],
- 'Hyrule Castle - Boomerang Guard Key Drop': [0x140033, 0x140034],
- 'Hyrule Castle - Key Rat Key Drop': [0x14000c, 0x14000d],
- 'Hyrule Castle - Big Key Drop': [0x14003c, 0x14003d],
- 'Eastern Palace - Dark Square Pot Key': [0x14005a, 0x14005b],
- 'Eastern Palace - Dark Eyegore Key Drop': [0x140048, 0x140049],
- 'Desert Palace - Desert Tiles 1 Pot Key': [0x140030, 0x140031],
- 'Desert Palace - Beamos Hall Pot Key': [0x14002a, 0x14002b],
- 'Desert Palace - Desert Tiles 2 Pot Key': [0x140027, 0x140028],
- 'Castle Tower - Dark Archer Key Drop': [0x140060, 0x140061],
- 'Castle Tower - Circle of Pots Key Drop': [0x140051, 0x140052],
- 'Swamp Palace - Pot Row Pot Key': [0x140018, 0x140019],
- 'Swamp Palace - Trench 1 Pot Key': [0x140015, 0x140016],
- 'Swamp Palace - Hookshot Pot Key': [0x140012, 0x140013],
- 'Swamp Palace - Trench 2 Pot Key': [0x14000f, 0x140010],
- 'Swamp Palace - Waterway Pot Key': [0x140009, 0x14000a],
- 'Skull Woods - West Lobby Pot Key': [0x14002d, 0x14002e],
- 'Skull Woods - Spike Corner Key Drop': [0x14001b, 0x14001c],
- 'Thieves\' Town - Hallway Pot Key': [0x14005d, 0x14005e],
- 'Thieves\' Town - Spike Switch Pot Key': [0x14004e, 0x14004f],
- 'Ice Palace - Jelly Key Drop': [0x140003, 0x140004],
- 'Ice Palace - Conveyor Key Drop': [0x140021, 0x140022],
- 'Ice Palace - Hammer Block Key Drop': [0x140024, 0x140025],
- 'Ice Palace - Many Pots Pot Key': [0x140045, 0x140046],
- 'Misery Mire - Spikes Pot Key': [0x140054, 0x140055],
- 'Misery Mire - Fishbone Pot Key': [0x14004b, 0x14004c],
- 'Misery Mire - Conveyor Crystal Key Drop': [0x140063, 0x140064],
- 'Turtle Rock - Pokey 1 Key Drop': [0x140057, 0x140058],
- 'Turtle Rock - Pokey 2 Key Drop': [0x140006, 0x140007],
- 'Ganons Tower - Conveyor Cross Pot Key': [0x14003f, 0x140040],
- 'Ganons Tower - Double Switch Pot Key': [0x140042, 0x140043],
- 'Ganons Tower - Conveyor Star Pits Pot Key': [0x140039, 0x14003a],
- 'Ganons Tower - Mini Helmasaur Key Drop': [0x14001e, 0x14001f]
+ 'Hyrule Castle - Map Guard Key Drop': [0x140036, 0x140037, 'in Hyrule Castle', 'Small Key (Hyrule Castle)'],
+ 'Hyrule Castle - Boomerang Guard Key Drop': [0x140033, 0x140034, 'in Hyrule Castle', 'Small Key (Hyrule Castle)'],
+ 'Sewers - Key Rat Key Drop': [0x14000c, 0x14000d, 'in the sewers', 'Small Key (Hyrule Castle)'],
+ 'Hyrule Castle - Big Key Drop': [0x14003c, 0x14003d, 'in Hyrule Castle', 'Big Key (Hyrule Castle)'],
+ 'Eastern Palace - Dark Square Pot Key': [0x14005a, 0x14005b, 'in Eastern Palace', 'Small Key (Eastern Palace)'],
+ 'Eastern Palace - Dark Eyegore Key Drop': [0x140048, 0x140049, 'in Eastern Palace', 'Small Key (Eastern Palace)'],
+ 'Desert Palace - Desert Tiles 1 Pot Key': [0x140030, 0x140031, 'in Desert Palace', 'Small Key (Desert Palace)'],
+ 'Desert Palace - Beamos Hall Pot Key': [0x14002a, 0x14002b, 'in Desert Palace', 'Small Key (Desert Palace)'],
+ 'Desert Palace - Desert Tiles 2 Pot Key': [0x140027, 0x140028, 'in Desert Palace', 'Small Key (Desert Palace)'],
+ 'Castle Tower - Dark Archer Key Drop': [0x140060, 0x140061, 'in Castle Tower', 'Small Key (Agahnims Tower)'],
+ 'Castle Tower - Circle of Pots Key Drop': [0x140051, 0x140052, 'in Castle Tower', 'Small Key (Agahnims Tower)'],
+ 'Swamp Palace - Pot Row Pot Key': [0x140018, 0x140019, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
+ 'Swamp Palace - Trench 1 Pot Key': [0x140015, 0x140016, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
+ 'Swamp Palace - Hookshot Pot Key': [0x140012, 0x140013, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
+ 'Swamp Palace - Trench 2 Pot Key': [0x14000f, 0x140010, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
+ 'Swamp Palace - Waterway Pot Key': [0x140009, 0x14000a, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
+ 'Skull Woods - West Lobby Pot Key': [0x14002d, 0x14002e, 'in Skull Woods', 'Small Key (Skull Woods)'],
+ 'Skull Woods - Spike Corner Key Drop': [0x14001b, 0x14001c, 'near Mothula', 'Small Key (Skull Woods)'],
+ "Thieves' Town - Hallway Pot Key": [0x14005d, 0x14005e, "in Thieves' Town", 'Small Key (Thieves Town)'],
+ "Thieves' Town - Spike Switch Pot Key": [0x14004e, 0x14004f, "in Thieves' Town", 'Small Key (Thieves Town)'],
+ 'Ice Palace - Jelly Key Drop': [0x140003, 0x140004, 'in Ice Palace', 'Small Key (Ice Palace)'],
+ 'Ice Palace - Conveyor Key Drop': [0x140021, 0x140022, 'in Ice Palace', 'Small Key (Ice Palace)'],
+ 'Ice Palace - Hammer Block Key Drop': [0x140024, 0x140025, 'in Ice Palace', 'Small Key (Ice Palace)'],
+ 'Ice Palace - Many Pots Pot Key': [0x140045, 0x140046, 'in Ice Palace', 'Small Key (Ice Palace)'],
+ 'Misery Mire - Spikes Pot Key': [0x140054, 0x140055 , 'in Misery Mire', 'Small Key (Misery Mire)'],
+ 'Misery Mire - Fishbone Pot Key': [0x14004b, 0x14004c, 'in forgotten Mire', 'Small Key (Misery Mire)'],
+ 'Misery Mire - Conveyor Crystal Key Drop': [0x140063, 0x140064 , 'in Misery Mire', 'Small Key (Misery Mire)'],
+ 'Turtle Rock - Pokey 1 Key Drop': [0x140057, 0x140058, 'in Turtle Rock', 'Small Key (Turtle Rock)'],
+ 'Turtle Rock - Pokey 2 Key Drop': [0x140006, 0x140007, 'in Turtle Rock', 'Small Key (Turtle Rock)'],
+ 'Ganons Tower - Conveyor Cross Pot Key': [0x14003f, 0x140040, "in Ganon's Tower", 'Small Key (Ganons Tower)'],
+ 'Ganons Tower - Double Switch Pot Key': [0x140042, 0x140043, "in Ganon's Tower", 'Small Key (Ganons Tower)'],
+ 'Ganons Tower - Conveyor Star Pits Pot Key': [0x140039, 0x14003a, "in Ganon's Tower", 'Small Key (Ganons Tower)'],
+ 'Ganons Tower - Mini Helmasaur Key Drop': [0x14001e, 0x14001f, "atop Ganon's Tower", 'Small Key (Ganons Tower)']
}
# tuple contents:
@@ -856,6 +740,7 @@ def mark_light_world_regions(world, player: int):
'Missing Smith': (None, None, False, None),
'Dark Blacksmith Ruins': (None, None, False, None),
'Flute Activation Spot': (None, None, False, None),
+ 'Capacity Upgrade Shop': (None, None, False, None),
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'),
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'),
'Tower of Hera - Prize': (
@@ -875,7 +760,7 @@ def mark_light_world_regions(world, player: int):
'Turtle Rock - Prize': (
[0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
-from worlds.alttp.Shops import shop_table_by_location_id, shop_table_by_location
+from .Shops import shop_table_by_location_id, shop_table_by_location
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}}
lookup_id_to_name.update(shop_table_by_location_id)
diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py
index ed222b5f5d14..224de6aaf7f3 100644
--- a/worlds/alttp/Rom.py
+++ b/worlds/alttp/Rom.py
@@ -4,7 +4,7 @@
import worlds.Files
LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173"
-RANDOMIZERBASEHASH: str = "9952c2a3ec1b421e408df0d20c8f0c7f"
+RANDOMIZERBASEHASH: str = "8704fb9b9fa4fad52d4d2f9a95fb5360"
ROM_PLAYER_LIMIT: int = 255
import io
@@ -18,14 +18,14 @@
import threading
import concurrent.futures
import bsdiff4
-from typing import Optional, List
+from typing import Collection, Optional, List, SupportsIndex
from BaseClasses import CollectionState, Region, Location, MultiWorld
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
from .Shops import ShopType, ShopPriceType
from .Dungeons import dungeon_music_addresses
-from .Regions import old_location_address_to_new_location_address
+from .Regions import old_location_address_to_new_location_address, key_drop_data
from .Text import MultiByteTextMapper, text_addresses, Credits, TextTable
from .Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, \
Blind_texts, \
@@ -34,9 +34,9 @@
DeathMountain_texts, \
LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \
SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
-from .Items import ItemFactory, item_table, item_name_groups, progression_items
+from .Items import item_table, item_name_groups, progression_items
from .EntranceShuffle import door_addresses
-from .Options import smallkey_shuffle
+from .Options import small_key_shuffle
try:
from maseya import z3pr
@@ -52,7 +52,7 @@
enemizer_logger = logging.getLogger("Enemizer")
-class LocalRom(object):
+class LocalRom:
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
self.name = name
@@ -71,13 +71,13 @@ def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
def read_byte(self, address: int) -> int:
return self.buffer[address]
- def read_bytes(self, startaddress: int, length: int) -> bytes:
+ def read_bytes(self, startaddress: int, length: int) -> bytearray:
return self.buffer[startaddress:startaddress + length]
def write_byte(self, address: int, value: int):
self.buffer[address] = value
- def write_bytes(self, startaddress: int, values):
+ def write_bytes(self, startaddress: int, values: Collection[SupportsIndex]) -> None:
self.buffer[startaddress:startaddress + len(values)] = values
def encrypt_range(self, startaddress: int, length: int, key: bytes):
@@ -294,7 +294,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
'RandomizeBushEnemyChance': multiworld.bush_shuffle[player].value,
'RandomizeEnemyHealthRange': multiworld.enemy_health[player] != 'default',
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[
- multiworld.enemy_health[player]],
+ multiworld.enemy_health[player].current_key],
'OHKO': False,
'RandomizeEnemyDamage': multiworld.enemy_damage[player] != 'default',
'AllowEnemyZeroDamage': True,
@@ -428,6 +428,18 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
rom.write_byte(0x04DE81, 6)
rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry.
+ # Moblins attached to "key drop" locations crash the game when dropping their item when Key Drop Shuffle is on.
+ # Replace them with a Slime enemy if they are placed.
+ if multiworld.key_drop_shuffle[player]:
+ key_drop_enemies = {
+ 0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201,
+ 0x4E20A, 0x4E326, 0x4E4F7, 0x4E687, 0x4E70C, 0x4E7C8, 0x4E7FA
+ }
+ for enemy in key_drop_enemies:
+ if rom.read_byte(enemy) == 0x12:
+ logging.debug(f"Moblin found and replaced at {enemy} in world {player}")
+ rom.write_byte(enemy, 0x8F)
+
for used in (randopatch_path, options_path):
try:
os.remove(used)
@@ -771,11 +783,12 @@ def get_nonnative_item_sprite(code: int) -> int:
def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
local_random = world.per_slot_randoms[player]
+ local_world = world.worlds[player]
# patch items
- for location in world.get_locations():
- if location.player != player or location.address is None or location.shop_slot is not None:
+ for location in world.get_locations(player):
+ if location.address is None or location.shop_slot is not None:
continue
itemid = location.item.code if location.item is not None else 0x5A
@@ -845,21 +858,21 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# Thanks to Zarby89 for originally finding these values
# todo fix screen scrolling
- if world.shuffle[player] not in {'insanity', 'insanity_legacy', 'madness_legacy'} and \
+ if world.entrance_shuffle[player] != 'insanity' and \
exit.name in {'Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit',
'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit',
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit',
'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)',
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)'} and \
- (world.logic[player] not in ['hybridglitches', 'nologic'] or
+ (world.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic'] or
exit.name not in {'Palace of Darkness Exit', 'Tower of Hera Exit', 'Swamp Palace Exit'}):
# For exits that connot be reached from another, no need to apply offset fixes.
rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else
- elif room_id == 0x0059 and world.fix_skullwoods_exit[player]:
+ elif room_id == 0x0059 and local_world.fix_skullwoods_exit:
rom.write_int16(0x15DB5 + 2 * offset, 0x00F8)
- elif room_id == 0x004a and world.fix_palaceofdarkness_exit[player]:
+ elif room_id == 0x004a and local_world.fix_palaceofdarkness_exit:
rom.write_int16(0x15DB5 + 2 * offset, 0x0640)
- elif room_id == 0x00d6 and world.fix_trock_exit[player]:
+ elif room_id == 0x00d6 and local_world.fix_trock_exit:
rom.write_int16(0x15DB5 + 2 * offset, 0x0134)
elif room_id == 0x000c and world.shuffle_ganon: # fix ganons tower exit point
rom.write_int16(0x15DB5 + 2 * offset, 0x00A4)
@@ -894,9 +907,34 @@ def credits_digit(num):
if world.retro_caves[player]: # Old man cave and Take any caves will count towards collection rate.
credits_total += 5
if world.shop_item_slots[player]: # Potion shop only counts towards collection rate if included in the shuffle.
- credits_total += 30 if 'w' in world.shop_shuffle[player] else 27
+ credits_total += 30 if world.include_witch_hut[player] else 27
+ if world.shuffle_capacity_upgrades[player]:
+ credits_total += 2
rom.write_byte(0x187010, credits_total) # dynamic credits
+
+ if world.key_drop_shuffle[player]:
+ rom.write_byte(0x140000, 1) # enable key drop shuffle
+ credits_total += len(key_drop_data)
+ # update dungeon counters
+ rom.write_byte(0x187001, 12) # Hyrule Castle
+ rom.write_byte(0x187002, 8) # Eastern Palace
+ rom.write_byte(0x187003, 9) # Desert Palace
+ rom.write_byte(0x187004, 4) # Agahnims Tower
+ rom.write_byte(0x187005, 15) # Swamp Palace
+ rom.write_byte(0x187007, 11) # Misery Mire
+ rom.write_byte(0x187008, 10) # Skull Woods
+ rom.write_byte(0x187009, 12) # Ice Palace
+ rom.write_byte(0x18700B, 10) # Thieves Town
+ rom.write_byte(0x18700C, 14) # Turtle Rock
+ rom.write_byte(0x18700D, 31) # Ganons Tower
+ # update credits GT Big Key counter
+ gt_bigkey_top, gt_bigkey_bottom = credits_digit(5)
+ rom.write_byte(0x118B6A, gt_bigkey_top)
+ rom.write_byte(0x118B88, gt_bigkey_bottom)
+
+
+
# collection rate address: 238C37
first_top, first_bot = credits_digit((credits_total / 100) % 10)
mid_top, mid_bot = credits_digit((credits_total / 10) % 10)
@@ -907,22 +945,22 @@ def credits_digit(num):
rom.write_bytes(0x118C64, [first_bot, mid_bot, last_bot])
# patch medallion requirements
- if world.required_medallions[player][0] == 'Bombos':
+ if local_world.required_medallions[0] == 'Bombos':
rom.write_byte(0x180022, 0x00) # requirement
rom.write_byte(0x4FF2, 0x31) # sprite
rom.write_byte(0x50D1, 0x80)
rom.write_byte(0x51B0, 0x00)
- elif world.required_medallions[player][0] == 'Quake':
+ elif local_world.required_medallions[0] == 'Quake':
rom.write_byte(0x180022, 0x02) # requirement
rom.write_byte(0x4FF2, 0x31) # sprite
rom.write_byte(0x50D1, 0x88)
rom.write_byte(0x51B0, 0x00)
- if world.required_medallions[player][1] == 'Bombos':
+ if local_world.required_medallions[1] == 'Bombos':
rom.write_byte(0x180023, 0x00) # requirement
rom.write_byte(0x5020, 0x31) # sprite
rom.write_byte(0x50FF, 0x90)
rom.write_byte(0x51DE, 0x00)
- elif world.required_medallions[player][1] == 'Ether':
+ elif local_world.required_medallions[1] == 'Ether':
rom.write_byte(0x180023, 0x01) # requirement
rom.write_byte(0x5020, 0x31) # sprite
rom.write_byte(0x50FF, 0x98)
@@ -958,7 +996,7 @@ def credits_digit(num):
rom.write_byte(0x18003A, 0x01 if world.dark_world_light_cone else 0x00)
GREEN_TWENTY_RUPEES = 0x47
- GREEN_CLOCK = ItemFactory('Green Clock', player).code
+ GREEN_CLOCK = item_table["Green Clock"].item_code
rom.write_byte(0x18004F, 0x01) # Byrna Invulnerability: on
@@ -1023,7 +1061,7 @@ def credits_digit(num):
# Set stun items
rom.write_byte(0x180180, 0x03) # All standard items
# Set overflow items for progressive equipment
- if world.timer[player] in ['timed', 'timed-countdown', 'timed-ohko']:
+ if world.timer[player] in ['timed', 'timed_countdown', 'timed_ohko']:
overflow_replacement = GREEN_CLOCK
else:
overflow_replacement = GREEN_TWENTY_RUPEES
@@ -1031,7 +1069,7 @@ def credits_digit(num):
# Byrna residual magic cost
rom.write_bytes(0x45C42, [0x04, 0x02, 0x01])
- difficulty = world.difficulty_requirements[player]
+ difficulty = local_world.difficulty_requirements
# Set overflow items for progressive equipment
rom.write_bytes(0x180090,
@@ -1043,7 +1081,7 @@ def credits_digit(num):
difficulty.progressive_bow_limit, item_table[difficulty.basicbow[-1]].item_code])
if difficulty.progressive_bow_limit < 2 and (
- world.swordless[player] or world.logic[player] == 'noglitches'):
+ world.swordless[player] or world.glitches_required[player] == 'no_glitches'):
rom.write_bytes(0x180098, [2, item_table["Silver Bow"].item_code])
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
@@ -1059,7 +1097,7 @@ def credits_digit(num):
prize_replacements[0xE1] = 0xDA # 5 Arrows -> Blue Rupee
prize_replacements[0xE2] = 0xDB # 10 Arrows -> Red Rupee
- if "g" in world.shuffle_prizes[player]:
+ if world.shuffle_prizes[player] in ("general", "both"):
# shuffle prize packs
prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0,
0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF,
@@ -1121,7 +1159,7 @@ def chunk(l, n):
byte = int(rom.read_byte(address))
rom.write_byte(address, prize_replacements.get(byte, byte))
- if "b" in world.shuffle_prizes[player]:
+ if world.shuffle_prizes[player] in ("bonk", "both"):
# set bonk prizes
bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC,
0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79,
@@ -1155,12 +1193,8 @@ def chunk(l, n):
])
# set Fountain bottle exchange items
- if world.difficulty[player] in ['hard', 'expert']:
- rom.write_byte(0x348FF, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x48][local_random.randint(0, 5)])
- rom.write_byte(0x3493B, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x48][local_random.randint(0, 5)])
- else:
- rom.write_byte(0x348FF, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][local_random.randint(0, 6)])
- rom.write_byte(0x3493B, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][local_random.randint(0, 6)])
+ rom.write_byte(0x348FF, item_table[local_world.waterfall_fairy_bottle_fill].item_code)
+ rom.write_byte(0x3493B, item_table[local_world.pyramid_fairy_bottle_fill].item_code)
# enable Fat Fairy Chests
rom.write_bytes(0x1FC16, [0xB1, 0xC6, 0xF9, 0xC9, 0xC6, 0xF9])
@@ -1206,17 +1240,17 @@ def chunk(l, n):
rom.write_byte(0x180044, 0x01) # hammer activates tablets
# set up clocks for timed modes
- if world.clock_mode[player] in ['ohko', 'countdown-ohko']:
+ if local_world.clock_mode in ['ohko', 'countdown-ohko']:
rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality
- elif world.clock_mode[player] == 'stopwatch':
+ elif local_world.clock_mode == 'stopwatch':
rom.write_bytes(0x180190, [0x02, 0x01, 0x00]) # set stopwatch mode
- elif world.clock_mode[player] == 'countdown':
+ elif local_world.clock_mode == 'countdown':
rom.write_bytes(0x180190, [0x01, 0x01, 0x00]) # set countdown, with no reset available
else:
rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode
# Set up requested clock settings
- if world.clock_mode[player] in ['countdown-ohko', 'stopwatch', 'countdown']:
+ if local_world.clock_mode in ['countdown-ohko', 'stopwatch', 'countdown']:
rom.write_int32(0x180200,
world.red_clock_time[player] * 60 * 60) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204,
@@ -1229,34 +1263,35 @@ def chunk(l, n):
rom.write_int32(0x180208, 0) # green clock adjustment time (in frames, sint32)
# Set up requested start time for countdown modes
- if world.clock_mode[player] in ['countdown-ohko', 'countdown']:
+ if local_world.clock_mode in ['countdown-ohko', 'countdown']:
rom.write_int32(0x18020C, world.countdown_start_time[player] * 60 * 60) # starting time (in frames, sint32)
else:
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32)
# set up goals for treasure hunt
- rom.write_int16(0x180163, world.treasure_hunt_count[player])
- rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28])
+ rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required -
+ sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")))
+ rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed
gametype = 0x04 # item
- if world.shuffle[player] != 'vanilla':
+ if world.entrance_shuffle[player] != 'vanilla':
gametype |= 0x02 # entrance
if enemized:
gametype |= 0x01 # enemizer
rom.write_byte(0x180211, gametype) # Game type
# assorted fixes
- rom.write_byte(0x1800A2, 0x01 if world.fix_fake_world[
- player] else 0x00) # Toggle whether to be in real/fake dark world when dying in a DW dungeon before killing aga1
+ # Toggle whether to be in real/fake dark world when dying in a DW dungeon before killing aga1
+ rom.write_byte(0x1800A2, 0x01 if local_world.fix_fake_world else 0x00)
# Lock or unlock aga tower door during escape sequence.
rom.write_byte(0x180169, 0x00)
if world.mode[player] == 'inverted':
rom.write_byte(0x180169, 0x02) # lock aga/ganon tower door with crystals in inverted
rom.write_byte(0x180171,
- 0x01 if world.ganon_at_pyramid[player] else 0x00) # Enable respawning on pyramid after ganon death
+ 0x01 if local_world.ganon_at_pyramid else 0x00) # Enable respawning on pyramid after ganon death
rom.write_byte(0x180173, 0x01) # Bob is enabled
rom.write_byte(0x180168, 0x08) # Spike Cave Damage
rom.write_bytes(0x18016B, [0x04, 0x02, 0x01]) # Set spike cave and MM spike room Cape usage
@@ -1272,7 +1307,7 @@ def chunk(l, n):
rom.write_byte(0x180086, 0x00 if world.aga_randomness else 0x01) # set blue ball and ganon warp randomness
rom.write_byte(0x1800A0, 0x01) # return to light world on s+q without mirror
rom.write_byte(0x1800A1, 0x01) # enable overworld screen transition draining for water level inside swamp
- rom.write_byte(0x180174, 0x01 if world.fix_fake_world[player] else 0x00)
+ rom.write_byte(0x180174, 0x01 if local_world.fix_fake_world else 0x00)
rom.write_byte(0x18017E, 0x01) # Fairy fountains only trade in bottles
# Starting equipment
@@ -1280,7 +1315,7 @@ def chunk(l, n):
equip[0x36C] = 0x18
equip[0x36D] = 0x18
equip[0x379] = 0x68
- starting_max_bombs = 10
+ starting_max_bombs = 0 if world.bombless_start[player] else 10
starting_max_arrows = 30
startingstate = CollectionState(world)
@@ -1338,7 +1373,7 @@ def chunk(l, n):
'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword',
'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield',
'Red Mail', 'Blue Mail', 'Progressive Mail',
- 'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)'}:
+ 'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)', 'Triforce Piece'}:
continue
set_table = {'Book of Mudora': (0x34E, 1), 'Hammer': (0x34B, 1), 'Bug Catching Net': (0x34D, 1),
@@ -1398,8 +1433,8 @@ def chunk(l, n):
'Bottle (Fairy)': 6, 'Bottle (Bee)': 7, 'Bottle (Good Bee)': 8}
rupees = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)': 50, 'Rupees (100)': 100,
'Rupees (300)': 300}
- bomb_caps = {'Bomb Upgrade (+5)': 5, 'Bomb Upgrade (+10)': 10}
- arrow_caps = {'Arrow Upgrade (+5)': 5, 'Arrow Upgrade (+10)': 10}
+ bomb_caps = {'Bomb Upgrade (+5)': 5, 'Bomb Upgrade (+10)': 10, 'Bomb Upgrade (50)': 50}
+ arrow_caps = {'Arrow Upgrade (+5)': 5, 'Arrow Upgrade (+10)': 10, 'Arrow Upgrade (70)': 70}
bombs = {'Single Bomb': 1, 'Bombs (3)': 3, 'Bombs (10)': 10}
arrows = {'Single Arrow': 1, 'Arrows (10)': 10}
@@ -1414,7 +1449,7 @@ def chunk(l, n):
for address in keys[item.name]:
equip[address] = min(equip[address] + 1, 99)
elif item.name in bottles:
- if equip[0x34F] < world.difficulty_requirements[player].progressive_bottle_limit:
+ if equip[0x34F] < local_world.difficulty_requirements.progressive_bottle_limit:
equip[0x35C + equip[0x34F]] = bottles[item.name]
equip[0x34F] += 1
elif item.name in rupees:
@@ -1466,22 +1501,22 @@ def chunk(l, n):
rom.write_byte(0x3A96D, 0xF0 if world.mode[
player] != 'inverted' else 0xD0) # Residual Portal: Normal (F0= Light Side, D0=Dark Side, 42 = both (Darth Vader))
rom.write_byte(0x3A9A7, 0xD0) # Residual Portal: Normal (D0= Light Side, F0=Dark Side, 42 = both (Darth Vader))
- if 'u' in world.shop_shuffle[player]:
+ if world.shuffle_capacity_upgrades[player]:
rom.write_bytes(0x180080,
[5, 10, 5, 10]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10)
else:
rom.write_bytes(0x180080,
[50, 50, 70, 70]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10)
- rom.write_byte(0x18004D, ((0x01 if 'arrows' in world.escape_assist[player] else 0x00) |
- (0x02 if 'bombs' in world.escape_assist[player] else 0x00) |
- (0x04 if 'magic' in world.escape_assist[player] else 0x00))) # Escape assist
+ rom.write_byte(0x18004D, ((0x01 if 'arrows' in local_world.escape_assist else 0x00) |
+ (0x02 if 'bombs' in local_world.escape_assist else 0x00) |
+ (0x04 if 'magic' in local_world.escape_assist else 0x00))) # Escape assist
- if world.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt', 'icerodhunt']:
+ if world.goal[player] in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']:
rom.write_byte(0x18003E, 0x01) # make ganon invincible
- elif world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']:
+ elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
rom.write_byte(0x18003E, 0x05) # make ganon invincible until enough triforce pieces are collected
- elif world.goal[player] in ['ganonpedestal']:
+ elif world.goal[player] in ['ganon_pedestal']:
rom.write_byte(0x18003E, 0x06)
elif world.goal[player] in ['bosses']:
rom.write_byte(0x18003E, 0x02) # make ganon invincible until all bosses are beat
@@ -1502,17 +1537,17 @@ def chunk(l, n):
# c - enabled for inside compasses
# s - enabled for inside small keys
# block HC upstairs doors in rain state in standard mode
- rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.shuffle[player] != 'vanilla' else 0x00)
+ rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.entrance_shuffle[player] != 'vanilla' else 0x00)
- rom.write_byte(0x18016A, 0x10 | ((0x01 if world.smallkey_shuffle[player] else 0x00)
+ rom.write_byte(0x18016A, 0x10 | ((0x01 if world.small_key_shuffle[player] else 0x00)
| (0x02 if world.compass_shuffle[player] else 0x00)
| (0x04 if world.map_shuffle[player] else 0x00)
- | (0x08 if world.bigkey_shuffle[
+ | (0x08 if world.big_key_shuffle[
player] else 0x00))) # free roaming item text boxes
rom.write_byte(0x18003B, 0x01 if world.map_shuffle[player] else 0x00) # maps showing crystals on overworld
# compasses showing dungeon count
- if world.clock_mode[player] or not world.dungeon_counters[player]:
+ if local_world.clock_mode or not world.dungeon_counters[player]:
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
elif world.dungeon_counters[player] is True:
rom.write_byte(0x18003C, 0x02) # always on
@@ -1529,9 +1564,9 @@ def chunk(l, n):
# b - Big Key
# a - Small Key
#
- rom.write_byte(0x180045, ((0x00 if (world.smallkey_shuffle[player] == smallkey_shuffle.option_original_dungeon or
- world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) else 0x01)
- | (0x02 if world.bigkey_shuffle[player] else 0x00)
+ rom.write_byte(0x180045, ((0x00 if (world.small_key_shuffle[player] == small_key_shuffle.option_original_dungeon or
+ world.small_key_shuffle[player] == small_key_shuffle.option_universal) else 0x01)
+ | (0x02 if world.big_key_shuffle[player] else 0x00)
| (0x04 if world.map_shuffle[player] else 0x00)
| (0x08 if world.compass_shuffle[player] else 0x00))) # free roaming items in menu
@@ -1563,8 +1598,8 @@ def get_reveal_bytes(itemName):
rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if world.map_shuffle[
player] else 0x0000) # Bomb Shop Reveal
- rom.write_byte(0x180172, 0x01 if world.smallkey_shuffle[
- player] == smallkey_shuffle.option_universal else 0x00) # universal keys
+ rom.write_byte(0x180172, 0x01 if world.small_key_shuffle[
+ player] == small_key_shuffle.option_universal else 0x00) # universal keys
rom.write_byte(0x18637E, 0x01 if world.retro_bow[player] else 0x00) # Skip quiver in item shops once bought
rom.write_byte(0x180175, 0x01 if world.retro_bow[player] else 0x00) # rupee bow
rom.write_byte(0x180176, 0x0A if world.retro_bow[player] else 0x00) # wood arrow cost
@@ -1581,9 +1616,9 @@ def get_reveal_bytes(itemName):
rom.write_byte(0x180020, digging_game_rng)
rom.write_byte(0xEFD95, digging_game_rng)
rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills
- rom.write_byte(0x1800A4, 0x01 if world.logic[player] != 'nologic' else 0x00) # enable POD EG fix
- rom.write_byte(0x186383, 0x01 if world.glitch_triforce or world.logic[
- player] == 'nologic' else 0x00) # disable glitching to Triforce from Ganons Room
+ rom.write_byte(0x1800A4, 0x01 if world.glitches_required[player] != 'no_logic' else 0x00) # enable POD EG fix
+ rom.write_byte(0x186383, 0x01 if world.glitches_required[
+ player] == 'no_logic' else 0x00) # disable glitching to Triforce from Ganons Room
rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
# remove shield from uncle
@@ -1619,17 +1654,17 @@ def get_reveal_bytes(itemName):
rom.write_bytes(0x18018B, [0x20, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
# patch swamp: Need to enable permanent drain of water as dam or swamp were moved
- rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required[player] else 0x00)
+ rom.write_byte(0x18003D, 0x01 if local_world.swamp_patch_required else 0x00)
# powder patch: remove the need to leave the screen after powder, since it causes problems for potion shop at race game
# temporarally we are just nopping out this check we will conver this to a rom fix soon.
rom.write_bytes(0x02F539,
- [0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if world.powder_patch_required[player] else [0xAD, 0xBF, 0x0A, 0xF0,
- 0x4F])
+ [0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if local_world.powder_patch_required else [
+ 0xAD, 0xBF, 0x0A, 0xF0, 0x4F])
# allow smith into multi-entrance caves in appropriate shuffles
- if world.shuffle[player] in ['restricted', 'full', 'crossed', 'insanity', 'madness'] or (
- world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'):
+ if world.entrance_shuffle[player] in ['restricted', 'full', 'crossed', 'insanity'] or (
+ world.entrance_shuffle[player] == 'simple' and world.mode[player] == 'inverted'):
rom.write_byte(0x18004C, 0x01)
# set correct flag for hera basement item
@@ -1640,14 +1675,14 @@ def get_reveal_bytes(itemName):
rom.write_byte(0x4E3BB, 0xEB)
# fix trock doors for reverse entrances
- if world.fix_trock_doors[player]:
+ if local_world.fix_trock_doors:
rom.write_byte(0xFED31, 0x0E) # preopen bombable exit
rom.write_byte(0xFEE41, 0x0E) # preopen bombable exit
# included unconditionally in base2current
# rom.write_byte(0xFE465, 0x1E) # remove small key door on backside of big key door
else:
- rom.write_byte(0xFED31, 0x2A) # preopen bombable exit
- rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit
+ rom.write_byte(0xFED31, 0x2A) # bombable exit
+ rom.write_byte(0xFEE41, 0x2A) # bombable exit
if world.tile_shuffle[player]:
tile_set = TileSet.get_random_tile_set(world.per_slot_randoms[player])
@@ -1726,8 +1761,8 @@ def write_custom_shops(rom, world, player):
if item is None:
break
if world.shop_item_slots[player] or shop.type == ShopType.TakeAny:
- count_shop = (shop.region.name != 'Potion Shop' or 'w' in world.shop_shuffle[player]) and \
- shop.region.name != 'Capacity Upgrade'
+ count_shop = (shop.region.name != 'Potion Shop' or world.include_witch_hut[player]) and \
+ (shop.region.name != 'Capacity Upgrade' or world.shuffle_capacity_upgrades[player])
rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0)
if item['item'] == 'Single Arrow' and item['player'] == 0:
arrow_mask |= 1 << index
@@ -1743,13 +1778,13 @@ def write_custom_shops(rom, world, player):
if item['player'] and world.game[item['player']] != "A Link to the Past": # item not native to ALTTP
item_code = get_nonnative_item_sprite(world.worlds[item['player']].item_name_to_id[item['item']])
else:
- item_code = ItemFactory(item['item'], player).code
+ item_code = item_table[item["item"]].item_code
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro_bow[player]:
rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask)
item_data = [shop_id, item_code] + price_data + \
- [item['max'], ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + \
- replacement_price_data + [0 if item['player'] == player else min(ROM_PLAYER_LIMIT, item['player'])]
+ [item["max"], item_table[item["replacement"]].item_code if item["replacement"] else 0xFF] + \
+ replacement_price_data + [0 if item["player"] == player else min(ROM_PLAYER_LIMIT, item["player"])]
items_data.extend(item_data)
rom.write_bytes(0x184800, shop_data)
@@ -1824,10 +1859,10 @@ def apply_oof_sfx(rom, oof: str):
# (We need to insert the second sigil at the end)
rom.write_bytes(0x12803A, oof_bytes)
rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB])
-
- #Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
+
+ # Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
-
+
def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options,
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
@@ -2169,7 +2204,7 @@ def write_strings(rom, world, player):
tt.removeUnwantedText()
# Let's keep this guy's text accurate to the shuffle setting.
- if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple', 'dungeonscrossed']:
+ if world.entrance_shuffle[player] in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
@@ -2212,7 +2247,7 @@ def hint_text(dest, ped_hint=False):
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
hint_locations = HintLocations.copy()
local_random.shuffle(hint_locations)
- all_entrances = [entrance for entrance in world.get_entrances() if entrance.player == player]
+ all_entrances = list(world.get_entrances(player))
local_random.shuffle(all_entrances)
# First we take care of the one inconvenient dungeon in the appropriately simple shuffles.
@@ -2223,7 +2258,7 @@ def hint_text(dest, ped_hint=False):
entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'})
else:
entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'})
- if world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']:
+ if world.entrance_shuffle[player] in ['simple', 'restricted']:
for entrance in all_entrances:
if entrance.name in entrances_to_hint:
this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(
@@ -2233,9 +2268,9 @@ def hint_text(dest, ped_hint=False):
break
# Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones.
entrances_to_hint.update(InconvenientOtherEntrances)
- if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
+ if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
hint_count = 0
- elif world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']:
+ elif world.entrance_shuffle[player] in ['simple', 'restricted']:
hint_count = 2
else:
hint_count = 4
@@ -2252,14 +2287,14 @@ def hint_text(dest, ped_hint=False):
# Next we handle hints for randomly selected other entrances,
# curating the selection intelligently based on shuffle.
- if world.shuffle[player] not in ['simple', 'restricted', 'restricted_legacy']:
+ if world.entrance_shuffle[player] not in ['simple', 'restricted']:
entrances_to_hint.update(ConnectorEntrances)
entrances_to_hint.update(DungeonEntrances)
if world.mode[player] == 'inverted':
entrances_to_hint.update({'Inverted Agahnims Tower': 'The dark mountain tower'})
else:
entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'})
- elif world.shuffle[player] == 'restricted':
+ elif world.entrance_shuffle[player] == 'restricted':
entrances_to_hint.update(ConnectorEntrances)
entrances_to_hint.update(OtherEntrances)
if world.mode[player] == 'inverted':
@@ -2269,15 +2304,15 @@ def hint_text(dest, ped_hint=False):
else:
entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'})
entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'})
- if world.shuffle[player] in ['insanity', 'madness_legacy', 'insanity_legacy']:
+ if world.entrance_shuffle[player] != 'insanity':
entrances_to_hint.update(InsanityEntrances)
if world.shuffle_ganon:
if world.mode[player] == 'inverted':
entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'})
else:
entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'})
- hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull',
- 'dungeonscrossed'] else 0
+ hint_count = 4 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full',
+ 'dungeons_crossed'] else 0
for entrance in all_entrances:
if entrance.name in entrances_to_hint:
if hint_count:
@@ -2291,11 +2326,11 @@ def hint_text(dest, ped_hint=False):
# Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable.
locations_to_hint = InconvenientLocations.copy()
- if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
+ if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
locations_to_hint.extend(InconvenientVanillaLocations)
local_random.shuffle(locations_to_hint)
- hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull',
- 'dungeonscrossed'] else 5
+ hint_count = 3 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full',
+ 'dungeons_crossed'] else 5
for location in locations_to_hint[:hint_count]:
if location == 'Swamp Left':
if local_random.randint(0, 1):
@@ -2349,20 +2384,23 @@ def hint_text(dest, ped_hint=False):
# Lastly we write hints to show where certain interesting items are.
items_to_hint = RelevantItems.copy()
- if world.smallkey_shuffle[player].hints_useful:
+ if world.small_key_shuffle[player].hints_useful:
items_to_hint |= item_name_groups["Small Keys"]
- if world.bigkey_shuffle[player].hints_useful:
+ if world.big_key_shuffle[player].hints_useful:
items_to_hint |= item_name_groups["Big Keys"]
if world.hints[player] == "full":
hint_count = len(hint_locations) # fill all remaining hint locations with Item hints.
else:
- hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull',
- 'dungeonscrossed'] else 8
+ hint_count = 5 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full',
+ 'dungeons_crossed'] else 8
hint_count = min(hint_count, len(items_to_hint), len(hint_locations))
if hint_count:
locations = world.find_items_in_locations(items_to_hint, player, True)
local_random.shuffle(locations)
+ # make locked locations less likely to appear as hint,
+ # chances are the lock means the player already knows.
+ locations.sort(key=lambda sorting_location: not sorting_location.locked)
for x in range(min(hint_count, len(locations))):
this_location = locations.pop()
this_hint = this_location.item.hint_text + ' can be found ' + hint_text(this_location) + '.'
@@ -2384,8 +2422,8 @@ def hint_text(dest, ped_hint=False):
' %s?' % hint_text(silverarrows[0]).replace('Ganon\'s', 'my')) if silverarrows else '?\nI think not!'
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint
- if world.worlds[player].has_progressive_bows and (world.difficulty_requirements[player].progressive_bow_limit >= 2 or (
- world.swordless[player] or world.logic[player] == 'noglitches')):
+ if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
+ world.swordless[player] or world.glitches_required[player] == 'no_glitches')):
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
world.per_slot_randoms[player].shuffle(prog_bow_locs)
found_bow = False
@@ -2416,7 +2454,7 @@ def hint_text(dest, ped_hint=False):
if world.goal[player] == 'bosses':
tt['sign_ganon'] = 'You need to kill all bosses, Ganon last.'
- elif world.goal[player] == 'ganonpedestal':
+ elif world.goal[player] == 'ganon_pedestal':
tt['sign_ganon'] = 'You need to pull the pedestal to defeat Ganon.'
elif world.goal[player] == "ganon":
if world.crystals_needed_for_ganon[player] == 1:
@@ -2424,14 +2462,6 @@ def hint_text(dest, ped_hint=False):
else:
tt['sign_ganon'] = f'You need {world.crystals_needed_for_ganon[player]} crystals to beat Ganon and ' \
f'have beaten Agahnim atop Ganons Tower'
- elif world.goal[player] == "icerodhunt":
- tt['sign_ganon'] = 'Go find the Ice Rod and Kill Trinexx, then talk to Murahdahla... Ganon is invincible!'
- tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Go kill Trinexx instead.'
- tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
- tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
- "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
- "hidden in a hollow tree. " \
- "If you bring me the Triforce piece from Turtle Rock, I can reassemble it."
else:
if world.crystals_needed_for_ganon[player] == 1:
tt['sign_ganon'] = 'You need a crystal to beat Ganon.'
@@ -2446,23 +2476,26 @@ def hint_text(dest, ped_hint=False):
tt['sahasrahla_quest_have_master_sword'] = Sahasrahla2_texts[local_random.randint(0, len(Sahasrahla2_texts) - 1)]
tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)]
- if world.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']:
+ triforce_pieces_required = max(0, w.treasure_hunt_required -
+ sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))
+
+ if world.goal[player] in ['triforce_hunt', 'local_triforce_hunt']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
- if world.goal[player] == 'triforcehunt' and world.players > 1:
+ if world.goal[player] == 'triforce_hunt' and world.players > 1:
tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!'
else:
tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!'
- if world.treasure_hunt_count[player] > 1:
+ if triforce_pieces_required > 1:
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
"hidden in a hollow tree. If you bring\n%d Triforce pieces out of %d, I can reassemble it." % \
- (world.treasure_hunt_count[player], world.triforce_pieces_available[player])
+ (triforce_pieces_required, w.treasure_hunt_total)
else:
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
"hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \
- (world.treasure_hunt_count[player], world.triforce_pieces_available[player])
+ (triforce_pieces_required, w.treasure_hunt_total)
elif world.goal[player] in ['pedestal']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
@@ -2471,20 +2504,20 @@ def hint_text(dest, ped_hint=False):
tt['ganon_fall_in'] = Ganon1_texts[local_random.randint(0, len(Ganon1_texts) - 1)]
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
- if world.treasure_hunt_count[player] > 1:
- if world.goal[player] == 'ganontriforcehunt' and world.players > 1:
+ if triforce_pieces_required > 1:
+ if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1:
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \
- (world.treasure_hunt_count[player], world.triforce_pieces_available[player])
- elif world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']:
+ (triforce_pieces_required, w.treasure_hunt_total)
+ elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \
- (world.treasure_hunt_count[player], world.triforce_pieces_available[player])
+ (triforce_pieces_required, w.treasure_hunt_total)
else:
- if world.goal[player] == 'ganontriforcehunt' and world.players > 1:
+ if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1:
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \
- (world.treasure_hunt_count[player], world.triforce_pieces_available[player])
- elif world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']:
+ (triforce_pieces_required, w.treasure_hunt_total)
+ elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \
- (world.treasure_hunt_count[player], world.triforce_pieces_available[player])
+ (triforce_pieces_required, w.treasure_hunt_total)
tt['kakariko_tavern_fisherman'] = TavernMan_texts[local_random.randint(0, len(TavernMan_texts) - 1)]
@@ -2509,12 +2542,12 @@ def hint_text(dest, ped_hint=False):
tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}"
tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}"
- for at, text in world.plando_texts[player].items():
+ for at, text, _ in world.plando_texts[player]:
if at not in tt:
raise Exception(f"No text target \"{at}\" found.")
else:
- tt[at] = text
+ tt[at] = "\n".join(text)
rom.write_bytes(0xE0000, tt.getBytes())
@@ -2582,12 +2615,12 @@ def set_inverted_mode(world, player, rom):
rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof
# the following bytes should only be written in vanilla
# or they'll overwrite the randomizer's shuffles
- if world.shuffle[player] == 'vanilla':
+ if world.entrance_shuffle[player] == 'vanilla':
rom.write_byte(0xDBB73 + 0x23, 0x37) # switch AT and GT
rom.write_byte(0xDBB73 + 0x36, 0x24)
rom.write_int16(0x15AEE + 2 * 0x38, 0x00E0)
rom.write_int16(0x15AEE + 2 * 0x25, 0x000C)
- if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
+ if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(0x15B8C, 0x6C)
rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house
rom.write_byte(0xDBB73 + 0x52, 0x01)
@@ -2645,7 +2678,7 @@ def set_inverted_mode(world, player, rom):
rom.write_int16(snes_to_pc(0x02D9A6), 0x005A)
rom.write_byte(snes_to_pc(0x02D9B3), 0x12)
# keep the old man spawn point at old man house unless shuffle is vanilla
- if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple', 'dungeonscrossed']:
+ if world.entrance_shuffle[player] in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01])
rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1)
rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03])
@@ -2708,7 +2741,7 @@ def set_inverted_mode(world, player, rom):
rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B])
rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance
rom.write_int16(snes_to_pc(0x308320), 0x001B)
- if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
+ if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(snes_to_pc(0x308340), 0x7B)
rom.write_int16(snes_to_pc(0x1af504), 0x148B)
rom.write_int16(snes_to_pc(0x1af50c), 0x149B)
@@ -2745,10 +2778,10 @@ def set_inverted_mode(world, player, rom):
rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82])
rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door
rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4)
- if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
+ if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(0xDBB73 + 0x35, 0x36)
rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp
- if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
+ if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area
rom.write_byte(0x15B8C + 0x37, 0x1B)
rom.write_int16(0x15BDB + 2 * 0x37, 0x0418)
@@ -2992,7 +3025,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
def get_base_rom_path(file_name: str = "") -> str:
- options = Utils.get_options()
+ options = Utils.get_settings()
if not file_name:
file_name = options["lttp_options"]["rom_file"]
if not os.path.exists(file_name):
diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py
index ce4a941ead1b..f596749ae669 100644
--- a/worlds/alttp/Rules.py
+++ b/worlds/alttp/Rules.py
@@ -2,51 +2,52 @@
import logging
from typing import Iterator, Set
+from Options import ItemsAccessibility
from BaseClasses import Entrance, MultiWorld
from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item,
item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items)
from . import OverworldGlitchRules
from .Bosses import GanonDefeatRule
-from .Items import ItemFactory, item_name_groups, item_table, progression_items
-from .Options import smallkey_shuffle
-from .OverworldGlitchRules import no_logic_rules, overworld_glitches_rules
+from .Items import item_factory, item_name_groups, item_table, progression_items
+from .Options import small_key_shuffle
+from .OverworldGlitchRules import overworld_glitches_rules
from .Regions import LTTPRegionType, location_table
from .StateHelpers import (can_extend_magic, can_kill_most_things,
can_lift_heavy_rocks, can_lift_rocks,
can_melt_things, can_retrieve_tablet,
can_shoot_arrows, has_beam_sword, has_crystals,
- has_fire_source, has_hearts,
+ has_fire_source, has_hearts, has_melee_weapon,
has_misery_mire_medallion, has_sword, has_turtle_rock_medallion,
- has_triforce_pieces)
+ has_triforce_pieces, can_use_bombs, can_bomb_or_bonk,
+ can_activate_crystal_switch)
from .UnderworldGlitchRules import underworld_glitches_rules
def set_rules(world):
player = world.player
world = world.multiworld
- if world.logic[player] == 'nologic':
+ if world.glitches_required[player] == 'no_logic':
if player == next(player_id for player_id in world.get_game_players("A Link to the Past")
- if world.logic[player_id] == 'nologic'): # only warn one time
+ if world.glitches_required[player_id] == 'no_logic'): # only warn one time
logging.info(
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
if world.players == 1:
- no_logic_rules(world, player)
for exit in world.get_region('Menu', player).exits:
exit.hide_path = True
return
else:
# Set access rules according to max glitches for multiworld progression.
# Set accessibility to none, and shuffle assuming the no logic players can always win
- world.accessibility[player] = world.accessibility[player].from_text("minimal")
+ world.accessibility[player].value = ItemsAccessibility.option_minimal
world.progression_balancing[player].value = 0
else:
world.completion_condition[player] = lambda state: state.has('Triforce', player)
- global_rules(world, player)
dungeon_boss_rules(world, player)
+ global_rules(world, player)
if world.mode[player] != 'inverted':
default_rules(world, player)
@@ -61,24 +62,28 @@ def set_rules(world):
else:
raise NotImplementedError(f'World state {world.mode[player]} is not implemented yet')
- if world.logic[player] == 'noglitches':
+ if world.glitches_required[player] == 'no_glitches':
no_glitches_rules(world, player)
- elif world.logic[player] == 'owglitches':
+ forbid_bomb_jump_requirements(world, player)
+ elif world.glitches_required[player] == 'overworld_glitches':
# Initially setting no_glitches_rules to set the baseline rules for some
# entrances. The overworld_glitches_rules set is primarily additive.
no_glitches_rules(world, player)
fake_flipper_rules(world, player)
overworld_glitches_rules(world, player)
- elif world.logic[player] in ['hybridglitches', 'nologic']:
+ forbid_bomb_jump_requirements(world, player)
+ elif world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']:
no_glitches_rules(world, player)
fake_flipper_rules(world, player)
overworld_glitches_rules(world, player)
underworld_glitches_rules(world, player)
- elif world.logic[player] == 'minorglitches':
+ bomb_jump_requirements(world, player)
+ elif world.glitches_required[player] == 'minor_glitches':
no_glitches_rules(world, player)
fake_flipper_rules(world, player)
+ forbid_bomb_jump_requirements(world, player)
else:
- raise NotImplementedError(f'Not implemented yet: Logic - {world.logic[player]}')
+ raise NotImplementedError(f'Not implemented yet: Logic - {world.glitches_required[player]}')
if world.goal[player] == 'bosses':
# require all bosses to beat ganon
@@ -89,7 +94,7 @@ def set_rules(world):
if world.mode[player] != 'inverted':
set_big_bomb_rules(world, player)
- if world.logic[player] in {'owglitches', 'hybridglitches', 'nologic'} and world.shuffle[player] not in {'insanity', 'insanity_legacy', 'madness'}:
+ if world.glitches_required[player].current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.entrance_shuffle[player].current_key not in {'insanity', 'insanity_legacy', 'madness'}:
path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player)
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.multiworld.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or')
else:
@@ -97,18 +102,18 @@ def set_rules(world):
# if swamp and dam have not been moved we require mirror for swamp palace
# however there is mirrorless swamp in hybrid MG, so we don't necessarily want this. HMG handles this requirement itself.
- if not world.swamp_patch_required[player] and world.logic[player] not in ['hybridglitches', 'nologic']:
+ if not world.worlds[player].swamp_patch_required and world.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
# GT Entrance may be required for Turtle Rock for OWG and < 7 required
ganons_tower = world.get_entrance('Inverted Ganons Tower' if world.mode[player] == 'inverted' else 'Ganons Tower', player)
- if world.crystals_needed_for_gt[player] == 7 and not (world.logic[player] in ['owglitches', 'hybridglitches', 'nologic'] and world.mode[player] != 'inverted'):
+ if world.crystals_needed_for_gt[player] == 7 and not (world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and world.mode[player] != 'inverted'):
set_rule(ganons_tower, lambda state: False)
set_trock_key_rules(world, player)
set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_gt[player], player))
- if world.mode[player] != 'inverted' and world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']:
+ if world.mode[player] != 'inverted' and world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or')
set_bunny_rules(world, player, world.mode[player] == 'inverted')
@@ -136,7 +141,9 @@ def mirrorless_path_to_castle_courtyard(world, player):
def set_defeat_dungeon_boss_rule(location):
# Lambda required to defer evaluation of dungeon.boss since it will change later if boss shuffle is used
- set_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state))
+ add_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state))
+
+
def set_always_allow(spot, rule):
spot.always_allow = rule
@@ -183,253 +190,436 @@ def dungeon_boss_rules(world, player):
for location in boss_locations:
set_defeat_dungeon_boss_rule(world.get_location(location, player))
-def global_rules(world, player):
+
+def global_rules(multiworld: MultiWorld, player: int):
+ world = multiworld.worlds[player]
# ganon can only carry triforce
- add_item_rule(world.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player)
+ add_item_rule(multiworld.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player)
# dungeon prizes can only be crystals/pendants
crystals_and_pendants: Set[str] = \
{item for item, item_data in item_table.items() if item_data.type == "Crystal"}
prize_locations: Iterator[str] = \
(locations for locations, location_data in location_table.items() if location_data[2] == True)
for prize_location in prize_locations:
- add_item_rule(world.get_location(prize_location, player),
+ add_item_rule(multiworld.get_location(prize_location, player),
lambda item: item.name in crystals_and_pendants and item.player == player)
# determines which S&Q locations are available - hide from paths since it isn't an in-game location
- for exit in world.get_region('Menu', player).exits:
+ for exit in multiworld.get_region('Menu', player).exits:
exit.hide_path = True
+ try:
+ old_man_sq = multiworld.get_entrance('Old Man S&Q', player)
+ except KeyError:
+ pass # it doesn't exist, should be dungeon-only unittests
+ else:
+ old_man = multiworld.get_location("Old Man", player)
+ set_rule(old_man_sq, lambda state: old_man.can_reach(state))
- set_rule(world.get_entrance('Old Man S&Q', player), lambda state: state.can_reach('Old Man', 'Location', player))
-
- set_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player))
- set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player))
- set_rule(world.get_location('Purple Chest', player),
+ set_rule(multiworld.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player))
+ set_rule(multiworld.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player))
+ set_rule(multiworld.get_location('Purple Chest', player),
lambda state: state.has('Pick Up Purple Chest', player)) # Can S&Q with chest
- set_rule(world.get_location('Ether Tablet', player), lambda state: can_retrieve_tablet(state, player))
- set_rule(world.get_location('Master Sword Pedestal', player), lambda state: state.has('Red Pendant', player) and state.has('Blue Pendant', player) and state.has('Green Pendant', player))
-
- set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith
- set_rule(world.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player))
- set_rule(world.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player))
- set_rule(world.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player))
- set_rule(world.get_location('Library', player), lambda state: state.has('Pegasus Boots', player))
- set_rule(world.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player))
- set_rule(world.get_location('Sahasrahla', player), lambda state: state.has('Green Pendant', player))
-
-
- set_rule(world.get_location('Spike Cave', player), lambda state:
+ set_rule(multiworld.get_location('Ether Tablet', player), lambda state: can_retrieve_tablet(state, player))
+ set_rule(multiworld.get_location('Master Sword Pedestal', player), lambda state: state.has('Red Pendant', player) and state.has('Blue Pendant', player) and state.has('Green Pendant', player))
+
+ set_rule(multiworld.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith
+ set_rule(multiworld.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player))
+ set_rule(multiworld.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player))
+ set_rule(multiworld.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player))
+ set_rule(multiworld.get_location('Library', player), lambda state: state.has('Pegasus Boots', player))
+
+ if multiworld.enemy_shuffle[player]:
+ set_rule(multiworld.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player) and
+ can_kill_most_things(state, player, 4))
+ else:
+ set_rule(multiworld.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player)
+ and ((state.multiworld.enemy_health[player] in ("easy", "default") and can_use_bombs(state, player, 4))
+ or can_shoot_arrows(state, player) or state.has("Cane of Somaria", player)
+ or has_beam_sword(state, player)))
+
+ set_rule(multiworld.get_location('Sahasrahla', player), lambda state: state.has('Green Pendant', player))
+
+ set_rule(multiworld.get_location('Aginah\'s Cave', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_location('Blind\'s Hideout - Top', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_location('Chicken House', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_location('Kakariko Well - Top', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_location('Graveyard Cave', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_location('Sahasrahla\'s Hut - Left', player), lambda state: can_bomb_or_bonk(state, player))
+ set_rule(multiworld.get_location('Sahasrahla\'s Hut - Middle', player), lambda state: can_bomb_or_bonk(state, player))
+ set_rule(multiworld.get_location('Sahasrahla\'s Hut - Right', player), lambda state: can_bomb_or_bonk(state, player))
+ set_rule(multiworld.get_location('Paradox Cave Lower - Left', player), lambda state: can_use_bombs(state, player)
+ or has_beam_sword(state, player) or can_shoot_arrows(state, player)
+ or state.has_any(["Fire Rod", "Cane of Somaria"], player))
+ set_rule(multiworld.get_location('Paradox Cave Lower - Right', player), lambda state: can_use_bombs(state, player)
+ or has_beam_sword(state, player) or can_shoot_arrows(state, player)
+ or state.has_any(["Fire Rod", "Cane of Somaria"], player))
+ set_rule(multiworld.get_location('Paradox Cave Lower - Far Right', player), lambda state: can_use_bombs(state, player)
+ or has_beam_sword(state, player) or can_shoot_arrows(state, player)
+ or state.has_any(["Fire Rod", "Cane of Somaria"], player))
+ set_rule(multiworld.get_location('Paradox Cave Lower - Middle', player), lambda state: can_use_bombs(state, player)
+ or has_beam_sword(state, player) or can_shoot_arrows(state, player)
+ or state.has_any(["Fire Rod", "Cane of Somaria"], player))
+ set_rule(multiworld.get_location('Paradox Cave Lower - Far Left', player), lambda state: can_use_bombs(state, player)
+ or has_beam_sword(state, player) or can_shoot_arrows(state, player)
+ or state.has_any(["Fire Rod", "Cane of Somaria"], player))
+ set_rule(multiworld.get_location('Paradox Cave Upper - Left', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_location('Paradox Cave Upper - Right', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_location('Mini Moldorm Cave - Far Left', player), lambda state: can_kill_most_things(state, player, 4))
+ set_rule(multiworld.get_location('Mini Moldorm Cave - Left', player), lambda state: can_kill_most_things(state, player, 4))
+ set_rule(multiworld.get_location('Mini Moldorm Cave - Far Right', player), lambda state: can_kill_most_things(state, player, 4))
+ set_rule(multiworld.get_location('Mini Moldorm Cave - Right', player), lambda state: can_kill_most_things(state, player, 4))
+ set_rule(multiworld.get_location('Mini Moldorm Cave - Generous Guy', player), lambda state: can_kill_most_things(state, player, 4))
+ set_rule(multiworld.get_location('Hype Cave - Bottom', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_location('Hype Cave - Middle Left', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_location('Hype Cave - Middle Right', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_location('Hype Cave - Top', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Light World Death Mountain Shop', player), lambda state: can_use_bombs(state, player))
+
+ set_rule(multiworld.get_entrance('Two Brothers House Exit (West)', player), lambda state: can_bomb_or_bonk(state, player))
+ set_rule(multiworld.get_entrance('Two Brothers House Exit (East)', player), lambda state: can_bomb_or_bonk(state, player))
+
+ set_rule(multiworld.get_location('Spike Cave', player), lambda state:
state.has('Hammer', player) and can_lift_rocks(state, player) and
((state.has('Cape', player) and can_extend_magic(state, player, 16, True)) or
(state.has('Cane of Byrna', player) and
(can_extend_magic(state, player, 12, True) or
- (state.multiworld.can_take_damage[player] and (state.has('Pegasus Boots', player) or has_hearts(state, player, 4))))))
+ (world.can_take_damage and (state.has('Pegasus Boots', player) or has_hearts(state, player, 4))))))
)
- set_rule(world.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player))
- set_rule(world.get_location('Hookshot Cave - Top Left', player), lambda state: state.has('Hookshot', player))
- set_rule(world.get_location('Hookshot Cave - Bottom Right', player),
+ set_rule(multiworld.get_entrance('Hookshot Cave Bomb Wall (North)', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Hookshot Cave Bomb Wall (South)', player), lambda state: can_use_bombs(state, player))
+
+ set_rule(multiworld.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player))
+ set_rule(multiworld.get_location('Hookshot Cave - Top Left', player), lambda state: state.has('Hookshot', player))
+ set_rule(multiworld.get_location('Hookshot Cave - Bottom Right', player),
lambda state: state.has('Hookshot', player) or state.has('Pegasus Boots', player))
- set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player))
+ set_rule(multiworld.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player))
- set_rule(world.get_entrance('Sewers Door', player),
- lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player) or (
- world.smallkey_shuffle[player] == smallkey_shuffle.option_universal and world.mode[
+ set_rule(multiworld.get_location('Hyrule Castle - Map Guard Key Drop', player),
+ lambda state: can_kill_most_things(state, player, 1))
+
+ set_rule(multiworld.get_entrance('Sewers Door', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) or (
+ multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal and multiworld.mode[
player] == 'standard')) # standard universal small keys cannot access the shop
- set_rule(world.get_entrance('Sewers Back Door', player),
- lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
- set_rule(world.get_entrance('Agahnim 1', player),
- lambda state: has_sword(state, player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
-
- set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: can_kill_most_things(state, player, 8))
- set_rule(world.get_location('Castle Tower - Dark Maze', player),
- lambda state: can_kill_most_things(state, player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)',
- player))
+ set_rule(multiworld.get_entrance('Sewers Back Door', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4))
+ set_rule(multiworld.get_entrance('Sewers Secret Room', player), lambda state: can_bomb_or_bonk(state, player))
+
+ set_rule(multiworld.get_entrance('Agahnim 1', player),
+ lambda state: has_sword(state, player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 4))
- set_rule(world.get_location('Eastern Palace - Big Chest', player),
+ set_rule(multiworld.get_location('Castle Tower - Room 03', player), lambda state: can_kill_most_things(state, player, 4))
+ set_rule(multiworld.get_location('Castle Tower - Dark Maze', player),
+ lambda state: can_kill_most_things(state, player, 4) and state._lttp_has_key('Small Key (Agahnims Tower)',
+ player))
+ set_rule(multiworld.get_location('Castle Tower - Dark Archer Key Drop', player),
+ lambda state: can_kill_most_things(state, player, 4) and state._lttp_has_key('Small Key (Agahnims Tower)',
+ player, 2))
+ set_rule(multiworld.get_location('Castle Tower - Circle of Pots Key Drop', player),
+ lambda state: can_kill_most_things(state, player, 4) and state._lttp_has_key('Small Key (Agahnims Tower)',
+ player, 3))
+ set_always_allow(multiworld.get_location('Eastern Palace - Big Key Chest', player),
+ lambda state, item: item.name == 'Big Key (Eastern Palace)' and item.player == player)
+ set_rule(multiworld.get_location('Eastern Palace - Big Key Chest', player),
+ lambda state: can_kill_most_things(state, player, 5) and (state._lttp_has_key('Small Key (Eastern Palace)',
+ player, 2) or ((location_item_name(state, 'Eastern Palace - Big Key Chest', player)
+ == ('Big Key (Eastern Palace)', player) and state.has('Small Key (Eastern Palace)',
+ player)))))
+ set_rule(multiworld.get_location('Eastern Palace - Dark Eyegore Key Drop', player),
+ lambda state: state.has('Big Key (Eastern Palace)', player) and can_kill_most_things(state, player, 1))
+ set_rule(multiworld.get_location('Eastern Palace - Big Chest', player),
lambda state: state.has('Big Key (Eastern Palace)', player))
- ep_boss = world.get_location('Eastern Palace - Boss', player)
- set_rule(ep_boss, lambda state: state.has('Big Key (Eastern Palace)', player) and
+ # not bothering to check for can_kill_most_things in the rooms leading to boss, as if you can kill a boss you should
+ # be able to get through these rooms
+ ep_boss = multiworld.get_location('Eastern Palace - Boss', player)
+ add_rule(ep_boss, lambda state: state.has('Big Key (Eastern Palace)', player) and
+ state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and
ep_boss.parent_region.dungeon.boss.can_defeat(state))
- ep_prize = world.get_location('Eastern Palace - Prize', player)
- set_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and
+ ep_prize = multiworld.get_location('Eastern Palace - Prize', player)
+ add_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and
+ state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and
ep_prize.parent_region.dungeon.boss.can_defeat(state))
- if not world.enemy_shuffle[player]:
+ if not multiworld.enemy_shuffle[player]:
add_rule(ep_boss, lambda state: can_shoot_arrows(state, player))
add_rule(ep_prize, lambda state: can_shoot_arrows(state, player))
- set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player))
- set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player))
- set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player))
- set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
- set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
+ # You can always kill the Stalfos' with the pots on easy/normal
+ if multiworld.enemy_health[player] in ("hard", "expert") or multiworld.enemy_shuffle[player]:
+ stalfos_rule = lambda state: can_kill_most_things(state, player, 4)
+ for location in ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
+ 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop',
+ 'Eastern Palace - Big Key Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize']:
+ add_rule(multiworld.get_location(location, player), stalfos_rule)
+
+ set_rule(multiworld.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player))
+ set_rule(multiworld.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player))
+
+ set_rule(multiworld.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4))
+ set_rule(multiworld.get_location('Desert Palace - Big Key Chest', player), lambda state: can_kill_most_things(state, player, 3))
+ set_rule(multiworld.get_location('Desert Palace - Beamos Hall Pot Key', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 2) and can_kill_most_things(state, player, 4))
+ set_rule(multiworld.get_location('Desert Palace - Desert Tiles 2 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 3) and can_kill_most_things(state, player, 4))
+ add_rule(multiworld.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
+ add_rule(multiworld.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
# logic patch to prevent placing a crystal in Desert that's required to reach the required keys
- if not (world.smallkey_shuffle[player] and world.bigkey_shuffle[player]):
- add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state))
-
- set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or location_item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player))
- set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player))
- set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
- set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player))
- if world.accessibility[player] != 'locations':
- set_always_allow(world.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
-
- set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
- set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player))
- set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player))
- set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
- if world.accessibility[player] != 'locations':
- allow_self_locking_items(world.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
- set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player))
- if not world.smallkey_shuffle[player] and world.logic[player] not in ['hybridglitches', 'nologic']:
- forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
-
- set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
- set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
- set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player)) and state.has('Hammer', player))
- if world.accessibility[player] != 'locations':
- allow_self_locking_items(world.get_location('Thieves\' Town - Big Chest', player), 'Small Key (Thieves Town)')
- set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
-
- set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
- set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
- set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section
- set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2))
- set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player))
- if world.accessibility[player] != 'locations':
- allow_self_locking_items(world.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
- set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
-
- set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: can_melt_things(state, player))
- set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player))
- set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1))))
- set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or (
- item_name_in_location_names(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
- set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player))
-
- set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (has_sword(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or can_shoot_arrows(state, player))) # need to defeat wizzrobes, bombs don't work ...
- set_rule(world.get_location('Misery Mire - Big Chest', player), lambda state: state.has('Big Key (Misery Mire)', player))
- set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.multiworld.can_take_damage[player] and has_hearts(state, player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
- set_rule(world.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player))
- # you can squander the free small key from the pot by opening the south door to the north west switch room, locking you out of accessing a color switch ...
- # big key gives backdoor access to that from the teleporter in the north west
- set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state.has('Big Key (Misery Mire)', player))
- set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state._lttp_has_key('Big Key (Misery Mire)', player))
+ if not (multiworld.small_key_shuffle[player] and multiworld.big_key_shuffle[player]):
+ add_rule(multiworld.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state))
+
+ set_rule(multiworld.get_location('Tower of Hera - Basement Cage', player), lambda state: can_activate_crystal_switch(state, player))
+ set_rule(multiworld.get_location('Tower of Hera - Map Chest', player), lambda state: can_activate_crystal_switch(state, player))
+ set_rule(multiworld.get_entrance('Tower of Hera Small Key Door', player), lambda state: can_activate_crystal_switch(state, player) and (state._lttp_has_key('Small Key (Tower of Hera)', player) or location_item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player)))
+ set_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_activate_crystal_switch(state, player) and state.has('Big Key (Tower of Hera)', player))
+ if multiworld.enemy_shuffle[player]:
+ add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_kill_most_things(state, player, 3))
+ else:
+ add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player),
+ lambda state: (has_melee_weapon(state, player) or (state.has('Silver Bow', player)
+ and can_shoot_arrows(state, player)) or state.has("Cane of Byrna", player)
+ or state.has("Cane of Somaria", player)))
+ set_rule(multiworld.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
+ set_rule(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player))
+ if multiworld.accessibility[player] != 'full':
+ set_always_allow(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
+
+ set_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
+ set_rule(multiworld.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player))
+ set_rule(multiworld.get_location('Swamp Palace - Map Chest', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_location('Swamp Palace - Trench 1 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 2))
+ set_rule(multiworld.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 3))
+ set_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: state.has('Hookshot', player))
+ if multiworld.pot_shuffle[player]:
+ # it could move the key to the top right platform which can only be reached with bombs
+ add_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Swamp Palace (West)', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)
+ if state.has('Hookshot', player)
+ else state._lttp_has_key('Small Key (Swamp Palace)', player, 4))
+ set_rule(multiworld.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
+ if multiworld.accessibility[player] != 'full':
+ allow_self_locking_items(multiworld.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
+ set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5))
+ if not multiworld.small_key_shuffle[player] and multiworld.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
+ forbid_item(multiworld.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
+ add_rule(multiworld.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
+ add_rule(multiworld.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
+ if multiworld.pot_shuffle[player]:
+ # key can (and probably will) be moved behind bombable wall
+ set_rule(multiworld.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player))
+
+ set_rule(multiworld.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
+ if multiworld.worlds[player].dungeons["Thieves Town"].boss.enemizer_name == "Blind":
+ set_rule(multiworld.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3) and can_use_bombs(state, player))
+ set_rule(multiworld.get_location('Thieves\' Town - Big Chest', player),
+ lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
+ set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player),
+ lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
+ if multiworld.accessibility[player] != 'locations' and not multiworld.key_drop_shuffle[player]:
+ set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
+ set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
+ set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
+ lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
+
+ # We need so many keys in the SW doors because they are all reachable as the last door (except for the door to mothula)
+ set_rule(multiworld.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
+ set_rule(multiworld.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
+ set_rule(multiworld.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
+ set_rule(multiworld.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
+ set_rule(multiworld.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player))
+ if multiworld.accessibility[player] != 'full':
+ allow_self_locking_items(multiworld.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
+ set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
+ add_rule(multiworld.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
+ add_rule(multiworld.get_location('Skull Woods - Boss', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
+
+ set_rule(multiworld.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_melt_things(state, player))
+ set_rule(multiworld.get_location('Ice Palace - Compass Chest', player), lambda state: can_melt_things(state, player) and state._lttp_has_key('Small Key (Ice Palace)', player))
+ set_rule(multiworld.get_entrance('Ice Palace (Second Section)', player), lambda state: can_melt_things(state, player) and state._lttp_has_key('Small Key (Ice Palace)', player) and can_use_bombs(state, player))
+
+ set_rule(multiworld.get_entrance('Ice Palace (Main)', player), lambda state: state._lttp_has_key('Small Key (Ice Palace)', player, 2))
+ set_rule(multiworld.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player))
+ set_rule(multiworld.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 6) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 5))))
+ # This is a complicated rule, so let's break it down.
+ # Hookshot always suffices to get to the right side.
+ # Also, once you get over there, you have to cross the spikes, so that's the last line.
+ # Alternatively, we could not have hookshot. Then we open the keydoor into right side in order to get there.
+ # This is conditional on whether we have the big key or not, as big key opens the ability to waste more keys.
+ # Specifically, if we have big key we can burn 2 extra keys near the boss and will need +2 keys. That's all of them as this could be the last door.
+ # Hence if big key is available then it's 6 keys, otherwise 4 keys.
+ # If key_drop is off, then we have 3 drop keys available, and can never satisfy the 6 key requirement because one key is on right side,
+ # so this reduces perfectly to original logic.
+ set_rule(multiworld.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or
+ (state._lttp_has_key('Small Key (Ice Palace)', player, 4)
+ if item_name_in_location_names(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player),
+ ('Ice Palace - Hammer Block Key Drop', player),
+ ('Ice Palace - Big Key Chest', player),
+ ('Ice Palace - Map Chest', player)])
+ else state._lttp_has_key('Small Key (Ice Palace)', player, 6))) and (
+ world.can_take_damage or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
+ set_rule(multiworld.get_entrance('Ice Palace (East Top)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player))
+
+ set_rule(multiworld.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (has_sword(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or can_shoot_arrows(state, player))) # need to defeat wizzrobes, bombs don't work ...
+ set_rule(multiworld.get_location('Misery Mire - Fishbone Pot Key', player), lambda state: state.has('Big Key (Misery Mire)', player) or state._lttp_has_key('Small Key (Misery Mire)', player, 4))
+
+ set_rule(multiworld.get_location('Misery Mire - Big Chest', player), lambda state: state.has('Big Key (Misery Mire)', player))
+ set_rule(multiworld.get_location('Misery Mire - Spike Chest', player), lambda state: (world.can_take_damage and has_hearts(state, player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
+ set_rule(multiworld.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player))
+ # How to access crystal switch:
+ # If have big key: then you will need 2 small keys to be able to hit switch and return to main area, as you can burn key in dark room
+ # If not big key: cannot burn key in dark room, hence need only 1 key. all doors immediately available lead to a crystal switch.
+ # The listed chests are those which can be reached if you can reach a crystal switch.
+ set_rule(multiworld.get_location('Misery Mire - Map Chest', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2))
+ set_rule(multiworld.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2))
# we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet
- set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2) if ((
- location_item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or
- (
- location_item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3))
- set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: has_fire_source(state, player))
- set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: has_fire_source(state, player))
- set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player))
-
- set_rule(world.get_entrance('Turtle Rock Entrance Gap', player), lambda state: state.has('Cane of Somaria', player))
- set_rule(world.get_entrance('Turtle Rock Entrance Gap Reverse', player), lambda state: state.has('Cane of Somaria', player))
- set_rule(world.get_location('Turtle Rock - Compass Chest', player), lambda state: state.has('Cane of Somaria', player)) # We could get here from the middle section without Cane as we don't cross the entrance gap!
- set_rule(world.get_location('Turtle Rock - Roller Room - Left', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
- set_rule(world.get_location('Turtle Rock - Roller Room - Right', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
- set_rule(world.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player)))
- set_rule(world.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
- set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player))
- set_rule(world.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player))
- set_rule(world.get_entrance('Turtle Rock (Dark Room) (South)', player), lambda state: state.has('Cane of Somaria', player))
- set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
- set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
- set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
- set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
- set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
-
- if not world.enemy_shuffle[player]:
- set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_shoot_arrows(state, player))
- set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player))
- set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area
- set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and can_shoot_arrows(state, player) and state.has('Hammer', player))
- set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))
- set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player))
-
- set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
- location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3)))
- if world.accessibility[player] != 'locations':
- set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
-
- set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
+ set_rule(multiworld.get_location('Misery Mire - Conveyor Crystal Key Drop', player),
+ lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 4)
+ if location_item_name(state, 'Misery Mire - Compass Chest', player) == ('Big Key (Misery Mire)', player) or location_item_name(state, 'Misery Mire - Big Key Chest', player) == ('Big Key (Misery Mire)', player) or location_item_name(state, 'Misery Mire - Conveyor Crystal Key Drop', player) == ('Big Key (Misery Mire)', player)
+ else state._lttp_has_key('Small Key (Misery Mire)', player, 5))
+ set_rule(multiworld.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 5)
+ if ((location_item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or (location_item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)]))
+ else state._lttp_has_key('Small Key (Misery Mire)', player, 6))
+ set_rule(multiworld.get_location('Misery Mire - Compass Chest', player), lambda state: has_fire_source(state, player))
+ set_rule(multiworld.get_location('Misery Mire - Big Key Chest', player), lambda state: has_fire_source(state, player))
+ set_rule(multiworld.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player) and can_use_bombs(state, player))
+
+ set_rule(multiworld.get_entrance('Turtle Rock Entrance Gap', player), lambda state: state.has('Cane of Somaria', player))
+ set_rule(multiworld.get_entrance('Turtle Rock Entrance Gap Reverse', player), lambda state: state.has('Cane of Somaria', player))
+ set_rule(multiworld.get_location('Turtle Rock - Pokey 1 Key Drop', player), lambda state: can_kill_most_things(state, player, 5))
+ set_rule(multiworld.get_location('Turtle Rock - Pokey 2 Key Drop', player), lambda state: can_kill_most_things(state, player, 5))
+ set_rule(multiworld.get_location('Turtle Rock - Compass Chest', player), lambda state: state.has('Cane of Somaria', player))
+ set_rule(multiworld.get_location('Turtle Rock - Roller Room - Left', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
+ set_rule(multiworld.get_location('Turtle Rock - Roller Room - Right', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
+ set_rule(multiworld.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player)))
+ set_rule(multiworld.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
+ set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10) and can_bomb_or_bonk(state, player))
+ set_rule(multiworld.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player)
+ or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player))
+ set_rule(multiworld.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player))
+ set_rule(multiworld.get_entrance('Turtle Rock (Dark Room) (South)', player), lambda state: state.has('Cane of Somaria', player))
+ set_rule(multiworld.get_location('Turtle Rock - Eye Bridge - Bottom Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
+ set_rule(multiworld.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
+ set_rule(multiworld.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
+ set_rule(multiworld.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
+ set_rule(multiworld.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
+ set_rule(multiworld.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_kill_most_things(state, player, 10))
+
+ if not multiworld.worlds[player].fix_trock_doors:
+ add_rule(multiworld.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Turtle Rock Second Section from Bomb Wall', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Turtle Rock Eye Bridge from Bomb Wall', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Turtle Rock Eye Bridge Bomb Wall', player), lambda state: can_use_bombs(state, player))
+
+ if multiworld.enemy_shuffle[player]:
+ set_rule(multiworld.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_kill_most_things(state, player, 3))
+ else:
+ set_rule(multiworld.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_shoot_arrows(state, player))
+ set_rule(multiworld.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player))
+ set_rule(multiworld.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area
+ set_rule(multiworld.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and can_shoot_arrows(state, player) and state.has('Hammer', player))
+ set_rule(multiworld.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))
+ set_rule(multiworld.get_location('Palace of Darkness - Big Chest', player), lambda state: can_use_bombs(state, player) and state.has('Big Key (Palace of Darkness)', player))
+ set_rule(multiworld.get_location('Palace of Darkness - The Arena - Ledge', player), lambda state: can_use_bombs(state, player))
+ if multiworld.pot_shuffle[player]:
+ # chest switch may be up on ledge where bombs are required
+ set_rule(multiworld.get_location('Palace of Darkness - Stalfos Basement', player), lambda state: can_use_bombs(state, player))
+
+ set_rule(multiworld.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
+ location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))))
+ if multiworld.accessibility[player] != 'full':
+ set_always_allow(multiworld.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
+
+ set_rule(multiworld.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
- if world.accessibility[player] != 'locations':
- set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
+ if multiworld.accessibility[player] != 'full':
+ set_always_allow(multiworld.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
- set_rule(world.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
+ set_rule(multiworld.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
# these key rules are conservative, you might be able to get away with more lenient rules
randomizer_room_chests = ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right']
- compass_room_chests = ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right']
-
- set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player))
- set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
- set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
- set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
- location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
- if world.accessibility[player] != 'locations':
- set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
-
- # It is possible to need more than 2 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
- # However we need to leave these at the lower values to derive that with 3 keys it is always possible to reach Bob and Ice Armos.
- set_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 2))
- # It is possible to need more than 3 keys ....
- set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3))
-
- #The actual requirements for these rooms to avoid key-lock
- set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) or ((
- item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_name_in_location_names(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 2)))
+ compass_room_chests = ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right', 'Ganons Tower - Conveyor Star Pits Pot Key']
+ back_chests = ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']
+
+ set_rule(multiworld.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player))
+ set_rule(multiworld.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
+ set_rule(multiworld.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
+ if multiworld.pot_shuffle[player]:
+ set_rule(multiworld.get_location('Ganons Tower - Conveyor Cross Pot Key', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
+ set_rule(multiworld.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
+ location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
+
+ # this seemed to be causing generation failure, disable for now
+ # if world.accessibility[player] != 'locations':
+ # set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
+
+ # It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
+ # However we need to leave these at the lower values to derive that with 7 keys it is always possible to reach Bob and Ice Armos.
+ set_rule(multiworld.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 6))
+ # It is possible to need more than 7 keys ....
+ set_rule(multiworld.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
+ item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests + back_chests, [player] * len(randomizer_room_chests + back_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5)))
+
+ # The actual requirements for these rooms to avoid key-lock
+ set_rule(multiworld.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or
+ ((item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_name_in_location_names(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5)))
for location in randomizer_room_chests:
- set_rule(world.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
- item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
-
- # Once again it is possible to need more than 3 keys...
- set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player))
+ set_rule(multiworld.get_location(location, player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
+ item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 6))))
+
+ # Once again it is possible to need more than 7 keys...
+ set_rule(multiworld.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
+ item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5))))
+ set_rule(multiworld.get_entrance('Ganons Tower (Bottom) (East)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
+ item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(back_chests, [player] * len(back_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5)))
# Actual requirements
for location in compass_room_chests:
- set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
- item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))))
-
- set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player))
-
- set_rule(world.get_location('Ganons Tower - Big Key Room - Left', player),
- lambda state: state.multiworld.get_location('Ganons Tower - Big Key Room - Left', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
- set_rule(world.get_location('Ganons Tower - Big Key Chest', player),
- lambda state: state.multiworld.get_location('Ganons Tower - Big Key Chest', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
- set_rule(world.get_location('Ganons Tower - Big Key Room - Right', player),
- lambda state: state.multiworld.get_location('Ganons Tower - Big Key Room - Right', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
- if world.enemy_shuffle[player]:
- set_rule(world.get_entrance('Ganons Tower Big Key Door', player),
+ set_rule(multiworld.get_location(location, player), lambda state: (can_use_bombs(state, player) or state.has("Cane of Somaria", player)) and state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or (
+ item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5))))
+
+ set_rule(multiworld.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player))
+
+ set_rule(multiworld.get_location('Ganons Tower - Big Key Room - Left', player),
+ lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Room - Left', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
+ set_rule(multiworld.get_location('Ganons Tower - Big Key Chest', player),
+ lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Chest', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
+ set_rule(multiworld.get_location('Ganons Tower - Big Key Room - Right', player),
+ lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Room - Right', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
+ if multiworld.enemy_shuffle[player]:
+ set_rule(multiworld.get_entrance('Ganons Tower Big Key Door', player),
lambda state: state.has('Big Key (Ganons Tower)', player))
else:
- set_rule(world.get_entrance('Ganons Tower Big Key Door', player),
+ set_rule(multiworld.get_entrance('Ganons Tower Big Key Door', player),
lambda state: state.has('Big Key (Ganons Tower)', player) and can_shoot_arrows(state, player))
- set_rule(world.get_entrance('Ganons Tower Torch Rooms', player),
- lambda state: has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
- set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player),
- lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3))
- set_rule(world.get_entrance('Ganons Tower Moldorm Door', player),
- lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4))
- set_rule(world.get_entrance('Ganons Tower Moldorm Gap', player),
+ set_rule(multiworld.get_entrance('Ganons Tower Torch Rooms', player),
+ lambda state: can_kill_most_things(state, player, 8) and has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
+ set_rule(multiworld.get_location('Ganons Tower - Mini Helmasaur Key Drop', player), lambda state: can_kill_most_things(state, player, 1))
+ set_rule(multiworld.get_location('Ganons Tower - Pre-Moldorm Chest', player),
+ lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7))
+ set_rule(multiworld.get_entrance('Ganons Tower Moldorm Door', player),
+ lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8))
+ set_rule(multiworld.get_entrance('Ganons Tower Moldorm Gap', player),
lambda state: state.has('Hookshot', player) and state.multiworld.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state))
- set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player))
- ganon = world.get_location('Ganon', player)
+ set_defeat_dungeon_boss_rule(multiworld.get_location('Agahnim 2', player))
+ ganon = multiworld.get_location('Ganon', player)
set_rule(ganon, lambda state: GanonDefeatRule(state, player))
- if world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']:
+ if multiworld.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
add_rule(ganon, lambda state: has_triforce_pieces(state, player))
- elif world.goal[player] == 'ganonpedestal':
- add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player))
+ elif multiworld.goal[player] == 'ganon_pedestal':
+ add_rule(multiworld.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player))
else:
add_rule(ganon, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_ganon[player], player))
- set_rule(world.get_entrance('Ganon Drop', player), lambda state: has_beam_sword(state, player)) # need to damage ganon to get tiles to drop
+ set_rule(multiworld.get_entrance('Ganon Drop', player), lambda state: has_beam_sword(state, player)) # need to damage ganon to get tiles to drop
- set_rule(world.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player))
+ set_rule(multiworld.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player))
def default_rules(world, player):
"""Default world rules when world state is not inverted."""
# overworld requirements
+
+ set_rule(world.get_entrance('Light World Bomb Hut', player), lambda state: can_use_bombs(state, player))
+ set_rule(world.get_entrance('Light Hype Fairy', player), lambda state: can_use_bombs(state, player))
+ set_rule(world.get_entrance('Mini Moldorm Cave', player), lambda state: can_use_bombs(state, player))
+ set_rule(world.get_entrance('Ice Rod Cave', player), lambda state: can_use_bombs(state, player))
+
set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player))
set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
@@ -485,12 +675,12 @@ def default_rules(world, player):
set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: (state.has('Moon Pearl', player) and state.has('Flippers', player) or state.has('Magic Mirror', player))) # Overworld Bunny Revival
set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player))
set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # ToDo any fake flipper set up?
- set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player)) # bomb required
+ set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player)) # bomb required
- set_rule(world.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player)) # bomb required
+ set_rule(world.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
+ set_rule(world.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
set_rule(world.get_entrance('Thieves Town', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot pull
set_rule(world.get_entrance('Skull Woods First Section Hole (North)', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush
set_rule(world.get_entrance('Skull Woods Second Section Hole', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush
@@ -544,9 +734,9 @@ def inverted_rules(world, player):
# overworld requirements
set_rule(world.get_location('Maze Race', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Mini Moldorm Cave', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Ice Rod Cave', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Light Hype Fairy', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(world.get_entrance('Mini Moldorm Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
+ set_rule(world.get_entrance('Ice Rod Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
+ set_rule(world.get_entrance('Light Hype Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
set_rule(world.get_entrance('Potion Shop Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Light World Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player))
@@ -592,7 +782,7 @@ def inverted_rules(world, player):
set_rule(world.get_entrance('Bush Covered Lawn Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Bomb Hut Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Bomb Hut Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Light World Bomb Hut', player), lambda state: state.has('Moon Pearl', player)) # need bomb
+ set_rule(world.get_entrance('Light World Bomb Hut', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
set_rule(world.get_entrance('North Fairy Cave Drop', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_entrance('Lost Woods Hideout Drop', player), lambda state: state.has('Moon Pearl', player))
set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy
@@ -638,6 +828,11 @@ def inverted_rules(world, player):
set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player))
set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player))
+ set_rule(world.get_entrance('Hype Cave', player), lambda state: can_use_bombs(state, player))
+ set_rule(world.get_entrance('Brewery', player), lambda state: can_use_bombs(state, player))
+ set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: can_use_bombs(state, player))
+
+
set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player))
set_rule(world.get_entrance('Misery Mire', player), lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!)
@@ -712,7 +907,6 @@ def no_glitches_rules(world, player):
add_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: False) # no glitches does not require block override
- forbid_bomb_jump_requirements(world, player)
add_conditional_lamps(world, player)
def fake_flipper_rules(world, player):
@@ -740,12 +934,20 @@ def fake_flipper_rules(world, player):
set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player))
-def forbid_bomb_jump_requirements(world, player):
+def bomb_jump_requirements(multiworld, player):
+ DMs_room_chests = ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right']
+ for location in DMs_room_chests:
+ add_rule(multiworld.get_location(location, player), lambda state: can_use_bombs(state, player), combine="or")
+ set_rule(multiworld.get_entrance('Paradox Cave Bomb Jump', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Skull Woods First Section Bomb Jump', player), lambda state: can_use_bombs(state, player))
+
+
+def forbid_bomb_jump_requirements(multiworld, player):
DMs_room_chests = ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right']
for location in DMs_room_chests:
- add_rule(world.get_location(location, player), lambda state: state.has('Hookshot', player))
- set_rule(world.get_entrance('Paradox Cave Bomb Jump', player), lambda state: False)
- set_rule(world.get_entrance('Skull Woods First Section Bomb Jump', player), lambda state: False)
+ add_rule(multiworld.get_location(location, player), lambda state: state.has('Hookshot', player))
+ set_rule(multiworld.get_entrance('Paradox Cave Bomb Jump', player), lambda state: False)
+ set_rule(multiworld.get_entrance('Skull Woods First Section Bomb Jump', player), lambda state: False)
DW_Entrances = ['Bumper Cave (Bottom)',
@@ -797,15 +999,21 @@ def add_conditional_lamp(spot, region, spottype='Location', accessible_torch=Fal
if world.mode[player] != 'inverted':
add_conditional_lamp('Agahnim 1', 'Agahnims Tower', 'Entrance')
add_conditional_lamp('Castle Tower - Dark Maze', 'Agahnims Tower')
+ add_conditional_lamp('Castle Tower - Dark Archer Key Drop', 'Agahnims Tower')
+ add_conditional_lamp('Castle Tower - Circle of Pots Key Drop', 'Agahnims Tower')
else:
add_conditional_lamp('Agahnim 1', 'Inverted Agahnims Tower', 'Entrance')
add_conditional_lamp('Castle Tower - Dark Maze', 'Inverted Agahnims Tower')
+ add_conditional_lamp('Castle Tower - Dark Archer Key Drop', 'Inverted Agahnims Tower')
+ add_conditional_lamp('Castle Tower - Circle of Pots Key Drop', 'Inverted Agahnims Tower')
add_conditional_lamp('Old Man', 'Old Man Cave')
add_conditional_lamp('Old Man Cave Exit (East)', 'Old Man Cave', 'Entrance')
add_conditional_lamp('Death Mountain Return Cave Exit (East)', 'Death Mountain Return Cave', 'Entrance')
add_conditional_lamp('Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave', 'Entrance')
add_conditional_lamp('Old Man House Front to Back', 'Old Man House', 'Entrance')
add_conditional_lamp('Old Man House Back to Front', 'Old Man House', 'Entrance')
+ add_conditional_lamp('Eastern Palace - Dark Square Pot Key', 'Eastern Palace')
+ add_conditional_lamp('Eastern Palace - Dark Eyegore Key Drop', 'Eastern Palace', 'Location', True)
add_conditional_lamp('Eastern Palace - Big Key Chest', 'Eastern Palace')
add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location', True)
add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location', True)
@@ -817,17 +1025,37 @@ def add_conditional_lamp(spot, region, spottype='Location', accessible_torch=Fal
def open_rules(world, player):
- # softlock protection as you can reach the sewers small key door with a guard drop key
- set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player),
- lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
+
+ def basement_key_rule(state):
+ if location_item_name(state, 'Sewers - Key Rat Key Drop', player) == ("Small Key (Hyrule Castle)", player):
+ return state._lttp_has_key("Small Key (Hyrule Castle)", player, 2)
+ else:
+ return state._lttp_has_key("Small Key (Hyrule Castle)", player, 3)
+
+ set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
+ lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 2))
+ set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 1))
+
+ set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3) and can_kill_most_things(state, player, 1))
+
+ set_rule(world.get_location('Hyrule Castle - Big Key Drop', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) and can_kill_most_things(state, player, 1))
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
- lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4)
+ and state.has('Big Key (Hyrule Castle)', player)
+ and (world.enemy_health[player] in ("easy", "default")
+ or can_kill_most_things(state, player, 1)))
def swordless_rules(world, player):
set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or can_shoot_arrows(state, player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
- set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace
+
+ set_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player))
+ set_rule(world.get_location('Ice Palace - Compass Chest', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player))
+ set_rule(world.get_entrance('Ice Palace (Second Section)', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player))
+
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
if world.mode[player] != 'inverted':
@@ -847,14 +1075,39 @@ def add_connection(parent_name, target_name, entrance_name, world, player):
parent.exits.append(connection)
connection.connect(target)
+
def standard_rules(world, player):
add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', world, player)
world.get_entrance('Uncle S&Q', player).hide_path = True
+ set_rule(world.get_entrance('Throne Room', player), lambda state: state.can_reach('Hyrule Castle - Zelda\'s Chest', 'Location', player))
set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
set_rule(world.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
+ if world.small_key_shuffle[player] != small_key_shuffle.option_universal:
+ set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)
+ and can_kill_most_things(state, player, 2))
+ set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)
+ and can_kill_most_things(state, player, 1))
+
+ set_rule(world.get_location('Hyrule Castle - Big Key Drop', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2))
+ set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2)
+ and state.has('Big Key (Hyrule Castle)', player)
+ and (world.enemy_health[player] in ("easy", "default")
+ or can_kill_most_things(state, player, 1)))
+
+ set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
+ lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3)
+ and can_kill_most_things(state, player, 1))
+ else:
+ set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
+ lambda state: state.has('Big Key (Hyrule Castle)', player))
+
def toss_junk_item(world, player):
items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)',
'Single Arrow', 'Rupees (50)', 'Rupees (100)', 'Single Bomb', 'Bee', 'Bee Trap',
@@ -869,7 +1122,7 @@ def toss_junk_item(world, player):
def set_trock_key_rules(world, player):
# First set all relevant locked doors to impassible.
- for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Pokey Room', 'Turtle Rock Big Key Door']:
+ for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']:
set_rule(world.get_entrance(entrance, player), lambda state: False)
all_state = world.get_all_state(use_cache=False)
@@ -877,14 +1130,10 @@ def set_trock_key_rules(world, player):
all_state.stale[player] = True
# Check if each of the four main regions of the dungoen can be reached. The previous code section prevents key-costing moves within the dungeon.
- can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player)) if world.can_access_trock_eyebridge[player] is None else world.can_access_trock_eyebridge[player]
- world.can_access_trock_eyebridge[player] = can_reach_back
- can_reach_front = all_state.can_reach(world.get_region('Turtle Rock (Entrance)', player)) if world.can_access_trock_front[player] is None else world.can_access_trock_front[player]
- world.can_access_trock_front[player] = can_reach_front
- can_reach_big_chest = all_state.can_reach(world.get_region('Turtle Rock (Big Chest)', player)) if world.can_access_trock_big_chest[player] is None else world.can_access_trock_big_chest[player]
- world.can_access_trock_big_chest[player] = can_reach_big_chest
- can_reach_middle = all_state.can_reach(world.get_region('Turtle Rock (Second Section)', player)) if world.can_access_trock_middle[player] is None else world.can_access_trock_middle[player]
- world.can_access_trock_middle[player] = can_reach_middle
+ can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player))
+ can_reach_front = all_state.can_reach(world.get_region('Turtle Rock (Entrance)', player))
+ can_reach_big_chest = all_state.can_reach(world.get_region('Turtle Rock (Big Chest)', player))
+ can_reach_middle = all_state.can_reach(world.get_region('Turtle Rock (Second Section)', player))
# If you can't enter from the back, the door to the front of TR requires only 2 small keys if the big key is in one of these chests since 2 key doors are locked behind the big key door.
# If you can only enter from the middle, this includes all locations that can only be reached by exiting the front. This can include Laser Bridge and Crystaroller if the front and back connect via Dark DM Ledge!
@@ -892,6 +1141,7 @@ def set_trock_key_rules(world, player):
if can_reach_middle and not can_reach_back and not can_reach_front:
normal_regions = all_state.reachable_regions[player].copy()
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: True)
+ set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: True)
all_state.update_reachable_regions(player)
front_locked_regions = all_state.reachable_regions[player].difference(normal_regions)
front_locked_locations = set((location.name, player) for region in front_locked_regions for location in region.locations)
@@ -903,26 +1153,33 @@ def set_trock_key_rules(world, player):
# otherwise crystaroller room might not be properly marked as reachable through the back.
set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player))
- # No matter what, the key requirement for going from the middle to the bottom should be three keys.
- set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
+ # No matter what, the key requirement for going from the middle to the bottom should be five keys.
+ set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
# Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we
- # might open all the locked doors in any order so we need maximally restrictive rules.
+ # might open all the locked doors in any order, so we need maximally restrictive rules.
if can_reach_back:
- set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
- set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
- # Only consider wasting the key on the Trinexx door for going from the front entrance to middle section. If other key doors are accessible, then these doors can be avoided
- set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
- set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
+ set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 6) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
+ set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
+ set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
+
+ set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
+ set_rule(world.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
+ set_rule(world.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
else:
- # Middle to front requires 2 keys if the back is locked, otherwise 4
- set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2)
+ # Middle to front requires 3 keys if the back is locked by this door, otherwise 5
+ set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3)
+ if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations.union({('Turtle Rock - Pokey 1 Key Drop', player)}))
+ else state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
+ # Middle to front requires 4 keys if the back is locked by this door, otherwise 6
+ set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4)
if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations)
- else state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
+ else state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
- # Front to middle requires 2 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted)
- set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
- set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1))
+ # Front to middle requires 3 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted)
+ set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
+ set_rule(world.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
+ set_rule(world.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1))
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
@@ -933,31 +1190,32 @@ def tr_big_key_chest_keys_needed(state):
if item in [('Small Key (Turtle Rock)', player)]:
return 0
if item in [('Big Key (Turtle Rock)', player)]:
- return 2
- return 4
+ return 4
+ return 6
# If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential
- if not can_reach_front and not world.smallkey_shuffle[player]:
+ if not can_reach_front and not world.small_key_shuffle[player]:
# Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests
forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
if not can_reach_big_chest:
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
- if world.accessibility[player] == 'locations' and world.goal[player] != 'icerodhunt':
- if world.bigkey_shuffle[player] and can_reach_big_chest:
+ forbid_item(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
+ if world.accessibility[player] == 'full':
+ if world.big_key_shuffle[player] and can_reach_big_chest:
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
+ 'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop',
'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']:
forbid_item(world.get_location(location, player), 'Big Key (Turtle Rock)', player)
else:
# A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works
- item = ItemFactory('Small Key (Turtle Rock)', player)
+ item = item_factory('Small Key (Turtle Rock)', world.worlds[player])
location = world.get_location('Turtle Rock - Big Key Chest', player)
location.place_locked_item(item)
- location.event = True
toss_junk_item(world, player)
- if world.accessibility[player] != 'locations':
+ if world.accessibility[player] != 'full':
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
@@ -1388,8 +1646,10 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
# regions for the exits of multi-entrance caves/drops that bunny cannot pass
# Note spiral cave and two brothers house are passable in superbunny state for glitch logic with extra requirements.
- bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)', 'Turtle Rock (Entrance)', 'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Skull Woods Second Section (Drop)',
- 'Turtle Rock (Eye Bridge)', 'Sewers', 'Pyramid', 'Spiral Cave (Top)', 'Desert Palace Main (Inner)', 'Fairy Ascension Cave (Drop)']
+ bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave', 'Skull Woods First Section (Right)',
+ 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)', 'Turtle Rock (Entrance)', 'Turtle Rock (Second Section)',
+ 'Turtle Rock (Big Chest)', 'Skull Woods Second Section (Drop)', 'Turtle Rock (Eye Bridge)', 'Sewers', 'Pyramid',
+ 'Spiral Cave (Top)', 'Desert Palace Main (Inner)', 'Fairy Ascension Cave (Drop)']
bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree',
'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid',
@@ -1408,21 +1668,21 @@ def options_to_access_rule(options):
# Helper functions to determine if the moon pearl is required
if inverted:
def is_bunny(region):
- return region.is_light_world
+ return region and region.is_light_world
def is_link(region):
- return region.is_dark_world
+ return region and region.is_dark_world
else:
def is_bunny(region):
- return region.is_dark_world
+ return region and region.is_dark_world
def is_link(region):
- return region.is_light_world
+ return region and region.is_light_world
def get_rule_to_add(region, location = None, connecting_entrance = None):
# In OWG, a location can potentially be superbunny-mirror accessible or
# bunny revival accessible.
- if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic']:
+ if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
if region.name == 'Swamp Palace (Entrance)': # Need to 0hp revive - not in logic
return lambda state: state.has('Moon Pearl', player)
if region.name == 'Tower of Hera (Bottom)': # Need to hit the crystal switch
@@ -1462,7 +1722,7 @@ def get_rule_to_add(region, location = None, connecting_entrance = None):
seen.add(new_region)
if not is_link(new_region):
# For glitch rulesets, establish superbunny and revival rules.
- if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
+ if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions():
possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and has_sword(state, player))
elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions()
@@ -1485,22 +1745,21 @@ def get_rule_to_add(region, location = None, connecting_entrance = None):
return options_to_access_rule(possible_options)
# Add requirements for bunny-impassible caves if link is a bunny in them
- for region in [world.get_region(name, player) for name in bunny_impassable_caves]:
-
+ for region in (world.get_region(name, player) for name in bunny_impassable_caves):
if not is_bunny(region):
continue
rule = get_rule_to_add(region)
- for exit in region.exits:
- add_rule(exit, rule)
+ for region_exit in region.exits:
+ add_rule(region_exit, rule)
paradox_shop = world.get_region('Light World Death Mountain Shop', player)
if is_bunny(paradox_shop):
add_rule(paradox_shop.entrances[0], get_rule_to_add(paradox_shop))
# Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival
- for entrance in world.get_entrances():
- if entrance.player == player and is_bunny(entrance.connected_region):
- if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] :
+ for entrance in world.get_entrances(player):
+ if is_bunny(entrance.connected_region):
+ if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] :
if entrance.connected_region.type == LTTPRegionType.Dungeon:
if entrance.parent_region.type != LTTPRegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
add_rule(entrance, get_rule_to_add(entrance.connected_region, None, entrance))
@@ -1508,7 +1767,7 @@ def get_rule_to_add(region, location = None, connecting_entrance = None):
if entrance.connected_region.name == 'Turtle Rock (Entrance)':
add_rule(world.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance))
for location in entrance.connected_region.locations:
- if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances():
+ if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances():
continue
if location.name in bunny_accessible_locations:
continue
diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py
index f17eb1eadbca..db2b5b680c1d 100644
--- a/worlds/alttp/Shops.py
+++ b/worlds/alttp/Shops.py
@@ -5,11 +5,14 @@
from Utils import int16_as_bytes
+from worlds.generic.Rules import add_rule
+
+from BaseClasses import CollectionState
from .SubClasses import ALttPLocation
-from .EntranceShuffle import door_addresses
-from .Items import item_name_groups, item_table, ItemFactory, trap_replaceable, GetBeemizerItem
-from .Options import smallkey_shuffle
+from .Items import item_name_groups
+
+from .StateHelpers import has_hearts, can_use_bombs, can_hold_arrows
logger = logging.getLogger("Shops")
@@ -36,9 +39,9 @@ class ShopPriceType(IntEnum):
Item = 10
-class Shop():
+class Shop:
slots: int = 3 # slot count is not dynamic in asm, however inventory can have None as empty slots
- blacklist: Set[str] = set() # items that don't work, todo: actually check against this
+ blacklist: Set[str] = set() # items that don't work
type = ShopType.Shop
slot_names: Dict[int, str] = {
0: " Left",
@@ -63,6 +66,7 @@ def item_count(self) -> int:
return 0
def get_bytes(self) -> List[int]:
+ from .EntranceShuffle import door_addresses
# [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index]
entrances = self.region.entrances
config = self.item_count
@@ -103,7 +107,7 @@ def clear_inventory(self):
self.inventory = [None] * self.slots
def add_inventory(self, slot: int, item: str, price: int, max: int = 0,
- replacement: Optional[str] = None, replacement_price: int = 0, create_location: bool = False,
+ replacement: Optional[str] = None, replacement_price: int = 0,
player: int = 0, price_type: int = ShopPriceType.Rupees,
replacement_price_type: int = ShopPriceType.Rupees):
self.inventory[slot] = {
@@ -114,33 +118,23 @@ def add_inventory(self, slot: int, item: str, price: int, max: int = 0,
'replacement': replacement,
'replacement_price': replacement_price,
'replacement_price_type': replacement_price_type,
- 'create_location': create_location,
'player': player
}
def push_inventory(self, slot: int, item: str, price: int, max: int = 1, player: int = 0,
price_type: int = ShopPriceType.Rupees):
- if not self.inventory[slot]:
- raise ValueError("Inventory can't be pushed back if it doesn't exist")
-
- if not self.can_push_inventory(slot):
- logging.warning(f'Warning, there is already an item pushed into this slot.')
self.inventory[slot] = {
'item': item,
'price': price,
'price_type': price_type,
'max': max,
- 'replacement': self.inventory[slot]["item"],
- 'replacement_price': self.inventory[slot]["price"],
- 'replacement_price_type': self.inventory[slot]["price_type"],
- 'create_location': self.inventory[slot]["create_location"],
+ 'replacement': self.inventory[slot]["item"] if self.inventory[slot] else None,
+ 'replacement_price': self.inventory[slot]["price"] if self.inventory[slot] else 0,
+ 'replacement_price_type': self.inventory[slot]["price_type"] if self.inventory[slot] else ShopPriceType.Rupees,
'player': player
}
- def can_push_inventory(self, slot: int):
- return self.inventory[slot] and not self.inventory[slot]["replacement"]
-
class TakeAny(Shop):
type = ShopType.TakeAny
@@ -156,6 +150,10 @@ class UpgradeShop(Shop):
# Potions break due to VRAM flags set in UpgradeShop.
# Didn't check for more things breaking as not much else can be shuffled here currently
blacklist = item_name_groups["Potions"]
+ slot_names: Dict[int, str] = {
+ 0: " Left",
+ 1: " Right"
+ }
shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop,
@@ -163,192 +161,87 @@ class UpgradeShop(Shop):
ShopType.TakeAny: TakeAny}
-def FillDisabledShopSlots(world):
- shop_slots: Set[ALttPLocation] = {location for shop_locations in (shop.region.locations for shop in world.shops)
- for location in shop_locations
- if location.shop_slot is not None and location.shop_slot_disabled}
- for location in shop_slots:
- location.shop_slot_disabled = True
- shop: Shop = location.parent_region.shop
- location.item = ItemFactory(shop.inventory[location.shop_slot]['item'], location.player)
- location.item_rule = lambda item: item.name == location.item.name and item.player == location.player
- location.locked = True
-
-
-def ShopSlotFill(multiworld):
- shop_slots: Set[ALttPLocation] = {location for shop_locations in
- (shop.region.locations for shop in multiworld.shops if shop.type != ShopType.TakeAny)
- for location in shop_locations if location.shop_slot is not None}
+def push_shop_inventories(multiworld):
+ shop_slots = [location for shop_locations in (shop.region.locations for shop in multiworld.shops if shop.type
+ != ShopType.TakeAny) for location in shop_locations if location.shop_slot is not None]
- removed = set()
for location in shop_slots:
- shop: Shop = location.parent_region.shop
- if not shop.can_push_inventory(location.shop_slot) or location.shop_slot_disabled:
- location.shop_slot_disabled = True
- removed.add(location)
-
- if removed:
- shop_slots -= removed
-
- if shop_slots:
- logger.info("Filling LttP Shop Slots")
- del shop_slots
-
- from Fill import swap_location_item
- # TODO: allow each game to register a blacklist to be used here?
- blacklist_words = {"Rupee"}
- blacklist_words = {item_name for item_name in item_table if any(
- blacklist_word in item_name for blacklist_word in blacklist_words)}
- blacklist_words.add("Bee")
-
- locations_per_sphere = [sorted(sphere, key=lambda location: (location.name, location.player))
- for sphere in multiworld.get_spheres()]
-
- # currently special care needs to be taken so that Shop.region.locations.item is identical to Shop.inventory
- # Potentially create Locations as needed and make inventory the only source, to prevent divergence
- cumu_weights = []
- shops_per_sphere = []
- candidates_per_sphere = []
-
- # sort spheres into piles of valid candidates and shops
- for sphere in locations_per_sphere:
- current_shops_slots = []
- current_candidates = []
- shops_per_sphere.append(current_shops_slots)
- candidates_per_sphere.append(current_candidates)
- for location in sphere:
- if isinstance(location, ALttPLocation) and location.shop_slot is not None:
- if not location.shop_slot_disabled:
- current_shops_slots.append(location)
- elif not location.locked and location.item.name not in blacklist_words:
- current_candidates.append(location)
- if cumu_weights:
- x = cumu_weights[-1]
- else:
- x = 0
- cumu_weights.append(len(current_candidates) + x)
-
- multiworld.random.shuffle(current_candidates)
-
- del locations_per_sphere
-
- for i, current_shop_slots in enumerate(shops_per_sphere):
- if current_shop_slots:
- # getting all candidates and shuffling them feels cpu expensive, there may be a better method
- candidates = [(location, i) for i, candidates in enumerate(candidates_per_sphere[i:], start=i)
- for location in candidates]
- multiworld.random.shuffle(candidates)
- for location in current_shop_slots:
- shop: Shop = location.parent_region.shop
- for index, (c, swapping_sphere_id) in enumerate(candidates): # chosen item locations
- if c.item_rule(location.item) and location.item_rule(c.item):
- swap_location_item(c, location, check_locked=False)
- logger.debug(f"Swapping {c} into {location}:: {location.item}")
- # remove candidate
- candidates_per_sphere[swapping_sphere_id].remove(c)
- candidates.pop(index)
- break
-
- else:
- # This *should* never happen. But let's fail safely just in case.
- logger.warning("Ran out of ShopShuffle Item candidate locations.")
- location.shop_slot_disabled = True
- continue
-
- item_name = location.item.name
- if location.item.game != "A Link to the Past":
- if location.item.advancement:
- price = multiworld.random.randrange(8, 56)
- elif location.item.useful:
- price = multiworld.random.randrange(4, 28)
- else:
- price = multiworld.random.randrange(2, 14)
- elif any(x in item_name for x in
- ['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
- price = multiworld.random.randrange(1, 7)
- elif any(x in item_name for x in ['Arrow', 'Bomb', 'Clock']):
- price = multiworld.random.randrange(2, 14)
- elif any(x in item_name for x in ['Small Key', 'Heart']):
- price = multiworld.random.randrange(4, 28)
- else:
- price = multiworld.random.randrange(8, 56)
-
- shop.push_inventory(location.shop_slot, item_name,
- min(int(price * multiworld.shop_price_modifier[location.player] / 100) * 5, 9999), 1,
- location.item.player if location.item.player != location.player else 0)
- if 'P' in multiworld.shop_shuffle[location.player]:
- price_to_funny_price(multiworld, shop.inventory[location.shop_slot], location.player)
-
- FillDisabledShopSlots(multiworld)
-
-
-def create_shops(world, player: int):
- option = world.shop_shuffle[player]
-
+ item_name = location.item.name
+ # Retro Bow arrows will already have been pushed
+ if (not multiworld.retro_bow[location.player]) or ((item_name, location.item.player)
+ != ("Single Arrow", location.player)):
+ location.shop.push_inventory(location.shop_slot, item_name, location.shop_price,
+ 1, location.item.player if location.item.player != location.player else 0,
+ location.shop_price_type)
+ location.shop_price = location.shop.inventory[location.shop_slot]["price"] = min(location.shop_price,
+ get_price(multiworld, location.shop.inventory[location.shop_slot], location.player,
+ location.shop_price_type)[1])
+
+ for world in multiworld.get_game_worlds("A Link to the Past"):
+ world.pushed_shop_inventories.set()
+
+
+def create_shops(multiworld, player: int):
+ from .Options import RandomizeShopInventories
player_shop_table = shop_table.copy()
- if "w" in option:
+ if multiworld.include_witch_hut[player]:
player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False)
dynamic_shop_slots = total_dynamic_shop_slots + 3
else:
dynamic_shop_slots = total_dynamic_shop_slots
+ if multiworld.shuffle_capacity_upgrades[player]:
+ player_shop_table["Capacity Upgrade"] = player_shop_table["Capacity Upgrade"]._replace(locked=False)
- num_slots = min(dynamic_shop_slots, world.shop_item_slots[player])
+ num_slots = min(dynamic_shop_slots, multiworld.shop_item_slots[player])
single_purchase_slots: List[bool] = [True] * num_slots + [False] * (dynamic_shop_slots - num_slots)
- world.random.shuffle(single_purchase_slots)
+ multiworld.random.shuffle(single_purchase_slots)
- if 'g' in option or 'f' in option:
+ if multiworld.randomize_shop_inventories[player]:
default_shop_table = [i for l in
[shop_generation_types[x] for x in ['arrows', 'bombs', 'potions', 'shields', 'bottle'] if
- not world.retro_bow[player] or x != 'arrows'] for i in l]
- new_basic_shop = world.random.sample(default_shop_table, k=3)
- new_dark_shop = world.random.sample(default_shop_table, k=3)
+ not multiworld.retro_bow[player] or x != 'arrows'] for i in l]
+ new_basic_shop = multiworld.random.sample(default_shop_table, k=3)
+ new_dark_shop = multiworld.random.sample(default_shop_table, k=3)
for name, shop in player_shop_table.items():
typ, shop_id, keeper, custom, locked, items, sram_offset = shop
if not locked:
- new_items = world.random.sample(default_shop_table, k=3)
- if 'f' not in option:
+ new_items = multiworld.random.sample(default_shop_table, k=len(items))
+ if multiworld.randomize_shop_inventories[player] == RandomizeShopInventories.option_randomize_by_shop_type:
if items == _basic_shop_defaults:
new_items = new_basic_shop
elif items == _dark_world_shop_defaults:
new_items = new_dark_shop
- keeper = world.random.choice([0xA0, 0xC1, 0xFF])
+ keeper = multiworld.random.choice([0xA0, 0xC1, 0xFF])
player_shop_table[name] = ShopData(typ, shop_id, keeper, custom, locked, new_items, sram_offset)
- if world.mode[player] == "inverted":
+ if multiworld.mode[player] == "inverted":
# make sure that blue potion is available in inverted, special case locked = None; lock when done.
player_shop_table["Dark Lake Hylia Shop"] = \
player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None)
- chance_100 = int(world.retro_bow[player]) * 0.25 + int(
- world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) * 0.5
for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram_offset) in player_shop_table.items():
- region = world.get_region(region_name, player)
+ region = multiworld.get_region(region_name, player)
shop: Shop = shop_class_mapping[type](region, room_id, shopkeeper, custom, locked, sram_offset)
# special case: allow shop slots, but do not allow overwriting of base inventory behind them
if locked is None:
shop.locked = True
region.shop = shop
- world.shops.append(shop)
+ multiworld.shops.append(shop)
for index, item in enumerate(inventory):
shop.add_inventory(index, *item)
- if not locked and num_slots:
+ if not locked and (num_slots or type == ShopType.UpgradeShop):
slot_name = f"{region.name}{shop.slot_names[index]}"
loc = ALttPLocation(player, slot_name, address=shop_table_by_location[slot_name],
parent=region, hint_text="for sale")
+ loc.shop_price_type, loc.shop_price = get_price(multiworld, None, player)
+ loc.item_rule = lambda item, spot=loc: not any(i for i in price_blacklist[spot.shop_price_type] if i in item.name)
+ add_rule(loc, lambda state, spot=loc: shop_price_rules(state, player, spot))
+ loc.shop = shop
loc.shop_slot = index
- loc.locked = True
- if single_purchase_slots.pop():
- if world.goal[player] != 'icerodhunt':
- if world.random.random() < chance_100:
- additional_item = 'Rupees (100)'
- else:
- additional_item = 'Rupees (50)'
- else:
- additional_item = GetBeemizerItem(world, player, 'Nothing')
- loc.item = ItemFactory(additional_item, player)
- else:
- loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)
+ if ((not (multiworld.shuffle_capacity_upgrades[player] and type == ShopType.UpgradeShop))
+ and not single_purchase_slots.pop()):
loc.shop_slot_disabled = True
- shop.region.locations.append(loc)
- world.clear_location_cache()
+ loc.locked = True
+ else:
+ shop.region.locations.append(loc)
class ShopData(NamedTuple):
@@ -388,9 +281,10 @@ class ShopData(NamedTuple):
SHOP_ID_START = 0x400000
shop_table_by_location_id = dict(enumerate(
- (f"{name}{Shop.slot_names[num]}" for name, shop_data in
- sorted(shop_table.items(), key=lambda item: item[1].sram_offset)
- for num in range(3)), start=SHOP_ID_START))
+ (f"{name}{UpgradeShop.slot_names[num]}" if shop_data.type == ShopType.UpgradeShop else
+ f"{name}{Shop.slot_names[num]}" for name, shop_data in sorted(shop_table.items(),
+ key=lambda item: item[1].sram_offset)
+ for num in range(2 if shop_data.type == ShopType.UpgradeShop else 3)), start=SHOP_ID_START))
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots)] = "Old Man Sword Cave"
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 1)] = "Take-Any #1"
@@ -410,114 +304,55 @@ class ShopData(NamedTuple):
}
-def set_up_shops(world, player: int):
+def set_up_shops(multiworld, player: int):
+ from .Options import small_key_shuffle
# TODO: move hard+ mode changes for shields here, utilizing the new shops
- if world.retro_bow[player]:
- rss = world.get_region('Red Shield Shop', player).shop
+ if multiworld.retro_bow[player]:
+ rss = multiworld.get_region('Red Shield Shop', player).shop
replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50],
['Blue Shield', 50], ['Small Heart',
10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them.
- if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
+ if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
replacement_items.append(['Small Key (Universal)', 100])
- replacement_item = world.random.choice(replacement_items)
+ replacement_item = multiworld.random.choice(replacement_items)
rss.add_inventory(2, 'Single Arrow', 80, 1, replacement_item[0], replacement_item[1])
rss.locked = True
- if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal or world.retro_bow[player]:
- for shop in world.random.sample([s for s in world.shops if
- s.custom and not s.locked and s.type == ShopType.Shop and s.region.player == player],
- 5):
+ if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal or multiworld.retro_bow[player]:
+ for shop in multiworld.random.sample([s for s in multiworld.shops if
+ s.custom and not s.locked and s.type == ShopType.Shop
+ and s.region.player == player], 5):
shop.locked = True
slots = [0, 1, 2]
- world.random.shuffle(slots)
+ multiworld.random.shuffle(slots)
slots = iter(slots)
- if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
+ if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
shop.add_inventory(next(slots), 'Small Key (Universal)', 100)
- if world.retro_bow[player]:
+ if multiworld.retro_bow[player]:
shop.push_inventory(next(slots), 'Single Arrow', 80)
-
-def shuffle_shops(world, items, player: int):
- option = world.shop_shuffle[player]
- if 'u' in option:
- progressive = world.progressive[player]
- progressive = world.random.choice([True, False]) if progressive == 'grouped_random' else progressive == 'on'
- progressive &= world.goal == 'icerodhunt'
- new_items = ["Bomb Upgrade (+5)"] * 6
- new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
-
- if not world.retro_bow[player]:
- new_items += ["Arrow Upgrade (+5)"] * 6
- new_items.append("Arrow Upgrade (+5)" if progressive else "Arrow Upgrade (+10)")
-
- world.random.shuffle(new_items) # Decide what gets tossed randomly if it can't insert everything.
-
- capacityshop: Optional[Shop] = None
- for shop in world.shops:
+ if multiworld.shuffle_capacity_upgrades[player]:
+ for shop in multiworld.shops:
if shop.type == ShopType.UpgradeShop and shop.region.player == player and \
shop.region.name == "Capacity Upgrade":
shop.clear_inventory()
- capacityshop = shop
-
- if world.goal[player] != 'icerodhunt':
- for i, item in enumerate(items):
- if item.name in trap_replaceable:
- items[i] = ItemFactory(new_items.pop(), player)
- if not new_items:
- break
- else:
- logging.warning(
- f"Not all upgrades put into Player{player}' item pool. Putting remaining items in Capacity Upgrade shop instead.")
- bombupgrades = sum(1 for item in new_items if 'Bomb Upgrade' in item)
- arrowupgrades = sum(1 for item in new_items if 'Arrow Upgrade' in item)
- slots = iter(range(2))
- if bombupgrades:
- capacityshop.add_inventory(next(slots), 'Bomb Upgrade (+5)', 100, bombupgrades)
- if arrowupgrades:
- capacityshop.add_inventory(next(slots), 'Arrow Upgrade (+5)', 100, arrowupgrades)
- else:
- for item in new_items:
- world.push_precollected(ItemFactory(item, player))
- if any(setting in option for setting in 'ipP'):
+ if (multiworld.shuffle_shop_inventories[player] or multiworld.randomize_shop_prices[player]
+ or multiworld.randomize_cost_types[player]):
shops = []
- upgrade_shops = []
total_inventory = []
- for shop in world.shops:
+ for shop in multiworld.shops:
if shop.region.player == player:
- if shop.type == ShopType.UpgradeShop:
- upgrade_shops.append(shop)
- elif shop.type == ShopType.Shop and not shop.locked:
+ if shop.type == ShopType.Shop and not shop.locked:
shops.append(shop)
total_inventory.extend(shop.inventory)
- if 'p' in option:
- def price_adjust(price: int) -> int:
- # it is important that a base price of 0 always returns 0 as new price!
- adjust = 2 if price < 100 else 5
- return int((price / adjust) * (0.5 + world.random.random() * 1.5)) * adjust
-
- def adjust_item(item):
- if item:
- item["price"] = price_adjust(item["price"])
- item['replacement_price'] = price_adjust(item["price"])
-
- for item in total_inventory:
- adjust_item(item)
- for shop in upgrade_shops:
- for item in shop.inventory:
- adjust_item(item)
-
- if 'P' in option:
- for item in total_inventory:
- price_to_funny_price(world, item, player)
- # Don't apply to upgrade shops
- # Upgrade shop is only one place, and will generally be too easy to
- # replenish hearts and bombs
-
- if 'i' in option:
- world.random.shuffle(total_inventory)
+ for item in total_inventory:
+ item["price_type"], item["price"] = get_price(multiworld, item, player)
+
+ if multiworld.shuffle_shop_inventories[player]:
+ multiworld.random.shuffle(total_inventory)
i = 0
for shop in shops:
@@ -540,16 +375,18 @@ def adjust_item(item):
}
price_chart = {
- ShopPriceType.Rupees: lambda p: p,
- ShopPriceType.Hearts: lambda p: min(5, p // 5) * 8, # Each heart is 0x8 in memory, Max of 5 hearts (20 total??)
- ShopPriceType.Magic: lambda p: min(15, p // 5) * 8, # Each pip is 0x8 in memory, Max of 15 pips (16 total...)
- ShopPriceType.Bombs: lambda p: max(1, min(10, p // 5)), # 10 Bombs max
- ShopPriceType.Arrows: lambda p: max(1, min(30, p // 5)), # 30 Arrows Max
- ShopPriceType.HeartContainer: lambda p: 0x8,
- ShopPriceType.BombUpgrade: lambda p: 0x1,
- ShopPriceType.ArrowUpgrade: lambda p: 0x1,
- ShopPriceType.Keys: lambda p: min(3, (p // 100) + 1), # Max of 3 keys for a price
- ShopPriceType.Potion: lambda p: (p // 5) % 5,
+ ShopPriceType.Rupees: lambda p, d: p,
+ # Each heart is 0x8 in memory, Max of 19 hearts on easy/normal, 9 on hard, 7 on expert
+ ShopPriceType.Hearts: lambda p, d: max(8, min([19, 19, 9, 7][d], p // 14) * 8),
+ # Each pip is 0x8 in memory, Max of 15 pips (16 total)
+ ShopPriceType.Magic: lambda p, d: max(8, min(15, p // 18) * 8),
+ ShopPriceType.Bombs: lambda p, d: max(1, min(50, p // 5)), # 50 Bombs max
+ ShopPriceType.Arrows: lambda p, d: max(1, min(70, p // 4)), # 70 Arrows Max
+ ShopPriceType.HeartContainer: lambda p, d: 0x8,
+ ShopPriceType.BombUpgrade: lambda p, d: 0x1,
+ ShopPriceType.ArrowUpgrade: lambda p, d: 0x1,
+ ShopPriceType.Keys: lambda p, d: max(1, min(3, (p // 90) + 1)), # Max of 3 keys for a price
+ ShopPriceType.Potion: lambda p, d: (p // 5) % 5,
}
price_type_display_name = {
@@ -558,6 +395,8 @@ def adjust_item(item):
ShopPriceType.Bombs: "Bombs",
ShopPriceType.Arrows: "Arrows",
ShopPriceType.Keys: "Keys",
+ ShopPriceType.Item: "Item",
+ ShopPriceType.Magic: "Magic"
}
# price division
@@ -566,59 +405,74 @@ def adjust_item(item):
ShopPriceType.Magic: 8,
}
-# prices with no? logic requirements
-simple_price_types = [
- ShopPriceType.Rupees,
- ShopPriceType.Hearts,
- ShopPriceType.Bombs,
- ShopPriceType.Arrows,
- ShopPriceType.Keys
-]
+
+def get_price_modifier(item):
+ if item.game == "A Link to the Past":
+ if any(x in item.name for x in
+ ['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
+ return 0.125
+ elif any(x in item.name for x in
+ ['Arrow', 'Bomb', 'Clock']) and item.name != "Bombos" and "(50)" not in item.name:
+ return 0.25
+ elif any(x in item.name for x in ['Small Key', 'Heart']):
+ return 0.5
+ else:
+ return 1
+ if item.advancement:
+ return 1
+ elif item.useful:
+ return 0.5
+ else:
+ return 0.25
-def price_to_funny_price(world, item: dict, player: int):
- """
- Converts a raw Rupee price into a special price type
- """
+def get_price(multiworld, item, player: int, price_type=None):
+ """Converts a raw Rupee price into a special price type"""
+ from .Options import small_key_shuffle
+ if price_type:
+ price_types = [price_type]
+ else:
+ price_types = [ShopPriceType.Rupees] # included as a chance to not change price
+ if multiworld.randomize_cost_types[player]:
+ price_types += [
+ ShopPriceType.Hearts,
+ ShopPriceType.Bombs,
+ ShopPriceType.Magic,
+ ]
+ if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
+ if item and item["item"] == "Small Key (Universal)":
+ price_types = [ShopPriceType.Rupees, ShopPriceType.Magic] # no logical requirements for repeatable keys
+ else:
+ price_types.append(ShopPriceType.Keys)
+ if multiworld.retro_bow[player]:
+ if item and item["item"] == "Single Arrow":
+ price_types = [ShopPriceType.Rupees, ShopPriceType.Magic] # no logical requirements for arrows
+ else:
+ price_types.append(ShopPriceType.Arrows)
+ diff = multiworld.item_pool[player].value
if item:
- price_types = [
- ShopPriceType.Rupees, # included as a chance to not change price type
- ShopPriceType.Hearts,
- ShopPriceType.Bombs,
- ]
- # don't pay in universal keys to get access to universal keys
- if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal \
- and not "Small Key (Universal)" == item['replacement']:
- price_types.append(ShopPriceType.Keys)
- if not world.retro_bow[player]:
- price_types.append(ShopPriceType.Arrows)
- world.random.shuffle(price_types)
+ # This is for a shop's regular inventory, the item is already determined, and we will decide the price here
+ price = item["price"]
+ if multiworld.randomize_shop_prices[player]:
+ adjust = 2 if price < 100 else 5
+ price = int((price / adjust) * (0.5 + multiworld.per_slot_randoms[player].random() * 1.5)) * adjust
+ multiworld.per_slot_randoms[player].shuffle(price_types)
for p_type in price_types:
- # Ignore rupee prices
- if p_type == ShopPriceType.Rupees:
- return
if any(x in item['item'] for x in price_blacklist[p_type]):
continue
- else:
- item['price'] = min(price_chart[p_type](item['price']), 255)
- item['price_type'] = p_type
- break
-
-
-def create_dynamic_shop_locations(world, player):
- for shop in world.shops:
- if shop.region.player == player:
- for i, item in enumerate(shop.inventory):
- if item is None:
- continue
- if item['create_location']:
- slot_name = f"{shop.region.name}{shop.slot_names[i]}"
- loc = ALttPLocation(player, slot_name,
- address=shop_table_by_location[slot_name], parent=shop.region)
- loc.place_locked_item(ItemFactory(item['item'], player))
- if shop.type == ShopType.TakeAny:
- loc.shop_slot_disabled = True
- shop.region.locations.append(loc)
- world.clear_location_cache()
-
- loc.shop_slot = i
+ return p_type, price_chart[p_type](price, diff)
+ else:
+ # This is an AP location and the price will be adjusted after an item is shuffled into it
+ p_type = multiworld.per_slot_randoms[player].choice(price_types)
+ return p_type, price_chart[p_type](min(int(multiworld.per_slot_randoms[player].randint(8, 56)
+ * multiworld.shop_price_modifier[player] / 100) * 5, 9999), diff)
+
+
+def shop_price_rules(state: CollectionState, player: int, location: ALttPLocation):
+ if location.shop_price_type == ShopPriceType.Hearts:
+ return has_hearts(state, player, (location.shop_price / 8) + 1)
+ elif location.shop_price_type == ShopPriceType.Bombs:
+ return can_use_bombs(state, player, location.shop_price)
+ elif location.shop_price_type == ShopPriceType.Arrows:
+ return can_hold_arrows(state, player, location.shop_price)
+ return True
diff --git a/worlds/alttp/StateHelpers.py b/worlds/alttp/StateHelpers.py
index 95e31e5ba328..964a77fefbaf 100644
--- a/worlds/alttp/StateHelpers.py
+++ b/worlds/alttp/StateHelpers.py
@@ -10,7 +10,7 @@ def is_not_bunny(state: CollectionState, region: LTTPRegion, player: int) -> boo
def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bool:
- return is_not_bunny(state, region, player) and state.has('Pegasus Boots', player)
+ return can_use_bombs(state, player) and is_not_bunny(state, region, player) and state.has('Pegasus Boots', player)
def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool:
@@ -30,8 +30,8 @@ def can_shoot_arrows(state: CollectionState, player: int) -> bool:
def has_triforce_pieces(state: CollectionState, player: int) -> bool:
- count = state.multiworld.treasure_hunt_count[player]
- return state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= count
+ count = state.multiworld.worlds[player].treasure_hunt_required
+ return state.count('Triforce Piece', player) + state.count('Power Star', player) >= count
def has_crystals(state: CollectionState, count: int, player: int) -> bool:
@@ -48,8 +48,8 @@ def can_lift_heavy_rocks(state: CollectionState, player: int) -> bool:
def bottle_count(state: CollectionState, player: int) -> int:
- return min(state.multiworld.difficulty_requirements[player].progressive_bottle_limit,
- state.count_group("Bottles", player))
+ return min(state.multiworld.worlds[player].difficulty_requirements.progressive_bottle_limit,
+ state.count_group("Bottles", player))
def has_hearts(state: CollectionState, player: int, count: int) -> int:
@@ -59,10 +59,10 @@ def has_hearts(state: CollectionState, player: int, count: int) -> int:
def heart_count(state: CollectionState, player: int) -> int:
# Warning: This only considers items that are marked as advancement items
- diff = state.multiworld.difficulty_requirements[player]
- return min(state.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \
- + state.item_count('Sanctuary Heart Container', player) \
- + min(state.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
+ diff = state.multiworld.worlds[player].difficulty_requirements
+ return min(state.count('Boss Heart Container', player), diff.boss_heart_container_limit) \
+ + state.count('Sanctuary Heart Container', player) \
+ + min(state.count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
+ 3 # starting hearts
@@ -83,13 +83,53 @@ def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16,
return basemagic >= smallmagic
+def can_hold_arrows(state: CollectionState, player: int, quantity: int):
+ arrows = 30 + ((state.count("Arrow Upgrade (+5)", player) * 5) + (state.count("Arrow Upgrade (+10)", player) * 10)
+ + (state.count("Bomb Upgrade (50)", player) * 50))
+ # Arrow Upgrade (+5) beyond the 6th gives +10
+ arrows += max(0, ((state.count("Arrow Upgrade (+5)", player) - 6) * 10))
+ return min(70, arrows) >= quantity
+
+
+def can_use_bombs(state: CollectionState, player: int, quantity: int = 1) -> bool:
+ bombs = 0 if state.multiworld.bombless_start[player] else 10
+ bombs += ((state.count("Bomb Upgrade (+5)", player) * 5) + (state.count("Bomb Upgrade (+10)", player) * 10)
+ + (state.count("Bomb Upgrade (50)", player) * 50))
+ # Bomb Upgrade (+5) beyond the 6th gives +10
+ bombs += max(0, ((state.count("Bomb Upgrade (+5)", player) - 6) * 10))
+ if (not state.multiworld.shuffle_capacity_upgrades[player]) and state.has("Capacity Upgrade Shop", player):
+ bombs += 40
+ return bombs >= min(quantity, 50)
+
+
+def can_bomb_or_bonk(state: CollectionState, player: int) -> bool:
+ return state.has("Pegasus Boots", player) or can_use_bombs(state, player)
+
+
+def can_activate_crystal_switch(state: CollectionState, player: int) -> bool:
+ return (has_melee_weapon(state, player) or can_use_bombs(state, player) or can_shoot_arrows(state, player)
+ or state.has_any(["Hookshot", "Cane of Somaria", "Cane of Byrna", "Fire Rod", "Ice Rod", "Blue Boomerang",
+ "Red Boomerang"], player))
+
+
def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) -> bool:
- return (has_melee_weapon(state, player)
- or state.has('Cane of Somaria', player)
- or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player)))
- or can_shoot_arrows(state, player)
- or state.has('Fire Rod', player)
- or (state.has('Bombs (10)', player) and enemies < 6))
+ if state.multiworld.enemy_shuffle[player]:
+ # I don't fully understand Enemizer's logic for placing enemies in spots where they need to be killable, if any.
+ # Just go with maximal requirements for now.
+ return (has_melee_weapon(state, player)
+ and state.has('Cane of Somaria', player)
+ and state.has('Cane of Byrna', player) and can_extend_magic(state, player)
+ and can_shoot_arrows(state, player)
+ and state.has('Fire Rod', player)
+ and can_use_bombs(state, player, enemies * 4))
+ else:
+ return (has_melee_weapon(state, player)
+ or state.has('Cane of Somaria', player)
+ or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player)))
+ or can_shoot_arrows(state, player)
+ or state.has('Fire Rod', player)
+ or (state.multiworld.enemy_health[player] in ("easy", "default")
+ and can_use_bombs(state, player, enemies * 4)))
def can_get_good_bee(state: CollectionState, player: int) -> bool:
@@ -137,10 +177,11 @@ def can_melt_things(state: CollectionState, player: int) -> bool:
def has_misery_mire_medallion(state: CollectionState, player: int) -> bool:
- return state.has(state.multiworld.required_medallions[player][0], player)
+ return state.has(state.multiworld.worlds[player].required_medallions[0], player)
+
def has_turtle_rock_medallion(state: CollectionState, player: int) -> bool:
- return state.has(state.multiworld.required_medallions[player][1], player)
+ return state.has(state.multiworld.worlds[player].required_medallions[1], player)
def can_boots_clip_lw(state: CollectionState, player: int) -> bool:
@@ -159,4 +200,4 @@ def can_get_glitched_speed_dw(state: CollectionState, player: int) -> bool:
rules = [state.has('Pegasus Boots', player), any([state.has('Hookshot', player), has_sword(state, player)])]
if state.multiworld.mode[player] != 'inverted':
rules.append(state.has('Moon Pearl', player))
- return all(rules)
\ No newline at end of file
+ return all(rules)
diff --git a/worlds/alttp/SubClasses.py b/worlds/alttp/SubClasses.py
index 64e4adaec9a2..328e28da9346 100644
--- a/worlds/alttp/SubClasses.py
+++ b/worlds/alttp/SubClasses.py
@@ -14,9 +14,12 @@ class ALttPLocation(Location):
crystal: bool
player_address: Optional[int]
_hint_text: Optional[str]
+ shop: None
shop_slot: Optional[int] = None
"""If given as integer, shop_slot is the shop's inventory index."""
shop_slot_disabled: bool = False
+ shop_price = 0
+ shop_price_type = None
parent_region: "LTTPRegion"
def __init__(self, player: int, name: str, address: Optional[int] = None, crystal: bool = False,
@@ -26,6 +29,13 @@ def __init__(self, player: int, name: str, address: Optional[int] = None, crysta
self.player_address = player_address
self._hint_text = hint_text
+ @property
+ def hint_text(self) -> str:
+ hint_text = getattr(self, "_hint_text", None)
+ if hint_text:
+ return hint_text
+ return "at " + self.name.replace("_", " ").replace("-", " ")
+
class ALttPItem(Item):
game: str = "A Link to the Past"
@@ -66,10 +76,6 @@ def dungeon_item(self) -> Optional[str]:
if self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
return self.type
- @property
- def locked_dungeon_item(self):
- return self.location.locked and self.dungeon_item
-
class LTTPRegionType(IntEnum):
LightWorld = 1
diff --git a/worlds/alttp/Text.py b/worlds/alttp/Text.py
index b479a9b8e002..c005cacd8f9f 100644
--- a/worlds/alttp/Text.py
+++ b/worlds/alttp/Text.py
@@ -1289,6 +1289,415 @@ class LargeCreditBottomMapper(CharTextMapper):
class TextTable(object):
SIZE = 0x7355
+ valid_keys = [
+ "set_cursor",
+ "set_cursor2",
+ "game_over_menu",
+ "var_test",
+ "follower_no_enter",
+ "choice_1_3",
+ "choice_2_3",
+ "choice_3_3",
+ "choice_1_2",
+ "choice_2_2",
+ "uncle_leaving_text",
+ "uncle_dying_sewer",
+ "tutorial_guard_1",
+ "tutorial_guard_2",
+ "tutorial_guard_3",
+ "tutorial_guard_4",
+ "tutorial_guard_5",
+ "tutorial_guard_6",
+ "tutorial_guard_7",
+ "priest_sanctuary_before_leave",
+ "sanctuary_enter",
+ "zelda_sanctuary_story",
+ "priest_sanctuary_before_pendants",
+ "priest_sanctuary_after_pendants_before_master_sword",
+ "priest_sanctuary_dying",
+ "zelda_save_sewers",
+ "priest_info",
+ "zelda_sanctuary_before_leave",
+ "telepathic_intro",
+ "telepathic_reminder",
+ "zelda_go_to_throne",
+ "zelda_push_throne",
+ "zelda_switch_room_pull",
+ "zelda_save_lets_go",
+ "zelda_save_repeat",
+ "zelda_before_pendants",
+ "zelda_after_pendants_before_master_sword",
+ "telepathic_zelda_right_after_master_sword",
+ "zelda_sewers",
+ "zelda_switch_room",
+ "kakariko_saharalasa_wife",
+ "kakariko_saharalasa_wife_sword_story",
+ "kakariko_saharalasa_wife_closing",
+ "kakariko_saharalasa_after_master_sword",
+ "kakariko_alert_guards",
+ "sahasrahla_quest_have_pendants",
+ "sahasrahla_quest_have_master_sword",
+ "sahasrahla_quest_information",
+ "sahasrahla_bring_courage",
+ "sahasrahla_have_ice_rod",
+ "telepathic_sahasrahla_beat_agahnim",
+ "telepathic_sahasrahla_beat_agahnim_no_pearl",
+ "sahasrahla_have_boots_no_icerod",
+ "sahasrahla_have_courage",
+ "sahasrahla_found",
+ "sign_rain_north_of_links_house",
+ "sign_north_of_links_house",
+ "sign_path_to_death_mountain",
+ "sign_lost_woods",
+ "sign_zoras",
+ "sign_outside_magic_shop",
+ "sign_death_mountain_cave_back",
+ "sign_east_of_links_house",
+ "sign_south_of_lumberjacks",
+ "sign_east_of_desert",
+ "sign_east_of_sanctuary",
+ "sign_east_of_castle",
+ "sign_north_of_lake",
+ "sign_desert_thief",
+ "sign_lumberjacks_house",
+ "sign_north_kakariko",
+ "witch_bring_mushroom",
+ "witch_brewing_the_item",
+ "witch_assistant_no_bottle",
+ "witch_assistant_no_empty_bottle",
+ "witch_assistant_informational",
+ "witch_assistant_no_bottle_buying",
+ "potion_shop_no_empty_bottles",
+ "item_get_lamp",
+ "item_get_boomerang",
+ "item_get_bow",
+ "item_get_shovel",
+ "item_get_magic_cape",
+ "item_get_powder",
+ "item_get_flippers",
+ "item_get_power_gloves",
+ "item_get_pendant_courage",
+ "item_get_pendant_power",
+ "item_get_pendant_wisdom",
+ "item_get_mushroom",
+ "item_get_book",
+ "item_get_moonpearl",
+ "item_get_compass",
+ "item_get_map",
+ "item_get_ice_rod",
+ "item_get_fire_rod",
+ "item_get_ether",
+ "item_get_bombos",
+ "item_get_quake",
+ "item_get_hammer",
+ "item_get_flute",
+ "item_get_cane_of_somaria",
+ "item_get_hookshot",
+ "item_get_bombs",
+ "item_get_bottle",
+ "item_get_big_key",
+ "item_get_titans_mitts",
+ "item_get_magic_mirror",
+ "item_get_fake_mastersword",
+ "post_item_get_mastersword",
+ "item_get_red_potion",
+ "item_get_green_potion",
+ "item_get_blue_potion",
+ "item_get_bug_net",
+ "item_get_blue_mail",
+ "item_get_red_mail",
+ "item_get_temperedsword",
+ "item_get_mirror_shield",
+ "item_get_cane_of_byrna",
+ "missing_big_key",
+ "missing_magic",
+ "item_get_pegasus_boots",
+ "talking_tree_info_start",
+ "talking_tree_info_1",
+ "talking_tree_info_2",
+ "talking_tree_info_3",
+ "talking_tree_info_4",
+ "talking_tree_other",
+ "item_get_pendant_power_alt",
+ "item_get_pendant_wisdom_alt",
+ "game_shooting_choice",
+ "game_shooting_yes",
+ "game_shooting_no",
+ "game_shooting_continue",
+ "pond_of_wishing",
+ "pond_item_select",
+ "pond_item_test",
+ "pond_will_upgrade",
+ "pond_item_test_no",
+ "pond_item_test_no_no",
+ "pond_item_boomerang",
+ "pond_item_shield",
+ "pond_item_silvers",
+ "pond_item_bottle_filled",
+ "pond_item_sword",
+ "pond_of_wishing_happiness",
+ "pond_of_wishing_choice",
+ "pond_of_wishing_bombs",
+ "pond_of_wishing_arrows",
+ "pond_of_wishing_full_upgrades",
+ "mountain_old_man_first",
+ "mountain_old_man_deadend",
+ "mountain_old_man_turn_right",
+ "mountain_old_man_lost_and_alone",
+ "mountain_old_man_drop_off",
+ "mountain_old_man_in_his_cave_pre_agahnim",
+ "mountain_old_man_in_his_cave",
+ "mountain_old_man_in_his_cave_post_agahnim",
+ "tavern_old_man_awake",
+ "tavern_old_man_unactivated_flute",
+ "tavern_old_man_know_tree_unactivated_flute",
+ "tavern_old_man_have_flute",
+ "chicken_hut_lady",
+ "running_man",
+ "game_race_sign",
+ "sign_bumper_cave",
+ "sign_catfish",
+ "sign_north_village_of_outcasts",
+ "sign_south_of_bumper_cave",
+ "sign_east_of_pyramid",
+ "sign_east_of_bomb_shop",
+ "sign_east_of_mire",
+ "sign_village_of_outcasts",
+ "sign_before_wishing_pond",
+ "sign_before_catfish_area",
+ "castle_wall_guard",
+ "gate_guard",
+ "telepathic_tile_eastern_palace",
+ "telepathic_tile_tower_of_hera_floor_4",
+ "hylian_text_1",
+ "mastersword_pedestal_translated",
+ "telepathic_tile_spectacle_rock",
+ "telepathic_tile_swamp_entrance",
+ "telepathic_tile_thieves_town_upstairs",
+ "telepathic_tile_misery_mire",
+ "hylian_text_2",
+ "desert_entry_translated",
+ "telepathic_tile_under_ganon",
+ "telepathic_tile_palace_of_darkness",
+ "telepathic_tile_desert_bonk_torch_room",
+ "telepathic_tile_castle_tower",
+ "telepathic_tile_ice_large_room",
+ "telepathic_tile_turtle_rock",
+ "telepathic_tile_ice_entrance",
+ "telepathic_tile_ice_stalfos_knights_room",
+ "telepathic_tile_tower_of_hera_entrance",
+ "houlihan_room",
+ "caught_a_bee",
+ "caught_a_fairy",
+ "no_empty_bottles",
+ "game_race_boy_time",
+ "game_race_girl",
+ "game_race_boy_success",
+ "game_race_boy_failure",
+ "game_race_boy_already_won",
+ "game_race_boy_sneaky",
+ "bottle_vendor_choice",
+ "bottle_vendor_get",
+ "bottle_vendor_no",
+ "bottle_vendor_already_collected",
+ "bottle_vendor_bee",
+ "bottle_vendor_fish",
+ "hobo_item_get_bottle",
+ "blacksmiths_what_you_want",
+ "blacksmiths_paywall",
+ "blacksmiths_extra_okay",
+ "blacksmiths_tempered_already",
+ "blacksmiths_temper_no",
+ "blacksmiths_bogart_sword",
+ "blacksmiths_get_sword",
+ "blacksmiths_shop_before_saving",
+ "blacksmiths_shop_saving",
+ "blacksmiths_collect_frog",
+ "blacksmiths_still_working",
+ "blacksmiths_saving_bows",
+ "blacksmiths_hammer_anvil",
+ "dark_flute_boy_storytime",
+ "dark_flute_boy_get_shovel",
+ "dark_flute_boy_no_get_shovel",
+ "dark_flute_boy_flute_not_found",
+ "dark_flute_boy_after_shovel_get",
+ "shop_fortune_teller_lw_hint_0",
+ "shop_fortune_teller_lw_hint_1",
+ "shop_fortune_teller_lw_hint_2",
+ "shop_fortune_teller_lw_hint_3",
+ "shop_fortune_teller_lw_hint_4",
+ "shop_fortune_teller_lw_hint_5",
+ "shop_fortune_teller_lw_hint_6",
+ "shop_fortune_teller_lw_hint_7",
+ "shop_fortune_teller_lw_no_rupees",
+ "shop_fortune_teller_lw",
+ "shop_fortune_teller_lw_post_hint",
+ "shop_fortune_teller_lw_no",
+ "shop_fortune_teller_lw_hint_8",
+ "shop_fortune_teller_lw_hint_9",
+ "shop_fortune_teller_lw_hint_10",
+ "shop_fortune_teller_lw_hint_11",
+ "shop_fortune_teller_lw_hint_12",
+ "shop_fortune_teller_lw_hint_13",
+ "shop_fortune_teller_lw_hint_14",
+ "shop_fortune_teller_lw_hint_15",
+ "dark_sanctuary",
+ "dark_sanctuary_hint_0",
+ "dark_sanctuary_no",
+ "dark_sanctuary_hint_1",
+ "dark_sanctuary_yes",
+ "dark_sanctuary_hint_2",
+ "sick_kid_no_bottle",
+ "sick_kid_trade",
+ "sick_kid_post_trade",
+ "desert_thief_sitting",
+ "desert_thief_following",
+ "desert_thief_question",
+ "desert_thief_question_yes",
+ "desert_thief_after_item_get",
+ "desert_thief_reassure",
+ "hylian_text_3",
+ "tablet_ether_book",
+ "tablet_bombos_book",
+ "magic_bat_wake",
+ "magic_bat_give_half_magic",
+ "intro_main",
+ "intro_throne_room",
+ "intro_zelda_cell",
+ "intro_agahnim",
+ "pickup_purple_chest",
+ "bomb_shop",
+ "bomb_shop_big_bomb",
+ "bomb_shop_big_bomb_buy",
+ "item_get_big_bomb",
+ "kiki_second_extortion",
+ "kiki_second_extortion_no",
+ "kiki_second_extortion_yes",
+ "kiki_first_extortion",
+ "kiki_first_extortion_yes",
+ "kiki_first_extortion_no",
+ "kiki_leaving_screen",
+ "blind_in_the_cell",
+ "blind_by_the_light",
+ "blind_not_that_way",
+ "aginah_l1sword_no_book",
+ "aginah_l1sword_with_pendants",
+ "aginah",
+ "aginah_need_better_sword",
+ "aginah_have_better_sword",
+ "catfish",
+ "catfish_after_item",
+ "lumberjack_right",
+ "lumberjack_left",
+ "lumberjack_left_post_agahnim",
+ "fighting_brothers_right",
+ "fighting_brothers_right_opened",
+ "fighting_brothers_left",
+ "maiden_crystal_1",
+ "maiden_crystal_2",
+ "maiden_crystal_3",
+ "maiden_crystal_4",
+ "maiden_crystal_5",
+ "maiden_crystal_6",
+ "maiden_crystal_7",
+ "maiden_ending",
+ "maiden_confirm_understood",
+ "barrier_breaking",
+ "maiden_crystal_7_again",
+ "agahnim_zelda_teleport",
+ "agahnim_magic_running_away",
+ "agahnim_hide_and_seek_found",
+ "agahnim_defeated",
+ "agahnim_final_meeting",
+ "zora_meeting",
+ "zora_tells_cost",
+ "zora_get_flippers",
+ "zora_no_cash",
+ "zora_no_buy_item",
+ "kakariko_saharalasa_grandson",
+ "kakariko_saharalasa_grandson_next",
+ "dark_palace_tree_dude",
+ "fairy_wishing_ponds",
+ "fairy_wishing_ponds_no",
+ "pond_of_wishing_no",
+ "pond_of_wishing_return_item",
+ "pond_of_wishing_throw",
+ "pond_pre_item_silvers",
+ "pond_of_wishing_great_luck",
+ "pond_of_wishing_good_luck",
+ "pond_of_wishing_meh_luck",
+ "pond_of_wishing_bad_luck",
+ "pond_of_wishing_fortune",
+ "item_get_14_heart",
+ "item_get_24_heart",
+ "item_get_34_heart",
+ "item_get_whole_heart",
+ "item_get_sanc_heart",
+ "fairy_fountain_refill",
+ "death_mountain_bullied_no_pearl",
+ "death_mountain_bullied_with_pearl",
+ "death_mountain_bully_no_pearl",
+ "death_mountain_bully_with_pearl",
+ "shop_darkworld_enter",
+ "game_chest_village_of_outcasts",
+ "game_chest_no_cash",
+ "game_chest_not_played",
+ "game_chest_played",
+ "game_chest_village_of_outcasts_play",
+ "shop_first_time",
+ "shop_already_have",
+ "shop_buy_shield",
+ "shop_buy_red_potion",
+ "shop_buy_arrows",
+ "shop_buy_bombs",
+ "shop_buy_bee",
+ "shop_buy_heart",
+ "shop_first_no_bottle_buy",
+ "shop_buy_no_space",
+ "ganon_fall_in",
+ "ganon_phase_3",
+ "lost_woods_thief",
+ "blinds_hut_dude",
+ "end_triforce",
+ "toppi_fallen",
+ "kakariko_tavern_fisherman",
+ "thief_money",
+ "thief_desert_rupee_cave",
+ "thief_ice_rupee_cave",
+ "telepathic_tile_south_east_darkworld_cave",
+ "cukeman",
+ "cukeman_2",
+ "potion_shop_no_cash",
+ "kakariko_powdered_chicken",
+ "game_chest_south_of_kakariko",
+ "game_chest_play_yes",
+ "game_chest_play_no",
+ "game_chest_lost_woods",
+ "kakariko_flophouse_man_no_flippers",
+ "kakariko_flophouse_man",
+ "menu_start_2",
+ "menu_start_3",
+ "menu_pause",
+ "game_digging_choice",
+ "game_digging_start",
+ "game_digging_no_cash",
+ "game_digging_end_time",
+ "game_digging_come_back_later",
+ "game_digging_no_follower",
+ "menu_start_4",
+ "ganon_fall_in_alt",
+ "ganon_phase_3_alt",
+ "sign_east_death_mountain_bridge",
+ "fish_money",
+ "sign_ganons_tower",
+ "sign_ganon",
+ "ganon_phase_3_no_bow",
+ "ganon_phase_3_no_silvers_alt",
+ "ganon_phase_3_no_silvers",
+ "ganon_phase_3_silvers",
+ "murahdahla",
+ ]
+
def __init__(self):
self._text = OrderedDict()
self.setDefaultText()
diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py
index 11a95bf7cd6c..50397dea166c 100644
--- a/worlds/alttp/UnderworldGlitchRules.py
+++ b/worlds/alttp/UnderworldGlitchRules.py
@@ -15,7 +15,7 @@ def underworld_glitch_connections(world, player):
specrock.exits.append(kikiskip)
mire.exits.extend([mire_to_hera, mire_to_swamp])
- if world.fix_fake_world[player]:
+ if world.worlds[player].fix_fake_world:
kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region)
mire_to_hera.connect(world.get_entrance('Tower of Hera Exit', player).connected_region)
mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit', player).connected_region)
@@ -31,18 +31,18 @@ def fake_pearl_state(state, player):
if state.has('Moon Pearl', player):
return state
fake_state = state.copy()
- fake_state.prog_items['Moon Pearl', player] += 1
+ fake_state.prog_items[player]['Moon Pearl'] += 1
return fake_state
# Sets the rules on where we can actually go using this clip.
# Behavior differs based on what type of ER shuffle we're playing.
def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, dungeon_exit: str):
- fix_dungeon_exits = world.fix_palaceofdarkness_exit[player]
- fix_fake_worlds = world.fix_fake_world[player]
+ fix_dungeon_exits = world.worlds[player].fix_palaceofdarkness_exit
+ fix_fake_worlds = world.worlds[player].fix_fake_world
dungeon_entrance = [r for r in world.get_region(dungeon_region, player).entrances if r.name != clip.name][0]
- if not fix_dungeon_exits: # vanilla, simple, restricted, dungeonssimple; should never have fake worlds fix
+ if not fix_dungeon_exits: # vanilla, simple, restricted, dungeons_simple; should never have fake worlds fix
# Dungeons are only shuffled among themselves. We need to check SW, MM, and AT because they can't be reentered trivially.
if dungeon_entrance.name == 'Skull Woods Final Section':
set_rule(clip, lambda state: False) # entrance doesn't exist until you fire rod it from the other side
@@ -52,23 +52,23 @@ def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, du
add_rule(clip, lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier
# Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally.
add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
- elif not fix_fake_worlds: # full, dungeonsfull; fixed dungeon exits, but no fake worlds fix
+ elif not fix_fake_worlds: # full, dungeons_full; fixed dungeon exits, but no fake worlds fix
# Entry requires the entrance's requirements plus a fake pearl, but you don't gain logical access to the surrounding region.
add_rule(clip, lambda state: dungeon_entrance.access_rule(fake_pearl_state(state, player)))
# exiting restriction
add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
- # Otherwise, the shuffle type is crossed, dungeonscrossed, or insanity; all of these do not need additional rules on where we can go,
+ # Otherwise, the shuffle type is crossed, dungeons_crossed, or insanity; all of these do not need additional rules on where we can go,
# since the clip links directly to the exterior region.
def underworld_glitches_rules(world, player):
- fix_dungeon_exits = world.fix_palaceofdarkness_exit[player]
- fix_fake_worlds = world.fix_fake_world[player]
-
# Ice Palace Entrance Clip
- # This is the easiest one since it's a simple internal clip. Just need to also add melting to freezor chest since it's otherwise assumed.
- add_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or')
+ # This is the easiest one since it's a simple internal clip.
+ # Need to also add melting to freezor chest since it's otherwise assumed.
+ # Also can pick up the first jelly key from behind.
+ add_rule(world.get_entrance('Ice Palace (Main)', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or')
add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player))
+ add_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or')
# Kiki Skip
@@ -89,8 +89,8 @@ def underworld_glitches_rules(world, player):
# Build the rule for SP moat.
# We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT.
# First we require a certain type of entrance shuffle, then build the rule from its pieces.
- if not world.swamp_patch_required[player]:
- if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']:
+ if not world.worlds[player].swamp_patch_required:
+ if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rule_map = {
'Misery Mire (Entrance)': (lambda state: True),
'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player))
diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py
index 8815fae092e6..3176f7a7fcce 100644
--- a/worlds/alttp/__init__.py
+++ b/worlds/alttp/__init__.py
@@ -13,14 +13,14 @@
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
from .ItemPool import generate_itempool, difficulties
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
-from .Options import alttp_options, smallkey_shuffle
+from .Options import alttp_options, small_key_shuffle
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \
- is_main_entrance
+ is_main_entrance, key_drop_data
from .Client import ALTTPSNIClient
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
get_hash_string, get_base_rom_path, LttPDeltaPatch
from .Rules import set_rules
-from .Shops import create_shops, Shop, ShopSlotFill, ShopType, price_rate_display, price_type_display_name
+from .Shops import create_shops, Shop, push_shop_inventories, ShopType, price_rate_display, price_type_display_name
from .SubClasses import ALttPItem, LTTPRegionType
from worlds.AutoWorld import World, WebWorld, LogicMixin
from .StateHelpers import can_buy_unlimited
@@ -42,7 +42,7 @@ class RomFile(settings.SNESRomPath):
class ALTTPWeb(WebWorld):
setup_en = Tutorial(
- "Multiworld Setup Tutorial",
+ "Multiworld Setup Guide",
"A guide to setting up the Archipelago ALttP Software on your computer. This guide covers single-player, multiworld, and related software.",
"English",
"multiworld_en.md",
@@ -78,7 +78,7 @@ class ALTTPWeb(WebWorld):
)
msu = Tutorial(
- "MSU-1 Setup Tutorial",
+ "MSU-1 Setup Guide",
"A guide to setting up MSU-1, which allows for custom in-game music.",
"English",
"msu1_en.md",
@@ -105,7 +105,7 @@ class ALTTPWeb(WebWorld):
)
plando = Tutorial(
- "Plando Tutorial",
+ "Plando Guide",
"A guide to creating Multiworld Plandos with LTTP",
"English",
"plando_en.md",
@@ -195,7 +195,7 @@ class ALTTPWorld(World):
"Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganons Tower - Hope Room - Left",
"Ganons Tower - Hope Room - Right", "Ganons Tower - Tile Room",
"Ganons Tower - Compass Room - Top Left", "Ganons Tower - Compass Room - Top Right",
- "Ganons Tower - Compass Room - Bottom Left", "Ganons Tower - Compass Room - Bottom Left",
+ "Ganons Tower - Compass Room - Bottom Left", "Ganons Tower - Compass Room - Bottom Right",
"Ganons Tower - DMs Room - Top Left", "Ganons Tower - DMs Room - Top Right",
"Ganons Tower - DMs Room - Bottom Left", "Ganons Tower - DMs Room - Bottom Right",
"Ganons Tower - Map Chest", "Ganons Tower - Firesnake Room",
@@ -213,7 +213,6 @@ class ALTTPWorld(World):
item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int}
location_name_to_id = lookup_name_to_id
- data_version = 8
required_client_version = (0, 4, 1)
web = ALTTPWeb()
@@ -249,13 +248,36 @@ def enemizer_path(self) -> str:
rom_name_available_event: threading.Event
has_progressive_bows: bool
dungeons: typing.Dict[str, Dungeon]
+ waterfall_fairy_bottle_fill: str
+ pyramid_fairy_bottle_fill: str
+ escape_assist: list
+
+ can_take_damage: bool = True
+ swamp_patch_required: bool = False
+ powder_patch_required: bool = False
+ ganon_at_pyramid: bool = True
+ ganonstower_vanilla: bool = True
+ fix_fake_world: bool = True
+
+ clock_mode: str = ""
+ treasure_hunt_required: int = 0
+ treasure_hunt_total: int = 0
def __init__(self, *args, **kwargs):
self.dungeon_local_item_names = set()
self.dungeon_specific_item_names = set()
self.rom_name_available_event = threading.Event()
+ self.pushed_shop_inventories = threading.Event()
self.has_progressive_bows = False
self.dungeons = {}
+ self.waterfall_fairy_bottle_fill = "Bottle"
+ self.pyramid_fairy_bottle_fill = "Bottle"
+ self.fix_trock_doors = None
+ self.fix_skullwoods_exit = None
+ self.fix_palaceofdarkness_exit = None
+ self.fix_trock_exit = None
+ self.required_medallions = ["Ether", "Quake"]
+ self.escape_assist = []
super(ALTTPWorld, self).__init__(*args, **kwargs)
@classmethod
@@ -273,95 +295,119 @@ def stage_assert_generate(cls, multiworld: MultiWorld):
def generate_early(self):
player = self.player
- world = self.multiworld
+ multiworld = self.multiworld
- if world.mode[player] == 'standard' \
- and world.smallkey_shuffle[player] \
- and world.smallkey_shuffle[player] != smallkey_shuffle.option_universal \
- and world.smallkey_shuffle[player] != smallkey_shuffle.option_own_dungeons \
- and world.smallkey_shuffle[player] != smallkey_shuffle.option_start_with:
- self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
+ self.fix_trock_doors = (multiworld.entrance_shuffle[player] != 'vanilla'
+ or multiworld.mode[player] == 'inverted')
+ self.fix_skullwoods_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted',
+ 'dungeons_simple']
+ self.fix_palaceofdarkness_exit = multiworld.entrance_shuffle[player] not in ['dungeons_simple', 'vanilla',
+ 'simple', 'restricted']
+ self.fix_trock_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted',
+ 'dungeons_simple']
+
+ # fairy bottle fills
+ bottle_options = [
+ "Bottle (Red Potion)", "Bottle (Green Potion)", "Bottle (Blue Potion)",
+ "Bottle (Bee)", "Bottle (Good Bee)"
+ ]
+ if multiworld.item_pool[player] not in ["hard", "expert"]:
+ bottle_options.append("Bottle (Fairy)")
+ self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
+ self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
+
+ if multiworld.mode[player] == 'standard':
+ if multiworld.small_key_shuffle[player]:
+ if (multiworld.small_key_shuffle[player] not in
+ (small_key_shuffle.option_universal, small_key_shuffle.option_own_dungeons,
+ small_key_shuffle.option_start_with)):
+ self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
+ self.multiworld.local_items[self.player].value.add("Small Key (Hyrule Castle)")
+ self.multiworld.non_local_items[self.player].value.discard("Small Key (Hyrule Castle)")
+ if multiworld.big_key_shuffle[player]:
+ self.multiworld.local_items[self.player].value.add("Big Key (Hyrule Castle)")
+ self.multiworld.non_local_items[self.player].value.discard("Big Key (Hyrule Castle)")
# system for sharing ER layouts
- self.er_seed = str(world.random.randint(0, 2 ** 64))
+ self.er_seed = str(multiworld.random.randint(0, 2 ** 64))
- if "-" in world.shuffle[player]:
- shuffle, seed = world.shuffle[player].split("-", 1)
- world.shuffle[player] = shuffle
+ if multiworld.entrance_shuffle[player] != "vanilla" and multiworld.entrance_shuffle_seed[player] != "random":
+ shuffle = multiworld.entrance_shuffle[player].current_key
if shuffle == "vanilla":
self.er_seed = "vanilla"
- elif seed.startswith("group-") or world.is_race:
- self.er_seed = get_same_seed(world, (
- shuffle, seed, world.retro_caves[player], world.mode[player], world.logic[player]))
+ elif (not multiworld.entrance_shuffle_seed[player].value.isdigit()) or multiworld.is_race:
+ self.er_seed = get_same_seed(multiworld, (
+ shuffle, multiworld.entrance_shuffle_seed[player].value, multiworld.retro_caves[player], multiworld.mode[player],
+ multiworld.glitches_required[player]))
else: # not a race or group seed, use set seed as is.
- self.er_seed = seed
- elif world.shuffle[player] == "vanilla":
+ self.er_seed = int(multiworld.entrance_shuffle_seed[player].value)
+ elif multiworld.entrance_shuffle[player] == "vanilla":
self.er_seed = "vanilla"
- for dungeon_item in ["smallkey_shuffle", "bigkey_shuffle", "compass_shuffle", "map_shuffle"]:
- option = getattr(world, dungeon_item)[player]
+
+ for dungeon_item in ["small_key_shuffle", "big_key_shuffle", "compass_shuffle", "map_shuffle"]:
+ option = getattr(multiworld, dungeon_item)[player]
if option == "own_world":
- world.local_items[player].value |= self.item_name_groups[option.item_name_group]
+ multiworld.local_items[player].value |= self.item_name_groups[option.item_name_group]
elif option == "different_world":
- world.non_local_items[player].value |= self.item_name_groups[option.item_name_group]
+ multiworld.non_local_items[player].value |= self.item_name_groups[option.item_name_group]
+ if multiworld.mode[player] == "standard":
+ multiworld.non_local_items[player].value -= {"Small Key (Hyrule Castle)"}
elif option.in_dungeon:
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
if option == "original_dungeon":
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
- world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
+ self.difficulty_requirements = difficulties[multiworld.item_pool[player].current_key]
# enforce pre-defined local items.
- if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
- world.local_items[player].value.add('Triforce Piece')
+ if multiworld.goal[player] in ["local_triforce_hunt", "local_ganon_triforce_hunt"]:
+ multiworld.local_items[player].value.add('Triforce Piece')
# Not possible to place crystals outside boss prizes yet (might as well make it consistent with pendants too).
- world.non_local_items[player].value -= item_name_groups['Pendants']
- world.non_local_items[player].value -= item_name_groups['Crystals']
+ multiworld.non_local_items[player].value -= item_name_groups['Pendants']
+ multiworld.non_local_items[player].value -= item_name_groups['Crystals']
create_dungeons = create_dungeons
def create_regions(self):
player = self.player
- world = self.multiworld
-
- world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player],
- world.triforce_pieces_required[player])
+ multiworld = self.multiworld
- if world.mode[player] != 'inverted':
- create_regions(world, player)
+ if multiworld.mode[player] != 'inverted':
+ create_regions(multiworld, player)
else:
- create_inverted_regions(world, player)
- create_shops(world, player)
+ create_inverted_regions(multiworld, player)
+ create_shops(multiworld, player)
self.create_dungeons()
- if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \
- {"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}:
- world.fix_fake_world[player] = False
+ if (multiworld.glitches_required[player] not in ["no_glitches", "minor_glitches"] and
+ multiworld.entrance_shuffle[player] in [
+ "vanilla", "dungeons_simple", "dungeons_full", "simple", "restricted", "full"]):
+ self.fix_fake_world = False
# seeded entrance shuffle
- old_random = world.random
- world.random = random.Random(self.er_seed)
+ old_random = multiworld.random
+ multiworld.random = random.Random(self.er_seed)
- if world.mode[player] != 'inverted':
- link_entrances(world, player)
- mark_light_world_regions(world, player)
+ if multiworld.mode[player] != 'inverted':
+ link_entrances(multiworld, player)
+ mark_light_world_regions(multiworld, player)
for region_name, entrance_name in indirect_connections_not_inverted.items():
- world.register_indirect_condition(world.get_region(region_name, player),
- world.get_entrance(entrance_name, player))
+ multiworld.register_indirect_condition(multiworld.get_region(region_name, player),
+ multiworld.get_entrance(entrance_name, player))
else:
- link_inverted_entrances(world, player)
- mark_dark_world_regions(world, player)
+ link_inverted_entrances(multiworld, player)
+ mark_dark_world_regions(multiworld, player)
for region_name, entrance_name in indirect_connections_inverted.items():
- world.register_indirect_condition(world.get_region(region_name, player),
- world.get_entrance(entrance_name, player))
+ multiworld.register_indirect_condition(multiworld.get_region(region_name, player),
+ multiworld.get_entrance(entrance_name, player))
- world.random = old_random
- plando_connect(world, player)
+ multiworld.random = old_random
+ plando_connect(multiworld, player)
for region_name, entrance_name in indirect_connections.items():
- world.register_indirect_condition(world.get_region(region_name, player),
- world.get_entrance(entrance_name, player))
-
+ multiworld.register_indirect_condition(multiworld.get_region(region_name, player),
+ multiworld.get_entrance(entrance_name, player))
def collect_item(self, state: CollectionState, item: Item, remove=False):
item_name = item.name
@@ -405,15 +451,16 @@ def collect_item(self, state: CollectionState, item: Item, remove=False):
if 'Sword' in item_name:
if state.has('Golden Sword', item.player):
pass
- elif state.has('Tempered Sword', item.player) and self.multiworld.difficulty_requirements[
- item.player].progressive_sword_limit >= 4:
+ elif (state.has('Tempered Sword', item.player) and
+ self.difficulty_requirements.progressive_sword_limit >= 4):
return 'Golden Sword'
- elif state.has('Master Sword', item.player) and self.multiworld.difficulty_requirements[
- item.player].progressive_sword_limit >= 3:
+ elif (state.has('Master Sword', item.player) and
+ self.difficulty_requirements.progressive_sword_limit >= 3):
return 'Tempered Sword'
- elif state.has('Fighter Sword', item.player) and self.multiworld.difficulty_requirements[item.player].progressive_sword_limit >= 2:
+ elif (state.has('Fighter Sword', item.player) and
+ self.difficulty_requirements.progressive_sword_limit >= 2):
return 'Master Sword'
- elif self.multiworld.difficulty_requirements[item.player].progressive_sword_limit >= 1:
+ elif self.difficulty_requirements.progressive_sword_limit >= 1:
return 'Fighter Sword'
elif 'Glove' in item_name:
if state.has('Titans Mitts', item.player):
@@ -425,20 +472,22 @@ def collect_item(self, state: CollectionState, item: Item, remove=False):
elif 'Shield' in item_name:
if state.has('Mirror Shield', item.player):
return
- elif state.has('Red Shield', item.player) and self.multiworld.difficulty_requirements[item.player].progressive_shield_limit >= 3:
+ elif (state.has('Red Shield', item.player) and
+ self.difficulty_requirements.progressive_shield_limit >= 3):
return 'Mirror Shield'
- elif state.has('Blue Shield', item.player) and self.multiworld.difficulty_requirements[item.player].progressive_shield_limit >= 2:
+ elif (state.has('Blue Shield', item.player) and
+ self.difficulty_requirements.progressive_shield_limit >= 2):
return 'Red Shield'
- elif self.multiworld.difficulty_requirements[item.player].progressive_shield_limit >= 1:
+ elif self.difficulty_requirements.progressive_shield_limit >= 1:
return 'Blue Shield'
elif 'Bow' in item_name:
if state.has('Silver Bow', item.player):
return
- elif state.has('Bow', item.player) and (self.multiworld.difficulty_requirements[item.player].progressive_bow_limit >= 2
- or self.multiworld.logic[item.player] == 'noglitches'
- or self.multiworld.swordless[item.player]): # modes where silver bow is always required for ganon
+ elif state.has('Bow', item.player) and (self.difficulty_requirements.progressive_bow_limit >= 2
+ or self.multiworld.glitches_required[self.player] == 'no_glitches'
+ or self.multiworld.swordless[self.player]): # modes where silver bow is always required for ganon
return 'Silver Bow'
- elif self.multiworld.difficulty_requirements[item.player].progressive_bow_limit >= 1:
+ elif self.difficulty_requirements.progressive_bow_limit >= 1:
return 'Bow'
elif item.advancement:
return item_name
@@ -468,7 +517,8 @@ def pre_fill(self):
prizepool = unplaced_prizes.copy()
prize_locs = empty_crystal_locations.copy()
world.random.shuffle(prize_locs)
- fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True)
+ fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True,
+ name="LttP Dungeon Prizes")
except FillError as e:
lttp_logger.exception("Failed to place dungeon prizes (%s). Will retry %s more times", e,
attempts - attempt)
@@ -478,6 +528,10 @@ def pre_fill(self):
break
else:
raise FillError('Unable to place dungeon prizes')
+ if world.mode[player] == 'standard' and world.small_key_shuffle[player] \
+ and world.small_key_shuffle[player] != small_key_shuffle.option_universal and \
+ world.small_key_shuffle[player] != small_key_shuffle.option_own_dungeons:
+ world.local_early_items[player]["Small Key (Hyrule Castle)"] = 1
@classmethod
def stage_pre_fill(cls, world):
@@ -485,8 +539,8 @@ def stage_pre_fill(cls, world):
fill_dungeons_restrictive(world)
@classmethod
- def stage_post_fill(cls, world):
- ShopSlotFill(world)
+ def stage_generate_output(cls, multiworld, output_directory):
+ push_shop_inventories(multiworld)
@property
def use_enemizer(self) -> bool:
@@ -500,6 +554,9 @@ def use_enemizer(self) -> bool:
def generate_output(self, output_directory: str):
multiworld = self.multiworld
player = self.player
+
+ self.pushed_shop_inventories.wait()
+
try:
use_enemizer = self.use_enemizer
@@ -553,7 +610,7 @@ def generate_output(self, output_directory: str):
@classmethod
def stage_extend_hint_information(cls, world, hint_data: typing.Dict[int, typing.Dict[int, str]]):
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
- world.shuffle[player] != "vanilla" or world.retro_caves[player]}
+ world.entrance_shuffle[player] != "vanilla" or world.retro_caves[player]}
for region in world.regions:
if region.player in er_hint_data and region.locations:
@@ -578,27 +635,26 @@ def stage_modify_multidata(cls, multiworld, multidata: dict):
for player in checks_in_area:
checks_in_area[player]["Total"] = 0
-
- for location in multiworld.get_locations():
- if location.game == cls.game and type(location.address) is int:
- main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
- if location.parent_region.dungeon:
- dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
- 'Inverted Ganons Tower': 'Ganons Tower'} \
- .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
- checks_in_area[location.player][dungeonname].append(location.address)
- elif location.parent_region.type == LTTPRegionType.LightWorld:
- checks_in_area[location.player]["Light World"].append(location.address)
- elif location.parent_region.type == LTTPRegionType.DarkWorld:
- checks_in_area[location.player]["Dark World"].append(location.address)
- elif main_entrance.parent_region.type == LTTPRegionType.LightWorld:
- checks_in_area[location.player]["Light World"].append(location.address)
- elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld:
- checks_in_area[location.player]["Dark World"].append(location.address)
- else:
- assert False, "Unknown Location area."
- # TODO: remove Total as it's duplicated data and breaks consistent typing
- checks_in_area[location.player]["Total"] += 1
+ for location in multiworld.get_locations(player):
+ if location.game == cls.game and type(location.address) is int:
+ main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
+ if location.parent_region.dungeon:
+ dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
+ 'Inverted Ganons Tower': 'Ganons Tower'} \
+ .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
+ checks_in_area[location.player][dungeonname].append(location.address)
+ elif location.parent_region.type == LTTPRegionType.LightWorld:
+ checks_in_area[location.player]["Light World"].append(location.address)
+ elif location.parent_region.type == LTTPRegionType.DarkWorld:
+ checks_in_area[location.player]["Dark World"].append(location.address)
+ elif main_entrance.parent_region.type == LTTPRegionType.LightWorld:
+ checks_in_area[location.player]["Light World"].append(location.address)
+ elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld:
+ checks_in_area[location.player]["Dark World"].append(location.address)
+ else:
+ assert False, "Unknown Location area."
+ # TODO: remove Total as it's duplicated data and breaks consistent typing
+ checks_in_area[location.player]["Total"] += 1
multidata["checks_in_area"].update(checks_in_area)
@@ -616,18 +672,18 @@ def create_item(self, name: str) -> Item:
return ALttPItem(name, self.player, **item_init_table[name])
@classmethod
- def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
+ def stage_fill_hook(cls, multiworld, progitempool, usefulitempool, filleritempool, fill_locations):
trash_counts = {}
-
- for player in world.get_game_players("A Link to the Past"):
- if not world.ganonstower_vanilla[player] or \
- world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
+ for player in multiworld.get_game_players("A Link to the Past"):
+ world = multiworld.worlds[player]
+ if not world.ganonstower_vanilla or \
+ world.options.glitches_required.current_key in {'overworld_glitches', 'hybrid_major_glitches', "no_logic"}:
pass
- elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1):
- trash_counts[player] = world.random.randint(world.crystals_needed_for_gt[player] * 2,
- world.crystals_needed_for_gt[player] * 4)
+ elif 'triforce_hunt' in world.options.goal.current_key and ('local' in world.options.goal.current_key or multiworld.players == 1):
+ trash_counts[player] = multiworld.random.randint(world.options.crystals_needed_for_gt * 2,
+ world.options.crystals_needed_for_gt * 4)
else:
- trash_counts[player] = world.random.randint(0, world.crystals_needed_for_gt[player] * 2)
+ trash_counts[player] = multiworld.random.randint(0, world.options.crystals_needed_for_gt * 2)
if trash_counts:
locations_mapping = {player: [] for player in trash_counts}
@@ -637,14 +693,14 @@ def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fi
for player, trash_count in trash_counts.items():
gtower_locations = locations_mapping[player]
- world.random.shuffle(gtower_locations)
+ multiworld.random.shuffle(gtower_locations)
while gtower_locations and filleritempool and trash_count > 0:
spot_to_fill = gtower_locations.pop()
for index, item in enumerate(filleritempool):
if spot_to_fill.item_rule(item):
filleritempool.pop(index) # remove from outer fill
- world.push_item(spot_to_fill, item, False)
+ multiworld.push_item(spot_to_fill, item, False)
fill_locations.remove(spot_to_fill) # very slow, unfortunately
trash_count -= 1
break
@@ -657,43 +713,19 @@ def bool_to_text(variable: typing.Union[bool, str]) -> str:
return variable
return "Yes" if variable else "No"
- spoiler_handle.write('Logic: %s\n' % self.multiworld.logic[self.player])
- spoiler_handle.write('Dark Room Logic: %s\n' % self.multiworld.dark_room_logic[self.player])
- spoiler_handle.write('Mode: %s\n' % self.multiworld.mode[self.player])
- spoiler_handle.write('Goal: %s\n' % self.multiworld.goal[self.player])
- if "triforce" in self.multiworld.goal[self.player]: # triforce hunt
- spoiler_handle.write("Pieces available for Triforce: %s\n" %
- self.multiworld.triforce_pieces_available[self.player])
- spoiler_handle.write("Pieces required for Triforce: %s\n" %
- self.multiworld.triforce_pieces_required[self.player])
- spoiler_handle.write('Difficulty: %s\n' % self.multiworld.difficulty[self.player])
- spoiler_handle.write('Item Functionality: %s\n' % self.multiworld.item_functionality[self.player])
- spoiler_handle.write('Entrance Shuffle: %s\n' % self.multiworld.shuffle[self.player])
- if self.multiworld.shuffle[self.player] != "vanilla":
- spoiler_handle.write('Entrance Shuffle Seed %s\n' % self.er_seed)
- spoiler_handle.write('Shop inventory shuffle: %s\n' %
- bool_to_text("i" in self.multiworld.shop_shuffle[self.player]))
- spoiler_handle.write('Shop price shuffle: %s\n' %
- bool_to_text("p" in self.multiworld.shop_shuffle[self.player]))
- spoiler_handle.write('Shop upgrade shuffle: %s\n' %
- bool_to_text("u" in self.multiworld.shop_shuffle[self.player]))
- spoiler_handle.write('New Shop inventory: %s\n' %
- bool_to_text("g" in self.multiworld.shop_shuffle[self.player] or
- "f" in self.multiworld.shop_shuffle[self.player]))
- spoiler_handle.write('Custom Potion Shop: %s\n' %
- bool_to_text("w" in self.multiworld.shop_shuffle[self.player]))
- spoiler_handle.write('Enemy health: %s\n' % self.multiworld.enemy_health[self.player])
- spoiler_handle.write('Enemy damage: %s\n' % self.multiworld.enemy_damage[self.player])
- spoiler_handle.write('Prize shuffle %s\n' % self.multiworld.shuffle_prizes[self.player])
-
def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
+ player_name = self.multiworld.get_player_name(self.player)
spoiler_handle.write("\n\nMedallions:\n")
- spoiler_handle.write(f"\nMisery Mire ({self.multiworld.get_player_name(self.player)}):"
- f" {self.multiworld.required_medallions[self.player][0]}")
+ spoiler_handle.write(f"\nMisery Mire ({player_name}):"
+ f" {self.required_medallions[0]}")
spoiler_handle.write(
- f"\nTurtle Rock ({self.multiworld.get_player_name(self.player)}):"
- f" {self.multiworld.required_medallions[self.player][1]}")
-
+ f"\nTurtle Rock ({player_name}):"
+ f" {self.required_medallions[1]}")
+ spoiler_handle.write("\n\nFairy Fountain Bottle Fill:\n")
+ spoiler_handle.write(f"\nPyramid Fairy ({player_name}):"
+ f" {self.pyramid_fairy_bottle_fill}")
+ spoiler_handle.write(f"\nWaterfall Fairy ({player_name}):"
+ f" {self.waterfall_fairy_bottle_fill}")
if self.multiworld.boss_shuffle[self.player] != "none":
def create_boss_map() -> typing.Dict:
boss_map = {
@@ -754,7 +786,7 @@ def build_shop_info(shop: Shop) -> typing.Dict[str, str]:
if item["replacement"] is None:
continue
shop_data["item_{}".format(index)] +=\
- f", {item['replacement']} - {item['replacement_price']}" \
+ f", {item['replacement']} - {item['replacement_price'] // price_rate_display.get(item['replacement_price_type'], 1)}" \
f" {price_type_display_name[item['replacement_price_type']]}"
return shop_data
@@ -767,10 +799,7 @@ def build_shop_info(shop: Shop) -> typing.Dict[str, str]:
item)))
def get_filler_item_name(self) -> str:
- if self.multiworld.goal[self.player] == "icerodhunt":
- item = "Nothing"
- else:
- item = self.multiworld.random.choice(extras_list)
+ item = self.multiworld.random.choice(extras_list)
return GetBeemizerItem(self.multiworld, self.player, item)
def get_pre_fill_items(self):
@@ -790,24 +819,24 @@ def fill_slot_data(self):
# for convenient auto-tracking of the generated settings and adjusting the tracker accordingly
slot_options = ["crystals_needed_for_gt", "crystals_needed_for_ganon", "open_pyramid",
- "bigkey_shuffle", "smallkey_shuffle", "compass_shuffle", "map_shuffle",
+ "big_key_shuffle", "small_key_shuffle", "compass_shuffle", "map_shuffle",
"progressive", "swordless", "retro_bow", "retro_caves", "shop_item_slots",
- "boss_shuffle", "pot_shuffle", "enemy_shuffle"]
+ "boss_shuffle", "pot_shuffle", "enemy_shuffle", "key_drop_shuffle", "bombless_start",
+ "randomize_shop_inventories", "shuffle_shop_inventories", "shuffle_capacity_upgrades",
+ "entrance_shuffle", "dark_room_logic", "goal", "mode",
+ "triforce_pieces_mode", "triforce_pieces_percentage", "triforce_pieces_required",
+ "triforce_pieces_available", "triforce_pieces_extra",
+ ]
slot_data = {option_name: getattr(self.multiworld, option_name)[self.player].value for option_name in slot_options}
slot_data.update({
- 'mode': self.multiworld.mode[self.player],
- 'goal': self.multiworld.goal[self.player],
- 'dark_room_logic': self.multiworld.dark_room_logic[self.player],
- 'mm_medalion': self.multiworld.required_medallions[self.player][0],
- 'tr_medalion': self.multiworld.required_medallions[self.player][1],
- 'shop_shuffle': self.multiworld.shop_shuffle[self.player],
- 'entrance_shuffle': self.multiworld.shuffle[self.player]
+ 'mm_medalion': self.required_medallions[0],
+ 'tr_medalion': self.required_medallions[1],
}
)
return slot_data
-
+
def get_same_seed(world, seed_def: tuple) -> str:
seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {})
@@ -820,8 +849,8 @@ def get_same_seed(world, seed_def: tuple) -> str:
class ALttPLogic(LogicMixin):
def _lttp_has_key(self, item, player, count: int = 1):
- if self.multiworld.logic[player] == 'nologic':
+ if self.multiworld.glitches_required[player] == 'no_logic':
return True
- if self.multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
+ if self.multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
return can_buy_unlimited(self, 'Small Key (Universal)', player)
- return self.prog_items[item, player] >= count
+ return self.prog_items[player][item] >= count
diff --git a/worlds/alttp/docs/en_A Link to the Past.md b/worlds/alttp/docs/en_A Link to the Past.md
index 6808f69e759f..1a2cb310ce07 100644
--- a/worlds/alttp/docs/en_A Link to the Past.md
+++ b/worlds/alttp/docs/en_A Link to the Past.md
@@ -1,8 +1,8 @@
# A Link to the Past
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
diff --git a/worlds/alttp/docs/multiworld_de.md b/worlds/alttp/docs/multiworld_de.md
index 38009fb58ed3..c8c802d75040 100644
--- a/worlds/alttp/docs/multiworld_de.md
+++ b/worlds/alttp/docs/multiworld_de.md
@@ -47,12 +47,12 @@ wählen können!
### Wo bekomme ich so eine YAML-Datei her?
-Die [Player Settings](/games/A Link to the Past/player-settings) Seite auf der Website ermöglicht das einfache Erstellen
+Die [Player Options](/games/A Link to the Past/player-options) Seite auf der Website ermöglicht das einfache Erstellen
und Herunterladen deiner eigenen `yaml` Datei. Drei verschiedene Voreinstellungen können dort gespeichert werden.
### Deine YAML-Datei ist gewichtet!
-Die **Player Settings** Seite hat eine Menge Optionen, die man per Schieber einstellen kann. Das ermöglicht es,
+Die **Player Options** Seite hat eine Menge Optionen, die man per Schieber einstellen kann. Das ermöglicht es,
verschiedene Optionen mit unterschiedlichen Wahrscheinlichkeiten in einer Kategorie ausgewürfelt zu werden
Als Beispiel kann man sich die Option "Map Shuffle" als einen Eimer mit Zetteln zur Abstimmung Vorstellen. So kann man
@@ -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_en.md b/worlds/alttp/docs/multiworld_en.md
index 3d27a7f7ffe0..5d7fc43e310f 100644
--- a/worlds/alttp/docs/multiworld_en.md
+++ b/worlds/alttp/docs/multiworld_en.md
@@ -2,15 +2,15 @@
## Required Software
-- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for
-`SNI Client - A Link to the Past Patch Setup`
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- [SNI](https://github.com/alttpo/sni/releases). This is automatically included with your Archipelago installation above.
- SNI is not compatible with (Q)Usb2Snes.
-- Hardware or software capable of loading and playing SNES ROM files
+- Hardware or software capable of loading and playing SNES ROM files, including:
- An emulator capable of connecting to SNI
- ([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
- [BizHawk](https://tasvideos.org/BizHawk), or
- [RetroArch](https://retroarch.com?page=platforms) 1.10.1 or newer). Or,
+ ([snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases), [snes9x-rr](https://github.com/gocha/snes9x-rr/releases),
+ [BSNES-plus](https://github.com/black-sliver/bsnes-plus),
+ [BizHawk](http://tasvideos.org/BizHawk.html), or
+ [RetroArch](https://retroarch.com?page=platforms) 1.10.1 or newer)
- An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware. **note:
modded SNES minis are currently not supported by SNI. Some users have claimed success with QUsb2Snes for this system,
but it is not supported.**
@@ -18,11 +18,12 @@ but it is not supported.**
## Installation Procedures
-1. Download and install SNIClient from the link above, making sure to install the most recent version.
- **The installer file is located in the assets section at the bottom of the version information**.
- - During setup, you will be asked to locate your base ROM file. This is your Japanese Link to the Past ROM file.
+1. Download and install [Archipelago](). **The installer
+ file is located in the assets section at the bottom of the version information.**
+2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
+ This is your Japanese Link to the Past ROM file. This only needs to be done once.
-2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
+3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
2. Right-click on a ROM file and select **Open with...**
@@ -47,6 +48,11 @@ client, and will also create your ROM in the same place as your patch file.
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
+#### snes9x-nwa
+
+1. Click on the Network Menu and check **Enable Emu Network Control**
+2. Load your ROM file if it hasn't already been loaded.
+
##### snes9x-rr
1. Load your ROM file if it hasn't already been loaded.
@@ -58,6 +64,11 @@ first time launching, you may be prompted to allow it to communicate through the
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
+#### BSNES-Plus
+
+1. Load your ROM file if it hasn't already been loaded.
+2. The emulator should automatically connect while SNI is running.
+
##### BizHawk
1. Ensure you have the BSNES core loaded. This is done with the main menubar, under:
diff --git a/worlds/alttp/docs/multiworld_es.md b/worlds/alttp/docs/multiworld_es.md
index 8576318bb997..a8ed11cd3202 100644
--- a/worlds/alttp/docs/multiworld_es.md
+++ b/worlds/alttp/docs/multiworld_es.md
@@ -59,12 +59,13 @@ de multiworld puede tener diferentes opciones.
### Donde puedo obtener un fichero YAML?
-La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-settings)" en el sitio web te permite configurar tu
+La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-options)" en el sitio web te permite configurar tu
configuración personal y descargar un fichero "YAML".
### Configuración YAML avanzada
-Una version mas avanzada del fichero Yaml puede ser creada usando la pagina ["Weighted settings"](/weighted-settings),
+Una version mas avanzada del fichero Yaml puede ser creada usando la pagina
+["Weighted settings"](/games/A Link to the Past/weighted-options),
la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones
representadas con controles deslizantes. Esto permite elegir cuan probable los valores de una categorÃa pueden ser
elegidos sobre otros de la misma.
@@ -82,11 +83,11 @@ 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
-1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-settings), configura tus opciones, haz
+1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-options), configura tus opciones, haz
click en el boton "Generate game".
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
3. Haz doble click en tu fichero de parche, y el emulador deberÃa ejecutar tu juego automáticamente. Como el Cliente no
diff --git a/worlds/alttp/docs/multiworld_fr.md b/worlds/alttp/docs/multiworld_fr.md
index 329ca6537573..310f3a4f96c4 100644
--- a/worlds/alttp/docs/multiworld_fr.md
+++ b/worlds/alttp/docs/multiworld_fr.md
@@ -60,15 +60,16 @@ peuvent avoir différentes options.
### Où est-ce que j'obtiens un fichier YAML ?
-La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings) vous permet de configurer vos
+La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-options) vous permet de configurer vos
paramètres personnels et de les exporter vers un fichier YAML.
### Configuration avancée du fichier YAML
Une version plus avancée du fichier YAML peut être créée en utilisant la page
-des [paramètres de pondération](/weighted-settings), qui vous permet de configurer jusqu'à trois préréglages. Cette page
-a de nombreuses options qui sont essentiellement représentées avec des curseurs glissants. Cela vous permet de choisir
-quelles sont les chances qu'une certaine option apparaisse par rapport aux autres disponibles dans une même catégorie.
+des [paramètres de pondération](/games/A Link to the Past/weighted-options), qui vous permet de configurer jusqu'Ã
+trois préréglages. Cette page a de nombreuses options qui sont essentiellement représentées avec des curseurs
+glissants. Cela vous permet de choisir quelles sont les chances qu'une certaine option apparaisse par rapport aux
+autres disponibles dans une même catégorie.
Par exemple, imaginez que le générateur crée un seau étiqueté "Mélange des cartes", et qu'il place un morceau de papier
pour chaque sous-option. Imaginez également que la valeur pour "On" est 20 et la valeur pour "Off" est 40.
@@ -83,11 +84,11 @@ 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
-1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings), configurez vos options,
+1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-options), configurez vos options,
et cliquez sur le bouton "Generate Game".
2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch.
3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client
@@ -207,4 +208,4 @@ Le logiciel recommandé pour l'auto-tracking actuellement est
3. Sélectionnez votre appareil SNES dans la liste déroulante.
4. Si vous voulez tracquer les petites clés ainsi que les objets des donjons, cochez la case **Race Illegal Tracking**
5. Cliquez sur le bouton **Start Autotracking**
-6. Fermez la fenêtre "AutoTracker" maintenant, elle n'est plus nécessaire
\ No newline at end of file
+6. Fermez la fenêtre "AutoTracker" maintenant, elle n'est plus nécessaire
diff --git a/worlds/alttp/test/__init__.py b/worlds/alttp/test/__init__.py
index e69de29bb2d1..307e75381d7e 100644
--- a/worlds/alttp/test/__init__.py
+++ b/worlds/alttp/test/__init__.py
@@ -0,0 +1,22 @@
+import unittest
+from argparse import Namespace
+
+from BaseClasses import MultiWorld, CollectionState
+from worlds import AutoWorldRegister
+
+
+class LTTPTestBase(unittest.TestCase):
+ def world_setup(self):
+ from worlds.alttp.Options import Medallion
+ self.multiworld = MultiWorld(1)
+ self.multiworld.game[1] = "A Link to the Past"
+ self.multiworld.state = CollectionState(self.multiworld)
+ self.multiworld.set_seed(None)
+ args = Namespace()
+ for name, option in AutoWorldRegister.world_types["A Link to the Past"].options_dataclass.type_hints.items():
+ setattr(args, name, {1: option.from_any(getattr(option, "default"))})
+ self.multiworld.set_options(args)
+ self.world = self.multiworld.worlds[1]
+ # by default medallion access is randomized, for unittests we set it to vanilla
+ self.world.options.misery_mire_medallion.value = Medallion.option_ether
+ self.world.options.turtle_rock_medallion.value = Medallion.option_quake
diff --git a/worlds/alttp/test/dungeons/TestAgahnimsTower.py b/worlds/alttp/test/dungeons/TestAgahnimsTower.py
index 6d0e1085f5cb..93c3f60463d0 100644
--- a/worlds/alttp/test/dungeons/TestAgahnimsTower.py
+++ b/worlds/alttp/test/dungeons/TestAgahnimsTower.py
@@ -7,18 +7,30 @@ def testTower(self):
self.starting_regions = ['Agahnims Tower']
self.run_tests([
["Castle Tower - Room 03", False, []],
- ["Castle Tower - Room 03", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Castle Tower - Room 03", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']],
["Castle Tower - Room 03", True, ['Progressive Sword']],
["Castle Tower - Dark Maze", False, []],
["Castle Tower - Dark Maze", False, [], ['Small Key (Agahnims Tower)']],
["Castle Tower - Dark Maze", False, [], ['Lamp']],
- ["Castle Tower - Dark Maze", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Castle Tower - Dark Maze", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']],
["Castle Tower - Dark Maze", True, ['Progressive Sword', 'Small Key (Agahnims Tower)', 'Lamp']],
+ ["Castle Tower - Dark Archer Key Drop", False, []],
+ ["Castle Tower - Dark Archer Key Drop", False, ['Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)']],
+ ["Castle Tower - Dark Archer Key Drop", False, [], ['Lamp']],
+ ["Castle Tower - Dark Archer Key Drop", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Castle Tower - Dark Archer Key Drop", True, ['Progressive Sword', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Lamp']],
+
+ ["Castle Tower - Circle of Pots Key Drop", False, []],
+ ["Castle Tower - Circle of Pots Key Drop", False, ['Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)']],
+ ["Castle Tower - Circle of Pots Key Drop", False, [], ['Lamp']],
+ ["Castle Tower - Circle of Pots Key Drop", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Castle Tower - Circle of Pots Key Drop", True, ['Progressive Sword', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Lamp']],
+
["Agahnim 1", False, []],
- ["Agahnim 1", False, ['Small Key (Agahnims Tower)'], ['Small Key (Agahnims Tower)']],
+ ["Agahnim 1", False, ['Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)']],
["Agahnim 1", False, [], ['Progressive Sword']],
["Agahnim 1", False, [], ['Lamp']],
- ["Agahnim 1", True, ['Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Lamp', 'Progressive Sword']],
+ ["Agahnim 1", True, ['Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Lamp', 'Progressive Sword']],
])
\ No newline at end of file
diff --git a/worlds/alttp/test/dungeons/TestDarkPalace.py b/worlds/alttp/test/dungeons/TestDarkPalace.py
index e3974e777da3..3912fbd282d9 100644
--- a/worlds/alttp/test/dungeons/TestDarkPalace.py
+++ b/worlds/alttp/test/dungeons/TestDarkPalace.py
@@ -11,29 +11,37 @@ def testDarkPalace(self):
["Palace of Darkness - The Arena - Ledge", False, []],
["Palace of Darkness - The Arena - Ledge", False, [], ['Progressive Bow']],
- ["Palace of Darkness - The Arena - Ledge", True, ['Progressive Bow']],
+ ["Palace of Darkness - The Arena - Ledge", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Palace of Darkness - The Arena - Ledge", True, ['Progressive Bow', 'Bomb Upgrade (+5)']],
["Palace of Darkness - Map Chest", False, []],
["Palace of Darkness - Map Chest", False, [], ['Progressive Bow']],
- ["Palace of Darkness - Map Chest", True, ['Progressive Bow']],
+ ["Palace of Darkness - Map Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Palace of Darkness - Map Chest", True, ['Progressive Bow', 'Bomb Upgrade (+5)']],
+ ["Palace of Darkness - Map Chest", True, ['Progressive Bow', 'Pegasus Boots']],
#Lower requirement for self-locking key
#No lower requirement when bow/hammer is out of logic
["Palace of Darkness - Big Key Chest", False, []],
["Palace of Darkness - Big Key Chest", False, [key]*5, [key]],
- ["Palace of Darkness - Big Key Chest", True, [key]*6],
+ ["Palace of Darkness - Big Key Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Palace of Darkness - Big Key Chest", True, [key]*6 + ['Bomb Upgrade (+5)']],
["Palace of Darkness - The Arena - Bridge", False, []],
["Palace of Darkness - The Arena - Bridge", False, [], [key, 'Progressive Bow']],
["Palace of Darkness - The Arena - Bridge", False, [], [key, 'Hammer']],
+ ["Palace of Darkness - The Arena - Bridge", False, [], [key, 'Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
["Palace of Darkness - The Arena - Bridge", True, [key]],
- ["Palace of Darkness - The Arena - Bridge", True, ['Progressive Bow', 'Hammer']],
+ ["Palace of Darkness - The Arena - Bridge", True, ['Progressive Bow', 'Hammer', 'Bomb Upgrade (+5)']],
+ ["Palace of Darkness - The Arena - Bridge", True, ['Progressive Bow', 'Hammer', 'Pegasus Boots']],
["Palace of Darkness - Stalfos Basement", False, []],
["Palace of Darkness - Stalfos Basement", False, [], [key, 'Progressive Bow']],
["Palace of Darkness - Stalfos Basement", False, [], [key, 'Hammer']],
+ ["Palace of Darkness - Stalfos Basement", False, [], [key, 'Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
["Palace of Darkness - Stalfos Basement", True, [key]],
- ["Palace of Darkness - Stalfos Basement", True, ['Progressive Bow', 'Hammer']],
+ ["Palace of Darkness - Stalfos Basement", True, ['Progressive Bow', 'Hammer', 'Bomb Upgrade (+5)']],
+ ["Palace of Darkness - Stalfos Basement", True, ['Progressive Bow', 'Hammer', 'Pegasus Boots']],
["Palace of Darkness - Compass Chest", False, []],
["Palace of Darkness - Compass Chest", False, [key]*3, [key]],
@@ -67,8 +75,9 @@ def testDarkPalace(self):
["Palace of Darkness - Big Chest", False, []],
["Palace of Darkness - Big Chest", False, [], ['Lamp']],
["Palace of Darkness - Big Chest", False, [], ['Big Key (Palace of Darkness)']],
+ ["Palace of Darkness - Big Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
["Palace of Darkness - Big Chest", False, [key]*5, [key]],
- ["Palace of Darkness - Big Chest", True, ['Lamp', 'Big Key (Palace of Darkness)'] + [key]*6],
+ ["Palace of Darkness - Big Chest", True, ['Bomb Upgrade (+5)', 'Lamp', 'Big Key (Palace of Darkness)'] + [key]*6],
["Palace of Darkness - Boss", False, []],
["Palace of Darkness - Boss", False, [], ['Lamp']],
diff --git a/worlds/alttp/test/dungeons/TestDesertPalace.py b/worlds/alttp/test/dungeons/TestDesertPalace.py
index 8423e681cf16..58e441f94585 100644
--- a/worlds/alttp/test/dungeons/TestDesertPalace.py
+++ b/worlds/alttp/test/dungeons/TestDesertPalace.py
@@ -18,22 +18,36 @@ def testDesertPalace(self):
["Desert Palace - Compass Chest", False, []],
["Desert Palace - Compass Chest", False, [], ['Small Key (Desert Palace)']],
- ["Desert Palace - Compass Chest", True, ['Small Key (Desert Palace)']],
+ ["Desert Palace - Compass Chest", False, ['Progressive Sword', 'Hammer', 'Fire Rod', 'Ice Rod', 'Progressive Bow', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Desert Palace - Compass Chest", True, ['Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)']],
+ ["Desert Palace - Compass Chest", True, ['Progressive Sword', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)']],
- #@todo: Require a real weapon for enemizer?
["Desert Palace - Big Key Chest", False, []],
["Desert Palace - Big Key Chest", False, [], ['Small Key (Desert Palace)']],
- ["Desert Palace - Big Key Chest", True, ['Small Key (Desert Palace)']],
+ ["Desert Palace - Big Key Chest", False, ['Progressive Sword', 'Hammer', 'Fire Rod', 'Ice Rod', 'Progressive Bow', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Desert Palace - Big Key Chest", False, ['Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)']],
+ ["Desert Palace - Big Key Chest", True, ['Progressive Sword', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)']],
+
+ ["Desert Palace - Desert Tiles 1 Pot Key", True, []],
+
+ ["Desert Palace - Beamos Hall Pot Key", False, []],
+ ["Desert Palace - Beamos Hall Pot Key", False, ['Small Key (Desert Palace)']],
+ ["Desert Palace - Beamos Hall Pot Key", False, ['Progressive Sword', 'Hammer', 'Fire Rod', 'Ice Rod', 'Progressive Bow', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Desert Palace - Beamos Hall Pot Key", True, ['Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Progressive Sword']],
+
+ ["Desert Palace - Desert Tiles 2 Pot Key", False, []],
+ ["Desert Palace - Desert Tiles 2 Pot Key", False, ['Small Key (Desert Palace)', 'Small Key (Desert Palace)']],
+ ["Desert Palace - Desert Tiles 2 Pot Key", False, ['Progressive Sword', 'Hammer', 'Fire Rod', 'Ice Rod', 'Progressive Bow', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Desert Palace - Desert Tiles 2 Pot Key", True, ['Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Progressive Sword']],
["Desert Palace - Boss", False, []],
- ["Desert Palace - Boss", False, [], ['Small Key (Desert Palace)']],
+ ["Desert Palace - Boss", False, ['Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)']],
["Desert Palace - Boss", False, [], ['Big Key (Desert Palace)']],
["Desert Palace - Boss", False, [], ['Lamp', 'Fire Rod']],
["Desert Palace - Boss", False, [], ['Progressive Sword', 'Hammer', 'Fire Rod', 'Ice Rod', 'Progressive Bow', 'Cane of Somaria', 'Cane of Byrna']],
- ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Fire Rod']],
- ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Progressive Sword']],
- ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Hammer']],
- ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Ice Rod']],
- ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Cane of Somaria']],
- ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Cane of Byrna']],
+ ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Fire Rod']],
+ ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Progressive Sword']],
+ ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Hammer']],
+ ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Cane of Somaria']],
+ ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Cane of Byrna']],
])
\ No newline at end of file
diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py
index 73ece1541733..91fc462c4ecc 100644
--- a/worlds/alttp/test/dungeons/TestDungeon.py
+++ b/worlds/alttp/test/dungeons/TestDungeon.py
@@ -1,28 +1,21 @@
-import unittest
-from argparse import Namespace
-
-from BaseClasses import MultiWorld, CollectionState, ItemClassification
+from BaseClasses import CollectionState, ItemClassification
from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import mandatory_connections, connect_simple
from worlds.alttp.ItemPool import difficulties
-from worlds.alttp.Items import ItemFactory
+from worlds.alttp.Items import item_factory
from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops
-from worlds import AutoWorld
+from worlds.alttp.test import LTTPTestBase
-class TestDungeon(unittest.TestCase):
+class TestDungeon(LTTPTestBase):
def setUp(self):
- self.multiworld = MultiWorld(1)
- self.multiworld.set_seed(None)
- args = Namespace()
- for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
- setattr(args, name, {1: option.from_any(option.default)})
- self.multiworld.set_options(args)
- self.multiworld.set_default_common_options()
+ self.world_setup()
self.starting_regions = [] # Where to start exploring
self.remove_exits = [] # Block dungeon exits
- self.multiworld.difficulty_requirements[1] = difficulties['normal']
+ self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
+ self.multiworld.bombless_start[1].value = True
+ self.multiworld.shuffle_capacity_upgrades[1].value = 2
create_regions(self.multiworld, 1)
self.multiworld.worlds[1].create_dungeons()
create_shops(self.multiworld, 1)
@@ -30,11 +23,11 @@ def setUp(self):
connect_simple(self.multiworld, exitname, regionname, 1)
connect_simple(self.multiworld, 'Big Bomb Shop', 'Big Bomb Shop', 1)
self.multiworld.get_region('Menu', 1).exits = []
- self.multiworld.swamp_patch_required[1] = True
- self.multiworld.worlds[1].set_rules()
- self.multiworld.worlds[1].create_items()
+ self.multiworld.worlds[1].swamp_patch_required = True
+ self.world.set_rules()
+ self.world.create_items()
self.multiworld.itempool.extend(get_dungeon_item_pool(self.multiworld))
- self.multiworld.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
+ self.multiworld.itempool.extend(item_factory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], self.world))
def run_tests(self, access_pool):
for exit in self.remove_exits:
@@ -47,9 +40,9 @@ def run_tests(self, access_pool):
if all_except and len(all_except) > 0:
items = self.multiworld.itempool[:]
items = [item for item in items if item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
- items.extend(ItemFactory(item_pool[0], 1))
+ items.extend(item_factory(item_pool[0], self.world))
else:
- items = ItemFactory(items, 1)
+ items = item_factory(items, self.world)
state = CollectionState(self.multiworld)
state.reachable_regions[1].add(self.multiworld.get_region('Menu', 1))
for region_name in self.starting_regions:
@@ -61,6 +54,7 @@ def run_tests(self, access_pool):
for item in items:
item.classification = ItemClassification.progression
- state.collect(item)
+ state.collect(item, event=True) # event=True prevents running sweep_for_events() and picking up
+ state.sweep_for_events() # key drop keys repeatedly
- self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access)
\ No newline at end of file
+ self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
\ No newline at end of file
diff --git a/worlds/alttp/test/dungeons/TestEasternPalace.py b/worlds/alttp/test/dungeons/TestEasternPalace.py
index 0497a1132ee3..ee8b7a16246f 100644
--- a/worlds/alttp/test/dungeons/TestEasternPalace.py
+++ b/worlds/alttp/test/dungeons/TestEasternPalace.py
@@ -18,12 +18,13 @@ def testEastern(self):
["Eastern Palace - Big Key Chest", False, []],
["Eastern Palace - Big Key Chest", False, [], ['Lamp']],
- ["Eastern Palace - Big Key Chest", True, ['Lamp']],
+ ["Eastern Palace - Big Key Chest", True, ['Lamp', 'Small Key (Eastern Palace)', 'Small Key (Eastern Palace)', 'Progressive Sword']],
#@todo: Advanced?
["Eastern Palace - Boss", False, []],
["Eastern Palace - Boss", False, [], ['Lamp']],
["Eastern Palace - Boss", False, [], ['Progressive Bow']],
["Eastern Palace - Boss", False, [], ['Big Key (Eastern Palace)']],
- ["Eastern Palace - Boss", True, ['Lamp', 'Progressive Bow', 'Big Key (Eastern Palace)']]
+ ["Eastern Palace - Boss", False, ['Small Key (Eastern Palace)', 'Small Key (Eastern Palace)']],
+ ["Eastern Palace - Boss", True, ['Lamp', 'Small Key (Eastern Palace)', 'Small Key (Eastern Palace)', 'Progressive Bow', 'Big Key (Eastern Palace)']]
])
diff --git a/worlds/alttp/test/dungeons/TestGanonsTower.py b/worlds/alttp/test/dungeons/TestGanonsTower.py
index f81509273f00..08274d0fe7d9 100644
--- a/worlds/alttp/test/dungeons/TestGanonsTower.py
+++ b/worlds/alttp/test/dungeons/TestGanonsTower.py
@@ -33,46 +33,50 @@ def testGanonsTower(self):
["Ganons Tower - Randomizer Room - Top Left", False, []],
["Ganons Tower - Randomizer Room - Top Left", False, [], ['Hammer']],
["Ganons Tower - Randomizer Room - Top Left", False, [], ['Hookshot']],
- ["Ganons Tower - Randomizer Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Randomizer Room - Top Left", False, [], ['Bomb Upgrade (50)']],
+ ["Ganons Tower - Randomizer Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Bomb Upgrade (50)']],
["Ganons Tower - Randomizer Room - Top Right", False, []],
["Ganons Tower - Randomizer Room - Top Right", False, [], ['Hammer']],
["Ganons Tower - Randomizer Room - Top Right", False, [], ['Hookshot']],
- ["Ganons Tower - Randomizer Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Randomizer Room - Top Right", False, [], ['Bomb Upgrade (50)']],
+ ["Ganons Tower - Randomizer Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Bomb Upgrade (50)']],
["Ganons Tower - Randomizer Room - Bottom Left", False, []],
["Ganons Tower - Randomizer Room - Bottom Left", False, [], ['Hammer']],
["Ganons Tower - Randomizer Room - Bottom Left", False, [], ['Hookshot']],
- ["Ganons Tower - Randomizer Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Randomizer Room - Bottom Left", False, [], ['Bomb Upgrade (50)']],
+ ["Ganons Tower - Randomizer Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Bomb Upgrade (50)']],
["Ganons Tower - Randomizer Room - Bottom Right", False, []],
["Ganons Tower - Randomizer Room - Bottom Right", False, [], ['Hammer']],
["Ganons Tower - Randomizer Room - Bottom Right", False, [], ['Hookshot']],
- ["Ganons Tower - Randomizer Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Randomizer Room - Bottom Right", False, [], ['Bomb Upgrade (50)']],
+ ["Ganons Tower - Randomizer Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Bomb Upgrade (50)']],
["Ganons Tower - Firesnake Room", False, []],
["Ganons Tower - Firesnake Room", False, [], ['Hammer']],
["Ganons Tower - Firesnake Room", False, [], ['Hookshot']],
- ["Ganons Tower - Firesnake Room", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Firesnake Room", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Map Chest", False, []],
["Ganons Tower - Map Chest", False, [], ['Hammer']],
["Ganons Tower - Map Chest", False, [], ['Hookshot', 'Pegasus Boots']],
- ["Ganons Tower - Map Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
- ["Ganons Tower - Map Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hammer', 'Pegasus Boots']],
+ ["Ganons Tower - Map Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Map Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hammer', 'Pegasus Boots']],
["Ganons Tower - Big Chest", False, []],
["Ganons Tower - Big Chest", False, [], ['Big Key (Ganons Tower)']],
- ["Ganons Tower - Big Chest", True, ['Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
- ["Ganons Tower - Big Chest", True, ['Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Big Chest", True, ['Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
+ ["Ganons Tower - Big Chest", True, ['Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Hope Room - Left", True, []],
["Ganons Tower - Hope Room - Right", True, []],
["Ganons Tower - Bob's Chest", False, []],
- ["Ganons Tower - Bob's Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
- ["Ganons Tower - Bob's Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Bob's Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
+ ["Ganons Tower - Bob's Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Tile Room", False, []],
["Ganons Tower - Tile Room", False, [], ['Cane of Somaria']],
@@ -81,34 +85,34 @@ def testGanonsTower(self):
["Ganons Tower - Compass Room - Top Left", False, []],
["Ganons Tower - Compass Room - Top Left", False, [], ['Cane of Somaria']],
["Ganons Tower - Compass Room - Top Left", False, [], ['Fire Rod']],
- ["Ganons Tower - Compass Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
+ ["Ganons Tower - Compass Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
["Ganons Tower - Compass Room - Top Right", False, []],
["Ganons Tower - Compass Room - Top Right", False, [], ['Cane of Somaria']],
["Ganons Tower - Compass Room - Top Right", False, [], ['Fire Rod']],
- ["Ganons Tower - Compass Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
+ ["Ganons Tower - Compass Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
["Ganons Tower - Compass Room - Bottom Left", False, []],
["Ganons Tower - Compass Room - Bottom Left", False, [], ['Cane of Somaria']],
["Ganons Tower - Compass Room - Bottom Left", False, [], ['Fire Rod']],
- ["Ganons Tower - Compass Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
+ ["Ganons Tower - Compass Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
["Ganons Tower - Compass Room - Bottom Right", False, []],
["Ganons Tower - Compass Room - Bottom Right", False, [], ['Cane of Somaria']],
["Ganons Tower - Compass Room - Bottom Right", False, [], ['Fire Rod']],
- ["Ganons Tower - Compass Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
+ ["Ganons Tower - Compass Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']],
["Ganons Tower - Big Key Chest", False, []],
- ["Ganons Tower - Big Key Chest", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
- ["Ganons Tower - Big Key Chest", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Big Key Chest", True, ['Bomb Upgrade (+5)', 'Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
+ ["Ganons Tower - Big Key Chest", True, ['Bomb Upgrade (+5)', 'Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Big Key Room - Left", False, []],
- ["Ganons Tower - Big Key Room - Left", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
- ["Ganons Tower - Big Key Room - Left", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Big Key Room - Left", True, ['Bomb Upgrade (+5)', 'Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
+ ["Ganons Tower - Big Key Room - Left", True, ['Bomb Upgrade (+5)', 'Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Big Key Room - Right", False, []],
- ["Ganons Tower - Big Key Room - Right", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
- ["Ganons Tower - Big Key Room - Right", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Big Key Room - Right", True, ['Bomb Upgrade (+5)', 'Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']],
+ ["Ganons Tower - Big Key Room - Right", True, ['Bomb Upgrade (+5)', 'Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']],
["Ganons Tower - Mini Helmasaur Room - Left", False, []],
["Ganons Tower - Mini Helmasaur Room - Left", False, [], ['Progressive Bow']],
@@ -128,8 +132,8 @@ def testGanonsTower(self):
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Progressive Bow']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Big Key (Ganons Tower)']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Lamp', 'Fire Rod']],
- ["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
- ["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
+ ["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
+ ["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
["Ganons Tower - Validation Chest", False, []],
["Ganons Tower - Validation Chest", False, [], ['Hookshot']],
@@ -137,8 +141,8 @@ def testGanonsTower(self):
["Ganons Tower - Validation Chest", False, [], ['Big Key (Ganons Tower)']],
["Ganons Tower - Validation Chest", False, [], ['Lamp', 'Fire Rod']],
["Ganons Tower - Validation Chest", False, [], ['Progressive Sword', 'Hammer']],
- ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
- ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
- ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
- ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
+ ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
+ ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
+ ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
])
\ No newline at end of file
diff --git a/worlds/alttp/test/dungeons/TestIcePalace.py b/worlds/alttp/test/dungeons/TestIcePalace.py
index 3c075fe5ea48..868631587375 100644
--- a/worlds/alttp/test/dungeons/TestIcePalace.py
+++ b/worlds/alttp/test/dungeons/TestIcePalace.py
@@ -11,8 +11,9 @@ def testIcePalace(self):
["Ice Palace - Big Key Chest", False, [], ['Progressive Glove']],
["Ice Palace - Big Key Chest", False, [], ['Fire Rod', 'Bombos']],
["Ice Palace - Big Key Chest", False, [], ['Fire Rod', 'Progressive Sword']],
- ["Ice Palace - Big Key Chest", True, ['Progressive Glove', 'Fire Rod', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)']],
- ["Ice Palace - Big Key Chest", True, ['Progressive Glove', 'Bombos', 'Progressive Sword', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)']],
+ ["Ice Palace - Big Key Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Ice Palace - Big Key Chest", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Fire Rod', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
+ ["Ice Palace - Big Key Chest", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Bombos', 'Progressive Sword', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
#@todo: Change from item randomizer - Right side key door is only in logic if big key is in there
#["Ice Palace - Big Key Chest", True, ['Progressive Glove', 'Cane of Byrna', 'Fire Rod', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
#["Ice Palace - Big Key Chest", True, ['Progressive Glove', 'Cane of Byrna', 'Bombos', 'Progressive Sword', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
@@ -22,16 +23,17 @@ def testIcePalace(self):
["Ice Palace - Compass Chest", False, []],
["Ice Palace - Compass Chest", False, [], ['Fire Rod', 'Bombos']],
["Ice Palace - Compass Chest", False, [], ['Fire Rod', 'Progressive Sword']],
- ["Ice Palace - Compass Chest", True, ['Fire Rod']],
- ["Ice Palace - Compass Chest", True, ['Bombos', 'Progressive Sword']],
+ ["Ice Palace - Compass Chest", True, ['Small Key (Ice Palace)', 'Fire Rod']],
+ ["Ice Palace - Compass Chest", True, ['Small Key (Ice Palace)', 'Bombos', 'Progressive Sword']],
["Ice Palace - Map Chest", False, []],
["Ice Palace - Map Chest", False, [], ['Hammer']],
["Ice Palace - Map Chest", False, [], ['Progressive Glove']],
["Ice Palace - Map Chest", False, [], ['Fire Rod', 'Bombos']],
["Ice Palace - Map Chest", False, [], ['Fire Rod', 'Progressive Sword']],
- ["Ice Palace - Map Chest", True, ['Progressive Glove', 'Fire Rod', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)']],
- ["Ice Palace - Map Chest", True, ['Progressive Glove', 'Bombos', 'Progressive Sword', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)']],
+ ["Ice Palace - Map Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Ice Palace - Map Chest", True, ['Small Key (Ice Palace)', 'Bomb Upgrade (+5)', 'Progressive Glove', 'Fire Rod', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)']],
+ ["Ice Palace - Map Chest", True, ['Small Key (Ice Palace)', 'Bomb Upgrade (+5)', 'Progressive Glove', 'Bombos', 'Progressive Sword', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)']],
#["Ice Palace - Map Chest", True, ['Progressive Glove', 'Cane of Byrna', 'Fire Rod', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
#["Ice Palace - Map Chest", True, ['Progressive Glove', 'Cane of Byrna', 'Bombos', 'Progressive Sword', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
#["Ice Palace - Map Chest", True, ['Progressive Glove', 'Cape', 'Fire Rod', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
@@ -40,8 +42,9 @@ def testIcePalace(self):
["Ice Palace - Spike Room", False, []],
["Ice Palace - Spike Room", False, [], ['Fire Rod', 'Bombos']],
["Ice Palace - Spike Room", False, [], ['Fire Rod', 'Progressive Sword']],
- ["Ice Palace - Spike Room", True, ['Fire Rod', 'Hookshot', 'Small Key (Ice Palace)']],
- ["Ice Palace - Spike Room", True, ['Bombos', 'Progressive Sword', 'Hookshot', 'Small Key (Ice Palace)']],
+ ["Ice Palace - Spike Room", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Ice Palace - Spike Room", True, ['Bomb Upgrade (+5)', 'Fire Rod', 'Hookshot', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
+ ["Ice Palace - Spike Room", True, ['Bomb Upgrade (+5)', 'Bombos', 'Progressive Sword', 'Hookshot', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
#["Ice Palace - Spike Room", True, ['Cape', 'Fire Rod', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
#["Ice Palace - Spike Room", True, ['Cape', 'Bombos', 'Progressive Sword', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
#["Ice Palace - Spike Room", True, ['Cane of Byrna', 'Fire Rod', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
@@ -50,21 +53,24 @@ def testIcePalace(self):
["Ice Palace - Freezor Chest", False, []],
["Ice Palace - Freezor Chest", False, [], ['Fire Rod', 'Bombos']],
["Ice Palace - Freezor Chest", False, [], ['Fire Rod', 'Progressive Sword']],
- ["Ice Palace - Freezor Chest", True, ['Fire Rod']],
- ["Ice Palace - Freezor Chest", True, ['Bombos', 'Progressive Sword']],
+ ["Ice Palace - Freezor Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Ice Palace - Freezor Chest", True, ['Bomb Upgrade (+5)', 'Fire Rod', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
+ ["Ice Palace - Freezor Chest", True, ['Bomb Upgrade (+5)', 'Bombos', 'Progressive Sword', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
["Ice Palace - Iced T Room", False, []],
["Ice Palace - Iced T Room", False, [], ['Fire Rod', 'Bombos']],
["Ice Palace - Iced T Room", False, [], ['Fire Rod', 'Progressive Sword']],
- ["Ice Palace - Iced T Room", True, ['Fire Rod']],
- ["Ice Palace - Iced T Room", True, ['Bombos', 'Progressive Sword']],
+ ["Ice Palace - Iced T Room", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Ice Palace - Iced T Room", True, ['Bomb Upgrade (+5)', 'Fire Rod', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
+ ["Ice Palace - Iced T Room", True, ['Bomb Upgrade (+5)', 'Bombos', 'Progressive Sword', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
["Ice Palace - Big Chest", False, []],
["Ice Palace - Big Chest", False, [], ['Big Key (Ice Palace)']],
["Ice Palace - Big Chest", False, [], ['Fire Rod', 'Bombos']],
["Ice Palace - Big Chest", False, [], ['Fire Rod', 'Progressive Sword']],
- ["Ice Palace - Big Chest", True, ['Big Key (Ice Palace)', 'Fire Rod']],
- ["Ice Palace - Big Chest", True, ['Big Key (Ice Palace)', 'Bombos', 'Progressive Sword']],
+ ["Ice Palace - Big Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Ice Palace - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Fire Rod']],
+ ["Ice Palace - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Bombos', 'Progressive Sword']],
["Ice Palace - Boss", False, []],
["Ice Palace - Boss", False, [], ['Hammer']],
@@ -72,8 +78,10 @@ def testIcePalace(self):
["Ice Palace - Boss", False, [], ['Big Key (Ice Palace)']],
["Ice Palace - Boss", False, [], ['Fire Rod', 'Bombos']],
["Ice Palace - Boss", False, [], ['Fire Rod', 'Progressive Sword']],
- ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
- ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)']],
- ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']],
- ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)']],
+ ["Ice Palace - Boss", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ # need hookshot now to reach the right side for the 6th key
+ ["Ice Palace - Boss", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Hookshot']],
+ ["Ice Palace - Boss", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Hookshot']],
+ ["Ice Palace - Boss", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Hookshot']],
+ ["Ice Palace - Boss", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Hookshot']],
])
\ No newline at end of file
diff --git a/worlds/alttp/test/dungeons/TestMiseryMire.py b/worlds/alttp/test/dungeons/TestMiseryMire.py
index ea5fb288450d..ca74e9365ee6 100644
--- a/worlds/alttp/test/dungeons/TestMiseryMire.py
+++ b/worlds/alttp/test/dungeons/TestMiseryMire.py
@@ -32,36 +32,32 @@ def testMiseryMire(self):
["Misery Mire - Main Lobby", False, []],
["Misery Mire - Main Lobby", False, [], ['Pegasus Boots', 'Hookshot']],
["Misery Mire - Main Lobby", False, [], ['Small Key (Misery Mire)', 'Big Key (Misery Mire)']],
- ["Misery Mire - Main Lobby", True, ['Small Key (Misery Mire)', 'Hookshot', 'Progressive Sword']],
- ["Misery Mire - Main Lobby", True, ['Small Key (Misery Mire)', 'Pegasus Boots', 'Progressive Sword']],
- ["Misery Mire - Main Lobby", True, ['Big Key (Misery Mire)', 'Hookshot', 'Progressive Sword']],
- ["Misery Mire - Main Lobby", True, ['Big Key (Misery Mire)', 'Pegasus Boots', 'Progressive Sword']],
+ ["Misery Mire - Main Lobby", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Hookshot', 'Progressive Sword']],
+ ["Misery Mire - Main Lobby", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Pegasus Boots', 'Progressive Sword']],
["Misery Mire - Big Key Chest", False, []],
["Misery Mire - Big Key Chest", False, [], ['Fire Rod', 'Lamp']],
["Misery Mire - Big Key Chest", False, [], ['Pegasus Boots', 'Hookshot']],
- ["Misery Mire - Big Key Chest", False, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)'], ['Small Key (Misery Mire)']],
- ["Misery Mire - Big Key Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Lamp', 'Progressive Sword', 'Pegasus Boots']],
- ["Misery Mire - Big Key Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Lamp', 'Progressive Sword', 'Hookshot']],
- ["Misery Mire - Big Key Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Fire Rod', 'Progressive Sword', 'Pegasus Boots']],
- ["Misery Mire - Big Key Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Fire Rod', 'Progressive Sword', 'Hookshot']],
+ ["Misery Mire - Big Key Chest", False, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)']],
+ ["Misery Mire - Big Key Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Lamp', 'Progressive Sword', 'Pegasus Boots']],
+ ["Misery Mire - Big Key Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Lamp', 'Progressive Sword', 'Hookshot']],
+ ["Misery Mire - Big Key Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Fire Rod', 'Progressive Sword', 'Pegasus Boots']],
+ ["Misery Mire - Big Key Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Fire Rod', 'Progressive Sword', 'Hookshot']],
["Misery Mire - Compass Chest", False, []],
["Misery Mire - Compass Chest", False, [], ['Fire Rod', 'Lamp']],
["Misery Mire - Compass Chest", False, [], ['Pegasus Boots', 'Hookshot']],
- ["Misery Mire - Compass Chest", False, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)'], ['Small Key (Misery Mire)']],
- ["Misery Mire - Compass Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Lamp', 'Progressive Sword', 'Pegasus Boots']],
- ["Misery Mire - Compass Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Lamp', 'Progressive Sword', 'Hookshot']],
- ["Misery Mire - Compass Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Fire Rod', 'Progressive Sword', 'Pegasus Boots']],
- ["Misery Mire - Compass Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Fire Rod', 'Progressive Sword', 'Hookshot']],
+ ["Misery Mire - Compass Chest", False, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)']],
+ ["Misery Mire - Compass Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Lamp', 'Progressive Sword', 'Pegasus Boots']],
+ ["Misery Mire - Compass Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Lamp', 'Progressive Sword', 'Hookshot']],
+ ["Misery Mire - Compass Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Fire Rod', 'Progressive Sword', 'Pegasus Boots']],
+ ["Misery Mire - Compass Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Fire Rod', 'Progressive Sword', 'Hookshot']],
["Misery Mire - Map Chest", False, []],
- ["Misery Mire - Map Chest", False, [], ['Small Key (Misery Mire)', 'Big Key (Misery Mire)']],
+ ["Misery Mire - Map Chest", False, [], ['Small Key (Misery Mire)']],
["Misery Mire - Map Chest", False, [], ['Pegasus Boots', 'Hookshot']],
- ["Misery Mire - Map Chest", True, ['Small Key (Misery Mire)', 'Progressive Sword', 'Pegasus Boots']],
- ["Misery Mire - Map Chest", True, ['Small Key (Misery Mire)', 'Progressive Sword', 'Hookshot']],
- ["Misery Mire - Map Chest", True, ['Big Key (Misery Mire)', 'Progressive Sword', 'Pegasus Boots']],
- ["Misery Mire - Map Chest", True, ['Big Key (Misery Mire)', 'Progressive Sword', 'Hookshot']],
+ ["Misery Mire - Map Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Progressive Sword', 'Pegasus Boots']],
+ ["Misery Mire - Map Chest", True, ['Small Key (Misery Mire)', 'Small Key (Misery Mire)', 'Progressive Sword', 'Hookshot']],
["Misery Mire - Spike Chest", False, []],
["Misery Mire - Spike Chest", False, [], ['Pegasus Boots', 'Hookshot']],
@@ -78,7 +74,8 @@ def testMiseryMire(self):
["Misery Mire - Boss", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow']],
["Misery Mire - Boss", False, [], ['Big Key (Misery Mire)']],
["Misery Mire - Boss", False, [], ['Pegasus Boots', 'Hookshot']],
- ["Misery Mire - Boss", True, ['Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Sword', 'Pegasus Boots']],
- ["Misery Mire - Boss", True, ['Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Hammer', 'Pegasus Boots']],
- ["Misery Mire - Boss", True, ['Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Bow', 'Pegasus Boots']],
+ ["Misery Mire - Boss", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Sword', 'Pegasus Boots']],
+ ["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Hammer', 'Pegasus Boots']],
+ ["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Bow', 'Pegasus Boots']],
])
\ No newline at end of file
diff --git a/worlds/alttp/test/dungeons/TestSkullWoods.py b/worlds/alttp/test/dungeons/TestSkullWoods.py
index 2dab840cf449..7650e785c871 100644
--- a/worlds/alttp/test/dungeons/TestSkullWoods.py
+++ b/worlds/alttp/test/dungeons/TestSkullWoods.py
@@ -8,7 +8,8 @@ def testSkullWoodsFrontAllEntrances(self):
self.run_tests([
["Skull Woods - Big Chest", False, []],
["Skull Woods - Big Chest", False, [], ['Big Key (Skull Woods)']],
- ["Skull Woods - Big Chest", True, ['Big Key (Skull Woods)']],
+ ["Skull Woods - Big Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Skull Woods - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Skull Woods)']],
["Skull Woods - Compass Chest", True, []],
@@ -26,18 +27,18 @@ def testSkullWoodsFrontOnly(self):
["Skull Woods - Big Chest", False, [], ['Never in logic']],
["Skull Woods - Compass Chest", False, []],
- ["Skull Woods - Compass Chest", False, ['Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
- ["Skull Woods - Compass Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
+ ["Skull Woods - Compass Chest", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Compass Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
["Skull Woods - Map Chest", True, []],
["Skull Woods - Pot Prison", False, []],
- ["Skull Woods - Pot Prison", False, ['Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
- ["Skull Woods - Pot Prison", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
+ ["Skull Woods - Pot Prison", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Pot Prison", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
["Skull Woods - Pinball Room", False, []],
- ["Skull Woods - Pinball Room", False, [], ['Small Key (Skull Woods)']],
- ["Skull Woods - Pinball Room", True, ['Small Key (Skull Woods)']]
+ ["Skull Woods - Pinball Room", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Pinball Room", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
])
def testSkullWoodsLeftOnly(self):
@@ -50,8 +51,8 @@ def testSkullWoodsLeftOnly(self):
["Skull Woods - Compass Chest", True, []],
["Skull Woods - Map Chest", False, []],
- ["Skull Woods - Map Chest", False, [], ['Small Key (Skull Woods)']],
- ["Skull Woods - Map Chest", True, ['Small Key (Skull Woods)']],
+ ["Skull Woods - Map Chest", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Map Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
["Skull Woods - Pot Prison", True, []],
@@ -64,21 +65,22 @@ def testSkullWoodsBackOnly(self):
self.run_tests([
["Skull Woods - Big Chest", False, []],
["Skull Woods - Big Chest", False, [], ['Big Key (Skull Woods)']],
- ["Skull Woods - Big Chest", True, ['Big Key (Skull Woods)']],
+ ["Skull Woods - Big Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Skull Woods - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Skull Woods)']],
["Skull Woods - Compass Chest", False, []],
- ["Skull Woods - Compass Chest", False, ['Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
- ["Skull Woods - Compass Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
+ ["Skull Woods - Compass Chest", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Compass Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
["Skull Woods - Map Chest", True, []],
["Skull Woods - Pot Prison", False, []],
- ["Skull Woods - Pot Prison", False, ['Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
- ["Skull Woods - Pot Prison", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
+ ["Skull Woods - Pot Prison", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Pot Prison", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']],
["Skull Woods - Pinball Room", False, []],
- ["Skull Woods - Pinball Room", False, [], ['Small Key (Skull Woods)']],
- ["Skull Woods - Pinball Room", True, ['Small Key (Skull Woods)']]
+ ["Skull Woods - Pinball Room", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Pinball Room", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']]
])
def testSkullWoodsMiddle(self):
@@ -94,6 +96,6 @@ def testSkullWoodsBack(self):
["Skull Woods - Boss", False, []],
["Skull Woods - Boss", False, [], ['Fire Rod']],
["Skull Woods - Boss", False, [], ['Progressive Sword']],
- ["Skull Woods - Boss", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
- ["Skull Woods - Boss", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Fire Rod', 'Progressive Sword']],
+ ["Skull Woods - Boss", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']],
+ ["Skull Woods - Boss", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Fire Rod', 'Progressive Sword']],
])
\ No newline at end of file
diff --git a/worlds/alttp/test/dungeons/TestSwampPalace.py b/worlds/alttp/test/dungeons/TestSwampPalace.py
index 51440f6ccc4d..fb0672a5a9cc 100644
--- a/worlds/alttp/test/dungeons/TestSwampPalace.py
+++ b/worlds/alttp/test/dungeons/TestSwampPalace.py
@@ -16,35 +16,36 @@ def testSwampPalace(self):
["Swamp Palace - Big Chest", False, [], ['Open Floodgate']],
["Swamp Palace - Big Chest", False, [], ['Hammer']],
["Swamp Palace - Big Chest", False, [], ['Big Key (Swamp Palace)']],
- ["Swamp Palace - Big Chest", False, [], ['Small Key (Swamp Palace)']],
- ["Swamp Palace - Big Chest", True, ['Open Floodgate', 'Big Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
+ ["Swamp Palace - Big Chest", False, [], ['Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)']],
+ ["Swamp Palace - Big Chest", True, ['Open Floodgate', 'Big Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
["Swamp Palace - Big Key Chest", False, []],
["Swamp Palace - Big Key Chest", False, [], ['Flippers']],
["Swamp Palace - Big Key Chest", False, [], ['Open Floodgate']],
["Swamp Palace - Big Key Chest", False, [], ['Hammer']],
["Swamp Palace - Big Key Chest", False, [], ['Small Key (Swamp Palace)']],
- ["Swamp Palace - Big Key Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
+ ["Swamp Palace - Big Key Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
["Swamp Palace - Map Chest", False, []],
["Swamp Palace - Map Chest", False, [], ['Flippers']],
["Swamp Palace - Map Chest", False, [], ['Open Floodgate']],
["Swamp Palace - Map Chest", False, [], ['Small Key (Swamp Palace)']],
- ["Swamp Palace - Map Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Flippers']],
+ ["Swamp Palace - Map Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Swamp Palace - Map Chest", True, ['Bomb Upgrade (+5)', 'Open Floodgate', 'Small Key (Swamp Palace)', 'Flippers']],
["Swamp Palace - West Chest", False, []],
["Swamp Palace - West Chest", False, [], ['Flippers']],
["Swamp Palace - West Chest", False, [], ['Open Floodgate']],
["Swamp Palace - West Chest", False, [], ['Hammer']],
["Swamp Palace - West Chest", False, [], ['Small Key (Swamp Palace)']],
- ["Swamp Palace - West Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
+ ["Swamp Palace - West Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
["Swamp Palace - Compass Chest", False, []],
["Swamp Palace - Compass Chest", False, [], ['Flippers']],
["Swamp Palace - Compass Chest", False, [], ['Open Floodgate']],
["Swamp Palace - Compass Chest", False, [], ['Hammer']],
["Swamp Palace - Compass Chest", False, [], ['Small Key (Swamp Palace)']],
- ["Swamp Palace - Compass Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
+ ["Swamp Palace - Compass Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
["Swamp Palace - Flooded Room - Left", False, []],
["Swamp Palace - Flooded Room - Left", False, [], ['Flippers']],
@@ -52,7 +53,7 @@ def testSwampPalace(self):
["Swamp Palace - Flooded Room - Left", False, [], ['Hammer']],
["Swamp Palace - Flooded Room - Left", False, [], ['Hookshot']],
["Swamp Palace - Flooded Room - Left", False, [], ['Small Key (Swamp Palace)']],
- ["Swamp Palace - Flooded Room - Left", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer', 'Hookshot']],
+ ["Swamp Palace - Flooded Room - Left", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer', 'Hookshot']],
["Swamp Palace - Flooded Room - Right", False, []],
["Swamp Palace - Flooded Room - Right", False, [], ['Flippers']],
@@ -60,7 +61,7 @@ def testSwampPalace(self):
["Swamp Palace - Flooded Room - Right", False, [], ['Hammer']],
["Swamp Palace - Flooded Room - Right", False, [], ['Hookshot']],
["Swamp Palace - Flooded Room - Right", False, [], ['Small Key (Swamp Palace)']],
- ["Swamp Palace - Flooded Room - Right", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer', 'Hookshot']],
+ ["Swamp Palace - Flooded Room - Right", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer', 'Hookshot']],
["Swamp Palace - Waterfall Room", False, []],
["Swamp Palace - Waterfall Room", False, [], ['Flippers']],
@@ -68,7 +69,7 @@ def testSwampPalace(self):
["Swamp Palace - Waterfall Room", False, [], ['Hammer']],
["Swamp Palace - Waterfall Room", False, [], ['Hookshot']],
["Swamp Palace - Waterfall Room", False, [], ['Small Key (Swamp Palace)']],
- ["Swamp Palace - Waterfall Room", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer', 'Hookshot']],
+ ["Swamp Palace - Waterfall Room", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer', 'Hookshot']],
["Swamp Palace - Boss", False, []],
["Swamp Palace - Boss", False, [], ['Flippers']],
@@ -76,5 +77,5 @@ def testSwampPalace(self):
["Swamp Palace - Boss", False, [], ['Hammer']],
["Swamp Palace - Boss", False, [], ['Hookshot']],
["Swamp Palace - Boss", False, [], ['Small Key (Swamp Palace)']],
- ["Swamp Palace - Boss", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer', 'Hookshot']],
+ ["Swamp Palace - Boss", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer', 'Hookshot']],
])
\ No newline at end of file
diff --git a/worlds/alttp/test/dungeons/TestThievesTown.py b/worlds/alttp/test/dungeons/TestThievesTown.py
index a7e20bc52014..1cd3f5ca8f39 100644
--- a/worlds/alttp/test/dungeons/TestThievesTown.py
+++ b/worlds/alttp/test/dungeons/TestThievesTown.py
@@ -6,10 +6,6 @@ class TestThievesTown(TestDungeon):
def testThievesTown(self):
self.starting_regions = ['Thieves Town (Entrance)']
self.run_tests([
- ["Thieves' Town - Attic", False, []],
- ["Thieves' Town - Attic", False, [], ['Big Key (Thieves Town)']],
- ["Thieves' Town - Attic", False, [], ['Small Key (Thieves Town)']],
- ["Thieves' Town - Attic", True, ['Big Key (Thieves Town)', 'Small Key (Thieves Town)']],
["Thieves' Town - Big Key Chest", True, []],
@@ -19,22 +15,37 @@ def testThievesTown(self):
["Thieves' Town - Ambush Chest", True, []],
+ ["Thieves' Town - Hallway Pot Key", False, []],
+ ["Thieves' Town - Hallway Pot Key", False, [], ['Big Key (Thieves Town)']],
+ ["Thieves' Town - Hallway Pot Key", True, ['Big Key (Thieves Town)']],
+
+ ["Thieves' Town - Spike Switch Pot Key", False, []],
+ ["Thieves' Town - Spike Switch Pot Key", False, [], ['Big Key (Thieves Town)']],
+ ["Thieves' Town - Spike Switch Pot Key", False, [], ['Small Key (Thieves Town)']],
+ ["Thieves' Town - Spike Switch Pot Key", True, ['Big Key (Thieves Town)', 'Small Key (Thieves Town)']],
+
+ ["Thieves' Town - Attic", False, []],
+ ["Thieves' Town - Attic", False, [], ['Big Key (Thieves Town)']],
+ ["Thieves' Town - Attic", False, [], ['Small Key (Thieves Town)']],
+ ["Thieves' Town - Attic", True, ['Big Key (Thieves Town)', 'Small Key (Thieves Town)', 'Small Key (Thieves Town)', 'Small Key (Thieves Town)']],
+
["Thieves' Town - Big Chest", False, []],
["Thieves' Town - Big Chest", False, [], ['Big Key (Thieves Town)']],
["Thieves' Town - Big Chest", False, [], ['Small Key (Thieves Town)']],
["Thieves' Town - Big Chest", False, [], ['Hammer']],
- ["Thieves' Town - Big Chest", True, ['Hammer', 'Small Key (Thieves Town)', 'Big Key (Thieves Town)']],
+ ["Thieves' Town - Big Chest", True, ['Hammer', 'Small Key (Thieves Town)', 'Small Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Thieves Town)']],
["Thieves' Town - Blind's Cell", False, []],
["Thieves' Town - Blind's Cell", False, [], ['Big Key (Thieves Town)']],
- ["Thieves' Town - Blind's Cell", True, ['Big Key (Thieves Town)']],
+ ["Thieves' Town - Blind's Cell", False, [], ['Small Key (Thieves Town)']],
+ ["Thieves' Town - Blind's Cell", True, ['Big Key (Thieves Town)', 'Small Key (Thieves Town)']],
["Thieves' Town - Boss", False, []],
["Thieves' Town - Boss", False, [], ['Big Key (Thieves Town)']],
- ["Thieves' Town - Boss", False, [], ['Small Key (Thieves Town)']],
["Thieves' Town - Boss", False, [], ['Hammer', 'Progressive Sword', 'Cane of Somaria', 'Cane of Byrna']],
- ["Thieves' Town - Boss", True, ['Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Hammer']],
- ["Thieves' Town - Boss", True, ['Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Progressive Sword']],
- ["Thieves' Town - Boss", True, ['Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Cane of Somaria']],
- ["Thieves' Town - Boss", True, ['Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Cane of Byrna']],
+ ["Thieves' Town - Boss", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Thieves' Town - Boss", True, ['Bomb Upgrade (+5)', 'Small Key (Thieves Town)', 'Small Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Hammer']],
+ ["Thieves' Town - Boss", True, ['Bomb Upgrade (+5)', 'Small Key (Thieves Town)', 'Small Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Progressive Sword']],
+ ["Thieves' Town - Boss", True, ['Bomb Upgrade (+5)', 'Small Key (Thieves Town)', 'Small Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Cane of Somaria']],
+ ["Thieves' Town - Boss", True, ['Bomb Upgrade (+5)', 'Small Key (Thieves Town)', 'Small Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Cane of Byrna']],
])
\ No newline at end of file
diff --git a/worlds/alttp/test/dungeons/TestTowerOfHera.py b/worlds/alttp/test/dungeons/TestTowerOfHera.py
index 04685a66a876..29cbcbf91fe2 100644
--- a/worlds/alttp/test/dungeons/TestTowerOfHera.py
+++ b/worlds/alttp/test/dungeons/TestTowerOfHera.py
@@ -9,20 +9,24 @@ def testTowerOfHera(self):
["Tower of Hera - Big Key Chest", False, []],
["Tower of Hera - Big Key Chest", False, [], ['Small Key (Tower of Hera)']],
["Tower of Hera - Big Key Chest", False, [], ['Lamp', 'Fire Rod']],
- ["Tower of Hera - Big Key Chest", True, ['Small Key (Tower of Hera)', 'Lamp']],
+ ["Tower of Hera - Big Key Chest", True, ['Small Key (Tower of Hera)', 'Lamp', 'Bomb Upgrade (50)']],
["Tower of Hera - Big Key Chest", True, ['Small Key (Tower of Hera)', 'Fire Rod']],
- ["Tower of Hera - Basement Cage", True, []],
+ ["Tower of Hera - Basement Cage", False, []],
+ ["Tower of Hera - Basement Cage", True, ['Bomb Upgrade (50)']],
+ ["Tower of Hera - Basement Cage", True, ['Progressive Sword']],
- ["Tower of Hera - Map Chest", True, []],
+ ["Tower of Hera - Map Chest", False, []],
+ ["Tower of Hera - Map Chest", True, ['Bomb Upgrade (50)']],
+ ["Tower of Hera - Map Chest", True, ['Progressive Sword']],
["Tower of Hera - Compass Chest", False, []],
["Tower of Hera - Compass Chest", False, [], ['Big Key (Tower of Hera)']],
- ["Tower of Hera - Compass Chest", True, ['Big Key (Tower of Hera)']],
+ ["Tower of Hera - Compass Chest", True, ['Big Key (Tower of Hera)', 'Progressive Sword']],
["Tower of Hera - Big Chest", False, []],
["Tower of Hera - Big Chest", False, [], ['Big Key (Tower of Hera)']],
- ["Tower of Hera - Big Chest", True, ['Big Key (Tower of Hera)']],
+ ["Tower of Hera - Big Chest", True, ['Big Key (Tower of Hera)', 'Progressive Sword']],
["Tower of Hera - Boss", False, []],
["Tower of Hera - Boss", False, [], ['Big Key (Tower of Hera)']],
diff --git a/worlds/alttp/test/inverted/TestInverted.py b/worlds/alttp/test/inverted/TestInverted.py
index ad7458202ead..a0a654991b43 100644
--- a/worlds/alttp/test/inverted/TestInverted.py
+++ b/worlds/alttp/test/inverted/TestInverted.py
@@ -1,37 +1,30 @@
-from argparse import Namespace
-
-from BaseClasses import MultiWorld
-from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
+from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances
from worlds.alttp.InvertedRegions import create_inverted_regions
from worlds.alttp.ItemPool import difficulties
-from worlds.alttp.Items import ItemFactory
+from worlds.alttp.Items import item_factory
from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops
-from test.TestBase import TestBase
+from test.bases import TestBase
+
+from worlds.alttp.test import LTTPTestBase
-from worlds import AutoWorld
-class TestInverted(TestBase):
+class TestInverted(TestBase, LTTPTestBase):
def setUp(self):
- self.multiworld = MultiWorld(1)
- self.multiworld.set_seed(None)
- args = Namespace()
- for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
- setattr(args, name, {1: option.from_any(option.default)})
- self.multiworld.set_options(args)
- self.multiworld.set_default_common_options()
- self.multiworld.difficulty_requirements[1] = difficulties['normal']
- self.multiworld.mode[1] = "inverted"
+ self.world_setup()
+ self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
+ self.multiworld.mode[1].value = 2
+ self.multiworld.bombless_start[1].value = True
+ self.multiworld.shuffle_capacity_upgrades[1].value = 2
create_inverted_regions(self.multiworld, 1)
- self.multiworld.worlds[1].create_dungeons()
+ self.world.create_dungeons()
create_shops(self.multiworld, 1)
link_inverted_entrances(self.multiworld, 1)
- self.multiworld.worlds[1].create_items()
- self.multiworld.required_medallions[1] = ['Ether', 'Quake']
+ self.world.create_items()
self.multiworld.itempool.extend(get_dungeon_item_pool(self.multiworld))
- self.multiworld.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
+ self.multiworld.itempool.extend(item_factory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], self.world))
self.multiworld.get_location('Agahnim 1', 1).item = None
self.multiworld.get_location('Agahnim 2', 1).item = None
mark_light_world_regions(self.multiworld, 1)
- self.multiworld.worlds[1].set_rules()
+ self.world.set_rules()
diff --git a/worlds/alttp/test/inverted/TestInvertedBombRules.py b/worlds/alttp/test/inverted/TestInvertedBombRules.py
index 89c5d7860323..a33beca7a9f9 100644
--- a/worlds/alttp/test/inverted/TestInvertedBombRules.py
+++ b/worlds/alttp/test/inverted/TestInvertedBombRules.py
@@ -1,28 +1,18 @@
-import unittest
-from argparse import Namespace
-
-from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons
from worlds.alttp.EntranceShuffle import connect_entrance, Inverted_LW_Entrances, Inverted_LW_Dungeon_Entrances, Inverted_LW_Single_Cave_Doors, Inverted_Old_Man_Entrances, Inverted_DW_Entrances, Inverted_DW_Dungeon_Entrances, Inverted_DW_Single_Cave_Doors, \
Inverted_LW_Entrances_Must_Exit, Inverted_LW_Dungeon_Entrances_Must_Exit, Inverted_Bomb_Shop_Multi_Cave_Doors, Inverted_Bomb_Shop_Single_Cave_Doors, Blacksmith_Single_Cave_Doors, Inverted_Blacksmith_Multi_Cave_Doors
from worlds.alttp.InvertedRegions import create_inverted_regions
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Rules import set_inverted_big_bomb_rules
-from worlds import AutoWorld
+from worlds.alttp.test import LTTPTestBase
-class TestInvertedBombRules(unittest.TestCase):
+class TestInvertedBombRules(LTTPTestBase):
def setUp(self):
- self.multiworld = MultiWorld(1)
- self.multiworld.set_seed(None)
- self.multiworld.mode[1] = "inverted"
- args = Namespace
- for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
- setattr(args, name, {1: option.from_any(option.default)})
- self.multiworld.set_options(args)
- self.multiworld.set_default_common_options()
- self.multiworld.difficulty_requirements[1] = difficulties['normal']
+ self.world_setup()
+ self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
+ self.multiworld.mode[1].value = 2
create_inverted_regions(self.multiworld, 1)
self.multiworld.worlds[1].create_dungeons()
diff --git a/worlds/alttp/test/inverted/TestInvertedDarkWorld.py b/worlds/alttp/test/inverted/TestInvertedDarkWorld.py
index 710ee07f2b6d..16b837ee6556 100644
--- a/worlds/alttp/test/inverted/TestInvertedDarkWorld.py
+++ b/worlds/alttp/test/inverted/TestInvertedDarkWorld.py
@@ -5,7 +5,8 @@ class TestInvertedDarkWorld(TestInverted):
def testNorthWest(self):
self.run_location_tests([
- ["Brewery", True, []],
+ ["Brewery", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Brewery", True, ['Bomb Upgrade (+5)']],
["C-Shaped House", True, []],
@@ -77,15 +78,16 @@ def testNorthEast(self):
def testSouth(self):
self.run_location_tests([
- ["Hype Cave - Top", True, []],
-
- ["Hype Cave - Middle Right", True, []],
-
- ["Hype Cave - Middle Left", True, []],
-
- ["Hype Cave - Bottom", True, []],
-
- ["Hype Cave - Generous Guy", True, []],
+ ["Hype Cave - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)']],
+ ["Hype Cave - Middle Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)']],
+ ["Hype Cave - Middle Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)']],
+ ["Hype Cave - Bottom", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)']],
+ ["Hype Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)']],
["Stumpy", True, []],
diff --git a/worlds/alttp/test/inverted/TestInvertedDeathMountain.py b/worlds/alttp/test/inverted/TestInvertedDeathMountain.py
index aedec2a1daa6..605a9dc3f3a9 100644
--- a/worlds/alttp/test/inverted/TestInvertedDeathMountain.py
+++ b/worlds/alttp/test/inverted/TestInvertedDeathMountain.py
@@ -40,10 +40,12 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Far Left", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Far Left", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']],
["Paradox Cave Lower - Far Left", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Left", True, ['Flute', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Paradox Cave Lower - Far Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Cane of Somaria', 'Fire Rod']],
+ ["Paradox Cave Lower - Far Left", True, ['Flute', 'Hookshot', 'Moon Pearl', 'Bomb Upgrade (+5)']],
+ ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Progressive Sword', 'Progressive Sword']],
+ ["Paradox Cave Lower - Far Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Fire Rod']],
+ ["Paradox Cave Lower - Far Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Progressive Bow']],
["Paradox Cave Lower - Left", False, []],
["Paradox Cave Lower - Left", False, [], ['Moon Pearl']],
@@ -52,10 +54,12 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Left", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Left", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']],
["Paradox Cave Lower - Left", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']],
- ["Paradox Cave Lower - Left", True, ['Flute', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
- ["Paradox Cave Lower - Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Paradox Cave Lower - Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Cane of Somaria', 'Fire Rod']],
+ ["Paradox Cave Lower - Left", True, ['Flute', 'Hookshot', 'Moon Pearl', 'Bomb Upgrade (+5)']],
+ ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Progressive Sword', 'Progressive Sword']],
+ ["Paradox Cave Lower - Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Fire Rod']],
+ ["Paradox Cave Lower - Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Progressive Bow']],
["Paradox Cave Lower - Middle", False, []],
["Paradox Cave Lower - Middle", False, [], ['Moon Pearl']],
@@ -64,10 +68,12 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Middle", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Middle", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']],
["Paradox Cave Lower - Middle", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']],
- ["Paradox Cave Lower - Middle", True, ['Flute', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
- ["Paradox Cave Lower - Middle", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Paradox Cave Lower - Middle", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Cane of Somaria', 'Fire Rod']],
+ ["Paradox Cave Lower - Middle", True, ['Flute', 'Hookshot', 'Moon Pearl', 'Bomb Upgrade (+5)']],
+ ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Progressive Sword', 'Progressive Sword']],
+ ["Paradox Cave Lower - Middle", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Fire Rod']],
+ ["Paradox Cave Lower - Middle", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Progressive Bow']],
["Paradox Cave Lower - Right", False, []],
["Paradox Cave Lower - Right", False, [], ['Moon Pearl']],
@@ -76,10 +82,12 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Right", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Right", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']],
["Paradox Cave Lower - Right", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']],
- ["Paradox Cave Lower - Right", True, ['Flute', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
- ["Paradox Cave Lower - Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Paradox Cave Lower - Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Cane of Somaria', 'Fire Rod']],
+ ["Paradox Cave Lower - Right", True, ['Flute', 'Hookshot', 'Moon Pearl', 'Bomb Upgrade (+5)']],
+ ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Progressive Sword', 'Progressive Sword']],
+ ["Paradox Cave Lower - Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Fire Rod']],
+ ["Paradox Cave Lower - Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Progressive Bow']],
["Paradox Cave Lower - Far Right", False, []],
["Paradox Cave Lower - Far Right", False, [], ['Moon Pearl']],
@@ -88,10 +96,12 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Far Right", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Far Right", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']],
["Paradox Cave Lower - Far Right", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Right", True, ['Flute', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Paradox Cave Lower - Far Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Cane of Somaria', 'Fire Rod']],
+ ["Paradox Cave Lower - Far Right", True, ['Flute', 'Hookshot', 'Moon Pearl', 'Bomb Upgrade (+5)']],
+ ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Progressive Sword', 'Progressive Sword']],
+ ["Paradox Cave Lower - Far Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Fire Rod']],
+ ["Paradox Cave Lower - Far Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Progressive Bow']],
["Paradox Cave Upper - Left", False, []],
["Paradox Cave Upper - Left", False, [], ['Moon Pearl']],
@@ -100,10 +110,11 @@ def testEastDeathMountain(self):
["Paradox Cave Upper - Left", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']],
["Paradox Cave Upper - Left", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']],
["Paradox Cave Upper - Left", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']],
- ["Paradox Cave Upper - Left", True, ['Flute', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
- ["Paradox Cave Upper - Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Paradox Cave Upper - Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
["Paradox Cave Upper - Right", False, []],
["Paradox Cave Upper - Right", False, [], ['Moon Pearl']],
@@ -112,20 +123,22 @@ def testEastDeathMountain(self):
["Paradox Cave Upper - Right", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']],
["Paradox Cave Upper - Right", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']],
["Paradox Cave Upper - Right", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']],
- ["Paradox Cave Upper - Right", True, ['Flute', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
- ["Paradox Cave Upper - Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Paradox Cave Upper - Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
["Mimic Cave", False, []],
["Mimic Cave", False, [], ['Moon Pearl']],
["Mimic Cave", False, [], ['Hammer']],
["Mimic Cave", False, [], ['Progressive Glove', 'Flute']],
["Mimic Cave", False, [], ['Lamp', 'Flute']],
- ["Mimic Cave", True, ['Flute', 'Moon Pearl', 'Hammer', 'Hookshot']],
- ["Mimic Cave", True, ['Flute', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hammer']],
- ["Mimic Cave", True, ['Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer', 'Hookshot']],
- ["Mimic Cave", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer']],
+ ["Mimic Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Bow', 'Cane of Somaria', 'Progressive Sword']],
+ ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Flute', 'Moon Pearl', 'Hammer', 'Hookshot']],
+ ["Mimic Cave", True, ['Progressive Bow', 'Flute', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hammer']],
+ ["Mimic Cave", True, ['Cane of Somaria', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer', 'Hookshot']],
+ ["Mimic Cave", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer']],
["Ether Tablet", False, []],
["Ether Tablet", False, [], ['Moon Pearl']],
diff --git a/worlds/alttp/test/inverted/TestInvertedLightWorld.py b/worlds/alttp/test/inverted/TestInvertedLightWorld.py
index 9d4b9099daae..77af09317241 100644
--- a/worlds/alttp/test/inverted/TestInvertedLightWorld.py
+++ b/worlds/alttp/test/inverted/TestInvertedLightWorld.py
@@ -44,15 +44,17 @@ def testKakariko(self):
["Chicken House", False, []],
["Chicken House", False, [], ['Moon Pearl']],
- ["Chicken House", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Chicken House", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Chicken House", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Chicken House", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Chicken House", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Chicken House", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Chicken House", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Kakariko Well - Top", False, []],
["Kakariko Well - Top", False, [], ['Moon Pearl']],
- ["Kakariko Well - Top", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Kakariko Well - Top", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Kakariko Well - Top", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Kakariko Well - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Kakariko Well - Left", False, []],
["Kakariko Well - Left", False, [], ['Moon Pearl']],
@@ -80,9 +82,10 @@ def testKakariko(self):
["Blind's Hideout - Top", False, []],
["Blind's Hideout - Top", False, [], ['Moon Pearl']],
- ["Blind's Hideout - Top", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Blind's Hideout - Top", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Blind's Hideout - Top", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Blind's Hideout - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Blind's Hideout - Left", False, []],
["Blind's Hideout - Left", False, [], ['Moon Pearl']],
@@ -161,9 +164,10 @@ def testKakariko(self):
["Maze Race", False, []],
["Maze Race", False, [], ['Moon Pearl']],
- ["Maze Race", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Maze Race", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Maze Race", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Maze Race", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Maze Race", True, ['Pegasus Boots', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Maze Race", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Maze Race", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
])
def testSouthLightWorld(self):
@@ -184,9 +188,10 @@ def testSouthLightWorld(self):
["Aginah's Cave", False, []],
["Aginah's Cave", False, [], ['Moon Pearl']],
- ["Aginah's Cave", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Aginah's Cave", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Aginah's Cave", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Aginah's Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Aginah's Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Aginah's Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Aginah's Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Bombos Tablet", False, []],
["Bombos Tablet", False, ['Progressive Sword'], ['Progressive Sword']],
@@ -212,39 +217,45 @@ def testSouthLightWorld(self):
["Mini Moldorm Cave - Far Left", False, []],
["Mini Moldorm Cave - Far Left", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Far Left", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Mini Moldorm Cave - Far Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Mini Moldorm Cave - Far Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Mini Moldorm Cave - Far Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Progressive Sword']],
["Mini Moldorm Cave - Left", False, []],
["Mini Moldorm Cave - Left", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Left", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Mini Moldorm Cave - Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Mini Moldorm Cave - Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Mini Moldorm Cave - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Progressive Sword']],
["Mini Moldorm Cave - Generous Guy", False, []],
["Mini Moldorm Cave - Generous Guy", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Mini Moldorm Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Mini Moldorm Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Mini Moldorm Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Progressive Sword']],
["Mini Moldorm Cave - Right", False, []],
["Mini Moldorm Cave - Right", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Right", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Mini Moldorm Cave - Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Mini Moldorm Cave - Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Mini Moldorm Cave - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Progressive Sword']],
["Mini Moldorm Cave - Far Right", False, []],
["Mini Moldorm Cave - Far Right", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Far Right", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Mini Moldorm Cave - Far Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Mini Moldorm Cave - Far Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Mini Moldorm Cave - Far Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Progressive Sword']],
["Ice Rod Cave", False, []],
["Ice Rod Cave", False, [], ['Moon Pearl']],
- ["Ice Rod Cave", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Ice Rod Cave", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Ice Rod Cave", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Ice Rod Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Ice Rod Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Ice Rod Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Ice Rod Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
])
def testZoraArea(self):
@@ -302,21 +313,24 @@ def testLightWorld(self):
["Sahasrahla's Hut - Left", False, []],
["Sahasrahla's Hut - Left", False, [], ['Moon Pearl']],
- ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Sahasrahla's Hut - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Sahasrahla's Hut - Middle", False, []],
["Sahasrahla's Hut - Middle", False, [], ['Moon Pearl']],
- ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Sahasrahla's Hut - Middle", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Middle", True, ['Pegasus Boots', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Sahasrahla's Hut - Middle", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Sahasrahla's Hut - Middle", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Sahasrahla's Hut - Right", False, []],
["Sahasrahla's Hut - Right", False, [], ['Moon Pearl']],
- ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Sahasrahla's Hut - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Right", True, ['Pegasus Boots', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Sahasrahla's Hut - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Sahasrahla's Hut - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Sahasrahla", False, []],
["Sahasrahla", False, [], ['Green Pendant']],
@@ -346,9 +360,10 @@ def testLightWorld(self):
["Graveyard Cave", False, []],
["Graveyard Cave", False, [], ['Moon Pearl']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Beat Agahnim 1']],
+ ["Graveyard Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
["Potion Shop", False, []],
["Potion Shop", False, [], ['Mushroom']],
diff --git a/worlds/alttp/test/inverted/TestInvertedTurtleRock.py b/worlds/alttp/test/inverted/TestInvertedTurtleRock.py
index 533e3c650f70..db3084b02a5b 100644
--- a/worlds/alttp/test/inverted/TestInvertedTurtleRock.py
+++ b/worlds/alttp/test/inverted/TestInvertedTurtleRock.py
@@ -11,21 +11,20 @@ def testTurtleRock(self):
["Turtle Rock - Compass Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Quake', 'Small Key (Turtle Rock)']],
["Turtle Rock - Compass Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria']],
["Turtle Rock - Compass Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria']],
- ["Turtle Rock - Compass Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Compass Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Compass Chest", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Compass Chest", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Compass Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Compass Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Compass Chest", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Compass Chest", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Chain Chomps", False, []],
["Turtle Rock - Chain Chomps", False, [], ['Magic Mirror', 'Cane of Somaria']],
- # Item rando only needs 1 key. ER needs to consider the case when the back is accessible, but not the middle (key wasted on Trinexx door)
["Turtle Rock - Chain Chomps", False, ['Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Chain Chomps", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
+ ["Turtle Rock - Chain Chomps", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Chain Chomps", True, ['Bomb Upgrade (+5)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
["Turtle Rock - Chain Chomps", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
- ["Turtle Rock - Chain Chomps", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror']],
+ ["Turtle Rock - Chain Chomps", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Fire Rod']],
["Turtle Rock - Roller Room - Left", False, []],
["Turtle Rock - Roller Room - Left", False, [], ['Cane of Somaria']],
@@ -34,10 +33,10 @@ def testTurtleRock(self):
["Turtle Rock - Roller Room - Left", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Quake', 'Small Key (Turtle Rock)']],
["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria']],
["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria']],
- ["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Roller Room - Left", True, ['Moon Pearl', 'Fire Rod', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Left", True, ['Moon Pearl', 'Fire Rod', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Roller Room - Right", False, []],
["Turtle Rock - Roller Room - Right", False, [], ['Cane of Somaria']],
@@ -46,17 +45,17 @@ def testTurtleRock(self):
["Turtle Rock - Roller Room - Right", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Quake', 'Small Key (Turtle Rock)']],
["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria']],
["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria']],
- ["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Roller Room - Right", True, ['Moon Pearl', 'Fire Rod', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Right", True, ['Moon Pearl', 'Fire Rod', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Big Chest", False, []],
["Turtle Rock - Big Chest", False, [], ['Big Key (Turtle Rock)']],
["Turtle Rock - Big Chest", False, [], ['Magic Mirror', 'Cane of Somaria']],
["Turtle Rock - Big Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria']],
["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hookshot']],
["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
@@ -66,13 +65,13 @@ def testTurtleRock(self):
["Turtle Rock - Big Key Chest", False, []],
["Turtle Rock - Big Key Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Key Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
# Mirror in from ledge, use left side entrance, have enough keys to get to the chest
- ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Key Chest", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Key Chest", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", False, []],
["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)', 'Magic Mirror']],
@@ -80,8 +79,8 @@ def testTurtleRock(self):
["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)', 'Lamp']],
["Turtle Rock - Crystaroller Room", False, [], ['Magic Mirror', 'Cane of Somaria']],
["Turtle Rock - Crystaroller Room", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
@@ -98,13 +97,16 @@ def testTurtleRock(self):
["Turtle Rock - Boss", False, [], ['Big Key (Turtle Rock)']],
["Turtle Rock - Boss", False, [], ['Magic Mirror', 'Lamp']],
["Turtle Rock - Boss", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Flute', 'Magic Mirror', 'Moon Pearl', 'Hookshot', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']]
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Flute', 'Magic Mirror', 'Moon Pearl', 'Hookshot', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']]
+
])
+
+
def testEyeBridge(self):
for location in ["Turtle Rock - Eye Bridge - Top Right", "Turtle Rock - Eye Bridge - Top Left",
"Turtle Rock - Eye Bridge - Bottom Right", "Turtle Rock - Eye Bridge - Bottom Left"]:
@@ -115,12 +117,12 @@ def testEyeBridge(self):
[location, False, [], ['Magic Mirror', 'Cane of Somaria']],
[location, False, [], ['Magic Mirror', 'Lamp']],
[location, False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
- [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
- [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
- [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
- [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
- [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
+ [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
+ [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
+ [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
+ [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
+ [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
+ [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
# Mirroring into Eye Bridge does not require Cane of Somaria
[location, True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Byrna']],
diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedDarkWorld.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedDarkWorld.py
index 69f564489700..dd4a74b6c4d2 100644
--- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedDarkWorld.py
+++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedDarkWorld.py
@@ -5,7 +5,8 @@ class TestInvertedDarkWorld(TestInvertedMinor):
def testNorthWest(self):
self.run_location_tests([
- ["Brewery", True, []],
+ ["Brewery", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Brewery", True, ['Bomb Upgrade (+5)']],
["C-Shaped House", True, []],
@@ -67,15 +68,16 @@ def testNorthEast(self):
def testSouth(self):
self.run_location_tests([
- ["Hype Cave - Top", True, []],
-
- ["Hype Cave - Middle Right", True, []],
-
- ["Hype Cave - Middle Left", True, []],
-
- ["Hype Cave - Bottom", True, []],
-
- ["Hype Cave - Generous Guy", True, []],
+ ["Hype Cave - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)']],
+ ["Hype Cave - Middle Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)']],
+ ["Hype Cave - Middle Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)']],
+ ["Hype Cave - Bottom", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)']],
+ ["Hype Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)']],
["Stumpy", True, []],
diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedDeathMountain.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedDeathMountain.py
index c68a8e5f0c89..c189d107d976 100644
--- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedDeathMountain.py
+++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedDeathMountain.py
@@ -40,10 +40,11 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Far Left", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Far Left", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']],
["Paradox Cave Lower - Far Left", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Left", True, ['Flute', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Paradox Cave Lower - Far Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod']],
+ ["Paradox Cave Lower - Far Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Lower - Far Left", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Lower - Far Left", True, ['Progressive Bow', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
+ ["Paradox Cave Lower - Far Left", True, ['Cane of Somaria', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
["Paradox Cave Lower - Left", False, []],
["Paradox Cave Lower - Left", False, [], ['Moon Pearl']],
@@ -52,10 +53,11 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Left", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Left", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']],
["Paradox Cave Lower - Left", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']],
- ["Paradox Cave Lower - Left", True, ['Flute', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
- ["Paradox Cave Lower - Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Paradox Cave Lower - Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod']],
+ ["Paradox Cave Lower - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Lower - Left", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Lower - Left", True, ['Progressive Bow', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
+ ["Paradox Cave Lower - Left", True, ['Cane of Somaria', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
["Paradox Cave Lower - Middle", False, []],
["Paradox Cave Lower - Middle", False, [], ['Moon Pearl']],
@@ -64,10 +66,11 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Middle", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Middle", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']],
["Paradox Cave Lower - Middle", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']],
- ["Paradox Cave Lower - Middle", True, ['Flute', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
- ["Paradox Cave Lower - Middle", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Paradox Cave Lower - Middle", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod']],
+ ["Paradox Cave Lower - Middle", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Lower - Middle", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Lower - Middle", True, ['Progressive Bow', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
+ ["Paradox Cave Lower - Middle", True, ['Cane of Somaria', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
["Paradox Cave Lower - Right", False, []],
["Paradox Cave Lower - Right", False, [], ['Moon Pearl']],
@@ -76,10 +79,11 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Right", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Right", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']],
["Paradox Cave Lower - Right", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']],
- ["Paradox Cave Lower - Right", True, ['Flute', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
- ["Paradox Cave Lower - Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Paradox Cave Lower - Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod']],
+ ["Paradox Cave Lower - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Lower - Right", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Lower - Right", True, ['Progressive Bow', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
+ ["Paradox Cave Lower - Right", True, ['Cane of Somaria', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
["Paradox Cave Lower - Far Right", False, []],
["Paradox Cave Lower - Far Right", False, [], ['Moon Pearl']],
@@ -88,10 +92,11 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Far Right", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Far Right", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']],
["Paradox Cave Lower - Far Right", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Right", True, ['Flute', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
- ["Paradox Cave Lower - Far Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Paradox Cave Lower - Far Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod']],
+ ["Paradox Cave Lower - Far Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Lower - Far Right", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Lower - Far Right", True, ['Progressive Bow', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
+ ["Paradox Cave Lower - Far Right", True, ['Cane of Somaria', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
["Paradox Cave Upper - Left", False, []],
["Paradox Cave Upper - Left", False, [], ['Moon Pearl']],
@@ -100,10 +105,11 @@ def testEastDeathMountain(self):
["Paradox Cave Upper - Left", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']],
["Paradox Cave Upper - Left", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']],
["Paradox Cave Upper - Left", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']],
- ["Paradox Cave Upper - Left", True, ['Flute', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
- ["Paradox Cave Upper - Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Paradox Cave Upper - Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)','Bomb Upgrade (50)']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
["Paradox Cave Upper - Right", False, []],
["Paradox Cave Upper - Right", False, [], ['Moon Pearl']],
@@ -112,20 +118,21 @@ def testEastDeathMountain(self):
["Paradox Cave Upper - Right", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']],
["Paradox Cave Upper - Right", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']],
["Paradox Cave Upper - Right", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']],
- ["Paradox Cave Upper - Right", True, ['Flute', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
- ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
- ["Paradox Cave Upper - Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Paradox Cave Upper - Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)','Bomb Upgrade (50)']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
["Mimic Cave", False, []],
["Mimic Cave", False, [], ['Moon Pearl']],
["Mimic Cave", False, [], ['Hammer']],
["Mimic Cave", False, [], ['Progressive Glove', 'Flute']],
["Mimic Cave", False, [], ['Lamp', 'Flute']],
- ["Mimic Cave", True, ['Flute', 'Moon Pearl', 'Hammer', 'Hookshot']],
- ["Mimic Cave", True, ['Flute', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hammer']],
- ["Mimic Cave", True, ['Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer', 'Hookshot']],
- ["Mimic Cave", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer']],
+ ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Flute', 'Moon Pearl', 'Hammer', 'Hookshot']],
+ ["Mimic Cave", True, ['Progressive Sword', 'Progressive Sword', 'Flute', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hammer']],
+ ["Mimic Cave", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer', 'Hookshot']],
+ ["Mimic Cave", True, ['Cane of Somaria', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer']],
["Ether Tablet", False, []],
["Ether Tablet", False, [], ['Moon Pearl']],
diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedLightWorld.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedLightWorld.py
index 376e7b4bec49..086c1c92b5dd 100644
--- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedLightWorld.py
+++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedLightWorld.py
@@ -43,16 +43,18 @@ def testKakariko(self):
["Chicken House", False, []],
["Chicken House", False, [], ['Moon Pearl']],
- ["Chicken House", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Chicken House", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Chicken House", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Chicken House", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)','Bomb Upgrade (50)']],
+ ["Chicken House", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Chicken House", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Chicken House", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
# top can't be bombed as super bunny and needs Moon Pearl
["Kakariko Well - Top", False, []],
["Kakariko Well - Top", False, [], ['Moon Pearl']],
- ["Kakariko Well - Top", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Kakariko Well - Top", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Kakariko Well - Top", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Kakariko Well - Top", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)','Bomb Upgrade (50)']],
+ ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Kakariko Well - Left", False, []],
["Kakariko Well - Left", True, ['Beat Agahnim 1']],
@@ -76,9 +78,10 @@ def testKakariko(self):
["Blind's Hideout - Top", False, []],
["Blind's Hideout - Top", False, [], ['Moon Pearl', 'Magic Mirror']],
- ["Blind's Hideout - Top", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Blind's Hideout - Top", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Blind's Hideout - Top", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Blind's Hideout - Top", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)','Bomb Upgrade (50)']],
+ ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Blind's Hideout - Left", False, []],
["Blind's Hideout - Left", False, [], ['Moon Pearl', 'Magic Mirror']],
@@ -157,9 +160,10 @@ def testKakariko(self):
["Maze Race", False, []],
["Maze Race", False, [], ['Moon Pearl']],
- ["Maze Race", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Maze Race", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Maze Race", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Maze Race", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)','Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Maze Race", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Maze Race", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Maze Race", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
])
def testSouthLightWorld(self):
@@ -179,9 +183,10 @@ def testSouthLightWorld(self):
["Aginah's Cave", False, []],
["Aginah's Cave", False, [], ['Moon Pearl']],
- ["Aginah's Cave", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Aginah's Cave", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Aginah's Cave", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Aginah's Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Aginah's Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Aginah's Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Aginah's Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Bombos Tablet", False, []],
["Bombos Tablet", False, ['Progressive Sword'], ['Progressive Sword']],
@@ -209,39 +214,45 @@ def testSouthLightWorld(self):
["Mini Moldorm Cave - Far Left", False, []],
["Mini Moldorm Cave - Far Left", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Far Left", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Mini Moldorm Cave - Far Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Mini Moldorm Cave - Far Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Mini Moldorm Cave - Far Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Mini Moldorm Cave - Left", False, []],
["Mini Moldorm Cave - Left", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Left", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Mini Moldorm Cave - Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Mini Moldorm Cave - Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Mini Moldorm Cave - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Mini Moldorm Cave - Generous Guy", False, []],
["Mini Moldorm Cave - Generous Guy", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Mini Moldorm Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Mini Moldorm Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Mini Moldorm Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Mini Moldorm Cave - Right", False, []],
["Mini Moldorm Cave - Right", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Right", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Mini Moldorm Cave - Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Mini Moldorm Cave - Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Mini Moldorm Cave - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Mini Moldorm Cave - Far Right", False, []],
["Mini Moldorm Cave - Far Right", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Far Right", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Mini Moldorm Cave - Far Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Mini Moldorm Cave - Far Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Mini Moldorm Cave - Far Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
["Ice Rod Cave", False, []],
["Ice Rod Cave", False, [], ['Moon Pearl']],
- ["Ice Rod Cave", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Ice Rod Cave", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Ice Rod Cave", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Ice Rod Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Ice Rod Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Ice Rod Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Ice Rod Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
])
def testZoraArea(self):
@@ -297,25 +308,28 @@ def testLightWorld(self):
["Sahasrahla's Hut - Left", False, []],
["Sahasrahla's Hut - Left", False, [], ['Moon Pearl', 'Magic Mirror']],
- ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Sahasrahla's Hut - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Sahasrahla's Hut - Left", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Sahasrahla's Hut - Left", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
# super bunny bonk
["Sahasrahla's Hut - Left", True, ['Magic Mirror', 'Beat Agahnim 1', 'Pegasus Boots']],
["Sahasrahla's Hut - Middle", False, []],
["Sahasrahla's Hut - Middle", False, [], ['Moon Pearl', 'Magic Mirror']],
- ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Sahasrahla's Hut - Middle", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Middle", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Sahasrahla's Hut - Middle", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Sahasrahla's Hut - Middle", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
# super bunny bonk
["Sahasrahla's Hut - Middle", True, ['Magic Mirror', 'Beat Agahnim 1', 'Pegasus Boots']],
["Sahasrahla's Hut - Right", False, []],
["Sahasrahla's Hut - Right", False, [], ['Moon Pearl', 'Magic Mirror']],
- ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Beat Agahnim 1']],
- ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Sahasrahla's Hut - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
+ ["Sahasrahla's Hut - Right", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Sahasrahla's Hut - Right", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
# super bunny bonk
["Sahasrahla's Hut - Right", True, ['Magic Mirror', 'Beat Agahnim 1', 'Pegasus Boots']],
@@ -347,9 +361,10 @@ def testLightWorld(self):
["Graveyard Cave", False, []],
["Graveyard Cave", False, [], ['Moon Pearl']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Beat Agahnim 1']],
+ ["Graveyard Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
["Potion Shop", False, []],
["Potion Shop", False, [], ['Mushroom']],
diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py
index 72049e17742c..bf25c5c9a164 100644
--- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py
+++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py
@@ -1,39 +1,32 @@
-from argparse import Namespace
-
-from BaseClasses import MultiWorld
-from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
+from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances
from worlds.alttp.InvertedRegions import create_inverted_regions
-from worlds.alttp.ItemPool import generate_itempool, difficulties
-from worlds.alttp.Items import ItemFactory
+from worlds.alttp.ItemPool import difficulties
+from worlds.alttp.Items import item_factory
+from worlds.alttp.Options import GlitchesRequired
from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops
-from worlds.alttp.Rules import set_rules
-from test.TestBase import TestBase
+from test.bases import TestBase
+
+from worlds.alttp.test import LTTPTestBase
-from worlds import AutoWorld
-class TestInvertedMinor(TestBase):
+class TestInvertedMinor(TestBase, LTTPTestBase):
def setUp(self):
- self.multiworld = MultiWorld(1)
- self.multiworld.set_seed(None)
- args = Namespace()
- for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
- setattr(args, name, {1: option.from_any(option.default)})
- self.multiworld.set_options(args)
- self.multiworld.set_default_common_options()
- self.multiworld.mode[1] = "inverted"
- self.multiworld.logic[1] = "minorglitches"
- self.multiworld.difficulty_requirements[1] = difficulties['normal']
+ self.world_setup()
+ self.multiworld.mode[1].value = 2
+ self.multiworld.glitches_required[1] = GlitchesRequired.from_any("minor_glitches")
+ self.multiworld.bombless_start[1].value = True
+ self.multiworld.shuffle_capacity_upgrades[1].value = 2
+ self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
create_inverted_regions(self.multiworld, 1)
- self.multiworld.worlds[1].create_dungeons()
+ self.world.create_dungeons()
create_shops(self.multiworld, 1)
link_inverted_entrances(self.multiworld, 1)
- self.multiworld.worlds[1].create_items()
- self.multiworld.required_medallions[1] = ['Ether', 'Quake']
+ self.world.create_items()
self.multiworld.itempool.extend(get_dungeon_item_pool(self.multiworld))
- self.multiworld.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
+ self.multiworld.itempool.extend(item_factory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], self.world))
self.multiworld.get_location('Agahnim 1', 1).item = None
self.multiworld.get_location('Agahnim 2', 1).item = None
mark_light_world_regions(self.multiworld, 1)
- self.multiworld.worlds[1].set_rules()
+ self.world.set_rules()
diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py
index a25d89a6f4f9..a416e1b35d33 100644
--- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py
+++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py
@@ -11,21 +11,21 @@ def testTurtleRock(self):
["Turtle Rock - Compass Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Quake', 'Small Key (Turtle Rock)']],
["Turtle Rock - Compass Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria']],
["Turtle Rock - Compass Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria']],
- ["Turtle Rock - Compass Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Compass Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Compass Chest", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Compass Chest", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Compass Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Compass Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Compass Chest", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Compass Chest", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Chain Chomps", False, []],
["Turtle Rock - Chain Chomps", False, [], ['Magic Mirror', 'Cane of Somaria']],
# Item rando only needs 1 key. ER needs to consider the case when the back is accessible, but not the middle (key wasted on Trinexx door)
["Turtle Rock - Chain Chomps", False, ['Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Chain Chomps", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
+ ["Turtle Rock - Chain Chomps", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Chain Chomps", True, ['Bomb Upgrade (+5)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
["Turtle Rock - Chain Chomps", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
- ["Turtle Rock - Chain Chomps", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror']],
+ ["Turtle Rock - Chain Chomps", True, ['Fire Rod', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror']],
["Turtle Rock - Roller Room - Left", False, []],
["Turtle Rock - Roller Room - Left", False, [], ['Cane of Somaria']],
@@ -34,10 +34,10 @@ def testTurtleRock(self):
["Turtle Rock - Roller Room - Left", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Quake', 'Small Key (Turtle Rock)']],
["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria']],
["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria']],
- ["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Roller Room - Left", True, ['Moon Pearl', 'Fire Rod', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Left", True, ['Moon Pearl', 'Fire Rod', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Left", True, ['Fire Rod', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Roller Room - Right", False, []],
["Turtle Rock - Roller Room - Right", False, [], ['Cane of Somaria']],
@@ -46,17 +46,17 @@ def testTurtleRock(self):
["Turtle Rock - Roller Room - Right", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Quake', 'Small Key (Turtle Rock)']],
["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria']],
["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria']],
- ["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Roller Room - Right", True, ['Moon Pearl', 'Fire Rod', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Right", True, ['Moon Pearl', 'Fire Rod', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Roller Room - Right", True, ['Fire Rod', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Big Chest", False, []],
["Turtle Rock - Big Chest", False, [], ['Big Key (Turtle Rock)']],
["Turtle Rock - Big Chest", False, [], ['Magic Mirror', 'Cane of Somaria']],
["Turtle Rock - Big Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria']],
["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hookshot']],
["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
@@ -66,13 +66,13 @@ def testTurtleRock(self):
["Turtle Rock - Big Key Chest", False, []],
["Turtle Rock - Big Key Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Key Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
# Mirror in from ledge, use left side entrance, have enough keys to get to the chest
- ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Key Chest", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Big Key Chest", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Big Key Chest", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", False, []],
["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)', 'Magic Mirror']],
@@ -80,8 +80,8 @@ def testTurtleRock(self):
["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)', 'Lamp']],
["Turtle Rock - Crystaroller Room", False, [], ['Magic Mirror', 'Cane of Somaria']],
["Turtle Rock - Crystaroller Room", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
@@ -98,11 +98,11 @@ def testTurtleRock(self):
["Turtle Rock - Boss", False, [], ['Big Key (Turtle Rock)']],
["Turtle Rock - Boss", False, [], ['Magic Mirror', 'Lamp']],
["Turtle Rock - Boss", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
- ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Flute', 'Magic Mirror', 'Moon Pearl', 'Hookshot', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']]
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
+ ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Flute', 'Magic Mirror', 'Moon Pearl', 'Hookshot', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']]
])
@@ -116,12 +116,12 @@ def testEyeBridge(self):
[location, False, [], ['Magic Mirror', 'Cane of Somaria']],
[location, False, [], ['Magic Mirror', 'Lamp']],
[location, False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
- [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
- [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
- [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
- [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
- [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
- [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
+ [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
+ [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
+ [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
+ [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
+ [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
+ [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
# Mirroring into Eye Bridge does not require Cane of Somaria
[location, True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Byrna']],
diff --git a/worlds/alttp/test/inverted_owg/TestDarkWorld.py b/worlds/alttp/test/inverted_owg/TestDarkWorld.py
index e7e720d2b853..8fb234f0b50a 100644
--- a/worlds/alttp/test/inverted_owg/TestDarkWorld.py
+++ b/worlds/alttp/test/inverted_owg/TestDarkWorld.py
@@ -5,15 +5,16 @@ class TestDarkWorld(TestInvertedOWG):
def testSouthDarkWorld(self):
self.run_location_tests([
- ["Hype Cave - Top", True, []],
-
- ["Hype Cave - Middle Right", True, []],
-
- ["Hype Cave - Middle Left", True, []],
-
- ["Hype Cave - Bottom", True, []],
-
- ["Hype Cave - Generous Guy", True, []],
+ ["Hype Cave - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Middle Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Middle Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Bottom", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)']],
["Stumpy", True, []],
@@ -22,7 +23,8 @@ def testSouthDarkWorld(self):
def testWestDarkWorld(self):
self.run_location_tests([
- ["Brewery", True, []],
+ ["Brewery", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Brewery", True, ['Bomb Upgrade (+5)']],
["C-Shaped House", True, []],
diff --git a/worlds/alttp/test/inverted_owg/TestDeathMountain.py b/worlds/alttp/test/inverted_owg/TestDeathMountain.py
index 79796a7aeb1e..5186ae9106df 100644
--- a/worlds/alttp/test/inverted_owg/TestDeathMountain.py
+++ b/worlds/alttp/test/inverted_owg/TestDeathMountain.py
@@ -24,36 +24,38 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Far Left", False, []],
["Paradox Cave Lower - Far Left", False, [], ['Moon Pearl']],
- ["Paradox Cave Lower - Far Left", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Paradox Cave Lower - Far Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
["Paradox Cave Lower - Left", False, []],
["Paradox Cave Lower - Left", False, [], ['Moon Pearl']],
- ["Paradox Cave Lower - Left", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Paradox Cave Lower - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
["Paradox Cave Lower - Middle", False, []],
["Paradox Cave Lower - Middle", False, [], ['Moon Pearl']],
- ["Paradox Cave Lower - Middle", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Paradox Cave Lower - Middle", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
["Paradox Cave Lower - Right", False, []],
["Paradox Cave Lower - Right", False, [], ['Moon Pearl']],
- ["Paradox Cave Lower - Right", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Paradox Cave Lower - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
["Paradox Cave Lower - Far Right", False, []],
["Paradox Cave Lower - Far Right", False, [], ['Moon Pearl']],
- ["Paradox Cave Lower - Far Right", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Paradox Cave Lower - Far Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
["Paradox Cave Upper - Left", False, []],
["Paradox Cave Upper - Left", False, [], ['Moon Pearl']],
- ["Paradox Cave Upper - Left", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Paradox Cave Upper - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
["Paradox Cave Upper - Right", False, []],
["Paradox Cave Upper - Right", False, [], ['Moon Pearl']],
- ["Paradox Cave Upper - Right", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Paradox Cave Upper - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
["Mimic Cave", False, []],
["Mimic Cave", False, [], ['Moon Pearl']],
["Mimic Cave", False, [], ['Hammer']],
- ["Mimic Cave", True, ['Moon Pearl', 'Hammer', 'Pegasus Boots']],
+ ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Hammer', 'Pegasus Boots']],
["Ether Tablet", False, []],
["Ether Tablet", False, ['Progressive Sword'], ['Progressive Sword']],
@@ -99,20 +101,20 @@ def testEastDarkWorldDeathMountain(self):
["Hookshot Cave - Bottom Right", False, []],
["Hookshot Cave - Bottom Right", False, [], ['Hookshot', 'Pegasus Boots']],
["Hookshot Cave - Bottom Right", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
- ["Hookshot Cave - Bottom Right", True, ['Pegasus Boots']],
+ ["Hookshot Cave - Bottom Right", True, ['Pegasus Boots', 'Bomb Upgrade (50)']],
["Hookshot Cave - Bottom Left", False, []],
["Hookshot Cave - Bottom Left", False, [], ['Hookshot']],
["Hookshot Cave - Bottom Left", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
- ["Hookshot Cave - Bottom Left", True, ['Pegasus Boots', 'Hookshot']],
+ ["Hookshot Cave - Bottom Left", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
["Hookshot Cave - Top Left", False, []],
["Hookshot Cave - Top Left", False, [], ['Hookshot']],
["Hookshot Cave - Top Left", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
- ["Hookshot Cave - Top Left", True, ['Pegasus Boots', 'Hookshot']],
+ ["Hookshot Cave - Top Left", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
["Hookshot Cave - Top Right", False, []],
["Hookshot Cave - Top Right", False, [], ['Hookshot']],
["Hookshot Cave - Top Right", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
- ["Hookshot Cave - Top Right", True, ['Pegasus Boots', 'Hookshot']],
+ ["Hookshot Cave - Top Right", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
])
\ No newline at end of file
diff --git a/worlds/alttp/test/inverted_owg/TestDungeons.py b/worlds/alttp/test/inverted_owg/TestDungeons.py
index f5d07544aaea..ada1b92fca49 100644
--- a/worlds/alttp/test/inverted_owg/TestDungeons.py
+++ b/worlds/alttp/test/inverted_owg/TestDungeons.py
@@ -13,16 +13,15 @@ def testFirstDungeonChests(self):
["Sanctuary", False, []],
["Sanctuary", False, ['Beat Agahnim 1']],
["Sanctuary", True, ['Magic Mirror', 'Beat Agahnim 1']],
- ["Sanctuary", True, ['Lamp', 'Beat Agahnim 1', 'Small Key (Hyrule Castle)']],
+ ["Sanctuary", True, ['Lamp', 'Beat Agahnim 1', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)']],
["Sanctuary", True, ['Moon Pearl', 'Pegasus Boots']],
["Sanctuary", True, ['Magic Mirror', 'Pegasus Boots']],
["Sewers - Secret Room - Left", False, []],
["Sewers - Secret Room - Left", True, ['Moon Pearl', 'Progressive Glove', 'Pegasus Boots']],
- ["Sewers - Secret Room - Left", True, ['Moon Pearl', 'Pegasus Boots', 'Lamp', 'Small Key (Hyrule Castle)']],
- ["Sewers - Secret Room - Left", True,
- ['Magic Mirror', 'Pegasus Boots', 'Lamp', 'Small Key (Hyrule Castle)']],
- ["Sewers - Secret Room - Left", True, ['Beat Agahnim 1', 'Lamp', 'Small Key (Hyrule Castle)']],
+ ["Sewers - Secret Room - Left", True, ['Moon Pearl', 'Pegasus Boots', 'Lamp', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)']],
+ ["Sewers - Secret Room - Left", True, ['Magic Mirror', 'Pegasus Boots', 'Lamp', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)']],
+ ["Sewers - Secret Room - Left", True, ['Bomb Upgrade (+5)', 'Beat Agahnim 1', 'Lamp', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)']],
["Eastern Palace - Compass Chest", False, []],
["Eastern Palace - Compass Chest", True, ['Moon Pearl', 'Pegasus Boots']],
@@ -37,15 +36,16 @@ def testFirstDungeonChests(self):
["Desert Palace - Boss", False, [], ['Small Key (Desert Palace)']],
["Desert Palace - Boss", False, [], ['Big Key (Desert Palace)']],
["Desert Palace - Boss", False, [], ['Lamp', 'Fire Rod']],
- ["Desert Palace - Boss", True, ['Progressive Sword', 'Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Moon Pearl', 'Pegasus Boots', 'Lamp']],
- ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Moon Pearl', 'Pegasus Boots', 'Fire Rod']],
+ ["Desert Palace - Boss", True, ['Progressive Sword', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Moon Pearl', 'Pegasus Boots', 'Lamp']],
+ ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Moon Pearl', 'Pegasus Boots', 'Fire Rod']],
["Tower of Hera - Basement Cage", False, []],
["Tower of Hera - Basement Cage", False, [], ['Moon Pearl']],
- ["Tower of Hera - Basement Cage", True, ['Pegasus Boots', 'Moon Pearl']],
+ ["Tower of Hera - Basement Cage", True, ['Pegasus Boots', 'Moon Pearl', 'Bomb Upgrade (50)']],
+ ["Tower of Hera - Basement Cage", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Sword']],
["Castle Tower - Room 03", False, []],
- ["Castle Tower - Room 03", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']],
+ ["Castle Tower - Room 03", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']],
["Castle Tower - Room 03", True, ['Pegasus Boots', 'Progressive Sword']],
["Castle Tower - Room 03", True, ['Pegasus Boots', 'Progressive Bow']],
@@ -62,7 +62,8 @@ def testFirstDungeonChests(self):
["Skull Woods - Big Chest", False, []],
["Skull Woods - Big Chest", False, [], ['Big Key (Skull Woods)']],
- ["Skull Woods - Big Chest", True, ['Big Key (Skull Woods)']],
+ ["Skull Woods - Big Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Skull Woods - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Skull Woods)']],
["Skull Woods - Big Key Chest", True, []],
@@ -75,8 +76,8 @@ def testFirstDungeonChests(self):
["Ice Palace - Compass Chest", False, []],
["Ice Palace - Compass Chest", False, [], ['Fire Rod', 'Bombos', 'Progressive Sword']],
# Qirn Jump
- ["Ice Palace - Compass Chest", True, ['Fire Rod']],
- ["Ice Palace - Compass Chest", True, ['Bombos', 'Progressive Sword']],
+ ["Ice Palace - Compass Chest", True, ['Fire Rod', 'Small Key (Ice Palace)']],
+ ["Ice Palace - Compass Chest", True, ['Bombos', 'Progressive Sword', 'Small Key (Ice Palace)']],
["Misery Mire - Bridge Chest", False, []],
["Misery Mire - Bridge Chest", False, [], ['Ether']],
@@ -85,11 +86,20 @@ def testFirstDungeonChests(self):
["Turtle Rock - Compass Chest", False, []],
["Turtle Rock - Compass Chest", False, [], ['Cane of Somaria']],
- ["Turtle Rock - Compass Chest", True, ['Pegasus Boots', 'Magic Mirror', 'Moon Pearl', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Compass Chest", True, ['Pegasus Boots', 'Magic Mirror', 'Moon Pearl', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Compass Chest", True, ['Pegasus Boots', 'Quake', 'Progressive Sword', 'Cane of Somaria']],
["Turtle Rock - Chain Chomps", False, []],
- ["Turtle Rock - Chain Chomps", True, ['Pegasus Boots', 'Magic Mirror', 'Moon Pearl']],
+ ["Turtle Rock - Chain Chomps", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Hookshot', 'Progressive Sword', 'Progressive Bow', 'Blue Boomerang', 'Red Boomerang', 'Cane of Somaria', 'Fire Rod', 'Ice Rod']],
+ ["Turtle Rock - Chain Chomps", True, ['Bomb Upgrade (+5)', 'Pegasus Boots', 'Magic Mirror', 'Moon Pearl']],
+ ["Turtle Rock - Chain Chomps", True, ['Hookshot', 'Pegasus Boots']],
+ ["Turtle Rock - Chain Chomps", True, ['Progressive Bow', 'Pegasus Boots', 'Magic Mirror', 'Moon Pearl']],
+ ["Turtle Rock - Chain Chomps", True, ['Blue Boomerang', 'Pegasus Boots', 'Magic Mirror', 'Moon Pearl']],
+ ["Turtle Rock - Chain Chomps", True, ['Red Boomerang', 'Pegasus Boots', 'Magic Mirror', 'Moon Pearl']],
+ ["Turtle Rock - Chain Chomps", True, ['Cane of Somaria', 'Pegasus Boots', 'Magic Mirror', 'Moon Pearl']],
+ ["Turtle Rock - Chain Chomps", True, ['Fire Rod', 'Pegasus Boots', 'Magic Mirror', 'Moon Pearl']],
+ ["Turtle Rock - Chain Chomps", True, ['Ice Rod', 'Pegasus Boots', 'Magic Mirror', 'Moon Pearl']],
+ ["Turtle Rock - Chain Chomps", True, ['Progressive Sword', 'Progressive Sword', 'Pegasus Boots']],
["Turtle Rock - Crystaroller Room", False, []],
["Turtle Rock - Crystaroller Room", True, ['Pegasus Boots', 'Magic Mirror', 'Moon Pearl', 'Big Key (Turtle Rock)']],
diff --git a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py
index 77a551db6f87..1de22b95e593 100644
--- a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py
+++ b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py
@@ -1,42 +1,34 @@
-from argparse import Namespace
-
-from BaseClasses import MultiWorld
-from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
+from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances
from worlds.alttp.InvertedRegions import create_inverted_regions
-from worlds.alttp.ItemPool import generate_itempool, difficulties
-from worlds.alttp.Items import ItemFactory
+from worlds.alttp.ItemPool import difficulties
+from worlds.alttp.Items import item_factory
+from worlds.alttp.Options import GlitchesRequired
from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops
-from worlds.alttp.Rules import set_rules
-from test.TestBase import TestBase
+from test.bases import TestBase
-from worlds import AutoWorld
+from worlds.alttp.test import LTTPTestBase
-class TestInvertedOWG(TestBase):
+class TestInvertedOWG(TestBase, LTTPTestBase):
def setUp(self):
- self.multiworld = MultiWorld(1)
- self.multiworld.set_seed(None)
- args = Namespace()
- for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
- setattr(args, name, {1: option.from_any(option.default)})
- self.multiworld.set_options(args)
- self.multiworld.set_default_common_options()
- self.multiworld.logic[1] = "owglitches"
- self.multiworld.mode[1] = "inverted"
- self.multiworld.difficulty_requirements[1] = difficulties['normal']
+ self.world_setup()
+ self.multiworld.glitches_required[1] = GlitchesRequired.from_any("overworld_glitches")
+ self.multiworld.mode[1].value = 2
+ self.multiworld.bombless_start[1].value = True
+ self.multiworld.shuffle_capacity_upgrades[1].value = 2
+ self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
create_inverted_regions(self.multiworld, 1)
- self.multiworld.worlds[1].create_dungeons()
+ self.world.create_dungeons()
create_shops(self.multiworld, 1)
link_inverted_entrances(self.multiworld, 1)
- self.multiworld.worlds[1].create_items()
- self.multiworld.required_medallions[1] = ['Ether', 'Quake']
+ self.world.create_items()
self.multiworld.itempool.extend(get_dungeon_item_pool(self.multiworld))
- self.multiworld.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
+ self.multiworld.itempool.extend(item_factory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], self.world))
self.multiworld.get_location('Agahnim 1', 1).item = None
self.multiworld.get_location('Agahnim 2', 1).item = None
self.multiworld.precollected_items[1].clear()
- self.multiworld.itempool.append(ItemFactory('Pegasus Boots', 1))
+ self.multiworld.itempool.append(item_factory('Pegasus Boots', self.world))
mark_light_world_regions(self.multiworld, 1)
- self.multiworld.worlds[1].set_rules()
+ self.world.set_rules()
diff --git a/worlds/alttp/test/inverted_owg/TestLightWorld.py b/worlds/alttp/test/inverted_owg/TestLightWorld.py
index de92b4ef854d..bd18259bec3f 100644
--- a/worlds/alttp/test/inverted_owg/TestLightWorld.py
+++ b/worlds/alttp/test/inverted_owg/TestLightWorld.py
@@ -40,40 +40,46 @@ def testLightWorld(self):
["Chicken House", False, []],
["Chicken House", False, [], ['Moon Pearl']],
- ["Chicken House", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Chicken House", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Chicken House", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
["Aginah's Cave", False, []],
["Aginah's Cave", False, [], ['Moon Pearl']],
- ["Aginah's Cave", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Aginah's Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Aginah's Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
["Sahasrahla's Hut - Left", False, []],
["Sahasrahla's Hut - Left", False, [], ['Moon Pearl', 'Magic Mirror']],
["Sahasrahla's Hut - Left", False, [], ['Moon Pearl', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Pegasus Boots']],
["Sahasrahla's Hut - Left", True, ['Magic Mirror', 'Pegasus Boots']],
##todo: Damage boost superbunny not in logic
#["Sahasrahla's Hut - Left", True, ['Beat Agahnim 1', 'Pegasus Boots']],
- ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Beat Agahnim 1']],
+ ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
["Sahasrahla's Hut - Middle", False, []],
["Sahasrahla's Hut - Middle", False, [], ['Moon Pearl', 'Magic Mirror']],
["Sahasrahla's Hut - Middle", False, [], ['Moon Pearl', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Middle", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Pegasus Boots']],
["Sahasrahla's Hut - Middle", True, ['Magic Mirror', 'Pegasus Boots']],
#["Sahasrahla's Hut - Middle", True, ['Beat Agahnim 1', 'Pegasus Boots']],
- ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Beat Agahnim 1']],
+ ["Sahasrahla's Hut - Middle", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
["Sahasrahla's Hut - Right", False, []],
["Sahasrahla's Hut - Right", False, [], ['Moon Pearl', 'Magic Mirror']],
["Sahasrahla's Hut - Right", False, [], ['Moon Pearl', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Pegasus Boots']],
["Sahasrahla's Hut - Right", True, ['Magic Mirror', 'Pegasus Boots']],
#["Sahasrahla's Hut - Right", True, ['Beat Agahnim 1', 'Pegasus Boots']],
- ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Beat Agahnim 1']],
+ ["Sahasrahla's Hut - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']],
["Kakariko Well - Top", False, []],
["Kakariko Well - Top", False, [], ['Moon Pearl']],
- ["Kakariko Well - Top", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Kakariko Well - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
["Kakariko Well - Left", False, []],
["Kakariko Well - Left", True, ['Moon Pearl', 'Pegasus Boots']],
@@ -101,7 +107,8 @@ def testLightWorld(self):
["Blind's Hideout - Top", False, []],
["Blind's Hideout - Top", False, [], ['Moon Pearl']],
- ["Blind's Hideout - Top", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Blind's Hideout - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
["Blind's Hideout - Left", False, []],
["Blind's Hideout - Left", False, [], ['Moon Pearl', 'Magic Mirror']],
@@ -134,27 +141,33 @@ def testLightWorld(self):
["Mini Moldorm Cave - Far Left", False, []],
["Mini Moldorm Cave - Far Left", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Far Left", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Mini Moldorm Cave - Far Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Pegasus Boots']],
["Mini Moldorm Cave - Left", False, []],
["Mini Moldorm Cave - Left", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Left", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Mini Moldorm Cave - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Pegasus Boots']],
["Mini Moldorm Cave - Right", False, []],
["Mini Moldorm Cave - Right", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Right", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Mini Moldorm Cave - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Pegasus Boots']],
["Mini Moldorm Cave - Far Right", False, []],
["Mini Moldorm Cave - Far Right", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Far Right", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Mini Moldorm Cave - Far Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Pegasus Boots']],
["Mini Moldorm Cave - Generous Guy", False, []],
["Mini Moldorm Cave - Generous Guy", False, [], ['Moon Pearl']],
- ["Mini Moldorm Cave - Generous Guy", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Mini Moldorm Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Pegasus Boots']],
["Ice Rod Cave", False, []],
["Ice Rod Cave", False, [], ['Moon Pearl']],
- ["Ice Rod Cave", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Ice Rod Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Ice Rod Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
#I don't think so
#["Ice Rod Cave", True, ['Magic Mirror', 'Pegasus Boots', 'BigRedBomb']],
#["Ice Rod Cave", True, ['Magic Mirror', 'Beat Agahnim 1', 'BigRedBomb']],
@@ -236,7 +249,8 @@ def testLightWorld(self):
["Graveyard Cave", False, []],
["Graveyard Cave", False, [], ['Moon Pearl']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Graveyard Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
["Checkerboard Cave", False, []],
["Checkerboard Cave", False, [], ['Progressive Glove']],
diff --git a/worlds/alttp/test/minor_glitches/TestDarkWorld.py b/worlds/alttp/test/minor_glitches/TestDarkWorld.py
index 3a6f97254c95..9b0e43ea9494 100644
--- a/worlds/alttp/test/minor_glitches/TestDarkWorld.py
+++ b/worlds/alttp/test/minor_glitches/TestDarkWorld.py
@@ -7,43 +7,48 @@ def testSouthDarkWorld(self):
self.run_location_tests([
["Hype Cave - Top", False, []],
["Hype Cave - Top", False, [], ['Moon Pearl']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Hype Cave - Middle Right", False, []],
["Hype Cave - Middle Right", False, [], ['Moon Pearl']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Middle Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Hype Cave - Middle Left", False, []],
["Hype Cave - Middle Left", False, [], ['Moon Pearl']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Middle Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Hype Cave - Bottom", False, []],
["Hype Cave - Bottom", False, [], ['Moon Pearl']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Bottom", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Hype Cave - Generous Guy", False, []],
["Hype Cave - Generous Guy", False, [], ['Moon Pearl']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Stumpy", False, []],
["Stumpy", False, [], ['Moon Pearl']],
@@ -66,10 +71,11 @@ def testWestDarkWorld(self):
self.run_location_tests([
["Brewery", False, []],
["Brewery", False, [], ['Moon Pearl']],
- ["Brewery", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Brewery", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Brewery", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Brewery", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Brewery", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["C-Shaped House", False, []],
["C-Shaped House", False, [], ['Moon Pearl']],
diff --git a/worlds/alttp/test/minor_glitches/TestDeathMountain.py b/worlds/alttp/test/minor_glitches/TestDeathMountain.py
index 2603aaeb7b9e..7d7589d2f7fe 100644
--- a/worlds/alttp/test/minor_glitches/TestDeathMountain.py
+++ b/worlds/alttp/test/minor_glitches/TestDeathMountain.py
@@ -48,7 +48,8 @@ def testEastDeathMountain(self):
["Mimic Cave", False, [], ['Moon Pearl']],
["Mimic Cave", False, [], ['Cane of Somaria']],
["Mimic Cave", False, ['Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']],
- ["Mimic Cave", True, ['Quake', 'Progressive Sword', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Moon Pearl', 'Cane of Somaria', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Mimic Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Quake', 'Progressive Sword', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Moon Pearl', 'Cane of Somaria', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Spiral Cave", False, []],
["Spiral Cave", False, [], ['Progressive Glove', 'Flute']],
@@ -73,10 +74,11 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Far Left", False, ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Far Left", False, ['Flute', 'Magic Mirror']],
["Paradox Cave Lower - Far Left", False, ['Flute', 'Hammer']],
- ["Paradox Cave Lower - Far Left", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
- ["Paradox Cave Lower - Far Left", True, ['Flute', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Far Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Far Left", True, ['Cane of Somaria', 'Flute', 'Hookshot']],
+ ["Paradox Cave Lower - Far Left", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Lower - Far Left", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Far Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']],
["Paradox Cave Lower - Left", False, []],
["Paradox Cave Lower - Left", False, [], ['Progressive Glove', 'Flute']],
@@ -87,10 +89,11 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Left", False, ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Left", False, ['Flute', 'Magic Mirror']],
["Paradox Cave Lower - Left", False, ['Flute', 'Hammer']],
- ["Paradox Cave Lower - Left", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
- ["Paradox Cave Lower - Left", True, ['Flute', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Left", True, ['Cane of Somaria', 'Flute', 'Hookshot']],
+ ["Paradox Cave Lower - Left", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Lower - Left", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']],
["Paradox Cave Lower - Middle", False, []],
["Paradox Cave Lower - Middle", False, [], ['Progressive Glove', 'Flute']],
@@ -101,10 +104,11 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Middle", False, ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Middle", False, ['Flute', 'Magic Mirror']],
["Paradox Cave Lower - Middle", False, ['Flute', 'Hammer']],
- ["Paradox Cave Lower - Middle", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
- ["Paradox Cave Lower - Middle", True, ['Flute', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Middle", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Middle", True, ['Cane of Somaria', 'Flute', 'Hookshot']],
+ ["Paradox Cave Lower - Middle", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Lower - Middle", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Middle", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']],
["Paradox Cave Lower - Right", False, []],
["Paradox Cave Lower - Right", False, [], ['Progressive Glove', 'Flute']],
@@ -115,10 +119,11 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Right", False, ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Right", False, ['Flute', 'Magic Mirror']],
["Paradox Cave Lower - Right", False, ['Flute', 'Hammer']],
- ["Paradox Cave Lower - Right", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
- ["Paradox Cave Lower - Right", True, ['Flute', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Right", True, ['Cane of Somaria', 'Flute', 'Hookshot']],
+ ["Paradox Cave Lower - Right", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Lower - Right", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']],
["Paradox Cave Lower - Far Right", False, []],
["Paradox Cave Lower - Far Right", False, [], ['Progressive Glove', 'Flute']],
@@ -129,10 +134,11 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Far Right", False, ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Far Right", False, ['Flute', 'Magic Mirror']],
["Paradox Cave Lower - Far Right", False, ['Flute', 'Hammer']],
- ["Paradox Cave Lower - Far Right", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
- ["Paradox Cave Lower - Far Right", True, ['Flute', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Far Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Far Right", True, ['Cane of Somaria', 'Flute', 'Hookshot']],
+ ["Paradox Cave Lower - Far Right", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Lower - Far Right", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Far Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']],
["Paradox Cave Upper - Left", False, []],
["Paradox Cave Upper - Left", False, [], ['Progressive Glove', 'Flute']],
@@ -143,10 +149,11 @@ def testEastDeathMountain(self):
["Paradox Cave Upper - Left", False, ['Progressive Glove', 'Hookshot']],
["Paradox Cave Upper - Left", False, ['Flute', 'Magic Mirror']],
["Paradox Cave Upper - Left", False, ['Flute', 'Hammer']],
- ["Paradox Cave Upper - Left", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
- ["Paradox Cave Upper - Left", True, ['Flute', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Upper - Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']],
["Paradox Cave Upper - Right", False, []],
["Paradox Cave Upper - Right", False, [], ['Progressive Glove', 'Flute']],
@@ -157,10 +164,11 @@ def testEastDeathMountain(self):
["Paradox Cave Upper - Right", False, ['Progressive Glove', 'Hookshot']],
["Paradox Cave Upper - Right", False, ['Flute', 'Magic Mirror']],
["Paradox Cave Upper - Right", False, ['Flute', 'Hammer']],
- ["Paradox Cave Upper - Right", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
- ["Paradox Cave Upper - Right", True, ['Flute', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Upper - Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']],
])
def testWestDarkWorldDeathMountain(self):
diff --git a/worlds/alttp/test/minor_glitches/TestLightWorld.py b/worlds/alttp/test/minor_glitches/TestLightWorld.py
index bdfdc2349691..017f2d64a8f8 100644
--- a/worlds/alttp/test/minor_glitches/TestLightWorld.py
+++ b/worlds/alttp/test/minor_glitches/TestLightWorld.py
@@ -29,17 +29,21 @@ def testLightWorld(self):
["Kakariko Tavern", True, []],
- ["Chicken House", True, []],
+ ["Chicken House", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Chicken House", True, ['Bomb Upgrade (+5)']],
- ["Aginah's Cave", True, []],
+ ["Aginah's Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Aginah's Cave", True, ['Bomb Upgrade (+5)']],
- ["Sahasrahla's Hut - Left", True, []],
+ ["Sahasrahla's Hut - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)']],
+ ["Sahasrahla's Hut - Middle", True, ['Pegasus Boots']],
+ ["Sahasrahla's Hut - Middle", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Right", True, ['Bomb Upgrade (+5)']],
+ ["Sahasrahla's Hut - Right", True, ['Pegasus Boots']],
- ["Sahasrahla's Hut - Middle", True, []],
-
- ["Sahasrahla's Hut - Right", True, []],
-
- ["Kakariko Well - Top", True, []],
+ ["Kakariko Well - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)']],
["Kakariko Well - Left", True, []],
@@ -49,7 +53,8 @@ def testLightWorld(self):
["Kakariko Well - Bottom", True, []],
- ["Blind's Hideout - Top", True, []],
+ ["Blind's Hideout - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)']],
["Blind's Hideout - Left", True, []],
@@ -63,15 +68,19 @@ def testLightWorld(self):
["Bonk Rock Cave", False, [], ['Pegasus Boots']],
["Bonk Rock Cave", True, ['Pegasus Boots']],
- ["Mini Moldorm Cave - Far Left", True, []],
-
- ["Mini Moldorm Cave - Left", True, []],
+ ["Mini Moldorm Cave - Far Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Far Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
- ["Mini Moldorm Cave - Right", True, []],
-
- ["Mini Moldorm Cave - Far Right", True, []],
-
- ["Ice Rod Cave", True, []],
+ ["Ice Rod Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Ice Rod Cave", True, ['Bomb Upgrade (+5)']],
["Bottle Merchant", True, []],
@@ -131,11 +140,12 @@ def testLightWorld(self):
["Graveyard Cave", False, []],
["Graveyard Cave", False, [], ['Magic Mirror']],
["Graveyard Cave", False, [], ['Moon Pearl']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Hammer']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Hammer', 'Hookshot']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Graveyard Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Hammer']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Hammer', 'Hookshot']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Checkerboard Cave", False, []],
["Checkerboard Cave", False, [], ['Progressive Glove']],
@@ -143,8 +153,6 @@ def testLightWorld(self):
["Checkerboard Cave", False, [], ['Magic Mirror']],
["Checkerboard Cave", True, ['Flute', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
- ["Mini Moldorm Cave - Generous Guy", True, []],
-
["Library", False, []],
["Library", False, [], ['Pegasus Boots']],
["Library", True, ['Pegasus Boots']],
@@ -155,7 +163,10 @@ def testLightWorld(self):
["Potion Shop", False, [], ['Mushroom']],
["Potion Shop", True, ['Mushroom']],
- ["Maze Race", True, []],
+ ["Maze Race", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Magic Mirror', 'Pegasus Boots']],
+ ["Maze Race", True, ['Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Maze Race", True, ['Bomb Upgrade (+5)']],
+ ["Maze Race", True, ['Pegasus Boots']],
["Desert Ledge", False, []],
["Desert Ledge", False, [], ['Book of Mudora', 'Flute']],
diff --git a/worlds/alttp/test/minor_glitches/TestMinor.py b/worlds/alttp/test/minor_glitches/TestMinor.py
index fdf626fe9d37..8432028bf007 100644
--- a/worlds/alttp/test/minor_glitches/TestMinor.py
+++ b/worlds/alttp/test/minor_glitches/TestMinor.py
@@ -1,38 +1,28 @@
-from argparse import Namespace
-
-from BaseClasses import MultiWorld
-from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
-from worlds.alttp.EntranceShuffle import link_entrances
+from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.InvertedRegions import mark_dark_world_regions
from worlds.alttp.ItemPool import difficulties
-from worlds.alttp.Items import ItemFactory
-from worlds.alttp.Regions import create_regions
-from worlds.alttp.Shops import create_shops
+from worlds.alttp.Items import item_factory
from test.TestBase import TestBase
+from worlds.alttp.Options import GlitchesRequired
-from worlds import AutoWorld
+from worlds.alttp.test import LTTPTestBase
-class TestMinor(TestBase):
+class TestMinor(TestBase, LTTPTestBase):
def setUp(self):
- self.multiworld = MultiWorld(1)
- self.multiworld.set_seed(None)
- args = Namespace()
- for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
- setattr(args, name, {1: option.from_any(option.default)})
- self.multiworld.set_options(args)
- self.multiworld.set_default_common_options()
- self.multiworld.logic[1] = "minorglitches"
- self.multiworld.difficulty_requirements[1] = difficulties['normal']
- self.multiworld.worlds[1].er_seed = 0
- self.multiworld.worlds[1].create_regions()
- self.multiworld.worlds[1].create_items()
- self.multiworld.required_medallions[1] = ['Ether', 'Quake']
+ self.world_setup()
+ self.multiworld.glitches_required[1] = GlitchesRequired.from_any("minor_glitches")
+ self.multiworld.bombless_start[1].value = True
+ self.multiworld.shuffle_capacity_upgrades[1].value = 2
+ self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
+ self.world.er_seed = 0
+ self.world.create_regions()
+ self.world.create_items()
self.multiworld.itempool.extend(get_dungeon_item_pool(self.multiworld))
- self.multiworld.itempool.extend(ItemFactory(
+ self.multiworld.itempool.extend(item_factory(
['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1',
- 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
+ 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], self.world))
self.multiworld.get_location('Agahnim 1', 1).item = None
self.multiworld.get_location('Agahnim 2', 1).item = None
mark_dark_world_regions(self.multiworld, 1)
- self.multiworld.worlds[1].set_rules()
+ self.world.set_rules()
diff --git a/worlds/alttp/test/options/TestOpenPyramid.py b/worlds/alttp/test/options/TestOpenPyramid.py
index c66eb2ee98ec..c7912c43d72b 100644
--- a/worlds/alttp/test/options/TestOpenPyramid.py
+++ b/worlds/alttp/test/options/TestOpenPyramid.py
@@ -1,5 +1,5 @@
-from test.TestBase import WorldTestBase
-from ...Items import ItemFactory
+from test.bases import WorldTestBase
+from ...Items import item_factory
class PyramidTestBase(WorldTestBase):
@@ -23,7 +23,7 @@ class GoalPyramidTest(PyramidTestBase):
}
def testCrystalsGoalAccess(self):
- self.multiworld.goal[1] = "crystals"
+ self.multiworld.goal[1].value = 1 # crystals
self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
self.collect_by_name(["Hammer", "Progressive Glove", "Moon Pearl"])
self.assertTrue(self.can_reach_entrance("Pyramid Hole"))
@@ -32,6 +32,6 @@ def testGanonGoalAccess(self):
self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
self.collect_by_name(["Hammer", "Progressive Glove", "Moon Pearl"])
self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
- self.multiworld.state.collect(ItemFactory("Beat Agahnim 2", 1))
+ self.collect(item_factory("Beat Agahnim 2", self.multiworld.worlds[1]))
self.assertTrue(self.can_reach_entrance("Pyramid Hole"))
diff --git a/worlds/alttp/test/owg/TestDarkWorld.py b/worlds/alttp/test/owg/TestDarkWorld.py
index 93324656bd93..c671f6485c92 100644
--- a/worlds/alttp/test/owg/TestDarkWorld.py
+++ b/worlds/alttp/test/owg/TestDarkWorld.py
@@ -7,48 +7,53 @@ def testSouthDarkWorld(self):
self.run_location_tests([
["Hype Cave - Top", False, []],
["Hype Cave - Top", False, [], ['Moon Pearl']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Pegasus Boots']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Hype Cave - Middle Right", False, []],
["Hype Cave - Middle Right", False, [], ['Moon Pearl']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Pegasus Boots']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Middle Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Hype Cave - Middle Left", False, []],
["Hype Cave - Middle Left", False, [], ['Moon Pearl']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Pegasus Boots']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Middle Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Hype Cave - Bottom", False, []],
["Hype Cave - Bottom", False, [], ['Moon Pearl']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Pegasus Boots']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Bottom", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Hype Cave - Generous Guy", False, []],
["Hype Cave - Generous Guy", False, [], ['Moon Pearl']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Pegasus Boots']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Stumpy", False, []],
["Stumpy", False, [], ['Moon Pearl']],
@@ -129,13 +134,14 @@ def testWestDarkWorld(self):
self.run_location_tests([
["Brewery", False, []],
["Brewery", False, [], ['Moon Pearl']],
+ ["Brewery", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
["Brewery", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot', 'Progressive Glove']],
- ["Brewery", True, ['Moon Pearl', 'Pegasus Boots']],
- ["Brewery", True, ['Moon Pearl', 'Flute', 'Magic Mirror']],
- ["Brewery", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Brewery", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Brewery", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Brewery", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']],
+ ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Flute', 'Magic Mirror']],
+ ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["C-Shaped House", False, []],
["C-Shaped House", False, [], ['Moon Pearl', 'Magic Mirror']],
diff --git a/worlds/alttp/test/owg/TestDeathMountain.py b/worlds/alttp/test/owg/TestDeathMountain.py
index 41031c65c593..59308b65f092 100644
--- a/worlds/alttp/test/owg/TestDeathMountain.py
+++ b/worlds/alttp/test/owg/TestDeathMountain.py
@@ -48,9 +48,10 @@ def testEastDeathMountain(self):
["Mimic Cave", False, [], ['Hammer']],
["Mimic Cave", False, [], ['Pegasus Boots', 'Flute', 'Lamp']],
["Mimic Cave", False, [], ['Pegasus Boots', 'Flute', 'Progressive Glove']],
- ["Mimic Cave", True, ['Magic Mirror', 'Hammer', 'Pegasus Boots']],
- ["Mimic Cave", True, ['Magic Mirror', 'Hammer', 'Progressive Glove', 'Lamp']],
- ["Mimic Cave", True, ['Magic Mirror', 'Hammer', 'Flute']],
+ ["Mimic Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Hookshot', 'Hammer']],
+ ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Magic Mirror', 'Hammer', 'Pegasus Boots']],
+ ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Magic Mirror', 'Hammer', 'Progressive Glove', 'Lamp']],
+ ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Magic Mirror', 'Hammer', 'Flute']],
["Spiral Cave", False, []],
["Spiral Cave", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']],
@@ -64,65 +65,72 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Far Left", False, []],
["Paradox Cave Lower - Far Left", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']],
["Paradox Cave Lower - Far Left", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot']],
- ["Paradox Cave Lower - Far Left", True, ['Pegasus Boots']],
- ["Paradox Cave Lower - Far Left", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']],
- ["Paradox Cave Lower - Far Left", True, ['Flute', 'Magic Mirror']],
+ ["Paradox Cave Lower - Far Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Far Left", True, ['Fire Rod', 'Pegasus Boots']],
+ ["Paradox Cave Lower - Far Left", True, ['Cane of Somaria', 'Flute', 'Hookshot']],
+ ["Paradox Cave Lower - Far Left", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Lower - Far Left", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror']],
+ ["Paradox Cave Lower - Far Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror']],
["Paradox Cave Lower - Left", False, []],
["Paradox Cave Lower - Left", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']],
["Paradox Cave Lower - Left", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot']],
- ["Paradox Cave Lower - Left", True, ['Pegasus Boots']],
- ["Paradox Cave Lower - Left", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']],
- ["Paradox Cave Lower - Left", True, ['Flute', 'Magic Mirror']],
+ ["Paradox Cave Lower - Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Left", True, ['Fire Rod', 'Pegasus Boots']],
+ ["Paradox Cave Lower - Left", True, ['Cane of Somaria', 'Flute', 'Hookshot']],
+ ["Paradox Cave Lower - Left", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Lower - Left", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror']],
+ ["Paradox Cave Lower - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror']],
["Paradox Cave Lower - Middle", False, []],
["Paradox Cave Lower - Middle", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']],
["Paradox Cave Lower - Middle", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot']],
- ["Paradox Cave Lower - Middle", True, ['Pegasus Boots']],
- ["Paradox Cave Lower - Middle", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']],
- ["Paradox Cave Lower - Middle", True, ['Flute', 'Magic Mirror']],
+ ["Paradox Cave Lower - Middle", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Middle", True, ['Fire Rod', 'Pegasus Boots']],
+ ["Paradox Cave Lower - Middle", True, ['Cane of Somaria', 'Flute', 'Hookshot']],
+ ["Paradox Cave Lower - Middle", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Lower - Middle", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror']],
+ ["Paradox Cave Lower - Middle", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror']],
["Paradox Cave Lower - Right", False, []],
["Paradox Cave Lower - Right", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']],
["Paradox Cave Lower - Right", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot']],
- ["Paradox Cave Lower - Right", True, ['Pegasus Boots']],
- ["Paradox Cave Lower - Right", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']],
- ["Paradox Cave Lower - Right", True, ['Flute', 'Magic Mirror']],
+ ["Paradox Cave Lower - Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Right", True, ['Fire Rod', 'Pegasus Boots']],
+ ["Paradox Cave Lower - Right", True, ['Cane of Somaria', 'Flute', 'Hookshot']],
+ ["Paradox Cave Lower - Right", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Lower - Right", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror']],
+ ["Paradox Cave Lower - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror']],
["Paradox Cave Lower - Far Right", False, []],
["Paradox Cave Lower - Far Right", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']],
["Paradox Cave Lower - Far Right", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot']],
- ["Paradox Cave Lower - Far Right", True, ['Pegasus Boots']],
- ["Paradox Cave Lower - Far Right", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']],
- ["Paradox Cave Lower - Far Right", True, ['Flute', 'Magic Mirror']],
+ ["Paradox Cave Lower - Far Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Far Right", True, ['Fire Rod', 'Pegasus Boots']],
+ ["Paradox Cave Lower - Far Right", True, ['Cane of Somaria', 'Flute', 'Hookshot']],
+ ["Paradox Cave Lower - Far Right", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Lower - Far Right", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror']],
+ ["Paradox Cave Lower - Far Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror']],
["Paradox Cave Upper - Left", False, []],
["Paradox Cave Upper - Left", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']],
["Paradox Cave Upper - Left", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot']],
- ["Paradox Cave Upper - Left", True, ['Pegasus Boots']],
- ["Paradox Cave Upper - Left", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']],
- ["Paradox Cave Upper - Left", True, ['Flute', 'Magic Mirror']],
+ ["Paradox Cave Upper - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Pegasus Boots']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Magic Mirror']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror']],
["Paradox Cave Upper - Right", False, []],
["Paradox Cave Upper - Right", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']],
["Paradox Cave Upper - Right", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot']],
- ["Paradox Cave Upper - Right", True, ['Pegasus Boots']],
- ["Paradox Cave Upper - Right", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']],
- ["Paradox Cave Upper - Right", True, ['Flute', 'Magic Mirror']],
+ ["Paradox Cave Upper - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Pegasus Boots']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Magic Mirror']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror']],
])
def testWestDarkWorldDeathMountain(self):
@@ -169,7 +177,7 @@ def testEastDarkWorldDeathMountain(self):
["Hookshot Cave - Bottom Right", False, []],
["Hookshot Cave - Bottom Right", False, [], ['Progressive Glove', 'Pegasus Boots']],
["Hookshot Cave - Bottom Right", False, [], ['Moon Pearl']],
- ["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Pegasus Boots']],
+ ["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Pegasus Boots', 'Bomb Upgrade (50)']],
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
@@ -177,7 +185,7 @@ def testEastDarkWorldDeathMountain(self):
["Hookshot Cave - Bottom Left", False, [], ['Progressive Glove', 'Pegasus Boots']],
["Hookshot Cave - Bottom Left", False, [], ['Moon Pearl']],
["Hookshot Cave - Bottom Left", False, [], ['Hookshot']],
- ["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
+ ["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
@@ -185,7 +193,7 @@ def testEastDarkWorldDeathMountain(self):
["Hookshot Cave - Top Left", False, [], ['Progressive Glove', 'Pegasus Boots']],
["Hookshot Cave - Top Left", False, [], ['Moon Pearl']],
["Hookshot Cave - Top Left", False, [], ['Hookshot']],
- ["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
+ ["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
@@ -193,7 +201,7 @@ def testEastDarkWorldDeathMountain(self):
["Hookshot Cave - Top Right", False, [], ['Progressive Glove', 'Pegasus Boots']],
["Hookshot Cave - Top Right", False, [], ['Moon Pearl']],
["Hookshot Cave - Top Right", False, [], ['Hookshot']],
- ["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
+ ["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
])
\ No newline at end of file
diff --git a/worlds/alttp/test/owg/TestDungeons.py b/worlds/alttp/test/owg/TestDungeons.py
index 284b489b1626..2e55b308d327 100644
--- a/worlds/alttp/test/owg/TestDungeons.py
+++ b/worlds/alttp/test/owg/TestDungeons.py
@@ -6,12 +6,14 @@ class TestDungeons(TestVanillaOWG):
def testFirstDungeonChests(self):
self.run_location_tests([
["Hyrule Castle - Map Chest", True, []],
+ ["Hyrule Castle - Map Guard Key Drop", False, []],
+ ["Hyrule Castle - Map Guard Key Drop", True, ['Progressive Sword']],
["Sanctuary", True, []],
["Sewers - Secret Room - Left", False, []],
- ["Sewers - Secret Room - Left", True, ['Progressive Glove']],
- ["Sewers - Secret Room - Left", True, ['Lamp', 'Small Key (Hyrule Castle)']],
+ ["Sewers - Secret Room - Left", True, ['Pegasus Boots', 'Progressive Glove']],
+ ["Sewers - Secret Room - Left", True, ['Bomb Upgrade (+5)', 'Lamp', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)', 'Small Key (Hyrule Castle)']],
["Eastern Palace - Compass Chest", True, []],
@@ -24,26 +26,25 @@ def testFirstDungeonChests(self):
["Desert Palace - Boss", False, [], ['Small Key (Desert Palace)']],
["Desert Palace - Boss", False, [], ['Big Key (Desert Palace)']],
["Desert Palace - Boss", False, [], ['Lamp', 'Fire Rod']],
- ["Desert Palace - Boss", True, ['Progressive Sword', 'Small Key (Desert Palace)', 'Pegasus Boots', 'Lamp', 'Big Key (Desert Palace)']],
- ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Pegasus Boots', 'Fire Rod', 'Big Key (Desert Palace)']],
+ ["Desert Palace - Boss", True, ['Progressive Sword', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Pegasus Boots', 'Lamp', 'Big Key (Desert Palace)']],
+ ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Small Key (Desert Palace)', 'Pegasus Boots', 'Fire Rod', 'Big Key (Desert Palace)']],
["Tower of Hera - Basement Cage", False, []],
["Tower of Hera - Basement Cage", False, [], ['Pegasus Boots', "Flute", "Progressive Glove"]],
["Tower of Hera - Basement Cage", False, [], ['Pegasus Boots', "Flute", "Lamp"]],
["Tower of Hera - Basement Cage", False, [], ['Pegasus Boots', "Magic Mirror", "Hammer"]],
["Tower of Hera - Basement Cage", False, [], ['Pegasus Boots', "Magic Mirror", "Hookshot"]],
- ["Tower of Hera - Basement Cage", True, ['Pegasus Boots']],
- ["Tower of Hera - Basement Cage", True, ["Flute", "Magic Mirror"]],
- ["Tower of Hera - Basement Cage", True, ["Progressive Glove", "Lamp", "Magic Mirror"]],
+ ["Tower of Hera - Basement Cage", True, ['Pegasus Boots', 'Bomb Upgrade (50)']],
+ ["Tower of Hera - Basement Cage", True, ["Flute", "Magic Mirror", 'Bomb Upgrade (50)']],
+ ["Tower of Hera - Basement Cage", True, ["Progressive Glove", "Lamp", "Magic Mirror", 'Bomb Upgrade (50)']],
["Tower of Hera - Basement Cage", True, ["Flute", "Hookshot", "Hammer"]],
- ["Tower of Hera - Basement Cage", True, ["Progressive Glove", "Lamp", "Magic Mirror"]],
+ ["Tower of Hera - Basement Cage", True, ["Progressive Glove", "Lamp", "Magic Mirror", 'Bomb Upgrade (50)']],
["Castle Tower - Room 03", False, []],
["Castle Tower - Room 03", False, ['Progressive Sword'], ['Progressive Sword', 'Cape', 'Beat Agahnim 1']],
- ["Castle Tower - Room 03", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']],
["Castle Tower - Room 03", True, ['Progressive Sword', 'Progressive Sword']],
- ["Castle Tower - Room 03", True, ['Cape', 'Progressive Bow']],
- ["Castle Tower - Room 03", True, ['Beat Agahnim 1', 'Fire Rod']],
+ ["Castle Tower - Room 03", True, ['Progressive Sword', 'Cape']],
+ ["Castle Tower - Room 03", True, ['Progressive Sword', 'Beat Agahnim 1']],
["Palace of Darkness - Shooter Room", False, []],
["Palace of Darkness - Shooter Room", False, [], ['Moon Pearl']],
@@ -68,9 +69,10 @@ def testFirstDungeonChests(self):
["Skull Woods - Big Chest", False, []],
["Skull Woods - Big Chest", False, [], ['Big Key (Skull Woods)']],
+ ["Skull Woods - Big Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
#todo: Bomb Jump in logic?
#["Skull Woods - Big Chest", True, ['Magic Mirror', 'Pegasus Boots', 'Big Key (Skull Woods)']],
- ["Skull Woods - Big Chest", True, ['Moon Pearl', 'Pegasus Boots', 'Big Key (Skull Woods)']],
+ ["Skull Woods - Big Chest", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots', 'Big Key (Skull Woods)']],
["Skull Woods - Big Key Chest", False, []],
["Skull Woods - Big Key Chest", True, ['Magic Mirror', 'Pegasus Boots']],
@@ -88,10 +90,10 @@ def testFirstDungeonChests(self):
["Ice Palace - Compass Chest", False, []],
["Ice Palace - Compass Chest", False, [], ['Fire Rod', 'Bombos']],
["Ice Palace - Compass Chest", False, [], ['Fire Rod', 'Progressive Sword']],
- ["Ice Palace - Compass Chest", True, ['Moon Pearl', 'Pegasus Boots', 'Flippers', 'Fire Rod']],
- ["Ice Palace - Compass Chest", True, ['Moon Pearl', 'Pegasus Boots', 'Flippers', 'Bombos', 'Progressive Sword']],
- ["Ice Palace - Compass Chest", True, ['Progressive Glove', 'Progressive Glove', 'Fire Rod']],
- ["Ice Palace - Compass Chest", True, ['Progressive Glove', 'Progressive Glove', 'Bombos', 'Progressive Sword']],
+ ["Ice Palace - Compass Chest", True, ['Small Key (Ice Palace)', 'Moon Pearl', 'Pegasus Boots', 'Flippers', 'Fire Rod']],
+ ["Ice Palace - Compass Chest", True, ['Small Key (Ice Palace)', 'Moon Pearl', 'Pegasus Boots', 'Flippers', 'Bombos', 'Progressive Sword']],
+ ["Ice Palace - Compass Chest", True, ['Small Key (Ice Palace)', 'Progressive Glove', 'Progressive Glove', 'Fire Rod']],
+ ["Ice Palace - Compass Chest", True, ['Small Key (Ice Palace)', 'Progressive Glove', 'Progressive Glove', 'Bombos', 'Progressive Sword']],
["Misery Mire - Bridge Chest", False, []],
["Misery Mire - Bridge Chest", False, [], ['Moon Pearl']],
@@ -103,15 +105,15 @@ def testFirstDungeonChests(self):
["Turtle Rock - Compass Chest", False, [], ['Cane of Somaria']],
#todo: does clip require sword?
#["Turtle Rock - Compass Chest", True, ['Moon Pearl', 'Pegasus Boots', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
- ["Turtle Rock - Compass Chest", True, ['Moon Pearl', 'Pegasus Boots', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Sword']],
+ ["Turtle Rock - Compass Chest", True, ['Moon Pearl', 'Pegasus Boots', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Sword']],
["Turtle Rock - Compass Chest", True, ['Moon Pearl', 'Pegasus Boots', 'Cane of Somaria', 'Progressive Sword', 'Quake']],
- ["Turtle Rock - Compass Chest", True, ['Pegasus Boots', 'Magic Mirror', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Turtle Rock - Compass Chest", True, ['Pegasus Boots', 'Magic Mirror', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Chain Chomps", False, []],
#todo: does clip require sword?
#["Turtle Rock - Chain Chomps", True, ['Moon Pearl', 'Pegasus Boots']],
- ["Turtle Rock - Chain Chomps", True, ['Moon Pearl', 'Pegasus Boots', 'Progressive Sword']],
- ["Turtle Rock - Chain Chomps", True, ['Pegasus Boots', 'Magic Mirror']],
+ ["Turtle Rock - Chain Chomps", True, ['Moon Pearl', 'Pegasus Boots', 'Progressive Sword', 'Progressive Sword']],
+ ["Turtle Rock - Chain Chomps", True, ['Pegasus Boots', 'Magic Mirror', 'Progressive Bow']],
["Turtle Rock - Crystaroller Room", False, []],
["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)']],
diff --git a/worlds/alttp/test/owg/TestLightWorld.py b/worlds/alttp/test/owg/TestLightWorld.py
index f3f1ba0c2703..84342a33c856 100644
--- a/worlds/alttp/test/owg/TestLightWorld.py
+++ b/worlds/alttp/test/owg/TestLightWorld.py
@@ -25,17 +25,21 @@ def testLightWorld(self):
["Kakariko Tavern", True, []],
- ["Chicken House", True, []],
+ ["Chicken House", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Chicken House", True, ['Bomb Upgrade (+5)']],
- ["Aginah's Cave", True, []],
+ ["Aginah's Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Aginah's Cave", True, ['Bomb Upgrade (+5)']],
- ["Sahasrahla's Hut - Left", True, []],
+ ["Sahasrahla's Hut - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)']],
+ ["Sahasrahla's Hut - Middle", True, ['Pegasus Boots']],
+ ["Sahasrahla's Hut - Middle", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Right", True, ['Bomb Upgrade (+5)']],
+ ["Sahasrahla's Hut - Right", True, ['Pegasus Boots']],
- ["Sahasrahla's Hut - Middle", True, []],
-
- ["Sahasrahla's Hut - Right", True, []],
-
- ["Kakariko Well - Top", True, []],
+ ["Kakariko Well - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)']],
["Kakariko Well - Left", True, []],
@@ -45,7 +49,8 @@ def testLightWorld(self):
["Kakariko Well - Bottom", True, []],
- ["Blind's Hideout - Top", True, []],
+ ["Blind's Hideout - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)']],
["Blind's Hideout - Left", True, []],
@@ -59,15 +64,19 @@ def testLightWorld(self):
["Bonk Rock Cave", False, [], ['Pegasus Boots']],
["Bonk Rock Cave", True, ['Pegasus Boots']],
- ["Mini Moldorm Cave - Far Left", True, []],
-
- ["Mini Moldorm Cave - Left", True, []],
+ ["Mini Moldorm Cave - Far Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Far Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
- ["Mini Moldorm Cave - Right", True, []],
-
- ["Mini Moldorm Cave - Far Right", True, []],
-
- ["Ice Rod Cave", True, []],
+ ["Ice Rod Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Ice Rod Cave", True, ['Bomb Upgrade (+5)']],
["Bottle Merchant", True, []],
@@ -126,12 +135,13 @@ def testLightWorld(self):
["Graveyard Cave", False, []],
["Graveyard Cave", False, [], ['Pegasus Boots', 'Magic Mirror']],
["Graveyard Cave", False, [], ['Pegasus Boots', 'Moon Pearl']],
- ["Graveyard Cave", True, ['Pegasus Boots']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Hammer']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Hammer', 'Hookshot']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Graveyard Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Pegasus Boots']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Hammer']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Hammer', 'Hookshot']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Checkerboard Cave", False, []],
["Checkerboard Cave", False, [], ['Progressive Glove']],
@@ -140,8 +150,6 @@ def testLightWorld(self):
["Checkerboard Cave", True, ['Pegasus Boots', 'Progressive Glove']],
["Checkerboard Cave", True, ['Flute', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
- ["Mini Moldorm Cave - Generous Guy", True, []],
-
["Library", False, []],
["Library", False, [], ['Pegasus Boots']],
["Library", True, ['Pegasus Boots']],
@@ -152,7 +160,10 @@ def testLightWorld(self):
["Potion Shop", False, [], ['Mushroom']],
["Potion Shop", True, ['Mushroom']],
- ["Maze Race", True, []],
+ ["Maze Race", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Magic Mirror', 'Pegasus Boots']],
+ ["Maze Race", True, ['Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Maze Race", True, ['Bomb Upgrade (+5)']],
+ ["Maze Race", True, ['Pegasus Boots']],
["Desert Ledge", False, []],
["Desert Ledge", False, [], ['Pegasus Boots', 'Book of Mudora', 'Flute']],
diff --git a/worlds/alttp/test/owg/TestVanillaOWG.py b/worlds/alttp/test/owg/TestVanillaOWG.py
index c0888aa32fe6..67156eb97275 100644
--- a/worlds/alttp/test/owg/TestVanillaOWG.py
+++ b/worlds/alttp/test/owg/TestVanillaOWG.py
@@ -1,35 +1,28 @@
-from argparse import Namespace
-
-from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.InvertedRegions import mark_dark_world_regions
from worlds.alttp.ItemPool import difficulties
-from worlds.alttp.Items import ItemFactory
+from worlds.alttp.Items import item_factory
from test.TestBase import TestBase
+from worlds.alttp.Options import GlitchesRequired
-from worlds import AutoWorld
+from worlds.alttp.test import LTTPTestBase
-class TestVanillaOWG(TestBase):
+class TestVanillaOWG(TestBase, LTTPTestBase):
def setUp(self):
- self.multiworld = MultiWorld(1)
- self.multiworld.set_seed(None)
- args = Namespace()
- for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
- setattr(args, name, {1: option.from_any(option.default)})
- self.multiworld.set_options(args)
- self.multiworld.set_default_common_options()
- self.multiworld.difficulty_requirements[1] = difficulties['normal']
- self.multiworld.logic[1] = "owglitches"
+ self.world_setup()
+ self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
+ self.multiworld.glitches_required[1] = GlitchesRequired.from_any("overworld_glitches")
+ self.multiworld.bombless_start[1].value = True
+ self.multiworld.shuffle_capacity_upgrades[1].value = 2
self.multiworld.worlds[1].er_seed = 0
self.multiworld.worlds[1].create_regions()
self.multiworld.worlds[1].create_items()
- self.multiworld.required_medallions[1] = ['Ether', 'Quake']
self.multiworld.itempool.extend(get_dungeon_item_pool(self.multiworld))
- self.multiworld.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
+ self.multiworld.itempool.extend(item_factory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], self.world))
self.multiworld.get_location('Agahnim 1', 1).item = None
self.multiworld.get_location('Agahnim 2', 1).item = None
self.multiworld.precollected_items[1].clear()
- self.multiworld.itempool.append(ItemFactory('Pegasus Boots', 1))
+ self.multiworld.itempool.append(item_factory('Pegasus Boots', self.world))
mark_dark_world_regions(self.multiworld, 1)
- self.multiworld.worlds[1].set_rules()
\ No newline at end of file
+ self.world.set_rules()
diff --git a/worlds/alttp/test/vanilla/TestDarkWorld.py b/worlds/alttp/test/vanilla/TestDarkWorld.py
index ecb3e5583098..8ff09c527de8 100644
--- a/worlds/alttp/test/vanilla/TestDarkWorld.py
+++ b/worlds/alttp/test/vanilla/TestDarkWorld.py
@@ -7,43 +7,48 @@ def testSouthDarkWorld(self):
self.run_location_tests([
["Hype Cave - Top", False, []],
["Hype Cave - Top", False, [], ['Moon Pearl']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Hype Cave - Middle Right", False, []],
["Hype Cave - Middle Right", False, [], ['Moon Pearl']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Middle Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Hype Cave - Middle Left", False, []],
["Hype Cave - Middle Left", False, [], ['Moon Pearl']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Middle Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Hype Cave - Bottom", False, []],
["Hype Cave - Bottom", False, [], ['Moon Pearl']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Bottom", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Hype Cave - Generous Guy", False, []],
["Hype Cave - Generous Guy", False, [], ['Moon Pearl']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Hype Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Stumpy", False, []],
["Stumpy", False, [], ['Moon Pearl']],
@@ -66,10 +71,11 @@ def testWestDarkWorld(self):
self.run_location_tests([
["Brewery", False, []],
["Brewery", False, [], ['Moon Pearl']],
- ["Brewery", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
- ["Brewery", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']],
- ["Brewery", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Brewery", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Brewery", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']],
+ ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']],
+ ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["C-Shaped House", False, []],
["C-Shaped House", False, [], ['Moon Pearl']],
diff --git a/worlds/alttp/test/vanilla/TestDeathMountain.py b/worlds/alttp/test/vanilla/TestDeathMountain.py
index ecb3831f6ad1..a559d8869c2f 100644
--- a/worlds/alttp/test/vanilla/TestDeathMountain.py
+++ b/worlds/alttp/test/vanilla/TestDeathMountain.py
@@ -48,7 +48,8 @@ def testEastDeathMountain(self):
["Mimic Cave", False, [], ['Moon Pearl']],
["Mimic Cave", False, [], ['Cane of Somaria']],
["Mimic Cave", False, ['Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']],
- ["Mimic Cave", True, ['Quake', 'Progressive Sword', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Moon Pearl', 'Cane of Somaria', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
+ ["Mimic Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Quake', 'Progressive Sword', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Moon Pearl', 'Cane of Somaria', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Spiral Cave", False, []],
["Spiral Cave", False, [], ['Progressive Glove', 'Flute']],
@@ -73,10 +74,10 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Far Left", False, ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Far Left", False, ['Flute', 'Magic Mirror']],
["Paradox Cave Lower - Far Left", False, ['Flute', 'Hammer']],
- ["Paradox Cave Lower - Far Left", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
- ["Paradox Cave Lower - Far Left", True, ['Flute', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Far Left", True, ['Flute', 'Hookshot', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Bomb Upgrade (+5)']],
+ ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer', 'Progressive Sword', 'Progressive Sword']],
+ ["Paradox Cave Lower - Far Left", True, ['Flute', 'Magic Mirror', 'Hammer', 'Fire Rod']],
["Paradox Cave Lower - Left", False, []],
["Paradox Cave Lower - Left", False, [], ['Progressive Glove', 'Flute']],
@@ -87,10 +88,10 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Left", False, ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Left", False, ['Flute', 'Magic Mirror']],
["Paradox Cave Lower - Left", False, ['Flute', 'Hammer']],
- ["Paradox Cave Lower - Left", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
- ["Paradox Cave Lower - Left", True, ['Flute', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Left", True, ['Flute', 'Hookshot', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Bomb Upgrade (+5)']],
+ ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer', 'Progressive Sword', 'Progressive Sword']],
+ ["Paradox Cave Lower - Left", True, ['Flute', 'Magic Mirror', 'Hammer', 'Fire Rod']],
["Paradox Cave Lower - Middle", False, []],
["Paradox Cave Lower - Middle", False, [], ['Progressive Glove', 'Flute']],
@@ -101,10 +102,10 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Middle", False, ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Middle", False, ['Flute', 'Magic Mirror']],
["Paradox Cave Lower - Middle", False, ['Flute', 'Hammer']],
- ["Paradox Cave Lower - Middle", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
- ["Paradox Cave Lower - Middle", True, ['Flute', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Middle", True, ['Flute', 'Hookshot', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Bomb Upgrade (+5)']],
+ ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer', 'Progressive Sword', 'Progressive Sword']],
+ ["Paradox Cave Lower - Middle", True, ['Flute', 'Magic Mirror', 'Hammer', 'Fire Rod']],
["Paradox Cave Lower - Right", False, []],
["Paradox Cave Lower - Right", False, [], ['Progressive Glove', 'Flute']],
@@ -115,10 +116,10 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Right", False, ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Right", False, ['Flute', 'Magic Mirror']],
["Paradox Cave Lower - Right", False, ['Flute', 'Hammer']],
- ["Paradox Cave Lower - Right", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
- ["Paradox Cave Lower - Right", True, ['Flute', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Right", True, ['Flute', 'Hookshot', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Bomb Upgrade (+5)']],
+ ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer', 'Progressive Sword', 'Progressive Sword']],
+ ["Paradox Cave Lower - Right", True, ['Flute', 'Magic Mirror', 'Hammer', 'Fire Rod']],
["Paradox Cave Lower - Far Right", False, []],
["Paradox Cave Lower - Far Right", False, [], ['Progressive Glove', 'Flute']],
@@ -129,10 +130,10 @@ def testEastDeathMountain(self):
["Paradox Cave Lower - Far Right", False, ['Progressive Glove', 'Hookshot']],
["Paradox Cave Lower - Far Right", False, ['Flute', 'Magic Mirror']],
["Paradox Cave Lower - Far Right", False, ['Flute', 'Hammer']],
- ["Paradox Cave Lower - Far Right", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
- ["Paradox Cave Lower - Far Right", True, ['Flute', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Lower - Far Right", True, ['Flute', 'Hookshot', 'Cane of Somaria']],
+ ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Bomb Upgrade (+5)']],
+ ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer', 'Progressive Sword', 'Progressive Sword']],
+ ["Paradox Cave Lower - Far Right", True, ['Flute', 'Magic Mirror', 'Hammer', 'Fire Rod']],
["Paradox Cave Upper - Left", False, []],
["Paradox Cave Upper - Left", False, [], ['Progressive Glove', 'Flute']],
@@ -143,10 +144,11 @@ def testEastDeathMountain(self):
["Paradox Cave Upper - Left", False, ['Progressive Glove', 'Hookshot']],
["Paradox Cave Upper - Left", False, ['Flute', 'Magic Mirror']],
["Paradox Cave Upper - Left", False, ['Flute', 'Hammer']],
- ["Paradox Cave Upper - Left", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
- ["Paradox Cave Upper - Left", True, ['Flute', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Upper - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']],
["Paradox Cave Upper - Right", False, []],
["Paradox Cave Upper - Right", False, [], ['Progressive Glove', 'Flute']],
@@ -157,10 +159,11 @@ def testEastDeathMountain(self):
["Paradox Cave Upper - Right", False, ['Progressive Glove', 'Hookshot']],
["Paradox Cave Upper - Right", False, ['Flute', 'Magic Mirror']],
["Paradox Cave Upper - Right", False, ['Flute', 'Hammer']],
- ["Paradox Cave Upper - Right", True, ['Flute', 'Hookshot']],
- ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']],
- ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
- ["Paradox Cave Upper - Right", True, ['Flute', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Upper - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']],
+ ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']],
])
def testWestDarkWorldDeathMountain(self):
diff --git a/worlds/alttp/test/vanilla/TestLightWorld.py b/worlds/alttp/test/vanilla/TestLightWorld.py
index 977e807290d1..6d9284aba0d3 100644
--- a/worlds/alttp/test/vanilla/TestLightWorld.py
+++ b/worlds/alttp/test/vanilla/TestLightWorld.py
@@ -29,17 +29,21 @@ def testLightWorld(self):
["Kakariko Tavern", True, []],
- ["Chicken House", True, []],
+ ["Chicken House", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Chicken House", True, ['Bomb Upgrade (+5)']],
- ["Aginah's Cave", True, []],
+ ["Aginah's Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Aginah's Cave", True, ['Bomb Upgrade (+5)']],
- ["Sahasrahla's Hut - Left", True, []],
+ ["Sahasrahla's Hut - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)']],
+ ["Sahasrahla's Hut - Middle", True, ['Pegasus Boots']],
+ ["Sahasrahla's Hut - Middle", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']],
+ ["Sahasrahla's Hut - Right", True, ['Bomb Upgrade (+5)']],
+ ["Sahasrahla's Hut - Right", True, ['Pegasus Boots']],
- ["Sahasrahla's Hut - Middle", True, []],
-
- ["Sahasrahla's Hut - Right", True, []],
-
- ["Kakariko Well - Top", True, []],
+ ["Kakariko Well - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)']],
["Kakariko Well - Left", True, []],
@@ -49,7 +53,8 @@ def testLightWorld(self):
["Kakariko Well - Bottom", True, []],
- ["Blind's Hideout - Top", True, []],
+ ["Blind's Hideout - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)']],
["Blind's Hideout - Left", True, []],
@@ -63,15 +68,19 @@ def testLightWorld(self):
["Bonk Rock Cave", False, [], ['Pegasus Boots']],
["Bonk Rock Cave", True, ['Pegasus Boots']],
- ["Mini Moldorm Cave - Far Left", True, []],
-
- ["Mini Moldorm Cave - Left", True, []],
-
- ["Mini Moldorm Cave - Right", True, []],
-
- ["Mini Moldorm Cave - Far Right", True, []],
+ ["Mini Moldorm Cave - Far Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Far Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
+ ["Mini Moldorm Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Progressive Sword']],
- ["Ice Rod Cave", True, []],
+ ["Ice Rod Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Ice Rod Cave", True, ['Bomb Upgrade (+5)']],
["Bottle Merchant", True, []],
@@ -136,11 +145,12 @@ def testLightWorld(self):
["Graveyard Cave", False, []],
["Graveyard Cave", False, [], ['Magic Mirror']],
["Graveyard Cave", False, [], ['Moon Pearl']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Hammer']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Hammer', 'Hookshot']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
- ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
+ ["Graveyard Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Hammer']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Hammer', 'Hookshot']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']],
+ ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Flippers', 'Hookshot']],
["Checkerboard Cave", False, []],
["Checkerboard Cave", False, [], ['Progressive Glove']],
@@ -148,7 +158,6 @@ def testLightWorld(self):
["Checkerboard Cave", False, [], ['Magic Mirror']],
["Checkerboard Cave", True, ['Flute', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
- ["Mini Moldorm Cave - Generous Guy", True, []],
["Library", False, []],
["Library", False, [], ['Pegasus Boots']],
@@ -160,7 +169,10 @@ def testLightWorld(self):
["Potion Shop", False, [], ['Mushroom']],
["Potion Shop", True, ['Mushroom']],
- ["Maze Race", True, []],
+ ["Maze Race", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Magic Mirror', 'Pegasus Boots']],
+ ["Maze Race", True, ['Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']],
+ ["Maze Race", True, ['Bomb Upgrade (+5)']],
+ ["Maze Race", True, ['Pegasus Boots']],
["Desert Ledge", False, []],
["Desert Ledge", False, [], ['Book of Mudora', 'Flute']],
diff --git a/worlds/alttp/test/vanilla/TestVanilla.py b/worlds/alttp/test/vanilla/TestVanilla.py
index e338410df208..7eebc349d43f 100644
--- a/worlds/alttp/test/vanilla/TestVanilla.py
+++ b/worlds/alttp/test/vanilla/TestVanilla.py
@@ -1,31 +1,25 @@
-from argparse import Namespace
-
-from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.InvertedRegions import mark_dark_world_regions
from worlds.alttp.ItemPool import difficulties
-from worlds.alttp.Items import ItemFactory
+from worlds.alttp.Items import item_factory
from test.TestBase import TestBase
-from worlds import AutoWorld
+from worlds.alttp.Options import GlitchesRequired
+from worlds.alttp.test import LTTPTestBase
+
-class TestVanilla(TestBase):
+class TestVanilla(TestBase, LTTPTestBase):
def setUp(self):
- self.multiworld = MultiWorld(1)
- self.multiworld.set_seed(None)
- args = Namespace()
- for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
- setattr(args, name, {1: option.from_any(option.default)})
- self.multiworld.set_options(args)
- self.multiworld.set_default_common_options()
- self.multiworld.logic[1] = "noglitches"
- self.multiworld.difficulty_requirements[1] = difficulties['normal']
+ self.world_setup()
+ self.multiworld.glitches_required[1] = GlitchesRequired.from_any("no_glitches")
+ self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
+ self.multiworld.bombless_start[1].value = True
+ self.multiworld.shuffle_capacity_upgrades[1].value = 2
self.multiworld.worlds[1].er_seed = 0
self.multiworld.worlds[1].create_regions()
self.multiworld.worlds[1].create_items()
- self.multiworld.required_medallions[1] = ['Ether', 'Quake']
self.multiworld.itempool.extend(get_dungeon_item_pool(self.multiworld))
- self.multiworld.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
+ self.multiworld.itempool.extend(item_factory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], self.world))
self.multiworld.get_location('Agahnim 1', 1).item = None
self.multiworld.get_location('Agahnim 2', 1).item = None
mark_dark_world_regions(self.multiworld, 1)
- self.multiworld.worlds[1].set_rules()
\ No newline at end of file
+ self.world.set_rules()
diff --git a/worlds/apsudoku/__init__.py b/worlds/apsudoku/__init__.py
new file mode 100644
index 000000000000..c6bd02bdc262
--- /dev/null
+++ b/worlds/apsudoku/__init__.py
@@ -0,0 +1,34 @@
+from typing import Dict
+
+from BaseClasses import Tutorial
+from ..AutoWorld import WebWorld, World
+
+class AP_SudokuWebWorld(WebWorld):
+ options_page = "games/Sudoku/info/en"
+ theme = 'partyTime'
+
+ setup_en = Tutorial(
+ tutorial_name='Setup Guide',
+ description='A guide to playing APSudoku',
+ language='English',
+ file_name='setup_en.md',
+ link='setup/en',
+ authors=['EmilyV']
+ )
+
+ tutorials = [setup_en]
+
+class AP_SudokuWorld(World):
+ """
+ Play a little Sudoku while you're in BK mode to maybe get some useful hints
+ """
+ game = "Sudoku"
+ web = AP_SudokuWebWorld()
+
+ item_name_to_id: Dict[str, int] = {}
+ location_name_to_id: Dict[str, int] = {}
+
+ @classmethod
+ def stage_assert_generate(cls, multiworld):
+ raise Exception("APSudoku cannot be used for generating worlds, the client can instead connect to any slot from any world")
+
diff --git a/worlds/apsudoku/docs/en_Sudoku.md b/worlds/apsudoku/docs/en_Sudoku.md
new file mode 100644
index 000000000000..e81f773e0291
--- /dev/null
+++ b/worlds/apsudoku/docs/en_Sudoku.md
@@ -0,0 +1,13 @@
+# APSudoku
+
+## Hint Games
+
+HintGames do not need to be added at the start of a seed, and do not create a 'slot'- instead, you connect the HintGame client to a different game's slot. By playing a HintGame, you can earn hints for the connected slot.
+
+## What is this game?
+
+Play Sudoku puzzles of varying difficulties, earning a hint for each puzzle correctly solved. Harder puzzles are more likely to grant a hint towards a Progression item, though otherwise what hint is granted is random.
+
+## Where is the options page?
+
+There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld.
diff --git a/worlds/apsudoku/docs/setup_en.md b/worlds/apsudoku/docs/setup_en.md
new file mode 100644
index 000000000000..cf2c755bd837
--- /dev/null
+++ b/worlds/apsudoku/docs/setup_en.md
@@ -0,0 +1,37 @@
+# APSudoku Setup Guide
+
+## Required Software
+- [APSudoku](https://github.com/EmilyV99/APSudoku)
+- Windows (most tested on Win10)
+- Other platforms might be able to build from source themselves; and may be included in the future.
+
+## General Concept
+
+This is a HintGame client, which can connect to any multiworld slot, allowing you to play Sudoku to unlock random hints for that slot's locations.
+
+Does not need to be added at the start of a seed, as it does not create any slots of its own, nor does it have any YAML files.
+
+## Installation Procedures
+
+Go to the latest release from the [APSudoku Releases page](https://github.com/EmilyV99/APSudoku/releases). Download and extract the `APSudoku.zip` file.
+
+## Joining a MultiWorld Game
+
+1. Run APSudoku.exe
+2. Under the 'Archipelago' tab at the top-right:
+ - Enter the server url & port number
+ - Enter the name of the slot you wish to connect to
+ - Enter the room password (optional)
+ - Select DeathLink related settings (optional)
+ - Press connect
+3. Go back to the 'Sudoku' tab
+ - Click the various '?' buttons for information on how to play / control
+4. Choose puzzle difficulty
+5. Try to solve the Sudoku. Click 'Check' when done.
+
+## DeathLink Support
+
+If 'DeathLink' is enabled when you click 'Connect':
+- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or quit a puzzle without solving it (including disconnecting).
+- Life count customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
+- On receiving a DeathLink from another player, your puzzle resets.
diff --git a/worlds/aquaria/Items.py b/worlds/aquaria/Items.py
new file mode 100644
index 000000000000..34557d95d00d
--- /dev/null
+++ b/worlds/aquaria/Items.py
@@ -0,0 +1,214 @@
+"""
+Author: Louis M
+Date: Fri, 15 Mar 2024 18:41:40 +0000
+Description: Manage items in the Aquaria game multiworld randomizer
+"""
+
+from typing import Optional
+from enum import Enum
+from BaseClasses import Item, ItemClassification
+
+
+class ItemType(Enum):
+ """
+ Used to indicate to the multi-world if an item is useful or not
+ """
+ NORMAL = 0
+ PROGRESSION = 1
+ JUNK = 2
+
+
+class ItemGroup(Enum):
+ """
+ Used to group items
+ """
+ COLLECTIBLE = 0
+ INGREDIENT = 1
+ RECIPE = 2
+ HEALTH = 3
+ UTILITY = 4
+ SONG = 5
+ TURTLE = 6
+
+
+class AquariaItem(Item):
+ """
+ A single item in the Aquaria game.
+ """
+ game: str = "Aquaria"
+ """The name of the game"""
+
+ def __init__(self, name: str, classification: ItemClassification,
+ code: Optional[int], player: int):
+ """
+ Initialisation of the Item
+ :param name: The name of the item
+ :param classification: If the item is useful or not
+ :param code: The ID of the item (if None, it is an event)
+ :param player: The ID of the player in the multiworld
+ """
+ super().__init__(name, classification, code, player)
+
+
+class ItemData:
+ """
+ Data of an item.
+ """
+ id: int
+ count: int
+ type: ItemType
+ group: ItemGroup
+
+ def __init__(self, id: int, count: int, type: ItemType, group: ItemGroup):
+ """
+ Initialisation of the item data
+ @param id: The item ID
+ @param count: the number of items in the pool
+ @param type: the importance type of the item
+ @param group: the usage of the item in the game
+ """
+ self.id = id
+ self.count = count
+ self.type = type
+ self.group = group
+
+
+"""Information data for every (not event) item."""
+item_table = {
+ # name: ID, Nb, Item Type, Item Group
+ "Anemone": ItemData(698000, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_anemone
+ "Arnassi Statue": ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue
+ "Big Seed": ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed
+ "Glowing Seed": ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed
+ "Black Pearl": ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl
+ "Baby Blaster": ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster
+ "Crab Armor": ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume
+ "Baby Dumbo": ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo
+ "Tooth": ItemData(698008, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_boss
+ "Energy Statue": ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue
+ "Krotite Armor": ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple
+ "Golden Starfish": ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star
+ "Golden Gear": ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear
+ "Jelly Beacon": ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon
+ "Jelly Costume": ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume
+ "Jelly Plant": ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant
+ "Mithalas Doll": ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll
+ "Mithalan Dress": ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume
+ "Mithalas Banner": ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner
+ "Mithalas Pot": ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot
+ "Mutant Costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume
+ "Baby Nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus
+ "Baby Piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha
+ "Arnassi Armor": ItemData(698023, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_seahorse_costume
+ "Seed Bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag
+ "King's Skull": ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull
+ "Song Plant Spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed
+ "Stone Head": ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head
+ "Sun Key": ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key
+ "Girl Costume": ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume
+ "Odd Container": ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest
+ "Trident": ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head
+ "Turtle Egg": ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg
+ "Jelly Egg": ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed
+ "Urchin Costume": ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
+ "Baby Walker": ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker
+ "Vedha's Cure-All-All": ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All
+ "Zuuna's perogi": ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi
+ "Arcane poultice": ItemData(698038, 7, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_arcanepoultice
+ "Berry ice cream": ItemData(698039, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_berryicecream
+ "Buttery sea loaf": ItemData(698040, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_butterysealoaf
+ "Cold borscht": ItemData(698041, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldborscht
+ "Cold soup": ItemData(698042, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldsoup
+ "Crab cake": ItemData(698043, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_crabcake
+ "Divine soup": ItemData(698044, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_divinesoup
+ "Dumbo ice cream": ItemData(698045, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_dumboicecream
+ "Fish oil": ItemData(698046, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
+ "Glowing egg": ItemData(698047, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
+ "Hand roll": ItemData(698048, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_handroll
+ "Healing poultice": ItemData(698049, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
+ "Hearty soup": ItemData(698050, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_heartysoup
+ "Hot borscht": ItemData(698051, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_hotborscht
+ "Hot soup": ItemData(698052, 3, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
+ "Ice cream": ItemData(698053, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_icecream
+ "Leadership roll": ItemData(698054, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
+ "Leaf poultice": ItemData(698055, 5, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_leafpoultice
+ "Leeching poultice": ItemData(698056, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leechingpoultice
+ "Legendary cake": ItemData(698057, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_legendarycake
+ "Loaf of life": ItemData(698058, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_loafoflife
+ "Long life soup": ItemData(698059, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_longlifesoup
+ "Magic soup": ItemData(698060, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_magicsoup
+ "Mushroom x 2": ItemData(698061, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_mushroom
+ "Perogi": ItemData(698062, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_perogi
+ "Plant leaf": ItemData(698063, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
+ "Plump perogi": ItemData(698064, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_plumpperogi
+ "Poison loaf": ItemData(698065, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonloaf
+ "Poison soup": ItemData(698066, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonsoup
+ "Rainbow mushroom": ItemData(698067, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_rainbowmushroom
+ "Rainbow soup": ItemData(698068, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_rainbowsoup
+ "Red berry": ItemData(698069, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redberry
+ "Red bulb x 2": ItemData(698070, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redbulb
+ "Rotten cake": ItemData(698071, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottencake
+ "Rotten loaf x 8": ItemData(698072, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottenloaf
+ "Rotten meat": ItemData(698073, 5, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
+ "Royal soup": ItemData(698074, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_royalsoup
+ "Sea cake": ItemData(698075, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_seacake
+ "Sea loaf": ItemData(698076, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
+ "Shark fin soup": ItemData(698077, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sharkfinsoup
+ "Sight poultice": ItemData(698078, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sightpoultice
+ "Small bone x 2": ItemData(698079, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
+ "Small egg": ItemData(698080, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
+ "Small tentacle x 2": ItemData(698081, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smalltentacle
+ "Special bulb": ItemData(698082, 5, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_specialbulb
+ "Special cake": ItemData(698083, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_specialcake
+ "Spicy meat x 2": ItemData(698084, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_spicymeat
+ "Spicy roll": ItemData(698085, 11, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicyroll
+ "Spicy soup": ItemData(698086, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicysoup
+ "Spider roll": ItemData(698087, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spiderroll
+ "Swamp cake": ItemData(698088, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_swampcake
+ "Tasty cake": ItemData(698089, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastycake
+ "Tasty roll": ItemData(698090, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastyroll
+ "Tough cake": ItemData(698091, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_toughcake
+ "Turtle soup": ItemData(698092, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_turtlesoup
+ "Vedha sea crisp": ItemData(698093, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_vedhaseacrisp
+ "Veggie cake": ItemData(698094, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiecake
+ "Veggie ice cream": ItemData(698095, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggieicecream
+ "Veggie soup": ItemData(698096, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiesoup
+ "Volcano roll": ItemData(698097, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_volcanoroll
+ "Health upgrade": ItemData(698098, 5, ItemType.NORMAL, ItemGroup.HEALTH), # upgrade_health_?
+ "Wok": ItemData(698099, 1, ItemType.NORMAL, ItemGroup.UTILITY), # upgrade_wok
+ "Eel oil x 2": ItemData(698100, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_eeloil
+ "Fish meat x 2": ItemData(698101, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishmeat
+ "Fish oil x 3": ItemData(698102, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
+ "Glowing egg x 2": ItemData(698103, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
+ "Healing poultice x 2": ItemData(698104, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
+ "Hot soup x 2": ItemData(698105, 1, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
+ "Leadership roll x 2": ItemData(698106, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
+ "Leaf poultice x 3": ItemData(698107, 2, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_leafpoultice
+ "Plant leaf x 2": ItemData(698108, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
+ "Plant leaf x 3": ItemData(698109, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
+ "Rotten meat x 2": ItemData(698110, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
+ "Rotten meat x 8": ItemData(698111, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
+ "Sea loaf x 2": ItemData(698112, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
+ "Small bone x 3": ItemData(698113, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
+ "Small egg x 2": ItemData(698114, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
+ "Li and Li song": ItemData(698115, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_li
+ "Shield song": ItemData(698116, 1, ItemType.NORMAL, ItemGroup.SONG), # song_shield
+ "Beast form": ItemData(698117, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_beast
+ "Sun form": ItemData(698118, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_sun
+ "Nature form": ItemData(698119, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_nature
+ "Energy form": ItemData(698120, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_energy
+ "Bind song": ItemData(698121, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_bind
+ "Fish form": ItemData(698122, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_fish
+ "Spirit form": ItemData(698123, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_spirit
+ "Dual form": ItemData(698124, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_dual
+ "Transturtle Veil top left": ItemData(698125, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil01
+ "Transturtle Veil top right": ItemData(698126, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil02
+ "Transturtle Open Water top right": ItemData(698127, 1, ItemType.PROGRESSION,
+ ItemGroup.TURTLE), # transport_openwater03
+ "Transturtle Forest bottom left": ItemData(698128, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest04
+ "Transturtle Home Water": ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea
+ "Transturtle Abyss right": ItemData(698130, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_abyss03
+ "Transturtle Final Boss": ItemData(698131, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_finalboss
+ "Transturtle Simon Says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05
+ "Transturtle Arnassi Ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse
+}
diff --git a/worlds/aquaria/Locations.py b/worlds/aquaria/Locations.py
new file mode 100644
index 000000000000..2eb9d1e9a29d
--- /dev/null
+++ b/worlds/aquaria/Locations.py
@@ -0,0 +1,575 @@
+"""
+Author: Louis M
+Date: Fri, 15 Mar 2024 18:41:40 +0000
+Description: Manage locations in the Aquaria game multiworld randomizer
+"""
+
+from BaseClasses import Location
+
+
+class AquariaLocation(Location):
+ """
+ A location in the game.
+ """
+ game: str = "Aquaria"
+ """The name of the game"""
+
+ def __init__(self, player: int, name="", code=None, parent=None) -> None:
+ """
+ Initialisation of the object
+ :param player: the ID of the player
+ :param name: the name of the location
+ :param code: the ID (or address) of the location (Event if None)
+ :param parent: the Region that this location belongs to
+ """
+ super(AquariaLocation, self).__init__(player, name, code, parent)
+ self.event = code is None
+
+
+class AquariaLocations:
+
+ locations_verse_cave_r = {
+ "Verse Cave, bulb in the skeleton room": 698107,
+ "Verse Cave, bulb in the path right of the skeleton room": 698108,
+ "Verse Cave right area, Big Seed": 698175,
+ }
+
+ locations_verse_cave_l = {
+ "Verse Cave, the Naija hint about the shield ability": 698200,
+ "Verse Cave left area, bulb in the center part": 698021,
+ "Verse Cave left area, bulb in the right part": 698022,
+ "Verse Cave left area, bulb under the rock at the end of the path": 698023,
+ }
+
+ locations_home_water = {
+ "Home Water, bulb below the grouper fish": 698058,
+ "Home Water, bulb in the path below Nautilus Prime": 698059,
+ "Home Water, bulb in the little room above the grouper fish": 698060,
+ "Home Water, bulb in the end of the left path from the Verse Cave": 698061,
+ "Home Water, bulb in the top left path": 698062,
+ "Home Water, bulb in the bottom left room": 698063,
+ "Home Water, bulb close to Naija's Home": 698064,
+ "Home Water, bulb under the rock in the left path from the Verse Cave": 698065,
+ }
+
+ locations_home_water_nautilus = {
+ "Home Water, Nautilus Egg": 698194,
+ }
+
+ locations_home_water_transturtle = {
+ "Home Water, Transturtle": 698213,
+ }
+
+ locations_naija_home = {
+ "Naija's Home, bulb after the energy door": 698119,
+ "Naija's Home, bulb under the rock at the right of the main path": 698120,
+ }
+
+ locations_song_cave = {
+ "Song Cave, Erulian spirit": 698206,
+ "Song Cave, bulb in the top left part": 698071,
+ "Song Cave, bulb in the big anemone room": 698072,
+ "Song Cave, bulb in the path to the singing statues": 698073,
+ "Song Cave, bulb under the rock in the path to the singing statues": 698074,
+ "Song Cave, bulb under the rock close to the song door": 698075,
+ "Song Cave, Verse Egg": 698160,
+ "Song Cave, Jelly Beacon": 698178,
+ "Song Cave, Anemone Seed": 698162,
+ }
+
+ locations_energy_temple_1 = {
+ "Energy Temple first area, beating the Energy Statue": 698205,
+ "Energy Temple first area, bulb in the bottom room blocked by a rock": 698027,
+ }
+
+ locations_energy_temple_idol = {
+ "Energy Temple first area, Energy Idol": 698170,
+ }
+
+ locations_energy_temple_2 = {
+ "Energy Temple second area, bulb under the rock": 698028,
+ }
+
+ locations_energy_temple_altar = {
+ "Energy Temple bottom entrance, Krotite Armor": 698163,
+ }
+
+ locations_energy_temple_3 = {
+ "Energy Temple third area, bulb in the bottom path": 698029,
+ }
+
+ locations_energy_temple_boss = {
+ "Energy Temple boss area, Fallen God Tooth": 698169,
+ }
+
+ locations_energy_temple_blaster_room = {
+ "Energy Temple blaster room, Blaster Egg": 698195,
+ }
+
+ locations_openwater_tl = {
+ "Open Water top left area, bulb under the rock in the right path": 698001,
+ "Open Water top left area, bulb under the rock in the left path": 698002,
+ "Open Water top left area, bulb to the right of the save crystal": 698003,
+ }
+
+ locations_openwater_tr = {
+ "Open Water top right area, bulb in the small path before Mithalas": 698004,
+ "Open Water top right area, bulb in the path from the left entrance": 698005,
+ "Open Water top right area, bulb in the clearing close to the bottom exit": 698006,
+ "Open Water top right area, bulb in the big clearing close to the save crystal": 698007,
+ "Open Water top right area, bulb in the big clearing to the top exit": 698008,
+ "Open Water top right area, first urn in the Mithalas exit": 698148,
+ "Open Water top right area, second urn in the Mithalas exit": 698149,
+ "Open Water top right area, third urn in the Mithalas exit": 698150,
+ }
+
+ locations_openwater_tr_turtle = {
+ "Open Water top right area, bulb in the turtle room": 698009,
+ "Open Water top right area, Transturtle": 698211,
+ }
+
+ locations_openwater_bl = {
+ "Open Water bottom left area, bulb behind the chomper fish": 698011,
+ "Open Water bottom left area, bulb inside the lowest fish pass": 698010,
+ }
+
+ locations_skeleton_path = {
+ "Open Water skeleton path, bulb close to the right exit": 698012,
+ "Open Water skeleton path, bulb behind the chomper fish": 698013,
+ }
+
+ locations_skeleton_path_sc = {
+ "Open Water skeleton path, King Skull": 698177,
+ }
+
+ locations_arnassi = {
+ "Arnassi Ruins, bulb in the right part": 698014,
+ "Arnassi Ruins, bulb in the left part": 698015,
+ "Arnassi Ruins, bulb in the center part": 698016,
+ "Arnassi Ruins, Song Plant Spore": 698179,
+ "Arnassi Ruins, Arnassi Armor": 698191,
+ }
+
+ locations_arnassi_path = {
+ "Arnassi Ruins, Arnassi Statue": 698164,
+ "Arnassi Ruins, Transturtle": 698217,
+ }
+
+ locations_arnassi_crab_boss = {
+ "Arnassi Ruins, Crab Armor": 698187,
+ }
+
+ locations_simon = {
+ "Simon Says area, beating Simon Says": 698156,
+ "Simon Says area, Transturtle": 698216,
+ }
+
+ locations_mithalas_city = {
+ "Mithalas City, first bulb in the left city part": 698030,
+ "Mithalas City, second bulb in the left city part": 698035,
+ "Mithalas City, bulb in the right part": 698031,
+ "Mithalas City, bulb at the top of the city": 698033,
+ "Mithalas City, first bulb in a broken home": 698034,
+ "Mithalas City, second bulb in a broken home": 698041,
+ "Mithalas City, bulb in the bottom left part": 698037,
+ "Mithalas City, first bulb in one of the homes": 698038,
+ "Mithalas City, second bulb in one of the homes": 698039,
+ "Mithalas City, first urn in one of the homes": 698123,
+ "Mithalas City, second urn in one of the homes": 698124,
+ "Mithalas City, first urn in the city reserve": 698125,
+ "Mithalas City, second urn in the city reserve": 698126,
+ "Mithalas City, third urn in the city reserve": 698127,
+ }
+
+ locations_mithalas_city_top_path = {
+ "Mithalas City, first bulb at the end of the top path": 698032,
+ "Mithalas City, second bulb at the end of the top path": 698040,
+ "Mithalas City, bulb in the top path": 698036,
+ "Mithalas City, Mithalas Pot": 698174,
+ "Mithalas City, urn in the Castle flower tube entrance": 698128,
+ }
+
+ locations_mithalas_city_fishpass = {
+ "Mithalas City, Doll": 698173,
+ "Mithalas City, urn inside a home fish pass": 698129,
+ }
+
+ locations_cathedral_l = {
+ "Mithalas City Castle, bulb in the flesh hole": 698042,
+ "Mithalas City Castle, Blue Banner": 698165,
+ "Mithalas City Castle, urn in the bedroom": 698130,
+ "Mithalas City Castle, first urn of the single lamp path": 698131,
+ "Mithalas City Castle, second urn of the single lamp path": 698132,
+ "Mithalas City Castle, urn in the bottom room": 698133,
+ "Mithalas City Castle, first urn on the entrance path": 698134,
+ "Mithalas City Castle, second urn on the entrance path": 698135,
+ }
+
+ locations_cathedral_l_tube = {
+ "Mithalas City Castle, beating the Priests": 698208,
+ }
+
+ locations_cathedral_l_sc = {
+ "Mithalas City Castle, Trident Head": 698183,
+ }
+
+ locations_cathedral_r = {
+ "Mithalas Cathedral, first urn in the top right room": 698136,
+ "Mithalas Cathedral, second urn in the top right room": 698137,
+ "Mithalas Cathedral, third urn in the top right room": 698138,
+ "Mithalas Cathedral, urn in the flesh room with fleas": 698139,
+ "Mithalas Cathedral, first urn in the bottom right path": 698140,
+ "Mithalas Cathedral, second urn in the bottom right path": 698141,
+ "Mithalas Cathedral, urn behind the flesh vein": 698142,
+ "Mithalas Cathedral, urn in the top left eyes boss room": 698143,
+ "Mithalas Cathedral, first urn in the path behind the flesh vein": 698144,
+ "Mithalas Cathedral, second urn in the path behind the flesh vein": 698145,
+ "Mithalas Cathedral, third urn in the path behind the flesh vein": 698146,
+ "Mithalas Cathedral, fourth urn in the top right room": 698147,
+ "Mithalas Cathedral, Mithalan Dress": 698189,
+ "Mithalas Cathedral, urn below the left entrance": 698198,
+ }
+
+ locations_cathedral_underground = {
+ "Cathedral Underground, bulb in the center part": 698113,
+ "Cathedral Underground, first bulb in the top left part": 698114,
+ "Cathedral Underground, second bulb in the top left part": 698115,
+ "Cathedral Underground, third bulb in the top left part": 698116,
+ "Cathedral Underground, bulb close to the save crystal": 698117,
+ "Cathedral Underground, bulb in the bottom right path": 698118,
+ }
+
+ locations_cathedral_boss = {
+ "Mithalas boss area, beating Mithalan God": 698202,
+ }
+
+ locations_forest_tl = {
+ "Kelp Forest top left area, bulb in the bottom left clearing": 698044,
+ "Kelp Forest top left area, bulb in the path down from the top left clearing": 698045,
+ "Kelp Forest top left area, bulb in the top left clearing": 698046,
+ "Kelp Forest top left area, Jelly Egg": 698185,
+ }
+
+ locations_forest_tl_fp = {
+ "Kelp Forest top left area, bulb close to the Verse Egg": 698047,
+ "Kelp Forest top left area, Verse Egg": 698158,
+ }
+
+ locations_forest_tr = {
+ "Kelp Forest top right area, bulb under the rock in the right path": 698048,
+ "Kelp Forest top right area, bulb at the left of the center clearing": 698049,
+ "Kelp Forest top right area, bulb in the left path's big room": 698051,
+ "Kelp Forest top right area, bulb in the left path's small room": 698052,
+ "Kelp Forest top right area, bulb at the top of the center clearing": 698053,
+ "Kelp Forest top right area, Black Pearl": 698167,
+ }
+
+ locations_forest_tr_fp = {
+ "Kelp Forest top right area, bulb in the top fish pass": 698050,
+ }
+
+ locations_forest_bl = {
+ "Kelp Forest bottom left area, bulb close to the spirit crystals": 698054,
+ "Kelp Forest bottom left area, Walker Baby": 698186,
+ "Kelp Forest bottom left area, Transturtle": 698212,
+ }
+
+ locations_forest_br = {
+ "Kelp Forest bottom right area, Odd Container": 698168,
+ }
+
+ locations_forest_boss = {
+ "Kelp Forest boss area, beating Drunian God": 698204,
+ }
+
+ locations_forest_boss_entrance = {
+ "Kelp Forest boss room, bulb at the bottom of the area": 698055,
+ }
+
+ locations_forest_fish_cave = {
+ "Kelp Forest bottom left area, Fish Cave puzzle": 698207,
+ }
+
+ locations_forest_sprite_cave = {
+ "Kelp Forest sprite cave, bulb inside the fish pass": 698056,
+ }
+
+ locations_forest_sprite_cave_tube = {
+ "Kelp Forest sprite cave, bulb in the second room": 698057,
+ "Kelp Forest sprite cave, Seed Bag": 698176,
+ }
+
+ locations_mermog_cave = {
+ "Mermog cave, bulb in the left part of the cave": 698121,
+ }
+
+ locations_mermog_boss = {
+ "Mermog cave, Piranha Egg": 698197,
+ }
+
+ locations_veil_tl = {
+ "The Veil top left area, In Li's cave": 698199,
+ "The Veil top left area, bulb under the rock in the top right path": 698078,
+ "The Veil top left area, bulb hidden behind the blocking rock": 698076,
+ "The Veil top left area, Transturtle": 698209,
+ }
+
+ locations_veil_tl_fp = {
+ "The Veil top left area, bulb inside the fish pass": 698077,
+ }
+
+ locations_turtle_cave = {
+ "Turtle cave, Turtle Egg": 698184,
+ }
+
+ locations_turtle_cave_bubble = {
+ "Turtle cave, bulb in Bubble Cliff": 698000,
+ "Turtle cave, Urchin Costume": 698193,
+ }
+
+ locations_veil_tr_r = {
+ "The Veil top right area, bulb in the middle of the wall jump cliff": 698079,
+ "The Veil top right area, Golden Starfish": 698180,
+ }
+
+ locations_veil_tr_l = {
+ "The Veil top right area, bulb at the top of the waterfall": 698080,
+ "The Veil top right area, Transturtle": 698210,
+ }
+
+ locations_veil_bl = {
+ "The Veil bottom area, bulb in the left path": 698082,
+ }
+
+ locations_veil_b_sc = {
+ "The Veil bottom area, bulb in the spirit path": 698081,
+ }
+
+ locations_veil_bl_fp = {
+ "The Veil bottom area, Verse Egg": 698157,
+ }
+
+ locations_veil_br = {
+ "The Veil bottom area, Stone Head": 698181,
+ }
+
+ locations_octo_cave_t = {
+ "Octopus Cave, Dumbo Egg": 698196,
+ }
+
+ locations_octo_cave_b = {
+ "Octopus Cave, bulb in the path below the Octopus Cave path": 698122,
+ }
+
+ locations_sun_temple_l = {
+ "Sun Temple, bulb in the top left part": 698094,
+ "Sun Temple, bulb in the top right part": 698095,
+ "Sun Temple, bulb at the top of the high dark room": 698096,
+ "Sun Temple, Golden Gear": 698171,
+ }
+
+ locations_sun_temple_r = {
+ "Sun Temple, first bulb of the temple": 698091,
+ "Sun Temple, bulb on the left part": 698092,
+ "Sun Temple, bulb in the hidden room of the right part": 698093,
+ "Sun Temple, Sun Key": 698182,
+ }
+
+ locations_sun_temple_boss_path = {
+ "Sun Worm path, first path bulb": 698017,
+ "Sun Worm path, second path bulb": 698018,
+ "Sun Worm path, first cliff bulb": 698019,
+ "Sun Worm path, second cliff bulb": 698020,
+ }
+
+ locations_sun_temple_boss = {
+ "Sun Temple boss area, beating Sun God": 698203,
+ }
+
+ locations_abyss_l = {
+ "Abyss left area, bulb in hidden path room": 698024,
+ "Abyss left area, bulb in the right part": 698025,
+ "Abyss left area, Glowing Seed": 698166,
+ "Abyss left area, Glowing Plant": 698172,
+ }
+
+ locations_abyss_lb = {
+ "Abyss left area, bulb in the bottom fish pass": 698026,
+ }
+
+ locations_abyss_r = {
+ "Abyss right area, bulb behind the rock in the whale room": 698109,
+ "Abyss right area, bulb in the middle path": 698110,
+ "Abyss right area, bulb behind the rock in the middle path": 698111,
+ "Abyss right area, bulb in the left green room": 698112,
+ "Abyss right area, Transturtle": 698214,
+ }
+
+ locations_ice_cave = {
+ "Ice Cave, bulb in the room to the right": 698083,
+ "Ice Cave, first bulb in the top exit room": 698084,
+ "Ice Cave, second bulb in the top exit room": 698085,
+ "Ice Cave, third bulb in the top exit room": 698086,
+ "Ice Cave, bulb in the left room": 698087,
+ }
+
+ locations_bubble_cave = {
+ "Bubble Cave, bulb in the left cave wall": 698089,
+ "Bubble Cave, bulb in the right cave wall (behind the ice crystal)": 698090,
+ }
+
+ locations_bubble_cave_boss = {
+ "Bubble Cave, Verse Egg": 698161,
+ }
+
+ locations_king_jellyfish_cave = {
+ "King Jellyfish Cave, bulb in the right path from King Jelly": 698088,
+ "King Jellyfish Cave, Jellyfish Costume": 698188,
+ }
+
+ locations_whale = {
+ "The Whale, Verse Egg": 698159,
+ }
+
+ locations_sunken_city_r = {
+ "Sunken City right area, crate close to the save crystal": 698154,
+ "Sunken City right area, crate in the left bottom room": 698155,
+ }
+
+ locations_sunken_city_l = {
+ "Sunken City left area, crate in the little pipe room": 698151,
+ "Sunken City left area, crate close to the save crystal": 698152,
+ "Sunken City left area, crate before the bedroom": 698153,
+ }
+
+ locations_sunken_city_l_bedroom = {
+ "Sunken City left area, Girl Costume": 698192,
+ }
+
+ locations_sunken_city_boss = {
+ "Sunken City, bulb on top of the boss area": 698043,
+ }
+
+ locations_body_c = {
+ "The Body center area, breaking Li's cage": 698201,
+ "The Body center area, bulb on the main path blocking tube": 698097,
+ }
+
+ locations_body_l = {
+ "The Body left area, first bulb in the top face room": 698066,
+ "The Body left area, second bulb in the top face room": 698069,
+ "The Body left area, bulb below the water stream": 698067,
+ "The Body left area, bulb in the top path to the top face room": 698068,
+ "The Body left area, bulb in the bottom face room": 698070,
+ }
+
+ locations_body_rt = {
+ "The Body right area, bulb in the top face room": 698100,
+ }
+
+ locations_body_rb = {
+ "The Body right area, bulb in the top path to the bottom face room": 698098,
+ "The Body right area, bulb in the bottom face room": 698099,
+ }
+
+ locations_body_b = {
+ "The Body bottom area, bulb in the Jelly Zap room": 698101,
+ "The Body bottom area, bulb in the nautilus room": 698102,
+ "The Body bottom area, Mutant Costume": 698190,
+ }
+
+ locations_final_boss_tube = {
+ "Final Boss area, first bulb in the turtle room": 698103,
+ "Final Boss area, second bulb in the turtle room": 698104,
+ "Final Boss area, third bulb in the turtle room": 698105,
+ "Final Boss area, Transturtle": 698215,
+ }
+
+ locations_final_boss = {
+ "Final Boss area, bulb in the boss third form room": 698106,
+ }
+
+
+location_table = {
+ **AquariaLocations.locations_openwater_tl,
+ **AquariaLocations.locations_openwater_tr,
+ **AquariaLocations.locations_openwater_tr_turtle,
+ **AquariaLocations.locations_openwater_bl,
+ **AquariaLocations.locations_skeleton_path,
+ **AquariaLocations.locations_skeleton_path_sc,
+ **AquariaLocations.locations_arnassi,
+ **AquariaLocations.locations_arnassi_path,
+ **AquariaLocations.locations_arnassi_crab_boss,
+ **AquariaLocations.locations_sun_temple_l,
+ **AquariaLocations.locations_sun_temple_r,
+ **AquariaLocations.locations_sun_temple_boss_path,
+ **AquariaLocations.locations_sun_temple_boss,
+ **AquariaLocations.locations_verse_cave_r,
+ **AquariaLocations.locations_verse_cave_l,
+ **AquariaLocations.locations_abyss_l,
+ **AquariaLocations.locations_abyss_lb,
+ **AquariaLocations.locations_abyss_r,
+ **AquariaLocations.locations_energy_temple_1,
+ **AquariaLocations.locations_energy_temple_2,
+ **AquariaLocations.locations_energy_temple_3,
+ **AquariaLocations.locations_energy_temple_boss,
+ **AquariaLocations.locations_energy_temple_blaster_room,
+ **AquariaLocations.locations_energy_temple_altar,
+ **AquariaLocations.locations_energy_temple_idol,
+ **AquariaLocations.locations_mithalas_city,
+ **AquariaLocations.locations_mithalas_city_top_path,
+ **AquariaLocations.locations_mithalas_city_fishpass,
+ **AquariaLocations.locations_cathedral_l,
+ **AquariaLocations.locations_cathedral_l_tube,
+ **AquariaLocations.locations_cathedral_l_sc,
+ **AquariaLocations.locations_cathedral_r,
+ **AquariaLocations.locations_cathedral_underground,
+ **AquariaLocations.locations_cathedral_boss,
+ **AquariaLocations.locations_forest_tl,
+ **AquariaLocations.locations_forest_tl_fp,
+ **AquariaLocations.locations_forest_tr,
+ **AquariaLocations.locations_forest_tr_fp,
+ **AquariaLocations.locations_forest_bl,
+ **AquariaLocations.locations_forest_br,
+ **AquariaLocations.locations_forest_boss,
+ **AquariaLocations.locations_forest_boss_entrance,
+ **AquariaLocations.locations_forest_sprite_cave,
+ **AquariaLocations.locations_forest_sprite_cave_tube,
+ **AquariaLocations.locations_forest_fish_cave,
+ **AquariaLocations.locations_home_water,
+ **AquariaLocations.locations_home_water_transturtle,
+ **AquariaLocations.locations_home_water_nautilus,
+ **AquariaLocations.locations_body_l,
+ **AquariaLocations.locations_body_rt,
+ **AquariaLocations.locations_body_rb,
+ **AquariaLocations.locations_body_c,
+ **AquariaLocations.locations_body_b,
+ **AquariaLocations.locations_final_boss_tube,
+ **AquariaLocations.locations_final_boss,
+ **AquariaLocations.locations_song_cave,
+ **AquariaLocations.locations_veil_tl,
+ **AquariaLocations.locations_veil_tl_fp,
+ **AquariaLocations.locations_turtle_cave,
+ **AquariaLocations.locations_turtle_cave_bubble,
+ **AquariaLocations.locations_veil_tr_r,
+ **AquariaLocations.locations_veil_tr_l,
+ **AquariaLocations.locations_veil_bl,
+ **AquariaLocations.locations_veil_b_sc,
+ **AquariaLocations.locations_veil_bl_fp,
+ **AquariaLocations.locations_veil_br,
+ **AquariaLocations.locations_ice_cave,
+ **AquariaLocations.locations_king_jellyfish_cave,
+ **AquariaLocations.locations_bubble_cave,
+ **AquariaLocations.locations_bubble_cave_boss,
+ **AquariaLocations.locations_naija_home,
+ **AquariaLocations.locations_mermog_cave,
+ **AquariaLocations.locations_mermog_boss,
+ **AquariaLocations.locations_octo_cave_t,
+ **AquariaLocations.locations_octo_cave_b,
+ **AquariaLocations.locations_sunken_city_l,
+ **AquariaLocations.locations_sunken_city_r,
+ **AquariaLocations.locations_sunken_city_boss,
+ **AquariaLocations.locations_sunken_city_l_bedroom,
+ **AquariaLocations.locations_simon,
+ **AquariaLocations.locations_whale,
+}
diff --git a/worlds/aquaria/Options.py b/worlds/aquaria/Options.py
new file mode 100644
index 000000000000..8c0142debff0
--- /dev/null
+++ b/worlds/aquaria/Options.py
@@ -0,0 +1,153 @@
+"""
+Author: Louis M
+Date: Fri, 15 Mar 2024 18:41:40 +0000
+Description: Manage options in the Aquaria game multiworld randomizer
+"""
+
+from dataclasses import dataclass
+from Options import Toggle, Choice, Range, PerGameCommonOptions, DefaultOnToggle, StartInventoryPool
+
+
+class IngredientRandomizer(Choice):
+ """
+ Select if the simple ingredients (that do not have a recipe) should be randomized.
+ If "Common Ingredients" is selected, the randomization will exclude the "Red Bulb", "Special Bulb" and "Rukh Egg".
+ """
+ display_name = "Randomize Ingredients"
+ option_off = 0
+ option_common_ingredients = 1
+ option_all_ingredients = 2
+ default = 0
+
+
+class DishRandomizer(Toggle):
+ """Randomize the drop of Dishes (Ingredients with recipe)."""
+ display_name = "Dish Randomizer"
+
+
+class TurtleRandomizer(Choice):
+ """Randomize the transportation turtle."""
+ display_name = "Turtle Randomizer"
+ option_none = 0
+ option_all = 1
+ option_all_except_final = 2
+ default = 2
+
+
+class EarlyEnergyForm(DefaultOnToggle):
+ """ Force the Energy Form to be in a location early in the game """
+ display_name = "Early Energy Form"
+
+
+class AquarianTranslation(Toggle):
+ """Translate the Aquarian scripture in the game into English."""
+ display_name = "Translate Aquarian"
+
+
+class BigBossesToBeat(Range):
+ """
+ The number of big bosses to beat before having access to the creator (the final boss). The big bosses are
+ "Fallen God", "Mithalan God", "Drunian God", "Sun God" and "The Golem".
+ """
+ display_name = "Big bosses to beat"
+ range_start = 0
+ range_end = 5
+ default = 0
+
+
+class MiniBossesToBeat(Range):
+ """
+ The number of minibosses to beat before having access to the creator (the final boss). The minibosses are
+ "Nautilus Prime", "Blaster Peg Prime", "Mergog", "Mithalan priests", "Octopus Prime", "Crabbius Maximus",
+ "Mantis Shrimp Prime" and "King Jellyfish God Prime".
+ Note that the Energy Statue and Simon Says are not minibosses.
+ """
+ display_name = "Minibosses to beat"
+ range_start = 0
+ range_end = 8
+ default = 0
+
+
+class Objective(Choice):
+ """
+ The game objective can be to kill the creator or to kill the creator after obtaining all three secret memories.
+ """
+ display_name = "Objective"
+ option_kill_the_creator = 0
+ option_obtain_secrets_and_kill_the_creator = 1
+ default = 0
+
+
+class SkipFirstVision(Toggle):
+ """
+ The first vision in the game, where Naija transforms into Energy Form and gets flooded by enemies, is quite cool but
+ can be quite long when you already know what is going on. This option can be used to skip this vision.
+ """
+ display_name = "Skip Naija's first vision"
+
+
+class NoProgressionHardOrHiddenLocation(Toggle):
+ """
+ Make sure that there are no progression items at hard-to-reach or hard-to-find locations.
+ Those locations are very High locations (that need beast form, soup and skill to get),
+ every location in the bubble cave, locations where need you to cross a false wall without any indication,
+ the Arnassi race, bosses and minibosses. Useful for those that want a more casual run.
+ """
+ display_name = "No progression in hard or hidden locations"
+
+
+class LightNeededToGetToDarkPlaces(DefaultOnToggle):
+ """
+ Make sure that the sun form or the dumbo pet can be acquired before getting to dark places.
+ Be aware that navigating in dark places without light is extremely difficult.
+ """
+ display_name = "Light needed to get to dark places"
+
+
+class BindSongNeededToGetUnderRockBulb(Toggle):
+ """
+ Make sure that the bind song can be acquired before having to obtain sing bulbs under rocks.
+ """
+ display_name = "Bind song needed to get sing bulbs under rocks"
+
+
+class BlindGoal(Toggle):
+ """
+ Hide the goal's requirements from the help page so that you have to go to the last boss door to know
+ what is needed to access the boss.
+ """
+ display_name = "Hide the goal's requirements"
+
+
+class UnconfineHomeWater(Choice):
+ """
+ Open the way out of the Home Water area so that Naija can go to open water and beyond without the bind song.
+ """
+ display_name = "Unconfine Home Water Area"
+ option_off = 0
+ option_via_energy_door = 1
+ option_via_transturtle = 2
+ option_via_both = 3
+ default = 0
+
+
+@dataclass
+class AquariaOptions(PerGameCommonOptions):
+ """
+ Every option in the Aquaria randomizer
+ """
+ start_inventory_from_pool: StartInventoryPool
+ objective: Objective
+ mini_bosses_to_beat: MiniBossesToBeat
+ big_bosses_to_beat: BigBossesToBeat
+ turtle_randomizer: TurtleRandomizer
+ early_energy_form: EarlyEnergyForm
+ light_needed_to_get_to_dark_places: LightNeededToGetToDarkPlaces
+ bind_song_needed_to_get_under_rock_bulb: BindSongNeededToGetUnderRockBulb
+ unconfine_home_water: UnconfineHomeWater
+ no_progression_hard_or_hidden_locations: NoProgressionHardOrHiddenLocation
+ ingredient_randomizer: IngredientRandomizer
+ dish_randomizer: DishRandomizer
+ aquarian_translation: AquarianTranslation
+ skip_first_vision: SkipFirstVision
+ blind_goal: BlindGoal
diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py
new file mode 100755
index 000000000000..93c02d4e6766
--- /dev/null
+++ b/worlds/aquaria/Regions.py
@@ -0,0 +1,1391 @@
+"""
+Author: Louis M
+Date: Fri, 15 Mar 2024 18:41:40 +0000
+Description: Used to manage Regions in the Aquaria game multiworld randomizer
+"""
+
+from typing import Dict, Optional
+from BaseClasses import MultiWorld, Region, Entrance, ItemClassification, CollectionState
+from .Items import AquariaItem
+from .Locations import AquariaLocations, AquariaLocation
+from .Options import AquariaOptions
+from worlds.generic.Rules import add_rule, set_rule
+
+
+# Every condition to connect regions
+
+def _has_hot_soup(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has the hotsoup item"""
+ return state.has("Hot soup", player)
+
+
+def _has_tongue_cleared(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has the Body tongue cleared item"""
+ return state.has("Body tongue cleared", player)
+
+
+def _has_sun_crystal(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has the Sun crystal item"""
+ return state.has("Has sun crystal", player) and _has_bind_song(state, player)
+
+
+def _has_li(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has Li in its team"""
+ return state.has("Li and Li song", player)
+
+
+def _has_damaging_item(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has the shield song item"""
+ return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby Nautilus",
+ "Baby Piranha", "Baby Blaster"}, player)
+
+
+def _has_shield_song(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has the shield song item"""
+ return state.has("Shield song", player)
+
+
+def _has_bind_song(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has the bind song item"""
+ return state.has("Bind song", player)
+
+
+def _has_energy_form(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has the energy form item"""
+ return state.has("Energy form", player)
+
+
+def _has_beast_form(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has the beast form item"""
+ return state.has("Beast form", player)
+
+
+def _has_nature_form(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has the nature form item"""
+ return state.has("Nature form", player)
+
+
+def _has_sun_form(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has the sun form item"""
+ return state.has("Sun form", player)
+
+
+def _has_light(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has the light item"""
+ return state.has("Baby Dumbo", player) or _has_sun_form(state, player)
+
+
+def _has_dual_form(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has the dual form item"""
+ return _has_li(state, player) and state.has("Dual form", player)
+
+
+def _has_fish_form(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has the fish form item"""
+ return state.has("Fish form", player)
+
+
+def _has_spirit_form(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has the spirit form item"""
+ return state.has("Spirit form", player)
+
+
+def _has_big_bosses(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has beated every big bosses"""
+ return state.has_all({"Fallen God beated", "Mithalan God beated", "Drunian God beated",
+ "Sun God beated", "The Golem beated"}, player)
+
+
+def _has_mini_bosses(state:CollectionState, player: int) -> bool:
+ """`player` in `state` has beated every big bosses"""
+ return state.has_all({"Nautilus Prime beated", "Blaster Peg Prime beated", "Mergog beated",
+ "Mithalan priests beated", "Octopus Prime beated", "Crabbius Maximus beated",
+ "Mantis Shrimp Prime beated", "King Jellyfish God Prime beated"}, player)
+
+
+def _has_secrets(state:CollectionState, player: int) -> bool:
+ return state.has_all({"First secret obtained", "Second secret obtained", "Third secret obtained"},player)
+
+
+class AquariaRegions:
+ """
+ Class used to create regions of the Aquaria game
+ """
+ menu: Region
+ verse_cave_r: Region
+ verse_cave_l: Region
+ home_water: Region
+ home_water_nautilus: Region
+ home_water_transturtle: Region
+ naija_home: Region
+ song_cave: Region
+ energy_temple_1: Region
+ energy_temple_2: Region
+ energy_temple_3: Region
+ energy_temple_boss: Region
+ energy_temple_idol: Region
+ energy_temple_blaster_room: Region
+ energy_temple_altar: Region
+ openwater_tl: Region
+ openwater_tr: Region
+ openwater_tr_turtle: Region
+ openwater_bl: Region
+ openwater_br: Region
+ skeleton_path: Region
+ skeleton_path_sc: Region
+ arnassi: Region
+ arnassi_path: Region
+ arnassi_crab_boss: Region
+ simon: Region
+ mithalas_city: Region
+ mithalas_city_top_path: Region
+ mithalas_city_fishpass: Region
+ cathedral_l: Region
+ cathedral_l_tube: Region
+ cathedral_l_sc: Region
+ cathedral_r: Region
+ cathedral_underground: Region
+ cathedral_boss_l: Region
+ cathedral_boss_r: Region
+ forest_tl: Region
+ forest_tl_fp: Region
+ forest_tr: Region
+ forest_tr_fp: Region
+ forest_bl: Region
+ forest_br: Region
+ forest_boss: Region
+ forest_boss_entrance: Region
+ forest_sprite_cave: Region
+ forest_sprite_cave_tube: Region
+ mermog_cave: Region
+ mermog_boss: Region
+ forest_fish_cave: Region
+ veil_tl: Region
+ veil_tl_fp: Region
+ veil_tr_l: Region
+ veil_tr_r: Region
+ veil_bl: Region
+ veil_b_sc: Region
+ veil_bl_fp: Region
+ veil_br: Region
+ octo_cave_t: Region
+ octo_cave_b: Region
+ turtle_cave: Region
+ turtle_cave_bubble: Region
+ sun_temple_l: Region
+ sun_temple_r: Region
+ sun_temple_boss_path: Region
+ sun_temple_boss: Region
+ abyss_l: Region
+ abyss_lb: Region
+ abyss_r: Region
+ ice_cave: Region
+ bubble_cave: Region
+ bubble_cave_boss: Region
+ king_jellyfish_cave: Region
+ whale: Region
+ first_secret: Region
+ sunken_city_l: Region
+ sunken_city_r: Region
+ sunken_city_boss: Region
+ sunken_city_l_bedroom: Region
+ body_c: Region
+ body_l: Region
+ body_rt: Region
+ body_rb: Region
+ body_b: Region
+ final_boss_loby: Region
+ final_boss_tube: Region
+ final_boss: Region
+ final_boss_end: Region
+ """
+ Every Region of the game
+ """
+
+ multiworld: MultiWorld
+ """
+ The Current Multiworld game.
+ """
+
+ player: int
+ """
+ The ID of the player
+ """
+
+ def __add_region(self, hint: str,
+ locations: Optional[Dict[str, Optional[int]]]) -> Region:
+ """
+ Create a new Region, add it to the `world` regions and return it.
+ Be aware that this function have a side effect on ``world`.`regions`
+ """
+ region: Region = Region(hint, self.player, self.multiworld, hint)
+ if locations is not None:
+ region.add_locations(locations, AquariaLocation)
+ return region
+
+ def __create_home_water_area(self) -> None:
+ """
+ Create the `verse_cave`, `home_water` and `song_cave*` regions
+ """
+ self.menu = self.__add_region("Menu", None)
+ self.verse_cave_r = self.__add_region("Verse Cave right area",
+ AquariaLocations.locations_verse_cave_r)
+ self.verse_cave_l = self.__add_region("Verse Cave left area",
+ AquariaLocations.locations_verse_cave_l)
+ self.home_water = self.__add_region("Home Water", AquariaLocations.locations_home_water)
+ self.home_water_nautilus = self.__add_region("Home Water, Nautilus nest",
+ AquariaLocations.locations_home_water_nautilus)
+ self.home_water_transturtle = self.__add_region("Home Water, turtle room",
+ AquariaLocations.locations_home_water_transturtle)
+ self.naija_home = self.__add_region("Naija's Home", AquariaLocations.locations_naija_home)
+ self.song_cave = self.__add_region("Song Cave", AquariaLocations.locations_song_cave)
+
+ def __create_energy_temple(self) -> None:
+ """
+ Create the `energy_temple_*` regions
+ """
+ self.energy_temple_1 = self.__add_region("Energy Temple first area",
+ AquariaLocations.locations_energy_temple_1)
+ self.energy_temple_2 = self.__add_region("Energy Temple second area",
+ AquariaLocations.locations_energy_temple_2)
+ self.energy_temple_3 = self.__add_region("Energy Temple third area",
+ AquariaLocations.locations_energy_temple_3)
+ self.energy_temple_altar = self.__add_region("Energy Temple bottom entrance",
+ AquariaLocations.locations_energy_temple_altar)
+ self.energy_temple_boss = self.__add_region("Energy Temple fallen God room",
+ AquariaLocations.locations_energy_temple_boss)
+ self.energy_temple_idol = self.__add_region("Energy Temple Idol room",
+ AquariaLocations.locations_energy_temple_idol)
+ self.energy_temple_blaster_room = self.__add_region("Energy Temple blaster room",
+ AquariaLocations.locations_energy_temple_blaster_room)
+
+ def __create_openwater(self) -> None:
+ """
+ Create the `openwater_*`, `skeleton_path`, `arnassi*` and `simon`
+ regions
+ """
+ self.openwater_tl = self.__add_region("Open Water top left area",
+ AquariaLocations.locations_openwater_tl)
+ self.openwater_tr = self.__add_region("Open Water top right area",
+ AquariaLocations.locations_openwater_tr)
+ self.openwater_tr_turtle = self.__add_region("Open Water top right area, turtle room",
+ AquariaLocations.locations_openwater_tr_turtle)
+ self.openwater_bl = self.__add_region("Open Water bottom left area",
+ AquariaLocations.locations_openwater_bl)
+ self.openwater_br = self.__add_region("Open Water bottom right area", None)
+ self.skeleton_path = self.__add_region("Open Water skeleton path",
+ AquariaLocations.locations_skeleton_path)
+ self.skeleton_path_sc = self.__add_region("Open Water skeleton path spirit crystal",
+ AquariaLocations.locations_skeleton_path_sc)
+ self.arnassi = self.__add_region("Arnassi Ruins", AquariaLocations.locations_arnassi)
+ self.arnassi_path = self.__add_region("Arnassi Ruins, back entrance path",
+ AquariaLocations.locations_arnassi_path)
+ self.arnassi_crab_boss = self.__add_region("Arnassi Ruins, Crabbius Maximus lair",
+ AquariaLocations.locations_arnassi_crab_boss)
+
+ def __create_mithalas(self) -> None:
+ """
+ Create the `mithalas_city*` and `cathedral_*` regions
+ """
+ self.mithalas_city = self.__add_region("Mithalas City",
+ AquariaLocations.locations_mithalas_city)
+ self.mithalas_city_fishpass = self.__add_region("Mithalas City fish pass",
+ AquariaLocations.locations_mithalas_city_fishpass)
+ self.mithalas_city_top_path = self.__add_region("Mithalas City top path",
+ AquariaLocations.locations_mithalas_city_top_path)
+ self.cathedral_l = self.__add_region("Mithalas castle", AquariaLocations.locations_cathedral_l)
+ self.cathedral_l_tube = self.__add_region("Mithalas castle, plant tube entrance",
+ AquariaLocations.locations_cathedral_l_tube)
+ self.cathedral_l_sc = self.__add_region("Mithalas castle spirit crystal",
+ AquariaLocations.locations_cathedral_l_sc)
+ self.cathedral_r = self.__add_region("Mithalas Cathedral",
+ AquariaLocations.locations_cathedral_r)
+ self.cathedral_underground = self.__add_region("Mithalas Cathedral underground",
+ AquariaLocations.locations_cathedral_underground)
+ self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room",
+ AquariaLocations.locations_cathedral_boss)
+ self.cathedral_boss_l = self.__add_region("Mithalas Cathedral, after Mithalan God room", None)
+
+ def __create_forest(self) -> None:
+ """
+ Create the `forest_*` dans `mermog_cave` regions
+ """
+ self.forest_tl = self.__add_region("Kelp Forest top left area",
+ AquariaLocations.locations_forest_tl)
+ self.forest_tl_fp = self.__add_region("Kelp Forest top left area fish pass",
+ AquariaLocations.locations_forest_tl_fp)
+ self.forest_tr = self.__add_region("Kelp Forest top right area",
+ AquariaLocations.locations_forest_tr)
+ self.forest_tr_fp = self.__add_region("Kelp Forest top right area fish pass",
+ AquariaLocations.locations_forest_tr_fp)
+ self.forest_bl = self.__add_region("Kelp Forest bottom left area",
+ AquariaLocations.locations_forest_bl)
+ self.forest_br = self.__add_region("Kelp Forest bottom right area",
+ AquariaLocations.locations_forest_br)
+ self.forest_sprite_cave = self.__add_region("Kelp Forest spirit cave",
+ AquariaLocations.locations_forest_sprite_cave)
+ self.forest_sprite_cave_tube = self.__add_region("Kelp Forest spirit cave after the plant tube",
+ AquariaLocations.locations_forest_sprite_cave_tube)
+ self.forest_boss = self.__add_region("Kelp Forest Drunian God room",
+ AquariaLocations.locations_forest_boss)
+ self.forest_boss_entrance = self.__add_region("Kelp Forest Drunian God room entrance",
+ AquariaLocations.locations_forest_boss_entrance)
+ self.mermog_cave = self.__add_region("Kelp Forest Mermog cave",
+ AquariaLocations.locations_mermog_cave)
+ self.mermog_boss = self.__add_region("Kelp Forest Mermog cave boss",
+ AquariaLocations.locations_mermog_boss)
+ self.forest_fish_cave = self.__add_region("Kelp Forest fish cave",
+ AquariaLocations.locations_forest_fish_cave)
+ self.simon = self.__add_region("Kelp Forest, Simon's room", AquariaLocations.locations_simon)
+
+ def __create_veil(self) -> None:
+ """
+ Create the `veil_*`, `octo_cave` and `turtle_cave` regions
+ """
+ self.veil_tl = self.__add_region("The Veil top left area", AquariaLocations.locations_veil_tl)
+ self.veil_tl_fp = self.__add_region("The Veil top left area fish pass",
+ AquariaLocations.locations_veil_tl_fp)
+ self.turtle_cave = self.__add_region("The Veil top left area, turtle cave",
+ AquariaLocations.locations_turtle_cave)
+ self.turtle_cave_bubble = self.__add_region("The Veil top left area, turtle cave Bubble Cliff",
+ AquariaLocations.locations_turtle_cave_bubble)
+ self.veil_tr_l = self.__add_region("The Veil top right area, left of temple",
+ AquariaLocations.locations_veil_tr_l)
+ self.veil_tr_r = self.__add_region("The Veil top right area, right of temple",
+ AquariaLocations.locations_veil_tr_r)
+ self.octo_cave_t = self.__add_region("Octopus Cave top entrance",
+ AquariaLocations.locations_octo_cave_t)
+ self.octo_cave_b = self.__add_region("Octopus Cave bottom entrance",
+ AquariaLocations.locations_octo_cave_b)
+ self.veil_bl = self.__add_region("The Veil bottom left area",
+ AquariaLocations.locations_veil_bl)
+ self.veil_b_sc = self.__add_region("The Veil bottom spirit crystal area",
+ AquariaLocations.locations_veil_b_sc)
+ self.veil_bl_fp = self.__add_region("The Veil bottom left area, in the sunken ship",
+ AquariaLocations.locations_veil_bl_fp)
+ self.veil_br = self.__add_region("The Veil bottom right area",
+ AquariaLocations.locations_veil_br)
+
+ def __create_sun_temple(self) -> None:
+ """
+ Create the `sun_temple*` regions
+ """
+ self.sun_temple_l = self.__add_region("Sun Temple left area",
+ AquariaLocations.locations_sun_temple_l)
+ self.sun_temple_r = self.__add_region("Sun Temple right area",
+ AquariaLocations.locations_sun_temple_r)
+ self.sun_temple_boss_path = self.__add_region("Sun Temple before boss area",
+ AquariaLocations.locations_sun_temple_boss_path)
+ self.sun_temple_boss = self.__add_region("Sun Temple boss area",
+ AquariaLocations.locations_sun_temple_boss)
+
+ def __create_abyss(self) -> None:
+ """
+ Create the `abyss_*`, `ice_cave`, `king_jellyfish_cave` and `whale`
+ regions
+ """
+ self.abyss_l = self.__add_region("Abyss left area",
+ AquariaLocations.locations_abyss_l)
+ self.abyss_lb = self.__add_region("Abyss left bottom area", AquariaLocations.locations_abyss_lb)
+ self.abyss_r = self.__add_region("Abyss right area", AquariaLocations.locations_abyss_r)
+ self.ice_cave = self.__add_region("Ice Cave", AquariaLocations.locations_ice_cave)
+ self.bubble_cave = self.__add_region("Bubble Cave", AquariaLocations.locations_bubble_cave)
+ self.bubble_cave_boss = self.__add_region("Bubble Cave boss area", AquariaLocations.locations_bubble_cave_boss)
+ self.king_jellyfish_cave = self.__add_region("Abyss left area, King jellyfish cave",
+ AquariaLocations.locations_king_jellyfish_cave)
+ self.whale = self.__add_region("Inside the whale", AquariaLocations.locations_whale)
+ self.first_secret = self.__add_region("First secret area", None)
+
+ def __create_sunken_city(self) -> None:
+ """
+ Create the `sunken_city_*` regions
+ """
+ self.sunken_city_l = self.__add_region("Sunken City left area",
+ AquariaLocations.locations_sunken_city_l)
+ self.sunken_city_l_bedroom = self.__add_region("Sunken City left area, bedroom",
+ AquariaLocations.locations_sunken_city_l_bedroom)
+ self.sunken_city_r = self.__add_region("Sunken City right area",
+ AquariaLocations.locations_sunken_city_r)
+ self.sunken_city_boss = self.__add_region("Sunken City boss area",
+ AquariaLocations.locations_sunken_city_boss)
+
+ def __create_body(self) -> None:
+ """
+ Create the `body_*` and `final_boss* regions
+ """
+ self.body_c = self.__add_region("The Body center area",
+ AquariaLocations.locations_body_c)
+ self.body_l = self.__add_region("The Body left area",
+ AquariaLocations.locations_body_l)
+ self.body_rt = self.__add_region("The Body right area, top path",
+ AquariaLocations.locations_body_rt)
+ self.body_rb = self.__add_region("The Body right area, bottom path",
+ AquariaLocations.locations_body_rb)
+ self.body_b = self.__add_region("The Body bottom area",
+ AquariaLocations.locations_body_b)
+ self.final_boss_loby = self.__add_region("The Body, before final boss", None)
+ self.final_boss_tube = self.__add_region("The Body, final boss area turtle room",
+ AquariaLocations.locations_final_boss_tube)
+ self.final_boss = self.__add_region("The Body, final boss",
+ AquariaLocations.locations_final_boss)
+ self.final_boss_end = self.__add_region("The Body, final boss area", None)
+
+ def __connect_one_way_regions(self, source_name: str, destination_name: str,
+ source_region: Region,
+ destination_region: Region, rule=None) -> None:
+ """
+ Connect from the `source_region` to the `destination_region`
+ """
+ entrance = Entrance(source_region.player, source_name + " to " + destination_name, source_region)
+ source_region.exits.append(entrance)
+ entrance.connect(destination_region)
+ if rule is not None:
+ set_rule(entrance, rule)
+
+ def __connect_regions(self, source_name: str, destination_name: str,
+ source_region: Region,
+ destination_region: Region, rule=None) -> None:
+ """
+ Connect the `source_region` and the `destination_region` (two-way)
+ """
+ self.__connect_one_way_regions(source_name, destination_name, source_region, destination_region, rule)
+ self.__connect_one_way_regions(destination_name, source_name, destination_region, source_region, rule)
+
+ def __connect_home_water_regions(self) -> None:
+ """
+ Connect entrances of the different regions around `home_water`
+ """
+ self.__connect_regions("Menu", "Verse Cave right area",
+ self.menu, self.verse_cave_r)
+ self.__connect_regions("Verse Cave left area", "Verse Cave right area",
+ self.verse_cave_l, self.verse_cave_r)
+ self.__connect_regions("Verse Cave", "Home Water", self.verse_cave_l, self.home_water)
+ self.__connect_regions("Home Water", "Haija's home", self.home_water, self.naija_home)
+ self.__connect_regions("Home Water", "Song Cave", self.home_water, self.song_cave)
+ self.__connect_regions("Home Water", "Home Water, nautilus nest",
+ self.home_water, self.home_water_nautilus,
+ lambda state: _has_energy_form(state, self.player) and _has_bind_song(state, self.player))
+ self.__connect_regions("Home Water", "Home Water transturtle room",
+ self.home_water, self.home_water_transturtle)
+ self.__connect_regions("Home Water", "Energy Temple first area",
+ self.home_water, self.energy_temple_1,
+ lambda state: _has_bind_song(state, self.player))
+ self.__connect_regions("Home Water", "Energy Temple_altar",
+ self.home_water, self.energy_temple_altar,
+ lambda state: _has_energy_form(state, self.player) and
+ _has_bind_song(state, self.player))
+ self.__connect_regions("Energy Temple first area", "Energy Temple second area",
+ self.energy_temple_1, self.energy_temple_2,
+ lambda state: _has_energy_form(state, self.player))
+ self.__connect_regions("Energy Temple first area", "Energy Temple idol room",
+ self.energy_temple_1, self.energy_temple_idol,
+ lambda state: _has_fish_form(state, self.player))
+ self.__connect_regions("Energy Temple idol room", "Energy Temple boss area",
+ self.energy_temple_idol, self.energy_temple_boss,
+ lambda state: _has_energy_form(state, self.player))
+ self.__connect_one_way_regions("Energy Temple first area", "Energy Temple boss area",
+ self.energy_temple_1, self.energy_temple_boss,
+ lambda state: _has_beast_form(state, self.player) and
+ _has_energy_form(state, self.player))
+ self.__connect_one_way_regions("Energy Temple boss area", "Energy Temple first area",
+ self.energy_temple_boss, self.energy_temple_1,
+ lambda state: _has_energy_form(state, self.player))
+ self.__connect_regions("Energy Temple second area", "Energy Temple third area",
+ self.energy_temple_2, self.energy_temple_3,
+ lambda state: _has_bind_song(state, self.player) and
+ _has_energy_form(state, self.player))
+ self.__connect_regions("Energy Temple boss area", "Energy Temple blaster room",
+ self.energy_temple_boss, self.energy_temple_blaster_room,
+ lambda state: _has_nature_form(state, self.player) and
+ _has_bind_song(state, self.player) and
+ _has_energy_form(state, self.player))
+ self.__connect_regions("Energy Temple first area", "Energy Temple blaster room",
+ self.energy_temple_1, self.energy_temple_blaster_room,
+ lambda state: _has_nature_form(state, self.player) and
+ _has_bind_song(state, self.player) and
+ _has_energy_form(state, self.player) and
+ _has_beast_form(state, self.player))
+ self.__connect_regions("Home Water", "Open Water top left area",
+ self.home_water, self.openwater_tl)
+
+ def __connect_open_water_regions(self) -> None:
+ """
+ Connect entrances of the different regions around open water
+ """
+ self.__connect_regions("Open Water top left area", "Open Water top right area",
+ self.openwater_tl, self.openwater_tr)
+ self.__connect_regions("Open Water top left area", "Open Water bottom left area",
+ self.openwater_tl, self.openwater_bl)
+ self.__connect_regions("Open Water top left area", "forest bottom right area",
+ self.openwater_tl, self.forest_br)
+ self.__connect_regions("Open Water top right area", "Open Water top right area, turtle room",
+ self.openwater_tr, self.openwater_tr_turtle,
+ lambda state: _has_beast_form(state, self.player))
+ self.__connect_regions("Open Water top right area", "Open Water bottom right area",
+ self.openwater_tr, self.openwater_br)
+ self.__connect_regions("Open Water top right area", "Mithalas City",
+ self.openwater_tr, self.mithalas_city)
+ self.__connect_regions("Open Water top right area", "Veil bottom left area",
+ self.openwater_tr, self.veil_bl)
+ self.__connect_one_way_regions("Open Water top right area", "Veil bottom right",
+ self.openwater_tr, self.veil_br,
+ lambda state: _has_beast_form(state, self.player))
+ self.__connect_one_way_regions("Veil bottom right", "Open Water top right area",
+ self.veil_br, self.openwater_tr,
+ lambda state: _has_beast_form(state, self.player))
+ self.__connect_regions("Open Water bottom left area", "Open Water bottom right area",
+ self.openwater_bl, self.openwater_br)
+ self.__connect_regions("Open Water bottom left area", "Skeleton path",
+ self.openwater_bl, self.skeleton_path)
+ self.__connect_regions("Abyss left area", "Open Water bottom left area",
+ self.abyss_l, self.openwater_bl)
+ self.__connect_regions("Skeleton path", "skeleton_path_sc",
+ self.skeleton_path, self.skeleton_path_sc,
+ lambda state: _has_spirit_form(state, self.player))
+ self.__connect_regions("Abyss right area", "Open Water bottom right area",
+ self.abyss_r, self.openwater_br)
+ self.__connect_one_way_regions("Open Water bottom right area", "Arnassi",
+ self.openwater_br, self.arnassi,
+ lambda state: _has_beast_form(state, self.player))
+ self.__connect_one_way_regions("Arnassi", "Open Water bottom right area",
+ self.arnassi, self.openwater_br)
+ self.__connect_regions("Arnassi", "Arnassi path",
+ self.arnassi, self.arnassi_path)
+ self.__connect_one_way_regions("Arnassi path", "Arnassi crab boss area",
+ self.arnassi_path, self.arnassi_crab_boss,
+ lambda state: _has_beast_form(state, self.player) and
+ _has_energy_form(state, self.player))
+ self.__connect_one_way_regions("Arnassi crab boss area", "Arnassi path",
+ self.arnassi_crab_boss, self.arnassi_path)
+
+ def __connect_mithalas_regions(self) -> None:
+ """
+ Connect entrances of the different regions around Mithalas
+ """
+ self.__connect_one_way_regions("Mithalas City", "Mithalas City top path",
+ self.mithalas_city, self.mithalas_city_top_path,
+ lambda state: _has_beast_form(state, self.player))
+ self.__connect_one_way_regions("Mithalas City_top_path", "Mithalas City",
+ self.mithalas_city_top_path, self.mithalas_city)
+ self.__connect_regions("Mithalas City", "Mithalas City home with fishpass",
+ self.mithalas_city, self.mithalas_city_fishpass,
+ lambda state: _has_fish_form(state, self.player))
+ self.__connect_regions("Mithalas City", "Mithalas castle",
+ self.mithalas_city, self.cathedral_l,
+ lambda state: _has_fish_form(state, self.player))
+ self.__connect_one_way_regions("Mithalas City top path", "Mithalas castle, flower tube",
+ self.mithalas_city_top_path,
+ self.cathedral_l_tube,
+ lambda state: _has_nature_form(state, self.player) and
+ _has_energy_form(state, self.player))
+ self.__connect_one_way_regions("Mithalas castle, flower tube area", "Mithalas City top path",
+ self.cathedral_l_tube,
+ self.mithalas_city_top_path,
+ lambda state: _has_beast_form(state, self.player) and
+ _has_nature_form(state, self.player))
+ self.__connect_one_way_regions("Mithalas castle flower tube area", "Mithalas castle, spirit crystals",
+ self.cathedral_l_tube, self.cathedral_l_sc,
+ lambda state: _has_spirit_form(state, self.player))
+ self.__connect_one_way_regions("Mithalas castle_flower tube area", "Mithalas castle",
+ self.cathedral_l_tube, self.cathedral_l,
+ lambda state: _has_spirit_form(state, self.player))
+ self.__connect_regions("Mithalas castle", "Mithalas castle, spirit crystals",
+ self.cathedral_l, self.cathedral_l_sc,
+ lambda state: _has_spirit_form(state, self.player))
+ self.__connect_regions("Mithalas castle", "Cathedral boss left area",
+ self.cathedral_l, self.cathedral_boss_l,
+ lambda state: _has_beast_form(state, self.player) and
+ _has_energy_form(state, self.player) and
+ _has_bind_song(state, self.player))
+ self.__connect_regions("Mithalas castle", "Mithalas Cathedral underground",
+ self.cathedral_l, self.cathedral_underground,
+ lambda state: _has_beast_form(state, self.player) and
+ _has_bind_song(state, self.player))
+ self.__connect_regions("Mithalas castle", "Mithalas Cathedral",
+ self.cathedral_l, self.cathedral_r,
+ lambda state: _has_bind_song(state, self.player) and
+ _has_energy_form(state, self.player))
+ self.__connect_regions("Mithalas Cathedral", "Mithalas Cathedral underground",
+ self.cathedral_r, self.cathedral_underground,
+ lambda state: _has_energy_form(state, self.player))
+ self.__connect_one_way_regions("Mithalas Cathedral underground", "Cathedral boss left area",
+ self.cathedral_underground, self.cathedral_boss_r,
+ lambda state: _has_energy_form(state, self.player) and
+ _has_bind_song(state, self.player))
+ self.__connect_one_way_regions("Cathedral boss left area", "Mithalas Cathedral underground",
+ self.cathedral_boss_r, self.cathedral_underground,
+ lambda state: _has_beast_form(state, self.player))
+ self.__connect_regions("Cathedral boss right area", "Cathedral boss left area",
+ self.cathedral_boss_r, self.cathedral_boss_l,
+ lambda state: _has_bind_song(state, self.player) and
+ _has_energy_form(state, self.player))
+
+ def __connect_forest_regions(self) -> None:
+ """
+ Connect entrances of the different regions around the Kelp Forest
+ """
+ self.__connect_regions("Forest bottom right", "Veil bottom left area",
+ self.forest_br, self.veil_bl)
+ self.__connect_regions("Forest bottom right", "Forest bottom left area",
+ self.forest_br, self.forest_bl)
+ self.__connect_regions("Forest bottom right", "Forest top right area",
+ self.forest_br, self.forest_tr)
+ self.__connect_regions("Forest bottom left area", "Forest fish cave",
+ self.forest_bl, self.forest_fish_cave)
+ self.__connect_regions("Forest bottom left area", "Forest top left area",
+ self.forest_bl, self.forest_tl)
+ self.__connect_regions("Forest bottom left area", "Forest boss entrance",
+ self.forest_bl, self.forest_boss_entrance,
+ lambda state: _has_nature_form(state, self.player))
+ self.__connect_regions("Forest top left area", "Forest top left area, fish pass",
+ self.forest_tl, self.forest_tl_fp,
+ lambda state: _has_nature_form(state, self.player) and
+ _has_bind_song(state, self.player) and
+ _has_energy_form(state, self.player) and
+ _has_fish_form(state, self.player))
+ self.__connect_regions("Forest top left area", "Forest top right area",
+ self.forest_tl, self.forest_tr)
+ self.__connect_regions("Forest top left area", "Forest boss entrance",
+ self.forest_tl, self.forest_boss_entrance)
+ self.__connect_regions("Forest boss area", "Forest boss entrance",
+ self.forest_boss, self.forest_boss_entrance,
+ lambda state: _has_energy_form(state, self.player))
+ self.__connect_regions("Forest top right area", "Forest top right area fish pass",
+ self.forest_tr, self.forest_tr_fp,
+ lambda state: _has_fish_form(state, self.player))
+ self.__connect_regions("Forest top right area", "Forest sprite cave",
+ self.forest_tr, self.forest_sprite_cave)
+ self.__connect_regions("Forest sprite cave", "Forest sprite cave flower tube",
+ self.forest_sprite_cave, self.forest_sprite_cave_tube,
+ lambda state: _has_nature_form(state, self.player))
+ self.__connect_regions("Forest top right area", "Mermog cave",
+ self.forest_tr_fp, self.mermog_cave)
+ self.__connect_regions("Fermog cave", "Fermog boss",
+ self.mermog_cave, self.mermog_boss,
+ lambda state: _has_beast_form(state, self.player) and
+ _has_energy_form(state, self.player))
+
+ def __connect_veil_regions(self) -> None:
+ """
+ Connect entrances of the different regions around The Veil
+ """
+ self.__connect_regions("Veil bottom left area", "Veil bottom left area, fish pass",
+ self.veil_bl, self.veil_bl_fp,
+ lambda state: _has_fish_form(state, self.player) and
+ _has_bind_song(state, self.player) and
+ _has_damaging_item(state, self.player))
+ self.__connect_regions("Veil bottom left area", "Veil bottom area spirit crystals path",
+ self.veil_bl, self.veil_b_sc,
+ lambda state: _has_spirit_form(state, self.player))
+ self.__connect_regions("Veil bottom area spirit crystals path", "Veil bottom right",
+ self.veil_b_sc, self.veil_br,
+ lambda state: _has_spirit_form(state, self.player))
+ self.__connect_regions("Veil bottom right", "Veil top left area",
+ self.veil_br, self.veil_tl,
+ lambda state: _has_beast_form(state, self.player))
+ self.__connect_regions("Veil top left area", "Veil_top left area, fish pass",
+ self.veil_tl, self.veil_tl_fp,
+ lambda state: _has_fish_form(state, self.player))
+ self.__connect_regions("Veil top left area", "Veil right of sun temple",
+ self.veil_tl, self.veil_tr_r)
+ self.__connect_regions("Veil top left area", "Turtle cave",
+ self.veil_tl, self.turtle_cave)
+ self.__connect_regions("Turtle cave", "Turtle cave Bubble Cliff",
+ self.turtle_cave, self.turtle_cave_bubble,
+ lambda state: _has_beast_form(state, self.player))
+ self.__connect_regions("Veil right of sun temple", "Sun Temple right area",
+ self.veil_tr_r, self.sun_temple_r)
+ self.__connect_regions("Sun Temple right area", "Sun Temple left area",
+ self.sun_temple_r, self.sun_temple_l,
+ lambda state: _has_bind_song(state, self.player))
+ self.__connect_regions("Sun Temple left area", "Veil left of sun temple",
+ self.sun_temple_l, self.veil_tr_l)
+ self.__connect_regions("Sun Temple left area", "Sun Temple before boss area",
+ self.sun_temple_l, self.sun_temple_boss_path)
+ self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area",
+ self.sun_temple_boss_path, self.sun_temple_boss,
+ lambda state: _has_energy_form(state, self.player))
+ self.__connect_one_way_regions("Sun Temple boss area", "Veil left of sun temple",
+ self.sun_temple_boss, self.veil_tr_l)
+ self.__connect_regions("Veil left of sun temple", "Octo cave top path",
+ self.veil_tr_l, self.octo_cave_t,
+ lambda state: _has_fish_form(state, self.player) and
+ _has_sun_form(state, self.player) and
+ _has_beast_form(state, self.player) and
+ _has_energy_form(state, self.player))
+ self.__connect_regions("Veil left of sun temple", "Octo cave bottom path",
+ self.veil_tr_l, self.octo_cave_b,
+ lambda state: _has_fish_form(state, self.player))
+
+ def __connect_abyss_regions(self) -> None:
+ """
+ Connect entrances of the different regions around The Abyss
+ """
+ self.__connect_regions("Abyss left area", "Abyss bottom of left area",
+ self.abyss_l, self.abyss_lb,
+ lambda state: _has_nature_form(state, self.player))
+ self.__connect_regions("Abyss left bottom area", "Sunken City right area",
+ self.abyss_lb, self.sunken_city_r,
+ lambda state: _has_li(state, self.player))
+ self.__connect_one_way_regions("Abyss left bottom area", "Body center area",
+ self.abyss_lb, self.body_c,
+ lambda state: _has_tongue_cleared(state, self.player))
+ self.__connect_one_way_regions("Body center area", "Abyss left bottom area",
+ self.body_c, self.abyss_lb)
+ self.__connect_regions("Abyss left area", "King jellyfish cave",
+ self.abyss_l, self.king_jellyfish_cave,
+ lambda state: _has_energy_form(state, self.player) and
+ _has_beast_form(state, self.player))
+ self.__connect_regions("Abyss left area", "Abyss right area",
+ self.abyss_l, self.abyss_r)
+ self.__connect_regions("Abyss right area", "Inside the whale",
+ self.abyss_r, self.whale,
+ lambda state: _has_spirit_form(state, self.player) and
+ _has_sun_form(state, self.player))
+ self.__connect_regions("Abyss right area", "First secret area",
+ self.abyss_r, self.first_secret,
+ lambda state: _has_spirit_form(state, self.player) and
+ _has_sun_form(state, self.player) and
+ _has_bind_song(state, self.player) and
+ _has_energy_form(state, self.player))
+ self.__connect_regions("Abyss right area", "Ice Cave",
+ self.abyss_r, self.ice_cave,
+ lambda state: _has_spirit_form(state, self.player))
+ self.__connect_regions("Abyss right area", "Bubble Cave",
+ self.ice_cave, self.bubble_cave,
+ lambda state: _has_beast_form(state, self.player))
+ self.__connect_regions("Bubble Cave boss area", "Bubble Cave",
+ self.bubble_cave, self.bubble_cave_boss,
+ lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player)
+ )
+
+ def __connect_sunken_city_regions(self) -> None:
+ """
+ Connect entrances of the different regions around The Sunken City
+ """
+ self.__connect_regions("Sunken City right area", "Sunken City left area",
+ self.sunken_city_r, self.sunken_city_l)
+ self.__connect_regions("Sunken City left area", "Sunken City bedroom",
+ self.sunken_city_l, self.sunken_city_l_bedroom,
+ lambda state: _has_spirit_form(state, self.player))
+ self.__connect_regions("Sunken City left area", "Sunken City boss area",
+ self.sunken_city_l, self.sunken_city_boss,
+ lambda state: _has_beast_form(state, self.player) and
+ _has_sun_form(state, self.player) and
+ _has_energy_form(state, self.player) and
+ _has_bind_song(state, self.player))
+
+ def __connect_body_regions(self) -> None:
+ """
+ Connect entrances of the different regions around The Body
+ """
+ self.__connect_regions("Body center area", "Body left area",
+ self.body_c, self.body_l)
+ self.__connect_regions("Body center area", "Body right area top path",
+ self.body_c, self.body_rt)
+ self.__connect_regions("Body center area", "Body right area bottom path",
+ self.body_c, self.body_rb)
+ self.__connect_regions("Body center area", "Body bottom area",
+ self.body_c, self.body_b,
+ lambda state: _has_dual_form(state, self.player))
+ self.__connect_regions("Body bottom area", "Final Boss area",
+ self.body_b, self.final_boss_loby,
+ lambda state: _has_dual_form(state, self.player))
+ self.__connect_regions("Before Final Boss", "Final Boss tube",
+ self.final_boss_loby, self.final_boss_tube,
+ lambda state: _has_nature_form(state, self.player))
+ self.__connect_one_way_regions("Before Final Boss", "Final Boss",
+ self.final_boss_loby, self.final_boss,
+ lambda state: _has_energy_form(state, self.player) and
+ _has_dual_form(state, self.player) and
+ _has_sun_form(state, self.player) and
+ _has_bind_song(state, self.player))
+ self.__connect_one_way_regions("final boss third form area", "final boss end",
+ self.final_boss, self.final_boss_end)
+
+ def __connect_transturtle(self, item_source: str, item_target: str, region_source: Region, region_target: Region,
+ rule=None) -> None:
+ """Connect a single transturtle to another one"""
+ if item_source != item_target:
+ if rule is None:
+ self.__connect_one_way_regions(item_source, item_target, region_source, region_target,
+ lambda state: state.has(item_target, self.player))
+ else:
+ self.__connect_one_way_regions(item_source, item_target, region_source, region_target, rule)
+
+ def __connect_arnassi_path_transturtle(self, item_source: str, item_target: str, region_source: Region,
+ region_target: Region) -> None:
+ """Connect the Arnassi Ruins transturtle to another one"""
+ self.__connect_one_way_regions(item_source, item_target, region_source, region_target,
+ lambda state: state.has(item_target, self.player) and
+ _has_fish_form(state, self.player))
+
+ def _connect_transturtle_to_other(self, item: str, region: Region) -> None:
+ """Connect a single transturtle to all others"""
+ self.__connect_transturtle(item, "Transturtle Veil top left", region, self.veil_tl)
+ self.__connect_transturtle(item, "Transturtle Veil top right", region, self.veil_tr_l)
+ self.__connect_transturtle(item, "Transturtle Open Water top right", region, self.openwater_tr_turtle)
+ self.__connect_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl)
+ self.__connect_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle)
+ self.__connect_transturtle(item, "Transturtle Abyss right", region, self.abyss_r)
+ self.__connect_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube)
+ self.__connect_transturtle(item, "Transturtle Simon Says", region, self.simon)
+ self.__connect_transturtle(item, "Transturtle Arnassi Ruins", region, self.arnassi_path,
+ lambda state: state.has("Transturtle Arnassi Ruins", self.player) and
+ _has_fish_form(state, self.player))
+
+ def _connect_arnassi_path_transturtle_to_other(self, item: str, region: Region) -> None:
+ """Connect the Arnassi Ruins transturtle to all others"""
+ self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top left", region, self.veil_tl)
+ self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top right", region, self.veil_tr_l)
+ self.__connect_arnassi_path_transturtle(item, "Transturtle Open Water top right", region,
+ self.openwater_tr_turtle)
+ self.__connect_arnassi_path_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl)
+ self.__connect_arnassi_path_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle)
+ self.__connect_arnassi_path_transturtle(item, "Transturtle Abyss right", region, self.abyss_r)
+ self.__connect_arnassi_path_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube)
+ self.__connect_arnassi_path_transturtle(item, "Transturtle Simon Says", region, self.simon)
+
+ def __connect_transturtles(self) -> None:
+ """Connect every transturtle with others"""
+ self._connect_transturtle_to_other("Transturtle Veil top left", self.veil_tl)
+ self._connect_transturtle_to_other("Transturtle Veil top right", self.veil_tr_l)
+ self._connect_transturtle_to_other("Transturtle Open Water top right", self.openwater_tr_turtle)
+ self._connect_transturtle_to_other("Transturtle Forest bottom left", self.forest_bl)
+ self._connect_transturtle_to_other("Transturtle Home Water", self.home_water_transturtle)
+ self._connect_transturtle_to_other("Transturtle Abyss right", self.abyss_r)
+ self._connect_transturtle_to_other("Transturtle Final Boss", self.final_boss_tube)
+ self._connect_transturtle_to_other("Transturtle Simon Says", self.simon)
+ self._connect_arnassi_path_transturtle_to_other("Transturtle Arnassi Ruins", self.arnassi_path)
+
+ def connect_regions(self) -> None:
+ """
+ Connect every region (entrances and exits)
+ """
+ self.__connect_home_water_regions()
+ self.__connect_open_water_regions()
+ self.__connect_mithalas_regions()
+ self.__connect_forest_regions()
+ self.__connect_veil_regions()
+ self.__connect_abyss_regions()
+ self.__connect_sunken_city_regions()
+ self.__connect_body_regions()
+ self.__connect_transturtles()
+
+ def __add_event_location(self, region: Region, name: str, event_name: str) -> None:
+ """
+ Add an event to the `region` with the name `name` and the item
+ `event_name`
+ """
+ location: AquariaLocation = AquariaLocation(
+ self.player, name, None, region
+ )
+ region.locations.append(location)
+ location.place_locked_item(AquariaItem(event_name,
+ ItemClassification.progression,
+ None,
+ self.player))
+
+ def __add_event_big_bosses(self) -> None:
+ """
+ Add every bit bosses (other than the creator) events to the `world`
+ """
+ self.__add_event_location(self.energy_temple_boss,
+ "Beating Fallen God",
+ "Fallen God beated")
+ self.__add_event_location(self.cathedral_boss_r,
+ "Beating Mithalan God",
+ "Mithalan God beated")
+ self.__add_event_location(self.forest_boss,
+ "Beating Drunian God",
+ "Drunian God beated")
+ self.__add_event_location(self.sun_temple_boss,
+ "Beating Sun God",
+ "Sun God beated")
+ self.__add_event_location(self.sunken_city_boss,
+ "Beating the Golem",
+ "The Golem beated")
+
+ def __add_event_mini_bosses(self) -> None:
+ """
+ Add every mini bosses (excluding Energy Statue and Simon Says)
+ events to the `world`
+ """
+ self.__add_event_location(self.home_water_nautilus,
+ "Beating Nautilus Prime",
+ "Nautilus Prime beated")
+ self.__add_event_location(self.energy_temple_blaster_room,
+ "Beating Blaster Peg Prime",
+ "Blaster Peg Prime beated")
+ self.__add_event_location(self.mermog_boss,
+ "Beating Mergog",
+ "Mergog beated")
+ self.__add_event_location(self.cathedral_l_tube,
+ "Beating Mithalan priests",
+ "Mithalan priests beated")
+ self.__add_event_location(self.octo_cave_t,
+ "Beating Octopus Prime",
+ "Octopus Prime beated")
+ self.__add_event_location(self.arnassi_crab_boss,
+ "Beating Crabbius Maximus",
+ "Crabbius Maximus beated")
+ self.__add_event_location(self.bubble_cave_boss,
+ "Beating Mantis Shrimp Prime",
+ "Mantis Shrimp Prime beated")
+ self.__add_event_location(self.king_jellyfish_cave,
+ "Beating King Jellyfish God Prime",
+ "King Jellyfish God Prime beated")
+
+ def __add_event_secrets(self) -> None:
+ """
+ Add secrets events to the `world`
+ """
+ self.__add_event_location(self.first_secret, # Doit ajouter une région pour le "first secret"
+ "First secret",
+ "First secret obtained")
+ self.__add_event_location(self.mithalas_city,
+ "Second secret",
+ "Second secret obtained")
+ self.__add_event_location(self.sun_temple_l,
+ "Third secret",
+ "Third secret obtained")
+
+ def add_event_locations(self) -> None:
+ """
+ Add every event (locations and items) to the `world`
+ """
+ self.__add_event_mini_bosses()
+ self.__add_event_big_bosses()
+ self.__add_event_secrets()
+ self.__add_event_location(self.sunken_city_boss,
+ "Sunken City cleared",
+ "Body tongue cleared")
+ self.__add_event_location(self.sun_temple_r,
+ "Sun Crystal",
+ "Has sun crystal")
+ self.__add_event_location(self.final_boss_end, "Objective complete",
+ "Victory")
+
+ def __adjusting_urns_rules(self) -> None:
+ """Since Urns need to be broken, add a damaging item to rules"""
+ add_rule(self.multiworld.get_location("Open Water top right area, first urn in the Mithalas exit", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Open Water top right area, second urn in the Mithalas exit", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Open Water top right area, third urn in the Mithalas exit", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Mithalas City, first urn in one of the homes", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Mithalas City, second urn in one of the homes", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Mithalas City, first urn in the city reserve", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Mithalas City, second urn in the city reserve", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Mithalas City, third urn in the city reserve", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Mithalas City, urn in the Castle flower tube entrance", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bedroom", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Mithalas City Castle, first urn of the single lamp path", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Mithalas City Castle, second urn of the single lamp path", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bottom room", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Mithalas City Castle, first urn on the entrance path", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Mithalas City Castle, second urn on the entrance path", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Mithalas City, urn inside a home fish pass", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+
+ def __adjusting_crates_rules(self) -> None:
+ """Since Crate need to be broken, add a damaging item to rules"""
+ add_rule(self.multiworld.get_location("Sunken City right area, crate close to the save crystal", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Sunken City right area, crate in the left bottom room", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Sunken City left area, crate in the little pipe room", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Sunken City left area, crate close to the save crystal", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+ add_rule(self.multiworld.get_location("Sunken City left area, crate before the bedroom", self.player),
+ lambda state: _has_damaging_item(state, self.player))
+
+ def __adjusting_soup_rules(self) -> None:
+ """
+ Modify rules for location that need soup
+ """
+ add_rule(self.multiworld.get_location("Turtle cave, Urchin Costume", self.player),
+ lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
+ add_rule(self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player),
+ lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
+ add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player),
+ lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
+ add_rule(self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.player),
+ lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
+
+ def __adjusting_under_rock_location(self) -> None:
+ """
+ Modify rules implying bind song needed for bulb under rocks
+ """
+ add_rule(self.multiworld.get_location("Home Water, bulb under the rock in the left path from the Verse Cave",
+ self.player), lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Verse Cave left area, bulb under the rock at the end of the path",
+ self.player), lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Naija's Home, bulb under the rock at the right of the main path",
+ self.player), lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Song Cave, bulb under the rock in the path to the singing statues",
+ self.player), lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Song Cave, bulb under the rock close to the song door",
+ self.player), lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Energy Temple second area, bulb under the rock",
+ self.player), lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the right path",
+ self.player), lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the left path",
+ self.player), lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Kelp Forest top right area, bulb under the rock in the right path",
+ self.player), lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path",
+ self.player), lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room",
+ self.player), lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Abyss right area, bulb in the middle path",
+ self.player), lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path",
+ self.player), lambda state: _has_bind_song(state, self.player))
+
+ def __adjusting_light_in_dark_place_rules(self) -> None:
+ add_rule(self.multiworld.get_location("Kelp Forest top right area, Black Pearl", self.player),
+ lambda state: _has_light(state, self.player))
+ add_rule(self.multiworld.get_location("Kelp Forest bottom right area, Odd Container", self.player),
+ lambda state: _has_light(state, self.player))
+ add_rule(self.multiworld.get_entrance("Transturtle Veil top left to Transturtle Abyss right", self.player),
+ lambda state: _has_light(state, self.player))
+ add_rule(self.multiworld.get_entrance("Transturtle Open Water top right to Transturtle Abyss right", self.player),
+ lambda state: _has_light(state, self.player))
+ add_rule(self.multiworld.get_entrance("Transturtle Veil top right to Transturtle Abyss right", self.player),
+ lambda state: _has_light(state, self.player))
+ add_rule(self.multiworld.get_entrance("Transturtle Forest bottom left to Transturtle Abyss right", self.player),
+ lambda state: _has_light(state, self.player))
+ add_rule(self.multiworld.get_entrance("Transturtle Home Water to Transturtle Abyss right", self.player),
+ lambda state: _has_light(state, self.player))
+ add_rule(self.multiworld.get_entrance("Transturtle Final Boss to Transturtle Abyss right", self.player),
+ lambda state: _has_light(state, self.player))
+ add_rule(self.multiworld.get_entrance("Transturtle Simon Says to Transturtle Abyss right", self.player),
+ lambda state: _has_light(state, self.player))
+ add_rule(self.multiworld.get_entrance("Transturtle Arnassi Ruins to Transturtle Abyss right", self.player),
+ lambda state: _has_light(state, self.player))
+ add_rule(self.multiworld.get_entrance("Body center area to Abyss left bottom area", self.player),
+ lambda state: _has_light(state, self.player))
+ add_rule(self.multiworld.get_entrance("Veil left of sun temple to Octo cave top path", self.player),
+ lambda state: _has_light(state, self.player))
+ add_rule(self.multiworld.get_entrance("Open Water bottom right area to Abyss right area", self.player),
+ lambda state: _has_light(state, self.player))
+ add_rule(self.multiworld.get_entrance("Open Water bottom left area to Abyss left area", self.player),
+ lambda state: _has_light(state, self.player))
+ add_rule(self.multiworld.get_entrance("Sun Temple left area to Sun Temple right area", self.player),
+ lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player))
+ add_rule(self.multiworld.get_entrance("Sun Temple right area to Sun Temple left area", self.player),
+ lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player))
+ add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun Temple left area", self.player),
+ lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player))
+
+ def __adjusting_manual_rules(self) -> None:
+ add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player),
+ lambda state: _has_beast_form(state, self.player))
+ add_rule(self.multiworld.get_location("Open Water bottom left area, bulb inside the lowest fish pass", self.player),
+ lambda state: _has_fish_form(state, self.player))
+ add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", self.player),
+ lambda state: _has_spirit_form(state, self.player))
+ add_rule(self.multiworld.get_location("The Veil top left area, bulb hidden behind the blocking rock", self.player),
+ lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Turtle cave, Turtle Egg", self.player),
+ lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Abyss left area, bulb in the bottom fish pass", self.player),
+ lambda state: _has_fish_form(state, self.player))
+ add_rule(self.multiworld.get_location("Song Cave, Anemone Seed", self.player),
+ lambda state: _has_nature_form(state, self.player))
+ add_rule(self.multiworld.get_location("Song Cave, Verse Egg", self.player),
+ lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Verse Cave right area, Big Seed", self.player),
+ lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Arnassi Ruins, Song Plant Spore", self.player),
+ lambda state: _has_beast_form(state, self.player))
+ add_rule(self.multiworld.get_location("Energy Temple first area, bulb in the bottom room blocked by a rock",
+ self.player), lambda state: _has_energy_form(state, self.player))
+ add_rule(self.multiworld.get_location("Home Water, bulb in the bottom left room", self.player),
+ lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Home Water, bulb in the path below Nautilus Prime", self.player),
+ lambda state: _has_bind_song(state, self.player))
+ add_rule(self.multiworld.get_location("Naija's Home, bulb after the energy door", self.player),
+ lambda state: _has_energy_form(state, self.player))
+ add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", self.player),
+ lambda state: _has_spirit_form(state, self.player) and
+ _has_sun_form(state, self.player))
+ add_rule(self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player),
+ lambda state: _has_fish_form(state, self.player) and
+ _has_spirit_form(state, self.player))
+
+ def __no_progression_hard_or_hidden_location(self) -> None:
+ self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Mithalas boss area, beating Mithalan God",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Kelp Forest boss area, beating Drunian God",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Sun Temple boss area, beating Sun God",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Sunken City, bulb on top of the boss area",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Home Water, Nautilus Egg",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Energy Temple blaster room, Blaster Egg",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Mithalas City Castle, beating the Priests",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Mermog cave, Piranha Egg",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Octopus Cave, Dumbo Egg",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Final Boss area, bulb in the boss third form room",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Sun Worm path, first cliff bulb",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Sun Worm path, second cliff bulb",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Bubble Cave, bulb in the left cave wall",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Bubble Cave, Verse Egg",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Sun Temple, Sun Key",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("The Body bottom area, Mutant Costume",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+ self.multiworld.get_location("Arnassi Ruins, Arnassi Armor",
+ self.player).item_rule =\
+ lambda item: item.classification != ItemClassification.progression
+
+ def adjusting_rules(self, options: AquariaOptions) -> None:
+ """
+ Modify rules for single location or optional rules
+ """
+ self.__adjusting_urns_rules()
+ self.__adjusting_crates_rules()
+ self.__adjusting_soup_rules()
+ self.__adjusting_manual_rules()
+ if options.light_needed_to_get_to_dark_places:
+ self.__adjusting_light_in_dark_place_rules()
+ if options.bind_song_needed_to_get_under_rock_bulb:
+ self.__adjusting_under_rock_location()
+
+ if options.mini_bosses_to_beat.value > 0:
+ add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player),
+ lambda state: _has_mini_bosses(state, self.player))
+ if options.big_bosses_to_beat.value > 0:
+ add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player),
+ lambda state: _has_big_bosses(state, self.player))
+ if options.objective.value == 1:
+ add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player),
+ lambda state: _has_secrets(state, self.player))
+ if options.unconfine_home_water.value in [0, 1]:
+ add_rule(self.multiworld.get_entrance("Home Water to Home Water transturtle room", self.player),
+ lambda state: _has_bind_song(state, self.player))
+ if options.unconfine_home_water.value in [0, 2]:
+ add_rule(self.multiworld.get_entrance("Home Water to Open Water top left area", self.player),
+ lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player))
+ if options.early_energy_form:
+ self.multiworld.early_items[self.player]["Energy form"] = 1
+
+ if options.no_progression_hard_or_hidden_locations:
+ self.__no_progression_hard_or_hidden_location()
+
+ def __add_home_water_regions_to_world(self) -> None:
+ """
+ Add every region around home water to the `world`
+ """
+ self.multiworld.regions.append(self.menu)
+ self.multiworld.regions.append(self.verse_cave_r)
+ self.multiworld.regions.append(self.verse_cave_l)
+ self.multiworld.regions.append(self.home_water)
+ self.multiworld.regions.append(self.home_water_nautilus)
+ self.multiworld.regions.append(self.home_water_transturtle)
+ self.multiworld.regions.append(self.naija_home)
+ self.multiworld.regions.append(self.song_cave)
+ self.multiworld.regions.append(self.energy_temple_1)
+ self.multiworld.regions.append(self.energy_temple_2)
+ self.multiworld.regions.append(self.energy_temple_3)
+ self.multiworld.regions.append(self.energy_temple_boss)
+ self.multiworld.regions.append(self.energy_temple_blaster_room)
+ self.multiworld.regions.append(self.energy_temple_altar)
+
+ def __add_open_water_regions_to_world(self) -> None:
+ """
+ Add every region around open water to the `world`
+ """
+ self.multiworld.regions.append(self.openwater_tl)
+ self.multiworld.regions.append(self.openwater_tr)
+ self.multiworld.regions.append(self.openwater_tr_turtle)
+ self.multiworld.regions.append(self.openwater_bl)
+ self.multiworld.regions.append(self.openwater_br)
+ self.multiworld.regions.append(self.skeleton_path)
+ self.multiworld.regions.append(self.skeleton_path_sc)
+ self.multiworld.regions.append(self.arnassi)
+ self.multiworld.regions.append(self.arnassi_path)
+ self.multiworld.regions.append(self.arnassi_crab_boss)
+ self.multiworld.regions.append(self.simon)
+
+ def __add_mithalas_regions_to_world(self) -> None:
+ """
+ Add every region around Mithalas to the `world`
+ """
+ self.multiworld.regions.append(self.mithalas_city)
+ self.multiworld.regions.append(self.mithalas_city_top_path)
+ self.multiworld.regions.append(self.mithalas_city_fishpass)
+ self.multiworld.regions.append(self.cathedral_l)
+ self.multiworld.regions.append(self.cathedral_l_tube)
+ self.multiworld.regions.append(self.cathedral_l_sc)
+ self.multiworld.regions.append(self.cathedral_r)
+ self.multiworld.regions.append(self.cathedral_underground)
+ self.multiworld.regions.append(self.cathedral_boss_l)
+ self.multiworld.regions.append(self.cathedral_boss_r)
+
+ def __add_forest_regions_to_world(self) -> None:
+ """
+ Add every region around the kelp forest to the `world`
+ """
+ self.multiworld.regions.append(self.forest_tl)
+ self.multiworld.regions.append(self.forest_tl_fp)
+ self.multiworld.regions.append(self.forest_tr)
+ self.multiworld.regions.append(self.forest_tr_fp)
+ self.multiworld.regions.append(self.forest_bl)
+ self.multiworld.regions.append(self.forest_br)
+ self.multiworld.regions.append(self.forest_boss)
+ self.multiworld.regions.append(self.forest_boss_entrance)
+ self.multiworld.regions.append(self.forest_sprite_cave)
+ self.multiworld.regions.append(self.forest_sprite_cave_tube)
+ self.multiworld.regions.append(self.mermog_cave)
+ self.multiworld.regions.append(self.mermog_boss)
+ self.multiworld.regions.append(self.forest_fish_cave)
+
+ def __add_veil_regions_to_world(self) -> None:
+ """
+ Add every region around the Veil to the `world`
+ """
+ self.multiworld.regions.append(self.veil_tl)
+ self.multiworld.regions.append(self.veil_tl_fp)
+ self.multiworld.regions.append(self.veil_tr_l)
+ self.multiworld.regions.append(self.veil_tr_r)
+ self.multiworld.regions.append(self.veil_bl)
+ self.multiworld.regions.append(self.veil_b_sc)
+ self.multiworld.regions.append(self.veil_bl_fp)
+ self.multiworld.regions.append(self.veil_br)
+ self.multiworld.regions.append(self.octo_cave_t)
+ self.multiworld.regions.append(self.octo_cave_b)
+ self.multiworld.regions.append(self.turtle_cave)
+ self.multiworld.regions.append(self.turtle_cave_bubble)
+ self.multiworld.regions.append(self.sun_temple_l)
+ self.multiworld.regions.append(self.sun_temple_r)
+ self.multiworld.regions.append(self.sun_temple_boss_path)
+ self.multiworld.regions.append(self.sun_temple_boss)
+
+ def __add_abyss_regions_to_world(self) -> None:
+ """
+ Add every region around the Abyss to the `world`
+ """
+ self.multiworld.regions.append(self.abyss_l)
+ self.multiworld.regions.append(self.abyss_lb)
+ self.multiworld.regions.append(self.abyss_r)
+ self.multiworld.regions.append(self.ice_cave)
+ self.multiworld.regions.append(self.bubble_cave)
+ self.multiworld.regions.append(self.bubble_cave_boss)
+ self.multiworld.regions.append(self.king_jellyfish_cave)
+ self.multiworld.regions.append(self.whale)
+ self.multiworld.regions.append(self.sunken_city_l)
+ self.multiworld.regions.append(self.sunken_city_r)
+ self.multiworld.regions.append(self.sunken_city_boss)
+ self.multiworld.regions.append(self.sunken_city_l_bedroom)
+
+ def __add_body_regions_to_world(self) -> None:
+ """
+ Add every region around the Body to the `world`
+ """
+ self.multiworld.regions.append(self.body_c)
+ self.multiworld.regions.append(self.body_l)
+ self.multiworld.regions.append(self.body_rt)
+ self.multiworld.regions.append(self.body_rb)
+ self.multiworld.regions.append(self.body_b)
+ self.multiworld.regions.append(self.final_boss_loby)
+ self.multiworld.regions.append(self.final_boss_tube)
+ self.multiworld.regions.append(self.final_boss)
+ self.multiworld.regions.append(self.final_boss_end)
+
+ def add_regions_to_world(self) -> None:
+ """
+ Add every region to the `world`
+ """
+ self.__add_home_water_regions_to_world()
+ self.__add_open_water_regions_to_world()
+ self.__add_mithalas_regions_to_world()
+ self.__add_forest_regions_to_world()
+ self.__add_veil_regions_to_world()
+ self.__add_abyss_regions_to_world()
+ self.__add_body_regions_to_world()
+
+ def __init__(self, multiworld: MultiWorld, player: int):
+ """
+ Initialisation of the regions
+ """
+ self.multiworld = multiworld
+ self.player = player
+ self.__create_home_water_area()
+ self.__create_energy_temple()
+ self.__create_openwater()
+ self.__create_mithalas()
+ self.__create_forest()
+ self.__create_veil()
+ self.__create_sun_temple()
+ self.__create_abyss()
+ self.__create_sunken_city()
+ self.__create_body()
diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py
new file mode 100644
index 000000000000..1fb04036d81b
--- /dev/null
+++ b/worlds/aquaria/__init__.py
@@ -0,0 +1,215 @@
+"""
+Author: Louis M
+Date: Fri, 15 Mar 2024 18:41:40 +0000
+Description: Main module for Aquaria game multiworld randomizer
+"""
+
+from typing import List, Dict, ClassVar, Any
+from worlds.AutoWorld import World, WebWorld
+from BaseClasses import Tutorial, MultiWorld, ItemClassification
+from .Items import item_table, AquariaItem, ItemType, ItemGroup
+from .Locations import location_table
+from .Options import AquariaOptions
+from .Regions import AquariaRegions
+
+
+class AquariaWeb(WebWorld):
+ """
+ Class used to generate the Aquaria Game Web pages (setup, tutorial, etc.)
+ """
+ theme = "ocean"
+
+ bug_report_page = "https://github.com/tioui/Aquaria_Randomizer/issues"
+
+ setup = Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to setting up Aquaria for MultiWorld.",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["Tioui"]
+ )
+
+ setup_fr = Tutorial(
+ "Guide de configuration Multimonde",
+ "Un guide pour configurer Aquaria MultiWorld",
+ "Français",
+ "setup_fr.md",
+ "setup/fr",
+ ["Tioui"]
+ )
+
+ tutorials = [setup, setup_fr]
+
+
+class AquariaWorld(World):
+ """
+ Aquaria is a side-scrolling action-adventure game. It follows Naija, an
+ aquatic humanoid woman, as she explores the underwater world of Aquaria.
+ Along her journey, she learns about the history of the world she inhabits
+ as well as her own past. The gameplay focuses on a combination of swimming,
+ singing, and combat, through which Naija can interact with the world. Her
+ songs can move items, affect plants and animals, and change her physical
+ appearance into other forms that have different abilities, like firing
+ projectiles at hostile creatures, or passing through barriers inaccessible
+ to her in her natural form.
+ From: https://en.wikipedia.org/wiki/Aquaria_(video_game)
+ """
+
+ game: str = "Aquaria"
+ "The name of the game"
+
+ topology_present = True
+ "show path to required location checks in spoiler"
+
+ web: WebWorld = AquariaWeb()
+ "The web page generation informations"
+
+ item_name_to_id: ClassVar[Dict[str, int]] =\
+ {name: data.id for name, data in item_table.items()}
+ "The name and associated ID of each item of the world"
+
+ item_name_groups = {
+ "Damage": {"Energy form", "Nature form", "Beast form",
+ "Li and Li song", "Baby Nautilus", "Baby Piranha",
+ "Baby Blaster"},
+ "Light": {"Sun form", "Baby Dumbo"}
+ }
+ """Grouping item make it easier to find them"""
+
+ location_name_to_id = location_table
+ "The name and associated ID of each location of the world"
+
+ base_id = 698000
+ "The starting ID of the items and locations of the world"
+
+ ingredients_substitution: List[int]
+ "Used to randomize ingredient drop"
+
+ options_dataclass = AquariaOptions
+ "Used to manage world options"
+
+ options: AquariaOptions
+ "Every options of the world"
+
+ regions: AquariaRegions
+ "Used to manage Regions"
+
+ exclude: List[str]
+
+ def __init__(self, multiworld: MultiWorld, player: int):
+ """Initialisation of the Aquaria World"""
+ super(AquariaWorld, self).__init__(multiworld, player)
+ self.regions = AquariaRegions(multiworld, player)
+ self.ingredients_substitution = []
+ self.exclude = []
+
+ def create_regions(self) -> None:
+ """
+ Create every Region in `regions`
+ """
+ self.regions.add_regions_to_world()
+ self.regions.connect_regions()
+ self.regions.add_event_locations()
+
+ def create_item(self, name: str) -> AquariaItem:
+ """
+ Create an AquariaItem using 'name' as item name.
+ """
+ result: AquariaItem
+ try:
+ data = item_table[name]
+ classification: ItemClassification = ItemClassification.useful
+ if data.type == ItemType.JUNK:
+ classification = ItemClassification.filler
+ elif data.type == ItemType.PROGRESSION:
+ classification = ItemClassification.progression
+ result = AquariaItem(name, classification, data.id, self.player)
+ except BaseException:
+ raise Exception('The item ' + name + ' is not valid.')
+
+ return result
+
+ def __pre_fill_item(self, item_name: str, location_name: str, precollected) -> None:
+ """Pre-assign an item to a location"""
+ if item_name not in precollected:
+ self.exclude.append(item_name)
+ data = item_table[item_name]
+ item = AquariaItem(item_name, ItemClassification.useful, data.id, self.player)
+ self.multiworld.get_location(location_name, self.player).place_locked_item(item)
+
+ def get_filler_item_name(self):
+ """Getting a random ingredient item as filler"""
+ ingredients = []
+ for name, data in item_table.items():
+ if data.group == ItemGroup.INGREDIENT:
+ ingredients.append(name)
+ filler_item_name = self.random.choice(ingredients)
+ return filler_item_name
+
+ def create_items(self) -> None:
+ """Create every item in the world"""
+ precollected = [item.name for item in self.multiworld.precollected_items[self.player]]
+ if self.options.turtle_randomizer.value > 0:
+ if self.options.turtle_randomizer.value == 2:
+ self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected)
+ else:
+ self.__pre_fill_item("Transturtle Veil top left", "The Veil top left area, Transturtle", precollected)
+ self.__pre_fill_item("Transturtle Veil top right", "The Veil top right area, Transturtle", precollected)
+ self.__pre_fill_item("Transturtle Open Water top right", "Open Water top right area, Transturtle",
+ precollected)
+ self.__pre_fill_item("Transturtle Forest bottom left", "Kelp Forest bottom left area, Transturtle",
+ precollected)
+ self.__pre_fill_item("Transturtle Home Water", "Home Water, Transturtle", precollected)
+ self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected)
+ self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected)
+ # The last two are inverted because in the original game, they are special turtle that communicate directly
+ self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected)
+ self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected)
+ for name, data in item_table.items():
+ if name not in self.exclude:
+ for i in range(data.count):
+ item = self.create_item(name)
+ self.multiworld.itempool.append(item)
+
+ def set_rules(self) -> None:
+ """
+ Launched when the Multiworld generator is ready to generate rules
+ """
+
+ self.regions.adjusting_rules(self.options)
+ self.multiworld.completion_condition[self.player] = lambda \
+ state: state.has("Victory", self.player)
+
+ def generate_basic(self) -> None:
+ """
+ Player-specific randomization that does not affect logic.
+ Used to fill then `ingredients_substitution` list
+ """
+ simple_ingredients_substitution = [i for i in range(27)]
+ if self.options.ingredient_randomizer.value > 0:
+ if self.options.ingredient_randomizer.value == 1:
+ simple_ingredients_substitution.pop(-1)
+ simple_ingredients_substitution.pop(-1)
+ simple_ingredients_substitution.pop(-1)
+ self.random.shuffle(simple_ingredients_substitution)
+ if self.options.ingredient_randomizer.value == 1:
+ simple_ingredients_substitution.extend([24, 25, 26])
+ dishes_substitution = [i for i in range(27, 76)]
+ if self.options.dish_randomizer:
+ self.random.shuffle(dishes_substitution)
+ self.ingredients_substitution.clear()
+ self.ingredients_substitution.extend(simple_ingredients_substitution)
+ self.ingredients_substitution.extend(dishes_substitution)
+
+ def fill_slot_data(self) -> Dict[str, Any]:
+ return {"ingredientReplacement": self.ingredients_substitution,
+ "aquarian_translate": bool(self.options.aquarian_translation.value),
+ "blind_goal": bool(self.options.blind_goal.value),
+ "secret_needed": self.options.objective.value > 0,
+ "minibosses_to_kill": self.options.mini_bosses_to_beat.value,
+ "bigbosses_to_kill": self.options.big_bosses_to_beat.value,
+ "skip_first_vision": bool(self.options.skip_first_vision.value),
+ "unconfine_home_water_energy_door": self.options.unconfine_home_water.value in [1, 3],
+ "unconfine_home_water_transturtle": self.options.unconfine_home_water.value in [2, 3],
+ }
diff --git a/worlds/aquaria/docs/en_Aquaria.md b/worlds/aquaria/docs/en_Aquaria.md
new file mode 100644
index 000000000000..c3e5f54dd66a
--- /dev/null
+++ b/worlds/aquaria/docs/en_Aquaria.md
@@ -0,0 +1,64 @@
+# Aquaria
+
+## Game page in other languages:
+* [Français](/games/Aquaria/info/fr)
+
+## Where is the options page?
+
+The player options page for this game contains all the options you need to configure and export a config file. Player
+options page link: [Aquaria Player Options Page](../player-options).
+
+## What does randomization do to this game?
+The locations in the randomizer are:
+
+- All sing bulbs
+- All Mithalas Urns
+- All Sunken City crates
+- Collectible treasure locations (including pet eggs and costumes)
+- Beating Simon Says
+- Li cave
+- Every Transportation Turtle (also called transturtle)
+- Locations where you get songs:
+ * Erulian spirit crystal
+ * Energy status mini-boss
+ * Beating Mithalan God boss
+ * Fish Cave puzzle
+ * Beating Drunian God boss
+ * Beating Sun God boss
+ * Breaking Li cage in the body
+
+Note that, unlike the vanilla game, when opening sing bulbs, Mithalas urns and Sunken City crates,
+nothing will come out of them. The moment those bulbs, urns and crates are opened, the location is considered checked.
+
+The items in the randomizer are:
+- Dishes (used to learn recipes)*
+- Some ingredients
+- The Wok (third plate used to cook 3-ingredient recipes everywhere)
+- All collectible treasure (including pet eggs and costumes)
+- Li and Li's song
+- All songs (other than Li's song since it is learned when Li is obtained)
+- Transportation to transturtles
+
+Also, there is the option to randomize every ingredient drops (from fishes, monsters
+or plants).
+
+* Note that, unlike in the vanilla game, the recipes for dishes (other than the Sea Loaf)
+cannot be cooked (or learned) before being obtained as randomized items. Also, enemies and plants
+that drop dishes that have not been learned before will drop ingredients of this dish instead.
+
+## What is the goal of the game?
+The goal of the Aquaria game is to beat the creator. You can also add other goals like getting
+secret memories, beating a number of mini-bosses and beating a number of bosses.
+
+## Which items can be in another player's world?
+Any items specified above can be in another player's world.
+
+## What does another world's item look like in Aquaria?
+No visuals are shown when finding locations other than collectible treasure.
+For those treasures, the visual of the treasure is visually unchanged.
+After collecting a location check, a message will be shown to inform the player
+what has been collected and who will receive it.
+
+## When the player receives an item, what happens?
+When you receive an item, a message will pop up to inform you where you received
+the item from and which one it was.
diff --git a/worlds/aquaria/docs/fr_Aquaria.md b/worlds/aquaria/docs/fr_Aquaria.md
new file mode 100644
index 000000000000..4395b6dff95e
--- /dev/null
+++ b/worlds/aquaria/docs/fr_Aquaria.md
@@ -0,0 +1,65 @@
+# Aquaria
+
+## Où se trouve la page des options ?
+
+La [page des options du joueur pour ce jeu](../player-options) contient tous
+les options dont vous avez besoin pour configurer et exporter le fichier.
+
+## Quel est l'effet de la randomisation sur ce jeu ?
+
+Les localisations du "Ransomizer" sont:
+
+- tous les bulbes musicaux;
+- toutes les urnes de Mithalas;
+- toutes les caisses de la cité engloutie;
+- les localisations des trésors de collections (incluant les oeufs d'animaux de compagnie et les costumes);
+- Battre Simom dit;
+- La caverne de Li;
+- Les tortues de transportation (transturtle);
+- Localisation ou on obtient normalement les musiques,
+ * cristal de l'esprit Erulien,
+ * le mini-boss de la statue de l'énergie,
+ * battre le dieu de Mithalas,
+ * résoudre l'énigme de la caverne des poissons,
+ * battre le dieu Drunien,
+ * battre le dieu du soleil,
+ * détruire la cage de Li dans le corps,
+
+À noter que, contrairement au jeu original, lors de l'ouverture d'un bulbe musical, d'une urne de Mithalas ou
+d'une caisse de la cité engloutie, aucun objet n'en sortira. La localisation représentée par l'objet ouvert est reçue
+dès l'ouverture.
+
+Les objets pouvant être obtenus sont:
+- les recettes (permettant d'apprendre les recettes*);
+- certains ingrédients;
+- le Wok (la troisième assiette permettant de cuisiner avec trois ingrédients n'importe où);
+- Tous les trésors de collection (incluant les oeufs d'animal de compagnie et les costumes);
+- Li et la musique de Li;
+- Toutes les musiques (autre que la musique de Li puisque cette dernière est apprise en obtenant Li);
+- Les localisations de transportation.
+
+Il y a également l'option pour mélanger les ingrédients obtenus en éliminant des monstres, des poissons ou des plantes.
+
+*À noter que, contrairement au jeu original, il est impossible de cuisiner une recette qui n'a pas préalablement
+été apprise en obtenant un repas en tant qu'objet. À noter également que les ennemies et plantes qui
+donnent un repas dont la recette n'a pas préalablement été apprise vont donner les ingrédients de cette
+recette.
+
+## Quel est le but de Aquaria ?
+
+Dans Aquaria, le but est de battre le monstre final (le créateur). Il est également possible d'ajouter
+des buts comme obtenir les trois souvenirs secrets, ou devoir battre une quantité de boss ou de mini-boss.
+
+## Quels objets peuvent se trouver dans le monde d'un autre joueur ?
+
+Tous les objets indiqués plus haut peuvent être obtenus à partir du monde d'un autre joueur.
+
+## À quoi ressemble un objet d'un autre monde dans ce jeu
+
+Autre que pour les trésors de collection (dont le visuel demeure inchangé),
+les autres localisations n'ont aucun visuel. Lorsqu'une localisation randomisée est obtenue,
+un message est affiché à l'écran pour indiquer quel objet a été trouvé et pour quel joueur.
+
+## Que se passe-t-il lorsque le joueur reçoit un objet ?
+
+Chaque fois qu'un objet est reçu, un message apparaît à l'écran pour en informer le joueur.
diff --git a/worlds/aquaria/docs/setup_en.md b/worlds/aquaria/docs/setup_en.md
new file mode 100644
index 000000000000..34196757a31c
--- /dev/null
+++ b/worlds/aquaria/docs/setup_en.md
@@ -0,0 +1,115 @@
+# Aquaria Randomizer Setup Guide
+
+## Required Software
+
+- The original Aquaria Game (purchasable from most online game stores)
+- The [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases)
+
+## Optional Software
+
+- For sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
+
+## Installation and execution Procedures
+
+### Windows
+
+First, you should copy the original Aquaria folder game. The randomizer will possibly modify the game so that
+the original game will stop working. Copying the folder will guarantee that the original game keeps on working.
+Also, in Windows, the save files are stored in the Aquaria folder. So copying the Aquaria folder for every Multiworld
+game you play will make sure that every game has its own save game.
+
+Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria game folder. The unzipped files are:
+- aquaria_randomizer.exe
+- OpenAL32.dll
+- override (directory)
+- SDL2.dll
+- usersettings.xml
+- wrap_oal.dll
+- cacert.pem
+
+If there is a conflict between files in the original game folder and the unzipped files, you should overwrite
+the original files with the ones from the unzipped randomizer.
+
+Finally, to launch the randomizer, you must use the command line interface (you can open the command line interface
+by typing `cmd` in the address bar of the Windows File Explorer). Here is the command line used to start the
+randomizer:
+
+```bash
+aquaria_randomizer.exe --name YourName --server theServer:thePort
+```
+
+or, if the room has a password:
+
+```bash
+aquaria_randomizer.exe --name YourName --server theServer:thePort --password thePassword
+```
+
+### Linux when using the AppImage
+
+If you use the AppImage, just copy it into the Aquaria game folder. You then have to make it executable. You
+can do that from command line by using:
+
+```bash
+chmod +x Aquaria_Randomizer-*.AppImage
+```
+
+or by using the Graphical Explorer of your system.
+
+To launch the randomizer, just launch in command line:
+
+```bash
+./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort
+```
+
+or, if the room has a password:
+
+```bash
+./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort --password thePassword
+```
+
+Note that you should not have multiple Aquaria_Randomizer AppImage file in the same folder. If this situation occurs,
+the preceding commands will launch the game multiple times.
+
+### Linux when using the tar file
+
+First, you should copy the original Aquaria folder game. The randomizer will possibly modify the game so that
+the original game will stop working. Copying the folder will guarantee that the original game keeps on working.
+
+Untar the Aquaria randomizer release and copy all extracted files in the Aquaria game folder. The extracted files are:
+- aquaria_randomizer
+- override (directory)
+- usersettings.xml
+- cacert.pem
+
+If there is a conflict between files in the original game folder and the extracted files, you should overwrite
+the original files with the ones from the extracted randomizer files.
+
+Then, you should use your system package manager to install `liblua5`, `libogg`, `libvorbis`, `libopenal` and `libsdl2`.
+On Debian base system (like Ubuntu), you can use the following command:
+
+```bash
+sudo apt install liblua5.1-0-dev libogg-dev libvorbis-dev libopenal-dev libsdl2-dev
+```
+
+Also, if there are certain `.so` files in the original Aquaria game folder (`libgcc_s.so.1`, `libopenal.so.1`,
+`libSDL-1.2.so.0` and `libstdc++.so.6`), you should remove them from the Aquaria Randomizer game folder. Those are
+old libraries that will not work on the recent build of the randomizer.
+
+To launch the randomizer, just launch in command line:
+
+```bash
+./aquaria_randomizer --name YourName --server theServer:thePort
+```
+
+or, if the room has a password:
+
+```bash
+./aquaria_randomizer --name YourName --server theServer:thePort --password thePassword
+```
+
+Note: If you get a permission denied error when using the command line, you can use this command to be
+sure that your executable has executable permission:
+
+```bash
+chmod +x aquaria_randomizer
+```
diff --git a/worlds/aquaria/docs/setup_fr.md b/worlds/aquaria/docs/setup_fr.md
new file mode 100644
index 000000000000..2c34f1e6a50f
--- /dev/null
+++ b/worlds/aquaria/docs/setup_fr.md
@@ -0,0 +1,118 @@
+# Guide de configuration MultiWorld d'Aquaria
+
+## Logiciels nécessaires
+
+- Le jeu Aquaria original (trouvable sur la majorité des sites de ventes de jeux vidéo en ligne)
+- Le client Randomizer d'Aquaria [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases)
+- De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
+
+## Procédures d'installation et d'exécution
+
+### Windows
+
+En premier lieu, vous devriez effectuer une nouvelle copie du jeu d'Aquaria original à chaque fois que vous effectuez une
+nouvelle partie. La première raison de cette copie est que le randomizer modifie des fichiers qui rendront possiblement
+le jeu original non fonctionnel. La seconde raison d'effectuer cette copie est que les sauvegardes sont créées
+directement dans le répertoire du jeu. Donc, la copie permet d'éviter de perdre vos sauvegardes du jeu d'origine ou
+encore de charger une sauvegarde d'une ancienne partie de multiworld (ce qui pourrait avoir comme conséquence de briser
+la logique du multiworld).
+
+Désarchiver le randomizer d'Aquaria et copier tous les fichiers de l'archive dans le répertoire du jeu d'Aquaria. Le
+fichier d'archive devrait contenir les fichiers suivants:
+- aquaria_randomizer.exe
+- OpenAL32.dll
+- override (directory)
+- SDL2.dll
+- usersettings.xml
+- wrap_oal.dll
+- cacert.pem
+
+S'il y a des conflits entre les fichiers de l'archive zip et les fichiers du jeu original, vous devez utiliser
+les fichiers contenus dans l'archive zip.
+
+Finalement, pour lancer le randomizer, vous devez utiliser la ligne de commande (vous pouvez ouvrir une interface de
+ligne de commande, entrez l'adresse `cmd` dans la barre d'adresse de l'explorateur de fichier de Windows). Voici
+la ligne de commande à utiliser pour lancer le randomizer:
+
+```bash
+aquaria_randomizer.exe --name VotreNom --server leServeur:LePort
+```
+
+ou, si vous devez entrer un mot de passe:
+
+```bash
+aquaria_randomizer.exe --name VotreNom --server leServeur:LePort --password leMotDePasse
+```
+
+### Linux avec le fichier AppImage
+
+Si vous utilisez le fichier AppImage, copiez le fichier dans le répertoire du jeu d'Aquaria. Ensuite, assurez-vous de
+le mettre exécutable. Vous pouvez mettre le fichier exécutable avec la commande suivante:
+
+```bash
+chmod +x Aquaria_Randomizer-*.AppImage
+```
+
+ou bien en utilisant l'explorateur graphique de votre système.
+
+Pour lancer le randomizer, utiliser la commande suivante:
+
+```bash
+./Aquaria_Randomizer-*.AppImage --name VotreNom --server LeServeur:LePort
+```
+
+Si vous devez entrer un mot de passe:
+
+```bash
+./Aquaria_Randomizer-*.AppImage --name VotreNom --server LeServeur:LePort --password LeMotDePasse
+```
+
+À noter que vous ne devez pas avoir plusieurs fichiers AppImage différents dans le même répertoire. Si cette situation
+survient, le jeu sera lancé plusieurs fois.
+
+### Linux avec le fichier tar
+
+En premier lieu, assurez-vous de faire une copie du répertoire du jeu d'origine d'Aquaria. Les fichiers contenus
+dans le randomizer auront comme impact de rendre le jeu d'origine non fonctionnel. Donc, effectuer la copie du jeu
+avant de déposer le randomizer à l'intérieur permet de vous assurer de garder une version du jeu d'origine fonctionnel.
+
+Désarchiver le fichier tar et copier tous les fichiers qu'il contient dans le répertoire du jeu d'origine d'Aquaria. Les
+fichiers extraient du fichier tar devraient être les suivants:
+- aquaria_randomizer
+- override (directory)
+- usersettings.xml
+- cacert.pem
+
+S'il y a des conflits entre les fichiers de l'archive tar et les fichiers du jeu original, vous devez utiliser
+les fichiers contenus dans l'archive tar.
+
+Ensuite, vous devez installer manuellement les librairies dont dépend le jeu: liblua5, libogg, libvorbis, libopenal and
+libsdl2. Vous pouvez utiliser le système de "package" de votre système pour les installer. Voici un exemple avec
+Debian (et Ubuntu):
+
+```bash
+sudo apt install liblua5.1-0-dev libogg-dev libvorbis-dev libopenal-dev libsdl2-dev
+```
+
+Notez également que s'il y a des fichiers ".so" dans le répertoire d'Aquaria (`libgcc_s.so.1`, `libopenal.so.1`,
+`libSDL-1.2.so.0` and `libstdc++.so.6`), vous devriez les retirer. Il s'agit de vieille version des librairies qui
+ne sont plus fonctionnelles dans les systèmes modernes et qui pourrait empêcher le randomizer de fonctionner.
+
+Pour lancer le randomizer, utiliser la commande suivante:
+
+```bash
+./aquaria_randomizer --name VotreNom --server LeServeur:LePort
+```
+
+Si vous devez entrer un mot de passe:
+
+```bash
+./aquaria_randomizer --name VotreNom --server LeServeur:LePort --password LeMotDePasse
+```
+
+Note: Si vous avez une erreur de permission lors de l'exécution du randomizer, vous pouvez utiliser cette commande
+pour vous assurer que votre fichier est exécutable:
+
+```bash
+chmod +x aquaria_randomizer
+```
diff --git a/worlds/aquaria/test/__init__.py b/worlds/aquaria/test/__init__.py
new file mode 100644
index 000000000000..029db691b66b
--- /dev/null
+++ b/worlds/aquaria/test/__init__.py
@@ -0,0 +1,218 @@
+"""
+Author: Louis M
+Date: Thu, 18 Apr 2024 18:45:56 +0000
+Description: Base class for the Aquaria randomizer unit tests
+"""
+
+
+from test.bases import WorldTestBase
+
+# Every location accessible after the home water.
+after_home_water_locations = [
+ "Sun Crystal",
+ "Home Water, Transturtle",
+ "Open Water top left area, bulb under the rock in the right path",
+ "Open Water top left area, bulb under the rock in the left path",
+ "Open Water top left area, bulb to the right of the save crystal",
+ "Open Water top right area, bulb in the small path before Mithalas",
+ "Open Water top right area, bulb in the path from the left entrance",
+ "Open Water top right area, bulb in the clearing close to the bottom exit",
+ "Open Water top right area, bulb in the big clearing close to the save crystal",
+ "Open Water top right area, bulb in the big clearing to the top exit",
+ "Open Water top right area, first urn in the Mithalas exit",
+ "Open Water top right area, second urn in the Mithalas exit",
+ "Open Water top right area, third urn in the Mithalas exit",
+ "Open Water top right area, bulb in the turtle room",
+ "Open Water top right area, Transturtle",
+ "Open Water bottom left area, bulb behind the chomper fish",
+ "Open Water bottom left area, bulb inside the lowest fish pass",
+ "Open Water skeleton path, bulb close to the right exit",
+ "Open Water skeleton path, bulb behind the chomper fish",
+ "Open Water skeleton path, King Skull",
+ "Arnassi Ruins, bulb in the right part",
+ "Arnassi Ruins, bulb in the left part",
+ "Arnassi Ruins, bulb in the center part",
+ "Arnassi Ruins, Song Plant Spore",
+ "Arnassi Ruins, Arnassi Armor",
+ "Arnassi Ruins, Arnassi Statue",
+ "Arnassi Ruins, Transturtle",
+ "Arnassi Ruins, Crab Armor",
+ "Simon Says area, Transturtle",
+ "Mithalas City, first bulb in the left city part",
+ "Mithalas City, second bulb in the left city part",
+ "Mithalas City, bulb in the right part",
+ "Mithalas City, bulb at the top of the city",
+ "Mithalas City, first bulb in a broken home",
+ "Mithalas City, second bulb in a broken home",
+ "Mithalas City, bulb in the bottom left part",
+ "Mithalas City, first bulb in one of the homes",
+ "Mithalas City, second bulb in one of the homes",
+ "Mithalas City, first urn in one of the homes",
+ "Mithalas City, second urn in one of the homes",
+ "Mithalas City, first urn in the city reserve",
+ "Mithalas City, second urn in the city reserve",
+ "Mithalas City, third urn in the city reserve",
+ "Mithalas City, first bulb at the end of the top path",
+ "Mithalas City, second bulb at the end of the top path",
+ "Mithalas City, bulb in the top path",
+ "Mithalas City, Mithalas Pot",
+ "Mithalas City, urn in the Castle flower tube entrance",
+ "Mithalas City, Doll",
+ "Mithalas City, urn inside a home fish pass",
+ "Mithalas City Castle, bulb in the flesh hole",
+ "Mithalas City Castle, Blue Banner",
+ "Mithalas City Castle, urn in the bedroom",
+ "Mithalas City Castle, first urn of the single lamp path",
+ "Mithalas City Castle, second urn of the single lamp path",
+ "Mithalas City Castle, urn in the bottom room",
+ "Mithalas City Castle, first urn on the entrance path",
+ "Mithalas City Castle, second urn on the entrance path",
+ "Mithalas City Castle, beating the Priests",
+ "Mithalas City Castle, Trident Head",
+ "Mithalas Cathedral, first urn in the top right room",
+ "Mithalas Cathedral, second urn in the top right room",
+ "Mithalas Cathedral, third urn in the top right room",
+ "Mithalas Cathedral, urn in the flesh room with fleas",
+ "Mithalas Cathedral, first urn in the bottom right path",
+ "Mithalas Cathedral, second urn in the bottom right path",
+ "Mithalas Cathedral, urn behind the flesh vein",
+ "Mithalas Cathedral, urn in the top left eyes boss room",
+ "Mithalas Cathedral, first urn in the path behind the flesh vein",
+ "Mithalas Cathedral, second urn in the path behind the flesh vein",
+ "Mithalas Cathedral, third urn in the path behind the flesh vein",
+ "Mithalas Cathedral, fourth urn in the top right room",
+ "Mithalas Cathedral, Mithalan Dress",
+ "Mithalas Cathedral, urn below the left entrance",
+ "Cathedral Underground, bulb in the center part",
+ "Cathedral Underground, first bulb in the top left part",
+ "Cathedral Underground, second bulb in the top left part",
+ "Cathedral Underground, third bulb in the top left part",
+ "Cathedral Underground, bulb close to the save crystal",
+ "Cathedral Underground, bulb in the bottom right path",
+ "Mithalas boss area, beating Mithalan God",
+ "Kelp Forest top left area, bulb in the bottom left clearing",
+ "Kelp Forest top left area, bulb in the path down from the top left clearing",
+ "Kelp Forest top left area, bulb in the top left clearing",
+ "Kelp Forest top left area, Jelly Egg",
+ "Kelp Forest top left area, bulb close to the Verse Egg",
+ "Kelp Forest top left area, Verse Egg",
+ "Kelp Forest top right area, bulb under the rock in the right path",
+ "Kelp Forest top right area, bulb at the left of the center clearing",
+ "Kelp Forest top right area, bulb in the left path's big room",
+ "Kelp Forest top right area, bulb in the left path's small room",
+ "Kelp Forest top right area, bulb at the top of the center clearing",
+ "Kelp Forest top right area, Black Pearl",
+ "Kelp Forest top right area, bulb in the top fish pass",
+ "Kelp Forest bottom left area, bulb close to the spirit crystals",
+ "Kelp Forest bottom left area, Walker Baby",
+ "Kelp Forest bottom left area, Transturtle",
+ "Kelp Forest bottom right area, Odd Container",
+ "Kelp Forest boss area, beating Drunian God",
+ "Kelp Forest boss room, bulb at the bottom of the area",
+ "Kelp Forest bottom left area, Fish Cave puzzle",
+ "Kelp Forest sprite cave, bulb inside the fish pass",
+ "Kelp Forest sprite cave, bulb in the second room",
+ "Kelp Forest sprite cave, Seed Bag",
+ "Mermog cave, bulb in the left part of the cave",
+ "Mermog cave, Piranha Egg",
+ "The Veil top left area, In Li's cave",
+ "The Veil top left area, bulb under the rock in the top right path",
+ "The Veil top left area, bulb hidden behind the blocking rock",
+ "The Veil top left area, Transturtle",
+ "The Veil top left area, bulb inside the fish pass",
+ "Turtle cave, Turtle Egg",
+ "Turtle cave, bulb in Bubble Cliff",
+ "Turtle cave, Urchin Costume",
+ "The Veil top right area, bulb in the middle of the wall jump cliff",
+ "The Veil top right area, Golden Starfish",
+ "The Veil top right area, bulb at the top of the waterfall",
+ "The Veil top right area, Transturtle",
+ "The Veil bottom area, bulb in the left path",
+ "The Veil bottom area, bulb in the spirit path",
+ "The Veil bottom area, Verse Egg",
+ "The Veil bottom area, Stone Head",
+ "Octopus Cave, Dumbo Egg",
+ "Octopus Cave, bulb in the path below the Octopus Cave path",
+ "Bubble Cave, bulb in the left cave wall",
+ "Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
+ "Bubble Cave, Verse Egg",
+ "Sun Temple, bulb in the top left part",
+ "Sun Temple, bulb in the top right part",
+ "Sun Temple, bulb at the top of the high dark room",
+ "Sun Temple, Golden Gear",
+ "Sun Temple, first bulb of the temple",
+ "Sun Temple, bulb on the left part",
+ "Sun Temple, bulb in the hidden room of the right part",
+ "Sun Temple, Sun Key",
+ "Sun Worm path, first path bulb",
+ "Sun Worm path, second path bulb",
+ "Sun Worm path, first cliff bulb",
+ "Sun Worm path, second cliff bulb",
+ "Sun Temple boss area, beating Sun God",
+ "Abyss left area, bulb in hidden path room",
+ "Abyss left area, bulb in the right part",
+ "Abyss left area, Glowing Seed",
+ "Abyss left area, Glowing Plant",
+ "Abyss left area, bulb in the bottom fish pass",
+ "Abyss right area, bulb behind the rock in the whale room",
+ "Abyss right area, bulb in the middle path",
+ "Abyss right area, bulb behind the rock in the middle path",
+ "Abyss right area, bulb in the left green room",
+ "Abyss right area, Transturtle",
+ "Ice Cave, bulb in the room to the right",
+ "Ice Cave, first bulb in the top exit room",
+ "Ice Cave, second bulb in the top exit room",
+ "Ice Cave, third bulb in the top exit room",
+ "Ice Cave, bulb in the left room",
+ "King Jellyfish Cave, bulb in the right path from King Jelly",
+ "King Jellyfish Cave, Jellyfish Costume",
+ "The Whale, Verse Egg",
+ "Sunken City right area, crate close to the save crystal",
+ "Sunken City right area, crate in the left bottom room",
+ "Sunken City left area, crate in the little pipe room",
+ "Sunken City left area, crate close to the save crystal",
+ "Sunken City left area, crate before the bedroom",
+ "Sunken City left area, Girl Costume",
+ "Sunken City, bulb on top of the boss area",
+ "The Body center area, breaking Li's cage",
+ "The Body center area, bulb on the main path blocking tube",
+ "The Body left area, first bulb in the top face room",
+ "The Body left area, second bulb in the top face room",
+ "The Body left area, bulb below the water stream",
+ "The Body left area, bulb in the top path to the top face room",
+ "The Body left area, bulb in the bottom face room",
+ "The Body right area, bulb in the top face room",
+ "The Body right area, bulb in the top path to the bottom face room",
+ "The Body right area, bulb in the bottom face room",
+ "The Body bottom area, bulb in the Jelly Zap room",
+ "The Body bottom area, bulb in the nautilus room",
+ "The Body bottom area, Mutant Costume",
+ "Final Boss area, first bulb in the turtle room",
+ "Final Boss area, second bulb in the turtle room",
+ "Final Boss area, third bulb in the turtle room",
+ "Final Boss area, Transturtle",
+ "Final Boss area, bulb in the boss third form room",
+ "Simon Says area, beating Simon Says",
+ "Beating Fallen God",
+ "Beating Mithalan God",
+ "Beating Drunian God",
+ "Beating Sun God",
+ "Beating the Golem",
+ "Beating Nautilus Prime",
+ "Beating Blaster Peg Prime",
+ "Beating Mergog",
+ "Beating Mithalan priests",
+ "Beating Octopus Prime",
+ "Beating Crabbius Maximus",
+ "Beating Mantis Shrimp Prime",
+ "Beating King Jellyfish God Prime",
+ "First secret",
+ "Second secret",
+ "Third secret",
+ "Sunken City cleared",
+ "Objective complete",
+]
+
+class AquariaTestBase(WorldTestBase):
+ """Base class for Aquaria unit tests"""
+ game = "Aquaria"
diff --git a/worlds/aquaria/test/test_beast_form_access.py b/worlds/aquaria/test/test_beast_form_access.py
new file mode 100644
index 000000000000..0efc3e7388fe
--- /dev/null
+++ b/worlds/aquaria/test/test_beast_form_access.py
@@ -0,0 +1,48 @@
+"""
+Author: Louis M
+Date: Thu, 18 Apr 2024 18:45:56 +0000
+Description: Unit test used to test accessibility of locations with and without the beast form
+"""
+
+from . import AquariaTestBase
+
+
+class BeastFormAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of locations with and without the beast form"""
+
+ def test_beast_form_location(self) -> None:
+ """Test locations that require beast form"""
+ locations = [
+ "Mithalas City Castle, beating the Priests",
+ "Arnassi Ruins, Crab Armor",
+ "Arnassi Ruins, Song Plant Spore",
+ "Mithalas City, first bulb at the end of the top path",
+ "Mithalas City, second bulb at the end of the top path",
+ "Mithalas City, bulb in the top path",
+ "Mithalas City, Mithalas Pot",
+ "Mithalas City, urn in the Castle flower tube entrance",
+ "Mermog cave, Piranha Egg",
+ "Mithalas Cathedral, Mithalan Dress",
+ "Turtle cave, bulb in Bubble Cliff",
+ "Turtle cave, Urchin Costume",
+ "Sun Worm path, first cliff bulb",
+ "Sun Worm path, second cliff bulb",
+ "The Veil top right area, bulb at the top of the waterfall",
+ "Bubble Cave, bulb in the left cave wall",
+ "Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
+ "Bubble Cave, Verse Egg",
+ "Sunken City, bulb on top of the boss area",
+ "Octopus Cave, Dumbo Egg",
+ "Beating the Golem",
+ "Beating Mergog",
+ "Beating Crabbius Maximus",
+ "Beating Octopus Prime",
+ "Beating Mantis Shrimp Prime",
+ "King Jellyfish Cave, Jellyfish Costume",
+ "King Jellyfish Cave, bulb in the right path from King Jelly",
+ "Beating King Jellyfish God Prime",
+ "Beating Mithalan priests",
+ "Sunken City cleared"
+ ]
+ items = [["Beast form"]]
+ self.assertAccessDependency(locations, items)
diff --git a/worlds/aquaria/test/test_bind_song_access.py b/worlds/aquaria/test/test_bind_song_access.py
new file mode 100644
index 000000000000..05f96edb9192
--- /dev/null
+++ b/worlds/aquaria/test/test_bind_song_access.py
@@ -0,0 +1,36 @@
+"""
+Author: Louis M
+Date: Thu, 18 Apr 2024 18:45:56 +0000
+Description: Unit test used to test accessibility of locations with and without the bind song (without the location
+ under rock needing bind song option)
+"""
+
+from . import AquariaTestBase, after_home_water_locations
+
+
+class BindSongAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of locations with and without the bind song"""
+ options = {
+ "bind_song_needed_to_get_under_rock_bulb": False,
+ }
+
+ def test_bind_song_location(self) -> None:
+ """Test locations that require Bind song"""
+ locations = [
+ "Verse Cave right area, Big Seed",
+ "Home Water, bulb in the path below Nautilus Prime",
+ "Home Water, bulb in the bottom left room",
+ "Home Water, Nautilus Egg",
+ "Song Cave, Verse Egg",
+ "Energy Temple first area, beating the Energy Statue",
+ "Energy Temple first area, bulb in the bottom room blocked by a rock",
+ "Energy Temple first area, Energy Idol",
+ "Energy Temple second area, bulb under the rock",
+ "Energy Temple bottom entrance, Krotite Armor",
+ "Energy Temple third area, bulb in the bottom path",
+ "Energy Temple boss area, Fallen God Tooth",
+ "Energy Temple blaster room, Blaster Egg",
+ *after_home_water_locations
+ ]
+ items = [["Bind song"]]
+ self.assertAccessDependency(locations, items)
diff --git a/worlds/aquaria/test/test_bind_song_option_access.py b/worlds/aquaria/test/test_bind_song_option_access.py
new file mode 100644
index 000000000000..e391eef101bf
--- /dev/null
+++ b/worlds/aquaria/test/test_bind_song_option_access.py
@@ -0,0 +1,42 @@
+"""
+Author: Louis M
+Date: Thu, 18 Apr 2024 18:45:56 +0000
+Description: Unit test used to test accessibility of locations with and without the bind song (with the location
+ under rock needing bind song option)
+"""
+
+from . import AquariaTestBase
+from .test_bind_song_access import after_home_water_locations
+
+
+class BindSongOptionAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of locations with and without the bind song"""
+ options = {
+ "bind_song_needed_to_get_under_rock_bulb": True,
+ }
+
+ def test_bind_song_location(self) -> None:
+ """Test locations that require Bind song with the bind song needed option activated"""
+ locations = [
+ "Verse Cave right area, Big Seed",
+ "Verse Cave left area, bulb under the rock at the end of the path",
+ "Home Water, bulb under the rock in the left path from the Verse Cave",
+ "Song Cave, bulb under the rock close to the song door",
+ "Song Cave, bulb under the rock in the path to the singing statues",
+ "Naija's Home, bulb under the rock at the right of the main path",
+ "Home Water, bulb in the path below Nautilus Prime",
+ "Home Water, bulb in the bottom left room",
+ "Home Water, Nautilus Egg",
+ "Song Cave, Verse Egg",
+ "Energy Temple first area, beating the Energy Statue",
+ "Energy Temple first area, bulb in the bottom room blocked by a rock",
+ "Energy Temple first area, Energy Idol",
+ "Energy Temple second area, bulb under the rock",
+ "Energy Temple bottom entrance, Krotite Armor",
+ "Energy Temple third area, bulb in the bottom path",
+ "Energy Temple boss area, Fallen God Tooth",
+ "Energy Temple blaster room, Blaster Egg",
+ *after_home_water_locations
+ ]
+ items = [["Bind song"]]
+ self.assertAccessDependency(locations, items)
diff --git a/worlds/aquaria/test/test_confined_home_water.py b/worlds/aquaria/test/test_confined_home_water.py
new file mode 100644
index 000000000000..89c51ac5c775
--- /dev/null
+++ b/worlds/aquaria/test/test_confined_home_water.py
@@ -0,0 +1,20 @@
+"""
+Author: Louis M
+Date: Fri, 03 May 2024 14:07:35 +0000
+Description: Unit test used to test accessibility of region with the home water confine via option
+"""
+
+from . import AquariaTestBase
+
+
+class ConfinedHomeWaterAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of region with the unconfine home water option disabled"""
+ options = {
+ "unconfine_home_water": 0,
+ "early_energy_form": False
+ }
+
+ def test_confine_home_water_location(self) -> None:
+ """Test region accessible with confined home water"""
+ self.assertFalse(self.can_reach_region("Open Water top left area"), "Can reach Open Water top left area")
+ self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room")
diff --git a/worlds/aquaria/test/test_dual_song_access.py b/worlds/aquaria/test/test_dual_song_access.py
new file mode 100644
index 000000000000..bb9b2e739604
--- /dev/null
+++ b/worlds/aquaria/test/test_dual_song_access.py
@@ -0,0 +1,26 @@
+"""
+Author: Louis M
+Date: Thu, 18 Apr 2024 18:45:56 +0000
+Description: Unit test used to test accessibility of locations with and without the dual song
+"""
+
+from . import AquariaTestBase
+
+
+class LiAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of locations with and without the dual song"""
+ options = {
+ "turtle_randomizer": 1,
+ }
+
+ def test_li_song_location(self) -> None:
+ """Test locations that require the dual song"""
+ locations = [
+ "The Body bottom area, bulb in the Jelly Zap room",
+ "The Body bottom area, bulb in the nautilus room",
+ "The Body bottom area, Mutant Costume",
+ "Final Boss area, bulb in the boss third form room",
+ "Objective complete"
+ ]
+ items = [["Dual form"]]
+ self.assertAccessDependency(locations, items)
diff --git a/worlds/aquaria/test/test_energy_form_access.py b/worlds/aquaria/test/test_energy_form_access.py
new file mode 100644
index 000000000000..82d8e89a0066
--- /dev/null
+++ b/worlds/aquaria/test/test_energy_form_access.py
@@ -0,0 +1,72 @@
+"""
+Author: Louis M
+Date: Thu, 18 Apr 2024 18:45:56 +0000
+Description: Unit test used to test accessibility of locations with and without the bind song (without the early
+ energy form option)
+"""
+
+from . import AquariaTestBase
+
+
+class EnergyFormAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of locations with and without the energy form"""
+ options = {
+ "early_energy_form": False,
+ }
+
+ def test_energy_form_location(self) -> None:
+ """Test locations that require Energy form"""
+ locations = [
+ "Home Water, Nautilus Egg",
+ "Naija's Home, bulb after the energy door",
+ "Energy Temple first area, bulb in the bottom room blocked by a rock",
+ "Energy Temple second area, bulb under the rock",
+ "Energy Temple bottom entrance, Krotite Armor",
+ "Energy Temple third area, bulb in the bottom path",
+ "Energy Temple boss area, Fallen God Tooth",
+ "Energy Temple blaster room, Blaster Egg",
+ "Mithalas City Castle, beating the Priests",
+ "Mithalas Cathedral, first urn in the top right room",
+ "Mithalas Cathedral, second urn in the top right room",
+ "Mithalas Cathedral, third urn in the top right room",
+ "Mithalas Cathedral, urn in the flesh room with fleas",
+ "Mithalas Cathedral, first urn in the bottom right path",
+ "Mithalas Cathedral, second urn in the bottom right path",
+ "Mithalas Cathedral, urn behind the flesh vein",
+ "Mithalas Cathedral, urn in the top left eyes boss room",
+ "Mithalas Cathedral, first urn in the path behind the flesh vein",
+ "Mithalas Cathedral, second urn in the path behind the flesh vein",
+ "Mithalas Cathedral, third urn in the path behind the flesh vein",
+ "Mithalas Cathedral, fourth urn in the top right room",
+ "Mithalas Cathedral, Mithalan Dress",
+ "Mithalas Cathedral, urn below the left entrance",
+ "Mithalas boss area, beating Mithalan God",
+ "Kelp Forest top left area, bulb close to the Verse Egg",
+ "Kelp Forest top left area, Verse Egg",
+ "Kelp Forest boss area, beating Drunian God",
+ "Mermog cave, Piranha Egg",
+ "Octopus Cave, Dumbo Egg",
+ "Sun Temple boss area, beating Sun God",
+ "Arnassi Ruins, Crab Armor",
+ "King Jellyfish Cave, bulb in the right path from King Jelly",
+ "King Jellyfish Cave, Jellyfish Costume",
+ "Sunken City, bulb on top of the boss area",
+ "Final Boss area, bulb in the boss third form room",
+ "Beating Fallen God",
+ "Beating Mithalan God",
+ "Beating Drunian God",
+ "Beating Sun God",
+ "Beating the Golem",
+ "Beating Nautilus Prime",
+ "Beating Blaster Peg Prime",
+ "Beating Mergog",
+ "Beating Mithalan priests",
+ "Beating Octopus Prime",
+ "Beating Crabbius Maximus",
+ "Beating King Jellyfish God Prime",
+ "First secret",
+ "Sunken City cleared",
+ "Objective complete",
+ ]
+ items = [["Energy form"]]
+ self.assertAccessDependency(locations, items)
diff --git a/worlds/aquaria/test/test_fish_form_access.py b/worlds/aquaria/test/test_fish_form_access.py
new file mode 100644
index 000000000000..c98a53e92438
--- /dev/null
+++ b/worlds/aquaria/test/test_fish_form_access.py
@@ -0,0 +1,37 @@
+"""
+Author: Louis M
+Date: Thu, 18 Apr 2024 18:45:56 +0000
+Description: Unit test used to test accessibility of locations with and without the fish form
+"""
+
+from . import AquariaTestBase
+
+
+class FishFormAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of locations with and without the fish form"""
+ options = {
+ "turtle_randomizer": 1,
+ }
+
+ def test_fish_form_location(self) -> None:
+ """Test locations that require fish form"""
+ locations = [
+ "The Veil top left area, bulb inside the fish pass",
+ "Mithalas City, Doll",
+ "Mithalas City, urn inside a home fish pass",
+ "Kelp Forest top right area, bulb in the top fish pass",
+ "The Veil bottom area, Verse Egg",
+ "Open Water bottom left area, bulb inside the lowest fish pass",
+ "Kelp Forest top left area, bulb close to the Verse Egg",
+ "Kelp Forest top left area, Verse Egg",
+ "Mermog cave, bulb in the left part of the cave",
+ "Mermog cave, Piranha Egg",
+ "Beating Mergog",
+ "Octopus Cave, Dumbo Egg",
+ "Octopus Cave, bulb in the path below the Octopus Cave path",
+ "Beating Octopus Prime",
+ "Abyss left area, bulb in the bottom fish pass",
+ "Arnassi Ruins, Arnassi Armor"
+ ]
+ items = [["Fish form"]]
+ self.assertAccessDependency(locations, items)
diff --git a/worlds/aquaria/test/test_li_song_access.py b/worlds/aquaria/test/test_li_song_access.py
new file mode 100644
index 000000000000..f615fb10c640
--- /dev/null
+++ b/worlds/aquaria/test/test_li_song_access.py
@@ -0,0 +1,45 @@
+"""
+Author: Louis M
+Date: Thu, 18 Apr 2024 18:45:56 +0000
+Description: Unit test used to test accessibility of locations with and without Li
+"""
+
+from . import AquariaTestBase
+
+
+class LiAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of locations with and without Li"""
+ options = {
+ "turtle_randomizer": 1,
+ }
+
+ def test_li_song_location(self) -> None:
+ """Test locations that require Li"""
+ locations = [
+ "Sunken City right area, crate close to the save crystal",
+ "Sunken City right area, crate in the left bottom room",
+ "Sunken City left area, crate in the little pipe room",
+ "Sunken City left area, crate close to the save crystal",
+ "Sunken City left area, crate before the bedroom",
+ "Sunken City left area, Girl Costume",
+ "Sunken City, bulb on top of the boss area",
+ "The Body center area, breaking Li's cage",
+ "The Body center area, bulb on the main path blocking tube",
+ "The Body left area, first bulb in the top face room",
+ "The Body left area, second bulb in the top face room",
+ "The Body left area, bulb below the water stream",
+ "The Body left area, bulb in the top path to the top face room",
+ "The Body left area, bulb in the bottom face room",
+ "The Body right area, bulb in the top face room",
+ "The Body right area, bulb in the top path to the bottom face room",
+ "The Body right area, bulb in the bottom face room",
+ "The Body bottom area, bulb in the Jelly Zap room",
+ "The Body bottom area, bulb in the nautilus room",
+ "The Body bottom area, Mutant Costume",
+ "Final Boss area, bulb in the boss third form room",
+ "Beating the Golem",
+ "Sunken City cleared",
+ "Objective complete"
+ ]
+ items = [["Li and Li song", "Body tongue cleared"]]
+ self.assertAccessDependency(locations, items)
diff --git a/worlds/aquaria/test/test_light_access.py b/worlds/aquaria/test/test_light_access.py
new file mode 100644
index 000000000000..b5d7cf99fea2
--- /dev/null
+++ b/worlds/aquaria/test/test_light_access.py
@@ -0,0 +1,71 @@
+"""
+Author: Louis M
+Date: Thu, 18 Apr 2024 18:45:56 +0000
+Description: Unit test used to test accessibility of locations with and without a light (Dumbo pet or sun form)
+"""
+
+from . import AquariaTestBase
+
+
+class LightAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of locations with and without light"""
+ options = {
+ "turtle_randomizer": 1,
+ "light_needed_to_get_to_dark_places": True,
+ }
+
+ def test_light_location(self) -> None:
+ """Test locations that require light"""
+ locations = [
+ # Since the `assertAccessDependency` sweep for events even if I tell it not to, those location cannot be
+ # tested.
+ # "Third secret",
+ # "Sun Temple, bulb in the top left part",
+ # "Sun Temple, bulb in the top right part",
+ # "Sun Temple, bulb at the top of the high dark room",
+ # "Sun Temple, Golden Gear",
+ # "Sun Worm path, first path bulb",
+ # "Sun Worm path, second path bulb",
+ # "Sun Worm path, first cliff bulb",
+ "Octopus Cave, Dumbo Egg",
+ "Kelp Forest bottom right area, Odd Container",
+ "Kelp Forest top right area, Black Pearl",
+ "Abyss left area, bulb in hidden path room",
+ "Abyss left area, bulb in the right part",
+ "Abyss left area, Glowing Seed",
+ "Abyss left area, Glowing Plant",
+ "Abyss left area, bulb in the bottom fish pass",
+ "Abyss right area, bulb behind the rock in the whale room",
+ "Abyss right area, bulb in the middle path",
+ "Abyss right area, bulb behind the rock in the middle path",
+ "Abyss right area, bulb in the left green room",
+ "Abyss right area, Transturtle",
+ "Ice Cave, bulb in the room to the right",
+ "Ice Cave, first bulb in the top exit room",
+ "Ice Cave, second bulb in the top exit room",
+ "Ice Cave, third bulb in the top exit room",
+ "Ice Cave, bulb in the left room",
+ "Bubble Cave, bulb in the left cave wall",
+ "Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
+ "Bubble Cave, Verse Egg",
+ "Beating Mantis Shrimp Prime",
+ "King Jellyfish Cave, bulb in the right path from King Jelly",
+ "King Jellyfish Cave, Jellyfish Costume",
+ "Beating King Jellyfish God Prime",
+ "The Whale, Verse Egg",
+ "First secret",
+ "Sunken City right area, crate close to the save crystal",
+ "Sunken City right area, crate in the left bottom room",
+ "Sunken City left area, crate in the little pipe room",
+ "Sunken City left area, crate close to the save crystal",
+ "Sunken City left area, crate before the bedroom",
+ "Sunken City left area, Girl Costume",
+ "Sunken City, bulb on top of the boss area",
+ "Sunken City cleared",
+ "Beating the Golem",
+ "Beating Octopus Prime",
+ "Final Boss area, bulb in the boss third form room",
+ "Objective complete",
+ ]
+ items = [["Sun form", "Baby Dumbo", "Has sun crystal"]]
+ self.assertAccessDependency(locations, items)
diff --git a/worlds/aquaria/test/test_nature_form_access.py b/worlds/aquaria/test/test_nature_form_access.py
new file mode 100644
index 000000000000..1d3b8f4150eb
--- /dev/null
+++ b/worlds/aquaria/test/test_nature_form_access.py
@@ -0,0 +1,57 @@
+"""
+Author: Louis M
+Date: Thu, 18 Apr 2024 18:45:56 +0000
+Description: Unit test used to test accessibility of locations with and without the nature form
+"""
+
+from . import AquariaTestBase
+
+
+class NatureFormAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of locations with and without the nature form"""
+ options = {
+ "turtle_randomizer": 1,
+ }
+
+ def test_nature_form_location(self) -> None:
+ """Test locations that require nature form"""
+ locations = [
+ "Song Cave, Anemone Seed",
+ "Energy Temple blaster room, Blaster Egg",
+ "Beating Blaster Peg Prime",
+ "Kelp Forest top left area, Verse Egg",
+ "Kelp Forest top left area, bulb close to the Verse Egg",
+ "Mithalas City Castle, beating the Priests",
+ "Kelp Forest sprite cave, bulb in the second room",
+ "Kelp Forest sprite cave, Seed Bag",
+ "Beating Mithalan priests",
+ "Abyss left area, bulb in the bottom fish pass",
+ "Bubble Cave, Verse Egg",
+ "Beating Mantis Shrimp Prime",
+ "Sunken City right area, crate close to the save crystal",
+ "Sunken City right area, crate in the left bottom room",
+ "Sunken City left area, crate in the little pipe room",
+ "Sunken City left area, crate close to the save crystal",
+ "Sunken City left area, crate before the bedroom",
+ "Sunken City left area, Girl Costume",
+ "Sunken City, bulb on top of the boss area",
+ "Beating the Golem",
+ "Sunken City cleared",
+ "The Body center area, breaking Li's cage",
+ "The Body center area, bulb on the main path blocking tube",
+ "The Body left area, first bulb in the top face room",
+ "The Body left area, second bulb in the top face room",
+ "The Body left area, bulb below the water stream",
+ "The Body left area, bulb in the top path to the top face room",
+ "The Body left area, bulb in the bottom face room",
+ "The Body right area, bulb in the top face room",
+ "The Body right area, bulb in the top path to the bottom face room",
+ "The Body right area, bulb in the bottom face room",
+ "The Body bottom area, bulb in the Jelly Zap room",
+ "The Body bottom area, bulb in the nautilus room",
+ "The Body bottom area, Mutant Costume",
+ "Final Boss area, bulb in the boss third form room",
+ "Objective complete"
+ ]
+ items = [["Nature form"]]
+ self.assertAccessDependency(locations, items)
diff --git a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py
new file mode 100644
index 000000000000..f015b26de10b
--- /dev/null
+++ b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py
@@ -0,0 +1,60 @@
+"""
+Author: Louis M
+Date: Fri, 03 May 2024 14:07:35 +0000
+Description: Unit test used to test that no progression items can be put in hard or hidden locations when option enabled
+"""
+
+from . import AquariaTestBase
+from BaseClasses import ItemClassification
+
+
+class UNoProgressionHardHiddenTest(AquariaTestBase):
+ """Unit test used to test that no progression items can be put in hard or hidden locations when option enabled"""
+ options = {
+ "no_progression_hard_or_hidden_locations": True
+ }
+
+ unfillable_locations = [
+ "Energy Temple boss area, Fallen God Tooth",
+ "Mithalas boss area, beating Mithalan God",
+ "Kelp Forest boss area, beating Drunian God",
+ "Sun Temple boss area, beating Sun God",
+ "Sunken City, bulb on top of the boss area",
+ "Home Water, Nautilus Egg",
+ "Energy Temple blaster room, Blaster Egg",
+ "Mithalas City Castle, beating the Priests",
+ "Mermog cave, Piranha Egg",
+ "Octopus Cave, Dumbo Egg",
+ "King Jellyfish Cave, bulb in the right path from King Jelly",
+ "King Jellyfish Cave, Jellyfish Costume",
+ "Final Boss area, bulb in the boss third form room",
+ "Sun Worm path, first cliff bulb",
+ "Sun Worm path, second cliff bulb",
+ "The Veil top right area, bulb at the top of the waterfall",
+ "Bubble Cave, bulb in the left cave wall",
+ "Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
+ "Bubble Cave, Verse Egg",
+ "Kelp Forest bottom left area, bulb close to the spirit crystals",
+ "Kelp Forest bottom left area, Walker Baby",
+ "Sun Temple, Sun Key",
+ "The Body bottom area, Mutant Costume",
+ "Sun Temple, bulb in the hidden room of the right part",
+ "Arnassi Ruins, Arnassi Armor",
+ ]
+
+ def test_unconfine_home_water_both_location_fillable(self) -> None:
+ """
+ Unit test used to test that no progression items can be put in hard or hidden locations when option enabled
+ """
+ for location in self.unfillable_locations:
+ for item_name in self.world.item_names:
+ item = self.get_item_by_name(item_name)
+ if item.classification == ItemClassification.progression:
+ self.assertFalse(
+ self.world.get_location(location).can_fill(self.multiworld.state, item, False),
+ "The location \"" + location + "\" can be filled with \"" + item_name + "\"")
+ else:
+ self.assertTrue(
+ self.world.get_location(location).can_fill(self.multiworld.state, item, False),
+ "The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
+
diff --git a/worlds/aquaria/test/test_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_progression_hard_hidden_locations.py
new file mode 100644
index 000000000000..a1493c5d0f39
--- /dev/null
+++ b/worlds/aquaria/test/test_progression_hard_hidden_locations.py
@@ -0,0 +1,52 @@
+"""
+Author: Louis M
+Date: Fri, 03 May 2024 14:07:35 +0000
+Description: Unit test used to test that progression items can be put in hard or hidden locations when option disabled
+"""
+
+from . import AquariaTestBase
+
+
+class UNoProgressionHardHiddenTest(AquariaTestBase):
+ """Unit test used to test that no progression items can be put in hard or hidden locations when option disabled"""
+ options = {
+ "no_progression_hard_or_hidden_locations": False
+ }
+
+ unfillable_locations = [
+ "Energy Temple boss area, Fallen God Tooth",
+ "Mithalas boss area, beating Mithalan God",
+ "Kelp Forest boss area, beating Drunian God",
+ "Sun Temple boss area, beating Sun God",
+ "Sunken City, bulb on top of the boss area",
+ "Home Water, Nautilus Egg",
+ "Energy Temple blaster room, Blaster Egg",
+ "Mithalas City Castle, beating the Priests",
+ "Mermog cave, Piranha Egg",
+ "Octopus Cave, Dumbo Egg",
+ "King Jellyfish Cave, bulb in the right path from King Jelly",
+ "King Jellyfish Cave, Jellyfish Costume",
+ "Final Boss area, bulb in the boss third form room",
+ "Sun Worm path, first cliff bulb",
+ "Sun Worm path, second cliff bulb",
+ "The Veil top right area, bulb at the top of the waterfall",
+ "Bubble Cave, bulb in the left cave wall",
+ "Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
+ "Bubble Cave, Verse Egg",
+ "Kelp Forest bottom left area, bulb close to the spirit crystals",
+ "Kelp Forest bottom left area, Walker Baby",
+ "Sun Temple, Sun Key",
+ "The Body bottom area, Mutant Costume",
+ "Sun Temple, bulb in the hidden room of the right part",
+ "Arnassi Ruins, Arnassi Armor",
+ ]
+
+ def test_unconfine_home_water_both_location_fillable(self) -> None:
+ """Unit test used to test that progression items can be put in hard or hidden locations when option disabled"""
+ for location in self.unfillable_locations:
+ for item_name in self.world.item_names:
+ item = self.get_item_by_name(item_name)
+ self.assertTrue(
+ self.world.get_location(location).can_fill(self.multiworld.state, item, False),
+ "The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
+
diff --git a/worlds/aquaria/test/test_spirit_form_access.py b/worlds/aquaria/test/test_spirit_form_access.py
new file mode 100644
index 000000000000..3bcbd7d72e02
--- /dev/null
+++ b/worlds/aquaria/test/test_spirit_form_access.py
@@ -0,0 +1,36 @@
+"""
+Author: Louis M
+Date: Thu, 18 Apr 2024 18:45:56 +0000
+Description: Unit test used to test accessibility of locations with and without the spirit form
+"""
+
+from . import AquariaTestBase
+
+
+class SpiritFormAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of locations with and without the spirit form"""
+
+ def test_spirit_form_location(self) -> None:
+ """Test locations that require spirit form"""
+ locations = [
+ "The Veil bottom area, bulb in the spirit path",
+ "Mithalas City Castle, Trident Head",
+ "Open Water skeleton path, King Skull",
+ "Kelp Forest bottom left area, Walker Baby",
+ "Abyss right area, bulb behind the rock in the whale room",
+ "The Whale, Verse Egg",
+ "Ice Cave, bulb in the room to the right",
+ "Ice Cave, first bulb in the top exit room",
+ "Ice Cave, second bulb in the top exit room",
+ "Ice Cave, third bulb in the top exit room",
+ "Ice Cave, bulb in the left room",
+ "Bubble Cave, bulb in the left cave wall",
+ "Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
+ "Bubble Cave, Verse Egg",
+ "Sunken City left area, Girl Costume",
+ "Beating Mantis Shrimp Prime",
+ "First secret",
+ "Arnassi Ruins, Arnassi Armor",
+ ]
+ items = [["Spirit form"]]
+ self.assertAccessDependency(locations, items)
diff --git a/worlds/aquaria/test/test_sun_form_access.py b/worlds/aquaria/test/test_sun_form_access.py
new file mode 100644
index 000000000000..394d5e4b27ae
--- /dev/null
+++ b/worlds/aquaria/test/test_sun_form_access.py
@@ -0,0 +1,28 @@
+"""
+Author: Louis M
+Date: Thu, 18 Apr 2024 18:45:56 +0000
+Description: Unit test used to test accessibility of locations with and without the sun form
+"""
+
+from . import AquariaTestBase
+
+
+class SunFormAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of locations with and without the sun form"""
+
+ def test_sun_form_location(self) -> None:
+ """Test locations that require sun form"""
+ locations = [
+ "First secret",
+ "The Whale, Verse Egg",
+ "Abyss right area, bulb behind the rock in the whale room",
+ "Octopus Cave, Dumbo Egg",
+ "Beating Octopus Prime",
+ "Sunken City, bulb on top of the boss area",
+ "Beating the Golem",
+ "Sunken City cleared",
+ "Final Boss area, bulb in the boss third form room",
+ "Objective complete"
+ ]
+ items = [["Sun form"]]
+ self.assertAccessDependency(locations, items)
diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_both.py b/worlds/aquaria/test/test_unconfine_home_water_via_both.py
new file mode 100644
index 000000000000..5b8689bc53a2
--- /dev/null
+++ b/worlds/aquaria/test/test_unconfine_home_water_via_both.py
@@ -0,0 +1,21 @@
+"""
+Author: Louis M
+Date: Fri, 03 May 2024 14:07:35 +0000
+Description: Unit test used to test accessibility of region with the unconfined home water option via transportation
+ turtle and energy door
+"""
+
+from . import AquariaTestBase
+
+
+class UnconfineHomeWaterBothAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of region with the unconfine home water option enabled"""
+ options = {
+ "unconfine_home_water": 3,
+ "early_energy_form": False
+ }
+
+ def test_unconfine_home_water_both_location(self) -> None:
+ """Test locations accessible with unconfined home water via energy door and transportation turtle"""
+ self.assertTrue(self.can_reach_region("Open Water top left area"), "Cannot reach Open Water top left area")
+ self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room")
diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py b/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py
new file mode 100644
index 000000000000..37a5c98610b5
--- /dev/null
+++ b/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py
@@ -0,0 +1,20 @@
+"""
+Author: Louis M
+Date: Fri, 03 May 2024 14:07:35 +0000
+Description: Unit test used to test accessibility of region with the unconfined home water option via the energy door
+"""
+
+from . import AquariaTestBase
+
+
+class UnconfineHomeWaterEnergyDoorAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of region with the unconfine home water option enabled"""
+ options = {
+ "unconfine_home_water": 1,
+ "early_energy_form": False
+ }
+
+ def test_unconfine_home_water_energy_door_location(self) -> None:
+ """Test locations accessible with unconfined home water via energy door"""
+ self.assertTrue(self.can_reach_region("Open Water top left area"), "Cannot reach Open Water top left area")
+ self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room")
diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py b/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py
new file mode 100644
index 000000000000..da4c83c2bc7f
--- /dev/null
+++ b/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py
@@ -0,0 +1,20 @@
+"""
+Author: Louis M
+Date: Fri, 03 May 2024 14:07:35 +0000
+Description: Unit test used to test accessibility of region with the unconfined home water option via transturtle
+"""
+
+from . import AquariaTestBase
+
+
+class UnconfineHomeWaterTransturtleAccessTest(AquariaTestBase):
+ """Unit test used to test accessibility of region with the unconfine home water option enabled"""
+ options = {
+ "unconfine_home_water": 2,
+ "early_energy_form": False
+ }
+
+ def test_unconfine_home_water_transturtle_location(self) -> None:
+ """Test locations accessible with unconfined home water via transportation turtle"""
+ self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room")
+ self.assertFalse(self.can_reach_region("Open Water top left area"), "Can reach Open Water top left area")
diff --git a/worlds/archipidle/Items.py b/worlds/archipidle/Items.py
index 2b5e6e9a81d0..94665631b711 100644
--- a/worlds/archipidle/Items.py
+++ b/worlds/archipidle/Items.py
@@ -1,4 +1,7 @@
item_table = (
+ 'An Old GeoCities Profile',
+ 'Very Funny Joke',
+ 'Motivational Video',
'Staples Easy Button',
'One Million Dollars',
'Replica Master Sword',
@@ -13,7 +16,7 @@
'2012 Magic the Gathering Core Set Starter Box',
'Poke\'mon Booster Pack',
'USB Speakers',
- 'Plastic Spork',
+ 'Eco-Friendly Spork',
'Cheeseburger',
'Brand New Car',
'Hunting Knife',
@@ -22,7 +25,7 @@
'One-Up Mushroom',
'Nokia N-GAGE',
'2-Liter of Sprite',
- 'Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward expansion up to level 60 with no restrictions on playtime!',
+ 'Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward and Stormblood expansions up to level 70 with no restrictions on playtime!',
'Can of Compressed Air',
'Striped Kitten',
'USB Power Adapter',
diff --git a/worlds/archipidle/Rules.py b/worlds/archipidle/Rules.py
index cdd48e760445..2cc6220c6927 100644
--- a/worlds/archipidle/Rules.py
+++ b/worlds/archipidle/Rules.py
@@ -1,43 +1,28 @@
from BaseClasses import MultiWorld
-from ..AutoWorld import LogicMixin
-from ..generic.Rules import set_rule
+from worlds.AutoWorld import LogicMixin
class ArchipIDLELogic(LogicMixin):
def _archipidle_location_is_accessible(self, player_id, items_required):
- items_received = 0
- for item in self.prog_items:
- if item[1] == player_id:
- items_received += 1
-
- return items_received >= items_required
+ return sum(self.prog_items[player_id].values()) >= items_required
def set_rules(world: MultiWorld, player: int):
for i in range(16, 31):
- set_rule(
- world.get_location(f"IDLE item number {i}", player),
- lambda state: state._archipidle_location_is_accessible(player, 4)
- )
+ world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
+ state: state._archipidle_location_is_accessible(player, 4)
for i in range(31, 51):
- set_rule(
- world.get_location(f"IDLE item number {i}", player),
- lambda state: state._archipidle_location_is_accessible(player, 10)
- )
+ world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
+ state: state._archipidle_location_is_accessible(player, 10)
for i in range(51, 101):
- set_rule(
- world.get_location(f"IDLE item number {i}", player),
- lambda state: state._archipidle_location_is_accessible(player, 20)
- )
+ world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
+ state: state._archipidle_location_is_accessible(player, 20)
for i in range(101, 201):
- set_rule(
- world.get_location(f"IDLE item number {i}", player),
- lambda state: state._archipidle_location_is_accessible(player, 40)
- )
+ world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
+ state: state._archipidle_location_is_accessible(player, 40)
world.completion_condition[player] =\
- lambda state:\
- state.can_reach(world.get_location("IDLE item number 200", player), "Location", player)
+ lambda state: state.can_reach(world.get_location("IDLE item number 200", player), "Location", player)
diff --git a/worlds/archipidle/__init__.py b/worlds/archipidle/__init__.py
index 2d182f31dc20..f4345444efb9 100644
--- a/worlds/archipidle/__init__.py
+++ b/worlds/archipidle/__init__.py
@@ -1,8 +1,8 @@
from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification
+from worlds.AutoWorld import World, WebWorld
+from datetime import datetime
from .Items import item_table
from .Rules import set_rules
-from ..AutoWorld import World, WebWorld
-from datetime import datetime
class ArchipIDLEWebWorld(WebWorld):
@@ -29,11 +29,10 @@ class ArchipIDLEWebWorld(WebWorld):
class ArchipIDLEWorld(World):
"""
- An idle game which sends a check every thirty seconds, up to two hundred checks.
+ An idle game which sends a check every thirty to sixty seconds, up to two hundred checks.
"""
game = "ArchipIDLE"
topology_present = False
- data_version = 5
hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April
web = ArchipIDLEWebWorld()
@@ -56,18 +55,40 @@ def create_item(self, name: str) -> Item:
return Item(name, ItemClassification.progression, self.item_name_to_id[name], self.player)
def create_items(self):
- item_table_copy = list(item_table)
- self.multiworld.random.shuffle(item_table_copy)
+ item_pool = [
+ ArchipIDLEItem(
+ item_table[0],
+ ItemClassification.progression,
+ self.item_name_to_id[item_table[0]],
+ self.player
+ )
+ ]
- item_pool = []
- for i in range(200):
- item = ArchipIDLEItem(
+ for i in range(40):
+ item_pool.append(ArchipIDLEItem(
+ item_table[1],
+ ItemClassification.progression,
+ self.item_name_to_id[item_table[1]],
+ self.player
+ ))
+
+ for i in range(40):
+ item_pool.append(ArchipIDLEItem(
+ item_table[2],
+ ItemClassification.filler,
+ self.item_name_to_id[item_table[2]],
+ self.player
+ ))
+
+ item_table_copy = list(item_table[3:])
+ self.random.shuffle(item_table_copy)
+ for i in range(119):
+ item_pool.append(ArchipIDLEItem(
item_table_copy[i],
- ItemClassification.progression if i < 40 else ItemClassification.filler,
+ ItemClassification.progression if i < 9 else ItemClassification.filler,
self.item_name_to_id[item_table_copy[i]],
self.player
- )
- item_pool.append(item)
+ ))
self.multiworld.itempool += item_pool
diff --git a/worlds/archipidle/docs/en_ArchipIDLE.md b/worlds/archipidle/docs/en_ArchipIDLE.md
index 3d57e3a0551a..c3b396c64901 100644
--- a/worlds/archipidle/docs/en_ArchipIDLE.md
+++ b/worlds/archipidle/docs/en_ArchipIDLE.md
@@ -6,8 +6,8 @@ ArchipIDLE was originally the 2022 Archipelago April Fools' Day joke. It is an i
on regular intervals. Updated annually with more items, gimmicks, and features, the game is visible
only during the month of April.
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure
+The [player options page for this game](../player-options) contains all the options you need to configure
and export a config file.
diff --git a/worlds/archipidle/docs/guide_en.md b/worlds/archipidle/docs/guide_en.md
index e1a6532992b5..c450ec421dfc 100644
--- a/worlds/archipidle/docs/guide_en.md
+++ b/worlds/archipidle/docs/guide_en.md
@@ -1,12 +1,12 @@
# ArchipIdle Setup Guide
## Joining a MultiWorld Game
-1. Generate a `.yaml` file from the [ArchipIDLE Player Settings Page](/games/ArchipIDLE/player-settings)
+1. Generate a `.yaml` file from the [ArchipIDLE Player Options Page](/games/ArchipIDLE/player-options)
2. Open the ArchipIDLE Client in your web browser by either:
- Navigate to the [ArchipIDLE Client](http://idle.multiworld.link)
- Download the client and run it locally from the
[ArchipIDLE GitHub Releases Page](https://github.com/ArchipelagoMW/archipidle/releases)
3. Enter the server address in the `Server Address` field and press enter
4. Enter your slot name when prompted. This should be the same as the `name` you entered on the
- setting page above, or the `name` field in your yaml file.
+ options page above, or the `name` field in your yaml file.
5. Click the "Begin!" button.
diff --git a/worlds/archipidle/docs/guide_fr.md b/worlds/archipidle/docs/guide_fr.md
index c3842ed7db6e..dc0c8af3218c 100644
--- a/worlds/archipidle/docs/guide_fr.md
+++ b/worlds/archipidle/docs/guide_fr.md
@@ -1,11 +1,10 @@
# Guide de configuration d'ArchipIdle
## Rejoindre une partie MultiWorld
-1. Générez un fichier `.yaml` à partir de la [page des paramètres du lecteur ArchipIDLE](/games/ArchipIDLE/player-settings)
+1. Générez un fichier `.yaml` à partir de la [page des paramètres du lecteur ArchipIDLE](/games/ArchipIDLE/player-options)
2. Ouvrez le client ArchipIDLE dans votre navigateur Web en :
- - Accédez au [Client ArchipIDLE](http://idle.multiworld.link)
- - Téléchargez le client et exécutez-le localement à partir du
- [Page des versions d'ArchipIDLE GitHub](https://github.com/ArchipelagoMW/archipidle/releases)
+ - Accédez au [Client ArchipIDLE](http://idle.multiworld.link)
+ - Téléchargez le client et exécutez-le localement à partir du [Page des versions d'ArchipIDLE GitHub](https://github.com/ArchipelagoMW/archipidle/releases)
3. Entrez l'adresse du serveur dans le champ `Server Address` et appuyez sur Entrée
4. Entrez votre nom d'emplacement lorsque vous y êtes invité. Il doit être le même que le `name` que vous avez saisi sur le
page de configuration ci-dessus, ou le champ `name` dans votre fichier yaml.
diff --git a/worlds/bk_sudoku/__init__.py b/worlds/bk_sudoku/__init__.py
deleted file mode 100644
index f914baf066aa..000000000000
--- a/worlds/bk_sudoku/__init__.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from typing import Dict
-
-from BaseClasses import Tutorial
-from ..AutoWorld import WebWorld, World
-
-
-class Bk_SudokuWebWorld(WebWorld):
- settings_page = "games/Sudoku/info/en"
- theme = 'partyTime'
- tutorials = [
- Tutorial(
- tutorial_name='Setup Guide',
- description='A guide to playing BK Sudoku',
- language='English',
- file_name='setup_en.md',
- link='setup/en',
- authors=['Jarno']
- )
- ]
-
-
-class Bk_SudokuWorld(World):
- """
- Play a little Sudoku while you're in BK mode to maybe get some useful hints
- """
- game = "Sudoku"
- web = Bk_SudokuWebWorld()
- data_version = 1
-
- item_name_to_id: Dict[str, int] = {}
- location_name_to_id: Dict[str, int] = {}
-
- @classmethod
- def stage_assert_generate(cls, multiworld):
- raise Exception("BK Sudoku cannot be used for generating worlds, the client can instead connect to any other world")
diff --git a/worlds/bk_sudoku/docs/en_Sudoku.md b/worlds/bk_sudoku/docs/en_Sudoku.md
deleted file mode 100644
index d69514752460..000000000000
--- a/worlds/bk_sudoku/docs/en_Sudoku.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Bk Sudoku
-
-## What is this game?
-
-BK Sudoku is not a typical Archipelago game; instead, it is a generic Sudoku client that can connect to any existing multiworld. When connected, you can play Sudoku to unlock random hints for your game. While slow, it will give you something to do when you can't reach the checks in your game.
-
-## What hints are unlocked?
-
-After completing a Sudoku puzzle, the game will unlock 1 random hint for an unchecked location in the slot you are connected to.
-
-## Where is the settings page?
-
-There is no settings page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld.
diff --git a/worlds/bk_sudoku/docs/setup_en.md b/worlds/bk_sudoku/docs/setup_en.md
deleted file mode 100644
index eda17e701bb8..000000000000
--- a/worlds/bk_sudoku/docs/setup_en.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# BK Sudoku Setup Guide
-
-## Required Software
-- [Bk Sudoku](https://github.com/Jarno458/sudoku)
-- Windows 8 or higher
-
-## General Concept
-
-This is a client that can connect to any multiworld slot, and lets you play Sudoku to unlock random hints for that slot's locations.
-
-Due to the fact that the Sudoku client may connect to any slot, it is not necessary to generate a YAML for this game as it does not generate any new slots in the multiworld session.
-
-## Installation Procedures
-
-Go to the latest release on [BK Sudoku Releases](https://github.com/Jarno458/sudoku/releases). Download and extract the `Bk_Sudoku.zip` file.
-
-## Joining a MultiWorld Game
-
-1. Run Bk_Sudoku.exe
-2. Enter the name of the slot you wish to connect to
-3. Enter the server url & port number
-4. Press connect
-5. Choose difficulty
-6. Try to solve the Sudoku
diff --git a/worlds/blasphemous/Options.py b/worlds/blasphemous/Options.py
index ea304d22ed66..127a1dc77669 100644
--- a/worlds/blasphemous/Options.py
+++ b/worlds/blasphemous/Options.py
@@ -67,6 +67,7 @@ class StartingLocation(ChoiceIsRandom):
class Ending(Choice):
"""Choose which ending is required to complete the game.
+ Talking to Tirso in Albero will tell you the selected ending for the current game.
Ending A: Collect all thorn upgrades.
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation."""
display_name = "Ending"
diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py
index 248ff645bc12..5d8829213163 100644
--- a/worlds/blasphemous/Rules.py
+++ b/worlds/blasphemous/Rules.py
@@ -578,11 +578,12 @@ def rules(blasphemousworld):
or state.has("Purified Hand of the Nun", player)
or state.has("D01Z02S03[NW]", player)
and (
- can_cross_gap(state, logic, player, 1)
+ can_cross_gap(state, logic, player, 2)
or state.has("Lorquiana", player)
or aubade(state, player)
or state.has("Cantina of the Blue Rose", player)
or charge_beam(state, player)
+ or state.has("Ranged Skill", player)
)
))
set_rule(world.get_location("Albero: Lvdovico's 1st reward", player),
@@ -702,10 +703,11 @@ def rules(blasphemousworld):
# Items
set_rule(world.get_location("WotBC: Cliffside Child of Moonlight", player),
lambda state: (
- can_cross_gap(state, logic, player, 1)
+ can_cross_gap(state, logic, player, 2)
or aubade(state, player)
or charge_beam(state, player)
- or state.has_any({"Lorquiana", "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", "Cloistered Ruby"}, player)
+ or state.has_any({"Lorquiana", "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", \
+ "Cloistered Ruby", "Ranged Skill"}, player)
or precise_skips_allowed(logic)
))
# Doors
@@ -4193,8 +4195,9 @@ def rules(blasphemousworld):
# Items
set_rule(world.get_location("BotSS: Platforming gauntlet", player),
lambda state: (
- state.has("D17BZ02S01[FrontR]", player)
- or state.has_all({"Dash Ability", "Wall Climb Ability"}, player)
+ #state.has("D17BZ02S01[FrontR]", player) or
+ # TODO: actually fix this once door rando is real
+ state.has_all({"Dash Ability", "Wall Climb Ability"}, player)
))
# Doors
set_rule(world.get_entrance("D17BZ02S01[FrontR]", player),
diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py
index 9abcd81b20e1..a46fb55b9541 100644
--- a/worlds/blasphemous/__init__.py
+++ b/worlds/blasphemous/__init__.py
@@ -32,7 +32,6 @@ class BlasphemousWorld(World):
game: str = "Blasphemous"
web = BlasphemousWeb()
- data_version = 2
item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)}
location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)}
diff --git a/worlds/blasphemous/docs/en_Blasphemous.md b/worlds/blasphemous/docs/en_Blasphemous.md
index 15223213ac67..a99eea6fa4b8 100644
--- a/worlds/blasphemous/docs/en_Blasphemous.md
+++ b/worlds/blasphemous/docs/en_Blasphemous.md
@@ -1,8 +1,8 @@
# Blasphemous
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
+The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
## What does randomization do to this game?
@@ -19,6 +19,7 @@ In addition, there are other changes to the game that make it better optimized f
- The Apodictic Heart of Mea Culpa can be unequipped.
- Dying with the Immaculate Bead is unnecessary, it is automatically upgraded to the Weight of True Guilt.
- If the option is enabled, the 34 corpses in game will have their messages changed to give hints about certain items and locations. The Shroud of Dreamt Sins is not required to hear them.
+- Talking to Tirso in Albero will tell you the selected ending for the current game.
## What has been changed about the side quests?
diff --git a/worlds/blasphemous/docs/setup_en.md b/worlds/blasphemous/docs/setup_en.md
index cc238a492eb3..070d1ca4964b 100644
--- a/worlds/blasphemous/docs/setup_en.md
+++ b/worlds/blasphemous/docs/setup_en.md
@@ -15,7 +15,6 @@ Optional:
- Quick Prie Dieu warp mod: [GitHub](https://github.com/BadMagic100/Blasphemous-PrieWarp)
- Boots of Pleading mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Boots-of-Pleading)
- Double Jump mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Double-Jump)
-- PopTracker pack: [GitHub](https://github.com/sassyvania/Blasphemous-Randomizer-Maptracker)
## Mod Installer (Recommended)
diff --git a/worlds/bomb_rush_cyberfunk/Items.py b/worlds/bomb_rush_cyberfunk/Items.py
new file mode 100644
index 000000000000..b8aa877205e3
--- /dev/null
+++ b/worlds/bomb_rush_cyberfunk/Items.py
@@ -0,0 +1,553 @@
+from typing import TypedDict, List, Dict, Set
+from enum import Enum
+
+
+class BRCType(Enum):
+ Music = 0
+ GraffitiM = 1
+ GraffitiL = 2
+ GraffitiXL = 3
+ Skateboard = 4
+ InlineSkates = 5
+ BMX = 6
+ Character = 7
+ Outfit = 8
+ REP = 9
+ Camera = 10
+
+
+class ItemDict(TypedDict, total=False):
+ name: str
+ count: int
+ type: BRCType
+
+
+base_id = 2308000
+
+
+item_table: List[ItemDict] = [
+ # Music
+ {'name': "Music (GET ENUF)",
+ 'type': BRCType.Music},
+ {'name': "Music (Chuckin Up)",
+ 'type': BRCType.Music},
+ {'name': "Music (Spectres)",
+ 'type': BRCType.Music},
+ {'name': "Music (You Can Say Hi)",
+ 'type': BRCType.Music},
+ {'name': "Music (JACK DA FUNK)",
+ 'type': BRCType.Music},
+ {'name': "Music (Feel The Funk (Computer Love))",
+ 'type': BRCType.Music},
+ {'name': "Music (Big City Life)",
+ 'type': BRCType.Music},
+ {'name': "Music (I Wanna Kno)",
+ 'type': BRCType.Music},
+ {'name': "Music (Plume)",
+ 'type': BRCType.Music},
+ {'name': "Music (Two Days Off)",
+ 'type': BRCType.Music},
+ {'name': "Music (Scraped On The Way Out)",
+ 'type': BRCType.Music},
+ {'name': "Music (Last Hoorah)",
+ 'type': BRCType.Music},
+ {'name': "Music (State of Mind)",
+ 'type': BRCType.Music},
+ {'name': "Music (AGUA)",
+ 'type': BRCType.Music},
+ {'name': "Music (Condensed milk)",
+ 'type': BRCType.Music},
+ {'name': "Music (Light Switch)",
+ 'type': BRCType.Music},
+ {'name': "Music (Hair Dun Nails Dun)",
+ 'type': BRCType.Music},
+ {'name': "Music (Precious Thing)",
+ 'type': BRCType.Music},
+ {'name': "Music (Next To Me)",
+ 'type': BRCType.Music},
+ {'name': "Music (Refuse)",
+ 'type': BRCType.Music},
+ {'name': "Music (Iridium)",
+ 'type': BRCType.Music},
+ {'name': "Music (Funk Express)",
+ 'type': BRCType.Music},
+ {'name': "Music (In The Pocket)",
+ 'type': BRCType.Music},
+ {'name': "Music (Bounce Upon A Time)",
+ 'type': BRCType.Music},
+ {'name': "Music (hwbouths)",
+ 'type': BRCType.Music},
+ {'name': "Music (Morning Glow)",
+ 'type': BRCType.Music},
+ {'name': "Music (Chromebies)",
+ 'type': BRCType.Music},
+ {'name': "Music (watchyaback!)",
+ 'type': BRCType.Music},
+ {'name': "Music (Anime Break)",
+ 'type': BRCType.Music},
+ {'name': "Music (DA PEOPLE)",
+ 'type': BRCType.Music},
+ {'name': "Music (Trinitron)",
+ 'type': BRCType.Music},
+ {'name': "Music (Operator)",
+ 'type': BRCType.Music},
+ {'name': "Music (Sunshine Popping Mixtape)",
+ 'type': BRCType.Music},
+ {'name': "Music (House Cats Mixtape)",
+ 'type': BRCType.Music},
+ {'name': "Music (Breaking Machine Mixtape)",
+ 'type': BRCType.Music},
+ {'name': "Music (Beastmode Hip Hop Mixtape)",
+ 'type': BRCType.Music},
+
+ # Graffiti
+ {'name': "Graffiti (M - OVERWHELMME)",
+ 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - QUICK BING)",
+ 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - BLOCKY)",
+ 'type': BRCType.GraffitiM},
+ #{'name': "Graffiti (M - Flow)",
+ # 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - Pora)",
+ 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - Teddy 4)",
+ 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - BOMB BEATS)",
+ 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - SPRAYTANICPANIC!)",
+ 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - SHOGUN)",
+ 'type': BRCType.GraffitiM},
+ #{'name': "Graffiti (M - EVIL DARUMA)",
+ # 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - TeleBinge)",
+ 'type': BRCType.GraffitiM},
+ #{'name': "Graffiti (M - All Screws Loose)",
+ # 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - 0m33)",
+ 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - Vom'B)",
+ 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - Street classic)",
+ 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - Thick Candy)",
+ 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - colorBOMB)",
+ 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - Zona Leste)",
+ 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - Stacked Symbols)",
+ 'type': BRCType.GraffitiM},
+ #{'name': "Graffiti (M - Constellation Circle)",
+ # 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - B-boy Love)",
+ 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - Devil 68)",
+ 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (M - pico pow)",
+ 'type': BRCType.GraffitiM},
+ #{'name': "Graffiti (M - 8 MINUTES OF LEAN MEAN)",
+ # 'type': BRCType.GraffitiM},
+ {'name': "Graffiti (L - WHOLE SIXER)",
+ 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - INFINITY)",
+ 'type': BRCType.GraffitiL},
+ #{'name': "Graffiti (L - Dynamo)",
+ # 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - VoodooBoy)",
+ 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - Fang It Up!)",
+ 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - FREAKS)",
+ 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - Graffo Le Fou)",
+ 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - Lauder)",
+ 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - SpawningSeason)",
+ 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - Moai Marathon)",
+ 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - Tius)",
+ 'type': BRCType.GraffitiL},
+ #{'name': "Graffiti (L - KANI-BOZU)",
+ # 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - NOISY NINJA)",
+ 'type': BRCType.GraffitiL},
+ #{'name': "Graffiti (L - Dinner On The Court)",
+ # 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - Campaign Trail)",
+ 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - skate or di3)",
+ 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - Jd Vila Formosa)",
+ 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - Messenger Mural)",
+ 'type': BRCType.GraffitiL},
+ #{'name': "Graffiti (L - Solstice Script)",
+ # 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - RECORD.HEAD)",
+ 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - Boom)",
+ 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - wild rush)",
+ 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (L - buttercup)",
+ 'type': BRCType.GraffitiL},
+ #{'name': "Graffiti (L - DIGITAL BLOCKBUSTER)",
+ # 'type': BRCType.GraffitiL},
+ {'name': "Graffiti (XL - Gold Rush)",
+ 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - WILD STRUXXA)",
+ 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - VIBRATIONS)",
+ 'type': BRCType.GraffitiXL},
+ #{'name': "Graffiti (XL - Bevel)",
+ # 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - SECOND SIGHT)",
+ 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - Bomb Croc)",
+ 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - FATE)",
+ 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - Web Spitter)",
+ 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - MOTORCYCLE GANG)",
+ 'type': BRCType.GraffitiXL},
+ #{'name': "Graffiti (XL - CYBER TENGU)",
+ # 'type': BRCType.GraffitiXL},
+ #{'name': "Graffiti (XL - Don't Screw Around)",
+ # 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - Deep Dive)",
+ 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - MegaHood)",
+ 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - Gamex UPA ABL)",
+ 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - BiGSHiNYBoMB)",
+ 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - Bomb Burner)",
+ 'type': BRCType.GraffitiXL},
+ #{'name': "Graffiti (XL - Astrological Augury)",
+ # 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - Pirate's Life 4 Me)",
+ 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - Bombing by FireMan)",
+ 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - end 2 end)",
+ 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - Raver Funk)",
+ 'type': BRCType.GraffitiXL},
+ {'name': "Graffiti (XL - headphones on Helmet on)",
+ 'type': BRCType.GraffitiXL},
+ #{'name': "Graffiti (XL - HIGH TECH WS)",
+ # 'type': BRCType.GraffitiXL},
+
+ # Skateboards
+ {'name': "Skateboard (Devon)",
+ 'type': BRCType.Skateboard},
+ {'name': "Skateboard (Terrence)",
+ 'type': BRCType.Skateboard},
+ {'name': "Skateboard (Maceo)",
+ 'type': BRCType.Skateboard},
+ {'name': "Skateboard (Lazer Accuracy)",
+ 'type': BRCType.Skateboard},
+ {'name': "Skateboard (Death Boogie)",
+ 'type': BRCType.Skateboard},
+ {'name': "Skateboard (Sylk)",
+ 'type': BRCType.Skateboard},
+ {'name': "Skateboard (Taiga)",
+ 'type': BRCType.Skateboard},
+ {'name': "Skateboard (Just Swell)",
+ 'type': BRCType.Skateboard},
+ {'name': "Skateboard (Mantra)",
+ 'type': BRCType.Skateboard},
+
+ # Inline Skates
+ {'name': "Inline Skates (Glaciers)",
+ 'type': BRCType.InlineSkates},
+ {'name': "Inline Skates (Sweet Royale)",
+ 'type': BRCType.InlineSkates},
+ {'name': "Inline Skates (Strawberry Missiles)",
+ 'type': BRCType.InlineSkates},
+ {'name': "Inline Skates (Ice Cold Killers)",
+ 'type': BRCType.InlineSkates},
+ {'name': "Inline Skates (Red Industry)",
+ 'type': BRCType.InlineSkates},
+ {'name': "Inline Skates (Mech Adversary)",
+ 'type': BRCType.InlineSkates},
+ {'name': "Inline Skates (Orange Blasters)",
+ 'type': BRCType.InlineSkates},
+ {'name': "Inline Skates (ck)",
+ 'type': BRCType.InlineSkates},
+ {'name': "Inline Skates (Sharpshooters)",
+ 'type': BRCType.InlineSkates},
+
+ # BMX
+ {'name': "BMX (Mr. Taupe)",
+ 'type': BRCType.BMX},
+ {'name': "BMX (Gum)",
+ 'type': BRCType.BMX},
+ {'name': "BMX (Steel Wheeler)",
+ 'type': BRCType.BMX},
+ {'name': "BMX (oyo)",
+ 'type': BRCType.BMX},
+ {'name': "BMX (Rigid No.6)",
+ 'type': BRCType.BMX},
+ {'name': "BMX (Ceremony)",
+ 'type': BRCType.BMX},
+ {'name': "BMX (XXX)",
+ 'type': BRCType.BMX},
+ {'name': "BMX (Terrazza)",
+ 'type': BRCType.BMX},
+ {'name': "BMX (Dedication)",
+ 'type': BRCType.BMX},
+
+ # Outfits
+ {'name': "Outfit (Red - Autumn)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Red - Winter)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Tryce - Autumn)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Tryce - Winter)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Bel - Autumn)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Bel - Winter)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Vinyl - Autumn)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Vinyl - Winter)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Solace - Autumn)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Solace - Winter)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Felix - Autumn)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Felix - Winter)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Rave - Autumn)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Rave - Winter)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Mesh - Autumn)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Mesh - Winter)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Shine - Autumn)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Shine - Winter)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Rise - Autumn)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Rise - Winter)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Coil - Autumn)",
+ 'type': BRCType.Outfit},
+ {'name': "Outfit (Coil - Winter)",
+ 'type': BRCType.Outfit},
+
+ # Characters
+ {'name': "Tryce",
+ 'type': BRCType.Character},
+ {'name': "Bel",
+ 'type': BRCType.Character},
+ {'name': "Vinyl",
+ 'type': BRCType.Character},
+ {'name': "Solace",
+ 'type': BRCType.Character},
+ {'name': "Rave",
+ 'type': BRCType.Character},
+ {'name': "Mesh",
+ 'type': BRCType.Character},
+ {'name': "Shine",
+ 'type': BRCType.Character},
+ {'name': "Rise",
+ 'type': BRCType.Character},
+ {'name': "Coil",
+ 'type': BRCType.Character},
+ {'name': "Frank",
+ 'type': BRCType.Character},
+ {'name': "Rietveld",
+ 'type': BRCType.Character},
+ {'name': "DJ Cyber",
+ 'type': BRCType.Character},
+ {'name': "Eclipse",
+ 'type': BRCType.Character},
+ {'name': "DOT.EXE",
+ 'type': BRCType.Character},
+ {'name': "Devil Theory",
+ 'type': BRCType.Character},
+ {'name': "Flesh Prince",
+ 'type': BRCType.Character},
+ {'name': "Futurism",
+ 'type': BRCType.Character},
+ {'name': "Oldhead",
+ 'type': BRCType.Character},
+
+ # REP
+ {'name': "8 REP",
+ 'type': BRCType.REP},
+ {'name': "16 REP",
+ 'type': BRCType.REP},
+ {'name': "24 REP",
+ 'type': BRCType.REP},
+ {'name': "32 REP",
+ 'type': BRCType.REP},
+ {'name': "48 REP",
+ 'type': BRCType.REP},
+
+ # App
+ {'name': "Camera App",
+ 'type': BRCType.Camera}
+]
+
+
+group_table: Dict[str, Set[str]] = {
+ "graffitim": {"Graffiti (M - OVERWHELMME)",
+ "Graffiti (M - QUICK BING)",
+ "Graffiti (M - BLOCKY)",
+ "Graffiti (M - Pora)",
+ "Graffiti (M - Teddy 4)",
+ "Graffiti (M - BOMB BEATS)",
+ "Graffiti (M - SPRAYTANICPANIC!)",
+ "Graffiti (M - SHOGUN)",
+ "Graffiti (M - TeleBinge)",
+ "Graffiti (M - 0m33)",
+ "Graffiti (M - Vom'B)",
+ "Graffiti (M - Street classic)",
+ "Graffiti (M - Thick Candy)",
+ "Graffiti (M - colorBOMB)",
+ "Graffiti (M - Zona Leste)",
+ "Graffiti (M - Stacked Symbols)",
+ "Graffiti (M - B-boy Love)",
+ "Graffiti (M - Devil 68)",
+ "Graffiti (M - pico pow)"},
+ "graffitil": {"Graffiti (L - WHOLE SIXER)",
+ "Graffiti (L - INFINITY)",
+ "Graffiti (L - VoodooBoy)",
+ "Graffiti (L - Fang It Up!)",
+ "Graffiti (L - FREAKS)",
+ "Graffiti (L - Graffo Le Fou)",
+ "Graffiti (L - Lauder)",
+ "Graffiti (L - SpawningSeason)",
+ "Graffiti (L - Moai Marathon)",
+ "Graffiti (L - Tius)",
+ "Graffiti (L - NOISY NINJA)",
+ "Graffiti (L - Campaign Trail)",
+ "Graffiti (L - skate or di3)",
+ "Graffiti (L - Jd Vila Formosa)",
+ "Graffiti (L - Messenger Mural)",
+ "Graffiti (L - RECORD.HEAD)",
+ "Graffiti (L - Boom)",
+ "Graffiti (L - wild rush)",
+ "Graffiti (L - buttercup)"},
+ "graffitixl": {"Graffiti (XL - Gold Rush)",
+ "Graffiti (XL - WILD STRUXXA)",
+ "Graffiti (XL - VIBRATIONS)",
+ "Graffiti (XL - SECOND SIGHT)",
+ "Graffiti (XL - Bomb Croc)",
+ "Graffiti (XL - FATE)",
+ "Graffiti (XL - Web Spitter)",
+ "Graffiti (XL - MOTORCYCLE GANG)",
+ "Graffiti (XL - Deep Dive)",
+ "Graffiti (XL - MegaHood)",
+ "Graffiti (XL - Gamex UPA ABL)",
+ "Graffiti (XL - BiGSHiNYBoMB)",
+ "Graffiti (XL - Bomb Burner)",
+ "Graffiti (XL - Pirate's Life 4 Me)",
+ "Graffiti (XL - Bombing by FireMan)",
+ "Graffiti (XL - end 2 end)",
+ "Graffiti (XL - Raver Funk)",
+ "Graffiti (XL - headphones on Helmet on)"},
+ "skateboard": {"Skateboard (Devon)",
+ "Skateboard (Terrence)",
+ "Skateboard (Maceo)",
+ "Skateboard (Lazer Accuracy)",
+ "Skateboard (Death Boogie)",
+ "Skateboard (Sylk)",
+ "Skateboard (Taiga)",
+ "Skateboard (Just Swell)",
+ "Skateboard (Mantra)"},
+ "inline skates": {"Inline Skates (Glaciers)",
+ "Inline Skates (Sweet Royale)",
+ "Inline Skates (Strawberry Missiles)",
+ "Inline Skates (Ice Cold Killers)",
+ "Inline Skates (Red Industry)",
+ "Inline Skates (Mech Adversary)",
+ "Inline Skates (Orange Blasters)",
+ "Inline Skates (ck)",
+ "Inline Skates (Sharpshooters)"},
+ "skates": {"Inline Skates (Glaciers)",
+ "Inline Skates (Sweet Royale)",
+ "Inline Skates (Strawberry Missiles)",
+ "Inline Skates (Ice Cold Killers)",
+ "Inline Skates (Red Industry)",
+ "Inline Skates (Mech Adversary)",
+ "Inline Skates (Orange Blasters)",
+ "Inline Skates (ck)",
+ "Inline Skates (Sharpshooters)"},
+ "inline": {"Inline Skates (Glaciers)",
+ "Inline Skates (Sweet Royale)",
+ "Inline Skates (Strawberry Missiles)",
+ "Inline Skates (Ice Cold Killers)",
+ "Inline Skates (Red Industry)",
+ "Inline Skates (Mech Adversary)",
+ "Inline Skates (Orange Blasters)",
+ "Inline Skates (ck)",
+ "Inline Skates (Sharpshooters)"},
+ "bmx": {"BMX (Mr. Taupe)",
+ "BMX (Gum)",
+ "BMX (Steel Wheeler)",
+ "BMX (oyo)",
+ "BMX (Rigid No.6)",
+ "BMX (Ceremony)",
+ "BMX (XXX)",
+ "BMX (Terrazza)",
+ "BMX (Dedication)"},
+ "bike": {"BMX (Mr. Taupe)",
+ "BMX (Gum)",
+ "BMX (Steel Wheeler)",
+ "BMX (oyo)",
+ "BMX (Rigid No.6)",
+ "BMX (Ceremony)",
+ "BMX (XXX)",
+ "BMX (Terrazza)",
+ "BMX (Dedication)"},
+ "bicycle": {"BMX (Mr. Taupe)",
+ "BMX (Gum)",
+ "BMX (Steel Wheeler)",
+ "BMX (oyo)",
+ "BMX (Rigid No.6)",
+ "BMX (Ceremony)",
+ "BMX (XXX)",
+ "BMX (Terrazza)",
+ "BMX (Dedication)"},
+ "characters": {"Tryce",
+ "Bel",
+ "Vinyl",
+ "Solace",
+ "Rave",
+ "Mesh",
+ "Shine",
+ "Rise",
+ "Coil",
+ "Frank",
+ "Rietveld",
+ "DJ Cyber",
+ "Eclipse",
+ "DOT.EXE",
+ "Devil Theory",
+ "Flesh Prince",
+ "Futurism",
+ "Oldhead"},
+ "girl": {"Bel",
+ "Vinyl",
+ "Rave",
+ "Shine",
+ "Rise",
+ "Futurism"}
+}
\ No newline at end of file
diff --git a/worlds/bomb_rush_cyberfunk/Locations.py b/worlds/bomb_rush_cyberfunk/Locations.py
new file mode 100644
index 000000000000..7ea959019067
--- /dev/null
+++ b/worlds/bomb_rush_cyberfunk/Locations.py
@@ -0,0 +1,785 @@
+from typing import TypedDict, List
+from .Regions import Stages
+
+
+class LocationDict(TypedDict):
+ name: str
+ stage: Stages
+ game_id: str
+
+
+class EventDict(TypedDict):
+ name: str
+ stage: str
+ item: str
+
+
+location_table: List[LocationDict] = [
+ {'name': "Hideout: Half pipe CD",
+ 'stage': Stages.H,
+ 'game_id': "MusicTrack_CondensedMilk"},
+ {'name': "Hideout: Garage tower CD",
+ 'stage': Stages.H,
+ 'game_id': "MusicTrack_MorningGlow"},
+ {'name': "Hideout: Rooftop CD",
+ 'stage': Stages.H,
+ 'game_id': "MusicTrack_LightSwitch"},
+ {'name': "Hideout: Under staircase graffiti",
+ 'stage': Stages.H,
+ 'game_id': "UnlockGraffiti_grafTex_M1"},
+ {'name': "Hideout: Secret area graffiti",
+ 'stage': Stages.H,
+ 'game_id': "UnlockGraffiti_grafTex_L1"},
+ {'name': "Hideout: Rear studio graffiti",
+ 'stage': Stages.H,
+ 'game_id': "UnlockGraffiti_grafTex_XL1"},
+ {'name': "Hideout: Corner ledge graffiti",
+ 'stage': Stages.H,
+ 'game_id': "UnlockGraffiti_grafTex_M2"},
+ {'name': "Hideout: Upper platform skateboard",
+ 'stage': Stages.H,
+ 'game_id': "SkateboardDeck3"},
+ {'name': "Hideout: BMX garage skateboard",
+ 'stage': Stages.H,
+ 'game_id': "SkateboardDeck2"},
+ {'name': "Hideout: Unlock phone app",
+ 'stage': Stages.H,
+ 'game_id': "camera"},
+ {'name': "Hideout: Vinyl joins the crew",
+ 'stage': Stages.H,
+ 'game_id': "girl1"},
+ {'name': "Hideout: Solace joins the crew",
+ 'stage': Stages.H,
+ 'game_id': "dummy"},
+
+ {'name': "Versum Hill: Main street Robo Post graffiti",
+ 'stage': Stages.VH1,
+ 'game_id': "UnlockGraffiti_grafTex_L4"},
+ {'name': "Versum Hill: Behind glass graffiti",
+ 'stage': Stages.VH1,
+ 'game_id': "UnlockGraffiti_grafTex_L3"},
+ {'name': "Versum Hill: Office room graffiti",
+ 'stage': Stages.VH1,
+ 'game_id': "UnlockGraffiti_grafTex_M4"},
+ {'name': "Versum Hill: Under bridge graffiti",
+ 'stage': Stages.VH2,
+ 'game_id': "UnlockGraffiti_grafTex_XL4"},
+ {'name': "Versum Hill: Train rail ledge skateboard",
+ 'stage': Stages.VH2,
+ 'game_id': "SkateboardDeck6"},
+ {'name': "Versum Hill: Train station CD",
+ 'stage': Stages.VH2,
+ 'game_id': "MusicTrack_PreciousThing"},
+ {'name': "Versum Hill: Billboard platform outfit",
+ 'stage': Stages.VH2,
+ 'game_id': "MetalheadOutfit3"},
+ {'name': "Versum Hill: Hilltop Robo Post CD",
+ 'stage': Stages.VH2,
+ 'game_id': "MusicTrack_BounceUponATime"},
+ {'name': "Versum Hill: Hill secret skateboard",
+ 'stage': Stages.VH2,
+ 'game_id': "SkateboardDeck7"},
+ {'name': "Versum Hill: Rooftop CD",
+ 'stage': Stages.VH2,
+ 'game_id': "MusicTrack_NextToMe"},
+ {'name': "Versum Hill: Wallrunning challenge reward",
+ 'stage': Stages.VH2,
+ 'game_id': "UnlockGraffiti_grafTex_M3"},
+ {'name': "Versum Hill: Manual challenge reward",
+ 'stage': Stages.VH2,
+ 'game_id': "UnlockGraffiti_grafTex_L2"},
+ {'name': "Versum Hill: Corner challenge reward",
+ 'stage': Stages.VH2,
+ 'game_id': "UnlockGraffiti_grafTex_M13"},
+ {'name': "Versum Hill: Side street alley outfit",
+ 'stage': Stages.VH3,
+ 'game_id': "MetalheadOutfit4"},
+ {'name': "Versum Hill: Side street secret skateboard",
+ 'stage': Stages.VH3,
+ 'game_id': "SkateboardDeck9"},
+ {'name': "Versum Hill: Basketball court alley skateboard",
+ 'stage': Stages.VH4,
+ 'game_id': "SkateboardDeck5"},
+ {'name': "Versum Hill: Basketball court Robo Post CD",
+ 'stage': Stages.VH4,
+ 'game_id': "MusicTrack_Operator"},
+ {'name': "Versum Hill: Underground mall billboard graffiti",
+ 'stage': Stages.VHO,
+ 'game_id': "UnlockGraffiti_grafTex_XL3"},
+ {'name': "Versum Hill: Underground mall vending machine skateboard",
+ 'stage': Stages.VHO,
+ 'game_id': "SkateboardDeck8"},
+ {'name': "Versum Hill: BMX gate outfit",
+ 'stage': Stages.VH1,
+ 'game_id': "AngelOutfit3"},
+ {'name': "Versum Hill: Glass floor skates",
+ 'stage': Stages.VH2,
+ 'game_id': "InlineSkates4"},
+ {'name': "Versum Hill: Basketball court shortcut CD",
+ 'stage': Stages.VH4,
+ 'game_id': "MusicTrack_GetEnuf"},
+ {'name': "Versum Hill: Rave joins the crew",
+ 'stage': Stages.VHO,
+ 'game_id': "angel"},
+ {'name': "Versum Hill: Frank joins the crew",
+ 'stage': Stages.VH2,
+ 'game_id': "frank"},
+ {'name': "Versum Hill: Rietveld joins the crew",
+ 'stage': Stages.VH4,
+ 'game_id': "jetpackBossPlayer"},
+ {'name': "Versum Hill: Big Polo",
+ 'stage': Stages.VH1,
+ 'game_id': "PoloBuilding/Mascot_Polo_sit_big"},
+ {'name': "Versum Hill: Trash Polo",
+ 'stage': Stages.VH1,
+ 'game_id': "TrashCluster (1)/Mascot_Polo_street"},
+ {'name': "Versum Hill: Fruit stand Polo",
+ 'stage': Stages.VHO,
+ 'game_id': "SecretRoom/Mascot_Polo_street"},
+
+ {'name': "Millennium Square: Center ramp graffiti",
+ 'stage': Stages.MS,
+ 'game_id': "UnlockGraffiti_grafTex_L6"},
+ {'name': "Millennium Square: Rooftop staircase graffiti",
+ 'stage': Stages.MS,
+ 'game_id': "UnlockGraffiti_grafTex_M8"},
+ {'name': "Millennium Square: Toilet graffiti",
+ 'stage': Stages.MS,
+ 'game_id': "UnlockGraffiti_grafTex_XL6"},
+ {'name': "Millennium Square: Trash graffiti",
+ 'stage': Stages.MS,
+ 'game_id': "UnlockGraffiti_grafTex_M5"},
+ {'name': "Millennium Square: Center tower graffiti",
+ 'stage': Stages.MS,
+ 'game_id': "UnlockGraffiti_grafTex_M6"},
+ {'name': "Millennium Square: Rooftop billboard graffiti",
+ 'stage': Stages.MS,
+ 'game_id': "UnlockGraffiti_grafTex_XL7"},
+ {'name': "Millennium Square: Center Robo Post CD",
+ 'stage': Stages.MS,
+ 'game_id': "MusicTrack_FeelTheFunk"},
+ {'name': "Millennium Square: Parking garage Robo Post CD",
+ 'stage': Stages.MS,
+ 'game_id': "MusicTrack_Plume"},
+ {'name': "Millennium Square: Mall ledge outfit",
+ 'stage': Stages.MS,
+ 'game_id': "BlockGuyOutfit3"},
+ {'name': "Millennium Square: Alley rooftop outfit",
+ 'stage': Stages.MS,
+ 'game_id': "BlockGuyOutfit4"},
+ {'name': "Millennium Square: Alley staircase skateboard",
+ 'stage': Stages.MS,
+ 'game_id': "SkateboardDeck4"},
+ {'name': "Millennium Square: Secret painting skates",
+ 'stage': Stages.MS,
+ 'game_id': "InlineSkates2"},
+ {'name': "Millennium Square: Vending machine skates",
+ 'stage': Stages.MS,
+ 'game_id': "InlineSkates3"},
+ {'name': "Millennium Square: Walkway roof skates",
+ 'stage': Stages.MS,
+ 'game_id': "InlineSkates5"},
+ {'name': "Millennium Square: Alley ledge skates",
+ 'stage': Stages.MS,
+ 'game_id': "InlineSkates6"},
+ {'name': "Millennium Square: DJ Cyber joins the crew",
+ 'stage': Stages.MS,
+ 'game_id': "dj"},
+ {'name': "Millennium Square: Half pipe Polo",
+ 'stage': Stages.MS,
+ 'game_id': "propsSecretArea/Mascot_Polo_street"},
+
+ {'name': "Brink Terminal: Upside grind challenge reward",
+ 'stage': Stages.BT1,
+ 'game_id': "UnlockGraffiti_grafTex_M10"},
+ {'name': "Brink Terminal: Manual challenge reward",
+ 'stage': Stages.BT1,
+ 'game_id': "UnlockGraffiti_grafTex_L8"},
+ {'name': "Brink Terminal: Score challenge reward",
+ 'stage': Stages.BT1,
+ 'game_id': "UnlockGraffiti_grafTex_M12"},
+ {'name': "Brink Terminal: Under square ledge graffiti",
+ 'stage': Stages.BT1,
+ 'game_id': "UnlockGraffiti_grafTex_L9"},
+ {'name': "Brink Terminal: Bus graffiti",
+ 'stage': Stages.BT1,
+ 'game_id': "UnlockGraffiti_grafTex_XL9"},
+ {'name': "Brink Terminal: Under square Robo Post graffiti",
+ 'stage': Stages.BT1,
+ 'game_id': "UnlockGraffiti_grafTex_M9"},
+ {'name': "Brink Terminal: BMX gate graffiti",
+ 'stage': Stages.BT1,
+ 'game_id': "UnlockGraffiti_grafTex_L7"},
+ {'name': "Brink Terminal: Square tower CD",
+ 'stage': Stages.BT1,
+ 'game_id': "MusicTrack_Chapter1Mixtape"},
+ {'name': "Brink Terminal: Trash CD",
+ 'stage': Stages.BT1,
+ 'game_id': "MusicTrack_HairDunNailsDun"},
+ {'name': "Brink Terminal: Shop roof outfit",
+ 'stage': Stages.BT1,
+ 'game_id': "AngelOutfit4"},
+ {'name': "Brink Terminal: Underground glass skates",
+ 'stage': Stages.BTO1,
+ 'game_id': "InlineSkates8"},
+ {'name': "Brink Terminal: Glass roof skates",
+ 'stage': Stages.BT1,
+ 'game_id': "InlineSkates10"},
+ {'name': "Brink Terminal: Mesh's skateboard",
+ 'stage': Stages.BTO2,
+ 'game_id': "SkateboardDeck10"}, # double check this one
+ {'name': "Brink Terminal: Underground ramp skates",
+ 'stage': Stages.BTO1,
+ 'game_id': "InlineSkates7"},
+ {'name': "Brink Terminal: Rooftop halfpipe graffiti",
+ 'stage': Stages.BT3,
+ 'game_id': "UnlockGraffiti_grafTex_M11"},
+ {'name': "Brink Terminal: Wire grind CD",
+ 'stage': Stages.BT2,
+ 'game_id': "MusicTrack_Watchyaback"},
+ {'name': "Brink Terminal: Rooftop glass CD",
+ 'stage': Stages.BT3,
+ 'game_id': "MusicTrack_Refuse"},
+ {'name': "Brink Terminal: Tower core outfit",
+ 'stage': Stages.BT3,
+ 'game_id': "SpacegirlOutfit4"},
+ {'name': "Brink Terminal: High rooftop outfit",
+ 'stage': Stages.BT3,
+ 'game_id': "WideKidOutfit3"},
+ {'name': "Brink Terminal: Ocean platform CD",
+ 'stage': Stages.BTO2,
+ 'game_id': "MusicTrack_ScrapedOnTheWayOut"},
+ {'name': "Brink Terminal: End of dock CD",
+ 'stage': Stages.BTO2,
+ 'game_id': "MusicTrack_Hwbouths"},
+ {'name': "Brink Terminal: Dock Robo Post outfit",
+ 'stage': Stages.BTO2,
+ 'game_id': "WideKidOutfit4"},
+ {'name': "Brink Terminal: Control room skates",
+ 'stage': Stages.BTO2,
+ 'game_id': "InlineSkates9"},
+ {'name': "Brink Terminal: Mesh joins the crew",
+ 'stage': Stages.BTO2,
+ 'game_id': "wideKid"},
+ {'name': "Brink Terminal: Eclipse joins the crew",
+ 'stage': Stages.BT1,
+ 'game_id': "medusa"},
+ {'name': "Brink Terminal: Behind glass Polo",
+ 'stage': Stages.BT1,
+ 'game_id': "KingFood (Bear)/Mascot_Polo_street"},
+
+ {'name': "Millennium Mall: Warehouse pallet graffiti",
+ 'stage': Stages.MM1,
+ 'game_id': "UnlockGraffiti_grafTex_L5"},
+ {'name': "Millennium Mall: Wall alcove graffiti",
+ 'stage': Stages.MM1,
+ 'game_id': "UnlockGraffiti_grafTex_XL10"},
+ {'name': "Millennium Mall: Maintenance shaft CD",
+ 'stage': Stages.MM1,
+ 'game_id': "MusicTrack_MissingBreak"},
+ {'name': "Millennium Mall: Glass cylinder CD",
+ 'stage': Stages.MM1,
+ 'game_id': "MusicTrack_DAPEOPLE"},
+ {'name': "Millennium Mall: Lower Robo Post outfit",
+ 'stage': Stages.MM1,
+ 'game_id': "SpacegirlOutfit3"},
+ {'name': "Millennium Mall: Atrium vending machine graffiti",
+ 'stage': Stages.MM2,
+ 'game_id': "UnlockGraffiti_grafTex_M15"},
+ {'name': "Millennium Mall: Trick challenge reward",
+ 'stage': Stages.MM2,
+ 'game_id': "UnlockGraffiti_grafTex_XL8"},
+ {'name': "Millennium Mall: Slide challenge reward",
+ 'stage': Stages.MM2,
+ 'game_id': "UnlockGraffiti_grafTex_L10"},
+ {'name': "Millennium Mall: Fish challenge reward",
+ 'stage': Stages.MM2,
+ 'game_id': "UnlockGraffiti_grafTex_L12"},
+ {'name': "Millennium Mall: Score challenge reward",
+ 'stage': Stages.MM2,
+ 'game_id': "UnlockGraffiti_grafTex_XL11"},
+ {'name': "Millennium Mall: Atrium top floor Robo Post CD",
+ 'stage': Stages.MM2,
+ 'game_id': "MusicTrack_TwoDaysOff"},
+ {'name': "Millennium Mall: Atrium top floor floating CD",
+ 'stage': Stages.MM2,
+ 'game_id': "MusicTrack_Spectres"},
+ {'name': "Millennium Mall: Atrium top floor BMX",
+ 'stage': Stages.MM2,
+ 'game_id': "BMXBike2"},
+ {'name': "Millennium Mall: Theater entrance BMX",
+ 'stage': Stages.MM2,
+ 'game_id': "BMXBike3"},
+ {'name': "Millennium Mall: Atrium BMX gate BMX",
+ 'stage': Stages.MM2,
+ 'game_id': "BMXBike5"},
+ {'name': "Millennium Mall: Upside down rail outfit",
+ 'stage': Stages.MM2,
+ 'game_id': "BunGirlOutfit3"},
+ {'name': "Millennium Mall: Theater stage corner graffiti",
+ 'stage': Stages.MM3,
+ 'game_id': "UnlockGraffiti_grafTex_L15"},
+ {'name': "Millennium Mall: Theater hanging billboards graffiti",
+ 'stage': Stages.MM3,
+ 'game_id': "UnlockGraffiti_grafTex_XL15"},
+ {'name': "Millennium Mall: Theater garage graffiti",
+ 'stage': Stages.MM3,
+ 'game_id': "UnlockGraffiti_grafTex_M16"},
+ {'name': "Millennium Mall: Theater maintenance CD",
+ 'stage': Stages.MM3,
+ 'game_id': "MusicTrack_WannaKno"},
+ {'name': "Millennium Mall: Race track Robo Post CD",
+ 'stage': Stages.MMO2,
+ 'game_id': "MusicTrack_StateOfMind"},
+ {'name': "Millennium Mall: Hanging lights CD",
+ 'stage': Stages.MMO1,
+ 'game_id': "MusicTrack_Chapter2Mixtape"},
+ {'name': "Millennium Mall: Shine joins the crew",
+ 'stage': Stages.MM3,
+ 'game_id': "bunGirl"},
+ {'name': "Millennium Mall: DOT.EXE joins the crew",
+ 'stage': Stages.MM2,
+ 'game_id': "eightBall"},
+
+ {'name': "Pyramid Island: Lower rooftop graffiti",
+ 'stage': Stages.PI1,
+ 'game_id': "UnlockGraffiti_grafTex_L18"},
+ {'name': "Pyramid Island: Polo graffiti",
+ 'stage': Stages.PI1,
+ 'game_id': "UnlockGraffiti_grafTex_L16"},
+ {'name': "Pyramid Island: Above entrance graffiti",
+ 'stage': Stages.PI1,
+ 'game_id': "UnlockGraffiti_grafTex_XL16"},
+ {'name': "Pyramid Island: BMX gate BMX",
+ 'stage': Stages.PI1,
+ 'game_id': "BMXBike6"},
+ {'name': "Pyramid Island: Quarter pipe rooftop graffiti",
+ 'stage': Stages.PI2,
+ 'game_id': "UnlockGraffiti_grafTex_M17"},
+ {'name': "Pyramid Island: Supply port Robo Post CD",
+ 'stage': Stages.PI2,
+ 'game_id': "MusicTrack_Trinitron"},
+ {'name': "Pyramid Island: Above gate ledge CD",
+ 'stage': Stages.PI2,
+ 'game_id': "MusicTrack_Agua"},
+ {'name': "Pyramid Island: Smoke hole BMX",
+ 'stage': Stages.PI2,
+ 'game_id': "BMXBike8"},
+ {'name': "Pyramid Island: Above gate rail outfit",
+ 'stage': Stages.PI2,
+ 'game_id': "VinylOutfit3"},
+ {'name': "Pyramid Island: Rail loop outfit",
+ 'stage': Stages.PI2,
+ 'game_id': "BunGirlOutfit4"},
+ {'name': "Pyramid Island: Score challenge reward",
+ 'stage': Stages.PI2,
+ 'game_id': "UnlockGraffiti_grafTex_XL2"},
+ {'name': "Pyramid Island: Score challenge 2 reward",
+ 'stage': Stages.PI2,
+ 'game_id': "UnlockGraffiti_grafTex_L13"},
+ {'name': "Pyramid Island: Quarter pipe challenge reward",
+ 'stage': Stages.PI2,
+ 'game_id': "UnlockGraffiti_grafTex_XL12"},
+ {'name': "Pyramid Island: Wind turbines CD",
+ 'stage': Stages.PI3,
+ 'game_id': "MusicTrack_YouCanSayHi"},
+ {'name': "Pyramid Island: Shortcut glass CD",
+ 'stage': Stages.PI3,
+ 'game_id': "MusicTrack_Chromebies"},
+ {'name': "Pyramid Island: Turret jump CD",
+ 'stage': Stages.PI3,
+ 'game_id': "MusicTrack_ChuckinUp"},
+ {'name': "Pyramid Island: Helipad BMX",
+ 'stage': Stages.PI3,
+ 'game_id': "BMXBike7"},
+ {'name': "Pyramid Island: Pipe outfit",
+ 'stage': Stages.PI3,
+ 'game_id': "PufferGirlOutfit3"},
+ {'name': "Pyramid Island: Trash outfit",
+ 'stage': Stages.PI3,
+ 'game_id': "PufferGirlOutfit4"},
+ {'name': "Pyramid Island: Pyramid top CD",
+ 'stage': Stages.PI4,
+ 'game_id': "MusicTrack_BigCityLife"},
+ {'name': "Pyramid Island: Pyramid top Robo Post CD",
+ 'stage': Stages.PI4,
+ 'game_id': "MusicTrack_Chapter3Mixtape"},
+ {'name': "Pyramid Island: Maze outfit",
+ 'stage': Stages.PIO,
+ 'game_id': "VinylOutfit4"},
+ {'name': "Pyramid Island: Rise joins the crew",
+ 'stage': Stages.PI4,
+ 'game_id': "pufferGirl"},
+ {'name': "Pyramid Island: Devil Theory joins the crew",
+ 'stage': Stages.PI3,
+ 'game_id': "boarder"},
+ {'name': "Pyramid Island: Polo pile 1",
+ 'stage': Stages.PI1,
+ 'game_id': "Secret01Trash/Mascot_Polo_sit_big_wave"},
+ {'name': "Pyramid Island: Polo pile 2",
+ 'stage': Stages.PI1,
+ 'game_id': "Secret01Trash/Mascot_Polo_sit_big_wave (1)"},
+ {'name': "Pyramid Island: Polo pile 3",
+ 'stage': Stages.PI1,
+ 'game_id': "Secret01Trash/Mascot_Polo_sit_big_wave (2)"},
+ {'name': "Pyramid Island: Polo pile 4",
+ 'stage': Stages.PI1,
+ 'game_id': "Secret01Trash/Mascot_Polo_sit_big_wave (3)"},
+ {'name': "Pyramid Island: Maze glass Polo",
+ 'stage': Stages.PIO,
+ 'game_id': "Start/Mascot_Polo_sit_big (1)"},
+ {'name': "Pyramid Island: Maze classroom Polo",
+ 'stage': Stages.PIO,
+ 'game_id': "PeteRoom/Mascot_Polo_sit_big_wave (1)"},
+ {'name': "Pyramid Island: Maze vent Polo",
+ 'stage': Stages.PIO,
+ 'game_id': "CheckerRoom/Mascot_Polo_street"},
+ {'name': "Pyramid Island: Big maze Polo",
+ 'stage': Stages.PIO,
+ 'game_id': "YellowPoloRoom/Mascot_Polo_sit_big"},
+ {'name': "Pyramid Island: Maze desk Polo",
+ 'stage': Stages.PIO,
+ 'game_id': "PoloRoom/Mascot_Polo_sit_big"},
+ {'name': "Pyramid Island: Maze forklift Polo",
+ 'stage': Stages.PIO,
+ 'game_id': "ForkliftRoom/Mascot_Polo_sit_big_wave"},
+
+ {'name': "Mataan: Robo Post graffiti",
+ 'stage': Stages.MA1,
+ 'game_id': "UnlockGraffiti_grafTex_XL17"},
+ {'name': "Mataan: Secret ledge BMX",
+ 'stage': Stages.MA1,
+ 'game_id': "BMXBike9"},
+ {'name': "Mataan: Highway rooftop BMX",
+ 'stage': Stages.MA1,
+ 'game_id': "BMXBike10"},
+ {'name': "Mataan: Trash CD",
+ 'stage': Stages.MA2,
+ 'game_id': "MusicTrack_JackDaFunk"},
+ {'name': "Mataan: Half pipe CD",
+ 'stage': Stages.MA2,
+ 'game_id': "MusicTrack_FunkExpress"},
+ {'name': "Mataan: Across bull horns graffiti",
+ 'stage': Stages.MA2,
+ 'game_id': "UnlockGraffiti_grafTex_L17"},
+ {'name': "Mataan: Small rooftop graffiti",
+ 'stage': Stages.MA2,
+ 'game_id': "UnlockGraffiti_grafTex_M18"},
+ {'name': "Mataan: Trash graffiti",
+ 'stage': Stages.MA2,
+ 'game_id': "UnlockGraffiti_grafTex_XL5"},
+ {'name': "Mataan: Deep city Robo Post CD",
+ 'stage': Stages.MA3,
+ 'game_id': "MusicTrack_LastHoorah"},
+ {'name': "Mataan: Deep city tower CD",
+ 'stage': Stages.MA3,
+ 'game_id': "MusicTrack_Chapter4Mixtape"},
+ {'name': "Mataan: Race challenge reward",
+ 'stage': Stages.MA3,
+ 'game_id': "UnlockGraffiti_grafTex_M14"},
+ {'name': "Mataan: Wallrunning challenge reward",
+ 'stage': Stages.MA3,
+ 'game_id': "UnlockGraffiti_grafTex_L14"},
+ {'name': "Mataan: Score challenge reward",
+ 'stage': Stages.MA3,
+ 'game_id': "UnlockGraffiti_grafTex_XL13"},
+ {'name': "Mataan: Deep city vent jump BMX",
+ 'stage': Stages.MA3,
+ 'game_id': "BMXBike4"},
+ {'name': "Mataan: Deep city side wires outfit",
+ 'stage': Stages.MA3,
+ 'game_id': "DummyOutfit3"},
+ {'name': "Mataan: Deep city center island outfit",
+ 'stage': Stages.MA3,
+ 'game_id': "DummyOutfit4"},
+ {'name': "Mataan: Red light rail graffiti",
+ 'stage': Stages.MAO,
+ 'game_id': "UnlockGraffiti_grafTex_XL18"},
+ {'name': "Mataan: Red light side alley outfit",
+ 'stage': Stages.MAO,
+ 'game_id': "RingDudeOutfit3"},
+ {'name': "Mataan: Statue hand outfit",
+ 'stage': Stages.MA4,
+ 'game_id': "RingDudeOutfit4"},
+ {'name': "Mataan: Crane CD",
+ 'stage': Stages.MA5,
+ 'game_id': "MusicTrack_InThePocket"},
+ {'name': "Mataan: Elephant tower glass outfit",
+ 'stage': Stages.MA5,
+ 'game_id': "LegendFaceOutfit3"},
+ {'name': "Mataan: Helipad outfit",
+ 'stage': Stages.MA5,
+ 'game_id': "LegendFaceOutfit4"},
+ {'name': "Mataan: Vending machine CD",
+ 'stage': Stages.MA5,
+ 'game_id': "MusicTrack_Iridium"},
+ {'name': "Mataan: Coil joins the crew",
+ 'stage': Stages.MA5,
+ 'game_id': "ringdude"},
+ {'name': "Mataan: Flesh Prince joins the crew",
+ 'stage': Stages.MA5,
+ 'game_id': "prince"},
+ {'name': "Mataan: Futurism joins the crew",
+ 'stage': Stages.MA5,
+ 'game_id': "futureGirl"},
+ {'name': "Mataan: Trash Polo",
+ 'stage': Stages.MA2,
+ 'game_id': "PropsMallArea/Mascot_Polo_street"},
+ {'name': "Mataan: Shopping Polo",
+ 'stage': Stages.MA5,
+ 'game_id': "propsMarket/Mascot_Polo_street"},
+
+ {'name': "Tagged 5 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf5"},
+ {'name': "Tagged 10 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf10"},
+ {'name': "Tagged 15 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf15"},
+ {'name': "Tagged 20 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf20"},
+ {'name': "Tagged 25 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf25"},
+ {'name': "Tagged 30 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf30"},
+ {'name': "Tagged 35 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf35"},
+ {'name': "Tagged 40 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf40"},
+ {'name': "Tagged 45 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf45"},
+ {'name': "Tagged 50 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf50"},
+ {'name': "Tagged 55 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf55"},
+ {'name': "Tagged 60 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf60"},
+ {'name': "Tagged 65 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf65"},
+ {'name': "Tagged 70 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf70"},
+ {'name': "Tagged 75 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf75"},
+ {'name': "Tagged 80 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf80"},
+ {'name': "Tagged 85 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf85"},
+ {'name': "Tagged 90 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf90"},
+ {'name': "Tagged 95 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf95"},
+ {'name': "Tagged 100 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf100"},
+ {'name': "Tagged 105 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf105"},
+ {'name': "Tagged 110 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf110"},
+ {'name': "Tagged 115 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf115"},
+ {'name': "Tagged 120 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf120"},
+ {'name': "Tagged 125 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf125"},
+ {'name': "Tagged 130 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf130"},
+ {'name': "Tagged 135 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf135"},
+ {'name': "Tagged 140 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf140"},
+ {'name': "Tagged 145 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf145"},
+ {'name': "Tagged 150 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf150"},
+ {'name': "Tagged 155 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf155"},
+ {'name': "Tagged 160 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf160"},
+ {'name': "Tagged 165 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf165"},
+ {'name': "Tagged 170 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf170"},
+ {'name': "Tagged 175 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf175"},
+ {'name': "Tagged 180 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf180"},
+ {'name': "Tagged 185 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf185"},
+ {'name': "Tagged 190 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf190"},
+ {'name': "Tagged 195 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf195"},
+ {'name': "Tagged 200 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf200"},
+ {'name': "Tagged 205 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf205"},
+ {'name': "Tagged 210 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf210"},
+ {'name': "Tagged 215 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf215"},
+ {'name': "Tagged 220 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf220"},
+ {'name': "Tagged 225 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf225"},
+ {'name': "Tagged 230 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf230"},
+ {'name': "Tagged 235 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf235"},
+ {'name': "Tagged 240 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf240"},
+ {'name': "Tagged 245 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf245"},
+ {'name': "Tagged 250 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf250"},
+ {'name': "Tagged 255 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf255"},
+ {'name': "Tagged 260 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf260"},
+ {'name': "Tagged 265 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf265"},
+ {'name': "Tagged 270 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf270"},
+ {'name': "Tagged 275 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf275"},
+ {'name': "Tagged 280 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf280"},
+ {'name': "Tagged 285 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf285"},
+ {'name': "Tagged 290 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf290"},
+ {'name': "Tagged 295 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf295"},
+ {'name': "Tagged 300 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf300"},
+ {'name': "Tagged 305 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf305"},
+ {'name': "Tagged 310 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf310"},
+ {'name': "Tagged 315 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf315"},
+ {'name': "Tagged 320 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf320"},
+ {'name': "Tagged 325 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf325"},
+ {'name': "Tagged 330 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf330"},
+ {'name': "Tagged 335 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf335"},
+ {'name': "Tagged 340 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf340"},
+ {'name': "Tagged 345 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf345"},
+ {'name': "Tagged 350 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf350"},
+ {'name': "Tagged 355 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf355"},
+ {'name': "Tagged 360 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf360"},
+ {'name': "Tagged 365 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf365"},
+ {'name': "Tagged 370 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf370"},
+ {'name': "Tagged 375 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf375"},
+ {'name': "Tagged 380 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf380"},
+ {'name': "Tagged 385 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf385"},
+ {'name': "Tagged 389 Graffiti Spots",
+ 'stage': Stages.Misc,
+ 'game_id': "graf389"},
+]
+
+
+event_table: List[EventDict] = [
+ {'name': "Versum Hill: Complete Chapter 1",
+ 'stage': Stages.VH4,
+ 'item': "Chapter Completed"},
+ {'name': "Brink Terminal: Complete Chapter 2",
+ 'stage': Stages.BT3,
+ 'item': "Chapter Completed"},
+ {'name': "Millennium Mall: Complete Chapter 3",
+ 'stage': Stages.MM3,
+ 'item': "Chapter Completed"},
+ {'name': "Pyramid Island: Complete Chapter 4",
+ 'stage': Stages.PI3,
+ 'item': "Chapter Completed"},
+ {'name': "Defeat Faux",
+ 'stage': Stages.MA5,
+ 'item': "Victory"},
+]
\ No newline at end of file
diff --git a/worlds/bomb_rush_cyberfunk/Options.py b/worlds/bomb_rush_cyberfunk/Options.py
new file mode 100644
index 000000000000..80831d064526
--- /dev/null
+++ b/worlds/bomb_rush_cyberfunk/Options.py
@@ -0,0 +1,200 @@
+from dataclasses import dataclass
+from Options import Choice, Toggle, DefaultOnToggle, Range, DeathLink, PerGameCommonOptions
+import typing
+
+if typing.TYPE_CHECKING:
+ from random import Random
+else:
+ Random = typing.Any
+
+
+class Logic(Choice):
+ """
+ Choose the logic used by the randomizer.
+ """
+ display_name = "Logic"
+ option_glitchless = 0
+ option_glitched = 1
+ default = 0
+
+
+class SkipIntro(DefaultOnToggle):
+ """
+ Skips escaping the police station.
+
+ Graffiti spots tagged during the intro will not unlock items.
+ """
+ display_name = "Skip Intro"
+
+
+class SkipDreams(Toggle):
+ """
+ Skips the dream sequences at the end of each chapter.
+
+ This can be changed later in the options menu inside the Archipelago phone app.
+ """
+ display_name = "Skip Dreams"
+
+
+class SkipHands(Toggle):
+ """
+ Skips spraying the lion statue hands after the dream in Chapter 5.
+ """
+ display_name = "Skip Statue Hands"
+
+
+class TotalRep(Range):
+ """
+ Change the total amount of REP in your world.
+
+ At least 960 REP is needed to finish the game.
+
+ Will be rounded to the nearest number divisible by 8.
+ """
+ display_name = "Total REP"
+ range_start = 1000
+ range_end = 2000
+ default = 1400
+
+ def round_to_nearest_step(self):
+ rem: int = self.value % 8
+ if rem >= 5:
+ self.value = self.value - rem + 8
+ else:
+ self.value = self.value - rem
+
+ def get_rep_item_counts(self, random_source: Random, location_count: int) -> typing.List[int]:
+ def increment_item(item: int) -> int:
+ if item >= 32:
+ item = 48
+ else:
+ item += 8
+ return item
+
+ items = [8]*location_count
+ while sum(items) < self.value:
+ index = random_source.randint(0, location_count-1)
+ while items[index] >= 48:
+ index = random_source.randint(0, location_count-1)
+ items[index] = increment_item(items[index])
+
+ while sum(items) > self.value:
+ index = random_source.randint(0, location_count-1)
+ while not (items[index] == 16 or items[index] == 24 or items[index] == 32):
+ index = random_source.randint(0, location_count-1)
+ items[index] -= 8
+
+ return [items.count(8), items.count(16), items.count(24), items.count(32), items.count(48)]
+
+
+class EndingREP(Toggle):
+ """
+ Changes the final boss to require 1000 REP instead of 960 REP to start.
+ """
+ display_name = "Extra REP Required"
+
+
+class StartStyle(Choice):
+ """
+ Choose which movestyle to start with.
+ """
+ display_name = "Starting Movestyle"
+ option_skateboard = 2
+ option_inline_skates = 3
+ option_bmx = 1
+ default = 2
+
+
+class LimitedGraffiti(Toggle):
+ """
+ Each graffiti design can only be used a limited number of times before being removed from your inventory.
+
+ In some cases, such as completing a dream, using graffiti to defeat enemies, or spraying over your own graffiti, uses will not be counted.
+
+ If enabled, doing graffiti is disabled during crew battles, to prevent softlocking.
+ """
+ display_name = "Limited Graffiti"
+
+
+class SGraffiti(Choice):
+ """
+ Choose if small graffiti should be separate, meaning that you will need to switch characters every time you run out, or combined, meaning that unlocking new characters will add 5 uses that any character can use.
+
+ Has no effect if Limited Graffiti is disabled.
+ """
+ display_name = "Small Graffiti Uses"
+ option_separate = 0
+ option_combined = 1
+ default = 0
+
+
+class JunkPhotos(Toggle):
+ """
+ Skip taking pictures of Polo for items.
+ """
+ display_name = "Skip Polo Photos"
+
+
+class DontSavePhotos(Toggle):
+ """
+ Photos taken with the Camera app will not be saved.
+
+ This can be changed later in the options menu inside the Archipelago phone app.
+ """
+ display_name = "Don't Save Photos"
+
+
+class ScoreDifficulty(Choice):
+ """
+ Alters the score required to win score challenges and crew battles.
+
+ This can be changed later in the options menu inside the Archipelago phone app.
+ """
+ display_name = "Score Difficulty"
+ option_normal = 0
+ option_medium = 1
+ option_hard = 2
+ option_very_hard = 3
+ option_extreme = 4
+ default = 0
+
+
+class DamageMultiplier(Range):
+ """
+ Multiplies all damage received.
+
+ At 3x, most damage will OHKO the player, including falling into pits.
+ At 6x, all damage will OHKO the player.
+
+ This can be changed later in the options menu inside the Archipelago phone app.
+ """
+ display_name = "Damage Multiplier"
+ range_start = 1
+ range_end = 6
+ default = 1
+
+
+class BRCDeathLink(DeathLink):
+ """
+ When you die, everyone dies. The reverse is also true.
+
+ This can be changed later in the options menu inside the Archipelago phone app.
+ """
+
+
+@dataclass
+class BombRushCyberfunkOptions(PerGameCommonOptions):
+ logic: Logic
+ skip_intro: SkipIntro
+ skip_dreams: SkipDreams
+ skip_statue_hands: SkipHands
+ total_rep: TotalRep
+ extra_rep_required: EndingREP
+ starting_movestyle: StartStyle
+ limited_graffiti: LimitedGraffiti
+ small_graffiti_uses: SGraffiti
+ skip_polo_photos: JunkPhotos
+ dont_save_photos: DontSavePhotos
+ score_difficulty: ScoreDifficulty
+ damage_multiplier: DamageMultiplier
+ death_link: BRCDeathLink
diff --git a/worlds/bomb_rush_cyberfunk/Regions.py b/worlds/bomb_rush_cyberfunk/Regions.py
new file mode 100644
index 000000000000..206ae4ea5d6b
--- /dev/null
+++ b/worlds/bomb_rush_cyberfunk/Regions.py
@@ -0,0 +1,103 @@
+from typing import Dict
+
+
+class Stages:
+ Misc = "Misc"
+ H = "Hideout"
+ VH1 = "Versum Hill"
+ VH2 = "Versum Hill - After Roadblock"
+ VHO = "Versum Hill - Underground Mall"
+ VH3 = "Versum Hill - Side Street"
+ VH4 = "Versum Hill - Basketball Court"
+ MS = "Millennium Square"
+ BT1 = "Brink Terminal"
+ BTO1 = "Brink Terminal - Underground"
+ BTO2 = "Brink Terminal - Dock"
+ BT2 = "Brink Terminal - Planet Plaza"
+ BT3 = "Brink Terminal - Tower"
+ MM1 = "Millennium Mall"
+ MMO1 = "Millennium Mall - Hanging Lights"
+ MM2 = "Millennium Mall - Atrium"
+ MMO2 = "Millennium Mall - Race Track"
+ MM3 = "Millennium Mall - Theater"
+ PI1 = "Pyramid Island - Base"
+ PI2 = "Pyramid Island - After Gate"
+ PIO = "Pyramid Island - Maze"
+ PI3 = "Pyramid Island - Upper Areas"
+ PI4 = "Pyramid Island - Top"
+ MA1 = "Mataan - Streets"
+ MA2 = "Mataan - After Smoke Wall"
+ MA3 = "Mataan - Deep City"
+ MAO = "Mataan - Red Light District"
+ MA4 = "Mataan - Lion Statue"
+ MA5 = "Mataan - Skyscrapers"
+
+
+region_exits: Dict[str, str] = {
+ Stages.Misc: [Stages.H],
+ Stages.H: [Stages.Misc,
+ Stages.VH1,
+ Stages.MS,
+ Stages.MA1],
+ Stages.VH1: [Stages.H,
+ Stages.VH2],
+ Stages.VH2: [Stages.H,
+ Stages.VH1,
+ Stages.MS,
+ Stages.VHO,
+ Stages.VH3,
+ Stages.VH4],
+ Stages.VHO: [Stages.VH2],
+ Stages.VH3: [Stages.VH2],
+ Stages.VH4: [Stages.VH2,
+ Stages.VH1],
+ Stages.MS: [Stages.VH2,
+ Stages.BT1,
+ Stages.MM1,
+ Stages.PI1,
+ Stages.MA1],
+ Stages.BT1: [Stages.MS,
+ Stages.BTO1,
+ Stages.BTO2,
+ Stages.BT2],
+ Stages.BTO1: [Stages.BT1],
+ Stages.BTO2: [Stages.BT1],
+ Stages.BT2: [Stages.BT1,
+ Stages.BT3],
+ Stages.BT3: [Stages.BT1,
+ Stages.BT2],
+ Stages.MM1: [Stages.MS,
+ Stages.MMO1,
+ Stages.MM2],
+ Stages.MMO1: [Stages.MM1],
+ Stages.MM2: [Stages.MM1,
+ Stages.MMO2,
+ Stages.MM3],
+ Stages.MMO2: [Stages.MM2],
+ Stages.MM3: [Stages.MM2,
+ Stages.MM1],
+ Stages.PI1: [Stages.MS,
+ Stages.PI2],
+ Stages.PI2: [Stages.PI1,
+ Stages.PIO,
+ Stages.PI3],
+ Stages.PIO: [Stages.PI2],
+ Stages.PI3: [Stages.PI1,
+ Stages.PI2,
+ Stages.PI4],
+ Stages.PI4: [Stages.PI1,
+ Stages.PI2,
+ Stages.PI3],
+ Stages.MA1: [Stages.H,
+ Stages.MS,
+ Stages.MA2],
+ Stages.MA2: [Stages.MA1,
+ Stages.MA3],
+ Stages.MA3: [Stages.MA2,
+ Stages.MAO,
+ Stages.MA4],
+ Stages.MAO: [Stages.MA3],
+ Stages.MA4: [Stages.MA3,
+ Stages.MA5],
+ Stages.MA5: [Stages.MA1]
+}
diff --git a/worlds/bomb_rush_cyberfunk/Rules.py b/worlds/bomb_rush_cyberfunk/Rules.py
new file mode 100644
index 000000000000..8f283ee613b7
--- /dev/null
+++ b/worlds/bomb_rush_cyberfunk/Rules.py
@@ -0,0 +1,1041 @@
+from worlds.generic.Rules import set_rule, add_rule
+from BaseClasses import CollectionState
+from typing import Dict
+from .Regions import Stages
+
+
+def graffitiM(state: CollectionState, player: int, limit: bool, spots: int) -> bool:
+ return state.count_group_unique("graffitim", player) * 7 >= spots if limit \
+ else state.has_group("graffitim", player)
+
+
+def graffitiL(state: CollectionState, player: int, limit: bool, spots: int) -> bool:
+ return state.count_group_unique("graffitil", player) * 6 >= spots if limit \
+ else state.has_group("graffitil", player)
+
+
+def graffitiXL(state: CollectionState, player: int, limit: bool, spots: int) -> bool:
+ return state.count_group_unique("graffitixl", player) * 4 >= spots if limit \
+ else state.has_group("graffitixl", player)
+
+
+def skateboard(state: CollectionState, player: int, movestyle: int) -> bool:
+ return True if movestyle == 2 else state.has_group("skateboard", player)
+
+
+def inline_skates(state: CollectionState, player: int, movestyle: int) -> bool:
+ return True if movestyle == 3 else state.has_group("skates", player)
+
+
+def bmx(state: CollectionState, player: int, movestyle: int) -> bool:
+ return True if movestyle == 1 else state.has_group("bmx", player)
+
+
+def camera(state: CollectionState, player: int) -> bool:
+ return state.has("Camera App", player)
+
+
+def is_girl(state: CollectionState, player: int) -> bool:
+ return state.has_group("girl", player)
+
+
+def current_chapter(state: CollectionState, player: int, chapter: int) -> bool:
+ return state.has("Chapter Completed", player, chapter-1)
+
+
+def versum_hill_entrance(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 20)
+
+
+def versum_hill_ch1_roadblock(state: CollectionState, player: int, limit: bool) -> bool:
+ return graffitiL(state, player, limit, 10)
+
+
+def versum_hill_challenge1(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 50)
+
+
+def versum_hill_challenge2(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 58)
+
+
+def versum_hill_challenge3(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 65)
+
+
+def versum_hill_all_challenges(state: CollectionState, player: int) -> bool:
+ return versum_hill_challenge3(state, player)
+
+
+def versum_hill_basketball_court(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 90)
+
+
+def versum_hill_oldhead(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 120)
+
+
+def versum_hill_crew_battle(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ if glitched:
+ return (
+ rep(state, player, 90)
+ and graffitiM(state, player, limit, 98)
+ )
+ else:
+ return (
+ rep(state, player, 90)
+ and graffitiM(state, player, limit, 27)
+ )
+
+
+def versum_hill_rietveld(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ if glitched:
+ return (
+ current_chapter(state, player, 2)
+ and graffitiM(state, player, limit, 114)
+ )
+ else:
+ return (
+ current_chapter(state, player, 2)
+ and graffitiM(state, player, limit, 67)
+ )
+
+
+def versum_hill_rave(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ if glitched:
+ if current_chapter(state, player, 4):
+ return (
+ graffitiL(state, player, limit, 90)
+ and graffitiXL(state, player, limit, 51)
+ )
+ elif current_chapter(state, player, 3):
+ return (
+ graffitiL(state, player, limit, 89)
+ and graffitiXL(state, player, limit, 51)
+ )
+ else:
+ return (
+ graffitiL(state, player, limit, 85)
+ and graffitiXL(state, player, limit, 48)
+ )
+ else:
+ return (
+ graffitiL(state, player, limit, 26)
+ and graffitiXL(state, player, limit, 10)
+ )
+
+
+def millennium_square_entrance(state: CollectionState, player: int) -> bool:
+ return current_chapter(state, player, 2)
+
+
+def brink_terminal_entrance(state: CollectionState, player: int) -> bool:
+ return (
+ is_girl(state, player)
+ and rep(state, player, 180)
+ and current_chapter(state, player, 2)
+ )
+
+
+def brink_terminal_challenge1(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 188)
+
+
+def brink_terminal_challenge2(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 200)
+
+
+def brink_terminal_challenge3(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 220)
+
+
+def brink_terminal_all_challenges(state: CollectionState, player: int) -> bool:
+ return brink_terminal_challenge3(state, player)
+
+
+def brink_terminal_plaza(state: CollectionState, player: int) -> bool:
+ return brink_terminal_all_challenges(state, player)
+
+
+def brink_terminal_tower(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 280)
+
+
+def brink_terminal_oldhead_underground(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 250)
+
+
+def brink_terminal_oldhead_dock(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 320)
+
+
+def brink_terminal_crew_battle(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ if glitched:
+ return (
+ rep(state, player, 280)
+ and graffitiL(state, player, limit, 103)
+ )
+ else:
+ return (
+ rep(state, player, 280)
+ and graffitiL(state, player, limit, 62)
+ )
+
+
+def brink_terminal_mesh(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ if glitched:
+ return (
+ graffitiM(state, player, limit, 114)
+ and graffitiXL(state, player, limit, 45)
+ )
+ else:
+ return (
+ graffitiM(state, player, limit, 67)
+ and graffitiXL(state, player, limit, 45)
+ )
+
+
+def millennium_mall_entrance(state: CollectionState, player: int) -> bool:
+ return (
+ rep(state, player, 380)
+ and current_chapter(state, player, 3)
+ )
+
+
+def millennium_mall_oldhead_ceiling(state: CollectionState, player: int, limit: bool) -> bool:
+ return (
+ rep(state, player, 580)
+ or millennium_mall_theater(state, player, limit)
+ )
+
+
+def millennium_mall_switch(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ if glitched:
+ return (
+ graffitiM(state, player, limit, 114)
+ and current_chapter(state, player, 3)
+ )
+ else:
+ return (
+ graffitiM(state, player, limit, 72)
+ and current_chapter(state, player, 3)
+ )
+
+
+def millennium_mall_big(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ return millennium_mall_switch(state, player, limit, glitched)
+
+
+def millennium_mall_oldhead_race(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 530)
+
+
+def millennium_mall_challenge1(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 434)
+
+
+def millennium_mall_challenge2(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 442)
+
+
+def millennium_mall_challenge3(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 450)
+
+
+def millennium_mall_challenge4(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 458)
+
+
+def millennium_mall_all_challenges(state: CollectionState, player: int) -> bool:
+ return millennium_mall_challenge4(state, player)
+
+
+def millennium_mall_theater(state: CollectionState, player: int, limit: bool) -> bool:
+ return (
+ rep(state, player, 491)
+ and graffitiM(state, player, limit, 78)
+ )
+
+
+def millennium_mall_crew_battle(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ if glitched:
+ return (
+ rep(state, player, 491)
+ and graffitiM(state, player, limit, 114)
+ and graffitiL(state, player, limit, 107)
+ )
+ else:
+ return (
+ rep(state, player, 491)
+ and graffitiM(state, player, limit, 78)
+ and graffitiL(state, player, limit, 80)
+ )
+
+
+def pyramid_island_entrance(state: CollectionState, player: int) -> bool:
+ return current_chapter(state, player, 4)
+
+
+def pyramid_island_gate(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 620)
+
+
+def pyramid_island_oldhead(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 780)
+
+
+def pyramid_island_challenge1(state: CollectionState, player: int) -> bool:
+ return (
+ rep(state, player, 630)
+ and current_chapter(state, player, 4)
+ )
+
+
+def pyramid_island_race(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ if glitched:
+ return (
+ pyramid_island_challenge1(state, player)
+ and graffitiL(state, player, limit, 108)
+ )
+ else:
+ return (
+ pyramid_island_challenge1(state, player)
+ and graffitiL(state, player, limit, 93)
+ )
+
+
+def pyramid_island_challenge2(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 650)
+
+
+def pyramid_island_challenge3(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 660)
+
+
+def pyramid_island_all_challenges(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ if glitched:
+ return (
+ graffitiM(state, player, limit, 114)
+ and rep(state, player, 660)
+ )
+ else:
+ return (
+ graffitiM(state, player, limit, 88)
+ and rep(state, player, 660)
+ )
+
+
+def pyramid_island_upper_half(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ return pyramid_island_all_challenges(state, player, limit, glitched)
+
+
+def pyramid_island_crew_battle(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ if glitched:
+ return (
+ rep(state, player, 730)
+ and graffitiL(state, player, limit, 108)
+ )
+ else:
+ return (
+ rep(state, player, 730)
+ and graffitiL(state, player, limit, 97)
+ )
+
+
+def pyramid_island_top(state: CollectionState, player: int) -> bool:
+ return current_chapter(state, player, 5)
+
+
+def mataan_entrance(state: CollectionState, player: int) -> bool:
+ return current_chapter(state, player, 2)
+
+
+def mataan_smoke_wall(state: CollectionState, player: int) -> bool:
+ return (
+ current_chapter(state, player, 5)
+ and rep(state, player, 850)
+ )
+
+
+def mataan_challenge1(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ if glitched:
+ return (
+ current_chapter(state, player, 5)
+ and rep(state, player, 864)
+ and graffitiL(state, player, limit, 108)
+ )
+ else:
+ return (
+ current_chapter(state, player, 5)
+ and rep(state, player, 864)
+ and graffitiL(state, player, limit, 98)
+ )
+
+
+def mataan_deep_city(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ return mataan_challenge1(state, player, limit, glitched)
+
+
+def mataan_oldhead(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 935)
+
+
+def mataan_challenge2(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ if glitched:
+ return (
+ rep(state, player, 880)
+ and graffitiXL(state, player, limit, 59)
+ )
+ else:
+ return (
+ rep(state, player, 880)
+ and graffitiXL(state, player, limit, 57)
+ )
+
+
+def mataan_challenge3(state: CollectionState, player: int) -> bool:
+ return rep(state, player, 920)
+
+
+def mataan_all_challenges(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ return (
+ mataan_challenge2(state, player, limit, glitched)
+ and mataan_challenge3(state, player)
+ )
+
+
+def mataan_smoke_wall2(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ return (
+ mataan_all_challenges(state, player, limit, glitched)
+ and rep(state, player, 960)
+ )
+
+
+def mataan_crew_battle(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ if glitched:
+ return (
+ mataan_smoke_wall2(state, player, limit, glitched)
+ and graffitiM(state, player, limit, 122)
+ and graffitiXL(state, player, limit, 59)
+ )
+ else:
+ return (
+ mataan_smoke_wall2(state, player, limit, glitched)
+ and graffitiM(state, player, limit, 117)
+ and graffitiXL(state, player, limit, 57)
+ )
+
+
+def mataan_deepest(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ return mataan_crew_battle(state, player, limit, glitched)
+
+
+def mataan_faux(state: CollectionState, player: int, limit: bool, glitched: bool) -> bool:
+ return (
+ mataan_deepest(state, player, limit, glitched)
+ and graffitiM(state, player, limit, 122)
+ )
+
+
+def spots_s_glitchless(state: CollectionState, player: int, limit: bool, access_cache: Dict[str, bool]) -> int:
+ total: int = 10
+ conditions: Dict[str, int] = {
+ "versum_hill_entrance": 1,
+ "versum_hill_ch1_roadblock": 11,
+ "chapter2": 12,
+ "versum_hill_oldhead": 1,
+ "brink_terminal_entrance": 9,
+ "brink_terminal_plaza": 3,
+ "brink_terminal_tower": 0,
+ "chapter3": 6,
+ "brink_terminal_oldhead_dock": 1,
+ "millennium_mall_entrance": 3,
+ "millennium_mall_switch": 4,
+ "millennium_mall_theater": 3,
+ "chapter4": 2,
+ "pyramid_island_gate": 5,
+ "pyramid_island_upper_half": 8,
+ "pyramid_island_oldhead": 2,
+ "mataan_smoke_wall": 3,
+ "mataan_deep_city": 5,
+ "mataan_oldhead": 3,
+ "mataan_deepest": 2
+ }
+
+ for access_name, graffiti_count in conditions.items():
+ if access_cache[access_name]:
+ total += graffiti_count
+ else:
+ break
+
+ if limit:
+ sprayable: int = 5 + (state.count_group_unique("characters", player) * 5)
+ if total <= sprayable:
+ return total
+ else:
+ return sprayable
+ else:
+ return total
+
+
+def spots_s_glitched(state: CollectionState, player: int, limit: bool, access_cache: Dict[str, bool]) -> int:
+ total: int = 75
+ conditions: Dict[str, int] = {
+ "brink_terminal_entrance": 13,
+ "chapter3": 6
+ }
+
+ for access_name, graffiti_count in conditions.items():
+ if access_cache[access_name]:
+ total += graffiti_count
+ else:
+ break
+
+ if limit:
+ sprayable: int = 5 + (state.count_group_unique("characters", player) * 5)
+ if total <= sprayable:
+ return total
+ else:
+ return sprayable
+ else:
+ return total
+
+
+def spots_m_glitchless(state: CollectionState, player: int, limit: bool, access_cache: Dict[str, bool]) -> int:
+ total: int = 4
+ conditions: Dict[str, int] = {
+ "versum_hill_entrance": 3,
+ "versum_hill_ch1_roadblock": 13,
+ "versum_hill_all_challenges": 3,
+ "chapter2": 16,
+ "versum_hill_oldhead": 4,
+ "brink_terminal_entrance": 13,
+ "brink_terminal_plaza": 4,
+ "brink_terminal_tower": 0,
+ "chapter3": 3,
+ "brink_terminal_oldhead_dock": 4,
+ "millennium_mall_entrance": 5,
+ "millennium_mall_big": 6,
+ "millennium_mall_theater": 4,
+ "chapter4": 2,
+ "millennium_mall_oldhead_ceiling": 1,
+ "pyramid_island_gate": 3,
+ "pyramid_island_upper_half": 8,
+ "chapter5": 2,
+ "pyramid_island_oldhead": 5,
+ "mataan_deep_city": 7,
+ "skateboard": 1,
+ "mataan_oldhead": 1,
+ "mataan_smoke_wall2": 1,
+ "mataan_deepest": 10
+ }
+
+ for access_name, graffiti_count in conditions.items():
+ if access_cache[access_name]:
+ total += graffiti_count
+ elif access_name != "skateboard":
+ break
+
+ if limit:
+ sprayable: int = state.count_group_unique("graffitim", player) * 7
+ if total <= sprayable:
+ return total
+ else:
+ return sprayable
+ else:
+ if state.has_group("graffitim", player):
+ return total
+ else:
+ return 0
+
+
+def spots_m_glitched(state: CollectionState, player: int, limit: bool, access_cache: Dict[str, bool]) -> int:
+ total: int = 99
+ conditions: Dict[str, int] = {
+ "brink_terminal_entrance": 21,
+ "chapter3": 3
+ }
+
+ for access_name, graffiti_count in conditions.items():
+ if access_cache[access_name]:
+ total += graffiti_count
+ else:
+ break
+
+ if limit:
+ sprayable: int = state.count_group_unique("graffitim", player) * 7
+ if total <= sprayable:
+ return total
+ else:
+ return sprayable
+ else:
+ if state.has_group("graffitim", player):
+ return total
+ else:
+ return 0
+
+
+def spots_l_glitchless(state: CollectionState, player: int, limit: bool, access_cache: Dict[str, bool]) -> int:
+ total: int = 7
+ conditions: Dict[str, int] = {
+ "inline_skates": 1,
+ "versum_hill_entrance": 2,
+ "versum_hill_ch1_roadblock": 13,
+ "versum_hill_all_challenges": 1,
+ "chapter2": 14,
+ "versum_hill_oldhead": 2,
+ "brink_terminal_entrance": 10,
+ "brink_terminal_plaza": 2,
+ "brink_terminal_oldhead_underground": 1,
+ "brink_terminal_tower": 1,
+ "chapter3": 4,
+ "brink_terminal_oldhead_dock": 4,
+ "millennium_mall_entrance": 3,
+ "millennium_mall_big": 8,
+ "millennium_mall_theater": 4,
+ "chapter4": 5,
+ "millennium_mall_oldhead_ceiling": 3,
+ "pyramid_island_gate": 4,
+ "pyramid_island_upper_half": 5,
+ "pyramid_island_crew_battle": 1,
+ "chapter5": 1,
+ "pyramid_island_oldhead": 2,
+ "mataan_smoke_wall": 1,
+ "mataan_deep_city": 2,
+ "skateboard": 1,
+ "mataan_oldhead": 2,
+ "mataan_deepest": 7
+ }
+
+ for access_name, graffiti_count in conditions.items():
+ if access_cache[access_name]:
+ total += graffiti_count
+ elif not (access_name == "inline_skates" or access_name == "skateboard"):
+ break
+
+ if limit:
+ sprayable: int = state.count_group_unique("graffitil", player) * 6
+ if total <= sprayable:
+ return total
+ else:
+ return sprayable
+ else:
+ if state.has_group("graffitil", player):
+ return total
+ else:
+ return 0
+
+
+def spots_l_glitched(state: CollectionState, player: int, limit: bool, access_cache: Dict[str, bool]) -> int:
+ total: int = 88
+ conditions: Dict[str, int] = {
+ "brink_terminal_entrance": 18,
+ "chapter3": 4,
+ "chapter4": 1
+ }
+
+ for access_name, graffiti_count in conditions.items():
+ if access_cache[access_name]:
+ total += graffiti_count
+ else:
+ break
+
+ if limit:
+ sprayable: int = state.count_group_unique("graffitil", player) * 6
+ if total <= sprayable:
+ return total
+ else:
+ return sprayable
+ else:
+ if state.has_group("graffitil", player):
+ return total
+ else:
+ return 0
+
+
+def spots_xl_glitchless(state: CollectionState, player: int, limit: bool, access_cache: Dict[str, bool]) -> int:
+ total: int = 3
+ conditions: Dict[str, int] = {
+ "versum_hill_ch1_roadblock": 6,
+ "versum_hill_basketball_court": 1,
+ "chapter2": 9,
+ "brink_terminal_entrance": 3,
+ "brink_terminal_plaza": 1,
+ "brink_terminal_oldhead_underground": 1,
+ "brink_terminal_tower": 1,
+ "chapter3": 3,
+ "brink_terminal_oldhead_dock": 2,
+ "millennium_mall_entrance": 2,
+ "millennium_mall_big": 5,
+ "millennium_mall_theater": 5,
+ "chapter4": 3,
+ "millennium_mall_oldhead_ceiling": 1,
+ "pyramid_island_upper_half": 5,
+ "pyramid_island_oldhead": 3,
+ "mataan_smoke_wall": 2,
+ "mataan_deep_city": 2,
+ "mataan_oldhead": 2,
+ "mataan_deepest": 2
+ }
+
+ for access_name, graffiti_count in conditions.items():
+ if access_cache[access_name]:
+ total += graffiti_count
+ else:
+ break
+
+ if limit:
+ sprayable: int = state.count_group_unique("graffitixl", player) * 4
+ if total <= sprayable:
+ return total
+ else:
+ return sprayable
+ else:
+ if state.has_group("graffitixl", player):
+ return total
+ else:
+ return 0
+
+
+def spots_xl_glitched(state: CollectionState, player: int, limit: bool, access_cache: Dict[str, bool]) -> int:
+ total: int = 51
+ conditions: Dict[str, int] = {
+ "brink_terminal_entrance": 7,
+ "chapter3": 3,
+ "chapter4": 1
+ }
+
+ for access_name, graffiti_count in conditions.items():
+ if access_cache[access_name]:
+ total += graffiti_count
+ else:
+ break
+
+ if limit:
+ sprayable: int = state.count_group_unique("graffitixl", player) * 4
+ if total <= sprayable:
+ return total
+ else:
+ return sprayable
+ else:
+ if state.has_group("graffitixl", player):
+ return total
+ else:
+ return 0
+
+
+def build_access_cache(state: CollectionState, player: int, movestyle: int, limit: bool, glitched: bool) -> Dict[str, bool]:
+ funcs: Dict[str, tuple] = {
+ "versum_hill_entrance": (state, player),
+ "versum_hill_ch1_roadblock": (state, player, limit),
+ "versum_hill_oldhead": (state, player),
+ "versum_hill_all_challenges": (state, player),
+ "versum_hill_basketball_court": (state, player),
+ "brink_terminal_entrance": (state, player),
+ "brink_terminal_oldhead_underground": (state, player),
+ "brink_terminal_oldhead_dock": (state, player),
+ "brink_terminal_plaza": (state, player),
+ "brink_terminal_tower": (state, player),
+ "millennium_mall_entrance": (state, player),
+ "millennium_mall_switch": (state, player, limit, glitched),
+ "millennium_mall_oldhead_ceiling": (state, player, limit),
+ "millennium_mall_big": (state, player, limit, glitched),
+ "millennium_mall_theater": (state, player, limit),
+ "pyramid_island_gate": (state, player),
+ "pyramid_island_oldhead": (state, player),
+ "pyramid_island_upper_half": (state, player, limit, glitched),
+ "pyramid_island_crew_battle": (state, player, limit, glitched),
+ "mataan_smoke_wall": (state, player),
+ "mataan_deep_city": (state, player, limit, glitched),
+ "mataan_oldhead": (state, player),
+ "mataan_smoke_wall2": (state, player, limit, glitched),
+ "mataan_deepest": (state, player, limit, glitched)
+ }
+
+ access_cache: Dict[str, bool] = {
+ "skateboard": skateboard(state, player, movestyle),
+ "inline_skates": inline_skates(state, player, movestyle),
+ "chapter2": current_chapter(state, player, 2),
+ "chapter3": current_chapter(state, player, 3),
+ "chapter4": current_chapter(state, player, 4),
+ "chapter5": current_chapter(state, player, 5)
+ }
+
+ stop: bool = False
+ for fname, fvars in funcs.items():
+ if stop:
+ access_cache[fname] = False
+ continue
+ func = globals()[fname]
+ access: bool = func(*fvars)
+ access_cache[fname] = access
+ if not access and "oldhead" not in fname:
+ stop = True
+
+ return access_cache
+
+
+def graffiti_spots(state: CollectionState, player: int, movestyle: int, limit: bool, glitched: bool, spots: int) -> bool:
+ access_cache = build_access_cache(state, player, movestyle, limit, glitched)
+
+ total: int = 0
+
+ if glitched:
+ total = spots_s_glitched(state, player, limit, access_cache) \
+ + spots_m_glitched(state, player, limit, access_cache) \
+ + spots_l_glitched(state, player, limit, access_cache) \
+ + spots_xl_glitched(state, player, limit, access_cache)
+ else:
+ total = spots_s_glitchless(state, player, limit, access_cache) \
+ + spots_m_glitchless(state, player, limit, access_cache) \
+ + spots_l_glitchless(state, player, limit, access_cache) \
+ + spots_xl_glitchless(state, player, limit, access_cache)
+
+ return total >= spots
+
+
+def rep(state: CollectionState, player: int, required: int) -> bool:
+ return state.has("rep", player, required)
+
+
+def rules(brcworld):
+ multiworld = brcworld.multiworld
+ player = brcworld.player
+
+ movestyle = brcworld.options.starting_movestyle
+ limit = brcworld.options.limited_graffiti
+ glitched = brcworld.options.logic
+ extra = brcworld.options.extra_rep_required
+ photos = not brcworld.options.skip_polo_photos
+
+ # entrances
+ for e in multiworld.get_region(Stages.BT1, player).entrances:
+ set_rule(e, lambda state: brink_terminal_entrance(state, player))
+
+ if not glitched:
+ # versum hill
+ for e in multiworld.get_region(Stages.VH1, player).entrances:
+ set_rule(e, lambda state: versum_hill_entrance(state, player))
+ for e in multiworld.get_region(Stages.VH2, player).entrances:
+ set_rule(e, lambda state: versum_hill_ch1_roadblock(state, player, limit))
+ for e in multiworld.get_region(Stages.VHO, player).entrances:
+ set_rule(e, lambda state: versum_hill_oldhead(state, player))
+ for e in multiworld.get_region(Stages.VH3, player).entrances:
+ set_rule(e, lambda state: versum_hill_all_challenges(state, player))
+ for e in multiworld.get_region(Stages.VH4, player).entrances:
+ set_rule(e, lambda state: versum_hill_basketball_court(state, player))
+
+ # millennium square
+ for e in multiworld.get_region(Stages.MS, player).entrances:
+ set_rule(e, lambda state: millennium_square_entrance(state, player))
+
+ # brink terminal
+ for e in multiworld.get_region(Stages.BTO1, player).entrances:
+ set_rule(e, lambda state: brink_terminal_oldhead_underground(state, player))
+ for e in multiworld.get_region(Stages.BTO2, player).entrances:
+ set_rule(e, lambda state: brink_terminal_oldhead_dock(state, player))
+ for e in multiworld.get_region(Stages.BT2, player).entrances:
+ set_rule(e, lambda state: brink_terminal_plaza(state, player))
+ for e in multiworld.get_region(Stages.BT3, player).entrances:
+ set_rule(e, lambda state: brink_terminal_tower(state, player))
+
+ # millennium mall
+ for e in multiworld.get_region(Stages.MM1, player).entrances:
+ set_rule(e, lambda state: millennium_mall_entrance(state, player))
+ for e in multiworld.get_region(Stages.MMO1, player).entrances:
+ set_rule(e, lambda state: millennium_mall_oldhead_ceiling(state, player, limit))
+ for e in multiworld.get_region(Stages.MM2, player).entrances:
+ set_rule(e, lambda state: millennium_mall_big(state, player, limit, glitched))
+ for e in multiworld.get_region(Stages.MMO2, player).entrances:
+ set_rule(e, lambda state: millennium_mall_oldhead_race(state, player))
+ for e in multiworld.get_region(Stages.MM3, player).entrances:
+ set_rule(e, lambda state: millennium_mall_theater(state, player, limit))
+
+ # pyramid island
+ for e in multiworld.get_region(Stages.PI1, player).entrances:
+ set_rule(e, lambda state: pyramid_island_entrance(state, player))
+ for e in multiworld.get_region(Stages.PI2, player).entrances:
+ set_rule(e, lambda state: pyramid_island_gate(state, player))
+ for e in multiworld.get_region(Stages.PIO, player).entrances:
+ set_rule(e, lambda state: pyramid_island_oldhead(state, player))
+ for e in multiworld.get_region(Stages.PI3, player).entrances:
+ set_rule(e, lambda state: pyramid_island_upper_half(state, player, limit, glitched))
+ for e in multiworld.get_region(Stages.PI4, player).entrances:
+ set_rule(e, lambda state: pyramid_island_top(state, player))
+
+ # mataan
+ for e in multiworld.get_region(Stages.MA1, player).entrances:
+ set_rule(e, lambda state: mataan_entrance(state, player))
+ for e in multiworld.get_region(Stages.MA2, player).entrances:
+ set_rule(e, lambda state: mataan_smoke_wall(state, player))
+ for e in multiworld.get_region(Stages.MA3, player).entrances:
+ set_rule(e, lambda state: mataan_deep_city(state, player, limit, glitched))
+ for e in multiworld.get_region(Stages.MAO, player).entrances:
+ set_rule(e, lambda state: mataan_oldhead(state, player))
+ for e in multiworld.get_region(Stages.MA4, player).entrances:
+ set_rule(e, lambda state: mataan_smoke_wall2(state, player, limit, glitched))
+ for e in multiworld.get_region(Stages.MA5, player).entrances:
+ set_rule(e, lambda state: mataan_deepest(state, player, limit, glitched))
+
+ # locations
+ # hideout
+ set_rule(multiworld.get_location("Hideout: BMX garage skateboard", player),
+ lambda state: bmx(state, player, movestyle))
+ set_rule(multiworld.get_location("Hideout: Unlock phone app", player),
+ lambda state: current_chapter(state, player, 2))
+ set_rule(multiworld.get_location("Hideout: Vinyl joins the crew", player),
+ lambda state: current_chapter(state, player, 4))
+ set_rule(multiworld.get_location("Hideout: Solace joins the crew", player),
+ lambda state: current_chapter(state, player, 5))
+
+ # versum hill
+ set_rule(multiworld.get_location("Versum Hill: Wallrunning challenge reward", player),
+ lambda state: versum_hill_challenge1(state, player))
+ set_rule(multiworld.get_location("Versum Hill: Manual challenge reward", player),
+ lambda state: versum_hill_challenge2(state, player))
+ set_rule(multiworld.get_location("Versum Hill: Corner challenge reward", player),
+ lambda state: versum_hill_challenge3(state, player))
+ set_rule(multiworld.get_location("Versum Hill: BMX gate outfit", player),
+ lambda state: bmx(state, player, movestyle))
+ set_rule(multiworld.get_location("Versum Hill: Glass floor skates", player),
+ lambda state: inline_skates(state, player, movestyle))
+ set_rule(multiworld.get_location("Versum Hill: Basketball court shortcut CD", player),
+ lambda state: current_chapter(state, player, 2))
+ set_rule(multiworld.get_location("Versum Hill: Rave joins the crew", player),
+ lambda state: versum_hill_rave(state, player, limit, glitched))
+ set_rule(multiworld.get_location("Versum Hill: Frank joins the crew", player),
+ lambda state: current_chapter(state, player, 2))
+ set_rule(multiworld.get_location("Versum Hill: Rietveld joins the crew", player),
+ lambda state: versum_hill_rietveld(state, player, limit, glitched))
+ if photos:
+ set_rule(multiworld.get_location("Versum Hill: Big Polo", player),
+ lambda state: camera(state, player))
+ set_rule(multiworld.get_location("Versum Hill: Trash Polo", player),
+ lambda state: camera(state, player))
+ set_rule(multiworld.get_location("Versum Hill: Fruit stand Polo", player),
+ lambda state: camera(state, player))
+
+ # millennium square
+ if photos:
+ set_rule(multiworld.get_location("Millennium Square: Half pipe Polo", player),
+ lambda state: camera(state, player))
+
+ # brink terminal
+ set_rule(multiworld.get_location("Brink Terminal: Upside grind challenge reward", player),
+ lambda state: brink_terminal_challenge1(state, player))
+ set_rule(multiworld.get_location("Brink Terminal: Manual challenge reward", player),
+ lambda state: brink_terminal_challenge2(state, player))
+ set_rule(multiworld.get_location("Brink Terminal: Score challenge reward", player),
+ lambda state: brink_terminal_challenge3(state, player))
+ set_rule(multiworld.get_location("Brink Terminal: BMX gate graffiti", player),
+ lambda state: bmx(state, player, movestyle))
+ set_rule(multiworld.get_location("Brink Terminal: Mesh's skateboard", player),
+ lambda state: brink_terminal_mesh(state, player, limit, glitched))
+ set_rule(multiworld.get_location("Brink Terminal: Rooftop glass CD", player),
+ lambda state: inline_skates(state, player, movestyle))
+ set_rule(multiworld.get_location("Brink Terminal: Mesh joins the crew", player),
+ lambda state: brink_terminal_mesh(state, player, limit, glitched))
+ set_rule(multiworld.get_location("Brink Terminal: Eclipse joins the crew", player),
+ lambda state: current_chapter(state, player, 3))
+ if photos:
+ set_rule(multiworld.get_location("Brink Terminal: Behind glass Polo", player),
+ lambda state: camera(state, player))
+
+ # millennium mall
+ set_rule(multiworld.get_location("Millennium Mall: Glass cylinder CD", player),
+ lambda state: inline_skates(state, player, movestyle))
+ set_rule(multiworld.get_location("Millennium Mall: Trick challenge reward", player),
+ lambda state: millennium_mall_challenge1(state, player))
+ set_rule(multiworld.get_location("Millennium Mall: Slide challenge reward", player),
+ lambda state: millennium_mall_challenge2(state, player))
+ set_rule(multiworld.get_location("Millennium Mall: Fish challenge reward", player),
+ lambda state: millennium_mall_challenge3(state, player))
+ set_rule(multiworld.get_location("Millennium Mall: Score challenge reward", player),
+ lambda state: millennium_mall_challenge4(state, player))
+ set_rule(multiworld.get_location("Millennium Mall: Atrium BMX gate BMX", player),
+ lambda state: bmx(state, player, movestyle))
+ set_rule(multiworld.get_location("Millennium Mall: Shine joins the crew", player),
+ lambda state: current_chapter(state, player, 4))
+ set_rule(multiworld.get_location("Millennium Mall: DOT.EXE joins the crew", player),
+ lambda state: current_chapter(state, player, 4))
+
+ # pyramid island
+ set_rule(multiworld.get_location("Pyramid Island: BMX gate BMX", player),
+ lambda state: bmx(state, player, movestyle))
+ set_rule(multiworld.get_location("Pyramid Island: Score challenge reward", player),
+ lambda state: pyramid_island_challenge1(state, player))
+ set_rule(multiworld.get_location("Pyramid Island: Score challenge 2 reward", player),
+ lambda state: pyramid_island_challenge2(state, player))
+ set_rule(multiworld.get_location("Pyramid Island: Quarter pipe challenge reward", player),
+ lambda state: pyramid_island_challenge3(state, player))
+ set_rule(multiworld.get_location("Pyramid Island: Shortcut glass CD", player),
+ lambda state: inline_skates(state, player, movestyle))
+ set_rule(multiworld.get_location("Pyramid Island: Maze outfit", player),
+ lambda state: skateboard(state, player, movestyle))
+ if not glitched:
+ add_rule(multiworld.get_location("Pyramid Island: Rise joins the crew", player),
+ lambda state: camera(state, player))
+ set_rule(multiworld.get_location("Pyramid Island: Devil Theory joins the crew", player),
+ lambda state: current_chapter(state, player, 5))
+ if photos:
+ set_rule(multiworld.get_location("Pyramid Island: Polo pile 1", player),
+ lambda state: camera(state, player))
+ set_rule(multiworld.get_location("Pyramid Island: Polo pile 2", player),
+ lambda state: camera(state, player))
+ set_rule(multiworld.get_location("Pyramid Island: Polo pile 3", player),
+ lambda state: camera(state, player))
+ set_rule(multiworld.get_location("Pyramid Island: Polo pile 4", player),
+ lambda state: camera(state, player))
+ set_rule(multiworld.get_location("Pyramid Island: Maze glass Polo", player),
+ lambda state: camera(state, player))
+ set_rule(multiworld.get_location("Pyramid Island: Maze classroom Polo", player),
+ lambda state: camera(state, player))
+ set_rule(multiworld.get_location("Pyramid Island: Maze vent Polo", player),
+ lambda state: camera(state, player))
+ set_rule(multiworld.get_location("Pyramid Island: Big maze Polo", player),
+ lambda state: camera(state, player))
+ set_rule(multiworld.get_location("Pyramid Island: Maze desk Polo", player),
+ lambda state: camera(state, player))
+ set_rule(multiworld.get_location("Pyramid Island: Maze forklift Polo", player),
+ lambda state: camera(state, player))
+
+ # mataan
+ set_rule(multiworld.get_location("Mataan: Race challenge reward", player),
+ lambda state: mataan_challenge1(state, player, limit, glitched))
+ set_rule(multiworld.get_location("Mataan: Wallrunning challenge reward", player),
+ lambda state: mataan_challenge2(state, player, limit, glitched))
+ set_rule(multiworld.get_location("Mataan: Score challenge reward", player),
+ lambda state: mataan_challenge3(state, player))
+ set_rule(multiworld.get_location("Mataan: Coil joins the crew", player),
+ lambda state: mataan_deepest(state, player, limit, glitched))
+ if photos:
+ set_rule(multiworld.get_location("Mataan: Trash Polo", player),
+ lambda state: camera(state, player))
+ set_rule(multiworld.get_location("Mataan: Shopping Polo", player),
+ lambda state: camera(state, player))
+
+ # events
+ set_rule(multiworld.get_location("Versum Hill: Complete Chapter 1", player),
+ lambda state: versum_hill_crew_battle(state, player, limit, glitched))
+ set_rule(multiworld.get_location("Brink Terminal: Complete Chapter 2", player),
+ lambda state: brink_terminal_crew_battle(state, player, limit, glitched))
+ set_rule(multiworld.get_location("Millennium Mall: Complete Chapter 3", player),
+ lambda state: millennium_mall_crew_battle(state, player, limit, glitched))
+ set_rule(multiworld.get_location("Pyramid Island: Complete Chapter 4", player),
+ lambda state: pyramid_island_crew_battle(state, player, limit, glitched))
+ set_rule(multiworld.get_location("Defeat Faux", player),
+ lambda state: mataan_faux(state, player, limit, glitched))
+
+ if extra:
+ add_rule(multiworld.get_location("Defeat Faux", player),
+ lambda state: rep(state, player, 1000))
+
+ # graffiti spots
+ spots: int = 0
+ while spots < 385:
+ spots += 5
+ set_rule(multiworld.get_location(f"Tagged {spots} Graffiti Spots", player),
+ lambda state, spot_count=spots: graffiti_spots(state, player, movestyle, limit, glitched, spot_count))
+
+ set_rule(multiworld.get_location("Tagged 389 Graffiti Spots", player),
+ lambda state: graffiti_spots(state, player, movestyle, limit, glitched, 389))
diff --git a/worlds/bomb_rush_cyberfunk/__init__.py b/worlds/bomb_rush_cyberfunk/__init__.py
new file mode 100644
index 000000000000..98926e335138
--- /dev/null
+++ b/worlds/bomb_rush_cyberfunk/__init__.py
@@ -0,0 +1,203 @@
+from typing import Any, Dict
+from BaseClasses import MultiWorld, Region, Location, Item, Tutorial, ItemClassification, CollectionState
+from worlds.AutoWorld import World, WebWorld
+from .Items import base_id, item_table, group_table, BRCType
+from .Locations import location_table, event_table
+from .Regions import region_exits
+from .Rules import rules
+from .Options import BombRushCyberfunkOptions, StartStyle
+
+
+class BombRushCyberfunkWeb(WebWorld):
+ theme = "ocean"
+ tutorials = [Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to setting up Bomb Rush Cyberfunk randomizer and connecting to an Archipelago Multiworld",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["TRPG"]
+ )]
+
+
+class BombRushCyberfunkWorld(World):
+ """Bomb Rush Cyberfunk is 1 second per second of advanced funkstyle. Battle rival crews and dispatch militarized
+ police to conquer the five boroughs of New Amsterdam. Become All City."""
+
+ game = "Bomb Rush Cyberfunk"
+ web = BombRushCyberfunkWeb()
+
+ item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)}
+ item_name_to_type = {item["name"]: item["type"] for item in item_table}
+ location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)}
+
+ item_name_groups = group_table
+ options_dataclass = BombRushCyberfunkOptions
+ options: BombRushCyberfunkOptions
+
+ def __init__(self, multiworld: MultiWorld, player: int):
+ super(BombRushCyberfunkWorld, self).__init__(multiworld, player)
+ self.item_classification: Dict[BRCType, ItemClassification] = {
+ BRCType.Music: ItemClassification.filler,
+ BRCType.GraffitiM: ItemClassification.progression,
+ BRCType.GraffitiL: ItemClassification.progression,
+ BRCType.GraffitiXL: ItemClassification.progression,
+ BRCType.Outfit: ItemClassification.filler,
+ BRCType.Character: ItemClassification.progression,
+ BRCType.REP: ItemClassification.progression_skip_balancing,
+ BRCType.Camera: ItemClassification.progression
+ }
+
+ def collect(self, state: "CollectionState", item: "Item") -> bool:
+ change = super().collect(state, item)
+ if change and "REP" in item.name:
+ rep: int = int(item.name[0:len(item.name)-4])
+ state.prog_items[item.player]["rep"] += rep
+ return change
+
+ def remove(self, state: "CollectionState", item: "Item") -> bool:
+ change = super().remove(state, item)
+ if change and "REP" in item.name:
+ rep: int = int(item.name[0:len(item.name)-4])
+ state.prog_items[item.player]["rep"] -= rep
+ return change
+
+ def set_rules(self):
+ rules(self)
+
+ def get_item_classification(self, item_type: BRCType) -> ItemClassification:
+ classification = ItemClassification.filler
+ if item_type in self.item_classification.keys():
+ classification = self.item_classification[item_type]
+
+ return classification
+
+ def create_item(self, name: str) -> "BombRushCyberfunkItem":
+ item_id: int = self.item_name_to_id[name]
+ item_type: BRCType = self.item_name_to_type[name]
+ classification = self.get_item_classification(item_type)
+
+ return BombRushCyberfunkItem(name, classification, item_id, self.player)
+
+ def create_event(self, event: str) -> "BombRushCyberfunkItem":
+ return BombRushCyberfunkItem(event, ItemClassification.progression_skip_balancing, None, self.player)
+
+ def get_filler_item_name(self) -> str:
+ item = self.random.choice(item_table)
+
+ while self.get_item_classification(item["type"]) == ItemClassification.progression:
+ item = self.random.choice(item_table)
+
+ return item["name"]
+
+ def generate_early(self):
+ if self.options.starting_movestyle == StartStyle.option_skateboard:
+ self.item_classification[BRCType.Skateboard] = ItemClassification.filler
+ else:
+ self.item_classification[BRCType.Skateboard] = ItemClassification.progression
+
+ if self.options.starting_movestyle == StartStyle.option_inline_skates:
+ self.item_classification[BRCType.InlineSkates] = ItemClassification.filler
+ else:
+ self.item_classification[BRCType.InlineSkates] = ItemClassification.progression
+
+ if self.options.starting_movestyle == StartStyle.option_bmx:
+ self.item_classification[BRCType.BMX] = ItemClassification.filler
+ else:
+ self.item_classification[BRCType.BMX] = ItemClassification.progression
+
+ def create_items(self):
+ rep_locations: int = 87
+ if self.options.skip_polo_photos:
+ rep_locations -= 17
+
+ self.options.total_rep.round_to_nearest_step()
+ rep_counts = self.options.total_rep.get_rep_item_counts(self.random, rep_locations)
+ #print(sum([8*rep_counts[0], 16*rep_counts[1], 24*rep_counts[2], 32*rep_counts[3], 48*rep_counts[4]]), \
+ # rep_counts)
+
+ pool = []
+
+ for item in item_table:
+ if "REP" in item["name"]:
+ count: int = 0
+
+ if item["name"] == "8 REP":
+ count = rep_counts[0]
+ elif item["name"] == "16 REP":
+ count = rep_counts[1]
+ elif item["name"] == "24 REP":
+ count = rep_counts[2]
+ elif item["name"] == "32 REP":
+ count = rep_counts[3]
+ elif item["name"] == "48 REP":
+ count = rep_counts[4]
+
+ if count > 0:
+ for _ in range(count):
+ pool.append(self.create_item(item["name"]))
+ else:
+ pool.append(self.create_item(item["name"]))
+
+ self.multiworld.itempool += pool
+
+ def create_regions(self):
+ multiworld = self.multiworld
+ player = self.player
+
+ menu = Region("Menu", player, multiworld)
+ multiworld.regions.append(menu)
+
+ for n in region_exits:
+ multiworld.regions += [Region(n, player, multiworld)]
+
+ menu.add_exits({"Hideout": "New Game"})
+
+ for n in region_exits:
+ self.get_region(n).add_exits(region_exits[n])
+
+ for index, loc in enumerate(location_table):
+ if self.options.skip_polo_photos and "Polo" in loc["game_id"]:
+ continue
+ stage: Region = self.get_region(loc["stage"])
+ stage.add_locations({loc["name"]: base_id + index})
+
+ for e in event_table:
+ stage: Region = self.get_region(e["stage"])
+ event = BombRushCyberfunkLocation(player, e["name"], None, stage)
+ event.show_in_spoiler = False
+ event.place_locked_item(self.create_event(e["item"]))
+ stage.locations += [event]
+
+ multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
+
+ def fill_slot_data(self) -> Dict[str, Any]:
+ options = self.options
+
+ slot_data: Dict[str, Any] = {
+ "locations": {loc["game_id"]: (base_id + index) for index, loc in enumerate(location_table)},
+ "logic": options.logic.value,
+ "skip_intro": bool(options.skip_intro.value),
+ "skip_dreams": bool(options.skip_dreams.value),
+ "skip_statue_hands": bool(options.skip_statue_hands.value),
+ "total_rep": options.total_rep.value,
+ "extra_rep_required": bool(options.extra_rep_required.value),
+ "starting_movestyle": options.starting_movestyle.value,
+ "limited_graffiti": bool(options.limited_graffiti.value),
+ "small_graffiti_uses": options.small_graffiti_uses.value,
+ "skip_polo_photos": bool(options.skip_polo_photos.value),
+ "dont_save_photos": bool(options.dont_save_photos.value),
+ "score_difficulty": int(options.score_difficulty.value),
+ "damage_multiplier": options.damage_multiplier.value,
+ "death_link": bool(options.death_link.value)
+ }
+
+ return slot_data
+
+
+class BombRushCyberfunkItem(Item):
+ game: str = "Bomb Rush Cyberfunk"
+
+
+class BombRushCyberfunkLocation(Location):
+ game: str = "Bomb Rush Cyberfunk"
diff --git a/worlds/bomb_rush_cyberfunk/docs/en_Bomb Rush Cyberfunk.md b/worlds/bomb_rush_cyberfunk/docs/en_Bomb Rush Cyberfunk.md
new file mode 100644
index 000000000000..c6866e489ffb
--- /dev/null
+++ b/worlds/bomb_rush_cyberfunk/docs/en_Bomb Rush Cyberfunk.md
@@ -0,0 +1,29 @@
+# Bomb Rush Cyberfunk
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains all the options you need to configure and export
+a config file.
+
+## What does randomization do in this game?
+
+The goal of Bomb Rush Cyberfunk randomizer is to defeat all rival crews in each borough of New Amsterdam. REP is no
+longer earned from doing graffiti, and is instead earned by finding it randomly in the multiworld.
+
+Items can be found by picking up any type of collectible, unlocking characters, taking pictures of Polo, and for every
+5 graffiti spots tagged. The types of items that can be found are Music, Graffiti (M), Graffiti (L), Graffiti (XL),
+Skateboards, Inline Skates, BMX, Outfits, Characters, REP, and the Camera.
+
+Several changes have been made to the game for a better experience as a randomizer:
+
+- The prelude in the police station can be skipped.
+- The map for each stage is always unlocked.
+- The taxi is always unlocked, but you will still need to visit each stage's taxi stop before you can use them.
+- No M, L, or XL graffiti is unlocked at the beginning.
+- Optionally, graffiti can be depleted after a certain number of uses.
+- All characters except Red are locked.
+- One single REP count is used throughout the game, instead of having separate totals for each stage. REP requirements
+are the same as the original game, but added together in order. At least 960 REP is needed to finish the game.
+
+The mod also adds two new apps to the phone, an "Encounter" app which lets you retry certain events early, and the
+"Archipelago" app which lets you view chat messages and change some options while playing.
\ No newline at end of file
diff --git a/worlds/bomb_rush_cyberfunk/docs/setup_en.md b/worlds/bomb_rush_cyberfunk/docs/setup_en.md
new file mode 100644
index 000000000000..14da25adb32b
--- /dev/null
+++ b/worlds/bomb_rush_cyberfunk/docs/setup_en.md
@@ -0,0 +1,41 @@
+# Bomb Rush Cyberfunk Multiworld Setup Guide
+
+## Quick Links
+
+- Bomb Rush Cyberfunk: [Steam](https://store.steampowered.com/app/1353230/Bomb_Rush_Cyberfunk/)
+- Archipelago Mod: [Thunderstore](https://thunderstore.io/c/bomb-rush-cyberfunk/p/TRPG/BRC_Archipelago/),
+[GitHub](https://github.com/TRPG0/BRC-Archipelago/releases)
+
+## Setup
+
+To install the Archipelago mod, you can use a mod manager like
+[r2modman](https://thunderstore.io/c/bomb-rush-cyberfunk/p/ebkr/r2modman/), or install manually by following these steps:
+
+1. Download and install [BepInEx 5.4.22 x64](https://github.com/BepInEx/BepInEx/releases/tag/v5.4.22) in your Bomb Rush
+Cyberfunk root folder. *Do not use any pre-release versions of BepInEx 6.*
+
+2. Start Bomb Rush Cyberfunk once so that BepInEx can create its required configuration files.
+
+3. Download the zip archive from the [releases](https://github.com/TRPG0/BRC-Archipelago/releases) page, and extract its
+contents into `BepInEx\plugins`.
+
+After installing Archipelago, there are some additional mods that can also be installed for a better experience:
+
+- [MoreMap](https://thunderstore.io/c/bomb-rush-cyberfunk/p/TRPG/MoreMap/) by TRPG
+ - Adds pins to the map for every type of collectible.
+- [FasterLoadTimes](https://thunderstore.io/c/bomb-rush-cyberfunk/p/cspotcode/FasterLoadTimes/) by cspotcode
+ - Load stages faster by skipping assets that are already loaded.
+- [CutsceneSkip](https://thunderstore.io/c/bomb-rush-cyberfunk/p/Jay/CutsceneSkip/) by Jay
+ - Makes every cutscene skippable.
+- [GimmeMyBoost](https://thunderstore.io/c/bomb-rush-cyberfunk/p/Yuri/GimmeMyBoost/) by Yuri
+ - Retains boost when loading into a new stage.
+- [DisableAnnoyingCutscenes](https://thunderstore.io/c/bomb-rush-cyberfunk/p/viliger/DisableAnnoyingCutscenes/) by viliger
+ - Disables the police cutscenes when increasing your heat level.
+- [FastTravel](https://thunderstore.io/c/bomb-rush-cyberfunk/p/tari/FastTravel/) by tari
+ - Adds an app to the phone to call for a taxi from anywhere.
+
+## Connecting
+
+To connect to an Archipelago server, click one of the Archipelago buttons next to the save files. If the save file is
+blank or already has randomizer save data, it will open a menu where you can enter the server address and port, your
+name, and a password if necessary. Then click the check mark to connect to the server.
\ No newline at end of file
diff --git a/worlds/bomb_rush_cyberfunk/test/__init__.py b/worlds/bomb_rush_cyberfunk/test/__init__.py
new file mode 100644
index 000000000000..9cd6c3a504bf
--- /dev/null
+++ b/worlds/bomb_rush_cyberfunk/test/__init__.py
@@ -0,0 +1,5 @@
+from test.bases import WorldTestBase
+
+
+class BombRushCyberfunkTestBase(WorldTestBase):
+ game = "Bomb Rush Cyberfunk"
\ No newline at end of file
diff --git a/worlds/bomb_rush_cyberfunk/test/test_graffiti_spots.py b/worlds/bomb_rush_cyberfunk/test/test_graffiti_spots.py
new file mode 100644
index 000000000000..af5402323038
--- /dev/null
+++ b/worlds/bomb_rush_cyberfunk/test/test_graffiti_spots.py
@@ -0,0 +1,284 @@
+from . import BombRushCyberfunkTestBase
+from ..Rules import build_access_cache, spots_s_glitchless, spots_s_glitched, spots_m_glitchless, spots_m_glitched, \
+ spots_l_glitchless, spots_l_glitched, spots_xl_glitched, spots_xl_glitchless
+
+
+class TestSpotsGlitchless(BombRushCyberfunkTestBase):
+ @property
+ def run_default_tests(self) -> bool:
+ return False
+
+ def test_spots_glitchless(self) -> None:
+ player = self.player
+
+ self.collect_by_name([
+ "Graffiti (M - OVERWHELMME)",
+ "Graffiti (L - WHOLE SIXER)",
+ "Graffiti (XL - Gold Rush)"
+ ])
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 1 - hideout
+ self.assertEqual(10, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(4, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(7, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(3, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.collect_by_name("Inline Skates (Glaciers)")
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+ self.assertEqual(8, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 20
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 1 - VH1-2
+ self.assertEqual(22, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(20, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(23, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(9, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 65
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 1 - VH3
+ self.assertEqual(23, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(24, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 90
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 1 - VH4
+ self.assertEqual(10, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["Chapter Completed"] = 1
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 2 - MS + MA1
+ self.assertEqual(34, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(39, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(38, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(19, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 120
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 2 - VHO
+ self.assertEqual(35, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(43, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(40, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.collect_by_name("Bel")
+ self.multiworld.state.prog_items[player]["rep"] = 180
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 2 - BT1
+ self.assertEqual(44, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(56, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(50, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(22, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 220
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 2 - BT2
+ self.assertEqual(47, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(60, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(52, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(23, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 250
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 2 - BTO1
+ self.assertEqual(53, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(24, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 280
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 2 - BT3 / chapter 3 - MS
+ self.assertEqual(58, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(28, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 320
+ self.multiworld.state.prog_items[player]["Chapter Completed"] = 2
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 2 - BTO2 / chapter 3 - MS
+ self.assertEqual(54, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(67, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(62, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(30, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 380
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 3 - MM1-2
+ self.assertEqual(61, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(78, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(73, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(37, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 491
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 3 - MM3
+ self.assertEqual(64, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(82, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(77, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(42, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["Chapter Completed"] = 3
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 4 - MS / BT / MMO1 / PI1
+ self.assertEqual(66, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(85, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(85, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(46, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 620
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 4 - PI2
+ self.assertEqual(71, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(88, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(89, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 660
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 4 - PI3
+ self.assertEqual(79, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(96, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(94, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(51, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 730
+ self.multiworld.state.prog_items[player]["Chapter Completed"] = 4
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 5 - PI4
+ self.assertEqual(98, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(96, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 780
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 5 - PIO
+ self.assertEqual(81, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(103, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(98, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(54, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 850
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 5 - MA2
+ self.assertEqual(84, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(99, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(56, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 864
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 5 - MA3
+ self.assertEqual(89, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(111, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(102, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(58, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 935
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 5 - MAO
+ self.assertEqual(92, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(112, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(104, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(60, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["rep"] = 960
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, False)
+
+ # chapter 5 - MA4-5
+ self.assertEqual(94, spots_s_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(123, spots_m_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(111, spots_l_glitchless(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(62, spots_xl_glitchless(self.multiworld.state, player, False, access_cache))
+
+
+class TestSpotsGlitched(BombRushCyberfunkTestBase):
+ options = {
+ "logic": "glitched"
+ }
+
+ @property
+ def run_default_tests(self) -> bool:
+ return False
+
+ def test_spots_glitched(self) -> None:
+ player = self.player
+
+ self.collect_by_name([
+ "Graffiti (M - OVERWHELMME)",
+ "Graffiti (L - WHOLE SIXER)",
+ "Graffiti (XL - Gold Rush)"
+ ])
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, True)
+
+ self.assertEqual(75, spots_s_glitched(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(99, spots_m_glitched(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(88, spots_l_glitched(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(51, spots_xl_glitched(self.multiworld.state, player, False, access_cache))
+
+
+ self.collect_by_name("Bel")
+ self.multiworld.state.prog_items[player]["Chapter Completed"] = 1
+ self.multiworld.state.prog_items[player]["rep"] = 180
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, True)
+
+ # brink terminal
+ self.assertEqual(88, spots_s_glitched(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(120, spots_m_glitched(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(106, spots_l_glitched(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(58, spots_xl_glitched(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["Chapter Completed"] = 2
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, True)
+
+ # chapter 3
+ self.assertEqual(94, spots_s_glitched(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(123, spots_m_glitched(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(110, spots_l_glitched(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(61, spots_xl_glitched(self.multiworld.state, player, False, access_cache))
+
+
+ self.multiworld.state.prog_items[player]["Chapter Completed"] = 3
+ access_cache = build_access_cache(self.multiworld.state, player, 2, False, True)
+
+ # chapter 4
+ self.assertEqual(111, spots_l_glitched(self.multiworld.state, player, False, access_cache))
+ self.assertEqual(62, spots_xl_glitched(self.multiworld.state, player, False, access_cache))
\ No newline at end of file
diff --git a/worlds/bomb_rush_cyberfunk/test/test_options.py b/worlds/bomb_rush_cyberfunk/test/test_options.py
new file mode 100644
index 000000000000..7640700dc06f
--- /dev/null
+++ b/worlds/bomb_rush_cyberfunk/test/test_options.py
@@ -0,0 +1,29 @@
+from . import BombRushCyberfunkTestBase
+
+
+class TestRegularGraffitiGlitchless(BombRushCyberfunkTestBase):
+ options = {
+ "logic": "glitchless",
+ "limited_graffiti": False
+ }
+
+
+class TestLimitedGraffitiGlitchless(BombRushCyberfunkTestBase):
+ options = {
+ "logic": "glitchless",
+ "limited_graffiti": True
+ }
+
+
+class TestRegularGraffitiGlitched(BombRushCyberfunkTestBase):
+ options = {
+ "logic": "glitched",
+ "limited_graffiti": False
+ }
+
+
+class TestLimitedGraffitiGlitched(BombRushCyberfunkTestBase):
+ options = {
+ "logic": "glitched",
+ "limited_graffiti": True
+ }
\ No newline at end of file
diff --git a/worlds/bomb_rush_cyberfunk/test/test_rep_items.py b/worlds/bomb_rush_cyberfunk/test/test_rep_items.py
new file mode 100644
index 000000000000..61272a3f0977
--- /dev/null
+++ b/worlds/bomb_rush_cyberfunk/test/test_rep_items.py
@@ -0,0 +1,45 @@
+from . import BombRushCyberfunkTestBase
+from typing import List
+
+
+rep_item_names: List[str] = [
+ "8 REP",
+ "16 REP",
+ "24 REP",
+ "32 REP",
+ "48 REP"
+]
+
+
+class TestCollectAndRemoveREP(BombRushCyberfunkTestBase):
+ @property
+ def run_default_tests(self) -> bool:
+ return False
+
+ def test_default_rep_total(self) -> None:
+ self.collect_by_name(rep_item_names)
+ self.assertEqual(1400, self.multiworld.state.prog_items[self.player]["rep"])
+
+ new_total = 1400
+
+ if self.count("8 REP") > 0:
+ new_total -= 8
+ self.remove(self.get_item_by_name("8 REP"))
+
+ if self.count("16 REP") > 0:
+ new_total -= 16
+ self.remove(self.get_item_by_name("16 REP"))
+
+ if self.count("24 REP") > 0:
+ new_total -= 24
+ self.remove(self.get_item_by_name("24 REP"))
+
+ if self.count("32 REP") > 0:
+ new_total -= 32
+ self.remove(self.get_item_by_name("32 REP"))
+
+ if self.count("48 REP") > 0:
+ new_total -= 48
+ self.remove(self.get_item_by_name("48 REP"))
+
+ self.assertEqual(new_total, self.multiworld.state.prog_items[self.player]["rep"])
\ No newline at end of file
diff --git a/worlds/bumpstik/Options.py b/worlds/bumpstik/Options.py
index 021f10af2016..a781178ad161 100644
--- a/worlds/bumpstik/Options.py
+++ b/worlds/bumpstik/Options.py
@@ -3,8 +3,10 @@
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
+from dataclasses import dataclass
+
import typing
-from Options import Option, Range
+from Options import Option, Range, PerGameCommonOptions
class TaskAdvances(Range):
@@ -69,12 +71,12 @@ class KillerTrapWeight(Range):
default = 0
-bumpstik_options: typing.Dict[str, type(Option)] = {
- "task_advances": TaskAdvances,
- "turners": Turners,
- "paint_cans": PaintCans,
- "trap_count": Traps,
- "rainbow_trap_weight": RainbowTrapWeight,
- "spinner_trap_weight": SpinnerTrapWeight,
- "killer_trap_weight": KillerTrapWeight
-}
+@dataclass
+class BumpstikOptions(PerGameCommonOptions):
+ task_advances: TaskAdvances
+ turners: Turners
+ paint_cans: PaintCans
+ trap_count: Traps
+ rainbow_trap_weight: RainbowTrapWeight
+ spinner_trap_weight: SpinnerTrapWeight
+ killer_trap_weight: KillerTrapWeight
diff --git a/worlds/bumpstik/Regions.py b/worlds/bumpstik/Regions.py
index 247d6d61a34b..401b62b2d34b 100644
--- a/worlds/bumpstik/Regions.py
+++ b/worlds/bumpstik/Regions.py
@@ -11,7 +11,7 @@ def _generate_entrances(player: int, entrance_list: [str], parent: Region):
return [Entrance(player, entrance, parent) for entrance in entrance_list]
-def create_regions(world: MultiWorld, player: int):
+def create_regions(multiworld: MultiWorld, player: int):
region_map = {
"Menu": level1_locs + ["Bonus Booster 1"] + [f"Treasure Bumper {i + 1}" for i in range(8)],
"Level 1": level2_locs + ["Bonus Booster 2"] + [f"Treasure Bumper {i + 9}" for i in range(8)],
@@ -23,18 +23,18 @@ def create_regions(world: MultiWorld, player: int):
entrance_map = {
"Level 1": lambda state:
- state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 9),
+ state.has("Booster Bumper", player, 1) and state.has("Treasure Bumper", player, 8),
"Level 2": lambda state:
- state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 17),
+ state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 16),
"Level 3": lambda state:
- state.has("Booster Bumper", player, 4) and state.has("Treasure Bumper", player, 25),
+ state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 24),
"Level 4": lambda state:
- state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 33)
+ state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 32)
}
for x, region_name in enumerate(region_map):
region_list = region_map[region_name]
- region = Region(region_name, player, world)
+ region = Region(region_name, player, multiworld)
for location_name in region_list:
region.locations += [BumpStikLocation(
player, location_name, location_table[location_name], region)]
@@ -42,9 +42,9 @@ def create_regions(world: MultiWorld, player: int):
region.exits += _generate_entrances(player,
[f"To Level {x + 1}"], region)
- world.regions += [region]
+ multiworld.regions += [region]
for entrance in entrance_map:
- connection = world.get_entrance(f"To {entrance}", player)
+ connection = multiworld.get_entrance(f"To {entrance}", player)
connection.access_rule = entrance_map[entrance]
- connection.connect(world.get_region(entrance, player))
+ connection.connect(multiworld.get_region(entrance, player))
diff --git a/worlds/bumpstik/__init__.py b/worlds/bumpstik/__init__.py
index 9eeb3325e38f..fe261dc94d30 100644
--- a/worlds/bumpstik/__init__.py
+++ b/worlds/bumpstik/__init__.py
@@ -14,7 +14,7 @@
class BumpStikWeb(WebWorld):
tutorials = [Tutorial(
- "Bumper Stickers Setup Tutorial",
+ "Bumper Stickers Setup Guide",
"A guide to setting up the Archipelago Bumper Stickers software on your computer.",
"English",
"setup_en.md",
@@ -39,14 +39,13 @@ class BumpStikWorld(World):
location_name_to_id = location_table
item_name_groups = item_groups
- data_version = 1
-
required_client_version = (0, 3, 8)
- option_definitions = bumpstik_options
+ options: BumpstikOptions
+ options_dataclass = BumpstikOptions
- def __init__(self, world: MultiWorld, player: int):
- super(BumpStikWorld, self).__init__(world, player)
+ def __init__(self, multiworld: MultiWorld, player: int):
+ super(BumpStikWorld, self).__init__(multiworld, player)
self.task_advances = TaskAdvances.default
self.turners = Turners.default
self.paint_cans = PaintCans.default
@@ -86,13 +85,13 @@ def get_filler_item_name(self) -> str:
return "Nothing"
def generate_early(self):
- self.task_advances = self.multiworld.task_advances[self.player].value
- self.turners = self.multiworld.turners[self.player].value
- self.paint_cans = self.multiworld.paint_cans[self.player].value
- self.traps = self.multiworld.trap_count[self.player].value
- self.rainbow_trap_weight = self.multiworld.rainbow_trap_weight[self.player].value
- self.spinner_trap_weight = self.multiworld.spinner_trap_weight[self.player].value
- self.killer_trap_weight = self.multiworld.killer_trap_weight[self.player].value
+ self.task_advances = self.options.task_advances.value
+ self.turners = self.options.turners.value
+ self.paint_cans = self.options.paint_cans.value
+ self.traps = self.options.trap_count.value
+ self.rainbow_trap_weight = self.options.rainbow_trap_weight.value
+ self.spinner_trap_weight = self.options.spinner_trap_weight.value
+ self.killer_trap_weight = self.options.killer_trap_weight.value
def create_regions(self):
create_regions(self.multiworld, self.player)
@@ -108,7 +107,7 @@ def create_items(self):
item_pool += self._create_item_in_quantities(
name, frequencies[i])
- item_delta = len(location_table) - len(item_pool) - 1
+ item_delta = len(location_table) - len(item_pool)
if item_delta > 0:
item_pool += self._create_item_in_quantities(
"Score Bonus", item_delta)
@@ -116,13 +115,16 @@ def create_items(self):
self.multiworld.itempool += item_pool
def set_rules(self):
- forbid_item(self.multiworld.get_location("Bonus Booster 5", self.player),
- "Booster Bumper", self.player)
-
- def generate_basic(self):
- self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).place_locked_item(
- self.create_item(self.get_filler_item_name()))
-
+ for treasure_count in range(1, 33):
+ self.multiworld.get_location(f"Treasure Bumper {treasure_count}", self.player).access_rule = \
+ lambda state, treasure_held = treasure_count: state.has("Treasure Bumper", self.player, treasure_held)
+ for booster_count in range(1, 6):
+ self.multiworld.get_location(f"Bonus Booster {booster_count}", self.player).access_rule = \
+ lambda state, booster_held = booster_count: state.has("Booster Bumper", self.player, booster_held)
+ self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).access_rule = \
+ lambda state: state.has("Hazard Bumper", self.player, 25)
+
self.multiworld.completion_condition[self.player] = \
lambda state: state.has("Booster Bumper", self.player, 5) and \
state.has("Treasure Bumper", self.player, 32)
+
diff --git a/worlds/bumpstik/docs/en_Bumper Stickers.md b/worlds/bumpstik/docs/en_Bumper Stickers.md
index 17a66d76122a..42599ec64121 100644
--- a/worlds/bumpstik/docs/en_Bumper Stickers.md
+++ b/worlds/bumpstik/docs/en_Bumper Stickers.md
@@ -1,7 +1,7 @@
# Bumper Stickers
-## Where is the settings page?
-The [player settings page for Bumper Stickers](../player-settings) contains all the options you need to configure and export a config file.
+## Where is the options page?
+The [player options page for Bumper Stickers](../player-options) contains all the options you need to configure and export a config file.
## What does randomization do to this game?
Playing this in Archipelago is a very different experience from Classic mode. You start with a very small board and a set of tasks. Completing those tasks will give you a larger board and more, harder tasks. In addition, special types of bumpers exist that must be cleared in order to progress.
diff --git a/worlds/bumpstik/docs/setup_en.md b/worlds/bumpstik/docs/setup_en.md
index 51334aa27701..e64a6e9f297d 100644
--- a/worlds/bumpstik/docs/setup_en.md
+++ b/worlds/bumpstik/docs/setup_en.md
@@ -1,21 +1,19 @@
## Required Software
-Download the game from the [Bumper Stickers GitHub releases page](https://github.com/FelicitusNeko/FlixelBumpStik/releases).
-
-*A web version will be made available on itch.io at a later time.*
+Download the game from the [Bumper Stickers GitHub releases page](https://github.com/FelicitusNeko/FlixelBumpStik/releases), or from the [Bumper Stickers AP Itch page](https://kewliomzx.itch.io/bumpstik-ap), where you can also play it in your browser.
## Installation Procedures
Simply download the latest version of Bumper Stickers from the link above, and extract it wherever you like.
-- âš ï¸ Do not extract Bumper Stickers to Program Files, as this will cause file access issues.
+- âš ï¸ It is not recommended to copy this game, or any files, directly into your Program Files folder under Windows.
## Joining a Multiworld Game
-1. Run `BumpStik-AP.exe`.
+1. Run `BumpStikAP.exe`.
2. Select "Archipelago Mode".
3. Enter your server details in the fields provided, and click "Start".
- - ※ If you are connecting to a WSS server (such as archipelago.gg), specify `wss://` in the host name. Otherwise, the game will assume `ws://`.
+ - The game will attempt to automatically detect whether to connect via normal (WS) or secure (WSS) server, but you can specify `ws://` or `wss://` to prioritise one or the other.
## How to play Bumper Stickers (Classic)
diff --git a/worlds/bumpstik/test/TestLogic.py b/worlds/bumpstik/test/TestLogic.py
new file mode 100644
index 000000000000..a252f1e58461
--- /dev/null
+++ b/worlds/bumpstik/test/TestLogic.py
@@ -0,0 +1,41 @@
+from . import BumpStikTestBase
+
+
+class TestRuleLogic(BumpStikTestBase):
+ def testLogic(self):
+ for treasure_bumpers_held in range(1, 33):
+ if treasure_bumpers_held == 32:
+ self.assertFalse(self.can_reach_location("Level 5 - Cleared all Hazards"))
+
+ self.collect(self.get_item_by_name("Treasure Bumper"))
+ if treasure_bumpers_held % 8 == 0:
+ bb_count = round(treasure_bumpers_held / 8)
+
+ if bb_count < 4:
+ self.assertFalse(self.can_reach_location(f"Treasure Bumper {treasure_bumpers_held + 1}"))
+ # Can't reach Treasure Bumper 9 check until level 2 is unlocked, etc.
+ # But we don't have enough Treasure Bumpers to reach this check anyway??
+ elif bb_count == 4:
+ bb_count += 1
+ # Level 4 has two new Bonus Booster checks; need to check both
+
+ for booster_bumpers_held in range(self.count("Booster Bumper"), bb_count + 1):
+ if booster_bumpers_held > 0:
+ self.assertTrue(self.can_reach_location(f"Bonus Booster {booster_bumpers_held}"),
+ f"Bonus Booster {booster_bumpers_held} check not reachable with {self.count('Booster Bumper')} Booster Bumpers")
+ if booster_bumpers_held < 5:
+ self.assertFalse(self.can_reach_location(f"Bonus Booster {booster_bumpers_held + 1}"),
+ f"Bonus Booster {booster_bumpers_held + 1} check reachable with {self.count('Treasure Bumper')} Treasure Bumpers and {self.count('Booster Bumper')} Booster Bumpers")
+ if booster_bumpers_held < bb_count:
+ self.collect(self.get_item_by_name("Booster Bumper"))
+
+ self.assertTrue(self.can_reach_location(f"Treasure Bumper {treasure_bumpers_held}"),
+ f"Treasure Bumper {treasure_bumpers_held} check not reachable with {self.count('Treasure Bumper')} Treasure Bumpers")
+
+ if treasure_bumpers_held < 32:
+ self.assertFalse(self.can_reach_location(f"Treasure Bumper {treasure_bumpers_held + 1}"))
+ elif treasure_bumpers_held == 32:
+ self.assertTrue(self.can_reach_location("Level 5 - 50,000+ Total Points"))
+ self.assertFalse(self.can_reach_location("Level 5 - Cleared all Hazards"))
+ self.collect(self.get_items_by_name("Hazard Bumper"))
+ self.assertTrue(self.can_reach_location("Level 5 - Cleared all Hazards"))
diff --git a/worlds/bumpstik/test/__init__.py b/worlds/bumpstik/test/__init__.py
new file mode 100644
index 000000000000..1199d7b8e506
--- /dev/null
+++ b/worlds/bumpstik/test/__init__.py
@@ -0,0 +1,5 @@
+from test.TestBase import WorldTestBase
+
+
+class BumpStikTestBase(WorldTestBase):
+ game = "Bumper Stickers"
diff --git a/worlds/celeste64/CHANGELOG.md b/worlds/celeste64/CHANGELOG.md
new file mode 100644
index 000000000000..5e562e17f443
--- /dev/null
+++ b/worlds/celeste64/CHANGELOG.md
@@ -0,0 +1,53 @@
+# Celeste 64 - Changelog
+
+
+## v1.2
+
+### Features:
+
+- New optional Location Checks
+ - Friendsanity
+ - Signsanity
+ - Carsanity
+- Move Shuffle
+ - Basic movement abilities can be shuffled into the item pool
+ - Ground Dash
+ - Air Dash
+ - Skid Jump
+ - Climb
+- Logic Difficulty
+ - Completely overhauled logic system
+ - Standard or Hard logic difficulty can be chosen
+- Badeline Chasers
+ - Opt-in options which cause Badelines to start following you as you play, which will kill on contact
+ - These can be set to spawn based on either:
+ - The number of locations you've checked
+ - The number of Strawberry items you've received
+ - How fast they follow behind you can be specified
+
+### Quality of Life:
+
+- The maximum number of Strawberries in the item pool can be directly set
+ - The required amount of Strawberries is now set via percentage
+ - All items beyond the amount placed in the item pool will be `Raspberry` items, which have no effect
+- Any unique items placed into the `start_inventory` will not be placed into the item pool
+
+
+## v1.1 - First Stable Release
+
+### Features:
+
+- Goal is to collect a certain amount of Strawberries and visit Badeline on her island
+- Locations included:
+ - Strawberries
+- Items included:
+ - Strawberries
+ - Dash Refills
+ - Double Dash Refills
+ - Feathers
+ - Coins
+ - Cassettes
+ - Traffic Blocks
+ - Springs
+ - Breakable Blocks
+- DeathLink is supported
\ No newline at end of file
diff --git a/worlds/celeste64/Items.py b/worlds/celeste64/Items.py
new file mode 100644
index 000000000000..36c9f670c798
--- /dev/null
+++ b/worlds/celeste64/Items.py
@@ -0,0 +1,44 @@
+from typing import Dict, NamedTuple, Optional
+
+from BaseClasses import Item, ItemClassification
+from .Names import ItemName
+
+
+celeste_64_base_id: int = 0xCA0000
+
+
+class Celeste64Item(Item):
+ game = "Celeste 64"
+
+
+class Celeste64ItemData(NamedTuple):
+ code: Optional[int] = None
+ type: ItemClassification = ItemClassification.filler
+
+
+collectable_item_data_table: Dict[str, Celeste64ItemData] = {
+ ItemName.strawberry: Celeste64ItemData(celeste_64_base_id + 0x0, ItemClassification.progression_skip_balancing),
+ ItemName.raspberry: Celeste64ItemData(celeste_64_base_id + 0x9, ItemClassification.filler),
+}
+
+unlockable_item_data_table: Dict[str, Celeste64ItemData] = {
+ ItemName.dash_refill: Celeste64ItemData(celeste_64_base_id + 0x1, ItemClassification.progression),
+ ItemName.double_dash_refill: Celeste64ItemData(celeste_64_base_id + 0x2, ItemClassification.progression),
+ ItemName.feather: Celeste64ItemData(celeste_64_base_id + 0x3, ItemClassification.progression),
+ ItemName.coin: Celeste64ItemData(celeste_64_base_id + 0x4, ItemClassification.progression),
+ ItemName.cassette: Celeste64ItemData(celeste_64_base_id + 0x5, ItemClassification.progression),
+ ItemName.traffic_block: Celeste64ItemData(celeste_64_base_id + 0x6, ItemClassification.progression),
+ ItemName.spring: Celeste64ItemData(celeste_64_base_id + 0x7, ItemClassification.progression),
+ ItemName.breakables: Celeste64ItemData(celeste_64_base_id + 0x8, ItemClassification.progression),
+}
+
+move_item_data_table: Dict[str, Celeste64ItemData] = {
+ ItemName.ground_dash: Celeste64ItemData(celeste_64_base_id + 0xA, ItemClassification.progression),
+ ItemName.air_dash: Celeste64ItemData(celeste_64_base_id + 0xB, ItemClassification.progression),
+ ItemName.skid_jump: Celeste64ItemData(celeste_64_base_id + 0xC, ItemClassification.progression),
+ ItemName.climb: Celeste64ItemData(celeste_64_base_id + 0xD, ItemClassification.progression),
+}
+
+item_data_table: Dict[str, Celeste64ItemData] = {**collectable_item_data_table, **unlockable_item_data_table, **move_item_data_table}
+
+item_table = {name: data.code for name, data in item_data_table.items() if data.code is not None}
diff --git a/worlds/celeste64/Locations.py b/worlds/celeste64/Locations.py
new file mode 100644
index 000000000000..6341529da356
--- /dev/null
+++ b/worlds/celeste64/Locations.py
@@ -0,0 +1,82 @@
+from typing import Dict, NamedTuple, Optional
+
+from BaseClasses import Location
+from .Names import LocationName
+
+
+celeste_64_base_id: int = 0xCA0000
+
+
+class Celeste64Location(Location):
+ game = "Celeste 64"
+
+
+class Celeste64LocationData(NamedTuple):
+ region: str
+ address: Optional[int] = None
+
+
+strawberry_location_data_table: Dict[str, Celeste64LocationData] = {
+ LocationName.strawberry_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x00),
+ LocationName.strawberry_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x01),
+ LocationName.strawberry_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x02),
+ LocationName.strawberry_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x03),
+ LocationName.strawberry_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x04),
+ LocationName.strawberry_6: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x05),
+ LocationName.strawberry_7: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x06),
+ LocationName.strawberry_8: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x07),
+ LocationName.strawberry_9: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x08),
+ LocationName.strawberry_10: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x09),
+ LocationName.strawberry_11: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0A),
+ LocationName.strawberry_12: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0B),
+ LocationName.strawberry_13: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0C),
+ LocationName.strawberry_14: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0D),
+ LocationName.strawberry_15: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0E),
+ LocationName.strawberry_16: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0F),
+ LocationName.strawberry_17: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x10),
+ LocationName.strawberry_18: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x11),
+ LocationName.strawberry_19: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x12),
+ LocationName.strawberry_20: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x13),
+ LocationName.strawberry_21: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x14),
+ LocationName.strawberry_22: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x15),
+ LocationName.strawberry_23: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x16),
+ LocationName.strawberry_24: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x17),
+ LocationName.strawberry_25: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x18),
+ LocationName.strawberry_26: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x19),
+ LocationName.strawberry_27: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1A),
+ LocationName.strawberry_28: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1B),
+ LocationName.strawberry_29: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1C),
+ LocationName.strawberry_30: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1D),
+}
+
+friend_location_data_table: Dict[str, Celeste64LocationData] = {
+ LocationName.granny_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x00),
+ LocationName.granny_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x01),
+ LocationName.granny_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x02),
+ LocationName.theo_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x03),
+ LocationName.theo_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x04),
+ LocationName.theo_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x05),
+ LocationName.badeline_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x06),
+ LocationName.badeline_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x07),
+ LocationName.badeline_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x08),
+}
+
+sign_location_data_table: Dict[str, Celeste64LocationData] = {
+ LocationName.sign_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x00),
+ LocationName.sign_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x01),
+ LocationName.sign_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x02),
+ LocationName.sign_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x03),
+ LocationName.sign_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x04),
+}
+
+car_location_data_table: Dict[str, Celeste64LocationData] = {
+ LocationName.car_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x00),
+ LocationName.car_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x01),
+}
+
+location_data_table: Dict[str, Celeste64LocationData] = {**strawberry_location_data_table,
+ **friend_location_data_table,
+ **sign_location_data_table,
+ **car_location_data_table}
+
+location_table = {name: data.address for name, data in location_data_table.items() if data.address is not None}
diff --git a/worlds/celeste64/Names/ItemName.py b/worlds/celeste64/Names/ItemName.py
new file mode 100644
index 000000000000..d4fb9496007f
--- /dev/null
+++ b/worlds/celeste64/Names/ItemName.py
@@ -0,0 +1,17 @@
+strawberry = "Strawberry"
+raspberry = "Raspberry"
+
+dash_refill = "Dash Refills"
+double_dash_refill = "Double Dash Refills"
+feather = "Feathers"
+coin = "Coins"
+cassette = "Cassettes"
+
+traffic_block = "Traffic Blocks"
+spring = "Springs"
+breakables = "Breakable Blocks"
+
+ground_dash = "Ground Dash"
+air_dash = "Air Dash"
+skid_jump = "Skid Jump"
+climb = "Climb"
diff --git a/worlds/celeste64/Names/LocationName.py b/worlds/celeste64/Names/LocationName.py
new file mode 100644
index 000000000000..1b784f3875f1
--- /dev/null
+++ b/worlds/celeste64/Names/LocationName.py
@@ -0,0 +1,53 @@
+# Strawberry Locations
+strawberry_1 = "First Strawberry"
+strawberry_2 = "Floating Blocks Strawberry"
+strawberry_3 = "South-East Tower Top Strawberry"
+strawberry_4 = "Theo Strawberry"
+strawberry_5 = "Fall Through Spike Floor Strawberry"
+strawberry_6 = "Troll Strawberry"
+strawberry_7 = "Falling Blocks Strawberry"
+strawberry_8 = "Traffic Block Strawberry"
+strawberry_9 = "South-West Dash Refills Strawberry"
+strawberry_10 = "South-East Tower Side Strawberry"
+strawberry_11 = "Girders Strawberry"
+strawberry_12 = "North-East Tower Bottom Strawberry"
+strawberry_13 = "Breakable Blocks Strawberry"
+strawberry_14 = "Feather Maze Strawberry"
+strawberry_15 = "Feather Chain Strawberry"
+strawberry_16 = "Feather Hidden Strawberry"
+strawberry_17 = "Double Dash Puzzle Strawberry"
+strawberry_18 = "Double Dash Spike Climb Strawberry"
+strawberry_19 = "Double Dash Spring Strawberry"
+strawberry_20 = "North-East Tower Breakable Bottom Strawberry"
+strawberry_21 = "Theo Tower Lower Cassette Strawberry"
+strawberry_22 = "Theo Tower Upper Cassette Strawberry"
+strawberry_23 = "South End of Bridge Cassette Strawberry"
+strawberry_24 = "You Are Ready Cassette Strawberry"
+strawberry_25 = "Cassette Hidden in the House Strawberry"
+strawberry_26 = "North End of Bridge Cassette Strawberry"
+strawberry_27 = "Distant Feather Cassette Strawberry"
+strawberry_28 = "Feather Arches Cassette Strawberry"
+strawberry_29 = "North-East Tower Cassette Strawberry"
+strawberry_30 = "Badeline Cassette Strawberry"
+
+# Friend Locations
+granny_1 = "Granny Conversation 1"
+granny_2 = "Granny Conversation 2"
+granny_3 = "Granny Conversation 3"
+theo_1 = "Theo Conversation 1"
+theo_2 = "Theo Conversation 2"
+theo_3 = "Theo Conversation 3"
+badeline_1 = "Badeline Conversation 1"
+badeline_2 = "Badeline Conversation 2"
+badeline_3 = "Badeline Conversation 3"
+
+# Sign Locations
+sign_1 = "Camera Sign"
+sign_2 = "Skid Jump Sign"
+sign_3 = "Dash Jump Sign"
+sign_4 = "Lonely Sign"
+sign_5 = "Credits Sign"
+
+# Car Locations
+car_1 = "Intro Car"
+car_2 = "Secret Car"
diff --git a/worlds/celeste64/Names/__init__.py b/worlds/celeste64/Names/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/celeste64/Options.py b/worlds/celeste64/Options.py
new file mode 100644
index 000000000000..9a67e7d7d4f5
--- /dev/null
+++ b/worlds/celeste64/Options.py
@@ -0,0 +1,148 @@
+from dataclasses import dataclass
+
+from Options import Choice, Range, Toggle, DeathLink, OptionGroup, PerGameCommonOptions
+
+
+class DeathLinkAmnesty(Range):
+ """
+ How many deaths it takes to send a DeathLink
+ """
+ display_name = "Death Link Amnesty"
+ range_start = 1
+ range_end = 30
+ default = 10
+
+class TotalStrawberries(Range):
+ """
+ How many Strawberries exist
+ """
+ display_name = "Total Strawberries"
+ range_start = 0
+ range_end = 46
+ default = 20
+
+class StrawberriesRequiredPercentage(Range):
+ """
+ Percentage of existing Strawberries you must receive to finish
+ """
+ display_name = "Strawberries Required Percentage"
+ range_start = 0
+ range_end = 100
+ default = 80
+
+
+class LogicDifficulty(Choice):
+ """
+ Whether the logic expects you to play the intended way, or to be able to use advanced tricks and skips
+ """
+ display_name = "Logic Difficulty"
+ option_standard = 0
+ option_hard = 1
+ default = 0
+
+class MoveShuffle(Toggle):
+ """
+ Whether the following base movement abilities are shuffled into the item pool:
+ - Ground Dash
+ - Air Dash
+ - Skid Jump
+ - Climb
+
+ NOTE: Having Move Shuffle and Standard Logic Difficulty will guarantee that one of the four Move items will be immediately accessible
+
+ WARNING: Combining Move Shuffle and Hard Logic Difficulty can require very difficult tricks
+ """
+ display_name = "Move Shuffle"
+
+
+class Friendsanity(Toggle):
+ """
+ Whether chatting with your friends grants location checks
+ """
+ display_name = "Friendsanity"
+
+class Signsanity(Toggle):
+ """
+ Whether reading signs grants location checks
+ """
+ display_name = "Signsanity"
+
+class Carsanity(Toggle):
+ """
+ Whether riding on cars grants location checks
+ """
+ display_name = "Carsanity"
+
+
+class BadelineChaserSource(Choice):
+ """
+ What type of action causes more Badeline Chasers to start spawning
+
+ Locations: The number of locations you've checked contributes to Badeline Chasers
+
+ Strawberries: The number of Strawberry items you've received contributes to Badeline Chasers
+ """
+ display_name = "Badeline Chaser Source"
+ option_locations = 0
+ option_strawberries = 1
+ default = 0
+
+class BadelineChaserFrequency(Range):
+ """
+ How many of the `Badeline Chaser Source` actions must occur to make each Badeline Chaser start spawning
+
+ NOTE: Choosing `0` disables Badeline Chasers entirely
+
+ WARNING: Turning on Badeline Chasers alongside Move Shuffle could result in extremely difficult situations
+ """
+ display_name = "Badeline Chaser Frequency"
+ range_start = 0
+ range_end = 10
+ default = 0
+
+class BadelineChaserSpeed(Range):
+ """
+ How many seconds behind you each Badeline Chaser will be
+ """
+ display_name = "Badeline Chaser Speed"
+ range_start = 2
+ range_end = 10
+ default = 3
+
+
+celeste_64_option_groups = [
+ OptionGroup("Goal Options", [
+ TotalStrawberries,
+ StrawberriesRequiredPercentage,
+ ]),
+ OptionGroup("Sanity Options", [
+ Friendsanity,
+ Signsanity,
+ Carsanity,
+ ]),
+ OptionGroup("Badeline Chasers", [
+ BadelineChaserSource,
+ BadelineChaserFrequency,
+ BadelineChaserSpeed,
+ ]),
+]
+
+
+@dataclass
+class Celeste64Options(PerGameCommonOptions):
+ death_link: DeathLink
+ death_link_amnesty: DeathLinkAmnesty
+
+ total_strawberries: TotalStrawberries
+ strawberries_required_percentage: StrawberriesRequiredPercentage
+
+ logic_difficulty: LogicDifficulty
+ move_shuffle: MoveShuffle
+
+ friendsanity: Friendsanity
+ signsanity: Signsanity
+ carsanity: Carsanity
+
+ badeline_chaser_source: BadelineChaserSource
+ badeline_chaser_frequency: BadelineChaserFrequency
+ badeline_chaser_speed: BadelineChaserSpeed
diff --git a/worlds/celeste64/Regions.py b/worlds/celeste64/Regions.py
new file mode 100644
index 000000000000..6f01c873a4f9
--- /dev/null
+++ b/worlds/celeste64/Regions.py
@@ -0,0 +1,11 @@
+from typing import Dict, List, NamedTuple
+
+
+class Celeste64RegionData(NamedTuple):
+ connecting_regions: List[str] = []
+
+
+region_data_table: Dict[str, Celeste64RegionData] = {
+ "Menu": Celeste64RegionData(["Forsaken City"]),
+ "Forsaken City": Celeste64RegionData(),
+}
diff --git a/worlds/celeste64/Rules.py b/worlds/celeste64/Rules.py
new file mode 100644
index 000000000000..ebb47cca3093
--- /dev/null
+++ b/worlds/celeste64/Rules.py
@@ -0,0 +1,343 @@
+from typing import Dict, List
+
+from BaseClasses import CollectionState
+from worlds.generic.Rules import set_rule
+
+from . import Celeste64World
+from .Names import ItemName, LocationName
+
+
+def set_rules(world: Celeste64World):
+ if world.options.logic_difficulty == "standard":
+ if world.options.move_shuffle:
+ world.active_logic_mapping = location_standard_moves_logic
+ else:
+ world.active_logic_mapping = location_standard_logic
+ else:
+ if world.options.move_shuffle:
+ world.active_logic_mapping = location_hard_moves_logic
+ else:
+ world.active_logic_mapping = location_hard_logic
+
+ for location in world.multiworld.get_locations(world.player):
+ set_rule(location, lambda state, location=location: location_rule(state, world, location.name))
+
+ if world.options.logic_difficulty == "standard":
+ if world.options.move_shuffle:
+ world.goal_logic_mapping = goal_standard_moves_logic
+ else:
+ world.goal_logic_mapping = goal_standard_logic
+ else:
+ if world.options.move_shuffle:
+ world.goal_logic_mapping = goal_hard_moves_logic
+ else:
+ world.goal_logic_mapping = goal_hard_logic
+
+ # Completion condition.
+ world.multiworld.completion_condition[world.player] = lambda state: goal_rule(state, world)
+
+
+goal_standard_logic: List[List[str]] = [[ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.double_dash_refill]]
+goal_hard_logic: List[List[str]] = [[]]
+goal_standard_moves_logic: List[List[str]] = [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]]
+goal_hard_moves_logic: List[List[str]] = [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
+ [ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
+ [ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
+ [ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]]
+
+
+location_standard_logic: Dict[str, List[List[str]]] = {
+ LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables]],
+ LocationName.strawberry_6: [[ItemName.dash_refill],
+ [ItemName.traffic_block]],
+ LocationName.strawberry_7: [[ItemName.dash_refill],
+ [ItemName.traffic_block]],
+ LocationName.strawberry_8: [[ItemName.traffic_block]],
+ LocationName.strawberry_9: [[ItemName.dash_refill]],
+ LocationName.strawberry_11: [[ItemName.dash_refill],
+ [ItemName.traffic_block]],
+ LocationName.strawberry_12: [[ItemName.dash_refill, ItemName.double_dash_refill],
+ [ItemName.traffic_block, ItemName.double_dash_refill]],
+ LocationName.strawberry_13: [[ItemName.dash_refill, ItemName.breakables],
+ [ItemName.traffic_block, ItemName.breakables]],
+ LocationName.strawberry_14: [[ItemName.dash_refill, ItemName.feather],
+ [ItemName.traffic_block, ItemName.feather]],
+ LocationName.strawberry_15: [[ItemName.dash_refill, ItemName.feather],
+ [ItemName.traffic_block, ItemName.feather]],
+ LocationName.strawberry_16: [[ItemName.dash_refill, ItemName.feather],
+ [ItemName.traffic_block, ItemName.feather]],
+ LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block]],
+ LocationName.strawberry_18: [[ItemName.dash_refill, ItemName.double_dash_refill],
+ [ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill]],
+ LocationName.strawberry_19: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.spring],
+ [ItemName.traffic_block, ItemName.double_dash_refill, ItemName.feather, ItemName.spring]],
+ LocationName.strawberry_20: [[ItemName.dash_refill, ItemName.feather, ItemName.breakables],
+ [ItemName.traffic_block, ItemName.feather, ItemName.breakables]],
+
+ LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables]],
+ LocationName.strawberry_22: [[ItemName.cassette, ItemName.dash_refill, ItemName.breakables]],
+ LocationName.strawberry_23: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin],
+ [ItemName.cassette, ItemName.traffic_block, ItemName.coin]],
+ LocationName.strawberry_24: [[ItemName.cassette, ItemName.dash_refill, ItemName.traffic_block]],
+ LocationName.strawberry_25: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill],
+ [ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill]],
+ LocationName.strawberry_26: [[ItemName.cassette, ItemName.dash_refill],
+ [ItemName.cassette, ItemName.traffic_block]],
+ LocationName.strawberry_27: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin],
+ [ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin]],
+ LocationName.strawberry_28: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin],
+ [ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin]],
+ LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin]],
+ LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.spring, ItemName.breakables]],
+
+ LocationName.theo_1: [[ItemName.traffic_block, ItemName.breakables]],
+ LocationName.theo_2: [[ItemName.traffic_block, ItemName.breakables]],
+ LocationName.theo_3: [[ItemName.traffic_block, ItemName.breakables]],
+ LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
+ LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
+ LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
+
+ LocationName.sign_2: [[ItemName.breakables]],
+ LocationName.sign_3: [[ItemName.dash_refill],
+ [ItemName.traffic_block]],
+ LocationName.sign_4: [[ItemName.dash_refill, ItemName.double_dash_refill],
+ [ItemName.dash_refill, ItemName.feather],
+ [ItemName.traffic_block, ItemName.feather]],
+ LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
+
+ LocationName.car_2: [[ItemName.breakables]],
+}
+
+location_hard_logic: Dict[str, List[List[str]]] = {
+ LocationName.strawberry_13: [[ItemName.breakables]],
+ LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.traffic_block]],
+ LocationName.strawberry_20: [[ItemName.breakables]],
+
+ LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables]],
+ LocationName.strawberry_22: [[ItemName.cassette]],
+ LocationName.strawberry_23: [[ItemName.cassette, ItemName.coin]],
+ LocationName.strawberry_24: [[ItemName.cassette]],
+ LocationName.strawberry_25: [[ItemName.cassette, ItemName.double_dash_refill]],
+ LocationName.strawberry_26: [[ItemName.cassette]],
+ LocationName.strawberry_27: [[ItemName.cassette]],
+ LocationName.strawberry_28: [[ItemName.cassette, ItemName.feather]],
+ LocationName.strawberry_29: [[ItemName.cassette]],
+ LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables]],
+
+ LocationName.sign_2: [[ItemName.breakables]],
+
+ LocationName.car_2: [[ItemName.breakables]],
+}
+
+location_standard_moves_logic: Dict[str, List[List[str]]] = {
+ LocationName.strawberry_1: [[ItemName.ground_dash],
+ [ItemName.air_dash],
+ [ItemName.skid_jump],
+ [ItemName.climb]],
+ LocationName.strawberry_2: [[ItemName.ground_dash],
+ [ItemName.air_dash],
+ [ItemName.skid_jump],
+ [ItemName.climb]],
+ LocationName.strawberry_3: [[ItemName.air_dash],
+ [ItemName.skid_jump]],
+ LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
+ LocationName.strawberry_5: [[ItemName.air_dash]],
+ LocationName.strawberry_6: [[ItemName.dash_refill, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.ground_dash],
+ [ItemName.traffic_block, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.skid_jump],
+ [ItemName.traffic_block, ItemName.climb]],
+ LocationName.strawberry_7: [[ItemName.dash_refill, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.ground_dash],
+ [ItemName.traffic_block, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.skid_jump],
+ [ItemName.traffic_block, ItemName.climb]],
+ LocationName.strawberry_8: [[ItemName.traffic_block, ItemName.ground_dash],
+ [ItemName.traffic_block, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.skid_jump],
+ [ItemName.traffic_block, ItemName.climb]],
+ LocationName.strawberry_9: [[ItemName.dash_refill, ItemName.air_dash]],
+ LocationName.strawberry_10: [[ItemName.climb]],
+ LocationName.strawberry_11: [[ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
+ [ItemName.traffic_block, ItemName.climb]],
+ LocationName.strawberry_12: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.double_dash_refill, ItemName.air_dash]],
+ LocationName.strawberry_13: [[ItemName.dash_refill, ItemName.breakables, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.breakables, ItemName.ground_dash],
+ [ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
+ LocationName.strawberry_14: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.feather, ItemName.air_dash]],
+ LocationName.strawberry_15: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash, ItemName.climb],
+ [ItemName.traffic_block, ItemName.feather, ItemName.climb]],
+ LocationName.strawberry_16: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.feather]],
+ LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.ground_dash],
+ [ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
+ [ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.skid_jump],
+ [ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.climb]],
+ LocationName.strawberry_18: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb],
+ [ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
+ LocationName.strawberry_19: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.spring, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.double_dash_refill, ItemName.feather, ItemName.spring, ItemName.air_dash]],
+ LocationName.strawberry_20: [[ItemName.dash_refill, ItemName.feather, ItemName.breakables, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.feather, ItemName.breakables, ItemName.air_dash]],
+
+ LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
+ LocationName.strawberry_22: [[ItemName.cassette, ItemName.dash_refill, ItemName.breakables, ItemName.air_dash]],
+ LocationName.strawberry_23: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin, ItemName.air_dash, ItemName.climb],
+ [ItemName.cassette, ItemName.traffic_block, ItemName.coin, ItemName.air_dash, ItemName.climb]],
+ LocationName.strawberry_24: [[ItemName.cassette, ItemName.dash_refill, ItemName.traffic_block, ItemName.air_dash]],
+ LocationName.strawberry_25: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb],
+ [ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
+ LocationName.strawberry_26: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
+ [ItemName.cassette, ItemName.traffic_block, ItemName.air_dash, ItemName.climb]],
+ LocationName.strawberry_27: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash],
+ [ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin, ItemName.air_dash]],
+ LocationName.strawberry_28: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb],
+ [ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
+ LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.skid_jump]],
+ LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.spring, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
+
+ LocationName.granny_1: [[ItemName.ground_dash],
+ [ItemName.air_dash],
+ [ItemName.skid_jump],
+ [ItemName.climb]],
+ LocationName.granny_2: [[ItemName.ground_dash],
+ [ItemName.air_dash],
+ [ItemName.skid_jump],
+ [ItemName.climb]],
+ LocationName.granny_3: [[ItemName.ground_dash],
+ [ItemName.air_dash],
+ [ItemName.skid_jump],
+ [ItemName.climb]],
+ LocationName.theo_1: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
+ LocationName.theo_2: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
+ LocationName.theo_3: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
+ LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
+ LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
+ LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
+
+ LocationName.sign_1: [[ItemName.ground_dash],
+ [ItemName.air_dash],
+ [ItemName.skid_jump],
+ [ItemName.climb]],
+ LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
+ [ItemName.breakables, ItemName.air_dash]],
+ LocationName.sign_3: [[ItemName.dash_refill, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.ground_dash],
+ [ItemName.traffic_block, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.skid_jump],
+ [ItemName.traffic_block, ItemName.climb]],
+ LocationName.sign_4: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash],
+ [ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.feather, ItemName.ground_dash],
+ [ItemName.traffic_block, ItemName.feather, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.feather, ItemName.skid_jump],
+ [ItemName.traffic_block, ItemName.feather, ItemName.climb]],
+ LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
+
+ LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash],
+ [ItemName.breakables, ItemName.air_dash]],
+}
+
+location_hard_moves_logic: Dict[str, List[List[str]]] = {
+ LocationName.strawberry_3: [[ItemName.air_dash],
+ [ItemName.skid_jump]],
+ LocationName.strawberry_5: [[ItemName.ground_dash],
+ [ItemName.air_dash]],
+ LocationName.strawberry_8: [[ItemName.traffic_block],
+ [ItemName.ground_dash, ItemName.air_dash]],
+ LocationName.strawberry_10: [[ItemName.air_dash],
+ [ItemName.climb]],
+ LocationName.strawberry_11: [[ItemName.ground_dash],
+ [ItemName.air_dash],
+ [ItemName.skid_jump]],
+ LocationName.strawberry_12: [[ItemName.feather],
+ [ItemName.ground_dash],
+ [ItemName.air_dash]],
+ LocationName.strawberry_13: [[ItemName.breakables, ItemName.ground_dash],
+ [ItemName.breakables, ItemName.air_dash]],
+ LocationName.strawberry_14: [[ItemName.feather, ItemName.air_dash],
+ [ItemName.air_dash, ItemName.climb]],
+ LocationName.strawberry_15: [[ItemName.feather],
+ [ItemName.ground_dash, ItemName.air_dash]],
+ LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.traffic_block]],
+ LocationName.strawberry_18: [[ItemName.air_dash, ItemName.climb],
+ [ItemName.double_dash_refill, ItemName.air_dash]],
+ LocationName.strawberry_19: [[ItemName.air_dash, ItemName.skid_jump],
+ [ItemName.double_dash_refill, ItemName.spring, ItemName.air_dash],
+ [ItemName.spring, ItemName.ground_dash, ItemName.air_dash]],
+ LocationName.strawberry_20: [[ItemName.breakables, ItemName.air_dash]],
+
+ LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
+ LocationName.strawberry_22: [[ItemName.cassette, ItemName.ground_dash, ItemName.air_dash],
+ [ItemName.cassette, ItemName.dash_refill, ItemName.air_dash]],
+ LocationName.strawberry_23: [[ItemName.cassette, ItemName.coin, ItemName.air_dash]],
+ LocationName.strawberry_24: [[ItemName.cassette, ItemName.ground_dash, ItemName.air_dash],
+ [ItemName.cassette, ItemName.traffic_block, ItemName.air_dash]],
+ LocationName.strawberry_25: [[ItemName.cassette, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
+ LocationName.strawberry_26: [[ItemName.cassette, ItemName.ground_dash],
+ [ItemName.cassette, ItemName.air_dash]],
+ LocationName.strawberry_27: [[ItemName.cassette, ItemName.air_dash, ItemName.skid_jump],
+ [ItemName.cassette, ItemName.traffic_block, ItemName.coin, ItemName.air_dash],
+ [ItemName.cassette, ItemName.coin, ItemName.ground_dash],
+ [ItemName.cassette, ItemName.feather, ItemName.coin, ItemName.air_dash]],
+ LocationName.strawberry_28: [[ItemName.cassette, ItemName.feather, ItemName.air_dash],
+ [ItemName.cassette, ItemName.feather, ItemName.climb]],
+ LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.skid_jump],
+ [ItemName.cassette, ItemName.ground_dash, ItemName.air_dash]],
+ LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.ground_dash, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
+ [ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.feather, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
+ [ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.ground_dash, ItemName.air_dash, ItemName.climb],
+ [ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.feather, ItemName.air_dash, ItemName.climb]],
+
+ LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
+ [ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
+ [ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
+ [ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
+ LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
+ [ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
+ [ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
+ [ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
+ LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
+ [ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
+ [ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
+ [ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
+
+ LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
+ [ItemName.breakables, ItemName.air_dash]],
+ LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
+ [ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
+ [ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
+ [ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
+ [ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
+
+ LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash],
+ [ItemName.breakables, ItemName.air_dash]],
+}
+
+
+def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bool:
+
+ if loc not in world.active_logic_mapping:
+ return True
+
+ for possible_access in world.active_logic_mapping[loc]:
+ if state.has_all(possible_access, world.player):
+ return True
+
+ return False
+
+def goal_rule(state: CollectionState, world: Celeste64World) -> bool:
+ if not state.has(ItemName.strawberry, world.player, world.strawberries_required):
+ return False
+
+ for possible_access in world.goal_logic_mapping:
+ if state.has_all(possible_access, world.player):
+ return True
+
+ return False
diff --git a/worlds/celeste64/__init__.py b/worlds/celeste64/__init__.py
new file mode 100644
index 000000000000..7786e381230a
--- /dev/null
+++ b/worlds/celeste64/__init__.py
@@ -0,0 +1,167 @@
+from copy import deepcopy
+from typing import Dict, List
+
+from BaseClasses import ItemClassification, Location, Region, Tutorial
+from worlds.AutoWorld import WebWorld, World
+from .Items import Celeste64Item, unlockable_item_data_table, move_item_data_table, item_data_table, item_table
+from .Locations import Celeste64Location, strawberry_location_data_table, friend_location_data_table,\
+ sign_location_data_table, car_location_data_table, location_table
+from .Names import ItemName, LocationName
+from .Options import Celeste64Options, celeste_64_option_groups
+
+
+class Celeste64WebWorld(WebWorld):
+ theme = "ice"
+
+ setup_en = Tutorial(
+ tutorial_name="Start Guide",
+ description="A guide to playing Celeste 64 in Archipelago.",
+ language="English",
+ file_name="guide_en.md",
+ link="guide/en",
+ authors=["PoryGone"]
+ )
+
+ tutorials = [setup_en]
+
+ option_groups = celeste_64_option_groups
+
+
+class Celeste64World(World):
+ """Relive the magic of Celeste Mountain alongside Madeline in this small, heartfelt 3D platformer.
+ Created in a week(ish) by the Celeste team to celebrate the game’s sixth anniversary ðŸ“✨"""
+
+ # Class Data
+ game = "Celeste 64"
+ web = Celeste64WebWorld()
+ options_dataclass = Celeste64Options
+ options: Celeste64Options
+ location_name_to_id = location_table
+ item_name_to_id = item_table
+
+ # Instance Data
+ strawberries_required: int
+ active_logic_mapping: Dict[str, List[List[str]]]
+ goal_logic_mapping: Dict[str, List[List[str]]]
+
+
+ def create_item(self, name: str) -> Celeste64Item:
+ # Only make required amount of strawberries be Progression
+ if getattr(self, "strawberries_required", None) and name == ItemName.strawberry:
+ classification: ItemClassification = ItemClassification.filler
+ self.prog_strawberries = getattr(self, "prog_strawberries", 0)
+ if self.prog_strawberries < self.strawberries_required:
+ classification = ItemClassification.progression_skip_balancing
+ self.prog_strawberries += 1
+
+ return Celeste64Item(name, classification, item_data_table[name].code, self.player)
+ else:
+ return Celeste64Item(name, item_data_table[name].type, item_data_table[name].code, self.player)
+
+ def create_items(self) -> None:
+ item_pool: List[Celeste64Item] = []
+
+ location_count: int = 30
+
+ if self.options.friendsanity:
+ location_count += 9
+
+ if self.options.signsanity:
+ location_count += 5
+
+ if self.options.carsanity:
+ location_count += 2
+
+ item_pool += [self.create_item(name)
+ for name in unlockable_item_data_table.keys()
+ if name not in self.options.start_inventory]
+
+ if self.options.move_shuffle:
+ move_items_for_itempool: List[str] = deepcopy(list(move_item_data_table.keys()))
+
+ if self.options.logic_difficulty == "standard":
+ # If the start_inventory already includes a move, don't worry about giving it one
+ if not [move for move in move_items_for_itempool if move in self.options.start_inventory]:
+ chosen_start_move = self.random.choice(move_items_for_itempool)
+ move_items_for_itempool.remove(chosen_start_move)
+
+ if self.options.carsanity:
+ intro_car_loc: Location = self.multiworld.get_location(LocationName.car_1, self.player)
+ intro_car_loc.place_locked_item(self.create_item(chosen_start_move))
+ location_count -= 1
+ else:
+ self.multiworld.push_precollected(self.create_item(chosen_start_move))
+
+ item_pool += [self.create_item(name)
+ for name in move_items_for_itempool
+ if name not in self.options.start_inventory]
+
+ real_total_strawberries: int = min(self.options.total_strawberries.value, location_count - len(item_pool))
+ self.strawberries_required = int(real_total_strawberries * (self.options.strawberries_required_percentage / 100))
+
+ item_pool += [self.create_item(ItemName.strawberry) for _ in range(real_total_strawberries)]
+
+ filler_item_count: int = location_count - len(item_pool)
+ item_pool += [self.create_item(ItemName.raspberry) for _ in range(filler_item_count)]
+
+ self.multiworld.itempool += item_pool
+
+
+ def create_regions(self) -> None:
+ from .Regions import region_data_table
+ # Create regions.
+ for region_name in region_data_table.keys():
+ region = Region(region_name, self.player, self.multiworld)
+ self.multiworld.regions.append(region)
+
+ # Create locations.
+ for region_name, region_data in region_data_table.items():
+ region = self.multiworld.get_region(region_name, self.player)
+ region.add_locations({
+ location_name: location_data.address for location_name, location_data in strawberry_location_data_table.items()
+ if location_data.region == region_name
+ }, Celeste64Location)
+
+ if self.options.friendsanity:
+ region.add_locations({
+ location_name: location_data.address for location_name, location_data in friend_location_data_table.items()
+ if location_data.region == region_name
+ }, Celeste64Location)
+
+ if self.options.signsanity:
+ region.add_locations({
+ location_name: location_data.address for location_name, location_data in sign_location_data_table.items()
+ if location_data.region == region_name
+ }, Celeste64Location)
+
+ if self.options.carsanity:
+ region.add_locations({
+ location_name: location_data.address for location_name, location_data in car_location_data_table.items()
+ if location_data.region == region_name
+ }, Celeste64Location)
+
+ region.add_exits(region_data_table[region_name].connecting_regions)
+
+
+ def get_filler_item_name(self) -> str:
+ return ItemName.raspberry
+
+
+ def set_rules(self) -> None:
+ from .Rules import set_rules
+ set_rules(self)
+
+
+ def fill_slot_data(self):
+ return {
+ "death_link": self.options.death_link.value,
+ "death_link_amnesty": self.options.death_link_amnesty.value,
+ "strawberries_required": self.strawberries_required,
+ "move_shuffle": self.options.move_shuffle.value,
+ "friendsanity": self.options.friendsanity.value,
+ "signsanity": self.options.signsanity.value,
+ "carsanity": self.options.carsanity.value,
+ "badeline_chaser_source": self.options.badeline_chaser_source.value,
+ "badeline_chaser_frequency": self.options.badeline_chaser_frequency.value,
+ "badeline_chaser_speed": self.options.badeline_chaser_speed.value,
+ }
diff --git a/worlds/celeste64/docs/en_Celeste 64.md b/worlds/celeste64/docs/en_Celeste 64.md
new file mode 100644
index 000000000000..efc42bfe56eb
--- /dev/null
+++ b/worlds/celeste64/docs/en_Celeste 64.md
@@ -0,0 +1,24 @@
+# Celeste 64
+
+## What is this game?
+
+Relive the magic of Celeste Mountain alongside Madeline in this small, heartfelt 3D platformer.
+Created in a week(ish) by the Celeste team to celebrate the game's sixth anniversary.
+
+Ported to Archipelago in a week(ish) by PoryGone, this World provides the following as unlockable items:
+- Strawberries
+- Dash Refills
+- Double Dash Refills
+- Feathers
+- Coins
+- Cassettes
+- Traffic Blocks
+- Springs
+- Breakable Blocks
+
+The goal is to collect a certain number of Strawberries, then visit Badeline on her floating island.
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains all the options you need to configure
+and export a config file.
diff --git a/worlds/celeste64/docs/guide_en.md b/worlds/celeste64/docs/guide_en.md
new file mode 100644
index 000000000000..24fea92e3528
--- /dev/null
+++ b/worlds/celeste64/docs/guide_en.md
@@ -0,0 +1,37 @@
+# Celeste 64 Setup Guide
+
+## Required Software
+- Archipelago Build of Celeste 64 from: [Celeste 64 Archipelago Releases Page](https://github.com/PoryGoneDev/Celeste64/releases/)
+
+## Optional Software
+- Celeste 64 Tracker
+ - PopTracker from: [PopTracker Releases Page](https://github.com/black-sliver/PopTracker/releases/)
+ - Celeste 64 Archipelago PopTracker pack from: [Celeste 64 AP Tracker Releases Page](https://github.com/PoryGone/Celeste-64-AP-Tracker/releases/)
+
+## Installation Procedures (Windows)
+
+1. Download the above release and extract it.
+
+## Joining a MultiWorld Game
+
+1. Before launching the game, edit the `AP.json` file in the root of the Celeste 64 install.
+
+2. For the `Url` field, enter the address of the server, such as `archipelago.gg:38281`. Your server host should be able to tell you this.
+
+3. For the `SlotName` field, enter your "name" field from the yaml or website config.
+
+4. For the `Password` field, enter the server password if one exists; otherwise leave this field blank.
+
+5. Save the file, and run `Celeste64.exe`. If you can continue past the title screen, then you are successfully connected.
+
+An Example `AP.json` file:
+
+```
+{
+ "Url": "archipelago:12345",
+ "SlotName": "Maddy",
+ "Password": ""
+}
+```
+
+
diff --git a/worlds/checksfinder/Locations.py b/worlds/checksfinder/Locations.py
index 8a2ae07b27b6..59a96c83ea8a 100644
--- a/worlds/checksfinder/Locations.py
+++ b/worlds/checksfinder/Locations.py
@@ -10,10 +10,6 @@ class AdvData(typing.NamedTuple):
class ChecksFinderAdvancement(Location):
game: str = "ChecksFinder"
- def __init__(self, player: int, name: str, address: typing.Optional[int], parent):
- super().__init__(player, name, address, parent)
- self.event = not address
-
advancement_table = {
"Tile 1": AdvData(81000, 'Board'),
diff --git a/worlds/checksfinder/Rules.py b/worlds/checksfinder/Rules.py
index 4e12668798dc..38d7d77ad393 100644
--- a/worlds/checksfinder/Rules.py
+++ b/worlds/checksfinder/Rules.py
@@ -1,37 +1,34 @@
-from ..generic.Rules import set_rule, add_rule
-from BaseClasses import MultiWorld
-from ..AutoWorld import LogicMixin
+from ..generic.Rules import set_rule
+from BaseClasses import MultiWorld, CollectionState
-class ChecksFinderLogic(LogicMixin):
-
- def _has_total(self, player: int, total: int):
- return (self.item_count('Map Width', player)+self.item_count('Map Height', player)+
- self.item_count('Map Bombs', player)) >= total
+def _has_total(state: CollectionState, player: int, total: int):
+ return (state.count('Map Width', player) + state.count('Map Height', player) +
+ state.count('Map Bombs', player)) >= total
# Sets rules on entrances and advancements that are always applied
def set_rules(world: MultiWorld, player: int):
- set_rule(world.get_location(("Tile 6"), player), lambda state: state._has_total(player, 1))
- set_rule(world.get_location(("Tile 7"), player), lambda state: state._has_total(player, 2))
- set_rule(world.get_location(("Tile 8"), player), lambda state: state._has_total(player, 3))
- set_rule(world.get_location(("Tile 9"), player), lambda state: state._has_total(player, 4))
- set_rule(world.get_location(("Tile 10"), player), lambda state: state._has_total(player, 5))
- set_rule(world.get_location(("Tile 11"), player), lambda state: state._has_total(player, 6))
- set_rule(world.get_location(("Tile 12"), player), lambda state: state._has_total(player, 7))
- set_rule(world.get_location(("Tile 13"), player), lambda state: state._has_total(player, 8))
- set_rule(world.get_location(("Tile 14"), player), lambda state: state._has_total(player, 9))
- set_rule(world.get_location(("Tile 15"), player), lambda state: state._has_total(player, 10))
- set_rule(world.get_location(("Tile 16"), player), lambda state: state._has_total(player, 11))
- set_rule(world.get_location(("Tile 17"), player), lambda state: state._has_total(player, 12))
- set_rule(world.get_location(("Tile 18"), player), lambda state: state._has_total(player, 13))
- set_rule(world.get_location(("Tile 19"), player), lambda state: state._has_total(player, 14))
- set_rule(world.get_location(("Tile 20"), player), lambda state: state._has_total(player, 15))
- set_rule(world.get_location(("Tile 21"), player), lambda state: state._has_total(player, 16))
- set_rule(world.get_location(("Tile 22"), player), lambda state: state._has_total(player, 17))
- set_rule(world.get_location(("Tile 23"), player), lambda state: state._has_total(player, 18))
- set_rule(world.get_location(("Tile 24"), player), lambda state: state._has_total(player, 19))
- set_rule(world.get_location(("Tile 25"), player), lambda state: state._has_total(player, 20))
+ set_rule(world.get_location("Tile 6", player), lambda state: _has_total(state, player, 1))
+ set_rule(world.get_location("Tile 7", player), lambda state: _has_total(state, player, 2))
+ set_rule(world.get_location("Tile 8", player), lambda state: _has_total(state, player, 3))
+ set_rule(world.get_location("Tile 9", player), lambda state: _has_total(state, player, 4))
+ set_rule(world.get_location("Tile 10", player), lambda state: _has_total(state, player, 5))
+ set_rule(world.get_location("Tile 11", player), lambda state: _has_total(state, player, 6))
+ set_rule(world.get_location("Tile 12", player), lambda state: _has_total(state, player, 7))
+ set_rule(world.get_location("Tile 13", player), lambda state: _has_total(state, player, 8))
+ set_rule(world.get_location("Tile 14", player), lambda state: _has_total(state, player, 9))
+ set_rule(world.get_location("Tile 15", player), lambda state: _has_total(state, player, 10))
+ set_rule(world.get_location("Tile 16", player), lambda state: _has_total(state, player, 11))
+ set_rule(world.get_location("Tile 17", player), lambda state: _has_total(state, player, 12))
+ set_rule(world.get_location("Tile 18", player), lambda state: _has_total(state, player, 13))
+ set_rule(world.get_location("Tile 19", player), lambda state: _has_total(state, player, 14))
+ set_rule(world.get_location("Tile 20", player), lambda state: _has_total(state, player, 15))
+ set_rule(world.get_location("Tile 21", player), lambda state: _has_total(state, player, 16))
+ set_rule(world.get_location("Tile 22", player), lambda state: _has_total(state, player, 17))
+ set_rule(world.get_location("Tile 23", player), lambda state: _has_total(state, player, 18))
+ set_rule(world.get_location("Tile 24", player), lambda state: _has_total(state, player, 19))
+ set_rule(world.get_location("Tile 25", player), lambda state: _has_total(state, player, 20))
# Sets rules on completion condition
diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py
index 9ca16ca0d2ae..c8b9587f8500 100644
--- a/worlds/checksfinder/__init__.py
+++ b/worlds/checksfinder/__init__.py
@@ -10,12 +10,12 @@
class ChecksFinderWeb(WebWorld):
tutorials = [Tutorial(
- "Multiworld Setup Tutorial",
+ "Multiworld Setup Guide",
"A guide to setting up the Archipelago ChecksFinder software on your computer. This guide covers "
"single-player, multiworld, and related software.",
"English",
- "checksfinder_en.md",
- "checksfinder/en",
+ "setup_en.md",
+ "setup/en",
["Mewlif"]
)]
@@ -33,8 +33,6 @@ class ChecksFinderWorld(World):
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {name: data.id for name, data in advancement_table.items()}
- data_version = 4
-
def _get_checksfinder_data(self):
return {
'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32),
@@ -45,7 +43,7 @@ def _get_checksfinder_data(self):
'race': self.multiworld.is_race,
}
- def generate_basic(self):
+ def create_items(self):
# Generate item pool
itempool = []
@@ -69,8 +67,8 @@ def set_rules(self):
def create_regions(self):
menu = Region("Menu", self.player, self.multiworld)
board = Region("Board", self.player, self.multiworld)
- board.locations = [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board)
- for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name]
+ board.locations += [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board)
+ for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name]
connection = Entrance(self.player, "New Board", menu)
menu.exits.append(connection)
diff --git a/worlds/checksfinder/docs/checksfinder_en.md b/worlds/checksfinder/docs/checksfinder_en.md
deleted file mode 100644
index 28c7c32580c3..000000000000
--- a/worlds/checksfinder/docs/checksfinder_en.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# ChecksFinder Randomizer Setup Guide
-
-## Required Software
-
-- ChecksFinder from
- the [Github releases Page for the game](https://github.com/jonloveslegos/ChecksFinder/releases) (latest version)
-- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- - (select `ChecksFinder Client` during installation.)
-
-## Configuring your YAML file
-
-### What is a YAML file and why do I need one?
-
-See the guide on setting up a basic YAML at the Archipelago setup
-guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
-
-### Where do I get a YAML file?
-
-You can customize your settings by visiting the [ChecksFinder Player Settings Page](/games/ChecksFinder/player-settings)
-
-### Generating a ChecksFinder game
-
-**ChecksFinder is meant to be played _alongside_ another game! You may not be playing it for long periods of time if
-you play it by itself with another person!**
-
-When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
-the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
-files. You do not have a file inside that zip though!
-
-You need to start ChecksFinder client yourself, it is located within the Archipelago folder.
-
-### Connect to the MultiServer
-
-First start ChecksFinder.
-
-Once both ChecksFinder and the client are started. In the client at the top type in the spot labeled `Server` type the
-`Ip Address` and `Port` separated with a `:` symbol.
-
-The client will then ask for the username you chose, input that in the text box at the bottom of the client.
-
-### Play the game
-
-When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a
-multiworld game!
-
diff --git a/worlds/checksfinder/docs/en_ChecksFinder.md b/worlds/checksfinder/docs/en_ChecksFinder.md
index bd82660b09ba..c9569376c5f6 100644
--- a/worlds/checksfinder/docs/en_ChecksFinder.md
+++ b/worlds/checksfinder/docs/en_ChecksFinder.md
@@ -1,8 +1,8 @@
# ChecksFinder
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What is considered a location check in ChecksFinder?
@@ -14,11 +14,18 @@ many checks as you have gained items, plus five to start with being available.
## When the player receives an item, what happens?
When the player receives an item in ChecksFinder, it either can make the future boards they play be bigger in width or
-height, or add a new bomb to the future boards, with a limit to having up to one fifth of the _current_ board being
-bombs. The items you have gained _before_ the current board was made will be said at the bottom of the screen as a number
+height, or add a new bomb to the future boards, with a limit to having up to one fifth of the _current_ board being
+bombs. The items you have gained _before_ the current board was made will be said at the bottom of the screen as a
+number
next to an icon, the number is how many you have gotten and the icon represents which item it is.
## What is the victory condition?
Victory is achieved when the player wins a board they were given after they have received all of their Map Width, Map
-Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received.
\ No newline at end of file
+Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received.
+
+## Unique Local Commands
+
+The following command is only available when using the ChecksFinderClient to play with Archipelago.
+
+- `/resync` Manually trigger a resync.
diff --git a/worlds/checksfinder/docs/setup_en.md b/worlds/checksfinder/docs/setup_en.md
new file mode 100644
index 000000000000..673b34900af7
--- /dev/null
+++ b/worlds/checksfinder/docs/setup_en.md
@@ -0,0 +1,44 @@
+# ChecksFinder Randomizer Setup Guide
+
+## Required Software
+
+- ChecksFinder from
+ the [Github releases Page for the game](https://github.com/jonloveslegos/ChecksFinder/releases) (latest version)
+- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
+
+## Configuring your YAML file
+
+### What is a YAML file and why do I need one?
+
+See the guide on setting up a basic YAML at the Archipelago setup
+guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
+
+### Where do I get a YAML file?
+
+You can customize your options by visiting the [ChecksFinder Player Options Page](/games/ChecksFinder/player-options)
+
+### Generating a ChecksFinder game
+
+**ChecksFinder is meant to be played _alongside_ another game! You may not be playing it for long periods of time if
+you play it by itself with another person!**
+
+When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
+the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
+files. You do not have a file inside that zip though!
+
+You need to start ChecksFinder client yourself, it is located within the Archipelago folder.
+
+### Connect to the MultiServer
+
+First start ChecksFinder.
+
+Once both ChecksFinder and the client are started. In the client at the top type in the spot labeled `Server` type the
+`Ip Address` and `Port` separated with a `:` symbol.
+
+The client will then ask for the username you chose, input that in the text box at the bottom of the client.
+
+### Play the game
+
+When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a
+multiworld game!
+
diff --git a/worlds/clique/__init__.py b/worlds/clique/__init__.py
index 583838904726..b5cc74d94ac0 100644
--- a/worlds/clique/__init__.py
+++ b/worlds/clique/__init__.py
@@ -11,23 +11,32 @@
class CliqueWebWorld(WebWorld):
theme = "partyTime"
- tutorials = [
- Tutorial(
- tutorial_name="Start Guide",
- description="A guide to playing Clique.",
- language="English",
- file_name="guide_en.md",
- link="guide/en",
- authors=["Phar"]
- )
- ]
+
+ setup_en = Tutorial(
+ tutorial_name="Start Guide",
+ description="A guide to playing Clique.",
+ language="English",
+ file_name="guide_en.md",
+ link="guide/en",
+ authors=["Phar"]
+ )
+
+ setup_de = Tutorial(
+ tutorial_name="Anleitung zum Anfangen",
+ description="Eine Anleitung um Clique zu spielen.",
+ language="Deutsch",
+ file_name="guide_de.md",
+ link="guide/de",
+ authors=["Held_der_Zeit"]
+ )
+
+ tutorials = [setup_en, setup_de]
class CliqueWorld(World):
"""The greatest game of all time."""
game = "Clique"
- data_version = 3
web = CliqueWebWorld()
option_definitions = clique_options
location_name_to_id = location_table
diff --git a/worlds/clique/docs/de_Clique.md b/worlds/clique/docs/de_Clique.md
new file mode 100644
index 000000000000..cde0a23cf6fe
--- /dev/null
+++ b/worlds/clique/docs/de_Clique.md
@@ -0,0 +1,18 @@
+# Clique
+
+## Was ist das für ein Spiel?
+
+~~Clique ist ein psychologisches Überlebens-Horror Spiel, in dem der Spieler der Versuchung wiederstehen muss große~~
+~~(rote) Knöpfe zu drücken.~~
+
+Clique ist ein scherzhaftes Spiel, welches für Archipelago im März 2023 entwickelt wurde, um zu zeigen, wie einfach
+es sein kann eine Welt für Archipelago zu entwicklen. Das Ziel des Spiels ist es den großen (standardmäßig) roten
+Knopf zu drücken. Wenn ein Spieler auf dem `hard_mode` (schwieriger Modus) spielt, muss dieser warten bis jemand
+anderes in der Multiworld den Knopf aktiviert, damit er gedrückt werden kann.
+
+Clique kann auf den meisten modernen, HTML5-fähigen Browsern gespielt werden.
+
+## Wo ist die Seite für die Einstellungen?
+
+Die [Seite für die Spielereinstellungen dieses Spiels](../player-options) enthält alle Optionen die man benötigt um
+eine YAML-Datei zu konfigurieren und zu exportieren.
diff --git a/worlds/clique/docs/en_Clique.md b/worlds/clique/docs/en_Clique.md
index 862454f5c613..e9cb164fecbf 100644
--- a/worlds/clique/docs/en_Clique.md
+++ b/worlds/clique/docs/en_Clique.md
@@ -10,7 +10,7 @@ wait for someone else in the multiworld to "activate" their button before they c
Clique can be played on most modern HTML5-capable browsers.
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure
+The [player options page for this game](../player-options) contains all the options you need to configure
and export a config file.
diff --git a/worlds/clique/docs/guide_de.md b/worlds/clique/docs/guide_de.md
new file mode 100644
index 000000000000..26e08dbbdd7e
--- /dev/null
+++ b/worlds/clique/docs/guide_de.md
@@ -0,0 +1,25 @@
+# Clique Anleitung
+
+Nachdem dein Seed generiert wurde, gehe auf die Website von [Clique dem Spiel](http://clique.pharware.com/) und gib
+Server-Daten, deinen Slot-Namen und ein Passwort (falls vorhanden) ein. Klicke dann auf "Connect" (Verbinden).
+
+Wenn du auf "Einfach" spielst, kannst du unbedenklich den Knopf drücken und deine "Befriedigung" erhalten.
+
+Wenn du auf "Schwer" spielst, ist es sehr wahrscheinlich, dass du warten musst bevor du dein Ziel erreichen kannst.
+Glücklicherweise läuft Click auf den meißten großen Browsern, die HTML5 unterstützen. Das heißt du kannst Clique auf
+deinem Handy starten und produktiv sein während du wartest!
+
+Falls du einige Ideen brauchst was du tun kannst, während du wartest bis der Knopf aktiviert wurde, versuche
+(mindestens) eins der Folgenden:
+
+- Dein Zimmer aufräumen.
+- Die Wäsche machen.
+- Etwas Essen von einem X-Belieben Fast Food Restaruant holen.
+- Das tägliche Wordle machen.
+- ~~Deine Seele an **Phar** verkaufen.~~
+- Deine Hausaufgaben erledigen.
+- Deine Post abholen.
+
+
+~~Solltest du auf irgendwelche Probleme in diesem Spiel stoßen, solltest du keinesfalls nicht **thephar** auf~~
+~~Discord kontaktieren. *zwinker* *zwinker*~~
diff --git a/worlds/cv64/__init__.py b/worlds/cv64/__init__.py
new file mode 100644
index 000000000000..0d384acc8f3d
--- /dev/null
+++ b/worlds/cv64/__init__.py
@@ -0,0 +1,317 @@
+import os
+import typing
+import settings
+import base64
+import logging
+
+from BaseClasses import Item, Region, Tutorial, ItemClassification
+from .items import CV64Item, filler_item_names, get_item_info, get_item_names_to_ids, get_item_counts
+from .locations import CV64Location, get_location_info, verify_locations, get_location_names_to_ids, base_id
+from .entrances import verify_entrances, get_warp_entrances
+from .options import CV64Options, cv64_option_groups, CharacterStages, DraculasCondition, SubWeaponShuffle
+from .stages import get_locations_from_stage, get_normal_stage_exits, vanilla_stage_order, \
+ shuffle_stages, generate_warps, get_region_names
+from .regions import get_region_info
+from .rules import CV64Rules
+from .data import iname, rname, ename
+from worlds.AutoWorld import WebWorld, World
+from .aesthetics import randomize_lighting, shuffle_sub_weapons, rom_empty_breakables_flags, rom_sub_weapon_flags, \
+ randomize_music, get_start_inventory_data, get_location_data, randomize_shop_prices, get_loading_zone_bytes, \
+ get_countdown_numbers
+from .rom import RomData, write_patch, get_base_rom_path, CV64ProcedurePatch, CV64_US_10_HASH
+from .client import Castlevania64Client
+
+
+class CV64Settings(settings.Group):
+ class RomFile(settings.UserFilePath):
+ """File name of the CV64 US 1.0 rom"""
+ copy_to = "Castlevania (USA).z64"
+ description = "CV64 (US 1.0) ROM File"
+ md5s = [CV64_US_10_HASH]
+
+ rom_file: RomFile = RomFile(RomFile.copy_to)
+
+
+class CV64Web(WebWorld):
+ theme = "stone"
+
+ tutorials = [Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to setting up the Archipleago Castlevania 64 randomizer on your computer and connecting it to a "
+ "multiworld.",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["Liquid Cat"]
+ )]
+
+ option_groups = cv64_option_groups
+
+
+class CV64World(World):
+ """
+ Castlevania for the Nintendo 64 is the first 3D game in the Castlevania franchise. As either whip-wielding Belmont
+ descendant Reinhardt Schneider or powerful sorceress Carrie Fernandez, brave many terrifying traps and foes as you
+ make your way to Dracula's chamber and stop his rule of terror!
+ """
+ game = "Castlevania 64"
+ item_name_groups = {
+ "Bomb": {iname.magical_nitro, iname.mandragora},
+ "Ingredient": {iname.magical_nitro, iname.mandragora},
+ }
+ location_name_groups = {stage: set(get_locations_from_stage(stage)) for stage in vanilla_stage_order}
+ options_dataclass = CV64Options
+ options: CV64Options
+ settings: typing.ClassVar[CV64Settings]
+ topology_present = True
+
+ item_name_to_id = get_item_names_to_ids()
+ location_name_to_id = get_location_names_to_ids()
+
+ active_stage_exits: typing.Dict[str, typing.Dict]
+ active_stage_list: typing.List[str]
+ active_warp_list: typing.List[str]
+
+ # Default values to possibly be updated in generate_early
+ reinhardt_stages: bool = True
+ carrie_stages: bool = True
+ branching_stages: bool = False
+ starting_stage: str = rname.forest_of_silence
+ total_s1s: int = 7
+ s1s_per_warp: int = 1
+ total_s2s: int = 0
+ required_s2s: int = 0
+ drac_condition: int = 0
+
+ auth: bytearray
+
+ web = CV64Web()
+
+ def generate_early(self) -> None:
+ # Generate the player's unique authentication
+ self.auth = bytearray(self.multiworld.random.getrandbits(8) for _ in range(16))
+
+ self.total_s1s = self.options.total_special1s.value
+ self.s1s_per_warp = self.options.special1s_per_warp.value
+ self.drac_condition = self.options.draculas_condition.value
+
+ # If there are more S1s needed to unlock the whole warp menu than there are S1s in total, drop S1s per warp to
+ # something manageable.
+ if self.s1s_per_warp * 7 > self.total_s1s:
+ self.s1s_per_warp = self.total_s1s // 7
+ logging.warning(f"[{self.multiworld.player_name[self.player]}] Too many required Special1s "
+ f"({self.options.special1s_per_warp.value * 7}) for Special1s Per Warp setting: "
+ f"{self.options.special1s_per_warp.value} with Total Special1s setting: "
+ f"{self.options.total_special1s.value}. Lowering Special1s Per Warp to: "
+ f"{self.s1s_per_warp}")
+ self.options.special1s_per_warp.value = self.s1s_per_warp
+
+ # Set the total and required Special2s to 1 if the drac condition is the Crystal, to the specified YAML numbers
+ # if it's Specials, or to 0 if it's None or Bosses. The boss totals will be figured out later.
+ if self.drac_condition == DraculasCondition.option_crystal:
+ self.total_s2s = 1
+ self.required_s2s = 1
+ elif self.drac_condition == DraculasCondition.option_specials:
+ self.total_s2s = self.options.total_special2s.value
+ self.required_s2s = int(self.options.percent_special2s_required.value / 100 * self.total_s2s)
+
+ # Enable/disable character stages and branching paths accordingly
+ if self.options.character_stages == CharacterStages.option_reinhardt_only:
+ self.carrie_stages = False
+ elif self.options.character_stages == CharacterStages.option_carrie_only:
+ self.reinhardt_stages = False
+ elif self.options.character_stages == CharacterStages.option_both:
+ self.branching_stages = True
+
+ self.active_stage_exits = get_normal_stage_exits(self)
+
+ stage_1_blacklist = []
+
+ # Prevent Clock Tower from being Stage 1 if more than 4 S1s are needed to warp out of it.
+ if self.s1s_per_warp > 4 and not self.options.multi_hit_breakables:
+ stage_1_blacklist.append(rname.clock_tower)
+
+ # Shuffle the stages if the option is on.
+ if self.options.stage_shuffle:
+ self.active_stage_exits, self.starting_stage, self.active_stage_list = \
+ shuffle_stages(self, stage_1_blacklist)
+ else:
+ self.active_stage_list = [stage for stage in vanilla_stage_order if stage in self.active_stage_exits]
+
+ # Create a list of warps from the active stage list. They are in a random order by default and will never
+ # include the starting stage.
+ self.active_warp_list = generate_warps(self)
+
+ def create_regions(self) -> None:
+ # Add the Menu Region.
+ created_regions = [Region("Menu", self.player, self.multiworld)]
+
+ # Add every stage Region by checking to see if that stage is active.
+ created_regions.extend([Region(name, self.player, self.multiworld)
+ for name in get_region_names(self.active_stage_exits)])
+
+ # Add the Renon's shop Region if shopsanity is on.
+ if self.options.shopsanity:
+ created_regions.append(Region(rname.renon, self.player, self.multiworld))
+
+ # Add the Dracula's chamber (the end) Region.
+ created_regions.append(Region(rname.ck_drac_chamber, self.player, self.multiworld))
+
+ # Set up the Regions correctly.
+ self.multiworld.regions.extend(created_regions)
+
+ # Add the warp Entrances to the Menu Region (the one always at the start of the Region list).
+ created_regions[0].add_exits(get_warp_entrances(self.active_warp_list))
+
+ for reg in created_regions:
+
+ # Add the Entrances to all the Regions.
+ ent_names = get_region_info(reg.name, "entrances")
+ if ent_names is not None:
+ reg.add_exits(verify_entrances(self.options, ent_names, self.active_stage_exits))
+
+ # Add the Locations to all the Regions.
+ loc_names = get_region_info(reg.name, "locations")
+ if loc_names is None:
+ continue
+ verified_locs, events = verify_locations(self.options, loc_names)
+ reg.add_locations(verified_locs, CV64Location)
+
+ # Place event Items on all of their associated Locations.
+ for event_loc, event_item in events.items():
+ self.get_location(event_loc).place_locked_item(self.create_item(event_item, "progression"))
+ # If we're looking at a boss kill trophy, increment the total S2s and, if we're not already at the
+ # set number of required bosses, the total required number. This way, we can prevent gen failures
+ # should the player set more bosses required than there are total.
+ if event_item == iname.trophy:
+ self.total_s2s += 1
+ if self.required_s2s < self.options.bosses_required.value:
+ self.required_s2s += 1
+
+ # If Dracula's Condition is Bosses and there are less calculated required S2s than the value specified by the
+ # player (meaning there weren't enough bosses to reach the player's setting), throw a warning and lower the
+ # option value.
+ if self.options.draculas_condition == DraculasCondition.option_bosses and self.required_s2s < \
+ self.options.bosses_required.value:
+ logging.warning(f"[{self.multiworld.player_name[self.player]}] Not enough bosses for Bosses Required "
+ f"setting: {self.options.bosses_required.value}. Lowering to: {self.required_s2s}")
+ self.options.bosses_required.value = self.required_s2s
+
+ def create_item(self, name: str, force_classification: typing.Optional[str] = None) -> Item:
+ if force_classification is not None:
+ classification = getattr(ItemClassification, force_classification)
+ else:
+ classification = getattr(ItemClassification, get_item_info(name, "default classification"))
+
+ code = get_item_info(name, "code")
+ if code is not None:
+ code += base_id
+
+ created_item = CV64Item(name, classification, code, self.player)
+
+ return created_item
+
+ def create_items(self) -> None:
+ item_counts = get_item_counts(self)
+
+ # Set up the items correctly
+ self.multiworld.itempool += [self.create_item(item, classification) for classification in item_counts for item
+ in item_counts[classification] for _ in range(item_counts[classification][item])]
+
+ def set_rules(self) -> None:
+ # Set all the Entrance rules properly.
+ CV64Rules(self).set_cv64_rules()
+
+ def pre_fill(self) -> None:
+ # If we need more Special1s to warp out of Sphere 1 than there are locations available, then AP's fill
+ # algorithm may try placing the Special1s anyway despite placing the stage's single key always being an option.
+ # To get around this problem in the fill algorithm, the keys will be forced early in these situations to ensure
+ # the algorithm will pick them over the Special1s.
+ if self.starting_stage == rname.tower_of_science:
+ if self.s1s_per_warp > 3:
+ self.multiworld.local_early_items[self.player][iname.science_key2] = 1
+ elif self.starting_stage == rname.clock_tower:
+ if (self.s1s_per_warp > 2 and not self.options.multi_hit_breakables) or \
+ (self.s1s_per_warp > 8 and self.options.multi_hit_breakables):
+ self.multiworld.local_early_items[self.player][iname.clocktower_key1] = 1
+ elif self.starting_stage == rname.castle_wall:
+ if self.s1s_per_warp > 5 and not self.options.hard_logic and \
+ not self.options.multi_hit_breakables:
+ self.multiworld.local_early_items[self.player][iname.left_tower_key] = 1
+
+ def generate_output(self, output_directory: str) -> None:
+ active_locations = self.multiworld.get_locations(self.player)
+
+ # Location data and shop names, descriptions, and colors
+ offset_data, shop_name_list, shop_colors_list, shop_desc_list = \
+ get_location_data(self, active_locations)
+ # Shop prices
+ if self.options.shop_prices:
+ offset_data.update(randomize_shop_prices(self))
+ # Map lighting
+ if self.options.map_lighting:
+ offset_data.update(randomize_lighting(self))
+ # Sub-weapons
+ if self.options.sub_weapon_shuffle == SubWeaponShuffle.option_own_pool:
+ offset_data.update(shuffle_sub_weapons(self))
+ elif self.options.sub_weapon_shuffle == SubWeaponShuffle.option_anywhere:
+ offset_data.update(rom_sub_weapon_flags)
+ # Empty breakables
+ if self.options.empty_breakables:
+ offset_data.update(rom_empty_breakables_flags)
+ # Music
+ if self.options.background_music:
+ offset_data.update(randomize_music(self))
+ # Loading zones
+ offset_data.update(get_loading_zone_bytes(self.options, self.starting_stage, self.active_stage_exits))
+ # Countdown
+ if self.options.countdown:
+ offset_data.update(get_countdown_numbers(self.options, active_locations))
+ # Start Inventory
+ offset_data.update(get_start_inventory_data(self.player, self.options,
+ self.multiworld.precollected_items[self.player]))
+
+ patch = CV64ProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player])
+ write_patch(self, patch, offset_data, shop_name_list, shop_desc_list, shop_colors_list, active_locations)
+
+ rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}"
+ f"{patch.patch_file_ending}")
+
+ patch.write(rom_path)
+
+ def get_filler_item_name(self) -> str:
+ return self.random.choice(filler_item_names)
+
+ def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]):
+ # Attach each location's stage's position to its hint information if Stage Shuffle is on.
+ if not self.options.stage_shuffle:
+ return
+
+ stage_pos_data = {}
+ for loc in list(self.multiworld.get_locations(self.player)):
+ stage = get_region_info(loc.parent_region.name, "stage")
+ if stage is not None and loc.address is not None:
+ num = str(self.active_stage_exits[stage]["position"]).zfill(2)
+ path = self.active_stage_exits[stage]["path"]
+ stage_pos_data[loc.address] = f"Stage {num}"
+ if path != " ":
+ stage_pos_data[loc.address] += path
+ hint_data[self.player] = stage_pos_data
+
+ def modify_multidata(self, multidata: typing.Dict[str, typing.Any]):
+ # Put the player's unique authentication in connect_names.
+ multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = \
+ multidata["connect_names"][self.multiworld.player_name[self.player]]
+
+ def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
+ # Write the stage order to the spoiler log
+ spoiler_handle.write(f"\nCastlevania 64 stage & warp orders for {self.multiworld.player_name[self.player]}:\n")
+ for stage in self.active_stage_list:
+ num = str(self.active_stage_exits[stage]["position"]).zfill(2)
+ path = self.active_stage_exits[stage]["path"]
+ spoiler_handle.writelines(f"Stage {num}{path}:\t{stage}\n")
+
+ # Write the warp order to the spoiler log
+ spoiler_handle.writelines(f"\nStart :\t{self.active_stage_list[0]}\n")
+ for i in range(1, len(self.active_warp_list)):
+ spoiler_handle.writelines(f"Warp {i}:\t{self.active_warp_list[i]}\n")
diff --git a/worlds/cv64/aesthetics.py b/worlds/cv64/aesthetics.py
new file mode 100644
index 000000000000..66709174d837
--- /dev/null
+++ b/worlds/cv64/aesthetics.py
@@ -0,0 +1,653 @@
+import logging
+
+from BaseClasses import ItemClassification, Location, Item
+from .data import iname, rname
+from .options import CV64Options, BackgroundMusic, Countdown, IceTrapAppearance, InvisibleItems, CharacterStages
+from .stages import vanilla_stage_order, get_stage_info
+from .locations import get_location_info, base_id
+from .regions import get_region_info
+from .items import get_item_info, item_info
+
+from typing import TYPE_CHECKING, Dict, List, Tuple, Union, Iterable
+
+if TYPE_CHECKING:
+ from . import CV64World
+
+rom_sub_weapon_offsets = {
+ 0x10C6EB: (b"\x10", rname.forest_of_silence), # Forest
+ 0x10C6F3: (b"\x0F", rname.forest_of_silence),
+ 0x10C6FB: (b"\x0E", rname.forest_of_silence),
+ 0x10C703: (b"\x0D", rname.forest_of_silence),
+
+ 0x10C81F: (b"\x0F", rname.castle_wall), # Castle Wall
+ 0x10C827: (b"\x10", rname.castle_wall),
+ 0x10C82F: (b"\x0E", rname.castle_wall),
+ 0x7F9A0F: (b"\x0D", rname.castle_wall),
+
+ 0x83A5D9: (b"\x0E", rname.villa), # Villa
+ 0x83A5E5: (b"\x0D", rname.villa),
+ 0x83A5F1: (b"\x0F", rname.villa),
+ 0xBFC903: (b"\x10", rname.villa),
+ 0x10C987: (b"\x10", rname.villa),
+ 0x10C98F: (b"\x0D", rname.villa),
+ 0x10C997: (b"\x0F", rname.villa),
+ 0x10CF73: (b"\x10", rname.villa),
+
+ 0x10CA57: (b"\x0D", rname.tunnel), # Tunnel
+ 0x10CA5F: (b"\x0E", rname.tunnel),
+ 0x10CA67: (b"\x10", rname.tunnel),
+ 0x10CA6F: (b"\x0D", rname.tunnel),
+ 0x10CA77: (b"\x0F", rname.tunnel),
+ 0x10CA7F: (b"\x0E", rname.tunnel),
+
+ 0x10CBC7: (b"\x0E", rname.castle_center), # Castle Center
+ 0x10CC0F: (b"\x0D", rname.castle_center),
+ 0x10CC5B: (b"\x0F", rname.castle_center),
+
+ 0x10CD3F: (b"\x0E", rname.tower_of_execution), # Character towers
+ 0x10CD65: (b"\x0D", rname.tower_of_execution),
+ 0x10CE2B: (b"\x0E", rname.tower_of_science),
+ 0x10CE83: (b"\x10", rname.duel_tower),
+
+ 0x10CF8B: (b"\x0F", rname.room_of_clocks), # Room of Clocks
+ 0x10CF93: (b"\x0D", rname.room_of_clocks),
+
+ 0x99BC5A: (b"\x0D", rname.clock_tower), # Clock Tower
+ 0x10CECB: (b"\x10", rname.clock_tower),
+ 0x10CED3: (b"\x0F", rname.clock_tower),
+ 0x10CEDB: (b"\x0E", rname.clock_tower),
+ 0x10CEE3: (b"\x0D", rname.clock_tower),
+}
+
+rom_sub_weapon_flags = {
+ 0x10C6EC: b"\x02\x00\xFF\x04", # Forest of Silence
+ 0x10C6FC: b"\x04\x00\xFF\x04",
+ 0x10C6F4: b"\x08\x00\xFF\x04",
+ 0x10C704: b"\x40\x00\xFF\x04",
+
+ 0x10C831: b"\x08", # Castle Wall
+ 0x10C829: b"\x10",
+ 0x10C821: b"\x20",
+ 0xBFCA97: b"\x04",
+
+ # Villa
+ 0xBFC926: b"\xFF\x04",
+ 0xBFC93A: b"\x80",
+ 0xBFC93F: b"\x01",
+ 0xBFC943: b"\x40",
+ 0xBFC947: b"\x80",
+ 0x10C989: b"\x10",
+ 0x10C991: b"\x20",
+ 0x10C999: b"\x40",
+ 0x10CF77: b"\x80",
+
+ 0x10CA58: b"\x40\x00\xFF\x0E", # Tunnel
+ 0x10CA6B: b"\x80",
+ 0x10CA60: b"\x10\x00\xFF\x05",
+ 0x10CA70: b"\x20\x00\xFF\x05",
+ 0x10CA78: b"\x40\x00\xFF\x05",
+ 0x10CA80: b"\x80\x00\xFF\x05",
+
+ 0x10CBCA: b"\x02", # Castle Center
+ 0x10CC10: b"\x80",
+ 0x10CC5C: b"\x40",
+
+ 0x10CE86: b"\x01", # Duel Tower
+ 0x10CD43: b"\x02", # Tower of Execution
+ 0x10CE2E: b"\x20", # Tower of Science
+
+ 0x10CF8E: b"\x04", # Room of Clocks
+ 0x10CF96: b"\x08",
+
+ 0x10CECE: b"\x08", # Clock Tower
+ 0x10CED6: b"\x10",
+ 0x10CEE6: b"\x20",
+ 0x10CEDE: b"\x80",
+}
+
+rom_empty_breakables_flags = {
+ 0x10C74D: b"\x40\xFF\x05", # Forest of Silence
+ 0x10C765: b"\x20\xFF\x0E",
+ 0x10C774: b"\x08\x00\xFF\x0E",
+ 0x10C755: b"\x80\xFF\x05",
+ 0x10C784: b"\x01\x00\xFF\x0E",
+ 0x10C73C: b"\x02\x00\xFF\x0E",
+
+ 0x10C8D0: b"\x04\x00\xFF\x0E", # Villa foyer
+
+ 0x10CF9F: b"\x08", # Room of Clocks flags
+ 0x10CFA7: b"\x01",
+ 0xBFCB6F: b"\x04", # Room of Clocks candle property IDs
+ 0xBFCB73: b"\x05",
+}
+
+rom_axe_cross_lower_values = {
+ 0x6: [0x7C7F97, 0x07], # Forest
+ 0x8: [0x7C7FA6, 0xF9],
+
+ 0x30: [0x83A60A, 0x71], # Villa hallway
+ 0x27: [0x83A617, 0x26],
+ 0x2C: [0x83A624, 0x6E],
+
+ 0x16C: [0x850FE6, 0x07], # Villa maze
+
+ 0x10A: [0x8C44D3, 0x08], # CC factory floor
+ 0x109: [0x8C44E1, 0x08],
+
+ 0x74: [0x8DF77C, 0x07], # CC invention area
+ 0x60: [0x90FD37, 0x43],
+ 0x55: [0xBFCC2B, 0x43],
+ 0x65: [0x90FBA1, 0x51],
+ 0x64: [0x90FBAD, 0x50],
+ 0x61: [0x90FE56, 0x43]
+}
+
+rom_looping_music_fade_ins = {
+ 0x10: None,
+ 0x11: None,
+ 0x12: None,
+ 0x13: None,
+ 0x14: None,
+ 0x15: None,
+ 0x16: 0x17,
+ 0x18: 0x19,
+ 0x1A: 0x1B,
+ 0x21: 0x75,
+ 0x27: None,
+ 0x2E: 0x23,
+ 0x39: None,
+ 0x45: 0x63,
+ 0x56: None,
+ 0x57: 0x58,
+ 0x59: None,
+ 0x5A: None,
+ 0x5B: 0x5C,
+ 0x5D: None,
+ 0x5E: None,
+ 0x5F: None,
+ 0x60: 0x61,
+ 0x62: None,
+ 0x64: None,
+ 0x65: None,
+ 0x66: None,
+ 0x68: None,
+ 0x69: None,
+ 0x6D: 0x78,
+ 0x6E: None,
+ 0x6F: None,
+ 0x73: None,
+ 0x74: None,
+ 0x77: None,
+ 0x79: None
+}
+
+music_sfx_ids = [0x1C, 0x4B, 0x4C, 0x4D, 0x4E, 0x55, 0x6C, 0x76]
+
+renon_item_dialogue = {
+ 0x02: "More Sub-weapon uses!\n"
+ "Just what you need!",
+ 0x03: "Galamoth told me it's\n"
+ "a heart in other times.",
+ 0x04: "Who needs Warp Rooms\n"
+ "when you have these?",
+ 0x05: "I was told to safeguard\n"
+ "this, but I dunno why.",
+ 0x06: "Fresh off a Behemoth!\n"
+ "Those cows are weird.",
+ 0x07: "Preserved with special\n"
+ " wall-based methods.",
+ 0x08: "Don't tell Geneva\n"
+ "about this...",
+ 0x09: "If this existed in 1094,\n"
+ "that whip wouldn't...",
+ 0x0A: "For when some lizard\n"
+ "brain spits on your ego.",
+ 0x0C: "It'd be a shame if you\n"
+ "lost it immediately...",
+ 0x10C: "No consequences should\n"
+ "you perish with this!",
+ 0x0D: "Arthur was far better\n"
+ "with it than you!",
+ 0x0E: "Night Creatures handle\n"
+ "with care!",
+ 0x0F: "Some may call it a\n"
+ "\"Banshee Boomerang.\"",
+ 0x10: "No weapon triangle\n"
+ "advantages with this.",
+ 0x12: "It looks sus? Trust me,"
+ "my wares are genuine.",
+ 0x15: "This non-volatile kind\n"
+ "is safe to handle.",
+ 0x16: "If you can soul-wield,\n"
+ "they have a good one!",
+ 0x17: "Calls the morning sun\n"
+ "to vanquish the night.",
+ 0x18: "1 on-demand horrible\n"
+ "night. Devils love it!",
+ 0x1A: "Want to study here?\n"
+ "It will cost you.",
+ 0x1B: "\"Let them eat cake!\"\n"
+ "Said no princess ever.",
+ 0x1C: "Why do I suspect this\n"
+ "was a toilet room?",
+ 0x1D: "When you see Coller,\n"
+ "tell him I said hi!",
+ 0x1E: "Atomic number is 29\n"
+ "and weight is 63.546.",
+ 0x1F: "One torture per pay!\n"
+ "Who will it be?",
+ 0x20: "Being here feels like\n"
+ "time is slowing down.",
+ 0x21: "Only one thing beind\n"
+ "this. Do you dare?",
+ 0x22: "The key 2 Science!\n"
+ "Both halves of it!",
+ 0x23: "This warehouse can\n"
+ "be yours for a fee.",
+ 0x24: "Long road ahead if you\n"
+ "don't have the others.",
+ 0x25: "Will you get the curse\n"
+ "of eternal burning?",
+ 0x26: "What's beyond time?\n"
+ "Find out your",
+ 0x27: "Want to take out a\n"
+ "loan? By all means!",
+ 0x28: "The bag is green,\n"
+ "so it must be lucky!",
+ 0x29: "(Does this fool realize?)\n"
+ "Oh, sorry.",
+ "prog": "They will absolutely\n"
+ "need it in time!",
+ "useful": "Now, this would be\n"
+ "useful to send...",
+ "common": "Every last little bit\n"
+ "helps, right?",
+ "trap": "I'll teach this fool\n"
+ " a lesson for a price!",
+ "dlc coin": "1 coin out of... wha!?\n"
+ "You imp, why I oughta!"
+}
+
+
+def randomize_lighting(world: "CV64World") -> Dict[int, bytes]:
+ """Generates randomized data for the map lighting table."""
+ randomized_lighting = {}
+ for entry in range(67):
+ for sub_entry in range(19):
+ if sub_entry not in [3, 7, 11, 15] and entry != 4:
+ # The fourth entry in the lighting table affects the lighting on some item pickups; skip it
+ randomized_lighting[0x1091A0 + (entry * 28) + sub_entry] = bytes([world.random.randint(0, 255)])
+ return randomized_lighting
+
+
+def shuffle_sub_weapons(world: "CV64World") -> Dict[int, bytes]:
+ """Shuffles the sub-weapons amongst themselves."""
+ sub_weapon_dict = {offset: rom_sub_weapon_offsets[offset][0] for offset in rom_sub_weapon_offsets if
+ rom_sub_weapon_offsets[offset][1] in world.active_stage_exits}
+
+ # Remove the one 3HB sub-weapon in Tower of Execution if 3HBs are not shuffled.
+ if not world.options.multi_hit_breakables and 0x10CD65 in sub_weapon_dict:
+ del (sub_weapon_dict[0x10CD65])
+
+ sub_bytes = list(sub_weapon_dict.values())
+ world.random.shuffle(sub_bytes)
+ return dict(zip(sub_weapon_dict, sub_bytes))
+
+
+def randomize_music(world: "CV64World") -> Dict[int, bytes]:
+ """Generates randomized or disabled data for all the music in the game."""
+ music_array = bytearray(0x7A)
+ for number in music_sfx_ids:
+ music_array[number] = number
+ if world.options.background_music == BackgroundMusic.option_randomized:
+ looping_songs = []
+ non_looping_songs = []
+ fade_in_songs = {}
+ # Create shuffle-able lists of all the looping, non-looping, and fade-in track IDs
+ for i in range(0x10, len(music_array)):
+ if i not in rom_looping_music_fade_ins.keys() and i not in rom_looping_music_fade_ins.values() and \
+ i != 0x72: # Credits song is blacklisted
+ non_looping_songs.append(i)
+ elif i in rom_looping_music_fade_ins.keys():
+ looping_songs.append(i)
+ elif i in rom_looping_music_fade_ins.values():
+ fade_in_songs[i] = i
+ # Shuffle the looping songs
+ rando_looping_songs = looping_songs.copy()
+ world.random.shuffle(rando_looping_songs)
+ looping_songs = dict(zip(looping_songs, rando_looping_songs))
+ # Shuffle the non-looping songs
+ rando_non_looping_songs = non_looping_songs.copy()
+ world.random.shuffle(rando_non_looping_songs)
+ non_looping_songs = dict(zip(non_looping_songs, rando_non_looping_songs))
+ non_looping_songs[0x72] = 0x72
+ # Figure out the new fade-in songs if applicable
+ for vanilla_song in looping_songs:
+ if rom_looping_music_fade_ins[vanilla_song]:
+ if rom_looping_music_fade_ins[looping_songs[vanilla_song]]:
+ fade_in_songs[rom_looping_music_fade_ins[vanilla_song]] = rom_looping_music_fade_ins[
+ looping_songs[vanilla_song]]
+ else:
+ fade_in_songs[rom_looping_music_fade_ins[vanilla_song]] = looping_songs[vanilla_song]
+ # Build the new music array
+ for i in range(0x10, len(music_array)):
+ if i in looping_songs.keys():
+ music_array[i] = looping_songs[i]
+ elif i in non_looping_songs.keys():
+ music_array[i] = non_looping_songs[i]
+ else:
+ music_array[i] = fade_in_songs[i]
+ del (music_array[0x00: 0x10])
+
+ return {0xBFCD30: bytes(music_array)}
+
+
+def randomize_shop_prices(world: "CV64World") -> Dict[int, bytes]:
+ """Randomize the shop prices based on the minimum and maximum values chosen.
+ The minimum price will adjust if it's higher than the max."""
+ min_price = world.options.minimum_gold_price.value
+ max_price = world.options.maximum_gold_price.value
+
+ if min_price > max_price:
+ min_price = world.random.randint(0, max_price)
+ logging.warning(f"[{world.multiworld.player_name[world.player]}] The Minimum Gold Price "
+ f"({world.options.minimum_gold_price.value * 100}) is higher than the "
+ f"Maximum Gold Price ({max_price * 100}). Lowering the minimum to: {min_price * 100}")
+ world.options.minimum_gold_price.value = min_price
+
+ shop_price_list = [world.random.randint(min_price * 100, max_price * 100) for _ in range(7)]
+
+ # Convert the price list into a data dict.
+ price_dict = {}
+ for i in range(len(shop_price_list)):
+ price_dict[0x103D6C + (i * 12)] = int.to_bytes(shop_price_list[i], 4, "big")
+
+ return price_dict
+
+
+def get_countdown_numbers(options: CV64Options, active_locations: Iterable[Location]) -> Dict[int, bytes]:
+ """Figures out which Countdown numbers to increase for each Location after verifying the Item on the Location should
+ increase a number.
+
+ First, check the location's info to see if it has a countdown number override.
+ If not, then figure it out based on the parent region's stage's position in the vanilla stage order.
+ If the parent region is not part of any stage (as is the case for Renon's shop), skip the location entirely."""
+ countdown_list = [0 for _ in range(15)]
+ for loc in active_locations:
+ if loc.address is not None and (options.countdown == Countdown.option_all_locations or
+ (options.countdown == Countdown.option_majors
+ and loc.item.advancement)):
+
+ countdown_number = get_location_info(loc.name, "countdown")
+
+ if countdown_number is None:
+ stage = get_region_info(loc.parent_region.name, "stage")
+ if stage is not None:
+ countdown_number = vanilla_stage_order.index(stage)
+
+ if countdown_number is not None:
+ countdown_list[countdown_number] += 1
+
+ return {0xBFD818: bytes(countdown_list)}
+
+
+def get_location_data(world: "CV64World", active_locations: Iterable[Location]) \
+ -> Tuple[Dict[int, bytes], List[str], List[bytearray], List[List[Union[int, str, None]]]]:
+ """Gets ALL the item data to go into the ROM. Item data consists of two bytes: the first dictates the appearance of
+ the item, the second determines what the item actually is when picked up. All items from other worlds will be AP
+ items that do nothing when picked up other than set their flag, and their appearance will depend on whether it's
+ another CV64 player's item and, if so, what item it is in their game. Ice Traps can assume the form of any item that
+ is progression, non-progression, or either depending on the player's settings.
+
+ Appearance does not matter if it's one of the two NPC-given items (from either Vincent or Heinrich Meyer). For
+ Renon's shop items, a list containing the shop item names, descriptions, and colors will be returned alongside the
+ regular data."""
+
+ # Figure out the list of possible Ice Trap appearances to use based on the settings, first and foremost.
+ if world.options.ice_trap_appearance == IceTrapAppearance.option_major_only:
+ allowed_classifications = ["progression", "progression skip balancing"]
+ elif world.options.ice_trap_appearance == IceTrapAppearance.option_junk_only:
+ allowed_classifications = ["filler", "useful"]
+ else:
+ allowed_classifications = ["progression", "progression skip balancing", "filler", "useful"]
+
+ trap_appearances = []
+ for item in item_info:
+ if item_info[item]["default classification"] in allowed_classifications and item != "Ice Trap" and \
+ get_item_info(item, "code") is not None:
+ trap_appearances.append(item)
+
+ shop_name_list = []
+ shop_desc_list = []
+ shop_colors_list = []
+
+ location_bytes = {}
+
+ for loc in active_locations:
+ # If the Location is an event, skip it.
+ if loc.address is None:
+ continue
+
+ loc_type = get_location_info(loc.name, "type")
+
+ # Figure out the item ID bytes to put in each Location here. Write the item itself if either it's the player's
+ # very own, or it belongs to an Item Link that the player is a part of.
+ if loc.item.player == world.player:
+ if loc_type not in ["npc", "shop"] and get_item_info(loc.item.name, "pickup actor id") is not None:
+ location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "pickup actor id")
+ else:
+ location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "code") & 0xFF
+ else:
+ # Make the item the unused Wooden Stake - our multiworld item.
+ location_bytes[get_location_info(loc.name, "offset")] = 0x11
+
+ # Figure out the item's appearance. If it's a CV64 player's item, change the multiworld item's model to
+ # match what it is. Otherwise, change it to an Archipelago progress or not progress icon. The model "change"
+ # has to be applied to even local items because this is how the game knows to count it on the Countdown.
+ if loc.item.game == "Castlevania 64":
+ location_bytes[get_location_info(loc.name, "offset") - 1] = get_item_info(loc.item.name, "code")
+ elif loc.item.advancement:
+ location_bytes[get_location_info(loc.name, "offset") - 1] = 0x11 # Wooden Stakes are majors
+ else:
+ location_bytes[get_location_info(loc.name, "offset") - 1] = 0x12 # Roses are minors
+
+ # If it's a PermaUp, change the item's model to a big PowerUp no matter what.
+ if loc.item.game == "Castlevania 64" and loc.item.code == 0x10C + base_id:
+ location_bytes[get_location_info(loc.name, "offset") - 1] = 0x0B
+
+ # If it's an Ice Trap, change its model to one of the appearances we determined before.
+ # Unless it's an NPC item, in which case use the Ice Trap's regular ID so that it won't decrement the majors
+ # Countdown due to how I set up the NPC items to work.
+ if loc.item.game == "Castlevania 64" and loc.item.code == 0x12 + base_id:
+ if loc_type == "npc":
+ location_bytes[get_location_info(loc.name, "offset") - 1] = 0x12
+ else:
+ location_bytes[get_location_info(loc.name, "offset") - 1] = \
+ get_item_info(world.random.choice(trap_appearances), "code")
+ # If we chose a PermaUp as our trap appearance, change it to its actual in-game ID of 0x0B.
+ if location_bytes[get_location_info(loc.name, "offset") - 1] == 0x10C:
+ location_bytes[get_location_info(loc.name, "offset") - 1] = 0x0B
+
+ # Apply the invisibility variable depending on the "invisible items" setting.
+ if (world.options.invisible_items == InvisibleItems.option_vanilla and loc_type == "inv") or \
+ (world.options.invisible_items == InvisibleItems.option_hide_all and loc_type not in ["npc", "shop"]):
+ location_bytes[get_location_info(loc.name, "offset") - 1] += 0x80
+ elif world.options.invisible_items == InvisibleItems.option_chance and loc_type not in ["npc", "shop"]:
+ invisible = world.random.randint(0, 1)
+ if invisible:
+ location_bytes[get_location_info(loc.name, "offset") - 1] += 0x80
+
+ # If it's an Axe or Cross in a higher freestanding location, lower it into grab range.
+ # KCEK made these spawn 3.2 units higher for some reason.
+ if loc.address & 0xFFF in rom_axe_cross_lower_values and loc.item.code & 0xFF in [0x0F, 0x10]:
+ location_bytes[rom_axe_cross_lower_values[loc.address & 0xFFF][0]] = \
+ rom_axe_cross_lower_values[loc.address & 0xFFF][1]
+
+ # Figure out the list of shop names, descriptions, and text colors here.
+ if loc.parent_region.name != rname.renon:
+ continue
+
+ shop_name = loc.item.name
+ if len(shop_name) > 18:
+ shop_name = shop_name[0:18]
+ shop_name_list.append(shop_name)
+
+ if loc.item.player == world.player:
+ shop_desc_list.append([get_item_info(loc.item.name, "code"), None])
+ elif loc.item.game == "Castlevania 64":
+ shop_desc_list.append([get_item_info(loc.item.name, "code"),
+ world.multiworld.get_player_name(loc.item.player)])
+ else:
+ if loc.item.game == "DLCQuest" and loc.item.name in ["DLC Quest: Coin Bundle",
+ "Live Freemium or Die: Coin Bundle"]:
+ if getattr(world.multiworld.worlds[loc.item.player].options, "coinbundlequantity") == 1:
+ shop_desc_list.append(["dlc coin", world.multiworld.get_player_name(loc.item.player)])
+ shop_colors_list.append(get_item_text_color(loc))
+ continue
+
+ if loc.item.advancement:
+ shop_desc_list.append(["prog", world.multiworld.get_player_name(loc.item.player)])
+ elif loc.item.classification == ItemClassification.useful:
+ shop_desc_list.append(["useful", world.multiworld.get_player_name(loc.item.player)])
+ elif loc.item.classification == ItemClassification.trap:
+ shop_desc_list.append(["trap", world.multiworld.get_player_name(loc.item.player)])
+ else:
+ shop_desc_list.append(["common", world.multiworld.get_player_name(loc.item.player)])
+
+ shop_colors_list.append(get_item_text_color(loc))
+
+ return {offset: int.to_bytes(byte, 1, "big") for offset, byte in location_bytes.items()}, shop_name_list,\
+ shop_colors_list, shop_desc_list
+
+
+def get_loading_zone_bytes(options: CV64Options, starting_stage: str,
+ active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> Dict[int, bytes]:
+ """Figure out all the bytes for loading zones and map transitions based on which stages are where in the exit data.
+ The same data was used earlier in figuring out the logic. Map transitions consist of two major components: which map
+ to send the player to, and which spot within the map to spawn the player at."""
+
+ # Write the byte for the starting stage to send the player to after the intro narration.
+ loading_zone_bytes = {0xB73308: get_stage_info(starting_stage, "start map id")}
+
+ for stage in active_stage_exits:
+
+ # Start loading zones
+ # If the start zone is the start of the line, have it simply refresh the map.
+ if active_stage_exits[stage]["prev"] == "Menu":
+ loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = b"\xFF"
+ loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = b"\x00"
+ elif active_stage_exits[stage]["prev"]:
+ loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = \
+ get_stage_info(active_stage_exits[stage]["prev"], "end map id")
+ loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = \
+ get_stage_info(active_stage_exits[stage]["prev"], "end spawn id")
+
+ # Change CC's end-spawn ID to put you at Carrie's exit if appropriate
+ if active_stage_exits[stage]["prev"] == rname.castle_center:
+ if options.character_stages == CharacterStages.option_carrie_only or \
+ active_stage_exits[rname.castle_center]["alt"] == stage:
+ loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = b"\x03"
+
+ # End loading zones
+ if active_stage_exits[stage]["next"]:
+ loading_zone_bytes[get_stage_info(stage, "endzone map offset")] = \
+ get_stage_info(active_stage_exits[stage]["next"], "start map id")
+ loading_zone_bytes[get_stage_info(stage, "endzone spawn offset")] = \
+ get_stage_info(active_stage_exits[stage]["next"], "start spawn id")
+
+ # Alternate end loading zones
+ if active_stage_exits[stage]["alt"]:
+ loading_zone_bytes[get_stage_info(stage, "altzone map offset")] = \
+ get_stage_info(active_stage_exits[stage]["alt"], "start map id")
+ loading_zone_bytes[get_stage_info(stage, "altzone spawn offset")] = \
+ get_stage_info(active_stage_exits[stage]["alt"], "start spawn id")
+
+ return loading_zone_bytes
+
+
+def get_start_inventory_data(player: int, options: CV64Options, precollected_items: List[Item]) -> Dict[int, bytes]:
+ """Calculate and return the starting inventory values. Not every Item goes into the menu inventory, so everything
+ has to be handled appropriately."""
+ start_inventory_data = {}
+
+ inventory_items_array = [0 for _ in range(35)]
+ total_money = 0
+ total_jewels = 0
+ total_powerups = 0
+ total_ice_traps = 0
+
+ items_max = 10
+
+ # Raise the items max if Increase Item Limit is enabled.
+ if options.increase_item_limit:
+ items_max = 99
+
+ for item in precollected_items:
+ if item.player != player:
+ continue
+
+ inventory_offset = get_item_info(item.name, "inventory offset")
+ sub_equip_id = get_item_info(item.name, "sub equip id")
+ # Starting inventory items
+ if inventory_offset is not None:
+ inventory_items_array[inventory_offset] += 1
+ if inventory_items_array[inventory_offset] > items_max and "Special" not in item.name:
+ inventory_items_array[inventory_offset] = items_max
+ if item.name == iname.permaup:
+ if inventory_items_array[inventory_offset] > 2:
+ inventory_items_array[inventory_offset] = 2
+ # Starting sub-weapon
+ elif sub_equip_id is not None:
+ start_inventory_data[0xBFD883] = bytes(sub_equip_id)
+ # Starting PowerUps
+ elif item.name == iname.powerup:
+ total_powerups += 1
+ # Can't have more than 2 PowerUps.
+ if total_powerups > 2:
+ total_powerups = 2
+ # Starting Gold
+ elif "GOLD" in item.name:
+ total_money += int(item.name[0:4])
+ # Money cannot be higher than 99999.
+ if total_money > 99999:
+ total_money = 99999
+ # Starting Jewels
+ elif "jewel" in item.name:
+ if "L" in item.name:
+ total_jewels += 10
+ else:
+ total_jewels += 5
+ # Jewels cannot be higher than 99.
+ if total_jewels > 99:
+ total_jewels = 99
+ # Starting Ice Traps
+ else:
+ total_ice_traps += 1
+ # Ice Traps cannot be higher than 255.
+ if total_ice_traps > 0xFF:
+ total_ice_traps = 0xFF
+
+ # Convert the jewels into data.
+ start_inventory_data[0xBFD867] = bytes([total_jewels])
+
+ # Convert the Ice Traps into data.
+ start_inventory_data[0xBFD88B] = bytes([total_ice_traps])
+
+ # Convert the inventory items into data.
+ start_inventory_data[0xBFE518] = bytes(inventory_items_array)
+
+ # Convert the starting money into data.
+ start_inventory_data[0xBFE514] = int.to_bytes(total_money, 4, "big")
+
+ return start_inventory_data
+
+
+def get_item_text_color(loc: Location) -> bytearray:
+ if loc.item.advancement:
+ return bytearray([0xA2, 0x0C])
+ elif loc.item.classification == ItemClassification.useful:
+ return bytearray([0xA2, 0x0A])
+ elif loc.item.classification == ItemClassification.trap:
+ return bytearray([0xA2, 0x0B])
+ else:
+ return bytearray([0xA2, 0x02])
diff --git a/worlds/cv64/client.py b/worlds/cv64/client.py
new file mode 100644
index 000000000000..2430cc5ffc67
--- /dev/null
+++ b/worlds/cv64/client.py
@@ -0,0 +1,207 @@
+from typing import TYPE_CHECKING, Set
+from .locations import base_id
+from .text import cv64_text_wrap, cv64_string_to_bytearray
+
+from NetUtils import ClientStatus
+import worlds._bizhawk as bizhawk
+import base64
+from worlds._bizhawk.client import BizHawkClient
+
+if TYPE_CHECKING:
+ from worlds._bizhawk.context import BizHawkClientContext
+
+
+class Castlevania64Client(BizHawkClient):
+ game = "Castlevania 64"
+ system = "N64"
+ patch_suffix = ".apcv64"
+ self_induced_death = False
+ received_deathlinks = 0
+ death_causes = []
+ currently_shopping = False
+ local_checked_locations: Set[int]
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.local_checked_locations = set()
+
+ async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
+ from CommonClient import logger
+
+ try:
+ # Check ROM name/patch version
+ game_names = await bizhawk.read(ctx.bizhawk_ctx, [(0x20, 0x14, "ROM"), (0xBFBFD0, 12, "ROM")])
+ if game_names[0].decode("ascii") != "CASTLEVANIA ":
+ return False
+ if game_names[1] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00':
+ logger.info("ERROR: You appear to be running an unpatched version of Castlevania 64. "
+ "You need to generate a patch file and use it to create a patched ROM.")
+ return False
+ if game_names[1].decode("ascii") != "ARCHIPELAGO1":
+ logger.info("ERROR: The patch file used to create this ROM is not compatible with "
+ "this client. Double check your client version against the version being "
+ "used by the generator.")
+ return False
+ except UnicodeDecodeError:
+ return False
+ except bizhawk.RequestFailedError:
+ return False # Should verify on the next pass
+
+ ctx.game = self.game
+ ctx.items_handling = 0b001
+ ctx.want_slot_data = False
+ ctx.watcher_timeout = 0.125
+ return True
+
+ async def set_auth(self, ctx: "BizHawkClientContext") -> None:
+ auth_raw = (await bizhawk.read(ctx.bizhawk_ctx, [(0xBFBFE0, 16, "ROM")]))[0]
+ ctx.auth = base64.b64encode(auth_raw).decode("utf-8")
+
+ def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None:
+ if cmd != "Bounced":
+ return
+ if "tags" not in args:
+ return
+ if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
+ self.received_deathlinks += 1
+ if "cause" in args["data"]:
+ cause = args["data"]["cause"]
+ if len(cause) > 88:
+ cause = cause[0x00:0x89]
+ else:
+ cause = f"{args['data']['source']} killed you!"
+ self.death_causes.append(cause)
+
+ async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
+
+ try:
+ read_state = await bizhawk.read(ctx.bizhawk_ctx, [(0x342084, 4, "RDRAM"),
+ (0x389BDE, 6, "RDRAM"),
+ (0x389BE4, 224, "RDRAM"),
+ (0x389EFB, 1, "RDRAM"),
+ (0x389EEF, 1, "RDRAM"),
+ (0xBFBFDE, 2, "ROM")])
+
+ game_state = int.from_bytes(read_state[0], "big")
+ save_struct = read_state[2]
+ written_deathlinks = int.from_bytes(bytearray(read_state[1][4:6]), "big")
+ deathlink_induced_death = int.from_bytes(bytearray(read_state[1][0:1]), "big")
+ cutscene_value = int.from_bytes(read_state[3], "big")
+ current_menu = int.from_bytes(read_state[4], "big")
+ num_received_items = int.from_bytes(bytearray(save_struct[0xDA:0xDC]), "big")
+ rom_flags = int.from_bytes(read_state[5], "big")
+
+ # Make sure we are in the Gameplay or Credits states before detecting sent locations and/or DeathLinks.
+ # If we are in any other state, such as the Game Over state, set self_induced_death to false, so we can once
+ # again send a DeathLink once we are back in the Gameplay state.
+ if game_state not in [0x00000002, 0x0000000B]:
+ self.self_induced_death = False
+ return
+
+ # Enable DeathLink if the bit for it is set in our ROM flags.
+ if "DeathLink" not in ctx.tags and rom_flags & 0x0100:
+ await ctx.update_death_link(True)
+
+ # Scout the Renon shop locations if the shopsanity flag is written in the ROM.
+ if rom_flags & 0x0001 and ctx.locations_info == {}:
+ await ctx.send_msgs([{
+ "cmd": "LocationScouts",
+ "locations": [base_id + i for i in range(0x1C8, 0x1CF)],
+ "create_as_hint": 0
+ }])
+
+ # Send a DeathLink if we died on our own independently of receiving another one.
+ if "DeathLink" in ctx.tags and save_struct[0xA4] & 0x80 and not self.self_induced_death and not \
+ deathlink_induced_death:
+ self.self_induced_death = True
+ if save_struct[0xA4] & 0x08:
+ # Special death message for dying while having the Vamp status.
+ await ctx.send_death(f"{ctx.player_names[ctx.slot]} became a vampire and drank your blood!")
+ else:
+ await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished. Dracula has won!")
+
+ # Write any DeathLinks received along with the corresponding death cause starting with the oldest.
+ # To minimize Bizhawk Write jank, the DeathLink write will be prioritized over the item received one.
+ if self.received_deathlinks and not self.self_induced_death and not written_deathlinks:
+ death_text, num_lines = cv64_text_wrap(self.death_causes[0], 96)
+ await bizhawk.write(ctx.bizhawk_ctx, [(0x389BE3, [0x01], "RDRAM"),
+ (0x389BDF, [0x11], "RDRAM"),
+ (0x18BF98, bytearray([0xA2, 0x0B]) +
+ cv64_string_to_bytearray(death_text, False), "RDRAM"),
+ (0x18C097, [num_lines], "RDRAM")])
+ self.received_deathlinks -= 1
+ del self.death_causes[0]
+ else:
+ # If the game hasn't received all items yet, the received item struct doesn't contain an item, the
+ # current number of received items still matches what we read before, and there are no open text boxes,
+ # then fill it with the next item and write the "item from player" text in its buffer. The game will
+ # increment the number of received items on its own.
+ if num_received_items < len(ctx.items_received):
+ next_item = ctx.items_received[num_received_items]
+ if next_item.flags & 0b001:
+ text_color = bytearray([0xA2, 0x0C])
+ elif next_item.flags & 0b010:
+ text_color = bytearray([0xA2, 0x0A])
+ elif next_item.flags & 0b100:
+ text_color = bytearray([0xA2, 0x0B])
+ else:
+ text_color = bytearray([0xA2, 0x02])
+ received_text, num_lines = cv64_text_wrap(f"{ctx.item_names.lookup_in_game(next_item.item)}\n"
+ f"from {ctx.player_names[next_item.player]}", 96)
+ await bizhawk.guarded_write(ctx.bizhawk_ctx,
+ [(0x389BE1, [next_item.item & 0xFF], "RDRAM"),
+ (0x18C0A8, text_color + cv64_string_to_bytearray(received_text, False),
+ "RDRAM"),
+ (0x18C1A7, [num_lines], "RDRAM")],
+ [(0x389BE1, [0x00], "RDRAM"), # Remote item reward buffer
+ (0x389CBE, save_struct[0xDA:0xDC], "RDRAM"), # Received items
+ (0x342891, [0x02], "RDRAM")]) # Textbox state
+
+ flag_bytes = bytearray(save_struct[0x00:0x44]) + bytearray(save_struct[0x90:0x9F])
+ locs_to_send = set()
+
+ # Check for set location flags.
+ for byte_i, byte in enumerate(flag_bytes):
+ for i in range(8):
+ and_value = 0x80 >> i
+ if byte & and_value != 0:
+ flag_id = byte_i * 8 + i
+
+ location_id = flag_id + base_id
+ if location_id in ctx.server_locations:
+ locs_to_send.add(location_id)
+
+ # Send locations if there are any to send.
+ if locs_to_send != self.local_checked_locations:
+ self.local_checked_locations = locs_to_send
+
+ if locs_to_send is not None:
+ await ctx.send_msgs([{
+ "cmd": "LocationChecks",
+ "locations": list(locs_to_send)
+ }])
+
+ # Check the menu value to see if we are in Renon's shop, and set currently_shopping to True if we are.
+ if current_menu == 0xA:
+ self.currently_shopping = True
+
+ # If we are currently shopping, and the current menu value is 0 (meaning we just left the shop), hint the
+ # un-bought shop locations that have progression.
+ if current_menu == 0 and self.currently_shopping:
+ await ctx.send_msgs([{
+ "cmd": "LocationScouts",
+ "locations": [loc for loc, n_item in ctx.locations_info.items() if n_item.flags & 0b001],
+ "create_as_hint": 2
+ }])
+ self.currently_shopping = False
+
+ # Send game clear if we're in either any ending cutscene or the credits state.
+ if not ctx.finished_game and (0x26 <= int(cutscene_value) <= 0x2E or game_state == 0x0000000B):
+ await ctx.send_msgs([{
+ "cmd": "StatusUpdate",
+ "status": ClientStatus.CLIENT_GOAL
+ }])
+
+ except bizhawk.RequestFailedError:
+ # Exit handler and return to main loop to reconnect.
+ pass
diff --git a/worlds/cv64/data/APLogo-LICENSE.txt b/worlds/cv64/data/APLogo-LICENSE.txt
new file mode 100644
index 000000000000..69d1e3ecd137
--- /dev/null
+++ b/worlds/cv64/data/APLogo-LICENSE.txt
@@ -0,0 +1,3 @@
+The Archipelago Logo is © 2022 by Krista Corkos and Christopher Wilson is licensed under Attribution-NonCommercial 4.0 International. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/
+
+Logo modified by Liquid Cat to fit artstyle and uses within the mod.
diff --git a/worlds/cv64/data/ap_icons.bin b/worlds/cv64/data/ap_icons.bin
new file mode 100644
index 000000000000..0a4129ac409c
Binary files /dev/null and b/worlds/cv64/data/ap_icons.bin differ
diff --git a/worlds/cv64/data/ename.py b/worlds/cv64/data/ename.py
new file mode 100644
index 000000000000..26cd7151de89
--- /dev/null
+++ b/worlds/cv64/data/ename.py
@@ -0,0 +1,71 @@
+forest_dbridge_gate = "Descending bridge gate"
+forest_werewolf_gate = "Werewolf gate"
+forest_end = "Dracula's drawbridge"
+
+cw_portcullis_c = "Central portcullis"
+cw_lt_skip = "Do Left Tower Skip"
+cw_lt_door = "Left Tower door"
+cw_end = "End portcullis"
+
+villa_dog_gates = "Front dog gates"
+villa_snipe_dogs = "Orb snipe the dogs"
+villa_to_storeroom = "To Storeroom door"
+villa_to_archives = "To Archives door"
+villa_renon = "Villa contract"
+villa_to_maze = "To maze gate"
+villa_from_storeroom = "From Storeroom door"
+villa_from_maze = "From maze gate"
+villa_servant_door = "Servants' door"
+villa_copper_door = "Copper door"
+villa_copper_skip = "Get Copper Skip"
+villa_bridge_door = "From bridge door"
+villa_end_r = "Villa Reinhardt (daytime) exit"
+villa_end_c = "Villa Carrie (nighttime) exit"
+
+tunnel_start_renon = "Tunnel start contract"
+tunnel_gondolas = "Gondola ride"
+tunnel_end_renon = "Tunnel end contract"
+tunnel_end = "End Tunnel door"
+
+uw_final_waterfall = "Final waterfall"
+uw_waterfall_skip = "Do Waterfall Skip"
+uw_renon = "Underground Waterway contract"
+uw_end = "End Waterway door"
+
+cc_tc_door = "Torture Chamber door"
+cc_renon = "Castle Center contract"
+cc_lower_wall = "Lower sealed cracked wall"
+cc_upper_wall = "Upper cracked wall"
+cc_elevator = "Activate crystal and ride elevator"
+cc_exit_r = "Castle Center Reinhardt (Medusa Head) exit"
+cc_exit_c = "Castle Center Carrie (Ghost) exit"
+
+dt_start = "Duel Tower start passage"
+dt_end = "Duel Tower end passage"
+
+toe_start = "Tower of Execution start passage"
+toe_gate = "Execution gate"
+toe_gate_skip = "Just jump past the gate from above, bro!"
+toe_end = "Tower of Execution end staircase"
+
+tosci_start = "Tower of Science start passage"
+tosci_key1_door = "Science Door 1"
+tosci_to_key2_door = "To Science Door 2"
+tosci_from_key2_door = "From Science Door 2"
+tosci_key3_door = "Science Door 3"
+tosci_end = "Tower of Science end passage"
+
+tosor_start = "Tower of Sorcery start passage"
+tosor_end = "Tower of Sorcery end passage"
+
+roc_gate = "Defeat boss gate"
+
+ct_to_door1 = "To Clocktower Door 1"
+ct_from_door1 = "From Clocktower Door 1"
+ct_to_door2 = "To Clocktower Door 2"
+ct_from_door2 = "From Clocktower Door 2"
+ct_renon = "Clock Tower contract"
+ct_door_3 = "Clocktower Door 3"
+
+ck_slope_jump = "Slope Jump to boss tower"
+ck_drac_door = "Dracula's door"
diff --git a/worlds/cv64/data/iname.py b/worlds/cv64/data/iname.py
new file mode 100644
index 000000000000..9b9e225ca5c7
--- /dev/null
+++ b/worlds/cv64/data/iname.py
@@ -0,0 +1,49 @@
+# Items
+white_jewel = "White jewel"
+special_one = "Special1"
+special_two = "Special2"
+red_jewel_s = "Red jewel(S)"
+red_jewel_l = "Red jewel(L)"
+roast_chicken = "Roast chicken"
+roast_beef = "Roast beef"
+healing_kit = "Healing kit"
+purifying = "Purifying"
+cure_ampoule = "Cure ampoule"
+pot_pourri = "pot-pourri"
+powerup = "PowerUp"
+permaup = "PermaUp"
+holy_water = "Holy water"
+cross = "Cross"
+axe = "Axe"
+knife = "Knife"
+wooden_stake = "Wooden stake"
+rose = "Rose"
+ice_trap = "Ice Trap"
+the_contract = "The contract"
+engagement_ring = "engagement ring"
+magical_nitro = "Magical Nitro"
+mandragora = "Mandragora"
+sun_card = "Sun card"
+moon_card = "Moon card"
+incandescent_gaze = "Incandescent gaze"
+five_hundred_gold = "500 GOLD"
+three_hundred_gold = "300 GOLD"
+one_hundred_gold = "100 GOLD"
+archives_key = "Archives Key"
+left_tower_key = "Left Tower Key"
+storeroom_key = "Storeroom Key"
+garden_key = "Garden Key"
+copper_key = "Copper Key"
+chamber_key = "Chamber Key"
+execution_key = "Execution Key"
+science_key1 = "Science Key1"
+science_key2 = "Science Key2"
+science_key3 = "Science Key3"
+clocktower_key1 = "Clocktower Key1"
+clocktower_key2 = "Clocktower Key2"
+clocktower_key3 = "Clocktower Key3"
+
+trophy = "Trophy"
+crystal = "Crystal"
+
+victory = "The Count Downed"
diff --git a/worlds/cv64/data/lname.py b/worlds/cv64/data/lname.py
new file mode 100644
index 000000000000..09db86b38083
--- /dev/null
+++ b/worlds/cv64/data/lname.py
@@ -0,0 +1,479 @@
+# Forest of Silence main locations
+forest_pillars_right = "Forest of Silence: Grab practice pillars - Right"
+forest_pillars_top = "Forest of Silence: Grab practice pillars - Top"
+forest_king_skeleton = "Forest of Silence: King Skeleton's bridge"
+forest_lgaz_in = "Forest of Silence: Moon gazebo inside"
+forest_lgaz_top = "Forest of Silence: Moon gazebo roof"
+forest_hgaz_in = "Forest of Silence: Sun gazebo inside"
+forest_hgaz_top = "Forest of Silence: Sun gazebo roof"
+forest_weretiger_sw = "Forest of Silence: Were-tiger switch"
+forest_weretiger_gate = "Forest of Silence: Dirge maiden gate"
+forest_dirge_tomb_u = "Forest of Silence: Dirge maiden crypt - Upper"
+forest_dirge_plaque = "Forest of Silence: Dirge maiden pedestal plaque"
+forest_corpse_save = "Forest of Silence: Tri-corpse save junction"
+forest_dbridge_wall = "Forest of Silence: Descending bridge wall side"
+forest_dbridge_sw = "Forest of Silence: Descending bridge switch side"
+forest_dbridge_gate_r = "Forest of Silence: Tri-corpse gate - Right"
+forest_dbridge_tomb_uf = "Forest of Silence: Three-crypt plaza main path crypt - Upper-front"
+forest_bface_tomb_lf = "Forest of Silence: Three-crypt plaza back-facing crypt - Lower-front"
+forest_bface_tomb_u = "Forest of Silence: Three-crypt plaza back-facing crypt - Upper"
+forest_ibridge = "Forest of Silence: Invisible bridge platform"
+forest_werewolf_tomb_r = "Forest of Silence: Werewolf crypt - Right"
+forest_werewolf_plaque = "Forest of Silence: Werewolf statue plaque"
+forest_werewolf_tree = "Forest of Silence: Werewolf path near tree"
+forest_final_sw = "Forest of Silence: Three-crypt plaza switch"
+
+# Forest of Silence empty breakables
+forest_dirge_tomb_l = "Forest of Silence: Dirge maiden crypt - Lower"
+forest_dbridge_tomb_l = "Forest of Silence: Three-crypt plaza main path crypt - Lower"
+forest_dbridge_tomb_ur = "Forest of Silence: Three-crypt plaza main path crypt - Upper-rear"
+forest_bface_tomb_lr = "Forest of Silence: Three-crypt plaza back-facing crypt - Lower-rear"
+forest_werewolf_tomb_lf = "Forest of Silence: Werewolf crypt - Left-front"
+forest_werewolf_tomb_lr = "Forest of Silence: Werewolf crypt - Left-rear"
+
+# Forest of Silence 3-hit breakables
+forest_dirge_rock1 = "Forest of Silence: Dirge maiden rock - Item 1"
+forest_dirge_rock2 = "Forest of Silence: Dirge maiden rock - Item 2"
+forest_dirge_rock3 = "Forest of Silence: Dirge maiden rock - Item 3"
+forest_dirge_rock4 = "Forest of Silence: Dirge maiden rock - Item 4"
+forest_dirge_rock5 = "Forest of Silence: Dirge maiden rock - Item 5"
+forest_bridge_rock1 = "Forest of Silence: Bat archway rock - Item 1"
+forest_bridge_rock2 = "Forest of Silence: Bat archway rock - Item 2"
+forest_bridge_rock3 = "Forest of Silence: Bat archway rock - Item 3"
+forest_bridge_rock4 = "Forest of Silence: Bat archway rock - Item 4"
+
+# Forest of Silence sub-weapons
+forest_pillars_left = "Forest of Silence: Grab practice pillars - Left"
+forest_dirge_ped = "Forest of Silence: Dirge maiden pedestal"
+forest_dbridge_gate_l = "Forest of Silence: Tri-corpse gate - Left"
+forest_werewolf_island = "Forest of Silence: Werewolf path island switch platforms"
+
+
+# Castle Wall main locations
+cw_ground_middle = "Castle Wall: Ground gatehouse - Middle"
+cw_rrampart = "Castle Wall: Central rampart near right tower"
+cw_lrampart = "Castle Wall: Central rampart near left tower"
+cw_dragon_sw = "Castle Wall: White Dragons switch door"
+cw_drac_sw = "Castle Wall: Dracula cutscene switch door"
+cw_shelf_visible = "Castle Wall: Sandbag shelf - Visible"
+cw_shelf_sandbags = "Castle Wall: Sandbag shelf - Invisible"
+
+# Castle Wall towers main locations
+cwr_bottom = "Castle Wall: Above bottom right tower door"
+cwl_bottom = "Castle Wall: Above bottom left tower door"
+cwl_bridge = "Castle Wall: Left tower child ledge"
+
+# Castle Wall 3-hit breakables
+cw_save_slab1 = "Castle Wall: Central rampart savepoint slab - Item 1"
+cw_save_slab2 = "Castle Wall: Central rampart savepoint slab - Item 2"
+cw_save_slab3 = "Castle Wall: Central rampart savepoint slab - Item 3"
+cw_save_slab4 = "Castle Wall: Central rampart savepoint slab - Item 4"
+cw_save_slab5 = "Castle Wall: Central rampart savepoint slab - Item 5"
+cw_drac_slab1 = "Castle Wall: Dracula cutscene switch slab - Item 1"
+cw_drac_slab2 = "Castle Wall: Dracula cutscene switch slab - Item 2"
+cw_drac_slab3 = "Castle Wall: Dracula cutscene switch slab - Item 3"
+cw_drac_slab4 = "Castle Wall: Dracula cutscene switch slab - Item 4"
+cw_drac_slab5 = "Castle Wall: Dracula cutscene switch slab - Item 5"
+
+# Castle Wall sub-weapons
+cw_ground_left = "Castle Wall: Ground gatehouse - Left"
+cw_ground_right = "Castle Wall: Ground gatehouse - Right"
+cw_shelf_torch = "Castle Wall: Sandbag shelf floor torch"
+cw_pillar = "Castle Wall: Central rampart broken pillar"
+
+
+# Villa front yard main locations
+villafy_outer_gate_l = "Villa: Outer front gate - Left"
+villafy_outer_gate_r = "Villa: Outer front gate - Right"
+villafy_inner_gate = "Villa: Inner front gate dog food"
+villafy_dog_platform = "Villa: Outer front gate platform"
+villafy_gate_marker = "Villa: Front yard cross grave near gates"
+villafy_villa_marker = "Villa: Front yard cross grave near porch"
+villafy_tombstone = "Villa: Front yard visitor's tombstone"
+villafy_fountain_fl = "Villa: Midnight fountain - Front-left"
+villafy_fountain_fr = "Villa: Midnight fountain - Front-right"
+villafy_fountain_ml = "Villa: Midnight fountain - Middle-left"
+villafy_fountain_mr = "Villa: Midnight fountain - Middle-right"
+villafy_fountain_rl = "Villa: Midnight fountain - Rear-left"
+villafy_fountain_rr = "Villa: Midnight fountain - Rear-right"
+
+# Villa foyer main locations
+villafo_sofa = "Villa: Foyer sofa"
+villafo_pot_r = "Villa: Foyer upper-right pot"
+villafo_pot_l = "Villa: Foyer upper-left pot"
+villafo_rear_r = "Villa: Foyer lower level - Rear-right"
+villafo_rear_l = "Villa: Foyer lower level - Rear-left"
+villafo_mid_l = "Villa: Foyer lower level - Middle-left"
+villafo_front_r = "Villa: Foyer lower level - Front-right"
+villafo_front_l = "Villa: Foyer lower level - Front-left"
+villafo_serv_ent = "Villa: Servants' entrance"
+
+# Villa empty breakables
+villafo_mid_r = "Villa: Foyer lower level - Middle-right"
+
+# Villa 3-hit breakables
+villafo_chandelier1 = "Villa: Foyer chandelier - Item 1"
+villafo_chandelier2 = "Villa: Foyer chandelier - Item 2"
+villafo_chandelier3 = "Villa: Foyer chandelier - Item 3"
+villafo_chandelier4 = "Villa: Foyer chandelier - Item 4"
+villafo_chandelier5 = "Villa: Foyer chandelier - Item 5"
+
+# Villa living area main locations
+villala_hallway_stairs = "Villa: Rose garden staircase bottom"
+villala_bedroom_chairs = "Villa: Bedroom near chairs"
+villala_bedroom_bed = "Villa: Bedroom near bed"
+villala_vincent = "Villa: Vincent"
+villala_slivingroom_table = "Villa: Mary's room table"
+villala_storeroom_l = "Villa: Storeroom - Left"
+villala_storeroom_r = "Villa: Storeroom - Right"
+villala_storeroom_s = "Villa: Storeroom statue"
+villala_diningroom_roses = "Villa: Dining room rose vase"
+villala_archives_table = "Villa: Archives table"
+villala_archives_rear = "Villa: Archives rear corner"
+villala_llivingroom_lion = "Villa: Living room lion head"
+villala_llivingroom_pot_r = "Villa: Living room - Right pot"
+villala_llivingroom_pot_l = "Villa: Living room - Left pot"
+villala_llivingroom_light = "Villa: Living room ceiling light"
+villala_llivingroom_painting = "Villa: Living room clawed painting"
+villala_exit_knight = "Villa: Maze garden exit knight"
+
+# Villa maze main locations
+villam_malus_torch = "Villa: Front maze garden - Malus start torch"
+villam_malus_bush = "Villa: Front maze garden - Malus's hiding bush"
+villam_frankieturf_r = "Villa: Front maze garden - Frankie's right dead-end"
+villam_frankieturf_l = "Villa: Front maze garden - Frankie's left dead-end"
+villam_frankieturf_ru = "Villa: Front maze garden - Frankie's right dead-end urn"
+villam_fgarden_f = "Villa: Rear maze garden - Iron Thorn Fenced area - Front"
+villam_fgarden_mf = "Villa: Rear maze garden - Iron Thorn Fenced area - Mid-front"
+villam_fgarden_mr = "Villa: Rear maze garden - Iron Thorn Fenced area - Mid-rear"
+villam_fgarden_r = "Villa: Rear maze garden - Iron Thorn Fenced area - Rear"
+villam_rplatform_de = "Villa: Rear maze garden - Viewing platform dead-end"
+villam_exit_de = "Villa: Rear maze garden - Past-exit dead-end"
+villam_serv_path = "Villa: Servants' path small alcove"
+villam_crypt_ent = "Villa: Crypt entrance"
+villam_crypt_upstream = "Villa: Crypt bridge upstream"
+
+# Villa crypt main locations
+villac_ent_l = "Villa: Crypt - Left from entrance"
+villac_ent_r = "Villa: Crypt - Right from entrance"
+villac_wall_l = "Villa: Crypt - Left wall"
+villac_wall_r = "Villa: Crypt - Right wall"
+villac_coffin_r = "Villa: Crypt - Right of coffin"
+
+# Villa sub-weapons
+villala_hallway_l = "Villa: Hallway near rose garden stairs - Left"
+villala_hallway_r = "Villa: Hallway near rose garden stairs - Right"
+villala_slivingroom_mirror = "Villa: Mary's room corner"
+villala_archives_entrance = "Villa: Archives near entrance"
+villam_fplatform = "Villa: Front maze garden - Viewing platform"
+villam_rplatform = "Villa: Rear maze garden - Viewing platform"
+villac_coffin_l = "Villa: Crypt - Left of coffin"
+
+
+# Tunnel main locations
+tunnel_landing = "Tunnel: Landing point"
+tunnel_landing_rc = "Tunnel: Landing point rock crusher"
+tunnel_stone_alcove_l = "Tunnel: Stepping stone alcove - Left"
+tunnel_twin_arrows = "Tunnel: Twin arrow signs"
+tunnel_lonesome_bucket = "Tunnel: Near lonesome bucket"
+tunnel_lbucket_quag = "Tunnel: Lonesome bucket poison pit"
+tunnel_lbucket_albert = "Tunnel: Lonesome bucket-Albert junction"
+tunnel_albert_camp = "Tunnel: Albert's campsite"
+tunnel_albert_quag = "Tunnel: Albert's poison pit"
+tunnel_gondola_rc_sdoor_r = "Tunnel: Gondola rock crusher sun door - Right"
+tunnel_gondola_rc_sdoor_m = "Tunnel: Gondola rock crusher sun door - Middle"
+tunnel_gondola_rc = "Tunnel: Gondola rock crusher"
+tunnel_rgondola_station = "Tunnel: Red gondola station"
+tunnel_gondola_transfer = "Tunnel: Gondola transfer point"
+tunnel_corpse_bucket_quag = "Tunnel: Corpse bucket poison pit"
+tunnel_corpse_bucket_mdoor_r = "Tunnel: Corpse bucket moon door - Right"
+tunnel_shovel_quag_start = "Tunnel: Shovel poison pit start"
+tunnel_exit_quag_start = "Tunnel: Exit door poison pit start"
+tunnel_shovel_quag_end = "Tunnel: Shovel poison pit end"
+tunnel_exit_quag_end = "Tunnel: Exit door poison pit end"
+tunnel_shovel = "Tunnel: Shovel"
+tunnel_shovel_save = "Tunnel: Shovel zone save junction"
+tunnel_shovel_mdoor_l = "Tunnel: Shovel zone moon door - Left"
+tunnel_shovel_sdoor_l = "Tunnel: Shovel zone sun door - Left"
+tunnel_shovel_sdoor_m = "Tunnel: Shovel zone sun door - Middle"
+
+# Tunnel 3-hit breakables
+tunnel_arrows_rock1 = "Tunnel: Twin arrow signs rock - Item 1"
+tunnel_arrows_rock2 = "Tunnel: Twin arrow signs rock - Item 2"
+tunnel_arrows_rock3 = "Tunnel: Twin arrow signs rock - Item 3"
+tunnel_arrows_rock4 = "Tunnel: Twin arrow signs rock - Item 4"
+tunnel_arrows_rock5 = "Tunnel: Twin arrow signs rock - Item 5"
+tunnel_bucket_quag_rock1 = "Tunnel: Lonesome bucket poison pit rock - Item 1"
+tunnel_bucket_quag_rock2 = "Tunnel: Lonesome bucket poison pit rock - Item 2"
+tunnel_bucket_quag_rock3 = "Tunnel: Lonesome bucket poison pit rock - Item 3"
+
+# Tunnel sub-weapons
+tunnel_stone_alcove_r = "Tunnel: Stepping stone alcove - Right"
+tunnel_lbucket_mdoor_l = "Tunnel: Lonesome bucket moon door"
+tunnel_gondola_rc_sdoor_l = "Tunnel: Gondola rock crusher sun door - Left"
+tunnel_corpse_bucket_mdoor_l = "Tunnel: Corpse bucket moon door - Left"
+tunnel_shovel_mdoor_r = "Tunnel: Shovel zone moon door - Right"
+tunnel_shovel_sdoor_r = "Tunnel: Shovel zone sun door - Right"
+
+
+# Underground Waterway main locations
+uw_near_ent = "Underground Waterway: Near entrance corridor"
+uw_across_ent = "Underground Waterway: Across from entrance"
+uw_poison_parkour = "Underground Waterway: Across poison parkour ledges"
+uw_waterfall_alcove = "Underground Waterway: Waterfall alcove ledge"
+uw_carrie1 = "Underground Waterway: Carrie crawlspace corridor - First left"
+uw_carrie2 = "Underground Waterway: Carrie crawlspace corridor - Second left"
+uw_bricks_save = "Underground Waterway: Brick platforms save corridor"
+uw_above_skel_ledge = "Underground Waterway: Above skeleton crusher ledge"
+
+# Underground Waterway 3-hit breakables
+uw_first_ledge1 = "Underground Waterway: First poison parkour ledge - Item 1"
+uw_first_ledge2 = "Underground Waterway: First poison parkour ledge - Item 2"
+uw_first_ledge3 = "Underground Waterway: First poison parkour ledge - Item 3"
+uw_first_ledge4 = "Underground Waterway: First poison parkour ledge - Item 4"
+uw_first_ledge5 = "Underground Waterway: First poison parkour ledge - Item 5"
+uw_first_ledge6 = "Underground Waterway: First poison parkour ledge - Item 6"
+uw_in_skel_ledge1 = "Underground Waterway: Inside skeleton crusher ledge - Item 1"
+uw_in_skel_ledge2 = "Underground Waterway: Inside skeleton crusher ledge - Item 2"
+uw_in_skel_ledge3 = "Underground Waterway: Inside skeleton crusher ledge - Item 3"
+
+
+# Castle Center basement main locations
+ccb_skel_hallway_ent = "Castle Center: Entrance hallway"
+ccb_skel_hallway_jun = "Castle Center: Basement hallway junction"
+ccb_skel_hallway_tc = "Castle Center: Torture chamber hallway"
+ccb_behemoth_l_ff = "Castle Center: Behemoth arena - Left far-front torch"
+ccb_behemoth_l_mf = "Castle Center: Behemoth arena - Left mid-front torch"
+ccb_behemoth_l_mr = "Castle Center: Behemoth arena - Left mid-rear torch"
+ccb_behemoth_l_fr = "Castle Center: Behemoth arena - Left far-rear torch"
+ccb_behemoth_r_ff = "Castle Center: Behemoth arena - Right far-front torch"
+ccb_behemoth_r_mf = "Castle Center: Behemoth arena - Right mid-front torch"
+ccb_behemoth_r_mr = "Castle Center: Behemoth arena - Right mid-rear torch"
+ccb_behemoth_r_fr = "Castle Center: Behemoth arena - Right far-rear torch"
+ccb_mandrag_shelf_l = "Castle Center: Mandragora shelf - Left"
+ccb_mandrag_shelf_r = "Castle Center: Mandragora shelf - Right"
+ccb_torture_rack = "Castle Center: Torture chamber instrument rack"
+ccb_torture_rafters = "Castle Center: Torture chamber rafters"
+
+# Castle Center elevator room main locations
+ccelv_near_machine = "Castle Center: Near elevator room machine"
+ccelv_atop_machine = "Castle Center: Atop elevator room machine"
+ccelv_pipes = "Castle Center: Elevator pipe device"
+ccelv_staircase = "Castle Center: Elevator room staircase"
+
+# Castle Center factory floor main locations
+ccff_redcarpet_knight = "Castle Center: Red carpet hall knight"
+ccff_gears_side = "Castle Center: Gear room side"
+ccff_gears_mid = "Castle Center: Gear room center"
+ccff_gears_corner = "Castle Center: Gear room corner"
+ccff_lizard_knight = "Castle Center: Lizard locker knight"
+ccff_lizard_pit = "Castle Center: Lizard locker room near pit"
+ccff_lizard_corner = "Castle Center: Lizard locker room corner"
+
+# Castle Center lizard lab main locations
+ccll_brokenstairs_floor = "Castle Center: Broken staircase floor"
+ccll_brokenstairs_knight = "Castle Center: Broken staircase knight"
+ccll_brokenstairs_save = "Castle Center: Above broken staircase savepoint"
+ccll_glassknight_l = "Castle Center: Stained Glass Knight room - Left"
+ccll_glassknight_r = "Castle Center: Stained Glass Knight room - Right"
+ccll_butlers_door = "Castle Center: Butler bros. room near door"
+ccll_butlers_side = "Castle Center: Butler bros. room inner"
+ccll_cwhall_butlerflames_past = "Castle Center: Past butler room flamethrowers"
+ccll_cwhall_flamethrower = "Castle Center: Inside cracked wall hallway flamethrower"
+ccll_cwhall_cwflames = "Castle Center: Past upper cracked wall flamethrowers"
+ccll_cwhall_wall = "Castle Center: Inside upper cracked wall"
+ccll_heinrich = "Castle Center: Heinrich Meyer"
+
+# Castle Center library main locations
+ccl_bookcase = "Castle Center: Library bookshelf"
+
+# Castle Center invention area main locations
+ccia_nitro_crates = "Castle Center: Nitro room crates"
+ccia_nitro_shelf_h = "Castle Center: Magical Nitro shelf - Heinrich side"
+ccia_nitro_shelf_i = "Castle Center: Magical Nitro shelf - Invention side"
+ccia_nitrohall_torch = "Castle Center: Past nitro room flamethrowers"
+ccia_nitrohall_flamethrower = "Castle Center: Inside nitro hallway flamethrower"
+ccia_inventions_crusher = "Castle Center: Invention room spike crusher door"
+ccia_inventions_maids = "Castle Center: Invention room maid sisters door"
+ccia_inventions_round = "Castle Center: Invention room round machine"
+ccia_inventions_famicart = "Castle Center: Invention room giant Famicart"
+ccia_inventions_zeppelin = "Castle Center: Invention room zeppelin"
+ccia_maids_outer = "Castle Center: Maid sisters room outer table"
+ccia_maids_inner = "Castle Center: Maid sisters room inner table"
+ccia_maids_vase = "Castle Center: Maid sisters room vase"
+ccia_stairs_knight = "Castle Center: Hell Knight landing corner knight"
+
+# Castle Center sub-weapons
+ccb_skel_hallway_ba = "Castle Center: Behemoth arena hallway"
+ccelv_switch = "Castle Center: Near elevator switch"
+ccff_lizard_near_knight = "Castle Center: Near lizard locker knight"
+
+# Castle Center lizard lockers
+ccff_lizard_locker_nfr = "Castle Center: Far-right near-side lizard locker"
+ccff_lizard_locker_nmr = "Castle Center: Mid-right near-side lizard locker"
+ccff_lizard_locker_nml = "Castle Center: Mid-left near-side lizard locker"
+ccff_lizard_locker_nfl = "Castle Center: Far-left near-side lizard locker"
+ccff_lizard_locker_fl = "Castle Center: Left far-side lizard locker"
+ccff_lizard_locker_fr = "Castle Center: Right far-side lizard locker"
+
+# Castle Center 3-hit breakables
+ccb_behemoth_crate1 = "Castle Center: Behemoth arena crate - Item 1"
+ccb_behemoth_crate2 = "Castle Center: Behemoth arena crate - Item 2"
+ccb_behemoth_crate3 = "Castle Center: Behemoth arena crate - Item 3"
+ccb_behemoth_crate4 = "Castle Center: Behemoth arena crate - Item 4"
+ccb_behemoth_crate5 = "Castle Center: Behemoth arena crate - Item 5"
+ccelv_stand1 = "Castle Center: Elevator room unoccupied statue stand - Item 1"
+ccelv_stand2 = "Castle Center: Elevator room unoccupied statue stand - Item 2"
+ccelv_stand3 = "Castle Center: Elevator room unoccupied statue stand - Item 3"
+ccff_lizard_slab1 = "Castle Center: Lizard locker room slab - Item 1"
+ccff_lizard_slab2 = "Castle Center: Lizard locker room slab - Item 2"
+ccff_lizard_slab3 = "Castle Center: Lizard locker room slab - Item 3"
+ccff_lizard_slab4 = "Castle Center: Lizard locker room slab - Item 4"
+
+
+# Duel Tower main locations
+dt_stones_start = "Duel Tower: Stepping stone path start"
+dt_werebull_arena = "Duel Tower: Above Were-bull arena"
+dt_ibridge_l = "Duel Tower: Invisible bridge balcony - Left"
+dt_ibridge_r = "Duel Tower: Invisible bridge balcony - Right"
+
+# Duel Tower sub-weapons
+dt_stones_end = "Duel Tower: Stepping stone path end"
+
+
+# Tower of Execution main locations
+toe_midsavespikes_r = "Tower of Execution: Past mid-savepoint spikes - Right"
+toe_midsavespikes_l = "Tower of Execution: Past mid-savepoint spikes - Left"
+toe_elec_grate = "Tower of Execution: Electric grate ledge"
+toe_ibridge = "Tower of Execution: Invisible bridge ledge"
+toe_top = "Tower of Execution: Guillotine tower top level"
+toe_keygate_l = "Tower of Execution: Key gate alcove - Left"
+
+# Tower of Execution 3-hit breakables
+toe_ledge1 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 1"
+toe_ledge2 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 2"
+toe_ledge3 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 3"
+toe_ledge4 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 4"
+toe_ledge5 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 5"
+
+# Tower of Execution sub-weapons
+toe_keygate_r = "Tower of Execution: Key gate alcove - Right"
+
+
+# Tower of Science main locations
+tosci_elevator = "Tower of Science: Elevator hallway"
+tosci_plain_sr = "Tower of Science: Plain sight side room"
+tosci_stairs_sr = "Tower of Science: Staircase side room"
+tosci_three_door_hall = "Tower of Science: Pick-a-door hallway locked middle room"
+tosci_ibridge_t = "Tower of Science: Invisible bridge platform torch"
+tosci_conveyor_sr = "Tower of Science: Spiky conveyor side room"
+tosci_exit = "Tower of Science: Exit hallway"
+tosci_key3_r = "Tower of Science: Locked Key3 room - Right"
+tosci_key3_l = "Tower of Science: Locked Key3 room - Left"
+
+# Tower of Science 3-hit breakables
+tosci_ibridge_b1 = "Tower of Science: Invisible bridge platform crate - Item 1"
+tosci_ibridge_b2 = "Tower of Science: Invisible bridge platform crate - Item 2"
+tosci_ibridge_b3 = "Tower of Science: Invisible bridge platform crate - Item 3"
+tosci_ibridge_b4 = "Tower of Science: Invisible bridge platform crate - Item 4"
+tosci_ibridge_b5 = "Tower of Science: Invisible bridge platform crate - Item 5"
+tosci_ibridge_b6 = "Tower of Science: Invisible bridge platform crate - Item 6"
+
+# Tower of Science sub-weapons
+tosci_key3_m = "Tower of Science: Locked Key3 room - Middle"
+
+
+# Tower of Sorcery main locations
+tosor_stained_tower = "Tower of Sorcery: Stained glass tower"
+tosor_savepoint = "Tower of Sorcery: Mid-savepoint platform"
+tosor_trickshot = "Tower of Sorcery: Trick shot from mid-savepoint platform"
+tosor_yellow_bubble = "Tower of Sorcery: Above yellow bubble"
+tosor_blue_platforms = "Tower of Sorcery: Above tiny blue platforms start"
+tosor_side_isle = "Tower of Sorcery: Lone red platform side island"
+tosor_ibridge = "Tower of Sorcery: Invisible bridge platform"
+
+# Room of Clocks main locations
+roc_ent_l = "Room of Clocks: Left from entrance hallway"
+roc_cont_r = "Room of Clocks: Right of Contract"
+roc_ent_r = "Room of Clocks: Right from entrance hallway"
+
+# Room of Clocks sub-weapons
+roc_elev_l = "Room of Clocks: Left of elevator hallway"
+roc_elev_r = "Room of Clocks: Right of elevator hallway"
+
+# Room of Clocks empty breakables
+roc_cont_l = "Room of Clocks: Left of Contract"
+roc_exit = "Room of Clocks: Left of exit"
+
+# Clock Tower main locations
+ct_gearclimb_corner = "Clock Tower: Gear climb room corner"
+ct_gearclimb_side = "Clock Tower: Gear climb room side"
+ct_bp_chasm_fl = "Clock Tower: Bone Pillar chasm room - Front-left"
+ct_bp_chasm_fr = "Clock Tower: Bone Pillar chasm room - Front-right"
+ct_bp_chasm_k = "Clock Tower: Bone Pillar chasm room key alcove"
+ct_finalroom_platform = "Clock Tower: Final room key ledge"
+
+# Clock Tower 3-hit breakables
+ct_gearclimb_battery_slab1 = "Clock Tower: Gear climb room beneath battery slab - Item 1"
+ct_gearclimb_battery_slab2 = "Clock Tower: Gear climb room beneath battery slab - Item 2"
+ct_gearclimb_battery_slab3 = "Clock Tower: Gear climb room beneath battery slab - Item 3"
+ct_gearclimb_door_slab1 = "Clock Tower: Gear climb room beneath door slab - Item 1"
+ct_gearclimb_door_slab2 = "Clock Tower: Gear climb room beneath door slab - Item 2"
+ct_gearclimb_door_slab3 = "Clock Tower: Gear climb room beneath door slab - Item 3"
+ct_finalroom_door_slab1 = "Clock Tower: Final room entrance slab - Item 1"
+ct_finalroom_door_slab2 = "Clock Tower: Final room entrance slab - Item 2"
+ct_finalroom_renon_slab1 = "Clock Tower: Renon's final offers slab - Item 1"
+ct_finalroom_renon_slab2 = "Clock Tower: Renon's final offers slab - Item 2"
+ct_finalroom_renon_slab3 = "Clock Tower: Renon's final offers slab - Item 3"
+ct_finalroom_renon_slab4 = "Clock Tower: Renon's final offers slab - Item 4"
+ct_finalroom_renon_slab5 = "Clock Tower: Renon's final offers slab - Item 5"
+ct_finalroom_renon_slab6 = "Clock Tower: Renon's final offers slab - Item 6"
+ct_finalroom_renon_slab7 = "Clock Tower: Renon's final offers slab - Item 7"
+ct_finalroom_renon_slab8 = "Clock Tower: Renon's final offers slab - Item 8"
+
+# Clock Tower sub-weapons
+ct_bp_chasm_rl = "Clock Tower: Bone Pillar chasm room - Rear-left"
+ct_finalroom_fr = "Clock Tower: Final room floor - front-right"
+ct_finalroom_fl = "Clock Tower: Final room floor - front-left"
+ct_finalroom_rr = "Clock Tower: Final room floor - rear-right"
+ct_finalroom_rl = "Clock Tower: Final room floor - rear-left"
+
+
+# Castle Keep main locations
+ck_flame_l = "Castle Keep: Left Dracula door flame"
+ck_flame_r = "Castle Keep: Right Dracula door flame"
+ck_behind_drac = "Castle Keep: Behind Dracula's chamber"
+ck_cube = "Castle Keep: Dracula's floating cube"
+
+
+# Renon's shop locations
+renon1 = "Renon's shop: Roast Chicken purchase"
+renon2 = "Renon's shop: Roast Beef purchase"
+renon3 = "Renon's shop: Healing Kit purchase"
+renon4 = "Renon's shop: Purifying purchase"
+renon5 = "Renon's shop: Cure Ampoule purchase"
+renon6 = "Renon's shop: Sun Card purchase"
+renon7 = "Renon's shop: Moon Card purchase"
+
+
+# Events
+forest_boss_one = "Forest of Silence: King Skeleton 1"
+forest_boss_two = "Forest of Silence: Were-tiger"
+forest_boss_three = "Forest of Silence: King Skeleton 2"
+cw_boss = "Castle Wall: Bone Dragons"
+villa_boss_one = "Villa: J. A. Oldrey"
+villa_boss_two = "Villa: Undead Maiden"
+uw_boss = "Underground Waterway: Lizard-man trio"
+cc_boss_one = "Castle Center: Behemoth"
+cc_boss_two = "Castle Center: Rosa/Camilla"
+dt_boss_one = "Duel Tower: Were-jaguar"
+dt_boss_two = "Duel Tower: Werewolf"
+dt_boss_three = "Duel Tower: Were-bull"
+dt_boss_four = "Duel Tower: Were-tiger"
+roc_boss = "Room of Clocks: Death/Actrise"
+ck_boss_one = "Castle Keep: Renon"
+ck_boss_two = "Castle Keep: Vincent"
+
+cc_behind_the_seal = "Castle Center: Behind the seal"
+
+the_end = "Dracula"
diff --git a/worlds/cv64/data/patches.py b/worlds/cv64/data/patches.py
new file mode 100644
index 000000000000..938b615b3213
--- /dev/null
+++ b/worlds/cv64/data/patches.py
@@ -0,0 +1,2878 @@
+normal_door_hook = [
+ 0x00862021, # ADDU A0, A0, A2
+ 0x80849C60, # LB A0, 0x9C60 (A0)
+ 0x0C0FF174, # JAL 0x803FC5D0
+ 0x308900FF # ANDI T1, A0, 0x00FF
+]
+
+normal_door_code = [
+ 0x00024080, # SLL T0, V0, 2
+ 0x3C048039, # LUI A0, 0x8039
+ 0x00882021, # ADDU A0, A0, T0
+ 0x8C849BE4, # LW A0, 0x9BE4 (A0)
+ 0x8C6A0008, # LW T2, 0x0008 (V1)
+ 0x008A5824, # AND T3, A0, T2
+ 0x11600003, # BEQZ T3, [forward 0x03]
+ 0x00000000, # NOP
+ 0x24020003, # ADDIU V0, R0, 0x0003
+ 0x27FF006C, # ADDIU RA, RA, 0x006C
+ 0x03E00008 # JR RA
+]
+
+ct_door_hook = [
+ 0x0C0FF182, # JAL 0x803FC608
+ 0x00000000, # NOP
+ 0x315900FF # ANDI T9, T2, 0x00FF
+]
+
+ct_door_code = [
+ 0x3C0A8039, # LUI T2, 0x8039
+ 0x8D429BF8, # LW V0, 0x9BF8 (T2)
+ 0x01465021, # ADDU T2, T2, A2
+ 0x814A9C60, # LB T2, 0x9C60 (T2)
+ 0x00495824, # AND T3, V0, T1
+ 0x55600001, # BNEZL T3, [forward 0x01]
+ 0x27FF0010, # ADDIU RA, RA, 0x0010
+ 0x03E00008 # JR RA
+]
+
+stage_select_overwrite = [
+ # Replacement for the "wipe world state" function when using the warp menu. Now it's the "Special1 jewel checker"
+ # to see how many destinations can be selected on it with the current count.
+ 0x8FA60018, # LW A2, 0x0018 (SP)
+ 0xA0606437, # SB R0, 0x6437 (V1)
+ 0x10000029, # B [forward 0x29]
+ 0x00000000, # NOP
+ 0x3C0A8039, # LUI T2, 0x8039
+ 0x254A9C4B, # ADDIU T2, T2, 0x9C4B
+ 0x814B0000, # LB T3, 0x0000 (T2)
+ 0x240C000A, # ADDIU T4, R0, 0x000A
+ 0x016C001B, # DIVU T3, T4
+ 0x00003012, # MFLO A2
+ 0x24C60001, # ADDIU A2, A2, 0x0001
+ 0x28CA0009, # SLTI T2, A2, 0x0009
+ 0x51400001, # BEQZL T2, 0x8012AC7C
+ 0x24060008, # ADDIU A2, R0, 0x0008
+ 0x3C0A800D, # LUI T2, 0x800D
+ 0x914A5E20, # LBU T2, 0x5E20 (T2)
+ 0x314A0040, # ANDI T2, T2, 0x0040
+ 0x11400003, # BEQZ T2, [forward 0x03]
+ 0x240BFFFE, # ADDIU T3, R0, 0xFFFE
+ 0x3C0C8034, # LUI T4, 0x8034
+ 0xAD8B2084, # SW T3, 0x2084 (T4)
+ 0x03200008, # JR T9
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+]
+
+custom_code_loader = [
+ # On boot, when the company logos show up, this will trigger and load most of the custom ASM data in this module
+ # off from ROM offsets 0xBFC000-0xBFFFFF and into the 803FC000-803FFFFF range in RAM.
+ 0x3C080C10, # LUI T0, 0x0C10
+ 0x2508F1C0, # ADDIU T0, T0, 0xF1C0
+ 0x3C098000, # LUI T1, 0x8000
+ 0xAD282438, # SW T0, 0x2438 (T1)
+ 0x3C088040, # LUI T0, 0x8040
+ 0x9108C000, # ADDIU T0, 0xC000 (T0)
+ 0x15000007, # BNEZ T0, [forward 0x07]
+ 0x3C0400C0, # LUI A0, 0x00C0
+ 0x2484C000, # ADDIU A0, A0, 0xC000
+ 0x3C058040, # LUI A1, 0x8040
+ 0x24A5C000, # ADDIU A1, A1, 0xC000
+ 0x24064000, # ADDIU A2, R0, 0x4000
+ 0x08005DFB, # J 0x800177EC
+ 0x00000000, # NOP
+ 0x03E00008 # JR RA
+]
+
+remote_item_giver = [
+ # The essential multiworld function. Every frame wherein the player is in control and not looking at a text box,
+ # this thing will check some bytes in RAM to see if an item or DeathLink has been received and trigger the right
+ # functions accordingly to either reward items or kill the player.
+
+ # Primary checks
+ 0x3C088034, # LUI T0, 0x8034
+ 0x9509244A, # LHU T1, 0x244A (T0)
+ 0x3C088039, # LUI T0, 0x8039
+ 0x910A9EFB, # LBU T2, 0x9EFF (T0)
+ 0x012A4821, # ADDU T1, T1, T2
+ 0x910A9EFF, # LBU T2, 0x9EFF (T0)
+ 0x012A4821, # ADDU T1, T1, T2
+ 0x910A9CCF, # LBU T2, 0x9CCF (T0)
+ 0x012A4821, # ADDU T1, T1, T2
+ 0x910A9EEF, # LBU T2, 0x9EEF (T0)
+ 0x012A4821, # ADDU T1, T1, T2
+ 0x910A9CD3, # LBU T2, 0x9CD3 (T0)
+ 0x012A4821, # ADDU T1, T1, T2
+ 0x3C088038, # LUI T0, 0x8038
+ 0x910A7ADD, # LBU T2, 0x7ADD (T0)
+ 0x012A4821, # ADDU T1, T1, T2
+ 0x3C0B8039, # LUI T3, 0x8039
+ 0x916A9BE0, # LBU T2, 0x9BE0 (T3)
+ 0x012A4821, # ADDU T1, T1, T2
+ 0x11200006, # BEQZ T1, [forward 0x06]
+ 0x00000000, # NOP
+ 0x11400002, # BEQZ T2, [forward 0x02]
+ 0x254AFFFF, # ADDIU T2, T2, 0xFFFF
+ 0xA16A9BE0, # SB T2, 0x9BE0 (T3)
+ 0x03E00008, # JR RA
+ 0x00000000, # NOP
+ # Item-specific checks
+ 0x3C088034, # LUI T0, 0x8034
+ 0x91082891, # LBU T0, 0x2891 (T0)
+ 0x24090002, # ADDIU T1, R0, 0x0002
+ 0x15090012, # BNE T0, T1, [forward 0x12]
+ 0x00000000, # NOP
+ 0x256B9BDF, # ADDIU T3, T3, 0x9BDF
+ 0x91640000, # LBU A0, 0x0000 (T3)
+ 0x14800003, # BNEZ A0, [forward 0x03]
+ 0x00000000, # NOP
+ 0x10000005, # B [forward 0x05]
+ 0x256B0002, # ADDIU T3, T3, 0x0002
+ 0x2409000F, # ADDIU T1, R0, 0x000F
+ 0xA1690001, # SB T1, 0x0001 (T3)
+ 0x080FF8DD, # J 0x803FE374
+ 0xA1600000, # SB R0, 0x0000 (T3)
+ 0x91640000, # LBU A0, 0x0000 (T3)
+ 0x14800002, # BNEZ A0, [forward 0x02]
+ 0x00000000, # NOP
+ 0x10000003, # B [forward 0x03]
+ 0x2409000F, # ADDIU T1, R0, 0x000F
+ 0x080FF864, # J 0x803FE190
+ 0xA169FFFF, # SB T1, 0xFFFF (T3)
+ # DeathLink-specific checks
+ 0x3C0B8039, # LUI T3, 0x8039
+ 0x256B9BE1, # ADDIU T3, T3, 0x9BE1
+ 0x91640002, # LBU A0, 0x0002 (T3)
+ 0x14800002, # BNEZ A0, [forward 0x02]
+ 0x916900A7, # LBU T1, 0x00A7 (T3)
+ 0x080FF9C0, # J 0x803FE700
+ 0x312A0080, # ANDI T2, T1, 0x0080
+ 0x11400002, # BEQZ T2, [forward 0x02]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ 0x35290080, # ORI T1, T1, 0x0080
+ 0xA16900A7, # SB T1, 0x00A7 (T3)
+ 0x2484FFFF, # ADDIU A0, A0, 0xFFFF
+ 0x24080001, # ADDIU T0, R0, 0x0001
+ 0x03E00008, # JR RA
+ 0xA168FFFD, # SB T0, 0xFFFD (T3)
+]
+
+deathlink_nitro_edition = [
+ # Alternative to the end of the above DeathLink-specific checks that kills the player with the Nitro explosion
+ # instead of the normal death.
+ 0x91690043, # LBU T1, 0x0043 (T3)
+ 0x080FF9C0, # J 0x803FE700
+ 0x3C088034, # LUI T0, 0x8034
+ 0x91082BFE, # LBU T0, 0x2BFE (T0)
+ 0x11000002, # BEQZ T0, [forward 0x02]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ 0x35290080, # ORI T1, T1, 0x0080
+ 0xA1690043, # SB T1, 0x0043 (T3)
+ 0x2484FFFF, # ADDIU A0, A0, 0xFFFF
+ 0x24080001, # ADDIU T0, R0, 0x0001
+ 0x03E00008, # JR RA
+ 0xA168FFFD, # SB T0, 0xFFFD (T3)
+]
+
+launch_fall_killer = [
+ # Custom code to force the instant fall death if at a high enough falling speed after getting killed by something
+ # that launches you (whether it be the Nitro explosion or a Big Toss hit). The game doesn't normally run the check
+ # that would trigger the fall death after you get killed by some other means, which could result in a softlock
+ # when a killing blow launches you into an abyss.
+ 0x3C0C8035, # LUI T4, 0x8035
+ 0x918807E2, # LBU T0, 0x07E2 (T4)
+ 0x24090008, # ADDIU T1, R0, 0x0008
+ 0x11090002, # BEQ T0, T1, [forward 0x02]
+ 0x2409000C, # ADDIU T1, R0, 0x000C
+ 0x15090006, # BNE T0, T1, [forward 0x06]
+ 0x3C098035, # LUI T1, 0x8035
+ 0x91290810, # LBU T1, 0x0810 (T1)
+ 0x240A00C1, # ADDIU T2, R0, 0x00C1
+ 0x152A0002, # BNE T1, T2, [forward 0x02]
+ 0x240B0001, # ADDIU T3, R0, 0x0001
+ 0xA18B07E2, # SB T3, 0x07E2 (T4)
+ 0x03E00008 # JR RA
+]
+
+deathlink_counter_decrementer = [
+ # Decrements the DeathLink counter if it's above zero upon loading a previous state. Checking this number will be
+ # how the client will tell if a player's cause of death was something in-game or a DeathLink (and send a DeathLink
+ # to the server if it was the former). Also resets the remote item values to 00 so the player's received items don't
+ # get mucked up in-game.
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91099BE3, # LBU T1, 0x9BE3 (T0)
+ 0x11200002, # BEQZ T1, 0x803FC154
+ 0x2529FFFF, # ADDIU T1, T1, 0xFFFF
+ 0xA1099BE3, # SB T1, 0x9BE3
+ 0x240900FF, # ADDIU T1, R0, 0x00FF
+ 0xA1099BE0, # SB T1, 0x9BE0 (T0)
+ 0xA1009BDF, # SB R0, 0x9BDF (T0)
+ 0xA1009BE1, # SB R0, 0x9BE1 (T0)
+ 0x91099BDE, # LBU T1, 0x9BDE (T0)
+ 0x55200001, # BNEZL T1, [forward 0x01]
+ 0x24090000, # ADDIU T1, R0, 0x0000
+ 0xA1099BDE, # SB T1, 0x9BDE (T0)
+ 0x91099C24, # LBU T1, 0x9C24 (T0)
+ 0x312A0080, # ANDI T2, T1, 0x0080
+ 0x55400001, # BNEZL T2, [forward 0x01]
+ 0x3129007F, # ANDI T1, T1, 0x007F
+ 0x03E00008, # JR RA
+ 0xA1099C24 # SB T1, 0x9C24 (T0)
+]
+
+death_flag_unsetter = [
+ # Un-sets the Death status bitflag when overwriting the "Restart this stage" state and sets health to full if it's
+ # empty. This is to ensure DeathLinked players won't get trapped in a perpetual death loop for eternity should they
+ # receive one right before transitioning to a different stage.
+ 0x3C048039, # LUI A0, 0x8039
+ 0x90889C88, # LBU T0, 0x9C88 (A0)
+ 0x31090080, # ANDI T1, T0, 0x0080
+ 0x01094023, # SUBU T0, T0, T1
+ 0x908A9C3F, # LBU T2, 0x9C3F (A0)
+ 0x24090064, # ADDIU T1, R0, 0x0064
+ 0x51400001, # BEQZL T2, [forward 0x01]
+ 0xA0899C3F, # SB T1, 0x9C3F (A0)
+ 0x08006DAE, # J 0x8001B6B8
+ 0xA0889C88 # SB T0, 0x9C88 (A0)
+]
+
+warp_menu_opener = [
+ # Enables opening the Stage Select menu by pausing while holding Z + R when not in a boss fight, the castle
+ # crumbling sequence following Fake Dracula, or Renon's arena (in the few seconds after his health bar vanishes).
+ 0x3C08800D, # LUI T0, 0x800D
+ 0x85095E20, # LH T1, 0x5E20 (T0)
+ 0x24083010, # ADDIU T0, R0, 0x3010
+ 0x15090011, # BNE T0, T1, [forward 0x11]
+ 0x3C088035, # LUI T0, 0x8035
+ 0x9108F7D8, # LBU T0, 0xF7D8 (T0)
+ 0x24090020, # ADDIU T1, R0, 0x0020
+ 0x1109000D, # BEQ T0, T1, [forward 0x0D]
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91099BFA, # LBU T1, 0x9BFA (T0)
+ 0x31290001, # ANDI T1, T1, 0x0001
+ 0x15200009, # BNEZ T1, [forward 0x09]
+ 0x8D099EE0, # LW T1, 0x9EE0
+ 0x3C0A001B, # LUI T2, 0x001B
+ 0x254A0003, # ADDIU T2, T2, 0x0003
+ 0x112A0005, # BEQ T1, T2, [forward 0x05]
+ 0x3C098034, # LUI T1, 0x8034
+ 0xA1009BE1, # SB R0, 0x9BE1 (T0)
+ 0x2408FFFC, # ADDIU T0, R0, 0xFFFC
+ 0x0804DA70, # J 0x80136960
+ 0xAD282084, # SW T0, 0x2084 (T1)
+ 0x0804DA70, # J 0x80136960
+ 0xA44E6436 # SH T6, 0x6436 (V0)
+]
+
+give_subweapon_stopper = [
+ # Extension to "give subweapon" function to not change the player's weapon if the received item is a Stake or Rose.
+ # Can also increment the Ice Trap counter if getting a Rose or jump to prev_subweapon_dropper if applicable.
+ 0x24090011, # ADDIU T1, R0, 0x0011
+ 0x11240009, # BEQ T1, A0, [forward 0x09]
+ 0x24090012, # ADDIU T1, R0, 0x0012
+ 0x11240003, # BEQ T1, A0, [forward 0x03]
+ 0x9465618A, # LHU A1, 0x618A (V1)
+ 0xA46D618A, # SH T5, 0x618A (V1)
+ 0x0804F0BF, # J 0x8013C2FC
+ 0x3C098039, # LUI T1, 0x8039
+ 0x912A9BE2, # LBU T2, 0x9BE2 (T1)
+ 0x254A0001, # ADDIU T2, T2, 0x0001
+ 0xA12A9BE2, # SB T2, 0x9BE2 (T1)
+ 0x0804F0BF, # J 0x8013C2FC
+]
+
+give_powerup_stopper = [
+ # Extension to "give PowerUp" function to not increase the player's PowerUp count beyond 2
+ 0x240D0002, # ADDIU T5, R0, 0x0002
+ 0x556D0001, # BNEL T3, T5, [forward 0x01]
+ 0xA46C6234, # SH T4, 0x6234 (V1)
+ 0x0804F0BF # J 0x8013C2FC
+]
+
+npc_item_hack = [
+ # Hack to make NPC items show item textboxes when received (and decrease the Countdown if applicable).
+ 0x3C098039, # LUI T1, 0x8039
+ 0x001F5602, # SRL T2, RA, 24
+ 0x240B0080, # ADDIU T3, R0, 0x0080
+ 0x114B001F, # BEQ T2, T3, [forward 0x1F]
+ 0x240A001A, # ADDIU T2, R0, 0x001A
+ 0x27BD0020, # ADDIU SP, SP, 0x20
+ 0x15440004, # BNE T2, A0, [forward 0x04]
+ 0x240B0029, # ADDIU T3, R0, 0x0029
+ 0x34199464, # ORI T9, R0, 0x9464
+ 0x10000004, # B [forward 0x04]
+ 0x240C0002, # ADDIU T4, R0, 0x0002
+ 0x3419DA64, # ORI T9, R0, 0xDA64
+ 0x240B0002, # ADDIU T3, R0, 0x0002
+ 0x240C000E, # ADDIU T4, R0, 0x000E
+ 0x012C7021, # ADDU T6, T1, T4
+ 0x316C00FF, # ANDI T4, T3, 0x00FF
+ 0x000B5A02, # SRL T3, T3, 8
+ 0x91CA9CA4, # LBU T2, 0x9CA4 (T6)
+ 0x3C0D8040, # LUI T5, 0x8040
+ 0x256FFFFF, # ADDIU T7, T3, 0xFFFF
+ 0x01AF6821, # ADDU T5, T5, T7
+ 0x91B8D71C, # LBU T8, 0xD71C (T5)
+ 0x29EF0019, # SLTI T7, T7, 0x0019
+ 0x51E00001, # BEQZL T7, [forward 0x01]
+ 0x91B8D71F, # LBU T8, 0xD71F (T5)
+ 0x13000002, # BEQZ T8, [forward 0x02]
+ 0x254AFFFF, # ADDIU T2, T2, 0xFFFF
+ 0xA1CA9CA4, # SB T2, 0x9CA4 (T6)
+ 0xA12C9BDF, # SB T4, 0x9BDF (T1)
+ 0x3C0400BB, # LUI A0, 0x00BB
+ 0x00992025, # OR A0, A0, T9
+ 0x3C058019, # LUI A1, 0x8019
+ 0x24A5BF98, # ADDIU A1, A1, 0xBF98
+ 0x08005DFB, # J 0x800177EC
+ 0x24060100, # ADDIU A2, R0, 0x0100
+ 0x0804EFFD, # J 0x8013BFF4
+ 0xAFBF0014 # SW RA, 0x0014 (SP)
+]
+
+overlay_modifiers = [
+ # Whenever a compressed overlay gets decompressed and mapped in the 0F or 0E domains, this thing will check the
+ # number ID in the T0 register to tell which one it is and overwrite some instructions in it on-the-fly accordingly
+ # to said number before it runs. Confirmed to NOT be a foolproof solution on console and Simple64; the instructions
+ # may not be properly overwritten on the first execution of the overlay.
+
+ # Prevent being able to throw Nitro into the Hazardous Waste Disposals
+ 0x3C0A2402, # LUI T2, 0x2402
+ 0x254A0001, # ADDIU T2, T2, 0x0001
+ 0x24090023, # ADDIU T1, R0, 0x0023
+ 0x15090003, # BNE T0, T1, [forward 0x03]
+ 0x00000000, # NOP
+ 0x03200008, # JR T9
+ 0xAF2A01D4, # SW T2, 0x01D4 (T9)
+ # Make it so nothing can be taken from the Nitro or Mandragora shelves through the textboxes
+ 0x24090022, # ADDIU T1, R0, 0x0022
+ 0x11090002, # BEQ T0, T1, [forward 0x02]
+ 0x24090021, # ADDIU T1, R0, 0x0021
+ 0x15090003, # BNE T0, T1, [forward 0x03]
+ 0x254AFFFF, # ADDIU T2, T2, 0xFFFF
+ 0x03200008, # JR T9
+ 0xAF2A0194, # SW T2, 0x0194 (T9)
+ # Fix to allow placing both bomb components at a cracked wall at once while having multiple copies of each, and
+ # prevent placing them at the downstairs crack altogether until the seal is removed. Also enables placing both in
+ # one interaction.
+ 0x24090024, # ADDIU T1, R0, 0x0024
+ 0x15090012, # BNE T0, T1, [forward 0x12]
+ 0x240A0040, # ADDIU T2, R0, 0x0040
+ 0x240BC338, # ADDIU T3, R0, 0xC338
+ 0x240CC3D4, # ADDIU T4, R0, 0xC3D4
+ 0x240DC38C, # ADDIU T5, R0, 0xC38C
+ 0xA32A030F, # SB T2, 0x030F (T9)
+ 0xA72B0312, # SH T3, 0x0312 (T9)
+ 0xA32A033F, # SB T2, 0x033F (T9)
+ 0xA72B0342, # SH T3, 0x0342 (T9)
+ 0xA32A03E3, # SB T2, 0x03E3 (T9)
+ 0xA72C03E6, # SH T4, 0x03E6 (T9)
+ 0xA32A039F, # SB T2, 0x039F (T9)
+ 0xA72D03A2, # SH T5, 0x03A2 (T9)
+ 0xA32A03CB, # SB T2, 0x03CB (T9)
+ 0xA72D03CE, # SH T5, 0x03CE (T9)
+ 0xA32A05CF, # SB T2, 0x05CF (T9)
+ 0x240EE074, # ADDIU T6, R0, 0xE074
+ 0xA72E05D2, # SH T6, 0x05D2 (T9)
+ 0x03200008, # JR T9
+ # Disable the costume and Hard Mode flag checks so that pressing Up on the Player Select screen will always allow
+ # the characters' alternate costumes to be used as well as Hard Mode being selectable without creating save data.
+ 0x2409012E, # ADDIU T1, R0, 0x012E
+ 0x1509000A, # BNE T0, T1, [forward 0x0A]
+ 0x3C0A3C0B, # LUI T2, 0x3C0B
+ 0x254A8000, # ADDIU T2, T2, 0x8000
+ 0x240B240E, # ADDIU T3, R0, 0x240E
+ 0x240C240F, # ADDIU T4, R0, 0x240F
+ 0x240D0024, # ADDIU T5, R0, 0x0024
+ 0xAF2A0C78, # SW T2, 0x0C78 (T9)
+ 0xA72B0CA0, # SH T3, 0x0CA0 (T9)
+ 0xA72C0CDC, # SH T4, 0x0CDC (T9)
+ 0xA32D0168, # SB T5, 0x0024 (T9)
+ 0x03200008, # JR T9
+ # Overwrite instructions in the Forest end cutscene script to store a spawn position ID instead of a cutscene ID.
+ 0x2409002E, # ADDIU T1, R0, 0x002E
+ 0x15090005, # BNE T0, T1, [forward 0x05]
+ 0x3C0AA058, # LUI T2, 0xA058
+ 0x254A642B, # ADDIU T2, T2, 0x642B
+ 0xAF2A0D88, # SW T2, 0x0D88 (T9)
+ 0xAF200D98, # SW R0, 0x0D98 (T9)
+ 0x03200008, # JR T9
+ # Disable the rapid flashing effect in the CC planetarium cutscene to ensure it won't trigger seizures.
+ 0x2409003E, # ADDIU T1, R0, 0x003E
+ 0x1509000C, # BNE T0, T1, [forward 0x0C]
+ 0x00000000, # NOP
+ 0xAF200C5C, # SW R0, 0x0C5C
+ 0xAF200CD0, # SW R0, 0x0CD0
+ 0xAF200C64, # SW R0, 0x0C64
+ 0xAF200C74, # SW R0, 0x0C74
+ 0xAF200C80, # SW R0, 0x0C80
+ 0xAF200C88, # SW R0, 0x0C88
+ 0xAF200C90, # SW R0, 0x0C90
+ 0xAF200C9C, # SW R0, 0x0C9C
+ 0xAF200CB4, # SW R0, 0x0CB4
+ 0xAF200CC8, # SW R0, 0x0CC8
+ 0x03200008, # JR T9
+ 0x24090134, # ADDIU T1, R0, 0x0134
+ 0x15090005, # BNE T0, T1, [forward 0x05]
+ 0x340B8040, # ORI T3, R0, 0x8040
+ 0x340CDD20, # ORI T4, R0, 0xDD20
+ 0xA72B1D1E, # SH T3, 0x1D1E (T9)
+ 0xA72C1D22, # SH T4, 0x1D22 (T9)
+ 0x03200008, # JR T9
+ # Make the Ice Trap model check branch properly
+ 0x24090125, # ADDIU T1, R0, 0x0125
+ 0x15090003, # BNE T0, T1, [forward 0x03]
+ 0x3C0B3C19, # LUI T3, 0x3C19
+ 0x356B803F, # ORI T3, T3, 0x803F
+ 0xAF2B04D0, # SW T3, 0x04D0 (T9)
+ 0x03200008 # JR T9
+]
+
+double_component_checker = [
+ # When checking to see if a bomb component can be placed at a cracked wall, this will run if the code lands at the
+ # "no need to set 2" outcome to see if the other can be set.
+
+ # Mandragora checker
+ 0x10400007, # BEQZ V0, [forward 0x07]
+ 0x3C0A8039, # LUI T2, 0x8039
+ 0x31098000, # ANDI T1, T0, 0x8000
+ 0x15200008, # BNEZ T1, [forward 0x08]
+ 0x91499C5D, # LBU T1, 0x9C5D (T2)
+ 0x11200006, # BEQZ T1, 0x80183938
+ 0x00000000, # NOP
+ 0x10000007, # B [forward 0x07]
+ 0x31E90100, # ANDI T1, T7, 0x0100
+ 0x15200002, # BNEZ T1, [forward 0x02]
+ 0x91499C5D, # LBU T1, 0x9C5D (T2)
+ 0x15200003, # BNEZ T1, [forward 0x03]
+ 0x3C198000, # LUI T9, 0x8000
+ 0x27391590, # ADDIU T9, T9, 0x1590
+ 0x03200008, # JR T9
+ 0x24090001, # ADDIU T1, R0, 0x0001
+ 0xA4E9004C, # SH T1, 0x004C (A3)
+ 0x3C190E00, # LUI T9, 0x0E00
+ 0x273903E0, # ADDIU T9, T9, 0x03E0
+ 0x03200008, # JR T9
+ 0x00000000, # NOP
+ # Nitro checker
+ 0x10400007, # BEQZ V0, [forward 0x07]
+ 0x3C0A8039, # LUI T2, 0x8039
+ 0x31694000, # ANDI T1, T3, 0x4000
+ 0x15200008, # BNEZ T1, [forward 0x08]
+ 0x91499C5C, # LBU T1, 0x9C5C
+ 0x11200006, # BEQZ T1, [forward 0x06]
+ 0x00000000, # NOP
+ 0x1000FFF4, # B [backward 0x0B]
+ 0x914F9C18, # LBU T7, 0x9C18 (T2)
+ 0x31E90002, # ANDI T1, T7, 0x0002
+ 0x1520FFEC, # BNEZ T1, [backward 0x13]
+ 0x91499C5C, # LBU T1, 0x9C5C (T2)
+ 0x1520FFEF, # BNEZ T1, [backward 0x15]
+ 0x00000000, # NOP
+ 0x1000FFE8, # B [backward 0x17]
+ 0x00000000, # NOP
+]
+
+downstairs_seal_checker = [
+ # This will run specifically for the downstairs crack to see if the seal has been removed before then deciding to
+ # let the player set the bomb components or not. An anti-dick measure, since there is a limited number of each
+ # component per world.
+ 0x14400004, # BNEZ V0, [forward 0x04]
+ 0x3C0A8039, # LUI T2, 0x8039
+ 0x914A9C18, # LBU T2, 0x9C18 (T2)
+ 0x314A0001, # ANDI T2, T2, 0x0001
+ 0x11400003, # BEQZ T2, [forward 0x03]
+ 0x3C198000, # LUI T9, 0x8000
+ 0x27391448, # ADDIU T9, T9, 0x1448
+ 0x03200008, # JR T9
+ 0x3C190E00, # LUI T9, 0x0E00
+ 0x273902B4, # ADDIU T9, T9, 0x02B4
+ 0x03200008, # JR T9
+ 0x00000000, # NOP
+]
+
+map_data_modifiers = [
+ # Overwrites the map data table on-the-fly after it loads and before the game reads it to load objects. Good for
+ # changing anything that is part of a compression chain in the ROM data, including some freestanding item IDs.
+ # Also jumps to the function that overwrites the "Restart this stage" data if entering through the back of a level.
+
+ 0x08006DAA, # J 0x8001B6A8
+ 0x00000000, # NOP
+ # Demo checker (if we're in a title demo, don't do any of this)
+ 0x3C028034, # LUI V0, 0x8034
+ 0x9449244A, # LHU T1, 0x244A (V0)
+ 0x11200002, # BEQZ T1, [forward 0x02]
+ # Zero checker (if there are zeroes in the word at 0x8034244A, where the entity list address is stored, don't do
+ # any of this either)
+ 0x8C422B00, # LW V0, 0x2B00 (V0)
+ 0x03E00008, # JR RA
+ 0x00000000, # NOP
+ 0x14400002, # BNEZ V0, [forward 0x02]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91199EE3, # LBU T9, 0x9EE3 (T0)
+ 0x91089EE1, # LBU T0, 0x9EE1 (T0)
+ # Forest of Silence (replaces 1 invisible chicken)
+ 0x15000006, # BNEZ T0, [forward 0x06]
+ 0x340A0001, # ORI T2, R0, 0x0001 <- Werewolf plaque
+ 0xA44A01C8, # SH T2, 0x01C8 (V0)
+ 0x24090001, # ADDIU T1, R0, 0x0001
+ 0x1139FFED, # BEQ T1, T9, [backward 0x12]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ # Villa front yard (replaces 1 moneybag and 2 beefs)
+ 0x24090003, # ADDIU T1, R0, 0x0003
+ 0x15090008, # BNE T0, T1, [forward 0x08]
+ 0x340A0001, # ORI T2, R0, 0x0001 <- Fountain FL
+ 0x340B0001, # ORI T3, R0, 0x0001 <- Fountain RL
+ 0x340C001F, # ORI T4, R0, 0x0001 <- Dog food gate
+ 0xA44A0058, # SH T2, 0x0058 (V0)
+ 0xA44B0038, # SH T3, 0x0038 (V0)
+ 0xA44C0068, # SH T4, 0x0068 (V0)
+ 0x03E00008, # JR RA
+ 0x00000000, # NOP
+ # Villa living area (Replaces 1 chicken, 1 knife, and 3 invisible Purifyings and assigns flags to the sub-weapons)
+ 0x24090005, # ADDIU T1, R0, 0x0005
+ 0x15090025, # BNE T0, T1, [forward 0x25]
+ 0x340B0010, # ORI T3, R0, 0x0001 <- Hallway axe
+ 0xA44B00B8, # SH T3, 0x00B8 (V0)
+ 0x340A0001, # ORI T2, R0, 0x0001 <- Storeroom R
+ 0x340B0010, # ORI T3, R0, 0x0001 <- Hallway knife
+ 0x340C0001, # ORI T4, R0, 0x0001 <- Living Room painting
+ 0x340D0001, # ORI T5, R0, 0x0001 <- Dining Room vase
+ 0x340E0001, # ORI T6, R0, 0x0001 <- Archives table
+ 0xA44A0078, # SH T2, 0x0078 (V0)
+ 0xA44B00C8, # SH T3, 0x00C8 (V0)
+ 0xA44C0108, # SH T4, 0x0108 (V0)
+ 0xA44D0128, # SH T5, 0x0128 (V0)
+ 0xA44E0138, # SH T6, 0x0138 (V0)
+ 0x340A0000, # ORI T2, R0, 0x0000 <- Sub-weapons left flag half
+ 0xA44A009C, # SH T2, 0x009C (V0)
+ 0xA44A00AC, # SH T2, 0x00AC (V0)
+ 0xA44A00BC, # SH T2, 0x00BC (V0)
+ 0xA44A00CC, # SH T2, 0x00CC (V0)
+ 0x340A0000, # ORI T2, R0, 0x0000 <- Sub-weapons right flag halves
+ 0x240B0000, # ADDIU T3, R0, 0x0000
+ 0x240C0000, # ADDIU T4, R0, 0x0000
+ 0x240D0000, # ADDIU T5, R0, 0x0000
+ 0xA44A00CA, # SH T2, 0x00CA (V0)
+ 0xA44B00BA, # SH T3, 0x00BA (V0)
+ 0xA44C009A, # SH T4, 0x009A (V0)
+ 0xA44D00AA, # SH T5, 0x00AA (V0)
+ 0x340A0001, # ORI T2, R0, 0x0001 <- Near bed
+ 0x340B0010, # ORI T3, R0, 0x0001 <- Storeroom L
+ 0x340C0001, # ORI T4, R0, 0x0001 <- Storeroom statue
+ 0x340D0001, # ORI T5, R0, 0x0001 <- Exit knight
+ 0x340E0001, # ORI T6, R0, 0x0001 <- Sitting room table
+ 0xA44A0048, # SH T2, 0x0078 (V0)
+ 0xA44B0088, # SH T3, 0x00C8 (V0)
+ 0xA44C00D8, # SH T4, 0x0108 (V0)
+ 0xA44D00F8, # SH T5, 0x0128 (V0)
+ 0xA44E0118, # SH T6, 0x0138 (V0)
+ 0x03E00008, # JR RA
+ 0x00000000, # NOP
+ # Tunnel (replaces 1 invisible Cure Ampoule)
+ 0x24090007, # ADDIU T1, R0, 0x0007
+ 0x1509000A, # BNE T0, T1, [forward 0x0A]
+ 0x340A0001, # ORI T2, R0, 0x0001 <- Twin arrow signs
+ 0xA44A0268, # SH T2, 0x0268 (V0)
+ 0x340A0001, # ORI T2, R0, 0x0001 <- Bucket
+ 0xA44A0258, # SH T2, 0x0258 (V0)
+ 0x240B0005, # ADDIU T3, R0, 0x0005
+ 0xA04B0150, # SB T3, 0x0150 (V0)
+ 0x24090011, # ADDIU T1, R0, 0x0011
+ 0x1139FFB0, # BEQ T1, T9, [backward 0x50]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ # Castle Center factory floor (replaces 1 moneybag, 1 jewel, and gives every lizard man coffin item a unique flag)
+ 0x2409000B, # ADDIU T1, R0, 0x000B
+ 0x15090016, # BNE T0, T1, [forward 0x16]
+ 0x340A001A, # ORI T2, R0, 0x001A <- Lizard coffin nearside mid-right
+ 0x340B0003, # ORI T3, R0, 0x0003 <- Lizard coffin nearside mid-left
+ 0xA44A00C8, # SH T2, 0x00C8 (V0)
+ 0xA44B00D8, # SH T3, 0x00D8 (V0)
+ 0x240A1000, # ADDIU T2, R0, 0x1000
+ 0x240B2000, # ADDIU T3, R0, 0x2000
+ 0x240C0400, # ADDIU T4, R0, 0x0400
+ 0x240D0800, # ADDIU T5, R0, 0x0800
+ 0x240E0200, # ADDIU T6, R0, 0x0200
+ 0x240F0100, # ADDIU T7, R0, 0x0100
+ 0xA44A009A, # SH T2, 0x009A (V0)
+ 0xA44B00AA, # SH T3, 0x00AA (V0)
+ 0xA44C00CA, # SH T4, 0x00CA (V0)
+ 0xA44D00BA, # SH T5, 0x00BA (V0)
+ 0xA44E00DA, # SH T6, 0x00DA (V0)
+ 0xA44F00EA, # SH T7, 0x00EA (V0)
+ 0x340A0017, # ORI T2, R0, 0x0017 <- Lizard coffin nearside mid-right
+ 0x340B000C, # ORI T3, R0, 0x000C <- Lizard coffin nearside mid-left
+ 0xA44A00A8, # SH T2, 0x00C8 (V0)
+ 0xA44B00E8, # SH T3, 0x00D8 (V0)
+ 0x03E00008, # JR RA
+ 0x00000000, # NOP
+ # Duel Tower (replaces a flame on top of a rotating lion pillar with a White Jewel on the invisible bridge ledge)
+ 0x24090013, # ADDIU T1, R0, 0x0013
+ 0x1509000F, # BNE T0, T1, [forward 0x0F]
+ 0x3C0A00B9, # LUI T2, 0x00BB
+ 0x254A012B, # ADDIU T2, T2, 0x012B
+ 0x3C0BFE2A, # LUI T3, 0xFE2A
+ 0x256B0027, # ADDIU T3, T3, 0x0027
+ 0x3C0C0001, # LUI T4, 0x0001
+ 0x3C0D0022, # LUI T5, 0x0022
+ 0x25AD0100, # ADDIU T5, T5, 0x0100
+ 0xAC4A0A80, # SW T2, 0x0AE0 (V0)
+ 0xAC4B0A84, # SW T3, 0x0AE4 (V0)
+ 0xAC4C0A88, # SW T4, 0x0AE8 (V0)
+ 0xAC4D0A8C, # SW T5, 0x0AEC (V0)
+ 0x24090001, # ADDIU T1, R0, 0x0001
+ 0x1139FF87, # BEQ T1, T9, [backward 0x77]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ # Castle Keep outside (replaces 1 invisible Healing Kit and gives both invisible Healing Kits pickup flags)
+ 0x24090014, # ADDIU T1, R0, 0x0014
+ 0x1509000A, # BNE T0, T1, [forward 0x0A]
+ 0x340A0001, # ORI T2, R0, 0x0001 <- Right flame
+ 0xA44A0058, # SH T2, 0x0058 (V0)
+ 0x240A0001, # ADDIU T2, R0, 0x0001
+ 0x240B0002, # ADDIU T3, R0, 0x0002
+ 0xA44A004A, # SH T2, 0x004A (V0)
+ 0xA44B005A, # SH T3, 0x005A (V0)
+ 0x24090002, # ADDIU T1, R0, 0x0002
+ 0x1139FF7B, # BEQ T0, T1, [backward 0x74]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ # Castle Wall main area (sets a flag for the freestanding Holy Water if applicable and the "beginning of stage"
+ # state if entered from the rear)
+ 0x24090002, # ADDIU T1, R0, 0x0002
+ 0x15090006, # BNE T0, T1, [forward 0x06]
+ 0x24090000, # ADDIU T1, R0, 0x0000
+ 0xA049009B, # SB T1, 0x009B (V0)
+ 0x24090010, # ADDIU T1, R0, 0x0010
+ 0x1139FF73, # BEQ T1, T9, [backward 0x8D]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ # Villa vampire crypt (sets the "beginning of stage" state if entered from the rear, as well as the "can warp here"
+ # flag if arriving for the first time)
+ 0x2409001A, # ADDIU T1, R0, 0x001A
+ 0x15090008, # BNE T0, T1, [forward 0x08]
+ 0x3C0A8039, # LUI T2, 0x8039
+ 0x914B9C1C, # LBU T3, 0x9C1C (T2)
+ 0x356B0001, # ORI T3, T3, 0x0001
+ 0xA14B9C1C, # SB T3, 0x9C1C (T2)
+ 0x24090003, # ADDIU T1, R0, 0x0003
+ 0x1139FF69, # BEQ T1, T9, [backward 0x98]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ # Underground Waterway (sets the "beginning of stage" state if entered from the rear)
+ 0x24090008, # ADDIU T1, R0, 0x0008
+ 0x15090004, # BNE T0, T1, [forward 0x04]
+ 0x24090001, # ADDIU T1, R0, 0x0001
+ 0x1139FF63, # BEQ T1, T9, [backward 0x9F]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ # Castle Center elevator top (sets the "beginning of stage" state if entered from either rear, as well as the "can
+ # warp here" flag if arriving for the first time)
+ 0x2409000F, # ADDIU T1, R0, 0x000F
+ 0x1509000A, # BNE T0, T1, [forward 0x0A]
+ 0x3C0A8039, # LUI T2, 0x8039
+ 0x914B9C1C, # LBU T3, 0x9C1C (T2)
+ 0x356B0002, # ORI T3, T3, 0x0002
+ 0xA14B9C1C, # SB T3, 0x9C1C (T2)
+ 0x24090002, # ADDIU T1, R0, 0x0002
+ 0x1139FF59, # BEQ T1, T9, [backward 0xAA]
+ 0x24090003, # ADDIU T1, R0, 0x0003
+ 0x1139FF57, # BEQ T1, T9, [backward 0xAC]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ # Tower of Execution (sets the "beginning of stage" state if entered from the rear)
+ 0x24090010, # ADDIU T1, R0, 0x0010
+ 0x15090004, # BNE T0, T1, [forward 0x10]
+ 0x24090012, # ADDIU T1, R0, 0x0012
+ 0x1139FF51, # BEQ T1, T9, [backward 0xAF]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ # Tower of Sorcery (sets the "beginning of stage" state if entered from the rear)
+ 0x24090011, # ADDIU T1, R0, 0x0011
+ 0x15090004, # BNE T0, T1, [forward 0x04]
+ 0x24090013, # ADDIU T1, R0, 0x0013
+ 0x1139FF4B, # BEQ T1, T9, [backward 0xBA]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ # Tower of Science (sets the "beginning of stage" state if entered from the rear)
+ 0x24090012, # ADDIU T1, R0, 0x0012
+ 0x15090004, # BNE T0, T1, [forward 0x04]
+ 0x24090004, # ADDIU T1, R0, 0x0004
+ 0x1139FF45, # BEQ T1, T9, [backward 0xC1]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ # Room of Clocks (changes 2 candle settings if applicable and sets the "begging of stage" state if spawning at end)
+ 0x2409001B, # ADDIU T1, R0, 0x001B
+ 0x15090008, # BNE T0, T1, [forward 0x08]
+ 0x24090006, # ADDIU T1, R0, 0x0006
+ 0x240A0006, # ADDIU T2, R0, 0x0006
+ 0xA0490059, # SB T1, 0x0059 (V0)
+ 0xA04A0069, # SB T2, 0x0069 (V0)
+ 0x24090014, # ADDIU T1, R0, 0x0014
+ 0x1139FF3B, # BEQ T1, T9, [backward 0xCC]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ # Castle Center basement (changes 2 non-pickup-able Mandragoras into 2 real items and moves the torture shelf item
+ # forward slightly if it's turned visible)
+ 0x24090009, # ADDIU T1, R0, 0x0009
+ 0x15090011, # BNE T0, T1, [forward 0x11]
+ 0x3409FFFC, # ORI T1, R0, 0xFFFC
+ 0xA44907C0, # SH T1, 0x07C0 (V0)
+ 0xA44907D0, # SH T1, 0x07D0 (V0)
+ 0x240A0027, # ADDIU T2, R0, 0x0027
+ 0xA44A07C6, # SH T2, 0x07C6 (V0)
+ 0xA44A07D6, # SH T2, 0x07D6 (V0)
+ 0x340B0001, # ORI T3, R0, 0x0001 <- Right Mandragora
+ 0x340C0001, # ORI T4, R0, 0x0001 <- Left Mandragora
+ 0xA44B07C8, # SH T3, 0x07C8 (V0)
+ 0xA44C07D8, # SH T4, 0x07D8 (V0)
+ 0x240D00F5, # ADDIU T5, R0, 0x00F5
+ 0xA04D06D1, # SB T5, 0x06D1 (V0)
+ 0x24090040, # ADDIU T1, R0, 0x0040
+ 0x240A0080, # ADDIU T2, R0, 0x0080
+ 0xA04907CA, # SB T1, 0x07CA (V0)
+ 0xA04A07DA, # SB T2, 0x07DA (V0)
+ 0x03E00008, # JR RA
+ # Castle Center nitro area (changes 2 non-pickup-able Nitros into 2 real items)
+ 0x2409000E, # ADDIU T1, R0, 0x000E
+ 0x15090015, # BNE T0, T1, [forward 0x15]
+ 0x240900C0, # ADDIU T1, R0, 0x00C0
+ 0x240A00CE, # ADDIU T2, R0, 0x00CE
+ 0xA0490471, # SB T1, 0x0471 (V0)
+ 0xA04A04A1, # SB T2, 0x04A1 (V0)
+ 0x24090027, # ADDIU T1, R0, 0x0027
+ 0xA4490476, # SH T1, 0x0476 (V0)
+ 0xA44904A6, # SH T1, 0x04A6 (V0)
+ 0x340A0001, # ORI T2, R0, 0x0001 <- Invention-side shelf
+ 0x340B0001, # ORI T3, R0, 0x0001 <- Heinrich-side shelf
+ 0xA44A0478, # SH T2, 0x0478 (V0)
+ 0xA44B04A8, # SH T3, 0x04A8 (V0)
+ 0x24090080, # ADDIU T1, R0, 0x0080
+ 0xA049047A, # SB T1, 0x047A (V0)
+ 0xA440047C, # SH R0, 0x047C (V0)
+ 0x240A0400, # ADDIU T2, R0, 0x0400
+ 0x340BFF05, # ORI T3, R0, 0xFF05
+ 0xA44A04AA, # SH T2, 0x04AA (V0)
+ 0xA44B04AC, # SH T3, 0x04AC (V0)
+ 0x24090046, # ADDIU T1, R0, 0x0046
+ 0xA04904A3, # SB T1, 0x04A3 (V0)
+ 0x03E00008, # JR RA
+ # Fan meeting room (sets "beginning of stage" flag)
+ 0x24090019, # ADDIU T1, R0, 0x0019
+ 0x1109FF0D, # BEQ T1, T9, [backward 0xFB]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+]
+
+renon_cutscene_checker = [
+ # Prevents Renon's departure/pre-fight cutscene from playing if the player is either in the escape sequence or both
+ # did not spend the required 30K to fight him and lacks the required Special2s to fight Dracula.
+ 0x15810002, # BNE T4, AT, [forward 0x02]
+ 0x00000000, # NOP
+ 0x08049EB3, # J 0x80127ACC
+ 0x24090016, # ADDIU T1, R0, 0x0016
+ 0x11C90002, # BEQ T6, T1, [forward 0x02]
+ 0x00000000, # NOP
+ 0x08049ECA, # J 0x80127B28
+ 0x24190000, # ADDIU T9, R0, 0x0000
+ 0x8C696208, # LW T1, 0x6208 (V1)
+ 0x292A7531, # SLTI T2, T1, 0x7531
+ 0x51400001, # BEQZL T2, [forward 0x01]
+ 0x24190001, # ADDIU T9, R0, 0x0001
+ 0x3C0B8013, # LUI T3, 0x8013
+ 0x916BAC9F, # LBU T3, 0xAC9F (T3)
+ 0x906C6194, # LBU T4, 0x6194 (V1)
+ 0x018B502A, # SLT T2, T4, T3
+ 0x51400001, # BEQZL T2, [forward 0x01]
+ 0x24190001, # ADDIU T9, R0, 0x0001
+ 0x90696142, # LBU T1, 0x6142 (V1)
+ 0x31290002, # ANDI T1, T1, 0x0002
+ 0x55200001, # BNEZL T1, [forward 0x01]
+ 0x24190000, # ADDIU T9, R0, 0x0000
+ 0x17200003, # BNEZ T9, [forward 0x03]
+ 0x00000000, # NOP
+ 0x08049ECC, # J 0x80127B30
+ 0x00000000, # NOP
+ 0x08049ECA # J 0x80127B28
+]
+
+renon_cutscene_checker_jr = [
+ # Like renon_cutscene_checker, but without the checks for the Special2 and spent money counters. Inserted instead if
+ # the player chooses to guarantee or disable the Renon fight on their YAML.
+ 0x15810002, # BNE T4, AT, [forward 0x02]
+ 0x00000000, # NOP
+ 0x08049EB3, # J 0x80127ACC
+ 0x24090016, # ADDIU T1, R0, 0x0016
+ 0x11C90002, # BEQ T6, T1, [forward 0x02]
+ 0x00000000, # NOP
+ 0x08049ECA, # J 0x80127B28
+ 0x24190001, # ADDIU T9, R0, 0x0001
+ 0x90696142, # LBU T1, 0x6142 (V1)
+ 0x31290002, # ANDI T1, T1, 0x0002
+ 0x55200001, # BNEZL T1, [forward 0x01]
+ 0x24190000, # ADDIU T9, R0, 0x0000
+ 0x17200003, # BNEZ T9, [forward 0x03]
+ 0x00000000, # NOP
+ 0x08049ECC, # J 0x80127B30
+ 0x00000000, # NOP
+ 0x08049ECA # J 0x80127B28
+]
+
+ck_door_music_player = [
+ # Plays Castle Keep's song if you spawn in front of Dracula's door (teleporting via the warp menu) and haven't
+ # started the escape sequence yet.
+ 0x17010002, # BNE T8, AT, [forward 0x02]
+ 0x00000000, # NOP
+ 0x08063DF9, # J 0x8018F7E4
+ 0x240A0000, # ADDIU T2, R0, 0x0000
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91089BFA, # LBU T0, 0x9BFA (T0)
+ 0x31080002, # ANDI T0, T0, 0x0002
+ 0x51090001, # BEQL T0, T1, [forward 0x01]
+ 0x254A0001, # ADDIU T2, T2, 0x0001
+ 0x24080003, # ADDIU T0, R0, 0x0003
+ 0x51180001, # BEQL T0, T8, [forward 0x01]
+ 0x254A0001, # ADDIU T2, T2, 0x0001
+ 0x240B0002, # ADDIU T3, R0, 0x0002
+ 0x114B0002, # BEQ T2, T3, [forward 0x02]
+ 0x00000000, # NOP
+ 0x08063DFD, # J 0x8018F7F4
+ 0x00000000, # NOP
+ 0x08063DF9 # J 0x8018F7E4
+]
+
+dracula_door_text_redirector = [
+ # Switches the standard pointer to the map text with one to a custom message for Dracula's chamber door if the
+ # current scene is Castle Keep exterior (Scene 0x14).
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91089EE1, # LBU T0, 0x9EE1 (T0)
+ 0x24090014, # ADDIU T1, R0, 0x0014
+ 0x15090006, # BNE T0, T1, [forward 0x06]
+ 0x3C088014, # LUI T0, 0x8014
+ 0x2508B9F4, # ADDIU T0, T0, 0xB9F4
+ 0x151F0003, # BNE T0, RA, [forward 0x03]
+ 0x00000000, # NOP
+ 0x3C028040, # LUI V0, 0x8040
+ 0x2442CC48, # ADDIU V0, V0, 0xCC48
+ 0x03E00008 # JR RA
+]
+
+coffin_time_checker = [
+ # When entering the Villa coffin, this will check to see whether it's day or night and send you to either the Tunnel
+ # or Underground Waterway level slot accordingly regardless of which character you are
+ 0x28490006, # SLTI T1, V0, 0x0006
+ 0x15200005, # BNEZ T1, [forward 0x05]
+ 0x28490012, # SLTI T1, V0, 0x0012
+ 0x11200003, # BEQZ T1, [forward 0x03]
+ 0x00000000, # NOP
+ 0x08055AEB, # J 0x80156BAC
+ 0x00000000, # NOP
+ 0x08055AED # J 0x80156BB4
+]
+
+werebull_flag_unsetter = [
+ # This will un-set Were-bull's defeat flag in Duel Tower after beating him so that the check above his arena can
+ # still be acquired later, if it hasn't been acquired already. This is the only check in the entire game that can be
+ # permanently missed even with the ability to return to levels.
+ 0x3C0E0400, # LUI T6, 0x0400
+ 0x15CF0006, # BNE T6, T7, [forward 0x06]
+ 0x00187402, # SRL T6, T8, 16
+ 0x31CE2000, # ANDI T6, T6, 0x2000
+ 0x15C00003, # BNEZ T6, [forward 0x03]
+ 0x3C0E0020, # LUI T6, 0x0020
+ 0x014E5025, # OR T2, T2, T6
+ 0xAC4A613C, # SW T2, 0x613C (V0)
+ 0x03200008 # JR T9
+]
+
+werebull_flag_unsetter_special2_electric_boogaloo = [
+ # Like werebull_flag_unsetter, but with the added feature of awarding a Special2 after determining the player isn't
+ # trying to beat Were-bull twice! This will be inserted over the former if the goal is set to boss hunt.
+ 0x3C0E0400, # LUI T6, 0x0400
+ 0x15CF0008, # BNE T6, T7, [forward 0x06]
+ 0x00187402, # SRL T6, T8, 16
+ 0x31CE2000, # ANDI T6, T6, 0x2000
+ 0x15C00005, # BNEZ T6, [forward 0x05]
+ 0x3C0E0020, # LUI T6, 0x0020
+ 0x014EC024, # AND T8, T2, T6
+ 0x014E5025, # OR T2, T2, T6
+ 0xAC4A613C, # SW T2, 0x613C (V0)
+ 0x17000003, # BNEZ T8, [forward 0x03]
+ 0x3C188039, # LUI T8, 0x8039
+ 0x240E0005, # ADDIU T6, R0, 0x0005
+ 0xA30E9BDF, # SB T6, 0x9BDF (T8)
+ 0x03200008 # JR T9
+]
+
+werebull_flag_pickup_setter = [
+ # Checks to see if an item being picked up is the one on top of Were-bull's arena. If it is, then it'll check to see
+ # if our makeshift "Were-bull defeated once" flag and, if it is, set Were-bull's arena flag proper, so it'll
+ # permanently stay down.
+ 0x3C088038, # LUI T0, 0x8038
+ 0x25083AC8, # ADDIU T0, T0, 0x3AC8
+ 0x15020007, # BNE T0, V0, [forward 0x07]
+ 0x3C082000, # LUI T0, 0x2000
+ 0x15040005, # BNE T0, A0, [forward 0x05]
+ 0x9449612C, # LHU T1, 0x612C (V0)
+ 0x31290020, # ANDI T1, T1, 0x0020
+ 0x11200002, # BEQZ T1, [forward 0x02]
+ 0x3C0A0400, # LUI T2, 0x0400
+ 0x014D6825, # OR T5, T2, T5
+ 0xAC4D612C, # SW T5, 0x612C (V0)
+ 0x03E00008 # JR RA
+]
+
+boss_special2_giver = [
+ # Enables the rewarding of Special2s upon the vanishing of a boss's health bar when defeating it.
+
+ # Also sets a flag in the case of the Castle Wall White Dragons' health bar going away. Their defeat flag in vanilla
+ # is tied to hitting the lever after killing them, so this alternate flag is used to track them for the "All Bosses"
+ # goal in the event someone kills them and then warps out opting to not be a Konami pachinko champ.
+ 0x3C118035, # LUI S1, 0x8035
+ 0x962DF834, # LHU T5, 0xF834 (S1)
+ 0x240E3F73, # ADDIU T6, R0, 0x3F73
+ 0x15AE0012, # BNE T5, T6, [forward 0x12]
+ 0x3C118039, # LUI S1, 0x8039
+ 0x922D9EE1, # LBU T5, 0x9EE1 (S1)
+ 0x240E0013, # ADDIU T6, R0, 0x0013
+ 0x11AE000E, # BEQ T5, T6, [forward 0x0E]
+ 0x922F9BFA, # LBU T7, 0x9BFA (S1)
+ 0x31EF0001, # ANDI T7, T7, 0x0001
+ 0x15E0000B, # BNEZ T7, [forward 0x0B]
+ 0x240E0002, # ADDIU T6, R0, 0x0002
+ 0x15AE0006, # BNE T5, T6, [forward 0x06]
+ 0x00000000, # NOP
+ 0x862F9BF4, # LH T7, 0x9BF4 (S1)
+ 0x31ED0080, # ANDI T5, T7, 0x0080
+ 0x15A00005, # BNEZ T5, [forward 0x05]
+ 0x35EF0080, # ORI T7, T7, 0x0080
+ 0xA62F9BF4, # SH T7, 0x9BF4 (S1)
+ 0x240D0005, # ADDIU T5, R0, 0x0005
+ 0xA22D9BDF, # SB T5, 0x9BDF (S1)
+ 0xA22D9BE0, # SB T5, 0x9BE0 (S1)
+ 0x03E00008 # JR RA
+]
+
+boss_goal_checker = [
+ # Checks each boss flag to see if every boss with a health meter has been defeated and puts 0x0004 in V0 to
+ # disallow opening Dracula's door if not all have been.
+ 0x3C0A8039, # LUI T2, 0x8039
+ 0x954B9BF4, # LHU T3, 0x9BF4 (T2)
+ 0x316D0BA0, # ANDI T5, T3, 0x0BA0
+ 0x914B9BFB, # LBU T3, 0x9BFB (T2)
+ 0x000B6182, # SRL T4, T3, 6
+ 0x11800010, # BEQZ T4, [forward 0x10]
+ 0x240C00C0, # ADDIU T4, R0, 0x00C0
+ 0x01AC6821, # ADDU T5, T5, T4
+ 0x914B9BFD, # LBU T3, 0x9BFD (T2)
+ 0x316C0020, # ANDI T4, T3, 0x0020
+ 0x01AC6821, # ADDU T5, T5, T4
+ 0x914B9BFE, # LBU T3, 0x9BFE (T2)
+ 0x316C0010, # ANDI T4, T3, 0x0010
+ 0x01AC6821, # ADDU T5, T5, T4
+ 0x914B9C18, # LBU T3, 0x9C18 (T2)
+ 0x316C0010, # ANDI T4, T3, 0x0010
+ 0x01AC6821, # ADDU T5, T5, T4
+ 0x914B9C1B, # LBU T3, 0x9C1B (T2)
+ 0x000B6102, # SRL T4, T3, 4
+ 0x11800005, # BEQZ T4, [forward 0x05]
+ 0x240C0050, # ADDIU T4, R0, 0x0050
+ 0x01AC6821, # ADDU T5, T5, T4
+ 0x240E0CF0, # ADDIU T6, R0, 0x0CF0
+ 0x55CD0001, # BNEL T6, T5, [forward 0x01]
+ 0x24020004, # ADDIU V0, R0, 0x0004
+ 0x03E00008 # JR RA
+]
+
+special_goal_checker = [
+ # Checks the Special2 counter to see if the specified threshold has been reached and puts 0x0001 in V0 to disallow
+ # opening Dracula's door if it hasn't been.
+ 0x3C0A8039, # LUI T2, 0x8039
+ 0x914B9C4C, # LBU T3, 0x9C4C (T2)
+ 0x296A001E, # SLTI T2, T3, 0x001E
+ 0x55400001, # BNEZL T2, 0x8012AC8C
+ 0x24020001, # ADDIU V0, R0, 0x0001
+ 0x03E00008 # JR RA
+]
+
+warp_menu_rewrite = [
+ # Rewrite to the warp menu code to ensure each option can have its own scene ID, spawn ID, and fade color.
+ # Start Warp
+ 0x3C0E0000, # LUI T6, 0x0000
+ 0x25CE0000, # ADDIU T6, T6, 0x0000
+ 0x1000001F, # B [forward 0x1F]
+ 0x3C0F8000, # LUI T7, 0x8000
+ # Warp 1
+ 0x3C0E0000, # LUI T6, 0x0000
+ 0x25CE0000, # ADDIU T6, T6, 0x0000
+ 0x1000001B, # B [forward 0x1B]
+ 0x3C0F8040, # LUI T7, 0x8040
+ # Warp 2
+ 0x3C0E0000, # LUI T6, 0x0000
+ 0x25CE0000, # ADDIU T6, T6, 0x0000
+ 0x10000017, # B [forward 0x17]
+ 0x3C0F8080, # LUI T7, 0x8080
+ # Warp 3
+ 0x3C0E0000, # LUI T6, 0x0000
+ 0x25CE0000, # ADDIU T6, T6, 0x0000
+ 0x10000013, # B [forward 0x13]
+ 0x3C0F0080, # LUI T7, 0x0080
+ # Warp 4
+ 0x3C0E0000, # LUI T6, 0x0000
+ 0x25CE0000, # ADDIU T6, T6, 0x0000
+ 0x3C0F0080, # LUI T7, 0x0080
+ 0x1000000E, # B [forward 0x0E]
+ 0x25EF8000, # ADDIU T7, T7, 0x8000
+ # Warp 5
+ 0x3C0E0000, # LUI T6, 0x0000
+ 0x25CE0000, # ADDIU T6, T6, 0x0000
+ 0x1000000A, # B [forward 0x0A]
+ 0x340F8000, # ORI T7, R0, 0x8000
+ # Warp 6
+ 0x3C0E0000, # LUI T6, 0x0000
+ 0x25CE0000, # ADDIU T6, T6, 0x0000
+ 0x3C0F8000, # LUI T7, 0x8000
+ 0x10000005, # B [forward 0x05]
+ 0x35EF8000, # ORI T7, T7, 0x8000
+ # Warp 7
+ 0x3C0E0000, # LUI T6, 0x0000
+ 0x25CE0000, # ADDIU T6, T6, 0x0000
+ 0x3C0F8040, # LUI T7, 0x8040
+ 0x35EF8000, # ORI T7, T7, 0x8000
+ # Warp Crypt
+ 0x3C18800D, # LUI T8, 0x800D
+ 0x97185E20, # LHU T8, 0x5E20 (T8)
+ 0x24192000, # ADDIU T9, R0, 0x2000
+ 0x17190009, # BNE T8, T9, [forward 0x09]
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91089C1C, # LBU T0, 0x9C1C (T0)
+ 0x31080001, # ANDI T0, T0, 0x0001
+ 0x1100000F, # BEQZ T0, [forward 0x0F]
+ 0x00000000, # NOP
+ 0x3C0E001A, # LUI T6, 0x001A
+ 0x25CE0003, # ADDIU T6, T6, 0x0003
+ 0x1000000B, # B [forward 0x0B]
+ 0x240F0000, # ADDIU T7, R0, 0x0000
+ # Warp Elevator
+ 0x24190010, # ADDIU T9, R0, 0x0010
+ 0x17190008, # BNE T8, T9, [forward 0x08]
+ 0x91089C1C, # LBU T0, 0x9C1C (T0)
+ 0x31080002, # ANDI T0, T0, 0x0002
+ 0x11000005, # BEQZ T0, [forward 0x05]
+ 0x00000000, # NOP
+ 0x3C0E000F, # LUI T6, 0x000F
+ 0x25CE0001, # ADDIU T6, T6, 0x0001
+ 0x3C0F8080, # LUI T7, 0x8080
+ 0x35EF8000, # ORI T7, T7, 0x8000
+ # All
+ 0xAC6E6428, # SW T6, 0x6428 (V1)
+ 0xAC6F642C, # SW T7, 0x642C (V1)
+ 0x2402001E, # ADDIU V0, R0, 0x001E
+ 0xA4626430, # SH V0, 0x6430 (V1)
+ 0xA4626432, # SH V0, 0x6432 (V1)
+]
+
+warp_pointer_table = [
+ # Changed pointer table addresses to go with the warp menu rewrite
+ 0x8012AD74,
+ 0x8012AD84,
+ 0x8012AD94,
+ 0x8012ADA4,
+ 0x8012ADB4,
+ 0x8012ADC8,
+ 0x8012ADD8,
+ 0x8012ADEC,
+]
+
+spawn_coordinates_extension = [
+ # Checks if the 0x10 bit is set in the spawn ID and references the below list of custom spawn coordinates if it is.
+ 0x316A0010, # ANDI T2, T3, 0x0010
+ 0x11400003, # BEQZ T2, [forward 0x03]
+ 0x8CD90008, # LW T9, 0x0008 (A2)
+ 0x3C198040, # LUI T9, 0x8040
+ 0x2739C2CC, # ADDIU T9, T9, 0xC2CC
+ 0x08054A83, # J 0x80152A0C
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+
+ # Castle Wall end: 10
+ # player camera focus point
+ # x = 0xFFFF 0xFFFF 0xFFFF
+ # y = 0x0003 0x0012 0x000D
+ # z = 0xFFF3 0xEDFF 0xFFF3
+ # r = 0xC000
+ 0x0000FFFF,
+ 0x0003FFF3,
+ 0xC000FFFF,
+ 0x0012FFED,
+ 0xFFFF000D,
+ 0xFFF30000,
+
+ # Tunnel end: 11
+ # player camera focus point
+ # x = 0x0088 0x0087 0x0088
+ # y = 0x01D6 0x01F1 0x01E5
+ # z = 0xF803 0xF7D2 0xF803
+ # r = 0xC000
+ 0x008801D6,
+ 0xF803C000,
+ 0x008701F1,
+ 0xF7D20088,
+ 0x01E5F803,
+
+ # Tower of Execution end: 12
+ # player camera focus point
+ # x = 0x00AC 0x00EC 0x00AC
+ # y = 0x0154 0x0183 0x0160
+ # z = 0xFE8F 0xFE8F 0xFE8F
+ # r = 0x8000
+ 0x000000AC,
+ 0x0154FE8F,
+ 0x800000EC,
+ 0x0183FE8F,
+ 0x00AC0160,
+ 0xFE8F0000,
+
+ # Tower of Sorcery end: 13
+ # player camera focus point
+ # x = 0xFEB0 0xFE60 0xFEB0
+ # y = 0x0348 0x036D 0x0358
+ # z = 0xFEFB 0xFEFB 0xFEFB
+ # r = 0x0000
+ 0xFEB00348,
+ 0xFEFB0000,
+ 0xFE60036D,
+ 0xFEFBFEB0,
+ 0x0358FEFB,
+
+ # Room of Clocks end: 14
+ # player camera focus point
+ # x = 0x01B1 0x01BE 0x01B1
+ # y = 0x0006 0x001B 0x0015
+ # z = 0xFFCD 0xFFCD 0xFFCD
+ # r = 0x8000
+ 0x000001B1,
+ 0x0006FFCD,
+ 0x800001BE,
+ 0x001BFFCD,
+ 0x01B10015,
+ 0xFFCD0000,
+
+ # Duel Tower savepoint: 15
+ # player camera focus point
+ # x = 0x00B9 0x00B9 0x00B9
+ # y = 0x012B 0x0150 0x0138
+ # z = 0xFE20 0xFE92 0xFE20
+ # r = 0xC000
+ 0x00B9012B,
+ 0xFE20C000,
+ 0x00B90150,
+ 0xFE9200B9,
+ 0x0138FE20
+]
+
+waterway_end_coordinates = [
+ # Underground Waterway end: 01
+ # player camera focus point
+ # x = 0x0397 0x03A1 0x0397
+ # y = 0xFFC4 0xFFDC 0xFFD3
+ # z = 0xFDB9 0xFDB8 0xFDB9
+ # r = 0x8000
+ 0x00000397,
+ 0xFFC4FDB9,
+ 0x800003A1,
+ 0xFFDCFDB8,
+ 0x0397FFD3,
+ 0xFDB90000
+]
+
+continue_cursor_start_checker = [
+ # This is used to improve the Game Over screen's "Continue" menu by starting the cursor on whichever checkpoint
+ # is most recent instead of always on "Previously saved". If a menu has a cursor start value of 0xFF in its text
+ # data, this will read the byte at 0x80389BC0 to determine which option to start the cursor on.
+ 0x8208001C, # LB T0, 0x001C(S0)
+ 0x05010003, # BGEZ T0, [forward 0x03]
+ 0x3C098039, # LUI T1, 0x8039
+ 0x81289BC0, # LB T0, 0x9BC0 (T1)
+ 0xA208001C, # SB T0, 0x001C (S0)
+ 0x03E00008 # JR RA
+]
+
+savepoint_cursor_updater = [
+ # Sets the value at 0x80389BC0 to 0x00 after saving to let the Game Over screen's "Continue" menu know to start the
+ # cursor on "Previously saved" as well as updates the entrance variable for B warping. It then jumps to
+ # deathlink_counter_decrementer in the event we're loading a save from the Game Over screen.
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91099C95, # LBU T1, 0x9C95 (T0)
+ 0x000948C0, # SLL T1, T1, 3
+ 0x3C0A8018, # LUI T2, 0x8018
+ 0x01495021, # ADDU T2, T2, T1
+ 0x914B17CF, # LBU T3, 0x17CF (T2)
+ 0xA10B9EE3, # SB T3, 0x9EE3 (T0)
+ 0xA1009BC0, # SB R0, 0x9BC0 (T0)
+ 0x080FF8F0 # J 0x803FE3C0
+]
+
+stage_start_cursor_updater = [
+ # Sets the value at 0x80389BC0 to 0x01 after entering a stage to let the Game Over screen's "Continue" menu know to
+ # start the cursor on "Restart this stage".
+ 0x3C088039, # LUI T0, 0x8039
+ 0x24090001, # ADDIU T1, R0, 0x0001
+ 0xA1099BC0, # SB T1, 0x9BC0 (T0)
+ 0x03E00008 # JR RA
+]
+
+elevator_flag_checker = [
+ # Prevents the top elevator in Castle Center from activating if the bottom elevator switch is not turned on.
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91089C07, # LBU T0, 0x9C07 (T0)
+ 0x31080002, # ANDI T0, T0, 0x0002
+ 0x15000002, # BNEZ T0, [forward 0x02]
+ 0x848E004C, # LH T6, 0x004C (A0)
+ 0x240E0000, # ADDIU T6, R0, 0x0000
+ 0x03E00008 # JR RA
+]
+
+crystal_special2_giver = [
+ # Gives a Special2 upon activating the big crystal in CC basement.
+ 0x3C098039, # LUI T1, 0x8039
+ 0x24190005, # ADDIU T9, R0, 0x0005
+ 0xA1399BDF, # SB T9, 0x9BDF (T1)
+ 0x03E00008, # JR RA
+ 0x3C198000 # LUI T9, 0x8000
+]
+
+boss_save_stopper = [
+ # Prevents usage of a White Jewel if in a boss fight. Important for the lizard-man trio in Waterway as escaping
+ # their fight by saving/reloading can render a Special2 permanently missable.
+ 0x24080001, # ADDIU T0, R0, 0x0001
+ 0x15030005, # BNE T0, V1, [forward 0x05]
+ 0x3C088035, # LUI T0, 0x8035
+ 0x9108F7D8, # LBU T0, 0xF7D8 (T0)
+ 0x24090020, # ADDIU T1, R0, 0x0020
+ 0x51090001, # BEQL T0, T1, [forward 0x01]
+ 0x24020000, # ADDIU V0, R0, 0x0000
+ 0x03E00008 # JR RA
+]
+
+music_modifier = [
+ # Uses the ID of a song about to be played to pull a switcheroo by grabbing a new ID from a custom table to play
+ # instead. A hacky way to circumvent song IDs in the compressed overlays' "play song" function calls, but it works!
+ 0xAFBF001C, # SW RA, 0x001C (SP)
+ 0x0C004A6B, # JAL 0x800129AC
+ 0x44800000, # MTC1 R0, F0
+ 0x10400003, # BEQZ V0, [forward 0x03]
+ 0x3C088040, # LUI T0, 0x8040
+ 0x01044821, # ADDU T1, T0, A0
+ 0x9124CD20, # LBU A0, 0xCD20 (T1)
+ 0x08004E64 # J 0x80013990
+]
+
+music_comparer_modifier = [
+ # The same as music_modifier, but for the function that compares the "song to play" ID with the one that's currently
+ # playing. This will ensure the randomized music doesn't reset when going through a loading zone in Villa or CC.
+ 0x3C088040, # LUI T0, 0x8040
+ 0x01044821, # ADDU T1, T0, A0
+ 0x9124CD20, # LBU A0, 0xCD20 (T1)
+ 0x08004A60, # J 0x80012980
+]
+
+item_customizer = [
+ # Allows changing an item's appearance settings and visibility independent of what it actually is as well as setting
+ # its bitflag literally anywhere in the save file by changing things in the item actor's data as it's being created
+ # for the below three functions to then utilize.
+ 0x03205825, # OR T3, T9, R0
+ 0x000B5A02, # SRL T3, T3, 8
+ 0x316C0080, # ANDI T4, T3, 0x0080
+ 0xA0CC0041, # SB T4, 0x0041 (A2)
+ 0x016C5823, # SUBU T3, T3, T4
+ 0xA0CB0040, # SB T3, 0x0040 (A2)
+ 0x333900FF, # ANDI T9, T9, 0x00FF
+ 0xA4D90038, # SH T9, 0x0038 (A2)
+ 0x8CCD0058, # LW T5, 0x0058 (A2)
+ 0x31ACFF00, # ANDI T4, T5, 0xFF00
+ 0x340EFF00, # ORI T6, R0, 0xFF00
+ 0x158E000A, # BNE T4, T6, [forward 0x0A]
+ 0x31AC00FF, # ANDI T4, T5, 0x00FF
+ 0x240E0002, # ADDIU T6, R0, 0x0002
+ 0x018E001B, # DIVU T4, T6
+ 0x00006010, # MFHI T4
+ 0x000D5C02, # SRL T3, T5, 16
+ 0x51800001, # BEQZL T4, [forward 0x01]
+ 0x000B5C00, # SLL T3, T3, 16
+ 0x00006012, # MFLO T4
+ 0xA0CC0055, # SB T4, 0x0055 (A2)
+ 0xACCB0058, # SW T3, 0x0058 (A2)
+ 0x080494E5, # J 0x80125394
+ 0x032A0019 # MULTU T9, T2
+]
+
+item_appearance_switcher = [
+ # Determines an item's model appearance by checking to see if a different item appearance ID was written in a
+ # specific spot in the actor's data; if one wasn't, then the appearance value will be grabbed from the item's entry
+ # in the item property table like normal instead.
+ 0x92080040, # LBU T0, 0x0040 (S0)
+ 0x55000001, # BNEZL T0, T1, [forward 0x01]
+ 0x01002025, # OR A0, T0, R0
+ 0x03E00008, # JR RA
+ 0xAFA70024 # SW A3, 0x0024 (SP)
+]
+
+item_model_visibility_switcher = [
+ # If 80 is written one byte ahead of the appearance switch value in the item's actor data, parse 0C00 to the
+ # function that checks if an item should be invisible or not. Otherwise, grab that setting from the item property
+ # table like normal.
+ 0x920B0041, # LBU T3, 0x0041 (S0)
+ 0x316E0080, # ANDI T6, T3, 0x0080
+ 0x11C00003, # BEQZ T6, [forward 0x03]
+ 0x240D0C00, # ADDIU T5, R0, 0x0C00
+ 0x03E00008, # JR RA
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ 0x958D0004 # LHU T5, 0x0004 (T4)
+]
+
+item_shine_visibility_switcher = [
+ # Same as the above, but for item shines instead of the model.
+ 0x920B0041, # LBU T3, 0x0041 (S0)
+ 0x31690080, # ANDI T1, T3, 0x0080
+ 0x11200003, # BEQZ T1, [forward 0x03]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ 0x240C0C00, # ADDIU T4, R0, 0x0C00
+ 0x03E00008, # JR RA
+ 0x958CA908 # LHU T4, 0xA908 (T4)
+]
+
+three_hit_item_flags_setter = [
+ # As the function to create items from the 3HB item lists iterates through said item lists, this will pass unique
+ # flag values to each item when calling the "create item instance" function by right-shifting said flag by a number
+ # of bits depending on which item in the list it is. Unlike the vanilla game which always puts flags of 0x00000000
+ # on each of these.
+ 0x8DC80008, # LW T0, 0x0008 (T6)
+ 0x240A0000, # ADDIU T2, R0, 0x0000
+ 0x00084C02, # SRL T1, T0, 16
+ 0x3108FFFF, # ANDI T0, T0, 0xFFFF
+ 0x00094842, # SRL T1, T1, 1
+ 0x15200003, # BNEZ T1, [forward 0x03]
+ 0x00000000, # NOP
+ 0x34098000, # ORI T1, R0, 0x8000
+ 0x25080001, # ADDIU T0, T0, 0x0001
+ 0x0154582A, # SLT T3, T2, S4
+ 0x1560FFF9, # BNEZ T3, [backward 0x07]
+ 0x254A0001, # ADDIU T2, T2, 0x0001
+ 0x00094C00, # SLL T1, T1, 16
+ 0x01094025, # OR T0, T0, T1
+ 0x0805971E, # J 0x80165C78
+ 0xAFA80010 # SW T0, 0x0010 (SP)
+]
+
+chandelier_item_flags_setter = [
+ # Same as the above, but for the unique function made specifically and ONLY for the Villa foyer chandelier's item
+ # list. KCEK, why the heck did you have to do this!?
+ 0x8F280014, # LW T0, 0x0014 (T9)
+ 0x240A0000, # ADDIU T2, R0, 0x0000
+ 0x00084C02, # SRL T1, T0, 16
+ 0x3108FFFF, # ANDI T0, T0, 0xFFFF
+ 0x00094842, # SRL T1, T1, 1
+ 0x15200003, # BNEZ T1, [forward 0x03]
+ 0x00000000, # NOP
+ 0x34098000, # ORI T1, R0, 0x8000
+ 0x25080001, # ADDIU T0, T0, 0x0001
+ 0x0155582A, # SLT T3, T2, S5
+ 0x1560FFF9, # BNEZ T3, [backward 0x07]
+ 0x254A0001, # ADDIU T2, T2, 0x0001
+ 0x00094C00, # SLL T1, T1, 16
+ 0x01094025, # OR T0, T0, T1
+ 0x0805971E, # J 0x80165C78
+ 0xAFA80010 # SW T0, 0x0010 (SP)
+]
+
+prev_subweapon_spawn_checker = [
+ # When picking up a sub-weapon this will check to see if it's different from the one the player already had (if they
+ # did have one) and jump to prev_subweapon_dropper, which will spawn a subweapon actor of what they had before
+ # directly behind them.
+ 0x322F3031, # Previous sub-weapon bytes
+ 0x10A00009, # BEQZ A1, [forward 0x09]
+ 0x00000000, # NOP
+ 0x10AD0007, # BEQ A1, T5, [forward 0x07]
+ 0x3C088040, # LUI T0, 0x8040
+ 0x01054021, # ADDU T0, T0, A1
+ 0x0C0FF418, # JAL 0x803FD060
+ 0x9104CFC3, # LBU A0, 0xCFC3 (T0)
+ 0x2484FF9C, # ADDIU A0, A0, 0xFF9C
+ 0x3C088039, # LUI T0, 0x8039
+ 0xAD049BD4, # SW A0, 0x9BD4 (T0)
+ 0x0804F0BF, # J 0x8013C2FC
+ 0x24020001 # ADDIU V0, R0, 0x0001
+]
+
+prev_subweapon_fall_checker = [
+ # Checks to see if a pointer to a previous sub-weapon drop actor spawned by prev_subweapon_dropper is in 80389BD4
+ # and calls the function in prev_subweapon_dropper to lower the weapon closer to the ground on the next frame if a
+ # pointer exists and its actor ID is 0x0027. Once it hits the ground or despawns, the connection to the actor will
+ # be severed by 0-ing out the pointer.
+ 0x3C088039, # LUI T0, 0x8039
+ 0x8D049BD4, # LW A0, 0x9BD4 (T0)
+ 0x10800008, # BEQZ A0, [forward 0x08]
+ 0x00000000, # NOP
+ 0x84890000, # LH T1, 0x0000 (A0)
+ 0x240A0027, # ADDIU T2, R0, 0x0027
+ 0x152A0004, # BNE T1, T2, [forward 0x04]
+ 0x00000000, # NOP
+ 0x0C0FF452, # JAL 0x803FD148
+ 0x00000000, # NOP
+ 0x50400001, # BEQZL V0, [forward 0x01]
+ 0xAD009BD4, # SW R0, 0x9BD4 (T0)
+ 0x080FF40F # J 0x803FD03C
+]
+
+prev_subweapon_dropper = [
+ # Spawns a pickup actor of the sub-weapon the player had before picking up a new one behind them at their current
+ # position like in other CVs. This will enable them to pick it back up again if they still want it.
+ # Courtesy of Moisés; see derp.c in the src folder for the C source code.
+ 0x27BDFFC8,
+ 0xAFBF001C,
+ 0xAFA40038,
+ 0xAFB00018,
+ 0x0C0006B4,
+ 0x2404016C,
+ 0x00402025,
+ 0x0C000660,
+ 0x24050027,
+ 0x1040002B,
+ 0x00408025,
+ 0x3C048035,
+ 0x848409DE,
+ 0x00042023,
+ 0x0C0230D4,
+ 0x3084FFFF,
+ 0x44822000,
+ 0x3C018040,
+ 0xC428D370,
+ 0x468021A0,
+ 0x3C048035,
+ 0x848409DE,
+ 0x00042023,
+ 0x46083282,
+ 0x3084FFFF,
+ 0x0C01FFAC,
+ 0xE7AA0024,
+ 0x44828000,
+ 0x3C018040,
+ 0xC424D374,
+ 0x468084A0,
+ 0x27A40024,
+ 0x00802825,
+ 0x3C064100,
+ 0x46049182,
+ 0x0C004562,
+ 0xE7A6002C,
+ 0x3C058035,
+ 0x24A509D0,
+ 0x26040064,
+ 0x0C004530,
+ 0x27A60024,
+ 0x3C018035,
+ 0xC42809D4,
+ 0x3C0140A0,
+ 0x44815000,
+ 0x00000000,
+ 0x460A4400,
+ 0xE6100068,
+ 0xC6120068,
+ 0xE6120034,
+ 0x8FAE0038,
+ 0xA60E0038,
+ 0x8FBF001C,
+ 0x8FB00018,
+ 0x27BD0038,
+ 0x03E00008,
+ 0x00000000,
+ 0x3C068040,
+ 0x24C6D368,
+ 0x90CE0000,
+ 0x27BDFFE8,
+ 0xAFBF0014,
+ 0x15C00027,
+ 0x00802825,
+ 0x240400DB,
+ 0x0C0006B4,
+ 0xAFA50018,
+ 0x44802000,
+ 0x3C038040,
+ 0x2463D364,
+ 0x3C068040,
+ 0x24C6D368,
+ 0x8FA50018,
+ 0x1040000A,
+ 0xE4640000,
+ 0x8C4F0024,
+ 0x3C013F80,
+ 0x44814000,
+ 0xC5E60044,
+ 0xC4700000,
+ 0x3C018040,
+ 0x46083280,
+ 0x460A8480,
+ 0xE432D364,
+ 0x94A20038,
+ 0x2401000F,
+ 0x24180001,
+ 0x10410006,
+ 0x24010010,
+ 0x10410004,
+ 0x2401002F,
+ 0x10410002,
+ 0x24010030,
+ 0x14410005,
+ 0x3C014040,
+ 0x44813000,
+ 0xC4640000,
+ 0x46062200,
+ 0xE4680000,
+ 0xA0D80000,
+ 0x10000023,
+ 0x24020001,
+ 0x3C038040,
+ 0x2463D364,
+ 0xC4600000,
+ 0xC4A20068,
+ 0x3C038039,
+ 0x24639BD0,
+ 0x4600103E,
+ 0x00001025,
+ 0x45000006,
+ 0x00000000,
+ 0x44808000,
+ 0xE4A00068,
+ 0xA0C00000,
+ 0x10000014,
+ 0xE4700000,
+ 0x3C038039,
+ 0x24639BD0,
+ 0x3C018019,
+ 0xC42AC870,
+ 0xC4600000,
+ 0x460A003C,
+ 0x00000000,
+ 0x45000006,
+ 0x3C018019,
+ 0xC432C878,
+ 0x46120100,
+ 0xE4640000,
+ 0xC4600000,
+ 0xC4A20068,
+ 0x46001181,
+ 0x24020001,
+ 0xE4A60068,
+ 0xC4A80068,
+ 0xE4A80034,
+ 0x8FBF0014,
+ 0x27BD0018,
+ 0x03E00008,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x0000001B,
+ 0x060048E0,
+ 0x40000000,
+ 0x06AEFFD3,
+ 0x06004B30,
+ 0x40000000,
+ 0x00000000,
+ 0x06004CB8,
+ 0x0000031A,
+ 0x002C0000,
+ 0x060059B8,
+ 0x40000248,
+ 0xFFB50186,
+ 0x06005B68,
+ 0xC00001DF,
+ 0x00000000,
+ 0x06005C88,
+ 0x80000149,
+ 0x00000000,
+ 0x06005DC0,
+ 0xC0000248,
+ 0xFFB5FE7B,
+ 0x06005F70,
+ 0xC00001E0,
+ 0x00000000,
+ 0x06006090,
+ 0x8000014A,
+ 0x00000000,
+ 0x06007D28,
+ 0x4000010E,
+ 0xFFF100A5,
+ 0x06007F60,
+ 0xC0000275,
+ 0x00000000,
+ 0x06008208,
+ 0x800002B2,
+ 0x00000000,
+ 0x060083B0,
+ 0xC000010D,
+ 0xFFF2FF5C,
+ 0x060085E8,
+ 0xC0000275,
+ 0x00000000,
+ 0x06008890,
+ 0x800002B2,
+ 0x00000000,
+ 0x3D4CCCCD,
+ 0x3FC00000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0xB8000100,
+ 0xB8000100,
+]
+
+subweapon_surface_checker = [
+ # During the process of remotely giving an item received via multiworld, this will check to see if the item being
+ # received is a subweapon and, if it is, wait until the player is not above an abyss or instant kill surface before
+ # giving it. This is to ensure dropped previous subweapons won't land somewhere inaccessible.
+ 0x2408000D, # ADDIU T0, R0, 0x000D
+ 0x11040006, # BEQ T0, A0, [forward 0x06]
+ 0x2409000E, # ADDIU T1, R0, 0x000E
+ 0x11240004, # BEQ T1, A0, [forward 0x04]
+ 0x2408000F, # ADDIU T0, R0, 0x000F
+ 0x11040002, # BEQ T0, A0, [forward 0x02]
+ 0x24090010, # ADDIU T1, R0, 0x0010
+ 0x1524000B, # BNE T1, A0, [forward 0x0B]
+ 0x3C0A800D, # LUI T2, 0x800D
+ 0x8D4A7B5C, # LW T2, 0x7B5C (T2)
+ 0x1140000E, # BEQZ T2, [forward 0x0E]
+ 0x00000000, # NOP
+ 0x914A0001, # LBU T2, 0x0001 (T2)
+ 0x240800A2, # ADDIU T0, R0, 0x00A2
+ 0x110A000A, # BEQ T0, T2, [forward 0x0A]
+ 0x24090092, # ADDIU T1, R0, 0x0092
+ 0x112A0008, # BEQ T1, T2, [forward 0x08]
+ 0x24080080, # ADDIU T0, R0, 0x0080
+ 0x110A0006, # BEQ T0, T2, [forward 0x06]
+ 0x956C00DD, # LHU T4, 0x00DD (T3)
+ 0xA1600000, # SB R0, 0x0000 (T3)
+ 0x258C0001, # ADDIU T4, T4, 0x0001
+ 0x080FF8D0, # J 0x803FE340
+ 0xA56C00DD, # SH T4, 0x00DD (T3)
+ 0x00000000, # NOP
+ 0x03E00008 # JR RA
+]
+
+countdown_number_displayer = [
+ # Displays a number below the HUD clock of however many items are left to find in whichever stage the player is in.
+ # Which number in the save file to display depends on which map the player is currently on. It can track either
+ # items marked progression only or all locations in the stage.
+ # Courtesy of Moisés; see print_text_ovl.c in the src folder for the C source code.
+ 0x27BDFFD8,
+ 0xAFBF0024,
+ 0x00002025,
+ 0x0C000360,
+ 0x2405000C,
+ 0x3C038040,
+ 0x3C198034,
+ 0x2463D6D0,
+ 0x37392814,
+ 0x240E0002,
+ 0x3C0F0860,
+ 0x24180014,
+ 0xAC620000,
+ 0xAFB80018,
+ 0xAFAF0014,
+ 0xAFAE0010,
+ 0xAFB9001C,
+ 0x00002025,
+ 0x00402825,
+ 0x2406001E,
+ 0x0C0FF55D,
+ 0x24070028,
+ 0x8FBF0024,
+ 0x3C018040,
+ 0xAC22D6D4,
+ 0x03E00008,
+ 0x27BD0028,
+ 0x27BDFFE0,
+ 0xAFA40020,
+ 0x93AE0023,
+ 0x3C058039,
+ 0xAFBF001C,
+ 0x3C048040,
+ 0x3C068040,
+ 0x240F0014,
+ 0x00AE2821,
+ 0x90A59CA4,
+ 0xAFAF0010,
+ 0x8CC6D6D0,
+ 0x8C84D6D4,
+ 0x0C0FF58A,
+ 0x24070002,
+ 0x8FBF001C,
+ 0x27BD0020,
+ 0x03E00008,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x90820000,
+ 0x00001825,
+ 0x50400008,
+ 0xA4A00000,
+ 0xA4A20000,
+ 0x90820001,
+ 0x24840001,
+ 0x24A50002,
+ 0x1440FFFB,
+ 0x24630001,
+ 0xA4A00000,
+ 0x03E00008,
+ 0x00601025,
+ 0x27BDFFD8,
+ 0xAFBF0024,
+ 0xAFB0001C,
+ 0xAFA40028,
+ 0xAFA5002C,
+ 0xAFB10020,
+ 0xAFA60030,
+ 0xAFA70034,
+ 0x00008025,
+ 0x24050064,
+ 0x0C000360,
+ 0x00002025,
+ 0x8FA40040,
+ 0x00408825,
+ 0x3C05800A,
+ 0x10800004,
+ 0x8FA6003C,
+ 0x0C04B2E2,
+ 0x8CA5B450,
+ 0x00408025,
+ 0x5200001A,
+ 0x8FBF0024,
+ 0x12200017,
+ 0x8FAE0028,
+ 0x11C00015,
+ 0x02002025,
+ 0x97A5002E,
+ 0x97A60032,
+ 0x0C04B33F,
+ 0x24070001,
+ 0x02002025,
+ 0x83A50037,
+ 0x87A6003A,
+ 0x00003825,
+ 0x0C04B345,
+ 0xAFA00010,
+ 0x8FA40028,
+ 0x0C0FF51C,
+ 0x02202825,
+ 0x0C006CF0,
+ 0x02202025,
+ 0x02002025,
+ 0x02202825,
+ 0x00003025,
+ 0x0C04B34E,
+ 0x00003825,
+ 0x8FBF0024,
+ 0x02001025,
+ 0x8FB0001C,
+ 0x8FB10020,
+ 0x03E00008,
+ 0x27BD0028,
+ 0x27BDFFD8,
+ 0x8FAE0044,
+ 0xAFB00020,
+ 0xAFBF0024,
+ 0xAFA40028,
+ 0xAFA5002C,
+ 0xAFA60030,
+ 0xAFA70034,
+ 0x11C00007,
+ 0x00008025,
+ 0x3C05800A,
+ 0x8CA5B450,
+ 0x01C02025,
+ 0x0C04B2E2,
+ 0x8FA6003C,
+ 0x00408025,
+ 0x12000017,
+ 0x8FAF002C,
+ 0x11E00015,
+ 0x02002025,
+ 0x97A50032,
+ 0x97A60036,
+ 0x0C04B33F,
+ 0x24070001,
+ 0x02002025,
+ 0x24050001,
+ 0x24060064,
+ 0x00003825,
+ 0x0C04B345,
+ 0xAFA00010,
+ 0x8FA40028,
+ 0x8FA5002C,
+ 0x93A6003B,
+ 0x0C04B5BD,
+ 0x8FA70040,
+ 0x02002025,
+ 0x8FA5002C,
+ 0x00003025,
+ 0x0C04B34E,
+ 0x00003825,
+ 0x8FBF0024,
+ 0x02001025,
+ 0x8FB00020,
+ 0x03E00008,
+ 0x27BD0028,
+ 0x27BDFFE8,
+ 0xAFBF0014,
+ 0xAFA40018,
+ 0xAFA5001C,
+ 0xAFA60020,
+ 0x10C0000B,
+ 0xAFA70024,
+ 0x00A02025,
+ 0x00C02825,
+ 0x93A60027,
+ 0x0C04B5BD,
+ 0x8FA70028,
+ 0x8FA20018,
+ 0x3C010100,
+ 0x8C4F0000,
+ 0x01E1C025,
+ 0xAC580000,
+ 0x8FBF0014,
+ 0x27BD0018,
+ 0x03E00008,
+ 0x00000000,
+ 0xAFA50004,
+ 0x1080000E,
+ 0x30A500FF,
+ 0x24010001,
+ 0x54A10008,
+ 0x8C980000,
+ 0x8C8E0000,
+ 0x3C017FFF,
+ 0x3421FFFF,
+ 0x01C17824,
+ 0x03E00008,
+ 0xAC8F0000,
+ 0x8C980000,
+ 0x3C018000,
+ 0x0301C825,
+ 0xAC990000,
+ 0x03E00008,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000
+]
+
+countdown_number_manager = [
+ # Tables and code for managing things about the Countdown number at the appropriate times.
+ 0x00010102, # Map ID offset table start
+ 0x02020D03,
+ 0x04050505,
+ 0x0E0E0E05,
+ 0x07090806,
+ 0x0C0C000B,
+ 0x0C050D0A,
+ 0x00000000, # Table end
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000001, # Major identifiers table start
+ 0x01000000,
+ 0x00000000,
+ 0x00000000,
+ 0x01000000,
+ 0x01010000,
+ 0x00010101,
+ 0x01010101,
+ 0x01010101,
+ 0x01010000,
+ 0x00000000, # Table end
+ # Decrements the counter upon picking up an item if the counter should be decremented.
+ 0x90E80039, # LBU T0, 0x0039 (A3)
+ 0x240B0011, # ADDIU T3, R0, 0x0011
+ 0x110B0002, # BEQ T0, T3, [forward 0x02]
+ 0x90EA0040, # LBU T2, 0x0040 (A3)
+ 0x2548FFFF, # ADDIU T0, T2, 0xFFFF
+ 0x3C098040, # LUI T1, 0x8040
+ 0x01284821, # ADDIU T1, T1, T0
+ 0x9129D71C, # LBU T1, 0xD71C (T1)
+ 0x11200009, # BEQZ T1, [forward 0x09]
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91099EE1, # LBU T1, 0x9EE1 (T0)
+ 0x3C0A8040, # LUI T2, 0x8040
+ 0x01495021, # ADDU T2, T2, T1
+ 0x914AD6DC, # LBU T2, 0xD6DC (T2)
+ 0x010A4021, # ADDU T0, T0, T2
+ 0x91099CA4, # LBU T1, 0x9CA4 (T0)
+ 0x2529FFFF, # ADDIU T1, T1, 0xFFFF
+ 0xA1099CA4, # SB T1, 0x9CA4 (T0)
+ 0x03E00008, # JR RA
+ 0x00000000, # NOP
+ # Moves the number to/from its pause menu position when pausing/un-pausing.
+ 0x3C088040, # LUI T0, 0x8040
+ 0x8D08D6D4, # LW T0, 0xD6D4
+ 0x11000009, # BEQZ T0, [forward 0x09]
+ 0x92090000, # LBU T1, 0x0000 (S0)
+ 0x14200004, # BNEZ AT, [forward 0x04]
+ 0x3C0A0033, # LUI T2, 0x0033
+ 0x254A001F, # ADDIU T2, T2, 0x001F
+ 0x03E00008, # JR RA
+ 0xAD0A0014, # SW T2, 0x0014 (T0)
+ 0x3C0A00D4, # LUI T2, 0x00D4
+ 0x254A003C, # ADDIU T2, T2, 0x003C
+ 0xAD0A0014, # SW T2, 0x0014 (T0)
+ 0x03E00008, # JR RA
+ 0x00000000, # NOP
+ # Hides the number when going into a cutscene or the Options menu.
+ 0x3C048040, # LUI A0, 0x8040
+ 0x8C84D6D4, # LW A0, 0xD6D4 (A0)
+ 0x0C0FF59F, # JAL 0x803FD67C
+ 0x24050000, # ADDIU A1, R0, 0x0000
+ 0x0804DFE0, # J 0x80137FB0
+ 0x3C048000, # LUI A0, 0x8000
+ 0x00000000, # NOP
+ # Un-hides the number when leaving a cutscene or the Options menu.
+ 0x3C048040, # LUI A0, 0x8040
+ 0x8C84D6D4, # LW A0, 0xD6D4 (A0)
+ 0x0C0FF59F, # JAL 0x803FD67C
+ 0x24050001, # ADDIU A1, R0, 0x0000
+ 0x0804DFFA, # J 0x8013
+ 0x3C047FFF, # LUI A0, 0x7FFFF
+ 0x00000000, # NOP
+ # Kills the last map's pointer to the Countdown stuff.
+ 0x3C088040, # LUI T0, 0x8040
+ 0xFD00D6D0, # SD R0, 0xD6D0 (T0)
+ 0x03E00008 # JR RA
+]
+
+new_game_extras = [
+ # Upon starting a new game, this will write anything extra to the save file data that the run should have at the
+ # start. The initial Countdown numbers begin here.
+ 0x24080000, # ADDIU T0, R0, 0x0000
+ 0x24090010, # ADDIU T1, R0, 0x0010
+ 0x11090008, # BEQ T0, T1, [forward 0x08]
+ 0x3C0A8040, # LUI T2, 0x8040
+ 0x01485021, # ADDU T2, T2, T0
+ 0x8D4AD818, # LW T2, 0xD818 (T2)
+ 0x3C0B8039, # LUI T3, 0x8039
+ 0x01685821, # ADDU T3, T3, T0
+ 0xAD6A9CA4, # SW T2, 0x9CA4 (T3)
+ 0x1000FFF8, # B [backward 0x08]
+ 0x25080004, # ADDIU T0, T0, 0x0004
+ # start_inventory begins here
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91099C27, # LBU T1, 0x9C27 (T0)
+ 0x31290010, # ANDI T1, T1, 0x0010
+ 0x15200005, # BNEZ T1, [forward 0x05]
+ 0x24090000, # ADDIU T1, R0, 0x0000 <- Starting jewels
+ 0xA1099C49, # SB T1, 0x9C49
+ 0x3C0A8040, # LUI T2, 0x8040
+ 0x8D4BE514, # LW T3, 0xE514 (T2) <- Starting money
+ 0xAD0B9C44, # SW T3, 0x9C44 (T0)
+ 0x24090000, # ADDIU T1, R0, 0x0000 <- Starting PowerUps
+ 0xA1099CED, # SB T1, 0x9CED (T0)
+ 0x24090000, # ADDIU T1, R0, 0x0000 <- Starting sub-weapon
+ 0xA1099C43, # SB T1, 0x9C43 (T0)
+ 0x24090000, # ADDIU T1, R0, 0x0000 <- Starting Ice Traps
+ 0xA1099BE2, # SB T1, 0x9BE2 (T0)
+ 0x240C0000, # ADDIU T4, R0, 0x0000
+ 0x240D0022, # ADDIU T5, R0, 0x0022
+ 0x11AC0007, # BEQ T5, T4, [forward 0x07]
+ 0x3C0A8040, # LUI T2, 0x8040
+ 0x014C5021, # ADDU T2, T2, T4
+ 0x814AE518, # LB T2, 0xE518 <- Starting inventory items
+ 0x25080001, # ADDIU T0, T0, 0x0001
+ 0xA10A9C4A, # SB T2, 0x9C4A (T0)
+ 0x1000FFF9, # B [backward 0x07]
+ 0x258C0001, # ADDIU T4, T4, 0x0001
+ 0x03E00008 # JR RA
+]
+
+shopsanity_stuff = [
+ # Everything related to shopsanity.
+ # Flag table (in bytes) start
+ 0x80402010,
+ 0x08000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00040200,
+ # Replacement item table (in halfwords) start
+ 0x00030003,
+ 0x00030003,
+ 0x00030000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000003,
+ 0x00030000,
+ # Switches the vanilla item being bought with the randomized one, if its flag is un-set, and sets its flag.
+ 0x3C088040, # LUI T0, 0x8040
+ 0x01044021, # ADDU T0, T0, A0
+ 0x9109D8CA, # LBU T1, 0xD8CA (T0)
+ 0x3C0B8039, # LUI T3, 0x8039
+ 0x916A9C1D, # LBU T2, 0x9C1D (T3)
+ 0x01496024, # AND T4, T2, T1
+ 0x15800005, # BNEZ T4, [forward 0x05]
+ 0x01495025, # OR T2, T2, T1
+ 0xA16A9C1D, # SB T2, 0x9C1D (T3)
+ 0x01044021, # ADDU T0, T0, A0
+ 0x9504D8D8, # LHU A0, 0xD8D8 (T0)
+ 0x308400FF, # ANDI A0, A0, 0x00FF
+ 0x0804EFFB, # J 0x8013BFEC
+ 0x00000000, # NOP
+ # Switches the vanilla item model on the buy menu with the randomized item if the randomized item isn't purchased.
+ 0x3C088040, # LUI T0, 0x8040
+ 0x01044021, # ADDU T0, T0, A0
+ 0x9109D8CA, # LBU T1, 0xD8CA (T0)
+ 0x3C0B8039, # LUI T3, 0x8039
+ 0x916A9C1D, # LBU T2, 0x9C1D (T3)
+ 0x01495024, # AND T2, T2, T1
+ 0x15400005, # BNEZ T2, [forward 0x05]
+ 0x01044021, # ADDU T0, T0, A0
+ 0x9504D8D8, # LHU A0, 0xD8D8 (T0)
+ 0x00046202, # SRL T4, A0, 8
+ 0x55800001, # BNEZL T4, [forward 0x01]
+ 0x01802021, # ADDU A0, T4, R0
+ 0x0804F180, # J 0x8013C600
+ 0x00000000, # NOP
+ # Replacement item names table start.
+ 0x00010203,
+ 0x04000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00050600,
+ 0x00000000,
+ # Switches the vanilla item name in the shop menu with the randomized item if the randomized item isn't purchased.
+ 0x3C088040, # LUI T0, 0x8040
+ 0x01064021, # ADDU T0, T0, A2
+ 0x9109D8CA, # LBU T1, 0xD8CA (T0)
+ 0x3C0B8039, # LUI T3, 0x8039
+ 0x916A9C1D, # LBU T2, 0x9C1D (T3)
+ 0x01495024, # AND T2, T2, T1
+ 0x15400004, # BNEZ T2, [forward 0x04]
+ 0x00000000, # NOP
+ 0x9105D976, # LBU A1, 0xD976 (T0)
+ 0x3C048001, # LUI A0, 8001
+ 0x3484A100, # ORI A0, A0, 0xA100
+ 0x0804B39F, # J 0x8012CE7C
+ 0x00000000, # NOP
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ # Displays "Not purchased." if the selected randomized item is nor purchased, or the current holding amount of that
+ # slot's vanilla item if it is.
+ 0x3C0C8040, # LUI T4, 0x8040
+ 0x018B6021, # ADDU T4, T4, T3
+ 0x918DD8CA, # LBU T5, 0xD8CA (T4)
+ 0x3C0E8039, # LUI T6, 0x8039
+ 0x91D89C1D, # LBU T8, 0x9C1D (T6)
+ 0x030DC024, # AND T8, T8, T5
+ 0x13000003, # BEQZ T8, [forward 0x03]
+ 0x00000000, # NOP
+ 0x0804E819, # J 0x8013A064
+ 0x00000000, # NOP
+ 0x0804E852, # J 0x8013A148
+ 0x820F0061, # LB T7, 0x0061 (S0)
+ 0x00000000, # NOP
+ # Displays a custom item description if the selected randomized item is not purchased.
+ 0x3C088040, # LUI T0, 0x8040
+ 0x01054021, # ADDU T0, T0, A1
+ 0x9109D8D0, # LBU T1, 0xD8D0 (T0)
+ 0x3C0A8039, # LUI T2, 0x8039
+ 0x914B9C1D, # LBU T3, 0x9C1D (T2)
+ 0x01695824, # AND T3, T3, T1
+ 0x15600003, # BNEZ T3, [forward 0x03]
+ 0x00000000, # NOP
+ 0x3C048002, # LUI A0, 0x8002
+ 0x24849C00, # ADDIU A0, A0, 0x9C00
+ 0x0804B39F # J 0x8012CE7C
+]
+
+special_sound_notifs = [
+ # Plays a distinct sound whenever you get enough Special1s to unlock a new location or enough Special2s to unlock
+ # Dracula's door.
+ 0x3C088013, # LUI A0, 0x8013
+ 0x9108AC9F, # LBU T0, 0xAC57 (T0)
+ 0x3C098039, # LUI T1, 0x8039
+ 0x91299C4C, # LBU T1, 0x9C4B (T1)
+ 0x15090003, # BNE T0, T1, [forward 0x03]
+ 0x00000000, # NOP
+ 0x0C004FAB, # JAL 0x80013EAC
+ 0x24040162, # ADDIU A0, R0, 0x0162
+ 0x0804F0BF, # J 0x8013C2FC
+ 0x00000000, # NOP
+ 0x3C088013, # LUI T0, 0x8013
+ 0x9108AC57, # LBU T0, 0xAC57 (T0)
+ 0x3C098039, # LUI T1, 0x8039
+ 0x91299C4B, # LBU T1, 0x9C4B (T1)
+ 0x0128001B, # DIVU T1, T0
+ 0x00005010, # MFHI
+ 0x15400006, # BNEZ T2, [forward 0x06]
+ 0x00005812, # MFLO T3
+ 0x296C0008, # SLTI T4, T3, 0x0008
+ 0x11800003, # BEQZ T4, [forward 0x03]
+ 0x00000000, # NOP
+ 0x0C004FAB, # JAL 0x80013EAC
+ 0x2404019E, # ADDIU A0, R0, 0x019E
+ 0x0804F0BF # J 0x8013C2FC
+]
+
+map_text_redirector = [
+ # Checks for Map Texts 06 or 08 if in the Forest or Castle Wall Main maps respectively and redirects the text
+ # pointer to a blank string, skipping all the yes/no prompt text for pulling levers.
+ 0x0002FFFF, # Dummy text string
+ 0x3C0B8039, # LUI T3, 0x8039
+ 0x91689EE1, # LBU T0, 0x9EE1 (T3)
+ 0x1100000F, # BEQZ T0, [forward 0x0F]
+ 0x24090006, # ADDIU T1, R0, 0x0006
+ 0x240A0002, # ADDIU T2, R0, 0x0002
+ 0x110A000C, # BEQ T0, T2, [forward 0x0C]
+ 0x24090008, # ADDIU T1, R0, 0x0008
+ 0x240A0009, # ADDIU T2, R0, 0x0009
+ 0x110A0009, # BEQ T0, T2, [forward 0x09]
+ 0x24090004, # ADDIU T1, R0, 0x0004
+ 0x240A000A, # ADDIU T2, R0, 0x000A
+ 0x110A0006, # BEQ T0, T2, [forward 0x06]
+ 0x24090001, # ADDIU T1, R0, 0x0001
+ 0x240A000C, # ADDIU T2, R0, 0x000C
+ 0x110A0003, # BEQ T0, T2, [forward 0x03]
+ 0x2409000C, # ADDIU T1, R0, 0x000C
+ 0x10000008, # B 0x803FDB34
+ 0x00000000, # NOP
+ 0x15250006, # BNE T1, A1, [forward 0x06]
+ 0x00000000, # NOP
+ 0x3C04803F, # LUI A0, 0x803F
+ 0x3484DACC, # ORI A0, A0, 0xDACC
+ 0x24050000, # ADDIU A1, R0, 0x0000
+ 0x0804B39F, # J 0x8012CE7C
+ 0x00000000, # NOP
+ # Redirects to a custom message if you try placing the bomb ingredients at the bottom CC crack before deactivating
+ # the seal.
+ 0x24090009, # ADDIU T1, R0, 0x0009
+ 0x15090009, # BNE T0, T1, [forward 0x09]
+ 0x240A0002, # ADDIU T2, R0, 0x0002
+ 0x15450007, # BNE T2, A1, [forward 0x07]
+ 0x916A9C18, # LBU T2, 0x9C18 (T3)
+ 0x314A0001, # ANDI T2, T2, 0x0001
+ 0x15400004, # BNEZ T2, [forward 0x04]
+ 0x00000000, # NOP
+ 0x3C04803F, # LUI A0, 0x803F
+ 0x3484DBAC, # ORI A0, A0, 0xDBAC
+ 0x24050000, # ADDIU A1, R0, 0x0000
+ 0x0804B39F, # J 0x8012CE7C
+ 0x00000000, # NOP
+ # Checks for Map Texts 02 or 00 if in the Villa hallway or CC lizard lab maps respectively and redirects the text
+ # pointer to a blank string, skipping all the NPC dialogue mandatory for checks.
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91089EE1, # LBU T0, 0x9EE1 (T0)
+ 0x240A0005, # ADDIU T2, R0, 0x0005
+ 0x110A0006, # BEQ T0, T2, [forward 0x06]
+ 0x24090002, # ADDIU T1, R0, 0x0002
+ 0x240A000C, # ADDIU T2, R0, 0x000C
+ 0x110A0003, # BEQ T0, T2, [forward 0x03]
+ 0x24090000, # ADDIU T1, R0, 0x0000
+ 0x0804B39F, # J 0x8012CE7C
+ 0x00000000, # NOP
+ 0x15250004, # BNE T1, A1, [forward 0x04]
+ 0x00000000, # NOP
+ 0x3C04803F, # LUI A0, 0x803F
+ 0x3484DACC, # ORI A0, A0, 0xDACC
+ 0x24050000, # ADDIU A1, R0, 0x0000
+ 0x0804B39F # J 0x8012CE7C
+]
+
+special_descriptions_redirector = [
+ # Redirects the menu description when looking at the Special1 and 2 items to different, custom strings that tell
+ # how many are needed per warp and to fight Dracula respectively, and how many there are of both in the whole seed.
+ 0x240A0003, # ADDIU T2, R0, 0x0003
+ 0x10AA0005, # BEQ A1, T2, [forward 0x05]
+ 0x240A0004, # ADDIU T2, R0, 0x0004
+ 0x10AA0003, # BEQ A1, T2, [forward 0x03]
+ 0x00000000, # NOP
+ 0x0804B39F, # J 0x8012CE7C
+ 0x00000000, # NOP
+ 0x3C04803F, # LUI A0, 0x803F
+ 0x3484E53C, # ORI A0, A0, 0xE53C
+ 0x24A5FFFD, # ADDIU A1, A1, 0xFFFD
+ 0x0804B39F # J 0x8012CE7C
+]
+
+forest_cw_villa_intro_cs_player = [
+ # Plays the Forest, Castle Wall, or Villa intro cutscene after transitioning to a different map if the map being
+ # transitioned to is the start of their levels respectively. Gets around the fact that they have to be set on the
+ # previous loading zone for them to play normally.
+ 0x3C088039, # LUI T0, 0x8039
+ 0x8D099EE0, # LW T1, 0x9EE0 (T0)
+ 0x1120000B, # BEQZ T1 T1, [forward 0x0B]
+ 0x240B0000, # ADDIU T3, R0, 0x0000
+ 0x3C0A0002, # LUI T2, 0x0002
+ 0x112A0008, # BEQ T1, T2, [forward 0x08]
+ 0x240B0007, # ADDIU T3, R0, 0x0007
+ 0x254A0007, # ADDIU T2, T2, 0x0007
+ 0x112A0005, # BEQ T1, T2, [forward 0x05]
+ 0x3C0A0003, # LUI T2, 0x0003
+ 0x112A0003, # BEQ T1, T2, [forward 0x03]
+ 0x240B0003, # ADDIU T3, R0, 0x0003
+ 0x08005FAA, # J 0x80017EA8
+ 0x00000000, # NOP
+ 0x010B6021, # ADDU T4, T0, T3
+ 0x918D9C08, # LBU T5, 0x9C08 (T4)
+ 0x31AF0001, # ANDI T7, T5, 0x0001
+ 0x15E00009, # BNEZ T7, [forward 0x09]
+ 0x240E0009, # ADDIU T6, R0, 0x0009
+ 0x3C180003, # LUI T8, 0x0003
+ 0x57090001, # BNEL T8, T1, [forward 0x01]
+ 0x240E0004, # ADDIU T6, R0, 0x0004
+ 0x15200003, # BNEZ T1, [forward 0x03]
+ 0x240F0001, # ADDIU T7, R0, 0x0001
+ 0xA18F9C08, # SB T7, 0x9C08 (T4)
+ 0x240E003C, # ADDIU T6, R0, 0x003C
+ 0xA10E9EFF, # SB T6, 0x9EFF (T0)
+ 0x08005FAA # J 0x80017EA8
+]
+
+map_id_refresher = [
+ # After transitioning to a different map, if this detects the map ID being transitioned to as FF, it will write back
+ # the past map ID so that the map will reset. Useful for thngs like getting around a bug wherein the camera fixes in
+ # place if you enter a loading zone that doesn't actually change the map, which can happen in a seed that gives you
+ # any character tower stage at the very start.
+ 0x240800FF, # ADDIU T0, R0, 0x00FF
+ 0x110E0003, # BEQ T0, T6, [forward 0x03]
+ 0x00000000, # NOP
+ 0x03E00008, # JR RA
+ 0xA44E61D8, # SH T6, 0x61D8
+ 0x904961D9, # LBU T1, 0x61D9
+ 0xA0496429, # SB T1, 0x6429
+ 0x03E00008 # JR RA
+]
+
+character_changer = [
+ # Changes the character being controlled if the player is holding L while loading into a map by swapping the
+ # character ID.
+ 0x3C08800D, # LUI T0, 0x800D
+ 0x910B5E21, # LBU T3, 0x5E21 (T0)
+ 0x31680020, # ANDI T0, T3, 0x0020
+ 0x3C0A8039, # LUI T2, 0x8039
+ 0x1100000B, # BEQZ T0, [forward 0x0B]
+ 0x91499C3D, # LBU T1, 0x9C3D (T2)
+ 0x11200005, # BEQZ T1, [forward 0x05]
+ 0x24080000, # ADDIU T0, R0, 0x0000
+ 0xA1489C3D, # SB T0, 0x9C3D (T2)
+ 0x25080001, # ADDIU T0, T0, 0x0001
+ 0xA1489BC2, # SB T0, 0x9BC2 (T2)
+ 0x10000004, # B [forward 0x04]
+ 0x24080001, # ADDIU T0, R0, 0x0001
+ 0xA1489C3D, # SB T0, 0x9C3D (T2)
+ 0x25080001, # ADDIU T0, T0, 0x0001
+ 0xA1489BC2, # SB T0, 0x9BC2 (T2)
+ # Changes the alternate costume variables if the player is holding C-up.
+ 0x31680008, # ANDI T0, T3, 0x0008
+ 0x11000009, # BEQZ T0, [forward 0x09]
+ 0x91499C24, # LBU T1, 0x9C24 (T2)
+ 0x312B0040, # ANDI T3, T1, 0x0040
+ 0x2528FFC0, # ADDIU T0, T1, 0xFFC0
+ 0x15600003, # BNEZ T3, [forward 0x03]
+ 0x240C0000, # ADDIU T4, R0, 0x0000
+ 0x25280040, # ADDIU T0, T1, 0x0040
+ 0x240C0001, # ADDIU T4, R0, 0x0001
+ 0xA1489C24, # SB T0, 0x9C24 (T2)
+ 0xA14C9CEE, # SB T4, 0x9CEE (T2)
+ 0x080062AA, # J 0x80018AA8
+ 0x00000000, # NOP
+ # Plays the attack sound of the character being changed into to indicate the change was successful.
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91099BC2, # LBU T1, 0x9BC2 (T0)
+ 0xA1009BC2, # SB R0, 0x9BC2 (T0)
+ 0xA1009BC1, # SB R0, 0x9BC1 (T0)
+ 0x11200006, # BEQZ T1, [forward 0x06]
+ 0x2529FFFF, # ADDIU T1, T1, 0xFFFF
+ 0x240402F6, # ADDIU A0, R0, 0x02F6
+ 0x55200001, # BNEZL T1, [forward 0x01]
+ 0x240402F8, # ADDIU A0, R0, 0x02F8
+ 0x08004FAB, # J 0x80013EAC
+ 0x00000000, # NOP
+ 0x03E00008 # JR RA
+]
+
+panther_dash = [
+ # Changes various movement parameters when holding C-right so the player will move way faster.
+ # Increases movement speed and speeds up the running animation.
+ 0x3C08800D, # LUI T0, 0x800D
+ 0x91085E21, # LBU T0, 0x5E21 (T0)
+ 0x31080001, # ANDI T0, T0, 0x0001
+ 0x24093FEA, # ADDIU T1, R0, 0x3FEA
+ 0x11000004, # BEQZ T0, [forward 0x04]
+ 0x240B0010, # ADDIU T3, R0, 0x0010
+ 0x3C073F20, # LUI A3, 0x3F20
+ 0x240940AA, # ADDIU T1, R0, 0x40AA
+ 0x240B000A, # ADDIU T3, R0, 0x000A
+ 0x3C0C8035, # LUI T4, 0x8035
+ 0xA18B07AE, # SB T3, 0x07AE (T4)
+ 0xA18B07C2, # SB T3, 0x07C2 (T4)
+ 0x3C0A8034, # LUI T2, 0x8034
+ 0x03200008, # JR T9
+ 0xA5492BD8, # SH T1, 0x2BD8 (T2)
+ 0x00000000, # NOP
+ # Increases the turning speed so that handling is better.
+ 0x3C08800D, # LUI T0, 0x800D
+ 0x91085E21, # LBU T0, 0x5E21 (T0)
+ 0x31080001, # ANDI T0, T0, 0x0001
+ 0x11000002, # BEQZ T0, [forward 0x02]
+ 0x240A00D9, # ADDIU T2, R0, 0x00D9
+ 0x240A00F0, # ADDIU T2, R0, 0x00F0
+ 0x3C0B8039, # LUI T3, 0x8039
+ 0x916B9C3D, # LBU T3, 0x9C3D (T3)
+ 0x11600003, # BEQZ T3, [forward 0x03]
+ 0xD428DD58, # LDC1 F8, 0xDD58 (AT)
+ 0x03E00008, # JR RA
+ 0xA02ADD59, # SB T2, 0xDD59 (AT)
+ 0xD428D798, # LDC1 F8, 0xD798 (AT)
+ 0x03E00008, # JR RA
+ 0xA02AD799, # SB T2, 0xD799 (AT)
+ 0x00000000, # NOP
+ # Increases crouch-walking x and z speed.
+ 0x3C08800D, # LUI T0, 0x800D
+ 0x91085E21, # LBU T0, 0x5E21 (T0)
+ 0x31080001, # ANDI T0, T0, 0x0001
+ 0x11000002, # BEQZ T0, [forward 0x02]
+ 0x240A00C5, # ADDIU T2, R0, 0x00C5
+ 0x240A00F8, # ADDIU T2, R0, 0x00F8
+ 0x3C0B8039, # LUI T3, 0x8039
+ 0x916B9C3D, # LBU T3, 0x9C3D (T3)
+ 0x15600005, # BNEZ T3, [forward 0x05]
+ 0x00000000, # NOP
+ 0xA02AD801, # SB T2, 0xD801 (AT)
+ 0xA02AD809, # SB T2, 0xD809 (AT)
+ 0x03E00008, # JR RA
+ 0xD430D800, # LDC1 F16, 0xD800 (AT)
+ 0xA02ADDC1, # SB T2, 0xDDC1 (AT)
+ 0xA02ADDC9, # SB T2, 0xDDC9 (AT)
+ 0x03E00008, # JR RA
+ 0xD430DDC0 # LDC1 F16, 0xDDC0 (AT)
+]
+
+panther_jump_preventer = [
+ # Optional hack to prevent jumping while moving at the increased panther dash speed as a way to prevent logic
+ # sequence breaks that would otherwise be impossible without it. Such sequence breaks are never considered in logic
+ # either way.
+
+ # Decreases a "can running jump" value by 1 per frame unless it's at 0, or while in the sliding state. When the
+ # player lets go of C-right, their running speed should have returned to a normal amount by the time it hits 0.
+ 0x9208007F, # LBU T0, 0x007F (S0)
+ 0x24090008, # ADDIU T1, R0, 0x0008
+ 0x11090005, # BEQ T0, T1, [forward 0x05]
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91099BC1, # LBU T1, 0x9BC1 (T0)
+ 0x11200002, # BEQZ T1, [forward 0x02]
+ 0x2529FFFF, # ADDIU T1, T1, 0xFFFF
+ 0xA1099BC1, # SB T1, 0x9BC1 (T0)
+ 0x080FF413, # J 0x803FD04C
+ 0x00000000, # NOP
+ # Increases the "can running jump" value by 2 per frame while panther dashing unless it's at 8 or higher, at which
+ # point the player should be at the max panther dash speed.
+ 0x00074402, # SRL T0, A3, 16
+ 0x29083F7F, # SLTI T0, T0, 0x3F7F
+ 0x11000006, # BEQZ T0, [forward 0x06]
+ 0x3C098039, # LUI T1, 0x8039
+ 0x912A9BC1, # LBU T2, 0x9BC1 (T1)
+ 0x254A0002, # ADDIU T2, T2, 0x0002
+ 0x294B0008, # SLTI T3, T2, 0x0008
+ 0x55600001, # BNEZL T3, [forward 0x01]
+ 0xA12A9BC1, # SB T2, 0x9BC1 (T1)
+ 0x03200008, # JR T9
+ 0x00000000, # NOP
+ # Makes running jumps only work while the "can running jump" value is at 0. Otherwise, their state won't change.
+ 0x3C010001, # LUI AT, 0x0001
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91089BC1, # LBU T0, 0x9BC1 (T0)
+ 0x55000001, # BNEZL T0, [forward 0x01]
+ 0x3C010000, # LUI AT, 0x0000
+ 0x03E00008 # JR RA
+]
+
+gondola_skipper = [
+ # Upon stepping on one of the gondolas in Tunnel to activate it, this will instantly teleport you to the other end
+ # of the gondola course depending on which one activated, skipping the entire 3-minute wait to get there.
+ 0x3C088039, # LUI T0, 0x8039
+ 0x240900FF, # ADDIU T1, R0, 0x00FF
+ 0xA1099EE1, # SB T1, 0x9EE1 (T0)
+ 0x31EA0020, # ANDI T2, T7, 0x0020
+ 0x3C0C3080, # LUI T4, 0x3080
+ 0x358C9700, # ORI T4, T4, 0x9700
+ 0x154B0003, # BNE T2, T3, [forward 0x03]
+ 0x24090002, # ADDIU T1, R0, 0x0002
+ 0x24090003, # ADDIU T1, R0, 0x0003
+ 0x3C0C7A00, # LUI T4, 0x7A00
+ 0xA1099EE3, # SB T1, 0x9EE3 (T0)
+ 0xAD0C9EE4, # SW T4, 0x9EE4 (T0)
+ 0x3C0D0010, # LUI T5, 0x0010
+ 0x25AD0010, # ADDIU T5, T5, 0x0010
+ 0xAD0D9EE8, # SW T5, 0x9EE8 (T0)
+ 0x08063E68 # J 0x8018F9A0
+]
+
+mandragora_with_nitro_setter = [
+ # When setting a Nitro, if Mandragora is in the inventory too and the wall's "Mandragora set" flag is not set, this
+ # will automatically subtract a Mandragora from the inventory and set its flag so the wall can be blown up in just
+ # one interaction instead of two.
+ 0x3C088039, # LUI T0, 0x8039
+ 0x81099EE1, # LB T1, 0x9EE1 (T0)
+ 0x240A000C, # ADDIU T2, R0, 0x000C
+ 0x112A000E, # BEQ T1, T2, [forward 0x0E]
+ 0x81099C18, # LB T1, 0x9C18 (T0)
+ 0x31290002, # ANDI T1, T1, 0x0002
+ 0x11200009, # BEQZ T1, [forward 0x09]
+ 0x91099C5D, # LBU T1, 0x9C5D (T0)
+ 0x11200007, # BEQZ T1, [forward 0x07]
+ 0x910B9C1A, # LBU T3, 0x9C1A (T0)
+ 0x316A0001, # ANDI T2, T3, 0x0001
+ 0x15400004, # BNEZ T2, [forward 0x04]
+ 0x2529FFFF, # ADDIU T1, T1, 0xFFFF
+ 0xA1099C5D, # SB T1, 0x9C5D (T0)
+ 0x356B0001, # ORI T3, T3, 0x0001
+ 0xA10B9C1A, # SB T3, 0x9C1A (T0)
+ 0x08000512, # J 0x80001448
+ 0x00000000, # NOP
+ 0x810B9BF2, # LB T3, 0x9BF2 (T0)
+ 0x31690040, # ANDI T1, T3, 0x0040
+ 0x11200008, # BEQZ T1, [forward 0x08]
+ 0x91099C5D, # LBU T1, 0x9C5D (T0)
+ 0x11200006, # BEQZ T1, [forward 0x06]
+ 0x316A0080, # ANDI T2, T3, 0x0080
+ 0x15400004, # BNEZ T2, 0x803FE0E8
+ 0x2529FFFF, # ADDIU T1, T1, 0xFFFF
+ 0xA1099C5D, # SB T1, 0x9C5D (T0)
+ 0x356B0080, # ORI T3, T3, 0x0080
+ 0xA10B9BF2, # SB T3, 0x9BF2 (T0)
+ 0x08000512 # J 0x80001448
+]
+
+ambience_silencer = [
+ # Silences all map-specific ambience when loading into a different map, so we don't have to live with, say, Tower of
+ # Science/Clock Tower machinery noises everywhere until either resetting, dying, or going into a map that is
+ # normally set up to disable said noises.
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91089EE1, # LBU T0, 0x9EE1 (T0)
+ 0x24090012, # ADDIU T1, R0, 0x0012
+ 0x11090003, # BEQ T0, T1, [forward 0x03]
+ 0x00000000, # NOP
+ 0x0C004FAB, # JAL 0x80013EAC
+ 0x3404818C, # ORI A0, R0, 0x818C
+ 0x0C004FAB, # JAL 0x80013EAC
+ 0x34048134, # ORI A0, R0, 0x8134
+ 0x0C004FAB, # JAL 0x80013EAC
+ 0x34048135, # ORI A0, R0, 0x8135
+ 0x0C004FAB, # JAL 0x80013EAC
+ 0x34048136, # ORI A0, R0, 0x8136
+ 0x08054987, # J 0x8015261C
+ 0x00000000, # NOP
+ # Plays the fan ambience when loading into the fan meeting room if this detects the active character's cutscene flag
+ # here already being set.
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91099EE1, # LBU T1, 0x9EE1 (T0)
+ 0x240A0019, # ADDIU T2, R0, 0x0019
+ 0x152A000A, # BNE T1, T2, [forward 0x0A]
+ 0x910B9BFE, # LBU T3, 0x9BFE (T0)
+ 0x910C9C3D, # LBU T4, 0x9C3D (T0)
+ 0x240D0001, # ADDIU T5, R0, 0x0001
+ 0x55800001, # BNEZL T4, [forward 0x01]
+ 0x240D0002, # ADDIU T5, R0, 0x0002
+ 0x016D7024, # AND T6, T3, T5
+ 0x11C00003, # BEQZ T6, [forward 0x03]
+ 0x00000000, # NOP
+ 0x0C0052B4, # JAL 0x80014AD0
+ 0x34040169, # ORI A0, R0, 0x0169
+ 0x0805581C # J 0x80156070
+]
+
+coffin_cutscene_skipper = [
+ # Kills the normally-unskippable "Found a hidden path" cutscene at the end of Villa if this detects, in the current
+ # module in the modules array, the cutscene's module number of 0x205C and the "skip" value 0f 0x01 normally set by
+ # all cutscenes upon pressing Start.
+ 0x10A0000B, # BEQZ A1, [forward 0x0B]
+ 0x00000000, # NOP
+ 0x94A80000, # LHU T0, 0x0000 (A1)
+ 0x2409205C, # ADDIU T1, R0, 0x205C
+ 0x15090007, # BNE T0, T1, [forward 0x07]
+ 0x90AA0070, # LBU T2, 0x0070 (A1)
+ 0x11400005, # BEQZ T2, [forward 0x05]
+ 0x90AB0009, # LBU T3, 0x0009 (A1)
+ 0x240C0003, # ADDIU T4, R0, 0x0003
+ 0x156C0002, # BNE T3, T4, [forward 0x02]
+ 0x240B0004, # ADDIU T3, R0, 0x0004
+ 0xA0AB0009, # SB T3, 0x0009 (A1)
+ 0x03E00008 # JR RA
+]
+
+multiworld_item_name_loader = [
+ # When picking up an item from another world, this will load from ROM the custom message for that item explaining
+ # in the item textbox what the item is and who it's for. The flag index it calculates determines from what part of
+ # the ROM to load the item name from. If the item being picked up is a white jewel or a contract, it will always
+ # load from a part of the ROM that has nothing in it to ensure their set "flag" values don't yield unintended names.
+ 0x3C088040, # LUI T0, 0x8040
+ 0xAD03E238, # SW V1, 0xE238 (T0)
+ 0x92080039, # LBU T0, 0x0039 (S0)
+ 0x11000003, # BEQZ T0, [forward 0x03]
+ 0x24090012, # ADDIU T1, R0, 0x0012
+ 0x15090003, # BNE T0, T1, [forward 0x03]
+ 0x24080000, # ADDIU T0, R0, 0x0000
+ 0x10000010, # B [forward 0x10]
+ 0x24080000, # ADDIU T0, R0, 0x0000
+ 0x920C0055, # LBU T4, 0x0055 (S0)
+ 0x8E090058, # LW T1, 0x0058 (S0)
+ 0x1120000C, # BEQZ T1, [forward 0x0C]
+ 0x298A0011, # SLTI T2, T4, 0x0011
+ 0x51400001, # BEQZL T2, [forward 0x01]
+ 0x258CFFED, # ADDIU T4, T4, 0xFFED
+ 0x240A0000, # ADDIU T2, R0, 0x0000
+ 0x00094840, # SLL T1, T1, 1
+ 0x5520FFFE, # BNEZL T1, [backward 0x02]
+ 0x254A0001, # ADDIU T2, T2, 0x0001
+ 0x240B0020, # ADDIU T3, R0, 0x0020
+ 0x018B0019, # MULTU T4, T3
+ 0x00004812, # MFLO T1
+ 0x012A4021, # ADDU T0, T1, T2
+ 0x00084200, # SLL T0, T0, 8
+ 0x3C0400BB, # LUI A0, 0x00BB
+ 0x24847164, # ADDIU A0, A0, 0x7164
+ 0x00882020, # ADD A0, A0, T0
+ 0x3C058018, # LUI A1, 0x8018
+ 0x34A5BF98, # ORI A1, A1, 0xBF98
+ 0x0C005DFB, # JAL 0x800177EC
+ 0x24060100, # ADDIU A2, R0, 0x0100
+ 0x3C088040, # LUI T0, 0x8040
+ 0x8D03E238, # LW V1, 0xE238 (T0)
+ 0x3C1F8012, # LUI RA, 0x8012
+ 0x27FF5BA4, # ADDIU RA, RA, 0x5BA4
+ 0x0804EF54, # J 0x8013BD50
+ 0x94640002, # LHU A0, 0x0002 (V1)
+ # Changes the Y screen position of the textbox depending on how many line breaks there are.
+ 0x3C088019, # LUI T0, 0x8019
+ 0x9108C097, # LBU T0, 0xC097 (T0)
+ 0x11000005, # BEQZ T0, [forward 0x05]
+ 0x2508FFFF, # ADDIU T0, T0, 0xFFFF
+ 0x11000003, # BEQZ T0, [forward 0x03]
+ 0x00000000, # NOP
+ 0x1000FFFC, # B [backward 0x04]
+ 0x24C6FFF1, # ADDIU A2, A2, 0xFFF1
+ 0x0804B33F, # J 0x8012CCFC
+ # Changes the length and number of lines on the textbox if there's a multiworld message in the buffer.
+ 0x3C088019, # LUI T0, 0x8019
+ 0x9108C097, # LBU T0, 0xC097 (T0)
+ 0x11000003, # BEQZ T0, [forward 0x03]
+ 0x00000000, # NOP
+ 0x00082821, # ADDU A1, R0, T0
+ 0x240600B6, # ADDIU A2, R0, 0x00B6
+ 0x0804B345, # J 0x8012CD14
+ 0x00000000, # NOP
+ # Redirects the text to the multiworld message buffer if a message exists in it.
+ 0x3C088019, # LUI T0, 0x8019
+ 0x9108C097, # LBU T0, 0xC097 (T0)
+ 0x11000004, # BEQZ T0, [forward 0x04]
+ 0x00000000, # NOP
+ 0x3C048018, # LUI A0, 0x8018
+ 0x3484BF98, # ORI A0, A0, 0xBF98
+ 0x24050000, # ADDIU A1, R0, 0x0000
+ 0x0804B39F, # J 0x8012CE7C
+ # Copy the "item from player" text when being given an item through the multiworld via the game's copy function.
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x00000000, # NOP
+ 0x3C088040, # LUI T0, 0x8040
+ 0xAD1FE33C, # SW RA, 0xE33C (T0)
+ 0xA104E338, # SB A0, 0xE338 (T0)
+ 0x3C048019, # LUI A0, 0x8019
+ 0x2484C0A8, # ADDIU A0, A0, 0xC0A8
+ 0x3C058019, # LUI A1, 0x8019
+ 0x24A5BF98, # ADDIU A1, A1, 0xBF98
+ 0x0C000234, # JAL 0x800008D0
+ 0x24060100, # ADDIU A2, R0, 0x0100
+ 0x3C088040, # LUI T0, 0x8040
+ 0x8D1FE33C, # LW RA, 0xE33C (T0)
+ 0x0804EDCE, # J 0x8013B738
+ 0x9104E338, # LBU A0, 0xE338 (T0)
+ 0x00000000, # NOP
+ # Neuters the multiworld item text buffer if giving a non-multiworld item through the in-game remote item rewarder
+ # byte before then jumping to item_prepareTextbox.
+ 0x24080011, # ADDIU T0, R0, 0x0011
+ 0x10880004, # BEQ A0, T0, [forward 0x04]
+ 0x24080012, # ADDIU T0, R0, 0x0012
+ 0x10880002, # BEQ A0, T0, [forward 0x02]
+ 0x3C088019, # LUI T0, 0x8019
+ 0xA100C097, # SB R0, 0xC097 (T0)
+ 0x0804EDCE # J 0x8013B738
+]
+
+ice_trap_initializer = [
+ # During a map load, creates the module that allows the ice block model to appear while in the frozen state if not
+ # on the intro narration map (map 0x16).
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91089EE1, # LBU T0, 0x9EE1 (T0)
+ 0x24090016, # ADDIU T1, R0, 0x0016
+ 0x11090004, # BEQ T0, T1, [forward 0x04]
+ 0x3C048034, # LUI A0, 0x8034
+ 0x24842ACC, # ADDIU A0, A0, 0x2ACC
+ 0x08000660, # J 0x80001980
+ 0x24052125, # ADDIU A1, R0, 0x2125
+ 0x03E00008 # JR RA
+]
+
+the_deep_freezer = [
+ # Writes 000C0000 into the player state to freeze the player on the spot if Ice Traps have been received, writes the
+ # Ice Trap code into the pointer value (0x20B8, which is also Camilla's boss code),and decrements the Ice Traps
+ # remaining counter. All after verifying the player is in a "safe" state to be frozen in.
+ 0x3C0B8039, # LUI T3, 0x8039
+ 0x91699BE2, # LBU T3, 0x9BE2 (T0)
+ 0x11200015, # BEQZ T1, [forward 0x15]
+ 0x3C088034, # LUI T0, 0x8034
+ 0x910827A9, # LBU T0, 0x27A9 (T0)
+ 0x240A0005, # ADDIU T2, R0, 0x0005
+ 0x110A0011, # BEQ T0, T2, [forward 0x11]
+ 0x240A000C, # ADDIU T2, R0, 0x000C
+ 0x110A000F, # BEQ T0, T2, [forward 0x0F]
+ 0x240A0002, # ADDIU T2, R0, 0x0002
+ 0x110A000D, # BEQ T0, T2, [forward 0x0D]
+ 0x240A0008, # ADDIU T2, R0, 0x0008
+ 0x110A000B, # BEQ T0, T2, [forward 0x0B]
+ 0x2529FFFF, # ADDIU T1, T1, 0xFFFF
+ 0xA1699BE2, # SB T1, 0x9BE2 (T3)
+ 0x3C088034, # LUI T0, 0x8034
+ 0x3C09000C, # LUI T1, 0x000C
+ 0xAD0927A8, # SW T1, 0x27A8 (T0)
+ 0x240C20B8, # ADDIU T4, R0, 0x20B8
+ 0xA56C9E6E, # SH T4, 0x9E6E (T3)
+ 0x8D0927C8, # LW T1, 0x27C8 (T0)
+ 0x912A0048, # LBU T2, 0x0068 (T1)
+ 0x314A007F, # ANDI T2, T2, 0x007F
+ 0xA12A0048, # SB T2, 0x0068 (T1)
+ 0x03E00008 # JR RA
+]
+
+freeze_verifier = [
+ # Verifies for the ice chunk module that a freeze should spawn the ice model. The player must be in the frozen state
+ # (0x000C) and 0x20B8 must be in either the freeze pointer value or the current boss ID (Camilla's); otherwise, we
+ # weill assume that the freeze happened due to a vampire grab or Actrise shard tornado and not spawn the ice chunk.
+ 0x8C4E000C, # LW T6, 0x000C (V0)
+ 0x00803025, # OR A2, A0, R0
+ 0x8DC30008, # LW V1, 0x0008 (T6)
+ 0x3C088039, # LUI T0, 0x8039
+ 0x240920B8, # ADDIU T1, R0, 0x20B8
+ 0x950A9E72, # LHU T2, 0x9E72 (T0)
+ 0x3C0C8034, # LUI T4, 0x8034
+ 0x918C27A9, # LBU T4, 0x27A9 (T4)
+ 0x240D000C, # ADDIU T5, R0, 0x000C
+ 0x158D0004, # BNE T4, T5, [forward 0x04]
+ 0x3C0B0F00, # LUI T3, 0x0F00
+ 0x112A0005, # BEQ T1, T2, [forward 0x05]
+ 0x950A9E78, # LHU T2, 0x9E78 (T0)
+ 0x112A0003, # BEQ T1, T2, [forward 0x03]
+ 0x357996A0, # ORI T9, T3, 0x96A0
+ 0x03200008, # JR T9
+ 0x00000000, # NOP
+ 0x35799640, # ORI T9, T3, 0x9640
+ 0x03200008, # JR T9
+]
+
+countdown_extra_safety_check = [
+ # Checks to see if the multiworld message is a red flashing trap before then truly deciding to decrement the
+ # Countdown number. This was a VERY last minute thing I caught, since Ice Traps for other CV64 players can take the
+ # appearance of majors with no other way of the game knowing.
+ 0x3C0B8019, # LUI T3, 0x8019
+ 0x956BBF98, # LHU T3, 0xBF98 (T3)
+ 0x240C0000, # ADDIU T4, R0, 0x0000
+ 0x358CA20B, # ORI T4, T4, 0xA20B
+ 0x556C0001, # BNEL T3, T4, [forward 0x01]
+ 0xA1099CA4, # SB T1, 0x9CA4 (T0)
+ 0x03E00008 # JR RA
+]
+
+countdown_demo_hider = [
+ # Hides the Countdown number if we are not in the Gameplay state (state 2), which would happen if we were in the
+ # Demo state (state 9). This is to ensure the demo maps' number is not peep-able before starting a run proper, for
+ # the sake of preventing a marginal unfair advantage. Otherwise, updates the number once per frame.
+ 0x3C088039, # LUI T0, 0x8039
+ 0x91089EE1, # LBU T0, 0x9EE1 (T0)
+ 0x3C098040, # LUI T1, 0x8040
+ 0x01284821, # ADDU T1, T1, T0
+ 0x0C0FF507, # JAL 0x803FD41C
+ 0x9124D6DC, # LBU A0, 0xD6DC (T1)
+ 0x3C088034, # LUI T0, 0x8034
+ 0x91092087, # LBU T0, 0x2087 (T0)
+ 0x240A0002, # ADDIU T2, R0, 0x0002
+ 0x112A0003, # BEQ T1, T2, [forward 0x03]
+ 0x3C048040, # LUI A0, 0x8040
+ 0x8C84D6D4, # LW A0, 0xD6D4 (A0)
+ 0x0C0FF59F, # JAL 0x803FD67C
+ 0x24050000, # ADDIU A1, R0, 0x0000
+ 0x080FF411, # J 0x803FD044
+]
+
+item_drop_spin_corrector = [
+ # Corrects how far AP-placed items drop and how fast they spin based on what appearance they take.
+
+ # Pickup actor ID table for the item appearance IDs to reference.
+ 0x01020304,
+ 0x05060708,
+ 0x090A0B0C,
+ 0x100D0E0F,
+ 0x11121314,
+ 0x15161718,
+ 0x191D1E1F,
+ 0x20212223,
+ 0x24252627,
+ 0x28291A1B,
+ 0x1C000000,
+ 0x00000000,
+ # Makes AP-placed items in 1-hit breakables drop to their correct, dev-intended height depending on what appearance
+ # we gave it. Primarily intended for the Axe and the Cross to ensure they don't land half buried in the ground.
+ 0x000C4202, # SRL T0, T4, 8
+ 0x318C00FF, # ANDI T4, T4, 0x00FF
+ 0x11000003, # BEQZ T0, [forward 0x03]
+ 0x3C098040, # LUI T1, 0x8040
+ 0x01284821, # ADDU T1, T1, T0
+ 0x912CE7DB, # LBU T4, 0xE7D8
+ 0x03E00008, # JR RA
+ 0xAC600000, # SW R0, 0x0000 (V1)
+ 0x00000000, # NOP
+ # Makes items with changed appearances spin at their correct speed. Unless it's a local Ice Trap, wherein it will
+ # instead spin at the speed it isn't supposed to.
+ 0x920B0040, # LBU T3, 0x0040 (S0)
+ 0x1160000D, # BEQZ T3, [forward 0x0D]
+ 0x3C0C8040, # LUI T4, 0x8040
+ 0x016C6021, # ADDU T4, T3, T4
+ 0x918CE7DB, # LBU T4, 0xE7DB (T4)
+ 0x258CFFFF, # ADDIU T4, T4, 0xFFFF
+ 0x240D0011, # ADDIU T5, R0, 0x0011
+ 0x154D0006, # BNE T2, T5, [forward 0x06]
+ 0x29AE0006, # SLTI T6, T5, 0x0006
+ 0x240A0001, # ADDIU T2, R0, 0x0001
+ 0x55C00001, # BNEZL T6, [forward 0x01]
+ 0x240A0007, # ADDIU T2, R0, 0x0007
+ 0x10000002, # B [forward 0x02]
+ 0x00000000, # NOP
+ 0x258A0000, # ADDIU T2, T4, 0x0000
+ 0x08049648, # J 0x80125920
+ 0x3C028017, # LUI V0, 0x8017
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ 0x00000000,
+ # Makes AP-placed items in 3-hit breakables drop to their correct, dev-intended height depending on what appearance
+ # we gave it.
+ 0x00184202, # SRL T0, T8, 8
+ 0x331800FF, # ANDI T8, T8, 0x00FF
+ 0x11000003, # BEQZ T0, [forward 0x03]
+ 0x3C098040, # LUI T1, 0x8040
+ 0x01284821, # ADDU T1, T1, T0
+ 0x9138E7DB, # LBU T8, 0xE7D8
+ 0x03E00008, # JR RA
+ 0xAC60FFD8, # SW R0, 0xFFD8 (V1)
+ 0x00000000,
+ # Makes AP-placed items in the Villa chandelier drop to their correct, dev-intended height depending on what
+ # appearance we gave it. (why must this singular breakable be such a problem child with its own code? :/)
+ 0x000D4202, # SRL T0, T5, 8
+ 0x31AD00FF, # ANDI T5, T5, 0x00FF
+ 0x11000003, # BEQZ T0, [forward 0x03]
+ 0x3C098040, # LUI T1, 0x8040
+ 0x01284821, # ADDU T1, T1, T0
+ 0x912DE7DB, # LBU T5, 0xE7D8
+ 0x03E00008, # JR RA
+ 0xAC60FFD8, # SW R0, 0xFFD8 (V1)
+]
+
+big_tosser = [
+ # Makes every hit the player takes that does not immobilize them send them flying backwards with the power of
+ # Behemoth's charge.
+ 0x3C0A8038, # LUI T2, 0x8038
+ 0x914A7D7E, # LBU T2, 0x7D7E (T2)
+ 0x314A0020, # ANDI T2, T2, 0x0020
+ 0x1540000D, # BEQZ T2, [forward 0x0D]
+ 0x3C0A800E, # LUI T2, 0x800E
+ 0x954B8290, # LHU T3, 0x8290 (T2)
+ 0x356B2000, # ORI T3, T3, 0x2000
+ 0xA54B8290, # SH T3, 0x8290 (T2)
+ 0x3C0C8035, # LUI T4, 0x8035
+ 0x958C09DE, # LHU T4, 0x09DE (T4)
+ 0x258C8000, # ADDIU T4, T4, 0x8000
+ 0x3C0D8039, # LUI T5, 0x8039
+ 0xA5AC9CF0, # SH T4, 0x9CF0 (T5)
+ 0x3C0C4160, # LUI T4, 0x4160
+ 0xADAC9CF4, # SW T4, 0x9CF4 (T5)
+ 0x3C0C4040, # LUI T4, 0x4040
+ 0xADAC9CF8, # SW T4, 0x9CF8 (T5)
+ 0x03E00008, # JR RA
+ 0x8C680048, # LW T0, 0x0048 (V1)
+ 0x00000000,
+ 0x00000000,
+ # Allows pressing A while getting launched to cancel all XZ momentum. Useful for saving oneself from getting
+ # launched into an instant death trap.
+ 0x3C088038, # LUI T0, 0x8038
+ 0x91087D80, # LBU T0, 0x7D80 (T0)
+ 0x31090080, # ANDI T1, T0, 0x0080
+ 0x11200009, # BEQZ T1, [forward 0x09]
+ 0x3C088035, # LUI T0, 0x8035
+ 0x8D0A079C, # LW T2, 0x079C (T0)
+ 0x3C0B000C, # LUI T3, 0x000C
+ 0x256B4000, # ADDIU T3, T3, 0x4000
+ 0x014B5024, # AND T2, T2, T3
+ 0x154B0003, # BNE T2, T3, [forward 0x03]
+ 0x00000000, # NOP
+ 0xAD00080C, # SW R0, 0x080C (T0)
+ 0xAD000814, # SW R0, 0x0814 (T0)
+ 0x03200008 # JR T9
+]
+
+dog_bite_ice_trap_fix = [
+ # Sets the freeze timer to 0 when a maze garden dog bites the player to ensure the ice chunk model will break if the
+ # player gets bitten while frozen via Ice Trap.
+ 0x3C088039, # LUI T0, 0x8039
+ 0xA5009E76, # SH R0, 0x9E76 (T0)
+ 0x3C090F00, # LUI T1, 0x0F00
+ 0x25291CB8, # ADDIU T1, T1, 0x1CB8
+ 0x01200008 # JR T1
+]
diff --git a/worlds/cv64/data/rname.py b/worlds/cv64/data/rname.py
new file mode 100644
index 000000000000..851ee618af05
--- /dev/null
+++ b/worlds/cv64/data/rname.py
@@ -0,0 +1,63 @@
+forest_of_silence = "Forest of Silence"
+forest_start = "Forest of Silence: first half"
+forest_mid = "Forest of Silence: second half"
+forest_end = "Forest of Silence: end area"
+
+castle_wall = "Castle Wall"
+cw_start = "Castle Wall: main area"
+cw_exit = "Castle Wall: exit room"
+cw_ltower = "Castle Wall: left tower"
+
+villa = "Villa"
+villa_start = "Villa: dog gates"
+villa_main = "Villa: main interior"
+villa_storeroom = "Villa: storeroom"
+villa_archives = "Villa: archives"
+villa_maze = "Villa: maze"
+villa_servants = "Villa: servants entrance"
+villa_crypt = "Villa: crypt"
+
+tunnel = "Tunnel"
+tunnel_start = "Tunnel: first half"
+tunnel_end = "Tunnel: second half"
+
+underground_waterway = "Underground Waterway"
+uw_main = "Underground Waterway: main area"
+uw_end = "Underground Waterway: end"
+
+castle_center = "Castle Center"
+cc_main = "Castle Center: main area"
+cc_crystal = "Castle Center: big crystal"
+cc_torture_chamber = "Castle Center: torture chamber"
+cc_library = "Castle Center: library"
+cc_elev_top = "Castle Center: elevator top"
+
+duel_tower = "Duel Tower"
+dt_main = "Duel Tower"
+
+tower_of_sorcery = "Tower of Sorcery"
+tosor_main = "Tower of Sorcery"
+
+tower_of_execution = "Tower of Execution"
+toe_main = "Tower of Execution: main area"
+toe_ledge = "Tower of Execution: gated ledge"
+
+tower_of_science = "Tower of Science"
+tosci_start = "Tower of Science: turret lab"
+tosci_three_doors = "Tower of Science: locked key1 room"
+tosci_conveyors = "Tower of Science: spiky conveyors"
+tosci_key3 = "Tower of Science: locked key3 room"
+
+room_of_clocks = "Room of Clocks"
+roc_main = "Room of Clocks"
+
+clock_tower = "Clock Tower"
+ct_start = "Clock Tower: start"
+ct_middle = "Clock Tower: middle"
+ct_end = "Clock Tower: end"
+
+castle_keep = "Castle Keep"
+ck_main = "Castle Keep: exterior"
+ck_drac_chamber = "Castle Keep: Dracula's chamber"
+
+renon = "Renon's shop"
diff --git a/worlds/cv64/docs/en_Castlevania 64.md b/worlds/cv64/docs/en_Castlevania 64.md
new file mode 100644
index 000000000000..55f7eb03012f
--- /dev/null
+++ b/worlds/cv64/docs/en_Castlevania 64.md
@@ -0,0 +1,148 @@
+# Castlevania 64
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
+config file.
+
+## What does randomization do to this game?
+
+All items that you would normally pick up throughout the game, be it from candles, breakables, or sitting out, have been
+moved around. This includes the key items that the player would normally need to find to progress in some stages, which can
+now be found outside their own stages, so returning to previously-visited stages will very likely be necessary (see: [How do
+I jump to a different stage?](#how-do-i-jump-to-a-different-stage?)). The positions of the stages can optionally be randomized
+too, so you may start out in Duel Tower and get Forest of Silence as your penultimate stage before Castle Keep, amongst
+many other possibilities.
+
+## How do I jump to a different stage?
+
+Instant travel to an earlier or later stage is made possible through the Warp Menu, a major addition to the game that can
+be pulled up while not in a boss fight by pressing START while holding Z and R. By finding Special1 jewels (the item that
+unlocks Hard Mode in vanilla Castlevania 64), more destinations become available to be selected on this menu. The destinations
+on the list are randomized per seed and the first one, which requires no Special1s to select, will always be your starting
+area.
+
+NOTE: Regardless of which option on the menu you are currently highlighting, you can hold Z or R while making your selection
+to return to Villa's crypt or Castle Center's top elevator room respectively, provided you've already been to that place at
+least once. This can make checking out both character stages at the start of a route divergence far less of a hassle.
+
+## Can I do everything as one character?
+
+Yes! The Villa end-of-level coffin has had its behavior modified so that which character stage slot it sends you to
+depends on the time of day, and likewise both bridges at the top of Castle Center's elevator are intact so both exits are
+reachable regardless of who you are. With these changes in game behavior, every stage can be accessed by any character
+in a singular run.
+
+NOTE: By holding L while loading into a map (this can even be done while cancelling out of the Warp Menu), you can swap to
+the other character you are not playing as, and/or hold C-Up to swap to and from the characters' alternate costumes. Unless
+you have Carrie Logic enabled, and you are not playing as her, switching should never be necessary.
+
+## What is the goal of Castlevania 64 when randomized?
+
+Make it to Castle Keep, enter Dracula's chamber, and defeat him to trigger an ending and complete your goal. Whether you
+get your character's good or bad ending does **not** matter; the goal will send regardless. Options exist to force a specific
+ending for those who prefer a specific one.
+
+Dracula's chamber's entrance door is initially locked until whichever of the following objectives that was specified on your
+YAML under `draculas_condition` is completed:
+- `crystal`: Activate the big crystal in the basement of Castle Center. Doing this entails finding two Magical Nitros and
+two Mandragoras to blow up both cracked walls (see: [How does the Nitro transport work in this?](#how-does-the-nitro-transport-work-in-this?)).
+Behemoth and Rosa/Camilla do **NOT** have to be defeated.
+- `bosses`: Kill bosses with visible health meters to earn Trophies. The number of Trophies required can be specified under
+`bosses_required`.
+- `special2s`: Find enough Special2 jewels (the item that normally unlocks alternate costumes) that are shuffled in the
+regular item pool. The total amount and percent needed can be specified under `total_special2s` and `percent_special2s_required` respectively.
+
+If `none` was specified, then there is no objective. Dracula's chamber door is unlocked from the start, and you merely have to reach it.
+
+## What items and locations get shuffled?
+
+Inventory items, jewels, moneybags, and PowerUps are all placed in the item pool by default. Randomizing Sub-weapons is optional,
+and they can be shuffled in their own separate pool or in the main item pool. An optional hack can be enabled to make your
+old sub-weapon drop behind you when you receive a different one, so you can pick it up again if you still want it. Location
+checks by default include freestanding items, items from one-hit breakables, and the very few items given through NPC text. Additional
+locations that can be toggled are:
+- Objects that break in three hits.
+- Sub-weapon locations if they have been shuffled anywhere.
+- Seven items sold by the shopkeeper Renon.
+- The two items beyond the crawlspace in Waterway that normally require Carrie, if Carrie Logic is on.
+- The six items inside the Lizard-man generators in Castle Center that open randomly to spawn Lizard-men. These are particularly annoying!
+
+## How does the Nitro transport work in this?
+
+Two Magical Nitros and two Mandragoras are placed into the item pool for blowing up the cracked walls in Castle Center
+and two randomized items are placed on both of their shelves. The randomized Magical Nitro will **NOT** kill you upon landing
+or taking damage, so don't panic when you receive one! Hazardous Waste Dispoal bins are disabled and the basement crack with
+a seal will not let you set anything at it until said seal is removed so none of the limited ingredients can be wasted.
+
+In short, Nitro is still in, explode-y business is not! Unless you turn on explosive DeathLink, that is...
+
+## Which items can be in another player's world?
+
+Any of the items which can be shuffled may also be placed into another player's world. The exception is if sub-weapons
+are shuffled in their own pool, in which case they will only appear in your world in sub-weapon spots.
+
+## What does another world's item look like in Castlevania 64?
+
+An item belonging to another world will show up as that item if it's from another Castlevania 64 world, or one of two
+Archipelago logo icons if it's from a different game entirely. If the icon is big and has an orange arrow in the top-right
+corner, it is a progression item for that world; definitely get these! Otherwise, if it's small and with no arrow, it is
+either filler, useful, or a trap.
+
+When you pick up someone else's item, you will not receive anything and the item textbox will show up to announce what you
+found and who it was for. The color of the text will tell you its classification:
+- Light brown-ish : Filler
+- White /Yellow : Useful
+- Yellow /Green : Progression
+- Yellow /Red : Trap
+
+## When the player receives an item, what happens?
+
+A textbox containing the name of the item and the player who sent it will appear, and they will get it.
+Just like the textbox that appears when sending an item, the color of the text will tell you its classification.
+
+NOTE: You can press B to close the item textbox instantly and get through your item queue quicker.
+
+## What tricks and glitches should I know for Hard Logic?
+
+The following tricks always have a chance to be required:
+- Left Tower Skip in Castle Wall
+- Copper Door Skip in Villa (both characters have their own methods for this)
+- Waterfall Skip if you travel backwards into Underground Waterway
+- Slope Jump to Room of Clocks from Castle Keep
+- Jump to the gated ledge from the level above in Tower of Execution
+
+Enabling Carrie Logic will also expect the following:
+
+- Orb-sniping dogs through the front gates in Villa
+
+Library Skip is **NOT** logically expected by any options. The basement arena crack will always logically expect two Nitros
+and two Mandragoras even with Hard Logic on due to the possibility of wasting a pair on the upper wall, after managing
+to skip past it. And plus, the RNG manip may not even be possible after picking up all the items in the Nitro room.
+
+## What are the item name groups?
+The groups you can use for Castlevania 64 are `bomb` and `ingredient`, both of which will hint randomly for either a
+Magical Nitro or Mandragora.
+
+## What are the location name groups?
+In Castlevania 64, every location that is specific to a stage is part of a location group under that stage's name.
+So if you want to exclude all of, say, Duel Tower, you can do so by just excluding "Duel Tower" as a whole.
+
+## I'm stuck and/or I can't find this hinted location...is there a map tracker?
+At the moment, no map tracker exists. [Here](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds/cv64/docs/obscure_checks.md)
+is a list of many checks that someone could very likely miss, with instructions on how to find them. See if the check you
+are missing is on there and if it isn't, or you still can't find it, reach out in the [Archipelago Discord server](https://discord.gg/archipelago)
+to inquire about having the list updated if you think it should be.
+
+If you are new to this randomizer, it is strongly recommended to play with the Countdown option enabled to at least give you a general
+idea of where you should be looking if you get completely stuck. It can track the total number of unchecked locations in the
+area you are currently in, or the total remaining majors.
+
+## Why does the game stop working when I sit on the title screen for too long?
+This is an issue that existed with Castlevania 64 on mupen64plus way back in 2017, and BizHawk never updated their
+mupen64plus core since it was fixed way back then. This is a Castlevania 64 in general problem that happens even with the
+vanilla ROM, so there's not much that can done about it besides opening an issue to them (which [has been done](https://github.com/TASEmulators/BizHawk/issues/3670))
+and hoping they update their mupen64plus core one day...
+
+## How the f*** do I set Nitro/Mandragora?
+(>)
diff --git a/worlds/cv64/docs/obscure_checks.md b/worlds/cv64/docs/obscure_checks.md
new file mode 100644
index 000000000000..6f0e0cdbb34e
--- /dev/null
+++ b/worlds/cv64/docs/obscure_checks.md
@@ -0,0 +1,429 @@
+# Obscure locations in the AP Castlevania 64 randomizer
+
+
+
+## Forest of Silence
+
+#### Invisible bridge platform
+A square platform floating off to the side of the broken bridge between the three-crypt and Werewolf areas. There's an
+invisible bridge connecting it with the middle piece on the broken bridge that you can cross over to reach it. This is
+where you normally get the Special1 in vanilla that unlocks Hard Mode.
+
+### Invisible Items
+#### Dirge maiden pedestal plaque
+This plaque in question can be found on the statue pedestal with a torch on it in the area right after the first switch gate,
+near the cliff where the breakable rock can be found. The plaque reads "A maiden sings a dirge" if you check it after you
+pick up the item there, hence the name of all the locations in this area.
+
+#### Werewolf statue plaque
+The plaque on the statue pedestal in the area inhabited by the Werewolf. Reading this plaque after picking up the item
+says it's "the lady who blesses and restores."
+
+### 3-Hit Breakables
+#### Dirge maiden pedestal rock
+This rock can be found near the cliff behind the empty above-mentioned dirge maiden pedestal. Normally has a ton of money
+in vanilla, contains 5 checks in rando.
+
+#### Bat archway rock
+After the broken bridge containing the invisible pathway to the Special1 in vanilla, this rock is off to the side in front
+of the gate frame with a swarm of bats that come at you, before the Werewolf's territory. Contains 4 checks. If you are new
+to speedrunning the vanilla game and haven't yet learned the RNG manip strats, this is a guaranteed spot to find a PowerUp at.
+
+
+
+## Castle Wall
+#### Above bottom right/left tower door
+These checks are located on top of the bottom doorways inside the both the Left and Right Tower. You have to drop from above
+to reach them. In the case of the left tower, it's probably easiest to wait on the green platform directly above it until it flips.
+
+#### Left tower child ledge
+When you reach the bridge of four rotating green platforms, look towards the pillar in the center of the room (hold C-up to
+enter first person view), and you'll see this. There's an invisible bridge between the rotating platforms and the tiny ledge
+that you can use to get to and from it. In Legacy of Darkness, it is on this ledge where one of Henry's children is found.
+
+### Invisible Items
+#### Sandbag shelf - Left
+If you thought the PowerUp on this shelf in vanilla CV64 was the only thing here, then you'd be wrong! Hidden inside the
+sandbags near the item is another item you can pick up before subsequent checks on this spot yield "only sand and gravel".
+Legacy took this item out entirely, interestingly enough.
+
+### 3-Hit Breakables
+#### Upper rampart savepoint slab
+After killing the two White Dragons and flipping their switch, drop down onto this platform from the top, and you'll find
+it near the White Jewel. Contains 5 checks that are all normally Red Jewels in vanilla, making it an excellent place to
+fuel up at if you're not doing Left Tower Skip. Just be careful of the infinitely spawning skeletons!
+
+#### Dracula switch slab
+Located behind the door that you come out of at the top of the left tower where you encounter Totally Real Dracula in a
+cutscene. Contains 5 checks that are all normally money; take note of all these money spots if you're plaing vanilla and
+plan on trying to trigger the Renon fight.
+
+
+
+## Villa
+#### Outer front gate platform
+From the start of the level, turn right, and you'll see a platform with a torch above a torch on the ground. This upper torch
+is reachable via an invisible platform that you can grab and pull yourself up onto. The PAL version and onwards removed
+this secret entirely, interestingly enough.
+
+#### Front yard cross grave near gates/porch
+In the Villa's front yard area are two cross-shaped grave markers that are actually 1-hit breakables just like torches.
+They contain a check each.
+
+#### Midnight fountain
+At exactly midnight (0:00 on the pause screen), a pillar in the fountain's base will rise to give you access to the six
+checks on the upper parts of the fountain. If you're playing with Disable Time Requirements enabled, this pillar will be
+raised regardless of the current time.
+
+#### Vincent
+Vincent has a check that he will give to you by speaking to him after triggering the Rosa cutscene at 3 AM in the rose
+garden. With Disable Time Requirements enabled, the Rosa cutscene will trigger at any time.
+
+#### Living room ceiling light
+In the rectangular living room with ghosts and flying skulls that come at you, there are two yellow lights on the ceiling
+and one red light between them. The red light can be broken for a check; just jump directly below it and use your c-left
+attack to hit it.
+
+#### Front maze garden - Frankie's right dead-end urn
+When you first enter the maze, before going to trigger the Malus cutscene, go forward, right at the one-way door, then right
+at the T-junction, and you'll reach a dead-end where the Gardner is just going about his business. The urn on the left
+at this dead-end can be broken for a check; it's the ONLY urn in the entire maze that can be broken like this.
+
+#### Crypt bridge upstream
+After unlocking the Copper Door, follow the stream all the way past the bridge to end up at this torch.
+I see many people miss this one.
+
+### Invisible Items
+#### Front yard visitor's tombstone
+The tombstone closest to the Villa building itself, in the top-right corner if approaching from the gates. If you are
+familiar with the puzzle here in Cornell's quest in Legacy, it's the tombstone prepared for "anybody else who drops by
+to visit".
+
+#### Foyer sofa
+The first sofa in the foyer, on the upper floor to the right.
+
+#### Mary's room table
+The table closer to the mirror on the right in the small room adjacent to the bedroom, where Mary would normally be found
+in Cornell's story in Legacy.
+
+#### Dining room rose vase
+The vase of roses in the dining room that a rose falls out of in the cutscene here to warn Reinhardt/Carrie of the vampire
+villager.
+
+#### Living room clawed painting
+The painting with claw marks on it above the fireplace in the middle of the living room.
+
+#### Living room lion head
+The lion head on the left wall of the living room (if you entered from one of the doors to the main hallway).
+
+#### Maze garden exit knight
+The suit of armor in the stairs room before the Maze Garden, where Renon normally introduces himself.
+
+#### Storeroom statue
+The weird statue in the back of the Storeroom. If you check it again after taking its item, the game questions why would
+someone make something like it.
+
+#### Archives table
+The table in the middle of the Archives. In Legacy, this is where Oldrey's diary normally sits if you are playing Cornell.
+
+#### Malus's hiding bush
+The bush that Reinhardt/Carrie find Malus hiding in at the start of the Maze Garden chase sequence.
+
+### 3-Hit Breakables
+#### Foyer chandelier
+The big chandelier above the foyer can be broken for 5 assorted items, all of which become checks in rando with the multi
+hits setting on. This is the only 3-hit breakable in the entire stage.
+
+Here's a fun fact about the chandelier: for some reason, KCEK made this thing a completely separate object from every other
+3-hit breakable in the game, complete with its own distinct code that I had to modify entirely separately as I was making
+this rando to make the 3-hit breakable setting feasible! What fun!
+
+
+
+## Tunnel
+#### Stepping stone alcove
+After the first save following the initial Spider Women encounter, take the first right you see, and you'll arrive back at
+the poison river where there's a second set of stepping stones similar to the one you just jumped across earlier. Jump on
+these to find the secret alcove containing one or two checks depending on whether sub-weapons are randomized anywhere or not.
+
+### Sun/Moon Doors
+
+In total, there are six of these throughout the entire stage. One of them you are required to open in order to leave the stage,
+while the other five lead to optional side rooms containing items. These are all very skippable in the vanilla game, but in a
+rando context, it is obviously *very* important you learn where all of them are for when the day comes that a Hookshot
+lands behind one! If it helps, two of them are before the gondolas, while all the rest are after them.
+
+#### Lonesome bucket moon door
+After you ride up on the second elevator, turn left at the first split path you see, and you will find, as I called it, the
+"Lonesome bucket". Keep moving forward past this, and you will arrive at the moon door. The only thing of value, beside a shop
+point, is a sub-weapon location. So if you don't have sub-weapons shuffled anywhere, you can very much skip this one.
+
+#### Gondola rock crusher sun door
+Once you get past the first poison pit that you are literally required to platform over, go forward at the next junction
+instead of left (going left is progress and will take you to the rock crusher right before the gondolas). This door notably
+hides two Roast Beefs normally, making it probably the most worthwhile one to visit in vanilla.
+
+#### Corpse bucket moon door
+After the poison pit immediately following the gondola ride, you will arrive at a bucket surrounded by corpses (hence the name).
+Go left here, and you will arrive at this door.
+
+#### Shovel zone moon door
+On the straight path to the end-of-level sun door are two separate poison pits on the right that you can platform over.
+Both of these lead to and from the same optional area, the "shovel zone" as I call it due to the random shovel you can find
+here. Follow the path near the shovel that leads away from both poison pits, and you'll arrive at a junction with a save jewel.
+Go straight on at this junction to arrive at this moon door. This particular one is more notable in Legacy of Darkness as it
+contains one of the locations of Henry's children.
+
+#### Shovel zone sun door
+Same as the above moon door, but go left at the save jewel junction instead of straight.
+
+### Invisible Items
+#### Twin arrow signs
+From the save point after the stepping stones following the initial Spider Women encounter, travel forward until you reach a
+T-junction with two arrow signs at it. The right-pointing sign here contains an item on its post.
+
+#### Near lonesome bucket
+After riding the first upwards elevator following turning left at the twin arrow signs, you'll arrive at the lonesome bucket
+area, with said bucket being found if you turn left at the first opportunity after said elevator. The item here is not
+found *in* the bucket, but rather on a completely unmarked spot some meters from it. This had to have been a mistake,
+seeing as Legacy moved it to actually be in the bucket.
+
+#### Shovel
+Can be found by taking either platforming course on the right side of the straightaway to the end after the gondolas.
+This entire zone is noteable for the fact that there's no reason for Reinhardt to come here in either game; it's only ever
+required for Henry to rescue one of his children.
+
+### 3-Hit Breakables
+#### Twin arrow signs rock
+Turn right at the twin arrow signs junction, and you'll find this rock at the dead-end by the river. It contains a bunch of
+healing and status items that translate into 5 rando checks.
+
+#### Lonesome bucket poison pit rock
+Near the lonesome bucket is the start of a platforming course over poison water that connects near Albert's campsite...which
+you could reach anyway just by traveling forward at the prior junction instead of left. So what's the point of this poison
+pit, then? Look out into the middle of it, and you'll see this rock on a tiny island out in the middle of it. If you choose
+to take the hard way here, your reward will be three meat checks.
+
+
+
+## Underground Waterway
+#### Carrie Crawlspace
+This is located shortly after the corridor following the ledges that let you reach the first waterfall's source alcove.
+Notably, only Carrie is able to crouch and go through this, making these the only checks in the *entire* game that are
+hard impossible without being a specific character. So if you have Carrie Logic on and your character is Reinhardt, you'll
+have to hold L while loading into a map to change to Carrie just for this one secret. If Carrie Logic is off, then these
+locations will not be added and you can just skip them entirely.
+
+### 3-Hit Breakables
+#### First poison parkour ledge
+Near the start of the level is a series of ledges you can climb onto and platform across to reach a corner with a lantern
+that you can normally get a Cure Ampoule from. The first of these ledges can be broken for an assortment of 6 things.
+
+#### Inside skeleton crusher ledge
+To the left of the hallway entrance leading to the third switch is a long shimmy-able ledge that you can grab onto and shimmy
+for a whole eternity (I implemented a setting JUST to make shimmying this ledge faster!) to get to a couple stand on-able ledges,
+one of which has a lantern above it containing a check. This ledge can be broken for 3 chickens. I'd highly suggest bringing
+Holy Water for this because otherwise you're forced to break it from the other, lower ledge that's here. And this ledge
+will drop endless crawling skeletons on you as long as you're on it.
+
+
+
+## Castle Center
+#### Atop elevator room machine
+In the elevator room, right from the entrance coming in from the vampire triplets' room, is a machine that you can press
+C-Right on to get dialog reading "An enormous machine." There's a torch on top of this machine that you can reach by
+climbing onto the slanted part of the walls in the room.
+
+#### Heinrich Meyer
+The friendly lizard-man who normally gives you the Chamber Key in vanilla has a check for you just like Vincent.
+Yes, he has a name! And you'd best not forget it!
+
+#### Torture chamber rafters
+A check can be found in the rafters in the room with the Mandragora shelf. Get onto and jump off the giant scythe or the
+torture instrument shelf to make it up there. It's less annoying to do without Mandragora since having it will cause ghosts to
+infinitely spawn in here.
+
+### Invisible Items
+#### Red carpet hall knight
+The suit of armor in the red carpet hallway after the bottom elevator room, directly next to the door leading into the
+Lizard Locker Room.
+
+#### Lizard locker knight
+The suit of armor in the Lizard Locker Room itself, directly across from the door connecting to the red carpet hallway.
+
+#### Broken staircase knight
+The suit of armor in the broken staircase room following the Lizard Locker Room.
+
+#### Inside cracked wall hallway flamethrower
+In the upper cracked wall hallway, it is in the lower flamethrower that is part of the pair between the Butler Bros. Room
+and the main part of the hallway.
+
+#### Nitro room crates
+The wall of crates in the Nitro room on Heinrich Meyer's side. This is notable for being one of the very rare Healing Kits
+that you can get for free in vanilla.
+
+#### Hell Knight landing corner knight
+The inactive suit of armor in the corner of the room before the Maid Sisters' Room, which also contains an active knight.
+
+#### Maid sisters room vase
+The lone vase in the vampire Maid Sisters' Room, directly across from the door leading to the Hell Knight Landing.
+Yes, you are actually supposed to *check* this with C-right to get its item; not break it like you did to the pots in
+the Villa earlier!
+
+#### Invention room giant Famicart
+The giant square-shaped thing in one corner of the invention room that looks vaguely like a massive video game cartridge.
+A Famicom cartridge, perhaps?
+
+#### Invention room round machine
+The brown circular machine in the invention room, close to the middle of the wall on the side of the Spike Crusher Room.
+
+#### Inside nitro hallway flamethrower
+The lower flamethrower in the hallway between the Nitro room from the Spike Crusher Room, near the two doors to said rooms.
+
+#### Torture chamber instrument rack
+The shelf full of torture instruments in the torture chamber, to the right of the Mandragora shelf.
+
+### 3-Hit Breakables
+#### Behemoth arena crate
+This large crate can be found in the back-right corner of Behemoth's arena and is pretty hard to miss. Break it to get 5
+moneybags-turned-checks.
+
+#### Elevator room unoccupied statue stand
+In the bottom elevator room is a statue on a stand that will cry literal Bloody Tears if you get near it. On the opposite
+side of the room from this, near the enormous machine, is a stand much like the one the aforementioned statue is on only
+this one is completely vacant. This stand can be broken for 3 roast beefs-turned checks.
+
+#### Lizard locker room slab
+In the Lizard Locker Room, on top of the second locker from the side of the room with the door to the red carpet hallway,
+is a metallic box-like slab thingy that can be broken normally for 4 status items. This 3HB is notable for being one of two
+funny ones in the game that does NOT set a flag when you break it in vanilla, meaning you can keep breaking it over and
+over again for infinite Purifyings and Cure Ampoules!
+
+### The Lizard Lockers
+If you turned on the Lizard Locker Items setting, then hoo boy, you are in for a FUN =) time! Inside each one of the six Lizard
+Lockers is a check, and the way you get these checks is by camping near each of the lockers as you defeat said Lizards,
+praying that one will emerge from it and cause it to open to give you a chance to grab it. It is *completely* luck-based,
+you have a 1-in-6 (around 16%) chance per Lizard-man that emerges, and you have to repeat this process six times for each
+check inside each locker. You can open and cancel the warp menu to make things easier, but other than that, enjoy playing
+with the Lizards!
+
+
+
+## Duel Tower
+#### Invisible bridge balcony
+Located between Werewolf and Were-bull's arenas. Use an invisible bridge to reach it; it starts at the highest platform
+on the same wall as it that appears after defeating Werewolf. The balcony contains two checks and a save point that I
+added specifically for this rando to make the level less frustrating.
+
+#### Above Were-bull arena
+The only check that can be permanently missed in the vanilla game depending on the order you do things, not counting any
+points of no return. Between Werewolf and Were-bull's arenas is an alternate path that you can take downward and around
+to avoid Were-bull completely and get on top of his still-raised arena, so you can reach this. In the rando, I set it up so
+that his arena will go back to being raised if you leave the area and come back, and if you get the item later his arena flag
+will be set then. If you're playing with Dracula's bosses condition, then you can only get one Special2 off of him the first
+time you beat him and then none more after that.
+
+
+
+## Tower of Execution
+#### Invisible bridge ledge
+There are two ways to reach this one; use the invisible bridge that starts at the walkway above the entrance, or jump to
+it from the Execution Key gate alcove. Collecting this Special2 in vanilla unlocks Reinhardt's alternate costume.
+
+#### Guillotine tower top level
+This iron maiden is strategically placed in such a way that you will very likely miss it if you aren't looking carefully for
+it, so I am including it here. When you make it to the top floor of the level, as you approach the tower for the final time,
+look on the opposite side of it from where you approach it, and you will find this. The laggiest check in the whole game!
+I'd dare someone to find some check in some other Archipelago game that lags harder than this.
+
+### 3-Hit Breakables
+#### Pre-mid-savepoint platforms ledge
+Here's a really weird one that even I never found about until well after I finished the 3HB setting and moved on to deving
+other things, and I'd honestly be shocked if ANYONE knew about outside the context of this rando! This square-shaped
+platform can be found right before the second set of expanding and retracting wall platforms, leading up to the mid-save
+point, after going towards the central tower structure for the second time on the second floor. Breaking it will drop an
+assortment of 5 items, one of which is notable for being the ONE sub-weapon that drops from a 3HB. This meant I had to really
+change how things work to account for sub-weapons being in 3HBs!
+
+
+
+## Tower of Science
+#### Invisible bridge platform
+Following the hallway with a save jewel beyond the Science Key2 door, look to your right, and you'll see this. Mind the
+gap separating the invisible bridge from the solid ground of the bottom part of this section!
+
+### 3-Hit Breakables
+#### Invisible bridge platform crate
+Near the candle on the above-mentioned invisible bridge platform is a small metallic crate. Break it for a total of 6
+checks, which in vanilla are 2 chickens, moneybags, and jewels.
+
+
+
+## Tower of Sorcery
+#### Trick shot from mid-savepoint platform
+From the platform with the save jewel, look back towards the vanishing red platforms that you had to conquer to get up
+there, and you'll see a breakable diamond floating high above a solid platform before it. An ultra-precise orb shot from
+Carrie can hit it to reveal the check, so you can then go down and get it. If you are playing as Reinhardt, you'll have
+to use a sub-weapon. Any sub-weapon that's not Holy Water will work. Sub-weapons and the necessary jewels can both be
+farmed off the local Icemen if it really comes down to it.
+
+#### Above yellow bubble
+Directly above the yellow bubble that you break to raise the middle yellow large platform. Jump off the final red platform
+in the series of red platforms right after the save point when said platform bobs all the way up, and you'll be able to
+hit this diamond with good timing.
+
+#### Above tiny blue platforms start
+Above the large platform after the yellow ones from whence you can reach the first of the series of tiny blue platforms.
+This diamond is low enough that you can reach it by simply jumping straight up to it.
+
+#### Invisible bridge platform
+Located at the very end of the stage, off to the side. Use the invisible bridge to reach it. The Special2 that unlocks
+Carrie's costume can be normally found here (or Special3, depending on if you are playing the PAL version or not).
+
+
+
+## Clock Tower
+All the normal items here and in Room of Clocks are self-explanatory, but the 3HBs in Clock Tower are a whoooooooole 'nother story. So buckle up...
+
+### 3-Hit Breakables
+#### Gear climb room battery underside slab
+In the first room, on the side you initially climb up, go up until you find a battery-like object that you can stand on
+as a platform. From the platform right after this, you can hit this 3HB that can be found on the underside of the structure
+in the corner, above the structure before the first set of gears that you initially start the climb on. 3 chickens/checks
+can be gotten out of this one.
+
+#### Gear climb room door underside slab
+Now THIS one can be very annoying to get, doubly so if you're playing as Reinhardt, triply so if you're playing Hard Mode
+on top of that because you will also have to deal with invincible, bone-throwing red skeletons here! This slab can be
+found on the underside of the platform in the first room with the door leading out into the second area and drops 3
+beefs/checks when you break it. Carrie is small enough to crouch under the platform and shoot the thing a few times
+without *too* much hassle, but the only way I know of for Reinhardt to get it is to hang him off the side of the gear
+and pull him up once he gets moved under the platform. Getting him to then face the breakable and whip it without falling
+off due to the gear's rotation is next-to-impossible, I find, so the safest method to breaking it as him is to just rush
+it with his sword, go back up, repeat 2 more times. Pray the aftermentioned Hard Mode skellies don't decide to cause *too*
+much trouble during all of this!
+
+#### Final room entrance slab
+Simply next to the entrance when you come into the third and final room from the intended way. Drops 2 moneybags-turned-checks,
+which funnily normally have their item IDs shared with the other 3HB in this room's items, so I had to separate those for
+the rando.
+
+#### Renon's final offers slab
+At the top of the final room, on a platform near the Renon contract that would normally be the very last in the game.
+This 3HB drops a whopping 8 items, more than any other 3HB in the entire game, and 6 of those are all moneybags. They
+*really* shower you in gold in preparation for the finale, huh?
+
+
+
+## Castle Keep
+#### Behind Dracula's chamber/Dracula's floating cube
+This game continues the CV tradition of having a hidden secret around Dracula's chamber that you can get helpful things
+from before the final battle begins. Jump onto the torch ledges and use the thin walkway to reach the backside of Dracula's
+chamber where these can both be found. The floating cube, in question, can be reached with an invisible bridge. The other
+candle here is noteworthy for being the sole jewel candle in vanilla that doesn't set a flag, meaning you can keep going
+back down and up the stairs to farm infinite sub-weapon ammo for Dracula like in old school Castlevania!
+
+### Invisible Items
+#### Left/Right Dracula door flame
+Inside the torches on either side of the door to Dracula's chamber. Similar to the above-mentioned jewel torch, these do
+not set flags. So you can get infinite healing kits for free by constantly going down and back up!
\ No newline at end of file
diff --git a/worlds/cv64/docs/setup_en.md b/worlds/cv64/docs/setup_en.md
new file mode 100644
index 000000000000..707618a1eba5
--- /dev/null
+++ b/worlds/cv64/docs/setup_en.md
@@ -0,0 +1,63 @@
+# Castlevania 64 Setup Guide
+
+## Required Software
+
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
+- A Castlevania 64 ROM of the US 1.0 version specifically. The Archipelago community cannot provide this.
+- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later
+
+### Configuring BizHawk
+
+Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings:
+
+- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from
+`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.)
+- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're
+tabbed out of EmuHawk.
+- Open a `.z64` file in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click
+`Controllers…`, load any `.z64` ROM first.
+- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to
+clear it.
+- All non-Japanese versions of the N64 Castlevanias require a Controller Pak to save game data. To enable this, while
+you still have the `.z64` ROM loaded, go to `N64 > Controller Settings...`, click the dropdown by `Controller 1`, and
+click `Memory Card`. You must then restart EmuHawk for it to take effect.
+- After enabling the `Memory Card` setting, next time you boot up your Castlevania 64 ROM, you will see the
+No "CASTLEVANIA" Note Found screen. Pick `Create "CASTLEVANIA" Note Now > Yes` to create save data and enable saving at
+the White Jewels.
+
+
+## Generating and Patching a Game
+
+1. Create your options file (YAML). You can make one on the
+[Castlevania 64 options page](../../../games/Castlevania%2064/player-options).
+2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
+This will generate an output file for you. Your patch file will have the `.apcv64` file extension.
+3. Open `ArchipelagoLauncher.exe`
+4. Select "Open Patch" on the left side and select your patch file.
+5. If this is your first time patching, you will be prompted to locate your vanilla ROM.
+6. A patched `.z64` file will be created in the same place as the patch file.
+7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your
+BizHawk install.
+
+If you're playing a single-player seed, and you don't care about hints, you can stop here, close the client, and load
+the patched ROM in any emulator or EverDrive of your choice. However, for multiworlds and other Archipelago features,
+continue below using BizHawk as your emulator.
+
+## Connecting to a Server
+
+By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just
+in case you have to close and reopen a window mid-game for some reason.
+
+1. Castlevania 64 uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your game,
+you can re-open it from the launcher.
+2. Ensure EmuHawk is running the patched ROM.
+3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing.
+4. In the Lua Console window, go to `Script > Open Script…`.
+5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
+6. The emulator may freeze every few seconds until it manages to connect to the client. This is expected. The BizHawk
+Client window should indicate that it connected and recognized Castlevania 64.
+7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the
+top text field of the client and click Connect.
+
+You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect. It is
+perfectly safe to make progress offline; everything will re-sync when you reconnect.
diff --git a/worlds/cv64/entrances.py b/worlds/cv64/entrances.py
new file mode 100644
index 000000000000..74537f92441b
--- /dev/null
+++ b/worlds/cv64/entrances.py
@@ -0,0 +1,149 @@
+from .data import ename, iname, rname
+from .stages import get_stage_info
+from .options import CV64Options
+
+from typing import Dict, List, Tuple, Union
+
+# # # KEY # # #
+# "connection" = The name of the Region the Entrance connects into. If it's a Tuple[str, str], we take the stage in
+# active_stage_exits given in the second string and then the stage given in that stage's slot given in
+# the first string, and take the start or end Region of that stage.
+# "rule" = What rule should be applied to the Entrance during set_rules, as defined in self.rules in the CV64Rules class
+# definition in rules.py.
+# "add conds" = A list of player options conditions that must be satisfied for the Entrance to be added. Can be of
+# varying length depending on how many conditions need to be satisfied. In the add_conds dict's tuples,
+# the first element is the name of the option, the second is the option value to check for, and the third
+# is a boolean for whether we are evaluating for the option value or not.
+entrance_info = {
+ # Forest of Silence
+ ename.forest_dbridge_gate: {"connection": rname.forest_mid},
+ ename.forest_werewolf_gate: {"connection": rname.forest_end},
+ ename.forest_end: {"connection": ("next", rname.forest_of_silence)},
+ # Castle Wall
+ ename.cw_portcullis_c: {"connection": rname.cw_exit},
+ ename.cw_lt_skip: {"connection": ("next", rname.castle_wall), "add conds": ["hard"]},
+ ename.cw_lt_door: {"connection": rname.cw_ltower, "rule": iname.left_tower_key},
+ ename.cw_end: {"connection": ("next", rname.castle_wall)},
+ # Villa
+ ename.villa_dog_gates: {"connection": rname.villa_main},
+ ename.villa_snipe_dogs: {"connection": rname.villa_start, "add conds": ["carrie", "hard"]},
+ ename.villa_to_storeroom: {"connection": rname.villa_storeroom, "rule": iname.storeroom_key},
+ ename.villa_to_archives: {"connection": rname.villa_archives, "rule": iname.archives_key},
+ ename.villa_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
+ ename.villa_to_maze: {"connection": rname.villa_maze, "rule": iname.garden_key},
+ ename.villa_from_storeroom: {"connection": rname.villa_main, "rule": iname.storeroom_key},
+ ename.villa_from_maze: {"connection": rname.villa_servants, "rule": iname.garden_key},
+ ename.villa_servant_door: {"connection": rname.villa_main},
+ ename.villa_copper_door: {"connection": rname.villa_crypt, "rule": iname.copper_key,
+ "add conds": ["not hard"]},
+ ename.villa_copper_skip: {"connection": rname.villa_crypt, "add conds": ["hard"]},
+ ename.villa_bridge_door: {"connection": rname.villa_maze},
+ ename.villa_end_r: {"connection": ("next", rname.villa)},
+ ename.villa_end_c: {"connection": ("alt", rname.villa)},
+ # Tunnel
+ ename.tunnel_start_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
+ ename.tunnel_gondolas: {"connection": rname.tunnel_end},
+ ename.tunnel_end_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
+ ename.tunnel_end: {"connection": ("next", rname.tunnel)},
+ # Underground Waterway
+ ename.uw_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
+ ename.uw_final_waterfall: {"connection": rname.uw_end},
+ ename.uw_waterfall_skip: {"connection": rname.uw_main, "add conds": ["hard"]},
+ ename.uw_end: {"connection": ("next", rname.underground_waterway)},
+ # Castle Center
+ ename.cc_tc_door: {"connection": rname.cc_torture_chamber, "rule": iname.chamber_key},
+ ename.cc_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
+ ename.cc_lower_wall: {"connection": rname.cc_crystal, "rule": "Bomb 2"},
+ ename.cc_upper_wall: {"connection": rname.cc_library, "rule": "Bomb 1"},
+ ename.cc_elevator: {"connection": rname.cc_elev_top},
+ ename.cc_exit_r: {"connection": ("next", rname.castle_center)},
+ ename.cc_exit_c: {"connection": ("alt", rname.castle_center)},
+ # Duel Tower
+ ename.dt_start: {"connection": ("prev", rname.duel_tower)},
+ ename.dt_end: {"connection": ("next", rname.duel_tower)},
+ # Tower of Execution
+ ename.toe_start: {"connection": ("prev", rname.tower_of_execution)},
+ ename.toe_gate: {"connection": rname.toe_ledge, "rule": iname.execution_key,
+ "add conds": ["not hard"]},
+ ename.toe_gate_skip: {"connection": rname.toe_ledge, "add conds": ["hard"]},
+ ename.toe_end: {"connection": ("next", rname.tower_of_execution)},
+ # Tower of Science
+ ename.tosci_start: {"connection": ("prev", rname.tower_of_science)},
+ ename.tosci_key1_door: {"connection": rname.tosci_three_doors, "rule": iname.science_key1},
+ ename.tosci_to_key2_door: {"connection": rname.tosci_conveyors, "rule": iname.science_key2},
+ ename.tosci_from_key2_door: {"connection": rname.tosci_start, "rule": iname.science_key2},
+ ename.tosci_key3_door: {"connection": rname.tosci_key3, "rule": iname.science_key3},
+ ename.tosci_end: {"connection": ("next", rname.tower_of_science)},
+ # Tower of Sorcery
+ ename.tosor_start: {"connection": ("prev", rname.tower_of_sorcery)},
+ ename.tosor_end: {"connection": ("next", rname.tower_of_sorcery)},
+ # Room of Clocks
+ ename.roc_gate: {"connection": ("next", rname.room_of_clocks)},
+ # Clock Tower
+ ename.ct_to_door1: {"connection": rname.ct_middle, "rule": iname.clocktower_key1},
+ ename.ct_from_door1: {"connection": rname.ct_start, "rule": iname.clocktower_key1},
+ ename.ct_to_door2: {"connection": rname.ct_end, "rule": iname.clocktower_key2},
+ ename.ct_from_door2: {"connection": rname.ct_middle, "rule": iname.clocktower_key2},
+ ename.ct_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
+ ename.ct_door_3: {"connection": ("next", rname.clock_tower), "rule": iname.clocktower_key3},
+ # Castle Keep
+ ename.ck_slope_jump: {"connection": rname.roc_main, "add conds": ["hard"]},
+ ename.ck_drac_door: {"connection": rname.ck_drac_chamber, "rule": "Dracula"}
+}
+
+add_conds = {"carrie": ("carrie_logic", True, True),
+ "hard": ("hard_logic", True, True),
+ "not hard": ("hard_logic", False, True),
+ "shopsanity": ("shopsanity", True, True)}
+
+stage_connection_types = {"prev": "end region",
+ "next": "start region",
+ "alt": "start region"}
+
+
+def get_entrance_info(entrance: str, info: str) -> Union[str, Tuple[str, str], List[str], None]:
+ return entrance_info[entrance].get(info, None)
+
+
+def get_warp_entrances(active_warp_list: List[str]) -> Dict[str, str]:
+ # Create the starting stage Entrance.
+ warp_entrances = {get_stage_info(active_warp_list[0], "start region"): "Start stage"}
+
+ # Create the warp Entrances.
+ for i in range(1, len(active_warp_list)):
+ mid_stage_region = get_stage_info(active_warp_list[i], "mid region")
+ warp_entrances.update({mid_stage_region: f"Warp {i}"})
+
+ return warp_entrances
+
+
+def verify_entrances(options: CV64Options, entrances: List[str],
+ active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> Dict[str, str]:
+ verified_entrances = {}
+
+ for ent_name in entrances:
+ ent_add_conds = get_entrance_info(ent_name, "add conds")
+
+ # Check any options that might be associated with the Entrance before adding it.
+ add_it = True
+ if ent_add_conds is not None:
+ for cond in ent_add_conds:
+ if not ((getattr(options, add_conds[cond][0]).value == add_conds[cond][1]) == add_conds[cond][2]):
+ add_it = False
+
+ if not add_it:
+ continue
+
+ # Add the Entrance to the verified Entrances if the above check passes.
+ connection = get_entrance_info(ent_name, "connection")
+
+ # If the Entrance is a connection to a different stage, get the corresponding other stage Region.
+ if isinstance(connection, tuple):
+ connecting_stage = active_stage_exits[connection[1]][connection[0]]
+ # Stages that lead backwards at the beginning of the line will appear leading to "Menu".
+ if connecting_stage in ["Menu", None]:
+ continue
+ connection = get_stage_info(connecting_stage, stage_connection_types[connection[0]])
+ verified_entrances.update({connection: ent_name})
+
+ return verified_entrances
diff --git a/worlds/cv64/items.py b/worlds/cv64/items.py
new file mode 100644
index 000000000000..d40f5d53cb41
--- /dev/null
+++ b/worlds/cv64/items.py
@@ -0,0 +1,214 @@
+from BaseClasses import Item
+from .data import iname
+from .locations import base_id, get_location_info
+from .options import DraculasCondition, SpareKeys
+
+from typing import TYPE_CHECKING, Dict, Union
+
+if TYPE_CHECKING:
+ from . import CV64World
+
+import math
+
+
+class CV64Item(Item):
+ game: str = "Castlevania 64"
+
+
+# # # KEY # # #
+# "code" = The unique part of the Item's AP code attribute, as well as the value to call the in-game "prepare item
+# textbox" function with to give the Item in-game. Add this + base_id to get the actual AP code.
+# "default classification" = The AP Item Classification that gets assigned to instances of that Item in create_item
+# by default, unless I deliberately override it (as is the case for some Special1s).
+# "inventory offset" = What offset from the start of the in-game inventory array (beginning at 0x80389C4B) stores the
+# current count for that Item. Used for start inventory purposes.
+# "pickup actor id" = The ID for the Item's in-game Item pickup actor. If it's not in the Item's data dict, it's the
+# same as the Item's code. This is what gets written in the ROM to replace non-NPC/shop items.
+# "sub equip id" = For sub-weapons specifically, this is the number to put in the game's "current sub-weapon" value to
+# indicate the player currently having that weapon. Used for start inventory purposes.
+item_info = {
+ # White jewel
+ iname.red_jewel_s: {"code": 0x02, "default classification": "filler"},
+ iname.red_jewel_l: {"code": 0x03, "default classification": "filler"},
+ iname.special_one: {"code": 0x04, "default classification": "progression_skip_balancing",
+ "inventory offset": 0},
+ iname.special_two: {"code": 0x05, "default classification": "progression_skip_balancing",
+ "inventory offset": 1},
+ iname.roast_chicken: {"code": 0x06, "default classification": "filler", "inventory offset": 2},
+ iname.roast_beef: {"code": 0x07, "default classification": "filler", "inventory offset": 3},
+ iname.healing_kit: {"code": 0x08, "default classification": "useful", "inventory offset": 4},
+ iname.purifying: {"code": 0x09, "default classification": "filler", "inventory offset": 5},
+ iname.cure_ampoule: {"code": 0x0A, "default classification": "filler", "inventory offset": 6},
+ # pot-pourri
+ iname.powerup: {"code": 0x0C, "default classification": "filler"},
+ iname.permaup: {"code": 0x10C, "default classification": "useful", "pickup actor id": 0x0C,
+ "inventory offset": 8},
+ iname.knife: {"code": 0x0D, "default classification": "filler", "pickup actor id": 0x10,
+ "sub equip id": 1},
+ iname.holy_water: {"code": 0x0E, "default classification": "filler", "pickup actor id": 0x0D,
+ "sub equip id": 2},
+ iname.cross: {"code": 0x0F, "default classification": "filler", "pickup actor id": 0x0E,
+ "sub equip id": 3},
+ iname.axe: {"code": 0x10, "default classification": "filler", "pickup actor id": 0x0F,
+ "sub equip id": 4},
+ # Wooden stake (AP item)
+ iname.ice_trap: {"code": 0x12, "default classification": "trap"},
+ # The contract
+ # engagement ring
+ iname.magical_nitro: {"code": 0x15, "default classification": "progression", "inventory offset": 17},
+ iname.mandragora: {"code": 0x16, "default classification": "progression", "inventory offset": 18},
+ iname.sun_card: {"code": 0x17, "default classification": "filler", "inventory offset": 19},
+ iname.moon_card: {"code": 0x18, "default classification": "filler", "inventory offset": 20},
+ # Incandescent gaze
+ iname.archives_key: {"code": 0x1A, "default classification": "progression", "pickup actor id": 0x1D,
+ "inventory offset": 22},
+ iname.left_tower_key: {"code": 0x1B, "default classification": "progression", "pickup actor id": 0x1E,
+ "inventory offset": 23},
+ iname.storeroom_key: {"code": 0x1C, "default classification": "progression", "pickup actor id": 0x1F,
+ "inventory offset": 24},
+ iname.garden_key: {"code": 0x1D, "default classification": "progression", "pickup actor id": 0x20,
+ "inventory offset": 25},
+ iname.copper_key: {"code": 0x1E, "default classification": "progression", "pickup actor id": 0x21,
+ "inventory offset": 26},
+ iname.chamber_key: {"code": 0x1F, "default classification": "progression", "pickup actor id": 0x22,
+ "inventory offset": 27},
+ iname.execution_key: {"code": 0x20, "default classification": "progression", "pickup actor id": 0x23,
+ "inventory offset": 28},
+ iname.science_key1: {"code": 0x21, "default classification": "progression", "pickup actor id": 0x24,
+ "inventory offset": 29},
+ iname.science_key2: {"code": 0x22, "default classification": "progression", "pickup actor id": 0x25,
+ "inventory offset": 30},
+ iname.science_key3: {"code": 0x23, "default classification": "progression", "pickup actor id": 0x26,
+ "inventory offset": 31},
+ iname.clocktower_key1: {"code": 0x24, "default classification": "progression", "pickup actor id": 0x27,
+ "inventory offset": 32},
+ iname.clocktower_key2: {"code": 0x25, "default classification": "progression", "pickup actor id": 0x28,
+ "inventory offset": 33},
+ iname.clocktower_key3: {"code": 0x26, "default classification": "progression", "pickup actor id": 0x29,
+ "inventory offset": 34},
+ iname.five_hundred_gold: {"code": 0x27, "default classification": "filler", "pickup actor id": 0x1A},
+ iname.three_hundred_gold: {"code": 0x28, "default classification": "filler", "pickup actor id": 0x1B},
+ iname.one_hundred_gold: {"code": 0x29, "default classification": "filler", "pickup actor id": 0x1C},
+ iname.crystal: {"default classification": "progression"},
+ iname.trophy: {"default classification": "progression"},
+ iname.victory: {"default classification": "progression"}
+}
+
+filler_item_names = [iname.red_jewel_s, iname.red_jewel_l, iname.five_hundred_gold, iname.three_hundred_gold,
+ iname.one_hundred_gold]
+
+
+def get_item_info(item: str, info: str) -> Union[str, int, None]:
+ return item_info[item].get(info, None)
+
+
+def get_item_names_to_ids() -> Dict[str, int]:
+ return {name: get_item_info(name, "code")+base_id for name in item_info if get_item_info(name, "code") is not None}
+
+
+def get_item_counts(world: "CV64World") -> Dict[str, Dict[str, int]]:
+
+ active_locations = world.multiworld.get_unfilled_locations(world.player)
+
+ item_counts = {
+ "progression": {},
+ "progression_skip_balancing": {},
+ "useful": {},
+ "filler": {},
+ "trap": {}
+ }
+ total_items = 0
+ extras_count = 0
+
+ # Get from each location its vanilla item and add it to the default item counts.
+ for loc in active_locations:
+ if loc.address is None:
+ continue
+
+ if world.options.hard_item_pool and get_location_info(loc.name, "hard item") is not None:
+ item_to_add = get_location_info(loc.name, "hard item")
+ else:
+ item_to_add = get_location_info(loc.name, "normal item")
+
+ classification = get_item_info(item_to_add, "default classification")
+
+ if item_to_add not in item_counts[classification]:
+ item_counts[classification][item_to_add] = 1
+ else:
+ item_counts[classification][item_to_add] += 1
+ total_items += 1
+
+ # Replace all but 2 PowerUps with junk if Permanent PowerUps is on and mark those two PowerUps as Useful.
+ if world.options.permanent_powerups:
+ for i in range(item_counts["filler"][iname.powerup] - 2):
+ item_counts["filler"][world.get_filler_item_name()] += 1
+ del(item_counts["filler"][iname.powerup])
+ item_counts["useful"][iname.permaup] = 2
+
+ # Add the total Special1s.
+ item_counts["progression_skip_balancing"][iname.special_one] = world.options.total_special1s.value
+ extras_count += world.options.total_special1s.value
+
+ # Add the total Special2s if Dracula's Condition is Special2s.
+ if world.options.draculas_condition == DraculasCondition.option_specials:
+ item_counts["progression_skip_balancing"][iname.special_two] = world.options.total_special2s.value
+ extras_count += world.options.total_special2s.value
+
+ # Determine the extra key counts if applicable. Doing this before moving Special1s will ensure only the keys and
+ # bomb components are affected by this.
+ for key in item_counts["progression"]:
+ spare_keys = 0
+ if world.options.spare_keys == SpareKeys.option_on:
+ spare_keys = item_counts["progression"][key]
+ elif world.options.spare_keys == SpareKeys.option_chance:
+ if item_counts["progression"][key] > 0:
+ for i in range(item_counts["progression"][key]):
+ spare_keys += world.random.randint(0, 1)
+ item_counts["progression"][key] += spare_keys
+ extras_count += spare_keys
+
+ # Move the total number of Special1s needed to warp everywhere to normal progression balancing if S1s per warp is
+ # 3 or lower.
+ if world.s1s_per_warp <= 3:
+ item_counts["progression_skip_balancing"][iname.special_one] -= world.s1s_per_warp * 7
+ item_counts["progression"][iname.special_one] = world.s1s_per_warp * 7
+
+ # Determine the total amounts of replaceable filler and non-filler junk.
+ total_filler_junk = 0
+ total_non_filler_junk = 0
+ for junk in item_counts["filler"]:
+ if junk in filler_item_names:
+ total_filler_junk += item_counts["filler"][junk]
+ else:
+ total_non_filler_junk += item_counts["filler"][junk]
+
+ # Subtract from the filler counts total number of "extra" items we've added. get_filler_item_name() filler will be
+ # subtracted from first until we run out of that, at which point we'll start subtracting from the rest. At this
+ # moment, non-filler item name filler cannot run out no matter the settings, so I haven't bothered adding handling
+ # for when it does yet.
+ available_filler_junk = filler_item_names.copy()
+ for i in range(extras_count):
+ if total_filler_junk > 0:
+ total_filler_junk -= 1
+ item_to_subtract = world.random.choice(available_filler_junk)
+ else:
+ total_non_filler_junk -= 1
+ item_to_subtract = world.random.choice(list(item_counts["filler"].keys()))
+
+ item_counts["filler"][item_to_subtract] -= 1
+ if item_counts["filler"][item_to_subtract] == 0:
+ del(item_counts["filler"][item_to_subtract])
+ if item_to_subtract in available_filler_junk:
+ available_filler_junk.remove(item_to_subtract)
+
+ # Determine the Ice Trap count by taking a certain % of the total filler remaining at this point.
+ item_counts["trap"][iname.ice_trap] = math.floor((total_filler_junk + total_non_filler_junk) *
+ (world.options.ice_trap_percentage.value / 100.0))
+ for i in range(item_counts["trap"][iname.ice_trap]):
+ # Subtract the remaining filler after determining the ice trap count.
+ item_to_subtract = world.random.choice(list(item_counts["filler"].keys()))
+ item_counts["filler"][item_to_subtract] -= 1
+ if item_counts["filler"][item_to_subtract] == 0:
+ del (item_counts["filler"][item_to_subtract])
+
+ return item_counts
diff --git a/worlds/cv64/locations.py b/worlds/cv64/locations.py
new file mode 100644
index 000000000000..264f2f7c0b9c
--- /dev/null
+++ b/worlds/cv64/locations.py
@@ -0,0 +1,699 @@
+from BaseClasses import Location
+from .data import lname, iname
+from .options import CV64Options, SubWeaponShuffle, DraculasCondition, RenonFightCondition, VincentFightCondition
+
+from typing import Dict, Optional, Union, List, Tuple
+
+base_id = 0xC64000
+
+
+class CV64Location(Location):
+ game: str = "Castlevania 64"
+
+
+# # # KEY # # #
+# "code" = The unique part of the Location's AP code attribute, as well as the in-game bitflag index starting from
+# 0x80389BE4 that indicates the Location has been checked. Add this + base_id to get the actual AP code.
+# "offset" = The offset in the ROM to overwrite to change the Item on that Location.
+# "normal item" = The Item normally there in vanilla on most difficulties in most versions of the game. Used to
+# determine the World's Item counts by checking what Locations are active.
+# "hard item" = The Item normally there in Hard Mode in the PAL version of CV64 specifically. Used instead of the
+# normal Item when the hard Item pool is enabled if it's in the Location's data dict.
+# "add conds" = A list of player options conditions that must be satisfied for the Location to be added. Can be of
+# varying length depending on how many conditions need to be satisfied. In the add_conds dict's tuples,
+# the first element is the name of the option, the second is the option value to check for, and the third
+# is a boolean for whether we are evaluating for the option value or not.
+# "event" = What event Item to place on that Location, for Locations that are events specifically.
+# "countdown" = What Countdown number in the array of Countdown numbers that Location contributes to. For the most part,
+# this is figured out by taking that Location's corresponding stage's postion in the vanilla stage order,
+# but there are some exceptions made for Locations in parts of Villa and Castle Center that split off into
+# their own numbers.
+# "type" = Anything special about this Location in-game, whether it be NPC-given, invisible, etc.
+location_info = {
+ # Forest of Silence
+ lname.forest_pillars_right: {"code": 0x1C, "offset": 0x10C67B, "normal item": iname.red_jewel_l,
+ "hard item": iname.red_jewel_s},
+ lname.forest_pillars_left: {"code": 0x46, "offset": 0x10C6EB, "normal item": iname.knife,
+ "add conds": ["sub"]},
+ lname.forest_pillars_top: {"code": 0x13, "offset": 0x10C71B, "normal item": iname.roast_beef,
+ "hard item": iname.red_jewel_l},
+ lname.forest_boss_one: {"event": iname.trophy, "add conds": ["boss"]},
+ lname.forest_king_skeleton: {"code": 0xC, "offset": 0x10C6BB, "normal item": iname.five_hundred_gold},
+ lname.forest_lgaz_in: {"code": 0x1A, "offset": 0x10C68B, "normal item": iname.moon_card},
+ lname.forest_lgaz_top: {"code": 0x19, "offset": 0x10C693, "normal item": iname.red_jewel_l,
+ "hard item": iname.red_jewel_s},
+ lname.forest_hgaz_in: {"code": 0xB, "offset": 0x10C6C3, "normal item": iname.sun_card},
+ lname.forest_hgaz_top: {"code": 0x3, "offset": 0x10C6E3, "normal item": iname.roast_chicken,
+ "hard item": iname.five_hundred_gold},
+ lname.forest_weretiger_sw: {"code": 0xA, "offset": 0x10C6CB, "normal item": iname.five_hundred_gold},
+ lname.forest_boss_two: {"event": iname.trophy, "add conds": ["boss"]},
+ lname.forest_weretiger_gate: {"code": 0x7, "offset": 0x10C683, "normal item": iname.powerup},
+ lname.forest_dirge_tomb_l: {"code": 0x59, "offset": 0x10C74B, "normal item": iname.one_hundred_gold,
+ "add conds": ["empty"]},
+ lname.forest_dirge_tomb_u: {"code": 0x8, "offset": 0x10C743, "normal item": iname.one_hundred_gold},
+ lname.forest_dirge_plaque: {"code": 0x6, "offset": 0x7C7F9D, "normal item": iname.roast_chicken,
+ "hard item": iname.one_hundred_gold, "type": "inv"},
+ lname.forest_dirge_ped: {"code": 0x45, "offset": 0x10C6FB, "normal item": iname.cross,
+ "add conds": ["sub"]},
+ lname.forest_dirge_rock1: {"code": 0x221, "offset": 0x10C791, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.forest_dirge_rock2: {"code": 0x222, "offset": 0x10C793, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.forest_dirge_rock3: {"code": 0x223, "offset": 0x10C795, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.forest_dirge_rock4: {"code": 0x224, "offset": 0x10C797, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.forest_dirge_rock5: {"code": 0x225, "offset": 0x10C799, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.forest_corpse_save: {"code": 0xF, "offset": 0x10C6A3, "normal item": iname.red_jewel_s},
+ lname.forest_dbridge_wall: {"code": 0x18, "offset": 0x10C69B, "normal item": iname.red_jewel_s},
+ lname.forest_dbridge_sw: {"code": 0x9, "offset": 0x10C6D3, "normal item": iname.roast_beef,
+ "hard item": iname.one_hundred_gold},
+ lname.forest_dbridge_gate_l: {"code": 0x44, "offset": 0x10C6F3, "normal item": iname.axe, "add conds": ["sub"]},
+ lname.forest_dbridge_gate_r: {"code": 0xE, "offset": 0x10C6AB, "normal item": iname.red_jewel_l,
+ "hard item": iname.red_jewel_s},
+ lname.forest_dbridge_tomb_l: {"code": 0xEA, "offset": 0x10C763, "normal item": iname.three_hundred_gold,
+ "add conds": ["empty"]},
+ lname.forest_dbridge_tomb_ur: {"code": 0xE4, "offset": 0x10C773, "normal item": iname.three_hundred_gold,
+ "add conds": ["empty"]},
+ lname.forest_dbridge_tomb_uf: {"code": 0x1B, "offset": 0x10C76B, "normal item": iname.red_jewel_s},
+ lname.forest_bface_tomb_lf: {"code": 0x10, "offset": 0x10C75B, "normal item": iname.roast_chicken},
+ lname.forest_bface_tomb_lr: {"code": 0x58, "offset": 0x10C753, "normal item": iname.three_hundred_gold,
+ "add conds": ["empty"]},
+ lname.forest_bface_tomb_u: {"code": 0x1E, "offset": 0x10C77B, "normal item": iname.one_hundred_gold},
+ lname.forest_ibridge: {"code": 0x2, "offset": 0x10C713, "normal item": iname.one_hundred_gold},
+ lname.forest_bridge_rock1: {"code": 0x227, "offset": 0x10C79D, "normal item": iname.red_jewel_l,
+ "add conds": ["3hb"]},
+ lname.forest_bridge_rock2: {"code": 0x228, "offset": 0x10C79F, "normal item": iname.five_hundred_gold,
+ "hard item": iname.three_hundred_gold, "add conds": ["3hb"]},
+ lname.forest_bridge_rock3: {"code": 0x229, "offset": 0x10C7A1, "normal item": iname.powerup,
+ "hard item": iname.three_hundred_gold, "add conds": ["3hb"]},
+ lname.forest_bridge_rock4: {"code": 0x22A, "offset": 0x10C7A3, "normal item": iname.roast_chicken,
+ "hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
+ lname.forest_werewolf_tomb_lf: {"code": 0xE7, "offset": 0x10C783, "normal item": iname.one_hundred_gold,
+ "add conds": ["empty"]},
+ lname.forest_werewolf_tomb_lr: {"code": 0xE6, "offset": 0x10C73B, "normal item": iname.three_hundred_gold,
+ "add conds": ["empty"]},
+ lname.forest_werewolf_tomb_r: {"code": 0x4, "offset": 0x10C733, "normal item": iname.sun_card},
+ lname.forest_werewolf_plaque: {"code": 0x1, "offset": 0xBFC8AF, "normal item": iname.roast_chicken,
+ "type": "inv"},
+ lname.forest_werewolf_tree: {"code": 0xD, "offset": 0x10C6B3, "normal item": iname.red_jewel_s},
+ lname.forest_werewolf_island: {"code": 0x41, "offset": 0x10C703, "normal item": iname.holy_water,
+ "add conds": ["sub"]},
+ lname.forest_final_sw: {"code": 0x12, "offset": 0x10C72B, "normal item": iname.roast_beef},
+ lname.forest_boss_three: {"event": iname.trophy, "add conds": ["boss"]},
+
+ # Castle Wall
+ lname.cwr_bottom: {"code": 0x1DD, "offset": 0x10C7E7, "normal item": iname.sun_card,
+ "hard item": iname.one_hundred_gold},
+ lname.cw_dragon_sw: {"code": 0x153, "offset": 0x10C817, "normal item": iname.roast_chicken},
+ lname.cw_boss: {"event": iname.trophy, "add conds": ["boss"]},
+ lname.cw_save_slab1: {"code": 0x22C, "offset": 0x10C84D, "normal item": iname.red_jewel_l,
+ "add conds": ["3hb"]},
+ lname.cw_save_slab2: {"code": 0x22D, "offset": 0x10C84F, "normal item": iname.red_jewel_l,
+ "hard item": iname.red_jewel_s, "add conds": ["3hb"]},
+ lname.cw_save_slab3: {"code": 0x22E, "offset": 0x10C851, "normal item": iname.red_jewel_l,
+ "hard item": iname.red_jewel_s, "add conds": ["3hb"]},
+ lname.cw_save_slab4: {"code": 0x22F, "offset": 0x10C853, "normal item": iname.red_jewel_l,
+ "hard item": iname.red_jewel_s, "add conds": ["3hb"]},
+ lname.cw_save_slab5: {"code": 0x230, "offset": 0x10C855, "normal item": iname.red_jewel_l,
+ "hard item": iname.red_jewel_s, "add conds": ["3hb"]},
+ lname.cw_rrampart: {"code": 0x156, "offset": 0x10C7FF, "normal item": iname.five_hundred_gold},
+ lname.cw_lrampart: {"code": 0x155, "offset": 0x10C807, "normal item": iname.moon_card,
+ "hard item": iname.one_hundred_gold},
+ lname.cw_pillar: {"code": 0x14D, "offset": 0x7F9A0F, "normal item": iname.holy_water, "add conds": ["sub"]},
+ lname.cw_shelf_visible: {"code": 0x158, "offset": 0x7F99A9, "normal item": iname.powerup},
+ lname.cw_shelf_sandbags: {"code": 0x14E, "offset": 0x7F9A3E, "normal item": iname.five_hundred_gold, "type": "inv"},
+ lname.cw_shelf_torch: {"code": 0x14C, "offset": 0x10C82F, "normal item": iname.cross, "add conds": ["sub"]},
+ lname.cw_ground_left: {"code": 0x14B, "offset": 0x10C827, "normal item": iname.knife, "add conds": ["sub"]},
+ lname.cw_ground_middle: {"code": 0x159, "offset": 0x10C7F7, "normal item": iname.left_tower_key},
+ lname.cw_ground_right: {"code": 0x14A, "offset": 0x10C81F, "normal item": iname.axe, "add conds": ["sub"]},
+ lname.cwl_bottom: {"code": 0x1DE, "offset": 0x10C7DF, "normal item": iname.moon_card},
+ lname.cwl_bridge: {"code": 0x1DC, "offset": 0x10C7EF, "normal item": iname.roast_beef},
+ lname.cw_drac_sw: {"code": 0x154, "offset": 0x10C80F, "normal item": iname.roast_chicken,
+ "hard item": iname.one_hundred_gold},
+ lname.cw_drac_slab1: {"code": 0x232, "offset": 0x10C859, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.cw_drac_slab2: {"code": 0x233, "offset": 0x10C85B, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.cw_drac_slab3: {"code": 0x234, "offset": 0x10C85D, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.cw_drac_slab4: {"code": 0x235, "offset": 0x10C85F, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.cw_drac_slab5: {"code": 0x236, "offset": 0x10C861, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ # Villa
+ lname.villafy_outer_gate_l: {"code": 0x133, "offset": 0x10C87F, "normal item": iname.red_jewel_l},
+ lname.villafy_outer_gate_r: {"code": 0x132, "offset": 0x10C887, "normal item": iname.red_jewel_l},
+ lname.villafy_dog_platform: {"code": 0x134, "offset": 0x10C89F, "normal item": iname.red_jewel_l},
+ lname.villafy_inner_gate: {"code": 0x138, "offset": 0xBFC8D7, "normal item": iname.roast_beef},
+ lname.villafy_gate_marker: {"code": 0x131, "offset": 0x10C8A7, "normal item": iname.powerup,
+ "hard item": iname.one_hundred_gold},
+ lname.villafy_villa_marker: {"code": 0x13E, "offset": 0x10C897, "normal item": iname.roast_beef,
+ "hard item": iname.one_hundred_gold},
+ lname.villafy_tombstone: {"code": 0x12F, "offset": 0x8099CC, "normal item": iname.moon_card,
+ "type": "inv"},
+ lname.villafy_fountain_fl: {"code": 0x139, "offset": 0xBFC8CF, "normal item": iname.five_hundred_gold},
+ lname.villafy_fountain_fr: {"code": 0x130, "offset": 0x80997D, "normal item": iname.purifying},
+ lname.villafy_fountain_ml: {"code": 0x13A, "offset": 0x809956, "normal item": iname.sun_card},
+ lname.villafy_fountain_mr: {"code": 0x13D, "offset": 0x80992D, "normal item": iname.moon_card},
+ lname.villafy_fountain_rl: {"code": 0x13B, "offset": 0xBFC8D3, "normal item": iname.roast_beef,
+ "hard item": iname.five_hundred_gold},
+ lname.villafy_fountain_rr: {"code": 0x13C, "offset": 0x80993C, "normal item": iname.five_hundred_gold},
+ lname.villafo_front_r: {"code": 0x3D, "offset": 0x10C8E7, "normal item": iname.red_jewel_l,
+ "hard item": iname.five_hundred_gold},
+ lname.villafo_front_l: {"code": 0x3B, "offset": 0x10C8DF, "normal item": iname.red_jewel_s},
+ lname.villafo_mid_l: {"code": 0x3C, "offset": 0x10C8D7, "normal item": iname.red_jewel_s},
+ lname.villafo_mid_r: {"code": 0xE5, "offset": 0x10C8CF, "normal item": iname.three_hundred_gold,
+ "add conds": ["empty"]},
+ lname.villafo_rear_r: {"code": 0x38, "offset": 0x10C8C7, "normal item": iname.red_jewel_s},
+ lname.villafo_rear_l: {"code": 0x39, "offset": 0x10C8BF, "normal item": iname.red_jewel_l,
+ "hard item": iname.red_jewel_s},
+ lname.villafo_pot_r: {"code": 0x2E, "offset": 0x10C8AF, "normal item": iname.red_jewel_l,
+ "hard item": iname.red_jewel_s},
+ lname.villafo_pot_l: {"code": 0x2F, "offset": 0x10C8B7, "normal item": iname.red_jewel_s},
+ lname.villafo_sofa: {"code": 0x2D, "offset": 0x81F07C, "normal item": iname.purifying,
+ "type": "inv"},
+ lname.villafo_chandelier1: {"code": 0x27D, "offset": 0x10C8F5, "normal item": iname.red_jewel_l,
+ "add conds": ["3hb"]},
+ lname.villafo_chandelier2: {"code": 0x27E, "offset": 0x10C8F7, "normal item": iname.purifying,
+ "add conds": ["3hb"]},
+ lname.villafo_chandelier3: {"code": 0x27F, "offset": 0x10C8F9, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.villafo_chandelier4: {"code": 0x280, "offset": 0x10C8FB, "normal item": iname.cure_ampoule,
+ "add conds": ["3hb"]},
+ lname.villafo_chandelier5: {"code": 0x281, "offset": 0x10C8FD, "normal item": iname.roast_chicken,
+ "add conds": ["3hb"]},
+ lname.villala_hallway_stairs: {"code": 0x34, "offset": 0x10C927, "normal item": iname.red_jewel_l},
+ lname.villala_hallway_l: {"code": 0x40, "offset": 0xBFC903, "normal item": iname.knife,
+ "add conds": ["sub"]},
+ lname.villala_hallway_r: {"code": 0x4F, "offset": 0xBFC8F7, "normal item": iname.axe,
+ "add conds": ["sub"]},
+ lname.villala_bedroom_chairs: {"code": 0x33, "offset": 0x83A588, "normal item": iname.purifying,
+ "hard item": iname.three_hundred_gold},
+ lname.villala_bedroom_bed: {"code": 0x32, "offset": 0xBFC95B, "normal item": iname.red_jewel_l,
+ "hard item": iname.three_hundred_gold},
+ lname.villala_vincent: {"code": 0x23, "offset": 0xBFE42F, "normal item": iname.archives_key,
+ "type": "npc"},
+ lname.villala_slivingroom_table: {"code": 0x2B, "offset": 0xBFC96B, "normal item": iname.five_hundred_gold,
+ "type": "inv"},
+ lname.villala_slivingroom_mirror: {"code": 0x49, "offset": 0x83A5D9, "normal item": iname.cross,
+ "add conds": ["sub"]},
+ lname.villala_diningroom_roses: {"code": 0x2A, "offset": 0xBFC90B, "normal item": iname.purifying,
+ "hard item": iname.three_hundred_gold, "type": "inv"},
+ lname.villala_llivingroom_pot_r: {"code": 0x26, "offset": 0x10C90F, "normal item": iname.storeroom_key},
+ lname.villala_llivingroom_pot_l: {"code": 0x25, "offset": 0x10C917, "normal item": iname.roast_chicken},
+ lname.villala_llivingroom_painting: {"code": 0x2C, "offset": 0xBFC907, "normal item": iname.purifying,
+ "hard item": iname.one_hundred_gold, "type": "inv"},
+ lname.villala_llivingroom_light: {"code": 0x28, "offset": 0x10C91F, "normal item": iname.purifying},
+ lname.villala_llivingroom_lion: {"code": 0x30, "offset": 0x83A610, "normal item": iname.roast_chicken,
+ "hard item": iname.five_hundred_gold, "type": "inv"},
+ lname.villala_exit_knight: {"code": 0x27, "offset": 0xBFC967, "normal item": iname.purifying,
+ "type": "inv"},
+ lname.villala_storeroom_l: {"code": 0x36, "offset": 0xBFC95F, "normal item": iname.roast_beef},
+ lname.villala_storeroom_r: {"code": 0x37, "offset": 0xBFC8FF, "normal item": iname.roast_chicken,
+ "hard item": iname.five_hundred_gold},
+ lname.villala_storeroom_s: {"code": 0x31, "offset": 0xBFC963, "normal item": iname.purifying,
+ "hard item": iname.one_hundred_gold, "type": "inv"},
+ lname.villala_archives_entrance: {"code": 0x48, "offset": 0x83A5E5, "normal item": iname.holy_water,
+ "add conds": ["sub"]},
+ lname.villala_archives_table: {"code": 0x29, "offset": 0xBFC90F, "normal item": iname.purifying,
+ "type": "inv"},
+ lname.villala_archives_rear: {"code": 0x24, "offset": 0x83A5B1, "normal item": iname.garden_key},
+ lname.villam_malus_torch: {"code": 0x173, "offset": 0x10C967, "normal item": iname.red_jewel_s,
+ "countdown": 13},
+ lname.villam_malus_bush: {"code": 0x16C, "offset": 0x850FEC, "normal item": iname.roast_chicken,
+ "type": "inv", "countdown": 13},
+ lname.villam_fplatform: {"code": 0x16B, "offset": 0x10C987, "normal item": iname.knife,
+ "add conds": ["sub"], "countdown": 13},
+ lname.villam_frankieturf_l: {"code": 0x177, "offset": 0x10C947, "normal item": iname.three_hundred_gold,
+ "countdown": 13},
+ lname.villam_frankieturf_r: {"code": 0x16A, "offset": 0x10C98F, "normal item": iname.holy_water,
+ "add conds": ["sub"], "countdown": 13},
+ lname.villam_frankieturf_ru: {"code": 0x16E, "offset": 0x10C9A7, "normal item": iname.red_jewel_s,
+ "countdown": 13},
+ lname.villam_fgarden_f: {"code": 0x172, "offset": 0x10C96F, "normal item": iname.red_jewel_s,
+ "countdown": 13},
+ lname.villam_fgarden_mf: {"code": 0x171, "offset": 0x10C977, "normal item": iname.red_jewel_s,
+ "countdown": 13},
+ lname.villam_fgarden_mr: {"code": 0x174, "offset": 0x10C95F, "normal item": iname.roast_chicken,
+ "countdown": 13},
+ lname.villam_fgarden_r: {"code": 0x170, "offset": 0x10C97F, "normal item": iname.red_jewel_l,
+ "countdown": 13},
+ lname.villam_rplatform: {"code": 0x169, "offset": 0x10C997, "normal item": iname.axe,
+ "add conds": ["sub"], "countdown": 13},
+ lname.villam_rplatform_de: {"code": 0x176, "offset": 0x10C94F, "normal item": iname.five_hundred_gold,
+ "countdown": 13},
+ lname.villam_exit_de: {"code": 0x175, "offset": 0x10C957, "normal item": iname.three_hundred_gold,
+ "countdown": 13},
+ lname.villam_serv_path: {"code": 0x17A, "offset": 0x10C92F, "normal item": iname.copper_key,
+ "countdown": 13},
+ lname.villafo_serv_ent: {"code": 0x3E, "offset": 0x10C8EF, "normal item": iname.roast_chicken},
+ lname.villam_crypt_ent: {"code": 0x178, "offset": 0x10C93F, "normal item": iname.purifying,
+ "countdown": 13},
+ lname.villam_crypt_upstream: {"code": 0x179, "offset": 0x10C937, "normal item": iname.roast_beef,
+ "countdown": 13},
+ lname.villac_ent_l: {"code": 0xC9, "offset": 0x10CF4B, "normal item": iname.red_jewel_s,
+ "countdown": 13},
+ lname.villac_ent_r: {"code": 0xC0, "offset": 0x10CF63, "normal item": iname.five_hundred_gold,
+ "countdown": 13},
+ lname.villac_wall_l: {"code": 0xC2, "offset": 0x10CF6B, "normal item": iname.roast_chicken,
+ "countdown": 13},
+ lname.villac_wall_r: {"code": 0xC1, "offset": 0x10CF5B, "normal item": iname.red_jewel_l,
+ "countdown": 13},
+ lname.villac_coffin_l: {"code": 0xD8, "offset": 0x10CF73, "normal item": iname.knife,
+ "add conds": ["sub"], "countdown": 13},
+ lname.villac_coffin_r: {"code": 0xC8, "offset": 0x10CF53, "normal item": iname.red_jewel_s,
+ "countdown": 13},
+ lname.villa_boss_one: {"event": iname.trophy, "add conds": ["boss"]},
+ lname.villa_boss_two: {"event": iname.trophy, "add conds": ["boss"]},
+ # Tunnel
+ lname.tunnel_landing: {"code": 0x197, "offset": 0x10C9AF, "normal item": iname.red_jewel_l,
+ "hard item": iname.one_hundred_gold},
+ lname.tunnel_landing_rc: {"code": 0x196, "offset": 0x10C9B7, "normal item": iname.red_jewel_s,
+ "hard item": iname.one_hundred_gold},
+ lname.tunnel_stone_alcove_r: {"code": 0xE1, "offset": 0x10CA57, "normal item": iname.holy_water,
+ "add conds": ["sub"]},
+ lname.tunnel_stone_alcove_l: {"code": 0x187, "offset": 0x10CA9F, "normal item": iname.roast_beef,
+ "hard item": iname.roast_chicken},
+ lname.tunnel_twin_arrows: {"code": 0x195, "offset": 0xBFC993, "normal item": iname.cure_ampoule,
+ "type": "inv"},
+ lname.tunnel_arrows_rock1: {"code": 0x238, "offset": 0x10CABD, "normal item": iname.purifying,
+ "add conds": ["3hb"]},
+ lname.tunnel_arrows_rock2: {"code": 0x239, "offset": 0x10CABF, "normal item": iname.purifying,
+ "hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
+ lname.tunnel_arrows_rock3: {"code": 0x23A, "offset": 0x10CAC1, "normal item": iname.cure_ampoule,
+ "add conds": ["3hb"]},
+ lname.tunnel_arrows_rock4: {"code": 0x23B, "offset": 0x10CAC3, "normal item": iname.cure_ampoule,
+ "hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
+ lname.tunnel_arrows_rock5: {"code": 0x23C, "offset": 0x10CAC5, "normal item": iname.roast_chicken,
+ "hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
+ lname.tunnel_lonesome_bucket: {"code": 0x189, "offset": 0xBFC99B, "normal item": iname.cure_ampoule,
+ "type": "inv"},
+ lname.tunnel_lbucket_mdoor_l: {"code": 0x198, "offset": 0x10CA67, "normal item": iname.knife,
+ "add conds": ["sub"]},
+ lname.tunnel_lbucket_quag: {"code": 0x191, "offset": 0x10C9DF, "normal item": iname.red_jewel_l},
+ lname.tunnel_bucket_quag_rock1: {"code": 0x23E, "offset": 0x10CAC9, "normal item": iname.roast_beef,
+ "hard item": iname.roast_chicken, "add conds": ["3hb"]},
+ lname.tunnel_bucket_quag_rock2: {"code": 0x23F, "offset": 0x10CACB, "normal item": iname.roast_beef,
+ "hard item": iname.roast_chicken, "add conds": ["3hb"]},
+ lname.tunnel_bucket_quag_rock3: {"code": 0x240, "offset": 0x10CACD, "normal item": iname.roast_beef,
+ "hard item": iname.roast_chicken, "add conds": ["3hb"]},
+ lname.tunnel_lbucket_albert: {"code": 0x190, "offset": 0x10C9E7, "normal item": iname.red_jewel_s},
+ lname.tunnel_albert_camp: {"code": 0x192, "offset": 0x10C9D7, "normal item": iname.red_jewel_s},
+ lname.tunnel_albert_quag: {"code": 0x193, "offset": 0x10C9CF, "normal item": iname.red_jewel_l},
+ lname.tunnel_gondola_rc_sdoor_l: {"code": 0x53, "offset": 0x10CA5F, "normal item": iname.cross,
+ "add conds": ["sub"]},
+ lname.tunnel_gondola_rc_sdoor_m: {"code": 0x19E, "offset": 0x10CAA7, "normal item": iname.roast_beef,
+ "hard item": iname.one_hundred_gold},
+ lname.tunnel_gondola_rc_sdoor_r: {"code": 0x188, "offset": 0x10CA27, "normal item": iname.roast_beef,
+ "hard item": iname.one_hundred_gold},
+ lname.tunnel_gondola_rc: {"code": 0x19C, "offset": 0x10CAB7, "normal item": iname.powerup},
+ lname.tunnel_rgondola_station: {"code": 0x194, "offset": 0x10C9C7, "normal item": iname.red_jewel_s},
+ lname.tunnel_gondola_transfer: {"code": 0x186, "offset": 0x10CA2F, "normal item": iname.five_hundred_gold},
+ lname.tunnel_corpse_bucket_quag: {"code": 0x18E, "offset": 0x10C9F7, "normal item": iname.red_jewel_s},
+ lname.tunnel_corpse_bucket_mdoor_l: {"code": 0x52, "offset": 0x10CA6F, "normal item": iname.holy_water,
+ "add conds": ["sub"]},
+ lname.tunnel_corpse_bucket_mdoor_r: {"code": 0x185, "offset": 0x10CA37, "normal item": iname.sun_card,
+ "hard item": iname.one_hundred_gold},
+ lname.tunnel_shovel_quag_start: {"code": 0x18D, "offset": 0x10C9FF, "normal item": iname.red_jewel_l},
+ lname.tunnel_exit_quag_start: {"code": 0x18C, "offset": 0x10CA07, "normal item": iname.red_jewel_l},
+ lname.tunnel_shovel_quag_end: {"code": 0x18B, "offset": 0x10CA0F, "normal item": iname.red_jewel_l},
+ lname.tunnel_exit_quag_end: {"code": 0x184, "offset": 0x10CA3F, "normal item": iname.five_hundred_gold},
+ lname.tunnel_shovel: {"code": 0x18F, "offset": 0x86D8FC, "normal item": iname.roast_beef,
+ "type": "inv"},
+ lname.tunnel_shovel_save: {"code": 0x18A, "offset": 0x10CA17, "normal item": iname.red_jewel_l},
+ lname.tunnel_shovel_mdoor_l: {"code": 0x183, "offset": 0x10CA47, "normal item": iname.sun_card,
+ "hard item": iname.one_hundred_gold},
+ lname.tunnel_shovel_mdoor_r: {"code": 0x51, "offset": 0x10CA77, "normal item": iname.axe,
+ "add conds": ["sub"]},
+ lname.tunnel_shovel_sdoor_l: {"code": 0x182, "offset": 0x10CA4F, "normal item": iname.moon_card},
+ lname.tunnel_shovel_sdoor_m: {"code": 0x19D, "offset": 0x10CAAF, "normal item": iname.roast_chicken},
+ lname.tunnel_shovel_sdoor_r: {"code": 0x50, "offset": 0x10CA7F, "normal item": iname.cross,
+ "add conds": ["sub"]},
+ # Underground Waterway
+ lname.uw_near_ent: {"code": 0x4C, "offset": 0x10CB03, "normal item": iname.three_hundred_gold},
+ lname.uw_across_ent: {"code": 0x4E, "offset": 0x10CAF3, "normal item": iname.five_hundred_gold},
+ lname.uw_first_ledge1: {"code": 0x242, "offset": 0x10CB39, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.uw_first_ledge2: {"code": 0x243, "offset": 0x10CB3B, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.uw_first_ledge3: {"code": 0x244, "offset": 0x10CB3D, "normal item": iname.purifying,
+ "hard item": iname.five_hundred_gold, "add conds": ["3hb"]},
+ lname.uw_first_ledge4: {"code": 0x245, "offset": 0x10CB3F, "normal item": iname.cure_ampoule,
+ "hard item": iname.five_hundred_gold, "add conds": ["3hb"]},
+ lname.uw_first_ledge5: {"code": 0x246, "offset": 0x10CB41, "normal item": iname.purifying,
+ "hard item": iname.three_hundred_gold, "add conds": ["3hb"]},
+ lname.uw_first_ledge6: {"code": 0x247, "offset": 0x10CB43, "normal item": iname.cure_ampoule,
+ "hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
+ lname.uw_poison_parkour: {"code": 0x4D, "offset": 0x10CAFB, "normal item": iname.cure_ampoule},
+ lname.uw_boss: {"event": iname.trophy, "add conds": ["boss"]},
+ lname.uw_waterfall_alcove: {"code": 0x57, "offset": 0x10CB23, "normal item": iname.five_hundred_gold},
+ lname.uw_carrie1: {"code": 0x4B, "offset": 0x10CB0B, "normal item": iname.moon_card,
+ "hard item": iname.five_hundred_gold, "add conds": ["carrie"]},
+ lname.uw_carrie2: {"code": 0x4A, "offset": 0x10CB13, "normal item": iname.roast_beef,
+ "hard item": iname.five_hundred_gold, "add conds": ["carrie"]},
+ lname.uw_bricks_save: {"code": 0x5A, "offset": 0x10CB33, "normal item": iname.powerup,
+ "hard item": iname.one_hundred_gold},
+ lname.uw_above_skel_ledge: {"code": 0x56, "offset": 0x10CB2B, "normal item": iname.roast_chicken},
+ lname.uw_in_skel_ledge1: {"code": 0x249, "offset": 0x10CB45, "normal item": iname.roast_chicken,
+ "add conds": ["3hb"]},
+ lname.uw_in_skel_ledge2: {"code": 0x24A, "offset": 0x10CB47, "normal item": iname.roast_chicken,
+ "add conds": ["3hb"]},
+ lname.uw_in_skel_ledge3: {"code": 0x24B, "offset": 0x10CB49, "normal item": iname.roast_chicken,
+ "add conds": ["3hb"]},
+ # Castle Center
+ lname.ccb_skel_hallway_ent: {"code": 0x1AF, "offset": 0x10CB67, "normal item": iname.red_jewel_s},
+ lname.ccb_skel_hallway_jun: {"code": 0x1A8, "offset": 0x10CBD7, "normal item": iname.powerup},
+ lname.ccb_skel_hallway_tc: {"code": 0x1AE, "offset": 0x10CB6F, "normal item": iname.red_jewel_l},
+ lname.ccb_skel_hallway_ba: {"code": 0x1B6, "offset": 0x10CBC7, "normal item": iname.cross,
+ "add conds": ["sub"]},
+ lname.ccb_behemoth_l_ff: {"code": 0x1AD, "offset": 0x10CB77, "normal item": iname.red_jewel_s},
+ lname.ccb_behemoth_l_mf: {"code": 0x1B3, "offset": 0x10CBA7, "normal item": iname.three_hundred_gold,
+ "hard item": iname.one_hundred_gold},
+ lname.ccb_behemoth_l_mr: {"code": 0x1AC, "offset": 0x10CB7F, "normal item": iname.red_jewel_l},
+ lname.ccb_behemoth_l_fr: {"code": 0x1B2, "offset": 0x10CBAF, "normal item": iname.three_hundred_gold,
+ "hard item": iname.one_hundred_gold},
+ lname.ccb_behemoth_r_ff: {"code": 0x1B1, "offset": 0x10CBB7, "normal item": iname.three_hundred_gold,
+ "hard item": iname.one_hundred_gold},
+ lname.ccb_behemoth_r_mf: {"code": 0x1AB, "offset": 0x10CB87, "normal item": iname.red_jewel_s},
+ lname.ccb_behemoth_r_mr: {"code": 0x1B0, "offset": 0x10CBBF, "normal item": iname.three_hundred_gold,
+ "hard item": iname.one_hundred_gold},
+ lname.ccb_behemoth_r_fr: {"code": 0x1AA, "offset": 0x10CB8F, "normal item": iname.red_jewel_l},
+ lname.ccb_behemoth_crate1: {"code": 0x24D, "offset": 0x10CBDD, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.ccb_behemoth_crate2: {"code": 0x24E, "offset": 0x10CBDF, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.ccb_behemoth_crate3: {"code": 0x24F, "offset": 0x10CBE1, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.ccb_behemoth_crate4: {"code": 0x250, "offset": 0x10CBE3, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.ccb_behemoth_crate5: {"code": 0x251, "offset": 0x10CBE5, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.ccelv_near_machine: {"code": 0x11A, "offset": 0x10CBF7, "normal item": iname.red_jewel_s},
+ lname.ccelv_atop_machine: {"code": 0x118, "offset": 0x10CC17, "normal item": iname.powerup,
+ "hard item": iname.three_hundred_gold},
+ lname.ccelv_stand1: {"code": 0x253, "offset": 0x10CC1D, "normal item": iname.roast_beef,
+ "add conds": ["3hb"]},
+ lname.ccelv_stand2: {"code": 0x254, "offset": 0x10CC1F, "normal item": iname.roast_beef,
+ "hard item": iname.three_hundred_gold, "add conds": ["3hb"]},
+ lname.ccelv_stand3: {"code": 0x255, "offset": 0x10CC21, "normal item": iname.roast_beef,
+ "hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
+ lname.ccelv_pipes: {"code": 0x11B, "offset": 0x10CC07, "normal item": iname.one_hundred_gold},
+ lname.ccelv_switch: {"code": 0x100, "offset": 0x10CC0F, "normal item": iname.holy_water,
+ "add conds": ["sub"]},
+ lname.ccelv_staircase: {"code": 0x119, "offset": 0x10CBFF, "normal item": iname.red_jewel_l,
+ "hard item": iname.five_hundred_gold},
+ lname.ccff_redcarpet_knight: {"code": 0x10A, "offset": 0x8C44D9, "normal item": iname.red_jewel_l,
+ "hard item": iname.red_jewel_s, "type": "inv"},
+ lname.ccff_gears_side: {"code": 0x10F, "offset": 0x10CC33, "normal item": iname.red_jewel_s},
+ lname.ccff_gears_mid: {"code": 0x10E, "offset": 0x10CC3B, "normal item": iname.purifying,
+ "hard item": iname.one_hundred_gold},
+ lname.ccff_gears_corner: {"code": 0x10D, "offset": 0x10CC43, "normal item": iname.roast_chicken,
+ "hard item": iname.one_hundred_gold},
+ lname.ccff_lizard_knight: {"code": 0x109, "offset": 0x8C44E7, "normal item": iname.roast_chicken,
+ "hard item": iname.three_hundred_gold, "type": "inv"},
+ lname.ccff_lizard_near_knight: {"code": 0x101, "offset": 0x10CC5B, "normal item": iname.axe,
+ "add conds": ["sub"]},
+ lname.ccff_lizard_pit: {"code": 0x10C, "offset": 0x10CC4B, "normal item": iname.sun_card,
+ "hard item": iname.five_hundred_gold},
+ lname.ccff_lizard_corner: {"code": 0x10B, "offset": 0x10CC53, "normal item": iname.moon_card,
+ "hard item": iname.five_hundred_gold},
+ lname.ccff_lizard_locker_nfr: {"code": 0x104, "offset": 0x8C450A, "normal item": iname.red_jewel_l,
+ "add conds": ["liz"]},
+ lname.ccff_lizard_locker_nmr: {"code": 0x105, "offset": 0xBFC9C3, "normal item": iname.five_hundred_gold,
+ "add conds": ["liz"]},
+ lname.ccff_lizard_locker_nml: {"code": 0x106, "offset": 0xBFC9C7, "normal item": iname.red_jewel_l,
+ "hard item": iname.cure_ampoule, "add conds": ["liz"]},
+ lname.ccff_lizard_locker_nfl: {"code": 0x107, "offset": 0xBFCA07, "normal item": iname.powerup,
+ "add conds": ["liz"]},
+ lname.ccff_lizard_locker_fl: {"code": 0x102, "offset": 0xBFCA03, "normal item": iname.five_hundred_gold,
+ "add conds": ["liz"]},
+ lname.ccff_lizard_locker_fr: {"code": 0x103, "offset": 0x8C44F5, "normal item": iname.sun_card,
+ "hard item": iname.three_hundred_gold, "add conds": ["liz"]},
+ lname.ccff_lizard_slab1: {"code": 0x257, "offset": 0x10CC61, "normal item": iname.purifying,
+ "hard item": iname.roast_chicken, "add conds": ["3hb"]},
+ lname.ccff_lizard_slab2: {"code": 0x258, "offset": 0x10CC63, "normal item": iname.purifying,
+ "hard item": iname.powerup, "add conds": ["3hb"]},
+ lname.ccff_lizard_slab3: {"code": 0x259, "offset": 0x10CC65, "normal item": iname.cure_ampoule,
+ "hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
+ lname.ccff_lizard_slab4: {"code": 0x25A, "offset": 0x10CC67, "normal item": iname.cure_ampoule,
+ "hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
+ lname.ccb_mandrag_shelf_l: {"code": 0x1A0, "offset": 0xBFCBB3, "normal item": iname.mandragora},
+ lname.ccb_mandrag_shelf_r: {"code": 0x1A1, "offset": 0xBFCBAF, "normal item": iname.mandragora},
+ lname.ccb_torture_rack: {"code": 0x1A9, "offset": 0x8985E5, "normal item": iname.purifying,
+ "type": "inv"},
+ lname.ccb_torture_rafters: {"code": 0x1A2, "offset": 0x8985D6, "normal item": iname.roast_beef},
+ lname.cc_behind_the_seal: {"event": iname.crystal, "add conds": ["crystal"]},
+ lname.cc_boss_one: {"event": iname.trophy, "add conds": ["boss"]},
+ lname.cc_boss_two: {"event": iname.trophy, "add conds": ["boss"]},
+ lname.ccll_brokenstairs_floor: {"code": 0x7B, "offset": 0x10CC8F, "normal item": iname.red_jewel_l,
+ "countdown": 14},
+ lname.ccll_brokenstairs_knight: {"code": 0x74, "offset": 0x8DF782, "normal item": iname.roast_beef,
+ "hard item": iname.one_hundred_gold, "type": "inv", "countdown": 14},
+ lname.ccll_brokenstairs_save: {"code": 0x7C, "offset": 0x10CC87, "normal item": iname.red_jewel_l,
+ "countdown": 14},
+ lname.ccll_glassknight_l: {"code": 0x7A, "offset": 0x10CC97, "normal item": iname.red_jewel_s,
+ "hard item": iname.five_hundred_gold, "countdown": 14},
+ lname.ccll_glassknight_r: {"code": 0x7E, "offset": 0x10CC77, "normal item": iname.red_jewel_s,
+ "hard item": iname.five_hundred_gold, "countdown": 14},
+ lname.ccll_butlers_door: {"code": 0x7D, "offset": 0x10CC7F, "normal item": iname.red_jewel_s,
+ "countdown": 14},
+ lname.ccll_butlers_side: {"code": 0x79, "offset": 0x10CC9F, "normal item": iname.purifying,
+ "hard item": iname.one_hundred_gold, "countdown": 14},
+ lname.ccll_cwhall_butlerflames_past: {"code": 0x78, "offset": 0x10CCA7, "normal item": iname.cure_ampoule,
+ "hard item": iname.red_jewel_l, "countdown": 14},
+ lname.ccll_cwhall_flamethrower: {"code": 0x73, "offset": 0x8DF580, "normal item": iname.five_hundred_gold,
+ "type": "inv", "countdown": 14},
+ lname.ccll_cwhall_cwflames: {"code": 0x77, "offset": 0x10CCAF, "normal item": iname.roast_chicken,
+ "hard item": iname.red_jewel_l, "countdown": 14},
+ lname.ccll_heinrich: {"code": 0x69, "offset": 0xBFE443, "normal item": iname.chamber_key,
+ "type": "npc", "countdown": 14},
+ lname.ccia_nitro_crates: {"code": 0x66, "offset": 0x90FCE9, "normal item": iname.healing_kit,
+ "hard item": iname.one_hundred_gold, "type": "inv", "countdown": 14},
+ lname.ccia_nitro_shelf_h: {"code": 0x55, "offset": 0xBFCC03, "normal item": iname.magical_nitro,
+ "countdown": 14},
+ lname.ccia_stairs_knight: {"code": 0x61, "offset": 0x90FE5C, "normal item": iname.five_hundred_gold,
+ "type": "inv", "countdown": 14},
+ lname.ccia_maids_vase: {"code": 0x63, "offset": 0x90FF1D, "normal item": iname.red_jewel_l,
+ "type": "inv", "countdown": 14},
+ lname.ccia_maids_outer: {"code": 0x6B, "offset": 0x10CCFF, "normal item": iname.purifying,
+ "hard item": iname.three_hundred_gold, "countdown": 14},
+ lname.ccia_maids_inner: {"code": 0x6A, "offset": 0x10CD07, "normal item": iname.cure_ampoule,
+ "hard item": iname.three_hundred_gold, "countdown": 14},
+ lname.ccia_inventions_maids: {"code": 0x6C, "offset": 0x10CCE7, "normal item": iname.moon_card,
+ "hard item": iname.one_hundred_gold, "countdown": 14},
+ lname.ccia_inventions_crusher: {"code": 0x6E, "offset": 0x10CCDF, "normal item": iname.sun_card,
+ "hard item": iname.one_hundred_gold, "countdown": 14},
+ lname.ccia_inventions_famicart: {"code": 0x64, "offset": 0x90FBB3, "normal item": iname.five_hundred_gold,
+ "type": "inv", "countdown": 14},
+ lname.ccia_inventions_zeppelin: {"code": 0x6D, "offset": 0x90FBC0, "normal item": iname.roast_beef,
+ "countdown": 14},
+ lname.ccia_inventions_round: {"code": 0x65, "offset": 0x90FBA7, "normal item": iname.roast_beef,
+ "hard item": iname.five_hundred_gold, "type": "inv", "countdown": 14},
+ lname.ccia_nitrohall_flamethrower: {"code": 0x62, "offset": 0x90FCDA, "normal item": iname.red_jewel_l,
+ "type": "inv", "countdown": 14},
+ lname.ccia_nitrohall_torch: {"code": 0x6F, "offset": 0x10CCD7, "normal item": iname.roast_chicken,
+ "hard item": iname.red_jewel_s, "countdown": 14},
+ lname.ccia_nitro_shelf_i: {"code": 0x60, "offset": 0xBFCBFF, "normal item": iname.magical_nitro,
+ "countdown": 14},
+ lname.ccll_cwhall_wall: {"code": 0x76, "offset": 0x10CCB7, "normal item": iname.roast_beef,
+ "hard item": iname.one_hundred_gold, "countdown": 14},
+ lname.ccl_bookcase: {"code": 0x166, "offset": 0x8F1197, "normal item": iname.sun_card,
+ "countdown": 14},
+ # Duel Tower
+ lname.dt_boss_one: {"event": iname.trophy, "add conds": ["boss"]},
+ lname.dt_boss_two: {"event": iname.trophy, "add conds": ["boss"]},
+ lname.dt_ibridge_l: {"code": 0x81, "offset": 0x10CE8B, "normal item": iname.roast_beef,
+ "hard item": iname.five_hundred_gold},
+ lname.dt_ibridge_r: {"code": 0x80, "offset": 0x10CE93, "normal item": iname.powerup},
+ lname.dt_stones_start: {"code": 0x83, "offset": 0x10CE73, "normal item": iname.roast_chicken,
+ "hard item": iname.five_hundred_gold},
+ lname.dt_stones_end: {"code": 0x97, "offset": 0x10CE83, "normal item": iname.knife, "add conds": ["sub"]},
+ lname.dt_werebull_arena: {"code": 0x82, "offset": 0x10CE7B, "normal item": iname.roast_beef},
+ lname.dt_boss_three: {"event": iname.trophy, "add conds": ["boss"]},
+ lname.dt_boss_four: {"event": iname.trophy, "add conds": ["boss"]},
+ # Tower of Execution
+ lname.toe_ledge1: {"code": 0x25C, "offset": 0x10CD5D, "normal item": iname.red_jewel_l,
+ "add conds": ["3hb"]},
+ lname.toe_ledge2: {"code": 0x25D, "offset": 0x10CD5F, "normal item": iname.purifying,
+ "hard item": iname.red_jewel_s, "add conds": ["3hb"]},
+ lname.toe_ledge3: {"code": 0x25E, "offset": 0x10CD61, "normal item": iname.five_hundred_gold,
+ "hard item": iname.red_jewel_s, "add conds": ["3hb"]},
+ lname.toe_ledge4: {"code": 0x25F, "offset": 0x10CD63, "normal item": iname.cure_ampoule,
+ "hard item": iname.five_hundred_gold, "add conds": ["3hb"]},
+ lname.toe_ledge5: {"code": 0x260, "offset": 0x10CD65, "normal item": iname.holy_water,
+ "add conds": ["3hb", "sub"]},
+ lname.toe_midsavespikes_r: {"code": 0x9C, "offset": 0x10CD1F, "normal item": iname.five_hundred_gold},
+ lname.toe_midsavespikes_l: {"code": 0x9B, "offset": 0x10CD27, "normal item": iname.roast_chicken,
+ "hard item": iname.five_hundred_gold},
+ lname.toe_elec_grate: {"code": 0x99, "offset": 0x10CD17, "normal item": iname.execution_key},
+ lname.toe_ibridge: {"code": 0x98, "offset": 0x10CD47, "normal item": iname.one_hundred_gold},
+ lname.toe_top: {"code": 0x9D, "offset": 0x10CD4F, "normal item": iname.red_jewel_l},
+ lname.toe_keygate_l: {"code": 0x9A, "offset": 0x10CD37, "normal item": iname.roast_beef,
+ "hard item": iname.one_hundred_gold},
+ lname.toe_keygate_r: {"code": 0x9E, "offset": 0x10CD3F, "normal item": iname.cross, "add conds": ["sub"]},
+ # Tower of Science
+ lname.tosci_elevator: {"code": 0x1FC, "offset": 0x10CE0B, "normal item": iname.three_hundred_gold},
+ lname.tosci_plain_sr: {"code": 0x1FF, "offset": 0x10CDF3, "normal item": iname.science_key1},
+ lname.tosci_stairs_sr: {"code": 0x1FB, "offset": 0x10CE13, "normal item": iname.three_hundred_gold},
+ lname.tosci_three_door_hall: {"code": 0x1FE, "offset": 0x10CDFB, "normal item": iname.science_key2},
+ lname.tosci_ibridge_t: {"code": 0x1F3, "offset": 0x10CE3B, "normal item": iname.roast_beef,
+ "hard item": iname.red_jewel_l},
+ lname.tosci_ibridge_b1: {"code": 0x262, "offset": 0x10CE59, "normal item": iname.red_jewel_l,
+ "add conds": ["3hb"]},
+ lname.tosci_ibridge_b2: {"code": 0x263, "offset": 0x10CE5B, "normal item": iname.red_jewel_l,
+ "add conds": ["3hb"]},
+ lname.tosci_ibridge_b3: {"code": 0x264, "offset": 0x10CE5D, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.tosci_ibridge_b4: {"code": 0x265, "offset": 0x10CE5F, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.tosci_ibridge_b5: {"code": 0x266, "offset": 0x10CE61, "normal item": iname.roast_chicken,
+ "add conds": ["3hb"]},
+ lname.tosci_ibridge_b6: {"code": 0x267, "offset": 0x10CE63, "normal item": iname.roast_chicken,
+ "hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
+ lname.tosci_conveyor_sr: {"code": 0x1F7, "offset": 0x10CE33, "normal item": iname.red_jewel_l,
+ "hard item": iname.red_jewel_s},
+ lname.tosci_exit: {"code": 0x1FD, "offset": 0x10CE03, "normal item": iname.science_key3},
+ lname.tosci_key3_r: {"code": 0x1FA, "offset": 0x10CE1B, "normal item": iname.five_hundred_gold},
+ lname.tosci_key3_m: {"code": 0x1F2, "offset": 0x10CE2B, "normal item": iname.cross, "add conds": ["sub"]},
+ lname.tosci_key3_l: {"code": 0x1F9, "offset": 0x10CE23, "normal item": iname.five_hundred_gold},
+ # Tower of Sorcery
+ lname.tosor_stained_tower: {"code": 0x96, "offset": 0x10CDB3, "normal item": iname.red_jewel_l},
+ lname.tosor_savepoint: {"code": 0x95, "offset": 0x10CDBB, "normal item": iname.red_jewel_l},
+ lname.tosor_trickshot: {"code": 0x92, "offset": 0x10CDD3, "normal item": iname.roast_beef},
+ lname.tosor_yellow_bubble: {"code": 0x91, "offset": 0x10CDDB, "normal item": iname.five_hundred_gold},
+ lname.tosor_blue_platforms: {"code": 0x94, "offset": 0x10CDC3, "normal item": iname.red_jewel_s},
+ lname.tosor_side_isle: {"code": 0x93, "offset": 0x10CDCB, "normal item": iname.red_jewel_s},
+ lname.tosor_ibridge: {"code": 0x90, "offset": 0x10CDE3, "normal item": iname.three_hundred_gold},
+ # Room of Clocks
+ lname.roc_ent_l: {"code": 0xC6, "offset": 0x10CF7B, "normal item": iname.roast_beef,
+ "hard item": iname.red_jewel_l},
+ lname.roc_ent_r: {"code": 0xC3, "offset": 0x10CFBB, "normal item": iname.powerup,
+ "hard item": iname.five_hundred_gold},
+ lname.roc_elev_r: {"code": 0xD4, "offset": 0x10CF93, "normal item": iname.holy_water, "add conds": ["sub"]},
+ lname.roc_elev_l: {"code": 0xD5, "offset": 0x10CF8B, "normal item": iname.axe, "add conds": ["sub"]},
+ lname.roc_cont_r: {"code": 0xC5, "offset": 0x10CFB3, "normal item": iname.powerup,
+ "hard item": iname.one_hundred_gold},
+ lname.roc_cont_l: {"code": 0xDF, "offset": 0x10CFA3, "normal item": iname.three_hundred_gold,
+ "add conds": ["empty"]},
+ lname.roc_exit: {"code": 0xDC, "offset": 0x10CF9B, "normal item": iname.three_hundred_gold,
+ "add conds": ["empty"]},
+ lname.roc_boss: {"event": iname.trophy, "add conds": ["boss"]},
+ # Clock Tower
+ lname.ct_gearclimb_battery_slab1: {"code": 0x269, "offset": 0x10CEF9, "normal item": iname.roast_chicken,
+ "add conds": ["3hb"]},
+ lname.ct_gearclimb_battery_slab2: {"code": 0x26A, "offset": 0x10CEFB, "normal item": iname.roast_chicken,
+ "hard item": iname.red_jewel_s, "add conds": ["3hb"]},
+ lname.ct_gearclimb_battery_slab3: {"code": 0x26B, "offset": 0x10CEFD, "normal item": iname.roast_chicken,
+ "hard item": iname.red_jewel_s, "add conds": ["3hb"]},
+ lname.ct_gearclimb_corner: {"code": 0xA7, "offset": 0x10CEB3, "normal item": iname.red_jewel_s},
+ lname.ct_gearclimb_side: {"code": 0xAD, "offset": 0x10CEC3, "normal item": iname.clocktower_key1},
+ lname.ct_gearclimb_door_slab1: {"code": 0x26D, "offset": 0x10CF01, "normal item": iname.roast_beef,
+ "add conds": ["3hb"]},
+ lname.ct_gearclimb_door_slab2: {"code": 0x26E, "offset": 0x10CF03, "normal item": iname.roast_beef,
+ "hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
+ lname.ct_gearclimb_door_slab3: {"code": 0x26F, "offset": 0x10CF05, "normal item": iname.roast_beef,
+ "hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
+ lname.ct_bp_chasm_fl: {"code": 0xA5, "offset": 0x99BC4D, "normal item": iname.five_hundred_gold},
+ lname.ct_bp_chasm_fr: {"code": 0xA6, "offset": 0x99BC3E, "normal item": iname.red_jewel_l},
+ lname.ct_bp_chasm_rl: {"code": 0xA4, "offset": 0x99BC5A, "normal item": iname.holy_water,
+ "add conds": ["sub"]},
+ lname.ct_bp_chasm_k: {"code": 0xAC, "offset": 0x99BC30, "normal item": iname.clocktower_key2},
+ lname.ct_finalroom_door_slab1: {"code": 0x271, "offset": 0x10CEF5, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.ct_finalroom_door_slab2: {"code": 0x272, "offset": 0x10CEF7, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.ct_finalroom_fl: {"code": 0xB3, "offset": 0x10CED3, "normal item": iname.axe,
+ "add conds": ["sub"]},
+ lname.ct_finalroom_fr: {"code": 0xB4, "offset": 0x10CECB, "normal item": iname.knife,
+ "add conds": ["sub"]},
+ lname.ct_finalroom_rl: {"code": 0xB2, "offset": 0x10CEE3, "normal item": iname.holy_water,
+ "add conds": ["sub"]},
+ lname.ct_finalroom_rr: {"code": 0xB0, "offset": 0x10CEDB, "normal item": iname.cross,
+ "add conds": ["sub"]},
+ lname.ct_finalroom_platform: {"code": 0xAB, "offset": 0x10CEBB, "normal item": iname.clocktower_key3},
+ lname.ct_finalroom_renon_slab1: {"code": 0x274, "offset": 0x10CF09, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.ct_finalroom_renon_slab2: {"code": 0x275, "offset": 0x10CF0B, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.ct_finalroom_renon_slab3: {"code": 0x276, "offset": 0x10CF0D, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.ct_finalroom_renon_slab4: {"code": 0x277, "offset": 0x10CF0F, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.ct_finalroom_renon_slab5: {"code": 0x278, "offset": 0x10CF11, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.ct_finalroom_renon_slab6: {"code": 0x279, "offset": 0x10CF13, "normal item": iname.five_hundred_gold,
+ "add conds": ["3hb"]},
+ lname.ct_finalroom_renon_slab7: {"code": 0x27A, "offset": 0x10CF15, "normal item": iname.red_jewel_l,
+ "add conds": ["3hb"]},
+ lname.ct_finalroom_renon_slab8: {"code": 0x27B, "offset": 0x10CF17, "normal item": iname.red_jewel_l,
+ "add conds": ["3hb"]},
+ # Castle Keep
+ lname.ck_boss_one: {"event": iname.trophy, "add conds": ["boss", "renon"]},
+ lname.ck_boss_two: {"event": iname.trophy, "add conds": ["boss", "vincent"]},
+ lname.ck_flame_l: {"code": 0xAF, "offset": 0x9778C8, "normal item": iname.healing_kit, "type": "inv"},
+ lname.ck_flame_r: {"code": 0xAE, "offset": 0xBFCA67, "normal item": iname.healing_kit, "type": "inv"},
+ lname.ck_behind_drac: {"code": 0xBF, "offset": 0x10CE9B, "normal item": iname.red_jewel_l},
+ lname.ck_cube: {"code": 0xB5, "offset": 0x10CEA3, "normal item": iname.healing_kit},
+ lname.renon1: {"code": 0x1C8, "offset": 0xBFD8E5, "normal item": iname.roast_chicken, "type": "shop"},
+ lname.renon2: {"code": 0x1C9, "offset": 0xBFD8E7, "normal item": iname.roast_beef, "type": "shop"},
+ lname.renon3: {"code": 0x1CA, "offset": 0xBFD8E9, "normal item": iname.healing_kit, "type": "shop"},
+ lname.renon4: {"code": 0x1CB, "offset": 0xBFD8EB, "normal item": iname.purifying, "type": "shop"},
+ lname.renon5: {"code": 0x1CC, "offset": 0xBFD8ED, "normal item": iname.cure_ampoule, "type": "shop"},
+ lname.renon6: {"code": 0x1CD, "offset": 0xBFD907, "normal item": iname.sun_card, "type": "shop"},
+ lname.renon7: {"code": 0x1CE, "offset": 0xBFD909, "normal item": iname.moon_card, "type": "shop"},
+ lname.the_end: {"event": iname.victory},
+}
+
+
+add_conds = {"carrie": ("carrie_logic", True, True),
+ "liz": ("lizard_locker_items", True, True),
+ "sub": ("sub_weapon_shuffle", SubWeaponShuffle.option_anywhere, True),
+ "3hb": ("multi_hit_breakables", True, True),
+ "empty": ("empty_breakables", True, True),
+ "shop": ("shopsanity", True, True),
+ "crystal": ("draculas_condition", DraculasCondition.option_crystal, True),
+ "boss": ("draculas_condition", DraculasCondition.option_bosses, True),
+ "renon": ("renon_fight_condition", RenonFightCondition.option_never, False),
+ "vincent": ("vincent_fight_condition", VincentFightCondition.option_never, False)}
+
+
+def get_location_info(location: str, info: str) -> Union[int, str, List[str], None]:
+ return location_info[location].get(info, None)
+
+
+def get_location_names_to_ids() -> Dict[str, int]:
+ return {name: get_location_info(name, "code")+base_id for name in location_info if get_location_info(name, "code")
+ is not None}
+
+
+def verify_locations(options: CV64Options, locations: List[str]) -> Tuple[Dict[str, Optional[int]], Dict[str, str]]:
+
+ verified_locations = {}
+ events = {}
+
+ for loc in locations:
+ loc_add_conds = get_location_info(loc, "add conds")
+ loc_code = get_location_info(loc, "code")
+
+ # Check any options that might be associated with the Location before adding it.
+ add_it = True
+ if isinstance(loc_add_conds, list):
+ for cond in loc_add_conds:
+ if not ((getattr(options, add_conds[cond][0]).value == add_conds[cond][1]) == add_conds[cond][2]):
+ add_it = False
+
+ if not add_it:
+ continue
+
+ # Add the location to the verified Locations if the above check passes.
+ # If we are looking at an event Location, add its associated event Item to the events' dict.
+ # Otherwise, add the base_id to the Location's code.
+ if loc_code is None:
+ events[loc] = get_location_info(loc, "event")
+ else:
+ loc_code += base_id
+ verified_locations.update({loc: loc_code})
+
+ return verified_locations, events
diff --git a/worlds/cv64/lzkn64.py b/worlds/cv64/lzkn64.py
new file mode 100644
index 000000000000..9a94cebbb4eb
--- /dev/null
+++ b/worlds/cv64/lzkn64.py
@@ -0,0 +1,266 @@
+# **************************************************************
+# * LZKN64 Compression and Decompression Utility *
+# * Original repo at https://github.com/Fluvian/lzkn64, *
+# * converted from C to Python with permission from Fluvian. *
+# **************************************************************
+
+TYPE_COMPRESS = 1
+TYPE_DECOMPRESS = 2
+
+MODE_NONE = 0x7F
+MODE_WINDOW_COPY = 0x00
+MODE_RAW_COPY = 0x80
+MODE_RLE_WRITE_A = 0xC0
+MODE_RLE_WRITE_B = 0xE0
+MODE_RLE_WRITE_C = 0xFF
+
+WINDOW_SIZE = 0x3FF
+COPY_SIZE = 0x21
+RLE_SIZE = 0x101
+
+
+# Compresses the data in the buffer specified in the arguments.
+def compress_buffer(file_buffer: bytearray) -> bytearray:
+ # Size of the buffer to compress
+ buffer_size = len(file_buffer) - 1
+
+ # Position of the current read location in the buffer.
+ buffer_position = 0
+
+ # Position of the current write location in the written buffer.
+ write_position = 4
+
+ # Allocate write_buffer with size of 0xFFFFFF (24-bit).
+ write_buffer = bytearray(0xFFFFFF)
+
+ # Position in the input buffer of the last time one of the copy modes was used.
+ buffer_last_copy_position = 0
+
+ while buffer_position < buffer_size:
+ # Calculate maximum length we are able to copy without going out of bounds.
+ if COPY_SIZE < (buffer_size - 1) - buffer_position:
+ sliding_window_maximum_length = COPY_SIZE
+ else:
+ sliding_window_maximum_length = (buffer_size - 1) - buffer_position
+
+ # Calculate how far we are able to look back without going behind the start of the uncompressed buffer.
+ if buffer_position - WINDOW_SIZE > 0:
+ sliding_window_maximum_offset = buffer_position - WINDOW_SIZE
+ else:
+ sliding_window_maximum_offset = 0
+
+ # Calculate maximum length the forwarding looking window is able to search.
+ if RLE_SIZE < (buffer_size - 1) - buffer_position:
+ forward_window_maximum_length = RLE_SIZE
+ else:
+ forward_window_maximum_length = (buffer_size - 1) - buffer_position
+
+ sliding_window_match_position = -1
+ sliding_window_match_size = 0
+
+ forward_window_match_value = 0
+ forward_window_match_size = 0
+
+ # The current mode the compression algorithm prefers. (0x7F == None)
+ current_mode = MODE_NONE
+
+ # The current submode the compression algorithm prefers.
+ current_submode = MODE_NONE
+
+ # How many bytes will have to be copied in the raw copy command.
+ raw_copy_size = buffer_position - buffer_last_copy_position
+
+ # How many bytes we still have to copy in RLE matches with more than 0x21 bytes.
+ rle_bytes_left = 0
+
+ """Go backwards in the buffer, is there a matching value?
+ If yes, search forward and check for more matching values in a loop.
+ If no, go further back and repeat."""
+ for search_position in range(buffer_position - 1, sliding_window_maximum_offset - 1, -1):
+ matching_sequence_size = 0
+
+ while file_buffer[search_position + matching_sequence_size] == file_buffer[buffer_position +
+ matching_sequence_size]:
+ matching_sequence_size += 1
+
+ if matching_sequence_size >= sliding_window_maximum_length:
+ break
+
+ # Once we find a match or a match that is bigger than the match before it, we save its position and length.
+ if matching_sequence_size > sliding_window_match_size:
+ sliding_window_match_position = search_position
+ sliding_window_match_size = matching_sequence_size
+
+ """Look one step forward in the buffer, is there a matching value?
+ If yes, search further and check for a repeating value in a loop.
+ If no, continue to the rest of the function."""
+ matching_sequence_value = file_buffer[buffer_position]
+ matching_sequence_size = 0
+
+ while file_buffer[buffer_position + matching_sequence_size] == matching_sequence_value:
+ matching_sequence_size += 1
+
+ if matching_sequence_size >= forward_window_maximum_length:
+ break
+
+ # If we find a sequence of matching values, save them.
+ if matching_sequence_size >= 1:
+ forward_window_match_value = matching_sequence_value
+ forward_window_match_size = matching_sequence_size
+
+ # Try to pick which mode works best with the current values.
+ if sliding_window_match_size >= 3:
+ current_mode = MODE_WINDOW_COPY
+ elif forward_window_match_size >= 3:
+ current_mode = MODE_RLE_WRITE_A
+
+ if forward_window_match_value != 0x00 and forward_window_match_size <= COPY_SIZE:
+ current_submode = MODE_RLE_WRITE_A
+ elif forward_window_match_value != 0x00 and forward_window_match_size > COPY_SIZE:
+ current_submode = MODE_RLE_WRITE_A
+ rle_bytes_left = forward_window_match_size
+ elif forward_window_match_value == 0x00 and forward_window_match_size <= COPY_SIZE:
+ current_submode = MODE_RLE_WRITE_B
+ elif forward_window_match_value == 0x00 and forward_window_match_size > COPY_SIZE:
+ current_submode = MODE_RLE_WRITE_C
+ elif forward_window_match_size >= 2 and forward_window_match_value == 0x00:
+ current_mode = MODE_RLE_WRITE_A
+ current_submode = MODE_RLE_WRITE_B
+
+ """Write a raw copy command when these following conditions are met:
+ The current mode is set and there are raw bytes available to be copied.
+ The raw byte length exceeds the maximum length that can be stored.
+ Raw bytes need to be written due to the proximity to the end of the buffer."""
+ if (current_mode != MODE_NONE and raw_copy_size >= 1) or raw_copy_size >= 0x1F or \
+ (buffer_position + 1) == buffer_size:
+ if buffer_position + 1 == buffer_size:
+ raw_copy_size = buffer_size - buffer_last_copy_position
+
+ write_buffer[write_position] = MODE_RAW_COPY | raw_copy_size & 0x1F
+ write_position += 1
+
+ for written_bytes in range(raw_copy_size):
+ write_buffer[write_position] = file_buffer[buffer_last_copy_position]
+ write_position += 1
+ buffer_last_copy_position += 1
+
+ if current_mode == MODE_WINDOW_COPY:
+ write_buffer[write_position] = MODE_WINDOW_COPY | ((sliding_window_match_size - 2) & 0x1F) << 2 | \
+ (((buffer_position - sliding_window_match_position) & 0x300) >> 8)
+ write_position += 1
+ write_buffer[write_position] = (buffer_position - sliding_window_match_position) & 0xFF
+ write_position += 1
+
+ buffer_position += sliding_window_match_size
+ buffer_last_copy_position = buffer_position
+ elif current_mode == MODE_RLE_WRITE_A:
+ if current_submode == MODE_RLE_WRITE_A:
+ if rle_bytes_left > 0:
+ while rle_bytes_left > 0:
+ # Dump raw bytes if we have less than two bytes left, not doing so would cause an underflow
+ # error.
+ if rle_bytes_left < 2:
+ write_buffer[write_position] = MODE_RAW_COPY | rle_bytes_left & 0x1F
+ write_position += 1
+
+ for writtenBytes in range(rle_bytes_left):
+ write_buffer[write_position] = forward_window_match_value & 0xFF
+ write_position += 1
+
+ rle_bytes_left = 0
+ break
+
+ if rle_bytes_left < COPY_SIZE:
+ write_buffer[write_position] = MODE_RLE_WRITE_A | (rle_bytes_left - 2) & 0x1F
+ write_position += 1
+ else:
+ write_buffer[write_position] = MODE_RLE_WRITE_A | (COPY_SIZE - 2) & 0x1F
+ write_position += 1
+ write_buffer[write_position] = forward_window_match_value & 0xFF
+ write_position += 1
+ rle_bytes_left -= COPY_SIZE
+ else:
+ write_buffer[write_position] = MODE_RLE_WRITE_A | (forward_window_match_size - 2) & 0x1F
+ write_position += 1
+ write_buffer[write_position] = forward_window_match_value & 0xFF
+ write_position += 1
+
+ elif current_submode == MODE_RLE_WRITE_B:
+ write_buffer[write_position] = MODE_RLE_WRITE_B | (forward_window_match_size - 2) & 0x1F
+ write_position += 1
+ elif current_submode == MODE_RLE_WRITE_C:
+ write_buffer[write_position] = MODE_RLE_WRITE_C
+ write_position += 1
+ write_buffer[write_position] = (forward_window_match_size - 2) & 0xFF
+ write_position += 1
+
+ buffer_position += forward_window_match_size
+ buffer_last_copy_position = buffer_position
+ else:
+ buffer_position += 1
+
+ # Write the compressed size.
+ write_buffer[1] = 0x00
+ write_buffer[1] = write_position >> 16 & 0xFF
+ write_buffer[2] = write_position >> 8 & 0xFF
+ write_buffer[3] = write_position & 0xFF
+
+ # Return the compressed write buffer.
+ return write_buffer[0:write_position]
+
+
+# Decompresses the data in the buffer specified in the arguments.
+def decompress_buffer(file_buffer: bytearray) -> bytearray:
+ # Position of the current read location in the buffer.
+ buffer_position = 4
+
+ # Position of the current write location in the written buffer.
+ write_position = 0
+
+ # Get compressed size.
+ compressed_size = (file_buffer[1] << 16) + (file_buffer[2] << 8) + file_buffer[3] - 1
+
+ # Allocate writeBuffer with size of 0xFFFFFF (24-bit).
+ write_buffer = bytearray(0xFFFFFF)
+
+ while buffer_position < compressed_size:
+ mode_command = file_buffer[buffer_position]
+ buffer_position += 1
+
+ if MODE_WINDOW_COPY <= mode_command < MODE_RAW_COPY:
+ copy_length = (mode_command >> 2) + 2
+ copy_offset = file_buffer[buffer_position] + (mode_command << 8) & 0x3FF
+ buffer_position += 1
+
+ for current_length in range(copy_length, 0, -1):
+ write_buffer[write_position] = write_buffer[write_position - copy_offset]
+ write_position += 1
+ elif MODE_RAW_COPY <= mode_command < MODE_RLE_WRITE_A:
+ copy_length = mode_command & 0x1F
+
+ for current_length in range(copy_length, 0, -1):
+ write_buffer[write_position] = file_buffer[buffer_position]
+ write_position += 1
+ buffer_position += 1
+ elif MODE_RLE_WRITE_A <= mode_command <= MODE_RLE_WRITE_C:
+ write_length = 0
+ write_value = 0x00
+
+ if MODE_RLE_WRITE_A <= mode_command < MODE_RLE_WRITE_B:
+ write_length = (mode_command & 0x1F) + 2
+ write_value = file_buffer[buffer_position]
+ buffer_position += 1
+ elif MODE_RLE_WRITE_B <= mode_command < MODE_RLE_WRITE_C:
+ write_length = (mode_command & 0x1F) + 2
+ elif mode_command == MODE_RLE_WRITE_C:
+ write_length = file_buffer[buffer_position] + 2
+ buffer_position += 1
+
+ for current_length in range(write_length, 0, -1):
+ write_buffer[write_position] = write_value
+ write_position += 1
+
+ # Return the current position of the write buffer, essentially giving us the size of the write buffer.
+ while write_position % 16 != 0:
+ write_position += 1
+ return write_buffer[0:write_position]
diff --git a/worlds/cv64/options.py b/worlds/cv64/options.py
new file mode 100644
index 000000000000..07e86347bda6
--- /dev/null
+++ b/worlds/cv64/options.py
@@ -0,0 +1,591 @@
+from dataclasses import dataclass
+from Options import (OptionGroup, Choice, DefaultOnToggle, ItemsAccessibility, PerGameCommonOptions, Range, Toggle,
+ StartInventoryPool)
+
+
+class CharacterStages(Choice):
+ """
+ Whether to include Reinhardt-only stages, Carrie-only stages, or both with or without branching paths at the end of Villa and Castle Center.
+ """
+ display_name = "Character Stages"
+ option_both = 0
+ option_branchless_both = 1
+ option_reinhardt_only = 2
+ option_carrie_only = 3
+ default = 0
+
+
+class StageShuffle(Toggle):
+ """
+ Shuffles which stages appear in which stage slots.
+ Villa and Castle Center will never appear in any character stage slots if Character Stages is set to Both; they can only be somewhere on the main path.
+ Castle Keep will always be at the end of the line.
+ """
+ display_name = "Stage Shuffle"
+
+
+class StartingStage(Choice):
+ """
+ Which stage to start at if Stage Shuffle is turned on.
+ """
+ display_name = "Starting Stage"
+ option_forest_of_silence = 0
+ option_castle_wall = 1
+ option_villa = 2
+ option_tunnel = 3
+ option_underground_waterway = 4
+ option_castle_center = 5
+ option_duel_tower = 6
+ option_tower_of_execution = 7
+ option_tower_of_science = 8
+ option_tower_of_sorcery = 9
+ option_room_of_clocks = 10
+ option_clock_tower = 11
+ default = "random"
+
+
+class WarpOrder(Choice):
+ """
+ Arranges the warps in the warp menu in whichever stage order chosen, thereby changing the order they are unlocked in.
+ """
+ display_name = "Warp Order"
+ option_seed_stage_order = 0
+ option_vanilla_stage_order = 1
+ option_randomized_order = 2
+ default = 0
+
+
+class SubWeaponShuffle(Choice):
+ """
+ Shuffles all sub-weapons in the game within each other in their own pool or in the main item pool.
+ """
+ display_name = "Sub-weapon Shuffle"
+ option_off = 0
+ option_own_pool = 1
+ option_anywhere = 2
+ default = 0
+
+
+class SpareKeys(Choice):
+ """
+ Puts an additional copy of every non-Special key item in the pool for every key item that there is.
+ Chance gives each key item a 50% chance of having a duplicate instead of guaranteeing one for all of them.
+ """
+ display_name = "Spare Keys"
+ option_off = 0
+ option_on = 1
+ option_chance = 2
+ default = 0
+
+
+class HardItemPool(Toggle):
+ """
+ Replaces some items in the item pool with less valuable ones, to make the item pool sort of resemble Hard Mode in the PAL version.
+ """
+ display_name = "Hard Item Pool"
+
+
+class Special1sPerWarp(Range):
+ """
+ Sets how many Special1 jewels are needed per warp menu option unlock.
+ This will decrease until the number x 7 is less than or equal to the Total Specail1s if it isn't already.
+ """
+ range_start = 1
+ range_end = 10
+ default = 1
+ display_name = "Special1s Per Warp"
+
+
+class TotalSpecial1s(Range):
+ """
+ Sets how many Speical1 jewels are in the pool in total.
+ """
+ range_start = 7
+ range_end = 70
+ default = 7
+ display_name = "Total Special1s"
+
+
+class DraculasCondition(Choice):
+ """
+ Sets the requirement for unlocking and opening the door to Dracula's chamber.
+ None: No requirement. Door is unlocked from the start.
+ Crystal: Activate the big crystal in Castle Center's basement. Neither boss afterwards has to be defeated.
+ Bosses: Kill a specified number of bosses with health bars and claim their Trophies.
+ Specials: Find a specified number of Special2 jewels shuffled in the main item pool.
+ """
+ display_name = "Dracula's Condition"
+ option_none = 0
+ option_crystal = 1
+ option_bosses = 2
+ option_specials = 3
+ default = 1
+
+
+class PercentSpecial2sRequired(Range):
+ """
+ Percentage of Special2s required to enter Dracula's chamber when Dracula's Condition is Special2s.
+ """
+ range_start = 1
+ range_end = 100
+ default = 80
+ display_name = "Percent Special2s Required"
+
+
+class TotalSpecial2s(Range):
+ """
+ How many Speical2 jewels are in the pool in total when Dracula's Condition is Special2s.
+ """
+ range_start = 1
+ range_end = 70
+ default = 25
+ display_name = "Total Special2s"
+
+
+class BossesRequired(Range):
+ """
+ How many bosses need to be defeated to enter Dracula's chamber when Dracula's Condition is set to Bosses.
+ This will automatically adjust if there are fewer available bosses than the chosen number.
+ """
+ range_start = 1
+ range_end = 16
+ default = 12
+ display_name = "Bosses Required"
+
+
+class CarrieLogic(Toggle):
+ """
+ Adds the 2 checks inside Underground Waterway's crawlspace to the pool.
+ If you (and everyone else if racing the same seed) are planning to only ever play Reinhardt, don't enable this.
+ Can be combined with Hard Logic to include Carrie-only tricks.
+ """
+ display_name = "Carrie Logic"
+
+
+class HardLogic(Toggle):
+ """
+ Properly considers sequence break tricks in logic (i.e. maze skip). Can be combined with Carrie Logic to include Carrie-only tricks.
+ See the Game Page for a full list of tricks and glitches that may be logically required.
+ """
+ display_name = "Hard Logic"
+
+
+class MultiHitBreakables(Toggle):
+ """
+ Adds the items that drop from the objects that break in three hits to the pool.
+ There are 18 of these throughout the game, adding up to 79 or 80 checks (depending on sub-weapons being shuffled anywhere or not) in total with all stages.
+ The game will be modified to remember exactly which of their items you've picked up instead of simply whether they were broken or not.
+ """
+ display_name = "Multi-hit Breakables"
+
+
+class EmptyBreakables(Toggle):
+ """
+ Adds 9 check locations in the form of breakables that normally have nothing (all empty Forest coffins, etc.) and some additional Red Jewels and/or moneybags into the item pool to compensate.
+ """
+ display_name = "Empty Breakables"
+
+
+class LizardLockerItems(Toggle):
+ """
+ Adds the 6 items inside Castle Center 2F's Lizard-man generators to the pool.
+ Picking up all of these can be a very tedious luck-based process, so they are off by default.
+ """
+ display_name = "Lizard Locker Items"
+
+
+class Shopsanity(Toggle):
+ """
+ Adds 7 one-time purchases from Renon's shop into the location pool.
+ After buying an item from a slot, it will revert to whatever it is in the vanilla game.
+ """
+ display_name = "Shopsanity"
+
+
+class ShopPrices(Choice):
+ """
+ Randomizes the amount of gold each item costs in Renon's shop.
+ Use the Minimum and Maximum Gold Price options to control how much or how little an item can cost.
+ """
+ display_name = "Shop Prices"
+ option_vanilla = 0
+ option_randomized = 1
+ default = 0
+
+
+class MinimumGoldPrice(Range):
+ """
+ The lowest amount of gold an item can cost in Renon's shop, divided by 100.
+ """
+ display_name = "Minimum Gold Price"
+ range_start = 1
+ range_end = 50
+ default = 2
+
+
+class MaximumGoldPrice(Range):
+ """
+ The highest amount of gold an item can cost in Renon's shop, divided by 100.
+ """
+ display_name = "Maximum Gold Price"
+ range_start = 1
+ range_end = 50
+ default = 30
+
+
+class PostBehemothBoss(Choice):
+ """
+ Sets which boss is fought in the vampire triplets' room in Castle Center by which characters after defeating Behemoth.
+ """
+ display_name = "Post-Behemoth Boss"
+ option_vanilla = 0
+ option_inverted = 1
+ option_always_rosa = 2
+ option_always_camilla = 3
+ default = 0
+
+
+class RoomOfClocksBoss(Choice):
+ """
+ Sets which boss is fought at Room of Clocks by which characters.
+ """
+ display_name = "Room of Clocks Boss"
+ option_vanilla = 0
+ option_inverted = 1
+ option_always_death = 2
+ option_always_actrise = 3
+ default = 0
+
+
+class RenonFightCondition(Choice):
+ """
+ Sets the condition on which the Renon fight will trigger.
+ """
+ display_name = "Renon Fight Condition"
+ option_never = 0
+ option_spend_30k = 1
+ option_always = 2
+ default = 1
+
+
+class VincentFightCondition(Choice):
+ """
+ Sets the condition on which the vampire Vincent fight will trigger.
+ """
+ display_name = "Vincent Fight Condition"
+ option_never = 0
+ option_wait_16_days = 1
+ option_always = 2
+ default = 1
+
+
+class BadEndingCondition(Choice):
+ """
+ Sets the condition on which the currently-controlled character's Bad Ending will trigger.
+ """
+ display_name = "Bad Ending Condition"
+ option_never = 0
+ option_kill_vincent = 1
+ option_always = 2
+ default = 1
+
+
+class IncreaseItemLimit(DefaultOnToggle):
+ """
+ Increases the holding limit of usable items from 10 to 99 of each item.
+ """
+ display_name = "Increase Item Limit"
+
+
+class NerfHealingItems(Toggle):
+ """
+ Decreases the amount of health healed by Roast Chickens to 25%, Roast Beefs to 50%, and Healing Kits to 80%.
+ """
+ display_name = "Nerf Healing Items"
+
+
+class LoadingZoneHeals(DefaultOnToggle):
+ """
+ Whether end-of-level loading zones restore health and cure status aliments or not.
+ Recommended off for those looking for more of a survival horror experience!
+ """
+ display_name = "Loading Zone Heals"
+
+
+class InvisibleItems(Choice):
+ """
+ Sets which items are visible in their locations and which are invisible until picked up.
+ 'Chance' gives each item a 50/50 chance of being visible or invisible.
+ """
+ display_name = "Invisible Items"
+ option_vanilla = 0
+ option_reveal_all = 1
+ option_hide_all = 2
+ option_chance = 3
+ default = 0
+
+
+class DropPreviousSubWeapon(Toggle):
+ """
+ When receiving a sub-weapon, the one you had before will drop behind you, so it can be taken back if desired.
+ """
+ display_name = "Drop Previous Sub-weapon"
+
+
+class PermanentPowerUps(Toggle):
+ """
+ Replaces PowerUps with PermaUps, which upgrade your B weapon level permanently and will stay even after dying and/or continuing.
+ To compensate, only two will be in the pool overall, and they will not drop from any enemy or projectile.
+ """
+ display_name = "Permanent PowerUps"
+
+
+class IceTrapPercentage(Range):
+ """
+ Replaces a percentage of junk items with Ice Traps.
+ These will be visibly disguised as other items, and receiving one will freeze you as if you were hit by Camilla's ice cloud attack.
+ """
+ display_name = "Ice Trap Percentage"
+ range_start = 0
+ range_end = 100
+ default = 0
+
+
+class IceTrapAppearance(Choice):
+ """
+ What items Ice Traps can possibly be disguised as.
+ """
+ display_name = "Ice Trap Appearance"
+ option_major_only = 0
+ option_junk_only = 1
+ option_anything = 2
+ default = 0
+
+
+class DisableTimeRestrictions(Toggle):
+ """
+ Disables the restriction on every event and door that requires the current time to be within a specific range, so they can be triggered at any time.
+ This includes all sun/moon doors and, in the Villa, the meeting with Rosa and the fountain pillar.
+ The Villa coffin is not affected by this.
+ """
+ display_name = "Disable Time Requirements"
+
+
+class SkipGondolas(Toggle):
+ """
+ Makes jumping on and activating a gondola in Tunnel instantly teleport you to the other station, thereby skipping the entire three-minute ride.
+ The item normally at the gondola transfer point is moved to instead be near the red gondola at its station.
+ """
+ display_name = "Skip Gondolas"
+
+
+class SkipWaterwayBlocks(Toggle):
+ """
+ Opens the door to the third switch in Underground Waterway from the start so that the jumping across floating brick platforms won't have to be done.
+ Shopping at the Contract on the other side of them may still be logically required if Shopsanity is on.
+ """
+ display_name = "Skip Waterway Blocks"
+
+
+class Countdown(Choice):
+ """
+ Displays, near the HUD clock and below the health bar, the number of unobtained progression-marked items or the total check locations remaining in the stage you are currently in.
+ """
+ display_name = "Countdown"
+ option_none = 0
+ option_majors = 1
+ option_all_locations = 2
+ default = 0
+
+
+class BigToss(Toggle):
+ """
+ Makes every non-immobilizing damage source launch you as if you got hit by Behemoth's charge.
+ Press A while tossed to cancel the launch momentum and avoid being thrown off ledges.
+ Hold Z to have all incoming damage be treated as it normally would.
+ Any tricks that might be possible with it are not in logic.
+ """
+ display_name = "Big Toss"
+
+
+class PantherDash(Choice):
+ """
+ Hold C-right at any time to sprint way faster.
+ Any tricks that are possible with it are not in logic and any boss fights with boss health meters, if started, are expected to be finished before leaving their arenas if Dracula's Condition is bosses.
+ Jumpless will prevent jumping while moving at the increased speed to make logic harder to break with it.
+ """
+ display_name = "Panther Dash"
+ option_off = 0
+ option_on = 1
+ option_jumpless = 2
+ default = 0
+
+
+class IncreaseShimmySpeed(Toggle):
+ """
+ Increases the speed at which characters shimmy left and right while hanging on ledges.
+ """
+ display_name = "Increase Shimmy Speed"
+
+
+class FallGuard(Toggle):
+ """
+ Removes fall damage from landing too hard. Note that falling for too long will still result in instant death.
+ """
+ display_name = "Fall Guard"
+
+
+class BackgroundMusic(Choice):
+ """
+ Randomizes or disables the music heard throughout the game.
+ Randomized music is split into two pools: songs that loop and songs that don't.
+ The "lead-in" versions of some songs will be paired accordingly.
+ """
+ display_name = "Background Music"
+ option_normal = 0
+ option_disabled = 1
+ option_randomized = 2
+ default = 0
+
+
+class MapLighting(Choice):
+ """
+ Randomizes the lighting color RGB values on every map during every time of day to be literally anything.
+ The colors and/or shading of the following things are affected: fog, maps, player, enemies, and some objects.
+ """
+ display_name = "Map Lighting"
+ option_normal = 0
+ option_randomized = 1
+ default = 0
+
+
+class CinematicExperience(Toggle):
+ """
+ Enables an unused film reel effect on every cutscene in the game. Purely cosmetic.
+ """
+ display_name = "Cinematic Experience"
+
+
+class WindowColorR(Range):
+ """
+ The red value for the background color of the text windows during gameplay.
+ """
+ display_name = "Window Color R"
+ range_start = 0
+ range_end = 15
+ default = 1
+
+
+class WindowColorG(Range):
+ """
+ The green value for the background color of the text windows during gameplay.
+ """
+ display_name = "Window Color G"
+ range_start = 0
+ range_end = 15
+ default = 5
+
+
+class WindowColorB(Range):
+ """
+ The blue value for the background color of the text windows during gameplay.
+ """
+ display_name = "Window Color B"
+ range_start = 0
+ range_end = 15
+ default = 15
+
+
+class WindowColorA(Range):
+ """
+ The alpha value for the background color of the text windows during gameplay.
+ """
+ display_name = "Window Color A"
+ range_start = 0
+ range_end = 15
+ default = 8
+
+
+class DeathLink(Choice):
+ """
+ When you die, everyone dies. Of course the reverse is true too.
+ Explosive: Makes received DeathLinks kill you via the Magical Nitro explosion instead of the normal death animation.
+ """
+ display_name = "DeathLink"
+ option_off = 0
+ alias_no = 0
+ alias_true = 1
+ alias_yes = 1
+ option_on = 1
+ option_explosive = 2
+
+
+@dataclass
+class CV64Options(PerGameCommonOptions):
+ accessibility: ItemsAccessibility
+ start_inventory_from_pool: StartInventoryPool
+ character_stages: CharacterStages
+ stage_shuffle: StageShuffle
+ starting_stage: StartingStage
+ warp_order: WarpOrder
+ sub_weapon_shuffle: SubWeaponShuffle
+ spare_keys: SpareKeys
+ hard_item_pool: HardItemPool
+ special1s_per_warp: Special1sPerWarp
+ total_special1s: TotalSpecial1s
+ draculas_condition: DraculasCondition
+ percent_special2s_required: PercentSpecial2sRequired
+ total_special2s: TotalSpecial2s
+ bosses_required: BossesRequired
+ carrie_logic: CarrieLogic
+ hard_logic: HardLogic
+ multi_hit_breakables: MultiHitBreakables
+ empty_breakables: EmptyBreakables
+ lizard_locker_items: LizardLockerItems
+ shopsanity: Shopsanity
+ shop_prices: ShopPrices
+ minimum_gold_price: MinimumGoldPrice
+ maximum_gold_price: MaximumGoldPrice
+ post_behemoth_boss: PostBehemothBoss
+ room_of_clocks_boss: RoomOfClocksBoss
+ renon_fight_condition: RenonFightCondition
+ vincent_fight_condition: VincentFightCondition
+ bad_ending_condition: BadEndingCondition
+ increase_item_limit: IncreaseItemLimit
+ nerf_healing_items: NerfHealingItems
+ loading_zone_heals: LoadingZoneHeals
+ invisible_items: InvisibleItems
+ drop_previous_sub_weapon: DropPreviousSubWeapon
+ permanent_powerups: PermanentPowerUps
+ ice_trap_percentage: IceTrapPercentage
+ ice_trap_appearance: IceTrapAppearance
+ disable_time_restrictions: DisableTimeRestrictions
+ skip_gondolas: SkipGondolas
+ skip_waterway_blocks: SkipWaterwayBlocks
+ countdown: Countdown
+ big_toss: BigToss
+ panther_dash: PantherDash
+ increase_shimmy_speed: IncreaseShimmySpeed
+ window_color_r: WindowColorR
+ window_color_g: WindowColorG
+ window_color_b: WindowColorB
+ window_color_a: WindowColorA
+ background_music: BackgroundMusic
+ map_lighting: MapLighting
+ fall_guard: FallGuard
+ cinematic_experience: CinematicExperience
+ death_link: DeathLink
+
+
+cv64_option_groups = [
+ OptionGroup("gameplay tweaks", [
+ HardItemPool, ShopPrices, MinimumGoldPrice, MaximumGoldPrice, PostBehemothBoss, RoomOfClocksBoss,
+ RenonFightCondition, VincentFightCondition, BadEndingCondition, IncreaseItemLimit, NerfHealingItems,
+ LoadingZoneHeals, InvisibleItems, DropPreviousSubWeapon, PermanentPowerUps, IceTrapPercentage,
+ IceTrapAppearance, DisableTimeRestrictions, SkipGondolas, SkipWaterwayBlocks, Countdown, BigToss, PantherDash,
+ IncreaseShimmySpeed, FallGuard, DeathLink
+ ]),
+ OptionGroup("cosmetics", [
+ WindowColorR, WindowColorG, WindowColorB, WindowColorA, BackgroundMusic, MapLighting, CinematicExperience
+ ])
+]
diff --git a/worlds/cv64/regions.py b/worlds/cv64/regions.py
new file mode 100644
index 000000000000..2194828a19ae
--- /dev/null
+++ b/worlds/cv64/regions.py
@@ -0,0 +1,517 @@
+from .data import lname, rname, ename
+from typing import List, Union
+
+
+# # # KEY # # #
+# "stage" = What stage the Region is a part of. The Region and its corresponding Locations and Entrances will only be
+# put in if its stage is active.
+# "locations" = The Locations to add to that Region when putting in said Region (provided their add conditions pass).
+# "entrances" = The Entrances to add to that Region when putting in said Region (provided their add conditions pass).
+region_info = {
+ "Menu": {},
+
+ rname.forest_start: {"stage": rname.forest_of_silence,
+ "locations": [lname.forest_pillars_right,
+ lname.forest_pillars_left,
+ lname.forest_pillars_top,
+ lname.forest_king_skeleton,
+ lname.forest_boss_one,
+ lname.forest_lgaz_in,
+ lname.forest_lgaz_top,
+ lname.forest_hgaz_in,
+ lname.forest_hgaz_top,
+ lname.forest_weretiger_sw,
+ lname.forest_boss_two,
+ lname.forest_weretiger_gate,
+ lname.forest_dirge_tomb_l,
+ lname.forest_dirge_tomb_u,
+ lname.forest_dirge_plaque,
+ lname.forest_dirge_ped,
+ lname.forest_dirge_rock1,
+ lname.forest_dirge_rock2,
+ lname.forest_dirge_rock3,
+ lname.forest_dirge_rock4,
+ lname.forest_dirge_rock5,
+ lname.forest_corpse_save,
+ lname.forest_dbridge_wall,
+ lname.forest_dbridge_sw],
+ "entrances": [ename.forest_dbridge_gate]},
+
+ rname.forest_mid: {"stage": rname.forest_of_silence,
+ "locations": [lname.forest_dbridge_gate_l,
+ lname.forest_dbridge_gate_r,
+ lname.forest_dbridge_tomb_l,
+ lname.forest_dbridge_tomb_ur,
+ lname.forest_dbridge_tomb_uf,
+ lname.forest_bface_tomb_lf,
+ lname.forest_bface_tomb_lr,
+ lname.forest_bface_tomb_u,
+ lname.forest_ibridge,
+ lname.forest_bridge_rock1,
+ lname.forest_bridge_rock2,
+ lname.forest_bridge_rock3,
+ lname.forest_bridge_rock4,
+ lname.forest_werewolf_tomb_lf,
+ lname.forest_werewolf_tomb_lr,
+ lname.forest_werewolf_tomb_r,
+ lname.forest_werewolf_plaque,
+ lname.forest_werewolf_tree,
+ lname.forest_werewolf_island,
+ lname.forest_final_sw],
+ "entrances": [ename.forest_werewolf_gate]},
+
+ rname.forest_end: {"stage": rname.forest_of_silence,
+ "locations": [lname.forest_boss_three],
+ "entrances": [ename.forest_end]},
+
+ rname.cw_start: {"stage": rname.castle_wall,
+ "locations": [lname.cwr_bottom,
+ lname.cw_dragon_sw,
+ lname.cw_boss,
+ lname.cw_save_slab1,
+ lname.cw_save_slab2,
+ lname.cw_save_slab3,
+ lname.cw_save_slab4,
+ lname.cw_save_slab5,
+ lname.cw_rrampart,
+ lname.cw_lrampart,
+ lname.cw_pillar,
+ lname.cw_shelf_visible,
+ lname.cw_shelf_sandbags,
+ lname.cw_shelf_torch],
+ "entrances": [ename.cw_portcullis_c,
+ ename.cw_lt_skip,
+ ename.cw_lt_door]},
+
+ rname.cw_exit: {"stage": rname.castle_wall,
+ "locations": [lname.cw_ground_left,
+ lname.cw_ground_middle,
+ lname.cw_ground_right]},
+
+ rname.cw_ltower: {"stage": rname.castle_wall,
+ "locations": [lname.cwl_bottom,
+ lname.cwl_bridge,
+ lname.cw_drac_sw,
+ lname.cw_drac_slab1,
+ lname.cw_drac_slab2,
+ lname.cw_drac_slab3,
+ lname.cw_drac_slab4,
+ lname.cw_drac_slab5],
+ "entrances": [ename.cw_end]},
+
+ rname.villa_start: {"stage": rname.villa,
+ "locations": [lname.villafy_outer_gate_l,
+ lname.villafy_outer_gate_r,
+ lname.villafy_dog_platform,
+ lname.villafy_inner_gate],
+ "entrances": [ename.villa_dog_gates]},
+
+ rname.villa_main: {"stage": rname.villa,
+ "locations": [lname.villafy_gate_marker,
+ lname.villafy_villa_marker,
+ lname.villafy_tombstone,
+ lname.villafy_fountain_fl,
+ lname.villafy_fountain_fr,
+ lname.villafy_fountain_ml,
+ lname.villafy_fountain_mr,
+ lname.villafy_fountain_rl,
+ lname.villafy_fountain_rr,
+ lname.villafo_front_r,
+ lname.villafo_front_l,
+ lname.villafo_mid_l,
+ lname.villafo_mid_r,
+ lname.villafo_rear_r,
+ lname.villafo_rear_l,
+ lname.villafo_pot_r,
+ lname.villafo_pot_l,
+ lname.villafo_sofa,
+ lname.villafo_chandelier1,
+ lname.villafo_chandelier2,
+ lname.villafo_chandelier3,
+ lname.villafo_chandelier4,
+ lname.villafo_chandelier5,
+ lname.villala_hallway_stairs,
+ lname.villala_hallway_l,
+ lname.villala_hallway_r,
+ lname.villala_bedroom_chairs,
+ lname.villala_bedroom_bed,
+ lname.villala_vincent,
+ lname.villala_slivingroom_table,
+ lname.villala_slivingroom_mirror,
+ lname.villala_diningroom_roses,
+ lname.villala_llivingroom_pot_r,
+ lname.villala_llivingroom_pot_l,
+ lname.villala_llivingroom_painting,
+ lname.villala_llivingroom_light,
+ lname.villala_llivingroom_lion,
+ lname.villala_exit_knight],
+ "entrances": [ename.villa_snipe_dogs,
+ ename.villa_renon,
+ ename.villa_to_storeroom,
+ ename.villa_to_archives,
+ ename.villa_to_maze]},
+
+ rname.villa_storeroom: {"stage": rname.villa,
+ "locations": [lname.villala_storeroom_l,
+ lname.villala_storeroom_r,
+ lname.villala_storeroom_s],
+ "entrances": [ename.villa_from_storeroom]},
+
+ rname.villa_archives: {"stage": rname.villa,
+ "locations": [lname.villala_archives_entrance,
+ lname.villala_archives_table,
+ lname.villala_archives_rear]},
+
+ rname.villa_maze: {"stage": rname.villa,
+ "locations": [lname.villam_malus_torch,
+ lname.villam_malus_bush,
+ lname.villam_fplatform,
+ lname.villam_frankieturf_l,
+ lname.villam_frankieturf_r,
+ lname.villam_frankieturf_ru,
+ lname.villam_fgarden_f,
+ lname.villam_fgarden_mf,
+ lname.villam_fgarden_mr,
+ lname.villam_fgarden_r,
+ lname.villam_rplatform,
+ lname.villam_rplatform_de,
+ lname.villam_exit_de,
+ lname.villam_serv_path],
+ "entrances": [ename.villa_from_maze,
+ ename.villa_copper_door,
+ ename.villa_copper_skip]},
+
+ rname.villa_servants: {"stage": rname.villa,
+ "locations": [lname.villafo_serv_ent],
+ "entrances": [ename.villa_servant_door]},
+
+ rname.villa_crypt: {"stage": rname.villa,
+ "locations": [lname.villam_crypt_ent,
+ lname.villam_crypt_upstream,
+ lname.villac_ent_l,
+ lname.villac_ent_r,
+ lname.villac_wall_l,
+ lname.villac_wall_r,
+ lname.villac_coffin_l,
+ lname.villac_coffin_r,
+ lname.villa_boss_one,
+ lname.villa_boss_two],
+ "entrances": [ename.villa_bridge_door,
+ ename.villa_end_r,
+ ename.villa_end_c]},
+
+ rname.tunnel_start: {"stage": rname.tunnel,
+ "locations": [lname.tunnel_landing,
+ lname.tunnel_landing_rc,
+ lname.tunnel_stone_alcove_r,
+ lname.tunnel_stone_alcove_l,
+ lname.tunnel_twin_arrows,
+ lname.tunnel_arrows_rock1,
+ lname.tunnel_arrows_rock2,
+ lname.tunnel_arrows_rock3,
+ lname.tunnel_arrows_rock4,
+ lname.tunnel_arrows_rock5,
+ lname.tunnel_lonesome_bucket,
+ lname.tunnel_lbucket_mdoor_l,
+ lname.tunnel_lbucket_quag,
+ lname.tunnel_bucket_quag_rock1,
+ lname.tunnel_bucket_quag_rock2,
+ lname.tunnel_bucket_quag_rock3,
+ lname.tunnel_lbucket_albert,
+ lname.tunnel_albert_camp,
+ lname.tunnel_albert_quag,
+ lname.tunnel_gondola_rc_sdoor_l,
+ lname.tunnel_gondola_rc_sdoor_m,
+ lname.tunnel_gondola_rc_sdoor_r,
+ lname.tunnel_gondola_rc,
+ lname.tunnel_rgondola_station,
+ lname.tunnel_gondola_transfer],
+ "entrances": [ename.tunnel_start_renon,
+ ename.tunnel_gondolas]},
+
+ rname.tunnel_end: {"stage": rname.tunnel,
+ "locations": [lname.tunnel_corpse_bucket_quag,
+ lname.tunnel_corpse_bucket_mdoor_l,
+ lname.tunnel_corpse_bucket_mdoor_r,
+ lname.tunnel_shovel_quag_start,
+ lname.tunnel_exit_quag_start,
+ lname.tunnel_shovel_quag_end,
+ lname.tunnel_exit_quag_end,
+ lname.tunnel_shovel,
+ lname.tunnel_shovel_save,
+ lname.tunnel_shovel_mdoor_l,
+ lname.tunnel_shovel_mdoor_r,
+ lname.tunnel_shovel_sdoor_l,
+ lname.tunnel_shovel_sdoor_m,
+ lname.tunnel_shovel_sdoor_r],
+ "entrances": [ename.tunnel_end_renon,
+ ename.tunnel_end]},
+
+ rname.uw_main: {"stage": rname.underground_waterway,
+ "locations": [lname.uw_near_ent,
+ lname.uw_across_ent,
+ lname.uw_first_ledge1,
+ lname.uw_first_ledge2,
+ lname.uw_first_ledge3,
+ lname.uw_first_ledge4,
+ lname.uw_first_ledge5,
+ lname.uw_first_ledge6,
+ lname.uw_poison_parkour,
+ lname.uw_boss,
+ lname.uw_waterfall_alcove,
+ lname.uw_carrie1,
+ lname.uw_carrie2,
+ lname.uw_bricks_save,
+ lname.uw_above_skel_ledge,
+ lname.uw_in_skel_ledge1,
+ lname.uw_in_skel_ledge2,
+ lname.uw_in_skel_ledge3],
+ "entrances": [ename.uw_final_waterfall,
+ ename.uw_renon]},
+
+ rname.uw_end: {"stage": rname.underground_waterway,
+ "entrances": [ename.uw_waterfall_skip,
+ ename.uw_end]},
+
+ rname.cc_main: {"stage": rname.castle_center,
+ "locations": [lname.ccb_skel_hallway_ent,
+ lname.ccb_skel_hallway_jun,
+ lname.ccb_skel_hallway_tc,
+ lname.ccb_skel_hallway_ba,
+ lname.ccb_behemoth_l_ff,
+ lname.ccb_behemoth_l_mf,
+ lname.ccb_behemoth_l_mr,
+ lname.ccb_behemoth_l_fr,
+ lname.ccb_behemoth_r_ff,
+ lname.ccb_behemoth_r_mf,
+ lname.ccb_behemoth_r_mr,
+ lname.ccb_behemoth_r_fr,
+ lname.ccb_behemoth_crate1,
+ lname.ccb_behemoth_crate2,
+ lname.ccb_behemoth_crate3,
+ lname.ccb_behemoth_crate4,
+ lname.ccb_behemoth_crate5,
+ lname.ccelv_near_machine,
+ lname.ccelv_atop_machine,
+ lname.ccelv_stand1,
+ lname.ccelv_stand2,
+ lname.ccelv_stand3,
+ lname.ccelv_pipes,
+ lname.ccelv_switch,
+ lname.ccelv_staircase,
+ lname.ccff_redcarpet_knight,
+ lname.ccff_gears_side,
+ lname.ccff_gears_mid,
+ lname.ccff_gears_corner,
+ lname.ccff_lizard_knight,
+ lname.ccff_lizard_near_knight,
+ lname.ccff_lizard_pit,
+ lname.ccff_lizard_corner,
+ lname.ccff_lizard_locker_nfr,
+ lname.ccff_lizard_locker_nmr,
+ lname.ccff_lizard_locker_nml,
+ lname.ccff_lizard_locker_nfl,
+ lname.ccff_lizard_locker_fl,
+ lname.ccff_lizard_locker_fr,
+ lname.ccff_lizard_slab1,
+ lname.ccff_lizard_slab2,
+ lname.ccff_lizard_slab3,
+ lname.ccff_lizard_slab4,
+ lname.ccll_brokenstairs_floor,
+ lname.ccll_brokenstairs_knight,
+ lname.ccll_brokenstairs_save,
+ lname.ccll_glassknight_l,
+ lname.ccll_glassknight_r,
+ lname.ccll_butlers_door,
+ lname.ccll_butlers_side,
+ lname.ccll_cwhall_butlerflames_past,
+ lname.ccll_cwhall_flamethrower,
+ lname.ccll_cwhall_cwflames,
+ lname.ccll_heinrich,
+ lname.ccia_nitro_crates,
+ lname.ccia_nitro_shelf_h,
+ lname.ccia_stairs_knight,
+ lname.ccia_maids_vase,
+ lname.ccia_maids_outer,
+ lname.ccia_maids_inner,
+ lname.ccia_inventions_maids,
+ lname.ccia_inventions_crusher,
+ lname.ccia_inventions_famicart,
+ lname.ccia_inventions_zeppelin,
+ lname.ccia_inventions_round,
+ lname.ccia_nitrohall_flamethrower,
+ lname.ccia_nitrohall_torch,
+ lname.ccia_nitro_shelf_i],
+ "entrances": [ename.cc_tc_door,
+ ename.cc_lower_wall,
+ ename.cc_renon,
+ ename.cc_upper_wall]},
+
+ rname.cc_torture_chamber: {"stage": rname.castle_center,
+ "locations": [lname.ccb_mandrag_shelf_l,
+ lname.ccb_mandrag_shelf_r,
+ lname.ccb_torture_rack,
+ lname.ccb_torture_rafters]},
+
+ rname.cc_library: {"stage": rname.castle_center,
+ "locations": [lname.ccll_cwhall_wall,
+ lname.ccl_bookcase]},
+
+ rname.cc_crystal: {"stage": rname.castle_center,
+ "locations": [lname.cc_behind_the_seal,
+ lname.cc_boss_one,
+ lname.cc_boss_two],
+ "entrances": [ename.cc_elevator]},
+
+ rname.cc_elev_top: {"stage": rname.castle_center,
+ "entrances": [ename.cc_exit_r,
+ ename.cc_exit_c]},
+
+ rname.dt_main: {"stage": rname.duel_tower,
+ "locations": [lname.dt_boss_one,
+ lname.dt_boss_two,
+ lname.dt_ibridge_l,
+ lname.dt_ibridge_r,
+ lname.dt_stones_start,
+ lname.dt_stones_end,
+ lname.dt_werebull_arena,
+ lname.dt_boss_three,
+ lname.dt_boss_four],
+ "entrances": [ename.dt_start,
+ ename.dt_end]},
+
+ rname.toe_main: {"stage": rname.tower_of_execution,
+ "locations": [lname.toe_ledge1,
+ lname.toe_ledge2,
+ lname.toe_ledge3,
+ lname.toe_ledge4,
+ lname.toe_ledge5,
+ lname.toe_midsavespikes_r,
+ lname.toe_midsavespikes_l,
+ lname.toe_elec_grate,
+ lname.toe_ibridge,
+ lname.toe_top],
+ "entrances": [ename.toe_start,
+ ename.toe_gate,
+ ename.toe_gate_skip,
+ ename.toe_end]},
+
+ rname.toe_ledge: {"stage": rname.tower_of_execution,
+ "locations": [lname.toe_keygate_l,
+ lname.toe_keygate_r]},
+
+ rname.tosci_start: {"stage": rname.tower_of_science,
+ "locations": [lname.tosci_elevator,
+ lname.tosci_plain_sr,
+ lname.tosci_stairs_sr],
+ "entrances": [ename.tosci_start,
+ ename.tosci_key1_door,
+ ename.tosci_to_key2_door]},
+
+ rname.tosci_three_doors: {"stage": rname.tower_of_science,
+ "locations": [lname.tosci_three_door_hall]},
+
+ rname.tosci_conveyors: {"stage": rname.tower_of_science,
+ "locations": [lname.tosci_ibridge_t,
+ lname.tosci_ibridge_b1,
+ lname.tosci_ibridge_b2,
+ lname.tosci_ibridge_b3,
+ lname.tosci_ibridge_b4,
+ lname.tosci_ibridge_b5,
+ lname.tosci_ibridge_b6,
+ lname.tosci_conveyor_sr,
+ lname.tosci_exit],
+ "entrances": [ename.tosci_from_key2_door,
+ ename.tosci_key3_door,
+ ename.tosci_end]},
+
+ rname.tosci_key3: {"stage": rname.tower_of_science,
+ "locations": [lname.tosci_key3_r,
+ lname.tosci_key3_m,
+ lname.tosci_key3_l]},
+
+ rname.tosor_main: {"stage": rname.tower_of_sorcery,
+ "locations": [lname.tosor_stained_tower,
+ lname.tosor_savepoint,
+ lname.tosor_trickshot,
+ lname.tosor_yellow_bubble,
+ lname.tosor_blue_platforms,
+ lname.tosor_side_isle,
+ lname.tosor_ibridge],
+ "entrances": [ename.tosor_start,
+ ename.tosor_end]},
+
+ rname.roc_main: {"stage": rname.room_of_clocks,
+ "locations": [lname.roc_ent_l,
+ lname.roc_ent_r,
+ lname.roc_elev_r,
+ lname.roc_elev_l,
+ lname.roc_cont_r,
+ lname.roc_cont_l,
+ lname.roc_exit,
+ lname.roc_boss],
+ "entrances": [ename.roc_gate]},
+
+ rname.ct_start: {"stage": rname.clock_tower,
+ "locations": [lname.ct_gearclimb_battery_slab1,
+ lname.ct_gearclimb_battery_slab2,
+ lname.ct_gearclimb_battery_slab3,
+ lname.ct_gearclimb_side,
+ lname.ct_gearclimb_corner,
+ lname.ct_gearclimb_door_slab1,
+ lname.ct_gearclimb_door_slab2,
+ lname.ct_gearclimb_door_slab3],
+ "entrances": [ename.ct_to_door1]},
+
+ rname.ct_middle: {"stage": rname.clock_tower,
+ "locations": [lname.ct_bp_chasm_fl,
+ lname.ct_bp_chasm_fr,
+ lname.ct_bp_chasm_rl,
+ lname.ct_bp_chasm_k],
+ "entrances": [ename.ct_from_door1,
+ ename.ct_to_door2]},
+
+ rname.ct_end: {"stage": rname.clock_tower,
+ "locations": [lname.ct_finalroom_door_slab1,
+ lname.ct_finalroom_door_slab2,
+ lname.ct_finalroom_fl,
+ lname.ct_finalroom_fr,
+ lname.ct_finalroom_rl,
+ lname.ct_finalroom_rr,
+ lname.ct_finalroom_platform,
+ lname.ct_finalroom_renon_slab1,
+ lname.ct_finalroom_renon_slab2,
+ lname.ct_finalroom_renon_slab3,
+ lname.ct_finalroom_renon_slab4,
+ lname.ct_finalroom_renon_slab5,
+ lname.ct_finalroom_renon_slab6,
+ lname.ct_finalroom_renon_slab7,
+ lname.ct_finalroom_renon_slab8],
+ "entrances": [ename.ct_from_door2,
+ ename.ct_renon,
+ ename.ct_door_3]},
+
+ rname.ck_main: {"stage": rname.castle_keep,
+ "locations": [lname.ck_boss_one,
+ lname.ck_boss_two,
+ lname.ck_flame_l,
+ lname.ck_flame_r,
+ lname.ck_behind_drac,
+ lname.ck_cube],
+ "entrances": [ename.ck_slope_jump,
+ ename.ck_drac_door]},
+
+ rname.renon: {"locations": [lname.renon1,
+ lname.renon2,
+ lname.renon3,
+ lname.renon4,
+ lname.renon5,
+ lname.renon6,
+ lname.renon7]},
+
+ rname.ck_drac_chamber: {"locations": [lname.the_end]}
+}
+
+
+def get_region_info(region: str, info: str) -> Union[str, List[str], None]:
+ return region_info[region].get(info, None)
diff --git a/worlds/cv64/rom.py b/worlds/cv64/rom.py
new file mode 100644
index 000000000000..ab4371b0ac12
--- /dev/null
+++ b/worlds/cv64/rom.py
@@ -0,0 +1,1026 @@
+import json
+import Utils
+
+from BaseClasses import Location
+from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension
+from typing import List, Dict, Union, Iterable, Collection, Optional, TYPE_CHECKING
+
+import hashlib
+import os
+import pkgutil
+
+from . import lzkn64
+from .data import patches
+from .stages import get_stage_info
+from .text import cv64_string_to_bytearray, cv64_text_truncate, cv64_text_wrap
+from .aesthetics import renon_item_dialogue, get_item_text_color
+from .locations import get_location_info
+from .options import CharacterStages, VincentFightCondition, RenonFightCondition, PostBehemothBoss, RoomOfClocksBoss, \
+ BadEndingCondition, DeathLink, DraculasCondition, InvisibleItems, Countdown, PantherDash
+from settings import get_settings
+
+if TYPE_CHECKING:
+ from . import CV64World
+
+CV64_US_10_HASH = "1cc5cf3b4d29d8c3ade957648b529dc1"
+
+warp_map_offsets = [0xADF67, 0xADF77, 0xADF87, 0xADF97, 0xADFA7, 0xADFBB, 0xADFCB, 0xADFDF]
+
+
+class RomData:
+ orig_buffer: None
+ buffer: bytearray
+
+ def __init__(self, file: bytes, name: Optional[str] = None) -> None:
+ self.file = bytearray(file)
+ self.name = name
+
+ def read_bit(self, address: int, bit_number: int) -> bool:
+ bitflag = (1 << bit_number)
+ return (self.buffer[address] & bitflag) != 0
+
+ def read_byte(self, address: int) -> int:
+ return self.file[address]
+
+ def read_bytes(self, start_address: int, length: int) -> bytearray:
+ return self.file[start_address:start_address + length]
+
+ def write_byte(self, address: int, value: int) -> None:
+ self.file[address] = value
+
+ def write_bytes(self, start_address: int, values: Collection[int]) -> None:
+ self.file[start_address:start_address + len(values)] = values
+
+ def write_int16(self, address: int, value: int) -> None:
+ value = value & 0xFFFF
+ self.write_bytes(address, [(value >> 8) & 0xFF, value & 0xFF])
+
+ def write_int16s(self, start_address: int, values: List[int]) -> None:
+ for i, value in enumerate(values):
+ self.write_int16(start_address + (i * 2), value)
+
+ def write_int24(self, address: int, value: int) -> None:
+ value = value & 0xFFFFFF
+ self.write_bytes(address, [(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF])
+
+ def write_int24s(self, start_address: int, values: List[int]) -> None:
+ for i, value in enumerate(values):
+ self.write_int24(start_address + (i * 3), value)
+
+ def write_int32(self, address, value: int) -> None:
+ value = value & 0xFFFFFFFF
+ self.write_bytes(address, [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF])
+
+ def write_int32s(self, start_address: int, values: list) -> None:
+ for i, value in enumerate(values):
+ self.write_int32(start_address + (i * 4), value)
+
+ def get_bytes(self) -> bytes:
+ return bytes(self.file)
+
+
+class CV64PatchExtensions(APPatchExtension):
+ game = "Castlevania 64"
+
+ @staticmethod
+ def apply_patches(caller: APProcedurePatch, rom: bytes, options_file: str) -> bytes:
+ rom_data = RomData(rom)
+ options = json.loads(caller.get_file(options_file).decode("utf-8"))
+
+ # NOP out the CRC BNEs
+ rom_data.write_int32(0x66C, 0x00000000)
+ rom_data.write_int32(0x678, 0x00000000)
+
+ # Always offer Hard Mode on file creation
+ rom_data.write_int32(0xC8810, 0x240A0100) # ADDIU T2, R0, 0x0100
+
+ # Disable the Easy Mode cutoff point at Castle Center's elevator.
+ rom_data.write_int32(0xD9E18, 0x240D0000) # ADDIU T5, R0, 0x0000
+
+ # Disable the Forest, Castle Wall, and Villa intro cutscenes and make it possible to change the starting level
+ rom_data.write_byte(0xB73308, 0x00)
+ rom_data.write_byte(0xB7331A, 0x40)
+ rom_data.write_byte(0xB7332B, 0x4C)
+ rom_data.write_byte(0xB6302B, 0x00)
+ rom_data.write_byte(0x109F8F, 0x00)
+
+ # Prevent Forest end cutscene flag from setting so it can be triggered infinitely.
+ rom_data.write_byte(0xEEA51, 0x01)
+
+ # Hack to make the Forest, CW and Villa intro cutscenes play at the start of their levels no matter what map
+ # came before them.
+ rom_data.write_int32(0x97244, 0x803FDD60)
+ rom_data.write_int32s(0xBFDD60, patches.forest_cw_villa_intro_cs_player)
+
+ # Make changing the map ID to 0xFF reset the map. Helpful to work around a bug wherein the camera gets stuck
+ # when entering a loading zone that doesn't change the map.
+ rom_data.write_int32s(0x197B0, [0x0C0FF7E6, # JAL 0x803FDF98
+ 0x24840008]) # ADDIU A0, A0, 0x0008
+ rom_data.write_int32s(0xBFDF98, patches.map_id_refresher)
+
+ # Enable swapping characters when loading into a map by holding L.
+ rom_data.write_int32(0x97294, 0x803FDFC4)
+ rom_data.write_int32(0x19710, 0x080FF80E) # J 0x803FE038
+ rom_data.write_int32s(0xBFDFC4, patches.character_changer)
+
+ # Villa coffin time-of-day hack
+ rom_data.write_byte(0xD9D83, 0x74)
+ rom_data.write_int32(0xD9D84, 0x080FF14D) # J 0x803FC534
+ rom_data.write_int32s(0xBFC534, patches.coffin_time_checker)
+
+ # Fix both Castle Center elevator bridges for both characters unless enabling only one character's stages.
+ # At which point one bridge will be always broken and one always repaired instead.
+ if options["character_stages"] == CharacterStages.option_reinhardt_only:
+ rom_data.write_int32(0x6CEAA0, 0x240B0000) # ADDIU T3, R0, 0x0000
+ elif options["character_stages"] == CharacterStages.option_carrie_only:
+ rom_data.write_int32(0x6CEAA0, 0x240B0001) # ADDIU T3, R0, 0x0001
+ else:
+ rom_data.write_int32(0x6CEAA0, 0x240B0001) # ADDIU T3, R0, 0x0001
+ rom_data.write_int32(0x6CEAA4, 0x240D0001) # ADDIU T5, R0, 0x0001
+
+ # Were-bull arena flag hack
+ rom_data.write_int32(0x6E38F0, 0x0C0FF157) # JAL 0x803FC55C
+ rom_data.write_int32s(0xBFC55C, patches.werebull_flag_unsetter)
+ rom_data.write_int32(0xA949C, 0x0C0FF380) # JAL 0x803FCE00
+ rom_data.write_int32s(0xBFCE00, patches.werebull_flag_pickup_setter)
+
+ # Enable being able to carry multiple Special jewels, Nitros, and Mandragoras simultaneously
+ rom_data.write_int32(0xBF1F4, 0x3C038039) # LUI V1, 0x8039
+ # Special1
+ rom_data.write_int32(0xBF210, 0x80659C4B) # LB A1, 0x9C4B (V1)
+ rom_data.write_int32(0xBF214, 0x24A50001) # ADDIU A1, A1, 0x0001
+ rom_data.write_int32(0xBF21C, 0xA0659C4B) # SB A1, 0x9C4B (V1)
+ # Special2
+ rom_data.write_int32(0xBF230, 0x80659C4C) # LB A1, 0x9C4C (V1)
+ rom_data.write_int32(0xBF234, 0x24A50001) # ADDIU A1, A1, 0x0001
+ rom_data.write_int32(0xbf23C, 0xA0659C4C) # SB A1, 0x9C4C (V1)
+ # Magical Nitro
+ rom_data.write_int32(0xBF360, 0x10000004) # B 0x8013C184
+ rom_data.write_int32(0xBF378, 0x25E50001) # ADDIU A1, T7, 0x0001
+ rom_data.write_int32(0xBF37C, 0x10000003) # B 0x8013C19C
+ # Mandragora
+ rom_data.write_int32(0xBF3A8, 0x10000004) # B 0x8013C1CC
+ rom_data.write_int32(0xBF3C0, 0x25050001) # ADDIU A1, T0, 0x0001
+ rom_data.write_int32(0xBF3C4, 0x10000003) # B 0x8013C1E4
+
+ # Give PowerUps their Legacy of Darkness behavior when attempting to pick up more than two
+ rom_data.write_int16(0xA9624, 0x1000)
+ rom_data.write_int32(0xA9730, 0x24090000) # ADDIU T1, R0, 0x0000
+ rom_data.write_int32(0xBF2FC, 0x080FF16D) # J 0x803FC5B4
+ rom_data.write_int32(0xBF300, 0x00000000) # NOP
+ rom_data.write_int32s(0xBFC5B4, patches.give_powerup_stopper)
+
+ # Rename the Wooden Stake and Rose to "You are a FOOL!"
+ rom_data.write_bytes(0xEFE34,
+ bytearray([0xFF, 0xFF, 0xA2, 0x0B]) + cv64_string_to_bytearray("You are a FOOL!",
+ append_end=False))
+ # Capitalize the "k" in "Archives key" to be consistent with...literally every other key name!
+ rom_data.write_byte(0xEFF21, 0x2D)
+
+ # Skip the "There is a white jewel" text so checking one saves the game instantly.
+ rom_data.write_int32s(0xEFC72, [0x00020002 for _ in range(37)])
+ rom_data.write_int32(0xA8FC0, 0x24020001) # ADDIU V0, R0, 0x0001
+ # Skip the yes/no prompts when activating things.
+ rom_data.write_int32s(0xBFDACC, patches.map_text_redirector)
+ rom_data.write_int32(0xA9084, 0x24020001) # ADDIU V0, R0, 0x0001
+ rom_data.write_int32(0xBEBE8, 0x0C0FF6B4) # JAL 0x803FDAD0
+ # Skip Vincent and Heinrich's mandatory-for-a-check dialogue
+ rom_data.write_int32(0xBED9C, 0x0C0FF6DA) # JAL 0x803FDB68
+ # Skip the long yes/no prompt in the CC planetarium to set the pieces.
+ rom_data.write_int32(0xB5C5DF, 0x24030001) # ADDIU V1, R0, 0x0001
+ # Skip the yes/no prompt to activate the CC elevator.
+ rom_data.write_int32(0xB5E3FB, 0x24020001) # ADDIU V0, R0, 0x0001
+ # Skip the yes/no prompts to set Nitro/Mandragora at both walls.
+ rom_data.write_int32(0xB5DF3E, 0x24030001) # ADDIU V1, R0, 0x0001
+
+ # Custom message if you try checking the downstairs CC crack before removing the seal.
+ rom_data.write_bytes(0xBFDBAC, cv64_string_to_bytearray("The Furious Nerd Curse\n"
+ "prevents you from setting\n"
+ "anything until the seal\n"
+ "is removed!", True))
+
+ rom_data.write_int32s(0xBFDD20, patches.special_descriptions_redirector)
+
+ # Change the Stage Select menu options
+ rom_data.write_int32s(0xADF64, patches.warp_menu_rewrite)
+ rom_data.write_int32s(0x10E0C8, patches.warp_pointer_table)
+
+ # Play the "teleportation" sound effect when teleporting
+ rom_data.write_int32s(0xAE088, [0x08004FAB, # J 0x80013EAC
+ 0x2404019E]) # ADDIU A0, R0, 0x019E
+
+ # Lizard-man save proofing
+ rom_data.write_int32(0xA99AC, 0x080FF0B8) # J 0x803FC2E0
+ rom_data.write_int32s(0xBFC2E0, patches.boss_save_stopper)
+
+ # Disable or guarantee vampire Vincent's fight
+ if options["vincent_fight_condition"] == VincentFightCondition.option_never:
+ rom_data.write_int32(0xAACC0, 0x24010001) # ADDIU AT, R0, 0x0001
+ rom_data.write_int32(0xAACE0, 0x24180000) # ADDIU T8, R0, 0x0000
+ elif options["vincent_fight_condition"] == VincentFightCondition.option_always:
+ rom_data.write_int32(0xAACE0, 0x24180010) # ADDIU T8, R0, 0x0010
+ else:
+ rom_data.write_int32(0xAACE0, 0x24180000) # ADDIU T8, R0, 0x0000
+
+ # Disable or guarantee Renon's fight
+ rom_data.write_int32(0xAACB4, 0x080FF1A4) # J 0x803FC690
+ if options["renon_fight_condition"] == RenonFightCondition.option_never:
+ rom_data.write_byte(0xB804F0, 0x00)
+ rom_data.write_byte(0xB80632, 0x00)
+ rom_data.write_byte(0xB807E3, 0x00)
+ rom_data.write_byte(0xB80988, 0xB8)
+ rom_data.write_byte(0xB816BD, 0xB8)
+ rom_data.write_byte(0xB817CF, 0x00)
+ rom_data.write_int32s(0xBFC690, patches.renon_cutscene_checker_jr)
+ elif options["renon_fight_condition"] == RenonFightCondition.option_always:
+ rom_data.write_byte(0xB804F0, 0x0C)
+ rom_data.write_byte(0xB80632, 0x0C)
+ rom_data.write_byte(0xB807E3, 0x0C)
+ rom_data.write_byte(0xB80988, 0xC4)
+ rom_data.write_byte(0xB816BD, 0xC4)
+ rom_data.write_byte(0xB817CF, 0x0C)
+ rom_data.write_int32s(0xBFC690, patches.renon_cutscene_checker_jr)
+ else:
+ rom_data.write_int32s(0xBFC690, patches.renon_cutscene_checker)
+
+ # NOP the Easy Mode check when buying a thing from Renon, so his fight can be triggered even on this mode.
+ rom_data.write_int32(0xBD8B4, 0x00000000)
+
+ # Disable or guarantee the Bad Ending
+ if options["bad_ending_condition"] == BadEndingCondition.option_never:
+ rom_data.write_int32(0xAEE5C6, 0x3C0A0000) # LUI T2, 0x0000
+ elif options["bad_ending_condition"] == BadEndingCondition.option_always:
+ rom_data.write_int32(0xAEE5C6, 0x3C0A0040) # LUI T2, 0x0040
+
+ # Play Castle Keep's song if teleporting in front of Dracula's door outside the escape sequence
+ rom_data.write_int32(0x6E937C, 0x080FF12E) # J 0x803FC4B8
+ rom_data.write_int32s(0xBFC4B8, patches.ck_door_music_player)
+
+ # Increase item capacity to 100 if "Increase Item Limit" is turned on
+ if options["increase_item_limit"]:
+ rom_data.write_byte(0xBF30B, 0x63) # Most items
+ rom_data.write_byte(0xBF3F7, 0x63) # Sun/Moon cards
+ rom_data.write_byte(0xBF353, 0x64) # Keys (increase regardless)
+
+ # Change the item healing values if "Nerf Healing" is turned on
+ if options["nerf_healing_items"]:
+ rom_data.write_byte(0xB56371, 0x50) # Healing kit (100 -> 80)
+ rom_data.write_byte(0xB56374, 0x32) # Roast beef ( 80 -> 50)
+ rom_data.write_byte(0xB56377, 0x19) # Roast chicken ( 50 -> 25)
+
+ # Disable loading zone healing if turned off
+ if not options["loading_zone_heals"]:
+ rom_data.write_byte(0xD99A5, 0x00) # Skip all loading zone checks
+ rom_data.write_byte(0xA9DFFB,
+ 0x40) # Disable free heal from King Skeleton by reading the unused magic meter value
+
+ # Disable spinning on the Special1 and 2 pickup models so colorblind people can more easily identify them
+ rom_data.write_byte(0xEE4F5, 0x00) # Special1
+ rom_data.write_byte(0xEE505, 0x00) # Special2
+ # Make the Special2 the same size as a Red jewel(L) to further distinguish them
+ rom_data.write_int32(0xEE4FC, 0x3FA66666)
+
+ # Prevent the vanilla Magical Nitro transport's "can explode" flag from setting
+ rom_data.write_int32(0xB5D7AA, 0x00000000) # NOP
+
+ # Ensure the vampire Nitro check will always pass, so they'll never not spawn and crash the Villa cutscenes
+ rom_data.write_byte(0xA6253D, 0x03)
+
+ # Enable the Game Over's "Continue" menu starting the cursor on whichever checkpoint is most recent
+ rom_data.write_int32(0xB4DDC, 0x0C060D58) # JAL 0x80183560
+ rom_data.write_int32s(0x106750, patches.continue_cursor_start_checker)
+ rom_data.write_int32(0x1C444, 0x080FF08A) # J 0x803FC228
+ rom_data.write_int32(0x1C2A0, 0x080FF08A) # J 0x803FC228
+ rom_data.write_int32s(0xBFC228, patches.savepoint_cursor_updater)
+ rom_data.write_int32(0x1C2D0, 0x080FF094) # J 0x803FC250
+ rom_data.write_int32s(0xBFC250, patches.stage_start_cursor_updater)
+ rom_data.write_byte(0xB585C8, 0xFF)
+
+ # Make the Special1 and 2 play sounds when you reach milestones with them.
+ rom_data.write_int32s(0xBFDA50, patches.special_sound_notifs)
+ rom_data.write_int32(0xBF240, 0x080FF694) # J 0x803FDA50
+ rom_data.write_int32(0xBF220, 0x080FF69E) # J 0x803FDA78
+
+ # Add data for White Jewel #22 (the new Duel Tower savepoint) at the end of the White Jewel ID data list
+ rom_data.write_int16s(0x104AC8, [0x0000, 0x0006,
+ 0x0013, 0x0015])
+
+ # Take the contract in Waterway off of its 00400000 bitflag.
+ rom_data.write_byte(0x87E3DA, 0x00)
+
+ # Spawn coordinates list extension
+ rom_data.write_int32(0xD5BF4, 0x080FF103) # J 0x803FC40C
+ rom_data.write_int32s(0xBFC40C, patches.spawn_coordinates_extension)
+ rom_data.write_int32s(0x108A5E, patches.waterway_end_coordinates)
+
+ # Fix a vanilla issue wherein saving in a character-exclusive stage as the other character would incorrectly
+ # display the name of that character's equivalent stage on the save file instead of the one they're actually in.
+ rom_data.write_byte(0xC9FE3, 0xD4)
+ rom_data.write_byte(0xCA055, 0x08)
+ rom_data.write_byte(0xCA066, 0x40)
+ rom_data.write_int32(0xCA068, 0x860C17D0) # LH T4, 0x17D0 (S0)
+ rom_data.write_byte(0xCA06D, 0x08)
+ rom_data.write_byte(0x104A31, 0x01)
+ rom_data.write_byte(0x104A39, 0x01)
+ rom_data.write_byte(0x104A89, 0x01)
+ rom_data.write_byte(0x104A91, 0x01)
+ rom_data.write_byte(0x104A99, 0x01)
+ rom_data.write_byte(0x104AA1, 0x01)
+
+ # CC top elevator switch check
+ rom_data.write_int32(0x6CF0A0, 0x0C0FF0B0) # JAL 0x803FC2C0
+ rom_data.write_int32s(0xBFC2C0, patches.elevator_flag_checker)
+
+ # Disable time restrictions
+ if options["disable_time_restrictions"]:
+ # Fountain
+ rom_data.write_int32(0x6C2340, 0x00000000) # NOP
+ rom_data.write_int32(0x6C257C, 0x10000023) # B [forward 0x23]
+ # Rosa
+ rom_data.write_byte(0xEEAAB, 0x00)
+ rom_data.write_byte(0xEEAAD, 0x18)
+ # Moon doors
+ rom_data.write_int32(0xDC3E0, 0x00000000) # NOP
+ rom_data.write_int32(0xDC3E8, 0x00000000) # NOP
+ # Sun doors
+ rom_data.write_int32(0xDC410, 0x00000000) # NOP
+ rom_data.write_int32(0xDC418, 0x00000000) # NOP
+
+ # Custom data-loading code
+ rom_data.write_int32(0x6B5028, 0x08060D70) # J 0x801835D0
+ rom_data.write_int32s(0x1067B0, patches.custom_code_loader)
+
+ # Custom remote item rewarding and DeathLink receiving code
+ rom_data.write_int32(0x19B98, 0x080FF000) # J 0x803FC000
+ rom_data.write_int32s(0xBFC000, patches.remote_item_giver)
+ rom_data.write_int32s(0xBFE190, patches.subweapon_surface_checker)
+
+ # Make received DeathLinks blow you to smithereens instead of kill you normally.
+ if options["death_link"] == DeathLink.option_explosive:
+ rom_data.write_int32(0x27A70, 0x10000008) # B [forward 0x08]
+ rom_data.write_int32s(0xBFC0D0, patches.deathlink_nitro_edition)
+
+ # Set the DeathLink ROM flag if it's on at all.
+ if options["death_link"] != DeathLink.option_off:
+ rom_data.write_byte(0xBFBFDE, 0x01)
+
+ # DeathLink counter decrementer code
+ rom_data.write_int32(0x1C340, 0x080FF8F0) # J 0x803FE3C0
+ rom_data.write_int32s(0xBFE3C0, patches.deathlink_counter_decrementer)
+ rom_data.write_int32(0x25B6C, 0x080FFA5E) # J 0x803FE978
+ rom_data.write_int32s(0xBFE978, patches.launch_fall_killer)
+
+ # Death flag un-setter on "Beginning of stage" state overwrite code
+ rom_data.write_int32(0x1C2B0, 0x080FF047) # J 0x803FC11C
+ rom_data.write_int32s(0xBFC11C, patches.death_flag_unsetter)
+
+ # Warp menu-opening code
+ rom_data.write_int32(0xB9BA8, 0x080FF099) # J 0x803FC264
+ rom_data.write_int32s(0xBFC264, patches.warp_menu_opener)
+
+ # NPC item textbox hack
+ rom_data.write_int32(0xBF1DC, 0x080FF904) # J 0x803FE410
+ rom_data.write_int32(0xBF1E0, 0x27BDFFE0) # ADDIU SP, SP, -0x20
+ rom_data.write_int32s(0xBFE410, patches.npc_item_hack)
+
+ # Sub-weapon check function hook
+ rom_data.write_int32(0xBF32C, 0x00000000) # NOP
+ rom_data.write_int32(0xBF330, 0x080FF05E) # J 0x803FC178
+ rom_data.write_int32s(0xBFC178, patches.give_subweapon_stopper)
+
+ # Warp menu Special1 restriction
+ rom_data.write_int32(0xADD68, 0x0C04AB12) # JAL 0x8012AC48
+ rom_data.write_int32s(0xADE28, patches.stage_select_overwrite)
+ rom_data.write_byte(0xADE47, options["s1s_per_warp"])
+
+ # Dracula's door text pointer hijack
+ rom_data.write_int32(0xD69F0, 0x080FF141) # J 0x803FC504
+ rom_data.write_int32s(0xBFC504, patches.dracula_door_text_redirector)
+
+ # Dracula's chamber condition
+ rom_data.write_int32(0xE2FDC, 0x0804AB25) # J 0x8012AC78
+ rom_data.write_int32s(0xADE84, patches.special_goal_checker)
+ rom_data.write_bytes(0xBFCC48,
+ [0xA0, 0x00, 0xFF, 0xFF, 0xA0, 0x01, 0xFF, 0xFF, 0xA0, 0x02, 0xFF, 0xFF, 0xA0, 0x03, 0xFF,
+ 0xFF, 0xA0, 0x04, 0xFF, 0xFF, 0xA0, 0x05, 0xFF, 0xFF, 0xA0, 0x06, 0xFF, 0xFF, 0xA0, 0x07,
+ 0xFF, 0xFF, 0xA0, 0x08, 0xFF, 0xFF, 0xA0, 0x09])
+ if options["draculas_condition"] == DraculasCondition.option_crystal:
+ rom_data.write_int32(0x6C8A54, 0x0C0FF0C1) # JAL 0x803FC304
+ rom_data.write_int32s(0xBFC304, patches.crystal_special2_giver)
+ rom_data.write_bytes(0xBFCC6E, cv64_string_to_bytearray(f"It won't budge!\n"
+ f"You'll need the power\n"
+ f"of the basement crystal\n"
+ f"to undo the seal.", True))
+ special2_name = "Crystal "
+ special2_text = "The crystal is on!\n" \
+ "Time to teach the old man\n" \
+ "a lesson!"
+ elif options["draculas_condition"] == DraculasCondition.option_bosses:
+ rom_data.write_int32(0xBBD50, 0x080FF18C) # J 0x803FC630
+ rom_data.write_int32s(0xBFC630, patches.boss_special2_giver)
+ rom_data.write_int32s(0xBFC55C, patches.werebull_flag_unsetter_special2_electric_boogaloo)
+ rom_data.write_bytes(0xBFCC6E, cv64_string_to_bytearray(f"It won't budge!\n"
+ f"You'll need to defeat\n"
+ f"{options['required_s2s']} powerful monsters\n"
+ f"to undo the seal.", True))
+ special2_name = "Trophy "
+ special2_text = f"Proof you killed a powerful\n" \
+ f"Night Creature. Earn {options['required_s2s']}/{options['total_s2s']}\n" \
+ f"to battle Dracula."
+ elif options["draculas_condition"] == DraculasCondition.option_specials:
+ special2_name = "Special2"
+ rom_data.write_bytes(0xBFCC6E, cv64_string_to_bytearray(f"It won't budge!\n"
+ f"You'll need to find\n"
+ f"{options['required_s2s']} Special2 jewels\n"
+ f"to undo the seal.", True))
+ special2_text = f"Need {options['required_s2s']}/{options['total_s2s']} to kill Dracula.\n" \
+ f"Looking closely, you see...\n" \
+ f"a piece of him within?"
+ else:
+ rom_data.write_byte(0xADE8F, 0x00)
+ special2_name = "Special2"
+ special2_text = "If you're reading this,\n" \
+ "how did you get a Special2!?"
+ rom_data.write_byte(0xADE8F, options["required_s2s"])
+ # Change the Special2 name depending on the setting.
+ rom_data.write_bytes(0xEFD4E, cv64_string_to_bytearray(special2_name))
+ # Change the Special1 and 2 menu descriptions to tell you how many you need to unlock a warp and fight Dracula
+ # respectively.
+ special_text_bytes = cv64_string_to_bytearray(f"{options['s1s_per_warp']} per warp unlock.\n"
+ f"{options['total_special1s']} exist in total.\n"
+ f"Z + R + START to warp.") + cv64_string_to_bytearray(
+ special2_text)
+ rom_data.write_bytes(0xBFE53C, special_text_bytes)
+
+ # On-the-fly overlay modifier
+ rom_data.write_int32s(0xBFC338, patches.double_component_checker)
+ rom_data.write_int32s(0xBFC3D4, patches.downstairs_seal_checker)
+ rom_data.write_int32s(0xBFE074, patches.mandragora_with_nitro_setter)
+ rom_data.write_int32s(0xBFC700, patches.overlay_modifiers)
+
+ # On-the-fly actor data modifier hook
+ rom_data.write_int32(0xEAB04, 0x080FF21E) # J 0x803FC878
+ rom_data.write_int32s(0xBFC870, patches.map_data_modifiers)
+
+ # Fix to make flags apply to freestanding invisible items properly
+ rom_data.write_int32(0xA84F8, 0x90CC0039) # LBU T4, 0x0039 (A2)
+
+ # Fix locked doors to check the key counters instead of their vanilla key locations' bitflags
+ # Pickup flag check modifications:
+ rom_data.write_int32(0x10B2D8, 0x00000002) # Left Tower Door
+ rom_data.write_int32(0x10B2F0, 0x00000003) # Storeroom Door
+ rom_data.write_int32(0x10B2FC, 0x00000001) # Archives Door
+ rom_data.write_int32(0x10B314, 0x00000004) # Maze Gate
+ rom_data.write_int32(0x10B350, 0x00000005) # Copper Door
+ rom_data.write_int32(0x10B3A4, 0x00000006) # Torture Chamber Door
+ rom_data.write_int32(0x10B3B0, 0x00000007) # ToE Gate
+ rom_data.write_int32(0x10B3BC, 0x00000008) # Science Door1
+ rom_data.write_int32(0x10B3C8, 0x00000009) # Science Door2
+ rom_data.write_int32(0x10B3D4, 0x0000000A) # Science Door3
+ rom_data.write_int32(0x6F0094, 0x0000000B) # CT Door 1
+ rom_data.write_int32(0x6F00A4, 0x0000000C) # CT Door 2
+ rom_data.write_int32(0x6F00B4, 0x0000000D) # CT Door 3
+ # Item counter decrement check modifications:
+ rom_data.write_int32(0xEDA84, 0x00000001) # Archives Door
+ rom_data.write_int32(0xEDA8C, 0x00000002) # Left Tower Door
+ rom_data.write_int32(0xEDA94, 0x00000003) # Storeroom Door
+ rom_data.write_int32(0xEDA9C, 0x00000004) # Maze Gate
+ rom_data.write_int32(0xEDAA4, 0x00000005) # Copper Door
+ rom_data.write_int32(0xEDAAC, 0x00000006) # Torture Chamber Door
+ rom_data.write_int32(0xEDAB4, 0x00000007) # ToE Gate
+ rom_data.write_int32(0xEDABC, 0x00000008) # Science Door1
+ rom_data.write_int32(0xEDAC4, 0x00000009) # Science Door2
+ rom_data.write_int32(0xEDACC, 0x0000000A) # Science Door3
+ rom_data.write_int32(0xEDAD4, 0x0000000B) # CT Door 1
+ rom_data.write_int32(0xEDADC, 0x0000000C) # CT Door 2
+ rom_data.write_int32(0xEDAE4, 0x0000000D) # CT Door 3
+
+ # Fix ToE gate's "unlocked" flag in the locked door flags table
+ rom_data.write_int16(0x10B3B6, 0x0001)
+
+ rom_data.write_int32(0x10AB2C, 0x8015FBD4) # Maze Gates' check code pointer adjustments
+ rom_data.write_int32(0x10AB40, 0x8015FBD4)
+ rom_data.write_int32s(0x10AB50, [0x0D0C0000,
+ 0x8015FBD4])
+ rom_data.write_int32s(0x10AB64, [0x0D0C0000,
+ 0x8015FBD4])
+ rom_data.write_int32s(0xE2E14, patches.normal_door_hook)
+ rom_data.write_int32s(0xBFC5D0, patches.normal_door_code)
+ rom_data.write_int32s(0x6EF298, patches.ct_door_hook)
+ rom_data.write_int32s(0xBFC608, patches.ct_door_code)
+ # Fix key counter not decrementing if 2 or above
+ rom_data.write_int32(0xAA0E0, 0x24020000) # ADDIU V0, R0, 0x0000
+
+ # Make the Easy-only candle drops in Room of Clocks appear on any difficulty
+ rom_data.write_byte(0x9B518F, 0x01)
+
+ # Slightly move some once-invisible freestanding items to be more visible
+ if options["invisible_items"] == InvisibleItems.option_reveal_all:
+ rom_data.write_byte(0x7C7F95, 0xEF) # Forest dirge maiden statue
+ rom_data.write_byte(0x7C7FA8, 0xAB) # Forest werewolf statue
+ rom_data.write_byte(0x8099C4, 0x8C) # Villa courtyard tombstone
+ rom_data.write_byte(0x83A626, 0xC2) # Villa living room painting
+ # rom_data.write_byte(0x83A62F, 0x64) # Villa Mary's room table
+ rom_data.write_byte(0xBFCB97, 0xF5) # CC torture instrument rack
+ rom_data.write_byte(0x8C44D5, 0x22) # CC red carpet hallway knight
+ rom_data.write_byte(0x8DF57C, 0xF1) # CC cracked wall hallway flamethrower
+ rom_data.write_byte(0x90FCD6, 0xA5) # CC nitro hallway flamethrower
+ rom_data.write_byte(0x90FB9F, 0x9A) # CC invention room round machine
+ rom_data.write_byte(0x90FBAF, 0x03) # CC invention room giant famicart
+ rom_data.write_byte(0x90FE54, 0x97) # CC staircase knight (x)
+ rom_data.write_byte(0x90FE58, 0xFB) # CC staircase knight (z)
+
+ # Change the bitflag on the item in upper coffin in Forest final switch gate tomb to one that's not used by
+ # something else.
+ rom_data.write_int32(0x10C77C, 0x00000002)
+
+ # Make the torch directly behind Dracula's chamber that normally doesn't set a flag set bitflag 0x08 in
+ # 0x80389BFA.
+ rom_data.write_byte(0x10CE9F, 0x01)
+
+ # Change the CC post-Behemoth boss depending on the option for Post-Behemoth Boss
+ if options["post_behemoth_boss"] == PostBehemothBoss.option_inverted:
+ rom_data.write_byte(0xEEDAD, 0x02)
+ rom_data.write_byte(0xEEDD9, 0x01)
+ elif options["post_behemoth_boss"] == PostBehemothBoss.option_always_rosa:
+ rom_data.write_byte(0xEEDAD, 0x00)
+ rom_data.write_byte(0xEEDD9, 0x03)
+ # Put both on the same flag so changing character won't trigger a rematch with the same boss.
+ rom_data.write_byte(0xEED8B, 0x40)
+ elif options["post_behemoth_boss"] == PostBehemothBoss.option_always_camilla:
+ rom_data.write_byte(0xEEDAD, 0x03)
+ rom_data.write_byte(0xEEDD9, 0x00)
+ rom_data.write_byte(0xEED8B, 0x40)
+
+ # Change the RoC boss depending on the option for Room of Clocks Boss
+ if options["room_of_clocks_boss"] == RoomOfClocksBoss.option_inverted:
+ rom_data.write_byte(0x109FB3, 0x56)
+ rom_data.write_byte(0x109FBF, 0x44)
+ rom_data.write_byte(0xD9D44, 0x14)
+ rom_data.write_byte(0xD9D4C, 0x14)
+ elif options["room_of_clocks_boss"] == RoomOfClocksBoss.option_always_death:
+ rom_data.write_byte(0x109FBF, 0x44)
+ rom_data.write_byte(0xD9D45, 0x00)
+ # Put both on the same flag so changing character won't trigger a rematch with the same boss.
+ rom_data.write_byte(0x109FB7, 0x90)
+ rom_data.write_byte(0x109FC3, 0x90)
+ elif options["room_of_clocks_boss"] == RoomOfClocksBoss.option_always_actrise:
+ rom_data.write_byte(0x109FB3, 0x56)
+ rom_data.write_int32(0xD9D44, 0x00000000)
+ rom_data.write_byte(0xD9D4D, 0x00)
+ rom_data.write_byte(0x109FB7, 0x90)
+ rom_data.write_byte(0x109FC3, 0x90)
+
+ # Un-nerf Actrise when playing as Reinhardt.
+ # This is likely a leftover TGS demo feature in which players could battle Actrise as Reinhardt.
+ rom_data.write_int32(0xB318B4, 0x240E0001) # ADDIU T6, R0, 0x0001
+
+ # Tunnel gondola skip
+ if options["skip_gondolas"]:
+ rom_data.write_int32(0x6C5F58, 0x080FF7D0) # J 0x803FDF40
+ rom_data.write_int32s(0xBFDF40, patches.gondola_skipper)
+ # New gondola transfer point candle coordinates
+ rom_data.write_byte(0xBFC9A3, 0x04)
+ rom_data.write_bytes(0x86D824, [0x27, 0x01, 0x10, 0xF7, 0xA0])
+
+ # Waterway brick platforms skip
+ if options["skip_waterway_blocks"]:
+ rom_data.write_int32(0x6C7E2C, 0x00000000) # NOP
+
+ # Ambience silencing fix
+ rom_data.write_int32(0xD9270, 0x080FF840) # J 0x803FE100
+ rom_data.write_int32s(0xBFE100, patches.ambience_silencer)
+ # Fix for the door sliding sound playing infinitely if leaving the fan meeting room before the door closes
+ # entirely. Hooking this in the ambience silencer code does nothing for some reason.
+ rom_data.write_int32s(0xAE10C, [0x08004FAB, # J 0x80013EAC
+ 0x3404829B]) # ORI A0, R0, 0x829B
+ rom_data.write_int32s(0xD9E8C, [0x08004FAB, # J 0x80013EAC
+ 0x3404829B]) # ORI A0, R0, 0x829B
+ # Fan meeting room ambience fix
+ rom_data.write_int32(0x109964, 0x803FE13C)
+
+ # Make the Villa coffin cutscene skippable
+ rom_data.write_int32(0xAA530, 0x080FF880) # J 0x803FE200
+ rom_data.write_int32s(0xBFE200, patches.coffin_cutscene_skipper)
+
+ # Increase shimmy speed
+ if options["increase_shimmy_speed"]:
+ rom_data.write_byte(0xA4241, 0x5A)
+
+ # Disable landing fall damage
+ if options["fall_guard"]:
+ rom_data.write_byte(0x27B23, 0x00)
+
+ # Enable the unused film reel effect on all cutscenes
+ if options["cinematic_experience"]:
+ rom_data.write_int32(0xAA33C, 0x240A0001) # ADDIU T2, R0, 0x0001
+ rom_data.write_byte(0xAA34B, 0x0C)
+ rom_data.write_int32(0xAA4C4, 0x24090001) # ADDIU T1, R0, 0x0001
+
+ # Permanent PowerUp stuff
+ if options["permanent_powerups"]:
+ # Make receiving PowerUps increase the unused menu PowerUp counter instead of the one outside the save
+ # struct.
+ rom_data.write_int32(0xBF2EC, 0x806B619B) # LB T3, 0x619B (V1)
+ rom_data.write_int32(0xBFC5BC, 0xA06C619B) # SB T4, 0x619B (V1)
+ # Make Reinhardt's whip check the menu PowerUp counter
+ rom_data.write_int32(0x69FA08, 0x80CC619B) # LB T4, 0x619B (A2)
+ rom_data.write_int32(0x69FBFC, 0x80C3619B) # LB V1, 0x619B (A2)
+ rom_data.write_int32(0x69FFE0, 0x818C9C53) # LB T4, 0x9C53 (T4)
+ # Make Carrie's orb check the menu PowerUp counter
+ rom_data.write_int32(0x6AC86C, 0x8105619B) # LB A1, 0x619B (T0)
+ rom_data.write_int32(0x6AC950, 0x8105619B) # LB A1, 0x619B (T0)
+ rom_data.write_int32(0x6AC99C, 0x810E619B) # LB T6, 0x619B (T0)
+ rom_data.write_int32(0x5AFA0, 0x80639C53) # LB V1, 0x9C53 (V1)
+ rom_data.write_int32(0x5B0A0, 0x81089C53) # LB T0, 0x9C53 (T0)
+ rom_data.write_byte(0x391C7, 0x00) # Prevent PowerUps from dropping from regular enemies
+ rom_data.write_byte(0xEDEDF, 0x03) # Make any vanishing PowerUps that do show up L jewels instead
+ # Rename the PowerUp to "PermaUp"
+ rom_data.write_bytes(0xEFDEE, cv64_string_to_bytearray("PermaUp"))
+ # Replace the PowerUp in the Forest Special1 Bridge 3HB rock with an L jewel if 3HBs aren't randomized
+ if not options["multi_hit_breakables"]:
+ rom_data.write_byte(0x10C7A1, 0x03)
+ # Change the appearance of the Pot-Pourri to that of a larger PowerUp regardless of the above setting, so other
+ # game PermaUps are distinguishable.
+ rom_data.write_int32s(0xEE558, [0x06005F08, 0x3FB00000, 0xFFFFFF00])
+
+ # Write the associated code for the randomized (or disabled) music list.
+ if options["background_music"]:
+ rom_data.write_int32(0x14588, 0x08060D60) # J 0x80183580
+ rom_data.write_int32(0x14590, 0x00000000) # NOP
+ rom_data.write_int32s(0x106770, patches.music_modifier)
+ rom_data.write_int32(0x15780, 0x0C0FF36E) # JAL 0x803FCDB8
+ rom_data.write_int32s(0xBFCDB8, patches.music_comparer_modifier)
+
+ # Enable storing item flags anywhere and changing the item model/visibility on any item instance.
+ rom_data.write_int32s(0xA857C, [0x080FF38F, # J 0x803FCE3C
+ 0x94D90038]) # LHU T9, 0x0038 (A2)
+ rom_data.write_int32s(0xBFCE3C, patches.item_customizer)
+ rom_data.write_int32s(0xA86A0, [0x0C0FF3AF, # JAL 0x803FCEBC
+ 0x95C40002]) # LHU A0, 0x0002 (T6)
+ rom_data.write_int32s(0xBFCEBC, patches.item_appearance_switcher)
+ rom_data.write_int32s(0xA8728, [0x0C0FF3B8, # JAL 0x803FCEE4
+ 0x01396021]) # ADDU T4, T1, T9
+ rom_data.write_int32s(0xBFCEE4, patches.item_model_visibility_switcher)
+ rom_data.write_int32s(0xA8A04, [0x0C0FF3C2, # JAL 0x803FCF08
+ 0x018B6021]) # ADDU T4, T4, T3
+ rom_data.write_int32s(0xBFCF08, patches.item_shine_visibility_switcher)
+
+ # Make Axes and Crosses in AP Locations drop to their correct height, and make items with changed appearances
+ # spin their correct speed.
+ rom_data.write_int32s(0xE649C, [0x0C0FFA03, # JAL 0x803FE80C
+ 0x956C0002]) # LHU T4, 0x0002 (T3)
+ rom_data.write_int32s(0xA8B08, [0x080FFA0C, # J 0x803FE830
+ 0x960A0038]) # LHU T2, 0x0038 (S0)
+ rom_data.write_int32s(0xE8584, [0x0C0FFA21, # JAL 0x803FE884
+ 0x95D80000]) # LHU T8, 0x0000 (T6)
+ rom_data.write_int32s(0xE7AF0, [0x0C0FFA2A, # JAL 0x803FE8A8
+ 0x958D0000]) # LHU T5, 0x0000 (T4)
+ rom_data.write_int32s(0xBFE7DC, patches.item_drop_spin_corrector)
+
+ # Disable the 3HBs checking and setting flags when breaking them and enable their individual items checking and
+ # setting flags instead.
+ if options["multi_hit_breakables"]:
+ rom_data.write_int32(0xE87F8, 0x00000000) # NOP
+ rom_data.write_int16(0xE836C, 0x1000)
+ rom_data.write_int32(0xE8B40, 0x0C0FF3CD) # JAL 0x803FCF34
+ rom_data.write_int32s(0xBFCF34, patches.three_hit_item_flags_setter)
+ # Villa foyer chandelier-specific functions (yeah, IDK why KCEK made different functions for this one)
+ rom_data.write_int32(0xE7D54, 0x00000000) # NOP
+ rom_data.write_int16(0xE7908, 0x1000)
+ rom_data.write_byte(0xE7A5C, 0x10)
+ rom_data.write_int32(0xE7F08, 0x0C0FF3DF) # JAL 0x803FCF7C
+ rom_data.write_int32s(0xBFCF7C, patches.chandelier_item_flags_setter)
+
+ # New flag values to put in each 3HB vanilla flag's spot
+ rom_data.write_int32(0x10C7C8, 0x8000FF48) # FoS dirge maiden rock
+ rom_data.write_int32(0x10C7B0, 0x0200FF48) # FoS S1 bridge rock
+ rom_data.write_int32(0x10C86C, 0x0010FF48) # CW upper rampart save nub
+ rom_data.write_int32(0x10C878, 0x4000FF49) # CW Dracula switch slab
+ rom_data.write_int32(0x10CAD8, 0x0100FF49) # Tunnel twin arrows slab
+ rom_data.write_int32(0x10CAE4, 0x0004FF49) # Tunnel lonesome bucket pit rock
+ rom_data.write_int32(0x10CB54, 0x4000FF4A) # UW poison parkour ledge
+ rom_data.write_int32(0x10CB60, 0x0080FF4A) # UW skeleton crusher ledge
+ rom_data.write_int32(0x10CBF0, 0x0008FF4A) # CC Behemoth crate
+ rom_data.write_int32(0x10CC2C, 0x2000FF4B) # CC elevator pedestal
+ rom_data.write_int32(0x10CC70, 0x0200FF4B) # CC lizard locker slab
+ rom_data.write_int32(0x10CD88, 0x0010FF4B) # ToE pre-midsavepoint platforms ledge
+ rom_data.write_int32(0x10CE6C, 0x4000FF4C) # ToSci invisible bridge crate
+ rom_data.write_int32(0x10CF20, 0x0080FF4C) # CT inverted battery slab
+ rom_data.write_int32(0x10CF2C, 0x0008FF4C) # CT inverted door slab
+ rom_data.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab
+ rom_data.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab
+ rom_data.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier
+ rom_data.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data
+
+ # Once-per-frame gameplay checks
+ rom_data.write_int32(0x6C848, 0x080FF40D) # J 0x803FD034
+ rom_data.write_int32(0xBFD058, 0x0801AEB5) # J 0x8006BAD4
+
+ # Everything related to dropping the previous sub-weapon
+ if options["drop_previous_sub_weapon"]:
+ rom_data.write_int32(0xBFD034, 0x080FF3FF) # J 0x803FCFFC
+ rom_data.write_int32(0xBFC190, 0x080FF3F2) # J 0x803FCFC8
+ rom_data.write_int32s(0xBFCFC4, patches.prev_subweapon_spawn_checker)
+ rom_data.write_int32s(0xBFCFFC, patches.prev_subweapon_fall_checker)
+ rom_data.write_int32s(0xBFD060, patches.prev_subweapon_dropper)
+
+ # Everything related to the Countdown counter
+ if options["countdown"]:
+ rom_data.write_int32(0xBFD03C, 0x080FF9DC) # J 0x803FE770
+ rom_data.write_int32(0xD5D48, 0x080FF4EC) # J 0x803FD3B0
+ rom_data.write_int32s(0xBFD3B0, patches.countdown_number_displayer)
+ rom_data.write_int32s(0xBFD6DC, patches.countdown_number_manager)
+ rom_data.write_int32s(0xBFE770, patches.countdown_demo_hider)
+ rom_data.write_int32(0xBFCE2C, 0x080FF5D2) # J 0x803FD748
+ rom_data.write_int32s(0xBB168, [0x080FF5F4, # J 0x803FD7D0
+ 0x8E020028]) # LW V0, 0x0028 (S0)
+ rom_data.write_int32s(0xBB1D0, [0x080FF5FB, # J 0x803FD7EC
+ 0x8E020028]) # LW V0, 0x0028 (S0)
+ rom_data.write_int32(0xBC4A0, 0x080FF5E6) # J 0x803FD798
+ rom_data.write_int32(0xBC4C4, 0x080FF5E6) # J 0x803FD798
+ rom_data.write_int32(0x19844, 0x080FF602) # J 0x803FD808
+ # If the option is set to "all locations", count it down no matter what the item is.
+ if options["countdown"] == Countdown.option_all_locations:
+ rom_data.write_int32s(0xBFD71C, [0x01010101, 0x01010101, 0x01010101, 0x01010101, 0x01010101, 0x01010101,
+ 0x01010101, 0x01010101, 0x01010101, 0x01010101, 0x01010101])
+ else:
+ # If it's majors, then insert this last minute check I threw together for the weird edge case of a CV64
+ # ice trap for another CV64 player taking the form of a major.
+ rom_data.write_int32s(0xBFD788, [0x080FF717, # J 0x803FDC5C
+ 0x2529FFFF]) # ADDIU T1, T1, 0xFFFF
+ rom_data.write_int32s(0xBFDC5C, patches.countdown_extra_safety_check)
+ rom_data.write_int32(0xA9ECC,
+ 0x00000000) # NOP the pointless overwrite of the item actor appearance custom value.
+
+ # Ice Trap stuff
+ rom_data.write_int32(0x697C60, 0x080FF06B) # J 0x803FC18C
+ rom_data.write_int32(0x6A5160, 0x080FF06B) # J 0x803FC18C
+ rom_data.write_int32s(0xBFC1AC, patches.ice_trap_initializer)
+ rom_data.write_int32s(0xBFE700, patches.the_deep_freezer)
+ rom_data.write_int32s(0xB2F354, [0x3739E4C0, # ORI T9, T9, 0xE4C0
+ 0x03200008, # JR T9
+ 0x00000000]) # NOP
+ rom_data.write_int32s(0xBFE4C0, patches.freeze_verifier)
+
+ # Fix for the ice chunk model staying when getting bitten by the maze garden dogs
+ rom_data.write_int32(0xA2DC48, 0x803FE9C0)
+ rom_data.write_int32s(0xBFE9C0, patches.dog_bite_ice_trap_fix)
+
+ # Initial Countdown numbers
+ rom_data.write_int32(0xAD6A8, 0x080FF60A) # J 0x803FD828
+ rom_data.write_int32s(0xBFD828, patches.new_game_extras)
+
+ # Everything related to shopsanity
+ if options["shopsanity"]:
+ rom_data.write_byte(0xBFBFDF, 0x01)
+ rom_data.write_bytes(0x103868, cv64_string_to_bytearray("Not obtained. "))
+ rom_data.write_int32s(0xBFD8D0, patches.shopsanity_stuff)
+ rom_data.write_int32(0xBD828, 0x0C0FF643) # JAL 0x803FD90C
+ rom_data.write_int32(0xBD5B8, 0x0C0FF651) # JAL 0x803FD944
+ rom_data.write_int32(0xB0610, 0x0C0FF665) # JAL 0x803FD994
+ rom_data.write_int32s(0xBD24C, [0x0C0FF677, # J 0x803FD9DC
+ 0x00000000]) # NOP
+ rom_data.write_int32(0xBD618, 0x0C0FF684) # JAL 0x803FDA10
+
+ # Panther Dash running
+ if options["panther_dash"]:
+ rom_data.write_int32(0x69C8C4, 0x0C0FF77E) # JAL 0x803FDDF8
+ rom_data.write_int32(0x6AA228, 0x0C0FF77E) # JAL 0x803FDDF8
+ rom_data.write_int32s(0x69C86C, [0x0C0FF78E, # JAL 0x803FDE38
+ 0x3C01803E]) # LUI AT, 0x803E
+ rom_data.write_int32s(0x6AA1D0, [0x0C0FF78E, # JAL 0x803FDE38
+ 0x3C01803E]) # LUI AT, 0x803E
+ rom_data.write_int32(0x69D37C, 0x0C0FF79E) # JAL 0x803FDE78
+ rom_data.write_int32(0x6AACE0, 0x0C0FF79E) # JAL 0x803FDE78
+ rom_data.write_int32s(0xBFDDF8, patches.panther_dash)
+ # Jump prevention
+ if options["panther_dash"] == PantherDash.option_jumpless:
+ rom_data.write_int32(0xBFDE2C, 0x080FF7BB) # J 0x803FDEEC
+ rom_data.write_int32(0xBFD044, 0x080FF7B1) # J 0x803FDEC4
+ rom_data.write_int32s(0x69B630, [0x0C0FF7C6, # JAL 0x803FDF18
+ 0x8CCD0000]) # LW T5, 0x0000 (A2)
+ rom_data.write_int32s(0x6A8EC0, [0x0C0FF7C6, # JAL 0x803FDF18
+ 0x8CCC0000]) # LW T4, 0x0000 (A2)
+ # Fun fact: KCEK put separate code to handle coyote time jumping
+ rom_data.write_int32s(0x69910C, [0x0C0FF7C6, # JAL 0x803FDF18
+ 0x8C4E0000]) # LW T6, 0x0000 (V0)
+ rom_data.write_int32s(0x6A6718, [0x0C0FF7C6, # JAL 0x803FDF18
+ 0x8C4E0000]) # LW T6, 0x0000 (V0)
+ rom_data.write_int32s(0xBFDEC4, patches.panther_jump_preventer)
+
+ # Everything related to Big Toss.
+ if options["big_toss"]:
+ rom_data.write_int32s(0x27E90, [0x0C0FFA38, # JAL 0x803FE8E0
+ 0xAFB80074]) # SW T8, 0x0074 (SP)
+ rom_data.write_int32(0x26F54, 0x0C0FFA4D) # JAL 0x803FE934
+ rom_data.write_int32s(0xBFE8E0, patches.big_tosser)
+
+ # Write the specified window colors
+ rom_data.write_byte(0xAEC23, options["window_color_r"] << 4)
+ rom_data.write_byte(0xAEC33, options["window_color_g"] << 4)
+ rom_data.write_byte(0xAEC47, options["window_color_b"] << 4)
+ rom_data.write_byte(0xAEC43, options["window_color_a"] << 4)
+
+ # Everything relating to loading the other game items text
+ rom_data.write_int32(0xA8D8C, 0x080FF88F) # J 0x803FE23C
+ rom_data.write_int32(0xBEA98, 0x0C0FF8B4) # JAL 0x803FE2D0
+ rom_data.write_int32(0xBEAB0, 0x0C0FF8BD) # JAL 0x803FE2F8
+ rom_data.write_int32(0xBEACC, 0x0C0FF8C5) # JAL 0x803FE314
+ rom_data.write_int32s(0xBFE23C, patches.multiworld_item_name_loader)
+ rom_data.write_bytes(0x10F188, [0x00 for _ in range(264)])
+ rom_data.write_bytes(0x10F298, [0x00 for _ in range(264)])
+
+ # When the game normally JALs to the item prepare textbox function after the player picks up an item, set the
+ # "no receiving" timer to ensure the item textbox doesn't freak out if you pick something up while there's a
+ # queue of unreceived items.
+ rom_data.write_int32(0xA8D94, 0x0C0FF9F0) # JAL 0x803FE7C0
+ rom_data.write_int32s(0xBFE7C0, [0x3C088039, # LUI T0, 0x8039
+ 0x24090020, # ADDIU T1, R0, 0x0020
+ 0x0804EDCE, # J 0x8013B738
+ 0xA1099BE0]) # SB T1, 0x9BE0 (T0)
+
+ return rom_data.get_bytes()
+
+ @staticmethod
+ def patch_ap_graphics(caller: APProcedurePatch, rom: bytes) -> bytes:
+ rom_data = RomData(rom)
+
+ # Extract the item models file, decompress it, append the AP icons, compress it back, re-insert it.
+ items_file = lzkn64.decompress_buffer(rom_data.read_bytes(0x9C5310, 0x3D28))
+ compressed_file = lzkn64.compress_buffer(items_file[0:0x69B6] + pkgutil.get_data(__name__, "data/ap_icons.bin"))
+ rom_data.write_bytes(0xBB2D88, compressed_file)
+ # Update the items' Nisitenma-Ichigo table entry to point to the new file's start and end addresses in the rom.
+ rom_data.write_int32s(0x95F04, [0x80BB2D88, 0x00BB2D88 + len(compressed_file)])
+ # Update the items' decompressed file size tables with the new file's decompressed file size.
+ rom_data.write_int16(0x95706, 0x7BF0)
+ rom_data.write_int16(0x104CCE, 0x7BF0)
+ # Update the Wooden Stake and Roses' item appearance settings table to point to the Archipelago item graphics.
+ rom_data.write_int16(0xEE5BA, 0x7B38)
+ rom_data.write_int16(0xEE5CA, 0x7280)
+ # Change the items' sizes. The progression one will be larger than the non-progression one.
+ rom_data.write_int32(0xEE5BC, 0x3FF00000)
+ rom_data.write_int32(0xEE5CC, 0x3FA00000)
+
+ return rom_data.get_bytes()
+
+
+class CV64ProcedurePatch(APProcedurePatch, APTokenMixin):
+ hash = [CV64_US_10_HASH]
+ patch_file_ending: str = ".apcv64"
+ result_file_ending: str = ".z64"
+
+ game = "Castlevania 64"
+
+ procedure = [
+ ("apply_patches", ["options.json"]),
+ ("apply_tokens", ["token_data.bin"]),
+ ("patch_ap_graphics", [])
+ ]
+
+ @classmethod
+ def get_source_data(cls) -> bytes:
+ return get_base_rom_bytes()
+
+
+def write_patch(world: "CV64World", patch: CV64ProcedurePatch, offset_data: Dict[int, bytes], shop_name_list: List[str],
+ shop_desc_list: List[List[Union[int, str, None]]], shop_colors_list: List[bytearray],
+ active_locations: Iterable[Location]) -> None:
+ active_warp_list = world.active_warp_list
+ s1s_per_warp = world.s1s_per_warp
+
+ # Write all the new item/loading zone/shop/lighting/music/etc. values.
+ for offset, data in offset_data.items():
+ patch.write_token(APTokenTypes.WRITE, offset, data)
+
+ # Write the new Stage Select menu destinations.
+ for i in range(len(active_warp_list)):
+ if i == 0:
+ patch.write_token(APTokenTypes.WRITE,
+ warp_map_offsets[i], get_stage_info(active_warp_list[i], "start map id"))
+ patch.write_token(APTokenTypes.WRITE,
+ warp_map_offsets[i] + 4, get_stage_info(active_warp_list[i], "start spawn id"))
+ else:
+ patch.write_token(APTokenTypes.WRITE,
+ warp_map_offsets[i], get_stage_info(active_warp_list[i], "mid map id"))
+ patch.write_token(APTokenTypes.WRITE,
+ warp_map_offsets[i] + 4, get_stage_info(active_warp_list[i], "mid spawn id"))
+
+ # Change the Stage Select menu's text to reflect its new purpose.
+ patch.write_token(APTokenTypes.WRITE, 0xEFAD0, bytes(
+ cv64_string_to_bytearray(f"Where to...?\t{active_warp_list[0]}\t"
+ f"`{str(s1s_per_warp).zfill(2)} {active_warp_list[1]}\t"
+ f"`{str(s1s_per_warp * 2).zfill(2)} {active_warp_list[2]}\t"
+ f"`{str(s1s_per_warp * 3).zfill(2)} {active_warp_list[3]}\t"
+ f"`{str(s1s_per_warp * 4).zfill(2)} {active_warp_list[4]}\t"
+ f"`{str(s1s_per_warp * 5).zfill(2)} {active_warp_list[5]}\t"
+ f"`{str(s1s_per_warp * 6).zfill(2)} {active_warp_list[6]}\t"
+ f"`{str(s1s_per_warp * 7).zfill(2)} {active_warp_list[7]}")))
+
+ # Write the new File Select stage numbers.
+ for stage in world.active_stage_exits:
+ for offset in get_stage_info(stage, "save number offsets"):
+ patch.write_token(APTokenTypes.WRITE, offset, bytes([world.active_stage_exits[stage]["position"]]))
+
+ # Write all the shop text.
+ if world.options.shopsanity:
+ patch.write_token(APTokenTypes.WRITE, 0x103868, bytes(cv64_string_to_bytearray("Not obtained. ")))
+
+ shopsanity_name_text = bytearray(0)
+ shopsanity_desc_text = bytearray(0)
+ for i in range(len(shop_name_list)):
+ shopsanity_name_text += bytearray([0xA0, i]) + shop_colors_list[i] + \
+ cv64_string_to_bytearray(cv64_text_truncate(shop_name_list[i], 74))
+
+ shopsanity_desc_text += bytearray([0xA0, i])
+ if shop_desc_list[i][1] is not None:
+ shopsanity_desc_text += cv64_string_to_bytearray("For " + shop_desc_list[i][1] + ".\n",
+ append_end=False)
+ shopsanity_desc_text += cv64_string_to_bytearray(renon_item_dialogue[shop_desc_list[i][0]])
+ patch.write_token(APTokenTypes.WRITE, 0x1AD00, bytes(shopsanity_name_text))
+ patch.write_token(APTokenTypes.WRITE, 0x1A800, bytes(shopsanity_desc_text))
+
+ # Write the item/player names for other game items.
+ for loc in active_locations:
+ if loc.address is None or get_location_info(loc.name, "type") == "shop" or loc.item.player == world.player:
+ continue
+ if len(loc.item.name) > 67:
+ item_name = loc.item.name[0x00:0x68]
+ else:
+ item_name = loc.item.name
+ inject_address = 0xBB7164 + (256 * (loc.address & 0xFFF))
+ wrapped_name, num_lines = cv64_text_wrap(item_name + "\nfor " +
+ world.multiworld.get_player_name(loc.item.player), 96)
+ patch.write_token(APTokenTypes.WRITE, inject_address, bytes(get_item_text_color(loc) +
+ cv64_string_to_bytearray(wrapped_name)))
+ patch.write_token(APTokenTypes.WRITE, inject_address + 255, bytes([num_lines]))
+
+ # Write the secondary name the client will use to distinguish a vanilla ROM from an AP one.
+ patch.write_token(APTokenTypes.WRITE, 0xBFBFD0, "ARCHIPELAGO1".encode("utf-8"))
+ # Write the slot authentication
+ patch.write_token(APTokenTypes.WRITE, 0xBFBFE0, bytes(world.auth))
+
+ patch.write_file("token_data.bin", patch.get_token_binary())
+
+ # Write these slot options to a JSON.
+ options_dict = {
+ "character_stages": world.options.character_stages.value,
+ "vincent_fight_condition": world.options.vincent_fight_condition.value,
+ "renon_fight_condition": world.options.renon_fight_condition.value,
+ "bad_ending_condition": world.options.bad_ending_condition.value,
+ "increase_item_limit": world.options.increase_item_limit.value,
+ "nerf_healing_items": world.options.nerf_healing_items.value,
+ "loading_zone_heals": world.options.loading_zone_heals.value,
+ "disable_time_restrictions": world.options.disable_time_restrictions.value,
+ "death_link": world.options.death_link.value,
+ "draculas_condition": world.options.draculas_condition.value,
+ "invisible_items": world.options.invisible_items.value,
+ "post_behemoth_boss": world.options.post_behemoth_boss.value,
+ "room_of_clocks_boss": world.options.room_of_clocks_boss.value,
+ "skip_gondolas": world.options.skip_gondolas.value,
+ "skip_waterway_blocks": world.options.skip_waterway_blocks.value,
+ "s1s_per_warp": world.options.special1s_per_warp.value,
+ "required_s2s": world.required_s2s,
+ "total_s2s": world.total_s2s,
+ "total_special1s": world.options.total_special1s.value,
+ "increase_shimmy_speed": world.options.increase_shimmy_speed.value,
+ "fall_guard": world.options.fall_guard.value,
+ "cinematic_experience": world.options.cinematic_experience.value,
+ "permanent_powerups": world.options.permanent_powerups.value,
+ "background_music": world.options.background_music.value,
+ "multi_hit_breakables": world.options.multi_hit_breakables.value,
+ "drop_previous_sub_weapon": world.options.drop_previous_sub_weapon.value,
+ "countdown": world.options.countdown.value,
+ "shopsanity": world.options.shopsanity.value,
+ "panther_dash": world.options.panther_dash.value,
+ "big_toss": world.options.big_toss.value,
+ "window_color_r": world.options.window_color_r.value,
+ "window_color_g": world.options.window_color_g.value,
+ "window_color_b": world.options.window_color_b.value,
+ "window_color_a": world.options.window_color_a.value,
+ }
+
+ patch.write_file("options.json", json.dumps(options_dict).encode('utf-8'))
+
+
+def get_base_rom_bytes(file_name: str = "") -> bytes:
+ base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
+ if not base_rom_bytes:
+ file_name = get_base_rom_path(file_name)
+ base_rom_bytes = bytes(open(file_name, "rb").read())
+
+ basemd5 = hashlib.md5()
+ basemd5.update(base_rom_bytes)
+ if CV64_US_10_HASH != basemd5.hexdigest():
+ raise Exception("Supplied Base Rom does not match known MD5 for Castlevania 64 US 1.0."
+ "Get the correct game and version, then dump it.")
+ setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)
+ return base_rom_bytes
+
+
+def get_base_rom_path(file_name: str = "") -> str:
+ if not file_name:
+ file_name = get_settings()["cv64_options"]["rom_file"]
+ if not os.path.exists(file_name):
+ file_name = Utils.user_path(file_name)
+ return file_name
diff --git a/worlds/cv64/rules.py b/worlds/cv64/rules.py
new file mode 100644
index 000000000000..9642073341e4
--- /dev/null
+++ b/worlds/cv64/rules.py
@@ -0,0 +1,103 @@
+from typing import Dict, TYPE_CHECKING
+
+from BaseClasses import CollectionState
+from worlds.generic.Rules import allow_self_locking_items, CollectionRule
+from .options import DraculasCondition
+from .entrances import get_entrance_info
+from .data import iname, rname
+
+if TYPE_CHECKING:
+ from . import CV64World
+
+
+class CV64Rules:
+ player: int
+ world: "CV64World"
+ rules: Dict[str, CollectionRule]
+ s1s_per_warp: int
+ required_s2s: int
+ drac_condition: int
+
+ def __init__(self, world: "CV64World") -> None:
+ self.player = world.player
+ self.world = world
+ self.s1s_per_warp = world.s1s_per_warp
+ self.required_s2s = world.required_s2s
+ self.drac_condition = world.drac_condition
+
+ self.rules = {
+ iname.left_tower_key: lambda state: state.has(iname.left_tower_key, self.player),
+ iname.storeroom_key: lambda state: state.has(iname.storeroom_key, self.player),
+ iname.archives_key: lambda state: state.has(iname.archives_key, self.player),
+ iname.garden_key: lambda state: state.has(iname.garden_key, self.player),
+ iname.copper_key: lambda state: state.has(iname.copper_key, self.player),
+ iname.chamber_key: lambda state: state.has(iname.chamber_key, self.player),
+ "Bomb 1": lambda state: state.has_all({iname.magical_nitro, iname.mandragora}, self.player),
+ "Bomb 2": lambda state: state.has(iname.magical_nitro, self.player, 2)
+ and state.has(iname.mandragora, self.player, 2),
+ iname.execution_key: lambda state: state.has(iname.execution_key, self.player),
+ iname.science_key1: lambda state: state.has(iname.science_key1, self.player),
+ iname.science_key2: lambda state: state.has(iname.science_key2, self.player),
+ iname.science_key3: lambda state: state.has(iname.science_key3, self.player),
+ iname.clocktower_key1: lambda state: state.has(iname.clocktower_key1, self.player),
+ iname.clocktower_key2: lambda state: state.has(iname.clocktower_key2, self.player),
+ iname.clocktower_key3: lambda state: state.has(iname.clocktower_key3, self.player),
+ "Dracula": self.can_enter_dracs_chamber
+ }
+
+ def can_enter_dracs_chamber(self, state: CollectionState) -> bool:
+ drac_object_name = None
+ if self.drac_condition == DraculasCondition.option_crystal:
+ drac_object_name = "Crystal"
+ elif self.drac_condition == DraculasCondition.option_bosses:
+ drac_object_name = "Trophy"
+ elif self.drac_condition == DraculasCondition.option_specials:
+ drac_object_name = "Special2"
+
+ if drac_object_name is not None:
+ return state.has(drac_object_name, self.player, self.required_s2s)
+ return True
+
+ def set_cv64_rules(self) -> None:
+ multiworld = self.world.multiworld
+
+ for region in multiworld.get_regions(self.player):
+ # Set each entrance's rule if it should have one.
+ # Warp entrances have their own special handling.
+ for entrance in region.entrances:
+ if entrance.parent_region.name == "Menu":
+ if entrance.name.startswith("Warp "):
+ entrance.access_rule = lambda state, warp_num=int(entrance.name[5]): \
+ state.has(iname.special_one, self.player, self.s1s_per_warp * warp_num)
+ else:
+ ent_rule = get_entrance_info(entrance.name, "rule")
+ if ent_rule in self.rules:
+ entrance.access_rule = self.rules[ent_rule]
+
+ multiworld.completion_condition[self.player] = lambda state: state.has(iname.victory, self.player)
+ if self.world.options.accessibility: # not locations accessibility
+ self.set_self_locking_items()
+
+ def set_self_locking_items(self) -> None:
+ multiworld = self.world.multiworld
+
+ # Do the regions that we know for a fact always exist, and we always do no matter what.
+ allow_self_locking_items(multiworld.get_region(rname.villa_archives, self.player), iname.archives_key)
+ allow_self_locking_items(multiworld.get_region(rname.cc_torture_chamber, self.player), iname.chamber_key)
+
+ # Add this region if the world doesn't have the Villa Storeroom warp entrance.
+ if "Villa" not in self.world.active_warp_list[1:]:
+ allow_self_locking_items(multiworld.get_region(rname.villa_storeroom, self.player), iname.storeroom_key)
+
+ # Add this region if Hard Logic is on and Multi Hit Breakables are off.
+ if self.world.options.hard_logic and not self.world.options.multi_hit_breakables:
+ allow_self_locking_items(multiworld.get_region(rname.cw_ltower, self.player), iname.left_tower_key)
+
+ # Add these regions if Tower of Science is in the world.
+ if "Tower of Science" in self.world.active_stage_exits:
+ allow_self_locking_items(multiworld.get_region(rname.tosci_three_doors, self.player), iname.science_key1)
+ allow_self_locking_items(multiworld.get_region(rname.tosci_key3, self.player), iname.science_key3)
+
+ # Add this region if Tower of Execution is in the world and Hard Logic is not on.
+ if "Tower of Execution" in self.world.active_stage_exits and self.world.options.hard_logic:
+ allow_self_locking_items(multiworld.get_region(rname.toe_ledge, self.player), iname.execution_key)
diff --git a/worlds/cv64/src/drop_sub_weapon.c b/worlds/cv64/src/drop_sub_weapon.c
new file mode 100644
index 000000000000..d1a4b52269c6
--- /dev/null
+++ b/worlds/cv64/src/drop_sub_weapon.c
@@ -0,0 +1,69 @@
+// Written by Moisés
+#include "include/game/module.h"
+#include "include/game/math.h"
+#include "cv64.h"
+
+extern vec3f player_pos;
+extern vec3s player_angle; // player_angle.y = Player's facing angle (yaw)
+extern f32 player_height_with_respect_of_floor; // Stored negative in-game
+
+#define SHT_MAX 32767.0f
+#define SHT_MINV (1.0f / SHT_MAX)
+
+void spawn_item_behind_player(s32 item) {
+ interactuablesModule* pickable_item = NULL;
+ const f32 spawnDistance = 8.0f;
+ vec3f player_backwards_dir;
+
+ pickable_item = (interactuablesModule*)module_createAndSetChild(moduleList_findFirstModuleByID(ACTOR_CREATOR), ACTOR_ITEM);
+ if (pickable_item != NULL) {
+ // Convert facing angle to a vec3f
+ // SHT_MINV needs to be negative here for the item to be spawned properly on the character's back
+ player_backwards_dir.x = coss(-player_angle.y) * -SHT_MINV;
+ player_backwards_dir.z = sins(-player_angle.y) * -SHT_MINV;
+ // Multiply facing vector with distance away from the player
+ vec3f_multiplyScalar(&player_backwards_dir, &player_backwards_dir, spawnDistance);
+ // Assign the position of the item relative to the player's current position.
+ vec3f_add(&pickable_item->position, &player_pos, &player_backwards_dir);
+ // The Y position of the item will be the same as the floor right under the player
+ // The player's height with respect of the flower under them is already stored negative in-game,
+ // so no need to substract
+ pickable_item->position.y = player_pos.y + 5.0f;
+ pickable_item->height = pickable_item->position.y;
+
+ // Assign item ID
+ pickable_item->item_ID = item;
+ }
+}
+
+
+const f32 droppingAccel = 0.05f;
+const f32 maxDroppingSpeed = 1.5f;
+f32 droppingSpeed = 0.0f;
+f32 droppingTargetYPos = 0.0f;
+u8 dropItemCalcFuncCalled = FALSE;
+
+s32 drop_item_calc(interactuablesModule* pickable_item) {
+ if (dropItemCalcFuncCalled == FALSE) {
+ droppingTargetYPos = player_pos.y + player_height_with_respect_of_floor + 1.0f;
+ if (pickable_item->item_ID == CROSS || pickable_item->item_ID == AXE ||
+ pickable_item->item_ID == CROSS__VANISH || pickable_item->item_ID == AXE__VANISH) {
+ droppingTargetYPos += 3.0f;
+ }
+ dropItemCalcFuncCalled = TRUE;
+ return TRUE;
+ }
+ if (pickable_item->position.y <= droppingTargetYPos) {
+ droppingSpeed = 0.0f;
+ dropItemCalcFuncCalled = FALSE;
+ return FALSE;
+ }
+ else {
+ if (droppingSpeed < maxDroppingSpeed) {
+ droppingSpeed += droppingAccel;
+ }
+ pickable_item->position.y -= droppingSpeed;
+ pickable_item->height = pickable_item->position.y;
+ return TRUE;
+ }
+}
\ No newline at end of file
diff --git a/worlds/cv64/src/print.c b/worlds/cv64/src/print.c
new file mode 100644
index 000000000000..7f77afb00f0f
--- /dev/null
+++ b/worlds/cv64/src/print.c
@@ -0,0 +1,116 @@
+// Written by Moisés.
+// NOTE: This is an earlier version to-be-replaced.
+#include
+#include
+
+// Helper function
+// https://decomp.me/scratch/9H1Uy
+u32 convertUTF8StringToUTF16(char* src, u16* buffer) {
+ u32 string_length = 0;
+
+ // If the source string starts with a null char (0), we assume the string empty.
+ if (*src != 0) {
+ // Copy the char from the source string into the bufferination.
+ // Then advance to the next char until we find the null char (0).
+ do {
+ *buffer = *src;
+ src++;
+ buffer++;
+ string_length++;
+ } while (*src != 0);
+ }
+ // Make sure to add the null char at the end of the bufferination string,
+ // and then return the length of the string.
+ *buffer = 0;
+ return string_length;
+}
+
+// Begin printing ASCII text stored in a char*
+textbox* print_text(const char* message, const s16 X_pos, const s16 Y_pos, const u8 number_of_lines, const s16 textbox_width, const u32 txtbox_flags, const void* module) {
+ textbox* (*ptr_textbox_create)(void*, void*, u32) = textbox_create;
+ void (*ptr_textbox_setPos)(textbox*, u16, u16, s32) = textbox_setPos;
+ void (*ptr_textbox_setDimensions)(textbox*, s8, s16, u8, u8) = textbox_setDimensions;
+ void (*ptr_textbox_setMessagePtr)(textbox*, u16*, s32, s16) = textbox_setMessagePtr;
+ u16* (*ptr_convertUTF16ToCustomTextFormat)(u16*) = convertUTF16ToCustomTextFormat;
+ void* (*ptr_malloc)(s32, u32) = malloc;
+
+ textbox* txtbox = NULL;
+
+ // Allocate memory for the text buffer
+ u16* text_buffer = (u16*) ptr_malloc(0, 100);
+
+ // Create the textbox data structure
+ if (module != NULL) {
+ txtbox = ptr_textbox_create(module, HUD_camera, txtbox_flags);
+ }
+
+ if (txtbox != NULL && text_buffer != NULL && message != NULL) {
+ // Set text position and dimensions
+ ptr_textbox_setPos(txtbox, X_pos, Y_pos, 1);
+ ptr_textbox_setDimensions(txtbox, number_of_lines, textbox_width, 0, 0);
+
+ // Convert the ASCII message to the CV64 custom format
+ convertUTF8StringToUTF16(message, text_buffer);
+ ptr_convertUTF16ToCustomTextFormat(text_buffer);
+
+ // Set the text buffer pointer to the textbox data structure
+ ptr_textbox_setMessagePtr(txtbox, text_buffer, 0, 0);
+ }
+ // We return the textbox so that we can modify its properties once it begins printing
+ // (say to show, hide the text)
+ return txtbox;
+}
+
+// Begin printing signed integer
+textbox* print_number(const s32 number, u16* text_buffer, const s16 X_pos, const s16 Y_pos, const u8 number_of_digits, const u32 txtbox_flags, const u32 additional_text_flag, const void* module) {
+ textbox* (*ptr_textbox_create)(void*, void*, u32) = textbox_create;
+ void (*ptr_textbox_setPos)(textbox*, u16, u16, s32) = textbox_setPos;
+ void (*ptr_textbox_setDimensions)(textbox*, s8, s16, u8, u8) = textbox_setDimensions;
+ void (*ptr_textbox_setMessagePtr)(textbox*, u16*, s32, s16) = textbox_setMessagePtr;
+ void (*ptr_text_convertIntNumberToText)(u32, u16*, u8, u32) = text_convertIntNumberToText;
+
+ textbox* txtbox = NULL;
+
+ // Create the textbox data structure
+ if (module != NULL) {
+ txtbox = ptr_textbox_create(module, HUD_camera, txtbox_flags);
+ }
+
+ if (txtbox != NULL && text_buffer != NULL) {
+ // Set text position and dimensions
+ ptr_textbox_setPos(txtbox, X_pos, Y_pos, 1);
+ ptr_textbox_setDimensions(txtbox, 1, 100, 0, 0);
+
+ // Convert the number to the CV64 custom format
+ ptr_text_convertIntNumberToText(number, text_buffer, number_of_digits, additional_text_flag);
+
+ // Set the text buffer pointer to the textbox data structure
+ ptr_textbox_setMessagePtr(txtbox, text_buffer, 0, 0);
+ }
+ // We return the textbox so that we can modify its properties once it begins printing
+ // (say to show, hide the text)
+ return txtbox;
+}
+
+// Update the value of a number that began printing after calling "print_number()"
+void update_printed_number(textbox* txtbox, const s32 number, u16* text_buffer, const u8 number_of_digits, const u32 additional_text_flag) {
+ void (*ptr_text_convertIntNumberToText)(u32, u16*, u8, u32) = text_convertIntNumberToText;
+
+ if (text_buffer != NULL) {
+ ptr_text_convertIntNumberToText(number, text_buffer, number_of_digits, additional_text_flag);
+ txtbox->flags |= 0x1000000; // Needed to make sure the number updates properly
+ }
+}
+
+void display_text(textbox* txtbox, const u8 display_textbox) {
+ if (txtbox != NULL) {
+ if (display_textbox == TRUE) {
+ // Show text
+ txtbox->flags &= ~HIDE_TEXTBOX;
+ }
+ else {
+ // Hide text
+ txtbox->flags |= HIDE_TEXTBOX;
+ }
+ }
+}
diff --git a/worlds/cv64/src/print_text_ovl.c b/worlds/cv64/src/print_text_ovl.c
new file mode 100644
index 000000000000..7ca8e6f35e2d
--- /dev/null
+++ b/worlds/cv64/src/print_text_ovl.c
@@ -0,0 +1,26 @@
+// Written by Moisés
+#include "print.h"
+#include
+#include
+
+#define counter_X_pos 30
+#define counter_Y_pos 40
+#define counter_number_of_digits 2
+#define GOLD_JEWEL_FONT 0x14
+
+extern u8 bytes[13];
+
+u16* number_text_buffer = NULL;
+textbox* txtbox = NULL;
+
+void begin_print() {
+ // Allocate memory for the number text
+ number_text_buffer = (u16*) malloc(0, 12);
+
+ // Assuming that 0x80342814 = HUD Module
+ txtbox = print_number(0, number_text_buffer, counter_X_pos, counter_Y_pos, counter_number_of_digits, 0x08600000, GOLD_JEWEL_FONT, (void*) 0x80342814);
+}
+
+void update_print(u8 i) {
+ update_printed_number(txtbox, (s32) bytes[i], number_text_buffer, counter_number_of_digits, GOLD_JEWEL_FONT);
+}
diff --git a/worlds/cv64/stages.py b/worlds/cv64/stages.py
new file mode 100644
index 000000000000..d7059b3580f2
--- /dev/null
+++ b/worlds/cv64/stages.py
@@ -0,0 +1,490 @@
+import logging
+
+from .data import rname
+from .regions import get_region_info
+from .locations import get_location_info
+from .options import WarpOrder
+
+from typing import TYPE_CHECKING, Dict, List, Tuple, Union
+
+if TYPE_CHECKING:
+ from . import CV64World
+
+
+# # # KEY # # #
+# "start region" = The Region that the start of the stage is in. Used for connecting the previous stage's end and
+# alternate end (if it exists) Entrances to the start of the next one.
+# "start map id" = The map ID that the start of the stage is in.
+# "start spawn id" = The player spawn location ID for the start of the stage. This and "start map id" are both written
+# to the previous stage's end loading zone to make it send the player to the next stage in the
+# world's determined stage order.
+# "mid region" = The Region that the stage's middle warp point is in. Used for connecting the warp Entrances after the
+# starting stage to where they should be connecting to.
+# "mid map id" = The map ID that the stage's middle warp point is in.
+# "mid spawn id" = The player spawn location ID for the stage's middle warp point. This and "mid map id" are both
+# written to the warp menu code to make it send the player to where it should be sending them.
+# "end region" = The Region that the end of the stage is in. Used for connecting the next stage's beginning Entrance
+# (if it exists) to the end of the previous one.
+# "end map id" = The map ID that the end of the stage is in.
+# "end spawn id" = The player spawn location ID for the end of the stage. This and "end map id" are both written to the
+# next stage's beginning loading zone (if it exists) to make it send the player to the previous stage
+# in the world's determined stage order.
+# startzone map offset = The offset in the ROM to overwrite to change where the start of the stage leads.
+# startzone spawn offset = The offset in the ROM to overwrite to change what spawn location in the previous map the
+# start of the stage puts the player at.
+# endzone map offset = The offset in the ROM to overwrite to change where the end of the stage leads.
+# endzone spawn offset = The offset in the ROM to overwrite to change what spawn location in the next map the end of
+# the stage puts the player at.
+# altzone map offset = The offset in the ROM to overwrite to change where the alternate end of the stage leads
+# (if it exists).
+# altzone spawn offset = The offset in the ROM to overwrite to change what spawn location in the next map the alternate
+# end of the stage puts the player at.
+# character = What character that stage is exclusively meant for normally. Used in determining what stages to leave out
+# depending on what character stage setting was chosen in the player options.
+# save number offsets = The offsets to overwrite to change what stage number is displayed on the save file when saving
+# at the stage's White Jewels.
+# regions = All Regions that make up the stage. If the stage is in the world's active stages, its Regions and their
+# corresponding Locations and Entrances will all be created.
+stage_info = {
+ "Forest of Silence": {
+ "start region": rname.forest_start, "start map id": b"\x00", "start spawn id": b"\x00",
+ "mid region": rname.forest_mid, "mid map id": b"\x00", "mid spawn id": b"\x04",
+ "end region": rname.forest_end, "end map id": b"\x00", "end spawn id": b"\x01",
+ "endzone map offset": 0xB6302F, "endzone spawn offset": 0xB6302B,
+ "save number offsets": [0x1049C5, 0x1049CD, 0x1049D5],
+ "regions": [rname.forest_start,
+ rname.forest_mid,
+ rname.forest_end]
+ },
+
+ "Castle Wall": {
+ "start region": rname.cw_start, "start map id": b"\x02", "start spawn id": b"\x00",
+ "mid region": rname.cw_start, "mid map id": b"\x02", "mid spawn id": b"\x07",
+ "end region": rname.cw_exit, "end map id": b"\x02", "end spawn id": b"\x10",
+ "endzone map offset": 0x109A5F, "endzone spawn offset": 0x109A61,
+ "save number offsets": [0x1049DD, 0x1049E5, 0x1049ED],
+ "regions": [rname.cw_start,
+ rname.cw_exit,
+ rname.cw_ltower]
+ },
+
+ "Villa": {
+ "start region": rname.villa_start, "start map id": b"\x03", "start spawn id": b"\x00",
+ "mid region": rname.villa_storeroom, "mid map id": b"\x05", "mid spawn id": b"\x04",
+ "end region": rname.villa_crypt, "end map id": b"\x1A", "end spawn id": b"\x03",
+ "endzone map offset": 0xD9DA3, "endzone spawn offset": 0x109E81,
+ "altzone map offset": 0xD9DAB, "altzone spawn offset": 0x109E81,
+ "save number offsets": [0x1049F5, 0x1049FD, 0x104A05, 0x104A0D],
+ "regions": [rname.villa_start,
+ rname.villa_main,
+ rname.villa_storeroom,
+ rname.villa_archives,
+ rname.villa_maze,
+ rname.villa_servants,
+ rname.villa_crypt]
+ },
+
+ "Tunnel": {
+ "start region": rname.tunnel_start, "start map id": b"\x07", "start spawn id": b"\x00",
+ "mid region": rname.tunnel_end, "mid map id": b"\x07", "mid spawn id": b"\x03",
+ "end region": rname.tunnel_end, "end map id": b"\x07", "end spawn id": b"\x11",
+ "endzone map offset": 0x109B4F, "endzone spawn offset": 0x109B51, "character": "Reinhardt",
+ "save number offsets": [0x104A15, 0x104A1D, 0x104A25, 0x104A2D],
+ "regions": [rname.tunnel_start,
+ rname.tunnel_end]
+ },
+
+ "Underground Waterway": {
+ "start region": rname.uw_main, "start map id": b"\x08", "start spawn id": b"\x00",
+ "mid region": rname.uw_main, "mid map id": b"\x08", "mid spawn id": b"\x03",
+ "end region": rname.uw_end, "end map id": b"\x08", "end spawn id": b"\x01",
+ "endzone map offset": 0x109B67, "endzone spawn offset": 0x109B69, "character": "Carrie",
+ "save number offsets": [0x104A35, 0x104A3D],
+ "regions": [rname.uw_main,
+ rname.uw_end]
+ },
+
+ "Castle Center": {
+ "start region": rname.cc_main, "start map id": b"\x19", "start spawn id": b"\x00",
+ "mid region": rname.cc_main, "mid map id": b"\x0E", "mid spawn id": b"\x03",
+ "end region": rname.cc_elev_top, "end map id": b"\x0F", "end spawn id": b"\x02",
+ "endzone map offset": 0x109CB7, "endzone spawn offset": 0x109CB9,
+ "altzone map offset": 0x109CCF, "altzone spawn offset": 0x109CD1,
+ "save number offsets": [0x104A45, 0x104A4D, 0x104A55, 0x104A5D, 0x104A65, 0x104A6D, 0x104A75],
+ "regions": [rname.cc_main,
+ rname.cc_torture_chamber,
+ rname.cc_library,
+ rname.cc_crystal,
+ rname.cc_elev_top]
+ },
+
+ "Duel Tower": {
+ "start region": rname.dt_main, "start map id": b"\x13", "start spawn id": b"\x00",
+ "startzone map offset": 0x109DA7, "startzone spawn offset": 0x109DA9,
+ "mid region": rname.dt_main, "mid map id": b"\x13", "mid spawn id": b"\x15",
+ "end region": rname.dt_main, "end map id": b"\x13", "end spawn id": b"\x01",
+ "endzone map offset": 0x109D8F, "endzone spawn offset": 0x109D91, "character": "Reinhardt",
+ "save number offsets": [0x104ACD],
+ "regions": [rname.dt_main]
+ },
+
+ "Tower of Execution": {
+ "start region": rname.toe_main, "start map id": b"\x10", "start spawn id": b"\x00",
+ "startzone map offset": 0x109D17, "startzone spawn offset": 0x109D19,
+ "mid region": rname.toe_main, "mid map id": b"\x10", "mid spawn id": b"\x02",
+ "end region": rname.toe_main, "end map id": b"\x10", "end spawn id": b"\x12",
+ "endzone map offset": 0x109CFF, "endzone spawn offset": 0x109D01, "character": "Reinhardt",
+ "save number offsets": [0x104A7D, 0x104A85],
+ "regions": [rname.toe_main,
+ rname.toe_ledge]
+ },
+
+ "Tower of Science": {
+ "start region": rname.tosci_start, "start map id": b"\x12", "start spawn id": b"\x00",
+ "startzone map offset": 0x109D77, "startzone spawn offset": 0x109D79,
+ "mid region": rname.tosci_conveyors, "mid map id": b"\x12", "mid spawn id": b"\x03",
+ "end region": rname.tosci_conveyors, "end map id": b"\x12", "end spawn id": b"\x04",
+ "endzone map offset": 0x109D5F, "endzone spawn offset": 0x109D61, "character": "Carrie",
+ "save number offsets": [0x104A95, 0x104A9D, 0x104AA5],
+ "regions": [rname.tosci_start,
+ rname.tosci_three_doors,
+ rname.tosci_conveyors,
+ rname.tosci_key3]
+ },
+
+ "Tower of Sorcery": {
+ "start region": rname.tosor_main, "start map id": b"\x11", "start spawn id": b"\x00",
+ "startzone map offset": 0x109D47, "startzone spawn offset": 0x109D49,
+ "mid region": rname.tosor_main, "mid map id": b"\x11", "mid spawn id": b"\x01",
+ "end region": rname.tosor_main, "end map id": b"\x11", "end spawn id": b"\x13",
+ "endzone map offset": 0x109D2F, "endzone spawn offset": 0x109D31, "character": "Carrie",
+ "save number offsets": [0x104A8D],
+ "regions": [rname.tosor_main]
+ },
+
+ "Room of Clocks": {
+ "start region": rname.roc_main, "start map id": b"\x1B", "start spawn id": b"\x00",
+ "mid region": rname.roc_main, "mid map id": b"\x1B", "mid spawn id": b"\x02",
+ "end region": rname.roc_main, "end map id": b"\x1B", "end spawn id": b"\x14",
+ "endzone map offset": 0x109EAF, "endzone spawn offset": 0x109EB1,
+ "save number offsets": [0x104AC5],
+ "regions": [rname.roc_main]
+ },
+
+ "Clock Tower": {
+ "start region": rname.ct_start, "start map id": b"\x17", "start spawn id": b"\x00",
+ "mid region": rname.ct_middle, "mid map id": b"\x17", "mid spawn id": b"\x02",
+ "end region": rname.ct_end, "end map id": b"\x17", "end spawn id": b"\x03",
+ "endzone map offset": 0x109E37, "endzone spawn offset": 0x109E39,
+ "save number offsets": [0x104AB5, 0x104ABD],
+ "regions": [rname.ct_start,
+ rname.ct_middle,
+ rname.ct_end]
+ },
+
+ "Castle Keep": {
+ "start region": rname.ck_main, "start map id": b"\x14", "start spawn id": b"\x02",
+ "mid region": rname.ck_main, "mid map id": b"\x14", "mid spawn id": b"\x03",
+ "end region": rname.ck_drac_chamber,
+ "save number offsets": [0x104AAD],
+ "regions": [rname.ck_main]
+ },
+}
+
+vanilla_stage_order = ("Forest of Silence", "Castle Wall", "Villa", "Tunnel", "Underground Waterway", "Castle Center",
+ "Duel Tower", "Tower of Execution", "Tower of Science", "Tower of Sorcery", "Room of Clocks",
+ "Clock Tower", "Castle Keep")
+
+# # # KEY # # #
+# "prev" = The previous stage in the line.
+# "next" = The next stage in the line.
+# "alt" = The alternate next stage in the line (if one exists).
+# "position" = The stage's number in the order of stages.
+# "path" = Character indicating whether the stage is on the main path or an alternate path, similar to Rondo of Blood.
+# Used in writing the randomized stage order to the spoiler.
+vanilla_stage_exits = {rname.forest_of_silence: {"prev": None, "next": rname.castle_wall,
+ "alt": None, "position": 1, "path": " "},
+ rname.castle_wall: {"prev": None, "next": rname.villa,
+ "alt": None, "position": 2, "path": " "},
+ rname.villa: {"prev": None, "next": rname.tunnel,
+ "alt": rname.underground_waterway, "position": 3, "path": " "},
+ rname.tunnel: {"prev": None, "next": rname.castle_center,
+ "alt": None, "position": 4, "path": " "},
+ rname.underground_waterway: {"prev": None, "next": rname.castle_center,
+ "alt": None, "position": 4, "path": "'"},
+ rname.castle_center: {"prev": None, "next": rname.duel_tower,
+ "alt": rname.tower_of_science, "position": 5, "path": " "},
+ rname.duel_tower: {"prev": rname.castle_center, "next": rname.tower_of_execution,
+ "alt": None, "position": 6, "path": " "},
+ rname.tower_of_execution: {"prev": rname.duel_tower, "next": rname.room_of_clocks,
+ "alt": None, "position": 7, "path": " "},
+ rname.tower_of_science: {"prev": rname.castle_center, "next": rname.tower_of_sorcery,
+ "alt": None, "position": 6, "path": "'"},
+ rname.tower_of_sorcery: {"prev": rname.tower_of_science, "next": rname.room_of_clocks,
+ "alt": None, "position": 7, "path": "'"},
+ rname.room_of_clocks: {"prev": None, "next": rname.clock_tower,
+ "alt": None, "position": 8, "path": " "},
+ rname.clock_tower: {"prev": None, "next": rname.castle_keep,
+ "alt": None, "position": 9, "path": " "},
+ rname.castle_keep: {"prev": None, "next": None,
+ "alt": None, "position": 10, "path": " "}}
+
+
+def get_stage_info(stage: str, info: str) -> Union[str, int, Union[List[int], List[str]], None]:
+ return stage_info[stage].get(info, None)
+
+
+def get_locations_from_stage(stage: str) -> List[str]:
+ overall_locations = []
+ for region in get_stage_info(stage, "regions"):
+ stage_locations = get_region_info(region, "locations")
+ if stage_locations is not None:
+ overall_locations += stage_locations
+
+ final_locations = []
+ for loc in overall_locations:
+ if get_location_info(loc, "code") is not None:
+ final_locations.append(loc)
+ return final_locations
+
+
+def verify_character_stage(world: "CV64World", stage: str) -> bool:
+ # Verify a character stage is in the world if the given stage is a character stage.
+ stage_char = get_stage_info(stage, "character")
+ return stage_char is None or (world.reinhardt_stages and stage_char == "Reinhardt") or \
+ (world.carrie_stages and stage_char == "Carrie")
+
+
+def get_normal_stage_exits(world: "CV64World") -> Dict[str, dict]:
+ exits = {name: vanilla_stage_exits[name].copy() for name in vanilla_stage_exits}
+ non_branching_pos = 1
+
+ for stage in stage_info:
+ # Remove character stages that are not enabled.
+ if not verify_character_stage(world, stage):
+ del exits[stage]
+ continue
+
+ # If branching pathways are not enabled, update the exit info to converge said stages on a single path.
+ if world.branching_stages:
+ continue
+ if world.carrie_stages and not world.reinhardt_stages and exits[stage]["alt"] is not None:
+ exits[stage]["next"] = exits[stage]["alt"]
+ elif world.carrie_stages and world.reinhardt_stages and stage != rname.castle_keep:
+ exits[stage]["next"] = vanilla_stage_order[vanilla_stage_order.index(stage) + 1]
+ exits[stage]["alt"] = None
+ exits[stage]["position"] = non_branching_pos
+ exits[stage]["path"] = " "
+ non_branching_pos += 1
+
+ return exits
+
+
+def shuffle_stages(world: "CV64World", stage_1_blacklist: List[str]) \
+ -> Tuple[Dict[str, Dict[str, Union[str, int, None]]], str, List[str]]:
+ """Woah, this is a lot! I should probably summarize what's happening in here, huh?
+
+ So, in the vanilla game, all the stages are basically laid out on a linear "timeline" with some stages being
+ different depending on who you are playing as. The different character stages, in question, are the one following
+ Villa and the two following Castle Center. The ends of these two stages are considered the route divergences and, in
+ this rando, the game's behavior has been changed in such that both characters can access each other's exclusive
+ stages (thereby making the entire game playable in just one character run). With this in mind, when shuffling the
+ stages around, there is one particularly big rule that must be kept in mind to ensure things don't get too wacky.
+ That being:
+
+ Villa and Castle Center cannot appear in branching path stage slots; they can only be on "main" path slots.
+
+ So for this reason, generating a new stage layout is not as simple as just scrambling a list of stages around. It
+ must be done in such a way that whatever stages directly follow Villa or CC is not the other stage. The exception is
+ if branching stages are not a thing at all due to the player settings, in which case everything I said above does
+ not matter. Consider the following representation of a stage "timeline", wherein each "-" represents a main stage
+ and a "=" represents a pair of branching stages:
+
+ -==---=---
+
+ In the above example, CC is the first "-" and Villa is the fourth. CC and Villa can only be "-"s whereas every other
+ stage can be literally anywhere, including on one of the "=" dashes. Villa will always be followed by one pair of
+ branching stages and CC will be followed by two pairs.
+
+ This code starts by first generating a singular list of stages that fit the criteria of Castle Center not being in
+ the next two entries following Villa and Villa not being in the next four entries after Castle Center. Once that has
+ been figured out, it will then generate a dictionary of stages with the appropriate information regarding what
+ stages come before and after them to then be used for Entrance creation as well as what position in the list they
+ are in for the purposes of the spoiler log and extended hint information.
+
+ I opted to use the Rondo of Blood "'" stage notation to represent Carrie stage slots specifically. If a main stage
+ with a backwards connection connects backwards into a pair of branching stages, it will be the non-"'" stage
+ (Reinhardt's) that it connects to. The Carrie stage slot cannot be accessed this way.
+
+ If anyone has any ideas or suggestions on how to improve this, I'd love to hear them! Because it's only going to get
+ uglier come Legacy of Darkness and Cornell's funny side route later on.
+ """
+
+ starting_stage_value = world.options.starting_stage.value
+
+ # Verify the starting stage is valid. If it isn't, pick a stage at random.
+ if vanilla_stage_order[starting_stage_value] not in stage_1_blacklist and \
+ verify_character_stage(world, vanilla_stage_order[starting_stage_value]):
+ starting_stage = vanilla_stage_order[starting_stage_value]
+ else:
+ logging.warning(f"[{world.multiworld.player_name[world.player]}] {vanilla_stage_order[starting_stage_value]} "
+ f"cannot be the starting stage with the chosen settings. Picking a different stage instead...")
+ possible_stages = []
+ for stage in vanilla_stage_order:
+ if stage in world.active_stage_exits and stage != rname.castle_keep:
+ possible_stages.append(stage)
+ starting_stage = world.random.choice(possible_stages)
+ world.options.starting_stage.value = vanilla_stage_order.index(starting_stage)
+
+ remaining_stage_pool = [stage for stage in world.active_stage_exits]
+ remaining_stage_pool.remove(rname.castle_keep)
+
+ total_stages = len(remaining_stage_pool)
+
+ new_stage_order = []
+ villa_cc_ids = [2, 3]
+ alt_villa_stage = []
+ alt_cc_stages = []
+
+ # If there are branching stages, remove Villa and CC from the list and determine their placements first.
+ if world.branching_stages:
+ villa_cc_ids = world.random.sample(range(1, 5), 2)
+ remaining_stage_pool.remove(rname.villa)
+ remaining_stage_pool.remove(rname.castle_center)
+
+ # Remove the starting stage from the remaining pool if it's in there at this point.
+ if starting_stage in remaining_stage_pool:
+ remaining_stage_pool.remove(starting_stage)
+
+ # If Villa or CC is our starting stage, force its respective ID to be 0 and re-randomize the other.
+ if starting_stage == rname.villa:
+ villa_cc_ids[0] = 0
+ villa_cc_ids[1] = world.random.randint(1, 5)
+ elif starting_stage == rname.castle_center:
+ villa_cc_ids[1] = 0
+ villa_cc_ids[0] = world.random.randint(1, 5)
+
+ for i in range(total_stages):
+ # If we're on Villa or CC's ID while in branching stage mode, put the respective stage in the slot.
+ if world.branching_stages and i == villa_cc_ids[0] and rname.villa not in new_stage_order:
+ new_stage_order.append(rname.villa)
+ villa_cc_ids[1] += 2
+ elif world.branching_stages and i == villa_cc_ids[1] and rname.castle_center not in new_stage_order:
+ new_stage_order.append(rname.castle_center)
+ villa_cc_ids[0] += 4
+ else:
+ # If neither of the above are true, if we're looking at Stage 1, append the starting stage.
+ # Otherwise, draw a random stage from the active list and delete it from there.
+ if i == 0:
+ new_stage_order.append(starting_stage)
+ else:
+ new_stage_order.append(world.random.choice(remaining_stage_pool))
+ remaining_stage_pool.remove(new_stage_order[i])
+
+ # If we're looking at an alternate stage slot, put the stage in one of these lists to indicate it as such
+ if not world.branching_stages:
+ continue
+ if i - 2 >= 0:
+ if new_stage_order[i - 2] == rname.villa:
+ alt_villa_stage.append(new_stage_order[i])
+ if i - 3 >= 0:
+ if new_stage_order[i - 3] == rname.castle_center:
+ alt_cc_stages.append(new_stage_order[i])
+ if i - 4 >= 0:
+ if new_stage_order[i - 4] == rname.castle_center:
+ alt_cc_stages.append(new_stage_order[i])
+
+ new_stage_order.append(rname.castle_keep)
+
+ # Update the dictionary of stage exits
+ current_stage_number = 1
+ for i in range(len(new_stage_order)):
+ # Stage position number and alternate path indicator
+ world.active_stage_exits[new_stage_order[i]]["position"] = current_stage_number
+ if new_stage_order[i] in alt_villa_stage + alt_cc_stages:
+ world.active_stage_exits[new_stage_order[i]]["path"] = "'"
+ else:
+ world.active_stage_exits[new_stage_order[i]]["path"] = " "
+
+ # Previous stage
+ if world.active_stage_exits[new_stage_order[i]]["prev"]:
+ if i - 1 < 0:
+ world.active_stage_exits[new_stage_order[i]]["prev"] = "Menu"
+ elif world.branching_stages:
+ if new_stage_order[i - 1] == alt_villa_stage[0] or new_stage_order[i] == alt_villa_stage[0]:
+ world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 2]
+ elif new_stage_order[i - 1] == alt_cc_stages[1] or new_stage_order[i] == alt_cc_stages[0]:
+ world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 3]
+ else:
+ world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 1]
+ else:
+ world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 1]
+
+ # Next stage
+ if world.active_stage_exits[new_stage_order[i]]["next"]:
+ if world.branching_stages:
+ if new_stage_order[i + 1] == alt_villa_stage[0]:
+ world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 2]
+ current_stage_number -= 1
+ elif new_stage_order[i + 1] == alt_cc_stages[0]:
+ world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 3]
+ current_stage_number -= 2
+ else:
+ world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 1]
+ else:
+ world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 1]
+
+ # Alternate next stage
+ if world.active_stage_exits[new_stage_order[i]]["alt"]:
+ if world.branching_stages:
+ if new_stage_order[i] == rname.villa:
+ world.active_stage_exits[new_stage_order[i]]["alt"] = alt_villa_stage[0]
+ else:
+ world.active_stage_exits[new_stage_order[i]]["alt"] = alt_cc_stages[0]
+ else:
+ world.active_stage_exits[new_stage_order[i]]["alt"] = None
+
+ current_stage_number += 1
+
+ return world.active_stage_exits, starting_stage, new_stage_order
+
+
+def generate_warps(world: "CV64World") -> List[str]:
+ # Create a list of warps from the active stage list. They are in a random order by default and will never
+ # include the starting stage.
+ possible_warps = [stage for stage in world.active_stage_list]
+
+ # Remove the starting stage from the possible warps.
+ del (possible_warps[0])
+
+ active_warp_list = world.random.sample(possible_warps, 7)
+
+ if world.options.warp_order == WarpOrder.option_seed_stage_order:
+ # Arrange the warps to be in the seed's stage order
+ new_list = world.active_stage_list.copy()
+ for warp in world.active_stage_list:
+ if warp not in active_warp_list:
+ new_list.remove(warp)
+ active_warp_list = new_list
+ elif world.options.warp_order == WarpOrder.option_vanilla_stage_order:
+ # Arrange the warps to be in the vanilla game's stage order
+ new_list = list(vanilla_stage_order)
+ for warp in vanilla_stage_order:
+ if warp not in active_warp_list:
+ new_list.remove(warp)
+ active_warp_list = new_list
+
+ # Insert the starting stage at the start of the warp list
+ active_warp_list.insert(0, world.active_stage_list[0])
+
+ return active_warp_list
+
+
+def get_region_names(active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> List[str]:
+ region_names = []
+ for stage in active_stage_exits:
+ stage_regions = get_stage_info(stage, "regions")
+ for region in stage_regions:
+ region_names.append(region)
+
+ return region_names
diff --git a/worlds/cv64/test/__init__.py b/worlds/cv64/test/__init__.py
new file mode 100644
index 000000000000..2d09e27cb316
--- /dev/null
+++ b/worlds/cv64/test/__init__.py
@@ -0,0 +1,6 @@
+from test.bases import WorldTestBase
+
+
+class CV64TestBase(WorldTestBase):
+ game = "Castlevania 64"
+ player: int = 1
diff --git a/worlds/cv64/test/test_access.py b/worlds/cv64/test/test_access.py
new file mode 100644
index 000000000000..79b1e14e11ea
--- /dev/null
+++ b/worlds/cv64/test/test_access.py
@@ -0,0 +1,250 @@
+from . import CV64TestBase
+
+
+class WarpTest(CV64TestBase):
+ options = {
+ "special1s_per_warp": 3,
+ "total_special1s": 21
+ }
+
+ def test_warps(self) -> None:
+ for i in range(1, 8):
+ self.assertFalse(self.can_reach_entrance(f"Warp {i}"))
+ self.collect([self.get_item_by_name("Special1")] * 2)
+ self.assertFalse(self.can_reach_entrance(f"Warp {i}"))
+ self.collect([self.get_item_by_name("Special1")] * 1)
+ self.assertTrue(self.can_reach_entrance(f"Warp {i}"))
+
+
+class CastleWallTest(CV64TestBase):
+ options = {
+ "stage_shuffle": True,
+ "starting_stage": 1
+ }
+
+ def test_doors(self) -> None:
+ self.assertFalse(self.can_reach_entrance(f"Left Tower door"))
+ self.collect([self.get_item_by_name("Left Tower Key")] * 1)
+ self.assertTrue(self.can_reach_entrance(f"Left Tower door"))
+
+
+class VillaTest(CV64TestBase):
+ options = {
+ "stage_shuffle": True,
+ "starting_stage": 2
+ }
+
+ def test_doors(self) -> None:
+ self.assertFalse(self.can_reach_entrance("To Storeroom door"))
+ self.collect([self.get_item_by_name("Storeroom Key")] * 1)
+ self.assertTrue(self.can_reach_entrance("To Storeroom door"))
+ self.assertFalse(self.can_reach_entrance("To Archives door"))
+ self.collect([self.get_item_by_name("Archives Key")] * 1)
+ self.assertTrue(self.can_reach_entrance("To Archives door"))
+ self.assertFalse(self.can_reach_entrance("To maze gate"))
+ self.assertFalse(self.can_reach_entrance("Copper door"))
+ self.collect([self.get_item_by_name("Garden Key")] * 1)
+ self.assertTrue(self.can_reach_entrance("To maze gate"))
+ self.assertFalse(self.can_reach_entrance("Copper door"))
+ self.collect([self.get_item_by_name("Copper Key")] * 1)
+ self.assertTrue(self.can_reach_entrance("Copper door"))
+
+
+class CastleCenterTest(CV64TestBase):
+ options = {
+ "stage_shuffle": True,
+ "starting_stage": 5
+ }
+
+ def test_doors(self) -> None:
+ self.assertFalse(self.can_reach_entrance("Torture Chamber door"))
+ self.collect([self.get_item_by_name("Chamber Key")] * 1)
+ self.assertTrue(self.can_reach_entrance("Torture Chamber door"))
+ self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
+ self.assertFalse(self.can_reach_entrance("Upper cracked wall"))
+ self.collect([self.get_item_by_name("Magical Nitro")] * 1)
+ self.assertFalse(self.can_reach_entrance("Upper cracked wall"))
+ self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
+ self.collect([self.get_item_by_name("Mandragora")] * 1)
+ self.assertTrue(self.can_reach_entrance("Upper cracked wall"))
+ self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
+ self.collect([self.get_item_by_name("Magical Nitro")] * 1)
+ self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
+ self.collect([self.get_item_by_name("Mandragora")] * 1)
+ self.assertTrue(self.can_reach_entrance("Upper cracked wall"))
+
+
+class ExecutionTest(CV64TestBase):
+ options = {
+ "stage_shuffle": True,
+ "starting_stage": 7
+ }
+
+ def test_doors(self) -> None:
+ self.assertFalse(self.can_reach_entrance("Execution gate"))
+ self.collect([self.get_item_by_name("Execution Key")] * 1)
+ self.assertTrue(self.can_reach_entrance("Execution gate"))
+
+
+class ScienceTest(CV64TestBase):
+ options = {
+ "stage_shuffle": True,
+ "starting_stage": 8
+ }
+
+ def test_doors(self) -> None:
+ self.assertFalse(self.can_reach_entrance("Science Door 1"))
+ self.collect([self.get_item_by_name("Science Key1")] * 1)
+ self.assertTrue(self.can_reach_entrance("Science Door 1"))
+ self.assertFalse(self.can_reach_entrance("To Science Door 2"))
+ self.assertFalse(self.can_reach_entrance("Science Door 3"))
+ self.collect([self.get_item_by_name("Science Key2")] * 1)
+ self.assertTrue(self.can_reach_entrance("To Science Door 2"))
+ self.assertFalse(self.can_reach_entrance("Science Door 3"))
+ self.collect([self.get_item_by_name("Science Key3")] * 1)
+ self.assertTrue(self.can_reach_entrance("Science Door 3"))
+
+
+class ClocktowerTest(CV64TestBase):
+ options = {
+ "stage_shuffle": True,
+ "starting_stage": 11
+ }
+
+ def test_doors(self) -> None:
+ self.assertFalse(self.can_reach_entrance("To Clocktower Door 1"))
+ self.assertFalse(self.can_reach_entrance("To Clocktower Door 2"))
+ self.assertFalse(self.can_reach_entrance("Clocktower Door 3"))
+ self.collect([self.get_item_by_name("Clocktower Key1")] * 1)
+ self.assertTrue(self.can_reach_entrance("To Clocktower Door 1"))
+ self.assertFalse(self.can_reach_entrance("To Clocktower Door 2"))
+ self.assertFalse(self.can_reach_entrance("Clocktower Door 3"))
+ self.collect([self.get_item_by_name("Clocktower Key2")] * 1)
+ self.assertTrue(self.can_reach_entrance("To Clocktower Door 2"))
+ self.assertFalse(self.can_reach_entrance("Clocktower Door 3"))
+ self.collect([self.get_item_by_name("Clocktower Key3")] * 1)
+ self.assertTrue(self.can_reach_entrance("Clocktower Door 3"))
+
+
+class DraculaNoneTest(CV64TestBase):
+ options = {
+ "draculas_condition": 0,
+ "stage_shuffle": True,
+ "starting_stage": 5,
+ }
+
+ def test_dracula_none_condition(self) -> None:
+ self.assertFalse(self.can_reach_entrance("Dracula's door"))
+ self.collect([self.get_item_by_name("Left Tower Key"),
+ self.get_item_by_name("Garden Key"),
+ self.get_item_by_name("Copper Key"),
+ self.get_item_by_name("Science Key1"),
+ self.get_item_by_name("Science Key2"),
+ self.get_item_by_name("Science Key3"),
+ self.get_item_by_name("Clocktower Key1"),
+ self.get_item_by_name("Clocktower Key2"),
+ self.get_item_by_name("Clocktower Key3")] * 1)
+ self.assertFalse(self.can_reach_entrance("Dracula's door"))
+ self.collect([self.get_item_by_name("Special1")] * 7)
+ self.assertTrue(self.can_reach_entrance("Dracula's door"))
+
+
+class DraculaSpecialTest(CV64TestBase):
+ options = {
+ "draculas_condition": 3
+ }
+
+ def test_dracula_special_condition(self) -> None:
+ self.assertFalse(self.can_reach_entrance("Clocktower Door 3"))
+ self.collect([self.get_item_by_name("Left Tower Key"),
+ self.get_item_by_name("Garden Key"),
+ self.get_item_by_name("Copper Key"),
+ self.get_item_by_name("Magical Nitro"),
+ self.get_item_by_name("Mandragora"),
+ self.get_item_by_name("Clocktower Key1"),
+ self.get_item_by_name("Clocktower Key2"),
+ self.get_item_by_name("Clocktower Key3")] * 2)
+ self.assertTrue(self.can_reach_entrance("Clocktower Door 3"))
+ self.assertFalse(self.can_reach_entrance("Dracula's door"))
+ self.collect([self.get_item_by_name("Special2")] * 19)
+ self.assertFalse(self.can_reach_entrance("Dracula's door"))
+ self.collect([self.get_item_by_name("Special2")] * 1)
+ self.assertTrue(self.can_reach_entrance("Dracula's door"))
+
+
+class DraculaCrystalTest(CV64TestBase):
+ options = {
+ "draculas_condition": 1,
+ "stage_shuffle": True,
+ "starting_stage": 5,
+ "hard_logic": True
+ }
+
+ def test_dracula_crystal_condition(self) -> None:
+ self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower"))
+ self.collect([self.get_item_by_name("Left Tower Key"),
+ self.get_item_by_name("Garden Key"),
+ self.get_item_by_name("Copper Key"),
+ self.get_item_by_name("Science Key1"),
+ self.get_item_by_name("Science Key2"),
+ self.get_item_by_name("Science Key3"),
+ self.get_item_by_name("Clocktower Key1"),
+ self.get_item_by_name("Clocktower Key2"),
+ self.get_item_by_name("Clocktower Key3")] * 1)
+ self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower"))
+ self.collect([self.get_item_by_name("Special1")] * 7)
+ self.assertTrue(self.can_reach_entrance("Slope Jump to boss tower"))
+ self.assertFalse(self.can_reach_entrance("Dracula's door"))
+ self.collect([self.get_item_by_name("Magical Nitro"),
+ self.get_item_by_name("Mandragora")] * 1)
+ self.assertFalse(self.can_reach_entrance("Dracula's door"))
+ self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
+ self.collect([self.get_item_by_name("Magical Nitro"),
+ self.get_item_by_name("Mandragora")] * 1)
+ self.assertTrue(self.can_reach_entrance("Lower sealed cracked wall"))
+ self.assertTrue(self.can_reach_entrance("Dracula's door"))
+
+
+class DraculaBossTest(CV64TestBase):
+ options = {
+ "draculas_condition": 2,
+ "stage_shuffle": True,
+ "starting_stage": 5,
+ "hard_logic": True,
+ "bosses_required": 16
+ }
+
+ def test_dracula_boss_condition(self) -> None:
+ self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower"))
+ self.collect([self.get_item_by_name("Left Tower Key"),
+ self.get_item_by_name("Garden Key"),
+ self.get_item_by_name("Copper Key"),
+ self.get_item_by_name("Science Key1"),
+ self.get_item_by_name("Science Key2"),
+ self.get_item_by_name("Science Key3"),
+ self.get_item_by_name("Clocktower Key1"),
+ self.get_item_by_name("Clocktower Key2"),
+ self.get_item_by_name("Clocktower Key3")] * 1)
+ self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower"))
+ self.collect([self.get_item_by_name("Special1")] * 7)
+ self.assertTrue(self.can_reach_entrance("Slope Jump to boss tower"))
+ self.assertFalse(self.can_reach_entrance("Dracula's door"))
+ self.collect([self.get_item_by_name("Magical Nitro"),
+ self.get_item_by_name("Mandragora")] * 1)
+ self.assertFalse(self.can_reach_entrance("Dracula's door"))
+ self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
+ self.collect([self.get_item_by_name("Magical Nitro"),
+ self.get_item_by_name("Mandragora")] * 1)
+ self.assertTrue(self.can_reach_entrance("Lower sealed cracked wall"))
+ self.assertTrue(self.can_reach_entrance("Dracula's door"))
+
+
+class LizardTest(CV64TestBase):
+ options = {
+ "stage_shuffle": True,
+ "draculas_condition": 2,
+ "starting_stage": 4
+ }
+
+ def test_lizard_man_trio(self) -> None:
+ self.assertTrue(self.can_reach_location("Underground Waterway: Lizard-man trio"))
diff --git a/worlds/cv64/text.py b/worlds/cv64/text.py
new file mode 100644
index 000000000000..3ba0b9153e9c
--- /dev/null
+++ b/worlds/cv64/text.py
@@ -0,0 +1,98 @@
+from typing import Tuple
+
+cv64_char_dict = {"\n": (0x01, 0), " ": (0x02, 4), "!": (0x03, 2), '"': (0x04, 5), "#": (0x05, 6), "$": (0x06, 5),
+ "%": (0x07, 8), "&": (0x08, 7), "'": (0x09, 4), "(": (0x0A, 3), ")": (0x0B, 3), "*": (0x0C, 4),
+ "+": (0x0D, 5), ",": (0x0E, 3), "-": (0x0F, 4), ".": (0x10, 3), "/": (0x11, 6), "0": (0x12, 5),
+ "1": (0x13, 3), "2": (0x14, 5), "3": (0x15, 4), "4": (0x16, 5), "5": (0x17, 5), "6": (0x18, 5),
+ "7": (0x19, 5), "8": (0x1A, 5), "9": (0x1B, 5), ":": (0x1C, 3), ";": (0x1D, 3), "<": (0x1E, 3),
+ "=": (0x1F, 4), ">": (0x20, 3), "?": (0x21, 5), "@": (0x22, 8), "A": (0x23, 7), "B": (0x24, 6),
+ "C": (0x25, 5), "D": (0x26, 7), "E": (0x27, 5), "F": (0x28, 6), "G": (0x29, 6), "H": (0x2A, 7),
+ "I": (0x2B, 3), "J": (0x2C, 3), "K": (0x2D, 6), "L": (0x2E, 6), "M": (0x2F, 8), "N": (0x30, 7),
+ "O": (0x31, 6), "P": (0x32, 6), "Q": (0x33, 8), "R": (0x34, 6), "S": (0x35, 5), "T": (0x36, 6),
+ "U": (0x37, 6), "V": (0x38, 7), "W": (0x39, 8), "X": (0x3A, 6), "Y": (0x3B, 7), "Z": (0x3C, 6),
+ "[": (0x3D, 3), "\\": (0x3E, 6), "]": (0x3F, 3), "^": (0x40, 6), "_": (0x41, 5), "a": (0x43, 5),
+ "b": (0x44, 6), "c": (0x45, 4), "d": (0x46, 6), "e": (0x47, 5), "f": (0x48, 5), "g": (0x49, 5),
+ "h": (0x4A, 6), "i": (0x4B, 3), "j": (0x4C, 3), "k": (0x4D, 6), "l": (0x4E, 3), "m": (0x4F, 8),
+ "n": (0x50, 6), "o": (0x51, 5), "p": (0x52, 5), "q": (0x53, 5), "r": (0x54, 4), "s": (0x55, 4),
+ "t": (0x56, 4), "u": (0x57, 5), "v": (0x58, 6), "w": (0x59, 8), "x": (0x5A, 5), "y": (0x5B, 5),
+ "z": (0x5C, 4), "{": (0x5D, 4), "|": (0x5E, 2), "}": (0x5F, 3), "`": (0x61, 4), "「": (0x62, 3),
+ "ã€": (0x63, 3), "~": (0x65, 3), "″": (0x72, 3), "°": (0x73, 3), "∞": (0x74, 8)}
+# [0] = CV64's in-game ID for that text character.
+# [1] = How much space towards the in-game line length limit it contributes.
+
+
+def cv64_string_to_bytearray(cv64text: str, a_advance: bool = False, append_end: bool = True) -> bytearray:
+ """Converts a string into a bytearray following CV64's string format."""
+ text_bytes = bytearray(0)
+ for i, char in enumerate(cv64text):
+ if char == "\t":
+ text_bytes.extend([0xFF, 0xFF])
+ else:
+ if char in cv64_char_dict:
+ text_bytes.extend([0x00, cv64_char_dict[char][0]])
+ else:
+ text_bytes.extend([0x00, 0x21])
+
+ if a_advance:
+ text_bytes.extend([0xA3, 0x00])
+ if append_end:
+ text_bytes.extend([0xFF, 0xFF])
+ return text_bytes
+
+
+def cv64_text_truncate(cv64text: str, textbox_len_limit: int) -> str:
+ """Truncates a string at a given in-game text line length."""
+ line_len = 0
+
+ for i in range(len(cv64text)):
+ if cv64text[i] in cv64_char_dict:
+ line_len += cv64_char_dict[cv64text[i]][1]
+ else:
+ line_len += 5
+
+ if line_len > textbox_len_limit:
+ return cv64text[0x00:i]
+
+ return cv64text
+
+
+def cv64_text_wrap(cv64text: str, textbox_len_limit: int) -> Tuple[str, int]:
+ """Rebuilds a string with some of its spaces replaced with newlines to ensure the text wraps properly in an in-game
+ textbox of a given length."""
+ words = cv64text.split(" ")
+ new_text = ""
+ line_len = 0
+ num_lines = 1
+
+ for i in range(len(words)):
+ word_len = 0
+ word_divider = " "
+
+ if line_len != 0:
+ line_len += 4
+ else:
+ word_divider = ""
+
+ for char in words[i]:
+ if char in cv64_char_dict:
+ line_len += cv64_char_dict[char][1]
+ word_len += cv64_char_dict[char][1]
+ else:
+ line_len += 5
+ word_len += 5
+
+ if word_len > textbox_len_limit or char in ["\n", "\t"]:
+ word_len = 0
+ line_len = 0
+ if num_lines < 4:
+ num_lines += 1
+
+ if line_len > textbox_len_limit:
+ word_divider = "\n"
+ line_len = word_len
+ if num_lines < 4:
+ num_lines += 1
+
+ new_text += word_divider + words[i]
+
+ return new_text, num_lines
diff --git a/worlds/dark_souls_3/Items.py b/worlds/dark_souls_3/Items.py
index 754282e73647..3dd5cb2d3c3f 100644
--- a/worlds/dark_souls_3/Items.py
+++ b/worlds/dark_souls_3/Items.py
@@ -1271,6 +1271,10 @@ def get_name_to_id() -> dict:
("Dorris Swarm", 0x40393870, DS3ItemCategory.SKIP),
]]
+item_descriptions = {
+ "Cinders": "All four Cinders of a Lord.\n\nOnce you have these four, you can fight Soul of Cinder and win the game.",
+}
+
_all_items = _vanilla_items + _dlc_items
item_dictionary = {item_data.name: item_data for item_data in _all_items}
diff --git a/worlds/dark_souls_3/Locations.py b/worlds/dark_souls_3/Locations.py
index 4e595ad36ac5..df241a5fd1fb 100644
--- a/worlds/dark_souls_3/Locations.py
+++ b/worlds/dark_souls_3/Locations.py
@@ -77,6 +77,7 @@ def get_name_to_id() -> dict:
"Progressive Items 3",
"Progressive Items 4",
"Progressive Items DLC",
+ "Progressive Items Health",
]
output = {}
@@ -581,11 +582,7 @@ def get_name_to_id() -> dict:
[DS3LocationData(f"Titanite Shard #{i + 1}", "Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(26)] +
[DS3LocationData(f"Large Titanite Shard #{i + 1}", "Large Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(28)] +
[DS3LocationData(f"Titanite Slab #{i + 1}", "Titanite Slab", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] +
- [DS3LocationData(f"Twinkling Titanite #{i + 1}", "Twinkling Titanite", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(15)] +
-
- # Healing
- [DS3LocationData(f"Estus Shard #{i + 1}", "Estus Shard", DS3LocationCategory.HEALTH) for i in range(11)] +
- [DS3LocationData(f"Undead Bone Shard #{i + 1}", "Undead Bone Shard", DS3LocationCategory.HEALTH) for i in range(10)],
+ [DS3LocationData(f"Twinkling Titanite #{i + 1}", "Twinkling Titanite", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(15)],
"Progressive Items 2": [] +
# Items
@@ -683,7 +680,12 @@ def get_name_to_id() -> dict:
[DS3LocationData(f"Dark Gem ${i + 1}", "Dark Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
[DS3LocationData(f"Blood Gem ${i + 1}", "Blood Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] +
[DS3LocationData(f"Blessed Gem ${i + 1}", "Blessed Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] +
- [DS3LocationData(f"Hollow Gem ${i + 1}", "Hollow Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)]
+ [DS3LocationData(f"Hollow Gem ${i + 1}", "Hollow Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)],
+
+ "Progressive Items Health": [] +
+ # Healing
+ [DS3LocationData(f"Estus Shard #{i + 1}", "Estus Shard", DS3LocationCategory.HEALTH) for i in range(11)] +
+ [DS3LocationData(f"Undead Bone Shard #{i + 1}", "Undead Bone Shard", DS3LocationCategory.HEALTH) for i in range(10)],
}
location_dictionary: Dict[str, DS3LocationData] = {}
diff --git a/worlds/dark_souls_3/Options.py b/worlds/dark_souls_3/Options.py
index d613e4733406..df0bb953b8d9 100644
--- a/worlds/dark_souls_3/Options.py
+++ b/worlds/dark_souls_3/Options.py
@@ -171,6 +171,16 @@ class MaxLevelsIn10WeaponPoolOption(Range):
default = 10
+class EarlySmallLothricBanner(Choice):
+ """This option makes it so the user can choose to force the Small Lothric Banner into an early sphere in their world or
+ into an early sphere across all worlds."""
+ display_name = "Early Small Lothric Banner"
+ option_off = 0
+ option_early_global = 1
+ option_early_local = 2
+ default = option_off
+
+
class LateBasinOfVowsOption(Toggle):
"""This option makes it so the Basin of Vows is still randomized, but guarantees you that you wont have to venture into
Lothric Castle to find your Small Lothric Banner to get out of High Wall of Lothric. So you may find Basin of Vows early,
@@ -215,6 +225,7 @@ class EnableDLCOption(Toggle):
"max_levels_in_5": MaxLevelsIn5WeaponPoolOption,
"min_levels_in_10": MinLevelsIn10WeaponPoolOption,
"max_levels_in_10": MaxLevelsIn10WeaponPoolOption,
+ "early_banner": EarlySmallLothricBanner,
"late_basin_of_vows": LateBasinOfVowsOption,
"late_dlc": LateDLCOption,
"no_spell_requirements": NoSpellRequirementsOption,
diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py
index 5d845e3ccce1..020010981160 100644
--- a/worlds/dark_souls_3/__init__.py
+++ b/worlds/dark_souls_3/__init__.py
@@ -7,15 +7,16 @@
from worlds.AutoWorld import World, WebWorld
from worlds.generic.Rules import set_rule, add_rule, add_item_rule
-from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names
+from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names, item_descriptions
from .Locations import DarkSouls3Location, DS3LocationCategory, location_tables, location_dictionary
-from .Options import RandomizeWeaponLevelOption, PoolTypeOption, dark_souls_options
+from .Options import RandomizeWeaponLevelOption, PoolTypeOption, EarlySmallLothricBanner, dark_souls_options
class DarkSouls3Web(WebWorld):
bug_report_page = "https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/issues"
+ theme = "stone"
setup_en = Tutorial(
- "Multiworld Setup Tutorial",
+ "Multiworld Setup Guide",
"A guide to setting up the Archipelago Dark Souls III randomizer on your computer.",
"English",
"setup_en.md",
@@ -34,6 +35,8 @@ class DarkSouls3Web(WebWorld):
tutorials = [setup_en, setup_fr]
+ item_descriptions = item_descriptions
+
class DarkSouls3World(World):
"""
@@ -46,13 +49,19 @@ class DarkSouls3World(World):
option_definitions = dark_souls_options
topology_present: bool = True
web = DarkSouls3Web()
- data_version = 7
base_id = 100000
enabled_location_categories: Set[DS3LocationCategory]
required_client_version = (0, 4, 2)
item_name_to_id = DarkSouls3Item.get_name_to_id()
location_name_to_id = DarkSouls3Location.get_name_to_id()
-
+ item_name_groups = {
+ "Cinders": {
+ "Cinders of a Lord - Abyss Watcher",
+ "Cinders of a Lord - Aldrich",
+ "Cinders of a Lord - Yhorm the Giant",
+ "Cinders of a Lord - Lothric Prince"
+ }
+ }
def __init__(self, multiworld: MultiWorld, player: int):
super().__init__(multiworld, player)
@@ -77,6 +86,10 @@ def generate_early(self):
self.enabled_location_categories.add(DS3LocationCategory.NPC)
if self.multiworld.enable_key_locations[self.player] == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.KEY)
+ if self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_global:
+ self.multiworld.early_items[self.player]['Small Lothric Banner'] = 1
+ elif self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_local:
+ self.multiworld.local_early_items[self.player]['Small Lothric Banner'] = 1
if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true:
self.enabled_location_categories.add(DS3LocationCategory.BOSS)
if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true:
@@ -89,7 +102,7 @@ def generate_early(self):
def create_regions(self):
progressive_location_table = []
- if self.multiworld.enable_progressive_locations[self.player].value:
+ if self.multiworld.enable_progressive_locations[self.player]:
progressive_location_table = [] + \
location_tables["Progressive Items 1"] + \
location_tables["Progressive Items 2"] + \
@@ -99,6 +112,9 @@ def create_regions(self):
if self.multiworld.enable_dlc[self.player].value:
progressive_location_table += location_tables["Progressive Items DLC"]
+ if self.multiworld.enable_health_upgrade_locations[self.player]:
+ progressive_location_table += location_tables["Progressive Items Health"]
+
# Create Vanilla Regions
regions: Dict[str, Region] = {}
regions["Menu"] = self.create_region("Menu", progressive_location_table)
diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md
index e844925df1ea..f31358bb9c2f 100644
--- a/worlds/dark_souls_3/docs/en_Dark Souls III.md
+++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md
@@ -1,8 +1,8 @@
# Dark Souls III
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -13,7 +13,7 @@ location "Titanite Shard #5" is the fifth titanite shard you pick up, no matter
happens when you randomize Estus Shards and Undead Bone Shards.
It's also possible to randomize the upgrade level of weapons and shields as well as their infusions (if they can have
-one). Additionally, there are settings that can make the randomized experience more convenient or more interesting, such as
+one). Additionally, there are options that can make the randomized experience more convenient or more interesting, such as
removing weapon requirements or auto-equipping whatever equipment you most recently received.
The goal is to find the four "Cinders of a Lord" items randomized into the multiworld and defeat the Soul of Cinder.
diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md
index d9dbb2e54729..61215dbc6043 100644
--- a/worlds/dark_souls_3/docs/setup_en.md
+++ b/worlds/dark_souls_3/docs/setup_en.md
@@ -11,7 +11,7 @@
## General Concept
-
+
**This mod can ban you permanently from the FromSoftware servers if used online.**
The Dark Souls III AP Client is a dinput8.dll triggered when launching Dark Souls III. This .dll file will launch a command
@@ -21,22 +21,34 @@ This client has only been tested with the Official Steam version of the game at
## Downpatching Dark Souls III
-Follow instructions from the [speedsouls wiki](https://wiki.speedsouls.com/darksouls3:Downpatching) to download version 1.15. Your download command, including the correct depot and manifest ids, will be "download_depot 374320 374321 4471176929659548333"
+To downpatch DS3 for use with Archipelago, use the following instructions from the speedsouls wiki database.
+
+1. Launch Steam (in online mode).
+2. Press the Windows Key + R. This will open the Run window.
+3. Open the Steam console by typing the following string: `steam://open/console`. Steam should now open in Console Mode.
+4. Insert the string of the depot you wish to download. For the AP-supported v1.15, you will want to use: `download_depot 374320 374321 4471176929659548333`.
+5. Steam will now download the depot. Note: There is no progress bar for the download in Steam, but it is still downloading in the background.
+6. Back up your existing game executable (`DarkSoulsIII.exe`) found in `\Steam\steamapps\common\DARK SOULS III\Game`. Easiest way to do this is to move it to another directory. If you have file extensions enabled, you can instead rename the executable to `DarkSoulsIII.exe.bak`.
+7. Return to the Steam console. Once the download is complete, it should say so along with the temporary local directory in which the depot has been stored. This is usually something like `\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX`.
+8. Take the `DarkSoulsIII.exe` from that folder and place it in `\Steam\steamapps\common\DARK SOULS III\Game`.
+9. Back up and delete your save file (`DS30000.sl2`) in AppData. AppData is hidden by default. To locate it, press Windows Key + R, type `%appdata%` and hit enter. Alternatively: open File Explorer > View > Hidden Items and follow `C:\Users\\AppData\Roaming\DarkSoulsIII\`.
+10. If you did all these steps correctly, you should be able to confirm your game version in the upper-left corner after launching Dark Souls III.
+
## Installing the Archipelago mod
-Get the dinput8.dll from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) and
-add it at the root folder of your game (e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game")
+Get the `dinput8.dll` from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) and
+add it at the root folder of your game (e.g. `SteamLibrary\steamapps\common\DARK SOULS III\Game`)
## Joining a MultiWorld Game
-1. Run Steam in offline mode, both to avoid being banned and to prevent Steam from updating the game files
-2. Launch Dark Souls III
-3. Type in "/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}" in the "Windows Command Prompt" that opened
-4. Once connected, create a new game, choose a class and wait for the others before starting
-5. You can quit and launch at anytime during a game
+1. Run Steam in offline mode to avoid being banned.
+2. Launch Dark Souls III.
+3. Type in `/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME} password:{PASSWORD}` in the "Windows Command Prompt" that opened. For example: `/connect archipelago.gg:38281 "Example Name" password:"Example Password"`. The password parameter is only necessary if your game requires one.
+4. Once connected, create a new game, choose a class and wait for the others before starting.
+5. You can quit and launch at anytime during a game.
## Where do I get a config file?
-The [Player Settings](/games/Dark%20Souls%20III/player-settings) page on the website allows you to
-configure your personal settings and export them into a config file.
+The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to
+configure your personal options and export them into a config file.
diff --git a/worlds/dark_souls_3/docs/setup_fr.md b/worlds/dark_souls_3/docs/setup_fr.md
index 6ad86c4aff13..ea4d8f818604 100644
--- a/worlds/dark_souls_3/docs/setup_fr.md
+++ b/worlds/dark_souls_3/docs/setup_fr.md
@@ -12,7 +12,7 @@ permettant de lire des informations de la partie et écrire des commandes pour i
## Procédures d'installation
-
+
**Il y a des risques de bannissement permanent des serveurs FromSoftware si ce mod est utilisé en ligne.**
Ce client a été testé sur la version Steam officielle du jeu (v1.15/1.35), peu importe les DLCs actuellement installés.
@@ -29,5 +29,5 @@ placez-le à la racine du jeu (ex: "SteamLibrary\steamapps\common\DARK SOULS III
## Où trouver le fichier de configuration ?
-La [Page de configuration](/games/Dark%20Souls%20III/player-settings) sur le site vous permez de configurer vos
+La [Page de configuration](/games/Dark%20Souls%20III/player-options) sur le site vous permez de configurer vos
paramètres et de les exporter sous la forme d'un fichier.
diff --git a/worlds/dkc3/CHANGELOG.md b/worlds/dkc3/CHANGELOG.md
new file mode 100644
index 000000000000..dc853a5aa944
--- /dev/null
+++ b/worlds/dkc3/CHANGELOG.md
@@ -0,0 +1,47 @@
+# Donkey Kong Country 3 - Changelog
+
+
+## v1.1
+
+### Features:
+
+- KONGsanity option (Collect all KONG letters in each level for a check)
+- Autosave option
+- Difficulty option
+- MERRY option
+- Handle collected/co-op locations
+
+### Bug Fixes:
+
+- Fixed Mekanos softlock
+- Prevent Brothers Bear giving extra Banana Birds
+- Fixed Banana Bird Mother check sending prematurely
+- Fix Logic bug with Krematoa level costs
+
+
+## v1.0
+
+### Features:
+
+- Goal
+ - Knautilus
+ - Scuttle the Knautilus in Krematoa and defeat Baron K. Roolenstein to win
+ - Banana Bird Hunt
+ - Find the Banana Birds and rescue their mother to win
+- Locations included:
+ - Level Flags
+ - Bonuses
+ - DK Coins
+ - Banana Bird Caves
+- Items included:
+ - Progressive Boat Upgrade
+ - Three are placed into the item pool (Patch -> First Ski -> Second Ski)
+ - Bonus Coins
+ - DK Coins
+ - Krematoa Cogs
+ - Bear Coins
+ - 1-Up Balloons
+- Level Shuffle is supported
+- Music Shuffle is supported
+- Kong Palette options are supported
+- Starting life count can be set
diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py
index 77ed51fecbd0..ee2bd1dbdfb8 100644
--- a/worlds/dkc3/Client.py
+++ b/worlds/dkc3/Client.py
@@ -24,6 +24,7 @@
class DKC3SNIClient(SNIClient):
game = "Donkey Kong Country 3"
+ patch_suffix = ".apdkc3"
async def deathlink_kill_player(self, ctx):
pass
@@ -59,7 +60,7 @@ async def game_watcher(self, ctx):
return
new_checks = []
- from worlds.dkc3.Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map
+ from .Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map
location_ram_data = await snes_read(ctx, WRAM_START + 0x5FE, 0x81)
for loc_id, loc_data in location_rom_data.items():
if loc_id not in ctx.locations_checked:
@@ -85,7 +86,7 @@ async def game_watcher(self, ctx):
for new_check_id in new_checks:
ctx.locations_checked.add(new_check_id)
- location = ctx.location_names[new_check_id]
+ location = ctx.location_names.lookup_in_game(new_check_id)
snes_logger.info(
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
@@ -98,9 +99,9 @@ async def game_watcher(self, ctx):
item = ctx.items_received[recv_index]
recv_index += 1
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
- color(ctx.item_names[item.item], 'red', 'bold'),
+ color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'),
color(ctx.player_names[item.player], 'yellow'),
- ctx.location_names[item.location], recv_index, len(ctx.items_received)))
+ ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received)))
snes_buffered_write(ctx, DKC3_RECV_PROGRESS_ADDR, bytes([recv_index]))
if item.item in item_rom_data:
diff --git a/worlds/dkc3/Locations.py b/worlds/dkc3/Locations.py
index e8d5409b1563..6d8833872b03 100644
--- a/worlds/dkc3/Locations.py
+++ b/worlds/dkc3/Locations.py
@@ -2,6 +2,7 @@
from BaseClasses import Location
from .Names import LocationName
+from worlds.AutoWorld import World
class DKC3Location(Location):
@@ -321,13 +322,13 @@ def __init__(self, player: int, name: str = '', address: int = None, parent=None
location_table = {}
-def setup_locations(world, player: int):
+def setup_locations(world: World):
location_table = {**level_location_table, **boss_location_table, **secret_cave_location_table}
- if False:#world.include_trade_sequence[player].value:
+ if False:#world.options.include_trade_sequence:
location_table.update({**brothers_bear_location_table})
- if world.kongsanity[player].value:
+ if world.options.kongsanity:
location_table.update({**kong_location_table})
return location_table
diff --git a/worlds/dkc3/Names/LocationName.py b/worlds/dkc3/Names/LocationName.py
index f79a25f143de..dbd63623ab22 100644
--- a/worlds/dkc3/Names/LocationName.py
+++ b/worlds/dkc3/Names/LocationName.py
@@ -294,7 +294,7 @@
blue_region = "Blue's Beach Hut Region"
blizzard_region = "Bizzard's Basecamp Region"
-lake_orangatanga_region = "Lake_Orangatanga"
+lake_orangatanga_region = "Lake Orangatanga"
kremwood_forest_region = "Kremwood Forest"
cotton_top_cove_region = "Cotton-Top Cove"
mekanos_region = "Mekanos"
diff --git a/worlds/dkc3/Options.py b/worlds/dkc3/Options.py
index 7c0f532cfc76..b114a503b982 100644
--- a/worlds/dkc3/Options.py
+++ b/worlds/dkc3/Options.py
@@ -1,12 +1,15 @@
+from dataclasses import dataclass
import typing
-from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList
+from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions
class Goal(Choice):
"""
Determines the goal of the seed
+
Knautilus: Scuttle the Knautilus in Krematoa and defeat Baron K. Roolenstein
+
Banana Bird Hunt: Find a certain number of Banana Birds and rescue their mother
"""
display_name = "Goal"
@@ -25,6 +28,7 @@ class IncludeTradeSequence(Toggle):
class DKCoinsForGyrocopter(Range):
"""
How many DK Coins are needed to unlock the Gyrocopter
+
Note: Achieving this number before unlocking the Turbo Ski will cause the game to grant you a
one-time upgrade to the next non-unlocked boat, until you return to Funky. Logic does not assume
that you will use this.
@@ -92,6 +96,7 @@ class LevelShuffle(Toggle):
class Difficulty(Choice):
"""
Which Difficulty Level to use
+
NORML: The Normal Difficulty
HARDR: Many DK Barrels are removed
TUFST: Most DK Barrels and all Midway Barrels are removed
@@ -158,21 +163,42 @@ class StartingLifeCount(Range):
default = 5
-dkc3_options: typing.Dict[str, type(Option)] = {
- #"death_link": DeathLink, # Disabled
- "goal": Goal,
- #"include_trade_sequence": IncludeTradeSequence, # Disabled
- "dk_coins_for_gyrocopter": DKCoinsForGyrocopter,
- "krematoa_bonus_coin_cost": KrematoaBonusCoinCost,
- "percentage_of_extra_bonus_coins": PercentageOfExtraBonusCoins,
- "number_of_banana_birds": NumberOfBananaBirds,
- "percentage_of_banana_birds": PercentageOfBananaBirds,
- "kongsanity": KONGsanity,
- "level_shuffle": LevelShuffle,
- "difficulty": Difficulty,
- "autosave": Autosave,
- "merry": MERRY,
- "music_shuffle": MusicShuffle,
- "kong_palette_swap": KongPaletteSwap,
- "starting_life_count": StartingLifeCount,
-}
+dkc3_option_groups = [
+ OptionGroup("Goal Options", [
+ Goal,
+ KrematoaBonusCoinCost,
+ PercentageOfExtraBonusCoins,
+ NumberOfBananaBirds,
+ PercentageOfBananaBirds,
+ ]),
+ OptionGroup("Aesthetics", [
+ Autosave,
+ MERRY,
+ MusicShuffle,
+ KongPaletteSwap,
+ StartingLifeCount,
+ ]),
+]
+
+
+@dataclass
+class DKC3Options(PerGameCommonOptions):
+ #death_link: DeathLink # Disabled
+ #include_trade_sequence: IncludeTradeSequence # Disabled
+
+ goal: Goal
+ krematoa_bonus_coin_cost: KrematoaBonusCoinCost
+ percentage_of_extra_bonus_coins: PercentageOfExtraBonusCoins
+ number_of_banana_birds: NumberOfBananaBirds
+ percentage_of_banana_birds: PercentageOfBananaBirds
+
+ dk_coins_for_gyrocopter: DKCoinsForGyrocopter
+ kongsanity: KONGsanity
+ level_shuffle: LevelShuffle
+ difficulty: Difficulty
+
+ autosave: Autosave
+ merry: MERRY
+ music_shuffle: MusicShuffle
+ kong_palette_swap: KongPaletteSwap
+ starting_life_count: StartingLifeCount
diff --git a/worlds/dkc3/Regions.py b/worlds/dkc3/Regions.py
index ca6545ca14cc..ae505b78d84b 100644
--- a/worlds/dkc3/Regions.py
+++ b/worlds/dkc3/Regions.py
@@ -4,38 +4,39 @@
from .Items import DKC3Item
from .Locations import DKC3Location
from .Names import LocationName, ItemName
+from worlds.AutoWorld import World
-def create_regions(world, player: int, active_locations):
- menu_region = create_region(world, player, active_locations, 'Menu', None)
+def create_regions(world: World, active_locations):
+ menu_region = create_region(world, active_locations, 'Menu', None)
overworld_1_region_locations = {}
- if world.goal[player] != "knautilus":
+ if world.options.goal != "knautilus":
overworld_1_region_locations.update({LocationName.banana_bird_mother: []})
- overworld_1_region = create_region(world, player, active_locations, LocationName.overworld_1_region,
+ overworld_1_region = create_region(world, active_locations, LocationName.overworld_1_region,
overworld_1_region_locations)
overworld_2_region_locations = {}
- overworld_2_region = create_region(world, player, active_locations, LocationName.overworld_2_region,
+ overworld_2_region = create_region(world, active_locations, LocationName.overworld_2_region,
overworld_2_region_locations)
overworld_3_region_locations = {}
- overworld_3_region = create_region(world, player, active_locations, LocationName.overworld_3_region,
+ overworld_3_region = create_region(world, active_locations, LocationName.overworld_3_region,
overworld_3_region_locations)
overworld_4_region_locations = {}
- overworld_4_region = create_region(world, player, active_locations, LocationName.overworld_4_region,
+ overworld_4_region = create_region(world, active_locations, LocationName.overworld_4_region,
overworld_4_region_locations)
- lake_orangatanga_region = create_region(world, player, active_locations, LocationName.lake_orangatanga_region, None)
- kremwood_forest_region = create_region(world, player, active_locations, LocationName.kremwood_forest_region, None)
- cotton_top_cove_region = create_region(world, player, active_locations, LocationName.cotton_top_cove_region, None)
- mekanos_region = create_region(world, player, active_locations, LocationName.mekanos_region, None)
- k3_region = create_region(world, player, active_locations, LocationName.k3_region, None)
- razor_ridge_region = create_region(world, player, active_locations, LocationName.razor_ridge_region, None)
- kaos_kore_region = create_region(world, player, active_locations, LocationName.kaos_kore_region, None)
- krematoa_region = create_region(world, player, active_locations, LocationName.krematoa_region, None)
+ lake_orangatanga_region = create_region(world, active_locations, LocationName.lake_orangatanga_region, None)
+ kremwood_forest_region = create_region(world, active_locations, LocationName.kremwood_forest_region, None)
+ cotton_top_cove_region = create_region(world, active_locations, LocationName.cotton_top_cove_region, None)
+ mekanos_region = create_region(world, active_locations, LocationName.mekanos_region, None)
+ k3_region = create_region(world, active_locations, LocationName.k3_region, None)
+ razor_ridge_region = create_region(world, active_locations, LocationName.razor_ridge_region, None)
+ kaos_kore_region = create_region(world, active_locations, LocationName.kaos_kore_region, None)
+ krematoa_region = create_region(world, active_locations, LocationName.krematoa_region, None)
lakeside_limbo_region_locations = {
@@ -44,9 +45,9 @@ def create_regions(world, player: int, active_locations):
LocationName.lakeside_limbo_bonus_2 : [0x657, 3],
LocationName.lakeside_limbo_dk : [0x657, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
lakeside_limbo_region_locations[LocationName.lakeside_limbo_kong] = []
- lakeside_limbo_region = create_region(world, player, active_locations, LocationName.lakeside_limbo_region,
+ lakeside_limbo_region = create_region(world, active_locations, LocationName.lakeside_limbo_region,
lakeside_limbo_region_locations)
doorstop_dash_region_locations = {
@@ -55,9 +56,9 @@ def create_regions(world, player: int, active_locations):
LocationName.doorstop_dash_bonus_2 : [0x65A, 3],
LocationName.doorstop_dash_dk : [0x65A, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
doorstop_dash_region_locations[LocationName.doorstop_dash_kong] = []
- doorstop_dash_region = create_region(world, player, active_locations, LocationName.doorstop_dash_region,
+ doorstop_dash_region = create_region(world, active_locations, LocationName.doorstop_dash_region,
doorstop_dash_region_locations)
tidal_trouble_region_locations = {
@@ -66,9 +67,9 @@ def create_regions(world, player: int, active_locations):
LocationName.tidal_trouble_bonus_2 : [0x659, 3],
LocationName.tidal_trouble_dk : [0x659, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
tidal_trouble_region_locations[LocationName.tidal_trouble_kong] = []
- tidal_trouble_region = create_region(world, player, active_locations, LocationName.tidal_trouble_region,
+ tidal_trouble_region = create_region(world, active_locations, LocationName.tidal_trouble_region,
tidal_trouble_region_locations)
skiddas_row_region_locations = {
@@ -77,9 +78,9 @@ def create_regions(world, player: int, active_locations):
LocationName.skiddas_row_bonus_2 : [0x65D, 3],
LocationName.skiddas_row_dk : [0x65D, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
skiddas_row_region_locations[LocationName.skiddas_row_kong] = []
- skiddas_row_region = create_region(world, player, active_locations, LocationName.skiddas_row_region,
+ skiddas_row_region = create_region(world, active_locations, LocationName.skiddas_row_region,
skiddas_row_region_locations)
murky_mill_region_locations = {
@@ -88,9 +89,9 @@ def create_regions(world, player: int, active_locations):
LocationName.murky_mill_bonus_2 : [0x65C, 3],
LocationName.murky_mill_dk : [0x65C, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
murky_mill_region_locations[LocationName.murky_mill_kong] = []
- murky_mill_region = create_region(world, player, active_locations, LocationName.murky_mill_region,
+ murky_mill_region = create_region(world, active_locations, LocationName.murky_mill_region,
murky_mill_region_locations)
barrel_shield_bust_up_region_locations = {
@@ -99,9 +100,9 @@ def create_regions(world, player: int, active_locations):
LocationName.barrel_shield_bust_up_bonus_2 : [0x662, 3],
LocationName.barrel_shield_bust_up_dk : [0x662, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
barrel_shield_bust_up_region_locations[LocationName.barrel_shield_bust_up_kong] = []
- barrel_shield_bust_up_region = create_region(world, player, active_locations,
+ barrel_shield_bust_up_region = create_region(world, active_locations,
LocationName.barrel_shield_bust_up_region,
barrel_shield_bust_up_region_locations)
@@ -111,9 +112,9 @@ def create_regions(world, player: int, active_locations):
LocationName.riverside_race_bonus_2 : [0x664, 3],
LocationName.riverside_race_dk : [0x664, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
riverside_race_region_locations[LocationName.riverside_race_kong] = []
- riverside_race_region = create_region(world, player, active_locations, LocationName.riverside_race_region,
+ riverside_race_region = create_region(world, active_locations, LocationName.riverside_race_region,
riverside_race_region_locations)
squeals_on_wheels_region_locations = {
@@ -122,9 +123,9 @@ def create_regions(world, player: int, active_locations):
LocationName.squeals_on_wheels_bonus_2 : [0x65B, 3],
LocationName.squeals_on_wheels_dk : [0x65B, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
squeals_on_wheels_region_locations[LocationName.squeals_on_wheels_kong] = []
- squeals_on_wheels_region = create_region(world, player, active_locations, LocationName.squeals_on_wheels_region,
+ squeals_on_wheels_region = create_region(world, active_locations, LocationName.squeals_on_wheels_region,
squeals_on_wheels_region_locations)
springin_spiders_region_locations = {
@@ -133,9 +134,9 @@ def create_regions(world, player: int, active_locations):
LocationName.springin_spiders_bonus_2 : [0x661, 3],
LocationName.springin_spiders_dk : [0x661, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
springin_spiders_region_locations[LocationName.springin_spiders_kong] = []
- springin_spiders_region = create_region(world, player, active_locations, LocationName.springin_spiders_region,
+ springin_spiders_region = create_region(world, active_locations, LocationName.springin_spiders_region,
springin_spiders_region_locations)
bobbing_barrel_brawl_region_locations = {
@@ -144,9 +145,9 @@ def create_regions(world, player: int, active_locations):
LocationName.bobbing_barrel_brawl_bonus_2 : [0x666, 3],
LocationName.bobbing_barrel_brawl_dk : [0x666, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
bobbing_barrel_brawl_region_locations[LocationName.bobbing_barrel_brawl_kong] = []
- bobbing_barrel_brawl_region = create_region(world, player, active_locations,
+ bobbing_barrel_brawl_region = create_region(world, active_locations,
LocationName.bobbing_barrel_brawl_region,
bobbing_barrel_brawl_region_locations)
@@ -156,9 +157,9 @@ def create_regions(world, player: int, active_locations):
LocationName.bazzas_blockade_bonus_2 : [0x667, 3],
LocationName.bazzas_blockade_dk : [0x667, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
bazzas_blockade_region_locations[LocationName.bazzas_blockade_kong] = []
- bazzas_blockade_region = create_region(world, player, active_locations, LocationName.bazzas_blockade_region,
+ bazzas_blockade_region = create_region(world, active_locations, LocationName.bazzas_blockade_region,
bazzas_blockade_region_locations)
rocket_barrel_ride_region_locations = {
@@ -167,9 +168,9 @@ def create_regions(world, player: int, active_locations):
LocationName.rocket_barrel_ride_bonus_2 : [0x66A, 3],
LocationName.rocket_barrel_ride_dk : [0x66A, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
rocket_barrel_ride_region_locations[LocationName.rocket_barrel_ride_kong] = []
- rocket_barrel_ride_region = create_region(world, player, active_locations, LocationName.rocket_barrel_ride_region,
+ rocket_barrel_ride_region = create_region(world, active_locations, LocationName.rocket_barrel_ride_region,
rocket_barrel_ride_region_locations)
kreeping_klasps_region_locations = {
@@ -178,9 +179,9 @@ def create_regions(world, player: int, active_locations):
LocationName.kreeping_klasps_bonus_2 : [0x658, 3],
LocationName.kreeping_klasps_dk : [0x658, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
kreeping_klasps_region_locations[LocationName.kreeping_klasps_kong] = []
- kreeping_klasps_region = create_region(world, player, active_locations, LocationName.kreeping_klasps_region,
+ kreeping_klasps_region = create_region(world, active_locations, LocationName.kreeping_klasps_region,
kreeping_klasps_region_locations)
tracker_barrel_trek_region_locations = {
@@ -189,9 +190,9 @@ def create_regions(world, player: int, active_locations):
LocationName.tracker_barrel_trek_bonus_2 : [0x66B, 3],
LocationName.tracker_barrel_trek_dk : [0x66B, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
tracker_barrel_trek_region_locations[LocationName.tracker_barrel_trek_kong] = []
- tracker_barrel_trek_region = create_region(world, player, active_locations, LocationName.tracker_barrel_trek_region,
+ tracker_barrel_trek_region = create_region(world, active_locations, LocationName.tracker_barrel_trek_region,
tracker_barrel_trek_region_locations)
fish_food_frenzy_region_locations = {
@@ -200,9 +201,9 @@ def create_regions(world, player: int, active_locations):
LocationName.fish_food_frenzy_bonus_2 : [0x668, 3],
LocationName.fish_food_frenzy_dk : [0x668, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
fish_food_frenzy_region_locations[LocationName.fish_food_frenzy_kong] = []
- fish_food_frenzy_region = create_region(world, player, active_locations, LocationName.fish_food_frenzy_region,
+ fish_food_frenzy_region = create_region(world, active_locations, LocationName.fish_food_frenzy_region,
fish_food_frenzy_region_locations)
fire_ball_frenzy_region_locations = {
@@ -211,9 +212,9 @@ def create_regions(world, player: int, active_locations):
LocationName.fire_ball_frenzy_bonus_2 : [0x66D, 3],
LocationName.fire_ball_frenzy_dk : [0x66D, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
fire_ball_frenzy_region_locations[LocationName.fire_ball_frenzy_kong] = []
- fire_ball_frenzy_region = create_region(world, player, active_locations, LocationName.fire_ball_frenzy_region,
+ fire_ball_frenzy_region = create_region(world, active_locations, LocationName.fire_ball_frenzy_region,
fire_ball_frenzy_region_locations)
demolition_drain_pipe_region_locations = {
@@ -222,9 +223,9 @@ def create_regions(world, player: int, active_locations):
LocationName.demolition_drain_pipe_bonus_2 : [0x672, 3],
LocationName.demolition_drain_pipe_dk : [0x672, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
demolition_drain_pipe_region_locations[LocationName.demolition_drain_pipe_kong] = []
- demolition_drain_pipe_region = create_region(world, player, active_locations,
+ demolition_drain_pipe_region = create_region(world, active_locations,
LocationName.demolition_drain_pipe_region,
demolition_drain_pipe_region_locations)
@@ -234,9 +235,9 @@ def create_regions(world, player: int, active_locations):
LocationName.ripsaw_rage_bonus_2 : [0x660, 3],
LocationName.ripsaw_rage_dk : [0x660, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
ripsaw_rage_region_locations[LocationName.ripsaw_rage_kong] = []
- ripsaw_rage_region = create_region(world, player, active_locations, LocationName.ripsaw_rage_region,
+ ripsaw_rage_region = create_region(world, active_locations, LocationName.ripsaw_rage_region,
ripsaw_rage_region_locations)
blazing_bazookas_region_locations = {
@@ -245,9 +246,9 @@ def create_regions(world, player: int, active_locations):
LocationName.blazing_bazookas_bonus_2 : [0x66E, 3],
LocationName.blazing_bazookas_dk : [0x66E, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
blazing_bazookas_region_locations[LocationName.blazing_bazookas_kong] = []
- blazing_bazookas_region = create_region(world, player, active_locations, LocationName.blazing_bazookas_region,
+ blazing_bazookas_region = create_region(world, active_locations, LocationName.blazing_bazookas_region,
blazing_bazookas_region_locations)
low_g_labyrinth_region_locations = {
@@ -256,9 +257,9 @@ def create_regions(world, player: int, active_locations):
LocationName.low_g_labyrinth_bonus_2 : [0x670, 3],
LocationName.low_g_labyrinth_dk : [0x670, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
low_g_labyrinth_region_locations[LocationName.low_g_labyrinth_kong] = []
- low_g_labyrinth_region = create_region(world, player, active_locations, LocationName.low_g_labyrinth_region,
+ low_g_labyrinth_region = create_region(world, active_locations, LocationName.low_g_labyrinth_region,
low_g_labyrinth_region_locations)
krevice_kreepers_region_locations = {
@@ -267,9 +268,9 @@ def create_regions(world, player: int, active_locations):
LocationName.krevice_kreepers_bonus_2 : [0x673, 3],
LocationName.krevice_kreepers_dk : [0x673, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
krevice_kreepers_region_locations[LocationName.krevice_kreepers_kong] = []
- krevice_kreepers_region = create_region(world, player, active_locations, LocationName.krevice_kreepers_region,
+ krevice_kreepers_region = create_region(world, active_locations, LocationName.krevice_kreepers_region,
krevice_kreepers_region_locations)
tearaway_toboggan_region_locations = {
@@ -278,9 +279,9 @@ def create_regions(world, player: int, active_locations):
LocationName.tearaway_toboggan_bonus_2 : [0x65F, 3],
LocationName.tearaway_toboggan_dk : [0x65F, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
tearaway_toboggan_region_locations[LocationName.tearaway_toboggan_kong] = []
- tearaway_toboggan_region = create_region(world, player, active_locations, LocationName.tearaway_toboggan_region,
+ tearaway_toboggan_region = create_region(world, active_locations, LocationName.tearaway_toboggan_region,
tearaway_toboggan_region_locations)
barrel_drop_bounce_region_locations = {
@@ -289,9 +290,9 @@ def create_regions(world, player: int, active_locations):
LocationName.barrel_drop_bounce_bonus_2 : [0x66C, 3],
LocationName.barrel_drop_bounce_dk : [0x66C, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
barrel_drop_bounce_region_locations[LocationName.barrel_drop_bounce_kong] = []
- barrel_drop_bounce_region = create_region(world, player, active_locations, LocationName.barrel_drop_bounce_region,
+ barrel_drop_bounce_region = create_region(world, active_locations, LocationName.barrel_drop_bounce_region,
barrel_drop_bounce_region_locations)
krack_shot_kroc_region_locations = {
@@ -300,9 +301,9 @@ def create_regions(world, player: int, active_locations):
LocationName.krack_shot_kroc_bonus_2 : [0x66F, 3],
LocationName.krack_shot_kroc_dk : [0x66F, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
krack_shot_kroc_region_locations[LocationName.krack_shot_kroc_kong] = []
- krack_shot_kroc_region = create_region(world, player, active_locations, LocationName.krack_shot_kroc_region,
+ krack_shot_kroc_region = create_region(world, active_locations, LocationName.krack_shot_kroc_region,
krack_shot_kroc_region_locations)
lemguin_lunge_region_locations = {
@@ -311,9 +312,9 @@ def create_regions(world, player: int, active_locations):
LocationName.lemguin_lunge_bonus_2 : [0x65E, 3],
LocationName.lemguin_lunge_dk : [0x65E, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
lemguin_lunge_region_locations[LocationName.lemguin_lunge_kong] = []
- lemguin_lunge_region = create_region(world, player, active_locations, LocationName.lemguin_lunge_region,
+ lemguin_lunge_region = create_region(world, active_locations, LocationName.lemguin_lunge_region,
lemguin_lunge_region_locations)
buzzer_barrage_region_locations = {
@@ -322,9 +323,9 @@ def create_regions(world, player: int, active_locations):
LocationName.buzzer_barrage_bonus_2 : [0x676, 3],
LocationName.buzzer_barrage_dk : [0x676, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
buzzer_barrage_region_locations[LocationName.buzzer_barrage_kong] = []
- buzzer_barrage_region = create_region(world, player, active_locations, LocationName.buzzer_barrage_region,
+ buzzer_barrage_region = create_region(world, active_locations, LocationName.buzzer_barrage_region,
buzzer_barrage_region_locations)
kong_fused_cliffs_region_locations = {
@@ -333,9 +334,9 @@ def create_regions(world, player: int, active_locations):
LocationName.kong_fused_cliffs_bonus_2 : [0x674, 3],
LocationName.kong_fused_cliffs_dk : [0x674, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
kong_fused_cliffs_region_locations[LocationName.kong_fused_cliffs_kong] = []
- kong_fused_cliffs_region = create_region(world, player, active_locations, LocationName.kong_fused_cliffs_region,
+ kong_fused_cliffs_region = create_region(world, active_locations, LocationName.kong_fused_cliffs_region,
kong_fused_cliffs_region_locations)
floodlit_fish_region_locations = {
@@ -344,9 +345,9 @@ def create_regions(world, player: int, active_locations):
LocationName.floodlit_fish_bonus_2 : [0x669, 3],
LocationName.floodlit_fish_dk : [0x669, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
floodlit_fish_region_locations[LocationName.floodlit_fish_kong] = []
- floodlit_fish_region = create_region(world, player, active_locations, LocationName.floodlit_fish_region,
+ floodlit_fish_region = create_region(world, active_locations, LocationName.floodlit_fish_region,
floodlit_fish_region_locations)
pothole_panic_region_locations = {
@@ -355,9 +356,9 @@ def create_regions(world, player: int, active_locations):
LocationName.pothole_panic_bonus_2 : [0x677, 3],
LocationName.pothole_panic_dk : [0x677, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
pothole_panic_region_locations[LocationName.pothole_panic_kong] = []
- pothole_panic_region = create_region(world, player, active_locations, LocationName.pothole_panic_region,
+ pothole_panic_region = create_region(world, active_locations, LocationName.pothole_panic_region,
pothole_panic_region_locations)
ropey_rumpus_region_locations = {
@@ -366,9 +367,9 @@ def create_regions(world, player: int, active_locations):
LocationName.ropey_rumpus_bonus_2 : [0x675, 3],
LocationName.ropey_rumpus_dk : [0x675, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
ropey_rumpus_region_locations[LocationName.ropey_rumpus_kong] = []
- ropey_rumpus_region = create_region(world, player, active_locations, LocationName.ropey_rumpus_region,
+ ropey_rumpus_region = create_region(world, active_locations, LocationName.ropey_rumpus_region,
ropey_rumpus_region_locations)
konveyor_rope_clash_region_locations = {
@@ -377,9 +378,9 @@ def create_regions(world, player: int, active_locations):
LocationName.konveyor_rope_clash_bonus_2 : [0x657, 3],
LocationName.konveyor_rope_clash_dk : [0x657, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
konveyor_rope_clash_region_locations[LocationName.konveyor_rope_clash_kong] = []
- konveyor_rope_clash_region = create_region(world, player, active_locations, LocationName.konveyor_rope_clash_region,
+ konveyor_rope_clash_region = create_region(world, active_locations, LocationName.konveyor_rope_clash_region,
konveyor_rope_clash_region_locations)
creepy_caverns_region_locations = {
@@ -388,9 +389,9 @@ def create_regions(world, player: int, active_locations):
LocationName.creepy_caverns_bonus_2 : [0x678, 3],
LocationName.creepy_caverns_dk : [0x678, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
creepy_caverns_region_locations[LocationName.creepy_caverns_kong] = []
- creepy_caverns_region = create_region(world, player, active_locations, LocationName.creepy_caverns_region,
+ creepy_caverns_region = create_region(world, active_locations, LocationName.creepy_caverns_region,
creepy_caverns_region_locations)
lightning_lookout_region_locations = {
@@ -399,9 +400,9 @@ def create_regions(world, player: int, active_locations):
LocationName.lightning_lookout_bonus_2 : [0x665, 3],
LocationName.lightning_lookout_dk : [0x665, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
lightning_lookout_region_locations[LocationName.lightning_lookout_kong] = []
- lightning_lookout_region = create_region(world, player, active_locations, LocationName.lightning_lookout_region,
+ lightning_lookout_region = create_region(world, active_locations, LocationName.lightning_lookout_region,
lightning_lookout_region_locations)
koindozer_klamber_region_locations = {
@@ -410,9 +411,9 @@ def create_regions(world, player: int, active_locations):
LocationName.koindozer_klamber_bonus_2 : [0x679, 3],
LocationName.koindozer_klamber_dk : [0x679, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
koindozer_klamber_region_locations[LocationName.koindozer_klamber_kong] = []
- koindozer_klamber_region = create_region(world, player, active_locations, LocationName.koindozer_klamber_region,
+ koindozer_klamber_region = create_region(world, active_locations, LocationName.koindozer_klamber_region,
koindozer_klamber_region_locations)
poisonous_pipeline_region_locations = {
@@ -421,9 +422,9 @@ def create_regions(world, player: int, active_locations):
LocationName.poisonous_pipeline_bonus_2 : [0x671, 3],
LocationName.poisonous_pipeline_dk : [0x671, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
poisonous_pipeline_region_locations[LocationName.poisonous_pipeline_kong] = []
- poisonous_pipeline_region = create_region(world, player, active_locations, LocationName.poisonous_pipeline_region,
+ poisonous_pipeline_region = create_region(world, active_locations, LocationName.poisonous_pipeline_region,
poisonous_pipeline_region_locations)
stampede_sprint_region_locations = {
@@ -433,9 +434,9 @@ def create_regions(world, player: int, active_locations):
LocationName.stampede_sprint_bonus_3 : [0x67B, 4],
LocationName.stampede_sprint_dk : [0x67B, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
stampede_sprint_region_locations[LocationName.stampede_sprint_kong] = []
- stampede_sprint_region = create_region(world, player, active_locations, LocationName.stampede_sprint_region,
+ stampede_sprint_region = create_region(world, active_locations, LocationName.stampede_sprint_region,
stampede_sprint_region_locations)
criss_cross_cliffs_region_locations = {
@@ -444,9 +445,9 @@ def create_regions(world, player: int, active_locations):
LocationName.criss_cross_cliffs_bonus_2 : [0x67C, 3],
LocationName.criss_cross_cliffs_dk : [0x67C, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
criss_cross_cliffs_region_locations[LocationName.criss_cross_cliffs_kong] = []
- criss_cross_cliffs_region = create_region(world, player, active_locations, LocationName.criss_cross_cliffs_region,
+ criss_cross_cliffs_region = create_region(world, active_locations, LocationName.criss_cross_cliffs_region,
criss_cross_cliffs_region_locations)
tyrant_twin_tussle_region_locations = {
@@ -456,9 +457,9 @@ def create_regions(world, player: int, active_locations):
LocationName.tyrant_twin_tussle_bonus_3 : [0x67D, 4],
LocationName.tyrant_twin_tussle_dk : [0x67D, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
tyrant_twin_tussle_region_locations[LocationName.tyrant_twin_tussle_kong] = []
- tyrant_twin_tussle_region = create_region(world, player, active_locations, LocationName.tyrant_twin_tussle_region,
+ tyrant_twin_tussle_region = create_region(world, active_locations, LocationName.tyrant_twin_tussle_region,
tyrant_twin_tussle_region_locations)
swoopy_salvo_region_locations = {
@@ -468,147 +469,147 @@ def create_regions(world, player: int, active_locations):
LocationName.swoopy_salvo_bonus_3 : [0x663, 4],
LocationName.swoopy_salvo_dk : [0x663, 5],
}
- if world.kongsanity[player]:
+ if world.options.kongsanity:
swoopy_salvo_region_locations[LocationName.swoopy_salvo_kong] = []
- swoopy_salvo_region = create_region(world, player, active_locations, LocationName.swoopy_salvo_region,
+ swoopy_salvo_region = create_region(world, active_locations, LocationName.swoopy_salvo_region,
swoopy_salvo_region_locations)
rocket_rush_region_locations = {
LocationName.rocket_rush_flag : [0x67E, 1],
LocationName.rocket_rush_dk : [0x67E, 5],
}
- rocket_rush_region = create_region(world, player, active_locations, LocationName.rocket_rush_region,
+ rocket_rush_region = create_region(world, active_locations, LocationName.rocket_rush_region,
rocket_rush_region_locations)
belchas_barn_region_locations = {
LocationName.belchas_barn: [0x64F, 1],
}
- belchas_barn_region = create_region(world, player, active_locations, LocationName.belchas_barn_region,
+ belchas_barn_region = create_region(world, active_locations, LocationName.belchas_barn_region,
belchas_barn_region_locations)
arichs_ambush_region_locations = {
LocationName.arichs_ambush: [0x650, 1],
}
- arichs_ambush_region = create_region(world, player, active_locations, LocationName.arichs_ambush_region,
+ arichs_ambush_region = create_region(world, active_locations, LocationName.arichs_ambush_region,
arichs_ambush_region_locations)
squirts_showdown_region_locations = {
LocationName.squirts_showdown: [0x651, 1],
}
- squirts_showdown_region = create_region(world, player, active_locations, LocationName.squirts_showdown_region,
+ squirts_showdown_region = create_region(world, active_locations, LocationName.squirts_showdown_region,
squirts_showdown_region_locations)
kaos_karnage_region_locations = {
LocationName.kaos_karnage: [0x652, 1],
}
- kaos_karnage_region = create_region(world, player, active_locations, LocationName.kaos_karnage_region,
+ kaos_karnage_region = create_region(world, active_locations, LocationName.kaos_karnage_region,
kaos_karnage_region_locations)
bleaks_house_region_locations = {
LocationName.bleaks_house: [0x653, 1],
}
- bleaks_house_region = create_region(world, player, active_locations, LocationName.bleaks_house_region,
+ bleaks_house_region = create_region(world, active_locations, LocationName.bleaks_house_region,
bleaks_house_region_locations)
barboss_barrier_region_locations = {
LocationName.barboss_barrier: [0x654, 1],
}
- barboss_barrier_region = create_region(world, player, active_locations, LocationName.barboss_barrier_region,
+ barboss_barrier_region = create_region(world, active_locations, LocationName.barboss_barrier_region,
barboss_barrier_region_locations)
kastle_kaos_region_locations = {
LocationName.kastle_kaos: [0x655, 1],
}
- kastle_kaos_region = create_region(world, player, active_locations, LocationName.kastle_kaos_region,
+ kastle_kaos_region = create_region(world, active_locations, LocationName.kastle_kaos_region,
kastle_kaos_region_locations)
knautilus_region_locations = {
LocationName.knautilus: [0x656, 1],
}
- knautilus_region = create_region(world, player, active_locations, LocationName.knautilus_region,
+ knautilus_region = create_region(world, active_locations, LocationName.knautilus_region,
knautilus_region_locations)
belchas_burrow_region_locations = {
LocationName.belchas_burrow: [0x647, 1],
}
- belchas_burrow_region = create_region(world, player, active_locations, LocationName.belchas_burrow_region,
+ belchas_burrow_region = create_region(world, active_locations, LocationName.belchas_burrow_region,
belchas_burrow_region_locations)
kong_cave_region_locations = {
LocationName.kong_cave: [0x645, 1],
}
- kong_cave_region = create_region(world, player, active_locations, LocationName.kong_cave_region,
+ kong_cave_region = create_region(world, active_locations, LocationName.kong_cave_region,
kong_cave_region_locations)
undercover_cove_region_locations = {
LocationName.undercover_cove: [0x644, 1],
}
- undercover_cove_region = create_region(world, player, active_locations, LocationName.undercover_cove_region,
+ undercover_cove_region = create_region(world, active_locations, LocationName.undercover_cove_region,
undercover_cove_region_locations)
ks_cache_region_locations = {
LocationName.ks_cache: [0x642, 1],
}
- ks_cache_region = create_region(world, player, active_locations, LocationName.ks_cache_region,
+ ks_cache_region = create_region(world, active_locations, LocationName.ks_cache_region,
ks_cache_region_locations)
hill_top_hoard_region_locations = {
LocationName.hill_top_hoard: [0x643, 1],
}
- hill_top_hoard_region = create_region(world, player, active_locations, LocationName.hill_top_hoard_region,
+ hill_top_hoard_region = create_region(world, active_locations, LocationName.hill_top_hoard_region,
hill_top_hoard_region_locations)
bounty_beach_region_locations = {
LocationName.bounty_beach: [0x646, 1],
}
- bounty_beach_region = create_region(world, player, active_locations, LocationName.bounty_beach_region,
+ bounty_beach_region = create_region(world, active_locations, LocationName.bounty_beach_region,
bounty_beach_region_locations)
smugglers_cove_region_locations = {
LocationName.smugglers_cove: [0x648, 1],
}
- smugglers_cove_region = create_region(world, player, active_locations, LocationName.smugglers_cove_region,
+ smugglers_cove_region = create_region(world, active_locations, LocationName.smugglers_cove_region,
smugglers_cove_region_locations)
arichs_hoard_region_locations = {
LocationName.arichs_hoard: [0x649, 1],
}
- arichs_hoard_region = create_region(world, player, active_locations, LocationName.arichs_hoard_region,
+ arichs_hoard_region = create_region(world, active_locations, LocationName.arichs_hoard_region,
arichs_hoard_region_locations)
bounty_bay_region_locations = {
LocationName.bounty_bay: [0x64A, 1],
}
- bounty_bay_region = create_region(world, player, active_locations, LocationName.bounty_bay_region,
+ bounty_bay_region = create_region(world, active_locations, LocationName.bounty_bay_region,
bounty_bay_region_locations)
sky_high_secret_region_locations = {}
- if False:#world.include_trade_sequence[player]:
+ if False:#world.options.include_trade_sequence:
sky_high_secret_region_locations[LocationName.sky_high_secret] = [0x64B, 1]
- sky_high_secret_region = create_region(world, player, active_locations, LocationName.sky_high_secret_region,
+ sky_high_secret_region = create_region(world, active_locations, LocationName.sky_high_secret_region,
sky_high_secret_region_locations)
glacial_grotto_region_locations = {
LocationName.glacial_grotto: [0x64C, 1],
}
- glacial_grotto_region = create_region(world, player, active_locations, LocationName.glacial_grotto_region,
+ glacial_grotto_region = create_region(world, active_locations, LocationName.glacial_grotto_region,
glacial_grotto_region_locations)
cifftop_cache_region_locations = {}
- if False:#world.include_trade_sequence[player]:
+ if False:#world.options.include_trade_sequence:
cifftop_cache_region_locations[LocationName.cifftop_cache] = [0x64D, 1]
- cifftop_cache_region = create_region(world, player, active_locations, LocationName.cifftop_cache_region,
+ cifftop_cache_region = create_region(world, active_locations, LocationName.cifftop_cache_region,
cifftop_cache_region_locations)
sewer_stockpile_region_locations = {
LocationName.sewer_stockpile: [0x64E, 1],
}
- sewer_stockpile_region = create_region(world, player, active_locations, LocationName.sewer_stockpile_region,
+ sewer_stockpile_region = create_region(world, active_locations, LocationName.sewer_stockpile_region,
sewer_stockpile_region_locations)
# Set up the regions correctly.
- world.regions += [
+ world.multiworld.regions += [
menu_region,
overworld_1_region,
overworld_2_region,
@@ -693,7 +694,7 @@ def create_regions(world, player: int, active_locations):
blue_region_locations = {}
blizzard_region_locations = {}
- if False:#world.include_trade_sequence[player]:
+ if False:#world.options.include_trade_sequence:
bazaar_region_locations.update({
LocationName.bazaars_general_store_1: [0x615, 2, True],
LocationName.bazaars_general_store_2: [0x615, 3, True],
@@ -713,19 +714,19 @@ def create_regions(world, player: int, active_locations):
blizzard_region_locations[LocationName.blizzards_basecamp] = [0x625, 4, True]
- bazaar_region = create_region(world, player, active_locations, LocationName.bazaar_region, bazaar_region_locations)
- bramble_region = create_region(world, player, active_locations, LocationName.bramble_region,
+ bazaar_region = create_region(world, active_locations, LocationName.bazaar_region, bazaar_region_locations)
+ bramble_region = create_region(world, active_locations, LocationName.bramble_region,
bramble_region_locations)
- flower_spot_region = create_region(world, player, active_locations, LocationName.flower_spot_region,
+ flower_spot_region = create_region(world, active_locations, LocationName.flower_spot_region,
flower_spot_region_locations)
- barter_region = create_region(world, player, active_locations, LocationName.barter_region, barter_region_locations)
- barnacle_region = create_region(world, player, active_locations, LocationName.barnacle_region,
+ barter_region = create_region(world, active_locations, LocationName.barter_region, barter_region_locations)
+ barnacle_region = create_region(world, active_locations, LocationName.barnacle_region,
barnacle_region_locations)
- blue_region = create_region(world, player, active_locations, LocationName.blue_region, blue_region_locations)
- blizzard_region = create_region(world, player, active_locations, LocationName.blizzard_region,
+ blue_region = create_region(world, active_locations, LocationName.blue_region, blue_region_locations)
+ blizzard_region = create_region(world, active_locations, LocationName.blizzard_region,
blizzard_region_locations)
- world.regions += [
+ world.multiworld.regions += [
bazaar_region,
bramble_region,
flower_spot_region,
@@ -736,41 +737,41 @@ def create_regions(world, player: int, active_locations):
]
-def connect_regions(world, player, level_list):
+def connect_regions(world: World, level_list):
names: typing.Dict[str, int] = {}
# Overworld
- connect(world, player, names, 'Menu', LocationName.overworld_1_region)
- connect(world, player, names, LocationName.overworld_1_region, LocationName.overworld_2_region,
- lambda state: (state.has(ItemName.progressive_boat, player, 1)))
- connect(world, player, names, LocationName.overworld_2_region, LocationName.overworld_3_region,
- lambda state: (state.has(ItemName.progressive_boat, player, 3)))
- connect(world, player, names, LocationName.overworld_1_region, LocationName.overworld_4_region,
- lambda state: (state.has(ItemName.dk_coin, player, world.dk_coins_for_gyrocopter[player].value) and
- state.has(ItemName.progressive_boat, player, 3)))
+ connect(world, world.player, names, 'Menu', LocationName.overworld_1_region)
+ connect(world, world.player, names, LocationName.overworld_1_region, LocationName.overworld_2_region,
+ lambda state: (state.has(ItemName.progressive_boat, world.player, 1)))
+ connect(world, world.player, names, LocationName.overworld_2_region, LocationName.overworld_3_region,
+ lambda state: (state.has(ItemName.progressive_boat, world.player, 3)))
+ connect(world, world.player, names, LocationName.overworld_1_region, LocationName.overworld_4_region,
+ lambda state: (state.has(ItemName.dk_coin, world.player, world.options.dk_coins_for_gyrocopter.value) and
+ state.has(ItemName.progressive_boat, world.player, 3)))
# World Connections
- connect(world, player, names, LocationName.overworld_1_region, LocationName.lake_orangatanga_region)
- connect(world, player, names, LocationName.overworld_1_region, LocationName.kremwood_forest_region)
- connect(world, player, names, LocationName.overworld_1_region, LocationName.bounty_beach_region)
- connect(world, player, names, LocationName.overworld_1_region, LocationName.bazaar_region)
+ connect(world, world.player, names, LocationName.overworld_1_region, LocationName.lake_orangatanga_region)
+ connect(world, world.player, names, LocationName.overworld_1_region, LocationName.kremwood_forest_region)
+ connect(world, world.player, names, LocationName.overworld_1_region, LocationName.bounty_beach_region)
+ connect(world, world.player, names, LocationName.overworld_1_region, LocationName.bazaar_region)
- connect(world, player, names, LocationName.overworld_2_region, LocationName.cotton_top_cove_region)
- connect(world, player, names, LocationName.overworld_2_region, LocationName.mekanos_region)
- connect(world, player, names, LocationName.overworld_2_region, LocationName.kong_cave_region)
- connect(world, player, names, LocationName.overworld_2_region, LocationName.bramble_region)
+ connect(world, world.player, names, LocationName.overworld_2_region, LocationName.cotton_top_cove_region)
+ connect(world, world.player, names, LocationName.overworld_2_region, LocationName.mekanos_region)
+ connect(world, world.player, names, LocationName.overworld_2_region, LocationName.kong_cave_region)
+ connect(world, world.player, names, LocationName.overworld_2_region, LocationName.bramble_region)
- connect(world, player, names, LocationName.overworld_3_region, LocationName.k3_region)
- connect(world, player, names, LocationName.overworld_3_region, LocationName.razor_ridge_region)
- connect(world, player, names, LocationName.overworld_3_region, LocationName.kaos_kore_region)
- connect(world, player, names, LocationName.overworld_3_region, LocationName.krematoa_region)
- connect(world, player, names, LocationName.overworld_3_region, LocationName.undercover_cove_region)
- connect(world, player, names, LocationName.overworld_3_region, LocationName.flower_spot_region)
- connect(world, player, names, LocationName.overworld_3_region, LocationName.barter_region)
+ connect(world, world.player, names, LocationName.overworld_3_region, LocationName.k3_region)
+ connect(world, world.player, names, LocationName.overworld_3_region, LocationName.razor_ridge_region)
+ connect(world, world.player, names, LocationName.overworld_3_region, LocationName.kaos_kore_region)
+ connect(world, world.player, names, LocationName.overworld_3_region, LocationName.krematoa_region)
+ connect(world, world.player, names, LocationName.overworld_3_region, LocationName.undercover_cove_region)
+ connect(world, world.player, names, LocationName.overworld_3_region, LocationName.flower_spot_region)
+ connect(world, world.player, names, LocationName.overworld_3_region, LocationName.barter_region)
- connect(world, player, names, LocationName.overworld_4_region, LocationName.belchas_burrow_region)
- connect(world, player, names, LocationName.overworld_4_region, LocationName.ks_cache_region)
- connect(world, player, names, LocationName.overworld_4_region, LocationName.hill_top_hoard_region)
+ connect(world, world.player, names, LocationName.overworld_4_region, LocationName.belchas_burrow_region)
+ connect(world, world.player, names, LocationName.overworld_4_region, LocationName.ks_cache_region)
+ connect(world, world.player, names, LocationName.overworld_4_region, LocationName.hill_top_hoard_region)
# Lake Orangatanga Connections
@@ -786,7 +787,7 @@ def connect_regions(world, player, level_list):
]
for i in range(0, len(lake_orangatanga_levels)):
- connect(world, player, names, LocationName.lake_orangatanga_region, lake_orangatanga_levels[i])
+ connect(world, world.player, names, LocationName.lake_orangatanga_region, lake_orangatanga_levels[i])
# Kremwood Forest Connections
kremwood_forest_levels = [
@@ -800,10 +801,10 @@ def connect_regions(world, player, level_list):
]
for i in range(0, len(kremwood_forest_levels) - 1):
- connect(world, player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[i])
+ connect(world, world.player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[i])
- connect(world, player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[-1],
- lambda state: (state.can_reach(LocationName.riverside_race_flag, "Location", player)))
+ connect(world, world.player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[-1],
+ lambda state: (state.can_reach(LocationName.riverside_race_flag, "Location", world.player)))
# Cotton-Top Cove Connections
cotton_top_cove_levels = [
@@ -818,7 +819,7 @@ def connect_regions(world, player, level_list):
]
for i in range(0, len(cotton_top_cove_levels)):
- connect(world, player, names, LocationName.cotton_top_cove_region, cotton_top_cove_levels[i])
+ connect(world, world.player, names, LocationName.cotton_top_cove_region, cotton_top_cove_levels[i])
# Mekanos Connections
mekanos_levels = [
@@ -831,14 +832,14 @@ def connect_regions(world, player, level_list):
]
for i in range(0, len(mekanos_levels)):
- connect(world, player, names, LocationName.mekanos_region, mekanos_levels[i])
+ connect(world, world.player, names, LocationName.mekanos_region, mekanos_levels[i])
- if False:#world.include_trade_sequence[player]:
- connect(world, player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
- lambda state: (state.has(ItemName.bowling_ball, player, 1)))
+ if False:#world.options.include_trade_sequence:
+ connect(world, world.player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
+ lambda state: (state.has(ItemName.bowling_ball, world.player, 1)))
else:
- connect(world, player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
- lambda state: (state.can_reach(LocationName.bleaks_house, "Location", player)))
+ connect(world, world.player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
+ lambda state: (state.can_reach(LocationName.bleaks_house, "Location", world.player)))
# K3 Connections
k3_levels = [
@@ -853,7 +854,7 @@ def connect_regions(world, player, level_list):
]
for i in range(0, len(k3_levels)):
- connect(world, player, names, LocationName.k3_region, k3_levels[i])
+ connect(world, world.player, names, LocationName.k3_region, k3_levels[i])
# Razor Ridge Connections
razor_ridge_levels = [
@@ -866,13 +867,13 @@ def connect_regions(world, player, level_list):
]
for i in range(0, len(razor_ridge_levels)):
- connect(world, player, names, LocationName.razor_ridge_region, razor_ridge_levels[i])
+ connect(world, world.player, names, LocationName.razor_ridge_region, razor_ridge_levels[i])
- if False:#world.include_trade_sequence[player]:
- connect(world, player, names, LocationName.razor_ridge_region, LocationName.cifftop_cache_region,
- lambda state: (state.has(ItemName.wrench, player, 1)))
+ if False:#world.options.include_trade_sequence:
+ connect(world, world.player, names, LocationName.razor_ridge_region, LocationName.cifftop_cache_region,
+ lambda state: (state.has(ItemName.wrench, world.player, 1)))
else:
- connect(world, player, names, LocationName.razor_ridge_region, LocationName.cifftop_cache_region)
+ connect(world, world.player, names, LocationName.razor_ridge_region, LocationName.cifftop_cache_region)
# KAOS Kore Connections
kaos_kore_levels = [
@@ -885,7 +886,7 @@ def connect_regions(world, player, level_list):
]
for i in range(0, len(kaos_kore_levels)):
- connect(world, player, names, LocationName.kaos_kore_region, kaos_kore_levels[i])
+ connect(world, world.player, names, LocationName.kaos_kore_region, kaos_kore_levels[i])
# Krematoa Connections
krematoa_levels = [
@@ -897,22 +898,22 @@ def connect_regions(world, player, level_list):
]
for i in range(0, len(krematoa_levels)):
- connect(world, player, names, LocationName.krematoa_region, krematoa_levels[i],
- lambda state, i=i: (state.has(ItemName.bonus_coin, player, world.krematoa_bonus_coin_cost[player].value * (i+1))))
+ connect(world, world.player, names, LocationName.krematoa_region, krematoa_levels[i],
+ lambda state, i=i: (state.has(ItemName.bonus_coin, world.player, world.options.krematoa_bonus_coin_cost.value * (i+1))))
- if world.goal[player] == "knautilus":
- connect(world, player, names, LocationName.kaos_kore_region, LocationName.knautilus_region)
- connect(world, player, names, LocationName.krematoa_region, LocationName.kastle_kaos_region,
- lambda state: (state.has(ItemName.krematoa_cog, player, 5)))
+ if world.options.goal == "knautilus":
+ connect(world, world.player, names, LocationName.kaos_kore_region, LocationName.knautilus_region)
+ connect(world, world.player, names, LocationName.krematoa_region, LocationName.kastle_kaos_region,
+ lambda state: (state.has(ItemName.krematoa_cog, world.player, 5)))
else:
- connect(world, player, names, LocationName.kaos_kore_region, LocationName.kastle_kaos_region)
- connect(world, player, names, LocationName.krematoa_region, LocationName.knautilus_region,
- lambda state: (state.has(ItemName.krematoa_cog, player, 5)))
+ connect(world, world.player, names, LocationName.kaos_kore_region, LocationName.kastle_kaos_region)
+ connect(world, world.player, names, LocationName.krematoa_region, LocationName.knautilus_region,
+ lambda state: (state.has(ItemName.krematoa_cog, world.player, 5)))
-def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None):
+def create_region(world: World, active_locations, name: str, locations=None):
# Shamelessly stolen from the ROR2 definition
- ret = Region(name, player, world)
+ ret = Region(name, world.player, world.multiworld)
if locations:
for locationName, locationData in locations.items():
loc_id = active_locations.get(locationName, 0)
@@ -921,16 +922,16 @@ def create_region(world: MultiWorld, player: int, active_locations, name: str, l
loc_bit = locationData[1] if (len(locationData) > 1) else 0
loc_invert = locationData[2] if (len(locationData) > 2) else False
- location = DKC3Location(player, locationName, loc_id, ret, loc_byte, loc_bit, loc_invert)
+ location = DKC3Location(world.player, locationName, loc_id, ret, loc_byte, loc_bit, loc_invert)
ret.locations.append(location)
return ret
-def connect(world: MultiWorld, player: int, used_names: typing.Dict[str, int], source: str, target: str,
+def connect(world: World, player: int, used_names: typing.Dict[str, int], source: str, target: str,
rule: typing.Optional[typing.Callable] = None):
- source_region = world.get_region(source, player)
- target_region = world.get_region(target, player)
+ source_region = world.multiworld.get_region(source, player)
+ target_region = world.multiworld.get_region(target, player)
if target not in used_names:
used_names[target] = 1
diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py
index 4255a0a38280..0dc722a73868 100644
--- a/worlds/dkc3/Rom.py
+++ b/worlds/dkc3/Rom.py
@@ -1,5 +1,6 @@
import Utils
from Utils import read_snes_rom
+from worlds.AutoWorld import World
from worlds.Files import APDeltaPatch
from .Locations import lookup_id_to_name, all_locations
from .Levels import level_list, level_dict
@@ -433,7 +434,7 @@
0x21,
]
-class LocalRom(object):
+class LocalRom:
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
self.name = name
@@ -456,7 +457,7 @@ def read_bit(self, address: int, bit_number: int) -> bool:
def read_byte(self, address: int) -> int:
return self.buffer[address]
- def read_bytes(self, startaddress: int, length: int) -> bytes:
+ def read_bytes(self, startaddress: int, length: int) -> bytearray:
return self.buffer[startaddress:startaddress + length]
def write_byte(self, address: int, value: int):
@@ -475,11 +476,10 @@ def read_from_file(self, file):
-def patch_rom(world, rom, player, active_level_list):
- local_random = world.per_slot_randoms[player]
+def patch_rom(world: World, rom: LocalRom, active_level_list):
# Boomer Costs
- bonus_coin_cost = world.krematoa_bonus_coin_cost[player]
+ bonus_coin_cost = world.options.krematoa_bonus_coin_cost
inverted_bonus_coin_cost = 0x100 - bonus_coin_cost
rom.write_byte(0x3498B9, inverted_bonus_coin_cost)
rom.write_byte(0x3498BA, inverted_bonus_coin_cost)
@@ -491,7 +491,7 @@ def patch_rom(world, rom, player, active_level_list):
rom.write_byte(0x349862, bonus_coin_cost)
# Gyrocopter Costs
- dk_coin_cost = world.dk_coins_for_gyrocopter[player]
+ dk_coin_cost = world.options.dk_coins_for_gyrocopter
rom.write_byte(0x3484A6, dk_coin_cost)
rom.write_byte(0x3484D5, dk_coin_cost)
rom.write_byte(0x3484D7, 0x90)
@@ -508,8 +508,8 @@ def patch_rom(world, rom, player, active_level_list):
rom.write_bytes(0x34ACD0, bytearray([0xEA, 0xEA]))
# Banana Bird Costs
- if world.goal[player] == "banana_bird_hunt":
- banana_bird_cost = math.floor(world.number_of_banana_birds[player] * world.percentage_of_banana_birds[player] / 100.0)
+ if world.options.goal == "banana_bird_hunt":
+ banana_bird_cost = math.floor(world.options.number_of_banana_birds * world.options.percentage_of_banana_birds / 100.0)
rom.write_byte(0x34AB85, banana_bird_cost)
rom.write_byte(0x329FD8, banana_bird_cost)
rom.write_byte(0x32A025, banana_bird_cost)
@@ -528,65 +528,65 @@ def patch_rom(world, rom, player, active_level_list):
# Palette Swap
rom.write_byte(0x3B96A5, 0xD0)
- if world.kong_palette_swap[player] == "default":
+ if world.options.kong_palette_swap == "default":
rom.write_byte(0x3B96A9, 0x00)
rom.write_byte(0x3B96A8, 0x00)
- elif world.kong_palette_swap[player] == "purple":
+ elif world.options.kong_palette_swap == "purple":
rom.write_byte(0x3B96A9, 0x00)
rom.write_byte(0x3B96A8, 0x3C)
- elif world.kong_palette_swap[player] == "spooky":
+ elif world.options.kong_palette_swap == "spooky":
rom.write_byte(0x3B96A9, 0x00)
rom.write_byte(0x3B96A8, 0xA0)
- elif world.kong_palette_swap[player] == "dark":
+ elif world.options.kong_palette_swap == "dark":
rom.write_byte(0x3B96A9, 0x05)
rom.write_byte(0x3B96A8, 0xA0)
- elif world.kong_palette_swap[player] == "chocolate":
+ elif world.options.kong_palette_swap == "chocolate":
rom.write_byte(0x3B96A9, 0x1D)
rom.write_byte(0x3B96A8, 0xA0)
- elif world.kong_palette_swap[player] == "shadow":
+ elif world.options.kong_palette_swap == "shadow":
rom.write_byte(0x3B96A9, 0x45)
rom.write_byte(0x3B96A8, 0xA0)
- elif world.kong_palette_swap[player] == "red_gold":
+ elif world.options.kong_palette_swap == "red_gold":
rom.write_byte(0x3B96A9, 0x5D)
rom.write_byte(0x3B96A8, 0xA0)
- elif world.kong_palette_swap[player] == "gbc":
+ elif world.options.kong_palette_swap == "gbc":
rom.write_byte(0x3B96A9, 0x20)
rom.write_byte(0x3B96A8, 0x3C)
- elif world.kong_palette_swap[player] == "halloween":
+ elif world.options.kong_palette_swap == "halloween":
rom.write_byte(0x3B96A9, 0x70)
rom.write_byte(0x3B96A8, 0x3C)
- if world.music_shuffle[player]:
+ if world.options.music_shuffle:
for address in music_rom_data:
- rand_song = local_random.choice(level_music_ids)
+ rand_song = world.random.choice(level_music_ids)
rom.write_byte(address, rand_song)
# Starting Lives
- rom.write_byte(0x9130, world.starting_life_count[player].value)
- rom.write_byte(0x913B, world.starting_life_count[player].value)
+ rom.write_byte(0x9130, world.options.starting_life_count.value)
+ rom.write_byte(0x913B, world.options.starting_life_count.value)
# Cheat options
cheat_bytes = [0x00, 0x00]
- if world.merry[player]:
+ if world.options.merry:
cheat_bytes[0] |= 0x01
- if world.autosave[player]:
+ if world.options.autosave:
cheat_bytes[0] |= 0x02
- if world.difficulty[player] == "tufst":
+ if world.options.difficulty == "tufst":
cheat_bytes[0] |= 0x80
cheat_bytes[1] |= 0x80
- elif world.difficulty[player] == "hardr":
+ elif world.options.difficulty == "hardr":
cheat_bytes[0] |= 0x00
cheat_bytes[1] |= 0x00
- elif world.difficulty[player] == "norml":
+ elif world.options.difficulty == "norml":
cheat_bytes[1] |= 0x40
rom.write_bytes(0x8303, bytearray(cheat_bytes))
# Handle Level Shuffle Here
- if world.level_shuffle[player]:
+ if world.options.level_shuffle:
for i in range(len(active_level_list)):
rom.write_byte(level_dict[level_list[i]].nameIDAddress, level_dict[active_level_list[i]].nameID)
rom.write_byte(level_dict[level_list[i]].levelIDAddress, level_dict[active_level_list[i]].levelID)
@@ -611,7 +611,7 @@ def patch_rom(world, rom, player, active_level_list):
rom.write_byte(0x34C213, (0x32 + level_dict[active_level_list[25]].levelID))
rom.write_byte(0x34C21B, (0x32 + level_dict[active_level_list[26]].levelID))
- if world.goal[player] == "knautilus":
+ if world.options.goal == "knautilus":
# Swap Kastle KAOS and Knautilus
rom.write_byte(0x34D4E1, 0xC2)
rom.write_byte(0x34D4E2, 0x24)
@@ -621,7 +621,7 @@ def patch_rom(world, rom, player, active_level_list):
rom.write_byte(0x32F339, 0x55)
# Handle KONGsanity Here
- if world.kongsanity[player]:
+ if world.options.kongsanity:
# Arich's Hoard KONGsanity fix
rom.write_bytes(0x34BA8C, bytearray([0xEA, 0xEA]))
@@ -668,7 +668,7 @@ def patch_rom(world, rom, player, active_level_list):
rom.write_bytes(0x32A5EE, bytearray([0x00, 0x03, 0x50, 0x4F, 0x52, 0x59, 0x47, 0x4F, 0x4E, 0xC5])) # "PORYGONE"
from Utils import __version__
- rom.name = bytearray(f'D3{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
+ rom.name = bytearray(f'D3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, rom.name)
diff --git a/worlds/dkc3/Rules.py b/worlds/dkc3/Rules.py
index dc90eefd1367..cc45e4ef3ad5 100644
--- a/worlds/dkc3/Rules.py
+++ b/worlds/dkc3/Rules.py
@@ -1,32 +1,31 @@
import math
-from BaseClasses import MultiWorld
from .Names import LocationName, ItemName
-from worlds.AutoWorld import LogicMixin
+from worlds.AutoWorld import LogicMixin, World
from worlds.generic.Rules import add_rule, set_rule
-def set_rules(world: MultiWorld, player: int):
+def set_rules(world: World):
- if False:#world.include_trade_sequence[player]:
- add_rule(world.get_location(LocationName.barnacles_island, player),
- lambda state: state.has(ItemName.shell, player))
+ if False:#world.options.include_trade_sequence:
+ add_rule(world.multiworld.get_location(LocationName.barnacles_island, world.player),
+ lambda state: state.has(ItemName.shell, world.player))
- add_rule(world.get_location(LocationName.blues_beach_hut, player),
- lambda state: state.has(ItemName.present, player))
+ add_rule(world.multiworld.get_location(LocationName.blues_beach_hut, world.player),
+ lambda state: state.has(ItemName.present, world.player))
- add_rule(world.get_location(LocationName.brambles_bungalow, player),
- lambda state: state.has(ItemName.flower, player))
+ add_rule(world.multiworld.get_location(LocationName.brambles_bungalow, world.player),
+ lambda state: state.has(ItemName.flower, world.player))
- add_rule(world.get_location(LocationName.barters_swap_shop, player),
- lambda state: state.has(ItemName.mirror, player))
+ add_rule(world.multiworld.get_location(LocationName.barters_swap_shop, world.player),
+ lambda state: state.has(ItemName.mirror, world.player))
- if world.goal[player] != "knautilus":
+ if world.options.goal != "knautilus":
required_banana_birds = math.floor(
- world.number_of_banana_birds[player].value * (world.percentage_of_banana_birds[player].value / 100.0))
+ world.options.number_of_banana_birds.value * (world.options.percentage_of_banana_birds.value / 100.0))
- add_rule(world.get_location(LocationName.banana_bird_mother, player),
- lambda state: state.has(ItemName.banana_bird, player, required_banana_birds))
+ add_rule(world.multiworld.get_location(LocationName.banana_bird_mother, world.player),
+ lambda state: state.has(ItemName.banana_bird, world.player, required_banana_birds))
- world.completion_condition[player] = lambda state: state.has(ItemName.victory, player)
+ world.multiworld.completion_condition[world.player] = lambda state: state.has(ItemName.victory, world.player)
diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py
index 462e1416d9e6..de6fb4a44a03 100644
--- a/worlds/dkc3/__init__.py
+++ b/worlds/dkc3/__init__.py
@@ -1,21 +1,24 @@
+import dataclasses
import os
import typing
import math
import threading
-import settings
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
+from Options import PerGameCommonOptions
+import Patch
+import settings
+from worlds.AutoWorld import WebWorld, World
+
+from .Client import DKC3SNIClient
from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table
-from .Locations import DKC3Location, all_locations, setup_locations
-from .Options import dkc3_options
-from .Regions import create_regions, connect_regions
from .Levels import level_list
-from .Rules import set_rules
+from .Locations import DKC3Location, all_locations, setup_locations
from .Names import ItemName, LocationName
-from .Client import DKC3SNIClient
-from worlds.AutoWorld import WebWorld, World
+from .Options import DKC3Options, dkc3_option_groups
+from .Regions import create_regions, connect_regions
from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch
-import Patch
+from .Rules import set_rules
class DK3Settings(settings.Group):
@@ -39,9 +42,11 @@ class DKC3Web(WebWorld):
"setup/en",
["PoryGone"]
)
-
+
tutorials = [setup_en]
+ option_groups = dkc3_option_groups
+
class DKC3World(World):
"""
@@ -50,10 +55,12 @@ class DKC3World(World):
mystery of why Donkey Kong and Diddy disappeared while on vacation.
"""
game: str = "Donkey Kong Country 3"
- option_definitions = dkc3_options
settings: typing.ClassVar[DK3Settings]
+
+ options_dataclass = DKC3Options
+ options: DKC3Options
+
topology_present = False
- data_version = 2
#hint_blacklist = {LocationName.rocket_rush_flag}
item_name_to_id = {name: data.code for name, data in item_table.items()}
@@ -74,24 +81,25 @@ def stage_assert_generate(cls, multiworld: MultiWorld):
def _get_slot_data(self):
return {
- #"death_link": self.world.death_link[self.player].value,
+ #"death_link": self.options.death_link.value,
"active_levels": self.active_level_list,
}
def fill_slot_data(self) -> dict:
slot_data = self._get_slot_data()
- for option_name in dkc3_options:
- option = getattr(self.multiworld, option_name)[self.player]
+ for option_name in (attr.name for attr in dataclasses.fields(DKC3Options)
+ if attr not in dataclasses.fields(PerGameCommonOptions)):
+ option = getattr(self.options, option_name)
slot_data[option_name] = option.value
return slot_data
def create_regions(self):
- location_table = setup_locations(self.multiworld, self.player)
- create_regions(self.multiworld, self.player, location_table)
+ location_table = setup_locations(self)
+ create_regions(self, location_table)
# Not generate basic
- self.topology_present = self.multiworld.level_shuffle[self.player].value
+ self.topology_present = self.options.level_shuffle.value
itempool: typing.List[DKC3Item] = []
# Levels
@@ -103,12 +111,12 @@ def create_regions(self):
number_of_cogs = 4
self.multiworld.get_location(LocationName.rocket_rush_flag, self.player).place_locked_item(self.create_item(ItemName.krematoa_cog))
number_of_bosses = 8
- if self.multiworld.goal[self.player] == "knautilus":
+ if self.options.goal == "knautilus":
self.multiworld.get_location(LocationName.kastle_kaos, self.player).place_locked_item(self.create_item(ItemName.victory))
number_of_bosses = 7
else:
self.multiworld.get_location(LocationName.banana_bird_mother, self.player).place_locked_item(self.create_item(ItemName.victory))
- number_of_banana_birds = self.multiworld.number_of_banana_birds[self.player]
+ number_of_banana_birds = self.options.number_of_banana_birds
# Bosses
total_required_locations += number_of_bosses
@@ -116,15 +124,15 @@ def create_regions(self):
# Secret Caves
total_required_locations += 13
- if self.multiworld.kongsanity[self.player]:
+ if self.options.kongsanity:
total_required_locations += 39
## Brothers Bear
- if False:#self.world.include_trade_sequence[self.player]:
+ if False:#self.options.include_trade_sequence:
total_required_locations += 10
- number_of_bonus_coins = (self.multiworld.krematoa_bonus_coin_cost[self.player] * 5)
- number_of_bonus_coins += math.ceil((85 - number_of_bonus_coins) * self.multiworld.percentage_of_extra_bonus_coins[self.player] / 100)
+ number_of_bonus_coins = (self.options.krematoa_bonus_coin_cost * 5)
+ number_of_bonus_coins += math.ceil((85 - number_of_bonus_coins) * self.options.percentage_of_extra_bonus_coins / 100)
itempool += [self.create_item(ItemName.bonus_coin) for _ in range(number_of_bonus_coins)]
itempool += [self.create_item(ItemName.dk_coin) for _ in range(41)]
@@ -142,20 +150,17 @@ def create_regions(self):
self.active_level_list = level_list.copy()
- if self.multiworld.level_shuffle[self.player]:
- self.multiworld.random.shuffle(self.active_level_list)
+ if self.options.level_shuffle:
+ self.random.shuffle(self.active_level_list)
- connect_regions(self.multiworld, self.player, self.active_level_list)
+ connect_regions(self, self.active_level_list)
self.multiworld.itempool += itempool
def generate_output(self, output_directory: str):
try:
- world = self.multiworld
- player = self.player
-
rom = LocalRom(get_base_rom_path())
- patch_rom(self.multiworld, rom, self.player, self.active_level_list)
+ patch_rom(self, rom, self.active_level_list)
self.active_level_list.append(LocationName.rocket_rush_region)
@@ -163,15 +168,15 @@ def generate_output(self, output_directory: str):
rom.write_to_file(rompath)
self.rom_name = rom.name
- patch = DKC3DeltaPatch(os.path.splitext(rompath)[0]+DKC3DeltaPatch.patch_file_ending, player=player,
- player_name=world.player_name[player], patched_path=rompath)
+ patch = DKC3DeltaPatch(os.path.splitext(rompath)[0]+DKC3DeltaPatch.patch_file_ending, player=self.player,
+ player_name=self.multiworld.player_name[self.player], patched_path=rompath)
patch.write()
except:
raise
finally:
+ self.rom_name_available_event.set() # make sure threading continues and errors are collected
if os.path.exists(rompath):
os.unlink(rompath)
- self.rom_name_available_event.set() # make sure threading continues and errors are collected
def modify_multidata(self, multidata: dict):
import base64
@@ -183,6 +188,7 @@ def modify_multidata(self, multidata: dict):
new_name = base64.b64encode(bytes(self.rom_name)).decode()
multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]
+ def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]):
if self.topology_present:
world_names = [
LocationName.lake_orangatanga_region,
@@ -197,10 +203,16 @@ def modify_multidata(self, multidata: dict):
er_hint_data = {}
for world_index in range(len(world_names)):
for level_index in range(5):
- level_region = self.multiworld.get_region(self.active_level_list[world_index * 5 + level_index], self.player)
+ level_id: int = world_index * 5 + level_index
+
+ if level_id >= len(self.active_level_list):
+ break
+
+ level_region = self.multiworld.get_region(self.active_level_list[level_id], self.player)
for location in level_region.locations:
er_hint_data[location.address] = world_names[world_index]
- multidata['er_hint_data'][self.player] = er_hint_data
+
+ hint_data[self.player] = er_hint_data
def create_item(self, name: str, force_non_progression=False) -> Item:
data = item_table[name]
@@ -220,4 +232,4 @@ def get_filler_item_name(self) -> str:
return self.multiworld.random.choice(list(junk_table.keys()))
def set_rules(self):
- set_rules(self.multiworld, self.player)
+ set_rules(self)
diff --git a/worlds/dkc3/docs/en_Donkey Kong Country 3.md b/worlds/dkc3/docs/en_Donkey Kong Country 3.md
index 2041f0a41bd2..83ba25a9960b 100644
--- a/worlds/dkc3/docs/en_Donkey Kong Country 3.md
+++ b/worlds/dkc3/docs/en_Donkey Kong Country 3.md
@@ -1,8 +1,8 @@
# Donkey Kong Country 3
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
+The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
## What does randomization do to this game?
diff --git a/worlds/dkc3/docs/setup_en.md b/worlds/dkc3/docs/setup_en.md
index 56ef80d4a55f..c832bcd702e9 100644
--- a/worlds/dkc3/docs/setup_en.md
+++ b/worlds/dkc3/docs/setup_en.md
@@ -2,7 +2,7 @@
## Required Software
-- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Donkey Kong Country 3 Patch Setup`
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- Hardware or software capable of loading and playing SNES ROM files
@@ -23,9 +23,10 @@
### Windows Setup
-1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
- or you are on an older version, you may run the installer again to install the SNI Client.
-2. During setup, you will be asked to locate your base ROM file. This is your Donkey Kong Country 3 ROM file.
+1. Download and install [Archipelago](). **The installer
+ file is located in the assets section at the bottom of the version information.**
+2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
+ This is your Donkey Kong Country 3 ROM file. This only needs to be done once.
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
@@ -44,18 +45,18 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a config file?
-The Player Settings page on the website allows you to configure your personal settings and export a config file from
-them. Player settings page: [Donkey Kong Country 3 Player Settings Page](/games/Donkey%20Kong%20Country%203/player-settings)
+The Player Options page on the website allows you to configure your personal options and export a config file from
+them. Player options page: [Donkey Kong Country 3 Player Options Page](/games/Donkey%20Kong%20Country%203/player-options)
### 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
-1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button.
- - Player Settings page: [Donkey Kong Country 3 Player Settings Page](/games/Donkey%20Kong%20Country%203/player-settings)
+1. Navigate to the Player Options page, configure your options, and click the "Generate Game" button.
+ - Player Options page: [Donkey Kong Country 3 Player Options Page](/games/Donkey%20Kong%20Country%203/player-options)
2. You will be presented with a "Seed Info" page.
3. Click the "Create New Room" link.
4. You will be presented with a server page, from which you can download your patch file.
@@ -87,8 +88,7 @@ first time launching, you may be prompted to allow it to communicate through the
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Select the connector lua file included with your client
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
@@ -100,8 +100,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas
2. Load your ROM file if it hasn't already been loaded.
If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R).
3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window.
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
- You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua`
with the file picker.
diff --git a/worlds/dlcquest/Items.py b/worlds/dlcquest/Items.py
index 61f4cd30fabf..65b36fe61732 100644
--- a/worlds/dlcquest/Items.py
+++ b/worlds/dlcquest/Items.py
@@ -1,15 +1,18 @@
import csv
import enum
import math
-from typing import Protocol, Union, Dict, List, Set
-from BaseClasses import Item, ItemClassification
-from . import Options, data
from dataclasses import dataclass, field
from random import Random
+from typing import Dict, List, Set
+
+from BaseClasses import Item, ItemClassification
+from . import Options, data
class DLCQuestItem(Item):
game: str = "DLCQuest"
+ coins: int = 0
+ coin_suffix: str = ""
offset = 120_000
@@ -22,6 +25,10 @@ class Group(enum.Enum):
Item = enum.auto()
Coin = enum.auto()
Trap = enum.auto()
+ Twice = enum.auto()
+ Piece = enum.auto()
+ Deprecated = enum.auto()
+
@dataclass(frozen=True)
@@ -82,52 +89,75 @@ def initialize_groups():
initialize_groups()
-def create_trap_items(world, World_Options: Options.DLCQuestOptions, trap_needed: int, random: Random) -> List[Item]:
+def create_trap_items(world, world_options: Options.DLCQuestOptions, trap_needed: int, random: Random) -> List[Item]:
traps = []
for i in range(trap_needed):
trap = random.choice(items_by_group[Group.Trap])
- traps.append(world.create_item(trap))
+ traps.append(world.create_item(trap, ItemClassification.trap))
return traps
-def create_items(world, World_Options: Options.DLCQuestOptions, locations_count: int, random: Random):
+def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, random: Random):
created_items = []
- if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[
- Options.Campaign] == Options.Campaign.option_both:
- for item in items_by_group[Group.DLCQuest]:
- if item.has_any_group(Group.DLC):
- created_items.append(world.create_item(item))
- if item.has_any_group(Group.Item) and World_Options[
- Options.ItemShuffle] == Options.ItemShuffle.option_shuffled:
+ if world_options.campaign == Options.Campaign.option_basic or world_options.campaign == Options.Campaign.option_both:
+ create_items_basic(world_options, created_items, world)
+
+ if (world_options.campaign == Options.Campaign.option_live_freemium_or_die or
+ world_options.campaign == Options.Campaign.option_both):
+ create_items_lfod(world_options, created_items, world)
+
+ trap_items = create_trap_items(world, world_options, locations_count - len(created_items), random)
+ created_items += trap_items
+
+ return created_items
+
+
+def create_items_lfod(world_options, created_items, world):
+ for item in items_by_group[Group.Freemium]:
+ if item.has_any_group(Group.DLC):
+ created_items.append(world.create_item(item))
+ if item.has_any_group(Group.Item) and world_options.item_shuffle == Options.ItemShuffle.option_shuffled:
+ created_items.append(world.create_item(item))
+ if item.has_any_group(Group.Twice):
created_items.append(world.create_item(item))
- if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin:
- coin_bundle_needed = math.floor(825 / World_Options[Options.CoinSanityRange])
- for item in items_by_group[Group.DLCQuest]:
- if item.has_any_group(Group.Coin):
- for i in range(coin_bundle_needed):
- created_items.append(world.create_item(item))
- if 825 % World_Options[Options.CoinSanityRange] != 0:
- created_items.append(world.create_item(item))
-
- if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[
- Options.Campaign] == Options.Campaign.option_both:
- for item in items_by_group[Group.Freemium]:
- if item.has_any_group(Group.DLC):
+ if world_options.coinsanity == Options.CoinSanity.option_coin:
+ if world_options.coinbundlequantity == -1:
+ create_coin_piece(created_items, world, 889, 200, Group.Freemium)
+ return
+ create_coin(world_options, created_items, world, 889, 200, Group.Freemium)
+
+
+def create_items_basic(world_options, created_items, world):
+ for item in items_by_group[Group.DLCQuest]:
+ if item.has_any_group(Group.DLC):
+ created_items.append(world.create_item(item))
+ if item.has_any_group(Group.Item) and world_options.item_shuffle == Options.ItemShuffle.option_shuffled:
+ created_items.append(world.create_item(item))
+ if item.has_any_group(Group.Twice):
created_items.append(world.create_item(item))
- if item.has_any_group(Group.Item) and World_Options[
- Options.ItemShuffle] == Options.ItemShuffle.option_shuffled:
+ if world_options.coinsanity == Options.CoinSanity.option_coin:
+ if world_options.coinbundlequantity == -1:
+ create_coin_piece(created_items, world, 825, 250, Group.DLCQuest)
+ return
+ create_coin(world_options, created_items, world, 825, 250, Group.DLCQuest)
+
+
+def create_coin(world_options, created_items, world, total_coins, required_coins, group):
+ coin_bundle_required = math.ceil(required_coins / world_options.coinbundlequantity)
+ coin_bundle_useful = math.ceil((total_coins - coin_bundle_required * world_options.coinbundlequantity) / world_options.coinbundlequantity)
+ for item in items_by_group[group]:
+ if item.has_any_group(Group.Coin):
+ for i in range(coin_bundle_required):
created_items.append(world.create_item(item))
- if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin:
- coin_bundle_needed = math.floor(889 / World_Options[Options.CoinSanityRange])
- for item in items_by_group[Group.Freemium]:
- if item.has_any_group(Group.Coin):
- for i in range(coin_bundle_needed):
- created_items.append(world.create_item(item))
- if 889 % World_Options[Options.CoinSanityRange] != 0:
- created_items.append(world.create_item(item))
-
- trap_items = create_trap_items(world, World_Options, locations_count - len(created_items), random)
- created_items += trap_items
+ for i in range(coin_bundle_useful):
+ created_items.append(world.create_item(item, ItemClassification.useful))
- return created_items
+
+def create_coin_piece(created_items, world, total_coins, required_coins, group):
+ for item in items_by_group[group]:
+ if item.has_any_group(Group.Piece):
+ for i in range(required_coins*10):
+ created_items.append(world.create_item(item))
+ for i in range((total_coins - required_coins) * 10):
+ created_items.append(world.create_item(item, ItemClassification.useful))
diff --git a/worlds/dlcquest/Locations.py b/worlds/dlcquest/Locations.py
index 08d37e781216..dfc524852903 100644
--- a/worlds/dlcquest/Locations.py
+++ b/worlds/dlcquest/Locations.py
@@ -1,5 +1,4 @@
-from BaseClasses import Location, MultiWorld
-from . import Options
+from BaseClasses import Location
class DLCQuestLocation(Location):
@@ -77,3 +76,14 @@ class DLCQuestLocation(Location):
for i in range(1, 890):
item_coin_freemium = f"Live Freemium or Die: {i} Coin"
location_table[item_coin_freemium] = offset + 825 + 58 + i
+
+
+offset_special = 3829200000
+
+for i in range(1, 8251):
+ item_coin_piece = f"DLC Quest: {i} Coin Piece"
+ location_table[item_coin_piece] = offset_special + i
+
+for i in range(1, 8891):
+ item_coin_piece_freemium = f"Live Freemium or Die: {i} Coin Piece"
+ location_table[item_coin_piece_freemium] = offset_special + 8250 + i
\ No newline at end of file
diff --git a/worlds/dlcquest/Options.py b/worlds/dlcquest/Options.py
index a1674a4d5a8b..067e349b94a2 100644
--- a/worlds/dlcquest/Options.py
+++ b/worlds/dlcquest/Options.py
@@ -1,22 +1,7 @@
-from typing import Union, Dict, runtime_checkable, Protocol
-from Options import Option, DeathLink, Choice, Toggle, SpecialRange
from dataclasses import dataclass
+import datetime
-
-@runtime_checkable
-class DLCQuestOption(Protocol):
- internal_name: str
-
-
-@dataclass
-class DLCQuestOptions:
- options: Dict[str, Union[bool, int]]
-
- def __getitem__(self, item: Union[str, DLCQuestOption]) -> Union[bool, int]:
- if isinstance(item, DLCQuestOption):
- item = item.internal_name
-
- return self.options.get(item, None)
+from Options import Choice, DeathLink, NamedRange, PerGameCommonOptions
class DoubleJumpGlitch(Choice):
@@ -49,7 +34,7 @@ class CoinSanity(Choice):
default = 0
-class CoinSanityRange(SpecialRange):
+class CoinSanityRange(NamedRange):
"""This is the amount of coins in a coin bundle
You need to collect that number of coins to get a location check, and when receiving coin items, you will get bundles of this size
It is highly recommended to not set this value below 10, as it generates a very large number of boring locations and items.
@@ -64,6 +49,20 @@ class CoinSanityRange(SpecialRange):
"normal": 20,
"high": 50,
}
+ if datetime.datetime.today().month == 4:
+ if datetime.datetime.today().day == 1:
+ special_range_names["surprise"] = -1
+ else:
+ special_range_names["coin piece"] = -1
+
+
+class PermanentCoins(Choice):
+ """If purchasing a pack decreases your current coins amounts."""
+ internal_name = "permanent_coins"
+ display_name = "Permanent Coins"
+ option_false = 0
+ option_true = 1
+ default = 0
class EndingChoice(Choice):
@@ -94,31 +93,14 @@ class ItemShuffle(Choice):
default = 0
-DLCQuest_options: Dict[str, type(Option)] = {
- option.internal_name: option
- for option in [
- DoubleJumpGlitch,
- CoinSanity,
- CoinSanityRange,
- TimeIsMoney,
- EndingChoice,
- Campaign,
- ItemShuffle,
- ]
-}
-default_options = {option.internal_name: option.default for option in DLCQuest_options.values()}
-DLCQuest_options["death_link"] = DeathLink
-
-
-def fetch_options(world, player: int) -> DLCQuestOptions:
- return DLCQuestOptions({option: get_option_value(world, player, option) for option in DLCQuest_options})
-
-
-def get_option_value(world, player: int, name: str) -> Union[bool, int]:
- assert name in DLCQuest_options, f"{name} is not a valid option for DLC Quest."
-
- value = getattr(world, name)
-
- if issubclass(DLCQuest_options[name], Toggle):
- return bool(value[player].value)
- return value[player].value
+@dataclass
+class DLCQuestOptions(PerGameCommonOptions):
+ double_jump_glitch: DoubleJumpGlitch
+ coinsanity: CoinSanity
+ coinbundlequantity: CoinSanityRange
+ permanent_coins: PermanentCoins
+ time_is_money: TimeIsMoney
+ ending_choice: EndingChoice
+ campaign: Campaign
+ item_shuffle: ItemShuffle
+ death_link: DeathLink
diff --git a/worlds/dlcquest/Regions.py b/worlds/dlcquest/Regions.py
index 8135a1c362c5..5b256afd45a6 100644
--- a/worlds/dlcquest/Regions.py
+++ b/worlds/dlcquest/Regions.py
@@ -1,325 +1,203 @@
import math
-from BaseClasses import MultiWorld, Region, Location, Entrance, ItemClassification
+from typing import List
+
+from BaseClasses import Entrance, MultiWorld, Region
+from . import Options
from .Locations import DLCQuestLocation, location_table
from .Rules import create_event
-from . import Options
DLCQuestRegion = ["Movement Pack", "Behind Tree", "Psychological Warfare", "Double Jump Left",
"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):
- number_coin = f"{Coin} coins"
- location_coin = f"{region.name} coins"
+def add_coin_dlcquest(region: Region, coin: int, player: int):
+ add_coin(region, coin, player, " 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[Options.Campaign] == Options.Campaign.option_basic or World_Options[
- Options.Campaign] == Options.Campaign.option_both:
- Regmenu.exits += [Entrance(player, "DLC Quest Basic", Regmenu)]
- if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[
- Options.Campaign] == Options.Campaign.option_both:
- Regmenu.exits += [Entrance(player, "Live Freemium or Die", Regmenu)]
- world.regions.append(Regmenu)
-
- if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[
- 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[Options.CoinSanity] == Options.CoinSanity.option_coin:
- coin_bundle_needed = math.floor(825 / World_Options[Options.CoinSanityRange])
- for i in range(coin_bundle_needed):
- item_coin = f"DLC Quest: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin"
- Regmoveright.locations += [
- DLCQuestLocation(player, item_coin, location_table[item_coin], Regmoveright)]
- if 825 % World_Options[Options.CoinSanityRange] != 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[Options.ItemShuffle] == 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[Options.ItemShuffle] == 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[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[
- 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[Options.ItemShuffle] == 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[Options.CoinSanity] == Options.CoinSanity.option_coin:
- coin_bundle_needed = math.floor(889 / World_Options[Options.CoinSanityRange])
- for i in range(coin_bundle_needed):
- item_coin_freemium = f"Live Freemium or Die: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin"
- Regfreemiumstart.locations += [
- DLCQuestLocation(player, item_coin_freemium, location_table[item_coin_freemium],
- Regfreemiumstart)]
- if 889 % World_Options[Options.CoinSanityRange] != 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[Options.ItemShuffle] == 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[Options.ItemShuffle] == 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[Options.ItemShuffle] == 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))
+ event = create_event(player, number_coin)
+ event.coins = coin
+ event.coin_suffix = suffix
+ location.place_locked_item(event)
+
+
+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
+ if coin_bundle_size == -1:
+ create_coinsanity_piece_locations(player, region, last_coin_number, campaign_prefix)
+ 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)]
+
+
+def create_coinsanity_piece_locations(player: int, region: Region, total_coin: int, campaign_prefix:str):
+
+ pieces_needed = total_coin * 10
+ for i in range(1, pieces_needed + 1):
+ number_piece = i
+ item_piece = f"{campaign_prefix}: {number_piece} Coin Piece"
+ region.locations += [DLCQuestLocation(player, item_piece, location_table[item_piece], region)]
diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py
index d2495121f4e8..3461d0633ef1 100644
--- a/worlds/dlcquest/Rules.py
+++ b/worlds/dlcquest/Rules.py
@@ -1,57 +1,40 @@
import math
-import re
-from .Locations import DLCQuestLocation
-from ..generic.Rules import add_rule, set_rule, item_name_in_locations
-from .Items import DLCQuestItem
+
from BaseClasses import ItemClassification
+from worlds.generic.Rules import add_rule, item_name_in_locations, set_rule
from . import Options
+from .Items import DLCQuestItem
-def create_event(player, event: str):
+def create_event(player, event: str) -> DLCQuestItem:
return DLCQuestItem(event, ItemClassification.progression, None, player)
-def set_rules(world, player, World_Options: Options.DLCQuestOptions):
- def has_enough_coin(player: int, coin: int):
- def has_coin(state, player: int, coins: int):
- coin_possessed = 0
- for i in [4, 7, 9, 10, 46, 50, 60, 76, 89, 100, 171, 203]:
- name_coin = f"{i} coins"
- if state.has(name_coin, player):
- coin_possessed += i
-
- return coin_possessed >= coins
+def has_enough_coin(player: int, coin: int):
+ return lambda state: state.prog_items[player][" coins"] >= coin
- return lambda state: has_coin(state, player, coin)
- def has_enough_coin_freemium(player: int, coin: int):
- def has_coin(state, player: int, coins: int):
- coin_possessed = 0
- for i in [20, 50, 90, 95, 130, 150, 154, 200]:
- name_coin = f"{i} coins freemium"
- if state.has(name_coin, player):
- coin_possessed += i
+def has_enough_coin_freemium(player: int, coin: int):
+ return lambda state: state.prog_items[player][" coins freemium"] >= coin
- return coin_possessed >= coins
- return lambda state: has_coin(state, player, coin)
+def set_rules(world, player, world_options: Options.DLCQuestOptions):
+ set_basic_rules(world_options, player, world)
+ set_lfod_rules(world_options, player, world)
+ set_completion_condition(world_options, player, world)
- set_basic_rules(World_Options, has_enough_coin, player, world)
- set_lfod_rules(World_Options, has_enough_coin_freemium, player, world)
- set_completion_condition(World_Options, player, world)
-
-def set_basic_rules(World_Options, has_enough_coin, player, world):
- if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die:
+def set_basic_rules(world_options, player, world):
+ if world_options.campaign == Options.Campaign.option_live_freemium_or_die:
return
set_basic_entrance_rules(player, world)
- set_basic_self_obtained_items_rules(World_Options, player, world)
- set_basic_shuffled_items_rules(World_Options, player, world)
- set_double_jump_glitchless_rules(World_Options, player, world)
- set_easy_double_jump_glitch_rules(World_Options, player, world)
- self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world)
- set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world)
- self_basic_win_condition(World_Options, player, world)
+ set_basic_self_obtained_items_rules(world_options, player, world)
+ set_basic_shuffled_items_rules(world_options, player, world)
+ set_double_jump_glitchless_rules(world_options, player, world)
+ set_easy_double_jump_glitch_rules(world_options, player, world)
+ self_basic_coinsanity_funded_purchase_rules(world_options, player, world)
+ set_basic_self_funded_purchase_rules(world_options, player, world)
+ self_basic_win_condition(world_options, player, world)
def set_basic_entrance_rules(player, world):
@@ -65,13 +48,13 @@ def set_basic_entrance_rules(player, world):
lambda state: state.has("Double Jump Pack", player))
-def set_basic_self_obtained_items_rules(World_Options, player, world):
- if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_disabled:
+def set_basic_self_obtained_items_rules(world_options, player, world):
+ if world_options.item_shuffle != Options.ItemShuffle.option_disabled:
return
set_rule(world.get_entrance("Behind Ogre", player),
lambda state: state.has("Gun Pack", player))
- if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required:
+ if world_options.time_is_money == Options.TimeIsMoney.option_required:
set_rule(world.get_entrance("Tree", player),
lambda state: state.has("Time is Money Pack", player))
set_rule(world.get_entrance("Cave Tree", player),
@@ -86,35 +69,35 @@ def set_basic_self_obtained_items_rules(World_Options, player, world):
lambda state: state.has("Time is Money Pack", player))
-def set_basic_shuffled_items_rules(World_Options, player, world):
- if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_shuffled:
+def set_basic_shuffled_items_rules(world_options, player, world):
+ if world_options.item_shuffle != Options.ItemShuffle.option_shuffled:
return
set_rule(world.get_entrance("Behind Ogre", player),
- lambda state: state.has("Gun", player))
+ lambda state: state.has("DLC Quest: Progressive Weapon", player, 2))
set_rule(world.get_entrance("Tree", player),
- lambda state: state.has("Sword", player) or state.has("Gun", player))
+ lambda state: state.has("DLC Quest: Progressive Weapon", player))
set_rule(world.get_entrance("Cave Tree", player),
- lambda state: state.has("Sword", player) or state.has("Gun", player))
+ lambda state: state.has("DLC Quest: Progressive Weapon", player))
set_rule(world.get_entrance("True Double Jump", player),
lambda state: state.has("Double Jump Pack", player))
set_rule(world.get_location("Shepherd Sheep", player),
- lambda state: state.has("Sword", player) or state.has("Gun", player))
+ lambda state: state.has("DLC Quest: Progressive Weapon", player))
set_rule(world.get_location("North West Ceiling Sheep", player),
- lambda state: state.has("Sword", player) or state.has("Gun", player))
+ lambda state: state.has("DLC Quest: Progressive Weapon", player))
set_rule(world.get_location("North West Alcove Sheep", player),
- lambda state: state.has("Sword", player) or state.has("Gun", player))
+ lambda state: state.has("DLC Quest: Progressive Weapon", player))
set_rule(world.get_location("West Cave Sheep", player),
- lambda state: state.has("Sword", player) or state.has("Gun", player))
+ lambda state: state.has("DLC Quest: Progressive Weapon", player))
set_rule(world.get_location("Gun", player),
lambda state: state.has("Gun Pack", player))
- if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required:
+ if world_options.time_is_money == Options.TimeIsMoney.option_required:
set_rule(world.get_location("Sword", player),
lambda state: state.has("Time is Money Pack", player))
-def set_double_jump_glitchless_rules(World_Options, player, world):
- if World_Options[Options.DoubleJumpGlitch] != Options.DoubleJumpGlitch.option_none:
+def set_double_jump_glitchless_rules(world_options, player, world):
+ if world_options.double_jump_glitch != Options.DoubleJumpGlitch.option_none:
return
set_rule(world.get_entrance("Cloud Double Jump", player),
lambda state: state.has("Double Jump Pack", player))
@@ -122,8 +105,8 @@ def set_double_jump_glitchless_rules(World_Options, player, world):
lambda state: state.has("Double Jump Pack", player))
-def set_easy_double_jump_glitch_rules(World_Options, player, world):
- if World_Options[Options.DoubleJumpGlitch] == Options.DoubleJumpGlitch.option_all:
+def set_easy_double_jump_glitch_rules(world_options, player, world):
+ if world_options.double_jump_glitch == Options.DoubleJumpGlitch.option_all:
return
set_rule(world.get_entrance("Behind Tree Double Jump", player),
lambda state: state.has("Double Jump Pack", player))
@@ -131,71 +114,74 @@ def set_easy_double_jump_glitch_rules(World_Options, player, world):
lambda state: state.has("Double Jump Pack", player))
-def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world):
- if World_Options[Options.CoinSanity] != Options.CoinSanity.option_coin:
+def self_basic_coinsanity_funded_purchase_rules(world_options, player, world):
+ if world_options.coinsanity != Options.CoinSanity.option_coin:
+ return
+ if world_options.coinbundlequantity == -1:
+ self_basic_coinsanity_piece_rules(player, world)
return
- number_of_bundle = math.floor(825 / World_Options[Options.CoinSanityRange])
+ number_of_bundle = math.floor(825 / world_options.coinbundlequantity)
for i in range(number_of_bundle):
- item_coin = f"DLC Quest: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin"
+ item_coin = f"DLC Quest: {world_options.coinbundlequantity * (i + 1)} Coin"
set_rule(world.get_location(item_coin, player),
- has_enough_coin(player, World_Options[Options.CoinSanityRange] * (i + 1)))
- if 825 % World_Options[Options.CoinSanityRange] != 0:
+ has_enough_coin(player, world_options.coinbundlequantity * (i + 1)))
+ if 825 % world_options.coinbundlequantity != 0:
set_rule(world.get_location("DLC Quest: 825 Coin", player),
has_enough_coin(player, 825))
set_rule(world.get_location("Movement Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(4 / World_Options[Options.CoinSanityRange])))
+ math.ceil(4 / world_options.coinbundlequantity)))
set_rule(world.get_location("Animation Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(5 / World_Options[Options.CoinSanityRange])))
+ math.ceil(5 / world_options.coinbundlequantity)))
set_rule(world.get_location("Audio Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(5 / World_Options[Options.CoinSanityRange])))
+ math.ceil(5 / world_options.coinbundlequantity)))
set_rule(world.get_location("Pause Menu Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(5 / World_Options[Options.CoinSanityRange])))
+ math.ceil(5 / world_options.coinbundlequantity)))
set_rule(world.get_location("Time is Money Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(20 / World_Options[Options.CoinSanityRange])))
+ math.ceil(20 / world_options.coinbundlequantity)))
set_rule(world.get_location("Double Jump Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(100 / World_Options[Options.CoinSanityRange])))
+ math.ceil(100 / world_options.coinbundlequantity)))
set_rule(world.get_location("Pet Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(5 / World_Options[Options.CoinSanityRange])))
+ math.ceil(5 / world_options.coinbundlequantity)))
set_rule(world.get_location("Sexy Outfits Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(5 / World_Options[Options.CoinSanityRange])))
+ math.ceil(5 / world_options.coinbundlequantity)))
set_rule(world.get_location("Top Hat Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(5 / World_Options[Options.CoinSanityRange])))
+ math.ceil(5 / world_options.coinbundlequantity)))
set_rule(world.get_location("Map Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(140 / World_Options[Options.CoinSanityRange])))
+ math.ceil(140 / world_options.coinbundlequantity)))
set_rule(world.get_location("Gun Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(75 / World_Options[Options.CoinSanityRange])))
+ math.ceil(75 / world_options.coinbundlequantity)))
set_rule(world.get_location("The Zombie Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(5 / World_Options[Options.CoinSanityRange])))
+ math.ceil(5 / world_options.coinbundlequantity)))
set_rule(world.get_location("Night Map Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(75 / World_Options[Options.CoinSanityRange])))
+ math.ceil(75 / world_options.coinbundlequantity)))
set_rule(world.get_location("Psychological Warfare Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(50 / World_Options[Options.CoinSanityRange])))
+ math.ceil(50 / world_options.coinbundlequantity)))
set_rule(world.get_location("Armor for your Horse Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(250 / World_Options[Options.CoinSanityRange])))
+ math.ceil(250 / world_options.coinbundlequantity)))
set_rule(world.get_location("Finish the Fight Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", player,
- math.ceil(5 / World_Options[Options.CoinSanityRange])))
+ math.ceil(5 / world_options.coinbundlequantity)))
-def set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world):
- if World_Options[Options.CoinSanity] != Options.CoinSanity.option_none:
+def set_basic_self_funded_purchase_rules(world_options, player, world):
+ if world_options.coinsanity != Options.CoinSanity.option_none:
return
set_rule(world.get_location("Movement Pack", player),
has_enough_coin(player, 4))
@@ -231,25 +217,25 @@ def set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player,
has_enough_coin(player, 5))
-def self_basic_win_condition(World_Options, player, world):
- if World_Options[Options.EndingChoice] == Options.EndingChoice.option_any:
+def self_basic_win_condition(world_options, player, world):
+ if world_options.ending_choice == Options.EndingChoice.option_any:
set_rule(world.get_location("Winning Basic", player),
lambda state: state.has("Finish the Fight Pack", player))
- if World_Options[Options.EndingChoice] == Options.EndingChoice.option_true:
+ if world_options.ending_choice == Options.EndingChoice.option_true:
set_rule(world.get_location("Winning Basic", player),
lambda state: state.has("Armor for your Horse Pack", player) and state.has("Finish the Fight Pack",
player))
-def set_lfod_rules(World_Options, has_enough_coin_freemium, player, world):
- if World_Options[Options.Campaign] == Options.Campaign.option_basic:
+def set_lfod_rules(world_options, player, world):
+ if world_options.campaign == Options.Campaign.option_basic:
return
set_lfod_entrance_rules(player, world)
set_boss_door_requirements_rules(player, world)
- set_lfod_self_obtained_items_rules(World_Options, player, world)
- set_lfod_shuffled_items_rules(World_Options, player, world)
- self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world)
- set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world)
+ set_lfod_self_obtained_items_rules(world_options, player, world)
+ set_lfod_shuffled_items_rules(world_options, player, world)
+ self_lfod_coinsanity_funded_purchase_rules(world_options, player, world)
+ set_lfod_self_funded_purchase_rules(world_options, has_enough_coin_freemium, player, world)
def set_lfod_entrance_rules(player, world):
@@ -267,8 +253,6 @@ def set_lfod_entrance_rules(player, world):
lambda state: state.has("Death of Comedy Pack", player))
set_rule(world.get_location("Story is Important", player),
lambda state: state.has("DLC NPC Pack", player))
- set_rule(world.get_entrance("Pickaxe Hard Cave", player),
- lambda state: state.has("Pickaxe", player))
def set_boss_door_requirements_rules(player, world):
@@ -296,8 +280,8 @@ def set_boss_door_requirements_rules(player, world):
set_rule(world.get_entrance("Boss Door", player), has_3_swords)
-def set_lfod_self_obtained_items_rules(World_Options, player, world):
- if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_disabled:
+def set_lfod_self_obtained_items_rules(world_options, player, world):
+ if world_options.item_shuffle != Options.ItemShuffle.option_disabled:
return
set_rule(world.get_entrance("Vines", player),
lambda state: state.has("Incredibly Important Pack", player))
@@ -308,13 +292,15 @@ def set_lfod_self_obtained_items_rules(World_Options, player, world):
state.has("Name Change Pack", player))
-def set_lfod_shuffled_items_rules(World_Options, player, world):
- if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_shuffled:
+def set_lfod_shuffled_items_rules(world_options, player, world):
+ if world_options.item_shuffle != Options.ItemShuffle.option_shuffled:
return
set_rule(world.get_entrance("Vines", player),
- lambda state: state.has("Wooden Sword", player) or state.has("Pickaxe", player))
+ lambda state: state.has("Live Freemium or Die: Progressive Weapon", player))
set_rule(world.get_entrance("Behind Rocks", player),
- lambda state: state.has("Pickaxe", player))
+ lambda state: state.has("Live Freemium or Die: Progressive Weapon", player, 2))
+ set_rule(world.get_entrance("Pickaxe Hard Cave", player),
+ lambda state: state.has("Live Freemium or Die: Progressive Weapon", player, 2))
set_rule(world.get_location("Wooden Sword", player),
lambda state: state.has("Incredibly Important Pack", player))
@@ -327,83 +313,84 @@ def set_lfod_shuffled_items_rules(World_Options, player, world):
lambda state: state.can_reach("Cut Content", 'region', player))
-def self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world):
- if World_Options[Options.CoinSanity] != Options.CoinSanity.option_coin:
+def self_lfod_coinsanity_funded_purchase_rules(world_options, player, world):
+ if world_options.coinsanity != Options.CoinSanity.option_coin:
+ return
+ if world_options.coinbundlequantity == -1:
+ self_lfod_coinsanity_piece_rules(player, world)
return
- number_of_bundle = math.floor(889 / World_Options[Options.CoinSanityRange])
+ number_of_bundle = math.floor(889 / world_options.coinbundlequantity)
for i in range(number_of_bundle):
- item_coin_freemium = "Live Freemium or Die: number Coin"
- item_coin_loc_freemium = re.sub("number", str(World_Options[Options.CoinSanityRange] * (i + 1)),
- item_coin_freemium)
- set_rule(world.get_location(item_coin_loc_freemium, player),
- has_enough_coin_freemium(player, World_Options[Options.CoinSanityRange] * (i + 1)))
- if 889 % World_Options[Options.CoinSanityRange] != 0:
+ item_coin_freemium = f"Live Freemium or Die: {world_options.coinbundlequantity * (i + 1)} Coin"
+ set_rule(world.get_location(item_coin_freemium, player),
+ has_enough_coin_freemium(player, world_options.coinbundlequantity * (i + 1)))
+ if 889 % world_options.coinbundlequantity != 0:
set_rule(world.get_location("Live Freemium or Die: 889 Coin", player),
has_enough_coin_freemium(player, 889))
add_rule(world.get_entrance("Boss Door", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(889 / World_Options[Options.CoinSanityRange])))
+ math.ceil(200 / world_options.coinbundlequantity)))
set_rule(world.get_location("Particles Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(5 / World_Options[Options.CoinSanityRange])))
+ math.ceil(5 / world_options.coinbundlequantity)))
set_rule(world.get_location("Day One Patch Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(5 / World_Options[Options.CoinSanityRange])))
+ math.ceil(5 / world_options.coinbundlequantity)))
set_rule(world.get_location("Checkpoint Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(5 / World_Options[Options.CoinSanityRange])))
+ math.ceil(5 / world_options.coinbundlequantity)))
set_rule(world.get_location("Incredibly Important Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(15 / World_Options[Options.CoinSanityRange])))
+ math.ceil(15 / world_options.coinbundlequantity)))
set_rule(world.get_location("Wall Jump Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(35 / World_Options[Options.CoinSanityRange])))
+ math.ceil(35 / world_options.coinbundlequantity)))
set_rule(world.get_location("Health Bar Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(5 / World_Options[Options.CoinSanityRange])))
+ math.ceil(5 / world_options.coinbundlequantity)))
set_rule(world.get_location("Parallax Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(5 / World_Options[Options.CoinSanityRange])))
+ math.ceil(5 / world_options.coinbundlequantity)))
set_rule(world.get_location("Harmless Plants Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(130 / World_Options[Options.CoinSanityRange])))
+ math.ceil(130 / world_options.coinbundlequantity)))
set_rule(world.get_location("Death of Comedy Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(15 / World_Options[Options.CoinSanityRange])))
+ math.ceil(15 / world_options.coinbundlequantity)))
set_rule(world.get_location("Canadian Dialog Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(10 / World_Options[Options.CoinSanityRange])))
+ math.ceil(10 / world_options.coinbundlequantity)))
set_rule(world.get_location("DLC NPC Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(15 / World_Options[Options.CoinSanityRange])))
+ math.ceil(15 / world_options.coinbundlequantity)))
set_rule(world.get_location("Cut Content Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(40 / World_Options[Options.CoinSanityRange])))
+ math.ceil(40 / world_options.coinbundlequantity)))
set_rule(world.get_location("Name Change Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(150 / World_Options[Options.CoinSanityRange])))
+ math.ceil(150 / world_options.coinbundlequantity)))
set_rule(world.get_location("Season Pass", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(199 / World_Options[Options.CoinSanityRange])))
+ math.ceil(199 / world_options.coinbundlequantity)))
set_rule(world.get_location("High Definition Next Gen Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(20 / World_Options[Options.CoinSanityRange])))
+ math.ceil(20 / world_options.coinbundlequantity)))
set_rule(world.get_location("Increased HP Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(10 / World_Options[Options.CoinSanityRange])))
+ math.ceil(10 / world_options.coinbundlequantity)))
set_rule(world.get_location("Remove Ads Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", player,
- math.ceil(25 / World_Options[Options.CoinSanityRange])))
+ math.ceil(25 / world_options.coinbundlequantity)))
-def set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world):
- if World_Options[Options.CoinSanity] != Options.CoinSanity.option_none:
+def set_lfod_self_funded_purchase_rules(world_options, has_enough_coin_freemium, player, world):
+ if world_options.coinsanity != Options.CoinSanity.option_none:
return
add_rule(world.get_entrance("Boss Door", player),
- has_enough_coin_freemium(player, 889))
+ has_enough_coin_freemium(player, 200))
set_rule(world.get_location("Particles Pack", player),
has_enough_coin_freemium(player, 5))
@@ -441,11 +428,98 @@ def set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium,
has_enough_coin_freemium(player, 25))
-def set_completion_condition(World_Options, player, world):
- if World_Options[Options.Campaign] == Options.Campaign.option_basic:
+def set_completion_condition(world_options, player, world):
+ if world_options.campaign == Options.Campaign.option_basic:
world.completion_condition[player] = lambda state: state.has("Victory Basic", player)
- if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die:
+ if world_options.campaign == Options.Campaign.option_live_freemium_or_die:
world.completion_condition[player] = lambda state: state.has("Victory Freemium", player)
- if World_Options[Options.Campaign] == Options.Campaign.option_both:
+ if world_options.campaign == Options.Campaign.option_both:
world.completion_condition[player] = lambda state: state.has("Victory Basic", player) and state.has(
"Victory Freemium", player)
+
+
+def self_basic_coinsanity_piece_rules(player, world):
+ for i in range(1,8251):
+
+ item_coin = f"DLC Quest: {i} Coin Piece"
+ set_rule(world.get_location(item_coin, player),
+ has_enough_coin(player, math.ceil(i / 10)))
+
+ set_rule(world.get_location("Movement Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 40))
+ set_rule(world.get_location("Animation Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 50))
+ set_rule(world.get_location("Audio Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 50))
+ set_rule(world.get_location("Pause Menu Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 50))
+ set_rule(world.get_location("Time is Money Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 200))
+ set_rule(world.get_location("Double Jump Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 100))
+ set_rule(world.get_location("Pet Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 50))
+ set_rule(world.get_location("Sexy Outfits Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 50))
+ set_rule(world.get_location("Top Hat Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 50))
+ set_rule(world.get_location("Map Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 1400))
+ set_rule(world.get_location("Gun Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 750))
+ set_rule(world.get_location("The Zombie Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 50))
+ set_rule(world.get_location("Night Map Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 750))
+ set_rule(world.get_location("Psychological Warfare Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 500))
+ set_rule(world.get_location("Armor for your Horse Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 2500))
+ set_rule(world.get_location("Finish the Fight Pack", player),
+ lambda state: state.has("DLC Quest: Coin Piece", player, 50))
+
+
+def self_lfod_coinsanity_piece_rules(player, world):
+ for i in range(1, 8891):
+
+ item_coin_freemium = f"Live Freemium or Die: {i} Coin Piece"
+ set_rule(world.get_location(item_coin_freemium, player),
+ has_enough_coin_freemium(player, math.ceil(i / 10)))
+
+ add_rule(world.get_entrance("Boss Door", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 2000))
+
+ set_rule(world.get_location("Particles Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 50))
+ set_rule(world.get_location("Day One Patch Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 50))
+ set_rule(world.get_location("Checkpoint Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 50))
+ set_rule(world.get_location("Incredibly Important Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 150))
+ set_rule(world.get_location("Wall Jump Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 350))
+ set_rule(world.get_location("Health Bar Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 50))
+ set_rule(world.get_location("Parallax Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 50))
+ set_rule(world.get_location("Harmless Plants Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 1300))
+ set_rule(world.get_location("Death of Comedy Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 150))
+ set_rule(world.get_location("Canadian Dialog Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 100))
+ set_rule(world.get_location("DLC NPC Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 150))
+ set_rule(world.get_location("Cut Content Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 400))
+ set_rule(world.get_location("Name Change Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 1500))
+ set_rule(world.get_location("Season Pass", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 199))
+ set_rule(world.get_location("High Definition Next Gen Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 20))
+ set_rule(world.get_location("Increased HP Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 100))
+ set_rule(world.get_location("Remove Ads Pack", player),
+ lambda state: state.has("Live Freemium or Die: Coin Piece", player, 250))
diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py
index 9569d0efcc1a..b8f2aad6ff94 100644
--- a/worlds/dlcquest/__init__.py
+++ b/worlds/dlcquest/__init__.py
@@ -1,25 +1,39 @@
-from typing import Dict, Any, Iterable, Optional, Union
-from BaseClasses import Tutorial
-from worlds.AutoWorld import World, WebWorld
-from .Items import DLCQuestItem, item_table, ItemData, create_items
-from .Locations import location_table, DLCQuestLocation
-from .Options import DLCQuest_options, DLCQuestOptions, fetch_options
-from .Rules import set_rules
-from .Regions import create_regions
+from typing import Union
+
+from BaseClasses import Tutorial, CollectionState, ItemClassification
+from worlds.AutoWorld import WebWorld, World
from . import Options
+from .Items import DLCQuestItem, ItemData, create_items, item_table, items_by_group, Group
+from .Locations import DLCQuestLocation, location_table
+from .Options import DLCQuestOptions
+from .Regions import create_regions
+from .Rules import set_rules
+from .presets import dlcq_options_presets
+from .option_groups import dlcq_option_groups
client_version = 0
class DLCqwebworld(WebWorld):
- tutorials = [Tutorial(
- "Multiworld Setup Tutorial",
+ options_presets = dlcq_options_presets
+ option_groups = dlcq_option_groups
+ setup_en = Tutorial(
+ "Multiworld Setup Guide",
"A guide to setting up the Archipelago DLCQuest game on your computer.",
"English",
"setup_en.md",
"setup/en",
["axe_y"]
- )]
+ )
+ setup_fr = Tutorial(
+ "Guide de configuration MultiWorld",
+ "Un guide pour configurer DLCQuest sur votre PC.",
+ "Français",
+ "setup_fr.md",
+ "setup/fr",
+ ["Deoxis"]
+ )
+ tutorials = [setup_en, setup_fr]
class DLCqworld(World):
@@ -33,12 +47,8 @@ class DLCqworld(World):
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = location_table
- data_version = 1
-
- option_definitions = DLCQuest_options
-
- def generate_early(self):
- self.options = fetch_options(self.multiworld, self.player)
+ options_dataclass = DLCQuestOptions
+ options: DLCQuestOptions
def create_regions(self):
create_regions(self.multiworld, self.player, self.options)
@@ -53,7 +63,7 @@ def create_items(self):
self.precollect_coinsanity()
locations_count = len([location
for location in self.multiworld.get_locations(self.player)
- if not location.event])
+ if not location.advancement])
items_to_exclude = [excluded_items
for excluded_items in self.multiworld.precollected_items[self.player]]
@@ -61,31 +71,53 @@ def create_items(self):
created_items = create_items(self, self.options, locations_count + len(items_to_exclude), self.multiworld.random)
self.multiworld.itempool += created_items
- self.multiworld.early_items[self.player]["Movement Pack"] = 1
+
+ if self.options.campaign == Options.Campaign.option_basic or self.options.campaign == Options.Campaign.option_both:
+ self.multiworld.early_items[self.player]["Movement Pack"] = 1
for item in items_to_exclude:
if item in self.multiworld.itempool:
self.multiworld.itempool.remove(item)
def precollect_coinsanity(self):
- if self.options[Options.Campaign] == Options.Campaign.option_basic:
- if self.options[Options.CoinSanity] == Options.CoinSanity.option_coin and self.options[Options.CoinSanityRange] >= 5:
+ if self.options.campaign == Options.Campaign.option_basic:
+ if self.options.coinsanity == Options.CoinSanity.option_coin and self.options.coinbundlequantity >= 5:
self.multiworld.push_precollected(self.create_item("Movement Pack"))
-
- def create_item(self, item: Union[str, ItemData]) -> DLCQuestItem:
+ def create_item(self, item: Union[str, ItemData], classification: ItemClassification = None) -> DLCQuestItem:
if isinstance(item, str):
item = item_table[item]
+ if classification is None:
+ classification = item.classification
+
+ return DLCQuestItem(item.name, classification, item.code, self.player)
- return DLCQuestItem(item.name, item.classification, item.code, self.player)
+ def get_filler_item_name(self) -> str:
+ trap = self.multiworld.random.choice(items_by_group[Group.Trap])
+ return trap.name
def fill_slot_data(self):
- return {
- "death_link": self.multiworld.death_link[self.player].value,
- "ending_choice": self.multiworld.ending_choice[self.player].value,
- "campaign": self.multiworld.campaign[self.player].value,
- "coinsanity": self.multiworld.coinsanity[self.player].value,
- "coinbundlerange": self.multiworld.coinbundlequantity[self.player].value,
- "item_shuffle": self.multiworld.item_shuffle[self.player].value,
- "seed": self.multiworld.per_slot_randoms[self.player].randrange(99999999)
- }
+ options_dict = self.options.as_dict(
+ "death_link", "ending_choice", "campaign", "coinsanity", "item_shuffle", "permanent_coins"
+ )
+ options_dict.update({
+ "coinbundlerange": self.options.coinbundlequantity.value,
+ "seed": self.random.randrange(99999999)
+ })
+ return options_dict
+
+ def collect(self, state: CollectionState, item: DLCQuestItem) -> bool:
+ change = super().collect(state, item)
+ if change:
+ suffix = item.coin_suffix
+ if suffix:
+ state.prog_items[self.player][suffix] += item.coins
+ return change
+
+ def remove(self, state: CollectionState, item: DLCQuestItem) -> bool:
+ change = super().remove(state, item)
+ if change:
+ suffix = item.coin_suffix
+ if suffix:
+ state.prog_items[self.player][suffix] -= item.coins
+ return change
diff --git a/worlds/dlcquest/data/items.csv b/worlds/dlcquest/data/items.csv
index cc5ac0bbe438..82150254b3c1 100644
--- a/worlds/dlcquest/data/items.csv
+++ b/worlds/dlcquest/data/items.csv
@@ -27,8 +27,8 @@ id,name,classification,groups
25,Canadian Dialog Pack,filler,"DLC,Freemium"
26,DLC NPC Pack,progression,"DLC,Freemium"
27,Cut Content Pack,progression,"DLC,Freemium"
-28,Name Change Pack,progression,"DLC,Freemium"
-29,Pickaxe,progression,"Item,Freemium"
+28,Name Change Pack,progression,"DLC,Freemium,Trap"
+29,Pickaxe,progression,"Deprecated"
30,Season Pass,progression,"DLC,Freemium"
31,High Definition Next Gen Pack,filler,"DLC,Freemium"
32,Increased HP Pack,useful,"DLC,Freemium"
@@ -36,13 +36,17 @@ id,name,classification,groups
34,Big Sword Pack,progression,"DLC,Freemium"
35,Really Big Sword Pack,progression,"DLC,Freemium"
36,Unfathomable Sword Pack,progression,"DLC,Freemium"
-37,Gun,progression,"Item,DLCQuest"
-38,Sword,progression,"Item,DLCQuest"
-39,Wooden Sword,progression,"Item,Freemium"
+37,Gun,progression,"Deprecated"
+38,Sword,progression,"Deprecated"
+39,Wooden Sword,progression,"Deprecated"
40,Box of Various Supplies,progression,"Item,Freemium"
41,Humble Indie Bindle,progression,"Item,Freemium"
42,DLC Quest: Coin Bundle,progression,"Coin,DLCQuest"
43,Live Freemium or Die: Coin Bundle,progression,"Coin,Freemium"
44,Zombie Sheep,trap,Trap
45,Temporary Spike,trap,Trap
-46,Loading Screen,trap,Trap
\ No newline at end of file
+46,Loading Screen,trap,Trap
+48,DLC Quest: Progressive Weapon,progression,"Item,Twice,DLCQuest"
+49,Live Freemium or Die: Progressive Weapon,progression,"Item,Twice,Freemium"
+50,DLC Quest: Coin Piece,progression,"Piece,DLCQuest"
+51,Live Freemium or Die: Coin Piece,progression,"Piece,Freemium"
\ No newline at end of file
diff --git a/worlds/dlcquest/docs/en_DLCQuest.md b/worlds/dlcquest/docs/en_DLCQuest.md
index eaccc8ff0a46..0ae8f2291e1f 100644
--- a/worlds/dlcquest/docs/en_DLCQuest.md
+++ b/worlds/dlcquest/docs/en_DLCQuest.md
@@ -1,8 +1,8 @@
# DLC Quest
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
diff --git a/worlds/dlcquest/docs/fr_DLCQuest.md b/worlds/dlcquest/docs/fr_DLCQuest.md
new file mode 100644
index 000000000000..25f2d728160e
--- /dev/null
+++ b/worlds/dlcquest/docs/fr_DLCQuest.md
@@ -0,0 +1,49 @@
+# DLC Quest
+
+## Où se trouve la page des paramètres ?
+
+La [page des paramètres du joueur pour ce jeu](../player-options) contient tous les paramètres dont vous avez besoin pour configurer et exporter le fichier.
+
+
+## Quel est l'effet de la randomisation sur ce jeu ?
+
+Les DLC seront obtenus en tant que check pour le multiworld. Il existe également d'autres checks optionnels dans DLC Quest.
+
+## Quel est le but de DLC Quest ?
+
+DLC Quest a deux campagnes, et le joueur peut choisir celle qu'il veut jouer pour sa partie.
+Il peut également choisir de faire les deux campagnes.
+
+
+## Quels sont les emplacements dans DLC quest ?
+
+Les emplacements dans DLC Quest comprennent toujours
+- les achats de DLC auprès du commerçant
+- Les objectifs liés aux récompenses
+ - Tuer des moutons dans DLC Quest
+ - Objectifs spécifiques de l'attribution dans Live Freemium or Die
+
+Il existe également un certain nombres de critères de localisation qui sont optionnels et que les joueurs peuvent choisir d'inclure ou non dans leur sélection :
+- Objets que votre personnage peut obtenir de différentes manières
+ - Swords
+ - Gun
+ - Box of Various Supplies
+ - Humble Indie Bindle
+ - Pickaxe
+- Coinsanity : Pièces de monnaie, soit individuellement, soit sous forme de lots personnalisés
+
+## Quels objets peuvent se trouver dans le monde d'un autre joueur ?
+
+Tous les DLC du jeu sont mélangés dans le stock d'objets. Les objets liés aux contrôles optionnels décrits ci-dessus sont également dans le stock
+
+Il y a aussi de nouveaux objets pièges, utilisés comme substituts, basés sur les désagréments du jeu vanille.
+- Zombie Sheep
+- Loading Screens
+- Temporary Spikes
+
+## Que se passe-t-il lorsque le joueur reçoit un objet ?
+
+Chaque fois qu'un objet est reçu en ligne, une notification apparaît à l'écran pour en informer le joueur.
+Certains objets sont accompagnés d'une animation ou d'une scène qui se déroule immédiatement après leur réception.
+
+Les objets reçus hors ligne ne sont pas accompagnés d'une animation ou d'une scène, et sont simplement activés lors de la connexion.
diff --git a/worlds/dlcquest/docs/setup_en.md b/worlds/dlcquest/docs/setup_en.md
index 47e22e0f74ee..7c82b9d69fc0 100644
--- a/worlds/dlcquest/docs/setup_en.md
+++ b/worlds/dlcquest/docs/setup_en.md
@@ -19,7 +19,7 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a YAML file?
-You can customize your settings by visiting the [DLC Quest Player Settings Page](/games/DLCQuest/player-settings)
+You can customize your options by visiting the [DLC Quest Player Options Page](/games/DLCQuest/player-options)
## Joining a MultiWorld Game
diff --git a/worlds/dlcquest/docs/setup_fr.md b/worlds/dlcquest/docs/setup_fr.md
new file mode 100644
index 000000000000..e4b431215d47
--- /dev/null
+++ b/worlds/dlcquest/docs/setup_fr.md
@@ -0,0 +1,55 @@
+# # Guide de configuration MultiWorld de DLCQuest
+
+## Logiciels requis
+
+- DLC Quest sur PC (Recommandé: [Version Steam](https://store.steampowered.com/app/230050/DLC_Quest/))
+- [DLCQuestipelago](https://github.com/agilbert1412/DLCQuestipelago/releases)
+- BepinEx (utilisé comme un modloader pour DLCQuest. La version du mod ci-dessus inclut BepInEx si vous choisissez la version d'installation complète)
+
+## Logiciels optionnels
+- [Archipelago] (https://github.com/ArchipelagoMW/Archipelago/releases)
+ - (Uniquement pour le TextClient)
+
+## Créer un fichier de configuration (.yaml)
+
+### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ?
+
+Voir le guide d'Archipelago sur la mise en place d'un YAML de base : [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
+
+### Où puis-je obtenir un fichier YAML ?
+
+Vous pouvez personnaliser vos paramètres en visitant la [page des paramètres du joueur DLC Quest](/games/DLCQuest/player-options).
+
+## Rejoindre une partie multi-monde
+
+### Installer le mod
+
+- Télécharger le [DLCQuestipelago mod release](https://github.com/agilbert1412/DLCQuestipelago/releases). Si c'est la première fois que vous installez le mod, ou si vous n'êtes pas à l'aise avec l'édition manuelle de fichiers, vous devriez choisir l'Installateur. Il se chargera de la plus grande partie du travail pour vous
+
+
+- Extraire l'archive .zip à l'emplacement de votre choix
+
+
+- Exécutez "DLCQuestipelagoInstaller.exe".
+
+![image](https://i.imgur.com/2sPhMgs.png)
+- Le programme d'installation devrait décrire ce qu'il fait à chaque étape, et vous demandera votre avis si nécessaire.
+ - Il vous permettra de choisir l'emplacement d'installation de votre jeu moddé et vous proposera un emplacement par défaut
+ - Il **essayera** de trouver votre jeu DLCQuest sur votre ordinateur et, en cas d'échec, vous demandera d'indiquer le chemin d'accès.
+ - Il vous offrira la possibilité de créer un raccourci sur le bureau pour le lanceur moddé.
+
+### Se connecter au MultiServer
+
+- Localisez le fichier "ArchipelagoConnectionInfo.json", qui se situe dans le même emplacement que votre installation moddée. Vous pouvez éditer ce fichier avec n'importe quel éditeur de texte, et vous devez entrer l'adresse IP du serveur, le port et votre nom de joueur dans les champs appropriés.
+
+- Exécutez BepInEx.NET.Framework.Launcher.exe. Si vous avez opté pour un raccourci sur le bureau, vous le trouverez avec une icône et un nom plus reconnaissable.
+![image](https://i.imgur.com/ZUiFrhf.png)
+
+- Votre jeu devrait se lancer en même temps qu'une console de modloader, qui contiendra des informations de débogage importantes si vous rencontrez des problèmes.
+- Le jeu devrait se connecter automatiquement, et tenter de se reconnecter si votre internet ou le serveur se déconnecte, pendant que vous jouez.
+
+### Interagir avec le MultiWorld depuis le jeu
+
+Vous ne pouvez pas envoyer de commandes au serveur ou discuter avec les autres joueurs depuis DLC Quest, car le jeu ne dispose pas d'un moyen approprié pour saisir du texte.
+Vous pouvez suivre l'activité du serveur dans votre console BepInEx, car les messages de chat d'Archipelago y seront affichés.
+Vous devrez utiliser [Archipelago Text Client] (https://github.com/ArchipelagoMW/Archipelago/releases) si vous voulez envoyer des commandes.
diff --git a/worlds/dlcquest/option_groups.py b/worlds/dlcquest/option_groups.py
new file mode 100644
index 000000000000..9510c061e18f
--- /dev/null
+++ b/worlds/dlcquest/option_groups.py
@@ -0,0 +1,27 @@
+from typing import List
+
+from Options import ProgressionBalancing, Accessibility, OptionGroup
+from .Options import (Campaign, ItemShuffle, TimeIsMoney, EndingChoice, PermanentCoins, DoubleJumpGlitch, CoinSanity,
+ CoinSanityRange, DeathLink)
+
+dlcq_option_groups: List[OptionGroup] = [
+ OptionGroup("General", [
+ Campaign,
+ ItemShuffle,
+ CoinSanity,
+ ]),
+ OptionGroup("Customization", [
+ EndingChoice,
+ PermanentCoins,
+ CoinSanityRange,
+ ]),
+ OptionGroup("Tedious and Grind", [
+ TimeIsMoney,
+ DoubleJumpGlitch,
+ ]),
+ OptionGroup("Advanced Options", [
+ DeathLink,
+ ProgressionBalancing,
+ Accessibility,
+ ]),
+]
diff --git a/worlds/dlcquest/presets.py b/worlds/dlcquest/presets.py
new file mode 100644
index 000000000000..ccfd79399521
--- /dev/null
+++ b/worlds/dlcquest/presets.py
@@ -0,0 +1,68 @@
+from typing import Any, Dict
+
+from .Options import DoubleJumpGlitch, CoinSanity, CoinSanityRange, PermanentCoins, TimeIsMoney, EndingChoice, Campaign, ItemShuffle
+
+all_random_settings = {
+ DoubleJumpGlitch.internal_name: "random",
+ CoinSanity.internal_name: "random",
+ CoinSanityRange.internal_name: "random",
+ PermanentCoins.internal_name: "random",
+ TimeIsMoney.internal_name: "random",
+ EndingChoice.internal_name: "random",
+ Campaign.internal_name: "random",
+ ItemShuffle.internal_name: "random",
+ "death_link": "random",
+}
+
+main_campaign_settings = {
+ DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none,
+ CoinSanity.internal_name: CoinSanity.option_coin,
+ CoinSanityRange.internal_name: 30,
+ PermanentCoins.internal_name: PermanentCoins.option_false,
+ TimeIsMoney.internal_name: TimeIsMoney.option_required,
+ EndingChoice.internal_name: EndingChoice.option_true,
+ Campaign.internal_name: Campaign.option_basic,
+ ItemShuffle.internal_name: ItemShuffle.option_shuffled,
+}
+
+lfod_campaign_settings = {
+ DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none,
+ CoinSanity.internal_name: CoinSanity.option_coin,
+ CoinSanityRange.internal_name: 30,
+ PermanentCoins.internal_name: PermanentCoins.option_false,
+ TimeIsMoney.internal_name: TimeIsMoney.option_required,
+ EndingChoice.internal_name: EndingChoice.option_true,
+ Campaign.internal_name: Campaign.option_live_freemium_or_die,
+ ItemShuffle.internal_name: ItemShuffle.option_shuffled,
+}
+
+easy_settings = {
+ DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none,
+ CoinSanity.internal_name: CoinSanity.option_none,
+ CoinSanityRange.internal_name: 40,
+ PermanentCoins.internal_name: PermanentCoins.option_true,
+ TimeIsMoney.internal_name: TimeIsMoney.option_required,
+ EndingChoice.internal_name: EndingChoice.option_true,
+ Campaign.internal_name: Campaign.option_both,
+ ItemShuffle.internal_name: ItemShuffle.option_shuffled,
+}
+
+hard_settings = {
+ DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_simple,
+ CoinSanity.internal_name: CoinSanity.option_coin,
+ CoinSanityRange.internal_name: 30,
+ PermanentCoins.internal_name: PermanentCoins.option_false,
+ TimeIsMoney.internal_name: TimeIsMoney.option_optional,
+ EndingChoice.internal_name: EndingChoice.option_true,
+ Campaign.internal_name: Campaign.option_both,
+ ItemShuffle.internal_name: ItemShuffle.option_shuffled,
+}
+
+
+dlcq_options_presets: Dict[str, Dict[str, Any]] = {
+ "All random": all_random_settings,
+ "Main campaign": main_campaign_settings,
+ "LFOD campaign": lfod_campaign_settings,
+ "Both easy": easy_settings,
+ "Both hard": hard_settings,
+}
diff --git a/worlds/dlcquest/test/TestItemShuffle.py b/worlds/dlcquest/test/TestItemShuffle.py
new file mode 100644
index 000000000000..7a9e5d95ba32
--- /dev/null
+++ b/worlds/dlcquest/test/TestItemShuffle.py
@@ -0,0 +1,138 @@
+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"
+locations = [sword, gun, wooden_sword, pickaxe, humble_bindle, box_supplies]
+prog_weapon_basic = "DLC Quest: Progressive Weapon"
+prog_weapon_lfod = "Live Freemium or Die: Progressive Weapon"
+items = [prog_weapon_basic, prog_weapon_lfod, 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_progressive_weapon_in_pool(self):
+ item_names = [item.name for item in self.multiworld.get_items()]
+ self.assertEqual(item_names.count(prog_weapon_basic), 2)
+ self.assertEqual(item_names.count(prog_weapon_lfod), 2)
+
+ def test_item_locations_in_pool(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for item_location in locations:
+ 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(prog_weapon_basic, 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(prog_weapon_lfod, 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(prog_weapon_lfod, 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(prog_weapon_lfod, 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 locations:
+ 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..3e9acac7e791
--- /dev/null
+++ b/worlds/dlcquest/test/TestOptionsLong.py
@@ -0,0 +1,87 @@
+from typing import Dict
+
+from BaseClasses import MultiWorld
+from Options import NamedRange
+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, NamedRange):
+ 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..8a39b43a2cfd
--- /dev/null
+++ b/worlds/dlcquest/test/__init__.py
@@ -0,0 +1,52 @@
+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, (), 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..cc2fa7f51ad2
--- /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.advancement]
+
+
+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.advancement]
+ 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/doom_1993/Items.py b/worlds/doom_1993/Items.py
index fe5576c4dfc4..3c5124d4d57b 100644
--- a/worlds/doom_1993/Items.py
+++ b/worlds/doom_1993/Items.py
@@ -1165,6 +1165,7 @@ class ItemDict(TypedDict, total=False):
item_name_groups: Dict[str, Set[str]] = {
'Ammos': {'Box of bullets', 'Box of rockets', 'Box of shotgun shells', 'Energy cell pack', },
+ 'Computer area maps': {'Against Thee Wickedly (E4M6) - Computer area map', 'And Hell Followed (E4M7) - Computer area map', 'Central Processing (E1M6) - Computer area map', 'Command Center (E2M5) - Computer area map', 'Command Control (E1M4) - Computer area map', 'Computer Station (E1M7) - Computer area map', 'Containment Area (E2M2) - Computer area map', 'Deimos Anomaly (E2M1) - Computer area map', 'Deimos Lab (E2M4) - Computer area map', 'Dis (E3M8) - Computer area map', 'Fear (E4M9) - Computer area map', 'Fortress of Mystery (E2M9) - Computer area map', 'Halls of the Damned (E2M6) - Computer area map', 'Hangar (E1M1) - Computer area map', 'Hell Beneath (E4M1) - Computer area map', 'Hell Keep (E3M1) - Computer area map', 'House of Pain (E3M4) - Computer area map', 'Limbo (E3M7) - Computer area map', 'Military Base (E1M9) - Computer area map', 'Mt. Erebus (E3M6) - Computer area map', 'Nuclear Plant (E1M2) - Computer area map', 'Pandemonium (E3M3) - Computer area map', 'Perfect Hatred (E4M2) - Computer area map', 'Phobos Anomaly (E1M8) - Computer area map', 'Phobos Lab (E1M5) - Computer area map', 'Refinery (E2M3) - Computer area map', 'Sever the Wicked (E4M3) - Computer area map', 'Slough of Despair (E3M2) - Computer area map', 'Spawning Vats (E2M7) - Computer area map', 'They Will Repent (E4M5) - Computer area map', 'Tower of Babel (E2M8) - Computer area map', 'Toxin Refinery (E1M3) - Computer area map', 'Unholy Cathedral (E3M5) - Computer area map', 'Unruly Evil (E4M4) - Computer area map', 'Unto the Cruel (E4M8) - Computer area map', 'Warrens (E3M9) - Computer area map', },
'Keys': {'Against Thee Wickedly (E4M6) - Blue skull key', 'Against Thee Wickedly (E4M6) - Red skull key', 'Against Thee Wickedly (E4M6) - Yellow skull key', 'And Hell Followed (E4M7) - Blue skull key', 'And Hell Followed (E4M7) - Red skull key', 'And Hell Followed (E4M7) - Yellow skull key', 'Central Processing (E1M6) - Blue keycard', 'Central Processing (E1M6) - Red keycard', 'Central Processing (E1M6) - Yellow keycard', 'Command Control (E1M4) - Blue keycard', 'Command Control (E1M4) - Yellow keycard', 'Computer Station (E1M7) - Blue keycard', 'Computer Station (E1M7) - Red keycard', 'Computer Station (E1M7) - Yellow keycard', 'Containment Area (E2M2) - Blue keycard', 'Containment Area (E2M2) - Red keycard', 'Containment Area (E2M2) - Yellow keycard', 'Deimos Anomaly (E2M1) - Blue keycard', 'Deimos Anomaly (E2M1) - Red keycard', 'Deimos Lab (E2M4) - Blue keycard', 'Deimos Lab (E2M4) - Yellow keycard', 'Fear (E4M9) - Yellow skull key', 'Fortress of Mystery (E2M9) - Blue skull key', 'Fortress of Mystery (E2M9) - Red skull key', 'Fortress of Mystery (E2M9) - Yellow skull key', 'Halls of the Damned (E2M6) - Blue skull key', 'Halls of the Damned (E2M6) - Red skull key', 'Halls of the Damned (E2M6) - Yellow skull key', 'Hell Beneath (E4M1) - Blue skull key', 'Hell Beneath (E4M1) - Red skull key', 'House of Pain (E3M4) - Blue skull key', 'House of Pain (E3M4) - Red skull key', 'House of Pain (E3M4) - Yellow skull key', 'Limbo (E3M7) - Blue skull key', 'Limbo (E3M7) - Red skull key', 'Limbo (E3M7) - Yellow skull key', 'Military Base (E1M9) - Blue keycard', 'Military Base (E1M9) - Red keycard', 'Military Base (E1M9) - Yellow keycard', 'Mt. Erebus (E3M6) - Blue skull key', 'Nuclear Plant (E1M2) - Red keycard', 'Pandemonium (E3M3) - Blue skull key', 'Perfect Hatred (E4M2) - Blue skull key', 'Perfect Hatred (E4M2) - Yellow skull key', 'Phobos Lab (E1M5) - Blue keycard', 'Phobos Lab (E1M5) - Yellow keycard', 'Refinery (E2M3) - Blue keycard', 'Sever the Wicked (E4M3) - Blue skull key', 'Sever the Wicked (E4M3) - Red skull key', 'Slough of Despair (E3M2) - Blue skull key', 'Spawning Vats (E2M7) - Blue keycard', 'Spawning Vats (E2M7) - Red keycard', 'Spawning Vats (E2M7) - Yellow keycard', 'They Will Repent (E4M5) - Blue skull key', 'They Will Repent (E4M5) - Red skull key', 'They Will Repent (E4M5) - Yellow skull key', 'Toxin Refinery (E1M3) - Blue keycard', 'Toxin Refinery (E1M3) - Yellow keycard', 'Unholy Cathedral (E3M5) - Blue skull key', 'Unholy Cathedral (E3M5) - Yellow skull key', 'Unruly Evil (E4M4) - Red skull key', 'Unto the Cruel (E4M8) - Red skull key', 'Unto the Cruel (E4M8) - Yellow skull key', 'Warrens (E3M9) - Blue skull key', 'Warrens (E3M9) - Red skull key', },
'Levels': {'Against Thee Wickedly (E4M6)', 'And Hell Followed (E4M7)', 'Central Processing (E1M6)', 'Command Center (E2M5)', 'Command Control (E1M4)', 'Computer Station (E1M7)', 'Containment Area (E2M2)', 'Deimos Anomaly (E2M1)', 'Deimos Lab (E2M4)', 'Dis (E3M8)', 'Fear (E4M9)', 'Fortress of Mystery (E2M9)', 'Halls of the Damned (E2M6)', 'Hangar (E1M1)', 'Hell Beneath (E4M1)', 'Hell Keep (E3M1)', 'House of Pain (E3M4)', 'Limbo (E3M7)', 'Military Base (E1M9)', 'Mt. Erebus (E3M6)', 'Nuclear Plant (E1M2)', 'Pandemonium (E3M3)', 'Perfect Hatred (E4M2)', 'Phobos Anomaly (E1M8)', 'Phobos Lab (E1M5)', 'Refinery (E2M3)', 'Sever the Wicked (E4M3)', 'Slough of Despair (E3M2)', 'Spawning Vats (E2M7)', 'They Will Repent (E4M5)', 'Tower of Babel (E2M8)', 'Toxin Refinery (E1M3)', 'Unholy Cathedral (E3M5)', 'Unruly Evil (E4M4)', 'Unto the Cruel (E4M8)', 'Warrens (E3M9)', },
'Powerups': {'Armor', 'Berserk', 'Invulnerability', 'Mega Armor', 'Partial invisibility', 'Supercharge', },
diff --git a/worlds/doom_1993/Locations.py b/worlds/doom_1993/Locations.py
index 942c7d2a42d4..2cbb9b9d150e 100644
--- a/worlds/doom_1993/Locations.py
+++ b/worlds/doom_1993/Locations.py
@@ -1794,13 +1794,13 @@ class LocationDict(TypedDict, total=False):
'map': 7,
'index': 65,
'doom_type': 2004,
- 'region': "Limbo (E3M7) Red"},
+ 'region': "Limbo (E3M7) Green"},
351297: {'name': 'Limbo (E3M7) - Armor',
'episode': 3,
'map': 7,
'index': 67,
'doom_type': 2018,
- 'region': "Limbo (E3M7) Red"},
+ 'region': "Limbo (E3M7) Green"},
351298: {'name': 'Limbo (E3M7) - Yellow skull key',
'episode': 3,
'map': 7,
@@ -1968,7 +1968,7 @@ class LocationDict(TypedDict, total=False):
'map': 2,
'index': -1,
'doom_type': -1,
- 'region': "Containment Area (E2M2) Red"},
+ 'region': "Containment Area (E2M2) Red Exit"},
351326: {'name': 'Deimos Anomaly (E2M1) - Exit',
'episode': 2,
'map': 1,
@@ -2496,19 +2496,19 @@ class LocationDict(TypedDict, total=False):
'map': 6,
'index': 77,
'doom_type': 38,
- 'region': "Against Thee Wickedly (E4M6) Yellow"},
+ 'region': "Against Thee Wickedly (E4M6) Magenta"},
351414: {'name': 'Against Thee Wickedly (E4M6) - Invulnerability',
'episode': 4,
'map': 6,
'index': 78,
'doom_type': 2022,
- 'region': "Against Thee Wickedly (E4M6) Red"},
+ 'region': "Against Thee Wickedly (E4M6) Pink"},
351415: {'name': 'Against Thee Wickedly (E4M6) - Invulnerability 2',
'episode': 4,
'map': 6,
'index': 89,
'doom_type': 2022,
- 'region': "Against Thee Wickedly (E4M6) Red"},
+ 'region': "Against Thee Wickedly (E4M6) Magenta"},
351416: {'name': 'Against Thee Wickedly (E4M6) - BFG9000',
'episode': 4,
'map': 6,
@@ -2520,7 +2520,7 @@ class LocationDict(TypedDict, total=False):
'map': 6,
'index': 102,
'doom_type': 8,
- 'region': "Against Thee Wickedly (E4M6) Red"},
+ 'region': "Against Thee Wickedly (E4M6) Pink"},
351418: {'name': 'Against Thee Wickedly (E4M6) - Berserk',
'episode': 4,
'map': 6,
@@ -2550,7 +2550,7 @@ class LocationDict(TypedDict, total=False):
'map': 6,
'index': -1,
'doom_type': -1,
- 'region': "Against Thee Wickedly (E4M6) Red"},
+ 'region': "Against Thee Wickedly (E4M6) Magenta"},
351423: {'name': 'And Hell Followed (E4M7) - Shotgun',
'episode': 4,
'map': 7,
@@ -2628,7 +2628,7 @@ class LocationDict(TypedDict, total=False):
'map': 7,
'index': 182,
'doom_type': 39,
- 'region': "And Hell Followed (E4M7) Main"},
+ 'region': "And Hell Followed (E4M7) Blue"},
351436: {'name': 'And Hell Followed (E4M7) - Red skull key',
'episode': 4,
'map': 7,
@@ -3414,6 +3414,7 @@ class LocationDict(TypedDict, total=False):
"Command Control (E1M4) - Supercharge",
"Command Control (E1M4) - Mega Armor",
"Containment Area (E2M2) - Supercharge",
+ "Containment Area (E2M2) - Plasma gun",
"Pandemonium (E3M3) - Mega Armor",
"House of Pain (E3M4) - Chaingun",
"House of Pain (E3M4) - Invulnerability",
diff --git a/worlds/doom_1993/Options.py b/worlds/doom_1993/Options.py
index 72bb7c3aea4e..f65952d3eb49 100644
--- a/worlds/doom_1993/Options.py
+++ b/worlds/doom_1993/Options.py
@@ -1,6 +1,17 @@
-import typing
+from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
+from dataclasses import dataclass
-from Options import AssembleOptions, Choice, Toggle, DeathLink, DefaultOnToggle
+
+class Goal(Choice):
+ """
+ Choose the main goal.
+ complete_all_levels: All levels of the selected episodes
+ complete_boss_levels: Boss levels (E#M8) of selected episodes
+ """
+ display_name = "Goal"
+ option_complete_all_levels = 0
+ option_complete_boss_levels = 1
+ default = 0
class Difficulty(Choice):
@@ -27,11 +38,13 @@ class RandomMonsters(Choice):
vanilla: No randomization
shuffle: Monsters are shuffled within the level
random_balanced: Monsters are completely randomized, but balanced based on existing ratio in the level. (Small monsters vs medium vs big)
+ random_chaotic: Monsters are completely randomized, but balanced based on existing ratio in the entire game.
"""
display_name = "Random Monsters"
option_vanilla = 0
option_shuffle = 1
option_random_balanced = 2
+ option_random_chaotic = 3
default = 1
@@ -49,6 +62,34 @@ class RandomPickups(Choice):
default = 1
+class RandomMusic(Choice):
+ """
+ Level musics will be randomized.
+ vanilla: No randomization
+ shuffle_selected: Selected episodes' levels will be shuffled
+ shuffle_game: All the music will be shuffled
+ """
+ display_name = "Random Music"
+ option_vanilla = 0
+ option_shuffle_selected = 1
+ option_shuffle_game = 2
+ default = 0
+
+
+class FlipLevels(Choice):
+ """
+ Flip levels on one axis.
+ vanilla: No flipping
+ flipped: All levels are flipped
+ randomly_flipped: Random levels are flipped
+ """
+ display_name = "Flip Levels"
+ option_vanilla = 0
+ option_flipped = 1
+ option_randomly_flipped = 2
+ default = 0
+
+
class AllowDeathLogic(Toggle):
"""Some locations require a timed puzzle that can only be tried once.
After which, if the player failed to get it, the location cannot be checked anymore.
@@ -56,12 +97,24 @@ class AllowDeathLogic(Toggle):
Get killed in the current map. The map will reset, you can now attempt the puzzle again."""
display_name = "Allow Death Logic"
+
+class Pro(Toggle):
+ """Include difficult tricks into rules. Mostly employed by speed runners.
+ i.e.: Leaps across to a locked area, trigger a switch behind a window at the right angle, etc."""
+ display_name = "Pro Doom"
+
class StartWithComputerAreaMaps(Toggle):
"""Give the player all Computer Area Map items from the start."""
display_name = "Start With Computer Area Maps"
+class ResetLevelOnDeath(DefaultOnToggle):
+ """When dying, levels are reset and monsters respawned. But inventory and checks are kept.
+ Turning this setting off is considered easy mode. Good for new players that don't know the levels well."""
+ display_name="Reset Level on Death"
+
+
class Episode1(DefaultOnToggle):
"""Knee-Deep in the Dead.
If none of the episodes are chosen, Episode 1 will be chosen by default."""
@@ -86,15 +139,22 @@ class Episode4(Toggle):
display_name = "Episode 4"
-options: typing.Dict[str, AssembleOptions] = {
- "difficulty": Difficulty,
- "random_monsters": RandomMonsters,
- "random_pickups": RandomPickups,
- "allow_death_logic": AllowDeathLogic,
- "start_with_computer_area_maps": StartWithComputerAreaMaps,
- "death_link": DeathLink,
- "episode1": Episode1,
- "episode2": Episode2,
- "episode3": Episode3,
- "episode4": Episode4
-}
+@dataclass
+class DOOM1993Options(PerGameCommonOptions):
+ start_inventory_from_pool: StartInventoryPool
+ goal: Goal
+ difficulty: Difficulty
+ random_monsters: RandomMonsters
+ random_pickups: RandomPickups
+ random_music: RandomMusic
+ flip_levels: FlipLevels
+ allow_death_logic: AllowDeathLogic
+ pro: Pro
+ start_with_computer_area_maps: StartWithComputerAreaMaps
+ death_link: DeathLink
+ reset_level_on_death: ResetLevelOnDeath
+ episode1: Episode1
+ episode2: Episode2
+ episode3: Episode3
+ episode4: Episode4
+
diff --git a/worlds/doom_1993/Regions.py b/worlds/doom_1993/Regions.py
index 58626e62ae25..f013bdceaf07 100644
--- a/worlds/doom_1993/Regions.py
+++ b/worlds/doom_1993/Regions.py
@@ -3,11 +3,15 @@
from typing import List
from BaseClasses import TypedDict
-class RegionDict(TypedDict, total=False):
+class ConnectionDict(TypedDict, total=False):
+ target: str
+ pro: bool
+
+class RegionDict(TypedDict, total=False):
name: str
connects_to_hub: bool
episode: int
- connections: List[str]
+ connections: List[ConnectionDict]
regions:List[RegionDict] = [
@@ -21,121 +25,131 @@ class RegionDict(TypedDict, total=False):
{"name":"Nuclear Plant (E1M2) Main",
"connects_to_hub":True,
"episode":1,
- "connections":["Nuclear Plant (E1M2) Red"]},
+ "connections":[{"target":"Nuclear Plant (E1M2) Red","pro":False}]},
{"name":"Nuclear Plant (E1M2) Red",
"connects_to_hub":False,
"episode":1,
- "connections":["Nuclear Plant (E1M2) Main"]},
+ "connections":[{"target":"Nuclear Plant (E1M2) Main","pro":False}]},
# Toxin Refinery (E1M3)
{"name":"Toxin Refinery (E1M3) Main",
"connects_to_hub":True,
"episode":1,
- "connections":["Toxin Refinery (E1M3) Blue"]},
+ "connections":[{"target":"Toxin Refinery (E1M3) Blue","pro":False}]},
{"name":"Toxin Refinery (E1M3) Blue",
"connects_to_hub":False,
"episode":1,
"connections":[
- "Toxin Refinery (E1M3) Yellow",
- "Toxin Refinery (E1M3) Main"]},
+ {"target":"Toxin Refinery (E1M3) Yellow","pro":False},
+ {"target":"Toxin Refinery (E1M3) Main","pro":False}]},
{"name":"Toxin Refinery (E1M3) Yellow",
"connects_to_hub":False,
"episode":1,
- "connections":["Toxin Refinery (E1M3) Blue"]},
+ "connections":[{"target":"Toxin Refinery (E1M3) Blue","pro":False}]},
# Command Control (E1M4)
{"name":"Command Control (E1M4) Main",
"connects_to_hub":True,
"episode":1,
"connections":[
- "Command Control (E1M4) Blue",
- "Command Control (E1M4) Yellow"]},
+ {"target":"Command Control (E1M4) Blue","pro":False},
+ {"target":"Command Control (E1M4) Yellow","pro":False},
+ {"target":"Command Control (E1M4) Ledge","pro":True}]},
{"name":"Command Control (E1M4) Blue",
"connects_to_hub":False,
"episode":1,
- "connections":["Command Control (E1M4) Main"]},
+ "connections":[
+ {"target":"Command Control (E1M4) Ledge","pro":False},
+ {"target":"Command Control (E1M4) Main","pro":False}]},
{"name":"Command Control (E1M4) Yellow",
"connects_to_hub":False,
"episode":1,
- "connections":["Command Control (E1M4) Main"]},
+ "connections":[{"target":"Command Control (E1M4) Main","pro":False}]},
+ {"name":"Command Control (E1M4) Ledge",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"Command Control (E1M4) Main","pro":False},
+ {"target":"Command Control (E1M4) Blue","pro":False},
+ {"target":"Command Control (E1M4) Yellow","pro":False}]},
# Phobos Lab (E1M5)
{"name":"Phobos Lab (E1M5) Main",
"connects_to_hub":True,
"episode":1,
- "connections":["Phobos Lab (E1M5) Yellow"]},
+ "connections":[{"target":"Phobos Lab (E1M5) Yellow","pro":False}]},
{"name":"Phobos Lab (E1M5) Yellow",
"connects_to_hub":False,
"episode":1,
"connections":[
- "Phobos Lab (E1M5) Main",
- "Phobos Lab (E1M5) Blue",
- "Phobos Lab (E1M5) Green"]},
+ {"target":"Phobos Lab (E1M5) Main","pro":False},
+ {"target":"Phobos Lab (E1M5) Blue","pro":False},
+ {"target":"Phobos Lab (E1M5) Green","pro":False}]},
{"name":"Phobos Lab (E1M5) Blue",
"connects_to_hub":False,
"episode":1,
"connections":[
- "Phobos Lab (E1M5) Green",
- "Phobos Lab (E1M5) Yellow"]},
+ {"target":"Phobos Lab (E1M5) Green","pro":False},
+ {"target":"Phobos Lab (E1M5) Yellow","pro":False}]},
{"name":"Phobos Lab (E1M5) Green",
"connects_to_hub":False,
"episode":1,
"connections":[
- "Phobos Lab (E1M5) Main",
- "Phobos Lab (E1M5) Blue"]},
+ {"target":"Phobos Lab (E1M5) Main","pro":False},
+ {"target":"Phobos Lab (E1M5) Blue","pro":False}]},
# Central Processing (E1M6)
{"name":"Central Processing (E1M6) Main",
"connects_to_hub":True,
"episode":1,
"connections":[
- "Central Processing (E1M6) Yellow",
- "Central Processing (E1M6) Red",
- "Central Processing (E1M6) Blue",
- "Central Processing (E1M6) Nukage"]},
+ {"target":"Central Processing (E1M6) Yellow","pro":False},
+ {"target":"Central Processing (E1M6) Red","pro":False},
+ {"target":"Central Processing (E1M6) Blue","pro":False},
+ {"target":"Central Processing (E1M6) Nukage","pro":False}]},
{"name":"Central Processing (E1M6) Red",
"connects_to_hub":False,
"episode":1,
- "connections":["Central Processing (E1M6) Main"]},
+ "connections":[{"target":"Central Processing (E1M6) Main","pro":False}]},
{"name":"Central Processing (E1M6) Blue",
"connects_to_hub":False,
"episode":1,
- "connections":["Central Processing (E1M6) Main"]},
+ "connections":[{"target":"Central Processing (E1M6) Main","pro":False}]},
{"name":"Central Processing (E1M6) Yellow",
"connects_to_hub":False,
"episode":1,
- "connections":["Central Processing (E1M6) Main"]},
+ "connections":[{"target":"Central Processing (E1M6) Main","pro":False}]},
{"name":"Central Processing (E1M6) Nukage",
"connects_to_hub":False,
"episode":1,
- "connections":["Central Processing (E1M6) Yellow"]},
+ "connections":[{"target":"Central Processing (E1M6) Yellow","pro":False}]},
# Computer Station (E1M7)
{"name":"Computer Station (E1M7) Main",
"connects_to_hub":True,
"episode":1,
"connections":[
- "Computer Station (E1M7) Red",
- "Computer Station (E1M7) Yellow"]},
+ {"target":"Computer Station (E1M7) Red","pro":False},
+ {"target":"Computer Station (E1M7) Yellow","pro":False}]},
{"name":"Computer Station (E1M7) Blue",
"connects_to_hub":False,
"episode":1,
- "connections":["Computer Station (E1M7) Yellow"]},
+ "connections":[{"target":"Computer Station (E1M7) Yellow","pro":False}]},
{"name":"Computer Station (E1M7) Red",
"connects_to_hub":False,
"episode":1,
- "connections":["Computer Station (E1M7) Main"]},
+ "connections":[{"target":"Computer Station (E1M7) Main","pro":False}]},
{"name":"Computer Station (E1M7) Yellow",
"connects_to_hub":False,
"episode":1,
"connections":[
- "Computer Station (E1M7) Blue",
- "Computer Station (E1M7) Courtyard",
- "Computer Station (E1M7) Main"]},
+ {"target":"Computer Station (E1M7) Blue","pro":False},
+ {"target":"Computer Station (E1M7) Courtyard","pro":False},
+ {"target":"Computer Station (E1M7) Main","pro":False}]},
{"name":"Computer Station (E1M7) Courtyard",
"connects_to_hub":False,
"episode":1,
- "connections":["Computer Station (E1M7) Yellow"]},
+ "connections":[{"target":"Computer Station (E1M7) Yellow","pro":False}]},
# Phobos Anomaly (E1M8)
{"name":"Phobos Anomaly (E1M8) Main",
@@ -145,91 +159,98 @@ class RegionDict(TypedDict, total=False):
{"name":"Phobos Anomaly (E1M8) Start",
"connects_to_hub":True,
"episode":1,
- "connections":["Phobos Anomaly (E1M8) Main"]},
+ "connections":[{"target":"Phobos Anomaly (E1M8) Main","pro":False}]},
# Military Base (E1M9)
{"name":"Military Base (E1M9) Main",
"connects_to_hub":True,
"episode":1,
"connections":[
- "Military Base (E1M9) Blue",
- "Military Base (E1M9) Yellow",
- "Military Base (E1M9) Red"]},
+ {"target":"Military Base (E1M9) Blue","pro":False},
+ {"target":"Military Base (E1M9) Yellow","pro":False},
+ {"target":"Military Base (E1M9) Red","pro":False}]},
{"name":"Military Base (E1M9) Blue",
"connects_to_hub":False,
"episode":1,
- "connections":["Military Base (E1M9) Main"]},
+ "connections":[{"target":"Military Base (E1M9) Main","pro":False}]},
{"name":"Military Base (E1M9) Red",
"connects_to_hub":False,
"episode":1,
- "connections":["Military Base (E1M9) Main"]},
+ "connections":[{"target":"Military Base (E1M9) Main","pro":False}]},
{"name":"Military Base (E1M9) Yellow",
"connects_to_hub":False,
"episode":1,
- "connections":["Military Base (E1M9) Main"]},
+ "connections":[{"target":"Military Base (E1M9) Main","pro":False}]},
# Deimos Anomaly (E2M1)
{"name":"Deimos Anomaly (E2M1) Main",
"connects_to_hub":True,
"episode":2,
"connections":[
- "Deimos Anomaly (E2M1) Red",
- "Deimos Anomaly (E2M1) Blue"]},
+ {"target":"Deimos Anomaly (E2M1) Red","pro":False},
+ {"target":"Deimos Anomaly (E2M1) Blue","pro":False}]},
{"name":"Deimos Anomaly (E2M1) Blue",
"connects_to_hub":False,
"episode":2,
- "connections":["Deimos Anomaly (E2M1) Main"]},
+ "connections":[{"target":"Deimos Anomaly (E2M1) Main","pro":False}]},
{"name":"Deimos Anomaly (E2M1) Red",
"connects_to_hub":False,
"episode":2,
- "connections":["Deimos Anomaly (E2M1) Main"]},
+ "connections":[{"target":"Deimos Anomaly (E2M1) Main","pro":False}]},
# Containment Area (E2M2)
{"name":"Containment Area (E2M2) Main",
"connects_to_hub":True,
"episode":2,
"connections":[
- "Containment Area (E2M2) Yellow",
- "Containment Area (E2M2) Blue",
- "Containment Area (E2M2) Red"]},
+ {"target":"Containment Area (E2M2) Yellow","pro":False},
+ {"target":"Containment Area (E2M2) Blue","pro":False},
+ {"target":"Containment Area (E2M2) Red","pro":False},
+ {"target":"Containment Area (E2M2) Red Exit","pro":True}]},
{"name":"Containment Area (E2M2) Blue",
"connects_to_hub":False,
"episode":2,
- "connections":["Containment Area (E2M2) Main"]},
+ "connections":[{"target":"Containment Area (E2M2) Main","pro":False}]},
{"name":"Containment Area (E2M2) Red",
"connects_to_hub":False,
"episode":2,
- "connections":["Containment Area (E2M2) Main"]},
+ "connections":[
+ {"target":"Containment Area (E2M2) Main","pro":False},
+ {"target":"Containment Area (E2M2) Red Exit","pro":False}]},
{"name":"Containment Area (E2M2) Yellow",
"connects_to_hub":False,
"episode":2,
- "connections":["Containment Area (E2M2) Main"]},
+ "connections":[{"target":"Containment Area (E2M2) Main","pro":False}]},
+ {"name":"Containment Area (E2M2) Red Exit",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[]},
# Refinery (E2M3)
{"name":"Refinery (E2M3) Main",
"connects_to_hub":True,
"episode":2,
- "connections":["Refinery (E2M3) Blue"]},
+ "connections":[{"target":"Refinery (E2M3) Blue","pro":False}]},
{"name":"Refinery (E2M3) Blue",
"connects_to_hub":False,
"episode":2,
- "connections":["Refinery (E2M3) Main"]},
+ "connections":[{"target":"Refinery (E2M3) Main","pro":False}]},
# Deimos Lab (E2M4)
{"name":"Deimos Lab (E2M4) Main",
"connects_to_hub":True,
"episode":2,
- "connections":["Deimos Lab (E2M4) Blue"]},
+ "connections":[{"target":"Deimos Lab (E2M4) Blue","pro":False}]},
{"name":"Deimos Lab (E2M4) Blue",
"connects_to_hub":False,
"episode":2,
"connections":[
- "Deimos Lab (E2M4) Main",
- "Deimos Lab (E2M4) Yellow"]},
+ {"target":"Deimos Lab (E2M4) Main","pro":False},
+ {"target":"Deimos Lab (E2M4) Yellow","pro":False}]},
{"name":"Deimos Lab (E2M4) Yellow",
"connects_to_hub":False,
"episode":2,
- "connections":["Deimos Lab (E2M4) Blue"]},
+ "connections":[{"target":"Deimos Lab (E2M4) Blue","pro":False}]},
# Command Center (E2M5)
{"name":"Command Center (E2M5) Main",
@@ -242,47 +263,54 @@ class RegionDict(TypedDict, total=False):
"connects_to_hub":True,
"episode":2,
"connections":[
- "Halls of the Damned (E2M6) Blue Yellow Red",
- "Halls of the Damned (E2M6) Yellow",
- "Halls of the Damned (E2M6) One way Yellow"]},
+ {"target":"Halls of the Damned (E2M6) Blue Yellow Red","pro":False},
+ {"target":"Halls of the Damned (E2M6) Yellow","pro":False},
+ {"target":"Halls of the Damned (E2M6) One way Yellow","pro":False}]},
{"name":"Halls of the Damned (E2M6) Yellow",
"connects_to_hub":False,
"episode":2,
- "connections":["Halls of the Damned (E2M6) Main"]},
+ "connections":[{"target":"Halls of the Damned (E2M6) Main","pro":False}]},
{"name":"Halls of the Damned (E2M6) Blue Yellow Red",
"connects_to_hub":False,
"episode":2,
- "connections":["Halls of the Damned (E2M6) Main"]},
+ "connections":[{"target":"Halls of the Damned (E2M6) Main","pro":False}]},
{"name":"Halls of the Damned (E2M6) One way Yellow",
"connects_to_hub":False,
"episode":2,
- "connections":["Halls of the Damned (E2M6) Main"]},
+ "connections":[{"target":"Halls of the Damned (E2M6) Main","pro":False}]},
# Spawning Vats (E2M7)
{"name":"Spawning Vats (E2M7) Main",
"connects_to_hub":True,
"episode":2,
"connections":[
- "Spawning Vats (E2M7) Blue",
- "Spawning Vats (E2M7) Entrance Secret",
- "Spawning Vats (E2M7) Red",
- "Spawning Vats (E2M7) Yellow"]},
+ {"target":"Spawning Vats (E2M7) Blue","pro":False},
+ {"target":"Spawning Vats (E2M7) Entrance Secret","pro":False},
+ {"target":"Spawning Vats (E2M7) Red","pro":False},
+ {"target":"Spawning Vats (E2M7) Yellow","pro":False},
+ {"target":"Spawning Vats (E2M7) Red Exit","pro":True}]},
{"name":"Spawning Vats (E2M7) Blue",
"connects_to_hub":False,
"episode":2,
- "connections":["Spawning Vats (E2M7) Main"]},
+ "connections":[{"target":"Spawning Vats (E2M7) Main","pro":False}]},
{"name":"Spawning Vats (E2M7) Yellow",
"connects_to_hub":False,
"episode":2,
- "connections":["Spawning Vats (E2M7) Main"]},
+ "connections":[{"target":"Spawning Vats (E2M7) Main","pro":False}]},
{"name":"Spawning Vats (E2M7) Red",
"connects_to_hub":False,
"episode":2,
- "connections":["Spawning Vats (E2M7) Main"]},
+ "connections":[
+ {"target":"Spawning Vats (E2M7) Main","pro":False},
+ {"target":"Spawning Vats (E2M7) Red Exit","pro":False}]},
{"name":"Spawning Vats (E2M7) Entrance Secret",
"connects_to_hub":False,
"episode":2,
- "connections":["Spawning Vats (E2M7) Main"]},
+ "connections":[{"target":"Spawning Vats (E2M7) Main","pro":False}]},
+ {"name":"Spawning Vats (E2M7) Red Exit",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[]},
# Tower of Babel (E2M8)
{"name":"Tower of Babel (E2M8) Main",
@@ -295,120 +323,134 @@ class RegionDict(TypedDict, total=False):
"connects_to_hub":True,
"episode":2,
"connections":[
- "Fortress of Mystery (E2M9) Blue",
- "Fortress of Mystery (E2M9) Red",
- "Fortress of Mystery (E2M9) Yellow"]},
+ {"target":"Fortress of Mystery (E2M9) Blue","pro":False},
+ {"target":"Fortress of Mystery (E2M9) Red","pro":False},
+ {"target":"Fortress of Mystery (E2M9) Yellow","pro":False}]},
{"name":"Fortress of Mystery (E2M9) Blue",
"connects_to_hub":False,
"episode":2,
- "connections":["Fortress of Mystery (E2M9) Main"]},
+ "connections":[{"target":"Fortress of Mystery (E2M9) Main","pro":False}]},
{"name":"Fortress of Mystery (E2M9) Red",
"connects_to_hub":False,
"episode":2,
- "connections":["Fortress of Mystery (E2M9) Main"]},
+ "connections":[{"target":"Fortress of Mystery (E2M9) Main","pro":False}]},
{"name":"Fortress of Mystery (E2M9) Yellow",
"connects_to_hub":False,
"episode":2,
- "connections":["Fortress of Mystery (E2M9) Main"]},
+ "connections":[{"target":"Fortress of Mystery (E2M9) Main","pro":False}]},
# Hell Keep (E3M1)
{"name":"Hell Keep (E3M1) Main",
"connects_to_hub":True,
"episode":3,
- "connections":["Hell Keep (E3M1) Narrow"]},
+ "connections":[{"target":"Hell Keep (E3M1) Narrow","pro":False}]},
{"name":"Hell Keep (E3M1) Narrow",
"connects_to_hub":False,
"episode":3,
- "connections":["Hell Keep (E3M1) Main"]},
+ "connections":[{"target":"Hell Keep (E3M1) Main","pro":False}]},
# Slough of Despair (E3M2)
{"name":"Slough of Despair (E3M2) Main",
"connects_to_hub":True,
"episode":3,
- "connections":["Slough of Despair (E3M2) Blue"]},
+ "connections":[{"target":"Slough of Despair (E3M2) Blue","pro":False}]},
{"name":"Slough of Despair (E3M2) Blue",
"connects_to_hub":False,
"episode":3,
- "connections":["Slough of Despair (E3M2) Main"]},
+ "connections":[{"target":"Slough of Despair (E3M2) Main","pro":False}]},
# Pandemonium (E3M3)
{"name":"Pandemonium (E3M3) Main",
"connects_to_hub":True,
"episode":3,
- "connections":["Pandemonium (E3M3) Blue"]},
+ "connections":[{"target":"Pandemonium (E3M3) Blue","pro":False}]},
{"name":"Pandemonium (E3M3) Blue",
"connects_to_hub":False,
"episode":3,
- "connections":["Pandemonium (E3M3) Main"]},
+ "connections":[{"target":"Pandemonium (E3M3) Main","pro":False}]},
# House of Pain (E3M4)
{"name":"House of Pain (E3M4) Main",
"connects_to_hub":True,
"episode":3,
- "connections":["House of Pain (E3M4) Blue"]},
+ "connections":[{"target":"House of Pain (E3M4) Blue","pro":False}]},
{"name":"House of Pain (E3M4) Blue",
"connects_to_hub":False,
"episode":3,
"connections":[
- "House of Pain (E3M4) Main",
- "House of Pain (E3M4) Yellow",
- "House of Pain (E3M4) Red"]},
+ {"target":"House of Pain (E3M4) Main","pro":False},
+ {"target":"House of Pain (E3M4) Yellow","pro":False},
+ {"target":"House of Pain (E3M4) Red","pro":False}]},
{"name":"House of Pain (E3M4) Red",
"connects_to_hub":False,
"episode":3,
- "connections":["House of Pain (E3M4) Blue"]},
+ "connections":[{"target":"House of Pain (E3M4) Blue","pro":False}]},
{"name":"House of Pain (E3M4) Yellow",
"connects_to_hub":False,
"episode":3,
- "connections":["House of Pain (E3M4) Blue"]},
+ "connections":[{"target":"House of Pain (E3M4) Blue","pro":False}]},
# Unholy Cathedral (E3M5)
{"name":"Unholy Cathedral (E3M5) Main",
"connects_to_hub":True,
"episode":3,
"connections":[
- "Unholy Cathedral (E3M5) Yellow",
- "Unholy Cathedral (E3M5) Blue"]},
+ {"target":"Unholy Cathedral (E3M5) Yellow","pro":False},
+ {"target":"Unholy Cathedral (E3M5) Blue","pro":False}]},
{"name":"Unholy Cathedral (E3M5) Blue",
"connects_to_hub":False,
"episode":3,
- "connections":["Unholy Cathedral (E3M5) Main"]},
+ "connections":[{"target":"Unholy Cathedral (E3M5) Main","pro":False}]},
{"name":"Unholy Cathedral (E3M5) Yellow",
"connects_to_hub":False,
"episode":3,
- "connections":["Unholy Cathedral (E3M5) Main"]},
+ "connections":[{"target":"Unholy Cathedral (E3M5) Main","pro":False}]},
# Mt. Erebus (E3M6)
{"name":"Mt. Erebus (E3M6) Main",
"connects_to_hub":True,
"episode":3,
- "connections":["Mt. Erebus (E3M6) Blue"]},
+ "connections":[{"target":"Mt. Erebus (E3M6) Blue","pro":False}]},
{"name":"Mt. Erebus (E3M6) Blue",
"connects_to_hub":False,
"episode":3,
- "connections":["Mt. Erebus (E3M6) Main"]},
+ "connections":[{"target":"Mt. Erebus (E3M6) Main","pro":False}]},
# Limbo (E3M7)
{"name":"Limbo (E3M7) Main",
"connects_to_hub":True,
"episode":3,
"connections":[
- "Limbo (E3M7) Red",
- "Limbo (E3M7) Blue"]},
+ {"target":"Limbo (E3M7) Red","pro":False},
+ {"target":"Limbo (E3M7) Blue","pro":False},
+ {"target":"Limbo (E3M7) Pink","pro":False}]},
{"name":"Limbo (E3M7) Blue",
"connects_to_hub":False,
"episode":3,
- "connections":["Limbo (E3M7) Main"]},
+ "connections":[{"target":"Limbo (E3M7) Main","pro":False}]},
{"name":"Limbo (E3M7) Red",
"connects_to_hub":False,
"episode":3,
"connections":[
- "Limbo (E3M7) Main",
- "Limbo (E3M7) Yellow"]},
+ {"target":"Limbo (E3M7) Main","pro":False},
+ {"target":"Limbo (E3M7) Yellow","pro":False},
+ {"target":"Limbo (E3M7) Green","pro":False}]},
{"name":"Limbo (E3M7) Yellow",
"connects_to_hub":False,
"episode":3,
- "connections":["Limbo (E3M7) Red"]},
+ "connections":[{"target":"Limbo (E3M7) Red","pro":False}]},
+ {"name":"Limbo (E3M7) Pink",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ {"target":"Limbo (E3M7) Green","pro":False},
+ {"target":"Limbo (E3M7) Main","pro":False}]},
+ {"name":"Limbo (E3M7) Green",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ {"target":"Limbo (E3M7) Pink","pro":False},
+ {"target":"Limbo (E3M7) Red","pro":False}]},
# Dis (E3M8)
{"name":"Dis (E3M8) Main",
@@ -421,8 +463,8 @@ class RegionDict(TypedDict, total=False):
"connects_to_hub":True,
"episode":3,
"connections":[
- "Warrens (E3M9) Blue",
- "Warrens (E3M9) Blue trigger"]},
+ {"target":"Warrens (E3M9) Blue","pro":False},
+ {"target":"Warrens (E3M9) Blue trigger","pro":False}]},
{"name":"Warrens (E3M9) Red",
"connects_to_hub":False,
"episode":3,
@@ -431,8 +473,8 @@ class RegionDict(TypedDict, total=False):
"connects_to_hub":False,
"episode":3,
"connections":[
- "Warrens (E3M9) Main",
- "Warrens (E3M9) Red"]},
+ {"target":"Warrens (E3M9) Main","pro":False},
+ {"target":"Warrens (E3M9) Red","pro":False}]},
{"name":"Warrens (E3M9) Blue trigger",
"connects_to_hub":False,
"episode":3,
@@ -443,36 +485,36 @@ class RegionDict(TypedDict, total=False):
"connects_to_hub":True,
"episode":4,
"connections":[
- "Hell Beneath (E4M1) Red",
- "Hell Beneath (E4M1) Blue"]},
+ {"target":"Hell Beneath (E4M1) Red","pro":False},
+ {"target":"Hell Beneath (E4M1) Blue","pro":False}]},
{"name":"Hell Beneath (E4M1) Red",
"connects_to_hub":False,
"episode":4,
- "connections":["Hell Beneath (E4M1) Main"]},
+ "connections":[{"target":"Hell Beneath (E4M1) Main","pro":False}]},
{"name":"Hell Beneath (E4M1) Blue",
"connects_to_hub":False,
"episode":4,
- "connections":["Hell Beneath (E4M1) Main"]},
+ "connections":[{"target":"Hell Beneath (E4M1) Main","pro":False}]},
# Perfect Hatred (E4M2)
{"name":"Perfect Hatred (E4M2) Main",
"connects_to_hub":True,
"episode":4,
"connections":[
- "Perfect Hatred (E4M2) Blue",
- "Perfect Hatred (E4M2) Yellow"]},
+ {"target":"Perfect Hatred (E4M2) Blue","pro":False},
+ {"target":"Perfect Hatred (E4M2) Yellow","pro":False}]},
{"name":"Perfect Hatred (E4M2) Blue",
"connects_to_hub":False,
"episode":4,
"connections":[
- "Perfect Hatred (E4M2) Main",
- "Perfect Hatred (E4M2) Cave"]},
+ {"target":"Perfect Hatred (E4M2) Main","pro":False},
+ {"target":"Perfect Hatred (E4M2) Cave","pro":False}]},
{"name":"Perfect Hatred (E4M2) Yellow",
"connects_to_hub":False,
"episode":4,
"connections":[
- "Perfect Hatred (E4M2) Main",
- "Perfect Hatred (E4M2) Cave"]},
+ {"target":"Perfect Hatred (E4M2) Main","pro":False},
+ {"target":"Perfect Hatred (E4M2) Cave","pro":False}]},
{"name":"Perfect Hatred (E4M2) Cave",
"connects_to_hub":False,
"episode":4,
@@ -482,119 +524,135 @@ class RegionDict(TypedDict, total=False):
{"name":"Sever the Wicked (E4M3) Main",
"connects_to_hub":True,
"episode":4,
- "connections":["Sever the Wicked (E4M3) Red"]},
+ "connections":[{"target":"Sever the Wicked (E4M3) Red","pro":False}]},
{"name":"Sever the Wicked (E4M3) Red",
"connects_to_hub":False,
"episode":4,
"connections":[
- "Sever the Wicked (E4M3) Blue",
- "Sever the Wicked (E4M3) Main"]},
+ {"target":"Sever the Wicked (E4M3) Blue","pro":False},
+ {"target":"Sever the Wicked (E4M3) Main","pro":False}]},
{"name":"Sever the Wicked (E4M3) Blue",
"connects_to_hub":False,
"episode":4,
- "connections":["Sever the Wicked (E4M3) Red"]},
+ "connections":[{"target":"Sever the Wicked (E4M3) Red","pro":False}]},
# Unruly Evil (E4M4)
{"name":"Unruly Evil (E4M4) Main",
"connects_to_hub":True,
"episode":4,
- "connections":["Unruly Evil (E4M4) Red"]},
+ "connections":[{"target":"Unruly Evil (E4M4) Red","pro":False}]},
{"name":"Unruly Evil (E4M4) Red",
"connects_to_hub":False,
"episode":4,
- "connections":["Unruly Evil (E4M4) Main"]},
+ "connections":[{"target":"Unruly Evil (E4M4) Main","pro":False}]},
# They Will Repent (E4M5)
{"name":"They Will Repent (E4M5) Main",
"connects_to_hub":True,
"episode":4,
- "connections":["They Will Repent (E4M5) Red"]},
+ "connections":[{"target":"They Will Repent (E4M5) Red","pro":False}]},
{"name":"They Will Repent (E4M5) Yellow",
"connects_to_hub":False,
"episode":4,
- "connections":["They Will Repent (E4M5) Red"]},
+ "connections":[{"target":"They Will Repent (E4M5) Red","pro":False}]},
{"name":"They Will Repent (E4M5) Blue",
"connects_to_hub":False,
"episode":4,
- "connections":["They Will Repent (E4M5) Red"]},
+ "connections":[{"target":"They Will Repent (E4M5) Red","pro":False}]},
{"name":"They Will Repent (E4M5) Red",
"connects_to_hub":False,
"episode":4,
"connections":[
- "They Will Repent (E4M5) Main",
- "They Will Repent (E4M5) Yellow",
- "They Will Repent (E4M5) Blue"]},
+ {"target":"They Will Repent (E4M5) Main","pro":False},
+ {"target":"They Will Repent (E4M5) Yellow","pro":False},
+ {"target":"They Will Repent (E4M5) Blue","pro":False}]},
# Against Thee Wickedly (E4M6)
{"name":"Against Thee Wickedly (E4M6) Main",
"connects_to_hub":True,
"episode":4,
"connections":[
- "Against Thee Wickedly (E4M6) Blue",
- "Against Thee Wickedly (E4M6) Yellow",
- "Against Thee Wickedly (E4M6) Red"]},
+ {"target":"Against Thee Wickedly (E4M6) Blue","pro":False},
+ {"target":"Against Thee Wickedly (E4M6) Pink","pro":True}]},
{"name":"Against Thee Wickedly (E4M6) Red",
"connects_to_hub":False,
"episode":4,
- "connections":["Against Thee Wickedly (E4M6) Main"]},
+ "connections":[
+ {"target":"Against Thee Wickedly (E4M6) Blue","pro":False},
+ {"target":"Against Thee Wickedly (E4M6) Pink","pro":False},
+ {"target":"Against Thee Wickedly (E4M6) Main","pro":False},
+ {"target":"Against Thee Wickedly (E4M6) Magenta","pro":True}]},
{"name":"Against Thee Wickedly (E4M6) Blue",
"connects_to_hub":False,
"episode":4,
- "connections":["Against Thee Wickedly (E4M6) Main"]},
+ "connections":[
+ {"target":"Against Thee Wickedly (E4M6) Main","pro":False},
+ {"target":"Against Thee Wickedly (E4M6) Yellow","pro":False},
+ {"target":"Against Thee Wickedly (E4M6) Red","pro":False}]},
+ {"name":"Against Thee Wickedly (E4M6) Magenta",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[{"target":"Against Thee Wickedly (E4M6) Main","pro":False}]},
{"name":"Against Thee Wickedly (E4M6) Yellow",
"connects_to_hub":False,
"episode":4,
- "connections":["Against Thee Wickedly (E4M6) Main"]},
+ "connections":[
+ {"target":"Against Thee Wickedly (E4M6) Blue","pro":False},
+ {"target":"Against Thee Wickedly (E4M6) Magenta","pro":False}]},
+ {"name":"Against Thee Wickedly (E4M6) Pink",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[{"target":"Against Thee Wickedly (E4M6) Main","pro":False}]},
# And Hell Followed (E4M7)
{"name":"And Hell Followed (E4M7) Main",
"connects_to_hub":True,
"episode":4,
"connections":[
- "And Hell Followed (E4M7) Blue",
- "And Hell Followed (E4M7) Red",
- "And Hell Followed (E4M7) Yellow"]},
+ {"target":"And Hell Followed (E4M7) Blue","pro":False},
+ {"target":"And Hell Followed (E4M7) Red","pro":False},
+ {"target":"And Hell Followed (E4M7) Yellow","pro":False}]},
{"name":"And Hell Followed (E4M7) Red",
"connects_to_hub":False,
"episode":4,
- "connections":["And Hell Followed (E4M7) Main"]},
+ "connections":[{"target":"And Hell Followed (E4M7) Main","pro":False}]},
{"name":"And Hell Followed (E4M7) Blue",
"connects_to_hub":False,
"episode":4,
- "connections":["And Hell Followed (E4M7) Main"]},
+ "connections":[{"target":"And Hell Followed (E4M7) Main","pro":False}]},
{"name":"And Hell Followed (E4M7) Yellow",
"connects_to_hub":False,
"episode":4,
- "connections":["And Hell Followed (E4M7) Main"]},
+ "connections":[{"target":"And Hell Followed (E4M7) Main","pro":False}]},
# Unto the Cruel (E4M8)
{"name":"Unto the Cruel (E4M8) Main",
"connects_to_hub":True,
"episode":4,
"connections":[
- "Unto the Cruel (E4M8) Red",
- "Unto the Cruel (E4M8) Yellow",
- "Unto the Cruel (E4M8) Orange"]},
+ {"target":"Unto the Cruel (E4M8) Red","pro":False},
+ {"target":"Unto the Cruel (E4M8) Yellow","pro":False},
+ {"target":"Unto the Cruel (E4M8) Orange","pro":False}]},
{"name":"Unto the Cruel (E4M8) Yellow",
"connects_to_hub":False,
"episode":4,
- "connections":["Unto the Cruel (E4M8) Main"]},
+ "connections":[{"target":"Unto the Cruel (E4M8) Main","pro":False}]},
{"name":"Unto the Cruel (E4M8) Red",
"connects_to_hub":False,
"episode":4,
- "connections":["Unto the Cruel (E4M8) Main"]},
+ "connections":[{"target":"Unto the Cruel (E4M8) Main","pro":False}]},
{"name":"Unto the Cruel (E4M8) Orange",
"connects_to_hub":False,
"episode":4,
- "connections":["Unto the Cruel (E4M8) Main"]},
+ "connections":[{"target":"Unto the Cruel (E4M8) Main","pro":False}]},
# Fear (E4M9)
{"name":"Fear (E4M9) Main",
"connects_to_hub":True,
"episode":4,
- "connections":["Fear (E4M9) Yellow"]},
+ "connections":[{"target":"Fear (E4M9) Yellow","pro":False}]},
{"name":"Fear (E4M9) Yellow",
"connects_to_hub":False,
"episode":4,
- "connections":["Fear (E4M9) Main"]},
+ "connections":[{"target":"Fear (E4M9) Main","pro":False}]},
]
diff --git a/worlds/doom_1993/Rules.py b/worlds/doom_1993/Rules.py
index 6f24112cbefa..4faeb4a27dbd 100644
--- a/worlds/doom_1993/Rules.py
+++ b/worlds/doom_1993/Rules.py
@@ -7,108 +7,105 @@
from . import DOOM1993World
-def set_episode1_rules(player, world):
+def set_episode1_rules(player, multiworld, pro):
# Hangar (E1M1)
- set_rule(world.get_entrance("Hub -> Hangar (E1M1) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Hangar (E1M1) Main", player), lambda state:
state.has("Hangar (E1M1)", player, 1))
# Nuclear Plant (E1M2)
- set_rule(world.get_entrance("Hub -> Nuclear Plant (E1M2) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Nuclear Plant (E1M2) Main", player), lambda state:
(state.has("Nuclear Plant (E1M2)", player, 1)) and
(state.has("Shotgun", player, 1) or
state.has("Chaingun", player, 1)))
- set_rule(world.get_entrance("Nuclear Plant (E1M2) Main -> Nuclear Plant (E1M2) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Nuclear Plant (E1M2) Main -> Nuclear Plant (E1M2) Red", player), lambda state:
state.has("Nuclear Plant (E1M2) - Red keycard", player, 1))
- set_rule(world.get_entrance("Nuclear Plant (E1M2) Red -> Nuclear Plant (E1M2) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Nuclear Plant (E1M2) Red -> Nuclear Plant (E1M2) Main", player), lambda state:
state.has("Nuclear Plant (E1M2) - Red keycard", player, 1))
# Toxin Refinery (E1M3)
- set_rule(world.get_entrance("Hub -> Toxin Refinery (E1M3) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Toxin Refinery (E1M3) Main", player), lambda state:
(state.has("Toxin Refinery (E1M3)", player, 1)) and
(state.has("Shotgun", player, 1) or
state.has("Chaingun", player, 1)))
- set_rule(world.get_entrance("Toxin Refinery (E1M3) Main -> Toxin Refinery (E1M3) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Main -> Toxin Refinery (E1M3) Blue", player), lambda state:
state.has("Toxin Refinery (E1M3) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Toxin Refinery (E1M3) Blue -> Toxin Refinery (E1M3) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Blue -> Toxin Refinery (E1M3) Yellow", player), lambda state:
state.has("Toxin Refinery (E1M3) - Yellow keycard", player, 1))
- set_rule(world.get_entrance("Toxin Refinery (E1M3) Blue -> Toxin Refinery (E1M3) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Blue -> Toxin Refinery (E1M3) Main", player), lambda state:
state.has("Toxin Refinery (E1M3) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Toxin Refinery (E1M3) Yellow -> Toxin Refinery (E1M3) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Toxin Refinery (E1M3) Yellow -> Toxin Refinery (E1M3) Blue", player), lambda state:
state.has("Toxin Refinery (E1M3) - Yellow keycard", player, 1))
# Command Control (E1M4)
- set_rule(world.get_entrance("Hub -> Command Control (E1M4) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Command Control (E1M4) Main", player), lambda state:
state.has("Command Control (E1M4)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1))
- set_rule(world.get_entrance("Command Control (E1M4) Main -> Command Control (E1M4) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Command Control (E1M4) Main -> Command Control (E1M4) Blue", player), lambda state:
state.has("Command Control (E1M4) - Blue keycard", player, 1) or
state.has("Command Control (E1M4) - Yellow keycard", player, 1))
- set_rule(world.get_entrance("Command Control (E1M4) Main -> Command Control (E1M4) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Command Control (E1M4) Main -> Command Control (E1M4) Yellow", player), lambda state:
state.has("Command Control (E1M4) - Blue keycard", player, 1) or
state.has("Command Control (E1M4) - Yellow keycard", player, 1))
- set_rule(world.get_entrance("Command Control (E1M4) Blue -> Command Control (E1M4) Main", player), lambda state:
- state.has("Command Control (E1M4) - Yellow keycard", player, 1) or
- state.has("Command Control (E1M4) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Command Control (E1M4) Yellow -> Command Control (E1M4) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Command Control (E1M4) Blue -> Command Control (E1M4) Main", player), lambda state:
state.has("Command Control (E1M4) - Yellow keycard", player, 1) or
state.has("Command Control (E1M4) - Blue keycard", player, 1))
# Phobos Lab (E1M5)
- set_rule(world.get_entrance("Hub -> Phobos Lab (E1M5) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Phobos Lab (E1M5) Main", player), lambda state:
state.has("Phobos Lab (E1M5)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1))
- set_rule(world.get_entrance("Phobos Lab (E1M5) Main -> Phobos Lab (E1M5) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Phobos Lab (E1M5) Main -> Phobos Lab (E1M5) Yellow", player), lambda state:
state.has("Phobos Lab (E1M5) - Yellow keycard", player, 1))
- set_rule(world.get_entrance("Phobos Lab (E1M5) Yellow -> Phobos Lab (E1M5) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Phobos Lab (E1M5) Yellow -> Phobos Lab (E1M5) Blue", player), lambda state:
state.has("Phobos Lab (E1M5) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Phobos Lab (E1M5) Blue -> Phobos Lab (E1M5) Green", player), lambda state:
+ set_rule(multiworld.get_entrance("Phobos Lab (E1M5) Blue -> Phobos Lab (E1M5) Green", player), lambda state:
state.has("Phobos Lab (E1M5) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Phobos Lab (E1M5) Green -> Phobos Lab (E1M5) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Phobos Lab (E1M5) Green -> Phobos Lab (E1M5) Blue", player), lambda state:
state.has("Phobos Lab (E1M5) - Blue keycard", player, 1))
# Central Processing (E1M6)
- set_rule(world.get_entrance("Hub -> Central Processing (E1M6) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Central Processing (E1M6) Main", player), lambda state:
state.has("Central Processing (E1M6)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Rocket launcher", player, 1))
- set_rule(world.get_entrance("Central Processing (E1M6) Main -> Central Processing (E1M6) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Central Processing (E1M6) Main -> Central Processing (E1M6) Yellow", player), lambda state:
state.has("Central Processing (E1M6) - Yellow keycard", player, 1))
- set_rule(world.get_entrance("Central Processing (E1M6) Main -> Central Processing (E1M6) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Central Processing (E1M6) Main -> Central Processing (E1M6) Red", player), lambda state:
state.has("Central Processing (E1M6) - Red keycard", player, 1))
- set_rule(world.get_entrance("Central Processing (E1M6) Main -> Central Processing (E1M6) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Central Processing (E1M6) Main -> Central Processing (E1M6) Blue", player), lambda state:
state.has("Central Processing (E1M6) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Central Processing (E1M6) Main -> Central Processing (E1M6) Nukage", player), lambda state:
+ set_rule(multiworld.get_entrance("Central Processing (E1M6) Main -> Central Processing (E1M6) Nukage", player), lambda state:
state.has("Central Processing (E1M6) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Central Processing (E1M6) Yellow -> Central Processing (E1M6) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Central Processing (E1M6) Yellow -> Central Processing (E1M6) Main", player), lambda state:
state.has("Central Processing (E1M6) - Yellow keycard", player, 1))
# Computer Station (E1M7)
- set_rule(world.get_entrance("Hub -> Computer Station (E1M7) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Computer Station (E1M7) Main", player), lambda state:
state.has("Computer Station (E1M7)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Rocket launcher", player, 1))
- set_rule(world.get_entrance("Computer Station (E1M7) Main -> Computer Station (E1M7) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Computer Station (E1M7) Main -> Computer Station (E1M7) Red", player), lambda state:
state.has("Computer Station (E1M7) - Red keycard", player, 1))
- set_rule(world.get_entrance("Computer Station (E1M7) Main -> Computer Station (E1M7) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Computer Station (E1M7) Main -> Computer Station (E1M7) Yellow", player), lambda state:
state.has("Computer Station (E1M7) - Yellow keycard", player, 1))
- set_rule(world.get_entrance("Computer Station (E1M7) Blue -> Computer Station (E1M7) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Computer Station (E1M7) Blue -> Computer Station (E1M7) Yellow", player), lambda state:
state.has("Computer Station (E1M7) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Computer Station (E1M7) Red -> Computer Station (E1M7) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Computer Station (E1M7) Red -> Computer Station (E1M7) Main", player), lambda state:
state.has("Computer Station (E1M7) - Red keycard", player, 1))
- set_rule(world.get_entrance("Computer Station (E1M7) Yellow -> Computer Station (E1M7) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Computer Station (E1M7) Yellow -> Computer Station (E1M7) Blue", player), lambda state:
state.has("Computer Station (E1M7) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Computer Station (E1M7) Yellow -> Computer Station (E1M7) Courtyard", player), lambda state:
+ set_rule(multiworld.get_entrance("Computer Station (E1M7) Yellow -> Computer Station (E1M7) Courtyard", player), lambda state:
state.has("Computer Station (E1M7) - Yellow keycard", player, 1) and
state.has("Computer Station (E1M7) - Red keycard", player, 1))
- set_rule(world.get_entrance("Computer Station (E1M7) Courtyard -> Computer Station (E1M7) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Computer Station (E1M7) Courtyard -> Computer Station (E1M7) Yellow", player), lambda state:
state.has("Computer Station (E1M7) - Yellow keycard", player, 1))
# Phobos Anomaly (E1M8)
- set_rule(world.get_entrance("Hub -> Phobos Anomaly (E1M8) Start", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Phobos Anomaly (E1M8) Start", player), lambda state:
(state.has("Phobos Anomaly (E1M8)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1)) and
@@ -117,248 +114,260 @@ def set_episode1_rules(player, world):
state.has("BFG9000", player, 1)))
# Military Base (E1M9)
- set_rule(world.get_entrance("Hub -> Military Base (E1M9) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Military Base (E1M9) Main", player), lambda state:
state.has("Military Base (E1M9)", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Shotgun", player, 1))
- set_rule(world.get_entrance("Military Base (E1M9) Main -> Military Base (E1M9) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Military Base (E1M9) Main -> Military Base (E1M9) Blue", player), lambda state:
state.has("Military Base (E1M9) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Military Base (E1M9) Main -> Military Base (E1M9) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Military Base (E1M9) Main -> Military Base (E1M9) Yellow", player), lambda state:
state.has("Military Base (E1M9) - Yellow keycard", player, 1))
- set_rule(world.get_entrance("Military Base (E1M9) Main -> Military Base (E1M9) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Military Base (E1M9) Main -> Military Base (E1M9) Red", player), lambda state:
state.has("Military Base (E1M9) - Red keycard", player, 1))
- set_rule(world.get_entrance("Military Base (E1M9) Blue -> Military Base (E1M9) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Military Base (E1M9) Blue -> Military Base (E1M9) Main", player), lambda state:
state.has("Military Base (E1M9) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Military Base (E1M9) Yellow -> Military Base (E1M9) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Military Base (E1M9) Yellow -> Military Base (E1M9) Main", player), lambda state:
state.has("Military Base (E1M9) - Yellow keycard", player, 1))
-def set_episode2_rules(player, world):
+def set_episode2_rules(player, multiworld, pro):
# Deimos Anomaly (E2M1)
- set_rule(world.get_entrance("Hub -> Deimos Anomaly (E2M1) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Deimos Anomaly (E2M1) Main", player), lambda state:
state.has("Deimos Anomaly (E2M1)", player, 1))
- set_rule(world.get_entrance("Deimos Anomaly (E2M1) Main -> Deimos Anomaly (E2M1) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Deimos Anomaly (E2M1) Main -> Deimos Anomaly (E2M1) Red", player), lambda state:
state.has("Deimos Anomaly (E2M1) - Red keycard", player, 1))
- set_rule(world.get_entrance("Deimos Anomaly (E2M1) Main -> Deimos Anomaly (E2M1) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Deimos Anomaly (E2M1) Main -> Deimos Anomaly (E2M1) Blue", player), lambda state:
state.has("Deimos Anomaly (E2M1) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Deimos Anomaly (E2M1) Blue -> Deimos Anomaly (E2M1) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Deimos Anomaly (E2M1) Blue -> Deimos Anomaly (E2M1) Main", player), lambda state:
state.has("Deimos Anomaly (E2M1) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Deimos Anomaly (E2M1) Red -> Deimos Anomaly (E2M1) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Deimos Anomaly (E2M1) Red -> Deimos Anomaly (E2M1) Main", player), lambda state:
state.has("Deimos Anomaly (E2M1) - Red keycard", player, 1))
# Containment Area (E2M2)
- set_rule(world.get_entrance("Hub -> Containment Area (E2M2) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Containment Area (E2M2) Main", player), lambda state:
(state.has("Containment Area (E2M2)", player, 1) and
state.has("Shotgun", player, 1)) and
(state.has("Chaingun", player, 1) or
state.has("Plasma gun", player, 1)))
- set_rule(world.get_entrance("Containment Area (E2M2) Main -> Containment Area (E2M2) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Containment Area (E2M2) Main -> Containment Area (E2M2) Yellow", player), lambda state:
state.has("Containment Area (E2M2) - Yellow keycard", player, 1))
- set_rule(world.get_entrance("Containment Area (E2M2) Main -> Containment Area (E2M2) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Containment Area (E2M2) Main -> Containment Area (E2M2) Blue", player), lambda state:
state.has("Containment Area (E2M2) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Containment Area (E2M2) Main -> Containment Area (E2M2) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Containment Area (E2M2) Main -> Containment Area (E2M2) Red", player), lambda state:
state.has("Containment Area (E2M2) - Red keycard", player, 1))
- set_rule(world.get_entrance("Containment Area (E2M2) Blue -> Containment Area (E2M2) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Containment Area (E2M2) Blue -> Containment Area (E2M2) Main", player), lambda state:
state.has("Containment Area (E2M2) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Containment Area (E2M2) Red -> Containment Area (E2M2) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Containment Area (E2M2) Red -> Containment Area (E2M2) Main", player), lambda state:
state.has("Containment Area (E2M2) - Red keycard", player, 1))
# Refinery (E2M3)
- set_rule(world.get_entrance("Hub -> Refinery (E2M3) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Refinery (E2M3) Main", player), lambda state:
(state.has("Refinery (E2M3)", player, 1) and
state.has("Shotgun", player, 1)) and
(state.has("Chaingun", player, 1) or
state.has("Plasma gun", player, 1)))
- set_rule(world.get_entrance("Refinery (E2M3) Main -> Refinery (E2M3) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Refinery (E2M3) Main -> Refinery (E2M3) Blue", player), lambda state:
state.has("Refinery (E2M3) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Refinery (E2M3) Blue -> Refinery (E2M3) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Refinery (E2M3) Blue -> Refinery (E2M3) Main", player), lambda state:
state.has("Refinery (E2M3) - Blue keycard", player, 1))
# Deimos Lab (E2M4)
- set_rule(world.get_entrance("Hub -> Deimos Lab (E2M4) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Deimos Lab (E2M4) Main", player), lambda state:
state.has("Deimos Lab (E2M4)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Plasma gun", player, 1))
- set_rule(world.get_entrance("Deimos Lab (E2M4) Main -> Deimos Lab (E2M4) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Deimos Lab (E2M4) Main -> Deimos Lab (E2M4) Blue", player), lambda state:
state.has("Deimos Lab (E2M4) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Deimos Lab (E2M4) Blue -> Deimos Lab (E2M4) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Deimos Lab (E2M4) Blue -> Deimos Lab (E2M4) Yellow", player), lambda state:
state.has("Deimos Lab (E2M4) - Yellow keycard", player, 1))
# Command Center (E2M5)
- set_rule(world.get_entrance("Hub -> Command Center (E2M5) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Command Center (E2M5) Main", player), lambda state:
state.has("Command Center (E2M5)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Plasma gun", player, 1))
# Halls of the Damned (E2M6)
- set_rule(world.get_entrance("Hub -> Halls of the Damned (E2M6) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Halls of the Damned (E2M6) Main", player), lambda state:
state.has("Halls of the Damned (E2M6)", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Plasma gun", player, 1))
- set_rule(world.get_entrance("Halls of the Damned (E2M6) Main -> Halls of the Damned (E2M6) Blue Yellow Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Halls of the Damned (E2M6) Main -> Halls of the Damned (E2M6) Blue Yellow Red", player), lambda state:
state.has("Halls of the Damned (E2M6) - Blue skull key", player, 1) and
state.has("Halls of the Damned (E2M6) - Yellow skull key", player, 1) and
state.has("Halls of the Damned (E2M6) - Red skull key", player, 1))
- set_rule(world.get_entrance("Halls of the Damned (E2M6) Main -> Halls of the Damned (E2M6) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Halls of the Damned (E2M6) Main -> Halls of the Damned (E2M6) Yellow", player), lambda state:
state.has("Halls of the Damned (E2M6) - Yellow skull key", player, 1))
- set_rule(world.get_entrance("Halls of the Damned (E2M6) Main -> Halls of the Damned (E2M6) One way Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Halls of the Damned (E2M6) Main -> Halls of the Damned (E2M6) One way Yellow", player), lambda state:
state.has("Halls of the Damned (E2M6) - Yellow skull key", player, 1))
- set_rule(world.get_entrance("Halls of the Damned (E2M6) Blue Yellow Red -> Halls of the Damned (E2M6) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Halls of the Damned (E2M6) Blue Yellow Red -> Halls of the Damned (E2M6) Main", player), lambda state:
state.has("Halls of the Damned (E2M6) - Blue skull key", player, 1) and
state.has("Halls of the Damned (E2M6) - Yellow skull key", player, 1) and
state.has("Halls of the Damned (E2M6) - Red skull key", player, 1))
- set_rule(world.get_entrance("Halls of the Damned (E2M6) One way Yellow -> Halls of the Damned (E2M6) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Halls of the Damned (E2M6) One way Yellow -> Halls of the Damned (E2M6) Main", player), lambda state:
state.has("Halls of the Damned (E2M6) - Yellow skull key", player, 1))
# Spawning Vats (E2M7)
- set_rule(world.get_entrance("Hub -> Spawning Vats (E2M7) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Spawning Vats (E2M7) Main", player), lambda state:
state.has("Spawning Vats (E2M7)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Plasma gun", player, 1) and
state.has("Rocket launcher", player, 1))
- set_rule(world.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Blue", player), lambda state:
state.has("Spawning Vats (E2M7) - Blue keycard", player, 1))
- set_rule(world.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Entrance Secret", player), lambda state:
+ set_rule(multiworld.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Entrance Secret", player), lambda state:
state.has("Spawning Vats (E2M7) - Blue keycard", player, 1) and
state.has("Spawning Vats (E2M7) - Red keycard", player, 1))
- set_rule(world.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Red", player), lambda state:
state.has("Spawning Vats (E2M7) - Red keycard", player, 1))
- set_rule(world.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Yellow", player), lambda state:
state.has("Spawning Vats (E2M7) - Yellow keycard", player, 1))
- set_rule(world.get_entrance("Spawning Vats (E2M7) Yellow -> Spawning Vats (E2M7) Main", player), lambda state:
+ if pro:
+ set_rule(multiworld.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Red Exit", player), lambda state:
+ state.has("Rocket launcher", player, 1))
+ set_rule(multiworld.get_entrance("Spawning Vats (E2M7) Yellow -> Spawning Vats (E2M7) Main", player), lambda state:
state.has("Spawning Vats (E2M7) - Yellow keycard", player, 1))
- set_rule(world.get_entrance("Spawning Vats (E2M7) Red -> Spawning Vats (E2M7) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Spawning Vats (E2M7) Red -> Spawning Vats (E2M7) Main", player), lambda state:
state.has("Spawning Vats (E2M7) - Red keycard", player, 1))
- set_rule(world.get_entrance("Spawning Vats (E2M7) Entrance Secret -> Spawning Vats (E2M7) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Spawning Vats (E2M7) Entrance Secret -> Spawning Vats (E2M7) Main", player), lambda state:
state.has("Spawning Vats (E2M7) - Blue keycard", player, 1) and
state.has("Spawning Vats (E2M7) - Red keycard", player, 1))
# Tower of Babel (E2M8)
- set_rule(world.get_entrance("Hub -> Tower of Babel (E2M8) Main", player), lambda state:
- state.has("Tower of Babel (E2M8)", player, 1))
+ set_rule(multiworld.get_entrance("Hub -> Tower of Babel (E2M8) Main", player), lambda state:
+ (state.has("Tower of Babel (E2M8)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
# Fortress of Mystery (E2M9)
- set_rule(world.get_entrance("Hub -> Fortress of Mystery (E2M9) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Fortress of Mystery (E2M9) Main", player), lambda state:
(state.has("Fortress of Mystery (E2M9)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
- set_rule(world.get_entrance("Fortress of Mystery (E2M9) Main -> Fortress of Mystery (E2M9) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Fortress of Mystery (E2M9) Main -> Fortress of Mystery (E2M9) Blue", player), lambda state:
state.has("Fortress of Mystery (E2M9) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Fortress of Mystery (E2M9) Main -> Fortress of Mystery (E2M9) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Fortress of Mystery (E2M9) Main -> Fortress of Mystery (E2M9) Red", player), lambda state:
state.has("Fortress of Mystery (E2M9) - Red skull key", player, 1))
- set_rule(world.get_entrance("Fortress of Mystery (E2M9) Main -> Fortress of Mystery (E2M9) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Fortress of Mystery (E2M9) Main -> Fortress of Mystery (E2M9) Yellow", player), lambda state:
state.has("Fortress of Mystery (E2M9) - Yellow skull key", player, 1))
- set_rule(world.get_entrance("Fortress of Mystery (E2M9) Blue -> Fortress of Mystery (E2M9) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Fortress of Mystery (E2M9) Blue -> Fortress of Mystery (E2M9) Main", player), lambda state:
state.has("Fortress of Mystery (E2M9) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Fortress of Mystery (E2M9) Red -> Fortress of Mystery (E2M9) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Fortress of Mystery (E2M9) Red -> Fortress of Mystery (E2M9) Main", player), lambda state:
state.has("Fortress of Mystery (E2M9) - Red skull key", player, 1))
- set_rule(world.get_entrance("Fortress of Mystery (E2M9) Yellow -> Fortress of Mystery (E2M9) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Fortress of Mystery (E2M9) Yellow -> Fortress of Mystery (E2M9) Main", player), lambda state:
state.has("Fortress of Mystery (E2M9) - Yellow skull key", player, 1))
-def set_episode3_rules(player, world):
+def set_episode3_rules(player, multiworld, pro):
# Hell Keep (E3M1)
- set_rule(world.get_entrance("Hub -> Hell Keep (E3M1) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Hell Keep (E3M1) Main", player), lambda state:
state.has("Hell Keep (E3M1)", player, 1))
- set_rule(world.get_entrance("Hell Keep (E3M1) Main -> Hell Keep (E3M1) Narrow", player), lambda state:
+ set_rule(multiworld.get_entrance("Hell Keep (E3M1) Main -> Hell Keep (E3M1) Narrow", player), lambda state:
state.has("Chaingun", player, 1) or
state.has("Shotgun", player, 1))
# Slough of Despair (E3M2)
- set_rule(world.get_entrance("Hub -> Slough of Despair (E3M2) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Slough of Despair (E3M2) Main", player), lambda state:
(state.has("Slough of Despair (E3M2)", player, 1)) and
(state.has("Shotgun", player, 1) or
state.has("Chaingun", player, 1)))
- set_rule(world.get_entrance("Slough of Despair (E3M2) Main -> Slough of Despair (E3M2) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Slough of Despair (E3M2) Main -> Slough of Despair (E3M2) Blue", player), lambda state:
state.has("Slough of Despair (E3M2) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Slough of Despair (E3M2) Blue -> Slough of Despair (E3M2) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Slough of Despair (E3M2) Blue -> Slough of Despair (E3M2) Main", player), lambda state:
state.has("Slough of Despair (E3M2) - Blue skull key", player, 1))
# Pandemonium (E3M3)
- set_rule(world.get_entrance("Hub -> Pandemonium (E3M3) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Pandemonium (E3M3) Main", player), lambda state:
(state.has("Pandemonium (E3M3)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
- set_rule(world.get_entrance("Pandemonium (E3M3) Main -> Pandemonium (E3M3) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Pandemonium (E3M3) Main -> Pandemonium (E3M3) Blue", player), lambda state:
state.has("Pandemonium (E3M3) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Pandemonium (E3M3) Blue -> Pandemonium (E3M3) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Pandemonium (E3M3) Blue -> Pandemonium (E3M3) Main", player), lambda state:
state.has("Pandemonium (E3M3) - Blue skull key", player, 1))
# House of Pain (E3M4)
- set_rule(world.get_entrance("Hub -> House of Pain (E3M4) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> House of Pain (E3M4) Main", player), lambda state:
(state.has("House of Pain (E3M4)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
- set_rule(world.get_entrance("House of Pain (E3M4) Main -> House of Pain (E3M4) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("House of Pain (E3M4) Main -> House of Pain (E3M4) Blue", player), lambda state:
state.has("House of Pain (E3M4) - Blue skull key", player, 1))
- set_rule(world.get_entrance("House of Pain (E3M4) Blue -> House of Pain (E3M4) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("House of Pain (E3M4) Blue -> House of Pain (E3M4) Main", player), lambda state:
state.has("House of Pain (E3M4) - Blue skull key", player, 1))
- set_rule(world.get_entrance("House of Pain (E3M4) Blue -> House of Pain (E3M4) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("House of Pain (E3M4) Blue -> House of Pain (E3M4) Yellow", player), lambda state:
state.has("House of Pain (E3M4) - Yellow skull key", player, 1))
- set_rule(world.get_entrance("House of Pain (E3M4) Blue -> House of Pain (E3M4) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("House of Pain (E3M4) Blue -> House of Pain (E3M4) Red", player), lambda state:
state.has("House of Pain (E3M4) - Red skull key", player, 1))
- set_rule(world.get_entrance("House of Pain (E3M4) Red -> House of Pain (E3M4) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("House of Pain (E3M4) Red -> House of Pain (E3M4) Blue", player), lambda state:
state.has("House of Pain (E3M4) - Red skull key", player, 1))
- set_rule(world.get_entrance("House of Pain (E3M4) Yellow -> House of Pain (E3M4) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("House of Pain (E3M4) Yellow -> House of Pain (E3M4) Blue", player), lambda state:
state.has("House of Pain (E3M4) - Yellow skull key", player, 1))
# Unholy Cathedral (E3M5)
- set_rule(world.get_entrance("Hub -> Unholy Cathedral (E3M5) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Unholy Cathedral (E3M5) Main", player), lambda state:
(state.has("Unholy Cathedral (E3M5)", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Shotgun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
- set_rule(world.get_entrance("Unholy Cathedral (E3M5) Main -> Unholy Cathedral (E3M5) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Main -> Unholy Cathedral (E3M5) Yellow", player), lambda state:
state.has("Unholy Cathedral (E3M5) - Yellow skull key", player, 1))
- set_rule(world.get_entrance("Unholy Cathedral (E3M5) Main -> Unholy Cathedral (E3M5) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Main -> Unholy Cathedral (E3M5) Blue", player), lambda state:
state.has("Unholy Cathedral (E3M5) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Unholy Cathedral (E3M5) Blue -> Unholy Cathedral (E3M5) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Blue -> Unholy Cathedral (E3M5) Main", player), lambda state:
state.has("Unholy Cathedral (E3M5) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Unholy Cathedral (E3M5) Yellow -> Unholy Cathedral (E3M5) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Unholy Cathedral (E3M5) Yellow -> Unholy Cathedral (E3M5) Main", player), lambda state:
state.has("Unholy Cathedral (E3M5) - Yellow skull key", player, 1))
# Mt. Erebus (E3M6)
- set_rule(world.get_entrance("Hub -> Mt. Erebus (E3M6) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Mt. Erebus (E3M6) Main", player), lambda state:
state.has("Mt. Erebus (E3M6)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1))
- set_rule(world.get_entrance("Mt. Erebus (E3M6) Main -> Mt. Erebus (E3M6) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Mt. Erebus (E3M6) Main -> Mt. Erebus (E3M6) Blue", player), lambda state:
state.has("Mt. Erebus (E3M6) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Mt. Erebus (E3M6) Blue -> Mt. Erebus (E3M6) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Mt. Erebus (E3M6) Blue -> Mt. Erebus (E3M6) Main", player), lambda state:
state.has("Mt. Erebus (E3M6) - Blue skull key", player, 1))
# Limbo (E3M7)
- set_rule(world.get_entrance("Hub -> Limbo (E3M7) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Limbo (E3M7) Main", player), lambda state:
(state.has("Limbo (E3M7)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
- set_rule(world.get_entrance("Limbo (E3M7) Main -> Limbo (E3M7) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Limbo (E3M7) Main -> Limbo (E3M7) Red", player), lambda state:
state.has("Limbo (E3M7) - Red skull key", player, 1))
- set_rule(world.get_entrance("Limbo (E3M7) Main -> Limbo (E3M7) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Limbo (E3M7) Main -> Limbo (E3M7) Blue", player), lambda state:
+ state.has("Limbo (E3M7) - Blue skull key", player, 1))
+ set_rule(multiworld.get_entrance("Limbo (E3M7) Main -> Limbo (E3M7) Pink", player), lambda state:
state.has("Limbo (E3M7) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Limbo (E3M7) Red -> Limbo (E3M7) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Limbo (E3M7) Red -> Limbo (E3M7) Yellow", player), lambda state:
state.has("Limbo (E3M7) - Yellow skull key", player, 1))
+ set_rule(multiworld.get_entrance("Limbo (E3M7) Pink -> Limbo (E3M7) Green", player), lambda state:
+ state.has("Limbo (E3M7) - Red skull key", player, 1))
# Dis (E3M8)
- set_rule(world.get_entrance("Hub -> Dis (E3M8) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Dis (E3M8) Main", player), lambda state:
(state.has("Dis (E3M8)", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Shotgun", player, 1)) and
@@ -367,131 +376,129 @@ def set_episode3_rules(player, world):
state.has("Rocket launcher", player, 1)))
# Warrens (E3M9)
- set_rule(world.get_entrance("Hub -> Warrens (E3M9) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Warrens (E3M9) Main", player), lambda state:
(state.has("Warrens (E3M9)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Plasma gun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("BFG9000", player, 1)))
- set_rule(world.get_entrance("Warrens (E3M9) Main -> Warrens (E3M9) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Warrens (E3M9) Main -> Warrens (E3M9) Blue", player), lambda state:
state.has("Warrens (E3M9) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Warrens (E3M9) Main -> Warrens (E3M9) Blue trigger", player), lambda state:
+ set_rule(multiworld.get_entrance("Warrens (E3M9) Main -> Warrens (E3M9) Blue trigger", player), lambda state:
state.has("Warrens (E3M9) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Warrens (E3M9) Blue -> Warrens (E3M9) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Warrens (E3M9) Blue -> Warrens (E3M9) Main", player), lambda state:
state.has("Warrens (E3M9) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Warrens (E3M9) Blue -> Warrens (E3M9) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Warrens (E3M9) Blue -> Warrens (E3M9) Red", player), lambda state:
state.has("Warrens (E3M9) - Red skull key", player, 1))
-def set_episode4_rules(player, world):
+def set_episode4_rules(player, multiworld, pro):
# Hell Beneath (E4M1)
- set_rule(world.get_entrance("Hub -> Hell Beneath (E4M1) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Hell Beneath (E4M1) Main", player), lambda state:
state.has("Hell Beneath (E4M1)", player, 1))
- set_rule(world.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Red", player), lambda state:
(state.has("Hell Beneath (E4M1) - Red skull key", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1)) and (state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
- set_rule(world.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Blue", player), lambda state:
state.has("Shotgun", player, 1) or
state.has("Chaingun", player, 1) or
state.has("Hell Beneath (E4M1) - Blue skull key", player, 1))
# Perfect Hatred (E4M2)
- set_rule(world.get_entrance("Hub -> Perfect Hatred (E4M2) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Perfect Hatred (E4M2) Main", player), lambda state:
(state.has("Perfect Hatred (E4M2)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
- set_rule(world.get_entrance("Perfect Hatred (E4M2) Main -> Perfect Hatred (E4M2) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Perfect Hatred (E4M2) Main -> Perfect Hatred (E4M2) Blue", player), lambda state:
state.has("Perfect Hatred (E4M2) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Perfect Hatred (E4M2) Main -> Perfect Hatred (E4M2) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Perfect Hatred (E4M2) Main -> Perfect Hatred (E4M2) Yellow", player), lambda state:
state.has("Perfect Hatred (E4M2) - Yellow skull key", player, 1))
# Sever the Wicked (E4M3)
- set_rule(world.get_entrance("Hub -> Sever the Wicked (E4M3) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Sever the Wicked (E4M3) Main", player), lambda state:
(state.has("Sever the Wicked (E4M3)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
- set_rule(world.get_entrance("Sever the Wicked (E4M3) Main -> Sever the Wicked (E4M3) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Sever the Wicked (E4M3) Main -> Sever the Wicked (E4M3) Red", player), lambda state:
state.has("Sever the Wicked (E4M3) - Red skull key", player, 1))
- set_rule(world.get_entrance("Sever the Wicked (E4M3) Red -> Sever the Wicked (E4M3) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Sever the Wicked (E4M3) Red -> Sever the Wicked (E4M3) Blue", player), lambda state:
state.has("Sever the Wicked (E4M3) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Sever the Wicked (E4M3) Red -> Sever the Wicked (E4M3) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Sever the Wicked (E4M3) Red -> Sever the Wicked (E4M3) Main", player), lambda state:
state.has("Sever the Wicked (E4M3) - Red skull key", player, 1))
- set_rule(world.get_entrance("Sever the Wicked (E4M3) Blue -> Sever the Wicked (E4M3) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Sever the Wicked (E4M3) Blue -> Sever the Wicked (E4M3) Red", player), lambda state:
state.has("Sever the Wicked (E4M3) - Blue skull key", player, 1))
# Unruly Evil (E4M4)
- set_rule(world.get_entrance("Hub -> Unruly Evil (E4M4) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Unruly Evil (E4M4) Main", player), lambda state:
(state.has("Unruly Evil (E4M4)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
- set_rule(world.get_entrance("Unruly Evil (E4M4) Main -> Unruly Evil (E4M4) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Unruly Evil (E4M4) Main -> Unruly Evil (E4M4) Red", player), lambda state:
state.has("Unruly Evil (E4M4) - Red skull key", player, 1))
# They Will Repent (E4M5)
- set_rule(world.get_entrance("Hub -> They Will Repent (E4M5) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> They Will Repent (E4M5) Main", player), lambda state:
(state.has("They Will Repent (E4M5)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
- set_rule(world.get_entrance("They Will Repent (E4M5) Main -> They Will Repent (E4M5) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("They Will Repent (E4M5) Main -> They Will Repent (E4M5) Red", player), lambda state:
state.has("They Will Repent (E4M5) - Red skull key", player, 1))
- set_rule(world.get_entrance("They Will Repent (E4M5) Red -> They Will Repent (E4M5) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("They Will Repent (E4M5) Red -> They Will Repent (E4M5) Main", player), lambda state:
state.has("They Will Repent (E4M5) - Red skull key", player, 1))
- set_rule(world.get_entrance("They Will Repent (E4M5) Red -> They Will Repent (E4M5) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("They Will Repent (E4M5) Red -> They Will Repent (E4M5) Yellow", player), lambda state:
state.has("They Will Repent (E4M5) - Yellow skull key", player, 1))
- set_rule(world.get_entrance("They Will Repent (E4M5) Red -> They Will Repent (E4M5) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("They Will Repent (E4M5) Red -> They Will Repent (E4M5) Blue", player), lambda state:
state.has("They Will Repent (E4M5) - Blue skull key", player, 1))
# Against Thee Wickedly (E4M6)
- set_rule(world.get_entrance("Hub -> Against Thee Wickedly (E4M6) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Against Thee Wickedly (E4M6) Main", player), lambda state:
(state.has("Against Thee Wickedly (E4M6)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
- set_rule(world.get_entrance("Against Thee Wickedly (E4M6) Main -> Against Thee Wickedly (E4M6) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("Against Thee Wickedly (E4M6) Main -> Against Thee Wickedly (E4M6) Blue", player), lambda state:
state.has("Against Thee Wickedly (E4M6) - Blue skull key", player, 1))
- set_rule(world.get_entrance("Against Thee Wickedly (E4M6) Main -> Against Thee Wickedly (E4M6) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Against Thee Wickedly (E4M6) Blue -> Against Thee Wickedly (E4M6) Yellow", player), lambda state:
state.has("Against Thee Wickedly (E4M6) - Yellow skull key", player, 1))
- set_rule(world.get_entrance("Against Thee Wickedly (E4M6) Main -> Against Thee Wickedly (E4M6) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Against Thee Wickedly (E4M6) Blue -> Against Thee Wickedly (E4M6) Red", player), lambda state:
state.has("Against Thee Wickedly (E4M6) - Red skull key", player, 1))
- set_rule(world.get_entrance("Against Thee Wickedly (E4M6) Blue -> Against Thee Wickedly (E4M6) Main", player), lambda state:
- state.has("Against Thee Wickedly (E4M6) - Blue skull key", player, 1))
# And Hell Followed (E4M7)
- set_rule(world.get_entrance("Hub -> And Hell Followed (E4M7) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> And Hell Followed (E4M7) Main", player), lambda state:
(state.has("And Hell Followed (E4M7)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1)) and
(state.has("Rocket launcher", player, 1) or
state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
- set_rule(world.get_entrance("And Hell Followed (E4M7) Main -> And Hell Followed (E4M7) Blue", player), lambda state:
+ set_rule(multiworld.get_entrance("And Hell Followed (E4M7) Main -> And Hell Followed (E4M7) Blue", player), lambda state:
state.has("And Hell Followed (E4M7) - Blue skull key", player, 1))
- set_rule(world.get_entrance("And Hell Followed (E4M7) Main -> And Hell Followed (E4M7) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("And Hell Followed (E4M7) Main -> And Hell Followed (E4M7) Red", player), lambda state:
state.has("And Hell Followed (E4M7) - Red skull key", player, 1))
- set_rule(world.get_entrance("And Hell Followed (E4M7) Main -> And Hell Followed (E4M7) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("And Hell Followed (E4M7) Main -> And Hell Followed (E4M7) Yellow", player), lambda state:
state.has("And Hell Followed (E4M7) - Yellow skull key", player, 1))
- set_rule(world.get_entrance("And Hell Followed (E4M7) Red -> And Hell Followed (E4M7) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("And Hell Followed (E4M7) Red -> And Hell Followed (E4M7) Main", player), lambda state:
state.has("And Hell Followed (E4M7) - Red skull key", player, 1))
# Unto the Cruel (E4M8)
- set_rule(world.get_entrance("Hub -> Unto the Cruel (E4M8) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Unto the Cruel (E4M8) Main", player), lambda state:
(state.has("Unto the Cruel (E4M8)", player, 1) and
state.has("Chainsaw", player, 1) and
state.has("Shotgun", player, 1) and
@@ -499,37 +506,37 @@ def set_episode4_rules(player, world):
state.has("Rocket launcher", player, 1)) and
(state.has("BFG9000", player, 1) or
state.has("Plasma gun", player, 1)))
- set_rule(world.get_entrance("Unto the Cruel (E4M8) Main -> Unto the Cruel (E4M8) Red", player), lambda state:
+ set_rule(multiworld.get_entrance("Unto the Cruel (E4M8) Main -> Unto the Cruel (E4M8) Red", player), lambda state:
state.has("Unto the Cruel (E4M8) - Red skull key", player, 1))
- set_rule(world.get_entrance("Unto the Cruel (E4M8) Main -> Unto the Cruel (E4M8) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Unto the Cruel (E4M8) Main -> Unto the Cruel (E4M8) Yellow", player), lambda state:
state.has("Unto the Cruel (E4M8) - Yellow skull key", player, 1))
- set_rule(world.get_entrance("Unto the Cruel (E4M8) Main -> Unto the Cruel (E4M8) Orange", player), lambda state:
+ set_rule(multiworld.get_entrance("Unto the Cruel (E4M8) Main -> Unto the Cruel (E4M8) Orange", player), lambda state:
state.has("Unto the Cruel (E4M8) - Yellow skull key", player, 1) and
state.has("Unto the Cruel (E4M8) - Red skull key", player, 1))
# Fear (E4M9)
- set_rule(world.get_entrance("Hub -> Fear (E4M9) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Hub -> Fear (E4M9) Main", player), lambda state:
state.has("Fear (E4M9)", player, 1) and
state.has("Shotgun", player, 1) and
state.has("Chaingun", player, 1) and
state.has("Rocket launcher", player, 1) and
state.has("Plasma gun", player, 1) and
state.has("BFG9000", player, 1))
- set_rule(world.get_entrance("Fear (E4M9) Main -> Fear (E4M9) Yellow", player), lambda state:
+ set_rule(multiworld.get_entrance("Fear (E4M9) Main -> Fear (E4M9) Yellow", player), lambda state:
state.has("Fear (E4M9) - Yellow skull key", player, 1))
- set_rule(world.get_entrance("Fear (E4M9) Yellow -> Fear (E4M9) Main", player), lambda state:
+ set_rule(multiworld.get_entrance("Fear (E4M9) Yellow -> Fear (E4M9) Main", player), lambda state:
state.has("Fear (E4M9) - Yellow skull key", player, 1))
-def set_rules(doom_1993_world: "DOOM1993World", included_episodes):
+def set_rules(doom_1993_world: "DOOM1993World", included_episodes, pro):
player = doom_1993_world.player
- world = doom_1993_world.multiworld
+ multiworld = doom_1993_world.multiworld
if included_episodes[0]:
- set_episode1_rules(player, world)
+ set_episode1_rules(player, multiworld, pro)
if included_episodes[1]:
- set_episode2_rules(player, world)
+ set_episode2_rules(player, multiworld, pro)
if included_episodes[2]:
- set_episode3_rules(player, world)
+ set_episode3_rules(player, multiworld, pro)
if included_episodes[3]:
- set_episode4_rules(player, world)
+ set_episode4_rules(player, multiworld, pro)
diff --git a/worlds/doom_1993/__init__.py b/worlds/doom_1993/__init__.py
index 83a8652af1d1..b6138ae07103 100644
--- a/worlds/doom_1993/__init__.py
+++ b/worlds/doom_1993/__init__.py
@@ -2,9 +2,10 @@
import logging
from typing import Any, Dict, List
-from BaseClasses import Entrance, CollectionState, Item, ItemClassification, Location, MultiWorld, Region, Tutorial
+from BaseClasses import Entrance, CollectionState, Item, Location, MultiWorld, Region, Tutorial
from worlds.AutoWorld import WebWorld, World
-from . import Items, Locations, Maps, Options, Regions, Rules
+from . import Items, Locations, Maps, Regions, Rules
+from .Options import DOOM1993Options
logger = logging.getLogger("DOOM 1993")
@@ -37,10 +38,10 @@ class DOOM1993World(World):
Developed by id Software, and originally released in 1993, DOOM pioneered and popularized the first-person shooter,
setting a standard for all FPS games.
"""
- option_definitions = Options.options
+ options_dataclass = DOOM1993Options
+ options: DOOM1993Options
game = "DOOM 1993"
web = DOOM1993Web()
- data_version = 3
required_client_version = (0, 3, 9)
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
@@ -56,6 +57,13 @@ class DOOM1993World(World):
"Hell Beneath (E4M1)"
]
+ boss_level_for_espidoes: List[str] = [
+ "Phobos Anomaly (E1M8)",
+ "Tower of Babel (E2M8)",
+ "Dis (E3M8)",
+ "Unto the Cruel (E4M8)"
+ ]
+
# Item ratio that scales depending on episode count. These are the ratio for 3 episode.
items_ratio: Dict[str, float] = {
"Armor": 41,
@@ -71,25 +79,29 @@ class DOOM1993World(World):
"Energy cell pack": 10
}
- def __init__(self, world: MultiWorld, player: int):
+ def __init__(self, multiworld: MultiWorld, player: int):
self.included_episodes = [1, 1, 1, 0]
self.location_count = 0
- super().__init__(world, player)
+ super().__init__(multiworld, player)
def get_episode_count(self):
return functools.reduce(lambda count, episode: count + episode, self.included_episodes)
def generate_early(self):
# Cache which episodes are included
- for i in range(4):
- self.included_episodes[i] = getattr(self.multiworld, f"episode{i + 1}")[self.player].value
+ self.included_episodes[0] = self.options.episode1.value
+ self.included_episodes[1] = self.options.episode2.value
+ self.included_episodes[2] = self.options.episode3.value
+ self.included_episodes[3] = self.options.episode4.value
# If no episodes selected, select Episode 1
if self.get_episode_count() == 0:
self.included_episodes[0] = 1
def create_regions(self):
+ pro = self.options.pro.value
+
# Main regions
menu_region = Region("Menu", self.player, self.multiworld)
hub_region = Region("Hub", self.player, self.multiworld)
@@ -116,8 +128,11 @@ def create_regions(self):
self.multiworld.regions.append(region)
- for connection in region_dict["connections"]:
- connections.append((region, connection))
+ for connection_dict in region_dict["connections"]:
+ # Check if it's a pro-only connection
+ if connection_dict["pro"] and not pro:
+ continue
+ connections.append((region, connection_dict["target"]))
# Connect main regions to Hub
hub_region.add_exits(main_regions)
@@ -135,7 +150,11 @@ def create_regions(self):
self.location_count = len(self.multiworld.get_locations(self.player))
def completion_rule(self, state: CollectionState):
- for map_name in Maps.map_names:
+ goal_levels = Maps.map_names
+ if self.options.goal.value:
+ goal_levels = self.boss_level_for_espidoes
+
+ for map_name in goal_levels:
if map_name + " - Exit" not in self.location_name_to_id:
continue
@@ -151,23 +170,25 @@ def completion_rule(self, state: CollectionState):
return True
def set_rules(self):
- Rules.set_rules(self, self.included_episodes)
+ pro = self.options.pro.value
+ allow_death_logic = self.options.allow_death_logic.value
+
+ Rules.set_rules(self, self.included_episodes, pro)
self.multiworld.completion_condition[self.player] = lambda state: self.completion_rule(state)
# Forbid progression items to locations that can be missed and can't be picked up. (e.g. One-time timed
# platform) Unless the user allows for it.
- if not getattr(self.multiworld, "allow_death_logic")[self.player].value:
+ if not allow_death_logic:
for death_logic_location in Locations.death_logic_locations:
- self.multiworld.exclude_locations[self.player].value.add(death_logic_location)
+ self.options.exclude_locations.value.add(death_logic_location)
def create_item(self, name: str) -> DOOM1993Item:
item_id: int = self.item_name_to_id[name]
return DOOM1993Item(name, Items.item_table[item_id]["classification"], item_id, self.player)
def create_items(self):
- is_only_first_episode: bool = self.get_episode_count() == 1 and self.included_episodes[0]
itempool: List[DOOM1993Item] = []
- start_with_computer_area_maps: bool = getattr(self.multiworld, "start_with_computer_area_maps")[self.player].value
+ start_with_computer_area_maps: bool = self.options.start_with_computer_area_maps.value
# Items
for item_id, item in Items.item_table.items():
@@ -180,9 +201,6 @@ def create_items(self):
if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]:
continue
- if item["name"] in {"BFG9000", "Plasma Gun"} and is_only_first_episode:
- continue # Don't include those guns if only first episode
-
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
itempool += [self.create_item(item["name"]) for _ in range(count)]
@@ -210,10 +228,12 @@ def create_items(self):
self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i]))
# Give Computer area maps if option selected
- if getattr(self.multiworld, "start_with_computer_area_maps")[self.player].value:
+ if self.options.start_with_computer_area_maps.value:
for item_id, item_dict in Items.item_table.items():
- if item_dict["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP:
- self.multiworld.push_precollected(self.create_item(item_dict["name"]))
+ item_episode = item_dict["episode"]
+ if item_episode > 0:
+ if item_dict["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP and self.included_episodes[item_episode - 1]:
+ self.multiworld.push_precollected(self.create_item(item_dict["name"]))
# Fill the rest starting with powerups, then fillers
self.create_ratioed_items("Armor", itempool)
@@ -252,7 +272,7 @@ def create_ratioed_items(self, item_name: str, itempool: List[DOOM1993Item]):
itempool.append(self.create_item(item_name))
def fill_slot_data(self) -> Dict[str, Any]:
- slot_data = {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions}
+ slot_data = self.options.as_dict("goal", "difficulty", "random_monsters", "random_pickups", "random_music", "flip_levels", "allow_death_logic", "pro", "start_with_computer_area_maps", "death_link", "reset_level_on_death", "episode1", "episode2", "episode3", "episode4")
# E2M6 and E3M9 each have one way keydoor. You can enter, but required the keycard to get out.
# We used to force place the keycard behind those doors. Limiting the randomness for those items. A change
diff --git a/worlds/doom_1993/docs/en_DOOM 1993.md b/worlds/doom_1993/docs/en_DOOM 1993.md
index 0419741bc308..ea7e3200307f 100644
--- a/worlds/doom_1993/docs/en_DOOM 1993.md
+++ b/worlds/doom_1993/docs/en_DOOM 1993.md
@@ -1,8 +1,8 @@
# DOOM 1993
-## Where is the settings page?
+## Where is the options page?
-The [player settings page](../player-settings) contains the options needed to configure your game session.
+The [player options page](../player-options) contains the options needed to configure your game session.
## What does randomization do to this game?
diff --git a/worlds/doom_1993/docs/setup_en.md b/worlds/doom_1993/docs/setup_en.md
index 5afe6d3e26e4..8906efac9cea 100644
--- a/worlds/doom_1993/docs/setup_en.md
+++ b/worlds/doom_1993/docs/setup_en.md
@@ -8,19 +8,22 @@
## Optional Software
- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases)
+- [PopTracker](https://github.com/black-sliver/PopTracker/)
+ - [OZone's APDoom tracker pack](https://github.com/Ozone31/doom-ap-tracker/releases)
## Installing AP Doom
1. Download [APDOOM.zip](https://github.com/Daivuk/apdoom/releases) and extract it.
-2. Copy DOOM.WAD from your steam install into the extracted folder.
+2. Copy `DOOM.WAD` from your game's installation directory into the newly extracted folder.
You can find the folder in steam by finding the game in your library,
- right clicking it and choosing *Manage→Browse Local Files*.
+ right-clicking it and choosing **Manage -> Browse Local Files**. The WAD file is in the `/base/` folder.
## Joining a MultiWorld Game
-1. Launch APDoomLauncher.exe
-2. Enter the Archipelago server address, slot name, and password (if you have one)
-3. Press "Launch DOOM"
-4. Enjoy!
+1. Launch apdoom-launcher.exe
+2. Select `Ultimate DOOM` from the drop-down
+3. Enter the Archipelago server address, slot name, and password (if you have one)
+4. Press "Launch DOOM"
+5. Enjoy!
To continue a game, follow the same connection steps.
Connecting with a different seed won't erase your progress in other seeds.
@@ -31,8 +34,23 @@ We recommend having Archipelago's Text Client open on the side to keep track of
APDOOM has in-game messages,
but they disappear quickly and there's no reasonable way to check your message history in-game.
+### Hinting
+
+To hint from in-game, use the chat (Default key: 'T'). Hinting from DOOM can be difficult because names are rather long and contain special characters. For example:
+```
+!hint Toxin Refinery (E1M3) - Computer area map
+```
+The game has a hint helper implemented, where you can simply type this:
+```
+!hint e1m3 map
+```
+For this to work, include the map short name (`E1M1`), followed by one of the keywords: `map`, `blue`, `yellow`, `red`.
+
## Auto-Tracking
APDOOM has a functional map tracker integrated into the level select screen.
It tells you which levels you have unlocked, which keys you have for each level, which levels have been completed,
and how many of the checks you have completed in each level.
+
+For better tracking, try OZone's poptracker package: https://github.com/Ozone31/doom-ap-tracker/releases .
+Requires [PopTracker](https://github.com/black-sliver/PopTracker/).
diff --git a/worlds/doom_ii/Items.py b/worlds/doom_ii/Items.py
new file mode 100644
index 000000000000..fc426cc883f2
--- /dev/null
+++ b/worlds/doom_ii/Items.py
@@ -0,0 +1,1071 @@
+# This file is auto generated. More info: https://github.com/Daivuk/apdoom
+
+from BaseClasses import ItemClassification
+from typing import TypedDict, Dict, Set
+
+
+class ItemDict(TypedDict, total=False):
+ classification: ItemClassification
+ count: int
+ name: str
+ doom_type: int # Unique numerical id used to spawn the item. -1 is level item, -2 is level complete item.
+ episode: int # Relevant if that item targets a specific level, like keycard or map reveal pickup.
+ map: int
+
+
+item_table: Dict[int, ItemDict] = {
+ 360000: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Shotgun',
+ 'doom_type': 2001,
+ 'episode': -1,
+ 'map': -1},
+ 360001: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Rocket launcher',
+ 'doom_type': 2003,
+ 'episode': -1,
+ 'map': -1},
+ 360002: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Plasma gun',
+ 'doom_type': 2004,
+ 'episode': -1,
+ 'map': -1},
+ 360003: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Chainsaw',
+ 'doom_type': 2005,
+ 'episode': -1,
+ 'map': -1},
+ 360004: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Chaingun',
+ 'doom_type': 2002,
+ 'episode': -1,
+ 'map': -1},
+ 360005: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'BFG9000',
+ 'doom_type': 2006,
+ 'episode': -1,
+ 'map': -1},
+ 360006: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Super Shotgun',
+ 'doom_type': 82,
+ 'episode': -1,
+ 'map': -1},
+ 360007: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Backpack',
+ 'doom_type': 8,
+ 'episode': -1,
+ 'map': -1},
+ 360008: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Armor',
+ 'doom_type': 2018,
+ 'episode': -1,
+ 'map': -1},
+ 360009: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Mega Armor',
+ 'doom_type': 2019,
+ 'episode': -1,
+ 'map': -1},
+ 360010: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Berserk',
+ 'doom_type': 2023,
+ 'episode': -1,
+ 'map': -1},
+ 360011: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Invulnerability',
+ 'doom_type': 2022,
+ 'episode': -1,
+ 'map': -1},
+ 360012: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Partial invisibility',
+ 'doom_type': 2024,
+ 'episode': -1,
+ 'map': -1},
+ 360013: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Supercharge',
+ 'doom_type': 2013,
+ 'episode': -1,
+ 'map': -1},
+ 360014: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Megasphere',
+ 'doom_type': 83,
+ 'episode': -1,
+ 'map': -1},
+ 360015: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Medikit',
+ 'doom_type': 2012,
+ 'episode': -1,
+ 'map': -1},
+ 360016: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Box of bullets',
+ 'doom_type': 2048,
+ 'episode': -1,
+ 'map': -1},
+ 360017: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Box of rockets',
+ 'doom_type': 2046,
+ 'episode': -1,
+ 'map': -1},
+ 360018: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Box of shotgun shells',
+ 'doom_type': 2049,
+ 'episode': -1,
+ 'map': -1},
+ 360019: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Energy cell pack',
+ 'doom_type': 17,
+ 'episode': -1,
+ 'map': -1},
+ 360200: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Underhalls (MAP02) - Red keycard',
+ 'doom_type': 13,
+ 'episode': 1,
+ 'map': 2},
+ 360201: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Underhalls (MAP02) - Blue keycard',
+ 'doom_type': 5,
+ 'episode': 1,
+ 'map': 2},
+ 360202: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Gantlet (MAP03) - Blue keycard',
+ 'doom_type': 5,
+ 'episode': 1,
+ 'map': 3},
+ 360203: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Gantlet (MAP03) - Red keycard',
+ 'doom_type': 13,
+ 'episode': 1,
+ 'map': 3},
+ 360204: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Focus (MAP04) - Blue keycard',
+ 'doom_type': 5,
+ 'episode': 1,
+ 'map': 4},
+ 360205: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Focus (MAP04) - Red keycard',
+ 'doom_type': 13,
+ 'episode': 1,
+ 'map': 4},
+ 360206: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Focus (MAP04) - Yellow keycard',
+ 'doom_type': 6,
+ 'episode': 1,
+ 'map': 4},
+ 360207: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Waste Tunnels (MAP05) - Blue keycard',
+ 'doom_type': 5,
+ 'episode': 1,
+ 'map': 5},
+ 360208: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Waste Tunnels (MAP05) - Red keycard',
+ 'doom_type': 13,
+ 'episode': 1,
+ 'map': 5},
+ 360209: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Waste Tunnels (MAP05) - Yellow keycard',
+ 'doom_type': 6,
+ 'episode': 1,
+ 'map': 5},
+ 360210: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Crusher (MAP06) - Red keycard',
+ 'doom_type': 13,
+ 'episode': 1,
+ 'map': 6},
+ 360211: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Crusher (MAP06) - Yellow keycard',
+ 'doom_type': 6,
+ 'episode': 1,
+ 'map': 6},
+ 360212: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Crusher (MAP06) - Blue keycard',
+ 'doom_type': 5,
+ 'episode': 1,
+ 'map': 6},
+ 360213: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Tricks and Traps (MAP08) - Yellow skull key',
+ 'doom_type': 39,
+ 'episode': 1,
+ 'map': 8},
+ 360214: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Tricks and Traps (MAP08) - Red skull key',
+ 'doom_type': 38,
+ 'episode': 1,
+ 'map': 8},
+ 360215: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Pit (MAP09) - Blue keycard',
+ 'doom_type': 5,
+ 'episode': 1,
+ 'map': 9},
+ 360216: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Pit (MAP09) - Yellow keycard',
+ 'doom_type': 6,
+ 'episode': 1,
+ 'map': 9},
+ 360217: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Refueling Base (MAP10) - Blue keycard',
+ 'doom_type': 5,
+ 'episode': 1,
+ 'map': 10},
+ 360218: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Refueling Base (MAP10) - Yellow keycard',
+ 'doom_type': 6,
+ 'episode': 1,
+ 'map': 10},
+ 360219: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Circle of Death (MAP11) - Red keycard',
+ 'doom_type': 13,
+ 'episode': 1,
+ 'map': 11},
+ 360220: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Circle of Death (MAP11) - Blue keycard',
+ 'doom_type': 5,
+ 'episode': 1,
+ 'map': 11},
+ 360221: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Factory (MAP12) - Blue keycard',
+ 'doom_type': 5,
+ 'episode': 2,
+ 'map': 1},
+ 360222: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Factory (MAP12) - Yellow keycard',
+ 'doom_type': 6,
+ 'episode': 2,
+ 'map': 1},
+ 360223: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Downtown (MAP13) - Blue keycard',
+ 'doom_type': 5,
+ 'episode': 2,
+ 'map': 2},
+ 360224: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Downtown (MAP13) - Yellow keycard',
+ 'doom_type': 6,
+ 'episode': 2,
+ 'map': 2},
+ 360225: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Downtown (MAP13) - Red keycard',
+ 'doom_type': 13,
+ 'episode': 2,
+ 'map': 2},
+ 360226: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Inmost Dens (MAP14) - Red skull key',
+ 'doom_type': 38,
+ 'episode': 2,
+ 'map': 3},
+ 360227: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Inmost Dens (MAP14) - Blue skull key',
+ 'doom_type': 40,
+ 'episode': 2,
+ 'map': 3},
+ 360228: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Industrial Zone (MAP15) - Yellow keycard',
+ 'doom_type': 6,
+ 'episode': 2,
+ 'map': 4},
+ 360229: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Industrial Zone (MAP15) - Red keycard',
+ 'doom_type': 13,
+ 'episode': 2,
+ 'map': 4},
+ 360230: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Industrial Zone (MAP15) - Blue keycard',
+ 'doom_type': 5,
+ 'episode': 2,
+ 'map': 4},
+ 360231: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Suburbs (MAP16) - Blue skull key',
+ 'doom_type': 40,
+ 'episode': 2,
+ 'map': 5},
+ 360232: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Suburbs (MAP16) - Red skull key',
+ 'doom_type': 38,
+ 'episode': 2,
+ 'map': 5},
+ 360233: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Tenements (MAP17) - Red keycard',
+ 'doom_type': 13,
+ 'episode': 2,
+ 'map': 6},
+ 360234: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Tenements (MAP17) - Blue keycard',
+ 'doom_type': 5,
+ 'episode': 2,
+ 'map': 6},
+ 360235: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Tenements (MAP17) - Yellow skull key',
+ 'doom_type': 39,
+ 'episode': 2,
+ 'map': 6},
+ 360236: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Courtyard (MAP18) - Yellow skull key',
+ 'doom_type': 39,
+ 'episode': 2,
+ 'map': 7},
+ 360237: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Courtyard (MAP18) - Blue skull key',
+ 'doom_type': 40,
+ 'episode': 2,
+ 'map': 7},
+ 360238: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Citadel (MAP19) - Blue skull key',
+ 'doom_type': 40,
+ 'episode': 2,
+ 'map': 8},
+ 360239: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Citadel (MAP19) - Red skull key',
+ 'doom_type': 38,
+ 'episode': 2,
+ 'map': 8},
+ 360240: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Citadel (MAP19) - Yellow skull key',
+ 'doom_type': 39,
+ 'episode': 2,
+ 'map': 8},
+ 360241: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Nirvana (MAP21) - Yellow skull key',
+ 'doom_type': 39,
+ 'episode': 3,
+ 'map': 1},
+ 360242: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Nirvana (MAP21) - Blue skull key',
+ 'doom_type': 40,
+ 'episode': 3,
+ 'map': 1},
+ 360243: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Nirvana (MAP21) - Red skull key',
+ 'doom_type': 38,
+ 'episode': 3,
+ 'map': 1},
+ 360244: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Catacombs (MAP22) - Blue skull key',
+ 'doom_type': 40,
+ 'episode': 3,
+ 'map': 2},
+ 360245: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Catacombs (MAP22) - Red skull key',
+ 'doom_type': 38,
+ 'episode': 3,
+ 'map': 2},
+ 360246: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Barrels o Fun (MAP23) - Yellow skull key',
+ 'doom_type': 39,
+ 'episode': 3,
+ 'map': 3},
+ 360247: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Chasm (MAP24) - Blue keycard',
+ 'doom_type': 5,
+ 'episode': 3,
+ 'map': 4},
+ 360248: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Chasm (MAP24) - Red keycard',
+ 'doom_type': 13,
+ 'episode': 3,
+ 'map': 4},
+ 360249: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Bloodfalls (MAP25) - Blue skull key',
+ 'doom_type': 40,
+ 'episode': 3,
+ 'map': 5},
+ 360250: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Abandoned Mines (MAP26) - Blue keycard',
+ 'doom_type': 5,
+ 'episode': 3,
+ 'map': 6},
+ 360251: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Abandoned Mines (MAP26) - Red keycard',
+ 'doom_type': 13,
+ 'episode': 3,
+ 'map': 6},
+ 360252: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Abandoned Mines (MAP26) - Yellow keycard',
+ 'doom_type': 6,
+ 'episode': 3,
+ 'map': 6},
+ 360253: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Monster Condo (MAP27) - Yellow skull key',
+ 'doom_type': 39,
+ 'episode': 3,
+ 'map': 7},
+ 360254: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Monster Condo (MAP27) - Red skull key',
+ 'doom_type': 38,
+ 'episode': 3,
+ 'map': 7},
+ 360255: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Monster Condo (MAP27) - Blue skull key',
+ 'doom_type': 40,
+ 'episode': 3,
+ 'map': 7},
+ 360256: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Spirit World (MAP28) - Yellow skull key',
+ 'doom_type': 39,
+ 'episode': 3,
+ 'map': 8},
+ 360257: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Spirit World (MAP28) - Red skull key',
+ 'doom_type': 38,
+ 'episode': 3,
+ 'map': 8},
+ 360400: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Entryway (MAP01)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 1},
+ 360401: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Entryway (MAP01) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 1},
+ 360402: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Entryway (MAP01) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 1,
+ 'map': 1},
+ 360403: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Underhalls (MAP02)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 2},
+ 360404: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Underhalls (MAP02) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 2},
+ 360405: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Underhalls (MAP02) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 1,
+ 'map': 2},
+ 360406: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Gantlet (MAP03)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 3},
+ 360407: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Gantlet (MAP03) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 3},
+ 360408: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Gantlet (MAP03) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 1,
+ 'map': 3},
+ 360409: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Focus (MAP04)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 4},
+ 360410: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Focus (MAP04) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 4},
+ 360411: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Focus (MAP04) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 1,
+ 'map': 4},
+ 360412: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Waste Tunnels (MAP05)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 5},
+ 360413: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Waste Tunnels (MAP05) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 5},
+ 360414: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Waste Tunnels (MAP05) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 1,
+ 'map': 5},
+ 360415: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Crusher (MAP06)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 6},
+ 360416: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Crusher (MAP06) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 6},
+ 360417: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Crusher (MAP06) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 1,
+ 'map': 6},
+ 360418: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Dead Simple (MAP07)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 7},
+ 360419: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Dead Simple (MAP07) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 7},
+ 360420: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Dead Simple (MAP07) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 1,
+ 'map': 7},
+ 360421: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Tricks and Traps (MAP08)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 8},
+ 360422: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Tricks and Traps (MAP08) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 8},
+ 360423: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Tricks and Traps (MAP08) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 1,
+ 'map': 8},
+ 360424: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Pit (MAP09)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 9},
+ 360425: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Pit (MAP09) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 9},
+ 360426: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Pit (MAP09) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 1,
+ 'map': 9},
+ 360427: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Refueling Base (MAP10)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 10},
+ 360428: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Refueling Base (MAP10) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 10},
+ 360429: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Refueling Base (MAP10) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 1,
+ 'map': 10},
+ 360430: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Circle of Death (MAP11)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 11},
+ 360431: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Circle of Death (MAP11) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 11},
+ 360432: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Circle of Death (MAP11) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 1,
+ 'map': 11},
+ 360433: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Factory (MAP12)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 1},
+ 360434: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Factory (MAP12) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 1},
+ 360435: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Factory (MAP12) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 2,
+ 'map': 1},
+ 360436: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Downtown (MAP13)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 2},
+ 360437: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Downtown (MAP13) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 2},
+ 360438: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Downtown (MAP13) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 2,
+ 'map': 2},
+ 360439: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Inmost Dens (MAP14)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 3},
+ 360440: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Inmost Dens (MAP14) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 3},
+ 360441: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Inmost Dens (MAP14) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 2,
+ 'map': 3},
+ 360442: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Industrial Zone (MAP15)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 4},
+ 360443: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Industrial Zone (MAP15) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 4},
+ 360444: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Industrial Zone (MAP15) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 2,
+ 'map': 4},
+ 360445: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Suburbs (MAP16)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 5},
+ 360446: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Suburbs (MAP16) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 5},
+ 360447: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Suburbs (MAP16) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 2,
+ 'map': 5},
+ 360448: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Tenements (MAP17)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 6},
+ 360449: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Tenements (MAP17) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 6},
+ 360450: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Tenements (MAP17) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 2,
+ 'map': 6},
+ 360451: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Courtyard (MAP18)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 7},
+ 360452: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Courtyard (MAP18) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 7},
+ 360453: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Courtyard (MAP18) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 2,
+ 'map': 7},
+ 360454: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Citadel (MAP19)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 8},
+ 360455: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Citadel (MAP19) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 8},
+ 360456: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Citadel (MAP19) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 2,
+ 'map': 8},
+ 360457: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Gotcha! (MAP20)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 9},
+ 360458: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Gotcha! (MAP20) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 9},
+ 360459: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Gotcha! (MAP20) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 2,
+ 'map': 9},
+ 360460: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Nirvana (MAP21)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 1},
+ 360461: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Nirvana (MAP21) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 1},
+ 360462: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Nirvana (MAP21) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 3,
+ 'map': 1},
+ 360463: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Catacombs (MAP22)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 2},
+ 360464: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Catacombs (MAP22) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 2},
+ 360465: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Catacombs (MAP22) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 3,
+ 'map': 2},
+ 360466: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Barrels o Fun (MAP23)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 3},
+ 360467: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Barrels o Fun (MAP23) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 3},
+ 360468: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Barrels o Fun (MAP23) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 3,
+ 'map': 3},
+ 360469: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Chasm (MAP24)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 4},
+ 360470: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Chasm (MAP24) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 4},
+ 360471: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Chasm (MAP24) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 3,
+ 'map': 4},
+ 360472: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Bloodfalls (MAP25)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 5},
+ 360473: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Bloodfalls (MAP25) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 5},
+ 360474: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Bloodfalls (MAP25) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 3,
+ 'map': 5},
+ 360475: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Abandoned Mines (MAP26)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 6},
+ 360476: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Abandoned Mines (MAP26) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 6},
+ 360477: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Abandoned Mines (MAP26) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 3,
+ 'map': 6},
+ 360478: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Monster Condo (MAP27)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 7},
+ 360479: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Monster Condo (MAP27) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 7},
+ 360480: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Monster Condo (MAP27) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 3,
+ 'map': 7},
+ 360481: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Spirit World (MAP28)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 8},
+ 360482: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Spirit World (MAP28) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 8},
+ 360483: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Spirit World (MAP28) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 3,
+ 'map': 8},
+ 360484: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Living End (MAP29)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 9},
+ 360485: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Living End (MAP29) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 9},
+ 360486: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Living End (MAP29) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 3,
+ 'map': 9},
+ 360487: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Icon of Sin (MAP30)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 10},
+ 360488: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Icon of Sin (MAP30) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 10},
+ 360489: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Icon of Sin (MAP30) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 3,
+ 'map': 10},
+ 360490: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Wolfenstein2 (MAP31)',
+ 'doom_type': -1,
+ 'episode': 4,
+ 'map': 1},
+ 360491: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Wolfenstein2 (MAP31) - Complete',
+ 'doom_type': -2,
+ 'episode': 4,
+ 'map': 1},
+ 360492: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Wolfenstein2 (MAP31) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 4,
+ 'map': 1},
+ 360493: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Grosse2 (MAP32)',
+ 'doom_type': -1,
+ 'episode': 4,
+ 'map': 2},
+ 360494: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Grosse2 (MAP32) - Complete',
+ 'doom_type': -2,
+ 'episode': 4,
+ 'map': 2},
+ 360495: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Grosse2 (MAP32) - Computer area map',
+ 'doom_type': 2026,
+ 'episode': 4,
+ 'map': 2},
+}
+
+
+item_name_groups: Dict[str, Set[str]] = {
+ 'Ammos': {'Box of bullets', 'Box of rockets', 'Box of shotgun shells', 'Energy cell pack', },
+ 'Computer area maps': {'Barrels o Fun (MAP23) - Computer area map', 'Bloodfalls (MAP25) - Computer area map', 'Circle of Death (MAP11) - Computer area map', 'Dead Simple (MAP07) - Computer area map', 'Downtown (MAP13) - Computer area map', 'Entryway (MAP01) - Computer area map', 'Gotcha! (MAP20) - Computer area map', 'Grosse2 (MAP32) - Computer area map', 'Icon of Sin (MAP30) - Computer area map', 'Industrial Zone (MAP15) - Computer area map', 'Monster Condo (MAP27) - Computer area map', 'Nirvana (MAP21) - Computer area map', 'Refueling Base (MAP10) - Computer area map', 'Suburbs (MAP16) - Computer area map', 'Tenements (MAP17) - Computer area map', 'The Abandoned Mines (MAP26) - Computer area map', 'The Catacombs (MAP22) - Computer area map', 'The Chasm (MAP24) - Computer area map', 'The Citadel (MAP19) - Computer area map', 'The Courtyard (MAP18) - Computer area map', 'The Crusher (MAP06) - Computer area map', 'The Factory (MAP12) - Computer area map', 'The Focus (MAP04) - Computer area map', 'The Gantlet (MAP03) - Computer area map', 'The Inmost Dens (MAP14) - Computer area map', 'The Living End (MAP29) - Computer area map', 'The Pit (MAP09) - Computer area map', 'The Spirit World (MAP28) - Computer area map', 'The Waste Tunnels (MAP05) - Computer area map', 'Tricks and Traps (MAP08) - Computer area map', 'Underhalls (MAP02) - Computer area map', 'Wolfenstein2 (MAP31) - Computer area map', },
+ 'Keys': {'Barrels o Fun (MAP23) - Yellow skull key', 'Bloodfalls (MAP25) - Blue skull key', 'Circle of Death (MAP11) - Blue keycard', 'Circle of Death (MAP11) - Red keycard', 'Downtown (MAP13) - Blue keycard', 'Downtown (MAP13) - Red keycard', 'Downtown (MAP13) - Yellow keycard', 'Industrial Zone (MAP15) - Blue keycard', 'Industrial Zone (MAP15) - Red keycard', 'Industrial Zone (MAP15) - Yellow keycard', 'Monster Condo (MAP27) - Blue skull key', 'Monster Condo (MAP27) - Red skull key', 'Monster Condo (MAP27) - Yellow skull key', 'Nirvana (MAP21) - Blue skull key', 'Nirvana (MAP21) - Red skull key', 'Nirvana (MAP21) - Yellow skull key', 'Refueling Base (MAP10) - Blue keycard', 'Refueling Base (MAP10) - Yellow keycard', 'Suburbs (MAP16) - Blue skull key', 'Suburbs (MAP16) - Red skull key', 'Tenements (MAP17) - Blue keycard', 'Tenements (MAP17) - Red keycard', 'Tenements (MAP17) - Yellow skull key', 'The Abandoned Mines (MAP26) - Blue keycard', 'The Abandoned Mines (MAP26) - Red keycard', 'The Abandoned Mines (MAP26) - Yellow keycard', 'The Catacombs (MAP22) - Blue skull key', 'The Catacombs (MAP22) - Red skull key', 'The Chasm (MAP24) - Blue keycard', 'The Chasm (MAP24) - Red keycard', 'The Citadel (MAP19) - Blue skull key', 'The Citadel (MAP19) - Red skull key', 'The Citadel (MAP19) - Yellow skull key', 'The Courtyard (MAP18) - Blue skull key', 'The Courtyard (MAP18) - Yellow skull key', 'The Crusher (MAP06) - Blue keycard', 'The Crusher (MAP06) - Red keycard', 'The Crusher (MAP06) - Yellow keycard', 'The Factory (MAP12) - Blue keycard', 'The Factory (MAP12) - Yellow keycard', 'The Focus (MAP04) - Blue keycard', 'The Focus (MAP04) - Red keycard', 'The Focus (MAP04) - Yellow keycard', 'The Gantlet (MAP03) - Blue keycard', 'The Gantlet (MAP03) - Red keycard', 'The Inmost Dens (MAP14) - Blue skull key', 'The Inmost Dens (MAP14) - Red skull key', 'The Pit (MAP09) - Blue keycard', 'The Pit (MAP09) - Yellow keycard', 'The Spirit World (MAP28) - Red skull key', 'The Spirit World (MAP28) - Yellow skull key', 'The Waste Tunnels (MAP05) - Blue keycard', 'The Waste Tunnels (MAP05) - Red keycard', 'The Waste Tunnels (MAP05) - Yellow keycard', 'Tricks and Traps (MAP08) - Red skull key', 'Tricks and Traps (MAP08) - Yellow skull key', 'Underhalls (MAP02) - Blue keycard', 'Underhalls (MAP02) - Red keycard', },
+ 'Levels': {'Barrels o Fun (MAP23)', 'Bloodfalls (MAP25)', 'Circle of Death (MAP11)', 'Dead Simple (MAP07)', 'Downtown (MAP13)', 'Entryway (MAP01)', 'Gotcha! (MAP20)', 'Grosse2 (MAP32)', 'Icon of Sin (MAP30)', 'Industrial Zone (MAP15)', 'Monster Condo (MAP27)', 'Nirvana (MAP21)', 'Refueling Base (MAP10)', 'Suburbs (MAP16)', 'Tenements (MAP17)', 'The Abandoned Mines (MAP26)', 'The Catacombs (MAP22)', 'The Chasm (MAP24)', 'The Citadel (MAP19)', 'The Courtyard (MAP18)', 'The Crusher (MAP06)', 'The Factory (MAP12)', 'The Focus (MAP04)', 'The Gantlet (MAP03)', 'The Inmost Dens (MAP14)', 'The Living End (MAP29)', 'The Pit (MAP09)', 'The Spirit World (MAP28)', 'The Waste Tunnels (MAP05)', 'Tricks and Traps (MAP08)', 'Underhalls (MAP02)', 'Wolfenstein2 (MAP31)', },
+ 'Powerups': {'Armor', 'Berserk', 'Invulnerability', 'Mega Armor', 'Megasphere', 'Partial invisibility', 'Supercharge', },
+ 'Weapons': {'BFG9000', 'Chaingun', 'Chainsaw', 'Plasma gun', 'Rocket launcher', 'Shotgun', 'Super Shotgun', },
+}
diff --git a/worlds/doom_ii/Locations.py b/worlds/doom_ii/Locations.py
new file mode 100644
index 000000000000..3ce87b8a6662
--- /dev/null
+++ b/worlds/doom_ii/Locations.py
@@ -0,0 +1,3442 @@
+# This file is auto generated. More info: https://github.com/Daivuk/apdoom
+
+from typing import Dict, TypedDict, List, Set
+
+
+class LocationDict(TypedDict, total=False):
+ name: str
+ episode: int
+ map: int
+ index: int # Thing index as it is stored in the wad file.
+ doom_type: int # In case index end up unreliable, we can use doom type. Maps have often only one of each important things.
+ region: str
+
+
+location_table: Dict[int, LocationDict] = {
+ 361000: {'name': 'Entryway (MAP01) - Armor',
+ 'episode': 1,
+ 'map': 1,
+ 'index': 17,
+ 'doom_type': 2018,
+ 'region': "Entryway (MAP01) Main"},
+ 361001: {'name': 'Entryway (MAP01) - Shotgun',
+ 'episode': 1,
+ 'map': 1,
+ 'index': 37,
+ 'doom_type': 2001,
+ 'region': "Entryway (MAP01) Main"},
+ 361002: {'name': 'Entryway (MAP01) - Rocket launcher',
+ 'episode': 1,
+ 'map': 1,
+ 'index': 52,
+ 'doom_type': 2003,
+ 'region': "Entryway (MAP01) Main"},
+ 361003: {'name': 'Entryway (MAP01) - Chainsaw',
+ 'episode': 1,
+ 'map': 1,
+ 'index': 68,
+ 'doom_type': 2005,
+ 'region': "Entryway (MAP01) Main"},
+ 361004: {'name': 'Entryway (MAP01) - Exit',
+ 'episode': 1,
+ 'map': 1,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Entryway (MAP01) Main"},
+ 361005: {'name': 'Underhalls (MAP02) - Red keycard',
+ 'episode': 1,
+ 'map': 2,
+ 'index': 31,
+ 'doom_type': 13,
+ 'region': "Underhalls (MAP02) Main"},
+ 361006: {'name': 'Underhalls (MAP02) - Blue keycard',
+ 'episode': 1,
+ 'map': 2,
+ 'index': 44,
+ 'doom_type': 5,
+ 'region': "Underhalls (MAP02) Red"},
+ 361007: {'name': 'Underhalls (MAP02) - Mega Armor',
+ 'episode': 1,
+ 'map': 2,
+ 'index': 116,
+ 'doom_type': 2019,
+ 'region': "Underhalls (MAP02) Main"},
+ 361008: {'name': 'Underhalls (MAP02) - Super Shotgun',
+ 'episode': 1,
+ 'map': 2,
+ 'index': 127,
+ 'doom_type': 82,
+ 'region': "Underhalls (MAP02) Main"},
+ 361009: {'name': 'Underhalls (MAP02) - Exit',
+ 'episode': 1,
+ 'map': 2,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Underhalls (MAP02) Blue"},
+ 361010: {'name': 'The Gantlet (MAP03) - Mega Armor',
+ 'episode': 1,
+ 'map': 3,
+ 'index': 5,
+ 'doom_type': 2019,
+ 'region': "The Gantlet (MAP03) Main"},
+ 361011: {'name': 'The Gantlet (MAP03) - Shotgun',
+ 'episode': 1,
+ 'map': 3,
+ 'index': 6,
+ 'doom_type': 2001,
+ 'region': "The Gantlet (MAP03) Main"},
+ 361012: {'name': 'The Gantlet (MAP03) - Blue keycard',
+ 'episode': 1,
+ 'map': 3,
+ 'index': 85,
+ 'doom_type': 5,
+ 'region': "The Gantlet (MAP03) Main"},
+ 361013: {'name': 'The Gantlet (MAP03) - Rocket launcher',
+ 'episode': 1,
+ 'map': 3,
+ 'index': 86,
+ 'doom_type': 2003,
+ 'region': "The Gantlet (MAP03) Main"},
+ 361014: {'name': 'The Gantlet (MAP03) - Partial invisibility',
+ 'episode': 1,
+ 'map': 3,
+ 'index': 96,
+ 'doom_type': 2024,
+ 'region': "The Gantlet (MAP03) Main"},
+ 361015: {'name': 'The Gantlet (MAP03) - Supercharge',
+ 'episode': 1,
+ 'map': 3,
+ 'index': 97,
+ 'doom_type': 2013,
+ 'region': "The Gantlet (MAP03) Main"},
+ 361016: {'name': 'The Gantlet (MAP03) - Mega Armor 2',
+ 'episode': 1,
+ 'map': 3,
+ 'index': 98,
+ 'doom_type': 2019,
+ 'region': "The Gantlet (MAP03) Main"},
+ 361017: {'name': 'The Gantlet (MAP03) - Red keycard',
+ 'episode': 1,
+ 'map': 3,
+ 'index': 104,
+ 'doom_type': 13,
+ 'region': "The Gantlet (MAP03) Blue"},
+ 361018: {'name': 'The Gantlet (MAP03) - Chaingun',
+ 'episode': 1,
+ 'map': 3,
+ 'index': 122,
+ 'doom_type': 2002,
+ 'region': "The Gantlet (MAP03) Main"},
+ 361019: {'name': 'The Gantlet (MAP03) - Backpack',
+ 'episode': 1,
+ 'map': 3,
+ 'index': 146,
+ 'doom_type': 8,
+ 'region': "The Gantlet (MAP03) Blue"},
+ 361020: {'name': 'The Gantlet (MAP03) - Exit',
+ 'episode': 1,
+ 'map': 3,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Gantlet (MAP03) Red"},
+ 361021: {'name': 'The Focus (MAP04) - Super Shotgun',
+ 'episode': 1,
+ 'map': 4,
+ 'index': 4,
+ 'doom_type': 82,
+ 'region': "The Focus (MAP04) Main"},
+ 361022: {'name': 'The Focus (MAP04) - Blue keycard',
+ 'episode': 1,
+ 'map': 4,
+ 'index': 21,
+ 'doom_type': 5,
+ 'region': "The Focus (MAP04) Main"},
+ 361023: {'name': 'The Focus (MAP04) - Red keycard',
+ 'episode': 1,
+ 'map': 4,
+ 'index': 32,
+ 'doom_type': 13,
+ 'region': "The Focus (MAP04) Blue"},
+ 361024: {'name': 'The Focus (MAP04) - Yellow keycard',
+ 'episode': 1,
+ 'map': 4,
+ 'index': 59,
+ 'doom_type': 6,
+ 'region': "The Focus (MAP04) Red"},
+ 361025: {'name': 'The Focus (MAP04) - Exit',
+ 'episode': 1,
+ 'map': 4,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Focus (MAP04) Yellow"},
+ 361026: {'name': 'The Waste Tunnels (MAP05) - Rocket launcher',
+ 'episode': 1,
+ 'map': 5,
+ 'index': 45,
+ 'doom_type': 2003,
+ 'region': "The Waste Tunnels (MAP05) Main"},
+ 361027: {'name': 'The Waste Tunnels (MAP05) - Super Shotgun',
+ 'episode': 1,
+ 'map': 5,
+ 'index': 46,
+ 'doom_type': 82,
+ 'region': "The Waste Tunnels (MAP05) Main"},
+ 361028: {'name': 'The Waste Tunnels (MAP05) - Blue keycard',
+ 'episode': 1,
+ 'map': 5,
+ 'index': 50,
+ 'doom_type': 5,
+ 'region': "The Waste Tunnels (MAP05) Red"},
+ 361029: {'name': 'The Waste Tunnels (MAP05) - Plasma gun',
+ 'episode': 1,
+ 'map': 5,
+ 'index': 53,
+ 'doom_type': 2004,
+ 'region': "The Waste Tunnels (MAP05) Main"},
+ 361030: {'name': 'The Waste Tunnels (MAP05) - Red keycard',
+ 'episode': 1,
+ 'map': 5,
+ 'index': 55,
+ 'doom_type': 13,
+ 'region': "The Waste Tunnels (MAP05) Main"},
+ 361031: {'name': 'The Waste Tunnels (MAP05) - Supercharge',
+ 'episode': 1,
+ 'map': 5,
+ 'index': 56,
+ 'doom_type': 2013,
+ 'region': "The Waste Tunnels (MAP05) Main"},
+ 361032: {'name': 'The Waste Tunnels (MAP05) - Mega Armor',
+ 'episode': 1,
+ 'map': 5,
+ 'index': 57,
+ 'doom_type': 2019,
+ 'region': "The Waste Tunnels (MAP05) Main"},
+ 361033: {'name': 'The Waste Tunnels (MAP05) - Yellow keycard',
+ 'episode': 1,
+ 'map': 5,
+ 'index': 78,
+ 'doom_type': 6,
+ 'region': "The Waste Tunnels (MAP05) Blue"},
+ 361034: {'name': 'The Waste Tunnels (MAP05) - Armor',
+ 'episode': 1,
+ 'map': 5,
+ 'index': 151,
+ 'doom_type': 2018,
+ 'region': "The Waste Tunnels (MAP05) Main"},
+ 361035: {'name': 'The Waste Tunnels (MAP05) - Supercharge 2',
+ 'episode': 1,
+ 'map': 5,
+ 'index': 170,
+ 'doom_type': 2013,
+ 'region': "The Waste Tunnels (MAP05) Main"},
+ 361036: {'name': 'The Waste Tunnels (MAP05) - Shotgun',
+ 'episode': 1,
+ 'map': 5,
+ 'index': 202,
+ 'doom_type': 2001,
+ 'region': "The Waste Tunnels (MAP05) Main"},
+ 361037: {'name': 'The Waste Tunnels (MAP05) - Berserk',
+ 'episode': 1,
+ 'map': 5,
+ 'index': 215,
+ 'doom_type': 2023,
+ 'region': "The Waste Tunnels (MAP05) Main"},
+ 361038: {'name': 'The Waste Tunnels (MAP05) - Exit',
+ 'episode': 1,
+ 'map': 5,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Waste Tunnels (MAP05) Yellow"},
+ 361039: {'name': 'The Crusher (MAP06) - Red keycard',
+ 'episode': 1,
+ 'map': 6,
+ 'index': 0,
+ 'doom_type': 13,
+ 'region': "The Crusher (MAP06) Blue"},
+ 361040: {'name': 'The Crusher (MAP06) - Yellow keycard',
+ 'episode': 1,
+ 'map': 6,
+ 'index': 1,
+ 'doom_type': 6,
+ 'region': "The Crusher (MAP06) Red"},
+ 361041: {'name': 'The Crusher (MAP06) - Blue keycard',
+ 'episode': 1,
+ 'map': 6,
+ 'index': 36,
+ 'doom_type': 5,
+ 'region': "The Crusher (MAP06) Main"},
+ 361042: {'name': 'The Crusher (MAP06) - Supercharge',
+ 'episode': 1,
+ 'map': 6,
+ 'index': 55,
+ 'doom_type': 2013,
+ 'region': "The Crusher (MAP06) Main"},
+ 361043: {'name': 'The Crusher (MAP06) - Plasma gun',
+ 'episode': 1,
+ 'map': 6,
+ 'index': 59,
+ 'doom_type': 2004,
+ 'region': "The Crusher (MAP06) Main"},
+ 361044: {'name': 'The Crusher (MAP06) - Blue keycard 2',
+ 'episode': 1,
+ 'map': 6,
+ 'index': 74,
+ 'doom_type': 5,
+ 'region': "The Crusher (MAP06) Main"},
+ 361045: {'name': 'The Crusher (MAP06) - Blue keycard 3',
+ 'episode': 1,
+ 'map': 6,
+ 'index': 75,
+ 'doom_type': 5,
+ 'region': "The Crusher (MAP06) Main"},
+ 361046: {'name': 'The Crusher (MAP06) - Megasphere',
+ 'episode': 1,
+ 'map': 6,
+ 'index': 94,
+ 'doom_type': 83,
+ 'region': "The Crusher (MAP06) Main"},
+ 361047: {'name': 'The Crusher (MAP06) - Armor',
+ 'episode': 1,
+ 'map': 6,
+ 'index': 130,
+ 'doom_type': 2018,
+ 'region': "The Crusher (MAP06) Main"},
+ 361048: {'name': 'The Crusher (MAP06) - Super Shotgun',
+ 'episode': 1,
+ 'map': 6,
+ 'index': 134,
+ 'doom_type': 82,
+ 'region': "The Crusher (MAP06) Blue"},
+ 361049: {'name': 'The Crusher (MAP06) - Mega Armor',
+ 'episode': 1,
+ 'map': 6,
+ 'index': 222,
+ 'doom_type': 2019,
+ 'region': "The Crusher (MAP06) Blue"},
+ 361050: {'name': 'The Crusher (MAP06) - Rocket launcher',
+ 'episode': 1,
+ 'map': 6,
+ 'index': 223,
+ 'doom_type': 2003,
+ 'region': "The Crusher (MAP06) Blue"},
+ 361051: {'name': 'The Crusher (MAP06) - Backpack',
+ 'episode': 1,
+ 'map': 6,
+ 'index': 225,
+ 'doom_type': 8,
+ 'region': "The Crusher (MAP06) Blue"},
+ 361052: {'name': 'The Crusher (MAP06) - Megasphere 2',
+ 'episode': 1,
+ 'map': 6,
+ 'index': 246,
+ 'doom_type': 83,
+ 'region': "The Crusher (MAP06) Blue"},
+ 361053: {'name': 'The Crusher (MAP06) - Exit',
+ 'episode': 1,
+ 'map': 6,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Crusher (MAP06) Yellow"},
+ 361054: {'name': 'Dead Simple (MAP07) - Megasphere',
+ 'episode': 1,
+ 'map': 7,
+ 'index': 4,
+ 'doom_type': 83,
+ 'region': "Dead Simple (MAP07) Main"},
+ 361055: {'name': 'Dead Simple (MAP07) - Rocket launcher',
+ 'episode': 1,
+ 'map': 7,
+ 'index': 5,
+ 'doom_type': 2003,
+ 'region': "Dead Simple (MAP07) Main"},
+ 361056: {'name': 'Dead Simple (MAP07) - Partial invisibility',
+ 'episode': 1,
+ 'map': 7,
+ 'index': 7,
+ 'doom_type': 2024,
+ 'region': "Dead Simple (MAP07) Main"},
+ 361057: {'name': 'Dead Simple (MAP07) - Super Shotgun',
+ 'episode': 1,
+ 'map': 7,
+ 'index': 8,
+ 'doom_type': 82,
+ 'region': "Dead Simple (MAP07) Main"},
+ 361058: {'name': 'Dead Simple (MAP07) - Chaingun',
+ 'episode': 1,
+ 'map': 7,
+ 'index': 9,
+ 'doom_type': 2002,
+ 'region': "Dead Simple (MAP07) Main"},
+ 361059: {'name': 'Dead Simple (MAP07) - Plasma gun',
+ 'episode': 1,
+ 'map': 7,
+ 'index': 10,
+ 'doom_type': 2004,
+ 'region': "Dead Simple (MAP07) Main"},
+ 361060: {'name': 'Dead Simple (MAP07) - Backpack',
+ 'episode': 1,
+ 'map': 7,
+ 'index': 43,
+ 'doom_type': 8,
+ 'region': "Dead Simple (MAP07) Main"},
+ 361061: {'name': 'Dead Simple (MAP07) - Berserk',
+ 'episode': 1,
+ 'map': 7,
+ 'index': 44,
+ 'doom_type': 2023,
+ 'region': "Dead Simple (MAP07) Main"},
+ 361062: {'name': 'Dead Simple (MAP07) - Partial invisibility 2',
+ 'episode': 1,
+ 'map': 7,
+ 'index': 60,
+ 'doom_type': 2024,
+ 'region': "Dead Simple (MAP07) Main"},
+ 361063: {'name': 'Dead Simple (MAP07) - Partial invisibility 3',
+ 'episode': 1,
+ 'map': 7,
+ 'index': 73,
+ 'doom_type': 2024,
+ 'region': "Dead Simple (MAP07) Main"},
+ 361064: {'name': 'Dead Simple (MAP07) - Partial invisibility 4',
+ 'episode': 1,
+ 'map': 7,
+ 'index': 74,
+ 'doom_type': 2024,
+ 'region': "Dead Simple (MAP07) Main"},
+ 361065: {'name': 'Dead Simple (MAP07) - Exit',
+ 'episode': 1,
+ 'map': 7,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Dead Simple (MAP07) Main"},
+ 361066: {'name': 'Tricks and Traps (MAP08) - Plasma gun',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 14,
+ 'doom_type': 2004,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361067: {'name': 'Tricks and Traps (MAP08) - Rocket launcher',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 17,
+ 'doom_type': 2003,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361068: {'name': 'Tricks and Traps (MAP08) - Armor',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 36,
+ 'doom_type': 2018,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361069: {'name': 'Tricks and Traps (MAP08) - Chaingun',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 48,
+ 'doom_type': 2002,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361070: {'name': 'Tricks and Traps (MAP08) - Shotgun',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 87,
+ 'doom_type': 2001,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361071: {'name': 'Tricks and Traps (MAP08) - Supercharge',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 119,
+ 'doom_type': 2013,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361072: {'name': 'Tricks and Traps (MAP08) - Invulnerability',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 120,
+ 'doom_type': 2022,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361073: {'name': 'Tricks and Traps (MAP08) - Invulnerability 2',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 122,
+ 'doom_type': 2022,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361074: {'name': 'Tricks and Traps (MAP08) - Yellow skull key',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 123,
+ 'doom_type': 39,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361075: {'name': 'Tricks and Traps (MAP08) - Backpack',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 133,
+ 'doom_type': 8,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361076: {'name': 'Tricks and Traps (MAP08) - Backpack 2',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 134,
+ 'doom_type': 8,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361077: {'name': 'Tricks and Traps (MAP08) - Invulnerability 3',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 135,
+ 'doom_type': 2022,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361078: {'name': 'Tricks and Traps (MAP08) - Invulnerability 4',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 136,
+ 'doom_type': 2022,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361079: {'name': 'Tricks and Traps (MAP08) - BFG9000',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 161,
+ 'doom_type': 2006,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361080: {'name': 'Tricks and Traps (MAP08) - Supercharge 2',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 162,
+ 'doom_type': 2013,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361081: {'name': 'Tricks and Traps (MAP08) - Backpack 3',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 163,
+ 'doom_type': 8,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361082: {'name': 'Tricks and Traps (MAP08) - Backpack 4',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 164,
+ 'doom_type': 8,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361083: {'name': 'Tricks and Traps (MAP08) - Chainsaw',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 168,
+ 'doom_type': 2005,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361084: {'name': 'Tricks and Traps (MAP08) - Red skull key',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 176,
+ 'doom_type': 38,
+ 'region': "Tricks and Traps (MAP08) Yellow"},
+ 361085: {'name': 'Tricks and Traps (MAP08) - Invulnerability 5',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 202,
+ 'doom_type': 2022,
+ 'region': "Tricks and Traps (MAP08) Yellow"},
+ 361086: {'name': 'Tricks and Traps (MAP08) - Armor 2',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 220,
+ 'doom_type': 2018,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361087: {'name': 'Tricks and Traps (MAP08) - Backpack 5',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 226,
+ 'doom_type': 8,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361088: {'name': 'Tricks and Traps (MAP08) - Partial invisibility',
+ 'episode': 1,
+ 'map': 8,
+ 'index': 235,
+ 'doom_type': 2024,
+ 'region': "Tricks and Traps (MAP08) Main"},
+ 361089: {'name': 'Tricks and Traps (MAP08) - Exit',
+ 'episode': 1,
+ 'map': 8,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Tricks and Traps (MAP08) Red"},
+ 361090: {'name': 'The Pit (MAP09) - Berserk',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 5,
+ 'doom_type': 2023,
+ 'region': "The Pit (MAP09) Main"},
+ 361091: {'name': 'The Pit (MAP09) - Shotgun',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 21,
+ 'doom_type': 2001,
+ 'region': "The Pit (MAP09) Main"},
+ 361092: {'name': 'The Pit (MAP09) - Mega Armor',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 26,
+ 'doom_type': 2019,
+ 'region': "The Pit (MAP09) Main"},
+ 361093: {'name': 'The Pit (MAP09) - Supercharge',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 78,
+ 'doom_type': 2013,
+ 'region': "The Pit (MAP09) Main"},
+ 361094: {'name': 'The Pit (MAP09) - Berserk 2',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 90,
+ 'doom_type': 2023,
+ 'region': "The Pit (MAP09) Main"},
+ 361095: {'name': 'The Pit (MAP09) - Rocket launcher',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 92,
+ 'doom_type': 2003,
+ 'region': "The Pit (MAP09) Main"},
+ 361096: {'name': 'The Pit (MAP09) - BFG9000',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 184,
+ 'doom_type': 2006,
+ 'region': "The Pit (MAP09) Main"},
+ 361097: {'name': 'The Pit (MAP09) - Blue keycard',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 185,
+ 'doom_type': 5,
+ 'region': "The Pit (MAP09) Main"},
+ 361098: {'name': 'The Pit (MAP09) - Yellow keycard',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 226,
+ 'doom_type': 6,
+ 'region': "The Pit (MAP09) Blue"},
+ 361099: {'name': 'The Pit (MAP09) - Backpack',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 244,
+ 'doom_type': 8,
+ 'region': "The Pit (MAP09) Blue"},
+ 361100: {'name': 'The Pit (MAP09) - Computer area map',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 245,
+ 'doom_type': 2026,
+ 'region': "The Pit (MAP09) Blue"},
+ 361101: {'name': 'The Pit (MAP09) - Supercharge 2',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 250,
+ 'doom_type': 2013,
+ 'region': "The Pit (MAP09) Blue"},
+ 361102: {'name': 'The Pit (MAP09) - Mega Armor 2',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 251,
+ 'doom_type': 2019,
+ 'region': "The Pit (MAP09) Blue"},
+ 361103: {'name': 'The Pit (MAP09) - Berserk 3',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 309,
+ 'doom_type': 2023,
+ 'region': "The Pit (MAP09) Blue"},
+ 361104: {'name': 'The Pit (MAP09) - Armor',
+ 'episode': 1,
+ 'map': 9,
+ 'index': 348,
+ 'doom_type': 2018,
+ 'region': "The Pit (MAP09) Main"},
+ 361105: {'name': 'The Pit (MAP09) - Exit',
+ 'episode': 1,
+ 'map': 9,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Pit (MAP09) Yellow"},
+ 361106: {'name': 'Refueling Base (MAP10) - BFG9000',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 17,
+ 'doom_type': 2006,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361107: {'name': 'Refueling Base (MAP10) - Supercharge',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 28,
+ 'doom_type': 2013,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361108: {'name': 'Refueling Base (MAP10) - Plasma gun',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 29,
+ 'doom_type': 2004,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361109: {'name': 'Refueling Base (MAP10) - Blue keycard',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 50,
+ 'doom_type': 5,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361110: {'name': 'Refueling Base (MAP10) - Shotgun',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 99,
+ 'doom_type': 2001,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361111: {'name': 'Refueling Base (MAP10) - Chaingun',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 158,
+ 'doom_type': 2002,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361112: {'name': 'Refueling Base (MAP10) - Armor',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 172,
+ 'doom_type': 2018,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361113: {'name': 'Refueling Base (MAP10) - Rocket launcher',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 291,
+ 'doom_type': 2003,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361114: {'name': 'Refueling Base (MAP10) - Supercharge 2',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 359,
+ 'doom_type': 2013,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361115: {'name': 'Refueling Base (MAP10) - Backpack',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 368,
+ 'doom_type': 8,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361116: {'name': 'Refueling Base (MAP10) - Berserk',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 392,
+ 'doom_type': 2023,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361117: {'name': 'Refueling Base (MAP10) - Mega Armor',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 395,
+ 'doom_type': 2019,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361118: {'name': 'Refueling Base (MAP10) - Invulnerability',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 396,
+ 'doom_type': 2022,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361119: {'name': 'Refueling Base (MAP10) - Invulnerability 2',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 398,
+ 'doom_type': 2022,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361120: {'name': 'Refueling Base (MAP10) - Armor 2',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 400,
+ 'doom_type': 2018,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361121: {'name': 'Refueling Base (MAP10) - Berserk 2',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 441,
+ 'doom_type': 2023,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361122: {'name': 'Refueling Base (MAP10) - Partial invisibility',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 470,
+ 'doom_type': 2024,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361123: {'name': 'Refueling Base (MAP10) - Chainsaw',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 472,
+ 'doom_type': 2005,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361124: {'name': 'Refueling Base (MAP10) - Yellow keycard',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 473,
+ 'doom_type': 6,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361125: {'name': 'Refueling Base (MAP10) - Megasphere',
+ 'episode': 1,
+ 'map': 10,
+ 'index': 507,
+ 'doom_type': 83,
+ 'region': "Refueling Base (MAP10) Main"},
+ 361126: {'name': 'Refueling Base (MAP10) - Exit',
+ 'episode': 1,
+ 'map': 10,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Refueling Base (MAP10) Yellow Blue"},
+ 361127: {'name': 'Circle of Death (MAP11) - Red keycard',
+ 'episode': 1,
+ 'map': 11,
+ 'index': 1,
+ 'doom_type': 13,
+ 'region': "Circle of Death (MAP11) Main"},
+ 361128: {'name': 'Circle of Death (MAP11) - Chaingun',
+ 'episode': 1,
+ 'map': 11,
+ 'index': 14,
+ 'doom_type': 2002,
+ 'region': "Circle of Death (MAP11) Main"},
+ 361129: {'name': 'Circle of Death (MAP11) - Supercharge',
+ 'episode': 1,
+ 'map': 11,
+ 'index': 23,
+ 'doom_type': 2013,
+ 'region': "Circle of Death (MAP11) Main"},
+ 361130: {'name': 'Circle of Death (MAP11) - Plasma gun',
+ 'episode': 1,
+ 'map': 11,
+ 'index': 30,
+ 'doom_type': 2004,
+ 'region': "Circle of Death (MAP11) Main"},
+ 361131: {'name': 'Circle of Death (MAP11) - Blue keycard',
+ 'episode': 1,
+ 'map': 11,
+ 'index': 40,
+ 'doom_type': 5,
+ 'region': "Circle of Death (MAP11) Main"},
+ 361132: {'name': 'Circle of Death (MAP11) - Armor',
+ 'episode': 1,
+ 'map': 11,
+ 'index': 42,
+ 'doom_type': 2018,
+ 'region': "Circle of Death (MAP11) Main"},
+ 361133: {'name': 'Circle of Death (MAP11) - Shotgun',
+ 'episode': 1,
+ 'map': 11,
+ 'index': 50,
+ 'doom_type': 2001,
+ 'region': "Circle of Death (MAP11) Main"},
+ 361134: {'name': 'Circle of Death (MAP11) - Mega Armor',
+ 'episode': 1,
+ 'map': 11,
+ 'index': 58,
+ 'doom_type': 2019,
+ 'region': "Circle of Death (MAP11) Blue"},
+ 361135: {'name': 'Circle of Death (MAP11) - Partial invisibility',
+ 'episode': 1,
+ 'map': 11,
+ 'index': 70,
+ 'doom_type': 2024,
+ 'region': "Circle of Death (MAP11) Main"},
+ 361136: {'name': 'Circle of Death (MAP11) - Invulnerability',
+ 'episode': 1,
+ 'map': 11,
+ 'index': 83,
+ 'doom_type': 2022,
+ 'region': "Circle of Death (MAP11) Red"},
+ 361137: {'name': 'Circle of Death (MAP11) - Rocket launcher',
+ 'episode': 1,
+ 'map': 11,
+ 'index': 86,
+ 'doom_type': 2003,
+ 'region': "Circle of Death (MAP11) Red"},
+ 361138: {'name': 'Circle of Death (MAP11) - Backpack',
+ 'episode': 1,
+ 'map': 11,
+ 'index': 88,
+ 'doom_type': 8,
+ 'region': "Circle of Death (MAP11) Red"},
+ 361139: {'name': 'Circle of Death (MAP11) - Supercharge 2',
+ 'episode': 1,
+ 'map': 11,
+ 'index': 108,
+ 'doom_type': 2013,
+ 'region': "Circle of Death (MAP11) Red"},
+ 361140: {'name': 'Circle of Death (MAP11) - BFG9000',
+ 'episode': 1,
+ 'map': 11,
+ 'index': 110,
+ 'doom_type': 2006,
+ 'region': "Circle of Death (MAP11) Red"},
+ 361141: {'name': 'Circle of Death (MAP11) - Exit',
+ 'episode': 1,
+ 'map': 11,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Circle of Death (MAP11) Red"},
+ 361142: {'name': 'The Factory (MAP12) - Shotgun',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 14,
+ 'doom_type': 2001,
+ 'region': "The Factory (MAP12) Main"},
+ 361143: {'name': 'The Factory (MAP12) - Berserk',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 35,
+ 'doom_type': 2023,
+ 'region': "The Factory (MAP12) Main"},
+ 361144: {'name': 'The Factory (MAP12) - Chaingun',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 38,
+ 'doom_type': 2002,
+ 'region': "The Factory (MAP12) Main"},
+ 361145: {'name': 'The Factory (MAP12) - Supercharge',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 52,
+ 'doom_type': 2013,
+ 'region': "The Factory (MAP12) Main"},
+ 361146: {'name': 'The Factory (MAP12) - Blue keycard',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 54,
+ 'doom_type': 5,
+ 'region': "The Factory (MAP12) Main"},
+ 361147: {'name': 'The Factory (MAP12) - Armor',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 63,
+ 'doom_type': 2018,
+ 'region': "The Factory (MAP12) Blue"},
+ 361148: {'name': 'The Factory (MAP12) - Backpack',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 70,
+ 'doom_type': 8,
+ 'region': "The Factory (MAP12) Blue"},
+ 361149: {'name': 'The Factory (MAP12) - Supercharge 2',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 83,
+ 'doom_type': 2013,
+ 'region': "The Factory (MAP12) Main"},
+ 361150: {'name': 'The Factory (MAP12) - Armor 2',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 92,
+ 'doom_type': 2018,
+ 'region': "The Factory (MAP12) Main"},
+ 361151: {'name': 'The Factory (MAP12) - Partial invisibility',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 93,
+ 'doom_type': 2024,
+ 'region': "The Factory (MAP12) Main"},
+ 361152: {'name': 'The Factory (MAP12) - Berserk 2',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 107,
+ 'doom_type': 2023,
+ 'region': "The Factory (MAP12) Main"},
+ 361153: {'name': 'The Factory (MAP12) - Yellow keycard',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 123,
+ 'doom_type': 6,
+ 'region': "The Factory (MAP12) Main"},
+ 361154: {'name': 'The Factory (MAP12) - BFG9000',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 135,
+ 'doom_type': 2006,
+ 'region': "The Factory (MAP12) Blue"},
+ 361155: {'name': 'The Factory (MAP12) - Berserk 3',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 189,
+ 'doom_type': 2023,
+ 'region': "The Factory (MAP12) Main"},
+ 361156: {'name': 'The Factory (MAP12) - Super Shotgun',
+ 'episode': 2,
+ 'map': 1,
+ 'index': 192,
+ 'doom_type': 82,
+ 'region': "The Factory (MAP12) Main"},
+ 361157: {'name': 'The Factory (MAP12) - Exit',
+ 'episode': 2,
+ 'map': 1,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Factory (MAP12) Yellow"},
+ 361158: {'name': 'Downtown (MAP13) - Rocket launcher',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 4,
+ 'doom_type': 2003,
+ 'region': "Downtown (MAP13) Main"},
+ 361159: {'name': 'Downtown (MAP13) - Shotgun',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 42,
+ 'doom_type': 2001,
+ 'region': "Downtown (MAP13) Main"},
+ 361160: {'name': 'Downtown (MAP13) - Supercharge',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 73,
+ 'doom_type': 2013,
+ 'region': "Downtown (MAP13) Main"},
+ 361161: {'name': 'Downtown (MAP13) - Berserk',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 131,
+ 'doom_type': 2023,
+ 'region': "Downtown (MAP13) Main"},
+ 361162: {'name': 'Downtown (MAP13) - Mega Armor',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 158,
+ 'doom_type': 2019,
+ 'region': "Downtown (MAP13) Main"},
+ 361163: {'name': 'Downtown (MAP13) - Chaingun',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 183,
+ 'doom_type': 2002,
+ 'region': "Downtown (MAP13) Main"},
+ 361164: {'name': 'Downtown (MAP13) - Blue keycard',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 195,
+ 'doom_type': 5,
+ 'region': "Downtown (MAP13) Main"},
+ 361165: {'name': 'Downtown (MAP13) - Yellow keycard',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 201,
+ 'doom_type': 6,
+ 'region': "Downtown (MAP13) Red"},
+ 361166: {'name': 'Downtown (MAP13) - Berserk 2',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 207,
+ 'doom_type': 2023,
+ 'region': "Downtown (MAP13) Red"},
+ 361167: {'name': 'Downtown (MAP13) - Plasma gun',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 231,
+ 'doom_type': 2004,
+ 'region': "Downtown (MAP13) Main"},
+ 361168: {'name': 'Downtown (MAP13) - Partial invisibility',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 249,
+ 'doom_type': 2024,
+ 'region': "Downtown (MAP13) Main"},
+ 361169: {'name': 'Downtown (MAP13) - Backpack',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 250,
+ 'doom_type': 8,
+ 'region': "Downtown (MAP13) Main"},
+ 361170: {'name': 'Downtown (MAP13) - Chainsaw',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 257,
+ 'doom_type': 2005,
+ 'region': "Downtown (MAP13) Blue"},
+ 361171: {'name': 'Downtown (MAP13) - BFG9000',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 258,
+ 'doom_type': 2006,
+ 'region': "Downtown (MAP13) Main"},
+ 361172: {'name': 'Downtown (MAP13) - Invulnerability',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 269,
+ 'doom_type': 2022,
+ 'region': "Downtown (MAP13) Blue"},
+ 361173: {'name': 'Downtown (MAP13) - Invulnerability 2',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 280,
+ 'doom_type': 2022,
+ 'region': "Downtown (MAP13) Main"},
+ 361174: {'name': 'Downtown (MAP13) - Partial invisibility 2',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 281,
+ 'doom_type': 2024,
+ 'region': "Downtown (MAP13) Main"},
+ 361175: {'name': 'Downtown (MAP13) - Partial invisibility 3',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 282,
+ 'doom_type': 2024,
+ 'region': "Downtown (MAP13) Main"},
+ 361176: {'name': 'Downtown (MAP13) - Red keycard',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 283,
+ 'doom_type': 13,
+ 'region': "Downtown (MAP13) Blue"},
+ 361177: {'name': 'Downtown (MAP13) - Berserk 3',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 296,
+ 'doom_type': 2023,
+ 'region': "Downtown (MAP13) Yellow"},
+ 361178: {'name': 'Downtown (MAP13) - Computer area map',
+ 'episode': 2,
+ 'map': 2,
+ 'index': 298,
+ 'doom_type': 2026,
+ 'region': "Downtown (MAP13) Main"},
+ 361179: {'name': 'Downtown (MAP13) - Exit',
+ 'episode': 2,
+ 'map': 2,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Downtown (MAP13) Yellow"},
+ 361180: {'name': 'The Inmost Dens (MAP14) - Shotgun',
+ 'episode': 2,
+ 'map': 3,
+ 'index': 13,
+ 'doom_type': 2001,
+ 'region': "The Inmost Dens (MAP14) Main"},
+ 361181: {'name': 'The Inmost Dens (MAP14) - Supercharge',
+ 'episode': 2,
+ 'map': 3,
+ 'index': 16,
+ 'doom_type': 2013,
+ 'region': "The Inmost Dens (MAP14) Main"},
+ 361182: {'name': 'The Inmost Dens (MAP14) - Mega Armor',
+ 'episode': 2,
+ 'map': 3,
+ 'index': 22,
+ 'doom_type': 2019,
+ 'region': "The Inmost Dens (MAP14) Main"},
+ 361183: {'name': 'The Inmost Dens (MAP14) - Berserk',
+ 'episode': 2,
+ 'map': 3,
+ 'index': 78,
+ 'doom_type': 2023,
+ 'region': "The Inmost Dens (MAP14) Main"},
+ 361184: {'name': 'The Inmost Dens (MAP14) - Chaingun',
+ 'episode': 2,
+ 'map': 3,
+ 'index': 80,
+ 'doom_type': 2002,
+ 'region': "The Inmost Dens (MAP14) Main"},
+ 361185: {'name': 'The Inmost Dens (MAP14) - Plasma gun',
+ 'episode': 2,
+ 'map': 3,
+ 'index': 81,
+ 'doom_type': 2004,
+ 'region': "The Inmost Dens (MAP14) Main"},
+ 361186: {'name': 'The Inmost Dens (MAP14) - Red skull key',
+ 'episode': 2,
+ 'map': 3,
+ 'index': 119,
+ 'doom_type': 38,
+ 'region': "The Inmost Dens (MAP14) Main"},
+ 361187: {'name': 'The Inmost Dens (MAP14) - Rocket launcher',
+ 'episode': 2,
+ 'map': 3,
+ 'index': 123,
+ 'doom_type': 2003,
+ 'region': "The Inmost Dens (MAP14) Main"},
+ 361188: {'name': 'The Inmost Dens (MAP14) - Blue skull key',
+ 'episode': 2,
+ 'map': 3,
+ 'index': 130,
+ 'doom_type': 40,
+ 'region': "The Inmost Dens (MAP14) Red South"},
+ 361189: {'name': 'The Inmost Dens (MAP14) - Partial invisibility',
+ 'episode': 2,
+ 'map': 3,
+ 'index': 138,
+ 'doom_type': 2024,
+ 'region': "The Inmost Dens (MAP14) Red South"},
+ 361190: {'name': 'The Inmost Dens (MAP14) - Exit',
+ 'episode': 2,
+ 'map': 3,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Inmost Dens (MAP14) Blue"},
+ 361191: {'name': 'Industrial Zone (MAP15) - Berserk',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 4,
+ 'doom_type': 2023,
+ 'region': "Industrial Zone (MAP15) Main"},
+ 361192: {'name': 'Industrial Zone (MAP15) - Rocket launcher',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 11,
+ 'doom_type': 2003,
+ 'region': "Industrial Zone (MAP15) Main"},
+ 361193: {'name': 'Industrial Zone (MAP15) - Shotgun',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 13,
+ 'doom_type': 2001,
+ 'region': "Industrial Zone (MAP15) Main"},
+ 361194: {'name': 'Industrial Zone (MAP15) - Partial invisibility',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 14,
+ 'doom_type': 2024,
+ 'region': "Industrial Zone (MAP15) Main"},
+ 361195: {'name': 'Industrial Zone (MAP15) - Backpack',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 24,
+ 'doom_type': 8,
+ 'region': "Industrial Zone (MAP15) Main"},
+ 361196: {'name': 'Industrial Zone (MAP15) - BFG9000',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 48,
+ 'doom_type': 2006,
+ 'region': "Industrial Zone (MAP15) Main"},
+ 361197: {'name': 'Industrial Zone (MAP15) - Supercharge',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 56,
+ 'doom_type': 2013,
+ 'region': "Industrial Zone (MAP15) Main"},
+ 361198: {'name': 'Industrial Zone (MAP15) - Mega Armor',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 57,
+ 'doom_type': 2019,
+ 'region': "Industrial Zone (MAP15) Main"},
+ 361199: {'name': 'Industrial Zone (MAP15) - Armor',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 59,
+ 'doom_type': 2018,
+ 'region': "Industrial Zone (MAP15) Main"},
+ 361200: {'name': 'Industrial Zone (MAP15) - Yellow keycard',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 71,
+ 'doom_type': 6,
+ 'region': "Industrial Zone (MAP15) Main"},
+ 361201: {'name': 'Industrial Zone (MAP15) - Chaingun',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 74,
+ 'doom_type': 2002,
+ 'region': "Industrial Zone (MAP15) Main"},
+ 361202: {'name': 'Industrial Zone (MAP15) - Plasma gun',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 86,
+ 'doom_type': 2004,
+ 'region': "Industrial Zone (MAP15) Yellow West"},
+ 361203: {'name': 'Industrial Zone (MAP15) - Partial invisibility 2',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 91,
+ 'doom_type': 2024,
+ 'region': "Industrial Zone (MAP15) Yellow West"},
+ 361204: {'name': 'Industrial Zone (MAP15) - Computer area map',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 93,
+ 'doom_type': 2026,
+ 'region': "Industrial Zone (MAP15) Yellow West"},
+ 361205: {'name': 'Industrial Zone (MAP15) - Invulnerability',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 94,
+ 'doom_type': 2022,
+ 'region': "Industrial Zone (MAP15) Main"},
+ 361206: {'name': 'Industrial Zone (MAP15) - Red keycard',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 100,
+ 'doom_type': 13,
+ 'region': "Industrial Zone (MAP15) Main"},
+ 361207: {'name': 'Industrial Zone (MAP15) - Backpack 2',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 103,
+ 'doom_type': 8,
+ 'region': "Industrial Zone (MAP15) Yellow West"},
+ 361208: {'name': 'Industrial Zone (MAP15) - Chainsaw',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 113,
+ 'doom_type': 2005,
+ 'region': "Industrial Zone (MAP15) Yellow East"},
+ 361209: {'name': 'Industrial Zone (MAP15) - Megasphere',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 125,
+ 'doom_type': 83,
+ 'region': "Industrial Zone (MAP15) Yellow East"},
+ 361210: {'name': 'Industrial Zone (MAP15) - Berserk 2',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 178,
+ 'doom_type': 2023,
+ 'region': "Industrial Zone (MAP15) Yellow East"},
+ 361211: {'name': 'Industrial Zone (MAP15) - Blue keycard',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 337,
+ 'doom_type': 5,
+ 'region': "Industrial Zone (MAP15) Yellow West"},
+ 361212: {'name': 'Industrial Zone (MAP15) - Mega Armor 2',
+ 'episode': 2,
+ 'map': 4,
+ 'index': 361,
+ 'doom_type': 2019,
+ 'region': "Industrial Zone (MAP15) Main"},
+ 361213: {'name': 'Industrial Zone (MAP15) - Exit',
+ 'episode': 2,
+ 'map': 4,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Industrial Zone (MAP15) Blue"},
+ 361214: {'name': 'Suburbs (MAP16) - Megasphere',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 7,
+ 'doom_type': 83,
+ 'region': "Suburbs (MAP16) Main"},
+ 361215: {'name': 'Suburbs (MAP16) - Super Shotgun',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 11,
+ 'doom_type': 82,
+ 'region': "Suburbs (MAP16) Main"},
+ 361216: {'name': 'Suburbs (MAP16) - Chaingun',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 15,
+ 'doom_type': 2002,
+ 'region': "Suburbs (MAP16) Main"},
+ 361217: {'name': 'Suburbs (MAP16) - Backpack',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 53,
+ 'doom_type': 8,
+ 'region': "Suburbs (MAP16) Main"},
+ 361218: {'name': 'Suburbs (MAP16) - Rocket launcher',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 59,
+ 'doom_type': 2003,
+ 'region': "Suburbs (MAP16) Main"},
+ 361219: {'name': 'Suburbs (MAP16) - Berserk',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 60,
+ 'doom_type': 2023,
+ 'region': "Suburbs (MAP16) Main"},
+ 361220: {'name': 'Suburbs (MAP16) - Plasma gun',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 62,
+ 'doom_type': 2004,
+ 'region': "Suburbs (MAP16) Blue"},
+ 361221: {'name': 'Suburbs (MAP16) - Plasma gun 2',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 63,
+ 'doom_type': 2004,
+ 'region': "Suburbs (MAP16) Blue"},
+ 361222: {'name': 'Suburbs (MAP16) - Plasma gun 3',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 64,
+ 'doom_type': 2004,
+ 'region': "Suburbs (MAP16) Blue"},
+ 361223: {'name': 'Suburbs (MAP16) - Plasma gun 4',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 65,
+ 'doom_type': 2004,
+ 'region': "Suburbs (MAP16) Blue"},
+ 361224: {'name': 'Suburbs (MAP16) - BFG9000',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 169,
+ 'doom_type': 2006,
+ 'region': "Suburbs (MAP16) Main"},
+ 361225: {'name': 'Suburbs (MAP16) - Shotgun',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 182,
+ 'doom_type': 2001,
+ 'region': "Suburbs (MAP16) Main"},
+ 361226: {'name': 'Suburbs (MAP16) - Supercharge',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 185,
+ 'doom_type': 2013,
+ 'region': "Suburbs (MAP16) Main"},
+ 361227: {'name': 'Suburbs (MAP16) - Blue skull key',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 186,
+ 'doom_type': 40,
+ 'region': "Suburbs (MAP16) Main"},
+ 361228: {'name': 'Suburbs (MAP16) - Invulnerability',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 221,
+ 'doom_type': 2022,
+ 'region': "Suburbs (MAP16) Main"},
+ 361229: {'name': 'Suburbs (MAP16) - Partial invisibility',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 231,
+ 'doom_type': 2024,
+ 'region': "Suburbs (MAP16) Main"},
+ 361230: {'name': 'Suburbs (MAP16) - Red skull key',
+ 'episode': 2,
+ 'map': 5,
+ 'index': 236,
+ 'doom_type': 38,
+ 'region': "Suburbs (MAP16) Blue"},
+ 361231: {'name': 'Suburbs (MAP16) - Exit',
+ 'episode': 2,
+ 'map': 5,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Suburbs (MAP16) Red"},
+ 361232: {'name': 'Tenements (MAP17) - Armor',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 1,
+ 'doom_type': 2018,
+ 'region': "Tenements (MAP17) Red"},
+ 361233: {'name': 'Tenements (MAP17) - Supercharge',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 7,
+ 'doom_type': 2013,
+ 'region': "Tenements (MAP17) Yellow"},
+ 361234: {'name': 'Tenements (MAP17) - Shotgun',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 18,
+ 'doom_type': 2001,
+ 'region': "Tenements (MAP17) Main"},
+ 361235: {'name': 'Tenements (MAP17) - Red keycard',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 34,
+ 'doom_type': 13,
+ 'region': "Tenements (MAP17) Main"},
+ 361236: {'name': 'Tenements (MAP17) - Blue keycard',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 69,
+ 'doom_type': 5,
+ 'region': "Tenements (MAP17) Red"},
+ 361237: {'name': 'Tenements (MAP17) - Supercharge 2',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 75,
+ 'doom_type': 2013,
+ 'region': "Tenements (MAP17) Blue"},
+ 361238: {'name': 'Tenements (MAP17) - Yellow skull key',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 76,
+ 'doom_type': 39,
+ 'region': "Tenements (MAP17) Blue"},
+ 361239: {'name': 'Tenements (MAP17) - Rocket launcher',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 77,
+ 'doom_type': 2003,
+ 'region': "Tenements (MAP17) Blue"},
+ 361240: {'name': 'Tenements (MAP17) - Partial invisibility',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 81,
+ 'doom_type': 2024,
+ 'region': "Tenements (MAP17) Blue"},
+ 361241: {'name': 'Tenements (MAP17) - Chaingun',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 92,
+ 'doom_type': 2002,
+ 'region': "Tenements (MAP17) Red"},
+ 361242: {'name': 'Tenements (MAP17) - BFG9000',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 102,
+ 'doom_type': 2006,
+ 'region': "Tenements (MAP17) Main"},
+ 361243: {'name': 'Tenements (MAP17) - Plasma gun',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 114,
+ 'doom_type': 2004,
+ 'region': "Tenements (MAP17) Yellow"},
+ 361244: {'name': 'Tenements (MAP17) - Mega Armor',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 168,
+ 'doom_type': 2019,
+ 'region': "Tenements (MAP17) Red"},
+ 361245: {'name': 'Tenements (MAP17) - Armor 2',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 179,
+ 'doom_type': 2018,
+ 'region': "Tenements (MAP17) Red"},
+ 361246: {'name': 'Tenements (MAP17) - Berserk',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 218,
+ 'doom_type': 2023,
+ 'region': "Tenements (MAP17) Red"},
+ 361247: {'name': 'Tenements (MAP17) - Backpack',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 261,
+ 'doom_type': 8,
+ 'region': "Tenements (MAP17) Blue"},
+ 361248: {'name': 'Tenements (MAP17) - Megasphere',
+ 'episode': 2,
+ 'map': 6,
+ 'index': 419,
+ 'doom_type': 83,
+ 'region': "Tenements (MAP17) Yellow"},
+ 361249: {'name': 'Tenements (MAP17) - Exit',
+ 'episode': 2,
+ 'map': 6,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Tenements (MAP17) Yellow"},
+ 361250: {'name': 'The Courtyard (MAP18) - Shotgun',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 12,
+ 'doom_type': 2001,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361251: {'name': 'The Courtyard (MAP18) - Plasma gun',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 36,
+ 'doom_type': 2004,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361252: {'name': 'The Courtyard (MAP18) - Armor',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 48,
+ 'doom_type': 2018,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361253: {'name': 'The Courtyard (MAP18) - Berserk',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 52,
+ 'doom_type': 2023,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361254: {'name': 'The Courtyard (MAP18) - Chaingun',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 95,
+ 'doom_type': 2002,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361255: {'name': 'The Courtyard (MAP18) - Rocket launcher',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 130,
+ 'doom_type': 2003,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361256: {'name': 'The Courtyard (MAP18) - Partial invisibility',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 170,
+ 'doom_type': 2024,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361257: {'name': 'The Courtyard (MAP18) - Partial invisibility 2',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 171,
+ 'doom_type': 2024,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361258: {'name': 'The Courtyard (MAP18) - Backpack',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 198,
+ 'doom_type': 8,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361259: {'name': 'The Courtyard (MAP18) - Supercharge',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 218,
+ 'doom_type': 2013,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361260: {'name': 'The Courtyard (MAP18) - Invulnerability',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 228,
+ 'doom_type': 2022,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361261: {'name': 'The Courtyard (MAP18) - Invulnerability 2',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 229,
+ 'doom_type': 2022,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361262: {'name': 'The Courtyard (MAP18) - Yellow skull key',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 254,
+ 'doom_type': 39,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361263: {'name': 'The Courtyard (MAP18) - Blue skull key',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 268,
+ 'doom_type': 40,
+ 'region': "The Courtyard (MAP18) Yellow"},
+ 361264: {'name': 'The Courtyard (MAP18) - BFG9000',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 400,
+ 'doom_type': 2006,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361265: {'name': 'The Courtyard (MAP18) - Computer area map',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 458,
+ 'doom_type': 2026,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361266: {'name': 'The Courtyard (MAP18) - Super Shotgun',
+ 'episode': 2,
+ 'map': 7,
+ 'index': 461,
+ 'doom_type': 82,
+ 'region': "The Courtyard (MAP18) Main"},
+ 361267: {'name': 'The Courtyard (MAP18) - Exit',
+ 'episode': 2,
+ 'map': 7,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Courtyard (MAP18) Blue"},
+ 361268: {'name': 'The Citadel (MAP19) - Armor',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 64,
+ 'doom_type': 2018,
+ 'region': "The Citadel (MAP19) Main"},
+ 361269: {'name': 'The Citadel (MAP19) - Chaingun',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 99,
+ 'doom_type': 2002,
+ 'region': "The Citadel (MAP19) Main"},
+ 361270: {'name': 'The Citadel (MAP19) - Berserk',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 116,
+ 'doom_type': 2023,
+ 'region': "The Citadel (MAP19) Main"},
+ 361271: {'name': 'The Citadel (MAP19) - Mega Armor',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 127,
+ 'doom_type': 2019,
+ 'region': "The Citadel (MAP19) Main"},
+ 361272: {'name': 'The Citadel (MAP19) - Supercharge',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 174,
+ 'doom_type': 2013,
+ 'region': "The Citadel (MAP19) Main"},
+ 361273: {'name': 'The Citadel (MAP19) - Armor 2',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 223,
+ 'doom_type': 2018,
+ 'region': "The Citadel (MAP19) Main"},
+ 361274: {'name': 'The Citadel (MAP19) - Backpack',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 232,
+ 'doom_type': 8,
+ 'region': "The Citadel (MAP19) Main"},
+ 361275: {'name': 'The Citadel (MAP19) - Invulnerability',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 315,
+ 'doom_type': 2022,
+ 'region': "The Citadel (MAP19) Main"},
+ 361276: {'name': 'The Citadel (MAP19) - Blue skull key',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 370,
+ 'doom_type': 40,
+ 'region': "The Citadel (MAP19) Main"},
+ 361277: {'name': 'The Citadel (MAP19) - Partial invisibility',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 403,
+ 'doom_type': 2024,
+ 'region': "The Citadel (MAP19) Main"},
+ 361278: {'name': 'The Citadel (MAP19) - Red skull key',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 404,
+ 'doom_type': 38,
+ 'region': "The Citadel (MAP19) Main"},
+ 361279: {'name': 'The Citadel (MAP19) - Yellow skull key',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 405,
+ 'doom_type': 39,
+ 'region': "The Citadel (MAP19) Main"},
+ 361280: {'name': 'The Citadel (MAP19) - Computer area map',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 415,
+ 'doom_type': 2026,
+ 'region': "The Citadel (MAP19) Main"},
+ 361281: {'name': 'The Citadel (MAP19) - Rocket launcher',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 416,
+ 'doom_type': 2003,
+ 'region': "The Citadel (MAP19) Main"},
+ 361282: {'name': 'The Citadel (MAP19) - Super Shotgun',
+ 'episode': 2,
+ 'map': 8,
+ 'index': 431,
+ 'doom_type': 82,
+ 'region': "The Citadel (MAP19) Main"},
+ 361283: {'name': 'The Citadel (MAP19) - Exit',
+ 'episode': 2,
+ 'map': 8,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Citadel (MAP19) Red"},
+ 361284: {'name': 'Gotcha! (MAP20) - Mega Armor',
+ 'episode': 2,
+ 'map': 9,
+ 'index': 9,
+ 'doom_type': 2019,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361285: {'name': 'Gotcha! (MAP20) - Rocket launcher',
+ 'episode': 2,
+ 'map': 9,
+ 'index': 10,
+ 'doom_type': 2003,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361286: {'name': 'Gotcha! (MAP20) - Supercharge',
+ 'episode': 2,
+ 'map': 9,
+ 'index': 12,
+ 'doom_type': 2013,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361287: {'name': 'Gotcha! (MAP20) - Armor',
+ 'episode': 2,
+ 'map': 9,
+ 'index': 33,
+ 'doom_type': 2018,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361288: {'name': 'Gotcha! (MAP20) - Megasphere',
+ 'episode': 2,
+ 'map': 9,
+ 'index': 43,
+ 'doom_type': 83,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361289: {'name': 'Gotcha! (MAP20) - Armor 2',
+ 'episode': 2,
+ 'map': 9,
+ 'index': 47,
+ 'doom_type': 2018,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361290: {'name': 'Gotcha! (MAP20) - Super Shotgun',
+ 'episode': 2,
+ 'map': 9,
+ 'index': 54,
+ 'doom_type': 82,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361291: {'name': 'Gotcha! (MAP20) - Plasma gun',
+ 'episode': 2,
+ 'map': 9,
+ 'index': 70,
+ 'doom_type': 2004,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361292: {'name': 'Gotcha! (MAP20) - Mega Armor 2',
+ 'episode': 2,
+ 'map': 9,
+ 'index': 96,
+ 'doom_type': 2019,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361293: {'name': 'Gotcha! (MAP20) - Berserk',
+ 'episode': 2,
+ 'map': 9,
+ 'index': 109,
+ 'doom_type': 2023,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361294: {'name': 'Gotcha! (MAP20) - Supercharge 2',
+ 'episode': 2,
+ 'map': 9,
+ 'index': 119,
+ 'doom_type': 2013,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361295: {'name': 'Gotcha! (MAP20) - Supercharge 3',
+ 'episode': 2,
+ 'map': 9,
+ 'index': 122,
+ 'doom_type': 2013,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361296: {'name': 'Gotcha! (MAP20) - BFG9000',
+ 'episode': 2,
+ 'map': 9,
+ 'index': 142,
+ 'doom_type': 2006,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361297: {'name': 'Gotcha! (MAP20) - Supercharge 4',
+ 'episode': 2,
+ 'map': 9,
+ 'index': 145,
+ 'doom_type': 2013,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361298: {'name': 'Gotcha! (MAP20) - Exit',
+ 'episode': 2,
+ 'map': 9,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Gotcha! (MAP20) Main"},
+ 361299: {'name': 'Nirvana (MAP21) - Super Shotgun',
+ 'episode': 3,
+ 'map': 1,
+ 'index': 70,
+ 'doom_type': 82,
+ 'region': "Nirvana (MAP21) Main"},
+ 361300: {'name': 'Nirvana (MAP21) - Rocket launcher',
+ 'episode': 3,
+ 'map': 1,
+ 'index': 76,
+ 'doom_type': 2003,
+ 'region': "Nirvana (MAP21) Main"},
+ 361301: {'name': 'Nirvana (MAP21) - Yellow skull key',
+ 'episode': 3,
+ 'map': 1,
+ 'index': 108,
+ 'doom_type': 39,
+ 'region': "Nirvana (MAP21) Main"},
+ 361302: {'name': 'Nirvana (MAP21) - Backpack',
+ 'episode': 3,
+ 'map': 1,
+ 'index': 109,
+ 'doom_type': 8,
+ 'region': "Nirvana (MAP21) Main"},
+ 361303: {'name': 'Nirvana (MAP21) - Megasphere',
+ 'episode': 3,
+ 'map': 1,
+ 'index': 112,
+ 'doom_type': 83,
+ 'region': "Nirvana (MAP21) Main"},
+ 361304: {'name': 'Nirvana (MAP21) - Invulnerability',
+ 'episode': 3,
+ 'map': 1,
+ 'index': 194,
+ 'doom_type': 2022,
+ 'region': "Nirvana (MAP21) Yellow"},
+ 361305: {'name': 'Nirvana (MAP21) - Blue skull key',
+ 'episode': 3,
+ 'map': 1,
+ 'index': 199,
+ 'doom_type': 40,
+ 'region': "Nirvana (MAP21) Yellow"},
+ 361306: {'name': 'Nirvana (MAP21) - Red skull key',
+ 'episode': 3,
+ 'map': 1,
+ 'index': 215,
+ 'doom_type': 38,
+ 'region': "Nirvana (MAP21) Yellow"},
+ 361307: {'name': 'Nirvana (MAP21) - Exit',
+ 'episode': 3,
+ 'map': 1,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Nirvana (MAP21) Magenta"},
+ 361308: {'name': 'The Catacombs (MAP22) - Rocket launcher',
+ 'episode': 3,
+ 'map': 2,
+ 'index': 4,
+ 'doom_type': 2003,
+ 'region': "The Catacombs (MAP22) Main"},
+ 361309: {'name': 'The Catacombs (MAP22) - Blue skull key',
+ 'episode': 3,
+ 'map': 2,
+ 'index': 5,
+ 'doom_type': 40,
+ 'region': "The Catacombs (MAP22) Main"},
+ 361310: {'name': 'The Catacombs (MAP22) - Red skull key',
+ 'episode': 3,
+ 'map': 2,
+ 'index': 12,
+ 'doom_type': 38,
+ 'region': "The Catacombs (MAP22) Blue"},
+ 361311: {'name': 'The Catacombs (MAP22) - Shotgun',
+ 'episode': 3,
+ 'map': 2,
+ 'index': 28,
+ 'doom_type': 2001,
+ 'region': "The Catacombs (MAP22) Main"},
+ 361312: {'name': 'The Catacombs (MAP22) - Berserk',
+ 'episode': 3,
+ 'map': 2,
+ 'index': 45,
+ 'doom_type': 2023,
+ 'region': "The Catacombs (MAP22) Main"},
+ 361313: {'name': 'The Catacombs (MAP22) - Plasma gun',
+ 'episode': 3,
+ 'map': 2,
+ 'index': 83,
+ 'doom_type': 2004,
+ 'region': "The Catacombs (MAP22) Main"},
+ 361314: {'name': 'The Catacombs (MAP22) - Supercharge',
+ 'episode': 3,
+ 'map': 2,
+ 'index': 118,
+ 'doom_type': 2013,
+ 'region': "The Catacombs (MAP22) Main"},
+ 361315: {'name': 'The Catacombs (MAP22) - Armor',
+ 'episode': 3,
+ 'map': 2,
+ 'index': 119,
+ 'doom_type': 2018,
+ 'region': "The Catacombs (MAP22) Main"},
+ 361316: {'name': 'The Catacombs (MAP22) - Exit',
+ 'episode': 3,
+ 'map': 2,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Catacombs (MAP22) Red"},
+ 361317: {'name': 'Barrels o Fun (MAP23) - Shotgun',
+ 'episode': 3,
+ 'map': 3,
+ 'index': 136,
+ 'doom_type': 2001,
+ 'region': "Barrels o Fun (MAP23) Main"},
+ 361318: {'name': 'Barrels o Fun (MAP23) - Berserk',
+ 'episode': 3,
+ 'map': 3,
+ 'index': 222,
+ 'doom_type': 2023,
+ 'region': "Barrels o Fun (MAP23) Main"},
+ 361319: {'name': 'Barrels o Fun (MAP23) - Backpack',
+ 'episode': 3,
+ 'map': 3,
+ 'index': 223,
+ 'doom_type': 8,
+ 'region': "Barrels o Fun (MAP23) Main"},
+ 361320: {'name': 'Barrels o Fun (MAP23) - Computer area map',
+ 'episode': 3,
+ 'map': 3,
+ 'index': 224,
+ 'doom_type': 2026,
+ 'region': "Barrels o Fun (MAP23) Main"},
+ 361321: {'name': 'Barrels o Fun (MAP23) - Armor',
+ 'episode': 3,
+ 'map': 3,
+ 'index': 249,
+ 'doom_type': 2018,
+ 'region': "Barrels o Fun (MAP23) Main"},
+ 361322: {'name': 'Barrels o Fun (MAP23) - Rocket launcher',
+ 'episode': 3,
+ 'map': 3,
+ 'index': 264,
+ 'doom_type': 2003,
+ 'region': "Barrels o Fun (MAP23) Main"},
+ 361323: {'name': 'Barrels o Fun (MAP23) - Megasphere',
+ 'episode': 3,
+ 'map': 3,
+ 'index': 266,
+ 'doom_type': 83,
+ 'region': "Barrels o Fun (MAP23) Main"},
+ 361324: {'name': 'Barrels o Fun (MAP23) - Supercharge',
+ 'episode': 3,
+ 'map': 3,
+ 'index': 277,
+ 'doom_type': 2013,
+ 'region': "Barrels o Fun (MAP23) Main"},
+ 361325: {'name': 'Barrels o Fun (MAP23) - Backpack 2',
+ 'episode': 3,
+ 'map': 3,
+ 'index': 301,
+ 'doom_type': 8,
+ 'region': "Barrels o Fun (MAP23) Main"},
+ 361326: {'name': 'Barrels o Fun (MAP23) - Yellow skull key',
+ 'episode': 3,
+ 'map': 3,
+ 'index': 307,
+ 'doom_type': 39,
+ 'region': "Barrels o Fun (MAP23) Main"},
+ 361327: {'name': 'Barrels o Fun (MAP23) - BFG9000',
+ 'episode': 3,
+ 'map': 3,
+ 'index': 342,
+ 'doom_type': 2006,
+ 'region': "Barrels o Fun (MAP23) Main"},
+ 361328: {'name': 'Barrels o Fun (MAP23) - Exit',
+ 'episode': 3,
+ 'map': 3,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Barrels o Fun (MAP23) Yellow"},
+ 361329: {'name': 'The Chasm (MAP24) - Plasma gun',
+ 'episode': 3,
+ 'map': 4,
+ 'index': 5,
+ 'doom_type': 2004,
+ 'region': "The Chasm (MAP24) Main"},
+ 361330: {'name': 'The Chasm (MAP24) - Shotgun',
+ 'episode': 3,
+ 'map': 4,
+ 'index': 6,
+ 'doom_type': 2001,
+ 'region': "The Chasm (MAP24) Main"},
+ 361331: {'name': 'The Chasm (MAP24) - Invulnerability',
+ 'episode': 3,
+ 'map': 4,
+ 'index': 12,
+ 'doom_type': 2022,
+ 'region': "The Chasm (MAP24) Main"},
+ 361332: {'name': 'The Chasm (MAP24) - Rocket launcher',
+ 'episode': 3,
+ 'map': 4,
+ 'index': 22,
+ 'doom_type': 2003,
+ 'region': "The Chasm (MAP24) Main"},
+ 361333: {'name': 'The Chasm (MAP24) - Blue keycard',
+ 'episode': 3,
+ 'map': 4,
+ 'index': 23,
+ 'doom_type': 5,
+ 'region': "The Chasm (MAP24) Main"},
+ 361334: {'name': 'The Chasm (MAP24) - Backpack',
+ 'episode': 3,
+ 'map': 4,
+ 'index': 31,
+ 'doom_type': 8,
+ 'region': "The Chasm (MAP24) Main"},
+ 361335: {'name': 'The Chasm (MAP24) - Berserk',
+ 'episode': 3,
+ 'map': 4,
+ 'index': 79,
+ 'doom_type': 2023,
+ 'region': "The Chasm (MAP24) Main"},
+ 361336: {'name': 'The Chasm (MAP24) - Berserk 2',
+ 'episode': 3,
+ 'map': 4,
+ 'index': 155,
+ 'doom_type': 2023,
+ 'region': "The Chasm (MAP24) Main"},
+ 361337: {'name': 'The Chasm (MAP24) - Armor',
+ 'episode': 3,
+ 'map': 4,
+ 'index': 169,
+ 'doom_type': 2018,
+ 'region': "The Chasm (MAP24) Main"},
+ 361338: {'name': 'The Chasm (MAP24) - Red keycard',
+ 'episode': 3,
+ 'map': 4,
+ 'index': 261,
+ 'doom_type': 13,
+ 'region': "The Chasm (MAP24) Main"},
+ 361339: {'name': 'The Chasm (MAP24) - BFG9000',
+ 'episode': 3,
+ 'map': 4,
+ 'index': 295,
+ 'doom_type': 2006,
+ 'region': "The Chasm (MAP24) Main"},
+ 361340: {'name': 'The Chasm (MAP24) - Super Shotgun',
+ 'episode': 3,
+ 'map': 4,
+ 'index': 353,
+ 'doom_type': 82,
+ 'region': "The Chasm (MAP24) Main"},
+ 361341: {'name': 'The Chasm (MAP24) - Megasphere',
+ 'episode': 3,
+ 'map': 4,
+ 'index': 355,
+ 'doom_type': 83,
+ 'region': "The Chasm (MAP24) Main"},
+ 361342: {'name': 'The Chasm (MAP24) - Megasphere 2',
+ 'episode': 3,
+ 'map': 4,
+ 'index': 362,
+ 'doom_type': 83,
+ 'region': "The Chasm (MAP24) Main"},
+ 361343: {'name': 'The Chasm (MAP24) - Exit',
+ 'episode': 3,
+ 'map': 4,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Chasm (MAP24) Red"},
+ 361344: {'name': 'Bloodfalls (MAP25) - Super Shotgun',
+ 'episode': 3,
+ 'map': 5,
+ 'index': 6,
+ 'doom_type': 82,
+ 'region': "Bloodfalls (MAP25) Main"},
+ 361345: {'name': 'Bloodfalls (MAP25) - Partial invisibility',
+ 'episode': 3,
+ 'map': 5,
+ 'index': 7,
+ 'doom_type': 2024,
+ 'region': "Bloodfalls (MAP25) Blue"},
+ 361346: {'name': 'Bloodfalls (MAP25) - Megasphere',
+ 'episode': 3,
+ 'map': 5,
+ 'index': 23,
+ 'doom_type': 83,
+ 'region': "Bloodfalls (MAP25) Main"},
+ 361347: {'name': 'Bloodfalls (MAP25) - BFG9000',
+ 'episode': 3,
+ 'map': 5,
+ 'index': 34,
+ 'doom_type': 2006,
+ 'region': "Bloodfalls (MAP25) Blue"},
+ 361348: {'name': 'Bloodfalls (MAP25) - Mega Armor',
+ 'episode': 3,
+ 'map': 5,
+ 'index': 103,
+ 'doom_type': 2019,
+ 'region': "Bloodfalls (MAP25) Main"},
+ 361349: {'name': 'Bloodfalls (MAP25) - Armor',
+ 'episode': 3,
+ 'map': 5,
+ 'index': 104,
+ 'doom_type': 2018,
+ 'region': "Bloodfalls (MAP25) Main"},
+ 361350: {'name': 'Bloodfalls (MAP25) - Blue skull key',
+ 'episode': 3,
+ 'map': 5,
+ 'index': 106,
+ 'doom_type': 40,
+ 'region': "Bloodfalls (MAP25) Main"},
+ 361351: {'name': 'Bloodfalls (MAP25) - Chaingun',
+ 'episode': 3,
+ 'map': 5,
+ 'index': 150,
+ 'doom_type': 2002,
+ 'region': "Bloodfalls (MAP25) Main"},
+ 361352: {'name': 'Bloodfalls (MAP25) - Plasma gun',
+ 'episode': 3,
+ 'map': 5,
+ 'index': 169,
+ 'doom_type': 2004,
+ 'region': "Bloodfalls (MAP25) Main"},
+ 361353: {'name': 'Bloodfalls (MAP25) - BFG9000 2',
+ 'episode': 3,
+ 'map': 5,
+ 'index': 186,
+ 'doom_type': 2006,
+ 'region': "Bloodfalls (MAP25) Main"},
+ 361354: {'name': 'Bloodfalls (MAP25) - Rocket launcher',
+ 'episode': 3,
+ 'map': 5,
+ 'index': 236,
+ 'doom_type': 2003,
+ 'region': "Bloodfalls (MAP25) Main"},
+ 361355: {'name': 'Bloodfalls (MAP25) - Exit',
+ 'episode': 3,
+ 'map': 5,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Bloodfalls (MAP25) Blue"},
+ 361356: {'name': 'The Abandoned Mines (MAP26) - Blue keycard',
+ 'episode': 3,
+ 'map': 6,
+ 'index': 20,
+ 'doom_type': 5,
+ 'region': "The Abandoned Mines (MAP26) Red"},
+ 361357: {'name': 'The Abandoned Mines (MAP26) - Super Shotgun',
+ 'episode': 3,
+ 'map': 6,
+ 'index': 21,
+ 'doom_type': 82,
+ 'region': "The Abandoned Mines (MAP26) Main"},
+ 361358: {'name': 'The Abandoned Mines (MAP26) - Rocket launcher',
+ 'episode': 3,
+ 'map': 6,
+ 'index': 49,
+ 'doom_type': 2003,
+ 'region': "The Abandoned Mines (MAP26) Main"},
+ 361359: {'name': 'The Abandoned Mines (MAP26) - Mega Armor',
+ 'episode': 3,
+ 'map': 6,
+ 'index': 95,
+ 'doom_type': 2019,
+ 'region': "The Abandoned Mines (MAP26) Red"},
+ 361360: {'name': 'The Abandoned Mines (MAP26) - Plasma gun',
+ 'episode': 3,
+ 'map': 6,
+ 'index': 107,
+ 'doom_type': 2004,
+ 'region': "The Abandoned Mines (MAP26) Main"},
+ 361361: {'name': 'The Abandoned Mines (MAP26) - Supercharge',
+ 'episode': 3,
+ 'map': 6,
+ 'index': 154,
+ 'doom_type': 2013,
+ 'region': "The Abandoned Mines (MAP26) Red"},
+ 361362: {'name': 'The Abandoned Mines (MAP26) - Chaingun',
+ 'episode': 3,
+ 'map': 6,
+ 'index': 155,
+ 'doom_type': 2002,
+ 'region': "The Abandoned Mines (MAP26) Main"},
+ 361363: {'name': 'The Abandoned Mines (MAP26) - Partial invisibility',
+ 'episode': 3,
+ 'map': 6,
+ 'index': 159,
+ 'doom_type': 2024,
+ 'region': "The Abandoned Mines (MAP26) Main"},
+ 361364: {'name': 'The Abandoned Mines (MAP26) - Armor',
+ 'episode': 3,
+ 'map': 6,
+ 'index': 170,
+ 'doom_type': 2018,
+ 'region': "The Abandoned Mines (MAP26) Main"},
+ 361365: {'name': 'The Abandoned Mines (MAP26) - Red keycard',
+ 'episode': 3,
+ 'map': 6,
+ 'index': 182,
+ 'doom_type': 13,
+ 'region': "The Abandoned Mines (MAP26) Main"},
+ 361366: {'name': 'The Abandoned Mines (MAP26) - Yellow keycard',
+ 'episode': 3,
+ 'map': 6,
+ 'index': 229,
+ 'doom_type': 6,
+ 'region': "The Abandoned Mines (MAP26) Blue"},
+ 361367: {'name': 'The Abandoned Mines (MAP26) - Backpack',
+ 'episode': 3,
+ 'map': 6,
+ 'index': 254,
+ 'doom_type': 8,
+ 'region': "The Abandoned Mines (MAP26) Main"},
+ 361368: {'name': 'The Abandoned Mines (MAP26) - Exit',
+ 'episode': 3,
+ 'map': 6,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Abandoned Mines (MAP26) Yellow"},
+ 361369: {'name': 'Monster Condo (MAP27) - Rocket launcher',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 4,
+ 'doom_type': 2003,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361370: {'name': 'Monster Condo (MAP27) - Partial invisibility',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 51,
+ 'doom_type': 2024,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361371: {'name': 'Monster Condo (MAP27) - Plasma gun',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 58,
+ 'doom_type': 2004,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361372: {'name': 'Monster Condo (MAP27) - Invulnerability',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 60,
+ 'doom_type': 2022,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361373: {'name': 'Monster Condo (MAP27) - Armor',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 86,
+ 'doom_type': 2018,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361374: {'name': 'Monster Condo (MAP27) - Backpack',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 105,
+ 'doom_type': 8,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361375: {'name': 'Monster Condo (MAP27) - Invulnerability 2',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 107,
+ 'doom_type': 2022,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361376: {'name': 'Monster Condo (MAP27) - Partial invisibility 2',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 122,
+ 'doom_type': 2024,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361377: {'name': 'Monster Condo (MAP27) - Supercharge',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 236,
+ 'doom_type': 2013,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361378: {'name': 'Monster Condo (MAP27) - Armor 2',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 239,
+ 'doom_type': 2018,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361379: {'name': 'Monster Condo (MAP27) - Chaingun',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 251,
+ 'doom_type': 2002,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361380: {'name': 'Monster Condo (MAP27) - BFG9000',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 279,
+ 'doom_type': 2006,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361381: {'name': 'Monster Condo (MAP27) - Backpack 2',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 285,
+ 'doom_type': 8,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361382: {'name': 'Monster Condo (MAP27) - Backpack 3',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 286,
+ 'doom_type': 8,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361383: {'name': 'Monster Condo (MAP27) - Backpack 4',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 287,
+ 'doom_type': 8,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361384: {'name': 'Monster Condo (MAP27) - Yellow skull key',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 310,
+ 'doom_type': 39,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361385: {'name': 'Monster Condo (MAP27) - Red skull key',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 364,
+ 'doom_type': 38,
+ 'region': "Monster Condo (MAP27) Blue"},
+ 361386: {'name': 'Monster Condo (MAP27) - Supercharge 2',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 365,
+ 'doom_type': 2013,
+ 'region': "Monster Condo (MAP27) Blue"},
+ 361387: {'name': 'Monster Condo (MAP27) - Blue skull key',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 382,
+ 'doom_type': 40,
+ 'region': "Monster Condo (MAP27) Yellow"},
+ 361388: {'name': 'Monster Condo (MAP27) - Supercharge 3',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 392,
+ 'doom_type': 2013,
+ 'region': "Monster Condo (MAP27) Yellow"},
+ 361389: {'name': 'Monster Condo (MAP27) - Computer area map',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 393,
+ 'doom_type': 2026,
+ 'region': "Monster Condo (MAP27) Yellow"},
+ 361390: {'name': 'Monster Condo (MAP27) - Berserk',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 394,
+ 'doom_type': 2023,
+ 'region': "Monster Condo (MAP27) Yellow"},
+ 361391: {'name': 'Monster Condo (MAP27) - Supercharge 4',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 414,
+ 'doom_type': 2013,
+ 'region': "Monster Condo (MAP27) Yellow"},
+ 361392: {'name': 'Monster Condo (MAP27) - Supercharge 5',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 424,
+ 'doom_type': 2013,
+ 'region': "Monster Condo (MAP27) Yellow"},
+ 361393: {'name': 'Monster Condo (MAP27) - Computer area map 2',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 425,
+ 'doom_type': 2026,
+ 'region': "Monster Condo (MAP27) Yellow"},
+ 361394: {'name': 'Monster Condo (MAP27) - Berserk 2',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 426,
+ 'doom_type': 2023,
+ 'region': "Monster Condo (MAP27) Yellow"},
+ 361395: {'name': 'Monster Condo (MAP27) - Partial invisibility 3',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 454,
+ 'doom_type': 2024,
+ 'region': "Monster Condo (MAP27) Yellow"},
+ 361396: {'name': 'Monster Condo (MAP27) - Invulnerability 3',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 455,
+ 'doom_type': 2022,
+ 'region': "Monster Condo (MAP27) Yellow"},
+ 361397: {'name': 'Monster Condo (MAP27) - Chainsaw',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 460,
+ 'doom_type': 2005,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361398: {'name': 'Monster Condo (MAP27) - Super Shotgun',
+ 'episode': 3,
+ 'map': 7,
+ 'index': 470,
+ 'doom_type': 82,
+ 'region': "Monster Condo (MAP27) Main"},
+ 361399: {'name': 'Monster Condo (MAP27) - Exit',
+ 'episode': 3,
+ 'map': 7,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Monster Condo (MAP27) Red"},
+ 361400: {'name': 'The Spirit World (MAP28) - Armor',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 19,
+ 'doom_type': 2018,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361401: {'name': 'The Spirit World (MAP28) - Chainsaw',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 66,
+ 'doom_type': 2005,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361402: {'name': 'The Spirit World (MAP28) - Invulnerability',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 76,
+ 'doom_type': 2022,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361403: {'name': 'The Spirit World (MAP28) - Yellow skull key',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 87,
+ 'doom_type': 39,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361404: {'name': 'The Spirit World (MAP28) - Supercharge',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 95,
+ 'doom_type': 2013,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361405: {'name': 'The Spirit World (MAP28) - Chaingun',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 96,
+ 'doom_type': 2002,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361406: {'name': 'The Spirit World (MAP28) - Rocket launcher',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 124,
+ 'doom_type': 2003,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361407: {'name': 'The Spirit World (MAP28) - Backpack',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 155,
+ 'doom_type': 8,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361408: {'name': 'The Spirit World (MAP28) - Backpack 2',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 156,
+ 'doom_type': 8,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361409: {'name': 'The Spirit World (MAP28) - Backpack 3',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 157,
+ 'doom_type': 8,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361410: {'name': 'The Spirit World (MAP28) - Backpack 4',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 158,
+ 'doom_type': 8,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361411: {'name': 'The Spirit World (MAP28) - Berserk',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 159,
+ 'doom_type': 2023,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361412: {'name': 'The Spirit World (MAP28) - Plasma gun',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 163,
+ 'doom_type': 2004,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361413: {'name': 'The Spirit World (MAP28) - Invulnerability 2',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 179,
+ 'doom_type': 2022,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361414: {'name': 'The Spirit World (MAP28) - Invulnerability 3',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 180,
+ 'doom_type': 2022,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361415: {'name': 'The Spirit World (MAP28) - BFG9000',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 181,
+ 'doom_type': 2006,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361416: {'name': 'The Spirit World (MAP28) - Megasphere',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 183,
+ 'doom_type': 83,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361417: {'name': 'The Spirit World (MAP28) - Megasphere 2',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 185,
+ 'doom_type': 83,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361418: {'name': 'The Spirit World (MAP28) - Invulnerability 4',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 186,
+ 'doom_type': 2022,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361419: {'name': 'The Spirit World (MAP28) - Invulnerability 5',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 195,
+ 'doom_type': 2022,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361420: {'name': 'The Spirit World (MAP28) - Super Shotgun',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 214,
+ 'doom_type': 82,
+ 'region': "The Spirit World (MAP28) Main"},
+ 361421: {'name': 'The Spirit World (MAP28) - Red skull key',
+ 'episode': 3,
+ 'map': 8,
+ 'index': 216,
+ 'doom_type': 38,
+ 'region': "The Spirit World (MAP28) Yellow"},
+ 361422: {'name': 'The Spirit World (MAP28) - Exit',
+ 'episode': 3,
+ 'map': 8,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Spirit World (MAP28) Red"},
+ 361423: {'name': 'The Living End (MAP29) - Chaingun',
+ 'episode': 3,
+ 'map': 9,
+ 'index': 85,
+ 'doom_type': 2002,
+ 'region': "The Living End (MAP29) Main"},
+ 361424: {'name': 'The Living End (MAP29) - Plasma gun',
+ 'episode': 3,
+ 'map': 9,
+ 'index': 124,
+ 'doom_type': 2004,
+ 'region': "The Living End (MAP29) Main"},
+ 361425: {'name': 'The Living End (MAP29) - Backpack',
+ 'episode': 3,
+ 'map': 9,
+ 'index': 179,
+ 'doom_type': 8,
+ 'region': "The Living End (MAP29) Main"},
+ 361426: {'name': 'The Living End (MAP29) - Super Shotgun',
+ 'episode': 3,
+ 'map': 9,
+ 'index': 195,
+ 'doom_type': 82,
+ 'region': "The Living End (MAP29) Main"},
+ 361427: {'name': 'The Living End (MAP29) - Mega Armor',
+ 'episode': 3,
+ 'map': 9,
+ 'index': 216,
+ 'doom_type': 2019,
+ 'region': "The Living End (MAP29) Main"},
+ 361428: {'name': 'The Living End (MAP29) - Armor',
+ 'episode': 3,
+ 'map': 9,
+ 'index': 224,
+ 'doom_type': 2018,
+ 'region': "The Living End (MAP29) Main"},
+ 361429: {'name': 'The Living End (MAP29) - Backpack 2',
+ 'episode': 3,
+ 'map': 9,
+ 'index': 235,
+ 'doom_type': 8,
+ 'region': "The Living End (MAP29) Main"},
+ 361430: {'name': 'The Living End (MAP29) - Supercharge',
+ 'episode': 3,
+ 'map': 9,
+ 'index': 237,
+ 'doom_type': 2013,
+ 'region': "The Living End (MAP29) Main"},
+ 361431: {'name': 'The Living End (MAP29) - Berserk',
+ 'episode': 3,
+ 'map': 9,
+ 'index': 241,
+ 'doom_type': 2023,
+ 'region': "The Living End (MAP29) Main"},
+ 361432: {'name': 'The Living End (MAP29) - Berserk 2',
+ 'episode': 3,
+ 'map': 9,
+ 'index': 263,
+ 'doom_type': 2023,
+ 'region': "The Living End (MAP29) Main"},
+ 361433: {'name': 'The Living End (MAP29) - Exit',
+ 'episode': 3,
+ 'map': 9,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Living End (MAP29) Main"},
+ 361434: {'name': 'Icon of Sin (MAP30) - Supercharge',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 25,
+ 'doom_type': 2013,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361435: {'name': 'Icon of Sin (MAP30) - Supercharge 2',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 26,
+ 'doom_type': 2013,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361436: {'name': 'Icon of Sin (MAP30) - Supercharge 3',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 28,
+ 'doom_type': 2013,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361437: {'name': 'Icon of Sin (MAP30) - Invulnerability',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 29,
+ 'doom_type': 2022,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361438: {'name': 'Icon of Sin (MAP30) - Invulnerability 2',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 30,
+ 'doom_type': 2022,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361439: {'name': 'Icon of Sin (MAP30) - Invulnerability 3',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 31,
+ 'doom_type': 2022,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361440: {'name': 'Icon of Sin (MAP30) - Invulnerability 4',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 32,
+ 'doom_type': 2022,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361441: {'name': 'Icon of Sin (MAP30) - BFG9000',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 40,
+ 'doom_type': 2006,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361442: {'name': 'Icon of Sin (MAP30) - Chaingun',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 41,
+ 'doom_type': 2002,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361443: {'name': 'Icon of Sin (MAP30) - Chainsaw',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 42,
+ 'doom_type': 2005,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361444: {'name': 'Icon of Sin (MAP30) - Plasma gun',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 43,
+ 'doom_type': 2004,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361445: {'name': 'Icon of Sin (MAP30) - Rocket launcher',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 44,
+ 'doom_type': 2003,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361446: {'name': 'Icon of Sin (MAP30) - Shotgun',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 45,
+ 'doom_type': 2001,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361447: {'name': 'Icon of Sin (MAP30) - Super Shotgun',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 46,
+ 'doom_type': 82,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361448: {'name': 'Icon of Sin (MAP30) - Backpack',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 47,
+ 'doom_type': 8,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361449: {'name': 'Icon of Sin (MAP30) - Megasphere',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 64,
+ 'doom_type': 83,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361450: {'name': 'Icon of Sin (MAP30) - Megasphere 2',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 85,
+ 'doom_type': 83,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361451: {'name': 'Icon of Sin (MAP30) - Berserk',
+ 'episode': 3,
+ 'map': 10,
+ 'index': 94,
+ 'doom_type': 2023,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361452: {'name': 'Icon of Sin (MAP30) - Exit',
+ 'episode': 3,
+ 'map': 10,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Icon of Sin (MAP30) Main"},
+ 361453: {'name': 'Wolfenstein2 (MAP31) - Rocket launcher',
+ 'episode': 4,
+ 'map': 1,
+ 'index': 110,
+ 'doom_type': 2003,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361454: {'name': 'Wolfenstein2 (MAP31) - Shotgun',
+ 'episode': 4,
+ 'map': 1,
+ 'index': 139,
+ 'doom_type': 2001,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361455: {'name': 'Wolfenstein2 (MAP31) - Berserk',
+ 'episode': 4,
+ 'map': 1,
+ 'index': 263,
+ 'doom_type': 2023,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361456: {'name': 'Wolfenstein2 (MAP31) - Supercharge',
+ 'episode': 4,
+ 'map': 1,
+ 'index': 278,
+ 'doom_type': 2013,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361457: {'name': 'Wolfenstein2 (MAP31) - Chaingun',
+ 'episode': 4,
+ 'map': 1,
+ 'index': 305,
+ 'doom_type': 2002,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361458: {'name': 'Wolfenstein2 (MAP31) - Super Shotgun',
+ 'episode': 4,
+ 'map': 1,
+ 'index': 308,
+ 'doom_type': 82,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361459: {'name': 'Wolfenstein2 (MAP31) - Partial invisibility',
+ 'episode': 4,
+ 'map': 1,
+ 'index': 309,
+ 'doom_type': 2024,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361460: {'name': 'Wolfenstein2 (MAP31) - Megasphere',
+ 'episode': 4,
+ 'map': 1,
+ 'index': 310,
+ 'doom_type': 83,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361461: {'name': 'Wolfenstein2 (MAP31) - Backpack',
+ 'episode': 4,
+ 'map': 1,
+ 'index': 311,
+ 'doom_type': 8,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361462: {'name': 'Wolfenstein2 (MAP31) - Backpack 2',
+ 'episode': 4,
+ 'map': 1,
+ 'index': 312,
+ 'doom_type': 8,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361463: {'name': 'Wolfenstein2 (MAP31) - Backpack 3',
+ 'episode': 4,
+ 'map': 1,
+ 'index': 313,
+ 'doom_type': 8,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361464: {'name': 'Wolfenstein2 (MAP31) - Backpack 4',
+ 'episode': 4,
+ 'map': 1,
+ 'index': 314,
+ 'doom_type': 8,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361465: {'name': 'Wolfenstein2 (MAP31) - BFG9000',
+ 'episode': 4,
+ 'map': 1,
+ 'index': 315,
+ 'doom_type': 2006,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361466: {'name': 'Wolfenstein2 (MAP31) - Plasma gun',
+ 'episode': 4,
+ 'map': 1,
+ 'index': 316,
+ 'doom_type': 2004,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361467: {'name': 'Wolfenstein2 (MAP31) - Exit',
+ 'episode': 4,
+ 'map': 1,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Wolfenstein2 (MAP31) Main"},
+ 361468: {'name': 'Grosse2 (MAP32) - Plasma gun',
+ 'episode': 4,
+ 'map': 2,
+ 'index': 33,
+ 'doom_type': 2004,
+ 'region': "Grosse2 (MAP32) Main"},
+ 361469: {'name': 'Grosse2 (MAP32) - Rocket launcher',
+ 'episode': 4,
+ 'map': 2,
+ 'index': 57,
+ 'doom_type': 2003,
+ 'region': "Grosse2 (MAP32) Main"},
+ 361470: {'name': 'Grosse2 (MAP32) - Invulnerability',
+ 'episode': 4,
+ 'map': 2,
+ 'index': 70,
+ 'doom_type': 2022,
+ 'region': "Grosse2 (MAP32) Main"},
+ 361471: {'name': 'Grosse2 (MAP32) - Super Shotgun',
+ 'episode': 4,
+ 'map': 2,
+ 'index': 74,
+ 'doom_type': 82,
+ 'region': "Grosse2 (MAP32) Main"},
+ 361472: {'name': 'Grosse2 (MAP32) - BFG9000',
+ 'episode': 4,
+ 'map': 2,
+ 'index': 75,
+ 'doom_type': 2006,
+ 'region': "Grosse2 (MAP32) Main"},
+ 361473: {'name': 'Grosse2 (MAP32) - Megasphere',
+ 'episode': 4,
+ 'map': 2,
+ 'index': 78,
+ 'doom_type': 83,
+ 'region': "Grosse2 (MAP32) Main"},
+ 361474: {'name': 'Grosse2 (MAP32) - Chaingun',
+ 'episode': 4,
+ 'map': 2,
+ 'index': 79,
+ 'doom_type': 2002,
+ 'region': "Grosse2 (MAP32) Main"},
+ 361475: {'name': 'Grosse2 (MAP32) - Chaingun 2',
+ 'episode': 4,
+ 'map': 2,
+ 'index': 80,
+ 'doom_type': 2002,
+ 'region': "Grosse2 (MAP32) Main"},
+ 361476: {'name': 'Grosse2 (MAP32) - Chaingun 3',
+ 'episode': 4,
+ 'map': 2,
+ 'index': 81,
+ 'doom_type': 2002,
+ 'region': "Grosse2 (MAP32) Main"},
+ 361477: {'name': 'Grosse2 (MAP32) - Berserk',
+ 'episode': 4,
+ 'map': 2,
+ 'index': 82,
+ 'doom_type': 2023,
+ 'region': "Grosse2 (MAP32) Main"},
+ 361478: {'name': 'Grosse2 (MAP32) - Exit',
+ 'episode': 4,
+ 'map': 2,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Grosse2 (MAP32) Main"},
+}
+
+
+location_name_groups: Dict[str, Set[str]] = {
+ 'Barrels o Fun (MAP23)': {
+ 'Barrels o Fun (MAP23) - Armor',
+ 'Barrels o Fun (MAP23) - BFG9000',
+ 'Barrels o Fun (MAP23) - Backpack',
+ 'Barrels o Fun (MAP23) - Backpack 2',
+ 'Barrels o Fun (MAP23) - Berserk',
+ 'Barrels o Fun (MAP23) - Computer area map',
+ 'Barrels o Fun (MAP23) - Exit',
+ 'Barrels o Fun (MAP23) - Megasphere',
+ 'Barrels o Fun (MAP23) - Rocket launcher',
+ 'Barrels o Fun (MAP23) - Shotgun',
+ 'Barrels o Fun (MAP23) - Supercharge',
+ 'Barrels o Fun (MAP23) - Yellow skull key',
+ },
+ 'Bloodfalls (MAP25)': {
+ 'Bloodfalls (MAP25) - Armor',
+ 'Bloodfalls (MAP25) - BFG9000',
+ 'Bloodfalls (MAP25) - BFG9000 2',
+ 'Bloodfalls (MAP25) - Blue skull key',
+ 'Bloodfalls (MAP25) - Chaingun',
+ 'Bloodfalls (MAP25) - Exit',
+ 'Bloodfalls (MAP25) - Mega Armor',
+ 'Bloodfalls (MAP25) - Megasphere',
+ 'Bloodfalls (MAP25) - Partial invisibility',
+ 'Bloodfalls (MAP25) - Plasma gun',
+ 'Bloodfalls (MAP25) - Rocket launcher',
+ 'Bloodfalls (MAP25) - Super Shotgun',
+ },
+ 'Circle of Death (MAP11)': {
+ 'Circle of Death (MAP11) - Armor',
+ 'Circle of Death (MAP11) - BFG9000',
+ 'Circle of Death (MAP11) - Backpack',
+ 'Circle of Death (MAP11) - Blue keycard',
+ 'Circle of Death (MAP11) - Chaingun',
+ 'Circle of Death (MAP11) - Exit',
+ 'Circle of Death (MAP11) - Invulnerability',
+ 'Circle of Death (MAP11) - Mega Armor',
+ 'Circle of Death (MAP11) - Partial invisibility',
+ 'Circle of Death (MAP11) - Plasma gun',
+ 'Circle of Death (MAP11) - Red keycard',
+ 'Circle of Death (MAP11) - Rocket launcher',
+ 'Circle of Death (MAP11) - Shotgun',
+ 'Circle of Death (MAP11) - Supercharge',
+ 'Circle of Death (MAP11) - Supercharge 2',
+ },
+ 'Dead Simple (MAP07)': {
+ 'Dead Simple (MAP07) - Backpack',
+ 'Dead Simple (MAP07) - Berserk',
+ 'Dead Simple (MAP07) - Chaingun',
+ 'Dead Simple (MAP07) - Exit',
+ 'Dead Simple (MAP07) - Megasphere',
+ 'Dead Simple (MAP07) - Partial invisibility',
+ 'Dead Simple (MAP07) - Partial invisibility 2',
+ 'Dead Simple (MAP07) - Partial invisibility 3',
+ 'Dead Simple (MAP07) - Partial invisibility 4',
+ 'Dead Simple (MAP07) - Plasma gun',
+ 'Dead Simple (MAP07) - Rocket launcher',
+ 'Dead Simple (MAP07) - Super Shotgun',
+ },
+ 'Downtown (MAP13)': {
+ 'Downtown (MAP13) - BFG9000',
+ 'Downtown (MAP13) - Backpack',
+ 'Downtown (MAP13) - Berserk',
+ 'Downtown (MAP13) - Berserk 2',
+ 'Downtown (MAP13) - Berserk 3',
+ 'Downtown (MAP13) - Blue keycard',
+ 'Downtown (MAP13) - Chaingun',
+ 'Downtown (MAP13) - Chainsaw',
+ 'Downtown (MAP13) - Computer area map',
+ 'Downtown (MAP13) - Exit',
+ 'Downtown (MAP13) - Invulnerability',
+ 'Downtown (MAP13) - Invulnerability 2',
+ 'Downtown (MAP13) - Mega Armor',
+ 'Downtown (MAP13) - Partial invisibility',
+ 'Downtown (MAP13) - Partial invisibility 2',
+ 'Downtown (MAP13) - Partial invisibility 3',
+ 'Downtown (MAP13) - Plasma gun',
+ 'Downtown (MAP13) - Red keycard',
+ 'Downtown (MAP13) - Rocket launcher',
+ 'Downtown (MAP13) - Shotgun',
+ 'Downtown (MAP13) - Supercharge',
+ 'Downtown (MAP13) - Yellow keycard',
+ },
+ 'Entryway (MAP01)': {
+ 'Entryway (MAP01) - Armor',
+ 'Entryway (MAP01) - Chainsaw',
+ 'Entryway (MAP01) - Exit',
+ 'Entryway (MAP01) - Rocket launcher',
+ 'Entryway (MAP01) - Shotgun',
+ },
+ 'Gotcha! (MAP20)': {
+ 'Gotcha! (MAP20) - Armor',
+ 'Gotcha! (MAP20) - Armor 2',
+ 'Gotcha! (MAP20) - BFG9000',
+ 'Gotcha! (MAP20) - Berserk',
+ 'Gotcha! (MAP20) - Exit',
+ 'Gotcha! (MAP20) - Mega Armor',
+ 'Gotcha! (MAP20) - Mega Armor 2',
+ 'Gotcha! (MAP20) - Megasphere',
+ 'Gotcha! (MAP20) - Plasma gun',
+ 'Gotcha! (MAP20) - Rocket launcher',
+ 'Gotcha! (MAP20) - Super Shotgun',
+ 'Gotcha! (MAP20) - Supercharge',
+ 'Gotcha! (MAP20) - Supercharge 2',
+ 'Gotcha! (MAP20) - Supercharge 3',
+ 'Gotcha! (MAP20) - Supercharge 4',
+ },
+ 'Grosse2 (MAP32)': {
+ 'Grosse2 (MAP32) - BFG9000',
+ 'Grosse2 (MAP32) - Berserk',
+ 'Grosse2 (MAP32) - Chaingun',
+ 'Grosse2 (MAP32) - Chaingun 2',
+ 'Grosse2 (MAP32) - Chaingun 3',
+ 'Grosse2 (MAP32) - Exit',
+ 'Grosse2 (MAP32) - Invulnerability',
+ 'Grosse2 (MAP32) - Megasphere',
+ 'Grosse2 (MAP32) - Plasma gun',
+ 'Grosse2 (MAP32) - Rocket launcher',
+ 'Grosse2 (MAP32) - Super Shotgun',
+ },
+ 'Icon of Sin (MAP30)': {
+ 'Icon of Sin (MAP30) - BFG9000',
+ 'Icon of Sin (MAP30) - Backpack',
+ 'Icon of Sin (MAP30) - Berserk',
+ 'Icon of Sin (MAP30) - Chaingun',
+ 'Icon of Sin (MAP30) - Chainsaw',
+ 'Icon of Sin (MAP30) - Exit',
+ 'Icon of Sin (MAP30) - Invulnerability',
+ 'Icon of Sin (MAP30) - Invulnerability 2',
+ 'Icon of Sin (MAP30) - Invulnerability 3',
+ 'Icon of Sin (MAP30) - Invulnerability 4',
+ 'Icon of Sin (MAP30) - Megasphere',
+ 'Icon of Sin (MAP30) - Megasphere 2',
+ 'Icon of Sin (MAP30) - Plasma gun',
+ 'Icon of Sin (MAP30) - Rocket launcher',
+ 'Icon of Sin (MAP30) - Shotgun',
+ 'Icon of Sin (MAP30) - Super Shotgun',
+ 'Icon of Sin (MAP30) - Supercharge',
+ 'Icon of Sin (MAP30) - Supercharge 2',
+ 'Icon of Sin (MAP30) - Supercharge 3',
+ },
+ 'Industrial Zone (MAP15)': {
+ 'Industrial Zone (MAP15) - Armor',
+ 'Industrial Zone (MAP15) - BFG9000',
+ 'Industrial Zone (MAP15) - Backpack',
+ 'Industrial Zone (MAP15) - Backpack 2',
+ 'Industrial Zone (MAP15) - Berserk',
+ 'Industrial Zone (MAP15) - Berserk 2',
+ 'Industrial Zone (MAP15) - Blue keycard',
+ 'Industrial Zone (MAP15) - Chaingun',
+ 'Industrial Zone (MAP15) - Chainsaw',
+ 'Industrial Zone (MAP15) - Computer area map',
+ 'Industrial Zone (MAP15) - Exit',
+ 'Industrial Zone (MAP15) - Invulnerability',
+ 'Industrial Zone (MAP15) - Mega Armor',
+ 'Industrial Zone (MAP15) - Mega Armor 2',
+ 'Industrial Zone (MAP15) - Megasphere',
+ 'Industrial Zone (MAP15) - Partial invisibility',
+ 'Industrial Zone (MAP15) - Partial invisibility 2',
+ 'Industrial Zone (MAP15) - Plasma gun',
+ 'Industrial Zone (MAP15) - Red keycard',
+ 'Industrial Zone (MAP15) - Rocket launcher',
+ 'Industrial Zone (MAP15) - Shotgun',
+ 'Industrial Zone (MAP15) - Supercharge',
+ 'Industrial Zone (MAP15) - Yellow keycard',
+ },
+ 'Monster Condo (MAP27)': {
+ 'Monster Condo (MAP27) - Armor',
+ 'Monster Condo (MAP27) - Armor 2',
+ 'Monster Condo (MAP27) - BFG9000',
+ 'Monster Condo (MAP27) - Backpack',
+ 'Monster Condo (MAP27) - Backpack 2',
+ 'Monster Condo (MAP27) - Backpack 3',
+ 'Monster Condo (MAP27) - Backpack 4',
+ 'Monster Condo (MAP27) - Berserk',
+ 'Monster Condo (MAP27) - Berserk 2',
+ 'Monster Condo (MAP27) - Blue skull key',
+ 'Monster Condo (MAP27) - Chaingun',
+ 'Monster Condo (MAP27) - Chainsaw',
+ 'Monster Condo (MAP27) - Computer area map',
+ 'Monster Condo (MAP27) - Computer area map 2',
+ 'Monster Condo (MAP27) - Exit',
+ 'Monster Condo (MAP27) - Invulnerability',
+ 'Monster Condo (MAP27) - Invulnerability 2',
+ 'Monster Condo (MAP27) - Invulnerability 3',
+ 'Monster Condo (MAP27) - Partial invisibility',
+ 'Monster Condo (MAP27) - Partial invisibility 2',
+ 'Monster Condo (MAP27) - Partial invisibility 3',
+ 'Monster Condo (MAP27) - Plasma gun',
+ 'Monster Condo (MAP27) - Red skull key',
+ 'Monster Condo (MAP27) - Rocket launcher',
+ 'Monster Condo (MAP27) - Super Shotgun',
+ 'Monster Condo (MAP27) - Supercharge',
+ 'Monster Condo (MAP27) - Supercharge 2',
+ 'Monster Condo (MAP27) - Supercharge 3',
+ 'Monster Condo (MAP27) - Supercharge 4',
+ 'Monster Condo (MAP27) - Supercharge 5',
+ 'Monster Condo (MAP27) - Yellow skull key',
+ },
+ 'Nirvana (MAP21)': {
+ 'Nirvana (MAP21) - Backpack',
+ 'Nirvana (MAP21) - Blue skull key',
+ 'Nirvana (MAP21) - Exit',
+ 'Nirvana (MAP21) - Invulnerability',
+ 'Nirvana (MAP21) - Megasphere',
+ 'Nirvana (MAP21) - Red skull key',
+ 'Nirvana (MAP21) - Rocket launcher',
+ 'Nirvana (MAP21) - Super Shotgun',
+ 'Nirvana (MAP21) - Yellow skull key',
+ },
+ 'Refueling Base (MAP10)': {
+ 'Refueling Base (MAP10) - Armor',
+ 'Refueling Base (MAP10) - Armor 2',
+ 'Refueling Base (MAP10) - BFG9000',
+ 'Refueling Base (MAP10) - Backpack',
+ 'Refueling Base (MAP10) - Berserk',
+ 'Refueling Base (MAP10) - Berserk 2',
+ 'Refueling Base (MAP10) - Blue keycard',
+ 'Refueling Base (MAP10) - Chaingun',
+ 'Refueling Base (MAP10) - Chainsaw',
+ 'Refueling Base (MAP10) - Exit',
+ 'Refueling Base (MAP10) - Invulnerability',
+ 'Refueling Base (MAP10) - Invulnerability 2',
+ 'Refueling Base (MAP10) - Mega Armor',
+ 'Refueling Base (MAP10) - Megasphere',
+ 'Refueling Base (MAP10) - Partial invisibility',
+ 'Refueling Base (MAP10) - Plasma gun',
+ 'Refueling Base (MAP10) - Rocket launcher',
+ 'Refueling Base (MAP10) - Shotgun',
+ 'Refueling Base (MAP10) - Supercharge',
+ 'Refueling Base (MAP10) - Supercharge 2',
+ 'Refueling Base (MAP10) - Yellow keycard',
+ },
+ 'Suburbs (MAP16)': {
+ 'Suburbs (MAP16) - BFG9000',
+ 'Suburbs (MAP16) - Backpack',
+ 'Suburbs (MAP16) - Berserk',
+ 'Suburbs (MAP16) - Blue skull key',
+ 'Suburbs (MAP16) - Chaingun',
+ 'Suburbs (MAP16) - Exit',
+ 'Suburbs (MAP16) - Invulnerability',
+ 'Suburbs (MAP16) - Megasphere',
+ 'Suburbs (MAP16) - Partial invisibility',
+ 'Suburbs (MAP16) - Plasma gun',
+ 'Suburbs (MAP16) - Plasma gun 2',
+ 'Suburbs (MAP16) - Plasma gun 3',
+ 'Suburbs (MAP16) - Plasma gun 4',
+ 'Suburbs (MAP16) - Red skull key',
+ 'Suburbs (MAP16) - Rocket launcher',
+ 'Suburbs (MAP16) - Shotgun',
+ 'Suburbs (MAP16) - Super Shotgun',
+ 'Suburbs (MAP16) - Supercharge',
+ },
+ 'Tenements (MAP17)': {
+ 'Tenements (MAP17) - Armor',
+ 'Tenements (MAP17) - Armor 2',
+ 'Tenements (MAP17) - BFG9000',
+ 'Tenements (MAP17) - Backpack',
+ 'Tenements (MAP17) - Berserk',
+ 'Tenements (MAP17) - Blue keycard',
+ 'Tenements (MAP17) - Chaingun',
+ 'Tenements (MAP17) - Exit',
+ 'Tenements (MAP17) - Mega Armor',
+ 'Tenements (MAP17) - Megasphere',
+ 'Tenements (MAP17) - Partial invisibility',
+ 'Tenements (MAP17) - Plasma gun',
+ 'Tenements (MAP17) - Red keycard',
+ 'Tenements (MAP17) - Rocket launcher',
+ 'Tenements (MAP17) - Shotgun',
+ 'Tenements (MAP17) - Supercharge',
+ 'Tenements (MAP17) - Supercharge 2',
+ 'Tenements (MAP17) - Yellow skull key',
+ },
+ 'The Abandoned Mines (MAP26)': {
+ 'The Abandoned Mines (MAP26) - Armor',
+ 'The Abandoned Mines (MAP26) - Backpack',
+ 'The Abandoned Mines (MAP26) - Blue keycard',
+ 'The Abandoned Mines (MAP26) - Chaingun',
+ 'The Abandoned Mines (MAP26) - Exit',
+ 'The Abandoned Mines (MAP26) - Mega Armor',
+ 'The Abandoned Mines (MAP26) - Partial invisibility',
+ 'The Abandoned Mines (MAP26) - Plasma gun',
+ 'The Abandoned Mines (MAP26) - Red keycard',
+ 'The Abandoned Mines (MAP26) - Rocket launcher',
+ 'The Abandoned Mines (MAP26) - Super Shotgun',
+ 'The Abandoned Mines (MAP26) - Supercharge',
+ 'The Abandoned Mines (MAP26) - Yellow keycard',
+ },
+ 'The Catacombs (MAP22)': {
+ 'The Catacombs (MAP22) - Armor',
+ 'The Catacombs (MAP22) - Berserk',
+ 'The Catacombs (MAP22) - Blue skull key',
+ 'The Catacombs (MAP22) - Exit',
+ 'The Catacombs (MAP22) - Plasma gun',
+ 'The Catacombs (MAP22) - Red skull key',
+ 'The Catacombs (MAP22) - Rocket launcher',
+ 'The Catacombs (MAP22) - Shotgun',
+ 'The Catacombs (MAP22) - Supercharge',
+ },
+ 'The Chasm (MAP24)': {
+ 'The Chasm (MAP24) - Armor',
+ 'The Chasm (MAP24) - BFG9000',
+ 'The Chasm (MAP24) - Backpack',
+ 'The Chasm (MAP24) - Berserk',
+ 'The Chasm (MAP24) - Berserk 2',
+ 'The Chasm (MAP24) - Blue keycard',
+ 'The Chasm (MAP24) - Exit',
+ 'The Chasm (MAP24) - Invulnerability',
+ 'The Chasm (MAP24) - Megasphere',
+ 'The Chasm (MAP24) - Megasphere 2',
+ 'The Chasm (MAP24) - Plasma gun',
+ 'The Chasm (MAP24) - Red keycard',
+ 'The Chasm (MAP24) - Rocket launcher',
+ 'The Chasm (MAP24) - Shotgun',
+ 'The Chasm (MAP24) - Super Shotgun',
+ },
+ 'The Citadel (MAP19)': {
+ 'The Citadel (MAP19) - Armor',
+ 'The Citadel (MAP19) - Armor 2',
+ 'The Citadel (MAP19) - Backpack',
+ 'The Citadel (MAP19) - Berserk',
+ 'The Citadel (MAP19) - Blue skull key',
+ 'The Citadel (MAP19) - Chaingun',
+ 'The Citadel (MAP19) - Computer area map',
+ 'The Citadel (MAP19) - Exit',
+ 'The Citadel (MAP19) - Invulnerability',
+ 'The Citadel (MAP19) - Mega Armor',
+ 'The Citadel (MAP19) - Partial invisibility',
+ 'The Citadel (MAP19) - Red skull key',
+ 'The Citadel (MAP19) - Rocket launcher',
+ 'The Citadel (MAP19) - Super Shotgun',
+ 'The Citadel (MAP19) - Supercharge',
+ 'The Citadel (MAP19) - Yellow skull key',
+ },
+ 'The Courtyard (MAP18)': {
+ 'The Courtyard (MAP18) - Armor',
+ 'The Courtyard (MAP18) - BFG9000',
+ 'The Courtyard (MAP18) - Backpack',
+ 'The Courtyard (MAP18) - Berserk',
+ 'The Courtyard (MAP18) - Blue skull key',
+ 'The Courtyard (MAP18) - Chaingun',
+ 'The Courtyard (MAP18) - Computer area map',
+ 'The Courtyard (MAP18) - Exit',
+ 'The Courtyard (MAP18) - Invulnerability',
+ 'The Courtyard (MAP18) - Invulnerability 2',
+ 'The Courtyard (MAP18) - Partial invisibility',
+ 'The Courtyard (MAP18) - Partial invisibility 2',
+ 'The Courtyard (MAP18) - Plasma gun',
+ 'The Courtyard (MAP18) - Rocket launcher',
+ 'The Courtyard (MAP18) - Shotgun',
+ 'The Courtyard (MAP18) - Super Shotgun',
+ 'The Courtyard (MAP18) - Supercharge',
+ 'The Courtyard (MAP18) - Yellow skull key',
+ },
+ 'The Crusher (MAP06)': {
+ 'The Crusher (MAP06) - Armor',
+ 'The Crusher (MAP06) - Backpack',
+ 'The Crusher (MAP06) - Blue keycard',
+ 'The Crusher (MAP06) - Blue keycard 2',
+ 'The Crusher (MAP06) - Blue keycard 3',
+ 'The Crusher (MAP06) - Exit',
+ 'The Crusher (MAP06) - Mega Armor',
+ 'The Crusher (MAP06) - Megasphere',
+ 'The Crusher (MAP06) - Megasphere 2',
+ 'The Crusher (MAP06) - Plasma gun',
+ 'The Crusher (MAP06) - Red keycard',
+ 'The Crusher (MAP06) - Rocket launcher',
+ 'The Crusher (MAP06) - Super Shotgun',
+ 'The Crusher (MAP06) - Supercharge',
+ 'The Crusher (MAP06) - Yellow keycard',
+ },
+ 'The Factory (MAP12)': {
+ 'The Factory (MAP12) - Armor',
+ 'The Factory (MAP12) - Armor 2',
+ 'The Factory (MAP12) - BFG9000',
+ 'The Factory (MAP12) - Backpack',
+ 'The Factory (MAP12) - Berserk',
+ 'The Factory (MAP12) - Berserk 2',
+ 'The Factory (MAP12) - Berserk 3',
+ 'The Factory (MAP12) - Blue keycard',
+ 'The Factory (MAP12) - Chaingun',
+ 'The Factory (MAP12) - Exit',
+ 'The Factory (MAP12) - Partial invisibility',
+ 'The Factory (MAP12) - Shotgun',
+ 'The Factory (MAP12) - Super Shotgun',
+ 'The Factory (MAP12) - Supercharge',
+ 'The Factory (MAP12) - Supercharge 2',
+ 'The Factory (MAP12) - Yellow keycard',
+ },
+ 'The Focus (MAP04)': {
+ 'The Focus (MAP04) - Blue keycard',
+ 'The Focus (MAP04) - Exit',
+ 'The Focus (MAP04) - Red keycard',
+ 'The Focus (MAP04) - Super Shotgun',
+ 'The Focus (MAP04) - Yellow keycard',
+ },
+ 'The Gantlet (MAP03)': {
+ 'The Gantlet (MAP03) - Backpack',
+ 'The Gantlet (MAP03) - Blue keycard',
+ 'The Gantlet (MAP03) - Chaingun',
+ 'The Gantlet (MAP03) - Exit',
+ 'The Gantlet (MAP03) - Mega Armor',
+ 'The Gantlet (MAP03) - Mega Armor 2',
+ 'The Gantlet (MAP03) - Partial invisibility',
+ 'The Gantlet (MAP03) - Red keycard',
+ 'The Gantlet (MAP03) - Rocket launcher',
+ 'The Gantlet (MAP03) - Shotgun',
+ 'The Gantlet (MAP03) - Supercharge',
+ },
+ 'The Inmost Dens (MAP14)': {
+ 'The Inmost Dens (MAP14) - Berserk',
+ 'The Inmost Dens (MAP14) - Blue skull key',
+ 'The Inmost Dens (MAP14) - Chaingun',
+ 'The Inmost Dens (MAP14) - Exit',
+ 'The Inmost Dens (MAP14) - Mega Armor',
+ 'The Inmost Dens (MAP14) - Partial invisibility',
+ 'The Inmost Dens (MAP14) - Plasma gun',
+ 'The Inmost Dens (MAP14) - Red skull key',
+ 'The Inmost Dens (MAP14) - Rocket launcher',
+ 'The Inmost Dens (MAP14) - Shotgun',
+ 'The Inmost Dens (MAP14) - Supercharge',
+ },
+ 'The Living End (MAP29)': {
+ 'The Living End (MAP29) - Armor',
+ 'The Living End (MAP29) - Backpack',
+ 'The Living End (MAP29) - Backpack 2',
+ 'The Living End (MAP29) - Berserk',
+ 'The Living End (MAP29) - Berserk 2',
+ 'The Living End (MAP29) - Chaingun',
+ 'The Living End (MAP29) - Exit',
+ 'The Living End (MAP29) - Mega Armor',
+ 'The Living End (MAP29) - Plasma gun',
+ 'The Living End (MAP29) - Super Shotgun',
+ 'The Living End (MAP29) - Supercharge',
+ },
+ 'The Pit (MAP09)': {
+ 'The Pit (MAP09) - Armor',
+ 'The Pit (MAP09) - BFG9000',
+ 'The Pit (MAP09) - Backpack',
+ 'The Pit (MAP09) - Berserk',
+ 'The Pit (MAP09) - Berserk 2',
+ 'The Pit (MAP09) - Berserk 3',
+ 'The Pit (MAP09) - Blue keycard',
+ 'The Pit (MAP09) - Computer area map',
+ 'The Pit (MAP09) - Exit',
+ 'The Pit (MAP09) - Mega Armor',
+ 'The Pit (MAP09) - Mega Armor 2',
+ 'The Pit (MAP09) - Rocket launcher',
+ 'The Pit (MAP09) - Shotgun',
+ 'The Pit (MAP09) - Supercharge',
+ 'The Pit (MAP09) - Supercharge 2',
+ 'The Pit (MAP09) - Yellow keycard',
+ },
+ 'The Spirit World (MAP28)': {
+ 'The Spirit World (MAP28) - Armor',
+ 'The Spirit World (MAP28) - BFG9000',
+ 'The Spirit World (MAP28) - Backpack',
+ 'The Spirit World (MAP28) - Backpack 2',
+ 'The Spirit World (MAP28) - Backpack 3',
+ 'The Spirit World (MAP28) - Backpack 4',
+ 'The Spirit World (MAP28) - Berserk',
+ 'The Spirit World (MAP28) - Chaingun',
+ 'The Spirit World (MAP28) - Chainsaw',
+ 'The Spirit World (MAP28) - Exit',
+ 'The Spirit World (MAP28) - Invulnerability',
+ 'The Spirit World (MAP28) - Invulnerability 2',
+ 'The Spirit World (MAP28) - Invulnerability 3',
+ 'The Spirit World (MAP28) - Invulnerability 4',
+ 'The Spirit World (MAP28) - Invulnerability 5',
+ 'The Spirit World (MAP28) - Megasphere',
+ 'The Spirit World (MAP28) - Megasphere 2',
+ 'The Spirit World (MAP28) - Plasma gun',
+ 'The Spirit World (MAP28) - Red skull key',
+ 'The Spirit World (MAP28) - Rocket launcher',
+ 'The Spirit World (MAP28) - Super Shotgun',
+ 'The Spirit World (MAP28) - Supercharge',
+ 'The Spirit World (MAP28) - Yellow skull key',
+ },
+ 'The Waste Tunnels (MAP05)': {
+ 'The Waste Tunnels (MAP05) - Armor',
+ 'The Waste Tunnels (MAP05) - Berserk',
+ 'The Waste Tunnels (MAP05) - Blue keycard',
+ 'The Waste Tunnels (MAP05) - Exit',
+ 'The Waste Tunnels (MAP05) - Mega Armor',
+ 'The Waste Tunnels (MAP05) - Plasma gun',
+ 'The Waste Tunnels (MAP05) - Red keycard',
+ 'The Waste Tunnels (MAP05) - Rocket launcher',
+ 'The Waste Tunnels (MAP05) - Shotgun',
+ 'The Waste Tunnels (MAP05) - Super Shotgun',
+ 'The Waste Tunnels (MAP05) - Supercharge',
+ 'The Waste Tunnels (MAP05) - Supercharge 2',
+ 'The Waste Tunnels (MAP05) - Yellow keycard',
+ },
+ 'Tricks and Traps (MAP08)': {
+ 'Tricks and Traps (MAP08) - Armor',
+ 'Tricks and Traps (MAP08) - Armor 2',
+ 'Tricks and Traps (MAP08) - BFG9000',
+ 'Tricks and Traps (MAP08) - Backpack',
+ 'Tricks and Traps (MAP08) - Backpack 2',
+ 'Tricks and Traps (MAP08) - Backpack 3',
+ 'Tricks and Traps (MAP08) - Backpack 4',
+ 'Tricks and Traps (MAP08) - Backpack 5',
+ 'Tricks and Traps (MAP08) - Chaingun',
+ 'Tricks and Traps (MAP08) - Chainsaw',
+ 'Tricks and Traps (MAP08) - Exit',
+ 'Tricks and Traps (MAP08) - Invulnerability',
+ 'Tricks and Traps (MAP08) - Invulnerability 2',
+ 'Tricks and Traps (MAP08) - Invulnerability 3',
+ 'Tricks and Traps (MAP08) - Invulnerability 4',
+ 'Tricks and Traps (MAP08) - Invulnerability 5',
+ 'Tricks and Traps (MAP08) - Partial invisibility',
+ 'Tricks and Traps (MAP08) - Plasma gun',
+ 'Tricks and Traps (MAP08) - Red skull key',
+ 'Tricks and Traps (MAP08) - Rocket launcher',
+ 'Tricks and Traps (MAP08) - Shotgun',
+ 'Tricks and Traps (MAP08) - Supercharge',
+ 'Tricks and Traps (MAP08) - Supercharge 2',
+ 'Tricks and Traps (MAP08) - Yellow skull key',
+ },
+ 'Underhalls (MAP02)': {
+ 'Underhalls (MAP02) - Blue keycard',
+ 'Underhalls (MAP02) - Exit',
+ 'Underhalls (MAP02) - Mega Armor',
+ 'Underhalls (MAP02) - Red keycard',
+ 'Underhalls (MAP02) - Super Shotgun',
+ },
+ 'Wolfenstein2 (MAP31)': {
+ 'Wolfenstein2 (MAP31) - BFG9000',
+ 'Wolfenstein2 (MAP31) - Backpack',
+ 'Wolfenstein2 (MAP31) - Backpack 2',
+ 'Wolfenstein2 (MAP31) - Backpack 3',
+ 'Wolfenstein2 (MAP31) - Backpack 4',
+ 'Wolfenstein2 (MAP31) - Berserk',
+ 'Wolfenstein2 (MAP31) - Chaingun',
+ 'Wolfenstein2 (MAP31) - Exit',
+ 'Wolfenstein2 (MAP31) - Megasphere',
+ 'Wolfenstein2 (MAP31) - Partial invisibility',
+ 'Wolfenstein2 (MAP31) - Plasma gun',
+ 'Wolfenstein2 (MAP31) - Rocket launcher',
+ 'Wolfenstein2 (MAP31) - Shotgun',
+ 'Wolfenstein2 (MAP31) - Super Shotgun',
+ 'Wolfenstein2 (MAP31) - Supercharge',
+ },
+}
+
+
+death_logic_locations = [
+ "Entryway (MAP01) - Armor",
+]
diff --git a/worlds/doom_ii/Maps.py b/worlds/doom_ii/Maps.py
new file mode 100644
index 000000000000..cf41939fa513
--- /dev/null
+++ b/worlds/doom_ii/Maps.py
@@ -0,0 +1,39 @@
+# This file is auto generated. More info: https://github.com/Daivuk/apdoom
+
+from typing import List
+
+
+map_names: List[str] = [
+ 'Entryway (MAP01)',
+ 'Underhalls (MAP02)',
+ 'The Gantlet (MAP03)',
+ 'The Focus (MAP04)',
+ 'The Waste Tunnels (MAP05)',
+ 'The Crusher (MAP06)',
+ 'Dead Simple (MAP07)',
+ 'Tricks and Traps (MAP08)',
+ 'The Pit (MAP09)',
+ 'Refueling Base (MAP10)',
+ 'Circle of Death (MAP11)',
+ 'The Factory (MAP12)',
+ 'Downtown (MAP13)',
+ 'The Inmost Dens (MAP14)',
+ 'Industrial Zone (MAP15)',
+ 'Suburbs (MAP16)',
+ 'Tenements (MAP17)',
+ 'The Courtyard (MAP18)',
+ 'The Citadel (MAP19)',
+ 'Gotcha! (MAP20)',
+ 'Nirvana (MAP21)',
+ 'The Catacombs (MAP22)',
+ 'Barrels o Fun (MAP23)',
+ 'The Chasm (MAP24)',
+ 'Bloodfalls (MAP25)',
+ 'The Abandoned Mines (MAP26)',
+ 'Monster Condo (MAP27)',
+ 'The Spirit World (MAP28)',
+ 'The Living End (MAP29)',
+ 'Icon of Sin (MAP30)',
+ 'Wolfenstein2 (MAP31)',
+ 'Grosse2 (MAP32)',
+]
diff --git a/worlds/doom_ii/Options.py b/worlds/doom_ii/Options.py
new file mode 100644
index 000000000000..cc39512a176e
--- /dev/null
+++ b/worlds/doom_ii/Options.py
@@ -0,0 +1,150 @@
+import typing
+
+from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
+from dataclasses import dataclass
+
+
+class Difficulty(Choice):
+ """
+ Choose the difficulty option. Those match DOOM's difficulty options.
+ baby (I'm too young to die.) double ammos, half damage, less monsters or strength.
+ easy (Hey, not too rough.) less monsters or strength.
+ medium (Hurt me plenty.) Default.
+ hard (Ultra-Violence.) More monsters or strength.
+ nightmare (Nightmare!) Monsters attack more rapidly and respawn.
+ """
+ display_name = "Difficulty"
+ option_baby = 0
+ option_easy = 1
+ option_medium = 2
+ option_hard = 3
+ option_nightmare = 4
+ default = 2
+
+
+class RandomMonsters(Choice):
+ """
+ Choose how monsters are randomized.
+ vanilla: No randomization
+ shuffle: Monsters are shuffled within the level
+ random_balanced: Monsters are completely randomized, but balanced based on existing ratio in the level. (Small monsters vs medium vs big)
+ random_chaotic: Monsters are completely randomized, but balanced based on existing ratio in the entire game.
+ """
+ display_name = "Random Monsters"
+ option_vanilla = 0
+ option_shuffle = 1
+ option_random_balanced = 2
+ option_random_chaotic = 3
+ default = 2
+
+
+class RandomPickups(Choice):
+ """
+ Choose how pickups are randomized.
+ vanilla: No randomization
+ shuffle: Pickups are shuffled within the level
+ random_balanced: Pickups are completely randomized, but balanced based on existing ratio in the level. (Small pickups vs Big)
+ """
+ display_name = "Random Pickups"
+ option_vanilla = 0
+ option_shuffle = 1
+ option_random_balanced = 2
+ default = 1
+
+
+class RandomMusic(Choice):
+ """
+ Level musics will be randomized.
+ vanilla: No randomization
+ shuffle_selected: Selected episodes' levels will be shuffled
+ shuffle_game: All the music will be shuffled
+ """
+ display_name = "Random Music"
+ option_vanilla = 0
+ option_shuffle_selected = 1
+ option_shuffle_game = 2
+ default = 0
+
+
+class FlipLevels(Choice):
+ """
+ Flip levels on one axis.
+ vanilla: No flipping
+ flipped: All levels are flipped
+ random: Random levels are flipped
+ """
+ display_name = "Flip Levels"
+ option_vanilla = 0
+ option_flipped = 1
+ option_randomly_flipped = 2
+ default = 0
+
+
+class AllowDeathLogic(Toggle):
+ """Some locations require a timed puzzle that can only be tried once.
+ After which, if the player failed to get it, the location cannot be checked anymore.
+ By default, no progression items are placed here. There is a way, hovewer, to still get them:
+ Get killed in the current map. The map will reset, you can now attempt the puzzle again."""
+ display_name = "Allow Death Logic"
+
+
+class Pro(Toggle):
+ """Include difficult tricks into rules. Mostly employed by speed runners.
+ i.e.: Leaps across to a locked area, trigger a switch behind a window at the right angle, etc."""
+ display_name = "Pro Doom"
+
+
+class StartWithComputerAreaMaps(Toggle):
+ """Give the player all Computer Area Map items from the start."""
+ display_name = "Start With Computer Area Maps"
+
+
+class ResetLevelOnDeath(DefaultOnToggle):
+ """When dying, levels are reset and monsters respawned. But inventory and checks are kept.
+ Turning this setting off is considered easy mode. Good for new players that don't know the levels well."""
+ display_message="Reset level on death"
+
+
+class Episode1(DefaultOnToggle):
+ """Subterranean and Outpost.
+ If none of the episodes are chosen, Episode 1 will be chosen by default."""
+ display_name = "Episode 1"
+
+
+class Episode2(DefaultOnToggle):
+ """City.
+ If none of the episodes are chosen, Episode 1 will be chosen by default."""
+ display_name = "Episode 2"
+
+
+class Episode3(DefaultOnToggle):
+ """Hell.
+ If none of the episodes are chosen, Episode 1 will be chosen by default."""
+ display_name = "Episode 3"
+
+
+class SecretLevels(Toggle):
+ """Secret levels.
+ This is too short to be an episode. It's additive.
+ Another episode will have to be selected along with this one.
+ Otherwise episode 1 will be added."""
+ display_name = "Secret Levels"
+
+
+@dataclass
+class DOOM2Options(PerGameCommonOptions):
+ start_inventory_from_pool: StartInventoryPool
+ difficulty: Difficulty
+ random_monsters: RandomMonsters
+ random_pickups: RandomPickups
+ random_music: RandomMusic
+ flip_levels: FlipLevels
+ allow_death_logic: AllowDeathLogic
+ pro: Pro
+ start_with_computer_area_maps: StartWithComputerAreaMaps
+ death_link: DeathLink
+ reset_level_on_death: ResetLevelOnDeath
+ episode1: Episode1
+ episode2: Episode2
+ episode3: Episode3
+ episode4: SecretLevels
diff --git a/worlds/doom_ii/Regions.py b/worlds/doom_ii/Regions.py
new file mode 100644
index 000000000000..3d81d7abb84e
--- /dev/null
+++ b/worlds/doom_ii/Regions.py
@@ -0,0 +1,502 @@
+# This file is auto generated. More info: https://github.com/Daivuk/apdoom
+
+from typing import List
+from BaseClasses import TypedDict
+
+class ConnectionDict(TypedDict, total=False):
+ target: str
+ pro: bool
+
+class RegionDict(TypedDict, total=False):
+ name: str
+ connects_to_hub: bool
+ episode: int
+ connections: List[ConnectionDict]
+
+
+regions:List[RegionDict] = [
+ # Entryway (MAP01)
+ {"name":"Entryway (MAP01) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[]},
+
+ # Underhalls (MAP02)
+ {"name":"Underhalls (MAP02) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[{"target":"Underhalls (MAP02) Red","pro":False}]},
+ {"name":"Underhalls (MAP02) Blue",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"Underhalls (MAP02) Red","pro":False}]},
+ {"name":"Underhalls (MAP02) Red",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"Underhalls (MAP02) Blue","pro":False},
+ {"target":"Underhalls (MAP02) Main","pro":False}]},
+
+ # The Gantlet (MAP03)
+ {"name":"The Gantlet (MAP03) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[
+ {"target":"The Gantlet (MAP03) Blue","pro":False},
+ {"target":"The Gantlet (MAP03) Blue Pro Jump","pro":True}]},
+ {"name":"The Gantlet (MAP03) Blue",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Gantlet (MAP03) Main","pro":False},
+ {"target":"The Gantlet (MAP03) Red","pro":False},
+ {"target":"The Gantlet (MAP03) Blue Pro Jump","pro":False}]},
+ {"name":"The Gantlet (MAP03) Red",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[]},
+ {"name":"The Gantlet (MAP03) Blue Pro Jump",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Gantlet (MAP03) Blue","pro":False}]},
+
+ # The Focus (MAP04)
+ {"name":"The Focus (MAP04) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[
+ {"target":"The Focus (MAP04) Red","pro":False},
+ {"target":"The Focus (MAP04) Blue","pro":False}]},
+ {"name":"The Focus (MAP04) Blue",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Focus (MAP04) Main","pro":False}]},
+ {"name":"The Focus (MAP04) Yellow",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Focus (MAP04) Red","pro":False}]},
+ {"name":"The Focus (MAP04) Red",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Focus (MAP04) Yellow","pro":False},
+ {"target":"The Focus (MAP04) Main","pro":False}]},
+
+ # The Waste Tunnels (MAP05)
+ {"name":"The Waste Tunnels (MAP05) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[
+ {"target":"The Waste Tunnels (MAP05) Red","pro":False},
+ {"target":"The Waste Tunnels (MAP05) Blue","pro":False}]},
+ {"name":"The Waste Tunnels (MAP05) Blue",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Waste Tunnels (MAP05) Yellow","pro":False},
+ {"target":"The Waste Tunnels (MAP05) Main","pro":False}]},
+ {"name":"The Waste Tunnels (MAP05) Yellow",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Waste Tunnels (MAP05) Blue","pro":False}]},
+ {"name":"The Waste Tunnels (MAP05) Red",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Waste Tunnels (MAP05) Main","pro":False}]},
+
+ # The Crusher (MAP06)
+ {"name":"The Crusher (MAP06) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[{"target":"The Crusher (MAP06) Blue","pro":False}]},
+ {"name":"The Crusher (MAP06) Blue",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Crusher (MAP06) Red","pro":False},
+ {"target":"The Crusher (MAP06) Main","pro":False}]},
+ {"name":"The Crusher (MAP06) Yellow",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Crusher (MAP06) Red","pro":False}]},
+ {"name":"The Crusher (MAP06) Red",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Crusher (MAP06) Yellow","pro":False},
+ {"target":"The Crusher (MAP06) Blue","pro":False},
+ {"target":"The Crusher (MAP06) Main","pro":False}]},
+
+ # Dead Simple (MAP07)
+ {"name":"Dead Simple (MAP07) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[]},
+
+ # Tricks and Traps (MAP08)
+ {"name":"Tricks and Traps (MAP08) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[
+ {"target":"Tricks and Traps (MAP08) Red","pro":False},
+ {"target":"Tricks and Traps (MAP08) Yellow","pro":False}]},
+ {"name":"Tricks and Traps (MAP08) Yellow",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"Tricks and Traps (MAP08) Main","pro":False}]},
+ {"name":"Tricks and Traps (MAP08) Red",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"Tricks and Traps (MAP08) Main","pro":False}]},
+
+ # The Pit (MAP09)
+ {"name":"The Pit (MAP09) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[
+ {"target":"The Pit (MAP09) Yellow","pro":False},
+ {"target":"The Pit (MAP09) Blue","pro":False}]},
+ {"name":"The Pit (MAP09) Blue",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[]},
+ {"name":"The Pit (MAP09) Yellow",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Pit (MAP09) Main","pro":False}]},
+
+ # Refueling Base (MAP10)
+ {"name":"Refueling Base (MAP10) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[{"target":"Refueling Base (MAP10) Yellow","pro":False}]},
+ {"name":"Refueling Base (MAP10) Yellow",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"Refueling Base (MAP10) Main","pro":False},
+ {"target":"Refueling Base (MAP10) Yellow Blue","pro":False}]},
+ {"name":"Refueling Base (MAP10) Yellow Blue",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"Refueling Base (MAP10) Yellow","pro":False}]},
+
+ # Circle of Death (MAP11)
+ {"name":"Circle of Death (MAP11) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[
+ {"target":"Circle of Death (MAP11) Blue","pro":False},
+ {"target":"Circle of Death (MAP11) Red","pro":False}]},
+ {"name":"Circle of Death (MAP11) Blue",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"Circle of Death (MAP11) Main","pro":False}]},
+ {"name":"Circle of Death (MAP11) Red",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"Circle of Death (MAP11) Main","pro":False}]},
+
+ # The Factory (MAP12)
+ {"name":"The Factory (MAP12) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[
+ {"target":"The Factory (MAP12) Yellow","pro":False},
+ {"target":"The Factory (MAP12) Blue","pro":False}]},
+ {"name":"The Factory (MAP12) Blue",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Factory (MAP12) Main","pro":False}]},
+ {"name":"The Factory (MAP12) Yellow",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[]},
+
+ # Downtown (MAP13)
+ {"name":"Downtown (MAP13) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[
+ {"target":"Downtown (MAP13) Yellow","pro":False},
+ {"target":"Downtown (MAP13) Red","pro":False},
+ {"target":"Downtown (MAP13) Blue","pro":False}]},
+ {"name":"Downtown (MAP13) Blue",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"Downtown (MAP13) Main","pro":False}]},
+ {"name":"Downtown (MAP13) Yellow",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"Downtown (MAP13) Main","pro":False}]},
+ {"name":"Downtown (MAP13) Red",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"Downtown (MAP13) Main","pro":False}]},
+
+ # The Inmost Dens (MAP14)
+ {"name":"The Inmost Dens (MAP14) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[{"target":"The Inmost Dens (MAP14) Red","pro":False}]},
+ {"name":"The Inmost Dens (MAP14) Blue",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[
+ {"target":"The Inmost Dens (MAP14) Main","pro":False},
+ {"target":"The Inmost Dens (MAP14) Red East","pro":False}]},
+ {"name":"The Inmost Dens (MAP14) Red",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[
+ {"target":"The Inmost Dens (MAP14) Main","pro":False},
+ {"target":"The Inmost Dens (MAP14) Red South","pro":False},
+ {"target":"The Inmost Dens (MAP14) Red East","pro":False}]},
+ {"name":"The Inmost Dens (MAP14) Red East",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[
+ {"target":"The Inmost Dens (MAP14) Blue","pro":False},
+ {"target":"The Inmost Dens (MAP14) Main","pro":False}]},
+ {"name":"The Inmost Dens (MAP14) Red South",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Inmost Dens (MAP14) Main","pro":False}]},
+
+ # Industrial Zone (MAP15)
+ {"name":"Industrial Zone (MAP15) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[
+ {"target":"Industrial Zone (MAP15) Yellow East","pro":False},
+ {"target":"Industrial Zone (MAP15) Yellow West","pro":False}]},
+ {"name":"Industrial Zone (MAP15) Blue",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"Industrial Zone (MAP15) Yellow East","pro":False}]},
+ {"name":"Industrial Zone (MAP15) Yellow East",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[
+ {"target":"Industrial Zone (MAP15) Blue","pro":False},
+ {"target":"Industrial Zone (MAP15) Main","pro":False}]},
+ {"name":"Industrial Zone (MAP15) Yellow West",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"Industrial Zone (MAP15) Main","pro":False}]},
+
+ # Suburbs (MAP16)
+ {"name":"Suburbs (MAP16) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[
+ {"target":"Suburbs (MAP16) Red","pro":False},
+ {"target":"Suburbs (MAP16) Blue","pro":False}]},
+ {"name":"Suburbs (MAP16) Blue",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"Suburbs (MAP16) Main","pro":False}]},
+ {"name":"Suburbs (MAP16) Red",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"Suburbs (MAP16) Main","pro":False}]},
+
+ # Tenements (MAP17)
+ {"name":"Tenements (MAP17) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[{"target":"Tenements (MAP17) Red","pro":False}]},
+ {"name":"Tenements (MAP17) Blue",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"Tenements (MAP17) Red","pro":False}]},
+ {"name":"Tenements (MAP17) Yellow",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[
+ {"target":"Tenements (MAP17) Red","pro":False},
+ {"target":"Tenements (MAP17) Blue","pro":False}]},
+ {"name":"Tenements (MAP17) Red",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[
+ {"target":"Tenements (MAP17) Yellow","pro":False},
+ {"target":"Tenements (MAP17) Blue","pro":False},
+ {"target":"Tenements (MAP17) Main","pro":False}]},
+
+ # The Courtyard (MAP18)
+ {"name":"The Courtyard (MAP18) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[
+ {"target":"The Courtyard (MAP18) Yellow","pro":False},
+ {"target":"The Courtyard (MAP18) Blue","pro":False}]},
+ {"name":"The Courtyard (MAP18) Blue",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Courtyard (MAP18) Main","pro":False}]},
+ {"name":"The Courtyard (MAP18) Yellow",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Courtyard (MAP18) Main","pro":False}]},
+
+ # The Citadel (MAP19)
+ {"name":"The Citadel (MAP19) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[{"target":"The Citadel (MAP19) Red","pro":False}]},
+ {"name":"The Citadel (MAP19) Red",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Citadel (MAP19) Main","pro":False}]},
+
+ # Gotcha! (MAP20)
+ {"name":"Gotcha! (MAP20) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[]},
+
+ # Nirvana (MAP21)
+ {"name":"Nirvana (MAP21) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[{"target":"Nirvana (MAP21) Yellow","pro":False}]},
+ {"name":"Nirvana (MAP21) Yellow",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ {"target":"Nirvana (MAP21) Main","pro":False},
+ {"target":"Nirvana (MAP21) Magenta","pro":False}]},
+ {"name":"Nirvana (MAP21) Magenta",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"Nirvana (MAP21) Yellow","pro":False}]},
+
+ # The Catacombs (MAP22)
+ {"name":"The Catacombs (MAP22) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[
+ {"target":"The Catacombs (MAP22) Blue","pro":False},
+ {"target":"The Catacombs (MAP22) Red","pro":False}]},
+ {"name":"The Catacombs (MAP22) Blue",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Catacombs (MAP22) Main","pro":False}]},
+ {"name":"The Catacombs (MAP22) Red",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Catacombs (MAP22) Main","pro":False}]},
+
+ # Barrels o Fun (MAP23)
+ {"name":"Barrels o Fun (MAP23) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[{"target":"Barrels o Fun (MAP23) Yellow","pro":False}]},
+ {"name":"Barrels o Fun (MAP23) Yellow",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"Barrels o Fun (MAP23) Main","pro":False}]},
+
+ # The Chasm (MAP24)
+ {"name":"The Chasm (MAP24) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[{"target":"The Chasm (MAP24) Red","pro":False}]},
+ {"name":"The Chasm (MAP24) Red",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Chasm (MAP24) Main","pro":False}]},
+
+ # Bloodfalls (MAP25)
+ {"name":"Bloodfalls (MAP25) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[{"target":"Bloodfalls (MAP25) Blue","pro":False}]},
+ {"name":"Bloodfalls (MAP25) Blue",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"Bloodfalls (MAP25) Main","pro":False}]},
+
+ # The Abandoned Mines (MAP26)
+ {"name":"The Abandoned Mines (MAP26) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[
+ {"target":"The Abandoned Mines (MAP26) Yellow","pro":False},
+ {"target":"The Abandoned Mines (MAP26) Red","pro":False},
+ {"target":"The Abandoned Mines (MAP26) Blue","pro":False}]},
+ {"name":"The Abandoned Mines (MAP26) Blue",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Abandoned Mines (MAP26) Main","pro":False}]},
+ {"name":"The Abandoned Mines (MAP26) Yellow",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Abandoned Mines (MAP26) Main","pro":False}]},
+ {"name":"The Abandoned Mines (MAP26) Red",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Abandoned Mines (MAP26) Main","pro":False}]},
+
+ # Monster Condo (MAP27)
+ {"name":"Monster Condo (MAP27) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[
+ {"target":"Monster Condo (MAP27) Yellow","pro":False},
+ {"target":"Monster Condo (MAP27) Red","pro":False},
+ {"target":"Monster Condo (MAP27) Blue","pro":False}]},
+ {"name":"Monster Condo (MAP27) Blue",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"Monster Condo (MAP27) Main","pro":False}]},
+ {"name":"Monster Condo (MAP27) Yellow",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"Monster Condo (MAP27) Main","pro":False}]},
+ {"name":"Monster Condo (MAP27) Red",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"Monster Condo (MAP27) Main","pro":False}]},
+
+ # The Spirit World (MAP28)
+ {"name":"The Spirit World (MAP28) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[
+ {"target":"The Spirit World (MAP28) Yellow","pro":False},
+ {"target":"The Spirit World (MAP28) Red","pro":False}]},
+ {"name":"The Spirit World (MAP28) Yellow",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Spirit World (MAP28) Main","pro":False}]},
+ {"name":"The Spirit World (MAP28) Red",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Spirit World (MAP28) Main","pro":False}]},
+
+ # The Living End (MAP29)
+ {"name":"The Living End (MAP29) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[]},
+
+ # Icon of Sin (MAP30)
+ {"name":"Icon of Sin (MAP30) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[]},
+
+ # Wolfenstein2 (MAP31)
+ {"name":"Wolfenstein2 (MAP31) Main",
+ "connects_to_hub":True,
+ "episode":4,
+ "connections":[]},
+
+ # Grosse2 (MAP32)
+ {"name":"Grosse2 (MAP32) Main",
+ "connects_to_hub":True,
+ "episode":4,
+ "connections":[]},
+]
diff --git a/worlds/doom_ii/Rules.py b/worlds/doom_ii/Rules.py
new file mode 100644
index 000000000000..139733c0eac8
--- /dev/null
+++ b/worlds/doom_ii/Rules.py
@@ -0,0 +1,497 @@
+# This file is auto generated. More info: https://github.com/Daivuk/apdoom
+
+from typing import TYPE_CHECKING
+from worlds.generic.Rules import set_rule
+
+if TYPE_CHECKING:
+ from . import DOOM2World
+
+
+def set_episode1_rules(player, multiworld, pro):
+ # Entryway (MAP01)
+ set_rule(multiworld.get_entrance("Hub -> Entryway (MAP01) Main", player), lambda state:
+ state.has("Entryway (MAP01)", player, 1))
+ set_rule(multiworld.get_entrance("Hub -> Entryway (MAP01) Main", player), lambda state:
+ state.has("Entryway (MAP01)", player, 1))
+
+ # Underhalls (MAP02)
+ set_rule(multiworld.get_entrance("Hub -> Underhalls (MAP02) Main", player), lambda state:
+ state.has("Underhalls (MAP02)", player, 1))
+ set_rule(multiworld.get_entrance("Underhalls (MAP02) Main -> Underhalls (MAP02) Red", player), lambda state:
+ state.has("Underhalls (MAP02) - Red keycard", player, 1))
+ set_rule(multiworld.get_entrance("Underhalls (MAP02) Blue -> Underhalls (MAP02) Red", player), lambda state:
+ state.has("Underhalls (MAP02) - Blue keycard", player, 1))
+ set_rule(multiworld.get_entrance("Underhalls (MAP02) Red -> Underhalls (MAP02) Blue", player), lambda state:
+ state.has("Underhalls (MAP02) - Blue keycard", player, 1))
+
+ # The Gantlet (MAP03)
+ set_rule(multiworld.get_entrance("Hub -> The Gantlet (MAP03) Main", player), lambda state:
+ (state.has("The Gantlet (MAP03)", player, 1)) and
+ (state.has("Shotgun", player, 1) or
+ state.has("Chaingun", player, 1) or
+ state.has("Super Shotgun", player, 1)))
+ set_rule(multiworld.get_entrance("The Gantlet (MAP03) Main -> The Gantlet (MAP03) Blue", player), lambda state:
+ state.has("The Gantlet (MAP03) - Blue keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Gantlet (MAP03) Blue -> The Gantlet (MAP03) Red", player), lambda state:
+ state.has("The Gantlet (MAP03) - Red keycard", player, 1))
+
+ # The Focus (MAP04)
+ set_rule(multiworld.get_entrance("Hub -> The Focus (MAP04) Main", player), lambda state:
+ (state.has("The Focus (MAP04)", player, 1)) and
+ (state.has("Shotgun", player, 1) or
+ state.has("Chaingun", player, 1) or
+ state.has("Super Shotgun", player, 1)))
+ set_rule(multiworld.get_entrance("The Focus (MAP04) Main -> The Focus (MAP04) Red", player), lambda state:
+ state.has("The Focus (MAP04) - Red keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Focus (MAP04) Main -> The Focus (MAP04) Blue", player), lambda state:
+ state.has("The Focus (MAP04) - Blue keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Focus (MAP04) Yellow -> The Focus (MAP04) Red", player), lambda state:
+ state.has("The Focus (MAP04) - Yellow keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Focus (MAP04) Red -> The Focus (MAP04) Yellow", player), lambda state:
+ state.has("The Focus (MAP04) - Yellow keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Focus (MAP04) Red -> The Focus (MAP04) Main", player), lambda state:
+ state.has("The Focus (MAP04) - Red keycard", player, 1))
+
+ # The Waste Tunnels (MAP05)
+ set_rule(multiworld.get_entrance("Hub -> The Waste Tunnels (MAP05) Main", player), lambda state:
+ (state.has("The Waste Tunnels (MAP05)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Main -> The Waste Tunnels (MAP05) Red", player), lambda state:
+ state.has("The Waste Tunnels (MAP05) - Red keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Main -> The Waste Tunnels (MAP05) Blue", player), lambda state:
+ state.has("The Waste Tunnels (MAP05) - Blue keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Blue -> The Waste Tunnels (MAP05) Yellow", player), lambda state:
+ state.has("The Waste Tunnels (MAP05) - Yellow keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Blue -> The Waste Tunnels (MAP05) Main", player), lambda state:
+ state.has("The Waste Tunnels (MAP05) - Blue keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Waste Tunnels (MAP05) Yellow -> The Waste Tunnels (MAP05) Blue", player), lambda state:
+ state.has("The Waste Tunnels (MAP05) - Yellow keycard", player, 1))
+
+ # The Crusher (MAP06)
+ set_rule(multiworld.get_entrance("Hub -> The Crusher (MAP06) Main", player), lambda state:
+ (state.has("The Crusher (MAP06)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("The Crusher (MAP06) Main -> The Crusher (MAP06) Blue", player), lambda state:
+ state.has("The Crusher (MAP06) - Blue keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Crusher (MAP06) Blue -> The Crusher (MAP06) Red", player), lambda state:
+ state.has("The Crusher (MAP06) - Red keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Crusher (MAP06) Blue -> The Crusher (MAP06) Main", player), lambda state:
+ state.has("The Crusher (MAP06) - Blue keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Crusher (MAP06) Yellow -> The Crusher (MAP06) Red", player), lambda state:
+ state.has("The Crusher (MAP06) - Yellow keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Crusher (MAP06) Red -> The Crusher (MAP06) Yellow", player), lambda state:
+ state.has("The Crusher (MAP06) - Yellow keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Crusher (MAP06) Red -> The Crusher (MAP06) Blue", player), lambda state:
+ state.has("The Crusher (MAP06) - Red keycard", player, 1))
+
+ # Dead Simple (MAP07)
+ set_rule(multiworld.get_entrance("Hub -> Dead Simple (MAP07) Main", player), lambda state:
+ (state.has("Dead Simple (MAP07)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+
+ # Tricks and Traps (MAP08)
+ set_rule(multiworld.get_entrance("Hub -> Tricks and Traps (MAP08) Main", player), lambda state:
+ (state.has("Tricks and Traps (MAP08)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("Tricks and Traps (MAP08) Main -> Tricks and Traps (MAP08) Red", player), lambda state:
+ state.has("Tricks and Traps (MAP08) - Red skull key", player, 1))
+ set_rule(multiworld.get_entrance("Tricks and Traps (MAP08) Main -> Tricks and Traps (MAP08) Yellow", player), lambda state:
+ state.has("Tricks and Traps (MAP08) - Yellow skull key", player, 1))
+
+ # The Pit (MAP09)
+ set_rule(multiworld.get_entrance("Hub -> The Pit (MAP09) Main", player), lambda state:
+ (state.has("The Pit (MAP09)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("The Pit (MAP09) Main -> The Pit (MAP09) Yellow", player), lambda state:
+ state.has("The Pit (MAP09) - Yellow keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Pit (MAP09) Main -> The Pit (MAP09) Blue", player), lambda state:
+ state.has("The Pit (MAP09) - Blue keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Pit (MAP09) Yellow -> The Pit (MAP09) Main", player), lambda state:
+ state.has("The Pit (MAP09) - Yellow keycard", player, 1))
+
+ # Refueling Base (MAP10)
+ set_rule(multiworld.get_entrance("Hub -> Refueling Base (MAP10) Main", player), lambda state:
+ (state.has("Refueling Base (MAP10)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("Refueling Base (MAP10) Main -> Refueling Base (MAP10) Yellow", player), lambda state:
+ state.has("Refueling Base (MAP10) - Yellow keycard", player, 1))
+ set_rule(multiworld.get_entrance("Refueling Base (MAP10) Yellow -> Refueling Base (MAP10) Yellow Blue", player), lambda state:
+ state.has("Refueling Base (MAP10) - Blue keycard", player, 1))
+
+ # Circle of Death (MAP11)
+ set_rule(multiworld.get_entrance("Hub -> Circle of Death (MAP11) Main", player), lambda state:
+ (state.has("Circle of Death (MAP11)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("Circle of Death (MAP11) Main -> Circle of Death (MAP11) Blue", player), lambda state:
+ state.has("Circle of Death (MAP11) - Blue keycard", player, 1))
+ set_rule(multiworld.get_entrance("Circle of Death (MAP11) Main -> Circle of Death (MAP11) Red", player), lambda state:
+ state.has("Circle of Death (MAP11) - Red keycard", player, 1))
+
+
+def set_episode2_rules(player, multiworld, pro):
+ # The Factory (MAP12)
+ set_rule(multiworld.get_entrance("Hub -> The Factory (MAP12) Main", player), lambda state:
+ (state.has("The Factory (MAP12)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Yellow", player), lambda state:
+ state.has("The Factory (MAP12) - Yellow keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Blue", player), lambda state:
+ state.has("The Factory (MAP12) - Blue keycard", player, 1))
+
+ # Downtown (MAP13)
+ set_rule(multiworld.get_entrance("Hub -> Downtown (MAP13) Main", player), lambda state:
+ (state.has("Downtown (MAP13)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("Downtown (MAP13) Main -> Downtown (MAP13) Yellow", player), lambda state:
+ state.has("Downtown (MAP13) - Yellow keycard", player, 1))
+ set_rule(multiworld.get_entrance("Downtown (MAP13) Main -> Downtown (MAP13) Red", player), lambda state:
+ state.has("Downtown (MAP13) - Red keycard", player, 1))
+ set_rule(multiworld.get_entrance("Downtown (MAP13) Main -> Downtown (MAP13) Blue", player), lambda state:
+ state.has("Downtown (MAP13) - Blue keycard", player, 1))
+
+ # The Inmost Dens (MAP14)
+ set_rule(multiworld.get_entrance("Hub -> The Inmost Dens (MAP14) Main", player), lambda state:
+ (state.has("The Inmost Dens (MAP14)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("The Inmost Dens (MAP14) Main -> The Inmost Dens (MAP14) Red", player), lambda state:
+ state.has("The Inmost Dens (MAP14) - Red skull key", player, 1))
+ set_rule(multiworld.get_entrance("The Inmost Dens (MAP14) Blue -> The Inmost Dens (MAP14) Red East", player), lambda state:
+ state.has("The Inmost Dens (MAP14) - Blue skull key", player, 1))
+ set_rule(multiworld.get_entrance("The Inmost Dens (MAP14) Red -> The Inmost Dens (MAP14) Main", player), lambda state:
+ state.has("The Inmost Dens (MAP14) - Red skull key", player, 1))
+ set_rule(multiworld.get_entrance("The Inmost Dens (MAP14) Red East -> The Inmost Dens (MAP14) Blue", player), lambda state:
+ state.has("The Inmost Dens (MAP14) - Blue skull key", player, 1))
+
+ # Industrial Zone (MAP15)
+ set_rule(multiworld.get_entrance("Hub -> Industrial Zone (MAP15) Main", player), lambda state:
+ (state.has("Industrial Zone (MAP15)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("Industrial Zone (MAP15) Main -> Industrial Zone (MAP15) Yellow East", player), lambda state:
+ state.has("Industrial Zone (MAP15) - Yellow keycard", player, 1))
+ set_rule(multiworld.get_entrance("Industrial Zone (MAP15) Main -> Industrial Zone (MAP15) Yellow West", player), lambda state:
+ state.has("Industrial Zone (MAP15) - Yellow keycard", player, 1))
+ set_rule(multiworld.get_entrance("Industrial Zone (MAP15) Blue -> Industrial Zone (MAP15) Yellow East", player), lambda state:
+ state.has("Industrial Zone (MAP15) - Blue keycard", player, 1))
+ set_rule(multiworld.get_entrance("Industrial Zone (MAP15) Yellow East -> Industrial Zone (MAP15) Blue", player), lambda state:
+ state.has("Industrial Zone (MAP15) - Blue keycard", player, 1))
+
+ # Suburbs (MAP16)
+ set_rule(multiworld.get_entrance("Hub -> Suburbs (MAP16) Main", player), lambda state:
+ (state.has("Suburbs (MAP16)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("Suburbs (MAP16) Main -> Suburbs (MAP16) Red", player), lambda state:
+ state.has("Suburbs (MAP16) - Red skull key", player, 1))
+ set_rule(multiworld.get_entrance("Suburbs (MAP16) Main -> Suburbs (MAP16) Blue", player), lambda state:
+ state.has("Suburbs (MAP16) - Blue skull key", player, 1))
+
+ # Tenements (MAP17)
+ set_rule(multiworld.get_entrance("Hub -> Tenements (MAP17) Main", player), lambda state:
+ (state.has("Tenements (MAP17)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("Tenements (MAP17) Main -> Tenements (MAP17) Red", player), lambda state:
+ state.has("Tenements (MAP17) - Red keycard", player, 1))
+ set_rule(multiworld.get_entrance("Tenements (MAP17) Red -> Tenements (MAP17) Yellow", player), lambda state:
+ state.has("Tenements (MAP17) - Yellow skull key", player, 1))
+ set_rule(multiworld.get_entrance("Tenements (MAP17) Red -> Tenements (MAP17) Blue", player), lambda state:
+ state.has("Tenements (MAP17) - Blue keycard", player, 1))
+
+ # The Courtyard (MAP18)
+ set_rule(multiworld.get_entrance("Hub -> The Courtyard (MAP18) Main", player), lambda state:
+ (state.has("The Courtyard (MAP18)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("The Courtyard (MAP18) Main -> The Courtyard (MAP18) Yellow", player), lambda state:
+ state.has("The Courtyard (MAP18) - Yellow skull key", player, 1))
+ set_rule(multiworld.get_entrance("The Courtyard (MAP18) Main -> The Courtyard (MAP18) Blue", player), lambda state:
+ state.has("The Courtyard (MAP18) - Blue skull key", player, 1))
+ set_rule(multiworld.get_entrance("The Courtyard (MAP18) Blue -> The Courtyard (MAP18) Main", player), lambda state:
+ state.has("The Courtyard (MAP18) - Blue skull key", player, 1))
+ set_rule(multiworld.get_entrance("The Courtyard (MAP18) Yellow -> The Courtyard (MAP18) Main", player), lambda state:
+ state.has("The Courtyard (MAP18) - Yellow skull key", player, 1))
+
+ # The Citadel (MAP19)
+ set_rule(multiworld.get_entrance("Hub -> The Citadel (MAP19) Main", player), lambda state:
+ (state.has("The Citadel (MAP19)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("The Citadel (MAP19) Main -> The Citadel (MAP19) Red", player), lambda state:
+ (state.has("The Citadel (MAP19) - Red skull key", player, 1)) and (state.has("The Citadel (MAP19) - Blue skull key", player, 1) or
+ state.has("The Citadel (MAP19) - Yellow skull key", player, 1)))
+ set_rule(multiworld.get_entrance("The Citadel (MAP19) Red -> The Citadel (MAP19) Main", player), lambda state:
+ (state.has("The Citadel (MAP19) - Red skull key", player, 1)) and (state.has("The Citadel (MAP19) - Yellow skull key", player, 1) or
+ state.has("The Citadel (MAP19) - Blue skull key", player, 1)))
+
+ # Gotcha! (MAP20)
+ set_rule(multiworld.get_entrance("Hub -> Gotcha! (MAP20) Main", player), lambda state:
+ (state.has("Gotcha! (MAP20)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+
+
+def set_episode3_rules(player, multiworld, pro):
+ # Nirvana (MAP21)
+ set_rule(multiworld.get_entrance("Hub -> Nirvana (MAP21) Main", player), lambda state:
+ (state.has("Nirvana (MAP21)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("Nirvana (MAP21) Main -> Nirvana (MAP21) Yellow", player), lambda state:
+ state.has("Nirvana (MAP21) - Yellow skull key", player, 1))
+ set_rule(multiworld.get_entrance("Nirvana (MAP21) Yellow -> Nirvana (MAP21) Main", player), lambda state:
+ state.has("Nirvana (MAP21) - Yellow skull key", player, 1))
+ set_rule(multiworld.get_entrance("Nirvana (MAP21) Yellow -> Nirvana (MAP21) Magenta", player), lambda state:
+ state.has("Nirvana (MAP21) - Red skull key", player, 1) and
+ state.has("Nirvana (MAP21) - Blue skull key", player, 1))
+ set_rule(multiworld.get_entrance("Nirvana (MAP21) Magenta -> Nirvana (MAP21) Yellow", player), lambda state:
+ state.has("Nirvana (MAP21) - Red skull key", player, 1) and
+ state.has("Nirvana (MAP21) - Blue skull key", player, 1))
+
+ # The Catacombs (MAP22)
+ set_rule(multiworld.get_entrance("Hub -> The Catacombs (MAP22) Main", player), lambda state:
+ (state.has("The Catacombs (MAP22)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("BFG9000", player, 1) or
+ state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1)))
+ set_rule(multiworld.get_entrance("The Catacombs (MAP22) Main -> The Catacombs (MAP22) Blue", player), lambda state:
+ state.has("The Catacombs (MAP22) - Blue skull key", player, 1))
+ set_rule(multiworld.get_entrance("The Catacombs (MAP22) Main -> The Catacombs (MAP22) Red", player), lambda state:
+ state.has("The Catacombs (MAP22) - Red skull key", player, 1))
+ set_rule(multiworld.get_entrance("The Catacombs (MAP22) Red -> The Catacombs (MAP22) Main", player), lambda state:
+ state.has("The Catacombs (MAP22) - Red skull key", player, 1))
+
+ # Barrels o Fun (MAP23)
+ set_rule(multiworld.get_entrance("Hub -> Barrels o Fun (MAP23) Main", player), lambda state:
+ (state.has("Barrels o Fun (MAP23)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+ set_rule(multiworld.get_entrance("Barrels o Fun (MAP23) Main -> Barrels o Fun (MAP23) Yellow", player), lambda state:
+ state.has("Barrels o Fun (MAP23) - Yellow skull key", player, 1))
+ set_rule(multiworld.get_entrance("Barrels o Fun (MAP23) Yellow -> Barrels o Fun (MAP23) Main", player), lambda state:
+ state.has("Barrels o Fun (MAP23) - Yellow skull key", player, 1))
+
+ # The Chasm (MAP24)
+ set_rule(multiworld.get_entrance("Hub -> The Chasm (MAP24) Main", player), lambda state:
+ state.has("The Chasm (MAP24)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Rocket launcher", player, 1) and
+ state.has("Plasma gun", player, 1) and
+ state.has("BFG9000", player, 1) and
+ state.has("Super Shotgun", player, 1))
+ set_rule(multiworld.get_entrance("The Chasm (MAP24) Main -> The Chasm (MAP24) Red", player), lambda state:
+ state.has("The Chasm (MAP24) - Red keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Chasm (MAP24) Red -> The Chasm (MAP24) Main", player), lambda state:
+ state.has("The Chasm (MAP24) - Red keycard", player, 1))
+
+ # Bloodfalls (MAP25)
+ set_rule(multiworld.get_entrance("Hub -> Bloodfalls (MAP25) Main", player), lambda state:
+ state.has("Bloodfalls (MAP25)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Rocket launcher", player, 1) and
+ state.has("Plasma gun", player, 1) and
+ state.has("BFG9000", player, 1) and
+ state.has("Super Shotgun", player, 1))
+ set_rule(multiworld.get_entrance("Bloodfalls (MAP25) Main -> Bloodfalls (MAP25) Blue", player), lambda state:
+ state.has("Bloodfalls (MAP25) - Blue skull key", player, 1))
+ set_rule(multiworld.get_entrance("Bloodfalls (MAP25) Blue -> Bloodfalls (MAP25) Main", player), lambda state:
+ state.has("Bloodfalls (MAP25) - Blue skull key", player, 1))
+
+ # The Abandoned Mines (MAP26)
+ set_rule(multiworld.get_entrance("Hub -> The Abandoned Mines (MAP26) Main", player), lambda state:
+ state.has("The Abandoned Mines (MAP26)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Rocket launcher", player, 1) and
+ state.has("BFG9000", player, 1) and
+ state.has("Plasma gun", player, 1) and
+ state.has("Super Shotgun", player, 1))
+ set_rule(multiworld.get_entrance("The Abandoned Mines (MAP26) Main -> The Abandoned Mines (MAP26) Yellow", player), lambda state:
+ state.has("The Abandoned Mines (MAP26) - Yellow keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Abandoned Mines (MAP26) Main -> The Abandoned Mines (MAP26) Red", player), lambda state:
+ state.has("The Abandoned Mines (MAP26) - Red keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Abandoned Mines (MAP26) Main -> The Abandoned Mines (MAP26) Blue", player), lambda state:
+ state.has("The Abandoned Mines (MAP26) - Blue keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Abandoned Mines (MAP26) Blue -> The Abandoned Mines (MAP26) Main", player), lambda state:
+ state.has("The Abandoned Mines (MAP26) - Blue keycard", player, 1))
+ set_rule(multiworld.get_entrance("The Abandoned Mines (MAP26) Yellow -> The Abandoned Mines (MAP26) Main", player), lambda state:
+ state.has("The Abandoned Mines (MAP26) - Yellow keycard", player, 1))
+
+ # Monster Condo (MAP27)
+ set_rule(multiworld.get_entrance("Hub -> Monster Condo (MAP27) Main", player), lambda state:
+ state.has("Monster Condo (MAP27)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Rocket launcher", player, 1) and
+ state.has("Plasma gun", player, 1) and
+ state.has("BFG9000", player, 1) and
+ state.has("Super Shotgun", player, 1))
+ set_rule(multiworld.get_entrance("Monster Condo (MAP27) Main -> Monster Condo (MAP27) Yellow", player), lambda state:
+ state.has("Monster Condo (MAP27) - Yellow skull key", player, 1))
+ set_rule(multiworld.get_entrance("Monster Condo (MAP27) Main -> Monster Condo (MAP27) Red", player), lambda state:
+ state.has("Monster Condo (MAP27) - Red skull key", player, 1))
+ set_rule(multiworld.get_entrance("Monster Condo (MAP27) Main -> Monster Condo (MAP27) Blue", player), lambda state:
+ state.has("Monster Condo (MAP27) - Blue skull key", player, 1))
+ set_rule(multiworld.get_entrance("Monster Condo (MAP27) Red -> Monster Condo (MAP27) Main", player), lambda state:
+ state.has("Monster Condo (MAP27) - Red skull key", player, 1))
+
+ # The Spirit World (MAP28)
+ set_rule(multiworld.get_entrance("Hub -> The Spirit World (MAP28) Main", player), lambda state:
+ state.has("The Spirit World (MAP28)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Rocket launcher", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Plasma gun", player, 1) and
+ state.has("BFG9000", player, 1) and
+ state.has("Super Shotgun", player, 1))
+ set_rule(multiworld.get_entrance("The Spirit World (MAP28) Main -> The Spirit World (MAP28) Yellow", player), lambda state:
+ state.has("The Spirit World (MAP28) - Yellow skull key", player, 1))
+ set_rule(multiworld.get_entrance("The Spirit World (MAP28) Main -> The Spirit World (MAP28) Red", player), lambda state:
+ state.has("The Spirit World (MAP28) - Red skull key", player, 1))
+ set_rule(multiworld.get_entrance("The Spirit World (MAP28) Yellow -> The Spirit World (MAP28) Main", player), lambda state:
+ state.has("The Spirit World (MAP28) - Yellow skull key", player, 1))
+ set_rule(multiworld.get_entrance("The Spirit World (MAP28) Red -> The Spirit World (MAP28) Main", player), lambda state:
+ state.has("The Spirit World (MAP28) - Red skull key", player, 1))
+
+ # The Living End (MAP29)
+ set_rule(multiworld.get_entrance("Hub -> The Living End (MAP29) Main", player), lambda state:
+ state.has("The Living End (MAP29)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Rocket launcher", player, 1) and
+ state.has("Plasma gun", player, 1) and
+ state.has("BFG9000", player, 1) and
+ state.has("Super Shotgun", player, 1))
+
+ # Icon of Sin (MAP30)
+ set_rule(multiworld.get_entrance("Hub -> Icon of Sin (MAP30) Main", player), lambda state:
+ state.has("Icon of Sin (MAP30)", player, 1) and
+ state.has("Rocket launcher", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Plasma gun", player, 1) and
+ state.has("BFG9000", player, 1) and
+ state.has("Super Shotgun", player, 1))
+
+
+def set_episode4_rules(player, multiworld, pro):
+ # Wolfenstein2 (MAP31)
+ set_rule(multiworld.get_entrance("Hub -> Wolfenstein2 (MAP31) Main", player), lambda state:
+ (state.has("Wolfenstein2 (MAP31)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+
+ # Grosse2 (MAP32)
+ set_rule(multiworld.get_entrance("Hub -> Grosse2 (MAP32) Main", player), lambda state:
+ (state.has("Grosse2 (MAP32)", player, 1) and
+ state.has("Shotgun", player, 1) and
+ state.has("Chaingun", player, 1) and
+ state.has("Super Shotgun", player, 1)) and
+ (state.has("Rocket launcher", player, 1) or
+ state.has("Plasma gun", player, 1) or
+ state.has("BFG9000", player, 1)))
+
+
+def set_rules(doom_ii_world: "DOOM2World", included_episodes, pro):
+ player = doom_ii_world.player
+ multiworld = doom_ii_world.multiworld
+
+ if included_episodes[0]:
+ set_episode1_rules(player, multiworld, pro)
+ if included_episodes[1]:
+ set_episode2_rules(player, multiworld, pro)
+ if included_episodes[2]:
+ set_episode3_rules(player, multiworld, pro)
+ if included_episodes[3]:
+ set_episode4_rules(player, multiworld, pro)
diff --git a/worlds/doom_ii/__init__.py b/worlds/doom_ii/__init__.py
new file mode 100644
index 000000000000..32c3cbd5a2c1
--- /dev/null
+++ b/worlds/doom_ii/__init__.py
@@ -0,0 +1,268 @@
+import functools
+import logging
+from typing import Any, Dict, List
+
+from BaseClasses import Entrance, CollectionState, Item, Location, MultiWorld, Region, Tutorial
+from worlds.AutoWorld import WebWorld, World
+from . import Items, Locations, Maps, Regions, Rules
+from .Options import DOOM2Options
+
+logger = logging.getLogger("DOOM II")
+
+DOOM_TYPE_LEVEL_COMPLETE = -2
+DOOM_TYPE_COMPUTER_AREA_MAP = 2026
+
+
+class DOOM2Location(Location):
+ game: str = "DOOM II"
+
+
+class DOOM2Item(Item):
+ game: str = "DOOM II"
+
+
+class DOOM2Web(WebWorld):
+ tutorials = [Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to setting up the DOOM II randomizer connected to an Archipelago Multiworld",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["Daivuk"]
+ )]
+ theme = "dirt"
+
+
+class DOOM2World(World):
+ """
+ Doom II, also known as Doom II: Hell on Earth, is a first-person shooter game by id Software.
+ It was released for MS-DOS in 1994.
+ Compared to its predecessor, Doom II features larger levels, new enemies, a new "super shotgun" weapon
+ """
+ options_dataclass = DOOM2Options
+ options: DOOM2Options
+ game = "DOOM II"
+ web = DOOM2Web()
+ required_client_version = (0, 3, 9)
+
+ item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
+ item_name_groups = Items.item_name_groups
+
+ location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
+ location_name_groups = Locations.location_name_groups
+
+ starting_level_for_episode: List[str] = [
+ "Entryway (MAP01)",
+ "The Factory (MAP12)",
+ "Nirvana (MAP21)"
+ ]
+
+ # Item ratio that scales depending on episode count. These are the ratio for 3 episode. In DOOM1.
+ # The ratio have been tweaked seem, and feel good.
+ items_ratio: Dict[str, float] = {
+ "Armor": 39,
+ "Mega Armor": 23,
+ "Berserk": 11,
+ "Invulnerability": 10,
+ "Partial invisibility": 18,
+ "Supercharge": 26,
+ "Medikit": 15,
+ "Box of bullets": 13,
+ "Box of rockets": 13,
+ "Box of shotgun shells": 13,
+ "Energy cell pack": 10,
+ "Megasphere": 7
+ }
+
+ def __init__(self, multiworld: MultiWorld, player: int):
+ self.included_episodes = [1, 1, 1, 0]
+ self.location_count = 0
+
+ super().__init__(multiworld, player)
+
+ def get_episode_count(self):
+ # Don't include 4th, those are secret levels they are additive
+ return sum(self.included_episodes[:3])
+
+ def generate_early(self):
+ # Cache which episodes are included
+ self.included_episodes[0] = self.options.episode1.value
+ self.included_episodes[1] = self.options.episode2.value
+ self.included_episodes[2] = self.options.episode3.value
+ self.included_episodes[3] = self.options.episode4.value # 4th episode are secret levels
+
+ # If no episodes selected, select Episode 1
+ if self.get_episode_count() == 0:
+ self.included_episodes[0] = 1
+
+ def create_regions(self):
+ pro = self.options.pro.value
+
+ # Main regions
+ menu_region = Region("Menu", self.player, self.multiworld)
+ hub_region = Region("Hub", self.player, self.multiworld)
+ self.multiworld.regions += [menu_region, hub_region]
+ menu_region.add_exits(["Hub"])
+
+ # Create regions and locations
+ main_regions = []
+ connections = []
+ for region_dict in Regions.regions:
+ if not self.included_episodes[region_dict["episode"] - 1]:
+ continue
+
+ region_name = region_dict["name"]
+ if region_dict["connects_to_hub"]:
+ main_regions.append(region_name)
+
+ region = Region(region_name, self.player, self.multiworld)
+ region.add_locations({
+ loc["name"]: loc_id
+ for loc_id, loc in Locations.location_table.items()
+ if loc["region"] == region_name and self.included_episodes[loc["episode"] - 1]
+ }, DOOM2Location)
+
+ self.multiworld.regions.append(region)
+
+ for connection_dict in region_dict["connections"]:
+ # Check if it's a pro-only connection
+ if connection_dict["pro"] and not pro:
+ continue
+ connections.append((region, connection_dict["target"]))
+
+ # Connect main regions to Hub
+ hub_region.add_exits(main_regions)
+
+ # Do the other connections between regions (They are not all both ways)
+ for connection in connections:
+ source = connection[0]
+ target = self.multiworld.get_region(connection[1], self.player)
+
+ entrance = Entrance(self.player, f"{source.name} -> {target.name}", source)
+ source.exits.append(entrance)
+ entrance.connect(target)
+
+ # Sum locations for items creation
+ self.location_count = len(self.multiworld.get_locations(self.player))
+
+ def completion_rule(self, state: CollectionState):
+ for map_name in Maps.map_names:
+ if map_name + " - Exit" not in self.location_name_to_id:
+ continue
+
+ # Exit location names are in form: Entryway (MAP01) - Exit
+ loc = Locations.location_table[self.location_name_to_id[map_name + " - Exit"]]
+ if not self.included_episodes[loc["episode"] - 1]:
+ continue
+
+ # Map complete item names are in form: Entryway (MAP01) - Complete
+ if not state.has(map_name + " - Complete", self.player, 1):
+ return False
+
+ return True
+
+ def set_rules(self):
+ pro = self.options.pro.value
+ allow_death_logic = self.options.allow_death_logic.value
+
+ Rules.set_rules(self, self.included_episodes, pro)
+ self.multiworld.completion_condition[self.player] = lambda state: self.completion_rule(state)
+
+ # Forbid progression items to locations that can be missed and can't be picked up. (e.g. One-time timed
+ # platform) Unless the user allows for it.
+ if not allow_death_logic:
+ for death_logic_location in Locations.death_logic_locations:
+ self.options.exclude_locations.value.add(death_logic_location)
+
+ def create_item(self, name: str) -> DOOM2Item:
+ item_id: int = self.item_name_to_id[name]
+ return DOOM2Item(name, Items.item_table[item_id]["classification"], item_id, self.player)
+
+ def create_items(self):
+ itempool: List[DOOM2Item] = []
+ start_with_computer_area_maps: bool = self.options.start_with_computer_area_maps.value
+
+ # Items
+ for item_id, item in Items.item_table.items():
+ if item["doom_type"] == DOOM_TYPE_LEVEL_COMPLETE:
+ continue # We'll fill it manually later
+
+ if item["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP and start_with_computer_area_maps:
+ continue # We'll fill it manually, and we will put fillers in place
+
+ if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]:
+ continue
+
+ count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
+ itempool += [self.create_item(item["name"]) for _ in range(count)]
+
+ # Place end level items in locked locations
+ for map_name in Maps.map_names:
+ loc_name = map_name + " - Exit"
+ item_name = map_name + " - Complete"
+
+ if loc_name not in self.location_name_to_id:
+ continue
+
+ if item_name not in self.item_name_to_id:
+ continue
+
+ loc = Locations.location_table[self.location_name_to_id[loc_name]]
+ if not self.included_episodes[loc["episode"] - 1]:
+ continue
+
+ self.multiworld.get_location(loc_name, self.player).place_locked_item(self.create_item(item_name))
+ self.location_count -= 1
+
+ # Give starting levels right away
+ for i in range(len(self.starting_level_for_episode)):
+ if self.included_episodes[i]:
+ self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i]))
+
+ # Give Computer area maps if option selected
+ if start_with_computer_area_maps:
+ for item_id, item_dict in Items.item_table.items():
+ item_episode = item_dict["episode"]
+ if item_episode > 0:
+ if item_dict["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP and self.included_episodes[item_episode - 1]:
+ self.multiworld.push_precollected(self.create_item(item_dict["name"]))
+
+ # Fill the rest starting with powerups, then fillers
+ self.create_ratioed_items("Armor", itempool)
+ self.create_ratioed_items("Mega Armor", itempool)
+ self.create_ratioed_items("Berserk", itempool)
+ self.create_ratioed_items("Invulnerability", itempool)
+ self.create_ratioed_items("Partial invisibility", itempool)
+ self.create_ratioed_items("Supercharge", itempool)
+ self.create_ratioed_items("Megasphere", itempool)
+
+ while len(itempool) < self.location_count:
+ itempool.append(self.create_item(self.get_filler_item_name()))
+
+ # add itempool to multiworld
+ self.multiworld.itempool += itempool
+
+ def get_filler_item_name(self):
+ return self.multiworld.random.choice([
+ "Medikit",
+ "Box of bullets",
+ "Box of rockets",
+ "Box of shotgun shells",
+ "Energy cell pack"
+ ])
+
+ def create_ratioed_items(self, item_name: str, itempool: List[DOOM2Item]):
+ remaining_loc = self.location_count - len(itempool)
+ ep_count = self.get_episode_count()
+
+ # Was balanced based on DOOM 1993's first 3 episodes
+ count = min(remaining_loc, max(1, int(round(self.items_ratio[item_name] * ep_count / 3))))
+ if count == 0:
+ logger.warning("Warning, no ", item_name, " will be placed.")
+ return
+
+ for i in range(count):
+ itempool.append(self.create_item(item_name))
+
+ def fill_slot_data(self) -> Dict[str, Any]:
+ return self.options.as_dict("difficulty", "random_monsters", "random_pickups", "random_music", "flip_levels", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "episode1", "episode2", "episode3", "episode4")
diff --git a/worlds/doom_ii/docs/en_DOOM II.md b/worlds/doom_ii/docs/en_DOOM II.md
new file mode 100644
index 000000000000..d02f75cb6c8f
--- /dev/null
+++ b/worlds/doom_ii/docs/en_DOOM II.md
@@ -0,0 +1,23 @@
+# DOOM II
+
+## Where is the options page?
+
+The [player options page](../player-options) contains the options needed to configure your game session.
+
+## What does randomization do to this game?
+
+Guns, keycards, and level unlocks have been randomized. Typically, you will end up playing different levels out of order to find your keycards and level unlocks and eventually complete your game.
+
+Maps can be selected on a level select screen. You can exit a level at any time by visiting the hub station at the beginning of each level. The state of each level is saved and restored upon re-entering the level.
+
+## What is the goal?
+
+The goal is to complete every level.
+
+## What is a "check" in DOOM II?
+
+Guns, keycards, and powerups have been replaced with Archipelago checks. The switch at the end of each level is also a check.
+
+## What "items" can you unlock in DOOM II?
+
+Keycards and level unlocks are your main progression items. Gun unlocks and some upgrades are your useful items. Temporary powerups, ammo, healing, and armor are filler items.
diff --git a/worlds/doom_ii/docs/setup_en.md b/worlds/doom_ii/docs/setup_en.md
new file mode 100644
index 000000000000..87054ab30783
--- /dev/null
+++ b/worlds/doom_ii/docs/setup_en.md
@@ -0,0 +1,51 @@
+# DOOM II Randomizer Setup
+
+## Required Software
+
+- [DOOM II (e.g. Steam version)](https://store.steampowered.com/app/2300/DOOM_II/)
+- [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases)
+
+## Optional Software
+
+- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases)
+
+## Installing AP Doom
+1. Download [APDOOM.zip](https://github.com/Daivuk/apdoom/releases) and extract it.
+2. Copy DOOM2.WAD from your steam install into the extracted folder.
+ You can find the folder in steam by finding the game in your library,
+ right clicking it and choosing *Manage→Browse Local Files*. The WAD file is in the `/base/` folder.
+
+## Joining a MultiWorld Game
+
+1. Launch apdoom-launcher.exe
+2. Select `DOOM II` from the drop-down
+3. Enter the Archipelago server address, slot name, and password (if you have one)
+4. Press "Launch DOOM"
+5. Enjoy!
+
+To continue a game, follow the same connection steps.
+Connecting with a different seed won't erase your progress in other seeds.
+
+## Archipelago Text Client
+
+We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send.
+APDOOM has in-game messages,
+but they disappear quickly and there's no reasonable way to check your message history in-game.
+
+### Hinting
+
+To hint from in-game, use the chat (Default key: 'T'). Hinting from DOOM II can be difficult because names are rather long and contain special characters. For example:
+```
+!hint Underhalls (MAP02) - Red keycard
+```
+The game has a hint helper implemented, where you can simply type this:
+```
+!hint map02 red
+```
+For this to work, include the map short name (`MAP01`), followed by one of the keywords: `map`, `blue`, `yellow`, `red`.
+
+## Auto-Tracking
+
+APDOOM has a functional map tracker integrated into the level select screen.
+It tells you which levels you have unlocked, which keys you have for each level, which levels have been completed,
+and how many of the checks you have completed in each level.
diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py
index 58dbb6df83dd..23dfa0633eb4 100644
--- a/worlds/factorio/Client.py
+++ b/worlds/factorio/Client.py
@@ -21,7 +21,7 @@
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled, get_base_parser
from MultiServer import mark_raw
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart
-from Utils import async_start
+from Utils import async_start, get_file_safe_name
def check_stdin() -> None:
@@ -120,7 +120,7 @@ def on_print_json(self, args: dict):
@property
def savegame_name(self) -> str:
- return f"AP_{self.seed_name}_{self.auth}_Save.zip"
+ return get_file_safe_name(f"AP_{self.seed_name}_{self.auth}")+"_Save.zip"
def print_to_game(self, text):
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
@@ -247,7 +247,7 @@ async def game_watcher(ctx: FactorioContext):
if ctx.locations_checked != research_data:
bridge_logger.debug(
f"New researches done: "
- f"{[ctx.location_names[rid] for rid in research_data - ctx.locations_checked]}")
+ f"{[ctx.location_names.lookup_in_game(rid) for rid in research_data - ctx.locations_checked]}")
ctx.locations_checked = research_data
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
death_link_tick = data.get("death_link_tick", 0)
@@ -360,7 +360,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
item_id = transfer_item.item
player_name = ctx.player_names[transfer_item.player]
- item_name = ctx.item_names[item_id]
+ item_name = ctx.item_names.lookup_in_game(item_id)
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
commands[ctx.send_index] = f"/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}"
ctx.send_index += 1
@@ -446,6 +446,10 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
logger.warning("It appears your mods are loaded from Appdata, "
"this can lead to problems with multiple Factorio instances. "
"If this is the case, you will get a file locked error running Factorio.")
+ elif "Couldn't create lock file" in msg:
+ raise Exception(f"This Factorio (at {executable}) is either already running, "
+ "or a Factorio sharing data directories is already running. "
+ "Server could not start up.")
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
if ctx.mod_version == ctx.__class__.mod_version:
@@ -517,7 +521,7 @@ def _handle_color(self, node: JSONMessagePart):
rcon_password = args.rcon_password if args.rcon_password else ''.join(
random.choice(string.ascii_letters) for x in range(32))
factorio_server_logger = logging.getLogger("FactorioServer")
-options = Utils.get_options()
+options = Utils.get_settings()
executable = options["factorio_options"]["executable"]
server_settings = args.server_settings if args.server_settings \
else options["factorio_options"].get("server_settings", None)
diff --git a/worlds/factorio/Locations.py b/worlds/factorio/Locations.py
index f9db5f4a2bd8..52f0954cba30 100644
--- a/worlds/factorio/Locations.py
+++ b/worlds/factorio/Locations.py
@@ -3,18 +3,13 @@
from .Technologies import factorio_base_id
from .Options import MaxSciencePack
-boundary: int = 0xff
-total_locations: int = 0xff
-
-assert total_locations <= boundary
-
def make_pools() -> Dict[str, List[str]]:
pools: Dict[str, List[str]] = {}
for i, pack in enumerate(MaxSciencePack.get_ordered_science_packs(), start=1):
- max_needed: int = 0xff
+ max_needed: int = 999
prefix: str = f"AP-{i}-"
- pools[pack] = [prefix + hex(x)[2:].upper().zfill(2) for x in range(1, max_needed + 1)]
+ pools[pack] = [prefix + str(x).upper().zfill(3) for x in range(1, max_needed + 1)]
return pools
diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py
index 270e7dacf087..d7b3d4b1ebca 100644
--- a/worlds/factorio/Mod.py
+++ b/worlds/factorio/Mod.py
@@ -5,7 +5,7 @@
import shutil
import threading
import zipfile
-from typing import Optional, TYPE_CHECKING
+from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple, Union
import jinja2
@@ -24,6 +24,7 @@
data_final_template: Optional[jinja2.Template] = None
locale_template: Optional[jinja2.Template] = None
control_template: Optional[jinja2.Template] = None
+settings_template: Optional[jinja2.Template] = None
template_load_lock = threading.Lock()
@@ -62,15 +63,24 @@
class FactorioModFile(worlds.Files.APContainer):
game = "Factorio"
compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives
+ writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]]
+
+ def __init__(self, *args: Any, **kwargs: Any):
+ super().__init__(*args, **kwargs)
+ self.writing_tasks = []
def write_contents(self, opened_zipfile: zipfile.ZipFile):
# directory containing Factorio mod has to come first, or Factorio won't recognize this file as a mod.
mod_dir = self.path[:-4] # cut off .zip
for root, dirs, files in os.walk(mod_dir):
for file in files:
- opened_zipfile.write(os.path.join(root, file),
- os.path.relpath(os.path.join(root, file),
+ filename = os.path.join(root, file)
+ opened_zipfile.write(filename,
+ os.path.relpath(filename,
os.path.join(mod_dir, '..')))
+ for task in self.writing_tasks:
+ target, content = task()
+ opened_zipfile.writestr(target, content)
# now we can add extras.
super(FactorioModFile, self).write_contents(opened_zipfile)
@@ -98,6 +108,7 @@ def load_template(name: str):
locations = [(location, location.item)
for location in world.science_locations]
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}"
+ versioned_mod_name = mod_name + "_" + Utils.__version__
random = multiworld.per_slot_randoms[player]
@@ -153,48 +164,40 @@ def flop_random(low, high, base=None):
template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value})
template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value})
- control_code = control_template.render(**template_data)
- data_template_code = data_template.render(**template_data)
- data_final_fixes_code = data_final_template.render(**template_data)
- settings_code = settings_template.render(**template_data)
-
- mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__)
- en_locale_dir = os.path.join(mod_dir, "locale", "en")
- os.makedirs(en_locale_dir, exist_ok=True)
+ zf_path = os.path.join(output_directory, versioned_mod_name + ".zip")
+ mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player])
if world.zip_path:
- # Maybe investigate read from zip, write to zip, without temp file?
with zipfile.ZipFile(world.zip_path) as zf:
for file in zf.infolist():
if not file.is_dir() and "/data/mod/" in file.filename:
path_part = Utils.get_text_after(file.filename, "/data/mod/")
- target = os.path.join(mod_dir, path_part)
- os.makedirs(os.path.split(target)[0], exist_ok=True)
-
- with open(target, "wb") as f:
- f.write(zf.read(file))
+ mod.writing_tasks.append(lambda arcpath=versioned_mod_name+"/"+path_part, content=zf.read(file):
+ (arcpath, content))
else:
- shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True)
-
- with open(os.path.join(mod_dir, "data.lua"), "wt") as f:
- f.write(data_template_code)
- with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f:
- f.write(data_final_fixes_code)
- with open(os.path.join(mod_dir, "control.lua"), "wt") as f:
- f.write(control_code)
- with open(os.path.join(mod_dir, "settings.lua"), "wt") as f:
- f.write(settings_code)
- locale_content = locale_template.render(**template_data)
- with open(os.path.join(en_locale_dir, "locale.cfg"), "wt") as f:
- f.write(locale_content)
+ basepath = os.path.join(os.path.dirname(__file__), "data", "mod")
+ for dirpath, dirnames, filenames in os.walk(basepath):
+ base_arc_path = (versioned_mod_name+"/"+os.path.relpath(dirpath, basepath)).rstrip("/.\\")
+ for filename in filenames:
+ mod.writing_tasks.append(lambda arcpath=base_arc_path+"/"+filename,
+ file_path=os.path.join(dirpath, filename):
+ (arcpath, open(file_path, "rb").read()))
+
+ mod.writing_tasks.append(lambda: (versioned_mod_name + "/data.lua",
+ data_template.render(**template_data)))
+ mod.writing_tasks.append(lambda: (versioned_mod_name + "/data-final-fixes.lua",
+ data_final_template.render(**template_data)))
+ mod.writing_tasks.append(lambda: (versioned_mod_name + "/control.lua",
+ control_template.render(**template_data)))
+ mod.writing_tasks.append(lambda: (versioned_mod_name + "/settings.lua",
+ settings_template.render(**template_data)))
+ mod.writing_tasks.append(lambda: (versioned_mod_name + "/locale/en/locale.cfg",
+ locale_template.render(**template_data)))
+
info = base_info.copy()
info["name"] = mod_name
- with open(os.path.join(mod_dir, "info.json"), "wt") as f:
- json.dump(info, f, indent=4)
+ mod.writing_tasks.append(lambda: (versioned_mod_name + "/info.json",
+ json.dumps(info, indent=4)))
- # zip the result
- zf_path = os.path.join(mod_dir + ".zip")
- mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player])
+ # write the mod file
mod.write()
-
- shutil.rmtree(mod_dir)
diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py
index 2b579658fc7d..3429ebbd4251 100644
--- a/worlds/factorio/Options.py
+++ b/worlds/factorio/Options.py
@@ -2,7 +2,7 @@
import typing
import datetime
-from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink, Toggle, \
+from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \
StartInventoryPool
from schema import Schema, Optional, And, Or
@@ -207,11 +207,10 @@ class RecipeIngredientsOffset(Range):
range_end = 5
-class FactorioStartItems(ItemDict):
+class FactorioStartItems(OptionDict):
"""Mapping of Factorio internal item-name to amount granted on start."""
display_name = "Starting Items"
- verify_item_name = False
- default = {"burner-mining-drill": 19, "stone-furnace": 19}
+ default = {"burner-mining-drill": 4, "stone-furnace": 4, "raw-fish": 50}
class FactorioFreeSampleBlacklist(OptionSet):
@@ -390,8 +389,8 @@ class FactorioWorldGen(OptionDict):
def __init__(self, value: typing.Dict[str, typing.Any]):
advanced = {"pollution", "enemy_evolution", "enemy_expansion"}
self.value = {
- "basic": {key: value[key] for key in value.keys() - advanced},
- "advanced": {key: value[key] for key in value.keys() & advanced}
+ "basic": {k: v for k, v in value.items() if k not in advanced},
+ "advanced": {k: v for k, v in value.items() if k in advanced}
}
# verify min_values <= max_values
diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py
index d68c6f2f779e..096396c0e774 100644
--- a/worlds/factorio/Technologies.py
+++ b/worlds/factorio/Technologies.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-import json
+import orjson
import logging
import os
import string
@@ -20,7 +20,7 @@
def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]:
- return json.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json").decode())
+ return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json"))
techs_future = pool.submit(load_json_data, "techs")
diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py
index 8308bb2d6559..1ea2f6e4c98c 100644
--- a/worlds/factorio/__init__.py
+++ b/worlds/factorio/__init__.py
@@ -53,7 +53,7 @@ class BridgeChatOut(settings.Bool):
class FactorioWeb(WebWorld):
tutorials = [Tutorial(
- "Multiworld Setup Tutorial",
+ "Multiworld Setup Guide",
"A guide to setting up the Archipelago Factorio software on your computer.",
"English",
"setup_en.md",
@@ -95,7 +95,6 @@ class Factorio(World):
item_name_groups = {
"Progressive": set(progressive_tech_table.keys()),
}
- data_version = 8
required_client_version = (0, 4, 2)
ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs()
@@ -246,7 +245,8 @@ def set_rules(self):
location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \
(ingredient not in technology_table or state.has(ingredient, player)) and \
all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients
- for technology in required_technologies[sub_ingredient])
+ for technology in required_technologies[sub_ingredient]) and \
+ all(state.has(technology.name, player) for technology in required_technologies[custom_recipe.crafting_machine])
else:
location.access_rule = lambda state, ingredient=ingredient: \
all(state.has(technology.name, player) for technology in required_technologies[ingredient])
@@ -541,7 +541,7 @@ def __init__(self, player: int, name: str, address: int, parent: Region):
super(FactorioScienceLocation, self).__init__(player, name, address, parent)
# "AP-{Complexity}-{Cost}"
self.complexity = int(self.name[3]) - 1
- self.rel_cost = int(self.name[5:], 16)
+ self.rel_cost = int(self.name[5:])
self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1}
for complexity in range(self.complexity):
diff --git a/worlds/factorio/data/mod/graphics/icons/ap.png b/worlds/factorio/data/mod/graphics/icons/ap.png
index 8f0da105a19c..fa6b80cccafc 100644
Binary files a/worlds/factorio/data/mod/graphics/icons/ap.png and b/worlds/factorio/data/mod/graphics/icons/ap.png differ
diff --git a/worlds/factorio/data/mod/graphics/icons/ap_unimportant.png b/worlds/factorio/data/mod/graphics/icons/ap_unimportant.png
index 8471317a9379..68ee52a5e8e0 100644
Binary files a/worlds/factorio/data/mod/graphics/icons/ap_unimportant.png and b/worlds/factorio/data/mod/graphics/icons/ap_unimportant.png differ
diff --git a/worlds/factorio/data/mod/info.json b/worlds/factorio/data/mod/info.json
deleted file mode 100644
index 70a951834428..000000000000
--- a/worlds/factorio/data/mod/info.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "name": "archipelago-client",
- "version": "0.0.1",
- "title": "Archipelago",
- "author": "Berserker and Dewiniaid",
- "homepage": "https://archipelago.gg",
- "description": "Integration client for the Archipelago Randomizer",
- "factorio_version": "1.1",
- "dependencies": [
- "base >= 1.1.0",
- "? science-not-invited",
- "? factory-levels"
- ]
-}
diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua
index 8ce0b45a5f67..ace231e12b4b 100644
--- a/worlds/factorio/data/mod_template/control.lua
+++ b/worlds/factorio/data/mod_template/control.lua
@@ -660,11 +660,18 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
end
local tech
local force = game.forces["player"]
+ if call.parameter == nil then
+ game.print("ap-get-technology is only to be used by the Archipelago Factorio Client")
+ return
+ end
chunks = split(call.parameter, "\t")
local item_name = chunks[1]
local index = chunks[2]
local source = chunks[3] or "Archipelago"
- if index == -1 then -- for coop sync and restoring from an older savegame
+ if index == nil then
+ game.print("ap-get-technology is only to be used by the Archipelago Factorio Client")
+ return
+ elseif index == -1 then -- for coop sync and restoring from an older savegame
tech = force.technologies[item_name]
if tech.researched ~= true then
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
diff --git a/worlds/factorio/docs/en_Factorio.md b/worlds/factorio/docs/en_Factorio.md
index 61bceb3820a1..94d2a5505bda 100644
--- a/worlds/factorio/docs/en_Factorio.md
+++ b/worlds/factorio/docs/en_Factorio.md
@@ -1,8 +1,8 @@
# Factorio
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -36,9 +36,15 @@ inventory.
## What is EnergyLink?
EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld.
-In Factorio, if enabled in the player settings, EnergyLink Bridge buildings can be crafted and placed, which allow
+In Factorio, if enabled in the player options, EnergyLink Bridge buildings can be crafted and placed, which allow
depositing excess energy and supplementing energy deficits, much like Accumulators.
Each placed EnergyLink Bridge provides 10 MW of throughput. The shared storage has unlimited capacity, but 25% of energy
is lost during depositing. The amount of energy currently in the shared storage is displayed in the Archipelago client.
It can also be queried by typing `/energy-link` in-game.
+
+## Unique Local Commands
+The following commands are only available when using the FactorioClient to play Factorio with Archipelago.
+
+- `/factorio ` Sends the command argument to the Factorio server as a command.
+- `/energy-link` Displays the amount of energy currently in shared storage for EnergyLink
diff --git a/worlds/factorio/docs/setup_en.md b/worlds/factorio/docs/setup_en.md
index 09ad431a21cc..0b4ccee0d7f2 100644
--- a/worlds/factorio/docs/setup_en.md
+++ b/worlds/factorio/docs/setup_en.md
@@ -25,13 +25,13 @@ options.
### Where do I get a config file?
-The Player Settings page on the website allows you to configure your personal settings and export a config file from
-them. Factorio player settings page: [Factorio Settings Page](/games/Factorio/player-settings)
+The Player Options page on the website allows you to configure your personal options and export a config file from
+them. Factorio player options page: [Factorio Options Page](/games/Factorio/player-options)
### 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
@@ -133,7 +133,7 @@ This allows you to host your own Factorio game.
For additional client features, issue the `/help` command in the Archipelago Client. Once connected to the AP server,
you can also issue the `!help` command to learn about additional commands like `!hint`.
For more information about the commands you can use, see the [Commands Guide](/tutorial/Archipelago/commands/en) and
-[Other Settings](#other-settings).
+[Other Options](#other-options).
## Allowing Other People to Join Your Game
@@ -148,11 +148,11 @@ For more information about the commands you can use, see the [Commands Guide](/t
By default, peaceful mode is disabled. There are two methods to enable peaceful mode:
### By config file
-You can specify Factorio game settings such as peaceful mode and terrain and resource generation parameters in your
-config .yaml file by including the `world_gen` setting. This setting is currently not supported by the web UI, so you'll
+You can specify Factorio game options such as peaceful mode and terrain and resource generation parameters in your
+config .yaml file by including the `world_gen` option. This option is currently not supported by the web UI, so you'll
have to manually create or edit your config file with a text editor of your choice.
The [template file](/static/generated/configs/Factorio.yaml) is a good starting point and contains the default value of
-the `world_gen` setting. If you already have a config file you may also just copy that setting over from the template.
+the `world_gen` option. If you already have a config file you may also just copy that option over from the template.
To enable peaceful mode, simply replace `peaceful_mode: false` with `peaceful_mode: true`. Finally, use the
[.yaml checker](/check) to ensure your file is valid.
@@ -165,7 +165,7 @@ enable peaceful mode by entering the following commands into your Archipelago Fa
```
(If this warns you that these commands may disable achievements, you may need to repeat them for them to take effect.)
-## Other Settings
+## Other Options
### filter_item_sends
diff --git a/worlds/factorio/requirements.txt b/worlds/factorio/requirements.txt
index c45fb771da6a..8d684401663b 100644
--- a/worlds/factorio/requirements.txt
+++ b/worlds/factorio/requirements.txt
@@ -1 +1 @@
-factorio-rcon-py>=2.0.1
+factorio-rcon-py>=2.1.2
diff --git a/worlds/ff1/Options.py b/worlds/ff1/Options.py
index 0993d103d575..d8d24a529f36 100644
--- a/worlds/ff1/Options.py
+++ b/worlds/ff1/Options.py
@@ -1,6 +1,6 @@
-from typing import Dict
+from dataclasses import dataclass
-from Options import OptionDict
+from Options import OptionDict, PerGameCommonOptions
class Locations(OptionDict):
@@ -18,8 +18,8 @@ class Rules(OptionDict):
display_name = "rules"
-ff1_options: Dict[str, OptionDict] = {
- "locations": Locations,
- "items": Items,
- "rules": Rules
-}
+@dataclass
+class FF1Options(PerGameCommonOptions):
+ locations: Locations
+ items: Items
+ rules: Rules
diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py
index 56b41d62d042..3a5047506850 100644
--- a/worlds/ff1/__init__.py
+++ b/worlds/ff1/__init__.py
@@ -5,7 +5,7 @@
from BaseClasses import Item, Location, MultiWorld, Tutorial, ItemClassification
from .Items import ItemData, FF1Items, FF1_STARTER_ITEMS, FF1_PROGRESSION_LIST, FF1_BRIDGE
from .Locations import EventId, FF1Locations, generate_rule, CHAOS_TERMINATED_EVENT
-from .Options import ff1_options
+from .Options import FF1Options
from ..AutoWorld import World, WebWorld
@@ -14,7 +14,7 @@ class FF1Settings(settings.Group):
class FF1Web(WebWorld):
- settings_page = "https://finalfantasyrandomizer.com/"
+ options_page = "https://finalfantasyrandomizer.com/"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to playing Final Fantasy multiworld. This guide only covers playing multiworld.",
@@ -34,12 +34,12 @@ class FF1World(World):
Part puzzle and part speed-run, it breathes new life into one of the most influential games ever made.
"""
- option_definitions = ff1_options
+ options: FF1Options
+ options_dataclass = FF1Options
settings: typing.ClassVar[FF1Settings]
settings_key = "ffr_options"
game = "Final Fantasy"
topology_present = False
- data_version = 2
ff1_items = FF1Items()
ff1_locations = FF1Locations()
@@ -58,22 +58,23 @@ def __init__(self, world: MultiWorld, player: int):
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
# Fail generation if there are no items in the pool
for player in multiworld.get_game_players(cls.game):
- options = get_options(multiworld, 'items', player)
- assert options,\
+ items = multiworld.worlds[player].options.items.value
+ assert items, \
f"FFR settings submitted with no key items ({multiworld.get_player_name(player)}). Please ensure you " \
f"generated the settings using finalfantasyrandomizer.com AND enabled the AP flag"
def create_regions(self):
- locations = get_options(self.multiworld, 'locations', self.player)
- rules = get_options(self.multiworld, 'rules', self.player)
+ locations = self.options.locations.value
+ rules = self.options.rules.value
menu_region = self.ff1_locations.create_menu_region(self.player, locations, rules, self.multiworld)
terminated_event = Location(self.player, CHAOS_TERMINATED_EVENT, EventId, menu_region)
terminated_item = Item(CHAOS_TERMINATED_EVENT, ItemClassification.progression, EventId, self.player)
terminated_event.place_locked_item(terminated_item)
- items = get_options(self.multiworld, 'items', self.player)
+ items = self.options.items.value
goal_rule = generate_rule([[name for name in items.keys() if name in FF1_PROGRESSION_LIST and name != "Shard"]],
self.player)
+ terminated_event.access_rule = goal_rule
if "Shard" in items.keys():
def goal_rule_and_shards(state):
return goal_rule(state) and state.has("Shard", self.player, 32)
@@ -91,8 +92,8 @@ def create_item(self, name: str) -> Item:
def set_rules(self):
self.multiworld.completion_condition[self.player] = lambda state: state.has(CHAOS_TERMINATED_EVENT, self.player)
- def generate_basic(self):
- items = get_options(self.multiworld, 'items', self.player)
+ def create_items(self):
+ items = self.options.items.value
if FF1_BRIDGE in items.keys():
self._place_locked_item_in_sphere0(FF1_BRIDGE)
if items:
@@ -108,7 +109,7 @@ def generate_basic(self):
def _place_locked_item_in_sphere0(self, progression_item: str):
if progression_item:
- rules = get_options(self.multiworld, 'rules', self.player)
+ rules = self.options.rules.value
sphere_0_locations = [name for name, rules in rules.items()
if rules and len(rules[0]) == 0 and name not in self.locked_locations]
if sphere_0_locations:
@@ -125,7 +126,3 @@ def fill_slot_data(self) -> Dict[str, object]:
def get_filler_item_name(self) -> str:
return self.multiworld.random.choice(["Heal", "Pure", "Soft", "Tent", "Cabin", "House"])
-
-
-def get_options(world: MultiWorld, name: str, player: int):
- return getattr(world, name, None)[player].value
diff --git a/worlds/ff1/data/locations.json b/worlds/ff1/data/locations.json
index 9771d51de088..2f465a78970e 100644
--- a/worlds/ff1/data/locations.json
+++ b/worlds/ff1/data/locations.json
@@ -1,253 +1,253 @@
{
- "Coneria1": 257,
- "Coneria2": 258,
- "ConeriaMajor": 259,
- "Coneria4": 260,
- "Coneria5": 261,
- "Coneria6": 262,
- "MatoyasCave1": 299,
- "MatoyasCave3": 301,
- "MatoyasCave2": 300,
- "NorthwestCastle1": 273,
- "NorthwestCastle3": 275,
- "NorthwestCastle2": 274,
- "ToFTopLeft1": 263,
- "ToFBottomLeft": 265,
- "ToFTopLeft2": 264,
- "ToFRevisited6": 509,
- "ToFRevisited4": 507,
- "ToFRMasmune": 504,
- "ToFRevisited5": 508,
- "ToFRevisited3": 506,
- "ToFRevisited2": 505,
- "ToFRevisited7": 510,
- "ToFTopRight1": 267,
- "ToFTopRight2": 268,
- "ToFBottomRight": 266,
- "IceCave15": 377,
- "IceCave16": 378,
- "IceCave9": 371,
- "IceCave11": 373,
- "IceCave10": 372,
- "IceCave12": 374,
- "IceCave13": 375,
- "IceCave14": 376,
- "IceCave1": 363,
- "IceCave2": 364,
- "IceCave3": 365,
- "IceCave4": 366,
- "IceCave5": 367,
- "IceCaveMajor": 370,
- "IceCave7": 369,
- "IceCave6": 368,
- "Elfland1": 269,
- "Elfland2": 270,
- "Elfland3": 271,
- "Elfland4": 272,
- "Ordeals5": 383,
- "Ordeals6": 384,
- "Ordeals7": 385,
- "Ordeals1": 379,
- "Ordeals2": 380,
- "Ordeals3": 381,
- "Ordeals4": 382,
- "OrdealsMajor": 387,
- "Ordeals8": 386,
- "SeaShrine7": 411,
- "SeaShrine8": 412,
- "SeaShrine9": 413,
- "SeaShrine10": 414,
- "SeaShrine1": 405,
- "SeaShrine2": 406,
- "SeaShrine3": 407,
- "SeaShrine4": 408,
- "SeaShrine5": 409,
- "SeaShrine6": 410,
- "SeaShrine13": 417,
- "SeaShrine14": 418,
- "SeaShrine11": 415,
- "SeaShrine15": 419,
- "SeaShrine16": 420,
- "SeaShrineLocked": 421,
- "SeaShrine18": 422,
- "SeaShrine19": 423,
- "SeaShrine20": 424,
- "SeaShrine23": 427,
- "SeaShrine21": 425,
- "SeaShrine22": 426,
- "SeaShrine24": 428,
- "SeaShrine26": 430,
- "SeaShrine28": 432,
- "SeaShrine25": 429,
- "SeaShrine30": 434,
- "SeaShrine31": 435,
- "SeaShrine27": 431,
- "SeaShrine29": 433,
- "SeaShrineMajor": 436,
- "SeaShrine12": 416,
- "DwarfCave3": 291,
- "DwarfCave4": 292,
- "DwarfCave6": 294,
- "DwarfCave7": 295,
- "DwarfCave5": 293,
- "DwarfCave8": 296,
- "DwarfCave9": 297,
- "DwarfCave10": 298,
- "DwarfCave1": 289,
- "DwarfCave2": 290,
- "Waterfall1": 437,
- "Waterfall2": 438,
- "Waterfall3": 439,
- "Waterfall4": 440,
- "Waterfall5": 441,
- "Waterfall6": 442,
- "MirageTower5": 456,
- "MirageTower16": 467,
- "MirageTower17": 468,
- "MirageTower15": 466,
- "MirageTower18": 469,
- "MirageTower14": 465,
- "SkyPalace1": 470,
- "SkyPalace2": 471,
- "SkyPalace3": 472,
- "SkyPalace4": 473,
- "SkyPalace18": 487,
- "SkyPalace19": 488,
- "SkyPalace16": 485,
- "SkyPalaceMajor": 489,
- "SkyPalace17": 486,
- "SkyPalace22": 491,
- "SkyPalace21": 490,
- "SkyPalace23": 492,
- "SkyPalace24": 493,
- "SkyPalace31": 500,
- "SkyPalace32": 501,
- "SkyPalace33": 502,
- "SkyPalace34": 503,
- "SkyPalace29": 498,
- "SkyPalace26": 495,
- "SkyPalace25": 494,
- "SkyPalace28": 497,
- "SkyPalace27": 496,
- "SkyPalace30": 499,
- "SkyPalace14": 483,
- "SkyPalace11": 480,
- "SkyPalace12": 481,
- "SkyPalace13": 482,
- "SkyPalace15": 484,
- "SkyPalace10": 479,
- "SkyPalace5": 474,
- "SkyPalace6": 475,
- "SkyPalace7": 476,
- "SkyPalace8": 477,
- "SkyPalace9": 478,
- "MirageTower9": 460,
- "MirageTower13": 464,
- "MirageTower10": 461,
- "MirageTower12": 463,
- "MirageTower11": 462,
- "MirageTower1": 452,
- "MirageTower2": 453,
- "MirageTower4": 455,
- "MirageTower3": 454,
- "MirageTower8": 459,
- "MirageTower7": 458,
- "MirageTower6": 457,
- "Volcano30": 359,
- "Volcano32": 361,
- "Volcano31": 360,
- "Volcano28": 357,
- "Volcano29": 358,
- "Volcano21": 350,
- "Volcano20": 349,
- "Volcano24": 353,
- "Volcano19": 348,
- "Volcano25": 354,
- "VolcanoMajor": 362,
- "Volcano26": 355,
- "Volcano27": 356,
- "Volcano22": 351,
- "Volcano23": 352,
- "Volcano1": 330,
- "Volcano9": 338,
- "Volcano2": 331,
- "Volcano10": 339,
- "Volcano3": 332,
- "Volcano8": 337,
- "Volcano4": 333,
- "Volcano13": 342,
- "Volcano11": 340,
- "Volcano7": 336,
- "Volcano6": 335,
- "Volcano5": 334,
- "Volcano14": 343,
- "Volcano12": 341,
- "Volcano15": 344,
- "Volcano18": 347,
- "Volcano17": 346,
- "Volcano16": 345,
- "MarshCave6": 281,
- "MarshCave5": 280,
- "MarshCave7": 282,
- "MarshCave8": 283,
- "MarshCave10": 285,
- "MarshCave2": 277,
- "MarshCave11": 286,
- "MarshCave3": 278,
- "MarshCaveMajor": 284,
- "MarshCave12": 287,
- "MarshCave4": 279,
- "MarshCave1": 276,
- "MarshCave13": 288,
- "TitansTunnel1": 326,
- "TitansTunnel2": 327,
- "TitansTunnel3": 328,
- "TitansTunnel4": 329,
- "EarthCave1": 302,
- "EarthCave2": 303,
- "EarthCave5": 306,
- "EarthCave3": 304,
- "EarthCave4": 305,
- "EarthCave9": 310,
- "EarthCave10": 311,
- "EarthCave11": 312,
- "EarthCave6": 307,
- "EarthCave7": 308,
- "EarthCave12": 313,
- "EarthCaveMajor": 317,
- "EarthCave19": 320,
- "EarthCave17": 318,
- "EarthCave18": 319,
- "EarthCave20": 321,
- "EarthCave24": 325,
- "EarthCave21": 322,
- "EarthCave22": 323,
- "EarthCave23": 324,
- "EarthCave13": 314,
- "EarthCave15": 316,
- "EarthCave14": 315,
- "EarthCave8": 309,
- "Cardia11": 398,
- "Cardia9": 396,
- "Cardia10": 397,
- "Cardia6": 393,
- "Cardia8": 395,
- "Cardia7": 394,
- "Cardia13": 400,
- "Cardia12": 399,
- "Cardia4": 391,
- "Cardia5": 392,
- "Cardia3": 390,
- "Cardia1": 388,
- "Cardia2": 389,
- "CaravanShop": 767,
+ "Matoya's Cave - Chest 1": 299,
+ "Matoya's Cave - Chest 2": 301,
+ "Matoya's Cave - Chest 3": 300,
+ "Dwarf Cave - Entrance 1": 289,
+ "Dwarf Cave - Entrance 2": 290,
+ "Dwarf Cave - Treasury 1": 291,
+ "Dwarf Cave - Treasury 2": 292,
+ "Dwarf Cave - Treasury 3": 295,
+ "Dwarf Cave - Treasury 4": 293,
+ "Dwarf Cave - Treasury 5": 294,
+ "Dwarf Cave - Treasury 6": 296,
+ "Dwarf Cave - Treasury 7": 297,
+ "Dwarf Cave - Treasury 8": 298,
+ "Coneria Castle - Treasury 1": 257,
+ "Coneria Castle - Treasury 2": 258,
+ "Coneria Castle - Treasury 3": 260,
+ "Coneria Castle - Treasury 4": 261,
+ "Coneria Castle - Treasury 5": 262,
+ "Coneria Castle - Treasury Major": 259,
+ "Elf Castle - Treasury 1": 269,
+ "Elf Castle - Treasury 2": 270,
+ "Elf Castle - Treasury 3": 271,
+ "Elf Castle - Treasury 4": 272,
+ "Northwest Castle - Treasury 1": 273,
+ "Northwest Castle - Treasury 2": 275,
+ "Northwest Castle - Treasury 3": 274,
+ "Titan's Tunnel - Chest 1": 327,
+ "Titan's Tunnel - Chest 2": 328,
+ "Titan's Tunnel - Chest 3": 329,
+ "Titan's Tunnel - Major": 326,
+ "Cardia Grass Island - Entrance": 398,
+ "Cardia Grass Island - Duo Room 1": 396,
+ "Cardia Grass Island - Duo Rooom 2": 397,
+ "Cardia Swamp Island - Chest 1": 393,
+ "Cardia Swamp Island - Chest 2": 395,
+ "Cardia Swamp Island - Chest 3": 394,
+ "Cardia Forest Island - Entrance 1": 389,
+ "Cardia Forest Island - Entrance 2": 388,
+ "Cardia Forest Island - Entrance 3": 390,
+ "Cardia Forest Island - Incentive 1": 400,
+ "Cardia Forest Island - Incentive 2": 399,
+ "Cardia Forest Island - Incentive 3": 392,
+ "Cardia Forest Island - Incentive Major": 391,
+ "Temple of Fiends - Unlocked Single": 265,
+ "Temple of Fiends - Unlocked Duo 1": 263,
+ "Temple of Fiends - Unlocked Duo 2": 264,
+ "Temple of Fiends - Locked Single": 266,
+ "Temple of Fiends - Locked Duo 1": 267,
+ "Temple of Fiends - Locked Duo 2": 268,
+ "Marsh Cave Top (B1) - Single": 283,
+ "Marsh Cave Top (B1) - Corner": 282,
+ "Marsh Cave Top (B1) - Duo 1": 281,
+ "Marsh Cave Top (B1) - Duo 2": 280,
+ "Marsh Cave Bottom (B2) - Distant": 276,
+ "Marsh Cave Bottom (B2) - Tetris-Z First": 277,
+ "Marsh Cave Bottom (B2) - Tetris-Z Middle 1": 278,
+ "Marsh Cave Bottom (B2) - Tetris-Z Middle 2": 285,
+ "Marsh Cave Bottom (B2) - Tetris-Z Incentive": 284,
+ "Marsh Cave Bottom (B2) - Tetris-Z Last": 279,
+ "Marsh Cave Bottom (B2) - Locked Corner": 286,
+ "Marsh Cave Bottom (B2) - Locked Middle": 287,
+ "Marsh Cave Bottom (B2) - Locked Incentive": 288,
+ "Earth Cave Giant's Floor (B1) - Single": 306,
+ "Earth Cave Giant's Floor (B1) - Appendix 1": 302,
+ "Earth Cave Giant's Floor (B1) - Appendix 2": 303,
+ "Earth Cave Giant's Floor (B1) - Side Path 1": 304,
+ "Earth Cave Giant's Floor (B1) - Side Path 2": 305,
+ "Earth Cave (B2) - Side Room 1": 307,
+ "Earth Cave (B2) - Side Room 2": 308,
+ "Earth Cave (B2) - Side Room 3": 309,
+ "Earth Cave (B2) - Guarded 1": 310,
+ "Earth Cave (B2) - Guarded 2": 311,
+ "Earth Cave (B2) - Guarded 3": 312,
+ "Earth Cave Vampire Floor (B3) - Side Room": 315,
+ "Earth Cave Vampire Floor (B3) - TFC": 316,
+ "Earth Cave Vampire Floor (B3) - Asher Trunk": 314,
+ "Earth Cave Vampire Floor (B3) - Vampire's Closet": 313,
+ "Earth Cave Vampire Floor (B3) - Incentive": 317,
+ "Earth Cave Rod Locked Floor (B4) - Armory 1": 321,
+ "Earth Cave Rod Locked Floor (B4) - Armory 2": 322,
+ "Earth Cave Rod Locked Floor (B4) - Armory 3": 325,
+ "Earth Cave Rod Locked Floor (B4) - Armory 4": 323,
+ "Earth Cave Rod Locked Floor (B4) - Armory 5": 324,
+ "Earth Cave Rod Locked Floor (B4) - Lich's Closet 1": 318,
+ "Earth Cave Rod Locked Floor (B4) - Lich's Closet 2": 319,
+ "Earth Cave Rod Locked Floor (B4) - Lich's Closet 3": 320,
+ "Gurgu Volcano Armory Floor (B2) - Guarded": 346,
+ "Gurgu Volcano Armory Floor (B2) - Center": 347,
+ "Gurgu Volcano Armory Floor (B2) - Hairpins": 344,
+ "Gurgu Volcano Armory Floor (B2) - Shortpins": 345,
+ "Gurgu Volcano Armory Floor (B2) - Vertpins 1": 342,
+ "Gurgu Volcano Armory Floor (B2) - Vertpins 2": 343,
+ "Gurgu Volcano Armory Floor (B2) - Armory 1": 338,
+ "Gurgu Volcano Armory Floor (B2) - Armory 2": 330,
+ "Gurgu Volcano Armory Floor (B2) - Armory 3": 331,
+ "Gurgu Volcano Armory Floor (B2) - Armory 4": 337,
+ "Gurgu Volcano Armory Floor (B2) - Armory 5": 335,
+ "Gurgu Volcano Armory Floor (B2) - Armory 6": 332,
+ "Gurgu Volcano Armory Floor (B2) - Armory 7": 333,
+ "Gurgu Volcano Armory Floor (B2) - Armory 8": 334,
+ "Gurgu Volcano Armory Floor (B2) - Armory 9": 341,
+ "Gurgu Volcano Armory Floor (B2) - Armory 10": 336,
+ "Gurgu Volcano Armory Floor (B2) - Armory 11": 340,
+ "Gurgu Volcano Armory Floor (B2) - Armory 12": 339,
+ "Gurgu Volcano Agama Floor (B4) - Entrance 1": 349,
+ "Gurgu Volcano Agama Floor (B4) - Entrance 2": 348,
+ "Gurgu Volcano Agama Floor (B4) - First Greed": 350,
+ "Gurgu Volcano Agama Floor (B4) - Worm Room 1": 361,
+ "Gurgu Volcano Agama Floor (B4) - Worm Room 2": 359,
+ "Gurgu Volcano Agama Floor (B4) - Worm Room 3": 360,
+ "Gurgu Volcano Agama Floor (B4) - Worm Room 4": 357,
+ "Gurgu Volcano Agama Floor (B4) - Worm Room 5": 358,
+ "Gurgu Volcano Agama Floor (B4) - Second Greed 1": 353,
+ "Gurgu Volcano Agama Floor (B4) - Second Greed 2": 354,
+ "Gurgu Volcano Agama Floor (B4) - Side Room 1": 355,
+ "Gurgu Volcano Agama Floor (B4) - Side Room 2": 356,
+ "Gurgu Volcano Agama Floor (B4) - Grind Room 1": 351,
+ "Gurgu Volcano Agama Floor (B4) - Grind Room 2": 352,
+ "Gurgu Volcano Kary Floor (B5) - Incentive": 362,
+ "Ice Cave Incentive Floor (B2) - Chest 1": 368,
+ "Ice Cave Incentive Floor (B2) - Chest 2": 369,
+ "Ice Cave Incentive Floor (B2) - Major": 370,
+ "Ice Cave Bottom (B3) - IceD Room 1": 377,
+ "Ice Cave Bottom (B3) - IceD Room 2": 378,
+ "Ice Cave Bottom (B3) - Six-Pack 1": 371,
+ "Ice Cave Bottom (B3) - Six-Pack 2": 372,
+ "Ice Cave Bottom (B3) - Six-Pack 3": 375,
+ "Ice Cave Bottom (B3) - Six-Pack 4": 373,
+ "Ice Cave Bottom (B3) - Six-Pack 5": 374,
+ "Ice Cave Bottom (B3) - Six-Pack 6": 376,
+ "Ice Cave Exit Floor (B1) - Greeds Checks 1": 363,
+ "Ice Cave Exit Floor (B1) - Greeds Checks 2": 364,
+ "Ice Cave Exit Floor (B1) - Drop Room 1": 365,
+ "Ice Cave Exit Floor (B1) - Drop Room 2": 366,
+ "Ice Cave Exit Floor (B1) - Drop Room 3": 367,
+ "Castle of Ordeals Top Floor (3F) - Single": 386,
+ "Castle of Ordeals Top Floor (3F) - Three-Pack 1": 383,
+ "Castle of Ordeals Top Floor (3F) - Three-Pack 2": 384,
+ "Castle of Ordeals Top Floor (3F) - Three-Pack 3": 385,
+ "Castle of Ordeals Top Floor (3F) - Four-Pack 1": 379,
+ "Castle of Ordeals Top Floor (3F) - Four-Pack 2": 380,
+ "Castle of Ordeals Top Floor (3F) - Four-Pack 3": 381,
+ "Castle of Ordeals Top Floor (3F) - Four-Pack 4": 382,
+ "Castle of Ordeals Top Floor (3F) - Incentive": 387,
+ "Sea Shrine Split Floor (B3) - Kraken Side": 415,
+ "Sea Shrine Split Floor (B3) - Mermaid Side": 416,
+ "Sea Shrine TFC Floor (B2) - TFC": 421,
+ "Sea Shrine TFC Floor (B2) - TFC North": 420,
+ "Sea Shrine TFC Floor (B2) - Side Corner": 419,
+ "Sea Shrine TFC Floor (B2) - First Greed": 422,
+ "Sea Shrine TFC Floor (B2) - Second Greed": 423,
+ "Sea Shrine Mermaids (B1) - Passby": 427,
+ "Sea Shrine Mermaids (B1) - Bubbles 1": 428,
+ "Sea Shrine Mermaids (B1) - Bubbles 2": 429,
+ "Sea Shrine Mermaids (B1) - Incentive 1": 434,
+ "Sea Shrine Mermaids (B1) - Incentive 2": 435,
+ "Sea Shrine Mermaids (B1) - Incentive Major": 436,
+ "Sea Shrine Mermaids (B1) - Entrance 1": 424,
+ "Sea Shrine Mermaids (B1) - Entrance 2": 425,
+ "Sea Shrine Mermaids (B1) - Entrance 3": 426,
+ "Sea Shrine Mermaids (B1) - Four-Corner First": 430,
+ "Sea Shrine Mermaids (B1) - Four-Corner Second": 431,
+ "Sea Shrine Mermaids (B1) - Four-Corner Third": 432,
+ "Sea Shrine Mermaids (B1) - Four-Corner Fourth": 433,
+ "Sea Shrine Greed Floor (B3) - Chest 1": 418,
+ "Sea Shrine Greed Floor (B3) - Chest 2": 417,
+ "Sea Shrine Sharknado Floor (B4) - Dengbait 1": 409,
+ "Sea Shrine Sharknado Floor (B4) - Dengbait 2": 410,
+ "Sea Shrine Sharknado Floor (B4) - Side Corner 1": 411,
+ "Sea Shrine Sharknado Floor (B4) - Side Corner 2": 412,
+ "Sea Shrine Sharknado Floor (B4) - Side Corner 3": 413,
+ "Sea Shrine Sharknado Floor (B4) - Exit": 414,
+ "Sea Shrine Sharknado Floor (B4) - Greed Room 1": 405,
+ "Sea Shrine Sharknado Floor (B4) - Greed Room 2": 406,
+ "Sea Shrine Sharknado Floor (B4) - Greed Room 3": 407,
+ "Sea Shrine Sharknado Floor (B4) - Greed Room 4": 408,
+ "Waterfall Cave - Chest 1": 437,
+ "Waterfall Cave - Chest 2": 438,
+ "Waterfall Cave - Chest 3": 439,
+ "Waterfall Cave - Chest 4": 440,
+ "Waterfall Cave - Chest 5": 441,
+ "Waterfall Cave - Chest 6": 442,
+ "Mirage Tower (1F) - Chest 1": 456,
+ "Mirage Tower (1F) - Chest 2": 452,
+ "Mirage Tower (1F) - Chest 3": 453,
+ "Mirage Tower (1F) - Chest 4": 455,
+ "Mirage Tower (1F) - Chest 5": 454,
+ "Mirage Tower (1F) - Chest 6": 459,
+ "Mirage Tower (1F) - Chest 7": 457,
+ "Mirage Tower (1F) - Chest 8": 458,
+ "Mirage Tower (2F) - Lesser 1": 469,
+ "Mirage Tower (2F) - Lesser 2": 468,
+ "Mirage Tower (2F) - Lesser 3": 467,
+ "Mirage Tower (2F) - Lesser 4": 466,
+ "Mirage Tower (2F) - Lesser 5": 465,
+ "Mirage Tower (2F) - Greater 1": 460,
+ "Mirage Tower (2F) - Greater 2": 461,
+ "Mirage Tower (2F) - Greater 3": 462,
+ "Mirage Tower (2F) - Greater 4": 463,
+ "Mirage Tower (2F) - Greater 5": 464,
+ "Sky Fortress Plus (1F) - Solo": 479,
+ "Sky Fortress Plus (1F) - Five-Pack 1": 474,
+ "Sky Fortress Plus (1F) - Five-Pack 2": 475,
+ "Sky Fortress Plus (1F) - Five-Pack 3": 476,
+ "Sky Fortress Plus (1F) - Five-Pack 4": 477,
+ "Sky Fortress Plus (1F) - Five-Pack 5": 478,
+ "Sky Fortress Plus (1F) - Four-Pack 1": 470,
+ "Sky Fortress Plus (1F) - Four-Pack 2": 471,
+ "Sky Fortress Plus (1F) - Four-Pack 3": 472,
+ "Sky Fortress Plus (1F) - Four-Pack 4": 473,
+ "Sky Fortress Spider (2F) - Cheap Room 1": 485,
+ "Sky Fortress Spider (2F) - Cheap Room 2": 486,
+ "Sky Fortress Spider (2F) - Vault 1": 487,
+ "Sky Fortress Spider (2F) - Vault 2": 488,
+ "Sky Fortress Spider (2F) - Incentive": 489,
+ "Sky Fortress Spider (2F) - Gauntlet Room": 483,
+ "Sky Fortress Spider (2F) - Ribbon Room 1": 482,
+ "Sky Fortress Spider (2F) - Ribbon Room 2": 484,
+ "Sky Fortress Spider (2F) - Wardrobe 1": 480,
+ "Sky Fortress Spider (2F) - Wardrobe 2": 481,
+ "Sky Fortress Provides (3F) - Six-Pack 1": 498,
+ "Sky Fortress Provides (3F) - Six-Pack 2": 495,
+ "Sky Fortress Provides (3F) - Six-Pack 3": 494,
+ "Sky Fortress Provides (3F) - Six-Pack 4": 497,
+ "Sky Fortress Provides (3F) - Six-Pack 5": 496,
+ "Sky Fortress Provides (3F) - Six-Pack 6": 499,
+ "Sky Fortress Provides (3F) - CC's Gambit 1": 500,
+ "Sky Fortress Provides (3F) - CC's Gambit 2": 501,
+ "Sky Fortress Provides (3F) - CC's Gambit 3": 502,
+ "Sky Fortress Provides (3F) - CC's Gambit 4": 503,
+ "Sky Fortress Provides (3F) - Greed 1": 491,
+ "Sky Fortress Provides (3F) - Greed 2": 490,
+ "Sky Fortress Provides (3F) - Greed 3": 492,
+ "Sky Fortress Provides (3F) - Greed 4": 493,
+ "Temple of Fiends Revisited (3F) - Validation 1": 509,
+ "Temple of Fiends Revisited (3F) - Validation 2": 510,
+ "Temple of Fiends Revisited Kary Floor (6F) - Greed Checks 1": 507,
+ "Temple of Fiends Revisited Kary Floor (6F) - Greed Checks 2": 508,
+ "Temple of Fiends Revisited Kary Floor (6F) - Katana Chest": 506,
+ "Temple of Fiends Revisited Kary Floor (6F) - Vault": 505,
+ "Temple of Fiends Revisited Tiamat Floor (8F) - Masamune Chest": 504,
+ "Shop Item": 767,
"King": 513,
- "Princess2": 530,
+ "Princess": 530,
"Matoya": 522,
"Astos": 519,
"Bikke": 516,
- "CanoeSage": 533,
- "ElfPrince": 518,
+ "Canoe Sage": 533,
+ "Elf Prince": 518,
"Nerrick": 520,
"Smith": 521,
"CubeBot": 529,
diff --git a/worlds/ff1/docs/en_Final Fantasy.md b/worlds/ff1/docs/en_Final Fantasy.md
index 29d4d29f8094..889bb46e0c35 100644
--- a/worlds/ff1/docs/en_Final Fantasy.md
+++ b/worlds/ff1/docs/en_Final Fantasy.md
@@ -1,9 +1,9 @@
# Final Fantasy 1 (NES)
-## Where is the settings page?
+## Where is the options page?
-Unlike most games on Archipelago.gg, Final Fantasy 1's settings are controlled entirely by the original randomzier. You
-can find an exhaustive list of documented settings on the FFR
+Unlike most games on Archipelago.gg, Final Fantasy 1's options are controlled entirely by the original randomzier. You
+can find an exhaustive list of documented options on the FFR
website: [FF1R Website](https://finalfantasyrandomizer.com/)
## What does randomization do to this game?
@@ -24,3 +24,9 @@ All items can appear in other players worlds, including consumables, shards, wea
All local and remote items appear the same. Final Fantasy will say that you received an item, then BOTH the client log and the
emulator will display what was found external to the in-game text box.
+
+## Unique Local Commands
+The following commands are only available when using the FF1Client for the Final Fantasy Randomizer.
+
+- `/nes` Shows the current status of the NES connection.
+- `/toggle_msgs` Toggle displaying messages in EmuHawk
diff --git a/worlds/ffmq/Client.py b/worlds/ffmq/Client.py
new file mode 100644
index 000000000000..93688a6116f6
--- /dev/null
+++ b/worlds/ffmq/Client.py
@@ -0,0 +1,119 @@
+
+from NetUtils import ClientStatus, color
+from worlds.AutoSNIClient import SNIClient
+from .Regions import offset
+import logging
+
+snes_logger = logging.getLogger("SNES")
+
+ROM_NAME = (0x7FC0, 0x7FD4 + 1 - 0x7FC0)
+
+READ_DATA_START = 0xF50EA8
+READ_DATA_END = 0xF50FE7 + 1
+
+GAME_FLAGS = (0xF50EA8, 64)
+COMPLETED_GAME = (0xF50F22, 1)
+BATTLEFIELD_DATA = (0xF50FD4, 20)
+
+RECEIVED_DATA = (0xE01FF0, 3)
+
+ITEM_CODE_START = 0x420000
+
+IN_GAME_FLAG = (4 * 8) + 2
+
+NPC_CHECKS = {
+ 4325676: ((6 * 8) + 4, False), # Old Man Level Forest
+ 4325677: ((3 * 8) + 6, True), # Kaeli Level Forest
+ 4325678: ((25 * 8) + 1, True), # Tristam
+ 4325680: ((26 * 8) + 0, True), # Aquaria Vendor Girl
+ 4325681: ((29 * 8) + 2, True), # Phoebe Wintry Cave
+ 4325682: ((25 * 8) + 6, False), # Mysterious Man (Life Temple)
+ 4325683: ((29 * 8) + 3, True), # Reuben Mine
+ 4325684: ((29 * 8) + 7, True), # Spencer
+ 4325685: ((29 * 8) + 6, False), # Venus Chest
+ 4325686: ((29 * 8) + 1, True), # Fireburg Tristam
+ 4325687: ((26 * 8) + 1, True), # Fireburg Vendor Girl
+ 4325688: ((14 * 8) + 4, True), # MegaGrenade Dude
+ 4325689: ((29 * 8) + 5, False), # Tristam's Chest
+ 4325690: ((29 * 8) + 4, True), # Arion
+ 4325691: ((29 * 8) + 0, True), # Windia Kaeli
+ 4325692: ((26 * 8) + 2, True), # Windia Vendor Girl
+
+}
+
+
+def get_flag(data, flag):
+ byte = int(flag / 8)
+ bit = int(0x80 / (2 ** (flag % 8)))
+ return (data[byte] & bit) > 0
+
+
+class FFMQClient(SNIClient):
+ game = "Final Fantasy Mystic Quest"
+
+ async def validate_rom(self, ctx):
+ from SNIClient import snes_read
+ rom_name = await snes_read(ctx, *ROM_NAME)
+ if rom_name is None:
+ return False
+ if rom_name[:2] != b"MQ":
+ return False
+
+ ctx.rom = rom_name
+ ctx.game = self.game
+ ctx.items_handling = 0b001
+ return True
+
+ async def game_watcher(self, ctx):
+ from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
+
+ check_1 = await snes_read(ctx, 0xF53749, 1)
+ received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1])
+ data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START)
+ check_2 = await snes_read(ctx, 0xF53749, 1)
+ if check_1 != b'\x01' or check_2 != b'\x01':
+ return
+
+ def get_range(data_range):
+ return data[data_range[0] - READ_DATA_START:data_range[0] + data_range[1] - READ_DATA_START]
+ completed_game = get_range(COMPLETED_GAME)
+ battlefield_data = get_range(BATTLEFIELD_DATA)
+ game_flags = get_range(GAME_FLAGS)
+
+ if game_flags is None:
+ return
+ if not get_flag(game_flags, IN_GAME_FLAG):
+ return
+
+ if not ctx.finished_game:
+ if completed_game[0] & 0x80 and game_flags[30] & 0x18:
+ await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
+ ctx.finished_game = True
+
+ old_locations_checked = ctx.locations_checked.copy()
+
+ for container in range(256):
+ if get_flag(game_flags, (0x20 * 8) + container):
+ ctx.locations_checked.add(offset["Chest"] + container)
+
+ for location, data in NPC_CHECKS.items():
+ if get_flag(game_flags, data[0]) is data[1]:
+ ctx.locations_checked.add(location)
+
+ for battlefield in range(20):
+ if battlefield_data[battlefield] == 0:
+ ctx.locations_checked.add(offset["BattlefieldItem"] + battlefield + 1)
+
+ if old_locations_checked != ctx.locations_checked:
+ await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": ctx.locations_checked}])
+
+ if received[0] == 0:
+ received_index = int.from_bytes(received[1:], "big")
+ if received_index < len(ctx.items_received):
+ item = ctx.items_received[received_index]
+ received_index += 1
+ code = (item.item - ITEM_CODE_START) + 1
+ if code > 256:
+ code -= 256
+ snes_buffered_write(ctx, RECEIVED_DATA[0], bytes([code, *received_index.to_bytes(2, "big")]))
+ await snes_flush_writes(ctx)
diff --git a/worlds/ffmq/Items.py b/worlds/ffmq/Items.py
new file mode 100644
index 000000000000..f1c102d34ef8
--- /dev/null
+++ b/worlds/ffmq/Items.py
@@ -0,0 +1,293 @@
+from BaseClasses import ItemClassification, Item
+
+fillers = {"Cure Potion": 61, "Heal Potion": 52, "Refresher": 17, "Seed": 2, "Bomb Refill": 19,
+ "Projectile Refill": 50}
+
+
+class ItemData:
+ def __init__(self, item_id, classification, groups=(), data_name=None):
+ self.groups = groups
+ self.classification = classification
+ self.id = None
+ if item_id is not None:
+ self.id = item_id + 0x420000
+ self.data_name = data_name
+
+
+item_table = {
+ "Elixir": ItemData(0, ItemClassification.progression, ["Key Items"]),
+ "Tree Wither": ItemData(1, ItemClassification.progression, ["Key Items"]),
+ "Wakewater": ItemData(2, ItemClassification.progression, ["Key Items"]),
+ "Venus Key": ItemData(3, ItemClassification.progression, ["Key Items"]),
+ "Multi Key": ItemData(4, ItemClassification.progression, ["Key Items"]),
+ "Mask": ItemData(5, ItemClassification.progression, ["Key Items"]),
+ "Magic Mirror": ItemData(6, ItemClassification.progression, ["Key Items"]),
+ "Thunder Rock": ItemData(7, ItemClassification.progression, ["Key Items"]),
+ "Captain's Cap": ItemData(8, ItemClassification.progression_skip_balancing, ["Key Items"]),
+ "Libra Crest": ItemData(9, ItemClassification.progression, ["Key Items"]),
+ "Gemini Crest": ItemData(10, ItemClassification.progression, ["Key Items"]),
+ "Mobius Crest": ItemData(11, ItemClassification.progression, ["Key Items"]),
+ "Sand Coin": ItemData(12, ItemClassification.progression, ["Key Items", "Coins"]),
+ "River Coin": ItemData(13, ItemClassification.progression, ["Key Items", "Coins"]),
+ "Sun Coin": ItemData(14, ItemClassification.progression, ["Key Items", "Coins"]),
+ "Sky Coin": ItemData(15, ItemClassification.progression_skip_balancing, ["Key Items", "Coins"]),
+ "Sky Fragment": ItemData(15 + 256, ItemClassification.progression_skip_balancing, ["Key Items"]),
+ "Cure Potion": ItemData(16, ItemClassification.filler, ["Consumables"]),
+ "Heal Potion": ItemData(17, ItemClassification.filler, ["Consumables"]),
+ "Seed": ItemData(18, ItemClassification.filler, ["Consumables"]),
+ "Refresher": ItemData(19, ItemClassification.filler, ["Consumables"]),
+ "Exit Book": ItemData(20, ItemClassification.useful, ["Spells"]),
+ "Cure Book": ItemData(21, ItemClassification.useful, ["Spells"]),
+ "Heal Book": ItemData(22, ItemClassification.useful, ["Spells"]),
+ "Life Book": ItemData(23, ItemClassification.useful, ["Spells"]),
+ "Quake Book": ItemData(24, ItemClassification.useful, ["Spells"]),
+ "Blizzard Book": ItemData(25, ItemClassification.useful, ["Spells"]),
+ "Fire Book": ItemData(26, ItemClassification.useful, ["Spells"]),
+ "Aero Book": ItemData(27, ItemClassification.useful, ["Spells"]),
+ "Thunder Seal": ItemData(28, ItemClassification.useful, ["Spells"]),
+ "White Seal": ItemData(29, ItemClassification.useful, ["Spells"]),
+ "Meteor Seal": ItemData(30, ItemClassification.useful, ["Spells"]),
+ "Flare Seal": ItemData(31, ItemClassification.useful, ["Spells"]),
+ "Progressive Sword": ItemData(32 + 256, ItemClassification.progression, ["Weapons", "Swords"]),
+ "Steel Sword": ItemData(32, ItemClassification.progression, ["Weapons", "Swords"]),
+ "Knight Sword": ItemData(33, ItemClassification.progression_skip_balancing, ["Weapons", "Swords"]),
+ "Excalibur": ItemData(34, ItemClassification.progression_skip_balancing, ["Weapons", "Swords"]),
+ "Progressive Axe": ItemData(35 + 256, ItemClassification.progression, ["Weapons", "Axes"]),
+ "Axe": ItemData(35, ItemClassification.progression, ["Weapons", "Axes"]),
+ "Battle Axe": ItemData(36, ItemClassification.progression_skip_balancing, ["Weapons", "Axes"]),
+ "Giant's Axe": ItemData(37, ItemClassification.progression_skip_balancing, ["Weapons", "Axes"]),
+ "Progressive Claw": ItemData(38 + 256, ItemClassification.progression, ["Weapons", "Axes"]),
+ "Cat Claw": ItemData(38, ItemClassification.progression, ["Weapons", "Claws"]),
+ "Charm Claw": ItemData(39, ItemClassification.progression_skip_balancing, ["Weapons", "Claws"]),
+ "Dragon Claw": ItemData(40, ItemClassification.progression, ["Weapons", "Claws"]),
+ "Progressive Bomb": ItemData(41 + 256, ItemClassification.progression, ["Weapons", "Bombs"]),
+ "Bomb": ItemData(41, ItemClassification.progression, ["Weapons", "Bombs"]),
+ "Jumbo Bomb": ItemData(42, ItemClassification.progression_skip_balancing, ["Weapons", "Bombs"]),
+ "Mega Grenade": ItemData(43, ItemClassification.progression, ["Weapons", "Bombs"]),
+ # Ally-only equipment does nothing when received, no reason to put them in the datapackage
+ #"Morning Star": ItemData(44, ItemClassification.progression, ["Weapons"]),
+ #"Bow Of Grace": ItemData(45, ItemClassification.progression, ["Weapons"]),
+ #"Ninja Star": ItemData(46, ItemClassification.progression, ["Weapons"]),
+
+ "Progressive Helm": ItemData(47 + 256, ItemClassification.useful, ["Helms"]),
+ "Steel Helm": ItemData(47, ItemClassification.useful, ["Helms"]),
+ "Moon Helm": ItemData(48, ItemClassification.useful, ["Helms"]),
+ "Apollo Helm": ItemData(49, ItemClassification.useful, ["Helms"]),
+ "Progressive Armor": ItemData(50 + 256, ItemClassification.useful, ["Armors"]),
+ "Steel Armor": ItemData(50, ItemClassification.useful, ["Armors"]),
+ "Noble Armor": ItemData(51, ItemClassification.useful, ["Armors"]),
+ "Gaia's Armor": ItemData(52, ItemClassification.useful, ["Armors"]),
+ #"Replica Armor": ItemData(53, ItemClassification.progression, ["Armors"]),
+ #"Mystic Robes": ItemData(54, ItemClassification.progression, ["Armors"]),
+ #"Flame Armor": ItemData(55, ItemClassification.progression, ["Armors"]),
+ #"Black Robe": ItemData(56, ItemClassification.progression, ["Armors"]),
+ "Progressive Shield": ItemData(57 + 256, ItemClassification.useful, ["Shields"]),
+ "Steel Shield": ItemData(57, ItemClassification.useful, ["Shields"]),
+ "Venus Shield": ItemData(58, ItemClassification.useful, ["Shields"]),
+ "Aegis Shield": ItemData(59, ItemClassification.useful, ["Shields"]),
+ #"Ether Shield": ItemData(60, ItemClassification.progression, ["Shields"]),
+ "Progressive Accessory": ItemData(61 + 256, ItemClassification.useful, ["Accessories"]),
+ "Charm": ItemData(61, ItemClassification.useful, ["Accessories"]),
+ "Magic Ring": ItemData(62, ItemClassification.useful, ["Accessories"]),
+ "Cupid Locket": ItemData(63, ItemClassification.useful, ["Accessories"]),
+
+ # these are understood by FFMQR and I could place these if I want, but it's easier to just let FFMQR
+ # place them. I want an option to make shuffle battlefield rewards NOT color-code the battlefields,
+ # and then I would make the non-item reward battlefields into AP checks and these would be put into those as
+ # the item for AP. But there is no such option right now.
+ # "54 XP": ItemData(96, ItemClassification.filler, data_name="Xp54"),
+ # "99 XP": ItemData(97, ItemClassification.filler, data_name="Xp99"),
+ # "540 XP": ItemData(98, ItemClassification.filler, data_name="Xp540"),
+ # "744 XP": ItemData(99, ItemClassification.filler, data_name="Xp744"),
+ # "816 XP": ItemData(100, ItemClassification.filler, data_name="Xp816"),
+ # "1068 XP": ItemData(101, ItemClassification.filler, data_name="Xp1068"),
+ # "1200 XP": ItemData(102, ItemClassification.filler, data_name="Xp1200"),
+ # "2700 XP": ItemData(103, ItemClassification.filler, data_name="Xp2700"),
+ # "2808 XP": ItemData(104, ItemClassification.filler, data_name="Xp2808"),
+ # "150 Gp": ItemData(105, ItemClassification.filler, data_name="Gp150"),
+ # "300 Gp": ItemData(106, ItemClassification.filler, data_name="Gp300"),
+ # "600 Gp": ItemData(107, ItemClassification.filler, data_name="Gp600"),
+ # "900 Gp": ItemData(108, ItemClassification.filler, data_name="Gp900"),
+ # "1200 Gp": ItemData(109, ItemClassification.filler, data_name="Gp1200"),
+
+
+ "Bomb Refill": ItemData(221, ItemClassification.filler, ["Refills"]),
+ "Projectile Refill": ItemData(222, ItemClassification.filler, ["Refills"]),
+ #"None": ItemData(255, ItemClassification.progression, []),
+
+ "Kaeli 1": ItemData(None, ItemClassification.progression),
+ "Kaeli 2": ItemData(None, ItemClassification.progression),
+ "Tristam": ItemData(None, ItemClassification.progression),
+ "Phoebe 1": ItemData(None, ItemClassification.progression),
+ "Reuben 1": ItemData(None, ItemClassification.progression),
+ "Reuben Dad Saved": ItemData(None, ItemClassification.progression),
+ "Otto": ItemData(None, ItemClassification.progression),
+ "Captain Mac": ItemData(None, ItemClassification.progression),
+ "Ship Steering Wheel": ItemData(None, ItemClassification.progression),
+ "Minotaur": ItemData(None, ItemClassification.progression),
+ "Flamerus Rex": ItemData(None, ItemClassification.progression),
+ "Phanquid": ItemData(None, ItemClassification.progression),
+ "Freezer Crab": ItemData(None, ItemClassification.progression),
+ "Ice Golem": ItemData(None, ItemClassification.progression),
+ "Jinn": ItemData(None, ItemClassification.progression),
+ "Medusa": ItemData(None, ItemClassification.progression),
+ "Dualhead Hydra": ItemData(None, ItemClassification.progression),
+ "Gidrah": ItemData(None, ItemClassification.progression),
+ "Dullahan": ItemData(None, ItemClassification.progression),
+ "Pazuzu": ItemData(None, ItemClassification.progression),
+ "Aquaria Plaza": ItemData(None, ItemClassification.progression),
+ "Summer Aquaria": ItemData(None, ItemClassification.progression),
+ "Reuben Mine": ItemData(None, ItemClassification.progression),
+ "Alive Forest": ItemData(None, ItemClassification.progression),
+ "Rainbow Bridge": ItemData(None, ItemClassification.progression),
+ "Collapse Spencer's Cave": ItemData(None, ItemClassification.progression),
+ "Ship Liberated": ItemData(None, ItemClassification.progression),
+ "Ship Loaned": ItemData(None, ItemClassification.progression),
+ "Ship Dock Access": ItemData(None, ItemClassification.progression),
+ "Stone Golem": ItemData(None, ItemClassification.progression),
+ "Twinhead Wyvern": ItemData(None, ItemClassification.progression),
+ "Zuh": ItemData(None, ItemClassification.progression),
+
+ "Libra Temple Crest Tile": ItemData(None, ItemClassification.progression),
+ "Life Temple Crest Tile": ItemData(None, ItemClassification.progression),
+ "Aquaria Vendor Crest Tile": ItemData(None, ItemClassification.progression),
+ "Fireburg Vendor Crest Tile": ItemData(None, ItemClassification.progression),
+ "Fireburg Grenademan Crest Tile": ItemData(None, ItemClassification.progression),
+ "Sealed Temple Crest Tile": ItemData(None, ItemClassification.progression),
+ "Wintry Temple Crest Tile": ItemData(None, ItemClassification.progression),
+ "Kaidge Temple Crest Tile": ItemData(None, ItemClassification.progression),
+ "Light Temple Crest Tile": ItemData(None, ItemClassification.progression),
+ "Windia Kids Crest Tile": ItemData(None, ItemClassification.progression),
+ "Windia Dock Crest Tile": ItemData(None, ItemClassification.progression),
+ "Ship Dock Crest Tile": ItemData(None, ItemClassification.progression),
+ "Alive Forest Libra Crest Tile": ItemData(None, ItemClassification.progression),
+ "Alive Forest Gemini Crest Tile": ItemData(None, ItemClassification.progression),
+ "Alive Forest Mobius Crest Tile": ItemData(None, ItemClassification.progression),
+ "Wood House Libra Crest Tile": ItemData(None, ItemClassification.progression),
+ "Wood House Gemini Crest Tile": ItemData(None, ItemClassification.progression),
+ "Wood House Mobius Crest Tile": ItemData(None, ItemClassification.progression),
+ "Barrel Pushed": ItemData(None, ItemClassification.progression),
+ "Long Spine Bombed": ItemData(None, ItemClassification.progression),
+ "Short Spine Bombed": ItemData(None, ItemClassification.progression),
+ "Skull 1 Bombed": ItemData(None, ItemClassification.progression),
+ "Skull 2 Bombed": ItemData(None, ItemClassification.progression),
+ "Ice Pyramid 1F Statue": ItemData(None, ItemClassification.progression),
+ "Ice Pyramid 3F Statue": ItemData(None, ItemClassification.progression),
+ "Ice Pyramid 4F Statue": ItemData(None, ItemClassification.progression),
+ "Ice Pyramid 5F Statue": ItemData(None, ItemClassification.progression),
+ "Spencer Cave Libra Block Bombed": ItemData(None, ItemClassification.progression),
+ "Lava Dome Plate": ItemData(None, ItemClassification.progression),
+ "Pazuzu 2F Lock": ItemData(None, ItemClassification.progression),
+ "Pazuzu 4F Lock": ItemData(None, ItemClassification.progression),
+ "Pazuzu 6F Lock": ItemData(None, ItemClassification.progression),
+ "Pazuzu 1F": ItemData(None, ItemClassification.progression),
+ "Pazuzu 2F": ItemData(None, ItemClassification.progression),
+ "Pazuzu 3F": ItemData(None, ItemClassification.progression),
+ "Pazuzu 4F": ItemData(None, ItemClassification.progression),
+ "Pazuzu 5F": ItemData(None, ItemClassification.progression),
+ "Pazuzu 6F": ItemData(None, ItemClassification.progression),
+ "Dark King": ItemData(None, ItemClassification.progression),
+ "Tristam Bone Item Given": ItemData(None, ItemClassification.progression),
+ #"Barred": ItemData(None, ItemClassification.progression),
+
+}
+
+prog_map = {
+ "Swords": "Progressive Sword",
+ "Axes": "Progressive Axe",
+ "Claws": "Progressive Claw",
+ "Bombs": "Progressive Bomb",
+ "Shields": "Progressive Shield",
+ "Armors": "Progressive Armor",
+ "Helms": "Progressive Helm",
+ "Accessories": "Progressive Accessory",
+}
+
+
+def yaml_item(text):
+ if text == "CaptainCap":
+ return "Captain's Cap"
+ elif text == "WakeWater":
+ return "Wakewater"
+ return "".join(
+ [(" " + c if (c.isupper() or c.isnumeric()) and not (text[i - 1].isnumeric() and c == "F") else c) for
+ i, c in enumerate(text)]).strip()
+
+
+item_groups = {}
+for item, data in item_table.items():
+ for group in data.groups:
+ item_groups[group] = item_groups.get(group, []) + [item]
+
+
+def create_items(self) -> None:
+ items = []
+ starting_weapon = self.options.starting_weapon.current_key.title().replace("_", " ")
+ self.multiworld.push_precollected(self.create_item(starting_weapon))
+ self.multiworld.push_precollected(self.create_item("Steel Armor"))
+ if self.options.sky_coin_mode == "start_with":
+ self.multiworld.push_precollected(self.create_item("Sky Coin"))
+
+ precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]}
+
+ def add_item(item_name):
+ if item_name in ["Steel Armor", "Sky Fragment"] or "Progressive" in item_name:
+ return
+ if item_name.lower().replace(" ", "_") == self.options.starting_weapon.current_key:
+ return
+ if self.options.progressive_gear:
+ for item_group in prog_map:
+ if item_name in self.item_name_groups[item_group]:
+ item_name = prog_map[item_group]
+ break
+ if item_name == "Sky Coin":
+ if self.options.sky_coin_mode == "shattered_sky_coin":
+ for _ in range(40):
+ items.append(self.create_item("Sky Fragment"))
+ return
+ elif self.options.sky_coin_mode == "save_the_crystals":
+ items.append(self.create_filler())
+ return
+ if item_name in precollected_item_names:
+ items.append(self.create_filler())
+ return
+ i = self.create_item(item_name)
+ if self.options.logic != "friendly" and item_name in ("Magic Mirror", "Mask"):
+ i.classification = ItemClassification.useful
+ if (self.options.logic == "expert" and self.options.map_shuffle == "none" and
+ item_name == "Exit Book"):
+ i.classification = ItemClassification.progression
+ items.append(i)
+
+ for item_group in ("Key Items", "Spells", "Armors", "Helms", "Shields", "Accessories", "Weapons"):
+ for item in self.item_name_groups[item_group]:
+ add_item(item)
+
+ if self.options.brown_boxes == "include":
+ filler_items = []
+ for item, count in fillers.items():
+ filler_items += [self.create_item(item) for _ in range(count)]
+ if self.options.sky_coin_mode == "shattered_sky_coin":
+ self.multiworld.random.shuffle(filler_items)
+ filler_items = filler_items[39:]
+ items += filler_items
+
+ self.multiworld.itempool += items
+
+ if len(self.multiworld.player_ids) > 1:
+ early_choices = ["Sand Coin", "River Coin"]
+ early_item = self.multiworld.random.choice(early_choices)
+ self.multiworld.early_items[self.player][early_item] = 1
+
+
+class FFMQItem(Item):
+ game = "Final Fantasy Mystic Quest"
+ type = None
+
+ def __init__(self, name, player: int = None):
+ item_data = item_table[name]
+ super(FFMQItem, self).__init__(
+ name,
+ item_data.classification,
+ item_data.id, player
+ )
\ No newline at end of file
diff --git a/worlds/ffmq/LICENSE b/worlds/ffmq/LICENSE
new file mode 100644
index 000000000000..46ad1c007466
--- /dev/null
+++ b/worlds/ffmq/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2023 Alex "Alchav" Avery
+Copyright (c) 2023 wildham
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/worlds/ffmq/Options.py b/worlds/ffmq/Options.py
new file mode 100644
index 000000000000..41c397315f87
--- /dev/null
+++ b/worlds/ffmq/Options.py
@@ -0,0 +1,357 @@
+from Options import Choice, FreeText, Toggle, Range, PerGameCommonOptions
+from dataclasses import dataclass
+
+
+class Logic(Choice):
+ """Placement logic sets the rules that will be applied when placing items. Friendly: Required Items to clear a
+ dungeon will never be placed in that dungeon to avoid the need to revisit it. Also, the Magic Mirror and the Mask
+ will always be available before Ice Pyramid and Volcano, respectively. Note: If Dungeons are shuffled, Friendly
+ logic will only ensure the availability of the Mirror and the Mask. Standard: Items are randomly placed and logic
+ merely verifies that they're all accessible. As for Region access, only the Coins are considered. Expert: Same as
+ Standard, but Items Placement logic also includes other routes than Coins: the Crests Teleporters, the
+ Fireburg-Aquaria Lava bridge and the Sealed Temple Exit trick."""
+ option_friendly = 0
+ option_standard = 1
+ option_expert = 2
+ default = 1
+ display_name = "Logic"
+
+
+class BrownBoxes(Choice):
+ """Include the 201 brown box locations from the original game. Brown Boxes are all the boxes that contained a
+ consumable in the original game. If shuffle is chosen, the consumables contained will be shuffled but the brown
+ boxes will not be Archipelago location checks."""
+ option_exclude = 0
+ option_include = 1
+ option_shuffle = 2
+ default = 1
+ display_name = "Brown Boxes"
+
+
+class SkyCoinMode(Choice):
+ """Configure how the Sky Coin is acquired. With standard, the Sky Coin will be placed randomly. With Start With, the
+ Sky Coin will be in your inventory at the start of the game. With Save The Crystals, the Sky Coin will be acquired
+ once you save all 4 crystals. With Shattered Sky Coin, the Sky Coin is split in 40 fragments; you can enter Doom
+ Castle once the required amount is found. Shattered Sky Coin will force brown box locations to be included."""
+ option_standard = 0
+ option_start_with = 1
+ option_save_the_crystals = 2
+ option_shattered_sky_coin = 3
+ default = 0
+ display_name = "Sky Coin Mode"
+
+
+class ShatteredSkyCoinQuantity(Choice):
+ """Configure the number of the 40 Sky Coin Fragments required to enter the Doom Castle. Only has an effect if
+ Sky Coin Mode is set to shattered. Low: 16. Mid: 24. High: 32. Random Narrow: random between 16 and 32.
+ Random Wide: random between 10 and 38."""
+ option_low_16 = 0
+ option_mid_24 = 1
+ option_high_32 = 2
+ option_random_narrow = 3
+ option_random_wide = 4
+ default = 1
+ display_name = "Shattered Sky Coin"
+
+
+class StartingWeapon(Choice):
+ """Choose your starting weapon."""
+ display_name = "Starting Weapon"
+ option_steel_sword = 0
+ option_axe = 1
+ option_cat_claw = 2
+ option_bomb = 3
+ default = "random"
+
+
+class ProgressiveGear(Toggle):
+ """Pieces of gear are always acquired from weakest to strongest in a set."""
+ display_name = "Progressive Gear"
+
+
+class EnemiesDensity(Choice):
+ """Set how many of the original enemies are on each map."""
+ display_name = "Enemies Density"
+ option_all = 0
+ option_three_quarter = 1
+ option_half = 2
+ option_quarter = 3
+ option_none = 4
+
+
+class EnemyScaling(Choice):
+ """Superclass for enemy scaling options."""
+ option_quarter = 0
+ option_half = 1
+ option_three_quarter = 2
+ option_normal = 3
+ option_one_and_quarter = 4
+ option_one_and_half = 5
+ option_double = 6
+ option_double_and_half = 7
+ option_triple = 8
+
+
+class EnemiesScalingLower(EnemyScaling):
+ """Randomly adjust enemies stats by the selected range percentage. Include mini-bosses' weaker clones."""
+ display_name = "Enemies Scaling Lower"
+ default = 0
+
+
+class EnemiesScalingUpper(EnemyScaling):
+ """Randomly adjust enemies stats by the selected range percentage. Include mini-bosses' weaker clones."""
+ display_name = "Enemies Scaling Upper"
+ default = 4
+
+
+class BossesScalingLower(EnemyScaling):
+ """Randomly adjust bosses stats by the selected range percentage. Include Mini-Bosses, Bosses, Bosses' refights and
+ the Dark King."""
+ display_name = "Bosses Scaling Lower"
+ default = 0
+
+
+class BossesScalingUpper(EnemyScaling):
+ """Randomly adjust bosses stats by the selected range percentage. Include Mini-Bosses, Bosses, Bosses' refights and
+ the Dark King."""
+ display_name = "Bosses Scaling Upper"
+ default = 4
+
+
+class EnemizerAttacks(Choice):
+ """Shuffles enemy attacks. Standard: No shuffle. Safe: Randomize every attack but leave out self-destruct and Dark
+ King attacks. Chaos: Randomize and include self-destruct and Dark King attacks. Self Destruct: Every enemy
+ self-destructs. Simple Shuffle: Instead of randomizing, shuffle one monster's attacks to another. Dark King is left
+ vanilla."""
+ display_name = "Enemizer Attacks"
+ option_normal = 0
+ option_safe = 1
+ option_chaos = 2
+ option_self_destruct = 3
+ option_simple_shuffle = 4
+ default = 0
+
+
+class EnemizerGroups(Choice):
+ """Set which enemy groups will be affected by Enemizer."""
+ display_name = "Enemizer Groups"
+ option_mobs_only = 0
+ option_mobs_and_bosses = 1
+ option_mobs_bosses_and_dark_king = 2
+ default = 1
+
+
+class ShuffleResWeakType(Toggle):
+ """Resistance and Weakness types are shuffled for all enemies."""
+ display_name = "Shuffle Resistance/Weakness Types"
+ default = 0
+
+
+class ShuffleEnemiesPositions(Toggle):
+ """Instead of their original position in a given map, enemies are randomly placed."""
+ display_name = "Shuffle Enemies' Positions"
+ default = 1
+
+
+class ProgressiveFormations(Choice):
+ """Enemies' formations are selected by regions, with the weakest formations always selected in Foresta and the
+ strongest in Windia. Disabled: Standard formations are used. Regions Strict: Formations will come exclusively
+ from the current region, whatever the map is. Regions Keep Type: Formations will keep the original formation type
+ and match with the nearest power level."""
+ display_name = "Progressive Formations"
+ option_disabled = 0
+ option_regions_strict = 1
+ option_regions_keep_type = 2
+
+
+class DoomCastle(Choice):
+ """Configure how you reach the Dark King. With Standard, you need to defeat all four bosses and their floors to
+ reach the Dark King. With Boss Rush, only the bosses are blocking your way in the corridor to the Dark King's room.
+ With Dark King Only, the way to the Dark King is free of any obstacle."""
+ display_name = "Doom Castle"
+ option_standard = 0
+ option_boss_rush = 1
+ option_dark_king_only = 2
+
+
+class DoomCastleShortcut(Toggle):
+ """Create a shortcut granting access from the start to Doom Castle at Focus Tower's entrance.
+ Also modify the Desert floor, so it can be navigated without the Mega Grenades and the Dragon Claw."""
+ display_name = "Doom Castle Shortcut"
+
+
+class TweakFrustratingDungeons(Toggle):
+ """Make some small changes to a few of the most annoying dungeons. Ice Pyramid: Add 3 shortcuts on the 1st floor.
+ Giant Tree: Add shortcuts on the 1st and 4th floors and curtail mushrooms population.
+ Pazuzu's Tower: Staircases are devoid of enemies (regardless of Enemies Density settings)."""
+ display_name = "Tweak Frustrating Dungeons"
+
+
+class MapShuffle(Choice):
+ """None: No shuffle. Overworld: Only shuffle the Overworld locations. Dungeons: Only shuffle the dungeons' floors
+ amongst themselves. Temples and Towns aren't included. Overworld And Dungeons: Shuffle the Overworld and dungeons
+ at the same time. Everything: Shuffle the Overworld, dungeons, temples and towns all amongst each others.
+ When dungeons are shuffled, defeating Pazuzu won't teleport you to the 7th floor, you have to get there normally to
+ save the Crystal and get Pazuzu's Chest."""
+ display_name = "Map Shuffle"
+ option_none = 0
+ option_overworld = 1
+ option_dungeons = 2
+ option_overworld_and_dungeons = 3
+ option_everything = 4
+ default = 0
+
+
+class CrestShuffle(Toggle):
+ """Shuffle the Crest tiles amongst themselves."""
+ display_name = "Crest Shuffle"
+
+
+class MapShuffleSeed(FreeText):
+ """If this is a number, it will be used as a set seed number for Map, Crest, Battlefield Reward, and Companion shuffles.
+ If this is "random" the seed will be chosen randomly. If it is any other text, it will be used as a seed group name.
+ All players using the same seed group name will get the same shuffle results, as long as their Map Shuffle,
+ Crest Shuffle, Shuffle Battlefield Rewards, Companion Shuffle, and Kaeli's Mom settings are the same."""
+ display_name = "Map Shuffle Seed"
+ default = "random"
+
+
+class LevelingCurve(Choice):
+ """Adjust the level gain rate."""
+ display_name = "Leveling Curve"
+ option_half = 0
+ option_normal = 1
+ option_one_and_half = 2
+ option_double = 3
+ option_double_and_half = 4
+ option_triple = 5
+ option_quadruple = 6
+ default = 4
+
+
+class ShuffleBattlefieldRewards(Toggle):
+ """Shuffle the type of reward (Item, XP, GP) given by battlefields and color code them by reward type.
+ Blue: Give an item. Grey: Give XP. Green: Give GP."""
+ display_name = "Shuffle Battlefield Rewards"
+
+
+class BattlefieldsBattlesQuantities(Choice):
+ """Adjust the number of battles that need to be fought to get a battlefield's reward."""
+ display_name = "Battlefields Battles Quantity"
+ option_ten = 0
+ option_seven = 1
+ option_five = 2
+ option_three = 3
+ option_one = 4
+ option_random_one_through_five = 5
+ option_random_one_through_ten = 6
+
+
+class CompanionLevelingType(Choice):
+ """Set how companions gain levels.
+ Quests: Complete each companion's individual quest for them to promote to their second version.
+ Quests Extended: Each companion has four exclusive quests, leveling each time a quest is completed.
+ Save the Crystals (All): Each time a Crystal is saved, all companions gain levels.
+ Save the Crystals (Individual): Each companion will level to their second version when a specific Crystal is saved.
+ Benjamin Level: Companions' level tracks Benjamin's."""
+ option_quests = 0
+ option_quests_extended = 1
+ option_save_crystals_individual = 2
+ option_save_crystals_all = 3
+ option_benjamin_level = 4
+ option_benjamin_level_plus_5 = 5
+ option_benjamin_level_plus_10 = 6
+ default = 0
+ display_name = "Companion Leveling Type"
+
+
+class CompanionSpellbookType(Choice):
+ """Update companions' spellbook.
+ Standard: Original game spellbooks.
+ Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard.
+ Random Balanced: Randomize the spellbooks with an appropriate mix of spells.
+ Random Chaos: Randomize the spellbooks in total free-for-all."""
+ option_standard = 0
+ option_extended = 1
+ option_random_balanced = 2
+ option_random_chaos = 3
+ default = 0
+ display_name = "Companion Spellbook Type"
+
+
+class StartingCompanion(Choice):
+ """Set a companion to start with.
+ Random Companion: Randomly select one companion.
+ Random Plus None: Randomly select a companion, with the possibility of none selected."""
+ display_name = "Starting Companion"
+ default = 0
+ option_none = 0
+ option_kaeli = 1
+ option_tristam = 2
+ option_phoebe = 3
+ option_reuben = 4
+ option_random_companion = 5
+ option_random_plus_none = 6
+
+
+class AvailableCompanions(Range):
+ """Select randomly which companions will join your party. Unavailable companions can still be reached to get their items and complete their quests if needed.
+ Note: If a Starting Companion is selected, it will always be available, regardless of this setting."""
+ display_name = "Available Companions"
+ default = 4
+ range_start = 0
+ range_end = 4
+
+
+class CompanionsLocations(Choice):
+ """Set the primary location of companions. Their secondary location is always the same.
+ Standard: Companions will be at the same locations as in the original game.
+ Shuffled: Companions' locations are shuffled amongst themselves.
+ Shuffled Extended: Add all the Temples, as well as Phoebe's House and the Rope Bridge as possible locations."""
+ display_name = "Companions' Locations"
+ default = 0
+ option_standard = 0
+ option_shuffled = 1
+ option_shuffled_extended = 2
+
+
+class KaelisMomFightsMinotaur(Toggle):
+ """Transfer Kaeli's requirements (Tree Wither, Elixir) and the two items she's giving to her mom.
+ Kaeli will be available to join the party right away without the Tree Wither."""
+ display_name = "Kaeli's Mom Fights Minotaur"
+ default = 0
+
+
+@dataclass
+class FFMQOptions(PerGameCommonOptions):
+ logic: Logic
+ brown_boxes: BrownBoxes
+ sky_coin_mode: SkyCoinMode
+ shattered_sky_coin_quantity: ShatteredSkyCoinQuantity
+ starting_weapon: StartingWeapon
+ progressive_gear: ProgressiveGear
+ leveling_curve: LevelingCurve
+ starting_companion: StartingCompanion
+ available_companions: AvailableCompanions
+ companions_locations: CompanionsLocations
+ kaelis_mom_fight_minotaur: KaelisMomFightsMinotaur
+ companion_leveling_type: CompanionLevelingType
+ companion_spellbook_type: CompanionSpellbookType
+ enemies_density: EnemiesDensity
+ enemies_scaling_lower: EnemiesScalingLower
+ enemies_scaling_upper: EnemiesScalingUpper
+ bosses_scaling_lower: BossesScalingLower
+ bosses_scaling_upper: BossesScalingUpper
+ enemizer_attacks: EnemizerAttacks
+ enemizer_groups: EnemizerGroups
+ shuffle_res_weak_types: ShuffleResWeakType
+ shuffle_enemies_position: ShuffleEnemiesPositions
+ progressive_formations: ProgressiveFormations
+ doom_castle_mode: DoomCastle
+ doom_castle_shortcut: DoomCastleShortcut
+ tweak_frustrating_dungeons: TweakFrustratingDungeons
+ map_shuffle: MapShuffle
+ crest_shuffle: CrestShuffle
+ shuffle_battlefield_rewards: ShuffleBattlefieldRewards
+ map_shuffle_seed: MapShuffleSeed
+ battlefields_battles_quantities: BattlefieldsBattlesQuantities
diff --git a/worlds/ffmq/Output.py b/worlds/ffmq/Output.py
new file mode 100644
index 000000000000..1e436a90c5fd
--- /dev/null
+++ b/worlds/ffmq/Output.py
@@ -0,0 +1,125 @@
+import yaml
+import os
+import zipfile
+import Utils
+from copy import deepcopy
+from .Regions import object_id_table
+from worlds.Files import APPatch
+import pkgutil
+
+settings_template = Utils.parse_yaml(pkgutil.get_data(__name__, "data/settings.yaml"))
+
+
+def generate_output(self, output_directory):
+ def output_item_name(item):
+ if item.player == self.player:
+ if item.code > 0x420000 + 256:
+ item_name = self.item_id_to_name[item.code - 256]
+ else:
+ item_name = item.name
+ item_name = "".join(item_name.split("'"))
+ item_name = "".join(item_name.split(" "))
+ else:
+ if item.advancement or item.useful or (item.trap and
+ self.random.randint(0, 1)):
+ item_name = "APItem"
+ else:
+ item_name = "APItemFiller"
+ return item_name
+
+ item_placement = []
+ for location in self.multiworld.get_locations(self.player):
+ if location.type != "Trigger":
+ item_placement.append({"object_id": object_id_table[location.name], "type": location.type, "content":
+ output_item_name(location.item), "player": self.multiworld.player_name[location.item.player],
+ "item_name": location.item.name})
+
+ def cc(option):
+ return option.current_key.title().replace("_", "").replace("OverworldAndDungeons",
+ "OverworldDungeons").replace("MobsAndBosses", "MobsBosses").replace("MobsBossesAndDarkKing",
+ "MobsBossesDK").replace("BenjaminLevelPlus", "BenPlus").replace("BenjaminLevel", "BenPlus0").replace(
+ "RandomCompanion", "Random")
+
+ def tf(option):
+ return True if option else False
+
+ options = deepcopy(settings_template)
+ options["name"] = self.multiworld.player_name[self.player]
+ option_writes = {
+ "enemies_density": cc(self.options.enemies_density),
+ "chests_shuffle": "Include",
+ "shuffle_boxes_content": self.options.brown_boxes == "shuffle",
+ "npcs_shuffle": "Include",
+ "battlefields_shuffle": "Include",
+ "logic_options": cc(self.options.logic),
+ "shuffle_enemies_position": tf(self.options.shuffle_enemies_position),
+ "enemies_scaling_lower": cc(self.options.enemies_scaling_lower),
+ "enemies_scaling_upper": cc(self.options.enemies_scaling_upper),
+ "bosses_scaling_lower": cc(self.options.bosses_scaling_lower),
+ "bosses_scaling_upper": cc(self.options.bosses_scaling_upper),
+ "enemizer_attacks": cc(self.options.enemizer_attacks),
+ "leveling_curve": cc(self.options.leveling_curve),
+ "battles_quantity": cc(self.options.battlefields_battles_quantities) if
+ self.options.battlefields_battles_quantities.value < 5 else
+ "RandomLow" if
+ self.options.battlefields_battles_quantities.value == 5 else
+ "RandomHigh",
+ "shuffle_battlefield_rewards": tf(self.options.shuffle_battlefield_rewards),
+ "random_starting_weapon": True,
+ "progressive_gear": tf(self.options.progressive_gear),
+ "tweaked_dungeons": tf(self.options.tweak_frustrating_dungeons),
+ "doom_castle_mode": cc(self.options.doom_castle_mode),
+ "doom_castle_shortcut": tf(self.options.doom_castle_shortcut),
+ "sky_coin_mode": cc(self.options.sky_coin_mode),
+ "sky_coin_fragments_qty": cc(self.options.shattered_sky_coin_quantity),
+ "enable_spoilers": False,
+ "progressive_formations": cc(self.options.progressive_formations),
+ "map_shuffling": cc(self.options.map_shuffle),
+ "crest_shuffle": tf(self.options.crest_shuffle),
+ "enemizer_groups": cc(self.options.enemizer_groups),
+ "shuffle_res_weak_type": tf(self.options.shuffle_res_weak_types),
+ "companion_leveling_type": cc(self.options.companion_leveling_type),
+ "companion_spellbook_type": cc(self.options.companion_spellbook_type),
+ "starting_companion": cc(self.options.starting_companion),
+ "available_companions": ["Zero", "One", "Two",
+ "Three", "Four"][self.options.available_companions.value],
+ "companions_locations": cc(self.options.companions_locations),
+ "kaelis_mom_fight_minotaur": tf(self.options.kaelis_mom_fight_minotaur),
+ }
+
+ for option, data in option_writes.items():
+ options["Final Fantasy Mystic Quest"][option][data] = 1
+
+ rom_name = f'MQ{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21]
+ self.rom_name = bytearray(rom_name,
+ 'utf8')
+ self.rom_name_available_event.set()
+
+ setup = {"version": "1.5", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed":
+ hex(self.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper()}
+
+ starting_items = [output_item_name(item) for item in self.multiworld.precollected_items[self.player]]
+ if self.options.sky_coin_mode == "shattered_sky_coin":
+ starting_items.append("SkyCoin")
+
+ file_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.apmq")
+
+ APMQ = APMQFile(file_path, player=self.player, player_name=self.multiworld.player_name[self.player])
+ with zipfile.ZipFile(file_path, mode="w", compression=zipfile.ZIP_DEFLATED,
+ compresslevel=9) as zf:
+ zf.writestr("itemplacement.yaml", yaml.dump(item_placement))
+ zf.writestr("flagset.yaml", yaml.dump(options))
+ zf.writestr("startingitems.yaml", yaml.dump(starting_items))
+ zf.writestr("setup.yaml", yaml.dump(setup))
+ zf.writestr("rooms.yaml", yaml.dump(self.rooms))
+
+ APMQ.write_contents(zf)
+
+
+class APMQFile(APPatch):
+ game = "Final Fantasy Mystic Quest"
+
+ def get_manifest(self):
+ manifest = super().get_manifest()
+ manifest["patch_file_ending"] = ".apmq"
+ return manifest
diff --git a/worlds/ffmq/Regions.py b/worlds/ffmq/Regions.py
new file mode 100644
index 000000000000..c1d3d619ffaa
--- /dev/null
+++ b/worlds/ffmq/Regions.py
@@ -0,0 +1,243 @@
+from BaseClasses import Region, MultiWorld, Entrance, Location, LocationProgressType, ItemClassification
+from worlds.generic.Rules import add_rule
+from .data.rooms import rooms, entrances
+from .Items import item_groups, yaml_item
+
+entrance_names = {entrance["id"]: entrance["name"] for entrance in entrances}
+
+object_id_table = {}
+object_type_table = {}
+offset = {"Chest": 0x420000, "Box": 0x420000, "NPC": 0x420000 + 300, "BattlefieldItem": 0x420000 + 350}
+for room in rooms:
+ for object in room["game_objects"]:
+ if "Hero Chest" in object["name"] or object["type"] == "Trigger":
+ continue
+ if object["type"] in ("BattlefieldItem", "BattlefieldXp", "BattlefieldGp"):
+ object_type_table[object["name"]] = "BattlefieldItem"
+ elif object["type"] in ("Chest", "NPC", "Box"):
+ object_type_table[object["name"]] = object["type"]
+ object_id_table[object["name"]] = object["object_id"]
+
+location_table = {loc_name: offset[object_type_table[loc_name]] + obj_id for loc_name, obj_id in
+ object_id_table.items()}
+
+weapons = ("Claw", "Bomb", "Sword", "Axe")
+crest_warps = [51, 52, 53, 76, 96, 108, 158, 171, 175, 191, 275, 276, 277, 308, 334, 336, 396, 397]
+
+
+def process_rules(spot, access):
+ for weapon in weapons:
+ if weapon in access:
+ add_rule(spot, lambda state, w=weapon: state.has_any(item_groups[w + "s"], spot.player))
+ access = [yaml_item(rule) for rule in access if rule not in weapons]
+ add_rule(spot, lambda state: state.has_all(access, spot.player))
+
+
+def create_region(world: MultiWorld, player: int, name: str, room_id=None, locations=None, links=None):
+ if links is None:
+ links = []
+ ret = Region(name, player, world)
+ if locations:
+ for location in locations:
+ location.parent_region = ret
+ ret.locations.append(location)
+ ret.links = links
+ ret.id = room_id
+ return ret
+
+
+def get_entrance_to(entrance_to):
+ for room in rooms:
+ if room["id"] == entrance_to["target_room"]:
+ for link in room["links"]:
+ if link["target_room"] == entrance_to["room"]:
+ return link
+ else:
+ raise Exception(f"Did not find entrance {entrance_to}")
+
+
+def create_regions(self):
+
+ menu_region = create_region(self.multiworld, self.player, "Menu")
+ self.multiworld.regions.append(menu_region)
+
+ for room in self.rooms:
+ self.multiworld.regions.append(create_region(self.multiworld, self.player, room["name"], room["id"],
+ [FFMQLocation(self.player, object["name"], location_table[object["name"]] if object["name"] in
+ location_table else None, object["type"], object["access"],
+ self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for object in
+ room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in ("BattlefieldGp",
+ "BattlefieldXp") and (object["type"] != "Box" or self.options.brown_boxes == "include") and
+ not (object["name"] == "Kaeli Companion" and not object["on_trigger"])], room["links"]))
+
+ dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player)
+ dark_king = FFMQLocation(self.player, "Dark King", None, "Trigger", [])
+ dark_king.parent_region = dark_king_room
+ dark_king.place_locked_item(self.create_item("Dark King"))
+ dark_king_room.locations.append(dark_king)
+
+ connection = Entrance(self.player, f"Enter Overworld", menu_region)
+ connection.connect(self.multiworld.get_region("Overworld", self.player))
+ menu_region.exits.append(connection)
+
+ for region in self.multiworld.get_regions(self.player):
+ for link in region.links:
+ for connect_room in self.multiworld.get_regions(self.player):
+ if connect_room.id == link["target_room"]:
+ connection = Entrance(self.player, entrance_names[link["entrance"]] if "entrance" in link and
+ link["entrance"] != -1 else f"{region.name} to {connect_room.name}", region)
+ if "entrance" in link and link["entrance"] != -1:
+ spoiler = False
+ if link["entrance"] in crest_warps:
+ if self.options.crest_shuffle:
+ spoiler = True
+ elif self.options.map_shuffle == "everything":
+ spoiler = True
+ elif "Subregion" in region.name and self.options.map_shuffle not in ("dungeons", "none"):
+ spoiler = True
+ elif "Subregion" not in region.name and self.options.map_shuffle not in ("none", "overworld"):
+ spoiler = True
+
+ if spoiler:
+ self.multiworld.spoiler.set_entrance(entrance_names[link["entrance"]], connect_room.name,
+ 'both', self.player)
+ if link["access"]:
+ process_rules(connection, link["access"])
+ region.exits.append(connection)
+ connection.connect(connect_room)
+ break
+
+
+non_dead_end_crest_rooms = [
+ 'Libra Temple', 'Aquaria Gemini Room', "GrenadeMan's Mobius Room", 'Fireburg Gemini Room',
+ 'Sealed Temple', 'Alive Forest', 'Kaidge Temple Upper Ledge',
+ 'Windia Kid House Basement', 'Windia Old People House Basement'
+]
+
+non_dead_end_crest_warps = [
+ 'Libra Temple - Libra Tile Script', 'Aquaria Gemini Room - Gemini Script',
+ 'GrenadeMan Mobius Room - Mobius Teleporter Script', 'Fireburg Gemini Room - Gemini Teleporter Script',
+ 'Sealed Temple - Gemini Tile Script', 'Alive Forest - Libra Teleporter Script',
+ 'Alive Forest - Gemini Teleporter Script', 'Alive Forest - Mobius Teleporter Script',
+ 'Kaidge Temple - Mobius Teleporter Script', 'Windia Kid House Basement - Mobius Teleporter',
+ 'Windia Old People House Basement - Mobius Teleporter Script',
+]
+
+
+vendor_locations = ["Aquaria - Vendor", "Fireburg - Vendor", "Windia - Vendor"]
+
+
+def set_rules(self) -> None:
+ self.multiworld.completion_condition[self.player] = lambda state: state.has("Dark King", self.player)
+
+ def hard_boss_logic(state):
+ return state.has_all(["River Coin", "Sand Coin"], self.player)
+
+ add_rule(self.multiworld.get_location("Pazuzu 1F", self.player), hard_boss_logic)
+ add_rule(self.multiworld.get_location("Gidrah", self.player), hard_boss_logic)
+ add_rule(self.multiworld.get_location("Dullahan", self.player), hard_boss_logic)
+
+ if self.options.map_shuffle:
+ for boss in ("Freezer Crab", "Ice Golem", "Jinn", "Medusa", "Dualhead Hydra"):
+ loc = self.multiworld.get_location(boss, self.player)
+ checked_regions = {loc.parent_region}
+
+ def check_foresta(region):
+ if region.name == "Subregion Foresta":
+ add_rule(loc, hard_boss_logic)
+ return True
+ elif "Subregion" in region.name:
+ return True
+ for entrance in region.entrances:
+ if entrance.parent_region not in checked_regions:
+ checked_regions.add(entrance.parent_region)
+ if check_foresta(entrance.parent_region):
+ return True
+ check_foresta(loc.parent_region)
+
+ if self.options.logic == "friendly":
+ process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player),
+ ["MagicMirror"])
+ process_rules(self.multiworld.get_entrance("Overworld - Volcano", self.player),
+ ["Mask"])
+ if self.options.map_shuffle in ("none", "overworld"):
+ process_rules(self.multiworld.get_entrance("Overworld - Bone Dungeon", self.player),
+ ["Bomb"])
+ process_rules(self.multiworld.get_entrance("Overworld - Wintry Cave", self.player),
+ ["Bomb", "Claw"])
+ process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player),
+ ["Bomb", "Claw"])
+ process_rules(self.multiworld.get_entrance("Overworld - Mine", self.player),
+ ["MegaGrenade", "Claw", "Reuben1"])
+ process_rules(self.multiworld.get_entrance("Overworld - Lava Dome", self.player),
+ ["MegaGrenade"])
+ process_rules(self.multiworld.get_entrance("Overworld - Giant Tree", self.player),
+ ["DragonClaw", "Axe"])
+ process_rules(self.multiworld.get_entrance("Overworld - Mount Gale", self.player),
+ ["DragonClaw"])
+ process_rules(self.multiworld.get_entrance("Overworld - Pazuzu Tower", self.player),
+ ["DragonClaw", "Bomb"])
+ process_rules(self.multiworld.get_entrance("Overworld - Mac Ship", self.player),
+ ["DragonClaw", "CaptainCap"])
+ process_rules(self.multiworld.get_entrance("Overworld - Mac Ship Doom", self.player),
+ ["DragonClaw", "CaptainCap"])
+
+ if self.options.logic == "expert":
+ if self.options.map_shuffle == "none" and not self.options.crest_shuffle:
+ inner_room = self.multiworld.get_region("Wintry Temple Inner Room", self.player)
+ connection = Entrance(self.player, "Sealed Temple Exit Trick", inner_room)
+ connection.connect(self.multiworld.get_region("Wintry Temple Outer Room", self.player))
+ connection.access_rule = lambda state: state.has("Exit Book", self.player)
+ inner_room.exits.append(connection)
+ else:
+ for crest_warp in non_dead_end_crest_warps:
+ entrance = self.multiworld.get_entrance(crest_warp, self.player)
+ if entrance.connected_region.name in non_dead_end_crest_rooms:
+ entrance.access_rule = lambda state: False
+
+ if self.options.sky_coin_mode == "shattered_sky_coin":
+ logic_coins = [16, 24, 32, 32, 38][self.options.shattered_sky_coin_quantity.value]
+ self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
+ lambda state: state.has("Sky Fragment", self.player, logic_coins)
+ elif self.options.sky_coin_mode == "save_the_crystals":
+ self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
+ lambda state: state.has_all(["Flamerus Rex", "Dualhead Hydra", "Ice Golem", "Pazuzu"], self.player)
+ elif self.options.sky_coin_mode in ("standard", "start_with"):
+ self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
+ lambda state: state.has("Sky Coin", self.player)
+
+
+def stage_set_rules(multiworld):
+ # If there's no enemies, there's no repeatable income sources
+ no_enemies_players = [player for player in multiworld.get_game_players("Final Fantasy Mystic Quest")
+ if multiworld.worlds[player].options.enemies_density == "none"]
+ if (len([item for item in multiworld.itempool if item.classification in (ItemClassification.filler,
+ ItemClassification.trap)]) > len([player for player in no_enemies_players if
+ multiworld.worlds[player].options.accessibility == "minimal"]) * 3):
+ for player in no_enemies_players:
+ for location in vendor_locations:
+ if multiworld.worlds[player].options.accessibility == "full":
+ multiworld.get_location(location, player).progress_type = LocationProgressType.EXCLUDED
+ else:
+ multiworld.get_location(location, player).access_rule = lambda state: False
+ else:
+ # There are not enough junk items to fill non-minimal players' vendors. Just set an item rule not allowing
+ # advancement items so that useful items can be placed.
+ for player in no_enemies_players:
+ for location in vendor_locations:
+ multiworld.get_location(location, player).item_rule = lambda item: not item.advancement
+
+
+class FFMQLocation(Location):
+ game = "Final Fantasy Mystic Quest"
+
+ def __init__(self, player, name, address, loc_type, access=None, event=None):
+ super(FFMQLocation, self).__init__(
+ player, name,
+ address
+ )
+ self.type = loc_type
+ if access:
+ process_rules(self, access)
+ if event:
+ self.place_locked_item(event)
diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py
new file mode 100644
index 000000000000..3c58487265a6
--- /dev/null
+++ b/worlds/ffmq/__init__.py
@@ -0,0 +1,221 @@
+import Utils
+import settings
+import base64
+import threading
+import requests
+import yaml
+from worlds.AutoWorld import World, WebWorld
+from BaseClasses import Tutorial
+from .Regions import create_regions, location_table, set_rules, stage_set_rules, rooms, non_dead_end_crest_rooms,\
+ non_dead_end_crest_warps
+from .Items import item_table, item_groups, create_items, FFMQItem, fillers
+from .Output import generate_output
+from .Options import FFMQOptions
+from .Client import FFMQClient
+
+
+# removed until lists are supported
+# class FFMQSettings(settings.Group):
+# class APIUrls(list):
+# """A list of API URLs to get map shuffle, crest shuffle, and battlefield reward shuffle data from."""
+# api_urls: APIUrls = [
+# "https://api.ffmqrando.net/",
+# "http://ffmqr.jalchavware.com:5271/"
+# ]
+
+
+class FFMQWebWorld(WebWorld):
+ setup_en = Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to playing Final Fantasy Mystic Quest with Archipelago.",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["Alchav"]
+ )
+
+ setup_fr = Tutorial(
+ setup_en.tutorial_name,
+ setup_en.description,
+ "Français",
+ "setup_fr.md",
+ "setup/fr",
+ ["Artea"]
+ )
+
+ tutorials = [setup_en, setup_fr]
+
+
+class FFMQWorld(World):
+ """Final Fantasy: Mystic Quest is a simple, humorous RPG for the Super Nintendo. You travel across four continents,
+ linked in the middle of the world by the Focus Tower, which has been locked by four magical coins. Make your way to
+ the bottom of the Focus Tower, then straight up through the top!"""
+ # -Giga Otomia
+
+ game = "Final Fantasy Mystic Quest"
+
+ item_name_to_id = {name: data.id for name, data in item_table.items() if data.id is not None}
+ location_name_to_id = location_table
+ options_dataclass = FFMQOptions
+ options: FFMQOptions
+
+ topology_present = True
+
+ item_name_groups = item_groups
+
+ generate_output = generate_output
+ create_items = create_items
+ create_regions = create_regions
+ set_rules = set_rules
+ stage_set_rules = stage_set_rules
+
+ web = FFMQWebWorld()
+ # settings: FFMQSettings
+
+ def __init__(self, world, player: int):
+ self.rom_name_available_event = threading.Event()
+ self.rom_name = None
+ self.rooms = None
+ super().__init__(world, player)
+
+ def generate_early(self):
+ if self.options.sky_coin_mode == "shattered_sky_coin":
+ self.options.brown_boxes.value = 1
+ if self.options.enemies_scaling_lower.value > self.options.enemies_scaling_upper.value:
+ self.options.enemies_scaling_lower.value, self.options.enemies_scaling_upper.value = \
+ self.options.enemies_scaling_upper.value, self.options.enemies_scaling_lower.value
+ if self.options.bosses_scaling_lower.value > self.options.bosses_scaling_upper.value:
+ self.options.bosses_scaling_lower.value, self.options.bosses_scaling_upper.value = \
+ self.options.bosses_scaling_upper.value, self.options.bosses_scaling_lower.value
+
+ @classmethod
+ def stage_generate_early(cls, multiworld):
+
+ # api_urls = Utils.get_options()["ffmq_options"].get("api_urls", None)
+ api_urls = [
+ "https://api.ffmqrando.net/",
+ "http://ffmqr.jalchavware.com:5271/"
+ ]
+
+ rooms_data = {}
+
+ for world in multiworld.get_game_worlds("Final Fantasy Mystic Quest"):
+ if (world.options.map_shuffle or world.options.crest_shuffle or world.options.shuffle_battlefield_rewards
+ or world.options.companions_locations):
+ if world.options.map_shuffle_seed.value.isdigit():
+ multiworld.random.seed(int(world.options.map_shuffle_seed.value))
+ elif world.options.map_shuffle_seed.value != "random":
+ multiworld.random.seed(int(hash(world.options.map_shuffle_seed.value))
+ + int(world.multiworld.seed))
+
+ seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper()
+ map_shuffle = world.options.map_shuffle.value
+ crest_shuffle = world.options.crest_shuffle.current_key
+ battlefield_shuffle = world.options.shuffle_battlefield_rewards.current_key
+ companion_shuffle = world.options.companions_locations.value
+ kaeli_mom = world.options.kaelis_mom_fight_minotaur.current_key
+
+ query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}&cs={companion_shuffle}&km={kaeli_mom}"
+
+ if query in rooms_data:
+ world.rooms = rooms_data[query]
+ continue
+
+ if not api_urls:
+ raise Exception("No FFMQR API URLs specified in host.yaml")
+
+ errors = []
+ for api_url in api_urls.copy():
+ try:
+ response = requests.get(f"{api_url}GenerateRooms?{query}")
+ except (ConnectionError, requests.exceptions.HTTPError, requests.exceptions.ConnectionError,
+ requests.exceptions.RequestException) as err:
+ api_urls.remove(api_url)
+ errors.append([api_url, err])
+ else:
+ if response.ok:
+ world.rooms = rooms_data[query] = yaml.load(response.text, yaml.Loader)
+ break
+ else:
+ api_urls.remove(api_url)
+ errors.append([api_url, response])
+ else:
+ error_text = f"Failed to fetch map shuffle data for FFMQ player {world.player}"
+ for error in errors:
+ error_text += f"\n{error[0]} - got error {error[1].status_code} {error[1].reason} {error[1].text}"
+ raise Exception(error_text)
+ api_urls.append(api_urls.pop(0))
+ else:
+ world.rooms = rooms
+
+ def create_item(self, name: str):
+ return FFMQItem(name, self.player)
+
+ def collect_item(self, state, item, remove=False):
+ if "Progressive" in item.name:
+ i = item.code - 256
+ if state.has(self.item_id_to_name[i], self.player):
+ if state.has(self.item_id_to_name[i+1], self.player):
+ return self.item_id_to_name[i+2]
+ return self.item_id_to_name[i+1]
+ return self.item_id_to_name[i]
+ return item.name if item.advancement else None
+
+ def modify_multidata(self, multidata):
+ # wait for self.rom_name to be available.
+ self.rom_name_available_event.wait()
+ rom_name = getattr(self, "rom_name", None)
+ # we skip in case of error, so that the original error in the output thread is the one that gets raised
+ if rom_name:
+ new_name = base64.b64encode(bytes(self.rom_name)).decode()
+ payload = multidata["connect_names"][self.multiworld.player_name[self.player]]
+ multidata["connect_names"][new_name] = payload
+
+ def get_filler_item_name(self):
+ r = self.multiworld.random.randint(0, 201)
+ for item, count in fillers.items():
+ r -= count
+ r -= fillers[item]
+ if r <= 0:
+ return item
+
+ def extend_hint_information(self, hint_data):
+ hint_data[self.player] = {}
+ if self.options.map_shuffle:
+ single_location_regions = ["Subregion Volcano Battlefield", "Subregion Mac's Ship", "Subregion Doom Castle"]
+ for subregion in ["Subregion Foresta", "Subregion Aquaria", "Subregion Frozen Fields", "Subregion Fireburg",
+ "Subregion Volcano Battlefield", "Subregion Windia", "Subregion Mac's Ship",
+ "Subregion Doom Castle"]:
+ region = self.multiworld.get_region(subregion, self.player)
+ for location in region.locations:
+ if location.address and self.options.map_shuffle != "dungeons":
+ hint_data[self.player][location.address] = (subregion.split("Subregion ")[-1]
+ + (" Region" if subregion not in
+ single_location_regions else ""))
+ for overworld_spot in region.exits:
+ if ("Subregion" in overworld_spot.connected_region.name or
+ overworld_spot.name == "Overworld - Mac Ship Doom" or "Focus Tower" in overworld_spot.name
+ or "Doom Castle" in overworld_spot.name or overworld_spot.name == "Overworld - Giant Tree"):
+ continue
+ exits = list(overworld_spot.connected_region.exits) + [overworld_spot]
+ checked_regions = set()
+ while exits:
+ exit_check = exits.pop()
+ if (exit_check.connected_region not in checked_regions and "Subregion" not in
+ exit_check.connected_region.name):
+ checked_regions.add(exit_check.connected_region)
+ exits.extend(exit_check.connected_region.exits)
+ for location in exit_check.connected_region.locations:
+ if location.address:
+ hint = []
+ if self.options.map_shuffle != "dungeons":
+ hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not
+ in single_location_regions else "")))
+ if self.options.map_shuffle != "overworld":
+ hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu",
+ "Pazuzu's"))
+ hint = " - ".join(hint).replace(" - Mac Ship", "")
+ if location.address in hint_data[self.player]:
+ hint_data[self.player][location.address] += f"/{hint}"
+ else:
+ hint_data[self.player][location.address] = hint
diff --git a/worlds/ffmq/data/rooms.py b/worlds/ffmq/data/rooms.py
new file mode 100644
index 000000000000..38634f107679
--- /dev/null
+++ b/worlds/ffmq/data/rooms.py
@@ -0,0 +1,2 @@
+rooms = [{'name': 'Overworld', 'id': 0, 'type': 'Overworld', 'game_objects': [], 'links': [{'target_room': 220, 'access': []}]}, {'name': 'Subregion Foresta', 'id': 220, 'type': 'Subregion', 'region': 'Foresta', 'game_objects': [{'name': 'Foresta South Battlefield', 'object_id': 1, 'location': 'ForestaSouthBattlefield', 'location_slot': 'ForestaSouthBattlefield', 'type': 'BattlefieldXp', 'access': []}, {'name': 'Foresta West Battlefield', 'object_id': 2, 'location': 'ForestaWestBattlefield', 'location_slot': 'ForestaWestBattlefield', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Foresta East Battlefield', 'object_id': 3, 'location': 'ForestaEastBattlefield', 'location_slot': 'ForestaEastBattlefield', 'type': 'BattlefieldGp', 'access': []}], 'links': [{'target_room': 15, 'location': 'LevelForest', 'location_slot': 'LevelForest', 'entrance': 445, 'teleporter': [46, 8], 'access': []}, {'target_room': 16, 'location': 'Foresta', 'location_slot': 'Foresta', 'entrance': 446, 'teleporter': [2, 1], 'access': []}, {'target_room': 24, 'location': 'SandTemple', 'location_slot': 'SandTemple', 'entrance': 447, 'teleporter': [3, 1], 'access': []}, {'target_room': 25, 'location': 'BoneDungeon', 'location_slot': 'BoneDungeon', 'entrance': 448, 'teleporter': [4, 1], 'access': []}, {'target_room': 3, 'location': 'FocusTowerForesta', 'location_slot': 'FocusTowerForesta', 'entrance': 449, 'teleporter': [5, 1], 'access': []}, {'target_room': 221, 'access': ['SandCoin']}, {'target_room': 224, 'access': ['RiverCoin']}, {'target_room': 226, 'access': ['SunCoin']}]}, {'name': 'Subregion Aquaria', 'id': 221, 'type': 'Subregion', 'region': 'Aquaria', 'game_objects': [{'name': 'South of Libra Temple Battlefield', 'object_id': 4, 'location': 'AquariaBattlefield01', 'location_slot': 'AquariaBattlefield01', 'type': 'BattlefieldXp', 'access': []}, {'name': 'East of Libra Temple Battlefield', 'object_id': 5, 'location': 'AquariaBattlefield02', 'location_slot': 'AquariaBattlefield02', 'type': 'BattlefieldGp', 'access': []}, {'name': 'South of Aquaria Battlefield', 'object_id': 6, 'location': 'AquariaBattlefield03', 'location_slot': 'AquariaBattlefield03', 'type': 'BattlefieldItem', 'access': []}, {'name': 'South of Wintry Cave Battlefield', 'object_id': 7, 'location': 'WintryBattlefield01', 'location_slot': 'WintryBattlefield01', 'type': 'BattlefieldXp', 'access': []}, {'name': 'West of Wintry Cave Battlefield', 'object_id': 8, 'location': 'WintryBattlefield02', 'location_slot': 'WintryBattlefield02', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Ice Pyramid Battlefield', 'object_id': 9, 'location': 'PyramidBattlefield01', 'location_slot': 'PyramidBattlefield01', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 10, 'location': 'FocusTowerAquaria', 'location_slot': 'FocusTowerAquaria', 'entrance': 450, 'teleporter': [19, 1], 'access': []}, {'target_room': 39, 'location': 'LibraTemple', 'location_slot': 'LibraTemple', 'entrance': 451, 'teleporter': [7, 1], 'access': []}, {'target_room': 40, 'location': 'Aquaria', 'location_slot': 'Aquaria', 'entrance': 452, 'teleporter': [8, 8], 'access': []}, {'target_room': 45, 'location': 'WintryCave', 'location_slot': 'WintryCave', 'entrance': 453, 'teleporter': [10, 1], 'access': []}, {'target_room': 52, 'location': 'FallsBasin', 'location_slot': 'FallsBasin', 'entrance': 455, 'teleporter': [12, 1], 'access': []}, {'target_room': 54, 'location': 'IcePyramid', 'location_slot': 'IcePyramid', 'entrance': 456, 'teleporter': [13, 1], 'access': []}, {'target_room': 220, 'access': ['SandCoin']}, {'target_room': 224, 'access': ['SandCoin', 'RiverCoin']}, {'target_room': 226, 'access': ['SandCoin', 'SunCoin']}, {'target_room': 223, 'access': ['SummerAquaria']}]}, {'name': 'Subregion Life Temple', 'id': 222, 'type': 'Subregion', 'region': 'LifeTemple', 'game_objects': [], 'links': [{'target_room': 51, 'location': 'LifeTemple', 'location_slot': 'LifeTemple', 'entrance': 454, 'teleporter': [11, 1], 'access': []}]}, {'name': 'Subregion Frozen Fields', 'id': 223, 'type': 'Subregion', 'region': 'AquariaFrozenField', 'game_objects': [{'name': 'North of Libra Temple Battlefield', 'object_id': 10, 'location': 'LibraBattlefield01', 'location_slot': 'LibraBattlefield01', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Aquaria Frozen Field Battlefield', 'object_id': 11, 'location': 'LibraBattlefield02', 'location_slot': 'LibraBattlefield02', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 74, 'location': 'WintryTemple', 'location_slot': 'WintryTemple', 'entrance': 458, 'teleporter': [16, 1], 'access': []}, {'target_room': 14, 'location': 'FocusTowerFrozen', 'location_slot': 'FocusTowerFrozen', 'entrance': 459, 'teleporter': [17, 1], 'access': []}, {'target_room': 221, 'access': []}, {'target_room': 225, 'access': ['SummerAquaria', 'DualheadHydra']}]}, {'name': 'Subregion Fireburg', 'id': 224, 'type': 'Subregion', 'region': 'Fireburg', 'game_objects': [{'name': 'Path to Fireburg Southern Battlefield', 'object_id': 12, 'location': 'FireburgBattlefield01', 'location_slot': 'FireburgBattlefield01', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Path to Fireburg Central Battlefield', 'object_id': 13, 'location': 'FireburgBattlefield02', 'location_slot': 'FireburgBattlefield02', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Path to Fireburg Northern Battlefield', 'object_id': 14, 'location': 'FireburgBattlefield03', 'location_slot': 'FireburgBattlefield03', 'type': 'BattlefieldXp', 'access': []}, {'name': 'Sealed Temple Battlefield', 'object_id': 15, 'location': 'MineBattlefield01', 'location_slot': 'MineBattlefield01', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Mine Battlefield', 'object_id': 16, 'location': 'MineBattlefield02', 'location_slot': 'MineBattlefield02', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Boulder Battlefield', 'object_id': 17, 'location': 'MineBattlefield03', 'location_slot': 'MineBattlefield03', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 13, 'location': 'FocusTowerFireburg', 'location_slot': 'FocusTowerFireburg', 'entrance': 460, 'teleporter': [18, 1], 'access': []}, {'target_room': 76, 'location': 'Fireburg', 'location_slot': 'Fireburg', 'entrance': 461, 'teleporter': [20, 1], 'access': []}, {'target_room': 84, 'location': 'Mine', 'location_slot': 'Mine', 'entrance': 462, 'teleporter': [21, 1], 'access': []}, {'target_room': 92, 'location': 'SealedTemple', 'location_slot': 'SealedTemple', 'entrance': 463, 'teleporter': [22, 1], 'access': []}, {'target_room': 93, 'location': 'Volcano', 'location_slot': 'Volcano', 'entrance': 464, 'teleporter': [23, 1], 'access': []}, {'target_room': 100, 'location': 'LavaDome', 'location_slot': 'LavaDome', 'entrance': 465, 'teleporter': [24, 1], 'access': []}, {'target_room': 220, 'access': ['RiverCoin']}, {'target_room': 221, 'access': ['SandCoin', 'RiverCoin']}, {'target_room': 226, 'access': ['RiverCoin', 'SunCoin']}, {'target_room': 225, 'access': ['DualheadHydra']}]}, {'name': 'Subregion Volcano Battlefield', 'id': 225, 'type': 'Subregion', 'region': 'VolcanoBattlefield', 'game_objects': [{'name': 'Volcano Battlefield', 'object_id': 18, 'location': 'VolcanoBattlefield01', 'location_slot': 'VolcanoBattlefield01', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 224, 'access': ['DualheadHydra']}, {'target_room': 223, 'access': ['SummerAquaria']}]}, {'name': 'Subregion Windia', 'id': 226, 'type': 'Subregion', 'region': 'Windia', 'game_objects': [{'name': 'Kaidge Temple Battlefield', 'object_id': 19, 'location': 'WindiaBattlefield01', 'location_slot': 'WindiaBattlefield01', 'type': 'BattlefieldXp', 'access': ['SandCoin', 'RiverCoin']}, {'name': 'South of Windia Battlefield', 'object_id': 20, 'location': 'WindiaBattlefield02', 'location_slot': 'WindiaBattlefield02', 'type': 'BattlefieldXp', 'access': ['SandCoin', 'RiverCoin']}], 'links': [{'target_room': 9, 'location': 'FocusTowerWindia', 'location_slot': 'FocusTowerWindia', 'entrance': 466, 'teleporter': [6, 1], 'access': []}, {'target_room': 123, 'location': 'RopeBridge', 'location_slot': 'RopeBridge', 'entrance': 467, 'teleporter': [25, 1], 'access': []}, {'target_room': 124, 'location': 'AliveForest', 'location_slot': 'AliveForest', 'entrance': 468, 'teleporter': [26, 1], 'access': []}, {'target_room': 125, 'location': 'GiantTree', 'location_slot': 'GiantTree', 'entrance': 469, 'teleporter': [27, 1], 'access': ['Barred']}, {'target_room': 152, 'location': 'KaidgeTemple', 'location_slot': 'KaidgeTemple', 'entrance': 470, 'teleporter': [28, 1], 'access': []}, {'target_room': 156, 'location': 'Windia', 'location_slot': 'Windia', 'entrance': 471, 'teleporter': [29, 1], 'access': []}, {'target_room': 154, 'location': 'WindholeTemple', 'location_slot': 'WindholeTemple', 'entrance': 472, 'teleporter': [30, 1], 'access': []}, {'target_room': 155, 'location': 'MountGale', 'location_slot': 'MountGale', 'entrance': 473, 'teleporter': [31, 1], 'access': []}, {'target_room': 166, 'location': 'PazuzusTower', 'location_slot': 'PazuzusTower', 'entrance': 474, 'teleporter': [32, 1], 'access': []}, {'target_room': 220, 'access': ['SunCoin']}, {'target_room': 221, 'access': ['SandCoin', 'SunCoin']}, {'target_room': 224, 'access': ['RiverCoin', 'SunCoin']}, {'target_room': 227, 'access': ['RainbowBridge']}]}, {'name': "Subregion Spencer's Cave", 'id': 227, 'type': 'Subregion', 'region': 'SpencerCave', 'game_objects': [], 'links': [{'target_room': 73, 'location': 'SpencersPlace', 'location_slot': 'SpencersPlace', 'entrance': 457, 'teleporter': [48, 8], 'access': []}, {'target_room': 226, 'access': ['RainbowBridge']}]}, {'name': 'Subregion Ship Dock', 'id': 228, 'type': 'Subregion', 'region': 'ShipDock', 'game_objects': [], 'links': [{'target_room': 186, 'location': 'ShipDock', 'location_slot': 'ShipDock', 'entrance': 475, 'teleporter': [62, 1], 'access': []}, {'target_room': 229, 'access': ['ShipLiberated', 'ShipDockAccess']}]}, {'name': "Subregion Mac's Ship", 'id': 229, 'type': 'Subregion', 'region': 'MacShip', 'game_objects': [], 'links': [{'target_room': 187, 'location': 'MacsShip', 'location_slot': 'MacsShip', 'entrance': 478, 'teleporter': [36, 1], 'access': []}, {'target_room': 228, 'access': ['ShipLiberated', 'ShipDockAccess']}, {'target_room': 231, 'access': ['ShipLoaned', 'ShipDockAccess', 'ShipSteeringWheel']}]}, {'name': 'Subregion Light Temple', 'id': 230, 'type': 'Subregion', 'region': 'LightTemple', 'game_objects': [], 'links': [{'target_room': 185, 'location': 'LightTemple', 'location_slot': 'LightTemple', 'entrance': 477, 'teleporter': [35, 1], 'access': []}]}, {'name': 'Subregion Doom Castle', 'id': 231, 'type': 'Subregion', 'region': 'DoomCastle', 'game_objects': [], 'links': [{'target_room': 1, 'location': 'DoomCastle', 'location_slot': 'DoomCastle', 'entrance': 476, 'teleporter': [33, 1], 'access': []}, {'target_room': 187, 'location': 'MacsShipDoom', 'location_slot': 'MacsShipDoom', 'entrance': 479, 'teleporter': [36, 1], 'access': ['Barred']}, {'target_room': 229, 'access': ['ShipLoaned', 'ShipDockAccess', 'ShipSteeringWheel']}]}, {'name': 'Doom Castle - Sand Floor', 'id': 1, 'game_objects': [{'name': 'Doom Castle B2 - Southeast Chest', 'object_id': 1, 'type': 'Chest', 'access': ['Bomb']}, {'name': 'Doom Castle B2 - Bone Ledge Box', 'object_id': 30, 'type': 'Box', 'access': []}, {'name': 'Doom Castle B2 - Hook Platform Box', 'object_id': 31, 'type': 'Box', 'access': ['DragonClaw']}], 'links': [{'target_room': 231, 'entrance': 1, 'teleporter': [1, 6], 'access': []}, {'target_room': 5, 'entrance': 0, 'teleporter': [0, 0], 'access': ['DragonClaw', 'MegaGrenade']}]}, {'name': 'Doom Castle - Aero Room', 'id': 2, 'game_objects': [{'name': 'Doom Castle B2 - Sun Door Chest', 'object_id': 0, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 4, 'entrance': 2, 'teleporter': [1, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Main Loop', 'id': 3, 'game_objects': [], 'links': [{'target_room': 220, 'entrance': 3, 'teleporter': [2, 6], 'access': []}, {'target_room': 6, 'entrance': 4, 'teleporter': [4, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Aero Corridor', 'id': 4, 'game_objects': [], 'links': [{'target_room': 9, 'entrance': 5, 'teleporter': [5, 0], 'access': []}, {'target_room': 2, 'entrance': 6, 'teleporter': [8, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Inner Loop', 'id': 5, 'game_objects': [], 'links': [{'target_room': 1, 'entrance': 8, 'teleporter': [7, 0], 'access': []}, {'target_room': 201, 'entrance': 7, 'teleporter': [6, 0], 'access': []}]}, {'name': 'Focus Tower 1F Main Lobby', 'id': 6, 'game_objects': [{'name': 'Focus Tower 1F - Main Lobby Box', 'object_id': 33, 'type': 'Box', 'access': []}], 'links': [{'target_room': 3, 'entrance': 11, 'teleporter': [11, 0], 'access': []}, {'target_room': 7, 'access': ['SandCoin']}, {'target_room': 8, 'access': ['RiverCoin']}, {'target_room': 9, 'access': ['SunCoin']}]}, {'name': 'Focus Tower 1F SandCoin Room', 'id': 7, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['SandCoin']}, {'target_room': 10, 'entrance': 10, 'teleporter': [10, 0], 'access': []}]}, {'name': 'Focus Tower 1F RiverCoin Room', 'id': 8, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['RiverCoin']}, {'target_room': 11, 'entrance': 14, 'teleporter': [14, 0], 'access': []}]}, {'name': 'Focus Tower 1F SunCoin Room', 'id': 9, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['SunCoin']}, {'target_room': 4, 'entrance': 12, 'teleporter': [12, 0], 'access': []}, {'target_room': 226, 'entrance': 9, 'teleporter': [3, 6], 'access': []}]}, {'name': 'Focus Tower 1F SkyCoin Room', 'id': 201, 'game_objects': [], 'links': [{'target_room': 195, 'entrance': 13, 'teleporter': [13, 0], 'access': ['SkyCoin', 'FlamerusRex', 'IceGolem', 'DualheadHydra', 'Pazuzu']}, {'target_room': 5, 'entrance': 15, 'teleporter': [15, 0], 'access': []}]}, {'name': 'Focus Tower 2F - Sand Coin Passage', 'id': 10, 'game_objects': [{'name': 'Focus Tower 2F - Sand Door Chest', 'object_id': 3, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 221, 'entrance': 16, 'teleporter': [4, 6], 'access': []}, {'target_room': 7, 'entrance': 17, 'teleporter': [17, 0], 'access': []}]}, {'name': 'Focus Tower 2F - River Coin Passage', 'id': 11, 'game_objects': [], 'links': [{'target_room': 8, 'entrance': 18, 'teleporter': [18, 0], 'access': []}, {'target_room': 13, 'entrance': 19, 'teleporter': [20, 0], 'access': []}]}, {'name': 'Focus Tower 2F - Venus Chest Room', 'id': 12, 'game_objects': [{'name': 'Focus Tower 2F - Back Door Chest', 'object_id': 2, 'type': 'Chest', 'access': []}, {'name': 'Focus Tower 2F - Venus Chest', 'object_id': 9, 'type': 'NPC', 'access': ['Bomb', 'VenusKey']}], 'links': [{'target_room': 14, 'entrance': 20, 'teleporter': [19, 0], 'access': []}]}, {'name': 'Focus Tower 3F - Lower Floor', 'id': 13, 'game_objects': [{'name': 'Focus Tower 3F - River Door Box', 'object_id': 34, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 22, 'teleporter': [6, 6], 'access': []}, {'target_room': 11, 'entrance': 23, 'teleporter': [24, 0], 'access': []}]}, {'name': 'Focus Tower 3F - Upper Floor', 'id': 14, 'game_objects': [], 'links': [{'target_room': 223, 'entrance': 24, 'teleporter': [5, 6], 'access': []}, {'target_room': 12, 'entrance': 25, 'teleporter': [23, 0], 'access': []}]}, {'name': 'Level Forest', 'id': 15, 'game_objects': [{'name': 'Level Forest - Northwest Box', 'object_id': 40, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Northeast Box', 'object_id': 41, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Middle Box', 'object_id': 42, 'type': 'Box', 'access': []}, {'name': 'Level Forest - Southwest Box', 'object_id': 43, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Southeast Box', 'object_id': 44, 'type': 'Box', 'access': ['Axe']}, {'name': 'Minotaur', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Minotaur'], 'access': ['Kaeli1']}, {'name': 'Level Forest - Old Man', 'object_id': 0, 'type': 'NPC', 'access': []}, {'name': 'Level Forest - Kaeli', 'object_id': 1, 'type': 'NPC', 'access': ['Kaeli1', 'Minotaur']}], 'links': [{'target_room': 220, 'entrance': 28, 'teleporter': [25, 0], 'access': []}]}, {'name': 'Foresta', 'id': 16, 'game_objects': [{'name': 'Foresta - Outside Box', 'object_id': 45, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 220, 'entrance': 38, 'teleporter': [31, 0], 'access': []}, {'target_room': 17, 'entrance': 44, 'teleporter': [0, 5], 'access': []}, {'target_room': 18, 'entrance': 42, 'teleporter': [32, 4], 'access': []}, {'target_room': 19, 'entrance': 43, 'teleporter': [33, 0], 'access': []}, {'target_room': 20, 'entrance': 45, 'teleporter': [1, 5], 'access': []}]}, {'name': "Kaeli's House", 'id': 17, 'game_objects': [{'name': "Foresta - Kaeli's House Box", 'object_id': 46, 'type': 'Box', 'access': []}, {'name': 'Kaeli Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Kaeli1'], 'access': ['TreeWither']}, {'name': 'Kaeli 2', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Kaeli2'], 'access': ['Kaeli1', 'Minotaur', 'Elixir']}], 'links': [{'target_room': 16, 'entrance': 46, 'teleporter': [86, 3], 'access': []}]}, {'name': "Foresta Houses - Old Man's House Main", 'id': 18, 'game_objects': [], 'links': [{'target_room': 19, 'access': ['BarrelPushed']}, {'target_room': 16, 'entrance': 47, 'teleporter': [34, 0], 'access': []}]}, {'name': "Foresta Houses - Old Man's House Back", 'id': 19, 'game_objects': [{'name': 'Foresta - Old Man House Chest', 'object_id': 5, 'type': 'Chest', 'access': []}, {'name': 'Old Man Barrel', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['BarrelPushed'], 'access': []}], 'links': [{'target_room': 18, 'access': ['BarrelPushed']}, {'target_room': 16, 'entrance': 48, 'teleporter': [35, 0], 'access': []}]}, {'name': 'Foresta Houses - Rest House', 'id': 20, 'game_objects': [{'name': 'Foresta - Rest House Box', 'object_id': 47, 'type': 'Box', 'access': []}], 'links': [{'target_room': 16, 'entrance': 50, 'teleporter': [87, 3], 'access': []}]}, {'name': 'Libra Treehouse', 'id': 21, 'game_objects': [{'name': 'Alive Forest - Libra Treehouse Box', 'object_id': 50, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 51, 'teleporter': [67, 8], 'access': ['LibraCrest']}]}, {'name': 'Gemini Treehouse', 'id': 22, 'game_objects': [{'name': 'Alive Forest - Gemini Treehouse Box', 'object_id': 51, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 52, 'teleporter': [68, 8], 'access': ['GeminiCrest']}]}, {'name': 'Mobius Treehouse', 'id': 23, 'game_objects': [{'name': 'Alive Forest - Mobius Treehouse West Box', 'object_id': 48, 'type': 'Box', 'access': []}, {'name': 'Alive Forest - Mobius Treehouse East Box', 'object_id': 49, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 53, 'teleporter': [69, 8], 'access': ['MobiusCrest']}]}, {'name': 'Sand Temple', 'id': 24, 'game_objects': [{'name': 'Tristam Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Tristam'], 'access': []}], 'links': [{'target_room': 220, 'entrance': 54, 'teleporter': [36, 0], 'access': []}]}, {'name': 'Bone Dungeon 1F', 'id': 25, 'game_objects': [{'name': 'Bone Dungeon 1F - Entrance Room West Box', 'object_id': 53, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon 1F - Entrance Room Middle Box', 'object_id': 54, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon 1F - Entrance Room East Box', 'object_id': 55, 'type': 'Box', 'access': []}], 'links': [{'target_room': 220, 'entrance': 55, 'teleporter': [37, 0], 'access': []}, {'target_room': 26, 'entrance': 56, 'teleporter': [2, 2], 'access': []}]}, {'name': 'Bone Dungeon B1 - Waterway', 'id': 26, 'game_objects': [{'name': 'Bone Dungeon B1 - Skull Chest', 'object_id': 6, 'type': 'Chest', 'access': ['Bomb']}, {'name': 'Bone Dungeon B1 - Tristam', 'object_id': 2, 'type': 'NPC', 'access': ['Tristam']}, {'name': 'Tristam Bone Dungeon Item Given', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['TristamBoneItemGiven'], 'access': ['Tristam']}], 'links': [{'target_room': 25, 'entrance': 59, 'teleporter': [88, 3], 'access': []}, {'target_room': 28, 'entrance': 57, 'teleporter': [3, 2], 'access': ['Bomb']}]}, {'name': 'Bone Dungeon B1 - Checker Room', 'id': 28, 'game_objects': [{'name': 'Bone Dungeon B1 - Checker Room Box', 'object_id': 56, 'type': 'Box', 'access': ['Bomb']}], 'links': [{'target_room': 26, 'entrance': 61, 'teleporter': [89, 3], 'access': []}, {'target_room': 30, 'entrance': 60, 'teleporter': [4, 2], 'access': []}]}, {'name': 'Bone Dungeon B1 - Hidden Room', 'id': 29, 'game_objects': [{'name': 'Bone Dungeon B1 - Ribcage Waterway Box', 'object_id': 57, 'type': 'Box', 'access': []}], 'links': [{'target_room': 31, 'entrance': 62, 'teleporter': [91, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - First Room', 'id': 30, 'game_objects': [{'name': 'Bone Dungeon B2 - Spines Room Alcove Box', 'object_id': 59, 'type': 'Box', 'access': []}, {'name': 'Long Spine', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['LongSpineBombed'], 'access': ['Bomb']}], 'links': [{'target_room': 28, 'entrance': 65, 'teleporter': [90, 3], 'access': []}, {'target_room': 31, 'access': ['LongSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - Second Room', 'id': 31, 'game_objects': [{'name': 'Bone Dungeon B2 - Spines Room Looped Hallway Box', 'object_id': 58, 'type': 'Box', 'access': []}, {'name': 'Short Spine', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShortSpineBombed'], 'access': ['Bomb']}], 'links': [{'target_room': 29, 'entrance': 63, 'teleporter': [5, 2], 'access': ['LongSpineBombed']}, {'target_room': 32, 'access': ['ShortSpineBombed']}, {'target_room': 30, 'access': ['LongSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - Third Room', 'id': 32, 'game_objects': [], 'links': [{'target_room': 35, 'entrance': 64, 'teleporter': [6, 2], 'access': []}, {'target_room': 31, 'access': ['ShortSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Box Room', 'id': 33, 'game_objects': [{'name': 'Bone Dungeon B2 - Lone Room Box', 'object_id': 61, 'type': 'Box', 'access': []}], 'links': [{'target_room': 36, 'entrance': 66, 'teleporter': [93, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Quake Room', 'id': 34, 'game_objects': [{'name': 'Bone Dungeon B2 - Penultimate Room Chest', 'object_id': 7, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 37, 'entrance': 67, 'teleporter': [94, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - First Room', 'id': 35, 'game_objects': [{'name': 'Bone Dungeon B2 - Two Skulls Room Box', 'object_id': 60, 'type': 'Box', 'access': []}, {'name': 'Skull 1', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Skull1Bombed'], 'access': ['Bomb']}], 'links': [{'target_room': 32, 'entrance': 71, 'teleporter': [92, 3], 'access': []}, {'target_room': 36, 'access': ['Skull1Bombed']}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - Second Room', 'id': 36, 'game_objects': [{'name': 'Skull 2', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Skull2Bombed'], 'access': ['Bomb']}], 'links': [{'target_room': 33, 'entrance': 68, 'teleporter': [7, 2], 'access': []}, {'target_room': 37, 'access': ['Skull2Bombed']}, {'target_room': 35, 'access': ['Skull1Bombed']}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - Third Room', 'id': 37, 'game_objects': [], 'links': [{'target_room': 34, 'entrance': 69, 'teleporter': [8, 2], 'access': []}, {'target_room': 38, 'entrance': 70, 'teleporter': [9, 2], 'access': ['Bomb']}, {'target_room': 36, 'access': ['Skull2Bombed']}]}, {'name': 'Bone Dungeon B2 - Boss Room', 'id': 38, 'game_objects': [{'name': 'Bone Dungeon B2 - North Box', 'object_id': 62, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon B2 - South Box', 'object_id': 63, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon B2 - Flamerus Rex Chest', 'object_id': 8, 'type': 'Chest', 'access': []}, {'name': "Bone Dungeon B2 - Tristam's Treasure Chest", 'object_id': 4, 'type': 'Chest', 'access': []}, {'name': 'Flamerus Rex', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['FlamerusRex'], 'access': []}], 'links': [{'target_room': 37, 'entrance': 74, 'teleporter': [95, 3], 'access': []}]}, {'name': 'Libra Temple', 'id': 39, 'game_objects': [{'name': 'Libra Temple - Box', 'object_id': 64, 'type': 'Box', 'access': []}, {'name': 'Phoebe Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Phoebe1'], 'access': []}], 'links': [{'target_room': 221, 'entrance': 75, 'teleporter': [13, 6], 'access': []}, {'target_room': 51, 'entrance': 76, 'teleporter': [59, 8], 'access': ['LibraCrest']}]}, {'name': 'Aquaria', 'id': 40, 'game_objects': [{'name': 'Summer Aquaria', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['SummerAquaria'], 'access': ['WakeWater']}], 'links': [{'target_room': 221, 'entrance': 77, 'teleporter': [8, 6], 'access': []}, {'target_room': 41, 'entrance': 81, 'teleporter': [10, 5], 'access': []}, {'target_room': 42, 'entrance': 82, 'teleporter': [44, 4], 'access': []}, {'target_room': 44, 'entrance': 83, 'teleporter': [11, 5], 'access': []}, {'target_room': 71, 'entrance': 89, 'teleporter': [42, 0], 'access': ['SummerAquaria']}, {'target_room': 71, 'entrance': 90, 'teleporter': [43, 0], 'access': ['SummerAquaria']}]}, {'name': "Phoebe's House", 'id': 41, 'game_objects': [{'name': "Aquaria - Phoebe's House Chest", 'object_id': 65, 'type': 'Box', 'access': []}], 'links': [{'target_room': 40, 'entrance': 93, 'teleporter': [5, 8], 'access': []}]}, {'name': 'Aquaria Vendor House', 'id': 42, 'game_objects': [{'name': 'Aquaria - Vendor', 'object_id': 4, 'type': 'NPC', 'access': []}, {'name': 'Aquaria - Vendor House Box', 'object_id': 66, 'type': 'Box', 'access': []}], 'links': [{'target_room': 40, 'entrance': 94, 'teleporter': [40, 8], 'access': []}, {'target_room': 43, 'entrance': 95, 'teleporter': [47, 0], 'access': []}]}, {'name': 'Aquaria Gemini Room', 'id': 43, 'game_objects': [], 'links': [{'target_room': 42, 'entrance': 97, 'teleporter': [48, 0], 'access': []}, {'target_room': 81, 'entrance': 96, 'teleporter': [72, 8], 'access': ['GeminiCrest']}]}, {'name': 'Aquaria INN', 'id': 44, 'game_objects': [], 'links': [{'target_room': 40, 'entrance': 98, 'teleporter': [75, 8], 'access': []}]}, {'name': 'Wintry Cave 1F - East Ledge', 'id': 45, 'game_objects': [{'name': 'Wintry Cave 1F - North Box', 'object_id': 67, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 1F - Entrance Box', 'object_id': 70, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 1F - Slippery Cliff Box', 'object_id': 68, 'type': 'Box', 'access': ['Claw']}, {'name': 'Wintry Cave 1F - Phoebe', 'object_id': 5, 'type': 'NPC', 'access': ['Phoebe1']}], 'links': [{'target_room': 221, 'entrance': 99, 'teleporter': [49, 0], 'access': []}, {'target_room': 49, 'entrance': 100, 'teleporter': [14, 2], 'access': ['Bomb']}, {'target_room': 46, 'access': ['Claw']}]}, {'name': 'Wintry Cave 1F - Central Space', 'id': 46, 'game_objects': [{'name': 'Wintry Cave 1F - Scenic Overlook Box', 'object_id': 69, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 45, 'access': ['Claw']}, {'target_room': 47, 'access': ['Claw']}]}, {'name': 'Wintry Cave 1F - West Ledge', 'id': 47, 'game_objects': [], 'links': [{'target_room': 48, 'entrance': 101, 'teleporter': [15, 2], 'access': ['Bomb']}, {'target_room': 46, 'access': ['Claw']}]}, {'name': 'Wintry Cave 2F', 'id': 48, 'game_objects': [{'name': 'Wintry Cave 2F - West Left Box', 'object_id': 71, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - West Right Box', 'object_id': 72, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - East Left Box', 'object_id': 73, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - East Right Box', 'object_id': 74, 'type': 'Box', 'access': []}], 'links': [{'target_room': 47, 'entrance': 104, 'teleporter': [97, 3], 'access': []}, {'target_room': 50, 'entrance': 103, 'teleporter': [50, 0], 'access': []}]}, {'name': 'Wintry Cave 3F Top', 'id': 49, 'game_objects': [{'name': 'Wintry Cave 3F - West Box', 'object_id': 75, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 3F - East Box', 'object_id': 76, 'type': 'Box', 'access': []}], 'links': [{'target_room': 45, 'entrance': 105, 'teleporter': [96, 3], 'access': []}]}, {'name': 'Wintry Cave 3F Bottom', 'id': 50, 'game_objects': [{'name': 'Wintry Cave 3F - Squidite Chest', 'object_id': 9, 'type': 'Chest', 'access': ['Phanquid']}, {'name': 'Phanquid', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Phanquid'], 'access': []}, {'name': 'Wintry Cave 3F - Before Boss Box', 'object_id': 77, 'type': 'Box', 'access': []}], 'links': [{'target_room': 48, 'entrance': 106, 'teleporter': [51, 0], 'access': []}]}, {'name': 'Life Temple', 'id': 51, 'game_objects': [{'name': 'Life Temple - Box', 'object_id': 78, 'type': 'Box', 'access': []}, {'name': 'Life Temple - Mysterious Man', 'object_id': 6, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 222, 'entrance': 107, 'teleporter': [14, 6], 'access': []}, {'target_room': 39, 'entrance': 108, 'teleporter': [60, 8], 'access': ['LibraCrest']}]}, {'name': 'Fall Basin', 'id': 52, 'game_objects': [{'name': 'Falls Basin - Snow Crab Chest', 'object_id': 10, 'type': 'Chest', 'access': ['FreezerCrab']}, {'name': 'Freezer Crab', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['FreezerCrab'], 'access': []}, {'name': 'Falls Basin - Box', 'object_id': 79, 'type': 'Box', 'access': []}], 'links': [{'target_room': 221, 'entrance': 111, 'teleporter': [53, 0], 'access': []}]}, {'name': 'Ice Pyramid B1 Taunt Room', 'id': 53, 'game_objects': [{'name': 'Ice Pyramid B1 - Chest', 'object_id': 11, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid B1 - West Box', 'object_id': 80, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid B1 - North Box', 'object_id': 81, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid B1 - East Box', 'object_id': 82, 'type': 'Box', 'access': []}], 'links': [{'target_room': 68, 'entrance': 113, 'teleporter': [55, 0], 'access': []}]}, {'name': 'Ice Pyramid 1F Maze Lobby', 'id': 54, 'game_objects': [{'name': 'Ice Pyramid 1F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid1FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 221, 'entrance': 114, 'teleporter': [56, 0], 'access': []}, {'target_room': 55, 'access': ['IcePyramid1FStatue']}]}, {'name': 'Ice Pyramid 1F Maze', 'id': 55, 'game_objects': [{'name': 'Ice Pyramid 1F - East Alcove Chest', 'object_id': 13, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid 1F - Sandwiched Alcove Box', 'object_id': 83, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 1F - Southwest Left Box', 'object_id': 84, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 1F - Southwest Right Box', 'object_id': 85, 'type': 'Box', 'access': []}], 'links': [{'target_room': 56, 'entrance': 116, 'teleporter': [57, 0], 'access': []}, {'target_room': 57, 'entrance': 117, 'teleporter': [58, 0], 'access': []}, {'target_room': 58, 'entrance': 118, 'teleporter': [59, 0], 'access': []}, {'target_room': 59, 'entrance': 119, 'teleporter': [60, 0], 'access': []}, {'target_room': 60, 'entrance': 120, 'teleporter': [61, 0], 'access': []}, {'target_room': 54, 'access': ['IcePyramid1FStatue']}]}, {'name': 'Ice Pyramid 2F South Tiled Room', 'id': 56, 'game_objects': [{'name': 'Ice Pyramid 2F - South Side Glass Door Box', 'object_id': 87, 'type': 'Box', 'access': ['Sword']}, {'name': 'Ice Pyramid 2F - South Side East Box', 'object_id': 91, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 122, 'teleporter': [62, 0], 'access': []}, {'target_room': 61, 'entrance': 123, 'teleporter': [67, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F West Room', 'id': 57, 'game_objects': [{'name': 'Ice Pyramid 2F - Northwest Room Box', 'object_id': 90, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 124, 'teleporter': [63, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F Center Room', 'id': 58, 'game_objects': [{'name': 'Ice Pyramid 2F - Center Room Box', 'object_id': 86, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 125, 'teleporter': [64, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F Small North Room', 'id': 59, 'game_objects': [{'name': 'Ice Pyramid 2F - North Room Glass Door Box', 'object_id': 88, 'type': 'Box', 'access': ['Sword']}], 'links': [{'target_room': 55, 'entrance': 126, 'teleporter': [65, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F North Corridor', 'id': 60, 'game_objects': [{'name': 'Ice Pyramid 2F - North Corridor Glass Door Box', 'object_id': 89, 'type': 'Box', 'access': ['Sword']}], 'links': [{'target_room': 55, 'entrance': 127, 'teleporter': [66, 0], 'access': []}, {'target_room': 62, 'entrance': 128, 'teleporter': [68, 0], 'access': []}]}, {'name': 'Ice Pyramid 3F Two Boxes Room', 'id': 61, 'game_objects': [{'name': 'Ice Pyramid 3F - Staircase Dead End Left Box', 'object_id': 94, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Staircase Dead End Right Box', 'object_id': 95, 'type': 'Box', 'access': []}], 'links': [{'target_room': 56, 'entrance': 129, 'teleporter': [69, 0], 'access': []}]}, {'name': 'Ice Pyramid 3F Main Loop', 'id': 62, 'game_objects': [{'name': 'Ice Pyramid 3F - Inner Room North Box', 'object_id': 92, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Inner Room South Box', 'object_id': 93, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - East Alcove Box', 'object_id': 96, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Leapfrog Box', 'object_id': 97, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid3FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 60, 'entrance': 130, 'teleporter': [70, 0], 'access': []}, {'target_room': 63, 'access': ['IcePyramid3FStatue']}]}, {'name': 'Ice Pyramid 3F Blocked Room', 'id': 63, 'game_objects': [], 'links': [{'target_room': 64, 'entrance': 131, 'teleporter': [71, 0], 'access': []}, {'target_room': 62, 'access': ['IcePyramid3FStatue']}]}, {'name': 'Ice Pyramid 4F Main Loop', 'id': 64, 'game_objects': [], 'links': [{'target_room': 66, 'entrance': 133, 'teleporter': [73, 0], 'access': []}, {'target_room': 63, 'entrance': 132, 'teleporter': [72, 0], 'access': []}, {'target_room': 65, 'access': ['IcePyramid4FStatue']}]}, {'name': 'Ice Pyramid 4F Treasure Room', 'id': 65, 'game_objects': [{'name': 'Ice Pyramid 4F - Chest', 'object_id': 12, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid 4F - Northwest Box', 'object_id': 98, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - West Left Box', 'object_id': 99, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - West Right Box', 'object_id': 100, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - South Left Box', 'object_id': 101, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - South Right Box', 'object_id': 102, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - East Left Box', 'object_id': 103, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - East Right Box', 'object_id': 104, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid4FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 64, 'access': ['IcePyramid4FStatue']}]}, {'name': 'Ice Pyramid 5F Leap of Faith Room', 'id': 66, 'game_objects': [{'name': 'Ice Pyramid 5F - Glass Door Left Box', 'object_id': 105, 'type': 'Box', 'access': ['IcePyramid5FStatue']}, {'name': 'Ice Pyramid 5F - West Ledge Box', 'object_id': 106, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - South Shelf Box', 'object_id': 107, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - South Leapfrog Box', 'object_id': 108, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - Glass Door Right Box', 'object_id': 109, 'type': 'Box', 'access': ['IcePyramid5FStatue']}, {'name': 'Ice Pyramid 5F - North Box', 'object_id': 110, 'type': 'Box', 'access': []}], 'links': [{'target_room': 64, 'entrance': 134, 'teleporter': [74, 0], 'access': []}, {'target_room': 65, 'access': []}, {'target_room': 53, 'access': ['Bomb', 'Claw', 'Sword']}]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem', 'id': 67, 'game_objects': [{'name': 'Ice Pyramid 5F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid5FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 69, 'entrance': 137, 'teleporter': [76, 0], 'access': []}, {'target_room': 65, 'access': []}, {'target_room': 70, 'entrance': 136, 'teleporter': [75, 0], 'access': []}]}, {'name': 'Ice Pyramid Climbing Wall Room Lower Space', 'id': 68, 'game_objects': [], 'links': [{'target_room': 53, 'entrance': 139, 'teleporter': [78, 0], 'access': []}, {'target_room': 69, 'access': ['Claw']}]}, {'name': 'Ice Pyramid Climbing Wall Room Upper Space', 'id': 69, 'game_objects': [], 'links': [{'target_room': 67, 'entrance': 140, 'teleporter': [79, 0], 'access': []}, {'target_room': 68, 'access': ['Claw']}]}, {'name': 'Ice Pyramid Ice Golem Room', 'id': 70, 'game_objects': [{'name': 'Ice Pyramid 6F - Ice Golem Chest', 'object_id': 14, 'type': 'Chest', 'access': ['IceGolem']}, {'name': 'Ice Golem', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IceGolem'], 'access': []}], 'links': [{'target_room': 67, 'entrance': 141, 'teleporter': [80, 0], 'access': []}, {'target_room': 66, 'access': []}]}, {'name': 'Spencer Waterfall', 'id': 71, 'game_objects': [], 'links': [{'target_room': 72, 'entrance': 143, 'teleporter': [81, 0], 'access': []}, {'target_room': 40, 'entrance': 145, 'teleporter': [82, 0], 'access': []}, {'target_room': 40, 'entrance': 148, 'teleporter': [83, 0], 'access': []}]}, {'name': 'Spencer Cave Normal Main', 'id': 72, 'game_objects': [{'name': "Spencer's Cave - Box", 'object_id': 111, 'type': 'Box', 'access': ['Claw']}, {'name': "Spencer's Cave - Spencer", 'object_id': 8, 'type': 'NPC', 'access': []}, {'name': "Spencer's Cave - Locked Chest", 'object_id': 13, 'type': 'NPC', 'access': ['VenusKey']}], 'links': [{'target_room': 71, 'entrance': 150, 'teleporter': [85, 0], 'access': []}]}, {'name': 'Spencer Cave Normal South Ledge', 'id': 73, 'game_objects': [{'name': "Collapse Spencer's Cave", 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipLiberated'], 'access': ['MegaGrenade']}], 'links': [{'target_room': 227, 'entrance': 151, 'teleporter': [7, 6], 'access': []}, {'target_room': 203, 'access': ['MegaGrenade']}]}, {'name': 'Spencer Cave Caved In Main Loop', 'id': 203, 'game_objects': [], 'links': [{'target_room': 73, 'access': []}, {'target_room': 207, 'entrance': 156, 'teleporter': [36, 8], 'access': ['MobiusCrest']}, {'target_room': 204, 'access': ['Claw']}, {'target_room': 205, 'access': ['Bomb']}]}, {'name': 'Spencer Cave Caved In Waters', 'id': 204, 'game_objects': [{'name': 'Bomb Libra Block', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['SpencerCaveLibraBlockBombed'], 'access': ['MegaGrenade', 'Claw']}], 'links': [{'target_room': 203, 'access': ['Claw']}]}, {'name': 'Spencer Cave Caved In Libra Nook', 'id': 205, 'game_objects': [], 'links': [{'target_room': 206, 'entrance': 153, 'teleporter': [33, 8], 'access': ['LibraCrest']}]}, {'name': 'Spencer Cave Caved In Libra Corridor', 'id': 206, 'game_objects': [], 'links': [{'target_room': 205, 'entrance': 154, 'teleporter': [34, 8], 'access': ['LibraCrest']}, {'target_room': 207, 'access': ['SpencerCaveLibraBlockBombed']}]}, {'name': 'Spencer Cave Caved In Mobius Chest', 'id': 207, 'game_objects': [{'name': "Spencer's Cave - Mobius Chest", 'object_id': 15, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 203, 'entrance': 155, 'teleporter': [35, 8], 'access': ['MobiusCrest']}, {'target_room': 206, 'access': ['Bomb']}]}, {'name': 'Wintry Temple Outer Room', 'id': 74, 'game_objects': [], 'links': [{'target_room': 223, 'entrance': 157, 'teleporter': [15, 6], 'access': []}]}, {'name': 'Wintry Temple Inner Room', 'id': 75, 'game_objects': [{'name': 'Wintry Temple - West Box', 'object_id': 112, 'type': 'Box', 'access': []}, {'name': 'Wintry Temple - North Box', 'object_id': 113, 'type': 'Box', 'access': []}], 'links': [{'target_room': 92, 'entrance': 158, 'teleporter': [62, 8], 'access': ['GeminiCrest']}]}, {'name': 'Fireburg Upper Plaza', 'id': 76, 'game_objects': [], 'links': [{'target_room': 224, 'entrance': 159, 'teleporter': [9, 6], 'access': []}, {'target_room': 80, 'entrance': 163, 'teleporter': [91, 0], 'access': []}, {'target_room': 77, 'entrance': 164, 'teleporter': [98, 8], 'access': []}, {'target_room': 82, 'entrance': 165, 'teleporter': [96, 8], 'access': []}, {'target_room': 208, 'access': ['Claw']}]}, {'name': 'Fireburg Lower Plaza', 'id': 208, 'game_objects': [{'name': 'Fireburg - Hidden Tunnel Box', 'object_id': 116, 'type': 'Box', 'access': []}], 'links': [{'target_room': 76, 'access': ['Claw']}, {'target_room': 78, 'entrance': 166, 'teleporter': [11, 8], 'access': ['MultiKey']}]}, {'name': "Reuben's House", 'id': 77, 'game_objects': [{'name': "Fireburg - Reuben's House Arion", 'object_id': 14, 'type': 'NPC', 'access': ['ReubenDadSaved']}, {'name': 'Reuben Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Reuben1'], 'access': []}, {'name': "Fireburg - Reuben's House Box", 'object_id': 117, 'type': 'Box', 'access': []}], 'links': [{'target_room': 76, 'entrance': 167, 'teleporter': [98, 3], 'access': []}]}, {'name': "GrenadeMan's House", 'id': 78, 'game_objects': [{'name': 'Fireburg - Locked House Man', 'object_id': 12, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 208, 'entrance': 168, 'teleporter': [9, 8], 'access': ['MultiKey']}, {'target_room': 79, 'entrance': 169, 'teleporter': [93, 0], 'access': []}]}, {'name': "GrenadeMan's Mobius Room", 'id': 79, 'game_objects': [], 'links': [{'target_room': 78, 'entrance': 170, 'teleporter': [94, 0], 'access': []}, {'target_room': 161, 'entrance': 171, 'teleporter': [54, 8], 'access': ['MobiusCrest']}]}, {'name': 'Fireburg Vendor House', 'id': 80, 'game_objects': [{'name': 'Fireburg - Vendor', 'object_id': 11, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 76, 'entrance': 172, 'teleporter': [95, 0], 'access': []}, {'target_room': 81, 'entrance': 173, 'teleporter': [96, 0], 'access': []}]}, {'name': 'Fireburg Gemini Room', 'id': 81, 'game_objects': [], 'links': [{'target_room': 80, 'entrance': 174, 'teleporter': [97, 0], 'access': []}, {'target_room': 43, 'entrance': 175, 'teleporter': [45, 8], 'access': ['GeminiCrest']}]}, {'name': 'Fireburg Hotel Lobby', 'id': 82, 'game_objects': [{'name': 'Fireburg - Tristam', 'object_id': 10, 'type': 'NPC', 'access': ['Tristam', 'TristamBoneItemGiven']}], 'links': [{'target_room': 76, 'entrance': 177, 'teleporter': [99, 3], 'access': []}, {'target_room': 83, 'entrance': 176, 'teleporter': [213, 0], 'access': []}]}, {'name': 'Fireburg Hotel Beds', 'id': 83, 'game_objects': [], 'links': [{'target_room': 82, 'entrance': 178, 'teleporter': [214, 0], 'access': []}]}, {'name': 'Mine Exterior North West Platforms', 'id': 84, 'game_objects': [], 'links': [{'target_room': 224, 'entrance': 179, 'teleporter': [98, 0], 'access': []}, {'target_room': 88, 'entrance': 181, 'teleporter': [20, 2], 'access': ['Bomb']}, {'target_room': 85, 'access': ['Claw']}, {'target_room': 86, 'access': ['Claw']}, {'target_room': 87, 'access': ['Claw']}]}, {'name': 'Mine Exterior Central Ledge', 'id': 85, 'game_objects': [], 'links': [{'target_room': 90, 'entrance': 183, 'teleporter': [22, 2], 'access': ['Bomb']}, {'target_room': 84, 'access': ['Claw']}]}, {'name': 'Mine Exterior North Ledge', 'id': 86, 'game_objects': [], 'links': [{'target_room': 89, 'entrance': 182, 'teleporter': [21, 2], 'access': ['Bomb']}, {'target_room': 85, 'access': ['Claw']}]}, {'name': 'Mine Exterior South East Platforms', 'id': 87, 'game_objects': [{'name': 'Jinn', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Jinn'], 'access': []}], 'links': [{'target_room': 91, 'entrance': 180, 'teleporter': [99, 0], 'access': ['Jinn']}, {'target_room': 86, 'access': []}, {'target_room': 85, 'access': ['Claw']}]}, {'name': 'Mine Parallel Room', 'id': 88, 'game_objects': [{'name': 'Mine - Parallel Room West Box', 'object_id': 119, 'type': 'Box', 'access': ['Claw']}, {'name': 'Mine - Parallel Room East Box', 'object_id': 120, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 84, 'entrance': 185, 'teleporter': [100, 3], 'access': []}]}, {'name': 'Mine Crescent Room', 'id': 89, 'game_objects': [{'name': 'Mine - Crescent Room Chest', 'object_id': 16, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 86, 'entrance': 186, 'teleporter': [101, 3], 'access': []}]}, {'name': 'Mine Climbing Room', 'id': 90, 'game_objects': [{'name': 'Mine - Glitchy Collision Cave Box', 'object_id': 118, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 85, 'entrance': 187, 'teleporter': [102, 3], 'access': []}]}, {'name': 'Mine Cliff', 'id': 91, 'game_objects': [{'name': 'Mine - Cliff Southwest Box', 'object_id': 121, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Northwest Box', 'object_id': 122, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Northeast Box', 'object_id': 123, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Southeast Box', 'object_id': 124, 'type': 'Box', 'access': []}, {'name': 'Mine - Reuben', 'object_id': 7, 'type': 'NPC', 'access': ['Reuben1']}, {'name': "Reuben's dad Saved", 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ReubenDadSaved'], 'access': ['MegaGrenade']}], 'links': [{'target_room': 87, 'entrance': 188, 'teleporter': [100, 0], 'access': []}]}, {'name': 'Sealed Temple', 'id': 92, 'game_objects': [{'name': 'Sealed Temple - West Box', 'object_id': 125, 'type': 'Box', 'access': []}, {'name': 'Sealed Temple - East Box', 'object_id': 126, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 190, 'teleporter': [16, 6], 'access': []}, {'target_room': 75, 'entrance': 191, 'teleporter': [63, 8], 'access': ['GeminiCrest']}]}, {'name': 'Volcano Base', 'id': 93, 'game_objects': [{'name': 'Volcano - Base Chest', 'object_id': 17, 'type': 'Chest', 'access': []}, {'name': 'Volcano - Base West Box', 'object_id': 127, 'type': 'Box', 'access': []}, {'name': 'Volcano - Base East Left Box', 'object_id': 128, 'type': 'Box', 'access': []}, {'name': 'Volcano - Base East Right Box', 'object_id': 129, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 192, 'teleporter': [103, 0], 'access': []}, {'target_room': 98, 'entrance': 196, 'teleporter': [31, 8], 'access': []}, {'target_room': 96, 'entrance': 197, 'teleporter': [30, 8], 'access': []}]}, {'name': 'Volcano Top Left', 'id': 94, 'game_objects': [{'name': 'Volcano - Medusa Chest', 'object_id': 18, 'type': 'Chest', 'access': ['Medusa']}, {'name': 'Medusa', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Medusa'], 'access': []}, {'name': 'Volcano - Behind Medusa Box', 'object_id': 130, 'type': 'Box', 'access': []}], 'links': [{'target_room': 209, 'entrance': 199, 'teleporter': [26, 8], 'access': []}]}, {'name': 'Volcano Top Right', 'id': 95, 'game_objects': [{'name': 'Volcano - Top of the Volcano Left Box', 'object_id': 131, 'type': 'Box', 'access': []}, {'name': 'Volcano - Top of the Volcano Right Box', 'object_id': 132, 'type': 'Box', 'access': []}], 'links': [{'target_room': 99, 'entrance': 200, 'teleporter': [79, 8], 'access': []}]}, {'name': 'Volcano Right Path', 'id': 96, 'game_objects': [{'name': 'Volcano - Right Path Box', 'object_id': 135, 'type': 'Box', 'access': []}], 'links': [{'target_room': 93, 'entrance': 201, 'teleporter': [15, 8], 'access': []}]}, {'name': 'Volcano Left Path', 'id': 98, 'game_objects': [{'name': 'Volcano - Left Path Box', 'object_id': 134, 'type': 'Box', 'access': []}], 'links': [{'target_room': 93, 'entrance': 204, 'teleporter': [27, 8], 'access': []}, {'target_room': 99, 'entrance': 202, 'teleporter': [25, 2], 'access': []}, {'target_room': 209, 'entrance': 203, 'teleporter': [26, 2], 'access': []}]}, {'name': 'Volcano Cross Left-Right', 'id': 99, 'game_objects': [], 'links': [{'target_room': 95, 'entrance': 206, 'teleporter': [29, 8], 'access': []}, {'target_room': 98, 'entrance': 205, 'teleporter': [103, 3], 'access': []}]}, {'name': 'Volcano Cross Right-Left', 'id': 209, 'game_objects': [{'name': 'Volcano - Crossover Section Box', 'object_id': 133, 'type': 'Box', 'access': []}], 'links': [{'target_room': 98, 'entrance': 208, 'teleporter': [104, 3], 'access': []}, {'target_room': 94, 'entrance': 207, 'teleporter': [28, 8], 'access': []}]}, {'name': 'Lava Dome Inner Ring Main Loop', 'id': 100, 'game_objects': [{'name': 'Lava Dome - Exterior Caldera Near Switch Cliff Box', 'object_id': 136, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Exterior South Cliff Box', 'object_id': 137, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 209, 'teleporter': [104, 0], 'access': []}, {'target_room': 113, 'entrance': 211, 'teleporter': [105, 0], 'access': []}, {'target_room': 114, 'entrance': 212, 'teleporter': [106, 0], 'access': []}, {'target_room': 116, 'entrance': 213, 'teleporter': [108, 0], 'access': []}, {'target_room': 118, 'entrance': 214, 'teleporter': [111, 0], 'access': []}]}, {'name': 'Lava Dome Inner Ring Center Ledge', 'id': 101, 'game_objects': [{'name': 'Lava Dome - Exterior Center Dropoff Ledge Box', 'object_id': 138, 'type': 'Box', 'access': []}], 'links': [{'target_room': 115, 'entrance': 215, 'teleporter': [107, 0], 'access': []}, {'target_room': 100, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Plate Ledge', 'id': 102, 'game_objects': [{'name': 'Lava Dome Plate', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['LavaDomePlate'], 'access': []}], 'links': [{'target_room': 119, 'entrance': 216, 'teleporter': [109, 0], 'access': []}]}, {'name': 'Lava Dome Inner Ring Upper Ledge West', 'id': 103, 'game_objects': [], 'links': [{'target_room': 111, 'entrance': 219, 'teleporter': [112, 0], 'access': []}, {'target_room': 108, 'entrance': 220, 'teleporter': [113, 0], 'access': []}, {'target_room': 104, 'access': ['Claw']}, {'target_room': 100, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Upper Ledge East', 'id': 104, 'game_objects': [], 'links': [{'target_room': 110, 'entrance': 218, 'teleporter': [110, 0], 'access': []}, {'target_room': 103, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Big Door Ledge', 'id': 105, 'game_objects': [], 'links': [{'target_room': 107, 'entrance': 221, 'teleporter': [114, 0], 'access': []}, {'target_room': 121, 'entrance': 222, 'teleporter': [29, 2], 'access': ['LavaDomePlate']}]}, {'name': 'Lava Dome Inner Ring Tiny Bottom Ledge', 'id': 106, 'game_objects': [{'name': 'Lava Dome - Exterior Dead End Caldera Box', 'object_id': 139, 'type': 'Box', 'access': []}], 'links': [{'target_room': 120, 'entrance': 226, 'teleporter': [115, 0], 'access': []}]}, {'name': 'Lava Dome Jump Maze II', 'id': 107, 'game_objects': [{'name': 'Lava Dome - Gold Maze Northwest Box', 'object_id': 140, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Southwest Box', 'object_id': 246, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Northeast Box', 'object_id': 247, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze North Box', 'object_id': 248, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Center Box', 'object_id': 249, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Southeast Box', 'object_id': 250, 'type': 'Box', 'access': []}], 'links': [{'target_room': 105, 'entrance': 227, 'teleporter': [116, 0], 'access': []}, {'target_room': 108, 'entrance': 228, 'teleporter': [119, 0], 'access': []}, {'target_room': 120, 'entrance': 229, 'teleporter': [120, 0], 'access': []}]}, {'name': 'Lava Dome Up-Down Corridor', 'id': 108, 'game_objects': [], 'links': [{'target_room': 107, 'entrance': 231, 'teleporter': [118, 0], 'access': []}, {'target_room': 103, 'entrance': 230, 'teleporter': [117, 0], 'access': []}]}, {'name': 'Lava Dome Jump Maze I', 'id': 109, 'game_objects': [{'name': 'Lava Dome - Bare Maze Leapfrog Alcove North Box', 'object_id': 141, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Leapfrog Alcove South Box', 'object_id': 142, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Center Box', 'object_id': 143, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Southwest Box', 'object_id': 144, 'type': 'Box', 'access': []}], 'links': [{'target_room': 118, 'entrance': 232, 'teleporter': [121, 0], 'access': []}, {'target_room': 111, 'entrance': 233, 'teleporter': [122, 0], 'access': []}]}, {'name': 'Lava Dome Pointless Room', 'id': 110, 'game_objects': [], 'links': [{'target_room': 104, 'entrance': 234, 'teleporter': [123, 0], 'access': []}]}, {'name': 'Lava Dome Lower Moon Helm Room', 'id': 111, 'game_objects': [{'name': 'Lava Dome - U-Bend Room North Box', 'object_id': 146, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - U-Bend Room South Box', 'object_id': 147, 'type': 'Box', 'access': []}], 'links': [{'target_room': 103, 'entrance': 235, 'teleporter': [124, 0], 'access': []}, {'target_room': 109, 'entrance': 236, 'teleporter': [125, 0], 'access': []}]}, {'name': 'Lava Dome Moon Helm Room', 'id': 112, 'game_objects': [{'name': 'Lava Dome - Beyond River Room Chest', 'object_id': 19, 'type': 'Chest', 'access': []}, {'name': 'Lava Dome - Beyond River Room Box', 'object_id': 145, 'type': 'Box', 'access': []}], 'links': [{'target_room': 117, 'entrance': 237, 'teleporter': [126, 0], 'access': []}]}, {'name': 'Lava Dome Three Jumps Room', 'id': 113, 'game_objects': [{'name': 'Lava Dome - Three Jumps Room Box', 'object_id': 150, 'type': 'Box', 'access': []}], 'links': [{'target_room': 100, 'entrance': 238, 'teleporter': [127, 0], 'access': []}]}, {'name': 'Lava Dome Life Chest Room Lower Ledge', 'id': 114, 'game_objects': [{'name': 'Lava Dome - Gold Bar Room Boulder Chest', 'object_id': 28, 'type': 'Chest', 'access': ['MegaGrenade']}], 'links': [{'target_room': 100, 'entrance': 239, 'teleporter': [128, 0], 'access': []}, {'target_room': 115, 'access': ['Claw']}]}, {'name': 'Lava Dome Life Chest Room Upper Ledge', 'id': 115, 'game_objects': [{'name': 'Lava Dome - Gold Bar Room Leapfrog Alcove Box West', 'object_id': 148, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Bar Room Leapfrog Alcove Box East', 'object_id': 149, 'type': 'Box', 'access': []}], 'links': [{'target_room': 101, 'entrance': 240, 'teleporter': [129, 0], 'access': []}, {'target_room': 114, 'access': ['Claw']}]}, {'name': 'Lava Dome Big Jump Room Main Area', 'id': 116, 'game_objects': [{'name': 'Lava Dome - Lava River Room North Box', 'object_id': 152, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Lava River Room East Box', 'object_id': 153, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Lava River Room South Box', 'object_id': 154, 'type': 'Box', 'access': []}], 'links': [{'target_room': 100, 'entrance': 241, 'teleporter': [133, 0], 'access': []}, {'target_room': 119, 'entrance': 243, 'teleporter': [132, 0], 'access': []}, {'target_room': 117, 'access': ['MegaGrenade']}]}, {'name': 'Lava Dome Big Jump Room MegaGrenade Area', 'id': 117, 'game_objects': [], 'links': [{'target_room': 112, 'entrance': 242, 'teleporter': [131, 0], 'access': []}, {'target_room': 116, 'access': ['Bomb']}]}, {'name': 'Lava Dome Split Corridor', 'id': 118, 'game_objects': [{'name': 'Lava Dome - Split Corridor Box', 'object_id': 151, 'type': 'Box', 'access': []}], 'links': [{'target_room': 109, 'entrance': 244, 'teleporter': [130, 0], 'access': []}, {'target_room': 100, 'entrance': 245, 'teleporter': [134, 0], 'access': []}]}, {'name': 'Lava Dome Plate Corridor', 'id': 119, 'game_objects': [], 'links': [{'target_room': 102, 'entrance': 246, 'teleporter': [135, 0], 'access': []}, {'target_room': 116, 'entrance': 247, 'teleporter': [137, 0], 'access': []}]}, {'name': 'Lava Dome Four Boxes Stairs', 'id': 120, 'game_objects': [{'name': 'Lava Dome - Caldera Stairway West Left Box', 'object_id': 155, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway West Right Box', 'object_id': 156, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway East Left Box', 'object_id': 157, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway East Right Box', 'object_id': 158, 'type': 'Box', 'access': []}], 'links': [{'target_room': 107, 'entrance': 248, 'teleporter': [136, 0], 'access': []}, {'target_room': 106, 'entrance': 249, 'teleporter': [16, 0], 'access': []}]}, {'name': 'Lava Dome Hydra Room', 'id': 121, 'game_objects': [{'name': 'Lava Dome - Dualhead Hydra Chest', 'object_id': 20, 'type': 'Chest', 'access': ['DualheadHydra']}, {'name': 'Dualhead Hydra', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['DualheadHydra'], 'access': []}, {'name': 'Lava Dome - Hydra Room Northwest Box', 'object_id': 159, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Hydra Room Southweast Box', 'object_id': 160, 'type': 'Box', 'access': []}], 'links': [{'target_room': 105, 'entrance': 250, 'teleporter': [105, 3], 'access': []}, {'target_room': 122, 'entrance': 251, 'teleporter': [138, 0], 'access': ['DualheadHydra']}]}, {'name': 'Lava Dome Escape Corridor', 'id': 122, 'game_objects': [], 'links': [{'target_room': 121, 'entrance': 253, 'teleporter': [139, 0], 'access': []}]}, {'name': 'Rope Bridge', 'id': 123, 'game_objects': [{'name': 'Rope Bridge - West Box', 'object_id': 163, 'type': 'Box', 'access': []}, {'name': 'Rope Bridge - East Box', 'object_id': 164, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 255, 'teleporter': [140, 0], 'access': []}]}, {'name': 'Alive Forest', 'id': 124, 'game_objects': [{'name': 'Alive Forest - Tree Stump Chest', 'object_id': 21, 'type': 'Chest', 'access': ['Axe']}, {'name': 'Alive Forest - Near Entrance Box', 'object_id': 165, 'type': 'Box', 'access': ['Axe']}, {'name': 'Alive Forest - After Bridge Box', 'object_id': 166, 'type': 'Box', 'access': ['Axe']}, {'name': 'Alive Forest - Gemini Stump Box', 'object_id': 167, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 226, 'entrance': 272, 'teleporter': [142, 0], 'access': ['Axe']}, {'target_room': 21, 'entrance': 275, 'teleporter': [64, 8], 'access': ['LibraCrest', 'Axe']}, {'target_room': 22, 'entrance': 276, 'teleporter': [65, 8], 'access': ['GeminiCrest', 'Axe']}, {'target_room': 23, 'entrance': 277, 'teleporter': [66, 8], 'access': ['MobiusCrest', 'Axe']}, {'target_room': 125, 'entrance': 274, 'teleporter': [143, 0], 'access': ['Axe']}]}, {'name': 'Giant Tree 1F Main Area', 'id': 125, 'game_objects': [{'name': 'Giant Tree 1F - Northwest Box', 'object_id': 168, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - Southwest Box', 'object_id': 169, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - Center Box', 'object_id': 170, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - East Box', 'object_id': 171, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 278, 'teleporter': [56, 1], 'access': []}, {'target_room': 202, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 1F North Island', 'id': 202, 'game_objects': [], 'links': [{'target_room': 127, 'entrance': 280, 'teleporter': [144, 0], 'access': []}, {'target_room': 125, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 1F Central Island', 'id': 126, 'game_objects': [], 'links': [{'target_room': 202, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Main Lobby', 'id': 127, 'game_objects': [{'name': 'Giant Tree 2F - North Box', 'object_id': 172, 'type': 'Box', 'access': []}], 'links': [{'target_room': 126, 'access': ['DragonClaw']}, {'target_room': 125, 'entrance': 281, 'teleporter': [145, 0], 'access': []}, {'target_room': 133, 'entrance': 283, 'teleporter': [149, 0], 'access': []}, {'target_room': 129, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F West Ledge', 'id': 128, 'game_objects': [{'name': 'Giant Tree 2F - Dropdown Ledge Box', 'object_id': 174, 'type': 'Box', 'access': []}], 'links': [{'target_room': 140, 'entrance': 284, 'teleporter': [147, 0], 'access': ['Sword']}, {'target_room': 130, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Lower Area', 'id': 129, 'game_objects': [{'name': 'Giant Tree 2F - South Box', 'object_id': 173, 'type': 'Box', 'access': []}], 'links': [{'target_room': 130, 'access': ['Claw']}, {'target_room': 131, 'access': ['Claw']}]}, {'name': 'Giant Tree 2F Central Island', 'id': 130, 'game_objects': [], 'links': [{'target_room': 129, 'access': ['Claw']}, {'target_room': 135, 'entrance': 282, 'teleporter': [146, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 2F East Ledge', 'id': 131, 'game_objects': [], 'links': [{'target_room': 129, 'access': ['Claw']}, {'target_room': 130, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Meteor Chest Room', 'id': 132, 'game_objects': [{'name': 'Giant Tree 2F - Gidrah Chest', 'object_id': 22, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 133, 'entrance': 285, 'teleporter': [148, 0], 'access': []}]}, {'name': 'Giant Tree 2F Mushroom Room', 'id': 133, 'game_objects': [{'name': 'Giant Tree 2F - Mushroom Tunnel West Box', 'object_id': 175, 'type': 'Box', 'access': ['Axe']}, {'name': 'Giant Tree 2F - Mushroom Tunnel East Box', 'object_id': 176, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 127, 'entrance': 286, 'teleporter': [150, 0], 'access': ['Axe']}, {'target_room': 132, 'entrance': 287, 'teleporter': [151, 0], 'access': ['Axe', 'Gidrah']}]}, {'name': 'Giant Tree 3F Central Island', 'id': 135, 'game_objects': [{'name': 'Giant Tree 3F - Central Island Box', 'object_id': 179, 'type': 'Box', 'access': []}], 'links': [{'target_room': 130, 'entrance': 288, 'teleporter': [152, 0], 'access': []}, {'target_room': 136, 'access': ['Claw']}, {'target_room': 137, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 3F Central Area', 'id': 136, 'game_objects': [{'name': 'Giant Tree 3F - Center North Box', 'object_id': 177, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 3F - Center West Box', 'object_id': 178, 'type': 'Box', 'access': []}], 'links': [{'target_room': 135, 'access': ['Claw']}, {'target_room': 127, 'access': []}, {'target_room': 131, 'access': []}]}, {'name': 'Giant Tree 3F Lower Ledge', 'id': 137, 'game_objects': [], 'links': [{'target_room': 135, 'access': ['DragonClaw']}, {'target_room': 142, 'entrance': 289, 'teleporter': [153, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 3F West Area', 'id': 138, 'game_objects': [{'name': 'Giant Tree 3F - West Side Box', 'object_id': 180, 'type': 'Box', 'access': []}], 'links': [{'target_room': 128, 'access': []}, {'target_room': 210, 'entrance': 290, 'teleporter': [154, 0], 'access': []}]}, {'name': 'Giant Tree 3F Middle Up Island', 'id': 139, 'game_objects': [], 'links': [{'target_room': 136, 'access': ['Claw']}]}, {'name': 'Giant Tree 3F West Platform', 'id': 140, 'game_objects': [], 'links': [{'target_room': 139, 'access': ['Claw']}, {'target_room': 141, 'access': ['Claw']}, {'target_room': 128, 'entrance': 291, 'teleporter': [155, 0], 'access': []}]}, {'name': 'Giant Tree 3F North Ledge', 'id': 141, 'game_objects': [], 'links': [{'target_room': 143, 'entrance': 292, 'teleporter': [156, 0], 'access': ['Sword']}, {'target_room': 139, 'access': ['Claw']}, {'target_room': 136, 'access': ['Claw']}]}, {'name': 'Giant Tree Worm Room Upper Ledge', 'id': 142, 'game_objects': [{'name': 'Giant Tree 3F - Worm Room North Box', 'object_id': 181, 'type': 'Box', 'access': ['Axe']}, {'name': 'Giant Tree 3F - Worm Room South Box', 'object_id': 182, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 137, 'entrance': 293, 'teleporter': [157, 0], 'access': ['Axe']}, {'target_room': 210, 'access': ['Axe', 'Claw']}]}, {'name': 'Giant Tree Worm Room Lower Ledge', 'id': 210, 'game_objects': [], 'links': [{'target_room': 138, 'entrance': 294, 'teleporter': [158, 0], 'access': []}]}, {'name': 'Giant Tree 4F Lower Floor', 'id': 143, 'game_objects': [], 'links': [{'target_room': 141, 'entrance': 295, 'teleporter': [159, 0], 'access': []}, {'target_room': 148, 'entrance': 296, 'teleporter': [160, 0], 'access': []}, {'target_room': 148, 'entrance': 297, 'teleporter': [161, 0], 'access': []}, {'target_room': 147, 'entrance': 298, 'teleporter': [162, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 4F Middle Floor', 'id': 144, 'game_objects': [{'name': 'Giant Tree 4F - Highest Platform North Box', 'object_id': 183, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 4F - Highest Platform South Box', 'object_id': 184, 'type': 'Box', 'access': []}], 'links': [{'target_room': 149, 'entrance': 299, 'teleporter': [163, 0], 'access': []}, {'target_room': 145, 'access': ['Claw']}, {'target_room': 146, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 4F Upper Floor', 'id': 145, 'game_objects': [], 'links': [{'target_room': 150, 'entrance': 300, 'teleporter': [164, 0], 'access': ['Sword']}, {'target_room': 144, 'access': ['Claw']}]}, {'name': 'Giant Tree 4F South Ledge', 'id': 146, 'game_objects': [{'name': 'Giant Tree 4F - Hook Ledge Northeast Box', 'object_id': 185, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 4F - Hook Ledge Southwest Box', 'object_id': 186, 'type': 'Box', 'access': []}], 'links': [{'target_room': 144, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 4F Slime Room East Area', 'id': 147, 'game_objects': [{'name': 'Giant Tree 4F - East Slime Room Box', 'object_id': 188, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 143, 'entrance': 304, 'teleporter': [168, 0], 'access': []}]}, {'name': 'Giant Tree 4F Slime Room West Area', 'id': 148, 'game_objects': [], 'links': [{'target_room': 143, 'entrance': 303, 'teleporter': [167, 0], 'access': ['Axe']}, {'target_room': 143, 'entrance': 302, 'teleporter': [166, 0], 'access': ['Axe']}, {'target_room': 149, 'access': ['Axe', 'Claw']}]}, {'name': 'Giant Tree 4F Slime Room Platform', 'id': 149, 'game_objects': [{'name': 'Giant Tree 4F - West Slime Room Box', 'object_id': 187, 'type': 'Box', 'access': []}], 'links': [{'target_room': 144, 'entrance': 301, 'teleporter': [165, 0], 'access': []}, {'target_room': 148, 'access': ['Claw']}]}, {'name': 'Giant Tree 5F Lower Area', 'id': 150, 'game_objects': [{'name': 'Giant Tree 5F - Northwest Left Box', 'object_id': 189, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - Northwest Right Box', 'object_id': 190, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - South Left Box', 'object_id': 191, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - South Right Box', 'object_id': 192, 'type': 'Box', 'access': []}], 'links': [{'target_room': 145, 'entrance': 305, 'teleporter': [169, 0], 'access': []}, {'target_room': 151, 'access': ['Claw']}, {'target_room': 143, 'access': []}]}, {'name': 'Giant Tree 5F Gidrah Platform', 'id': 151, 'game_objects': [{'name': 'Gidrah', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Gidrah'], 'access': []}], 'links': [{'target_room': 150, 'access': ['Claw']}]}, {'name': 'Kaidge Temple Lower Ledge', 'id': 152, 'game_objects': [], 'links': [{'target_room': 226, 'entrance': 307, 'teleporter': [18, 6], 'access': []}, {'target_room': 153, 'access': ['Claw']}]}, {'name': 'Kaidge Temple Upper Ledge', 'id': 153, 'game_objects': [{'name': 'Kaidge Temple - Box', 'object_id': 193, 'type': 'Box', 'access': []}], 'links': [{'target_room': 185, 'entrance': 308, 'teleporter': [71, 8], 'access': ['MobiusCrest']}, {'target_room': 152, 'access': ['Claw']}]}, {'name': 'Windhole Temple', 'id': 154, 'game_objects': [{'name': 'Windhole Temple - Box', 'object_id': 194, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 309, 'teleporter': [173, 0], 'access': []}]}, {'name': 'Mount Gale', 'id': 155, 'game_objects': [{'name': 'Mount Gale - Dullahan Chest', 'object_id': 23, 'type': 'Chest', 'access': ['DragonClaw', 'Dullahan']}, {'name': 'Dullahan', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Dullahan'], 'access': ['DragonClaw']}, {'name': 'Mount Gale - East Box', 'object_id': 195, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Mount Gale - West Box', 'object_id': 196, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 310, 'teleporter': [174, 0], 'access': []}]}, {'name': 'Windia', 'id': 156, 'game_objects': [], 'links': [{'target_room': 226, 'entrance': 312, 'teleporter': [10, 6], 'access': []}, {'target_room': 157, 'entrance': 320, 'teleporter': [30, 5], 'access': []}, {'target_room': 163, 'entrance': 321, 'teleporter': [97, 8], 'access': []}, {'target_room': 165, 'entrance': 322, 'teleporter': [32, 5], 'access': []}, {'target_room': 159, 'entrance': 323, 'teleporter': [176, 4], 'access': []}, {'target_room': 160, 'entrance': 324, 'teleporter': [177, 4], 'access': []}]}, {'name': "Otto's House", 'id': 157, 'game_objects': [{'name': 'Otto', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['RainbowBridge'], 'access': ['ThunderRock']}], 'links': [{'target_room': 156, 'entrance': 327, 'teleporter': [106, 3], 'access': []}, {'target_room': 158, 'entrance': 326, 'teleporter': [33, 2], 'access': []}]}, {'name': "Otto's Attic", 'id': 158, 'game_objects': [{'name': "Windia - Otto's Attic Box", 'object_id': 197, 'type': 'Box', 'access': []}], 'links': [{'target_room': 157, 'entrance': 328, 'teleporter': [107, 3], 'access': []}]}, {'name': 'Windia Kid House', 'id': 159, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 329, 'teleporter': [178, 0], 'access': []}, {'target_room': 161, 'entrance': 330, 'teleporter': [180, 0], 'access': []}]}, {'name': 'Windia Old People House', 'id': 160, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 331, 'teleporter': [179, 0], 'access': []}, {'target_room': 162, 'entrance': 332, 'teleporter': [181, 0], 'access': []}]}, {'name': 'Windia Kid House Basement', 'id': 161, 'game_objects': [], 'links': [{'target_room': 159, 'entrance': 333, 'teleporter': [182, 0], 'access': []}, {'target_room': 79, 'entrance': 334, 'teleporter': [44, 8], 'access': ['MobiusCrest']}]}, {'name': 'Windia Old People House Basement', 'id': 162, 'game_objects': [{'name': 'Windia - Mobius Basement West Box', 'object_id': 200, 'type': 'Box', 'access': []}, {'name': 'Windia - Mobius Basement East Box', 'object_id': 201, 'type': 'Box', 'access': []}], 'links': [{'target_room': 160, 'entrance': 335, 'teleporter': [183, 0], 'access': []}, {'target_room': 186, 'entrance': 336, 'teleporter': [43, 8], 'access': ['MobiusCrest']}]}, {'name': 'Windia Inn Lobby', 'id': 163, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 338, 'teleporter': [135, 3], 'access': []}, {'target_room': 164, 'entrance': 337, 'teleporter': [102, 8], 'access': []}]}, {'name': 'Windia Inn Beds', 'id': 164, 'game_objects': [{'name': 'Windia - Inn Bedroom North Box', 'object_id': 198, 'type': 'Box', 'access': []}, {'name': 'Windia - Inn Bedroom South Box', 'object_id': 199, 'type': 'Box', 'access': []}, {'name': 'Windia - Kaeli', 'object_id': 15, 'type': 'NPC', 'access': ['Kaeli2']}], 'links': [{'target_room': 163, 'entrance': 339, 'teleporter': [216, 0], 'access': []}]}, {'name': 'Windia Vendor House', 'id': 165, 'game_objects': [{'name': 'Windia - Vendor', 'object_id': 16, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 156, 'entrance': 340, 'teleporter': [108, 3], 'access': []}]}, {'name': 'Pazuzu Tower 1F Main Lobby', 'id': 166, 'game_objects': [{'name': 'Pazuzu 1F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu1F'], 'access': []}], 'links': [{'target_room': 226, 'entrance': 341, 'teleporter': [184, 0], 'access': []}, {'target_room': 180, 'entrance': 345, 'teleporter': [185, 0], 'access': []}]}, {'name': 'Pazuzu Tower 1F Boxes Room', 'id': 167, 'game_objects': [{'name': "Pazuzu's Tower 1F - Descent Bomb Wall West Box", 'object_id': 202, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Bomb Wall Center Box", 'object_id': 203, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Bomb Wall East Box", 'object_id': 204, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Box", 'object_id': 205, 'type': 'Box', 'access': []}], 'links': [{'target_room': 169, 'entrance': 349, 'teleporter': [187, 0], 'access': []}]}, {'name': 'Pazuzu Tower 1F Southern Platform', 'id': 168, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 346, 'teleporter': [186, 0], 'access': []}, {'target_room': 166, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 2F', 'id': 169, 'game_objects': [{'name': "Pazuzu's Tower 2F - East Room West Box", 'object_id': 206, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 2F - East Room East Box", 'object_id': 207, 'type': 'Box', 'access': []}, {'name': 'Pazuzu 2F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu2FLock'], 'access': ['Axe']}, {'name': 'Pazuzu 2F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu2F'], 'access': ['Bomb']}], 'links': [{'target_room': 183, 'entrance': 350, 'teleporter': [188, 0], 'access': []}, {'target_room': 168, 'entrance': 351, 'teleporter': [189, 0], 'access': []}, {'target_room': 167, 'entrance': 352, 'teleporter': [190, 0], 'access': []}, {'target_room': 171, 'entrance': 353, 'teleporter': [191, 0], 'access': []}]}, {'name': 'Pazuzu 3F Main Room', 'id': 170, 'game_objects': [{'name': "Pazuzu's Tower 3F - Guest Room West Box", 'object_id': 208, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 3F - Guest Room East Box", 'object_id': 209, 'type': 'Box', 'access': []}, {'name': 'Pazuzu 3F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu3F'], 'access': []}], 'links': [{'target_room': 180, 'entrance': 356, 'teleporter': [192, 0], 'access': []}, {'target_room': 181, 'entrance': 357, 'teleporter': [193, 0], 'access': []}]}, {'name': 'Pazuzu 3F Central Island', 'id': 171, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 360, 'teleporter': [194, 0], 'access': []}, {'target_room': 170, 'access': ['DragonClaw']}, {'target_room': 172, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 3F Southern Island', 'id': 172, 'game_objects': [{'name': "Pazuzu's Tower 3F - South Ledge Box", 'object_id': 210, 'type': 'Box', 'access': []}], 'links': [{'target_room': 173, 'entrance': 361, 'teleporter': [195, 0], 'access': []}, {'target_room': 171, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 4F', 'id': 173, 'game_objects': [{'name': "Pazuzu's Tower 4F - Elevator West Box", 'object_id': 211, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 4F - Elevator East Box", 'object_id': 212, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 4F - East Storage Room Chest", 'object_id': 24, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu 4F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu4FLock'], 'access': ['Axe']}, {'name': 'Pazuzu 4F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu4F'], 'access': ['Bomb']}], 'links': [{'target_room': 183, 'entrance': 362, 'teleporter': [196, 0], 'access': []}, {'target_room': 184, 'entrance': 363, 'teleporter': [197, 0], 'access': []}, {'target_room': 172, 'entrance': 364, 'teleporter': [198, 0], 'access': []}, {'target_room': 175, 'entrance': 365, 'teleporter': [199, 0], 'access': []}]}, {'name': 'Pazuzu 5F Pazuzu Loop', 'id': 174, 'game_objects': [{'name': 'Pazuzu 5F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu5F'], 'access': []}], 'links': [{'target_room': 181, 'entrance': 368, 'teleporter': [200, 0], 'access': []}, {'target_room': 182, 'entrance': 369, 'teleporter': [201, 0], 'access': []}]}, {'name': 'Pazuzu 5F Upper Loop', 'id': 175, 'game_objects': [{'name': "Pazuzu's Tower 5F - North Box", 'object_id': 213, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 5F - South Box", 'object_id': 214, 'type': 'Box', 'access': []}], 'links': [{'target_room': 173, 'entrance': 370, 'teleporter': [202, 0], 'access': []}, {'target_room': 176, 'entrance': 371, 'teleporter': [203, 0], 'access': []}]}, {'name': 'Pazuzu 6F', 'id': 176, 'game_objects': [{'name': "Pazuzu's Tower 6F - Box", 'object_id': 215, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 6F - Chest", 'object_id': 25, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu 6F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu6FLock'], 'access': ['Bomb', 'Axe']}, {'name': 'Pazuzu 6F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu6F'], 'access': ['Bomb']}], 'links': [{'target_room': 184, 'entrance': 374, 'teleporter': [204, 0], 'access': []}, {'target_room': 175, 'entrance': 375, 'teleporter': [205, 0], 'access': []}, {'target_room': 178, 'entrance': 376, 'teleporter': [206, 0], 'access': []}, {'target_room': 178, 'entrance': 377, 'teleporter': [207, 0], 'access': []}]}, {'name': 'Pazuzu 7F Southwest Area', 'id': 177, 'game_objects': [], 'links': [{'target_room': 182, 'entrance': 380, 'teleporter': [26, 0], 'access': []}, {'target_room': 178, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 7F Rest of the Area', 'id': 178, 'game_objects': [], 'links': [{'target_room': 177, 'access': ['DragonClaw']}, {'target_room': 176, 'entrance': 381, 'teleporter': [27, 0], 'access': []}, {'target_room': 176, 'entrance': 382, 'teleporter': [28, 0], 'access': []}, {'target_room': 179, 'access': ['DragonClaw', 'Pazuzu2FLock', 'Pazuzu4FLock', 'Pazuzu6FLock', 'Pazuzu1F', 'Pazuzu2F', 'Pazuzu3F', 'Pazuzu4F', 'Pazuzu5F', 'Pazuzu6F']}]}, {'name': 'Pazuzu 7F Sky Room', 'id': 179, 'game_objects': [{'name': "Pazuzu's Tower 7F - Pazuzu Chest", 'object_id': 26, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu'], 'access': ['Pazuzu2FLock', 'Pazuzu4FLock', 'Pazuzu6FLock', 'Pazuzu1F', 'Pazuzu2F', 'Pazuzu3F', 'Pazuzu4F', 'Pazuzu5F', 'Pazuzu6F']}], 'links': [{'target_room': 178, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 1F to 3F', 'id': 180, 'game_objects': [], 'links': [{'target_room': 166, 'entrance': 385, 'teleporter': [29, 0], 'access': []}, {'target_room': 170, 'entrance': 386, 'teleporter': [30, 0], 'access': []}]}, {'name': 'Pazuzu 3F to 5F', 'id': 181, 'game_objects': [], 'links': [{'target_room': 170, 'entrance': 387, 'teleporter': [40, 0], 'access': []}, {'target_room': 174, 'entrance': 388, 'teleporter': [41, 0], 'access': []}]}, {'name': 'Pazuzu 5F to 7F', 'id': 182, 'game_objects': [], 'links': [{'target_room': 174, 'entrance': 389, 'teleporter': [38, 0], 'access': []}, {'target_room': 177, 'entrance': 390, 'teleporter': [39, 0], 'access': []}]}, {'name': 'Pazuzu 2F to 4F', 'id': 183, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 391, 'teleporter': [21, 0], 'access': []}, {'target_room': 173, 'entrance': 392, 'teleporter': [22, 0], 'access': []}]}, {'name': 'Pazuzu 4F to 6F', 'id': 184, 'game_objects': [], 'links': [{'target_room': 173, 'entrance': 393, 'teleporter': [2, 0], 'access': []}, {'target_room': 176, 'entrance': 394, 'teleporter': [3, 0], 'access': []}]}, {'name': 'Light Temple', 'id': 185, 'game_objects': [{'name': 'Light Temple - Box', 'object_id': 216, 'type': 'Box', 'access': []}], 'links': [{'target_room': 230, 'entrance': 395, 'teleporter': [19, 6], 'access': []}, {'target_room': 153, 'entrance': 396, 'teleporter': [70, 8], 'access': ['MobiusCrest']}]}, {'name': 'Ship Dock', 'id': 186, 'game_objects': [{'name': 'Ship Dock Access', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipDockAccess'], 'access': []}], 'links': [{'target_room': 228, 'entrance': 399, 'teleporter': [17, 6], 'access': []}, {'target_room': 162, 'entrance': 397, 'teleporter': [61, 8], 'access': ['MobiusCrest']}]}, {'name': 'Mac Ship Deck', 'id': 187, 'game_objects': [{'name': 'Mac Ship Steering Wheel', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipSteeringWheel'], 'access': []}, {'name': "Mac's Ship Deck - North Box", 'object_id': 217, 'type': 'Box', 'access': []}, {'name': "Mac's Ship Deck - Center Box", 'object_id': 218, 'type': 'Box', 'access': []}, {'name': "Mac's Ship Deck - South Box", 'object_id': 219, 'type': 'Box', 'access': []}], 'links': [{'target_room': 229, 'entrance': 400, 'teleporter': [37, 8], 'access': []}, {'target_room': 188, 'entrance': 401, 'teleporter': [50, 8], 'access': []}, {'target_room': 188, 'entrance': 402, 'teleporter': [51, 8], 'access': []}, {'target_room': 188, 'entrance': 403, 'teleporter': [52, 8], 'access': []}, {'target_room': 189, 'entrance': 404, 'teleporter': [53, 8], 'access': []}]}, {'name': 'Mac Ship B1 Outer Ring', 'id': 188, 'game_objects': [{'name': "Mac's Ship B1 - Northwest Hook Platform Box", 'object_id': 228, 'type': 'Box', 'access': ['DragonClaw']}, {'name': "Mac's Ship B1 - Center Hook Platform Box", 'object_id': 229, 'type': 'Box', 'access': ['DragonClaw']}], 'links': [{'target_room': 187, 'entrance': 405, 'teleporter': [208, 0], 'access': []}, {'target_room': 187, 'entrance': 406, 'teleporter': [175, 0], 'access': []}, {'target_room': 187, 'entrance': 407, 'teleporter': [172, 0], 'access': []}, {'target_room': 193, 'entrance': 408, 'teleporter': [88, 0], 'access': []}, {'target_room': 193, 'access': []}]}, {'name': 'Mac Ship B1 Square Room', 'id': 189, 'game_objects': [], 'links': [{'target_room': 187, 'entrance': 409, 'teleporter': [141, 0], 'access': []}, {'target_room': 192, 'entrance': 410, 'teleporter': [87, 0], 'access': []}]}, {'name': 'Mac Ship B1 Central Corridor', 'id': 190, 'game_objects': [{'name': "Mac's Ship B1 - Central Corridor Box", 'object_id': 230, 'type': 'Box', 'access': []}], 'links': [{'target_room': 192, 'entrance': 413, 'teleporter': [86, 0], 'access': []}, {'target_room': 191, 'entrance': 412, 'teleporter': [102, 0], 'access': []}, {'target_room': 193, 'access': []}]}, {'name': 'Mac Ship B2 South Corridor', 'id': 191, 'game_objects': [], 'links': [{'target_room': 190, 'entrance': 415, 'teleporter': [55, 8], 'access': []}, {'target_room': 194, 'entrance': 414, 'teleporter': [57, 1], 'access': []}]}, {'name': 'Mac Ship B2 North Corridor', 'id': 192, 'game_objects': [], 'links': [{'target_room': 190, 'entrance': 416, 'teleporter': [56, 8], 'access': []}, {'target_room': 189, 'entrance': 417, 'teleporter': [57, 8], 'access': []}]}, {'name': 'Mac Ship B2 Outer Ring', 'id': 193, 'game_objects': [{'name': "Mac's Ship B2 - Barrel Room South Box", 'object_id': 223, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Barrel Room North Box", 'object_id': 224, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Southwest Room Box", 'object_id': 225, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Southeast Room Box", 'object_id': 226, 'type': 'Box', 'access': []}], 'links': [{'target_room': 188, 'entrance': 418, 'teleporter': [58, 8], 'access': []}]}, {'name': 'Mac Ship B1 Mac Room', 'id': 194, 'game_objects': [{'name': "Mac's Ship B1 - Mac Room Chest", 'object_id': 27, 'type': 'Chest', 'access': []}, {'name': 'Captain Mac', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipLoaned'], 'access': ['CaptainCap']}], 'links': [{'target_room': 191, 'entrance': 424, 'teleporter': [101, 0], 'access': []}]}, {'name': 'Doom Castle Corridor of Destiny', 'id': 195, 'game_objects': [], 'links': [{'target_room': 201, 'entrance': 428, 'teleporter': [84, 0], 'access': []}, {'target_room': 196, 'entrance': 429, 'teleporter': [35, 2], 'access': []}, {'target_room': 197, 'entrance': 430, 'teleporter': [209, 0], 'access': ['StoneGolem']}, {'target_room': 198, 'entrance': 431, 'teleporter': [211, 0], 'access': ['StoneGolem', 'TwinheadWyvern']}, {'target_room': 199, 'entrance': 432, 'teleporter': [13, 2], 'access': ['StoneGolem', 'TwinheadWyvern', 'Zuh']}]}, {'name': 'Doom Castle Ice Floor', 'id': 196, 'game_objects': [{'name': 'Doom Castle 4F - Northwest Room Box', 'object_id': 231, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Doom Castle 4F - Southwest Room Box', 'object_id': 232, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Doom Castle 4F - Northeast Room Box', 'object_id': 233, 'type': 'Box', 'access': ['Sword']}, {'name': 'Doom Castle 4F - Southeast Room Box', 'object_id': 234, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Stone Golem', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['StoneGolem'], 'access': ['Sword', 'DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 433, 'teleporter': [109, 3], 'access': []}]}, {'name': 'Doom Castle Lava Floor', 'id': 197, 'game_objects': [{'name': 'Doom Castle 5F - North Left Box', 'object_id': 235, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - North Right Box', 'object_id': 236, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - South Left Box', 'object_id': 237, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - South Right Box', 'object_id': 238, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Twinhead Wyvern', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['TwinheadWyvern'], 'access': ['DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 434, 'teleporter': [210, 0], 'access': []}]}, {'name': 'Doom Castle Sky Floor', 'id': 198, 'game_objects': [{'name': 'Doom Castle 6F - West Box', 'object_id': 239, 'type': 'Box', 'access': []}, {'name': 'Doom Castle 6F - East Box', 'object_id': 240, 'type': 'Box', 'access': []}, {'name': 'Zuh', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Zuh'], 'access': ['DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 435, 'teleporter': [212, 0], 'access': []}, {'target_room': 197, 'access': []}]}, {'name': 'Doom Castle Hero Room', 'id': 199, 'game_objects': [{'name': 'Doom Castle Hero Chest 01', 'object_id': 242, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 02', 'object_id': 243, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 03', 'object_id': 244, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 04', 'object_id': 245, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 200, 'entrance': 436, 'teleporter': [54, 0], 'access': []}, {'target_room': 195, 'entrance': 441, 'teleporter': [110, 3], 'access': []}]}, {'name': 'Doom Castle Dark King Room', 'id': 200, 'game_objects': [], 'links': [{'target_room': 199, 'entrance': 442, 'teleporter': [52, 0], 'access': []}]}]
+entrances = [{'name': 'Doom Castle - Sand Floor - To Sky Door - Sand Floor', 'id': 0, 'area': 7, 'coordinates': [24, 19], 'teleporter': [0, 0]}, {'name': 'Doom Castle - Sand Floor - Main Entrance - Sand Floor', 'id': 1, 'area': 7, 'coordinates': [19, 43], 'teleporter': [1, 6]}, {'name': 'Doom Castle - Aero Room - Aero Room Entrance', 'id': 2, 'area': 7, 'coordinates': [27, 39], 'teleporter': [1, 0]}, {'name': 'Focus Tower B1 - Main Loop - South Entrance', 'id': 3, 'area': 8, 'coordinates': [43, 60], 'teleporter': [2, 6]}, {'name': 'Focus Tower B1 - Main Loop - To Focus Tower 1F - Main Hall', 'id': 4, 'area': 8, 'coordinates': [37, 41], 'teleporter': [4, 0]}, {'name': 'Focus Tower B1 - Aero Corridor - To Focus Tower 1F - Sun Coin Room', 'id': 5, 'area': 8, 'coordinates': [59, 35], 'teleporter': [5, 0]}, {'name': 'Focus Tower B1 - Aero Corridor - To Sand Floor - Aero Chest', 'id': 6, 'area': 8, 'coordinates': [57, 59], 'teleporter': [8, 0]}, {'name': 'Focus Tower B1 - Inner Loop - To Focus Tower 1F - Sky Door', 'id': 7, 'area': 8, 'coordinates': [51, 49], 'teleporter': [6, 0]}, {'name': 'Focus Tower B1 - Inner Loop - To Doom Castle Sand Floor', 'id': 8, 'area': 8, 'coordinates': [51, 45], 'teleporter': [7, 0]}, {'name': 'Focus Tower 1F - Focus Tower West Entrance', 'id': 9, 'area': 9, 'coordinates': [25, 29], 'teleporter': [3, 6]}, {'name': 'Focus Tower 1F - To Focus Tower 2F - From SandCoin', 'id': 10, 'area': 9, 'coordinates': [16, 4], 'teleporter': [10, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - Main Hall', 'id': 11, 'area': 9, 'coordinates': [4, 23], 'teleporter': [11, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - To Aero Chest', 'id': 12, 'area': 9, 'coordinates': [26, 17], 'teleporter': [12, 0]}, {'name': 'Focus Tower 1F - Sky Door', 'id': 13, 'area': 9, 'coordinates': [16, 24], 'teleporter': [13, 0]}, {'name': 'Focus Tower 1F - To Focus Tower 2F - From RiverCoin', 'id': 14, 'area': 9, 'coordinates': [16, 10], 'teleporter': [14, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - From Sky Door', 'id': 15, 'area': 9, 'coordinates': [16, 29], 'teleporter': [15, 0]}, {'name': 'Focus Tower 2F - Sand Coin Passage - North Entrance', 'id': 16, 'area': 10, 'coordinates': [49, 30], 'teleporter': [4, 6]}, {'name': 'Focus Tower 2F - Sand Coin Passage - To Focus Tower 1F - To SandCoin', 'id': 17, 'area': 10, 'coordinates': [47, 33], 'teleporter': [17, 0]}, {'name': 'Focus Tower 2F - River Coin Passage - To Focus Tower 1F - To RiverCoin', 'id': 18, 'area': 10, 'coordinates': [47, 41], 'teleporter': [18, 0]}, {'name': 'Focus Tower 2F - River Coin Passage - To Focus Tower 3F - Lower Floor', 'id': 19, 'area': 10, 'coordinates': [38, 40], 'teleporter': [20, 0]}, {'name': 'Focus Tower 2F - Venus Chest Room - To Focus Tower 3F - Upper Floor', 'id': 20, 'area': 10, 'coordinates': [56, 40], 'teleporter': [19, 0]}, {'name': 'Focus Tower 2F - Venus Chest Room - Pillar Script', 'id': 21, 'area': 10, 'coordinates': [48, 53], 'teleporter': [13, 8]}, {'name': 'Focus Tower 3F - Lower Floor - To Fireburg Entrance', 'id': 22, 'area': 11, 'coordinates': [11, 39], 'teleporter': [6, 6]}, {'name': 'Focus Tower 3F - Lower Floor - To Focus Tower 2F - Jump on Pillar', 'id': 23, 'area': 11, 'coordinates': [6, 47], 'teleporter': [24, 0]}, {'name': 'Focus Tower 3F - Upper Floor - To Aquaria Entrance', 'id': 24, 'area': 11, 'coordinates': [21, 38], 'teleporter': [5, 6]}, {'name': 'Focus Tower 3F - Upper Floor - To Focus Tower 2F - Venus Chest Room', 'id': 25, 'area': 11, 'coordinates': [24, 47], 'teleporter': [23, 0]}, {'name': 'Level Forest - Boulder Script', 'id': 26, 'area': 14, 'coordinates': [52, 15], 'teleporter': [0, 8]}, {'name': 'Level Forest - Rotten Tree Script', 'id': 27, 'area': 14, 'coordinates': [47, 6], 'teleporter': [2, 8]}, {'name': 'Level Forest - Exit Level Forest 1', 'id': 28, 'area': 14, 'coordinates': [46, 25], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 2', 'id': 29, 'area': 14, 'coordinates': [46, 26], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 3', 'id': 30, 'area': 14, 'coordinates': [47, 25], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 4', 'id': 31, 'area': 14, 'coordinates': [47, 26], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 5', 'id': 32, 'area': 14, 'coordinates': [60, 14], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 6', 'id': 33, 'area': 14, 'coordinates': [61, 14], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 7', 'id': 34, 'area': 14, 'coordinates': [46, 4], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 8', 'id': 35, 'area': 14, 'coordinates': [46, 3], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 9', 'id': 36, 'area': 14, 'coordinates': [47, 4], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest A', 'id': 37, 'area': 14, 'coordinates': [47, 3], 'teleporter': [25, 0]}, {'name': 'Foresta - Exit Foresta 1', 'id': 38, 'area': 15, 'coordinates': [10, 25], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 2', 'id': 39, 'area': 15, 'coordinates': [10, 26], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 3', 'id': 40, 'area': 15, 'coordinates': [11, 25], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 4', 'id': 41, 'area': 15, 'coordinates': [11, 26], 'teleporter': [31, 0]}, {'name': 'Foresta - Old Man House - Front Door', 'id': 42, 'area': 15, 'coordinates': [25, 17], 'teleporter': [32, 4]}, {'name': 'Foresta - Old Man House - Back Door', 'id': 43, 'area': 15, 'coordinates': [25, 14], 'teleporter': [33, 0]}, {'name': "Foresta - Kaeli's House", 'id': 44, 'area': 15, 'coordinates': [7, 21], 'teleporter': [0, 5]}, {'name': 'Foresta - Rest House', 'id': 45, 'area': 15, 'coordinates': [23, 23], 'teleporter': [1, 5]}, {'name': "Kaeli's House - Kaeli's House Entrance", 'id': 46, 'area': 16, 'coordinates': [11, 20], 'teleporter': [86, 3]}, {'name': "Foresta Houses - Old Man's House - Old Man Front Exit", 'id': 47, 'area': 17, 'coordinates': [35, 44], 'teleporter': [34, 0]}, {'name': "Foresta Houses - Old Man's House - Old Man Back Exit", 'id': 48, 'area': 17, 'coordinates': [35, 27], 'teleporter': [35, 0]}, {'name': 'Foresta - Old Man House - Barrel Tile Script', 'id': 483, 'area': 17, 'coordinates': [35, 30], 'teleporter': [13, 8]}, {'name': 'Foresta Houses - Rest House - Bed Script', 'id': 49, 'area': 17, 'coordinates': [30, 6], 'teleporter': [1, 8]}, {'name': 'Foresta Houses - Rest House - Rest House Exit', 'id': 50, 'area': 17, 'coordinates': [35, 20], 'teleporter': [87, 3]}, {'name': 'Foresta Houses - Libra House - Libra House Script', 'id': 51, 'area': 17, 'coordinates': [8, 49], 'teleporter': [67, 8]}, {'name': 'Foresta Houses - Gemini House - Gemini House Script', 'id': 52, 'area': 17, 'coordinates': [26, 55], 'teleporter': [68, 8]}, {'name': 'Foresta Houses - Mobius House - Mobius House Script', 'id': 53, 'area': 17, 'coordinates': [14, 33], 'teleporter': [69, 8]}, {'name': 'Sand Temple - Sand Temple Entrance', 'id': 54, 'area': 18, 'coordinates': [56, 27], 'teleporter': [36, 0]}, {'name': 'Bone Dungeon 1F - Bone Dungeon Entrance', 'id': 55, 'area': 19, 'coordinates': [13, 60], 'teleporter': [37, 0]}, {'name': 'Bone Dungeon 1F - To Bone Dungeon B1', 'id': 56, 'area': 19, 'coordinates': [13, 39], 'teleporter': [2, 2]}, {'name': 'Bone Dungeon B1 - Waterway - Exit Waterway', 'id': 57, 'area': 20, 'coordinates': [27, 39], 'teleporter': [3, 2]}, {'name': "Bone Dungeon B1 - Waterway - Tristam's Script", 'id': 58, 'area': 20, 'coordinates': [27, 45], 'teleporter': [3, 8]}, {'name': 'Bone Dungeon B1 - Waterway - To Bone Dungeon 1F', 'id': 59, 'area': 20, 'coordinates': [54, 61], 'teleporter': [88, 3]}, {'name': 'Bone Dungeon B1 - Checker Room - Exit Checker Room', 'id': 60, 'area': 20, 'coordinates': [23, 40], 'teleporter': [4, 2]}, {'name': 'Bone Dungeon B1 - Checker Room - To Waterway', 'id': 61, 'area': 20, 'coordinates': [39, 49], 'teleporter': [89, 3]}, {'name': 'Bone Dungeon B1 - Hidden Room - To B2 - Exploding Skull Room', 'id': 62, 'area': 20, 'coordinates': [5, 33], 'teleporter': [91, 3]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Hidden Passage', 'id': 63, 'area': 21, 'coordinates': [19, 13], 'teleporter': [5, 2]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Two Skulls Room', 'id': 64, 'area': 21, 'coordinates': [29, 15], 'teleporter': [6, 2]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Checker Room', 'id': 65, 'area': 21, 'coordinates': [8, 25], 'teleporter': [90, 3]}, {'name': 'Bonne Dungeon B2 - Box Room - To B2 - Two Skulls Room', 'id': 66, 'area': 21, 'coordinates': [59, 12], 'teleporter': [93, 3]}, {'name': 'Bonne Dungeon B2 - Quake Room - To B2 - Two Skulls Room', 'id': 67, 'area': 21, 'coordinates': [59, 28], 'teleporter': [94, 3]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Box Room', 'id': 68, 'area': 21, 'coordinates': [53, 7], 'teleporter': [7, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Quake Room', 'id': 69, 'area': 21, 'coordinates': [41, 3], 'teleporter': [8, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Boss Room', 'id': 70, 'area': 21, 'coordinates': [47, 57], 'teleporter': [9, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To B2 - Exploding Skull Room', 'id': 71, 'area': 21, 'coordinates': [54, 23], 'teleporter': [92, 3]}, {'name': 'Bone Dungeon B2 - Boss Room - Flamerus Rex Script', 'id': 72, 'area': 22, 'coordinates': [29, 19], 'teleporter': [4, 8]}, {'name': 'Bone Dungeon B2 - Boss Room - Tristam Leave Script', 'id': 73, 'area': 22, 'coordinates': [29, 23], 'teleporter': [75, 8]}, {'name': 'Bone Dungeon B2 - Boss Room - To B2 - Two Skulls Room', 'id': 74, 'area': 22, 'coordinates': [30, 27], 'teleporter': [95, 3]}, {'name': 'Libra Temple - Entrance', 'id': 75, 'area': 23, 'coordinates': [10, 15], 'teleporter': [13, 6]}, {'name': 'Libra Temple - Libra Tile Script', 'id': 76, 'area': 23, 'coordinates': [9, 8], 'teleporter': [59, 8]}, {'name': 'Aquaria Winter - Winter Entrance 1', 'id': 77, 'area': 24, 'coordinates': [25, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 2', 'id': 78, 'area': 24, 'coordinates': [25, 26], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 3', 'id': 79, 'area': 24, 'coordinates': [26, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 4', 'id': 80, 'area': 24, 'coordinates': [26, 26], 'teleporter': [8, 6]}, {'name': "Aquaria Winter - Winter Phoebe's House Entrance Script", 'id': 81, 'area': 24, 'coordinates': [8, 19], 'teleporter': [10, 5]}, {'name': 'Aquaria Winter - Winter Vendor House Entrance', 'id': 82, 'area': 24, 'coordinates': [8, 5], 'teleporter': [44, 4]}, {'name': 'Aquaria Winter - Winter INN Entrance', 'id': 83, 'area': 24, 'coordinates': [26, 17], 'teleporter': [11, 5]}, {'name': 'Aquaria Summer - Summer Entrance 1', 'id': 84, 'area': 25, 'coordinates': [57, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 2', 'id': 85, 'area': 25, 'coordinates': [57, 26], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 3', 'id': 86, 'area': 25, 'coordinates': [58, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 4', 'id': 87, 'area': 25, 'coordinates': [58, 26], 'teleporter': [8, 6]}, {'name': "Aquaria Summer - Summer Phoebe's House Entrance", 'id': 88, 'area': 25, 'coordinates': [40, 19], 'teleporter': [10, 5]}, {'name': "Aquaria Summer - Spencer's Place Entrance Top", 'id': 89, 'area': 25, 'coordinates': [40, 16], 'teleporter': [42, 0]}, {'name': "Aquaria Summer - Spencer's Place Entrance Side", 'id': 90, 'area': 25, 'coordinates': [41, 18], 'teleporter': [43, 0]}, {'name': 'Aquaria Summer - Summer Vendor House Entrance', 'id': 91, 'area': 25, 'coordinates': [40, 5], 'teleporter': [44, 4]}, {'name': 'Aquaria Summer - Summer INN Entrance', 'id': 92, 'area': 25, 'coordinates': [58, 17], 'teleporter': [11, 5]}, {'name': "Phoebe's House - Entrance", 'id': 93, 'area': 26, 'coordinates': [29, 14], 'teleporter': [5, 8]}, {'name': "Aquaria Vendor House - Vendor House Entrance's Script", 'id': 94, 'area': 27, 'coordinates': [7, 10], 'teleporter': [40, 8]}, {'name': 'Aquaria Vendor House - Vendor House Stairs', 'id': 95, 'area': 27, 'coordinates': [1, 4], 'teleporter': [47, 0]}, {'name': 'Aquaria Gemini Room - Gemini Script', 'id': 96, 'area': 27, 'coordinates': [2, 40], 'teleporter': [72, 8]}, {'name': 'Aquaria Gemini Room - Gemini Room Stairs', 'id': 97, 'area': 27, 'coordinates': [4, 39], 'teleporter': [48, 0]}, {'name': 'Aquaria INN - Aquaria INN entrance', 'id': 98, 'area': 27, 'coordinates': [51, 46], 'teleporter': [75, 8]}, {'name': 'Wintry Cave 1F - Main Entrance', 'id': 99, 'area': 28, 'coordinates': [50, 58], 'teleporter': [49, 0]}, {'name': 'Wintry Cave 1F - To 3F Top', 'id': 100, 'area': 28, 'coordinates': [40, 25], 'teleporter': [14, 2]}, {'name': 'Wintry Cave 1F - To 2F', 'id': 101, 'area': 28, 'coordinates': [10, 43], 'teleporter': [15, 2]}, {'name': "Wintry Cave 1F - Phoebe's Script", 'id': 102, 'area': 28, 'coordinates': [44, 37], 'teleporter': [6, 8]}, {'name': 'Wintry Cave 2F - To 3F Bottom', 'id': 103, 'area': 29, 'coordinates': [58, 5], 'teleporter': [50, 0]}, {'name': 'Wintry Cave 2F - To 1F', 'id': 104, 'area': 29, 'coordinates': [38, 18], 'teleporter': [97, 3]}, {'name': 'Wintry Cave 3F Top - Exit from 3F Top', 'id': 105, 'area': 30, 'coordinates': [24, 6], 'teleporter': [96, 3]}, {'name': 'Wintry Cave 3F Bottom - Exit to 2F', 'id': 106, 'area': 31, 'coordinates': [4, 29], 'teleporter': [51, 0]}, {'name': 'Life Temple - Entrance', 'id': 107, 'area': 32, 'coordinates': [9, 60], 'teleporter': [14, 6]}, {'name': 'Life Temple - Libra Tile Script', 'id': 108, 'area': 32, 'coordinates': [3, 55], 'teleporter': [60, 8]}, {'name': 'Life Temple - Mysterious Man Script', 'id': 109, 'area': 32, 'coordinates': [9, 44], 'teleporter': [78, 8]}, {'name': 'Fall Basin - Back Exit Script', 'id': 110, 'area': 33, 'coordinates': [17, 5], 'teleporter': [9, 0]}, {'name': 'Fall Basin - Main Exit', 'id': 111, 'area': 33, 'coordinates': [15, 26], 'teleporter': [53, 0]}, {'name': "Fall Basin - Phoebe's Script", 'id': 112, 'area': 33, 'coordinates': [17, 6], 'teleporter': [9, 8]}, {'name': 'Ice Pyramid B1 Taunt Room - To Climbing Wall Room', 'id': 113, 'area': 34, 'coordinates': [43, 6], 'teleporter': [55, 0]}, {'name': 'Ice Pyramid 1F Maze - Main Entrance 1', 'id': 114, 'area': 35, 'coordinates': [18, 36], 'teleporter': [56, 0]}, {'name': 'Ice Pyramid 1F Maze - Main Entrance 2', 'id': 115, 'area': 35, 'coordinates': [19, 36], 'teleporter': [56, 0]}, {'name': 'Ice Pyramid 1F Maze - West Stairs To 2F South Tiled Room', 'id': 116, 'area': 35, 'coordinates': [3, 27], 'teleporter': [57, 0]}, {'name': 'Ice Pyramid 1F Maze - West Center Stairs to 2F West Room', 'id': 117, 'area': 35, 'coordinates': [11, 15], 'teleporter': [58, 0]}, {'name': 'Ice Pyramid 1F Maze - East Center Stairs to 2F Center Room', 'id': 118, 'area': 35, 'coordinates': [25, 16], 'teleporter': [59, 0]}, {'name': 'Ice Pyramid 1F Maze - Upper Stairs to 2F Small North Room', 'id': 119, 'area': 35, 'coordinates': [31, 1], 'teleporter': [60, 0]}, {'name': 'Ice Pyramid 1F Maze - East Stairs to 2F North Corridor', 'id': 120, 'area': 35, 'coordinates': [34, 9], 'teleporter': [61, 0]}, {'name': "Ice Pyramid 1F Maze - Statue's Script", 'id': 121, 'area': 35, 'coordinates': [21, 32], 'teleporter': [77, 8]}, {'name': 'Ice Pyramid 2F South Tiled Room - To 1F', 'id': 122, 'area': 36, 'coordinates': [4, 26], 'teleporter': [62, 0]}, {'name': 'Ice Pyramid 2F South Tiled Room - To 3F Two Boxes Room', 'id': 123, 'area': 36, 'coordinates': [22, 17], 'teleporter': [67, 0]}, {'name': 'Ice Pyramid 2F West Room - To 1F', 'id': 124, 'area': 36, 'coordinates': [9, 10], 'teleporter': [63, 0]}, {'name': 'Ice Pyramid 2F Center Room - To 1F', 'id': 125, 'area': 36, 'coordinates': [22, 14], 'teleporter': [64, 0]}, {'name': 'Ice Pyramid 2F Small North Room - To 1F', 'id': 126, 'area': 36, 'coordinates': [26, 4], 'teleporter': [65, 0]}, {'name': 'Ice Pyramid 2F North Corridor - To 1F', 'id': 127, 'area': 36, 'coordinates': [32, 8], 'teleporter': [66, 0]}, {'name': 'Ice Pyramid 2F North Corridor - To 3F Main Loop', 'id': 128, 'area': 36, 'coordinates': [12, 7], 'teleporter': [68, 0]}, {'name': 'Ice Pyramid 3F Two Boxes Room - To 2F South Tiled Room', 'id': 129, 'area': 37, 'coordinates': [24, 54], 'teleporter': [69, 0]}, {'name': 'Ice Pyramid 3F Main Loop - To 2F Corridor', 'id': 130, 'area': 37, 'coordinates': [16, 45], 'teleporter': [70, 0]}, {'name': 'Ice Pyramid 3F Main Loop - To 4F', 'id': 131, 'area': 37, 'coordinates': [19, 43], 'teleporter': [71, 0]}, {'name': 'Ice Pyramid 4F Treasure Room - To 3F Main Loop', 'id': 132, 'area': 38, 'coordinates': [52, 5], 'teleporter': [72, 0]}, {'name': 'Ice Pyramid 4F Treasure Room - To 5F Leap of Faith Room', 'id': 133, 'area': 38, 'coordinates': [62, 19], 'teleporter': [73, 0]}, {'name': 'Ice Pyramid 5F Leap of Faith Room - To 4F Treasure Room', 'id': 134, 'area': 39, 'coordinates': [54, 63], 'teleporter': [74, 0]}, {'name': 'Ice Pyramid 5F Leap of Faith Room - Bombed Ice Plate', 'id': 135, 'area': 39, 'coordinates': [47, 54], 'teleporter': [77, 8]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem - To Ice Golem Room', 'id': 136, 'area': 39, 'coordinates': [39, 43], 'teleporter': [75, 0]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem - To Climbing Wall Room', 'id': 137, 'area': 39, 'coordinates': [39, 60], 'teleporter': [76, 0]}, {'name': 'Ice Pyramid - Duplicate Ice Golem Room', 'id': 138, 'area': 40, 'coordinates': [44, 43], 'teleporter': [77, 0]}, {'name': 'Ice Pyramid Climbing Wall Room - To Taunt Room', 'id': 139, 'area': 41, 'coordinates': [4, 59], 'teleporter': [78, 0]}, {'name': 'Ice Pyramid Climbing Wall Room - To 5F Stairs', 'id': 140, 'area': 41, 'coordinates': [4, 45], 'teleporter': [79, 0]}, {'name': 'Ice Pyramid Ice Golem Room - To 5F Stairs', 'id': 141, 'area': 42, 'coordinates': [44, 43], 'teleporter': [80, 0]}, {'name': 'Ice Pyramid Ice Golem Room - Ice Golem Script', 'id': 142, 'area': 42, 'coordinates': [53, 32], 'teleporter': [10, 8]}, {'name': 'Spencer Waterfall - To Spencer Cave', 'id': 143, 'area': 43, 'coordinates': [48, 57], 'teleporter': [81, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 1', 'id': 144, 'area': 43, 'coordinates': [40, 5], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 2', 'id': 145, 'area': 43, 'coordinates': [40, 6], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 3', 'id': 146, 'area': 43, 'coordinates': [41, 5], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 4', 'id': 147, 'area': 43, 'coordinates': [41, 6], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Right Exit to Aquaria 1', 'id': 148, 'area': 43, 'coordinates': [46, 8], 'teleporter': [83, 0]}, {'name': 'Spencer Waterfall - Right Exit to Aquaria 2', 'id': 149, 'area': 43, 'coordinates': [47, 8], 'teleporter': [83, 0]}, {'name': 'Spencer Cave Normal Main - To Waterfall', 'id': 150, 'area': 44, 'coordinates': [14, 39], 'teleporter': [85, 0]}, {'name': 'Spencer Cave Normal From Overworld - Exit to Overworld', 'id': 151, 'area': 44, 'coordinates': [15, 57], 'teleporter': [7, 6]}, {'name': 'Spencer Cave Unplug - Exit to Overworld', 'id': 152, 'area': 45, 'coordinates': [40, 29], 'teleporter': [7, 6]}, {'name': 'Spencer Cave Unplug - Libra Teleporter Start Script', 'id': 153, 'area': 45, 'coordinates': [28, 21], 'teleporter': [33, 8]}, {'name': 'Spencer Cave Unplug - Libra Teleporter End Script', 'id': 154, 'area': 45, 'coordinates': [46, 4], 'teleporter': [34, 8]}, {'name': 'Spencer Cave Unplug - Mobius Teleporter Chest Script', 'id': 155, 'area': 45, 'coordinates': [21, 9], 'teleporter': [35, 8]}, {'name': 'Spencer Cave Unplug - Mobius Teleporter Start Script', 'id': 156, 'area': 45, 'coordinates': [29, 28], 'teleporter': [36, 8]}, {'name': 'Wintry Temple Outer Room - Main Entrance', 'id': 157, 'area': 46, 'coordinates': [8, 31], 'teleporter': [15, 6]}, {'name': 'Wintry Temple Inner Room - Gemini Tile to Sealed temple', 'id': 158, 'area': 46, 'coordinates': [9, 24], 'teleporter': [62, 8]}, {'name': 'Fireburg - To Overworld', 'id': 159, 'area': 47, 'coordinates': [4, 13], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 160, 'area': 47, 'coordinates': [5, 13], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 161, 'area': 47, 'coordinates': [28, 15], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 162, 'area': 47, 'coordinates': [27, 15], 'teleporter': [9, 6]}, {'name': 'Fireburg - Vendor House', 'id': 163, 'area': 47, 'coordinates': [10, 24], 'teleporter': [91, 0]}, {'name': 'Fireburg - Reuben House', 'id': 164, 'area': 47, 'coordinates': [14, 6], 'teleporter': [98, 8]}, {'name': 'Fireburg - Hotel', 'id': 165, 'area': 47, 'coordinates': [20, 8], 'teleporter': [96, 8]}, {'name': 'Fireburg - GrenadeMan House Script', 'id': 166, 'area': 47, 'coordinates': [12, 18], 'teleporter': [11, 8]}, {'name': 'Reuben House - Main Entrance', 'id': 167, 'area': 48, 'coordinates': [33, 46], 'teleporter': [98, 3]}, {'name': 'GrenadeMan House - Entrance Script', 'id': 168, 'area': 49, 'coordinates': [55, 60], 'teleporter': [9, 8]}, {'name': 'GrenadeMan House - To Mobius Crest Room', 'id': 169, 'area': 49, 'coordinates': [57, 52], 'teleporter': [93, 0]}, {'name': 'GrenadeMan Mobius Room - Stairs to House', 'id': 170, 'area': 49, 'coordinates': [39, 26], 'teleporter': [94, 0]}, {'name': 'GrenadeMan Mobius Room - Mobius Teleporter Script', 'id': 171, 'area': 49, 'coordinates': [39, 23], 'teleporter': [54, 8]}, {'name': 'Fireburg Vendor House - Entrance Script', 'id': 172, 'area': 49, 'coordinates': [7, 10], 'teleporter': [95, 0]}, {'name': 'Fireburg Vendor House - Stairs to Gemini Room', 'id': 173, 'area': 49, 'coordinates': [1, 4], 'teleporter': [96, 0]}, {'name': 'Fireburg Gemini Room - Stairs to Vendor House', 'id': 174, 'area': 49, 'coordinates': [4, 39], 'teleporter': [97, 0]}, {'name': 'Fireburg Gemini Room - Gemini Teleporter Script', 'id': 175, 'area': 49, 'coordinates': [2, 40], 'teleporter': [45, 8]}, {'name': 'Fireburg Hotel Lobby - Stairs to beds', 'id': 176, 'area': 49, 'coordinates': [4, 50], 'teleporter': [213, 0]}, {'name': 'Fireburg Hotel Lobby - Entrance', 'id': 177, 'area': 49, 'coordinates': [17, 56], 'teleporter': [99, 3]}, {'name': 'Fireburg Hotel Beds - Stairs to Hotel Lobby', 'id': 178, 'area': 49, 'coordinates': [45, 59], 'teleporter': [214, 0]}, {'name': 'Mine Exterior - Main Entrance', 'id': 179, 'area': 50, 'coordinates': [5, 28], 'teleporter': [98, 0]}, {'name': 'Mine Exterior - To Cliff', 'id': 180, 'area': 50, 'coordinates': [58, 29], 'teleporter': [99, 0]}, {'name': 'Mine Exterior - To Parallel Room', 'id': 181, 'area': 50, 'coordinates': [8, 7], 'teleporter': [20, 2]}, {'name': 'Mine Exterior - To Crescent Room', 'id': 182, 'area': 50, 'coordinates': [26, 15], 'teleporter': [21, 2]}, {'name': 'Mine Exterior - To Climbing Room', 'id': 183, 'area': 50, 'coordinates': [21, 35], 'teleporter': [22, 2]}, {'name': 'Mine Exterior - Jinn Fight Script', 'id': 184, 'area': 50, 'coordinates': [58, 31], 'teleporter': [74, 8]}, {'name': 'Mine Parallel Room - To Mine Exterior', 'id': 185, 'area': 51, 'coordinates': [7, 60], 'teleporter': [100, 3]}, {'name': 'Mine Crescent Room - To Mine Exterior', 'id': 186, 'area': 51, 'coordinates': [22, 61], 'teleporter': [101, 3]}, {'name': 'Mine Climbing Room - To Mine Exterior', 'id': 187, 'area': 51, 'coordinates': [56, 21], 'teleporter': [102, 3]}, {'name': 'Mine Cliff - Entrance', 'id': 188, 'area': 52, 'coordinates': [9, 5], 'teleporter': [100, 0]}, {'name': 'Mine Cliff - Reuben Grenade Script', 'id': 189, 'area': 52, 'coordinates': [15, 7], 'teleporter': [12, 8]}, {'name': 'Sealed Temple - To Overworld', 'id': 190, 'area': 53, 'coordinates': [58, 43], 'teleporter': [16, 6]}, {'name': 'Sealed Temple - Gemini Tile Script', 'id': 191, 'area': 53, 'coordinates': [56, 38], 'teleporter': [63, 8]}, {'name': 'Volcano Base - Main Entrance 1', 'id': 192, 'area': 54, 'coordinates': [23, 25], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 2', 'id': 193, 'area': 54, 'coordinates': [23, 26], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 3', 'id': 194, 'area': 54, 'coordinates': [24, 25], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 4', 'id': 195, 'area': 54, 'coordinates': [24, 26], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Left Stairs Script', 'id': 196, 'area': 54, 'coordinates': [20, 5], 'teleporter': [31, 8]}, {'name': 'Volcano Base - Right Stairs Script', 'id': 197, 'area': 54, 'coordinates': [32, 5], 'teleporter': [30, 8]}, {'name': 'Volcano Top Right - Top Exit', 'id': 198, 'area': 55, 'coordinates': [44, 8], 'teleporter': [9, 0]}, {'name': 'Volcano Top Left - To Right-Left Path Script', 'id': 199, 'area': 55, 'coordinates': [40, 24], 'teleporter': [26, 8]}, {'name': 'Volcano Top Right - To Left-Right Path Script', 'id': 200, 'area': 55, 'coordinates': [52, 24], 'teleporter': [79, 8]}, {'name': 'Volcano Right Path - To Volcano Base Script', 'id': 201, 'area': 56, 'coordinates': [48, 42], 'teleporter': [15, 8]}, {'name': 'Volcano Left Path - To Volcano Cross Left-Right', 'id': 202, 'area': 56, 'coordinates': [40, 31], 'teleporter': [25, 2]}, {'name': 'Volcano Left Path - To Volcano Cross Right-Left', 'id': 203, 'area': 56, 'coordinates': [52, 29], 'teleporter': [26, 2]}, {'name': 'Volcano Left Path - To Volcano Base Script', 'id': 204, 'area': 56, 'coordinates': [36, 42], 'teleporter': [27, 8]}, {'name': 'Volcano Cross Left-Right - To Volcano Left Path', 'id': 205, 'area': 56, 'coordinates': [10, 42], 'teleporter': [103, 3]}, {'name': 'Volcano Cross Left-Right - To Volcano Top Right Script', 'id': 206, 'area': 56, 'coordinates': [16, 24], 'teleporter': [29, 8]}, {'name': 'Volcano Cross Right-Left - To Volcano Top Left Script', 'id': 207, 'area': 56, 'coordinates': [8, 22], 'teleporter': [28, 8]}, {'name': 'Volcano Cross Right-Left - To Volcano Left Path', 'id': 208, 'area': 56, 'coordinates': [16, 42], 'teleporter': [104, 3]}, {'name': 'Lava Dome Inner Ring Main Loop - Main Entrance 1', 'id': 209, 'area': 57, 'coordinates': [32, 5], 'teleporter': [104, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - Main Entrance 2', 'id': 210, 'area': 57, 'coordinates': [33, 5], 'teleporter': [104, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Three Steps Room', 'id': 211, 'area': 57, 'coordinates': [14, 5], 'teleporter': [105, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Life Chest Room Lower', 'id': 212, 'area': 57, 'coordinates': [40, 17], 'teleporter': [106, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Big Jump Room Left', 'id': 213, 'area': 57, 'coordinates': [8, 11], 'teleporter': [108, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Split Corridor Room', 'id': 214, 'area': 57, 'coordinates': [11, 19], 'teleporter': [111, 0]}, {'name': 'Lava Dome Inner Ring Center Ledge - To Life Chest Room Higher', 'id': 215, 'area': 57, 'coordinates': [32, 11], 'teleporter': [107, 0]}, {'name': 'Lava Dome Inner Ring Plate Ledge - To Plate Corridor', 'id': 216, 'area': 57, 'coordinates': [12, 23], 'teleporter': [109, 0]}, {'name': 'Lava Dome Inner Ring Plate Ledge - Plate Script', 'id': 217, 'area': 57, 'coordinates': [5, 23], 'teleporter': [47, 8]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Pointless Room', 'id': 218, 'area': 57, 'coordinates': [0, 9], 'teleporter': [110, 0]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Lower Moon Helm Room', 'id': 219, 'area': 57, 'coordinates': [0, 15], 'teleporter': [112, 0]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Up-Down Corridor', 'id': 220, 'area': 57, 'coordinates': [54, 5], 'teleporter': [113, 0]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - To Jumping Maze II', 'id': 221, 'area': 57, 'coordinates': [54, 21], 'teleporter': [114, 0]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 1', 'id': 222, 'area': 57, 'coordinates': [62, 20], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 2', 'id': 223, 'area': 57, 'coordinates': [63, 20], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 3', 'id': 224, 'area': 57, 'coordinates': [62, 21], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 4', 'id': 225, 'area': 57, 'coordinates': [63, 21], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Tiny Bottom Ledge - To Four Boxes Corridor', 'id': 226, 'area': 57, 'coordinates': [50, 25], 'teleporter': [115, 0]}, {'name': 'Lava Dome Jump Maze II - Lower Right Entrance', 'id': 227, 'area': 58, 'coordinates': [55, 28], 'teleporter': [116, 0]}, {'name': 'Lava Dome Jump Maze II - Upper Entrance', 'id': 228, 'area': 58, 'coordinates': [35, 3], 'teleporter': [119, 0]}, {'name': 'Lava Dome Jump Maze II - Lower Left Entrance', 'id': 229, 'area': 58, 'coordinates': [34, 27], 'teleporter': [120, 0]}, {'name': 'Lava Dome Up-Down Corridor - Upper Entrance', 'id': 230, 'area': 58, 'coordinates': [29, 8], 'teleporter': [117, 0]}, {'name': 'Lava Dome Up-Down Corridor - Lower Entrance', 'id': 231, 'area': 58, 'coordinates': [28, 25], 'teleporter': [118, 0]}, {'name': 'Lava Dome Jump Maze I - South Entrance', 'id': 232, 'area': 59, 'coordinates': [20, 27], 'teleporter': [121, 0]}, {'name': 'Lava Dome Jump Maze I - North Entrance', 'id': 233, 'area': 59, 'coordinates': [7, 3], 'teleporter': [122, 0]}, {'name': 'Lava Dome Pointless Room - Entrance', 'id': 234, 'area': 60, 'coordinates': [2, 7], 'teleporter': [123, 0]}, {'name': 'Lava Dome Pointless Room - Visit Quest Script 1', 'id': 490, 'area': 60, 'coordinates': [4, 4], 'teleporter': [99, 8]}, {'name': 'Lava Dome Pointless Room - Visit Quest Script 2', 'id': 491, 'area': 60, 'coordinates': [4, 5], 'teleporter': [99, 8]}, {'name': 'Lava Dome Lower Moon Helm Room - Left Entrance', 'id': 235, 'area': 60, 'coordinates': [2, 19], 'teleporter': [124, 0]}, {'name': 'Lava Dome Lower Moon Helm Room - Right Entrance', 'id': 236, 'area': 60, 'coordinates': [11, 21], 'teleporter': [125, 0]}, {'name': 'Lava Dome Moon Helm Room - Entrance', 'id': 237, 'area': 60, 'coordinates': [15, 23], 'teleporter': [126, 0]}, {'name': 'Lava Dome Three Jumps Room - To Main Loop', 'id': 238, 'area': 61, 'coordinates': [58, 15], 'teleporter': [127, 0]}, {'name': 'Lava Dome Life Chest Room - Lower South Entrance', 'id': 239, 'area': 61, 'coordinates': [38, 27], 'teleporter': [128, 0]}, {'name': 'Lava Dome Life Chest Room - Upper South Entrance', 'id': 240, 'area': 61, 'coordinates': [28, 23], 'teleporter': [129, 0]}, {'name': 'Lava Dome Big Jump Room - Left Entrance', 'id': 241, 'area': 62, 'coordinates': [42, 51], 'teleporter': [133, 0]}, {'name': 'Lava Dome Big Jump Room - North Entrance', 'id': 242, 'area': 62, 'coordinates': [30, 29], 'teleporter': [131, 0]}, {'name': 'Lava Dome Big Jump Room - Lower Right Stairs', 'id': 243, 'area': 62, 'coordinates': [61, 59], 'teleporter': [132, 0]}, {'name': 'Lava Dome Split Corridor - Upper Stairs', 'id': 244, 'area': 62, 'coordinates': [30, 43], 'teleporter': [130, 0]}, {'name': 'Lava Dome Split Corridor - Lower Stairs', 'id': 245, 'area': 62, 'coordinates': [36, 61], 'teleporter': [134, 0]}, {'name': 'Lava Dome Plate Corridor - Right Entrance', 'id': 246, 'area': 63, 'coordinates': [19, 29], 'teleporter': [135, 0]}, {'name': 'Lava Dome Plate Corridor - Left Entrance', 'id': 247, 'area': 63, 'coordinates': [60, 21], 'teleporter': [137, 0]}, {'name': 'Lava Dome Four Boxes Stairs - Upper Entrance', 'id': 248, 'area': 63, 'coordinates': [22, 3], 'teleporter': [136, 0]}, {'name': 'Lava Dome Four Boxes Stairs - Lower Entrance', 'id': 249, 'area': 63, 'coordinates': [22, 17], 'teleporter': [16, 0]}, {'name': 'Lava Dome Hydra Room - South Entrance', 'id': 250, 'area': 64, 'coordinates': [14, 59], 'teleporter': [105, 3]}, {'name': 'Lava Dome Hydra Room - North Exit', 'id': 251, 'area': 64, 'coordinates': [25, 31], 'teleporter': [138, 0]}, {'name': 'Lava Dome Hydra Room - Hydra Script', 'id': 252, 'area': 64, 'coordinates': [14, 36], 'teleporter': [14, 8]}, {'name': 'Lava Dome Escape Corridor - South Entrance', 'id': 253, 'area': 65, 'coordinates': [22, 17], 'teleporter': [139, 0]}, {'name': 'Lava Dome Escape Corridor - North Entrance', 'id': 254, 'area': 65, 'coordinates': [22, 3], 'teleporter': [9, 0]}, {'name': 'Rope Bridge - West Entrance 1', 'id': 255, 'area': 66, 'coordinates': [3, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 2', 'id': 256, 'area': 66, 'coordinates': [3, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 3', 'id': 257, 'area': 66, 'coordinates': [3, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 4', 'id': 258, 'area': 66, 'coordinates': [3, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 5', 'id': 259, 'area': 66, 'coordinates': [4, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 6', 'id': 260, 'area': 66, 'coordinates': [4, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 7', 'id': 261, 'area': 66, 'coordinates': [4, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 8', 'id': 262, 'area': 66, 'coordinates': [4, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 1', 'id': 263, 'area': 66, 'coordinates': [59, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 2', 'id': 264, 'area': 66, 'coordinates': [59, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 3', 'id': 265, 'area': 66, 'coordinates': [59, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 4', 'id': 266, 'area': 66, 'coordinates': [59, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 5', 'id': 267, 'area': 66, 'coordinates': [60, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 6', 'id': 268, 'area': 66, 'coordinates': [60, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 7', 'id': 269, 'area': 66, 'coordinates': [60, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 8', 'id': 270, 'area': 66, 'coordinates': [60, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - Reuben Fall Script', 'id': 271, 'area': 66, 'coordinates': [13, 12], 'teleporter': [15, 8]}, {'name': 'Alive Forest - West Entrance 1', 'id': 272, 'area': 67, 'coordinates': [8, 13], 'teleporter': [142, 0]}, {'name': 'Alive Forest - West Entrance 2', 'id': 273, 'area': 67, 'coordinates': [9, 13], 'teleporter': [142, 0]}, {'name': 'Alive Forest - Giant Tree Entrance', 'id': 274, 'area': 67, 'coordinates': [42, 42], 'teleporter': [143, 0]}, {'name': 'Alive Forest - Libra Teleporter Script', 'id': 275, 'area': 67, 'coordinates': [8, 52], 'teleporter': [64, 8]}, {'name': 'Alive Forest - Gemini Teleporter Script', 'id': 276, 'area': 67, 'coordinates': [57, 49], 'teleporter': [65, 8]}, {'name': 'Alive Forest - Mobius Teleporter Script', 'id': 277, 'area': 67, 'coordinates': [24, 10], 'teleporter': [66, 8]}, {'name': 'Giant Tree 1F - Entrance Script 1', 'id': 278, 'area': 68, 'coordinates': [18, 31], 'teleporter': [56, 1]}, {'name': 'Giant Tree 1F - Entrance Script 2', 'id': 279, 'area': 68, 'coordinates': [19, 31], 'teleporter': [56, 1]}, {'name': 'Giant Tree 1F - North Entrance To 2F', 'id': 280, 'area': 68, 'coordinates': [16, 1], 'teleporter': [144, 0]}, {'name': 'Giant Tree 2F Main Lobby - North Entrance to 1F', 'id': 281, 'area': 69, 'coordinates': [44, 33], 'teleporter': [145, 0]}, {'name': 'Giant Tree 2F Main Lobby - Central Entrance to 3F', 'id': 282, 'area': 69, 'coordinates': [42, 47], 'teleporter': [146, 0]}, {'name': 'Giant Tree 2F Main Lobby - West Entrance to Mushroom Room', 'id': 283, 'area': 69, 'coordinates': [58, 49], 'teleporter': [149, 0]}, {'name': 'Giant Tree 2F West Ledge - To 3F Northwest Ledge', 'id': 284, 'area': 69, 'coordinates': [34, 37], 'teleporter': [147, 0]}, {'name': 'Giant Tree 2F Fall From Vine Script', 'id': 482, 'area': 69, 'coordinates': [46, 51], 'teleporter': [76, 8]}, {'name': 'Giant Tree Meteor Chest Room - To 2F Mushroom Room', 'id': 285, 'area': 69, 'coordinates': [58, 44], 'teleporter': [148, 0]}, {'name': 'Giant Tree 2F Mushroom Room - Entrance', 'id': 286, 'area': 70, 'coordinates': [55, 18], 'teleporter': [150, 0]}, {'name': 'Giant Tree 2F Mushroom Room - North Face to Meteor', 'id': 287, 'area': 70, 'coordinates': [56, 7], 'teleporter': [151, 0]}, {'name': 'Giant Tree 3F Central Room - Central Entrance to 2F', 'id': 288, 'area': 71, 'coordinates': [46, 53], 'teleporter': [152, 0]}, {'name': 'Giant Tree 3F Central Room - East Entrance to Worm Room', 'id': 289, 'area': 71, 'coordinates': [58, 39], 'teleporter': [153, 0]}, {'name': 'Giant Tree 3F Lower Corridor - Entrance from Worm Room', 'id': 290, 'area': 71, 'coordinates': [45, 39], 'teleporter': [154, 0]}, {'name': 'Giant Tree 3F West Platform - Lower Entrance', 'id': 291, 'area': 71, 'coordinates': [33, 43], 'teleporter': [155, 0]}, {'name': 'Giant Tree 3F West Platform - Top Entrance', 'id': 292, 'area': 71, 'coordinates': [52, 25], 'teleporter': [156, 0]}, {'name': 'Giant Tree Worm Room - East Entrance', 'id': 293, 'area': 72, 'coordinates': [20, 58], 'teleporter': [157, 0]}, {'name': 'Giant Tree Worm Room - West Entrance', 'id': 294, 'area': 72, 'coordinates': [6, 56], 'teleporter': [158, 0]}, {'name': 'Giant Tree 4F Lower Floor - Entrance', 'id': 295, 'area': 73, 'coordinates': [20, 7], 'teleporter': [159, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower West Mouth', 'id': 296, 'area': 73, 'coordinates': [8, 23], 'teleporter': [160, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower Central Mouth', 'id': 297, 'area': 73, 'coordinates': [14, 25], 'teleporter': [161, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower East Mouth', 'id': 298, 'area': 73, 'coordinates': [20, 25], 'teleporter': [162, 0]}, {'name': 'Giant Tree 4F Upper Floor - Upper West Mouth', 'id': 299, 'area': 73, 'coordinates': [8, 19], 'teleporter': [163, 0]}, {'name': 'Giant Tree 4F Upper Floor - Upper Central Mouth', 'id': 300, 'area': 73, 'coordinates': [12, 17], 'teleporter': [164, 0]}, {'name': 'Giant Tree 4F Slime Room - Exit', 'id': 301, 'area': 74, 'coordinates': [47, 10], 'teleporter': [165, 0]}, {'name': 'Giant Tree 4F Slime Room - West Entrance', 'id': 302, 'area': 74, 'coordinates': [45, 24], 'teleporter': [166, 0]}, {'name': 'Giant Tree 4F Slime Room - Central Entrance', 'id': 303, 'area': 74, 'coordinates': [50, 24], 'teleporter': [167, 0]}, {'name': 'Giant Tree 4F Slime Room - East Entrance', 'id': 304, 'area': 74, 'coordinates': [57, 28], 'teleporter': [168, 0]}, {'name': 'Giant Tree 5F - Entrance', 'id': 305, 'area': 75, 'coordinates': [14, 51], 'teleporter': [169, 0]}, {'name': 'Giant Tree 5F - Giant Tree Face', 'id': 306, 'area': 75, 'coordinates': [14, 37], 'teleporter': [170, 0]}, {'name': 'Kaidge Temple - Entrance', 'id': 307, 'area': 77, 'coordinates': [44, 63], 'teleporter': [18, 6]}, {'name': 'Kaidge Temple - Mobius Teleporter Script', 'id': 308, 'area': 77, 'coordinates': [35, 57], 'teleporter': [71, 8]}, {'name': 'Windhole Temple - Entrance', 'id': 309, 'area': 78, 'coordinates': [10, 29], 'teleporter': [173, 0]}, {'name': 'Mount Gale - Entrance 1', 'id': 310, 'area': 79, 'coordinates': [1, 45], 'teleporter': [174, 0]}, {'name': 'Mount Gale - Entrance 2', 'id': 311, 'area': 79, 'coordinates': [2, 45], 'teleporter': [174, 0]}, {'name': 'Mount Gale - Visit Quest', 'id': 494, 'area': 79, 'coordinates': [44, 7], 'teleporter': [101, 8]}, {'name': 'Windia - Main Entrance 1', 'id': 312, 'area': 80, 'coordinates': [12, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 2', 'id': 313, 'area': 80, 'coordinates': [13, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 3', 'id': 314, 'area': 80, 'coordinates': [14, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 4', 'id': 315, 'area': 80, 'coordinates': [15, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 5', 'id': 316, 'area': 80, 'coordinates': [12, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 6', 'id': 317, 'area': 80, 'coordinates': [13, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 7', 'id': 318, 'area': 80, 'coordinates': [14, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 8', 'id': 319, 'area': 80, 'coordinates': [15, 41], 'teleporter': [10, 6]}, {'name': "Windia - Otto's House", 'id': 320, 'area': 80, 'coordinates': [21, 39], 'teleporter': [30, 5]}, {'name': "Windia - INN's Script", 'id': 321, 'area': 80, 'coordinates': [18, 34], 'teleporter': [97, 8]}, {'name': 'Windia - Vendor House', 'id': 322, 'area': 80, 'coordinates': [8, 36], 'teleporter': [32, 5]}, {'name': 'Windia - Kid House', 'id': 323, 'area': 80, 'coordinates': [7, 23], 'teleporter': [176, 4]}, {'name': 'Windia - Old People House', 'id': 324, 'area': 80, 'coordinates': [19, 21], 'teleporter': [177, 4]}, {'name': 'Windia - Rainbow Bridge Script', 'id': 325, 'area': 80, 'coordinates': [21, 9], 'teleporter': [10, 6]}, {'name': "Otto's House - Attic Stairs", 'id': 326, 'area': 81, 'coordinates': [2, 19], 'teleporter': [33, 2]}, {'name': "Otto's House - Entrance", 'id': 327, 'area': 81, 'coordinates': [9, 30], 'teleporter': [106, 3]}, {'name': "Otto's Attic - Stairs", 'id': 328, 'area': 81, 'coordinates': [26, 23], 'teleporter': [107, 3]}, {'name': 'Windia Kid House - Entrance Script', 'id': 329, 'area': 82, 'coordinates': [7, 10], 'teleporter': [178, 0]}, {'name': 'Windia Kid House - Basement Stairs', 'id': 330, 'area': 82, 'coordinates': [1, 4], 'teleporter': [180, 0]}, {'name': 'Windia Old People House - Entrance', 'id': 331, 'area': 82, 'coordinates': [55, 12], 'teleporter': [179, 0]}, {'name': 'Windia Old People House - Basement Stairs', 'id': 332, 'area': 82, 'coordinates': [60, 5], 'teleporter': [181, 0]}, {'name': 'Windia Kid House Basement - Stairs', 'id': 333, 'area': 82, 'coordinates': [43, 8], 'teleporter': [182, 0]}, {'name': 'Windia Kid House Basement - Mobius Teleporter', 'id': 334, 'area': 82, 'coordinates': [41, 9], 'teleporter': [44, 8]}, {'name': 'Windia Old People House Basement - Stairs', 'id': 335, 'area': 82, 'coordinates': [39, 26], 'teleporter': [183, 0]}, {'name': 'Windia Old People House Basement - Mobius Teleporter Script', 'id': 336, 'area': 82, 'coordinates': [39, 23], 'teleporter': [43, 8]}, {'name': 'Windia Inn Lobby - Stairs to Beds', 'id': 337, 'area': 82, 'coordinates': [45, 24], 'teleporter': [102, 8]}, {'name': 'Windia Inn Lobby - Exit', 'id': 338, 'area': 82, 'coordinates': [53, 30], 'teleporter': [135, 3]}, {'name': 'Windia Inn Beds - Stairs to Lobby', 'id': 339, 'area': 82, 'coordinates': [33, 59], 'teleporter': [216, 0]}, {'name': 'Windia Vendor House - Entrance', 'id': 340, 'area': 82, 'coordinates': [29, 14], 'teleporter': [108, 3]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 1', 'id': 341, 'area': 83, 'coordinates': [47, 29], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 2', 'id': 342, 'area': 83, 'coordinates': [47, 30], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 3', 'id': 343, 'area': 83, 'coordinates': [48, 29], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 4', 'id': 344, 'area': 83, 'coordinates': [48, 30], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - East Entrance', 'id': 345, 'area': 83, 'coordinates': [55, 12], 'teleporter': [185, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - South Stairs', 'id': 346, 'area': 83, 'coordinates': [51, 25], 'teleporter': [186, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Pazuzu Script 1', 'id': 347, 'area': 83, 'coordinates': [47, 8], 'teleporter': [16, 8]}, {'name': 'Pazuzu Tower 1F Main Lobby - Pazuzu Script 2', 'id': 348, 'area': 83, 'coordinates': [48, 8], 'teleporter': [16, 8]}, {'name': 'Pazuzu Tower 1F Boxes Room - West Stairs', 'id': 349, 'area': 83, 'coordinates': [38, 17], 'teleporter': [187, 0]}, {'name': 'Pazuzu 2F - West Upper Stairs', 'id': 350, 'area': 84, 'coordinates': [7, 11], 'teleporter': [188, 0]}, {'name': 'Pazuzu 2F - South Stairs', 'id': 351, 'area': 84, 'coordinates': [20, 24], 'teleporter': [189, 0]}, {'name': 'Pazuzu 2F - West Lower Stairs', 'id': 352, 'area': 84, 'coordinates': [6, 17], 'teleporter': [190, 0]}, {'name': 'Pazuzu 2F - Central Stairs', 'id': 353, 'area': 84, 'coordinates': [15, 15], 'teleporter': [191, 0]}, {'name': 'Pazuzu 2F - Pazuzu 1', 'id': 354, 'area': 84, 'coordinates': [15, 8], 'teleporter': [17, 8]}, {'name': 'Pazuzu 2F - Pazuzu 2', 'id': 355, 'area': 84, 'coordinates': [16, 8], 'teleporter': [17, 8]}, {'name': 'Pazuzu 3F Main Room - North Stairs', 'id': 356, 'area': 85, 'coordinates': [23, 11], 'teleporter': [192, 0]}, {'name': 'Pazuzu 3F Main Room - West Stairs', 'id': 357, 'area': 85, 'coordinates': [7, 15], 'teleporter': [193, 0]}, {'name': 'Pazuzu 3F Main Room - Pazuzu Script 1', 'id': 358, 'area': 85, 'coordinates': [15, 8], 'teleporter': [18, 8]}, {'name': 'Pazuzu 3F Main Room - Pazuzu Script 2', 'id': 359, 'area': 85, 'coordinates': [16, 8], 'teleporter': [18, 8]}, {'name': 'Pazuzu 3F Central Island - Central Stairs', 'id': 360, 'area': 85, 'coordinates': [15, 14], 'teleporter': [194, 0]}, {'name': 'Pazuzu 3F Central Island - South Stairs', 'id': 361, 'area': 85, 'coordinates': [17, 25], 'teleporter': [195, 0]}, {'name': 'Pazuzu 4F - Northwest Stairs', 'id': 362, 'area': 86, 'coordinates': [39, 12], 'teleporter': [196, 0]}, {'name': 'Pazuzu 4F - Southwest Stairs', 'id': 363, 'area': 86, 'coordinates': [39, 19], 'teleporter': [197, 0]}, {'name': 'Pazuzu 4F - South Stairs', 'id': 364, 'area': 86, 'coordinates': [47, 24], 'teleporter': [198, 0]}, {'name': 'Pazuzu 4F - Northeast Stairs', 'id': 365, 'area': 86, 'coordinates': [54, 9], 'teleporter': [199, 0]}, {'name': 'Pazuzu 4F - Pazuzu Script 1', 'id': 366, 'area': 86, 'coordinates': [47, 8], 'teleporter': [19, 8]}, {'name': 'Pazuzu 4F - Pazuzu Script 2', 'id': 367, 'area': 86, 'coordinates': [48, 8], 'teleporter': [19, 8]}, {'name': 'Pazuzu 5F Pazuzu Loop - West Stairs', 'id': 368, 'area': 87, 'coordinates': [9, 49], 'teleporter': [200, 0]}, {'name': 'Pazuzu 5F Pazuzu Loop - South Stairs', 'id': 369, 'area': 87, 'coordinates': [16, 55], 'teleporter': [201, 0]}, {'name': 'Pazuzu 5F Upper Loop - Northeast Stairs', 'id': 370, 'area': 87, 'coordinates': [22, 40], 'teleporter': [202, 0]}, {'name': 'Pazuzu 5F Upper Loop - Northwest Stairs', 'id': 371, 'area': 87, 'coordinates': [9, 40], 'teleporter': [203, 0]}, {'name': 'Pazuzu 5F Upper Loop - Pazuzu Script 1', 'id': 372, 'area': 87, 'coordinates': [15, 40], 'teleporter': [20, 8]}, {'name': 'Pazuzu 5F Upper Loop - Pazuzu Script 2', 'id': 373, 'area': 87, 'coordinates': [16, 40], 'teleporter': [20, 8]}, {'name': 'Pazuzu 6F - West Stairs', 'id': 374, 'area': 88, 'coordinates': [41, 47], 'teleporter': [204, 0]}, {'name': 'Pazuzu 6F - Northwest Stairs', 'id': 375, 'area': 88, 'coordinates': [41, 40], 'teleporter': [205, 0]}, {'name': 'Pazuzu 6F - Northeast Stairs', 'id': 376, 'area': 88, 'coordinates': [54, 40], 'teleporter': [206, 0]}, {'name': 'Pazuzu 6F - South Stairs', 'id': 377, 'area': 88, 'coordinates': [52, 56], 'teleporter': [207, 0]}, {'name': 'Pazuzu 6F - Pazuzu Script 1', 'id': 378, 'area': 88, 'coordinates': [47, 40], 'teleporter': [21, 8]}, {'name': 'Pazuzu 6F - Pazuzu Script 2', 'id': 379, 'area': 88, 'coordinates': [48, 40], 'teleporter': [21, 8]}, {'name': 'Pazuzu 7F Main Room - Southwest Stairs', 'id': 380, 'area': 89, 'coordinates': [15, 54], 'teleporter': [26, 0]}, {'name': 'Pazuzu 7F Main Room - Northeast Stairs', 'id': 381, 'area': 89, 'coordinates': [21, 40], 'teleporter': [27, 0]}, {'name': 'Pazuzu 7F Main Room - Southeast Stairs', 'id': 382, 'area': 89, 'coordinates': [21, 56], 'teleporter': [28, 0]}, {'name': 'Pazuzu 7F Main Room - Pazuzu Script 1', 'id': 383, 'area': 89, 'coordinates': [15, 44], 'teleporter': [22, 8]}, {'name': 'Pazuzu 7F Main Room - Pazuzu Script 2', 'id': 384, 'area': 89, 'coordinates': [16, 44], 'teleporter': [22, 8]}, {'name': 'Pazuzu 7F Main Room - Crystal Script', 'id': 480, 'area': 89, 'coordinates': [15, 40], 'teleporter': [38, 8]}, {'name': 'Pazuzu 1F to 3F - South Stairs', 'id': 385, 'area': 90, 'coordinates': [43, 60], 'teleporter': [29, 0]}, {'name': 'Pazuzu 1F to 3F - North Stairs', 'id': 386, 'area': 90, 'coordinates': [43, 36], 'teleporter': [30, 0]}, {'name': 'Pazuzu 3F to 5F - South Stairs', 'id': 387, 'area': 91, 'coordinates': [43, 60], 'teleporter': [40, 0]}, {'name': 'Pazuzu 3F to 5F - North Stairs', 'id': 388, 'area': 91, 'coordinates': [43, 36], 'teleporter': [41, 0]}, {'name': 'Pazuzu 5F to 7F - South Stairs', 'id': 389, 'area': 92, 'coordinates': [43, 60], 'teleporter': [38, 0]}, {'name': 'Pazuzu 5F to 7F - North Stairs', 'id': 390, 'area': 92, 'coordinates': [43, 36], 'teleporter': [39, 0]}, {'name': 'Pazuzu 2F to 4F - South Stairs', 'id': 391, 'area': 93, 'coordinates': [43, 60], 'teleporter': [21, 0]}, {'name': 'Pazuzu 2F to 4F - North Stairs', 'id': 392, 'area': 93, 'coordinates': [43, 36], 'teleporter': [22, 0]}, {'name': 'Pazuzu 4F to 6F - South Stairs', 'id': 393, 'area': 94, 'coordinates': [43, 60], 'teleporter': [2, 0]}, {'name': 'Pazuzu 4F to 6F - North Stairs', 'id': 394, 'area': 94, 'coordinates': [43, 36], 'teleporter': [3, 0]}, {'name': 'Light Temple - Entrance', 'id': 395, 'area': 95, 'coordinates': [28, 57], 'teleporter': [19, 6]}, {'name': 'Light Temple - Mobius Teleporter Script', 'id': 396, 'area': 95, 'coordinates': [29, 37], 'teleporter': [70, 8]}, {'name': 'Light Temple - Visit Quest Script 1', 'id': 492, 'area': 95, 'coordinates': [34, 39], 'teleporter': [100, 8]}, {'name': 'Light Temple - Visit Quest Script 2', 'id': 493, 'area': 95, 'coordinates': [35, 39], 'teleporter': [100, 8]}, {'name': 'Ship Dock - Mobius Teleporter Script', 'id': 397, 'area': 96, 'coordinates': [15, 18], 'teleporter': [61, 8]}, {'name': 'Ship Dock - From Overworld', 'id': 398, 'area': 96, 'coordinates': [15, 11], 'teleporter': [73, 0]}, {'name': 'Ship Dock - Entrance', 'id': 399, 'area': 96, 'coordinates': [15, 23], 'teleporter': [17, 6]}, {'name': 'Mac Ship Deck - East Entrance Script', 'id': 400, 'area': 97, 'coordinates': [26, 40], 'teleporter': [37, 8]}, {'name': 'Mac Ship Deck - Central Stairs Script', 'id': 401, 'area': 97, 'coordinates': [16, 47], 'teleporter': [50, 8]}, {'name': 'Mac Ship Deck - West Stairs Script', 'id': 402, 'area': 97, 'coordinates': [8, 34], 'teleporter': [51, 8]}, {'name': 'Mac Ship Deck - East Stairs Script', 'id': 403, 'area': 97, 'coordinates': [24, 36], 'teleporter': [52, 8]}, {'name': 'Mac Ship Deck - North Stairs Script', 'id': 404, 'area': 97, 'coordinates': [12, 9], 'teleporter': [53, 8]}, {'name': 'Mac Ship B1 Outer Ring - South Stairs', 'id': 405, 'area': 98, 'coordinates': [16, 45], 'teleporter': [208, 0]}, {'name': 'Mac Ship B1 Outer Ring - West Stairs', 'id': 406, 'area': 98, 'coordinates': [8, 35], 'teleporter': [175, 0]}, {'name': 'Mac Ship B1 Outer Ring - East Stairs', 'id': 407, 'area': 98, 'coordinates': [25, 37], 'teleporter': [172, 0]}, {'name': 'Mac Ship B1 Outer Ring - Northwest Stairs', 'id': 408, 'area': 98, 'coordinates': [10, 23], 'teleporter': [88, 0]}, {'name': 'Mac Ship B1 Square Room - North Stairs', 'id': 409, 'area': 98, 'coordinates': [14, 9], 'teleporter': [141, 0]}, {'name': 'Mac Ship B1 Square Room - South Stairs', 'id': 410, 'area': 98, 'coordinates': [16, 12], 'teleporter': [87, 0]}, {'name': 'Mac Ship B1 Mac Room - Stairs', 'id': 411, 'area': 98, 'coordinates': [16, 51], 'teleporter': [101, 0]}, {'name': 'Mac Ship B1 Central Corridor - South Stairs', 'id': 412, 'area': 98, 'coordinates': [16, 38], 'teleporter': [102, 0]}, {'name': 'Mac Ship B1 Central Corridor - North Stairs', 'id': 413, 'area': 98, 'coordinates': [16, 26], 'teleporter': [86, 0]}, {'name': 'Mac Ship B2 South Corridor - South Stairs', 'id': 414, 'area': 99, 'coordinates': [48, 51], 'teleporter': [57, 1]}, {'name': 'Mac Ship B2 South Corridor - North Stairs Script', 'id': 415, 'area': 99, 'coordinates': [48, 38], 'teleporter': [55, 8]}, {'name': 'Mac Ship B2 North Corridor - South Stairs Script', 'id': 416, 'area': 99, 'coordinates': [48, 27], 'teleporter': [56, 8]}, {'name': 'Mac Ship B2 North Corridor - North Stairs Script', 'id': 417, 'area': 99, 'coordinates': [48, 12], 'teleporter': [57, 8]}, {'name': 'Mac Ship B2 Outer Ring - Northwest Stairs Script', 'id': 418, 'area': 99, 'coordinates': [55, 11], 'teleporter': [58, 8]}, {'name': 'Mac Ship B1 Outer Ring Cleared - South Stairs', 'id': 419, 'area': 100, 'coordinates': [16, 45], 'teleporter': [208, 0]}, {'name': 'Mac Ship B1 Outer Ring Cleared - West Stairs', 'id': 420, 'area': 100, 'coordinates': [8, 35], 'teleporter': [175, 0]}, {'name': 'Mac Ship B1 Outer Ring Cleared - East Stairs', 'id': 421, 'area': 100, 'coordinates': [25, 37], 'teleporter': [172, 0]}, {'name': 'Mac Ship B1 Square Room Cleared - North Stairs', 'id': 422, 'area': 100, 'coordinates': [14, 9], 'teleporter': [141, 0]}, {'name': 'Mac Ship B1 Square Room Cleared - South Stairs', 'id': 423, 'area': 100, 'coordinates': [16, 12], 'teleporter': [87, 0]}, {'name': 'Mac Ship B1 Mac Room Cleared - Main Stairs', 'id': 424, 'area': 100, 'coordinates': [16, 51], 'teleporter': [101, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - South Stairs', 'id': 425, 'area': 100, 'coordinates': [16, 38], 'teleporter': [102, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - North Stairs', 'id': 426, 'area': 100, 'coordinates': [16, 26], 'teleporter': [86, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - Northwest Stairs', 'id': 427, 'area': 100, 'coordinates': [23, 10], 'teleporter': [88, 0]}, {'name': 'Doom Castle Corridor of Destiny - South Entrance', 'id': 428, 'area': 101, 'coordinates': [59, 29], 'teleporter': [84, 0]}, {'name': 'Doom Castle Corridor of Destiny - Ice Floor Entrance', 'id': 429, 'area': 101, 'coordinates': [59, 21], 'teleporter': [35, 2]}, {'name': 'Doom Castle Corridor of Destiny - Lava Floor Entrance', 'id': 430, 'area': 101, 'coordinates': [59, 13], 'teleporter': [209, 0]}, {'name': 'Doom Castle Corridor of Destiny - Sky Floor Entrance', 'id': 431, 'area': 101, 'coordinates': [59, 5], 'teleporter': [211, 0]}, {'name': 'Doom Castle Corridor of Destiny - Hero Room Entrance', 'id': 432, 'area': 101, 'coordinates': [59, 61], 'teleporter': [13, 2]}, {'name': 'Doom Castle Ice Floor - Entrance', 'id': 433, 'area': 102, 'coordinates': [23, 42], 'teleporter': [109, 3]}, {'name': 'Doom Castle Lava Floor - Entrance', 'id': 434, 'area': 103, 'coordinates': [23, 40], 'teleporter': [210, 0]}, {'name': 'Doom Castle Sky Floor - Entrance', 'id': 435, 'area': 104, 'coordinates': [24, 41], 'teleporter': [212, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 1', 'id': 436, 'area': 106, 'coordinates': [15, 5], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 2', 'id': 437, 'area': 106, 'coordinates': [16, 5], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 3', 'id': 438, 'area': 106, 'coordinates': [15, 4], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 4', 'id': 439, 'area': 106, 'coordinates': [16, 4], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Hero Statue Script', 'id': 440, 'area': 106, 'coordinates': [15, 17], 'teleporter': [24, 8]}, {'name': 'Doom Castle Hero Room - Entrance', 'id': 441, 'area': 106, 'coordinates': [15, 24], 'teleporter': [110, 3]}, {'name': 'Doom Castle Dark King Room - Entrance', 'id': 442, 'area': 107, 'coordinates': [14, 26], 'teleporter': [52, 0]}, {'name': 'Doom Castle Dark King Room - Dark King Script', 'id': 443, 'area': 107, 'coordinates': [14, 15], 'teleporter': [25, 8]}, {'name': 'Doom Castle Dark King Room - Unknown', 'id': 444, 'area': 107, 'coordinates': [47, 54], 'teleporter': [77, 0]}, {'name': 'Overworld - Level Forest', 'id': 445, 'area': 0, 'type': 'Overworld', 'teleporter': [46, 8]}, {'name': 'Overworld - Foresta', 'id': 446, 'area': 0, 'type': 'Overworld', 'teleporter': [2, 1]}, {'name': 'Overworld - Sand Temple', 'id': 447, 'area': 0, 'type': 'Overworld', 'teleporter': [3, 1]}, {'name': 'Overworld - Bone Dungeon', 'id': 448, 'area': 0, 'type': 'Overworld', 'teleporter': [4, 1]}, {'name': 'Overworld - Focus Tower Foresta', 'id': 449, 'area': 0, 'type': 'Overworld', 'teleporter': [5, 1]}, {'name': 'Overworld - Focus Tower Aquaria', 'id': 450, 'area': 0, 'type': 'Overworld', 'teleporter': [19, 1]}, {'name': 'Overworld - Libra Temple', 'id': 451, 'area': 0, 'type': 'Overworld', 'teleporter': [7, 1]}, {'name': 'Overworld - Aquaria', 'id': 452, 'area': 0, 'type': 'Overworld', 'teleporter': [8, 8]}, {'name': 'Overworld - Wintry Cave', 'id': 453, 'area': 0, 'type': 'Overworld', 'teleporter': [10, 1]}, {'name': 'Overworld - Life Temple', 'id': 454, 'area': 0, 'type': 'Overworld', 'teleporter': [11, 1]}, {'name': 'Overworld - Falls Basin', 'id': 455, 'area': 0, 'type': 'Overworld', 'teleporter': [12, 1]}, {'name': 'Overworld - Ice Pyramid', 'id': 456, 'area': 0, 'type': 'Overworld', 'teleporter': [13, 1]}, {'name': "Overworld - Spencer's Place", 'id': 457, 'area': 0, 'type': 'Overworld', 'teleporter': [48, 8]}, {'name': 'Overworld - Wintry Temple', 'id': 458, 'area': 0, 'type': 'Overworld', 'teleporter': [16, 1]}, {'name': 'Overworld - Focus Tower Frozen Strip', 'id': 459, 'area': 0, 'type': 'Overworld', 'teleporter': [17, 1]}, {'name': 'Overworld - Focus Tower Fireburg', 'id': 460, 'area': 0, 'type': 'Overworld', 'teleporter': [18, 1]}, {'name': 'Overworld - Fireburg', 'id': 461, 'area': 0, 'type': 'Overworld', 'teleporter': [20, 1]}, {'name': 'Overworld - Mine', 'id': 462, 'area': 0, 'type': 'Overworld', 'teleporter': [21, 1]}, {'name': 'Overworld - Sealed Temple', 'id': 463, 'area': 0, 'type': 'Overworld', 'teleporter': [22, 1]}, {'name': 'Overworld - Volcano', 'id': 464, 'area': 0, 'type': 'Overworld', 'teleporter': [23, 1]}, {'name': 'Overworld - Lava Dome', 'id': 465, 'area': 0, 'type': 'Overworld', 'teleporter': [24, 1]}, {'name': 'Overworld - Focus Tower Windia', 'id': 466, 'area': 0, 'type': 'Overworld', 'teleporter': [6, 1]}, {'name': 'Overworld - Rope Bridge', 'id': 467, 'area': 0, 'type': 'Overworld', 'teleporter': [25, 1]}, {'name': 'Overworld - Alive Forest', 'id': 468, 'area': 0, 'type': 'Overworld', 'teleporter': [26, 1]}, {'name': 'Overworld - Giant Tree', 'id': 469, 'area': 0, 'type': 'Overworld', 'teleporter': [27, 1]}, {'name': 'Overworld - Kaidge Temple', 'id': 470, 'area': 0, 'type': 'Overworld', 'teleporter': [28, 1]}, {'name': 'Overworld - Windia', 'id': 471, 'area': 0, 'type': 'Overworld', 'teleporter': [29, 1]}, {'name': 'Overworld - Windhole Temple', 'id': 472, 'area': 0, 'type': 'Overworld', 'teleporter': [30, 1]}, {'name': 'Overworld - Mount Gale', 'id': 473, 'area': 0, 'type': 'Overworld', 'teleporter': [31, 1]}, {'name': 'Overworld - Pazuzu Tower', 'id': 474, 'area': 0, 'type': 'Overworld', 'teleporter': [32, 1]}, {'name': 'Overworld - Ship Dock', 'id': 475, 'area': 0, 'type': 'Overworld', 'teleporter': [62, 1]}, {'name': 'Overworld - Doom Castle', 'id': 476, 'area': 0, 'type': 'Overworld', 'teleporter': [33, 1]}, {'name': 'Overworld - Light Temple', 'id': 477, 'area': 0, 'type': 'Overworld', 'teleporter': [34, 1]}, {'name': 'Overworld - Mac Ship', 'id': 478, 'area': 0, 'type': 'Overworld', 'teleporter': [36, 1]}, {'name': 'Overworld - Mac Ship Doom', 'id': 479, 'area': 0, 'type': 'Overworld', 'teleporter': [36, 1]}, {'name': 'Dummy House - Bed Script', 'id': 480, 'area': 17, 'coordinates': [40, 56], 'teleporter': [1, 8]}, {'name': 'Dummy House - Entrance', 'id': 481, 'area': 17, 'coordinates': [41, 59], 'teleporter': [0, 10]}]
\ No newline at end of file
diff --git a/worlds/ffmq/data/settings.yaml b/worlds/ffmq/data/settings.yaml
new file mode 100644
index 000000000000..826a8c744d93
--- /dev/null
+++ b/worlds/ffmq/data/settings.yaml
@@ -0,0 +1,183 @@
+# YAML Preset file for FFMQR
+Final Fantasy Mystic Quest:
+ enemies_density:
+ All: 0
+ ThreeQuarter: 0
+ Half: 0
+ Quarter: 0
+ None: 0
+ chests_shuffle:
+ Prioritize: 0
+ Include: 0
+ shuffle_boxes_content:
+ true: 0
+ false: 0
+ npcs_shuffle:
+ Prioritize: 0
+ Include: 0
+ Exclude: 0
+ battlefields_shuffle:
+ Prioritize: 0
+ Include: 0
+ Exclude: 0
+ logic_options:
+ Friendly: 0
+ Standard: 0
+ Expert: 0
+ shuffle_enemies_position:
+ true: 0
+ false: 0
+ enemies_scaling_lower:
+ Quarter: 0
+ Half: 0
+ ThreeQuarter: 0
+ Normal: 0
+ OneAndQuarter: 0
+ OneAndHalf: 0
+ Double: 0
+ DoubleAndHalf: 0
+ Triple: 0
+ enemies_scaling_upper:
+ Quarter: 0
+ Half: 0
+ ThreeQuarter: 0
+ Normal: 0
+ OneAndQuarter: 0
+ OneAndHalf: 0
+ Double: 0
+ DoubleAndHalf: 0
+ Triple: 0
+ bosses_scaling_lower:
+ Quarter: 0
+ Half: 0
+ ThreeQuarter: 0
+ Normal: 0
+ OneAndQuarter: 0
+ OneAndHalf: 0
+ Double: 0
+ DoubleAndHalf: 0
+ Triple: 0
+ bosses_scaling_upper:
+ Quarter: 0
+ Half: 0
+ ThreeQuarter: 0
+ Normal: 0
+ OneAndQuarter: 0
+ OneAndHalf: 0
+ Double: 0
+ DoubleAndHalf: 0
+ Triple: 0
+ enemizer_attacks:
+ Normal: 0
+ Safe: 0
+ Chaos: 0
+ SelfDestruct: 0
+ SimpleShuffle: 0
+ enemizer_groups:
+ MobsOnly: 0
+ MobsBosses: 0
+ MobsBossesDK: 0
+ shuffle_res_weak_type:
+ true: 0
+ false: 0
+ leveling_curve:
+ Half: 0
+ Normal: 0
+ OneAndHalf: 0
+ Double: 0
+ DoubleAndHalf: 0
+ Triple: 0
+ Quadruple: 0
+ companion_leveling_type:
+ Quests: 0
+ QuestsExtended: 0
+ SaveCrystalsIndividual: 0
+ SaveCrystalsAll: 0
+ BenPlus0: 0
+ BenPlus5: 0
+ BenPlus10: 0
+ companion_spellbook_type:
+ Standard: 0
+ Extended: 0
+ RandomBalanced: 0
+ RandomChaos: 0
+ starting_companion:
+ None: 0
+ Kaeli: 0
+ Tristam: 0
+ Phoebe: 0
+ Reuben: 0
+ Random: 0
+ RandomPlusNone: 0
+ available_companions:
+ Zero: 0
+ One: 0
+ Two: 0
+ Three: 0
+ Four: 0
+ Random14: 0
+ Random04: 0
+ companions_locations:
+ Standard: 0
+ Shuffled: 0
+ ShuffledExtended: 0
+ kaelis_mom_fight_minotaur:
+ true: 0
+ false: 0
+ battles_quantity:
+ Ten: 0
+ Seven: 0
+ Five: 0
+ Three: 0
+ One: 0
+ RandomHigh: 0
+ RandomLow: 0
+ shuffle_battlefield_rewards:
+ true: 0
+ false: 0
+ random_starting_weapon:
+ true: 0
+ false: 0
+ progressive_gear:
+ true: 0
+ false: 0
+ tweaked_dungeons:
+ true: 0
+ false: 0
+ doom_castle_mode:
+ Standard: 0
+ BossRush: 0
+ DarkKingOnly: 0
+ doom_castle_shortcut:
+ true: 0
+ false: 0
+ sky_coin_mode:
+ Standard: 0
+ StartWith: 0
+ SaveTheCrystals: 0
+ ShatteredSkyCoin: 0
+ sky_coin_fragments_qty:
+ Low16: 0
+ Mid24: 0
+ High32: 0
+ RandomNarrow: 0
+ RandomWide: 0
+ enable_spoilers:
+ true: 0
+ false: 0
+ progressive_formations:
+ Disabled: 0
+ RegionsStrict: 0
+ RegionsKeepType: 0
+ map_shuffling:
+ None: 0
+ Overworld: 0
+ Dungeons: 0
+ OverworldDungeons: 0
+ Everything: 0
+ crest_shuffle:
+ true: 0
+ false: 0
+description: Generated by Archipelago
+game: Final Fantasy Mystic Quest
+name: Player
diff --git a/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md
new file mode 100644
index 000000000000..4e093930739d
--- /dev/null
+++ b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md
@@ -0,0 +1,36 @@
+# Final Fantasy Mystic Quest
+
+## Game page in other languages:
+* [Français](/games/Final%20Fantasy%20Mystic%20Quest/info/fr)
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
+config file.
+
+## What does randomization do to this game?
+
+Besides items being shuffled, you have multiple options for shuffling maps, crest warps, and battlefield locations.
+There are a number of other options for tweaking the difficulty of the game.
+
+## What items and locations get shuffled?
+
+Items received normally through chests, from NPCs, or battlefields are shuffled. Optionally, you may also include
+the items from brown boxes.
+
+## Which items can be in another player's world?
+
+Any of the items which can be shuffled may also be placed into another player's world.
+
+## What does another world's item look like in Final Fantasy Mystic Quest?
+
+For locations that are originally boxes or chests, they will appear as a box if the item in it is categorized as a
+filler item, and a chest if it contains a useful or advancement item. Trap items may randomly appear as a box or chest.
+When opening a chest with an item for another player, you will see the Archipelago icon and it will tell you you've
+found an "Archipelago Item"
+
+## When the player receives an item, what happens?
+
+A dialogue box will open to show you the item you've received. You will not receive items while you are in battle,
+menus, or the overworld (except sometimes when closing the menu).
+
diff --git a/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md b/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md
new file mode 100644
index 000000000000..70c2d938bfc6
--- /dev/null
+++ b/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md
@@ -0,0 +1,36 @@
+# Final Fantasy Mystic Quest
+
+## Page d'info dans d'autres langues :
+* [English](/games/Final%20Fantasy%20Mystic%20Quest/info/en)
+
+## Où se situe la page d'options?
+
+La [page de configuration](../player-options) contient toutes les options nécessaires pour créer un fichier de configuration.
+
+## Qu'est-ce qui est rendu aléatoire dans ce jeu?
+
+Outre les objets mélangés, il y a plusieurs options pour aussi mélanger les villes et donjons, les pièces dans les donjons, les téléporteurs et les champs de bataille.
+Il y a aussi plusieurs autres options afin d'ajuster la difficulté du jeu et la vitesse d'une partie.
+
+## Quels objets et emplacements sont mélangés?
+
+Les objets normalement reçus des coffres rouges, des PNJ et des champs de bataille sont mélangés. Vous pouvez aussi
+inclure les objets des coffres bruns (qui contiennent normalement des consommables) dans les objets mélangés.
+
+## Quels objets peuvent être dans les mondes des autres joueurs?
+
+Tous les objets qui ont été déterminés mélangés dans les options peuvent être placés dans d'autres mondes.
+
+## À quoi ressemblent les objets des autres joueurs dans Final Fantasy Mystic Quest?
+
+Les emplacements qui étaient à l'origine des coffres (rouges ou bruns si ceux-ci sont inclus) apparaîtront comme des coffres.
+Les coffres rouges seront des objets utiles ou de progression, alors que les coffres bruns seront des objets de remplissage.
+Les pièges peuvent apparaître comme des coffres rouges ou bruns.
+Lorsque vous ouvrirez un coffre contenant un objet d'un autre joueur, vous recevrez l'icône d'Archipelago et
+la boîte de dialogue vous indiquera avoir reçu un "Archipelago Item".
+
+
+## Lorsqu'un joueur reçoit un objet, qu'arrive-t-il?
+
+Une boîte de dialogue apparaîtra pour vous montrer l'objet que vous avez reçu. Vous ne pourrez pas recevoir d'objet si vous êtes
+en combat, dans la mappemonde ou dans les menus (Ã l'exception de lorsque vous fermez le menu).
diff --git a/worlds/ffmq/docs/setup_en.md b/worlds/ffmq/docs/setup_en.md
new file mode 100644
index 000000000000..77569c93f0c8
--- /dev/null
+++ b/worlds/ffmq/docs/setup_en.md
@@ -0,0 +1,167 @@
+# Final Fantasy Mystic Quest Setup Guide
+
+## Required Software
+
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
+
+- Hardware or software capable of loading and playing SNES ROM files
+ - An emulator capable of connecting to SNI such as:
+ - snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases),
+ - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html)
+ - RetroArch 1.10.1 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or,
+ - An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other
+ compatible hardware
+
+- Your legally obtained Final Fantasy Mystic Quest NA 1.0 or 1.1 ROM file, probably named `Final Fantasy - Mystic Quest (U) (V1.0).sfc` or `Final Fantasy - Mystic Quest (U) (V1.1).sfc`
+The Archipelago community cannot supply you with this.
+
+## Installation Procedures
+
+### Linux Setup
+
+1. Download and install [Archipelago](). **The installer
+ file is located in the assets section at the bottom of the version information. You'll likely be looking for the `.AppImage`.**
+2. It is recommended to use either RetroArch or BizHawk if you run on linux, as snes9x-rr isn't compatible.
+
+### Windows Setup
+
+1. Download and install [Archipelago](). **The installer
+ file is located in the assets section at the bottom of the version information.**
+2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
+ files.
+ 1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
+ 2. Right-click on a ROM file and select **Open with...**
+ 3. Check the box next to **Always use this app to open .sfc files**
+ 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
+ 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you
+ extracted in step one.
+
+## Create a Config (.yaml) File
+
+### What is a config file and why do I need one?
+
+See the guide on setting up a basic YAML at the Archipelago setup
+guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
+
+### Where do I get a config file?
+
+The Player Options page on the website allows you to configure your personal options and export a config file from
+them. Player options page: [Final Fantasy Mystic Quest Player Options Page](/games/Final%20Fantasy%20Mystic%20Quest/player-options)
+
+### 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)
+
+## Generating a Single-Player Game
+
+1. Navigate to the Player Options page, configure your options, and click the "Generate Game" button.
+ - Player Options page: [Final Fantasy Mystic Quest Player Options Page](/games/Final%20Fantasy%20Mystic%20Quest/player-options)
+2. You will be presented with a "Seed Info" page.
+3. Click the "Create New Room" link.
+4. You will be presented with a server page, from which you can download your `.apmq` patch file.
+5. Go to the [FFMQR website](https://ffmqrando.net/Archipelago) and select your Final Fantasy Mystic Quest ROM
+and the .apmq file you received, choose optional preferences, and click `Generate` to get your patched ROM.
+7. Since this is a single-player game, you will no longer need the client, so feel free to close it.
+
+## Joining a MultiWorld Game
+
+### Obtain your patch file and create your ROM
+
+When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
+the host will provide you with either a link to download your patch file, or with a zip file containing
+everyone's patch files. Your patch file should have a `.apmq` extension.
+
+Go to the [FFMQR website](https://ffmqrando.net/Archipelago) and select your Final Fantasy Mystic Quest ROM
+and the .apmq file you received, choose optional preferences, and click `Generate` to get your patched ROM.
+
+Manually launch the SNI Client, and run the patched ROM in your chosen software or hardware.
+
+### Connect to the client
+
+#### With an emulator
+
+If this is the first time SNI launches, you may be prompted to allow it to communicate through the Windows Firewall.
+
+##### snes9x-rr
+
+1. Load your ROM file if it hasn't already been loaded.
+2. Click on the File menu and hover on **Lua Scripting**
+3. Click on **New Lua Script Window...**
+4. In the new window, click **Browse...**
+5. Select the connector lua file included with your client
+ - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
+ emulator is 64-bit or 32-bit.
+6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
+the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
+
+##### BizHawk
+
+1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these
+ menu options:
+ `Config --> Cores --> SNES --> BSNES`
+ Once you have changed the loaded core, you must restart BizHawk.
+2. Load your ROM file if it hasn't already been loaded.
+3. Click on the Tools menu and click on **Lua Console**
+4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`.
+5. Select the `Connector.lua` file included with your client
+ - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
+ emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
+
+##### RetroArch 1.10.1 or newer
+
+You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.1.
+
+1. Enter the RetroArch main menu screen.
+2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON.
+3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default
+ Network Command Port at 55355.
+
+![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png)
+4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury
+ Performance)".
+
+When loading a ROM, be sure to select a **bsnes-mercury** core. These are the only cores that allow external tools to
+read ROM data.
+
+#### With hardware
+
+This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do
+this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES
+releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases)
+
+Other hardware may find helpful information on the usb2snes platforms
+page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms)
+
+1. Close your emulator, which may have auto-launched.
+2. Power on your device and load the ROM.
+
+### Connect to the Archipelago Server
+
+SNI serves as the interface between your emulator and the server. Since you launched it manually, you need to tell it what server to connect to.
+If the server is hosted on Archipelago.gg, get the port the server hosts your game on at the top of the game room (last line before the worlds are listed).
+In the SNI client, either type `/connect address` (where `address` is the address of the server, for example `/connect archipelago.gg:12345`), or type the address and port on the "Server" input field, then press `Connect`.
+If the server is hosted locally, simply ask the host for the address of the server, and copy/paste it into the "Server" input field then press `Connect`.
+
+The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
+
+### Play the game
+
+When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on
+successfully joining a multiworld game!
+
+## Hosting a MultiWorld game
+
+The recommended way to host a game is to use our hosting service. The process is relatively simple:
+
+1. Collect config files from your players.
+2. Create a zip file containing your players' config files.
+3. Upload that zip file to the Generate page above.
+ - Generate page: [WebHost Seed Generation Page](/generate)
+4. Wait a moment while the seed is generated.
+5. When the seed is generated, you will be redirected to a "Seed Info" page.
+6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so
+ they may download their patch files from there.
+7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all
+ players in the game. Any observers may also be given the link to this page.
+8. Once all players have joined, you may begin playing.
diff --git a/worlds/ffmq/docs/setup_fr.md b/worlds/ffmq/docs/setup_fr.md
new file mode 100644
index 000000000000..12ea41c6b3a0
--- /dev/null
+++ b/worlds/ffmq/docs/setup_fr.md
@@ -0,0 +1,178 @@
+# Final Fantasy Mystic Quest Setup Guide
+
+## Logiciels requis
+
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
+- Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES
+ - Un émulateur capable d'éxécuter des scripts Lua
+ - snes9x-rr de: [snes9x rr](https://github.com/gocha/snes9x-rr/releases),
+ - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html),
+ - RetroArch 1.10.1 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Ou,
+ - Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matérielle
+ compatible
+- Le fichier ROM de la v1.0 ou v1.1 NA de Final Fantasy Mystic Quest obtenu légalement, sûrement nommé `Final Fantasy - Mystic Quest (U) (V1.0).sfc` ou `Final Fantasy - Mystic Quest (U) (V1.1).sfc`
+La communauté d'Archipelago ne peut vous fournir avec ce fichier.
+
+## Procédure d'installation
+
+### Installation sur Linux
+
+1. Téléchargez et installez [Archipelago]().
+** Le fichier d'installation est situé dans la section "assets" dans le bas de la fenêtre d'information de la version. Vous voulez probablement le `.AppImage`**
+2. L'utilisation de RetroArch ou BizHawk est recommandé pour les utilisateurs linux, puisque snes9x-rr n'est pas compatible.
+
+### Installation sur Windows
+
+1. Téléchargez et installez [Archipelago]().
+** Le fichier d'installation est situé dans la section "assets" dans le bas de la fenêtre d'information de la version.**
+2. Si vous utilisez un émulateur, il est recommandé d'assigner votre émulateur capable d'éxécuter des scripts Lua comme
+ programme par défaut pour ouvrir vos ROMs.
+ 1. Extrayez votre dossier d'émulateur sur votre Bureau, ou à un endroit dont vous vous souviendrez.
+ 2. Faites un clic droit sur un fichier ROM et sélectionnez **Ouvrir avec...**
+ 3. Cochez la case à côté de **Toujours utiliser cette application pour ouvrir les fichiers `.sfc`**
+ 4. Descendez jusqu'en bas de la liste et sélectionnez **Rechercher une autre application sur ce PC**
+ 5. Naviguez dans les dossiers jusqu'au fichier `.exe` de votre émulateur et choisissez **Ouvrir**. Ce fichier
+ devrait se trouver dans le dossier que vous avez extrait à la première étape.
+
+
+## Créer son fichier de configuration (.yaml)
+
+### Qu'est-ce qu'un fichier de configuration et pourquoi en ai-je besoin ?
+
+Votre fichier de configuration contient un ensemble d'options de configuration pour indiquer au générateur
+comment il devrait générer votre seed. Chaque joueur d'un multiworld devra fournir son propre fichier de configuration. Cela permet
+à chaque joueur d'apprécier une expérience personalisée. Les différents joueurs d'un même multiworld
+pouront avoir des options de génération différentes.
+Vous pouvez lire le [guide pour créer un YAML de base](/tutorial/Archipelago/setup/en) en anglais.
+
+### Où est-ce que j'obtiens un fichier de configuration ?
+
+La [page d'options sur le site](/games/Final%20Fantasy%20Mystic%20Quest/player-options) vous permet de choisir vos
+options de génération et de les exporter vers un fichier de configuration.
+Il vous est aussi possible de trouver le fichier de configuration modèle de Mystic Quest dans votre répertoire d'installation d'Archipelago,
+dans le dossier Players/Templates.
+
+### Vérifier son fichier de configuration
+
+Si vous voulez valider votre fichier de configuration pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du
+[Validateur de YAML](/mysterycheck).
+
+## Générer une partie pour un joueur
+
+1. Aller sur la page [Génération de partie](/games/Final%20Fantasy%20Mystic%20Quest/player-options), configurez vos options,
+ et cliquez sur le bouton "Generate Game".
+2. Il vous sera alors présenté une page d'informations sur la seed
+3. Cliquez sur le lien "Create New Room".
+4. Vous verrez s'afficher la page du server, de laquelle vous pourrez télécharger votre fichier patch `.apmq`.
+5. Rendez-vous sur le [site FFMQR](https://ffmqrando.net/Archipelago).
+Sur cette page, sélectionnez votre ROM Final Fantasy Mystic Quest original dans le boîte "ROM", puis votre ficher patch `.apmq` dans la boîte "Load Archipelago Config File".
+Cliquez sur "Generate". Un téléchargement avec votre ROM aléatoire devrait s'amorcer.
+6. Puisque cette partie est à un seul joueur, vous n'avez plus besoin du client Archipelago ni du serveur, sentez-vous libre de les fermer.
+
+## Rejoindre un MultiWorld
+
+### Obtenir son patch et créer sa ROM
+
+Quand vous rejoignez un multiworld, il vous sera demandé de fournir votre fichier de configuration à celui qui héberge la partie ou
+s'occupe de la génération. Une fois cela fait, l'hôte vous fournira soit un lien pour télécharger votre patch, soit un
+fichier `.zip` contenant les patchs de tous les joueurs. Votre patch devrait avoir l'extension `.apmq`.
+
+Allez au [site FFMQR](https://ffmqrando.net/Archipelago) et sélectionnez votre ROM Final Fantasy Mystic Quest original dans le boîte "ROM", puis votre ficher patch `.apmq` dans la boîte "Load Archipelago Config File".
+Cliquez sur "Generate". Un téléchargement avec votre ROM aléatoire devrait s'amorcer.
+
+Ouvrez le client SNI (sur Windows ArchipelagoSNIClient.exe, sur Linux ouvrez le `.appImage` puis cliquez sur SNI Client), puis ouvrez le ROM téléchargé avec votre émulateur choisi.
+
+### Se connecter au client
+
+#### Avec un émulateur
+
+Quand le client se lance automatiquement, QUsb2Snes devrait également se lancer automatiquement en arrière-plan. Si
+c'est la première fois qu'il démarre, il vous sera peut-être demandé de l'autoriser à communiquer à travers le pare-feu
+Windows.
+
+##### snes9x-rr
+
+1. Chargez votre ROM si ce n'est pas déjà fait.
+2. Cliquez sur le menu "File" et survolez l'option **Lua Scripting**
+3. Cliquez alors sur **New Lua Script Window...**
+4. Dans la nouvelle fenêtre, sélectionnez **Browse...**
+5. Sélectionnez le fichier connecteur lua fourni avec votre client
+ - Regardez dans le dossier Archipelago et cherchez `/SNI/lua/x64` ou `/SNI/lua/x86`, dépendemment de si votre emulateur
+ est 64-bit ou 32-bit.
+6. Si vous obtenez une erreur `socket.dll missing` ou une erreur similaire lorsque vous chargez le script lua, vous devez naviguer dans le dossier
+contenant le script lua, puis copier le fichier `socket.dll` dans le dossier d'installation de votre emulateur snes9x.
+
+##### BizHawk
+
+1. Assurez vous d'avoir le coeur BSNES chargé. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant
+ ces options de menu :
+ `Config --> Cores --> SNES --> BSNES`
+ Une fois le coeur changé, vous devez redémarrer BizHawk.
+2. Chargez votre ROM si ce n'est pas déjà fait.
+3. Cliquez sur le menu "Tools" et cliquez sur **Lua Console**
+4. Cliquez sur le bouton pour ouvrir un nouveau script Lua, soit par le bouton avec un icône "Ouvrir un dossier",
+ en cliquant `Open Script...` dans le menu Script ou en appuyant sur `ctrl-O`.
+5. Sélectionnez le fichier `Connector.lua` inclus avec le client
+ - Regardez dans le dossier Archipelago et cherchez `/SNI/lua/x64` ou `/SNI/lua/x86`, dépendemment de si votre emulateur
+ est 64-bit ou 32-bit. Notez que les versions les plus récentes de BizHawk ne sont que 64-bit.
+
+##### RetroArch 1.10.1 ou plus récent
+
+Vous ne devez faire ces étapes qu'une fois. À noter que RetroArch 1.9.x ne fonctionnera pas puisqu'il s'agit d'une version moins récente que 1.10.1.
+
+1. Entrez dans le menu principal de RetroArch.
+2. Allez dans Settings --> User Interface. Activez l'option "Show Advanced Settings".
+3. Allez dans Settings --> Network. Activez l'option "Network Commands", qui se trouve sous "Request Device 16".
+ Laissez le "Network Command Port" à sa valeur par defaut, qui devrait être 55355.
+
+
+![Capture d'écran du menu Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png)
+4. Allez dans le Menu Principal --> Online Updater --> Core Downloader. Trouvez et sélectionnez "Nintendo - SNES / SFC (bsnes-mercury
+ Performance)".
+
+Lorsque vous chargez un ROM pour Archipelago, assurez vous de toujours sélectionner le coeur **bsnes-mercury**.
+Ce sont les seuls coeurs qui permettent à des outils extérieurs de lire les données du ROM.
+
+#### Avec une solution matérielle
+
+Ce guide suppose que vous avez téléchargé le bon micro-logiciel pour votre appareil. Si ce n'est pas déjà le cas, faites
+le maintenant. Les utilisateurs de SD2SNES et de FXPak Pro peuvent télécharger le micro-logiciel approprié
+[ici](https://github.com/RedGuyyyy/sd2snes/releases). Pour les autres solutions, de l'aide peut être trouvée
+[sur cette page](http://usb2snes.com/#supported-platforms).
+
+1. Fermez votre émulateur, qui s'est potentiellement lancé automatiquement.
+2. Ouvrez votre appareil et chargez le ROM.
+
+### Se connecter au MultiServer
+
+Puisque vous avez lancé SNI manuellement, vous devrez probablement lui indiquer l'adresse à laquelle il doit se connecter.
+Si le serveur est hébergé sur le site d'Archipelago, vous verrez l'adresse à laquelle vous connecter dans le haut de la page, dernière ligne avant la liste des mondes.
+Tapez `/connect adresse` (ou le "adresse" est remplacé par l'adresse archipelago, par exemple `/connect archipelago.gg:12345`) dans la boîte de commande au bas de votre client SNI, ou encore écrivez l'adresse dans la boîte "server" dans le haut du client, puis cliquez `Connect`.
+Si le serveur n'est pas hébergé sur le site d'Archipelago, demandez à l'hôte l'adresse du serveur, puis tapez `/connect adresse` (ou "adresse" est remplacé par l'adresse fourni par l'hôte) ou copiez/collez cette adresse dans le champ "Server" puis appuyez sur "Connect".
+
+Le client essaiera de vous reconnecter à la nouvelle adresse du serveur, et devrait mentionner "Server Status:
+Connected". Si le client ne se connecte pas après quelques instants, il faudra peut-être rafraîchir la page de
+l'interface Web.
+
+### Jouer au jeu
+
+Une fois que l'interface Web affiche que la SNES et le serveur sont connectés, vous êtes prêt à jouer. Félicitations
+pour avoir rejoint un multiworld !
+
+## Héberger un MultiWorld
+
+La méthode recommandée pour héberger une partie est d'utiliser le service d'hébergement fourni par
+Archipelago. Le processus est relativement simple :
+
+1. Récupérez les fichiers de configuration (.yaml) des joueurs.
+2. Créez une archive zip contenant ces fichiers de configuration.
+3. Téléversez l'archive zip sur le lien ci-dessous.
+ - Generate page: [WebHost Seed Generation Page](/generate)
+4. Attendez un moment que la seed soit générée.
+5. Lorsque la seed est générée, vous serez redirigé vers une page d'informations "Seed Info".
+6. Cliquez sur "Create New Room". Cela vous amènera à la page du serveur. Fournissez le lien de cette page aux autres
+ joueurs afin qu'ils puissent récupérer leurs patchs.
+7. Remarquez qu'un lien vers le traqueur du MultiWorld est en haut de la page de la salle. Vous devriez également
+ fournir ce lien aux joueurs pour qu'ils puissent suivre la progression de la partie. N'importe quelle personne voulant
+ observer devrait avoir accès à ce lien.
+8. Une fois que tous les joueurs ont rejoint, vous pouvez commencer à jouer.
diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py
index 520ad2252568..e930c4b8d6e9 100644
--- a/worlds/generic/Rules.py
+++ b/worlds/generic/Rules.py
@@ -1,4 +1,5 @@
import collections
+import logging
import typing
from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance
@@ -13,16 +14,16 @@
ItemRule = typing.Callable[[object], bool]
-def locality_needed(world: MultiWorld) -> bool:
- for player in world.player_ids:
- if world.local_items[player].value:
+def locality_needed(multiworld: MultiWorld) -> bool:
+ for player in multiworld.player_ids:
+ if multiworld.worlds[player].options.local_items.value:
return True
- if world.non_local_items[player].value:
+ if multiworld.worlds[player].options.non_local_items.value:
return True
# Group
- for group_id, group in world.groups.items():
- if set(world.player_ids) == set(group["players"]):
+ for group_id, group in multiworld.groups.items():
+ if set(multiworld.player_ids) == set(group["players"]):
continue
if group["local_items"]:
return True
@@ -30,8 +31,8 @@ def locality_needed(world: MultiWorld) -> bool:
return True
-def locality_rules(world: MultiWorld):
- if locality_needed(world):
+def locality_rules(multiworld: MultiWorld):
+ if locality_needed(multiworld):
forbid_data: typing.Dict[int, typing.Dict[int, typing.Set[str]]] = \
collections.defaultdict(lambda: collections.defaultdict(set))
@@ -39,32 +40,32 @@ def locality_rules(world: MultiWorld):
def forbid(sender: int, receiver: int, items: typing.Set[str]):
forbid_data[sender][receiver].update(items)
- for receiving_player in world.player_ids:
- local_items: typing.Set[str] = world.local_items[receiving_player].value
+ for receiving_player in multiworld.player_ids:
+ local_items: typing.Set[str] = multiworld.worlds[receiving_player].options.local_items.value
if local_items:
- for sending_player in world.player_ids:
+ for sending_player in multiworld.player_ids:
if receiving_player != sending_player:
forbid(sending_player, receiving_player, local_items)
- non_local_items: typing.Set[str] = world.non_local_items[receiving_player].value
+ non_local_items: typing.Set[str] = multiworld.worlds[receiving_player].options.non_local_items.value
if non_local_items:
forbid(receiving_player, receiving_player, non_local_items)
# Group
- for receiving_group_id, receiving_group in world.groups.items():
- if set(world.player_ids) == set(receiving_group["players"]):
+ for receiving_group_id, receiving_group in multiworld.groups.items():
+ if set(multiworld.player_ids) == set(receiving_group["players"]):
continue
if receiving_group["local_items"]:
- for sending_player in world.player_ids:
+ for sending_player in multiworld.player_ids:
if sending_player not in receiving_group["players"]:
forbid(sending_player, receiving_group_id, receiving_group["local_items"])
if receiving_group["non_local_items"]:
- for sending_player in world.player_ids:
+ for sending_player in multiworld.player_ids:
if sending_player in receiving_group["players"]:
forbid(sending_player, receiving_group_id, receiving_group["non_local_items"])
# create fewer lambda's to save memory and cache misses
func_cache = {}
- for location in world.get_locations():
+ for location in multiworld.get_locations():
if (location.player, location.item_rule) in func_cache:
location.item_rule = func_cache[location.player, location.item_rule]
# empty rule that just returns True, overwrite
@@ -81,15 +82,18 @@ def forbid(sender: int, receiver: int, items: typing.Set[str]):
i.name not in sending_blockers[i.player] and old_rule(i)
-def exclusion_rules(world: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None:
+def exclusion_rules(multiworld: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None:
for loc_name in exclude_locations:
try:
- location = world.get_location(loc_name, player)
+ location = multiworld.get_location(loc_name, player)
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
- if loc_name not in world.worlds[player].location_name_to_id:
+ if loc_name not in multiworld.worlds[player].location_name_to_id:
raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e
else:
- location.progress_type = LocationProgressType.EXCLUDED
+ if not location.advancement:
+ location.progress_type = LocationProgressType.EXCLUDED
+ else:
+ logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.")
def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule):
diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py
index 6b2ffdfee180..29f808b20272 100644
--- a/worlds/generic/__init__.py
+++ b/worlds/generic/__init__.py
@@ -40,7 +40,6 @@ class GenericWorld(World):
}
hidden = True
web = GenericWeb()
- data_version = 1
def generate_early(self):
self.multiworld.player_types[self.player] = SlotType.spectator # mark as spectator
@@ -69,9 +68,3 @@ def failed(self, warning: str, exception=Exception):
raise exception(warning)
else:
self.warn(warning)
-
-
-class PlandoConnection(NamedTuple):
- entrance: str
- exit: str
- direction: str # entrance, exit or both
diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md
index 456795dac4a7..37467eeb468e 100644
--- a/worlds/generic/docs/advanced_settings_en.md
+++ b/worlds/generic/docs/advanced_settings_en.md
@@ -2,27 +2,27 @@
This guide covers more the more advanced options available in YAML files. This guide is intended for the user who plans
to edit their YAML file manually. This guide should take about 10 minutes to read.
-If you would like to generate a basic, fully playable YAML without editing a file, then visit the settings page for the
-game you intend to play. The weighted settings page can also handle most of the advanced settings discussed here.
+If you would like to generate a basic, fully playable YAML without editing a file, then visit the options page for the
+game you intend to play.
+
+The options page can be found on the supported games page, just click the "Options Page" link under the name of the
+game you would like.
-The settings page can be found on the supported games page, just click the "Settings Page" link under the name of the
-game you would like.
* Supported games page: [Archipelago Games List](/games)
-* Weighted settings page: [Archipelago Weighted Settings](/weighted-settings)
-Clicking on the "Export Settings" button at the bottom-left will provide you with a pre-filled YAML with your options.
-The player settings page also has a link to download a full template file for that game which will have every option
+Clicking on the "Export Options" button at the bottom-left will provide you with a pre-filled YAML with your options.
+The player options page also has a link to download a full template file for that game which will have every option
possible for the game including some that don't display correctly on the site.
## YAML Overview
The Archipelago system generates games using player configuration files as input. These are going to be YAML files and
-each world will have one of these containing their custom settings for the game that world will play.
+each world will have one of these containing their custom options for the game that world will play.
## YAML Formatting
YAML files are a format of human-readable config files. The basic syntax of a yaml file will have a `root` node and then
-different levels of `nested` nodes that the generator reads in order to determine your settings.
+different levels of `nested` nodes that the generator reads in order to determine your options.
To nest text, the correct syntax is to indent **two spaces over** from its root option. A YAML file can be edited with
whatever text editor you choose to use though I personally recommend that you use Sublime Text. Sublime text
@@ -30,7 +30,8 @@ website: [SublimeText Website](https://www.sublimetext.com)
This program out of the box supports the correct formatting for the YAML file, so you will be able to use the tab key
and get proper highlighting for any potential errors made while editing the file. If using any other text editor you
-should ensure your indentation is done correctly with two spaces.
+should ensure your indentation is done correctly with two spaces. After editing your YAML file, you can validate it at
+the website's [validation page](/check).
A typical YAML file will look like:
@@ -53,13 +54,13 @@ so `option_one_setting_one` is guaranteed to occur.
For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `option_two_setting_two` will be rolled 43
times against each other. This means `option_two_setting_two` will be more likely to occur, but it isn't guaranteed,
-adding more randomness and "mystery" to your settings. Every configurable setting supports weights.
+adding more randomness and "mystery" to your options. Every configurable setting supports weights.
## Root Options
Currently, there are only a few options that are root options. Everything else should be nested within one of these root
options or in some cases nested within other nested options. The only options that should exist in root
-are `description`, `name`, `game`, `requires`, and the name of the games you want settings for.
+are `description`, `name`, `game`, `requires`, and the name of the games you want options for.
* `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files
using this to detail the intention of the file.
@@ -78,16 +79,16 @@ are `description`, `name`, `game`, `requires`, and the name of the games you wan
different weights.
* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this
- is good for detailing the version of Archipelago this YAML was prepared for as, if it is rolled on an older version,
- settings may be missing and as such it will not work as expected. If any plando is used in the file then requiring it
+ is good for detailing the version of Archipelago this YAML was prepared for. If it is rolled on an older version,
+ options may be missing and as such it will not work as expected. If any plando is used in the file then requiring it
here to ensure it will be used is good practice.
## Game Options
-One of your root settings will be the name of the game you would like to populate with settings. Since it is possible to
+One of your root options will be the name of the game you would like to populate with options. Since it is possible to
give a weight to any option, it is possible to have one file that can generate a seed for you where you don't know which
game you'll play. For these cases you'll want to fill the game options for every game that can be rolled by these
-settings. If a game can be rolled it **must** have a settings section even if it is empty.
+settings. If a game can be rolled it **must** have an options section even if it is empty.
### Universal Game Options
@@ -108,7 +109,9 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
* `minimal` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically
but may not be able to access all locations or acquire all items. A good example of this is having a big key in
the big chest in a dungeon in ALTTP making it impossible to get and finish the dungeon.
-* `progression_balancing` is a system the Archipelago generator uses to try and reduce "BK mode" as much as possible.
+* `progression_balancing` is a system the Archipelago generator uses to try and reduce
+ ["BK mode"](/glossary/en/#burger-king-/-bk-mode)
+ as much as possible.
This primarily involves moving necessary progression items into earlier logic spheres to make the games more
accessible so that players almost always have something to do. This can be in a range from 0 to 99, and is 50 by
default. This number represents a percentage of the furthest progressible player.
@@ -128,12 +131,13 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
the location without using any hint points.
* `start_location_hints` is the same as `start_hints` but for locations, allowing you to hint for the item contained
there without using any hint points.
-* `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk"
- item which isn't necessary for progression to go in these locations.
-* `priority_locations` is the inverse of `exlcude_locations`, forcing a progression item in the defined locations.
+* `exclude_locations` lets you define any locations that you don't want to do and forces a filler or trap item which
+ isn't necessary for progression into these locations.
+* `priority_locations` lets you define any locations that you want to do and forces a progression item into these
+ locations.
* `item_links` allows players to link their items into a group with the same item link name and game. The items declared
in `item_pool` get combined and when an item is found for the group, all players in the group receive it. Item links
- can also have local and non local items, forcing the items to either be placed within the worlds of the group or in
+ can also have local and non-local items, forcing the items to either be placed within the worlds of the group or in
worlds outside the group. If players have a varying amount of a specific item in the link, the lowest amount from the
players will be the amount put into the group.
@@ -273,12 +277,13 @@ one file, removing the need to manage separate files if one chooses to do so.
As a precautionary measure, before submitting a multi-game yaml like this one in a synchronous/sync multiworld, please
confirm that the other players in the multi are OK with what you are submitting, and please be fairly reasonable about
-the submission. (ie. Multiple long games (SMZ3, OoT, HK, etc.) for a game intended to be <2 hrs is not likely considered
+the submission. (i.e. Multiple long games (SMZ3, OoT, HK, etc.) for a game intended to be <2 hrs is not likely considered
reasonable, but submitting a ChecksFinder alongside another game OR submitting multiple Slay the Spire runs is likely
OK)
To configure your file to generate multiple worlds, use 3 dashes `---` on an empty line to separate the ending of one
-world and the beginning of another world.
+world and the beginning of another world. You can also combine multiple files by uploading them to the
+[validation page](/check).
### Example
@@ -290,7 +295,7 @@ requires:
version: 0.3.2
Super Mario 64:
progression_balancing: 50
- accessibilty: items
+ accessibility: items
EnableCoinStars: false
StrictCapRequirements: true
StrictCannonRequirements: true
@@ -310,7 +315,7 @@ name: Minecraft
game: Minecraft
Minecraft:
progression_balancing: 50
- accessibilty: items
+ accessibility: items
advancement_goal: 40
combat_difficulty: hard
include_hard_advancements: false
@@ -336,7 +341,7 @@ game: ChecksFinder
ChecksFinder:
progression_balancing: 50
- accessibilty: items
+ accessibility: items
```
The above example will generate 3 worlds - one Super Mario 64, one Minecraft, and one ChecksFinder.
diff --git a/worlds/generic/docs/commands_en.md b/worlds/generic/docs/commands_en.md
index e52ea20fd24d..317f724109e1 100644
--- a/worlds/generic/docs/commands_en.md
+++ b/worlds/generic/docs/commands_en.md
@@ -1,96 +1,110 @@
-### Helpful Commands
+# Helpful Commands
Commands are split into two types: client commands and server commands. Client commands are commands which are executed
by the client and do not affect the Archipelago remote session. Server commands are commands which are executed by the
Archipelago server and affect the Archipelago session or otherwise provide feedback from the server.
-In clients which have their own commands the commands are typically prepended by a forward slash:`/`. Remote commands
-are always submitted to the server prepended with an exclamation point: `!`.
+In clients which have their own commands the commands are typically prepended by a forward slash: `/`.
-#### Local Commands
+Server commands are always submitted to the server prepended with an exclamation point: `!`.
-The following list is a list of client commands which may be available to you through your Archipelago client. You
-execute these commands in your client window.
-
-The following commands are available in these clients: SNIClient, FactorioClient, FF1Client.
-
-- `/connect ` Connect to the multiworld server.
-- `/disconnect` Disconnects you from your current session.
-- `/received` Displays all the items you have found or been sent.
-- `/missing` Displays all the locations along with their current status (checked/missing).
-- `/items` Lists all the item names for the current game.
-- `/locations` Lists all the location names for the current game.
-- `/ready` Sends ready status to the server.
-- `/help` Returns a list of available commands.
-- `/license` Returns the software licensing information.
-- Just typing anything will broadcast a message to all players
-
-##### FF1Client Only
-
-The following command is only available when using the FF1Client for the Final Fantasy Randomizer.
-
-- `/nes` Shows the current status of the NES connection.
-
-##### SNIClient Only
-
-The following command is only available when using the SNIClient for SNES based games.
+# Server Commands
-- `/snes` Attempts to connect to your SNES device via SNI.
-- `/snes_close` Closes the current SNES connection.
-- `/slow_mode` Toggles on or off slow mode, which limits the rate in which you receive items.
-
-##### FactorioClient Only
-
-The following command is only available when using the FactorioClient to play Factorio with Archipelago.
-
-- `/factorio ` Sends the command argument to the Factorio server as a command.
-
-#### Remote Commands
-
-Remote commands may be executed by any client which allows for sending text chat to the Archipelago server. If your
+Server commands may be executed by any client which allows for sending text chat to the Archipelago server. If your
client does not allow for sending chat then you may connect to your game slot with the TextClient which comes with the
Archipelago installation. In order to execute the command you need to merely send a text message with the command,
including the exclamation point.
-- `!help` Returns a listing of available remote commands.
+### General
+- `!help` Returns a listing of available commands.
- `!license` Returns the software licensing information.
-- `!countdown ` Starts a countdown using the given seconds value. Useful for synchronizing starts.
- Defaults to 10 seconds if no argument is provided.
- `!options` Returns the current server options, including password in plaintext.
+- `!players` Returns info about the currently connected and non-connected players.
+- `!status` Returns information about the connection status and check completion numbers for all players in the current room. (Optionally mention a Tag name and get information on who has that Tag. For example: !status DeathLink)
+
+
+### Utilities
+- `!countdown ` Starts a countdown using the given seconds value. Useful for synchronizing starts.
+ Defaults to 10 seconds if no argument is provided.
+- `!alias ` Sets your alias, which allows you to use commands with the alias rather than your provided name.
- `!admin ` Executes a command as if you typed it into the server console. Remote administration must be
enabled.
-- `!players` Returns info about the currently connected and non-connected players.
-- `!status` Returns information about your team. (Currently all players as teams are unimplemented.)
+
+### Information
- `!remaining` Lists the items remaining in your game, but not where they are or who they go to.
- `!missing` Lists the location checks you are missing from the server's perspective.
- `!checked` Lists all the location checks you've done from the server's perspective.
-- `!alias ` Sets your alias.
-- `!getitem - ` Cheats an item, if it is enabled in the server.
-- `!hint_location
` Hints for a location specifically. Useful in games where item names may match location
- names such as Factorio.
-- `!hint - ` Tells you at which location in whose game your Item is. Note you need to have checked some
- locations to earn a hint. You can check how many you have by just running `!hint`
-- `!release` If you didn't turn on auto-release or if you allowed releasing prior to goal completion. Remember that "
- releasing" actually means sending out your remaining items in your world.
-- `!collect` Grants you all the remaining checks in your world. Typically used after goal completion.
-
-#### Host only (on Archipelago.gg or in your server console)
+### Hints
+- `!hint` Lists all hints relevant to your world, the number of points you have for hints, and how much a hint costs.
+- `!hint
- ` Tells you the game world and location your item is in, uses points earned from completing locations.
+- `!hint_location
` Tells you what item is in a specific location, uses points earned from completing locations.
+
+### Collect/Release
+- `!collect` Grants you all the remaining items for your world by collecting them from all games. Typically used after
+ goal completion.
+- `!release` Releases all items contained in your world to other worlds. Typically, done automatically by the server,
+ but can be configured to allow/require manual usage of this command.
+
+### Cheats
+- `!getitem - ` Cheats an item to the currently connected slot, if it is enabled in the server.
+
+
+## Host only (on Archipelago.gg or in your server console)
+
+### General
- `/help` Returns a list of commands available in the console.
- `/license` Returns the software licensing information.
-- `/countdown
` Starts a countdown which is sent to all players via text chat. Defaults to 10 seconds if no
- argument is provided.
- `/options` Lists the server's current options, including password in plaintext.
-- `/save` Saves the state of the current multiworld. Note that the server autosaves on a minute basis.
- `/players` List currently connected players.
+- `/save` Saves the state of the current multiworld. Note that the server auto-saves on a minute basis.
- `/exit` Shutdown the server
-- `/alias ` Assign a player an alias.
+
+### Utilities
+- `/countdown ` Starts a countdown sent to all players via text chat. Defaults to 10 seconds if no
+ argument is provided.
+- `/option ` Set a server option. For a list of options, use the `/options` command.
+- `/alias ` Assign a player an alias, allowing you to reference the player by the alias in commands.
+
+
+### Collect/Release
- `/collect ` Send out any items remaining in the multiworld belonging to the given player.
-- `/release ` Releases someone regardless of settings and game completion status
+- `/release ` Sends out all remaining items in this world regardless of settings and game completion status.
- `/allow_release ` Allows the given player to use the `!release` command.
-- `/forbid_release ` Bars the given player from using the `!release` command.
+- `/forbid_release ` Prevents the given player from using the `!release` command.
+
+### Cheats
- `/send - ` Grants the given player the specified item.
- `/send_multiple
- ` Grants the given player the stated amount of the specified item.
- `/send_location
` Send out the given location for the specified player as if the player checked it
- `/hint - ` Send out a hint for the given item or location for the specified player.
-- `/option
` Set a server option. For a list of options, use the `/options` command.
+
+
+
+# Local Commands
+
+This a list of client commands which may be available to you through your Archipelago client. You can
+execute these commands in your client window.
+
+The following commands are available in the clients that use the CommonClient, for example: TextClient, SNIClient, etc.
+
+- `/connect ` Connect to the multiworld server at the given address.
+- `/disconnect` Disconnects you from your current session.
+- `/help` Returns a list of available commands.
+- `/license` Returns the software licensing information.
+- `/received` Displays all the items you have received from all players, including yourself.
+- `/missing` Displays all the locations along with their current status (checked/missing).
+- `/items` Lists all the item names for the current game.
+- `/item_groups` Lists all the item group names for the current game.
+- `/locations` Lists all the location names for the current game.
+- `/location_groups` Lists all the location group names for the current game.
+- `/ready` Sends ready status to the server.
+- Typing anything that doesn't start with `/` will broadcast a message to all players.
+
+## SNIClient Only
+
+The following command is only available when using the SNIClient for SNES based games.
+
+- `/snes` Attempts to connect to your SNES device via SNI.
+- `/snes_close` Closes the current SNES connection.
+- `/slow_mode` Toggles on or off slow mode, which limits the rate in which you receive items.
diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md
index 2d40f45195ba..161b1e465b33 100644
--- a/worlds/generic/docs/plando_en.md
+++ b/worlds/generic/docs/plando_en.md
@@ -161,30 +161,62 @@ into any locations within the game slots named BobsSlaytheSpire and BobsRogueLeg
## Boss Plando
-As this is currently only supported by A Link to the Past, instead of finding an explanation here, please refer to the
-relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en)
+This is currently only supported by A Link to the Past and Kirby's Dream Land 3. Boss plando allows a player to place a
+given boss within an arena. More specific information for boss plando in A Link to the Past can be found in
+its [plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en).
+
+Boss plando takes in a list of instructions for placing bosses, separated by a semicolon `;`.
+There are three types of placement: direct, full, and shuffle.
+* Direct placement takes both an arena and a boss, and places the boss into that arena.
+ * `Eastern Palace-Trinexx`
+* Full placement will take a boss, and place it into as many remaining arenas as possible.
+ * `King Dedede`
+* Shuffle will fill any remaining arenas using a given boss shuffle option, typically to be used as the last instruction.
+ * `full`
+
+### Examples
+
+```yaml
+A Link to the Past:
+ boss_shuffle:
+ # Basic boss shuffle, but prevent Trinexx from being outside Turtle Rock
+ Turtle Rock-Trinexx;basic: 1
+ # Place as many Arrghus as possible, then let the rest be random
+ Arrghus;chaos: 1
+
+Kirby's Dream Land 3:
+ boss_shuffle:
+ # Ensure Iceberg's boss will be King Dedede, but randomize the rest
+ Iceberg-King Dedede;full: 1
+ # Have all bosses be Whispy Woods
+ Whispy Woods: 1
+ # Ensure Ripple Field's boss is Pon & Con, but let the method others
+ # are placed with be random
+ Ripple Field-Pon & Con;random: 1
+```
+
## Text Plando
As this is currently only supported by A Link to the Past, instead of finding an explanation here, please refer to the
relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en)
-## Connections Plando
+## Connection Plando
-This is currently only supported by Minecraft and A Link to the Past. As the way that these games interact with their
-connections is different, I will only explain the basics here, while more specifics for A Link to the Past connection
-plando can be found in its plando guide.
+This is currently only supported by a few games, including A Link to the Past, Minecraft, and Ocarina of Time. As the way that these games interact with their
+connections is different, only the basics are explained here. More specific information for connection plando in A Link to the Past can be found in
+its [plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en#connections).
* The options for connections are `percentage`, `entrance`, `exit`, and `direction`. Each of these options supports
subweights.
* `percentage` is the percentage chance for this connection from 0 to 100 and defaults to 100.
* Every connection has an `entrance` and an `exit`. These can be unlinked like in A Link to the Past insanity entrance
shuffle.
-* `direction` can be `both`, `entrance`, or `exit` and determines in which direction this connection will operate.
+* `direction` can be `both`, `entrance`, or `exit` and determines in which direction this connection will operate. `direction` defaults to `both`.
[A Link to the Past connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852)
-[Minecraft connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Regions.py#L62)
+[Minecraft connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/data/regions.json#L18****)
### Examples
diff --git a/worlds/generic/docs/setup_en.md b/worlds/generic/docs/setup_en.md
index 132b88e28553..22622cd0e94d 100644
--- a/worlds/generic/docs/setup_en.md
+++ b/worlds/generic/docs/setup_en.md
@@ -11,44 +11,47 @@ Some steps also assume use of Windows, so may vary with your OS.
## Installing the Archipelago software
-The most recent public release of Archipelago can be found on the GitHub Releases page:
-[Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases).
+The most recent public release of Archipelago can be found on GitHub:
+[Archipelago Latest Release](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
Run the exe file, and after accepting the license agreement you will be asked which components you would like to
install.
-The generator allows you to generate multiworld games on your computer. The ROM setups are required if anyone in the
-game that you generate wants to play any of those games as they are needed to generate the relevant patch files. If you
-do not own the game, uncheck the relevant box. If you gain the game later, the installer can be run again to install and
-set up new components.
+Archipelago installations are automatically bundled with some programs. These include a launcher, a generator, a
+server and some clients.
-The server will allow you to host the multiworld on your machine. Hosting on your machine requires forwarding the port
+- The launcher lets you quickly access Archipelago's different components and programs. It is found under the name
+ `ArchipelagoLauncher` and can be found in the main directory of your Archipelago installation.
+
+- The generator allows you to generate multiworld games on your computer. Please refer to the 'Generating a game'
+ section of this guide for more information about it.
+
+- The server will allow you to host the multiworld on your machine. Hosting on your machine requires forwarding the port
you are hosting on. The default port for Archipelago is `38281`. If you are unsure how to do this there are plenty of
other guides on the internet that will be more suited to your hardware.
-The `Clients` are what are used to connect your game to the multiworld. If the game you plan to play is available
-here, go ahead and install its client as well. If the game you choose to play is supported by Archipelago but not listed
-in the installation, check the setup guide for that game. Installing a client for a ROM based game requires you to have
-a legally obtained ROM for that game as well.
+- The clients are what are used to connect your game to the multiworld. Some games use a client that is automatically
+installed with an Archipelago installation. You can access those clients via the launcher or by navigating
+to your Archipelago installation.
## Generating a game
### What is a YAML?
YAML is the file format which Archipelago uses in order to configure a player's world. It allows you to dictate which
-game you will be playing as well as the settings you would like for that game.
+game you will be playing as well as the options 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
YAML files may be generated on the Archipelago website by visiting the [games page](/games) and clicking the
-"Settings Page" link under the relevant game. Clicking "Export Settings" in a game's settings page will download the
+"Options Page" link under the relevant game. Clicking "Export Options" in a game's options page will download the
YAML to your system.
-Alternatively, you can run `ArchipelagoLauncher.exe` and click on `Generate Template Settings` to create a set of template
+Alternatively, you can run `ArchipelagoLauncher.exe` and click on `Generate Template Options` to create a set of template
YAMLs for each game in your Archipelago install (including for APWorlds). These will be placed in your `Players/Templates` folder.
In a multiworld there must be one YAML per world. Any number of players can play on each world using either the game's
@@ -63,23 +66,27 @@ each player is planning on playing their own game then they will each need a YAM
#### On the website
The easiest way to get started playing an Archipelago generated game, after following the base setup from the game's
-setup guide, is to find the game on the [Archipelago Games List](/games), click on `Settings Page`, set the settings for
+setup guide, is to find the game on the [Archipelago Games List](/games), click on `Options Page`, set the options for
how you want to play, and click `Generate Game` at the bottom of the page. This will create a page for the seed, from
which you can create a room, and then [connect](#connecting-to-an-archipelago-server).
-If you have downloaded the settings, or have created a settings file manually, this file can be uploaded on the
+If you have downloaded the options, or have created an options file manually, this file can be uploaded on the
[Generation Page](/generate) where you can also set any specific hosting settings.
#### On your local installation
-To generate a game on your local machine, make sure to install the Archipelago software, and ensure to select the
-`Generator` component, as well as the `ROM setup` for any games you will want to play. Navigate to your Archipelago
-installation (usually C:\ProgramData\Archipelago), and place the settings file you have either created or downloaded
+To generate a game on your local machine, make sure to install the Archipelago software. Navigate to your Archipelago
+installation (usually C:\ProgramData\Archipelago), and place the options file you have either created or downloaded
from the website in the `Players` folder.
-Run `ArchipelagoGenerate.exe`, and it will inform you whether the generation was successful or not. If successful, there
-will be an output zip in the `output` folder (usually named something like `AP_XXXXX.zip`). This will contain all
-relevant information to the session, including the spoiler log, if one was generated.
+Run `ArchipelagoGenerate.exe`, or click on `Generate` in the launcher, and it will inform you whether the generation
+was successful or not. If successful, there will be an output zip in the `output` folder
+(usually named something like `AP_XXXXX.zip`). This will contain all relevant information to the session, including the
+spoiler log, if one was generated.
+
+Please note that some games require you to own their ROM files to generate with them as they are needed to generate the
+relevant patch files. When you generate with a ROM game for the first time, you will be asked to locate its base ROM file.
+This step only needs to be done once.
### Generating a multiplayer game
@@ -90,19 +97,16 @@ resources, and host the resulting multiworld on the website.
#### Gather All Player YAMLs
-All players that wish to play in the generated multiworld must have a YAML file which contains the settings that they
+All players that wish to play in the generated multiworld must have a YAML file which contains the options that they
wish to play with. One person should gather all files from all participants in the generated multiworld. It is possible
for a single player to have multiple games, or even multiple slots of a single game, but each YAML must have a unique
player name.
#### On the website
-Gather all player YAML files into a single place, and compress them into a zip file. This can be done by pressing
-ctrl/cmd + clicking on each file until all are selected, right-clicking one of the files, and clicking
-`compress to ZIP file` or `send to > compressed folder`.
-
-Navigate to the [Generate Page](/generate), select the host settings you would like, click on `Upload File`, and
-select the newly created zip from the opened window.
+Gather all player YAML files into a single place, then navigate to the [Generate Page](/generate). Select the host settings
+you would like, click on `Upload File(s)`, and select all player YAML files. The site also accepts `zip` archives containing YAML
+files.
After some time, you will be redirected to a seed info page that will display the generated seed, the time it was
created, the number of players, the spoiler (if one was created) and all rooms created from this seed.
@@ -114,19 +118,24 @@ It is possible to generate the multiworld locally, using a local Archipelago ins
Archipelago installation folder (usually C:\ProgramData\Archipelago) and placing each YAML file in the `Players` folder.
If the folder does not exist then it must be created manually. The files here should not be compressed.
-After filling the `Players` folder, the `ArchipelagoGenerate.exe` program should be run in order to generate a
-multiworld. The output of this process is placed in the `output` folder (usually named something like `AP_XXXXX.zip`).
+After filling the `Players` folder, run`ArchipelagoGenerate.exe` or click `Generate` in the launcher. The output of
+the generation is placed in the `output` folder (usually named something like `AP_XXXXX.zip`).
+
+Please note that if any player in the game you want to generate plays a game that needs a ROM file to generate, you will
+need the corresponding ROM files.
##### Changing local host settings for generation
Sometimes there are various settings that you may want to change before rolling a seed such as enabling race mode,
auto-release, plando support, or setting a password.
-All of these settings, plus other options, may be changed by modifying the `host.yaml` file in the Archipelago
-installation folder. The settings chosen here are baked into the `.archipelago` file that gets output with the other
-files after generation, so if you are rolling locally, ensure this file is edited to your liking **before** rolling the
-seed. This file is overwritten when running the Archipelago Installation software. If you have changed settings in this
-file, and would like to retain them, you may rename the file to `options.yaml`.
+All of these settings, plus more, can be changed by modifying the `host.yaml` file in the Archipelago
+installation folder. You can quickly access this file by clicking on `Open host.yaml` in the launcher. The settings
+chosen here are baked into the `.archipelago` file that gets output with the other files after generation, so if you
+are rolling locally, ensure this file is edited to your liking **before** rolling the seed. This file is overwritten
+when running the Archipelago Installation software. If you have changed settings in this file, and would like to retain
+them, you may rename the file to `options.yaml`.
+
## Hosting an Archipelago Server
@@ -198,4 +207,4 @@ when creating your [YAML file](#creating-a-yaml). If the game is hosted on the w
room page. The name is case-sensitive.
* `Password` is the password set by the host in order to join the multiworld. By default, this will be empty and is almost
never required, but one can be set when generating the game. Generally, leave this field blank when it exists,
-unless you know that a password was set, and what that password is.
\ No newline at end of file
+unless you know that a password was set, and what that password is.
diff --git a/worlds/generic/docs/triggers_en.md b/worlds/generic/docs/triggers_en.md
index a9ffebb4669a..b751b8a3ec01 100644
--- a/worlds/generic/docs/triggers_en.md
+++ b/worlds/generic/docs/triggers_en.md
@@ -6,7 +6,7 @@ about 5 minutes to read.
## What are triggers?
-Triggers allow you to customize your game settings by allowing you to define one or many options which only occur under
+Triggers allow you to customize your game options by allowing you to define one or many options which only occur under
specific conditions. These are essentially "if, then" statements for options in your game. A good example of what you
can do with triggers is the [custom mercenary mode YAML
](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml) that was
@@ -121,4 +121,42 @@ For example:
In this example (thanks to @Black-Sliver), if the `pupdunk` option is rolled, then the difficulty values will be rolled
again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and the exp modifier will be rerolled using
new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard`
-and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery".
\ No newline at end of file
+and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery".
+
+## Adding or Removing from a List, Set, or Dict Option
+
+List, set, and dict options can additionally have values added to or removed from itself without overriding the existing
+option value by prefixing the option name in the trigger block with `+` (add) or `-` (remove). The exact behavior for
+each will depend on the option type.
+
+- For sets, `+` will add the value(s) to the set and `-` will remove the value(s) from the set. Sets do not allow
+ duplicates.
+- For lists, `+` will add new values(s) to the list and `-` will remove the first matching values(s) it comes across.
+ Lists allow duplicate values.
+- For dicts, `+` will add the value(s) to the given key(s) inside the dict if it exists, or add it otherwise. `-` is the
+ inverse operation of addition (and negative values are allowed).
+
+For example:
+
+```yaml
+Super Metroid:
+ start_location:
+ landing_site: 50
+ aqueduct: 50
+ start_hints:
+ - Morph Ball
+ start_inventory:
+ Power Bombs: 1
+ triggers:
+ - option_category: Super Metroid
+ option_name: start_location
+ option_result: aqueduct
+ options:
+ Super Metroid:
+ +start_hints:
+ - Gravity Suit
+```
+
+In this example, if the `start_location` option rolls `landing_site`, only a starting hint for Morph Ball will be
+created. If `aqueduct` is rolled, a starting hint for Gravity Suit will also be created alongside the hint for Morph
+Ball.
diff --git a/worlds/heretic/Items.py b/worlds/heretic/Items.py
new file mode 100644
index 000000000000..a0907a3a3040
--- /dev/null
+++ b/worlds/heretic/Items.py
@@ -0,0 +1,1606 @@
+# This file is auto generated. More info: https://github.com/Daivuk/apdoom
+
+from BaseClasses import ItemClassification
+from typing import TypedDict, Dict, Set
+
+
+class ItemDict(TypedDict, total=False):
+ classification: ItemClassification
+ count: int
+ name: str
+ doom_type: int # Unique numerical id used to spawn the item. -1 is level item, -2 is level complete item.
+ episode: int # Relevant if that item targets a specific level, like keycard or map reveal pickup.
+ map: int
+
+
+item_table: Dict[int, ItemDict] = {
+ 370000: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Gauntlets of the Necromancer',
+ 'doom_type': 2005,
+ 'episode': -1,
+ 'map': -1},
+ 370001: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ethereal Crossbow',
+ 'doom_type': 2001,
+ 'episode': -1,
+ 'map': -1},
+ 370002: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Dragon Claw',
+ 'doom_type': 53,
+ 'episode': -1,
+ 'map': -1},
+ 370003: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Phoenix Rod',
+ 'doom_type': 2003,
+ 'episode': -1,
+ 'map': -1},
+ 370004: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Firemace',
+ 'doom_type': 2002,
+ 'episode': -1,
+ 'map': -1},
+ 370005: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Hellstaff',
+ 'doom_type': 2004,
+ 'episode': -1,
+ 'map': -1},
+ 370006: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Bag of Holding',
+ 'doom_type': 8,
+ 'episode': -1,
+ 'map': -1},
+ 370007: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Chaos Device',
+ 'doom_type': 36,
+ 'episode': -1,
+ 'map': -1},
+ 370008: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Morph Ovum',
+ 'doom_type': 30,
+ 'episode': -1,
+ 'map': -1},
+ 370009: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Mystic Urn',
+ 'doom_type': 32,
+ 'episode': -1,
+ 'map': -1},
+ 370010: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Quartz Flask',
+ 'doom_type': 82,
+ 'episode': -1,
+ 'map': -1},
+ 370011: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Ring of Invincibility',
+ 'doom_type': 84,
+ 'episode': -1,
+ 'map': -1},
+ 370012: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Shadowsphere',
+ 'doom_type': 75,
+ 'episode': -1,
+ 'map': -1},
+ 370013: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Timebomb of the Ancients',
+ 'doom_type': 34,
+ 'episode': -1,
+ 'map': -1},
+ 370014: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Tome of Power',
+ 'doom_type': 86,
+ 'episode': -1,
+ 'map': -1},
+ 370015: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Torch',
+ 'doom_type': 33,
+ 'episode': -1,
+ 'map': -1},
+ 370016: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Silver Shield',
+ 'doom_type': 85,
+ 'episode': -1,
+ 'map': -1},
+ 370017: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Enchanted Shield',
+ 'doom_type': 31,
+ 'episode': -1,
+ 'map': -1},
+ 370018: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Crystal Geode',
+ 'doom_type': 12,
+ 'episode': -1,
+ 'map': -1},
+ 370019: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Energy Orb',
+ 'doom_type': 55,
+ 'episode': -1,
+ 'map': -1},
+ 370020: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Greater Runes',
+ 'doom_type': 21,
+ 'episode': -1,
+ 'map': -1},
+ 370021: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Inferno Orb',
+ 'doom_type': 23,
+ 'episode': -1,
+ 'map': -1},
+ 370022: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Pile of Mace Spheres',
+ 'doom_type': 16,
+ 'episode': -1,
+ 'map': -1},
+ 370023: {'classification': ItemClassification.filler,
+ 'count': 0,
+ 'name': 'Quiver of Ethereal Arrows',
+ 'doom_type': 19,
+ 'episode': -1,
+ 'map': -1},
+ 370200: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Docks (E1M1) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 1,
+ 'map': 1},
+ 370201: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Dungeons (E1M2) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 1,
+ 'map': 2},
+ 370202: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Dungeons (E1M2) - Green key',
+ 'doom_type': 73,
+ 'episode': 1,
+ 'map': 2},
+ 370203: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Dungeons (E1M2) - Blue key',
+ 'doom_type': 79,
+ 'episode': 1,
+ 'map': 2},
+ 370204: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Gatehouse (E1M3) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 1,
+ 'map': 3},
+ 370205: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Gatehouse (E1M3) - Green key',
+ 'doom_type': 73,
+ 'episode': 1,
+ 'map': 3},
+ 370206: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Guard Tower (E1M4) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 1,
+ 'map': 4},
+ 370207: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Guard Tower (E1M4) - Green key',
+ 'doom_type': 73,
+ 'episode': 1,
+ 'map': 4},
+ 370208: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Citadel (E1M5) - Green key',
+ 'doom_type': 73,
+ 'episode': 1,
+ 'map': 5},
+ 370209: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Citadel (E1M5) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 1,
+ 'map': 5},
+ 370210: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Citadel (E1M5) - Blue key',
+ 'doom_type': 79,
+ 'episode': 1,
+ 'map': 5},
+ 370211: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Cathedral (E1M6) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 1,
+ 'map': 6},
+ 370212: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Cathedral (E1M6) - Green key',
+ 'doom_type': 73,
+ 'episode': 1,
+ 'map': 6},
+ 370213: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Crypts (E1M7) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 1,
+ 'map': 7},
+ 370214: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Crypts (E1M7) - Green key',
+ 'doom_type': 73,
+ 'episode': 1,
+ 'map': 7},
+ 370215: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Crypts (E1M7) - Blue key',
+ 'doom_type': 79,
+ 'episode': 1,
+ 'map': 7},
+ 370216: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Graveyard (E1M9) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 1,
+ 'map': 9},
+ 370217: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Graveyard (E1M9) - Green key',
+ 'doom_type': 73,
+ 'episode': 1,
+ 'map': 9},
+ 370218: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Graveyard (E1M9) - Blue key',
+ 'doom_type': 79,
+ 'episode': 1,
+ 'map': 9},
+ 370219: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Crater (E2M1) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 2,
+ 'map': 1},
+ 370220: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Crater (E2M1) - Green key',
+ 'doom_type': 73,
+ 'episode': 2,
+ 'map': 1},
+ 370221: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Lava Pits (E2M2) - Green key',
+ 'doom_type': 73,
+ 'episode': 2,
+ 'map': 2},
+ 370222: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Lava Pits (E2M2) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 2,
+ 'map': 2},
+ 370223: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The River of Fire (E2M3) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 2,
+ 'map': 3},
+ 370224: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The River of Fire (E2M3) - Blue key',
+ 'doom_type': 79,
+ 'episode': 2,
+ 'map': 3},
+ 370225: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The River of Fire (E2M3) - Green key',
+ 'doom_type': 73,
+ 'episode': 2,
+ 'map': 3},
+ 370226: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Ice Grotto (E2M4) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 2,
+ 'map': 4},
+ 370227: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Ice Grotto (E2M4) - Blue key',
+ 'doom_type': 79,
+ 'episode': 2,
+ 'map': 4},
+ 370228: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Ice Grotto (E2M4) - Green key',
+ 'doom_type': 73,
+ 'episode': 2,
+ 'map': 4},
+ 370229: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Catacombs (E2M5) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 2,
+ 'map': 5},
+ 370230: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Catacombs (E2M5) - Blue key',
+ 'doom_type': 79,
+ 'episode': 2,
+ 'map': 5},
+ 370231: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Catacombs (E2M5) - Green key',
+ 'doom_type': 73,
+ 'episode': 2,
+ 'map': 5},
+ 370232: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Labyrinth (E2M6) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 2,
+ 'map': 6},
+ 370233: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Labyrinth (E2M6) - Blue key',
+ 'doom_type': 79,
+ 'episode': 2,
+ 'map': 6},
+ 370234: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Labyrinth (E2M6) - Green key',
+ 'doom_type': 73,
+ 'episode': 2,
+ 'map': 6},
+ 370235: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Great Hall (E2M7) - Green key',
+ 'doom_type': 73,
+ 'episode': 2,
+ 'map': 7},
+ 370236: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Great Hall (E2M7) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 2,
+ 'map': 7},
+ 370237: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Great Hall (E2M7) - Blue key',
+ 'doom_type': 79,
+ 'episode': 2,
+ 'map': 7},
+ 370238: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Glacier (E2M9) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 2,
+ 'map': 9},
+ 370239: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Glacier (E2M9) - Blue key',
+ 'doom_type': 79,
+ 'episode': 2,
+ 'map': 9},
+ 370240: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Glacier (E2M9) - Green key',
+ 'doom_type': 73,
+ 'episode': 2,
+ 'map': 9},
+ 370241: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Storehouse (E3M1) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 3,
+ 'map': 1},
+ 370242: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Storehouse (E3M1) - Green key',
+ 'doom_type': 73,
+ 'episode': 3,
+ 'map': 1},
+ 370243: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Cesspool (E3M2) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 3,
+ 'map': 2},
+ 370244: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Cesspool (E3M2) - Green key',
+ 'doom_type': 73,
+ 'episode': 3,
+ 'map': 2},
+ 370245: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Cesspool (E3M2) - Blue key',
+ 'doom_type': 79,
+ 'episode': 3,
+ 'map': 2},
+ 370246: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Confluence (E3M3) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 3,
+ 'map': 3},
+ 370247: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Confluence (E3M3) - Green key',
+ 'doom_type': 73,
+ 'episode': 3,
+ 'map': 3},
+ 370248: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Confluence (E3M3) - Blue key',
+ 'doom_type': 79,
+ 'episode': 3,
+ 'map': 3},
+ 370249: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Azure Fortress (E3M4) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 3,
+ 'map': 4},
+ 370250: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Azure Fortress (E3M4) - Green key',
+ 'doom_type': 73,
+ 'episode': 3,
+ 'map': 4},
+ 370251: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Ophidian Lair (E3M5) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 3,
+ 'map': 5},
+ 370252: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Ophidian Lair (E3M5) - Green key',
+ 'doom_type': 73,
+ 'episode': 3,
+ 'map': 5},
+ 370253: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Halls of Fear (E3M6) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 3,
+ 'map': 6},
+ 370254: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Halls of Fear (E3M6) - Green key',
+ 'doom_type': 73,
+ 'episode': 3,
+ 'map': 6},
+ 370255: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Halls of Fear (E3M6) - Blue key',
+ 'doom_type': 79,
+ 'episode': 3,
+ 'map': 6},
+ 370256: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Chasm (E3M7) - Blue key',
+ 'doom_type': 79,
+ 'episode': 3,
+ 'map': 7},
+ 370257: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Chasm (E3M7) - Green key',
+ 'doom_type': 73,
+ 'episode': 3,
+ 'map': 7},
+ 370258: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Chasm (E3M7) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 3,
+ 'map': 7},
+ 370259: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Aquifier (E3M9) - Blue key',
+ 'doom_type': 79,
+ 'episode': 3,
+ 'map': 9},
+ 370260: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Aquifier (E3M9) - Green key',
+ 'doom_type': 73,
+ 'episode': 3,
+ 'map': 9},
+ 370261: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Aquifier (E3M9) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 3,
+ 'map': 9},
+ 370262: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Catafalque (E4M1) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 4,
+ 'map': 1},
+ 370263: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Catafalque (E4M1) - Green key',
+ 'doom_type': 73,
+ 'episode': 4,
+ 'map': 1},
+ 370264: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Blockhouse (E4M2) - Green key',
+ 'doom_type': 73,
+ 'episode': 4,
+ 'map': 2},
+ 370265: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Blockhouse (E4M2) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 4,
+ 'map': 2},
+ 370266: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Blockhouse (E4M2) - Blue key',
+ 'doom_type': 79,
+ 'episode': 4,
+ 'map': 2},
+ 370267: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ambulatory (E4M3) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 4,
+ 'map': 3},
+ 370268: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ambulatory (E4M3) - Green key',
+ 'doom_type': 73,
+ 'episode': 4,
+ 'map': 3},
+ 370269: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ambulatory (E4M3) - Blue key',
+ 'doom_type': 79,
+ 'episode': 4,
+ 'map': 3},
+ 370270: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Great Stair (E4M5) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 4,
+ 'map': 5},
+ 370271: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Great Stair (E4M5) - Green key',
+ 'doom_type': 73,
+ 'episode': 4,
+ 'map': 5},
+ 370272: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Great Stair (E4M5) - Blue key',
+ 'doom_type': 79,
+ 'episode': 4,
+ 'map': 5},
+ 370273: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Halls of the Apostate (E4M6) - Green key',
+ 'doom_type': 73,
+ 'episode': 4,
+ 'map': 6},
+ 370274: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Halls of the Apostate (E4M6) - Blue key',
+ 'doom_type': 79,
+ 'episode': 4,
+ 'map': 6},
+ 370275: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Halls of the Apostate (E4M6) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 4,
+ 'map': 6},
+ 370276: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ramparts of Perdition (E4M7) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 4,
+ 'map': 7},
+ 370277: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ramparts of Perdition (E4M7) - Green key',
+ 'doom_type': 73,
+ 'episode': 4,
+ 'map': 7},
+ 370278: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ramparts of Perdition (E4M7) - Blue key',
+ 'doom_type': 79,
+ 'episode': 4,
+ 'map': 7},
+ 370279: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Shattered Bridge (E4M8) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 4,
+ 'map': 8},
+ 370280: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Mausoleum (E4M9) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 4,
+ 'map': 9},
+ 370281: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ochre Cliffs (E5M1) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 5,
+ 'map': 1},
+ 370282: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ochre Cliffs (E5M1) - Blue key',
+ 'doom_type': 79,
+ 'episode': 5,
+ 'map': 1},
+ 370283: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ochre Cliffs (E5M1) - Green key',
+ 'doom_type': 73,
+ 'episode': 5,
+ 'map': 1},
+ 370284: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Rapids (E5M2) - Green key',
+ 'doom_type': 73,
+ 'episode': 5,
+ 'map': 2},
+ 370285: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Rapids (E5M2) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 5,
+ 'map': 2},
+ 370286: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Quay (E5M3) - Green key',
+ 'doom_type': 73,
+ 'episode': 5,
+ 'map': 3},
+ 370287: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Quay (E5M3) - Blue key',
+ 'doom_type': 79,
+ 'episode': 5,
+ 'map': 3},
+ 370288: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Quay (E5M3) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 5,
+ 'map': 3},
+ 370289: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Courtyard (E5M4) - Blue key',
+ 'doom_type': 79,
+ 'episode': 5,
+ 'map': 4},
+ 370290: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Courtyard (E5M4) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 5,
+ 'map': 4},
+ 370291: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Courtyard (E5M4) - Green key',
+ 'doom_type': 73,
+ 'episode': 5,
+ 'map': 4},
+ 370292: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Hydratyr (E5M5) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 5,
+ 'map': 5},
+ 370293: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Hydratyr (E5M5) - Green key',
+ 'doom_type': 73,
+ 'episode': 5,
+ 'map': 5},
+ 370294: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Hydratyr (E5M5) - Blue key',
+ 'doom_type': 79,
+ 'episode': 5,
+ 'map': 5},
+ 370295: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Colonnade (E5M6) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 5,
+ 'map': 6},
+ 370296: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Colonnade (E5M6) - Green key',
+ 'doom_type': 73,
+ 'episode': 5,
+ 'map': 6},
+ 370297: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Colonnade (E5M6) - Blue key',
+ 'doom_type': 79,
+ 'episode': 5,
+ 'map': 6},
+ 370298: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Foetid Manse (E5M7) - Blue key',
+ 'doom_type': 79,
+ 'episode': 5,
+ 'map': 7},
+ 370299: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Foetid Manse (E5M7) - Green key',
+ 'doom_type': 73,
+ 'episode': 5,
+ 'map': 7},
+ 370300: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Foetid Manse (E5M7) - Yellow key',
+ 'doom_type': 80,
+ 'episode': 5,
+ 'map': 7},
+ 370301: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': "Skein of D'Sparil (E5M9) - Blue key",
+ 'doom_type': 79,
+ 'episode': 5,
+ 'map': 9},
+ 370302: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': "Skein of D'Sparil (E5M9) - Green key",
+ 'doom_type': 73,
+ 'episode': 5,
+ 'map': 9},
+ 370303: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': "Skein of D'Sparil (E5M9) - Yellow key",
+ 'doom_type': 80,
+ 'episode': 5,
+ 'map': 9},
+ 370400: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Docks (E1M1)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 1},
+ 370401: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Docks (E1M1) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 1},
+ 370402: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Docks (E1M1) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 1,
+ 'map': 1},
+ 370403: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Dungeons (E1M2)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 2},
+ 370404: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Dungeons (E1M2) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 2},
+ 370405: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Dungeons (E1M2) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 1,
+ 'map': 2},
+ 370406: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Gatehouse (E1M3)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 3},
+ 370407: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Gatehouse (E1M3) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 3},
+ 370408: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Gatehouse (E1M3) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 1,
+ 'map': 3},
+ 370409: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Guard Tower (E1M4)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 4},
+ 370410: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Guard Tower (E1M4) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 4},
+ 370411: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Guard Tower (E1M4) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 1,
+ 'map': 4},
+ 370412: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Citadel (E1M5)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 5},
+ 370413: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Citadel (E1M5) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 5},
+ 370414: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Citadel (E1M5) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 1,
+ 'map': 5},
+ 370415: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Cathedral (E1M6)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 6},
+ 370416: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Cathedral (E1M6) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 6},
+ 370417: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Cathedral (E1M6) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 1,
+ 'map': 6},
+ 370418: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Crypts (E1M7)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 7},
+ 370419: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Crypts (E1M7) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 7},
+ 370420: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Crypts (E1M7) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 1,
+ 'map': 7},
+ 370421: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': "Hell's Maw (E1M8)",
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 8},
+ 370422: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': "Hell's Maw (E1M8) - Complete",
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 8},
+ 370423: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': "Hell's Maw (E1M8) - Map Scroll",
+ 'doom_type': 35,
+ 'episode': 1,
+ 'map': 8},
+ 370424: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Graveyard (E1M9)',
+ 'doom_type': -1,
+ 'episode': 1,
+ 'map': 9},
+ 370425: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Graveyard (E1M9) - Complete',
+ 'doom_type': -2,
+ 'episode': 1,
+ 'map': 9},
+ 370426: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Graveyard (E1M9) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 1,
+ 'map': 9},
+ 370427: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Crater (E2M1)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 1},
+ 370428: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Crater (E2M1) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 1},
+ 370429: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Crater (E2M1) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 2,
+ 'map': 1},
+ 370430: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Lava Pits (E2M2)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 2},
+ 370431: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Lava Pits (E2M2) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 2},
+ 370432: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Lava Pits (E2M2) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 2,
+ 'map': 2},
+ 370433: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The River of Fire (E2M3)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 3},
+ 370434: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The River of Fire (E2M3) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 3},
+ 370435: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The River of Fire (E2M3) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 2,
+ 'map': 3},
+ 370436: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Ice Grotto (E2M4)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 4},
+ 370437: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Ice Grotto (E2M4) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 4},
+ 370438: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Ice Grotto (E2M4) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 2,
+ 'map': 4},
+ 370439: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Catacombs (E2M5)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 5},
+ 370440: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Catacombs (E2M5) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 5},
+ 370441: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Catacombs (E2M5) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 2,
+ 'map': 5},
+ 370442: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Labyrinth (E2M6)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 6},
+ 370443: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Labyrinth (E2M6) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 6},
+ 370444: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Labyrinth (E2M6) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 2,
+ 'map': 6},
+ 370445: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Great Hall (E2M7)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 7},
+ 370446: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Great Hall (E2M7) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 7},
+ 370447: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Great Hall (E2M7) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 2,
+ 'map': 7},
+ 370448: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Portals of Chaos (E2M8)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 8},
+ 370449: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Portals of Chaos (E2M8) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 8},
+ 370450: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Portals of Chaos (E2M8) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 2,
+ 'map': 8},
+ 370451: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Glacier (E2M9)',
+ 'doom_type': -1,
+ 'episode': 2,
+ 'map': 9},
+ 370452: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Glacier (E2M9) - Complete',
+ 'doom_type': -2,
+ 'episode': 2,
+ 'map': 9},
+ 370453: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Glacier (E2M9) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 2,
+ 'map': 9},
+ 370454: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Storehouse (E3M1)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 1},
+ 370455: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Storehouse (E3M1) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 1},
+ 370456: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Storehouse (E3M1) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 3,
+ 'map': 1},
+ 370457: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Cesspool (E3M2)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 2},
+ 370458: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Cesspool (E3M2) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 2},
+ 370459: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Cesspool (E3M2) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 3,
+ 'map': 2},
+ 370460: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Confluence (E3M3)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 3},
+ 370461: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Confluence (E3M3) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 3},
+ 370462: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Confluence (E3M3) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 3,
+ 'map': 3},
+ 370463: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Azure Fortress (E3M4)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 4},
+ 370464: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Azure Fortress (E3M4) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 4},
+ 370465: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Azure Fortress (E3M4) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 3,
+ 'map': 4},
+ 370466: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Ophidian Lair (E3M5)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 5},
+ 370467: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Ophidian Lair (E3M5) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 5},
+ 370468: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Ophidian Lair (E3M5) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 3,
+ 'map': 5},
+ 370469: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Halls of Fear (E3M6)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 6},
+ 370470: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Halls of Fear (E3M6) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 6},
+ 370471: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Halls of Fear (E3M6) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 3,
+ 'map': 6},
+ 370472: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Chasm (E3M7)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 7},
+ 370473: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Chasm (E3M7) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 7},
+ 370474: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Chasm (E3M7) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 3,
+ 'map': 7},
+ 370475: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': "D'Sparil'S Keep (E3M8)",
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 8},
+ 370476: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': "D'Sparil'S Keep (E3M8) - Complete",
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 8},
+ 370477: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': "D'Sparil'S Keep (E3M8) - Map Scroll",
+ 'doom_type': 35,
+ 'episode': 3,
+ 'map': 8},
+ 370478: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Aquifier (E3M9)',
+ 'doom_type': -1,
+ 'episode': 3,
+ 'map': 9},
+ 370479: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'The Aquifier (E3M9) - Complete',
+ 'doom_type': -2,
+ 'episode': 3,
+ 'map': 9},
+ 370480: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'The Aquifier (E3M9) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 3,
+ 'map': 9},
+ 370481: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Catafalque (E4M1)',
+ 'doom_type': -1,
+ 'episode': 4,
+ 'map': 1},
+ 370482: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Catafalque (E4M1) - Complete',
+ 'doom_type': -2,
+ 'episode': 4,
+ 'map': 1},
+ 370483: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Catafalque (E4M1) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 4,
+ 'map': 1},
+ 370484: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Blockhouse (E4M2)',
+ 'doom_type': -1,
+ 'episode': 4,
+ 'map': 2},
+ 370485: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Blockhouse (E4M2) - Complete',
+ 'doom_type': -2,
+ 'episode': 4,
+ 'map': 2},
+ 370486: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Blockhouse (E4M2) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 4,
+ 'map': 2},
+ 370487: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ambulatory (E4M3)',
+ 'doom_type': -1,
+ 'episode': 4,
+ 'map': 3},
+ 370488: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ambulatory (E4M3) - Complete',
+ 'doom_type': -2,
+ 'episode': 4,
+ 'map': 3},
+ 370489: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Ambulatory (E4M3) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 4,
+ 'map': 3},
+ 370490: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Sepulcher (E4M4)',
+ 'doom_type': -1,
+ 'episode': 4,
+ 'map': 4},
+ 370491: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Sepulcher (E4M4) - Complete',
+ 'doom_type': -2,
+ 'episode': 4,
+ 'map': 4},
+ 370492: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Sepulcher (E4M4) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 4,
+ 'map': 4},
+ 370493: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Great Stair (E4M5)',
+ 'doom_type': -1,
+ 'episode': 4,
+ 'map': 5},
+ 370494: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Great Stair (E4M5) - Complete',
+ 'doom_type': -2,
+ 'episode': 4,
+ 'map': 5},
+ 370495: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Great Stair (E4M5) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 4,
+ 'map': 5},
+ 370496: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Halls of the Apostate (E4M6)',
+ 'doom_type': -1,
+ 'episode': 4,
+ 'map': 6},
+ 370497: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Halls of the Apostate (E4M6) - Complete',
+ 'doom_type': -2,
+ 'episode': 4,
+ 'map': 6},
+ 370498: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Halls of the Apostate (E4M6) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 4,
+ 'map': 6},
+ 370499: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ramparts of Perdition (E4M7)',
+ 'doom_type': -1,
+ 'episode': 4,
+ 'map': 7},
+ 370500: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ramparts of Perdition (E4M7) - Complete',
+ 'doom_type': -2,
+ 'episode': 4,
+ 'map': 7},
+ 370501: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Ramparts of Perdition (E4M7) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 4,
+ 'map': 7},
+ 370502: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Shattered Bridge (E4M8)',
+ 'doom_type': -1,
+ 'episode': 4,
+ 'map': 8},
+ 370503: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Shattered Bridge (E4M8) - Complete',
+ 'doom_type': -2,
+ 'episode': 4,
+ 'map': 8},
+ 370504: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Shattered Bridge (E4M8) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 4,
+ 'map': 8},
+ 370505: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Mausoleum (E4M9)',
+ 'doom_type': -1,
+ 'episode': 4,
+ 'map': 9},
+ 370506: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Mausoleum (E4M9) - Complete',
+ 'doom_type': -2,
+ 'episode': 4,
+ 'map': 9},
+ 370507: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Mausoleum (E4M9) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 4,
+ 'map': 9},
+ 370508: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ochre Cliffs (E5M1)',
+ 'doom_type': -1,
+ 'episode': 5,
+ 'map': 1},
+ 370509: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Ochre Cliffs (E5M1) - Complete',
+ 'doom_type': -2,
+ 'episode': 5,
+ 'map': 1},
+ 370510: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Ochre Cliffs (E5M1) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 5,
+ 'map': 1},
+ 370511: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Rapids (E5M2)',
+ 'doom_type': -1,
+ 'episode': 5,
+ 'map': 2},
+ 370512: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Rapids (E5M2) - Complete',
+ 'doom_type': -2,
+ 'episode': 5,
+ 'map': 2},
+ 370513: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Rapids (E5M2) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 5,
+ 'map': 2},
+ 370514: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Quay (E5M3)',
+ 'doom_type': -1,
+ 'episode': 5,
+ 'map': 3},
+ 370515: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Quay (E5M3) - Complete',
+ 'doom_type': -2,
+ 'episode': 5,
+ 'map': 3},
+ 370516: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Quay (E5M3) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 5,
+ 'map': 3},
+ 370517: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Courtyard (E5M4)',
+ 'doom_type': -1,
+ 'episode': 5,
+ 'map': 4},
+ 370518: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Courtyard (E5M4) - Complete',
+ 'doom_type': -2,
+ 'episode': 5,
+ 'map': 4},
+ 370519: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Courtyard (E5M4) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 5,
+ 'map': 4},
+ 370520: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Hydratyr (E5M5)',
+ 'doom_type': -1,
+ 'episode': 5,
+ 'map': 5},
+ 370521: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Hydratyr (E5M5) - Complete',
+ 'doom_type': -2,
+ 'episode': 5,
+ 'map': 5},
+ 370522: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Hydratyr (E5M5) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 5,
+ 'map': 5},
+ 370523: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Colonnade (E5M6)',
+ 'doom_type': -1,
+ 'episode': 5,
+ 'map': 6},
+ 370524: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Colonnade (E5M6) - Complete',
+ 'doom_type': -2,
+ 'episode': 5,
+ 'map': 6},
+ 370525: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Colonnade (E5M6) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 5,
+ 'map': 6},
+ 370526: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Foetid Manse (E5M7)',
+ 'doom_type': -1,
+ 'episode': 5,
+ 'map': 7},
+ 370527: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Foetid Manse (E5M7) - Complete',
+ 'doom_type': -2,
+ 'episode': 5,
+ 'map': 7},
+ 370528: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Foetid Manse (E5M7) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 5,
+ 'map': 7},
+ 370529: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Field of Judgement (E5M8)',
+ 'doom_type': -1,
+ 'episode': 5,
+ 'map': 8},
+ 370530: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': 'Field of Judgement (E5M8) - Complete',
+ 'doom_type': -2,
+ 'episode': 5,
+ 'map': 8},
+ 370531: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': 'Field of Judgement (E5M8) - Map Scroll',
+ 'doom_type': 35,
+ 'episode': 5,
+ 'map': 8},
+ 370532: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': "Skein of D'Sparil (E5M9)",
+ 'doom_type': -1,
+ 'episode': 5,
+ 'map': 9},
+ 370533: {'classification': ItemClassification.progression,
+ 'count': 1,
+ 'name': "Skein of D'Sparil (E5M9) - Complete",
+ 'doom_type': -2,
+ 'episode': 5,
+ 'map': 9},
+ 370534: {'classification': ItemClassification.filler,
+ 'count': 1,
+ 'name': "Skein of D'Sparil (E5M9) - Map Scroll",
+ 'doom_type': 35,
+ 'episode': 5,
+ 'map': 9},
+}
+
+
+item_name_groups: Dict[str, Set[str]] = {
+ 'Ammos': {'Crystal Geode', 'Energy Orb', 'Greater Runes', 'Inferno Orb', 'Pile of Mace Spheres', 'Quiver of Ethereal Arrows', },
+ 'Armors': {'Enchanted Shield', 'Silver Shield', },
+ 'Artifacts': {'Chaos Device', 'Morph Ovum', 'Mystic Urn', 'Quartz Flask', 'Ring of Invincibility', 'Shadowsphere', 'Timebomb of the Ancients', 'Tome of Power', 'Torch', },
+ 'Keys': {'Ambulatory (E4M3) - Blue key', 'Ambulatory (E4M3) - Green key', 'Ambulatory (E4M3) - Yellow key', 'Blockhouse (E4M2) - Blue key', 'Blockhouse (E4M2) - Green key', 'Blockhouse (E4M2) - Yellow key', 'Catafalque (E4M1) - Green key', 'Catafalque (E4M1) - Yellow key', 'Colonnade (E5M6) - Blue key', 'Colonnade (E5M6) - Green key', 'Colonnade (E5M6) - Yellow key', 'Courtyard (E5M4) - Blue key', 'Courtyard (E5M4) - Green key', 'Courtyard (E5M4) - Yellow key', 'Foetid Manse (E5M7) - Blue key', 'Foetid Manse (E5M7) - Green key', 'Foetid Manse (E5M7) - Yellow key', 'Great Stair (E4M5) - Blue key', 'Great Stair (E4M5) - Green key', 'Great Stair (E4M5) - Yellow key', 'Halls of the Apostate (E4M6) - Blue key', 'Halls of the Apostate (E4M6) - Green key', 'Halls of the Apostate (E4M6) - Yellow key', 'Hydratyr (E5M5) - Blue key', 'Hydratyr (E5M5) - Green key', 'Hydratyr (E5M5) - Yellow key', 'Mausoleum (E4M9) - Yellow key', 'Ochre Cliffs (E5M1) - Blue key', 'Ochre Cliffs (E5M1) - Green key', 'Ochre Cliffs (E5M1) - Yellow key', 'Quay (E5M3) - Blue key', 'Quay (E5M3) - Green key', 'Quay (E5M3) - Yellow key', 'Ramparts of Perdition (E4M7) - Blue key', 'Ramparts of Perdition (E4M7) - Green key', 'Ramparts of Perdition (E4M7) - Yellow key', 'Rapids (E5M2) - Green key', 'Rapids (E5M2) - Yellow key', 'Shattered Bridge (E4M8) - Yellow key', "Skein of D'Sparil (E5M9) - Blue key", "Skein of D'Sparil (E5M9) - Green key", "Skein of D'Sparil (E5M9) - Yellow key", 'The Aquifier (E3M9) - Blue key', 'The Aquifier (E3M9) - Green key', 'The Aquifier (E3M9) - Yellow key', 'The Azure Fortress (E3M4) - Green key', 'The Azure Fortress (E3M4) - Yellow key', 'The Catacombs (E2M5) - Blue key', 'The Catacombs (E2M5) - Green key', 'The Catacombs (E2M5) - Yellow key', 'The Cathedral (E1M6) - Green key', 'The Cathedral (E1M6) - Yellow key', 'The Cesspool (E3M2) - Blue key', 'The Cesspool (E3M2) - Green key', 'The Cesspool (E3M2) - Yellow key', 'The Chasm (E3M7) - Blue key', 'The Chasm (E3M7) - Green key', 'The Chasm (E3M7) - Yellow key', 'The Citadel (E1M5) - Blue key', 'The Citadel (E1M5) - Green key', 'The Citadel (E1M5) - Yellow key', 'The Confluence (E3M3) - Blue key', 'The Confluence (E3M3) - Green key', 'The Confluence (E3M3) - Yellow key', 'The Crater (E2M1) - Green key', 'The Crater (E2M1) - Yellow key', 'The Crypts (E1M7) - Blue key', 'The Crypts (E1M7) - Green key', 'The Crypts (E1M7) - Yellow key', 'The Docks (E1M1) - Yellow key', 'The Dungeons (E1M2) - Blue key', 'The Dungeons (E1M2) - Green key', 'The Dungeons (E1M2) - Yellow key', 'The Gatehouse (E1M3) - Green key', 'The Gatehouse (E1M3) - Yellow key', 'The Glacier (E2M9) - Blue key', 'The Glacier (E2M9) - Green key', 'The Glacier (E2M9) - Yellow key', 'The Graveyard (E1M9) - Blue key', 'The Graveyard (E1M9) - Green key', 'The Graveyard (E1M9) - Yellow key', 'The Great Hall (E2M7) - Blue key', 'The Great Hall (E2M7) - Green key', 'The Great Hall (E2M7) - Yellow key', 'The Guard Tower (E1M4) - Green key', 'The Guard Tower (E1M4) - Yellow key', 'The Halls of Fear (E3M6) - Blue key', 'The Halls of Fear (E3M6) - Green key', 'The Halls of Fear (E3M6) - Yellow key', 'The Ice Grotto (E2M4) - Blue key', 'The Ice Grotto (E2M4) - Green key', 'The Ice Grotto (E2M4) - Yellow key', 'The Labyrinth (E2M6) - Blue key', 'The Labyrinth (E2M6) - Green key', 'The Labyrinth (E2M6) - Yellow key', 'The Lava Pits (E2M2) - Green key', 'The Lava Pits (E2M2) - Yellow key', 'The Ophidian Lair (E3M5) - Green key', 'The Ophidian Lair (E3M5) - Yellow key', 'The River of Fire (E2M3) - Blue key', 'The River of Fire (E2M3) - Green key', 'The River of Fire (E2M3) - Yellow key', 'The Storehouse (E3M1) - Green key', 'The Storehouse (E3M1) - Yellow key', },
+ 'Levels': {'Ambulatory (E4M3)', 'Blockhouse (E4M2)', 'Catafalque (E4M1)', 'Colonnade (E5M6)', 'Courtyard (E5M4)', "D'Sparil'S Keep (E3M8)", 'Field of Judgement (E5M8)', 'Foetid Manse (E5M7)', 'Great Stair (E4M5)', 'Halls of the Apostate (E4M6)', "Hell's Maw (E1M8)", 'Hydratyr (E5M5)', 'Mausoleum (E4M9)', 'Ochre Cliffs (E5M1)', 'Quay (E5M3)', 'Ramparts of Perdition (E4M7)', 'Rapids (E5M2)', 'Sepulcher (E4M4)', 'Shattered Bridge (E4M8)', "Skein of D'Sparil (E5M9)", 'The Aquifier (E3M9)', 'The Azure Fortress (E3M4)', 'The Catacombs (E2M5)', 'The Cathedral (E1M6)', 'The Cesspool (E3M2)', 'The Chasm (E3M7)', 'The Citadel (E1M5)', 'The Confluence (E3M3)', 'The Crater (E2M1)', 'The Crypts (E1M7)', 'The Docks (E1M1)', 'The Dungeons (E1M2)', 'The Gatehouse (E1M3)', 'The Glacier (E2M9)', 'The Graveyard (E1M9)', 'The Great Hall (E2M7)', 'The Guard Tower (E1M4)', 'The Halls of Fear (E3M6)', 'The Ice Grotto (E2M4)', 'The Labyrinth (E2M6)', 'The Lava Pits (E2M2)', 'The Ophidian Lair (E3M5)', 'The Portals of Chaos (E2M8)', 'The River of Fire (E2M3)', 'The Storehouse (E3M1)', },
+ 'Map Scrolls': {'Ambulatory (E4M3) - Map Scroll', 'Blockhouse (E4M2) - Map Scroll', 'Catafalque (E4M1) - Map Scroll', 'Colonnade (E5M6) - Map Scroll', 'Courtyard (E5M4) - Map Scroll', "D'Sparil'S Keep (E3M8) - Map Scroll", 'Field of Judgement (E5M8) - Map Scroll', 'Foetid Manse (E5M7) - Map Scroll', 'Great Stair (E4M5) - Map Scroll', 'Halls of the Apostate (E4M6) - Map Scroll', "Hell's Maw (E1M8) - Map Scroll", 'Hydratyr (E5M5) - Map Scroll', 'Mausoleum (E4M9) - Map Scroll', 'Ochre Cliffs (E5M1) - Map Scroll', 'Quay (E5M3) - Map Scroll', 'Ramparts of Perdition (E4M7) - Map Scroll', 'Rapids (E5M2) - Map Scroll', 'Sepulcher (E4M4) - Map Scroll', 'Shattered Bridge (E4M8) - Map Scroll', "Skein of D'Sparil (E5M9) - Map Scroll", 'The Aquifier (E3M9) - Map Scroll', 'The Azure Fortress (E3M4) - Map Scroll', 'The Catacombs (E2M5) - Map Scroll', 'The Cathedral (E1M6) - Map Scroll', 'The Cesspool (E3M2) - Map Scroll', 'The Chasm (E3M7) - Map Scroll', 'The Citadel (E1M5) - Map Scroll', 'The Confluence (E3M3) - Map Scroll', 'The Crater (E2M1) - Map Scroll', 'The Crypts (E1M7) - Map Scroll', 'The Docks (E1M1) - Map Scroll', 'The Dungeons (E1M2) - Map Scroll', 'The Gatehouse (E1M3) - Map Scroll', 'The Glacier (E2M9) - Map Scroll', 'The Graveyard (E1M9) - Map Scroll', 'The Great Hall (E2M7) - Map Scroll', 'The Guard Tower (E1M4) - Map Scroll', 'The Halls of Fear (E3M6) - Map Scroll', 'The Ice Grotto (E2M4) - Map Scroll', 'The Labyrinth (E2M6) - Map Scroll', 'The Lava Pits (E2M2) - Map Scroll', 'The Ophidian Lair (E3M5) - Map Scroll', 'The Portals of Chaos (E2M8) - Map Scroll', 'The River of Fire (E2M3) - Map Scroll', 'The Storehouse (E3M1) - Map Scroll', },
+ 'Weapons': {'Dragon Claw', 'Ethereal Crossbow', 'Firemace', 'Gauntlets of the Necromancer', 'Hellstaff', 'Phoenix Rod', },
+}
diff --git a/worlds/heretic/Locations.py b/worlds/heretic/Locations.py
new file mode 100644
index 000000000000..ff32df7b34c5
--- /dev/null
+++ b/worlds/heretic/Locations.py
@@ -0,0 +1,8229 @@
+# This file is auto generated. More info: https://github.com/Daivuk/apdoom
+
+from typing import Dict, TypedDict, List, Set
+
+
+class LocationDict(TypedDict, total=False):
+ name: str
+ episode: int
+ check_sanity: bool
+ map: int
+ index: int # Thing index as it is stored in the wad file.
+ doom_type: int # In case index end up unreliable, we can use doom type. Maps have often only one of each important things.
+ region: str
+
+
+location_table: Dict[int, LocationDict] = {
+ 371000: {'name': 'The Docks (E1M1) - Yellow key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 5,
+ 'doom_type': 80,
+ 'region': "The Docks (E1M1) Main"},
+ 371001: {'name': 'The Docks (E1M1) - Silver Shield',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 47,
+ 'doom_type': 85,
+ 'region': "The Docks (E1M1) Main"},
+ 371002: {'name': 'The Docks (E1M1) - Gauntlets of the Necromancer',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 52,
+ 'doom_type': 2005,
+ 'region': "The Docks (E1M1) Yellow"},
+ 371003: {'name': 'The Docks (E1M1) - Ethereal Crossbow',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 55,
+ 'doom_type': 2001,
+ 'region': "The Docks (E1M1) Yellow"},
+ 371004: {'name': 'The Docks (E1M1) - Bag of Holding',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 91,
+ 'doom_type': 8,
+ 'region': "The Docks (E1M1) Sea"},
+ 371005: {'name': 'The Docks (E1M1) - Tome of Power',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 174,
+ 'doom_type': 86,
+ 'region': "The Docks (E1M1) Yellow"},
+ 371006: {'name': 'The Docks (E1M1) - Exit',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Docks (E1M1) Yellow"},
+ 371007: {'name': 'The Dungeons (E1M2) - Dragon Claw',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 1,
+ 'doom_type': 53,
+ 'region': "The Dungeons (E1M2) Yellow"},
+ 371008: {'name': 'The Dungeons (E1M2) - Yellow key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 5,
+ 'doom_type': 80,
+ 'region': "The Dungeons (E1M2) Main"},
+ 371009: {'name': 'The Dungeons (E1M2) - Green key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 17,
+ 'doom_type': 73,
+ 'region': "The Dungeons (E1M2) Yellow"},
+ 371010: {'name': 'The Dungeons (E1M2) - Silver Shield',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 18,
+ 'doom_type': 85,
+ 'region': "The Dungeons (E1M2) Main"},
+ 371011: {'name': 'The Dungeons (E1M2) - Torch',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 19,
+ 'doom_type': 33,
+ 'region': "The Dungeons (E1M2) Main"},
+ 371012: {'name': 'The Dungeons (E1M2) - Map Scroll',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 29,
+ 'doom_type': 35,
+ 'region': "The Dungeons (E1M2) Yellow"},
+ 371013: {'name': 'The Dungeons (E1M2) - Shadowsphere',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 41,
+ 'doom_type': 75,
+ 'region': "The Dungeons (E1M2) Yellow"},
+ 371014: {'name': 'The Dungeons (E1M2) - Bag of Holding',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 44,
+ 'doom_type': 8,
+ 'region': "The Dungeons (E1M2) Green"},
+ 371015: {'name': 'The Dungeons (E1M2) - Blue key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 45,
+ 'doom_type': 79,
+ 'region': "The Dungeons (E1M2) Green"},
+ 371016: {'name': 'The Dungeons (E1M2) - Ring of Invincibility',
+ 'episode': 1,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 46,
+ 'doom_type': 84,
+ 'region': "The Dungeons (E1M2) Yellow"},
+ 371017: {'name': 'The Dungeons (E1M2) - Tome of Power',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 77,
+ 'doom_type': 86,
+ 'region': "The Dungeons (E1M2) Main"},
+ 371018: {'name': 'The Dungeons (E1M2) - Ethereal Crossbow',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 80,
+ 'doom_type': 2001,
+ 'region': "The Dungeons (E1M2) Main"},
+ 371019: {'name': 'The Dungeons (E1M2) - Tome of Power 2',
+ 'episode': 1,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 81,
+ 'doom_type': 86,
+ 'region': "The Dungeons (E1M2) Yellow"},
+ 371020: {'name': 'The Dungeons (E1M2) - Gauntlets of the Necromancer',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 253,
+ 'doom_type': 2005,
+ 'region': "The Dungeons (E1M2) Yellow"},
+ 371021: {'name': 'The Dungeons (E1M2) - Silver Shield 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 303,
+ 'doom_type': 85,
+ 'region': "The Dungeons (E1M2) Yellow"},
+ 371022: {'name': 'The Dungeons (E1M2) - Exit',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Dungeons (E1M2) Blue"},
+ 371023: {'name': 'The Gatehouse (E1M3) - Yellow key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 8,
+ 'doom_type': 80,
+ 'region': "The Gatehouse (E1M3) Main"},
+ 371024: {'name': 'The Gatehouse (E1M3) - Green key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 9,
+ 'doom_type': 73,
+ 'region': "The Gatehouse (E1M3) Yellow"},
+ 371025: {'name': 'The Gatehouse (E1M3) - Dragon Claw',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 10,
+ 'doom_type': 53,
+ 'region': "The Gatehouse (E1M3) Main"},
+ 371026: {'name': 'The Gatehouse (E1M3) - Silver Shield',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 22,
+ 'doom_type': 85,
+ 'region': "The Gatehouse (E1M3) Main"},
+ 371027: {'name': 'The Gatehouse (E1M3) - Ethereal Crossbow',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 24,
+ 'doom_type': 2001,
+ 'region': "The Gatehouse (E1M3) Main"},
+ 371028: {'name': 'The Gatehouse (E1M3) - Tome of Power',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 81,
+ 'doom_type': 86,
+ 'region': "The Gatehouse (E1M3) Sea"},
+ 371029: {'name': 'The Gatehouse (E1M3) - Bag of Holding',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 134,
+ 'doom_type': 8,
+ 'region': "The Gatehouse (E1M3) Yellow"},
+ 371030: {'name': 'The Gatehouse (E1M3) - Gauntlets of the Necromancer',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 145,
+ 'doom_type': 2005,
+ 'region': "The Gatehouse (E1M3) Yellow"},
+ 371031: {'name': 'The Gatehouse (E1M3) - Torch',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 203,
+ 'doom_type': 33,
+ 'region': "The Gatehouse (E1M3) Main"},
+ 371032: {'name': 'The Gatehouse (E1M3) - Ring of Invincibility',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 220,
+ 'doom_type': 84,
+ 'region': "The Gatehouse (E1M3) Yellow"},
+ 371033: {'name': 'The Gatehouse (E1M3) - Shadowsphere',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 221,
+ 'doom_type': 75,
+ 'region': "The Gatehouse (E1M3) Main"},
+ 371034: {'name': 'The Gatehouse (E1M3) - Morph Ovum',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 222,
+ 'doom_type': 30,
+ 'region': "The Gatehouse (E1M3) Yellow"},
+ 371035: {'name': 'The Gatehouse (E1M3) - Tome of Power 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 286,
+ 'doom_type': 86,
+ 'region': "The Gatehouse (E1M3) Main"},
+ 371036: {'name': 'The Gatehouse (E1M3) - Tome of Power 3',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 287,
+ 'doom_type': 86,
+ 'region': "The Gatehouse (E1M3) Main"},
+ 371037: {'name': 'The Gatehouse (E1M3) - Exit',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Gatehouse (E1M3) Green"},
+ 371038: {'name': 'The Guard Tower (E1M4) - Gauntlets of the Necromancer',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 0,
+ 'doom_type': 2005,
+ 'region': "The Guard Tower (E1M4) Main"},
+ 371039: {'name': 'The Guard Tower (E1M4) - Dragon Claw',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 2,
+ 'doom_type': 53,
+ 'region': "The Guard Tower (E1M4) Main"},
+ 371040: {'name': 'The Guard Tower (E1M4) - Ethereal Crossbow',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 3,
+ 'doom_type': 2001,
+ 'region': "The Guard Tower (E1M4) Main"},
+ 371041: {'name': 'The Guard Tower (E1M4) - Yellow key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 4,
+ 'doom_type': 80,
+ 'region': "The Guard Tower (E1M4) Main"},
+ 371042: {'name': 'The Guard Tower (E1M4) - Morph Ovum',
+ 'episode': 1,
+ 'check_sanity': True,
+ 'map': 4,
+ 'index': 5,
+ 'doom_type': 30,
+ 'region': "The Guard Tower (E1M4) Main"},
+ 371043: {'name': 'The Guard Tower (E1M4) - Shadowsphere',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 57,
+ 'doom_type': 75,
+ 'region': "The Guard Tower (E1M4) Yellow"},
+ 371044: {'name': 'The Guard Tower (E1M4) - Green key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 60,
+ 'doom_type': 73,
+ 'region': "The Guard Tower (E1M4) Yellow"},
+ 371045: {'name': 'The Guard Tower (E1M4) - Bag of Holding',
+ 'episode': 1,
+ 'check_sanity': True,
+ 'map': 4,
+ 'index': 61,
+ 'doom_type': 8,
+ 'region': "The Guard Tower (E1M4) Main"},
+ 371046: {'name': 'The Guard Tower (E1M4) - Map Scroll',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 64,
+ 'doom_type': 35,
+ 'region': "The Guard Tower (E1M4) Main"},
+ 371047: {'name': 'The Guard Tower (E1M4) - Tome of Power',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 77,
+ 'doom_type': 86,
+ 'region': "The Guard Tower (E1M4) Main"},
+ 371048: {'name': 'The Guard Tower (E1M4) - Silver Shield',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 78,
+ 'doom_type': 85,
+ 'region': "The Guard Tower (E1M4) Main"},
+ 371049: {'name': 'The Guard Tower (E1M4) - Torch',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 143,
+ 'doom_type': 33,
+ 'region': "The Guard Tower (E1M4) Main"},
+ 371050: {'name': 'The Guard Tower (E1M4) - Tome of Power 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 220,
+ 'doom_type': 86,
+ 'region': "The Guard Tower (E1M4) Yellow"},
+ 371051: {'name': 'The Guard Tower (E1M4) - Tome of Power 3',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 221,
+ 'doom_type': 86,
+ 'region': "The Guard Tower (E1M4) Main"},
+ 371052: {'name': 'The Guard Tower (E1M4) - Exit',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Guard Tower (E1M4) Green"},
+ 371053: {'name': 'The Citadel (E1M5) - Green key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 1,
+ 'doom_type': 73,
+ 'region': "The Citadel (E1M5) Yellow"},
+ 371054: {'name': 'The Citadel (E1M5) - Yellow key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 5,
+ 'doom_type': 80,
+ 'region': "The Citadel (E1M5) Main"},
+ 371055: {'name': 'The Citadel (E1M5) - Blue key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 19,
+ 'doom_type': 79,
+ 'region': "The Citadel (E1M5) Green"},
+ 371056: {'name': 'The Citadel (E1M5) - Tome of Power',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 23,
+ 'doom_type': 86,
+ 'region': "The Citadel (E1M5) Well"},
+ 371057: {'name': 'The Citadel (E1M5) - Ethereal Crossbow',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 28,
+ 'doom_type': 2001,
+ 'region': "The Citadel (E1M5) Yellow"},
+ 371058: {'name': 'The Citadel (E1M5) - Gauntlets of the Necromancer',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 29,
+ 'doom_type': 2005,
+ 'region': "The Citadel (E1M5) Main"},
+ 371059: {'name': 'The Citadel (E1M5) - Dragon Claw',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 30,
+ 'doom_type': 53,
+ 'region': "The Citadel (E1M5) Green"},
+ 371060: {'name': 'The Citadel (E1M5) - Ring of Invincibility',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 31,
+ 'doom_type': 84,
+ 'region': "The Citadel (E1M5) Green"},
+ 371061: {'name': 'The Citadel (E1M5) - Tome of Power 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 78,
+ 'doom_type': 86,
+ 'region': "The Citadel (E1M5) Blue"},
+ 371062: {'name': 'The Citadel (E1M5) - Shadowsphere',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 79,
+ 'doom_type': 75,
+ 'region': "The Citadel (E1M5) Main"},
+ 371063: {'name': 'The Citadel (E1M5) - Bag of Holding',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 80,
+ 'doom_type': 8,
+ 'region': "The Citadel (E1M5) Green"},
+ 371064: {'name': 'The Citadel (E1M5) - Torch',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 103,
+ 'doom_type': 33,
+ 'region': "The Citadel (E1M5) Main"},
+ 371065: {'name': 'The Citadel (E1M5) - Tome of Power 3',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 105,
+ 'doom_type': 86,
+ 'region': "The Citadel (E1M5) Green"},
+ 371066: {'name': 'The Citadel (E1M5) - Silver Shield',
+ 'episode': 1,
+ 'check_sanity': True,
+ 'map': 5,
+ 'index': 129,
+ 'doom_type': 85,
+ 'region': "The Citadel (E1M5) Main"},
+ 371067: {'name': 'The Citadel (E1M5) - Morph Ovum',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 192,
+ 'doom_type': 30,
+ 'region': "The Citadel (E1M5) Green"},
+ 371068: {'name': 'The Citadel (E1M5) - Map Scroll',
+ 'episode': 1,
+ 'check_sanity': True,
+ 'map': 5,
+ 'index': 203,
+ 'doom_type': 35,
+ 'region': "The Citadel (E1M5) Blue"},
+ 371069: {'name': 'The Citadel (E1M5) - Silver Shield 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 204,
+ 'doom_type': 85,
+ 'region': "The Citadel (E1M5) Blue"},
+ 371070: {'name': 'The Citadel (E1M5) - Torch 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 205,
+ 'doom_type': 33,
+ 'region': "The Citadel (E1M5) Green"},
+ 371071: {'name': 'The Citadel (E1M5) - Tome of Power 4',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 319,
+ 'doom_type': 86,
+ 'region': "The Citadel (E1M5) Green"},
+ 371072: {'name': 'The Citadel (E1M5) - Tome of Power 5',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 320,
+ 'doom_type': 86,
+ 'region': "The Citadel (E1M5) Green"},
+ 371073: {'name': 'The Citadel (E1M5) - Exit',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Citadel (E1M5) Blue"},
+ 371074: {'name': 'The Cathedral (E1M6) - Yellow key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 8,
+ 'doom_type': 80,
+ 'region': "The Cathedral (E1M6) Main"},
+ 371075: {'name': 'The Cathedral (E1M6) - Gauntlets of the Necromancer',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 9,
+ 'doom_type': 2005,
+ 'region': "The Cathedral (E1M6) Main"},
+ 371076: {'name': 'The Cathedral (E1M6) - Ethereal Crossbow',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 39,
+ 'doom_type': 2001,
+ 'region': "The Cathedral (E1M6) Yellow"},
+ 371077: {'name': 'The Cathedral (E1M6) - Dragon Claw',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 45,
+ 'doom_type': 53,
+ 'region': "The Cathedral (E1M6) Yellow"},
+ 371078: {'name': 'The Cathedral (E1M6) - Tome of Power',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 56,
+ 'doom_type': 86,
+ 'region': "The Cathedral (E1M6) Yellow"},
+ 371079: {'name': 'The Cathedral (E1M6) - Shadowsphere',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 61,
+ 'doom_type': 75,
+ 'region': "The Cathedral (E1M6) Yellow"},
+ 371080: {'name': 'The Cathedral (E1M6) - Green key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 98,
+ 'doom_type': 73,
+ 'region': "The Cathedral (E1M6) Yellow"},
+ 371081: {'name': 'The Cathedral (E1M6) - Silver Shield',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 138,
+ 'doom_type': 85,
+ 'region': "The Cathedral (E1M6) Yellow"},
+ 371082: {'name': 'The Cathedral (E1M6) - Bag of Holding',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 139,
+ 'doom_type': 8,
+ 'region': "The Cathedral (E1M6) Yellow"},
+ 371083: {'name': 'The Cathedral (E1M6) - Ring of Invincibility',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 142,
+ 'doom_type': 84,
+ 'region': "The Cathedral (E1M6) Yellow"},
+ 371084: {'name': 'The Cathedral (E1M6) - Torch',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 217,
+ 'doom_type': 33,
+ 'region': "The Cathedral (E1M6) Yellow"},
+ 371085: {'name': 'The Cathedral (E1M6) - Tome of Power 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 273,
+ 'doom_type': 86,
+ 'region': "The Cathedral (E1M6) Yellow"},
+ 371086: {'name': 'The Cathedral (E1M6) - Tome of Power 3',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 274,
+ 'doom_type': 86,
+ 'region': "The Cathedral (E1M6) Main"},
+ 371087: {'name': 'The Cathedral (E1M6) - Morph Ovum',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 277,
+ 'doom_type': 30,
+ 'region': "The Cathedral (E1M6) Yellow"},
+ 371088: {'name': 'The Cathedral (E1M6) - Map Scroll',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 279,
+ 'doom_type': 35,
+ 'region': "The Cathedral (E1M6) Yellow"},
+ 371089: {'name': 'The Cathedral (E1M6) - Ring of Invincibility 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 280,
+ 'doom_type': 84,
+ 'region': "The Cathedral (E1M6) Yellow"},
+ 371090: {'name': 'The Cathedral (E1M6) - Silver Shield 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 281,
+ 'doom_type': 85,
+ 'region': "The Cathedral (E1M6) Green"},
+ 371091: {'name': 'The Cathedral (E1M6) - Tome of Power 4',
+ 'episode': 1,
+ 'check_sanity': True,
+ 'map': 6,
+ 'index': 371,
+ 'doom_type': 86,
+ 'region': "The Cathedral (E1M6) Green"},
+ 371092: {'name': 'The Cathedral (E1M6) - Bag of Holding 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 449,
+ 'doom_type': 8,
+ 'region': "The Cathedral (E1M6) Green"},
+ 371093: {'name': 'The Cathedral (E1M6) - Silver Shield 3',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 457,
+ 'doom_type': 85,
+ 'region': "The Cathedral (E1M6) Main Fly"},
+ 371094: {'name': 'The Cathedral (E1M6) - Bag of Holding 3',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 458,
+ 'doom_type': 8,
+ 'region': "The Cathedral (E1M6) Main Fly"},
+ 371095: {'name': 'The Cathedral (E1M6) - Exit',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Cathedral (E1M6) Green"},
+ 371096: {'name': 'The Crypts (E1M7) - Yellow key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 11,
+ 'doom_type': 80,
+ 'region': "The Crypts (E1M7) Main"},
+ 371097: {'name': 'The Crypts (E1M7) - Ethereal Crossbow',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 17,
+ 'doom_type': 2001,
+ 'region': "The Crypts (E1M7) Yellow"},
+ 371098: {'name': 'The Crypts (E1M7) - Green key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 21,
+ 'doom_type': 73,
+ 'region': "The Crypts (E1M7) Yellow"},
+ 371099: {'name': 'The Crypts (E1M7) - Blue key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 25,
+ 'doom_type': 79,
+ 'region': "The Crypts (E1M7) Green"},
+ 371100: {'name': 'The Crypts (E1M7) - Morph Ovum',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 26,
+ 'doom_type': 30,
+ 'region': "The Crypts (E1M7) Yellow"},
+ 371101: {'name': 'The Crypts (E1M7) - Dragon Claw',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 45,
+ 'doom_type': 53,
+ 'region': "The Crypts (E1M7) Yellow"},
+ 371102: {'name': 'The Crypts (E1M7) - Gauntlets of the Necromancer',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 46,
+ 'doom_type': 2005,
+ 'region': "The Crypts (E1M7) Main"},
+ 371103: {'name': 'The Crypts (E1M7) - Tome of Power',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 53,
+ 'doom_type': 86,
+ 'region': "The Crypts (E1M7) Yellow"},
+ 371104: {'name': 'The Crypts (E1M7) - Ring of Invincibility',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 90,
+ 'doom_type': 84,
+ 'region': "The Crypts (E1M7) Yellow"},
+ 371105: {'name': 'The Crypts (E1M7) - Silver Shield',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 98,
+ 'doom_type': 85,
+ 'region': "The Crypts (E1M7) Green"},
+ 371106: {'name': 'The Crypts (E1M7) - Bag of Holding',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 130,
+ 'doom_type': 8,
+ 'region': "The Crypts (E1M7) Blue"},
+ 371107: {'name': 'The Crypts (E1M7) - Torch',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 213,
+ 'doom_type': 33,
+ 'region': "The Crypts (E1M7) Green"},
+ 371108: {'name': 'The Crypts (E1M7) - Torch 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 214,
+ 'doom_type': 33,
+ 'region': "The Crypts (E1M7) Blue"},
+ 371109: {'name': 'The Crypts (E1M7) - Tome of Power 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 215,
+ 'doom_type': 86,
+ 'region': "The Crypts (E1M7) Yellow"},
+ 371110: {'name': 'The Crypts (E1M7) - Shadowsphere',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 224,
+ 'doom_type': 75,
+ 'region': "The Crypts (E1M7) Yellow"},
+ 371111: {'name': 'The Crypts (E1M7) - Map Scroll',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 231,
+ 'doom_type': 35,
+ 'region': "The Crypts (E1M7) Blue"},
+ 371112: {'name': 'The Crypts (E1M7) - Silver Shield 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 232,
+ 'doom_type': 85,
+ 'region': "The Crypts (E1M7) Green"},
+ 371113: {'name': 'The Crypts (E1M7) - Exit',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Crypts (E1M7) Blue"},
+ 371114: {'name': "Hell's Maw (E1M8) - Ethereal Crossbow",
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 10,
+ 'doom_type': 2001,
+ 'region': "Hell's Maw (E1M8) Main"},
+ 371115: {'name': "Hell's Maw (E1M8) - Dragon Claw",
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 11,
+ 'doom_type': 53,
+ 'region': "Hell's Maw (E1M8) Main"},
+ 371116: {'name': "Hell's Maw (E1M8) - Tome of Power",
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 63,
+ 'doom_type': 86,
+ 'region': "Hell's Maw (E1M8) Main"},
+ 371117: {'name': "Hell's Maw (E1M8) - Gauntlets of the Necromancer",
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 64,
+ 'doom_type': 2005,
+ 'region': "Hell's Maw (E1M8) Main"},
+ 371118: {'name': "Hell's Maw (E1M8) - Tome of Power 2",
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 65,
+ 'doom_type': 86,
+ 'region': "Hell's Maw (E1M8) Main"},
+ 371119: {'name': "Hell's Maw (E1M8) - Silver Shield",
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 101,
+ 'doom_type': 85,
+ 'region': "Hell's Maw (E1M8) Main"},
+ 371120: {'name': "Hell's Maw (E1M8) - Shadowsphere",
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 102,
+ 'doom_type': 75,
+ 'region': "Hell's Maw (E1M8) Main"},
+ 371121: {'name': "Hell's Maw (E1M8) - Ring of Invincibility",
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 103,
+ 'doom_type': 84,
+ 'region': "Hell's Maw (E1M8) Main"},
+ 371122: {'name': "Hell's Maw (E1M8) - Bag of Holding",
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 104,
+ 'doom_type': 8,
+ 'region': "Hell's Maw (E1M8) Main"},
+ 371123: {'name': "Hell's Maw (E1M8) - Ring of Invincibility 2",
+ 'episode': 1,
+ 'check_sanity': True,
+ 'map': 8,
+ 'index': 237,
+ 'doom_type': 84,
+ 'region': "Hell's Maw (E1M8) Main"},
+ 371124: {'name': "Hell's Maw (E1M8) - Bag of Holding 2",
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 238,
+ 'doom_type': 8,
+ 'region': "Hell's Maw (E1M8) Main"},
+ 371125: {'name': "Hell's Maw (E1M8) - Ring of Invincibility 3",
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 247,
+ 'doom_type': 84,
+ 'region': "Hell's Maw (E1M8) Main"},
+ 371126: {'name': "Hell's Maw (E1M8) - Morph Ovum",
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 290,
+ 'doom_type': 30,
+ 'region': "Hell's Maw (E1M8) Main"},
+ 371127: {'name': "Hell's Maw (E1M8) - Exit",
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Hell's Maw (E1M8) Main"},
+ 371128: {'name': 'The Graveyard (E1M9) - Yellow key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 2,
+ 'doom_type': 80,
+ 'region': "The Graveyard (E1M9) Main"},
+ 371129: {'name': 'The Graveyard (E1M9) - Green key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 21,
+ 'doom_type': 73,
+ 'region': "The Graveyard (E1M9) Yellow"},
+ 371130: {'name': 'The Graveyard (E1M9) - Blue key',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 22,
+ 'doom_type': 79,
+ 'region': "The Graveyard (E1M9) Green"},
+ 371131: {'name': 'The Graveyard (E1M9) - Bag of Holding',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 23,
+ 'doom_type': 8,
+ 'region': "The Graveyard (E1M9) Main"},
+ 371132: {'name': 'The Graveyard (E1M9) - Dragon Claw',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 109,
+ 'doom_type': 53,
+ 'region': "The Graveyard (E1M9) Yellow"},
+ 371133: {'name': 'The Graveyard (E1M9) - Ethereal Crossbow',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 110,
+ 'doom_type': 2001,
+ 'region': "The Graveyard (E1M9) Green"},
+ 371134: {'name': 'The Graveyard (E1M9) - Shadowsphere',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 128,
+ 'doom_type': 75,
+ 'region': "The Graveyard (E1M9) Green"},
+ 371135: {'name': 'The Graveyard (E1M9) - Silver Shield',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 129,
+ 'doom_type': 85,
+ 'region': "The Graveyard (E1M9) Main"},
+ 371136: {'name': 'The Graveyard (E1M9) - Ring of Invincibility',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 217,
+ 'doom_type': 84,
+ 'region': "The Graveyard (E1M9) Green"},
+ 371137: {'name': 'The Graveyard (E1M9) - Torch',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 253,
+ 'doom_type': 33,
+ 'region': "The Graveyard (E1M9) Green"},
+ 371138: {'name': 'The Graveyard (E1M9) - Tome of Power',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 254,
+ 'doom_type': 86,
+ 'region': "The Graveyard (E1M9) Main"},
+ 371139: {'name': 'The Graveyard (E1M9) - Morph Ovum',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 279,
+ 'doom_type': 30,
+ 'region': "The Graveyard (E1M9) Main"},
+ 371140: {'name': 'The Graveyard (E1M9) - Map Scroll',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 280,
+ 'doom_type': 35,
+ 'region': "The Graveyard (E1M9) Blue"},
+ 371141: {'name': 'The Graveyard (E1M9) - Dragon Claw 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 292,
+ 'doom_type': 53,
+ 'region': "The Graveyard (E1M9) Main"},
+ 371142: {'name': 'The Graveyard (E1M9) - Tome of Power 2',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 339,
+ 'doom_type': 86,
+ 'region': "The Graveyard (E1M9) Green"},
+ 371143: {'name': 'The Graveyard (E1M9) - Exit',
+ 'episode': 1,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Graveyard (E1M9) Blue"},
+ 371144: {'name': 'The Crater (E2M1) - Yellow key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 8,
+ 'doom_type': 80,
+ 'region': "The Crater (E2M1) Main"},
+ 371145: {'name': 'The Crater (E2M1) - Green key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 10,
+ 'doom_type': 73,
+ 'region': "The Crater (E2M1) Yellow"},
+ 371146: {'name': 'The Crater (E2M1) - Ethereal Crossbow',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 39,
+ 'doom_type': 2001,
+ 'region': "The Crater (E2M1) Main"},
+ 371147: {'name': 'The Crater (E2M1) - Tome of Power',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 49,
+ 'doom_type': 86,
+ 'region': "The Crater (E2M1) Main"},
+ 371148: {'name': 'The Crater (E2M1) - Dragon Claw',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 90,
+ 'doom_type': 53,
+ 'region': "The Crater (E2M1) Yellow"},
+ 371149: {'name': 'The Crater (E2M1) - Bag of Holding',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 98,
+ 'doom_type': 8,
+ 'region': "The Crater (E2M1) Yellow"},
+ 371150: {'name': 'The Crater (E2M1) - Hellstaff',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 1,
+ 'index': 103,
+ 'doom_type': 2004,
+ 'region': "The Crater (E2M1) Yellow"},
+ 371151: {'name': 'The Crater (E2M1) - Shadowsphere',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 141,
+ 'doom_type': 75,
+ 'region': "The Crater (E2M1) Yellow"},
+ 371152: {'name': 'The Crater (E2M1) - Silver Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 145,
+ 'doom_type': 85,
+ 'region': "The Crater (E2M1) Main"},
+ 371153: {'name': 'The Crater (E2M1) - Torch',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 146,
+ 'doom_type': 33,
+ 'region': "The Crater (E2M1) Main"},
+ 371154: {'name': 'The Crater (E2M1) - Mystic Urn',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 236,
+ 'doom_type': 32,
+ 'region': "The Crater (E2M1) Yellow"},
+ 371155: {'name': 'The Crater (E2M1) - Exit',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Crater (E2M1) Green"},
+ 371156: {'name': 'The Lava Pits (E2M2) - Green key',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 8,
+ 'doom_type': 73,
+ 'region': "The Lava Pits (E2M2) Yellow"},
+ 371157: {'name': 'The Lava Pits (E2M2) - Yellow key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 9,
+ 'doom_type': 80,
+ 'region': "The Lava Pits (E2M2) Main"},
+ 371158: {'name': 'The Lava Pits (E2M2) - Ethereal Crossbow',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 25,
+ 'doom_type': 2001,
+ 'region': "The Lava Pits (E2M2) Main"},
+ 371159: {'name': 'The Lava Pits (E2M2) - Shadowsphere',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 67,
+ 'doom_type': 75,
+ 'region': "The Lava Pits (E2M2) Main"},
+ 371160: {'name': 'The Lava Pits (E2M2) - Ring of Invincibility',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 98,
+ 'doom_type': 84,
+ 'region': "The Lava Pits (E2M2) Yellow"},
+ 371161: {'name': 'The Lava Pits (E2M2) - Dragon Claw',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 109,
+ 'doom_type': 53,
+ 'region': "The Lava Pits (E2M2) Yellow"},
+ 371162: {'name': 'The Lava Pits (E2M2) - Hellstaff',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 117,
+ 'doom_type': 2004,
+ 'region': "The Lava Pits (E2M2) Yellow"},
+ 371163: {'name': 'The Lava Pits (E2M2) - Bag of Holding',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 122,
+ 'doom_type': 8,
+ 'region': "The Lava Pits (E2M2) Green"},
+ 371164: {'name': 'The Lava Pits (E2M2) - Tome of Power',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 123,
+ 'doom_type': 86,
+ 'region': "The Lava Pits (E2M2) Yellow"},
+ 371165: {'name': 'The Lava Pits (E2M2) - Silver Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 124,
+ 'doom_type': 85,
+ 'region': "The Lava Pits (E2M2) Yellow"},
+ 371166: {'name': 'The Lava Pits (E2M2) - Gauntlets of the Necromancer',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 127,
+ 'doom_type': 2005,
+ 'region': "The Lava Pits (E2M2) Yellow"},
+ 371167: {'name': 'The Lava Pits (E2M2) - Enchanted Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 133,
+ 'doom_type': 31,
+ 'region': "The Lava Pits (E2M2) Green"},
+ 371168: {'name': 'The Lava Pits (E2M2) - Mystic Urn',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 230,
+ 'doom_type': 32,
+ 'region': "The Lava Pits (E2M2) Green"},
+ 371169: {'name': 'The Lava Pits (E2M2) - Map Scroll',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 232,
+ 'doom_type': 35,
+ 'region': "The Lava Pits (E2M2) Yellow"},
+ 371170: {'name': 'The Lava Pits (E2M2) - Tome of Power 2',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 233,
+ 'doom_type': 86,
+ 'region': "The Lava Pits (E2M2) Main"},
+ 371171: {'name': 'The Lava Pits (E2M2) - Chaos Device',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 234,
+ 'doom_type': 36,
+ 'region': "The Lava Pits (E2M2) Yellow"},
+ 371172: {'name': 'The Lava Pits (E2M2) - Tome of Power 3',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 323,
+ 'doom_type': 86,
+ 'region': "The Lava Pits (E2M2) Main"},
+ 371173: {'name': 'The Lava Pits (E2M2) - Silver Shield 2',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 324,
+ 'doom_type': 85,
+ 'region': "The Lava Pits (E2M2) Main"},
+ 371174: {'name': 'The Lava Pits (E2M2) - Bag of Holding 2',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 329,
+ 'doom_type': 8,
+ 'region': "The Lava Pits (E2M2) Main"},
+ 371175: {'name': 'The Lava Pits (E2M2) - Morph Ovum',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 341,
+ 'doom_type': 30,
+ 'region': "The Lava Pits (E2M2) Yellow"},
+ 371176: {'name': 'The Lava Pits (E2M2) - Exit',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Lava Pits (E2M2) Green"},
+ 371177: {'name': 'The River of Fire (E2M3) - Yellow key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 9,
+ 'doom_type': 80,
+ 'region': "The River of Fire (E2M3) Main"},
+ 371178: {'name': 'The River of Fire (E2M3) - Blue key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 10,
+ 'doom_type': 79,
+ 'region': "The River of Fire (E2M3) Green"},
+ 371179: {'name': 'The River of Fire (E2M3) - Green key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 17,
+ 'doom_type': 73,
+ 'region': "The River of Fire (E2M3) Yellow"},
+ 371180: {'name': 'The River of Fire (E2M3) - Gauntlets of the Necromancer',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 26,
+ 'doom_type': 2005,
+ 'region': "The River of Fire (E2M3) Main"},
+ 371181: {'name': 'The River of Fire (E2M3) - Ethereal Crossbow',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 57,
+ 'doom_type': 2001,
+ 'region': "The River of Fire (E2M3) Main"},
+ 371182: {'name': 'The River of Fire (E2M3) - Dragon Claw',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 92,
+ 'doom_type': 53,
+ 'region': "The River of Fire (E2M3) Main"},
+ 371183: {'name': 'The River of Fire (E2M3) - Phoenix Rod',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 122,
+ 'doom_type': 2003,
+ 'region': "The River of Fire (E2M3) Green"},
+ 371184: {'name': 'The River of Fire (E2M3) - Hellstaff',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 128,
+ 'doom_type': 2004,
+ 'region': "The River of Fire (E2M3) Blue"},
+ 371185: {'name': 'The River of Fire (E2M3) - Bag of Holding',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 136,
+ 'doom_type': 8,
+ 'region': "The River of Fire (E2M3) Blue"},
+ 371186: {'name': 'The River of Fire (E2M3) - Shadowsphere',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 145,
+ 'doom_type': 75,
+ 'region': "The River of Fire (E2M3) Green"},
+ 371187: {'name': 'The River of Fire (E2M3) - Tome of Power',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 146,
+ 'doom_type': 86,
+ 'region': "The River of Fire (E2M3) Main"},
+ 371188: {'name': 'The River of Fire (E2M3) - Ring of Invincibility',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 147,
+ 'doom_type': 84,
+ 'region': "The River of Fire (E2M3) Main"},
+ 371189: {'name': 'The River of Fire (E2M3) - Silver Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 148,
+ 'doom_type': 85,
+ 'region': "The River of Fire (E2M3) Main"},
+ 371190: {'name': 'The River of Fire (E2M3) - Enchanted Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 297,
+ 'doom_type': 31,
+ 'region': "The River of Fire (E2M3) Blue"},
+ 371191: {'name': 'The River of Fire (E2M3) - Chaos Device',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 298,
+ 'doom_type': 36,
+ 'region': "The River of Fire (E2M3) Blue"},
+ 371192: {'name': 'The River of Fire (E2M3) - Mystic Urn',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 299,
+ 'doom_type': 32,
+ 'region': "The River of Fire (E2M3) Green"},
+ 371193: {'name': 'The River of Fire (E2M3) - Morph Ovum',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 300,
+ 'doom_type': 30,
+ 'region': "The River of Fire (E2M3) Yellow"},
+ 371194: {'name': 'The River of Fire (E2M3) - Tome of Power 2',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 313,
+ 'doom_type': 86,
+ 'region': "The River of Fire (E2M3) Green"},
+ 371195: {'name': 'The River of Fire (E2M3) - Firemace',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 413,
+ 'doom_type': 2002,
+ 'region': "The River of Fire (E2M3) Green"},
+ 371196: {'name': 'The River of Fire (E2M3) - Firemace 2',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 441,
+ 'doom_type': 2002,
+ 'region': "The River of Fire (E2M3) Yellow"},
+ 371197: {'name': 'The River of Fire (E2M3) - Firemace 3',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 448,
+ 'doom_type': 2002,
+ 'region': "The River of Fire (E2M3) Blue"},
+ 371198: {'name': 'The River of Fire (E2M3) - Exit',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The River of Fire (E2M3) Blue"},
+ 371199: {'name': 'The Ice Grotto (E2M4) - Yellow key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 18,
+ 'doom_type': 80,
+ 'region': "The Ice Grotto (E2M4) Main"},
+ 371200: {'name': 'The Ice Grotto (E2M4) - Blue key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 19,
+ 'doom_type': 79,
+ 'region': "The Ice Grotto (E2M4) Green"},
+ 371201: {'name': 'The Ice Grotto (E2M4) - Green key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 28,
+ 'doom_type': 73,
+ 'region': "The Ice Grotto (E2M4) Yellow"},
+ 371202: {'name': 'The Ice Grotto (E2M4) - Phoenix Rod',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 29,
+ 'doom_type': 2003,
+ 'region': "The Ice Grotto (E2M4) Yellow"},
+ 371203: {'name': 'The Ice Grotto (E2M4) - Gauntlets of the Necromancer',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 4,
+ 'index': 30,
+ 'doom_type': 2005,
+ 'region': "The Ice Grotto (E2M4) Main"},
+ 371204: {'name': 'The Ice Grotto (E2M4) - Ethereal Crossbow',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 31,
+ 'doom_type': 2001,
+ 'region': "The Ice Grotto (E2M4) Main"},
+ 371205: {'name': 'The Ice Grotto (E2M4) - Hellstaff',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 32,
+ 'doom_type': 2004,
+ 'region': "The Ice Grotto (E2M4) Blue"},
+ 371206: {'name': 'The Ice Grotto (E2M4) - Dragon Claw',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 33,
+ 'doom_type': 53,
+ 'region': "The Ice Grotto (E2M4) Green"},
+ 371207: {'name': 'The Ice Grotto (E2M4) - Torch',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 34,
+ 'doom_type': 33,
+ 'region': "The Ice Grotto (E2M4) Green"},
+ 371208: {'name': 'The Ice Grotto (E2M4) - Bag of Holding',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 35,
+ 'doom_type': 8,
+ 'region': "The Ice Grotto (E2M4) Main"},
+ 371209: {'name': 'The Ice Grotto (E2M4) - Shadowsphere',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 36,
+ 'doom_type': 75,
+ 'region': "The Ice Grotto (E2M4) Green"},
+ 371210: {'name': 'The Ice Grotto (E2M4) - Chaos Device',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 37,
+ 'doom_type': 36,
+ 'region': "The Ice Grotto (E2M4) Green"},
+ 371211: {'name': 'The Ice Grotto (E2M4) - Silver Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 38,
+ 'doom_type': 85,
+ 'region': "The Ice Grotto (E2M4) Main"},
+ 371212: {'name': 'The Ice Grotto (E2M4) - Tome of Power',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 39,
+ 'doom_type': 86,
+ 'region': "The Ice Grotto (E2M4) Green"},
+ 371213: {'name': 'The Ice Grotto (E2M4) - Tome of Power 2',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 40,
+ 'doom_type': 86,
+ 'region': "The Ice Grotto (E2M4) Main"},
+ 371214: {'name': 'The Ice Grotto (E2M4) - Bag of Holding 2',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 41,
+ 'doom_type': 8,
+ 'region': "The Ice Grotto (E2M4) Green"},
+ 371215: {'name': 'The Ice Grotto (E2M4) - Tome of Power 3',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 128,
+ 'doom_type': 86,
+ 'region': "The Ice Grotto (E2M4) Yellow"},
+ 371216: {'name': 'The Ice Grotto (E2M4) - Map Scroll',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 283,
+ 'doom_type': 35,
+ 'region': "The Ice Grotto (E2M4) Green"},
+ 371217: {'name': 'The Ice Grotto (E2M4) - Mystic Urn',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 289,
+ 'doom_type': 32,
+ 'region': "The Ice Grotto (E2M4) Magenta"},
+ 371218: {'name': 'The Ice Grotto (E2M4) - Enchanted Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 291,
+ 'doom_type': 31,
+ 'region': "The Ice Grotto (E2M4) Green"},
+ 371219: {'name': 'The Ice Grotto (E2M4) - Morph Ovum',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 299,
+ 'doom_type': 30,
+ 'region': "The Ice Grotto (E2M4) Main"},
+ 371220: {'name': 'The Ice Grotto (E2M4) - Shadowsphere 2',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 300,
+ 'doom_type': 75,
+ 'region': "The Ice Grotto (E2M4) Main"},
+ 371221: {'name': 'The Ice Grotto (E2M4) - Exit',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Ice Grotto (E2M4) Blue"},
+ 371222: {'name': 'The Catacombs (E2M5) - Yellow key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 14,
+ 'doom_type': 80,
+ 'region': "The Catacombs (E2M5) Main"},
+ 371223: {'name': 'The Catacombs (E2M5) - Blue key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 25,
+ 'doom_type': 79,
+ 'region': "The Catacombs (E2M5) Green"},
+ 371224: {'name': 'The Catacombs (E2M5) - Hellstaff',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 27,
+ 'doom_type': 2004,
+ 'region': "The Catacombs (E2M5) Yellow"},
+ 371225: {'name': 'The Catacombs (E2M5) - Phoenix Rod',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 5,
+ 'index': 44,
+ 'doom_type': 2003,
+ 'region': "The Catacombs (E2M5) Green"},
+ 371226: {'name': 'The Catacombs (E2M5) - Ethereal Crossbow',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 107,
+ 'doom_type': 2001,
+ 'region': "The Catacombs (E2M5) Yellow"},
+ 371227: {'name': 'The Catacombs (E2M5) - Gauntlets of the Necromancer',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 108,
+ 'doom_type': 2005,
+ 'region': "The Catacombs (E2M5) Main"},
+ 371228: {'name': 'The Catacombs (E2M5) - Dragon Claw',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 109,
+ 'doom_type': 53,
+ 'region': "The Catacombs (E2M5) Main"},
+ 371229: {'name': 'The Catacombs (E2M5) - Bag of Holding',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 110,
+ 'doom_type': 8,
+ 'region': "The Catacombs (E2M5) Main"},
+ 371230: {'name': 'The Catacombs (E2M5) - Silver Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 112,
+ 'doom_type': 85,
+ 'region': "The Catacombs (E2M5) Yellow"},
+ 371231: {'name': 'The Catacombs (E2M5) - Shadowsphere',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 113,
+ 'doom_type': 75,
+ 'region': "The Catacombs (E2M5) Yellow"},
+ 371232: {'name': 'The Catacombs (E2M5) - Ring of Invincibility',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 114,
+ 'doom_type': 84,
+ 'region': "The Catacombs (E2M5) Yellow"},
+ 371233: {'name': 'The Catacombs (E2M5) - Tome of Power',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 115,
+ 'doom_type': 86,
+ 'region': "The Catacombs (E2M5) Yellow"},
+ 371234: {'name': 'The Catacombs (E2M5) - Green key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 116,
+ 'doom_type': 73,
+ 'region': "The Catacombs (E2M5) Yellow"},
+ 371235: {'name': 'The Catacombs (E2M5) - Chaos Device',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 263,
+ 'doom_type': 36,
+ 'region': "The Catacombs (E2M5) Yellow"},
+ 371236: {'name': 'The Catacombs (E2M5) - Tome of Power 2',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 322,
+ 'doom_type': 86,
+ 'region': "The Catacombs (E2M5) Yellow"},
+ 371237: {'name': 'The Catacombs (E2M5) - Map Scroll',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 323,
+ 'doom_type': 35,
+ 'region': "The Catacombs (E2M5) Green"},
+ 371238: {'name': 'The Catacombs (E2M5) - Mystic Urn',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 324,
+ 'doom_type': 32,
+ 'region': "The Catacombs (E2M5) Yellow"},
+ 371239: {'name': 'The Catacombs (E2M5) - Morph Ovum',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 325,
+ 'doom_type': 30,
+ 'region': "The Catacombs (E2M5) Green"},
+ 371240: {'name': 'The Catacombs (E2M5) - Enchanted Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 326,
+ 'doom_type': 31,
+ 'region': "The Catacombs (E2M5) Green"},
+ 371241: {'name': 'The Catacombs (E2M5) - Torch',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 327,
+ 'doom_type': 33,
+ 'region': "The Catacombs (E2M5) Main"},
+ 371242: {'name': 'The Catacombs (E2M5) - Tome of Power 3',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 328,
+ 'doom_type': 86,
+ 'region': "The Catacombs (E2M5) Yellow"},
+ 371243: {'name': 'The Catacombs (E2M5) - Exit',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Catacombs (E2M5) Blue"},
+ 371244: {'name': 'The Labyrinth (E2M6) - Yellow key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 7,
+ 'doom_type': 80,
+ 'region': "The Labyrinth (E2M6) Main"},
+ 371245: {'name': 'The Labyrinth (E2M6) - Blue key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 14,
+ 'doom_type': 79,
+ 'region': "The Labyrinth (E2M6) Green"},
+ 371246: {'name': 'The Labyrinth (E2M6) - Green key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 15,
+ 'doom_type': 73,
+ 'region': "The Labyrinth (E2M6) Yellow"},
+ 371247: {'name': 'The Labyrinth (E2M6) - Hellstaff',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 22,
+ 'doom_type': 2004,
+ 'region': "The Labyrinth (E2M6) Green"},
+ 371248: {'name': 'The Labyrinth (E2M6) - Gauntlets of the Necromancer',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 23,
+ 'doom_type': 2005,
+ 'region': "The Labyrinth (E2M6) Main"},
+ 371249: {'name': 'The Labyrinth (E2M6) - Ethereal Crossbow',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 24,
+ 'doom_type': 2001,
+ 'region': "The Labyrinth (E2M6) Main"},
+ 371250: {'name': 'The Labyrinth (E2M6) - Dragon Claw',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 25,
+ 'doom_type': 53,
+ 'region': "The Labyrinth (E2M6) Yellow"},
+ 371251: {'name': 'The Labyrinth (E2M6) - Phoenix Rod',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 26,
+ 'doom_type': 2003,
+ 'region': "The Labyrinth (E2M6) Green"},
+ 371252: {'name': 'The Labyrinth (E2M6) - Bag of Holding',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 27,
+ 'doom_type': 8,
+ 'region': "The Labyrinth (E2M6) Yellow"},
+ 371253: {'name': 'The Labyrinth (E2M6) - Shadowsphere',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 31,
+ 'doom_type': 75,
+ 'region': "The Labyrinth (E2M6) Yellow"},
+ 371254: {'name': 'The Labyrinth (E2M6) - Ring of Invincibility',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 32,
+ 'doom_type': 84,
+ 'region': "The Labyrinth (E2M6) Blue"},
+ 371255: {'name': 'The Labyrinth (E2M6) - Tome of Power',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 33,
+ 'doom_type': 86,
+ 'region': "The Labyrinth (E2M6) Green"},
+ 371256: {'name': 'The Labyrinth (E2M6) - Silver Shield',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 6,
+ 'index': 34,
+ 'doom_type': 85,
+ 'region': "The Labyrinth (E2M6) Main"},
+ 371257: {'name': 'The Labyrinth (E2M6) - Morph Ovum',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 35,
+ 'doom_type': 30,
+ 'region': "The Labyrinth (E2M6) Main"},
+ 371258: {'name': 'The Labyrinth (E2M6) - Map Scroll',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 282,
+ 'doom_type': 35,
+ 'region': "The Labyrinth (E2M6) Green"},
+ 371259: {'name': 'The Labyrinth (E2M6) - Enchanted Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 283,
+ 'doom_type': 31,
+ 'region': "The Labyrinth (E2M6) Green"},
+ 371260: {'name': 'The Labyrinth (E2M6) - Tome of Power 2',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 284,
+ 'doom_type': 86,
+ 'region': "The Labyrinth (E2M6) Green"},
+ 371261: {'name': 'The Labyrinth (E2M6) - Chaos Device',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 285,
+ 'doom_type': 36,
+ 'region': "The Labyrinth (E2M6) Yellow"},
+ 371262: {'name': 'The Labyrinth (E2M6) - Mystic Urn',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 6,
+ 'index': 336,
+ 'doom_type': 32,
+ 'region': "The Labyrinth (E2M6) Blue"},
+ 371263: {'name': 'The Labyrinth (E2M6) - Phoenix Rod 2',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 422,
+ 'doom_type': 2003,
+ 'region': "The Labyrinth (E2M6) Blue"},
+ 371264: {'name': 'The Labyrinth (E2M6) - Firemace',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 432,
+ 'doom_type': 2002,
+ 'region': "The Labyrinth (E2M6) Main"},
+ 371265: {'name': 'The Labyrinth (E2M6) - Firemace 2',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 6,
+ 'index': 456,
+ 'doom_type': 2002,
+ 'region': "The Labyrinth (E2M6) Yellow"},
+ 371266: {'name': 'The Labyrinth (E2M6) - Firemace 3',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 457,
+ 'doom_type': 2002,
+ 'region': "The Labyrinth (E2M6) Yellow"},
+ 371267: {'name': 'The Labyrinth (E2M6) - Firemace 4',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 458,
+ 'doom_type': 2002,
+ 'region': "The Labyrinth (E2M6) Blue"},
+ 371268: {'name': 'The Labyrinth (E2M6) - Exit',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Labyrinth (E2M6) Blue"},
+ 371269: {'name': 'The Great Hall (E2M7) - Green key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 8,
+ 'doom_type': 73,
+ 'region': "The Great Hall (E2M7) Yellow"},
+ 371270: {'name': 'The Great Hall (E2M7) - Yellow key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 9,
+ 'doom_type': 80,
+ 'region': "The Great Hall (E2M7) Main"},
+ 371271: {'name': 'The Great Hall (E2M7) - Blue key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 11,
+ 'doom_type': 79,
+ 'region': "The Great Hall (E2M7) Green"},
+ 371272: {'name': 'The Great Hall (E2M7) - Morph Ovum',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 64,
+ 'doom_type': 30,
+ 'region': "The Great Hall (E2M7) Main"},
+ 371273: {'name': 'The Great Hall (E2M7) - Shadowsphere',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 76,
+ 'doom_type': 75,
+ 'region': "The Great Hall (E2M7) Main"},
+ 371274: {'name': 'The Great Hall (E2M7) - Ring of Invincibility',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 77,
+ 'doom_type': 84,
+ 'region': "The Great Hall (E2M7) Yellow"},
+ 371275: {'name': 'The Great Hall (E2M7) - Mystic Urn',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 78,
+ 'doom_type': 32,
+ 'region': "The Great Hall (E2M7) Blue"},
+ 371276: {'name': 'The Great Hall (E2M7) - Tome of Power',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 80,
+ 'doom_type': 86,
+ 'region': "The Great Hall (E2M7) Yellow"},
+ 371277: {'name': 'The Great Hall (E2M7) - Chaos Device',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 81,
+ 'doom_type': 36,
+ 'region': "The Great Hall (E2M7) Yellow"},
+ 371278: {'name': 'The Great Hall (E2M7) - Torch',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 82,
+ 'doom_type': 33,
+ 'region': "The Great Hall (E2M7) Main"},
+ 371279: {'name': 'The Great Hall (E2M7) - Bag of Holding',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 83,
+ 'doom_type': 8,
+ 'region': "The Great Hall (E2M7) Main"},
+ 371280: {'name': 'The Great Hall (E2M7) - Silver Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 84,
+ 'doom_type': 85,
+ 'region': "The Great Hall (E2M7) Main"},
+ 371281: {'name': 'The Great Hall (E2M7) - Enchanted Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 85,
+ 'doom_type': 31,
+ 'region': "The Great Hall (E2M7) Main"},
+ 371282: {'name': 'The Great Hall (E2M7) - Map Scroll',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 86,
+ 'doom_type': 35,
+ 'region': "The Great Hall (E2M7) Yellow"},
+ 371283: {'name': 'The Great Hall (E2M7) - Ethereal Crossbow',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 91,
+ 'doom_type': 2001,
+ 'region': "The Great Hall (E2M7) Main"},
+ 371284: {'name': 'The Great Hall (E2M7) - Gauntlets of the Necromancer',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 92,
+ 'doom_type': 2005,
+ 'region': "The Great Hall (E2M7) Main"},
+ 371285: {'name': 'The Great Hall (E2M7) - Dragon Claw',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 93,
+ 'doom_type': 53,
+ 'region': "The Great Hall (E2M7) Yellow"},
+ 371286: {'name': 'The Great Hall (E2M7) - Hellstaff',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 94,
+ 'doom_type': 2004,
+ 'region': "The Great Hall (E2M7) Yellow"},
+ 371287: {'name': 'The Great Hall (E2M7) - Phoenix Rod',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 95,
+ 'doom_type': 2003,
+ 'region': "The Great Hall (E2M7) Main"},
+ 371288: {'name': 'The Great Hall (E2M7) - Tome of Power 2',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 477,
+ 'doom_type': 86,
+ 'region': "The Great Hall (E2M7) Main"},
+ 371289: {'name': 'The Great Hall (E2M7) - Exit',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Great Hall (E2M7) Blue"},
+ 371290: {'name': 'The Portals of Chaos (E2M8) - Ethereal Crossbow',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 9,
+ 'doom_type': 2001,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371291: {'name': 'The Portals of Chaos (E2M8) - Dragon Claw',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 10,
+ 'doom_type': 53,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371292: {'name': 'The Portals of Chaos (E2M8) - Gauntlets of the Necromancer',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 11,
+ 'doom_type': 2005,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371293: {'name': 'The Portals of Chaos (E2M8) - Hellstaff',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 12,
+ 'doom_type': 2004,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371294: {'name': 'The Portals of Chaos (E2M8) - Phoenix Rod',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 13,
+ 'doom_type': 2003,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371295: {'name': 'The Portals of Chaos (E2M8) - Tome of Power',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 14,
+ 'doom_type': 86,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371296: {'name': 'The Portals of Chaos (E2M8) - Bag of Holding',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 8,
+ 'index': 18,
+ 'doom_type': 8,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371297: {'name': 'The Portals of Chaos (E2M8) - Mystic Urn',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 8,
+ 'index': 40,
+ 'doom_type': 32,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371298: {'name': 'The Portals of Chaos (E2M8) - Shadowsphere',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 8,
+ 'index': 41,
+ 'doom_type': 75,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371299: {'name': 'The Portals of Chaos (E2M8) - Silver Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 42,
+ 'doom_type': 85,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371300: {'name': 'The Portals of Chaos (E2M8) - Enchanted Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 43,
+ 'doom_type': 31,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371301: {'name': 'The Portals of Chaos (E2M8) - Chaos Device',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 44,
+ 'doom_type': 36,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371302: {'name': 'The Portals of Chaos (E2M8) - Ring of Invincibility',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 272,
+ 'doom_type': 84,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371303: {'name': 'The Portals of Chaos (E2M8) - Morph Ovum',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 274,
+ 'doom_type': 30,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371304: {'name': 'The Portals of Chaos (E2M8) - Mystic Urn 2',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 8,
+ 'index': 275,
+ 'doom_type': 32,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371305: {'name': 'The Portals of Chaos (E2M8) - Exit',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Portals of Chaos (E2M8) Main"},
+ 371306: {'name': 'The Glacier (E2M9) - Yellow key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 6,
+ 'doom_type': 80,
+ 'region': "The Glacier (E2M9) Main"},
+ 371307: {'name': 'The Glacier (E2M9) - Blue key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 16,
+ 'doom_type': 79,
+ 'region': "The Glacier (E2M9) Green"},
+ 371308: {'name': 'The Glacier (E2M9) - Green key',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 17,
+ 'doom_type': 73,
+ 'region': "The Glacier (E2M9) Yellow"},
+ 371309: {'name': 'The Glacier (E2M9) - Phoenix Rod',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 34,
+ 'doom_type': 2003,
+ 'region': "The Glacier (E2M9) Green"},
+ 371310: {'name': 'The Glacier (E2M9) - Gauntlets of the Necromancer',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 9,
+ 'index': 39,
+ 'doom_type': 2005,
+ 'region': "The Glacier (E2M9) Main"},
+ 371311: {'name': 'The Glacier (E2M9) - Ethereal Crossbow',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 40,
+ 'doom_type': 2001,
+ 'region': "The Glacier (E2M9) Main"},
+ 371312: {'name': 'The Glacier (E2M9) - Dragon Claw',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 41,
+ 'doom_type': 53,
+ 'region': "The Glacier (E2M9) Yellow"},
+ 371313: {'name': 'The Glacier (E2M9) - Hellstaff',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 42,
+ 'doom_type': 2004,
+ 'region': "The Glacier (E2M9) Green"},
+ 371314: {'name': 'The Glacier (E2M9) - Bag of Holding',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 43,
+ 'doom_type': 8,
+ 'region': "The Glacier (E2M9) Main"},
+ 371315: {'name': 'The Glacier (E2M9) - Tome of Power',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 45,
+ 'doom_type': 86,
+ 'region': "The Glacier (E2M9) Main"},
+ 371316: {'name': 'The Glacier (E2M9) - Shadowsphere',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 46,
+ 'doom_type': 75,
+ 'region': "The Glacier (E2M9) Main"},
+ 371317: {'name': 'The Glacier (E2M9) - Ring of Invincibility',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 47,
+ 'doom_type': 84,
+ 'region': "The Glacier (E2M9) Green"},
+ 371318: {'name': 'The Glacier (E2M9) - Silver Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 48,
+ 'doom_type': 85,
+ 'region': "The Glacier (E2M9) Main"},
+ 371319: {'name': 'The Glacier (E2M9) - Enchanted Shield',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 49,
+ 'doom_type': 31,
+ 'region': "The Glacier (E2M9) Green"},
+ 371320: {'name': 'The Glacier (E2M9) - Mystic Urn',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 50,
+ 'doom_type': 32,
+ 'region': "The Glacier (E2M9) Blue"},
+ 371321: {'name': 'The Glacier (E2M9) - Map Scroll',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 51,
+ 'doom_type': 35,
+ 'region': "The Glacier (E2M9) Blue"},
+ 371322: {'name': 'The Glacier (E2M9) - Mystic Urn 2',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 52,
+ 'doom_type': 32,
+ 'region': "The Glacier (E2M9) Green"},
+ 371323: {'name': 'The Glacier (E2M9) - Torch',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 53,
+ 'doom_type': 33,
+ 'region': "The Glacier (E2M9) Green"},
+ 371324: {'name': 'The Glacier (E2M9) - Chaos Device',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 424,
+ 'doom_type': 36,
+ 'region': "The Glacier (E2M9) Yellow"},
+ 371325: {'name': 'The Glacier (E2M9) - Dragon Claw 2',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 456,
+ 'doom_type': 53,
+ 'region': "The Glacier (E2M9) Main"},
+ 371326: {'name': 'The Glacier (E2M9) - Tome of Power 2',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 9,
+ 'index': 457,
+ 'doom_type': 86,
+ 'region': "The Glacier (E2M9) Main"},
+ 371327: {'name': 'The Glacier (E2M9) - Torch 2',
+ 'episode': 2,
+ 'check_sanity': True,
+ 'map': 9,
+ 'index': 458,
+ 'doom_type': 33,
+ 'region': "The Glacier (E2M9) Main"},
+ 371328: {'name': 'The Glacier (E2M9) - Morph Ovum',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 474,
+ 'doom_type': 30,
+ 'region': "The Glacier (E2M9) Main"},
+ 371329: {'name': 'The Glacier (E2M9) - Firemace',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 479,
+ 'doom_type': 2002,
+ 'region': "The Glacier (E2M9) Main"},
+ 371330: {'name': 'The Glacier (E2M9) - Firemace 2',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 501,
+ 'doom_type': 2002,
+ 'region': "The Glacier (E2M9) Yellow"},
+ 371331: {'name': 'The Glacier (E2M9) - Firemace 3',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 502,
+ 'doom_type': 2002,
+ 'region': "The Glacier (E2M9) Blue"},
+ 371332: {'name': 'The Glacier (E2M9) - Firemace 4',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 503,
+ 'doom_type': 2002,
+ 'region': "The Glacier (E2M9) Main"},
+ 371333: {'name': 'The Glacier (E2M9) - Exit',
+ 'episode': 2,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Glacier (E2M9) Blue"},
+ 371334: {'name': 'The Storehouse (E3M1) - Yellow key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 9,
+ 'doom_type': 80,
+ 'region': "The Storehouse (E3M1) Main"},
+ 371335: {'name': 'The Storehouse (E3M1) - Green key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 10,
+ 'doom_type': 73,
+ 'region': "The Storehouse (E3M1) Yellow"},
+ 371336: {'name': 'The Storehouse (E3M1) - Bag of Holding',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 29,
+ 'doom_type': 8,
+ 'region': "The Storehouse (E3M1) Main"},
+ 371337: {'name': 'The Storehouse (E3M1) - Shadowsphere',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 38,
+ 'doom_type': 75,
+ 'region': "The Storehouse (E3M1) Main"},
+ 371338: {'name': 'The Storehouse (E3M1) - Ring of Invincibility',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 39,
+ 'doom_type': 84,
+ 'region': "The Storehouse (E3M1) Green"},
+ 371339: {'name': 'The Storehouse (E3M1) - Silver Shield',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 40,
+ 'doom_type': 85,
+ 'region': "The Storehouse (E3M1) Main"},
+ 371340: {'name': 'The Storehouse (E3M1) - Map Scroll',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 41,
+ 'doom_type': 35,
+ 'region': "The Storehouse (E3M1) Green"},
+ 371341: {'name': 'The Storehouse (E3M1) - Chaos Device',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 42,
+ 'doom_type': 36,
+ 'region': "The Storehouse (E3M1) Main"},
+ 371342: {'name': 'The Storehouse (E3M1) - Tome of Power',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 43,
+ 'doom_type': 86,
+ 'region': "The Storehouse (E3M1) Green"},
+ 371343: {'name': 'The Storehouse (E3M1) - Torch',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 1,
+ 'index': 44,
+ 'doom_type': 33,
+ 'region': "The Storehouse (E3M1) Main"},
+ 371344: {'name': 'The Storehouse (E3M1) - Dragon Claw',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 45,
+ 'doom_type': 53,
+ 'region': "The Storehouse (E3M1) Main"},
+ 371345: {'name': 'The Storehouse (E3M1) - Hellstaff',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 46,
+ 'doom_type': 2004,
+ 'region': "The Storehouse (E3M1) Green"},
+ 371346: {'name': 'The Storehouse (E3M1) - Gauntlets of the Necromancer',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 47,
+ 'doom_type': 2005,
+ 'region': "The Storehouse (E3M1) Main"},
+ 371347: {'name': 'The Storehouse (E3M1) - Exit',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Storehouse (E3M1) Green"},
+ 371348: {'name': 'The Cesspool (E3M2) - Yellow key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 4,
+ 'doom_type': 80,
+ 'region': "The Cesspool (E3M2) Main"},
+ 371349: {'name': 'The Cesspool (E3M2) - Green key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 19,
+ 'doom_type': 73,
+ 'region': "The Cesspool (E3M2) Yellow"},
+ 371350: {'name': 'The Cesspool (E3M2) - Blue key',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 20,
+ 'doom_type': 79,
+ 'region': "The Cesspool (E3M2) Green"},
+ 371351: {'name': 'The Cesspool (E3M2) - Ethereal Crossbow',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 144,
+ 'doom_type': 2001,
+ 'region': "The Cesspool (E3M2) Main"},
+ 371352: {'name': 'The Cesspool (E3M2) - Ring of Invincibility',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 145,
+ 'doom_type': 84,
+ 'region': "The Cesspool (E3M2) Green"},
+ 371353: {'name': 'The Cesspool (E3M2) - Gauntlets of the Necromancer',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 146,
+ 'doom_type': 2005,
+ 'region': "The Cesspool (E3M2) Green"},
+ 371354: {'name': 'The Cesspool (E3M2) - Dragon Claw',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 147,
+ 'doom_type': 53,
+ 'region': "The Cesspool (E3M2) Yellow"},
+ 371355: {'name': 'The Cesspool (E3M2) - Phoenix Rod',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 148,
+ 'doom_type': 2003,
+ 'region': "The Cesspool (E3M2) Green"},
+ 371356: {'name': 'The Cesspool (E3M2) - Hellstaff',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 149,
+ 'doom_type': 2004,
+ 'region': "The Cesspool (E3M2) Yellow"},
+ 371357: {'name': 'The Cesspool (E3M2) - Bag of Holding',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 150,
+ 'doom_type': 8,
+ 'region': "The Cesspool (E3M2) Yellow"},
+ 371358: {'name': 'The Cesspool (E3M2) - Silver Shield',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 151,
+ 'doom_type': 85,
+ 'region': "The Cesspool (E3M2) Yellow"},
+ 371359: {'name': 'The Cesspool (E3M2) - Silver Shield 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 152,
+ 'doom_type': 85,
+ 'region': "The Cesspool (E3M2) Main"},
+ 371360: {'name': 'The Cesspool (E3M2) - Morph Ovum',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 153,
+ 'doom_type': 30,
+ 'region': "The Cesspool (E3M2) Main"},
+ 371361: {'name': 'The Cesspool (E3M2) - Morph Ovum 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 154,
+ 'doom_type': 30,
+ 'region': "The Cesspool (E3M2) Green"},
+ 371362: {'name': 'The Cesspool (E3M2) - Mystic Urn',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 164,
+ 'doom_type': 32,
+ 'region': "The Cesspool (E3M2) Main"},
+ 371363: {'name': 'The Cesspool (E3M2) - Shadowsphere',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 165,
+ 'doom_type': 75,
+ 'region': "The Cesspool (E3M2) Yellow"},
+ 371364: {'name': 'The Cesspool (E3M2) - Enchanted Shield',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 166,
+ 'doom_type': 31,
+ 'region': "The Cesspool (E3M2) Green"},
+ 371365: {'name': 'The Cesspool (E3M2) - Map Scroll',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 167,
+ 'doom_type': 35,
+ 'region': "The Cesspool (E3M2) Green"},
+ 371366: {'name': 'The Cesspool (E3M2) - Chaos Device',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 168,
+ 'doom_type': 36,
+ 'region': "The Cesspool (E3M2) Green"},
+ 371367: {'name': 'The Cesspool (E3M2) - Tome of Power',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 169,
+ 'doom_type': 86,
+ 'region': "The Cesspool (E3M2) Main"},
+ 371368: {'name': 'The Cesspool (E3M2) - Tome of Power 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 170,
+ 'doom_type': 86,
+ 'region': "The Cesspool (E3M2) Green"},
+ 371369: {'name': 'The Cesspool (E3M2) - Tome of Power 3',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 171,
+ 'doom_type': 86,
+ 'region': "The Cesspool (E3M2) Yellow"},
+ 371370: {'name': 'The Cesspool (E3M2) - Torch',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 172,
+ 'doom_type': 33,
+ 'region': "The Cesspool (E3M2) Yellow"},
+ 371371: {'name': 'The Cesspool (E3M2) - Bag of Holding 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 233,
+ 'doom_type': 8,
+ 'region': "The Cesspool (E3M2) Green"},
+ 371372: {'name': 'The Cesspool (E3M2) - Firemace',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 555,
+ 'doom_type': 2002,
+ 'region': "The Cesspool (E3M2) Green"},
+ 371373: {'name': 'The Cesspool (E3M2) - Firemace 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 556,
+ 'doom_type': 2002,
+ 'region': "The Cesspool (E3M2) Yellow"},
+ 371374: {'name': 'The Cesspool (E3M2) - Firemace 3',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 557,
+ 'doom_type': 2002,
+ 'region': "The Cesspool (E3M2) Blue"},
+ 371375: {'name': 'The Cesspool (E3M2) - Firemace 4',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 558,
+ 'doom_type': 2002,
+ 'region': "The Cesspool (E3M2) Main"},
+ 371376: {'name': 'The Cesspool (E3M2) - Firemace 5',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 559,
+ 'doom_type': 2002,
+ 'region': "The Cesspool (E3M2) Yellow"},
+ 371377: {'name': 'The Cesspool (E3M2) - Exit',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Cesspool (E3M2) Blue"},
+ 371378: {'name': 'The Confluence (E3M3) - Yellow key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 4,
+ 'doom_type': 80,
+ 'region': "The Confluence (E3M3) Main"},
+ 371379: {'name': 'The Confluence (E3M3) - Green key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 7,
+ 'doom_type': 73,
+ 'region': "The Confluence (E3M3) Yellow"},
+ 371380: {'name': 'The Confluence (E3M3) - Blue key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 8,
+ 'doom_type': 79,
+ 'region': "The Confluence (E3M3) Green"},
+ 371381: {'name': 'The Confluence (E3M3) - Hellstaff',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 43,
+ 'doom_type': 2004,
+ 'region': "The Confluence (E3M3) Blue"},
+ 371382: {'name': 'The Confluence (E3M3) - Tome of Power',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 44,
+ 'doom_type': 86,
+ 'region': "The Confluence (E3M3) Blue"},
+ 371383: {'name': 'The Confluence (E3M3) - Dragon Claw',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 47,
+ 'doom_type': 53,
+ 'region': "The Confluence (E3M3) Green"},
+ 371384: {'name': 'The Confluence (E3M3) - Ethereal Crossbow',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 48,
+ 'doom_type': 2001,
+ 'region': "The Confluence (E3M3) Yellow"},
+ 371385: {'name': 'The Confluence (E3M3) - Hellstaff 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 49,
+ 'doom_type': 2004,
+ 'region': "The Confluence (E3M3) Blue"},
+ 371386: {'name': 'The Confluence (E3M3) - Gauntlets of the Necromancer',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 50,
+ 'doom_type': 2005,
+ 'region': "The Confluence (E3M3) Blue"},
+ 371387: {'name': 'The Confluence (E3M3) - Mystic Urn',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 51,
+ 'doom_type': 32,
+ 'region': "The Confluence (E3M3) Green"},
+ 371388: {'name': 'The Confluence (E3M3) - Tome of Power 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 52,
+ 'doom_type': 86,
+ 'region': "The Confluence (E3M3) Green"},
+ 371389: {'name': 'The Confluence (E3M3) - Tome of Power 3',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 53,
+ 'doom_type': 86,
+ 'region': "The Confluence (E3M3) Blue"},
+ 371390: {'name': 'The Confluence (E3M3) - Tome of Power 4',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 54,
+ 'doom_type': 86,
+ 'region': "The Confluence (E3M3) Blue"},
+ 371391: {'name': 'The Confluence (E3M3) - Tome of Power 5',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 55,
+ 'doom_type': 86,
+ 'region': "The Confluence (E3M3) Green"},
+ 371392: {'name': 'The Confluence (E3M3) - Bag of Holding',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 58,
+ 'doom_type': 8,
+ 'region': "The Confluence (E3M3) Green"},
+ 371393: {'name': 'The Confluence (E3M3) - Morph Ovum',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 60,
+ 'doom_type': 30,
+ 'region': "The Confluence (E3M3) Green"},
+ 371394: {'name': 'The Confluence (E3M3) - Mystic Urn 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 72,
+ 'doom_type': 32,
+ 'region': "The Confluence (E3M3) Blue"},
+ 371395: {'name': 'The Confluence (E3M3) - Shadowsphere',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 73,
+ 'doom_type': 75,
+ 'region': "The Confluence (E3M3) Main"},
+ 371396: {'name': 'The Confluence (E3M3) - Ring of Invincibility',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 74,
+ 'doom_type': 84,
+ 'region': "The Confluence (E3M3) Yellow"},
+ 371397: {'name': 'The Confluence (E3M3) - Map Scroll',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 75,
+ 'doom_type': 35,
+ 'region': "The Confluence (E3M3) Blue"},
+ 371398: {'name': 'The Confluence (E3M3) - Silver Shield',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 76,
+ 'doom_type': 85,
+ 'region': "The Confluence (E3M3) Main"},
+ 371399: {'name': 'The Confluence (E3M3) - Phoenix Rod',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 77,
+ 'doom_type': 2003,
+ 'region': "The Confluence (E3M3) Blue"},
+ 371400: {'name': 'The Confluence (E3M3) - Enchanted Shield',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 78,
+ 'doom_type': 31,
+ 'region': "The Confluence (E3M3) Blue"},
+ 371401: {'name': 'The Confluence (E3M3) - Silver Shield 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 79,
+ 'doom_type': 85,
+ 'region': "The Confluence (E3M3) Green"},
+ 371402: {'name': 'The Confluence (E3M3) - Chaos Device',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 80,
+ 'doom_type': 36,
+ 'region': "The Confluence (E3M3) Green"},
+ 371403: {'name': 'The Confluence (E3M3) - Torch',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 81,
+ 'doom_type': 33,
+ 'region': "The Confluence (E3M3) Green"},
+ 371404: {'name': 'The Confluence (E3M3) - Firemace',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 622,
+ 'doom_type': 2002,
+ 'region': "The Confluence (E3M3) Green"},
+ 371405: {'name': 'The Confluence (E3M3) - Firemace 2',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 623,
+ 'doom_type': 2002,
+ 'region': "The Confluence (E3M3) Green"},
+ 371406: {'name': 'The Confluence (E3M3) - Firemace 3',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 624,
+ 'doom_type': 2002,
+ 'region': "The Confluence (E3M3) Yellow"},
+ 371407: {'name': 'The Confluence (E3M3) - Firemace 4',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 625,
+ 'doom_type': 2002,
+ 'region': "The Confluence (E3M3) Blue"},
+ 371408: {'name': 'The Confluence (E3M3) - Firemace 5',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 626,
+ 'doom_type': 2002,
+ 'region': "The Confluence (E3M3) Blue"},
+ 371409: {'name': 'The Confluence (E3M3) - Firemace 6',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 627,
+ 'doom_type': 2002,
+ 'region': "The Confluence (E3M3) Blue"},
+ 371410: {'name': 'The Confluence (E3M3) - Exit',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Confluence (E3M3) Blue"},
+ 371411: {'name': 'The Azure Fortress (E3M4) - Yellow key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 6,
+ 'doom_type': 80,
+ 'region': "The Azure Fortress (E3M4) Main"},
+ 371412: {'name': 'The Azure Fortress (E3M4) - Green key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 21,
+ 'doom_type': 73,
+ 'region': "The Azure Fortress (E3M4) Yellow"},
+ 371413: {'name': 'The Azure Fortress (E3M4) - Dragon Claw',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 51,
+ 'doom_type': 53,
+ 'region': "The Azure Fortress (E3M4) Main"},
+ 371414: {'name': 'The Azure Fortress (E3M4) - Ring of Invincibility',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 52,
+ 'doom_type': 84,
+ 'region': "The Azure Fortress (E3M4) Main"},
+ 371415: {'name': 'The Azure Fortress (E3M4) - Ethereal Crossbow',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 53,
+ 'doom_type': 2001,
+ 'region': "The Azure Fortress (E3M4) Main"},
+ 371416: {'name': 'The Azure Fortress (E3M4) - Gauntlets of the Necromancer',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 54,
+ 'doom_type': 2005,
+ 'region': "The Azure Fortress (E3M4) Main"},
+ 371417: {'name': 'The Azure Fortress (E3M4) - Hellstaff',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 55,
+ 'doom_type': 2004,
+ 'region': "The Azure Fortress (E3M4) Yellow"},
+ 371418: {'name': 'The Azure Fortress (E3M4) - Phoenix Rod',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 56,
+ 'doom_type': 2003,
+ 'region': "The Azure Fortress (E3M4) Green"},
+ 371419: {'name': 'The Azure Fortress (E3M4) - Bag of Holding',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 58,
+ 'doom_type': 8,
+ 'region': "The Azure Fortress (E3M4) Main"},
+ 371420: {'name': 'The Azure Fortress (E3M4) - Bag of Holding 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 59,
+ 'doom_type': 8,
+ 'region': "The Azure Fortress (E3M4) Green"},
+ 371421: {'name': 'The Azure Fortress (E3M4) - Morph Ovum',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 60,
+ 'doom_type': 30,
+ 'region': "The Azure Fortress (E3M4) Main"},
+ 371422: {'name': 'The Azure Fortress (E3M4) - Mystic Urn',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 61,
+ 'doom_type': 32,
+ 'region': "The Azure Fortress (E3M4) Main"},
+ 371423: {'name': 'The Azure Fortress (E3M4) - Shadowsphere',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 62,
+ 'doom_type': 75,
+ 'region': "The Azure Fortress (E3M4) Main"},
+ 371424: {'name': 'The Azure Fortress (E3M4) - Silver Shield',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 63,
+ 'doom_type': 85,
+ 'region': "The Azure Fortress (E3M4) Main"},
+ 371425: {'name': 'The Azure Fortress (E3M4) - Silver Shield 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 64,
+ 'doom_type': 85,
+ 'region': "The Azure Fortress (E3M4) Green"},
+ 371426: {'name': 'The Azure Fortress (E3M4) - Enchanted Shield',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 65,
+ 'doom_type': 31,
+ 'region': "The Azure Fortress (E3M4) Green"},
+ 371427: {'name': 'The Azure Fortress (E3M4) - Map Scroll',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 66,
+ 'doom_type': 35,
+ 'region': "The Azure Fortress (E3M4) Green"},
+ 371428: {'name': 'The Azure Fortress (E3M4) - Chaos Device',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 67,
+ 'doom_type': 36,
+ 'region': "The Azure Fortress (E3M4) Yellow"},
+ 371429: {'name': 'The Azure Fortress (E3M4) - Tome of Power',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 68,
+ 'doom_type': 86,
+ 'region': "The Azure Fortress (E3M4) Green"},
+ 371430: {'name': 'The Azure Fortress (E3M4) - Torch',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 69,
+ 'doom_type': 33,
+ 'region': "The Azure Fortress (E3M4) Main"},
+ 371431: {'name': 'The Azure Fortress (E3M4) - Torch 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 70,
+ 'doom_type': 33,
+ 'region': "The Azure Fortress (E3M4) Green"},
+ 371432: {'name': 'The Azure Fortress (E3M4) - Torch 3',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 71,
+ 'doom_type': 33,
+ 'region': "The Azure Fortress (E3M4) Yellow"},
+ 371433: {'name': 'The Azure Fortress (E3M4) - Tome of Power 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 72,
+ 'doom_type': 86,
+ 'region': "The Azure Fortress (E3M4) Main"},
+ 371434: {'name': 'The Azure Fortress (E3M4) - Tome of Power 3',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 73,
+ 'doom_type': 86,
+ 'region': "The Azure Fortress (E3M4) Main"},
+ 371435: {'name': 'The Azure Fortress (E3M4) - Enchanted Shield 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 75,
+ 'doom_type': 31,
+ 'region': "The Azure Fortress (E3M4) Yellow"},
+ 371436: {'name': 'The Azure Fortress (E3M4) - Morph Ovum 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 76,
+ 'doom_type': 30,
+ 'region': "The Azure Fortress (E3M4) Yellow"},
+ 371437: {'name': 'The Azure Fortress (E3M4) - Mystic Urn 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 577,
+ 'doom_type': 32,
+ 'region': "The Azure Fortress (E3M4) Green"},
+ 371438: {'name': 'The Azure Fortress (E3M4) - Exit',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Azure Fortress (E3M4) Green"},
+ 371439: {'name': 'The Ophidian Lair (E3M5) - Yellow key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 16,
+ 'doom_type': 80,
+ 'region': "The Ophidian Lair (E3M5) Main"},
+ 371440: {'name': 'The Ophidian Lair (E3M5) - Green key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 30,
+ 'doom_type': 73,
+ 'region': "The Ophidian Lair (E3M5) Yellow"},
+ 371441: {'name': 'The Ophidian Lair (E3M5) - Hellstaff',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 48,
+ 'doom_type': 2004,
+ 'region': "The Ophidian Lair (E3M5) Main"},
+ 371442: {'name': 'The Ophidian Lair (E3M5) - Phoenix Rod',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 5,
+ 'index': 49,
+ 'doom_type': 2003,
+ 'region': "The Ophidian Lair (E3M5) Main"},
+ 371443: {'name': 'The Ophidian Lair (E3M5) - Dragon Claw',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 50,
+ 'doom_type': 53,
+ 'region': "The Ophidian Lair (E3M5) Yellow"},
+ 371444: {'name': 'The Ophidian Lair (E3M5) - Gauntlets of the Necromancer',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 51,
+ 'doom_type': 2005,
+ 'region': "The Ophidian Lair (E3M5) Yellow"},
+ 371445: {'name': 'The Ophidian Lair (E3M5) - Bag of Holding',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 52,
+ 'doom_type': 8,
+ 'region': "The Ophidian Lair (E3M5) Yellow"},
+ 371446: {'name': 'The Ophidian Lair (E3M5) - Morph Ovum',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 53,
+ 'doom_type': 30,
+ 'region': "The Ophidian Lair (E3M5) Yellow"},
+ 371447: {'name': 'The Ophidian Lair (E3M5) - Ethereal Crossbow',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 62,
+ 'doom_type': 2001,
+ 'region': "The Ophidian Lair (E3M5) Main"},
+ 371448: {'name': 'The Ophidian Lair (E3M5) - Mystic Urn',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 63,
+ 'doom_type': 32,
+ 'region': "The Ophidian Lair (E3M5) Green"},
+ 371449: {'name': 'The Ophidian Lair (E3M5) - Shadowsphere',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 64,
+ 'doom_type': 75,
+ 'region': "The Ophidian Lair (E3M5) Yellow"},
+ 371450: {'name': 'The Ophidian Lair (E3M5) - Ring of Invincibility',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 65,
+ 'doom_type': 84,
+ 'region': "The Ophidian Lair (E3M5) Main"},
+ 371451: {'name': 'The Ophidian Lair (E3M5) - Silver Shield',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 66,
+ 'doom_type': 85,
+ 'region': "The Ophidian Lair (E3M5) Main"},
+ 371452: {'name': 'The Ophidian Lair (E3M5) - Enchanted Shield',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 67,
+ 'doom_type': 31,
+ 'region': "The Ophidian Lair (E3M5) Main"},
+ 371453: {'name': 'The Ophidian Lair (E3M5) - Silver Shield 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 68,
+ 'doom_type': 85,
+ 'region': "The Ophidian Lair (E3M5) Green"},
+ 371454: {'name': 'The Ophidian Lair (E3M5) - Map Scroll',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 69,
+ 'doom_type': 35,
+ 'region': "The Ophidian Lair (E3M5) Green"},
+ 371455: {'name': 'The Ophidian Lair (E3M5) - Chaos Device',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 70,
+ 'doom_type': 36,
+ 'region': "The Ophidian Lair (E3M5) Yellow"},
+ 371456: {'name': 'The Ophidian Lair (E3M5) - Torch',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 71,
+ 'doom_type': 33,
+ 'region': "The Ophidian Lair (E3M5) Main"},
+ 371457: {'name': 'The Ophidian Lair (E3M5) - Tome of Power',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 5,
+ 'index': 72,
+ 'doom_type': 86,
+ 'region': "The Ophidian Lair (E3M5) Main"},
+ 371458: {'name': 'The Ophidian Lair (E3M5) - Mystic Urn 2',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 5,
+ 'index': 73,
+ 'doom_type': 32,
+ 'region': "The Ophidian Lair (E3M5) Main"},
+ 371459: {'name': 'The Ophidian Lair (E3M5) - Tome of Power 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 74,
+ 'doom_type': 86,
+ 'region': "The Ophidian Lair (E3M5) Main"},
+ 371460: {'name': 'The Ophidian Lair (E3M5) - Exit',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Ophidian Lair (E3M5) Green"},
+ 371461: {'name': 'The Halls of Fear (E3M6) - Yellow key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 10,
+ 'doom_type': 80,
+ 'region': "The Halls of Fear (E3M6) Main"},
+ 371462: {'name': 'The Halls of Fear (E3M6) - Green key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 12,
+ 'doom_type': 73,
+ 'region': "The Halls of Fear (E3M6) Yellow"},
+ 371463: {'name': 'The Halls of Fear (E3M6) - Blue key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 15,
+ 'doom_type': 79,
+ 'region': "The Halls of Fear (E3M6) Green"},
+ 371464: {'name': 'The Halls of Fear (E3M6) - Hellstaff',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 31,
+ 'doom_type': 2004,
+ 'region': "The Halls of Fear (E3M6) Green"},
+ 371465: {'name': 'The Halls of Fear (E3M6) - Phoenix Rod',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 32,
+ 'doom_type': 2003,
+ 'region': "The Halls of Fear (E3M6) Cyan"},
+ 371466: {'name': 'The Halls of Fear (E3M6) - Ethereal Crossbow',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 33,
+ 'doom_type': 2001,
+ 'region': "The Halls of Fear (E3M6) Main"},
+ 371467: {'name': 'The Halls of Fear (E3M6) - Dragon Claw',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 34,
+ 'doom_type': 53,
+ 'region': "The Halls of Fear (E3M6) Main"},
+ 371468: {'name': 'The Halls of Fear (E3M6) - Gauntlets of the Necromancer',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 35,
+ 'doom_type': 2005,
+ 'region': "The Halls of Fear (E3M6) Yellow"},
+ 371469: {'name': 'The Halls of Fear (E3M6) - Chaos Device',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 38,
+ 'doom_type': 36,
+ 'region': "The Halls of Fear (E3M6) Green"},
+ 371470: {'name': 'The Halls of Fear (E3M6) - Bag of Holding',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 40,
+ 'doom_type': 8,
+ 'region': "The Halls of Fear (E3M6) Blue"},
+ 371471: {'name': 'The Halls of Fear (E3M6) - Bag of Holding 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 41,
+ 'doom_type': 8,
+ 'region': "The Halls of Fear (E3M6) Blue"},
+ 371472: {'name': 'The Halls of Fear (E3M6) - Morph Ovum',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 42,
+ 'doom_type': 30,
+ 'region': "The Halls of Fear (E3M6) Yellow"},
+ 371473: {'name': 'The Halls of Fear (E3M6) - Mystic Urn',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 51,
+ 'doom_type': 32,
+ 'region': "The Halls of Fear (E3M6) Yellow"},
+ 371474: {'name': 'The Halls of Fear (E3M6) - Shadowsphere',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 52,
+ 'doom_type': 75,
+ 'region': "The Halls of Fear (E3M6) Green"},
+ 371475: {'name': 'The Halls of Fear (E3M6) - Ring of Invincibility',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 6,
+ 'index': 53,
+ 'doom_type': 84,
+ 'region': "The Halls of Fear (E3M6) Main"},
+ 371476: {'name': 'The Halls of Fear (E3M6) - Silver Shield',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 54,
+ 'doom_type': 85,
+ 'region': "The Halls of Fear (E3M6) Yellow"},
+ 371477: {'name': 'The Halls of Fear (E3M6) - Enchanted Shield',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 55,
+ 'doom_type': 31,
+ 'region': "The Halls of Fear (E3M6) Cyan"},
+ 371478: {'name': 'The Halls of Fear (E3M6) - Map Scroll',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 56,
+ 'doom_type': 35,
+ 'region': "The Halls of Fear (E3M6) Blue"},
+ 371479: {'name': 'The Halls of Fear (E3M6) - Tome of Power',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 57,
+ 'doom_type': 86,
+ 'region': "The Halls of Fear (E3M6) Cyan"},
+ 371480: {'name': 'The Halls of Fear (E3M6) - Tome of Power 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 58,
+ 'doom_type': 86,
+ 'region': "The Halls of Fear (E3M6) Green"},
+ 371481: {'name': 'The Halls of Fear (E3M6) - Mystic Urn 2',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 6,
+ 'index': 59,
+ 'doom_type': 32,
+ 'region': "The Halls of Fear (E3M6) Blue"},
+ 371482: {'name': 'The Halls of Fear (E3M6) - Bag of Holding 3',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 363,
+ 'doom_type': 8,
+ 'region': "The Halls of Fear (E3M6) Blue"},
+ 371483: {'name': 'The Halls of Fear (E3M6) - Tome of Power 3',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 364,
+ 'doom_type': 86,
+ 'region': "The Halls of Fear (E3M6) Main"},
+ 371484: {'name': 'The Halls of Fear (E3M6) - Firemace',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 468,
+ 'doom_type': 2002,
+ 'region': "The Halls of Fear (E3M6) Blue"},
+ 371485: {'name': 'The Halls of Fear (E3M6) - Hellstaff 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 472,
+ 'doom_type': 2004,
+ 'region': "The Halls of Fear (E3M6) Main"},
+ 371486: {'name': 'The Halls of Fear (E3M6) - Firemace 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 506,
+ 'doom_type': 2002,
+ 'region': "The Halls of Fear (E3M6) Green"},
+ 371487: {'name': 'The Halls of Fear (E3M6) - Firemace 3',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 6,
+ 'index': 507,
+ 'doom_type': 2002,
+ 'region': "The Halls of Fear (E3M6) Blue"},
+ 371488: {'name': 'The Halls of Fear (E3M6) - Firemace 4',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 508,
+ 'doom_type': 2002,
+ 'region': "The Halls of Fear (E3M6) Main"},
+ 371489: {'name': 'The Halls of Fear (E3M6) - Firemace 5',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 6,
+ 'index': 509,
+ 'doom_type': 2002,
+ 'region': "The Halls of Fear (E3M6) Green"},
+ 371490: {'name': 'The Halls of Fear (E3M6) - Firemace 6',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 6,
+ 'index': 510,
+ 'doom_type': 2002,
+ 'region': "The Halls of Fear (E3M6) Green"},
+ 371491: {'name': 'The Halls of Fear (E3M6) - Exit',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Halls of Fear (E3M6) Blue"},
+ 371492: {'name': 'The Chasm (E3M7) - Blue key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 5,
+ 'doom_type': 79,
+ 'region': "The Chasm (E3M7) Green"},
+ 371493: {'name': 'The Chasm (E3M7) - Green key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 12,
+ 'doom_type': 73,
+ 'region': "The Chasm (E3M7) Yellow"},
+ 371494: {'name': 'The Chasm (E3M7) - Yellow key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 26,
+ 'doom_type': 80,
+ 'region': "The Chasm (E3M7) Main"},
+ 371495: {'name': 'The Chasm (E3M7) - Ethereal Crossbow',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 254,
+ 'doom_type': 2001,
+ 'region': "The Chasm (E3M7) Main"},
+ 371496: {'name': 'The Chasm (E3M7) - Hellstaff',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 255,
+ 'doom_type': 2004,
+ 'region': "The Chasm (E3M7) Yellow"},
+ 371497: {'name': 'The Chasm (E3M7) - Gauntlets of the Necromancer',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 256,
+ 'doom_type': 2005,
+ 'region': "The Chasm (E3M7) Green"},
+ 371498: {'name': 'The Chasm (E3M7) - Dragon Claw',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 257,
+ 'doom_type': 53,
+ 'region': "The Chasm (E3M7) Main"},
+ 371499: {'name': 'The Chasm (E3M7) - Phoenix Rod',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 259,
+ 'doom_type': 2003,
+ 'region': "The Chasm (E3M7) Green"},
+ 371500: {'name': 'The Chasm (E3M7) - Shadowsphere',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 7,
+ 'index': 260,
+ 'doom_type': 75,
+ 'region': "The Chasm (E3M7) Green"},
+ 371501: {'name': 'The Chasm (E3M7) - Bag of Holding',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 262,
+ 'doom_type': 8,
+ 'region': "The Chasm (E3M7) Main"},
+ 371502: {'name': 'The Chasm (E3M7) - Silver Shield',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 268,
+ 'doom_type': 85,
+ 'region': "The Chasm (E3M7) Main"},
+ 371503: {'name': 'The Chasm (E3M7) - Tome of Power',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 269,
+ 'doom_type': 86,
+ 'region': "The Chasm (E3M7) Main"},
+ 371504: {'name': 'The Chasm (E3M7) - Torch',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 7,
+ 'index': 270,
+ 'doom_type': 33,
+ 'region': "The Chasm (E3M7) Yellow"},
+ 371505: {'name': 'The Chasm (E3M7) - Morph Ovum',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 278,
+ 'doom_type': 30,
+ 'region': "The Chasm (E3M7) Yellow"},
+ 371506: {'name': 'The Chasm (E3M7) - Ring of Invincibility',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 282,
+ 'doom_type': 84,
+ 'region': "The Chasm (E3M7) Green"},
+ 371507: {'name': 'The Chasm (E3M7) - Mystic Urn',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 283,
+ 'doom_type': 32,
+ 'region': "The Chasm (E3M7) Green"},
+ 371508: {'name': 'The Chasm (E3M7) - Enchanted Shield',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 284,
+ 'doom_type': 31,
+ 'region': "The Chasm (E3M7) Green"},
+ 371509: {'name': 'The Chasm (E3M7) - Map Scroll',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 285,
+ 'doom_type': 35,
+ 'region': "The Chasm (E3M7) Green"},
+ 371510: {'name': 'The Chasm (E3M7) - Chaos Device',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 286,
+ 'doom_type': 36,
+ 'region': "The Chasm (E3M7) Green"},
+ 371511: {'name': 'The Chasm (E3M7) - Tome of Power 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 287,
+ 'doom_type': 86,
+ 'region': "The Chasm (E3M7) Green"},
+ 371512: {'name': 'The Chasm (E3M7) - Tome of Power 3',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 288,
+ 'doom_type': 86,
+ 'region': "The Chasm (E3M7) Green"},
+ 371513: {'name': 'The Chasm (E3M7) - Torch 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 289,
+ 'doom_type': 33,
+ 'region': "The Chasm (E3M7) Green"},
+ 371514: {'name': 'The Chasm (E3M7) - Shadowsphere 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 337,
+ 'doom_type': 75,
+ 'region': "The Chasm (E3M7) Main"},
+ 371515: {'name': 'The Chasm (E3M7) - Bag of Holding 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 660,
+ 'doom_type': 8,
+ 'region': "The Chasm (E3M7) Main"},
+ 371516: {'name': 'The Chasm (E3M7) - Exit',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Chasm (E3M7) Blue"},
+ 371517: {'name': "D'Sparil'S Keep (E3M8) - Phoenix Rod",
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 55,
+ 'doom_type': 2003,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371518: {'name': "D'Sparil'S Keep (E3M8) - Ethereal Crossbow",
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 8,
+ 'index': 56,
+ 'doom_type': 2001,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371519: {'name': "D'Sparil'S Keep (E3M8) - Dragon Claw",
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 57,
+ 'doom_type': 53,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371520: {'name': "D'Sparil'S Keep (E3M8) - Gauntlets of the Necromancer",
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 58,
+ 'doom_type': 2005,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371521: {'name': "D'Sparil'S Keep (E3M8) - Hellstaff",
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 59,
+ 'doom_type': 2004,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371522: {'name': "D'Sparil'S Keep (E3M8) - Bag of Holding",
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 63,
+ 'doom_type': 8,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371523: {'name': "D'Sparil'S Keep (E3M8) - Mystic Urn",
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 64,
+ 'doom_type': 32,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371524: {'name': "D'Sparil'S Keep (E3M8) - Ring of Invincibility",
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 65,
+ 'doom_type': 84,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371525: {'name': "D'Sparil'S Keep (E3M8) - Shadowsphere",
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 66,
+ 'doom_type': 75,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371526: {'name': "D'Sparil'S Keep (E3M8) - Silver Shield",
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 67,
+ 'doom_type': 85,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371527: {'name': "D'Sparil'S Keep (E3M8) - Enchanted Shield",
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 68,
+ 'doom_type': 31,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371528: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power",
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 69,
+ 'doom_type': 86,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371529: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power 2",
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 8,
+ 'index': 70,
+ 'doom_type': 86,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371530: {'name': "D'Sparil'S Keep (E3M8) - Chaos Device",
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 8,
+ 'index': 71,
+ 'doom_type': 36,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371531: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power 3",
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 8,
+ 'index': 245,
+ 'doom_type': 86,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371532: {'name': "D'Sparil'S Keep (E3M8) - Exit",
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "D'Sparil'S Keep (E3M8) Main"},
+ 371533: {'name': 'The Aquifier (E3M9) - Blue key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 12,
+ 'doom_type': 79,
+ 'region': "The Aquifier (E3M9) Green"},
+ 371534: {'name': 'The Aquifier (E3M9) - Green key',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 13,
+ 'doom_type': 73,
+ 'region': "The Aquifier (E3M9) Yellow"},
+ 371535: {'name': 'The Aquifier (E3M9) - Yellow key',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 9,
+ 'index': 14,
+ 'doom_type': 80,
+ 'region': "The Aquifier (E3M9) Main"},
+ 371536: {'name': 'The Aquifier (E3M9) - Ethereal Crossbow',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 141,
+ 'doom_type': 2001,
+ 'region': "The Aquifier (E3M9) Main"},
+ 371537: {'name': 'The Aquifier (E3M9) - Phoenix Rod',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 142,
+ 'doom_type': 2003,
+ 'region': "The Aquifier (E3M9) Yellow"},
+ 371538: {'name': 'The Aquifier (E3M9) - Dragon Claw',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 143,
+ 'doom_type': 53,
+ 'region': "The Aquifier (E3M9) Green"},
+ 371539: {'name': 'The Aquifier (E3M9) - Hellstaff',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 144,
+ 'doom_type': 2004,
+ 'region': "The Aquifier (E3M9) Green"},
+ 371540: {'name': 'The Aquifier (E3M9) - Gauntlets of the Necromancer',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 145,
+ 'doom_type': 2005,
+ 'region': "The Aquifier (E3M9) Green"},
+ 371541: {'name': 'The Aquifier (E3M9) - Ring of Invincibility',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 148,
+ 'doom_type': 84,
+ 'region': "The Aquifier (E3M9) Yellow"},
+ 371542: {'name': 'The Aquifier (E3M9) - Mystic Urn',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 149,
+ 'doom_type': 32,
+ 'region': "The Aquifier (E3M9) Green"},
+ 371543: {'name': 'The Aquifier (E3M9) - Silver Shield',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 151,
+ 'doom_type': 85,
+ 'region': "The Aquifier (E3M9) Main"},
+ 371544: {'name': 'The Aquifier (E3M9) - Tome of Power',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 152,
+ 'doom_type': 86,
+ 'region': "The Aquifier (E3M9) Main"},
+ 371545: {'name': 'The Aquifier (E3M9) - Bag of Holding',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 153,
+ 'doom_type': 8,
+ 'region': "The Aquifier (E3M9) Yellow"},
+ 371546: {'name': 'The Aquifier (E3M9) - Morph Ovum',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 154,
+ 'doom_type': 30,
+ 'region': "The Aquifier (E3M9) Green"},
+ 371547: {'name': 'The Aquifier (E3M9) - Map Scroll',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 9,
+ 'index': 155,
+ 'doom_type': 35,
+ 'region': "The Aquifier (E3M9) Green"},
+ 371548: {'name': 'The Aquifier (E3M9) - Chaos Device',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 156,
+ 'doom_type': 36,
+ 'region': "The Aquifier (E3M9) Yellow"},
+ 371549: {'name': 'The Aquifier (E3M9) - Enchanted Shield',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 157,
+ 'doom_type': 31,
+ 'region': "The Aquifier (E3M9) Green"},
+ 371550: {'name': 'The Aquifier (E3M9) - Tome of Power 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 158,
+ 'doom_type': 86,
+ 'region': "The Aquifier (E3M9) Green"},
+ 371551: {'name': 'The Aquifier (E3M9) - Torch',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 159,
+ 'doom_type': 33,
+ 'region': "The Aquifier (E3M9) Main"},
+ 371552: {'name': 'The Aquifier (E3M9) - Shadowsphere',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 160,
+ 'doom_type': 75,
+ 'region': "The Aquifier (E3M9) Green"},
+ 371553: {'name': 'The Aquifier (E3M9) - Silver Shield 2',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 9,
+ 'index': 374,
+ 'doom_type': 85,
+ 'region': "The Aquifier (E3M9) Green"},
+ 371554: {'name': 'The Aquifier (E3M9) - Firemace',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 478,
+ 'doom_type': 2002,
+ 'region': "The Aquifier (E3M9) Green"},
+ 371555: {'name': 'The Aquifier (E3M9) - Firemace 2',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 526,
+ 'doom_type': 2002,
+ 'region': "The Aquifier (E3M9) Green"},
+ 371556: {'name': 'The Aquifier (E3M9) - Firemace 3',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 527,
+ 'doom_type': 2002,
+ 'region': "The Aquifier (E3M9) Green"},
+ 371557: {'name': 'The Aquifier (E3M9) - Firemace 4',
+ 'episode': 3,
+ 'check_sanity': True,
+ 'map': 9,
+ 'index': 528,
+ 'doom_type': 2002,
+ 'region': "The Aquifier (E3M9) Yellow"},
+ 371558: {'name': 'The Aquifier (E3M9) - Exit',
+ 'episode': 3,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "The Aquifier (E3M9) Blue"},
+ 371559: {'name': 'Catafalque (E4M1) - Yellow key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 4,
+ 'doom_type': 80,
+ 'region': "Catafalque (E4M1) Main"},
+ 371560: {'name': 'Catafalque (E4M1) - Green key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 10,
+ 'doom_type': 73,
+ 'region': "Catafalque (E4M1) Yellow"},
+ 371561: {'name': 'Catafalque (E4M1) - Ethereal Crossbow',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 100,
+ 'doom_type': 2001,
+ 'region': "Catafalque (E4M1) Main"},
+ 371562: {'name': 'Catafalque (E4M1) - Gauntlets of the Necromancer',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 101,
+ 'doom_type': 2005,
+ 'region': "Catafalque (E4M1) Yellow"},
+ 371563: {'name': 'Catafalque (E4M1) - Dragon Claw',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 102,
+ 'doom_type': 53,
+ 'region': "Catafalque (E4M1) Yellow"},
+ 371564: {'name': 'Catafalque (E4M1) - Hellstaff',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 103,
+ 'doom_type': 2004,
+ 'region': "Catafalque (E4M1) Green"},
+ 371565: {'name': 'Catafalque (E4M1) - Shadowsphere',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 114,
+ 'doom_type': 75,
+ 'region': "Catafalque (E4M1) Yellow"},
+ 371566: {'name': 'Catafalque (E4M1) - Ring of Invincibility',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 115,
+ 'doom_type': 84,
+ 'region': "Catafalque (E4M1) Green"},
+ 371567: {'name': 'Catafalque (E4M1) - Silver Shield',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 116,
+ 'doom_type': 85,
+ 'region': "Catafalque (E4M1) Main"},
+ 371568: {'name': 'Catafalque (E4M1) - Map Scroll',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 117,
+ 'doom_type': 35,
+ 'region': "Catafalque (E4M1) Green"},
+ 371569: {'name': 'Catafalque (E4M1) - Chaos Device',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 118,
+ 'doom_type': 36,
+ 'region': "Catafalque (E4M1) Yellow"},
+ 371570: {'name': 'Catafalque (E4M1) - Tome of Power',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 119,
+ 'doom_type': 86,
+ 'region': "Catafalque (E4M1) Yellow"},
+ 371571: {'name': 'Catafalque (E4M1) - Tome of Power 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 120,
+ 'doom_type': 86,
+ 'region': "Catafalque (E4M1) Main"},
+ 371572: {'name': 'Catafalque (E4M1) - Torch',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 121,
+ 'doom_type': 33,
+ 'region': "Catafalque (E4M1) Yellow"},
+ 371573: {'name': 'Catafalque (E4M1) - Bag of Holding',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 122,
+ 'doom_type': 8,
+ 'region': "Catafalque (E4M1) Main"},
+ 371574: {'name': 'Catafalque (E4M1) - Morph Ovum',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 123,
+ 'doom_type': 30,
+ 'region': "Catafalque (E4M1) Main"},
+ 371575: {'name': 'Catafalque (E4M1) - Exit',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Catafalque (E4M1) Green"},
+ 371576: {'name': 'Blockhouse (E4M2) - Green key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 18,
+ 'doom_type': 73,
+ 'region': "Blockhouse (E4M2) Yellow"},
+ 371577: {'name': 'Blockhouse (E4M2) - Yellow key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 19,
+ 'doom_type': 80,
+ 'region': "Blockhouse (E4M2) Main"},
+ 371578: {'name': 'Blockhouse (E4M2) - Blue key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 25,
+ 'doom_type': 79,
+ 'region': "Blockhouse (E4M2) Green"},
+ 371579: {'name': 'Blockhouse (E4M2) - Gauntlets of the Necromancer',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 46,
+ 'doom_type': 2005,
+ 'region': "Blockhouse (E4M2) Main"},
+ 371580: {'name': 'Blockhouse (E4M2) - Ethereal Crossbow',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 47,
+ 'doom_type': 2001,
+ 'region': "Blockhouse (E4M2) Main"},
+ 371581: {'name': 'Blockhouse (E4M2) - Dragon Claw',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 48,
+ 'doom_type': 53,
+ 'region': "Blockhouse (E4M2) Main"},
+ 371582: {'name': 'Blockhouse (E4M2) - Hellstaff',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 49,
+ 'doom_type': 2004,
+ 'region': "Blockhouse (E4M2) Main"},
+ 371583: {'name': 'Blockhouse (E4M2) - Phoenix Rod',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 50,
+ 'doom_type': 2003,
+ 'region': "Blockhouse (E4M2) Main"},
+ 371584: {'name': 'Blockhouse (E4M2) - Bag of Holding',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 58,
+ 'doom_type': 8,
+ 'region': "Blockhouse (E4M2) Main"},
+ 371585: {'name': 'Blockhouse (E4M2) - Mystic Urn',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 67,
+ 'doom_type': 32,
+ 'region': "Blockhouse (E4M2) Main"},
+ 371586: {'name': 'Blockhouse (E4M2) - Silver Shield',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 68,
+ 'doom_type': 85,
+ 'region': "Blockhouse (E4M2) Main"},
+ 371587: {'name': 'Blockhouse (E4M2) - Morph Ovum',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 69,
+ 'doom_type': 30,
+ 'region': "Blockhouse (E4M2) Main"},
+ 371588: {'name': 'Blockhouse (E4M2) - Tome of Power',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 70,
+ 'doom_type': 86,
+ 'region': "Blockhouse (E4M2) Main"},
+ 371589: {'name': 'Blockhouse (E4M2) - Chaos Device',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 71,
+ 'doom_type': 36,
+ 'region': "Blockhouse (E4M2) Green"},
+ 371590: {'name': 'Blockhouse (E4M2) - Ring of Invincibility',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 72,
+ 'doom_type': 84,
+ 'region': "Blockhouse (E4M2) Green"},
+ 371591: {'name': 'Blockhouse (E4M2) - Bag of Holding 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 73,
+ 'doom_type': 8,
+ 'region': "Blockhouse (E4M2) Green"},
+ 371592: {'name': 'Blockhouse (E4M2) - Enchanted Shield',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 74,
+ 'doom_type': 31,
+ 'region': "Blockhouse (E4M2) Yellow"},
+ 371593: {'name': 'Blockhouse (E4M2) - Shadowsphere',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 75,
+ 'doom_type': 75,
+ 'region': "Blockhouse (E4M2) Main"},
+ 371594: {'name': 'Blockhouse (E4M2) - Ring of Invincibility 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 226,
+ 'doom_type': 84,
+ 'region': "Blockhouse (E4M2) Lake"},
+ 371595: {'name': 'Blockhouse (E4M2) - Shadowsphere 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 227,
+ 'doom_type': 75,
+ 'region': "Blockhouse (E4M2) Lake"},
+ 371596: {'name': 'Blockhouse (E4M2) - Exit',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Blockhouse (E4M2) Blue"},
+ 371597: {'name': 'Ambulatory (E4M3) - Yellow key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 10,
+ 'doom_type': 80,
+ 'region': "Ambulatory (E4M3) Main"},
+ 371598: {'name': 'Ambulatory (E4M3) - Green key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 11,
+ 'doom_type': 73,
+ 'region': "Ambulatory (E4M3) Yellow"},
+ 371599: {'name': 'Ambulatory (E4M3) - Blue key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 12,
+ 'doom_type': 79,
+ 'region': "Ambulatory (E4M3) Green"},
+ 371600: {'name': 'Ambulatory (E4M3) - Ethereal Crossbow',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 265,
+ 'doom_type': 2001,
+ 'region': "Ambulatory (E4M3) Main"},
+ 371601: {'name': 'Ambulatory (E4M3) - Gauntlets of the Necromancer',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 266,
+ 'doom_type': 2005,
+ 'region': "Ambulatory (E4M3) Main"},
+ 371602: {'name': 'Ambulatory (E4M3) - Dragon Claw',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 267,
+ 'doom_type': 53,
+ 'region': "Ambulatory (E4M3) Yellow"},
+ 371603: {'name': 'Ambulatory (E4M3) - Hellstaff',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 268,
+ 'doom_type': 2004,
+ 'region': "Ambulatory (E4M3) Green"},
+ 371604: {'name': 'Ambulatory (E4M3) - Phoenix Rod',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 269,
+ 'doom_type': 2003,
+ 'region': "Ambulatory (E4M3) Blue"},
+ 371605: {'name': 'Ambulatory (E4M3) - Tome of Power',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 270,
+ 'doom_type': 86,
+ 'region': "Ambulatory (E4M3) Main"},
+ 371606: {'name': 'Ambulatory (E4M3) - Silver Shield',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 271,
+ 'doom_type': 85,
+ 'region': "Ambulatory (E4M3) Yellow"},
+ 371607: {'name': 'Ambulatory (E4M3) - Map Scroll',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 272,
+ 'doom_type': 35,
+ 'region': "Ambulatory (E4M3) Yellow"},
+ 371608: {'name': 'Ambulatory (E4M3) - Bag of Holding',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 273,
+ 'doom_type': 8,
+ 'region': "Ambulatory (E4M3) Yellow"},
+ 371609: {'name': 'Ambulatory (E4M3) - Shadowsphere',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 274,
+ 'doom_type': 75,
+ 'region': "Ambulatory (E4M3) Yellow"},
+ 371610: {'name': 'Ambulatory (E4M3) - Morph Ovum',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 275,
+ 'doom_type': 30,
+ 'region': "Ambulatory (E4M3) Yellow"},
+ 371611: {'name': 'Ambulatory (E4M3) - Torch',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 276,
+ 'doom_type': 33,
+ 'region': "Ambulatory (E4M3) Green"},
+ 371612: {'name': 'Ambulatory (E4M3) - Tome of Power 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 277,
+ 'doom_type': 86,
+ 'region': "Ambulatory (E4M3) Green"},
+ 371613: {'name': 'Ambulatory (E4M3) - Enchanted Shield',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 278,
+ 'doom_type': 31,
+ 'region': "Ambulatory (E4M3) Blue"},
+ 371614: {'name': 'Ambulatory (E4M3) - Mystic Urn',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 279,
+ 'doom_type': 32,
+ 'region': "Ambulatory (E4M3) Blue"},
+ 371615: {'name': 'Ambulatory (E4M3) - Ring of Invincibility',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 281,
+ 'doom_type': 84,
+ 'region': "Ambulatory (E4M3) Blue"},
+ 371616: {'name': 'Ambulatory (E4M3) - Chaos Device',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 282,
+ 'doom_type': 36,
+ 'region': "Ambulatory (E4M3) Green"},
+ 371617: {'name': 'Ambulatory (E4M3) - Ring of Invincibility 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 283,
+ 'doom_type': 84,
+ 'region': "Ambulatory (E4M3) Green"},
+ 371618: {'name': 'Ambulatory (E4M3) - Morph Ovum 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 284,
+ 'doom_type': 30,
+ 'region': "Ambulatory (E4M3) Blue"},
+ 371619: {'name': 'Ambulatory (E4M3) - Bag of Holding 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 285,
+ 'doom_type': 8,
+ 'region': "Ambulatory (E4M3) Yellow"},
+ 371620: {'name': 'Ambulatory (E4M3) - Firemace',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 297,
+ 'doom_type': 2002,
+ 'region': "Ambulatory (E4M3) Green Lock"},
+ 371621: {'name': 'Ambulatory (E4M3) - Firemace 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 298,
+ 'doom_type': 2002,
+ 'region': "Ambulatory (E4M3) Yellow"},
+ 371622: {'name': 'Ambulatory (E4M3) - Firemace 3',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 299,
+ 'doom_type': 2002,
+ 'region': "Ambulatory (E4M3) Yellow"},
+ 371623: {'name': 'Ambulatory (E4M3) - Firemace 4',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 300,
+ 'doom_type': 2002,
+ 'region': "Ambulatory (E4M3) Blue"},
+ 371624: {'name': 'Ambulatory (E4M3) - Firemace 5',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 301,
+ 'doom_type': 2002,
+ 'region': "Ambulatory (E4M3) Green"},
+ 371625: {'name': 'Ambulatory (E4M3) - Exit',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Ambulatory (E4M3) Blue"},
+ 371626: {'name': 'Sepulcher (E4M4) - Silver Shield',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 27,
+ 'doom_type': 85,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371627: {'name': 'Sepulcher (E4M4) - Hellstaff',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 28,
+ 'doom_type': 2004,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371628: {'name': 'Sepulcher (E4M4) - Dragon Claw',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 29,
+ 'doom_type': 53,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371629: {'name': 'Sepulcher (E4M4) - Ethereal Crossbow',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 30,
+ 'doom_type': 2001,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371630: {'name': 'Sepulcher (E4M4) - Tome of Power',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 31,
+ 'doom_type': 86,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371631: {'name': 'Sepulcher (E4M4) - Shadowsphere',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 40,
+ 'doom_type': 75,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371632: {'name': 'Sepulcher (E4M4) - Mystic Urn',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 4,
+ 'index': 41,
+ 'doom_type': 32,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371633: {'name': 'Sepulcher (E4M4) - Chaos Device',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 4,
+ 'index': 50,
+ 'doom_type': 36,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371634: {'name': 'Sepulcher (E4M4) - Enchanted Shield',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 51,
+ 'doom_type': 31,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371635: {'name': 'Sepulcher (E4M4) - Morph Ovum',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 65,
+ 'doom_type': 30,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371636: {'name': 'Sepulcher (E4M4) - Torch',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 66,
+ 'doom_type': 33,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371637: {'name': 'Sepulcher (E4M4) - Firemace',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 67,
+ 'doom_type': 2002,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371638: {'name': 'Sepulcher (E4M4) - Phoenix Rod',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 74,
+ 'doom_type': 2003,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371639: {'name': 'Sepulcher (E4M4) - Ring of Invincibility',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 137,
+ 'doom_type': 84,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371640: {'name': 'Sepulcher (E4M4) - Bag of Holding',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 138,
+ 'doom_type': 8,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371641: {'name': 'Sepulcher (E4M4) - Ethereal Crossbow 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 199,
+ 'doom_type': 2001,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371642: {'name': 'Sepulcher (E4M4) - Bag of Holding 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 235,
+ 'doom_type': 8,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371643: {'name': 'Sepulcher (E4M4) - Tome of Power 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 239,
+ 'doom_type': 86,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371644: {'name': 'Sepulcher (E4M4) - Torch 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 243,
+ 'doom_type': 33,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371645: {'name': 'Sepulcher (E4M4) - Silver Shield 2',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 4,
+ 'index': 244,
+ 'doom_type': 85,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371646: {'name': 'Sepulcher (E4M4) - Firemace 2',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 4,
+ 'index': 307,
+ 'doom_type': 2002,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371647: {'name': 'Sepulcher (E4M4) - Firemace 3',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 4,
+ 'index': 308,
+ 'doom_type': 2002,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371648: {'name': 'Sepulcher (E4M4) - Firemace 4',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 4,
+ 'index': 309,
+ 'doom_type': 2002,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371649: {'name': 'Sepulcher (E4M4) - Firemace 5',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 310,
+ 'doom_type': 2002,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371650: {'name': 'Sepulcher (E4M4) - Dragon Claw 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 325,
+ 'doom_type': 53,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371651: {'name': 'Sepulcher (E4M4) - Phoenix Rod 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 339,
+ 'doom_type': 2003,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371652: {'name': 'Sepulcher (E4M4) - Exit',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Sepulcher (E4M4) Main"},
+ 371653: {'name': 'Great Stair (E4M5) - Ethereal Crossbow',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 3,
+ 'doom_type': 2001,
+ 'region': "Great Stair (E4M5) Main"},
+ 371654: {'name': 'Great Stair (E4M5) - Yellow key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 27,
+ 'doom_type': 80,
+ 'region': "Great Stair (E4M5) Main"},
+ 371655: {'name': 'Great Stair (E4M5) - Dragon Claw',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 5,
+ 'index': 58,
+ 'doom_type': 53,
+ 'region': "Great Stair (E4M5) Yellow"},
+ 371656: {'name': 'Great Stair (E4M5) - Green key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 64,
+ 'doom_type': 73,
+ 'region': "Great Stair (E4M5) Yellow"},
+ 371657: {'name': 'Great Stair (E4M5) - Blue key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 71,
+ 'doom_type': 79,
+ 'region': "Great Stair (E4M5) Green"},
+ 371658: {'name': 'Great Stair (E4M5) - Silver Shield',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 78,
+ 'doom_type': 85,
+ 'region': "Great Stair (E4M5) Main"},
+ 371659: {'name': 'Great Stair (E4M5) - Gauntlets of the Necromancer',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 90,
+ 'doom_type': 2005,
+ 'region': "Great Stair (E4M5) Main"},
+ 371660: {'name': 'Great Stair (E4M5) - Hellstaff',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 91,
+ 'doom_type': 2004,
+ 'region': "Great Stair (E4M5) Yellow"},
+ 371661: {'name': 'Great Stair (E4M5) - Phoenix Rod',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 5,
+ 'index': 92,
+ 'doom_type': 2003,
+ 'region': "Great Stair (E4M5) Green"},
+ 371662: {'name': 'Great Stair (E4M5) - Bag of Holding',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 93,
+ 'doom_type': 8,
+ 'region': "Great Stair (E4M5) Main"},
+ 371663: {'name': 'Great Stair (E4M5) - Bag of Holding 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 94,
+ 'doom_type': 8,
+ 'region': "Great Stair (E4M5) Green"},
+ 371664: {'name': 'Great Stair (E4M5) - Morph Ovum',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 95,
+ 'doom_type': 30,
+ 'region': "Great Stair (E4M5) Main"},
+ 371665: {'name': 'Great Stair (E4M5) - Mystic Urn',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 110,
+ 'doom_type': 32,
+ 'region': "Great Stair (E4M5) Yellow"},
+ 371666: {'name': 'Great Stair (E4M5) - Shadowsphere',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 111,
+ 'doom_type': 75,
+ 'region': "Great Stair (E4M5) Yellow"},
+ 371667: {'name': 'Great Stair (E4M5) - Ring of Invincibility',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 112,
+ 'doom_type': 84,
+ 'region': "Great Stair (E4M5) Main"},
+ 371668: {'name': 'Great Stair (E4M5) - Enchanted Shield',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 113,
+ 'doom_type': 31,
+ 'region': "Great Stair (E4M5) Green"},
+ 371669: {'name': 'Great Stair (E4M5) - Map Scroll',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 114,
+ 'doom_type': 35,
+ 'region': "Great Stair (E4M5) Green"},
+ 371670: {'name': 'Great Stair (E4M5) - Chaos Device',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 115,
+ 'doom_type': 36,
+ 'region': "Great Stair (E4M5) Main"},
+ 371671: {'name': 'Great Stair (E4M5) - Tome of Power',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 5,
+ 'index': 116,
+ 'doom_type': 86,
+ 'region': "Great Stair (E4M5) Main"},
+ 371672: {'name': 'Great Stair (E4M5) - Tome of Power 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 117,
+ 'doom_type': 86,
+ 'region': "Great Stair (E4M5) Yellow"},
+ 371673: {'name': 'Great Stair (E4M5) - Torch',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 5,
+ 'index': 118,
+ 'doom_type': 33,
+ 'region': "Great Stair (E4M5) Main"},
+ 371674: {'name': 'Great Stair (E4M5) - Firemace',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 5,
+ 'index': 123,
+ 'doom_type': 2002,
+ 'region': "Great Stair (E4M5) Main"},
+ 371675: {'name': 'Great Stair (E4M5) - Firemace 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 124,
+ 'doom_type': 2002,
+ 'region': "Great Stair (E4M5) Main"},
+ 371676: {'name': 'Great Stair (E4M5) - Firemace 3',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 125,
+ 'doom_type': 2002,
+ 'region': "Great Stair (E4M5) Yellow"},
+ 371677: {'name': 'Great Stair (E4M5) - Firemace 4',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 126,
+ 'doom_type': 2002,
+ 'region': "Great Stair (E4M5) Blue"},
+ 371678: {'name': 'Great Stair (E4M5) - Firemace 5',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 127,
+ 'doom_type': 2002,
+ 'region': "Great Stair (E4M5) Yellow"},
+ 371679: {'name': 'Great Stair (E4M5) - Mystic Urn 2',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 5,
+ 'index': 507,
+ 'doom_type': 32,
+ 'region': "Great Stair (E4M5) Green"},
+ 371680: {'name': 'Great Stair (E4M5) - Tome of Power 3',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 508,
+ 'doom_type': 86,
+ 'region': "Great Stair (E4M5) Green"},
+ 371681: {'name': 'Great Stair (E4M5) - Exit',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Great Stair (E4M5) Blue"},
+ 371682: {'name': 'Halls of the Apostate (E4M6) - Green key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 17,
+ 'doom_type': 73,
+ 'region': "Halls of the Apostate (E4M6) Yellow"},
+ 371683: {'name': 'Halls of the Apostate (E4M6) - Blue key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 18,
+ 'doom_type': 79,
+ 'region': "Halls of the Apostate (E4M6) Green"},
+ 371684: {'name': 'Halls of the Apostate (E4M6) - Gauntlets of the Necromancer',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 59,
+ 'doom_type': 2005,
+ 'region': "Halls of the Apostate (E4M6) Main"},
+ 371685: {'name': 'Halls of the Apostate (E4M6) - Ethereal Crossbow',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 60,
+ 'doom_type': 2001,
+ 'region': "Halls of the Apostate (E4M6) Main"},
+ 371686: {'name': 'Halls of the Apostate (E4M6) - Dragon Claw',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 6,
+ 'index': 61,
+ 'doom_type': 53,
+ 'region': "Halls of the Apostate (E4M6) Yellow"},
+ 371687: {'name': 'Halls of the Apostate (E4M6) - Hellstaff',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 62,
+ 'doom_type': 2004,
+ 'region': "Halls of the Apostate (E4M6) Green"},
+ 371688: {'name': 'Halls of the Apostate (E4M6) - Phoenix Rod',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 63,
+ 'doom_type': 2003,
+ 'region': "Halls of the Apostate (E4M6) Blue"},
+ 371689: {'name': 'Halls of the Apostate (E4M6) - Bag of Holding',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 68,
+ 'doom_type': 8,
+ 'region': "Halls of the Apostate (E4M6) Main"},
+ 371690: {'name': 'Halls of the Apostate (E4M6) - Morph Ovum',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 79,
+ 'doom_type': 30,
+ 'region': "Halls of the Apostate (E4M6) Yellow"},
+ 371691: {'name': 'Halls of the Apostate (E4M6) - Mystic Urn',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 80,
+ 'doom_type': 32,
+ 'region': "Halls of the Apostate (E4M6) Main"},
+ 371692: {'name': 'Halls of the Apostate (E4M6) - Shadowsphere',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 81,
+ 'doom_type': 75,
+ 'region': "Halls of the Apostate (E4M6) Main"},
+ 371693: {'name': 'Halls of the Apostate (E4M6) - Silver Shield',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 82,
+ 'doom_type': 85,
+ 'region': "Halls of the Apostate (E4M6) Main"},
+ 371694: {'name': 'Halls of the Apostate (E4M6) - Silver Shield 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 83,
+ 'doom_type': 85,
+ 'region': "Halls of the Apostate (E4M6) Blue"},
+ 371695: {'name': 'Halls of the Apostate (E4M6) - Enchanted Shield',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 6,
+ 'index': 84,
+ 'doom_type': 31,
+ 'region': "Halls of the Apostate (E4M6) Green"},
+ 371696: {'name': 'Halls of the Apostate (E4M6) - Map Scroll',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 85,
+ 'doom_type': 35,
+ 'region': "Halls of the Apostate (E4M6) Green"},
+ 371697: {'name': 'Halls of the Apostate (E4M6) - Chaos Device',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 86,
+ 'doom_type': 36,
+ 'region': "Halls of the Apostate (E4M6) Yellow"},
+ 371698: {'name': 'Halls of the Apostate (E4M6) - Tome of Power',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 6,
+ 'index': 87,
+ 'doom_type': 86,
+ 'region': "Halls of the Apostate (E4M6) Main"},
+ 371699: {'name': 'Halls of the Apostate (E4M6) - Tome of Power 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 88,
+ 'doom_type': 86,
+ 'region': "Halls of the Apostate (E4M6) Blue"},
+ 371700: {'name': 'Halls of the Apostate (E4M6) - Bag of Holding 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 89,
+ 'doom_type': 8,
+ 'region': "Halls of the Apostate (E4M6) Green"},
+ 371701: {'name': 'Halls of the Apostate (E4M6) - Ring of Invincibility',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 108,
+ 'doom_type': 84,
+ 'region': "Halls of the Apostate (E4M6) Yellow"},
+ 371702: {'name': 'Halls of the Apostate (E4M6) - Yellow key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 420,
+ 'doom_type': 80,
+ 'region': "Halls of the Apostate (E4M6) Main"},
+ 371703: {'name': 'Halls of the Apostate (E4M6) - Exit',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Halls of the Apostate (E4M6) Blue"},
+ 371704: {'name': 'Ramparts of Perdition (E4M7) - Yellow key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 28,
+ 'doom_type': 80,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371705: {'name': 'Ramparts of Perdition (E4M7) - Green key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 33,
+ 'doom_type': 73,
+ 'region': "Ramparts of Perdition (E4M7) Yellow"},
+ 371706: {'name': 'Ramparts of Perdition (E4M7) - Blue key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 36,
+ 'doom_type': 79,
+ 'region': "Ramparts of Perdition (E4M7) Green"},
+ 371707: {'name': 'Ramparts of Perdition (E4M7) - Ring of Invincibility',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 39,
+ 'doom_type': 84,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371708: {'name': 'Ramparts of Perdition (E4M7) - Mystic Urn',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 40,
+ 'doom_type': 32,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371709: {'name': 'Ramparts of Perdition (E4M7) - Gauntlets of the Necromancer',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 124,
+ 'doom_type': 2005,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371710: {'name': 'Ramparts of Perdition (E4M7) - Ethereal Crossbow',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 7,
+ 'index': 125,
+ 'doom_type': 2001,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371711: {'name': 'Ramparts of Perdition (E4M7) - Dragon Claw',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 7,
+ 'index': 126,
+ 'doom_type': 53,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371712: {'name': 'Ramparts of Perdition (E4M7) - Phoenix Rod',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 127,
+ 'doom_type': 2003,
+ 'region': "Ramparts of Perdition (E4M7) Green"},
+ 371713: {'name': 'Ramparts of Perdition (E4M7) - Hellstaff',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 128,
+ 'doom_type': 2004,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371714: {'name': 'Ramparts of Perdition (E4M7) - Ethereal Crossbow 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 129,
+ 'doom_type': 2001,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371715: {'name': 'Ramparts of Perdition (E4M7) - Dragon Claw 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 130,
+ 'doom_type': 53,
+ 'region': "Ramparts of Perdition (E4M7) Blue"},
+ 371716: {'name': 'Ramparts of Perdition (E4M7) - Phoenix Rod 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 131,
+ 'doom_type': 2003,
+ 'region': "Ramparts of Perdition (E4M7) Blue"},
+ 371717: {'name': 'Ramparts of Perdition (E4M7) - Hellstaff 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 132,
+ 'doom_type': 2004,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371718: {'name': 'Ramparts of Perdition (E4M7) - Firemace',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 7,
+ 'index': 133,
+ 'doom_type': 2002,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371719: {'name': 'Ramparts of Perdition (E4M7) - Firemace 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 134,
+ 'doom_type': 2002,
+ 'region': "Ramparts of Perdition (E4M7) Yellow"},
+ 371720: {'name': 'Ramparts of Perdition (E4M7) - Firemace 3',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 135,
+ 'doom_type': 2002,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371721: {'name': 'Ramparts of Perdition (E4M7) - Firemace 4',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 136,
+ 'doom_type': 2002,
+ 'region': "Ramparts of Perdition (E4M7) Yellow"},
+ 371722: {'name': 'Ramparts of Perdition (E4M7) - Firemace 5',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 7,
+ 'index': 137,
+ 'doom_type': 2002,
+ 'region': "Ramparts of Perdition (E4M7) Yellow"},
+ 371723: {'name': 'Ramparts of Perdition (E4M7) - Firemace 6',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 138,
+ 'doom_type': 2002,
+ 'region': "Ramparts of Perdition (E4M7) Blue"},
+ 371724: {'name': 'Ramparts of Perdition (E4M7) - Bag of Holding',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 140,
+ 'doom_type': 8,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371725: {'name': 'Ramparts of Perdition (E4M7) - Tome of Power',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 141,
+ 'doom_type': 86,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371726: {'name': 'Ramparts of Perdition (E4M7) - Bag of Holding 2',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 7,
+ 'index': 142,
+ 'doom_type': 8,
+ 'region': "Ramparts of Perdition (E4M7) Green"},
+ 371727: {'name': 'Ramparts of Perdition (E4M7) - Morph Ovum',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 143,
+ 'doom_type': 30,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371728: {'name': 'Ramparts of Perdition (E4M7) - Shadowsphere',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 153,
+ 'doom_type': 75,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371729: {'name': 'Ramparts of Perdition (E4M7) - Silver Shield',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 154,
+ 'doom_type': 85,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371730: {'name': 'Ramparts of Perdition (E4M7) - Silver Shield 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 155,
+ 'doom_type': 85,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371731: {'name': 'Ramparts of Perdition (E4M7) - Enchanted Shield',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 156,
+ 'doom_type': 31,
+ 'region': "Ramparts of Perdition (E4M7) Yellow"},
+ 371732: {'name': 'Ramparts of Perdition (E4M7) - Tome of Power 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 157,
+ 'doom_type': 86,
+ 'region': "Ramparts of Perdition (E4M7) Yellow"},
+ 371733: {'name': 'Ramparts of Perdition (E4M7) - Tome of Power 3',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 158,
+ 'doom_type': 86,
+ 'region': "Ramparts of Perdition (E4M7) Blue"},
+ 371734: {'name': 'Ramparts of Perdition (E4M7) - Torch',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 159,
+ 'doom_type': 33,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371735: {'name': 'Ramparts of Perdition (E4M7) - Torch 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 160,
+ 'doom_type': 33,
+ 'region': "Ramparts of Perdition (E4M7) Yellow"},
+ 371736: {'name': 'Ramparts of Perdition (E4M7) - Mystic Urn 2',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 7,
+ 'index': 161,
+ 'doom_type': 32,
+ 'region': "Ramparts of Perdition (E4M7) Yellow"},
+ 371737: {'name': 'Ramparts of Perdition (E4M7) - Chaos Device',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 162,
+ 'doom_type': 36,
+ 'region': "Ramparts of Perdition (E4M7) Yellow"},
+ 371738: {'name': 'Ramparts of Perdition (E4M7) - Map Scroll',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 7,
+ 'index': 163,
+ 'doom_type': 35,
+ 'region': "Ramparts of Perdition (E4M7) Main"},
+ 371739: {'name': 'Ramparts of Perdition (E4M7) - Exit',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Ramparts of Perdition (E4M7) Blue"},
+ 371740: {'name': 'Shattered Bridge (E4M8) - Yellow key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 5,
+ 'doom_type': 80,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371741: {'name': 'Shattered Bridge (E4M8) - Dragon Claw',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 58,
+ 'doom_type': 53,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371742: {'name': 'Shattered Bridge (E4M8) - Phoenix Rod',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 79,
+ 'doom_type': 2003,
+ 'region': "Shattered Bridge (E4M8) Boss"},
+ 371743: {'name': 'Shattered Bridge (E4M8) - Ethereal Crossbow',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 80,
+ 'doom_type': 2001,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371744: {'name': 'Shattered Bridge (E4M8) - Gauntlets of the Necromancer',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 81,
+ 'doom_type': 2005,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371745: {'name': 'Shattered Bridge (E4M8) - Hellstaff',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 8,
+ 'index': 82,
+ 'doom_type': 2004,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371746: {'name': 'Shattered Bridge (E4M8) - Bag of Holding',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 8,
+ 'index': 96,
+ 'doom_type': 8,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371747: {'name': 'Shattered Bridge (E4M8) - Morph Ovum',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 8,
+ 'index': 97,
+ 'doom_type': 30,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371748: {'name': 'Shattered Bridge (E4M8) - Silver Shield',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 98,
+ 'doom_type': 85,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371749: {'name': 'Shattered Bridge (E4M8) - Bag of Holding 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 108,
+ 'doom_type': 8,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371750: {'name': 'Shattered Bridge (E4M8) - Mystic Urn',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 109,
+ 'doom_type': 32,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371751: {'name': 'Shattered Bridge (E4M8) - Shadowsphere',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 8,
+ 'index': 110,
+ 'doom_type': 75,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371752: {'name': 'Shattered Bridge (E4M8) - Ring of Invincibility',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 111,
+ 'doom_type': 84,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371753: {'name': 'Shattered Bridge (E4M8) - Chaos Device',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 112,
+ 'doom_type': 36,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371754: {'name': 'Shattered Bridge (E4M8) - Tome of Power',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 8,
+ 'index': 113,
+ 'doom_type': 86,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371755: {'name': 'Shattered Bridge (E4M8) - Torch',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 114,
+ 'doom_type': 33,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371756: {'name': 'Shattered Bridge (E4M8) - Tome of Power 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 115,
+ 'doom_type': 86,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371757: {'name': 'Shattered Bridge (E4M8) - Enchanted Shield',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 118,
+ 'doom_type': 31,
+ 'region': "Shattered Bridge (E4M8) Main"},
+ 371758: {'name': 'Shattered Bridge (E4M8) - Exit',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Shattered Bridge (E4M8) Boss"},
+ 371759: {'name': 'Mausoleum (E4M9) - Yellow key',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 50,
+ 'doom_type': 80,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371760: {'name': 'Mausoleum (E4M9) - Gauntlets of the Necromancer',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 59,
+ 'doom_type': 2005,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371761: {'name': 'Mausoleum (E4M9) - Ethereal Crossbow',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 60,
+ 'doom_type': 2001,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371762: {'name': 'Mausoleum (E4M9) - Dragon Claw',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 61,
+ 'doom_type': 53,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371763: {'name': 'Mausoleum (E4M9) - Hellstaff',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 62,
+ 'doom_type': 2004,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371764: {'name': 'Mausoleum (E4M9) - Phoenix Rod',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 63,
+ 'doom_type': 2003,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371765: {'name': 'Mausoleum (E4M9) - Firemace',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 64,
+ 'doom_type': 2002,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371766: {'name': 'Mausoleum (E4M9) - Firemace 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 65,
+ 'doom_type': 2002,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371767: {'name': 'Mausoleum (E4M9) - Firemace 3',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 66,
+ 'doom_type': 2002,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371768: {'name': 'Mausoleum (E4M9) - Firemace 4',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 67,
+ 'doom_type': 2002,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371769: {'name': 'Mausoleum (E4M9) - Bag of Holding',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 68,
+ 'doom_type': 8,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371770: {'name': 'Mausoleum (E4M9) - Bag of Holding 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 69,
+ 'doom_type': 8,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371771: {'name': 'Mausoleum (E4M9) - Bag of Holding 3',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 70,
+ 'doom_type': 8,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371772: {'name': 'Mausoleum (E4M9) - Bag of Holding 4',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 71,
+ 'doom_type': 8,
+ 'region': "Mausoleum (E4M9) Yellow"},
+ 371773: {'name': 'Mausoleum (E4M9) - Morph Ovum',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 79,
+ 'doom_type': 30,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371774: {'name': 'Mausoleum (E4M9) - Mystic Urn',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 81,
+ 'doom_type': 32,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371775: {'name': 'Mausoleum (E4M9) - Shadowsphere',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 82,
+ 'doom_type': 75,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371776: {'name': 'Mausoleum (E4M9) - Ring of Invincibility',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 83,
+ 'doom_type': 84,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371777: {'name': 'Mausoleum (E4M9) - Silver Shield',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 9,
+ 'index': 84,
+ 'doom_type': 85,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371778: {'name': 'Mausoleum (E4M9) - Silver Shield 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 85,
+ 'doom_type': 85,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371779: {'name': 'Mausoleum (E4M9) - Enchanted Shield',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 86,
+ 'doom_type': 31,
+ 'region': "Mausoleum (E4M9) Yellow"},
+ 371780: {'name': 'Mausoleum (E4M9) - Map Scroll',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 87,
+ 'doom_type': 35,
+ 'region': "Mausoleum (E4M9) Yellow"},
+ 371781: {'name': 'Mausoleum (E4M9) - Chaos Device',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 88,
+ 'doom_type': 36,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371782: {'name': 'Mausoleum (E4M9) - Torch',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 89,
+ 'doom_type': 33,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371783: {'name': 'Mausoleum (E4M9) - Torch 2',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 9,
+ 'index': 90,
+ 'doom_type': 33,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371784: {'name': 'Mausoleum (E4M9) - Tome of Power',
+ 'episode': 4,
+ 'check_sanity': True,
+ 'map': 9,
+ 'index': 91,
+ 'doom_type': 86,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371785: {'name': 'Mausoleum (E4M9) - Tome of Power 2',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 93,
+ 'doom_type': 86,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371786: {'name': 'Mausoleum (E4M9) - Tome of Power 3',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 94,
+ 'doom_type': 86,
+ 'region': "Mausoleum (E4M9) Main"},
+ 371787: {'name': 'Mausoleum (E4M9) - Exit',
+ 'episode': 4,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Mausoleum (E4M9) Yellow"},
+ 371788: {'name': 'Ochre Cliffs (E5M1) - Yellow key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 4,
+ 'doom_type': 80,
+ 'region': "Ochre Cliffs (E5M1) Main"},
+ 371789: {'name': 'Ochre Cliffs (E5M1) - Blue key',
+ 'episode': 5,
+ 'check_sanity': True,
+ 'map': 1,
+ 'index': 7,
+ 'doom_type': 79,
+ 'region': "Ochre Cliffs (E5M1) Green"},
+ 371790: {'name': 'Ochre Cliffs (E5M1) - Green key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 9,
+ 'doom_type': 73,
+ 'region': "Ochre Cliffs (E5M1) Yellow"},
+ 371791: {'name': 'Ochre Cliffs (E5M1) - Gauntlets of the Necromancer',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 92,
+ 'doom_type': 2005,
+ 'region': "Ochre Cliffs (E5M1) Main"},
+ 371792: {'name': 'Ochre Cliffs (E5M1) - Ethereal Crossbow',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 93,
+ 'doom_type': 2001,
+ 'region': "Ochre Cliffs (E5M1) Main"},
+ 371793: {'name': 'Ochre Cliffs (E5M1) - Dragon Claw',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 94,
+ 'doom_type': 53,
+ 'region': "Ochre Cliffs (E5M1) Yellow"},
+ 371794: {'name': 'Ochre Cliffs (E5M1) - Hellstaff',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 95,
+ 'doom_type': 2004,
+ 'region': "Ochre Cliffs (E5M1) Yellow"},
+ 371795: {'name': 'Ochre Cliffs (E5M1) - Phoenix Rod',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 96,
+ 'doom_type': 2003,
+ 'region': "Ochre Cliffs (E5M1) Yellow"},
+ 371796: {'name': 'Ochre Cliffs (E5M1) - Firemace',
+ 'episode': 5,
+ 'check_sanity': True,
+ 'map': 1,
+ 'index': 97,
+ 'doom_type': 2002,
+ 'region': "Ochre Cliffs (E5M1) Yellow"},
+ 371797: {'name': 'Ochre Cliffs (E5M1) - Firemace 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 98,
+ 'doom_type': 2002,
+ 'region': "Ochre Cliffs (E5M1) Yellow"},
+ 371798: {'name': 'Ochre Cliffs (E5M1) - Firemace 3',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 99,
+ 'doom_type': 2002,
+ 'region': "Ochre Cliffs (E5M1) Green"},
+ 371799: {'name': 'Ochre Cliffs (E5M1) - Firemace 4',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 100,
+ 'doom_type': 2002,
+ 'region': "Ochre Cliffs (E5M1) Main"},
+ 371800: {'name': 'Ochre Cliffs (E5M1) - Bag of Holding',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 101,
+ 'doom_type': 8,
+ 'region': "Ochre Cliffs (E5M1) Main"},
+ 371801: {'name': 'Ochre Cliffs (E5M1) - Morph Ovum',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 102,
+ 'doom_type': 30,
+ 'region': "Ochre Cliffs (E5M1) Yellow"},
+ 371802: {'name': 'Ochre Cliffs (E5M1) - Mystic Urn',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 112,
+ 'doom_type': 32,
+ 'region': "Ochre Cliffs (E5M1) Green"},
+ 371803: {'name': 'Ochre Cliffs (E5M1) - Shadowsphere',
+ 'episode': 5,
+ 'check_sanity': True,
+ 'map': 1,
+ 'index': 113,
+ 'doom_type': 75,
+ 'region': "Ochre Cliffs (E5M1) Main"},
+ 371804: {'name': 'Ochre Cliffs (E5M1) - Ring of Invincibility',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 114,
+ 'doom_type': 84,
+ 'region': "Ochre Cliffs (E5M1) Blue"},
+ 371805: {'name': 'Ochre Cliffs (E5M1) - Silver Shield',
+ 'episode': 5,
+ 'check_sanity': True,
+ 'map': 1,
+ 'index': 115,
+ 'doom_type': 85,
+ 'region': "Ochre Cliffs (E5M1) Main"},
+ 371806: {'name': 'Ochre Cliffs (E5M1) - Enchanted Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 116,
+ 'doom_type': 31,
+ 'region': "Ochre Cliffs (E5M1) Blue"},
+ 371807: {'name': 'Ochre Cliffs (E5M1) - Map Scroll',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 117,
+ 'doom_type': 35,
+ 'region': "Ochre Cliffs (E5M1) Yellow"},
+ 371808: {'name': 'Ochre Cliffs (E5M1) - Chaos Device',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 118,
+ 'doom_type': 36,
+ 'region': "Ochre Cliffs (E5M1) Yellow"},
+ 371809: {'name': 'Ochre Cliffs (E5M1) - Torch',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 119,
+ 'doom_type': 33,
+ 'region': "Ochre Cliffs (E5M1) Main"},
+ 371810: {'name': 'Ochre Cliffs (E5M1) - Tome of Power',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 120,
+ 'doom_type': 86,
+ 'region': "Ochre Cliffs (E5M1) Main"},
+ 371811: {'name': 'Ochre Cliffs (E5M1) - Tome of Power 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 121,
+ 'doom_type': 86,
+ 'region': "Ochre Cliffs (E5M1) Yellow"},
+ 371812: {'name': 'Ochre Cliffs (E5M1) - Tome of Power 3',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 122,
+ 'doom_type': 86,
+ 'region': "Ochre Cliffs (E5M1) Yellow"},
+ 371813: {'name': 'Ochre Cliffs (E5M1) - Bag of Holding 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': 129,
+ 'doom_type': 8,
+ 'region': "Ochre Cliffs (E5M1) Yellow"},
+ 371814: {'name': 'Ochre Cliffs (E5M1) - Exit',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 1,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Ochre Cliffs (E5M1) Blue"},
+ 371815: {'name': 'Rapids (E5M2) - Green key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 2,
+ 'doom_type': 73,
+ 'region': "Rapids (E5M2) Yellow"},
+ 371816: {'name': 'Rapids (E5M2) - Yellow key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 3,
+ 'doom_type': 80,
+ 'region': "Rapids (E5M2) Main"},
+ 371817: {'name': 'Rapids (E5M2) - Ethereal Crossbow',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 34,
+ 'doom_type': 2001,
+ 'region': "Rapids (E5M2) Main"},
+ 371818: {'name': 'Rapids (E5M2) - Gauntlets of the Necromancer',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 35,
+ 'doom_type': 2005,
+ 'region': "Rapids (E5M2) Main"},
+ 371819: {'name': 'Rapids (E5M2) - Dragon Claw',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 36,
+ 'doom_type': 53,
+ 'region': "Rapids (E5M2) Yellow"},
+ 371820: {'name': 'Rapids (E5M2) - Hellstaff',
+ 'episode': 5,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 37,
+ 'doom_type': 2004,
+ 'region': "Rapids (E5M2) Yellow"},
+ 371821: {'name': 'Rapids (E5M2) - Phoenix Rod',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 38,
+ 'doom_type': 2003,
+ 'region': "Rapids (E5M2) Green"},
+ 371822: {'name': 'Rapids (E5M2) - Bag of Holding',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 39,
+ 'doom_type': 8,
+ 'region': "Rapids (E5M2) Yellow"},
+ 371823: {'name': 'Rapids (E5M2) - Bag of Holding 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 40,
+ 'doom_type': 8,
+ 'region': "Rapids (E5M2) Yellow"},
+ 371824: {'name': 'Rapids (E5M2) - Bag of Holding 3',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 41,
+ 'doom_type': 8,
+ 'region': "Rapids (E5M2) Yellow"},
+ 371825: {'name': 'Rapids (E5M2) - Morph Ovum',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 42,
+ 'doom_type': 30,
+ 'region': "Rapids (E5M2) Yellow"},
+ 371826: {'name': 'Rapids (E5M2) - Mystic Urn',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 50,
+ 'doom_type': 32,
+ 'region': "Rapids (E5M2) Green"},
+ 371827: {'name': 'Rapids (E5M2) - Shadowsphere',
+ 'episode': 5,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 51,
+ 'doom_type': 75,
+ 'region': "Rapids (E5M2) Yellow"},
+ 371828: {'name': 'Rapids (E5M2) - Ring of Invincibility',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 52,
+ 'doom_type': 84,
+ 'region': "Rapids (E5M2) Green"},
+ 371829: {'name': 'Rapids (E5M2) - Silver Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 53,
+ 'doom_type': 85,
+ 'region': "Rapids (E5M2) Main"},
+ 371830: {'name': 'Rapids (E5M2) - Enchanted Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 54,
+ 'doom_type': 31,
+ 'region': "Rapids (E5M2) Yellow"},
+ 371831: {'name': 'Rapids (E5M2) - Map Scroll',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 55,
+ 'doom_type': 35,
+ 'region': "Rapids (E5M2) Yellow"},
+ 371832: {'name': 'Rapids (E5M2) - Tome of Power',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 56,
+ 'doom_type': 86,
+ 'region': "Rapids (E5M2) Yellow"},
+ 371833: {'name': 'Rapids (E5M2) - Chaos Device',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 57,
+ 'doom_type': 36,
+ 'region': "Rapids (E5M2) Green"},
+ 371834: {'name': 'Rapids (E5M2) - Tome of Power 2',
+ 'episode': 5,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 58,
+ 'doom_type': 86,
+ 'region': "Rapids (E5M2) Yellow"},
+ 371835: {'name': 'Rapids (E5M2) - Torch',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 59,
+ 'doom_type': 33,
+ 'region': "Rapids (E5M2) Main"},
+ 371836: {'name': 'Rapids (E5M2) - Enchanted Shield 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 66,
+ 'doom_type': 31,
+ 'region': "Rapids (E5M2) Main"},
+ 371837: {'name': 'Rapids (E5M2) - Hellstaff 2',
+ 'episode': 5,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 67,
+ 'doom_type': 2004,
+ 'region': "Rapids (E5M2) Main"},
+ 371838: {'name': 'Rapids (E5M2) - Phoenix Rod 2',
+ 'episode': 5,
+ 'check_sanity': True,
+ 'map': 2,
+ 'index': 68,
+ 'doom_type': 2003,
+ 'region': "Rapids (E5M2) Main"},
+ 371839: {'name': 'Rapids (E5M2) - Firemace',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 71,
+ 'doom_type': 2002,
+ 'region': "Rapids (E5M2) Main"},
+ 371840: {'name': 'Rapids (E5M2) - Firemace 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 72,
+ 'doom_type': 2002,
+ 'region': "Rapids (E5M2) Green"},
+ 371841: {'name': 'Rapids (E5M2) - Firemace 3',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 73,
+ 'doom_type': 2002,
+ 'region': "Rapids (E5M2) Green"},
+ 371842: {'name': 'Rapids (E5M2) - Firemace 4',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 74,
+ 'doom_type': 2002,
+ 'region': "Rapids (E5M2) Yellow"},
+ 371843: {'name': 'Rapids (E5M2) - Firemace 5',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': 75,
+ 'doom_type': 2002,
+ 'region': "Rapids (E5M2) Green"},
+ 371844: {'name': 'Rapids (E5M2) - Exit',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 2,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Rapids (E5M2) Green"},
+ 371845: {'name': 'Quay (E5M3) - Green key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 12,
+ 'doom_type': 73,
+ 'region': "Quay (E5M3) Yellow"},
+ 371846: {'name': 'Quay (E5M3) - Blue key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 13,
+ 'doom_type': 79,
+ 'region': "Quay (E5M3) Green"},
+ 371847: {'name': 'Quay (E5M3) - Yellow key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 15,
+ 'doom_type': 80,
+ 'region': "Quay (E5M3) Main"},
+ 371848: {'name': 'Quay (E5M3) - Ethereal Crossbow',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 212,
+ 'doom_type': 2001,
+ 'region': "Quay (E5M3) Main"},
+ 371849: {'name': 'Quay (E5M3) - Gauntlets of the Necromancer',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 213,
+ 'doom_type': 2005,
+ 'region': "Quay (E5M3) Main"},
+ 371850: {'name': 'Quay (E5M3) - Dragon Claw',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 214,
+ 'doom_type': 53,
+ 'region': "Quay (E5M3) Yellow"},
+ 371851: {'name': 'Quay (E5M3) - Hellstaff',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 215,
+ 'doom_type': 2004,
+ 'region': "Quay (E5M3) Green"},
+ 371852: {'name': 'Quay (E5M3) - Phoenix Rod',
+ 'episode': 5,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 216,
+ 'doom_type': 2003,
+ 'region': "Quay (E5M3) Blue"},
+ 371853: {'name': 'Quay (E5M3) - Bag of Holding',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 217,
+ 'doom_type': 8,
+ 'region': "Quay (E5M3) Main"},
+ 371854: {'name': 'Quay (E5M3) - Morph Ovum',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 218,
+ 'doom_type': 30,
+ 'region': "Quay (E5M3) Blue"},
+ 371855: {'name': 'Quay (E5M3) - Mystic Urn',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 229,
+ 'doom_type': 32,
+ 'region': "Quay (E5M3) Green"},
+ 371856: {'name': 'Quay (E5M3) - Enchanted Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 230,
+ 'doom_type': 31,
+ 'region': "Quay (E5M3) Green"},
+ 371857: {'name': 'Quay (E5M3) - Ring of Invincibility',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 231,
+ 'doom_type': 84,
+ 'region': "Quay (E5M3) Main"},
+ 371858: {'name': 'Quay (E5M3) - Shadowsphere',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 232,
+ 'doom_type': 75,
+ 'region': "Quay (E5M3) Main"},
+ 371859: {'name': 'Quay (E5M3) - Silver Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 233,
+ 'doom_type': 85,
+ 'region': "Quay (E5M3) Main"},
+ 371860: {'name': 'Quay (E5M3) - Silver Shield 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 234,
+ 'doom_type': 85,
+ 'region': "Quay (E5M3) Cyan"},
+ 371861: {'name': 'Quay (E5M3) - Map Scroll',
+ 'episode': 5,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 235,
+ 'doom_type': 35,
+ 'region': "Quay (E5M3) Blue"},
+ 371862: {'name': 'Quay (E5M3) - Chaos Device',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 236,
+ 'doom_type': 36,
+ 'region': "Quay (E5M3) Blue"},
+ 371863: {'name': 'Quay (E5M3) - Tome of Power',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 237,
+ 'doom_type': 86,
+ 'region': "Quay (E5M3) Main"},
+ 371864: {'name': 'Quay (E5M3) - Tome of Power 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 238,
+ 'doom_type': 86,
+ 'region': "Quay (E5M3) Green"},
+ 371865: {'name': 'Quay (E5M3) - Tome of Power 3',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 239,
+ 'doom_type': 86,
+ 'region': "Quay (E5M3) Cyan"},
+ 371866: {'name': 'Quay (E5M3) - Torch',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 240,
+ 'doom_type': 33,
+ 'region': "Quay (E5M3) Green"},
+ 371867: {'name': 'Quay (E5M3) - Firemace',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 242,
+ 'doom_type': 2002,
+ 'region': "Quay (E5M3) Cyan"},
+ 371868: {'name': 'Quay (E5M3) - Firemace 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 243,
+ 'doom_type': 2002,
+ 'region': "Quay (E5M3) Main"},
+ 371869: {'name': 'Quay (E5M3) - Firemace 3',
+ 'episode': 5,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 244,
+ 'doom_type': 2002,
+ 'region': "Quay (E5M3) Yellow"},
+ 371870: {'name': 'Quay (E5M3) - Firemace 4',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 245,
+ 'doom_type': 2002,
+ 'region': "Quay (E5M3) Yellow"},
+ 371871: {'name': 'Quay (E5M3) - Firemace 5',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 246,
+ 'doom_type': 2002,
+ 'region': "Quay (E5M3) Green"},
+ 371872: {'name': 'Quay (E5M3) - Firemace 6',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': 247,
+ 'doom_type': 2002,
+ 'region': "Quay (E5M3) Cyan"},
+ 371873: {'name': 'Quay (E5M3) - Bag of Holding 2',
+ 'episode': 5,
+ 'check_sanity': True,
+ 'map': 3,
+ 'index': 252,
+ 'doom_type': 8,
+ 'region': "Quay (E5M3) Yellow"},
+ 371874: {'name': 'Quay (E5M3) - Exit',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 3,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Quay (E5M3) Cyan"},
+ 371875: {'name': 'Courtyard (E5M4) - Blue key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 3,
+ 'doom_type': 79,
+ 'region': "Courtyard (E5M4) Main"},
+ 371876: {'name': 'Courtyard (E5M4) - Yellow key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 16,
+ 'doom_type': 80,
+ 'region': "Courtyard (E5M4) Main"},
+ 371877: {'name': 'Courtyard (E5M4) - Green key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 21,
+ 'doom_type': 73,
+ 'region': "Courtyard (E5M4) Kakis"},
+ 371878: {'name': 'Courtyard (E5M4) - Gauntlets of the Necromancer',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 84,
+ 'doom_type': 2005,
+ 'region': "Courtyard (E5M4) Main"},
+ 371879: {'name': 'Courtyard (E5M4) - Ethereal Crossbow',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 85,
+ 'doom_type': 2001,
+ 'region': "Courtyard (E5M4) Main"},
+ 371880: {'name': 'Courtyard (E5M4) - Dragon Claw',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 86,
+ 'doom_type': 53,
+ 'region': "Courtyard (E5M4) Main"},
+ 371881: {'name': 'Courtyard (E5M4) - Hellstaff',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 87,
+ 'doom_type': 2004,
+ 'region': "Courtyard (E5M4) Kakis"},
+ 371882: {'name': 'Courtyard (E5M4) - Phoenix Rod',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 88,
+ 'doom_type': 2003,
+ 'region': "Courtyard (E5M4) Main"},
+ 371883: {'name': 'Courtyard (E5M4) - Morph Ovum',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 89,
+ 'doom_type': 30,
+ 'region': "Courtyard (E5M4) Main"},
+ 371884: {'name': 'Courtyard (E5M4) - Bag of Holding',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 90,
+ 'doom_type': 8,
+ 'region': "Courtyard (E5M4) Main"},
+ 371885: {'name': 'Courtyard (E5M4) - Silver Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 91,
+ 'doom_type': 85,
+ 'region': "Courtyard (E5M4) Main"},
+ 371886: {'name': 'Courtyard (E5M4) - Mystic Urn',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 103,
+ 'doom_type': 32,
+ 'region': "Courtyard (E5M4) Main"},
+ 371887: {'name': 'Courtyard (E5M4) - Ring of Invincibility',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 104,
+ 'doom_type': 84,
+ 'region': "Courtyard (E5M4) Kakis"},
+ 371888: {'name': 'Courtyard (E5M4) - Shadowsphere',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 105,
+ 'doom_type': 75,
+ 'region': "Courtyard (E5M4) Main"},
+ 371889: {'name': 'Courtyard (E5M4) - Enchanted Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 106,
+ 'doom_type': 31,
+ 'region': "Courtyard (E5M4) Blue"},
+ 371890: {'name': 'Courtyard (E5M4) - Map Scroll',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 107,
+ 'doom_type': 35,
+ 'region': "Courtyard (E5M4) Kakis"},
+ 371891: {'name': 'Courtyard (E5M4) - Chaos Device',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 108,
+ 'doom_type': 36,
+ 'region': "Courtyard (E5M4) Main"},
+ 371892: {'name': 'Courtyard (E5M4) - Tome of Power',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 109,
+ 'doom_type': 86,
+ 'region': "Courtyard (E5M4) Main"},
+ 371893: {'name': 'Courtyard (E5M4) - Tome of Power 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 110,
+ 'doom_type': 86,
+ 'region': "Courtyard (E5M4) Blue"},
+ 371894: {'name': 'Courtyard (E5M4) - Tome of Power 3',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 111,
+ 'doom_type': 86,
+ 'region': "Courtyard (E5M4) Kakis"},
+ 371895: {'name': 'Courtyard (E5M4) - Torch',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 112,
+ 'doom_type': 33,
+ 'region': "Courtyard (E5M4) Main"},
+ 371896: {'name': 'Courtyard (E5M4) - Bag of Holding 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 213,
+ 'doom_type': 8,
+ 'region': "Courtyard (E5M4) Blue"},
+ 371897: {'name': 'Courtyard (E5M4) - Silver Shield 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 219,
+ 'doom_type': 85,
+ 'region': "Courtyard (E5M4) Kakis"},
+ 371898: {'name': 'Courtyard (E5M4) - Bag of Holding 3',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': 272,
+ 'doom_type': 8,
+ 'region': "Courtyard (E5M4) Main"},
+ 371899: {'name': 'Courtyard (E5M4) - Exit',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 4,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Courtyard (E5M4) Blue"},
+ 371900: {'name': 'Hydratyr (E5M5) - Yellow key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 3,
+ 'doom_type': 80,
+ 'region': "Hydratyr (E5M5) Main"},
+ 371901: {'name': 'Hydratyr (E5M5) - Green key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 5,
+ 'doom_type': 73,
+ 'region': "Hydratyr (E5M5) Yellow"},
+ 371902: {'name': 'Hydratyr (E5M5) - Blue key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 11,
+ 'doom_type': 79,
+ 'region': "Hydratyr (E5M5) Green"},
+ 371903: {'name': 'Hydratyr (E5M5) - Ethereal Crossbow',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 238,
+ 'doom_type': 2001,
+ 'region': "Hydratyr (E5M5) Main"},
+ 371904: {'name': 'Hydratyr (E5M5) - Gauntlets of the Necromancer',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 239,
+ 'doom_type': 2005,
+ 'region': "Hydratyr (E5M5) Yellow"},
+ 371905: {'name': 'Hydratyr (E5M5) - Hellstaff',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 240,
+ 'doom_type': 2004,
+ 'region': "Hydratyr (E5M5) Yellow"},
+ 371906: {'name': 'Hydratyr (E5M5) - Dragon Claw',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 241,
+ 'doom_type': 53,
+ 'region': "Hydratyr (E5M5) Yellow"},
+ 371907: {'name': 'Hydratyr (E5M5) - Phoenix Rod',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 242,
+ 'doom_type': 2003,
+ 'region': "Hydratyr (E5M5) Green"},
+ 371908: {'name': 'Hydratyr (E5M5) - Firemace',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 243,
+ 'doom_type': 2002,
+ 'region': "Hydratyr (E5M5) Green"},
+ 371909: {'name': 'Hydratyr (E5M5) - Firemace 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 244,
+ 'doom_type': 2002,
+ 'region': "Hydratyr (E5M5) Green"},
+ 371910: {'name': 'Hydratyr (E5M5) - Firemace 3',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 245,
+ 'doom_type': 2002,
+ 'region': "Hydratyr (E5M5) Green"},
+ 371911: {'name': 'Hydratyr (E5M5) - Firemace 4',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 246,
+ 'doom_type': 2002,
+ 'region': "Hydratyr (E5M5) Green"},
+ 371912: {'name': 'Hydratyr (E5M5) - Bag of Holding',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 248,
+ 'doom_type': 8,
+ 'region': "Hydratyr (E5M5) Main"},
+ 371913: {'name': 'Hydratyr (E5M5) - Morph Ovum',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 259,
+ 'doom_type': 30,
+ 'region': "Hydratyr (E5M5) Green"},
+ 371914: {'name': 'Hydratyr (E5M5) - Bag of Holding 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 260,
+ 'doom_type': 8,
+ 'region': "Hydratyr (E5M5) Green"},
+ 371915: {'name': 'Hydratyr (E5M5) - Mystic Urn',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 261,
+ 'doom_type': 32,
+ 'region': "Hydratyr (E5M5) Blue"},
+ 371916: {'name': 'Hydratyr (E5M5) - Tome of Power',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 262,
+ 'doom_type': 86,
+ 'region': "Hydratyr (E5M5) Main"},
+ 371917: {'name': 'Hydratyr (E5M5) - Shadowsphere',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 263,
+ 'doom_type': 75,
+ 'region': "Hydratyr (E5M5) Main"},
+ 371918: {'name': 'Hydratyr (E5M5) - Ring of Invincibility',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 264,
+ 'doom_type': 84,
+ 'region': "Hydratyr (E5M5) Yellow"},
+ 371919: {'name': 'Hydratyr (E5M5) - Chaos Device',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 265,
+ 'doom_type': 36,
+ 'region': "Hydratyr (E5M5) Yellow"},
+ 371920: {'name': 'Hydratyr (E5M5) - Map Scroll',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 266,
+ 'doom_type': 35,
+ 'region': "Hydratyr (E5M5) Yellow"},
+ 371921: {'name': 'Hydratyr (E5M5) - Enchanted Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 267,
+ 'doom_type': 31,
+ 'region': "Hydratyr (E5M5) Green"},
+ 371922: {'name': 'Hydratyr (E5M5) - Torch',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 268,
+ 'doom_type': 33,
+ 'region': "Hydratyr (E5M5) Main"},
+ 371923: {'name': 'Hydratyr (E5M5) - Tome of Power 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 269,
+ 'doom_type': 86,
+ 'region': "Hydratyr (E5M5) Blue"},
+ 371924: {'name': 'Hydratyr (E5M5) - Silver Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 270,
+ 'doom_type': 85,
+ 'region': "Hydratyr (E5M5) Blue"},
+ 371925: {'name': 'Hydratyr (E5M5) - Silver Shield 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 271,
+ 'doom_type': 85,
+ 'region': "Hydratyr (E5M5) Main"},
+ 371926: {'name': 'Hydratyr (E5M5) - Tome of Power 3',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': 272,
+ 'doom_type': 86,
+ 'region': "Hydratyr (E5M5) Yellow"},
+ 371927: {'name': 'Hydratyr (E5M5) - Exit',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 5,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Hydratyr (E5M5) Blue"},
+ 371928: {'name': 'Colonnade (E5M6) - Yellow key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 8,
+ 'doom_type': 80,
+ 'region': "Colonnade (E5M6) Main"},
+ 371929: {'name': 'Colonnade (E5M6) - Green key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 9,
+ 'doom_type': 73,
+ 'region': "Colonnade (E5M6) Yellow"},
+ 371930: {'name': 'Colonnade (E5M6) - Blue key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 10,
+ 'doom_type': 79,
+ 'region': "Colonnade (E5M6) Green"},
+ 371931: {'name': 'Colonnade (E5M6) - Dragon Claw',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 91,
+ 'doom_type': 53,
+ 'region': "Colonnade (E5M6) Main"},
+ 371932: {'name': 'Colonnade (E5M6) - Hellstaff',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 92,
+ 'doom_type': 2004,
+ 'region': "Colonnade (E5M6) Yellow"},
+ 371933: {'name': 'Colonnade (E5M6) - Phoenix Rod',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 93,
+ 'doom_type': 2003,
+ 'region': "Colonnade (E5M6) Green"},
+ 371934: {'name': 'Colonnade (E5M6) - Gauntlets of the Necromancer',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 94,
+ 'doom_type': 2005,
+ 'region': "Colonnade (E5M6) Yellow"},
+ 371935: {'name': 'Colonnade (E5M6) - Firemace',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 95,
+ 'doom_type': 2002,
+ 'region': "Colonnade (E5M6) Yellow"},
+ 371936: {'name': 'Colonnade (E5M6) - Firemace 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 96,
+ 'doom_type': 2002,
+ 'region': "Colonnade (E5M6) Yellow"},
+ 371937: {'name': 'Colonnade (E5M6) - Firemace 3',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 97,
+ 'doom_type': 2002,
+ 'region': "Colonnade (E5M6) Yellow"},
+ 371938: {'name': 'Colonnade (E5M6) - Firemace 4',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 98,
+ 'doom_type': 2002,
+ 'region': "Colonnade (E5M6) Main"},
+ 371939: {'name': 'Colonnade (E5M6) - Firemace 5',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 99,
+ 'doom_type': 2002,
+ 'region': "Colonnade (E5M6) Main"},
+ 371940: {'name': 'Colonnade (E5M6) - Enchanted Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 100,
+ 'doom_type': 31,
+ 'region': "Colonnade (E5M6) Yellow"},
+ 371941: {'name': 'Colonnade (E5M6) - Tome of Power',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 101,
+ 'doom_type': 86,
+ 'region': "Colonnade (E5M6) Yellow"},
+ 371942: {'name': 'Colonnade (E5M6) - Silver Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 102,
+ 'doom_type': 85,
+ 'region': "Colonnade (E5M6) Main"},
+ 371943: {'name': 'Colonnade (E5M6) - Morph Ovum',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 103,
+ 'doom_type': 30,
+ 'region': "Colonnade (E5M6) Main"},
+ 371944: {'name': 'Colonnade (E5M6) - Chaos Device',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 104,
+ 'doom_type': 36,
+ 'region': "Colonnade (E5M6) Main"},
+ 371945: {'name': 'Colonnade (E5M6) - Bag of Holding',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 105,
+ 'doom_type': 8,
+ 'region': "Colonnade (E5M6) Main"},
+ 371946: {'name': 'Colonnade (E5M6) - Bag of Holding 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 106,
+ 'doom_type': 8,
+ 'region': "Colonnade (E5M6) Green"},
+ 371947: {'name': 'Colonnade (E5M6) - Mystic Urn',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 121,
+ 'doom_type': 32,
+ 'region': "Colonnade (E5M6) Yellow"},
+ 371948: {'name': 'Colonnade (E5M6) - Shadowsphere',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 122,
+ 'doom_type': 75,
+ 'region': "Colonnade (E5M6) Yellow"},
+ 371949: {'name': 'Colonnade (E5M6) - Ring of Invincibility',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 123,
+ 'doom_type': 84,
+ 'region': "Colonnade (E5M6) Main"},
+ 371950: {'name': 'Colonnade (E5M6) - Torch',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 124,
+ 'doom_type': 33,
+ 'region': "Colonnade (E5M6) Yellow"},
+ 371951: {'name': 'Colonnade (E5M6) - Map Scroll',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 125,
+ 'doom_type': 35,
+ 'region': "Colonnade (E5M6) Yellow"},
+ 371952: {'name': 'Colonnade (E5M6) - Tome of Power 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 126,
+ 'doom_type': 86,
+ 'region': "Colonnade (E5M6) Yellow"},
+ 371953: {'name': 'Colonnade (E5M6) - Mystic Urn 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 127,
+ 'doom_type': 32,
+ 'region': "Colonnade (E5M6) Blue"},
+ 371954: {'name': 'Colonnade (E5M6) - Ring of Invincibility 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 128,
+ 'doom_type': 84,
+ 'region': "Colonnade (E5M6) Blue"},
+ 371955: {'name': 'Colonnade (E5M6) - Ethereal Crossbow',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': 348,
+ 'doom_type': 2001,
+ 'region': "Colonnade (E5M6) Main"},
+ 371956: {'name': 'Colonnade (E5M6) - Exit',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 6,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Colonnade (E5M6) Blue"},
+ 371957: {'name': 'Foetid Manse (E5M7) - Enchanted Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 7,
+ 'doom_type': 31,
+ 'region': "Foetid Manse (E5M7) Blue"},
+ 371958: {'name': 'Foetid Manse (E5M7) - Mystic Urn',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 8,
+ 'doom_type': 32,
+ 'region': "Foetid Manse (E5M7) Yellow"},
+ 371959: {'name': 'Foetid Manse (E5M7) - Morph Ovum',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 9,
+ 'doom_type': 30,
+ 'region': "Foetid Manse (E5M7) Green"},
+ 371960: {'name': 'Foetid Manse (E5M7) - Green key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 12,
+ 'doom_type': 73,
+ 'region': "Foetid Manse (E5M7) Yellow"},
+ 371961: {'name': 'Foetid Manse (E5M7) - Yellow key',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 15,
+ 'doom_type': 80,
+ 'region': "Foetid Manse (E5M7) Main"},
+ 371962: {'name': 'Foetid Manse (E5M7) - Ethereal Crossbow',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 218,
+ 'doom_type': 2001,
+ 'region': "Foetid Manse (E5M7) Main"},
+ 371963: {'name': 'Foetid Manse (E5M7) - Gauntlets of the Necromancer',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 219,
+ 'doom_type': 2005,
+ 'region': "Foetid Manse (E5M7) Main"},
+ 371964: {'name': 'Foetid Manse (E5M7) - Dragon Claw',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 220,
+ 'doom_type': 53,
+ 'region': "Foetid Manse (E5M7) Yellow"},
+ 371965: {'name': 'Foetid Manse (E5M7) - Hellstaff',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 221,
+ 'doom_type': 2004,
+ 'region': "Foetid Manse (E5M7) Green"},
+ 371966: {'name': 'Foetid Manse (E5M7) - Phoenix Rod',
+ 'episode': 5,
+ 'check_sanity': True,
+ 'map': 7,
+ 'index': 222,
+ 'doom_type': 2003,
+ 'region': "Foetid Manse (E5M7) Green"},
+ 371967: {'name': 'Foetid Manse (E5M7) - Shadowsphere',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 223,
+ 'doom_type': 75,
+ 'region': "Foetid Manse (E5M7) Yellow"},
+ 371968: {'name': 'Foetid Manse (E5M7) - Ring of Invincibility',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 224,
+ 'doom_type': 84,
+ 'region': "Foetid Manse (E5M7) Yellow"},
+ 371969: {'name': 'Foetid Manse (E5M7) - Silver Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 225,
+ 'doom_type': 85,
+ 'region': "Foetid Manse (E5M7) Green"},
+ 371970: {'name': 'Foetid Manse (E5M7) - Map Scroll',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 234,
+ 'doom_type': 35,
+ 'region': "Foetid Manse (E5M7) Green"},
+ 371971: {'name': 'Foetid Manse (E5M7) - Tome of Power',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 235,
+ 'doom_type': 86,
+ 'region': "Foetid Manse (E5M7) Yellow"},
+ 371972: {'name': 'Foetid Manse (E5M7) - Tome of Power 2',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 236,
+ 'doom_type': 86,
+ 'region': "Foetid Manse (E5M7) Green"},
+ 371973: {'name': 'Foetid Manse (E5M7) - Tome of Power 3',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 237,
+ 'doom_type': 86,
+ 'region': "Foetid Manse (E5M7) Green"},
+ 371974: {'name': 'Foetid Manse (E5M7) - Torch',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 238,
+ 'doom_type': 33,
+ 'region': "Foetid Manse (E5M7) Yellow"},
+ 371975: {'name': 'Foetid Manse (E5M7) - Chaos Device',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 239,
+ 'doom_type': 36,
+ 'region': "Foetid Manse (E5M7) Green"},
+ 371976: {'name': 'Foetid Manse (E5M7) - Bag of Holding',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': 240,
+ 'doom_type': 8,
+ 'region': "Foetid Manse (E5M7) Green"},
+ 371977: {'name': 'Foetid Manse (E5M7) - Exit',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 7,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Foetid Manse (E5M7) Blue"},
+ 371978: {'name': 'Field of Judgement (E5M8) - Hellstaff',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 18,
+ 'doom_type': 2004,
+ 'region': "Field of Judgement (E5M8) Main"},
+ 371979: {'name': 'Field of Judgement (E5M8) - Phoenix Rod',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 19,
+ 'doom_type': 2003,
+ 'region': "Field of Judgement (E5M8) Main"},
+ 371980: {'name': 'Field of Judgement (E5M8) - Ethereal Crossbow',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 20,
+ 'doom_type': 2001,
+ 'region': "Field of Judgement (E5M8) Main"},
+ 371981: {'name': 'Field of Judgement (E5M8) - Dragon Claw',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 21,
+ 'doom_type': 53,
+ 'region': "Field of Judgement (E5M8) Main"},
+ 371982: {'name': 'Field of Judgement (E5M8) - Gauntlets of the Necromancer',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 22,
+ 'doom_type': 2005,
+ 'region': "Field of Judgement (E5M8) Main"},
+ 371983: {'name': 'Field of Judgement (E5M8) - Mystic Urn',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 23,
+ 'doom_type': 32,
+ 'region': "Field of Judgement (E5M8) Main"},
+ 371984: {'name': 'Field of Judgement (E5M8) - Shadowsphere',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 24,
+ 'doom_type': 75,
+ 'region': "Field of Judgement (E5M8) Main"},
+ 371985: {'name': 'Field of Judgement (E5M8) - Enchanted Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 25,
+ 'doom_type': 31,
+ 'region': "Field of Judgement (E5M8) Main"},
+ 371986: {'name': 'Field of Judgement (E5M8) - Ring of Invincibility',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 26,
+ 'doom_type': 84,
+ 'region': "Field of Judgement (E5M8) Main"},
+ 371987: {'name': 'Field of Judgement (E5M8) - Tome of Power',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 27,
+ 'doom_type': 86,
+ 'region': "Field of Judgement (E5M8) Main"},
+ 371988: {'name': 'Field of Judgement (E5M8) - Chaos Device',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 28,
+ 'doom_type': 36,
+ 'region': "Field of Judgement (E5M8) Main"},
+ 371989: {'name': 'Field of Judgement (E5M8) - Silver Shield',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 29,
+ 'doom_type': 85,
+ 'region': "Field of Judgement (E5M8) Main"},
+ 371990: {'name': 'Field of Judgement (E5M8) - Bag of Holding',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': 62,
+ 'doom_type': 8,
+ 'region': "Field of Judgement (E5M8) Main"},
+ 371991: {'name': 'Field of Judgement (E5M8) - Exit',
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 8,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Field of Judgement (E5M8) Main"},
+ 371992: {'name': "Skein of D'Sparil (E5M9) - Blue key",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 0,
+ 'doom_type': 79,
+ 'region': "Skein of D'Sparil (E5M9) Green"},
+ 371993: {'name': "Skein of D'Sparil (E5M9) - Green key",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 1,
+ 'doom_type': 73,
+ 'region': "Skein of D'Sparil (E5M9) Yellow"},
+ 371994: {'name': "Skein of D'Sparil (E5M9) - Yellow key",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 13,
+ 'doom_type': 80,
+ 'region': "Skein of D'Sparil (E5M9) Main"},
+ 371995: {'name': "Skein of D'Sparil (E5M9) - Ethereal Crossbow",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 21,
+ 'doom_type': 2001,
+ 'region': "Skein of D'Sparil (E5M9) Main"},
+ 371996: {'name': "Skein of D'Sparil (E5M9) - Dragon Claw",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 44,
+ 'doom_type': 53,
+ 'region': "Skein of D'Sparil (E5M9) Main"},
+ 371997: {'name': "Skein of D'Sparil (E5M9) - Gauntlets of the Necromancer",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 45,
+ 'doom_type': 2005,
+ 'region': "Skein of D'Sparil (E5M9) Main"},
+ 371998: {'name': "Skein of D'Sparil (E5M9) - Hellstaff",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 46,
+ 'doom_type': 2004,
+ 'region': "Skein of D'Sparil (E5M9) Yellow"},
+ 371999: {'name': "Skein of D'Sparil (E5M9) - Phoenix Rod",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 47,
+ 'doom_type': 2003,
+ 'region': "Skein of D'Sparil (E5M9) Blue"},
+ 372000: {'name': "Skein of D'Sparil (E5M9) - Bag of Holding",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 48,
+ 'doom_type': 8,
+ 'region': "Skein of D'Sparil (E5M9) Main"},
+ 372001: {'name': "Skein of D'Sparil (E5M9) - Silver Shield",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 51,
+ 'doom_type': 85,
+ 'region': "Skein of D'Sparil (E5M9) Main"},
+ 372002: {'name': "Skein of D'Sparil (E5M9) - Tome of Power",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 52,
+ 'doom_type': 86,
+ 'region': "Skein of D'Sparil (E5M9) Green"},
+ 372003: {'name': "Skein of D'Sparil (E5M9) - Torch",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 53,
+ 'doom_type': 33,
+ 'region': "Skein of D'Sparil (E5M9) Main"},
+ 372004: {'name': "Skein of D'Sparil (E5M9) - Morph Ovum",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 54,
+ 'doom_type': 30,
+ 'region': "Skein of D'Sparil (E5M9) Main"},
+ 372005: {'name': "Skein of D'Sparil (E5M9) - Shadowsphere",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 64,
+ 'doom_type': 75,
+ 'region': "Skein of D'Sparil (E5M9) Yellow"},
+ 372006: {'name': "Skein of D'Sparil (E5M9) - Chaos Device",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 65,
+ 'doom_type': 36,
+ 'region': "Skein of D'Sparil (E5M9) Main"},
+ 372007: {'name': "Skein of D'Sparil (E5M9) - Ring of Invincibility",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 66,
+ 'doom_type': 84,
+ 'region': "Skein of D'Sparil (E5M9) Main"},
+ 372008: {'name': "Skein of D'Sparil (E5M9) - Enchanted Shield",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 67,
+ 'doom_type': 31,
+ 'region': "Skein of D'Sparil (E5M9) Blue"},
+ 372009: {'name': "Skein of D'Sparil (E5M9) - Mystic Urn",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 68,
+ 'doom_type': 32,
+ 'region': "Skein of D'Sparil (E5M9) Blue"},
+ 372010: {'name': "Skein of D'Sparil (E5M9) - Tome of Power 2",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 69,
+ 'doom_type': 86,
+ 'region': "Skein of D'Sparil (E5M9) Green"},
+ 372011: {'name': "Skein of D'Sparil (E5M9) - Map Scroll",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 70,
+ 'doom_type': 35,
+ 'region': "Skein of D'Sparil (E5M9) Green"},
+ 372012: {'name': "Skein of D'Sparil (E5M9) - Bag of Holding 2",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': 243,
+ 'doom_type': 8,
+ 'region': "Skein of D'Sparil (E5M9) Blue"},
+ 372013: {'name': "Skein of D'Sparil (E5M9) - Exit",
+ 'episode': 5,
+ 'check_sanity': False,
+ 'map': 9,
+ 'index': -1,
+ 'doom_type': -1,
+ 'region': "Skein of D'Sparil (E5M9) Blue"},
+}
+
+
+location_name_groups: Dict[str, Set[str]] = {
+ 'Ambulatory (E4M3)': {
+ 'Ambulatory (E4M3) - Bag of Holding',
+ 'Ambulatory (E4M3) - Bag of Holding 2',
+ 'Ambulatory (E4M3) - Blue key',
+ 'Ambulatory (E4M3) - Chaos Device',
+ 'Ambulatory (E4M3) - Dragon Claw',
+ 'Ambulatory (E4M3) - Enchanted Shield',
+ 'Ambulatory (E4M3) - Ethereal Crossbow',
+ 'Ambulatory (E4M3) - Exit',
+ 'Ambulatory (E4M3) - Firemace',
+ 'Ambulatory (E4M3) - Firemace 2',
+ 'Ambulatory (E4M3) - Firemace 3',
+ 'Ambulatory (E4M3) - Firemace 4',
+ 'Ambulatory (E4M3) - Firemace 5',
+ 'Ambulatory (E4M3) - Gauntlets of the Necromancer',
+ 'Ambulatory (E4M3) - Green key',
+ 'Ambulatory (E4M3) - Hellstaff',
+ 'Ambulatory (E4M3) - Map Scroll',
+ 'Ambulatory (E4M3) - Morph Ovum',
+ 'Ambulatory (E4M3) - Morph Ovum 2',
+ 'Ambulatory (E4M3) - Mystic Urn',
+ 'Ambulatory (E4M3) - Phoenix Rod',
+ 'Ambulatory (E4M3) - Ring of Invincibility',
+ 'Ambulatory (E4M3) - Ring of Invincibility 2',
+ 'Ambulatory (E4M3) - Shadowsphere',
+ 'Ambulatory (E4M3) - Silver Shield',
+ 'Ambulatory (E4M3) - Tome of Power',
+ 'Ambulatory (E4M3) - Tome of Power 2',
+ 'Ambulatory (E4M3) - Torch',
+ 'Ambulatory (E4M3) - Yellow key',
+ },
+ 'Blockhouse (E4M2)': {
+ 'Blockhouse (E4M2) - Bag of Holding',
+ 'Blockhouse (E4M2) - Bag of Holding 2',
+ 'Blockhouse (E4M2) - Blue key',
+ 'Blockhouse (E4M2) - Chaos Device',
+ 'Blockhouse (E4M2) - Dragon Claw',
+ 'Blockhouse (E4M2) - Enchanted Shield',
+ 'Blockhouse (E4M2) - Ethereal Crossbow',
+ 'Blockhouse (E4M2) - Exit',
+ 'Blockhouse (E4M2) - Gauntlets of the Necromancer',
+ 'Blockhouse (E4M2) - Green key',
+ 'Blockhouse (E4M2) - Hellstaff',
+ 'Blockhouse (E4M2) - Morph Ovum',
+ 'Blockhouse (E4M2) - Mystic Urn',
+ 'Blockhouse (E4M2) - Phoenix Rod',
+ 'Blockhouse (E4M2) - Ring of Invincibility',
+ 'Blockhouse (E4M2) - Ring of Invincibility 2',
+ 'Blockhouse (E4M2) - Shadowsphere',
+ 'Blockhouse (E4M2) - Shadowsphere 2',
+ 'Blockhouse (E4M2) - Silver Shield',
+ 'Blockhouse (E4M2) - Tome of Power',
+ 'Blockhouse (E4M2) - Yellow key',
+ },
+ 'Catafalque (E4M1)': {
+ 'Catafalque (E4M1) - Bag of Holding',
+ 'Catafalque (E4M1) - Chaos Device',
+ 'Catafalque (E4M1) - Dragon Claw',
+ 'Catafalque (E4M1) - Ethereal Crossbow',
+ 'Catafalque (E4M1) - Exit',
+ 'Catafalque (E4M1) - Gauntlets of the Necromancer',
+ 'Catafalque (E4M1) - Green key',
+ 'Catafalque (E4M1) - Hellstaff',
+ 'Catafalque (E4M1) - Map Scroll',
+ 'Catafalque (E4M1) - Morph Ovum',
+ 'Catafalque (E4M1) - Ring of Invincibility',
+ 'Catafalque (E4M1) - Shadowsphere',
+ 'Catafalque (E4M1) - Silver Shield',
+ 'Catafalque (E4M1) - Tome of Power',
+ 'Catafalque (E4M1) - Tome of Power 2',
+ 'Catafalque (E4M1) - Torch',
+ 'Catafalque (E4M1) - Yellow key',
+ },
+ 'Colonnade (E5M6)': {
+ 'Colonnade (E5M6) - Bag of Holding',
+ 'Colonnade (E5M6) - Bag of Holding 2',
+ 'Colonnade (E5M6) - Blue key',
+ 'Colonnade (E5M6) - Chaos Device',
+ 'Colonnade (E5M6) - Dragon Claw',
+ 'Colonnade (E5M6) - Enchanted Shield',
+ 'Colonnade (E5M6) - Ethereal Crossbow',
+ 'Colonnade (E5M6) - Exit',
+ 'Colonnade (E5M6) - Firemace',
+ 'Colonnade (E5M6) - Firemace 2',
+ 'Colonnade (E5M6) - Firemace 3',
+ 'Colonnade (E5M6) - Firemace 4',
+ 'Colonnade (E5M6) - Firemace 5',
+ 'Colonnade (E5M6) - Gauntlets of the Necromancer',
+ 'Colonnade (E5M6) - Green key',
+ 'Colonnade (E5M6) - Hellstaff',
+ 'Colonnade (E5M6) - Map Scroll',
+ 'Colonnade (E5M6) - Morph Ovum',
+ 'Colonnade (E5M6) - Mystic Urn',
+ 'Colonnade (E5M6) - Mystic Urn 2',
+ 'Colonnade (E5M6) - Phoenix Rod',
+ 'Colonnade (E5M6) - Ring of Invincibility',
+ 'Colonnade (E5M6) - Ring of Invincibility 2',
+ 'Colonnade (E5M6) - Shadowsphere',
+ 'Colonnade (E5M6) - Silver Shield',
+ 'Colonnade (E5M6) - Tome of Power',
+ 'Colonnade (E5M6) - Tome of Power 2',
+ 'Colonnade (E5M6) - Torch',
+ 'Colonnade (E5M6) - Yellow key',
+ },
+ 'Courtyard (E5M4)': {
+ 'Courtyard (E5M4) - Bag of Holding',
+ 'Courtyard (E5M4) - Bag of Holding 2',
+ 'Courtyard (E5M4) - Bag of Holding 3',
+ 'Courtyard (E5M4) - Blue key',
+ 'Courtyard (E5M4) - Chaos Device',
+ 'Courtyard (E5M4) - Dragon Claw',
+ 'Courtyard (E5M4) - Enchanted Shield',
+ 'Courtyard (E5M4) - Ethereal Crossbow',
+ 'Courtyard (E5M4) - Exit',
+ 'Courtyard (E5M4) - Gauntlets of the Necromancer',
+ 'Courtyard (E5M4) - Green key',
+ 'Courtyard (E5M4) - Hellstaff',
+ 'Courtyard (E5M4) - Map Scroll',
+ 'Courtyard (E5M4) - Morph Ovum',
+ 'Courtyard (E5M4) - Mystic Urn',
+ 'Courtyard (E5M4) - Phoenix Rod',
+ 'Courtyard (E5M4) - Ring of Invincibility',
+ 'Courtyard (E5M4) - Shadowsphere',
+ 'Courtyard (E5M4) - Silver Shield',
+ 'Courtyard (E5M4) - Silver Shield 2',
+ 'Courtyard (E5M4) - Tome of Power',
+ 'Courtyard (E5M4) - Tome of Power 2',
+ 'Courtyard (E5M4) - Tome of Power 3',
+ 'Courtyard (E5M4) - Torch',
+ 'Courtyard (E5M4) - Yellow key',
+ },
+ "D'Sparil'S Keep (E3M8)": {
+ "D'Sparil'S Keep (E3M8) - Bag of Holding",
+ "D'Sparil'S Keep (E3M8) - Chaos Device",
+ "D'Sparil'S Keep (E3M8) - Dragon Claw",
+ "D'Sparil'S Keep (E3M8) - Enchanted Shield",
+ "D'Sparil'S Keep (E3M8) - Ethereal Crossbow",
+ "D'Sparil'S Keep (E3M8) - Exit",
+ "D'Sparil'S Keep (E3M8) - Gauntlets of the Necromancer",
+ "D'Sparil'S Keep (E3M8) - Hellstaff",
+ "D'Sparil'S Keep (E3M8) - Mystic Urn",
+ "D'Sparil'S Keep (E3M8) - Phoenix Rod",
+ "D'Sparil'S Keep (E3M8) - Ring of Invincibility",
+ "D'Sparil'S Keep (E3M8) - Shadowsphere",
+ "D'Sparil'S Keep (E3M8) - Silver Shield",
+ "D'Sparil'S Keep (E3M8) - Tome of Power",
+ "D'Sparil'S Keep (E3M8) - Tome of Power 2",
+ "D'Sparil'S Keep (E3M8) - Tome of Power 3",
+ },
+ 'Field of Judgement (E5M8)': {
+ 'Field of Judgement (E5M8) - Bag of Holding',
+ 'Field of Judgement (E5M8) - Chaos Device',
+ 'Field of Judgement (E5M8) - Dragon Claw',
+ 'Field of Judgement (E5M8) - Enchanted Shield',
+ 'Field of Judgement (E5M8) - Ethereal Crossbow',
+ 'Field of Judgement (E5M8) - Exit',
+ 'Field of Judgement (E5M8) - Gauntlets of the Necromancer',
+ 'Field of Judgement (E5M8) - Hellstaff',
+ 'Field of Judgement (E5M8) - Mystic Urn',
+ 'Field of Judgement (E5M8) - Phoenix Rod',
+ 'Field of Judgement (E5M8) - Ring of Invincibility',
+ 'Field of Judgement (E5M8) - Shadowsphere',
+ 'Field of Judgement (E5M8) - Silver Shield',
+ 'Field of Judgement (E5M8) - Tome of Power',
+ },
+ 'Foetid Manse (E5M7)': {
+ 'Foetid Manse (E5M7) - Bag of Holding',
+ 'Foetid Manse (E5M7) - Chaos Device',
+ 'Foetid Manse (E5M7) - Dragon Claw',
+ 'Foetid Manse (E5M7) - Enchanted Shield',
+ 'Foetid Manse (E5M7) - Ethereal Crossbow',
+ 'Foetid Manse (E5M7) - Exit',
+ 'Foetid Manse (E5M7) - Gauntlets of the Necromancer',
+ 'Foetid Manse (E5M7) - Green key',
+ 'Foetid Manse (E5M7) - Hellstaff',
+ 'Foetid Manse (E5M7) - Map Scroll',
+ 'Foetid Manse (E5M7) - Morph Ovum',
+ 'Foetid Manse (E5M7) - Mystic Urn',
+ 'Foetid Manse (E5M7) - Phoenix Rod',
+ 'Foetid Manse (E5M7) - Ring of Invincibility',
+ 'Foetid Manse (E5M7) - Shadowsphere',
+ 'Foetid Manse (E5M7) - Silver Shield',
+ 'Foetid Manse (E5M7) - Tome of Power',
+ 'Foetid Manse (E5M7) - Tome of Power 2',
+ 'Foetid Manse (E5M7) - Tome of Power 3',
+ 'Foetid Manse (E5M7) - Torch',
+ 'Foetid Manse (E5M7) - Yellow key',
+ },
+ 'Great Stair (E4M5)': {
+ 'Great Stair (E4M5) - Bag of Holding',
+ 'Great Stair (E4M5) - Bag of Holding 2',
+ 'Great Stair (E4M5) - Blue key',
+ 'Great Stair (E4M5) - Chaos Device',
+ 'Great Stair (E4M5) - Dragon Claw',
+ 'Great Stair (E4M5) - Enchanted Shield',
+ 'Great Stair (E4M5) - Ethereal Crossbow',
+ 'Great Stair (E4M5) - Exit',
+ 'Great Stair (E4M5) - Firemace',
+ 'Great Stair (E4M5) - Firemace 2',
+ 'Great Stair (E4M5) - Firemace 3',
+ 'Great Stair (E4M5) - Firemace 4',
+ 'Great Stair (E4M5) - Firemace 5',
+ 'Great Stair (E4M5) - Gauntlets of the Necromancer',
+ 'Great Stair (E4M5) - Green key',
+ 'Great Stair (E4M5) - Hellstaff',
+ 'Great Stair (E4M5) - Map Scroll',
+ 'Great Stair (E4M5) - Morph Ovum',
+ 'Great Stair (E4M5) - Mystic Urn',
+ 'Great Stair (E4M5) - Mystic Urn 2',
+ 'Great Stair (E4M5) - Phoenix Rod',
+ 'Great Stair (E4M5) - Ring of Invincibility',
+ 'Great Stair (E4M5) - Shadowsphere',
+ 'Great Stair (E4M5) - Silver Shield',
+ 'Great Stair (E4M5) - Tome of Power',
+ 'Great Stair (E4M5) - Tome of Power 2',
+ 'Great Stair (E4M5) - Tome of Power 3',
+ 'Great Stair (E4M5) - Torch',
+ 'Great Stair (E4M5) - Yellow key',
+ },
+ 'Halls of the Apostate (E4M6)': {
+ 'Halls of the Apostate (E4M6) - Bag of Holding',
+ 'Halls of the Apostate (E4M6) - Bag of Holding 2',
+ 'Halls of the Apostate (E4M6) - Blue key',
+ 'Halls of the Apostate (E4M6) - Chaos Device',
+ 'Halls of the Apostate (E4M6) - Dragon Claw',
+ 'Halls of the Apostate (E4M6) - Enchanted Shield',
+ 'Halls of the Apostate (E4M6) - Ethereal Crossbow',
+ 'Halls of the Apostate (E4M6) - Exit',
+ 'Halls of the Apostate (E4M6) - Gauntlets of the Necromancer',
+ 'Halls of the Apostate (E4M6) - Green key',
+ 'Halls of the Apostate (E4M6) - Hellstaff',
+ 'Halls of the Apostate (E4M6) - Map Scroll',
+ 'Halls of the Apostate (E4M6) - Morph Ovum',
+ 'Halls of the Apostate (E4M6) - Mystic Urn',
+ 'Halls of the Apostate (E4M6) - Phoenix Rod',
+ 'Halls of the Apostate (E4M6) - Ring of Invincibility',
+ 'Halls of the Apostate (E4M6) - Shadowsphere',
+ 'Halls of the Apostate (E4M6) - Silver Shield',
+ 'Halls of the Apostate (E4M6) - Silver Shield 2',
+ 'Halls of the Apostate (E4M6) - Tome of Power',
+ 'Halls of the Apostate (E4M6) - Tome of Power 2',
+ 'Halls of the Apostate (E4M6) - Yellow key',
+ },
+ "Hell's Maw (E1M8)": {
+ "Hell's Maw (E1M8) - Bag of Holding",
+ "Hell's Maw (E1M8) - Bag of Holding 2",
+ "Hell's Maw (E1M8) - Dragon Claw",
+ "Hell's Maw (E1M8) - Ethereal Crossbow",
+ "Hell's Maw (E1M8) - Exit",
+ "Hell's Maw (E1M8) - Gauntlets of the Necromancer",
+ "Hell's Maw (E1M8) - Morph Ovum",
+ "Hell's Maw (E1M8) - Ring of Invincibility",
+ "Hell's Maw (E1M8) - Ring of Invincibility 2",
+ "Hell's Maw (E1M8) - Ring of Invincibility 3",
+ "Hell's Maw (E1M8) - Shadowsphere",
+ "Hell's Maw (E1M8) - Silver Shield",
+ "Hell's Maw (E1M8) - Tome of Power",
+ "Hell's Maw (E1M8) - Tome of Power 2",
+ },
+ 'Hydratyr (E5M5)': {
+ 'Hydratyr (E5M5) - Bag of Holding',
+ 'Hydratyr (E5M5) - Bag of Holding 2',
+ 'Hydratyr (E5M5) - Blue key',
+ 'Hydratyr (E5M5) - Chaos Device',
+ 'Hydratyr (E5M5) - Dragon Claw',
+ 'Hydratyr (E5M5) - Enchanted Shield',
+ 'Hydratyr (E5M5) - Ethereal Crossbow',
+ 'Hydratyr (E5M5) - Exit',
+ 'Hydratyr (E5M5) - Firemace',
+ 'Hydratyr (E5M5) - Firemace 2',
+ 'Hydratyr (E5M5) - Firemace 3',
+ 'Hydratyr (E5M5) - Firemace 4',
+ 'Hydratyr (E5M5) - Gauntlets of the Necromancer',
+ 'Hydratyr (E5M5) - Green key',
+ 'Hydratyr (E5M5) - Hellstaff',
+ 'Hydratyr (E5M5) - Map Scroll',
+ 'Hydratyr (E5M5) - Morph Ovum',
+ 'Hydratyr (E5M5) - Mystic Urn',
+ 'Hydratyr (E5M5) - Phoenix Rod',
+ 'Hydratyr (E5M5) - Ring of Invincibility',
+ 'Hydratyr (E5M5) - Shadowsphere',
+ 'Hydratyr (E5M5) - Silver Shield',
+ 'Hydratyr (E5M5) - Silver Shield 2',
+ 'Hydratyr (E5M5) - Tome of Power',
+ 'Hydratyr (E5M5) - Tome of Power 2',
+ 'Hydratyr (E5M5) - Tome of Power 3',
+ 'Hydratyr (E5M5) - Torch',
+ 'Hydratyr (E5M5) - Yellow key',
+ },
+ 'Mausoleum (E4M9)': {
+ 'Mausoleum (E4M9) - Bag of Holding',
+ 'Mausoleum (E4M9) - Bag of Holding 2',
+ 'Mausoleum (E4M9) - Bag of Holding 3',
+ 'Mausoleum (E4M9) - Bag of Holding 4',
+ 'Mausoleum (E4M9) - Chaos Device',
+ 'Mausoleum (E4M9) - Dragon Claw',
+ 'Mausoleum (E4M9) - Enchanted Shield',
+ 'Mausoleum (E4M9) - Ethereal Crossbow',
+ 'Mausoleum (E4M9) - Exit',
+ 'Mausoleum (E4M9) - Firemace',
+ 'Mausoleum (E4M9) - Firemace 2',
+ 'Mausoleum (E4M9) - Firemace 3',
+ 'Mausoleum (E4M9) - Firemace 4',
+ 'Mausoleum (E4M9) - Gauntlets of the Necromancer',
+ 'Mausoleum (E4M9) - Hellstaff',
+ 'Mausoleum (E4M9) - Map Scroll',
+ 'Mausoleum (E4M9) - Morph Ovum',
+ 'Mausoleum (E4M9) - Mystic Urn',
+ 'Mausoleum (E4M9) - Phoenix Rod',
+ 'Mausoleum (E4M9) - Ring of Invincibility',
+ 'Mausoleum (E4M9) - Shadowsphere',
+ 'Mausoleum (E4M9) - Silver Shield',
+ 'Mausoleum (E4M9) - Silver Shield 2',
+ 'Mausoleum (E4M9) - Tome of Power',
+ 'Mausoleum (E4M9) - Tome of Power 2',
+ 'Mausoleum (E4M9) - Tome of Power 3',
+ 'Mausoleum (E4M9) - Torch',
+ 'Mausoleum (E4M9) - Torch 2',
+ 'Mausoleum (E4M9) - Yellow key',
+ },
+ 'Ochre Cliffs (E5M1)': {
+ 'Ochre Cliffs (E5M1) - Bag of Holding',
+ 'Ochre Cliffs (E5M1) - Bag of Holding 2',
+ 'Ochre Cliffs (E5M1) - Blue key',
+ 'Ochre Cliffs (E5M1) - Chaos Device',
+ 'Ochre Cliffs (E5M1) - Dragon Claw',
+ 'Ochre Cliffs (E5M1) - Enchanted Shield',
+ 'Ochre Cliffs (E5M1) - Ethereal Crossbow',
+ 'Ochre Cliffs (E5M1) - Exit',
+ 'Ochre Cliffs (E5M1) - Firemace',
+ 'Ochre Cliffs (E5M1) - Firemace 2',
+ 'Ochre Cliffs (E5M1) - Firemace 3',
+ 'Ochre Cliffs (E5M1) - Firemace 4',
+ 'Ochre Cliffs (E5M1) - Gauntlets of the Necromancer',
+ 'Ochre Cliffs (E5M1) - Green key',
+ 'Ochre Cliffs (E5M1) - Hellstaff',
+ 'Ochre Cliffs (E5M1) - Map Scroll',
+ 'Ochre Cliffs (E5M1) - Morph Ovum',
+ 'Ochre Cliffs (E5M1) - Mystic Urn',
+ 'Ochre Cliffs (E5M1) - Phoenix Rod',
+ 'Ochre Cliffs (E5M1) - Ring of Invincibility',
+ 'Ochre Cliffs (E5M1) - Shadowsphere',
+ 'Ochre Cliffs (E5M1) - Silver Shield',
+ 'Ochre Cliffs (E5M1) - Tome of Power',
+ 'Ochre Cliffs (E5M1) - Tome of Power 2',
+ 'Ochre Cliffs (E5M1) - Tome of Power 3',
+ 'Ochre Cliffs (E5M1) - Torch',
+ 'Ochre Cliffs (E5M1) - Yellow key',
+ },
+ 'Quay (E5M3)': {
+ 'Quay (E5M3) - Bag of Holding',
+ 'Quay (E5M3) - Bag of Holding 2',
+ 'Quay (E5M3) - Blue key',
+ 'Quay (E5M3) - Chaos Device',
+ 'Quay (E5M3) - Dragon Claw',
+ 'Quay (E5M3) - Enchanted Shield',
+ 'Quay (E5M3) - Ethereal Crossbow',
+ 'Quay (E5M3) - Exit',
+ 'Quay (E5M3) - Firemace',
+ 'Quay (E5M3) - Firemace 2',
+ 'Quay (E5M3) - Firemace 3',
+ 'Quay (E5M3) - Firemace 4',
+ 'Quay (E5M3) - Firemace 5',
+ 'Quay (E5M3) - Firemace 6',
+ 'Quay (E5M3) - Gauntlets of the Necromancer',
+ 'Quay (E5M3) - Green key',
+ 'Quay (E5M3) - Hellstaff',
+ 'Quay (E5M3) - Map Scroll',
+ 'Quay (E5M3) - Morph Ovum',
+ 'Quay (E5M3) - Mystic Urn',
+ 'Quay (E5M3) - Phoenix Rod',
+ 'Quay (E5M3) - Ring of Invincibility',
+ 'Quay (E5M3) - Shadowsphere',
+ 'Quay (E5M3) - Silver Shield',
+ 'Quay (E5M3) - Silver Shield 2',
+ 'Quay (E5M3) - Tome of Power',
+ 'Quay (E5M3) - Tome of Power 2',
+ 'Quay (E5M3) - Tome of Power 3',
+ 'Quay (E5M3) - Torch',
+ 'Quay (E5M3) - Yellow key',
+ },
+ 'Ramparts of Perdition (E4M7)': {
+ 'Ramparts of Perdition (E4M7) - Bag of Holding',
+ 'Ramparts of Perdition (E4M7) - Bag of Holding 2',
+ 'Ramparts of Perdition (E4M7) - Blue key',
+ 'Ramparts of Perdition (E4M7) - Chaos Device',
+ 'Ramparts of Perdition (E4M7) - Dragon Claw',
+ 'Ramparts of Perdition (E4M7) - Dragon Claw 2',
+ 'Ramparts of Perdition (E4M7) - Enchanted Shield',
+ 'Ramparts of Perdition (E4M7) - Ethereal Crossbow',
+ 'Ramparts of Perdition (E4M7) - Ethereal Crossbow 2',
+ 'Ramparts of Perdition (E4M7) - Exit',
+ 'Ramparts of Perdition (E4M7) - Firemace',
+ 'Ramparts of Perdition (E4M7) - Firemace 2',
+ 'Ramparts of Perdition (E4M7) - Firemace 3',
+ 'Ramparts of Perdition (E4M7) - Firemace 4',
+ 'Ramparts of Perdition (E4M7) - Firemace 5',
+ 'Ramparts of Perdition (E4M7) - Firemace 6',
+ 'Ramparts of Perdition (E4M7) - Gauntlets of the Necromancer',
+ 'Ramparts of Perdition (E4M7) - Green key',
+ 'Ramparts of Perdition (E4M7) - Hellstaff',
+ 'Ramparts of Perdition (E4M7) - Hellstaff 2',
+ 'Ramparts of Perdition (E4M7) - Map Scroll',
+ 'Ramparts of Perdition (E4M7) - Morph Ovum',
+ 'Ramparts of Perdition (E4M7) - Mystic Urn',
+ 'Ramparts of Perdition (E4M7) - Mystic Urn 2',
+ 'Ramparts of Perdition (E4M7) - Phoenix Rod',
+ 'Ramparts of Perdition (E4M7) - Phoenix Rod 2',
+ 'Ramparts of Perdition (E4M7) - Ring of Invincibility',
+ 'Ramparts of Perdition (E4M7) - Shadowsphere',
+ 'Ramparts of Perdition (E4M7) - Silver Shield',
+ 'Ramparts of Perdition (E4M7) - Silver Shield 2',
+ 'Ramparts of Perdition (E4M7) - Tome of Power',
+ 'Ramparts of Perdition (E4M7) - Tome of Power 2',
+ 'Ramparts of Perdition (E4M7) - Tome of Power 3',
+ 'Ramparts of Perdition (E4M7) - Torch',
+ 'Ramparts of Perdition (E4M7) - Torch 2',
+ 'Ramparts of Perdition (E4M7) - Yellow key',
+ },
+ 'Rapids (E5M2)': {
+ 'Rapids (E5M2) - Bag of Holding',
+ 'Rapids (E5M2) - Bag of Holding 2',
+ 'Rapids (E5M2) - Bag of Holding 3',
+ 'Rapids (E5M2) - Chaos Device',
+ 'Rapids (E5M2) - Dragon Claw',
+ 'Rapids (E5M2) - Enchanted Shield',
+ 'Rapids (E5M2) - Enchanted Shield 2',
+ 'Rapids (E5M2) - Ethereal Crossbow',
+ 'Rapids (E5M2) - Exit',
+ 'Rapids (E5M2) - Firemace',
+ 'Rapids (E5M2) - Firemace 2',
+ 'Rapids (E5M2) - Firemace 3',
+ 'Rapids (E5M2) - Firemace 4',
+ 'Rapids (E5M2) - Firemace 5',
+ 'Rapids (E5M2) - Gauntlets of the Necromancer',
+ 'Rapids (E5M2) - Green key',
+ 'Rapids (E5M2) - Hellstaff',
+ 'Rapids (E5M2) - Hellstaff 2',
+ 'Rapids (E5M2) - Map Scroll',
+ 'Rapids (E5M2) - Morph Ovum',
+ 'Rapids (E5M2) - Mystic Urn',
+ 'Rapids (E5M2) - Phoenix Rod',
+ 'Rapids (E5M2) - Phoenix Rod 2',
+ 'Rapids (E5M2) - Ring of Invincibility',
+ 'Rapids (E5M2) - Shadowsphere',
+ 'Rapids (E5M2) - Silver Shield',
+ 'Rapids (E5M2) - Tome of Power',
+ 'Rapids (E5M2) - Tome of Power 2',
+ 'Rapids (E5M2) - Torch',
+ 'Rapids (E5M2) - Yellow key',
+ },
+ 'Sepulcher (E4M4)': {
+ 'Sepulcher (E4M4) - Bag of Holding',
+ 'Sepulcher (E4M4) - Bag of Holding 2',
+ 'Sepulcher (E4M4) - Chaos Device',
+ 'Sepulcher (E4M4) - Dragon Claw',
+ 'Sepulcher (E4M4) - Dragon Claw 2',
+ 'Sepulcher (E4M4) - Enchanted Shield',
+ 'Sepulcher (E4M4) - Ethereal Crossbow',
+ 'Sepulcher (E4M4) - Ethereal Crossbow 2',
+ 'Sepulcher (E4M4) - Exit',
+ 'Sepulcher (E4M4) - Firemace',
+ 'Sepulcher (E4M4) - Firemace 2',
+ 'Sepulcher (E4M4) - Firemace 3',
+ 'Sepulcher (E4M4) - Firemace 4',
+ 'Sepulcher (E4M4) - Firemace 5',
+ 'Sepulcher (E4M4) - Hellstaff',
+ 'Sepulcher (E4M4) - Morph Ovum',
+ 'Sepulcher (E4M4) - Mystic Urn',
+ 'Sepulcher (E4M4) - Phoenix Rod',
+ 'Sepulcher (E4M4) - Phoenix Rod 2',
+ 'Sepulcher (E4M4) - Ring of Invincibility',
+ 'Sepulcher (E4M4) - Shadowsphere',
+ 'Sepulcher (E4M4) - Silver Shield',
+ 'Sepulcher (E4M4) - Silver Shield 2',
+ 'Sepulcher (E4M4) - Tome of Power',
+ 'Sepulcher (E4M4) - Tome of Power 2',
+ 'Sepulcher (E4M4) - Torch',
+ 'Sepulcher (E4M4) - Torch 2',
+ },
+ 'Shattered Bridge (E4M8)': {
+ 'Shattered Bridge (E4M8) - Bag of Holding',
+ 'Shattered Bridge (E4M8) - Bag of Holding 2',
+ 'Shattered Bridge (E4M8) - Chaos Device',
+ 'Shattered Bridge (E4M8) - Dragon Claw',
+ 'Shattered Bridge (E4M8) - Enchanted Shield',
+ 'Shattered Bridge (E4M8) - Ethereal Crossbow',
+ 'Shattered Bridge (E4M8) - Exit',
+ 'Shattered Bridge (E4M8) - Gauntlets of the Necromancer',
+ 'Shattered Bridge (E4M8) - Hellstaff',
+ 'Shattered Bridge (E4M8) - Morph Ovum',
+ 'Shattered Bridge (E4M8) - Mystic Urn',
+ 'Shattered Bridge (E4M8) - Phoenix Rod',
+ 'Shattered Bridge (E4M8) - Ring of Invincibility',
+ 'Shattered Bridge (E4M8) - Shadowsphere',
+ 'Shattered Bridge (E4M8) - Silver Shield',
+ 'Shattered Bridge (E4M8) - Tome of Power',
+ 'Shattered Bridge (E4M8) - Tome of Power 2',
+ 'Shattered Bridge (E4M8) - Torch',
+ 'Shattered Bridge (E4M8) - Yellow key',
+ },
+ "Skein of D'Sparil (E5M9)": {
+ "Skein of D'Sparil (E5M9) - Bag of Holding",
+ "Skein of D'Sparil (E5M9) - Bag of Holding 2",
+ "Skein of D'Sparil (E5M9) - Blue key",
+ "Skein of D'Sparil (E5M9) - Chaos Device",
+ "Skein of D'Sparil (E5M9) - Dragon Claw",
+ "Skein of D'Sparil (E5M9) - Enchanted Shield",
+ "Skein of D'Sparil (E5M9) - Ethereal Crossbow",
+ "Skein of D'Sparil (E5M9) - Exit",
+ "Skein of D'Sparil (E5M9) - Gauntlets of the Necromancer",
+ "Skein of D'Sparil (E5M9) - Green key",
+ "Skein of D'Sparil (E5M9) - Hellstaff",
+ "Skein of D'Sparil (E5M9) - Map Scroll",
+ "Skein of D'Sparil (E5M9) - Morph Ovum",
+ "Skein of D'Sparil (E5M9) - Mystic Urn",
+ "Skein of D'Sparil (E5M9) - Phoenix Rod",
+ "Skein of D'Sparil (E5M9) - Ring of Invincibility",
+ "Skein of D'Sparil (E5M9) - Shadowsphere",
+ "Skein of D'Sparil (E5M9) - Silver Shield",
+ "Skein of D'Sparil (E5M9) - Tome of Power",
+ "Skein of D'Sparil (E5M9) - Tome of Power 2",
+ "Skein of D'Sparil (E5M9) - Torch",
+ "Skein of D'Sparil (E5M9) - Yellow key",
+ },
+ 'The Aquifier (E3M9)': {
+ 'The Aquifier (E3M9) - Bag of Holding',
+ 'The Aquifier (E3M9) - Blue key',
+ 'The Aquifier (E3M9) - Chaos Device',
+ 'The Aquifier (E3M9) - Dragon Claw',
+ 'The Aquifier (E3M9) - Enchanted Shield',
+ 'The Aquifier (E3M9) - Ethereal Crossbow',
+ 'The Aquifier (E3M9) - Exit',
+ 'The Aquifier (E3M9) - Firemace',
+ 'The Aquifier (E3M9) - Firemace 2',
+ 'The Aquifier (E3M9) - Firemace 3',
+ 'The Aquifier (E3M9) - Firemace 4',
+ 'The Aquifier (E3M9) - Gauntlets of the Necromancer',
+ 'The Aquifier (E3M9) - Green key',
+ 'The Aquifier (E3M9) - Hellstaff',
+ 'The Aquifier (E3M9) - Map Scroll',
+ 'The Aquifier (E3M9) - Morph Ovum',
+ 'The Aquifier (E3M9) - Mystic Urn',
+ 'The Aquifier (E3M9) - Phoenix Rod',
+ 'The Aquifier (E3M9) - Ring of Invincibility',
+ 'The Aquifier (E3M9) - Shadowsphere',
+ 'The Aquifier (E3M9) - Silver Shield',
+ 'The Aquifier (E3M9) - Silver Shield 2',
+ 'The Aquifier (E3M9) - Tome of Power',
+ 'The Aquifier (E3M9) - Tome of Power 2',
+ 'The Aquifier (E3M9) - Torch',
+ 'The Aquifier (E3M9) - Yellow key',
+ },
+ 'The Azure Fortress (E3M4)': {
+ 'The Azure Fortress (E3M4) - Bag of Holding',
+ 'The Azure Fortress (E3M4) - Bag of Holding 2',
+ 'The Azure Fortress (E3M4) - Chaos Device',
+ 'The Azure Fortress (E3M4) - Dragon Claw',
+ 'The Azure Fortress (E3M4) - Enchanted Shield',
+ 'The Azure Fortress (E3M4) - Enchanted Shield 2',
+ 'The Azure Fortress (E3M4) - Ethereal Crossbow',
+ 'The Azure Fortress (E3M4) - Exit',
+ 'The Azure Fortress (E3M4) - Gauntlets of the Necromancer',
+ 'The Azure Fortress (E3M4) - Green key',
+ 'The Azure Fortress (E3M4) - Hellstaff',
+ 'The Azure Fortress (E3M4) - Map Scroll',
+ 'The Azure Fortress (E3M4) - Morph Ovum',
+ 'The Azure Fortress (E3M4) - Morph Ovum 2',
+ 'The Azure Fortress (E3M4) - Mystic Urn',
+ 'The Azure Fortress (E3M4) - Mystic Urn 2',
+ 'The Azure Fortress (E3M4) - Phoenix Rod',
+ 'The Azure Fortress (E3M4) - Ring of Invincibility',
+ 'The Azure Fortress (E3M4) - Shadowsphere',
+ 'The Azure Fortress (E3M4) - Silver Shield',
+ 'The Azure Fortress (E3M4) - Silver Shield 2',
+ 'The Azure Fortress (E3M4) - Tome of Power',
+ 'The Azure Fortress (E3M4) - Tome of Power 2',
+ 'The Azure Fortress (E3M4) - Tome of Power 3',
+ 'The Azure Fortress (E3M4) - Torch',
+ 'The Azure Fortress (E3M4) - Torch 2',
+ 'The Azure Fortress (E3M4) - Torch 3',
+ 'The Azure Fortress (E3M4) - Yellow key',
+ },
+ 'The Catacombs (E2M5)': {
+ 'The Catacombs (E2M5) - Bag of Holding',
+ 'The Catacombs (E2M5) - Blue key',
+ 'The Catacombs (E2M5) - Chaos Device',
+ 'The Catacombs (E2M5) - Dragon Claw',
+ 'The Catacombs (E2M5) - Enchanted Shield',
+ 'The Catacombs (E2M5) - Ethereal Crossbow',
+ 'The Catacombs (E2M5) - Exit',
+ 'The Catacombs (E2M5) - Gauntlets of the Necromancer',
+ 'The Catacombs (E2M5) - Green key',
+ 'The Catacombs (E2M5) - Hellstaff',
+ 'The Catacombs (E2M5) - Map Scroll',
+ 'The Catacombs (E2M5) - Morph Ovum',
+ 'The Catacombs (E2M5) - Mystic Urn',
+ 'The Catacombs (E2M5) - Phoenix Rod',
+ 'The Catacombs (E2M5) - Ring of Invincibility',
+ 'The Catacombs (E2M5) - Shadowsphere',
+ 'The Catacombs (E2M5) - Silver Shield',
+ 'The Catacombs (E2M5) - Tome of Power',
+ 'The Catacombs (E2M5) - Tome of Power 2',
+ 'The Catacombs (E2M5) - Tome of Power 3',
+ 'The Catacombs (E2M5) - Torch',
+ 'The Catacombs (E2M5) - Yellow key',
+ },
+ 'The Cathedral (E1M6)': {
+ 'The Cathedral (E1M6) - Bag of Holding',
+ 'The Cathedral (E1M6) - Bag of Holding 2',
+ 'The Cathedral (E1M6) - Bag of Holding 3',
+ 'The Cathedral (E1M6) - Dragon Claw',
+ 'The Cathedral (E1M6) - Ethereal Crossbow',
+ 'The Cathedral (E1M6) - Exit',
+ 'The Cathedral (E1M6) - Gauntlets of the Necromancer',
+ 'The Cathedral (E1M6) - Green key',
+ 'The Cathedral (E1M6) - Map Scroll',
+ 'The Cathedral (E1M6) - Morph Ovum',
+ 'The Cathedral (E1M6) - Ring of Invincibility',
+ 'The Cathedral (E1M6) - Ring of Invincibility 2',
+ 'The Cathedral (E1M6) - Shadowsphere',
+ 'The Cathedral (E1M6) - Silver Shield',
+ 'The Cathedral (E1M6) - Silver Shield 2',
+ 'The Cathedral (E1M6) - Silver Shield 3',
+ 'The Cathedral (E1M6) - Tome of Power',
+ 'The Cathedral (E1M6) - Tome of Power 2',
+ 'The Cathedral (E1M6) - Tome of Power 3',
+ 'The Cathedral (E1M6) - Tome of Power 4',
+ 'The Cathedral (E1M6) - Torch',
+ 'The Cathedral (E1M6) - Yellow key',
+ },
+ 'The Cesspool (E3M2)': {
+ 'The Cesspool (E3M2) - Bag of Holding',
+ 'The Cesspool (E3M2) - Bag of Holding 2',
+ 'The Cesspool (E3M2) - Blue key',
+ 'The Cesspool (E3M2) - Chaos Device',
+ 'The Cesspool (E3M2) - Dragon Claw',
+ 'The Cesspool (E3M2) - Enchanted Shield',
+ 'The Cesspool (E3M2) - Ethereal Crossbow',
+ 'The Cesspool (E3M2) - Exit',
+ 'The Cesspool (E3M2) - Firemace',
+ 'The Cesspool (E3M2) - Firemace 2',
+ 'The Cesspool (E3M2) - Firemace 3',
+ 'The Cesspool (E3M2) - Firemace 4',
+ 'The Cesspool (E3M2) - Firemace 5',
+ 'The Cesspool (E3M2) - Gauntlets of the Necromancer',
+ 'The Cesspool (E3M2) - Green key',
+ 'The Cesspool (E3M2) - Hellstaff',
+ 'The Cesspool (E3M2) - Map Scroll',
+ 'The Cesspool (E3M2) - Morph Ovum',
+ 'The Cesspool (E3M2) - Morph Ovum 2',
+ 'The Cesspool (E3M2) - Mystic Urn',
+ 'The Cesspool (E3M2) - Phoenix Rod',
+ 'The Cesspool (E3M2) - Ring of Invincibility',
+ 'The Cesspool (E3M2) - Shadowsphere',
+ 'The Cesspool (E3M2) - Silver Shield',
+ 'The Cesspool (E3M2) - Silver Shield 2',
+ 'The Cesspool (E3M2) - Tome of Power',
+ 'The Cesspool (E3M2) - Tome of Power 2',
+ 'The Cesspool (E3M2) - Tome of Power 3',
+ 'The Cesspool (E3M2) - Torch',
+ 'The Cesspool (E3M2) - Yellow key',
+ },
+ 'The Chasm (E3M7)': {
+ 'The Chasm (E3M7) - Bag of Holding',
+ 'The Chasm (E3M7) - Bag of Holding 2',
+ 'The Chasm (E3M7) - Blue key',
+ 'The Chasm (E3M7) - Chaos Device',
+ 'The Chasm (E3M7) - Dragon Claw',
+ 'The Chasm (E3M7) - Enchanted Shield',
+ 'The Chasm (E3M7) - Ethereal Crossbow',
+ 'The Chasm (E3M7) - Exit',
+ 'The Chasm (E3M7) - Gauntlets of the Necromancer',
+ 'The Chasm (E3M7) - Green key',
+ 'The Chasm (E3M7) - Hellstaff',
+ 'The Chasm (E3M7) - Map Scroll',
+ 'The Chasm (E3M7) - Morph Ovum',
+ 'The Chasm (E3M7) - Mystic Urn',
+ 'The Chasm (E3M7) - Phoenix Rod',
+ 'The Chasm (E3M7) - Ring of Invincibility',
+ 'The Chasm (E3M7) - Shadowsphere',
+ 'The Chasm (E3M7) - Shadowsphere 2',
+ 'The Chasm (E3M7) - Silver Shield',
+ 'The Chasm (E3M7) - Tome of Power',
+ 'The Chasm (E3M7) - Tome of Power 2',
+ 'The Chasm (E3M7) - Tome of Power 3',
+ 'The Chasm (E3M7) - Torch',
+ 'The Chasm (E3M7) - Torch 2',
+ 'The Chasm (E3M7) - Yellow key',
+ },
+ 'The Citadel (E1M5)': {
+ 'The Citadel (E1M5) - Bag of Holding',
+ 'The Citadel (E1M5) - Blue key',
+ 'The Citadel (E1M5) - Dragon Claw',
+ 'The Citadel (E1M5) - Ethereal Crossbow',
+ 'The Citadel (E1M5) - Exit',
+ 'The Citadel (E1M5) - Gauntlets of the Necromancer',
+ 'The Citadel (E1M5) - Green key',
+ 'The Citadel (E1M5) - Map Scroll',
+ 'The Citadel (E1M5) - Morph Ovum',
+ 'The Citadel (E1M5) - Ring of Invincibility',
+ 'The Citadel (E1M5) - Shadowsphere',
+ 'The Citadel (E1M5) - Silver Shield',
+ 'The Citadel (E1M5) - Silver Shield 2',
+ 'The Citadel (E1M5) - Tome of Power',
+ 'The Citadel (E1M5) - Tome of Power 2',
+ 'The Citadel (E1M5) - Tome of Power 3',
+ 'The Citadel (E1M5) - Tome of Power 4',
+ 'The Citadel (E1M5) - Tome of Power 5',
+ 'The Citadel (E1M5) - Torch',
+ 'The Citadel (E1M5) - Torch 2',
+ 'The Citadel (E1M5) - Yellow key',
+ },
+ 'The Confluence (E3M3)': {
+ 'The Confluence (E3M3) - Bag of Holding',
+ 'The Confluence (E3M3) - Blue key',
+ 'The Confluence (E3M3) - Chaos Device',
+ 'The Confluence (E3M3) - Dragon Claw',
+ 'The Confluence (E3M3) - Enchanted Shield',
+ 'The Confluence (E3M3) - Ethereal Crossbow',
+ 'The Confluence (E3M3) - Exit',
+ 'The Confluence (E3M3) - Firemace',
+ 'The Confluence (E3M3) - Firemace 2',
+ 'The Confluence (E3M3) - Firemace 3',
+ 'The Confluence (E3M3) - Firemace 4',
+ 'The Confluence (E3M3) - Firemace 5',
+ 'The Confluence (E3M3) - Firemace 6',
+ 'The Confluence (E3M3) - Gauntlets of the Necromancer',
+ 'The Confluence (E3M3) - Green key',
+ 'The Confluence (E3M3) - Hellstaff',
+ 'The Confluence (E3M3) - Hellstaff 2',
+ 'The Confluence (E3M3) - Map Scroll',
+ 'The Confluence (E3M3) - Morph Ovum',
+ 'The Confluence (E3M3) - Mystic Urn',
+ 'The Confluence (E3M3) - Mystic Urn 2',
+ 'The Confluence (E3M3) - Phoenix Rod',
+ 'The Confluence (E3M3) - Ring of Invincibility',
+ 'The Confluence (E3M3) - Shadowsphere',
+ 'The Confluence (E3M3) - Silver Shield',
+ 'The Confluence (E3M3) - Silver Shield 2',
+ 'The Confluence (E3M3) - Tome of Power',
+ 'The Confluence (E3M3) - Tome of Power 2',
+ 'The Confluence (E3M3) - Tome of Power 3',
+ 'The Confluence (E3M3) - Tome of Power 4',
+ 'The Confluence (E3M3) - Tome of Power 5',
+ 'The Confluence (E3M3) - Torch',
+ 'The Confluence (E3M3) - Yellow key',
+ },
+ 'The Crater (E2M1)': {
+ 'The Crater (E2M1) - Bag of Holding',
+ 'The Crater (E2M1) - Dragon Claw',
+ 'The Crater (E2M1) - Ethereal Crossbow',
+ 'The Crater (E2M1) - Exit',
+ 'The Crater (E2M1) - Green key',
+ 'The Crater (E2M1) - Hellstaff',
+ 'The Crater (E2M1) - Mystic Urn',
+ 'The Crater (E2M1) - Shadowsphere',
+ 'The Crater (E2M1) - Silver Shield',
+ 'The Crater (E2M1) - Tome of Power',
+ 'The Crater (E2M1) - Torch',
+ 'The Crater (E2M1) - Yellow key',
+ },
+ 'The Crypts (E1M7)': {
+ 'The Crypts (E1M7) - Bag of Holding',
+ 'The Crypts (E1M7) - Blue key',
+ 'The Crypts (E1M7) - Dragon Claw',
+ 'The Crypts (E1M7) - Ethereal Crossbow',
+ 'The Crypts (E1M7) - Exit',
+ 'The Crypts (E1M7) - Gauntlets of the Necromancer',
+ 'The Crypts (E1M7) - Green key',
+ 'The Crypts (E1M7) - Map Scroll',
+ 'The Crypts (E1M7) - Morph Ovum',
+ 'The Crypts (E1M7) - Ring of Invincibility',
+ 'The Crypts (E1M7) - Shadowsphere',
+ 'The Crypts (E1M7) - Silver Shield',
+ 'The Crypts (E1M7) - Silver Shield 2',
+ 'The Crypts (E1M7) - Tome of Power',
+ 'The Crypts (E1M7) - Tome of Power 2',
+ 'The Crypts (E1M7) - Torch',
+ 'The Crypts (E1M7) - Torch 2',
+ 'The Crypts (E1M7) - Yellow key',
+ },
+ 'The Docks (E1M1)': {
+ 'The Docks (E1M1) - Bag of Holding',
+ 'The Docks (E1M1) - Ethereal Crossbow',
+ 'The Docks (E1M1) - Exit',
+ 'The Docks (E1M1) - Gauntlets of the Necromancer',
+ 'The Docks (E1M1) - Silver Shield',
+ 'The Docks (E1M1) - Tome of Power',
+ 'The Docks (E1M1) - Yellow key',
+ },
+ 'The Dungeons (E1M2)': {
+ 'The Dungeons (E1M2) - Bag of Holding',
+ 'The Dungeons (E1M2) - Blue key',
+ 'The Dungeons (E1M2) - Dragon Claw',
+ 'The Dungeons (E1M2) - Ethereal Crossbow',
+ 'The Dungeons (E1M2) - Exit',
+ 'The Dungeons (E1M2) - Gauntlets of the Necromancer',
+ 'The Dungeons (E1M2) - Green key',
+ 'The Dungeons (E1M2) - Map Scroll',
+ 'The Dungeons (E1M2) - Ring of Invincibility',
+ 'The Dungeons (E1M2) - Shadowsphere',
+ 'The Dungeons (E1M2) - Silver Shield',
+ 'The Dungeons (E1M2) - Silver Shield 2',
+ 'The Dungeons (E1M2) - Tome of Power',
+ 'The Dungeons (E1M2) - Tome of Power 2',
+ 'The Dungeons (E1M2) - Torch',
+ 'The Dungeons (E1M2) - Yellow key',
+ },
+ 'The Gatehouse (E1M3)': {
+ 'The Gatehouse (E1M3) - Bag of Holding',
+ 'The Gatehouse (E1M3) - Dragon Claw',
+ 'The Gatehouse (E1M3) - Ethereal Crossbow',
+ 'The Gatehouse (E1M3) - Exit',
+ 'The Gatehouse (E1M3) - Gauntlets of the Necromancer',
+ 'The Gatehouse (E1M3) - Green key',
+ 'The Gatehouse (E1M3) - Morph Ovum',
+ 'The Gatehouse (E1M3) - Ring of Invincibility',
+ 'The Gatehouse (E1M3) - Shadowsphere',
+ 'The Gatehouse (E1M3) - Silver Shield',
+ 'The Gatehouse (E1M3) - Tome of Power',
+ 'The Gatehouse (E1M3) - Tome of Power 2',
+ 'The Gatehouse (E1M3) - Tome of Power 3',
+ 'The Gatehouse (E1M3) - Torch',
+ 'The Gatehouse (E1M3) - Yellow key',
+ },
+ 'The Glacier (E2M9)': {
+ 'The Glacier (E2M9) - Bag of Holding',
+ 'The Glacier (E2M9) - Blue key',
+ 'The Glacier (E2M9) - Chaos Device',
+ 'The Glacier (E2M9) - Dragon Claw',
+ 'The Glacier (E2M9) - Dragon Claw 2',
+ 'The Glacier (E2M9) - Enchanted Shield',
+ 'The Glacier (E2M9) - Ethereal Crossbow',
+ 'The Glacier (E2M9) - Exit',
+ 'The Glacier (E2M9) - Firemace',
+ 'The Glacier (E2M9) - Firemace 2',
+ 'The Glacier (E2M9) - Firemace 3',
+ 'The Glacier (E2M9) - Firemace 4',
+ 'The Glacier (E2M9) - Gauntlets of the Necromancer',
+ 'The Glacier (E2M9) - Green key',
+ 'The Glacier (E2M9) - Hellstaff',
+ 'The Glacier (E2M9) - Map Scroll',
+ 'The Glacier (E2M9) - Morph Ovum',
+ 'The Glacier (E2M9) - Mystic Urn',
+ 'The Glacier (E2M9) - Mystic Urn 2',
+ 'The Glacier (E2M9) - Phoenix Rod',
+ 'The Glacier (E2M9) - Ring of Invincibility',
+ 'The Glacier (E2M9) - Shadowsphere',
+ 'The Glacier (E2M9) - Silver Shield',
+ 'The Glacier (E2M9) - Tome of Power',
+ 'The Glacier (E2M9) - Tome of Power 2',
+ 'The Glacier (E2M9) - Torch',
+ 'The Glacier (E2M9) - Torch 2',
+ 'The Glacier (E2M9) - Yellow key',
+ },
+ 'The Graveyard (E1M9)': {
+ 'The Graveyard (E1M9) - Bag of Holding',
+ 'The Graveyard (E1M9) - Blue key',
+ 'The Graveyard (E1M9) - Dragon Claw',
+ 'The Graveyard (E1M9) - Dragon Claw 2',
+ 'The Graveyard (E1M9) - Ethereal Crossbow',
+ 'The Graveyard (E1M9) - Exit',
+ 'The Graveyard (E1M9) - Green key',
+ 'The Graveyard (E1M9) - Map Scroll',
+ 'The Graveyard (E1M9) - Morph Ovum',
+ 'The Graveyard (E1M9) - Ring of Invincibility',
+ 'The Graveyard (E1M9) - Shadowsphere',
+ 'The Graveyard (E1M9) - Silver Shield',
+ 'The Graveyard (E1M9) - Tome of Power',
+ 'The Graveyard (E1M9) - Tome of Power 2',
+ 'The Graveyard (E1M9) - Torch',
+ 'The Graveyard (E1M9) - Yellow key',
+ },
+ 'The Great Hall (E2M7)': {
+ 'The Great Hall (E2M7) - Bag of Holding',
+ 'The Great Hall (E2M7) - Blue key',
+ 'The Great Hall (E2M7) - Chaos Device',
+ 'The Great Hall (E2M7) - Dragon Claw',
+ 'The Great Hall (E2M7) - Enchanted Shield',
+ 'The Great Hall (E2M7) - Ethereal Crossbow',
+ 'The Great Hall (E2M7) - Exit',
+ 'The Great Hall (E2M7) - Gauntlets of the Necromancer',
+ 'The Great Hall (E2M7) - Green key',
+ 'The Great Hall (E2M7) - Hellstaff',
+ 'The Great Hall (E2M7) - Map Scroll',
+ 'The Great Hall (E2M7) - Morph Ovum',
+ 'The Great Hall (E2M7) - Mystic Urn',
+ 'The Great Hall (E2M7) - Phoenix Rod',
+ 'The Great Hall (E2M7) - Ring of Invincibility',
+ 'The Great Hall (E2M7) - Shadowsphere',
+ 'The Great Hall (E2M7) - Silver Shield',
+ 'The Great Hall (E2M7) - Tome of Power',
+ 'The Great Hall (E2M7) - Tome of Power 2',
+ 'The Great Hall (E2M7) - Torch',
+ 'The Great Hall (E2M7) - Yellow key',
+ },
+ 'The Guard Tower (E1M4)': {
+ 'The Guard Tower (E1M4) - Bag of Holding',
+ 'The Guard Tower (E1M4) - Dragon Claw',
+ 'The Guard Tower (E1M4) - Ethereal Crossbow',
+ 'The Guard Tower (E1M4) - Exit',
+ 'The Guard Tower (E1M4) - Gauntlets of the Necromancer',
+ 'The Guard Tower (E1M4) - Green key',
+ 'The Guard Tower (E1M4) - Map Scroll',
+ 'The Guard Tower (E1M4) - Morph Ovum',
+ 'The Guard Tower (E1M4) - Shadowsphere',
+ 'The Guard Tower (E1M4) - Silver Shield',
+ 'The Guard Tower (E1M4) - Tome of Power',
+ 'The Guard Tower (E1M4) - Tome of Power 2',
+ 'The Guard Tower (E1M4) - Tome of Power 3',
+ 'The Guard Tower (E1M4) - Torch',
+ 'The Guard Tower (E1M4) - Yellow key',
+ },
+ 'The Halls of Fear (E3M6)': {
+ 'The Halls of Fear (E3M6) - Bag of Holding',
+ 'The Halls of Fear (E3M6) - Bag of Holding 2',
+ 'The Halls of Fear (E3M6) - Bag of Holding 3',
+ 'The Halls of Fear (E3M6) - Blue key',
+ 'The Halls of Fear (E3M6) - Chaos Device',
+ 'The Halls of Fear (E3M6) - Dragon Claw',
+ 'The Halls of Fear (E3M6) - Enchanted Shield',
+ 'The Halls of Fear (E3M6) - Ethereal Crossbow',
+ 'The Halls of Fear (E3M6) - Exit',
+ 'The Halls of Fear (E3M6) - Firemace',
+ 'The Halls of Fear (E3M6) - Firemace 2',
+ 'The Halls of Fear (E3M6) - Firemace 3',
+ 'The Halls of Fear (E3M6) - Firemace 4',
+ 'The Halls of Fear (E3M6) - Firemace 5',
+ 'The Halls of Fear (E3M6) - Firemace 6',
+ 'The Halls of Fear (E3M6) - Gauntlets of the Necromancer',
+ 'The Halls of Fear (E3M6) - Green key',
+ 'The Halls of Fear (E3M6) - Hellstaff',
+ 'The Halls of Fear (E3M6) - Hellstaff 2',
+ 'The Halls of Fear (E3M6) - Map Scroll',
+ 'The Halls of Fear (E3M6) - Morph Ovum',
+ 'The Halls of Fear (E3M6) - Mystic Urn',
+ 'The Halls of Fear (E3M6) - Mystic Urn 2',
+ 'The Halls of Fear (E3M6) - Phoenix Rod',
+ 'The Halls of Fear (E3M6) - Ring of Invincibility',
+ 'The Halls of Fear (E3M6) - Shadowsphere',
+ 'The Halls of Fear (E3M6) - Silver Shield',
+ 'The Halls of Fear (E3M6) - Tome of Power',
+ 'The Halls of Fear (E3M6) - Tome of Power 2',
+ 'The Halls of Fear (E3M6) - Tome of Power 3',
+ 'The Halls of Fear (E3M6) - Yellow key',
+ },
+ 'The Ice Grotto (E2M4)': {
+ 'The Ice Grotto (E2M4) - Bag of Holding',
+ 'The Ice Grotto (E2M4) - Bag of Holding 2',
+ 'The Ice Grotto (E2M4) - Blue key',
+ 'The Ice Grotto (E2M4) - Chaos Device',
+ 'The Ice Grotto (E2M4) - Dragon Claw',
+ 'The Ice Grotto (E2M4) - Enchanted Shield',
+ 'The Ice Grotto (E2M4) - Ethereal Crossbow',
+ 'The Ice Grotto (E2M4) - Exit',
+ 'The Ice Grotto (E2M4) - Gauntlets of the Necromancer',
+ 'The Ice Grotto (E2M4) - Green key',
+ 'The Ice Grotto (E2M4) - Hellstaff',
+ 'The Ice Grotto (E2M4) - Map Scroll',
+ 'The Ice Grotto (E2M4) - Morph Ovum',
+ 'The Ice Grotto (E2M4) - Mystic Urn',
+ 'The Ice Grotto (E2M4) - Phoenix Rod',
+ 'The Ice Grotto (E2M4) - Shadowsphere',
+ 'The Ice Grotto (E2M4) - Shadowsphere 2',
+ 'The Ice Grotto (E2M4) - Silver Shield',
+ 'The Ice Grotto (E2M4) - Tome of Power',
+ 'The Ice Grotto (E2M4) - Tome of Power 2',
+ 'The Ice Grotto (E2M4) - Tome of Power 3',
+ 'The Ice Grotto (E2M4) - Torch',
+ 'The Ice Grotto (E2M4) - Yellow key',
+ },
+ 'The Labyrinth (E2M6)': {
+ 'The Labyrinth (E2M6) - Bag of Holding',
+ 'The Labyrinth (E2M6) - Blue key',
+ 'The Labyrinth (E2M6) - Chaos Device',
+ 'The Labyrinth (E2M6) - Dragon Claw',
+ 'The Labyrinth (E2M6) - Enchanted Shield',
+ 'The Labyrinth (E2M6) - Ethereal Crossbow',
+ 'The Labyrinth (E2M6) - Exit',
+ 'The Labyrinth (E2M6) - Firemace',
+ 'The Labyrinth (E2M6) - Firemace 2',
+ 'The Labyrinth (E2M6) - Firemace 3',
+ 'The Labyrinth (E2M6) - Firemace 4',
+ 'The Labyrinth (E2M6) - Gauntlets of the Necromancer',
+ 'The Labyrinth (E2M6) - Green key',
+ 'The Labyrinth (E2M6) - Hellstaff',
+ 'The Labyrinth (E2M6) - Map Scroll',
+ 'The Labyrinth (E2M6) - Morph Ovum',
+ 'The Labyrinth (E2M6) - Mystic Urn',
+ 'The Labyrinth (E2M6) - Phoenix Rod',
+ 'The Labyrinth (E2M6) - Phoenix Rod 2',
+ 'The Labyrinth (E2M6) - Ring of Invincibility',
+ 'The Labyrinth (E2M6) - Shadowsphere',
+ 'The Labyrinth (E2M6) - Silver Shield',
+ 'The Labyrinth (E2M6) - Tome of Power',
+ 'The Labyrinth (E2M6) - Tome of Power 2',
+ 'The Labyrinth (E2M6) - Yellow key',
+ },
+ 'The Lava Pits (E2M2)': {
+ 'The Lava Pits (E2M2) - Bag of Holding',
+ 'The Lava Pits (E2M2) - Bag of Holding 2',
+ 'The Lava Pits (E2M2) - Chaos Device',
+ 'The Lava Pits (E2M2) - Dragon Claw',
+ 'The Lava Pits (E2M2) - Enchanted Shield',
+ 'The Lava Pits (E2M2) - Ethereal Crossbow',
+ 'The Lava Pits (E2M2) - Exit',
+ 'The Lava Pits (E2M2) - Gauntlets of the Necromancer',
+ 'The Lava Pits (E2M2) - Green key',
+ 'The Lava Pits (E2M2) - Hellstaff',
+ 'The Lava Pits (E2M2) - Map Scroll',
+ 'The Lava Pits (E2M2) - Morph Ovum',
+ 'The Lava Pits (E2M2) - Mystic Urn',
+ 'The Lava Pits (E2M2) - Ring of Invincibility',
+ 'The Lava Pits (E2M2) - Shadowsphere',
+ 'The Lava Pits (E2M2) - Silver Shield',
+ 'The Lava Pits (E2M2) - Silver Shield 2',
+ 'The Lava Pits (E2M2) - Tome of Power',
+ 'The Lava Pits (E2M2) - Tome of Power 2',
+ 'The Lava Pits (E2M2) - Tome of Power 3',
+ 'The Lava Pits (E2M2) - Yellow key',
+ },
+ 'The Ophidian Lair (E3M5)': {
+ 'The Ophidian Lair (E3M5) - Bag of Holding',
+ 'The Ophidian Lair (E3M5) - Chaos Device',
+ 'The Ophidian Lair (E3M5) - Dragon Claw',
+ 'The Ophidian Lair (E3M5) - Enchanted Shield',
+ 'The Ophidian Lair (E3M5) - Ethereal Crossbow',
+ 'The Ophidian Lair (E3M5) - Exit',
+ 'The Ophidian Lair (E3M5) - Gauntlets of the Necromancer',
+ 'The Ophidian Lair (E3M5) - Green key',
+ 'The Ophidian Lair (E3M5) - Hellstaff',
+ 'The Ophidian Lair (E3M5) - Map Scroll',
+ 'The Ophidian Lair (E3M5) - Morph Ovum',
+ 'The Ophidian Lair (E3M5) - Mystic Urn',
+ 'The Ophidian Lair (E3M5) - Mystic Urn 2',
+ 'The Ophidian Lair (E3M5) - Phoenix Rod',
+ 'The Ophidian Lair (E3M5) - Ring of Invincibility',
+ 'The Ophidian Lair (E3M5) - Shadowsphere',
+ 'The Ophidian Lair (E3M5) - Silver Shield',
+ 'The Ophidian Lair (E3M5) - Silver Shield 2',
+ 'The Ophidian Lair (E3M5) - Tome of Power',
+ 'The Ophidian Lair (E3M5) - Tome of Power 2',
+ 'The Ophidian Lair (E3M5) - Torch',
+ 'The Ophidian Lair (E3M5) - Yellow key',
+ },
+ 'The Portals of Chaos (E2M8)': {
+ 'The Portals of Chaos (E2M8) - Bag of Holding',
+ 'The Portals of Chaos (E2M8) - Chaos Device',
+ 'The Portals of Chaos (E2M8) - Dragon Claw',
+ 'The Portals of Chaos (E2M8) - Enchanted Shield',
+ 'The Portals of Chaos (E2M8) - Ethereal Crossbow',
+ 'The Portals of Chaos (E2M8) - Exit',
+ 'The Portals of Chaos (E2M8) - Gauntlets of the Necromancer',
+ 'The Portals of Chaos (E2M8) - Hellstaff',
+ 'The Portals of Chaos (E2M8) - Morph Ovum',
+ 'The Portals of Chaos (E2M8) - Mystic Urn',
+ 'The Portals of Chaos (E2M8) - Mystic Urn 2',
+ 'The Portals of Chaos (E2M8) - Phoenix Rod',
+ 'The Portals of Chaos (E2M8) - Ring of Invincibility',
+ 'The Portals of Chaos (E2M8) - Shadowsphere',
+ 'The Portals of Chaos (E2M8) - Silver Shield',
+ 'The Portals of Chaos (E2M8) - Tome of Power',
+ },
+ 'The River of Fire (E2M3)': {
+ 'The River of Fire (E2M3) - Bag of Holding',
+ 'The River of Fire (E2M3) - Blue key',
+ 'The River of Fire (E2M3) - Chaos Device',
+ 'The River of Fire (E2M3) - Dragon Claw',
+ 'The River of Fire (E2M3) - Enchanted Shield',
+ 'The River of Fire (E2M3) - Ethereal Crossbow',
+ 'The River of Fire (E2M3) - Exit',
+ 'The River of Fire (E2M3) - Firemace',
+ 'The River of Fire (E2M3) - Firemace 2',
+ 'The River of Fire (E2M3) - Firemace 3',
+ 'The River of Fire (E2M3) - Gauntlets of the Necromancer',
+ 'The River of Fire (E2M3) - Green key',
+ 'The River of Fire (E2M3) - Hellstaff',
+ 'The River of Fire (E2M3) - Morph Ovum',
+ 'The River of Fire (E2M3) - Mystic Urn',
+ 'The River of Fire (E2M3) - Phoenix Rod',
+ 'The River of Fire (E2M3) - Ring of Invincibility',
+ 'The River of Fire (E2M3) - Shadowsphere',
+ 'The River of Fire (E2M3) - Silver Shield',
+ 'The River of Fire (E2M3) - Tome of Power',
+ 'The River of Fire (E2M3) - Tome of Power 2',
+ 'The River of Fire (E2M3) - Yellow key',
+ },
+ 'The Storehouse (E3M1)': {
+ 'The Storehouse (E3M1) - Bag of Holding',
+ 'The Storehouse (E3M1) - Chaos Device',
+ 'The Storehouse (E3M1) - Dragon Claw',
+ 'The Storehouse (E3M1) - Exit',
+ 'The Storehouse (E3M1) - Gauntlets of the Necromancer',
+ 'The Storehouse (E3M1) - Green key',
+ 'The Storehouse (E3M1) - Hellstaff',
+ 'The Storehouse (E3M1) - Map Scroll',
+ 'The Storehouse (E3M1) - Ring of Invincibility',
+ 'The Storehouse (E3M1) - Shadowsphere',
+ 'The Storehouse (E3M1) - Silver Shield',
+ 'The Storehouse (E3M1) - Tome of Power',
+ 'The Storehouse (E3M1) - Torch',
+ 'The Storehouse (E3M1) - Yellow key',
+ },
+}
+
+
+death_logic_locations = [
+ "Ramparts of Perdition (E4M7) - Ring of Invincibility",
+ "Ramparts of Perdition (E4M7) - Ethereal Crossbow 2",
+]
diff --git a/worlds/heretic/Maps.py b/worlds/heretic/Maps.py
new file mode 100644
index 000000000000..716de2904144
--- /dev/null
+++ b/worlds/heretic/Maps.py
@@ -0,0 +1,52 @@
+# This file is auto generated. More info: https://github.com/Daivuk/apdoom
+
+from typing import List
+
+
+map_names: List[str] = [
+ 'The Docks (E1M1)',
+ 'The Dungeons (E1M2)',
+ 'The Gatehouse (E1M3)',
+ 'The Guard Tower (E1M4)',
+ 'The Citadel (E1M5)',
+ 'The Cathedral (E1M6)',
+ 'The Crypts (E1M7)',
+ "Hell's Maw (E1M8)",
+ 'The Graveyard (E1M9)',
+ 'The Crater (E2M1)',
+ 'The Lava Pits (E2M2)',
+ 'The River of Fire (E2M3)',
+ 'The Ice Grotto (E2M4)',
+ 'The Catacombs (E2M5)',
+ 'The Labyrinth (E2M6)',
+ 'The Great Hall (E2M7)',
+ 'The Portals of Chaos (E2M8)',
+ 'The Glacier (E2M9)',
+ 'The Storehouse (E3M1)',
+ 'The Cesspool (E3M2)',
+ 'The Confluence (E3M3)',
+ 'The Azure Fortress (E3M4)',
+ 'The Ophidian Lair (E3M5)',
+ 'The Halls of Fear (E3M6)',
+ 'The Chasm (E3M7)',
+ "D'Sparil'S Keep (E3M8)",
+ 'The Aquifier (E3M9)',
+ 'Catafalque (E4M1)',
+ 'Blockhouse (E4M2)',
+ 'Ambulatory (E4M3)',
+ 'Sepulcher (E4M4)',
+ 'Great Stair (E4M5)',
+ 'Halls of the Apostate (E4M6)',
+ 'Ramparts of Perdition (E4M7)',
+ 'Shattered Bridge (E4M8)',
+ 'Mausoleum (E4M9)',
+ 'Ochre Cliffs (E5M1)',
+ 'Rapids (E5M2)',
+ 'Quay (E5M3)',
+ 'Courtyard (E5M4)',
+ 'Hydratyr (E5M5)',
+ 'Colonnade (E5M6)',
+ 'Foetid Manse (E5M7)',
+ 'Field of Judgement (E5M8)',
+ "Skein of D'Sparil (E5M9)",
+]
diff --git a/worlds/heretic/Options.py b/worlds/heretic/Options.py
new file mode 100644
index 000000000000..75e2257a7336
--- /dev/null
+++ b/worlds/heretic/Options.py
@@ -0,0 +1,166 @@
+from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
+from dataclasses import dataclass
+
+
+class Goal(Choice):
+ """
+ Choose the main goal.
+ complete_all_levels: All levels of the selected episodes
+ complete_boss_levels: Boss levels (E#M8) of selected episodes
+ """
+ display_name = "Goal"
+ option_complete_all_levels = 0
+ option_complete_boss_levels = 1
+ default = 0
+
+
+class Difficulty(Choice):
+ """
+ Choose the difficulty option. Those match DOOM's difficulty options.
+ baby (I'm too young to die.) double ammos, half damage, less monsters or strength.
+ easy (Hey, not too rough.) less monsters or strength.
+ medium (Hurt me plenty.) Default.
+ hard (Ultra-Violence.) More monsters or strength.
+ nightmare (Nightmare!) Monsters attack more rapidly and respawn.
+
+ wet nurse (hou needeth a wet-nurse) - Fewer monsters and more items than medium. Damage taken is halved, and ammo pickups carry twice as much ammo. Any Quartz Flasks and Mystic Urns are automatically used when the player nears death.
+ easy (Yellowbellies-r-us) - Fewer monsters and more items than medium.
+ medium (Bringest them oneth) - Completely balanced, this is the standard difficulty level.
+ hard (Thou art a smite-meister) - More monsters and fewer items than medium.
+ black plague (Black plague possesses thee) - Same as hard, but monsters and their projectiles move much faster. Cheating is also disabled.
+ """
+ display_name = "Difficulty"
+ option_wet_nurse = 0
+ option_easy = 1
+ option_medium = 2
+ option_hard = 3
+ option_black_plague = 4
+ default = 2
+
+
+class RandomMonsters(Choice):
+ """
+ Choose how monsters are randomized.
+ vanilla: No randomization
+ shuffle: Monsters are shuffled within the level
+ random_balanced: Monsters are completely randomized, but balanced based on existing ratio in the level. (Small monsters vs medium vs big)
+ random_chaotic: Monsters are completely randomized, but balanced based on existing ratio in the entire game.
+ """
+ display_name = "Random Monsters"
+ option_vanilla = 0
+ option_shuffle = 1
+ option_random_balanced = 2
+ option_random_chaotic = 3
+ default = 1
+
+
+class RandomPickups(Choice):
+ """
+ Choose how pickups are randomized.
+ vanilla: No randomization
+ shuffle: Pickups are shuffled within the level
+ random_balanced: Pickups are completely randomized, but balanced based on existing ratio in the level. (Small pickups vs Big)
+ """
+ display_name = "Random Pickups"
+ option_vanilla = 0
+ option_shuffle = 1
+ option_random_balanced = 2
+ default = 1
+
+
+class RandomMusic(Choice):
+ """
+ Level musics will be randomized.
+ vanilla: No randomization
+ shuffle_selected: Selected episodes' levels will be shuffled
+ shuffle_game: All the music will be shuffled
+ """
+ display_name = "Random Music"
+ option_vanilla = 0
+ option_shuffle_selected = 1
+ option_shuffle_game = 2
+ default = 0
+
+
+class AllowDeathLogic(Toggle):
+ """Some locations require a timed puzzle that can only be tried once.
+ After which, if the player failed to get it, the location cannot be checked anymore.
+ By default, no progression items are placed here. There is a way, hovewer, to still get them:
+ Get killed in the current map. The map will reset, you can now attempt the puzzle again."""
+ display_name = "Allow Death Logic"
+
+
+class Pro(Toggle):
+ """Include difficult tricks into rules. Mostly employed by speed runners.
+ i.e.: Leaps across to a locked area, trigger a switch behind a window at the right angle, etc."""
+ display_name = "Pro Heretic"
+
+
+class StartWithMapScrolls(Toggle):
+ """Give the player all Map Scroll items from the start."""
+ display_name = "Start With Map Scrolls"
+
+
+class ResetLevelOnDeath(DefaultOnToggle):
+ """When dying, levels are reset and monsters respawned. But inventory and checks are kept.
+ Turning this setting off is considered easy mode. Good for new players that don't know the levels well."""
+ display_message="Reset level on death"
+
+
+class CheckSanity(Toggle):
+ """Include redundant checks. This increase total check count for the game.
+ i.e.: In a room, there might be 3 checks close to each other. By default, two of them will be remove.
+ This was done to lower the total count check for Heretic, as it is quite high compared to other games.
+ Check Sanity restores original checks."""
+ display_name = "Check Sanity"
+
+
+class Episode1(DefaultOnToggle):
+ """City of the Damned.
+ If none of the episodes are chosen, Episode 1 will be chosen by default."""
+ display_name = "Episode 1"
+
+
+class Episode2(DefaultOnToggle):
+ """Hell's Maw.
+ If none of the episodes are chosen, Episode 1 will be chosen by default."""
+ display_name = "Episode 2"
+
+
+class Episode3(DefaultOnToggle):
+ """The Dome of D'Sparil.
+ If none of the episodes are chosen, Episode 1 will be chosen by default."""
+ display_name = "Episode 3"
+
+
+class Episode4(Toggle):
+ """The Ossuary.
+ If none of the episodes are chosen, Episode 1 will be chosen by default."""
+ display_name = "Episode 4"
+
+
+class Episode5(Toggle):
+ """The Stagnant Demesne.
+ If none of the episodes are chosen, Episode 1 will be chosen by default."""
+ display_name = "Episode 5"
+
+
+@dataclass
+class HereticOptions(PerGameCommonOptions):
+ start_inventory_from_pool: StartInventoryPool
+ goal: Goal
+ difficulty: Difficulty
+ random_monsters: RandomMonsters
+ random_pickups: RandomPickups
+ random_music: RandomMusic
+ allow_death_logic: AllowDeathLogic
+ pro: Pro
+ check_sanity: CheckSanity
+ start_with_map_scrolls: StartWithMapScrolls
+ reset_level_on_death: ResetLevelOnDeath
+ death_link: DeathLink
+ episode1: Episode1
+ episode2: Episode2
+ episode3: Episode3
+ episode4: Episode4
+ episode5: Episode5
diff --git a/worlds/heretic/Regions.py b/worlds/heretic/Regions.py
new file mode 100644
index 000000000000..81a4c9ce49dc
--- /dev/null
+++ b/worlds/heretic/Regions.py
@@ -0,0 +1,903 @@
+# This file is auto generated. More info: https://github.com/Daivuk/apdoom
+
+from typing import List
+from BaseClasses import TypedDict
+
+class ConnectionDict(TypedDict, total=False):
+ target: str
+ pro: bool
+
+class RegionDict(TypedDict, total=False):
+ name: str
+ connects_to_hub: bool
+ episode: int
+ connections: List[ConnectionDict]
+
+
+regions:List[RegionDict] = [
+ # The Docks (E1M1)
+ {"name":"The Docks (E1M1) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[{"target":"The Docks (E1M1) Yellow","pro":False}]},
+ {"name":"The Docks (E1M1) Yellow",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Docks (E1M1) Main","pro":False},
+ {"target":"The Docks (E1M1) Sea","pro":False}]},
+ {"name":"The Docks (E1M1) Sea",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Docks (E1M1) Main","pro":False}]},
+
+ # The Dungeons (E1M2)
+ {"name":"The Dungeons (E1M2) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[
+ {"target":"The Dungeons (E1M2) Yellow","pro":False},
+ {"target":"The Dungeons (E1M2) Green","pro":False}]},
+ {"name":"The Dungeons (E1M2) Blue",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Dungeons (E1M2) Yellow","pro":False}]},
+ {"name":"The Dungeons (E1M2) Yellow",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Dungeons (E1M2) Main","pro":False},
+ {"target":"The Dungeons (E1M2) Blue","pro":False}]},
+ {"name":"The Dungeons (E1M2) Green",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Dungeons (E1M2) Main","pro":False},
+ {"target":"The Dungeons (E1M2) Yellow","pro":False}]},
+
+ # The Gatehouse (E1M3)
+ {"name":"The Gatehouse (E1M3) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[
+ {"target":"The Gatehouse (E1M3) Yellow","pro":False},
+ {"target":"The Gatehouse (E1M3) Sea","pro":False},
+ {"target":"The Gatehouse (E1M3) Green","pro":False}]},
+ {"name":"The Gatehouse (E1M3) Yellow",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Gatehouse (E1M3) Main","pro":False}]},
+ {"name":"The Gatehouse (E1M3) Green",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Gatehouse (E1M3) Main","pro":False}]},
+ {"name":"The Gatehouse (E1M3) Sea",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Gatehouse (E1M3) Main","pro":False}]},
+
+ # The Guard Tower (E1M4)
+ {"name":"The Guard Tower (E1M4) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[{"target":"The Guard Tower (E1M4) Yellow","pro":False}]},
+ {"name":"The Guard Tower (E1M4) Yellow",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Guard Tower (E1M4) Green","pro":False},
+ {"target":"The Guard Tower (E1M4) Main","pro":False}]},
+ {"name":"The Guard Tower (E1M4) Green",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Guard Tower (E1M4) Yellow","pro":False}]},
+
+ # The Citadel (E1M5)
+ {"name":"The Citadel (E1M5) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[{"target":"The Citadel (E1M5) Yellow","pro":False}]},
+ {"name":"The Citadel (E1M5) Blue",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Citadel (E1M5) Green","pro":False}]},
+ {"name":"The Citadel (E1M5) Yellow",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Citadel (E1M5) Main","pro":False},
+ {"target":"The Citadel (E1M5) Well","pro":False},
+ {"target":"The Citadel (E1M5) Green","pro":False}]},
+ {"name":"The Citadel (E1M5) Green",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Citadel (E1M5) Main","pro":False},
+ {"target":"The Citadel (E1M5) Well","pro":False},
+ {"target":"The Citadel (E1M5) Blue","pro":False}]},
+ {"name":"The Citadel (E1M5) Well",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Citadel (E1M5) Main","pro":False}]},
+
+ # The Cathedral (E1M6)
+ {"name":"The Cathedral (E1M6) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[{"target":"The Cathedral (E1M6) Yellow","pro":False}]},
+ {"name":"The Cathedral (E1M6) Yellow",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Cathedral (E1M6) Green","pro":False},
+ {"target":"The Cathedral (E1M6) Main","pro":False},
+ {"target":"The Cathedral (E1M6) Main Fly","pro":False}]},
+ {"name":"The Cathedral (E1M6) Green",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Cathedral (E1M6) Yellow","pro":False},
+ {"target":"The Cathedral (E1M6) Main Fly","pro":False}]},
+ {"name":"The Cathedral (E1M6) Main Fly",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Cathedral (E1M6) Main","pro":False}]},
+
+ # The Crypts (E1M7)
+ {"name":"The Crypts (E1M7) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[
+ {"target":"The Crypts (E1M7) Yellow","pro":False},
+ {"target":"The Crypts (E1M7) Green","pro":False}]},
+ {"name":"The Crypts (E1M7) Blue",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Crypts (E1M7) Yellow","pro":False},
+ {"target":"The Crypts (E1M7) Main","pro":False}]},
+ {"name":"The Crypts (E1M7) Yellow",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Crypts (E1M7) Main","pro":False},
+ {"target":"The Crypts (E1M7) Green","pro":False},
+ {"target":"The Crypts (E1M7) Blue","pro":False}]},
+ {"name":"The Crypts (E1M7) Green",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[
+ {"target":"The Crypts (E1M7) Yellow","pro":False},
+ {"target":"The Crypts (E1M7) Main","pro":False}]},
+
+ # Hell's Maw (E1M8)
+ {"name":"Hell's Maw (E1M8) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[]},
+
+ # The Graveyard (E1M9)
+ {"name":"The Graveyard (E1M9) Main",
+ "connects_to_hub":True,
+ "episode":1,
+ "connections":[
+ {"target":"The Graveyard (E1M9) Yellow","pro":False},
+ {"target":"The Graveyard (E1M9) Green","pro":False},
+ {"target":"The Graveyard (E1M9) Blue","pro":False}]},
+ {"name":"The Graveyard (E1M9) Blue",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Graveyard (E1M9) Main","pro":False}]},
+ {"name":"The Graveyard (E1M9) Yellow",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Graveyard (E1M9) Main","pro":False}]},
+ {"name":"The Graveyard (E1M9) Green",
+ "connects_to_hub":False,
+ "episode":1,
+ "connections":[{"target":"The Graveyard (E1M9) Main","pro":False}]},
+
+ # The Crater (E2M1)
+ {"name":"The Crater (E2M1) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[{"target":"The Crater (E2M1) Yellow","pro":False}]},
+ {"name":"The Crater (E2M1) Yellow",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[
+ {"target":"The Crater (E2M1) Main","pro":False},
+ {"target":"The Crater (E2M1) Green","pro":False}]},
+ {"name":"The Crater (E2M1) Green",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Crater (E2M1) Yellow","pro":False}]},
+
+ # The Lava Pits (E2M2)
+ {"name":"The Lava Pits (E2M2) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[{"target":"The Lava Pits (E2M2) Yellow","pro":False}]},
+ {"name":"The Lava Pits (E2M2) Yellow",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[
+ {"target":"The Lava Pits (E2M2) Green","pro":False},
+ {"target":"The Lava Pits (E2M2) Main","pro":False}]},
+ {"name":"The Lava Pits (E2M2) Green",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[
+ {"target":"The Lava Pits (E2M2) Main","pro":False},
+ {"target":"The Lava Pits (E2M2) Yellow","pro":False}]},
+
+ # The River of Fire (E2M3)
+ {"name":"The River of Fire (E2M3) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[
+ {"target":"The River of Fire (E2M3) Yellow","pro":False},
+ {"target":"The River of Fire (E2M3) Blue","pro":False},
+ {"target":"The River of Fire (E2M3) Green","pro":False}]},
+ {"name":"The River of Fire (E2M3) Blue",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The River of Fire (E2M3) Main","pro":False}]},
+ {"name":"The River of Fire (E2M3) Yellow",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The River of Fire (E2M3) Main","pro":False}]},
+ {"name":"The River of Fire (E2M3) Green",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The River of Fire (E2M3) Main","pro":False}]},
+
+ # The Ice Grotto (E2M4)
+ {"name":"The Ice Grotto (E2M4) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[
+ {"target":"The Ice Grotto (E2M4) Green","pro":False},
+ {"target":"The Ice Grotto (E2M4) Yellow","pro":False}]},
+ {"name":"The Ice Grotto (E2M4) Blue",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Ice Grotto (E2M4) Green","pro":False}]},
+ {"name":"The Ice Grotto (E2M4) Yellow",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[
+ {"target":"The Ice Grotto (E2M4) Main","pro":False},
+ {"target":"The Ice Grotto (E2M4) Magenta","pro":False}]},
+ {"name":"The Ice Grotto (E2M4) Green",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[
+ {"target":"The Ice Grotto (E2M4) Main","pro":False},
+ {"target":"The Ice Grotto (E2M4) Blue","pro":False}]},
+ {"name":"The Ice Grotto (E2M4) Magenta",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Ice Grotto (E2M4) Yellow","pro":False}]},
+
+ # The Catacombs (E2M5)
+ {"name":"The Catacombs (E2M5) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[{"target":"The Catacombs (E2M5) Yellow","pro":False}]},
+ {"name":"The Catacombs (E2M5) Blue",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Catacombs (E2M5) Green","pro":False}]},
+ {"name":"The Catacombs (E2M5) Yellow",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[
+ {"target":"The Catacombs (E2M5) Green","pro":False},
+ {"target":"The Catacombs (E2M5) Main","pro":False}]},
+ {"name":"The Catacombs (E2M5) Green",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[
+ {"target":"The Catacombs (E2M5) Blue","pro":False},
+ {"target":"The Catacombs (E2M5) Yellow","pro":False},
+ {"target":"The Catacombs (E2M5) Main","pro":False}]},
+
+ # The Labyrinth (E2M6)
+ {"name":"The Labyrinth (E2M6) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[
+ {"target":"The Labyrinth (E2M6) Blue","pro":False},
+ {"target":"The Labyrinth (E2M6) Yellow","pro":False},
+ {"target":"The Labyrinth (E2M6) Green","pro":False}]},
+ {"name":"The Labyrinth (E2M6) Blue",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Labyrinth (E2M6) Main","pro":False}]},
+ {"name":"The Labyrinth (E2M6) Yellow",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Labyrinth (E2M6) Main","pro":False}]},
+ {"name":"The Labyrinth (E2M6) Green",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Labyrinth (E2M6) Main","pro":False}]},
+
+ # The Great Hall (E2M7)
+ {"name":"The Great Hall (E2M7) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[
+ {"target":"The Great Hall (E2M7) Yellow","pro":False},
+ {"target":"The Great Hall (E2M7) Green","pro":False}]},
+ {"name":"The Great Hall (E2M7) Blue",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Great Hall (E2M7) Yellow","pro":False}]},
+ {"name":"The Great Hall (E2M7) Yellow",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[
+ {"target":"The Great Hall (E2M7) Blue","pro":False},
+ {"target":"The Great Hall (E2M7) Main","pro":False}]},
+ {"name":"The Great Hall (E2M7) Green",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Great Hall (E2M7) Main","pro":False}]},
+
+ # The Portals of Chaos (E2M8)
+ {"name":"The Portals of Chaos (E2M8) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[]},
+
+ # The Glacier (E2M9)
+ {"name":"The Glacier (E2M9) Main",
+ "connects_to_hub":True,
+ "episode":2,
+ "connections":[
+ {"target":"The Glacier (E2M9) Yellow","pro":False},
+ {"target":"The Glacier (E2M9) Blue","pro":False},
+ {"target":"The Glacier (E2M9) Green","pro":False}]},
+ {"name":"The Glacier (E2M9) Blue",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Glacier (E2M9) Main","pro":False}]},
+ {"name":"The Glacier (E2M9) Yellow",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Glacier (E2M9) Main","pro":False}]},
+ {"name":"The Glacier (E2M9) Green",
+ "connects_to_hub":False,
+ "episode":2,
+ "connections":[{"target":"The Glacier (E2M9) Main","pro":False}]},
+
+ # The Storehouse (E3M1)
+ {"name":"The Storehouse (E3M1) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[
+ {"target":"The Storehouse (E3M1) Yellow","pro":False},
+ {"target":"The Storehouse (E3M1) Green","pro":False}]},
+ {"name":"The Storehouse (E3M1) Yellow",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Storehouse (E3M1) Main","pro":False}]},
+ {"name":"The Storehouse (E3M1) Green",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Storehouse (E3M1) Main","pro":False}]},
+
+ # The Cesspool (E3M2)
+ {"name":"The Cesspool (E3M2) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[{"target":"The Cesspool (E3M2) Yellow","pro":False}]},
+ {"name":"The Cesspool (E3M2) Blue",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Cesspool (E3M2) Green","pro":False}]},
+ {"name":"The Cesspool (E3M2) Yellow",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ {"target":"The Cesspool (E3M2) Main","pro":False},
+ {"target":"The Cesspool (E3M2) Green","pro":False}]},
+ {"name":"The Cesspool (E3M2) Green",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ {"target":"The Cesspool (E3M2) Blue","pro":False},
+ {"target":"The Cesspool (E3M2) Main","pro":False},
+ {"target":"The Cesspool (E3M2) Yellow","pro":False}]},
+
+ # The Confluence (E3M3)
+ {"name":"The Confluence (E3M3) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[
+ {"target":"The Confluence (E3M3) Green","pro":False},
+ {"target":"The Confluence (E3M3) Yellow","pro":False}]},
+ {"name":"The Confluence (E3M3) Blue",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Confluence (E3M3) Green","pro":False}]},
+ {"name":"The Confluence (E3M3) Yellow",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Confluence (E3M3) Main","pro":False}]},
+ {"name":"The Confluence (E3M3) Green",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ {"target":"The Confluence (E3M3) Main","pro":False},
+ {"target":"The Confluence (E3M3) Blue","pro":False},
+ {"target":"The Confluence (E3M3) Yellow","pro":False}]},
+
+ # The Azure Fortress (E3M4)
+ {"name":"The Azure Fortress (E3M4) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[
+ {"target":"The Azure Fortress (E3M4) Green","pro":False},
+ {"target":"The Azure Fortress (E3M4) Yellow","pro":False}]},
+ {"name":"The Azure Fortress (E3M4) Yellow",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Azure Fortress (E3M4) Main","pro":False}]},
+ {"name":"The Azure Fortress (E3M4) Green",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Azure Fortress (E3M4) Main","pro":False}]},
+
+ # The Ophidian Lair (E3M5)
+ {"name":"The Ophidian Lair (E3M5) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[
+ {"target":"The Ophidian Lair (E3M5) Yellow","pro":False},
+ {"target":"The Ophidian Lair (E3M5) Green","pro":False}]},
+ {"name":"The Ophidian Lair (E3M5) Yellow",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Ophidian Lair (E3M5) Main","pro":False}]},
+ {"name":"The Ophidian Lair (E3M5) Green",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Ophidian Lair (E3M5) Main","pro":False}]},
+
+ # The Halls of Fear (E3M6)
+ {"name":"The Halls of Fear (E3M6) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[{"target":"The Halls of Fear (E3M6) Yellow","pro":False}]},
+ {"name":"The Halls of Fear (E3M6) Blue",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ {"target":"The Halls of Fear (E3M6) Yellow","pro":False},
+ {"target":"The Halls of Fear (E3M6) Cyan","pro":False}]},
+ {"name":"The Halls of Fear (E3M6) Yellow",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ {"target":"The Halls of Fear (E3M6) Blue","pro":False},
+ {"target":"The Halls of Fear (E3M6) Main","pro":False},
+ {"target":"The Halls of Fear (E3M6) Green","pro":False}]},
+ {"name":"The Halls of Fear (E3M6) Green",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ {"target":"The Halls of Fear (E3M6) Yellow","pro":False},
+ {"target":"The Halls of Fear (E3M6) Main","pro":False},
+ {"target":"The Halls of Fear (E3M6) Cyan","pro":False}]},
+ {"name":"The Halls of Fear (E3M6) Cyan",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ {"target":"The Halls of Fear (E3M6) Yellow","pro":False},
+ {"target":"The Halls of Fear (E3M6) Main","pro":False}]},
+
+ # The Chasm (E3M7)
+ {"name":"The Chasm (E3M7) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[{"target":"The Chasm (E3M7) Yellow","pro":False}]},
+ {"name":"The Chasm (E3M7) Blue",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[]},
+ {"name":"The Chasm (E3M7) Yellow",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ {"target":"The Chasm (E3M7) Main","pro":False},
+ {"target":"The Chasm (E3M7) Green","pro":False},
+ {"target":"The Chasm (E3M7) Blue","pro":False}]},
+ {"name":"The Chasm (E3M7) Green",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[{"target":"The Chasm (E3M7) Yellow","pro":False}]},
+
+ # D'Sparil'S Keep (E3M8)
+ {"name":"D'Sparil'S Keep (E3M8) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[]},
+
+ # The Aquifier (E3M9)
+ {"name":"The Aquifier (E3M9) Main",
+ "connects_to_hub":True,
+ "episode":3,
+ "connections":[{"target":"The Aquifier (E3M9) Yellow","pro":False}]},
+ {"name":"The Aquifier (E3M9) Blue",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[]},
+ {"name":"The Aquifier (E3M9) Yellow",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ {"target":"The Aquifier (E3M9) Green","pro":False},
+ {"target":"The Aquifier (E3M9) Main","pro":False}]},
+ {"name":"The Aquifier (E3M9) Green",
+ "connects_to_hub":False,
+ "episode":3,
+ "connections":[
+ {"target":"The Aquifier (E3M9) Yellow","pro":False},
+ {"target":"The Aquifier (E3M9) Main","pro":False},
+ {"target":"The Aquifier (E3M9) Blue","pro":False}]},
+
+ # Catafalque (E4M1)
+ {"name":"Catafalque (E4M1) Main",
+ "connects_to_hub":True,
+ "episode":4,
+ "connections":[{"target":"Catafalque (E4M1) Yellow","pro":False}]},
+ {"name":"Catafalque (E4M1) Yellow",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[
+ {"target":"Catafalque (E4M1) Green","pro":False},
+ {"target":"Catafalque (E4M1) Main","pro":False}]},
+ {"name":"Catafalque (E4M1) Green",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[{"target":"Catafalque (E4M1) Main","pro":False}]},
+
+ # Blockhouse (E4M2)
+ {"name":"Blockhouse (E4M2) Main",
+ "connects_to_hub":True,
+ "episode":4,
+ "connections":[
+ {"target":"Blockhouse (E4M2) Yellow","pro":False},
+ {"target":"Blockhouse (E4M2) Green","pro":False},
+ {"target":"Blockhouse (E4M2) Blue","pro":False}]},
+ {"name":"Blockhouse (E4M2) Yellow",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[
+ {"target":"Blockhouse (E4M2) Main","pro":False},
+ {"target":"Blockhouse (E4M2) Balcony","pro":False},
+ {"target":"Blockhouse (E4M2) Lake","pro":False}]},
+ {"name":"Blockhouse (E4M2) Green",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[{"target":"Blockhouse (E4M2) Main","pro":False}]},
+ {"name":"Blockhouse (E4M2) Blue",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[{"target":"Blockhouse (E4M2) Main","pro":False}]},
+ {"name":"Blockhouse (E4M2) Lake",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[{"target":"Blockhouse (E4M2) Balcony","pro":False}]},
+ {"name":"Blockhouse (E4M2) Balcony",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[]},
+
+ # Ambulatory (E4M3)
+ {"name":"Ambulatory (E4M3) Main",
+ "connects_to_hub":True,
+ "episode":4,
+ "connections":[
+ {"target":"Ambulatory (E4M3) Blue","pro":False},
+ {"target":"Ambulatory (E4M3) Yellow","pro":False},
+ {"target":"Ambulatory (E4M3) Green","pro":False},
+ {"target":"Ambulatory (E4M3) Green Lock","pro":False}]},
+ {"name":"Ambulatory (E4M3) Blue",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[
+ {"target":"Ambulatory (E4M3) Yellow","pro":False},
+ {"target":"Ambulatory (E4M3) Green","pro":False}]},
+ {"name":"Ambulatory (E4M3) Yellow",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[{"target":"Ambulatory (E4M3) Main","pro":False}]},
+ {"name":"Ambulatory (E4M3) Green",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[{"target":"Ambulatory (E4M3) Main","pro":False}]},
+ {"name":"Ambulatory (E4M3) Green Lock",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[
+ {"target":"Ambulatory (E4M3) Green","pro":False},
+ {"target":"Ambulatory (E4M3) Main","pro":False}]},
+
+ # Sepulcher (E4M4)
+ {"name":"Sepulcher (E4M4) Main",
+ "connects_to_hub":True,
+ "episode":4,
+ "connections":[]},
+
+ # Great Stair (E4M5)
+ {"name":"Great Stair (E4M5) Main",
+ "connects_to_hub":True,
+ "episode":4,
+ "connections":[{"target":"Great Stair (E4M5) Yellow","pro":False}]},
+ {"name":"Great Stair (E4M5) Blue",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[{"target":"Great Stair (E4M5) Green","pro":False}]},
+ {"name":"Great Stair (E4M5) Yellow",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[
+ {"target":"Great Stair (E4M5) Main","pro":False},
+ {"target":"Great Stair (E4M5) Green","pro":False}]},
+ {"name":"Great Stair (E4M5) Green",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[
+ {"target":"Great Stair (E4M5) Blue","pro":False},
+ {"target":"Great Stair (E4M5) Yellow","pro":False}]},
+
+ # Halls of the Apostate (E4M6)
+ {"name":"Halls of the Apostate (E4M6) Main",
+ "connects_to_hub":True,
+ "episode":4,
+ "connections":[{"target":"Halls of the Apostate (E4M6) Yellow","pro":False}]},
+ {"name":"Halls of the Apostate (E4M6) Blue",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[{"target":"Halls of the Apostate (E4M6) Green","pro":False}]},
+ {"name":"Halls of the Apostate (E4M6) Yellow",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[
+ {"target":"Halls of the Apostate (E4M6) Main","pro":False},
+ {"target":"Halls of the Apostate (E4M6) Green","pro":False}]},
+ {"name":"Halls of the Apostate (E4M6) Green",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[
+ {"target":"Halls of the Apostate (E4M6) Yellow","pro":False},
+ {"target":"Halls of the Apostate (E4M6) Blue","pro":False}]},
+
+ # Ramparts of Perdition (E4M7)
+ {"name":"Ramparts of Perdition (E4M7) Main",
+ "connects_to_hub":True,
+ "episode":4,
+ "connections":[{"target":"Ramparts of Perdition (E4M7) Yellow","pro":False}]},
+ {"name":"Ramparts of Perdition (E4M7) Blue",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[{"target":"Ramparts of Perdition (E4M7) Yellow","pro":False}]},
+ {"name":"Ramparts of Perdition (E4M7) Yellow",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[
+ {"target":"Ramparts of Perdition (E4M7) Main","pro":False},
+ {"target":"Ramparts of Perdition (E4M7) Green","pro":False},
+ {"target":"Ramparts of Perdition (E4M7) Blue","pro":False}]},
+ {"name":"Ramparts of Perdition (E4M7) Green",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[{"target":"Ramparts of Perdition (E4M7) Yellow","pro":False}]},
+
+ # Shattered Bridge (E4M8)
+ {"name":"Shattered Bridge (E4M8) Main",
+ "connects_to_hub":True,
+ "episode":4,
+ "connections":[{"target":"Shattered Bridge (E4M8) Yellow","pro":False}]},
+ {"name":"Shattered Bridge (E4M8) Yellow",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[
+ {"target":"Shattered Bridge (E4M8) Main","pro":False},
+ {"target":"Shattered Bridge (E4M8) Boss","pro":False}]},
+ {"name":"Shattered Bridge (E4M8) Boss",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[]},
+
+ # Mausoleum (E4M9)
+ {"name":"Mausoleum (E4M9) Main",
+ "connects_to_hub":True,
+ "episode":4,
+ "connections":[{"target":"Mausoleum (E4M9) Yellow","pro":False}]},
+ {"name":"Mausoleum (E4M9) Yellow",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[{"target":"Mausoleum (E4M9) Main","pro":False}]},
+
+ # Ochre Cliffs (E5M1)
+ {"name":"Ochre Cliffs (E5M1) Main",
+ "connects_to_hub":True,
+ "episode":5,
+ "connections":[{"target":"Ochre Cliffs (E5M1) Yellow","pro":False}]},
+ {"name":"Ochre Cliffs (E5M1) Blue",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[{"target":"Ochre Cliffs (E5M1) Yellow","pro":False}]},
+ {"name":"Ochre Cliffs (E5M1) Yellow",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[
+ {"target":"Ochre Cliffs (E5M1) Main","pro":False},
+ {"target":"Ochre Cliffs (E5M1) Green","pro":False},
+ {"target":"Ochre Cliffs (E5M1) Blue","pro":False}]},
+ {"name":"Ochre Cliffs (E5M1) Green",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[{"target":"Ochre Cliffs (E5M1) Yellow","pro":False}]},
+
+ # Rapids (E5M2)
+ {"name":"Rapids (E5M2) Main",
+ "connects_to_hub":True,
+ "episode":5,
+ "connections":[{"target":"Rapids (E5M2) Yellow","pro":False}]},
+ {"name":"Rapids (E5M2) Yellow",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[
+ {"target":"Rapids (E5M2) Main","pro":False},
+ {"target":"Rapids (E5M2) Green","pro":False}]},
+ {"name":"Rapids (E5M2) Green",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[
+ {"target":"Rapids (E5M2) Yellow","pro":False},
+ {"target":"Rapids (E5M2) Main","pro":False}]},
+
+ # Quay (E5M3)
+ {"name":"Quay (E5M3) Main",
+ "connects_to_hub":True,
+ "episode":5,
+ "connections":[
+ {"target":"Quay (E5M3) Yellow","pro":False},
+ {"target":"Quay (E5M3) Green","pro":False},
+ {"target":"Quay (E5M3) Blue","pro":False}]},
+ {"name":"Quay (E5M3) Blue",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[{"target":"Quay (E5M3) Main","pro":False}]},
+ {"name":"Quay (E5M3) Yellow",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[{"target":"Quay (E5M3) Main","pro":False}]},
+ {"name":"Quay (E5M3) Green",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[
+ {"target":"Quay (E5M3) Main","pro":False},
+ {"target":"Quay (E5M3) Cyan","pro":False}]},
+ {"name":"Quay (E5M3) Cyan",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[{"target":"Quay (E5M3) Main","pro":False}]},
+
+ # Courtyard (E5M4)
+ {"name":"Courtyard (E5M4) Main",
+ "connects_to_hub":True,
+ "episode":5,
+ "connections":[
+ {"target":"Courtyard (E5M4) Kakis","pro":False},
+ {"target":"Courtyard (E5M4) Blue","pro":False}]},
+ {"name":"Courtyard (E5M4) Blue",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[{"target":"Courtyard (E5M4) Main","pro":False}]},
+ {"name":"Courtyard (E5M4) Kakis",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[{"target":"Courtyard (E5M4) Main","pro":False}]},
+
+ # Hydratyr (E5M5)
+ {"name":"Hydratyr (E5M5) Main",
+ "connects_to_hub":True,
+ "episode":5,
+ "connections":[{"target":"Hydratyr (E5M5) Yellow","pro":False}]},
+ {"name":"Hydratyr (E5M5) Blue",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[{"target":"Hydratyr (E5M5) Green","pro":False}]},
+ {"name":"Hydratyr (E5M5) Yellow",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[
+ {"target":"Hydratyr (E5M5) Main","pro":False},
+ {"target":"Hydratyr (E5M5) Green","pro":False}]},
+ {"name":"Hydratyr (E5M5) Green",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[
+ {"target":"Hydratyr (E5M5) Main","pro":False},
+ {"target":"Hydratyr (E5M5) Yellow","pro":False},
+ {"target":"Hydratyr (E5M5) Blue","pro":False}]},
+
+ # Colonnade (E5M6)
+ {"name":"Colonnade (E5M6) Main",
+ "connects_to_hub":True,
+ "episode":5,
+ "connections":[
+ {"target":"Colonnade (E5M6) Yellow","pro":False},
+ {"target":"Colonnade (E5M6) Blue","pro":False}]},
+ {"name":"Colonnade (E5M6) Blue",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[{"target":"Colonnade (E5M6) Main","pro":False}]},
+ {"name":"Colonnade (E5M6) Yellow",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[
+ {"target":"Colonnade (E5M6) Main","pro":False},
+ {"target":"Colonnade (E5M6) Green","pro":False}]},
+ {"name":"Colonnade (E5M6) Green",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[{"target":"Colonnade (E5M6) Yellow","pro":False}]},
+
+ # Foetid Manse (E5M7)
+ {"name":"Foetid Manse (E5M7) Main",
+ "connects_to_hub":True,
+ "episode":5,
+ "connections":[{"target":"Foetid Manse (E5M7) Yellow","pro":False}]},
+ {"name":"Foetid Manse (E5M7) Blue",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[{"target":"Foetid Manse (E5M7) Yellow","pro":False}]},
+ {"name":"Foetid Manse (E5M7) Yellow",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[
+ {"target":"Foetid Manse (E5M7) Main","pro":False},
+ {"target":"Foetid Manse (E5M7) Green","pro":False},
+ {"target":"Foetid Manse (E5M7) Blue","pro":False}]},
+ {"name":"Foetid Manse (E5M7) Green",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[
+ {"target":"Foetid Manse (E5M7) Yellow","pro":False},
+ {"target":"Foetid Manse (E5M7) Main","pro":False}]},
+
+ # Field of Judgement (E5M8)
+ {"name":"Field of Judgement (E5M8) Main",
+ "connects_to_hub":True,
+ "episode":5,
+ "connections":[]},
+
+ # Skein of D'Sparil (E5M9)
+ {"name":"Skein of D'Sparil (E5M9) Main",
+ "connects_to_hub":True,
+ "episode":5,
+ "connections":[
+ {"target":"Skein of D'Sparil (E5M9) Blue","pro":False},
+ {"target":"Skein of D'Sparil (E5M9) Yellow","pro":False},
+ {"target":"Skein of D'Sparil (E5M9) Green","pro":False}]},
+ {"name":"Skein of D'Sparil (E5M9) Blue",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[{"target":"Skein of D'Sparil (E5M9) Main","pro":False}]},
+ {"name":"Skein of D'Sparil (E5M9) Yellow",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[{"target":"Skein of D'Sparil (E5M9) Main","pro":False}]},
+ {"name":"Skein of D'Sparil (E5M9) Green",
+ "connects_to_hub":False,
+ "episode":5,
+ "connections":[{"target":"Skein of D'Sparil (E5M9) Main","pro":False}]},
+]
diff --git a/worlds/heretic/Rules.py b/worlds/heretic/Rules.py
new file mode 100644
index 000000000000..579fd8b77179
--- /dev/null
+++ b/worlds/heretic/Rules.py
@@ -0,0 +1,736 @@
+# This file is auto generated. More info: https://github.com/Daivuk/apdoom
+
+from typing import TYPE_CHECKING
+from worlds.generic.Rules import set_rule
+
+if TYPE_CHECKING:
+ from . import HereticWorld
+
+
+def set_episode1_rules(player, multiworld, pro):
+ # The Docks (E1M1)
+ set_rule(multiworld.get_entrance("Hub -> The Docks (E1M1) Main", player), lambda state:
+ state.has("The Docks (E1M1)", player, 1))
+ set_rule(multiworld.get_entrance("The Docks (E1M1) Main -> The Docks (E1M1) Yellow", player), lambda state:
+ state.has("The Docks (E1M1) - Yellow key", player, 1))
+
+ # The Dungeons (E1M2)
+ set_rule(multiworld.get_entrance("Hub -> The Dungeons (E1M2) Main", player), lambda state:
+ (state.has("The Dungeons (E1M2)", player, 1)) and
+ (state.has("Dragon Claw", player, 1) or
+ state.has("Ethereal Crossbow", player, 1)))
+ set_rule(multiworld.get_entrance("The Dungeons (E1M2) Main -> The Dungeons (E1M2) Yellow", player), lambda state:
+ state.has("The Dungeons (E1M2) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Dungeons (E1M2) Main -> The Dungeons (E1M2) Green", player), lambda state:
+ state.has("The Dungeons (E1M2) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Dungeons (E1M2) Blue -> The Dungeons (E1M2) Yellow", player), lambda state:
+ state.has("The Dungeons (E1M2) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Dungeons (E1M2) Yellow -> The Dungeons (E1M2) Blue", player), lambda state:
+ state.has("The Dungeons (E1M2) - Blue key", player, 1))
+
+ # The Gatehouse (E1M3)
+ set_rule(multiworld.get_entrance("Hub -> The Gatehouse (E1M3) Main", player), lambda state:
+ (state.has("The Gatehouse (E1M3)", player, 1)) and
+ (state.has("Ethereal Crossbow", player, 1) or
+ state.has("Dragon Claw", player, 1)))
+ set_rule(multiworld.get_entrance("The Gatehouse (E1M3) Main -> The Gatehouse (E1M3) Yellow", player), lambda state:
+ state.has("The Gatehouse (E1M3) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Gatehouse (E1M3) Main -> The Gatehouse (E1M3) Sea", player), lambda state:
+ state.has("The Gatehouse (E1M3) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Gatehouse (E1M3) Main -> The Gatehouse (E1M3) Green", player), lambda state:
+ state.has("The Gatehouse (E1M3) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Gatehouse (E1M3) Green -> The Gatehouse (E1M3) Main", player), lambda state:
+ state.has("The Gatehouse (E1M3) - Green key", player, 1))
+
+ # The Guard Tower (E1M4)
+ set_rule(multiworld.get_entrance("Hub -> The Guard Tower (E1M4) Main", player), lambda state:
+ (state.has("The Guard Tower (E1M4)", player, 1)) and
+ (state.has("Ethereal Crossbow", player, 1) or
+ state.has("Dragon Claw", player, 1)))
+ set_rule(multiworld.get_entrance("The Guard Tower (E1M4) Main -> The Guard Tower (E1M4) Yellow", player), lambda state:
+ state.has("The Guard Tower (E1M4) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Guard Tower (E1M4) Yellow -> The Guard Tower (E1M4) Green", player), lambda state:
+ state.has("The Guard Tower (E1M4) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Guard Tower (E1M4) Green -> The Guard Tower (E1M4) Yellow", player), lambda state:
+ state.has("The Guard Tower (E1M4) - Green key", player, 1))
+
+ # The Citadel (E1M5)
+ set_rule(multiworld.get_entrance("Hub -> The Citadel (E1M5) Main", player), lambda state:
+ (state.has("The Citadel (E1M5)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1)) and
+ (state.has("Dragon Claw", player, 1) or
+ state.has("Gauntlets of the Necromancer", player, 1)))
+ set_rule(multiworld.get_entrance("The Citadel (E1M5) Main -> The Citadel (E1M5) Yellow", player), lambda state:
+ state.has("The Citadel (E1M5) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Citadel (E1M5) Blue -> The Citadel (E1M5) Green", player), lambda state:
+ state.has("The Citadel (E1M5) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Citadel (E1M5) Yellow -> The Citadel (E1M5) Green", player), lambda state:
+ state.has("The Citadel (E1M5) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Citadel (E1M5) Green -> The Citadel (E1M5) Blue", player), lambda state:
+ state.has("The Citadel (E1M5) - Blue key", player, 1))
+
+ # The Cathedral (E1M6)
+ set_rule(multiworld.get_entrance("Hub -> The Cathedral (E1M6) Main", player), lambda state:
+ (state.has("The Cathedral (E1M6)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1)) and
+ (state.has("Gauntlets of the Necromancer", player, 1) or
+ state.has("Dragon Claw", player, 1)))
+ set_rule(multiworld.get_entrance("The Cathedral (E1M6) Main -> The Cathedral (E1M6) Yellow", player), lambda state:
+ state.has("The Cathedral (E1M6) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Cathedral (E1M6) Yellow -> The Cathedral (E1M6) Green", player), lambda state:
+ state.has("The Cathedral (E1M6) - Green key", player, 1))
+
+ # The Crypts (E1M7)
+ set_rule(multiworld.get_entrance("Hub -> The Crypts (E1M7) Main", player), lambda state:
+ (state.has("The Crypts (E1M7)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1)) and
+ (state.has("Gauntlets of the Necromancer", player, 1) or
+ state.has("Dragon Claw", player, 1)))
+ set_rule(multiworld.get_entrance("The Crypts (E1M7) Main -> The Crypts (E1M7) Yellow", player), lambda state:
+ state.has("The Crypts (E1M7) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Crypts (E1M7) Main -> The Crypts (E1M7) Green", player), lambda state:
+ state.has("The Crypts (E1M7) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Crypts (E1M7) Yellow -> The Crypts (E1M7) Green", player), lambda state:
+ state.has("The Crypts (E1M7) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Crypts (E1M7) Yellow -> The Crypts (E1M7) Blue", player), lambda state:
+ state.has("The Crypts (E1M7) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Crypts (E1M7) Green -> The Crypts (E1M7) Main", player), lambda state:
+ state.has("The Crypts (E1M7) - Green key", player, 1))
+
+ # Hell's Maw (E1M8)
+ set_rule(multiworld.get_entrance("Hub -> Hell's Maw (E1M8) Main", player), lambda state:
+ state.has("Hell's Maw (E1M8)", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1))
+
+ # The Graveyard (E1M9)
+ set_rule(multiworld.get_entrance("Hub -> The Graveyard (E1M9) Main", player), lambda state:
+ state.has("The Graveyard (E1M9)", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1))
+ set_rule(multiworld.get_entrance("The Graveyard (E1M9) Main -> The Graveyard (E1M9) Yellow", player), lambda state:
+ state.has("The Graveyard (E1M9) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Graveyard (E1M9) Main -> The Graveyard (E1M9) Green", player), lambda state:
+ state.has("The Graveyard (E1M9) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Graveyard (E1M9) Main -> The Graveyard (E1M9) Blue", player), lambda state:
+ state.has("The Graveyard (E1M9) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Graveyard (E1M9) Yellow -> The Graveyard (E1M9) Main", player), lambda state:
+ state.has("The Graveyard (E1M9) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Graveyard (E1M9) Green -> The Graveyard (E1M9) Main", player), lambda state:
+ state.has("The Graveyard (E1M9) - Green key", player, 1))
+
+
+def set_episode2_rules(player, multiworld, pro):
+ # The Crater (E2M1)
+ set_rule(multiworld.get_entrance("Hub -> The Crater (E2M1) Main", player), lambda state:
+ state.has("The Crater (E2M1)", player, 1))
+ set_rule(multiworld.get_entrance("The Crater (E2M1) Main -> The Crater (E2M1) Yellow", player), lambda state:
+ state.has("The Crater (E2M1) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Crater (E2M1) Yellow -> The Crater (E2M1) Green", player), lambda state:
+ state.has("The Crater (E2M1) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Crater (E2M1) Green -> The Crater (E2M1) Yellow", player), lambda state:
+ state.has("The Crater (E2M1) - Green key", player, 1))
+
+ # The Lava Pits (E2M2)
+ set_rule(multiworld.get_entrance("Hub -> The Lava Pits (E2M2) Main", player), lambda state:
+ (state.has("The Lava Pits (E2M2)", player, 1)) and
+ (state.has("Ethereal Crossbow", player, 1) or
+ state.has("Dragon Claw", player, 1)))
+ set_rule(multiworld.get_entrance("The Lava Pits (E2M2) Main -> The Lava Pits (E2M2) Yellow", player), lambda state:
+ state.has("The Lava Pits (E2M2) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Lava Pits (E2M2) Yellow -> The Lava Pits (E2M2) Green", player), lambda state:
+ state.has("The Lava Pits (E2M2) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Lava Pits (E2M2) Yellow -> The Lava Pits (E2M2) Main", player), lambda state:
+ state.has("The Lava Pits (E2M2) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Lava Pits (E2M2) Green -> The Lava Pits (E2M2) Yellow", player), lambda state:
+ state.has("The Lava Pits (E2M2) - Green key", player, 1))
+
+ # The River of Fire (E2M3)
+ set_rule(multiworld.get_entrance("Hub -> The River of Fire (E2M3) Main", player), lambda state:
+ state.has("The River of Fire (E2M3)", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Ethereal Crossbow", player, 1))
+ set_rule(multiworld.get_entrance("The River of Fire (E2M3) Main -> The River of Fire (E2M3) Yellow", player), lambda state:
+ state.has("The River of Fire (E2M3) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The River of Fire (E2M3) Main -> The River of Fire (E2M3) Blue", player), lambda state:
+ state.has("The River of Fire (E2M3) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The River of Fire (E2M3) Main -> The River of Fire (E2M3) Green", player), lambda state:
+ state.has("The River of Fire (E2M3) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The River of Fire (E2M3) Blue -> The River of Fire (E2M3) Main", player), lambda state:
+ state.has("The River of Fire (E2M3) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The River of Fire (E2M3) Yellow -> The River of Fire (E2M3) Main", player), lambda state:
+ state.has("The River of Fire (E2M3) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The River of Fire (E2M3) Green -> The River of Fire (E2M3) Main", player), lambda state:
+ state.has("The River of Fire (E2M3) - Green key", player, 1))
+
+ # The Ice Grotto (E2M4)
+ set_rule(multiworld.get_entrance("Hub -> The Ice Grotto (E2M4) Main", player), lambda state:
+ (state.has("The Ice Grotto (E2M4)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1)) and
+ (state.has("Hellstaff", player, 1) or
+ state.has("Firemace", player, 1)))
+ set_rule(multiworld.get_entrance("The Ice Grotto (E2M4) Main -> The Ice Grotto (E2M4) Green", player), lambda state:
+ state.has("The Ice Grotto (E2M4) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Ice Grotto (E2M4) Main -> The Ice Grotto (E2M4) Yellow", player), lambda state:
+ state.has("The Ice Grotto (E2M4) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Ice Grotto (E2M4) Blue -> The Ice Grotto (E2M4) Green", player), lambda state:
+ state.has("The Ice Grotto (E2M4) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Ice Grotto (E2M4) Yellow -> The Ice Grotto (E2M4) Magenta", player), lambda state:
+ state.has("The Ice Grotto (E2M4) - Green key", player, 1) and
+ state.has("The Ice Grotto (E2M4) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Ice Grotto (E2M4) Green -> The Ice Grotto (E2M4) Blue", player), lambda state:
+ state.has("The Ice Grotto (E2M4) - Blue key", player, 1))
+
+ # The Catacombs (E2M5)
+ set_rule(multiworld.get_entrance("Hub -> The Catacombs (E2M5) Main", player), lambda state:
+ (state.has("The Catacombs (E2M5)", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1)) and
+ (state.has("Phoenix Rod", player, 1) or
+ state.has("Firemace", player, 1) or
+ state.has("Hellstaff", player, 1)))
+ set_rule(multiworld.get_entrance("The Catacombs (E2M5) Main -> The Catacombs (E2M5) Yellow", player), lambda state:
+ state.has("The Catacombs (E2M5) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Catacombs (E2M5) Blue -> The Catacombs (E2M5) Green", player), lambda state:
+ state.has("The Catacombs (E2M5) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Catacombs (E2M5) Yellow -> The Catacombs (E2M5) Green", player), lambda state:
+ state.has("The Catacombs (E2M5) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Catacombs (E2M5) Green -> The Catacombs (E2M5) Blue", player), lambda state:
+ state.has("The Catacombs (E2M5) - Blue key", player, 1))
+
+ # The Labyrinth (E2M6)
+ set_rule(multiworld.get_entrance("Hub -> The Labyrinth (E2M6) Main", player), lambda state:
+ (state.has("The Labyrinth (E2M6)", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1)) and
+ (state.has("Phoenix Rod", player, 1) or
+ state.has("Firemace", player, 1) or
+ state.has("Hellstaff", player, 1)))
+ set_rule(multiworld.get_entrance("The Labyrinth (E2M6) Main -> The Labyrinth (E2M6) Blue", player), lambda state:
+ state.has("The Labyrinth (E2M6) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Labyrinth (E2M6) Main -> The Labyrinth (E2M6) Yellow", player), lambda state:
+ state.has("The Labyrinth (E2M6) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Labyrinth (E2M6) Main -> The Labyrinth (E2M6) Green", player), lambda state:
+ state.has("The Labyrinth (E2M6) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Labyrinth (E2M6) Blue -> The Labyrinth (E2M6) Main", player), lambda state:
+ state.has("The Labyrinth (E2M6) - Blue key", player, 1))
+
+ # The Great Hall (E2M7)
+ set_rule(multiworld.get_entrance("Hub -> The Great Hall (E2M7) Main", player), lambda state:
+ (state.has("The Great Hall (E2M7)", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Firemace", player, 1)) and
+ (state.has("Phoenix Rod", player, 1) or
+ state.has("Hellstaff", player, 1)))
+ set_rule(multiworld.get_entrance("The Great Hall (E2M7) Main -> The Great Hall (E2M7) Yellow", player), lambda state:
+ state.has("The Great Hall (E2M7) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Great Hall (E2M7) Main -> The Great Hall (E2M7) Green", player), lambda state:
+ state.has("The Great Hall (E2M7) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Great Hall (E2M7) Blue -> The Great Hall (E2M7) Yellow", player), lambda state:
+ state.has("The Great Hall (E2M7) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Great Hall (E2M7) Yellow -> The Great Hall (E2M7) Blue", player), lambda state:
+ state.has("The Great Hall (E2M7) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Great Hall (E2M7) Yellow -> The Great Hall (E2M7) Main", player), lambda state:
+ state.has("The Great Hall (E2M7) - Yellow key", player, 1))
+
+ # The Portals of Chaos (E2M8)
+ set_rule(multiworld.get_entrance("Hub -> The Portals of Chaos (E2M8) Main", player), lambda state:
+ state.has("The Portals of Chaos (E2M8)", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Phoenix Rod", player, 1) and
+ state.has("Firemace", player, 1) and
+ state.has("Hellstaff", player, 1))
+
+ # The Glacier (E2M9)
+ set_rule(multiworld.get_entrance("Hub -> The Glacier (E2M9) Main", player), lambda state:
+ (state.has("The Glacier (E2M9)", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Firemace", player, 1)) and
+ (state.has("Phoenix Rod", player, 1) or
+ state.has("Hellstaff", player, 1)))
+ set_rule(multiworld.get_entrance("The Glacier (E2M9) Main -> The Glacier (E2M9) Yellow", player), lambda state:
+ state.has("The Glacier (E2M9) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Glacier (E2M9) Main -> The Glacier (E2M9) Blue", player), lambda state:
+ state.has("The Glacier (E2M9) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Glacier (E2M9) Main -> The Glacier (E2M9) Green", player), lambda state:
+ state.has("The Glacier (E2M9) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Glacier (E2M9) Blue -> The Glacier (E2M9) Main", player), lambda state:
+ state.has("The Glacier (E2M9) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Glacier (E2M9) Yellow -> The Glacier (E2M9) Main", player), lambda state:
+ state.has("The Glacier (E2M9) - Yellow key", player, 1))
+
+
+def set_episode3_rules(player, multiworld, pro):
+ # The Storehouse (E3M1)
+ set_rule(multiworld.get_entrance("Hub -> The Storehouse (E3M1) Main", player), lambda state:
+ state.has("The Storehouse (E3M1)", player, 1))
+ set_rule(multiworld.get_entrance("The Storehouse (E3M1) Main -> The Storehouse (E3M1) Yellow", player), lambda state:
+ state.has("The Storehouse (E3M1) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Storehouse (E3M1) Main -> The Storehouse (E3M1) Green", player), lambda state:
+ state.has("The Storehouse (E3M1) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Storehouse (E3M1) Yellow -> The Storehouse (E3M1) Main", player), lambda state:
+ state.has("The Storehouse (E3M1) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Storehouse (E3M1) Green -> The Storehouse (E3M1) Main", player), lambda state:
+ state.has("The Storehouse (E3M1) - Green key", player, 1))
+
+ # The Cesspool (E3M2)
+ set_rule(multiworld.get_entrance("Hub -> The Cesspool (E3M2) Main", player), lambda state:
+ state.has("The Cesspool (E3M2)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Firemace", player, 1) and
+ state.has("Hellstaff", player, 1))
+ set_rule(multiworld.get_entrance("The Cesspool (E3M2) Main -> The Cesspool (E3M2) Yellow", player), lambda state:
+ state.has("The Cesspool (E3M2) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Cesspool (E3M2) Blue -> The Cesspool (E3M2) Green", player), lambda state:
+ state.has("The Cesspool (E3M2) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Cesspool (E3M2) Yellow -> The Cesspool (E3M2) Green", player), lambda state:
+ state.has("The Cesspool (E3M2) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Cesspool (E3M2) Green -> The Cesspool (E3M2) Blue", player), lambda state:
+ state.has("The Cesspool (E3M2) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Cesspool (E3M2) Green -> The Cesspool (E3M2) Yellow", player), lambda state:
+ state.has("The Cesspool (E3M2) - Green key", player, 1))
+
+ # The Confluence (E3M3)
+ set_rule(multiworld.get_entrance("Hub -> The Confluence (E3M3) Main", player), lambda state:
+ (state.has("The Confluence (E3M3)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1)) and
+ (state.has("Gauntlets of the Necromancer", player, 1) or
+ state.has("Phoenix Rod", player, 1) or
+ state.has("Firemace", player, 1) or
+ state.has("Hellstaff", player, 1)))
+ set_rule(multiworld.get_entrance("The Confluence (E3M3) Main -> The Confluence (E3M3) Green", player), lambda state:
+ state.has("The Confluence (E3M3) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Confluence (E3M3) Main -> The Confluence (E3M3) Yellow", player), lambda state:
+ state.has("The Confluence (E3M3) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Confluence (E3M3) Blue -> The Confluence (E3M3) Green", player), lambda state:
+ state.has("The Confluence (E3M3) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Confluence (E3M3) Green -> The Confluence (E3M3) Main", player), lambda state:
+ state.has("The Confluence (E3M3) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Confluence (E3M3) Green -> The Confluence (E3M3) Blue", player), lambda state:
+ state.has("The Confluence (E3M3) - Blue key", player, 1))
+
+ # The Azure Fortress (E3M4)
+ set_rule(multiworld.get_entrance("Hub -> The Azure Fortress (E3M4) Main", player), lambda state:
+ (state.has("The Azure Fortress (E3M4)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Hellstaff", player, 1)) and
+ (state.has("Firemace", player, 1) or
+ state.has("Phoenix Rod", player, 1) or
+ state.has("Gauntlets of the Necromancer", player, 1)))
+ set_rule(multiworld.get_entrance("The Azure Fortress (E3M4) Main -> The Azure Fortress (E3M4) Green", player), lambda state:
+ state.has("The Azure Fortress (E3M4) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Azure Fortress (E3M4) Main -> The Azure Fortress (E3M4) Yellow", player), lambda state:
+ state.has("The Azure Fortress (E3M4) - Yellow key", player, 1))
+
+ # The Ophidian Lair (E3M5)
+ set_rule(multiworld.get_entrance("Hub -> The Ophidian Lair (E3M5) Main", player), lambda state:
+ (state.has("The Ophidian Lair (E3M5)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Hellstaff", player, 1)) and
+ (state.has("Gauntlets of the Necromancer", player, 1) or
+ state.has("Phoenix Rod", player, 1) or
+ state.has("Firemace", player, 1)))
+ set_rule(multiworld.get_entrance("The Ophidian Lair (E3M5) Main -> The Ophidian Lair (E3M5) Yellow", player), lambda state:
+ state.has("The Ophidian Lair (E3M5) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Ophidian Lair (E3M5) Main -> The Ophidian Lair (E3M5) Green", player), lambda state:
+ state.has("The Ophidian Lair (E3M5) - Green key", player, 1))
+
+ # The Halls of Fear (E3M6)
+ set_rule(multiworld.get_entrance("Hub -> The Halls of Fear (E3M6) Main", player), lambda state:
+ (state.has("The Halls of Fear (E3M6)", player, 1) and
+ state.has("Firemace", player, 1) and
+ state.has("Hellstaff", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Ethereal Crossbow", player, 1)) and
+ (state.has("Gauntlets of the Necromancer", player, 1) or
+ state.has("Phoenix Rod", player, 1)))
+ set_rule(multiworld.get_entrance("The Halls of Fear (E3M6) Main -> The Halls of Fear (E3M6) Yellow", player), lambda state:
+ state.has("The Halls of Fear (E3M6) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Halls of Fear (E3M6) Blue -> The Halls of Fear (E3M6) Yellow", player), lambda state:
+ state.has("The Halls of Fear (E3M6) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Halls of Fear (E3M6) Yellow -> The Halls of Fear (E3M6) Blue", player), lambda state:
+ state.has("The Halls of Fear (E3M6) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Halls of Fear (E3M6) Yellow -> The Halls of Fear (E3M6) Green", player), lambda state:
+ state.has("The Halls of Fear (E3M6) - Green key", player, 1))
+
+ # The Chasm (E3M7)
+ set_rule(multiworld.get_entrance("Hub -> The Chasm (E3M7) Main", player), lambda state:
+ (state.has("The Chasm (E3M7)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Firemace", player, 1) and
+ state.has("Hellstaff", player, 1)) and
+ (state.has("Gauntlets of the Necromancer", player, 1) or
+ state.has("Phoenix Rod", player, 1)))
+ set_rule(multiworld.get_entrance("The Chasm (E3M7) Main -> The Chasm (E3M7) Yellow", player), lambda state:
+ state.has("The Chasm (E3M7) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Chasm (E3M7) Yellow -> The Chasm (E3M7) Main", player), lambda state:
+ state.has("The Chasm (E3M7) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Chasm (E3M7) Yellow -> The Chasm (E3M7) Green", player), lambda state:
+ state.has("The Chasm (E3M7) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Chasm (E3M7) Yellow -> The Chasm (E3M7) Blue", player), lambda state:
+ state.has("The Chasm (E3M7) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("The Chasm (E3M7) Green -> The Chasm (E3M7) Yellow", player), lambda state:
+ state.has("The Chasm (E3M7) - Green key", player, 1))
+
+ # D'Sparil'S Keep (E3M8)
+ set_rule(multiworld.get_entrance("Hub -> D'Sparil'S Keep (E3M8) Main", player), lambda state:
+ state.has("D'Sparil'S Keep (E3M8)", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Phoenix Rod", player, 1) and
+ state.has("Firemace", player, 1) and
+ state.has("Hellstaff", player, 1))
+
+ # The Aquifier (E3M9)
+ set_rule(multiworld.get_entrance("Hub -> The Aquifier (E3M9) Main", player), lambda state:
+ state.has("The Aquifier (E3M9)", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Phoenix Rod", player, 1) and
+ state.has("Firemace", player, 1) and
+ state.has("Hellstaff", player, 1))
+ set_rule(multiworld.get_entrance("The Aquifier (E3M9) Main -> The Aquifier (E3M9) Yellow", player), lambda state:
+ state.has("The Aquifier (E3M9) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Aquifier (E3M9) Yellow -> The Aquifier (E3M9) Green", player), lambda state:
+ state.has("The Aquifier (E3M9) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("The Aquifier (E3M9) Yellow -> The Aquifier (E3M9) Main", player), lambda state:
+ state.has("The Aquifier (E3M9) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("The Aquifier (E3M9) Green -> The Aquifier (E3M9) Yellow", player), lambda state:
+ state.has("The Aquifier (E3M9) - Green key", player, 1))
+
+
+def set_episode4_rules(player, multiworld, pro):
+ # Catafalque (E4M1)
+ set_rule(multiworld.get_entrance("Hub -> Catafalque (E4M1) Main", player), lambda state:
+ state.has("Catafalque (E4M1)", player, 1))
+ set_rule(multiworld.get_entrance("Catafalque (E4M1) Main -> Catafalque (E4M1) Yellow", player), lambda state:
+ state.has("Catafalque (E4M1) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Catafalque (E4M1) Yellow -> Catafalque (E4M1) Green", player), lambda state:
+ (state.has("Catafalque (E4M1) - Green key", player, 1)) and (state.has("Ethereal Crossbow", player, 1) or
+ state.has("Dragon Claw", player, 1) or
+ state.has("Phoenix Rod", player, 1) or
+ state.has("Firemace", player, 1) or
+ state.has("Hellstaff", player, 1)))
+
+ # Blockhouse (E4M2)
+ set_rule(multiworld.get_entrance("Hub -> Blockhouse (E4M2) Main", player), lambda state:
+ state.has("Blockhouse (E4M2)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1))
+ set_rule(multiworld.get_entrance("Blockhouse (E4M2) Main -> Blockhouse (E4M2) Yellow", player), lambda state:
+ state.has("Blockhouse (E4M2) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Blockhouse (E4M2) Main -> Blockhouse (E4M2) Green", player), lambda state:
+ state.has("Blockhouse (E4M2) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Blockhouse (E4M2) Main -> Blockhouse (E4M2) Blue", player), lambda state:
+ state.has("Blockhouse (E4M2) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Blockhouse (E4M2) Green -> Blockhouse (E4M2) Main", player), lambda state:
+ state.has("Blockhouse (E4M2) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Blockhouse (E4M2) Blue -> Blockhouse (E4M2) Main", player), lambda state:
+ state.has("Blockhouse (E4M2) - Blue key", player, 1))
+
+ # Ambulatory (E4M3)
+ set_rule(multiworld.get_entrance("Hub -> Ambulatory (E4M3) Main", player), lambda state:
+ (state.has("Ambulatory (E4M3)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1)) and
+ (state.has("Phoenix Rod", player, 1) or
+ state.has("Firemace", player, 1) or
+ state.has("Hellstaff", player, 1)))
+ set_rule(multiworld.get_entrance("Ambulatory (E4M3) Main -> Ambulatory (E4M3) Blue", player), lambda state:
+ state.has("Ambulatory (E4M3) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Ambulatory (E4M3) Main -> Ambulatory (E4M3) Yellow", player), lambda state:
+ state.has("Ambulatory (E4M3) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Ambulatory (E4M3) Main -> Ambulatory (E4M3) Green", player), lambda state:
+ state.has("Ambulatory (E4M3) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Ambulatory (E4M3) Main -> Ambulatory (E4M3) Green Lock", player), lambda state:
+ state.has("Ambulatory (E4M3) - Green key", player, 1))
+
+ # Sepulcher (E4M4)
+ set_rule(multiworld.get_entrance("Hub -> Sepulcher (E4M4) Main", player), lambda state:
+ (state.has("Sepulcher (E4M4)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Firemace", player, 1)) and
+ (state.has("Phoenix Rod", player, 1) or
+ state.has("Hellstaff", player, 1)))
+
+ # Great Stair (E4M5)
+ set_rule(multiworld.get_entrance("Hub -> Great Stair (E4M5) Main", player), lambda state:
+ (state.has("Great Stair (E4M5)", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Firemace", player, 1)) and
+ (state.has("Hellstaff", player, 1) or
+ state.has("Phoenix Rod", player, 1)))
+ set_rule(multiworld.get_entrance("Great Stair (E4M5) Main -> Great Stair (E4M5) Yellow", player), lambda state:
+ state.has("Great Stair (E4M5) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Great Stair (E4M5) Blue -> Great Stair (E4M5) Green", player), lambda state:
+ state.has("Great Stair (E4M5) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Great Stair (E4M5) Yellow -> Great Stair (E4M5) Green", player), lambda state:
+ state.has("Great Stair (E4M5) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Great Stair (E4M5) Green -> Great Stair (E4M5) Blue", player), lambda state:
+ state.has("Great Stair (E4M5) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Great Stair (E4M5) Green -> Great Stair (E4M5) Yellow", player), lambda state:
+ state.has("Great Stair (E4M5) - Green key", player, 1))
+
+ # Halls of the Apostate (E4M6)
+ set_rule(multiworld.get_entrance("Hub -> Halls of the Apostate (E4M6) Main", player), lambda state:
+ (state.has("Halls of the Apostate (E4M6)", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Firemace", player, 1)) and
+ (state.has("Phoenix Rod", player, 1) or
+ state.has("Hellstaff", player, 1)))
+ set_rule(multiworld.get_entrance("Halls of the Apostate (E4M6) Main -> Halls of the Apostate (E4M6) Yellow", player), lambda state:
+ state.has("Halls of the Apostate (E4M6) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Halls of the Apostate (E4M6) Blue -> Halls of the Apostate (E4M6) Green", player), lambda state:
+ state.has("Halls of the Apostate (E4M6) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Halls of the Apostate (E4M6) Yellow -> Halls of the Apostate (E4M6) Green", player), lambda state:
+ state.has("Halls of the Apostate (E4M6) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Halls of the Apostate (E4M6) Green -> Halls of the Apostate (E4M6) Yellow", player), lambda state:
+ state.has("Halls of the Apostate (E4M6) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Halls of the Apostate (E4M6) Green -> Halls of the Apostate (E4M6) Blue", player), lambda state:
+ state.has("Halls of the Apostate (E4M6) - Blue key", player, 1))
+
+ # Ramparts of Perdition (E4M7)
+ set_rule(multiworld.get_entrance("Hub -> Ramparts of Perdition (E4M7) Main", player), lambda state:
+ (state.has("Ramparts of Perdition (E4M7)", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Firemace", player, 1)) and
+ (state.has("Phoenix Rod", player, 1) or
+ state.has("Hellstaff", player, 1)))
+ set_rule(multiworld.get_entrance("Ramparts of Perdition (E4M7) Main -> Ramparts of Perdition (E4M7) Yellow", player), lambda state:
+ state.has("Ramparts of Perdition (E4M7) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Ramparts of Perdition (E4M7) Blue -> Ramparts of Perdition (E4M7) Yellow", player), lambda state:
+ state.has("Ramparts of Perdition (E4M7) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Ramparts of Perdition (E4M7) Yellow -> Ramparts of Perdition (E4M7) Main", player), lambda state:
+ state.has("Ramparts of Perdition (E4M7) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Ramparts of Perdition (E4M7) Yellow -> Ramparts of Perdition (E4M7) Green", player), lambda state:
+ state.has("Ramparts of Perdition (E4M7) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Ramparts of Perdition (E4M7) Yellow -> Ramparts of Perdition (E4M7) Blue", player), lambda state:
+ state.has("Ramparts of Perdition (E4M7) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Ramparts of Perdition (E4M7) Green -> Ramparts of Perdition (E4M7) Yellow", player), lambda state:
+ state.has("Ramparts of Perdition (E4M7) - Green key", player, 1))
+
+ # Shattered Bridge (E4M8)
+ set_rule(multiworld.get_entrance("Hub -> Shattered Bridge (E4M8) Main", player), lambda state:
+ state.has("Shattered Bridge (E4M8)", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Phoenix Rod", player, 1) and
+ state.has("Firemace", player, 1) and
+ state.has("Hellstaff", player, 1))
+ set_rule(multiworld.get_entrance("Shattered Bridge (E4M8) Main -> Shattered Bridge (E4M8) Yellow", player), lambda state:
+ state.has("Shattered Bridge (E4M8) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Shattered Bridge (E4M8) Yellow -> Shattered Bridge (E4M8) Main", player), lambda state:
+ state.has("Shattered Bridge (E4M8) - Yellow key", player, 1))
+
+ # Mausoleum (E4M9)
+ set_rule(multiworld.get_entrance("Hub -> Mausoleum (E4M9) Main", player), lambda state:
+ (state.has("Mausoleum (E4M9)", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Firemace", player, 1)) and
+ (state.has("Phoenix Rod", player, 1) or
+ state.has("Hellstaff", player, 1)))
+ set_rule(multiworld.get_entrance("Mausoleum (E4M9) Main -> Mausoleum (E4M9) Yellow", player), lambda state:
+ state.has("Mausoleum (E4M9) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Mausoleum (E4M9) Yellow -> Mausoleum (E4M9) Main", player), lambda state:
+ state.has("Mausoleum (E4M9) - Yellow key", player, 1))
+
+
+def set_episode5_rules(player, multiworld, pro):
+ # Ochre Cliffs (E5M1)
+ set_rule(multiworld.get_entrance("Hub -> Ochre Cliffs (E5M1) Main", player), lambda state:
+ state.has("Ochre Cliffs (E5M1)", player, 1))
+ set_rule(multiworld.get_entrance("Ochre Cliffs (E5M1) Main -> Ochre Cliffs (E5M1) Yellow", player), lambda state:
+ state.has("Ochre Cliffs (E5M1) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Ochre Cliffs (E5M1) Blue -> Ochre Cliffs (E5M1) Yellow", player), lambda state:
+ state.has("Ochre Cliffs (E5M1) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Ochre Cliffs (E5M1) Yellow -> Ochre Cliffs (E5M1) Main", player), lambda state:
+ state.has("Ochre Cliffs (E5M1) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Ochre Cliffs (E5M1) Yellow -> Ochre Cliffs (E5M1) Green", player), lambda state:
+ state.has("Ochre Cliffs (E5M1) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Ochre Cliffs (E5M1) Yellow -> Ochre Cliffs (E5M1) Blue", player), lambda state:
+ state.has("Ochre Cliffs (E5M1) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Ochre Cliffs (E5M1) Green -> Ochre Cliffs (E5M1) Yellow", player), lambda state:
+ state.has("Ochre Cliffs (E5M1) - Green key", player, 1))
+
+ # Rapids (E5M2)
+ set_rule(multiworld.get_entrance("Hub -> Rapids (E5M2) Main", player), lambda state:
+ state.has("Rapids (E5M2)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1))
+ set_rule(multiworld.get_entrance("Rapids (E5M2) Main -> Rapids (E5M2) Yellow", player), lambda state:
+ state.has("Rapids (E5M2) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Rapids (E5M2) Yellow -> Rapids (E5M2) Main", player), lambda state:
+ state.has("Rapids (E5M2) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Rapids (E5M2) Yellow -> Rapids (E5M2) Green", player), lambda state:
+ state.has("Rapids (E5M2) - Green key", player, 1))
+
+ # Quay (E5M3)
+ set_rule(multiworld.get_entrance("Hub -> Quay (E5M3) Main", player), lambda state:
+ (state.has("Quay (E5M3)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1)) and
+ (state.has("Phoenix Rod", player, 1) or
+ state.has("Hellstaff", player, 1) or
+ state.has("Firemace", player, 1)))
+ set_rule(multiworld.get_entrance("Quay (E5M3) Main -> Quay (E5M3) Yellow", player), lambda state:
+ state.has("Quay (E5M3) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Quay (E5M3) Main -> Quay (E5M3) Green", player), lambda state:
+ state.has("Quay (E5M3) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Quay (E5M3) Main -> Quay (E5M3) Blue", player), lambda state:
+ state.has("Quay (E5M3) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Quay (E5M3) Yellow -> Quay (E5M3) Main", player), lambda state:
+ state.has("Quay (E5M3) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Quay (E5M3) Green -> Quay (E5M3) Main", player), lambda state:
+ state.has("Quay (E5M3) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Quay (E5M3) Green -> Quay (E5M3) Cyan", player), lambda state:
+ state.has("Quay (E5M3) - Blue key", player, 1))
+
+ # Courtyard (E5M4)
+ set_rule(multiworld.get_entrance("Hub -> Courtyard (E5M4) Main", player), lambda state:
+ (state.has("Courtyard (E5M4)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1)) and
+ (state.has("Phoenix Rod", player, 1) or
+ state.has("Firemace", player, 1) or
+ state.has("Hellstaff", player, 1)))
+ set_rule(multiworld.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Kakis", player), lambda state:
+ state.has("Courtyard (E5M4) - Yellow key", player, 1) or
+ state.has("Courtyard (E5M4) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Blue", player), lambda state:
+ state.has("Courtyard (E5M4) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Courtyard (E5M4) Blue -> Courtyard (E5M4) Main", player), lambda state:
+ state.has("Courtyard (E5M4) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Courtyard (E5M4) Kakis -> Courtyard (E5M4) Main", player), lambda state:
+ state.has("Courtyard (E5M4) - Yellow key", player, 1) or
+ state.has("Courtyard (E5M4) - Green key", player, 1))
+
+ # Hydratyr (E5M5)
+ set_rule(multiworld.get_entrance("Hub -> Hydratyr (E5M5) Main", player), lambda state:
+ (state.has("Hydratyr (E5M5)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Firemace", player, 1)) and
+ (state.has("Phoenix Rod", player, 1) or
+ state.has("Hellstaff", player, 1)))
+ set_rule(multiworld.get_entrance("Hydratyr (E5M5) Main -> Hydratyr (E5M5) Yellow", player), lambda state:
+ state.has("Hydratyr (E5M5) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Hydratyr (E5M5) Blue -> Hydratyr (E5M5) Green", player), lambda state:
+ state.has("Hydratyr (E5M5) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Hydratyr (E5M5) Yellow -> Hydratyr (E5M5) Green", player), lambda state:
+ state.has("Hydratyr (E5M5) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Hydratyr (E5M5) Green -> Hydratyr (E5M5) Blue", player), lambda state:
+ state.has("Hydratyr (E5M5) - Blue key", player, 1))
+
+ # Colonnade (E5M6)
+ set_rule(multiworld.get_entrance("Hub -> Colonnade (E5M6) Main", player), lambda state:
+ (state.has("Colonnade (E5M6)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Firemace", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1)) and
+ (state.has("Phoenix Rod", player, 1) or
+ state.has("Hellstaff", player, 1)))
+ set_rule(multiworld.get_entrance("Colonnade (E5M6) Main -> Colonnade (E5M6) Yellow", player), lambda state:
+ state.has("Colonnade (E5M6) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Colonnade (E5M6) Main -> Colonnade (E5M6) Blue", player), lambda state:
+ state.has("Colonnade (E5M6) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Colonnade (E5M6) Blue -> Colonnade (E5M6) Main", player), lambda state:
+ state.has("Colonnade (E5M6) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Colonnade (E5M6) Yellow -> Colonnade (E5M6) Green", player), lambda state:
+ state.has("Colonnade (E5M6) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Colonnade (E5M6) Green -> Colonnade (E5M6) Yellow", player), lambda state:
+ state.has("Colonnade (E5M6) - Green key", player, 1))
+
+ # Foetid Manse (E5M7)
+ set_rule(multiworld.get_entrance("Hub -> Foetid Manse (E5M7) Main", player), lambda state:
+ (state.has("Foetid Manse (E5M7)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Firemace", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1)) and
+ (state.has("Phoenix Rod", player, 1) or
+ state.has("Hellstaff", player, 1)))
+ set_rule(multiworld.get_entrance("Foetid Manse (E5M7) Main -> Foetid Manse (E5M7) Yellow", player), lambda state:
+ state.has("Foetid Manse (E5M7) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Foetid Manse (E5M7) Yellow -> Foetid Manse (E5M7) Green", player), lambda state:
+ state.has("Foetid Manse (E5M7) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Foetid Manse (E5M7) Yellow -> Foetid Manse (E5M7) Blue", player), lambda state:
+ state.has("Foetid Manse (E5M7) - Blue key", player, 1))
+
+ # Field of Judgement (E5M8)
+ set_rule(multiworld.get_entrance("Hub -> Field of Judgement (E5M8) Main", player), lambda state:
+ state.has("Field of Judgement (E5M8)", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Phoenix Rod", player, 1) and
+ state.has("Firemace", player, 1) and
+ state.has("Hellstaff", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Bag of Holding", player, 1))
+
+ # Skein of D'Sparil (E5M9)
+ set_rule(multiworld.get_entrance("Hub -> Skein of D'Sparil (E5M9) Main", player), lambda state:
+ state.has("Skein of D'Sparil (E5M9)", player, 1) and
+ state.has("Bag of Holding", player, 1) and
+ state.has("Hellstaff", player, 1) and
+ state.has("Phoenix Rod", player, 1) and
+ state.has("Dragon Claw", player, 1) and
+ state.has("Ethereal Crossbow", player, 1) and
+ state.has("Gauntlets of the Necromancer", player, 1) and
+ state.has("Firemace", player, 1))
+ set_rule(multiworld.get_entrance("Skein of D'Sparil (E5M9) Main -> Skein of D'Sparil (E5M9) Blue", player), lambda state:
+ state.has("Skein of D'Sparil (E5M9) - Blue key", player, 1))
+ set_rule(multiworld.get_entrance("Skein of D'Sparil (E5M9) Main -> Skein of D'Sparil (E5M9) Yellow", player), lambda state:
+ state.has("Skein of D'Sparil (E5M9) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Skein of D'Sparil (E5M9) Main -> Skein of D'Sparil (E5M9) Green", player), lambda state:
+ state.has("Skein of D'Sparil (E5M9) - Green key", player, 1))
+ set_rule(multiworld.get_entrance("Skein of D'Sparil (E5M9) Yellow -> Skein of D'Sparil (E5M9) Main", player), lambda state:
+ state.has("Skein of D'Sparil (E5M9) - Yellow key", player, 1))
+ set_rule(multiworld.get_entrance("Skein of D'Sparil (E5M9) Green -> Skein of D'Sparil (E5M9) Main", player), lambda state:
+ state.has("Skein of D'Sparil (E5M9) - Green key", player, 1))
+
+
+def set_rules(heretic_world: "HereticWorld", included_episodes, pro):
+ player = heretic_world.player
+ multiworld = heretic_world.multiworld
+
+ if included_episodes[0]:
+ set_episode1_rules(player, multiworld, pro)
+ if included_episodes[1]:
+ set_episode2_rules(player, multiworld, pro)
+ if included_episodes[2]:
+ set_episode3_rules(player, multiworld, pro)
+ if included_episodes[3]:
+ set_episode4_rules(player, multiworld, pro)
+ if included_episodes[4]:
+ set_episode5_rules(player, multiworld, pro)
diff --git a/worlds/heretic/__init__.py b/worlds/heretic/__init__.py
new file mode 100644
index 000000000000..bc0a54698a59
--- /dev/null
+++ b/worlds/heretic/__init__.py
@@ -0,0 +1,293 @@
+import functools
+import logging
+from typing import Any, Dict, List, Set
+
+from BaseClasses import Entrance, CollectionState, Item, Location, MultiWorld, Region, Tutorial
+from worlds.AutoWorld import WebWorld, World
+from . import Items, Locations, Maps, Regions, Rules
+from .Options import HereticOptions
+
+logger = logging.getLogger("Heretic")
+
+HERETIC_TYPE_LEVEL_COMPLETE = -2
+HERETIC_TYPE_MAP_SCROLL = 35
+
+
+class HereticLocation(Location):
+ game: str = "Heretic"
+
+
+class HereticItem(Item):
+ game: str = "Heretic"
+
+
+class HereticWeb(WebWorld):
+ tutorials = [Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to setting up the Heretic randomizer connected to an Archipelago Multiworld",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["Daivuk"]
+ )]
+ theme = "dirt"
+
+
+class HereticWorld(World):
+ """
+ Heretic is a dark fantasy first-person shooter video game released in December 1994. It was developed by Raven Software.
+ """
+ options_dataclass = HereticOptions
+ options: HereticOptions
+ game = "Heretic"
+ web = HereticWeb()
+ required_client_version = (0, 3, 9)
+
+ item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
+ item_name_groups = Items.item_name_groups
+
+ location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
+ location_name_groups = Locations.location_name_groups
+
+ starting_level_for_episode: List[str] = [
+ "The Docks (E1M1)",
+ "The Crater (E2M1)",
+ "The Storehouse (E3M1)",
+ "Catafalque (E4M1)",
+ "Ochre Cliffs (E5M1)"
+ ]
+
+ boss_level_for_episode: List[str] = [
+ "Hell's Maw (E1M8)",
+ "The Portals of Chaos (E2M8)",
+ "D'Sparil'S Keep (E3M8)",
+ "Shattered Bridge (E4M8)",
+ "Field of Judgement (E5M8)"
+ ]
+
+ # Item ratio that scales depending on episode count. These are the ratio for 1 episode.
+ items_ratio: Dict[str, float] = {
+ "Timebomb of the Ancients": 16,
+ "Tome of Power": 16,
+ "Silver Shield": 10,
+ "Enchanted Shield": 5,
+ "Torch": 5,
+ "Morph Ovum": 3,
+ "Mystic Urn": 2,
+ "Chaos Device": 1,
+ "Ring of Invincibility": 1,
+ "Shadowsphere": 1
+ }
+
+ def __init__(self, multiworld: MultiWorld, player: int):
+ self.included_episodes = [1, 1, 1, 0, 0]
+ self.location_count = 0
+
+ super().__init__(multiworld, player)
+
+ def get_episode_count(self):
+ return functools.reduce(lambda count, episode: count + episode, self.included_episodes)
+
+ def generate_early(self):
+ # Cache which episodes are included
+ self.included_episodes[0] = self.options.episode1.value
+ self.included_episodes[1] = self.options.episode2.value
+ self.included_episodes[2] = self.options.episode3.value
+ self.included_episodes[3] = self.options.episode4.value
+ self.included_episodes[4] = self.options.episode5.value
+
+ # If no episodes selected, select Episode 1
+ if self.get_episode_count() == 0:
+ self.included_episodes[0] = 1
+
+ def create_regions(self):
+ pro = self.options.pro.value
+ check_sanity = self.options.check_sanity.value
+
+ # Main regions
+ menu_region = Region("Menu", self.player, self.multiworld)
+ hub_region = Region("Hub", self.player, self.multiworld)
+ self.multiworld.regions += [menu_region, hub_region]
+ menu_region.add_exits(["Hub"])
+
+ # Create regions and locations
+ main_regions = []
+ connections = []
+ for region_dict in Regions.regions:
+ if not self.included_episodes[region_dict["episode"] - 1]:
+ continue
+
+ region_name = region_dict["name"]
+ if region_dict["connects_to_hub"]:
+ main_regions.append(region_name)
+
+ region = Region(region_name, self.player, self.multiworld)
+ region.add_locations({
+ loc["name"]: loc_id
+ for loc_id, loc in Locations.location_table.items()
+ if loc["region"] == region_name and (not loc["check_sanity"] or check_sanity)
+ }, HereticLocation)
+
+ self.multiworld.regions.append(region)
+
+ for connection_dict in region_dict["connections"]:
+ # Check if it's a pro-only connection
+ if connection_dict["pro"] and not pro:
+ continue
+ connections.append((region, connection_dict["target"]))
+
+ # Connect main regions to Hub
+ hub_region.add_exits(main_regions)
+
+ # Do the other connections between regions (They are not all both ways)
+ for connection in connections:
+ source = connection[0]
+ target = self.multiworld.get_region(connection[1], self.player)
+
+ entrance = Entrance(self.player, f"{source.name} -> {target.name}", source)
+ source.exits.append(entrance)
+ entrance.connect(target)
+
+ # Sum locations for items creation
+ self.location_count = len(self.multiworld.get_locations(self.player))
+
+ def completion_rule(self, state: CollectionState):
+ goal_levels = Maps.map_names
+ if self.options.goal.value:
+ goal_levels = self.boss_level_for_episode
+
+ for map_name in goal_levels:
+ if map_name + " - Exit" not in self.location_name_to_id:
+ continue
+
+ # Exit location names are in form: The Docks (E1M1) - Exit
+ loc = Locations.location_table[self.location_name_to_id[map_name + " - Exit"]]
+ if not self.included_episodes[loc["episode"] - 1]:
+ continue
+
+ # Map complete item names are in form: The Docks (E1M1) - Complete
+ if not state.has(map_name + " - Complete", self.player, 1):
+ return False
+
+ return True
+
+ def set_rules(self):
+ pro = self.options.pro.value
+ allow_death_logic = self.options.allow_death_logic.value
+
+ Rules.set_rules(self, self.included_episodes, pro)
+ self.multiworld.completion_condition[self.player] = lambda state: self.completion_rule(state)
+
+ # Forbid progression items to locations that can be missed and can't be picked up. (e.g. One-time timed
+ # platform) Unless the user allows for it.
+ if not allow_death_logic:
+ for death_logic_location in Locations.death_logic_locations:
+ self.options.exclude_locations.value.add(death_logic_location)
+
+ def create_item(self, name: str) -> HereticItem:
+ item_id: int = self.item_name_to_id[name]
+ return HereticItem(name, Items.item_table[item_id]["classification"], item_id, self.player)
+
+ def create_items(self):
+ itempool: List[HereticItem] = []
+ start_with_map_scrolls: bool = self.options.start_with_map_scrolls.value
+
+ # Items
+ for item_id, item in Items.item_table.items():
+ if item["doom_type"] == HERETIC_TYPE_LEVEL_COMPLETE:
+ continue # We'll fill it manually later
+
+ if item["doom_type"] == HERETIC_TYPE_MAP_SCROLL and start_with_map_scrolls:
+ continue # We'll fill it manually, and we will put fillers in place
+
+ if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]:
+ continue
+
+ count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
+ itempool += [self.create_item(item["name"]) for _ in range(count)]
+
+ # Place end level items in locked locations
+ for map_name in Maps.map_names:
+ loc_name = map_name + " - Exit"
+ item_name = map_name + " - Complete"
+
+ if loc_name not in self.location_name_to_id:
+ continue
+
+ if item_name not in self.item_name_to_id:
+ continue
+
+ loc = Locations.location_table[self.location_name_to_id[loc_name]]
+ if not self.included_episodes[loc["episode"] - 1]:
+ continue
+
+ self.multiworld.get_location(loc_name, self.player).place_locked_item(self.create_item(item_name))
+ self.location_count -= 1
+
+ # Give starting levels right away
+ for i in range(len(self.included_episodes)):
+ if self.included_episodes[i]:
+ self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i]))
+
+ # Give Computer area maps if option selected
+ if self.options.start_with_map_scrolls.value:
+ for item_id, item_dict in Items.item_table.items():
+ item_episode = item_dict["episode"]
+ if item_episode > 0:
+ if item_dict["doom_type"] == HERETIC_TYPE_MAP_SCROLL and self.included_episodes[item_episode - 1]:
+ self.multiworld.push_precollected(self.create_item(item_dict["name"]))
+
+ # Fill the rest starting with powerups, then fillers
+ self.create_ratioed_items("Chaos Device", itempool)
+ self.create_ratioed_items("Morph Ovum", itempool)
+ self.create_ratioed_items("Mystic Urn", itempool)
+ self.create_ratioed_items("Ring of Invincibility", itempool)
+ self.create_ratioed_items("Shadowsphere", itempool)
+ self.create_ratioed_items("Torch", itempool)
+ self.create_ratioed_items("Timebomb of the Ancients", itempool)
+ self.create_ratioed_items("Tome of Power", itempool)
+ self.create_ratioed_items("Silver Shield", itempool)
+ self.create_ratioed_items("Enchanted Shield", itempool)
+
+ while len(itempool) < self.location_count:
+ itempool.append(self.create_item(self.get_filler_item_name()))
+
+ # add itempool to multiworld
+ self.multiworld.itempool += itempool
+
+ def get_filler_item_name(self):
+ return self.multiworld.random.choice([
+ "Quartz Flask",
+ "Crystal Geode",
+ "Energy Orb",
+ "Greater Runes",
+ "Inferno Orb",
+ "Pile of Mace Spheres",
+ "Quiver of Ethereal Arrows"
+ ])
+
+ def create_ratioed_items(self, item_name: str, itempool: List[HereticItem]):
+ remaining_loc = self.location_count - len(itempool)
+ if remaining_loc <= 0:
+ return
+
+ episode_count = self.get_episode_count()
+ count = min(remaining_loc, max(1, self.items_ratio[item_name] * episode_count))
+ if count == 0:
+ logger.warning("Warning, no " + item_name + " will be placed.")
+ return
+
+ for i in range(count):
+ itempool.append(self.create_item(item_name))
+
+ def fill_slot_data(self) -> Dict[str, Any]:
+ slot_data = self.options.as_dict("goal", "difficulty", "random_monsters", "random_pickups", "random_music", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "check_sanity")
+
+ # Make sure we send proper episode settings
+ slot_data["episode1"] = self.included_episodes[0]
+ slot_data["episode2"] = self.included_episodes[1]
+ slot_data["episode3"] = self.included_episodes[2]
+ slot_data["episode4"] = self.included_episodes[3]
+ slot_data["episode5"] = self.included_episodes[4]
+
+ return slot_data
diff --git a/worlds/heretic/docs/en_Heretic.md b/worlds/heretic/docs/en_Heretic.md
new file mode 100644
index 000000000000..a7ae3d1ea748
--- /dev/null
+++ b/worlds/heretic/docs/en_Heretic.md
@@ -0,0 +1,23 @@
+# Heretic
+
+## Where is the options page?
+
+The [player options page](../player-options) contains the options needed to configure your game session.
+
+## What does randomization do to this game?
+
+Weapons, keys, and level unlocks have been randomized. Monsters and Pickups are also randomized. Typically, you will end up playing different levels out of order to find your keys and level unlocks and eventually complete your game.
+
+Maps can be selected on a level select screen. You can exit a level at any time by visiting the hub station at the beginning of each level. The state of each level is saved and restored upon re-entering the level.
+
+## What is the goal?
+
+The goal is to complete every level in the episodes you have chosen to play.
+
+## What is a "check" in The Heretic?
+
+Weapons, keys, and powerups have been replaced with Archipelago checks. Some have been selectively removed because Heretic contains a lot of collectibles. Usually when many bunch together, one was kept. The switch at the end of each level is also a check.
+
+## What "items" can you unlock in Heretic?
+
+Keys and level unlocks are your main progression items. Weapon unlocks and some upgrades are your useful items. Powerups, ammo, healing, and armor are filler items.
diff --git a/worlds/heretic/docs/setup_en.md b/worlds/heretic/docs/setup_en.md
new file mode 100644
index 000000000000..41b7fdab8078
--- /dev/null
+++ b/worlds/heretic/docs/setup_en.md
@@ -0,0 +1,51 @@
+# Heretic Randomizer Setup
+
+## Required Software
+
+- [Heretic (e.g. Steam version)](https://store.steampowered.com/app/2390/Heretic_Shadow_of_the_Serpent_Riders/)
+- [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases) (Same download for DOOM 1993, DOOM II and Heretic)
+
+## Optional Software
+
+- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases)
+
+## Installing APDoom
+1. Download [APDOOM.zip](https://github.com/Daivuk/apdoom/releases) and extract it.
+2. Copy HERETIC.WAD from your steam install into the extracted folder.
+ You can find the folder in steam by finding the game in your library,
+ right clicking it and choosing *Manage→Browse Local Files*. The WAD file is in the `/base/` folder.
+
+## Joining a MultiWorld Game
+
+1. Launch apdoom-launcher.exe
+2. Choose Heretic in the dropdown
+3. Enter the Archipelago server address, slot name, and password (if you have one)
+4. Press "Launch Game"
+5. Enjoy!
+
+To continue a game, follow the same connection steps.
+Connecting with a different seed won't erase your progress in other seeds.
+
+## Archipelago Text Client
+
+We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send.
+APDOOM has in-game messages,
+but they disappear quickly and there's no reasonable way to check your message history in-game.
+
+### Hinting
+
+To hint from in-game, use the chat (Default key: 'T'). Hinting from Heretic can be difficult because names are rather long and contain special characters. For example:
+```
+!hint The River of Fire (E2M3) - Green key
+```
+The game has a hint helper implemented, where you can simply type this:
+```
+!hint e2m3 green
+```
+For this to work, include the map short name (`E1M1`), followed by one of the keywords: `map`, `blue`, `yellow`, `green`.
+
+## Auto-Tracking
+
+APDOOM has a functional map tracker integrated into the level select screen.
+It tells you which levels you have unlocked, which keys you have for each level, which levels have been completed,
+and how many of the checks you have completed in each level.
diff --git a/worlds/hk/ExtractedData.py b/worlds/hk/ExtractedData.py
index cf796050e47f..0cbbc8bf8558 100644
--- a/worlds/hk/ExtractedData.py
+++ b/worlds/hk/ExtractedData.py
@@ -3,7 +3,7 @@
connectors = {'Room_temple[left1]': 'Crossroads_02[door1]', 'Tutorial_01[right1]': 'Town[left1]', 'Tutorial_01[top1]': None, 'Tutorial_01[top2]': 'Cliffs_02[bot1]', 'Town[left1]': 'Tutorial_01[right1]', 'Town[bot1]': 'Crossroads_01[top1]', 'Town[right1]': 'Mines_10[left1]', 'Town[top1]': None, 'Town[door_station]': 'Room_Town_Stag_Station[left1]', 'Town[door_sly]': 'Room_shop[left1]', 'Town[door_mapper]': 'Room_mapper[left1]', 'Town[door_jiji]': 'Room_Ouiji[left1]', 'Town[door_bretta]': 'Room_Bretta[right1]', 'Town[room_divine]': 'Grimm_Divine[left1]', 'Town[room_grimm]': 'Grimm_Main_Tent[left1]', 'Room_shop[left1]': 'Town[door_sly]', 'Room_Town_Stag_Station[left1]': 'Town[door_station]', 'Room_mapper[left1]': 'Town[door_mapper]', 'Room_Bretta[right1]': 'Town[door_bretta]', 'Room_Ouiji[left1]': 'Town[door_jiji]', 'Grimm_Divine[left1]': 'Town[room_divine]', 'Grimm_Main_Tent[left1]': 'Town[room_grimm]', 'Crossroads_01[top1]': 'Town[bot1]', 'Crossroads_01[left1]': 'Crossroads_07[right1]', 'Crossroads_01[right1]': 'Crossroads_02[left1]', 'Crossroads_02[left1]': 'Crossroads_01[right1]', 'Crossroads_02[door1]': 'Room_temple[left1]', 'Crossroads_02[right1]': 'Crossroads_39[left1]', 'Crossroads_03[right1]': 'Crossroads_15[left1]', 'Crossroads_03[right2]': 'Mines_33[left1]', 'Crossroads_03[left1]': 'Crossroads_21[right1]', 'Crossroads_03[left2]': 'Crossroads_47[right1]', 'Crossroads_03[bot1]': 'Crossroads_19[top1]', 'Crossroads_03[top1]': 'Crossroads_16[bot1]', 'Crossroads_04[left1]': 'Crossroads_19[right1]', 'Crossroads_04[top1]': 'Crossroads_27[bot1]', 'Crossroads_04[door_Mender_House]': 'Room_Mender_House[left1]', 'Crossroads_04[door1]': 'Room_ruinhouse[left1]', 'Crossroads_04[door_charmshop]': 'Room_Charm_Shop[left1]', 'Crossroads_04[right1]': 'Crossroads_50[left1]', 'Crossroads_05[left1]': 'Crossroads_07[right2]', 'Crossroads_05[right1]': 'Crossroads_40[left1]', 'Crossroads_06[left1]': 'Crossroads_33[right1]', 'Crossroads_06[door1]': 'Crossroads_ShamanTemple[left1]', 'Crossroads_06[right1]': 'Crossroads_10[left1]', 'Crossroads_07[left1]': 'Crossroads_38[right1]', 'Crossroads_07[left2]': 'Crossroads_11_alt[right1]', 'Crossroads_07[left3]': 'Crossroads_25[right1]', 'Crossroads_07[right1]': 'Crossroads_01[left1]', 'Crossroads_07[right2]': 'Crossroads_05[left1]', 'Crossroads_07[bot1]': 'Crossroads_33[top1]', 'Crossroads_08[left1]': 'Crossroads_33[right2]', 'Crossroads_08[left2]': 'Crossroads_18[right1]', 'Crossroads_08[right1]': 'Crossroads_30[left1]', 'Crossroads_08[right2]': 'Crossroads_13[left1]', 'Crossroads_09[left1]': 'Crossroads_36[right2]', 'Crossroads_09[right1]': 'Crossroads_33[left1]', 'Crossroads_10[left1]': 'Crossroads_06[right1]', 'Crossroads_10[right1]': 'Crossroads_21[left1]', 'Crossroads_11_alt[left1]': 'Fungus1_01[right1]', 'Crossroads_11_alt[right1]': 'Crossroads_07[left2]', 'Crossroads_12[left1]': 'Crossroads_35[right1]', 'Crossroads_12[right1]': 'Crossroads_33[left2]', 'Crossroads_13[left1]': 'Crossroads_08[right2]', 'Crossroads_13[right1]': 'Crossroads_42[left1]', 'Crossroads_14[left1]': 'Crossroads_39[right1]', 'Crossroads_14[left2]': 'Crossroads_16[right1]', 'Crossroads_14[right1]': 'Crossroads_48[left1]', 'Crossroads_14[right2]': 'Crossroads_45[left1]', 'Crossroads_15[left1]': 'Crossroads_03[right1]', 'Crossroads_15[right1]': 'Crossroads_27[left1]', 'Crossroads_16[left1]': 'Crossroads_40[right1]', 'Crossroads_16[right1]': 'Crossroads_14[left2]', 'Crossroads_16[bot1]': 'Crossroads_03[top1]', 'Crossroads_18[right1]': 'Crossroads_08[left2]', 'Crossroads_18[right2]': 'Crossroads_52[left1]', 'Crossroads_18[bot1]': 'Fungus2_06[top1]', 'Crossroads_19[right1]': 'Crossroads_04[left1]', 'Crossroads_19[top1]': 'Crossroads_03[bot1]', 'Crossroads_19[left1]': 'Crossroads_42[right1]', 'Crossroads_19[left2]': 'Crossroads_43[right1]', 'Crossroads_21[left1]': 'Crossroads_10[right1]', 'Crossroads_21[right1]': 'Crossroads_03[left1]', 'Crossroads_21[top1]': 'Crossroads_22[bot1]', 'Crossroads_22[bot1]': 'Crossroads_21[top1]', 'Crossroads_25[right1]': 'Crossroads_07[left3]', 'Crossroads_25[left1]': 'Crossroads_36[right1]', 'Crossroads_27[right1]': 'Crossroads_46[left1]', 'Crossroads_27[bot1]': 'Crossroads_04[top1]', 'Crossroads_27[left1]': 'Crossroads_15[right1]', 'Crossroads_27[left2]': 'Crossroads_31[right1]', 'Crossroads_30[left1]': 'Crossroads_08[right1]', 'Crossroads_31[right1]': 'Crossroads_27[left2]', 'Crossroads_33[top1]': 'Crossroads_07[bot1]', 'Crossroads_33[left1]': 'Crossroads_09[right1]', 'Crossroads_33[left2]': 'Crossroads_12[right1]', 'Crossroads_33[right1]': 'Crossroads_06[left1]', 'Crossroads_33[right2]': 'Crossroads_08[left1]', 'Crossroads_35[bot1]': 'Fungus3_26[top1]', 'Crossroads_35[right1]': 'Crossroads_12[left1]', 'Crossroads_36[right1]': 'Crossroads_25[left1]', 'Crossroads_36[right2]': 'Crossroads_09[left1]', 'Crossroads_37[right1]': 'Crossroads_49[left1]', 'Crossroads_38[right1]': 'Crossroads_07[left1]', 'Crossroads_39[right1]': 'Crossroads_14[left1]', 'Crossroads_39[left1]': 'Crossroads_02[right1]', 'Crossroads_40[right1]': 'Crossroads_16[left1]', 'Crossroads_40[left1]': 'Crossroads_05[right1]', 'Crossroads_42[left1]': 'Crossroads_13[right1]', 'Crossroads_42[right1]': 'Crossroads_19[left1]', 'Crossroads_43[left1]': 'Crossroads_49[right1]', 'Crossroads_43[right1]': 'Crossroads_19[left2]', 'Crossroads_45[right1]': 'Mines_01[left1]', 'Crossroads_45[left1]': 'Crossroads_14[right2]', 'Crossroads_46[left1]': 'Crossroads_27[right1]', 'Crossroads_46b[right1]': 'RestingGrounds_02[left1]', 'Crossroads_ShamanTemple[left1]': 'Crossroads_06[door1]', 'Crossroads_47[right1]': 'Crossroads_03[left2]', 'Crossroads_48[left1]': 'Crossroads_14[right1]', 'Crossroads_49[right1]': 'Crossroads_43[left1]', 'Crossroads_49[left1]': 'Crossroads_37[right1]', 'Crossroads_49b[right1]': 'Ruins1_28[left1]', 'Crossroads_50[right1]': 'RestingGrounds_06[left1]', 'Crossroads_50[left1]': 'Crossroads_04[right1]', 'Crossroads_52[left1]': 'Crossroads_18[right2]', 'Room_ruinhouse[left1]': 'Crossroads_04[door1]', 'Room_Charm_Shop[left1]': 'Crossroads_04[door_charmshop]', 'Room_Mender_House[left1]': 'Crossroads_04[door_Mender_House]', 'Fungus1_01[left1]': 'Fungus1_01b[right1]', 'Fungus1_01[right1]': 'Crossroads_11_alt[left1]', 'Fungus1_01b[left1]': 'Fungus1_02[right1]', 'Fungus1_01b[right1]': 'Fungus1_01[left1]', 'Fungus1_02[left1]': 'Fungus1_17[right1]', 'Fungus1_02[right1]': 'Fungus1_01b[left1]', 'Fungus1_02[right2]': 'Fungus1_06[left1]', 'Fungus1_03[left1]': 'Fungus1_31[right1]', 'Fungus1_03[right1]': 'Fungus1_17[left1]', 'Fungus1_03[bot1]': 'Fungus1_05[top1]', 'Fungus1_04[left1]': 'Fungus1_25[right1]', 'Fungus1_04[right1]': 'Fungus1_21[left1]', 'Fungus1_05[right1]': 'Fungus1_14[left1]', 'Fungus1_05[bot1]': 'Fungus1_10[top1]', 'Fungus1_05[top1]': 'Fungus1_03[bot1]', 'Fungus1_06[left1]': 'Fungus1_02[right2]', 'Fungus1_06[bot1]': 'Fungus1_07[top1]', 'Fungus1_07[top1]': 'Fungus1_06[bot1]', 'Fungus1_07[left1]': 'Fungus1_19[right1]', 'Fungus1_07[right1]': 'Fungus1_08[left1]', 'Fungus1_08[left1]': 'Fungus1_07[right1]', 'Fungus1_09[left1]': 'Fungus1_15[right1]', 'Fungus1_09[right1]': 'Fungus1_30[left1]', 'Fungus1_10[left1]': 'Fungus1_30[right1]', 'Fungus1_10[right1]': 'Fungus1_19[left1]', 'Fungus1_10[top1]': 'Fungus1_05[bot1]', 'Fungus1_11[top1]': 'Fungus1_19[bot1]', 'Fungus1_11[right1]': 'Fungus1_34[left1]', 'Fungus1_11[right2]': 'Fungus1_37[left1]', 'Fungus1_11[left1]': 'Fungus1_29[right1]', 'Fungus1_11[bot1]': 'Fungus3_01[top1]', 'Fungus1_12[left1]': 'Fungus1_13[right1]', 'Fungus1_12[right1]': 'Fungus1_29[left1]', 'Fungus1_13[right1]': 'Fungus1_12[left1]', 'Fungus1_13[left1]': 'Fungus3_22[right1]', 'Fungus1_14[left1]': 'Fungus1_05[right1]', 'Fungus1_15[door1]': 'Room_nailmaster_02[left1]', 'Fungus1_15[right1]': 'Fungus1_09[left1]', 'Fungus1_16_alt[right1]': 'Fungus1_22[left1]', 'Fungus1_17[left1]': 'Fungus1_03[right1]', 'Fungus1_17[right1]': 'Fungus1_02[left1]', 'Fungus1_19[left1]': 'Fungus1_10[right1]', 'Fungus1_19[right1]': 'Fungus1_07[left1]', 'Fungus1_19[bot1]': 'Fungus1_11[top1]', 'Fungus1_20_v02[bot1]': 'Fungus1_21[top1]', 'Fungus1_20_v02[bot2]': 'Fungus1_32[top1]', 'Fungus1_20_v02[right1]': 'Fungus1_28[left2]', 'Fungus1_21[bot1]': 'Fungus1_22[top1]', 'Fungus1_21[top1]': 'Fungus1_20_v02[bot1]', 'Fungus1_21[left1]': 'Fungus1_04[right1]', 'Fungus1_21[right1]': 'Fungus1_32[left1]', 'Fungus1_22[bot1]': 'Fungus1_30[top1]', 'Fungus1_22[top1]': 'Fungus1_21[bot1]', 'Fungus1_22[left1]': 'Fungus1_16_alt[right1]', 'Fungus1_23[left1]': 'Fungus3_48[right2]', 'Fungus1_23[right1]': 'Fungus3_13[left1]', 'Fungus1_24[left1]': 'Fungus3_05[right1]', 'Fungus1_25[right1]': 'Fungus1_04[left1]', 'Fungus1_25[left1]': 'Fungus1_26[right1]', 'Fungus1_26[right1]': 'Fungus1_25[left1]', 'Fungus1_26[left1]': 'Fungus1_Slug[right1]', 'Fungus1_26[door_SlugShrine]': 'Room_Slug_Shrine[left1]', 'Fungus1_28[left1]': 'Cliffs_01[right3]', 'Fungus1_28[left2]': 'Fungus1_20_v02[right1]', 'Fungus1_29[left1]': 'Fungus1_12[right1]', 'Fungus1_29[right1]': 'Fungus1_11[left1]', 'Fungus1_30[top1]': 'Fungus1_22[bot1]', 'Fungus1_30[top3]': 'Fungus1_31[bot1]', 'Fungus1_30[left1]': 'Fungus1_09[right1]', 'Fungus1_30[right1]': 'Fungus1_10[left1]', 'Fungus1_31[top1]': 'Fungus1_32[bot1]', 'Fungus1_31[bot1]': 'Fungus1_30[top3]', 'Fungus1_31[right1]': 'Fungus1_03[left1]', 'Fungus1_32[bot1]': 'Fungus1_31[top1]', 'Fungus1_32[top1]': 'Fungus1_20_v02[bot2]', 'Fungus1_32[left1]': 'Fungus1_21[right1]', 'Fungus1_34[door1]': 'Fungus1_35[left1]', 'Fungus1_34[left1]': 'Fungus1_11[right1]', 'Fungus1_35[left1]': 'Fungus1_34[door1]', 'Fungus1_35[right1]': 'Fungus1_36[left1]', 'Fungus1_36[left1]': 'Fungus1_35[right1]', 'Fungus1_37[left1]': 'Fungus1_11[right2]', 'Fungus1_Slug[right1]': 'Fungus1_26[left1]', 'Room_Slug_Shrine[left1]': 'Fungus1_26[door_SlugShrine]', 'Room_nailmaster_02[left1]': 'Fungus1_15[door1]', 'Fungus3_01[top1]': 'Fungus1_11[bot1]', 'Fungus3_01[right1]': 'Fungus3_25[left1]', 'Fungus3_01[left1]': 'Fungus3_24[right1]', 'Fungus3_01[right2]': 'Fungus3_02[left1]', 'Fungus3_02[left1]': 'Fungus3_01[right2]', 'Fungus3_02[left2]': 'Fungus3_03[right1]', 'Fungus3_02[left3]': 'Fungus3_35[right1]', 'Fungus3_02[right1]': 'Fungus3_47[left1]', 'Fungus3_02[right2]': 'Fungus2_01[left1]', 'Fungus3_03[right1]': 'Fungus3_02[left2]', 'Fungus3_03[left1]': 'Fungus3_34[right1]', 'Fungus3_24[right1]': 'Fungus3_01[left1]', 'Fungus3_24[left1]': 'Fungus3_44[right1]', 'Fungus3_24[top1]': 'Fungus3_30[bot1]', 'Fungus3_25[right1]': 'Fungus3_25b[left1]', 'Fungus3_25[left1]': 'Fungus3_01[right1]', 'Fungus3_25b[right1]': 'Fungus3_26[left2]', 'Fungus3_25b[left1]': 'Fungus3_25[right1]', 'Fungus3_26[top1]': 'Crossroads_35[bot1]', 'Fungus3_26[left1]': 'Fungus3_28[right1]', 'Fungus3_26[left2]': 'Fungus3_25b[right1]', 'Fungus3_26[left3]': 'Fungus3_27[right1]', 'Fungus3_26[right1]': 'Fungus2_33[left1]', 'Fungus3_27[left1]': 'Fungus3_47[right1]', 'Fungus3_27[right1]': 'Fungus3_26[left3]', 'Fungus3_28[right1]': 'Fungus3_26[left1]', 'Fungus3_30[bot1]': 'Fungus3_24[top1]', 'Fungus3_35[right1]': 'Fungus3_02[left3]', 'Fungus3_44[bot1]': 'Fungus3_34[top1]', 'Fungus3_44[door1]': 'Room_Fungus_Shaman[left1]', 'Fungus3_44[right1]': 'Fungus3_24[left1]', 'Fungus3_47[left1]': 'Fungus3_02[right1]', 'Fungus3_47[right1]': 'Fungus3_27[left1]', 'Fungus3_47[door1]': 'Fungus3_archive[left1]', 'Room_Fungus_Shaman[left1]': 'Fungus3_44[door1]', 'Fungus3_archive[left1]': 'Fungus3_47[door1]', 'Fungus3_archive[bot1]': 'Fungus3_archive_02[top1]', 'Fungus3_archive_02[top1]': 'Fungus3_archive[bot1]', 'Fungus2_01[left1]': 'Fungus3_02[right2]', 'Fungus2_01[left2]': 'Fungus2_02[right1]', 'Fungus2_01[left3]': 'Fungus2_34[right1]', 'Fungus2_01[right1]': 'Fungus2_03[left1]', 'Fungus2_02[right1]': 'Fungus2_01[left2]', 'Fungus2_34[right1]': 'Fungus2_01[left3]', 'Fungus2_03[left1]': 'Fungus2_01[right1]', 'Fungus2_03[bot1]': 'Fungus2_18[top1]', 'Fungus2_03[right1]': 'Fungus2_04[left1]', 'Fungus2_04[top1]': 'Fungus2_05[bot1]', 'Fungus2_04[right1]': 'Fungus2_28[left1]', 'Fungus2_04[left1]': 'Fungus2_03[right1]', 'Fungus2_04[right2]': 'Fungus2_28[left2]', 'Fungus2_05[bot1]': 'Fungus2_04[top1]', 'Fungus2_05[right1]': 'Fungus2_06[left1]', 'Fungus2_06[top1]': 'Crossroads_18[bot1]', 'Fungus2_06[left1]': 'Fungus2_05[right1]', 'Fungus2_06[left2]': 'Fungus2_33[right1]', 'Fungus2_06[right1]': 'Fungus2_26[left1]', 'Fungus2_06[right2]': 'Fungus2_07[left1]', 'Fungus2_07[left1]': 'Fungus2_06[right2]', 'Fungus2_07[right1]': 'Fungus2_08[left1]', 'Fungus2_08[left1]': 'Fungus2_07[right1]', 'Fungus2_08[left2]': 'Fungus2_09[right1]', 'Fungus2_08[right1]': 'Fungus2_32[left1]', 'Fungus2_09[left1]': 'Fungus2_10[right1]', 'Fungus2_09[right1]': 'Fungus2_08[left2]', 'Fungus2_10[right1]': 'Fungus2_09[left1]', 'Fungus2_10[right2]': 'Fungus2_21[left1]', 'Fungus2_10[bot1]': 'Fungus2_11[top1]', 'Fungus2_11[top1]': 'Fungus2_10[bot1]', 'Fungus2_11[left1]': 'Fungus2_18[right1]', 'Fungus2_11[left2]': 'Fungus2_17[right1]', 'Fungus2_11[right1]': 'Fungus2_12[left1]', 'Fungus2_12[left1]': 'Fungus2_11[right1]', 'Fungus2_12[bot1]': 'Fungus2_13[top1]', 'Fungus2_13[top1]': 'Fungus2_12[bot1]', 'Fungus2_13[left2]': 'Fungus2_14[right1]', 'Fungus2_13[left3]': 'Fungus2_23[right1]', 'Fungus2_14[top1]': 'Fungus2_17[bot1]', 'Fungus2_14[right1]': 'Fungus2_13[left2]', 'Fungus2_14[bot3]': 'Fungus2_15[top3]', 'Fungus2_15[top3]': 'Fungus2_14[bot3]', 'Fungus2_15[right1]': 'Fungus2_31[left1]', 'Fungus2_15[left1]': 'Fungus2_25[right1]', 'Fungus2_17[left1]': 'Fungus2_29[right1]', 'Fungus2_17[right1]': 'Fungus2_11[left2]', 'Fungus2_17[bot1]': 'Fungus2_14[top1]', 'Fungus2_18[right1]': 'Fungus2_11[left1]', 'Fungus2_18[bot1]': 'Fungus2_19[top1]', 'Fungus2_18[top1]': 'Fungus2_03[bot1]', 'Fungus2_19[top1]': 'Fungus2_18[bot1]', 'Fungus2_19[left1]': 'Fungus2_20[right1]', 'Fungus2_20[right1]': 'Fungus2_19[left1]', 'Fungus2_20[left1]': 'Deepnest_01[right1]', 'Fungus2_21[right1]': 'Ruins1_01[left1]', 'Fungus2_21[left1]': 'Fungus2_10[right2]', 'Fungus2_23[right1]': 'Fungus2_13[left3]', 'Fungus2_23[right2]': 'Waterways_09[left1]', 'Fungus2_26[left1]': 'Fungus2_06[right1]', 'Fungus2_28[left1]': 'Fungus2_04[right1]', 'Fungus2_28[left2]': 'Fungus2_04[right2]', 'Fungus2_29[right1]': 'Fungus2_17[left1]', 'Fungus2_29[bot1]': 'Fungus2_30[top1]', 'Fungus2_30[bot1]': 'Fungus2_25[top2]', 'Fungus2_30[top1]': 'Fungus2_29[bot1]', 'Fungus2_31[left1]': 'Fungus2_15[right1]', 'Fungus2_32[left1]': 'Fungus2_08[right1]', 'Fungus2_33[right1]': 'Fungus2_06[left2]', 'Fungus2_33[left1]': 'Fungus3_26[right1]', 'Deepnest_01[right1]': 'Fungus2_20[left1]', 'Deepnest_01[bot1]': 'Deepnest_01b[top1]', 'Deepnest_01[bot2]': 'Deepnest_01b[top2]', 'Deepnest_01[left1]': 'Fungus3_39[right1]', 'Deepnest_01b[top1]': 'Deepnest_01[bot1]', 'Deepnest_01b[top2]': None, 'Deepnest_01b[right1]': 'Deepnest_02[left1]', 'Deepnest_01b[right2]': 'Deepnest_02[left2]', 'Deepnest_01b[bot1]': 'Deepnest_17[top1]', 'Deepnest_02[left1]': 'Deepnest_01b[right1]', 'Deepnest_02[left2]': 'Deepnest_01b[right2]', 'Deepnest_02[right1]': 'Deepnest_36[left1]', 'Deepnest_03[right1]': 'Deepnest_30[left1]', 'Deepnest_03[left1]': 'Deepnest_34[right1]', 'Deepnest_03[top1]': 'Deepnest_33[bot1]', 'Deepnest_03[left2]': 'Deepnest_31[right1]', 'Deepnest_09[left1]': 'Deepnest_10[right1]', 'Deepnest_10[right1]': 'Deepnest_09[left1]', 'Deepnest_10[right2]': 'Deepnest_41[left1]', 'Deepnest_10[right3]': 'Deepnest_41[left2]', 'Deepnest_10[door1]': 'Deepnest_Spider_Town[left1]', 'Deepnest_10[door2]': 'Room_spider_small[left1]', 'Room_spider_small[left1]': 'Deepnest_10[door2]', 'Deepnest_Spider_Town[left1]': 'Deepnest_10[door1]', 'Deepnest_14[right1]': 'Deepnest_17[left1]', 'Deepnest_14[left1]': 'Deepnest_26[right1]', 'Deepnest_14[bot1]': 'Deepnest_33[top1]', 'Deepnest_14[bot2]': 'Deepnest_33[top2]', 'Deepnest_16[left1]': 'Deepnest_17[right1]', 'Deepnest_16[bot1]': 'Fungus2_25[top1]', 'Deepnest_17[left1]': 'Deepnest_14[right1]', 'Deepnest_17[right1]': 'Deepnest_16[left1]', 'Deepnest_17[top1]': 'Deepnest_01b[bot1]', 'Deepnest_17[bot1]': 'Deepnest_30[top1]', 'Fungus2_25[top1]': 'Deepnest_16[bot1]', 'Fungus2_25[top2]': None, 'Fungus2_25[right1]': 'Fungus2_15[left1]', 'Deepnest_26[left1]': 'Deepnest_26b[right1]', 'Deepnest_26[left2]': 'Deepnest_26b[right2]', 'Deepnest_26[right1]': 'Deepnest_14[left1]', 'Deepnest_26[bot1]': 'Deepnest_35[top1]', 'Deepnest_26b[right2]': 'Deepnest_26[left2]', 'Deepnest_26b[right1]': 'Deepnest_26[left1]', 'Deepnest_30[left1]': 'Deepnest_03[right1]', 'Deepnest_30[top1]': 'Deepnest_17[bot1]', 'Deepnest_30[right1]': 'Deepnest_37[left1]', 'Deepnest_31[right1]': 'Deepnest_03[left2]', 'Deepnest_31[right2]': 'Deepnest_32[left1]', 'Deepnest_32[left1]': 'Deepnest_31[right2]', 'Deepnest_33[top1]': 'Deepnest_14[bot1]', 'Deepnest_33[top2]': 'Deepnest_14[bot2]', 'Deepnest_33[bot1]': 'Deepnest_03[top1]', 'Deepnest_34[left1]': 'Deepnest_39[right1]', 'Deepnest_34[right1]': 'Deepnest_03[left1]', 'Deepnest_34[top1]': 'Deepnest_35[bot1]', 'Deepnest_35[left1]': 'Deepnest_40[right1]', 'Deepnest_35[top1]': 'Deepnest_26[bot1]', 'Deepnest_35[bot1]': 'Deepnest_34[top1]', 'Deepnest_36[left1]': 'Deepnest_02[right1]', 'Deepnest_37[left1]': 'Deepnest_30[right1]', 'Deepnest_37[right1]': 'Abyss_03_b[left1]', 'Deepnest_37[top1]': 'Deepnest_38[bot1]', 'Deepnest_37[bot1]': 'Deepnest_44[top1]', 'Deepnest_38[bot1]': 'Deepnest_37[top1]', 'Deepnest_39[left1]': 'Deepnest_41[right1]', 'Deepnest_39[top1]': 'Deepnest_42[bot1]', 'Deepnest_39[door1]': 'Deepnest_45_v02[left1]', 'Deepnest_39[right1]': 'Deepnest_34[left1]', 'Deepnest_40[right1]': 'Deepnest_35[left1]', 'Deepnest_41[right1]': 'Deepnest_39[left1]', 'Deepnest_41[left1]': 'Deepnest_10[right2]', 'Deepnest_41[left2]': 'Deepnest_10[right3]', 'Deepnest_42[bot1]': 'Deepnest_39[top1]', 'Deepnest_42[left1]': 'Room_Mask_Maker[right1]', 'Deepnest_42[top1]': 'Deepnest_43[bot1]', 'Deepnest_43[bot1]': 'Deepnest_42[top1]', 'Deepnest_43[left1]': 'Fungus3_50[right1]', 'Deepnest_43[right1]': 'Fungus3_08[left1]', 'Deepnest_44[top1]': 'Deepnest_37[bot1]', 'Deepnest_45_v02[left1]': 'Deepnest_39[door1]', 'Room_Mask_Maker[right1]': 'Deepnest_42[left1]', 'Deepnest_East_01[bot1]': 'Abyss_03_c[top1]', 'Deepnest_East_01[right1]': 'Hive_03_c[left1]', 'Deepnest_East_01[top1]': 'Deepnest_East_02[bot1]', 'Deepnest_East_02[bot1]': 'Deepnest_East_01[top1]', 'Deepnest_East_02[bot2]': 'Hive_03[top1]', 'Deepnest_East_02[top1]': 'Waterways_14[bot2]', 'Deepnest_East_02[right1]': 'Deepnest_East_03[left2]', 'Deepnest_East_03[left1]': 'Ruins2_07[right1]', 'Deepnest_East_03[left2]': 'Deepnest_East_02[right1]', 'Deepnest_East_03[top1]': 'Deepnest_East_07[bot1]', 'Deepnest_East_03[top2]': None, 'Deepnest_East_03[right1]': 'Deepnest_East_04[left1]', 'Deepnest_East_03[right2]': 'Deepnest_East_06[left1]', 'Deepnest_East_04[left1]': 'Deepnest_East_03[right1]', 'Deepnest_East_04[left2]': 'Deepnest_East_07[right1]', 'Deepnest_East_04[right2]': 'Deepnest_East_15[left1]', 'Deepnest_East_04[right1]': 'Deepnest_East_11[left1]', 'Deepnest_East_06[top1]': 'Deepnest_East_18[bot1]', 'Deepnest_East_06[left1]': 'Deepnest_East_03[right2]', 'Deepnest_East_06[bot1]': 'Deepnest_East_14b[top1]', 'Deepnest_East_06[door1]': 'Room_nailmaster_03[left1]', 'Deepnest_East_06[right1]': 'Deepnest_East_16[left1]', 'Deepnest_East_07[bot1]': 'Deepnest_East_03[top1]', 'Deepnest_East_07[bot2]': 'Deepnest_East_03[top2]', 'Deepnest_East_07[left1]': 'Deepnest_East_08[right1]', 'Deepnest_East_07[left2]': 'Ruins2_11_b[right1]', 'Deepnest_East_07[right1]': 'Deepnest_East_04[left2]', 'Deepnest_East_08[right1]': 'Deepnest_East_07[left1]', 'Deepnest_East_08[top1]': 'Deepnest_East_09[bot1]', 'Deepnest_East_09[right1]': 'Room_Colosseum_01[left1]', 'Deepnest_East_09[left1]': 'Ruins2_10b[right1]', 'Deepnest_East_09[bot1]': 'Deepnest_East_08[top1]', 'Deepnest_East_10[left1]': 'Deepnest_East_18[right2]', 'Deepnest_East_11[right1]': 'Deepnest_East_12[left1]', 'Deepnest_East_11[left1]': 'Deepnest_East_04[right1]', 'Deepnest_East_11[top1]': 'Deepnest_East_13[bot1]', 'Deepnest_East_11[bot1]': 'Deepnest_East_18[top1]', 'Deepnest_East_12[right1]': 'Deepnest_East_Hornet[left1]', 'Deepnest_East_12[left1]': 'Deepnest_East_11[right1]', 'Deepnest_East_13[bot1]': 'Deepnest_East_11[top1]', 'Deepnest_East_14[top2]': 'Deepnest_East_16[bot1]', 'Deepnest_East_14[left1]': 'Deepnest_East_14b[right1]', 'Deepnest_East_14[door1]': 'Deepnest_East_17[left1]', 'Deepnest_East_14b[right1]': 'Deepnest_East_14[left1]', 'Deepnest_East_14b[top1]': 'Deepnest_East_06[bot1]', 'Deepnest_East_15[left1]': 'Deepnest_East_04[right2]', 'Deepnest_East_16[left1]': 'Deepnest_East_06[right1]', 'Deepnest_East_16[bot1]': 'Deepnest_East_14[top2]', 'Deepnest_East_17[left1]': 'Deepnest_East_14[door1]', 'Deepnest_East_18[top1]': 'Deepnest_East_11[bot1]', 'Deepnest_East_18[bot1]': 'Deepnest_East_06[top1]', 'Deepnest_East_18[right2]': 'Deepnest_East_10[left1]', 'Room_nailmaster_03[left1]': 'Deepnest_East_06[door1]', 'Deepnest_East_Hornet[left1]': 'Deepnest_East_12[right1]', 'Deepnest_East_Hornet[left2]': 'Room_Wyrm[right1]', 'Room_Wyrm[right1]': 'Deepnest_East_Hornet[left2]', 'GG_Lurker[left1]': 'Room_Colosseum_Spectate[right1]', 'Hive_01[left1]': 'Abyss_03_c[right1]', 'Hive_01[right1]': 'Hive_02[left2]', 'Hive_01[right2]': 'Hive_02[left3]', 'Hive_02[left1]': 'Hive_03_c[right3]', 'Hive_02[left2]': 'Hive_01[right1]', 'Hive_02[left3]': 'Hive_01[right2]', 'Hive_03_c[left1]': 'Deepnest_East_01[right1]', 'Hive_03_c[right2]': 'Hive_04[left2]', 'Hive_03_c[right3]': 'Hive_02[left1]', 'Hive_03_c[top1]': 'Hive_03[bot1]', 'Hive_03[bot1]': 'Hive_03_c[top1]', 'Hive_03[right1]': 'Hive_04[left1]', 'Hive_03[top1]': 'Deepnest_East_02[bot2]', 'Hive_04[left1]': 'Hive_03[right1]', 'Hive_04[left2]': 'Hive_03_c[right2]', 'Hive_04[right1]': 'Hive_05[left1]', 'Hive_05[left1]': 'Hive_04[right1]', 'Room_Colosseum_01[left1]': 'Deepnest_East_09[right1]', 'Room_Colosseum_01[bot1]': 'Room_Colosseum_02[top1]', 'Room_Colosseum_02[top1]': 'Room_Colosseum_01[bot1]', 'Room_Colosseum_02[top2]': 'Room_Colosseum_Spectate[bot1]', 'Room_Colosseum_Spectate[bot1]': 'Room_Colosseum_02[top2]', 'Room_Colosseum_Spectate[right1]': 'GG_Lurker[left1]', 'Abyss_01[left1]': 'Waterways_05[right1]', 'Abyss_01[left2]': 'Waterways_06[right1]', 'Abyss_01[left3]': 'Abyss_02[right1]', 'Abyss_01[right1]': 'Ruins2_04[left2]', 'Abyss_01[right2]': 'Waterways_07[left1]', 'Abyss_02[right1]': 'Abyss_01[left3]', 'Abyss_02[bot1]': 'Abyss_03[top1]', 'Abyss_03[bot1]': 'Abyss_17[top1]', 'Abyss_03[bot2]': 'Abyss_04[top1]', 'Abyss_03[top1]': 'Abyss_02[bot1]', 'Abyss_03_b[left1]': 'Deepnest_37[right1]', 'Abyss_03_c[right1]': 'Hive_01[left1]', 'Abyss_03_c[top1]': 'Deepnest_East_01[bot1]', 'Abyss_04[top1]': 'Abyss_03[bot2]', 'Abyss_04[left1]': 'Abyss_18[right1]', 'Abyss_04[bot1]': 'Abyss_06_Core[top1]', 'Abyss_04[right1]': 'Abyss_05[left1]', 'Abyss_05[left1]': 'Abyss_04[right1]', 'Abyss_05[right1]': 'Abyss_22[left1]', 'Abyss_06_Core[top1]': 'Abyss_04[bot1]', 'Abyss_06_Core[left1]': 'Abyss_08[right1]', 'Abyss_06_Core[left3]': 'Abyss_12[right1]', 'Abyss_06_Core[right2]': 'Abyss_16[left1]', 'Abyss_06_Core[bot1]': 'Abyss_15[top1]', 'Abyss_08[right1]': 'Abyss_06_Core[left1]', 'Abyss_09[right1]': 'Abyss_10[left1]', 'Abyss_09[right2]': 'Abyss_Lighthouse_room[left1]', 'Abyss_09[right3]': 'Abyss_10[left2]', 'Abyss_09[left1]': 'Abyss_16[right1]', 'Abyss_10[left1]': 'Abyss_09[right1]', 'Abyss_10[left2]': 'Abyss_09[right3]', 'Abyss_12[right1]': 'Abyss_06_Core[left3]', 'Abyss_15[top1]': 'Abyss_06_Core[bot1]', 'Abyss_16[left1]': 'Abyss_06_Core[right2]', 'Abyss_16[right1]': 'Abyss_09[left1]', 'Abyss_17[top1]': 'Abyss_03[bot1]', 'Abyss_18[left1]': 'Abyss_19[right1]', 'Abyss_18[right1]': 'Abyss_04[left1]', 'Abyss_19[left1]': 'Abyss_21[right1]', 'Abyss_19[right1]': 'Abyss_18[left1]', 'Abyss_19[bot1]': 'Abyss_20[top1]', 'Abyss_19[bot2]': 'Abyss_20[top2]', 'Abyss_20[top1]': 'Abyss_19[bot1]', 'Abyss_20[top2]': 'Abyss_19[bot2]', 'Abyss_21[right1]': 'Abyss_19[left1]', 'Abyss_22[left1]': 'Abyss_05[right1]', 'Abyss_Lighthouse_room[left1]': 'Abyss_09[right2]', 'Waterways_01[top1]': 'Ruins1_05b[bot1]', 'Waterways_01[left1]': 'Waterways_04[right1]', 'Waterways_01[right1]': 'Waterways_03[left1]', 'Waterways_01[bot1]': 'Waterways_02[top1]', 'Waterways_02[top1]': 'Waterways_01[bot1]', 'Waterways_02[top2]': 'Waterways_05[bot1]', 'Waterways_02[top3]': 'Waterways_04[bot1]', 'Waterways_02[bot1]': 'Waterways_08[top1]', 'Waterways_02[bot2]': 'Waterways_06[top1]', 'Waterways_03[left1]': 'Waterways_01[right1]', 'Waterways_04[bot1]': 'Waterways_02[top3]', 'Waterways_04[right1]': 'Waterways_01[left1]', 'Waterways_04[left1]': 'Waterways_04b[right1]', 'Waterways_04[left2]': 'Waterways_04b[right2]', 'Waterways_04b[right1]': 'Waterways_04[left1]', 'Waterways_04b[right2]': 'Waterways_04[left2]', 'Waterways_04b[left1]': 'Waterways_09[right1]', 'Waterways_05[right1]': 'Abyss_01[left1]', 'Waterways_05[bot1]': 'Waterways_02[top2]', 'Waterways_05[bot2]': 'Waterways_15[top1]', 'Waterways_06[right1]': 'Abyss_01[left2]', 'Waterways_06[top1]': 'Waterways_02[bot2]', 'Waterways_07[right1]': 'Waterways_13[left1]', 'Waterways_07[right2]': 'Waterways_13[left2]', 'Waterways_07[left1]': 'Abyss_01[right2]', 'Waterways_07[door1]': 'Ruins_House_03[left2]', 'Waterways_07[top1]': 'Waterways_14[bot1]', 'Waterways_08[top1]': 'Waterways_02[bot1]', 'Waterways_08[left1]': 'Waterways_12[right1]', 'Waterways_08[left2]': 'GG_Pipeway[right1]', 'Waterways_09[right1]': 'Waterways_04b[left1]', 'Waterways_09[left1]': 'Fungus2_23[right2]', 'Waterways_12[right1]': 'Waterways_08[left1]', 'Waterways_13[left1]': 'Waterways_07[right1]', 'Waterways_13[left2]': 'Waterways_07[right2]', 'Waterways_14[bot1]': 'Waterways_07[top1]', 'Waterways_14[bot2]': 'Deepnest_East_02[top1]', 'Waterways_15[top1]': 'Waterways_05[bot2]', 'GG_Pipeway[right1]': 'Waterways_08[left2]', 'GG_Pipeway[left1]': 'GG_Waterways[right1]', 'GG_Waterways[right1]': 'GG_Pipeway[left1]', 'GG_Waterways[door1]': 'Room_GG_Shortcut[left1]', 'Room_GG_Shortcut[left1]': 'GG_Waterways[door1]', 'Room_GG_Shortcut[top1]': 'Ruins1_04[bot1]', 'Ruins1_01[left1]': 'Fungus2_21[right1]', 'Ruins1_01[top1]': 'Ruins1_17[bot1]', 'Ruins1_01[bot1]': 'Ruins1_02[top1]', 'Ruins1_02[top1]': 'Ruins1_01[bot1]', 'Ruins1_02[bot1]': 'Ruins1_03[top1]', 'Ruins1_03[top1]': 'Ruins1_02[bot1]', 'Ruins1_03[left1]': 'Ruins1_04[right1]', 'Ruins1_03[right1]': 'Ruins1_05c[left2]', 'Ruins1_03[right2]': 'Ruins1_05b[left1]', 'Ruins1_04[right1]': 'Ruins1_03[left1]', 'Ruins1_04[door1]': 'Room_nailsmith[left1]', 'Ruins1_04[bot1]': 'Room_GG_Shortcut[top1]', 'Ruins1_05b[left1]': 'Ruins1_03[right2]', 'Ruins1_05b[top1]': 'Ruins1_05c[bot1]', 'Ruins1_05b[bot1]': 'Waterways_01[top1]', 'Ruins1_05b[right1]': 'Ruins1_27[left1]', 'Ruins1_05c[left2]': 'Ruins1_03[right1]', 'Ruins1_05c[bot1]': 'Ruins1_05b[top1]', 'Ruins1_05c[top1]': 'Ruins1_05[bot1]', 'Ruins1_05c[top2]': 'Ruins1_05[bot2]', 'Ruins1_05c[top3]': 'Ruins1_05[bot3]', 'Ruins1_05[bot1]': 'Ruins1_05c[top1]', 'Ruins1_05[bot2]': 'Ruins1_05c[top2]', 'Ruins1_05[bot3]': 'Ruins1_05c[top3]', 'Ruins1_05[right1]': 'Ruins1_09[left1]', 'Ruins1_05[right2]': 'Ruins1_18[left1]', 'Ruins1_05[top1]': 'Ruins1_31[bot1]', 'Ruins1_06[left1]': 'Ruins1_17[right1]', 'Ruins1_06[right1]': 'Ruins1_31[left1]', 'Ruins1_09[top1]': 'Ruins1_23[bot1]', 'Ruins1_09[left1]': 'Ruins1_05[right1]', 'Ruins1_17[top1]': 'Ruins1_28[bot1]', 'Ruins1_17[right1]': 'Ruins1_06[left1]', 'Ruins1_17[bot1]': 'Ruins1_01[top1]', 'Ruins1_18[left1]': 'Ruins1_05[right2]', 'Ruins1_18[right1]': 'Ruins2_03b[left1]', 'Ruins1_18[right2]': 'Ruins2_01[left2]', 'Ruins1_23[top1]': 'Ruins1_30[bot1]', 'Ruins1_23[right1]': 'Ruins1_25[left2]', 'Ruins1_23[right2]': 'Ruins1_25[left3]', 'Ruins1_23[bot1]': 'Ruins1_09[top1]', 'Ruins1_23[left1]': 'Ruins1_31[right1]', 'Ruins1_24[left1]': 'Ruins1_32[right1]', 'Ruins1_24[right1]': 'Ruins1_30[left1]', 'Ruins1_24[left2]': 'Ruins1_32[right2]', 'Ruins1_24[right2]': 'Ruins1_30[left2]', 'Ruins1_25[left1]': 'Ruins1_30[right1]', 'Ruins1_25[left2]': 'Ruins1_23[right1]', 'Ruins1_25[left3]': 'Ruins1_23[right2]', 'Ruins1_27[left1]': 'Ruins1_05b[right1]', 'Ruins1_27[right1]': 'Ruins2_01_b[left1]', 'Ruins1_28[left1]': 'Crossroads_49b[right1]', 'Ruins1_28[right1]': 'Ruins1_29[left1]', 'Ruins1_28[bot1]': 'Ruins1_17[top1]', 'Ruins1_29[left1]': 'Ruins1_28[right1]', 'Ruins1_30[left1]': 'Ruins1_24[right1]', 'Ruins1_30[left2]': 'Ruins1_24[right2]', 'Ruins1_30[bot1]': 'Ruins1_23[top1]', 'Ruins1_30[right1]': 'Ruins1_25[left1]', 'Ruins1_31[bot1]': 'Ruins1_05[top1]', 'Ruins1_31[left1]': 'Ruins1_06[right1]', 'Ruins1_31[left2]': 'Ruins1_31b[right1]', 'Ruins1_31[left3]': 'Ruins1_31b[right2]', 'Ruins1_31[right1]': 'Ruins1_23[left1]', 'Ruins1_31b[right1]': 'Ruins1_31[left2]', 'Ruins1_31b[right2]': 'Ruins1_31[left3]', 'Ruins1_32[right1]': 'Ruins1_24[left1]', 'Ruins1_32[right2]': 'Ruins1_24[left2]', 'Room_nailsmith[left1]': 'Ruins1_04[door1]', 'Ruins2_01[top1]': 'Ruins2_03b[bot1]', 'Ruins2_01[bot1]': 'Ruins2_01_b[top1]', 'Ruins2_01[left2]': 'Ruins1_18[right2]', 'Ruins2_01_b[top1]': 'Ruins2_01[bot1]', 'Ruins2_01_b[left1]': 'Ruins1_27[right1]', 'Ruins2_01_b[right1]': 'Ruins2_04[left1]', 'Ruins2_03b[top1]': 'Ruins2_03[bot1]', 'Ruins2_03b[top2]': 'Ruins2_03[bot2]', 'Ruins2_03b[left1]': 'Ruins1_18[right1]', 'Ruins2_03b[bot1]': 'Ruins2_01[top1]', 'Ruins2_03[top1]': 'Ruins2_Watcher_Room[bot1]', 'Ruins2_03[bot1]': 'Ruins2_03b[top1]', 'Ruins2_03[bot2]': 'Ruins2_03b[top2]', 'Ruins2_04[left1]': 'Ruins2_01_b[right1]', 'Ruins2_04[left2]': 'Abyss_01[right1]', 'Ruins2_04[right1]': 'Ruins2_06[left1]', 'Ruins2_04[right2]': 'Ruins2_06[left2]', 'Ruins2_04[door_Ruin_House_01]': 'Ruins_House_01[left1]', 'Ruins2_04[door_Ruin_House_02]': 'Ruins_House_02[left1]', 'Ruins2_04[door_Ruin_House_03]': 'Ruins_House_03[left1]', 'Ruins2_04[door_Ruin_Elevator]': 'Ruins_Elevator[left1]', 'Ruins2_05[left1]': 'Ruins2_10b[right2]', 'Ruins2_05[top1]': 'Ruins2_09[bot1]', 'Ruins2_05[bot1]': 'Ruins2_06[top1]', 'Ruins2_06[left1]': 'Ruins2_04[right1]', 'Ruins2_06[left2]': 'Ruins2_04[right2]', 'Ruins2_06[right1]': 'Ruins2_08[left1]', 'Ruins2_06[right2]': 'Ruins2_07[left1]', 'Ruins2_06[top1]': 'Ruins2_05[bot1]', 'Ruins2_07[right1]': 'Deepnest_East_03[left1]', 'Ruins2_07[left1]': 'Ruins2_06[right2]', 'Ruins2_07[top1]': 'Ruins2_11_b[bot1]', 'Ruins2_08[left1]': 'Ruins2_06[right1]', 'Ruins2_09[bot1]': 'Ruins2_05[top1]', 'Ruins2_10[right1]': 'RestingGrounds_10[left1]', 'Ruins2_10[left1]': 'RestingGrounds_06[right1]', 'Ruins2_10b[right1]': 'Deepnest_East_09[left1]', 'Ruins2_10b[right2]': 'Ruins2_05[left1]', 'Ruins2_10b[left1]': 'Ruins_Bathhouse[right1]', 'Ruins2_11_b[right1]': 'Deepnest_East_07[left2]', 'Ruins2_11_b[left1]': 'Ruins2_11[right1]', 'Ruins2_11_b[bot1]': 'Ruins2_07[top1]', 'Ruins2_11[right1]': 'Ruins2_11_b[left1]', 'Ruins2_Watcher_Room[bot1]': 'Ruins2_03[top1]', 'Ruins_House_01[left1]': 'Ruins2_04[door_Ruin_House_01]', 'Ruins_House_02[left1]': 'Ruins2_04[door_Ruin_House_02]', 'Ruins_House_03[left1]': 'Ruins2_04[door_Ruin_House_03]', 'Ruins_House_03[left2]': 'Waterways_07[door1]', 'Ruins_Elevator[left1]': 'Ruins2_04[door_Ruin_Elevator]', 'Ruins_Elevator[left2]': 'Ruins_Bathhouse[door1]', 'Ruins_Bathhouse[door1]': 'Ruins_Elevator[left2]', 'Ruins_Bathhouse[right1]': 'Ruins2_10b[left1]', 'RestingGrounds_02[right1]': 'RestingGrounds_04[left1]', 'RestingGrounds_02[left1]': 'Crossroads_46b[right1]', 'RestingGrounds_02[bot1]': 'RestingGrounds_06[top1]', 'RestingGrounds_02[top1]': None, 'RestingGrounds_04[left1]': 'RestingGrounds_02[right1]', 'RestingGrounds_04[right1]': 'RestingGrounds_05[left1]', 'RestingGrounds_05[left1]': 'RestingGrounds_04[right1]', 'RestingGrounds_05[left2]': 'RestingGrounds_07[right1]', 'RestingGrounds_05[left3]': 'RestingGrounds_17[right1]', 'RestingGrounds_05[right1]': 'RestingGrounds_08[left1]', 'RestingGrounds_05[right2]': 'RestingGrounds_09[left1]', 'RestingGrounds_05[bot1]': 'RestingGrounds_10[top1]', 'RestingGrounds_06[left1]': 'Crossroads_50[right1]', 'RestingGrounds_06[right1]': 'Ruins2_10[left1]', 'RestingGrounds_06[top1]': 'RestingGrounds_02[bot1]', 'RestingGrounds_07[right1]': 'RestingGrounds_05[left2]', 'RestingGrounds_08[left1]': 'RestingGrounds_05[right1]', 'RestingGrounds_09[left1]': 'RestingGrounds_05[right2]', 'RestingGrounds_10[left1]': 'Ruins2_10[right1]', 'RestingGrounds_10[top1]': 'RestingGrounds_05[bot1]', 'RestingGrounds_10[top2]': 'RestingGrounds_12[bot1]', 'RestingGrounds_12[bot1]': 'RestingGrounds_10[top2]', 'RestingGrounds_12[door_Mansion]': 'Room_Mansion[left1]', 'RestingGrounds_17[right1]': 'RestingGrounds_05[left3]', 'Room_Mansion[left1]': 'RestingGrounds_12[door_Mansion]', 'Mines_01[bot1]': 'Mines_02[top1]', 'Mines_01[left1]': 'Crossroads_45[right1]', 'Mines_02[top1]': 'Mines_01[bot1]', 'Mines_02[top2]': 'Mines_03[bot1]', 'Mines_02[left1]': 'Mines_33[right1]', 'Mines_02[right1]': 'Mines_29[left1]', 'Mines_03[right1]': 'Mines_17[left1]', 'Mines_03[bot1]': 'Mines_02[top2]', 'Mines_03[top1]': 'Mines_05[bot1]', 'Mines_04[right1]': 'Mines_07[left1]', 'Mines_04[top1]': 'Mines_37[bot1]', 'Mines_04[left1]': 'Mines_17[right1]', 'Mines_04[left2]': 'Mines_29[right1]', 'Mines_04[left3]': 'Mines_29[right2]', 'Mines_05[right1]': 'Mines_19[left1]', 'Mines_05[top1]': 'Mines_11[bot1]', 'Mines_05[bot1]': 'Mines_03[top1]', 'Mines_05[left1]': 'Mines_30[right1]', 'Mines_05[left2]': 'Mines_06[right1]', 'Mines_06[right1]': 'Mines_05[left2]', 'Mines_06[left1]': 'Mines_36[right1]', 'Mines_07[right1]': 'Mines_28[left1]', 'Mines_07[left1]': 'Mines_04[right1]', 'Mines_10[right1]': 'Mines_30[left1]', 'Mines_10[left1]': 'Town[right1]', 'Mines_10[bot1]': 'Mines_16[top1]', 'Mines_11[right1]': 'Mines_18[left1]', 'Mines_11[top1]': 'Mines_13[bot1]', 'Mines_11[bot1]': 'Mines_05[top1]', 'Mines_13[right1]': 'Mines_20[left1]', 'Mines_13[top1]': None, 'Mines_13[bot1]': 'Mines_11[top1]', 'Mines_16[top1]': 'Mines_10[bot1]', 'Mines_17[right1]': 'Mines_04[left1]', 'Mines_17[left1]': 'Mines_03[right1]', 'Mines_18[top1]': 'Mines_32[bot1]', 'Mines_18[left1]': 'Mines_11[right1]', 'Mines_18[right1]': 'Mines_20[left2]', 'Mines_19[left1]': 'Mines_05[right1]', 'Mines_19[right1]': 'Mines_20[left3]', 'Mines_20[left1]': 'Mines_13[right1]', 'Mines_20[left2]': 'Mines_18[right1]', 'Mines_20[left3]': 'Mines_19[right1]', 'Mines_20[bot1]': 'Mines_37[top1]', 'Mines_20[right1]': 'Mines_23[left1]', 'Mines_20[right2]': 'Mines_31[left1]', 'Mines_23[left1]': 'Mines_20[right1]', 'Mines_23[right1]': 'Mines_25[left1]', 'Mines_23[right2]': 'Mines_24[left1]', 'Mines_23[top1]': None, 'Mines_24[left1]': 'Mines_23[right2]', 'Mines_25[left1]': 'Mines_23[right1]', 'Mines_25[top1]': 'Mines_34[bot1]', 'Mines_28[left1]': 'Mines_07[right1]', 'Mines_28[bot1]': 'RestingGrounds_02[top1]', 'Mines_28[door1]': 'Mines_35[left1]', 'Mines_29[left1]': 'Mines_02[right1]', 'Mines_29[right1]': 'Mines_04[left2]', 'Mines_29[right2]': 'Mines_04[left3]', 'Mines_30[left1]': 'Mines_10[right1]', 'Mines_30[right1]': 'Mines_05[left1]', 'Mines_31[left1]': 'Mines_20[right2]', 'Mines_32[bot1]': 'Mines_18[top1]', 'Mines_33[right1]': 'Mines_02[left1]', 'Mines_33[left1]': 'Crossroads_03[right2]', 'Mines_34[bot1]': 'Mines_25[top1]', 'Mines_34[bot2]': 'Mines_23[top1]', 'Mines_34[left1]': 'Mines_13[top1]', 'Mines_35[left1]': 'Mines_28[door1]', 'Mines_36[right1]': 'Mines_06[left1]', 'Mines_37[bot1]': 'Mines_04[top1]', 'Mines_37[top1]': 'Mines_20[bot1]', 'Fungus3_04[left1]': 'Fungus3_21[right1]', 'Fungus3_04[left2]': 'Fungus3_13[right1]', 'Fungus3_04[right1]': 'Fungus3_34[left1]', 'Fungus3_04[right2]': 'Fungus3_05[left1]', 'Fungus3_05[left1]': 'Fungus3_04[right2]', 'Fungus3_05[right1]': 'Fungus1_24[left1]', 'Fungus3_05[right2]': 'Fungus3_11[left1]', 'Fungus3_08[left1]': 'Deepnest_43[right1]', 'Fungus3_08[right1]': 'Fungus3_11[left2]', 'Fungus3_08[top1]': 'Fungus3_10[bot1]', 'Fungus3_10[top1]': 'Fungus3_13[bot1]', 'Fungus3_10[bot1]': 'Fungus3_08[top1]', 'Fungus3_11[left1]': 'Fungus3_05[right2]', 'Fungus3_11[left2]': 'Fungus3_08[right1]', 'Fungus3_11[right1]': 'Fungus3_39[left1]', 'Fungus3_13[left1]': 'Fungus1_23[right1]', 'Fungus3_13[left2]': 'Fungus3_40[right1]', 'Fungus3_13[left3]': 'Fungus3_49[right1]', 'Fungus3_13[bot1]': 'Fungus3_10[top1]', 'Fungus3_13[right1]': 'Fungus3_04[left2]', 'Fungus3_21[right1]': 'Fungus3_04[left1]', 'Fungus3_21[top1]': 'Fungus3_22[bot1]', 'Fungus3_22[right1]': 'Fungus1_13[left1]', 'Fungus3_22[left1]': 'Fungus3_23[right1]', 'Fungus3_22[bot1]': 'Fungus3_21[top1]', 'Fungus3_23[right1]': 'Fungus3_22[left1]', 'Fungus3_23[left1]': 'Fungus3_48[right1]', 'Fungus3_34[right1]': 'Fungus3_03[left1]', 'Fungus3_34[left1]': 'Fungus3_04[right1]', 'Fungus3_34[top1]': 'Fungus3_44[bot1]', 'Fungus3_39[right1]': 'Deepnest_01[left1]', 'Fungus3_39[left1]': 'Fungus3_11[right1]', 'Fungus3_40[right1]': 'Fungus3_13[left2]', 'Fungus3_40[top1]': 'Fungus3_48[bot1]', 'Fungus3_48[right1]': 'Fungus3_23[left1]', 'Fungus3_48[right2]': 'Fungus1_23[left1]', 'Fungus3_48[door1]': 'Room_Queen[left1]', 'Fungus3_48[bot1]': 'Fungus3_40[top1]', 'Fungus3_49[right1]': 'Fungus3_13[left3]', 'Fungus3_50[right1]': 'Deepnest_43[left1]', 'Room_Queen[left1]': 'Fungus3_48[door1]', 'Cliffs_01[right1]': 'Cliffs_02[left1]', 'Cliffs_01[right2]': 'Cliffs_04[left1]', 'Cliffs_01[right3]': 'Fungus1_28[left1]', 'Cliffs_01[right4]': 'Cliffs_06[left1]', 'Cliffs_02[right1]': 'Town[top1]', 'Cliffs_02[bot1]': 'Tutorial_01[top2]', 'Cliffs_02[bot2]': 'Tutorial_01[top1]', 'Cliffs_02[door1]': 'Room_nailmaster[left1]', 'Cliffs_02[left1]': 'Cliffs_01[right1]', 'Cliffs_02[left2]': 'Cliffs_03[right1]', 'Cliffs_03[right1]': 'Cliffs_02[left2]', 'Cliffs_04[right1]': 'Cliffs_05[left1]', 'Cliffs_04[left1]': 'Cliffs_01[right2]', 'Cliffs_05[left1]': 'Cliffs_04[right1]', 'Cliffs_06[left1]': 'Cliffs_01[right4]', 'Room_nailmaster[left1]': 'Cliffs_02[door1]', 'White_Palace_01[left1]': 'White_Palace_11[door2]', 'White_Palace_01[right1]': 'White_Palace_02[left1]', 'White_Palace_01[top1]': 'White_Palace_03_hub[bot1]', 'White_Palace_02[left1]': 'White_Palace_01[right1]', 'White_Palace_03_hub[left1]': 'White_Palace_14[right1]', 'White_Palace_03_hub[left2]': 'White_Palace_04[right2]', 'White_Palace_03_hub[right1]': 'White_Palace_15[left1]', 'White_Palace_03_hub[top1]': 'White_Palace_06[bot1]', 'White_Palace_03_hub[bot1]': 'White_Palace_01[top1]', 'White_Palace_04[top1]': 'White_Palace_14[bot1]', 'White_Palace_04[right2]': 'White_Palace_03_hub[left2]', 'White_Palace_05[left1]': 'White_Palace_15[right1]', 'White_Palace_05[left2]': 'White_Palace_15[right2]', 'White_Palace_05[right1]': 'White_Palace_16[left1]', 'White_Palace_05[right2]': 'White_Palace_16[left2]', 'White_Palace_06[left1]': 'White_Palace_18[right1]', 'White_Palace_06[top1]': 'White_Palace_07[bot1]', 'White_Palace_06[bot1]': 'White_Palace_03_hub[top1]', 'White_Palace_07[top1]': 'White_Palace_12[bot1]', 'White_Palace_07[bot1]': 'White_Palace_06[top1]', 'White_Palace_08[left1]': 'White_Palace_13[right1]', 'White_Palace_08[right1]': 'White_Palace_13[left3]', 'White_Palace_09[right1]': 'White_Palace_13[left1]', 'White_Palace_11[door2]': 'White_Palace_01[left1]', 'White_Palace_12[right1]': 'White_Palace_13[left2]', 'White_Palace_12[bot1]': 'White_Palace_07[top1]', 'White_Palace_13[right1]': 'White_Palace_08[left1]', 'White_Palace_13[left1]': 'White_Palace_09[right1]', 'White_Palace_13[left2]': 'White_Palace_12[right1]', 'White_Palace_13[left3]': 'White_Palace_08[right1]', 'White_Palace_14[bot1]': 'White_Palace_04[top1]', 'White_Palace_14[right1]': 'White_Palace_03_hub[left1]', 'White_Palace_15[left1]': 'White_Palace_03_hub[right1]', 'White_Palace_15[right1]': 'White_Palace_05[left1]', 'White_Palace_15[right2]': 'White_Palace_05[left2]', 'White_Palace_16[left1]': 'White_Palace_05[right1]', 'White_Palace_16[left2]': 'White_Palace_05[right2]', 'White_Palace_17[right1]': 'White_Palace_19[left1]', 'White_Palace_17[bot1]': 'White_Palace_18[top1]', 'White_Palace_18[top1]': 'White_Palace_17[bot1]', 'White_Palace_18[right1]': 'White_Palace_06[left1]', 'White_Palace_19[top1]': 'White_Palace_20[bot1]', 'White_Palace_19[left1]': 'White_Palace_17[right1]', 'White_Palace_20[bot1]': 'White_Palace_19[top1]'}
event_names = {'Abyss_01', 'Abyss_03', 'Abyss_03_b', 'Abyss_03_c', 'Abyss_04', 'Abyss_05', 'Abyss_06_Core', 'Abyss_09', 'Abyss_19', 'Broke_Sanctum_Glass_Floor', 'Can_Bench', 'Can_Repair_Fragile_Charms', 'Can_Replenish_Geo', 'Can_Replenish_Geo-Crossroads', 'Can_Stag', 'Cliffs_01', 'Cliffs_02', 'Completed_Path_of_Pain', 'Crossroads_03', 'Crossroads_07', 'Crossroads_08', 'Crossroads_14', 'Crossroads_18', 'Crossroads_19', 'Crossroads_21', 'Crossroads_27', 'Crossroads_33', 'Deepnest_01', 'Deepnest_01b', 'Deepnest_02', 'Deepnest_03', 'Deepnest_10', 'Deepnest_14', 'Deepnest_17', 'Deepnest_26', 'Deepnest_34', 'Deepnest_35', 'Deepnest_37', 'Deepnest_39', 'Deepnest_41', 'Deepnest_42', 'Deepnest_East_02', 'Deepnest_East_03', 'Deepnest_East_04', 'Deepnest_East_07', 'Deepnest_East_11', 'Deepnest_East_18', 'Defeated_Broken_Vessel', 'Defeated_Brooding_Mawlek', 'Defeated_Collector', 'Defeated_Colosseum_1', 'Defeated_Colosseum_2', 'Defeated_Colosseum_Zote', 'Defeated_Crystal_Guardian', 'Defeated_Dung_Defender', 'Defeated_Elder_Hu', 'Defeated_Elegant_Warrior', 'Defeated_Enraged_Guardian', 'Defeated_Failed_Champion', 'Defeated_False_Knight', 'Defeated_Flukemarm', 'Defeated_Galien', 'Defeated_Gorb', 'Defeated_Grey_Prince_Zote', 'Defeated_Grimm', 'Defeated_Gruz_Mother', 'Defeated_Hive_Knight', 'Defeated_Hornet_1', 'Defeated_Hornet_2', "Defeated_King's_Station_Arena", 'Defeated_Lost_Kin', 'Defeated_Mantis_Lords', 'Defeated_Markoth', 'Defeated_Marmu', 'Defeated_No_Eyes', 'Defeated_Nosk', 'Defeated_Pale_Lurker', 'Defeated_Path_of_Pain_Arena', 'Defeated_Sanctum_Warrior', 'Defeated_Shrumal_Ogre_Arena', 'Defeated_Soul_Master', 'Defeated_Soul_Tyrant', 'Defeated_Traitor_Lord', 'Defeated_Uumuu', 'Defeated_Watcher_Knights', "Defeated_West_Queen's_Gardens_Arena", 'Defeated_White_Defender', 'Defeated_Xero', 'First_Grimmchild_Upgrade', 'Fungus1_11', 'Fungus1_21', 'Fungus1_30', 'Fungus2_01', 'Fungus2_03', 'Fungus2_04', 'Fungus2_06', 'Fungus2_11', 'Fungus2_13', 'Fungus2_14', 'Fungus2_17', 'Fungus2_20', 'Fungus2_23', 'Fungus3_01', 'Fungus3_02', 'Fungus3_04', 'Fungus3_11', 'Fungus3_13', 'Fungus3_22', 'Fungus3_26', 'Fungus3_34', 'Fungus3_40', 'Fungus3_44', 'Fungus3_47', 'Hive_03_c', 'Left_Elevator', 'Lever-Dung_Defender', 'Lever-Shade_Soul', 'Lit_Abyss_Lighthouse', 'Lower_Tram', 'Mines_02', 'Mines_03', 'Mines_04', 'Mines_05', 'Mines_10', 'Mines_11', 'Mines_18', 'Mines_20', 'Mines_23', 'Nightmare_Lantern_Lit', 'Opened_Archives_Exit_Wall', 'Opened_Black_Egg_Temple', 'Opened_Dung_Defender_Wall', 'Opened_Emilitia_Door', 'Opened_Gardens_Stag_Exit', 'Opened_Glade_Door', "Opened_Lower_Kingdom's_Edge_Wall", 'Opened_Mawlek_Wall', 'Opened_Pleasure_House_Wall', 'Opened_Resting_Grounds_Catacombs_Wall', 'Opened_Resting_Grounds_Floor', 'Opened_Shaman_Pillar', 'Opened_Tramway_Exit_Gate', 'Opened_Waterways_Exit', 'Opened_Waterways_Manhole', 'Palace_Atrium_Gates_Opened', 'Palace_Entrance_Lantern_Lit', 'Palace_Left_Lantern_Lit', 'Palace_Right_Lantern_Lit', 'Rescued_Bretta', 'Rescued_Deepnest_Zote', 'Rescued_Sly', 'RestingGrounds_02', 'RestingGrounds_05', 'RestingGrounds_10', 'Right_Elevator', 'Ruins1_03', 'Ruins1_05', 'Ruins1_05b', 'Ruins1_05c', 'Ruins1_23', 'Ruins1_28', 'Ruins1_30', 'Ruins1_31', 'Ruins2_01', 'Ruins2_01_b', 'Ruins2_03b', 'Ruins2_04', 'Ruins2_10', 'Second_Grimmchild_Upgrade', 'Town', 'Tutorial_01', 'Upper_Tram', 'Warp-Lifeblood_Core_to_Abyss', 'Warp-Palace_Grounds_to_White_Palace', 'Warp-Path_of_Pain_Complete', 'Warp-White_Palace_Atrium_to_Palace_Grounds', 'Warp-White_Palace_Entrance_to_Palace_Grounds', 'Waterways_01', 'Waterways_02', 'Waterways_04', 'Waterways_04b', 'Waterways_07', 'White_Palace_01', 'White_Palace_03_hub', 'White_Palace_13'}
exits = {'Room_temple': ['Room_temple[left1]'], 'Tutorial_01': ['Tutorial_01[right1]', 'Tutorial_01[top1]', 'Tutorial_01[top2]'], 'Town': ['Town[left1]', 'Town[bot1]', 'Town[right1]', 'Town[top1]', 'Town[door_station]', 'Town[door_sly]', 'Town[door_mapper]', 'Town[door_jiji]', 'Town[door_bretta]', 'Town[room_divine]', 'Town[room_grimm]'], 'Room_shop': ['Room_shop[left1]'], 'Room_Town_Stag_Station': ['Room_Town_Stag_Station[left1]'], 'Room_mapper': ['Room_mapper[left1]'], 'Room_Bretta': ['Room_Bretta[right1]'], 'Room_Ouiji': ['Room_Ouiji[left1]'], 'Grimm_Divine': ['Grimm_Divine[left1]'], 'Grimm_Main_Tent': ['Grimm_Main_Tent[left1]'], 'Crossroads_01': ['Crossroads_01[top1]', 'Crossroads_01[left1]', 'Crossroads_01[right1]'], 'Crossroads_02': ['Crossroads_02[left1]', 'Crossroads_02[door1]', 'Crossroads_02[right1]'], 'Crossroads_03': ['Crossroads_03[right1]', 'Crossroads_03[right2]', 'Crossroads_03[left1]', 'Crossroads_03[left2]', 'Crossroads_03[bot1]', 'Crossroads_03[top1]'], 'Crossroads_04': ['Crossroads_04[left1]', 'Crossroads_04[top1]', 'Crossroads_04[door_Mender_House]', 'Crossroads_04[door1]', 'Crossroads_04[door_charmshop]', 'Crossroads_04[right1]'], 'Crossroads_05': ['Crossroads_05[left1]', 'Crossroads_05[right1]'], 'Crossroads_06': ['Crossroads_06[left1]', 'Crossroads_06[door1]', 'Crossroads_06[right1]'], 'Crossroads_07': ['Crossroads_07[left1]', 'Crossroads_07[left2]', 'Crossroads_07[left3]', 'Crossroads_07[right1]', 'Crossroads_07[right2]', 'Crossroads_07[bot1]'], 'Crossroads_08': ['Crossroads_08[left1]', 'Crossroads_08[left2]', 'Crossroads_08[right1]', 'Crossroads_08[right2]'], 'Crossroads_09': ['Crossroads_09[left1]', 'Crossroads_09[right1]'], 'Crossroads_10': ['Crossroads_10[left1]', 'Crossroads_10[right1]'], 'Crossroads_11_alt': ['Crossroads_11_alt[left1]', 'Crossroads_11_alt[right1]'], 'Crossroads_12': ['Crossroads_12[left1]', 'Crossroads_12[right1]'], 'Crossroads_13': ['Crossroads_13[left1]', 'Crossroads_13[right1]'], 'Crossroads_14': ['Crossroads_14[left1]', 'Crossroads_14[left2]', 'Crossroads_14[right1]', 'Crossroads_14[right2]'], 'Crossroads_15': ['Crossroads_15[left1]', 'Crossroads_15[right1]'], 'Crossroads_16': ['Crossroads_16[left1]', 'Crossroads_16[right1]', 'Crossroads_16[bot1]'], 'Crossroads_18': ['Crossroads_18[right1]', 'Crossroads_18[right2]', 'Crossroads_18[bot1]'], 'Crossroads_19': ['Crossroads_19[right1]', 'Crossroads_19[top1]', 'Crossroads_19[left1]', 'Crossroads_19[left2]'], 'Crossroads_21': ['Crossroads_21[left1]', 'Crossroads_21[right1]', 'Crossroads_21[top1]'], 'Crossroads_22': ['Crossroads_22[bot1]'], 'Crossroads_25': ['Crossroads_25[right1]', 'Crossroads_25[left1]'], 'Crossroads_27': ['Crossroads_27[right1]', 'Crossroads_27[bot1]', 'Crossroads_27[left1]', 'Crossroads_27[left2]'], 'Crossroads_30': ['Crossroads_30[left1]'], 'Crossroads_31': ['Crossroads_31[right1]'], 'Crossroads_33': ['Crossroads_33[top1]', 'Crossroads_33[left1]', 'Crossroads_33[left2]', 'Crossroads_33[right1]', 'Crossroads_33[right2]'], 'Crossroads_35': ['Crossroads_35[bot1]', 'Crossroads_35[right1]'], 'Crossroads_36': ['Crossroads_36[right1]', 'Crossroads_36[right2]'], 'Crossroads_37': ['Crossroads_37[right1]'], 'Crossroads_38': ['Crossroads_38[right1]'], 'Crossroads_39': ['Crossroads_39[right1]', 'Crossroads_39[left1]'], 'Crossroads_40': ['Crossroads_40[right1]', 'Crossroads_40[left1]'], 'Crossroads_42': ['Crossroads_42[left1]', 'Crossroads_42[right1]'], 'Crossroads_43': ['Crossroads_43[left1]', 'Crossroads_43[right1]'], 'Crossroads_45': ['Crossroads_45[right1]', 'Crossroads_45[left1]'], 'Crossroads_46': ['Crossroads_46[left1]'], 'Crossroads_46b': ['Crossroads_46b[right1]'], 'Crossroads_ShamanTemple': ['Crossroads_ShamanTemple[left1]'], 'Crossroads_47': ['Crossroads_47[right1]'], 'Crossroads_48': ['Crossroads_48[left1]'], 'Crossroads_49': ['Crossroads_49[right1]', 'Crossroads_49[left1]'], 'Crossroads_49b': ['Crossroads_49b[right1]'], 'Crossroads_50': ['Crossroads_50[right1]', 'Crossroads_50[left1]'], 'Crossroads_52': ['Crossroads_52[left1]'], 'Room_ruinhouse': ['Room_ruinhouse[left1]'], 'Room_Charm_Shop': ['Room_Charm_Shop[left1]'], 'Room_Mender_House': ['Room_Mender_House[left1]'], 'Fungus1_01': ['Fungus1_01[left1]', 'Fungus1_01[right1]'], 'Fungus1_01b': ['Fungus1_01b[left1]', 'Fungus1_01b[right1]'], 'Fungus1_02': ['Fungus1_02[left1]', 'Fungus1_02[right1]', 'Fungus1_02[right2]'], 'Fungus1_03': ['Fungus1_03[left1]', 'Fungus1_03[right1]', 'Fungus1_03[bot1]'], 'Fungus1_04': ['Fungus1_04[left1]', 'Fungus1_04[right1]'], 'Fungus1_05': ['Fungus1_05[right1]', 'Fungus1_05[bot1]', 'Fungus1_05[top1]'], 'Fungus1_06': ['Fungus1_06[left1]', 'Fungus1_06[bot1]'], 'Fungus1_07': ['Fungus1_07[top1]', 'Fungus1_07[left1]', 'Fungus1_07[right1]'], 'Fungus1_08': ['Fungus1_08[left1]'], 'Fungus1_09': ['Fungus1_09[left1]', 'Fungus1_09[right1]'], 'Fungus1_10': ['Fungus1_10[left1]', 'Fungus1_10[right1]', 'Fungus1_10[top1]'], 'Fungus1_11': ['Fungus1_11[top1]', 'Fungus1_11[right1]', 'Fungus1_11[right2]', 'Fungus1_11[left1]', 'Fungus1_11[bot1]'], 'Fungus1_12': ['Fungus1_12[left1]', 'Fungus1_12[right1]'], 'Fungus1_13': ['Fungus1_13[right1]', 'Fungus1_13[left1]'], 'Fungus1_14': ['Fungus1_14[left1]'], 'Fungus1_15': ['Fungus1_15[door1]', 'Fungus1_15[right1]'], 'Fungus1_16_alt': ['Fungus1_16_alt[right1]'], 'Fungus1_17': ['Fungus1_17[left1]', 'Fungus1_17[right1]'], 'Fungus1_19': ['Fungus1_19[left1]', 'Fungus1_19[right1]', 'Fungus1_19[bot1]'], 'Fungus1_20_v02': ['Fungus1_20_v02[bot1]', 'Fungus1_20_v02[bot2]', 'Fungus1_20_v02[right1]'], 'Fungus1_21': ['Fungus1_21[bot1]', 'Fungus1_21[top1]', 'Fungus1_21[left1]', 'Fungus1_21[right1]'], 'Fungus1_22': ['Fungus1_22[bot1]', 'Fungus1_22[top1]', 'Fungus1_22[left1]'], 'Fungus1_23': ['Fungus1_23[left1]', 'Fungus1_23[right1]'], 'Fungus1_24': ['Fungus1_24[left1]'], 'Fungus1_25': ['Fungus1_25[right1]', 'Fungus1_25[left1]'], 'Fungus1_26': ['Fungus1_26[right1]', 'Fungus1_26[left1]', 'Fungus1_26[door_SlugShrine]'], 'Fungus1_28': ['Fungus1_28[left1]', 'Fungus1_28[left2]'], 'Fungus1_29': ['Fungus1_29[left1]', 'Fungus1_29[right1]'], 'Fungus1_30': ['Fungus1_30[top1]', 'Fungus1_30[top3]', 'Fungus1_30[left1]', 'Fungus1_30[right1]'], 'Fungus1_31': ['Fungus1_31[top1]', 'Fungus1_31[bot1]', 'Fungus1_31[right1]'], 'Fungus1_32': ['Fungus1_32[bot1]', 'Fungus1_32[top1]', 'Fungus1_32[left1]'], 'Fungus1_34': ['Fungus1_34[door1]', 'Fungus1_34[left1]'], 'Fungus1_35': ['Fungus1_35[left1]', 'Fungus1_35[right1]'], 'Fungus1_36': ['Fungus1_36[left1]'], 'Fungus1_37': ['Fungus1_37[left1]'], 'Fungus1_Slug': ['Fungus1_Slug[right1]'], 'Room_Slug_Shrine': ['Room_Slug_Shrine[left1]'], 'Room_nailmaster_02': ['Room_nailmaster_02[left1]'], 'Fungus3_01': ['Fungus3_01[top1]', 'Fungus3_01[right1]', 'Fungus3_01[left1]', 'Fungus3_01[right2]'], 'Fungus3_02': ['Fungus3_02[left1]', 'Fungus3_02[left2]', 'Fungus3_02[left3]', 'Fungus3_02[right1]', 'Fungus3_02[right2]'], 'Fungus3_03': ['Fungus3_03[right1]', 'Fungus3_03[left1]'], 'Fungus3_24': ['Fungus3_24[right1]', 'Fungus3_24[left1]', 'Fungus3_24[top1]'], 'Fungus3_25': ['Fungus3_25[right1]', 'Fungus3_25[left1]'], 'Fungus3_25b': ['Fungus3_25b[right1]', 'Fungus3_25b[left1]'], 'Fungus3_26': ['Fungus3_26[top1]', 'Fungus3_26[left1]', 'Fungus3_26[left2]', 'Fungus3_26[left3]', 'Fungus3_26[right1]'], 'Fungus3_27': ['Fungus3_27[left1]', 'Fungus3_27[right1]'], 'Fungus3_28': ['Fungus3_28[right1]'], 'Fungus3_30': ['Fungus3_30[bot1]'], 'Fungus3_35': ['Fungus3_35[right1]'], 'Fungus3_44': ['Fungus3_44[bot1]', 'Fungus3_44[door1]', 'Fungus3_44[right1]'], 'Fungus3_47': ['Fungus3_47[left1]', 'Fungus3_47[right1]', 'Fungus3_47[door1]'], 'Room_Fungus_Shaman': ['Room_Fungus_Shaman[left1]'], 'Fungus3_archive': ['Fungus3_archive[left1]', 'Fungus3_archive[bot1]'], 'Fungus3_archive_02': ['Fungus3_archive_02[top1]'], 'Fungus2_01': ['Fungus2_01[left1]', 'Fungus2_01[left2]', 'Fungus2_01[left3]', 'Fungus2_01[right1]'], 'Fungus2_02': ['Fungus2_02[right1]'], 'Fungus2_34': ['Fungus2_34[right1]'], 'Fungus2_03': ['Fungus2_03[left1]', 'Fungus2_03[bot1]', 'Fungus2_03[right1]'], 'Fungus2_04': ['Fungus2_04[top1]', 'Fungus2_04[right1]', 'Fungus2_04[left1]', 'Fungus2_04[right2]'], 'Fungus2_05': ['Fungus2_05[bot1]', 'Fungus2_05[right1]'], 'Fungus2_06': ['Fungus2_06[top1]', 'Fungus2_06[left1]', 'Fungus2_06[left2]', 'Fungus2_06[right1]', 'Fungus2_06[right2]'], 'Fungus2_07': ['Fungus2_07[left1]', 'Fungus2_07[right1]'], 'Fungus2_08': ['Fungus2_08[left1]', 'Fungus2_08[left2]', 'Fungus2_08[right1]'], 'Fungus2_09': ['Fungus2_09[left1]', 'Fungus2_09[right1]'], 'Fungus2_10': ['Fungus2_10[right1]', 'Fungus2_10[right2]', 'Fungus2_10[bot1]'], 'Fungus2_11': ['Fungus2_11[top1]', 'Fungus2_11[left1]', 'Fungus2_11[left2]', 'Fungus2_11[right1]'], 'Fungus2_12': ['Fungus2_12[left1]', 'Fungus2_12[bot1]'], 'Fungus2_13': ['Fungus2_13[top1]', 'Fungus2_13[left2]', 'Fungus2_13[left3]'], 'Fungus2_14': ['Fungus2_14[top1]', 'Fungus2_14[right1]', 'Fungus2_14[bot3]'], 'Fungus2_15': ['Fungus2_15[top3]', 'Fungus2_15[right1]', 'Fungus2_15[left1]'], 'Fungus2_17': ['Fungus2_17[left1]', 'Fungus2_17[right1]', 'Fungus2_17[bot1]'], 'Fungus2_18': ['Fungus2_18[right1]', 'Fungus2_18[bot1]', 'Fungus2_18[top1]'], 'Fungus2_19': ['Fungus2_19[top1]', 'Fungus2_19[left1]'], 'Fungus2_20': ['Fungus2_20[right1]', 'Fungus2_20[left1]'], 'Fungus2_21': ['Fungus2_21[right1]', 'Fungus2_21[left1]'], 'Fungus2_23': ['Fungus2_23[right1]', 'Fungus2_23[right2]'], 'Fungus2_26': ['Fungus2_26[left1]'], 'Fungus2_28': ['Fungus2_28[left1]', 'Fungus2_28[left2]'], 'Fungus2_29': ['Fungus2_29[right1]', 'Fungus2_29[bot1]'], 'Fungus2_30': ['Fungus2_30[bot1]', 'Fungus2_30[top1]'], 'Fungus2_31': ['Fungus2_31[left1]'], 'Fungus2_32': ['Fungus2_32[left1]'], 'Fungus2_33': ['Fungus2_33[right1]', 'Fungus2_33[left1]'], 'Deepnest_01': ['Deepnest_01[right1]', 'Deepnest_01[bot1]', 'Deepnest_01[bot2]', 'Deepnest_01[left1]'], 'Deepnest_01b': ['Deepnest_01b[top1]', 'Deepnest_01b[top2]', 'Deepnest_01b[right1]', 'Deepnest_01b[right2]', 'Deepnest_01b[bot1]'], 'Deepnest_02': ['Deepnest_02[left1]', 'Deepnest_02[left2]', 'Deepnest_02[right1]'], 'Deepnest_03': ['Deepnest_03[right1]', 'Deepnest_03[left1]', 'Deepnest_03[top1]', 'Deepnest_03[left2]'], 'Deepnest_09': ['Deepnest_09[left1]'], 'Deepnest_10': ['Deepnest_10[right1]', 'Deepnest_10[right2]', 'Deepnest_10[right3]', 'Deepnest_10[door1]', 'Deepnest_10[door2]'], 'Room_spider_small': ['Room_spider_small[left1]'], 'Deepnest_Spider_Town': ['Deepnest_Spider_Town[left1]'], 'Deepnest_14': ['Deepnest_14[right1]', 'Deepnest_14[left1]', 'Deepnest_14[bot1]', 'Deepnest_14[bot2]'], 'Deepnest_16': ['Deepnest_16[left1]', 'Deepnest_16[bot1]'], 'Deepnest_17': ['Deepnest_17[left1]', 'Deepnest_17[right1]', 'Deepnest_17[top1]', 'Deepnest_17[bot1]'], 'Fungus2_25': ['Fungus2_25[top1]', 'Fungus2_25[top2]', 'Fungus2_25[right1]'], 'Deepnest_26': ['Deepnest_26[left1]', 'Deepnest_26[left2]', 'Deepnest_26[right1]', 'Deepnest_26[bot1]'], 'Deepnest_26b': ['Deepnest_26b[right2]', 'Deepnest_26b[right1]'], 'Deepnest_30': ['Deepnest_30[left1]', 'Deepnest_30[top1]', 'Deepnest_30[right1]'], 'Deepnest_31': ['Deepnest_31[right1]', 'Deepnest_31[right2]'], 'Deepnest_32': ['Deepnest_32[left1]'], 'Deepnest_33': ['Deepnest_33[top1]', 'Deepnest_33[top2]', 'Deepnest_33[bot1]'], 'Deepnest_34': ['Deepnest_34[left1]', 'Deepnest_34[right1]', 'Deepnest_34[top1]'], 'Deepnest_35': ['Deepnest_35[left1]', 'Deepnest_35[top1]', 'Deepnest_35[bot1]'], 'Deepnest_36': ['Deepnest_36[left1]'], 'Deepnest_37': ['Deepnest_37[left1]', 'Deepnest_37[right1]', 'Deepnest_37[top1]', 'Deepnest_37[bot1]'], 'Deepnest_38': ['Deepnest_38[bot1]'], 'Deepnest_39': ['Deepnest_39[left1]', 'Deepnest_39[top1]', 'Deepnest_39[door1]', 'Deepnest_39[right1]'], 'Deepnest_40': ['Deepnest_40[right1]'], 'Deepnest_41': ['Deepnest_41[right1]', 'Deepnest_41[left1]', 'Deepnest_41[left2]'], 'Deepnest_42': ['Deepnest_42[bot1]', 'Deepnest_42[left1]', 'Deepnest_42[top1]'], 'Deepnest_43': ['Deepnest_43[bot1]', 'Deepnest_43[left1]', 'Deepnest_43[right1]'], 'Deepnest_44': ['Deepnest_44[top1]'], 'Deepnest_45_v02': ['Deepnest_45_v02[left1]'], 'Room_Mask_Maker': ['Room_Mask_Maker[right1]'], 'Deepnest_East_01': ['Deepnest_East_01[bot1]', 'Deepnest_East_01[right1]', 'Deepnest_East_01[top1]'], 'Deepnest_East_02': ['Deepnest_East_02[bot1]', 'Deepnest_East_02[bot2]', 'Deepnest_East_02[top1]', 'Deepnest_East_02[right1]'], 'Deepnest_East_03': ['Deepnest_East_03[left1]', 'Deepnest_East_03[left2]', 'Deepnest_East_03[top1]', 'Deepnest_East_03[top2]', 'Deepnest_East_03[right1]', 'Deepnest_East_03[right2]'], 'Deepnest_East_04': ['Deepnest_East_04[left1]', 'Deepnest_East_04[left2]', 'Deepnest_East_04[right2]', 'Deepnest_East_04[right1]'], 'Deepnest_East_06': ['Deepnest_East_06[top1]', 'Deepnest_East_06[left1]', 'Deepnest_East_06[bot1]', 'Deepnest_East_06[door1]', 'Deepnest_East_06[right1]'], 'Deepnest_East_07': ['Deepnest_East_07[bot1]', 'Deepnest_East_07[bot2]', 'Deepnest_East_07[left1]', 'Deepnest_East_07[left2]', 'Deepnest_East_07[right1]'], 'Deepnest_East_08': ['Deepnest_East_08[right1]', 'Deepnest_East_08[top1]'], 'Deepnest_East_09': ['Deepnest_East_09[right1]', 'Deepnest_East_09[left1]', 'Deepnest_East_09[bot1]'], 'Deepnest_East_10': ['Deepnest_East_10[left1]'], 'Deepnest_East_11': ['Deepnest_East_11[right1]', 'Deepnest_East_11[left1]', 'Deepnest_East_11[top1]', 'Deepnest_East_11[bot1]'], 'Deepnest_East_12': ['Deepnest_East_12[right1]', 'Deepnest_East_12[left1]'], 'Deepnest_East_13': ['Deepnest_East_13[bot1]'], 'Deepnest_East_14': ['Deepnest_East_14[top2]', 'Deepnest_East_14[left1]', 'Deepnest_East_14[door1]'], 'Deepnest_East_14b': ['Deepnest_East_14b[right1]', 'Deepnest_East_14b[top1]'], 'Deepnest_East_15': ['Deepnest_East_15[left1]'], 'Deepnest_East_16': ['Deepnest_East_16[left1]', 'Deepnest_East_16[bot1]'], 'Deepnest_East_17': ['Deepnest_East_17[left1]'], 'Deepnest_East_18': ['Deepnest_East_18[top1]', 'Deepnest_East_18[bot1]', 'Deepnest_East_18[right2]'], 'Room_nailmaster_03': ['Room_nailmaster_03[left1]'], 'Deepnest_East_Hornet': ['Deepnest_East_Hornet[left1]', 'Deepnest_East_Hornet[left2]'], 'Room_Wyrm': ['Room_Wyrm[right1]'], 'GG_Lurker': ['GG_Lurker[left1]'], 'Hive_01': ['Hive_01[left1]', 'Hive_01[right1]', 'Hive_01[right2]'], 'Hive_02': ['Hive_02[left1]', 'Hive_02[left2]', 'Hive_02[left3]'], 'Hive_03_c': ['Hive_03_c[left1]', 'Hive_03_c[right2]', 'Hive_03_c[right3]', 'Hive_03_c[top1]'], 'Hive_03': ['Hive_03[bot1]', 'Hive_03[right1]', 'Hive_03[top1]'], 'Hive_04': ['Hive_04[left1]', 'Hive_04[left2]', 'Hive_04[right1]'], 'Hive_05': ['Hive_05[left1]'], 'Room_Colosseum_01': ['Room_Colosseum_01[left1]', 'Room_Colosseum_01[bot1]'], 'Room_Colosseum_02': ['Room_Colosseum_02[top1]', 'Room_Colosseum_02[top2]'], 'Room_Colosseum_Spectate': ['Room_Colosseum_Spectate[bot1]', 'Room_Colosseum_Spectate[right1]'], 'Abyss_01': ['Abyss_01[left1]', 'Abyss_01[left2]', 'Abyss_01[left3]', 'Abyss_01[right1]', 'Abyss_01[right2]'], 'Abyss_02': ['Abyss_02[right1]', 'Abyss_02[bot1]'], 'Abyss_03': ['Abyss_03[bot1]', 'Abyss_03[bot2]', 'Abyss_03[top1]'], 'Abyss_03_b': ['Abyss_03_b[left1]'], 'Abyss_03_c': ['Abyss_03_c[right1]', 'Abyss_03_c[top1]'], 'Abyss_04': ['Abyss_04[top1]', 'Abyss_04[left1]', 'Abyss_04[bot1]', 'Abyss_04[right1]'], 'Abyss_05': ['Abyss_05[left1]', 'Abyss_05[right1]'], 'Abyss_06_Core': ['Abyss_06_Core[top1]', 'Abyss_06_Core[left1]', 'Abyss_06_Core[left3]', 'Abyss_06_Core[right2]', 'Abyss_06_Core[bot1]'], 'Abyss_08': ['Abyss_08[right1]'], 'Abyss_09': ['Abyss_09[right1]', 'Abyss_09[right2]', 'Abyss_09[right3]', 'Abyss_09[left1]'], 'Abyss_10': ['Abyss_10[left1]', 'Abyss_10[left2]'], 'Abyss_12': ['Abyss_12[right1]'], 'Abyss_15': ['Abyss_15[top1]'], 'Abyss_16': ['Abyss_16[left1]', 'Abyss_16[right1]'], 'Abyss_17': ['Abyss_17[top1]'], 'Abyss_18': ['Abyss_18[left1]', 'Abyss_18[right1]'], 'Abyss_19': ['Abyss_19[left1]', 'Abyss_19[right1]', 'Abyss_19[bot1]', 'Abyss_19[bot2]'], 'Abyss_20': ['Abyss_20[top1]', 'Abyss_20[top2]'], 'Abyss_21': ['Abyss_21[right1]'], 'Abyss_22': ['Abyss_22[left1]'], 'Abyss_Lighthouse_room': ['Abyss_Lighthouse_room[left1]'], 'Waterways_01': ['Waterways_01[top1]', 'Waterways_01[left1]', 'Waterways_01[right1]', 'Waterways_01[bot1]'], 'Waterways_02': ['Waterways_02[top1]', 'Waterways_02[top2]', 'Waterways_02[top3]', 'Waterways_02[bot1]', 'Waterways_02[bot2]'], 'Waterways_03': ['Waterways_03[left1]'], 'Waterways_04': ['Waterways_04[bot1]', 'Waterways_04[right1]', 'Waterways_04[left1]', 'Waterways_04[left2]'], 'Waterways_04b': ['Waterways_04b[right1]', 'Waterways_04b[right2]', 'Waterways_04b[left1]'], 'Waterways_05': ['Waterways_05[right1]', 'Waterways_05[bot1]', 'Waterways_05[bot2]'], 'Waterways_06': ['Waterways_06[right1]', 'Waterways_06[top1]'], 'Waterways_07': ['Waterways_07[right1]', 'Waterways_07[right2]', 'Waterways_07[left1]', 'Waterways_07[door1]', 'Waterways_07[top1]'], 'Waterways_08': ['Waterways_08[top1]', 'Waterways_08[left1]', 'Waterways_08[left2]'], 'Waterways_09': ['Waterways_09[right1]', 'Waterways_09[left1]'], 'Waterways_12': ['Waterways_12[right1]'], 'Waterways_13': ['Waterways_13[left1]', 'Waterways_13[left2]'], 'Waterways_14': ['Waterways_14[bot1]', 'Waterways_14[bot2]'], 'Waterways_15': ['Waterways_15[top1]'], 'GG_Pipeway': ['GG_Pipeway[right1]', 'GG_Pipeway[left1]'], 'GG_Waterways': ['GG_Waterways[right1]', 'GG_Waterways[door1]'], 'Room_GG_Shortcut': ['Room_GG_Shortcut[left1]', 'Room_GG_Shortcut[top1]'], 'Ruins1_01': ['Ruins1_01[left1]', 'Ruins1_01[top1]', 'Ruins1_01[bot1]'], 'Ruins1_02': ['Ruins1_02[top1]', 'Ruins1_02[bot1]'], 'Ruins1_03': ['Ruins1_03[top1]', 'Ruins1_03[left1]', 'Ruins1_03[right1]', 'Ruins1_03[right2]'], 'Ruins1_04': ['Ruins1_04[right1]', 'Ruins1_04[door1]', 'Ruins1_04[bot1]'], 'Ruins1_05b': ['Ruins1_05b[left1]', 'Ruins1_05b[top1]', 'Ruins1_05b[bot1]', 'Ruins1_05b[right1]'], 'Ruins1_05c': ['Ruins1_05c[left2]', 'Ruins1_05c[bot1]', 'Ruins1_05c[top1]', 'Ruins1_05c[top2]', 'Ruins1_05c[top3]'], 'Ruins1_05': ['Ruins1_05[bot1]', 'Ruins1_05[bot2]', 'Ruins1_05[bot3]', 'Ruins1_05[right1]', 'Ruins1_05[right2]', 'Ruins1_05[top1]'], 'Ruins1_06': ['Ruins1_06[left1]', 'Ruins1_06[right1]'], 'Ruins1_09': ['Ruins1_09[top1]', 'Ruins1_09[left1]'], 'Ruins1_17': ['Ruins1_17[top1]', 'Ruins1_17[right1]', 'Ruins1_17[bot1]'], 'Ruins1_18': ['Ruins1_18[left1]', 'Ruins1_18[right1]', 'Ruins1_18[right2]'], 'Ruins1_23': ['Ruins1_23[top1]', 'Ruins1_23[right1]', 'Ruins1_23[right2]', 'Ruins1_23[bot1]', 'Ruins1_23[left1]'], 'Ruins1_24': ['Ruins1_24[left1]', 'Ruins1_24[right1]', 'Ruins1_24[left2]', 'Ruins1_24[right2]'], 'Ruins1_25': ['Ruins1_25[left1]', 'Ruins1_25[left2]', 'Ruins1_25[left3]'], 'Ruins1_27': ['Ruins1_27[left1]', 'Ruins1_27[right1]'], 'Ruins1_28': ['Ruins1_28[left1]', 'Ruins1_28[right1]', 'Ruins1_28[bot1]'], 'Ruins1_29': ['Ruins1_29[left1]'], 'Ruins1_30': ['Ruins1_30[left1]', 'Ruins1_30[left2]', 'Ruins1_30[bot1]', 'Ruins1_30[right1]'], 'Ruins1_31': ['Ruins1_31[bot1]', 'Ruins1_31[left1]', 'Ruins1_31[left2]', 'Ruins1_31[left3]', 'Ruins1_31[right1]'], 'Ruins1_31b': ['Ruins1_31b[right1]', 'Ruins1_31b[right2]'], 'Ruins1_32': ['Ruins1_32[right1]', 'Ruins1_32[right2]'], 'Room_nailsmith': ['Room_nailsmith[left1]'], 'Ruins2_01': ['Ruins2_01[top1]', 'Ruins2_01[bot1]', 'Ruins2_01[left2]'], 'Ruins2_01_b': ['Ruins2_01_b[top1]', 'Ruins2_01_b[left1]', 'Ruins2_01_b[right1]'], 'Ruins2_03b': ['Ruins2_03b[top1]', 'Ruins2_03b[top2]', 'Ruins2_03b[left1]', 'Ruins2_03b[bot1]'], 'Ruins2_03': ['Ruins2_03[top1]', 'Ruins2_03[bot1]', 'Ruins2_03[bot2]'], 'Ruins2_04': ['Ruins2_04[left1]', 'Ruins2_04[left2]', 'Ruins2_04[right1]', 'Ruins2_04[right2]', 'Ruins2_04[door_Ruin_House_01]', 'Ruins2_04[door_Ruin_House_02]', 'Ruins2_04[door_Ruin_House_03]', 'Ruins2_04[door_Ruin_Elevator]'], 'Ruins2_05': ['Ruins2_05[left1]', 'Ruins2_05[top1]', 'Ruins2_05[bot1]'], 'Ruins2_06': ['Ruins2_06[left1]', 'Ruins2_06[left2]', 'Ruins2_06[right1]', 'Ruins2_06[right2]', 'Ruins2_06[top1]'], 'Ruins2_07': ['Ruins2_07[right1]', 'Ruins2_07[left1]', 'Ruins2_07[top1]'], 'Ruins2_08': ['Ruins2_08[left1]'], 'Ruins2_09': ['Ruins2_09[bot1]'], 'Ruins2_10': ['Ruins2_10[right1]', 'Ruins2_10[left1]'], 'Ruins2_10b': ['Ruins2_10b[right1]', 'Ruins2_10b[right2]', 'Ruins2_10b[left1]'], 'Ruins2_11_b': ['Ruins2_11_b[right1]', 'Ruins2_11_b[left1]', 'Ruins2_11_b[bot1]'], 'Ruins2_11': ['Ruins2_11[right1]'], 'Ruins2_Watcher_Room': ['Ruins2_Watcher_Room[bot1]'], 'Ruins_House_01': ['Ruins_House_01[left1]'], 'Ruins_House_02': ['Ruins_House_02[left1]'], 'Ruins_House_03': ['Ruins_House_03[left1]', 'Ruins_House_03[left2]'], 'Ruins_Elevator': ['Ruins_Elevator[left1]', 'Ruins_Elevator[left2]'], 'Ruins_Bathhouse': ['Ruins_Bathhouse[door1]', 'Ruins_Bathhouse[right1]'], 'RestingGrounds_02': ['RestingGrounds_02[right1]', 'RestingGrounds_02[left1]', 'RestingGrounds_02[bot1]', 'RestingGrounds_02[top1]'], 'RestingGrounds_04': ['RestingGrounds_04[left1]', 'RestingGrounds_04[right1]'], 'RestingGrounds_05': ['RestingGrounds_05[left1]', 'RestingGrounds_05[left2]', 'RestingGrounds_05[left3]', 'RestingGrounds_05[right1]', 'RestingGrounds_05[right2]', 'RestingGrounds_05[bot1]'], 'RestingGrounds_06': ['RestingGrounds_06[left1]', 'RestingGrounds_06[right1]', 'RestingGrounds_06[top1]'], 'RestingGrounds_07': ['RestingGrounds_07[right1]'], 'RestingGrounds_08': ['RestingGrounds_08[left1]'], 'RestingGrounds_09': ['RestingGrounds_09[left1]'], 'RestingGrounds_10': ['RestingGrounds_10[left1]', 'RestingGrounds_10[top1]', 'RestingGrounds_10[top2]'], 'RestingGrounds_12': ['RestingGrounds_12[bot1]', 'RestingGrounds_12[door_Mansion]'], 'RestingGrounds_17': ['RestingGrounds_17[right1]'], 'Room_Mansion': ['Room_Mansion[left1]'], 'Mines_01': ['Mines_01[bot1]', 'Mines_01[left1]'], 'Mines_02': ['Mines_02[top1]', 'Mines_02[top2]', 'Mines_02[left1]', 'Mines_02[right1]'], 'Mines_03': ['Mines_03[right1]', 'Mines_03[bot1]', 'Mines_03[top1]'], 'Mines_04': ['Mines_04[right1]', 'Mines_04[top1]', 'Mines_04[left1]', 'Mines_04[left2]', 'Mines_04[left3]'], 'Mines_05': ['Mines_05[right1]', 'Mines_05[top1]', 'Mines_05[bot1]', 'Mines_05[left1]', 'Mines_05[left2]'], 'Mines_06': ['Mines_06[right1]', 'Mines_06[left1]'], 'Mines_07': ['Mines_07[right1]', 'Mines_07[left1]'], 'Mines_10': ['Mines_10[right1]', 'Mines_10[left1]', 'Mines_10[bot1]'], 'Mines_11': ['Mines_11[right1]', 'Mines_11[top1]', 'Mines_11[bot1]'], 'Mines_13': ['Mines_13[right1]', 'Mines_13[top1]', 'Mines_13[bot1]'], 'Mines_16': ['Mines_16[top1]'], 'Mines_17': ['Mines_17[right1]', 'Mines_17[left1]'], 'Mines_18': ['Mines_18[top1]', 'Mines_18[left1]', 'Mines_18[right1]'], 'Mines_19': ['Mines_19[left1]', 'Mines_19[right1]'], 'Mines_20': ['Mines_20[left1]', 'Mines_20[left2]', 'Mines_20[left3]', 'Mines_20[bot1]', 'Mines_20[right1]', 'Mines_20[right2]'], 'Mines_23': ['Mines_23[left1]', 'Mines_23[right1]', 'Mines_23[right2]', 'Mines_23[top1]'], 'Mines_24': ['Mines_24[left1]'], 'Mines_25': ['Mines_25[left1]', 'Mines_25[top1]'], 'Mines_28': ['Mines_28[left1]', 'Mines_28[bot1]', 'Mines_28[door1]'], 'Mines_29': ['Mines_29[left1]', 'Mines_29[right1]', 'Mines_29[right2]'], 'Mines_30': ['Mines_30[left1]', 'Mines_30[right1]'], 'Mines_31': ['Mines_31[left1]'], 'Mines_32': ['Mines_32[bot1]'], 'Mines_33': ['Mines_33[right1]', 'Mines_33[left1]'], 'Mines_34': ['Mines_34[bot1]', 'Mines_34[bot2]', 'Mines_34[left1]'], 'Mines_35': ['Mines_35[left1]'], 'Mines_36': ['Mines_36[right1]'], 'Mines_37': ['Mines_37[bot1]', 'Mines_37[top1]'], 'Fungus3_04': ['Fungus3_04[left1]', 'Fungus3_04[left2]', 'Fungus3_04[right1]', 'Fungus3_04[right2]'], 'Fungus3_05': ['Fungus3_05[left1]', 'Fungus3_05[right1]', 'Fungus3_05[right2]'], 'Fungus3_08': ['Fungus3_08[left1]', 'Fungus3_08[right1]', 'Fungus3_08[top1]'], 'Fungus3_10': ['Fungus3_10[top1]', 'Fungus3_10[bot1]'], 'Fungus3_11': ['Fungus3_11[left1]', 'Fungus3_11[left2]', 'Fungus3_11[right1]'], 'Fungus3_13': ['Fungus3_13[left1]', 'Fungus3_13[left2]', 'Fungus3_13[left3]', 'Fungus3_13[bot1]', 'Fungus3_13[right1]'], 'Fungus3_21': ['Fungus3_21[right1]', 'Fungus3_21[top1]'], 'Fungus3_22': ['Fungus3_22[right1]', 'Fungus3_22[left1]', 'Fungus3_22[bot1]'], 'Fungus3_23': ['Fungus3_23[right1]', 'Fungus3_23[left1]'], 'Fungus3_34': ['Fungus3_34[right1]', 'Fungus3_34[left1]', 'Fungus3_34[top1]'], 'Fungus3_39': ['Fungus3_39[right1]', 'Fungus3_39[left1]'], 'Fungus3_40': ['Fungus3_40[right1]', 'Fungus3_40[top1]'], 'Fungus3_48': ['Fungus3_48[right1]', 'Fungus3_48[right2]', 'Fungus3_48[door1]', 'Fungus3_48[bot1]'], 'Fungus3_49': ['Fungus3_49[right1]'], 'Fungus3_50': ['Fungus3_50[right1]'], 'Room_Queen': ['Room_Queen[left1]'], 'Cliffs_01': ['Cliffs_01[right1]', 'Cliffs_01[right2]', 'Cliffs_01[right3]', 'Cliffs_01[right4]'], 'Cliffs_02': ['Cliffs_02[right1]', 'Cliffs_02[bot1]', 'Cliffs_02[bot2]', 'Cliffs_02[door1]', 'Cliffs_02[left1]', 'Cliffs_02[left2]'], 'Cliffs_03': ['Cliffs_03[right1]'], 'Cliffs_04': ['Cliffs_04[right1]', 'Cliffs_04[left1]'], 'Cliffs_05': ['Cliffs_05[left1]'], 'Cliffs_06': ['Cliffs_06[left1]'], 'Room_nailmaster': ['Room_nailmaster[left1]'], 'White_Palace_01': ['White_Palace_01[left1]', 'White_Palace_01[right1]', 'White_Palace_01[top1]'], 'White_Palace_02': ['White_Palace_02[left1]'], 'White_Palace_03_hub': ['White_Palace_03_hub[left1]', 'White_Palace_03_hub[left2]', 'White_Palace_03_hub[right1]', 'White_Palace_03_hub[top1]', 'White_Palace_03_hub[bot1]'], 'White_Palace_04': ['White_Palace_04[top1]', 'White_Palace_04[right2]'], 'White_Palace_05': ['White_Palace_05[left1]', 'White_Palace_05[left2]', 'White_Palace_05[right1]', 'White_Palace_05[right2]'], 'White_Palace_06': ['White_Palace_06[left1]', 'White_Palace_06[top1]', 'White_Palace_06[bot1]'], 'White_Palace_07': ['White_Palace_07[top1]', 'White_Palace_07[bot1]'], 'White_Palace_08': ['White_Palace_08[left1]', 'White_Palace_08[right1]'], 'White_Palace_09': ['White_Palace_09[right1]'], 'White_Palace_11': ['White_Palace_11[door2]'], 'White_Palace_12': ['White_Palace_12[right1]', 'White_Palace_12[bot1]'], 'White_Palace_13': ['White_Palace_13[right1]', 'White_Palace_13[left1]', 'White_Palace_13[left2]', 'White_Palace_13[left3]'], 'White_Palace_14': ['White_Palace_14[bot1]', 'White_Palace_14[right1]'], 'White_Palace_15': ['White_Palace_15[left1]', 'White_Palace_15[right1]', 'White_Palace_15[right2]'], 'White_Palace_16': ['White_Palace_16[left1]', 'White_Palace_16[left2]'], 'White_Palace_17': ['White_Palace_17[right1]', 'White_Palace_17[bot1]'], 'White_Palace_18': ['White_Palace_18[top1]', 'White_Palace_18[right1]'], 'White_Palace_19': ['White_Palace_19[top1]', 'White_Palace_19[left1]'], 'White_Palace_20': ['White_Palace_20[bot1]']}
-item_effects = {'Lurien': {'DREAMER': 1}, 'Monomon': {'DREAMER': 1}, 'Herrah': {'DREAMER': 1}, 'Dreamer': {'DREAMER': 1}, 'Mothwing_Cloak': {'LEFTDASH': 1, 'RIGHTDASH': 1}, 'Mantis_Claw': {'LEFTCLAW': 1, 'RIGHTCLAW': 1}, 'Crystal_Heart': {'LEFTSUPERDASH': 1, 'RIGHTSUPERDASH': 1}, 'Monarch_Wings': {'WINGS': 1}, 'Shade_Cloak': {'LEFTDASH': 1, 'RIGHTDASH': 1}, "Isma's_Tear": {'ACID': 1}, 'Dream_Nail': {'DREAMNAIL': 1}, 'Dream_Gate': {'DREAMNAIL': 1}, 'Awoken_Dream_Nail': {'DREAMNAIL': 1}, 'Vengeful_Spirit': {'FIREBALL': 1, 'SPELLS': 1}, 'Shade_Soul': {'FIREBALL': 1, 'SPELLS': 1}, 'Desolate_Dive': {'QUAKE': 1, 'SPELLS': 1}, 'Descending_Dark': {'QUAKE': 1, 'SPELLS': 1}, 'Howling_Wraiths': {'SCREAM': 1, 'SPELLS': 1}, 'Abyss_Shriek': {'SCREAM': 1, 'SPELLS': 1}, 'Cyclone_Slash': {'CYCLONE': 1}, 'Focus': {'FOCUS': 1}, 'Swim': {'SWIM': 1}, 'Gathering_Swarm': {'CHARMS': 1}, 'Wayward_Compass': {'CHARMS': 1}, 'Grubsong': {'CHARMS': 1}, 'Stalwart_Shell': {'CHARMS': 1}, 'Baldur_Shell': {'CHARMS': 1}, 'Fury_of_the_Fallen': {'CHARMS': 1}, 'Quick_Focus': {'CHARMS': 1}, 'Lifeblood_Heart': {'CHARMS': 1}, 'Lifeblood_Core': {'CHARMS': 1}, "Defender's_Crest": {'CHARMS': 1}, 'Flukenest': {'CHARMS': 1}, 'Thorns_of_Agony': {'CHARMS': 1}, 'Mark_of_Pride': {'CHARMS': 1}, 'Steady_Body': {'CHARMS': 1}, 'Heavy_Blow': {'CHARMS': 1}, 'Sharp_Shadow': {'CHARMS': 1}, 'Spore_Shroom': {'CHARMS': 1}, 'Longnail': {'CHARMS': 1}, 'Shaman_Stone': {'CHARMS': 1}, 'Soul_Catcher': {'CHARMS': 1}, 'Soul_Eater': {'CHARMS': 1}, 'Glowing_Womb': {'CHARMS': 1}, 'Fragile_Heart': {'CHARMS': 1}, 'Unbreakable_Heart': {'Fragile_Heart': 1, 'CHARMS': 1}, 'Fragile_Greed': {'CHARMS': 1}, 'Unbreakable_Greed': {'Fragile_Greed': 1, 'CHARMS': 1}, 'Fragile_Strength': {'CHARMS': 1}, 'Unbreakable_Strength': {'Fragile_Strength': 1, 'CHARMS': 1}, "Nailmaster's_Glory": {'CHARMS': 1}, "Joni's_Blessing": {'CHARMS': 1}, 'Shape_of_Unn': {'CHARMS': 1}, 'Hiveblood': {'CHARMS': 1}, 'Dream_Wielder': {'CHARMS': 1}, 'Dashmaster': {'CHARMS': 1}, 'Quick_Slash': {'CHARMS': 1}, 'Spell_Twister': {'CHARMS': 1}, 'Deep_Focus': {'CHARMS': 1}, "Grubberfly's_Elegy": {'CHARMS': 1}, 'Queen_Fragment': {'WHITEFRAGMENT': 1}, 'King_Fragment': {'WHITEFRAGMENT': 1}, 'Void_Heart': {'WHITEFRAGMENT': 1}, 'Sprintmaster': {'CHARMS': 1}, 'Dreamshield': {'CHARMS': 1}, 'Weaversong': {'CHARMS': 1}, 'Grimmchild1': {'GRIMMCHILD': 1, 'CHARMS': 1}, 'Grimmchild2': {'GRIMMCHILD': 1, 'CHARMS': 1, 'FLAMES': 6, 'First_Grimmchild_Upgrade': 1}, 'City_Crest': {'CREST': 1}, 'Lumafly_Lantern': {'LANTERN': 1}, 'Tram_Pass': {'TRAM': 1}, 'Simple_Key': {'SIMPLE': 1}, "Shopkeeper's_Key": {'SHOPKEY': 1}, 'Elegant_Key': {'ELEGANT': 1}, 'Love_Key': {'LOVE': 1}, "King's_Brand": {'BRAND': 1}, 'Mask_Shard': {'MASKSHARDS': 1}, 'Double_Mask_Shard': {'MASKSHARDS': 2}, 'Full_Mask': {'MASKSHARDS': 4}, 'Vessel_Fragment': {'VESSELFRAGMENTS': 1}, 'Double_Vessel_Fragment': {'VESSELFRAGMENTS': 2}, 'Full_Soul_Vessel': {'VESSELFRAGMENTS': 3}, 'Charm_Notch': {'NOTCHES': 1}, 'Pale_Ore': {'PALEORE': 1}, 'Rancid_Egg': {'RANCIDEGGS': 1}, 'Whispering_Root-Crossroads': {'ESSENCE': 29}, 'Whispering_Root-Greenpath': {'ESSENCE': 44}, 'Whispering_Root-Leg_Eater': {'ESSENCE': 20}, 'Whispering_Root-Mantis_Village': {'ESSENCE': 18}, 'Whispering_Root-Deepnest': {'ESSENCE': 45}, 'Whispering_Root-Queens_Gardens': {'ESSENCE': 29}, 'Whispering_Root-Kingdoms_Edge': {'ESSENCE': 51}, 'Whispering_Root-Waterways': {'ESSENCE': 35}, 'Whispering_Root-City': {'ESSENCE': 28}, 'Whispering_Root-Resting_Grounds': {'ESSENCE': 20}, 'Whispering_Root-Spirits_Glade': {'ESSENCE': 34}, 'Whispering_Root-Crystal_Peak': {'ESSENCE': 21}, 'Whispering_Root-Howling_Cliffs': {'ESSENCE': 46}, 'Whispering_Root-Ancestral_Mound': {'ESSENCE': 42}, 'Whispering_Root-Hive': {'ESSENCE': 20}, 'Boss_Essence-Elder_Hu': {'ESSENCE': 100}, 'Boss_Essence-Xero': {'ESSENCE': 100}, 'Boss_Essence-Gorb': {'ESSENCE': 100}, 'Boss_Essence-Marmu': {'ESSENCE': 150}, 'Boss_Essence-No_Eyes': {'ESSENCE': 200}, 'Boss_Essence-Galien': {'ESSENCE': 200}, 'Boss_Essence-Markoth': {'ESSENCE': 250}, 'Boss_Essence-Failed_Champion': {'ESSENCE': 300}, 'Boss_Essence-Soul_Tyrant': {'ESSENCE': 300}, 'Boss_Essence-Lost_Kin': {'ESSENCE': 400}, 'Boss_Essence-White_Defender': {'ESSENCE': 300}, 'Boss_Essence-Grey_Prince_Zote': {'ESSENCE': 300}, 'Grub': {'GRUBS': 1}, 'Quill': {'QUILL': 1}, 'Crossroads_Stag': {'STAGS': 1}, 'Greenpath_Stag': {'STAGS': 1}, "Queen's_Station_Stag": {'STAGS': 1}, "Queen's_Gardens_Stag": {'STAGS': 1}, 'City_Storerooms_Stag': {'STAGS': 1}, "King's_Station_Stag": {'STAGS': 1}, 'Resting_Grounds_Stag': {'STAGS': 1}, 'Distant_Village_Stag': {'STAGS': 1}, 'Hidden_Station_Stag': {'STAGS': 1}, 'Stag_Nest_Stag': {'STAGS': 1}, 'Grimmkin_Flame': {'FLAMES': 1}, "Hunter's_Journal": {'JOURNAL': 1}, 'Right_Mantis_Claw': {'RIGHTCLAW': 1}, 'Left_Mantis_Claw': {'LEFTCLAW': 1}, 'Leftslash': {'LEFTSLASH': 1}, 'Rightslash': {'RIGHTSLASH': 1}, 'Upslash': {'UPSLASH': 1}, 'Downslash': {'DOWNSLASH': 1}, 'Left_Crystal_Heart': {'LEFTSUPERDASH': 1}, 'Right_Crystal_Heart': {'RIGHTSUPERDASH': 1}, 'Left_Mothwing_Cloak': {'LEFTDASH': 1}, 'Right_Mothwing_Cloak': {'RIGHTDASH': 1}}
+item_effects = {'Lurien': {'DREAMER': 1}, 'Monomon': {'DREAMER': 1}, 'Herrah': {'DREAMER': 1}, 'Dreamer': {'DREAMER': 1}, 'Mothwing_Cloak': {'LEFTDASH': 1, 'RIGHTDASH': 1}, 'Mantis_Claw': {'LEFTCLAW': 1, 'RIGHTCLAW': 1}, 'Crystal_Heart': {'LEFTSUPERDASH': 1, 'RIGHTSUPERDASH': 1}, 'Monarch_Wings': {'WINGS': 1}, 'Shade_Cloak': {'LEFTDASH': 1, 'RIGHTDASH': 1}, "Isma's_Tear": {'ACID': 1}, 'Dream_Nail': {'DREAMNAIL': 1}, 'Dream_Gate': {'DREAMNAIL': 1}, 'Awoken_Dream_Nail': {'DREAMNAIL': 1}, 'Vengeful_Spirit': {'FIREBALL': 1, 'SPELLS': 1}, 'Shade_Soul': {'FIREBALL': 1, 'SPELLS': 1}, 'Desolate_Dive': {'QUAKE': 1, 'SPELLS': 1}, 'Descending_Dark': {'QUAKE': 1, 'SPELLS': 1}, 'Howling_Wraiths': {'SCREAM': 1, 'SPELLS': 1}, 'Abyss_Shriek': {'SCREAM': 1, 'SPELLS': 1}, 'Cyclone_Slash': {'CYCLONE': 1}, 'Focus': {'FOCUS': 1}, 'Swim': {'SWIM': 1}, 'Gathering_Swarm': {'CHARMS': 1}, 'Wayward_Compass': {'CHARMS': 1}, 'Grubsong': {'CHARMS': 1}, 'Stalwart_Shell': {'CHARMS': 1}, 'Baldur_Shell': {'CHARMS': 1}, 'Fury_of_the_Fallen': {'CHARMS': 1}, 'Quick_Focus': {'CHARMS': 1}, 'Lifeblood_Heart': {'CHARMS': 1}, 'Lifeblood_Core': {'CHARMS': 1}, "Defender's_Crest": {'CHARMS': 1}, 'Flukenest': {'CHARMS': 1}, 'Thorns_of_Agony': {'CHARMS': 1}, 'Mark_of_Pride': {'CHARMS': 1}, 'Steady_Body': {'CHARMS': 1}, 'Heavy_Blow': {'CHARMS': 1}, 'Sharp_Shadow': {'CHARMS': 1}, 'Spore_Shroom': {'CHARMS': 1}, 'Longnail': {'CHARMS': 1}, 'Shaman_Stone': {'CHARMS': 1}, 'Soul_Catcher': {'CHARMS': 1}, 'Soul_Eater': {'CHARMS': 1}, 'Glowing_Womb': {'CHARMS': 1}, 'Fragile_Heart': {'CHARMS': 1}, 'Fragile_Greed': {'CHARMS': 1}, 'Fragile_Strength': {'CHARMS': 1}, "Nailmaster's_Glory": {'CHARMS': 1}, "Joni's_Blessing": {'CHARMS': 1}, 'Shape_of_Unn': {'CHARMS': 1}, 'Hiveblood': {'CHARMS': 1}, 'Dream_Wielder': {'CHARMS': 1}, 'Dashmaster': {'CHARMS': 1}, 'Quick_Slash': {'CHARMS': 1}, 'Spell_Twister': {'CHARMS': 1}, 'Deep_Focus': {'CHARMS': 1}, "Grubberfly's_Elegy": {'CHARMS': 1}, 'Queen_Fragment': {'CHARMS': 0.5, 'WHITEFRAGMENT': 1}, 'King_Fragment': {'CHARMS': 0.5, 'WHITEFRAGMENT': 1}, 'Void_Heart': {'CHARMS': 0.5, 'WHITEFRAGMENT': 1}, 'Sprintmaster': {'CHARMS': 1}, 'Dreamshield': {'CHARMS': 1}, 'Weaversong': {'CHARMS': 1}, 'Grimmchild1': {'GRIMMCHILD': 1, 'CHARMS': 1}, 'Grimmchild2': {'GRIMMCHILD': 1, 'CHARMS': 1, 'FLAMES': 6, 'First_Grimmchild_Upgrade': 1}, 'City_Crest': {'CREST': 1}, 'Lumafly_Lantern': {'LANTERN': 1}, 'Tram_Pass': {'TRAM': 1}, 'Simple_Key': {'SIMPLE': 1}, "Shopkeeper's_Key": {'SHOPKEY': 1}, 'Elegant_Key': {'ELEGANT': 1}, 'Love_Key': {'LOVE': 1}, "King's_Brand": {'BRAND': 1}, 'Mask_Shard': {'MASKSHARDS': 1}, 'Double_Mask_Shard': {'MASKSHARDS': 2}, 'Full_Mask': {'MASKSHARDS': 4}, 'Vessel_Fragment': {'VESSELFRAGMENTS': 1}, 'Double_Vessel_Fragment': {'VESSELFRAGMENTS': 2}, 'Full_Soul_Vessel': {'VESSELFRAGMENTS': 3}, 'Charm_Notch': {'NOTCHES': 1}, 'Pale_Ore': {'PALEORE': 1}, 'Rancid_Egg': {'RANCIDEGGS': 1}, 'Whispering_Root-Crossroads': {'ESSENCE': 29}, 'Whispering_Root-Greenpath': {'ESSENCE': 44}, 'Whispering_Root-Leg_Eater': {'ESSENCE': 20}, 'Whispering_Root-Mantis_Village': {'ESSENCE': 18}, 'Whispering_Root-Deepnest': {'ESSENCE': 45}, 'Whispering_Root-Queens_Gardens': {'ESSENCE': 29}, 'Whispering_Root-Kingdoms_Edge': {'ESSENCE': 51}, 'Whispering_Root-Waterways': {'ESSENCE': 35}, 'Whispering_Root-City': {'ESSENCE': 28}, 'Whispering_Root-Resting_Grounds': {'ESSENCE': 20}, 'Whispering_Root-Spirits_Glade': {'ESSENCE': 34}, 'Whispering_Root-Crystal_Peak': {'ESSENCE': 21}, 'Whispering_Root-Howling_Cliffs': {'ESSENCE': 46}, 'Whispering_Root-Ancestral_Mound': {'ESSENCE': 42}, 'Whispering_Root-Hive': {'ESSENCE': 20}, 'Boss_Essence-Elder_Hu': {'ESSENCE': 100}, 'Boss_Essence-Xero': {'ESSENCE': 100}, 'Boss_Essence-Gorb': {'ESSENCE': 100}, 'Boss_Essence-Marmu': {'ESSENCE': 150}, 'Boss_Essence-No_Eyes': {'ESSENCE': 200}, 'Boss_Essence-Galien': {'ESSENCE': 200}, 'Boss_Essence-Markoth': {'ESSENCE': 250}, 'Boss_Essence-Failed_Champion': {'ESSENCE': 300}, 'Boss_Essence-Soul_Tyrant': {'ESSENCE': 300}, 'Boss_Essence-Lost_Kin': {'ESSENCE': 400}, 'Boss_Essence-White_Defender': {'ESSENCE': 300}, 'Boss_Essence-Grey_Prince_Zote': {'ESSENCE': 300}, 'Grub': {'GRUBS': 1}, 'Quill': {'QUILL': 1}, 'Crossroads_Stag': {'STAGS': 1}, 'Greenpath_Stag': {'STAGS': 1}, "Queen's_Station_Stag": {'STAGS': 1}, "Queen's_Gardens_Stag": {'STAGS': 1}, 'City_Storerooms_Stag': {'STAGS': 1}, "King's_Station_Stag": {'STAGS': 1}, 'Resting_Grounds_Stag': {'STAGS': 1}, 'Distant_Village_Stag': {'STAGS': 1}, 'Hidden_Station_Stag': {'STAGS': 1}, 'Stag_Nest_Stag': {'STAGS': 1}, 'Grimmkin_Flame': {'FLAMES': 1}, "Hunter's_Journal": {'JOURNAL': 1}, 'Right_Mantis_Claw': {'RIGHTCLAW': 1}, 'Left_Mantis_Claw': {'LEFTCLAW': 1}, 'Leftslash': {'LEFTSLASH': 1}, 'Rightslash': {'RIGHTSLASH': 1}, 'Upslash': {'UPSLASH': 1}, 'Downslash': {'DOWNSLASH': 1}, 'Left_Crystal_Heart': {'LEFTSUPERDASH': 1}, 'Right_Crystal_Heart': {'RIGHTSUPERDASH': 1}, 'Left_Mothwing_Cloak': {'LEFTDASH': 1}, 'Right_Mothwing_Cloak': {'RIGHTDASH': 1}}
items = {'Lurien': 'Dreamer', 'Monomon': 'Dreamer', 'Herrah': 'Dreamer', 'World_Sense': 'Dreamer', 'Dreamer': 'Fake', 'Mothwing_Cloak': 'Skill', 'Mantis_Claw': 'Skill', 'Crystal_Heart': 'Skill', 'Monarch_Wings': 'Skill', 'Shade_Cloak': 'Skill', "Isma's_Tear": 'Skill', 'Dream_Nail': 'Skill', 'Dream_Gate': 'Skill', 'Awoken_Dream_Nail': 'Skill', 'Vengeful_Spirit': 'Skill', 'Shade_Soul': 'Skill', 'Desolate_Dive': 'Skill', 'Descending_Dark': 'Skill', 'Howling_Wraiths': 'Skill', 'Abyss_Shriek': 'Skill', 'Cyclone_Slash': 'Skill', 'Dash_Slash': 'Skill', 'Great_Slash': 'Skill', 'Focus': 'Focus', 'Swim': 'Swim', 'Gathering_Swarm': 'Charm', 'Wayward_Compass': 'Charm', 'Grubsong': 'Charm', 'Stalwart_Shell': 'Charm', 'Baldur_Shell': 'Charm', 'Fury_of_the_Fallen': 'Charm', 'Quick_Focus': 'Charm', 'Lifeblood_Heart': 'Charm', 'Lifeblood_Core': 'Charm', "Defender's_Crest": 'Charm', 'Flukenest': 'Charm', 'Thorns_of_Agony': 'Charm', 'Mark_of_Pride': 'Charm', 'Steady_Body': 'Charm', 'Heavy_Blow': 'Charm', 'Sharp_Shadow': 'Charm', 'Spore_Shroom': 'Charm', 'Longnail': 'Charm', 'Shaman_Stone': 'Charm', 'Soul_Catcher': 'Charm', 'Soul_Eater': 'Charm', 'Glowing_Womb': 'Charm', 'Fragile_Heart': 'Charm', 'Unbreakable_Heart': 'Charm', 'Fragile_Greed': 'Charm', 'Unbreakable_Greed': 'Charm', 'Fragile_Strength': 'Charm', 'Unbreakable_Strength': 'Charm', "Nailmaster's_Glory": 'Charm', "Joni's_Blessing": 'Charm', 'Shape_of_Unn': 'Charm', 'Hiveblood': 'Charm', 'Dream_Wielder': 'Charm', 'Dashmaster': 'Charm', 'Quick_Slash': 'Charm', 'Spell_Twister': 'Charm', 'Deep_Focus': 'Charm', "Grubberfly's_Elegy": 'Charm', 'Queen_Fragment': 'Charm', 'King_Fragment': 'Charm', 'Void_Heart': 'Charm', 'Sprintmaster': 'Charm', 'Dreamshield': 'Charm', 'Weaversong': 'Charm', 'Grimmchild1': 'Charm', 'Grimmchild2': 'Charm', 'City_Crest': 'Key', 'Lumafly_Lantern': 'Key', 'Tram_Pass': 'Key', 'Simple_Key': 'Key', "Shopkeeper's_Key": 'Key', 'Elegant_Key': 'Key', 'Love_Key': 'Key', "King's_Brand": 'Key', 'Godtuner': 'Key', "Collector's_Map": 'Key', 'Mask_Shard': 'Mask', 'Double_Mask_Shard': 'Mask', 'Full_Mask': 'Mask', 'Vessel_Fragment': 'Vessel', 'Double_Vessel_Fragment': 'Vessel', 'Full_Soul_Vessel': 'Vessel', 'Charm_Notch': 'Notch', "Salubra's_Blessing": 'Notch', 'Pale_Ore': 'Ore', 'Geo_Chest-False_Knight': 'Geo', 'Geo_Chest-Soul_Master': 'Geo', 'Geo_Chest-Watcher_Knights': 'Geo', 'Geo_Chest-Greenpath': 'Geo', 'Geo_Chest-Mantis_Lords': 'Geo', 'Geo_Chest-Resting_Grounds': 'Geo', 'Geo_Chest-Crystal_Peak': 'Geo', 'Geo_Chest-Weavers_Den': 'Geo', 'Geo_Chest-Junk_Pit_1': 'JunkPitChest', 'Geo_Chest-Junk_Pit_2': 'JunkPitChest', 'Geo_Chest-Junk_Pit_3': 'JunkPitChest', 'Geo_Chest-Junk_Pit_5': 'JunkPitChest', 'Lumafly_Escape': 'JunkPitChest', 'One_Geo': 'Fake', 'Rancid_Egg': 'Egg', "Wanderer's_Journal": 'Relic', 'Hallownest_Seal': 'Relic', "King's_Idol": 'Relic', 'Arcane_Egg': 'Relic', 'Whispering_Root-Crossroads': 'Root', 'Whispering_Root-Greenpath': 'Root', 'Whispering_Root-Leg_Eater': 'Root', 'Whispering_Root-Mantis_Village': 'Root', 'Whispering_Root-Deepnest': 'Root', 'Whispering_Root-Queens_Gardens': 'Root', 'Whispering_Root-Kingdoms_Edge': 'Root', 'Whispering_Root-Waterways': 'Root', 'Whispering_Root-City': 'Root', 'Whispering_Root-Resting_Grounds': 'Root', 'Whispering_Root-Spirits_Glade': 'Root', 'Whispering_Root-Crystal_Peak': 'Root', 'Whispering_Root-Howling_Cliffs': 'Root', 'Whispering_Root-Ancestral_Mound': 'Root', 'Whispering_Root-Hive': 'Root', 'Boss_Essence-Elder_Hu': 'DreamWarrior', 'Boss_Essence-Xero': 'DreamWarrior', 'Boss_Essence-Gorb': 'DreamWarrior', 'Boss_Essence-Marmu': 'DreamWarrior', 'Boss_Essence-No_Eyes': 'DreamWarrior', 'Boss_Essence-Galien': 'DreamWarrior', 'Boss_Essence-Markoth': 'DreamWarrior', 'Boss_Essence-Failed_Champion': 'DreamBoss', 'Boss_Essence-Soul_Tyrant': 'DreamBoss', 'Boss_Essence-Lost_Kin': 'DreamBoss', 'Boss_Essence-White_Defender': 'DreamBoss', 'Boss_Essence-Grey_Prince_Zote': 'DreamBoss', 'Grub': 'Grub', 'Mimic_Grub': 'Mimic', 'Quill': 'Map', 'Crossroads_Map': 'Map', 'Greenpath_Map': 'Map', 'Fog_Canyon_Map': 'Map', 'Fungal_Wastes_Map': 'Map', 'Deepnest_Map': 'Map', 'Ancient_Basin_Map': 'Map', "Kingdom's_Edge_Map": 'Map', 'City_of_Tears_Map': 'Map', 'Royal_Waterways_Map': 'Map', 'Howling_Cliffs_Map': 'Map', 'Crystal_Peak_Map': 'Map', "Queen's_Gardens_Map": 'Map', 'Resting_Grounds_Map': 'Map', 'Dirtmouth_Stag': 'Stag', 'Crossroads_Stag': 'Stag', 'Greenpath_Stag': 'Stag', "Queen's_Station_Stag": 'Stag', "Queen's_Gardens_Stag": 'Stag', 'City_Storerooms_Stag': 'Stag', "King's_Station_Stag": 'Stag', 'Resting_Grounds_Stag': 'Stag', 'Distant_Village_Stag': 'Stag', 'Hidden_Station_Stag': 'Stag', 'Stag_Nest_Stag': 'Stag', 'Lifeblood_Cocoon_Small': 'Cocoon', 'Lifeblood_Cocoon_Large': 'Cocoon', 'Grimmkin_Flame': 'Flame', "Hunter's_Journal": 'Journal', 'Journal_Entry-Void_Tendrils': 'Journal', 'Journal_Entry-Charged_Lumafly': 'Journal', 'Journal_Entry-Goam': 'Journal', 'Journal_Entry-Garpede': 'Journal', 'Journal_Entry-Seal_of_Binding': 'Journal', 'Elevator_Pass': 'ElevatorPass', 'Left_Mothwing_Cloak': 'SplitCloak', 'Right_Mothwing_Cloak': 'SplitCloak', 'Split_Shade_Cloak': 'SplitCloak', 'Left_Mantis_Claw': 'SplitClaw', 'Right_Mantis_Claw': 'SplitClaw', 'Leftslash': 'CursedNail', 'Rightslash': 'CursedNail', 'Upslash': 'CursedNail', 'Downslash': 'CursedNail', 'Left_Crystal_Heart': 'SplitSuperdash', 'Right_Crystal_Heart': 'SplitSuperdash', 'Geo_Rock-Default': 'Rock', 'Geo_Rock-Deepnest': 'Rock', 'Geo_Rock-Abyss': 'Rock', 'Geo_Rock-GreenPath01': 'Rock', 'Geo_Rock-Outskirts': 'Rock', 'Geo_Rock-Outskirts420': 'Rock', 'Geo_Rock-GreenPath02': 'Rock', 'Geo_Rock-Fung01': 'Rock', 'Geo_Rock-Fung02': 'Rock', 'Geo_Rock-City': 'Rock', 'Geo_Rock-Hive': 'Rock', 'Geo_Rock-Mine': 'Rock', 'Geo_Rock-Grave02': 'Rock', 'Geo_Rock-Grave01': 'Rock', 'Boss_Geo-Massive_Moss_Charger': 'Boss_Geo', 'Boss_Geo-Gorgeous_Husk': 'Boss_Geo', 'Boss_Geo-Sanctum_Soul_Warrior': 'Boss_Geo', 'Boss_Geo-Elegant_Soul_Warrior': 'Boss_Geo', 'Boss_Geo-Crystal_Guardian': 'Boss_Geo', 'Boss_Geo-Enraged_Guardian': 'Boss_Geo', 'Boss_Geo-Gruz_Mother': 'Boss_Geo', 'Boss_Geo-Vengefly_King': 'Boss_Geo', 'Soul_Refill': 'Soul', 'Soul_Totem-A': 'Soul', 'Soul_Totem-B': 'Soul', 'Soul_Totem-C': 'Soul', 'Soul_Totem-D': 'Soul', 'Soul_Totem-E': 'Soul', 'Soul_Totem-F': 'Soul', 'Soul_Totem-G': 'Soul', 'Soul_Totem-Palace': 'Soul', 'Soul_Totem-Path_of_Pain': 'Soul', 'Lore_Tablet-City_Entrance': 'Lore', 'Lore_Tablet-Pleasure_House': 'Lore', 'Lore_Tablet-Sanctum_Entrance': 'Lore', 'Lore_Tablet-Sanctum_Past_Soul_Master': 'Lore', "Lore_Tablet-Watcher's_Spire": 'Lore', 'Lore_Tablet-Archives_Upper': 'Lore', 'Lore_Tablet-Archives_Left': 'Lore', 'Lore_Tablet-Archives_Right': 'Lore', "Lore_Tablet-Pilgrim's_Way_1": 'Lore', "Lore_Tablet-Pilgrim's_Way_2": 'Lore', 'Lore_Tablet-Mantis_Outskirts': 'Lore', 'Lore_Tablet-Mantis_Village': 'Lore', 'Lore_Tablet-Greenpath_Upper_Hidden': 'Lore', 'Lore_Tablet-Greenpath_Below_Toll': 'Lore', 'Lore_Tablet-Greenpath_Lifeblood': 'Lore', 'Lore_Tablet-Greenpath_Stag': 'Lore', 'Lore_Tablet-Greenpath_QG': 'Lore', 'Lore_Tablet-Greenpath_Lower_Hidden': 'Lore', 'Lore_Tablet-Dung_Defender': 'Lore', 'Lore_Tablet-Spore_Shroom': 'Lore', 'Lore_Tablet-Fungal_Wastes_Hidden': 'Lore', 'Lore_Tablet-Fungal_Wastes_Below_Shrumal_Ogres': 'Lore', 'Lore_Tablet-Fungal_Core': 'Lore', 'Lore_Tablet-Ancient_Basin': 'Lore', "Lore_Tablet-King's_Pass_Focus": 'Lore', "Lore_Tablet-King's_Pass_Fury": 'Lore', "Lore_Tablet-King's_Pass_Exit": 'Lore', 'Lore_Tablet-World_Sense': 'Lore', 'Lore_Tablet-Howling_Cliffs': 'Lore', "Lore_Tablet-Kingdom's_Edge": 'Lore', 'Lore_Tablet-Palace_Workshop': 'PalaceLore', 'Lore_Tablet-Palace_Throne': 'PalaceLore', 'Lore_Tablet-Path_of_Pain_Entrance': 'PalaceLore'}
location_to_region_lookup = {'Sly_1': 'Room_shop', 'Sly_2': 'Room_shop', 'Sly_3': 'Room_shop', 'Sly_4': 'Room_shop', 'Sly_5': 'Room_shop', 'Sly_6': 'Room_shop', 'Sly_7': 'Room_shop', 'Sly_8': 'Room_shop', 'Sly_9': 'Room_shop', 'Sly_10': 'Room_shop', 'Sly_11': 'Room_shop', 'Sly_12': 'Room_shop', 'Sly_13': 'Room_shop', 'Sly_14': 'Room_shop', 'Sly_15': 'Room_shop', 'Sly_16': 'Room_shop', 'Sly_(Key)_1': 'Room_shop', 'Sly_(Key)_2': 'Room_shop', 'Sly_(Key)_3': 'Room_shop', 'Sly_(Key)_4': 'Room_shop', 'Sly_(Key)_5': 'Room_shop', 'Sly_(Key)_6': 'Room_shop', 'Sly_(Key)_7': 'Room_shop', 'Sly_(Key)_8': 'Room_shop', 'Sly_(Key)_9': 'Room_shop', 'Sly_(Key)_10': 'Room_shop', 'Sly_(Key)_11': 'Room_shop', 'Sly_(Key)_12': 'Room_shop', 'Sly_(Key)_13': 'Room_shop', 'Sly_(Key)_14': 'Room_shop', 'Sly_(Key)_15': 'Room_shop', 'Sly_(Key)_16': 'Room_shop', 'Iselda_1': 'Room_mapper', 'Iselda_2': 'Room_mapper', 'Iselda_3': 'Room_mapper', 'Iselda_4': 'Room_mapper', 'Iselda_5': 'Room_mapper', 'Iselda_6': 'Room_mapper', 'Iselda_7': 'Room_mapper', 'Iselda_8': 'Room_mapper', 'Iselda_9': 'Room_mapper', 'Iselda_10': 'Room_mapper', 'Iselda_11': 'Room_mapper', 'Iselda_12': 'Room_mapper', 'Iselda_13': 'Room_mapper', 'Iselda_14': 'Room_mapper', 'Iselda_15': 'Room_mapper', 'Iselda_16': 'Room_mapper', 'Salubra_1': 'Room_Charm_Shop', 'Salubra_2': 'Room_Charm_Shop', 'Salubra_3': 'Room_Charm_Shop', 'Salubra_4': 'Room_Charm_Shop', 'Salubra_5': 'Room_Charm_Shop', 'Salubra_6': 'Room_Charm_Shop', 'Salubra_7': 'Room_Charm_Shop', 'Salubra_8': 'Room_Charm_Shop', 'Salubra_9': 'Room_Charm_Shop', 'Salubra_10': 'Room_Charm_Shop', 'Salubra_11': 'Room_Charm_Shop', 'Salubra_12': 'Room_Charm_Shop', 'Salubra_13': 'Room_Charm_Shop', 'Salubra_14': 'Room_Charm_Shop', 'Salubra_15': 'Room_Charm_Shop', 'Salubra_16': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_1': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_2': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_3': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_4': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_5': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_6': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_7': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_8': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_9': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_10': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_11': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_12': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_13': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_14': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_15': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_16': 'Room_Charm_Shop', 'Leg_Eater_1': 'Fungus2_26', 'Leg_Eater_2': 'Fungus2_26', 'Leg_Eater_3': 'Fungus2_26', 'Leg_Eater_4': 'Fungus2_26', 'Leg_Eater_5': 'Fungus2_26', 'Leg_Eater_6': 'Fungus2_26', 'Leg_Eater_7': 'Fungus2_26', 'Leg_Eater_8': 'Fungus2_26', 'Leg_Eater_9': 'Fungus2_26', 'Leg_Eater_10': 'Fungus2_26', 'Leg_Eater_11': 'Fungus2_26', 'Leg_Eater_12': 'Fungus2_26', 'Leg_Eater_13': 'Fungus2_26', 'Leg_Eater_14': 'Fungus2_26', 'Leg_Eater_15': 'Fungus2_26', 'Leg_Eater_16': 'Fungus2_26', 'Grubfather_1': 'Crossroads_38', 'Grubfather_2': 'Crossroads_38', 'Grubfather_3': 'Crossroads_38', 'Grubfather_4': 'Crossroads_38', 'Grubfather_5': 'Crossroads_38', 'Grubfather_6': 'Crossroads_38', 'Grubfather_7': 'Crossroads_38', 'Grubfather_8': 'Crossroads_38', 'Grubfather_9': 'Crossroads_38', 'Grubfather_10': 'Crossroads_38', 'Grubfather_11': 'Crossroads_38', 'Grubfather_12': 'Crossroads_38', 'Grubfather_13': 'Crossroads_38', 'Grubfather_14': 'Crossroads_38', 'Grubfather_15': 'Crossroads_38', 'Grubfather_16': 'Crossroads_38', 'Seer_1': 'RestingGrounds_07', 'Seer_2': 'RestingGrounds_07', 'Seer_3': 'RestingGrounds_07', 'Seer_4': 'RestingGrounds_07', 'Seer_5': 'RestingGrounds_07', 'Seer_6': 'RestingGrounds_07', 'Seer_7': 'RestingGrounds_07', 'Seer_8': 'RestingGrounds_07', 'Seer_9': 'RestingGrounds_07', 'Seer_10': 'RestingGrounds_07', 'Seer_11': 'RestingGrounds_07', 'Seer_12': 'RestingGrounds_07', 'Seer_13': 'RestingGrounds_07', 'Seer_14': 'RestingGrounds_07', 'Seer_15': 'RestingGrounds_07', 'Seer_16': 'RestingGrounds_07', 'Egg_Shop_1': 'Room_Ouiji', 'Egg_Shop_2': 'Room_Ouiji', 'Egg_Shop_3': 'Room_Ouiji', 'Egg_Shop_4': 'Room_Ouiji', 'Egg_Shop_5': 'Room_Ouiji', 'Egg_Shop_6': 'Room_Ouiji', 'Egg_Shop_7': 'Room_Ouiji', 'Egg_Shop_8': 'Room_Ouiji', 'Egg_Shop_9': 'Room_Ouiji', 'Egg_Shop_10': 'Room_Ouiji', 'Egg_Shop_11': 'Room_Ouiji', 'Egg_Shop_12': 'Room_Ouiji', 'Egg_Shop_13': 'Room_Ouiji', 'Egg_Shop_14': 'Room_Ouiji', 'Egg_Shop_15': 'Room_Ouiji', 'Egg_Shop_16': 'Room_Ouiji', 'Lurien': 'Ruins2_Watcher_Room', 'Monomon': 'Fungus3_archive_02', 'Herrah': 'Deepnest_Spider_Town', 'World_Sense': 'Room_temple', 'Mothwing_Cloak': 'Fungus1_04', 'Mantis_Claw': 'Fungus2_14', 'Crystal_Heart': 'Mines_31', 'Monarch_Wings': 'Abyss_21', 'Shade_Cloak': 'Abyss_10', "Isma's_Tear": 'Waterways_13', 'Dream_Nail': 'RestingGrounds_04', 'Vengeful_Spirit': 'Crossroads_ShamanTemple', 'Shade_Soul': 'Ruins1_31b', 'Desolate_Dive': 'Ruins1_24', 'Descending_Dark': 'Mines_35', 'Howling_Wraiths': 'Room_Fungus_Shaman', 'Abyss_Shriek': 'Abyss_12', 'Cyclone_Slash': 'Room_nailmaster', 'Dash_Slash': 'Room_nailmaster_03', 'Great_Slash': 'Room_nailmaster_02', 'Focus': 'Tutorial_01', 'Baldur_Shell': 'Fungus1_28', 'Fury_of_the_Fallen': 'Tutorial_01', 'Lifeblood_Core': 'Abyss_08', "Defender's_Crest": 'Waterways_05', 'Flukenest': 'Waterways_12', 'Thorns_of_Agony': 'Fungus1_14', 'Mark_of_Pride': 'Fungus2_31', 'Sharp_Shadow': 'Deepnest_44', 'Spore_Shroom': 'Fungus2_20', 'Soul_Catcher': 'Crossroads_ShamanTemple', 'Soul_Eater': 'RestingGrounds_10', 'Glowing_Womb': 'Crossroads_22', "Nailmaster's_Glory": 'Room_shop', "Joni's_Blessing": 'Cliffs_05', 'Shape_of_Unn': 'Fungus1_Slug', 'Hiveblood': 'Hive_05', 'Dashmaster': 'Fungus2_23', 'Quick_Slash': 'Deepnest_East_14b', 'Spell_Twister': 'Ruins1_30', 'Deep_Focus': 'Mines_36', 'Queen_Fragment': 'Room_Queen', 'King_Fragment': 'White_Palace_09', 'Void_Heart': 'Abyss_15', 'Dreamshield': 'RestingGrounds_17', 'Weaversong': 'Deepnest_45_v02', 'Grimmchild': 'Grimm_Main_Tent', 'Unbreakable_Heart': 'Grimm_Divine', 'Unbreakable_Greed': 'Grimm_Divine', 'Unbreakable_Strength': 'Grimm_Divine', 'City_Crest': 'Crossroads_10', 'Tram_Pass': 'Deepnest_26b', 'Simple_Key-Basin': 'Abyss_20', 'Simple_Key-City': 'Ruins1_17', 'Simple_Key-Lurker': 'GG_Lurker', "Shopkeeper's_Key": 'Mines_11', 'Love_Key': 'Fungus3_39', "King's_Brand": 'Room_Wyrm', 'Godtuner': 'GG_Waterways', "Collector's_Map": 'Ruins2_11', 'Mask_Shard-Brooding_Mawlek': 'Crossroads_09', 'Mask_Shard-Crossroads_Goam': 'Crossroads_13', 'Mask_Shard-Stone_Sanctuary': 'Fungus1_36', "Mask_Shard-Queen's_Station": 'Fungus2_01', 'Mask_Shard-Deepnest': 'Fungus2_25', 'Mask_Shard-Waterways': 'Waterways_04b', 'Mask_Shard-Enraged_Guardian': 'Mines_32', 'Mask_Shard-Hive': 'Hive_04', 'Mask_Shard-Grey_Mourner': 'Room_Mansion', 'Mask_Shard-Bretta': 'Room_Bretta', 'Vessel_Fragment-Greenpath': 'Fungus1_13', 'Vessel_Fragment-City': 'Ruins2_09', 'Vessel_Fragment-Crossroads': 'Crossroads_37', 'Vessel_Fragment-Basin': 'Abyss_04', 'Vessel_Fragment-Deepnest': 'Deepnest_38', 'Vessel_Fragment-Stag_Nest': 'Cliffs_03', 'Charm_Notch-Shrumal_Ogres': 'Fungus2_05', 'Charm_Notch-Fog_Canyon': 'Fungus3_28', 'Charm_Notch-Colosseum': 'Room_Colosseum_01', 'Charm_Notch-Grimm': 'Grimm_Main_Tent', 'Pale_Ore-Basin': 'Abyss_17', 'Pale_Ore-Crystal_Peak': 'Mines_34', 'Pale_Ore-Nosk': 'Deepnest_32', 'Pale_Ore-Colosseum': 'Room_Colosseum_01', 'Geo_Chest-False_Knight': 'Crossroads_10', 'Geo_Chest-Soul_Master': 'Ruins1_32', 'Geo_Chest-Watcher_Knights': 'Ruins2_03', 'Geo_Chest-Greenpath': 'Fungus1_13', 'Geo_Chest-Mantis_Lords': 'Fungus2_31', 'Geo_Chest-Resting_Grounds': 'RestingGrounds_10', 'Geo_Chest-Crystal_Peak': 'Mines_37', 'Geo_Chest-Weavers_Den': 'Deepnest_45_v02', 'Geo_Chest-Junk_Pit_1': 'GG_Waterways', 'Geo_Chest-Junk_Pit_2': 'GG_Waterways', 'Geo_Chest-Junk_Pit_3': 'GG_Waterways', 'Geo_Chest-Junk_Pit_5': 'GG_Waterways', 'Lumafly_Escape-Junk_Pit_Chest_4': 'GG_Waterways', 'Rancid_Egg-Sheo': 'Fungus1_15', 'Rancid_Egg-Fungal_Core': 'Fungus2_29', "Rancid_Egg-Queen's_Gardens": 'Fungus3_34', 'Rancid_Egg-Blue_Lake': 'Crossroads_50', 'Rancid_Egg-Crystal_Peak_Dive_Entrance': 'Mines_01', 'Rancid_Egg-Crystal_Peak_Dark_Room': 'Mines_29', 'Rancid_Egg-Crystal_Peak_Tall_Room': 'Mines_20', 'Rancid_Egg-City_of_Tears_Left': 'Ruins1_05c', 'Rancid_Egg-City_of_Tears_Pleasure_House': 'Ruins_Elevator', "Rancid_Egg-Beast's_Den": 'Deepnest_Spider_Town', 'Rancid_Egg-Dark_Deepnest': 'Deepnest_39', "Rancid_Egg-Weaver's_Den": 'Deepnest_45_v02', 'Rancid_Egg-Near_Quick_Slash': 'Deepnest_East_14', "Rancid_Egg-Upper_Kingdom's_Edge": 'Deepnest_East_07', 'Rancid_Egg-Waterways_East': 'Waterways_07', 'Rancid_Egg-Waterways_Main': 'Waterways_02', 'Rancid_Egg-Waterways_West_Bluggsac': 'Waterways_04', 'Rancid_Egg-Waterways_West_Pickup': 'Waterways_04b', "Rancid_Egg-Tuk_Defender's_Crest": 'Waterways_03', "Wanderer's_Journal-Cliffs": 'Cliffs_01', "Wanderer's_Journal-Greenpath_Stag": 'Fungus1_22', "Wanderer's_Journal-Greenpath_Lower": 'Fungus1_11', "Wanderer's_Journal-Fungal_Wastes_Thorns_Gauntlet": 'Fungus2_04', "Wanderer's_Journal-Above_Mantis_Village": 'Fungus2_17', "Wanderer's_Journal-Crystal_Peak_Crawlers": 'Mines_20', "Wanderer's_Journal-Resting_Grounds_Catacombs": 'RestingGrounds_10', "Wanderer's_Journal-King's_Station": 'Ruins2_05', "Wanderer's_Journal-Pleasure_House": 'Ruins_Elevator', "Wanderer's_Journal-City_Storerooms": 'Ruins1_28', "Wanderer's_Journal-Ancient_Basin": 'Abyss_02', "Wanderer's_Journal-Kingdom's_Edge_Entrance": 'Deepnest_East_07', "Wanderer's_Journal-Kingdom's_Edge_Camp": 'Deepnest_East_13', "Wanderer's_Journal-Kingdom's_Edge_Requires_Dive": 'Deepnest_East_18', 'Hallownest_Seal-Crossroads_Well': 'Crossroads_01', 'Hallownest_Seal-Greenpath': 'Fungus1_10', 'Hallownest_Seal-Fog_Canyon_West': 'Fungus3_30', 'Hallownest_Seal-Fog_Canyon_East': 'Fungus3_26', "Hallownest_Seal-Queen's_Station": 'Fungus2_34', 'Hallownest_Seal-Fungal_Wastes_Sporgs': 'Fungus2_03', 'Hallownest_Seal-Mantis_Lords': 'Fungus2_31', 'Hallownest_Seal-Resting_Grounds_Catacombs': 'RestingGrounds_10', "Hallownest_Seal-King's_Station": 'Ruins2_08', 'Hallownest_Seal-City_Rafters': 'Ruins1_03', 'Hallownest_Seal-Soul_Sanctum': 'Ruins1_32', 'Hallownest_Seal-Watcher_Knight': 'Ruins2_03', 'Hallownest_Seal-Deepnest_By_Mantis_Lords': 'Deepnest_16', "Hallownest_Seal-Beast's_Den": 'Deepnest_Spider_Town', "Hallownest_Seal-Queen's_Gardens": 'Fungus3_48', "King's_Idol-Cliffs": 'Cliffs_01', "King's_Idol-Crystal_Peak": 'Mines_30', "King's_Idol-Glade_of_Hope": 'RestingGrounds_08', "King's_Idol-Dung_Defender": 'Waterways_15', "King's_Idol-Great_Hopper": 'Deepnest_East_08', "King's_Idol-Pale_Lurker": 'GG_Lurker', "King's_Idol-Deepnest": 'Deepnest_33', 'Arcane_Egg-Lifeblood_Core': 'Abyss_08', 'Arcane_Egg-Shade_Cloak': 'Abyss_10', 'Arcane_Egg-Birthplace': 'Abyss_15', 'Whispering_Root-Crossroads': 'Crossroads_07', 'Whispering_Root-Greenpath': 'Fungus1_13', 'Whispering_Root-Leg_Eater': 'Fungus2_33', 'Whispering_Root-Mantis_Village': 'Fungus2_17', 'Whispering_Root-Deepnest': 'Deepnest_39', 'Whispering_Root-Queens_Gardens': 'Fungus3_11', 'Whispering_Root-Kingdoms_Edge': 'Deepnest_East_07', 'Whispering_Root-Waterways': 'Abyss_01', 'Whispering_Root-City': 'Ruins1_17', 'Whispering_Root-Resting_Grounds': 'RestingGrounds_05', 'Whispering_Root-Spirits_Glade': 'RestingGrounds_08', 'Whispering_Root-Crystal_Peak': 'Mines_23', 'Whispering_Root-Howling_Cliffs': 'Cliffs_01', 'Whispering_Root-Ancestral_Mound': 'Crossroads_ShamanTemple', 'Whispering_Root-Hive': 'Hive_02', 'Boss_Essence-Elder_Hu': 'Fungus2_32', 'Boss_Essence-Xero': 'RestingGrounds_02', 'Boss_Essence-Gorb': 'Cliffs_02', 'Boss_Essence-Marmu': 'Fungus3_40', 'Boss_Essence-No_Eyes': 'Fungus1_35', 'Boss_Essence-Galien': 'Deepnest_40', 'Boss_Essence-Markoth': 'Deepnest_East_10', 'Boss_Essence-Failed_Champion': 'Crossroads_10', 'Boss_Essence-Soul_Tyrant': 'Ruins1_24', 'Boss_Essence-Lost_Kin': 'Abyss_19', 'Boss_Essence-White_Defender': 'Waterways_15', 'Boss_Essence-Grey_Prince_Zote': 'Room_Bretta', 'Grub-Crossroads_Acid': 'Crossroads_35', 'Grub-Crossroads_Center': 'Crossroads_05', 'Grub-Crossroads_Stag': 'Crossroads_03', 'Grub-Crossroads_Spike': 'Crossroads_31', 'Grub-Crossroads_Guarded': 'Crossroads_48', 'Grub-Greenpath_Cornifer': 'Fungus1_06', 'Grub-Greenpath_Journal': 'Fungus1_07', 'Grub-Greenpath_MMC': 'Fungus1_13', 'Grub-Greenpath_Stag': 'Fungus1_21', 'Grub-Fog_Canyon': 'Fungus3_47', 'Grub-Fungal_Bouncy': 'Fungus2_18', 'Grub-Fungal_Spore_Shroom': 'Fungus2_20', 'Grub-Deepnest_Mimic': 'Deepnest_36', 'Grub-Deepnest_Nosk': 'Deepnest_31', 'Grub-Deepnest_Spike': 'Deepnest_03', 'Grub-Dark_Deepnest': 'Deepnest_39', "Grub-Beast's_Den": 'Deepnest_Spider_Town', "Grub-Kingdom's_Edge_Oro": 'Deepnest_East_14', "Grub-Kingdom's_Edge_Camp": 'Deepnest_East_11', 'Grub-Hive_External': 'Hive_03', 'Grub-Hive_Internal': 'Hive_04', 'Grub-Basin_Requires_Wings': 'Abyss_19', 'Grub-Basin_Requires_Dive': 'Abyss_17', 'Grub-Waterways_Main': 'Waterways_04', "Grub-Isma's_Grove": 'Waterways_13', 'Grub-Waterways_Requires_Tram': 'Waterways_14', 'Grub-City_of_Tears_Left': 'Ruins1_05', 'Grub-Soul_Sanctum': 'Ruins1_32', "Grub-Watcher's_Spire": 'Ruins2_03', 'Grub-City_of_Tears_Guarded': 'Ruins_House_01', "Grub-King's_Station": 'Ruins2_07', 'Grub-Resting_Grounds': 'RestingGrounds_10', 'Grub-Crystal_Peak_Below_Chest': 'Mines_04', 'Grub-Crystallized_Mound': 'Mines_35', 'Grub-Crystal_Peak_Spike': 'Mines_03', 'Grub-Crystal_Peak_Mimic': 'Mines_16', 'Grub-Crystal_Peak_Crushers': 'Mines_19', 'Grub-Crystal_Heart': 'Mines_31', 'Grub-Hallownest_Crown': 'Mines_24', 'Grub-Howling_Cliffs': 'Fungus1_28', "Grub-Queen's_Gardens_Stag": 'Fungus3_10', "Grub-Queen's_Gardens_Marmu": 'Fungus3_48', "Grub-Queen's_Gardens_Top": 'Fungus3_22', 'Grub-Collector_1': 'Ruins2_11', 'Grub-Collector_2': 'Ruins2_11', 'Grub-Collector_3': 'Ruins2_11', 'Mimic_Grub-Deepnest_1': 'Deepnest_36', 'Mimic_Grub-Deepnest_2': 'Deepnest_36', 'Mimic_Grub-Deepnest_3': 'Deepnest_36', 'Mimic_Grub-Crystal_Peak': 'Mines_16', 'Crossroads_Map': 'Crossroads_33', 'Greenpath_Map': 'Fungus1_06', 'Fog_Canyon_Map': 'Fungus3_25', 'Fungal_Wastes_Map': 'Fungus2_18', 'Deepnest_Map-Upper': 'Deepnest_01b', 'Deepnest_Map-Right': 'Fungus2_25', 'Ancient_Basin_Map': 'Abyss_04', "Kingdom's_Edge_Map": 'Deepnest_East_03', 'City_of_Tears_Map': 'Ruins1_31', 'Royal_Waterways_Map': 'Waterways_09', 'Howling_Cliffs_Map': 'Cliffs_01', 'Crystal_Peak_Map': 'Mines_30', "Queen's_Gardens_Map": 'Fungus1_24', 'Resting_Grounds_Map': 'RestingGrounds_09', 'Dirtmouth_Stag': 'Room_Town_Stag_Station', 'Crossroads_Stag': 'Crossroads_47', 'Greenpath_Stag': 'Fungus1_16_alt', "Queen's_Station_Stag": 'Fungus2_02', "Queen's_Gardens_Stag": 'Fungus3_40', 'City_Storerooms_Stag': 'Ruins1_29', "King's_Station_Stag": 'Ruins2_08', 'Resting_Grounds_Stag': 'RestingGrounds_09', 'Distant_Village_Stag': 'Deepnest_09', 'Hidden_Station_Stag': 'Abyss_22', 'Stag_Nest_Stag': 'Cliffs_03', "Lifeblood_Cocoon-King's_Pass": 'Tutorial_01', 'Lifeblood_Cocoon-Ancestral_Mound': 'Crossroads_ShamanTemple', 'Lifeblood_Cocoon-Greenpath': 'Fungus1_32', 'Lifeblood_Cocoon-Fog_Canyon_West': 'Fungus3_30', 'Lifeblood_Cocoon-Mantis_Village': 'Fungus2_15', 'Lifeblood_Cocoon-Failed_Tramway': 'Deepnest_26', 'Lifeblood_Cocoon-Galien': 'Deepnest_40', "Lifeblood_Cocoon-Kingdom's_Edge": 'Deepnest_East_15', 'Grimmkin_Flame-City_Storerooms': 'Ruins1_28', 'Grimmkin_Flame-Greenpath': 'Fungus1_10', 'Grimmkin_Flame-Crystal_Peak': 'Mines_10', "Grimmkin_Flame-King's_Pass": 'Tutorial_01', 'Grimmkin_Flame-Resting_Grounds': 'RestingGrounds_06', "Grimmkin_Flame-Kingdom's_Edge": 'Deepnest_East_03', 'Grimmkin_Flame-Fungal_Core': 'Fungus2_30', 'Grimmkin_Flame-Ancient_Basin': 'Abyss_02', 'Grimmkin_Flame-Hive': 'Hive_03', 'Grimmkin_Flame-Brumm': 'Room_spider_small', "Hunter's_Journal": 'Fungus1_08', 'Journal_Entry-Void_Tendrils': 'Abyss_09', 'Journal_Entry-Charged_Lumafly': 'Fungus3_archive_02', 'Journal_Entry-Goam': 'Crossroads_52', 'Journal_Entry-Garpede': 'Deepnest_44', 'Journal_Entry-Seal_of_Binding': 'White_Palace_20', 'Elevator_Pass': 'Crossroads_49b', 'Split_Mothwing_Cloak': 'Fungus1_04', 'Left_Mantis_Claw': 'Fungus2_14', 'Right_Mantis_Claw': 'Fungus2_14', 'Leftslash': 'Tutorial_01', 'Rightslash': 'Tutorial_01', 'Upslash': 'Tutorial_01', 'Split_Crystal_Heart': 'Mines_31', 'Geo_Rock-Broken_Elevator_1': 'Abyss_01', 'Geo_Rock-Broken_Elevator_2': 'Abyss_01', 'Geo_Rock-Broken_Elevator_3': 'Abyss_01', 'Geo_Rock-Broken_Bridge_Upper': 'Abyss_02', 'Geo_Rock-Broken_Bridge_Lower': 'Abyss_02', 'Geo_Rock-Broken_Bridge_Lower_Dupe': 'Abyss_02', 'Geo_Rock-Abyss_1': 'Abyss_06_Core', 'Geo_Rock-Abyss_2': 'Abyss_06_Core', 'Geo_Rock-Abyss_3': 'Abyss_06_Core', 'Geo_Rock-Basin_Tunnel': 'Abyss_18', 'Geo_Rock-Basin_Grub': 'Abyss_19', 'Geo_Rock-Basin_Before_Broken_Vessel': 'Abyss_19', 'Geo_Rock-Cliffs_Main_1': 'Cliffs_01', 'Geo_Rock-Cliffs_Main_2': 'Cliffs_01', 'Geo_Rock-Cliffs_Main_3': 'Cliffs_01', 'Geo_Rock-Cliffs_Main_4': 'Cliffs_01', 'Geo_Rock-Below_Gorb_Dupe': 'Cliffs_02', 'Geo_Rock-Below_Gorb': 'Cliffs_02', 'Geo_Rock-Crossroads_Well': 'Crossroads_01', 'Geo_Rock-Crossroads_Center_Grub': 'Crossroads_05', 'Geo_Rock-Crossroads_Root': 'Crossroads_07', 'Geo_Rock-Crossroads_Root_Dupe_1': 'Crossroads_07', 'Geo_Rock-Crossroads_Root_Dupe_2': 'Crossroads_07', 'Geo_Rock-Crossroads_Aspid_Arena': 'Crossroads_08', 'Geo_Rock-Crossroads_Aspid_Arena_Dupe_1': 'Crossroads_08', 'Geo_Rock-Crossroads_Aspid_Arena_Dupe_2': 'Crossroads_08', 'Geo_Rock-Crossroads_Aspid_Arena_Hidden': 'Crossroads_08', 'Geo_Rock-Crossroads_Above_False_Knight': 'Crossroads_10', 'Geo_Rock-Crossroads_Before_Acid_Grub': 'Crossroads_12', 'Geo_Rock-Crossroads_Below_Goam_Mask_Shard': 'Crossroads_13', 'Geo_Rock-Crossroads_After_Goam_Mask_Shard': 'Crossroads_13', 'Geo_Rock-Crossroads_Above_Lever': 'Crossroads_16', 'Geo_Rock-Crossroads_Before_Fungal': 'Crossroads_18', 'Geo_Rock-Crossroads_Before_Fungal_Dupe_1': 'Crossroads_18', 'Geo_Rock-Crossroads_Before_Fungal_Dupe_2': 'Crossroads_18', 'Geo_Rock-Crossroads_Before_Shops': 'Crossroads_19', 'Geo_Rock-Crossroads_Before_Glowing_Womb': 'Crossroads_21', 'Geo_Rock-Crossroads_Above_Tram': 'Crossroads_27', 'Geo_Rock-Crossroads_Above_Mawlek': 'Crossroads_36', 'Geo_Rock-Crossroads_Vessel_Fragment': 'Crossroads_37', 'Geo_Rock-Crossroads_Goam_Alcove': 'Crossroads_42', 'Geo_Rock-Crossroads_Goam_Damage_Boost': 'Crossroads_42', 'Geo_Rock-Crossroads_Tram': 'Crossroads_46', 'Geo_Rock-Crossroads_Goam_Journal': 'Crossroads_52', 'Geo_Rock-Crossroads_Goam_Journal_Dupe': 'Crossroads_52', 'Geo_Rock-Ancestral_Mound': 'Crossroads_ShamanTemple', 'Geo_Rock-Ancestral_Mound_Dupe': 'Crossroads_ShamanTemple', 'Geo_Rock-Ancestral_Mound_Tree': 'Crossroads_ShamanTemple', 'Geo_Rock-Ancestral_Mound_Tree_Dupe': 'Crossroads_ShamanTemple', 'Geo_Rock-Moss_Prophet': 'Deepnest_01', 'Geo_Rock-Moss_Prophet_Dupe': 'Deepnest_01', 'Geo_Rock-Deepnest_Below_Mimics': 'Deepnest_02', 'Geo_Rock-Deepnest_Below_Mimics_Dupe': 'Deepnest_02', 'Geo_Rock-Deepnest_Below_Spike_Grub': 'Deepnest_03', 'Geo_Rock-Deepnest_Below_Spike_Grub_Dupe': 'Deepnest_03', 'Geo_Rock-Deepnest_Spike_Grub_Right': 'Deepnest_03', 'Geo_Rock-Deepnest_By_Mantis_Lords_Garpede_Pogo': 'Deepnest_16', 'Geo_Rock-Deepnest_By_Mantis_Lords_Garpede_Pogo_Dupe': 'Deepnest_16', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_1': 'Deepnest_16', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_2': 'Deepnest_16', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_3': 'Deepnest_16', 'Geo_Rock-Deepnest_Nosk_1': 'Deepnest_31', 'Geo_Rock-Deepnest_Nosk_2': 'Deepnest_31', 'Geo_Rock-Deepnest_Nosk_3': 'Deepnest_31', 'Geo_Rock-Deepnest_Above_Galien': 'Deepnest_35', 'Geo_Rock-Deepnest_Galien_Spike': 'Deepnest_35', 'Geo_Rock-Deepnest_Garpede_1': 'Deepnest_37', 'Geo_Rock-Deepnest_Garpede_2': 'Deepnest_37', 'Geo_Rock-Dark_Deepnest_Above_Grub_1': 'Deepnest_39', 'Geo_Rock-Dark_Deepnest_Above_Grub_2': 'Deepnest_39', 'Geo_Rock-Dark_Deepnest_Bottom_Left': 'Deepnest_39', 'Geo_Rock-Above_Mask_Maker_1': 'Deepnest_43', 'Geo_Rock-Above_Mask_Maker_2': 'Deepnest_43', "Geo_Rock-Lower_Kingdom's_Edge_1": 'Deepnest_East_01', "Geo_Rock-Lower_Kingdom's_Edge_2": 'Deepnest_East_01', "Geo_Rock-Lower_Kingdom's_Edge_3": 'Deepnest_East_02', "Geo_Rock-Lower_Kingdom's_Edge_Dive": 'Deepnest_East_02', "Geo_Rock-Kingdom's_Edge_Below_Bardoon": 'Deepnest_East_04', "Geo_Rock-Kingdom's_Edge_Oro_Far_Left": 'Deepnest_East_06', "Geo_Rock-Kingdom's_Edge_Oro_Middle_Left": 'Deepnest_East_06', "Geo_Rock-Kingdom's_Edge_Above_Root": 'Deepnest_East_07', "Geo_Rock-Kingdom's_Edge_Above_Tower": 'Deepnest_East_07', "Geo_Rock-Kingdom's_Edge_Below_Colosseum": 'Deepnest_East_08', "Geo_Rock-Kingdom's_Edge_Above_420_Geo_Rock": 'Deepnest_East_17', "Geo_Rock-Kingdom's_Edge_420_Geo_Rock": 'Deepnest_East_17', "Geo_Rock-Beast's_Den_Above_Trilobite": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Above_Trilobite_Dupe": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Below_Herrah": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Below_Egg": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Below_Egg_Dupe": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Bottom": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Bottom_Dupe": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_After_Herrah": 'Deepnest_Spider_Town', 'Geo_Rock-Greenpath_Entrance': 'Fungus1_01', 'Geo_Rock-Greenpath_Waterfall': 'Fungus1_01b', 'Geo_Rock-Greenpath_Below_Skip_Squit': 'Fungus1_02', 'Geo_Rock-Greenpath_Skip_Squit': 'Fungus1_02', 'Geo_Rock-Greenpath_Second_Skip_Fool_Eater': 'Fungus1_03', 'Geo_Rock-Greenpath_Second_Skip_Fool_Eater_Dupe': 'Fungus1_03', 'Geo_Rock-Greenpath_Second_Skip_Lower': 'Fungus1_03', 'Geo_Rock-Greenpath_Below_Hornet': 'Fungus1_04', 'Geo_Rock-Greenpath_Above_Thorns': 'Fungus1_05', "Geo_Rock-Greenpath_Hunter's_Journal": 'Fungus1_07', 'Geo_Rock-Greenpath_Acid_Bridge': 'Fungus1_10', 'Geo_Rock-Greenpath_After_MMC_Hidden': 'Fungus1_12', 'Geo_Rock-Greenpath_After_MMC': 'Fungus1_12', 'Geo_Rock-Greenpath_After_MMC_Dupe': 'Fungus1_12', 'Geo_Rock-Greenpath_Obbles_Fool_Eater': 'Fungus1_19', 'Geo_Rock-Greenpath_Moss_Knights': 'Fungus1_21', 'Geo_Rock-Greenpath_Moss_Knights_Dupe_1': 'Fungus1_21', 'Geo_Rock-Greenpath_Moss_Knights_Dupe_2': 'Fungus1_21', 'Geo_Rock-Greenpath_Below_Stag': 'Fungus1_22', 'Geo_Rock-Greenpath_Below_Stag_Fool_Eater': 'Fungus1_22', 'Geo_Rock-Baldur_Shell_Top_Left': 'Fungus1_28', 'Geo_Rock-Baldur_Shell_Alcove': 'Fungus1_28', 'Geo_Rock-Greenpath_MMC': 'Fungus1_29', 'Geo_Rock-Greenpath_Below_Toll': 'Fungus1_31', 'Geo_Rock-Greenpath_Toll_Hidden': 'Fungus1_31', 'Geo_Rock-Greenpath_Toll_Hidden_Dupe': 'Fungus1_31', 'Geo_Rock-Fungal_Below_Shrumal_Ogres': 'Fungus2_04', 'Geo_Rock-Fungal_Above_Cloth': 'Fungus2_08', 'Geo_Rock-Fungal_After_Cloth': 'Fungus2_10', "Geo_Rock-Fungal_Below_Pilgrim's_Way": 'Fungus2_11', "Geo_Rock-Fungal_Below_Pilgrim's_Way_Dupe": 'Fungus2_11', 'Geo_Rock-Mantis_Outskirts_Guarded': 'Fungus2_13', 'Geo_Rock-Mantis_Outskirts_Guarded_Dupe': 'Fungus2_13', 'Geo_Rock-Mantis_Outskirts_Alcove': 'Fungus2_13', 'Geo_Rock-Mantis_Village_After_Lever': 'Fungus2_14', 'Geo_Rock-Mantis_Village_Above_Claw': 'Fungus2_14', 'Geo_Rock-Mantis_Village_Above_Claw_Dupe': 'Fungus2_14', 'Geo_Rock-Mantis_Village_Below_Lore': 'Fungus2_14', 'Geo_Rock-Mantis_Village_Above_Lever': 'Fungus2_14', 'Geo_Rock-Above_Mantis_Lords_1': 'Fungus2_15', 'Geo_Rock-Above_Mantis_Lords_2': 'Fungus2_15', 'Geo_Rock-Fungal_After_Bouncy_Grub': 'Fungus2_18', 'Geo_Rock-Fungal_After_Bouncy_Grub_Dupe': 'Fungus2_18', 'Geo_Rock-Fungal_Bouncy_Grub_Lever': 'Fungus2_18', 'Geo_Rock-Fungal_After_Cornifer': 'Fungus2_18', 'Geo_Rock-Fungal_Above_City_Entrance': 'Fungus2_21', 'Geo_Rock-Deepnest_By_Mantis_Lords_1': 'Fungus2_25', 'Geo_Rock-Deepnest_By_Mantis_Lords_2': 'Fungus2_25', 'Geo_Rock-Deepnest_Lower_Cornifer': 'Fungus2_25', 'Geo_Rock-Fungal_Core_Entrance': 'Fungus2_29', 'Geo_Rock-Fungal_Core_Hidden': 'Fungus2_30', 'Geo_Rock-Fungal_Core_Above_Elder': 'Fungus2_30', "Geo_Rock-Queen's_Gardens_Acid_Entrance": 'Fungus3_03', "Geo_Rock-Queen's_Gardens_Below_Stag": 'Fungus3_10', 'Geo_Rock-Fog_Canyon_East': 'Fungus3_26', 'Geo_Rock-Love_Key': 'Fungus3_39', 'Geo_Rock-Love_Key_Dupe': 'Fungus3_39', "Geo_Rock-Queen's_Gardens_Above_Marmu": 'Fungus3_48', 'Geo_Rock-Pale_Lurker': 'GG_Lurker', 'Geo_Rock-Godhome_Pipeway': 'GG_Pipeway', 'Geo_Rock-Hive_Entrance': 'Hive_01', 'Geo_Rock-Hive_Outside_Bench': 'Hive_02', 'Geo_Rock-Hive_Below_Root': 'Hive_02', 'Geo_Rock-Hive_After_Root': 'Hive_02', 'Geo_Rock-Hive_Below_Stash': 'Hive_03', 'Geo_Rock-Hive_Stash': 'Hive_03', 'Geo_Rock-Hive_Stash_Dupe': 'Hive_03', 'Geo_Rock-Hive_Below_Grub': 'Hive_04', 'Geo_Rock-Hive_Above_Mask': 'Hive_04', 'Geo_Rock-Crystal_Peak_Lower_Middle': 'Mines_02', 'Geo_Rock-Crystal_Peak_Lower_Conveyer_1': 'Mines_02', 'Geo_Rock-Crystal_Peak_Lower_Conveyer_2': 'Mines_02', 'Geo_Rock-Crystal_Peak_Before_Dark_Room': 'Mines_04', 'Geo_Rock-Crystal_Peak_Before_Dark_Room_Dupe': 'Mines_04', 'Geo_Rock-Crystal_Peak_Above_Spike_Grub': 'Mines_05', 'Geo_Rock-Crystal_Peak_Mimic_Grub': 'Mines_16', 'Geo_Rock-Crystal_Peak_Dive_Egg': 'Mines_20', 'Geo_Rock-Crystal_Peak_Dive_Egg_Dupe': 'Mines_20', 'Geo_Rock-Crystal_Peak_Conga_Line': 'Mines_20', 'Geo_Rock-Hallownest_Crown_Dive': 'Mines_25', 'Geo_Rock-Hallownest_Crown_Dive_Dupe': 'Mines_25', 'Geo_Rock-Hallownest_Crown_Hidden': 'Mines_25', 'Geo_Rock-Hallownest_Crown_Hidden_Dupe_1': 'Mines_25', 'Geo_Rock-Hallownest_Crown_Hidden_Dupe_2': 'Mines_25', 'Geo_Rock-Crystal_Peak_Before_Crystal_Heart': 'Mines_31', 'Geo_Rock-Crystal_Peak_Entrance': 'Mines_33', 'Geo_Rock-Crystal_Peak_Entrance_Dupe_1': 'Mines_33', 'Geo_Rock-Crystal_Peak_Entrance_Dupe_2': 'Mines_33', 'Geo_Rock-Crystal_Peak_Above_Crushers_Lower': 'Mines_37', 'Geo_Rock-Crystal_Peak_Above_Crushers_Higher': 'Mines_37', 'Geo_Rock-Resting_Grounds_Catacombs_Grub': 'RestingGrounds_10', 'Geo_Rock-Resting_Grounds_Catacombs_Left_Dupe': 'RestingGrounds_10', 'Geo_Rock-Resting_Grounds_Catacombs_Left': 'RestingGrounds_10', 'Geo_Rock-Overgrown_Mound': 'Room_Fungus_Shaman', 'Geo_Rock-Fluke_Hermit_Dupe': 'Room_GG_Shortcut', 'Geo_Rock-Fluke_Hermit': 'Room_GG_Shortcut', 'Geo_Rock-Pleasure_House': 'Ruins_Elevator', 'Geo_Rock-City_of_Tears_Quirrel': 'Ruins1_03', 'Geo_Rock-City_of_Tears_Lemm': 'Ruins1_05b', 'Geo_Rock-City_of_Tears_Above_Lemm': 'Ruins1_05c', 'Geo_Rock-Soul_Sanctum': 'Ruins1_32', "Geo_Rock-Watcher's_Spire": 'Ruins2_01', "Geo_Rock-Above_King's_Station": 'Ruins2_05', "Geo_Rock-King's_Station": 'Ruins2_06', "Geo_Rock-King's_Pass_Left": 'Tutorial_01', "Geo_Rock-King's_Pass_Below_Fury": 'Tutorial_01', "Geo_Rock-King's_Pass_Hidden": 'Tutorial_01', "Geo_Rock-King's_Pass_Collapse": 'Tutorial_01', "Geo_Rock-King's_Pass_Above_Fury": 'Tutorial_01', 'Geo_Rock-Waterways_Tuk': 'Waterways_01', 'Geo_Rock-Waterways_Tuk_Alcove': 'Waterways_01', 'Geo_Rock-Waterways_Left': 'Waterways_04b', 'Geo_Rock-Waterways_East': 'Waterways_07', 'Geo_Rock-Waterways_Flukemarm': 'Waterways_08', 'Boss_Geo-Massive_Moss_Charger': 'Fungus1_29', 'Boss_Geo-Gorgeous_Husk': 'Ruins_House_02', 'Boss_Geo-Sanctum_Soul_Warrior': 'Ruins1_23', 'Boss_Geo-Elegant_Soul_Warrior': 'Ruins1_31b', 'Boss_Geo-Crystal_Guardian': 'Mines_18', 'Boss_Geo-Enraged_Guardian': 'Mines_32', 'Boss_Geo-Gruz_Mother': 'Crossroads_04', 'Boss_Geo-Vengefly_King': 'Fungus1_20_v02', 'Soul_Totem-Basin': 'Abyss_04', 'Soul_Totem-Cliffs_Main': 'Cliffs_01', 'Soul_Totem-Cliffs_Gorb': 'Cliffs_02', "Soul_Totem-Cliffs_Joni's": 'Cliffs_04', 'Soul_Totem-Crossroads_Goam_Journal': 'Crossroads_18', 'Soul_Totem-Crossroads_Shops': 'Crossroads_19', 'Soul_Totem-Crossroads_Mawlek_Upper': 'Crossroads_25', 'Soul_Totem-Crossroads_Acid': 'Crossroads_35', 'Soul_Totem-Crossroads_Mawlek_Lower': 'Crossroads_36', 'Soul_Totem-Crossroads_Myla': 'Crossroads_45', 'Soul_Totem-Ancestral_Mound': 'Crossroads_ShamanTemple', 'Soul_Totem-Distant_Village': 'Deepnest_10', 'Soul_Totem-Deepnest_Vessel': 'Deepnest_38', 'Soul_Totem-Mask_Maker': 'Deepnest_42', "Soul_Totem-Lower_Kingdom's_Edge_1": 'Deepnest_East_01', "Soul_Totem-Lower_Kingdom's_Edge_2": 'Deepnest_East_02', "Soul_Totem-Upper_Kingdom's_Edge": 'Deepnest_East_07', "Soul_Totem-Kingdom's_Edge_Camp": 'Deepnest_East_11', 'Soul_Totem-Oro_Dive_2': 'Deepnest_East_14', 'Soul_Totem-Oro_Dive_1': 'Deepnest_East_14', 'Soul_Totem-Oro': 'Deepnest_East_16', 'Soul_Totem-420_Geo_Rock': 'Deepnest_East_17', "Soul_Totem-Beast's_Den": 'Deepnest_Spider_Town', "Soul_Totem-Greenpath_Hunter's_Journal": 'Fungus1_07', 'Soul_Totem-Greenpath_MMC': 'Fungus1_29', 'Soul_Totem-Greenpath_Below_Toll': 'Fungus1_30', "Soul_Totem-Before_Pilgrim's_Way": 'Fungus2_10', "Soul_Totem-Pilgrim's_Way": 'Fungus2_21', 'Soul_Totem-Fungal_Core': 'Fungus2_29', "Soul_Totem-Top_Left_Queen's_Gardens": 'Fungus3_21', 'Soul_Totem-Below_Marmu': 'Fungus3_40', 'Soul_Totem-Upper_Crystal_Peak': 'Mines_20', 'Soul_Totem-Hallownest_Crown': 'Mines_25', 'Soul_Totem-Outside_Crystallized_Mound': 'Mines_28', 'Soul_Totem-Crystal_Heart_1': 'Mines_31', 'Soul_Totem-Crystal_Heart_2': 'Mines_31', 'Soul_Totem-Crystallized_Mound': 'Mines_35', 'Soul_Totem-Resting_Grounds': 'RestingGrounds_05', 'Soul_Totem-Below_Xero': 'RestingGrounds_06', 'Soul_Totem-Sanctum_Below_Soul_Master': 'Ruins1_24', 'Soul_Totem-Sanctum_Below_Chest': 'Ruins1_32', 'Soul_Totem-Sanctum_Above_Grub': 'Ruins1_32', 'Soul_Totem-Waterways_Entrance': 'Waterways_01', 'Soul_Totem-Top_Left_Waterways': 'Waterways_04b', 'Soul_Totem-Waterways_East': 'Waterways_07', 'Soul_Totem-Waterways_Flukemarm': 'Waterways_08', 'Soul_Totem-White_Palace_Entrance': 'White_Palace_02', 'Soul_Totem-White_Palace_Hub': 'White_Palace_03_hub', 'Soul_Totem-White_Palace_Left': 'White_Palace_04', 'Soul_Totem-White_Palace_Final': 'White_Palace_09', 'Soul_Totem-White_Palace_Right': 'White_Palace_15', 'Soul_Totem-Path_of_Pain_Below_Lever': 'White_Palace_17', 'Soul_Totem-Path_of_Pain_Left_of_Lever': 'White_Palace_17', 'Soul_Totem-Path_of_Pain_Entrance': 'White_Palace_18', 'Soul_Totem-Path_of_Pain_Second': 'White_Palace_18', 'Soul_Totem-Path_of_Pain_Hidden': 'White_Palace_19', 'Soul_Totem-Path_of_Pain_Below_Thornskip': 'White_Palace_19', 'Soul_Totem-Path_of_Pain_Final': 'White_Palace_20', 'Soul_Totem-Pale_Lurker': 'GG_Lurker', 'Lore_Tablet-City_Entrance': 'Ruins1_02', 'Lore_Tablet-Pleasure_House': 'Ruins_Elevator', 'Lore_Tablet-Sanctum_Entrance': 'Ruins1_23', 'Lore_Tablet-Sanctum_Past_Soul_Master': 'Ruins1_32', "Lore_Tablet-Watcher's_Spire": 'Ruins2_Watcher_Room', 'Lore_Tablet-Archives_Upper': 'Fungus3_archive_02', 'Lore_Tablet-Archives_Left': 'Fungus3_archive_02', 'Lore_Tablet-Archives_Right': 'Fungus3_archive_02', "Lore_Tablet-Pilgrim's_Way_1": 'Crossroads_11_alt', "Lore_Tablet-Pilgrim's_Way_2": 'Fungus2_21', 'Lore_Tablet-Mantis_Outskirts': 'Fungus2_12', 'Lore_Tablet-Mantis_Village': 'Fungus2_14', 'Lore_Tablet-Greenpath_Upper_Hidden': 'Fungus1_17', 'Lore_Tablet-Greenpath_Below_Toll': 'Fungus1_30', 'Lore_Tablet-Greenpath_Lifeblood': 'Fungus1_32', 'Lore_Tablet-Greenpath_Stag': 'Fungus1_21', 'Lore_Tablet-Greenpath_QG': 'Fungus1_13', 'Lore_Tablet-Greenpath_Lower_Hidden': 'Fungus1_19', 'Lore_Tablet-Dung_Defender': 'Waterways_07', 'Lore_Tablet-Spore_Shroom': 'Fungus2_20', 'Lore_Tablet-Fungal_Wastes_Hidden': 'Fungus2_07', 'Lore_Tablet-Fungal_Wastes_Below_Shrumal_Ogres': 'Fungus2_04', 'Lore_Tablet-Fungal_Core': 'Fungus2_30', 'Lore_Tablet-Ancient_Basin': 'Abyss_06_Core', "Lore_Tablet-King's_Pass_Focus": 'Tutorial_01', "Lore_Tablet-King's_Pass_Fury": 'Tutorial_01', "Lore_Tablet-King's_Pass_Exit": 'Tutorial_01', 'Lore_Tablet-World_Sense': 'Room_temple', 'Lore_Tablet-Howling_Cliffs': 'Cliffs_01', "Lore_Tablet-Kingdom's_Edge": 'Deepnest_East_17', 'Lore_Tablet-Palace_Workshop': 'White_Palace_08', 'Lore_Tablet-Palace_Throne': 'White_Palace_09', 'Lore_Tablet-Path_of_Pain_Entrance': 'White_Palace_18'}
locations = ['Sly_1', 'Sly_2', 'Sly_3', 'Sly_4', 'Sly_5', 'Sly_6', 'Sly_7', 'Sly_8', 'Sly_9', 'Sly_10', 'Sly_11', 'Sly_12', 'Sly_13', 'Sly_14', 'Sly_15', 'Sly_16', 'Sly_(Key)_1', 'Sly_(Key)_2', 'Sly_(Key)_3', 'Sly_(Key)_4', 'Sly_(Key)_5', 'Sly_(Key)_6', 'Sly_(Key)_7', 'Sly_(Key)_8', 'Sly_(Key)_9', 'Sly_(Key)_10', 'Sly_(Key)_11', 'Sly_(Key)_12', 'Sly_(Key)_13', 'Sly_(Key)_14', 'Sly_(Key)_15', 'Sly_(Key)_16', 'Iselda_1', 'Iselda_2', 'Iselda_3', 'Iselda_4', 'Iselda_5', 'Iselda_6', 'Iselda_7', 'Iselda_8', 'Iselda_9', 'Iselda_10', 'Iselda_11', 'Iselda_12', 'Iselda_13', 'Iselda_14', 'Iselda_15', 'Iselda_16', 'Salubra_1', 'Salubra_2', 'Salubra_3', 'Salubra_4', 'Salubra_5', 'Salubra_6', 'Salubra_7', 'Salubra_8', 'Salubra_9', 'Salubra_10', 'Salubra_11', 'Salubra_12', 'Salubra_13', 'Salubra_14', 'Salubra_15', 'Salubra_16', 'Salubra_(Requires_Charms)_1', 'Salubra_(Requires_Charms)_2', 'Salubra_(Requires_Charms)_3', 'Salubra_(Requires_Charms)_4', 'Salubra_(Requires_Charms)_5', 'Salubra_(Requires_Charms)_6', 'Salubra_(Requires_Charms)_7', 'Salubra_(Requires_Charms)_8', 'Salubra_(Requires_Charms)_9', 'Salubra_(Requires_Charms)_10', 'Salubra_(Requires_Charms)_11', 'Salubra_(Requires_Charms)_12', 'Salubra_(Requires_Charms)_13', 'Salubra_(Requires_Charms)_14', 'Salubra_(Requires_Charms)_15', 'Salubra_(Requires_Charms)_16', 'Leg_Eater_1', 'Leg_Eater_2', 'Leg_Eater_3', 'Leg_Eater_4', 'Leg_Eater_5', 'Leg_Eater_6', 'Leg_Eater_7', 'Leg_Eater_8', 'Leg_Eater_9', 'Leg_Eater_10', 'Leg_Eater_11', 'Leg_Eater_12', 'Leg_Eater_13', 'Leg_Eater_14', 'Leg_Eater_15', 'Leg_Eater_16', 'Grubfather_1', 'Grubfather_2', 'Grubfather_3', 'Grubfather_4', 'Grubfather_5', 'Grubfather_6', 'Grubfather_7', 'Grubfather_8', 'Grubfather_9', 'Grubfather_10', 'Grubfather_11', 'Grubfather_12', 'Grubfather_13', 'Grubfather_14', 'Grubfather_15', 'Grubfather_16', 'Seer_1', 'Seer_2', 'Seer_3', 'Seer_4', 'Seer_5', 'Seer_6', 'Seer_7', 'Seer_8', 'Seer_9', 'Seer_10', 'Seer_11', 'Seer_12', 'Seer_13', 'Seer_14', 'Seer_15', 'Seer_16', 'Egg_Shop_1', 'Egg_Shop_2', 'Egg_Shop_3', 'Egg_Shop_4', 'Egg_Shop_5', 'Egg_Shop_6', 'Egg_Shop_7', 'Egg_Shop_8', 'Egg_Shop_9', 'Egg_Shop_10', 'Egg_Shop_11', 'Egg_Shop_12', 'Egg_Shop_13', 'Egg_Shop_14', 'Egg_Shop_15', 'Egg_Shop_16', 'Lurien', 'Monomon', 'Herrah', 'World_Sense', 'Mothwing_Cloak', 'Mantis_Claw', 'Crystal_Heart', 'Monarch_Wings', 'Shade_Cloak', "Isma's_Tear", 'Dream_Nail', 'Vengeful_Spirit', 'Shade_Soul', 'Desolate_Dive', 'Descending_Dark', 'Howling_Wraiths', 'Abyss_Shriek', 'Cyclone_Slash', 'Dash_Slash', 'Great_Slash', 'Focus', 'Baldur_Shell', 'Fury_of_the_Fallen', 'Lifeblood_Core', "Defender's_Crest", 'Flukenest', 'Thorns_of_Agony', 'Mark_of_Pride', 'Sharp_Shadow', 'Spore_Shroom', 'Soul_Catcher', 'Soul_Eater', 'Glowing_Womb', "Nailmaster's_Glory", "Joni's_Blessing", 'Shape_of_Unn', 'Hiveblood', 'Dashmaster', 'Quick_Slash', 'Spell_Twister', 'Deep_Focus', 'Queen_Fragment', 'King_Fragment', 'Void_Heart', 'Dreamshield', 'Weaversong', 'Grimmchild', 'Unbreakable_Heart', 'Unbreakable_Greed', 'Unbreakable_Strength', 'City_Crest', 'Tram_Pass', 'Simple_Key-Basin', 'Simple_Key-City', 'Simple_Key-Lurker', "Shopkeeper's_Key", 'Love_Key', "King's_Brand", 'Godtuner', "Collector's_Map", 'Mask_Shard-Brooding_Mawlek', 'Mask_Shard-Crossroads_Goam', 'Mask_Shard-Stone_Sanctuary', "Mask_Shard-Queen's_Station", 'Mask_Shard-Deepnest', 'Mask_Shard-Waterways', 'Mask_Shard-Enraged_Guardian', 'Mask_Shard-Hive', 'Mask_Shard-Grey_Mourner', 'Mask_Shard-Bretta', 'Vessel_Fragment-Greenpath', 'Vessel_Fragment-City', 'Vessel_Fragment-Crossroads', 'Vessel_Fragment-Basin', 'Vessel_Fragment-Deepnest', 'Vessel_Fragment-Stag_Nest', 'Charm_Notch-Shrumal_Ogres', 'Charm_Notch-Fog_Canyon', 'Charm_Notch-Colosseum', 'Charm_Notch-Grimm', 'Pale_Ore-Basin', 'Pale_Ore-Crystal_Peak', 'Pale_Ore-Nosk', 'Pale_Ore-Colosseum', 'Geo_Chest-False_Knight', 'Geo_Chest-Soul_Master', 'Geo_Chest-Watcher_Knights', 'Geo_Chest-Greenpath', 'Geo_Chest-Mantis_Lords', 'Geo_Chest-Resting_Grounds', 'Geo_Chest-Crystal_Peak', 'Geo_Chest-Weavers_Den', 'Geo_Chest-Junk_Pit_1', 'Geo_Chest-Junk_Pit_2', 'Geo_Chest-Junk_Pit_3', 'Geo_Chest-Junk_Pit_5', 'Lumafly_Escape-Junk_Pit_Chest_4', 'Rancid_Egg-Sheo', 'Rancid_Egg-Fungal_Core', "Rancid_Egg-Queen's_Gardens", 'Rancid_Egg-Blue_Lake', 'Rancid_Egg-Crystal_Peak_Dive_Entrance', 'Rancid_Egg-Crystal_Peak_Dark_Room', 'Rancid_Egg-Crystal_Peak_Tall_Room', 'Rancid_Egg-City_of_Tears_Left', 'Rancid_Egg-City_of_Tears_Pleasure_House', "Rancid_Egg-Beast's_Den", 'Rancid_Egg-Dark_Deepnest', "Rancid_Egg-Weaver's_Den", 'Rancid_Egg-Near_Quick_Slash', "Rancid_Egg-Upper_Kingdom's_Edge", 'Rancid_Egg-Waterways_East', 'Rancid_Egg-Waterways_Main', 'Rancid_Egg-Waterways_West_Bluggsac', 'Rancid_Egg-Waterways_West_Pickup', "Rancid_Egg-Tuk_Defender's_Crest", "Wanderer's_Journal-Cliffs", "Wanderer's_Journal-Greenpath_Stag", "Wanderer's_Journal-Greenpath_Lower", "Wanderer's_Journal-Fungal_Wastes_Thorns_Gauntlet", "Wanderer's_Journal-Above_Mantis_Village", "Wanderer's_Journal-Crystal_Peak_Crawlers", "Wanderer's_Journal-Resting_Grounds_Catacombs", "Wanderer's_Journal-King's_Station", "Wanderer's_Journal-Pleasure_House", "Wanderer's_Journal-City_Storerooms", "Wanderer's_Journal-Ancient_Basin", "Wanderer's_Journal-Kingdom's_Edge_Entrance", "Wanderer's_Journal-Kingdom's_Edge_Camp", "Wanderer's_Journal-Kingdom's_Edge_Requires_Dive", 'Hallownest_Seal-Crossroads_Well', 'Hallownest_Seal-Greenpath', 'Hallownest_Seal-Fog_Canyon_West', 'Hallownest_Seal-Fog_Canyon_East', "Hallownest_Seal-Queen's_Station", 'Hallownest_Seal-Fungal_Wastes_Sporgs', 'Hallownest_Seal-Mantis_Lords', 'Hallownest_Seal-Resting_Grounds_Catacombs', "Hallownest_Seal-King's_Station", 'Hallownest_Seal-City_Rafters', 'Hallownest_Seal-Soul_Sanctum', 'Hallownest_Seal-Watcher_Knight', 'Hallownest_Seal-Deepnest_By_Mantis_Lords', "Hallownest_Seal-Beast's_Den", "Hallownest_Seal-Queen's_Gardens", "King's_Idol-Cliffs", "King's_Idol-Crystal_Peak", "King's_Idol-Glade_of_Hope", "King's_Idol-Dung_Defender", "King's_Idol-Great_Hopper", "King's_Idol-Pale_Lurker", "King's_Idol-Deepnest", 'Arcane_Egg-Lifeblood_Core', 'Arcane_Egg-Shade_Cloak', 'Arcane_Egg-Birthplace', 'Whispering_Root-Crossroads', 'Whispering_Root-Greenpath', 'Whispering_Root-Leg_Eater', 'Whispering_Root-Mantis_Village', 'Whispering_Root-Deepnest', 'Whispering_Root-Queens_Gardens', 'Whispering_Root-Kingdoms_Edge', 'Whispering_Root-Waterways', 'Whispering_Root-City', 'Whispering_Root-Resting_Grounds', 'Whispering_Root-Spirits_Glade', 'Whispering_Root-Crystal_Peak', 'Whispering_Root-Howling_Cliffs', 'Whispering_Root-Ancestral_Mound', 'Whispering_Root-Hive', 'Boss_Essence-Elder_Hu', 'Boss_Essence-Xero', 'Boss_Essence-Gorb', 'Boss_Essence-Marmu', 'Boss_Essence-No_Eyes', 'Boss_Essence-Galien', 'Boss_Essence-Markoth', 'Boss_Essence-Failed_Champion', 'Boss_Essence-Soul_Tyrant', 'Boss_Essence-Lost_Kin', 'Boss_Essence-White_Defender', 'Boss_Essence-Grey_Prince_Zote', 'Grub-Crossroads_Acid', 'Grub-Crossroads_Center', 'Grub-Crossroads_Stag', 'Grub-Crossroads_Spike', 'Grub-Crossroads_Guarded', 'Grub-Greenpath_Cornifer', 'Grub-Greenpath_Journal', 'Grub-Greenpath_MMC', 'Grub-Greenpath_Stag', 'Grub-Fog_Canyon', 'Grub-Fungal_Bouncy', 'Grub-Fungal_Spore_Shroom', 'Grub-Deepnest_Mimic', 'Grub-Deepnest_Nosk', 'Grub-Deepnest_Spike', 'Grub-Dark_Deepnest', "Grub-Beast's_Den", "Grub-Kingdom's_Edge_Oro", "Grub-Kingdom's_Edge_Camp", 'Grub-Hive_External', 'Grub-Hive_Internal', 'Grub-Basin_Requires_Wings', 'Grub-Basin_Requires_Dive', 'Grub-Waterways_Main', "Grub-Isma's_Grove", 'Grub-Waterways_Requires_Tram', 'Grub-City_of_Tears_Left', 'Grub-Soul_Sanctum', "Grub-Watcher's_Spire", 'Grub-City_of_Tears_Guarded', "Grub-King's_Station", 'Grub-Resting_Grounds', 'Grub-Crystal_Peak_Below_Chest', 'Grub-Crystallized_Mound', 'Grub-Crystal_Peak_Spike', 'Grub-Crystal_Peak_Mimic', 'Grub-Crystal_Peak_Crushers', 'Grub-Crystal_Heart', 'Grub-Hallownest_Crown', 'Grub-Howling_Cliffs', "Grub-Queen's_Gardens_Stag", "Grub-Queen's_Gardens_Marmu", "Grub-Queen's_Gardens_Top", 'Grub-Collector_1', 'Grub-Collector_2', 'Grub-Collector_3', 'Mimic_Grub-Deepnest_1', 'Mimic_Grub-Deepnest_2', 'Mimic_Grub-Deepnest_3', 'Mimic_Grub-Crystal_Peak', 'Crossroads_Map', 'Greenpath_Map', 'Fog_Canyon_Map', 'Fungal_Wastes_Map', 'Deepnest_Map-Upper', 'Deepnest_Map-Right', 'Ancient_Basin_Map', "Kingdom's_Edge_Map", 'City_of_Tears_Map', 'Royal_Waterways_Map', 'Howling_Cliffs_Map', 'Crystal_Peak_Map', "Queen's_Gardens_Map", 'Resting_Grounds_Map', 'Dirtmouth_Stag', 'Crossroads_Stag', 'Greenpath_Stag', "Queen's_Station_Stag", "Queen's_Gardens_Stag", 'City_Storerooms_Stag', "King's_Station_Stag", 'Resting_Grounds_Stag', 'Distant_Village_Stag', 'Hidden_Station_Stag', 'Stag_Nest_Stag', "Lifeblood_Cocoon-King's_Pass", 'Lifeblood_Cocoon-Ancestral_Mound', 'Lifeblood_Cocoon-Greenpath', 'Lifeblood_Cocoon-Fog_Canyon_West', 'Lifeblood_Cocoon-Mantis_Village', 'Lifeblood_Cocoon-Failed_Tramway', 'Lifeblood_Cocoon-Galien', "Lifeblood_Cocoon-Kingdom's_Edge", 'Grimmkin_Flame-City_Storerooms', 'Grimmkin_Flame-Greenpath', 'Grimmkin_Flame-Crystal_Peak', "Grimmkin_Flame-King's_Pass", 'Grimmkin_Flame-Resting_Grounds', "Grimmkin_Flame-Kingdom's_Edge", 'Grimmkin_Flame-Fungal_Core', 'Grimmkin_Flame-Ancient_Basin', 'Grimmkin_Flame-Hive', 'Grimmkin_Flame-Brumm', "Hunter's_Journal", 'Journal_Entry-Void_Tendrils', 'Journal_Entry-Charged_Lumafly', 'Journal_Entry-Goam', 'Journal_Entry-Garpede', 'Journal_Entry-Seal_of_Binding', 'Elevator_Pass', 'Split_Mothwing_Cloak', 'Left_Mantis_Claw', 'Right_Mantis_Claw', 'Leftslash', 'Rightslash', 'Upslash', 'Split_Crystal_Heart', 'Geo_Rock-Broken_Elevator_1', 'Geo_Rock-Broken_Elevator_2', 'Geo_Rock-Broken_Elevator_3', 'Geo_Rock-Broken_Bridge_Upper', 'Geo_Rock-Broken_Bridge_Lower', 'Geo_Rock-Broken_Bridge_Lower_Dupe', 'Geo_Rock-Abyss_1', 'Geo_Rock-Abyss_2', 'Geo_Rock-Abyss_3', 'Geo_Rock-Basin_Tunnel', 'Geo_Rock-Basin_Grub', 'Geo_Rock-Basin_Before_Broken_Vessel', 'Geo_Rock-Cliffs_Main_1', 'Geo_Rock-Cliffs_Main_2', 'Geo_Rock-Cliffs_Main_3', 'Geo_Rock-Cliffs_Main_4', 'Geo_Rock-Below_Gorb_Dupe', 'Geo_Rock-Below_Gorb', 'Geo_Rock-Crossroads_Well', 'Geo_Rock-Crossroads_Center_Grub', 'Geo_Rock-Crossroads_Root', 'Geo_Rock-Crossroads_Root_Dupe_1', 'Geo_Rock-Crossroads_Root_Dupe_2', 'Geo_Rock-Crossroads_Aspid_Arena', 'Geo_Rock-Crossroads_Aspid_Arena_Dupe_1', 'Geo_Rock-Crossroads_Aspid_Arena_Dupe_2', 'Geo_Rock-Crossroads_Aspid_Arena_Hidden', 'Geo_Rock-Crossroads_Above_False_Knight', 'Geo_Rock-Crossroads_Before_Acid_Grub', 'Geo_Rock-Crossroads_Below_Goam_Mask_Shard', 'Geo_Rock-Crossroads_After_Goam_Mask_Shard', 'Geo_Rock-Crossroads_Above_Lever', 'Geo_Rock-Crossroads_Before_Fungal', 'Geo_Rock-Crossroads_Before_Fungal_Dupe_1', 'Geo_Rock-Crossroads_Before_Fungal_Dupe_2', 'Geo_Rock-Crossroads_Before_Shops', 'Geo_Rock-Crossroads_Before_Glowing_Womb', 'Geo_Rock-Crossroads_Above_Tram', 'Geo_Rock-Crossroads_Above_Mawlek', 'Geo_Rock-Crossroads_Vessel_Fragment', 'Geo_Rock-Crossroads_Goam_Alcove', 'Geo_Rock-Crossroads_Goam_Damage_Boost', 'Geo_Rock-Crossroads_Tram', 'Geo_Rock-Crossroads_Goam_Journal', 'Geo_Rock-Crossroads_Goam_Journal_Dupe', 'Geo_Rock-Ancestral_Mound', 'Geo_Rock-Ancestral_Mound_Dupe', 'Geo_Rock-Ancestral_Mound_Tree', 'Geo_Rock-Ancestral_Mound_Tree_Dupe', 'Geo_Rock-Moss_Prophet', 'Geo_Rock-Moss_Prophet_Dupe', 'Geo_Rock-Deepnest_Below_Mimics', 'Geo_Rock-Deepnest_Below_Mimics_Dupe', 'Geo_Rock-Deepnest_Below_Spike_Grub', 'Geo_Rock-Deepnest_Below_Spike_Grub_Dupe', 'Geo_Rock-Deepnest_Spike_Grub_Right', 'Geo_Rock-Deepnest_By_Mantis_Lords_Garpede_Pogo', 'Geo_Rock-Deepnest_By_Mantis_Lords_Garpede_Pogo_Dupe', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_1', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_2', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_3', 'Geo_Rock-Deepnest_Nosk_1', 'Geo_Rock-Deepnest_Nosk_2', 'Geo_Rock-Deepnest_Nosk_3', 'Geo_Rock-Deepnest_Above_Galien', 'Geo_Rock-Deepnest_Galien_Spike', 'Geo_Rock-Deepnest_Garpede_1', 'Geo_Rock-Deepnest_Garpede_2', 'Geo_Rock-Dark_Deepnest_Above_Grub_1', 'Geo_Rock-Dark_Deepnest_Above_Grub_2', 'Geo_Rock-Dark_Deepnest_Bottom_Left', 'Geo_Rock-Above_Mask_Maker_1', 'Geo_Rock-Above_Mask_Maker_2', "Geo_Rock-Lower_Kingdom's_Edge_1", "Geo_Rock-Lower_Kingdom's_Edge_2", "Geo_Rock-Lower_Kingdom's_Edge_3", "Geo_Rock-Lower_Kingdom's_Edge_Dive", "Geo_Rock-Kingdom's_Edge_Below_Bardoon", "Geo_Rock-Kingdom's_Edge_Oro_Far_Left", "Geo_Rock-Kingdom's_Edge_Oro_Middle_Left", "Geo_Rock-Kingdom's_Edge_Above_Root", "Geo_Rock-Kingdom's_Edge_Above_Tower", "Geo_Rock-Kingdom's_Edge_Below_Colosseum", "Geo_Rock-Kingdom's_Edge_Above_420_Geo_Rock", "Geo_Rock-Kingdom's_Edge_420_Geo_Rock", "Geo_Rock-Beast's_Den_Above_Trilobite", "Geo_Rock-Beast's_Den_Above_Trilobite_Dupe", "Geo_Rock-Beast's_Den_Below_Herrah", "Geo_Rock-Beast's_Den_Below_Egg", "Geo_Rock-Beast's_Den_Below_Egg_Dupe", "Geo_Rock-Beast's_Den_Bottom", "Geo_Rock-Beast's_Den_Bottom_Dupe", "Geo_Rock-Beast's_Den_After_Herrah", 'Geo_Rock-Greenpath_Entrance', 'Geo_Rock-Greenpath_Waterfall', 'Geo_Rock-Greenpath_Below_Skip_Squit', 'Geo_Rock-Greenpath_Skip_Squit', 'Geo_Rock-Greenpath_Second_Skip_Fool_Eater', 'Geo_Rock-Greenpath_Second_Skip_Fool_Eater_Dupe', 'Geo_Rock-Greenpath_Second_Skip_Lower', 'Geo_Rock-Greenpath_Below_Hornet', 'Geo_Rock-Greenpath_Above_Thorns', "Geo_Rock-Greenpath_Hunter's_Journal", 'Geo_Rock-Greenpath_Acid_Bridge', 'Geo_Rock-Greenpath_After_MMC_Hidden', 'Geo_Rock-Greenpath_After_MMC', 'Geo_Rock-Greenpath_After_MMC_Dupe', 'Geo_Rock-Greenpath_Obbles_Fool_Eater', 'Geo_Rock-Greenpath_Moss_Knights', 'Geo_Rock-Greenpath_Moss_Knights_Dupe_1', 'Geo_Rock-Greenpath_Moss_Knights_Dupe_2', 'Geo_Rock-Greenpath_Below_Stag', 'Geo_Rock-Greenpath_Below_Stag_Fool_Eater', 'Geo_Rock-Baldur_Shell_Top_Left', 'Geo_Rock-Baldur_Shell_Alcove', 'Geo_Rock-Greenpath_MMC', 'Geo_Rock-Greenpath_Below_Toll', 'Geo_Rock-Greenpath_Toll_Hidden', 'Geo_Rock-Greenpath_Toll_Hidden_Dupe', 'Geo_Rock-Fungal_Below_Shrumal_Ogres', 'Geo_Rock-Fungal_Above_Cloth', 'Geo_Rock-Fungal_After_Cloth', "Geo_Rock-Fungal_Below_Pilgrim's_Way", "Geo_Rock-Fungal_Below_Pilgrim's_Way_Dupe", 'Geo_Rock-Mantis_Outskirts_Guarded', 'Geo_Rock-Mantis_Outskirts_Guarded_Dupe', 'Geo_Rock-Mantis_Outskirts_Alcove', 'Geo_Rock-Mantis_Village_After_Lever', 'Geo_Rock-Mantis_Village_Above_Claw', 'Geo_Rock-Mantis_Village_Above_Claw_Dupe', 'Geo_Rock-Mantis_Village_Below_Lore', 'Geo_Rock-Mantis_Village_Above_Lever', 'Geo_Rock-Above_Mantis_Lords_1', 'Geo_Rock-Above_Mantis_Lords_2', 'Geo_Rock-Fungal_After_Bouncy_Grub', 'Geo_Rock-Fungal_After_Bouncy_Grub_Dupe', 'Geo_Rock-Fungal_Bouncy_Grub_Lever', 'Geo_Rock-Fungal_After_Cornifer', 'Geo_Rock-Fungal_Above_City_Entrance', 'Geo_Rock-Deepnest_By_Mantis_Lords_1', 'Geo_Rock-Deepnest_By_Mantis_Lords_2', 'Geo_Rock-Deepnest_Lower_Cornifer', 'Geo_Rock-Fungal_Core_Entrance', 'Geo_Rock-Fungal_Core_Hidden', 'Geo_Rock-Fungal_Core_Above_Elder', "Geo_Rock-Queen's_Gardens_Acid_Entrance", "Geo_Rock-Queen's_Gardens_Below_Stag", 'Geo_Rock-Fog_Canyon_East', 'Geo_Rock-Love_Key', 'Geo_Rock-Love_Key_Dupe', "Geo_Rock-Queen's_Gardens_Above_Marmu", 'Geo_Rock-Pale_Lurker', 'Geo_Rock-Godhome_Pipeway', 'Geo_Rock-Hive_Entrance', 'Geo_Rock-Hive_Outside_Bench', 'Geo_Rock-Hive_Below_Root', 'Geo_Rock-Hive_After_Root', 'Geo_Rock-Hive_Below_Stash', 'Geo_Rock-Hive_Stash', 'Geo_Rock-Hive_Stash_Dupe', 'Geo_Rock-Hive_Below_Grub', 'Geo_Rock-Hive_Above_Mask', 'Geo_Rock-Crystal_Peak_Lower_Middle', 'Geo_Rock-Crystal_Peak_Lower_Conveyer_1', 'Geo_Rock-Crystal_Peak_Lower_Conveyer_2', 'Geo_Rock-Crystal_Peak_Before_Dark_Room', 'Geo_Rock-Crystal_Peak_Before_Dark_Room_Dupe', 'Geo_Rock-Crystal_Peak_Above_Spike_Grub', 'Geo_Rock-Crystal_Peak_Mimic_Grub', 'Geo_Rock-Crystal_Peak_Dive_Egg', 'Geo_Rock-Crystal_Peak_Dive_Egg_Dupe', 'Geo_Rock-Crystal_Peak_Conga_Line', 'Geo_Rock-Hallownest_Crown_Dive', 'Geo_Rock-Hallownest_Crown_Dive_Dupe', 'Geo_Rock-Hallownest_Crown_Hidden', 'Geo_Rock-Hallownest_Crown_Hidden_Dupe_1', 'Geo_Rock-Hallownest_Crown_Hidden_Dupe_2', 'Geo_Rock-Crystal_Peak_Before_Crystal_Heart', 'Geo_Rock-Crystal_Peak_Entrance', 'Geo_Rock-Crystal_Peak_Entrance_Dupe_1', 'Geo_Rock-Crystal_Peak_Entrance_Dupe_2', 'Geo_Rock-Crystal_Peak_Above_Crushers_Lower', 'Geo_Rock-Crystal_Peak_Above_Crushers_Higher', 'Geo_Rock-Resting_Grounds_Catacombs_Grub', 'Geo_Rock-Resting_Grounds_Catacombs_Left_Dupe', 'Geo_Rock-Resting_Grounds_Catacombs_Left', 'Geo_Rock-Overgrown_Mound', 'Geo_Rock-Fluke_Hermit_Dupe', 'Geo_Rock-Fluke_Hermit', 'Geo_Rock-Pleasure_House', 'Geo_Rock-City_of_Tears_Quirrel', 'Geo_Rock-City_of_Tears_Lemm', 'Geo_Rock-City_of_Tears_Above_Lemm', 'Geo_Rock-Soul_Sanctum', "Geo_Rock-Watcher's_Spire", "Geo_Rock-Above_King's_Station", "Geo_Rock-King's_Station", "Geo_Rock-King's_Pass_Left", "Geo_Rock-King's_Pass_Below_Fury", "Geo_Rock-King's_Pass_Hidden", "Geo_Rock-King's_Pass_Collapse", "Geo_Rock-King's_Pass_Above_Fury", 'Geo_Rock-Waterways_Tuk', 'Geo_Rock-Waterways_Tuk_Alcove', 'Geo_Rock-Waterways_Left', 'Geo_Rock-Waterways_East', 'Geo_Rock-Waterways_Flukemarm', 'Boss_Geo-Massive_Moss_Charger', 'Boss_Geo-Gorgeous_Husk', 'Boss_Geo-Sanctum_Soul_Warrior', 'Boss_Geo-Elegant_Soul_Warrior', 'Boss_Geo-Crystal_Guardian', 'Boss_Geo-Enraged_Guardian', 'Boss_Geo-Gruz_Mother', 'Boss_Geo-Vengefly_King', 'Soul_Totem-Basin', 'Soul_Totem-Cliffs_Main', 'Soul_Totem-Cliffs_Gorb', "Soul_Totem-Cliffs_Joni's", 'Soul_Totem-Crossroads_Goam_Journal', 'Soul_Totem-Crossroads_Shops', 'Soul_Totem-Crossroads_Mawlek_Upper', 'Soul_Totem-Crossroads_Acid', 'Soul_Totem-Crossroads_Mawlek_Lower', 'Soul_Totem-Crossroads_Myla', 'Soul_Totem-Ancestral_Mound', 'Soul_Totem-Distant_Village', 'Soul_Totem-Deepnest_Vessel', 'Soul_Totem-Mask_Maker', "Soul_Totem-Lower_Kingdom's_Edge_1", "Soul_Totem-Lower_Kingdom's_Edge_2", "Soul_Totem-Upper_Kingdom's_Edge", "Soul_Totem-Kingdom's_Edge_Camp", 'Soul_Totem-Oro_Dive_2', 'Soul_Totem-Oro_Dive_1', 'Soul_Totem-Oro', 'Soul_Totem-420_Geo_Rock', "Soul_Totem-Beast's_Den", "Soul_Totem-Greenpath_Hunter's_Journal", 'Soul_Totem-Greenpath_MMC', 'Soul_Totem-Greenpath_Below_Toll', "Soul_Totem-Before_Pilgrim's_Way", "Soul_Totem-Pilgrim's_Way", 'Soul_Totem-Fungal_Core', "Soul_Totem-Top_Left_Queen's_Gardens", 'Soul_Totem-Below_Marmu', 'Soul_Totem-Upper_Crystal_Peak', 'Soul_Totem-Hallownest_Crown', 'Soul_Totem-Outside_Crystallized_Mound', 'Soul_Totem-Crystal_Heart_1', 'Soul_Totem-Crystal_Heart_2', 'Soul_Totem-Crystallized_Mound', 'Soul_Totem-Resting_Grounds', 'Soul_Totem-Below_Xero', 'Soul_Totem-Sanctum_Below_Soul_Master', 'Soul_Totem-Sanctum_Below_Chest', 'Soul_Totem-Sanctum_Above_Grub', 'Soul_Totem-Waterways_Entrance', 'Soul_Totem-Top_Left_Waterways', 'Soul_Totem-Waterways_East', 'Soul_Totem-Waterways_Flukemarm', 'Soul_Totem-White_Palace_Entrance', 'Soul_Totem-White_Palace_Hub', 'Soul_Totem-White_Palace_Left', 'Soul_Totem-White_Palace_Final', 'Soul_Totem-White_Palace_Right', 'Soul_Totem-Path_of_Pain_Below_Lever', 'Soul_Totem-Path_of_Pain_Left_of_Lever', 'Soul_Totem-Path_of_Pain_Entrance', 'Soul_Totem-Path_of_Pain_Second', 'Soul_Totem-Path_of_Pain_Hidden', 'Soul_Totem-Path_of_Pain_Below_Thornskip', 'Soul_Totem-Path_of_Pain_Final', 'Soul_Totem-Pale_Lurker', 'Lore_Tablet-City_Entrance', 'Lore_Tablet-Pleasure_House', 'Lore_Tablet-Sanctum_Entrance', 'Lore_Tablet-Sanctum_Past_Soul_Master', "Lore_Tablet-Watcher's_Spire", 'Lore_Tablet-Archives_Upper', 'Lore_Tablet-Archives_Left', 'Lore_Tablet-Archives_Right', "Lore_Tablet-Pilgrim's_Way_1", "Lore_Tablet-Pilgrim's_Way_2", 'Lore_Tablet-Mantis_Outskirts', 'Lore_Tablet-Mantis_Village', 'Lore_Tablet-Greenpath_Upper_Hidden', 'Lore_Tablet-Greenpath_Below_Toll', 'Lore_Tablet-Greenpath_Lifeblood', 'Lore_Tablet-Greenpath_Stag', 'Lore_Tablet-Greenpath_QG', 'Lore_Tablet-Greenpath_Lower_Hidden', 'Lore_Tablet-Dung_Defender', 'Lore_Tablet-Spore_Shroom', 'Lore_Tablet-Fungal_Wastes_Hidden', 'Lore_Tablet-Fungal_Wastes_Below_Shrumal_Ogres', 'Lore_Tablet-Fungal_Core', 'Lore_Tablet-Ancient_Basin', "Lore_Tablet-King's_Pass_Focus", "Lore_Tablet-King's_Pass_Fury", "Lore_Tablet-King's_Pass_Exit", 'Lore_Tablet-World_Sense', 'Lore_Tablet-Howling_Cliffs', "Lore_Tablet-Kingdom's_Edge", 'Lore_Tablet-Palace_Workshop', 'Lore_Tablet-Palace_Throne', 'Lore_Tablet-Path_of_Pain_Entrance']
diff --git a/worlds/hk/GodhomeData.py b/worlds/hk/GodhomeData.py
new file mode 100644
index 000000000000..a2dd69ed73ef
--- /dev/null
+++ b/worlds/hk/GodhomeData.py
@@ -0,0 +1,55 @@
+from functools import partial
+
+
+godhome_event_names = ["Godhome_Flower_Quest", "Defeated_Pantheon_5", "GG_Atrium_Roof", "Defeated_Pantheon_1", "Defeated_Pantheon_2", "Defeated_Pantheon_3", "Opened_Pantheon_4", "Defeated_Pantheon_4", "GG_Atrium", "Hit_Pantheon_5_Unlock_Orb", "GG_Workshop", "Can_Damage_Crystal_Guardian", 'Defeated_Any_Soul_Warrior', "Defeated_Colosseum_3", "COMBAT[Radiance]", "COMBAT[Pantheon_1]", "COMBAT[Pantheon_2]", "COMBAT[Pantheon_3]", "COMBAT[Pantheon_4]", "COMBAT[Pantheon_5]", "COMBAT[Colosseum_3]", 'Warp-Junk_Pit_to_Godhome', 'Bench-Godhome_Atrium', 'Bench-Hall_of_Gods', "GODTUNERUNLOCK", "GG_Waterways", "Warp-Godhome_to_Junk_Pit", "NAILCOMBAT", "BOSS", "AERIALMINIBOSS"]
+
+
+def set_godhome_rules(hk_world, hk_set_rule):
+ player = hk_world.player
+ fn = partial(hk_set_rule, hk_world)
+
+ required_events = {
+ "Godhome_Flower_Quest": lambda state: state.count('Defeated_Pantheon_5', player) and state.count('Room_Mansion[left1]', player) and state.count('Fungus3_49[right1]', player) and state.has('Godtuner', player),
+
+ "Defeated_Pantheon_5": lambda state: state.has('GG_Atrium_Roof', player) and state.has('WINGS', player) and (state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player)) and ((state.has('Defeated_Pantheon_1', player) and state.has('Defeated_Pantheon_2', player) and state.has('Defeated_Pantheon_3', player) and state.has('Defeated_Pantheon_4', player) and state.has('COMBAT[Radiance]', player))),
+ "GG_Atrium_Roof": lambda state: state.has('GG_Atrium', player) and state.has('Hit_Pantheon_5_Unlock_Orb', player) and state.has('LEFTCLAW', player),
+
+ "Defeated_Pantheon_1": lambda state: state.has('GG_Atrium', player) and ((state.has('Defeated_Gruz_Mother', player) and state.has('Defeated_False_Knight', player) and (state.has('Fungus1_29[left1]', player) or state.has('Fungus1_29[right1]', player)) and state.has('Defeated_Hornet_1', player) and state.has('Defeated_Gorb', player) and state.has('Defeated_Dung_Defender', player) and state.has('Defeated_Any_Soul_Warrior', player) and state.has('Defeated_Brooding_Mawlek', player))),
+ "Defeated_Pantheon_2": lambda state: state.has('GG_Atrium', player) and ((state.has('Defeated_Xero', player) and state.has('Defeated_Crystal_Guardian', player) and state.has('Defeated_Soul_Master', player) and state.has('Defeated_Colosseum_2', player) and state.has('Defeated_Mantis_Lords', player) and state.has('Defeated_Marmu', player) and state.has('Defeated_Nosk', player) and state.has('Defeated_Flukemarm', player) and state.has('Defeated_Broken_Vessel', player))),
+ "Defeated_Pantheon_3": lambda state: state.has('GG_Atrium', player) and ((state.has('Defeated_Hive_Knight', player) and state.has('Defeated_Elder_Hu', player) and state.has('Defeated_Collector', player) and state.has('Defeated_Colosseum_2', player) and state.has('Defeated_Grimm', player) and state.has('Defeated_Galien', player) and state.has('Defeated_Uumuu', player) and state.has('Defeated_Hornet_2', player))),
+ "Opened_Pantheon_4": lambda state: state.has('GG_Atrium', player) and (state.has('Defeated_Pantheon_1', player) and state.has('Defeated_Pantheon_2', player) and state.has('Defeated_Pantheon_3', player)),
+ "Defeated_Pantheon_4": lambda state: state.has('GG_Atrium', player) and state.has('Opened_Pantheon_4', player) and ((state.has('Defeated_Enraged_Guardian', player) and state.has('Defeated_Broken_Vessel', player) and state.has('Defeated_No_Eyes', player) and state.has('Defeated_Traitor_Lord', player) and state.has('Defeated_Dung_Defender', player) and state.has('Defeated_False_Knight', player) and state.has('Defeated_Markoth', player) and state.has('Defeated_Watcher_Knights', player) and state.has('Defeated_Soul_Master', player))),
+ "GG_Atrium": lambda state: state.has('Warp-Junk_Pit_to_Godhome', player) and (state.has('RIGHTCLAW', player) or state.has('WINGS', player) or state.has('LEFTCLAW', player) and state.has('RIGHTSUPERDASH', player)) or state.has('GG_Workshop', player) and (state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player) and state.has('WINGS', player)) or state.has('Bench-Godhome_Atrium', player),
+ "Hit_Pantheon_5_Unlock_Orb": lambda state: state.has('GG_Atrium', player) and state.has('WINGS', player) and (state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player)) and (((state.has('Queen_Fragment', player) and state.has('King_Fragment', player) and state.has('Void_Heart', player)) and state.has('Defeated_Pantheon_1', player) and state.has('Defeated_Pantheon_2', player) and state.has('Defeated_Pantheon_3', player) and state.has('Defeated_Pantheon_4', player))),
+ "GG_Workshop": lambda state: state.has('GG_Atrium', player) or state.has('Bench-Hall_of_Gods', player),
+ "Can_Damage_Crystal_Guardian": lambda state: state.has('UPSLASH', player) or state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('CYCLONE', player) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat')) and (state.has('CYCLONE', player) or state.has('Great_Slash', player)) and (state.has('DREAMNAIL', player) and (state.has('SPELLS', player) or state.has('FOCUS', player) and state.has('Spore_Shroom', player) or state.has('Glowing_Womb', player)) or state.has('Weaversong', player)),
+ 'Defeated_Any_Soul_Warrior': lambda state: state.has('Defeated_Sanctum_Warrior', player) or state.has('Defeated_Elegant_Warrior', player) or state.has('Room_Colosseum_01[left1]', player) and state.has('Defeated_Colosseum_3', player),
+ "Defeated_Colosseum_3": lambda state: state.has('Room_Colosseum_01[left1]', player) and state.has('Can_Replenish_Geo', player) and ((state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player)) or ((state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat')) and state.has('WINGS', player))) and state.has('COMBAT[Colosseum_3]', player),
+
+ # MACROS
+ "COMBAT[Radiance]": lambda state: (state.has('LEFTDASH', player) and state.has('RIGHTDASH', player)) and ((((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('LEFTDASH', player)) and ((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('RIGHTDASH', player))) or state.has('QUAKE', player)) and (state.count('FIREBALL', player) > 1 and state.has('UPSLASH', player) or state.count('SCREAM', player) > 1 and state.has('UPSLASH', player) or state._hk_option(player, 'RemoveSpellUpgrades') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and state.has('UPSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and (state.has('UPSLASH', player) or (state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
+ "COMBAT[Pantheon_1]": lambda state: state.has('AERIALMINIBOSS', player) and state.count('SPELLS', player) > 1 and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
+ "COMBAT[Pantheon_2]": lambda state: state.has('AERIALMINIBOSS', player) and state.count('SPELLS', player) > 1 and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))) and state.has('Can_Damage_Crystal_Guardian', player),
+ "COMBAT[Pantheon_3]": lambda state: state.has('AERIALMINIBOSS', player) and state.count('SPELLS', player) > 1 and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
+ "COMBAT[Pantheon_4]": lambda state: state.has('AERIALMINIBOSS', player) and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))) and state.has('Can_Damage_Crystal_Guardian', player) and (state.has('LEFTDASH', player) and state.has('RIGHTDASH', player)) and ((((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('LEFTDASH', player)) and ((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('RIGHTDASH', player))) or state.has('QUAKE', player)) and (state.count('FIREBALL', player) > 1 and state.has('UPSLASH', player) or state.count('SCREAM', player) > 1 and state.has('UPSLASH', player) or state._hk_option(player, 'RemoveSpellUpgrades') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and state.has('UPSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and (state.has('UPSLASH', player) or (state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
+ "COMBAT[Pantheon_5]": lambda state: state.has('AERIALMINIBOSS', player) and state.has('FOCUS', player) and state.has('Can_Damage_Crystal_Guardian', player) and (state.has('LEFTDASH', player) and state.has('RIGHTDASH', player)) and ((((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('LEFTDASH', player)) and ((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('RIGHTDASH', player))) or state.has('QUAKE', player)) and (state.count('FIREBALL', player) > 1 and state.has('UPSLASH', player) or state.count('SCREAM', player) > 1 and state.has('UPSLASH', player) or state._hk_option(player, 'RemoveSpellUpgrades') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and state.has('UPSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and (state.has('UPSLASH', player) or (state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
+ "COMBAT[Colosseum_3]": lambda state: state.has('BOSS', player) and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
+
+ # MISC
+ 'Warp-Junk_Pit_to_Godhome': lambda state: state.has('GG_Waterways', player) and state.has('GODTUNERUNLOCK', player) and state.has('DREAMNAIL', player),
+ 'Bench-Godhome_Atrium': lambda state: state.has('GG_Atrium', player) and (state.has('RIGHTCLAW', player) and (state.has('RIGHTDASH', player) or state.has('LEFTCLAW', player) and state.has('RIGHTSUPERDASH', player) or state.has('WINGS', player)) or state.has('LEFTCLAW', player) and state.has('WINGS', player)),
+ 'Bench-Hall_of_Gods': lambda state: state.has('GG_Workshop', player) and ((state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player))),
+
+ "GODTUNERUNLOCK": lambda state: state.count('SIMPLE', player) > 3,
+ "GG_Waterways": lambda state: state.has('GG_Waterways[door1]', player) or state.has('GG_Waterways[right1]', player) and (state.has('LEFTSUPERDASH', player) or state.has('SWIM', player)) or state.has('Warp-Godhome_to_Junk_Pit', player),
+ "Warp-Godhome_to_Junk_Pit": lambda state: state.has('Warp-Junk_Pit_to_Godhome', player) or state.has('GG_Atrium', player),
+
+ # COMBAT MACROS
+ "NAILCOMBAT": lambda state: (state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('CYCLONE', player) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat')),
+ "BOSS": lambda state: state.count('SPELLS', player) > 1 and ((state.has('LEFTDASH', player) or state.has('RIGHTDASH', player)) and ((state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player)) or state._hk_option(player, 'ProficientCombat') and state.has('NAILCOMBAT', player)),
+ "AERIALMINIBOSS": lambda state: (state.has('FIREBALL', player) or state.has('SCREAM', player)) and (state.has('LEFTDASH', player) or state.has('RIGHTDASH', player)) and ((state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player)) or state._hk_option(player, 'ProficientCombat') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and ((state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat')) and ((state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player) or state.has('CYCLONE', player) or state.has('Great_Slash', player)),
+
+ }
+
+ for item, rule in required_events.items():
+ fn(item, rule)
diff --git a/worlds/hk/Items.py b/worlds/hk/Items.py
index a9acbf48f303..8515465826a5 100644
--- a/worlds/hk/Items.py
+++ b/worlds/hk/Items.py
@@ -1,5 +1,6 @@
from typing import Dict, Set, NamedTuple
from .ExtractedData import items, logic_items, item_effects
+from .GodhomeData import godhome_event_names
item_table = {}
@@ -14,23 +15,53 @@ class HKItemData(NamedTuple):
item_table[item_name] = HKItemData(advancement=item_name in logic_items or item_name in item_effects,
id=i, type=item_type)
+for item_name in godhome_event_names:
+ item_table[item_name] = HKItemData(advancement=True, id=None, type=None)
+
lookup_id_to_name: Dict[int, str] = {data.id: item_name for item_name, data in item_table.items()}
lookup_type_to_names: Dict[str, Set[str]] = {}
for item, item_data in item_table.items():
lookup_type_to_names.setdefault(item_data.type, set()).add(item)
-item_name_groups = {group: lookup_type_to_names[group] for group in ("Skill", "Charm", "Mask", "Vessel",
- "Relic", "Root", "Map", "Stag", "Cocoon",
- "Soul", "DreamWarrior", "DreamBoss")}
-
directionals = ('', 'Left_', 'Right_')
-
-item_name_groups.update({
+item_name_groups = ({
+ "BossEssence": lookup_type_to_names["DreamWarrior"] | lookup_type_to_names["DreamBoss"],
+ "BossGeo": lookup_type_to_names["Boss_Geo"],
+ "CDash": {x + "Crystal_Heart" for x in directionals},
+ "Charms": lookup_type_to_names["Charm"],
+ "CharmNotches": lookup_type_to_names["Notch"],
+ "Claw": {x + "Mantis_Claw" for x in directionals},
+ "Cloak": {x + "Mothwing_Cloak" for x in directionals} | {"Shade_Cloak", "Split_Shade_Cloak"},
+ "Dive": {"Desolate_Dive", "Descending_Dark"},
+ "LifebloodCocoons": lookup_type_to_names["Cocoon"],
"Dreamers": {"Herrah", "Monomon", "Lurien"},
- "Cloak": {x + 'Mothwing_Cloak' for x in directionals} | {'Shade_Cloak', 'Split_Shade_Cloak'},
- "Claw": {x + 'Mantis_Claw' for x in directionals},
- "CDash": {x + 'Crystal_Heart' for x in directionals},
- "Fragments": {"Queen_Fragment", "King_Fragment", "Void_Heart"},
+ "Fireball": {"Vengeful_Spirit", "Shade_Soul"},
+ "GeoChests": lookup_type_to_names["Geo"],
+ "GeoRocks": lookup_type_to_names["Rock"],
+ "GrimmkinFlames": lookup_type_to_names["Flame"],
+ "Grimmchild": {"Grimmchild1", "Grimmchild2"},
+ "Grubs": lookup_type_to_names["Grub"],
+ "JournalEntries": lookup_type_to_names["Journal"],
+ "JunkPitChests": lookup_type_to_names["JunkPitChest"],
+ "Keys": lookup_type_to_names["Key"],
+ "LoreTablets": lookup_type_to_names["Lore"] | lookup_type_to_names["PalaceLore"],
+ "Maps": lookup_type_to_names["Map"],
+ "MaskShards": lookup_type_to_names["Mask"],
+ "Mimics": lookup_type_to_names["Mimic"],
+ "Nail": lookup_type_to_names["CursedNail"],
+ "PalaceJournal": {"Journal_Entry-Seal_of_Binding"},
+ "PalaceLore": lookup_type_to_names["PalaceLore"],
+ "PalaceTotem": {"Soul_Totem-Palace", "Soul_Totem-Path_of_Pain"},
+ "RancidEggs": lookup_type_to_names["Egg"],
+ "Relics": lookup_type_to_names["Relic"],
+ "Scream": {"Howling_Wraiths", "Abyss_Shriek"},
+ "Skills": lookup_type_to_names["Skill"],
+ "SoulTotems": lookup_type_to_names["Soul"],
+ "Stags": lookup_type_to_names["Stag"],
+ "VesselFragments": lookup_type_to_names["Vessel"],
+ "WhisperingRoots": lookup_type_to_names["Root"],
+ "WhiteFragments": {"Queen_Fragment", "King_Fragment", "Void_Heart"},
})
item_name_groups['Horizontal'] = item_name_groups['Cloak'] | item_name_groups['CDash']
item_name_groups['Vertical'] = item_name_groups['Claw'] | {'Monarch_Wings'}
+item_name_groups['Skills'] |= item_name_groups['Vertical'] | item_name_groups['Horizontal']
diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py
index 2a19ffd3e7c3..e2602036a24e 100644
--- a/worlds/hk/Options.py
+++ b/worlds/hk/Options.py
@@ -1,8 +1,12 @@
import typing
+import re
+from dataclasses import dataclass, make_dataclass
+
from .ExtractedData import logic_options, starts, pool_options
from .Rules import cost_terms
+from schema import And, Schema, Optional
-from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, SpecialRange
+from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink, PerGameCommonOptions
from .Charms import vanilla_costs, names as charm_names
if typing.TYPE_CHECKING:
@@ -11,12 +15,16 @@
else:
Random = typing.Any
-
locations = {"option_" + start: i for i, start in enumerate(starts)}
# This way the dynamic start names are picked up by the MetaClass Choice belongs to
-StartLocation = type("StartLocation", (Choice,), {"__module__": __name__, "auto_display_name": False, **locations,
- "__doc__": "Choose your start location. "
- "This is currently only locked to King's Pass."})
+StartLocation = type("StartLocation", (Choice,), {
+ "__module__": __name__,
+ "auto_display_name": False,
+ "display_name": "Start Location",
+ "__doc__": "Choose your start location. "
+ "This is currently only locked to King's Pass.",
+ **locations,
+})
del (locations)
option_docstrings = {
@@ -49,8 +57,7 @@
"RandomizeBossEssence": "Randomize boss essence drops, such as those for defeating Warrior Dreams, into the item "
"pool and open their locations\n for randomization.",
"RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.",
- "RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization."
- "Mimic Grubs are always placed\n in your own game.",
+ "RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization.",
"RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see"
" and buy an item\n that is randomized into that location as well.",
"RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items "
@@ -99,8 +106,12 @@
"RandomizeKeys",
"RandomizeMaskShards",
"RandomizeVesselFragments",
+ "RandomizeCharmNotches",
"RandomizePaleOre",
- "RandomizeRelics"
+ "RandomizeRancidEggs",
+ "RandomizeRelics",
+ "RandomizeStags",
+ "RandomizeLifebloodCocoons"
}
shop_to_option = {
@@ -117,6 +128,7 @@
hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {}
+splitter_pattern = re.compile(r'(? typing.List[int]:
random_source.shuffle(charms)
return charms
else:
- charms = [0]*self.charm_count
+ charms = [0] * self.charm_count
for x in range(self.value):
- index = random_source.randint(0, self.charm_count-1)
+ index = random_source.randint(0, self.charm_count - 1)
while charms[index] > 5:
- index = random_source.randint(0, self.charm_count-1)
+ index = random_source.randint(0, self.charm_count - 1)
charms[index] += 1
return charms
@@ -283,6 +299,9 @@ class PlandoCharmCosts(OptionDict):
This is set after any random Charm Notch costs, if applicable."""
display_name = "Charm Notch Cost Plando"
valid_keys = frozenset(charm_names)
+ schema = Schema({
+ Optional(name): And(int, lambda n: 6 >= n >= 0) for name in charm_names
+ })
def get_costs(self, charm_costs: typing.List[int]) -> typing.List[int]:
for name, cost in self.value.items():
@@ -384,8 +403,8 @@ class Goal(Choice):
option_hollowknight = 1
option_siblings = 2
option_radiance = 3
- # Client support exists for this, but logic is a nightmare
- # option_godhome = 4
+ option_godhome = 4
+ option_godhome_flower = 5
default = 0
@@ -402,22 +421,47 @@ class WhitePalace(Choice):
default = 0
-class DeathLink(Choice):
+class ExtraPlatforms(DefaultOnToggle):
+ """Places additional platforms to make traveling throughout Hallownest more convenient."""
+ display_name = "Extra Platforms"
+
+
+class AddUnshuffledLocations(Toggle):
+ """Adds non-randomized locations to the location pool, which allows syncing
+ of location state with co-op or automatic collection via collect.
+
+ Note: This will increase the number of location checks required to purchase
+ hints to the total maximum.
"""
- When you die, everyone dies. Of course the reverse is true too.
- When enabled, choose how incoming deathlinks are handled:
- vanilla: DeathLink kills you and is just like any other death. RIP your previous shade and geo.
- shadeless: DeathLink kills you, but no shade spawns and no geo is lost. Your previous shade, if any, is untouched.
- shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise.
+ display_name = "Add Unshuffled Locations"
+
+
+class DeathLinkShade(Choice):
+ """Sets whether to create a shade when you are killed by a DeathLink and how to handle your existing shade, if any.
+
+ vanilla: DeathLink deaths function like any other death and overrides your existing shade (including geo), if any.
+ shadeless: DeathLink deaths do not spawn shades. Your existing shade (including geo), if any, is untouched.
+ shade: DeathLink deaths spawn a shade if you do not have an existing shade. Otherwise, it acts like shadeless.
+
+ * This option has no effect if DeathLink is disabled.
+ ** Self-death shade behavior is not changed; if a self-death normally creates a shade in vanilla, it will override
+ your existing shade, if any.
"""
- option_off = 0
- alias_no = 0
- alias_true = 1
- alias_on = 1
- alias_yes = 1
+ option_vanilla = 0
option_shadeless = 1
- option_vanilla = 2
- option_shade = 3
+ option_shade = 2
+ default = 2
+ display_name = "Deathlink Shade Handling"
+
+
+class DeathLinkBreaksFragileCharms(Toggle):
+ """Sets if fragile charms break when you are killed by a DeathLink.
+
+ * This option has no effect if DeathLink is disabled.
+ ** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it
+ will continue to do so.
+ """
+ display_name = "Deathlink Breaks Fragile Charms"
class StartingGeo(Range):
@@ -441,18 +485,20 @@ class CostSanity(Choice):
alias_yes = 1
option_shopsonly = 2
option_notshops = 3
- display_name = "Cost Sanity"
+ display_name = "Costsanity"
class CostSanityHybridChance(Range):
"""The chance that a CostSanity cost will include two components instead of one, e.g. Grubs + Essence"""
range_end = 100
default = 10
+ display_name = "Costsanity Hybrid Chance"
cost_sanity_weights: typing.Dict[str, type(Option)] = {}
for term, cost in cost_terms.items():
option_name = f"CostSanity{cost.option}Weight"
+ display_name = f"Costsanity {cost.option} Weight"
extra_data = {
"__module__": __name__, "range_end": 1000,
"__doc__": (
@@ -465,10 +511,10 @@ class CostSanityHybridChance(Range):
extra_data["__doc__"] += " Geo costs will never be chosen for Grubfather, Seer, or Egg Shop."
option = type(option_name, (Range,), extra_data)
+ option.display_name = display_name
globals()[option.__name__] = option
cost_sanity_weights[option.__name__] = option
-
hollow_knight_options: typing.Dict[str, type(Option)] = {
**hollow_knight_randomize_options,
RandomizeElevatorPass.__name__: RandomizeElevatorPass,
@@ -476,7 +522,8 @@ class CostSanityHybridChance(Range):
**{
option.__name__: option
for option in (
- StartLocation, Goal, WhitePalace, StartingGeo, DeathLink,
+ StartLocation, Goal, WhitePalace, ExtraPlatforms, AddUnshuffledLocations, StartingGeo,
+ DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms,
MinimumGeoPrice, MaximumGeoPrice,
MinimumGrubPrice, MaximumGrubPrice,
MinimumEssencePrice, MaximumEssencePrice,
@@ -488,8 +535,10 @@ class CostSanityHybridChance(Range):
LegEaterShopSlots, GrubfatherRewardSlots,
SeerRewardSlots, ExtraShopSlots,
SplitCrystalHeart, SplitMothwingCloak, SplitMantisClaw,
- CostSanity, CostSanityHybridChance,
+ CostSanity, CostSanityHybridChance
)
},
**cost_sanity_weights
}
+
+HKOptions = make_dataclass("HKOptions", [(name, option) for name, option in hollow_knight_options.items()], bases=(PerGameCommonOptions,))
diff --git a/worlds/hk/Rules.py b/worlds/hk/Rules.py
index 4fe4160b4cfc..e162e1dfa81c 100644
--- a/worlds/hk/Rules.py
+++ b/worlds/hk/Rules.py
@@ -1,7 +1,7 @@
from ..generic.Rules import set_rule, add_rule
-from BaseClasses import MultiWorld
from ..AutoWorld import World
from .GeneratedRules import set_generated_rules
+from .GodhomeData import set_godhome_rules
from typing import NamedTuple
@@ -39,14 +39,52 @@ def hk_set_rule(hk_world: World, location: str, rule):
def set_rules(hk_world: World):
player = hk_world.player
- world = hk_world.multiworld
set_generated_rules(hk_world, hk_set_rule)
+ set_godhome_rules(hk_world, hk_set_rule)
# Shop costs
- for region in world.get_regions(player):
- for location in region.locations:
- if location.costs:
- for term, amount in location.costs.items():
- if term == "GEO": # No geo logic!
- continue
- add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount)
+ for location in hk_world.multiworld.get_locations(player):
+ if location.costs:
+ for term, amount in location.costs.items():
+ if term == "GEO": # No geo logic!
+ continue
+ add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount)
+
+
+def _hk_nail_combat(state, player) -> bool:
+ return state.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player)
+
+
+def _hk_can_beat_thk(state, player) -> bool:
+ return (
+ state.has('Opened_Black_Egg_Temple', player)
+ and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1
+ and _hk_nail_combat(state, player)
+ and (
+ state.has_any({'LEFTDASH', 'RIGHTDASH'}, player)
+ or state._hk_option(player, 'ProficientCombat')
+ )
+ and state.has('FOCUS', player)
+ )
+
+
+def _hk_siblings_ending(state, player) -> bool:
+ return _hk_can_beat_thk(state, player) and state.has('WHITEFRAGMENT', player, 3)
+
+
+def _hk_can_beat_radiance(state, player) -> bool:
+ return (
+ state.has('Opened_Black_Egg_Temple', player)
+ and _hk_nail_combat(state, player)
+ and state.has('WHITEFRAGMENT', player, 3)
+ and state.has('DREAMNAIL', player)
+ and (
+ (state.has('LEFTCLAW', player) and state.has('RIGHTCLAW', player))
+ or state.has('WINGS', player)
+ )
+ and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1
+ and (
+ (state.has('LEFTDASH', player, 2) and state.has('RIGHTDASH', player, 2)) # Both Shade Cloaks
+ or (state._hk_option(player, 'ProficientCombat') and state.has('QUAKE', player)) # or Dive
+ )
+ )
diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py
index 1a9d4b5d6160..e5065876ddf3 100644
--- a/worlds/hk/__init__.py
+++ b/worlds/hk/__init__.py
@@ -10,9 +10,9 @@
from .Items import item_table, lookup_type_to_names, item_name_groups
from .Regions import create_regions
-from .Rules import set_rules, cost_terms
+from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance
from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \
- shop_to_option
+ shop_to_option, HKOptions
from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \
event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs
from .Charms import names as charm_names
@@ -142,7 +142,8 @@ class HKWorld(World):
As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils.
""" # from https://www.hollowknight.com
game: str = "Hollow Knight"
- option_definitions = hollow_knight_options
+ options_dataclass = HKOptions
+ options: HKOptions
web = HKWeb()
@@ -154,10 +155,9 @@ class HKWorld(World):
ranges: typing.Dict[str, typing.Tuple[int, int]]
charm_costs: typing.List[int]
cached_filler_items = {}
- data_version = 2
- def __init__(self, world, player):
- super(HKWorld, self).__init__(world, player)
+ def __init__(self, multiworld, player):
+ super(HKWorld, self).__init__(multiworld, player)
self.created_multi_locations: typing.Dict[str, typing.List[HKLocation]] = {
location: list() for location in multi_locations
}
@@ -166,30 +166,29 @@ def __init__(self, world, player):
self.vanilla_shop_costs = deepcopy(vanilla_shop_costs)
def generate_early(self):
- world = self.multiworld
- charm_costs = world.RandomCharmCosts[self.player].get_costs(world.random)
- self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs)
- # world.exclude_locations[self.player].value.update(white_palace_locations)
- world.local_items[self.player].value.add("Mimic_Grub")
+ options = self.options
+ charm_costs = options.RandomCharmCosts.get_costs(self.random)
+ self.charm_costs = options.PlandoCharmCosts.get_costs(charm_costs)
+ # options.exclude_locations.value.update(white_palace_locations)
for term, data in cost_terms.items():
- mini = getattr(world, f"Minimum{data.option}Price")[self.player]
- maxi = getattr(world, f"Maximum{data.option}Price")[self.player]
+ mini = getattr(options, f"Minimum{data.option}Price")
+ maxi = getattr(options, f"Maximum{data.option}Price")
# if minimum > maximum, set minimum to maximum
mini.value = min(mini.value, maxi.value)
self.ranges[term] = mini.value, maxi.value
- world.push_precollected(HKItem(starts[world.StartLocation[self.player].current_key],
+ self.multiworld.push_precollected(HKItem(starts[options.StartLocation.current_key],
True, None, "Event", self.player))
def white_palace_exclusions(self):
exclusions = set()
- wp = self.multiworld.WhitePalace[self.player]
+ wp = self.options.WhitePalace
if wp <= WhitePalace.option_nopathofpain:
exclusions.update(path_of_pain_locations)
if wp <= WhitePalace.option_kingfragment:
exclusions.update(white_palace_checks)
if wp == WhitePalace.option_exclude:
exclusions.add("King_Fragment")
- if self.multiworld.RandomizeCharms[self.player]:
+ if self.options.RandomizeCharms:
# If charms are randomized, this will be junk-filled -- so transitions and events are not progression
exclusions.update(white_palace_transitions)
exclusions.update(white_palace_events)
@@ -200,8 +199,14 @@ def create_regions(self):
self.multiworld.regions.append(menu_region)
# wp_exclusions = self.white_palace_exclusions()
+ # check for any goal that godhome events are relevant to
+ all_event_names = event_names.copy()
+ if self.options.Goal in [Goal.option_godhome, Goal.option_godhome_flower]:
+ from .GodhomeData import godhome_event_names
+ all_event_names.update(set(godhome_event_names))
+
# Link regions
- for event_name in event_names:
+ for event_name in all_event_names:
#if event_name in wp_exclusions:
# continue
loc = HKLocation(self.player, event_name, None, menu_region)
@@ -226,16 +231,16 @@ def create_items(self):
pool: typing.List[HKItem] = []
wp_exclusions = self.white_palace_exclusions()
junk_replace: typing.Set[str] = set()
- if self.multiworld.RemoveSpellUpgrades[self.player]:
+ if self.options.RemoveSpellUpgrades:
junk_replace.update(("Abyss_Shriek", "Shade_Soul", "Descending_Dark"))
randomized_starting_items = set()
for attr, items in randomizable_starting_items.items():
- if getattr(self.multiworld, attr)[self.player]:
+ if getattr(self.options, attr):
randomized_starting_items.update(items)
# noinspection PyShadowingNames
- def _add(item_name: str, location_name: str):
+ def _add(item_name: str, location_name: str, randomized: bool):
"""
Adds a pairing of an item and location, doing appropriate checks to see if it should be vanilla or not.
"""
@@ -253,7 +258,7 @@ def _add(item_name: str, location_name: str):
if item_name in junk_replace:
item_name = self.get_filler_item_name()
- item = self.create_item(item_name)
+ item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations else self.create_event(item_name)
if location_name == "Start":
if item_name in randomized_starting_items:
@@ -277,50 +282,55 @@ def _add(item_name: str, location_name: str):
location.progress_type = LocationProgressType.EXCLUDED
for option_key, option in hollow_knight_randomize_options.items():
- randomized = getattr(self.multiworld, option_key)[self.player]
+ randomized = getattr(self.options, option_key)
+ if all([not randomized, option_key in logicless_options, not self.options.AddUnshuffledLocations]):
+ continue
for item_name, location_name in zip(option.items, option.locations):
if item_name in junk_replace:
item_name = self.get_filler_item_name()
- if (item_name == "Crystal_Heart" and self.multiworld.SplitCrystalHeart[self.player]) or \
- (item_name == "Mothwing_Cloak" and self.multiworld.SplitMothwingCloak[self.player]):
- _add("Left_" + item_name, location_name)
- _add("Right_" + item_name, "Split_" + location_name)
+ if (item_name == "Crystal_Heart" and self.options.SplitCrystalHeart) or \
+ (item_name == "Mothwing_Cloak" and self.options.SplitMothwingCloak):
+ _add("Left_" + item_name, location_name, randomized)
+ _add("Right_" + item_name, "Split_" + location_name, randomized)
continue
- if item_name == "Mantis_Claw" and self.multiworld.SplitMantisClaw[self.player]:
- _add("Left_" + item_name, "Left_" + location_name)
- _add("Right_" + item_name, "Right_" + location_name)
+ if item_name == "Mantis_Claw" and self.options.SplitMantisClaw:
+ _add("Left_" + item_name, "Left_" + location_name, randomized)
+ _add("Right_" + item_name, "Right_" + location_name, randomized)
continue
- if item_name == "Shade_Cloak" and self.multiworld.SplitMothwingCloak[self.player]:
- if self.multiworld.random.randint(0, 1):
+ if item_name == "Shade_Cloak" and self.options.SplitMothwingCloak:
+ if self.random.randint(0, 1):
item_name = "Left_Mothwing_Cloak"
else:
item_name = "Right_Mothwing_Cloak"
+ if item_name == "Grimmchild2" and self.options.RandomizeGrimmkinFlames and self.options.RandomizeCharms:
+ _add("Grimmchild1", location_name, randomized)
+ continue
- _add(item_name, location_name)
+ _add(item_name, location_name, randomized)
- if self.multiworld.RandomizeElevatorPass[self.player]:
+ if self.options.RandomizeElevatorPass:
randomized = True
- _add("Elevator_Pass", "Elevator_Pass")
+ _add("Elevator_Pass", "Elevator_Pass", randomized)
for shop, locations in self.created_multi_locations.items():
- for _ in range(len(locations), getattr(self.multiworld, shop_to_option[shop])[self.player].value):
+ for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value):
loc = self.create_location(shop)
unfilled_locations += 1
# Balance the pool
item_count = len(pool)
- additional_shop_items = max(item_count - unfilled_locations, self.multiworld.ExtraShopSlots[self.player].value)
+ additional_shop_items = max(item_count - unfilled_locations, self.options.ExtraShopSlots.value)
# Add additional shop items, as needed.
if additional_shop_items > 0:
shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16)
- if not self.multiworld.EggShopSlots[self.player].value: # No eggshop, so don't place items there
+ if not self.options.EggShopSlots: # No eggshop, so don't place items there
shops.remove('Egg_Shop')
if shops:
for _ in range(additional_shop_items):
- shop = self.multiworld.random.choice(shops)
+ shop = self.random.choice(shops)
loc = self.create_location(shop)
unfilled_locations += 1
if len(self.created_multi_locations[shop]) >= 16:
@@ -346,7 +356,7 @@ def sort_shops_by_cost(self):
loc.costs = costs
def apply_costsanity(self):
- setting = self.multiworld.CostSanity[self.player].value
+ setting = self.options.CostSanity.value
if not setting:
return # noop
@@ -360,10 +370,10 @@ def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]:
return {k: v for k, v in weights.items() if v}
- random = self.multiworld.random
- hybrid_chance = getattr(self.multiworld, f"CostSanityHybridChance")[self.player].value
+ random = self.random
+ hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value
weights = {
- data.term: getattr(self.multiworld, f"CostSanity{data.option}Weight")[self.player].value
+ data.term: getattr(self.options, f"CostSanity{data.option}Weight").value
for data in cost_terms.values()
}
weights_geoless = dict(weights)
@@ -396,7 +406,7 @@ def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]:
continue
if setting == CostSanity.option_shopsonly and location.basename not in multi_locations:
continue
- if location.basename in {'Grubfather', 'Seer', 'Eggshop'}:
+ if location.basename in {'Grubfather', 'Seer', 'Egg_Shop'}:
our_weights = dict(weights_geoless)
else:
our_weights = dict(weights)
@@ -418,19 +428,22 @@ def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]:
location.sort_costs()
def set_rules(self):
- world = self.multiworld
+ multiworld = self.multiworld
player = self.player
- if world.logic[player] != 'nologic':
- goal = world.Goal[player]
- if goal == Goal.option_hollowknight:
- world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player)
- elif goal == Goal.option_siblings:
- world.completion_condition[player] = lambda state: state._hk_siblings_ending(player)
- elif goal == Goal.option_radiance:
- world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player)
- else:
- # Any goal
- world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player)
+ goal = self.options.Goal
+ if goal == Goal.option_hollowknight:
+ multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player)
+ elif goal == Goal.option_siblings:
+ multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player)
+ elif goal == Goal.option_radiance:
+ multiworld.completion_condition[player] = lambda state: _hk_can_beat_radiance(state, player)
+ elif goal == Goal.option_godhome:
+ multiworld.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player)
+ elif goal == Goal.option_godhome_flower:
+ multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player)
+ else:
+ # Any goal
+ multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) or _hk_can_beat_radiance(state, player)
set_rules(self)
@@ -438,8 +451,8 @@ def fill_slot_data(self):
slot_data = {}
options = slot_data["options"] = {}
- for option_name in self.option_definitions:
- option = getattr(self.multiworld, option_name)[self.player]
+ for option_name in hollow_knight_options:
+ option = getattr(self.options, option_name)
try:
optionvalue = int(option.value)
except TypeError:
@@ -448,10 +461,10 @@ def fill_slot_data(self):
options[option_name] = optionvalue
# 32 bit int
- slot_data["seed"] = self.multiworld.per_slot_randoms[self.player].randint(-2147483647, 2147483646)
+ slot_data["seed"] = self.random.randint(-2147483647, 2147483646)
# Backwards compatibility for shop cost data (HKAP < 0.1.0)
- if not self.multiworld.CostSanity[self.player]:
+ if not self.options.CostSanity:
for shop, terms in shop_cost_types.items():
unit = cost_terms[next(iter(terms))].option
if unit == "Geo":
@@ -477,12 +490,16 @@ def create_item(self, name: str) -> HKItem:
item_data = item_table[name]
return HKItem(name, item_data.advancement, item_data.id, item_data.type, self.player)
+ def create_event(self, name: str) -> HKItem:
+ item_data = item_table[name]
+ return HKItem(name, item_data.advancement, None, item_data.type, self.player)
+
def create_location(self, name: str, vanilla=False) -> HKLocation:
costs = None
basename = name
if name in shop_cost_types:
costs = {
- term: self.multiworld.random.randint(*self.ranges[term])
+ term: self.random.randint(*self.ranges[term])
for term in shop_cost_types[name]
}
elif name in vanilla_location_costs:
@@ -495,9 +512,15 @@ def create_location(self, name: str, vanilla=False) -> HKLocation:
name = f"{name}_{i}"
region = self.multiworld.get_region("Menu", self.player)
- loc = HKLocation(self.player, name,
- self.location_name_to_id[name], region, costs=costs, vanilla=vanilla,
- basename=basename)
+
+ if vanilla and not self.options.AddUnshuffledLocations:
+ loc = HKLocation(self.player, name,
+ None, region, costs=costs, vanilla=vanilla,
+ basename=basename)
+ else:
+ loc = HKLocation(self.player, name,
+ self.location_name_to_id[name], region, costs=costs, vanilla=vanilla,
+ basename=basename)
if multi is not None:
multi.append(loc)
@@ -517,12 +540,12 @@ def collect(self, state, item: HKItem) -> bool:
change = super(HKWorld, self).collect(state, item)
if change:
for effect_name, effect_value in item_effects.get(item.name, {}).items():
- state.prog_items[effect_name, item.player] += effect_value
+ state.prog_items[item.player][effect_name] += effect_value
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
- if state.prog_items.get(('RIGHTDASH', item.player), 0) and \
- state.prog_items.get(('LEFTDASH', item.player), 0):
- (state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player]) = \
- ([max(state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player])] * 2)
+ if state.prog_items[item.player].get('RIGHTDASH', 0) and \
+ state.prog_items[item.player].get('LEFTDASH', 0):
+ (state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \
+ ([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
return change
def remove(self, state, item: HKItem) -> bool:
@@ -530,33 +553,34 @@ def remove(self, state, item: HKItem) -> bool:
if change:
for effect_name, effect_value in item_effects.get(item.name, {}).items():
- if state.prog_items[effect_name, item.player] == effect_value:
- del state.prog_items[effect_name, item.player]
- state.prog_items[effect_name, item.player] -= effect_value
+ if state.prog_items[item.player][effect_name] == effect_value:
+ del state.prog_items[item.player][effect_name]
+ else:
+ state.prog_items[item.player][effect_name] -= effect_value
return change
@classmethod
- def stage_write_spoiler(cls, world: MultiWorld, spoiler_handle):
- hk_players = world.get_game_players(cls.game)
+ def stage_write_spoiler(cls, multiworld: MultiWorld, spoiler_handle):
+ hk_players = multiworld.get_game_players(cls.game)
spoiler_handle.write('\n\nCharm Notches:')
for player in hk_players:
- name = world.get_player_name(player)
+ name = multiworld.get_player_name(player)
spoiler_handle.write(f'\n{name}\n')
- hk_world: HKWorld = world.worlds[player]
+ hk_world: HKWorld = multiworld.worlds[player]
for charm_number, cost in enumerate(hk_world.charm_costs):
spoiler_handle.write(f"\n{charm_names[charm_number]}: {cost}")
spoiler_handle.write('\n\nShop Prices:')
for player in hk_players:
- name = world.get_player_name(player)
+ name = multiworld.get_player_name(player)
spoiler_handle.write(f'\n{name}\n')
- hk_world: HKWorld = world.worlds[player]
+ hk_world: HKWorld = multiworld.worlds[player]
- if world.CostSanity[player].value:
+ if hk_world.options.CostSanity:
for loc in sorted(
(
- loc for loc in itertools.chain(*(region.locations for region in world.get_regions(player)))
+ loc for loc in itertools.chain(*(region.locations for region in multiworld.get_regions(player)))
if loc.costs
), key=operator.attrgetter('name')
):
@@ -580,15 +604,15 @@ def get_filler_item_name(self) -> str:
'RandomizeGeoRocks', 'RandomizeSoulTotems', 'RandomizeLoreTablets', 'RandomizeJunkPitChests',
'RandomizeRancidEggs'
):
- if getattr(self.multiworld, group):
+ if getattr(self.options, group):
fillers.extend(item for item in hollow_knight_randomize_options[group].items if item not in
exclusions)
self.cached_filler_items[self.player] = fillers
- return self.multiworld.random.choice(self.cached_filler_items[self.player])
+ return self.random.choice(self.cached_filler_items[self.player])
-def create_region(world: MultiWorld, player: int, name: str, location_names=None) -> Region:
- ret = Region(name, player, world)
+def create_region(multiworld: MultiWorld, player: int, name: str, location_names=None) -> Region:
+ ret = Region(name, player, multiworld)
if location_names:
for location in location_names:
loc_id = HKWorld.location_name_to_id.get(location, None)
@@ -636,6 +660,8 @@ class HKItem(Item):
def __init__(self, name, advancement, code, type: str, player: int = None):
if name == "Mimic_Grub":
classification = ItemClassification.trap
+ elif name == "Godtuner":
+ classification = ItemClassification.progression
elif type in ("Grub", "DreamWarrior", "Root", "Egg", "Dreamer"):
classification = ItemClassification.progression_skip_balancing
elif type == "Charm" and name not in progression_charms:
@@ -659,42 +685,7 @@ def _hk_notches(self, player: int, *notches: int) -> int:
return sum(self.multiworld.worlds[player].charm_costs[notch] for notch in notches)
def _hk_option(self, player: int, option_name: str) -> int:
- return getattr(self.multiworld, option_name)[player].value
+ return getattr(self.multiworld.worlds[player].options, option_name).value
def _hk_start(self, player, start_location: str) -> bool:
- return self.multiworld.StartLocation[player] == start_location
-
- def _hk_nail_combat(self, player: int) -> bool:
- return self.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player)
-
- def _hk_can_beat_thk(self, player: int) -> bool:
- return (
- self.has('Opened_Black_Egg_Temple', player)
- and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1
- and self._hk_nail_combat(player)
- and (
- self.has_any({'LEFTDASH', 'RIGHTDASH'}, player)
- or self._hk_option(player, 'ProficientCombat')
- )
- and self.has('FOCUS', player)
- )
-
- def _hk_siblings_ending(self, player: int) -> bool:
- return self._hk_can_beat_thk(player) and self.has('WHITEFRAGMENT', player, 3)
-
- def _hk_can_beat_radiance(self, player: int) -> bool:
- return (
- self.has('Opened_Black_Egg_Temple', player)
- and self._hk_nail_combat(player)
- and self.has('WHITEFRAGMENT', player, 3)
- and self.has('DREAMNAIL', player)
- and (
- (self.has('LEFTCLAW', player) and self.has('RIGHTCLAW', player))
- or self.has('WINGS', player)
- )
- and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1
- and (
- (self.has('LEFTDASH', player, 2) and self.has('RIGHTDASH', player, 2)) # Both Shade Cloaks
- or (self._hk_option(player, 'ProficientCombat') and self.has('QUAKE', player)) # or Dive
- )
- )
+ return self.multiworld.worlds[player].options.StartLocation == start_location
diff --git a/worlds/hk/docs/en_Hollow Knight.md b/worlds/hk/docs/en_Hollow Knight.md
index 6e74c8a1fbbc..94398ec6ac14 100644
--- a/worlds/hk/docs/en_Hollow Knight.md
+++ b/worlds/hk/docs/en_Hollow Knight.md
@@ -1,18 +1,20 @@
# Hollow Knight
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Randomization swaps around the locations of items. The items being swapped around are chosen within your YAML.
-Shop costs are presently always randomized.
+Shop costs are presently always randomized. Items which could be randomized, but are not, will remain unmodified in
+their usual locations. In particular, when the items at Grubfather and Seer are partially randomized, randomized items
+will be obtained from a chest in the room, while unrandomized items will be given by the NPC as normal.
## What Hollow Knight items can appear in other players' worlds?
-This is dependent entirely upon your YAML settings. Some examples include: charms, grubs, lifeblood cocoons, geo, etc.
+This is dependent entirely upon your YAML options. Some examples include: charms, grubs, lifeblood cocoons, geo, etc.
## What does another world's item look like in Hollow Knight?
diff --git a/worlds/hk/docs/setup_en.md b/worlds/hk/docs/setup_en.md
index adf975ff515e..c046785038d8 100644
--- a/worlds/hk/docs/setup_en.md
+++ b/worlds/hk/docs/setup_en.md
@@ -1,37 +1,39 @@
# Hollow Knight for Archipelago Setup Guide
## Required Software
-* Download and unzip the Scarab+ Mod Manager from the [Scarab+ website](https://themulhima.github.io/Scarab/).
+* Download and unzip the Lumafly Mod Manager from the [Lumafly website](https://themulhima.github.io/Lumafly/).
* A legal copy of Hollow Knight.
+ * Steam, Gog, and Xbox Game Pass versions of the game are supported.
+ * Windows, Mac, and Linux (including Steam Deck) are supported.
-## Installing the Archipelago Mod using Scarab+
-1. Launch Scarab+ and ensure it locates your Hollow Knight installation directory.
+## Installing the Archipelago Mod using Lumafly
+1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.
2. Click the "Install" button near the "Archipelago" mod entry.
* If desired, also install "Archipelago Map Mod" to use as an in-game tracker.
3. Launch the game, you're all set!
-### What to do if Scarab+ fails to find your XBox Game Pass installation directory
-1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar.
-2. Click the three points then click "Manage".
-3. Go to the "Files" tab and select "Browse...".
-4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
-5. Run Scarab+ as an administrator and, when it asks you for the path, paste what you copied in step 4.
-
-#### Alternative Method:
-1. Click on your profile then "Settings".
-2. Go to the "General" tab and select "CHANGE FOLDER".
-3. Look for a folder where you want to install the game (preferably inside a folder on your desktop) and copy the path.
-4. Run Scarab+ as an administrator and, when it asks you for the path, paste what you copied in step 3.
-
-Note: The path folder needs to have the "Hollow Knight_Data" folder inside.
+### What to do if Lumafly fails to find your installation directory
+1. Find the directory manually.
+ * Xbox Game Pass:
+ 1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar.
+ 2. Click the three points then click "Manage".
+ 3. Go to the "Files" tab and select "Browse...".
+ 4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
+ * Steam:
+ 1. You likely put your Steam library in a non-standard place. If this is the case, you probably know where
+ it is. Find your steam library and then find the Hollow Knight folder and copy the path.
+ * Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
+ * Linux/Steam Deck - ~/.local/share/Steam/steamapps/common/Hollow Knight
+ * Mac - ~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app
+2. Run Lumafly as an administrator and, when it asks you for the path, paste the path you copied.
## Configuring your YAML File
### What is a YAML and why do I need one?
-You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn
-about why Archipelago uses YAML files and what they're for.
+An YAML file is the way that you provide your player options to Archipelago.
+See the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn more.
### Where do I get a YAML?
-You can use the [game settings page for Hollow Knight](/games/Hollow%20Knight/player-settings) here on the Archipelago
+You can use the [game options page for Hollow Knight](/games/Hollow%20Knight/player-options) here on the Archipelago
website to generate a YAML using a graphical interface.
### Joining an Archipelago Game in Hollow Knight
@@ -44,9 +46,7 @@ website to generate a YAML using a graphical interface.
* If you are waiting for a countdown then wait for it to lapse before hitting Start.
* Or hit Start then pause the game once you're in it.
-## Commands
-While playing the multiworld you can interact with the server using various commands listed in the
-[commands guide](/tutorial/Archipelago/commands/en). As this game does not have an in-game text client at the moment,
-You can optionally connect to the multiworld using the text client, which can be found in the
-[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases) as Archipelago Text Client to
-enter these commands.
+## Hints and other commands
+While playing in a multiworld, you can interact with the server using various commands listed in the
+[commands guide](/tutorial/Archipelago/commands/en). You can use the Archipelago Text Client to do this,
+which is included in the latest release of the [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
diff --git a/worlds/hylics2/Items.py b/worlds/hylics2/Items.py
index e09144c6cc99..4556a4a6748b 100644
--- a/worlds/hylics2/Items.py
+++ b/worlds/hylics2/Items.py
@@ -98,7 +98,7 @@ class ItemDict(TypedDict):
'count': 4,
'name': 'MULTI-JUICE'},
200651: {'classification': ItemClassification.filler,
- 'count': 1,
+ 'count': 3,
'name': 'MULTI STEM CELL'},
200652: {'classification': ItemClassification.filler,
'count': 6,
diff --git a/worlds/hylics2/Locations.py b/worlds/hylics2/Locations.py
index 80e02b1c7169..053bfbd5856c 100644
--- a/worlds/hylics2/Locations.py
+++ b/worlds/hylics2/Locations.py
@@ -220,6 +220,10 @@ class LocationDict(TypedDict, total=False):
'region': 15},
200754: {'name': "Sage Labyrinth: 2F Sarcophagus",
'region': 15},
+ 200786: {'name': "Sage Labyrinth: Boss Secret Chest 1",
+ 'region': 15},
+ 200787: {'name': "Sage Labyrinth: Boss Secret Chest 2",
+ 'region': 15},
200725: {'name': "Sage Labyrinth: Motor Hunter Sarcophagus",
'region': 15},
200726: {'name': "Sage Labyrinth: Sage Item 1",
diff --git a/worlds/hylics2/Options.py b/worlds/hylics2/Options.py
index ac57e666a1a2..db9c316a7b1b 100644
--- a/worlds/hylics2/Options.py
+++ b/worlds/hylics2/Options.py
@@ -1,41 +1,79 @@
-from Options import Choice, Toggle, DefaultOnToggle, DeathLink
+from dataclasses import dataclass
+from Options import Choice, Removed, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions
+
class PartyShuffle(Toggle):
- """Shuffles party members into the pool.
- Note that enabling this can potentially increase both the difficulty and length of a run."""
+ """
+ Shuffles party members into the item pool.
+
+ Note that enabling this can significantly increase both the difficulty and length of a run.
+ """
display_name = "Shuffle Party Members"
+
class GestureShuffle(Choice):
- """Choose where gestures will appear in the item pool."""
+ """
+ Choose where gestures will appear in the item pool.
+ """
display_name = "Shuffle Gestures"
option_anywhere = 0
option_tvs_only = 1
option_default_locations = 2
default = 0
+
class MedallionShuffle(Toggle):
- """Shuffles red medallions into the pool."""
+ """
+ Shuffles red medallions into the item pool.
+ """
display_name = "Shuffle Red Medallions"
-class RandomStart(Toggle):
- """Start the randomizer in 1 of 4 positions.
- (Waynehouse, Viewax's Edifice, TV Island, Shield Facility)"""
- display_name = "Randomize Start Location"
+
+class StartLocation(Choice):
+ """
+ Select the starting location from 1 of 4 positions.
+ """
+ display_name = "Start Location"
+ option_waynehouse = 0
+ option_viewaxs_edifice = 1
+ option_tv_island = 2
+ option_shield_facility = 3
+ default = 0
+
+ @classmethod
+ def get_option_name(cls, value: int) -> str:
+ if value == 1:
+ return "Viewax's Edifice"
+ if value == 2:
+ return "TV Island"
+ return super().get_option_name(value)
+
class ExtraLogic(DefaultOnToggle):
- """Include some extra items in logic (CHARGE UP, 1x PAPER CUP) to prevent the game from becoming too difficult."""
+ """
+ Include some extra items in logic (CHARGE UP, 1x PAPER CUP) to prevent the game from becoming too difficult.
+ """
display_name = "Extra Items in Logic"
+
class Hylics2DeathLink(DeathLink):
- """When you die, everyone dies. The reverse is also true.
+ """
+ When you die, everyone dies. The reverse is also true.
+
Note that this also includes death by using the PERISH gesture.
- Can be toggled via in-game console command "/deathlink"."""
-
-hylics2_options = {
- "party_shuffle": PartyShuffle,
- "gesture_shuffle" : GestureShuffle,
- "medallion_shuffle" : MedallionShuffle,
- "random_start" : RandomStart,
- "extra_items_in_logic": ExtraLogic,
- "death_link": Hylics2DeathLink
-}
\ No newline at end of file
+
+ Can be toggled via in-game console command "/deathlink".
+ """
+
+
+@dataclass
+class Hylics2Options(PerGameCommonOptions):
+ party_shuffle: PartyShuffle
+ gesture_shuffle: GestureShuffle
+ medallion_shuffle: MedallionShuffle
+ start_location: StartLocation
+ extra_items_in_logic: ExtraLogic
+ death_link: Hylics2DeathLink
+
+ # Removed options
+ random_start: Removed
diff --git a/worlds/hylics2/Rules.py b/worlds/hylics2/Rules.py
index 12c22e01cd2f..3914054193ce 100644
--- a/worlds/hylics2/Rules.py
+++ b/worlds/hylics2/Rules.py
@@ -1,435 +1,580 @@
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):
world = hylics2world.multiworld
player = hylics2world.player
+ extra = hylics2world.options.extra_items_in_logic
+ party = hylics2world.options.party_shuffle
+ medallion = hylics2world.options.medallion_shuffle
+ start_location = hylics2world.options.start_location
+
# 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]:
+ if extra:
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]:
+ if party:
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]:
+ if medallion:
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)
+ and air_dash(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)
+ and air_dash(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)
+ and air_dash(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)
+ and air_dash(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)
+ and air_dash(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("Arcade 1: Lava Medallion", 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
- if world.party_shuffle[player] and world.medallion_shuffle[player]:
+ # extra rules if Shuffle Red Medallions and Party Shuffle are enabled
+ if party and medallion:
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")):
+ if 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"):
+ elif start_location == "viewaxs_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"):
+ # start logic (TV Island)
+ elif 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"):
+ # start logic (Shield Facility)
+ elif 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))
diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py
index 11401b11371e..18bcb0edc143 100644
--- a/worlds/hylics2/__init__.py
+++ b/worlds/hylics2/__init__.py
@@ -1,8 +1,9 @@
from typing import Dict, List, Any
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
from worlds.generic.Rules import set_rule
-from . import Exits, Items, Locations, Options, Rules
-from ..AutoWorld import WebWorld, World
+from . import Exits, Items, Locations, Rules
+from .Options import Hylics2Options
+from worlds.AutoWorld import WebWorld, World
class Hylics2Web(WebWorld):
@@ -32,13 +33,9 @@ class Hylics2World(World):
item_name_to_id = {data["name"]: item_id for item_id, data in all_items.items()}
location_name_to_id = {data["name"]: loc_id for loc_id, data in all_locations.items()}
- option_definitions = Options.hylics2_options
- topology_present: bool = True
-
- data_version = 2
-
- start_location = "Waynehouse"
+ options_dataclass = Hylics2Options
+ options: Hylics2Options
def set_rules(self):
@@ -51,52 +48,35 @@ def create_item(self, name: str) -> "Hylics2Item":
return Hylics2Item(name, self.all_items[item_id]["classification"], item_id, player=self.player)
- def add_item(self, name: str, classification: ItemClassification, code: int) -> "Item":
- return Hylics2Item(name, classification, code, self.player)
-
-
def create_event(self, event: str):
return Hylics2Item(event, ItemClassification.progression_skip_balancing, None, self.player)
- # set random starting location if option is enabled
- def generate_early(self):
- if self.multiworld.random_start[self.player]:
- i = self.multiworld.random.randint(0, 3)
- if i == 0:
- self.start_location = "Waynehouse"
- elif i == 1:
- self.start_location = "Viewax's Edifice"
- elif i == 2:
- self.start_location = "TV Island"
- elif i == 3:
- self.start_location = "Shield Facility"
-
def create_items(self):
# create item pool
pool = []
# add regular items
- for i, data in Items.item_table.items():
- if data["count"] > 0:
- for j in range(data["count"]):
- pool.append(self.add_item(data["name"], data["classification"], i))
+ for item in Items.item_table.values():
+ if item["count"] > 0:
+ for _ in range(item["count"]):
+ pool.append(self.create_item(item["name"]))
# add party members if option is enabled
- if self.multiworld.party_shuffle[self.player]:
- for i, data in Items.party_item_table.items():
- pool.append(self.add_item(data["name"], data["classification"], i))
+ if self.options.party_shuffle:
+ for item in Items.party_item_table.values():
+ pool.append(self.create_item(item["name"]))
# handle gesture shuffle
- if not self.multiworld.gesture_shuffle[self.player]: # add gestures to pool like normal
- for i, data in Items.gesture_item_table.items():
- pool.append(self.add_item(data["name"], data["classification"], i))
+ if not self.options.gesture_shuffle: # add gestures to pool like normal
+ for item in Items.gesture_item_table.values():
+ pool.append(self.create_item(item["name"]))
# add '10 Bones' items if medallion shuffle is enabled
- if self.multiworld.medallion_shuffle[self.player]:
- for i, data in Items.medallion_item_table.items():
- for j in range(data["count"]):
- pool.append(self.add_item(data["name"], data["classification"], i))
+ if self.options.medallion_shuffle:
+ for item in Items.medallion_item_table.values():
+ for _ in range(item["count"]):
+ pool.append(self.create_item(item["name"]))
# add to world's pool
self.multiworld.itempool += pool
@@ -104,60 +84,57 @@ def create_items(self):
def pre_fill(self):
# handle gesture shuffle options
- if self.multiworld.gesture_shuffle[self.player] == 2: # vanilla locations
+ if self.options.gesture_shuffle == 2: # vanilla locations
gestures = Items.gesture_item_table
self.multiworld.get_location("Waynehouse: TV", self.player)\
- .place_locked_item(self.add_item(gestures[200678]["name"], gestures[200678]["classification"], 200678))
+ .place_locked_item(self.create_item("POROMER BLEB"))
self.multiworld.get_location("Afterlife: TV", self.player)\
- .place_locked_item(self.add_item(gestures[200683]["name"], gestures[200683]["classification"], 200683))
+ .place_locked_item(self.create_item("TELEDENUDATE"))
self.multiworld.get_location("New Muldul: TV", self.player)\
- .place_locked_item(self.add_item(gestures[200679]["name"], gestures[200679]["classification"], 200679))
+ .place_locked_item(self.create_item("SOUL CRISPER"))
self.multiworld.get_location("Viewax's Edifice: TV", self.player)\
- .place_locked_item(self.add_item(gestures[200680]["name"], gestures[200680]["classification"], 200680))
+ .place_locked_item(self.create_item("TIME SIGIL"))
self.multiworld.get_location("TV Island: TV", self.player)\
- .place_locked_item(self.add_item(gestures[200681]["name"], gestures[200681]["classification"], 200681))
+ .place_locked_item(self.create_item("CHARGE UP"))
self.multiworld.get_location("Juice Ranch: TV", self.player)\
- .place_locked_item(self.add_item(gestures[200682]["name"], gestures[200682]["classification"], 200682))
+ .place_locked_item(self.create_item("FATE SANDBOX"))
self.multiworld.get_location("Foglast: TV", self.player)\
- .place_locked_item(self.add_item(gestures[200684]["name"], gestures[200684]["classification"], 200684))
+ .place_locked_item(self.create_item("LINK MOLLUSC"))
self.multiworld.get_location("Drill Castle: TV", self.player)\
- .place_locked_item(self.add_item(gestures[200688]["name"], gestures[200688]["classification"], 200688))
+ .place_locked_item(self.create_item("NEMATODE INTERFACE"))
self.multiworld.get_location("Sage Airship: TV", self.player)\
- .place_locked_item(self.add_item(gestures[200685]["name"], gestures[200685]["classification"], 200685))
+ .place_locked_item(self.create_item("BOMBO - GENESIS"))
- elif self.multiworld.gesture_shuffle[self.player] == 1: # TVs only
- gestures = list(Items.gesture_item_table.items())
- tvs = list(Locations.tv_location_table.items())
+ elif self.options.gesture_shuffle == 1: # TVs only
+ gestures = [gesture["name"] for gesture in Items.gesture_item_table.values()]
+ tvs = [tv["name"] for tv in Locations.tv_location_table.values()]
# if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get
- # placed at Sage Airship: 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":
- 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"],
- gestures[gest]))
- gestures.remove(gestures[gest])
+ # placed at Sage Airship: TV or Foglast: TV
+ if self.options.extra_items_in_logic:
+ tv = self.random.choice(tvs)
+ while tv == "Sage Airship: TV" or tv == "Foglast: TV":
+ tv = self.random.choice(tvs)
+ self.multiworld.get_location(tv, self.player)\
+ .place_locked_item(self.create_item("CHARGE UP"))
+ gestures.remove("CHARGE UP")
tvs.remove(tv)
- for i in range(len(gestures)):
- 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]))
- gestures.remove(gest)
- tvs.remove(tv)
+ self.random.shuffle(gestures)
+ self.random.shuffle(tvs)
+ while gestures:
+ gesture = gestures.pop()
+ tv = tvs.pop()
+ self.get_location(tv).place_locked_item(self.create_item(gesture))
def fill_slot_data(self) -> Dict[str, Any]:
slot_data: Dict[str, Any] = {
- "party_shuffle": self.multiworld.party_shuffle[self.player].value,
- "medallion_shuffle": self.multiworld.medallion_shuffle[self.player].value,
- "random_start" : self.multiworld.random_start[self.player].value,
- "start_location" : self.start_location,
- "death_link": self.multiworld.death_link[self.player].value
+ "party_shuffle": self.options.party_shuffle.value,
+ "medallion_shuffle": self.options.medallion_shuffle.value,
+ "random_start": int(self.options.start_location != "waynehouse"),
+ "start_location" : self.options.start_location.current_option_name,
+ "death_link": self.options.death_link.value
}
return slot_data
@@ -193,16 +170,16 @@ def create_regions(self) -> None:
if j == i:
for k in exits:
# create entrance and connect it to parent and destination regions
- ent = Entrance(self.player, k, reg)
+ ent = Entrance(self.player, f"{reg.name} {k}", reg)
reg.exits.append(ent)
- if k == "New Game" and self.multiworld.random_start[self.player]:
- if self.start_location == "Waynehouse":
+ if k == "New Game":
+ if self.options.start_location == "waynehouse":
ent.connect(region_table[2])
- elif self.start_location == "Viewax's Edifice":
+ elif self.options.start_location == "viewaxs_edifice":
ent.connect(region_table[6])
- elif self.start_location == "TV Island":
+ elif self.options.start_location == "tv_island":
ent.connect(region_table[9])
- elif self.start_location == "Shield Facility":
+ elif self.options.start_location == "shield_facility":
ent.connect(region_table[11])
else:
for name, num in Exits.exit_lookup_table.items():
@@ -218,13 +195,13 @@ def create_regions(self) -> None:
.append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]]))
# add party member locations if option is enabled
- if self.multiworld.party_shuffle[self.player]:
+ if self.options.party_shuffle:
for i, data in Locations.party_location_table.items():
region_table[data["region"]].locations\
.append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]]))
# add medallion locations if option is enabled
- if self.multiworld.medallion_shuffle[self.player]:
+ if self.options.medallion_shuffle:
for i, data in Locations.medallion_location_table.items():
region_table[data["region"]].locations\
.append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]]))
@@ -232,8 +209,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/hylics2/docs/en_Hylics 2.md b/worlds/hylics2/docs/en_Hylics 2.md
index cb201a52bb3e..4ebd0742eba5 100644
--- a/worlds/hylics2/docs/en_Hylics 2.md
+++ b/worlds/hylics2/docs/en_Hylics 2.md
@@ -1,8 +1,8 @@
# Hylics 2
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
+The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
## What does randomization do to this game?
diff --git a/worlds/kdl3/Aesthetics.py b/worlds/kdl3/Aesthetics.py
new file mode 100644
index 000000000000..8c7363908f52
--- /dev/null
+++ b/worlds/kdl3/Aesthetics.py
@@ -0,0 +1,434 @@
+import struct
+from .Options import KirbyFlavorPreset, GooeyFlavorPreset
+
+kirby_flavor_presets = {
+ 1: {
+ "1": "B50029",
+ "2": "FF91C6",
+ "3": "B0123B",
+ "4": "630F0F",
+ "5": "D60052",
+ "6": "DE4873",
+ "7": "D07880",
+ "8": "000000",
+ "9": "F770A5",
+ "10": "E01784",
+ "11": "CA4C74",
+ "12": "A7443F",
+ "13": "FF1784",
+ "14": "FFA1DE",
+ "15": "B03830",
+ },
+ 2: {
+ "1": "C70057",
+ "2": "FF3554",
+ "3": "AA0040",
+ "4": "C02D47",
+ "5": "E02068",
+ "6": "C2183F",
+ "7": "D03F80",
+ "8": "872939",
+ "9": "E82B47",
+ "10": "E80067",
+ "11": "D52F40",
+ "12": "9F1C33",
+ "13": "FD187F",
+ "14": "F85068",
+ "15": "D2386F",
+ },
+ 3: {
+ "1": "5858e2",
+ "2": "e6e6fa",
+ "3": "bcbcf2",
+ "4": "8484e6",
+ "5": "2929ec",
+ "6": "b5b5f0",
+ "7": "847bd6",
+ "8": "3232d6",
+ "9": "d6d6ef",
+ "10": "4a52ef",
+ "11": "c6c6e6",
+ "12": "4343ad",
+ "13": "6767ff",
+ "14": "f6f6fd",
+ "15": "3139b6",
+ },
+ 4: {
+ "1": "B01810",
+ "2": "F0E08D",
+ "3": "C8A060",
+ "4": "A87043",
+ "5": "E03700",
+ "6": "EFC063",
+ "7": "D07818",
+ "8": "A8501C",
+ "9": "E8D070",
+ "10": "E2501E",
+ "11": "E8C55C",
+ "12": "B08833",
+ "13": "E8783B",
+ "14": "F8F8A5",
+ "15": "B03800",
+ },
+ 5: {
+ "1": "9F4410",
+ "2": "88F27B",
+ "3": "57A044",
+ "4": "227029",
+ "5": "C75418",
+ "6": "57BA23",
+ "7": "1C6B00",
+ "8": "2D6823",
+ "9": "3FD744",
+ "10": "E06C16",
+ "11": "54C053",
+ "12": "1A541E",
+ "13": "F06B10",
+ "14": "98F89A",
+ "15": "B05830",
+ },
+ 6: {
+ "1": "7C1060",
+ "2": "CA8AE8",
+ "3": "8250A5",
+ "4": "604B7B",
+ "5": "A52068",
+ "6": "8D64B8",
+ "7": "B73B80",
+ "8": "672D9A",
+ "9": "BA82D5",
+ "10": "B55098",
+ "11": "9F5CCF",
+ "12": "632B74",
+ "13": "CF78B5",
+ "14": "DA98F8",
+ "15": "8D3863",
+ },
+ 7: {
+ "1": "6F1410",
+ "2": "C2735C",
+ "3": "5C351C",
+ "4": "875440",
+ "5": "9F2F0C",
+ "6": "874C3B",
+ "7": "88534C",
+ "8": "4C1E00",
+ "9": "B06458",
+ "10": "921C16",
+ "11": "9F5C54",
+ "12": "5B3125",
+ "13": "C01A14",
+ "14": "CF785B",
+ "15": "6B3125",
+ },
+ 8: {
+ "1": "a6a6a6",
+ "2": "e6e6e6",
+ "3": "bcbcbc",
+ "4": "848484",
+ "5": "909090",
+ "6": "b5b5b5",
+ "7": "848484",
+ "8": "646464",
+ "9": "d6d6d6",
+ "10": "525252",
+ "11": "c6c6c6",
+ "12": "737373",
+ "13": "949494",
+ "14": "f6f6f6",
+ "15": "545454",
+ },
+ 9: {
+ "1": "400000",
+ "2": "6B6B6B",
+ "3": "2B2B2B",
+ "4": "181818",
+ "5": "640000",
+ "6": "3D3D3D",
+ "7": "878787",
+ "8": "020202",
+ "9": "606060",
+ "10": "980000",
+ "11": "505050",
+ "12": "474747",
+ "13": "C80000",
+ "14": "808080",
+ "15": "AF0000",
+ },
+ 10: {
+ "1": "2B4B10",
+ "2": "EF8A9D",
+ "3": "C84F6B",
+ "4": "B74F54",
+ "5": "126018",
+ "6": "D85F6F",
+ "7": "D06870",
+ "8": "A24858",
+ "9": "E77B8D",
+ "10": "168025",
+ "11": "DF5C68",
+ "12": "9D4353",
+ "13": "48953F",
+ "14": "F897AD",
+ "15": "B03830",
+ },
+ 11: {
+ "1": "7B290C",
+ "2": "FF9A00",
+ "3": "B05C1C",
+ "4": "8F3F0E",
+ "5": "D23B0C",
+ "6": "E08200",
+ "7": "D05800",
+ "8": "8A2B16",
+ "9": "EF970A",
+ "10": "E24800",
+ "11": "E58F00",
+ "12": "A03700",
+ "13": "ED3B00",
+ "14": "FFAF27",
+ "15": "A84700",
+ },
+ 12: {
+ "1": "AFA810",
+ "2": "4FF29D",
+ "3": "2BA04C",
+ "4": "007043",
+ "5": "C7C218",
+ "6": "33BA5F",
+ "7": "006B40",
+ "8": "2D6823",
+ "9": "1CD773",
+ "10": "E0CF16",
+ "11": "2DC06C",
+ "12": "00543F",
+ "13": "F0F010",
+ "14": "43F8B2",
+ "15": "B0A230",
+ },
+ 13: {
+ "1": "7C73B0",
+ "2": "CACAE7",
+ "3": "7B7BA8",
+ "4": "5F5FA7",
+ "5": "B57EDC",
+ "6": "8585C5",
+ "7": "5B5B82",
+ "8": "474796",
+ "9": "B2B2D8",
+ "10": "B790EF",
+ "11": "9898C2",
+ "12": "6B6BB7",
+ "13": "CDADFA",
+ "14": "E6E6FA",
+ "15": "976FBD",
+ },
+}
+
+gooey_flavor_presets = {
+ 1: {
+ "1": "CD539D",
+ "2": "D270AD",
+ "3": "F27CBF",
+ "4": "FF91C6",
+ "5": "FFA1DE",
+ "6": "B51810",
+ "7": "EF524A",
+ "8": "D6C6C6",
+ "9": "FFFFFF",
+ },
+ 2: {
+ "1": "161600",
+ "2": "592910",
+ "3": "5A3118",
+ "4": "AB3918",
+ "5": "EB3918",
+ "6": "B51810",
+ "7": "EF524A",
+ "8": "D6C6C6",
+ "9": "FFFFFF",
+ },
+ 3: {
+ "1": "001616",
+ "2": "102959",
+ "3": "18315A",
+ "4": "1839AB",
+ "5": "1839EB",
+ "6": "B51810",
+ "7": "EF524A",
+ "8": "D6C6C6",
+ "9": "FFFFFF",
+ },
+ 4: {
+ "1": "C8A031",
+ "2": "C5BD38",
+ "3": "D2CD48",
+ "4": "E2E040",
+ "5": "EAE2A0",
+ "6": "B51810",
+ "7": "EF524A",
+ "8": "D6C6C6",
+ "9": "FFFFFF",
+ },
+ 5: {
+ "1": "54A208",
+ "2": "5CB021",
+ "3": "6CB206",
+ "4": "8AC54C",
+ "5": "8DD554",
+ "6": "B51810",
+ "7": "EF524A",
+ "8": "D6C6C6",
+ "9": "FFFFFF",
+ },
+ 6: {
+ "1": "3D083D",
+ "2": "4B024B",
+ "3": "4C104C",
+ "4": "5F0A5F",
+ "5": "9F1D9F",
+ "6": "B51810",
+ "7": "EF524A",
+ "8": "D6C6C6",
+ "9": "FFFFFF",
+ },
+ 7: {
+ "1": "270C08",
+ "2": "481C10",
+ "3": "581E10",
+ "4": "5B2712",
+ "5": "743316",
+ "6": "B51810",
+ "7": "EF524A",
+ "8": "D6C6C6",
+ "9": "FFFFFF",
+ },
+ 8: {
+ "1": "7F7F7F",
+ "2": "909090",
+ "3": "9D9D9D",
+ "4": "BFBFBF",
+ "5": "D2D2D2",
+ "6": "B51810",
+ "7": "EF524A",
+ "8": "D6C6C6",
+ "9": "FFFFFF",
+ },
+ 9: {
+ "1": "141414",
+ "2": "2D2D2D",
+ "3": "404040",
+ "4": "585858",
+ "5": "7F7F7F",
+ "6": "B51810",
+ "7": "EF524A",
+ "8": "D6C6C6",
+ "9": "FFFFFF",
+ },
+ 10: {
+ "1": "954353",
+ "2": "AF4F68",
+ "3": "CD6073",
+ "4": "E06774",
+ "5": "E587A2",
+ "6": "17AF10",
+ "7": "4FE748",
+ "8": "D6C6C6",
+ "9": "FFFFFF",
+ },
+ 11: {
+ "1": "CF4700",
+ "2": "D85C08",
+ "3": "E26C04",
+ "4": "EA7B16",
+ "5": "EF8506",
+ "6": "B51810",
+ "7": "EF524A",
+ "8": "D6C6C6",
+ "9": "FFFFFF",
+ },
+ 12: {
+ "1": "1C4708",
+ "2": "105B1C",
+ "3": "186827",
+ "4": "187C3B",
+ "5": "188831",
+ "6": "B51810",
+ "7": "EF524A",
+ "8": "D6C6C6",
+ "9": "FFFFFF",
+ },
+ 13: {
+ "1": "501E70",
+ "2": "673B87",
+ "3": "7848A7",
+ "4": "9067C7",
+ "5": "B57EDC",
+ "6": "B51810",
+ "7": "EF524A",
+ "8": "D6C6C6",
+ "9": "FFFFFF",
+ },
+}
+
+kirby_target_palettes = {
+ 0x64646: (["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], 0, 1),
+ 0x64846: (["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], 0, 1),
+ 0x1E007E: (["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], 0, 1),
+ 0x1E009C: (["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], 0, 0.5),
+ 0x1E00F6: (["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], 0, 1),
+ 0x1E0114: (["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], 0, 0.5),
+ 0x1E0216: (["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], 0, 1),
+ 0x1E0234: (["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], 0, 0.5),
+ 0x1E0486: (["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], 0, 1),
+ 0x1E04A4: (["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], 0, 0.5),
+}
+
+gooey_target_palettes = {
+ 0x604C2: (["1", "2", "3", "4", "5", "6", "7", "8", "9"], 0, 1),
+ 0x64592: (["1", "2", "3", "4", "5", "6", "7", "8", "9"], 0, 1),
+ 0x64692: (["1", "2", "3", "4", "5", "6", "7", "8", "9"], 0, 1),
+ 0x64892: (["1", "2", "3", "4", "5", "6", "7", "8", "9"], 0, 1),
+ 0x1E02CA: (["1", "2", "3", "4", "5", "6", "7", "8", "9"], 0, 1),
+ 0x1E0342: (["1", "2", "3", "4", "5", "6", "7", "8", "9"], 0, 1),
+ 0x1E05A6: (["1", "2", "3", "4", "5", "6", "7", "8", "9"], 0, 1),
+ 0x1E05B8: (["1", "2", "3", "4", "5", "6", "7", "8", "9"], 0, 0.5),
+ 0x1E0636: (["1", "2", "3", "4", "5", "6", "7", "8", "9"], 0, 1),
+ 0x1E065A: (["1", "2", "3", "4", "5", "6", "7", "8", "9"], 0, 1.5),
+}
+
+
+def get_kirby_palette(world):
+ palette = world.options.kirby_flavor_preset.value
+ if palette == KirbyFlavorPreset.option_custom:
+ return world.options.kirby_flavor.value
+ return kirby_flavor_presets.get(palette, None)
+
+
+def get_gooey_palette(world):
+ palette = world.options.gooey_flavor_preset.value
+ if palette == GooeyFlavorPreset.option_custom:
+ return world.options.gooey_flavor.value
+ return gooey_flavor_presets.get(palette, None)
+
+
+def rgb888_to_bgr555(red, green, blue) -> bytes:
+ red = red >> 3
+ green = green >> 3
+ blue = blue >> 3
+ outcol = (blue << 10) + (green << 5) + red
+ return struct.pack("H", outcol)
+
+
+def get_palette_bytes(palette, target, offset, factor):
+ output_data = bytearray()
+ for color in target:
+ hexcol = palette[color]
+ if hexcol.startswith("#"):
+ hexcol = hexcol.replace("#", "")
+ colint = int(hexcol, 16)
+ col = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF)
+ col = tuple(int(int(factor*x) + offset) for x in col)
+ byte_data = rgb888_to_bgr555(col[0], col[1], col[2])
+ output_data.extend(bytearray(byte_data))
+ return output_data
diff --git a/worlds/kdl3/Client.py b/worlds/kdl3/Client.py
new file mode 100644
index 000000000000..1ca21d550e67
--- /dev/null
+++ b/worlds/kdl3/Client.py
@@ -0,0 +1,429 @@
+import logging
+import struct
+import time
+import typing
+import uuid
+from struct import unpack, pack
+from collections import defaultdict
+import random
+
+from MultiServer import mark_raw
+from NetUtils import ClientStatus, color
+from Utils import async_start
+from worlds.AutoSNIClient import SNIClient
+from .Locations import boss_locations
+from .Gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes
+from .ClientAddrs import consumable_addrs, star_addrs
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from SNIClient import SNIClientCommandProcessor
+
+snes_logger = logging.getLogger("SNES")
+
+# FXPAK Pro protocol memory mapping used by SNI
+ROM_START = 0x000000
+SRAM_1_START = 0xE00000
+
+# KDL3
+KDL3_HALKEN = SRAM_1_START + 0x80F0
+KDL3_NINTEN = SRAM_1_START + 0x8FF0
+KDL3_ROMNAME = SRAM_1_START + 0x8100
+KDL3_DEATH_LINK_ADDR = SRAM_1_START + 0x9010
+KDL3_GOAL_ADDR = SRAM_1_START + 0x9012
+KDL3_CONSUMABLE_FLAG = SRAM_1_START + 0x9018
+KDL3_STARS_FLAG = SRAM_1_START + 0x901A
+KDL3_GIFTING_FLAG = SRAM_1_START + 0x901C
+KDL3_LEVEL_ADDR = SRAM_1_START + 0x9020
+KDL3_IS_DEMO = SRAM_1_START + 0x5AD5
+KDL3_GAME_SAVE = SRAM_1_START + 0x3617
+KDL3_CURRENT_WORLD = SRAM_1_START + 0x363F
+KDL3_CURRENT_LEVEL = SRAM_1_START + 0x3641
+KDL3_GAME_STATE = SRAM_1_START + 0x36D0
+KDL3_LIFE_COUNT = SRAM_1_START + 0x39CF
+KDL3_KIRBY_HP = SRAM_1_START + 0x39D1
+KDL3_BOSS_HP = SRAM_1_START + 0x39D5
+KDL3_STAR_COUNT = SRAM_1_START + 0x39D7
+KDL3_LIFE_VISUAL = SRAM_1_START + 0x39E3
+KDL3_HEART_STARS = SRAM_1_START + 0x53A7
+KDL3_WORLD_UNLOCK = SRAM_1_START + 0x53CB
+KDL3_LEVEL_UNLOCK = SRAM_1_START + 0x53CD
+KDL3_BOSS_STATUS = SRAM_1_START + 0x53D5
+KDL3_INVINCIBILITY_TIMER = SRAM_1_START + 0x54B1
+KDL3_MG5_STATUS = SRAM_1_START + 0x5EE4
+KDL3_BOSS_BUTCH_STATUS = SRAM_1_START + 0x5EEA
+KDL3_JUMPING_STATUS = SRAM_1_START + 0x5EF0
+KDL3_CURRENT_BGM = SRAM_1_START + 0x733E
+KDL3_SOUND_FX = SRAM_1_START + 0x7F62
+KDL3_ANIMAL_FRIENDS = SRAM_1_START + 0x8000
+KDL3_ABILITY_ARRAY = SRAM_1_START + 0x8020
+KDL3_RECV_COUNT = SRAM_1_START + 0x8050
+KDL3_HEART_STAR_COUNT = SRAM_1_START + 0x8070
+KDL3_GOOEY_TRAP = SRAM_1_START + 0x8080
+KDL3_SLOWNESS_TRAP = SRAM_1_START + 0x8082
+KDL3_ABILITY_TRAP = SRAM_1_START + 0x8084
+KDL3_GIFTING_SEND = SRAM_1_START + 0x8086
+KDL3_COMPLETED_STAGES = SRAM_1_START + 0x8200
+KDL3_CONSUMABLES = SRAM_1_START + 0xA000
+KDL3_STARS = SRAM_1_START + 0xB000
+KDL3_ITEM_QUEUE = SRAM_1_START + 0xC000
+
+deathlink_messages = defaultdict(lambda: " was defeated.", {
+ 0x0200: " was bonked by apples from Whispy Woods.",
+ 0x0201: " was out-maneuvered by Acro.",
+ 0x0202: " was out-numbered by Pon & Con.",
+ 0x0203: " was defeated by Ado's powerful paintings.",
+ 0x0204: " was clobbered by King Dedede.",
+ 0x0205: " lost their battle against Dark Matter.",
+ 0x0300: " couldn't overcome the Boss Butch.",
+ 0x0400: " is bad at jumping.",
+})
+
+
+@mark_raw
+def cmd_gift(self: "SNIClientCommandProcessor"):
+ """Toggles gifting for the current game."""
+ if not getattr(self.ctx, "gifting", None):
+ self.ctx.gifting = True
+ else:
+ self.ctx.gifting = not self.ctx.gifting
+ self.output(f"Gifting set to {self.ctx.gifting}")
+ async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", {
+ f"{self.ctx.slot}":
+ {
+ "IsOpen": self.ctx.gifting,
+ **kdl3_gifting_options
+ }
+ }))
+
+
+class KDL3SNIClient(SNIClient):
+ game = "Kirby's Dream Land 3"
+ patch_suffix = ".apkdl3"
+ levels = None
+ consumables = None
+ stars = None
+ item_queue: typing.List = []
+ initialize_gifting = False
+ giftbox_key: str = ""
+ motherbox_key: str = ""
+ client_random: random.Random = random.Random()
+
+ async def deathlink_kill_player(self, ctx) -> None:
+ from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
+ game_state = await snes_read(ctx, KDL3_GAME_STATE, 1)
+ if game_state[0] == 0xFF:
+ return # despite how funny it is, don't try to kill Kirby in a menu
+
+ current_stage = await snes_read(ctx, KDL3_CURRENT_LEVEL, 1)
+ if current_stage[0] == 0x7: # boss stage
+ boss_hp = await snes_read(ctx, KDL3_BOSS_HP, 1)
+ if boss_hp[0] == 0:
+ return # receiving a deathlink after defeating a boss has softlock potential
+
+ current_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1)
+ if current_hp[0] == 0:
+ return # don't kill Kirby while he's already dead
+ snes_buffered_write(ctx, KDL3_KIRBY_HP, bytes([0x00]))
+
+ await snes_flush_writes(ctx)
+
+ ctx.death_state = DeathState.dead
+ ctx.last_death_link = time.time()
+
+ async def validate_rom(self, ctx) -> bool:
+ from SNIClient import snes_read
+ rom_name = await snes_read(ctx, KDL3_ROMNAME, 0x15)
+ if rom_name is None or rom_name == bytes([0] * 0x15) or rom_name[:4] != b"KDL3":
+ if "gift" in ctx.command_processor.commands:
+ ctx.command_processor.commands.pop("gift")
+ return False
+
+ ctx.game = self.game
+ ctx.rom = rom_name
+ ctx.items_handling = 0b111 # always remote items
+ ctx.allow_collect = True
+ if "gift" not in ctx.command_processor.commands:
+ ctx.command_processor.commands["gift"] = cmd_gift
+
+ death_link = await snes_read(ctx, KDL3_DEATH_LINK_ADDR, 1)
+ if death_link:
+ await ctx.update_death_link(bool(death_link[0] & 0b1))
+ return True
+
+ async def pop_item(self, ctx, in_stage):
+ from SNIClient import snes_buffered_write, snes_read
+ if len(self.item_queue) > 0:
+ item = self.item_queue.pop()
+ if not in_stage and item & 0xC0:
+ # can't handle this item right now, send it to the back and return to handle the rest
+ self.item_queue.append(item)
+ return
+ ingame_queue = list(unpack("HHHHHHHH", await snes_read(ctx, KDL3_ITEM_QUEUE, 16)))
+ for i in range(len(ingame_queue)):
+ if ingame_queue[i] == 0x00:
+ ingame_queue[i] = item
+ snes_buffered_write(ctx, KDL3_ITEM_QUEUE, pack("HHHHHHHH", *ingame_queue))
+ break
+ else:
+ self.item_queue.append(item) # no more slots, get it next go around
+
+ async def pop_gift(self, ctx):
+ if ctx.stored_data[self.giftbox_key]:
+ from SNIClient import snes_read, snes_buffered_write
+ key, gift = ctx.stored_data[self.giftbox_key].popitem()
+ await pop_object(ctx, self.giftbox_key, key)
+ # first, special cases
+ traits = [trait["Trait"] for trait in gift["Traits"]]
+ if "Candy" in traits or "Invincible" in traits:
+ # apply invincibility candy
+ self.item_queue.append(0x43)
+ elif "Tomato" in traits or "tomato" in gift["ItemName"].lower():
+ # apply maxim tomato
+ # only want tomatos here, no other vegetable is that good
+ self.item_queue.append(0x42)
+ elif "Life" in traits:
+ # Apply 1-Up
+ self.item_queue.append(0x41)
+ elif "Currency" in traits or "Star" in traits:
+ value = gift["ItemValue"]
+ if value >= 50000:
+ self.item_queue.append(0x46)
+ elif value >= 30000:
+ self.item_queue.append(0x45)
+ else:
+ self.item_queue.append(0x44)
+ elif "Trap" in traits:
+ # find the best trap to apply
+ if "Goo" in traits or "Gel" in traits:
+ self.item_queue.append(0x80)
+ elif "Slow" in traits or "Slowness" in traits:
+ self.item_queue.append(0x81)
+ elif "Eject" in traits or "Removal" in traits:
+ self.item_queue.append(0x82)
+ else:
+ # just deal damage to Kirby
+ kirby_hp = struct.unpack("H", await snes_read(ctx, KDL3_KIRBY_HP, 2))[0]
+ snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", max(kirby_hp - 1, 0)))
+ else:
+ # check if it's tasty
+ if any(x in traits for x in ["Consumable", "Food", "Drink", "Heal", "Health"]):
+ # it's tasty!, use quality to decide how much to heal
+ quality = max((trait["Quality"] for trait in gift["Traits"]
+ if trait["Trait"] in ["Consumable", "Food", "Drink", "Heal", "Health"]))
+ quality = min(10, quality * 2)
+ else:
+ # it's not really edible, but he'll eat it anyway
+ quality = self.client_random.choices(range(0, 2), {0: 75, 1: 25})[0]
+ kirby_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1)
+ gooey_hp = await snes_read(ctx, KDL3_KIRBY_HP + 2, 1)
+ snes_buffered_write(ctx, KDL3_SOUND_FX, bytes([0x26]))
+ if gooey_hp[0] > 0x00:
+ snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality // 2, 8)))
+ snes_buffered_write(ctx, KDL3_KIRBY_HP + 2, struct.pack("H", min(gooey_hp[0] + quality // 2, 8)))
+ else:
+ snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality, 10)))
+
+ async def pick_gift_recipient(self, ctx, gift):
+ if gift != 4:
+ gift_base = kdl3_gifts[gift]
+ else:
+ gift_base = kdl3_trap_gifts[self.client_random.randint(0, 3)]
+ most_applicable = -1
+ most_applicable_slot = ctx.slot
+ for slot, info in ctx.stored_data[self.motherbox_key].items():
+ if int(slot) == ctx.slot and len(ctx.stored_data[self.motherbox_key]) > 1:
+ continue
+ desire = len(set(info["DesiredTraits"]).intersection([trait["Trait"] for trait in gift_base["Traits"]]))
+ if desire > most_applicable:
+ most_applicable = desire
+ most_applicable_slot = int(slot)
+ elif most_applicable_slot == ctx.slot and info["AcceptsAnyGift"]:
+ # only send to ourselves if no one else will take it
+ most_applicable_slot = int(slot)
+ # print(most_applicable, most_applicable_slot)
+ item_uuid = uuid.uuid4().hex
+ item = {
+ **gift_base,
+ "ID": item_uuid,
+ "Sender": ctx.player_names[ctx.slot],
+ "Receiver": ctx.player_names[most_applicable_slot],
+ "SenderTeam": ctx.team,
+ "ReceiverTeam": ctx.team, # for the moment
+ "IsRefund": False
+ }
+ # print(item)
+ await update_object(ctx, f"Giftbox;{ctx.team};{most_applicable_slot}", {
+ item_uuid: item,
+ })
+
+ async def game_watcher(self, ctx) -> None:
+ try:
+ from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
+ rom = await snes_read(ctx, KDL3_ROMNAME, 0x15)
+ if rom != ctx.rom:
+ ctx.rom = None
+ halken = await snes_read(ctx, KDL3_HALKEN, 6)
+ if halken != b"halken":
+ return
+ ninten = await snes_read(ctx, KDL3_NINTEN, 6)
+ if ninten != b"ninten":
+ return
+ if not ctx.slot:
+ return
+ if not self.initialize_gifting:
+ self.giftbox_key = f"Giftbox;{ctx.team};{ctx.slot}"
+ self.motherbox_key = f"Giftboxes;{ctx.team}"
+ enable_gifting = await snes_read(ctx, KDL3_GIFTING_FLAG, 0x01)
+ await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key, bool(enable_gifting[0]))
+ self.initialize_gifting = True
+ # can't check debug anymore, without going and copying the value. might be important later.
+ if self.levels is None:
+ self.levels = dict()
+ for i in range(5):
+ level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14)
+ self.levels[i] = unpack("HHHHHHH", level_data)
+ self.levels[5] = [0x0205, # Hyper Zone
+ 0, # MG-5, can't send from here
+ 0x0300, # Boss Butch
+ 0x0400, # Jumping
+ 0, 0, 0]
+
+ if self.consumables is None:
+ consumables = await snes_read(ctx, KDL3_CONSUMABLE_FLAG, 1)
+ self.consumables = consumables[0] == 0x01
+ if self.stars is None:
+ stars = await snes_read(ctx, KDL3_STARS_FLAG, 1)
+ self.stars = stars[0] == 0x01
+ is_demo = await snes_read(ctx, KDL3_IS_DEMO, 1)
+ # 1 - recording a demo, 2 - playing back recorded, 3+ is a demo
+ if is_demo[0] > 0x00:
+ return
+ current_save = await snes_read(ctx, KDL3_GAME_SAVE, 1)
+ goal = await snes_read(ctx, KDL3_GOAL_ADDR, 1)
+ boss_butch_status = await snes_read(ctx, KDL3_BOSS_BUTCH_STATUS + (current_save[0] * 2), 1)
+ mg5_status = await snes_read(ctx, KDL3_MG5_STATUS + (current_save[0] * 2), 1)
+ jumping_status = await snes_read(ctx, KDL3_JUMPING_STATUS + (current_save[0] * 2), 1)
+ if boss_butch_status[0] == 0xFF:
+ return # save file is not created, ignore
+ if (goal[0] == 0x00 and boss_butch_status[0] == 0x01) \
+ or (goal[0] == 0x01 and boss_butch_status[0] == 0x03) \
+ or (goal[0] == 0x02 and mg5_status[0] == 0x03) \
+ or (goal[0] == 0x03 and jumping_status[0] == 0x03):
+ await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
+ ctx.finished_game = True
+ current_bgm = await snes_read(ctx, KDL3_CURRENT_BGM, 1)
+ if current_bgm[0] in (0x00, 0x21, 0x22, 0x23, 0x25, 0x2A, 0x2B):
+ return # null, title screen, opening, save select, true and false endings
+ game_state = await snes_read(ctx, KDL3_GAME_STATE, 1)
+ if "DeathLink" in ctx.tags and game_state[0] == 0x00 and ctx.last_death_link + 1 < time.time():
+ current_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1)
+ current_world = struct.unpack("H", await snes_read(ctx, KDL3_CURRENT_WORLD, 2))[0]
+ current_level = struct.unpack("H", await snes_read(ctx, KDL3_CURRENT_LEVEL, 2))[0]
+ currently_dead = current_hp[0] == 0x00
+ message = deathlink_messages[self.levels[current_world][current_level]]
+ await ctx.handle_deathlink_state(currently_dead, f"{ctx.player_names[ctx.slot]}{message}")
+
+ recv_count = await snes_read(ctx, KDL3_RECV_COUNT, 2)
+ recv_amount = unpack("H", recv_count)[0]
+ if recv_amount < len(ctx.items_received):
+ item = ctx.items_received[recv_amount]
+ recv_amount += 1
+ logging.info('Received %s from %s (%s) (%d/%d in list)' % (
+ color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'),
+ color(ctx.player_names[item.player], 'yellow'),
+ ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received)))
+
+ snes_buffered_write(ctx, KDL3_RECV_COUNT, pack("H", recv_amount))
+ item_idx = item.item & 0x00000F
+ if item.item & 0x000070 == 0:
+ self.item_queue.append(item_idx | 0x10)
+ elif item.item & 0x000010 > 0:
+ self.item_queue.append(item_idx | 0x20)
+ elif item.item & 0x000020 > 0:
+ # Positive
+ self.item_queue.append(item_idx | 0x40)
+ elif item.item & 0x000040 > 0:
+ self.item_queue.append(item_idx | 0x80)
+
+ # handle gifts here
+ gifting_status = await snes_read(ctx, KDL3_GIFTING_FLAG, 0x01)
+ if hasattr(ctx, "gifting") and ctx.gifting:
+ if gifting_status[0]:
+ gift = await snes_read(ctx, KDL3_GIFTING_SEND, 0x01)
+ if gift[0]:
+ # we have a gift to send
+ await self.pick_gift_recipient(ctx, gift[0])
+ snes_buffered_write(ctx, KDL3_GIFTING_SEND, bytes([0x00]))
+ else:
+ snes_buffered_write(ctx, KDL3_GIFTING_FLAG, bytes([0x01]))
+ else:
+ if gifting_status[0]:
+ snes_buffered_write(ctx, KDL3_GIFTING_FLAG, bytes([0x00]))
+
+ await snes_flush_writes(ctx)
+
+ new_checks = []
+ # level completion status
+ world_unlocks = await snes_read(ctx, KDL3_WORLD_UNLOCK, 1)
+ if world_unlocks[0] > 0x06:
+ return # save is not loaded, ignore
+ stages_raw = await snes_read(ctx, KDL3_COMPLETED_STAGES, 60)
+ stages = struct.unpack("HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", stages_raw)
+ for i in range(30):
+ loc_id = 0x770000 + i + 1
+ if stages[i] == 1 and loc_id not in ctx.checked_locations:
+ new_checks.append(loc_id)
+ elif loc_id in ctx.checked_locations:
+ snes_buffered_write(ctx, KDL3_COMPLETED_STAGES + (i * 2), struct.pack("H", 1))
+
+ # heart star status
+ heart_stars = await snes_read(ctx, KDL3_HEART_STARS, 35)
+ for i in range(5):
+ start_ind = i * 7
+ for j in range(1, 7):
+ level_ind = start_ind + j - 1
+ loc_id = 0x770100 + (6 * i) + j
+ if heart_stars[level_ind] and loc_id not in ctx.checked_locations:
+ new_checks.append(loc_id)
+ elif loc_id in ctx.checked_locations:
+ snes_buffered_write(ctx, KDL3_HEART_STARS + level_ind, bytes([0x01]))
+ if self.consumables:
+ consumables = await snes_read(ctx, KDL3_CONSUMABLES, 1920)
+ for consumable in consumable_addrs:
+ # TODO: see if this can be sped up in any way
+ loc_id = 0x770300 + consumable
+ if loc_id not in ctx.checked_locations and consumables[consumable_addrs[consumable]] == 0x01:
+ new_checks.append(loc_id)
+ if self.stars:
+ stars = await snes_read(ctx, KDL3_STARS, 1920)
+ for star in star_addrs:
+ if star not in ctx.checked_locations and stars[star_addrs[star]] == 0x01:
+ new_checks.append(star)
+
+ if game_state[0] != 0xFF:
+ await self.pop_gift(ctx)
+ await self.pop_item(ctx, game_state[0] != 0xFF)
+ await snes_flush_writes(ctx)
+
+ # boss status
+ boss_flag_bytes = await snes_read(ctx, KDL3_BOSS_STATUS, 2)
+ boss_flag = unpack("H", boss_flag_bytes)[0]
+ for bitmask, boss in zip(range(1, 11, 2), boss_locations.keys()):
+ if boss_flag & (1 << bitmask) > 0 and boss not in ctx.checked_locations:
+ new_checks.append(boss)
+
+ for new_check_id in new_checks:
+ ctx.locations_checked.add(new_check_id)
+ location = ctx.location_names.lookup_in_game(new_check_id)
+ snes_logger.info(
+ f'New Check: {location} ({len(ctx.locations_checked)}/'
+ f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
+ await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
+ except Exception as ex:
+ # we crashed, so print log and clean up
+ snes_logger.error("", exc_info=ex)
+ if "gift" in ctx.command_processor.commands:
+ ctx.command_processor.commands.pop("gift")
+ ctx.rom = None
+ ctx.game = None
diff --git a/worlds/kdl3/ClientAddrs.py b/worlds/kdl3/ClientAddrs.py
new file mode 100644
index 000000000000..1f5475741b3b
--- /dev/null
+++ b/worlds/kdl3/ClientAddrs.py
@@ -0,0 +1,816 @@
+consumable_addrs = {
+ 0: 14,
+ 1: 15,
+ 2: 84,
+ 3: 138,
+ 4: 139,
+ 5: 204,
+ 6: 214,
+ 7: 215,
+ 8: 224,
+ 9: 330,
+ 10: 353,
+ 11: 458,
+ 12: 459,
+ 13: 522,
+ 14: 525,
+ 15: 605,
+ 16: 606,
+ 17: 630,
+ 18: 671,
+ 19: 672,
+ 20: 693,
+ 21: 791,
+ 22: 851,
+ 23: 883,
+ 24: 971,
+ 25: 985,
+ 26: 986,
+ 27: 1024,
+ 28: 1035,
+ 29: 1036,
+ 30: 1038,
+ 31: 1039,
+ 32: 1170,
+ 33: 1171,
+ 34: 1377,
+ 35: 1378,
+ 36: 1413,
+ 37: 1494,
+ 38: 1666,
+ 39: 1808,
+ 40: 1809,
+ 41: 1816,
+ 42: 1856,
+ 43: 1857,
+}
+
+star_addrs = {
+ 0x770401: 0,
+ 0x770402: 1,
+ 0x770403: 2,
+ 0x770404: 3,
+ 0x770405: 4,
+ 0x770406: 5,
+ 0x770407: 7,
+ 0x770408: 8,
+ 0x770409: 9,
+ 0x77040a: 10,
+ 0x77040b: 11,
+ 0x77040c: 12,
+ 0x77040d: 13,
+ 0x77040e: 16,
+ 0x77040f: 17,
+ 0x770410: 19,
+ 0x770411: 20,
+ 0x770412: 21,
+ 0x770413: 22,
+ 0x770414: 23,
+ 0x770415: 24,
+ 0x770416: 25,
+ 0x770417: 26,
+ 0x770418: 65,
+ 0x770419: 66,
+ 0x77041a: 67,
+ 0x77041b: 68,
+ 0x77041c: 69,
+ 0x77041d: 70,
+ 0x77041e: 71,
+ 0x77041f: 72,
+ 0x770420: 73,
+ 0x770421: 74,
+ 0x770422: 76,
+ 0x770423: 77,
+ 0x770424: 78,
+ 0x770425: 79,
+ 0x770426: 80,
+ 0x770427: 81,
+ 0x770428: 82,
+ 0x770429: 83,
+ 0x77042a: 85,
+ 0x77042b: 86,
+ 0x77042c: 87,
+ 0x77042d: 128,
+ 0x77042e: 129,
+ 0x77042f: 130,
+ 0x770430: 131,
+ 0x770431: 132,
+ 0x770432: 133,
+ 0x770433: 134,
+ 0x770434: 135,
+ 0x770435: 136,
+ 0x770436: 137,
+ 0x770437: 140,
+ 0x770438: 141,
+ 0x770439: 142,
+ 0x77043a: 143,
+ 0x77043b: 144,
+ 0x77043c: 145,
+ 0x77043d: 146,
+ 0x77043e: 147,
+ 0x77043f: 148,
+ 0x770440: 149,
+ 0x770441: 150,
+ 0x770442: 151,
+ 0x770443: 152,
+ 0x770444: 153,
+ 0x770445: 154,
+ 0x770446: 155,
+ 0x770447: 156,
+ 0x770448: 157,
+ 0x770449: 158,
+ 0x77044a: 159,
+ 0x77044b: 160,
+ 0x77044c: 192,
+ 0x77044d: 193,
+ 0x77044e: 194,
+ 0x77044f: 195,
+ 0x770450: 197,
+ 0x770451: 198,
+ 0x770452: 199,
+ 0x770453: 200,
+ 0x770454: 201,
+ 0x770455: 203,
+ 0x770456: 205,
+ 0x770457: 206,
+ 0x770458: 207,
+ 0x770459: 208,
+ 0x77045a: 209,
+ 0x77045b: 210,
+ 0x77045c: 211,
+ 0x77045d: 212,
+ 0x77045e: 213,
+ 0x77045f: 216,
+ 0x770460: 217,
+ 0x770461: 218,
+ 0x770462: 219,
+ 0x770463: 220,
+ 0x770464: 221,
+ 0x770465: 222,
+ 0x770466: 225,
+ 0x770467: 227,
+ 0x770468: 228,
+ 0x770469: 229,
+ 0x77046a: 230,
+ 0x77046b: 231,
+ 0x77046c: 232,
+ 0x77046d: 233,
+ 0x77046e: 234,
+ 0x77046f: 235,
+ 0x770470: 236,
+ 0x770471: 257,
+ 0x770472: 258,
+ 0x770473: 259,
+ 0x770474: 260,
+ 0x770475: 261,
+ 0x770476: 262,
+ 0x770477: 263,
+ 0x770478: 264,
+ 0x770479: 265,
+ 0x77047a: 266,
+ 0x77047b: 267,
+ 0x77047c: 268,
+ 0x77047d: 270,
+ 0x77047e: 271,
+ 0x77047f: 272,
+ 0x770480: 273,
+ 0x770481: 275,
+ 0x770482: 276,
+ 0x770483: 277,
+ 0x770484: 278,
+ 0x770485: 279,
+ 0x770486: 280,
+ 0x770487: 281,
+ 0x770488: 282,
+ 0x770489: 283,
+ 0x77048a: 284,
+ 0x77048b: 285,
+ 0x77048c: 286,
+ 0x77048d: 287,
+ 0x77048e: 321,
+ 0x77048f: 322,
+ 0x770490: 323,
+ 0x770491: 324,
+ 0x770492: 325,
+ 0x770493: 326,
+ 0x770494: 327,
+ 0x770495: 328,
+ 0x770496: 329,
+ 0x770497: 332,
+ 0x770498: 334,
+ 0x770499: 335,
+ 0x77049a: 336,
+ 0x77049b: 337,
+ 0x77049c: 340,
+ 0x77049d: 341,
+ 0x77049e: 342,
+ 0x77049f: 343,
+ 0x7704a0: 345,
+ 0x7704a1: 346,
+ 0x7704a2: 347,
+ 0x7704a3: 348,
+ 0x7704a4: 349,
+ 0x7704a5: 350,
+ 0x7704a6: 351,
+ 0x7704a7: 354,
+ 0x7704a8: 355,
+ 0x7704a9: 356,
+ 0x7704aa: 357,
+ 0x7704ab: 384,
+ 0x7704ac: 385,
+ 0x7704ad: 386,
+ 0x7704ae: 387,
+ 0x7704af: 388,
+ 0x7704b0: 389,
+ 0x7704b1: 391,
+ 0x7704b2: 392,
+ 0x7704b3: 393,
+ 0x7704b4: 394,
+ 0x7704b5: 396,
+ 0x7704b6: 397,
+ 0x7704b7: 398,
+ 0x7704b8: 399,
+ 0x7704b9: 400,
+ 0x7704ba: 401,
+ 0x7704bb: 402,
+ 0x7704bc: 403,
+ 0x7704bd: 404,
+ 0x7704be: 449,
+ 0x7704bf: 450,
+ 0x7704c0: 451,
+ 0x7704c1: 453,
+ 0x7704c2: 454,
+ 0x7704c3: 455,
+ 0x7704c4: 456,
+ 0x7704c5: 457,
+ 0x7704c6: 460,
+ 0x7704c7: 461,
+ 0x7704c8: 462,
+ 0x7704c9: 463,
+ 0x7704ca: 464,
+ 0x7704cb: 465,
+ 0x7704cc: 466,
+ 0x7704cd: 467,
+ 0x7704ce: 468,
+ 0x7704cf: 513,
+ 0x7704d0: 514,
+ 0x7704d1: 515,
+ 0x7704d2: 516,
+ 0x7704d3: 517,
+ 0x7704d4: 518,
+ 0x7704d5: 519,
+ 0x7704d6: 520,
+ 0x7704d7: 521,
+ 0x7704d8: 523,
+ 0x7704d9: 524,
+ 0x7704da: 527,
+ 0x7704db: 528,
+ 0x7704dc: 529,
+ 0x7704dd: 531,
+ 0x7704de: 532,
+ 0x7704df: 533,
+ 0x7704e0: 534,
+ 0x7704e1: 535,
+ 0x7704e2: 536,
+ 0x7704e3: 537,
+ 0x7704e4: 576,
+ 0x7704e5: 577,
+ 0x7704e6: 578,
+ 0x7704e7: 579,
+ 0x7704e8: 580,
+ 0x7704e9: 582,
+ 0x7704ea: 583,
+ 0x7704eb: 584,
+ 0x7704ec: 585,
+ 0x7704ed: 586,
+ 0x7704ee: 587,
+ 0x7704ef: 588,
+ 0x7704f0: 589,
+ 0x7704f1: 590,
+ 0x7704f2: 591,
+ 0x7704f3: 592,
+ 0x7704f4: 593,
+ 0x7704f5: 594,
+ 0x7704f6: 595,
+ 0x7704f7: 596,
+ 0x7704f8: 597,
+ 0x7704f9: 598,
+ 0x7704fa: 599,
+ 0x7704fb: 600,
+ 0x7704fc: 601,
+ 0x7704fd: 602,
+ 0x7704fe: 603,
+ 0x7704ff: 604,
+ 0x770500: 607,
+ 0x770501: 608,
+ 0x770502: 609,
+ 0x770503: 610,
+ 0x770504: 611,
+ 0x770505: 612,
+ 0x770506: 613,
+ 0x770507: 614,
+ 0x770508: 615,
+ 0x770509: 616,
+ 0x77050a: 617,
+ 0x77050b: 618,
+ 0x77050c: 619,
+ 0x77050d: 620,
+ 0x77050e: 621,
+ 0x77050f: 622,
+ 0x770510: 623,
+ 0x770511: 624,
+ 0x770512: 625,
+ 0x770513: 626,
+ 0x770514: 627,
+ 0x770515: 628,
+ 0x770516: 629,
+ 0x770517: 640,
+ 0x770518: 641,
+ 0x770519: 642,
+ 0x77051a: 643,
+ 0x77051b: 644,
+ 0x77051c: 645,
+ 0x77051d: 646,
+ 0x77051e: 647,
+ 0x77051f: 648,
+ 0x770520: 649,
+ 0x770521: 650,
+ 0x770522: 651,
+ 0x770523: 652,
+ 0x770524: 653,
+ 0x770525: 654,
+ 0x770526: 655,
+ 0x770527: 656,
+ 0x770528: 657,
+ 0x770529: 658,
+ 0x77052a: 659,
+ 0x77052b: 660,
+ 0x77052c: 661,
+ 0x77052d: 662,
+ 0x77052e: 663,
+ 0x77052f: 664,
+ 0x770530: 665,
+ 0x770531: 666,
+ 0x770532: 667,
+ 0x770533: 668,
+ 0x770534: 669,
+ 0x770535: 670,
+ 0x770536: 674,
+ 0x770537: 675,
+ 0x770538: 676,
+ 0x770539: 677,
+ 0x77053a: 678,
+ 0x77053b: 679,
+ 0x77053c: 680,
+ 0x77053d: 681,
+ 0x77053e: 682,
+ 0x77053f: 683,
+ 0x770540: 684,
+ 0x770541: 686,
+ 0x770542: 687,
+ 0x770543: 688,
+ 0x770544: 689,
+ 0x770545: 690,
+ 0x770546: 691,
+ 0x770547: 692,
+ 0x770548: 694,
+ 0x770549: 695,
+ 0x77054a: 704,
+ 0x77054b: 705,
+ 0x77054c: 706,
+ 0x77054d: 707,
+ 0x77054e: 708,
+ 0x77054f: 709,
+ 0x770550: 710,
+ 0x770551: 711,
+ 0x770552: 712,
+ 0x770553: 713,
+ 0x770554: 714,
+ 0x770555: 715,
+ 0x770556: 716,
+ 0x770557: 717,
+ 0x770558: 718,
+ 0x770559: 719,
+ 0x77055a: 720,
+ 0x77055b: 721,
+ 0x77055c: 722,
+ 0x77055d: 723,
+ 0x77055e: 724,
+ 0x77055f: 725,
+ 0x770560: 726,
+ 0x770561: 769,
+ 0x770562: 770,
+ 0x770563: 771,
+ 0x770564: 772,
+ 0x770565: 773,
+ 0x770566: 774,
+ 0x770567: 775,
+ 0x770568: 776,
+ 0x770569: 777,
+ 0x77056a: 778,
+ 0x77056b: 779,
+ 0x77056c: 780,
+ 0x77056d: 781,
+ 0x77056e: 782,
+ 0x77056f: 783,
+ 0x770570: 784,
+ 0x770571: 785,
+ 0x770572: 786,
+ 0x770573: 787,
+ 0x770574: 788,
+ 0x770575: 789,
+ 0x770576: 790,
+ 0x770577: 832,
+ 0x770578: 833,
+ 0x770579: 834,
+ 0x77057a: 835,
+ 0x77057b: 836,
+ 0x77057c: 837,
+ 0x77057d: 838,
+ 0x77057e: 839,
+ 0x77057f: 840,
+ 0x770580: 841,
+ 0x770581: 842,
+ 0x770582: 843,
+ 0x770583: 844,
+ 0x770584: 845,
+ 0x770585: 846,
+ 0x770586: 847,
+ 0x770587: 848,
+ 0x770588: 849,
+ 0x770589: 850,
+ 0x77058a: 854,
+ 0x77058b: 855,
+ 0x77058c: 856,
+ 0x77058d: 857,
+ 0x77058e: 858,
+ 0x77058f: 859,
+ 0x770590: 860,
+ 0x770591: 861,
+ 0x770592: 862,
+ 0x770593: 863,
+ 0x770594: 864,
+ 0x770595: 865,
+ 0x770596: 866,
+ 0x770597: 867,
+ 0x770598: 868,
+ 0x770599: 869,
+ 0x77059a: 870,
+ 0x77059b: 871,
+ 0x77059c: 872,
+ 0x77059d: 873,
+ 0x77059e: 874,
+ 0x77059f: 875,
+ 0x7705a0: 876,
+ 0x7705a1: 877,
+ 0x7705a2: 878,
+ 0x7705a3: 879,
+ 0x7705a4: 880,
+ 0x7705a5: 881,
+ 0x7705a6: 882,
+ 0x7705a7: 896,
+ 0x7705a8: 897,
+ 0x7705a9: 898,
+ 0x7705aa: 899,
+ 0x7705ab: 900,
+ 0x7705ac: 901,
+ 0x7705ad: 902,
+ 0x7705ae: 903,
+ 0x7705af: 904,
+ 0x7705b0: 905,
+ 0x7705b1: 960,
+ 0x7705b2: 961,
+ 0x7705b3: 962,
+ 0x7705b4: 963,
+ 0x7705b5: 964,
+ 0x7705b6: 965,
+ 0x7705b7: 966,
+ 0x7705b8: 967,
+ 0x7705b9: 968,
+ 0x7705ba: 969,
+ 0x7705bb: 970,
+ 0x7705bc: 972,
+ 0x7705bd: 973,
+ 0x7705be: 974,
+ 0x7705bf: 975,
+ 0x7705c0: 977,
+ 0x7705c1: 978,
+ 0x7705c2: 979,
+ 0x7705c3: 980,
+ 0x7705c4: 981,
+ 0x7705c5: 982,
+ 0x7705c6: 983,
+ 0x7705c7: 984,
+ 0x7705c8: 1025,
+ 0x7705c9: 1026,
+ 0x7705ca: 1027,
+ 0x7705cb: 1028,
+ 0x7705cc: 1029,
+ 0x7705cd: 1030,
+ 0x7705ce: 1031,
+ 0x7705cf: 1032,
+ 0x7705d0: 1033,
+ 0x7705d1: 1034,
+ 0x7705d2: 1037,
+ 0x7705d3: 1040,
+ 0x7705d4: 1041,
+ 0x7705d5: 1042,
+ 0x7705d6: 1043,
+ 0x7705d7: 1044,
+ 0x7705d8: 1045,
+ 0x7705d9: 1046,
+ 0x7705da: 1049,
+ 0x7705db: 1050,
+ 0x7705dc: 1051,
+ 0x7705dd: 1052,
+ 0x7705de: 1053,
+ 0x7705df: 1054,
+ 0x7705e0: 1055,
+ 0x7705e1: 1056,
+ 0x7705e2: 1057,
+ 0x7705e3: 1058,
+ 0x7705e4: 1059,
+ 0x7705e5: 1060,
+ 0x7705e6: 1061,
+ 0x7705e7: 1062,
+ 0x7705e8: 1063,
+ 0x7705e9: 1064,
+ 0x7705ea: 1065,
+ 0x7705eb: 1066,
+ 0x7705ec: 1067,
+ 0x7705ed: 1068,
+ 0x7705ee: 1069,
+ 0x7705ef: 1070,
+ 0x7705f0: 1152,
+ 0x7705f1: 1154,
+ 0x7705f2: 1155,
+ 0x7705f3: 1156,
+ 0x7705f4: 1157,
+ 0x7705f5: 1158,
+ 0x7705f6: 1159,
+ 0x7705f7: 1160,
+ 0x7705f8: 1161,
+ 0x7705f9: 1162,
+ 0x7705fa: 1163,
+ 0x7705fb: 1164,
+ 0x7705fc: 1165,
+ 0x7705fd: 1166,
+ 0x7705fe: 1167,
+ 0x7705ff: 1168,
+ 0x770600: 1169,
+ 0x770601: 1173,
+ 0x770602: 1174,
+ 0x770603: 1175,
+ 0x770604: 1176,
+ 0x770605: 1177,
+ 0x770606: 1178,
+ 0x770607: 1216,
+ 0x770608: 1217,
+ 0x770609: 1218,
+ 0x77060a: 1219,
+ 0x77060b: 1220,
+ 0x77060c: 1221,
+ 0x77060d: 1222,
+ 0x77060e: 1223,
+ 0x77060f: 1224,
+ 0x770610: 1225,
+ 0x770611: 1226,
+ 0x770612: 1227,
+ 0x770613: 1228,
+ 0x770614: 1229,
+ 0x770615: 1230,
+ 0x770616: 1231,
+ 0x770617: 1232,
+ 0x770618: 1233,
+ 0x770619: 1234,
+ 0x77061a: 1235,
+ 0x77061b: 1236,
+ 0x77061c: 1237,
+ 0x77061d: 1238,
+ 0x77061e: 1239,
+ 0x77061f: 1240,
+ 0x770620: 1241,
+ 0x770621: 1242,
+ 0x770622: 1243,
+ 0x770623: 1244,
+ 0x770624: 1245,
+ 0x770625: 1246,
+ 0x770626: 1247,
+ 0x770627: 1248,
+ 0x770628: 1249,
+ 0x770629: 1250,
+ 0x77062a: 1251,
+ 0x77062b: 1252,
+ 0x77062c: 1253,
+ 0x77062d: 1254,
+ 0x77062e: 1255,
+ 0x77062f: 1256,
+ 0x770630: 1257,
+ 0x770631: 1258,
+ 0x770632: 1259,
+ 0x770633: 1260,
+ 0x770634: 1261,
+ 0x770635: 1262,
+ 0x770636: 1263,
+ 0x770637: 1264,
+ 0x770638: 1265,
+ 0x770639: 1266,
+ 0x77063a: 1267,
+ 0x77063b: 1268,
+ 0x77063c: 1269,
+ 0x77063d: 1280,
+ 0x77063e: 1281,
+ 0x77063f: 1282,
+ 0x770640: 1283,
+ 0x770641: 1284,
+ 0x770642: 1285,
+ 0x770643: 1286,
+ 0x770644: 1289,
+ 0x770645: 1290,
+ 0x770646: 1291,
+ 0x770647: 1292,
+ 0x770648: 1293,
+ 0x770649: 1294,
+ 0x77064a: 1295,
+ 0x77064b: 1296,
+ 0x77064c: 1297,
+ 0x77064d: 1298,
+ 0x77064e: 1299,
+ 0x77064f: 1300,
+ 0x770650: 1301,
+ 0x770651: 1302,
+ 0x770652: 1303,
+ 0x770653: 1344,
+ 0x770654: 1345,
+ 0x770655: 1346,
+ 0x770656: 1347,
+ 0x770657: 1348,
+ 0x770658: 1349,
+ 0x770659: 1350,
+ 0x77065a: 1351,
+ 0x77065b: 1352,
+ 0x77065c: 1354,
+ 0x77065d: 1355,
+ 0x77065e: 1356,
+ 0x77065f: 1357,
+ 0x770660: 1358,
+ 0x770661: 1359,
+ 0x770662: 1360,
+ 0x770663: 1361,
+ 0x770664: 1362,
+ 0x770665: 1363,
+ 0x770666: 1365,
+ 0x770667: 1366,
+ 0x770668: 1367,
+ 0x770669: 1368,
+ 0x77066a: 1369,
+ 0x77066b: 1370,
+ 0x77066c: 1371,
+ 0x77066d: 1372,
+ 0x77066e: 1374,
+ 0x77066f: 1375,
+ 0x770670: 1376,
+ 0x770671: 1379,
+ 0x770672: 1380,
+ 0x770673: 1381,
+ 0x770674: 1382,
+ 0x770675: 1383,
+ 0x770676: 1384,
+ 0x770677: 1385,
+ 0x770678: 1386,
+ 0x770679: 1387,
+ 0x77067a: 1388,
+ 0x77067b: 1389,
+ 0x77067c: 1390,
+ 0x77067d: 1391,
+ 0x77067e: 1392,
+ 0x77067f: 1393,
+ 0x770680: 1394,
+ 0x770681: 1395,
+ 0x770682: 1396,
+ 0x770683: 1397,
+ 0x770684: 1398,
+ 0x770685: 1408,
+ 0x770686: 1409,
+ 0x770687: 1410,
+ 0x770688: 1411,
+ 0x770689: 1412,
+ 0x77068a: 1414,
+ 0x77068b: 1472,
+ 0x77068c: 1473,
+ 0x77068d: 1474,
+ 0x77068e: 1475,
+ 0x77068f: 1476,
+ 0x770690: 1477,
+ 0x770691: 1478,
+ 0x770692: 1479,
+ 0x770693: 1480,
+ 0x770694: 1481,
+ 0x770695: 1482,
+ 0x770696: 1483,
+ 0x770697: 1484,
+ 0x770698: 1486,
+ 0x770699: 1487,
+ 0x77069a: 1488,
+ 0x77069b: 1489,
+ 0x77069c: 1490,
+ 0x77069d: 1491,
+ 0x77069e: 1495,
+ 0x77069f: 1496,
+ 0x7706a0: 1497,
+ 0x7706a1: 1498,
+ 0x7706a2: 1499,
+ 0x7706a3: 1500,
+ 0x7706a4: 1501,
+ 0x7706a5: 1502,
+ 0x7706a6: 1503,
+ 0x7706a7: 1504,
+ 0x7706a8: 1505,
+ 0x7706a9: 1506,
+ 0x7706aa: 1507,
+ 0x7706ab: 1508,
+ 0x7706ac: 1536,
+ 0x7706ad: 1537,
+ 0x7706ae: 1538,
+ 0x7706af: 1539,
+ 0x7706b0: 1540,
+ 0x7706b1: 1541,
+ 0x7706b2: 1600,
+ 0x7706b3: 1601,
+ 0x7706b4: 1602,
+ 0x7706b5: 1603,
+ 0x7706b6: 1604,
+ 0x7706b7: 1605,
+ 0x7706b8: 1606,
+ 0x7706b9: 1607,
+ 0x7706ba: 1612,
+ 0x7706bb: 1613,
+ 0x7706bc: 1614,
+ 0x7706bd: 1615,
+ 0x7706be: 1616,
+ 0x7706bf: 1617,
+ 0x7706c0: 1618,
+ 0x7706c1: 1619,
+ 0x7706c2: 1620,
+ 0x7706c3: 1621,
+ 0x7706c4: 1622,
+ 0x7706c5: 1664,
+ 0x7706c6: 1665,
+ 0x7706c7: 1667,
+ 0x7706c8: 1668,
+ 0x7706c9: 1670,
+ 0x7706ca: 1671,
+ 0x7706cb: 1672,
+ 0x7706cc: 1673,
+ 0x7706cd: 1674,
+ 0x7706ce: 1675,
+ 0x7706cf: 1676,
+ 0x7706d0: 1677,
+ 0x7706d1: 1678,
+ 0x7706d2: 1679,
+ 0x7706d3: 1680,
+ 0x7706d4: 1681,
+ 0x7706d5: 1682,
+ 0x7706d6: 1683,
+ 0x7706d7: 1684,
+ 0x7706d8: 1685,
+ 0x7706d9: 1686,
+ 0x7706da: 1730,
+ 0x7706db: 1732,
+ 0x7706dc: 1734,
+ 0x7706dd: 1792,
+ 0x7706de: 1793,
+ 0x7706df: 1794,
+ 0x7706e0: 1795,
+ 0x7706e1: 1796,
+ 0x7706e2: 1797,
+ 0x7706e3: 1798,
+ 0x7706e4: 1799,
+ 0x7706e5: 1800,
+ 0x7706e6: 1801,
+ 0x7706e7: 1802,
+ 0x7706e8: 1803,
+ 0x7706e9: 1804,
+ 0x7706ea: 1805,
+ 0x7706eb: 1810,
+ 0x7706ec: 1811,
+ 0x7706ed: 1812,
+ 0x7706ee: 1813,
+ 0x7706ef: 1814,
+ 0x7706f0: 1815,
+ 0x7706f1: 1817,
+ 0x7706f2: 1818,
+ 0x7706f3: 1819,
+ 0x7706f4: 1820,
+ 0x7706f5: 1821,
+ 0x7706f6: 1822,
+ 0x7706f7: 1823,
+ 0x7706f8: 1824,
+ 0x7706f9: 1825,
+ 0x7706fa: 1826,
+ 0x7706fb: 1827,
+ 0x7706fc: 1828,
+ 0x7706fd: 1831,
+ 0x7706fe: 1832,
+ 0x7706ff: 1858,
+}
diff --git a/worlds/kdl3/Compression.py b/worlds/kdl3/Compression.py
new file mode 100644
index 000000000000..ec5461fbec75
--- /dev/null
+++ b/worlds/kdl3/Compression.py
@@ -0,0 +1,57 @@
+def hal_decompress(comp: bytes) -> bytes:
+ """
+ HAL decompression based on exhal by devinacker
+ https://github.com/devinacker/exhal
+ """
+ inpos = 0
+
+ inval = 0
+ output = bytearray()
+ while inval != 0xFF:
+ remaining = 65536 - inpos
+ if remaining < 1:
+ return bytes()
+ inval = comp[inpos]
+ inpos += 1
+ if inval == 0xFF:
+ break
+ if (inval & 0xE0) == 0xE0:
+ command = (inval >> 2) & 0x07
+ length = (((inval & 0x03) << 8) | comp[inpos]) + 1
+ inpos += 1
+ else:
+ command = inval >> 5
+ length = (inval & 0x1F) + 1
+ if (command == 2 and ((len(output) + 2*length) > 65536)) or (len(output) + length) > 65536:
+ return bytes()
+ if command == 0:
+ output.extend(comp[inpos:inpos+length])
+ inpos += length
+ elif command == 1:
+ output.extend([comp[inpos] for _ in range(length)])
+ inpos += 1
+ elif command == 2:
+ output.extend([comp[x] for _ in range(length) for x in (inpos, inpos+1)])
+ inpos += 2
+ elif command == 3:
+ output.extend([comp[inpos] + i for i in range(length)])
+ inpos += 1
+ elif command == 4 or command == 7:
+ offset = (comp[inpos] << 8) | comp[inpos + 1]
+ if (offset + length) > 65536:
+ return bytes()
+ output.extend(output[offset:offset+length])
+ inpos += 2
+ elif command == 5:
+ offset = (comp[inpos] << 8) | comp[inpos + 1]
+ if (offset + length) > 65536:
+ return bytes()
+ output.extend([int('{:08b}'.format(x)[::-1], 2) for x in output[offset:offset+length]])
+ inpos += 2
+ elif command == 6:
+ offset = (comp[inpos] << 8) | comp[inpos + 1]
+ if offset < length - 1:
+ return bytes()
+ output.extend([output[offset - x] for x in range(length)])
+ inpos += 2
+ return bytes(output)
diff --git a/worlds/kdl3/Gifting.py b/worlds/kdl3/Gifting.py
new file mode 100644
index 000000000000..8ccba7ec1ae6
--- /dev/null
+++ b/worlds/kdl3/Gifting.py
@@ -0,0 +1,282 @@
+# Small subfile to handle gifting info such as desired traits and giftbox management
+import typing
+
+
+async def update_object(ctx, key: str, value: typing.Dict):
+ await ctx.send_msgs([
+ {
+ "cmd": "Set",
+ "key": key,
+ "default": {},
+ "want_reply": False,
+ "operations": [
+ {"operation": "update", "value": value}
+ ]
+ }
+ ])
+
+
+async def pop_object(ctx, key: str, value: str):
+ await ctx.send_msgs([
+ {
+ "cmd": "Set",
+ "key": key,
+ "default": {},
+ "want_reply": False,
+ "operations": [
+ {"operation": "pop", "value": value}
+ ]
+ }
+ ])
+
+
+async def initialize_giftboxes(ctx, giftbox_key: str, motherbox_key: str, is_open: bool):
+ ctx.set_notify(motherbox_key, giftbox_key)
+ await update_object(ctx, f"Giftboxes;{ctx.team}", {f"{ctx.slot}":
+ {
+ "IsOpen": is_open,
+ **kdl3_gifting_options
+ }})
+ ctx.gifting = is_open
+
+
+kdl3_gifting_options = {
+ "AcceptsAnyGift": True,
+ "DesiredTraits": [
+ "Consumable", "Food", "Drink", "Candy", "Tomato",
+ "Invincible", "Life", "Heal", "Health", "Trap",
+ "Goo", "Gel", "Slow", "Slowness", "Eject", "Removal"
+ ],
+ "MinimumGiftVersion": 2,
+}
+
+kdl3_gifts = {
+ 1: {
+ "ItemName": "1-Up",
+ "Amount": 1,
+ "ItemValue": 400000,
+ "Traits": [
+ {
+ "Trait": "Consumable",
+ "Quality": 1,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Life",
+ "Quality": 1,
+ "Duration": 1
+ }
+ ]
+ },
+ 2: {
+ "ItemName": "Maxim Tomato",
+ "Amount": 1,
+ "ItemValue": 500000,
+ "Traits": [
+ {
+ "Trait": "Consumable",
+ "Quality": 5,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Heal",
+ "Quality": 5,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Food",
+ "Quality": 5,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Tomato",
+ "Quality": 5,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Vegetable",
+ "Quality": 5,
+ "Duration": 1,
+ }
+ ]
+ },
+ 3: {
+ "ItemName": "Energy Drink",
+ "Amount": 1,
+ "ItemValue": 100000,
+ "Traits": [
+ {
+ "Trait": "Consumable",
+ "Quality": 1,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Heal",
+ "Quality": 1,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Drink",
+ "Quality": 1,
+ "Duration": 1,
+ },
+ ]
+ },
+ 5: {
+ "ItemName": "Small Star Piece",
+ "Amount": 1,
+ "ItemValue": 10000,
+ "Traits": [
+ {
+ "Trait": "Currency",
+ "Quality": 1,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Money",
+ "Quality": 1,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Star",
+ "Quality": 1,
+ "Duration": 1
+ }
+ ]
+ },
+ 6: {
+ "ItemName": "Medium Star Piece",
+ "Amount": 1,
+ "ItemValue": 30000,
+ "Traits": [
+ {
+ "Trait": "Currency",
+ "Quality": 3,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Money",
+ "Quality": 3,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Star",
+ "Quality": 3,
+ "Duration": 1
+ }
+ ]
+ },
+ 7: {
+ "ItemName": "Large Star Piece",
+ "Amount": 1,
+ "ItemValue": 50000,
+ "Traits": [
+ {
+ "Trait": "Currency",
+ "Quality": 5,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Money",
+ "Quality": 5,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Star",
+ "Quality": 5,
+ "Duration": 1
+ }
+ ]
+ },
+}
+
+kdl3_trap_gifts = {
+ 0: {
+ "ItemName": "Gooey Bag",
+ "Amount": 1,
+ "ItemValue": 10000,
+ "Traits": [
+ {
+ "Trait": "Trap",
+ "Quality": 1,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Goo",
+ "Quality": 1,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Gel",
+ "Quality": 1,
+ "Duration": 1
+ }
+ ]
+ },
+ 1: {
+ "ItemName": "Slowness",
+ "Amount": 1,
+ "ItemValue": 10000,
+ "Traits": [
+ {
+ "Trait": "Trap",
+ "Quality": 1,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Slow",
+ "Quality": 1,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Slowness",
+ "Quality": 1,
+ "Duration": 1
+ }
+ ]
+ },
+ 2: {
+ "ItemName": "Eject Ability",
+ "Amount": 1,
+ "ItemValue": 10000,
+ "Traits": [
+ {
+ "Trait": "Trap",
+ "Quality": 1,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Eject",
+ "Quality": 1,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Removal",
+ "Quality": 1,
+ "Duration": 1
+ }
+ ]
+ },
+ 3: {
+ "ItemName": "Bad Meal",
+ "Amount": 1,
+ "ItemValue": 10000,
+ "Traits": [
+ {
+ "Trait": "Trap",
+ "Quality": 1,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Damage",
+ "Quality": 1,
+ "Duration": 1,
+ },
+ {
+ "Trait": "Food",
+ "Quality": 1,
+ "Duration": 1
+ }
+ ]
+ },
+}
diff --git a/worlds/kdl3/Items.py b/worlds/kdl3/Items.py
new file mode 100644
index 000000000000..66c7f8fee323
--- /dev/null
+++ b/worlds/kdl3/Items.py
@@ -0,0 +1,105 @@
+from BaseClasses import Item
+import typing
+
+
+class ItemData(typing.NamedTuple):
+ code: typing.Optional[int]
+ progression: bool
+ skip_balancing: bool = False
+ trap: bool = False
+
+
+class KDL3Item(Item):
+ game = "Kirby's Dream Land 3"
+
+
+copy_ability_table = {
+ "Burning": ItemData(0x770001, True),
+ "Stone": ItemData(0x770002, True),
+ "Ice": ItemData(0x770003, True),
+ "Needle": ItemData(0x770004, True),
+ "Clean": ItemData(0x770005, True),
+ "Parasol": ItemData(0x770006, True),
+ "Spark": ItemData(0x770007, True),
+ "Cutter": ItemData(0x770008, True)
+}
+
+animal_friend_table = {
+ "Rick": ItemData(0x770010, True),
+ "Kine": ItemData(0x770011, True),
+ "Coo": ItemData(0x770012, True),
+ "Nago": ItemData(0x770013, True),
+ "ChuChu": ItemData(0x770014, True),
+ "Pitch": ItemData(0x770015, True)
+}
+
+animal_friend_spawn_table = {
+ "Rick Spawn": ItemData(None, True),
+ "Kine Spawn": ItemData(None, True),
+ "Coo Spawn": ItemData(None, True),
+ "Nago Spawn": ItemData(None, True),
+ "ChuChu Spawn": ItemData(None, True),
+ "Pitch Spawn": ItemData(None, True)
+}
+
+copy_ability_access_table = {
+ "No Ability": ItemData(None, False),
+ "Burning Ability": ItemData(None, True),
+ "Stone Ability": ItemData(None, True),
+ "Ice Ability": ItemData(None, True),
+ "Needle Ability": ItemData(None, True),
+ "Clean Ability": ItemData(None, True),
+ "Parasol Ability": ItemData(None, True),
+ "Spark Ability": ItemData(None, True),
+ "Cutter Ability": ItemData(None, True),
+}
+
+misc_item_table = {
+ "Heart Star": ItemData(0x770020, True, True),
+ "1-Up": ItemData(0x770021, False),
+ "Maxim Tomato": ItemData(0x770022, False),
+ "Invincible Candy": ItemData(0x770023, False),
+ "Little Star": ItemData(0x770024, False),
+ "Medium Star": ItemData(0x770025, False),
+ "Big Star": ItemData(0x770026, False),
+}
+
+trap_item_table = {
+ "Gooey Bag": ItemData(0x770040, False, False, True),
+ "Slowness": ItemData(0x770041, False, False, True),
+ "Eject Ability": ItemData(0x770042, False, False, True)
+}
+
+filler_item_weights = {
+ "1-Up": 4,
+ "Maxim Tomato": 2,
+ "Invincible Candy": 2
+}
+
+star_item_weights = {
+ "Little Star": 4,
+ "Medium Star": 2,
+ "Big Star": 1
+}
+
+total_filler_weights = {
+ **filler_item_weights,
+ **star_item_weights
+}
+
+
+item_table = {
+ **copy_ability_table,
+ **copy_ability_access_table,
+ **animal_friend_table,
+ **animal_friend_spawn_table,
+ **misc_item_table,
+ **trap_item_table
+}
+
+item_names = {
+ "Copy Ability": set(copy_ability_table),
+ "Animal Friend": set(animal_friend_table),
+}
+
+lookup_name_to_id: typing.Dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code}
diff --git a/worlds/kdl3/Locations.py b/worlds/kdl3/Locations.py
new file mode 100644
index 000000000000..4d039a13497c
--- /dev/null
+++ b/worlds/kdl3/Locations.py
@@ -0,0 +1,940 @@
+import typing
+from BaseClasses import Location, Region
+from .Names import LocationName
+
+if typing.TYPE_CHECKING:
+ from .Room import KDL3Room
+
+
+class KDL3Location(Location):
+ game: str = "Kirby's Dream Land 3"
+ room: typing.Optional["KDL3Room"] = None
+
+ def __init__(self, player: int, name: str, address: typing.Optional[int], parent: typing.Union[Region, None]):
+ super().__init__(player, name, address, parent)
+ if not address:
+ self.show_in_spoiler = False
+
+
+stage_locations = {
+ 0x770001: LocationName.grass_land_1,
+ 0x770002: LocationName.grass_land_2,
+ 0x770003: LocationName.grass_land_3,
+ 0x770004: LocationName.grass_land_4,
+ 0x770005: LocationName.grass_land_5,
+ 0x770006: LocationName.grass_land_6,
+ 0x770007: LocationName.ripple_field_1,
+ 0x770008: LocationName.ripple_field_2,
+ 0x770009: LocationName.ripple_field_3,
+ 0x77000A: LocationName.ripple_field_4,
+ 0x77000B: LocationName.ripple_field_5,
+ 0x77000C: LocationName.ripple_field_6,
+ 0x77000D: LocationName.sand_canyon_1,
+ 0x77000E: LocationName.sand_canyon_2,
+ 0x77000F: LocationName.sand_canyon_3,
+ 0x770010: LocationName.sand_canyon_4,
+ 0x770011: LocationName.sand_canyon_5,
+ 0x770012: LocationName.sand_canyon_6,
+ 0x770013: LocationName.cloudy_park_1,
+ 0x770014: LocationName.cloudy_park_2,
+ 0x770015: LocationName.cloudy_park_3,
+ 0x770016: LocationName.cloudy_park_4,
+ 0x770017: LocationName.cloudy_park_5,
+ 0x770018: LocationName.cloudy_park_6,
+ 0x770019: LocationName.iceberg_1,
+ 0x77001A: LocationName.iceberg_2,
+ 0x77001B: LocationName.iceberg_3,
+ 0x77001C: LocationName.iceberg_4,
+ 0x77001D: LocationName.iceberg_5,
+ 0x77001E: LocationName.iceberg_6,
+}
+
+heart_star_locations = {
+ 0x770101: LocationName.grass_land_tulip,
+ 0x770102: LocationName.grass_land_muchi,
+ 0x770103: LocationName.grass_land_pitcherman,
+ 0x770104: LocationName.grass_land_chao,
+ 0x770105: LocationName.grass_land_mine,
+ 0x770106: LocationName.grass_land_pierre,
+ 0x770107: LocationName.ripple_field_kamuribana,
+ 0x770108: LocationName.ripple_field_bakasa,
+ 0x770109: LocationName.ripple_field_elieel,
+ 0x77010A: LocationName.ripple_field_toad,
+ 0x77010B: LocationName.ripple_field_mama_pitch,
+ 0x77010C: LocationName.ripple_field_hb002,
+ 0x77010D: LocationName.sand_canyon_mushrooms,
+ 0x77010E: LocationName.sand_canyon_auntie,
+ 0x77010F: LocationName.sand_canyon_caramello,
+ 0x770110: LocationName.sand_canyon_hikari,
+ 0x770111: LocationName.sand_canyon_nyupun,
+ 0x770112: LocationName.sand_canyon_rob,
+ 0x770113: LocationName.cloudy_park_hibanamodoki,
+ 0x770114: LocationName.cloudy_park_piyokeko,
+ 0x770115: LocationName.cloudy_park_mrball,
+ 0x770116: LocationName.cloudy_park_mikarin,
+ 0x770117: LocationName.cloudy_park_pick,
+ 0x770118: LocationName.cloudy_park_hb007,
+ 0x770119: LocationName.iceberg_kogoesou,
+ 0x77011A: LocationName.iceberg_samus,
+ 0x77011B: LocationName.iceberg_kawasaki,
+ 0x77011C: LocationName.iceberg_name,
+ 0x77011D: LocationName.iceberg_shiro,
+ 0x77011E: LocationName.iceberg_angel,
+}
+
+boss_locations = {
+ 0x770200: LocationName.grass_land_whispy,
+ 0x770201: LocationName.ripple_field_acro,
+ 0x770202: LocationName.sand_canyon_poncon,
+ 0x770203: LocationName.cloudy_park_ado,
+ 0x770204: LocationName.iceberg_dedede,
+}
+
+consumable_locations = {
+ 0x770300: LocationName.grass_land_1_u1,
+ 0x770301: LocationName.grass_land_1_m1,
+ 0x770302: LocationName.grass_land_2_u1,
+ 0x770303: LocationName.grass_land_3_u1,
+ 0x770304: LocationName.grass_land_3_m1,
+ 0x770305: LocationName.grass_land_4_m1,
+ 0x770306: LocationName.grass_land_4_u1,
+ 0x770307: LocationName.grass_land_4_m2,
+ 0x770308: LocationName.grass_land_4_m3,
+ 0x770309: LocationName.grass_land_6_u1,
+ 0x77030A: LocationName.grass_land_6_u2,
+ 0x77030B: LocationName.ripple_field_2_u1,
+ 0x77030C: LocationName.ripple_field_2_m1,
+ 0x77030D: LocationName.ripple_field_3_m1,
+ 0x77030E: LocationName.ripple_field_3_u1,
+ 0x77030F: LocationName.ripple_field_4_m2,
+ 0x770310: LocationName.ripple_field_4_u1,
+ 0x770311: LocationName.ripple_field_4_m1,
+ 0x770312: LocationName.ripple_field_5_u1,
+ 0x770313: LocationName.ripple_field_5_m2,
+ 0x770314: LocationName.ripple_field_5_m1,
+ 0x770315: LocationName.sand_canyon_1_u1,
+ 0x770316: LocationName.sand_canyon_2_u1,
+ 0x770317: LocationName.sand_canyon_2_m1,
+ 0x770318: LocationName.sand_canyon_4_m1,
+ 0x770319: LocationName.sand_canyon_4_u1,
+ 0x77031A: LocationName.sand_canyon_4_m2,
+ 0x77031B: LocationName.sand_canyon_5_u1,
+ 0x77031C: LocationName.sand_canyon_5_u3,
+ 0x77031D: LocationName.sand_canyon_5_m1,
+ 0x77031E: LocationName.sand_canyon_5_u4,
+ 0x77031F: LocationName.sand_canyon_5_u2,
+ 0x770320: LocationName.cloudy_park_1_m1,
+ 0x770321: LocationName.cloudy_park_1_u1,
+ 0x770322: LocationName.cloudy_park_4_u1,
+ 0x770323: LocationName.cloudy_park_4_m1,
+ 0x770324: LocationName.cloudy_park_5_m1,
+ 0x770325: LocationName.cloudy_park_6_u1,
+ 0x770326: LocationName.iceberg_3_m1,
+ 0x770327: LocationName.iceberg_5_u1,
+ 0x770328: LocationName.iceberg_5_u2,
+ 0x770329: LocationName.iceberg_5_u3,
+ 0x77032A: LocationName.iceberg_6_m1,
+ 0x77032B: LocationName.iceberg_6_u1,
+}
+
+level_consumables = {
+ 1: [0, 1],
+ 2: [2],
+ 3: [3, 4],
+ 4: [5, 6, 7, 8],
+ 6: [9, 10],
+ 8: [11, 12],
+ 9: [13, 14],
+ 10: [15, 16, 17],
+ 11: [18, 19, 20],
+ 13: [21],
+ 14: [22, 23],
+ 16: [24, 25, 26],
+ 17: [27, 28, 29, 30, 31],
+ 19: [32, 33],
+ 22: [34, 35],
+ 23: [36],
+ 24: [37],
+ 27: [38],
+ 29: [39, 40, 41],
+ 30: [42, 43],
+}
+
+star_locations = {
+ 0x770401: LocationName.grass_land_1_s1,
+ 0x770402: LocationName.grass_land_1_s2,
+ 0x770403: LocationName.grass_land_1_s3,
+ 0x770404: LocationName.grass_land_1_s4,
+ 0x770405: LocationName.grass_land_1_s5,
+ 0x770406: LocationName.grass_land_1_s6,
+ 0x770407: LocationName.grass_land_1_s7,
+ 0x770408: LocationName.grass_land_1_s8,
+ 0x770409: LocationName.grass_land_1_s9,
+ 0x77040a: LocationName.grass_land_1_s10,
+ 0x77040b: LocationName.grass_land_1_s11,
+ 0x77040c: LocationName.grass_land_1_s12,
+ 0x77040d: LocationName.grass_land_1_s13,
+ 0x77040e: LocationName.grass_land_1_s14,
+ 0x77040f: LocationName.grass_land_1_s15,
+ 0x770410: LocationName.grass_land_1_s16,
+ 0x770411: LocationName.grass_land_1_s17,
+ 0x770412: LocationName.grass_land_1_s18,
+ 0x770413: LocationName.grass_land_1_s19,
+ 0x770414: LocationName.grass_land_1_s20,
+ 0x770415: LocationName.grass_land_1_s21,
+ 0x770416: LocationName.grass_land_1_s22,
+ 0x770417: LocationName.grass_land_1_s23,
+ 0x770418: LocationName.grass_land_2_s1,
+ 0x770419: LocationName.grass_land_2_s2,
+ 0x77041a: LocationName.grass_land_2_s3,
+ 0x77041b: LocationName.grass_land_2_s4,
+ 0x77041c: LocationName.grass_land_2_s5,
+ 0x77041d: LocationName.grass_land_2_s6,
+ 0x77041e: LocationName.grass_land_2_s7,
+ 0x77041f: LocationName.grass_land_2_s8,
+ 0x770420: LocationName.grass_land_2_s9,
+ 0x770421: LocationName.grass_land_2_s10,
+ 0x770422: LocationName.grass_land_2_s11,
+ 0x770423: LocationName.grass_land_2_s12,
+ 0x770424: LocationName.grass_land_2_s13,
+ 0x770425: LocationName.grass_land_2_s14,
+ 0x770426: LocationName.grass_land_2_s15,
+ 0x770427: LocationName.grass_land_2_s16,
+ 0x770428: LocationName.grass_land_2_s17,
+ 0x770429: LocationName.grass_land_2_s18,
+ 0x77042a: LocationName.grass_land_2_s19,
+ 0x77042b: LocationName.grass_land_2_s20,
+ 0x77042c: LocationName.grass_land_2_s21,
+ 0x77042d: LocationName.grass_land_3_s1,
+ 0x77042e: LocationName.grass_land_3_s2,
+ 0x77042f: LocationName.grass_land_3_s3,
+ 0x770430: LocationName.grass_land_3_s4,
+ 0x770431: LocationName.grass_land_3_s5,
+ 0x770432: LocationName.grass_land_3_s6,
+ 0x770433: LocationName.grass_land_3_s7,
+ 0x770434: LocationName.grass_land_3_s8,
+ 0x770435: LocationName.grass_land_3_s9,
+ 0x770436: LocationName.grass_land_3_s10,
+ 0x770437: LocationName.grass_land_3_s11,
+ 0x770438: LocationName.grass_land_3_s12,
+ 0x770439: LocationName.grass_land_3_s13,
+ 0x77043a: LocationName.grass_land_3_s14,
+ 0x77043b: LocationName.grass_land_3_s15,
+ 0x77043c: LocationName.grass_land_3_s16,
+ 0x77043d: LocationName.grass_land_3_s17,
+ 0x77043e: LocationName.grass_land_3_s18,
+ 0x77043f: LocationName.grass_land_3_s19,
+ 0x770440: LocationName.grass_land_3_s20,
+ 0x770441: LocationName.grass_land_3_s21,
+ 0x770442: LocationName.grass_land_3_s22,
+ 0x770443: LocationName.grass_land_3_s23,
+ 0x770444: LocationName.grass_land_3_s24,
+ 0x770445: LocationName.grass_land_3_s25,
+ 0x770446: LocationName.grass_land_3_s26,
+ 0x770447: LocationName.grass_land_3_s27,
+ 0x770448: LocationName.grass_land_3_s28,
+ 0x770449: LocationName.grass_land_3_s29,
+ 0x77044a: LocationName.grass_land_3_s30,
+ 0x77044b: LocationName.grass_land_3_s31,
+ 0x77044c: LocationName.grass_land_4_s1,
+ 0x77044d: LocationName.grass_land_4_s2,
+ 0x77044e: LocationName.grass_land_4_s3,
+ 0x77044f: LocationName.grass_land_4_s4,
+ 0x770450: LocationName.grass_land_4_s5,
+ 0x770451: LocationName.grass_land_4_s6,
+ 0x770452: LocationName.grass_land_4_s7,
+ 0x770453: LocationName.grass_land_4_s8,
+ 0x770454: LocationName.grass_land_4_s9,
+ 0x770455: LocationName.grass_land_4_s10,
+ 0x770456: LocationName.grass_land_4_s11,
+ 0x770457: LocationName.grass_land_4_s12,
+ 0x770458: LocationName.grass_land_4_s13,
+ 0x770459: LocationName.grass_land_4_s14,
+ 0x77045a: LocationName.grass_land_4_s15,
+ 0x77045b: LocationName.grass_land_4_s16,
+ 0x77045c: LocationName.grass_land_4_s17,
+ 0x77045d: LocationName.grass_land_4_s18,
+ 0x77045e: LocationName.grass_land_4_s19,
+ 0x77045f: LocationName.grass_land_4_s20,
+ 0x770460: LocationName.grass_land_4_s21,
+ 0x770461: LocationName.grass_land_4_s22,
+ 0x770462: LocationName.grass_land_4_s23,
+ 0x770463: LocationName.grass_land_4_s24,
+ 0x770464: LocationName.grass_land_4_s25,
+ 0x770465: LocationName.grass_land_4_s26,
+ 0x770466: LocationName.grass_land_4_s27,
+ 0x770467: LocationName.grass_land_4_s28,
+ 0x770468: LocationName.grass_land_4_s29,
+ 0x770469: LocationName.grass_land_4_s30,
+ 0x77046a: LocationName.grass_land_4_s31,
+ 0x77046b: LocationName.grass_land_4_s32,
+ 0x77046c: LocationName.grass_land_4_s33,
+ 0x77046d: LocationName.grass_land_4_s34,
+ 0x77046e: LocationName.grass_land_4_s35,
+ 0x77046f: LocationName.grass_land_4_s36,
+ 0x770470: LocationName.grass_land_4_s37,
+ 0x770471: LocationName.grass_land_5_s1,
+ 0x770472: LocationName.grass_land_5_s2,
+ 0x770473: LocationName.grass_land_5_s3,
+ 0x770474: LocationName.grass_land_5_s4,
+ 0x770475: LocationName.grass_land_5_s5,
+ 0x770476: LocationName.grass_land_5_s6,
+ 0x770477: LocationName.grass_land_5_s7,
+ 0x770478: LocationName.grass_land_5_s8,
+ 0x770479: LocationName.grass_land_5_s9,
+ 0x77047a: LocationName.grass_land_5_s10,
+ 0x77047b: LocationName.grass_land_5_s11,
+ 0x77047c: LocationName.grass_land_5_s12,
+ 0x77047d: LocationName.grass_land_5_s13,
+ 0x77047e: LocationName.grass_land_5_s14,
+ 0x77047f: LocationName.grass_land_5_s15,
+ 0x770480: LocationName.grass_land_5_s16,
+ 0x770481: LocationName.grass_land_5_s17,
+ 0x770482: LocationName.grass_land_5_s18,
+ 0x770483: LocationName.grass_land_5_s19,
+ 0x770484: LocationName.grass_land_5_s20,
+ 0x770485: LocationName.grass_land_5_s21,
+ 0x770486: LocationName.grass_land_5_s22,
+ 0x770487: LocationName.grass_land_5_s23,
+ 0x770488: LocationName.grass_land_5_s24,
+ 0x770489: LocationName.grass_land_5_s25,
+ 0x77048a: LocationName.grass_land_5_s26,
+ 0x77048b: LocationName.grass_land_5_s27,
+ 0x77048c: LocationName.grass_land_5_s28,
+ 0x77048d: LocationName.grass_land_5_s29,
+ 0x77048e: LocationName.grass_land_6_s1,
+ 0x77048f: LocationName.grass_land_6_s2,
+ 0x770490: LocationName.grass_land_6_s3,
+ 0x770491: LocationName.grass_land_6_s4,
+ 0x770492: LocationName.grass_land_6_s5,
+ 0x770493: LocationName.grass_land_6_s6,
+ 0x770494: LocationName.grass_land_6_s7,
+ 0x770495: LocationName.grass_land_6_s8,
+ 0x770496: LocationName.grass_land_6_s9,
+ 0x770497: LocationName.grass_land_6_s10,
+ 0x770498: LocationName.grass_land_6_s11,
+ 0x770499: LocationName.grass_land_6_s12,
+ 0x77049a: LocationName.grass_land_6_s13,
+ 0x77049b: LocationName.grass_land_6_s14,
+ 0x77049c: LocationName.grass_land_6_s15,
+ 0x77049d: LocationName.grass_land_6_s16,
+ 0x77049e: LocationName.grass_land_6_s17,
+ 0x77049f: LocationName.grass_land_6_s18,
+ 0x7704a0: LocationName.grass_land_6_s19,
+ 0x7704a1: LocationName.grass_land_6_s20,
+ 0x7704a2: LocationName.grass_land_6_s21,
+ 0x7704a3: LocationName.grass_land_6_s22,
+ 0x7704a4: LocationName.grass_land_6_s23,
+ 0x7704a5: LocationName.grass_land_6_s24,
+ 0x7704a6: LocationName.grass_land_6_s25,
+ 0x7704a7: LocationName.grass_land_6_s26,
+ 0x7704a8: LocationName.grass_land_6_s27,
+ 0x7704a9: LocationName.grass_land_6_s28,
+ 0x7704aa: LocationName.grass_land_6_s29,
+ 0x7704ab: LocationName.ripple_field_1_s1,
+ 0x7704ac: LocationName.ripple_field_1_s2,
+ 0x7704ad: LocationName.ripple_field_1_s3,
+ 0x7704ae: LocationName.ripple_field_1_s4,
+ 0x7704af: LocationName.ripple_field_1_s5,
+ 0x7704b0: LocationName.ripple_field_1_s6,
+ 0x7704b1: LocationName.ripple_field_1_s7,
+ 0x7704b2: LocationName.ripple_field_1_s8,
+ 0x7704b3: LocationName.ripple_field_1_s9,
+ 0x7704b4: LocationName.ripple_field_1_s10,
+ 0x7704b5: LocationName.ripple_field_1_s11,
+ 0x7704b6: LocationName.ripple_field_1_s12,
+ 0x7704b7: LocationName.ripple_field_1_s13,
+ 0x7704b8: LocationName.ripple_field_1_s14,
+ 0x7704b9: LocationName.ripple_field_1_s15,
+ 0x7704ba: LocationName.ripple_field_1_s16,
+ 0x7704bb: LocationName.ripple_field_1_s17,
+ 0x7704bc: LocationName.ripple_field_1_s18,
+ 0x7704bd: LocationName.ripple_field_1_s19,
+ 0x7704be: LocationName.ripple_field_2_s1,
+ 0x7704bf: LocationName.ripple_field_2_s2,
+ 0x7704c0: LocationName.ripple_field_2_s3,
+ 0x7704c1: LocationName.ripple_field_2_s4,
+ 0x7704c2: LocationName.ripple_field_2_s5,
+ 0x7704c3: LocationName.ripple_field_2_s6,
+ 0x7704c4: LocationName.ripple_field_2_s7,
+ 0x7704c5: LocationName.ripple_field_2_s8,
+ 0x7704c6: LocationName.ripple_field_2_s9,
+ 0x7704c7: LocationName.ripple_field_2_s10,
+ 0x7704c8: LocationName.ripple_field_2_s11,
+ 0x7704c9: LocationName.ripple_field_2_s12,
+ 0x7704ca: LocationName.ripple_field_2_s13,
+ 0x7704cb: LocationName.ripple_field_2_s14,
+ 0x7704cc: LocationName.ripple_field_2_s15,
+ 0x7704cd: LocationName.ripple_field_2_s16,
+ 0x7704ce: LocationName.ripple_field_2_s17,
+ 0x7704cf: LocationName.ripple_field_3_s1,
+ 0x7704d0: LocationName.ripple_field_3_s2,
+ 0x7704d1: LocationName.ripple_field_3_s3,
+ 0x7704d2: LocationName.ripple_field_3_s4,
+ 0x7704d3: LocationName.ripple_field_3_s5,
+ 0x7704d4: LocationName.ripple_field_3_s6,
+ 0x7704d5: LocationName.ripple_field_3_s7,
+ 0x7704d6: LocationName.ripple_field_3_s8,
+ 0x7704d7: LocationName.ripple_field_3_s9,
+ 0x7704d8: LocationName.ripple_field_3_s10,
+ 0x7704d9: LocationName.ripple_field_3_s11,
+ 0x7704da: LocationName.ripple_field_3_s12,
+ 0x7704db: LocationName.ripple_field_3_s13,
+ 0x7704dc: LocationName.ripple_field_3_s14,
+ 0x7704dd: LocationName.ripple_field_3_s15,
+ 0x7704de: LocationName.ripple_field_3_s16,
+ 0x7704df: LocationName.ripple_field_3_s17,
+ 0x7704e0: LocationName.ripple_field_3_s18,
+ 0x7704e1: LocationName.ripple_field_3_s19,
+ 0x7704e2: LocationName.ripple_field_3_s20,
+ 0x7704e3: LocationName.ripple_field_3_s21,
+ 0x7704e4: LocationName.ripple_field_4_s1,
+ 0x7704e5: LocationName.ripple_field_4_s2,
+ 0x7704e6: LocationName.ripple_field_4_s3,
+ 0x7704e7: LocationName.ripple_field_4_s4,
+ 0x7704e8: LocationName.ripple_field_4_s5,
+ 0x7704e9: LocationName.ripple_field_4_s6,
+ 0x7704ea: LocationName.ripple_field_4_s7,
+ 0x7704eb: LocationName.ripple_field_4_s8,
+ 0x7704ec: LocationName.ripple_field_4_s9,
+ 0x7704ed: LocationName.ripple_field_4_s10,
+ 0x7704ee: LocationName.ripple_field_4_s11,
+ 0x7704ef: LocationName.ripple_field_4_s12,
+ 0x7704f0: LocationName.ripple_field_4_s13,
+ 0x7704f1: LocationName.ripple_field_4_s14,
+ 0x7704f2: LocationName.ripple_field_4_s15,
+ 0x7704f3: LocationName.ripple_field_4_s16,
+ 0x7704f4: LocationName.ripple_field_4_s17,
+ 0x7704f5: LocationName.ripple_field_4_s18,
+ 0x7704f6: LocationName.ripple_field_4_s19,
+ 0x7704f7: LocationName.ripple_field_4_s20,
+ 0x7704f8: LocationName.ripple_field_4_s21,
+ 0x7704f9: LocationName.ripple_field_4_s22,
+ 0x7704fa: LocationName.ripple_field_4_s23,
+ 0x7704fb: LocationName.ripple_field_4_s24,
+ 0x7704fc: LocationName.ripple_field_4_s25,
+ 0x7704fd: LocationName.ripple_field_4_s26,
+ 0x7704fe: LocationName.ripple_field_4_s27,
+ 0x7704ff: LocationName.ripple_field_4_s28,
+ 0x770500: LocationName.ripple_field_4_s29,
+ 0x770501: LocationName.ripple_field_4_s30,
+ 0x770502: LocationName.ripple_field_4_s31,
+ 0x770503: LocationName.ripple_field_4_s32,
+ 0x770504: LocationName.ripple_field_4_s33,
+ 0x770505: LocationName.ripple_field_4_s34,
+ 0x770506: LocationName.ripple_field_4_s35,
+ 0x770507: LocationName.ripple_field_4_s36,
+ 0x770508: LocationName.ripple_field_4_s37,
+ 0x770509: LocationName.ripple_field_4_s38,
+ 0x77050a: LocationName.ripple_field_4_s39,
+ 0x77050b: LocationName.ripple_field_4_s40,
+ 0x77050c: LocationName.ripple_field_4_s41,
+ 0x77050d: LocationName.ripple_field_4_s42,
+ 0x77050e: LocationName.ripple_field_4_s43,
+ 0x77050f: LocationName.ripple_field_4_s44,
+ 0x770510: LocationName.ripple_field_4_s45,
+ 0x770511: LocationName.ripple_field_4_s46,
+ 0x770512: LocationName.ripple_field_4_s47,
+ 0x770513: LocationName.ripple_field_4_s48,
+ 0x770514: LocationName.ripple_field_4_s49,
+ 0x770515: LocationName.ripple_field_4_s50,
+ 0x770516: LocationName.ripple_field_4_s51,
+ 0x770517: LocationName.ripple_field_5_s1,
+ 0x770518: LocationName.ripple_field_5_s2,
+ 0x770519: LocationName.ripple_field_5_s3,
+ 0x77051a: LocationName.ripple_field_5_s4,
+ 0x77051b: LocationName.ripple_field_5_s5,
+ 0x77051c: LocationName.ripple_field_5_s6,
+ 0x77051d: LocationName.ripple_field_5_s7,
+ 0x77051e: LocationName.ripple_field_5_s8,
+ 0x77051f: LocationName.ripple_field_5_s9,
+ 0x770520: LocationName.ripple_field_5_s10,
+ 0x770521: LocationName.ripple_field_5_s11,
+ 0x770522: LocationName.ripple_field_5_s12,
+ 0x770523: LocationName.ripple_field_5_s13,
+ 0x770524: LocationName.ripple_field_5_s14,
+ 0x770525: LocationName.ripple_field_5_s15,
+ 0x770526: LocationName.ripple_field_5_s16,
+ 0x770527: LocationName.ripple_field_5_s17,
+ 0x770528: LocationName.ripple_field_5_s18,
+ 0x770529: LocationName.ripple_field_5_s19,
+ 0x77052a: LocationName.ripple_field_5_s20,
+ 0x77052b: LocationName.ripple_field_5_s21,
+ 0x77052c: LocationName.ripple_field_5_s22,
+ 0x77052d: LocationName.ripple_field_5_s23,
+ 0x77052e: LocationName.ripple_field_5_s24,
+ 0x77052f: LocationName.ripple_field_5_s25,
+ 0x770530: LocationName.ripple_field_5_s26,
+ 0x770531: LocationName.ripple_field_5_s27,
+ 0x770532: LocationName.ripple_field_5_s28,
+ 0x770533: LocationName.ripple_field_5_s29,
+ 0x770534: LocationName.ripple_field_5_s30,
+ 0x770535: LocationName.ripple_field_5_s31,
+ 0x770536: LocationName.ripple_field_5_s32,
+ 0x770537: LocationName.ripple_field_5_s33,
+ 0x770538: LocationName.ripple_field_5_s34,
+ 0x770539: LocationName.ripple_field_5_s35,
+ 0x77053a: LocationName.ripple_field_5_s36,
+ 0x77053b: LocationName.ripple_field_5_s37,
+ 0x77053c: LocationName.ripple_field_5_s38,
+ 0x77053d: LocationName.ripple_field_5_s39,
+ 0x77053e: LocationName.ripple_field_5_s40,
+ 0x77053f: LocationName.ripple_field_5_s41,
+ 0x770540: LocationName.ripple_field_5_s42,
+ 0x770541: LocationName.ripple_field_5_s43,
+ 0x770542: LocationName.ripple_field_5_s44,
+ 0x770543: LocationName.ripple_field_5_s45,
+ 0x770544: LocationName.ripple_field_5_s46,
+ 0x770545: LocationName.ripple_field_5_s47,
+ 0x770546: LocationName.ripple_field_5_s48,
+ 0x770547: LocationName.ripple_field_5_s49,
+ 0x770548: LocationName.ripple_field_5_s50,
+ 0x770549: LocationName.ripple_field_5_s51,
+ 0x77054a: LocationName.ripple_field_6_s1,
+ 0x77054b: LocationName.ripple_field_6_s2,
+ 0x77054c: LocationName.ripple_field_6_s3,
+ 0x77054d: LocationName.ripple_field_6_s4,
+ 0x77054e: LocationName.ripple_field_6_s5,
+ 0x77054f: LocationName.ripple_field_6_s6,
+ 0x770550: LocationName.ripple_field_6_s7,
+ 0x770551: LocationName.ripple_field_6_s8,
+ 0x770552: LocationName.ripple_field_6_s9,
+ 0x770553: LocationName.ripple_field_6_s10,
+ 0x770554: LocationName.ripple_field_6_s11,
+ 0x770555: LocationName.ripple_field_6_s12,
+ 0x770556: LocationName.ripple_field_6_s13,
+ 0x770557: LocationName.ripple_field_6_s14,
+ 0x770558: LocationName.ripple_field_6_s15,
+ 0x770559: LocationName.ripple_field_6_s16,
+ 0x77055a: LocationName.ripple_field_6_s17,
+ 0x77055b: LocationName.ripple_field_6_s18,
+ 0x77055c: LocationName.ripple_field_6_s19,
+ 0x77055d: LocationName.ripple_field_6_s20,
+ 0x77055e: LocationName.ripple_field_6_s21,
+ 0x77055f: LocationName.ripple_field_6_s22,
+ 0x770560: LocationName.ripple_field_6_s23,
+ 0x770561: LocationName.sand_canyon_1_s1,
+ 0x770562: LocationName.sand_canyon_1_s2,
+ 0x770563: LocationName.sand_canyon_1_s3,
+ 0x770564: LocationName.sand_canyon_1_s4,
+ 0x770565: LocationName.sand_canyon_1_s5,
+ 0x770566: LocationName.sand_canyon_1_s6,
+ 0x770567: LocationName.sand_canyon_1_s7,
+ 0x770568: LocationName.sand_canyon_1_s8,
+ 0x770569: LocationName.sand_canyon_1_s9,
+ 0x77056a: LocationName.sand_canyon_1_s10,
+ 0x77056b: LocationName.sand_canyon_1_s11,
+ 0x77056c: LocationName.sand_canyon_1_s12,
+ 0x77056d: LocationName.sand_canyon_1_s13,
+ 0x77056e: LocationName.sand_canyon_1_s14,
+ 0x77056f: LocationName.sand_canyon_1_s15,
+ 0x770570: LocationName.sand_canyon_1_s16,
+ 0x770571: LocationName.sand_canyon_1_s17,
+ 0x770572: LocationName.sand_canyon_1_s18,
+ 0x770573: LocationName.sand_canyon_1_s19,
+ 0x770574: LocationName.sand_canyon_1_s20,
+ 0x770575: LocationName.sand_canyon_1_s21,
+ 0x770576: LocationName.sand_canyon_1_s22,
+ 0x770577: LocationName.sand_canyon_2_s1,
+ 0x770578: LocationName.sand_canyon_2_s2,
+ 0x770579: LocationName.sand_canyon_2_s3,
+ 0x77057a: LocationName.sand_canyon_2_s4,
+ 0x77057b: LocationName.sand_canyon_2_s5,
+ 0x77057c: LocationName.sand_canyon_2_s6,
+ 0x77057d: LocationName.sand_canyon_2_s7,
+ 0x77057e: LocationName.sand_canyon_2_s8,
+ 0x77057f: LocationName.sand_canyon_2_s9,
+ 0x770580: LocationName.sand_canyon_2_s10,
+ 0x770581: LocationName.sand_canyon_2_s11,
+ 0x770582: LocationName.sand_canyon_2_s12,
+ 0x770583: LocationName.sand_canyon_2_s13,
+ 0x770584: LocationName.sand_canyon_2_s14,
+ 0x770585: LocationName.sand_canyon_2_s15,
+ 0x770586: LocationName.sand_canyon_2_s16,
+ 0x770587: LocationName.sand_canyon_2_s17,
+ 0x770588: LocationName.sand_canyon_2_s18,
+ 0x770589: LocationName.sand_canyon_2_s19,
+ 0x77058a: LocationName.sand_canyon_2_s20,
+ 0x77058b: LocationName.sand_canyon_2_s21,
+ 0x77058c: LocationName.sand_canyon_2_s22,
+ 0x77058d: LocationName.sand_canyon_2_s23,
+ 0x77058e: LocationName.sand_canyon_2_s24,
+ 0x77058f: LocationName.sand_canyon_2_s25,
+ 0x770590: LocationName.sand_canyon_2_s26,
+ 0x770591: LocationName.sand_canyon_2_s27,
+ 0x770592: LocationName.sand_canyon_2_s28,
+ 0x770593: LocationName.sand_canyon_2_s29,
+ 0x770594: LocationName.sand_canyon_2_s30,
+ 0x770595: LocationName.sand_canyon_2_s31,
+ 0x770596: LocationName.sand_canyon_2_s32,
+ 0x770597: LocationName.sand_canyon_2_s33,
+ 0x770598: LocationName.sand_canyon_2_s34,
+ 0x770599: LocationName.sand_canyon_2_s35,
+ 0x77059a: LocationName.sand_canyon_2_s36,
+ 0x77059b: LocationName.sand_canyon_2_s37,
+ 0x77059c: LocationName.sand_canyon_2_s38,
+ 0x77059d: LocationName.sand_canyon_2_s39,
+ 0x77059e: LocationName.sand_canyon_2_s40,
+ 0x77059f: LocationName.sand_canyon_2_s41,
+ 0x7705a0: LocationName.sand_canyon_2_s42,
+ 0x7705a1: LocationName.sand_canyon_2_s43,
+ 0x7705a2: LocationName.sand_canyon_2_s44,
+ 0x7705a3: LocationName.sand_canyon_2_s45,
+ 0x7705a4: LocationName.sand_canyon_2_s46,
+ 0x7705a5: LocationName.sand_canyon_2_s47,
+ 0x7705a6: LocationName.sand_canyon_2_s48,
+ 0x7705a7: LocationName.sand_canyon_3_s1,
+ 0x7705a8: LocationName.sand_canyon_3_s2,
+ 0x7705a9: LocationName.sand_canyon_3_s3,
+ 0x7705aa: LocationName.sand_canyon_3_s4,
+ 0x7705ab: LocationName.sand_canyon_3_s5,
+ 0x7705ac: LocationName.sand_canyon_3_s6,
+ 0x7705ad: LocationName.sand_canyon_3_s7,
+ 0x7705ae: LocationName.sand_canyon_3_s8,
+ 0x7705af: LocationName.sand_canyon_3_s9,
+ 0x7705b0: LocationName.sand_canyon_3_s10,
+ 0x7705b1: LocationName.sand_canyon_4_s1,
+ 0x7705b2: LocationName.sand_canyon_4_s2,
+ 0x7705b3: LocationName.sand_canyon_4_s3,
+ 0x7705b4: LocationName.sand_canyon_4_s4,
+ 0x7705b5: LocationName.sand_canyon_4_s5,
+ 0x7705b6: LocationName.sand_canyon_4_s6,
+ 0x7705b7: LocationName.sand_canyon_4_s7,
+ 0x7705b8: LocationName.sand_canyon_4_s8,
+ 0x7705b9: LocationName.sand_canyon_4_s9,
+ 0x7705ba: LocationName.sand_canyon_4_s10,
+ 0x7705bb: LocationName.sand_canyon_4_s11,
+ 0x7705bc: LocationName.sand_canyon_4_s12,
+ 0x7705bd: LocationName.sand_canyon_4_s13,
+ 0x7705be: LocationName.sand_canyon_4_s14,
+ 0x7705bf: LocationName.sand_canyon_4_s15,
+ 0x7705c0: LocationName.sand_canyon_4_s16,
+ 0x7705c1: LocationName.sand_canyon_4_s17,
+ 0x7705c2: LocationName.sand_canyon_4_s18,
+ 0x7705c3: LocationName.sand_canyon_4_s19,
+ 0x7705c4: LocationName.sand_canyon_4_s20,
+ 0x7705c5: LocationName.sand_canyon_4_s21,
+ 0x7705c6: LocationName.sand_canyon_4_s22,
+ 0x7705c7: LocationName.sand_canyon_4_s23,
+ 0x7705c8: LocationName.sand_canyon_5_s1,
+ 0x7705c9: LocationName.sand_canyon_5_s2,
+ 0x7705ca: LocationName.sand_canyon_5_s3,
+ 0x7705cb: LocationName.sand_canyon_5_s4,
+ 0x7705cc: LocationName.sand_canyon_5_s5,
+ 0x7705cd: LocationName.sand_canyon_5_s6,
+ 0x7705ce: LocationName.sand_canyon_5_s7,
+ 0x7705cf: LocationName.sand_canyon_5_s8,
+ 0x7705d0: LocationName.sand_canyon_5_s9,
+ 0x7705d1: LocationName.sand_canyon_5_s10,
+ 0x7705d2: LocationName.sand_canyon_5_s11,
+ 0x7705d3: LocationName.sand_canyon_5_s12,
+ 0x7705d4: LocationName.sand_canyon_5_s13,
+ 0x7705d5: LocationName.sand_canyon_5_s14,
+ 0x7705d6: LocationName.sand_canyon_5_s15,
+ 0x7705d7: LocationName.sand_canyon_5_s16,
+ 0x7705d8: LocationName.sand_canyon_5_s17,
+ 0x7705d9: LocationName.sand_canyon_5_s18,
+ 0x7705da: LocationName.sand_canyon_5_s19,
+ 0x7705db: LocationName.sand_canyon_5_s20,
+ 0x7705dc: LocationName.sand_canyon_5_s21,
+ 0x7705dd: LocationName.sand_canyon_5_s22,
+ 0x7705de: LocationName.sand_canyon_5_s23,
+ 0x7705df: LocationName.sand_canyon_5_s24,
+ 0x7705e0: LocationName.sand_canyon_5_s25,
+ 0x7705e1: LocationName.sand_canyon_5_s26,
+ 0x7705e2: LocationName.sand_canyon_5_s27,
+ 0x7705e3: LocationName.sand_canyon_5_s28,
+ 0x7705e4: LocationName.sand_canyon_5_s29,
+ 0x7705e5: LocationName.sand_canyon_5_s30,
+ 0x7705e6: LocationName.sand_canyon_5_s31,
+ 0x7705e7: LocationName.sand_canyon_5_s32,
+ 0x7705e8: LocationName.sand_canyon_5_s33,
+ 0x7705e9: LocationName.sand_canyon_5_s34,
+ 0x7705ea: LocationName.sand_canyon_5_s35,
+ 0x7705eb: LocationName.sand_canyon_5_s36,
+ 0x7705ec: LocationName.sand_canyon_5_s37,
+ 0x7705ed: LocationName.sand_canyon_5_s38,
+ 0x7705ee: LocationName.sand_canyon_5_s39,
+ 0x7705ef: LocationName.sand_canyon_5_s40,
+ 0x7705f0: LocationName.cloudy_park_1_s1,
+ 0x7705f1: LocationName.cloudy_park_1_s2,
+ 0x7705f2: LocationName.cloudy_park_1_s3,
+ 0x7705f3: LocationName.cloudy_park_1_s4,
+ 0x7705f4: LocationName.cloudy_park_1_s5,
+ 0x7705f5: LocationName.cloudy_park_1_s6,
+ 0x7705f6: LocationName.cloudy_park_1_s7,
+ 0x7705f7: LocationName.cloudy_park_1_s8,
+ 0x7705f8: LocationName.cloudy_park_1_s9,
+ 0x7705f9: LocationName.cloudy_park_1_s10,
+ 0x7705fa: LocationName.cloudy_park_1_s11,
+ 0x7705fb: LocationName.cloudy_park_1_s12,
+ 0x7705fc: LocationName.cloudy_park_1_s13,
+ 0x7705fd: LocationName.cloudy_park_1_s14,
+ 0x7705fe: LocationName.cloudy_park_1_s15,
+ 0x7705ff: LocationName.cloudy_park_1_s16,
+ 0x770600: LocationName.cloudy_park_1_s17,
+ 0x770601: LocationName.cloudy_park_1_s18,
+ 0x770602: LocationName.cloudy_park_1_s19,
+ 0x770603: LocationName.cloudy_park_1_s20,
+ 0x770604: LocationName.cloudy_park_1_s21,
+ 0x770605: LocationName.cloudy_park_1_s22,
+ 0x770606: LocationName.cloudy_park_1_s23,
+ 0x770607: LocationName.cloudy_park_2_s1,
+ 0x770608: LocationName.cloudy_park_2_s2,
+ 0x770609: LocationName.cloudy_park_2_s3,
+ 0x77060a: LocationName.cloudy_park_2_s4,
+ 0x77060b: LocationName.cloudy_park_2_s5,
+ 0x77060c: LocationName.cloudy_park_2_s6,
+ 0x77060d: LocationName.cloudy_park_2_s7,
+ 0x77060e: LocationName.cloudy_park_2_s8,
+ 0x77060f: LocationName.cloudy_park_2_s9,
+ 0x770610: LocationName.cloudy_park_2_s10,
+ 0x770611: LocationName.cloudy_park_2_s11,
+ 0x770612: LocationName.cloudy_park_2_s12,
+ 0x770613: LocationName.cloudy_park_2_s13,
+ 0x770614: LocationName.cloudy_park_2_s14,
+ 0x770615: LocationName.cloudy_park_2_s15,
+ 0x770616: LocationName.cloudy_park_2_s16,
+ 0x770617: LocationName.cloudy_park_2_s17,
+ 0x770618: LocationName.cloudy_park_2_s18,
+ 0x770619: LocationName.cloudy_park_2_s19,
+ 0x77061a: LocationName.cloudy_park_2_s20,
+ 0x77061b: LocationName.cloudy_park_2_s21,
+ 0x77061c: LocationName.cloudy_park_2_s22,
+ 0x77061d: LocationName.cloudy_park_2_s23,
+ 0x77061e: LocationName.cloudy_park_2_s24,
+ 0x77061f: LocationName.cloudy_park_2_s25,
+ 0x770620: LocationName.cloudy_park_2_s26,
+ 0x770621: LocationName.cloudy_park_2_s27,
+ 0x770622: LocationName.cloudy_park_2_s28,
+ 0x770623: LocationName.cloudy_park_2_s29,
+ 0x770624: LocationName.cloudy_park_2_s30,
+ 0x770625: LocationName.cloudy_park_2_s31,
+ 0x770626: LocationName.cloudy_park_2_s32,
+ 0x770627: LocationName.cloudy_park_2_s33,
+ 0x770628: LocationName.cloudy_park_2_s34,
+ 0x770629: LocationName.cloudy_park_2_s35,
+ 0x77062a: LocationName.cloudy_park_2_s36,
+ 0x77062b: LocationName.cloudy_park_2_s37,
+ 0x77062c: LocationName.cloudy_park_2_s38,
+ 0x77062d: LocationName.cloudy_park_2_s39,
+ 0x77062e: LocationName.cloudy_park_2_s40,
+ 0x77062f: LocationName.cloudy_park_2_s41,
+ 0x770630: LocationName.cloudy_park_2_s42,
+ 0x770631: LocationName.cloudy_park_2_s43,
+ 0x770632: LocationName.cloudy_park_2_s44,
+ 0x770633: LocationName.cloudy_park_2_s45,
+ 0x770634: LocationName.cloudy_park_2_s46,
+ 0x770635: LocationName.cloudy_park_2_s47,
+ 0x770636: LocationName.cloudy_park_2_s48,
+ 0x770637: LocationName.cloudy_park_2_s49,
+ 0x770638: LocationName.cloudy_park_2_s50,
+ 0x770639: LocationName.cloudy_park_2_s51,
+ 0x77063a: LocationName.cloudy_park_2_s52,
+ 0x77063b: LocationName.cloudy_park_2_s53,
+ 0x77063c: LocationName.cloudy_park_2_s54,
+ 0x77063d: LocationName.cloudy_park_3_s1,
+ 0x77063e: LocationName.cloudy_park_3_s2,
+ 0x77063f: LocationName.cloudy_park_3_s3,
+ 0x770640: LocationName.cloudy_park_3_s4,
+ 0x770641: LocationName.cloudy_park_3_s5,
+ 0x770642: LocationName.cloudy_park_3_s6,
+ 0x770643: LocationName.cloudy_park_3_s7,
+ 0x770644: LocationName.cloudy_park_3_s8,
+ 0x770645: LocationName.cloudy_park_3_s9,
+ 0x770646: LocationName.cloudy_park_3_s10,
+ 0x770647: LocationName.cloudy_park_3_s11,
+ 0x770648: LocationName.cloudy_park_3_s12,
+ 0x770649: LocationName.cloudy_park_3_s13,
+ 0x77064a: LocationName.cloudy_park_3_s14,
+ 0x77064b: LocationName.cloudy_park_3_s15,
+ 0x77064c: LocationName.cloudy_park_3_s16,
+ 0x77064d: LocationName.cloudy_park_3_s17,
+ 0x77064e: LocationName.cloudy_park_3_s18,
+ 0x77064f: LocationName.cloudy_park_3_s19,
+ 0x770650: LocationName.cloudy_park_3_s20,
+ 0x770651: LocationName.cloudy_park_3_s21,
+ 0x770652: LocationName.cloudy_park_3_s22,
+ 0x770653: LocationName.cloudy_park_4_s1,
+ 0x770654: LocationName.cloudy_park_4_s2,
+ 0x770655: LocationName.cloudy_park_4_s3,
+ 0x770656: LocationName.cloudy_park_4_s4,
+ 0x770657: LocationName.cloudy_park_4_s5,
+ 0x770658: LocationName.cloudy_park_4_s6,
+ 0x770659: LocationName.cloudy_park_4_s7,
+ 0x77065a: LocationName.cloudy_park_4_s8,
+ 0x77065b: LocationName.cloudy_park_4_s9,
+ 0x77065c: LocationName.cloudy_park_4_s10,
+ 0x77065d: LocationName.cloudy_park_4_s11,
+ 0x77065e: LocationName.cloudy_park_4_s12,
+ 0x77065f: LocationName.cloudy_park_4_s13,
+ 0x770660: LocationName.cloudy_park_4_s14,
+ 0x770661: LocationName.cloudy_park_4_s15,
+ 0x770662: LocationName.cloudy_park_4_s16,
+ 0x770663: LocationName.cloudy_park_4_s17,
+ 0x770664: LocationName.cloudy_park_4_s18,
+ 0x770665: LocationName.cloudy_park_4_s19,
+ 0x770666: LocationName.cloudy_park_4_s20,
+ 0x770667: LocationName.cloudy_park_4_s21,
+ 0x770668: LocationName.cloudy_park_4_s22,
+ 0x770669: LocationName.cloudy_park_4_s23,
+ 0x77066a: LocationName.cloudy_park_4_s24,
+ 0x77066b: LocationName.cloudy_park_4_s25,
+ 0x77066c: LocationName.cloudy_park_4_s26,
+ 0x77066d: LocationName.cloudy_park_4_s27,
+ 0x77066e: LocationName.cloudy_park_4_s28,
+ 0x77066f: LocationName.cloudy_park_4_s29,
+ 0x770670: LocationName.cloudy_park_4_s30,
+ 0x770671: LocationName.cloudy_park_4_s31,
+ 0x770672: LocationName.cloudy_park_4_s32,
+ 0x770673: LocationName.cloudy_park_4_s33,
+ 0x770674: LocationName.cloudy_park_4_s34,
+ 0x770675: LocationName.cloudy_park_4_s35,
+ 0x770676: LocationName.cloudy_park_4_s36,
+ 0x770677: LocationName.cloudy_park_4_s37,
+ 0x770678: LocationName.cloudy_park_4_s38,
+ 0x770679: LocationName.cloudy_park_4_s39,
+ 0x77067a: LocationName.cloudy_park_4_s40,
+ 0x77067b: LocationName.cloudy_park_4_s41,
+ 0x77067c: LocationName.cloudy_park_4_s42,
+ 0x77067d: LocationName.cloudy_park_4_s43,
+ 0x77067e: LocationName.cloudy_park_4_s44,
+ 0x77067f: LocationName.cloudy_park_4_s45,
+ 0x770680: LocationName.cloudy_park_4_s46,
+ 0x770681: LocationName.cloudy_park_4_s47,
+ 0x770682: LocationName.cloudy_park_4_s48,
+ 0x770683: LocationName.cloudy_park_4_s49,
+ 0x770684: LocationName.cloudy_park_4_s50,
+ 0x770685: LocationName.cloudy_park_5_s1,
+ 0x770686: LocationName.cloudy_park_5_s2,
+ 0x770687: LocationName.cloudy_park_5_s3,
+ 0x770688: LocationName.cloudy_park_5_s4,
+ 0x770689: LocationName.cloudy_park_5_s5,
+ 0x77068a: LocationName.cloudy_park_5_s6,
+ 0x77068b: LocationName.cloudy_park_6_s1,
+ 0x77068c: LocationName.cloudy_park_6_s2,
+ 0x77068d: LocationName.cloudy_park_6_s3,
+ 0x77068e: LocationName.cloudy_park_6_s4,
+ 0x77068f: LocationName.cloudy_park_6_s5,
+ 0x770690: LocationName.cloudy_park_6_s6,
+ 0x770691: LocationName.cloudy_park_6_s7,
+ 0x770692: LocationName.cloudy_park_6_s8,
+ 0x770693: LocationName.cloudy_park_6_s9,
+ 0x770694: LocationName.cloudy_park_6_s10,
+ 0x770695: LocationName.cloudy_park_6_s11,
+ 0x770696: LocationName.cloudy_park_6_s12,
+ 0x770697: LocationName.cloudy_park_6_s13,
+ 0x770698: LocationName.cloudy_park_6_s14,
+ 0x770699: LocationName.cloudy_park_6_s15,
+ 0x77069a: LocationName.cloudy_park_6_s16,
+ 0x77069b: LocationName.cloudy_park_6_s17,
+ 0x77069c: LocationName.cloudy_park_6_s18,
+ 0x77069d: LocationName.cloudy_park_6_s19,
+ 0x77069e: LocationName.cloudy_park_6_s20,
+ 0x77069f: LocationName.cloudy_park_6_s21,
+ 0x7706a0: LocationName.cloudy_park_6_s22,
+ 0x7706a1: LocationName.cloudy_park_6_s23,
+ 0x7706a2: LocationName.cloudy_park_6_s24,
+ 0x7706a3: LocationName.cloudy_park_6_s25,
+ 0x7706a4: LocationName.cloudy_park_6_s26,
+ 0x7706a5: LocationName.cloudy_park_6_s27,
+ 0x7706a6: LocationName.cloudy_park_6_s28,
+ 0x7706a7: LocationName.cloudy_park_6_s29,
+ 0x7706a8: LocationName.cloudy_park_6_s30,
+ 0x7706a9: LocationName.cloudy_park_6_s31,
+ 0x7706aa: LocationName.cloudy_park_6_s32,
+ 0x7706ab: LocationName.cloudy_park_6_s33,
+ 0x7706ac: LocationName.iceberg_1_s1,
+ 0x7706ad: LocationName.iceberg_1_s2,
+ 0x7706ae: LocationName.iceberg_1_s3,
+ 0x7706af: LocationName.iceberg_1_s4,
+ 0x7706b0: LocationName.iceberg_1_s5,
+ 0x7706b1: LocationName.iceberg_1_s6,
+ 0x7706b2: LocationName.iceberg_2_s1,
+ 0x7706b3: LocationName.iceberg_2_s2,
+ 0x7706b4: LocationName.iceberg_2_s3,
+ 0x7706b5: LocationName.iceberg_2_s4,
+ 0x7706b6: LocationName.iceberg_2_s5,
+ 0x7706b7: LocationName.iceberg_2_s6,
+ 0x7706b8: LocationName.iceberg_2_s7,
+ 0x7706b9: LocationName.iceberg_2_s8,
+ 0x7706ba: LocationName.iceberg_2_s9,
+ 0x7706bb: LocationName.iceberg_2_s10,
+ 0x7706bc: LocationName.iceberg_2_s11,
+ 0x7706bd: LocationName.iceberg_2_s12,
+ 0x7706be: LocationName.iceberg_2_s13,
+ 0x7706bf: LocationName.iceberg_2_s14,
+ 0x7706c0: LocationName.iceberg_2_s15,
+ 0x7706c1: LocationName.iceberg_2_s16,
+ 0x7706c2: LocationName.iceberg_2_s17,
+ 0x7706c3: LocationName.iceberg_2_s18,
+ 0x7706c4: LocationName.iceberg_2_s19,
+ 0x7706c5: LocationName.iceberg_3_s1,
+ 0x7706c6: LocationName.iceberg_3_s2,
+ 0x7706c7: LocationName.iceberg_3_s3,
+ 0x7706c8: LocationName.iceberg_3_s4,
+ 0x7706c9: LocationName.iceberg_3_s5,
+ 0x7706ca: LocationName.iceberg_3_s6,
+ 0x7706cb: LocationName.iceberg_3_s7,
+ 0x7706cc: LocationName.iceberg_3_s8,
+ 0x7706cd: LocationName.iceberg_3_s9,
+ 0x7706ce: LocationName.iceberg_3_s10,
+ 0x7706cf: LocationName.iceberg_3_s11,
+ 0x7706d0: LocationName.iceberg_3_s12,
+ 0x7706d1: LocationName.iceberg_3_s13,
+ 0x7706d2: LocationName.iceberg_3_s14,
+ 0x7706d3: LocationName.iceberg_3_s15,
+ 0x7706d4: LocationName.iceberg_3_s16,
+ 0x7706d5: LocationName.iceberg_3_s17,
+ 0x7706d6: LocationName.iceberg_3_s18,
+ 0x7706d7: LocationName.iceberg_3_s19,
+ 0x7706d8: LocationName.iceberg_3_s20,
+ 0x7706d9: LocationName.iceberg_3_s21,
+ 0x7706da: LocationName.iceberg_4_s1,
+ 0x7706db: LocationName.iceberg_4_s2,
+ 0x7706dc: LocationName.iceberg_4_s3,
+ 0x7706dd: LocationName.iceberg_5_s1,
+ 0x7706de: LocationName.iceberg_5_s2,
+ 0x7706df: LocationName.iceberg_5_s3,
+ 0x7706e0: LocationName.iceberg_5_s4,
+ 0x7706e1: LocationName.iceberg_5_s5,
+ 0x7706e2: LocationName.iceberg_5_s6,
+ 0x7706e3: LocationName.iceberg_5_s7,
+ 0x7706e4: LocationName.iceberg_5_s8,
+ 0x7706e5: LocationName.iceberg_5_s9,
+ 0x7706e6: LocationName.iceberg_5_s10,
+ 0x7706e7: LocationName.iceberg_5_s11,
+ 0x7706e8: LocationName.iceberg_5_s12,
+ 0x7706e9: LocationName.iceberg_5_s13,
+ 0x7706ea: LocationName.iceberg_5_s14,
+ 0x7706eb: LocationName.iceberg_5_s15,
+ 0x7706ec: LocationName.iceberg_5_s16,
+ 0x7706ed: LocationName.iceberg_5_s17,
+ 0x7706ee: LocationName.iceberg_5_s18,
+ 0x7706ef: LocationName.iceberg_5_s19,
+ 0x7706f0: LocationName.iceberg_5_s20,
+ 0x7706f1: LocationName.iceberg_5_s21,
+ 0x7706f2: LocationName.iceberg_5_s22,
+ 0x7706f3: LocationName.iceberg_5_s23,
+ 0x7706f4: LocationName.iceberg_5_s24,
+ 0x7706f5: LocationName.iceberg_5_s25,
+ 0x7706f6: LocationName.iceberg_5_s26,
+ 0x7706f7: LocationName.iceberg_5_s27,
+ 0x7706f8: LocationName.iceberg_5_s28,
+ 0x7706f9: LocationName.iceberg_5_s29,
+ 0x7706fa: LocationName.iceberg_5_s30,
+ 0x7706fb: LocationName.iceberg_5_s31,
+ 0x7706fc: LocationName.iceberg_5_s32,
+ 0x7706fd: LocationName.iceberg_5_s33,
+ 0x7706fe: LocationName.iceberg_5_s34,
+ 0x7706ff: LocationName.iceberg_6_s1,
+
+}
+
+location_table = {
+ **stage_locations,
+ **heart_star_locations,
+ **boss_locations,
+ **consumable_locations,
+ **star_locations
+}
diff --git a/worlds/kdl3/Names/AnimalFriendSpawns.py b/worlds/kdl3/Names/AnimalFriendSpawns.py
new file mode 100644
index 000000000000..4520cf143803
--- /dev/null
+++ b/worlds/kdl3/Names/AnimalFriendSpawns.py
@@ -0,0 +1,199 @@
+grass_land_1_a1 = "Grass Land 1 - Animal 1" # Nago
+grass_land_1_a2 = "Grass Land 1 - Animal 2" # Rick
+grass_land_2_a1 = "Grass Land 2 - Animal 1" # ChuChu
+grass_land_2_a2 = "Grass Land 2 - Animal 2" # Pitch
+grass_land_3_a1 = "Grass Land 3 - Animal 1" # Kine
+grass_land_3_a2 = "Grass Land 3 - Animal 2" # Coo
+grass_land_4_a1 = "Grass Land 4 - Animal 1" # ChuChu
+grass_land_4_a2 = "Grass Land 4 - Animal 2" # Nago
+grass_land_5_a1 = "Grass Land 5 - Animal 1" # Coo
+grass_land_5_a2 = "Grass Land 5 - Animal 2" # Kine
+grass_land_5_a3 = "Grass Land 5 - Animal 3" # Nago
+grass_land_5_a4 = "Grass Land 5 - Animal 4" # Rick
+grass_land_6_a1 = "Grass Land 6 - Animal 1" # Rick
+grass_land_6_a2 = "Grass Land 6 - Animal 2" # ChuChu
+grass_land_6_a3 = "Grass Land 6 - Animal 3" # Nago
+grass_land_6_a4 = "Grass Land 6 - Animal 4" # Coo
+ripple_field_1_a1 = "Ripple Field 1 - Animal 1" # Pitch
+ripple_field_1_a2 = "Ripple Field 1 - Animal 2" # Nago
+ripple_field_2_a1 = "Ripple Field 2 - Animal 1" # Kine
+ripple_field_2_a2 = "Ripple Field 2 - Animal 2" # ChuChu
+ripple_field_2_a3 = "Ripple Field 2 - Animal 3" # Rick
+ripple_field_2_a4 = "Ripple Field 2 - Animal 4" # Coo
+ripple_field_3_a1 = "Ripple Field 3 - Animal 1" # Kine
+ripple_field_3_a2 = "Ripple Field 3 - Animal 2" # Rick
+ripple_field_4_a1 = "Ripple Field 4 - Animal 1" # ChuChu
+ripple_field_4_a2 = "Ripple Field 4 - Animal 2" # Kine
+ripple_field_4_a3 = "Ripple Field 4 - Animal 3" # Nago
+ripple_field_5_a1 = "Ripple Field 5 - Animal 1" # Kine
+ripple_field_5_a2 = "Ripple Field 5 - Animal 2" # Pitch
+ripple_field_6_a1 = "Ripple Field 6 - Animal 1" # Nago
+ripple_field_6_a2 = "Ripple Field 6 - Animal 2" # Pitch
+ripple_field_6_a3 = "Ripple Field 6 - Animal 3" # Rick
+ripple_field_6_a4 = "Ripple Field 6 - Animal 4" # Coo
+sand_canyon_1_a1 = "Sand Canyon 1 - Animal 1" # Rick
+sand_canyon_1_a2 = "Sand Canyon 1 - Animal 2" # Pitch
+sand_canyon_2_a1 = "Sand Canyon 2 - Animal 1" # ChuChu
+sand_canyon_2_a2 = "Sand Canyon 2 - Animal 2" # Coo
+sand_canyon_3_a1 = "Sand Canyon 3 - Animal 1" # Pitch
+sand_canyon_3_a2 = "Sand Canyon 3 - Animal 2" # Coo
+sand_canyon_3_a3 = "Sand Canyon 3 - Animal 3" # ChuChu
+sand_canyon_4_a1 = "Sand Canyon 4 - Animal 1" # Rick
+sand_canyon_4_a2 = "Sand Canyon 4 - Animal 2" # Pitch
+sand_canyon_4_a3 = "Sand Canyon 4 - Animal 3" # Nago
+sand_canyon_5_a1 = "Sand Canyon 5 - Animal 1" # Rick
+sand_canyon_5_a2 = "Sand Canyon 5 - Animal 2" # ChuChu
+sand_canyon_6_a1 = "Sand Canyon 6 - Animal 1" # Coo
+sand_canyon_6_a2 = "Sand Canyon 6 - Animal 2" # Kine
+sand_canyon_6_a3 = "Sand Canyon 6 - Animal 3" # Rick
+sand_canyon_6_a4 = "Sand Canyon 6 - Animal 4" # ChuChu
+sand_canyon_6_a5 = "Sand Canyon 6 - Animal 5" # Nago
+sand_canyon_6_a6 = "Sand Canyon 6 - Animal 6" # Pitch
+cloudy_park_1_a1 = "Cloudy Park 1 - Animal 1" # Rick
+cloudy_park_1_a2 = "Cloudy Park 1 - Animal 2" # Nago
+cloudy_park_1_a3 = "Cloudy Park 1 - Animal 3" # Coo
+cloudy_park_1_a4 = "Cloudy Park 1 - Animal 4" # Kine
+cloudy_park_1_a5 = "Cloudy Park 1 - Animal 5" # ChuChu
+cloudy_park_1_a6 = "Cloudy Park 1 - Animal 6" # Pitch
+cloudy_park_2_a1 = "Cloudy Park 2 - Animal 1" # Nago
+cloudy_park_2_a2 = "Cloudy Park 2 - Animal 2" # Pitch
+cloudy_park_2_a3 = "Cloudy Park 2 - Animal 3" # ChuChu
+cloudy_park_3_a1 = "Cloudy Park 3 - Animal 1" # Kine
+cloudy_park_3_a2 = "Cloudy Park 3 - Animal 2" # Rick
+cloudy_park_3_a3 = "Cloudy Park 3 - Animal 3" # ChuChu
+cloudy_park_4_a1 = "Cloudy Park 4 - Animal 1" # Coo
+cloudy_park_4_a2 = "Cloudy Park 4 - Animal 2" # ChuChu
+cloudy_park_5_a1 = "Cloudy Park 5 - Animal 1" # Rick
+cloudy_park_5_a2 = "Cloudy Park 5 - Animal 2" # Coo
+cloudy_park_6_a1 = "Cloudy Park 6 - Animal 1" # Nago
+cloudy_park_6_a2 = "Cloudy Park 6 - Animal 2" # Coo
+cloudy_park_6_a3 = "Cloudy Park 6 - Animal 3" # Rick
+iceberg_1_a1 = "Iceberg 1 - Animal 1" # Pitch
+iceberg_1_a2 = "Iceberg 1 - Animal 2" # Rick
+iceberg_2_a1 = "Iceberg 2 - Animal 1" # Nago
+iceberg_2_a2 = "Iceberg 2 - Animal 2" # Pitch
+iceberg_3_a1 = "Iceberg 3 - Animal 1" # Pitch
+iceberg_3_a2 = "Iceberg 3 - Animal 2" # Coo
+iceberg_3_a3 = "Iceberg 3 - Animal 3" # Nago
+iceberg_3_a4 = "Iceberg 3 - Animal 4" # Rick
+iceberg_3_a5 = "Iceberg 3 - Animal 5" # Kine
+iceberg_4_a1 = "Iceberg 4 - Animal 1" # ChuChu
+iceberg_4_a2 = "Iceberg 4 - Animal 2" # Coo
+iceberg_4_a3 = "Iceberg 4 - Animal 3" # Pitch
+iceberg_4_a4 = "Iceberg 4 - Animal 4" # Coo
+iceberg_4_a5 = "Iceberg 4 - Animal 5" # Rick
+iceberg_5_a1 = "Iceberg 5 - Animal 1" # Kine
+iceberg_5_a2 = "Iceberg 5 - Animal 2" # Rick
+iceberg_5_a3 = "Iceberg 5 - Animal 3" # Pitch
+iceberg_5_a4 = "Iceberg 5 - Animal 4" # ChuChu
+iceberg_5_a5 = "Iceberg 5 - Animal 5" # Kine
+iceberg_5_a6 = "Iceberg 5 - Animal 6" # Coo
+iceberg_5_a7 = "Iceberg 5 - Animal 7" # Rick
+iceberg_5_a8 = "Iceberg 5 - Animal 8" # ChuChu
+iceberg_6_a1 = "Iceberg 6 - Animal 1" # Rick
+iceberg_6_a2 = "Iceberg 6 - Animal 2" # Coo
+iceberg_6_a3 = "Iceberg 6 - Animal 3" # Nago
+iceberg_6_a4 = "Iceberg 6 - Animal 4" # Kine
+iceberg_6_a5 = "Iceberg 6 - Animal 5" # ChuChu
+iceberg_6_a6 = "Iceberg 6 - Animal 6" # Nago
+
+animal_friend_spawns = {
+ grass_land_1_a1: "Nago Spawn",
+ grass_land_1_a2: "Rick Spawn",
+ grass_land_2_a1: "ChuChu Spawn",
+ grass_land_2_a2: "Pitch Spawn",
+ grass_land_3_a1: "Kine Spawn",
+ grass_land_3_a2: "Coo Spawn",
+ grass_land_4_a1: "ChuChu Spawn",
+ grass_land_4_a2: "Nago Spawn",
+ grass_land_5_a1: "Coo Spawn",
+ grass_land_5_a2: "Kine Spawn",
+ grass_land_5_a3: "Nago Spawn",
+ grass_land_5_a4: "Rick Spawn",
+ grass_land_6_a1: "Rick Spawn",
+ grass_land_6_a2: "ChuChu Spawn",
+ grass_land_6_a3: "Nago Spawn",
+ grass_land_6_a4: "Coo Spawn",
+ ripple_field_1_a1: "Pitch Spawn",
+ ripple_field_1_a2: "Nago Spawn",
+ ripple_field_2_a1: "Kine Spawn",
+ ripple_field_2_a2: "ChuChu Spawn",
+ ripple_field_2_a3: "Rick Spawn",
+ ripple_field_2_a4: "Coo Spawn",
+ ripple_field_3_a1: "Kine Spawn",
+ ripple_field_3_a2: "Rick Spawn",
+ ripple_field_4_a1: "ChuChu Spawn",
+ ripple_field_4_a2: "Kine Spawn",
+ ripple_field_4_a3: "Nago Spawn",
+ ripple_field_5_a1: "Kine Spawn",
+ ripple_field_5_a2: "Pitch Spawn",
+ ripple_field_6_a1: "Nago Spawn",
+ ripple_field_6_a2: "Pitch Spawn",
+ ripple_field_6_a3: "Rick Spawn",
+ ripple_field_6_a4: "Coo Spawn",
+ sand_canyon_1_a1: "Rick Spawn",
+ sand_canyon_1_a2: "Pitch Spawn",
+ sand_canyon_2_a1: "ChuChu Spawn",
+ sand_canyon_2_a2: "Coo Spawn",
+ sand_canyon_3_a1: "Pitch Spawn",
+ sand_canyon_3_a2: "Coo Spawn",
+ sand_canyon_3_a3: "ChuChu Spawn",
+ sand_canyon_4_a1: "Rick Spawn",
+ sand_canyon_4_a2: "Pitch Spawn",
+ sand_canyon_4_a3: "Nago Spawn",
+ sand_canyon_5_a1: "Rick Spawn",
+ sand_canyon_5_a2: "ChuChu Spawn",
+ sand_canyon_6_a1: "Coo Spawn",
+ sand_canyon_6_a2: "Kine Spawn",
+ sand_canyon_6_a3: "Rick Spawn",
+ sand_canyon_6_a4: "ChuChu Spawn",
+ sand_canyon_6_a5: "Nago Spawn",
+ sand_canyon_6_a6: "Pitch Spawn",
+ cloudy_park_1_a1: "Rick Spawn",
+ cloudy_park_1_a2: "Nago Spawn",
+ cloudy_park_1_a3: "Coo Spawn",
+ cloudy_park_1_a4: "Kine Spawn",
+ cloudy_park_1_a5: "ChuChu Spawn",
+ cloudy_park_1_a6: "Pitch Spawn",
+ cloudy_park_2_a1: "Nago Spawn",
+ cloudy_park_2_a2: "Pitch Spawn",
+ cloudy_park_2_a3: "ChuChu Spawn",
+ cloudy_park_3_a1: "Kine Spawn",
+ cloudy_park_3_a2: "Rick Spawn",
+ cloudy_park_3_a3: "ChuChu Spawn",
+ cloudy_park_4_a1: "Coo Spawn",
+ cloudy_park_4_a2: "ChuChu Spawn",
+ cloudy_park_5_a1: "Rick Spawn",
+ cloudy_park_5_a2: "Coo Spawn",
+ cloudy_park_6_a1: "Nago Spawn",
+ cloudy_park_6_a2: "Coo Spawn",
+ cloudy_park_6_a3: "Rick Spawn",
+ iceberg_1_a1: "Pitch Spawn",
+ iceberg_1_a2: "Rick Spawn",
+ iceberg_2_a1: "Nago Spawn",
+ iceberg_2_a2: "Pitch Spawn",
+ iceberg_3_a1: "Pitch Spawn",
+ iceberg_3_a2: "Coo Spawn",
+ iceberg_3_a3: "Nago Spawn",
+ iceberg_3_a4: "Rick Spawn",
+ iceberg_3_a5: "Kine Spawn",
+ iceberg_4_a1: "ChuChu Spawn",
+ iceberg_4_a2: "Coo Spawn",
+ iceberg_4_a3: "Pitch Spawn",
+ iceberg_4_a4: "Coo Spawn",
+ iceberg_4_a5: "Rick Spawn",
+ iceberg_5_a1: "Kine Spawn",
+ iceberg_5_a2: "Rick Spawn",
+ iceberg_5_a3: "Pitch Spawn",
+ iceberg_5_a4: "ChuChu Spawn",
+ iceberg_5_a5: "Kine Spawn",
+ iceberg_5_a6: "Coo Spawn",
+ iceberg_5_a7: "Rick Spawn",
+ iceberg_5_a8: "ChuChu Spawn",
+ iceberg_6_a1: "Rick Spawn",
+ iceberg_6_a2: "Coo Spawn",
+ iceberg_6_a3: "Nago Spawn",
+ iceberg_6_a4: "Kine Spawn",
+ iceberg_6_a5: "ChuChu Spawn",
+ iceberg_6_a6: "Nago Spawn",
+}
diff --git a/worlds/kdl3/Names/EnemyAbilities.py b/worlds/kdl3/Names/EnemyAbilities.py
new file mode 100644
index 000000000000..016e3033ab25
--- /dev/null
+++ b/worlds/kdl3/Names/EnemyAbilities.py
@@ -0,0 +1,822 @@
+from typing import List, Tuple, Set
+
+Grass_Land_1_E1 = "Grass Land 1 - Enemy 1 (Waddle Dee)"
+Grass_Land_1_E2 = "Grass Land 1 - Enemy 2 (Sir Kibble)"
+Grass_Land_1_E3 = "Grass Land 1 - Enemy 3 (Cappy)"
+Grass_Land_1_E4 = "Grass Land 1 - Enemy 4 (Sparky)"
+Grass_Land_1_E5 = "Grass Land 1 - Enemy 5 (Bronto Burt)"
+Grass_Land_1_E6 = "Grass Land 1 - Enemy 6 (Sasuke)"
+Grass_Land_1_E7 = "Grass Land 1 - Enemy 7 (Poppy Bros Jr.)"
+Grass_Land_2_E1 = "Grass Land 2 - Enemy 1 (Rocky)"
+Grass_Land_2_E2 = "Grass Land 2 - Enemy 2 (KeKe)"
+Grass_Land_2_E3 = "Grass Land 2 - Enemy 3 (Bobo)"
+Grass_Land_2_E4 = "Grass Land 2 - Enemy 4 (Poppy Bros Jr.)"
+Grass_Land_2_E5 = "Grass Land 2 - Enemy 5 (Waddle Dee)"
+Grass_Land_2_E6 = "Grass Land 2 - Enemy 6 (Popon Ball)"
+Grass_Land_2_E7 = "Grass Land 2 - Enemy 7 (Bouncy)"
+Grass_Land_2_E8 = "Grass Land 2 - Enemy 8 (Tick)"
+Grass_Land_2_E9 = "Grass Land 2 - Enemy 9 (Bronto Burt)"
+Grass_Land_2_E10 = "Grass Land 2 - Enemy 10 (Nruff)"
+Grass_Land_3_E1 = "Grass Land 3 - Enemy 1 (Sparky)"
+Grass_Land_3_E2 = "Grass Land 3 - Enemy 2 (Rocky)"
+Grass_Land_3_E3 = "Grass Land 3 - Enemy 3 (Nruff)"
+Grass_Land_3_E4 = "Grass Land 3 - Enemy 4 (Bouncy)"
+Grass_Land_4_E1 = "Grass Land 4 - Enemy 1 (Loud)"
+Grass_Land_4_E2 = "Grass Land 4 - Enemy 2 (Babut)"
+Grass_Land_4_E3 = "Grass Land 4 - Enemy 3 (Rocky)"
+Grass_Land_4_E4 = "Grass Land 4 - Enemy 4 (Kapar)"
+Grass_Land_4_E5 = "Grass Land 4 - Enemy 5 (Glunk)"
+Grass_Land_4_E6 = "Grass Land 4 - Enemy 6 (Oro)"
+Grass_Land_4_E7 = "Grass Land 4 - Enemy 7 (Peran)"
+Grass_Land_5_E1 = "Grass Land 5 - Enemy 1 (Propeller)"
+Grass_Land_5_E2 = "Grass Land 5 - Enemy 2 (Broom Hatter)"
+Grass_Land_5_E3 = "Grass Land 5 - Enemy 3 (Bouncy)"
+Grass_Land_5_E4 = "Grass Land 5 - Enemy 4 (Sir Kibble)"
+Grass_Land_5_E5 = "Grass Land 5 - Enemy 5 (Waddle Dee)"
+Grass_Land_5_E6 = "Grass Land 5 - Enemy 6 (Sasuke)"
+Grass_Land_5_E7 = "Grass Land 5 - Enemy 7 (Nruff)"
+Grass_Land_5_E8 = "Grass Land 5 - Enemy 8 (Tick)"
+Grass_Land_6_E1 = "Grass Land 6 - Enemy 1 (Como)"
+Grass_Land_6_E2 = "Grass Land 6 - Enemy 2 (Togezo)"
+Grass_Land_6_E3 = "Grass Land 6 - Enemy 3 (Bronto Burt)"
+Grass_Land_6_E4 = "Grass Land 6 - Enemy 4 (Cappy)"
+Grass_Land_6_E5 = "Grass Land 6 - Enemy 5 (Bobo)"
+Grass_Land_6_E6 = "Grass Land 6 - Enemy 6 (Mariel)"
+Grass_Land_6_E7 = "Grass Land 6 - Enemy 7 (Yaban)"
+Grass_Land_6_E8 = "Grass Land 6 - Enemy 8 (Broom Hatter)"
+Grass_Land_6_E9 = "Grass Land 6 - Enemy 9 (Apolo)"
+Grass_Land_6_E10 = "Grass Land 6 - Enemy 10 (Sasuke)"
+Grass_Land_6_E11 = "Grass Land 6 - Enemy 11 (Rocky)"
+Ripple_Field_1_E1 = "Ripple Field 1 - Enemy 1 (Waddle Dee)"
+Ripple_Field_1_E2 = "Ripple Field 1 - Enemy 2 (Glunk)"
+Ripple_Field_1_E3 = "Ripple Field 1 - Enemy 3 (Broom Hatter)"
+Ripple_Field_1_E4 = "Ripple Field 1 - Enemy 4 (Cappy)"
+Ripple_Field_1_E5 = "Ripple Field 1 - Enemy 5 (Bronto Burt)"
+Ripple_Field_1_E6 = "Ripple Field 1 - Enemy 6 (Rocky)"
+Ripple_Field_1_E7 = "Ripple Field 1 - Enemy 7 (Poppy Bros Jr.)"
+Ripple_Field_1_E8 = "Ripple Field 1 - Enemy 8 (Bobin)"
+Ripple_Field_2_E1 = "Ripple Field 2 - Enemy 1 (Togezo)"
+Ripple_Field_2_E2 = "Ripple Field 2 - Enemy 2 (Coconut)"
+Ripple_Field_2_E3 = "Ripple Field 2 - Enemy 3 (Blipper)"
+Ripple_Field_2_E4 = "Ripple Field 2 - Enemy 4 (Sasuke)"
+Ripple_Field_2_E5 = "Ripple Field 2 - Enemy 5 (Kany)"
+Ripple_Field_2_E6 = "Ripple Field 2 - Enemy 6 (Glunk)"
+Ripple_Field_3_E1 = "Ripple Field 3 - Enemy 1 (Raft Waddle Dee)"
+Ripple_Field_3_E2 = "Ripple Field 3 - Enemy 2 (Kapar)"
+Ripple_Field_3_E3 = "Ripple Field 3 - Enemy 3 (Blipper)"
+Ripple_Field_3_E4 = "Ripple Field 3 - Enemy 4 (Sparky)"
+Ripple_Field_3_E5 = "Ripple Field 3 - Enemy 5 (Glunk)"
+Ripple_Field_3_E6 = "Ripple Field 3 - Enemy 6 (Joe)"
+Ripple_Field_3_E7 = "Ripple Field 3 - Enemy 7 (Bobo)"
+Ripple_Field_4_E1 = "Ripple Field 4 - Enemy 1 (Bukiset (Stone))"
+Ripple_Field_4_E2 = "Ripple Field 4 - Enemy 2 (Bukiset (Needle))"
+Ripple_Field_4_E3 = "Ripple Field 4 - Enemy 3 (Bukiset (Clean))"
+Ripple_Field_4_E4 = "Ripple Field 4 - Enemy 4 (Bukiset (Parasol))"
+Ripple_Field_4_E5 = "Ripple Field 4 - Enemy 5 (Mony)"
+Ripple_Field_4_E6 = "Ripple Field 4 - Enemy 6 (Bukiset (Burning))"
+Ripple_Field_4_E7 = "Ripple Field 4 - Enemy 7 (Bobin)"
+Ripple_Field_4_E8 = "Ripple Field 4 - Enemy 8 (Blipper)"
+Ripple_Field_4_E9 = "Ripple Field 4 - Enemy 9 (Como)"
+Ripple_Field_4_E10 = "Ripple Field 4 - Enemy 10 (Oro)"
+Ripple_Field_4_E11 = "Ripple Field 4 - Enemy 11 (Gansan)"
+Ripple_Field_4_E12 = "Ripple Field 4 - Enemy 12 (Waddle Dee)"
+Ripple_Field_4_E13 = "Ripple Field 4 - Enemy 13 (Kapar)"
+Ripple_Field_4_E14 = "Ripple Field 4 - Enemy 14 (Squishy)"
+Ripple_Field_4_E15 = "Ripple Field 4 - Enemy 15 (Nidoo)"
+Ripple_Field_5_E1 = "Ripple Field 5 - Enemy 1 (Glunk)"
+Ripple_Field_5_E2 = "Ripple Field 5 - Enemy 2 (Joe)"
+Ripple_Field_5_E3 = "Ripple Field 5 - Enemy 3 (Bobin)"
+Ripple_Field_5_E4 = "Ripple Field 5 - Enemy 4 (Mony)"
+Ripple_Field_5_E5 = "Ripple Field 5 - Enemy 5 (Squishy)"
+Ripple_Field_5_E6 = "Ripple Field 5 - Enemy 6 (Yaban)"
+Ripple_Field_5_E7 = "Ripple Field 5 - Enemy 7 (Broom Hatter)"
+Ripple_Field_5_E8 = "Ripple Field 5 - Enemy 8 (Bouncy)"
+Ripple_Field_5_E9 = "Ripple Field 5 - Enemy 9 (Sparky)"
+Ripple_Field_5_E10 = "Ripple Field 5 - Enemy 10 (Rocky)"
+Ripple_Field_5_E11 = "Ripple Field 5 - Enemy 11 (Babut)"
+Ripple_Field_5_E12 = "Ripple Field 5 - Enemy 12 (Galbo)"
+Ripple_Field_6_E1 = "Ripple Field 6 - Enemy 1 (Kany)"
+Ripple_Field_6_E2 = "Ripple Field 6 - Enemy 2 (KeKe)"
+Ripple_Field_6_E3 = "Ripple Field 6 - Enemy 3 (Kapar)"
+Ripple_Field_6_E4 = "Ripple Field 6 - Enemy 4 (Rocky)"
+Ripple_Field_6_E5 = "Ripple Field 6 - Enemy 5 (Poppy Bros Jr.)"
+Ripple_Field_6_E6 = "Ripple Field 6 - Enemy 6 (Propeller)"
+Ripple_Field_6_E7 = "Ripple Field 6 - Enemy 7 (Coconut)"
+Ripple_Field_6_E8 = "Ripple Field 6 - Enemy 8 (Sasuke)"
+Ripple_Field_6_E9 = "Ripple Field 6 - Enemy 9 (Nruff)"
+Sand_Canyon_1_E1 = "Sand Canyon 1 - Enemy 1 (Bronto Burt)"
+Sand_Canyon_1_E2 = "Sand Canyon 1 - Enemy 2 (Galbo)"
+Sand_Canyon_1_E3 = "Sand Canyon 1 - Enemy 3 (Oro)"
+Sand_Canyon_1_E4 = "Sand Canyon 1 - Enemy 4 (Sparky)"
+Sand_Canyon_1_E5 = "Sand Canyon 1 - Enemy 5 (Propeller)"
+Sand_Canyon_1_E6 = "Sand Canyon 1 - Enemy 6 (Gansan)"
+Sand_Canyon_1_E7 = "Sand Canyon 1 - Enemy 7 (Babut)"
+Sand_Canyon_1_E8 = "Sand Canyon 1 - Enemy 8 (Loud)"
+Sand_Canyon_1_E9 = "Sand Canyon 1 - Enemy 9 (Dogon)"
+Sand_Canyon_1_E10 = "Sand Canyon 1 - Enemy 10 (Bouncy)"
+Sand_Canyon_1_E11 = "Sand Canyon 1 - Enemy 11 (Pteran)"
+Sand_Canyon_1_E12 = "Sand Canyon 1 - Enemy 12 (Polof)"
+Sand_Canyon_2_E1 = "Sand Canyon 2 - Enemy 1 (KeKe)"
+Sand_Canyon_2_E2 = "Sand Canyon 2 - Enemy 2 (Doka)"
+Sand_Canyon_2_E3 = "Sand Canyon 2 - Enemy 3 (Boten)"
+Sand_Canyon_2_E4 = "Sand Canyon 2 - Enemy 4 (Propeller)"
+Sand_Canyon_2_E5 = "Sand Canyon 2 - Enemy 5 (Waddle Dee)"
+Sand_Canyon_2_E6 = "Sand Canyon 2 - Enemy 6 (Sparky)"
+Sand_Canyon_2_E7 = "Sand Canyon 2 - Enemy 7 (Sasuke)"
+Sand_Canyon_2_E8 = "Sand Canyon 2 - Enemy 8 (Como)"
+Sand_Canyon_2_E9 = "Sand Canyon 2 - Enemy 9 (Bukiset (Ice))"
+Sand_Canyon_2_E10 = "Sand Canyon 2 - Enemy 10 (Bukiset (Needle))"
+Sand_Canyon_2_E11 = "Sand Canyon 2 - Enemy 11 (Bukiset (Clean))"
+Sand_Canyon_2_E12 = "Sand Canyon 2 - Enemy 12 (Bukiset (Parasol))"
+Sand_Canyon_2_E13 = "Sand Canyon 2 - Enemy 13 (Bukiset (Spark))"
+Sand_Canyon_2_E14 = "Sand Canyon 2 - Enemy 14 (Bukiset (Cutter))"
+Sand_Canyon_2_E15 = "Sand Canyon 2 - Enemy 15 (Nidoo)"
+Sand_Canyon_2_E16 = "Sand Canyon 2 - Enemy 16 (Mariel)"
+Sand_Canyon_2_E17 = "Sand Canyon 2 - Enemy 17 (Yaban)"
+Sand_Canyon_2_E18 = "Sand Canyon 2 - Enemy 18 (Wapod)"
+Sand_Canyon_2_E19 = "Sand Canyon 2 - Enemy 19 (Squishy)"
+Sand_Canyon_2_E20 = "Sand Canyon 2 - Enemy 20 (Pteran)"
+Sand_Canyon_3_E1 = "Sand Canyon 3 - Enemy 1 (Sir Kibble)"
+Sand_Canyon_3_E2 = "Sand Canyon 3 - Enemy 2 (Broom Hatter)"
+Sand_Canyon_3_E3 = "Sand Canyon 3 - Enemy 3 (Rocky)"
+Sand_Canyon_3_E4 = "Sand Canyon 3 - Enemy 4 (Gabon)"
+Sand_Canyon_3_E5 = "Sand Canyon 3 - Enemy 5 (Kany)"
+Sand_Canyon_3_E6 = "Sand Canyon 3 - Enemy 6 (Galbo)"
+Sand_Canyon_3_E7 = "Sand Canyon 3 - Enemy 7 (Propeller)"
+Sand_Canyon_3_E8 = "Sand Canyon 3 - Enemy 8 (Sasuke)"
+Sand_Canyon_3_E9 = "Sand Canyon 3 - Enemy 9 (Wapod)"
+Sand_Canyon_3_E10 = "Sand Canyon 3 - Enemy 10 (Bobo)"
+Sand_Canyon_3_E11 = "Sand Canyon 3 - Enemy 11 (Babut)"
+Sand_Canyon_3_E12 = "Sand Canyon 3 - Enemy 12 (Magoo)"
+Sand_Canyon_4_E1 = "Sand Canyon 4 - Enemy 1 (Popon Ball)"
+Sand_Canyon_4_E2 = "Sand Canyon 4 - Enemy 2 (Mariel)"
+Sand_Canyon_4_E3 = "Sand Canyon 4 - Enemy 3 (Chilly)"
+Sand_Canyon_4_E4 = "Sand Canyon 4 - Enemy 4 (Tick)"
+Sand_Canyon_4_E5 = "Sand Canyon 4 - Enemy 5 (Bronto Burt)"
+Sand_Canyon_4_E6 = "Sand Canyon 4 - Enemy 6 (Babut)"
+Sand_Canyon_4_E7 = "Sand Canyon 4 - Enemy 7 (Bobin)"
+Sand_Canyon_4_E8 = "Sand Canyon 4 - Enemy 8 (Joe)"
+Sand_Canyon_4_E9 = "Sand Canyon 4 - Enemy 9 (Mony)"
+Sand_Canyon_4_E10 = "Sand Canyon 4 - Enemy 10 (Blipper)"
+Sand_Canyon_4_E11 = "Sand Canyon 4 - Enemy 11 (Togezo)"
+Sand_Canyon_4_E12 = "Sand Canyon 4 - Enemy 12 (Rocky)"
+Sand_Canyon_4_E13 = "Sand Canyon 4 - Enemy 13 (Bobo)"
+Sand_Canyon_5_E1 = "Sand Canyon 5 - Enemy 1 (Wapod)"
+Sand_Canyon_5_E2 = "Sand Canyon 5 - Enemy 2 (Dogon)"
+Sand_Canyon_5_E3 = "Sand Canyon 5 - Enemy 3 (Tick)"
+Sand_Canyon_5_E4 = "Sand Canyon 5 - Enemy 4 (Rocky)"
+Sand_Canyon_5_E5 = "Sand Canyon 5 - Enemy 5 (Bobo)"
+Sand_Canyon_5_E6 = "Sand Canyon 5 - Enemy 6 (Chilly)"
+Sand_Canyon_5_E7 = "Sand Canyon 5 - Enemy 7 (Sparky)"
+Sand_Canyon_5_E8 = "Sand Canyon 5 - Enemy 8 (Togezo)"
+Sand_Canyon_5_E9 = "Sand Canyon 5 - Enemy 9 (Bronto Burt)"
+Sand_Canyon_5_E10 = "Sand Canyon 5 - Enemy 10 (Sasuke)"
+Sand_Canyon_5_E11 = "Sand Canyon 5 - Enemy 11 (Oro)"
+Sand_Canyon_5_E12 = "Sand Canyon 5 - Enemy 12 (Galbo)"
+Sand_Canyon_5_E13 = "Sand Canyon 5 - Enemy 13 (Nidoo)"
+Sand_Canyon_5_E14 = "Sand Canyon 5 - Enemy 14 (Propeller)"
+Sand_Canyon_5_E15 = "Sand Canyon 5 - Enemy 15 (Sir Kibble)"
+Sand_Canyon_5_E16 = "Sand Canyon 5 - Enemy 16 (KeKe)"
+Sand_Canyon_5_E17 = "Sand Canyon 5 - Enemy 17 (Kabu)"
+Sand_Canyon_6_E1 = "Sand Canyon 6 - Enemy 1 (Sparky)"
+Sand_Canyon_6_E2 = "Sand Canyon 6 - Enemy 2 (Doka)"
+Sand_Canyon_6_E3 = "Sand Canyon 6 - Enemy 3 (Cappy)"
+Sand_Canyon_6_E4 = "Sand Canyon 6 - Enemy 4 (Pteran)"
+Sand_Canyon_6_E5 = "Sand Canyon 6 - Enemy 5 (Bukiset (Parasol))"
+Sand_Canyon_6_E6 = "Sand Canyon 6 - Enemy 6 (Bukiset (Cutter))"
+Sand_Canyon_6_E7 = "Sand Canyon 6 - Enemy 7 (Bukiset (Clean))"
+Sand_Canyon_6_E8 = "Sand Canyon 6 - Enemy 8 (Bukiset (Spark))"
+Sand_Canyon_6_E9 = "Sand Canyon 6 - Enemy 9 (Bukiset (Ice))"
+Sand_Canyon_6_E10 = "Sand Canyon 6 - Enemy 10 (Bukiset (Needle))"
+Sand_Canyon_6_E11 = "Sand Canyon 6 - Enemy 11 (Bukiset (Burning))"
+Sand_Canyon_6_E12 = "Sand Canyon 6 - Enemy 12 (Bukiset (Stone))"
+Sand_Canyon_6_E13 = "Sand Canyon 6 - Enemy 13 (Nidoo)"
+Cloudy_Park_1_E1 = "Cloudy Park 1 - Enemy 1 (Waddle Dee)"
+Cloudy_Park_1_E2 = "Cloudy Park 1 - Enemy 2 (KeKe)"
+Cloudy_Park_1_E3 = "Cloudy Park 1 - Enemy 3 (Cappy)"
+Cloudy_Park_1_E4 = "Cloudy Park 1 - Enemy 4 (Yaban)"
+Cloudy_Park_1_E5 = "Cloudy Park 1 - Enemy 5 (Togezo)"
+Cloudy_Park_1_E6 = "Cloudy Park 1 - Enemy 6 (Galbo)"
+Cloudy_Park_1_E7 = "Cloudy Park 1 - Enemy 7 (Sparky)"
+Cloudy_Park_1_E8 = "Cloudy Park 1 - Enemy 8 (Como)"
+Cloudy_Park_1_E9 = "Cloudy Park 1 - Enemy 9 (Bronto Burt)"
+Cloudy_Park_1_E10 = "Cloudy Park 1 - Enemy 10 (Gabon)"
+Cloudy_Park_1_E11 = "Cloudy Park 1 - Enemy 11 (Sir Kibble)"
+Cloudy_Park_1_E12 = "Cloudy Park 1 - Enemy 12 (Mariel)"
+Cloudy_Park_1_E13 = "Cloudy Park 1 - Enemy 13 (Nruff)"
+Cloudy_Park_2_E1 = "Cloudy Park 2 - Enemy 1 (Chilly)"
+Cloudy_Park_2_E2 = "Cloudy Park 2 - Enemy 2 (Sasuke)"
+Cloudy_Park_2_E3 = "Cloudy Park 2 - Enemy 3 (Waddle Dee)"
+Cloudy_Park_2_E4 = "Cloudy Park 2 - Enemy 4 (Sparky)"
+Cloudy_Park_2_E5 = "Cloudy Park 2 - Enemy 5 (Broom Hatter)"
+Cloudy_Park_2_E6 = "Cloudy Park 2 - Enemy 6 (Sir Kibble)"
+Cloudy_Park_2_E7 = "Cloudy Park 2 - Enemy 7 (Pteran)"
+Cloudy_Park_2_E8 = "Cloudy Park 2 - Enemy 8 (Propeller)"
+Cloudy_Park_2_E9 = "Cloudy Park 2 - Enemy 9 (Dogon)"
+Cloudy_Park_2_E10 = "Cloudy Park 2 - Enemy 10 (Togezo)"
+Cloudy_Park_2_E11 = "Cloudy Park 2 - Enemy 11 (Oro)"
+Cloudy_Park_2_E12 = "Cloudy Park 2 - Enemy 12 (Bronto Burt)"
+Cloudy_Park_2_E13 = "Cloudy Park 2 - Enemy 13 (Rocky)"
+Cloudy_Park_2_E14 = "Cloudy Park 2 - Enemy 14 (Galbo)"
+Cloudy_Park_2_E15 = "Cloudy Park 2 - Enemy 15 (Kapar)"
+Cloudy_Park_3_E1 = "Cloudy Park 3 - Enemy 1 (Bronto Burt)"
+Cloudy_Park_3_E2 = "Cloudy Park 3 - Enemy 2 (Mopoo)"
+Cloudy_Park_3_E3 = "Cloudy Park 3 - Enemy 3 (Poppy Bros Jr.)"
+Cloudy_Park_3_E4 = "Cloudy Park 3 - Enemy 4 (Como)"
+Cloudy_Park_3_E5 = "Cloudy Park 3 - Enemy 5 (Glunk)"
+Cloudy_Park_3_E6 = "Cloudy Park 3 - Enemy 6 (Bobin)"
+Cloudy_Park_3_E7 = "Cloudy Park 3 - Enemy 7 (Loud)"
+Cloudy_Park_3_E8 = "Cloudy Park 3 - Enemy 8 (Kapar)"
+Cloudy_Park_3_E9 = "Cloudy Park 3 - Enemy 9 (Galbo)"
+Cloudy_Park_3_E10 = "Cloudy Park 3 - Enemy 10 (Batamon)"
+Cloudy_Park_3_E11 = "Cloudy Park 3 - Enemy 11 (Bouncy)"
+Cloudy_Park_4_E1 = "Cloudy Park 4 - Enemy 1 (Gabon)"
+Cloudy_Park_4_E2 = "Cloudy Park 4 - Enemy 2 (Como)"
+Cloudy_Park_4_E3 = "Cloudy Park 4 - Enemy 3 (Wapod)"
+Cloudy_Park_4_E4 = "Cloudy Park 4 - Enemy 4 (Cappy)"
+Cloudy_Park_4_E5 = "Cloudy Park 4 - Enemy 5 (Sparky)"
+Cloudy_Park_4_E6 = "Cloudy Park 4 - Enemy 6 (Togezo)"
+Cloudy_Park_4_E7 = "Cloudy Park 4 - Enemy 7 (Bronto Burt)"
+Cloudy_Park_4_E8 = "Cloudy Park 4 - Enemy 8 (KeKe)"
+Cloudy_Park_4_E9 = "Cloudy Park 4 - Enemy 9 (Bouncy)"
+Cloudy_Park_4_E10 = "Cloudy Park 4 - Enemy 10 (Sir Kibble)"
+Cloudy_Park_4_E11 = "Cloudy Park 4 - Enemy 11 (Mariel)"
+Cloudy_Park_4_E12 = "Cloudy Park 4 - Enemy 12 (Kabu)"
+Cloudy_Park_4_E13 = "Cloudy Park 4 - Enemy 13 (Wappa)"
+Cloudy_Park_5_E1 = "Cloudy Park 5 - Enemy 1 (Yaban)"
+Cloudy_Park_5_E2 = "Cloudy Park 5 - Enemy 2 (Sir Kibble)"
+Cloudy_Park_5_E3 = "Cloudy Park 5 - Enemy 3 (Cappy)"
+Cloudy_Park_5_E4 = "Cloudy Park 5 - Enemy 4 (Wappa)"
+Cloudy_Park_5_E5 = "Cloudy Park 5 - Enemy 5 (Galbo)"
+Cloudy_Park_5_E6 = "Cloudy Park 5 - Enemy 6 (Bronto Burt)"
+Cloudy_Park_5_E7 = "Cloudy Park 5 - Enemy 7 (KeKe)"
+Cloudy_Park_5_E8 = "Cloudy Park 5 - Enemy 8 (Propeller)"
+Cloudy_Park_5_E9 = "Cloudy Park 5 - Enemy 9 (Klinko)"
+Cloudy_Park_5_E10 = "Cloudy Park 5 - Enemy 10 (Wapod)"
+Cloudy_Park_5_E11 = "Cloudy Park 5 - Enemy 11 (Pteran)"
+Cloudy_Park_6_E1 = "Cloudy Park 6 - Enemy 1 (Madoo)"
+Cloudy_Park_6_E2 = "Cloudy Park 6 - Enemy 2 (Tick)"
+Cloudy_Park_6_E3 = "Cloudy Park 6 - Enemy 3 (Como)"
+Cloudy_Park_6_E4 = "Cloudy Park 6 - Enemy 4 (Waddle Dee Drawing)"
+Cloudy_Park_6_E5 = "Cloudy Park 6 - Enemy 5 (Bronto Burt Drawing)"
+Cloudy_Park_6_E6 = "Cloudy Park 6 - Enemy 6 (Bouncy Drawing)"
+Cloudy_Park_6_E7 = "Cloudy Park 6 - Enemy 7 (Propeller)"
+Cloudy_Park_6_E8 = "Cloudy Park 6 - Enemy 8 (Mopoo)"
+Cloudy_Park_6_E9 = "Cloudy Park 6 - Enemy 9 (Bukiset (Burning))"
+Cloudy_Park_6_E10 = "Cloudy Park 6 - Enemy 10 (Bukiset (Ice))"
+Cloudy_Park_6_E11 = "Cloudy Park 6 - Enemy 11 (Bukiset (Needle))"
+Cloudy_Park_6_E12 = "Cloudy Park 6 - Enemy 12 (Bukiset (Clean))"
+Cloudy_Park_6_E13 = "Cloudy Park 6 - Enemy 13 (Bukiset (Cutter))"
+Iceberg_1_E1 = "Iceberg 1 - Enemy 1 (Waddle Dee)"
+Iceberg_1_E2 = "Iceberg 1 - Enemy 2 (Klinko)"
+Iceberg_1_E3 = "Iceberg 1 - Enemy 3 (KeKe)"
+Iceberg_1_E4 = "Iceberg 1 - Enemy 4 (Como)"
+Iceberg_1_E5 = "Iceberg 1 - Enemy 5 (Galbo)"
+Iceberg_1_E6 = "Iceberg 1 - Enemy 6 (Rocky)"
+Iceberg_1_E7 = "Iceberg 1 - Enemy 7 (Kapar)"
+Iceberg_1_E8 = "Iceberg 1 - Enemy 8 (Mopoo)"
+Iceberg_1_E9 = "Iceberg 1 - Enemy 9 (Babut)"
+Iceberg_1_E10 = "Iceberg 1 - Enemy 10 (Wappa)"
+Iceberg_1_E11 = "Iceberg 1 - Enemy 11 (Bronto Burt)"
+Iceberg_1_E12 = "Iceberg 1 - Enemy 12 (Chilly)"
+Iceberg_1_E13 = "Iceberg 1 - Enemy 13 (Poppy Bros Jr.)"
+Iceberg_2_E1 = "Iceberg 2 - Enemy 1 (Gabon)"
+Iceberg_2_E2 = "Iceberg 2 - Enemy 2 (Nruff)"
+Iceberg_2_E3 = "Iceberg 2 - Enemy 3 (Waddle Dee)"
+Iceberg_2_E4 = "Iceberg 2 - Enemy 4 (Chilly)"
+Iceberg_2_E5 = "Iceberg 2 - Enemy 5 (Pteran)"
+Iceberg_2_E6 = "Iceberg 2 - Enemy 6 (Glunk)"
+Iceberg_2_E7 = "Iceberg 2 - Enemy 7 (Galbo)"
+Iceberg_2_E8 = "Iceberg 2 - Enemy 8 (Babut)"
+Iceberg_2_E9 = "Iceberg 2 - Enemy 9 (Magoo)"
+Iceberg_2_E10 = "Iceberg 2 - Enemy 10 (Propeller)"
+Iceberg_2_E11 = "Iceberg 2 - Enemy 11 (Nidoo)"
+Iceberg_2_E12 = "Iceberg 2 - Enemy 12 (Oro)"
+Iceberg_2_E13 = "Iceberg 2 - Enemy 13 (Klinko)"
+Iceberg_2_E14 = "Iceberg 2 - Enemy 14 (Bronto Burt)"
+Iceberg_3_E1 = "Iceberg 3 - Enemy 1 (Corori)"
+Iceberg_3_E2 = "Iceberg 3 - Enemy 2 (Bouncy)"
+Iceberg_3_E3 = "Iceberg 3 - Enemy 3 (Chilly)"
+Iceberg_3_E4 = "Iceberg 3 - Enemy 4 (Pteran)"
+Iceberg_3_E5 = "Iceberg 3 - Enemy 5 (Raft Waddle Dee)"
+Iceberg_3_E6 = "Iceberg 3 - Enemy 6 (Kapar)"
+Iceberg_3_E7 = "Iceberg 3 - Enemy 7 (Blipper)"
+Iceberg_3_E8 = "Iceberg 3 - Enemy 8 (Wapod)"
+Iceberg_3_E9 = "Iceberg 3 - Enemy 9 (Glunk)"
+Iceberg_3_E10 = "Iceberg 3 - Enemy 10 (Icicle)"
+Iceberg_4_E1 = "Iceberg 4 - Enemy 1 (Bronto Burt)"
+Iceberg_4_E2 = "Iceberg 4 - Enemy 2 (Galbo)"
+Iceberg_4_E3 = "Iceberg 4 - Enemy 3 (Klinko)"
+Iceberg_4_E4 = "Iceberg 4 - Enemy 4 (Chilly)"
+Iceberg_4_E5 = "Iceberg 4 - Enemy 5 (Babut)"
+Iceberg_4_E6 = "Iceberg 4 - Enemy 6 (Wappa)"
+Iceberg_4_E7 = "Iceberg 4 - Enemy 7 (Icicle)"
+Iceberg_4_E8 = "Iceberg 4 - Enemy 8 (Corori)"
+Iceberg_4_E9 = "Iceberg 4 - Enemy 9 (Gabon)"
+Iceberg_4_E10 = "Iceberg 4 - Enemy 10 (Kabu)"
+Iceberg_4_E11 = "Iceberg 4 - Enemy 11 (Broom Hatter)"
+Iceberg_4_E12 = "Iceberg 4 - Enemy 12 (Sasuke)"
+Iceberg_4_E13 = "Iceberg 4 - Enemy 13 (Nruff)"
+Iceberg_5_E1 = "Iceberg 5 - Enemy 1 (Bukiset (Burning))"
+Iceberg_5_E2 = "Iceberg 5 - Enemy 2 (Bukiset (Stone))"
+Iceberg_5_E3 = "Iceberg 5 - Enemy 3 (Bukiset (Ice))"
+Iceberg_5_E4 = "Iceberg 5 - Enemy 4 (Bukiset (Needle))"
+Iceberg_5_E5 = "Iceberg 5 - Enemy 5 (Bukiset (Clean))"
+Iceberg_5_E6 = "Iceberg 5 - Enemy 6 (Bukiset (Parasol))"
+Iceberg_5_E7 = "Iceberg 5 - Enemy 7 (Bukiset (Spark))"
+Iceberg_5_E8 = "Iceberg 5 - Enemy 8 (Bukiset (Cutter))"
+Iceberg_5_E9 = "Iceberg 5 - Enemy 9 (Glunk)"
+Iceberg_5_E10 = "Iceberg 5 - Enemy 10 (Wapod)"
+Iceberg_5_E11 = "Iceberg 5 - Enemy 11 (Tick)"
+Iceberg_5_E12 = "Iceberg 5 - Enemy 12 (Madoo)"
+Iceberg_5_E13 = "Iceberg 5 - Enemy 13 (Yaban)"
+Iceberg_5_E14 = "Iceberg 5 - Enemy 14 (Propeller)"
+Iceberg_5_E15 = "Iceberg 5 - Enemy 15 (Mariel)"
+Iceberg_5_E16 = "Iceberg 5 - Enemy 16 (Pteran)"
+Iceberg_5_E17 = "Iceberg 5 - Enemy 17 (Galbo)"
+Iceberg_5_E18 = "Iceberg 5 - Enemy 18 (KeKe)"
+Iceberg_5_E19 = "Iceberg 5 - Enemy 19 (Nidoo)"
+Iceberg_5_E20 = "Iceberg 5 - Enemy 20 (Waddle Dee Drawing)"
+Iceberg_5_E21 = "Iceberg 5 - Enemy 21 (Bronto Burt Drawing)"
+Iceberg_5_E22 = "Iceberg 5 - Enemy 22 (Bouncy Drawing)"
+Iceberg_5_E23 = "Iceberg 5 - Enemy 23 (Joe)"
+Iceberg_5_E24 = "Iceberg 5 - Enemy 24 (Kapar)"
+Iceberg_5_E25 = "Iceberg 5 - Enemy 25 (Gansan)"
+Iceberg_5_E26 = "Iceberg 5 - Enemy 26 (Sasuke)"
+Iceberg_5_E27 = "Iceberg 5 - Enemy 27 (Togezo)"
+Iceberg_5_E28 = "Iceberg 5 - Enemy 28 (Sparky)"
+Iceberg_5_E29 = "Iceberg 5 - Enemy 29 (Bobin)"
+Iceberg_5_E30 = "Iceberg 5 - Enemy 30 (Chilly)"
+Iceberg_5_E31 = "Iceberg 5 - Enemy 31 (Peran)"
+Iceberg_6_E1 = "Iceberg 6 - Enemy 1 (Nruff)"
+Iceberg_6_E2 = "Iceberg 6 - Enemy 2 (Nidoo)"
+Iceberg_6_E3 = "Iceberg 6 - Enemy 3 (Sparky)"
+Iceberg_6_E4 = "Iceberg 6 - Enemy 4 (Sir Kibble)"
+Grass_Land_4_M1 = "Grass Land 4 - Miniboss 1 (Boboo)"
+Ripple_Field_4_M1 = "Ripple Field 4 - Miniboss 1 (Captain Stitch)"
+Sand_Canyon_4_M1 = "Sand Canyon 4 - Miniboss 1 (Haboki)"
+Cloudy_Park_4_M1 = "Cloudy Park 4 - Miniboss 1 (Jumper Shoot)"
+Iceberg_4_M1 = "Iceberg 4 - Miniboss 1 (Yuki)"
+Iceberg_6_M1 = "Iceberg 6 - Miniboss 1 (Blocky)"
+Iceberg_6_M2 = "Iceberg 6 - Miniboss 2 (Jumper Shoot)"
+Iceberg_6_M3 = "Iceberg 6 - Miniboss 3 (Yuki)"
+Iceberg_6_M4 = "Iceberg 6 - Miniboss 4 (Haboki)"
+Iceberg_6_M5 = "Iceberg 6 - Miniboss 5 (Boboo)"
+Iceberg_6_M6 = "Iceberg 6 - Miniboss 6 (Captain Stitch)"
+
+
+enemy_mapping = {
+ Grass_Land_1_E1: "Waddle Dee",
+ Grass_Land_1_E2: "Sir Kibble",
+ Grass_Land_1_E3: "Cappy",
+ Grass_Land_1_E4: "Sparky",
+ Grass_Land_1_E5: "Bronto Burt",
+ Grass_Land_1_E6: "Sasuke",
+ Grass_Land_1_E7: "Poppy Bros Jr.",
+ Grass_Land_2_E1: "Rocky",
+ Grass_Land_2_E2: "KeKe",
+ Grass_Land_2_E3: "Bobo",
+ Grass_Land_2_E4: "Poppy Bros Jr.",
+ Grass_Land_2_E5: "Waddle Dee",
+ Grass_Land_2_E6: "Popon Ball",
+ Grass_Land_2_E7: "Bouncy",
+ Grass_Land_2_E8: "Tick",
+ Grass_Land_2_E9: "Bronto Burt",
+ Grass_Land_2_E10: "Nruff",
+ Grass_Land_3_E1: "Sparky",
+ Grass_Land_3_E2: "Rocky",
+ Grass_Land_3_E3: "Nruff",
+ Grass_Land_3_E4: "Bouncy",
+ Grass_Land_4_E1: "Loud",
+ Grass_Land_4_E2: "Babut",
+ Grass_Land_4_E3: "Rocky",
+ Grass_Land_4_E4: "Kapar",
+ Grass_Land_4_E5: "Glunk",
+ Grass_Land_4_E6: "Oro",
+ Grass_Land_4_E7: "Peran",
+ Grass_Land_5_E1: "Propeller",
+ Grass_Land_5_E2: "Broom Hatter",
+ Grass_Land_5_E3: "Bouncy",
+ Grass_Land_5_E4: "Sir Kibble",
+ Grass_Land_5_E5: "Waddle Dee",
+ Grass_Land_5_E6: "Sasuke",
+ Grass_Land_5_E7: "Nruff",
+ Grass_Land_5_E8: "Tick",
+ Grass_Land_6_E1: "Como",
+ Grass_Land_6_E2: "Togezo",
+ Grass_Land_6_E3: "Bronto Burt",
+ Grass_Land_6_E4: "Cappy",
+ Grass_Land_6_E5: "Bobo",
+ Grass_Land_6_E6: "Mariel",
+ Grass_Land_6_E7: "Yaban",
+ Grass_Land_6_E8: "Broom Hatter",
+ Grass_Land_6_E9: "Apolo",
+ Grass_Land_6_E10: "Sasuke",
+ Grass_Land_6_E11: "Rocky",
+ Ripple_Field_1_E1: "Waddle Dee",
+ Ripple_Field_1_E2: "Glunk",
+ Ripple_Field_1_E3: "Broom Hatter",
+ Ripple_Field_1_E4: "Cappy",
+ Ripple_Field_1_E5: "Bronto Burt",
+ Ripple_Field_1_E6: "Rocky",
+ Ripple_Field_1_E7: "Poppy Bros Jr.",
+ Ripple_Field_1_E8: "Bobin",
+ Ripple_Field_2_E1: "Togezo",
+ Ripple_Field_2_E2: "Coconut",
+ Ripple_Field_2_E3: "Blipper",
+ Ripple_Field_2_E4: "Sasuke",
+ Ripple_Field_2_E5: "Kany",
+ Ripple_Field_2_E6: "Glunk",
+ Ripple_Field_3_E1: "Raft Waddle Dee",
+ Ripple_Field_3_E2: "Kapar",
+ Ripple_Field_3_E3: "Blipper",
+ Ripple_Field_3_E4: "Sparky",
+ Ripple_Field_3_E5: "Glunk",
+ Ripple_Field_3_E6: "Joe",
+ Ripple_Field_3_E7: "Bobo",
+ Ripple_Field_4_E1: "Bukiset (Stone)",
+ Ripple_Field_4_E2: "Bukiset (Needle)",
+ Ripple_Field_4_E3: "Bukiset (Clean)",
+ Ripple_Field_4_E4: "Bukiset (Parasol)",
+ Ripple_Field_4_E5: "Mony",
+ Ripple_Field_4_E6: "Bukiset (Burning)",
+ Ripple_Field_4_E7: "Bobin",
+ Ripple_Field_4_E8: "Blipper",
+ Ripple_Field_4_E9: "Como",
+ Ripple_Field_4_E10: "Oro",
+ Ripple_Field_4_E11: "Gansan",
+ Ripple_Field_4_E12: "Waddle Dee",
+ Ripple_Field_4_E13: "Kapar",
+ Ripple_Field_4_E14: "Squishy",
+ Ripple_Field_4_E15: "Nidoo",
+ Ripple_Field_5_E1: "Glunk",
+ Ripple_Field_5_E2: "Joe",
+ Ripple_Field_5_E3: "Bobin",
+ Ripple_Field_5_E4: "Mony",
+ Ripple_Field_5_E5: "Squishy",
+ Ripple_Field_5_E6: "Yaban",
+ Ripple_Field_5_E7: "Broom Hatter",
+ Ripple_Field_5_E8: "Bouncy",
+ Ripple_Field_5_E9: "Sparky",
+ Ripple_Field_5_E10: "Rocky",
+ Ripple_Field_5_E11: "Babut",
+ Ripple_Field_5_E12: "Galbo",
+ Ripple_Field_6_E1: "Kany",
+ Ripple_Field_6_E2: "KeKe",
+ Ripple_Field_6_E3: "Kapar",
+ Ripple_Field_6_E4: "Rocky",
+ Ripple_Field_6_E5: "Poppy Bros Jr.",
+ Ripple_Field_6_E6: "Propeller",
+ Ripple_Field_6_E7: "Coconut",
+ Ripple_Field_6_E8: "Sasuke",
+ Ripple_Field_6_E9: "Nruff",
+ Sand_Canyon_1_E1: "Bronto Burt",
+ Sand_Canyon_1_E2: "Galbo",
+ Sand_Canyon_1_E3: "Oro",
+ Sand_Canyon_1_E4: "Sparky",
+ Sand_Canyon_1_E5: "Propeller",
+ Sand_Canyon_1_E6: "Gansan",
+ Sand_Canyon_1_E7: "Babut",
+ Sand_Canyon_1_E8: "Loud",
+ Sand_Canyon_1_E9: "Dogon",
+ Sand_Canyon_1_E10: "Bouncy",
+ Sand_Canyon_1_E11: "Pteran",
+ Sand_Canyon_1_E12: "Polof",
+ Sand_Canyon_2_E1: "KeKe",
+ Sand_Canyon_2_E2: "Doka",
+ Sand_Canyon_2_E3: "Boten",
+ Sand_Canyon_2_E4: "Propeller",
+ Sand_Canyon_2_E5: "Waddle Dee",
+ Sand_Canyon_2_E6: "Sparky",
+ Sand_Canyon_2_E7: "Sasuke",
+ Sand_Canyon_2_E8: "Como",
+ Sand_Canyon_2_E9: "Bukiset (Ice)",
+ Sand_Canyon_2_E10: "Bukiset (Needle)",
+ Sand_Canyon_2_E11: "Bukiset (Clean)",
+ Sand_Canyon_2_E12: "Bukiset (Parasol)",
+ Sand_Canyon_2_E13: "Bukiset (Spark)",
+ Sand_Canyon_2_E14: "Bukiset (Cutter)",
+ Sand_Canyon_2_E15: "Nidoo",
+ Sand_Canyon_2_E16: "Mariel",
+ Sand_Canyon_2_E17: "Yaban",
+ Sand_Canyon_2_E18: "Wapod",
+ Sand_Canyon_2_E19: "Squishy",
+ Sand_Canyon_2_E20: "Pteran",
+ Sand_Canyon_3_E1: "Sir Kibble",
+ Sand_Canyon_3_E2: "Broom Hatter",
+ Sand_Canyon_3_E3: "Rocky",
+ Sand_Canyon_3_E4: "Gabon",
+ Sand_Canyon_3_E5: "Kany",
+ Sand_Canyon_3_E6: "Galbo",
+ Sand_Canyon_3_E7: "Propeller",
+ Sand_Canyon_3_E8: "Sasuke",
+ Sand_Canyon_3_E9: "Wapod",
+ Sand_Canyon_3_E10: "Bobo",
+ Sand_Canyon_3_E11: "Babut",
+ Sand_Canyon_3_E12: "Magoo",
+ Sand_Canyon_4_E1: "Popon Ball",
+ Sand_Canyon_4_E2: "Mariel",
+ Sand_Canyon_4_E3: "Chilly",
+ Sand_Canyon_4_E4: "Tick",
+ Sand_Canyon_4_E5: "Bronto Burt",
+ Sand_Canyon_4_E6: "Babut",
+ Sand_Canyon_4_E7: "Bobin",
+ Sand_Canyon_4_E8: "Joe",
+ Sand_Canyon_4_E9: "Mony",
+ Sand_Canyon_4_E10: "Blipper",
+ Sand_Canyon_4_E11: "Togezo",
+ Sand_Canyon_4_E12: "Rocky",
+ Sand_Canyon_4_E13: "Bobo",
+ Sand_Canyon_5_E1: "Wapod",
+ Sand_Canyon_5_E2: "Dogon",
+ Sand_Canyon_5_E3: "Tick",
+ Sand_Canyon_5_E4: "Rocky",
+ Sand_Canyon_5_E5: "Bobo",
+ Sand_Canyon_5_E6: "Chilly",
+ Sand_Canyon_5_E7: "Sparky",
+ Sand_Canyon_5_E8: "Togezo",
+ Sand_Canyon_5_E9: "Bronto Burt",
+ Sand_Canyon_5_E10: "Sasuke",
+ Sand_Canyon_5_E11: "Oro",
+ Sand_Canyon_5_E12: "Galbo",
+ Sand_Canyon_5_E13: "Nidoo",
+ Sand_Canyon_5_E14: "Propeller",
+ Sand_Canyon_5_E15: "Sir Kibble",
+ Sand_Canyon_5_E16: "KeKe",
+ Sand_Canyon_5_E17: "Kabu",
+ Sand_Canyon_6_E1: "Sparky",
+ Sand_Canyon_6_E2: "Doka",
+ Sand_Canyon_6_E3: "Cappy",
+ Sand_Canyon_6_E4: "Pteran",
+ Sand_Canyon_6_E5: "Bukiset (Parasol)",
+ Sand_Canyon_6_E6: "Bukiset (Cutter)",
+ Sand_Canyon_6_E7: "Bukiset (Clean)",
+ Sand_Canyon_6_E8: "Bukiset (Spark)",
+ Sand_Canyon_6_E9: "Bukiset (Ice)",
+ Sand_Canyon_6_E10: "Bukiset (Needle)",
+ Sand_Canyon_6_E11: "Bukiset (Burning)",
+ Sand_Canyon_6_E12: "Bukiset (Stone)",
+ Sand_Canyon_6_E13: "Nidoo",
+ Cloudy_Park_1_E1: "Waddle Dee",
+ Cloudy_Park_1_E2: "KeKe",
+ Cloudy_Park_1_E3: "Cappy",
+ Cloudy_Park_1_E4: "Yaban",
+ Cloudy_Park_1_E5: "Togezo",
+ Cloudy_Park_1_E6: "Galbo",
+ Cloudy_Park_1_E7: "Sparky",
+ Cloudy_Park_1_E8: "Como",
+ Cloudy_Park_1_E9: "Bronto Burt",
+ Cloudy_Park_1_E10: "Gabon",
+ Cloudy_Park_1_E11: "Sir Kibble",
+ Cloudy_Park_1_E12: "Mariel",
+ Cloudy_Park_1_E13: "Nruff",
+ Cloudy_Park_2_E1: "Chilly",
+ Cloudy_Park_2_E2: "Sasuke",
+ Cloudy_Park_2_E3: "Waddle Dee",
+ Cloudy_Park_2_E4: "Sparky",
+ Cloudy_Park_2_E5: "Broom Hatter",
+ Cloudy_Park_2_E6: "Sir Kibble",
+ Cloudy_Park_2_E7: "Pteran",
+ Cloudy_Park_2_E8: "Propeller",
+ Cloudy_Park_2_E9: "Dogon",
+ Cloudy_Park_2_E10: "Togezo",
+ Cloudy_Park_2_E11: "Oro",
+ Cloudy_Park_2_E12: "Bronto Burt",
+ Cloudy_Park_2_E13: "Rocky",
+ Cloudy_Park_2_E14: "Galbo",
+ Cloudy_Park_2_E15: "Kapar",
+ Cloudy_Park_3_E1: "Bronto Burt",
+ Cloudy_Park_3_E2: "Mopoo",
+ Cloudy_Park_3_E3: "Poppy Bros Jr.",
+ Cloudy_Park_3_E4: "Como",
+ Cloudy_Park_3_E5: "Glunk",
+ Cloudy_Park_3_E6: "Bobin",
+ Cloudy_Park_3_E7: "Loud",
+ Cloudy_Park_3_E8: "Kapar",
+ Cloudy_Park_3_E9: "Galbo",
+ Cloudy_Park_3_E10: "Batamon",
+ Cloudy_Park_3_E11: "Bouncy",
+ Cloudy_Park_4_E1: "Gabon",
+ Cloudy_Park_4_E2: "Como",
+ Cloudy_Park_4_E3: "Wapod",
+ Cloudy_Park_4_E4: "Cappy",
+ Cloudy_Park_4_E5: "Sparky",
+ Cloudy_Park_4_E6: "Togezo",
+ Cloudy_Park_4_E7: "Bronto Burt",
+ Cloudy_Park_4_E8: "KeKe",
+ Cloudy_Park_4_E9: "Bouncy",
+ Cloudy_Park_4_E10: "Sir Kibble",
+ Cloudy_Park_4_E11: "Mariel",
+ Cloudy_Park_4_E12: "Kabu",
+ Cloudy_Park_4_E13: "Wappa",
+ Cloudy_Park_5_E1: "Yaban",
+ Cloudy_Park_5_E2: "Sir Kibble",
+ Cloudy_Park_5_E3: "Cappy",
+ Cloudy_Park_5_E4: "Wappa",
+ Cloudy_Park_5_E5: "Galbo",
+ Cloudy_Park_5_E6: "Bronto Burt",
+ Cloudy_Park_5_E7: "KeKe",
+ Cloudy_Park_5_E8: "Propeller",
+ Cloudy_Park_5_E9: "Klinko",
+ Cloudy_Park_5_E10: "Wapod",
+ Cloudy_Park_5_E11: "Pteran",
+ Cloudy_Park_6_E1: "Madoo",
+ Cloudy_Park_6_E2: "Tick",
+ Cloudy_Park_6_E3: "Como",
+ Cloudy_Park_6_E4: "Waddle Dee Drawing",
+ Cloudy_Park_6_E5: "Bronto Burt Drawing",
+ Cloudy_Park_6_E6: "Bouncy Drawing",
+ Cloudy_Park_6_E7: "Propeller",
+ Cloudy_Park_6_E8: "Mopoo",
+ Cloudy_Park_6_E9: "Bukiset (Burning)",
+ Cloudy_Park_6_E10: "Bukiset (Ice)",
+ Cloudy_Park_6_E11: "Bukiset (Needle)",
+ Cloudy_Park_6_E12: "Bukiset (Clean)",
+ Cloudy_Park_6_E13: "Bukiset (Cutter)",
+ Iceberg_1_E1: "Waddle Dee",
+ Iceberg_1_E2: "Klinko",
+ Iceberg_1_E3: "KeKe",
+ Iceberg_1_E4: "Como",
+ Iceberg_1_E5: "Galbo",
+ Iceberg_1_E6: "Rocky",
+ Iceberg_1_E7: "Kapar",
+ Iceberg_1_E8: "Mopoo",
+ Iceberg_1_E9: "Babut",
+ Iceberg_1_E10: "Wappa",
+ Iceberg_1_E11: "Bronto Burt",
+ Iceberg_1_E12: "Chilly",
+ Iceberg_1_E13: "Poppy Bros Jr.",
+ Iceberg_2_E1: "Gabon",
+ Iceberg_2_E2: "Nruff",
+ Iceberg_2_E3: "Waddle Dee",
+ Iceberg_2_E4: "Chilly",
+ Iceberg_2_E5: "Pteran",
+ Iceberg_2_E6: "Glunk",
+ Iceberg_2_E7: "Galbo",
+ Iceberg_2_E8: "Babut",
+ Iceberg_2_E9: "Magoo",
+ Iceberg_2_E10: "Propeller",
+ Iceberg_2_E11: "Nidoo",
+ Iceberg_2_E12: "Oro",
+ Iceberg_2_E13: "Klinko",
+ Iceberg_2_E14: "Bronto Burt",
+ Iceberg_3_E1: "Corori",
+ Iceberg_3_E2: "Bouncy",
+ Iceberg_3_E3: "Chilly",
+ Iceberg_3_E4: "Pteran",
+ Iceberg_3_E5: "Raft Waddle Dee",
+ Iceberg_3_E6: "Kapar",
+ Iceberg_3_E7: "Blipper",
+ Iceberg_3_E8: "Wapod",
+ Iceberg_3_E9: "Glunk",
+ Iceberg_3_E10: "Icicle",
+ Iceberg_4_E1: "Bronto Burt",
+ Iceberg_4_E2: "Galbo",
+ Iceberg_4_E3: "Klinko",
+ Iceberg_4_E4: "Chilly",
+ Iceberg_4_E5: "Babut",
+ Iceberg_4_E6: "Wappa",
+ Iceberg_4_E7: "Icicle",
+ Iceberg_4_E8: "Corori",
+ Iceberg_4_E9: "Gabon",
+ Iceberg_4_E10: "Kabu",
+ Iceberg_4_E11: "Broom Hatter",
+ Iceberg_4_E12: "Sasuke",
+ Iceberg_4_E13: "Nruff",
+ Iceberg_5_E1: "Bukiset (Burning)",
+ Iceberg_5_E2: "Bukiset (Stone)",
+ Iceberg_5_E3: "Bukiset (Ice)",
+ Iceberg_5_E4: "Bukiset (Needle)",
+ Iceberg_5_E5: "Bukiset (Clean)",
+ Iceberg_5_E6: "Bukiset (Parasol)",
+ Iceberg_5_E7: "Bukiset (Spark)",
+ Iceberg_5_E8: "Bukiset (Cutter)",
+ Iceberg_5_E9: "Glunk",
+ Iceberg_5_E10: "Wapod",
+ Iceberg_5_E11: "Tick",
+ Iceberg_5_E12: "Madoo",
+ Iceberg_5_E13: "Yaban",
+ Iceberg_5_E14: "Propeller",
+ Iceberg_5_E15: "Mariel",
+ Iceberg_5_E16: "Pteran",
+ Iceberg_5_E17: "Galbo",
+ Iceberg_5_E18: "KeKe",
+ Iceberg_5_E19: "Nidoo",
+ Iceberg_5_E20: "Waddle Dee Drawing",
+ Iceberg_5_E21: "Bronto Burt Drawing",
+ Iceberg_5_E22: "Bouncy Drawing",
+ Iceberg_5_E23: "Joe",
+ Iceberg_5_E24: "Kapar",
+ Iceberg_5_E25: "Gansan",
+ Iceberg_5_E26: "Sasuke",
+ Iceberg_5_E27: "Togezo",
+ Iceberg_5_E28: "Sparky",
+ Iceberg_5_E29: "Bobin",
+ Iceberg_5_E30: "Chilly",
+ Iceberg_5_E31: "Peran",
+ Iceberg_6_E1: "Nruff",
+ Iceberg_6_E2: "Nidoo",
+ Iceberg_6_E3: "Sparky",
+ Iceberg_6_E4: "Sir Kibble",
+ Grass_Land_4_M1: "Boboo",
+ Ripple_Field_4_M1: "Captain Stitch",
+ Sand_Canyon_4_M1: "Haboki",
+ Cloudy_Park_4_M1: "Jumper Shoot",
+ Iceberg_4_M1: "Yuki",
+ Iceberg_6_M1: "Blocky",
+ Iceberg_6_M2: "Jumper Shoot",
+ Iceberg_6_M3: "Yuki",
+ Iceberg_6_M4: "Haboki",
+ Iceberg_6_M5: "Boboo",
+ Iceberg_6_M6: "Captain Stitch",
+
+}
+
+vanilla_enemies = {'Waddle Dee': 'No Ability',
+ 'Bronto Burt': 'No Ability',
+ 'Rocky': 'Stone Ability',
+ 'Bobo': 'Burning Ability',
+ 'Chilly': 'Ice Ability',
+ 'Poppy Bros Jr.': 'No Ability',
+ 'Sparky': 'Spark Ability',
+ 'Polof': 'No Ability',
+ 'Broom Hatter': 'Clean Ability',
+ 'Cappy': 'No Ability',
+ 'Bouncy': 'No Ability',
+ 'Nruff': 'No Ability',
+ 'Glunk': 'No Ability',
+ 'Togezo': 'Needle Ability',
+ 'Kabu': 'No Ability',
+ 'Mony': 'No Ability',
+ 'Blipper': 'No Ability',
+ 'Squishy': 'No Ability',
+ 'Gabon': 'No Ability',
+ 'Oro': 'No Ability',
+ 'Galbo': 'Burning Ability',
+ 'Sir Kibble': 'Cutter Ability',
+ 'Nidoo': 'No Ability',
+ 'Kany': 'No Ability',
+ 'Sasuke': 'Parasol Ability',
+ 'Yaban': 'No Ability',
+ 'Boten': 'Needle Ability',
+ 'Coconut': 'No Ability',
+ 'Doka': 'No Ability',
+ 'Icicle': 'No Ability',
+ 'Pteran': 'No Ability',
+ 'Loud': 'No Ability',
+ 'Como': 'No Ability',
+ 'Klinko': 'Parasol Ability',
+ 'Babut': 'No Ability',
+ 'Wappa': 'Ice Ability',
+ 'Mariel': 'No Ability',
+ 'Tick': 'Needle Ability',
+ 'Apolo': 'No Ability',
+ 'Popon Ball': 'No Ability',
+ 'KeKe': 'Clean Ability',
+ 'Magoo': 'Burning Ability',
+ 'Raft Waddle Dee': 'No Ability',
+ 'Madoo': 'No Ability',
+ 'Corori': 'No Ability',
+ 'Kapar': 'Cutter Ability',
+ 'Batamon': 'No Ability',
+ 'Peran': 'No Ability',
+ 'Bobin': 'Spark Ability',
+ 'Mopoo': 'No Ability',
+ 'Gansan': 'Stone Ability',
+ 'Bukiset (Burning)': 'Burning Ability',
+ 'Bukiset (Stone)': 'Stone Ability',
+ 'Bukiset (Ice)': 'Ice Ability',
+ 'Bukiset (Needle)': 'Needle Ability',
+ 'Bukiset (Clean)': 'Clean Ability',
+ 'Bukiset (Parasol)': 'Parasol Ability',
+ 'Bukiset (Spark)': 'Spark Ability',
+ 'Bukiset (Cutter)': 'Cutter Ability',
+ 'Waddle Dee Drawing': 'No Ability',
+ 'Bronto Burt Drawing': 'No Ability',
+ 'Bouncy Drawing': 'No Ability',
+ 'Kabu (Dekabu)': 'No Ability',
+ 'Wapod': 'No Ability',
+ 'Propeller': 'No Ability',
+ 'Dogon': 'No Ability',
+ 'Joe': 'No Ability',
+ 'Captain Stitch': 'Needle Ability',
+ 'Yuki': 'Ice Ability',
+ 'Blocky': 'Stone Ability',
+ 'Jumper Shoot': 'Parasol Ability',
+ 'Boboo': 'Burning Ability',
+ 'Haboki': 'Clean Ability',
+ }
+
+enemy_restrictive: List[Tuple[List[str], List[str]]] = [
+ # abilities, enemies, set_all (False to set any)
+ (["Burning Ability", "Stone Ability"], ["Rocky", "Sparky", "Babut", "Squishy", ]), # Ribbon Field 5 - 7
+ # Sand Canyon 6
+ (["Parasol Ability", "Cutter Ability"], ['Bukiset (Parasol)', 'Bukiset (Cutter)']),
+ (["Spark Ability", "Clean Ability"], ['Bukiset (Spark)', 'Bukiset (Clean)']),
+ (["Ice Ability", "Needle Ability"], ['Bukiset (Ice)', 'Bukiset (Needle)']),
+ (["Stone Ability", "Burning Ability"], ['Bukiset (Stone)', 'Bukiset (Burning)']),
+ (["Stone Ability"], ['Bukiset (Burning)', 'Bukiset (Stone)', 'Bukiset (Ice)', 'Bukiset (Needle)',
+ 'Bukiset (Clean)', 'Bukiset (Spark)', 'Bukiset (Parasol)', 'Bukiset (Cutter)']),
+ (["Parasol Ability"], ['Bukiset (Burning)', 'Bukiset (Stone)', 'Bukiset (Ice)', 'Bukiset (Needle)',
+ 'Bukiset (Clean)', 'Bukiset (Spark)', 'Bukiset (Parasol)', 'Bukiset (Cutter)']),
+]
diff --git a/worlds/kdl3/Names/LocationName.py b/worlds/kdl3/Names/LocationName.py
new file mode 100644
index 000000000000..59a0a1d690f9
--- /dev/null
+++ b/worlds/kdl3/Names/LocationName.py
@@ -0,0 +1,928 @@
+# Level 1
+grass_land_1 = "Grass Land 1 - Complete"
+grass_land_2 = "Grass Land 2 - Complete"
+grass_land_3 = "Grass Land 3 - Complete"
+grass_land_4 = "Grass Land 4 - Complete"
+grass_land_5 = "Grass Land 5 - Complete"
+grass_land_6 = "Grass Land 6 - Complete"
+grass_land_tulip = "Grass Land 1 - Tulip"
+grass_land_muchi = "Grass Land 2 - Muchimuchi"
+grass_land_pitcherman = "Grass Land 3 - Pitcherman"
+grass_land_chao = "Grass Land 4 - Chao & Goku"
+grass_land_mine = "Grass Land 5 - Mine"
+grass_land_pierre = "Grass Land 6 - Pierre"
+grass_land_whispy = "Grass Land - Boss (Whispy Woods) Purified"
+
+# Level 2
+ripple_field_1 = "Ripple Field 1 - Complete"
+ripple_field_2 = "Ripple Field 2 - Complete"
+ripple_field_3 = "Ripple Field 3 - Complete"
+ripple_field_4 = "Ripple Field 4 - Complete"
+ripple_field_5 = "Ripple Field 5 - Complete"
+ripple_field_6 = "Ripple Field 6 - Complete"
+ripple_field_kamuribana = "Ripple Field 1 - Kamuribana"
+ripple_field_bakasa = "Ripple Field 2 - Bakasa"
+ripple_field_elieel = "Ripple Field 3 - Elieel"
+ripple_field_toad = "Ripple Field 4 - Toad & Little Toad"
+ripple_field_mama_pitch = "Ripple Field 5 - Mama Pitch"
+ripple_field_hb002 = "Ripple Field 6 - HB-002"
+ripple_field_acro = "Ripple Field - Boss (Acro) Purified"
+
+# Level 3
+sand_canyon_1 = "Sand Canyon 1 - Complete"
+sand_canyon_2 = "Sand Canyon 2 - Complete"
+sand_canyon_3 = "Sand Canyon 3 - Complete"
+sand_canyon_4 = "Sand Canyon 4 - Complete"
+sand_canyon_5 = "Sand Canyon 5 - Complete"
+sand_canyon_6 = "Sand Canyon 6 - Complete"
+sand_canyon_mushrooms = "Sand Canyon 1 - Geromuzudake"
+sand_canyon_auntie = "Sand Canyon 2 - Auntie"
+sand_canyon_caramello = "Sand Canyon 3 - Caramello"
+sand_canyon_hikari = "Sand Canyon 4 - Donbe & Hikari"
+sand_canyon_nyupun = "Sand Canyon 5 - Nyupun"
+sand_canyon_rob = "Sand Canyon 6 - Professor Hector & R.O.B"
+sand_canyon_poncon = "Sand Canyon - Boss (Pon & Con) Purified"
+
+# Level 4
+cloudy_park_1 = "Cloudy Park 1 - Complete"
+cloudy_park_2 = "Cloudy Park 2 - Complete"
+cloudy_park_3 = "Cloudy Park 3 - Complete"
+cloudy_park_4 = "Cloudy Park 4 - Complete"
+cloudy_park_5 = "Cloudy Park 5 - Complete"
+cloudy_park_6 = "Cloudy Park 6 - Complete"
+cloudy_park_hibanamodoki = "Cloudy Park 1 - Hibanamodoki"
+cloudy_park_piyokeko = "Cloudy Park 2 - Piyo & Keko"
+cloudy_park_mrball = "Cloudy Park 3 - Mr. Ball"
+cloudy_park_mikarin = "Cloudy Park 4 - Mikarin & Kagami Mocchi"
+cloudy_park_pick = "Cloudy Park 5 - Pick"
+cloudy_park_hb007 = "Cloudy Park 6 - HB-007"
+cloudy_park_ado = "Cloudy Park - Boss (Ado) Purified"
+
+# Level 5
+iceberg_1 = "Iceberg 1 - Complete"
+iceberg_2 = "Iceberg 2 - Complete"
+iceberg_3 = "Iceberg 3 - Complete"
+iceberg_4 = "Iceberg 4 - Complete"
+iceberg_5 = "Iceberg 5 - Complete"
+iceberg_6 = "Iceberg 6 - Complete"
+iceberg_kogoesou = "Iceberg 1 - Kogoesou"
+iceberg_samus = "Iceberg 2 - Samus"
+iceberg_kawasaki = "Iceberg 3 - Chef Kawasaki"
+iceberg_name = "Iceberg 4 - Name"
+iceberg_shiro = "Iceberg 5 - Shiro"
+iceberg_angel = "Iceberg 6 - Angel"
+iceberg_dedede = "Iceberg - Boss (Dedede) Purified"
+
+# Level 6
+hyper_zone = "Hyper Zone - Zero"
+
+# Extras
+boss_butch = "Boss Butch"
+mg5_p = "Minigame 5 - Perfect"
+jumping_clear = "Jumping - Target Score Reached"
+
+# 1-Ups
+grass_land_1_u1 = "Grass Land 1 - 1-Up (Parasol)" # Parasol
+grass_land_2_u1 = "Grass Land 2 - 1-Up (Needle)" # Needle
+grass_land_3_u1 = "Grass Land 3 - 1-Up (Climb)" # None
+grass_land_4_u1 = "Grass Land 4 - 1-Up (Gordo)" # None
+grass_land_6_u1 = "Grass Land 6 - 1-Up (Tower)" # None
+grass_land_6_u2 = "Grass Land 6 - 1-Up (Falling)" # None
+ripple_field_2_u1 = "Ripple Field 2 - 1-Up (Currents)" # Kine
+ripple_field_3_u1 = "Ripple Field 3 - 1-Up (Cutter/Spark)" # Cutter or Spark
+ripple_field_4_u1 = "Ripple Field 4 - 1-Up (Stone)" # Stone
+ripple_field_5_u1 = "Ripple Field 5 - 1-Up (Currents)" # Kine, Burning, Stone
+sand_canyon_1_u1 = "Sand Canyon 1 - 1-Up (Polof)" # None
+sand_canyon_2_u1 = "Sand Canyon 2 - 1-Up (Enclave)" # None
+sand_canyon_4_u1 = "Sand Canyon 4 - 1-Up (Clean)" # Clean
+sand_canyon_5_u1 = "Sand Canyon 5 - 1-Up (Falling Block)" # None
+sand_canyon_5_u2 = "Sand Canyon 5 - 1-Up (Ice 1)" # Ice
+sand_canyon_5_u3 = "Sand Canyon 5 - 1-Up (Ice 2)" # Ice
+sand_canyon_5_u4 = "Sand Canyon 5 - 1-Up (Ice 3)" # Ice
+cloudy_park_1_u1 = "Cloudy Park 1 - 1-Up (Shotzo)" # None
+cloudy_park_4_u1 = "Cloudy Park 4 - 1-Up (Windy)" # Coo
+cloudy_park_6_u1 = "Cloudy Park 6 - 1-Up (Cutter)" # Cutter
+iceberg_5_u1 = "Iceberg 5 - 1-Up (Boulder)" # None
+iceberg_5_u2 = "Iceberg 5 - 1-Up (Floor)" # None
+iceberg_5_u3 = "Iceberg 5 - 1-Up (Peloo)" # None, just let yourself get eaten by the Peloo
+iceberg_6_u1 = "Iceberg 6 - 1-Up (Middle)" # None
+
+# Maxim Tomatoes
+grass_land_1_m1 = "Grass Land 1 - Maxim Tomato (Spark)" # Spark
+grass_land_3_m1 = "Grass Land 3 - Maxim Tomato (Climb)" # None
+grass_land_4_m1 = "Grass Land 4 - Maxim Tomato (Zebon Right)" # None
+grass_land_4_m2 = "Grass Land 4 - Maxim Tomato (Gordo)" # None
+grass_land_4_m3 = "Grass Land 4 - Maxim Tomato (Cliff)" # None
+ripple_field_2_m1 = "Ripple Field 2 - Maxim Tomato (Currents)" # Kine
+ripple_field_3_m1 = "Ripple Field 3 - Maxim Tomato (Cove)" # None
+ripple_field_4_m1 = "Ripple Field 4 - Maxim Tomato (Dark)" # None (maybe Spark?)
+ripple_field_4_m2 = "Ripple Field 4 - Maxim Tomato (Stone)" # Stone
+ripple_field_5_m1 = "Ripple Field 5 - Maxim Tomato (Exit)" # Kine
+ripple_field_5_m2 = "Ripple Field 5 - Maxim Tomato (Currents)" # Kine, Burning, Stone
+sand_canyon_2_m1 = "Sand Canyon 2 - Maxim Tomato (Underwater)" # None
+sand_canyon_4_m1 = "Sand Canyon 4 - Maxim Tomato (Pacto)" # None
+sand_canyon_4_m2 = "Sand Canyon 4 - Maxim Tomato (Needle)" # Needle
+sand_canyon_5_m1 = "Sand Canyon 5 - Maxim Tomato (Pit)" # None
+cloudy_park_1_m1 = "Cloudy Park 1 - Maxim Tomato (Mariel)" # None
+cloudy_park_4_m1 = "Cloudy Park 4 - Maxim Tomato (Windy)" # Coo
+cloudy_park_5_m1 = "Cloudy Park 5 - Maxim Tomato (Pillars)" # None
+iceberg_3_m1 = "Iceberg 3 - Maxim Tomato (Ceiling)" # None
+iceberg_6_m1 = "Iceberg 6 - Maxim Tomato (Left)" # None
+
+# Level Names
+level_names = {
+ "Grass Land": 1,
+ "Ripple Field": 2,
+ "Sand Canyon": 3,
+ "Cloudy Park": 4,
+ "Iceberg": 5,
+}
+
+level_names_inverse = {
+ level_names[level]: level for level in level_names
+}
+
+# Boss Names
+boss_names = {
+ "Whispy Woods": 0x770200,
+ "Acro": 0x770201,
+ "Pon & Con": 0x770202,
+ "Ado": 0x770203,
+ "King Dedede": 0x770204
+}
+
+# Goal Mapping
+goals = {
+ 0: hyper_zone,
+ 1: boss_butch,
+ 2: mg5_p,
+ 3: jumping_clear
+}
+
+grass_land_1_s1 = "Grass Land 1 - Star 1"
+grass_land_1_s2 = "Grass Land 1 - Star 2"
+grass_land_1_s3 = "Grass Land 1 - Star 3"
+grass_land_1_s4 = "Grass Land 1 - Star 4"
+grass_land_1_s5 = "Grass Land 1 - Star 5"
+grass_land_1_s6 = "Grass Land 1 - Star 6"
+grass_land_1_s7 = "Grass Land 1 - Star 7"
+grass_land_1_s8 = "Grass Land 1 - Star 8"
+grass_land_1_s9 = "Grass Land 1 - Star 9"
+grass_land_1_s10 = "Grass Land 1 - Star 10"
+grass_land_1_s11 = "Grass Land 1 - Star 11"
+grass_land_1_s12 = "Grass Land 1 - Star 12"
+grass_land_1_s13 = "Grass Land 1 - Star 13"
+grass_land_1_s14 = "Grass Land 1 - Star 14"
+grass_land_1_s15 = "Grass Land 1 - Star 15"
+grass_land_1_s16 = "Grass Land 1 - Star 16"
+grass_land_1_s17 = "Grass Land 1 - Star 17"
+grass_land_1_s18 = "Grass Land 1 - Star 18"
+grass_land_1_s19 = "Grass Land 1 - Star 19"
+grass_land_1_s20 = "Grass Land 1 - Star 20"
+grass_land_1_s21 = "Grass Land 1 - Star 21"
+grass_land_1_s22 = "Grass Land 1 - Star 22"
+grass_land_1_s23 = "Grass Land 1 - Star 23"
+grass_land_2_s1 = "Grass Land 2 - Star 1"
+grass_land_2_s2 = "Grass Land 2 - Star 2"
+grass_land_2_s3 = "Grass Land 2 - Star 3"
+grass_land_2_s4 = "Grass Land 2 - Star 4"
+grass_land_2_s5 = "Grass Land 2 - Star 5"
+grass_land_2_s6 = "Grass Land 2 - Star 6"
+grass_land_2_s7 = "Grass Land 2 - Star 7"
+grass_land_2_s8 = "Grass Land 2 - Star 8"
+grass_land_2_s9 = "Grass Land 2 - Star 9"
+grass_land_2_s10 = "Grass Land 2 - Star 10"
+grass_land_2_s11 = "Grass Land 2 - Star 11"
+grass_land_2_s12 = "Grass Land 2 - Star 12"
+grass_land_2_s13 = "Grass Land 2 - Star 13"
+grass_land_2_s14 = "Grass Land 2 - Star 14"
+grass_land_2_s15 = "Grass Land 2 - Star 15"
+grass_land_2_s16 = "Grass Land 2 - Star 16"
+grass_land_2_s17 = "Grass Land 2 - Star 17"
+grass_land_2_s18 = "Grass Land 2 - Star 18"
+grass_land_2_s19 = "Grass Land 2 - Star 19"
+grass_land_2_s20 = "Grass Land 2 - Star 20"
+grass_land_2_s21 = "Grass Land 2 - Star 21"
+grass_land_3_s1 = "Grass Land 3 - Star 1"
+grass_land_3_s2 = "Grass Land 3 - Star 2"
+grass_land_3_s3 = "Grass Land 3 - Star 3"
+grass_land_3_s4 = "Grass Land 3 - Star 4"
+grass_land_3_s5 = "Grass Land 3 - Star 5"
+grass_land_3_s6 = "Grass Land 3 - Star 6"
+grass_land_3_s7 = "Grass Land 3 - Star 7"
+grass_land_3_s8 = "Grass Land 3 - Star 8"
+grass_land_3_s9 = "Grass Land 3 - Star 9"
+grass_land_3_s10 = "Grass Land 3 - Star 10"
+grass_land_3_s11 = "Grass Land 3 - Star 11"
+grass_land_3_s12 = "Grass Land 3 - Star 12"
+grass_land_3_s13 = "Grass Land 3 - Star 13"
+grass_land_3_s14 = "Grass Land 3 - Star 14"
+grass_land_3_s15 = "Grass Land 3 - Star 15"
+grass_land_3_s16 = "Grass Land 3 - Star 16"
+grass_land_3_s17 = "Grass Land 3 - Star 17"
+grass_land_3_s18 = "Grass Land 3 - Star 18"
+grass_land_3_s19 = "Grass Land 3 - Star 19"
+grass_land_3_s20 = "Grass Land 3 - Star 20"
+grass_land_3_s21 = "Grass Land 3 - Star 21"
+grass_land_3_s22 = "Grass Land 3 - Star 22"
+grass_land_3_s23 = "Grass Land 3 - Star 23"
+grass_land_3_s24 = "Grass Land 3 - Star 24"
+grass_land_3_s25 = "Grass Land 3 - Star 25"
+grass_land_3_s26 = "Grass Land 3 - Star 26"
+grass_land_3_s27 = "Grass Land 3 - Star 27"
+grass_land_3_s28 = "Grass Land 3 - Star 28"
+grass_land_3_s29 = "Grass Land 3 - Star 29"
+grass_land_3_s30 = "Grass Land 3 - Star 30"
+grass_land_3_s31 = "Grass Land 3 - Star 31"
+grass_land_4_s1 = "Grass Land 4 - Star 1"
+grass_land_4_s2 = "Grass Land 4 - Star 2"
+grass_land_4_s3 = "Grass Land 4 - Star 3"
+grass_land_4_s4 = "Grass Land 4 - Star 4"
+grass_land_4_s5 = "Grass Land 4 - Star 5"
+grass_land_4_s6 = "Grass Land 4 - Star 6"
+grass_land_4_s7 = "Grass Land 4 - Star 7"
+grass_land_4_s8 = "Grass Land 4 - Star 8"
+grass_land_4_s9 = "Grass Land 4 - Star 9"
+grass_land_4_s10 = "Grass Land 4 - Star 10"
+grass_land_4_s11 = "Grass Land 4 - Star 11"
+grass_land_4_s12 = "Grass Land 4 - Star 12"
+grass_land_4_s13 = "Grass Land 4 - Star 13"
+grass_land_4_s14 = "Grass Land 4 - Star 14"
+grass_land_4_s15 = "Grass Land 4 - Star 15"
+grass_land_4_s16 = "Grass Land 4 - Star 16"
+grass_land_4_s17 = "Grass Land 4 - Star 17"
+grass_land_4_s18 = "Grass Land 4 - Star 18"
+grass_land_4_s19 = "Grass Land 4 - Star 19"
+grass_land_4_s20 = "Grass Land 4 - Star 20"
+grass_land_4_s21 = "Grass Land 4 - Star 21"
+grass_land_4_s22 = "Grass Land 4 - Star 22"
+grass_land_4_s23 = "Grass Land 4 - Star 23"
+grass_land_4_s24 = "Grass Land 4 - Star 24"
+grass_land_4_s25 = "Grass Land 4 - Star 25"
+grass_land_4_s26 = "Grass Land 4 - Star 26"
+grass_land_4_s27 = "Grass Land 4 - Star 27"
+grass_land_4_s28 = "Grass Land 4 - Star 28"
+grass_land_4_s29 = "Grass Land 4 - Star 29"
+grass_land_4_s30 = "Grass Land 4 - Star 30"
+grass_land_4_s31 = "Grass Land 4 - Star 31"
+grass_land_4_s32 = "Grass Land 4 - Star 32"
+grass_land_4_s33 = "Grass Land 4 - Star 33"
+grass_land_4_s34 = "Grass Land 4 - Star 34"
+grass_land_4_s35 = "Grass Land 4 - Star 35"
+grass_land_4_s36 = "Grass Land 4 - Star 36"
+grass_land_4_s37 = "Grass Land 4 - Star 37"
+grass_land_5_s1 = "Grass Land 5 - Star 1"
+grass_land_5_s2 = "Grass Land 5 - Star 2"
+grass_land_5_s3 = "Grass Land 5 - Star 3"
+grass_land_5_s4 = "Grass Land 5 - Star 4"
+grass_land_5_s5 = "Grass Land 5 - Star 5"
+grass_land_5_s6 = "Grass Land 5 - Star 6"
+grass_land_5_s7 = "Grass Land 5 - Star 7"
+grass_land_5_s8 = "Grass Land 5 - Star 8"
+grass_land_5_s9 = "Grass Land 5 - Star 9"
+grass_land_5_s10 = "Grass Land 5 - Star 10"
+grass_land_5_s11 = "Grass Land 5 - Star 11"
+grass_land_5_s12 = "Grass Land 5 - Star 12"
+grass_land_5_s13 = "Grass Land 5 - Star 13"
+grass_land_5_s14 = "Grass Land 5 - Star 14"
+grass_land_5_s15 = "Grass Land 5 - Star 15"
+grass_land_5_s16 = "Grass Land 5 - Star 16"
+grass_land_5_s17 = "Grass Land 5 - Star 17"
+grass_land_5_s18 = "Grass Land 5 - Star 18"
+grass_land_5_s19 = "Grass Land 5 - Star 19"
+grass_land_5_s20 = "Grass Land 5 - Star 20"
+grass_land_5_s21 = "Grass Land 5 - Star 21"
+grass_land_5_s22 = "Grass Land 5 - Star 22"
+grass_land_5_s23 = "Grass Land 5 - Star 23"
+grass_land_5_s24 = "Grass Land 5 - Star 24"
+grass_land_5_s25 = "Grass Land 5 - Star 25"
+grass_land_5_s26 = "Grass Land 5 - Star 26"
+grass_land_5_s27 = "Grass Land 5 - Star 27"
+grass_land_5_s28 = "Grass Land 5 - Star 28"
+grass_land_5_s29 = "Grass Land 5 - Star 29"
+grass_land_6_s1 = "Grass Land 6 - Star 1"
+grass_land_6_s2 = "Grass Land 6 - Star 2"
+grass_land_6_s3 = "Grass Land 6 - Star 3"
+grass_land_6_s4 = "Grass Land 6 - Star 4"
+grass_land_6_s5 = "Grass Land 6 - Star 5"
+grass_land_6_s6 = "Grass Land 6 - Star 6"
+grass_land_6_s7 = "Grass Land 6 - Star 7"
+grass_land_6_s8 = "Grass Land 6 - Star 8"
+grass_land_6_s9 = "Grass Land 6 - Star 9"
+grass_land_6_s10 = "Grass Land 6 - Star 10"
+grass_land_6_s11 = "Grass Land 6 - Star 11"
+grass_land_6_s12 = "Grass Land 6 - Star 12"
+grass_land_6_s13 = "Grass Land 6 - Star 13"
+grass_land_6_s14 = "Grass Land 6 - Star 14"
+grass_land_6_s15 = "Grass Land 6 - Star 15"
+grass_land_6_s16 = "Grass Land 6 - Star 16"
+grass_land_6_s17 = "Grass Land 6 - Star 17"
+grass_land_6_s18 = "Grass Land 6 - Star 18"
+grass_land_6_s19 = "Grass Land 6 - Star 19"
+grass_land_6_s20 = "Grass Land 6 - Star 20"
+grass_land_6_s21 = "Grass Land 6 - Star 21"
+grass_land_6_s22 = "Grass Land 6 - Star 22"
+grass_land_6_s23 = "Grass Land 6 - Star 23"
+grass_land_6_s24 = "Grass Land 6 - Star 24"
+grass_land_6_s25 = "Grass Land 6 - Star 25"
+grass_land_6_s26 = "Grass Land 6 - Star 26"
+grass_land_6_s27 = "Grass Land 6 - Star 27"
+grass_land_6_s28 = "Grass Land 6 - Star 28"
+grass_land_6_s29 = "Grass Land 6 - Star 29"
+ripple_field_1_s1 = "Ripple Field 1 - Star 1"
+ripple_field_1_s2 = "Ripple Field 1 - Star 2"
+ripple_field_1_s3 = "Ripple Field 1 - Star 3"
+ripple_field_1_s4 = "Ripple Field 1 - Star 4"
+ripple_field_1_s5 = "Ripple Field 1 - Star 5"
+ripple_field_1_s6 = "Ripple Field 1 - Star 6"
+ripple_field_1_s7 = "Ripple Field 1 - Star 7"
+ripple_field_1_s8 = "Ripple Field 1 - Star 8"
+ripple_field_1_s9 = "Ripple Field 1 - Star 9"
+ripple_field_1_s10 = "Ripple Field 1 - Star 10"
+ripple_field_1_s11 = "Ripple Field 1 - Star 11"
+ripple_field_1_s12 = "Ripple Field 1 - Star 12"
+ripple_field_1_s13 = "Ripple Field 1 - Star 13"
+ripple_field_1_s14 = "Ripple Field 1 - Star 14"
+ripple_field_1_s15 = "Ripple Field 1 - Star 15"
+ripple_field_1_s16 = "Ripple Field 1 - Star 16"
+ripple_field_1_s17 = "Ripple Field 1 - Star 17"
+ripple_field_1_s18 = "Ripple Field 1 - Star 18"
+ripple_field_1_s19 = "Ripple Field 1 - Star 19"
+ripple_field_2_s1 = "Ripple Field 2 - Star 1"
+ripple_field_2_s2 = "Ripple Field 2 - Star 2"
+ripple_field_2_s3 = "Ripple Field 2 - Star 3"
+ripple_field_2_s4 = "Ripple Field 2 - Star 4"
+ripple_field_2_s5 = "Ripple Field 2 - Star 5"
+ripple_field_2_s6 = "Ripple Field 2 - Star 6"
+ripple_field_2_s7 = "Ripple Field 2 - Star 7"
+ripple_field_2_s8 = "Ripple Field 2 - Star 8"
+ripple_field_2_s9 = "Ripple Field 2 - Star 9"
+ripple_field_2_s10 = "Ripple Field 2 - Star 10"
+ripple_field_2_s11 = "Ripple Field 2 - Star 11"
+ripple_field_2_s12 = "Ripple Field 2 - Star 12"
+ripple_field_2_s13 = "Ripple Field 2 - Star 13"
+ripple_field_2_s14 = "Ripple Field 2 - Star 14"
+ripple_field_2_s15 = "Ripple Field 2 - Star 15"
+ripple_field_2_s16 = "Ripple Field 2 - Star 16"
+ripple_field_2_s17 = "Ripple Field 2 - Star 17"
+ripple_field_3_s1 = "Ripple Field 3 - Star 1"
+ripple_field_3_s2 = "Ripple Field 3 - Star 2"
+ripple_field_3_s3 = "Ripple Field 3 - Star 3"
+ripple_field_3_s4 = "Ripple Field 3 - Star 4"
+ripple_field_3_s5 = "Ripple Field 3 - Star 5"
+ripple_field_3_s6 = "Ripple Field 3 - Star 6"
+ripple_field_3_s7 = "Ripple Field 3 - Star 7"
+ripple_field_3_s8 = "Ripple Field 3 - Star 8"
+ripple_field_3_s9 = "Ripple Field 3 - Star 9"
+ripple_field_3_s10 = "Ripple Field 3 - Star 10"
+ripple_field_3_s11 = "Ripple Field 3 - Star 11"
+ripple_field_3_s12 = "Ripple Field 3 - Star 12"
+ripple_field_3_s13 = "Ripple Field 3 - Star 13"
+ripple_field_3_s14 = "Ripple Field 3 - Star 14"
+ripple_field_3_s15 = "Ripple Field 3 - Star 15"
+ripple_field_3_s16 = "Ripple Field 3 - Star 16"
+ripple_field_3_s17 = "Ripple Field 3 - Star 17"
+ripple_field_3_s18 = "Ripple Field 3 - Star 18"
+ripple_field_3_s19 = "Ripple Field 3 - Star 19"
+ripple_field_3_s20 = "Ripple Field 3 - Star 20"
+ripple_field_3_s21 = "Ripple Field 3 - Star 21"
+ripple_field_4_s1 = "Ripple Field 4 - Star 1"
+ripple_field_4_s2 = "Ripple Field 4 - Star 2"
+ripple_field_4_s3 = "Ripple Field 4 - Star 3"
+ripple_field_4_s4 = "Ripple Field 4 - Star 4"
+ripple_field_4_s5 = "Ripple Field 4 - Star 5"
+ripple_field_4_s6 = "Ripple Field 4 - Star 6"
+ripple_field_4_s7 = "Ripple Field 4 - Star 7"
+ripple_field_4_s8 = "Ripple Field 4 - Star 8"
+ripple_field_4_s9 = "Ripple Field 4 - Star 9"
+ripple_field_4_s10 = "Ripple Field 4 - Star 10"
+ripple_field_4_s11 = "Ripple Field 4 - Star 11"
+ripple_field_4_s12 = "Ripple Field 4 - Star 12"
+ripple_field_4_s13 = "Ripple Field 4 - Star 13"
+ripple_field_4_s14 = "Ripple Field 4 - Star 14"
+ripple_field_4_s15 = "Ripple Field 4 - Star 15"
+ripple_field_4_s16 = "Ripple Field 4 - Star 16"
+ripple_field_4_s17 = "Ripple Field 4 - Star 17"
+ripple_field_4_s18 = "Ripple Field 4 - Star 18"
+ripple_field_4_s19 = "Ripple Field 4 - Star 19"
+ripple_field_4_s20 = "Ripple Field 4 - Star 20"
+ripple_field_4_s21 = "Ripple Field 4 - Star 21"
+ripple_field_4_s22 = "Ripple Field 4 - Star 22"
+ripple_field_4_s23 = "Ripple Field 4 - Star 23"
+ripple_field_4_s24 = "Ripple Field 4 - Star 24"
+ripple_field_4_s25 = "Ripple Field 4 - Star 25"
+ripple_field_4_s26 = "Ripple Field 4 - Star 26"
+ripple_field_4_s27 = "Ripple Field 4 - Star 27"
+ripple_field_4_s28 = "Ripple Field 4 - Star 28"
+ripple_field_4_s29 = "Ripple Field 4 - Star 29"
+ripple_field_4_s30 = "Ripple Field 4 - Star 30"
+ripple_field_4_s31 = "Ripple Field 4 - Star 31"
+ripple_field_4_s32 = "Ripple Field 4 - Star 32"
+ripple_field_4_s33 = "Ripple Field 4 - Star 33"
+ripple_field_4_s34 = "Ripple Field 4 - Star 34"
+ripple_field_4_s35 = "Ripple Field 4 - Star 35"
+ripple_field_4_s36 = "Ripple Field 4 - Star 36"
+ripple_field_4_s37 = "Ripple Field 4 - Star 37"
+ripple_field_4_s38 = "Ripple Field 4 - Star 38"
+ripple_field_4_s39 = "Ripple Field 4 - Star 39"
+ripple_field_4_s40 = "Ripple Field 4 - Star 40"
+ripple_field_4_s41 = "Ripple Field 4 - Star 41"
+ripple_field_4_s42 = "Ripple Field 4 - Star 42"
+ripple_field_4_s43 = "Ripple Field 4 - Star 43"
+ripple_field_4_s44 = "Ripple Field 4 - Star 44"
+ripple_field_4_s45 = "Ripple Field 4 - Star 45"
+ripple_field_4_s46 = "Ripple Field 4 - Star 46"
+ripple_field_4_s47 = "Ripple Field 4 - Star 47"
+ripple_field_4_s48 = "Ripple Field 4 - Star 48"
+ripple_field_4_s49 = "Ripple Field 4 - Star 49"
+ripple_field_4_s50 = "Ripple Field 4 - Star 50"
+ripple_field_4_s51 = "Ripple Field 4 - Star 51"
+ripple_field_5_s1 = "Ripple Field 5 - Star 1"
+ripple_field_5_s2 = "Ripple Field 5 - Star 2"
+ripple_field_5_s3 = "Ripple Field 5 - Star 3"
+ripple_field_5_s4 = "Ripple Field 5 - Star 4"
+ripple_field_5_s5 = "Ripple Field 5 - Star 5"
+ripple_field_5_s6 = "Ripple Field 5 - Star 6"
+ripple_field_5_s7 = "Ripple Field 5 - Star 7"
+ripple_field_5_s8 = "Ripple Field 5 - Star 8"
+ripple_field_5_s9 = "Ripple Field 5 - Star 9"
+ripple_field_5_s10 = "Ripple Field 5 - Star 10"
+ripple_field_5_s11 = "Ripple Field 5 - Star 11"
+ripple_field_5_s12 = "Ripple Field 5 - Star 12"
+ripple_field_5_s13 = "Ripple Field 5 - Star 13"
+ripple_field_5_s14 = "Ripple Field 5 - Star 14"
+ripple_field_5_s15 = "Ripple Field 5 - Star 15"
+ripple_field_5_s16 = "Ripple Field 5 - Star 16"
+ripple_field_5_s17 = "Ripple Field 5 - Star 17"
+ripple_field_5_s18 = "Ripple Field 5 - Star 18"
+ripple_field_5_s19 = "Ripple Field 5 - Star 19"
+ripple_field_5_s20 = "Ripple Field 5 - Star 20"
+ripple_field_5_s21 = "Ripple Field 5 - Star 21"
+ripple_field_5_s22 = "Ripple Field 5 - Star 22"
+ripple_field_5_s23 = "Ripple Field 5 - Star 23"
+ripple_field_5_s24 = "Ripple Field 5 - Star 24"
+ripple_field_5_s25 = "Ripple Field 5 - Star 25"
+ripple_field_5_s26 = "Ripple Field 5 - Star 26"
+ripple_field_5_s27 = "Ripple Field 5 - Star 27"
+ripple_field_5_s28 = "Ripple Field 5 - Star 28"
+ripple_field_5_s29 = "Ripple Field 5 - Star 29"
+ripple_field_5_s30 = "Ripple Field 5 - Star 30"
+ripple_field_5_s31 = "Ripple Field 5 - Star 31"
+ripple_field_5_s32 = "Ripple Field 5 - Star 32"
+ripple_field_5_s33 = "Ripple Field 5 - Star 33"
+ripple_field_5_s34 = "Ripple Field 5 - Star 34"
+ripple_field_5_s35 = "Ripple Field 5 - Star 35"
+ripple_field_5_s36 = "Ripple Field 5 - Star 36"
+ripple_field_5_s37 = "Ripple Field 5 - Star 37"
+ripple_field_5_s38 = "Ripple Field 5 - Star 38"
+ripple_field_5_s39 = "Ripple Field 5 - Star 39"
+ripple_field_5_s40 = "Ripple Field 5 - Star 40"
+ripple_field_5_s41 = "Ripple Field 5 - Star 41"
+ripple_field_5_s42 = "Ripple Field 5 - Star 42"
+ripple_field_5_s43 = "Ripple Field 5 - Star 43"
+ripple_field_5_s44 = "Ripple Field 5 - Star 44"
+ripple_field_5_s45 = "Ripple Field 5 - Star 45"
+ripple_field_5_s46 = "Ripple Field 5 - Star 46"
+ripple_field_5_s47 = "Ripple Field 5 - Star 47"
+ripple_field_5_s48 = "Ripple Field 5 - Star 48"
+ripple_field_5_s49 = "Ripple Field 5 - Star 49"
+ripple_field_5_s50 = "Ripple Field 5 - Star 50"
+ripple_field_5_s51 = "Ripple Field 5 - Star 51"
+ripple_field_6_s1 = "Ripple Field 6 - Star 1"
+ripple_field_6_s2 = "Ripple Field 6 - Star 2"
+ripple_field_6_s3 = "Ripple Field 6 - Star 3"
+ripple_field_6_s4 = "Ripple Field 6 - Star 4"
+ripple_field_6_s5 = "Ripple Field 6 - Star 5"
+ripple_field_6_s6 = "Ripple Field 6 - Star 6"
+ripple_field_6_s7 = "Ripple Field 6 - Star 7"
+ripple_field_6_s8 = "Ripple Field 6 - Star 8"
+ripple_field_6_s9 = "Ripple Field 6 - Star 9"
+ripple_field_6_s10 = "Ripple Field 6 - Star 10"
+ripple_field_6_s11 = "Ripple Field 6 - Star 11"
+ripple_field_6_s12 = "Ripple Field 6 - Star 12"
+ripple_field_6_s13 = "Ripple Field 6 - Star 13"
+ripple_field_6_s14 = "Ripple Field 6 - Star 14"
+ripple_field_6_s15 = "Ripple Field 6 - Star 15"
+ripple_field_6_s16 = "Ripple Field 6 - Star 16"
+ripple_field_6_s17 = "Ripple Field 6 - Star 17"
+ripple_field_6_s18 = "Ripple Field 6 - Star 18"
+ripple_field_6_s19 = "Ripple Field 6 - Star 19"
+ripple_field_6_s20 = "Ripple Field 6 - Star 20"
+ripple_field_6_s21 = "Ripple Field 6 - Star 21"
+ripple_field_6_s22 = "Ripple Field 6 - Star 22"
+ripple_field_6_s23 = "Ripple Field 6 - Star 23"
+sand_canyon_1_s1 = "Sand Canyon 1 - Star 1"
+sand_canyon_1_s2 = "Sand Canyon 1 - Star 2"
+sand_canyon_1_s3 = "Sand Canyon 1 - Star 3"
+sand_canyon_1_s4 = "Sand Canyon 1 - Star 4"
+sand_canyon_1_s5 = "Sand Canyon 1 - Star 5"
+sand_canyon_1_s6 = "Sand Canyon 1 - Star 6"
+sand_canyon_1_s7 = "Sand Canyon 1 - Star 7"
+sand_canyon_1_s8 = "Sand Canyon 1 - Star 8"
+sand_canyon_1_s9 = "Sand Canyon 1 - Star 9"
+sand_canyon_1_s10 = "Sand Canyon 1 - Star 10"
+sand_canyon_1_s11 = "Sand Canyon 1 - Star 11"
+sand_canyon_1_s12 = "Sand Canyon 1 - Star 12"
+sand_canyon_1_s13 = "Sand Canyon 1 - Star 13"
+sand_canyon_1_s14 = "Sand Canyon 1 - Star 14"
+sand_canyon_1_s15 = "Sand Canyon 1 - Star 15"
+sand_canyon_1_s16 = "Sand Canyon 1 - Star 16"
+sand_canyon_1_s17 = "Sand Canyon 1 - Star 17"
+sand_canyon_1_s18 = "Sand Canyon 1 - Star 18"
+sand_canyon_1_s19 = "Sand Canyon 1 - Star 19"
+sand_canyon_1_s20 = "Sand Canyon 1 - Star 20"
+sand_canyon_1_s21 = "Sand Canyon 1 - Star 21"
+sand_canyon_1_s22 = "Sand Canyon 1 - Star 22"
+sand_canyon_2_s1 = "Sand Canyon 2 - Star 1"
+sand_canyon_2_s2 = "Sand Canyon 2 - Star 2"
+sand_canyon_2_s3 = "Sand Canyon 2 - Star 3"
+sand_canyon_2_s4 = "Sand Canyon 2 - Star 4"
+sand_canyon_2_s5 = "Sand Canyon 2 - Star 5"
+sand_canyon_2_s6 = "Sand Canyon 2 - Star 6"
+sand_canyon_2_s7 = "Sand Canyon 2 - Star 7"
+sand_canyon_2_s8 = "Sand Canyon 2 - Star 8"
+sand_canyon_2_s9 = "Sand Canyon 2 - Star 9"
+sand_canyon_2_s10 = "Sand Canyon 2 - Star 10"
+sand_canyon_2_s11 = "Sand Canyon 2 - Star 11"
+sand_canyon_2_s12 = "Sand Canyon 2 - Star 12"
+sand_canyon_2_s13 = "Sand Canyon 2 - Star 13"
+sand_canyon_2_s14 = "Sand Canyon 2 - Star 14"
+sand_canyon_2_s15 = "Sand Canyon 2 - Star 15"
+sand_canyon_2_s16 = "Sand Canyon 2 - Star 16"
+sand_canyon_2_s17 = "Sand Canyon 2 - Star 17"
+sand_canyon_2_s18 = "Sand Canyon 2 - Star 18"
+sand_canyon_2_s19 = "Sand Canyon 2 - Star 19"
+sand_canyon_2_s20 = "Sand Canyon 2 - Star 20"
+sand_canyon_2_s21 = "Sand Canyon 2 - Star 21"
+sand_canyon_2_s22 = "Sand Canyon 2 - Star 22"
+sand_canyon_2_s23 = "Sand Canyon 2 - Star 23"
+sand_canyon_2_s24 = "Sand Canyon 2 - Star 24"
+sand_canyon_2_s25 = "Sand Canyon 2 - Star 25"
+sand_canyon_2_s26 = "Sand Canyon 2 - Star 26"
+sand_canyon_2_s27 = "Sand Canyon 2 - Star 27"
+sand_canyon_2_s28 = "Sand Canyon 2 - Star 28"
+sand_canyon_2_s29 = "Sand Canyon 2 - Star 29"
+sand_canyon_2_s30 = "Sand Canyon 2 - Star 30"
+sand_canyon_2_s31 = "Sand Canyon 2 - Star 31"
+sand_canyon_2_s32 = "Sand Canyon 2 - Star 32"
+sand_canyon_2_s33 = "Sand Canyon 2 - Star 33"
+sand_canyon_2_s34 = "Sand Canyon 2 - Star 34"
+sand_canyon_2_s35 = "Sand Canyon 2 - Star 35"
+sand_canyon_2_s36 = "Sand Canyon 2 - Star 36"
+sand_canyon_2_s37 = "Sand Canyon 2 - Star 37"
+sand_canyon_2_s38 = "Sand Canyon 2 - Star 38"
+sand_canyon_2_s39 = "Sand Canyon 2 - Star 39"
+sand_canyon_2_s40 = "Sand Canyon 2 - Star 40"
+sand_canyon_2_s41 = "Sand Canyon 2 - Star 41"
+sand_canyon_2_s42 = "Sand Canyon 2 - Star 42"
+sand_canyon_2_s43 = "Sand Canyon 2 - Star 43"
+sand_canyon_2_s44 = "Sand Canyon 2 - Star 44"
+sand_canyon_2_s45 = "Sand Canyon 2 - Star 45"
+sand_canyon_2_s46 = "Sand Canyon 2 - Star 46"
+sand_canyon_2_s47 = "Sand Canyon 2 - Star 47"
+sand_canyon_2_s48 = "Sand Canyon 2 - Star 48"
+sand_canyon_3_s1 = "Sand Canyon 3 - Star 1"
+sand_canyon_3_s2 = "Sand Canyon 3 - Star 2"
+sand_canyon_3_s3 = "Sand Canyon 3 - Star 3"
+sand_canyon_3_s4 = "Sand Canyon 3 - Star 4"
+sand_canyon_3_s5 = "Sand Canyon 3 - Star 5"
+sand_canyon_3_s6 = "Sand Canyon 3 - Star 6"
+sand_canyon_3_s7 = "Sand Canyon 3 - Star 7"
+sand_canyon_3_s8 = "Sand Canyon 3 - Star 8"
+sand_canyon_3_s9 = "Sand Canyon 3 - Star 9"
+sand_canyon_3_s10 = "Sand Canyon 3 - Star 10"
+sand_canyon_4_s1 = "Sand Canyon 4 - Star 1"
+sand_canyon_4_s2 = "Sand Canyon 4 - Star 2"
+sand_canyon_4_s3 = "Sand Canyon 4 - Star 3"
+sand_canyon_4_s4 = "Sand Canyon 4 - Star 4"
+sand_canyon_4_s5 = "Sand Canyon 4 - Star 5"
+sand_canyon_4_s6 = "Sand Canyon 4 - Star 6"
+sand_canyon_4_s7 = "Sand Canyon 4 - Star 7"
+sand_canyon_4_s8 = "Sand Canyon 4 - Star 8"
+sand_canyon_4_s9 = "Sand Canyon 4 - Star 9"
+sand_canyon_4_s10 = "Sand Canyon 4 - Star 10"
+sand_canyon_4_s11 = "Sand Canyon 4 - Star 11"
+sand_canyon_4_s12 = "Sand Canyon 4 - Star 12"
+sand_canyon_4_s13 = "Sand Canyon 4 - Star 13"
+sand_canyon_4_s14 = "Sand Canyon 4 - Star 14"
+sand_canyon_4_s15 = "Sand Canyon 4 - Star 15"
+sand_canyon_4_s16 = "Sand Canyon 4 - Star 16"
+sand_canyon_4_s17 = "Sand Canyon 4 - Star 17"
+sand_canyon_4_s18 = "Sand Canyon 4 - Star 18"
+sand_canyon_4_s19 = "Sand Canyon 4 - Star 19"
+sand_canyon_4_s20 = "Sand Canyon 4 - Star 20"
+sand_canyon_4_s21 = "Sand Canyon 4 - Star 21"
+sand_canyon_4_s22 = "Sand Canyon 4 - Star 22"
+sand_canyon_4_s23 = "Sand Canyon 4 - Star 23"
+sand_canyon_5_s1 = "Sand Canyon 5 - Star 1"
+sand_canyon_5_s2 = "Sand Canyon 5 - Star 2"
+sand_canyon_5_s3 = "Sand Canyon 5 - Star 3"
+sand_canyon_5_s4 = "Sand Canyon 5 - Star 4"
+sand_canyon_5_s5 = "Sand Canyon 5 - Star 5"
+sand_canyon_5_s6 = "Sand Canyon 5 - Star 6"
+sand_canyon_5_s7 = "Sand Canyon 5 - Star 7"
+sand_canyon_5_s8 = "Sand Canyon 5 - Star 8"
+sand_canyon_5_s9 = "Sand Canyon 5 - Star 9"
+sand_canyon_5_s10 = "Sand Canyon 5 - Star 10"
+sand_canyon_5_s11 = "Sand Canyon 5 - Star 11"
+sand_canyon_5_s12 = "Sand Canyon 5 - Star 12"
+sand_canyon_5_s13 = "Sand Canyon 5 - Star 13"
+sand_canyon_5_s14 = "Sand Canyon 5 - Star 14"
+sand_canyon_5_s15 = "Sand Canyon 5 - Star 15"
+sand_canyon_5_s16 = "Sand Canyon 5 - Star 16"
+sand_canyon_5_s17 = "Sand Canyon 5 - Star 17"
+sand_canyon_5_s18 = "Sand Canyon 5 - Star 18"
+sand_canyon_5_s19 = "Sand Canyon 5 - Star 19"
+sand_canyon_5_s20 = "Sand Canyon 5 - Star 20"
+sand_canyon_5_s21 = "Sand Canyon 5 - Star 21"
+sand_canyon_5_s22 = "Sand Canyon 5 - Star 22"
+sand_canyon_5_s23 = "Sand Canyon 5 - Star 23"
+sand_canyon_5_s24 = "Sand Canyon 5 - Star 24"
+sand_canyon_5_s25 = "Sand Canyon 5 - Star 25"
+sand_canyon_5_s26 = "Sand Canyon 5 - Star 26"
+sand_canyon_5_s27 = "Sand Canyon 5 - Star 27"
+sand_canyon_5_s28 = "Sand Canyon 5 - Star 28"
+sand_canyon_5_s29 = "Sand Canyon 5 - Star 29"
+sand_canyon_5_s30 = "Sand Canyon 5 - Star 30"
+sand_canyon_5_s31 = "Sand Canyon 5 - Star 31"
+sand_canyon_5_s32 = "Sand Canyon 5 - Star 32"
+sand_canyon_5_s33 = "Sand Canyon 5 - Star 33"
+sand_canyon_5_s34 = "Sand Canyon 5 - Star 34"
+sand_canyon_5_s35 = "Sand Canyon 5 - Star 35"
+sand_canyon_5_s36 = "Sand Canyon 5 - Star 36"
+sand_canyon_5_s37 = "Sand Canyon 5 - Star 37"
+sand_canyon_5_s38 = "Sand Canyon 5 - Star 38"
+sand_canyon_5_s39 = "Sand Canyon 5 - Star 39"
+sand_canyon_5_s40 = "Sand Canyon 5 - Star 40"
+cloudy_park_1_s1 = "Cloudy Park 1 - Star 1"
+cloudy_park_1_s2 = "Cloudy Park 1 - Star 2"
+cloudy_park_1_s3 = "Cloudy Park 1 - Star 3"
+cloudy_park_1_s4 = "Cloudy Park 1 - Star 4"
+cloudy_park_1_s5 = "Cloudy Park 1 - Star 5"
+cloudy_park_1_s6 = "Cloudy Park 1 - Star 6"
+cloudy_park_1_s7 = "Cloudy Park 1 - Star 7"
+cloudy_park_1_s8 = "Cloudy Park 1 - Star 8"
+cloudy_park_1_s9 = "Cloudy Park 1 - Star 9"
+cloudy_park_1_s10 = "Cloudy Park 1 - Star 10"
+cloudy_park_1_s11 = "Cloudy Park 1 - Star 11"
+cloudy_park_1_s12 = "Cloudy Park 1 - Star 12"
+cloudy_park_1_s13 = "Cloudy Park 1 - Star 13"
+cloudy_park_1_s14 = "Cloudy Park 1 - Star 14"
+cloudy_park_1_s15 = "Cloudy Park 1 - Star 15"
+cloudy_park_1_s16 = "Cloudy Park 1 - Star 16"
+cloudy_park_1_s17 = "Cloudy Park 1 - Star 17"
+cloudy_park_1_s18 = "Cloudy Park 1 - Star 18"
+cloudy_park_1_s19 = "Cloudy Park 1 - Star 19"
+cloudy_park_1_s20 = "Cloudy Park 1 - Star 20"
+cloudy_park_1_s21 = "Cloudy Park 1 - Star 21"
+cloudy_park_1_s22 = "Cloudy Park 1 - Star 22"
+cloudy_park_1_s23 = "Cloudy Park 1 - Star 23"
+cloudy_park_2_s1 = "Cloudy Park 2 - Star 1"
+cloudy_park_2_s2 = "Cloudy Park 2 - Star 2"
+cloudy_park_2_s3 = "Cloudy Park 2 - Star 3"
+cloudy_park_2_s4 = "Cloudy Park 2 - Star 4"
+cloudy_park_2_s5 = "Cloudy Park 2 - Star 5"
+cloudy_park_2_s6 = "Cloudy Park 2 - Star 6"
+cloudy_park_2_s7 = "Cloudy Park 2 - Star 7"
+cloudy_park_2_s8 = "Cloudy Park 2 - Star 8"
+cloudy_park_2_s9 = "Cloudy Park 2 - Star 9"
+cloudy_park_2_s10 = "Cloudy Park 2 - Star 10"
+cloudy_park_2_s11 = "Cloudy Park 2 - Star 11"
+cloudy_park_2_s12 = "Cloudy Park 2 - Star 12"
+cloudy_park_2_s13 = "Cloudy Park 2 - Star 13"
+cloudy_park_2_s14 = "Cloudy Park 2 - Star 14"
+cloudy_park_2_s15 = "Cloudy Park 2 - Star 15"
+cloudy_park_2_s16 = "Cloudy Park 2 - Star 16"
+cloudy_park_2_s17 = "Cloudy Park 2 - Star 17"
+cloudy_park_2_s18 = "Cloudy Park 2 - Star 18"
+cloudy_park_2_s19 = "Cloudy Park 2 - Star 19"
+cloudy_park_2_s20 = "Cloudy Park 2 - Star 20"
+cloudy_park_2_s21 = "Cloudy Park 2 - Star 21"
+cloudy_park_2_s22 = "Cloudy Park 2 - Star 22"
+cloudy_park_2_s23 = "Cloudy Park 2 - Star 23"
+cloudy_park_2_s24 = "Cloudy Park 2 - Star 24"
+cloudy_park_2_s25 = "Cloudy Park 2 - Star 25"
+cloudy_park_2_s26 = "Cloudy Park 2 - Star 26"
+cloudy_park_2_s27 = "Cloudy Park 2 - Star 27"
+cloudy_park_2_s28 = "Cloudy Park 2 - Star 28"
+cloudy_park_2_s29 = "Cloudy Park 2 - Star 29"
+cloudy_park_2_s30 = "Cloudy Park 2 - Star 30"
+cloudy_park_2_s31 = "Cloudy Park 2 - Star 31"
+cloudy_park_2_s32 = "Cloudy Park 2 - Star 32"
+cloudy_park_2_s33 = "Cloudy Park 2 - Star 33"
+cloudy_park_2_s34 = "Cloudy Park 2 - Star 34"
+cloudy_park_2_s35 = "Cloudy Park 2 - Star 35"
+cloudy_park_2_s36 = "Cloudy Park 2 - Star 36"
+cloudy_park_2_s37 = "Cloudy Park 2 - Star 37"
+cloudy_park_2_s38 = "Cloudy Park 2 - Star 38"
+cloudy_park_2_s39 = "Cloudy Park 2 - Star 39"
+cloudy_park_2_s40 = "Cloudy Park 2 - Star 40"
+cloudy_park_2_s41 = "Cloudy Park 2 - Star 41"
+cloudy_park_2_s42 = "Cloudy Park 2 - Star 42"
+cloudy_park_2_s43 = "Cloudy Park 2 - Star 43"
+cloudy_park_2_s44 = "Cloudy Park 2 - Star 44"
+cloudy_park_2_s45 = "Cloudy Park 2 - Star 45"
+cloudy_park_2_s46 = "Cloudy Park 2 - Star 46"
+cloudy_park_2_s47 = "Cloudy Park 2 - Star 47"
+cloudy_park_2_s48 = "Cloudy Park 2 - Star 48"
+cloudy_park_2_s49 = "Cloudy Park 2 - Star 49"
+cloudy_park_2_s50 = "Cloudy Park 2 - Star 50"
+cloudy_park_2_s51 = "Cloudy Park 2 - Star 51"
+cloudy_park_2_s52 = "Cloudy Park 2 - Star 52"
+cloudy_park_2_s53 = "Cloudy Park 2 - Star 53"
+cloudy_park_2_s54 = "Cloudy Park 2 - Star 54"
+cloudy_park_3_s1 = "Cloudy Park 3 - Star 1"
+cloudy_park_3_s2 = "Cloudy Park 3 - Star 2"
+cloudy_park_3_s3 = "Cloudy Park 3 - Star 3"
+cloudy_park_3_s4 = "Cloudy Park 3 - Star 4"
+cloudy_park_3_s5 = "Cloudy Park 3 - Star 5"
+cloudy_park_3_s6 = "Cloudy Park 3 - Star 6"
+cloudy_park_3_s7 = "Cloudy Park 3 - Star 7"
+cloudy_park_3_s8 = "Cloudy Park 3 - Star 8"
+cloudy_park_3_s9 = "Cloudy Park 3 - Star 9"
+cloudy_park_3_s10 = "Cloudy Park 3 - Star 10"
+cloudy_park_3_s11 = "Cloudy Park 3 - Star 11"
+cloudy_park_3_s12 = "Cloudy Park 3 - Star 12"
+cloudy_park_3_s13 = "Cloudy Park 3 - Star 13"
+cloudy_park_3_s14 = "Cloudy Park 3 - Star 14"
+cloudy_park_3_s15 = "Cloudy Park 3 - Star 15"
+cloudy_park_3_s16 = "Cloudy Park 3 - Star 16"
+cloudy_park_3_s17 = "Cloudy Park 3 - Star 17"
+cloudy_park_3_s18 = "Cloudy Park 3 - Star 18"
+cloudy_park_3_s19 = "Cloudy Park 3 - Star 19"
+cloudy_park_3_s20 = "Cloudy Park 3 - Star 20"
+cloudy_park_3_s21 = "Cloudy Park 3 - Star 21"
+cloudy_park_3_s22 = "Cloudy Park 3 - Star 22"
+cloudy_park_4_s1 = "Cloudy Park 4 - Star 1"
+cloudy_park_4_s2 = "Cloudy Park 4 - Star 2"
+cloudy_park_4_s3 = "Cloudy Park 4 - Star 3"
+cloudy_park_4_s4 = "Cloudy Park 4 - Star 4"
+cloudy_park_4_s5 = "Cloudy Park 4 - Star 5"
+cloudy_park_4_s6 = "Cloudy Park 4 - Star 6"
+cloudy_park_4_s7 = "Cloudy Park 4 - Star 7"
+cloudy_park_4_s8 = "Cloudy Park 4 - Star 8"
+cloudy_park_4_s9 = "Cloudy Park 4 - Star 9"
+cloudy_park_4_s10 = "Cloudy Park 4 - Star 10"
+cloudy_park_4_s11 = "Cloudy Park 4 - Star 11"
+cloudy_park_4_s12 = "Cloudy Park 4 - Star 12"
+cloudy_park_4_s13 = "Cloudy Park 4 - Star 13"
+cloudy_park_4_s14 = "Cloudy Park 4 - Star 14"
+cloudy_park_4_s15 = "Cloudy Park 4 - Star 15"
+cloudy_park_4_s16 = "Cloudy Park 4 - Star 16"
+cloudy_park_4_s17 = "Cloudy Park 4 - Star 17"
+cloudy_park_4_s18 = "Cloudy Park 4 - Star 18"
+cloudy_park_4_s19 = "Cloudy Park 4 - Star 19"
+cloudy_park_4_s20 = "Cloudy Park 4 - Star 20"
+cloudy_park_4_s21 = "Cloudy Park 4 - Star 21"
+cloudy_park_4_s22 = "Cloudy Park 4 - Star 22"
+cloudy_park_4_s23 = "Cloudy Park 4 - Star 23"
+cloudy_park_4_s24 = "Cloudy Park 4 - Star 24"
+cloudy_park_4_s25 = "Cloudy Park 4 - Star 25"
+cloudy_park_4_s26 = "Cloudy Park 4 - Star 26"
+cloudy_park_4_s27 = "Cloudy Park 4 - Star 27"
+cloudy_park_4_s28 = "Cloudy Park 4 - Star 28"
+cloudy_park_4_s29 = "Cloudy Park 4 - Star 29"
+cloudy_park_4_s30 = "Cloudy Park 4 - Star 30"
+cloudy_park_4_s31 = "Cloudy Park 4 - Star 31"
+cloudy_park_4_s32 = "Cloudy Park 4 - Star 32"
+cloudy_park_4_s33 = "Cloudy Park 4 - Star 33"
+cloudy_park_4_s34 = "Cloudy Park 4 - Star 34"
+cloudy_park_4_s35 = "Cloudy Park 4 - Star 35"
+cloudy_park_4_s36 = "Cloudy Park 4 - Star 36"
+cloudy_park_4_s37 = "Cloudy Park 4 - Star 37"
+cloudy_park_4_s38 = "Cloudy Park 4 - Star 38"
+cloudy_park_4_s39 = "Cloudy Park 4 - Star 39"
+cloudy_park_4_s40 = "Cloudy Park 4 - Star 40"
+cloudy_park_4_s41 = "Cloudy Park 4 - Star 41"
+cloudy_park_4_s42 = "Cloudy Park 4 - Star 42"
+cloudy_park_4_s43 = "Cloudy Park 4 - Star 43"
+cloudy_park_4_s44 = "Cloudy Park 4 - Star 44"
+cloudy_park_4_s45 = "Cloudy Park 4 - Star 45"
+cloudy_park_4_s46 = "Cloudy Park 4 - Star 46"
+cloudy_park_4_s47 = "Cloudy Park 4 - Star 47"
+cloudy_park_4_s48 = "Cloudy Park 4 - Star 48"
+cloudy_park_4_s49 = "Cloudy Park 4 - Star 49"
+cloudy_park_4_s50 = "Cloudy Park 4 - Star 50"
+cloudy_park_5_s1 = "Cloudy Park 5 - Star 1"
+cloudy_park_5_s2 = "Cloudy Park 5 - Star 2"
+cloudy_park_5_s3 = "Cloudy Park 5 - Star 3"
+cloudy_park_5_s4 = "Cloudy Park 5 - Star 4"
+cloudy_park_5_s5 = "Cloudy Park 5 - Star 5"
+cloudy_park_5_s6 = "Cloudy Park 5 - Star 6"
+cloudy_park_6_s1 = "Cloudy Park 6 - Star 1"
+cloudy_park_6_s2 = "Cloudy Park 6 - Star 2"
+cloudy_park_6_s3 = "Cloudy Park 6 - Star 3"
+cloudy_park_6_s4 = "Cloudy Park 6 - Star 4"
+cloudy_park_6_s5 = "Cloudy Park 6 - Star 5"
+cloudy_park_6_s6 = "Cloudy Park 6 - Star 6"
+cloudy_park_6_s7 = "Cloudy Park 6 - Star 7"
+cloudy_park_6_s8 = "Cloudy Park 6 - Star 8"
+cloudy_park_6_s9 = "Cloudy Park 6 - Star 9"
+cloudy_park_6_s10 = "Cloudy Park 6 - Star 10"
+cloudy_park_6_s11 = "Cloudy Park 6 - Star 11"
+cloudy_park_6_s12 = "Cloudy Park 6 - Star 12"
+cloudy_park_6_s13 = "Cloudy Park 6 - Star 13"
+cloudy_park_6_s14 = "Cloudy Park 6 - Star 14"
+cloudy_park_6_s15 = "Cloudy Park 6 - Star 15"
+cloudy_park_6_s16 = "Cloudy Park 6 - Star 16"
+cloudy_park_6_s17 = "Cloudy Park 6 - Star 17"
+cloudy_park_6_s18 = "Cloudy Park 6 - Star 18"
+cloudy_park_6_s19 = "Cloudy Park 6 - Star 19"
+cloudy_park_6_s20 = "Cloudy Park 6 - Star 20"
+cloudy_park_6_s21 = "Cloudy Park 6 - Star 21"
+cloudy_park_6_s22 = "Cloudy Park 6 - Star 22"
+cloudy_park_6_s23 = "Cloudy Park 6 - Star 23"
+cloudy_park_6_s24 = "Cloudy Park 6 - Star 24"
+cloudy_park_6_s25 = "Cloudy Park 6 - Star 25"
+cloudy_park_6_s26 = "Cloudy Park 6 - Star 26"
+cloudy_park_6_s27 = "Cloudy Park 6 - Star 27"
+cloudy_park_6_s28 = "Cloudy Park 6 - Star 28"
+cloudy_park_6_s29 = "Cloudy Park 6 - Star 29"
+cloudy_park_6_s30 = "Cloudy Park 6 - Star 30"
+cloudy_park_6_s31 = "Cloudy Park 6 - Star 31"
+cloudy_park_6_s32 = "Cloudy Park 6 - Star 32"
+cloudy_park_6_s33 = "Cloudy Park 6 - Star 33"
+iceberg_1_s1 = "Iceberg 1 - Star 1"
+iceberg_1_s2 = "Iceberg 1 - Star 2"
+iceberg_1_s3 = "Iceberg 1 - Star 3"
+iceberg_1_s4 = "Iceberg 1 - Star 4"
+iceberg_1_s5 = "Iceberg 1 - Star 5"
+iceberg_1_s6 = "Iceberg 1 - Star 6"
+iceberg_2_s1 = "Iceberg 2 - Star 1"
+iceberg_2_s2 = "Iceberg 2 - Star 2"
+iceberg_2_s3 = "Iceberg 2 - Star 3"
+iceberg_2_s4 = "Iceberg 2 - Star 4"
+iceberg_2_s5 = "Iceberg 2 - Star 5"
+iceberg_2_s6 = "Iceberg 2 - Star 6"
+iceberg_2_s7 = "Iceberg 2 - Star 7"
+iceberg_2_s8 = "Iceberg 2 - Star 8"
+iceberg_2_s9 = "Iceberg 2 - Star 9"
+iceberg_2_s10 = "Iceberg 2 - Star 10"
+iceberg_2_s11 = "Iceberg 2 - Star 11"
+iceberg_2_s12 = "Iceberg 2 - Star 12"
+iceberg_2_s13 = "Iceberg 2 - Star 13"
+iceberg_2_s14 = "Iceberg 2 - Star 14"
+iceberg_2_s15 = "Iceberg 2 - Star 15"
+iceberg_2_s16 = "Iceberg 2 - Star 16"
+iceberg_2_s17 = "Iceberg 2 - Star 17"
+iceberg_2_s18 = "Iceberg 2 - Star 18"
+iceberg_2_s19 = "Iceberg 2 - Star 19"
+iceberg_3_s1 = "Iceberg 3 - Star 1"
+iceberg_3_s2 = "Iceberg 3 - Star 2"
+iceberg_3_s3 = "Iceberg 3 - Star 3"
+iceberg_3_s4 = "Iceberg 3 - Star 4"
+iceberg_3_s5 = "Iceberg 3 - Star 5"
+iceberg_3_s6 = "Iceberg 3 - Star 6"
+iceberg_3_s7 = "Iceberg 3 - Star 7"
+iceberg_3_s8 = "Iceberg 3 - Star 8"
+iceberg_3_s9 = "Iceberg 3 - Star 9"
+iceberg_3_s10 = "Iceberg 3 - Star 10"
+iceberg_3_s11 = "Iceberg 3 - Star 11"
+iceberg_3_s12 = "Iceberg 3 - Star 12"
+iceberg_3_s13 = "Iceberg 3 - Star 13"
+iceberg_3_s14 = "Iceberg 3 - Star 14"
+iceberg_3_s15 = "Iceberg 3 - Star 15"
+iceberg_3_s16 = "Iceberg 3 - Star 16"
+iceberg_3_s17 = "Iceberg 3 - Star 17"
+iceberg_3_s18 = "Iceberg 3 - Star 18"
+iceberg_3_s19 = "Iceberg 3 - Star 19"
+iceberg_3_s20 = "Iceberg 3 - Star 20"
+iceberg_3_s21 = "Iceberg 3 - Star 21"
+iceberg_4_s1 = "Iceberg 4 - Star 1"
+iceberg_4_s2 = "Iceberg 4 - Star 2"
+iceberg_4_s3 = "Iceberg 4 - Star 3"
+iceberg_5_s1 = "Iceberg 5 - Star 1"
+iceberg_5_s2 = "Iceberg 5 - Star 2"
+iceberg_5_s3 = "Iceberg 5 - Star 3"
+iceberg_5_s4 = "Iceberg 5 - Star 4"
+iceberg_5_s5 = "Iceberg 5 - Star 5"
+iceberg_5_s6 = "Iceberg 5 - Star 6"
+iceberg_5_s7 = "Iceberg 5 - Star 7"
+iceberg_5_s8 = "Iceberg 5 - Star 8"
+iceberg_5_s9 = "Iceberg 5 - Star 9"
+iceberg_5_s10 = "Iceberg 5 - Star 10"
+iceberg_5_s11 = "Iceberg 5 - Star 11"
+iceberg_5_s12 = "Iceberg 5 - Star 12"
+iceberg_5_s13 = "Iceberg 5 - Star 13"
+iceberg_5_s14 = "Iceberg 5 - Star 14"
+iceberg_5_s15 = "Iceberg 5 - Star 15"
+iceberg_5_s16 = "Iceberg 5 - Star 16"
+iceberg_5_s17 = "Iceberg 5 - Star 17"
+iceberg_5_s18 = "Iceberg 5 - Star 18"
+iceberg_5_s19 = "Iceberg 5 - Star 19"
+iceberg_5_s20 = "Iceberg 5 - Star 20"
+iceberg_5_s21 = "Iceberg 5 - Star 21"
+iceberg_5_s22 = "Iceberg 5 - Star 22"
+iceberg_5_s23 = "Iceberg 5 - Star 23"
+iceberg_5_s24 = "Iceberg 5 - Star 24"
+iceberg_5_s25 = "Iceberg 5 - Star 25"
+iceberg_5_s26 = "Iceberg 5 - Star 26"
+iceberg_5_s27 = "Iceberg 5 - Star 27"
+iceberg_5_s28 = "Iceberg 5 - Star 28"
+iceberg_5_s29 = "Iceberg 5 - Star 29"
+iceberg_5_s30 = "Iceberg 5 - Star 30"
+iceberg_5_s31 = "Iceberg 5 - Star 31"
+iceberg_5_s32 = "Iceberg 5 - Star 32"
+iceberg_5_s33 = "Iceberg 5 - Star 33"
+iceberg_5_s34 = "Iceberg 5 - Star 34"
+iceberg_6_s1 = "Iceberg 6 - Star 1"
diff --git a/worlds/kdl3/Names/__init__.py b/worlds/kdl3/Names/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/kdl3/Options.py b/worlds/kdl3/Options.py
new file mode 100644
index 000000000000..e0a4f12f15dc
--- /dev/null
+++ b/worlds/kdl3/Options.py
@@ -0,0 +1,437 @@
+import random
+from dataclasses import dataclass
+
+from Options import DeathLink, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \
+ PerGameCommonOptions, PlandoConnections
+from .Names import LocationName
+
+
+class KDL3PlandoConnections(PlandoConnections):
+ entrances = exits = {f"{i} {j}" for i in LocationName.level_names for j in range(1, 7)}
+
+
+class Goal(Choice):
+ """
+ Zero: collect the Heart Stars, and defeat Zero in the Hyper Zone.
+ Boss Butch: collect the Heart Stars, and then complete the boss rematches in the Boss Butch mode.
+ MG5: collect the Heart Stars, and then complete a perfect run through the minigame gauntlet within the Super MG5
+ Jumping: collect the Heart Stars, and then reach a designated score within the Jumping sub-game
+ """
+ display_name = "Goal"
+ option_zero = 0
+ option_boss_butch = 1
+ option_MG5 = 2
+ option_jumping = 3
+ default = 0
+
+ @classmethod
+ def get_option_name(cls, value: int) -> str:
+ if value == 2:
+ return cls.name_lookup[value].upper()
+ return super().get_option_name(value)
+
+class GoalSpeed(Choice):
+ """
+ Normal: the goal is unlocked after purifying the five bosses
+ Fast: the goal is unlocked after acquiring the target number of Heart Stars
+ """
+ display_name = "Goal Speed"
+ option_normal = 0
+ option_fast = 1
+
+
+class TotalHeartStars(Range):
+ """
+ Maximum number of heart stars to include in the pool of items.
+ """
+ display_name = "Max Heart Stars"
+ range_start = 5 # set to 5 so strict bosses does not degrade
+ range_end = 50 # 30 default locations + 30 stage clears + 5 bosses - 14 progression items = 51, so round down
+ default = 30
+
+
+class HeartStarsRequired(Range):
+ """
+ Percentage of heart stars required to purify the five bosses and reach Zero.
+ Each boss will require a differing amount of heart stars to purify.
+ """
+ display_name = "Required Heart Stars"
+ range_start = 1
+ range_end = 100
+ default = 50
+
+
+class LevelShuffle(Choice):
+ """
+ None: No stage shuffling.
+ Same World: shuffles stages around their world.
+ Pattern: shuffles stages according to the stage pattern (stage 3 will always be a minigame stage, etc.)
+ Shuffled: shuffles stages across all worlds.
+ """
+ display_name = "Stage Shuffle"
+ option_none = 0
+ option_same_world = 1
+ option_pattern = 2
+ option_shuffled = 3
+ default = 0
+
+
+class BossShuffle(PlandoBosses):
+ """
+ None: Bosses will remain in their vanilla locations
+ Shuffled: Bosses will be shuffled amongst each other
+ Full: Bosses will be randomized
+ Singularity: All (non-Zero) bosses will be replaced with a single boss
+ Supports plando placement.
+ """
+ bosses = frozenset(LocationName.boss_names.keys())
+
+ locations = frozenset(LocationName.level_names.keys())
+
+ duplicate_bosses = True
+
+ @classmethod
+ def can_place_boss(cls, boss: str, location: str) -> bool:
+ # Kirby has no logic about requiring bosses in specific locations (since we load in their stage)
+ return True
+
+ display_name = "Boss Shuffle"
+ option_none = 0
+ option_shuffled = 1
+ option_full = 2
+ option_singularity = 3
+
+
+class BossShuffleAllowBB(Choice):
+ """
+ Allow Boss Butch variants of bosses in Boss Shuffle.
+ Enabled: any boss placed will have a 50% chance of being the Boss Butch variant, including bosses not present
+ Enforced: all bosses will be their Boss Butch variant.
+ Boss Butch boss changes are only visual.
+ """
+ display_name = "Allow Boss Butch Bosses"
+ option_disabled = 0
+ option_enabled = 1
+ option_enforced = 2
+ default = 0
+
+
+class AnimalRandomization(Choice):
+ """
+ Disabled: all animal positions will be vanilla.
+ Shuffled: all animal positions will be shuffled amongst each other.
+ Full: random animals will be placed across the levels. At least one of each animal is guaranteed.
+ """
+ display_name = "Animal Randomization"
+ option_disabled = 0
+ option_shuffled = 1
+ option_full = 2
+ default = 0
+
+
+class CopyAbilityRandomization(Choice):
+ """
+ Disabled: enemies give regular copy abilities and health.
+ Enabled: all enemies will have the copy ability received from them randomized.
+ Enabled Plus Minus: enemies (except minibosses) can additionally give you anywhere from +2 health to -1 health when eaten.
+ """
+ display_name = "Copy Ability Randomization"
+ option_disabled = 0
+ option_enabled = 1
+ option_enabled_plus_minus = 2
+
+
+class StrictBosses(DefaultOnToggle):
+ """
+ If enabled, one will not be able to move onto the next world until the previous world's boss has been purified.
+ """
+ display_name = "Strict Bosses"
+
+
+class OpenWorld(DefaultOnToggle):
+ """
+ If enabled, all 6 stages will be unlocked upon entering a world for the first time. A certain amount of stages
+ will need to be completed in order to unlock the bosses
+ """
+ display_name = "Open World"
+
+
+class OpenWorldBossRequirement(Range):
+ """
+ The amount of stages completed needed to unlock the boss of a world when Open World is turned on.
+ """
+ display_name = "Open World Boss Requirement"
+ range_start = 1
+ range_end = 6
+ default = 3
+
+
+class BossRequirementRandom(Toggle):
+ """
+ If enabled, boss purification will require a random amount of Heart Stars. Depending on options, this may have
+ boss purification unlock in a random order.
+ """
+ display_name = "Randomize Purification Requirement"
+
+
+class JumpingTarget(Range):
+ """
+ The required score needed to complete the Jumping minigame.
+ """
+ display_name = "Jumping Target Score"
+ range_start = 1
+ range_end = 25
+ default = 10
+
+
+class GameLanguage(Choice):
+ """
+ The language that the game should display. This does not have to match the given rom.
+ """
+ display_name = "Game Language"
+ option_japanese = 0
+ option_english = 1
+ default = 1
+
+
+class FillerPercentage(Range):
+ """
+ Percentage of non-required Heart Stars to be converted to filler items (1-Ups, Maxim Tomatoes, Invincibility Candy).
+ """
+ display_name = "Filler Percentage"
+ range_start = 0
+ range_end = 100
+ default = 50
+
+
+class TrapPercentage(Range):
+ """
+ Percentage of filler items to be converted to trap items (Gooey Bags, Slowness, Eject Ability).
+ """
+ display_name = "Trap Percentage"
+ range_start = 0
+ range_end = 100
+ default = 50
+
+
+class GooeyTrapPercentage(Range):
+ """
+ Chance that any given trap is a Gooey Bag (spawns Gooey when you receive it).
+ """
+ display_name = "Gooey Trap Percentage"
+ range_start = 0
+ range_end = 100
+ default = 50
+
+
+class SlowTrapPercentage(Range):
+ """
+ Chance that any given trap is Slowness (halves your max speed for 15 seconds when you receive it).
+ """
+ display_name = "Slowness Trap Percentage"
+ range_start = 0
+ range_end = 100
+ default = 50
+
+
+class AbilityTrapPercentage(Range):
+ """
+ Chance that any given trap is an Eject Ability (ejects your ability when you receive it).
+ """
+ display_name = "Ability Trap Percentage"
+ range_start = 0
+ range_end = 100
+ default = 50
+
+
+class ConsumableChecks(Toggle):
+ """
+ When enabled, adds all 1-Ups and Maxim Tomatoes as possible locations.
+ """
+ display_name = "Consumable-sanity"
+
+
+class StarChecks(Toggle):
+ """
+ When enabled, every star in a given stage will become a check.
+ Will increase the possible filler pool to include 1/3/5 stars.
+ """
+ display_name = "Starsanity"
+
+
+class KirbyFlavorPreset(Choice):
+ """
+ The color of Kirby, from a list of presets.
+ """
+ display_name = "Kirby Flavor"
+ option_default = 0
+ option_bubblegum = 1
+ option_cherry = 2
+ option_blueberry = 3
+ option_lemon = 4
+ option_kiwi = 5
+ option_grape = 6
+ option_chocolate = 7
+ option_marshmallow = 8
+ option_licorice = 9
+ option_watermelon = 10
+ option_orange = 11
+ option_lime = 12
+ option_lavender = 13
+ option_custom = 14
+ default = 0
+
+ @classmethod
+ def from_text(cls, text: str) -> Choice:
+ text = text.lower()
+ if text == "random":
+ choice_list = list(cls.name_lookup)
+ choice_list.remove(14)
+ return cls(random.choice(choice_list))
+ return super().from_text(text)
+
+
+class KirbyFlavor(OptionDict):
+ """
+ A custom color for Kirby. To use a custom color, set the preset to Custom and then define a dict of keys from "1" to
+ "15", with their values being an HTML hex color.
+ """
+ default = {
+ "1": "B01810",
+ "2": "F0E0E8",
+ "3": "C8A0A8",
+ "4": "A87070",
+ "5": "E02018",
+ "6": "F0A0B8",
+ "7": "D07880",
+ "8": "A85048",
+ "9": "E8D0D0",
+ "10": "E85048",
+ "11": "D0C0C0",
+ "12": "B08888",
+ "13": "E87880",
+ "14": "F8F8F8",
+ "15": "B03830",
+ }
+
+
+class GooeyFlavorPreset(Choice):
+ """
+ The color of Gooey, from a list of presets.
+ """
+ display_name = "Gooey Flavor"
+ option_default = 0
+ option_bubblegum = 1
+ option_cherry = 2
+ option_blueberry = 3
+ option_lemon = 4
+ option_kiwi = 5
+ option_grape = 6
+ option_chocolate = 7
+ option_marshmallow = 8
+ option_licorice = 9
+ option_watermelon = 10
+ option_orange = 11
+ option_lime = 12
+ option_lavender = 13
+ option_custom = 14
+ default = 0
+
+ @classmethod
+ def from_text(cls, text: str) -> Choice:
+ text = text.lower()
+ if text == "random":
+ choice_list = list(cls.name_lookup)
+ choice_list.remove(14)
+ return cls(random.choice(choice_list))
+ return super().from_text(text)
+
+
+class GooeyFlavor(OptionDict):
+ """
+ A custom color for Gooey. To use a custom color, set the preset to Custom and then define a dict of keys from "1" to
+ "15", with their values being an HTML hex color.
+ """
+ default = {
+ "1": "000808",
+ "2": "102838",
+ "3": "183048",
+ "4": "183878",
+ "5": "1838A0",
+ "6": "B01810",
+ "7": "E85048",
+ "8": "D0C0C0",
+ "9": "F8F8F8",
+ }
+
+
+class MusicShuffle(Choice):
+ """
+ None: default music will play
+ Shuffled: music will be shuffled amongst each other
+ Full: random music will play in each room
+ Note that certain songs will not be chosen in shuffled or full
+ """
+ display_name = "Music Randomization"
+ option_none = 0
+ option_shuffled = 1
+ option_full = 2
+ default = 0
+
+
+class VirtualConsoleChanges(Choice):
+ """
+ Adds the ability to enable 2 of the Virtual Console changes.
+ Flash Reduction: reduces the flashing during the Zero battle.
+ Color Changes: changes the color of the background within the Zero Boss Butch rematch.
+ """
+ display_name = "Virtual Console Changes"
+ option_none = 0
+ option_flash_reduction = 1
+ option_color_changes = 2
+ option_both = 3
+ default = 1
+
+
+class Gifting(Toggle):
+ """
+ When enabled, the goal game item will be sent to other compatible games as a gift,
+ and you can receive gifts from other players. This can be enabled during gameplay
+ using the client.
+ """
+ display_name = "Gifting"
+
+
+@dataclass
+class KDL3Options(PerGameCommonOptions):
+ plando_connections: KDL3PlandoConnections
+ death_link: DeathLink
+ game_language: GameLanguage
+ goal: Goal
+ goal_speed: GoalSpeed
+ total_heart_stars: TotalHeartStars
+ heart_stars_required: HeartStarsRequired
+ filler_percentage: FillerPercentage
+ trap_percentage: TrapPercentage
+ gooey_trap_weight: GooeyTrapPercentage
+ slow_trap_weight: SlowTrapPercentage
+ ability_trap_weight: AbilityTrapPercentage
+ jumping_target: JumpingTarget
+ stage_shuffle: LevelShuffle
+ boss_shuffle: BossShuffle
+ allow_bb: BossShuffleAllowBB
+ animal_randomization: AnimalRandomization
+ copy_ability_randomization: CopyAbilityRandomization
+ strict_bosses: StrictBosses
+ open_world: OpenWorld
+ ow_boss_requirement: OpenWorldBossRequirement
+ boss_requirement_random: BossRequirementRandom
+ consumables: ConsumableChecks
+ starsanity: StarChecks
+ gifting: Gifting
+ kirby_flavor_preset: KirbyFlavorPreset
+ kirby_flavor: KirbyFlavor
+ gooey_flavor_preset: GooeyFlavorPreset
+ gooey_flavor: GooeyFlavor
+ music_shuffle: MusicShuffle
+ virtual_console: VirtualConsoleChanges
diff --git a/worlds/kdl3/Presets.py b/worlds/kdl3/Presets.py
new file mode 100644
index 000000000000..d3a7146ded5f
--- /dev/null
+++ b/worlds/kdl3/Presets.py
@@ -0,0 +1,56 @@
+from typing import Dict, Any
+
+all_random = {
+ "progression_balancing": "random",
+ "accessibility": "random",
+ "death_link": "random",
+ "game_language": "random",
+ "goal": "random",
+ "goal_speed": "random",
+ "total_heart_stars": "random",
+ "heart_stars_required": "random",
+ "filler_percentage": "random",
+ "trap_percentage": "random",
+ "gooey_trap_weight": "random",
+ "slow_trap_weight": "random",
+ "ability_trap_weight": "random",
+ "jumping_target": "random",
+ "stage_shuffle": "random",
+ "boss_shuffle": "random",
+ "allow_bb": "random",
+ "animal_randomization": "random",
+ "copy_ability_randomization": "random",
+ "strict_bosses": "random",
+ "open_world": "random",
+ "ow_boss_requirement": "random",
+ "boss_requirement_random": "random",
+ "consumables": "random",
+ "kirby_flavor_preset": "random",
+ "gooey_flavor_preset": "random",
+ "music_shuffle": "random",
+}
+
+beginner = {
+ "goal": "zero",
+ "goal_speed": "normal",
+ "total_heart_stars": 50,
+ "heart_stars_required": 30,
+ "filler_percentage": 25,
+ "trap_percentage": 0,
+ "gooey_trap_weight": "random",
+ "slow_trap_weight": "random",
+ "ability_trap_weight": "random",
+ "jumping_target": 5,
+ "stage_shuffle": "pattern",
+ "boss_shuffle": "shuffled",
+ "allow_bb": "random",
+ "strict_bosses": True,
+ "open_world": True,
+ "ow_boss_requirement": 3,
+}
+
+
+kdl3_options_presets: Dict[str, Dict[str, Any]] = {
+ "All Random": all_random,
+ "Beginner": beginner,
+}
diff --git a/worlds/kdl3/Regions.py b/worlds/kdl3/Regions.py
new file mode 100644
index 000000000000..407dcf9680f4
--- /dev/null
+++ b/worlds/kdl3/Regions.py
@@ -0,0 +1,242 @@
+import orjson
+import os
+from pkgutil import get_data
+
+from typing import TYPE_CHECKING, List, Dict, Optional, Union
+from BaseClasses import Region
+from worlds.generic.Rules import add_item_rule
+from .Locations import KDL3Location
+from .Names import LocationName
+from .Options import BossShuffle
+from .Room import KDL3Room
+
+if TYPE_CHECKING:
+ from . import KDL3World
+
+default_levels = {
+ 1: [0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770006, 0x770200],
+ 2: [0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x77000C, 0x770201],
+ 3: [0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770012, 0x770202],
+ 4: [0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770018, 0x770203],
+ 5: [0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x77001E, 0x770204],
+}
+
+first_stage_blacklist = {
+ # We want to confirm that the first stage can be completed without any items
+ 0x77000B, # 2-5 needs Kine
+ 0x770011, # 3-5 needs Cutter
+ 0x77001C, # 5-4 needs Burning
+}
+
+first_world_limit = {
+ # We need to limit the number of very restrictive stages in level 1 on solo gens
+ *first_stage_blacklist, # all three of the blacklist stages need 2+ items for both checks
+ 0x770007,
+ 0x770008,
+ 0x770013,
+ 0x77001E,
+
+}
+
+
+def generate_valid_level(world: "KDL3World", level: int, stage: int,
+ possible_stages: List[int], placed_stages: List[int]):
+ new_stage = world.random.choice(possible_stages)
+ if level == 1:
+ if stage == 0 and new_stage in first_stage_blacklist:
+ return generate_valid_level(world, level, stage, possible_stages, placed_stages)
+ elif (not (world.multiworld.players > 1 or world.options.consumables or world.options.starsanity) and
+ new_stage in first_world_limit and
+ sum(p_stage in first_world_limit for p_stage in placed_stages)
+ >= (2 if world.options.open_world else 1)):
+ return generate_valid_level(world, level, stage, possible_stages, placed_stages)
+ return new_stage
+
+
+def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]):
+ level_names = {LocationName.level_names[level]: level for level in LocationName.level_names}
+ room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json")))
+ rooms: Dict[str, KDL3Room] = dict()
+ for room_entry in room_data:
+ room = KDL3Room(room_entry["name"], world.player, world.multiworld, None, room_entry["level"],
+ room_entry["stage"], room_entry["room"], room_entry["pointer"], room_entry["music"],
+ room_entry["default_exits"], room_entry["animal_pointers"], room_entry["enemies"],
+ room_entry["entity_load"], room_entry["consumables"], room_entry["consumables_pointer"])
+ room.add_locations({location: world.location_name_to_id[location] if location in world.location_name_to_id else
+ None for location in room_entry["locations"]
+ if (not any(x in location for x in ["1-Up", "Maxim"]) or
+ world.options.consumables.value) and ("Star" not in location
+ or world.options.starsanity.value)},
+ KDL3Location)
+ rooms[room.name] = room
+ for location in room.locations:
+ if "Animal" in location.name:
+ add_item_rule(location, lambda item: item.name in {
+ "Rick Spawn", "Kine Spawn", "Coo Spawn", "Nago Spawn", "ChuChu Spawn", "Pitch Spawn"
+ })
+ world.rooms = list(rooms.values())
+ world.multiworld.regions.extend(world.rooms)
+
+ first_rooms: Dict[int, KDL3Room] = dict()
+ for name, room in rooms.items():
+ if room.room == 0:
+ if room.stage == 7:
+ first_rooms[0x770200 + room.level - 1] = room
+ else:
+ first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room
+ exits = dict()
+ for def_exit in room.default_exits:
+ target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}"
+ access_rule = tuple(def_exit["access_rule"])
+ exits[target] = lambda state, rule=access_rule: state.has_all(rule, world.player)
+ room.add_exits(
+ exits.keys(),
+ exits
+ )
+ if world.options.open_world:
+ if any("Complete" in location.name for location in room.locations):
+ room.add_locations({f"{level_names[room.level]} {room.stage} - Stage Completion": None},
+ KDL3Location)
+
+ for level in world.player_levels:
+ for stage in range(6):
+ proper_stage = world.player_levels[level][stage]
+ stage_name = world.multiworld.get_location(world.location_id_to_name[proper_stage],
+ world.player).name.replace(" - Complete", "")
+ stage_regions = [rooms[room] for room in rooms if stage_name in rooms[room].name]
+ for region in stage_regions:
+ region.level = level
+ region.stage = stage
+ if world.options.open_world or stage == 0:
+ level_regions[level].add_exits([first_rooms[proper_stage].name])
+ else:
+ world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][stage - 1]],
+ world.player).parent_region.add_exits([first_rooms[proper_stage].name])
+ if world.options.open_world:
+ level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name])
+ else:
+ world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player)\
+ .parent_region.add_exits([first_rooms[0x770200 + level - 1].name])
+
+
+def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_pattern: bool) -> dict:
+ levels: Dict[int, List[Optional[int]]] = {
+ 1: [None] * 7,
+ 2: [None] * 7,
+ 3: [None] * 7,
+ 4: [None] * 7,
+ 5: [None] * 7,
+ }
+
+ possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)]
+ if world.options.plando_connections:
+ for connection in world.options.plando_connections:
+ try:
+ entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1)
+ stage_world, stage_stage = connection.exit.rsplit(" ", 1)
+ new_stage = default_levels[LocationName.level_names[stage_world.strip()]][int(stage_stage) - 1]
+ levels[LocationName.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage
+ possible_stages.remove(new_stage)
+
+ except Exception:
+ raise Exception(
+ f"Invalid connection: {connection.entrance} =>"
+ f" {connection.exit} for player {world.player} ({world.player_name})")
+
+ for level in range(1, 6):
+ for stage in range(6):
+ # Randomize bosses separately
+ try:
+ if levels[level][stage] is None:
+ stage_candidates = [candidate for candidate in possible_stages
+ if (enforce_world and candidate in default_levels[level])
+ or (enforce_pattern and ((candidate - 1) & 0x00FFFF) % 6 == stage)
+ or (enforce_pattern == enforce_world)
+ ]
+ new_stage = generate_valid_level(world, level, stage, stage_candidates, levels[level])
+ possible_stages.remove(new_stage)
+ levels[level][stage] = new_stage
+ except Exception:
+ raise Exception(f"Failed to find valid stage for {level}-{stage}. Remaining Stages:{possible_stages}")
+
+ # now handle bosses
+ boss_shuffle: Union[int, str] = world.options.boss_shuffle.value
+ plando_bosses = []
+ if isinstance(boss_shuffle, str):
+ # boss plando
+ options = boss_shuffle.split(";")
+ boss_shuffle = BossShuffle.options[options.pop()]
+ for option in options:
+ if "-" in option:
+ loc, boss = option.split("-")
+ loc = loc.title()
+ boss = boss.title()
+ levels[LocationName.level_names[loc]][6] = LocationName.boss_names[boss]
+ plando_bosses.append(LocationName.boss_names[boss])
+ else:
+ option = option.title()
+ for level in levels:
+ if levels[level][6] is None:
+ levels[level][6] = LocationName.boss_names[option]
+ plando_bosses.append(LocationName.boss_names[option])
+
+ if boss_shuffle > 0:
+ if boss_shuffle == BossShuffle.option_full:
+ possible_bosses = [default_levels[world.random.randint(1, 5)][6]
+ for _ in range(5 - len(plando_bosses))]
+ elif boss_shuffle == BossShuffle.option_singularity:
+ boss = world.random.randint(1, 5)
+ possible_bosses = [default_levels[boss][6] for _ in range(5 - len(plando_bosses))]
+ else:
+ possible_bosses = [default_levels[level][6] for level in default_levels
+ if default_levels[level][6] not in plando_bosses]
+ for level in levels:
+ if levels[level][6] is None:
+ boss = world.random.choice(possible_bosses)
+ levels[level][6] = boss
+ possible_bosses.remove(boss)
+ else:
+ for level in levels:
+ if levels[level][6] is None:
+ levels[level][6] = default_levels[level][6]
+
+ for level in levels:
+ for stage in range(7):
+ assert levels[level][stage] is not None, "Level tried to be sent with a None stage, incorrect plando?"
+
+ return levels
+
+
+def create_levels(world: "KDL3World") -> None:
+ menu = Region("Menu", world.player, world.multiworld)
+ level1 = Region("Grass Land", world.player, world.multiworld)
+ level2 = Region("Ripple Field", world.player, world.multiworld)
+ level3 = Region("Sand Canyon", world.player, world.multiworld)
+ level4 = Region("Cloudy Park", world.player, world.multiworld)
+ level5 = Region("Iceberg", world.player, world.multiworld)
+ level6 = Region("Hyper Zone", world.player, world.multiworld)
+ levels = {
+ 1: level1,
+ 2: level2,
+ 3: level3,
+ 4: level4,
+ 5: level5,
+ }
+ level_shuffle = world.options.stage_shuffle.value
+ if level_shuffle != 0:
+ world.player_levels = generate_valid_levels(
+ world,
+ level_shuffle == 1,
+ level_shuffle == 2)
+
+ generate_rooms(world, levels)
+
+ level6.add_locations({LocationName.goals[world.options.goal]: None}, KDL3Location)
+
+ menu.connect(level1, "Start Game")
+ level1.connect(level2, "To Level 2")
+ level2.connect(level3, "To Level 3")
+ level3.connect(level4, "To Level 4")
+ level4.connect(level5, "To Level 5")
+ menu.connect(level6, "To Level 6") # put the connection on menu, since you can reach it before level 5 on fast goal
+ world.multiworld.regions += [menu, level1, level2, level3, level4, level5, level6]
diff --git a/worlds/kdl3/Rom.py b/worlds/kdl3/Rom.py
new file mode 100644
index 000000000000..5a846ab8be5e
--- /dev/null
+++ b/worlds/kdl3/Rom.py
@@ -0,0 +1,577 @@
+import typing
+from pkgutil import get_data
+
+import Utils
+from typing import Optional, TYPE_CHECKING
+import hashlib
+import os
+import struct
+
+import settings
+from worlds.Files import APDeltaPatch
+from .Aesthetics import get_palette_bytes, kirby_target_palettes, get_kirby_palette, gooey_target_palettes, \
+ get_gooey_palette
+from .Compression import hal_decompress
+import bsdiff4
+
+if TYPE_CHECKING:
+ from . import KDL3World
+
+KDL3UHASH = "201e7658f6194458a3869dde36bf8ec2"
+KDL3JHASH = "b2f2d004ea640c3db66df958fce122b2"
+
+level_pointers = {
+ 0x770001: 0x0084,
+ 0x770002: 0x009C,
+ 0x770003: 0x00B8,
+ 0x770004: 0x00D8,
+ 0x770005: 0x0104,
+ 0x770006: 0x0124,
+ 0x770007: 0x014C,
+ 0x770008: 0x0170,
+ 0x770009: 0x0190,
+ 0x77000A: 0x01B0,
+ 0x77000B: 0x01E8,
+ 0x77000C: 0x0218,
+ 0x77000D: 0x024C,
+ 0x77000E: 0x0270,
+ 0x77000F: 0x02A0,
+ 0x770010: 0x02C4,
+ 0x770011: 0x02EC,
+ 0x770012: 0x0314,
+ 0x770013: 0x03CC,
+ 0x770014: 0x0404,
+ 0x770015: 0x042C,
+ 0x770016: 0x044C,
+ 0x770017: 0x0478,
+ 0x770018: 0x049C,
+ 0x770019: 0x04E4,
+ 0x77001A: 0x0504,
+ 0x77001B: 0x0530,
+ 0x77001C: 0x0554,
+ 0x77001D: 0x05A8,
+ 0x77001E: 0x0640,
+ 0x770200: 0x0148,
+ 0x770201: 0x0248,
+ 0x770202: 0x03C8,
+ 0x770203: 0x04E0,
+ 0x770204: 0x06A4,
+ 0x770205: 0x06A8,
+}
+
+bb_bosses = {
+ 0x770200: 0xED85F1,
+ 0x770201: 0xF01360,
+ 0x770202: 0xEDA3DF,
+ 0x770203: 0xEDC2B9,
+ 0x770204: 0xED7C3F,
+ 0x770205: 0xEC29D2,
+}
+
+level_sprites = {
+ 0x19B2C6: 1827,
+ 0x1A195C: 1584,
+ 0x19F6F3: 1679,
+ 0x19DC8B: 1717,
+ 0x197900: 1872
+}
+
+stage_tiles = {
+ 0: [
+ 0, 1, 2,
+ 16, 17, 18,
+ 32, 33, 34,
+ 48, 49, 50
+ ],
+ 1: [
+ 3, 4, 5,
+ 19, 20, 21,
+ 35, 36, 37,
+ 51, 52, 53
+ ],
+ 2: [
+ 6, 7, 8,
+ 22, 23, 24,
+ 38, 39, 40,
+ 54, 55, 56
+ ],
+ 3: [
+ 9, 10, 11,
+ 25, 26, 27,
+ 41, 42, 43,
+ 57, 58, 59,
+ ],
+ 4: [
+ 12, 13, 64,
+ 28, 29, 65,
+ 44, 45, 66,
+ 60, 61, 67
+ ],
+ 5: [
+ 14, 15, 68,
+ 30, 31, 69,
+ 46, 47, 70,
+ 62, 63, 71
+ ]
+}
+
+heart_star_address = 0x2D0000
+heart_star_size = 456
+consumable_address = 0x2F91DD
+consumable_size = 698
+
+stage_palettes = [0x60964, 0x60B64, 0x60D64, 0x60F64, 0x61164]
+
+music_choices = [
+ 2, # Boss 1
+ 3, # Boss 2 (Unused)
+ 4, # Boss 3 (Miniboss)
+ 7, # Dedede
+ 9, # Event 2 (used once)
+ 10, # Field 1
+ 11, # Field 2
+ 12, # Field 3
+ 13, # Field 4
+ 14, # Field 5
+ 15, # Field 6
+ 16, # Field 7
+ 17, # Field 8
+ 18, # Field 9
+ 19, # Field 10
+ 20, # Field 11
+ 21, # Field 12 (Gourmet Race)
+ 23, # Dark Matter in the Hyper Zone
+ 24, # Zero
+ 25, # Level 1
+ 26, # Level 2
+ 27, # Level 4
+ 28, # Level 3
+ 29, # Heart Star Failed
+ 30, # Level 5
+ 31, # Minigame
+ 38, # Animal Friend 1
+ 39, # Animal Friend 2
+ 40, # Animal Friend 3
+]
+# extra room pointers we don't want to track other than for music
+room_pointers = [
+ 3079990, # Zero
+ 2983409, # BB Whispy
+ 3150688, # BB Acro
+ 2991071, # BB PonCon
+ 2998969, # BB Ado
+ 2980927, # BB Dedede
+ 2894290 # BB Zero
+]
+
+enemy_remap = {
+ "Waddle Dee": 0,
+ "Bronto Burt": 2,
+ "Rocky": 3,
+ "Bobo": 5,
+ "Chilly": 6,
+ "Poppy Bros Jr.": 7,
+ "Sparky": 8,
+ "Polof": 9,
+ "Broom Hatter": 11,
+ "Cappy": 12,
+ "Bouncy": 13,
+ "Nruff": 15,
+ "Glunk": 16,
+ "Togezo": 18,
+ "Kabu": 19,
+ "Mony": 20,
+ "Blipper": 21,
+ "Squishy": 22,
+ "Gabon": 24,
+ "Oro": 25,
+ "Galbo": 26,
+ "Sir Kibble": 27,
+ "Nidoo": 28,
+ "Kany": 29,
+ "Sasuke": 30,
+ "Yaban": 32,
+ "Boten": 33,
+ "Coconut": 34,
+ "Doka": 35,
+ "Icicle": 36,
+ "Pteran": 39,
+ "Loud": 40,
+ "Como": 41,
+ "Klinko": 42,
+ "Babut": 43,
+ "Wappa": 44,
+ "Mariel": 45,
+ "Tick": 48,
+ "Apolo": 49,
+ "Popon Ball": 50,
+ "KeKe": 51,
+ "Magoo": 53,
+ "Raft Waddle Dee": 57,
+ "Madoo": 58,
+ "Corori": 60,
+ "Kapar": 67,
+ "Batamon": 68,
+ "Peran": 72,
+ "Bobin": 73,
+ "Mopoo": 74,
+ "Gansan": 75,
+ "Bukiset (Burning)": 76,
+ "Bukiset (Stone)": 77,
+ "Bukiset (Ice)": 78,
+ "Bukiset (Needle)": 79,
+ "Bukiset (Clean)": 80,
+ "Bukiset (Parasol)": 81,
+ "Bukiset (Spark)": 82,
+ "Bukiset (Cutter)": 83,
+ "Waddle Dee Drawing": 84,
+ "Bronto Burt Drawing": 85,
+ "Bouncy Drawing": 86,
+ "Kabu (Dekabu)": 87,
+ "Wapod": 88,
+ "Propeller": 89,
+ "Dogon": 90,
+ "Joe": 91
+}
+
+miniboss_remap = {
+ "Captain Stitch": 0,
+ "Yuki": 1,
+ "Blocky": 2,
+ "Jumper Shoot": 3,
+ "Boboo": 4,
+ "Haboki": 5
+}
+
+ability_remap = {
+ "No Ability": 0,
+ "Burning Ability": 1,
+ "Stone Ability": 2,
+ "Ice Ability": 3,
+ "Needle Ability": 4,
+ "Clean Ability": 5,
+ "Parasol Ability": 6,
+ "Spark Ability": 7,
+ "Cutter Ability": 8,
+}
+
+
+class RomData:
+ def __init__(self, file: str, name: typing.Optional[str] = None):
+ self.file = bytearray()
+ self.read_from_file(file)
+ self.name = name
+
+ def read_byte(self, offset: int):
+ return self.file[offset]
+
+ def read_bytes(self, offset: int, length: int):
+ return self.file[offset:offset + length]
+
+ def write_byte(self, offset: int, value: int):
+ self.file[offset] = value
+
+ def write_bytes(self, offset: int, values: typing.Sequence) -> None:
+ self.file[offset:offset + len(values)] = values
+
+ def write_to_file(self, file: str):
+ with open(file, 'wb') as outfile:
+ outfile.write(self.file)
+
+ def read_from_file(self, file: str):
+ with open(file, 'rb') as stream:
+ self.file = bytearray(stream.read())
+
+ def apply_patch(self, patch: bytes):
+ self.file = bytearray(bsdiff4.patch(bytes(self.file), patch))
+
+ def write_crc(self):
+ crc = (sum(self.file[:0x7FDC] + self.file[0x7FE0:]) + 0x01FE) & 0xFFFF
+ inv = crc ^ 0xFFFF
+ self.write_bytes(0x7FDC, [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF])
+
+
+def handle_level_sprites(stages, sprites, palettes):
+ palette_by_level = list()
+ for palette in palettes:
+ palette_by_level.extend(palette[10:16])
+ for i in range(5):
+ for j in range(6):
+ palettes[i][10 + j] = palette_by_level[stages[i][j] - 1]
+ palettes[i] = [x for palette in palettes[i] for x in palette]
+ tiles_by_level = list()
+ for spritesheet in sprites:
+ decompressed = hal_decompress(spritesheet)
+ tiles = [decompressed[i:i + 32] for i in range(0, 2304, 32)]
+ tiles_by_level.extend([[tiles[x] for x in stage_tiles[stage]] for stage in stage_tiles])
+ for world in range(5):
+ levels = [stages[world][x] - 1 for x in range(6)]
+ world_tiles: typing.List[typing.Optional[bytes]] = [None for _ in range(72)]
+ for i in range(6):
+ for x in range(12):
+ world_tiles[stage_tiles[i][x]] = tiles_by_level[levels[i]][x]
+ sprites[world] = list()
+ for tile in world_tiles:
+ sprites[world].extend(tile)
+ # insert our fake compression
+ sprites[world][0:0] = [0xe3, 0xff]
+ sprites[world][1026:1026] = [0xe3, 0xff]
+ sprites[world][2052:2052] = [0xe0, 0xff]
+ sprites[world].append(0xff)
+ return sprites, palettes
+
+
+def write_heart_star_sprites(rom: RomData):
+ compressed = rom.read_bytes(heart_star_address, heart_star_size)
+ decompressed = hal_decompress(compressed)
+ patch = get_data(__name__, os.path.join("data", "APHeartStar.bsdiff4"))
+ patched = bytearray(bsdiff4.patch(decompressed, patch))
+ rom.write_bytes(0x1AF7DF, patched)
+ patched[0:0] = [0xE3, 0xFF]
+ patched.append(0xFF)
+ rom.write_bytes(0x1CD000, patched)
+ rom.write_bytes(0x3F0EBF, [0x00, 0xD0, 0x39])
+
+
+def write_consumable_sprites(rom: RomData, consumables: bool, stars: bool):
+ compressed = rom.read_bytes(consumable_address, consumable_size)
+ decompressed = hal_decompress(compressed)
+ patched = bytearray(decompressed)
+ if consumables:
+ patch = get_data(__name__, os.path.join("data", "APConsumable.bsdiff4"))
+ patched = bytearray(bsdiff4.patch(bytes(patched), patch))
+ if stars:
+ patch = get_data(__name__, os.path.join("data", "APStars.bsdiff4"))
+ patched = bytearray(bsdiff4.patch(bytes(patched), patch))
+ patched[0:0] = [0xE3, 0xFF]
+ patched.append(0xFF)
+ rom.write_bytes(0x1CD500, patched)
+ rom.write_bytes(0x3F0DAE, [0x00, 0xD5, 0x39])
+
+
+class KDL3DeltaPatch(APDeltaPatch):
+ hash = [KDL3UHASH, KDL3JHASH]
+ game = "Kirby's Dream Land 3"
+ patch_file_ending = ".apkdl3"
+
+ @classmethod
+ def get_source_data(cls) -> bytes:
+ return get_base_rom_bytes()
+
+ def patch(self, target: str):
+ super().patch(target)
+ rom = RomData(target)
+ target_language = rom.read_byte(0x3C020)
+ rom.write_byte(0x7FD9, target_language)
+ write_heart_star_sprites(rom)
+ if rom.read_bytes(0x3D014, 1)[0] > 0:
+ stages = [struct.unpack("HHHHHHH", rom.read_bytes(0x3D020 + x * 14, 14)) for x in range(5)]
+ palettes = [rom.read_bytes(full_pal, 512) for full_pal in stage_palettes]
+ palettes = [[palette[i:i + 32] for i in range(0, 512, 32)] for palette in palettes]
+ sprites = [rom.read_bytes(offset, level_sprites[offset]) for offset in level_sprites]
+ sprites, palettes = handle_level_sprites(stages, sprites, palettes)
+ for addr, palette in zip(stage_palettes, palettes):
+ rom.write_bytes(addr, palette)
+ for addr, level_sprite in zip([0x1CA000, 0x1CA920, 0x1CB230, 0x1CBB40, 0x1CC450], sprites):
+ rom.write_bytes(addr, level_sprite)
+ rom.write_bytes(0x460A, [0x00, 0xA0, 0x39, 0x20, 0xA9, 0x39, 0x30, 0xB2, 0x39, 0x40, 0xBB, 0x39,
+ 0x50, 0xC4, 0x39])
+ write_consumable_sprites(rom, rom.read_byte(0x3D018) > 0, rom.read_byte(0x3D01A) > 0)
+ rom_name = rom.read_bytes(0x3C000, 21)
+ rom.write_bytes(0x7FC0, rom_name)
+ rom.write_crc()
+ rom.write_to_file(target)
+
+
+def patch_rom(world: "KDL3World", rom: RomData):
+ rom.apply_patch(get_data(__name__, os.path.join("data", "kdl3_basepatch.bsdiff4")))
+ tiles = get_data(__name__, os.path.join("data", "APPauseIcons.dat"))
+ rom.write_bytes(0x3F000, tiles)
+
+ # Write open world patch
+ if world.options.open_world:
+ rom.write_bytes(0x143C7, [0xAD, 0xC1, 0x5A, 0xCD, 0xC1, 0x5A, ])
+ # changes the stage flag function to compare $5AC1 to $5AC1,
+ # always running the "new stage" function
+ # This has further checks present for bosses already, so we just
+ # need to handle regular stages
+ # write check for boss to be unlocked
+
+ if world.options.consumables:
+ # reroute maxim tomatoes to use the 1-UP function, then null out the function
+ rom.write_bytes(0x3002F, [0x37, 0x00])
+ rom.write_bytes(0x30037, [0xA9, 0x26, 0x00, # LDA #$0026
+ 0x22, 0x27, 0xD9, 0x00, # JSL $00D927
+ 0xA4, 0xD2, # LDY $D2
+ 0x6B, # RTL
+ 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, # NOP #10
+ ])
+
+ # stars handling is built into the rom, so no changes there
+
+ rooms = world.rooms
+ if world.options.music_shuffle > 0:
+ if world.options.music_shuffle == 1:
+ shuffled_music = music_choices.copy()
+ world.random.shuffle(shuffled_music)
+ music_map = dict(zip(music_choices, shuffled_music))
+ # Avoid putting star twinkle in the pool
+ music_map[5] = world.random.choice(music_choices)
+ # Heart Star music doesn't work on regular stages
+ music_map[8] = world.random.choice(music_choices)
+ for room in rooms:
+ room.music = music_map[room.music]
+ for room in room_pointers:
+ old_music = rom.read_byte(room + 2)
+ rom.write_byte(room + 2, music_map[old_music])
+ for i in range(5):
+ # level themes
+ old_music = rom.read_byte(0x133F2 + i)
+ rom.write_byte(0x133F2 + i, music_map[old_music])
+ # Zero
+ rom.write_byte(0x9AE79, music_map[0x18])
+ # Heart Star success and fail
+ rom.write_byte(0x4A388, music_map[0x08])
+ rom.write_byte(0x4A38D, music_map[0x1D])
+ elif world.options.music_shuffle == 2:
+ for room in rooms:
+ room.music = world.random.choice(music_choices)
+ for room in room_pointers:
+ rom.write_byte(room + 2, world.random.choice(music_choices))
+ for i in range(5):
+ # level themes
+ rom.write_byte(0x133F2 + i, world.random.choice(music_choices))
+ # Zero
+ rom.write_byte(0x9AE79, world.random.choice(music_choices))
+ # Heart Star success and fail
+ rom.write_byte(0x4A388, world.random.choice(music_choices))
+ rom.write_byte(0x4A38D, world.random.choice(music_choices))
+
+ for room in rooms:
+ room.patch(rom)
+
+ if world.options.virtual_console in [1, 3]:
+ # Flash Reduction
+ rom.write_byte(0x9AE68, 0x10)
+ rom.write_bytes(0x9AE8E, [0x08, 0x00, 0x22, 0x5D, 0xF7, 0x00, 0xA2, 0x08, ])
+ rom.write_byte(0x9AEA1, 0x08)
+ rom.write_byte(0x9AEC9, 0x01)
+ rom.write_bytes(0x9AED2, [0xA9, 0x1F])
+ rom.write_byte(0x9AEE1, 0x08)
+
+ if world.options.virtual_console in [2, 3]:
+ # Hyper Zone BB colors
+ rom.write_bytes(0x2C5E16, [0xEE, 0x1B, 0x18, 0x5B, 0xD3, 0x4A, 0xF4, 0x3B, ])
+ rom.write_bytes(0x2C8217, [0xFF, 0x1E, ])
+
+ # boss requirements
+ rom.write_bytes(0x3D000, struct.pack("HHHHH", world.boss_requirements[0], world.boss_requirements[1],
+ world.boss_requirements[2], world.boss_requirements[3],
+ world.boss_requirements[4]))
+ rom.write_bytes(0x3D00A, struct.pack("H", world.required_heart_stars if world.options.goal_speed == 1 else 0xFFFF))
+ rom.write_byte(0x3D00C, world.options.goal_speed.value)
+ rom.write_byte(0x3D00E, world.options.open_world.value)
+ rom.write_byte(0x3D010, world.options.death_link.value)
+ rom.write_byte(0x3D012, world.options.goal.value)
+ rom.write_byte(0x3D014, world.options.stage_shuffle.value)
+ rom.write_byte(0x3D016, world.options.ow_boss_requirement.value)
+ rom.write_byte(0x3D018, world.options.consumables.value)
+ rom.write_byte(0x3D01A, world.options.starsanity.value)
+ rom.write_byte(0x3D01C, world.options.gifting.value if world.multiworld.players > 1 else 0)
+ rom.write_byte(0x3D01E, world.options.strict_bosses.value)
+ # don't write gifting for solo game, since there's no one to send anything to
+
+ for level in world.player_levels:
+ for i in range(len(world.player_levels[level])):
+ rom.write_bytes(0x3F002E + ((level - 1) * 14) + (i * 2),
+ struct.pack("H", level_pointers[world.player_levels[level][i]]))
+ rom.write_bytes(0x3D020 + (level - 1) * 14 + (i * 2),
+ struct.pack("H", world.player_levels[level][i] & 0x00FFFF))
+ if (i == 0) or (i > 0 and i % 6 != 0):
+ rom.write_bytes(0x3D080 + (level - 1) * 12 + (i * 2),
+ struct.pack("H", (world.player_levels[level][i] & 0x00FFFF) % 6))
+
+ for i in range(6):
+ if world.boss_butch_bosses[i]:
+ rom.write_bytes(0x3F0000 + (level_pointers[0x770200 + i]), struct.pack("I", bb_bosses[0x770200 + i]))
+
+ # copy ability shuffle
+ if world.options.copy_ability_randomization.value > 0:
+ for enemy in world.copy_abilities:
+ if enemy in miniboss_remap:
+ rom.write_bytes(0xB417E + (miniboss_remap[enemy] << 1),
+ struct.pack("H", ability_remap[world.copy_abilities[enemy]]))
+ else:
+ rom.write_bytes(0xB3CAC + (enemy_remap[enemy] << 1),
+ struct.pack("H", ability_remap[world.copy_abilities[enemy]]))
+ # following only needs done on non-door rando
+ # incredibly lucky this follows the same order (including 5E == star block)
+ rom.write_byte(0x2F77EA, 0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1))
+ rom.write_byte(0x2F7811, 0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1))
+ rom.write_byte(0x2F9BC4, 0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1))
+ rom.write_byte(0x2F9BEB, 0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1))
+ rom.write_byte(0x2FAC06, 0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1))
+ rom.write_byte(0x2FAC2D, 0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1))
+ rom.write_byte(0x2F9E7B, 0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1))
+ rom.write_byte(0x2F9EA2, 0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1))
+ rom.write_byte(0x2FA951, 0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1))
+ rom.write_byte(0x2FA978, 0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1))
+ rom.write_byte(0x2FA132, 0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1))
+ rom.write_byte(0x2FA159, 0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1))
+ rom.write_byte(0x2FA3E8, 0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1))
+ rom.write_byte(0x2FA40F, 0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1))
+ rom.write_byte(0x2F90E2, 0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1))
+ rom.write_byte(0x2F9109, 0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1))
+
+ if world.options.copy_ability_randomization == 2:
+ for enemy in enemy_remap:
+ # we just won't include it for minibosses
+ rom.write_bytes(0xB3E40 + (enemy_remap[enemy] << 1), struct.pack("h", world.random.randint(-1, 2)))
+
+ # write jumping goal
+ rom.write_bytes(0x94F8, struct.pack("H", world.options.jumping_target))
+ rom.write_bytes(0x944E, struct.pack("H", world.options.jumping_target))
+
+ from Utils import __version__
+ rom.name = bytearray(
+ f'KDL3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21]
+ rom.name.extend([0] * (21 - len(rom.name)))
+ rom.write_bytes(0x3C000, rom.name)
+ rom.write_byte(0x3C020, world.options.game_language.value)
+
+ # handle palette
+ if world.options.kirby_flavor_preset.value != 0:
+ for addr in kirby_target_palettes:
+ target = kirby_target_palettes[addr]
+ palette = get_kirby_palette(world)
+ rom.write_bytes(addr, get_palette_bytes(palette, target[0], target[1], target[2]))
+
+ if world.options.gooey_flavor_preset.value != 0:
+ for addr in gooey_target_palettes:
+ target = gooey_target_palettes[addr]
+ palette = get_gooey_palette(world)
+ rom.write_bytes(addr, get_palette_bytes(palette, target[0], target[1], target[2]))
+
+
+def get_base_rom_bytes() -> bytes:
+ rom_file: str = get_base_rom_path()
+ base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None)
+ if not base_rom_bytes:
+ base_rom_bytes = bytes(Utils.read_snes_rom(open(rom_file, "rb")))
+
+ basemd5 = hashlib.md5()
+ basemd5.update(base_rom_bytes)
+ if basemd5.hexdigest() not in {KDL3UHASH, KDL3JHASH}:
+ raise Exception("Supplied Base Rom does not match known MD5 for US or JP release. "
+ "Get the correct game and version, then dump it")
+ get_base_rom_bytes.base_rom_bytes = base_rom_bytes
+ return base_rom_bytes
+
+
+def get_base_rom_path(file_name: str = "") -> str:
+ options: settings.Settings = settings.get_settings()
+ if not file_name:
+ file_name = options["kdl3_options"]["rom_file"]
+ if not os.path.exists(file_name):
+ file_name = Utils.user_path(file_name)
+ return file_name
diff --git a/worlds/kdl3/Room.py b/worlds/kdl3/Room.py
new file mode 100644
index 000000000000..256955b924ab
--- /dev/null
+++ b/worlds/kdl3/Room.py
@@ -0,0 +1,95 @@
+import struct
+import typing
+from BaseClasses import Region, ItemClassification
+
+if typing.TYPE_CHECKING:
+ from .Rom import RomData
+
+animal_map = {
+ "Rick Spawn": 0,
+ "Kine Spawn": 1,
+ "Coo Spawn": 2,
+ "Nago Spawn": 3,
+ "ChuChu Spawn": 4,
+ "Pitch Spawn": 5
+}
+
+
+class KDL3Room(Region):
+ pointer: int = 0
+ level: int = 0
+ stage: int = 0
+ room: int = 0
+ music: int = 0
+ default_exits: typing.List[typing.Dict[str, typing.Union[int, typing.List[str]]]]
+ animal_pointers: typing.List[int]
+ enemies: typing.List[str]
+ entity_load: typing.List[typing.List[int]]
+ consumables: typing.List[typing.Dict[str, typing.Union[int, str]]]
+
+ def __init__(self, name, player, multiworld, hint, level, stage, room, pointer, music, default_exits,
+ animal_pointers, enemies, entity_load, consumables, consumable_pointer):
+ super().__init__(name, player, multiworld, hint)
+ self.level = level
+ self.stage = stage
+ self.room = room
+ self.pointer = pointer
+ self.music = music
+ self.default_exits = default_exits
+ self.animal_pointers = animal_pointers
+ self.enemies = enemies
+ self.entity_load = entity_load
+ self.consumables = consumables
+ self.consumable_pointer = consumable_pointer
+
+ def patch(self, rom: "RomData"):
+ rom.write_byte(self.pointer + 2, self.music)
+ animals = [x.item.name for x in self.locations if "Animal" in x.name]
+ if len(animals) > 0:
+ for current_animal, address in zip(animals, self.animal_pointers):
+ rom.write_byte(self.pointer + address + 7, animal_map[current_animal])
+ if self.multiworld.worlds[self.player].options.consumables:
+ load_len = len(self.entity_load)
+ for consumable in self.consumables:
+ location = next(x for x in self.locations if x.name == consumable["name"])
+ assert location.item
+ is_progression = location.item.classification & ItemClassification.progression
+ if load_len == 8:
+ # edge case, there is exactly 1 room with 8 entities and only 1 consumable among them
+ if not (any(x in self.entity_load for x in [[0, 22], [1, 22]])
+ and any(x in self.entity_load for x in [[2, 22], [3, 22]])):
+ replacement_target = self.entity_load.index(
+ next(x for x in self.entity_load if x in [[0, 22], [1, 22], [2, 22], [3, 22]]))
+ if is_progression:
+ vtype = 0
+ else:
+ vtype = 2
+ rom.write_byte(self.pointer + 88 + (replacement_target * 2), vtype)
+ self.entity_load[replacement_target] = [vtype, 22]
+ else:
+ if is_progression:
+ # we need to see if 1-ups are in our load list
+ if any(x not in self.entity_load for x in [[0, 22], [1, 22]]):
+ self.entity_load.append([0, 22])
+ else:
+ if any(x not in self.entity_load for x in [[2, 22], [3, 22]]):
+ # edge case: if (1, 22) is in, we need to load (3, 22) instead
+ if [1, 22] in self.entity_load:
+ self.entity_load.append([3, 22])
+ else:
+ self.entity_load.append([2, 22])
+ if load_len < len(self.entity_load):
+ rom.write_bytes(self.pointer + 88 + (load_len * 2), bytes(self.entity_load[load_len]))
+ rom.write_bytes(self.pointer + 104 + (load_len * 2),
+ bytes(struct.pack("H", self.consumable_pointer)))
+ if is_progression:
+ if [1, 22] in self.entity_load:
+ vtype = 1
+ else:
+ vtype = 0
+ else:
+ if [3, 22] in self.entity_load:
+ vtype = 3
+ else:
+ vtype = 2
+ rom.write_byte(self.pointer + consumable["pointer"] + 7, vtype)
diff --git a/worlds/kdl3/Rules.py b/worlds/kdl3/Rules.py
new file mode 100644
index 000000000000..6a85ef84f054
--- /dev/null
+++ b/worlds/kdl3/Rules.py
@@ -0,0 +1,340 @@
+from worlds.generic.Rules import set_rule, add_rule
+from .Names import LocationName, EnemyAbilities
+from .Locations import location_table
+from .Options import GoalSpeed
+import typing
+
+if typing.TYPE_CHECKING:
+ from . import KDL3World
+ from BaseClasses import CollectionState
+
+
+def can_reach_boss(state: "CollectionState", player: int, level: int, open_world: int,
+ ow_boss_req: int, player_levels: typing.Dict[int, typing.Dict[int, int]]):
+ if open_world:
+ return state.has(f"{LocationName.level_names_inverse[level]} - Stage Completion", player, ow_boss_req)
+ else:
+ return state.can_reach(location_table[player_levels[level][5]], "Location", player)
+
+
+def can_reach_rick(state: "CollectionState", player: int) -> bool:
+ return state.has("Rick", player) and state.has("Rick Spawn", player)
+
+
+def can_reach_kine(state: "CollectionState", player: int) -> bool:
+ return state.has("Kine", player) and state.has("Kine Spawn", player)
+
+
+def can_reach_coo(state: "CollectionState", player: int) -> bool:
+ return state.has("Coo", player) and state.has("Coo Spawn", player)
+
+
+def can_reach_nago(state: "CollectionState", player: int) -> bool:
+ return state.has("Nago", player) and state.has("Nago Spawn", player)
+
+
+def can_reach_chuchu(state: "CollectionState", player: int) -> bool:
+ return state.has("ChuChu", player) and state.has("ChuChu Spawn", player)
+
+
+def can_reach_pitch(state: "CollectionState", player: int) -> bool:
+ return state.has("Pitch", player) and state.has("Pitch Spawn", player)
+
+
+def can_reach_burning(state: "CollectionState", player: int) -> bool:
+ return state.has("Burning", player) and state.has("Burning Ability", player)
+
+
+def can_reach_stone(state: "CollectionState", player: int) -> bool:
+ return state.has("Stone", player) and state.has("Stone Ability", player)
+
+
+def can_reach_ice(state: "CollectionState", player: int) -> bool:
+ return state.has("Ice", player) and state.has("Ice Ability", player)
+
+
+def can_reach_needle(state: "CollectionState", player: int) -> bool:
+ return state.has("Needle", player) and state.has("Needle Ability", player)
+
+
+def can_reach_clean(state: "CollectionState", player: int) -> bool:
+ return state.has("Clean", player) and state.has("Clean Ability", player)
+
+
+def can_reach_parasol(state: "CollectionState", player: int) -> bool:
+ return state.has("Parasol", player) and state.has("Parasol Ability", player)
+
+
+def can_reach_spark(state: "CollectionState", player: int) -> bool:
+ return state.has("Spark", player) and state.has("Spark Ability", player)
+
+
+def can_reach_cutter(state: "CollectionState", player: int) -> bool:
+ return state.has("Cutter", player) and state.has("Cutter Ability", player)
+
+
+ability_map: typing.Dict[str, typing.Callable[["CollectionState", int], bool]] = {
+ "No Ability": lambda state, player: True,
+ "Burning Ability": can_reach_burning,
+ "Stone Ability": can_reach_stone,
+ "Ice Ability": can_reach_ice,
+ "Needle Ability": can_reach_needle,
+ "Clean Ability": can_reach_clean,
+ "Parasol Ability": can_reach_parasol,
+ "Spark Ability": can_reach_spark,
+ "Cutter Ability": can_reach_cutter,
+}
+
+
+def can_assemble_rob(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]):
+ # check animal requirements
+ if not (can_reach_coo(state, player) and can_reach_kine(state, player)):
+ return False
+ for abilities, bukisets in EnemyAbilities.enemy_restrictive[1:5]:
+ iterator = iter(x for x in bukisets if copy_abilities[x] in abilities)
+ target_bukiset = next(iterator, None)
+ can_reach = False
+ while target_bukiset is not None:
+ can_reach = can_reach | ability_map[copy_abilities[target_bukiset]](state, player)
+ target_bukiset = next(iterator, None)
+ if not can_reach:
+ return False
+ # now the known needed abilities
+ return can_reach_parasol(state, player) and can_reach_stone(state, player)
+
+
+def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]):
+ can_reach = True
+ for enemy in {"Sparky", "Blocky", "Jumper Shoot", "Yuki", "Sir Kibble", "Haboki", "Boboo", "Captain Stitch"}:
+ can_reach = can_reach & ability_map[copy_abilities[enemy]](state, player)
+ return can_reach
+
+
+def set_rules(world: "KDL3World") -> None:
+ # Level 1
+ set_rule(world.multiworld.get_location(LocationName.grass_land_muchi, world.player),
+ lambda state: can_reach_chuchu(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.grass_land_chao, world.player),
+ lambda state: can_reach_stone(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.grass_land_mine, world.player),
+ lambda state: can_reach_kine(state, world.player))
+
+ # Level 2
+ set_rule(world.multiworld.get_location(LocationName.ripple_field_5, world.player),
+ lambda state: can_reach_kine(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.ripple_field_kamuribana, world.player),
+ lambda state: can_reach_pitch(state, world.player) and can_reach_clean(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.ripple_field_bakasa, world.player),
+ lambda state: can_reach_kine(state, world.player) and can_reach_parasol(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.ripple_field_toad, world.player),
+ lambda state: can_reach_needle(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.ripple_field_mama_pitch, world.player),
+ lambda state: (can_reach_pitch(state, world.player) and
+ can_reach_kine(state, world.player) and
+ can_reach_burning(state, world.player) and
+ can_reach_stone(state, world.player)))
+
+ # Level 3
+ set_rule(world.multiworld.get_location(LocationName.sand_canyon_5, world.player),
+ lambda state: can_reach_cutter(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.sand_canyon_auntie, world.player),
+ lambda state: can_reach_clean(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.sand_canyon_nyupun, world.player),
+ lambda state: can_reach_chuchu(state, world.player) and can_reach_cutter(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.sand_canyon_rob, world.player),
+ lambda state: can_assemble_rob(state, world.player, world.copy_abilities)
+ )
+
+ # Level 4
+ set_rule(world.multiworld.get_location(LocationName.cloudy_park_hibanamodoki, world.player),
+ lambda state: can_reach_coo(state, world.player) and can_reach_clean(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.cloudy_park_piyokeko, world.player),
+ lambda state: can_reach_needle(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.cloudy_park_mikarin, world.player),
+ lambda state: can_reach_coo(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.cloudy_park_pick, world.player),
+ lambda state: can_reach_rick(state, world.player))
+
+ # Level 5
+ set_rule(world.multiworld.get_location(LocationName.iceberg_4, world.player),
+ lambda state: can_reach_burning(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.iceberg_kogoesou, world.player),
+ lambda state: can_reach_burning(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.iceberg_samus, world.player),
+ lambda state: can_reach_ice(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.iceberg_name, world.player),
+ lambda state: (can_reach_coo(state, world.player) and
+ can_reach_burning(state, world.player) and
+ can_reach_chuchu(state, world.player)))
+ # ChuChu is guaranteed here, but we use this for consistency
+ set_rule(world.multiworld.get_location(LocationName.iceberg_shiro, world.player),
+ lambda state: can_reach_nago(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.iceberg_angel, world.player),
+ lambda state: can_fix_angel_wings(state, world.player, world.copy_abilities))
+
+ # Consumables
+ if world.options.consumables:
+ set_rule(world.multiworld.get_location(LocationName.grass_land_1_u1, world.player),
+ lambda state: can_reach_parasol(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.grass_land_1_m1, world.player),
+ lambda state: can_reach_spark(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.grass_land_2_u1, world.player),
+ lambda state: can_reach_needle(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.ripple_field_2_u1, world.player),
+ lambda state: can_reach_kine(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.ripple_field_2_m1, world.player),
+ lambda state: can_reach_kine(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.ripple_field_3_u1, world.player),
+ lambda state: can_reach_cutter(state, world.player) or can_reach_spark(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.ripple_field_4_u1, world.player),
+ lambda state: can_reach_stone(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.ripple_field_4_m2, world.player),
+ lambda state: can_reach_stone(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.ripple_field_5_m1, world.player),
+ lambda state: can_reach_kine(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.ripple_field_5_u1, world.player),
+ lambda state: (can_reach_kine(state, world.player) and
+ can_reach_burning(state, world.player) and
+ can_reach_stone(state, world.player)))
+ set_rule(world.multiworld.get_location(LocationName.ripple_field_5_m2, world.player),
+ lambda state: (can_reach_kine(state, world.player) and
+ can_reach_burning(state, world.player) and
+ can_reach_stone(state, world.player)))
+ set_rule(world.multiworld.get_location(LocationName.sand_canyon_4_u1, world.player),
+ lambda state: can_reach_clean(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.sand_canyon_4_m2, world.player),
+ lambda state: can_reach_needle(state, world.player))
+ set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u2, world.player),
+ lambda state: can_reach_ice(state, world.player) and
+ (can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
+ or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
+ or can_reach_nago(state, world.player)))
+ set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u3, world.player),
+ lambda state: can_reach_ice(state, world.player) and
+ (can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
+ or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
+ or can_reach_nago(state, world.player)))
+ set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u4, world.player),
+ lambda state: can_reach_ice(state, world.player) and
+ (can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
+ or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
+ or can_reach_nago(state, world.player)))
+ set_rule(world.multiworld.get_location(LocationName.cloudy_park_6_u1, world.player),
+ lambda state: can_reach_cutter(state, world.player))
+
+ if world.options.starsanity:
+ # ranges are our friend
+ for i in range(7, 11):
+ set_rule(world.multiworld.get_location(f"Grass Land 1 - Star {i}", world.player),
+ lambda state: can_reach_cutter(state, world.player))
+ for i in range(11, 14):
+ set_rule(world.multiworld.get_location(f"Grass Land 1 - Star {i}", world.player),
+ lambda state: can_reach_parasol(state, world.player))
+ for i in [1, 3, 4, 9, 10]:
+ set_rule(world.multiworld.get_location(f"Grass Land 2 - Star {i}", world.player),
+ lambda state: can_reach_stone(state, world.player))
+ set_rule(world.multiworld.get_location("Grass Land 2 - Star 2", world.player),
+ lambda state: can_reach_burning(state, world.player))
+ set_rule(world.multiworld.get_location("Ripple Field 2 - Star 17", world.player),
+ lambda state: can_reach_kine(state, world.player))
+ for i in range(41, 43):
+ # any star past this point also needs kine, but so does the exit
+ set_rule(world.multiworld.get_location(f"Ripple Field 5 - Star {i}", world.player),
+ lambda state: can_reach_kine(state, world.player))
+ for i in range(46, 49):
+ # also requires kine, but only for access from the prior room
+ set_rule(world.multiworld.get_location(f"Ripple Field 5 - Star {i}", world.player),
+ lambda state: can_reach_burning(state, world.player) and can_reach_stone(state, world.player))
+ for i in range(12, 18):
+ set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player),
+ lambda state: can_reach_ice(state, world.player) and
+ (can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
+ or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
+ or can_reach_nago(state, world.player)))
+ for i in range(21, 23):
+ set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player),
+ lambda state: can_reach_chuchu(state, world.player))
+ for r in [range(19, 21), range(23, 31)]:
+ for i in r:
+ set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player),
+ lambda state: can_reach_clean(state, world.player))
+ for i in range(31, 41):
+ set_rule(world.multiworld.get_location(f"Sand Canyon 5 - Star {i}", world.player),
+ lambda state: can_reach_burning(state, world.player))
+ for r in [range(1, 31), range(44, 51)]:
+ for i in r:
+ set_rule(world.multiworld.get_location(f"Cloudy Park 4 - Star {i}", world.player),
+ lambda state: can_reach_coo(state, world.player))
+ for i in [18, *list(range(20, 25))]:
+ set_rule(world.multiworld.get_location(f"Cloudy Park 6 - Star {i}", world.player),
+ lambda state: can_reach_ice(state, world.player))
+ for i in [19, *list(range(25, 30))]:
+ set_rule(world.multiworld.get_location(f"Cloudy Park 6 - Star {i}", world.player),
+ lambda state: can_reach_ice(state, world.player))
+ # copy ability access edge cases
+ # Kirby cannot eat enemies fully submerged in water. Vast majority of cases, the enemy can be brought to the surface
+ # and eaten by inhaling while falling on top of them
+ set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_2_E3, world.player),
+ lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
+ set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_3_E6, world.player),
+ lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
+ # Ripple Field 4 E5, E7, and E8 are doable, but too strict to leave in logic
+ set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E5, world.player),
+ lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
+ set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E7, world.player),
+ lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
+ set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E8, world.player),
+ lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
+ set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E1, world.player),
+ lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
+ set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E2, world.player),
+ lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
+ set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E3, world.player),
+ lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
+ set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E4, world.player),
+ lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
+ set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E7, world.player),
+ lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
+ set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E8, world.player),
+ lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
+ set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E9, world.player),
+ lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
+ set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E10, world.player),
+ lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
+
+ for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified",
+ "Level 3 Boss - Purified", "Level 4 Boss - Purified",
+ "Level 5 Boss - Purified"],
+ [LocationName.grass_land_whispy, LocationName.ripple_field_acro,
+ LocationName.sand_canyon_poncon, LocationName.cloudy_park_ado,
+ LocationName.iceberg_dedede],
+ range(1, 6)):
+ set_rule(world.multiworld.get_location(boss_flag, world.player),
+ lambda state, i=i: (state.has("Heart Star", world.player, world.boss_requirements[i - 1])
+ and can_reach_boss(state, world.player, i,
+ world.options.open_world.value,
+ world.options.ow_boss_requirement.value,
+ world.player_levels)))
+ set_rule(world.multiworld.get_location(purification, world.player),
+ lambda state, i=i: (state.has("Heart Star", world.player, world.boss_requirements[i - 1])
+ and can_reach_boss(state, world.player, i,
+ world.options.open_world.value,
+ world.options.ow_boss_requirement.value,
+ world.player_levels)))
+
+ set_rule(world.multiworld.get_entrance("To Level 6", world.player),
+ lambda state: state.has("Heart Star", world.player, world.required_heart_stars))
+
+ for level in range(2, 6):
+ set_rule(world.multiworld.get_entrance(f"To Level {level}", world.player),
+ lambda state, i=level: state.has(f"Level {i - 1} Boss Defeated", world.player))
+
+ if world.options.strict_bosses:
+ for level in range(2, 6):
+ add_rule(world.multiworld.get_entrance(f"To Level {level}", world.player),
+ lambda state, i=level: state.has(f"Level {i - 1} Boss Purified", world.player))
+
+ if world.options.goal_speed == GoalSpeed.option_normal:
+ add_rule(world.multiworld.get_entrance("To Level 6", world.player),
+ lambda state: state.has_all(["Level 1 Boss Purified", "Level 2 Boss Purified", "Level 3 Boss Purified",
+ "Level 4 Boss Purified", "Level 5 Boss Purified"], world.player))
diff --git a/worlds/kdl3/__init__.py b/worlds/kdl3/__init__.py
new file mode 100644
index 000000000000..8c9f3cc46a4e
--- /dev/null
+++ b/worlds/kdl3/__init__.py
@@ -0,0 +1,366 @@
+import logging
+import typing
+
+from BaseClasses import Tutorial, ItemClassification, MultiWorld
+from Fill import fill_restrictive
+from Options import PerGameCommonOptions
+from worlds.AutoWorld import World, WebWorld
+from .Items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \
+ trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights
+from .Locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations
+from .Names.AnimalFriendSpawns import animal_friend_spawns
+from .Names.EnemyAbilities import vanilla_enemies, enemy_mapping, enemy_restrictive
+from .Regions import create_levels, default_levels
+from .Options import KDL3Options
+from .Presets import kdl3_options_presets
+from .Names import LocationName
+from .Room import KDL3Room
+from .Rules import set_rules
+from .Rom import KDL3DeltaPatch, get_base_rom_path, RomData, patch_rom, KDL3JHASH, KDL3UHASH
+from .Client import KDL3SNIClient
+
+from typing import Dict, TextIO, Optional, List
+import os
+import math
+import threading
+import base64
+import settings
+
+logger = logging.getLogger("Kirby's Dream Land 3")
+
+
+class KDL3Settings(settings.Group):
+ class RomFile(settings.SNESRomPath):
+ """File name of the KDL3 JP or EN rom"""
+ description = "Kirby's Dream Land 3 ROM File"
+ copy_to = "Kirby's Dream Land 3.sfc"
+ md5s = [KDL3JHASH, KDL3UHASH]
+
+ rom_file: RomFile = RomFile(RomFile.copy_to)
+
+
+class KDL3WebWorld(WebWorld):
+ theme = "partyTime"
+ tutorials = [
+
+ Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to setting up the Kirby's Dream Land 3 randomizer connected to an Archipelago Multiworld.",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["Silvris"]
+ )
+ ]
+ options_presets = kdl3_options_presets
+
+
+class KDL3World(World):
+ """
+ Join Kirby and his Animal Friends on an adventure to collect Heart Stars and drive Dark Matter away from Dream Land!
+ """
+
+ game = "Kirby's Dream Land 3"
+ options_dataclass: typing.ClassVar[typing.Type[PerGameCommonOptions]] = KDL3Options
+ options: KDL3Options
+ item_name_to_id = {item: item_table[item].code for item in item_table}
+ location_name_to_id = {location_table[location]: location for location in location_table}
+ item_name_groups = item_names
+ web = KDL3WebWorld()
+ settings: typing.ClassVar[KDL3Settings]
+
+ def __init__(self, multiworld: MultiWorld, player: int):
+ self.rom_name = None
+ self.rom_name_available_event = threading.Event()
+ super().__init__(multiworld, player)
+ self.copy_abilities: Dict[str, str] = vanilla_enemies.copy()
+ self.required_heart_stars: int = 0 # we fill this during create_items
+ self.boss_requirements: Dict[int, int] = dict()
+ self.player_levels = default_levels.copy()
+ self.stage_shuffle_enabled = False
+ self.boss_butch_bosses: List[Optional[bool]] = list()
+ self.rooms: Optional[List[KDL3Room]] = None
+
+ @classmethod
+ def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
+ rom_file: str = get_base_rom_path()
+ if not os.path.exists(rom_file):
+ raise FileNotFoundError(f"Could not find base ROM for {cls.game}: {rom_file}")
+
+ create_regions = create_levels
+
+ def create_item(self, name: str, force_non_progression=False) -> KDL3Item:
+ item = item_table[name]
+ classification = ItemClassification.filler
+ if item.progression and not force_non_progression:
+ classification = ItemClassification.progression_skip_balancing \
+ if item.skip_balancing else ItemClassification.progression
+ elif item.trap:
+ classification = ItemClassification.trap
+ return KDL3Item(name, classification, item.code, self.player)
+
+ def get_filler_item_name(self, include_stars=True) -> str:
+ if include_stars:
+ return self.random.choices(list(total_filler_weights.keys()),
+ weights=list(total_filler_weights.values()))[0]
+ return self.random.choices(list(filler_item_weights.keys()),
+ weights=list(filler_item_weights.values()))[0]
+
+ def get_trap_item_name(self) -> str:
+ return self.random.choices(["Gooey Bag", "Slowness", "Eject Ability"],
+ weights=[self.options.gooey_trap_weight.value,
+ self.options.slow_trap_weight.value,
+ self.options.ability_trap_weight.value])[0]
+
+ def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: typing.List[str],
+ level: int, stage: int):
+ valid_rooms = [room for room in self.rooms if (room.level < level)
+ or (room.level == level and room.stage < stage)] # leave out the stage in question to avoid edge
+ valid_enemies = set()
+ for room in valid_rooms:
+ valid_enemies.update(room.enemies)
+ placed_enemies = [enemy for enemy in valid_enemies if enemy not in enemies_to_set]
+ if any(self.copy_abilities[enemy] == copy_ability for enemy in placed_enemies):
+ return None # a valid enemy got placed by a more restrictive placement
+ return self.random.choice(sorted([enemy for enemy in valid_enemies if enemy not in placed_enemies]))
+
+ def pre_fill(self) -> None:
+ if self.options.copy_ability_randomization:
+ # randomize copy abilities
+ valid_abilities = list(copy_ability_access_table.keys())
+ enemies_to_set = list(self.copy_abilities.keys())
+ unplaced_abilities = set(key for key in copy_ability_access_table.keys()
+ if key not in ("No Ability", "Cutter Ability", "Burning Ability"))
+ # now for the edge cases
+ for abilities, enemies in enemy_restrictive:
+ available_enemies = list()
+ for enemy in enemies:
+ if enemy not in enemies_to_set:
+ if self.copy_abilities[enemy] in abilities:
+ break
+ else:
+ available_enemies.append(enemy)
+ else:
+ chosen_enemy = self.random.choice(available_enemies)
+ chosen_ability = self.random.choice(abilities)
+ self.copy_abilities[chosen_enemy] = chosen_ability
+ enemies_to_set.remove(chosen_enemy)
+ unplaced_abilities.discard(chosen_ability)
+ # two less restrictive ones, we need to ensure Cutter and Burning appear before their required stages
+ sand_canyon_5 = self.get_region("Sand Canyon 5 - 9")
+ # this is primarily for typing, but if this ever hits it's fine to crash
+ assert isinstance(sand_canyon_5, KDL3Room)
+ cutter_enemy = self.get_restrictive_copy_ability_placement("Cutter Ability", enemies_to_set,
+ sand_canyon_5.level, sand_canyon_5.stage)
+ if cutter_enemy:
+ self.copy_abilities[cutter_enemy] = "Cutter Ability"
+ enemies_to_set.remove(cutter_enemy)
+ iceberg_4 = self.get_region("Iceberg 4 - 7")
+ # this is primarily for typing, but if this ever hits it's fine to crash
+ assert isinstance(iceberg_4, KDL3Room)
+ burning_enemy = self.get_restrictive_copy_ability_placement("Burning Ability", enemies_to_set,
+ iceberg_4.level, iceberg_4.stage)
+ if burning_enemy:
+ self.copy_abilities[burning_enemy] = "Burning Ability"
+ enemies_to_set.remove(burning_enemy)
+ # ensure we place one of every ability
+ if unplaced_abilities and self.options.accessibility != self.options.accessibility.option_minimal:
+ # failsafe, on non-minimal we need to guarantee every copy ability exists
+ for ability in sorted(unplaced_abilities):
+ enemy = self.random.choice(enemies_to_set)
+ self.copy_abilities[enemy] = ability
+ enemies_to_set.remove(enemy)
+ # place remaining
+ for enemy in enemies_to_set:
+ self.copy_abilities[enemy] = self.random.choice(valid_abilities)
+ for enemy in enemy_mapping:
+ self.multiworld.get_location(enemy, self.player) \
+ .place_locked_item(self.create_item(self.copy_abilities[enemy_mapping[enemy]]))
+ # fill animals
+ if self.options.animal_randomization != 0:
+ spawns = [animal for animal in animal_friend_spawns.keys() if
+ animal not in ["Ripple Field 5 - Animal 2", "Sand Canyon 6 - Animal 1", "Iceberg 4 - Animal 1"]]
+ self.multiworld.get_location("Iceberg 4 - Animal 1", self.player) \
+ .place_locked_item(self.create_item("ChuChu Spawn"))
+ # Not having ChuChu here makes the room impossible (since only she has vertical burning)
+ self.multiworld.get_location("Ripple Field 5 - Animal 2", self.player) \
+ .place_locked_item(self.create_item("Pitch Spawn"))
+ guaranteed_animal = self.random.choice(["Kine Spawn", "Coo Spawn"])
+ self.multiworld.get_location("Sand Canyon 6 - Animal 1", self.player) \
+ .place_locked_item(self.create_item(guaranteed_animal))
+ # Ripple Field 5 - Animal 2 needs to be Pitch to ensure accessibility on non-door rando
+ if self.options.animal_randomization == 1:
+ animal_pool = [animal_friend_spawns[spawn] for spawn in animal_friend_spawns
+ if spawn not in ["Ripple Field 5 - Animal 2", "Sand Canyon 6 - Animal 1",
+ "Iceberg 4 - Animal 1"]]
+ else:
+ animal_base = ["Rick Spawn", "Kine Spawn", "Coo Spawn", "Nago Spawn", "ChuChu Spawn", "Pitch Spawn"]
+ animal_pool = [self.random.choice(animal_base)
+ for _ in range(len(animal_friend_spawns) - 9)]
+ # have to guarantee one of each animal
+ animal_pool.extend(animal_base)
+ if guaranteed_animal == "Kine Spawn":
+ animal_pool.append("Coo Spawn")
+ else:
+ animal_pool.append("Kine Spawn")
+ # Weird fill hack, this forces ChuChu to be the last animal friend placed
+ # If Kine is ever the last animal friend placed, he will cause fill errors on closed world
+ animal_pool.sort()
+ locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns]
+ items = [self.create_item(animal) for animal in animal_pool]
+ allstate = self.multiworld.get_all_state(False)
+ self.random.shuffle(locations)
+ fill_restrictive(self.multiworld, allstate, locations, items, True, True)
+ else:
+ animal_friends = animal_friend_spawns.copy()
+ for animal in animal_friends:
+ self.multiworld.get_location(animal, self.player) \
+ .place_locked_item(self.create_item(animal_friends[animal]))
+
+ def create_items(self) -> None:
+ itempool = []
+ itempool.extend([self.create_item(name) for name in copy_ability_table])
+ itempool.extend([self.create_item(name) for name in animal_friend_table])
+ required_percentage = self.options.heart_stars_required / 100.0
+ remaining_items = len(location_table) - len(itempool)
+ if not self.options.consumables:
+ remaining_items -= len(consumable_locations)
+ remaining_items -= len(star_locations)
+ if self.options.starsanity:
+ # star fill, keep consumable pool locked to consumable and fill 767 stars specifically
+ star_items = list(star_item_weights.keys())
+ star_weights = list(star_item_weights.values())
+ itempool.extend([self.create_item(item) for item in self.random.choices(star_items, weights=star_weights,
+ k=767)])
+ total_heart_stars = self.options.total_heart_stars
+ # ensure at least 1 heart star required per world
+ required_heart_stars = max(int(total_heart_stars * required_percentage), 5)
+ filler_items = total_heart_stars - required_heart_stars
+ filler_amount = math.floor(filler_items * (self.options.filler_percentage / 100.0))
+ trap_amount = math.floor(filler_amount * (self.options.trap_percentage / 100.0))
+ filler_amount -= trap_amount
+ non_required_heart_stars = filler_items - filler_amount - trap_amount
+ self.required_heart_stars = required_heart_stars
+ # handle boss requirements here
+ requirements = [required_heart_stars]
+ quotient = required_heart_stars // 5 # since we set the last manually, we can afford imperfect rounding
+ if self.options.boss_requirement_random:
+ for i in range(1, 5):
+ if self.options.strict_bosses:
+ max_stars = quotient * i
+ else:
+ max_stars = required_heart_stars
+ requirements.insert(i, self.random.randint(
+ min(1, max_stars), max_stars))
+ if self.options.strict_bosses:
+ requirements.sort()
+ else:
+ self.random.shuffle(requirements)
+ else:
+ for i in range(1, 5):
+ requirements.insert(i - 1, quotient * i)
+ self.boss_requirements = requirements
+ itempool.extend([self.create_item("Heart Star") for _ in range(required_heart_stars)])
+ itempool.extend([self.create_item(self.get_filler_item_name(False))
+ for _ in range(filler_amount + (remaining_items - total_heart_stars))])
+ itempool.extend([self.create_item(self.get_trap_item_name())
+ for _ in range(trap_amount)])
+ itempool.extend([self.create_item("Heart Star", True) for _ in range(non_required_heart_stars)])
+ self.multiworld.itempool += itempool
+ if self.options.open_world:
+ for level in self.player_levels:
+ for stage in range(0, 6):
+ self.multiworld.get_location(location_table[self.player_levels[level][stage]]
+ .replace("Complete", "Stage Completion"), self.player) \
+ .place_locked_item(KDL3Item(
+ f"{LocationName.level_names_inverse[level]} - Stage Completion",
+ ItemClassification.progression, None, self.player))
+
+ set_rules = set_rules
+
+ def generate_basic(self) -> None:
+ self.stage_shuffle_enabled = self.options.stage_shuffle > 0
+ goal = self.options.goal
+ goal_location = self.multiworld.get_location(LocationName.goals[goal], self.player)
+ goal_location.place_locked_item(KDL3Item("Love-Love Rod", ItemClassification.progression, None, self.player))
+ for level in range(1, 6):
+ self.multiworld.get_location(f"Level {level} Boss - Defeated", self.player) \
+ .place_locked_item(
+ KDL3Item(f"Level {level} Boss Defeated", ItemClassification.progression, None, self.player))
+ self.multiworld.get_location(f"Level {level} Boss - Purified", self.player) \
+ .place_locked_item(
+ KDL3Item(f"Level {level} Boss Purified", ItemClassification.progression, None, self.player))
+ self.multiworld.completion_condition[self.player] = lambda state: state.has("Love-Love Rod", self.player)
+ # this can technically be done at any point before generate_output
+ if self.options.allow_bb:
+ if self.options.allow_bb == self.options.allow_bb.option_enforced:
+ self.boss_butch_bosses = [True for _ in range(6)]
+ else:
+ self.boss_butch_bosses = [self.random.choice([True, False]) for _ in range(6)]
+ else:
+ self.boss_butch_bosses = [False for _ in range(6)]
+
+ def generate_output(self, output_directory: str):
+ rom_path = ""
+ try:
+ rom = RomData(get_base_rom_path())
+ patch_rom(self, rom)
+
+ rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")
+ rom.write_to_file(rom_path)
+ self.rom_name = rom.name
+
+ patch = KDL3DeltaPatch(os.path.splitext(rom_path)[0] + KDL3DeltaPatch.patch_file_ending, player=self.player,
+ player_name=self.multiworld.player_name[self.player], patched_path=rom_path)
+ patch.write()
+ except Exception:
+ raise
+ finally:
+ self.rom_name_available_event.set() # make sure threading continues and errors are collected
+ if os.path.exists(rom_path):
+ os.unlink(rom_path)
+
+ def modify_multidata(self, multidata: dict):
+ # wait for self.rom_name to be available.
+ self.rom_name_available_event.wait()
+ rom_name = getattr(self, "rom_name", None)
+ # we skip in case of error, so that the original error in the output thread is the one that gets raised
+ if rom_name:
+ new_name = base64.b64encode(bytes(self.rom_name)).decode()
+ multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]
+
+ def write_spoiler(self, spoiler_handle: TextIO) -> None:
+ if self.stage_shuffle_enabled:
+ spoiler_handle.write(f"\nLevel Layout ({self.multiworld.get_player_name(self.player)}):\n")
+ for level in LocationName.level_names:
+ for stage, i in zip(self.player_levels[LocationName.level_names[level]], range(1, 7)):
+ spoiler_handle.write(f"{level} {i}: {location_table[stage].replace(' - Complete', '')}\n")
+ if self.options.animal_randomization:
+ spoiler_handle.write(f"\nAnimal Friends ({self.multiworld.get_player_name(self.player)}):\n")
+ for level in self.player_levels:
+ for stage in range(6):
+ rooms = [room for room in self.rooms if room.level == level and room.stage == stage]
+ animals = []
+ for room in rooms:
+ animals.extend([location.item.name.replace(" Spawn", "")
+ for location in room.locations if "Animal" in location.name])
+ spoiler_handle.write(f"{location_table[self.player_levels[level][stage]].replace(' - Complete','')}"
+ f": {', '.join(animals)}\n")
+ if self.options.copy_ability_randomization:
+ spoiler_handle.write(f"\nCopy Abilities ({self.multiworld.get_player_name(self.player)}):\n")
+ for enemy in self.copy_abilities:
+ spoiler_handle.write(f"{enemy}: {self.copy_abilities[enemy].replace('No Ability', 'None').replace(' Ability', '')}\n")
+
+ def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
+ if self.stage_shuffle_enabled:
+ regions = {LocationName.level_names[level]: level for level in LocationName.level_names}
+ level_hint_data = {}
+ for level in regions:
+ for stage in range(7):
+ stage_name = self.multiworld.get_location(self.location_id_to_name[self.player_levels[level][stage]],
+ self.player).name.replace(" - Complete", "")
+ stage_regions = [room for room in self.rooms if stage_name in room.name]
+ for region in stage_regions:
+ for location in [location for location in region.locations if location.address]:
+ level_hint_data[location.address] = f"{regions[level]} {stage + 1 if stage < 6 else 'Boss'}"
+ hint_data[self.player] = level_hint_data
diff --git a/worlds/kdl3/data/APConsumable.bsdiff4 b/worlds/kdl3/data/APConsumable.bsdiff4
new file mode 100644
index 000000000000..e930a4e2b30c
Binary files /dev/null and b/worlds/kdl3/data/APConsumable.bsdiff4 differ
diff --git a/worlds/kdl3/data/APHeartStar.bsdiff4 b/worlds/kdl3/data/APHeartStar.bsdiff4
new file mode 100644
index 000000000000..e442604f2600
Binary files /dev/null and b/worlds/kdl3/data/APHeartStar.bsdiff4 differ
diff --git a/worlds/kdl3/data/APPauseIcons.dat b/worlds/kdl3/data/APPauseIcons.dat
new file mode 100644
index 000000000000..e7773a0e6718
Binary files /dev/null and b/worlds/kdl3/data/APPauseIcons.dat differ
diff --git a/worlds/kdl3/data/APStars.bsdiff4 b/worlds/kdl3/data/APStars.bsdiff4
new file mode 100644
index 000000000000..c335bae9cada
Binary files /dev/null and b/worlds/kdl3/data/APStars.bsdiff4 differ
diff --git a/worlds/kdl3/data/Rooms.json b/worlds/kdl3/data/Rooms.json
new file mode 100644
index 000000000000..47fe76534c30
--- /dev/null
+++ b/worlds/kdl3/data/Rooms.json
@@ -0,0 +1 @@
+[{"name": "Grass Land 1 - 0", "level": 1, "stage": 1, "room": 0, "pointer": 3434257, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Waddle Dee", "Sir Kibble", "Cappy"], "default_exits": [{"room": 1, "unkn1": 205, "unkn2": 8, "x": 72, "y": 200, "name": "Grass Land 1 - 0 Exit 0", "access_rule": []}, {"room": 2, "unkn1": 87, "unkn2": 9, "x": 104, "y": 152, "name": "Grass Land 1 - 0 Exit 1", "access_rule": []}], "entity_load": [[0, 16], [1, 23], [0, 23], [14, 23], [27, 16], [12, 16], [4, 22]], "locations": ["Grass Land 1 - Enemy 1 (Waddle Dee)", "Grass Land 1 - Enemy 2 (Sir Kibble)", "Grass Land 1 - Enemy 3 (Cappy)", "Grass Land 1 - Star 1", "Grass Land 1 - Star 2", "Grass Land 1 - Star 3", "Grass Land 1 - Star 4", "Grass Land 1 - Star 5", "Grass Land 1 - Star 6", "Grass Land 1 - Star 7", "Grass Land 1 - Star 8", "Grass Land 1 - Star 9", "Grass Land 1 - Star 10"], "music": 20}, {"name": "Grass Land 1 - 1", "level": 1, "stage": 1, "room": 1, "pointer": 3368373, "animal_pointers": [], "consumables": [{"idx": 14, "pointer": 264, "x": 928, "y": 160, "etype": 22, "vtype": 0, "name": "Grass Land 1 - 1-Up (Parasol)"}, {"idx": 15, "pointer": 312, "x": 1456, "y": 176, "etype": 22, "vtype": 2, "name": "Grass Land 1 - Maxim Tomato (Spark)"}], "consumables_pointer": 304, "enemies": ["Sparky", "Bronto Burt", "Sasuke"], "default_exits": [{"room": 3, "unkn1": 143, "unkn2": 6, "x": 56, "y": 152, "name": "Grass Land 1 - 1 Exit 0", "access_rule": []}], "entity_load": [[2, 16], [30, 16], [12, 16], [14, 23], [8, 16], [0, 22], [2, 22]], "locations": ["Grass Land 1 - Enemy 4 (Sparky)", "Grass Land 1 - Enemy 5 (Bronto Burt)", "Grass Land 1 - Enemy 6 (Sasuke)", "Grass Land 1 - Star 11", "Grass Land 1 - Star 12", "Grass Land 1 - Star 13", "Grass Land 1 - Star 14", "Grass Land 1 - Star 15", "Grass Land 1 - 1-Up (Parasol)", "Grass Land 1 - Maxim Tomato (Spark)"], "music": 20}, {"name": "Grass Land 1 - 2", "level": 1, "stage": 1, "room": 2, "pointer": 2960650, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 0, "unkn1": 5, "unkn2": 9, "x": 1416, "y": 152, "name": "Grass Land 1 - 2 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Grass Land 1 - Animal 1", "Grass Land 1 - Animal 2"], "music": 38}, {"name": "Grass Land 1 - 3", "level": 1, "stage": 1, "room": 3, "pointer": 3478442, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Poppy Bros Jr."], "default_exits": [{"room": 4, "unkn1": 179, "unkn2": 9, "x": 56, "y": 152, "name": "Grass Land 1 - 3 Exit 0", "access_rule": []}], "entity_load": [[0, 19], [7, 16], [0, 23], [6, 22], [14, 23], [8, 16], [1, 23]], "locations": ["Grass Land 1 - Enemy 7 (Poppy Bros Jr.)", "Grass Land 1 - Star 16", "Grass Land 1 - Star 17", "Grass Land 1 - Star 18", "Grass Land 1 - Star 19", "Grass Land 1 - Star 20", "Grass Land 1 - Star 21", "Grass Land 1 - Star 22", "Grass Land 1 - Star 23"], "music": 20}, {"name": "Grass Land 1 - 4", "level": 1, "stage": 1, "room": 4, "pointer": 2978390, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 5, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Grass Land 1 - 4 Exit 0", "access_rule": []}], "entity_load": [[0, 19], [42, 19]], "locations": ["Grass Land 1 - Tulip"], "music": 8}, {"name": "Grass Land 1 - 5", "level": 1, "stage": 1, "room": 5, "pointer": 2890835, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Grass Land 1 - Complete"], "music": 5}, {"name": "Grass Land 2 - 0", "level": 1, "stage": 2, "room": 0, "pointer": 3293347, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Rocky", "KeKe", "Bobo", "Poppy Bros Jr."], "default_exits": [{"room": 1, "unkn1": 112, "unkn2": 9, "x": 72, "y": 152, "name": "Grass Land 2 - 0 Exit 0", "access_rule": []}], "entity_load": [[3, 16], [7, 16], [5, 16], [4, 22], [51, 16], [14, 23]], "locations": ["Grass Land 2 - Enemy 1 (Rocky)", "Grass Land 2 - Enemy 2 (KeKe)", "Grass Land 2 - Enemy 3 (Bobo)", "Grass Land 2 - Enemy 4 (Poppy Bros Jr.)", "Grass Land 2 - Star 1", "Grass Land 2 - Star 2", "Grass Land 2 - Star 3", "Grass Land 2 - Star 4", "Grass Land 2 - Star 5", "Grass Land 2 - Star 6", "Grass Land 2 - Star 7", "Grass Land 2 - Star 8", "Grass Land 2 - Star 9", "Grass Land 2 - Star 10"], "music": 11}, {"name": "Grass Land 2 - 1", "level": 1, "stage": 2, "room": 1, "pointer": 3059685, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 20, "unkn2": 9, "x": 56, "y": 136, "name": "Grass Land 2 - 1 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Grass Land 2 - Animal 1", "Grass Land 2 - Animal 2"], "music": 39}, {"name": "Grass Land 2 - 2", "level": 1, "stage": 2, "room": 2, "pointer": 3432109, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Waddle Dee", "Popon Ball", "Bouncy"], "default_exits": [{"room": 4, "unkn1": 133, "unkn2": 11, "x": 72, "y": 200, "name": "Grass Land 2 - 2 Exit 0", "access_rule": []}, {"room": 3, "unkn1": 52, "unkn2": 12, "x": 56, "y": 152, "name": "Grass Land 2 - 2 Exit 1", "access_rule": []}], "entity_load": [[13, 16], [50, 16], [4, 22], [3, 16], [0, 16], [14, 23]], "locations": ["Grass Land 2 - Enemy 5 (Waddle Dee)", "Grass Land 2 - Enemy 6 (Popon Ball)", "Grass Land 2 - Enemy 7 (Bouncy)", "Grass Land 2 - Star 11", "Grass Land 2 - Star 12", "Grass Land 2 - Star 13"], "music": 11}, {"name": "Grass Land 2 - 3", "level": 1, "stage": 2, "room": 3, "pointer": 2970029, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 2, "unkn2": 9, "x": 840, "y": 168, "name": "Grass Land 2 - 3 Exit 0", "access_rule": []}], "entity_load": [[1, 19]], "locations": [], "music": 11}, {"name": "Grass Land 2 - 4", "level": 1, "stage": 2, "room": 4, "pointer": 3578022, "animal_pointers": [], "consumables": [{"idx": 20, "pointer": 272, "x": 992, "y": 192, "etype": 22, "vtype": 0, "name": "Grass Land 2 - 1-Up (Needle)"}], "consumables_pointer": 352, "enemies": ["Tick", "Bronto Burt", "Nruff"], "default_exits": [{"room": 5, "unkn1": 154, "unkn2": 12, "x": 72, "y": 152, "name": "Grass Land 2 - 4 Exit 0", "access_rule": []}], "entity_load": [[15, 16], [5, 16], [2, 16], [48, 16], [14, 23], [0, 22]], "locations": ["Grass Land 2 - Enemy 8 (Tick)", "Grass Land 2 - Enemy 9 (Bronto Burt)", "Grass Land 2 - Enemy 10 (Nruff)", "Grass Land 2 - Star 14", "Grass Land 2 - Star 15", "Grass Land 2 - Star 16", "Grass Land 2 - Star 17", "Grass Land 2 - Star 18", "Grass Land 2 - Star 19", "Grass Land 2 - Star 20", "Grass Land 2 - Star 21", "Grass Land 2 - 1-Up (Needle)"], "music": 11}, {"name": "Grass Land 2 - 5", "level": 1, "stage": 2, "room": 5, "pointer": 2966057, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Grass Land 2 - 5 Exit 0", "access_rule": []}], "entity_load": [[1, 19], [42, 19]], "locations": ["Grass Land 2 - Muchimuchi"], "music": 8}, {"name": "Grass Land 2 - 6", "level": 1, "stage": 2, "room": 6, "pointer": 2887461, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Grass Land 2 - Complete"], "music": 5}, {"name": "Grass Land 3 - 0", "level": 1, "stage": 3, "room": 0, "pointer": 3149707, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Sparky", "Rocky", "Nruff"], "default_exits": [{"room": 1, "unkn1": 107, "unkn2": 7, "x": 72, "y": 840, "name": "Grass Land 3 - 0 Exit 0", "access_rule": []}, {"room": 4, "unkn1": 46, "unkn2": 9, "x": 152, "y": 152, "name": "Grass Land 3 - 0 Exit 1", "access_rule": []}], "entity_load": [[3, 16], [14, 23], [15, 16], [0, 16], [8, 16]], "locations": ["Grass Land 3 - Enemy 1 (Sparky)", "Grass Land 3 - Enemy 2 (Rocky)", "Grass Land 3 - Enemy 3 (Nruff)", "Grass Land 3 - Star 1", "Grass Land 3 - Star 2", "Grass Land 3 - Star 3", "Grass Land 3 - Star 4", "Grass Land 3 - Star 5", "Grass Land 3 - Star 6", "Grass Land 3 - Star 7", "Grass Land 3 - Star 8", "Grass Land 3 - Star 9", "Grass Land 3 - Star 10"], "music": 19}, {"name": "Grass Land 3 - 1", "level": 1, "stage": 3, "room": 1, "pointer": 3204939, "animal_pointers": [], "consumables": [{"idx": 10, "pointer": 360, "x": 208, "y": 344, "etype": 22, "vtype": 0, "name": "Grass Land 3 - 1-Up (Climb)"}, {"idx": 11, "pointer": 376, "x": 224, "y": 568, "etype": 22, "vtype": 2, "name": "Grass Land 3 - Maxim Tomato (Climb)"}], "consumables_pointer": 128, "enemies": [], "default_exits": [{"room": 3, "unkn1": 9, "unkn2": 9, "x": 56, "y": 152, "name": "Grass Land 3 - 1 Exit 0", "access_rule": []}], "entity_load": [[0, 22], [6, 23], [2, 22], [5, 23], [14, 23], [1, 23], [0, 23], [31, 16]], "locations": ["Grass Land 3 - Star 11", "Grass Land 3 - Star 12", "Grass Land 3 - Star 13", "Grass Land 3 - Star 14", "Grass Land 3 - Star 15", "Grass Land 3 - Star 16", "Grass Land 3 - 1-Up (Climb)", "Grass Land 3 - Maxim Tomato (Climb)"], "music": 19}, {"name": "Grass Land 3 - 2", "level": 1, "stage": 3, "room": 2, "pointer": 3200066, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 13, "unkn2": 55, "x": 56, "y": 152, "name": "Grass Land 3 - 2 Exit 0", "access_rule": []}], "entity_load": [[15, 16], [0, 16], [14, 23]], "locations": ["Grass Land 3 - Star 17", "Grass Land 3 - Star 18", "Grass Land 3 - Star 19", "Grass Land 3 - Star 20", "Grass Land 3 - Star 21", "Grass Land 3 - Star 22", "Grass Land 3 - Star 23", "Grass Land 3 - Star 24", "Grass Land 3 - Star 25", "Grass Land 3 - Star 26"], "music": 19}, {"name": "Grass Land 3 - 3", "level": 1, "stage": 3, "room": 3, "pointer": 2959784, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 15, "unkn2": 9, "x": 56, "y": 120, "name": "Grass Land 3 - 3 Exit 0", "access_rule": []}], "entity_load": [[2, 19]], "locations": [], "music": 31}, {"name": "Grass Land 3 - 4", "level": 1, "stage": 3, "room": 4, "pointer": 2979121, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 0, "unkn1": 8, "unkn2": 9, "x": 760, "y": 152, "name": "Grass Land 3 - 4 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Grass Land 3 - Animal 1", "Grass Land 3 - Animal 2"], "music": 40}, {"name": "Grass Land 3 - 5", "level": 1, "stage": 3, "room": 5, "pointer": 2997811, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 7, "unkn1": 15, "unkn2": 9, "x": 72, "y": 120, "name": "Grass Land 3 - 5 Exit 0", "access_rule": []}], "entity_load": [[2, 19]], "locations": [], "music": 8}, {"name": "Grass Land 3 - 6", "level": 1, "stage": 3, "room": 6, "pointer": 3084876, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bouncy"], "default_exits": [{"room": 5, "unkn1": 96, "unkn2": 9, "x": 40, "y": 152, "name": "Grass Land 3 - 6 Exit 0", "access_rule": []}], "entity_load": [[13, 16], [14, 16], [1, 23], [59, 16], [14, 23]], "locations": ["Grass Land 3 - Pitcherman", "Grass Land 3 - Enemy 4 (Bouncy)", "Grass Land 3 - Star 27", "Grass Land 3 - Star 28", "Grass Land 3 - Star 29", "Grass Land 3 - Star 30", "Grass Land 3 - Star 31"], "music": 19}, {"name": "Grass Land 3 - 7", "level": 1, "stage": 3, "room": 7, "pointer": 2891317, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Grass Land 3 - Complete"], "music": 5}, {"name": "Grass Land 4 - 0", "level": 1, "stage": 4, "room": 0, "pointer": 3471284, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Loud", "Babut", "Rocky"], "default_exits": [{"room": 1, "unkn1": 145, "unkn2": 13, "x": 72, "y": 136, "name": "Grass Land 4 - 0 Exit 0", "access_rule": []}], "entity_load": [[3, 16], [43, 16], [14, 23], [40, 16], [61, 16], [4, 22]], "locations": ["Grass Land 4 - Enemy 1 (Loud)", "Grass Land 4 - Enemy 2 (Babut)", "Grass Land 4 - Enemy 3 (Rocky)", "Grass Land 4 - Star 1", "Grass Land 4 - Star 2", "Grass Land 4 - Star 3", "Grass Land 4 - Star 4", "Grass Land 4 - Star 5", "Grass Land 4 - Star 6", "Grass Land 4 - Star 7", "Grass Land 4 - Star 8", "Grass Land 4 - Star 9"], "music": 10}, {"name": "Grass Land 4 - 1", "level": 1, "stage": 4, "room": 1, "pointer": 3436401, "animal_pointers": [], "consumables": [{"idx": 12, "pointer": 290, "x": 1008, "y": 144, "etype": 22, "vtype": 2, "name": "Grass Land 4 - Maxim Tomato (Zebon Right)"}], "consumables_pointer": 368, "enemies": ["Kapar"], "default_exits": [{"room": 5, "unkn1": 58, "unkn2": 5, "x": 184, "y": 312, "name": "Grass Land 4 - 1 Exit 0", "access_rule": []}, {"room": 9, "unkn1": 42, "unkn2": 18, "x": 168, "y": 88, "name": "Grass Land 4 - 1 Exit 1", "access_rule": []}], "entity_load": [[43, 16], [10, 23], [6, 22], [14, 23], [2, 22], [67, 16]], "locations": ["Grass Land 4 - Enemy 4 (Kapar)", "Grass Land 4 - Star 10", "Grass Land 4 - Maxim Tomato (Zebon Right)"], "music": 10}, {"name": "Grass Land 4 - 2", "level": 1, "stage": 4, "room": 2, "pointer": 3039401, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 14, "unkn2": 4, "x": 56, "y": 72, "name": "Grass Land 4 - 2 Exit 0", "access_rule": []}], "entity_load": [[4, 27]], "locations": [], "music": 9}, {"name": "Grass Land 4 - 3", "level": 1, "stage": 4, "room": 3, "pointer": 3722714, "animal_pointers": [], "consumables": [{"idx": 23, "pointer": 280, "x": 856, "y": 224, "etype": 22, "vtype": 2, "name": "Grass Land 4 - Maxim Tomato (Gordo)"}, {"idx": 22, "pointer": 480, "x": 1352, "y": 112, "etype": 22, "vtype": 0, "name": "Grass Land 4 - 1-Up (Gordo)"}], "consumables_pointer": 288, "enemies": ["Glunk", "Oro"], "default_exits": [{"room": 4, "unkn1": 95, "unkn2": 5, "x": 72, "y": 200, "name": "Grass Land 4 - 3 Exit 0", "access_rule": []}], "entity_load": [[1, 16], [55, 16], [16, 16], [25, 16], [14, 23], [0, 22], [2, 22], [4, 22]], "locations": ["Grass Land 4 - Enemy 5 (Glunk)", "Grass Land 4 - Enemy 6 (Oro)", "Grass Land 4 - Star 11", "Grass Land 4 - Star 12", "Grass Land 4 - Star 13", "Grass Land 4 - Star 14", "Grass Land 4 - Star 15", "Grass Land 4 - Star 16", "Grass Land 4 - Star 17", "Grass Land 4 - Star 18", "Grass Land 4 - Star 19", "Grass Land 4 - Star 20", "Grass Land 4 - Star 21", "Grass Land 4 - Star 22", "Grass Land 4 - Star 23", "Grass Land 4 - Star 24", "Grass Land 4 - Star 25", "Grass Land 4 - Star 26", "Grass Land 4 - Maxim Tomato (Gordo)", "Grass Land 4 - 1-Up (Gordo)"], "music": 10}, {"name": "Grass Land 4 - 4", "level": 1, "stage": 4, "room": 4, "pointer": 3304980, "animal_pointers": [], "consumables": [{"idx": 32, "pointer": 208, "x": 488, "y": 64, "etype": 22, "vtype": 2, "name": "Grass Land 4 - Maxim Tomato (Cliff)"}], "consumables_pointer": 160, "enemies": [], "default_exits": [{"room": 8, "unkn1": 94, "unkn2": 9, "x": 40, "y": 152, "name": "Grass Land 4 - 4 Exit 0", "access_rule": []}], "entity_load": [[43, 16], [2, 22], [54, 16], [1, 16], [40, 16], [14, 23]], "locations": ["Grass Land 4 - Star 27", "Grass Land 4 - Maxim Tomato (Cliff)"], "music": 10}, {"name": "Grass Land 4 - 5", "level": 1, "stage": 4, "room": 5, "pointer": 3498127, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Peran"], "default_exits": [{"room": 2, "unkn1": 61, "unkn2": 13, "x": 56, "y": 72, "name": "Grass Land 4 - 5 Exit 0", "access_rule": []}, {"room": 7, "unkn1": 61, "unkn2": 18, "x": 56, "y": 200, "name": "Grass Land 4 - 5 Exit 1", "access_rule": ["Stone", "Stone Ability"]}], "entity_load": [[72, 16], [43, 16], [4, 22], [14, 23], [10, 23], [3, 16]], "locations": ["Grass Land 4 - Enemy 7 (Peran)", "Grass Land 4 - Star 28", "Grass Land 4 - Star 29", "Grass Land 4 - Star 30", "Grass Land 4 - Star 31", "Grass Land 4 - Star 32", "Grass Land 4 - Star 33", "Grass Land 4 - Star 34", "Grass Land 4 - Star 35", "Grass Land 4 - Star 36", "Grass Land 4 - Star 37"], "music": 10}, {"name": "Grass Land 4 - 6", "level": 1, "stage": 4, "room": 6, "pointer": 3160191, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 3, "unkn1": 28, "unkn2": 4, "x": 72, "y": 376, "name": "Grass Land 4 - 6 Exit 0", "access_rule": []}, {"room": 3, "unkn1": 28, "unkn2": 12, "x": 72, "y": 440, "name": "Grass Land 4 - 6 Exit 1", "access_rule": []}], "entity_load": [[3, 19], [6, 23]], "locations": [], "music": 10}, {"name": "Grass Land 4 - 7", "level": 1, "stage": 4, "room": 7, "pointer": 3035801, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 14, "unkn2": 12, "x": 56, "y": 200, "name": "Grass Land 4 - 7 Exit 0", "access_rule": []}], "entity_load": [[4, 27]], "locations": ["Grass Land 4 - Miniboss 1 (Boboo)"], "music": 4}, {"name": "Grass Land 4 - 8", "level": 1, "stage": 4, "room": 8, "pointer": 2989794, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 10, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Grass Land 4 - 8 Exit 0", "access_rule": []}], "entity_load": [[3, 19], [42, 19]], "locations": ["Grass Land 4 - Chao & Goku"], "music": 8}, {"name": "Grass Land 4 - 9", "level": 1, "stage": 4, "room": 9, "pointer": 3043518, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 1, "unkn1": 9, "unkn2": 5, "x": 696, "y": 296, "name": "Grass Land 4 - 9 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Grass Land 4 - Animal 1", "Grass Land 4 - Animal 2"], "music": 38}, {"name": "Grass Land 4 - 10", "level": 1, "stage": 4, "room": 10, "pointer": 2888425, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Grass Land 4 - Complete"], "music": 5}, {"name": "Grass Land 5 - 0", "level": 1, "stage": 5, "room": 0, "pointer": 3303565, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Propeller", "Broom Hatter", "Bouncy"], "default_exits": [{"room": 1, "unkn1": 120, "unkn2": 9, "x": 56, "y": 152, "name": "Grass Land 5 - 0 Exit 0", "access_rule": []}], "entity_load": [[13, 16], [4, 22], [14, 23], [6, 23], [11, 16], [89, 16]], "locations": ["Grass Land 5 - Enemy 1 (Propeller)", "Grass Land 5 - Enemy 2 (Broom Hatter)", "Grass Land 5 - Enemy 3 (Bouncy)", "Grass Land 5 - Star 1", "Grass Land 5 - Star 2", "Grass Land 5 - Star 3", "Grass Land 5 - Star 4", "Grass Land 5 - Star 5", "Grass Land 5 - Star 6", "Grass Land 5 - Star 7", "Grass Land 5 - Star 8"], "music": 11}, {"name": "Grass Land 5 - 1", "level": 1, "stage": 5, "room": 1, "pointer": 3048718, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Sir Kibble"], "default_exits": [{"room": 3, "unkn1": 18, "unkn2": 4, "x": 184, "y": 152, "name": "Grass Land 5 - 1 Exit 0", "access_rule": []}, {"room": 4, "unkn1": 21, "unkn2": 4, "x": 184, "y": 152, "name": "Grass Land 5 - 1 Exit 1", "access_rule": []}, {"room": 2, "unkn1": 36, "unkn2": 9, "x": 56, "y": 88, "name": "Grass Land 5 - 1 Exit 2", "access_rule": []}], "entity_load": [[27, 16], [14, 23]], "locations": ["Grass Land 5 - Enemy 4 (Sir Kibble)", "Grass Land 5 - Star 9", "Grass Land 5 - Star 10"], "music": 11}, {"name": "Grass Land 5 - 2", "level": 1, "stage": 5, "room": 2, "pointer": 3327019, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Waddle Dee", "Sasuke", "Nruff"], "default_exits": [{"room": 7, "unkn1": 121, "unkn2": 6, "x": 56, "y": 72, "name": "Grass Land 5 - 2 Exit 0", "access_rule": []}], "entity_load": [[0, 16], [14, 23], [4, 22], [30, 16], [15, 16], [1, 16]], "locations": ["Grass Land 5 - Enemy 5 (Waddle Dee)", "Grass Land 5 - Enemy 6 (Sasuke)", "Grass Land 5 - Enemy 7 (Nruff)", "Grass Land 5 - Star 11", "Grass Land 5 - Star 12", "Grass Land 5 - Star 13", "Grass Land 5 - Star 14", "Grass Land 5 - Star 15", "Grass Land 5 - Star 16"], "music": 11}, {"name": "Grass Land 5 - 3", "level": 1, "stage": 5, "room": 3, "pointer": 2966459, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 1, "unkn1": 10, "unkn2": 9, "x": 312, "y": 72, "name": "Grass Land 5 - 3 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Grass Land 5 - Animal 1", "Grass Land 5 - Animal 2"], "music": 38}, {"name": "Grass Land 5 - 4", "level": 1, "stage": 5, "room": 4, "pointer": 2973509, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 1, "unkn1": 10, "unkn2": 9, "x": 360, "y": 72, "name": "Grass Land 5 - 4 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Grass Land 5 - Animal 3", "Grass Land 5 - Animal 4"], "music": 38}, {"name": "Grass Land 5 - 5", "level": 1, "stage": 5, "room": 5, "pointer": 2962351, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Grass Land 5 - 5 Exit 0", "access_rule": []}], "entity_load": [[4, 19], [42, 19]], "locations": ["Grass Land 5 - Mine"], "music": 8}, {"name": "Grass Land 5 - 6", "level": 1, "stage": 5, "room": 6, "pointer": 2886738, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Grass Land 5 - Complete"], "music": 5}, {"name": "Grass Land 5 - 7", "level": 1, "stage": 5, "room": 7, "pointer": 3255423, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Tick"], "default_exits": [{"room": 5, "unkn1": 96, "unkn2": 9, "x": 56, "y": 152, "name": "Grass Land 5 - 7 Exit 0", "access_rule": []}], "entity_load": [[48, 16], [4, 22], [14, 23]], "locations": ["Grass Land 5 - Enemy 8 (Tick)", "Grass Land 5 - Star 17", "Grass Land 5 - Star 18", "Grass Land 5 - Star 19", "Grass Land 5 - Star 20", "Grass Land 5 - Star 21", "Grass Land 5 - Star 22", "Grass Land 5 - Star 23", "Grass Land 5 - Star 24", "Grass Land 5 - Star 25", "Grass Land 5 - Star 26", "Grass Land 5 - Star 27", "Grass Land 5 - Star 28", "Grass Land 5 - Star 29"], "music": 11}, {"name": "Grass Land 6 - 0", "level": 1, "stage": 6, "room": 0, "pointer": 3376872, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Como", "Togezo", "Bronto Burt", "Cappy"], "default_exits": [{"room": 6, "unkn1": 51, "unkn2": 9, "x": 216, "y": 152, "name": "Grass Land 6 - 0 Exit 0", "access_rule": []}, {"room": 1, "unkn1": 96, "unkn2": 9, "x": 216, "y": 1144, "name": "Grass Land 6 - 0 Exit 1", "access_rule": []}], "entity_load": [[12, 16], [18, 16], [2, 16], [41, 16], [4, 22], [14, 23]], "locations": ["Grass Land 6 - Enemy 1 (Como)", "Grass Land 6 - Enemy 2 (Togezo)", "Grass Land 6 - Enemy 3 (Bronto Burt)", "Grass Land 6 - Enemy 4 (Cappy)", "Grass Land 6 - Star 1", "Grass Land 6 - Star 2", "Grass Land 6 - Star 3", "Grass Land 6 - Star 4", "Grass Land 6 - Star 5", "Grass Land 6 - Star 6", "Grass Land 6 - Star 7", "Grass Land 6 - Star 8", "Grass Land 6 - Star 9"], "music": 20}, {"name": "Grass Land 6 - 1", "level": 1, "stage": 6, "room": 1, "pointer": 3395125, "animal_pointers": [], "consumables": [{"idx": 10, "pointer": 192, "x": 104, "y": 1144, "etype": 22, "vtype": 0, "name": "Grass Land 6 - 1-Up (Tower)"}], "consumables_pointer": 256, "enemies": ["Bobo", "Mariel"], "default_exits": [{"room": 2, "unkn1": 16, "unkn2": 5, "x": 72, "y": 88, "name": "Grass Land 6 - 1 Exit 0", "access_rule": []}], "entity_load": [[5, 16], [5, 19], [45, 16], [0, 22], [4, 22], [14, 23], [55, 16]], "locations": ["Grass Land 6 - Enemy 5 (Bobo)", "Grass Land 6 - Enemy 6 (Mariel)", "Grass Land 6 - Star 10", "Grass Land 6 - Star 11", "Grass Land 6 - Star 12", "Grass Land 6 - Star 13", "Grass Land 6 - Star 14", "Grass Land 6 - 1-Up (Tower)"], "music": 20}, {"name": "Grass Land 6 - 2", "level": 1, "stage": 6, "room": 2, "pointer": 3375177, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Yaban", "Broom Hatter"], "default_exits": [{"room": 3, "unkn1": 93, "unkn2": 6, "x": 200, "y": 152, "name": "Grass Land 6 - 2 Exit 0", "access_rule": []}, {"room": 7, "unkn1": 49, "unkn2": 7, "x": 216, "y": 104, "name": "Grass Land 6 - 2 Exit 1", "access_rule": []}], "entity_load": [[11, 16], [45, 16], [41, 16], [4, 22], [32, 16], [14, 23]], "locations": ["Grass Land 6 - Enemy 7 (Yaban)", "Grass Land 6 - Enemy 8 (Broom Hatter)", "Grass Land 6 - Star 15", "Grass Land 6 - Star 16"], "music": 20}, {"name": "Grass Land 6 - 3", "level": 1, "stage": 6, "room": 3, "pointer": 3322977, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Apolo", "Sasuke"], "default_exits": [{"room": 4, "unkn1": 12, "unkn2": 52, "x": 72, "y": 104, "name": "Grass Land 6 - 3 Exit 0", "access_rule": []}], "entity_load": [[41, 16], [49, 16], [30, 16], [14, 23], [4, 22]], "locations": ["Grass Land 6 - Enemy 9 (Apolo)", "Grass Land 6 - Enemy 10 (Sasuke)", "Grass Land 6 - Star 17", "Grass Land 6 - Star 18", "Grass Land 6 - Star 19", "Grass Land 6 - Star 20", "Grass Land 6 - Star 21", "Grass Land 6 - Star 22", "Grass Land 6 - Star 23", "Grass Land 6 - Star 24", "Grass Land 6 - Star 25"], "music": 20}, {"name": "Grass Land 6 - 4", "level": 1, "stage": 6, "room": 4, "pointer": 3490819, "animal_pointers": [], "consumables": [{"idx": 33, "pointer": 192, "x": 40, "y": 104, "etype": 22, "vtype": 1, "name": "Grass Land 6 - 1-Up (Falling)"}], "consumables_pointer": 176, "enemies": ["Rocky"], "default_exits": [{"room": 5, "unkn1": 145, "unkn2": 6, "x": 56, "y": 152, "name": "Grass Land 6 - 4 Exit 0", "access_rule": []}], "entity_load": [[5, 16], [4, 22], [49, 16], [61, 16], [3, 16], [1, 22], [14, 23]], "locations": ["Grass Land 6 - Enemy 11 (Rocky)", "Grass Land 6 - Star 26", "Grass Land 6 - Star 27", "Grass Land 6 - Star 28", "Grass Land 6 - Star 29", "Grass Land 6 - 1-Up (Falling)"], "music": 20}, {"name": "Grass Land 6 - 5", "level": 1, "stage": 6, "room": 5, "pointer": 3076769, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 8, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Grass Land 6 - 5 Exit 0", "access_rule": []}], "entity_load": [[5, 19], [42, 19]], "locations": ["Grass Land 6 - Pierre"], "music": 8}, {"name": "Grass Land 6 - 6", "level": 1, "stage": 6, "room": 6, "pointer": 3047576, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 0, "unkn1": 12, "unkn2": 9, "x": 840, "y": 152, "name": "Grass Land 6 - 6 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Grass Land 6 - Animal 1", "Grass Land 6 - Animal 2"], "music": 39}, {"name": "Grass Land 6 - 7", "level": 1, "stage": 6, "room": 7, "pointer": 3022909, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 12, "unkn2": 6, "x": 808, "y": 120, "name": "Grass Land 6 - 7 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Grass Land 6 - Animal 3", "Grass Land 6 - Animal 4"], "music": 38}, {"name": "Grass Land 6 - 8", "level": 1, "stage": 6, "room": 8, "pointer": 2884569, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Grass Land 6 - Complete"], "music": 5}, {"name": "Grass Land Boss - 0", "level": 1, "stage": 7, "room": 0, "pointer": 2984105, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[0, 18]], "locations": ["Grass Land - Boss (Whispy Woods) Purified", "Level 1 Boss - Defeated", "Level 1 Boss - Purified"], "music": 2}, {"name": "Ripple Field 1 - 0", "level": 2, "stage": 1, "room": 0, "pointer": 3279855, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Waddle Dee", "Glunk", "Broom Hatter", "Cappy"], "default_exits": [{"room": 2, "unkn1": 102, "unkn2": 8, "x": 56, "y": 152, "name": "Ripple Field 1 - 0 Exit 0", "access_rule": []}], "entity_load": [[16, 16], [0, 16], [12, 16], [11, 16], [14, 23]], "locations": ["Ripple Field 1 - Enemy 1 (Waddle Dee)", "Ripple Field 1 - Enemy 2 (Glunk)", "Ripple Field 1 - Enemy 3 (Broom Hatter)", "Ripple Field 1 - Enemy 4 (Cappy)", "Ripple Field 1 - Star 1", "Ripple Field 1 - Star 2", "Ripple Field 1 - Star 3", "Ripple Field 1 - Star 4", "Ripple Field 1 - Star 5", "Ripple Field 1 - Star 6"], "music": 15}, {"name": "Ripple Field 1 - 1", "level": 2, "stage": 1, "room": 1, "pointer": 3588688, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bronto Burt", "Rocky", "Poppy Bros Jr."], "default_exits": [{"room": 3, "unkn1": 146, "unkn2": 11, "x": 40, "y": 232, "name": "Ripple Field 1 - 1 Exit 0", "access_rule": []}, {"room": 5, "unkn1": 73, "unkn2": 16, "x": 200, "y": 184, "name": "Ripple Field 1 - 1 Exit 1", "access_rule": []}, {"room": 6, "unkn1": 108, "unkn2": 16, "x": 200, "y": 184, "name": "Ripple Field 1 - 1 Exit 2", "access_rule": []}, {"room": 7, "unkn1": 138, "unkn2": 16, "x": 200, "y": 184, "name": "Ripple Field 1 - 1 Exit 3", "access_rule": []}], "entity_load": [[11, 16], [2, 16], [3, 16], [7, 16]], "locations": ["Ripple Field 1 - Enemy 5 (Bronto Burt)", "Ripple Field 1 - Enemy 6 (Rocky)", "Ripple Field 1 - Enemy 7 (Poppy Bros Jr.)"], "music": 15}, {"name": "Ripple Field 1 - 2", "level": 2, "stage": 1, "room": 2, "pointer": 2955848, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 1, "unkn1": 18, "unkn2": 9, "x": 56, "y": 168, "name": "Ripple Field 1 - 2 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Ripple Field 1 - Animal 1", "Ripple Field 1 - Animal 2"], "music": 40}, {"name": "Ripple Field 1 - 3", "level": 2, "stage": 1, "room": 3, "pointer": 3558828, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bobin"], "default_exits": [{"room": 4, "unkn1": 171, "unkn2": 5, "x": 40, "y": 152, "name": "Ripple Field 1 - 3 Exit 0", "access_rule": []}], "entity_load": [[73, 16], [6, 22], [14, 23], [4, 22], [0, 16], [10, 23]], "locations": ["Ripple Field 1 - Enemy 8 (Bobin)", "Ripple Field 1 - Star 7", "Ripple Field 1 - Star 8", "Ripple Field 1 - Star 9", "Ripple Field 1 - Star 10", "Ripple Field 1 - Star 11", "Ripple Field 1 - Star 12", "Ripple Field 1 - Star 13", "Ripple Field 1 - Star 14", "Ripple Field 1 - Star 15", "Ripple Field 1 - Star 16"], "music": 15}, {"name": "Ripple Field 1 - 4", "level": 2, "stage": 1, "room": 4, "pointer": 2974271, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 8, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Ripple Field 1 - 4 Exit 0", "access_rule": []}], "entity_load": [[7, 19], [42, 19]], "locations": ["Ripple Field 1 - Kamuribana"], "music": 8}, {"name": "Ripple Field 1 - 5", "level": 2, "stage": 1, "room": 5, "pointer": 3051513, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 1, "unkn1": 11, "unkn2": 11, "x": 1192, "y": 264, "name": "Ripple Field 1 - 5 Exit 0", "access_rule": []}], "entity_load": [[7, 19], [14, 23]], "locations": ["Ripple Field 1 - Star 17", "Ripple Field 1 - Star 18"], "music": 15}, {"name": "Ripple Field 1 - 6", "level": 2, "stage": 1, "room": 6, "pointer": 3049838, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 1, "unkn1": 11, "unkn2": 11, "x": 1752, "y": 264, "name": "Ripple Field 1 - 6 Exit 0", "access_rule": []}], "entity_load": [[7, 19], [14, 23]], "locations": ["Ripple Field 1 - Star 19"], "music": 15}, {"name": "Ripple Field 1 - 7", "level": 2, "stage": 1, "room": 7, "pointer": 3066407, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 1, "unkn1": 11, "unkn2": 11, "x": 2232, "y": 264, "name": "Ripple Field 1 - 7 Exit 0", "access_rule": []}], "entity_load": [[7, 19]], "locations": [], "music": 15}, {"name": "Ripple Field 1 - 8", "level": 2, "stage": 1, "room": 8, "pointer": 2889148, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Ripple Field 1 - Complete"], "music": 5}, {"name": "Ripple Field 2 - 0", "level": 2, "stage": 2, "room": 0, "pointer": 3342336, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Togezo", "Coconut", "Blipper", "Sasuke"], "default_exits": [{"room": 1, "unkn1": 103, "unkn2": 15, "x": 56, "y": 104, "name": "Ripple Field 2 - 0 Exit 0", "access_rule": []}], "entity_load": [[5, 22], [34, 16], [30, 16], [21, 16], [14, 23], [4, 22], [18, 16]], "locations": ["Ripple Field 2 - Enemy 1 (Togezo)", "Ripple Field 2 - Enemy 2 (Coconut)", "Ripple Field 2 - Enemy 3 (Blipper)", "Ripple Field 2 - Enemy 4 (Sasuke)", "Ripple Field 2 - Star 1", "Ripple Field 2 - Star 2", "Ripple Field 2 - Star 3", "Ripple Field 2 - Star 4"], "music": 10}, {"name": "Ripple Field 2 - 1", "level": 2, "stage": 2, "room": 1, "pointer": 3084099, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 3, "unkn1": 23, "unkn2": 8, "x": 72, "y": 248, "name": "Ripple Field 2 - 1 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Ripple Field 2 - Animal 1", "Ripple Field 2 - Animal 2"], "music": 39}, {"name": "Ripple Field 2 - 2", "level": 2, "stage": 2, "room": 2, "pointer": 3451207, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Kany"], "default_exits": [{"room": 4, "unkn1": 31, "unkn2": 5, "x": 72, "y": 152, "name": "Ripple Field 2 - 2 Exit 0", "access_rule": []}, {"room": 6, "unkn1": 96, "unkn2": 6, "x": 56, "y": 152, "name": "Ripple Field 2 - 2 Exit 1", "access_rule": []}, {"room": 5, "unkn1": 56, "unkn2": 17, "x": 136, "y": 264, "name": "Ripple Field 2 - 2 Exit 2", "access_rule": []}], "entity_load": [[29, 16], [47, 16], [1, 16], [46, 16], [14, 23]], "locations": ["Ripple Field 2 - Enemy 5 (Kany)", "Ripple Field 2 - Star 5", "Ripple Field 2 - Star 6", "Ripple Field 2 - Star 7", "Ripple Field 2 - Star 8"], "music": 10}, {"name": "Ripple Field 2 - 3", "level": 2, "stage": 2, "room": 3, "pointer": 3674327, "animal_pointers": [], "consumables": [{"idx": 11, "pointer": 480, "x": 1384, "y": 200, "etype": 22, "vtype": 2, "name": "Ripple Field 2 - Maxim Tomato (Currents)"}, {"idx": 10, "pointer": 520, "x": 1456, "y": 200, "etype": 22, "vtype": 0, "name": "Ripple Field 2 - 1-Up (Currents)"}], "consumables_pointer": 128, "enemies": ["Glunk"], "default_exits": [{"room": 2, "unkn1": 134, "unkn2": 5, "x": 40, "y": 136, "name": "Ripple Field 2 - 3 Exit 0", "access_rule": []}], "entity_load": [[0, 22], [2, 22], [14, 23], [16, 16], [21, 16], [4, 22]], "locations": ["Ripple Field 2 - Enemy 6 (Glunk)", "Ripple Field 2 - Star 9", "Ripple Field 2 - Star 10", "Ripple Field 2 - Star 11", "Ripple Field 2 - Star 12", "Ripple Field 2 - Star 13", "Ripple Field 2 - Star 14", "Ripple Field 2 - Star 15", "Ripple Field 2 - Star 16", "Ripple Field 2 - Star 17", "Ripple Field 2 - Maxim Tomato (Currents)", "Ripple Field 2 - 1-Up (Currents)"], "music": 10}, {"name": "Ripple Field 2 - 4", "level": 2, "stage": 2, "room": 4, "pointer": 2972744, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 3, "unkn2": 9, "x": 520, "y": 88, "name": "Ripple Field 2 - 4 Exit 0", "access_rule": []}], "entity_load": [[8, 19]], "locations": [], "music": 10}, {"name": "Ripple Field 2 - 5", "level": 2, "stage": 2, "room": 5, "pointer": 3109710, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 16, "unkn2": 16, "x": 1048, "y": 280, "name": "Ripple Field 2 - 5 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Ripple Field 2 - Animal 3", "Ripple Field 2 - Animal 4"], "music": 38}, {"name": "Ripple Field 2 - 6", "level": 2, "stage": 2, "room": 6, "pointer": 2973127, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 7, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Ripple Field 2 - 6 Exit 0", "access_rule": []}], "entity_load": [[8, 19], [42, 19]], "locations": ["Ripple Field 2 - Bakasa"], "music": 8}, {"name": "Ripple Field 2 - 7", "level": 2, "stage": 2, "room": 7, "pointer": 2890353, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Ripple Field 2 - Complete"], "music": 5}, {"name": "Ripple Field 3 - 0", "level": 2, "stage": 3, "room": 0, "pointer": 3517254, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Raft Waddle Dee", "Kapar", "Blipper"], "default_exits": [{"room": 3, "unkn1": 105, "unkn2": 8, "x": 40, "y": 104, "name": "Ripple Field 3 - 0 Exit 0", "access_rule": []}], "entity_load": [[21, 16], [57, 16], [62, 16], [67, 16], [4, 22], [14, 23]], "locations": ["Ripple Field 3 - Enemy 1 (Raft Waddle Dee)", "Ripple Field 3 - Enemy 2 (Kapar)", "Ripple Field 3 - Enemy 3 (Blipper)", "Ripple Field 3 - Star 1", "Ripple Field 3 - Star 2", "Ripple Field 3 - Star 3", "Ripple Field 3 - Star 4"], "music": 18}, {"name": "Ripple Field 3 - 1", "level": 2, "stage": 3, "room": 1, "pointer": 3604480, "animal_pointers": [], "consumables": [{"idx": 10, "pointer": 320, "x": 832, "y": 152, "etype": 22, "vtype": 2, "name": "Ripple Field 3 - Maxim Tomato (Cove)"}, {"idx": 13, "pointer": 424, "x": 1128, "y": 384, "etype": 22, "vtype": 0, "name": "Ripple Field 3 - 1-Up (Cutter/Spark)"}], "consumables_pointer": 160, "enemies": ["Sparky", "Glunk", "Joe"], "default_exits": [{"room": 7, "unkn1": 80, "unkn2": 24, "x": 104, "y": 328, "name": "Ripple Field 3 - 1 Exit 0", "access_rule": []}], "entity_load": [[13, 23], [14, 23], [91, 16], [16, 16], [2, 22], [8, 16], [0, 22]], "locations": ["Ripple Field 3 - Enemy 4 (Sparky)", "Ripple Field 3 - Enemy 5 (Glunk)", "Ripple Field 3 - Enemy 6 (Joe)", "Ripple Field 3 - Star 5", "Ripple Field 3 - Star 6", "Ripple Field 3 - Star 7", "Ripple Field 3 - Star 8", "Ripple Field 3 - Star 9", "Ripple Field 3 - Star 10", "Ripple Field 3 - Star 11", "Ripple Field 3 - Maxim Tomato (Cove)", "Ripple Field 3 - 1-Up (Cutter/Spark)"], "music": 18}, {"name": "Ripple Field 3 - 2", "level": 2, "stage": 3, "room": 2, "pointer": 3715428, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bobo"], "default_exits": [{"room": 4, "unkn1": 118, "unkn2": 9, "x": 56, "y": 152, "name": "Ripple Field 3 - 2 Exit 0", "access_rule": []}], "entity_load": [[91, 16], [16, 16], [21, 16], [4, 22], [46, 16], [47, 16], [5, 16], [14, 23]], "locations": ["Ripple Field 3 - Enemy 7 (Bobo)", "Ripple Field 3 - Star 12", "Ripple Field 3 - Star 13", "Ripple Field 3 - Star 14", "Ripple Field 3 - Star 15", "Ripple Field 3 - Star 16", "Ripple Field 3 - Star 17", "Ripple Field 3 - Star 18", "Ripple Field 3 - Star 19", "Ripple Field 3 - Star 20", "Ripple Field 3 - Star 21"], "music": 18}, {"name": "Ripple Field 3 - 3", "level": 2, "stage": 3, "room": 3, "pointer": 3071919, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 1, "unkn1": 15, "unkn2": 6, "x": 56, "y": 104, "name": "Ripple Field 3 - 3 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Ripple Field 3 - Animal 1", "Ripple Field 3 - Animal 2"], "music": 39}, {"name": "Ripple Field 3 - 4", "level": 2, "stage": 3, "room": 4, "pointer": 2970810, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Ripple Field 3 - 4 Exit 0", "access_rule": []}], "entity_load": [[9, 19]], "locations": ["Ripple Field 3 - Elieel"], "music": 8}, {"name": "Ripple Field 3 - 5", "level": 2, "stage": 3, "room": 5, "pointer": 2987502, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 7, "unkn1": 15, "unkn2": 9, "x": 232, "y": 88, "name": "Ripple Field 3 - 5 Exit 0", "access_rule": []}], "entity_load": [[9, 19]], "locations": [], "music": 31}, {"name": "Ripple Field 3 - 6", "level": 2, "stage": 3, "room": 6, "pointer": 2888666, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Ripple Field 3 - Complete"], "music": 5}, {"name": "Ripple Field 3 - 7", "level": 2, "stage": 3, "room": 7, "pointer": 3161120, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 5, "unkn1": 3, "unkn2": 5, "x": 40, "y": 152, "name": "Ripple Field 3 - 7 Exit 0", "access_rule": []}, {"room": 2, "unkn1": 11, "unkn2": 20, "x": 56, "y": 216, "name": "Ripple Field 3 - 7 Exit 1", "access_rule": []}], "entity_load": [[57, 16]], "locations": [], "music": 18}, {"name": "Ripple Field 4 - 0", "level": 2, "stage": 4, "room": 0, "pointer": 3082540, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bukiset (Stone)", "Bukiset (Needle)", "Bukiset (Clean)", "Bukiset (Parasol)", "Mony"], "default_exits": [{"room": 6, "unkn1": 4, "unkn2": 16, "x": 72, "y": 232, "name": "Ripple Field 4 - 0 Exit 0", "access_rule": []}], "entity_load": [[14, 23], [79, 16], [80, 16], [81, 16], [4, 22], [20, 16], [77, 16]], "locations": ["Ripple Field 4 - Enemy 1 (Bukiset (Stone))", "Ripple Field 4 - Enemy 2 (Bukiset (Needle))", "Ripple Field 4 - Enemy 3 (Bukiset (Clean))", "Ripple Field 4 - Enemy 4 (Bukiset (Parasol))", "Ripple Field 4 - Enemy 5 (Mony)", "Ripple Field 4 - Star 1", "Ripple Field 4 - Star 2", "Ripple Field 4 - Star 3", "Ripple Field 4 - Star 4", "Ripple Field 4 - Star 5"], "music": 15}, {"name": "Ripple Field 4 - 1", "level": 2, "stage": 4, "room": 1, "pointer": 2964846, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 14, "unkn2": 8, "x": 72, "y": 88, "name": "Ripple Field 4 - 1 Exit 0", "access_rule": []}], "entity_load": [[0, 27]], "locations": ["Ripple Field 4 - Miniboss 1 (Captain Stitch)"], "music": 4}, {"name": "Ripple Field 4 - 2", "level": 2, "stage": 4, "room": 2, "pointer": 3018503, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bukiset (Burning)"], "default_exits": [{"room": 11, "unkn1": 25, "unkn2": 5, "x": 56, "y": 88, "name": "Ripple Field 4 - 2 Exit 0", "access_rule": []}, {"room": 11, "unkn1": 25, "unkn2": 15, "x": 56, "y": 216, "name": "Ripple Field 4 - 2 Exit 1", "access_rule": []}], "entity_load": [[10, 19], [76, 16]], "locations": ["Ripple Field 4 - Enemy 6 (Bukiset (Burning))"], "music": 15}, {"name": "Ripple Field 4 - 3", "level": 2, "stage": 4, "room": 3, "pointer": 2988166, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 4, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Ripple Field 4 - 3 Exit 0", "access_rule": []}], "entity_load": [[10, 19], [42, 19]], "locations": ["Ripple Field 4 - Toad & Little Toad"], "music": 8}, {"name": "Ripple Field 4 - 4", "level": 2, "stage": 4, "room": 4, "pointer": 2885533, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Ripple Field 4 - Complete"], "music": 5}, {"name": "Ripple Field 4 - 5", "level": 2, "stage": 4, "room": 5, "pointer": 3042349, "animal_pointers": [222, 230, 238], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 5, "unkn2": 5, "x": 360, "y": 120, "name": "Ripple Field 4 - 5 Exit 0", "access_rule": []}, {"room": 6, "unkn1": 15, "unkn2": 5, "x": 488, "y": 120, "name": "Ripple Field 4 - 5 Exit 1", "access_rule": []}, {"room": 5, "unkn1": 16, "unkn2": 5, "x": 104, "y": 88, "name": "Ripple Field 4 - 5 Exit 2", "access_rule": []}, {"room": 6, "unkn1": 10, "unkn2": 11, "x": 440, "y": 216, "name": "Ripple Field 4 - 5 Exit 3", "access_rule": []}], "entity_load": [], "locations": ["Ripple Field 4 - Animal 1", "Ripple Field 4 - Animal 2", "Ripple Field 4 - Animal 3"], "music": 40}, {"name": "Ripple Field 4 - 6", "level": 2, "stage": 4, "room": 6, "pointer": 3234805, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bobin", "Blipper"], "default_exits": [{"room": 5, "unkn1": 21, "unkn2": 7, "x": 104, "y": 88, "name": "Ripple Field 4 - 6 Exit 0", "access_rule": []}, {"room": 5, "unkn1": 31, "unkn2": 7, "x": 232, "y": 88, "name": "Ripple Field 4 - 6 Exit 1", "access_rule": []}, {"room": 5, "unkn1": 26, "unkn2": 13, "x": 184, "y": 184, "name": "Ripple Field 4 - 6 Exit 2", "access_rule": []}, {"room": 12, "unkn1": 48, "unkn2": 15, "x": 88, "y": 216, "name": "Ripple Field 4 - 6 Exit 3", "access_rule": []}], "entity_load": [[73, 16], [14, 23], [21, 16], [13, 23]], "locations": ["Ripple Field 4 - Enemy 7 (Bobin)", "Ripple Field 4 - Enemy 8 (Blipper)", "Ripple Field 4 - Star 6", "Ripple Field 4 - Star 7"], "music": 15}, {"name": "Ripple Field 4 - 7", "level": 2, "stage": 4, "room": 7, "pointer": 3155468, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 1, "unkn1": 11, "unkn2": 6, "x": 104, "y": 136, "name": "Ripple Field 4 - 7 Exit 0", "access_rule": []}, {"room": 2, "unkn1": 18, "unkn2": 9, "x": 72, "y": 248, "name": "Ripple Field 4 - 7 Exit 1", "access_rule": []}], "entity_load": [[14, 23], [1, 16], [0, 23]], "locations": ["Ripple Field 4 - Star 8", "Ripple Field 4 - Star 9", "Ripple Field 4 - Star 10", "Ripple Field 4 - Star 11", "Ripple Field 4 - Star 12", "Ripple Field 4 - Star 13", "Ripple Field 4 - Star 14", "Ripple Field 4 - Star 15", "Ripple Field 4 - Star 16", "Ripple Field 4 - Star 17", "Ripple Field 4 - Star 18", "Ripple Field 4 - Star 19", "Ripple Field 4 - Star 20", "Ripple Field 4 - Star 21"], "music": 15}, {"name": "Ripple Field 4 - 8", "level": 2, "stage": 4, "room": 8, "pointer": 3350031, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Como", "Oro"], "default_exits": [{"room": 7, "unkn1": 24, "unkn2": 22, "x": 184, "y": 440, "name": "Ripple Field 4 - 8 Exit 0", "access_rule": []}, {"room": 7, "unkn1": 34, "unkn2": 22, "x": 296, "y": 440, "name": "Ripple Field 4 - 8 Exit 1", "access_rule": []}, {"room": 9, "unkn1": 16, "unkn2": 72, "x": 168, "y": 152, "name": "Ripple Field 4 - 8 Exit 2", "access_rule": []}, {"room": 10, "unkn1": 23, "unkn2": 72, "x": 120, "y": 152, "name": "Ripple Field 4 - 8 Exit 3", "access_rule": []}], "entity_load": [[41, 16], [68, 16], [14, 23], [25, 16], [6, 23]], "locations": ["Ripple Field 4 - Enemy 9 (Como)", "Ripple Field 4 - Enemy 10 (Oro)", "Ripple Field 4 - Star 22", "Ripple Field 4 - Star 23", "Ripple Field 4 - Star 24", "Ripple Field 4 - Star 25", "Ripple Field 4 - Star 26", "Ripple Field 4 - Star 27", "Ripple Field 4 - Star 28"], "music": 15}, {"name": "Ripple Field 4 - 9", "level": 2, "stage": 4, "room": 9, "pointer": 3050397, "animal_pointers": [], "consumables": [{"idx": 29, "pointer": 200, "x": 88, "y": 200, "etype": 22, "vtype": 2, "name": "Ripple Field 4 - Maxim Tomato (Stone)"}], "consumables_pointer": 176, "enemies": ["Gansan"], "default_exits": [{"room": 8, "unkn1": 11, "unkn2": 9, "x": 264, "y": 1144, "name": "Ripple Field 4 - 9 Exit 0", "access_rule": []}], "entity_load": [[75, 16], [2, 22]], "locations": ["Ripple Field 4 - Enemy 11 (Gansan)", "Ripple Field 4 - Maxim Tomato (Stone)"], "music": 15}, {"name": "Ripple Field 4 - 10", "level": 2, "stage": 4, "room": 10, "pointer": 3052069, "animal_pointers": [], "consumables": [{"idx": 30, "pointer": 192, "x": 200, "y": 200, "etype": 22, "vtype": 0, "name": "Ripple Field 4 - 1-Up (Stone)"}], "consumables_pointer": 128, "enemies": [], "default_exits": [{"room": 8, "unkn1": 6, "unkn2": 9, "x": 376, "y": 1144, "name": "Ripple Field 4 - 10 Exit 0", "access_rule": []}], "entity_load": [[0, 22], [75, 16]], "locations": ["Ripple Field 4 - 1-Up (Stone)"], "music": 15}, {"name": "Ripple Field 4 - 11", "level": 2, "stage": 4, "room": 11, "pointer": 3386974, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Waddle Dee", "Kapar", "Squishy"], "default_exits": [{"room": 3, "unkn1": 146, "unkn2": 13, "x": 72, "y": 152, "name": "Ripple Field 4 - 11 Exit 0", "access_rule": []}], "entity_load": [[22, 16], [67, 16], [14, 23], [62, 16], [0, 16]], "locations": ["Ripple Field 4 - Enemy 12 (Waddle Dee)", "Ripple Field 4 - Enemy 13 (Kapar)", "Ripple Field 4 - Enemy 14 (Squishy)", "Ripple Field 4 - Star 29", "Ripple Field 4 - Star 30", "Ripple Field 4 - Star 31", "Ripple Field 4 - Star 32", "Ripple Field 4 - Star 33", "Ripple Field 4 - Star 34", "Ripple Field 4 - Star 35", "Ripple Field 4 - Star 36", "Ripple Field 4 - Star 37", "Ripple Field 4 - Star 38", "Ripple Field 4 - Star 39", "Ripple Field 4 - Star 40", "Ripple Field 4 - Star 41", "Ripple Field 4 - Star 42", "Ripple Field 4 - Star 43"], "music": 15}, {"name": "Ripple Field 4 - 12", "level": 2, "stage": 4, "room": 12, "pointer": 3168339, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Nidoo"], "default_exits": [{"room": 8, "unkn1": 67, "unkn2": 7, "x": 88, "y": 1224, "name": "Ripple Field 4 - 12 Exit 0", "access_rule": []}, {"room": 13, "unkn1": 75, "unkn2": 7, "x": 88, "y": 136, "name": "Ripple Field 4 - 12 Exit 1", "access_rule": []}], "entity_load": [[59, 16], [13, 23], [28, 16]], "locations": ["Ripple Field 4 - Enemy 15 (Nidoo)"], "music": 15}, {"name": "Ripple Field 4 - 13", "level": 2, "stage": 4, "room": 13, "pointer": 2958478, "animal_pointers": [], "consumables": [{"idx": 54, "pointer": 264, "x": 216, "y": 136, "etype": 22, "vtype": 2, "name": "Ripple Field 4 - Maxim Tomato (Dark)"}], "consumables_pointer": 128, "enemies": [], "default_exits": [{"room": 12, "unkn1": 4, "unkn2": 8, "x": 1192, "y": 120, "name": "Ripple Field 4 - 13 Exit 0", "access_rule": []}], "entity_load": [[14, 23], [2, 22], [59, 16]], "locations": ["Ripple Field 4 - Star 44", "Ripple Field 4 - Star 45", "Ripple Field 4 - Star 46", "Ripple Field 4 - Star 47", "Ripple Field 4 - Star 48", "Ripple Field 4 - Star 49", "Ripple Field 4 - Star 50", "Ripple Field 4 - Star 51", "Ripple Field 4 - Maxim Tomato (Dark)"], "music": 15}, {"name": "Ripple Field 5 - 0", "level": 2, "stage": 5, "room": 0, "pointer": 3240369, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 9, "unkn2": 43, "x": 88, "y": 344, "name": "Ripple Field 5 - 0 Exit 0", "access_rule": []}], "entity_load": [[14, 23]], "locations": ["Ripple Field 5 - Star 1", "Ripple Field 5 - Star 2", "Ripple Field 5 - Star 3", "Ripple Field 5 - Star 4", "Ripple Field 5 - Star 5", "Ripple Field 5 - Star 6"], "music": 16}, {"name": "Ripple Field 5 - 1", "level": 2, "stage": 5, "room": 1, "pointer": 3547528, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 15, "unkn2": 4, "x": 184, "y": 344, "name": "Ripple Field 5 - 1 Exit 0", "access_rule": []}], "entity_load": [[1, 16], [14, 23]], "locations": ["Ripple Field 5 - Star 7", "Ripple Field 5 - Star 8", "Ripple Field 5 - Star 9", "Ripple Field 5 - Star 10", "Ripple Field 5 - Star 11", "Ripple Field 5 - Star 12", "Ripple Field 5 - Star 13", "Ripple Field 5 - Star 14", "Ripple Field 5 - Star 15", "Ripple Field 5 - Star 16", "Ripple Field 5 - Star 17"], "music": 16}, {"name": "Ripple Field 5 - 2", "level": 2, "stage": 5, "room": 2, "pointer": 3611327, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Glunk", "Joe"], "default_exits": [{"room": 4, "unkn1": 95, "unkn2": 21, "x": 56, "y": 184, "name": "Ripple Field 5 - 2 Exit 0", "access_rule": []}], "entity_load": [[91, 16], [16, 16], [14, 23]], "locations": ["Ripple Field 5 - Enemy 1 (Glunk)", "Ripple Field 5 - Enemy 2 (Joe)", "Ripple Field 5 - Star 18", "Ripple Field 5 - Star 19", "Ripple Field 5 - Star 20", "Ripple Field 5 - Star 21", "Ripple Field 5 - Star 22", "Ripple Field 5 - Star 23", "Ripple Field 5 - Star 24", "Ripple Field 5 - Star 25", "Ripple Field 5 - Star 26", "Ripple Field 5 - Star 27", "Ripple Field 5 - Star 28", "Ripple Field 5 - Star 29", "Ripple Field 5 - Star 30", "Ripple Field 5 - Star 31"], "music": 16}, {"name": "Ripple Field 5 - 3", "level": 2, "stage": 5, "room": 3, "pointer": 3926157, "animal_pointers": [], "consumables": [{"idx": 32, "pointer": 1258, "x": 1488, "y": 192, "etype": 22, "vtype": 2, "name": "Ripple Field 5 - Maxim Tomato (Currents)"}, {"idx": 31, "pointer": 1290, "x": 1520, "y": 192, "etype": 22, "vtype": 0, "name": "Ripple Field 5 - 1-Up (Currents)"}], "consumables_pointer": 128, "enemies": ["Bobin", "Mony", "Squishy"], "default_exits": [{"room": 8, "unkn1": 4, "unkn2": 38, "x": 152, "y": 152, "name": "Ripple Field 5 - 3 Exit 0", "access_rule": ["Kine", "Kine Spawn"]}, {"room": 1, "unkn1": 95, "unkn2": 38, "x": 248, "y": 1064, "name": "Ripple Field 5 - 3 Exit 1", "access_rule": []}], "entity_load": [[0, 22], [2, 22], [6, 22], [14, 23], [1, 16], [73, 16], [22, 16], [20, 16]], "locations": ["Ripple Field 5 - Enemy 3 (Bobin)", "Ripple Field 5 - Enemy 4 (Mony)", "Ripple Field 5 - Enemy 5 (Squishy)", "Ripple Field 5 - Star 32", "Ripple Field 5 - Star 33", "Ripple Field 5 - Star 34", "Ripple Field 5 - Star 35", "Ripple Field 5 - Star 36", "Ripple Field 5 - Star 37", "Ripple Field 5 - Maxim Tomato (Currents)", "Ripple Field 5 - 1-Up (Currents)"], "music": 16}, {"name": "Ripple Field 5 - 4", "level": 2, "stage": 5, "room": 4, "pointer": 3026639, "animal_pointers": [192], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 5, "unkn1": 14, "unkn2": 4, "x": 232, "y": 152, "name": "Ripple Field 5 - 4 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Ripple Field 5 - Animal 1"], "music": 40}, {"name": "Ripple Field 5 - 5", "level": 2, "stage": 5, "room": 5, "pointer": 3207333, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 4, "unkn1": 4, "unkn2": 9, "x": 56, "y": 72, "name": "Ripple Field 5 - 5 Exit 0", "access_rule": []}, {"room": 7, "unkn1": 24, "unkn2": 9, "x": 120, "y": 552, "name": "Ripple Field 5 - 5 Exit 1", "access_rule": ["Kine", "Kine Spawn"]}], "entity_load": [[14, 23]], "locations": ["Ripple Field 5 - Star 38", "Ripple Field 5 - Star 39", "Ripple Field 5 - Star 40", "Ripple Field 5 - Star 41", "Ripple Field 5 - Star 42"], "music": 16}, {"name": "Ripple Field 5 - 6", "level": 2, "stage": 5, "room": 6, "pointer": 3485896, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Yaban", "Broom Hatter", "Bouncy"], "default_exits": [{"room": 9, "unkn1": 121, "unkn2": 11, "x": 56, "y": 152, "name": "Ripple Field 5 - 6 Exit 0", "access_rule": []}], "entity_load": [[6, 22], [22, 16], [14, 23], [13, 16], [11, 16], [32, 16]], "locations": ["Ripple Field 5 - Enemy 6 (Yaban)", "Ripple Field 5 - Enemy 7 (Broom Hatter)", "Ripple Field 5 - Enemy 8 (Bouncy)", "Ripple Field 5 - Star 43", "Ripple Field 5 - Star 44", "Ripple Field 5 - Star 45"], "music": 16}, {"name": "Ripple Field 5 - 7", "level": 2, "stage": 5, "room": 7, "pointer": 3752698, "animal_pointers": [], "consumables": [{"idx": 53, "pointer": 418, "x": 1512, "y": 608, "etype": 22, "vtype": 2, "name": "Ripple Field 5 - Maxim Tomato (Exit)"}], "consumables_pointer": 352, "enemies": ["Sparky", "Rocky", "Babut"], "default_exits": [{"room": 10, "unkn1": 45, "unkn2": 31, "x": 152, "y": 152, "name": "Ripple Field 5 - 7 Exit 0", "access_rule": []}, {"room": 3, "unkn1": 94, "unkn2": 40, "x": 88, "y": 200, "name": "Ripple Field 5 - 7 Exit 1", "access_rule": []}], "entity_load": [[3, 16], [43, 16], [8, 16], [22, 16], [14, 23], [2, 22]], "locations": ["Ripple Field 5 - Enemy 9 (Sparky)", "Ripple Field 5 - Enemy 10 (Rocky)", "Ripple Field 5 - Enemy 11 (Babut)", "Ripple Field 5 - Star 46", "Ripple Field 5 - Star 47", "Ripple Field 5 - Star 48", "Ripple Field 5 - Star 49", "Ripple Field 5 - Maxim Tomato (Exit)"], "music": 16}, {"name": "Ripple Field 5 - 8", "level": 2, "stage": 5, "room": 8, "pointer": 3044682, "animal_pointers": [192], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 3, "unkn1": 8, "unkn2": 9, "x": 88, "y": 616, "name": "Ripple Field 5 - 8 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Ripple Field 5 - Animal 2"], "music": 39}, {"name": "Ripple Field 5 - 9", "level": 2, "stage": 5, "room": 9, "pointer": 2963193, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 11, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Ripple Field 5 - 9 Exit 0", "access_rule": []}], "entity_load": [[11, 19], [42, 19]], "locations": ["Ripple Field 5 - Mama Pitch"], "music": 8}, {"name": "Ripple Field 5 - 10", "level": 2, "stage": 5, "room": 10, "pointer": 3042934, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Galbo"], "default_exits": [{"room": 7, "unkn1": 8, "unkn2": 9, "x": 712, "y": 504, "name": "Ripple Field 5 - 10 Exit 0", "access_rule": []}], "entity_load": [[26, 16], [14, 23]], "locations": ["Ripple Field 5 - Enemy 12 (Galbo)", "Ripple Field 5 - Star 50", "Ripple Field 5 - Star 51"], "music": 16}, {"name": "Ripple Field 5 - 11", "level": 2, "stage": 5, "room": 11, "pointer": 2886256, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Ripple Field 5 - Complete"], "music": 5}, {"name": "Ripple Field 6 - 0", "level": 2, "stage": 6, "room": 0, "pointer": 2949576, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Kany"], "default_exits": [{"room": 1, "unkn1": 56, "unkn2": 7, "x": 40, "y": 152, "name": "Ripple Field 6 - 0 Exit 0", "access_rule": []}], "entity_load": [[29, 16], [13, 23]], "locations": ["Ripple Field 6 - Enemy 1 (Kany)"], "music": 15}, {"name": "Ripple Field 6 - 1", "level": 2, "stage": 6, "room": 1, "pointer": 2971200, "animal_pointers": [192, 200, 208], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 7, "unkn1": 23, "unkn2": 9, "x": 56, "y": 264, "name": "Ripple Field 6 - 1 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Ripple Field 6 - Animal 1", "Ripple Field 6 - Animal 2", "Ripple Field 6 - Animal 3"], "music": 38}, {"name": "Ripple Field 6 - 2", "level": 2, "stage": 6, "room": 2, "pointer": 3637749, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 8, "unkn1": 95, "unkn2": 9, "x": 104, "y": 872, "name": "Ripple Field 6 - 2 Exit 0", "access_rule": []}, {"room": 4, "unkn1": 50, "unkn2": 22, "x": 184, "y": 88, "name": "Ripple Field 6 - 2 Exit 1", "access_rule": []}, {"room": 3, "unkn1": 45, "unkn2": 26, "x": 88, "y": 88, "name": "Ripple Field 6 - 2 Exit 2", "access_rule": []}, {"room": 3, "unkn1": 55, "unkn2": 26, "x": 248, "y": 88, "name": "Ripple Field 6 - 2 Exit 3", "access_rule": []}], "entity_load": [[52, 16], [13, 23]], "locations": [], "music": 15}, {"name": "Ripple Field 6 - 3", "level": 2, "stage": 6, "room": 3, "pointer": 3092564, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 4, "unkn2": 5, "x": 744, "y": 424, "name": "Ripple Field 6 - 3 Exit 0", "access_rule": []}, {"room": 2, "unkn1": 16, "unkn2": 5, "x": 872, "y": 424, "name": "Ripple Field 6 - 3 Exit 1", "access_rule": []}], "entity_load": [], "locations": [], "music": 15}, {"name": "Ripple Field 6 - 4", "level": 2, "stage": 6, "room": 4, "pointer": 3133247, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 10, "unkn2": 5, "x": 824, "y": 360, "name": "Ripple Field 6 - 4 Exit 0", "access_rule": []}], "entity_load": [[12, 19]], "locations": [], "music": 15}, {"name": "Ripple Field 6 - 5", "level": 2, "stage": 6, "room": 5, "pointer": 3507762, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 76, "unkn2": 4, "x": 680, "y": 72, "name": "Ripple Field 6 - 5 Exit 0", "access_rule": []}, {"room": 6, "unkn1": 49, "unkn2": 6, "x": 440, "y": 104, "name": "Ripple Field 6 - 5 Exit 1", "access_rule": []}, {"room": 6, "unkn1": 49, "unkn2": 10, "x": 440, "y": 168, "name": "Ripple Field 6 - 5 Exit 2", "access_rule": []}, {"room": 2, "unkn1": 88, "unkn2": 10, "x": 104, "y": 152, "name": "Ripple Field 6 - 5 Exit 3", "access_rule": []}, {"room": 6, "unkn1": 22, "unkn2": 12, "x": 200, "y": 200, "name": "Ripple Field 6 - 5 Exit 4", "access_rule": []}, {"room": 6, "unkn1": 76, "unkn2": 12, "x": 680, "y": 200, "name": "Ripple Field 6 - 5 Exit 5", "access_rule": []}, {"room": 6, "unkn1": 76, "unkn2": 16, "x": 680, "y": 264, "name": "Ripple Field 6 - 5 Exit 6", "access_rule": []}], "entity_load": [[10, 23]], "locations": [], "music": 15}, {"name": "Ripple Field 6 - 6", "level": 2, "stage": 6, "room": 6, "pointer": 3211264, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 5, "unkn1": 4, "unkn2": 10, "x": 72, "y": 168, "name": "Ripple Field 6 - 6 Exit 0", "access_rule": []}], "entity_load": [[6, 23]], "locations": [], "music": 15}, {"name": "Ripple Field 6 - 7", "level": 2, "stage": 6, "room": 7, "pointer": 3586039, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["KeKe", "Kapar", "Rocky", "Poppy Bros Jr."], "default_exits": [{"room": 5, "unkn1": 134, "unkn2": 16, "x": 72, "y": 168, "name": "Ripple Field 6 - 7 Exit 0", "access_rule": []}], "entity_load": [[14, 23], [3, 16], [67, 16], [51, 16], [7, 16]], "locations": ["Ripple Field 6 - Enemy 2 (KeKe)", "Ripple Field 6 - Enemy 3 (Kapar)", "Ripple Field 6 - Enemy 4 (Rocky)", "Ripple Field 6 - Enemy 5 (Poppy Bros Jr.)", "Ripple Field 6 - Star 1", "Ripple Field 6 - Star 2", "Ripple Field 6 - Star 3", "Ripple Field 6 - Star 4", "Ripple Field 6 - Star 5", "Ripple Field 6 - Star 6", "Ripple Field 6 - Star 7", "Ripple Field 6 - Star 8", "Ripple Field 6 - Star 9", "Ripple Field 6 - Star 10", "Ripple Field 6 - Star 11", "Ripple Field 6 - Star 12", "Ripple Field 6 - Star 13", "Ripple Field 6 - Star 14"], "music": 15}, {"name": "Ripple Field 6 - 8", "level": 2, "stage": 6, "room": 8, "pointer": 3621483, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Propeller", "Coconut", "Sasuke", "Nruff"], "default_exits": [{"room": 10, "unkn1": 70, "unkn2": 11, "x": 56, "y": 152, "name": "Ripple Field 6 - 8 Exit 0", "access_rule": []}, {"room": 9, "unkn1": 26, "unkn2": 54, "x": 72, "y": 152, "name": "Ripple Field 6 - 8 Exit 1", "access_rule": []}], "entity_load": [[89, 16], [15, 16], [30, 16], [34, 16], [14, 23]], "locations": ["Ripple Field 6 - Enemy 6 (Propeller)", "Ripple Field 6 - Enemy 7 (Coconut)", "Ripple Field 6 - Enemy 8 (Sasuke)", "Ripple Field 6 - Enemy 9 (Nruff)", "Ripple Field 6 - Star 15", "Ripple Field 6 - Star 16", "Ripple Field 6 - Star 17", "Ripple Field 6 - Star 18", "Ripple Field 6 - Star 19", "Ripple Field 6 - Star 20", "Ripple Field 6 - Star 21", "Ripple Field 6 - Star 22", "Ripple Field 6 - Star 23"], "music": 15}, {"name": "Ripple Field 6 - 9", "level": 2, "stage": 6, "room": 9, "pointer": 2954523, "animal_pointers": [192], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 8, "unkn1": 3, "unkn2": 9, "x": 408, "y": 872, "name": "Ripple Field 6 - 9 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Ripple Field 6 - Animal 4"], "music": 39}, {"name": "Ripple Field 6 - 10", "level": 2, "stage": 6, "room": 10, "pointer": 3069438, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 11, "unkn1": 15, "unkn2": 9, "x": 72, "y": 120, "name": "Ripple Field 6 - 10 Exit 0", "access_rule": []}], "entity_load": [[12, 19], [42, 19]], "locations": ["Ripple Field 6 - HB-002"], "music": 8}, {"name": "Ripple Field 6 - 11", "level": 2, "stage": 6, "room": 11, "pointer": 2886497, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Ripple Field 6 - Complete"], "music": 5}, {"name": "Ripple Field Boss - 0", "level": 2, "stage": 7, "room": 0, "pointer": 3157370, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[7, 18]], "locations": ["Ripple Field - Boss (Acro) Purified", "Level 2 Boss - Defeated", "Level 2 Boss - Purified"], "music": 2}, {"name": "Sand Canyon 1 - 0", "level": 3, "stage": 1, "room": 0, "pointer": 3524267, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bronto Burt", "Galbo"], "default_exits": [{"room": 5, "unkn1": 196, "unkn2": 7, "x": 72, "y": 152, "name": "Sand Canyon 1 - 0 Exit 0", "access_rule": []}], "entity_load": [[14, 19], [1, 16], [26, 16], [2, 16]], "locations": ["Sand Canyon 1 - Enemy 1 (Bronto Burt)", "Sand Canyon 1 - Enemy 2 (Galbo)"], "music": 13}, {"name": "Sand Canyon 1 - 1", "level": 3, "stage": 1, "room": 1, "pointer": 3163860, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 34, "unkn2": 5, "x": 104, "y": 408, "name": "Sand Canyon 1 - 1 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Sand Canyon 1 - Animal 1", "Sand Canyon 1 - Animal 2"], "music": 38}, {"name": "Sand Canyon 1 - 2", "level": 3, "stage": 1, "room": 2, "pointer": 3512532, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Oro"], "default_exits": [{"room": 3, "unkn1": 73, "unkn2": 6, "x": 56, "y": 120, "name": "Sand Canyon 1 - 2 Exit 0", "access_rule": []}], "entity_load": [[14, 16], [26, 16], [25, 16], [4, 22], [14, 23], [1, 23], [0, 23], [4, 23]], "locations": ["Sand Canyon 1 - Enemy 3 (Oro)", "Sand Canyon 1 - Star 1", "Sand Canyon 1 - Star 2", "Sand Canyon 1 - Star 3", "Sand Canyon 1 - Star 4"], "music": 13}, {"name": "Sand Canyon 1 - 3", "level": 3, "stage": 1, "room": 3, "pointer": 3719146, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Sparky", "Propeller", "Gansan", "Babut"], "default_exits": [{"room": 4, "unkn1": 25, "unkn2": 73, "x": 72, "y": 280, "name": "Sand Canyon 1 - 3 Exit 0", "access_rule": []}], "entity_load": [[89, 16], [75, 16], [43, 16], [8, 16]], "locations": ["Sand Canyon 1 - Enemy 4 (Sparky)", "Sand Canyon 1 - Enemy 5 (Propeller)", "Sand Canyon 1 - Enemy 6 (Gansan)", "Sand Canyon 1 - Enemy 7 (Babut)"], "music": 13}, {"name": "Sand Canyon 1 - 4", "level": 3, "stage": 1, "room": 4, "pointer": 3421212, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Loud", "Dogon", "Bouncy", "Pteran"], "default_exits": [{"room": 7, "unkn1": 196, "unkn2": 8, "x": 56, "y": 152, "name": "Sand Canyon 1 - 4 Exit 0", "access_rule": []}], "entity_load": [[90, 16], [13, 16], [40, 16], [14, 23], [39, 16]], "locations": ["Sand Canyon 1 - Enemy 8 (Loud)", "Sand Canyon 1 - Enemy 9 (Dogon)", "Sand Canyon 1 - Enemy 10 (Bouncy)", "Sand Canyon 1 - Enemy 11 (Pteran)", "Sand Canyon 1 - Star 5", "Sand Canyon 1 - Star 6", "Sand Canyon 1 - Star 7", "Sand Canyon 1 - Star 8", "Sand Canyon 1 - Star 9", "Sand Canyon 1 - Star 10"], "music": 13}, {"name": "Sand Canyon 1 - 5", "level": 3, "stage": 1, "room": 5, "pointer": 3203325, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Polof"], "default_exits": [{"room": 6, "unkn1": 32, "unkn2": 9, "x": 104, "y": 152, "name": "Sand Canyon 1 - 5 Exit 0", "access_rule": []}, {"room": 1, "unkn1": 46, "unkn2": 9, "x": 56, "y": 344, "name": "Sand Canyon 1 - 5 Exit 1", "access_rule": []}], "entity_load": [[9, 16]], "locations": ["Sand Canyon 1 - Enemy 12 (Polof)"], "music": 13}, {"name": "Sand Canyon 1 - 6", "level": 3, "stage": 1, "room": 6, "pointer": 3138524, "animal_pointers": [], "consumables": [{"idx": 23, "pointer": 248, "x": 168, "y": 104, "etype": 22, "vtype": 0, "name": "Sand Canyon 1 - 1-Up (Polof)"}], "consumables_pointer": 128, "enemies": [], "default_exits": [{"room": 5, "unkn1": 5, "unkn2": 9, "x": 536, "y": 152, "name": "Sand Canyon 1 - 6 Exit 0", "access_rule": []}], "entity_load": [[14, 23], [0, 22]], "locations": ["Sand Canyon 1 - Star 11", "Sand Canyon 1 - Star 12", "Sand Canyon 1 - Star 13", "Sand Canyon 1 - Star 14", "Sand Canyon 1 - Star 15", "Sand Canyon 1 - Star 16", "Sand Canyon 1 - Star 17", "Sand Canyon 1 - Star 18", "Sand Canyon 1 - Star 19", "Sand Canyon 1 - Star 20", "Sand Canyon 1 - Star 21", "Sand Canyon 1 - Star 22", "Sand Canyon 1 - 1-Up (Polof)"], "music": 13}, {"name": "Sand Canyon 1 - 7", "level": 3, "stage": 1, "room": 7, "pointer": 2988822, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 8, "unkn1": 15, "unkn2": 9, "x": 72, "y": 120, "name": "Sand Canyon 1 - 7 Exit 0", "access_rule": []}], "entity_load": [[14, 19], [42, 19]], "locations": ["Sand Canyon 1 - Geromuzudake"], "music": 8}, {"name": "Sand Canyon 1 - 8", "level": 3, "stage": 1, "room": 8, "pointer": 2885292, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Sand Canyon 1 - Complete"], "music": 5}, {"name": "Sand Canyon 2 - 0", "level": 3, "stage": 2, "room": 0, "pointer": 3668370, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["KeKe", "Doka", "Boten"], "default_exits": [{"room": 1, "unkn1": 178, "unkn2": 8, "x": 184, "y": 104, "name": "Sand Canyon 2 - 0 Exit 0", "access_rule": []}, {"room": 3, "unkn1": 244, "unkn2": 11, "x": 56, "y": 152, "name": "Sand Canyon 2 - 0 Exit 1", "access_rule": []}], "entity_load": [[35, 16], [33, 16], [14, 23], [51, 16], [47, 16], [46, 16]], "locations": ["Sand Canyon 2 - Enemy 1 (KeKe)", "Sand Canyon 2 - Enemy 2 (Doka)", "Sand Canyon 2 - Enemy 3 (Boten)", "Sand Canyon 2 - Star 1", "Sand Canyon 2 - Star 2", "Sand Canyon 2 - Star 3", "Sand Canyon 2 - Star 4", "Sand Canyon 2 - Star 5", "Sand Canyon 2 - Star 6", "Sand Canyon 2 - Star 7", "Sand Canyon 2 - Star 8", "Sand Canyon 2 - Star 9", "Sand Canyon 2 - Star 10", "Sand Canyon 2 - Star 11"], "music": 21}, {"name": "Sand Canyon 2 - 1", "level": 3, "stage": 2, "room": 1, "pointer": 2952738, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 0, "unkn1": 10, "unkn2": 6, "x": 2872, "y": 136, "name": "Sand Canyon 2 - 1 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Sand Canyon 2 - Animal 1", "Sand Canyon 2 - Animal 2"], "music": 40}, {"name": "Sand Canyon 2 - 2", "level": 3, "stage": 2, "room": 2, "pointer": 3531156, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Propeller"], "default_exits": [{"room": 9, "unkn1": 45, "unkn2": 60, "x": 88, "y": 184, "name": "Sand Canyon 2 - 2 Exit 0", "access_rule": []}], "entity_load": [[6, 23], [89, 16], [14, 23]], "locations": ["Sand Canyon 2 - Enemy 4 (Propeller)", "Sand Canyon 2 - Star 12", "Sand Canyon 2 - Star 13", "Sand Canyon 2 - Star 14", "Sand Canyon 2 - Star 15", "Sand Canyon 2 - Star 16", "Sand Canyon 2 - Star 17"], "music": 21}, {"name": "Sand Canyon 2 - 3", "level": 3, "stage": 2, "room": 3, "pointer": 3263731, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Waddle Dee", "Sparky", "Sasuke", "Como"], "default_exits": [{"room": 4, "unkn1": 63, "unkn2": 5, "x": 88, "y": 184, "name": "Sand Canyon 2 - 3 Exit 0", "access_rule": []}, {"room": 4, "unkn1": 68, "unkn2": 5, "x": 184, "y": 184, "name": "Sand Canyon 2 - 3 Exit 1", "access_rule": []}, {"room": 4, "unkn1": 73, "unkn2": 5, "x": 248, "y": 184, "name": "Sand Canyon 2 - 3 Exit 2", "access_rule": []}, {"room": 5, "unkn1": 130, "unkn2": 9, "x": 72, "y": 312, "name": "Sand Canyon 2 - 3 Exit 3", "access_rule": []}], "entity_load": [[41, 16], [8, 16], [30, 16], [0, 16]], "locations": ["Sand Canyon 2 - Enemy 5 (Waddle Dee)", "Sand Canyon 2 - Enemy 6 (Sparky)", "Sand Canyon 2 - Enemy 7 (Sasuke)", "Sand Canyon 2 - Enemy 8 (Como)"], "music": 21}, {"name": "Sand Canyon 2 - 4", "level": 3, "stage": 2, "room": 4, "pointer": 3076300, "animal_pointers": [], "consumables": [{"idx": 19, "pointer": 228, "x": 168, "y": 72, "etype": 22, "vtype": 0, "name": "Sand Canyon 2 - 1-Up (Enclave)"}], "consumables_pointer": 128, "enemies": [], "default_exits": [{"room": 3, "unkn1": 5, "unkn2": 11, "x": 1016, "y": 88, "name": "Sand Canyon 2 - 4 Exit 0", "access_rule": []}, {"room": 3, "unkn1": 10, "unkn2": 11, "x": 1112, "y": 88, "name": "Sand Canyon 2 - 4 Exit 1", "access_rule": []}, {"room": 3, "unkn1": 15, "unkn2": 11, "x": 1176, "y": 88, "name": "Sand Canyon 2 - 4 Exit 2", "access_rule": []}], "entity_load": [[14, 23], [0, 22], [4, 22]], "locations": ["Sand Canyon 2 - Star 18", "Sand Canyon 2 - Star 19", "Sand Canyon 2 - 1-Up (Enclave)"], "music": 21}, {"name": "Sand Canyon 2 - 5", "level": 3, "stage": 2, "room": 5, "pointer": 3302133, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bukiset (Ice)", "Bukiset (Needle)", "Bukiset (Clean)", "Bukiset (Parasol)", "Bukiset (Spark)", "Bukiset (Cutter)"], "default_exits": [{"room": 6, "unkn1": 63, "unkn2": 15, "x": 120, "y": 216, "name": "Sand Canyon 2 - 5 Exit 0", "access_rule": []}, {"room": 8, "unkn1": 71, "unkn2": 18, "x": 152, "y": 136, "name": "Sand Canyon 2 - 5 Exit 1", "access_rule": []}, {"room": 2, "unkn1": 130, "unkn2": 19, "x": 72, "y": 952, "name": "Sand Canyon 2 - 5 Exit 2", "access_rule": []}, {"room": 7, "unkn1": 56, "unkn2": 23, "x": 88, "y": 216, "name": "Sand Canyon 2 - 5 Exit 3", "access_rule": []}], "entity_load": [[80, 16], [78, 16], [81, 16], [83, 16], [79, 16], [82, 16], [14, 23]], "locations": ["Sand Canyon 2 - Enemy 9 (Bukiset (Ice))", "Sand Canyon 2 - Enemy 10 (Bukiset (Needle))", "Sand Canyon 2 - Enemy 11 (Bukiset (Clean))", "Sand Canyon 2 - Enemy 12 (Bukiset (Parasol))", "Sand Canyon 2 - Enemy 13 (Bukiset (Spark))", "Sand Canyon 2 - Enemy 14 (Bukiset (Cutter))", "Sand Canyon 2 - Star 20", "Sand Canyon 2 - Star 21", "Sand Canyon 2 - Star 22", "Sand Canyon 2 - Star 23", "Sand Canyon 2 - Star 24", "Sand Canyon 2 - Star 25", "Sand Canyon 2 - Star 26", "Sand Canyon 2 - Star 27", "Sand Canyon 2 - Star 28", "Sand Canyon 2 - Star 29", "Sand Canyon 2 - Star 30", "Sand Canyon 2 - Star 31", "Sand Canyon 2 - Star 32", "Sand Canyon 2 - Star 33"], "music": 21}, {"name": "Sand Canyon 2 - 6", "level": 3, "stage": 2, "room": 6, "pointer": 3800612, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Nidoo"], "default_exits": [{"room": 5, "unkn1": 17, "unkn2": 13, "x": 1144, "y": 248, "name": "Sand Canyon 2 - 6 Exit 0", "access_rule": []}], "entity_load": [[28, 16], [14, 23]], "locations": ["Sand Canyon 2 - Enemy 15 (Nidoo)", "Sand Canyon 2 - Star 34"], "music": 21}, {"name": "Sand Canyon 2 - 7", "level": 3, "stage": 2, "room": 7, "pointer": 3073888, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 5, "unkn1": 15, "unkn2": 8, "x": 1016, "y": 296, "name": "Sand Canyon 2 - 7 Exit 0", "access_rule": []}, {"room": 5, "unkn1": 5, "unkn2": 13, "x": 904, "y": 376, "name": "Sand Canyon 2 - 7 Exit 1", "access_rule": []}], "entity_load": [[15, 19]], "locations": [], "music": 21}, {"name": "Sand Canyon 2 - 8", "level": 3, "stage": 2, "room": 8, "pointer": 3061270, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Mariel"], "default_exits": [{"room": 5, "unkn1": 19, "unkn2": 13, "x": 1256, "y": 376, "name": "Sand Canyon 2 - 8 Exit 0", "access_rule": []}], "entity_load": [[45, 16], [14, 23]], "locations": ["Sand Canyon 2 - Enemy 16 (Mariel)", "Sand Canyon 2 - Star 35"], "music": 21}, {"name": "Sand Canyon 2 - 9", "level": 3, "stage": 2, "room": 9, "pointer": 3453286, "animal_pointers": [], "consumables": [{"idx": 51, "pointer": 240, "x": 1408, "y": 216, "etype": 22, "vtype": 2, "name": "Sand Canyon 2 - Maxim Tomato (Underwater)"}], "consumables_pointer": 128, "enemies": ["Yaban", "Wapod", "Squishy", "Pteran"], "default_exits": [{"room": 10, "unkn1": 246, "unkn2": 10, "x": 56, "y": 152, "name": "Sand Canyon 2 - 9 Exit 0", "access_rule": []}], "entity_load": [[14, 23], [22, 16], [2, 22], [39, 16], [88, 16], [32, 16]], "locations": ["Sand Canyon 2 - Enemy 17 (Yaban)", "Sand Canyon 2 - Enemy 18 (Wapod)", "Sand Canyon 2 - Enemy 19 (Squishy)", "Sand Canyon 2 - Enemy 20 (Pteran)", "Sand Canyon 2 - Star 36", "Sand Canyon 2 - Star 37", "Sand Canyon 2 - Star 38", "Sand Canyon 2 - Star 39", "Sand Canyon 2 - Star 40", "Sand Canyon 2 - Star 41", "Sand Canyon 2 - Star 42", "Sand Canyon 2 - Star 43", "Sand Canyon 2 - Star 44", "Sand Canyon 2 - Star 45", "Sand Canyon 2 - Star 46", "Sand Canyon 2 - Star 47", "Sand Canyon 2 - Star 48", "Sand Canyon 2 - Maxim Tomato (Underwater)"], "music": 21}, {"name": "Sand Canyon 2 - 10", "level": 3, "stage": 2, "room": 10, "pointer": 2994821, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 11, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Sand Canyon 2 - 10 Exit 0", "access_rule": []}], "entity_load": [[15, 19], [42, 19]], "locations": ["Sand Canyon 2 - Auntie"], "music": 8}, {"name": "Sand Canyon 2 - 11", "level": 3, "stage": 2, "room": 11, "pointer": 2891799, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Sand Canyon 2 - Complete"], "music": 5}, {"name": "Sand Canyon 3 - 0", "level": 3, "stage": 3, "room": 0, "pointer": 3544676, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Sir Kibble", "Broom Hatter", "Rocky", "Gabon"], "default_exits": [{"room": 1, "unkn1": 6, "unkn2": 57, "x": 104, "y": 152, "name": "Sand Canyon 3 - 0 Exit 0", "access_rule": []}, {"room": 1, "unkn1": 11, "unkn2": 57, "x": 200, "y": 152, "name": "Sand Canyon 3 - 0 Exit 1", "access_rule": []}, {"room": 1, "unkn1": 16, "unkn2": 57, "x": 296, "y": 152, "name": "Sand Canyon 3 - 0 Exit 2", "access_rule": []}, {"room": 2, "unkn1": 23, "unkn2": 109, "x": 104, "y": 72, "name": "Sand Canyon 3 - 0 Exit 3", "access_rule": []}], "entity_load": [[24, 16], [3, 16], [11, 16], [27, 16]], "locations": ["Sand Canyon 3 - Enemy 1 (Sir Kibble)", "Sand Canyon 3 - Enemy 2 (Broom Hatter)", "Sand Canyon 3 - Enemy 3 (Rocky)", "Sand Canyon 3 - Enemy 4 (Gabon)"], "music": 16}, {"name": "Sand Canyon 3 - 1", "level": 3, "stage": 3, "room": 1, "pointer": 2965655, "animal_pointers": [212, 220, 228], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 0, "unkn1": 5, "unkn2": 9, "x": 88, "y": 920, "name": "Sand Canyon 3 - 1 Exit 0", "access_rule": []}, {"room": 0, "unkn1": 11, "unkn2": 9, "x": 168, "y": 920, "name": "Sand Canyon 3 - 1 Exit 1", "access_rule": []}, {"room": 0, "unkn1": 17, "unkn2": 9, "x": 248, "y": 920, "name": "Sand Canyon 3 - 1 Exit 2", "access_rule": []}], "entity_load": [], "locations": ["Sand Canyon 3 - Animal 1", "Sand Canyon 3 - Animal 2", "Sand Canyon 3 - Animal 3"], "music": 39}, {"name": "Sand Canyon 3 - 2", "level": 3, "stage": 3, "room": 2, "pointer": 3726270, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Kany"], "default_exits": [{"room": 3, "unkn1": 184, "unkn2": 20, "x": 104, "y": 232, "name": "Sand Canyon 3 - 2 Exit 0", "access_rule": []}], "entity_load": [[63, 16], [55, 16], [14, 23], [4, 23], [29, 16]], "locations": ["Sand Canyon 3 - Enemy 5 (Kany)", "Sand Canyon 3 - Star 1", "Sand Canyon 3 - Star 2", "Sand Canyon 3 - Star 3", "Sand Canyon 3 - Star 4", "Sand Canyon 3 - Star 5", "Sand Canyon 3 - Star 6"], "music": 16}, {"name": "Sand Canyon 3 - 3", "level": 3, "stage": 3, "room": 3, "pointer": 3416806, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Galbo"], "default_exits": [{"room": 8, "unkn1": 130, "unkn2": 14, "x": 40, "y": 152, "name": "Sand Canyon 3 - 3 Exit 0", "access_rule": []}], "entity_load": [[26, 16], [6, 23], [5, 23]], "locations": ["Sand Canyon 3 - Enemy 6 (Galbo)"], "music": 16}, {"name": "Sand Canyon 3 - 4", "level": 3, "stage": 3, "room": 4, "pointer": 3564419, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Propeller", "Sasuke"], "default_exits": [{"room": 5, "unkn1": 135, "unkn2": 11, "x": 56, "y": 152, "name": "Sand Canyon 3 - 4 Exit 0", "access_rule": []}], "entity_load": [[68, 16], [30, 16], [14, 23], [89, 16], [24, 16]], "locations": ["Sand Canyon 3 - Enemy 7 (Propeller)", "Sand Canyon 3 - Enemy 8 (Sasuke)", "Sand Canyon 3 - Star 7", "Sand Canyon 3 - Star 8", "Sand Canyon 3 - Star 9", "Sand Canyon 3 - Star 10"], "music": 16}, {"name": "Sand Canyon 3 - 5", "level": 3, "stage": 3, "room": 5, "pointer": 2973891, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 7, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Sand Canyon 3 - 5 Exit 0", "access_rule": []}], "entity_load": [[16, 19]], "locations": ["Sand Canyon 3 - Caramello"], "music": 8}, {"name": "Sand Canyon 3 - 6", "level": 3, "stage": 3, "room": 6, "pointer": 3247969, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Wapod", "Bobo", "Babut", "Magoo"], "default_exits": [{"room": 4, "unkn1": 95, "unkn2": 8, "x": 88, "y": 424, "name": "Sand Canyon 3 - 6 Exit 0", "access_rule": []}], "entity_load": [[53, 16], [88, 16], [46, 16], [47, 16], [5, 16], [43, 16]], "locations": ["Sand Canyon 3 - Enemy 9 (Wapod)", "Sand Canyon 3 - Enemy 10 (Bobo)", "Sand Canyon 3 - Enemy 11 (Babut)", "Sand Canyon 3 - Enemy 12 (Magoo)"], "music": 16}, {"name": "Sand Canyon 3 - 7", "level": 3, "stage": 3, "room": 7, "pointer": 2887702, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Sand Canyon 3 - Complete"], "music": 5}, {"name": "Sand Canyon 3 - 8", "level": 3, "stage": 3, "room": 8, "pointer": 2968057, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 14, "unkn2": 9, "x": 104, "y": 120, "name": "Sand Canyon 3 - 8 Exit 0", "access_rule": []}], "entity_load": [[16, 19]], "locations": [], "music": 31}, {"name": "Sand Canyon 4 - 0", "level": 3, "stage": 4, "room": 0, "pointer": 3521954, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Popon Ball", "Mariel", "Chilly"], "default_exits": [{"room": 1, "unkn1": 95, "unkn2": 8, "x": 216, "y": 88, "name": "Sand Canyon 4 - 0 Exit 0", "access_rule": []}, {"room": 2, "unkn1": 199, "unkn2": 15, "x": 104, "y": 88, "name": "Sand Canyon 4 - 0 Exit 1", "access_rule": []}], "entity_load": [[6, 16], [45, 16], [6, 23], [50, 16], [14, 23]], "locations": ["Sand Canyon 4 - Enemy 1 (Popon Ball)", "Sand Canyon 4 - Enemy 2 (Mariel)", "Sand Canyon 4 - Enemy 3 (Chilly)", "Sand Canyon 4 - Star 1", "Sand Canyon 4 - Star 2", "Sand Canyon 4 - Star 3", "Sand Canyon 4 - Star 4", "Sand Canyon 4 - Star 5", "Sand Canyon 4 - Star 6", "Sand Canyon 4 - Star 7", "Sand Canyon 4 - Star 8", "Sand Canyon 4 - Star 9", "Sand Canyon 4 - Star 10", "Sand Canyon 4 - Star 11"], "music": 18}, {"name": "Sand Canyon 4 - 1", "level": 3, "stage": 4, "room": 1, "pointer": 2956289, "animal_pointers": [192, 200, 208], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 0, "unkn1": 12, "unkn2": 5, "x": 1544, "y": 136, "name": "Sand Canyon 4 - 1 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Sand Canyon 4 - Animal 1", "Sand Canyon 4 - Animal 2", "Sand Canyon 4 - Animal 3"], "music": 39}, {"name": "Sand Canyon 4 - 2", "level": 3, "stage": 4, "room": 2, "pointer": 3505360, "animal_pointers": [], "consumables": [{"idx": 11, "pointer": 328, "x": 2024, "y": 72, "etype": 22, "vtype": 2, "name": "Sand Canyon 4 - Maxim Tomato (Pacto)"}], "consumables_pointer": 160, "enemies": ["Tick", "Bronto Burt", "Babut"], "default_exits": [{"room": 3, "unkn1": 149, "unkn2": 14, "x": 88, "y": 216, "name": "Sand Canyon 4 - 2 Exit 0", "access_rule": []}], "entity_load": [[62, 16], [2, 22], [43, 16], [2, 16], [48, 16]], "locations": ["Sand Canyon 4 - Enemy 4 (Tick)", "Sand Canyon 4 - Enemy 5 (Bronto Burt)", "Sand Canyon 4 - Enemy 6 (Babut)", "Sand Canyon 4 - Maxim Tomato (Pacto)"], "music": 18}, {"name": "Sand Canyon 4 - 3", "level": 3, "stage": 4, "room": 3, "pointer": 3412361, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bobin", "Joe", "Mony", "Blipper"], "default_exits": [{"room": 4, "unkn1": 115, "unkn2": 6, "x": 776, "y": 120, "name": "Sand Canyon 4 - 3 Exit 0", "access_rule": []}, {"room": 4, "unkn1": 29, "unkn2": 8, "x": 72, "y": 88, "name": "Sand Canyon 4 - 3 Exit 1", "access_rule": []}, {"room": 5, "unkn1": 105, "unkn2": 14, "x": 104, "y": 216, "name": "Sand Canyon 4 - 3 Exit 2", "access_rule": []}, {"room": 4, "unkn1": 115, "unkn2": 20, "x": 776, "y": 248, "name": "Sand Canyon 4 - 3 Exit 3", "access_rule": []}, {"room": 4, "unkn1": 64, "unkn2": 21, "x": 408, "y": 232, "name": "Sand Canyon 4 - 3 Exit 4", "access_rule": []}], "entity_load": [[73, 16], [20, 16], [91, 16], [14, 23], [21, 16]], "locations": ["Sand Canyon 4 - Enemy 7 (Bobin)", "Sand Canyon 4 - Enemy 8 (Joe)", "Sand Canyon 4 - Enemy 9 (Mony)", "Sand Canyon 4 - Enemy 10 (Blipper)", "Sand Canyon 4 - Star 12", "Sand Canyon 4 - Star 13", "Sand Canyon 4 - Star 14", "Sand Canyon 4 - Star 15"], "music": 18}, {"name": "Sand Canyon 4 - 4", "level": 3, "stage": 4, "room": 4, "pointer": 3307804, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 60, "unkn2": 6, "x": 88, "y": 104, "name": "Sand Canyon 4 - 4 Exit 0", "access_rule": []}, {"room": 6, "unkn1": 60, "unkn2": 16, "x": 88, "y": 360, "name": "Sand Canyon 4 - 4 Exit 1", "access_rule": []}], "entity_load": [[21, 16], [20, 16], [91, 16], [73, 16]], "locations": [], "music": 18}, {"name": "Sand Canyon 4 - 5", "level": 3, "stage": 4, "room": 5, "pointer": 2955407, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 16, "unkn2": 13, "x": 88, "y": 232, "name": "Sand Canyon 4 - 5 Exit 0", "access_rule": []}], "entity_load": [[5, 27]], "locations": ["Sand Canyon 4 - Miniboss 1 (Haboki)"], "music": 4}, {"name": "Sand Canyon 4 - 6", "level": 3, "stage": 4, "room": 6, "pointer": 3094851, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 7, "unkn1": 24, "unkn2": 6, "x": 56, "y": 56, "name": "Sand Canyon 4 - 6 Exit 0", "access_rule": []}, {"room": 7, "unkn1": 24, "unkn2": 14, "x": 56, "y": 104, "name": "Sand Canyon 4 - 6 Exit 1", "access_rule": []}, {"room": 7, "unkn1": 24, "unkn2": 22, "x": 56, "y": 152, "name": "Sand Canyon 4 - 6 Exit 2", "access_rule": []}], "entity_load": [[17, 19], [4, 23], [5, 23]], "locations": [], "music": 18}, {"name": "Sand Canyon 4 - 7", "level": 3, "stage": 4, "room": 7, "pointer": 3483428, "animal_pointers": [], "consumables": [{"idx": 25, "pointer": 200, "x": 344, "y": 144, "etype": 22, "vtype": 0, "name": "Sand Canyon 4 - 1-Up (Clean)"}, {"idx": 26, "pointer": 336, "x": 1656, "y": 144, "etype": 22, "vtype": 2, "name": "Sand Canyon 4 - Maxim Tomato (Needle)"}], "consumables_pointer": 128, "enemies": ["Togezo", "Rocky", "Bobo"], "default_exits": [{"room": 8, "unkn1": 266, "unkn2": 9, "x": 56, "y": 152, "name": "Sand Canyon 4 - 7 Exit 0", "access_rule": []}], "entity_load": [[6, 22], [3, 16], [14, 23], [5, 16], [2, 16], [18, 16], [0, 22], [2, 22]], "locations": ["Sand Canyon 4 - Enemy 11 (Togezo)", "Sand Canyon 4 - Enemy 12 (Rocky)", "Sand Canyon 4 - Enemy 13 (Bobo)", "Sand Canyon 4 - Star 16", "Sand Canyon 4 - Star 17", "Sand Canyon 4 - Star 18", "Sand Canyon 4 - Star 19", "Sand Canyon 4 - Star 20", "Sand Canyon 4 - Star 21", "Sand Canyon 4 - Star 22", "Sand Canyon 4 - Star 23", "Sand Canyon 4 - 1-Up (Clean)", "Sand Canyon 4 - Maxim Tomato (Needle)"], "music": 18}, {"name": "Sand Canyon 4 - 8", "level": 3, "stage": 4, "room": 8, "pointer": 3002086, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 9, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Sand Canyon 4 - 8 Exit 0", "access_rule": []}], "entity_load": [[17, 19], [42, 19]], "locations": ["Sand Canyon 4 - Donbe & Hikari"], "music": 8}, {"name": "Sand Canyon 4 - 9", "level": 3, "stage": 4, "room": 9, "pointer": 2885774, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Sand Canyon 4 - Complete"], "music": 5}, {"name": "Sand Canyon 5 - 0", "level": 3, "stage": 5, "room": 0, "pointer": 3662486, "animal_pointers": [], "consumables": [{"idx": 0, "pointer": 450, "x": 2256, "y": 232, "etype": 22, "vtype": 0, "name": "Sand Canyon 5 - 1-Up (Falling Block)"}], "consumables_pointer": 208, "enemies": ["Wapod", "Dogon", "Tick"], "default_exits": [{"room": 3, "unkn1": 151, "unkn2": 15, "x": 184, "y": 56, "name": "Sand Canyon 5 - 0 Exit 0", "access_rule": []}, {"room": 2, "unkn1": 83, "unkn2": 16, "x": 72, "y": 120, "name": "Sand Canyon 5 - 0 Exit 1", "access_rule": []}], "entity_load": [[55, 16], [48, 16], [0, 22], [14, 23], [90, 16], [88, 16]], "locations": ["Sand Canyon 5 - Enemy 1 (Wapod)", "Sand Canyon 5 - Enemy 2 (Dogon)", "Sand Canyon 5 - Enemy 3 (Tick)", "Sand Canyon 5 - Star 1", "Sand Canyon 5 - Star 2", "Sand Canyon 5 - Star 3", "Sand Canyon 5 - Star 4", "Sand Canyon 5 - Star 5", "Sand Canyon 5 - Star 6", "Sand Canyon 5 - Star 7", "Sand Canyon 5 - Star 8", "Sand Canyon 5 - Star 9", "Sand Canyon 5 - Star 10", "Sand Canyon 5 - 1-Up (Falling Block)"], "music": 13}, {"name": "Sand Canyon 5 - 1", "level": 3, "stage": 5, "room": 1, "pointer": 3052622, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Rocky", "Bobo", "Chilly", "Sparky", "Togezo"], "default_exits": [{"room": 9, "unkn1": 6, "unkn2": 9, "x": 216, "y": 1128, "name": "Sand Canyon 5 - 1 Exit 0", "access_rule": []}], "entity_load": [[5, 16], [6, 16], [3, 16], [8, 16], [18, 16]], "locations": ["Sand Canyon 5 - Enemy 4 (Rocky)", "Sand Canyon 5 - Enemy 5 (Bobo)", "Sand Canyon 5 - Enemy 6 (Chilly)", "Sand Canyon 5 - Enemy 7 (Sparky)", "Sand Canyon 5 - Enemy 8 (Togezo)"], "music": 13}, {"name": "Sand Canyon 5 - 2", "level": 3, "stage": 5, "room": 2, "pointer": 3057544, "animal_pointers": [192], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 0, "unkn1": 3, "unkn2": 7, "x": 1320, "y": 264, "name": "Sand Canyon 5 - 2 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Sand Canyon 5 - Animal 1"], "music": 40}, {"name": "Sand Canyon 5 - 3", "level": 3, "stage": 5, "room": 3, "pointer": 3419011, "animal_pointers": [], "consumables": [{"idx": 11, "pointer": 192, "x": 56, "y": 648, "etype": 22, "vtype": 0, "name": "Sand Canyon 5 - 1-Up (Ice 2)"}, {"idx": 14, "pointer": 200, "x": 56, "y": 664, "etype": 22, "vtype": 0, "name": "Sand Canyon 5 - 1-Up (Ice 3)"}, {"idx": 15, "pointer": 208, "x": 56, "y": 632, "etype": 22, "vtype": 0, "name": "Sand Canyon 5 - 1-Up (Ice 1)"}, {"idx": 12, "pointer": 368, "x": 320, "y": 264, "etype": 22, "vtype": 2, "name": "Sand Canyon 5 - Maxim Tomato (Pit)"}], "consumables_pointer": 304, "enemies": ["Bronto Burt", "Sasuke"], "default_exits": [{"room": 4, "unkn1": 12, "unkn2": 66, "x": 88, "y": 88, "name": "Sand Canyon 5 - 3 Exit 0", "access_rule": []}], "entity_load": [[6, 16], [30, 16], [2, 16], [0, 22], [2, 22], [14, 23]], "locations": ["Sand Canyon 5 - Enemy 9 (Bronto Burt)", "Sand Canyon 5 - Enemy 10 (Sasuke)", "Sand Canyon 5 - Star 11", "Sand Canyon 5 - Star 12", "Sand Canyon 5 - Star 13", "Sand Canyon 5 - Star 14", "Sand Canyon 5 - Star 15", "Sand Canyon 5 - Star 16", "Sand Canyon 5 - Star 17", "Sand Canyon 5 - 1-Up (Ice 2)", "Sand Canyon 5 - 1-Up (Ice 3)", "Sand Canyon 5 - 1-Up (Ice 1)", "Sand Canyon 5 - Maxim Tomato (Pit)"], "music": 13}, {"name": "Sand Canyon 5 - 4", "level": 3, "stage": 5, "room": 4, "pointer": 3644149, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Oro", "Galbo", "Nidoo"], "default_exits": [{"room": 5, "unkn1": 76, "unkn2": 9, "x": 88, "y": 120, "name": "Sand Canyon 5 - 4 Exit 0", "access_rule": []}, {"room": 9, "unkn1": 116, "unkn2": 14, "x": 200, "y": 1256, "name": "Sand Canyon 5 - 4 Exit 1", "access_rule": []}], "entity_load": [[54, 16], [26, 16], [28, 16], [25, 16], [14, 23]], "locations": ["Sand Canyon 5 - Enemy 11 (Oro)", "Sand Canyon 5 - Enemy 12 (Galbo)", "Sand Canyon 5 - Enemy 13 (Nidoo)", "Sand Canyon 5 - Star 18"], "music": 13}, {"name": "Sand Canyon 5 - 5", "level": 3, "stage": 5, "room": 5, "pointer": 3044100, "animal_pointers": [192], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 4, "unkn1": 4, "unkn2": 7, "x": 1240, "y": 152, "name": "Sand Canyon 5 - 5 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Sand Canyon 5 - Animal 2"], "music": 39}, {"name": "Sand Canyon 5 - 6", "level": 3, "stage": 5, "room": 6, "pointer": 3373481, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Propeller"], "default_exits": [{"room": 7, "unkn1": 25, "unkn2": 5, "x": 56, "y": 152, "name": "Sand Canyon 5 - 6 Exit 0", "access_rule": []}], "entity_load": [[2, 16], [31, 16], [89, 16], [6, 16]], "locations": ["Sand Canyon 5 - Enemy 14 (Propeller)"], "music": 13}, {"name": "Sand Canyon 5 - 7", "level": 3, "stage": 5, "room": 7, "pointer": 2964028, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 8, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Sand Canyon 5 - 7 Exit 0", "access_rule": []}], "entity_load": [[18, 19], [42, 19]], "locations": ["Sand Canyon 5 - Nyupun"], "music": 8}, {"name": "Sand Canyon 5 - 8", "level": 3, "stage": 5, "room": 8, "pointer": 2890112, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Sand Canyon 5 - Complete"], "music": 5}, {"name": "Sand Canyon 5 - 9", "level": 3, "stage": 5, "room": 9, "pointer": 3647264, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Sir Kibble", "KeKe", "Kabu"], "default_exits": [{"room": 6, "unkn1": 12, "unkn2": 6, "x": 72, "y": 760, "name": "Sand Canyon 5 - 9 Exit 0", "access_rule": ["Cutter", "Cutter Ability"]}, {"room": 1, "unkn1": 12, "unkn2": 70, "x": 120, "y": 152, "name": "Sand Canyon 5 - 9 Exit 1", "access_rule": []}], "entity_load": [[27, 16], [61, 16], [4, 22], [14, 23], [51, 16], [19, 16]], "locations": ["Sand Canyon 5 - Enemy 15 (Sir Kibble)", "Sand Canyon 5 - Enemy 16 (KeKe)", "Sand Canyon 5 - Enemy 17 (Kabu)", "Sand Canyon 5 - Star 19", "Sand Canyon 5 - Star 20", "Sand Canyon 5 - Star 21", "Sand Canyon 5 - Star 22", "Sand Canyon 5 - Star 23", "Sand Canyon 5 - Star 24", "Sand Canyon 5 - Star 25", "Sand Canyon 5 - Star 26", "Sand Canyon 5 - Star 27", "Sand Canyon 5 - Star 28", "Sand Canyon 5 - Star 29", "Sand Canyon 5 - Star 30", "Sand Canyon 5 - Star 31", "Sand Canyon 5 - Star 32", "Sand Canyon 5 - Star 33", "Sand Canyon 5 - Star 34", "Sand Canyon 5 - Star 35", "Sand Canyon 5 - Star 36", "Sand Canyon 5 - Star 37", "Sand Canyon 5 - Star 38", "Sand Canyon 5 - Star 39", "Sand Canyon 5 - Star 40"], "music": 13}, {"name": "Sand Canyon 6 - 0", "level": 3, "stage": 6, "room": 0, "pointer": 3314761, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Sparky", "Doka", "Cappy", "Pteran"], "default_exits": [{"room": 2, "unkn1": 132, "unkn2": 16, "x": 56, "y": 136, "name": "Sand Canyon 6 - 0 Exit 0", "access_rule": []}], "entity_load": [[8, 16], [35, 16], [39, 16], [12, 16]], "locations": ["Sand Canyon 6 - Enemy 1 (Sparky)", "Sand Canyon 6 - Enemy 2 (Doka)", "Sand Canyon 6 - Enemy 3 (Cappy)", "Sand Canyon 6 - Enemy 4 (Pteran)"], "music": 14}, {"name": "Sand Canyon 6 - 1", "level": 3, "stage": 6, "room": 1, "pointer": 2976913, "animal_pointers": [192, 200, 208], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 22, "unkn1": 24, "unkn2": 13, "x": 168, "y": 216, "name": "Sand Canyon 6 - 1 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Sand Canyon 6 - Animal 1", "Sand Canyon 6 - Animal 2", "Sand Canyon 6 - Animal 3"], "music": 39}, {"name": "Sand Canyon 6 - 2", "level": 3, "stage": 6, "room": 2, "pointer": 2978757, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 11, "unkn1": 55, "unkn2": 6, "x": 104, "y": 104, "name": "Sand Canyon 6 - 2 Exit 0", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 3", "level": 3, "stage": 6, "room": 3, "pointer": 3178082, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 16, "unkn1": 16, "unkn2": 4, "x": 184, "y": 712, "name": "Sand Canyon 6 - 3 Exit 0", "access_rule": []}, {"room": 24, "unkn1": 8, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 3 Exit 1", "access_rule": []}, {"room": 31, "unkn1": 24, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 3 Exit 2", "access_rule": []}, {"room": 11, "unkn1": 4, "unkn2": 16, "x": 600, "y": 104, "name": "Sand Canyon 6 - 3 Exit 3", "access_rule": []}, {"room": 12, "unkn1": 28, "unkn2": 16, "x": 88, "y": 136, "name": "Sand Canyon 6 - 3 Exit 4", "access_rule": []}, {"room": 14, "unkn1": 8, "unkn2": 24, "x": 552, "y": 152, "name": "Sand Canyon 6 - 3 Exit 5", "access_rule": []}, {"room": 19, "unkn1": 24, "unkn2": 24, "x": 104, "y": 152, "name": "Sand Canyon 6 - 3 Exit 6", "access_rule": []}, {"room": 38, "unkn1": 16, "unkn2": 28, "x": 168, "y": 88, "name": "Sand Canyon 6 - 3 Exit 7", "access_rule": []}], "entity_load": [[10, 23]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 4", "level": 3, "stage": 6, "room": 4, "pointer": 3181563, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 16, "unkn1": 16, "unkn2": 4, "x": 184, "y": 712, "name": "Sand Canyon 6 - 4 Exit 0", "access_rule": []}, {"room": 24, "unkn1": 8, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 4 Exit 1", "access_rule": []}, {"room": 31, "unkn1": 24, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 4 Exit 2", "access_rule": []}, {"room": 11, "unkn1": 4, "unkn2": 16, "x": 600, "y": 104, "name": "Sand Canyon 6 - 4 Exit 3", "access_rule": []}, {"room": 12, "unkn1": 28, "unkn2": 16, "x": 88, "y": 136, "name": "Sand Canyon 6 - 4 Exit 4", "access_rule": []}, {"room": 14, "unkn1": 8, "unkn2": 24, "x": 552, "y": 152, "name": "Sand Canyon 6 - 4 Exit 5", "access_rule": []}, {"room": 19, "unkn1": 24, "unkn2": 24, "x": 104, "y": 152, "name": "Sand Canyon 6 - 4 Exit 6", "access_rule": []}, {"room": 38, "unkn1": 16, "unkn2": 28, "x": 168, "y": 88, "name": "Sand Canyon 6 - 4 Exit 7", "access_rule": []}], "entity_load": [[10, 23]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 5", "level": 3, "stage": 6, "room": 5, "pointer": 3177209, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 16, "unkn1": 16, "unkn2": 4, "x": 184, "y": 712, "name": "Sand Canyon 6 - 5 Exit 0", "access_rule": []}, {"room": 24, "unkn1": 8, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 5 Exit 1", "access_rule": []}, {"room": 31, "unkn1": 24, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 5 Exit 2", "access_rule": []}, {"room": 11, "unkn1": 4, "unkn2": 16, "x": 600, "y": 104, "name": "Sand Canyon 6 - 5 Exit 3", "access_rule": []}, {"room": 12, "unkn1": 28, "unkn2": 16, "x": 88, "y": 136, "name": "Sand Canyon 6 - 5 Exit 4", "access_rule": []}, {"room": 14, "unkn1": 8, "unkn2": 24, "x": 552, "y": 152, "name": "Sand Canyon 6 - 5 Exit 5", "access_rule": []}, {"room": 19, "unkn1": 24, "unkn2": 24, "x": 104, "y": 152, "name": "Sand Canyon 6 - 5 Exit 6", "access_rule": []}, {"room": 38, "unkn1": 16, "unkn2": 28, "x": 168, "y": 88, "name": "Sand Canyon 6 - 5 Exit 7", "access_rule": []}], "entity_load": [[10, 23]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 6", "level": 3, "stage": 6, "room": 6, "pointer": 3183291, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 16, "unkn1": 16, "unkn2": 4, "x": 184, "y": 712, "name": "Sand Canyon 6 - 6 Exit 0", "access_rule": []}, {"room": 24, "unkn1": 8, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 6 Exit 1", "access_rule": []}, {"room": 31, "unkn1": 24, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 6 Exit 2", "access_rule": []}, {"room": 11, "unkn1": 4, "unkn2": 16, "x": 600, "y": 104, "name": "Sand Canyon 6 - 6 Exit 3", "access_rule": []}, {"room": 12, "unkn1": 28, "unkn2": 16, "x": 88, "y": 136, "name": "Sand Canyon 6 - 6 Exit 4", "access_rule": []}, {"room": 14, "unkn1": 8, "unkn2": 24, "x": 552, "y": 152, "name": "Sand Canyon 6 - 6 Exit 5", "access_rule": []}, {"room": 19, "unkn1": 24, "unkn2": 24, "x": 104, "y": 152, "name": "Sand Canyon 6 - 6 Exit 6", "access_rule": []}, {"room": 38, "unkn1": 16, "unkn2": 28, "x": 168, "y": 88, "name": "Sand Canyon 6 - 6 Exit 7", "access_rule": []}], "entity_load": [[10, 23]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 7", "level": 3, "stage": 6, "room": 7, "pointer": 3175453, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 16, "unkn1": 16, "unkn2": 4, "x": 184, "y": 712, "name": "Sand Canyon 6 - 7 Exit 0", "access_rule": []}, {"room": 24, "unkn1": 8, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 7 Exit 1", "access_rule": []}, {"room": 31, "unkn1": 24, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 7 Exit 2", "access_rule": []}, {"room": 11, "unkn1": 4, "unkn2": 16, "x": 600, "y": 104, "name": "Sand Canyon 6 - 7 Exit 3", "access_rule": []}, {"room": 12, "unkn1": 28, "unkn2": 16, "x": 88, "y": 136, "name": "Sand Canyon 6 - 7 Exit 4", "access_rule": []}, {"room": 14, "unkn1": 8, "unkn2": 24, "x": 552, "y": 152, "name": "Sand Canyon 6 - 7 Exit 5", "access_rule": []}, {"room": 19, "unkn1": 24, "unkn2": 24, "x": 104, "y": 152, "name": "Sand Canyon 6 - 7 Exit 6", "access_rule": []}, {"room": 38, "unkn1": 16, "unkn2": 28, "x": 168, "y": 88, "name": "Sand Canyon 6 - 7 Exit 7", "access_rule": []}], "entity_load": [[10, 23]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 8", "level": 3, "stage": 6, "room": 8, "pointer": 3179826, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 16, "unkn1": 16, "unkn2": 4, "x": 184, "y": 712, "name": "Sand Canyon 6 - 8 Exit 0", "access_rule": []}, {"room": 24, "unkn1": 8, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 8 Exit 1", "access_rule": []}, {"room": 31, "unkn1": 24, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 8 Exit 2", "access_rule": []}, {"room": 11, "unkn1": 4, "unkn2": 16, "x": 600, "y": 104, "name": "Sand Canyon 6 - 8 Exit 3", "access_rule": []}, {"room": 12, "unkn1": 28, "unkn2": 16, "x": 88, "y": 136, "name": "Sand Canyon 6 - 8 Exit 4", "access_rule": []}, {"room": 14, "unkn1": 8, "unkn2": 24, "x": 552, "y": 152, "name": "Sand Canyon 6 - 8 Exit 5", "access_rule": []}, {"room": 19, "unkn1": 24, "unkn2": 24, "x": 104, "y": 152, "name": "Sand Canyon 6 - 8 Exit 6", "access_rule": []}, {"room": 38, "unkn1": 16, "unkn2": 28, "x": 168, "y": 88, "name": "Sand Canyon 6 - 8 Exit 7", "access_rule": []}], "entity_load": [[10, 23]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 9", "level": 3, "stage": 6, "room": 9, "pointer": 3176334, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 16, "unkn1": 16, "unkn2": 4, "x": 184, "y": 712, "name": "Sand Canyon 6 - 9 Exit 0", "access_rule": []}, {"room": 24, "unkn1": 8, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 9 Exit 1", "access_rule": []}, {"room": 31, "unkn1": 24, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 9 Exit 2", "access_rule": []}, {"room": 11, "unkn1": 4, "unkn2": 16, "x": 600, "y": 104, "name": "Sand Canyon 6 - 9 Exit 3", "access_rule": []}, {"room": 12, "unkn1": 28, "unkn2": 16, "x": 88, "y": 136, "name": "Sand Canyon 6 - 9 Exit 4", "access_rule": []}, {"room": 14, "unkn1": 8, "unkn2": 24, "x": 552, "y": 152, "name": "Sand Canyon 6 - 9 Exit 5", "access_rule": []}, {"room": 19, "unkn1": 24, "unkn2": 24, "x": 104, "y": 152, "name": "Sand Canyon 6 - 9 Exit 6", "access_rule": []}, {"room": 38, "unkn1": 16, "unkn2": 28, "x": 168, "y": 88, "name": "Sand Canyon 6 - 9 Exit 7", "access_rule": []}], "entity_load": [[10, 23]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 10", "level": 3, "stage": 6, "room": 10, "pointer": 3178954, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 16, "unkn1": 16, "unkn2": 4, "x": 184, "y": 712, "name": "Sand Canyon 6 - 10 Exit 0", "access_rule": []}, {"room": 24, "unkn1": 8, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 10 Exit 1", "access_rule": []}, {"room": 31, "unkn1": 24, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 10 Exit 2", "access_rule": []}, {"room": 11, "unkn1": 4, "unkn2": 16, "x": 600, "y": 104, "name": "Sand Canyon 6 - 10 Exit 3", "access_rule": []}, {"room": 12, "unkn1": 28, "unkn2": 16, "x": 88, "y": 136, "name": "Sand Canyon 6 - 10 Exit 4", "access_rule": []}, {"room": 14, "unkn1": 8, "unkn2": 24, "x": 552, "y": 152, "name": "Sand Canyon 6 - 10 Exit 5", "access_rule": []}, {"room": 19, "unkn1": 24, "unkn2": 24, "x": 104, "y": 152, "name": "Sand Canyon 6 - 10 Exit 6", "access_rule": []}, {"room": 38, "unkn1": 16, "unkn2": 28, "x": 168, "y": 88, "name": "Sand Canyon 6 - 10 Exit 7", "access_rule": []}], "entity_load": [[10, 23]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 11", "level": 3, "stage": 6, "room": 11, "pointer": 2989149, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 3, "unkn1": 37, "unkn2": 6, "x": 88, "y": 264, "name": "Sand Canyon 6 - 11 Exit 0", "access_rule": []}], "entity_load": [[59, 16]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 12", "level": 3, "stage": 6, "room": 12, "pointer": 3015947, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 7, "unkn1": 4, "unkn2": 8, "x": 440, "y": 264, "name": "Sand Canyon 6 - 12 Exit 0", "access_rule": []}, {"room": 13, "unkn1": 86, "unkn2": 8, "x": 72, "y": 104, "name": "Sand Canyon 6 - 12 Exit 1", "access_rule": []}], "entity_load": [[64, 16], [59, 16], [55, 16]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 13", "level": 3, "stage": 6, "room": 13, "pointer": 2976539, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 23, "unkn1": 55, "unkn2": 8, "x": 56, "y": 152, "name": "Sand Canyon 6 - 13 Exit 0", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 14", "level": 3, "stage": 6, "room": 14, "pointer": 3030336, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 15, "unkn1": 5, "unkn2": 9, "x": 200, "y": 152, "name": "Sand Canyon 6 - 14 Exit 0", "access_rule": []}, {"room": 8, "unkn1": 35, "unkn2": 9, "x": 152, "y": 392, "name": "Sand Canyon 6 - 14 Exit 1", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 15", "level": 3, "stage": 6, "room": 15, "pointer": 2972361, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 14, "unkn1": 13, "unkn2": 9, "x": 104, "y": 152, "name": "Sand Canyon 6 - 15 Exit 0", "access_rule": []}], "entity_load": [[19, 19]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 16", "level": 3, "stage": 6, "room": 16, "pointer": 2953186, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 18, "unkn1": 10, "unkn2": 19, "x": 248, "y": 152, "name": "Sand Canyon 6 - 16 Exit 0", "access_rule": []}, {"room": 5, "unkn1": 10, "unkn2": 44, "x": 264, "y": 88, "name": "Sand Canyon 6 - 16 Exit 1", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 17", "level": 3, "stage": 6, "room": 17, "pointer": 2953633, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 18, "unkn1": 10, "unkn2": 19, "x": 248, "y": 152, "name": "Sand Canyon 6 - 17 Exit 0", "access_rule": []}, {"room": 5, "unkn1": 10, "unkn2": 44, "x": 264, "y": 88, "name": "Sand Canyon 6 - 17 Exit 1", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 18", "level": 3, "stage": 6, "room": 18, "pointer": 2991707, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 17, "unkn1": 14, "unkn2": 9, "x": 184, "y": 312, "name": "Sand Canyon 6 - 18 Exit 0", "access_rule": []}], "entity_load": [[19, 19]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 19", "level": 3, "stage": 6, "room": 19, "pointer": 2974651, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 10, "unkn1": 5, "unkn2": 9, "x": 376, "y": 392, "name": "Sand Canyon 6 - 19 Exit 0", "access_rule": []}, {"room": 20, "unkn1": 35, "unkn2": 9, "x": 88, "y": 152, "name": "Sand Canyon 6 - 19 Exit 1", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 20", "level": 3, "stage": 6, "room": 20, "pointer": 2969638, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 19, "unkn1": 4, "unkn2": 9, "x": 552, "y": 152, "name": "Sand Canyon 6 - 20 Exit 0", "access_rule": []}], "entity_load": [[19, 19]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 21", "level": 3, "stage": 6, "room": 21, "pointer": 2976163, "animal_pointers": [192, 200, 208], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 22, "unkn1": 9, "unkn2": 13, "x": 296, "y": 216, "name": "Sand Canyon 6 - 21 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Sand Canyon 6 - Animal 4", "Sand Canyon 6 - Animal 5", "Sand Canyon 6 - Animal 6"], "music": 40}, {"name": "Sand Canyon 6 - 22", "level": 3, "stage": 6, "room": 22, "pointer": 2950938, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 38, "unkn1": 14, "unkn2": 8, "x": 168, "y": 376, "name": "Sand Canyon 6 - 22 Exit 0", "access_rule": []}, {"room": 1, "unkn1": 9, "unkn2": 13, "x": 376, "y": 216, "name": "Sand Canyon 6 - 22 Exit 1", "access_rule": []}, {"room": 21, "unkn1": 19, "unkn2": 13, "x": 168, "y": 216, "name": "Sand Canyon 6 - 22 Exit 2", "access_rule": []}], "entity_load": [[4, 22]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 23", "level": 3, "stage": 6, "room": 23, "pointer": 3311993, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 43, "unkn1": 145, "unkn2": 7, "x": 72, "y": 152, "name": "Sand Canyon 6 - 23 Exit 0", "access_rule": []}], "entity_load": [[14, 16]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 24", "level": 3, "stage": 6, "room": 24, "pointer": 3079078, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 39, "unkn1": 7, "unkn2": 8, "x": 120, "y": 328, "name": "Sand Canyon 6 - 24 Exit 0", "access_rule": []}, {"room": 25, "unkn1": 11, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 24 Exit 1", "access_rule": []}, {"room": 39, "unkn1": 15, "unkn2": 8, "x": 248, "y": 328, "name": "Sand Canyon 6 - 24 Exit 2", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 25", "level": 3, "stage": 6, "room": 25, "pointer": 3065898, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 26, "unkn1": 7, "unkn2": 8, "x": 120, "y": 328, "name": "Sand Canyon 6 - 25 Exit 0", "access_rule": []}, {"room": 40, "unkn1": 11, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 25 Exit 1", "access_rule": []}, {"room": 40, "unkn1": 15, "unkn2": 8, "x": 248, "y": 328, "name": "Sand Canyon 6 - 25 Exit 2", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 26", "level": 3, "stage": 6, "room": 26, "pointer": 3063347, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 41, "unkn1": 7, "unkn2": 8, "x": 120, "y": 328, "name": "Sand Canyon 6 - 26 Exit 0", "access_rule": []}, {"room": 41, "unkn1": 11, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 26 Exit 1", "access_rule": []}, {"room": 27, "unkn1": 15, "unkn2": 8, "x": 248, "y": 328, "name": "Sand Canyon 6 - 26 Exit 2", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 27", "level": 3, "stage": 6, "room": 27, "pointer": 3066916, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 28, "unkn1": 7, "unkn2": 8, "x": 120, "y": 328, "name": "Sand Canyon 6 - 27 Exit 0", "access_rule": []}, {"room": 42, "unkn1": 11, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 27 Exit 1", "access_rule": []}, {"room": 42, "unkn1": 15, "unkn2": 8, "x": 248, "y": 328, "name": "Sand Canyon 6 - 27 Exit 2", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 28", "level": 3, "stage": 6, "room": 28, "pointer": 3067425, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 29, "unkn1": 7, "unkn2": 8, "x": 120, "y": 344, "name": "Sand Canyon 6 - 28 Exit 0", "access_rule": []}, {"room": 29, "unkn1": 11, "unkn2": 8, "x": 184, "y": 344, "name": "Sand Canyon 6 - 28 Exit 1", "access_rule": []}, {"room": 29, "unkn1": 15, "unkn2": 8, "x": 248, "y": 344, "name": "Sand Canyon 6 - 28 Exit 2", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 29", "level": 3, "stage": 6, "room": 29, "pointer": 2950032, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 30, "unkn1": 11, "unkn2": 8, "x": 168, "y": 136, "name": "Sand Canyon 6 - 29 Exit 0", "access_rule": []}], "entity_load": [[19, 19]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 30", "level": 3, "stage": 6, "room": 30, "pointer": 2986500, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 4, "unkn1": 10, "unkn2": 44, "x": 136, "y": 152, "name": "Sand Canyon 6 - 30 Exit 0", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 31", "level": 3, "stage": 6, "room": 31, "pointer": 3070930, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 32, "unkn1": 7, "unkn2": 8, "x": 120, "y": 328, "name": "Sand Canyon 6 - 31 Exit 0", "access_rule": []}, {"room": 36, "unkn1": 11, "unkn2": 8, "x": 168, "y": 648, "name": "Sand Canyon 6 - 31 Exit 1", "access_rule": []}, {"room": 32, "unkn1": 15, "unkn2": 8, "x": 248, "y": 328, "name": "Sand Canyon 6 - 31 Exit 2", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 32", "level": 3, "stage": 6, "room": 32, "pointer": 3054817, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bukiset (Parasol)", "Bukiset (Cutter)"], "default_exits": [{"room": 33, "unkn1": 7, "unkn2": 8, "x": 120, "y": 328, "name": "Sand Canyon 6 - 32 Exit 0", "access_rule": []}, {"room": 36, "unkn1": 11, "unkn2": 8, "x": 168, "y": 536, "name": "Sand Canyon 6 - 32 Exit 1", "access_rule": []}, {"room": 33, "unkn1": 15, "unkn2": 8, "x": 248, "y": 328, "name": "Sand Canyon 6 - 32 Exit 2", "access_rule": []}], "entity_load": [[81, 16], [83, 16]], "locations": ["Sand Canyon 6 - Enemy 5 (Bukiset (Parasol))", "Sand Canyon 6 - Enemy 6 (Bukiset (Cutter))"], "music": 14}, {"name": "Sand Canyon 6 - 33", "level": 3, "stage": 6, "room": 33, "pointer": 3053173, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bukiset (Clean)", "Bukiset (Spark)"], "default_exits": [{"room": 34, "unkn1": 7, "unkn2": 8, "x": 120, "y": 328, "name": "Sand Canyon 6 - 33 Exit 0", "access_rule": []}, {"room": 36, "unkn1": 11, "unkn2": 8, "x": 168, "y": 440, "name": "Sand Canyon 6 - 33 Exit 1", "access_rule": []}, {"room": 34, "unkn1": 15, "unkn2": 8, "x": 248, "y": 328, "name": "Sand Canyon 6 - 33 Exit 2", "access_rule": []}], "entity_load": [[80, 16], [82, 16]], "locations": ["Sand Canyon 6 - Enemy 7 (Bukiset (Clean))", "Sand Canyon 6 - Enemy 8 (Bukiset (Spark))"], "music": 14}, {"name": "Sand Canyon 6 - 34", "level": 3, "stage": 6, "room": 34, "pointer": 3053721, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bukiset (Ice)", "Bukiset (Needle)"], "default_exits": [{"room": 35, "unkn1": 7, "unkn2": 8, "x": 120, "y": 328, "name": "Sand Canyon 6 - 34 Exit 0", "access_rule": []}, {"room": 36, "unkn1": 11, "unkn2": 8, "x": 168, "y": 344, "name": "Sand Canyon 6 - 34 Exit 1", "access_rule": []}, {"room": 35, "unkn1": 15, "unkn2": 8, "x": 248, "y": 328, "name": "Sand Canyon 6 - 34 Exit 2", "access_rule": []}], "entity_load": [[79, 16], [78, 16]], "locations": ["Sand Canyon 6 - Enemy 9 (Bukiset (Ice))", "Sand Canyon 6 - Enemy 10 (Bukiset (Needle))"], "music": 14}, {"name": "Sand Canyon 6 - 35", "level": 3, "stage": 6, "room": 35, "pointer": 3054269, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bukiset (Burning)", "Bukiset (Stone)"], "default_exits": [{"room": 37, "unkn1": 7, "unkn2": 8, "x": 120, "y": 344, "name": "Sand Canyon 6 - 35 Exit 0", "access_rule": []}, {"room": 36, "unkn1": 11, "unkn2": 8, "x": 168, "y": 248, "name": "Sand Canyon 6 - 35 Exit 1", "access_rule": []}, {"room": 37, "unkn1": 15, "unkn2": 8, "x": 248, "y": 344, "name": "Sand Canyon 6 - 35 Exit 2", "access_rule": []}], "entity_load": [[77, 16], [76, 16]], "locations": ["Sand Canyon 6 - Enemy 11 (Bukiset (Burning))", "Sand Canyon 6 - Enemy 12 (Bukiset (Stone))"], "music": 14}, {"name": "Sand Canyon 6 - 36", "level": 3, "stage": 6, "room": 36, "pointer": 2986164, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 10, "unkn2": 44, "x": 392, "y": 152, "name": "Sand Canyon 6 - 36 Exit 0", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 37", "level": 3, "stage": 6, "room": 37, "pointer": 3074377, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 36, "unkn1": 11, "unkn2": 8, "x": 168, "y": 152, "name": "Sand Canyon 6 - 37 Exit 0", "access_rule": []}], "entity_load": [[19, 19]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 38", "level": 3, "stage": 6, "room": 38, "pointer": 2971589, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 9, "unkn1": 10, "unkn2": 5, "x": 264, "y": 440, "name": "Sand Canyon 6 - 38 Exit 0", "access_rule": []}, {"room": 22, "unkn1": 10, "unkn2": 23, "x": 248, "y": 136, "name": "Sand Canyon 6 - 38 Exit 1", "access_rule": []}], "entity_load": [[76, 16], [77, 16], [78, 16], [79, 16], [80, 16], [81, 16], [82, 16], [83, 16]], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 39", "level": 3, "stage": 6, "room": 39, "pointer": 3063858, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 40, "unkn1": 7, "unkn2": 8, "x": 120, "y": 328, "name": "Sand Canyon 6 - 39 Exit 0", "access_rule": []}, {"room": 40, "unkn1": 11, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 39 Exit 1", "access_rule": []}, {"room": 40, "unkn1": 15, "unkn2": 8, "x": 248, "y": 328, "name": "Sand Canyon 6 - 39 Exit 2", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 40", "level": 3, "stage": 6, "room": 40, "pointer": 3064368, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 41, "unkn1": 7, "unkn2": 8, "x": 120, "y": 328, "name": "Sand Canyon 6 - 40 Exit 0", "access_rule": []}, {"room": 41, "unkn1": 11, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 40 Exit 1", "access_rule": []}, {"room": 41, "unkn1": 15, "unkn2": 8, "x": 248, "y": 328, "name": "Sand Canyon 6 - 40 Exit 2", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 41", "level": 3, "stage": 6, "room": 41, "pointer": 3064878, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 42, "unkn1": 7, "unkn2": 8, "x": 120, "y": 328, "name": "Sand Canyon 6 - 41 Exit 0", "access_rule": []}, {"room": 42, "unkn1": 11, "unkn2": 8, "x": 184, "y": 328, "name": "Sand Canyon 6 - 41 Exit 1", "access_rule": []}, {"room": 42, "unkn1": 15, "unkn2": 8, "x": 248, "y": 328, "name": "Sand Canyon 6 - 41 Exit 2", "access_rule": []}], "entity_load": [], "locations": [], "music": 14}, {"name": "Sand Canyon 6 - 42", "level": 3, "stage": 6, "room": 42, "pointer": 3068939, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Nidoo"], "default_exits": [{"room": 29, "unkn1": 7, "unkn2": 8, "x": 120, "y": 344, "name": "Sand Canyon 6 - 42 Exit 0", "access_rule": []}, {"room": 29, "unkn1": 15, "unkn2": 8, "x": 248, "y": 344, "name": "Sand Canyon 6 - 42 Exit 1", "access_rule": []}], "entity_load": [[28, 16]], "locations": ["Sand Canyon 6 - Enemy 13 (Nidoo)"], "music": 14}, {"name": "Sand Canyon 6 - 43", "level": 3, "stage": 6, "room": 43, "pointer": 2988495, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 44, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Sand Canyon 6 - 43 Exit 0", "access_rule": []}], "entity_load": [[19, 19], [42, 19]], "locations": ["Sand Canyon 6 - Professor Hector & R.O.B"], "music": 8}, {"name": "Sand Canyon 6 - 44", "level": 3, "stage": 6, "room": 44, "pointer": 2886979, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Sand Canyon 6 - Complete"], "music": 5}, {"name": "Sand Canyon Boss - 0", "level": 3, "stage": 7, "room": 0, "pointer": 2991389, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[6, 18]], "locations": ["Sand Canyon - Boss (Pon & Con) Purified", "Level 3 Boss - Defeated", "Level 3 Boss - Purified"], "music": 2}, {"name": "Cloudy Park 1 - 0", "level": 4, "stage": 1, "room": 0, "pointer": 3172795, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Waddle Dee", "KeKe", "Cappy"], "default_exits": [{"room": 1, "unkn1": 95, "unkn2": 5, "x": 56, "y": 152, "name": "Cloudy Park 1 - 0 Exit 0", "access_rule": []}], "entity_load": [[0, 16], [12, 16], [51, 16], [1, 23], [0, 23]], "locations": ["Cloudy Park 1 - Enemy 1 (Waddle Dee)", "Cloudy Park 1 - Enemy 2 (KeKe)", "Cloudy Park 1 - Enemy 3 (Cappy)"], "music": 17}, {"name": "Cloudy Park 1 - 1", "level": 4, "stage": 1, "room": 1, "pointer": 3078163, "animal_pointers": [192], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 14, "unkn2": 8, "x": 88, "y": 424, "name": "Cloudy Park 1 - 1 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Cloudy Park 1 - Animal 1"], "music": 39}, {"name": "Cloudy Park 1 - 2", "level": 4, "stage": 1, "room": 2, "pointer": 3285882, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Yaban", "Togezo"], "default_exits": [{"room": 3, "unkn1": 95, "unkn2": 8, "x": 40, "y": 104, "name": "Cloudy Park 1 - 2 Exit 0", "access_rule": []}], "entity_load": [[32, 16], [4, 16], [18, 16], [6, 23]], "locations": ["Cloudy Park 1 - Enemy 4 (Yaban)", "Cloudy Park 1 - Enemy 5 (Togezo)"], "music": 17}, {"name": "Cloudy Park 1 - 3", "level": 4, "stage": 1, "room": 3, "pointer": 3062831, "animal_pointers": [192], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 10, "unkn1": 15, "unkn2": 6, "x": 56, "y": 264, "name": "Cloudy Park 1 - 3 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Cloudy Park 1 - Animal 2"], "music": 39}, {"name": "Cloudy Park 1 - 4", "level": 4, "stage": 1, "room": 4, "pointer": 3396729, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Galbo"], "default_exits": [{"room": 7, "unkn1": 92, "unkn2": 16, "x": 56, "y": 152, "name": "Cloudy Park 1 - 4 Exit 0", "access_rule": []}], "entity_load": [[54, 16], [0, 16], [14, 23], [68, 16], [26, 16], [4, 22]], "locations": ["Cloudy Park 1 - Enemy 6 (Galbo)", "Cloudy Park 1 - Star 1"], "music": 17}, {"name": "Cloudy Park 1 - 5", "level": 4, "stage": 1, "room": 5, "pointer": 3038805, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 6, "unkn2": 5, "x": 2344, "y": 232, "name": "Cloudy Park 1 - 5 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Cloudy Park 1 - Animal 3", "Cloudy Park 1 - Animal 4"], "music": 39}, {"name": "Cloudy Park 1 - 6", "level": 4, "stage": 1, "room": 6, "pointer": 3535736, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Sparky", "Como"], "default_exits": [{"room": 8, "unkn1": 5, "unkn2": 8, "x": 72, "y": 104, "name": "Cloudy Park 1 - 6 Exit 0", "access_rule": []}], "entity_load": [[54, 16], [14, 23], [0, 16], [41, 16], [8, 16]], "locations": ["Cloudy Park 1 - Enemy 7 (Sparky)", "Cloudy Park 1 - Enemy 8 (Como)", "Cloudy Park 1 - Star 2"], "music": 17}, {"name": "Cloudy Park 1 - 7", "level": 4, "stage": 1, "room": 7, "pointer": 2954080, "animal_pointers": [192], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 11, "unkn1": 14, "unkn2": 6, "x": 88, "y": 104, "name": "Cloudy Park 1 - 7 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Cloudy Park 1 - Animal 5"], "music": 39}, {"name": "Cloudy Park 1 - 8", "level": 4, "stage": 1, "room": 8, "pointer": 3465407, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bronto Burt"], "default_exits": [{"room": 12, "unkn1": 181, "unkn2": 6, "x": 56, "y": 152, "name": "Cloudy Park 1 - 8 Exit 0", "access_rule": []}], "entity_load": [[21, 19], [2, 16], [51, 16], [1, 23], [14, 23]], "locations": ["Cloudy Park 1 - Enemy 9 (Bronto Burt)", "Cloudy Park 1 - Star 3", "Cloudy Park 1 - Star 4", "Cloudy Park 1 - Star 5", "Cloudy Park 1 - Star 6", "Cloudy Park 1 - Star 7", "Cloudy Park 1 - Star 8", "Cloudy Park 1 - Star 9", "Cloudy Park 1 - Star 10", "Cloudy Park 1 - Star 11"], "music": 17}, {"name": "Cloudy Park 1 - 9", "level": 4, "stage": 1, "room": 9, "pointer": 3078621, "animal_pointers": [192], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 4, "unkn1": 14, "unkn2": 9, "x": 56, "y": 264, "name": "Cloudy Park 1 - 9 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Cloudy Park 1 - Animal 6"], "music": 39}, {"name": "Cloudy Park 1 - 10", "level": 4, "stage": 1, "room": 10, "pointer": 3281366, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Gabon", "Sir Kibble"], "default_exits": [{"room": 9, "unkn1": 99, "unkn2": 8, "x": 56, "y": 152, "name": "Cloudy Park 1 - 10 Exit 0", "access_rule": []}], "entity_load": [[1, 16], [24, 16], [27, 16], [4, 23]], "locations": ["Cloudy Park 1 - Enemy 10 (Gabon)", "Cloudy Park 1 - Enemy 11 (Sir Kibble)"], "music": 17}, {"name": "Cloudy Park 1 - 11", "level": 4, "stage": 1, "room": 11, "pointer": 3519608, "animal_pointers": [], "consumables": [{"idx": 19, "pointer": 192, "x": 216, "y": 600, "etype": 22, "vtype": 0, "name": "Cloudy Park 1 - 1-Up (Shotzo)"}, {"idx": 18, "pointer": 456, "x": 856, "y": 408, "etype": 22, "vtype": 2, "name": "Cloudy Park 1 - Maxim Tomato (Mariel)"}], "consumables_pointer": 352, "enemies": ["Mariel", "Nruff"], "default_exits": [{"room": 5, "unkn1": 64, "unkn2": 6, "x": 104, "y": 216, "name": "Cloudy Park 1 - 11 Exit 0", "access_rule": []}], "entity_load": [[14, 16], [27, 16], [15, 16], [45, 16], [14, 23], [2, 22], [0, 22], [6, 22]], "locations": ["Cloudy Park 1 - Enemy 12 (Mariel)", "Cloudy Park 1 - Enemy 13 (Nruff)", "Cloudy Park 1 - Star 12", "Cloudy Park 1 - Star 13", "Cloudy Park 1 - Star 14", "Cloudy Park 1 - Star 15", "Cloudy Park 1 - Star 16", "Cloudy Park 1 - Star 17", "Cloudy Park 1 - Star 18", "Cloudy Park 1 - Star 19", "Cloudy Park 1 - Star 20", "Cloudy Park 1 - Star 21", "Cloudy Park 1 - Star 22", "Cloudy Park 1 - Star 23", "Cloudy Park 1 - 1-Up (Shotzo)", "Cloudy Park 1 - Maxim Tomato (Mariel)"], "music": 17}, {"name": "Cloudy Park 1 - 12", "level": 4, "stage": 1, "room": 12, "pointer": 2958914, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 13, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Cloudy Park 1 - 12 Exit 0", "access_rule": []}], "entity_load": [[21, 19], [42, 19]], "locations": ["Cloudy Park 1 - Hibanamodoki"], "music": 8}, {"name": "Cloudy Park 1 - 13", "level": 4, "stage": 1, "room": 13, "pointer": 2886015, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Cloudy Park 1 - Complete"], "music": 5}, {"name": "Cloudy Park 2 - 0", "level": 4, "stage": 2, "room": 0, "pointer": 3142443, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Chilly", "Sasuke"], "default_exits": [{"room": 6, "unkn1": 96, "unkn2": 7, "x": 56, "y": 88, "name": "Cloudy Park 2 - 0 Exit 0", "access_rule": []}], "entity_load": [[23, 16], [7, 16], [30, 16], [6, 16], [14, 23]], "locations": ["Cloudy Park 2 - Enemy 1 (Chilly)", "Cloudy Park 2 - Enemy 2 (Sasuke)", "Cloudy Park 2 - Star 1", "Cloudy Park 2 - Star 2", "Cloudy Park 2 - Star 3", "Cloudy Park 2 - Star 4", "Cloudy Park 2 - Star 5", "Cloudy Park 2 - Star 6"], "music": 19}, {"name": "Cloudy Park 2 - 1", "level": 4, "stage": 2, "room": 1, "pointer": 3235925, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Waddle Dee", "Sparky", "Broom Hatter"], "default_exits": [{"room": 2, "unkn1": 146, "unkn2": 5, "x": 88, "y": 88, "name": "Cloudy Park 2 - 1 Exit 0", "access_rule": []}], "entity_load": [[0, 16], [14, 23], [8, 16], [11, 16]], "locations": ["Cloudy Park 2 - Enemy 3 (Waddle Dee)", "Cloudy Park 2 - Enemy 4 (Sparky)", "Cloudy Park 2 - Enemy 5 (Broom Hatter)", "Cloudy Park 2 - Star 7", "Cloudy Park 2 - Star 8", "Cloudy Park 2 - Star 9", "Cloudy Park 2 - Star 10", "Cloudy Park 2 - Star 11", "Cloudy Park 2 - Star 12", "Cloudy Park 2 - Star 13", "Cloudy Park 2 - Star 14", "Cloudy Park 2 - Star 15"], "music": 19}, {"name": "Cloudy Park 2 - 2", "level": 4, "stage": 2, "room": 2, "pointer": 3297758, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Sir Kibble", "Pteran"], "default_exits": [{"room": 3, "unkn1": 147, "unkn2": 8, "x": 72, "y": 136, "name": "Cloudy Park 2 - 2 Exit 0", "access_rule": []}], "entity_load": [[27, 16], [39, 16], [1, 23]], "locations": ["Cloudy Park 2 - Enemy 6 (Sir Kibble)", "Cloudy Park 2 - Enemy 7 (Pteran)"], "music": 19}, {"name": "Cloudy Park 2 - 3", "level": 4, "stage": 2, "room": 3, "pointer": 3341087, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Propeller", "Dogon"], "default_exits": [{"room": 4, "unkn1": 145, "unkn2": 12, "x": 72, "y": 136, "name": "Cloudy Park 2 - 3 Exit 0", "access_rule": []}], "entity_load": [[4, 16], [14, 23], [90, 16], [89, 16]], "locations": ["Cloudy Park 2 - Enemy 8 (Propeller)", "Cloudy Park 2 - Enemy 9 (Dogon)", "Cloudy Park 2 - Star 16", "Cloudy Park 2 - Star 17", "Cloudy Park 2 - Star 18", "Cloudy Park 2 - Star 19", "Cloudy Park 2 - Star 20", "Cloudy Park 2 - Star 21", "Cloudy Park 2 - Star 22", "Cloudy Park 2 - Star 23", "Cloudy Park 2 - Star 24", "Cloudy Park 2 - Star 25", "Cloudy Park 2 - Star 26", "Cloudy Park 2 - Star 27", "Cloudy Park 2 - Star 28", "Cloudy Park 2 - Star 29", "Cloudy Park 2 - Star 30", "Cloudy Park 2 - Star 31", "Cloudy Park 2 - Star 32", "Cloudy Park 2 - Star 33", "Cloudy Park 2 - Star 34", "Cloudy Park 2 - Star 35", "Cloudy Park 2 - Star 36", "Cloudy Park 2 - Star 37"], "music": 19}, {"name": "Cloudy Park 2 - 4", "level": 4, "stage": 2, "room": 4, "pointer": 3457404, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Togezo", "Oro", "Bronto Burt", "Rocky"], "default_exits": [{"room": 5, "unkn1": 165, "unkn2": 5, "x": 72, "y": 104, "name": "Cloudy Park 2 - 4 Exit 0", "access_rule": []}], "entity_load": [[2, 16], [3, 16], [14, 23], [18, 16], [25, 16]], "locations": ["Cloudy Park 2 - Enemy 10 (Togezo)", "Cloudy Park 2 - Enemy 11 (Oro)", "Cloudy Park 2 - Enemy 12 (Bronto Burt)", "Cloudy Park 2 - Enemy 13 (Rocky)", "Cloudy Park 2 - Star 38", "Cloudy Park 2 - Star 39", "Cloudy Park 2 - Star 40", "Cloudy Park 2 - Star 41", "Cloudy Park 2 - Star 42", "Cloudy Park 2 - Star 43", "Cloudy Park 2 - Star 44", "Cloudy Park 2 - Star 45", "Cloudy Park 2 - Star 46", "Cloudy Park 2 - Star 47"], "music": 19}, {"name": "Cloudy Park 2 - 5", "level": 4, "stage": 2, "room": 5, "pointer": 3273878, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Galbo", "Kapar"], "default_exits": [{"room": 7, "unkn1": 96, "unkn2": 8, "x": 56, "y": 88, "name": "Cloudy Park 2 - 5 Exit 0", "access_rule": []}], "entity_load": [[14, 23], [62, 16], [26, 16], [67, 16]], "locations": ["Cloudy Park 2 - Enemy 14 (Galbo)", "Cloudy Park 2 - Enemy 15 (Kapar)", "Cloudy Park 2 - Star 48", "Cloudy Park 2 - Star 49", "Cloudy Park 2 - Star 50", "Cloudy Park 2 - Star 51", "Cloudy Park 2 - Star 52", "Cloudy Park 2 - Star 53", "Cloudy Park 2 - Star 54"], "music": 19}, {"name": "Cloudy Park 2 - 6", "level": 4, "stage": 2, "room": 6, "pointer": 2984453, "animal_pointers": [192, 200, 208], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 1, "unkn1": 25, "unkn2": 5, "x": 72, "y": 152, "name": "Cloudy Park 2 - 6 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Cloudy Park 2 - Animal 1", "Cloudy Park 2 - Animal 2", "Cloudy Park 2 - Animal 3"], "music": 38}, {"name": "Cloudy Park 2 - 7", "level": 4, "stage": 2, "room": 7, "pointer": 2985482, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 8, "unkn1": 15, "unkn2": 5, "x": 40, "y": 88, "name": "Cloudy Park 2 - 7 Exit 0", "access_rule": []}], "entity_load": [[22, 19]], "locations": [], "music": 19}, {"name": "Cloudy Park 2 - 8", "level": 4, "stage": 2, "room": 8, "pointer": 2990753, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 9, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Cloudy Park 2 - 8 Exit 0", "access_rule": []}], "entity_load": [[22, 19], [42, 19]], "locations": ["Cloudy Park 2 - Piyo & Keko"], "music": 8}, {"name": "Cloudy Park 2 - 9", "level": 4, "stage": 2, "room": 9, "pointer": 2889630, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Cloudy Park 2 - Complete"], "music": 5}, {"name": "Cloudy Park 3 - 0", "level": 4, "stage": 3, "room": 0, "pointer": 3100859, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bronto Burt", "Mopoo", "Poppy Bros Jr."], "default_exits": [{"room": 2, "unkn1": 145, "unkn2": 8, "x": 56, "y": 136, "name": "Cloudy Park 3 - 0 Exit 0", "access_rule": []}], "entity_load": [[2, 16], [74, 16], [47, 16], [7, 16], [14, 23]], "locations": ["Cloudy Park 3 - Enemy 1 (Bronto Burt)", "Cloudy Park 3 - Enemy 2 (Mopoo)", "Cloudy Park 3 - Enemy 3 (Poppy Bros Jr.)", "Cloudy Park 3 - Star 1", "Cloudy Park 3 - Star 2", "Cloudy Park 3 - Star 3", "Cloudy Park 3 - Star 4", "Cloudy Park 3 - Star 5", "Cloudy Park 3 - Star 6", "Cloudy Park 3 - Star 7"], "music": 15}, {"name": "Cloudy Park 3 - 1", "level": 4, "stage": 3, "room": 1, "pointer": 3209719, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Como"], "default_exits": [{"room": 5, "unkn1": 13, "unkn2": 14, "x": 56, "y": 152, "name": "Cloudy Park 3 - 1 Exit 0", "access_rule": []}], "entity_load": [[10, 23], [31, 16], [4, 22], [41, 16], [14, 23]], "locations": ["Cloudy Park 3 - Enemy 4 (Como)", "Cloudy Park 3 - Star 8", "Cloudy Park 3 - Star 9"], "music": 15}, {"name": "Cloudy Park 3 - 2", "level": 4, "stage": 3, "room": 2, "pointer": 3216185, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Glunk", "Bobin", "Loud", "Kapar"], "default_exits": [{"room": 1, "unkn1": 145, "unkn2": 6, "x": 216, "y": 1064, "name": "Cloudy Park 3 - 2 Exit 0", "access_rule": []}, {"room": 3, "unkn1": 24, "unkn2": 14, "x": 104, "y": 152, "name": "Cloudy Park 3 - 2 Exit 1", "access_rule": []}], "entity_load": [[67, 16], [40, 16], [73, 16], [14, 23], [16, 16]], "locations": ["Cloudy Park 3 - Enemy 5 (Glunk)", "Cloudy Park 3 - Enemy 6 (Bobin)", "Cloudy Park 3 - Enemy 7 (Loud)", "Cloudy Park 3 - Enemy 8 (Kapar)", "Cloudy Park 3 - Star 10", "Cloudy Park 3 - Star 11", "Cloudy Park 3 - Star 12", "Cloudy Park 3 - Star 13", "Cloudy Park 3 - Star 14", "Cloudy Park 3 - Star 15", "Cloudy Park 3 - Star 16"], "music": 15}, {"name": "Cloudy Park 3 - 3", "level": 4, "stage": 3, "room": 3, "pointer": 2994208, "animal_pointers": [192, 200, 208], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 5, "unkn2": 9, "x": 408, "y": 232, "name": "Cloudy Park 3 - 3 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Cloudy Park 3 - Animal 1", "Cloudy Park 3 - Animal 2", "Cloudy Park 3 - Animal 3"], "music": 40}, {"name": "Cloudy Park 3 - 4", "level": 4, "stage": 3, "room": 4, "pointer": 3229151, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Galbo", "Batamon", "Bouncy"], "default_exits": [{"room": 6, "unkn1": 156, "unkn2": 6, "x": 56, "y": 152, "name": "Cloudy Park 3 - 4 Exit 0", "access_rule": []}, {"room": 6, "unkn1": 126, "unkn2": 9, "x": 56, "y": 152, "name": "Cloudy Park 3 - 4 Exit 1", "access_rule": []}], "entity_load": [[7, 16], [26, 16], [14, 23], [13, 16], [68, 16]], "locations": ["Cloudy Park 3 - Enemy 9 (Galbo)", "Cloudy Park 3 - Enemy 10 (Batamon)", "Cloudy Park 3 - Enemy 11 (Bouncy)", "Cloudy Park 3 - Star 17", "Cloudy Park 3 - Star 18", "Cloudy Park 3 - Star 19", "Cloudy Park 3 - Star 20", "Cloudy Park 3 - Star 21", "Cloudy Park 3 - Star 22"], "music": 15}, {"name": "Cloudy Park 3 - 5", "level": 4, "stage": 3, "room": 5, "pointer": 2969244, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 4, "unkn1": 14, "unkn2": 9, "x": 88, "y": 152, "name": "Cloudy Park 3 - 5 Exit 0", "access_rule": []}], "entity_load": [[23, 19]], "locations": [], "music": 31}, {"name": "Cloudy Park 3 - 6", "level": 4, "stage": 3, "room": 6, "pointer": 4128530, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 7, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Cloudy Park 3 - 6 Exit 0", "access_rule": []}], "entity_load": [[23, 19]], "locations": ["Cloudy Park 3 - Mr. Ball"], "music": 8}, {"name": "Cloudy Park 3 - 7", "level": 4, "stage": 3, "room": 7, "pointer": 2885051, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Cloudy Park 3 - Complete"], "music": 5}, {"name": "Cloudy Park 4 - 0", "level": 4, "stage": 4, "room": 0, "pointer": 3072905, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 1, "unkn1": 95, "unkn2": 9, "x": 88, "y": 152, "name": "Cloudy Park 4 - 0 Exit 0", "access_rule": []}], "entity_load": [[4, 23], [1, 23], [0, 23], [31, 16]], "locations": [], "music": 21}, {"name": "Cloudy Park 4 - 1", "level": 4, "stage": 4, "room": 1, "pointer": 3074863, "animal_pointers": [208, 224], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 21, "unkn2": 9, "x": 72, "y": 152, "name": "Cloudy Park 4 - 1 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Cloudy Park 4 - Animal 1", "Cloudy Park 4 - Animal 2"], "music": 38}, {"name": "Cloudy Park 4 - 2", "level": 4, "stage": 4, "room": 2, "pointer": 3309209, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Gabon", "Como", "Wapod", "Cappy"], "default_exits": [{"room": 3, "unkn1": 145, "unkn2": 9, "x": 104, "y": 152, "name": "Cloudy Park 4 - 2 Exit 0", "access_rule": []}], "entity_load": [[88, 16], [24, 16], [12, 16], [14, 23], [41, 16], [4, 22]], "locations": ["Cloudy Park 4 - Enemy 1 (Gabon)", "Cloudy Park 4 - Enemy 2 (Como)", "Cloudy Park 4 - Enemy 3 (Wapod)", "Cloudy Park 4 - Enemy 4 (Cappy)", "Cloudy Park 4 - Star 1", "Cloudy Park 4 - Star 2", "Cloudy Park 4 - Star 3", "Cloudy Park 4 - Star 4", "Cloudy Park 4 - Star 5", "Cloudy Park 4 - Star 6", "Cloudy Park 4 - Star 7", "Cloudy Park 4 - Star 8", "Cloudy Park 4 - Star 9", "Cloudy Park 4 - Star 10", "Cloudy Park 4 - Star 11", "Cloudy Park 4 - Star 12"], "music": 21}, {"name": "Cloudy Park 4 - 3", "level": 4, "stage": 4, "room": 3, "pointer": 3296291, "animal_pointers": [], "consumables": [{"idx": 33, "pointer": 776, "x": 1880, "y": 152, "etype": 22, "vtype": 0, "name": "Cloudy Park 4 - 1-Up (Windy)"}, {"idx": 34, "pointer": 912, "x": 2160, "y": 152, "etype": 22, "vtype": 2, "name": "Cloudy Park 4 - Maxim Tomato (Windy)"}], "consumables_pointer": 304, "enemies": ["Sparky", "Togezo"], "default_exits": [{"room": 5, "unkn1": 144, "unkn2": 9, "x": 56, "y": 136, "name": "Cloudy Park 4 - 3 Exit 0", "access_rule": []}], "entity_load": [[18, 16], [8, 16], [31, 16], [14, 23], [4, 22], [4, 16], [0, 22], [2, 22]], "locations": ["Cloudy Park 4 - Enemy 5 (Sparky)", "Cloudy Park 4 - Enemy 6 (Togezo)", "Cloudy Park 4 - Star 13", "Cloudy Park 4 - Star 14", "Cloudy Park 4 - Star 15", "Cloudy Park 4 - Star 16", "Cloudy Park 4 - Star 17", "Cloudy Park 4 - Star 18", "Cloudy Park 4 - Star 19", "Cloudy Park 4 - Star 20", "Cloudy Park 4 - Star 21", "Cloudy Park 4 - Star 22", "Cloudy Park 4 - Star 23", "Cloudy Park 4 - Star 24", "Cloudy Park 4 - Star 25", "Cloudy Park 4 - Star 26", "Cloudy Park 4 - Star 27", "Cloudy Park 4 - Star 28", "Cloudy Park 4 - Star 29", "Cloudy Park 4 - Star 30", "Cloudy Park 4 - 1-Up (Windy)", "Cloudy Park 4 - Maxim Tomato (Windy)"], "music": 21}, {"name": "Cloudy Park 4 - 4", "level": 4, "stage": 4, "room": 4, "pointer": 3330996, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bronto Burt", "KeKe", "Bouncy"], "default_exits": [{"room": 7, "unkn1": 4, "unkn2": 15, "x": 72, "y": 152, "name": "Cloudy Park 4 - 4 Exit 0", "access_rule": []}], "entity_load": [[13, 16], [51, 16], [17, 16], [14, 23], [2, 16]], "locations": ["Cloudy Park 4 - Enemy 7 (Bronto Burt)", "Cloudy Park 4 - Enemy 8 (KeKe)", "Cloudy Park 4 - Enemy 9 (Bouncy)", "Cloudy Park 4 - Star 31", "Cloudy Park 4 - Star 32", "Cloudy Park 4 - Star 33", "Cloudy Park 4 - Star 34", "Cloudy Park 4 - Star 35", "Cloudy Park 4 - Star 36", "Cloudy Park 4 - Star 37", "Cloudy Park 4 - Star 38"], "music": 21}, {"name": "Cloudy Park 4 - 5", "level": 4, "stage": 4, "room": 5, "pointer": 3332300, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Sir Kibble", "Mariel"], "default_exits": [{"room": 4, "unkn1": 21, "unkn2": 51, "x": 2328, "y": 120, "name": "Cloudy Park 4 - 5 Exit 0", "access_rule": []}], "entity_load": [[45, 16], [46, 16], [27, 16], [14, 23]], "locations": ["Cloudy Park 4 - Enemy 10 (Sir Kibble)", "Cloudy Park 4 - Enemy 11 (Mariel)", "Cloudy Park 4 - Star 39", "Cloudy Park 4 - Star 40", "Cloudy Park 4 - Star 41", "Cloudy Park 4 - Star 42", "Cloudy Park 4 - Star 43"], "music": 21}, {"name": "Cloudy Park 4 - 6", "level": 4, "stage": 4, "room": 6, "pointer": 3253310, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Kabu", "Wappa"], "default_exits": [{"room": 9, "unkn1": 3, "unkn2": 6, "x": 72, "y": 152, "name": "Cloudy Park 4 - 6 Exit 0", "access_rule": []}], "entity_load": [[44, 16], [31, 16], [19, 16], [14, 23]], "locations": ["Cloudy Park 4 - Enemy 12 (Kabu)", "Cloudy Park 4 - Enemy 13 (Wappa)", "Cloudy Park 4 - Star 44", "Cloudy Park 4 - Star 45", "Cloudy Park 4 - Star 46", "Cloudy Park 4 - Star 47", "Cloudy Park 4 - Star 48", "Cloudy Park 4 - Star 49", "Cloudy Park 4 - Star 50"], "music": 21}, {"name": "Cloudy Park 4 - 7", "level": 4, "stage": 4, "room": 7, "pointer": 2967658, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 8, "unkn1": 12, "unkn2": 9, "x": 152, "y": 120, "name": "Cloudy Park 4 - 7 Exit 0", "access_rule": []}], "entity_load": [[3, 27]], "locations": ["Cloudy Park 4 - Miniboss 1 (Jumper Shoot)"], "music": 4}, {"name": "Cloudy Park 4 - 8", "level": 4, "stage": 4, "room": 8, "pointer": 2981644, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 13, "unkn2": 9, "x": 344, "y": 680, "name": "Cloudy Park 4 - 8 Exit 0", "access_rule": []}], "entity_load": [[24, 19]], "locations": [], "music": 21}, {"name": "Cloudy Park 4 - 9", "level": 4, "stage": 4, "room": 9, "pointer": 3008001, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 10, "unkn1": 13, "unkn2": 9, "x": 72, "y": 120, "name": "Cloudy Park 4 - 9 Exit 0", "access_rule": []}], "entity_load": [[24, 19], [42, 19]], "locations": ["Cloudy Park 4 - Mikarin & Kagami Mocchi"], "music": 8}, {"name": "Cloudy Park 4 - 10", "level": 4, "stage": 4, "room": 10, "pointer": 2888184, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Cloudy Park 4 - Complete"], "music": 5}, {"name": "Cloudy Park 5 - 0", "level": 4, "stage": 5, "room": 0, "pointer": 3192630, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Yaban", "Sir Kibble", "Cappy", "Wappa"], "default_exits": [{"room": 1, "unkn1": 146, "unkn2": 6, "x": 168, "y": 616, "name": "Cloudy Park 5 - 0 Exit 0", "access_rule": []}], "entity_load": [[32, 16], [27, 16], [44, 16], [12, 16], [14, 23]], "locations": ["Cloudy Park 5 - Enemy 1 (Yaban)", "Cloudy Park 5 - Enemy 2 (Sir Kibble)", "Cloudy Park 5 - Enemy 3 (Cappy)", "Cloudy Park 5 - Enemy 4 (Wappa)", "Cloudy Park 5 - Star 1", "Cloudy Park 5 - Star 2"], "music": 17}, {"name": "Cloudy Park 5 - 1", "level": 4, "stage": 5, "room": 1, "pointer": 3050956, "animal_pointers": [], "consumables": [{"idx": 5, "pointer": 264, "x": 480, "y": 720, "etype": 22, "vtype": 2, "name": "Cloudy Park 5 - Maxim Tomato (Pillars)"}], "consumables_pointer": 288, "enemies": ["Galbo", "Bronto Burt", "KeKe"], "default_exits": [{"room": 2, "unkn1": 32, "unkn2": 9, "x": 72, "y": 152, "name": "Cloudy Park 5 - 1 Exit 0", "access_rule": []}], "entity_load": [[2, 16], [51, 16], [26, 16], [14, 23], [2, 22]], "locations": ["Cloudy Park 5 - Enemy 5 (Galbo)", "Cloudy Park 5 - Enemy 6 (Bronto Burt)", "Cloudy Park 5 - Enemy 7 (KeKe)", "Cloudy Park 5 - Star 3", "Cloudy Park 5 - Star 4", "Cloudy Park 5 - Star 5", "Cloudy Park 5 - Maxim Tomato (Pillars)"], "music": 17}, {"name": "Cloudy Park 5 - 2", "level": 4, "stage": 5, "room": 2, "pointer": 3604194, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 3, "unkn1": 17, "unkn2": 9, "x": 72, "y": 88, "name": "Cloudy Park 5 - 2 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Cloudy Park 5 - Animal 1", "Cloudy Park 5 - Animal 2"], "music": 40}, {"name": "Cloudy Park 5 - 3", "level": 4, "stage": 5, "room": 3, "pointer": 3131242, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Propeller", "Klinko"], "default_exits": [{"room": 4, "unkn1": 98, "unkn2": 5, "x": 72, "y": 120, "name": "Cloudy Park 5 - 3 Exit 0", "access_rule": []}], "entity_load": [[89, 16], [4, 16], [42, 16], [4, 23]], "locations": ["Cloudy Park 5 - Enemy 8 (Propeller)", "Cloudy Park 5 - Enemy 9 (Klinko)"], "music": 17}, {"name": "Cloudy Park 5 - 4", "level": 4, "stage": 5, "room": 4, "pointer": 2990116, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Wapod"], "default_exits": [{"room": 5, "unkn1": 23, "unkn2": 7, "x": 216, "y": 744, "name": "Cloudy Park 5 - 4 Exit 0", "access_rule": []}], "entity_load": [[88, 16]], "locations": ["Cloudy Park 5 - Enemy 10 (Wapod)"], "music": 17}, {"name": "Cloudy Park 5 - 5", "level": 4, "stage": 5, "room": 5, "pointer": 2975410, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 5, "unkn2": 4, "x": 104, "y": 296, "name": "Cloudy Park 5 - 5 Exit 0", "access_rule": []}], "entity_load": [[1, 16], [14, 23], [2, 16]], "locations": ["Cloudy Park 5 - Star 6"], "music": 17}, {"name": "Cloudy Park 5 - 6", "level": 4, "stage": 5, "room": 6, "pointer": 3173683, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Pteran"], "default_exits": [{"room": 7, "unkn1": 115, "unkn2": 6, "x": 56, "y": 136, "name": "Cloudy Park 5 - 6 Exit 0", "access_rule": []}], "entity_load": [[2, 16], [1, 23], [39, 16], [70, 16]], "locations": ["Cloudy Park 5 - Enemy 11 (Pteran)"], "music": 17}, {"name": "Cloudy Park 5 - 7", "level": 4, "stage": 5, "room": 7, "pointer": 2992340, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 8, "unkn1": 14, "unkn2": 8, "x": 72, "y": 120, "name": "Cloudy Park 5 - 7 Exit 0", "access_rule": []}], "entity_load": [[25, 19], [42, 19]], "locations": ["Cloudy Park 5 - Pick"], "music": 8}, {"name": "Cloudy Park 5 - 8", "level": 4, "stage": 5, "room": 8, "pointer": 2891558, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Cloudy Park 5 - Complete"], "music": 5}, {"name": "Cloudy Park 6 - 0", "level": 4, "stage": 6, "room": 0, "pointer": 3269847, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 65, "unkn2": 9, "x": 88, "y": 152, "name": "Cloudy Park 6 - 0 Exit 0", "access_rule": []}], "entity_load": [[14, 23]], "locations": ["Cloudy Park 6 - Star 1"], "music": 11}, {"name": "Cloudy Park 6 - 1", "level": 4, "stage": 6, "room": 1, "pointer": 3252248, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 10, "unkn1": 3, "unkn2": 25, "x": 72, "y": 72, "name": "Cloudy Park 6 - 1 Exit 0", "access_rule": []}], "entity_load": [[14, 16], [55, 16]], "locations": [], "music": 11}, {"name": "Cloudy Park 6 - 2", "level": 4, "stage": 6, "room": 2, "pointer": 3028494, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Madoo"], "default_exits": [{"room": 0, "unkn1": 4, "unkn2": 9, "x": 1032, "y": 152, "name": "Cloudy Park 6 - 2 Exit 0", "access_rule": []}, {"room": 1, "unkn1": 13, "unkn2": 9, "x": 56, "y": 72, "name": "Cloudy Park 6 - 2 Exit 1", "access_rule": []}], "entity_load": [[58, 16], [14, 23]], "locations": ["Cloudy Park 6 - Enemy 1 (Madoo)", "Cloudy Park 6 - Star 2", "Cloudy Park 6 - Star 3", "Cloudy Park 6 - Star 4", "Cloudy Park 6 - Star 5"], "music": 11}, {"name": "Cloudy Park 6 - 3", "level": 4, "stage": 6, "room": 3, "pointer": 3131911, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 10, "unkn1": 4, "unkn2": 9, "x": 72, "y": 152, "name": "Cloudy Park 6 - 3 Exit 0", "access_rule": []}, {"room": 10, "unkn1": 8, "unkn2": 9, "x": 136, "y": 152, "name": "Cloudy Park 6 - 3 Exit 1", "access_rule": []}, {"room": 10, "unkn1": 12, "unkn2": 9, "x": 200, "y": 152, "name": "Cloudy Park 6 - 3 Exit 2", "access_rule": []}, {"room": 10, "unkn1": 16, "unkn2": 9, "x": 264, "y": 152, "name": "Cloudy Park 6 - 3 Exit 3", "access_rule": []}, {"room": 10, "unkn1": 20, "unkn2": 9, "x": 328, "y": 152, "name": "Cloudy Park 6 - 3 Exit 4", "access_rule": []}], "entity_load": [[58, 16]], "locations": [], "music": 11}, {"name": "Cloudy Park 6 - 4", "level": 4, "stage": 6, "room": 4, "pointer": 3115416, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Tick", "Como"], "default_exits": [{"room": 8, "unkn1": 20, "unkn2": 4, "x": 72, "y": 488, "name": "Cloudy Park 6 - 4 Exit 0", "access_rule": []}, {"room": 11, "unkn1": 4, "unkn2": 9, "x": 72, "y": 152, "name": "Cloudy Park 6 - 4 Exit 1", "access_rule": []}, {"room": 11, "unkn1": 8, "unkn2": 9, "x": 200, "y": 152, "name": "Cloudy Park 6 - 4 Exit 2", "access_rule": []}, {"room": 11, "unkn1": 12, "unkn2": 9, "x": 328, "y": 152, "name": "Cloudy Park 6 - 4 Exit 3", "access_rule": []}, {"room": 11, "unkn1": 16, "unkn2": 9, "x": 136, "y": 152, "name": "Cloudy Park 6 - 4 Exit 4", "access_rule": []}, {"room": 11, "unkn1": 20, "unkn2": 9, "x": 264, "y": 152, "name": "Cloudy Park 6 - 4 Exit 5", "access_rule": []}], "entity_load": [[55, 16], [48, 16], [41, 16], [14, 23]], "locations": ["Cloudy Park 6 - Enemy 2 (Tick)", "Cloudy Park 6 - Enemy 3 (Como)", "Cloudy Park 6 - Star 6", "Cloudy Park 6 - Star 7", "Cloudy Park 6 - Star 8"], "music": 11}, {"name": "Cloudy Park 6 - 5", "level": 4, "stage": 6, "room": 5, "pointer": 3245809, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Waddle Dee Drawing", "Bronto Burt Drawing", "Bouncy Drawing"], "default_exits": [{"room": 15, "unkn1": 65, "unkn2": 9, "x": 72, "y": 152, "name": "Cloudy Park 6 - 5 Exit 0", "access_rule": []}], "entity_load": [[84, 16], [85, 16], [86, 16], [14, 23], [4, 22]], "locations": ["Cloudy Park 6 - Enemy 4 (Waddle Dee Drawing)", "Cloudy Park 6 - Enemy 5 (Bronto Burt Drawing)", "Cloudy Park 6 - Enemy 6 (Bouncy Drawing)", "Cloudy Park 6 - Star 9", "Cloudy Park 6 - Star 10", "Cloudy Park 6 - Star 11", "Cloudy Park 6 - Star 12", "Cloudy Park 6 - Star 13"], "music": 11}, {"name": "Cloudy Park 6 - 6", "level": 4, "stage": 6, "room": 6, "pointer": 3237044, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Propeller"], "default_exits": [{"room": 7, "unkn1": 56, "unkn2": 9, "x": 72, "y": 136, "name": "Cloudy Park 6 - 6 Exit 0", "access_rule": []}], "entity_load": [[89, 16]], "locations": ["Cloudy Park 6 - Enemy 7 (Propeller)"], "music": 11}, {"name": "Cloudy Park 6 - 7", "level": 4, "stage": 6, "room": 7, "pointer": 3262705, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Mopoo"], "default_exits": [{"room": 13, "unkn1": 12, "unkn2": 8, "x": 184, "y": 216, "name": "Cloudy Park 6 - 7 Exit 0", "access_rule": []}, {"room": 14, "unkn1": 57, "unkn2": 8, "x": 184, "y": 216, "name": "Cloudy Park 6 - 7 Exit 1", "access_rule": []}, {"room": 9, "unkn1": 64, "unkn2": 8, "x": 72, "y": 120, "name": "Cloudy Park 6 - 7 Exit 2", "access_rule": []}], "entity_load": [[74, 16], [14, 23]], "locations": ["Cloudy Park 6 - Enemy 8 (Mopoo)", "Cloudy Park 6 - Star 14", "Cloudy Park 6 - Star 15", "Cloudy Park 6 - Star 16", "Cloudy Park 6 - Star 17"], "music": 11}, {"name": "Cloudy Park 6 - 8", "level": 4, "stage": 6, "room": 8, "pointer": 3027259, "animal_pointers": [], "consumables": [{"idx": 22, "pointer": 312, "x": 224, "y": 256, "etype": 22, "vtype": 0, "name": "Cloudy Park 6 - 1-Up (Cutter)"}], "consumables_pointer": 304, "enemies": ["Bukiset (Burning)", "Bukiset (Ice)", "Bukiset (Needle)", "Bukiset (Clean)", "Bukiset (Cutter)"], "default_exits": [{"room": 12, "unkn1": 13, "unkn2": 4, "x": 88, "y": 152, "name": "Cloudy Park 6 - 8 Exit 0", "access_rule": []}], "entity_load": [[78, 16], [80, 16], [76, 16], [79, 16], [83, 16], [14, 23], [4, 22], [0, 22]], "locations": ["Cloudy Park 6 - Enemy 9 (Bukiset (Burning))", "Cloudy Park 6 - Enemy 10 (Bukiset (Ice))", "Cloudy Park 6 - Enemy 11 (Bukiset (Needle))", "Cloudy Park 6 - Enemy 12 (Bukiset (Clean))", "Cloudy Park 6 - Enemy 13 (Bukiset (Cutter))", "Cloudy Park 6 - Star 18", "Cloudy Park 6 - Star 19", "Cloudy Park 6 - Star 20", "Cloudy Park 6 - Star 21", "Cloudy Park 6 - Star 22", "Cloudy Park 6 - Star 23", "Cloudy Park 6 - Star 24", "Cloudy Park 6 - Star 25", "Cloudy Park 6 - Star 26", "Cloudy Park 6 - Star 27", "Cloudy Park 6 - Star 28", "Cloudy Park 6 - Star 29", "Cloudy Park 6 - 1-Up (Cutter)"], "music": 11}, {"name": "Cloudy Park 6 - 9", "level": 4, "stage": 6, "room": 9, "pointer": 3089504, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 4, "unkn1": 35, "unkn2": 7, "x": 72, "y": 72, "name": "Cloudy Park 6 - 9 Exit 0", "access_rule": []}], "entity_load": [[41, 16]], "locations": [], "music": 11}, {"name": "Cloudy Park 6 - 10", "level": 4, "stage": 6, "room": 10, "pointer": 3132579, "animal_pointers": [242, 250, 258], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 20, "unkn2": 4, "x": 72, "y": 152, "name": "Cloudy Park 6 - 10 Exit 0", "access_rule": []}, {"room": 3, "unkn1": 4, "unkn2": 9, "x": 88, "y": 152, "name": "Cloudy Park 6 - 10 Exit 1", "access_rule": []}, {"room": 3, "unkn1": 8, "unkn2": 9, "x": 136, "y": 152, "name": "Cloudy Park 6 - 10 Exit 2", "access_rule": []}, {"room": 3, "unkn1": 12, "unkn2": 9, "x": 200, "y": 152, "name": "Cloudy Park 6 - 10 Exit 3", "access_rule": []}, {"room": 3, "unkn1": 16, "unkn2": 9, "x": 264, "y": 152, "name": "Cloudy Park 6 - 10 Exit 4", "access_rule": []}, {"room": 3, "unkn1": 20, "unkn2": 9, "x": 328, "y": 152, "name": "Cloudy Park 6 - 10 Exit 5", "access_rule": []}], "entity_load": [], "locations": ["Cloudy Park 6 - Animal 1", "Cloudy Park 6 - Animal 2", "Cloudy Park 6 - Animal 3"], "music": 40}, {"name": "Cloudy Park 6 - 11", "level": 4, "stage": 6, "room": 11, "pointer": 3017866, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 4, "unkn1": 4, "unkn2": 9, "x": 72, "y": 152, "name": "Cloudy Park 6 - 11 Exit 0", "access_rule": []}, {"room": 4, "unkn1": 8, "unkn2": 9, "x": 264, "y": 152, "name": "Cloudy Park 6 - 11 Exit 1", "access_rule": []}, {"room": 4, "unkn1": 12, "unkn2": 9, "x": 136, "y": 152, "name": "Cloudy Park 6 - 11 Exit 2", "access_rule": []}, {"room": 4, "unkn1": 16, "unkn2": 9, "x": 328, "y": 152, "name": "Cloudy Park 6 - 11 Exit 3", "access_rule": []}, {"room": 4, "unkn1": 20, "unkn2": 9, "x": 200, "y": 152, "name": "Cloudy Park 6 - 11 Exit 4", "access_rule": []}], "entity_load": [[58, 16]], "locations": [], "music": 11}, {"name": "Cloudy Park 6 - 12", "level": 4, "stage": 6, "room": 12, "pointer": 3036404, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 8, "unkn1": 4, "unkn2": 9, "x": 200, "y": 72, "name": "Cloudy Park 6 - 12 Exit 0", "access_rule": []}, {"room": 5, "unkn1": 13, "unkn2": 9, "x": 88, "y": 152, "name": "Cloudy Park 6 - 12 Exit 1", "access_rule": []}], "entity_load": [[58, 16], [14, 23]], "locations": ["Cloudy Park 6 - Star 30", "Cloudy Park 6 - Star 31", "Cloudy Park 6 - Star 32", "Cloudy Park 6 - Star 33"], "music": 11}, {"name": "Cloudy Park 6 - 13", "level": 4, "stage": 6, "room": 13, "pointer": 2965251, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 7, "unkn1": 10, "unkn2": 13, "x": 216, "y": 136, "name": "Cloudy Park 6 - 13 Exit 0", "access_rule": []}], "entity_load": [[26, 19]], "locations": [], "music": 11}, {"name": "Cloudy Park 6 - 14", "level": 4, "stage": 6, "room": 14, "pointer": 3077236, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 7, "unkn1": 10, "unkn2": 13, "x": 936, "y": 136, "name": "Cloudy Park 6 - 14 Exit 0", "access_rule": []}], "entity_load": [], "locations": [], "music": 11}, {"name": "Cloudy Park 6 - 15", "level": 4, "stage": 6, "room": 15, "pointer": 3061794, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 16, "unkn1": 13, "unkn2": 9, "x": 72, "y": 120, "name": "Cloudy Park 6 - 15 Exit 0", "access_rule": []}], "entity_load": [[26, 19], [42, 19]], "locations": ["Cloudy Park 6 - HB-007"], "music": 8}, {"name": "Cloudy Park 6 - 16", "level": 4, "stage": 6, "room": 16, "pointer": 2888907, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Cloudy Park 6 - Complete"], "music": 5}, {"name": "Cloudy Park Boss - 0", "level": 4, "stage": 7, "room": 0, "pointer": 2998682, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[3, 18]], "locations": ["Cloudy Park - Boss (Ado) Purified", "Level 4 Boss - Defeated", "Level 4 Boss - Purified"], "music": 2}, {"name": "Iceberg 1 - 0", "level": 5, "stage": 1, "room": 0, "pointer": 3363111, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Waddle Dee", "Klinko", "KeKe"], "default_exits": [{"room": 1, "unkn1": 104, "unkn2": 8, "x": 312, "y": 1384, "name": "Iceberg 1 - 0 Exit 0", "access_rule": []}], "entity_load": [[42, 16], [0, 16], [51, 16]], "locations": ["Iceberg 1 - Enemy 1 (Waddle Dee)", "Iceberg 1 - Enemy 2 (Klinko)", "Iceberg 1 - Enemy 3 (KeKe)"], "music": 18}, {"name": "Iceberg 1 - 1", "level": 5, "stage": 1, "room": 1, "pointer": 3596524, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Como", "Galbo", "Rocky"], "default_exits": [{"room": 2, "unkn1": 20, "unkn2": 8, "x": 72, "y": 344, "name": "Iceberg 1 - 1 Exit 0", "access_rule": []}], "entity_load": [[3, 16], [41, 16], [26, 16], [14, 23], [4, 22], [6, 23]], "locations": ["Iceberg 1 - Enemy 4 (Como)", "Iceberg 1 - Enemy 5 (Galbo)", "Iceberg 1 - Enemy 6 (Rocky)", "Iceberg 1 - Star 1", "Iceberg 1 - Star 2", "Iceberg 1 - Star 3", "Iceberg 1 - Star 4", "Iceberg 1 - Star 5", "Iceberg 1 - Star 6"], "music": 18}, {"name": "Iceberg 1 - 2", "level": 5, "stage": 1, "room": 2, "pointer": 3288880, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Kapar"], "default_exits": [{"room": 3, "unkn1": 49, "unkn2": 9, "x": 184, "y": 152, "name": "Iceberg 1 - 2 Exit 0", "access_rule": []}, {"room": 5, "unkn1": 94, "unkn2": 21, "x": 120, "y": 168, "name": "Iceberg 1 - 2 Exit 1", "access_rule": []}], "entity_load": [[28, 19], [46, 16], [47, 16], [17, 16], [67, 16]], "locations": ["Iceberg 1 - Enemy 7 (Kapar)"], "music": 18}, {"name": "Iceberg 1 - 3", "level": 5, "stage": 1, "room": 3, "pointer": 3068439, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 10, "unkn2": 9, "x": 808, "y": 152, "name": "Iceberg 1 - 3 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Iceberg 1 - Animal 1", "Iceberg 1 - Animal 2"], "music": 38}, {"name": "Iceberg 1 - 4", "level": 5, "stage": 1, "room": 4, "pointer": 3233681, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Mopoo", "Babut", "Wappa"], "default_exits": [{"room": 6, "unkn1": 74, "unkn2": 4, "x": 56, "y": 152, "name": "Iceberg 1 - 4 Exit 0", "access_rule": []}], "entity_load": [[44, 16], [43, 16], [74, 16]], "locations": ["Iceberg 1 - Enemy 8 (Mopoo)", "Iceberg 1 - Enemy 9 (Babut)", "Iceberg 1 - Enemy 10 (Wappa)"], "music": 18}, {"name": "Iceberg 1 - 5", "level": 5, "stage": 1, "room": 5, "pointer": 3406133, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bronto Burt", "Chilly", "Poppy Bros Jr."], "default_exits": [{"room": 4, "unkn1": 196, "unkn2": 9, "x": 72, "y": 744, "name": "Iceberg 1 - 5 Exit 0", "access_rule": []}], "entity_load": [[6, 16], [7, 16], [2, 16], [0, 16]], "locations": ["Iceberg 1 - Enemy 11 (Bronto Burt)", "Iceberg 1 - Enemy 12 (Chilly)", "Iceberg 1 - Enemy 13 (Poppy Bros Jr.)"], "music": 18}, {"name": "Iceberg 1 - 6", "level": 5, "stage": 1, "room": 6, "pointer": 2985823, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 7, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Iceberg 1 - 6 Exit 0", "access_rule": []}], "entity_load": [[28, 19], [42, 19]], "locations": ["Iceberg 1 - Kogoesou"], "music": 8}, {"name": "Iceberg 1 - 7", "level": 5, "stage": 1, "room": 7, "pointer": 2892040, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Iceberg 1 - Complete"], "music": 5}, {"name": "Iceberg 2 - 0", "level": 5, "stage": 2, "room": 0, "pointer": 3106800, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Gabon", "Nruff"], "default_exits": [{"room": 1, "unkn1": 113, "unkn2": 36, "x": 88, "y": 152, "name": "Iceberg 2 - 0 Exit 0", "access_rule": []}], "entity_load": [[15, 16], [0, 16], [24, 16]], "locations": ["Iceberg 2 - Enemy 1 (Gabon)", "Iceberg 2 - Enemy 2 (Nruff)"], "music": 20}, {"name": "Iceberg 2 - 1", "level": 5, "stage": 2, "room": 1, "pointer": 3334841, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Waddle Dee", "Chilly", "Pteran"], "default_exits": [{"room": 2, "unkn1": 109, "unkn2": 20, "x": 88, "y": 72, "name": "Iceberg 2 - 1 Exit 0", "access_rule": []}], "entity_load": [[6, 16], [0, 16], [4, 16], [39, 16]], "locations": ["Iceberg 2 - Enemy 3 (Waddle Dee)", "Iceberg 2 - Enemy 4 (Chilly)", "Iceberg 2 - Enemy 5 (Pteran)"], "music": 20}, {"name": "Iceberg 2 - 2", "level": 5, "stage": 2, "room": 2, "pointer": 3473408, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Glunk", "Galbo", "Babut", "Magoo"], "default_exits": [{"room": 6, "unkn1": 102, "unkn2": 5, "x": 88, "y": 152, "name": "Iceberg 2 - 2 Exit 0", "access_rule": []}, {"room": 9, "unkn1": 24, "unkn2": 18, "x": 88, "y": 152, "name": "Iceberg 2 - 2 Exit 1", "access_rule": []}, {"room": 3, "unkn1": 37, "unkn2": 26, "x": 200, "y": 184, "name": "Iceberg 2 - 2 Exit 2", "access_rule": []}, {"room": 4, "unkn1": 55, "unkn2": 26, "x": 200, "y": 184, "name": "Iceberg 2 - 2 Exit 3", "access_rule": []}, {"room": 5, "unkn1": 73, "unkn2": 26, "x": 200, "y": 184, "name": "Iceberg 2 - 2 Exit 4", "access_rule": []}], "entity_load": [[53, 16], [26, 16], [43, 16], [14, 23], [16, 16]], "locations": ["Iceberg 2 - Enemy 6 (Glunk)", "Iceberg 2 - Enemy 7 (Galbo)", "Iceberg 2 - Enemy 8 (Babut)", "Iceberg 2 - Enemy 9 (Magoo)", "Iceberg 2 - Star 1", "Iceberg 2 - Star 2"], "music": 20}, {"name": "Iceberg 2 - 3", "level": 5, "stage": 2, "room": 3, "pointer": 3037006, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 11, "unkn2": 11, "x": 616, "y": 424, "name": "Iceberg 2 - 3 Exit 0", "access_rule": []}], "entity_load": [[29, 19], [14, 23]], "locations": ["Iceberg 2 - Star 3"], "music": 20}, {"name": "Iceberg 2 - 4", "level": 5, "stage": 2, "room": 4, "pointer": 3035198, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 11, "unkn2": 11, "x": 904, "y": 424, "name": "Iceberg 2 - 4 Exit 0", "access_rule": []}], "entity_load": [[29, 19], [14, 23]], "locations": ["Iceberg 2 - Star 4", "Iceberg 2 - Star 5"], "music": 20}, {"name": "Iceberg 2 - 5", "level": 5, "stage": 2, "room": 5, "pointer": 3128551, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 11, "unkn2": 11, "x": 1192, "y": 424, "name": "Iceberg 2 - 5 Exit 0", "access_rule": []}], "entity_load": [[29, 19], [14, 23]], "locations": ["Iceberg 2 - Star 6", "Iceberg 2 - Star 7", "Iceberg 2 - Star 8"], "music": 20}, {"name": "Iceberg 2 - 6", "level": 5, "stage": 2, "room": 6, "pointer": 3270857, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Propeller", "Nidoo", "Oro"], "default_exits": [{"room": 7, "unkn1": 65, "unkn2": 9, "x": 88, "y": 152, "name": "Iceberg 2 - 6 Exit 0", "access_rule": []}], "entity_load": [[62, 16], [4, 22], [89, 16], [28, 16], [25, 16]], "locations": ["Iceberg 2 - Enemy 10 (Propeller)", "Iceberg 2 - Enemy 11 (Nidoo)", "Iceberg 2 - Enemy 12 (Oro)"], "music": 20}, {"name": "Iceberg 2 - 7", "level": 5, "stage": 2, "room": 7, "pointer": 3212501, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Klinko", "Bronto Burt"], "default_exits": [{"room": 8, "unkn1": 124, "unkn2": 8, "x": 72, "y": 152, "name": "Iceberg 2 - 7 Exit 0", "access_rule": []}], "entity_load": [[42, 16], [6, 16], [14, 23], [2, 16], [4, 23]], "locations": ["Iceberg 2 - Enemy 13 (Klinko)", "Iceberg 2 - Enemy 14 (Bronto Burt)", "Iceberg 2 - Star 9", "Iceberg 2 - Star 10", "Iceberg 2 - Star 11", "Iceberg 2 - Star 12", "Iceberg 2 - Star 13", "Iceberg 2 - Star 14", "Iceberg 2 - Star 15", "Iceberg 2 - Star 16", "Iceberg 2 - Star 17", "Iceberg 2 - Star 18", "Iceberg 2 - Star 19"], "music": 20}, {"name": "Iceberg 2 - 8", "level": 5, "stage": 2, "room": 8, "pointer": 2994515, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 10, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Iceberg 2 - 8 Exit 0", "access_rule": []}], "entity_load": [[29, 19], [42, 19]], "locations": ["Iceberg 2 - Samus"], "music": 8}, {"name": "Iceberg 2 - 9", "level": 5, "stage": 2, "room": 9, "pointer": 3058084, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 4, "unkn2": 9, "x": 408, "y": 296, "name": "Iceberg 2 - 9 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Iceberg 2 - Animal 1", "Iceberg 2 - Animal 2"], "music": 39}, {"name": "Iceberg 2 - 10", "level": 5, "stage": 2, "room": 10, "pointer": 2887220, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Iceberg 2 - Complete"], "music": 5}, {"name": "Iceberg 3 - 0", "level": 5, "stage": 3, "room": 0, "pointer": 3455346, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Corori", "Bouncy", "Chilly", "Pteran"], "default_exits": [{"room": 1, "unkn1": 98, "unkn2": 6, "x": 200, "y": 232, "name": "Iceberg 3 - 0 Exit 0", "access_rule": []}, {"room": 2, "unkn1": 217, "unkn2": 7, "x": 40, "y": 120, "name": "Iceberg 3 - 0 Exit 1", "access_rule": []}], "entity_load": [[60, 16], [6, 16], [39, 16], [13, 16], [14, 23]], "locations": ["Iceberg 3 - Enemy 1 (Corori)", "Iceberg 3 - Enemy 2 (Bouncy)", "Iceberg 3 - Enemy 3 (Chilly)", "Iceberg 3 - Enemy 4 (Pteran)", "Iceberg 3 - Star 1", "Iceberg 3 - Star 2"], "music": 14}, {"name": "Iceberg 3 - 1", "level": 5, "stage": 3, "room": 1, "pointer": 3019769, "animal_pointers": [192, 200, 208], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 0, "unkn1": 11, "unkn2": 14, "x": 1592, "y": 104, "name": "Iceberg 3 - 1 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Iceberg 3 - Animal 1", "Iceberg 3 - Animal 2", "Iceberg 3 - Animal 3"], "music": 38}, {"name": "Iceberg 3 - 2", "level": 5, "stage": 3, "room": 2, "pointer": 3618121, "animal_pointers": [], "consumables": [{"idx": 2, "pointer": 352, "x": 1776, "y": 104, "etype": 22, "vtype": 2, "name": "Iceberg 3 - Maxim Tomato (Ceiling)"}], "consumables_pointer": 128, "enemies": ["Raft Waddle Dee", "Kapar", "Blipper"], "default_exits": [{"room": 3, "unkn1": 196, "unkn2": 11, "x": 72, "y": 152, "name": "Iceberg 3 - 2 Exit 0", "access_rule": []}], "entity_load": [[2, 22], [71, 16], [57, 16], [21, 16], [67, 16], [14, 23]], "locations": ["Iceberg 3 - Enemy 5 (Raft Waddle Dee)", "Iceberg 3 - Enemy 6 (Kapar)", "Iceberg 3 - Enemy 7 (Blipper)", "Iceberg 3 - Star 3", "Iceberg 3 - Maxim Tomato (Ceiling)"], "music": 14}, {"name": "Iceberg 3 - 3", "level": 5, "stage": 3, "room": 3, "pointer": 3650368, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Wapod"], "default_exits": [{"room": 5, "unkn1": 116, "unkn2": 11, "x": 40, "y": 152, "name": "Iceberg 3 - 3 Exit 0", "access_rule": []}, {"room": 4, "unkn1": 63, "unkn2": 13, "x": 184, "y": 136, "name": "Iceberg 3 - 3 Exit 1", "access_rule": []}], "entity_load": [[1, 16], [14, 23], [4, 22], [6, 16], [88, 16]], "locations": ["Iceberg 3 - Enemy 8 (Wapod)", "Iceberg 3 - Star 4", "Iceberg 3 - Star 5", "Iceberg 3 - Star 6", "Iceberg 3 - Star 7", "Iceberg 3 - Star 8", "Iceberg 3 - Star 9", "Iceberg 3 - Star 10"], "music": 14}, {"name": "Iceberg 3 - 4", "level": 5, "stage": 3, "room": 4, "pointer": 3038208, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 3, "unkn1": 10, "unkn2": 8, "x": 1032, "y": 216, "name": "Iceberg 3 - 4 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Iceberg 3 - Animal 4", "Iceberg 3 - Animal 5"], "music": 39}, {"name": "Iceberg 3 - 5", "level": 5, "stage": 3, "room": 5, "pointer": 3013938, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 15, "unkn2": 9, "x": 72, "y": 120, "name": "Iceberg 3 - 5 Exit 0", "access_rule": []}], "entity_load": [[30, 19]], "locations": [], "music": 31}, {"name": "Iceberg 3 - 6", "level": 5, "stage": 3, "room": 6, "pointer": 3624789, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Glunk", "Icicle"], "default_exits": [{"room": 7, "unkn1": 211, "unkn2": 7, "x": 40, "y": 152, "name": "Iceberg 3 - 6 Exit 0", "access_rule": []}], "entity_load": [[6, 16], [16, 16], [14, 23], [36, 16], [4, 22]], "locations": ["Iceberg 3 - Enemy 9 (Glunk)", "Iceberg 3 - Enemy 10 (Icicle)", "Iceberg 3 - Star 11", "Iceberg 3 - Star 12", "Iceberg 3 - Star 13", "Iceberg 3 - Star 14", "Iceberg 3 - Star 15", "Iceberg 3 - Star 16", "Iceberg 3 - Star 17", "Iceberg 3 - Star 18", "Iceberg 3 - Star 19", "Iceberg 3 - Star 20", "Iceberg 3 - Star 21"], "music": 14}, {"name": "Iceberg 3 - 7", "level": 5, "stage": 3, "room": 7, "pointer": 2989472, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 8, "unkn1": 15, "unkn2": 9, "x": 72, "y": 120, "name": "Iceberg 3 - 7 Exit 0", "access_rule": []}], "entity_load": [[30, 19]], "locations": ["Iceberg 3 - Chef Kawasaki"], "music": 8}, {"name": "Iceberg 3 - 8", "level": 5, "stage": 3, "room": 8, "pointer": 2889871, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Iceberg 3 - Complete"], "music": 5}, {"name": "Iceberg 4 - 0", "level": 5, "stage": 4, "room": 0, "pointer": 3274879, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bronto Burt", "Galbo", "Klinko", "Chilly"], "default_exits": [{"room": 1, "unkn1": 111, "unkn2": 8, "x": 72, "y": 104, "name": "Iceberg 4 - 0 Exit 0", "access_rule": []}], "entity_load": [[42, 16], [6, 16], [2, 16], [26, 16]], "locations": ["Iceberg 4 - Enemy 1 (Bronto Burt)", "Iceberg 4 - Enemy 2 (Galbo)", "Iceberg 4 - Enemy 3 (Klinko)", "Iceberg 4 - Enemy 4 (Chilly)"], "music": 19}, {"name": "Iceberg 4 - 1", "level": 5, "stage": 4, "room": 1, "pointer": 3371780, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Babut", "Wappa"], "default_exits": [{"room": 2, "unkn1": 60, "unkn2": 36, "x": 216, "y": 88, "name": "Iceberg 4 - 1 Exit 0", "access_rule": []}], "entity_load": [[43, 16], [4, 22], [44, 16]], "locations": ["Iceberg 4 - Enemy 5 (Babut)", "Iceberg 4 - Enemy 6 (Wappa)"], "music": 19}, {"name": "Iceberg 4 - 2", "level": 5, "stage": 4, "room": 2, "pointer": 3284378, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 16, "unkn1": 8, "unkn2": 39, "x": 168, "y": 152, "name": "Iceberg 4 - 2 Exit 0", "access_rule": ["Burning", "Burning Ability"]}, {"room": 3, "unkn1": 13, "unkn2": 39, "x": 88, "y": 136, "name": "Iceberg 4 - 2 Exit 1", "access_rule": []}, {"room": 17, "unkn1": 18, "unkn2": 39, "x": 120, "y": 152, "name": "Iceberg 4 - 2 Exit 2", "access_rule": ["Burning", "Burning Ability"]}], "entity_load": [[26, 16]], "locations": [], "music": 19}, {"name": "Iceberg 4 - 3", "level": 5, "stage": 4, "room": 3, "pointer": 3162957, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 4, "unkn1": 44, "unkn2": 8, "x": 216, "y": 104, "name": "Iceberg 4 - 3 Exit 0", "access_rule": []}], "entity_load": [[69, 16]], "locations": [], "music": 19}, {"name": "Iceberg 4 - 4", "level": 5, "stage": 4, "room": 4, "pointer": 3261679, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Icicle"], "default_exits": [{"room": 5, "unkn1": 4, "unkn2": 42, "x": 104, "y": 840, "name": "Iceberg 4 - 4 Exit 0", "access_rule": []}], "entity_load": [[36, 16]], "locations": ["Iceberg 4 - Enemy 7 (Icicle)"], "music": 19}, {"name": "Iceberg 4 - 5", "level": 5, "stage": 4, "room": 5, "pointer": 3217398, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Corori"], "default_exits": [{"room": 6, "unkn1": 19, "unkn2": 5, "x": 72, "y": 120, "name": "Iceberg 4 - 5 Exit 0", "access_rule": []}], "entity_load": [[60, 16], [44, 16], [4, 22]], "locations": ["Iceberg 4 - Enemy 8 (Corori)"], "music": 19}, {"name": "Iceberg 4 - 6", "level": 5, "stage": 4, "room": 6, "pointer": 3108265, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 7, "unkn1": 61, "unkn2": 7, "x": 456, "y": 72, "name": "Iceberg 4 - 6 Exit 0", "access_rule": []}], "entity_load": [[62, 16]], "locations": [], "music": 19}, {"name": "Iceberg 4 - 7", "level": 5, "stage": 4, "room": 7, "pointer": 3346202, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 8, "unkn1": 39, "unkn2": 6, "x": 168, "y": 104, "name": "Iceberg 4 - 7 Exit 0", "access_rule": []}, {"room": 13, "unkn1": 4, "unkn2": 21, "x": 88, "y": 168, "name": "Iceberg 4 - 7 Exit 1", "access_rule": ["Burning", "Burning Ability"]}, {"room": 13, "unkn1": 21, "unkn2": 21, "x": 280, "y": 168, "name": "Iceberg 4 - 7 Exit 2", "access_rule": ["Burning", "Burning Ability"]}], "entity_load": [[14, 23], [4, 22], [4, 23]], "locations": ["Iceberg 4 - Star 1", "Iceberg 4 - Star 2"], "music": 19}, {"name": "Iceberg 4 - 8", "level": 5, "stage": 4, "room": 8, "pointer": 3055911, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 7, "unkn1": 10, "unkn2": 5, "x": 648, "y": 104, "name": "Iceberg 4 - 8 Exit 0", "access_rule": []}], "entity_load": [[26, 16]], "locations": [], "music": 19}, {"name": "Iceberg 4 - 9", "level": 5, "stage": 4, "room": 9, "pointer": 3056457, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 11, "unkn1": 13, "unkn2": 9, "x": 136, "y": 136, "name": "Iceberg 4 - 9 Exit 0", "access_rule": []}], "entity_load": [[1, 27]], "locations": ["Iceberg 4 - Miniboss 1 (Yuki)"], "music": 4}, {"name": "Iceberg 4 - 10", "level": 5, "stage": 4, "room": 10, "pointer": 3257516, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 9, "unkn1": 8, "unkn2": 37, "x": 88, "y": 40, "name": "Iceberg 4 - 10 Exit 0", "access_rule": []}, {"room": 9, "unkn1": 15, "unkn2": 37, "x": 200, "y": 40, "name": "Iceberg 4 - 10 Exit 1", "access_rule": []}], "entity_load": [[31, 19]], "locations": [], "music": 19}, {"name": "Iceberg 4 - 11", "level": 5, "stage": 4, "room": 11, "pointer": 3083322, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Gabon"], "default_exits": [{"room": 12, "unkn1": 46, "unkn2": 8, "x": 88, "y": 120, "name": "Iceberg 4 - 11 Exit 0", "access_rule": []}], "entity_load": [[24, 16]], "locations": ["Iceberg 4 - Enemy 9 (Gabon)"], "music": 19}, {"name": "Iceberg 4 - 12", "level": 5, "stage": 4, "room": 12, "pointer": 3147724, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Kabu"], "default_exits": [{"room": 18, "unkn1": 64, "unkn2": 7, "x": 72, "y": 456, "name": "Iceberg 4 - 12 Exit 0", "access_rule": []}], "entity_load": [[19, 16], [61, 16]], "locations": ["Iceberg 4 - Enemy 10 (Kabu)"], "music": 19}, {"name": "Iceberg 4 - 13", "level": 5, "stage": 4, "room": 13, "pointer": 3370077, "animal_pointers": [232, 240, 248], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 15, "unkn1": 31, "unkn2": 4, "x": 216, "y": 120, "name": "Iceberg 4 - 13 Exit 0", "access_rule": []}, {"room": 10, "unkn1": 46, "unkn2": 10, "x": 72, "y": 88, "name": "Iceberg 4 - 13 Exit 1", "access_rule": []}, {"room": 10, "unkn1": 57, "unkn2": 10, "x": 312, "y": 88, "name": "Iceberg 4 - 13 Exit 2", "access_rule": []}, {"room": 14, "unkn1": 28, "unkn2": 21, "x": 152, "y": 136, "name": "Iceberg 4 - 13 Exit 3", "access_rule": []}, {"room": 14, "unkn1": 34, "unkn2": 21, "x": 280, "y": 136, "name": "Iceberg 4 - 13 Exit 4", "access_rule": []}], "entity_load": [], "locations": ["Iceberg 4 - Animal 1", "Iceberg 4 - Animal 2", "Iceberg 4 - Animal 3"], "music": 19}, {"name": "Iceberg 4 - 14", "level": 5, "stage": 4, "room": 14, "pointer": 3057002, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Broom Hatter", "Sasuke"], "default_exits": [{"room": 15, "unkn1": 4, "unkn2": 8, "x": 88, "y": 360, "name": "Iceberg 4 - 14 Exit 0", "access_rule": []}, {"room": 13, "unkn1": 10, "unkn2": 8, "x": 440, "y": 344, "name": "Iceberg 4 - 14 Exit 1", "access_rule": []}, {"room": 13, "unkn1": 16, "unkn2": 8, "x": 568, "y": 344, "name": "Iceberg 4 - 14 Exit 2", "access_rule": []}, {"room": 15, "unkn1": 22, "unkn2": 8, "x": 344, "y": 360, "name": "Iceberg 4 - 14 Exit 3", "access_rule": []}], "entity_load": [[11, 16], [30, 16]], "locations": ["Iceberg 4 - Enemy 11 (Broom Hatter)", "Iceberg 4 - Enemy 12 (Sasuke)"], "music": 19}, {"name": "Iceberg 4 - 15", "level": 5, "stage": 4, "room": 15, "pointer": 3116124, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 13, "unkn1": 13, "unkn2": 6, "x": 520, "y": 72, "name": "Iceberg 4 - 15 Exit 0", "access_rule": []}, {"room": 14, "unkn1": 4, "unkn2": 22, "x": 88, "y": 136, "name": "Iceberg 4 - 15 Exit 1", "access_rule": []}, {"room": 14, "unkn1": 22, "unkn2": 22, "x": 344, "y": 136, "name": "Iceberg 4 - 15 Exit 2", "access_rule": []}], "entity_load": [[4, 22]], "locations": [], "music": 19}, {"name": "Iceberg 4 - 16", "level": 5, "stage": 4, "room": 16, "pointer": 3069937, "animal_pointers": [192], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 11, "unkn2": 9, "x": 152, "y": 632, "name": "Iceberg 4 - 16 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Iceberg 4 - Animal 4"], "music": 38}, {"name": "Iceberg 4 - 17", "level": 5, "stage": 4, "room": 17, "pointer": 3072413, "animal_pointers": [192], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 6, "unkn2": 9, "x": 280, "y": 632, "name": "Iceberg 4 - 17 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Iceberg 4 - Animal 5"], "music": 38}, {"name": "Iceberg 4 - 18", "level": 5, "stage": 4, "room": 18, "pointer": 3404593, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Nruff"], "default_exits": [{"room": 19, "unkn1": 94, "unkn2": 12, "x": 72, "y": 152, "name": "Iceberg 4 - 18 Exit 0", "access_rule": []}], "entity_load": [[14, 23], [43, 16], [30, 16], [15, 16]], "locations": ["Iceberg 4 - Enemy 13 (Nruff)", "Iceberg 4 - Star 3"], "music": 19}, {"name": "Iceberg 4 - 19", "level": 5, "stage": 4, "room": 19, "pointer": 3075826, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 20, "unkn1": 14, "unkn2": 9, "x": 72, "y": 120, "name": "Iceberg 4 - 19 Exit 0", "access_rule": []}], "entity_load": [[31, 19], [42, 19]], "locations": ["Iceberg 4 - Name"], "music": 8}, {"name": "Iceberg 4 - 20", "level": 5, "stage": 4, "room": 20, "pointer": 2887943, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Iceberg 4 - Complete"], "music": 5}, {"name": "Iceberg 5 - 0", "level": 5, "stage": 5, "room": 0, "pointer": 3316135, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Bukiset (Burning)", "Bukiset (Stone)", "Bukiset (Ice)", "Bukiset (Needle)", "Bukiset (Clean)", "Bukiset (Parasol)", "Bukiset (Spark)", "Bukiset (Cutter)"], "default_exits": [{"room": 30, "unkn1": 75, "unkn2": 9, "x": 72, "y": 152, "name": "Iceberg 5 - 0 Exit 0", "access_rule": []}], "entity_load": [[79, 16], [76, 16], [81, 16], [78, 16], [77, 16], [82, 16], [80, 16], [83, 16]], "locations": ["Iceberg 5 - Enemy 1 (Bukiset (Burning))", "Iceberg 5 - Enemy 2 (Bukiset (Stone))", "Iceberg 5 - Enemy 3 (Bukiset (Ice))", "Iceberg 5 - Enemy 4 (Bukiset (Needle))", "Iceberg 5 - Enemy 5 (Bukiset (Clean))", "Iceberg 5 - Enemy 6 (Bukiset (Parasol))", "Iceberg 5 - Enemy 7 (Bukiset (Spark))", "Iceberg 5 - Enemy 8 (Bukiset (Cutter))"], "music": 16}, {"name": "Iceberg 5 - 1", "level": 5, "stage": 5, "room": 1, "pointer": 3037607, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 12, "unkn2": 6, "x": 72, "y": 104, "name": "Iceberg 5 - 1 Exit 0", "access_rule": []}], "entity_load": [[1, 16]], "locations": [], "music": 16}, {"name": "Iceberg 5 - 2", "level": 5, "stage": 5, "room": 2, "pointer": 3103842, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Glunk"], "default_exits": [{"room": 25, "unkn1": 27, "unkn2": 6, "x": 72, "y": 200, "name": "Iceberg 5 - 2 Exit 0", "access_rule": []}], "entity_load": [[16, 16]], "locations": ["Iceberg 5 - Enemy 9 (Glunk)"], "music": 16}, {"name": "Iceberg 5 - 3", "level": 5, "stage": 5, "room": 3, "pointer": 3135899, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Wapod"], "default_exits": [{"room": 4, "unkn1": 20, "unkn2": 7, "x": 72, "y": 88, "name": "Iceberg 5 - 3 Exit 0", "access_rule": []}], "entity_load": [[88, 16], [14, 23]], "locations": ["Iceberg 5 - Enemy 10 (Wapod)", "Iceberg 5 - Star 1", "Iceberg 5 - Star 2"], "music": 16}, {"name": "Iceberg 5 - 4", "level": 5, "stage": 5, "room": 4, "pointer": 3180695, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Tick"], "default_exits": [{"room": 5, "unkn1": 26, "unkn2": 5, "x": 56, "y": 104, "name": "Iceberg 5 - 4 Exit 0", "access_rule": []}], "entity_load": [[48, 16], [14, 23]], "locations": ["Iceberg 5 - Enemy 11 (Tick)", "Iceberg 5 - Star 3", "Iceberg 5 - Star 4", "Iceberg 5 - Star 5", "Iceberg 5 - Star 6", "Iceberg 5 - Star 7", "Iceberg 5 - Star 8"], "music": 16}, {"name": "Iceberg 5 - 5", "level": 5, "stage": 5, "room": 5, "pointer": 3106064, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Madoo"], "default_exits": [{"room": 24, "unkn1": 14, "unkn2": 6, "x": 168, "y": 152, "name": "Iceberg 5 - 5 Exit 0", "access_rule": []}], "entity_load": [[58, 16], [14, 23], [4, 22]], "locations": ["Iceberg 5 - Enemy 12 (Madoo)", "Iceberg 5 - Star 9", "Iceberg 5 - Star 10", "Iceberg 5 - Star 11", "Iceberg 5 - Star 12", "Iceberg 5 - Star 13", "Iceberg 5 - Star 14"], "music": 16}, {"name": "Iceberg 5 - 6", "level": 5, "stage": 5, "room": 6, "pointer": 3276800, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 7, "unkn1": 59, "unkn2": 12, "x": 72, "y": 120, "name": "Iceberg 5 - 6 Exit 0", "access_rule": []}], "entity_load": [[55, 16]], "locations": [], "music": 16}, {"name": "Iceberg 5 - 7", "level": 5, "stage": 5, "room": 7, "pointer": 3104585, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 26, "unkn1": 25, "unkn2": 7, "x": 72, "y": 136, "name": "Iceberg 5 - 7 Exit 0", "access_rule": []}], "entity_load": [[23, 16], [7, 16]], "locations": [], "music": 16}, {"name": "Iceberg 5 - 8", "level": 5, "stage": 5, "room": 8, "pointer": 3195121, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 9, "unkn1": 35, "unkn2": 9, "x": 72, "y": 152, "name": "Iceberg 5 - 8 Exit 0", "access_rule": []}], "entity_load": [[4, 16]], "locations": [], "music": 16}, {"name": "Iceberg 5 - 9", "level": 5, "stage": 5, "room": 9, "pointer": 3087198, "animal_pointers": [], "consumables": [{"idx": 16, "pointer": 200, "x": 256, "y": 88, "etype": 22, "vtype": 0, "name": "Iceberg 5 - 1-Up (Boulder)"}], "consumables_pointer": 128, "enemies": [], "default_exits": [{"room": 28, "unkn1": 20, "unkn2": 9, "x": 72, "y": 152, "name": "Iceberg 5 - 9 Exit 0", "access_rule": []}], "entity_load": [[0, 22], [54, 16]], "locations": ["Iceberg 5 - 1-Up (Boulder)"], "music": 16}, {"name": "Iceberg 5 - 10", "level": 5, "stage": 5, "room": 10, "pointer": 3321612, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 32, "unkn1": 45, "unkn2": 15, "x": 72, "y": 120, "name": "Iceberg 5 - 10 Exit 0", "access_rule": []}], "entity_load": [[14, 16]], "locations": [], "music": 16}, {"name": "Iceberg 5 - 11", "level": 5, "stage": 5, "room": 11, "pointer": 3139178, "animal_pointers": [], "consumables": [{"idx": 17, "pointer": 192, "x": 152, "y": 168, "etype": 22, "vtype": 0, "name": "Iceberg 5 - 1-Up (Floor)"}], "consumables_pointer": 176, "enemies": ["Yaban"], "default_exits": [{"room": 12, "unkn1": 17, "unkn2": 7, "x": 72, "y": 120, "name": "Iceberg 5 - 11 Exit 0", "access_rule": []}], "entity_load": [[32, 16], [0, 22]], "locations": ["Iceberg 5 - Enemy 13 (Yaban)", "Iceberg 5 - 1-Up (Floor)"], "music": 16}, {"name": "Iceberg 5 - 12", "level": 5, "stage": 5, "room": 12, "pointer": 3118231, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Propeller"], "default_exits": [{"room": 13, "unkn1": 13, "unkn2": 7, "x": 72, "y": 104, "name": "Iceberg 5 - 12 Exit 0", "access_rule": []}], "entity_load": [[89, 16], [14, 23]], "locations": ["Iceberg 5 - Enemy 14 (Propeller)", "Iceberg 5 - Star 15", "Iceberg 5 - Star 16", "Iceberg 5 - Star 17", "Iceberg 5 - Star 18", "Iceberg 5 - Star 19", "Iceberg 5 - Star 20"], "music": 16}, {"name": "Iceberg 5 - 13", "level": 5, "stage": 5, "room": 13, "pointer": 3021658, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Mariel"], "default_exits": [{"room": 27, "unkn1": 16, "unkn2": 6, "x": 72, "y": 152, "name": "Iceberg 5 - 13 Exit 0", "access_rule": []}], "entity_load": [[45, 16]], "locations": ["Iceberg 5 - Enemy 15 (Mariel)"], "music": 16}, {"name": "Iceberg 5 - 14", "level": 5, "stage": 5, "room": 14, "pointer": 3025398, "animal_pointers": [], "consumables": [{"idx": 24, "pointer": 200, "x": 208, "y": 88, "etype": 22, "vtype": 0, "name": "Iceberg 5 - 1-Up (Peloo)"}], "consumables_pointer": 176, "enemies": [], "default_exits": [{"room": 15, "unkn1": 13, "unkn2": 9, "x": 72, "y": 216, "name": "Iceberg 5 - 14 Exit 0", "access_rule": []}], "entity_load": [[64, 16], [0, 22]], "locations": ["Iceberg 5 - 1-Up (Peloo)"], "music": 16}, {"name": "Iceberg 5 - 15", "level": 5, "stage": 5, "room": 15, "pointer": 3167445, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Pteran"], "default_exits": [{"room": 16, "unkn1": 13, "unkn2": 13, "x": 72, "y": 152, "name": "Iceberg 5 - 15 Exit 0", "access_rule": []}], "entity_load": [[39, 16]], "locations": ["Iceberg 5 - Enemy 16 (Pteran)"], "music": 16}, {"name": "Iceberg 5 - 16", "level": 5, "stage": 5, "room": 16, "pointer": 3033990, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 33, "unkn1": 36, "unkn2": 9, "x": 168, "y": 152, "name": "Iceberg 5 - 16 Exit 0", "access_rule": []}], "entity_load": [[68, 16]], "locations": [], "music": 16}, {"name": "Iceberg 5 - 17", "level": 5, "stage": 5, "room": 17, "pointer": 3100111, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 20, "unkn1": 40, "unkn2": 7, "x": 72, "y": 200, "name": "Iceberg 5 - 17 Exit 0", "access_rule": []}], "entity_load": [[52, 16]], "locations": [], "music": 16}, {"name": "Iceberg 5 - 18", "level": 5, "stage": 5, "room": 18, "pointer": 3030947, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Galbo"], "default_exits": [{"room": 17, "unkn1": 13, "unkn2": 7, "x": 72, "y": 120, "name": "Iceberg 5 - 18 Exit 0", "access_rule": []}], "entity_load": [[26, 16]], "locations": ["Iceberg 5 - Enemy 17 (Galbo)"], "music": 16}, {"name": "Iceberg 5 - 19", "level": 5, "stage": 5, "room": 19, "pointer": 3105326, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["KeKe"], "default_exits": [{"room": 21, "unkn1": 13, "unkn2": 4, "x": 72, "y": 152, "name": "Iceberg 5 - 19 Exit 0", "access_rule": []}], "entity_load": [[51, 16]], "locations": ["Iceberg 5 - Enemy 18 (KeKe)"], "music": 16}, {"name": "Iceberg 5 - 20", "level": 5, "stage": 5, "room": 20, "pointer": 3118928, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Nidoo"], "default_exits": [{"room": 19, "unkn1": 13, "unkn2": 6, "x": 72, "y": 264, "name": "Iceberg 5 - 20 Exit 0", "access_rule": []}], "entity_load": [[28, 16]], "locations": ["Iceberg 5 - Enemy 19 (Nidoo)"], "music": 16}, {"name": "Iceberg 5 - 21", "level": 5, "stage": 5, "room": 21, "pointer": 3202517, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Waddle Dee Drawing", "Bronto Burt Drawing", "Bouncy Drawing"], "default_exits": [{"room": 22, "unkn1": 29, "unkn2": 9, "x": 72, "y": 72, "name": "Iceberg 5 - 21 Exit 0", "access_rule": []}], "entity_load": [[84, 16], [85, 16], [86, 16]], "locations": ["Iceberg 5 - Enemy 20 (Waddle Dee Drawing)", "Iceberg 5 - Enemy 21 (Bronto Burt Drawing)", "Iceberg 5 - Enemy 22 (Bouncy Drawing)"], "music": 16}, {"name": "Iceberg 5 - 22", "level": 5, "stage": 5, "room": 22, "pointer": 3014656, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Joe"], "default_exits": [{"room": 23, "unkn1": 13, "unkn2": 4, "x": 72, "y": 104, "name": "Iceberg 5 - 22 Exit 0", "access_rule": []}], "entity_load": [[91, 16], [14, 23]], "locations": ["Iceberg 5 - Enemy 23 (Joe)", "Iceberg 5 - Star 21", "Iceberg 5 - Star 22", "Iceberg 5 - Star 23", "Iceberg 5 - Star 24", "Iceberg 5 - Star 25"], "music": 16}, {"name": "Iceberg 5 - 23", "level": 5, "stage": 5, "room": 23, "pointer": 3166550, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Kapar"], "default_exits": [{"room": 34, "unkn1": 27, "unkn2": 6, "x": 72, "y": 152, "name": "Iceberg 5 - 23 Exit 0", "access_rule": []}], "entity_load": [[67, 16]], "locations": ["Iceberg 5 - Enemy 24 (Kapar)"], "music": 16}, {"name": "Iceberg 5 - 24", "level": 5, "stage": 5, "room": 24, "pointer": 3029110, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Gansan"], "default_exits": [{"room": 31, "unkn1": 10, "unkn2": 4, "x": 72, "y": 88, "name": "Iceberg 5 - 24 Exit 0", "access_rule": []}], "entity_load": [[75, 16]], "locations": ["Iceberg 5 - Enemy 25 (Gansan)"], "music": 16}, {"name": "Iceberg 5 - 25", "level": 5, "stage": 5, "room": 25, "pointer": 3156420, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Sasuke"], "default_exits": [{"room": 3, "unkn1": 25, "unkn2": 7, "x": 72, "y": 120, "name": "Iceberg 5 - 25 Exit 0", "access_rule": []}], "entity_load": [[30, 16]], "locations": ["Iceberg 5 - Enemy 26 (Sasuke)"], "music": 16}, {"name": "Iceberg 5 - 26", "level": 5, "stage": 5, "room": 26, "pointer": 3127877, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Togezo"], "default_exits": [{"room": 8, "unkn1": 24, "unkn2": 8, "x": 72, "y": 152, "name": "Iceberg 5 - 26 Exit 0", "access_rule": []}], "entity_load": [[18, 16]], "locations": ["Iceberg 5 - Enemy 27 (Togezo)"], "music": 16}, {"name": "Iceberg 5 - 27", "level": 5, "stage": 5, "room": 27, "pointer": 3256471, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Sparky", "Bobin"], "default_exits": [{"room": 14, "unkn1": 26, "unkn2": 9, "x": 72, "y": 152, "name": "Iceberg 5 - 27 Exit 0", "access_rule": []}], "entity_load": [[8, 16], [73, 16]], "locations": ["Iceberg 5 - Enemy 28 (Sparky)", "Iceberg 5 - Enemy 29 (Bobin)"], "music": 16}, {"name": "Iceberg 5 - 28", "level": 5, "stage": 5, "room": 28, "pointer": 3029723, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Chilly"], "default_exits": [{"room": 10, "unkn1": 13, "unkn2": 9, "x": 72, "y": 248, "name": "Iceberg 5 - 28 Exit 0", "access_rule": []}], "entity_load": [[6, 16]], "locations": ["Iceberg 5 - Enemy 30 (Chilly)"], "music": 16}, {"name": "Iceberg 5 - 29", "level": 5, "stage": 5, "room": 29, "pointer": 3526568, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 36, "unkn1": 10, "unkn2": 6, "x": 72, "y": 152, "name": "Iceberg 5 - 29 Exit 0", "access_rule": []}], "entity_load": [[10, 23], [31, 16], [14, 23]], "locations": ["Iceberg 5 - Star 26", "Iceberg 5 - Star 27", "Iceberg 5 - Star 28", "Iceberg 5 - Star 29", "Iceberg 5 - Star 30", "Iceberg 5 - Star 31", "Iceberg 5 - Star 32"], "music": 16}, {"name": "Iceberg 5 - 30", "level": 5, "stage": 5, "room": 30, "pointer": 3022285, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 1, "unkn1": 13, "unkn2": 5, "x": 88, "y": 104, "name": "Iceberg 5 - 30 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Iceberg 5 - Animal 1", "Iceberg 5 - Animal 2"], "music": 40}, {"name": "Iceberg 5 - 31", "level": 5, "stage": 5, "room": 31, "pointer": 3026019, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 13, "unkn2": 9, "x": 72, "y": 200, "name": "Iceberg 5 - 31 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Iceberg 5 - Animal 3", "Iceberg 5 - Animal 4"], "music": 40}, {"name": "Iceberg 5 - 32", "level": 5, "stage": 5, "room": 32, "pointer": 3039996, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 11, "unkn1": 13, "unkn2": 7, "x": 72, "y": 120, "name": "Iceberg 5 - 32 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Iceberg 5 - Animal 5", "Iceberg 5 - Animal 6"], "music": 40}, {"name": "Iceberg 5 - 33", "level": 5, "stage": 5, "room": 33, "pointer": 3015302, "animal_pointers": [192, 200], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 18, "unkn1": 10, "unkn2": 6, "x": 72, "y": 152, "name": "Iceberg 5 - 33 Exit 0", "access_rule": []}], "entity_load": [], "locations": ["Iceberg 5 - Animal 7", "Iceberg 5 - Animal 8"], "music": 40}, {"name": "Iceberg 5 - 34", "level": 5, "stage": 5, "room": 34, "pointer": 3185868, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Peran"], "default_exits": [{"room": 35, "unkn1": 35, "unkn2": 9, "x": 200, "y": 328, "name": "Iceberg 5 - 34 Exit 0", "access_rule": []}], "entity_load": [[72, 16], [4, 22], [14, 23]], "locations": ["Iceberg 5 - Enemy 31 (Peran)", "Iceberg 5 - Star 33", "Iceberg 5 - Star 34"], "music": 16}, {"name": "Iceberg 5 - 35", "level": 5, "stage": 5, "room": 35, "pointer": 3865635, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 29, "unkn1": 12, "unkn2": 7, "x": 168, "y": 1384, "name": "Iceberg 5 - 35 Exit 0", "access_rule": []}], "entity_load": [[17, 16], [4, 22]], "locations": [], "music": 16}, {"name": "Iceberg 5 - 36", "level": 5, "stage": 5, "room": 36, "pointer": 3024154, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 37, "unkn1": 13, "unkn2": 9, "x": 72, "y": 120, "name": "Iceberg 5 - 36 Exit 0", "access_rule": []}], "entity_load": [[32, 19], [42, 19]], "locations": ["Iceberg 5 - Shiro"], "music": 8}, {"name": "Iceberg 5 - 37", "level": 5, "stage": 5, "room": 37, "pointer": 2890594, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Iceberg 5 - Complete"], "music": 5}, {"name": "Iceberg 6 - 0", "level": 5, "stage": 6, "room": 0, "pointer": 3385305, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Nruff"], "default_exits": [{"room": 1, "unkn1": 6, "unkn2": 28, "x": 136, "y": 184, "name": "Iceberg 6 - 0 Exit 0", "access_rule": []}, {"room": 1, "unkn1": 12, "unkn2": 28, "x": 248, "y": 184, "name": "Iceberg 6 - 0 Exit 1", "access_rule": []}, {"room": 1, "unkn1": 18, "unkn2": 28, "x": 360, "y": 184, "name": "Iceberg 6 - 0 Exit 2", "access_rule": []}], "entity_load": [[15, 16]], "locations": ["Iceberg 6 - Enemy 1 (Nruff)"], "music": 12}, {"name": "Iceberg 6 - 1", "level": 5, "stage": 6, "room": 1, "pointer": 3197599, "animal_pointers": [212, 220, 228], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 2, "unkn1": 8, "unkn2": 5, "x": 152, "y": 184, "name": "Iceberg 6 - 1 Exit 0", "access_rule": []}, {"room": 2, "unkn1": 15, "unkn2": 5, "x": 248, "y": 184, "name": "Iceberg 6 - 1 Exit 1", "access_rule": []}, {"room": 2, "unkn1": 22, "unkn2": 5, "x": 344, "y": 184, "name": "Iceberg 6 - 1 Exit 2", "access_rule": []}], "entity_load": [], "locations": ["Iceberg 6 - Animal 1", "Iceberg 6 - Animal 2", "Iceberg 6 - Animal 3"], "music": 12}, {"name": "Iceberg 6 - 2", "level": 5, "stage": 6, "room": 2, "pointer": 3097113, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 5, "unkn1": 9, "unkn2": 5, "x": 136, "y": 184, "name": "Iceberg 6 - 2 Exit 0", "access_rule": []}, {"room": 5, "unkn1": 15, "unkn2": 5, "x": 248, "y": 184, "name": "Iceberg 6 - 2 Exit 1", "access_rule": []}, {"room": 5, "unkn1": 21, "unkn2": 5, "x": 360, "y": 184, "name": "Iceberg 6 - 2 Exit 2", "access_rule": []}], "entity_load": [], "locations": [], "music": 12}, {"name": "Iceberg 6 - 3", "level": 5, "stage": 6, "room": 3, "pointer": 3198422, "animal_pointers": [212, 220, 228], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 6, "unkn1": 8, "unkn2": 5, "x": 152, "y": 184, "name": "Iceberg 6 - 3 Exit 0", "access_rule": []}, {"room": 6, "unkn1": 15, "unkn2": 5, "x": 248, "y": 184, "name": "Iceberg 6 - 3 Exit 1", "access_rule": []}, {"room": 6, "unkn1": 22, "unkn2": 5, "x": 344, "y": 184, "name": "Iceberg 6 - 3 Exit 2", "access_rule": []}], "entity_load": [], "locations": ["Iceberg 6 - Animal 4", "Iceberg 6 - Animal 5", "Iceberg 6 - Animal 6"], "music": 12}, {"name": "Iceberg 6 - 4", "level": 5, "stage": 6, "room": 4, "pointer": 3210507, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 3, "unkn1": 9, "unkn2": 5, "x": 136, "y": 184, "name": "Iceberg 6 - 4 Exit 0", "access_rule": []}, {"room": 3, "unkn1": 15, "unkn2": 5, "x": 248, "y": 184, "name": "Iceberg 6 - 4 Exit 1", "access_rule": []}, {"room": 3, "unkn1": 21, "unkn2": 5, "x": 360, "y": 184, "name": "Iceberg 6 - 4 Exit 2", "access_rule": []}], "entity_load": [], "locations": [], "music": 12}, {"name": "Iceberg 6 - 5", "level": 5, "stage": 6, "room": 5, "pointer": 3196776, "animal_pointers": [], "consumables": [{"idx": 0, "pointer": 212, "x": 136, "y": 120, "etype": 22, "vtype": 2, "name": "Iceberg 6 - Maxim Tomato (Left)"}, {"idx": 1, "pointer": 220, "x": 248, "y": 120, "etype": 22, "vtype": 0, "name": "Iceberg 6 - 1-Up (Middle)"}], "consumables_pointer": 128, "enemies": [], "default_exits": [{"room": 4, "unkn1": 8, "unkn2": 5, "x": 152, "y": 184, "name": "Iceberg 6 - 5 Exit 0", "access_rule": []}, {"room": 4, "unkn1": 15, "unkn2": 5, "x": 248, "y": 184, "name": "Iceberg 6 - 5 Exit 1", "access_rule": []}, {"room": 4, "unkn1": 22, "unkn2": 5, "x": 344, "y": 184, "name": "Iceberg 6 - 5 Exit 2", "access_rule": []}], "entity_load": [[2, 22], [0, 22], [14, 23]], "locations": ["Iceberg 6 - Star 1", "Iceberg 6 - Maxim Tomato (Left)", "Iceberg 6 - 1-Up (Middle)"], "music": 12}, {"name": "Iceberg 6 - 6", "level": 5, "stage": 6, "room": 6, "pointer": 3208130, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Nidoo"], "default_exits": [{"room": 7, "unkn1": 9, "unkn2": 5, "x": 136, "y": 168, "name": "Iceberg 6 - 6 Exit 0", "access_rule": []}], "entity_load": [[28, 16]], "locations": ["Iceberg 6 - Enemy 2 (Nidoo)"], "music": 12}, {"name": "Iceberg 6 - 7", "level": 5, "stage": 6, "room": 7, "pointer": 3124478, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Sparky"], "default_exits": [{"room": 8, "unkn1": 17, "unkn2": 10, "x": 296, "y": 136, "name": "Iceberg 6 - 7 Exit 0", "access_rule": []}], "entity_load": [[8, 16]], "locations": ["Iceberg 6 - Enemy 3 (Sparky)"], "music": 12}, {"name": "Iceberg 6 - 8", "level": 5, "stage": 6, "room": 8, "pointer": 3110431, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 9, "unkn1": 7, "unkn2": 5, "x": 152, "y": 168, "name": "Iceberg 6 - 8 Exit 0", "access_rule": []}, {"room": 14, "unkn1": 14, "unkn2": 5, "x": 296, "y": 136, "name": "Iceberg 6 - 8 Exit 1", "access_rule": []}], "entity_load": [[4, 22], [33, 19]], "locations": [], "music": 12}, {"name": "Iceberg 6 - 9", "level": 5, "stage": 6, "room": 9, "pointer": 3139832, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 10, "unkn1": 16, "unkn2": 10, "x": 296, "y": 136, "name": "Iceberg 6 - 9 Exit 0", "access_rule": []}], "entity_load": [[2, 27]], "locations": ["Iceberg 6 - Miniboss 1 (Blocky)"], "music": 12}, {"name": "Iceberg 6 - 10", "level": 5, "stage": 6, "room": 10, "pointer": 3119624, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 11, "unkn1": 7, "unkn2": 5, "x": 152, "y": 168, "name": "Iceberg 6 - 10 Exit 0", "access_rule": []}], "entity_load": [[33, 19], [4, 22]], "locations": [], "music": 12}, {"name": "Iceberg 6 - 11", "level": 5, "stage": 6, "room": 11, "pointer": 3141139, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 12, "unkn1": 16, "unkn2": 10, "x": 296, "y": 136, "name": "Iceberg 6 - 11 Exit 0", "access_rule": []}], "entity_load": [[3, 27]], "locations": ["Iceberg 6 - Miniboss 2 (Jumper Shoot)"], "music": 12}, {"name": "Iceberg 6 - 12", "level": 5, "stage": 6, "room": 12, "pointer": 3123788, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 13, "unkn1": 7, "unkn2": 5, "x": 136, "y": 168, "name": "Iceberg 6 - 12 Exit 0", "access_rule": []}], "entity_load": [[33, 19], [4, 22]], "locations": [], "music": 12}, {"name": "Iceberg 6 - 13", "level": 5, "stage": 6, "room": 13, "pointer": 3143741, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 14, "unkn1": 15, "unkn2": 10, "x": 296, "y": 136, "name": "Iceberg 6 - 13 Exit 0", "access_rule": []}], "entity_load": [[1, 27]], "locations": ["Iceberg 6 - Miniboss 3 (Yuki)"], "music": 12}, {"name": "Iceberg 6 - 14", "level": 5, "stage": 6, "room": 14, "pointer": 3120319, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 15, "unkn1": 7, "unkn2": 5, "x": 136, "y": 168, "name": "Iceberg 6 - 14 Exit 0", "access_rule": []}], "entity_load": [[33, 19], [4, 22]], "locations": [], "music": 12}, {"name": "Iceberg 6 - 15", "level": 5, "stage": 6, "room": 15, "pointer": 3135238, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": ["Sir Kibble"], "default_exits": [{"room": 16, "unkn1": 15, "unkn2": 10, "x": 296, "y": 136, "name": "Iceberg 6 - 15 Exit 0", "access_rule": []}], "entity_load": [[27, 16]], "locations": ["Iceberg 6 - Enemy 4 (Sir Kibble)"], "music": 12}, {"name": "Iceberg 6 - 16", "level": 5, "stage": 6, "room": 16, "pointer": 3123096, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 17, "unkn1": 7, "unkn2": 5, "x": 136, "y": 168, "name": "Iceberg 6 - 16 Exit 0", "access_rule": []}], "entity_load": [[4, 22], [33, 19]], "locations": [], "music": 12}, {"name": "Iceberg 6 - 17", "level": 5, "stage": 6, "room": 17, "pointer": 3144389, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 18, "unkn1": 15, "unkn2": 10, "x": 296, "y": 136, "name": "Iceberg 6 - 17 Exit 0", "access_rule": []}], "entity_load": [[5, 27]], "locations": ["Iceberg 6 - Miniboss 4 (Haboki)"], "music": 12}, {"name": "Iceberg 6 - 18", "level": 5, "stage": 6, "room": 18, "pointer": 3121014, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 19, "unkn1": 7, "unkn2": 5, "x": 136, "y": 168, "name": "Iceberg 6 - 18 Exit 0", "access_rule": []}], "entity_load": [[33, 19], [4, 22]], "locations": [], "music": 12}, {"name": "Iceberg 6 - 19", "level": 5, "stage": 6, "room": 19, "pointer": 3017228, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 20, "unkn1": 15, "unkn2": 10, "x": 296, "y": 136, "name": "Iceberg 6 - 19 Exit 0", "access_rule": []}], "entity_load": [[4, 27]], "locations": ["Iceberg 6 - Miniboss 5 (Boboo)"], "music": 12}, {"name": "Iceberg 6 - 20", "level": 5, "stage": 6, "room": 20, "pointer": 3121709, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 21, "unkn1": 7, "unkn2": 5, "x": 136, "y": 168, "name": "Iceberg 6 - 20 Exit 0", "access_rule": []}], "entity_load": [[33, 19], [4, 22]], "locations": [], "music": 12}, {"name": "Iceberg 6 - 21", "level": 5, "stage": 6, "room": 21, "pointer": 3145036, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 22, "unkn1": 15, "unkn2": 10, "x": 296, "y": 136, "name": "Iceberg 6 - 21 Exit 0", "access_rule": []}], "entity_load": [[0, 27]], "locations": ["Iceberg 6 - Miniboss 6 (Captain Stitch)"], "music": 12}, {"name": "Iceberg 6 - 22", "level": 5, "stage": 6, "room": 22, "pointer": 3116830, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 23, "unkn1": 7, "unkn2": 5, "x": 136, "y": 152, "name": "Iceberg 6 - 22 Exit 0", "access_rule": []}], "entity_load": [[33, 19], [4, 22]], "locations": [], "music": 12}, {"name": "Iceberg 6 - 23", "level": 5, "stage": 6, "room": 23, "pointer": 3045263, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [{"room": 24, "unkn1": 17, "unkn2": 9, "x": 72, "y": 120, "name": "Iceberg 6 - 23 Exit 0", "access_rule": []}], "entity_load": [[33, 19], [42, 19]], "locations": ["Iceberg 6 - Angel"], "music": 8}, {"name": "Iceberg 6 - 24", "level": 5, "stage": 6, "room": 24, "pointer": 2889389, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[38, 19]], "locations": ["Iceberg 6 - Complete"], "music": 5}, {"name": "Iceberg Boss - 0", "level": 5, "stage": 7, "room": 0, "pointer": 2980207, "animal_pointers": [], "consumables": [], "consumables_pointer": 0, "enemies": [], "default_exits": [], "entity_load": [[8, 18]], "locations": ["Iceberg - Boss (Dedede) Purified", "Level 5 Boss - Defeated", "Level 5 Boss - Purified"], "music": 7}]
\ No newline at end of file
diff --git a/worlds/kdl3/data/kdl3_basepatch.bsdiff4 b/worlds/kdl3/data/kdl3_basepatch.bsdiff4
new file mode 100644
index 000000000000..cd002121cd38
Binary files /dev/null and b/worlds/kdl3/data/kdl3_basepatch.bsdiff4 differ
diff --git a/worlds/kdl3/docs/en_Kirby's Dream Land 3.md b/worlds/kdl3/docs/en_Kirby's Dream Land 3.md
new file mode 100644
index 000000000000..008ee0fcc1ee
--- /dev/null
+++ b/worlds/kdl3/docs/en_Kirby's Dream Land 3.md
@@ -0,0 +1,39 @@
+# Kirby's Dream Land 3
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
+config file.
+
+## What does randomization do to this game?
+Kirby will be unable to absorb Copy Abilities and meet up with his animal friends until they are sent to him. Items such
+as Heart Stars, 1-Ups, and Invincibility Candy will be shuffled into the pool for Kirby to receive.
+
+## What is considered a location check in Kirby's Dream Land 3?
+- Completing a stage for the first time
+- Completing the given task of a stage and receiving a Heart Star
+- Purifying a boss after acquiring a certain number of Heart Stars
+ (indicated by their portrait flashing in the level select)
+- If enabled, 1-Ups and Maxim Tomatoes
+- If enabled, every single Star Piece within a stage
+
+## When the player receives an item, what happens?
+A sound effect will play, and Kirby will immediately receive the effects of that item, such as being able to receive Copy Abilities from enemies that
+give said Copy Ability. Animal Friends will require leaving the room you are currently in before they will appear.
+
+## What is the goal of Kirby's Dream Land 3?
+Under the Zero goal, players must collect enough Heart Stars to purify the five bosses and gain access to the Hyper Zone,
+where Zero can be found and defeated.
+
+Under the Boss Butch goal, players must collect enough Heart Stars to purify the five bosses
+and then complete the Boss Butch game mode accessible from the main menu.
+
+Under the MG5 goal, players must collect enough Heart Stars to purify the five bosses
+and then perfect the Super MG5 game mode accessible from the main menu.
+
+Under the Jumping goal, players must collect enough Heart Stars to purify the five bosses
+and then reach a target score in the Jumping game mode accessible from the main menu.
+
+## Why is EmuHawk resizing itself while I'm playing?
+Kirby's Dream Land 3 changes the SNES's display resolution from 1x to 2x many times during gameplay (particularly in rooms with foreground effects).
+To counter-act this resizing, set SNES -> Options -> "Always use double-size frame buffer".
diff --git a/worlds/kdl3/docs/setup_en.md b/worlds/kdl3/docs/setup_en.md
new file mode 100644
index 000000000000..a73d248d4d18
--- /dev/null
+++ b/worlds/kdl3/docs/setup_en.md
@@ -0,0 +1,148 @@
+# Kirby's Dream Land 3 Randomizer Setup Guide
+
+## Required Software
+
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
+- Hardware or software capable of loading and playing SNES ROM files
+ - An emulator capable of connecting to SNI with ROM access. Any one of the following will work:
+ - snes9x-emunwa from: [snes9x-emunwa Releases Page](https://github.com/Skarsnik/snes9x-emunwa/releases)
+ - snes9x-rr from: [snes9x-rr Releases Page](https://github.com/gocha/snes9x-rr/releases)
+ - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html)
+ - bsnes-plus-nwa from: [bsnes-plus GitHub](https://github.com/black-sliver/bsnes-plus)
+ - **RetroArch is currently incompatible with Kirby's Dream Land 3**
+ - Or SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other
+ compatible hardware.
+- Your KDL3 ROM file, probably named either `Kirby's Dream Land 3 (USA).sfc` or `Hoshi no Kirby 3 (J).sfc`
+
+## Installation Procedures
+
+1. Download and install Archipelago from the link above, making sure to install the most recent version.
+ **The installer file is located in the assets section at the bottom of the version information**.
+ - During generation/patching, you will be asked to locate your base ROM file. This is your Kirby's Dream Land 3 ROM file.
+
+2. If you are using an emulator, you should assign your SNI-compatible emulator as your default program for launching ROM
+ files.
+ 1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
+ 2. Right-click on a ROM file and select **Open with...**
+ 3. Check the box next to **Always use this app to open .sfc files**
+ 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
+ 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you
+ extracted in step one.
+
+## Create a Config (.yaml) File
+
+### What is a config file and why do I need one?
+
+Your config file contains a set of configuration options which provide the generator with information about how it
+should generate your game. Each player of a multiworld will provide their own config file. This setup allows each player
+to enjoy an experience customized for their taste, and different players in the same multiworld can all have different
+options.
+
+See the guide on setting up a basic YAML at the Archipelago setup
+guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
+
+### Where do I get a config file?
+
+The [Player Options](/games/Kirby's%20Dream%20Land%203/player-options) page on the website allows you to configure
+your personal options 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.
+
+## Generating a Single-Player Game
+
+1. Navigate to the [Player Options](/games/Kirby's%20Dream%20Land%203/player-options) page, configure your options,
+ and click the "Generate Game" button.
+2. You will be presented with a "Seed Info" page.
+3. Click the "Create New Room" link.
+4. You will be presented with a server page, from which you can download your patch file.
+5. Double-click on your patch file, and SNIClient will launch automatically, create your ROM from the patch file, and
+ open your emulator for you.
+
+## Joining a MultiWorld Game
+
+### Obtain your patch file and create your ROM
+
+When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
+the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
+files. Your patch file should have a `.apkdl3` extension.
+
+Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the
+client, and will also create your ROM in the same place as your patch file.
+
+### Connect to the client
+
+#### With an emulator
+
+When the client launched automatically, SNI should have also automatically launched in the background. If this is its
+first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
+
+##### snes9x-rr
+
+1. Load your ROM file if it hasn't already been loaded.
+2. Click on the File menu and hover on **Lua Scripting**
+3. Click on **New Lua Script Window...**
+4. In the new window, click **Browse...**
+5. Select the connector lua file included with your client
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`
+6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
+the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
+
+##### BizHawk
+
+1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these
+ menu options:
+ `Config --> Cores --> SNES --> BSNES`
+ Once you have changed the loaded core, you must restart BizHawk.
+2. Load your ROM file if it hasn't already been loaded.
+3. Click on the Tools menu and click on **Lua Console**
+4. Click Script -> Open Script...
+5. Select the `Connector.lua` file you downloaded above
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`
+
+##### bsnes-plus-nwa and snes9x-nwa
+
+These should automatically connect to SNI. If this is the first time launching, you may be prompted to allow it to
+communicate through the Windows Firewall.
+
+#### With hardware
+
+This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do
+this now. SD2SNES and FXPak Pro users may download the appropriate firmware
+[here](https://github.com/RedGuyyyy/sd2snes/releases). Other hardware may find helpful information
+[on this page](http://usb2snes.com/#supported-platforms).
+
+1. Close your emulator, which may have auto-launched.
+2. Power on your device and load the ROM.
+
+### Connect to the Archipelago Server
+
+The patch file which launched your client should have automatically connected you to the AP Server. There are a few
+reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the
+client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it
+into the "Server" input field then press enter.
+
+The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
+
+### Play the game
+
+When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on
+successfully joining a multiworld game! You can execute various commands in your client. For more information regarding
+these commands you can use `/help` for local client commands and `!help` for server commands.
+
+## Hosting a MultiWorld game
+
+The recommended way to host a game is to use our [hosting service](/generate). The process is relatively simple:
+
+1. Collect config files from your players.
+2. Create a zip file containing your players' config files.
+3. Upload that zip file to the website linked above.
+4. Wait a moment while the seed is generated.
+5. When the seed is generated, you will be redirected to a "Seed Info" page.
+6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so
+ they may download their patch files from there.
+7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all
+ players in the game. Any observers may also be given the link to this page.
+8. Once all players have joined, you may begin playing.
diff --git a/worlds/kdl3/src/kdl3_basepatch.asm b/worlds/kdl3/src/kdl3_basepatch.asm
new file mode 100644
index 000000000000..e419d0632f0e
--- /dev/null
+++ b/worlds/kdl3/src/kdl3_basepatch.asm
@@ -0,0 +1,1237 @@
+fullsa1rom
+
+!GAME_STATUS = $36D0
+
+; SNES hardware registers
+!VMADDL = $002116
+!VMDATAL = $002118
+!MDMAEN = $00420B
+!DMAP0 = $004300
+!BBAD0 = $004301
+!A1T0L = $004302
+!A1B0 = $004304
+!DAS0L = $004305
+
+org $008033
+ JSL WriteBWRAM
+ NOP #5
+
+org $00A245
+HSVPatch:
+ BRA .Jump
+ PHX
+ LDA $6CA0,X
+ TAX
+ LDA $6E22,X
+ JSR $A25B
+ PLX
+ INX
+ .Jump:
+ JSL HeartStarVisual
+
+
+org $00A3FC
+ JSL NintenHalken
+ NOP
+
+org $00EAE4
+ JSL MainLoopHook
+ NOP
+
+org $00F85E
+ JSL SpeedTrap
+ NOP #2
+
+org $00FFC0
+ db "KDL3_BASEPATCH_ARCHI"
+
+org $00FFD8
+ db $06
+
+org $018405
+ JSL PauseMenu
+
+org $01AFC8
+ JSL HeartStarCheck
+ NOP #13
+
+org $01B013
+ SEC ; Remove Dedede Bad Ending
+
+org $02B7B0 ; Zero unlock
+ LDA $80A0
+ CMP #$0001
+
+org $02C238
+ LDA #$0006
+ JSL OpenWorldUnlock
+ NOP #5
+
+org $02C27C
+ JSL HeartStarSelectFix
+ NOP #2
+
+org $02C317
+ JSL LoadFont
+
+org $02C39D
+ JSL StageCompleteSet
+ NOP #2
+
+org $02C3D9
+ JSL StrictBosses
+ NOP #2
+
+org $02C3F0
+ JSL OpenWorldBossUnlock
+ NOP #6
+
+org $02C463
+ JSL NormalGoalSet
+ NOP #2
+
+org $049CD7
+ JSL AnimalFriendSpawn
+
+org $06801E
+ JSL ConsumableSet
+
+org $068518
+ JSL CopyAbilityOverride
+ NOP #2
+
+org $099F35
+ JSL HeartStarCutsceneFix
+
+org $09A01F
+ JSL HeartStarGraphicFix
+ NOP #2
+ db $B0
+
+org $09A0AE
+ JSL HeartStarGraphicFix
+ NOP #2
+ db $90
+
+org $0A87E8
+ JSL CopyAbilityAnimalOverride
+ NOP #2
+
+org $12B238
+ JSL FinalIcebergFix
+ NOP #10
+ db $B0
+
+org $14A3EB
+ LDA $07A2, Y
+ JSL StarsSet
+ NOP #3
+
+org $15BC13
+ JML GiftGiving
+ NOP
+
+org $0799A0
+CopyAbilityOverride:
+ LDA $54F3, Y
+ PHA
+ ASL
+ TAX
+ PLA
+ CMP $8020, X
+ NOP #2
+ BEQ .StoreAbilityK
+ LDA #$0000
+ .StoreAbilityK:
+ STA $54A9, Y
+ RTL
+ NOP #4
+CopyAbilityAnimalOverride:
+ PHA
+ ASL
+ TAY
+ PLA
+ CMP $8020, Y
+ NOP
+ BEQ .StoreAbilityA
+ LDA #$0000
+ .StoreAbilityA:
+ STA $54A9, X
+ STA $39DF, X
+ RTL
+
+org $079A00
+HeartStarCheck:
+ TXA
+ CMP #$0000 ; is this level 1
+ BEQ .PassToX
+ LSR
+ LSR
+ INC
+ .PassToX:
+ TAX
+ LDA $8070 ; heart stars
+ CLC
+ CMP $07D00A ;Compare to goal heart stars
+ BCC .CompareWorldHS ; we don't have enough
+ PHX
+ LDA #$0014
+ STA $7F62 ; play sound fx 0x14
+ LDA $07D012 ; goal
+ CMP #$0000 ; are we on zero goal?
+ BEQ .ZeroGoal ; we are
+ LDA #$0001
+ LDX $3617 ; current save
+ STA $53DD, X ; boss butch
+ STA $53DF, X ; MG5
+ STA $53E1, X ; Jumping
+ BRA .PullX
+ .ZeroGoal:
+ LDA #$0001
+ STA $80A0 ; zero unlock address
+ .PullX:
+ PLX
+ .CompareWorldHS:
+ LDA $8070 ; current heart stars
+ CMP $07D000, X ; compare to world heart stars
+ BCS .ReturnTrue
+ CLC
+ RTL
+ .ReturnTrue:
+ SEC
+ RTL
+
+org $079A80
+OpenWorldUnlock:
+ PHX
+ LDX $900E ; Are we on open world?
+ BNE .Open ; Branch if we are
+ LDA #$0001
+ .Open:
+ STA $5AC1 ;(cutscene)
+ STA $53CD ;(unlocked stages)
+ INC
+ STA $5AB9 ;(currently selectable stages)
+ CPX #$0001
+ BNE .Return ; Return if we aren't on open world
+ LDA #$0001
+ STA $5A9D
+ STA $5A9F
+ STA $5AA1
+ STA $5AA3
+ STA $5AA5
+ .Return:
+ PLX
+ RTL
+
+org $079B00
+MainLoopHook:
+ STA $D4
+ INC $3524
+ JSL ParseItemQueue
+ LDA $7F62 ; sfx to be played
+ BEQ .Traps ; skip if 0
+ JSL $00D927 ; play sfx
+ STZ $7F62
+ .Traps:
+ LDA $36D0
+ CMP #$FFFF ; are we in menus?
+ BEQ .Return ; return if we are
+ LDA $5541 ; gooey status
+ BPL .Slowness ; gooey is already spawned
+ LDA $8080
+ CMP #$0000 ; did we get a gooey trap
+ BEQ .Slowness ; branch if we did not
+ JSL GooeySpawn
+ STZ $8080
+ .Slowness:
+ LDA $8082 ; slowness
+ BEQ .Eject ; are we under the effects of a slowness trap
+ DEC
+ STA $8082 ; dec by 1 each frame
+ .Eject:
+ PHX
+ PHY
+ LDA $54A9 ; copy ability
+ BEQ .PullVars ; branch if we do not have a copy ability
+ LDA $8084 ; eject ability
+ BEQ .PullVars ; branch if we haven't received eject
+ LDA #$2000 ; select button press
+ STA $60C1 ; write to controller mirror
+ STZ $8084
+ .PullVars:
+ PLY
+ PLX
+ .Return:
+ RTL
+
+org $079B80
+HeartStarGraphicFix:
+ LDA #$0000
+ PHX
+ PHY
+ LDX $363F ; current level
+ LDY $3641 ; current stage
+ .LoopLevel:
+ CPX #$0000
+ BEQ .LoopStage
+ INC #6
+ DEX
+ BRA .LoopLevel ; return to loop head
+ .LoopStage:
+ CPY #$0000
+ BEQ .EndLoop
+ INC
+ DEY
+ BRA .LoopStage ; return to loop head
+ .EndLoop
+ ASL
+ TAX
+ LDA $07D080, X ; table of original stage number
+ CMP #$0003 ; is the current stage a minigame stage?
+ BEQ .ReturnTrue ; branch if so
+ CLC
+ BRA .Return
+ .ReturnTrue:
+ SEC
+ .Return:
+ PLY
+ PLX
+ RTL
+
+org $079BF0
+ParseItemQueue:
+; Local item queue parsing
+ NOP
+ LDX #$0000
+ .LoopHead:
+ LDA $C000,X
+ BIT #$0010
+ BNE .Ability
+ BIT #$0020
+ BNE .Animal
+ BIT #$0040
+ BNE .Positive
+ BIT #$0080
+ BNE .Negative
+ .LoopCheck:
+ INX
+ INX
+ CPX #$000F
+ BCC .LoopHead
+ RTL
+ .Ability:
+ JSL .ApplyAbility
+ RTL
+ .Animal:
+ JSL .ApplyAnimal
+ RTL
+ .Positive:
+ LDY $36D0
+ CPY #$FFFF
+ BEQ .LoopCheck
+ JSL .ApplyPositive
+ RTL
+ .Negative:
+ AND #$000F
+ ASL
+ TAY
+ LDA $8080,Y
+ BNE .LoopCheck
+ JSL .ApplyNegative
+ RTL
+ .ApplyAbility:
+ AND #$000F
+ PHA
+ ASL
+ TAY
+ PLA
+ STA $8020,Y
+ LDA #$0032
+ BRA .PlaySFX
+ .ApplyAnimal:
+ AND #$000F
+ PHA
+ ASL
+ TAY
+ PLA
+ INC
+ STA $8000,Y
+ LDA #$0032
+ BRA .PlaySFX
+ .PlaySFX:
+ STA $7F62
+ STZ $C000,X
+ .Return:
+ RTL
+ .ApplyPositive:
+ LDY $36D0
+ CPY #$FFFF
+ BEQ .Return
+ AND #$000F
+ BEQ .HeartStar
+ CMP #$0004
+ BCS .StarBit
+ CMP #$0002
+ BCS .Not1UP
+ LDA $39CF
+ INC
+ STA $39CF
+ STA $39E3
+ LDA #$0033
+ BRA .PlaySFX
+ .Not1UP:
+ CMP #$0003
+ BEQ .Invincibility
+ LDA $39D3
+ BEQ .JustKirby
+ LDA #$0008
+ STA $39D1
+ STA $39D3
+ BRA .PlayPositive
+ .JustKirby:
+ LDA #$000A
+ STA $39D1
+ BRA .PlayPositive
+ .Invincibility:
+ LDA #$0384
+ STA $54B1
+ BRA .PlayPositive
+ .HeartStar:
+ INC $8070
+ LDA #$0016
+ BRA .PlaySFX
+ .StarBit:
+ SEC
+ SBC #$0004
+ ASL
+ INC
+ CLC
+ ADC $39D7
+ ORA #$8000
+ STA $39D7
+ .PlayPositive:
+ LDA #$0026
+ .PlaySFXLong
+ BRA .PlaySFX
+ .ApplyNegative:
+ CPY #$0005
+ BCS .PlayNone
+ LDA $8080,Y
+ BNE .Return
+ LDA #$0384
+ STA $8080,Y
+ LDA #$00A7
+ BRA .PlaySFXLong
+ .PlayNone:
+ LDA #$0000
+ BRA .PlaySFXLong
+
+org $079D00
+AnimalFriendSpawn:
+ PHA
+ CPX #$0002 ; is this an animal friend?
+ BNE .Return
+ XBA
+ PHA
+ ASL
+ TAY
+ PLA
+ INC
+ CMP $8000, Y ; do we have this animal friend
+ BEQ .Return ; we have this animal friend
+ INX
+ .Return:
+ PLY
+ LDA #$9999
+ RTL
+
+org $079E00
+WriteBWRAM:
+ LDY #$6001 ;starting addr
+ LDA #$1FFE ;bytes to write
+ MVN $40, $40 ;copy $406000 from 406001 to 407FFE
+ LDX #$0000
+ LDY #$0014
+ .LoopHead:
+ LDA $8100, X ; rom header
+ CMP $07C000, X ; compare to real rom name
+ BNE .InitializeRAM ; area is uninitialized or corrupt, reset
+ INX
+ DEY
+ BMI .Return ; if Y is negative, rom header matches, valid bwram
+ BRA .LoopHead ; else continue loop
+ .InitializeRAM:
+ LDA #$0000
+ STA $8000 ; initialize first byte that gets copied
+ LDX #$8000
+ LDY #$8001
+ LDA #$7FFD
+ MVN $40, $40 ; initialize 0x8000 onward
+ LDX #$D000 ; seed info 0x3D000
+ LDY #$9000 ; target location
+ LDA #$1000
+ MVN $40, $07
+ LDX #$C000 ; ROM name
+ LDY #$8100 ; target
+ LDA #$0015
+ MVN $40, $07
+ .Return:
+ RTL
+
+org $079E80
+ConsumableSet:
+ PHA
+ PHX
+ PHY
+ AND #$00FF
+ PHA
+ LDX $53CF
+ LDY $53D3
+ LDA #$0000
+ DEY
+ .LoopLevel:
+ CPX #$0000
+ BEQ .LoopStage
+ CLC
+ ADC #$0007
+ DEX
+ BRA .LoopLevel ; return to loop head
+ .LoopStage:
+ CPY #$0000
+ BEQ .EndLoop
+ INC
+ DEY
+ BRA .LoopStage ; return to loop head
+ .EndLoop:
+ ASL
+ TAX
+ LDA $07D020, X ; current stage
+ DEC
+ ASL #6
+ TAX
+ PLA
+ .LoopHead:
+ CMP #$0000
+ BEQ .ApplyCheck
+ INX
+ DEC
+ BRA .LoopHead ; return to loop head
+ .ApplyCheck:
+ LDA $A000, X ; consumables index
+ ORA #$0001
+ STA $A000, X
+ PLY
+ PLX
+ PLA
+ XBA
+ AND #$00FF
+ RTL
+
+org $079F00
+NormalGoalSet:
+ PHX
+ LDA $07D012
+ CMP #$0000
+ BEQ .ZeroGoal
+ LDA #$0001
+ LDX $3617 ; current save
+ STA $53DD, X ; Boss butch
+ STA $53DF, X ; MG5
+ STA $53D1, X ; Jumping
+ BRA .Return
+ .ZeroGoal:
+ LDA #$0001
+ STA $80A0
+ .Return:
+ PLX
+ LDA #$0006
+ STA $5AC1 ; cutscene
+ RTL
+
+org $079F80
+FinalIcebergFix:
+ PHX
+ PHY
+ LDA #$0000
+ LDX $363F
+ LDY $3641
+ .LoopLevel:
+ CPX #$0000
+ BEQ .LoopStage
+ INC #7
+ DEX
+ BRA .LoopLevel ; return to loop head
+ .LoopStage:
+ CPY #$0000
+ BEQ .CheckStage
+ INC
+ DEY
+ BRA .LoopStage ; return to loop head
+ .CheckStage:
+ ASL
+ TAX
+ LDA $07D020, X
+ CMP #$001E
+ BEQ .ReturnTrue
+ CLC
+ BRA .Return
+ .ReturnTrue:
+ SEC
+ .Return:
+ PLY
+ PLX
+ RTL
+
+org $07A000
+StrictBosses:
+ PHX
+ LDA $901E ; Do we have strict bosses enabled?
+ BEQ .ReturnTrue ; Return True if we don't, always unlock the boss in this case
+ LDA $53CB ; unlocked level
+ CMP #$0005 ; have we unlocked level 5?
+ BCS .ReturnFalse ; we don't need to do anything if so
+ NOP #5 ;unsure when these got here
+ LDX $53CB
+ DEX
+ TXA
+ ASL
+ TAX
+ LDA $8070 ; current heart stars
+ CMP $07D000, X ; do we have enough HS to purify?
+ BCS .ReturnTrue ; branch if we do
+ .ReturnFalse:
+ SEC
+ BRA .Return
+ .ReturnTrue:
+ CLC
+ .Return:
+ PLX
+ LDA $53CD
+ RTL
+
+org $07A030
+NintenHalken:
+ LDX #$0005
+ .Halken:
+ LDA $00A405, X ; loop head (halken)
+ STA $4080F0, X
+ DEX
+ BPL .Halken ; branch if more letters to copy
+ LDX #$0005
+ .Ninten:
+ LDA $00A40B, X ; loop head (ninten)
+ STA $408FF0, X
+ DEX
+ BPL .Ninten ; branch if more letters to copy
+ REP #$20
+ LDA #$0001
+ RTL
+
+org $07A080
+StageCompleteSet:
+ PHX
+ LDA $5AC1 ; completed stage cutscene
+ BEQ .Return ; we have not completed a stage
+ LDA #$0000
+ LDX $53CF ; current level
+ .LoopLevel:
+ CPX #$0000
+ BEQ .StageStart
+ DEX
+ INC #7
+ BRA .LoopLevel ; return to loop head
+ .StageStart:
+ LDX $53D3 ; current stage
+ CPX #$0007 ; is this a boss stage
+ BEQ .Return ; return if so
+ DEX
+ .LoopStage:
+ CPX #$0000
+ BEQ .LoopEnd
+ INC
+ DEX
+ BRA .LoopStage ; return to loop head
+ .LoopEnd:
+ ASL
+ TAX
+ LDA $9020, X ; load the stage we completed
+ DEC
+ ASL
+ TAX
+ LDA #$0001
+ ORA $8200, X
+ STA $8200, X
+ .Return:
+ PLX
+ LDA $53CF
+ CMP $53CB
+ RTL
+
+org $07A100
+OpenWorldBossUnlock:
+ PHX
+ PHY
+ LDA $900E ; Are we on open world?
+ BEQ .ReturnTrue ; Return true if we aren't, always unlock boss
+ LDA $53CD
+ CMP #$0006
+ BNE .ReturnFalse ; return if we aren't on stage 6
+ LDA $53CF
+ INC
+ CMP $53CB ; are we on the most unlocked level?
+ BNE .ReturnFalse ; return if we aren't
+ LDA #$0000
+ LDX $53CF
+ .LoopLevel:
+ CPX #$0000
+ BEQ .LoopStages
+ ADC #$0006
+ DEX
+ BRA .LoopLevel ; return to loop head
+ .LoopStages:
+ ASL
+ TAX
+ LDA #$0000
+ LDY #$0006
+ PHY
+ PHX
+ .LoopStage:
+ PLX
+ LDY $9020, X ; get stage id
+ DEY
+ INX
+ INX
+ PHA
+ TYA
+ ASL
+ TAY
+ PLA
+ ADC $8200, Y ; add current stage value to total
+ PLY
+ DEY
+ PHY
+ PHX
+ CPY #$0000
+ BNE .LoopStage ; return to loop head
+ PLX
+ PLY
+ SEC
+ SBC $9016
+ BCC .ReturnFalse
+ .ReturnTrue
+ LDA $53CD
+ INC
+ STA $53CD
+ STA $5AC1
+ BRA .Return
+ .ReturnFalse:
+ STZ $5AC1
+ .Return:
+ PLY
+ PLX
+ RTL
+
+org $07A180
+GooeySpawn:
+ PHY
+ PHX
+ LDX #$0000
+ LDY #$0000
+ STA $5543
+ LDA $1922,Y
+ STA $C0
+ LDA $19A2,Y
+ STA $C2
+ LDA #$0008
+ STA $C4
+ LDA #$0002
+ STA $352A
+ LDA #$0003
+ JSL $00F54F
+ STX $5541
+ LDA #$FFFF
+ STA $0622,X
+ JSL $00BAEF
+ JSL $C4883C
+ LDX $39D1
+ CPX #$0001
+ BEQ .Return
+ LDA #$FFFF
+ CPX #$0002
+ BEQ .Call
+ DEC
+ .Call:
+ JSL $C43C22
+ .Return:
+ PLX
+ PLY
+ RTL
+
+org $07A200
+SpeedTrap:
+ PHX
+ LDX $8082 ; do we have slowness
+ BEQ .Apply ; branch if we do not
+ LSR
+ .Apply:
+ PLX
+ STA $1F22, Y ; player max speed
+ EOR #$FFFF
+ RTL
+
+org $07A280
+HeartStarVisual:
+ CPX #$0000
+ BEQ .SkipInx
+ INX
+ .SkipInx
+ CPX $651E
+ BCC .Return
+ CPX #$0000
+ BEQ .Return
+ LDA $4036D0
+ AND #$00FF
+ BEQ .ReturnTrue
+ LDA $3000
+ AND #$0200
+ CMP #$0000
+ BNE .ReturnTrue
+ PHY
+ LDA $3000
+ TAY
+ CLC
+ ADC #$0020
+ STA $3000
+ LDA $408070
+ LDX #$0000
+ .LoopHead:
+ CMP #$000A
+ BCC .LoopEnd
+ SEC
+ SBC #$000A
+ INX
+ BRA .LoopHead
+ .LoopEnd:
+ PHX
+ TAX
+ PLA
+ ORA #$2500
+ PHA
+ LDA #$2C70
+ STA $0000, Y
+ PLA
+ INY
+ INY
+ STA $0000, Y
+ INY
+ INY
+ TXA
+ ORA #$2500
+ PHA
+ LDA #$2C78
+ STA $0000, Y
+ INY
+ INY
+ PLA
+ STA $0000, Y
+ INY
+ INY
+ JSL HeartStarVisual2 ; we ran out of room
+ PLY
+ .ReturnTrue:
+ SEC
+ .Return:
+ RTL
+
+org $07A300
+LoadFont:
+ JSL $00D29F ; play sfx
+ PHX
+ PHB
+ LDA #$0000
+ PHA
+ PLB
+ PLB
+ LDA #$7000
+ STA $2116
+ LDX #$0000
+ .LoopHead:
+ CPX #$0140
+ BEQ .LoopEnd
+ LDA $D92F50, X
+ STA $2118
+ INX
+ INX
+ BRA .LoopHead
+ .LoopEnd:
+ LDX #$0000
+ .2LoopHead:
+ CPX #$0020
+ BEQ .2LoopEnd
+ LDA $D92E10, X
+ STA $2118
+ INX
+ INX
+ BRA .2LoopHead
+ .2LoopEnd:
+ PHY
+ LDA $07D012
+ ASL
+ TAX
+ LDA $07E000, X
+ TAX
+ LDY #$0000
+ .3LoopHead:
+ CPY #$0020
+ BEQ .3LoopEnd
+ LDA $D93170, X
+ STA $2118
+ INX
+ INX
+ INY
+ INY
+ BRA .3LoopHead
+ .3LoopEnd:
+ LDA $07D00C
+ ASL
+ TAX
+ LDA $07E010, X
+ TAX
+ LDY #$0000
+ .4LoopHead:
+ CPY #$0020
+ BEQ .4LoopEnd
+ LDA $D93170, X
+ STA $2118
+ INX
+ INX
+ INY
+ INY
+ BRA .4LoopHead
+ .4LoopEnd:
+ PLY
+ PLB
+ PLX
+ RTL
+
+org $07A380
+HeartStarVisual2:
+ LDA #$2C80
+ STA $0000, Y
+ INY
+ INY
+ LDA #$250A
+ STA $0000, Y
+ INY
+ INY
+ LDA $4053CF
+ ASL
+ TAX
+ .LoopHead:
+ LDA $409000, X
+ CMP #$FFFF
+ BNE .LoopEnd
+ DEX
+ DEX
+ BRA .LoopHead
+ .LoopEnd:
+ LDX #$0000
+ .2LoopHead:
+ CMP #$000A
+ BCC .2LoopEnd
+ SEC
+ SBC #$000A
+ INX
+ BRA .2LoopHead ; return to loop head
+ .2LoopEnd:
+ PHX
+ TAX
+ PLA
+ ORA #$2500
+ PHA
+ LDA #$2C88
+ STA $0000, Y
+ PLA
+ INY
+ INY
+ STA $0000, Y
+ INY
+ INY
+ TXA
+ ORA #$2500
+ PHA
+ LDA #$2C90
+ STA $0000, Y
+ INY
+ INY
+ PLA
+ STA $0000, Y
+ INY
+ INY
+ LDA #$14D8
+ STA $0000, Y
+ INY
+ INY
+ LDA #$250B
+ STA $0000, Y
+ INY
+ INY
+ LDA #$14E0
+ STA $0000, Y
+ INY
+ INY
+ LDA #$250A
+ STA $0000, Y
+ INY
+ INY
+ LDA #$14E8
+ STA $0000, Y
+ INY
+ INY
+ LDA #$250C
+ STA $0000, Y
+ INY
+ INY
+ LDA $3000
+ SEC
+ SBC #$3040
+ LSR
+ LSR
+ .3LoopHead:
+ CMP #$0004
+ BCC .3LoopEnd
+ DEC #4
+ BRA .3LoopHead ; return to loop head
+ .3LoopEnd:
+ STA $3240
+ LDA #$0004
+ SEC
+ SBC $3240
+ TAX
+ LDA #$00FF
+ .4LoopHead:
+ CPX #$0000
+ BEQ .4LoopEnd
+ LSR
+ LSR
+ DEX
+ BRA .4LoopHead
+ .4LoopEnd:
+ LDY $3002
+ AND $0000, Y
+ STA $0000, Y
+ INY
+ LDA #$0000
+ STA $0000, Y
+ INY
+ INY
+ STA $0000, Y
+ RTL
+
+org $07A480
+HeartStarSelectFix:
+ PHX
+ TXA
+ ASL
+ TAX
+ LDA $9020, X
+ DEC
+ TAX
+ .LoopHead:
+ CMP #$0006
+ BMI .LoopEnd
+ INX
+ SEC
+ SBC #$0006
+ BRA .LoopHead
+ .LoopEnd:
+ LDA $53A7, X
+ PLX
+ AND #$00FF
+ RTL
+
+org $07A500
+HeartStarCutsceneFix:
+ TAX
+ LDA $53D3
+ DEC
+ STA $5AC3
+ RTL
+
+org $07A510
+GiftGiving:
+ CMP #$0008
+ .This:
+ BCS .This ; this intentionally safe-crashes the game if hit
+ PHX
+ LDX $901C
+ BEQ .Return
+ PLX
+ STA $8086
+ LDA #$0026
+ JML $CABC99
+ .Return:
+ PLX
+ JML $CABC18
+
+org $07A550
+PauseMenu:
+ JSL $00D29F
+ PHX
+ PHY
+ LDA #$3300
+ STA !VMADDL
+ LDA #$0007
+ STA !A1B0
+ LDA #$F000
+ STA !A1T0L
+ LDA #$01C0
+ STA !DAS0L
+ SEP #$20
+ LDA #$01
+ STA !DMAP0
+ LDA #$18
+ STA !BBAD0
+ LDA #$01
+ STA !MDMAEN
+ REP #$20
+ LDY #$0000
+ .LoopHead:
+ INY ; loop head
+ CPY #$0009
+ BPL .LoopEnd
+ TYA
+ ASL
+ TAX
+ LDA $8020, X
+ BEQ .LoopHead ; return to loop head
+ TYA
+ CLC
+ ADC #$31E2
+ STA !VMADDL
+ LDA $07E020, X
+ STA !VMDATAL
+ BRA .LoopHead ; return to loop head
+ .LoopEnd:
+ LDY #$FFFF
+ .2LoopHead:
+ INY ; loop head
+ CPY #$0007
+ BPL .2LoopEnd
+ TYA
+ ASL
+ TAX
+ LDA $8000, X
+ BEQ .2LoopHead ; return to loop head
+ TYA
+ CLC
+ ADC #$3203
+ STA !VMADDL
+ LDA $07E040, X
+ STA !VMDATAL
+ BRA .2LoopHead ; return to loop head
+ .2LoopEnd:
+ PLY
+ PLX
+ RTL
+
+org $07A600
+StarsSet:
+ PHA
+ PHX
+ PHY
+ LDX $901A
+ BEQ .ApplyStar
+ AND #$00FF
+ PHA
+ LDX $53CF
+ LDY $53D3
+ LDA #$0000
+ DEY
+ .LoopLevel:
+ CPX #$0000
+ BEQ .LoopStage
+ CLC
+ ADC #$0007
+ DEX
+ BRA .LoopLevel
+ .LoopStage:
+ CPY #$0000
+ BEQ .LoopEnd
+ INC
+ DEY
+ BRA .LoopStage
+ .LoopEnd:
+ ASL
+ TAX
+ LDA $07D020, X
+ DEC
+ ASL
+ ASL
+ ASL
+ ASL
+ ASL
+ ASL
+ TAX
+ PLA
+ .2LoopHead:
+ CMP #$0000
+ BEQ .2LoopEnd
+ INX
+ DEC
+ BRA .2LoopHead
+ .2LoopEnd:
+ LDA $B000, X
+ ORA #$0001
+ STA $B000, X
+ .Return:
+ PLY
+ PLX
+ PLA
+ XBA
+ AND #$00FF
+ RTL
+ .ApplyStar:
+ LDA $39D7
+ INC
+ ORA #$8000
+ STA $39D7
+ BRA .Return
+
+
+org $07C000
+ db "KDL3_BASEPATCH_ARCHI"
+
+org $07E000
+ db $20, $03
+ db $20, $00
+ db $80, $01
+ db $20, $01
+ db $00, $00
+ db $00, $00
+ db $00, $00
+ db $00, $00
+ db $A0, $01
+ db $A0, $00
+
+; Pause Icons
+
+org $07E020
+ db $00, $0C
+ db $30, $09
+ db $31, $09
+ db $32, $09
+ db $33, $09
+ db $34, $09
+ db $35, $09
+ db $36, $09
+ db $37, $09
+
+org $07E040
+ db $38, $05
+ db $39, $05
+ db $3A, $01
+ db $3B, $05
+ db $3C, $05
+ db $3D, $05
\ No newline at end of file
diff --git a/worlds/kdl3/test/__init__.py b/worlds/kdl3/test/__init__.py
new file mode 100644
index 000000000000..4d3f4d70faae
--- /dev/null
+++ b/worlds/kdl3/test/__init__.py
@@ -0,0 +1,36 @@
+import typing
+from argparse import Namespace
+
+from BaseClasses import MultiWorld, PlandoOptions, CollectionState
+from test.bases import WorldTestBase
+from test.general import gen_steps
+from worlds import AutoWorld
+from worlds.AutoWorld import call_all
+
+
+class KDL3TestBase(WorldTestBase):
+ game = "Kirby's Dream Land 3"
+
+ def world_setup(self, seed: typing.Optional[int] = None) -> None:
+ if type(self) is WorldTestBase or \
+ (hasattr(WorldTestBase, self._testMethodName)
+ and not self.run_default_tests and
+ getattr(self, self._testMethodName).__code__ is
+ getattr(WorldTestBase, self._testMethodName, None).__code__):
+ return # setUp gets called for tests defined in the base class. We skip world_setup here.
+ if not hasattr(self, "game"):
+ raise NotImplementedError("didn't define game name")
+ self.multiworld = MultiWorld(1)
+ self.multiworld.game[1] = self.game
+ self.multiworld.player_name = {1: "Tester"}
+ self.multiworld.set_seed(seed)
+ self.multiworld.state = CollectionState(self.multiworld)
+ args = Namespace()
+ for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
+ setattr(args, name, {
+ 1: option.from_any(self.options.get(name, getattr(option, "default")))
+ })
+ self.multiworld.set_options(args)
+ self.multiworld.plando_options = PlandoOptions.connections
+ for step in gen_steps:
+ call_all(self.multiworld, step)
diff --git a/worlds/kdl3/test/test_goal.py b/worlds/kdl3/test/test_goal.py
new file mode 100644
index 000000000000..ce53642a9716
--- /dev/null
+++ b/worlds/kdl3/test/test_goal.py
@@ -0,0 +1,64 @@
+from . import KDL3TestBase
+
+
+class TestFastGoal(KDL3TestBase):
+ options = {
+ "open_world": False,
+ "goal_speed": "fast",
+ "total_heart_stars": 30,
+ "heart_stars_required": 50,
+ "filler_percentage": 0,
+ }
+
+ def test_goal(self):
+ self.assertBeatable(False)
+ heart_stars = self.get_items_by_name("Heart Star")
+ self.collect(heart_stars[0:14])
+ self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed))
+ self.assertBeatable(False)
+ self.collect(heart_stars[14:15])
+ self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed))
+ self.assertBeatable(True)
+ self.remove([self.get_item_by_name("Love-Love Rod")])
+ self.collect_by_name("Kine") # Ensure a little more progress, but leave out cutter and burning
+ self.collect(heart_stars[15:])
+ self.assertBeatable(True)
+
+
+class TestNormalGoal(KDL3TestBase):
+ # TODO: open world tests
+ options = {
+ "open_world": False,
+ "goal_speed": "normal",
+ "total_heart_stars": 30,
+ "heart_stars_required": 50,
+ "filler_percentage": 0,
+ }
+
+ def test_goal(self):
+ self.assertBeatable(False)
+ heart_stars = self.get_items_by_name("Heart Star")
+ self.collect(heart_stars[0:14])
+ self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed))
+ self.assertBeatable(False)
+ self.collect(heart_stars[14:15])
+ self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed))
+ self.assertBeatable(False)
+ self.collect_by_name(["Burning", "Cutter", "Kine"])
+ self.assertBeatable(True)
+ self.remove([self.get_item_by_name("Love-Love Rod")])
+ self.collect(heart_stars)
+ self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed))
+ self.assertBeatable(True)
+
+ def test_kine(self):
+ self.collect_by_name(["Cutter", "Burning", "Heart Star"])
+ self.assertBeatable(False)
+
+ def test_cutter(self):
+ self.collect_by_name(["Kine", "Burning", "Heart Star"])
+ self.assertBeatable(False)
+
+ def test_burning(self):
+ self.collect_by_name(["Cutter", "Kine", "Heart Star"])
+ self.assertBeatable(False)
diff --git a/worlds/kdl3/test/test_locations.py b/worlds/kdl3/test/test_locations.py
new file mode 100644
index 000000000000..bde9abc409ac
--- /dev/null
+++ b/worlds/kdl3/test/test_locations.py
@@ -0,0 +1,67 @@
+from . import KDL3TestBase
+from Options import PlandoConnection
+from ..Names import LocationName
+import typing
+
+
+class TestLocations(KDL3TestBase):
+ options = {
+ "open_world": True,
+ "ow_boss_requirement": 1,
+ "strict_bosses": False
+ # these ensure we can always reach all stages physically
+ }
+
+ def test_simple_heart_stars(self):
+ self.run_location_test(LocationName.grass_land_muchi, ["ChuChu"])
+ self.run_location_test(LocationName.grass_land_chao, ["Stone"])
+ self.run_location_test(LocationName.grass_land_mine, ["Kine"])
+ self.run_location_test(LocationName.ripple_field_kamuribana, ["Pitch", "Clean"])
+ self.run_location_test(LocationName.ripple_field_bakasa, ["Kine", "Parasol"])
+ self.run_location_test(LocationName.ripple_field_toad, ["Needle"])
+ self.run_location_test(LocationName.ripple_field_mama_pitch, ["Pitch", "Kine", "Burning", "Stone"])
+ self.run_location_test(LocationName.sand_canyon_auntie, ["Clean"])
+ self.run_location_test(LocationName.sand_canyon_nyupun, ["ChuChu", "Cutter"])
+ self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Ice"])
+ self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Ice"]),
+ self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Needle"]),
+ self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Needle"]),
+ self.run_location_test(LocationName.cloudy_park_hibanamodoki, ["Coo", "Clean"])
+ self.run_location_test(LocationName.cloudy_park_piyokeko, ["Needle"])
+ self.run_location_test(LocationName.cloudy_park_mikarin, ["Coo"])
+ self.run_location_test(LocationName.cloudy_park_pick, ["Rick"])
+ self.run_location_test(LocationName.iceberg_kogoesou, ["Burning"])
+ self.run_location_test(LocationName.iceberg_samus, ["Ice"])
+ self.run_location_test(LocationName.iceberg_name, ["Burning", "Coo", "ChuChu"])
+ self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean",
+ "Stone", "Ice"])
+
+ def run_location_test(self, location: str, itempool: typing.List[str]):
+ items = itempool.copy()
+ while len(itempool) > 0:
+ self.assertFalse(self.can_reach_location(location), str(self.multiworld.seed))
+ self.collect_by_name(itempool.pop())
+ self.assertTrue(self.can_reach_location(location), str(self.multiworld.seed))
+ self.remove(self.get_items_by_name(items))
+
+
+class TestShiro(KDL3TestBase):
+ options = {
+ "open_world": False,
+ "plando_connections": [
+ PlandoConnection("Grass Land 1", "Iceberg 5", "both"),
+ PlandoConnection("Grass Land 2", "Ripple Field 5", "both"),
+ PlandoConnection("Grass Land 3", "Grass Land 1", "both")
+ ],
+ "stage_shuffle": "shuffled",
+ "plando_options": "connections"
+ }
+
+ def test_shiro(self):
+ self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed))
+ self.collect_by_name("Nago")
+ self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed))
+ # despite Shiro only requiring Nago for logic, it cannot be in logic because our two accessible stages
+ # do not actually give the player access to Nago, thus we need Kine to pass 2-5
+ self.collect_by_name("Kine")
+ self.assertTrue(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed))
diff --git a/worlds/kdl3/test/test_shuffles.py b/worlds/kdl3/test/test_shuffles.py
new file mode 100644
index 000000000000..d676b641b056
--- /dev/null
+++ b/worlds/kdl3/test/test_shuffles.py
@@ -0,0 +1,245 @@
+from typing import List, Tuple
+from . import KDL3TestBase
+from ..Room import KDL3Room
+
+
+class TestCopyAbilityShuffle(KDL3TestBase):
+ options = {
+ "open_world": False,
+ "goal_speed": "normal",
+ "total_heart_stars": 30,
+ "heart_stars_required": 50,
+ "filler_percentage": 0,
+ "copy_ability_randomization": "enabled",
+ }
+
+ def test_goal(self):
+ self.assertBeatable(False)
+ heart_stars = self.get_items_by_name("Heart Star")
+ self.collect(heart_stars[0:14])
+ self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed))
+ self.assertBeatable(False)
+ self.collect(heart_stars[14:15])
+ self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed))
+ self.assertBeatable(False)
+ self.collect_by_name(["Burning", "Cutter", "Kine"])
+ self.assertBeatable(True)
+ self.remove([self.get_item_by_name("Love-Love Rod")])
+ self.collect(heart_stars)
+ self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed))
+ self.assertBeatable(True)
+
+ def test_kine(self):
+ self.collect_by_name(["Cutter", "Burning", "Heart Star"])
+ self.assertBeatable(False)
+
+ def test_cutter(self):
+ self.collect_by_name(["Kine", "Burning", "Heart Star"])
+ self.assertBeatable(False)
+
+ def test_burning(self):
+ self.collect_by_name(["Cutter", "Kine", "Heart Star"])
+ self.assertBeatable(False)
+
+ def test_cutter_and_burning_reachable(self):
+ rooms = self.multiworld.worlds[1].rooms
+ copy_abilities = self.multiworld.worlds[1].copy_abilities
+ sand_canyon_5 = self.multiworld.get_region("Sand Canyon 5 - 9", 1)
+ assert isinstance(sand_canyon_5, KDL3Room)
+ valid_rooms = [room for room in rooms if (room.level < sand_canyon_5.level)
+ or (room.level == sand_canyon_5.level and room.stage < sand_canyon_5.stage)]
+ for room in valid_rooms:
+ if any(copy_abilities[enemy] == "Cutter Ability" for enemy in room.enemies):
+ break
+ else:
+ self.fail("Could not reach Cutter Ability before Sand Canyon 5!")
+ iceberg_4 = self.multiworld.get_region("Iceberg 4 - 7", 1)
+ assert isinstance(iceberg_4, KDL3Room)
+ valid_rooms = [room for room in rooms if (room.level < iceberg_4.level)
+ or (room.level == iceberg_4.level and room.stage < iceberg_4.stage)]
+ for room in valid_rooms:
+ if any(copy_abilities[enemy] == "Burning Ability" for enemy in room.enemies):
+ break
+ else:
+ self.fail("Could not reach Burning Ability before Iceberg 4!")
+
+ def test_valid_abilities_for_ROB(self):
+ # there exists a subset of 4-7 abilities that will allow us access to ROB heart star on default settings
+ self.collect_by_name(["Heart Star", "Kine", "Coo"]) # we will guaranteed need Coo, Kine, and Heart Stars to reach
+ # first we need to identify our bukiset requirements
+ groups = [
+ ({"Parasol Ability", "Cutter Ability"}, {'Bukiset (Parasol)', 'Bukiset (Cutter)'}),
+ ({"Spark Ability", "Clean Ability"}, {'Bukiset (Spark)', 'Bukiset (Clean)'}),
+ ({"Ice Ability", "Needle Ability"}, {'Bukiset (Ice)', 'Bukiset (Needle)'}),
+ ({"Stone Ability", "Burning Ability"}, {'Bukiset (Stone)', 'Bukiset (Burning)'}),
+ ]
+ copy_abilities = self.multiworld.worlds[1].copy_abilities
+ required_abilities: List[Tuple[str]] = []
+ for abilities, bukisets in groups:
+ potential_abilities: List[str] = list()
+ for bukiset in bukisets:
+ if copy_abilities[bukiset] in abilities:
+ potential_abilities.append(copy_abilities[bukiset])
+ required_abilities.append(tuple(potential_abilities))
+ collected_abilities = list()
+ for group in required_abilities:
+ self.assertFalse(len(group) == 0, str(self.multiworld.seed))
+ collected_abilities.append(group[0])
+ self.collect_by_name([ability.replace(" Ability", "") for ability in collected_abilities])
+ if "Parasol Ability" not in collected_abilities or "Stone Ability" not in collected_abilities:
+ # required for non-Bukiset related portions
+ self.collect_by_name(["Parasol", "Stone"])
+
+ if "Cutter Ability" not in collected_abilities:
+ # we can't actually reach 3-6 without Cutter
+ self.assertFalse(self.can_reach_location("Sand Canyon 6 - Professor Hector & R.O.B"), str(self.multiworld.seed))
+ self.collect_by_name(["Cutter"])
+
+ self.assertTrue(self.can_reach_location("Sand Canyon 6 - Professor Hector & R.O.B"),
+ ''.join(str(self.multiworld.seed)).join(collected_abilities))
+
+
+class TestAnimalShuffle(KDL3TestBase):
+ options = {
+ "open_world": False,
+ "goal_speed": "normal",
+ "total_heart_stars": 30,
+ "heart_stars_required": 50,
+ "filler_percentage": 0,
+ "animal_randomization": "full",
+ }
+
+ def test_goal(self):
+ self.assertBeatable(False)
+ heart_stars = self.get_items_by_name("Heart Star")
+ self.collect(heart_stars[0:14])
+ self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed))
+ self.assertBeatable(False)
+ self.collect(heart_stars[14:15])
+ self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed))
+ self.assertBeatable(False)
+ self.collect_by_name(["Burning", "Cutter", "Kine"])
+ self.assertBeatable(True)
+ self.remove([self.get_item_by_name("Love-Love Rod")])
+ self.collect(heart_stars)
+ self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed))
+ self.assertBeatable(True)
+
+ def test_kine(self):
+ self.collect_by_name(["Cutter", "Burning", "Heart Star"])
+ self.assertBeatable(False)
+
+ def test_cutter(self):
+ self.collect_by_name(["Kine", "Burning", "Heart Star"])
+ self.assertBeatable(False)
+
+ def test_burning(self):
+ self.collect_by_name(["Cutter", "Kine", "Heart Star"])
+ self.assertBeatable(False)
+
+ def test_locked_animals(self):
+ self.assertTrue(self.multiworld.get_location("Ripple Field 5 - Animal 2", 1).item.name == "Pitch Spawn")
+ self.assertTrue(self.multiworld.get_location("Iceberg 4 - Animal 1", 1).item.name == "ChuChu Spawn")
+ self.assertTrue(self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1).item.name in {"Kine Spawn", "Coo Spawn"})
+
+
+class TestAllShuffle(KDL3TestBase):
+ options = {
+ "open_world": False,
+ "goal_speed": "normal",
+ "total_heart_stars": 30,
+ "heart_stars_required": 50,
+ "filler_percentage": 0,
+ "animal_randomization": "full",
+ "copy_ability_randomization": "enabled",
+ }
+
+ def test_goal(self):
+ self.assertBeatable(False)
+ heart_stars = self.get_items_by_name("Heart Star")
+ self.collect(heart_stars[0:14])
+ self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed))
+ self.assertBeatable(False)
+ self.collect(heart_stars[14:15])
+ self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed))
+ self.assertBeatable(False)
+ self.collect_by_name(["Burning", "Cutter", "Kine"])
+ self.assertBeatable(True)
+ self.remove([self.get_item_by_name("Love-Love Rod")])
+ self.collect(heart_stars)
+ self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed))
+ self.assertBeatable(True)
+
+ def test_kine(self):
+ self.collect_by_name(["Cutter", "Burning", "Heart Star"])
+ self.assertBeatable(False)
+
+ def test_cutter(self):
+ self.collect_by_name(["Kine", "Burning", "Heart Star"])
+ self.assertBeatable(False)
+
+ def test_burning(self):
+ self.collect_by_name(["Cutter", "Kine", "Heart Star"])
+ self.assertBeatable(False)
+
+ def test_locked_animals(self):
+ self.assertTrue(self.multiworld.get_location("Ripple Field 5 - Animal 2", 1).item.name == "Pitch Spawn")
+ self.assertTrue(self.multiworld.get_location("Iceberg 4 - Animal 1", 1).item.name == "ChuChu Spawn")
+ self.assertTrue(self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1).item.name in {"Kine Spawn", "Coo Spawn"})
+
+ def test_cutter_and_burning_reachable(self):
+ rooms = self.multiworld.worlds[1].rooms
+ copy_abilities = self.multiworld.worlds[1].copy_abilities
+ sand_canyon_5 = self.multiworld.get_region("Sand Canyon 5 - 9", 1)
+ assert isinstance(sand_canyon_5, KDL3Room)
+ valid_rooms = [room for room in rooms if (room.level < sand_canyon_5.level)
+ or (room.level == sand_canyon_5.level and room.stage < sand_canyon_5.stage)]
+ for room in valid_rooms:
+ if any(copy_abilities[enemy] == "Cutter Ability" for enemy in room.enemies):
+ break
+ else:
+ self.fail("Could not reach Cutter Ability before Sand Canyon 5!")
+ iceberg_4 = self.multiworld.get_region("Iceberg 4 - 7", 1)
+ assert isinstance(iceberg_4, KDL3Room)
+ valid_rooms = [room for room in rooms if (room.level < iceberg_4.level)
+ or (room.level == iceberg_4.level and room.stage < iceberg_4.stage)]
+ for room in valid_rooms:
+ if any(copy_abilities[enemy] == "Burning Ability" for enemy in room.enemies):
+ break
+ else:
+ self.fail("Could not reach Burning Ability before Iceberg 4!")
+
+ def test_valid_abilities_for_ROB(self):
+ # there exists a subset of 4-7 abilities that will allow us access to ROB heart star on default settings
+ self.collect_by_name(["Heart Star", "Kine", "Coo"]) # we will guaranteed need Coo, Kine, and Heart Stars to reach
+ # first we need to identify our bukiset requirements
+ groups = [
+ ({"Parasol Ability", "Cutter Ability"}, {'Bukiset (Parasol)', 'Bukiset (Cutter)'}),
+ ({"Spark Ability", "Clean Ability"}, {'Bukiset (Spark)', 'Bukiset (Clean)'}),
+ ({"Ice Ability", "Needle Ability"}, {'Bukiset (Ice)', 'Bukiset (Needle)'}),
+ ({"Stone Ability", "Burning Ability"}, {'Bukiset (Stone)', 'Bukiset (Burning)'}),
+ ]
+ copy_abilities = self.multiworld.worlds[1].copy_abilities
+ required_abilities: List[Tuple[str]] = []
+ for abilities, bukisets in groups:
+ potential_abilities: List[str] = list()
+ for bukiset in bukisets:
+ if copy_abilities[bukiset] in abilities:
+ potential_abilities.append(copy_abilities[bukiset])
+ required_abilities.append(tuple(potential_abilities))
+ collected_abilities = list()
+ for group in required_abilities:
+ self.assertFalse(len(group) == 0, str(self.multiworld.seed))
+ collected_abilities.append(group[0])
+ self.collect_by_name([ability.replace(" Ability", "") for ability in collected_abilities])
+ if "Parasol Ability" not in collected_abilities or "Stone Ability" not in collected_abilities:
+ # required for non-Bukiset related portions
+ self.collect_by_name(["Parasol", "Stone"])
+
+ if "Cutter Ability" not in collected_abilities:
+ # we can't actually reach 3-6 without Cutter
+ self.assertFalse(self.can_reach_location("Sand Canyon 6 - Professor Hector & R.O.B"), str(self.multiworld.seed))
+ self.collect_by_name(["Cutter"])
+
+ self.assertTrue(self.can_reach_location("Sand Canyon 6 - Professor Hector & R.O.B"),
+ ''.join(str(self.multiworld.seed)).join(collected_abilities))
diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py
new file mode 100644
index 000000000000..e2d2338b7651
--- /dev/null
+++ b/worlds/kh2/Client.py
@@ -0,0 +1,983 @@
+import ModuleUpdate
+
+ModuleUpdate.update()
+
+import os
+import asyncio
+import json
+from pymem import pymem
+from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, SupportAbility_Table, ActionAbility_Table, all_weapon_slot
+from .Names import ItemName
+from .WorldLocations import *
+
+from NetUtils import ClientStatus
+from CommonClient import gui_enabled, logger, get_base_parser, CommonContext, server_loop
+
+
+class KH2Context(CommonContext):
+ # command_processor: int = KH2CommandProcessor
+ game = "Kingdom Hearts 2"
+ items_handling = 0b111 # Indicates you get items sent from other worlds.
+
+ def __init__(self, server_address, password):
+ super(KH2Context, self).__init__(server_address, password)
+ self.goofy_ability_to_slot = dict()
+ self.donald_ability_to_slot = dict()
+ self.all_weapon_location_id = None
+ self.sora_ability_to_slot = dict()
+ self.kh2_seed_save = None
+ self.kh2_local_items = None
+ self.growthlevel = None
+ self.kh2connected = False
+ self.kh2_finished_game = False
+ self.serverconneced = False
+ self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
+ self.location_name_to_data = {name: data for name, data, in all_locations.items()}
+ self.kh2_loc_name_to_id = None
+ self.kh2_item_name_to_id = None
+ self.lookup_id_to_item = None
+ self.lookup_id_to_location = None
+ self.sora_ability_dict = {k: v.quantity for dic in [SupportAbility_Table, ActionAbility_Table] for k, v in
+ dic.items()}
+ self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()}
+
+ self.sending = []
+ # list used to keep track of locations+items player has. Used for disoneccting
+ self.kh2_seed_save_cache = {
+ "itemIndex": -1,
+ # back of soras invo is 0x25E2. Growth should be moved there
+ # Character: [back of invo, front of invo]
+ "SoraInvo": [0x25D8, 0x2546],
+ "DonaldInvo": [0x26F4, 0x2658],
+ "GoofyInvo": [0x2808, 0x276C],
+ "AmountInvo": {
+ "Ability": {},
+ "Amount": {
+ "Bounty": 0,
+ },
+ "Growth": {
+ "High Jump": 0, "Quick Run": 0, "Dodge Roll": 0,
+ "Aerial Dodge": 0, "Glide": 0
+ },
+ "Bitmask": [],
+ "Weapon": {"Sora": [], "Donald": [], "Goofy": []},
+ "Equipment": [],
+ "Magic": {
+ "Fire Element": 0,
+ "Blizzard Element": 0,
+ "Thunder Element": 0,
+ "Cure Element": 0,
+ "Magnet Element": 0,
+ "Reflect Element": 0
+ },
+ "StatIncrease": {
+ ItemName.MaxHPUp: 0,
+ ItemName.MaxMPUp: 0,
+ ItemName.DriveGaugeUp: 0,
+ ItemName.ArmorSlotUp: 0,
+ ItemName.AccessorySlotUp: 0,
+ ItemName.ItemSlotUp: 0,
+ },
+ },
+ }
+ self.kh2seedname = None
+ self.kh2slotdata = None
+ self.itemamount = {}
+ if "localappdata" in os.environ:
+ self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP")
+ self.hitlist_bounties = 0
+ # hooked object
+ self.kh2 = None
+ self.final_xemnas = False
+ self.worldid_to_locations = {
+ # 1: {}, # world of darkness (story cutscenes)
+ 2: TT_Checks,
+ # 3: {}, # destiny island doesn't have checks
+ 4: HB_Checks,
+ 5: BC_Checks,
+ 6: Oc_Checks,
+ 7: AG_Checks,
+ 8: LoD_Checks,
+ 9: HundredAcreChecks,
+ 10: PL_Checks,
+ 11: Atlantica_Checks,
+ 12: DC_Checks,
+ 13: TR_Checks,
+ 14: HT_Checks,
+ 15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb
+ 16: PR_Checks,
+ 17: SP_Checks,
+ 18: TWTNW_Checks,
+ # 255: {}, # starting screen
+ }
+ # 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room
+ # self.sveroom = 0x2A09C00 + 0x41
+ # 0 not in battle 1 in yellow battle 2 red battle #short
+ # self.inBattle = 0x2A0EAC4 + 0x40
+ # self.onDeath = 0xAB9078
+ # PC Address anchors
+ # self.Now = 0x0714DB8 old address
+ # epic addresses
+ self.Now = 0x0716DF8
+ self.Save = 0x09A92F0
+ self.Journal = 0x743260
+ self.Shop = 0x743350
+ self.Slot1 = 0x2A22FD8
+ # self.Sys3 = 0x2A59DF0
+ # self.Bt10 = 0x2A74880
+ # self.BtlEnd = 0x2A0D3E0
+ # self.Slot1 = 0x2A20C98 old address
+
+ self.kh2_game_version = None # can be egs or steam
+
+ self.chest_set = set(exclusion_table["Chests"])
+ self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"])
+ self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"])
+ self.shield_set = set(CheckDupingItems["Weapons"]["Shields"])
+
+ self.all_weapons = self.keyblade_set.union(self.staff_set).union(self.shield_set)
+
+ self.equipment_categories = CheckDupingItems["Equipment"]
+ self.armor_set = set(self.equipment_categories["Armor"])
+ self.accessories_set = set(self.equipment_categories["Accessories"])
+ self.all_equipment = self.armor_set.union(self.accessories_set)
+
+ self.Equipment_Anchor_Dict = {
+ "Armor": [0x2504, 0x2506, 0x2508, 0x250A],
+ "Accessories": [0x2514, 0x2516, 0x2518, 0x251A]
+ }
+
+ self.AbilityQuantityDict = {}
+ self.ability_categories = CheckDupingItems["Abilities"]
+
+ self.sora_ability_set = set(self.ability_categories["Sora"])
+ self.donald_ability_set = set(self.ability_categories["Donald"])
+ self.goofy_ability_set = set(self.ability_categories["Goofy"])
+
+ self.all_abilities = self.sora_ability_set.union(self.donald_ability_set).union(self.goofy_ability_set)
+
+ self.stat_increase_set = set(CheckDupingItems["Stat Increases"])
+ self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities}
+
+ # Growth:[level 1,level 4,slot]
+ self.growth_values_dict = {
+ "High Jump": [0x05E, 0x061, 0x25DA],
+ "Quick Run": [0x62, 0x65, 0x25DC],
+ "Dodge Roll": [0x234, 0x237, 0x25DE],
+ "Aerial Dodge": [0x66, 0x069, 0x25E0],
+ "Glide": [0x6A, 0x6D, 0x25E2]
+ }
+
+ self.ability_code_list = None
+ self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}
+
+ self.base_hp = 20
+ self.base_mp = 100
+ self.base_drive = 5
+ self.base_accessory_slots = 1
+ self.base_armor_slots = 1
+ self.base_item_slots = 3
+ self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, 0x2770, 0x2772]
+
+ async def server_auth(self, password_requested: bool = False):
+ if password_requested and not self.password:
+ await super(KH2Context, self).server_auth(password_requested)
+ await self.get_username()
+ await self.send_connect()
+
+ async def connection_closed(self):
+ self.kh2connected = False
+ self.serverconneced = False
+ if self.kh2seedname is not None and self.auth is not None:
+ with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
+ 'w') as f:
+ f.write(json.dumps(self.kh2_seed_save, indent=4))
+ await super(KH2Context, self).connection_closed()
+
+ async def disconnect(self, allow_autoreconnect: bool = False):
+ self.kh2connected = False
+ self.serverconneced = False
+ if self.kh2seedname not in {None} and self.auth not in {None}:
+ with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
+ 'w') as f:
+ f.write(json.dumps(self.kh2_seed_save, indent=4))
+ await super(KH2Context, self).disconnect()
+
+ @property
+ def endpoints(self):
+ if self.server:
+ return [self.server]
+ else:
+ return []
+
+ async def shutdown(self):
+ if self.kh2seedname not in {None} and self.auth not in {None}:
+ with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
+ 'w') as f:
+ f.write(json.dumps(self.kh2_seed_save, indent=4))
+ await super(KH2Context, self).shutdown()
+
+ def kh2_read_short(self, address):
+ return self.kh2.read_short(self.kh2.base_address + address)
+
+ def kh2_write_short(self, address, value):
+ return self.kh2.write_short(self.kh2.base_address + address, value)
+
+ def kh2_write_byte(self, address, value):
+ return self.kh2.write_bytes(self.kh2.base_address + address, value.to_bytes(1, 'big'), 1)
+
+ def kh2_read_byte(self, address):
+ return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1), "big")
+
+ def kh2_read_int(self, address):
+ return self.kh2.read_int(self.kh2.base_address + address)
+
+ def kh2_write_int(self, address, value):
+ self.kh2.write_int(self.kh2.base_address + address, value)
+
+ def kh2_read_string(self, address, length):
+ return self.kh2.read_string(self.kh2.base_address + address, length)
+
+ def on_package(self, cmd: str, args: dict):
+ if cmd in {"RoomInfo"}:
+ self.kh2seedname = args['seed_name']
+ if not os.path.exists(self.game_communication_path):
+ os.makedirs(self.game_communication_path)
+ if not os.path.exists(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json"):
+ self.kh2_seed_save = {
+ "Levels": {
+ "SoraLevel": 0,
+ "ValorLevel": 0,
+ "WisdomLevel": 0,
+ "LimitLevel": 0,
+ "MasterLevel": 0,
+ "FinalLevel": 0,
+ "SummonLevel": 0,
+ },
+ "SoldEquipment": [],
+ }
+ with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
+ 'wt') as f:
+ pass
+ # self.locations_checked = set()
+ elif os.path.exists(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json"):
+ with open(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json", 'r') as f:
+ self.kh2_seed_save = json.load(f)
+ if self.kh2_seed_save is None:
+ self.kh2_seed_save = {
+ "Levels": {
+ "SoraLevel": 0,
+ "ValorLevel": 0,
+ "WisdomLevel": 0,
+ "LimitLevel": 0,
+ "MasterLevel": 0,
+ "FinalLevel": 0,
+ "SummonLevel": 0,
+ },
+ "SoldEquipment": [],
+ }
+ # self.locations_checked = set(self.kh2_seed_save_cache["LocationsChecked"])
+ # self.serverconneced = True
+
+ if cmd in {"Connected"}:
+ asyncio.create_task(self.send_msgs([{"cmd": "GetDataPackage", "games": ["Kingdom Hearts 2"]}]))
+ self.kh2slotdata = args['slot_data']
+ # self.kh2_local_items = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()}
+ self.locations_checked = set(args["checked_locations"])
+
+ if cmd in {"ReceivedItems"}:
+ # 0x2546
+ # 0x2658
+ # 0x276A
+ start_index = args["index"]
+ if start_index == 0:
+ self.kh2_seed_save_cache = {
+ "itemIndex": -1,
+ # back of soras invo is 0x25E2. Growth should be moved there
+ # Character: [back of invo, front of invo]
+ "SoraInvo": [0x25D8, 0x2546],
+ "DonaldInvo": [0x26F4, 0x2658],
+ "GoofyInvo": [0x2808, 0x276C],
+ "AmountInvo": {
+ "Ability": {},
+ "Amount": {
+ "Bounty": 0,
+ },
+ "Growth": {
+ "High Jump": 0, "Quick Run": 0, "Dodge Roll": 0,
+ "Aerial Dodge": 0, "Glide": 0
+ },
+ "Bitmask": [],
+ "Weapon": {"Sora": [], "Donald": [], "Goofy": []},
+ "Equipment": [],
+ "Magic": {
+ "Fire Element": 0,
+ "Blizzard Element": 0,
+ "Thunder Element": 0,
+ "Cure Element": 0,
+ "Magnet Element": 0,
+ "Reflect Element": 0
+ },
+ "StatIncrease": {
+ ItemName.MaxHPUp: 0,
+ ItemName.MaxMPUp: 0,
+ ItemName.DriveGaugeUp: 0,
+ ItemName.ArmorSlotUp: 0,
+ ItemName.AccessorySlotUp: 0,
+ ItemName.ItemSlotUp: 0,
+ },
+ },
+ }
+ if start_index > self.kh2_seed_save_cache["itemIndex"] and self.serverconneced:
+ self.kh2_seed_save_cache["itemIndex"] = start_index
+ for item in args['items']:
+ asyncio.create_task(self.give_item(item.item, item.location))
+
+ if cmd in {"RoomUpdate"}:
+ if "checked_locations" in args:
+ new_locations = set(args["checked_locations"])
+ self.locations_checked |= new_locations
+
+ if cmd in {"DataPackage"}:
+ self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"]
+ self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()}
+ self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"]
+ self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()}
+ self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]]
+
+ if "keyblade_abilities" in self.kh2slotdata.keys():
+ sora_ability_dict = self.kh2slotdata["KeybladeAbilities"]
+ # sora ability to slot
+ # itemid:[slots that are available for that item]
+ for k, v in sora_ability_dict.items():
+ if v >= 1:
+ if k not in self.sora_ability_to_slot.keys():
+ self.sora_ability_to_slot[k] = []
+ for _ in range(sora_ability_dict[k]):
+ self.sora_ability_to_slot[k].append(self.kh2_seed_save_cache["SoraInvo"][0])
+ self.kh2_seed_save_cache["SoraInvo"][0] -= 2
+ donald_ability_dict = self.kh2slotdata["StaffAbilities"]
+ for k, v in donald_ability_dict.items():
+ if v >= 1:
+ if k not in self.donald_ability_to_slot.keys():
+ self.donald_ability_to_slot[k] = []
+ for _ in range(donald_ability_dict[k]):
+ self.donald_ability_to_slot[k].append(self.kh2_seed_save_cache["DonaldInvo"][0])
+ self.kh2_seed_save_cache["DonaldInvo"][0] -= 2
+ goofy_ability_dict = self.kh2slotdata["ShieldAbilities"]
+ for k, v in goofy_ability_dict.items():
+ if v >= 1:
+ if k not in self.goofy_ability_to_slot.keys():
+ self.goofy_ability_to_slot[k] = []
+ for _ in range(goofy_ability_dict[k]):
+ self.goofy_ability_to_slot[k].append(self.kh2_seed_save_cache["GoofyInvo"][0])
+ self.kh2_seed_save_cache["GoofyInvo"][0] -= 2
+
+ all_weapon_location_id = []
+ for weapon_location in all_weapon_slot:
+ all_weapon_location_id.append(self.kh2_loc_name_to_id[weapon_location])
+ self.all_weapon_location_id = set(all_weapon_location_id)
+
+ try:
+ self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
+ if self.kh2_game_version is None:
+ if self.kh2_read_string(0x09A9830, 4) == "KH2J":
+ self.kh2_game_version = "STEAM"
+ self.Now = 0x0717008
+ self.Save = 0x09A9830
+ self.Slot1 = 0x2A23518
+ self.Journal = 0x7434E0
+ self.Shop = 0x7435D0
+
+ elif self.kh2_read_string(0x09A92F0, 4) == "KH2J":
+ self.kh2_game_version = "EGS"
+ else:
+ self.kh2_game_version = None
+ logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.")
+ if self.kh2_game_version is not None:
+ logger.info(f"You are now auto-tracking. {self.kh2_game_version}")
+ self.kh2connected = True
+
+ except Exception as e:
+ if self.kh2connected:
+ self.kh2connected = False
+ logger.info("Game is not open.")
+ self.serverconneced = True
+ asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}]))
+
+ async def checkWorldLocations(self):
+ try:
+ currentworldint = self.kh2_read_byte(self.Now)
+ await self.send_msgs([{
+ "cmd": "Set", "key": "Slot: " + str(self.slot) + " :CurrentWorld",
+ "default": 0, "want_reply": True, "operations": [{
+ "operation": "replace",
+ "value": currentworldint
+ }]
+ }])
+ if currentworldint in self.worldid_to_locations:
+ curworldid = self.worldid_to_locations[currentworldint]
+ for location, data in curworldid.items():
+ if location in self.kh2_loc_name_to_id.keys():
+ locationId = self.kh2_loc_name_to_id[location]
+ if locationId not in self.locations_checked \
+ and self.kh2_read_byte(self.Save + data.addrObtained) & 0x1 << data.bitIndex > 0:
+ self.sending = self.sending + [(int(locationId))]
+ except Exception as e:
+ if self.kh2connected:
+ self.kh2connected = False
+ logger.info(e)
+ logger.info("line 425")
+
+ async def checkLevels(self):
+ try:
+ for location, data in SoraLevels.items():
+ currentLevel = self.kh2_read_byte(self.Save + 0x24FF)
+ locationId = self.kh2_loc_name_to_id[location]
+ if locationId not in self.locations_checked \
+ and currentLevel >= data.bitIndex:
+ if self.kh2_seed_save["Levels"]["SoraLevel"] < currentLevel:
+ self.kh2_seed_save["Levels"]["SoraLevel"] = currentLevel
+ self.sending = self.sending + [(int(locationId))]
+ formDict = {
+ 0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels],
+ 3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels], 5: ["SummonLevel", SummonLevels]
+ }
+ # TODO: remove formDict[i][0] in self.kh2_seed_save_cache["Levels"].keys() after 4.3
+ for i in range(6):
+ for location, data in formDict[i][1].items():
+ formlevel = self.kh2_read_byte(self.Save + data.addrObtained)
+ if location in self.kh2_loc_name_to_id.keys():
+ # if current form level is above other form level
+ locationId = self.kh2_loc_name_to_id[location]
+ if locationId not in self.locations_checked \
+ and formlevel >= data.bitIndex:
+ if formlevel > self.kh2_seed_save["Levels"][formDict[i][0]]:
+ self.kh2_seed_save["Levels"][formDict[i][0]] = formlevel
+ self.sending = self.sending + [(int(locationId))]
+ except Exception as e:
+ if self.kh2connected:
+ self.kh2connected = False
+ logger.info(e)
+ logger.info("line 456")
+
+ async def checkSlots(self):
+ try:
+ for location, data in weaponSlots.items():
+ locationId = self.kh2_loc_name_to_id[location]
+ if locationId not in self.locations_checked:
+ if self.kh2_read_byte(self.Save + data.addrObtained) > 0:
+ self.sending = self.sending + [(int(locationId))]
+
+ for location, data in formSlots.items():
+ locationId = self.kh2_loc_name_to_id[location]
+ if locationId not in self.locations_checked and self.kh2_read_byte(self.Save + 0x06B2) == 0:
+ if self.kh2_read_byte(self.Save + data.addrObtained) & 0x1 << data.bitIndex > 0:
+ self.sending = self.sending + [(int(locationId))]
+ except Exception as e:
+ if self.kh2connected:
+ self.kh2connected = False
+ logger.info(e)
+ logger.info("line 475")
+
+ async def verifyChests(self):
+ try:
+ for location in self.locations_checked:
+ locationName = self.lookup_id_to_location[location]
+ if locationName in self.chest_set:
+ if locationName in self.location_name_to_worlddata.keys():
+ locationData = self.location_name_to_worlddata[locationName]
+ if self.kh2_read_byte(self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0:
+ roomData = self.kh2_read_byte(self.Save + locationData.addrObtained)
+ self.kh2_write_byte(self.Save + locationData.addrObtained, roomData | 0x01 << locationData.bitIndex)
+
+ except Exception as e:
+ if self.kh2connected:
+ self.kh2connected = False
+ logger.info(e)
+ logger.info("line 491")
+
+ async def verifyLevel(self):
+ for leveltype, anchor in {
+ "SoraLevel": 0x24FF,
+ "ValorLevel": 0x32F6,
+ "WisdomLevel": 0x332E,
+ "LimitLevel": 0x3366,
+ "MasterLevel": 0x339E,
+ "FinalLevel": 0x33D6
+ }.items():
+ if self.kh2_read_byte(self.Save + anchor) < self.kh2_seed_save["Levels"][leveltype]:
+ self.kh2_write_byte(self.Save + anchor, self.kh2_seed_save["Levels"][leveltype])
+
+ async def give_item(self, item, location):
+ try:
+ # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites
+ itemname = self.lookup_id_to_item[item]
+ itemdata = self.item_name_to_data[itemname]
+ # itemcode = self.kh2_item_name_to_id[itemname]
+ if itemdata.ability:
+ if location in self.all_weapon_location_id:
+ return
+ if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}:
+ self.kh2_seed_save_cache["AmountInvo"]["Growth"][itemname] += 1
+ return
+
+ if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]:
+ self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname] = []
+ # appending the slot that the ability should be in
+ # for non beta. remove after 4.3
+ if "PoptrackerVersion" in self.kh2slotdata:
+ if self.kh2slotdata["PoptrackerVersionCheck"] < 4.3:
+ if (itemname in self.sora_ability_set
+ and len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < self.item_name_to_data[itemname].quantity) \
+ and self.kh2_seed_save_cache["SoraInvo"][1] > 0x254C:
+ ability_slot = self.kh2_seed_save_cache["SoraInvo"][1]
+ self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
+ self.kh2_seed_save_cache["SoraInvo"][1] -= 2
+ elif itemname in self.donald_ability_set:
+ ability_slot = self.kh2_seed_save_cache["DonaldInvo"][1]
+ self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
+ self.kh2_seed_save_cache["DonaldInvo"][1] -= 2
+ else:
+ ability_slot = self.kh2_seed_save_cache["GoofyInvo"][1]
+ self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
+ self.kh2_seed_save_cache["GoofyInvo"][1] -= 2
+ if ability_slot in self.front_ability_slots:
+ self.front_ability_slots.remove(ability_slot)
+
+ elif len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \
+ self.AbilityQuantityDict[itemname]:
+ if itemname in self.sora_ability_set:
+ ability_slot = self.kh2_seed_save_cache["SoraInvo"][0]
+ self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
+ self.kh2_seed_save_cache["SoraInvo"][0] -= 2
+ elif itemname in self.donald_ability_set:
+ ability_slot = self.kh2_seed_save_cache["DonaldInvo"][0]
+ self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
+ self.kh2_seed_save_cache["DonaldInvo"][0] -= 2
+ else:
+ ability_slot = self.kh2_seed_save_cache["GoofyInvo"][0]
+ self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot)
+ self.kh2_seed_save_cache["GoofyInvo"][0] -= 2
+
+ if ability_slot in self.front_ability_slots:
+ self.front_ability_slots.remove(ability_slot)
+
+ elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}:
+ # if memaddr is in a bitmask location in memory
+ if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]:
+ self.kh2_seed_save_cache["AmountInvo"]["Bitmask"].append(itemname)
+
+ elif itemdata.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}:
+ # if memaddr is in magic addresses
+ self.kh2_seed_save_cache["AmountInvo"]["Magic"][itemname] += 1
+
+ elif itemname in self.all_equipment:
+ self.kh2_seed_save_cache["AmountInvo"]["Equipment"].append(itemname)
+
+ elif itemname in self.all_weapons:
+ if itemname in self.keyblade_set:
+ self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"].append(itemname)
+ elif itemname in self.staff_set:
+ self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Donald"].append(itemname)
+ else:
+ self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Goofy"].append(itemname)
+
+ elif itemname in self.stat_increase_set:
+ self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][itemname] += 1
+ else:
+ if itemname in self.kh2_seed_save_cache["AmountInvo"]["Amount"]:
+ self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] += 1
+ else:
+ self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] = 1
+
+ except Exception as e:
+ if self.kh2connected:
+ self.kh2connected = False
+ logger.info(e)
+ logger.info("line 582")
+
+ def run_gui(self):
+ """Import kivy UI system and start running it as self.ui_task."""
+ from kvui import GameManager
+
+ class KH2Manager(GameManager):
+ logging_pairs = [
+ ("Client", "Archipelago")
+ ]
+ base_title = "Archipelago KH2 Client"
+
+ self.ui = KH2Manager(self)
+ self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
+
+ async def IsInShop(self, sellable):
+ # journal = 0x741230 shop = 0x741320
+ # if journal=-1 and shop = 5 then in shop
+ # if journal !=-1 and shop = 10 then journal
+
+ journal = self.kh2_read_short(self.Journal)
+ shop = self.kh2_read_short(self.Shop)
+ if (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
+ # print("your in the shop")
+ sellable_dict = {}
+ for itemName in sellable:
+ itemdata = self.item_name_to_data[itemName]
+ amount = self.kh2_read_byte(self.Save + itemdata.memaddr)
+ sellable_dict[itemName] = amount
+ while (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
+ journal = self.kh2_read_short(self.Journal)
+ shop = self.kh2_read_short(self.Shop)
+ await asyncio.sleep(0.5)
+ for item, amount in sellable_dict.items():
+ itemdata = self.item_name_to_data[item]
+ afterShop = self.kh2_read_byte(self.Save + itemdata.memaddr)
+ if afterShop < amount:
+ self.kh2_seed_save["SoldEquipment"].append(item)
+
+ async def verifyItems(self):
+ try:
+ master_amount = set(self.kh2_seed_save_cache["AmountInvo"]["Amount"].keys())
+
+ master_ability = set(self.kh2_seed_save_cache["AmountInvo"]["Ability"].keys())
+
+ master_bitmask = set(self.kh2_seed_save_cache["AmountInvo"]["Bitmask"])
+
+ master_keyblade = set(self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"])
+ master_staff = set(self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Donald"])
+ master_shield = set(self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Goofy"])
+
+ master_equipment = set(self.kh2_seed_save_cache["AmountInvo"]["Equipment"])
+
+ master_magic = set(self.kh2_seed_save_cache["AmountInvo"]["Magic"].keys())
+
+ master_stat = set(self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"].keys())
+
+ master_sell = master_equipment | master_staff | master_shield
+
+ await asyncio.create_task(self.IsInShop(master_sell))
+ # print(self.kh2_seed_save_cache["AmountInvo"]["Ability"])
+ for item_name in master_amount:
+ item_data = self.item_name_to_data[item_name]
+ amount_of_items = 0
+ amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Amount"][item_name]
+
+ if item_name == "Torn Page":
+ # Torn Pages are handled differently because they can be consumed.
+ # Will check the progression in 100 acre and - the amount of visits
+ # amountofitems-amount of visits done
+ for location, data in tornPageLocks.items():
+ if self.kh2_read_byte(self.Save + data.addrObtained) & 0x1 << data.bitIndex > 0:
+ amount_of_items -= 1
+ if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and amount_of_items >= 0:
+ self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
+
+ for item_name in master_keyblade:
+ item_data = self.item_name_to_data[item_name]
+ # if the inventory slot for that keyblade is less than the amount they should have,
+ # and they are not in stt
+ if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte(self.Save + 0x1CFF) != 13:
+ # Checking form anchors for the keyblade to remove extra keyblades
+ if self.kh2_read_short(self.Save + 0x24F0) == item_data.kh2id \
+ or self.kh2_read_short(self.Save + 0x32F4) == item_data.kh2id \
+ or self.kh2_read_short(self.Save + 0x339C) == item_data.kh2id \
+ or self.kh2_read_short(self.Save + 0x33D4) == item_data.kh2id:
+ self.kh2_write_byte(self.Save + item_data.memaddr, 0)
+ else:
+ self.kh2_write_byte(self.Save + item_data.memaddr, 1)
+
+ for item_name in master_staff:
+ item_data = self.item_name_to_data[item_name]
+ if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 \
+ and self.kh2_read_short(self.Save + 0x2604) != item_data.kh2id \
+ and item_name not in self.kh2_seed_save["SoldEquipment"]:
+ self.kh2_write_byte(self.Save + item_data.memaddr, 1)
+
+ for item_name in master_shield:
+ item_data = self.item_name_to_data[item_name]
+ if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 \
+ and self.kh2_read_short(self.Save + 0x2718) != item_data.kh2id \
+ and item_name not in self.kh2_seed_save["SoldEquipment"]:
+ self.kh2_write_byte(self.Save + item_data.memaddr, 1)
+
+ for item_name in master_ability:
+ item_data = self.item_name_to_data[item_name]
+ ability_slot = []
+ ability_slot += self.kh2_seed_save_cache["AmountInvo"]["Ability"][item_name]
+ for slot in ability_slot:
+ current = self.kh2_read_short(self.Save + slot)
+ ability = current & 0x0FFF
+ if ability | 0x8000 != (0x8000 + item_data.memaddr):
+ if current - 0x8000 > 0:
+ self.kh2_write_short(self.Save + slot, 0x8000 + item_data.memaddr)
+ else:
+ self.kh2_write_short(self.Save + slot, item_data.memaddr)
+ # removes the duped ability if client gave faster than the game.
+
+ for ability in self.front_ability_slots:
+ if self.kh2_read_short(self.Save + ability) != 0:
+ print(f"removed {self.Save + ability} from {ability}")
+ self.kh2_write_short(self.Save + ability, 0)
+
+ # remove the dummy level 1 growths if they are in these invo slots.
+ for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}:
+ current = self.kh2_read_short(self.Save + inventorySlot)
+ ability = current & 0x0FFF
+ if 0x05E <= ability <= 0x06D:
+ self.kh2_write_short(self.Save + inventorySlot, 0)
+
+ for item_name in self.master_growth:
+ growthLevel = self.kh2_seed_save_cache["AmountInvo"]["Growth"][item_name]
+ if growthLevel > 0:
+ slot = self.growth_values_dict[item_name][2]
+ min_growth = self.growth_values_dict[item_name][0]
+ max_growth = self.growth_values_dict[item_name][1]
+ if growthLevel > 4:
+ growthLevel = 4
+ current_growth_level = self.kh2_read_short(self.Save + slot)
+ ability = current_growth_level & 0x0FFF
+
+ # if the player should be getting a growth ability
+ if ability | 0x8000 != 0x8000 + min_growth - 1 + growthLevel:
+ # if it should be level one of that growth
+ if 0x8000 + min_growth - 1 + growthLevel <= 0x8000 + min_growth or ability < min_growth:
+ self.kh2_write_short(self.Save + slot, min_growth)
+ # if it is already in the inventory
+ elif ability | 0x8000 < (0x8000 + max_growth):
+ self.kh2_write_short(self.Save + slot, current_growth_level + 1)
+
+ for item_name in master_bitmask:
+ item_data = self.item_name_to_data[item_name]
+ itemMemory = self.kh2_read_byte(self.Save + item_data.memaddr)
+ if self.kh2_read_byte(self.Save + item_data.memaddr) & 0x1 << item_data.bitmask == 0:
+ # when getting a form anti points should be reset to 0 but bit-shift doesn't trigger the game.
+ if item_name in {"Valor Form", "Wisdom Form", "Limit Form", "Master Form", "Final Form"}:
+ self.kh2_write_byte(self.Save + 0x3410, 0)
+ self.kh2_write_byte(self.Save + item_data.memaddr, itemMemory | 0x01 << item_data.bitmask)
+
+ for item_name in master_equipment:
+ item_data = self.item_name_to_data[item_name]
+ is_there = False
+ if item_name in self.accessories_set:
+ Equipment_Anchor_List = self.Equipment_Anchor_Dict["Accessories"]
+ else:
+ Equipment_Anchor_List = self.Equipment_Anchor_Dict["Armor"]
+ # Checking form anchors for the equipment
+ for slot in Equipment_Anchor_List:
+ if self.kh2_read_short(self.Save + slot) == item_data.kh2id:
+ is_there = True
+ if self.kh2_read_byte(self.Save + item_data.memaddr) != 0:
+ self.kh2_write_byte(self.Save + item_data.memaddr, 0)
+ break
+ if not is_there and item_name not in self.kh2_seed_save["SoldEquipment"]:
+ if self.kh2_read_byte(self.Save + item_data.memaddr) != 1:
+ self.kh2_write_byte(self.Save + item_data.memaddr, 1)
+
+ for item_name in master_magic:
+ item_data = self.item_name_to_data[item_name]
+ amount_of_items = 0
+ amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Magic"][item_name]
+ if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(self.Shop) in {10, 8}:
+ self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
+
+ for item_name in master_stat:
+ amount_of_items = 0
+ amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][item_name]
+ if self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5:
+ if item_name == ItemName.MaxHPUp:
+ if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical
+ Bonus = 5
+ else: # Critical
+ Bonus = 2
+ if self.kh2_read_int(self.Slot1 + 0x004) != self.base_hp + (Bonus * amount_of_items):
+ self.kh2_write_int(self.Slot1 + 0x004, self.base_hp + (Bonus * amount_of_items))
+
+ elif item_name == ItemName.MaxMPUp:
+ if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical
+ Bonus = 10
+ else: # Critical
+ Bonus = 5
+ if self.kh2_read_int(self.Slot1 + 0x184) != self.base_mp + (Bonus * amount_of_items):
+ self.kh2_write_int(self.Slot1 + 0x184, self.base_mp + (Bonus * amount_of_items))
+
+ elif item_name == ItemName.DriveGaugeUp:
+ current_max_drive = self.kh2_read_byte(self.Slot1 + 0x1B2)
+ # change when max drive is changed from 6 to 4
+ if current_max_drive < 9 and current_max_drive != self.base_drive + amount_of_items:
+ self.kh2_write_byte(self.Slot1 + 0x1B2, self.base_drive + amount_of_items)
+
+ elif item_name == ItemName.AccessorySlotUp:
+ current_accessory = self.kh2_read_byte(self.Save + 0x2501)
+ if current_accessory != self.base_accessory_slots + amount_of_items:
+ if 4 > current_accessory < self.base_accessory_slots + amount_of_items:
+ self.kh2_write_byte(self.Save + 0x2501, current_accessory + 1)
+ elif self.base_accessory_slots + amount_of_items < 4:
+ self.kh2_write_byte(self.Save + 0x2501, self.base_accessory_slots + amount_of_items)
+
+ elif item_name == ItemName.ArmorSlotUp:
+ current_armor_slots = self.kh2_read_byte(self.Save + 0x2500)
+ if current_armor_slots != self.base_armor_slots + amount_of_items:
+ if 4 > current_armor_slots < self.base_armor_slots + amount_of_items:
+ self.kh2_write_byte(self.Save + 0x2500, current_armor_slots + 1)
+ elif self.base_armor_slots + amount_of_items < 4:
+ self.kh2_write_byte(self.Save + 0x2500, self.base_armor_slots + amount_of_items)
+
+ elif item_name == ItemName.ItemSlotUp:
+ current_item_slots = self.kh2_read_byte(self.Save + 0x2502)
+ if current_item_slots != self.base_item_slots + amount_of_items:
+ if 8 > current_item_slots < self.base_item_slots + amount_of_items:
+ self.kh2_write_byte(self.Save + 0x2502, current_item_slots + 1)
+ elif self.base_item_slots + amount_of_items < 8:
+ self.kh2_write_byte(self.Save + 0x2502, self.base_item_slots + amount_of_items)
+
+ # if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \
+ # and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \
+ # self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}:
+ # self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
+
+ if "PoptrackerVersionCheck" in self.kh2slotdata:
+ if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3
+ self.kh2_write_byte(self.Save + 0x3607, 1)
+
+ except Exception as e:
+ if self.kh2connected:
+ self.kh2connected = False
+ logger.info(e)
+ logger.info("line 840")
+
+
+def finishedGame(ctx: KH2Context, message):
+ if ctx.kh2slotdata['FinalXemnas'] == 1:
+ if not ctx.final_xemnas and ctx.kh2_read_byte(ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \
+ & 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0:
+ ctx.final_xemnas = True
+ # three proofs
+ if ctx.kh2slotdata['Goal'] == 0:
+ if ctx.kh2_read_byte(ctx.Save + 0x36B2) > 0 \
+ and ctx.kh2_read_byte(ctx.Save + 0x36B3) > 0 \
+ and ctx.kh2_read_byte(ctx.Save + 0x36B4) > 0:
+ if ctx.kh2slotdata['FinalXemnas'] == 1:
+ if ctx.final_xemnas:
+ return True
+ return False
+ return True
+ return False
+ elif ctx.kh2slotdata['Goal'] == 1:
+ if ctx.kh2_read_byte(ctx.Save + 0x3641) >= ctx.kh2slotdata['LuckyEmblemsRequired']:
+ if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1:
+ ctx.kh2_write_byte(ctx.Save + 0x36B2, 1)
+ ctx.kh2_write_byte(ctx.Save + 0x36B3, 1)
+ ctx.kh2_write_byte(ctx.Save + 0x36B4, 1)
+ logger.info("The Final Door is now Open")
+ if ctx.kh2slotdata['FinalXemnas'] == 1:
+ if ctx.final_xemnas:
+ return True
+ return False
+ return True
+ return False
+ elif ctx.kh2slotdata['Goal'] == 2:
+ # for backwards compat
+ if "hitlist" in ctx.kh2slotdata:
+ for boss in ctx.kh2slotdata["hitlist"]:
+ if boss in message[0]["locations"]:
+ ctx.hitlist_bounties += 1
+ if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"]["Bounty"] >= ctx.kh2slotdata["BountyRequired"]:
+ if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1:
+ ctx.kh2_write_byte(ctx.Save + 0x36B2, 1)
+ ctx.kh2_write_byte(ctx.Save + 0x36B3, 1)
+ ctx.kh2_write_byte(ctx.Save + 0x36B4, 1)
+ logger.info("The Final Door is now Open")
+ if ctx.kh2slotdata['FinalXemnas'] == 1:
+ if ctx.final_xemnas:
+ return True
+ return False
+ return True
+ return False
+ elif ctx.kh2slotdata["Goal"] == 3:
+ if ctx.kh2_seed_save_cache["AmountInvo"]["Amount"]["Bounty"] >= ctx.kh2slotdata["BountyRequired"] and \
+ ctx.kh2_read_byte(ctx.Save + 0x3641) >= ctx.kh2slotdata['LuckyEmblemsRequired']:
+ if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1:
+ ctx.kh2_write_byte(ctx.Save + 0x36B2, 1)
+ ctx.kh2_write_byte(ctx.Save + 0x36B3, 1)
+ ctx.kh2_write_byte(ctx.Save + 0x36B4, 1)
+ logger.info("The Final Door is now Open")
+ if ctx.kh2slotdata['FinalXemnas'] == 1:
+ if ctx.final_xemnas:
+ return True
+ return False
+ return True
+ return False
+
+
+async def kh2_watcher(ctx: KH2Context):
+ while not ctx.exit_event.is_set():
+ try:
+ if ctx.kh2connected and ctx.serverconneced:
+ ctx.sending = []
+ await asyncio.create_task(ctx.checkWorldLocations())
+ await asyncio.create_task(ctx.checkLevels())
+ await asyncio.create_task(ctx.checkSlots())
+ await asyncio.create_task(ctx.verifyChests())
+ await asyncio.create_task(ctx.verifyItems())
+ await asyncio.create_task(ctx.verifyLevel())
+ message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
+ if finishedGame(ctx, message) and not ctx.kh2_finished_game:
+ await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
+ ctx.kh2_finished_game = True
+ await ctx.send_msgs(message)
+ elif not ctx.kh2connected and ctx.serverconneced:
+ logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.")
+ ctx.kh2 = None
+ while not ctx.kh2connected and ctx.serverconneced:
+ await asyncio.sleep(15)
+ ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
+ if ctx.kh2 is not None:
+ if ctx.kh2_game_version is None:
+ if ctx.kh2_read_string(0x09A9830, 4) == "KH2J":
+ ctx.kh2_game_version = "STEAM"
+ ctx.Now = 0x0717008
+ ctx.Save = 0x09A9830
+ ctx.Slot1 = 0x2A23518
+ ctx.Journal = 0x7434E0
+ ctx.Shop = 0x7435D0
+
+ elif ctx.kh2_read_string(0x09A92F0, 4) == "KH2J":
+ ctx.kh2_game_version = "EGS"
+ else:
+ ctx.kh2_game_version = None
+ logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.")
+ if ctx.kh2_game_version is not None:
+ logger.info(f"You are now auto-tracking {ctx.kh2_game_version}")
+ ctx.kh2connected = True
+ except Exception as e:
+ if ctx.kh2connected:
+ ctx.kh2connected = False
+ logger.info(e)
+ logger.info("line 940")
+ await asyncio.sleep(0.5)
+
+
+def launch():
+ async def main(args):
+ ctx = KH2Context(args.connect, args.password)
+ ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
+ if gui_enabled:
+ ctx.run_gui()
+ ctx.run_cli()
+ progression_watcher = asyncio.create_task(
+ kh2_watcher(ctx), name="KH2ProgressionWatcher")
+
+ await ctx.exit_event.wait()
+ ctx.server_address = None
+
+ await progression_watcher
+
+ await ctx.shutdown()
+
+ import colorama
+
+ parser = get_base_parser(description="KH2 Client, for text interfacing.")
+
+ args, rest = parser.parse_known_args()
+ colorama.init()
+ asyncio.run(main(args))
+ colorama.deinit()
diff --git a/worlds/kh2/Items.py b/worlds/kh2/Items.py
index aa0e326c3da7..cb3d7c8d85ed 100644
--- a/worlds/kh2/Items.py
+++ b/worlds/kh2/Items.py
@@ -2,354 +2,423 @@
from BaseClasses import Item
from .Names import ItemName
+from .Subclasses import ItemData
-
-class KH2Item(Item):
- game: str = "Kingdom Hearts 2"
-
-
-class ItemData(typing.NamedTuple):
- code: typing.Optional[int]
- quantity: int = 0
- kh2id: int = 0
- # Save+ mem addr
- memaddr: int = 0
- # some items have bitmasks. if bitmask>0 bitor to give item else
- bitmask: int = 0
- # if ability then
- ability: bool = False
-
-
+# 0x130000
Reports_Table = {
- ItemName.SecretAnsemsReport1: ItemData(0x130000, 1, 226, 0x36C4, 6),
- ItemName.SecretAnsemsReport2: ItemData(0x130001, 1, 227, 0x36C4, 7),
- ItemName.SecretAnsemsReport3: ItemData(0x130002, 1, 228, 0x36C5, 0),
- ItemName.SecretAnsemsReport4: ItemData(0x130003, 1, 229, 0x36C5, 1),
- ItemName.SecretAnsemsReport5: ItemData(0x130004, 1, 230, 0x36C5, 2),
- ItemName.SecretAnsemsReport6: ItemData(0x130005, 1, 231, 0x36C5, 3),
- ItemName.SecretAnsemsReport7: ItemData(0x130006, 1, 232, 0x36C5, 4),
- ItemName.SecretAnsemsReport8: ItemData(0x130007, 1, 233, 0x36C5, 5),
- ItemName.SecretAnsemsReport9: ItemData(0x130008, 1, 234, 0x36C5, 6),
- ItemName.SecretAnsemsReport10: ItemData(0x130009, 1, 235, 0x36C5, 7),
- ItemName.SecretAnsemsReport11: ItemData(0x13000A, 1, 236, 0x36C6, 0),
- ItemName.SecretAnsemsReport12: ItemData(0x13000B, 1, 237, 0x36C6, 1),
- ItemName.SecretAnsemsReport13: ItemData(0x13000C, 1, 238, 0x36C6, 2),
+ ItemName.SecretAnsemsReport1: ItemData(1, 226, 0x36C4, 6),
+ ItemName.SecretAnsemsReport2: ItemData(1, 227, 0x36C4, 7),
+ ItemName.SecretAnsemsReport3: ItemData(1, 228, 0x36C5, 0),
+ ItemName.SecretAnsemsReport4: ItemData(1, 229, 0x36C5, 1),
+ ItemName.SecretAnsemsReport5: ItemData(1, 230, 0x36C5, 2),
+ ItemName.SecretAnsemsReport6: ItemData(1, 231, 0x36C5, 3),
+ ItemName.SecretAnsemsReport7: ItemData(1, 232, 0x36C5, 4),
+ ItemName.SecretAnsemsReport8: ItemData(1, 233, 0x36C5, 5),
+ ItemName.SecretAnsemsReport9: ItemData(1, 234, 0x36C5, 6),
+ ItemName.SecretAnsemsReport10: ItemData(1, 235, 0x36C5, 7),
+ ItemName.SecretAnsemsReport11: ItemData(1, 236, 0x36C6, 0),
+ ItemName.SecretAnsemsReport12: ItemData(1, 237, 0x36C6, 1),
+ ItemName.SecretAnsemsReport13: ItemData(1, 238, 0x36C6, 2),
}
Progression_Table = {
- ItemName.ProofofConnection: ItemData(0x13000D, 1, 593, 0x36B2),
- ItemName.ProofofNonexistence: ItemData(0x13000E, 1, 594, 0x36B3),
- ItemName.ProofofPeace: ItemData(0x13000F, 1, 595, 0x36B4),
- ItemName.PromiseCharm: ItemData(0x130010, 1, 524, 0x3694),
- ItemName.NamineSketches: ItemData(0x130011, 1, 368, 0x3642),
- ItemName.CastleKey: ItemData(0x130012, 2, 460, 0x365D), # dummy 13
- ItemName.BattlefieldsofWar: ItemData(0x130013, 2, 54, 0x35AE),
- ItemName.SwordoftheAncestor: ItemData(0x130014, 2, 55, 0x35AF),
- ItemName.BeastsClaw: ItemData(0x130015, 2, 59, 0x35B3),
- ItemName.BoneFist: ItemData(0x130016, 2, 60, 0x35B4),
- ItemName.ProudFang: ItemData(0x130017, 2, 61, 0x35B5),
- ItemName.SkillandCrossbones: ItemData(0x130018, 2, 62, 0x35B6),
- ItemName.Scimitar: ItemData(0x130019, 2, 72, 0x35C0),
- ItemName.MembershipCard: ItemData(0x13001A, 2, 369, 0x3643),
- ItemName.IceCream: ItemData(0x13001B, 3, 375, 0x3649),
+ ItemName.ProofofConnection: ItemData(1, 593, 0x36B2),
+ ItemName.ProofofNonexistence: ItemData(1, 594, 0x36B3),
+ ItemName.ProofofPeace: ItemData(1, 595, 0x36B4),
+ ItemName.PromiseCharm: ItemData(1, 524, 0x3694),
+ ItemName.NamineSketches: ItemData(1, 368, 0x3642),
+ ItemName.CastleKey: ItemData(2, 460, 0x365D), # dummy 13
+ ItemName.BattlefieldsofWar: ItemData(2, 54, 0x35AE),
+ ItemName.SwordoftheAncestor: ItemData(2, 55, 0x35AF),
+ ItemName.BeastsClaw: ItemData(2, 59, 0x35B3),
+ ItemName.BoneFist: ItemData(2, 60, 0x35B4),
+ ItemName.ProudFang: ItemData(2, 61, 0x35B5),
+ ItemName.SkillandCrossbones: ItemData(2, 62, 0x35B6),
+ ItemName.Scimitar: ItemData(2, 72, 0x35C0),
+ ItemName.MembershipCard: ItemData(2, 369, 0x3643),
+ ItemName.IceCream: ItemData(3, 375, 0x3649),
# Changed to 3 instead of one poster, picture and ice cream respectively
- ItemName.WaytotheDawn: ItemData(0x13001C, 1, 73, 0x35C1),
+ ItemName.WaytotheDawn: ItemData(2, 73, 0x35C1),
# currently first visit locking doesn't work for twtnw.When goa is updated should be 2
- ItemName.IdentityDisk: ItemData(0x13001D, 2, 74, 0x35C2),
- ItemName.TornPages: ItemData(0x13001E, 5, 32, 0x3598),
+ ItemName.IdentityDisk: ItemData(2, 74, 0x35C2),
+ ItemName.TornPages: ItemData(5, 32, 0x3598),
}
Forms_Table = {
- ItemName.ValorForm: ItemData(0x13001F, 1, 26, 0x36C0, 1),
- ItemName.WisdomForm: ItemData(0x130020, 1, 27, 0x36C0, 2),
- ItemName.LimitForm: ItemData(0x130021, 1, 563, 0x36CA, 3),
- ItemName.MasterForm: ItemData(0x130022, 1, 31, 0x36C0, 6),
- ItemName.FinalForm: ItemData(0x130023, 1, 29, 0x36C0, 4),
+ ItemName.ValorForm: ItemData(1, 26, 0x36C0, 1),
+ ItemName.WisdomForm: ItemData(1, 27, 0x36C0, 2),
+ ItemName.LimitForm: ItemData(1, 563, 0x36CA, 3),
+ ItemName.MasterForm: ItemData(1, 31, 0x36C0, 6),
+ ItemName.FinalForm: ItemData(1, 29, 0x36C0, 4),
+ ItemName.AntiForm: ItemData(1, 30, 0x36C0, 5)
}
Magic_Table = {
- ItemName.FireElement: ItemData(0x130024, 3, 21, 0x3594),
- ItemName.BlizzardElement: ItemData(0x130025, 3, 22, 0x3595),
- ItemName.ThunderElement: ItemData(0x130026, 3, 23, 0x3596),
- ItemName.CureElement: ItemData(0x130027, 3, 24, 0x3597),
- ItemName.MagnetElement: ItemData(0x130028, 3, 87, 0x35CF),
- ItemName.ReflectElement: ItemData(0x130029, 3, 88, 0x35D0),
- ItemName.Genie: ItemData(0x13002A, 1, 159, 0x36C4, 4),
- ItemName.PeterPan: ItemData(0x13002B, 1, 160, 0x36C4, 5),
- ItemName.Stitch: ItemData(0x13002C, 1, 25, 0x36C0, 0),
- ItemName.ChickenLittle: ItemData(0x13002D, 1, 383, 0x36C0, 3),
+ ItemName.FireElement: ItemData(3, 21, 0x3594),
+ ItemName.BlizzardElement: ItemData(3, 22, 0x3595),
+ ItemName.ThunderElement: ItemData(3, 23, 0x3596),
+ ItemName.CureElement: ItemData(3, 24, 0x3597),
+ ItemName.MagnetElement: ItemData(3, 87, 0x35CF),
+ ItemName.ReflectElement: ItemData(3, 88, 0x35D0),
+}
+Summon_Table = {
+ ItemName.Genie: ItemData(1, 159, 0x36C4, 4),
+ ItemName.PeterPan: ItemData(1, 160, 0x36C4, 5),
+ ItemName.Stitch: ItemData(1, 25, 0x36C0, 0),
+ ItemName.ChickenLittle: ItemData(1, 383, 0x36C0, 3),
}
-
Movement_Table = {
- ItemName.HighJump: ItemData(0x13002E, 4, 94, 0x05E, 0, True),
- ItemName.QuickRun: ItemData(0x13002F, 4, 98, 0x062, 0, True),
- ItemName.DodgeRoll: ItemData(0x130030, 4, 564, 0x234, 0, True),
- ItemName.AerialDodge: ItemData(0x130031, 4, 102, 0x066, 0, True),
- ItemName.Glide: ItemData(0x130032, 4, 106, 0x06A, 0, True),
+ ItemName.HighJump: ItemData(4, 94, 0x05E, ability=True),
+ ItemName.QuickRun: ItemData(4, 98, 0x062, ability=True),
+ ItemName.DodgeRoll: ItemData(4, 564, 0x234, ability=True),
+ ItemName.AerialDodge: ItemData(4, 102, 0x066, ability=True),
+ ItemName.Glide: ItemData(4, 106, 0x06A, ability=True),
}
Keyblade_Table = {
- ItemName.Oathkeeper: ItemData(0x130033, 1, 42, 0x35A2),
- ItemName.Oblivion: ItemData(0x130034, 1, 43, 0x35A3),
- ItemName.StarSeeker: ItemData(0x130035, 1, 480, 0x367B),
- ItemName.HiddenDragon: ItemData(0x130036, 1, 481, 0x367C),
- ItemName.HerosCrest: ItemData(0x130037, 1, 484, 0x367F),
- ItemName.Monochrome: ItemData(0x130038, 1, 485, 0x3680),
- ItemName.FollowtheWind: ItemData(0x130039, 1, 486, 0x3681),
- ItemName.CircleofLife: ItemData(0x13003A, 1, 487, 0x3682),
- ItemName.PhotonDebugger: ItemData(0x13003B, 1, 488, 0x3683),
- ItemName.GullWing: ItemData(0x13003C, 1, 489, 0x3684),
- ItemName.RumblingRose: ItemData(0x13003D, 1, 490, 0x3685),
- ItemName.GuardianSoul: ItemData(0x13003E, 1, 491, 0x3686),
- ItemName.WishingLamp: ItemData(0x13003F, 1, 492, 0x3687),
- ItemName.DecisivePumpkin: ItemData(0x130040, 1, 493, 0x3688),
- ItemName.SleepingLion: ItemData(0x130041, 1, 494, 0x3689),
- ItemName.SweetMemories: ItemData(0x130042, 1, 495, 0x368A),
- ItemName.MysteriousAbyss: ItemData(0x130043, 1, 496, 0x368B),
- ItemName.TwoBecomeOne: ItemData(0x130044, 1, 543, 0x3698),
- ItemName.FatalCrest: ItemData(0x130045, 1, 497, 0x368C),
- ItemName.BondofFlame: ItemData(0x130046, 1, 498, 0x368D),
- ItemName.Fenrir: ItemData(0x130047, 1, 499, 0x368E),
- ItemName.UltimaWeapon: ItemData(0x130048, 1, 500, 0x368F),
- ItemName.WinnersProof: ItemData(0x130049, 1, 544, 0x3699),
- ItemName.Pureblood: ItemData(0x13004A, 1, 71, 0x35BF),
+ ItemName.Oathkeeper: ItemData(1, 42, 0x35A2),
+ ItemName.Oblivion: ItemData(1, 43, 0x35A3),
+ ItemName.StarSeeker: ItemData(1, 480, 0x367B),
+ ItemName.HiddenDragon: ItemData(1, 481, 0x367C),
+ ItemName.HerosCrest: ItemData(1, 484, 0x367F),
+ ItemName.Monochrome: ItemData(1, 485, 0x3680),
+ ItemName.FollowtheWind: ItemData(1, 486, 0x3681),
+ ItemName.CircleofLife: ItemData(1, 487, 0x3682),
+ ItemName.PhotonDebugger: ItemData(1, 488, 0x3683),
+ ItemName.GullWing: ItemData(1, 489, 0x3684),
+ ItemName.RumblingRose: ItemData(1, 490, 0x3685),
+ ItemName.GuardianSoul: ItemData(1, 491, 0x3686),
+ ItemName.WishingLamp: ItemData(1, 492, 0x3687),
+ ItemName.DecisivePumpkin: ItemData(1, 493, 0x3688),
+ ItemName.SleepingLion: ItemData(1, 494, 0x3689),
+ ItemName.SweetMemories: ItemData(1, 495, 0x368A),
+ ItemName.MysteriousAbyss: ItemData(1, 496, 0x368B),
+ ItemName.TwoBecomeOne: ItemData(1, 543, 0x3698),
+ ItemName.FatalCrest: ItemData(1, 497, 0x368C),
+ ItemName.BondofFlame: ItemData(1, 498, 0x368D),
+ ItemName.Fenrir: ItemData(1, 499, 0x368E),
+ ItemName.UltimaWeapon: ItemData(1, 500, 0x368F),
+ ItemName.WinnersProof: ItemData(1, 544, 0x3699),
+ ItemName.Pureblood: ItemData(1, 71, 0x35BF),
}
Staffs_Table = {
- ItemName.Centurion2: ItemData(0x13004B, 1, 546, 0x369B),
- ItemName.MeteorStaff: ItemData(0x13004C, 1, 150, 0x35F1),
- ItemName.NobodyLance: ItemData(0x13004D, 1, 155, 0x35F6),
- ItemName.PreciousMushroom: ItemData(0x13004E, 1, 549, 0x369E),
- ItemName.PreciousMushroom2: ItemData(0x13004F, 1, 550, 0x369F),
- ItemName.PremiumMushroom: ItemData(0x130050, 1, 551, 0x36A0),
- ItemName.RisingDragon: ItemData(0x130051, 1, 154, 0x35F5),
- ItemName.SaveTheQueen2: ItemData(0x130052, 1, 503, 0x3692),
- ItemName.ShamansRelic: ItemData(0x130053, 1, 156, 0x35F7),
+ ItemName.Centurion2: ItemData(1, 546, 0x369B),
+ ItemName.MeteorStaff: ItemData(1, 150, 0x35F1),
+ ItemName.NobodyLance: ItemData(1, 155, 0x35F6),
+ ItemName.PreciousMushroom: ItemData(1, 549, 0x369E),
+ ItemName.PreciousMushroom2: ItemData(1, 550, 0x369F),
+ ItemName.PremiumMushroom: ItemData(1, 551, 0x36A0),
+ ItemName.RisingDragon: ItemData(1, 154, 0x35F5),
+ ItemName.SaveTheQueen2: ItemData(1, 503, 0x3692),
+ ItemName.ShamansRelic: ItemData(1, 156, 0x35F7),
}
Shields_Table = {
- ItemName.AkashicRecord: ItemData(0x130054, 1, 146, 0x35ED),
- ItemName.FrozenPride2: ItemData(0x130055, 1, 553, 0x36A2),
- ItemName.GenjiShield: ItemData(0x130056, 1, 145, 0x35EC),
- ItemName.MajesticMushroom: ItemData(0x130057, 1, 556, 0x36A5),
- ItemName.MajesticMushroom2: ItemData(0x130058, 1, 557, 0x36A6),
- ItemName.NobodyGuard: ItemData(0x130059, 1, 147, 0x35EE),
- ItemName.OgreShield: ItemData(0x13005A, 1, 141, 0x35E8),
- ItemName.SaveTheKing2: ItemData(0x13005B, 1, 504, 0x3693),
- ItemName.UltimateMushroom: ItemData(0x13005C, 1, 558, 0x36A7),
+ ItemName.AkashicRecord: ItemData(1, 146, 0x35ED),
+ ItemName.FrozenPride2: ItemData(1, 553, 0x36A2),
+ ItemName.GenjiShield: ItemData(1, 145, 0x35EC),
+ ItemName.MajesticMushroom: ItemData(1, 556, 0x36A5),
+ ItemName.MajesticMushroom2: ItemData(1, 557, 0x36A6),
+ ItemName.NobodyGuard: ItemData(1, 147, 0x35EE),
+ ItemName.OgreShield: ItemData(1, 141, 0x35E8),
+ ItemName.SaveTheKing2: ItemData(1, 504, 0x3693),
+ ItemName.UltimateMushroom: ItemData(1, 558, 0x36A7),
}
Accessory_Table = {
- ItemName.AbilityRing: ItemData(0x13005D, 1, 8, 0x3587),
- ItemName.EngineersRing: ItemData(0x13005E, 1, 9, 0x3588),
- ItemName.TechniciansRing: ItemData(0x13005F, 1, 10, 0x3589),
- ItemName.SkillRing: ItemData(0x130060, 1, 38, 0x359F),
- ItemName.SkillfulRing: ItemData(0x130061, 1, 39, 0x35A0),
- ItemName.ExpertsRing: ItemData(0x130062, 1, 11, 0x358A),
- ItemName.MastersRing: ItemData(0x130063, 1, 34, 0x359B),
- ItemName.CosmicRing: ItemData(0x130064, 1, 52, 0x35AD),
- ItemName.ExecutivesRing: ItemData(0x130065, 1, 599, 0x36B5),
- ItemName.SardonyxRing: ItemData(0x130066, 1, 12, 0x358B),
- ItemName.TourmalineRing: ItemData(0x130067, 1, 13, 0x358C),
- ItemName.AquamarineRing: ItemData(0x130068, 1, 14, 0x358D),
- ItemName.GarnetRing: ItemData(0x130069, 1, 15, 0x358E),
- ItemName.DiamondRing: ItemData(0x13006A, 1, 16, 0x358F),
- ItemName.SilverRing: ItemData(0x13006B, 1, 17, 0x3590),
- ItemName.GoldRing: ItemData(0x13006C, 1, 18, 0x3591),
- ItemName.PlatinumRing: ItemData(0x13006D, 1, 19, 0x3592),
- ItemName.MythrilRing: ItemData(0x13006E, 1, 20, 0x3593),
- ItemName.OrichalcumRing: ItemData(0x13006F, 1, 28, 0x359A),
- ItemName.SoldierEarring: ItemData(0x130070, 1, 40, 0x35A6),
- ItemName.FencerEarring: ItemData(0x130071, 1, 46, 0x35A7),
- ItemName.MageEarring: ItemData(0x130072, 1, 47, 0x35A8),
- ItemName.SlayerEarring: ItemData(0x130073, 1, 48, 0x35AC),
- ItemName.Medal: ItemData(0x130074, 1, 53, 0x35B0),
- ItemName.MoonAmulet: ItemData(0x130075, 1, 35, 0x359C),
- ItemName.StarCharm: ItemData(0x130076, 1, 36, 0x359E),
- ItemName.CosmicArts: ItemData(0x130077, 1, 56, 0x35B1),
- ItemName.ShadowArchive: ItemData(0x130078, 1, 57, 0x35B2),
- ItemName.ShadowArchive2: ItemData(0x130079, 1, 58, 0x35B7),
- ItemName.FullBloom: ItemData(0x13007A, 1, 64, 0x35B9),
- ItemName.FullBloom2: ItemData(0x13007B, 1, 66, 0x35BB),
- ItemName.DrawRing: ItemData(0x13007C, 1, 65, 0x35BA),
- ItemName.LuckyRing: ItemData(0x13007D, 1, 63, 0x35B8),
+ ItemName.AbilityRing: ItemData(1, 8, 0x3587),
+ ItemName.EngineersRing: ItemData(1, 9, 0x3588),
+ ItemName.TechniciansRing: ItemData(1, 10, 0x3589),
+ ItemName.SkillRing: ItemData(1, 38, 0x359F),
+ ItemName.SkillfulRing: ItemData(1, 39, 0x35A0),
+ ItemName.ExpertsRing: ItemData(1, 11, 0x358A),
+ ItemName.MastersRing: ItemData(1, 34, 0x359B),
+ ItemName.CosmicRing: ItemData(1, 52, 0x35AD),
+ ItemName.ExecutivesRing: ItemData(1, 599, 0x36B5),
+ ItemName.SardonyxRing: ItemData(1, 12, 0x358B),
+ ItemName.TourmalineRing: ItemData(1, 13, 0x358C),
+ ItemName.AquamarineRing: ItemData(1, 14, 0x358D),
+ ItemName.GarnetRing: ItemData(1, 15, 0x358E),
+ ItemName.DiamondRing: ItemData(1, 16, 0x358F),
+ ItemName.SilverRing: ItemData(1, 17, 0x3590),
+ ItemName.GoldRing: ItemData(1, 18, 0x3591),
+ ItemName.PlatinumRing: ItemData(1, 19, 0x3592),
+ ItemName.MythrilRing: ItemData(1, 20, 0x3593),
+ ItemName.OrichalcumRing: ItemData(1, 28, 0x359A),
+ ItemName.SoldierEarring: ItemData(1, 40, 0x35A6),
+ ItemName.FencerEarring: ItemData(1, 46, 0x35A7),
+ ItemName.MageEarring: ItemData(1, 47, 0x35A8),
+ ItemName.SlayerEarring: ItemData(1, 48, 0x35AC),
+ ItemName.Medal: ItemData(1, 53, 0x35B0),
+ ItemName.MoonAmulet: ItemData(1, 35, 0x359C),
+ ItemName.StarCharm: ItemData(1, 36, 0x359E),
+ ItemName.CosmicArts: ItemData(1, 56, 0x35B1),
+ ItemName.ShadowArchive: ItemData(1, 57, 0x35B2),
+ ItemName.ShadowArchive2: ItemData(1, 58, 0x35B7),
+ ItemName.FullBloom: ItemData(1, 64, 0x35B9),
+ ItemName.FullBloom2: ItemData(1, 66, 0x35BB),
+ ItemName.DrawRing: ItemData(1, 65, 0x35BA),
+ ItemName.LuckyRing: ItemData(1, 63, 0x35B8),
}
Armor_Table = {
- ItemName.ElvenBandana: ItemData(0x13007E, 1, 67, 0x35BC),
- ItemName.DivineBandana: ItemData(0x13007F, 1, 68, 0x35BD),
- ItemName.ProtectBelt: ItemData(0x130080, 1, 78, 0x35C7),
- ItemName.GaiaBelt: ItemData(0x130081, 1, 79, 0x35CA),
- ItemName.PowerBand: ItemData(0x130082, 1, 69, 0x35BE),
- ItemName.BusterBand: ItemData(0x130083, 1, 70, 0x35C6),
- ItemName.CosmicBelt: ItemData(0x130084, 1, 111, 0x35D1),
- ItemName.FireBangle: ItemData(0x130085, 1, 173, 0x35D7),
- ItemName.FiraBangle: ItemData(0x130086, 1, 174, 0x35D8),
- ItemName.FiragaBangle: ItemData(0x130087, 1, 197, 0x35D9),
- ItemName.FiragunBangle: ItemData(0x130088, 1, 284, 0x35DA),
- ItemName.BlizzardArmlet: ItemData(0x130089, 1, 286, 0x35DC),
- ItemName.BlizzaraArmlet: ItemData(0x13008A, 1, 287, 0x35DD),
- ItemName.BlizzagaArmlet: ItemData(0x13008B, 1, 288, 0x35DE),
- ItemName.BlizzagunArmlet: ItemData(0x13008C, 1, 289, 0x35DF),
- ItemName.ThunderTrinket: ItemData(0x13008D, 1, 291, 0x35E2),
- ItemName.ThundaraTrinket: ItemData(0x13008E, 1, 292, 0x35E3),
- ItemName.ThundagaTrinket: ItemData(0x13008F, 1, 293, 0x35E4),
- ItemName.ThundagunTrinket: ItemData(0x130090, 1, 294, 0x35E5),
- ItemName.ShockCharm: ItemData(0x130091, 1, 132, 0x35D2),
- ItemName.ShockCharm2: ItemData(0x130092, 1, 133, 0x35D3),
- ItemName.ShadowAnklet: ItemData(0x130093, 1, 296, 0x35F9),
- ItemName.DarkAnklet: ItemData(0x130094, 1, 297, 0x35FB),
- ItemName.MidnightAnklet: ItemData(0x130095, 1, 298, 0x35FC),
- ItemName.ChaosAnklet: ItemData(0x130096, 1, 299, 0x35FD),
- ItemName.ChampionBelt: ItemData(0x130097, 1, 305, 0x3603),
- ItemName.AbasChain: ItemData(0x130098, 1, 301, 0x35FF),
- ItemName.AegisChain: ItemData(0x130099, 1, 302, 0x3600),
- ItemName.Acrisius: ItemData(0x13009A, 1, 303, 0x3601),
- ItemName.Acrisius2: ItemData(0x13009B, 1, 307, 0x3605),
- ItemName.CosmicChain: ItemData(0x13009C, 1, 308, 0x3606),
- ItemName.PetiteRibbon: ItemData(0x13009D, 1, 306, 0x3604),
- ItemName.Ribbon: ItemData(0x13009E, 1, 304, 0x3602),
- ItemName.GrandRibbon: ItemData(0x13009F, 1, 157, 0x35D4),
+ ItemName.ElvenBandana: ItemData(1, 67, 0x35BC),
+ ItemName.DivineBandana: ItemData(1, 68, 0x35BD),
+ ItemName.ProtectBelt: ItemData(1, 78, 0x35C7),
+ ItemName.GaiaBelt: ItemData(1, 79, 0x35CA),
+ ItemName.PowerBand: ItemData(1, 69, 0x35BE),
+ ItemName.BusterBand: ItemData(1, 70, 0x35C6),
+ ItemName.CosmicBelt: ItemData(1, 111, 0x35D1),
+ ItemName.FireBangle: ItemData(1, 173, 0x35D7),
+ ItemName.FiraBangle: ItemData(1, 174, 0x35D8),
+ ItemName.FiragaBangle: ItemData(1, 197, 0x35D9),
+ ItemName.FiragunBangle: ItemData(1, 284, 0x35DA),
+ ItemName.BlizzardArmlet: ItemData(1, 286, 0x35DC),
+ ItemName.BlizzaraArmlet: ItemData(1, 287, 0x35DD),
+ ItemName.BlizzagaArmlet: ItemData(1, 288, 0x35DE),
+ ItemName.BlizzagunArmlet: ItemData(1, 289, 0x35DF),
+ ItemName.ThunderTrinket: ItemData(1, 291, 0x35E2),
+ ItemName.ThundaraTrinket: ItemData(1, 292, 0x35E3),
+ ItemName.ThundagaTrinket: ItemData(1, 293, 0x35E4),
+ ItemName.ThundagunTrinket: ItemData(1, 294, 0x35E5),
+ ItemName.ShockCharm: ItemData(1, 132, 0x35D2),
+ ItemName.ShockCharm2: ItemData(1, 133, 0x35D3),
+ ItemName.ShadowAnklet: ItemData(1, 296, 0x35F9),
+ ItemName.DarkAnklet: ItemData(1, 297, 0x35FB),
+ ItemName.MidnightAnklet: ItemData(1, 298, 0x35FC),
+ ItemName.ChaosAnklet: ItemData(1, 299, 0x35FD),
+ ItemName.ChampionBelt: ItemData(1, 305, 0x3603),
+ ItemName.AbasChain: ItemData(1, 301, 0x35FF),
+ ItemName.AegisChain: ItemData(1, 302, 0x3600),
+ ItemName.Acrisius: ItemData(1, 303, 0x3601),
+ ItemName.Acrisius2: ItemData(1, 307, 0x3605),
+ ItemName.CosmicChain: ItemData(1, 308, 0x3606),
+ ItemName.PetiteRibbon: ItemData(1, 306, 0x3604),
+ ItemName.Ribbon: ItemData(1, 304, 0x3602),
+ ItemName.GrandRibbon: ItemData(1, 157, 0x35D4),
}
Usefull_Table = {
- ItemName.MickyMunnyPouch: ItemData(0x1300A0, 3, 535, 0x3695), # 5000 munny per
- ItemName.OletteMunnyPouch: ItemData(0x1300A1, 6, 362, 0x363C), # 2500 munny per
- ItemName.HadesCupTrophy: ItemData(0x1300A2, 1, 537, 0x3696),
- ItemName.UnknownDisk: ItemData(0x1300A3, 1, 462, 0x365F),
- ItemName.OlympusStone: ItemData(0x1300A4, 1, 370, 0x3644),
- ItemName.MaxHPUp: ItemData(0x1300A5, 20, 470, 0x3671),
- ItemName.MaxMPUp: ItemData(0x1300A6, 4, 471, 0x3672),
- ItemName.DriveGaugeUp: ItemData(0x1300A7, 6, 472, 0x3673),
- ItemName.ArmorSlotUp: ItemData(0x1300A8, 3, 473, 0x3674),
- ItemName.AccessorySlotUp: ItemData(0x1300A9, 3, 474, 0x3675),
- ItemName.ItemSlotUp: ItemData(0x1300AA, 5, 463, 0x3660),
+ ItemName.MickeyMunnyPouch: ItemData(1, 535, 0x3695), # 5000 munny per
+ ItemName.OletteMunnyPouch: ItemData(2, 362, 0x363C), # 2500 munny per
+ ItemName.HadesCupTrophy: ItemData(1, 537, 0x3696),
+ ItemName.UnknownDisk: ItemData(1, 462, 0x365F),
+ ItemName.OlympusStone: ItemData(1, 370, 0x3644),
+ ItemName.MaxHPUp: ItemData(20, 112, 0x3671), # 470 is DUMMY 23, 112 is Encampment Area Map
+ ItemName.MaxMPUp: ItemData(4, 113, 0x3672), # 471 is DUMMY 24, 113 is Village Area Map
+ ItemName.DriveGaugeUp: ItemData(6, 114, 0x3673), # 472 is DUMMY 25, 114 is Cornerstone Hill Map
+ ItemName.ArmorSlotUp: ItemData(3, 116, 0x3674), # 473 is DUMMY 26, 116 is Lilliput Map
+ ItemName.AccessorySlotUp: ItemData(3, 117, 0x3675), # 474 is DUMMY 27, 117 is Building Site Map
+ ItemName.ItemSlotUp: ItemData(5, 118, 0x3660), # 463 is DUMMY 16, 118 is Mickey’s House Map
}
SupportAbility_Table = {
- ItemName.Scan: ItemData(0x1300AB, 2, 138, 0x08A, 0, True),
- ItemName.AerialRecovery: ItemData(0x1300AC, 1, 158, 0x09E, 0, True),
- ItemName.ComboMaster: ItemData(0x1300AD, 1, 539, 0x21B, 0, True),
- ItemName.ComboPlus: ItemData(0x1300AE, 3, 162, 0x0A2, 0, True),
- ItemName.AirComboPlus: ItemData(0x1300AF, 3, 163, 0x0A3, 0, True),
- ItemName.ComboBoost: ItemData(0x1300B0, 2, 390, 0x186, 0, True),
- ItemName.AirComboBoost: ItemData(0x1300B1, 2, 391, 0x187, 0, True),
- ItemName.ReactionBoost: ItemData(0x1300B2, 3, 392, 0x188, 0, True),
- ItemName.FinishingPlus: ItemData(0x1300B3, 3, 393, 0x189, 0, True),
- ItemName.NegativeCombo: ItemData(0x1300B4, 2, 394, 0x18A, 0, True),
- ItemName.BerserkCharge: ItemData(0x1300B5, 2, 395, 0x18B, 0, True),
- ItemName.DamageDrive: ItemData(0x1300B6, 2, 396, 0x18C, 0, True),
- ItemName.DriveBoost: ItemData(0x1300B7, 2, 397, 0x18D, 0, True),
- ItemName.FormBoost: ItemData(0x1300B8, 3, 398, 0x18E, 0, True),
- ItemName.SummonBoost: ItemData(0x1300B9, 1, 399, 0x18F, 0, True),
- ItemName.ExperienceBoost: ItemData(0x1300BA, 2, 401, 0x191, 0, True),
- ItemName.Draw: ItemData(0x1300BB, 4, 405, 0x195, 0, True),
- ItemName.Jackpot: ItemData(0x1300BC, 2, 406, 0x196, 0, True),
- ItemName.LuckyLucky: ItemData(0x1300BD, 3, 407, 0x197, 0, True),
- ItemName.DriveConverter: ItemData(0x1300BE, 2, 540, 0x21C, 0, True),
- ItemName.FireBoost: ItemData(0x1300BF, 2, 408, 0x198, 0, True),
- ItemName.BlizzardBoost: ItemData(0x1300C0, 2, 409, 0x199, 0, True),
- ItemName.ThunderBoost: ItemData(0x1300C1, 2, 410, 0x19A, 0, True),
- ItemName.ItemBoost: ItemData(0x1300C2, 2, 411, 0x19B, 0, True),
- ItemName.MPRage: ItemData(0x1300C3, 2, 412, 0x19C, 0, True),
- ItemName.MPHaste: ItemData(0x1300C4, 2, 413, 0x19D, 0, True),
- ItemName.MPHastera: ItemData(0x1300C5, 2, 421, 0x1A5, 0, True),
- ItemName.MPHastega: ItemData(0x1300C6, 1, 422, 0x1A6, 0, True),
- ItemName.Defender: ItemData(0x1300C7, 2, 414, 0x19E, 0, True),
- ItemName.DamageControl: ItemData(0x1300C8, 2, 542, 0x21E, 0, True),
- ItemName.NoExperience: ItemData(0x1300C9, 1, 404, 0x194, 0, True),
- ItemName.LightDarkness: ItemData(0x1300CA, 1, 541, 0x21D, 0, True),
- ItemName.MagicLock: ItemData(0x1300CB, 1, 403, 0x193, 0, True),
- ItemName.LeafBracer: ItemData(0x1300CC, 1, 402, 0x192, 0, True),
- ItemName.CombinationBoost: ItemData(0x1300CD, 1, 400, 0x190, 0, True),
- ItemName.OnceMore: ItemData(0x1300CE, 1, 416, 0x1A0, 0, True),
- ItemName.SecondChance: ItemData(0x1300CF, 1, 415, 0x19F, 0, True),
+ ItemName.Scan: ItemData(2, 138, 0x08A, ability=True),
+ ItemName.AerialRecovery: ItemData(1, 158, 0x09E, ability=True),
+ ItemName.ComboMaster: ItemData(1, 539, 0x21B, ability=True),
+ ItemName.ComboPlus: ItemData(3, 162, 0x0A2, ability=True),
+ ItemName.AirComboPlus: ItemData(3, 163, 0x0A3, ability=True),
+ ItemName.ComboBoost: ItemData(2, 390, 0x186, ability=True),
+ ItemName.AirComboBoost: ItemData(2, 391, 0x187, ability=True),
+ ItemName.ReactionBoost: ItemData(3, 392, 0x188, ability=True),
+ ItemName.FinishingPlus: ItemData(3, 393, 0x189, ability=True),
+ ItemName.NegativeCombo: ItemData(2, 394, 0x18A, ability=True),
+ ItemName.BerserkCharge: ItemData(2, 395, 0x18B, ability=True),
+ ItemName.DamageDrive: ItemData(2, 396, 0x18C, ability=True),
+ ItemName.DriveBoost: ItemData(2, 397, 0x18D, ability=True),
+ ItemName.FormBoost: ItemData(3, 398, 0x18E, ability=True),
+ ItemName.SummonBoost: ItemData(1, 399, 0x18F, ability=True),
+ ItemName.ExperienceBoost: ItemData(2, 401, 0x191, ability=True),
+ ItemName.Draw: ItemData(4, 405, 0x195, ability=True),
+ ItemName.Jackpot: ItemData(2, 406, 0x196, ability=True),
+ ItemName.LuckyLucky: ItemData(3, 407, 0x197, ability=True),
+ ItemName.DriveConverter: ItemData(2, 540, 0x21C, ability=True),
+ ItemName.FireBoost: ItemData(2, 408, 0x198, ability=True),
+ ItemName.BlizzardBoost: ItemData(2, 409, 0x199, ability=True),
+ ItemName.ThunderBoost: ItemData(2, 410, 0x19A, ability=True),
+ ItemName.ItemBoost: ItemData(2, 411, 0x19B, ability=True),
+ ItemName.MPRage: ItemData(2, 412, 0x19C, ability=True),
+ ItemName.MPHaste: ItemData(2, 413, 0x19D, ability=True),
+ ItemName.MPHastera: ItemData(2, 421, 0x1A5, ability=True),
+ ItemName.MPHastega: ItemData(1, 422, 0x1A6, ability=True),
+ ItemName.Defender: ItemData(2, 414, 0x19E, ability=True),
+ ItemName.DamageControl: ItemData(2, 542, 0x21E, ability=True),
+ ItemName.NoExperience: ItemData(0, 404, 0x194, ability=True), # quantity changed to 0 because the player starts with one always.
+ ItemName.LightDarkness: ItemData(1, 541, 0x21D, ability=True),
+ ItemName.MagicLock: ItemData(1, 403, 0x193, ability=True),
+ ItemName.LeafBracer: ItemData(1, 402, 0x192, ability=True),
+ ItemName.CombinationBoost: ItemData(1, 400, 0x190, ability=True),
+ ItemName.OnceMore: ItemData(1, 416, 0x1A0, ability=True),
+ ItemName.SecondChance: ItemData(1, 415, 0x19F, ability=True),
}
ActionAbility_Table = {
- ItemName.Guard: ItemData(0x1300D0, 1, 82, 0x052, 0, True),
- ItemName.UpperSlash: ItemData(0x1300D1, 1, 137, 0x089, 0, True),
- ItemName.HorizontalSlash: ItemData(0x1300D2, 1, 271, 0x10F, 0, True),
- ItemName.FinishingLeap: ItemData(0x1300D3, 1, 267, 0x10B, 0, True),
- ItemName.RetaliatingSlash: ItemData(0x1300D4, 1, 273, 0x111, 0, True),
- ItemName.Slapshot: ItemData(0x1300D5, 1, 262, 0x106, 0, True),
- ItemName.DodgeSlash: ItemData(0x1300D6, 1, 263, 0x107, 0, True),
- ItemName.FlashStep: ItemData(0x1300D7, 1, 559, 0x22F, 0, True),
- ItemName.SlideDash: ItemData(0x1300D8, 1, 264, 0x108, 0, True),
- ItemName.VicinityBreak: ItemData(0x1300D9, 1, 562, 0x232, 0, True),
- ItemName.GuardBreak: ItemData(0x1300DA, 1, 265, 0x109, 0, True),
- ItemName.Explosion: ItemData(0x1300DB, 1, 266, 0x10A, 0, True),
- ItemName.AerialSweep: ItemData(0x1300DC, 1, 269, 0x10D, 0, True),
- ItemName.AerialDive: ItemData(0x1300DD, 1, 560, 0x230, 0, True),
- ItemName.AerialSpiral: ItemData(0x1300DE, 1, 270, 0x10E, 0, True),
- ItemName.AerialFinish: ItemData(0x1300DF, 1, 272, 0x110, 0, True),
- ItemName.MagnetBurst: ItemData(0x1300E0, 1, 561, 0x231, 0, True),
- ItemName.Counterguard: ItemData(0x1300E1, 1, 268, 0x10C, 0, True),
- ItemName.AutoValor: ItemData(0x1300E2, 1, 385, 0x181, 0, True),
- ItemName.AutoWisdom: ItemData(0x1300E3, 1, 386, 0x182, 0, True),
- ItemName.AutoLimit: ItemData(0x1300E4, 1, 568, 0x238, 0, True),
- ItemName.AutoMaster: ItemData(0x1300E5, 1, 387, 0x183, 0, True),
- ItemName.AutoFinal: ItemData(0x1300E6, 1, 388, 0x184, 0, True),
- ItemName.AutoSummon: ItemData(0x1300E7, 1, 389, 0x185, 0, True),
- ItemName.TrinityLimit: ItemData(0x1300E8, 1, 198, 0x0C6, 0, True),
+ ItemName.Guard: ItemData(1, 82, 0x052, ability=True),
+ ItemName.UpperSlash: ItemData(1, 137, 0x089, ability=True),
+ ItemName.HorizontalSlash: ItemData(1, 271, 0x10F, ability=True),
+ ItemName.FinishingLeap: ItemData(1, 267, 0x10B, ability=True),
+ ItemName.RetaliatingSlash: ItemData(1, 273, 0x111, ability=True),
+ ItemName.Slapshot: ItemData(1, 262, 0x106, ability=True),
+ ItemName.DodgeSlash: ItemData(1, 263, 0x107, ability=True),
+ ItemName.FlashStep: ItemData(1, 559, 0x22F, ability=True),
+ ItemName.SlideDash: ItemData(1, 264, 0x108, ability=True),
+ ItemName.VicinityBreak: ItemData(1, 562, 0x232, ability=True),
+ ItemName.GuardBreak: ItemData(1, 265, 0x109, ability=True),
+ ItemName.Explosion: ItemData(1, 266, 0x10A, ability=True),
+ ItemName.AerialSweep: ItemData(1, 269, 0x10D, ability=True),
+ ItemName.AerialDive: ItemData(1, 560, 0x230, ability=True),
+ ItemName.AerialSpiral: ItemData(1, 270, 0x10E, ability=True),
+ ItemName.AerialFinish: ItemData(1, 272, 0x110, ability=True),
+ ItemName.MagnetBurst: ItemData(1, 561, 0x231, ability=True),
+ ItemName.Counterguard: ItemData(1, 268, 0x10C, ability=True),
+ ItemName.AutoValor: ItemData(1, 385, 0x181, ability=True),
+ ItemName.AutoWisdom: ItemData(1, 386, 0x182, ability=True),
+ ItemName.AutoLimit: ItemData(1, 568, 0x238, ability=True),
+ ItemName.AutoMaster: ItemData(1, 387, 0x183, ability=True),
+ ItemName.AutoFinal: ItemData(1, 388, 0x184, ability=True),
+ ItemName.AutoSummon: ItemData(1, 389, 0x185, ability=True),
+ ItemName.TrinityLimit: ItemData(1, 198, 0x0C6, ability=True),
}
-Items_Table = {
- ItemName.PowerBoost: ItemData(0x1300E9, 1, 276, 0x3666),
- ItemName.MagicBoost: ItemData(0x1300EA, 1, 277, 0x3667),
- ItemName.DefenseBoost: ItemData(0x1300EB, 1, 278, 0x3668),
- ItemName.APBoost: ItemData(0x1300EC, 1, 279, 0x3669),
+Boosts_Table = {
+ ItemName.PowerBoost: ItemData(1, 253, 0x359D), # 276, 0x3666, market place map
+ ItemName.MagicBoost: ItemData(1, 586, 0x35E0), # 277, 0x3667, dark rememberance map
+ ItemName.DefenseBoost: ItemData(1, 590, 0x35F8), # 278, 0x3668, depths of remembrance map
+ ItemName.APBoost: ItemData(1, 532, 0x35FE), # 279, 0x3669, mansion map
}
# These items cannot be in other games so these are done locally in kh2
DonaldAbility_Table = {
- ItemName.DonaldFire: ItemData(0x1300ED, 1, 165, 0xA5, 0, True),
- ItemName.DonaldBlizzard: ItemData(0x1300EE, 1, 166, 0xA6, 0, True),
- ItemName.DonaldThunder: ItemData(0x1300EF, 1, 167, 0xA7, 0, True),
- ItemName.DonaldCure: ItemData(0x1300F0, 1, 168, 0xA8, 0, True),
- ItemName.Fantasia: ItemData(0x1300F1, 1, 199, 0xC7, 0, True),
- ItemName.FlareForce: ItemData(0x1300F2, 1, 200, 0xC8, 0, True),
- ItemName.DonaldMPRage: ItemData(0x1300F3, 3, 412, 0x19C, 0, True),
- ItemName.DonaldJackpot: ItemData(0x1300F4, 1, 406, 0x196, 0, True),
- ItemName.DonaldLuckyLucky: ItemData(0x1300F5, 3, 407, 0x197, 0, True),
- ItemName.DonaldFireBoost: ItemData(0x1300F6, 2, 408, 0x198, 0, True),
- ItemName.DonaldBlizzardBoost: ItemData(0x1300F7, 2, 409, 0x199, 0, True),
- ItemName.DonaldThunderBoost: ItemData(0x1300F8, 2, 410, 0x19A, 0, True),
- ItemName.DonaldMPHaste: ItemData(0x1300F9, 1, 413, 0x19D, 0, True),
- ItemName.DonaldMPHastera: ItemData(0x1300FA, 2, 421, 0x1A5, 0, True),
- ItemName.DonaldMPHastega: ItemData(0x1300FB, 2, 422, 0x1A6, 0, True),
- ItemName.DonaldAutoLimit: ItemData(0x1300FC, 1, 417, 0x1A1, 0, True),
- ItemName.DonaldHyperHealing: ItemData(0x1300FD, 2, 419, 0x1A3, 0, True),
- ItemName.DonaldAutoHealing: ItemData(0x1300FE, 1, 420, 0x1A4, 0, True),
- ItemName.DonaldItemBoost: ItemData(0x1300FF, 1, 411, 0x19B, 0, True),
- ItemName.DonaldDamageControl: ItemData(0x130100, 2, 542, 0x21E, 0, True),
- ItemName.DonaldDraw: ItemData(0x130101, 1, 405, 0x195, 0, True),
+ ItemName.DonaldFire: ItemData(1, 165, 0xA5, ability=True),
+ ItemName.DonaldBlizzard: ItemData(1, 166, 0xA6, ability=True),
+ ItemName.DonaldThunder: ItemData(1, 167, 0xA7, ability=True),
+ ItemName.DonaldCure: ItemData(1, 168, 0xA8, ability=True),
+ ItemName.Fantasia: ItemData(1, 199, 0xC7, ability=True),
+ ItemName.FlareForce: ItemData(1, 200, 0xC8, ability=True),
+ ItemName.DonaldMPRage: ItemData(1, 412, 0x19C, ability=True), # originally 3 but swapped to 1 because crit checks
+ ItemName.DonaldJackpot: ItemData(1, 406, 0x196, ability=True),
+ ItemName.DonaldLuckyLucky: ItemData(3, 407, 0x197, ability=True),
+ ItemName.DonaldFireBoost: ItemData(2, 408, 0x198, ability=True),
+ ItemName.DonaldBlizzardBoost: ItemData(2, 409, 0x199, ability=True),
+ ItemName.DonaldThunderBoost: ItemData(2, 410, 0x19A, ability=True),
+ ItemName.DonaldMPHaste: ItemData(1, 413, 0x19D, ability=True),
+ ItemName.DonaldMPHastera: ItemData(2, 421, 0x1A5, ability=True),
+ ItemName.DonaldMPHastega: ItemData(2, 422, 0x1A6, ability=True),
+ ItemName.DonaldAutoLimit: ItemData(1, 417, 0x1A1, ability=True),
+ ItemName.DonaldHyperHealing: ItemData(2, 419, 0x1A3, ability=True),
+ ItemName.DonaldAutoHealing: ItemData(1, 420, 0x1A4, ability=True),
+ ItemName.DonaldItemBoost: ItemData(1, 411, 0x19B, ability=True),
+ ItemName.DonaldDamageControl: ItemData(2, 542, 0x21E, ability=True),
+ ItemName.DonaldDraw: ItemData(1, 405, 0x195, ability=True),
}
+
GoofyAbility_Table = {
- ItemName.GoofyTornado: ItemData(0x130102, 1, 423, 0x1A7, 0, True),
- ItemName.GoofyTurbo: ItemData(0x130103, 1, 425, 0x1A9, 0, True),
- ItemName.GoofyBash: ItemData(0x130104, 1, 429, 0x1AD, 0, True),
- ItemName.TornadoFusion: ItemData(0x130105, 1, 201, 0xC9, 0, True),
- ItemName.Teamwork: ItemData(0x130106, 1, 202, 0xCA, 0, True),
- ItemName.GoofyDraw: ItemData(0x130107, 1, 405, 0x195, 0, True),
- ItemName.GoofyJackpot: ItemData(0x130108, 1, 406, 0x196, 0, True),
- ItemName.GoofyLuckyLucky: ItemData(0x130109, 1, 407, 0x197, 0, True),
- ItemName.GoofyItemBoost: ItemData(0x13010A, 2, 411, 0x19B, 0, True),
- ItemName.GoofyMPRage: ItemData(0x13010B, 2, 412, 0x19C, 0, True),
- ItemName.GoofyDefender: ItemData(0x13010C, 2, 414, 0x19E, 0, True),
- ItemName.GoofyDamageControl: ItemData(0x13010D, 3, 542, 0x21E, 0, True),
- ItemName.GoofyAutoLimit: ItemData(0x13010E, 1, 417, 0x1A1, 0, True),
- ItemName.GoofySecondChance: ItemData(0x13010F, 1, 415, 0x19F, 0, True),
- ItemName.GoofyOnceMore: ItemData(0x130110, 1, 416, 0x1A0, 0, True),
- ItemName.GoofyAutoChange: ItemData(0x130111, 1, 418, 0x1A2, 0, True),
- ItemName.GoofyHyperHealing: ItemData(0x130112, 2, 419, 0x1A3, 0, True),
- ItemName.GoofyAutoHealing: ItemData(0x130113, 1, 420, 0x1A4, 0, True),
- ItemName.GoofyMPHaste: ItemData(0x130114, 1, 413, 0x19D, 0, True),
- ItemName.GoofyMPHastera: ItemData(0x130115, 1, 421, 0x1A5, 0, True),
- ItemName.GoofyMPHastega: ItemData(0x130116, 1, 422, 0x1A6, 0, True),
- ItemName.GoofyProtect: ItemData(0x130117, 2, 596, 0x254, 0, True),
- ItemName.GoofyProtera: ItemData(0x130118, 2, 597, 0x255, 0, True),
- ItemName.GoofyProtega: ItemData(0x130119, 2, 598, 0x256, 0, True),
+ ItemName.GoofyTornado: ItemData(1, 423, 0x1A7, ability=True),
+ ItemName.GoofyTurbo: ItemData(1, 425, 0x1A9, ability=True),
+ ItemName.GoofyBash: ItemData(1, 429, 0x1AD, ability=True),
+ ItemName.TornadoFusion: ItemData(1, 201, 0xC9, ability=True),
+ ItemName.Teamwork: ItemData(1, 202, 0xCA, ability=True),
+ ItemName.GoofyDraw: ItemData(1, 405, 0x195, ability=True),
+ ItemName.GoofyJackpot: ItemData(1, 406, 0x196, ability=True),
+ ItemName.GoofyLuckyLucky: ItemData(1, 407, 0x197, ability=True),
+ ItemName.GoofyItemBoost: ItemData(2, 411, 0x19B, ability=True),
+ ItemName.GoofyMPRage: ItemData(2, 412, 0x19C, ability=True),
+ ItemName.GoofyDefender: ItemData(2, 414, 0x19E, ability=True),
+ ItemName.GoofyDamageControl: ItemData(1, 542, 0x21E, ability=True), # originally 3 but swapped to 1 because crit checks
+ ItemName.GoofyAutoLimit: ItemData(1, 417, 0x1A1, ability=True),
+ ItemName.GoofySecondChance: ItemData(1, 415, 0x19F, ability=True),
+ ItemName.GoofyOnceMore: ItemData(1, 416, 0x1A0, ability=True),
+ ItemName.GoofyAutoChange: ItemData(1, 418, 0x1A2, ability=True),
+ ItemName.GoofyHyperHealing: ItemData(2, 419, 0x1A3, ability=True),
+ ItemName.GoofyAutoHealing: ItemData(1, 420, 0x1A4, ability=True),
+ ItemName.GoofyMPHaste: ItemData(1, 413, 0x19D, ability=True),
+ ItemName.GoofyMPHastera: ItemData(1, 421, 0x1A5, ability=True),
+ ItemName.GoofyMPHastega: ItemData(1, 422, 0x1A6, ability=True),
+ ItemName.GoofyProtect: ItemData(2, 596, 0x254, ability=True),
+ ItemName.GoofyProtera: ItemData(2, 597, 0x255, ability=True),
+ ItemName.GoofyProtega: ItemData(2, 598, 0x256, ability=True),
+
+}
+Wincon_Table = {
+ ItemName.LuckyEmblem: ItemData(kh2id=367, memaddr=0x3641), # letter item
+ # ItemName.Victory: ItemData(kh2id=263, memaddr=0x111),
+ ItemName.Bounty: ItemData(kh2id=461, memaddr=0x365E), # Dummy 14
+ # ItemName.UniversalKey:ItemData(,365,0x363F,0)#Tournament Poster
}
-Misc_Table = {
- ItemName.LuckyEmblem: ItemData(0x13011A, 0, 367, 0x3641), # letter item
- ItemName.Victory: ItemData(0x13011B, 0, 263, 0x111),
- ItemName.Bounty: ItemData(0x13011C, 0, 461, 0, 0), # Dummy 14
- # ItemName.UniversalKey:ItemData(0x130129,0,365,0x363F,0)#Tournament Poster
+Consumable_Table = {
+ ItemName.Potion: ItemData(1, 127, 0x36B8), # 1, 0x3580, piglets house map
+ ItemName.HiPotion: ItemData(1, 126, 0x36B9), # 2, 0x03581, rabbits house map
+ ItemName.Ether: ItemData(1, 128, 0x36BA), # 3, 0x3582, kangas house map
+ ItemName.Elixir: ItemData(1, 129, 0x36BB), # 4, 0x3583, spooky cave map
+ ItemName.Megalixir: ItemData(1, 124, 0x36BC), # 7, 0x3586, starry hill map
+ ItemName.Tent: ItemData(1, 512, 0x36BD), # 131,0x35E1, savannah map
+ ItemName.DriveRecovery: ItemData(1, 252, 0x36BE), # 274,0x3664, pride rock map
+ ItemName.HighDriveRecovery: ItemData(1, 511, 0x36BF), # 275,0x3665, oasis map
+}
+Events_Table = {
+ ItemName.HostileProgramEvent,
+ ItemName.McpEvent,
+ ItemName.ASLarxeneEvent,
+ ItemName.DataLarxeneEvent,
+ ItemName.BarbosaEvent,
+ ItemName.GrimReaper1Event,
+ ItemName.GrimReaper2Event,
+ ItemName.DataLuxordEvent,
+ ItemName.DataAxelEvent,
+ ItemName.CerberusEvent,
+ ItemName.OlympusPeteEvent,
+ ItemName.HydraEvent,
+ ItemName.OcPainAndPanicCupEvent,
+ ItemName.OcCerberusCupEvent,
+ ItemName.HadesEvent,
+ ItemName.ASZexionEvent,
+ ItemName.DataZexionEvent,
+ ItemName.Oc2TitanCupEvent,
+ ItemName.Oc2GofCupEvent,
+ ItemName.Oc2CupsEvent,
+ ItemName.HadesCupEvents,
+ ItemName.PrisonKeeperEvent,
+ ItemName.OogieBoogieEvent,
+ ItemName.ExperimentEvent,
+ ItemName.ASVexenEvent,
+ ItemName.DataVexenEvent,
+ ItemName.ShanYuEvent,
+ ItemName.AnsemRikuEvent,
+ ItemName.StormRiderEvent,
+ ItemName.DataXigbarEvent,
+ ItemName.RoxasEvent,
+ ItemName.XigbarEvent,
+ ItemName.LuxordEvent,
+ ItemName.SaixEvent,
+ ItemName.XemnasEvent,
+ ItemName.ArmoredXemnasEvent,
+ ItemName.ArmoredXemnas2Event,
+ ItemName.FinalXemnasEvent,
+ ItemName.DataXemnasEvent,
+ ItemName.ThresholderEvent,
+ ItemName.BeastEvent,
+ ItemName.DarkThornEvent,
+ ItemName.XaldinEvent,
+ ItemName.DataXaldinEvent,
+ ItemName.TwinLordsEvent,
+ ItemName.GenieJafarEvent,
+ ItemName.ASLexaeusEvent,
+ ItemName.DataLexaeusEvent,
+ ItemName.ScarEvent,
+ ItemName.GroundShakerEvent,
+ ItemName.DataSaixEvent,
+ ItemName.HBDemyxEvent,
+ ItemName.ThousandHeartlessEvent,
+ ItemName.Mushroom13Event,
+ ItemName.SephiEvent,
+ ItemName.DataDemyxEvent,
+ ItemName.CorFirstFightEvent,
+ ItemName.CorSecondFightEvent,
+ ItemName.TransportEvent,
+ ItemName.OldPeteEvent,
+ ItemName.FuturePeteEvent,
+ ItemName.ASMarluxiaEvent,
+ ItemName.DataMarluxiaEvent,
+ ItemName.TerraEvent,
+ ItemName.TwilightThornEvent,
+ ItemName.Axel1Event,
+ ItemName.Axel2Event,
+ ItemName.DataRoxasEvent,
}
# Items that are prone to duping.
# anchors for checking form keyblade
@@ -358,185 +427,37 @@ class ItemData(typing.NamedTuple):
# Equipped abilities have an offset of 0x8000 so check for if whatever || whatever+0x8000
CheckDupingItems = {
"Items": {
- ItemName.ProofofConnection,
- ItemName.ProofofNonexistence,
- ItemName.ProofofPeace,
- ItemName.PromiseCharm,
- ItemName.NamineSketches,
- ItemName.CastleKey,
- ItemName.BattlefieldsofWar,
- ItemName.SwordoftheAncestor,
- ItemName.BeastsClaw,
- ItemName.BoneFist,
- ItemName.ProudFang,
- ItemName.SkillandCrossbones,
- ItemName.Scimitar,
- ItemName.MembershipCard,
- ItemName.IceCream,
- ItemName.WaytotheDawn,
- ItemName.IdentityDisk,
- ItemName.TornPages,
- ItemName.LuckyEmblem,
- ItemName.MickyMunnyPouch,
- ItemName.OletteMunnyPouch,
- ItemName.HadesCupTrophy,
- ItemName.UnknownDisk,
- ItemName.OlympusStone,
+ item_name for keys in [Progression_Table.keys(), Wincon_Table.keys(), Consumable_Table, [ItemName.MickeyMunnyPouch,
+ ItemName.OletteMunnyPouch,
+ ItemName.HadesCupTrophy,
+ ItemName.UnknownDisk,
+ ItemName.OlympusStone, ], Boosts_Table.keys()]
+ for item_name in keys
+
},
"Magic": {
- ItemName.FireElement,
- ItemName.BlizzardElement,
- ItemName.ThunderElement,
- ItemName.CureElement,
- ItemName.MagnetElement,
- ItemName.ReflectElement,
+ magic for magic in Magic_Table.keys()
},
"Bitmask": {
- ItemName.ValorForm,
- ItemName.WisdomForm,
- ItemName.LimitForm,
- ItemName.MasterForm,
- ItemName.FinalForm,
- ItemName.Genie,
- ItemName.PeterPan,
- ItemName.Stitch,
- ItemName.ChickenLittle,
- ItemName.SecretAnsemsReport1,
- ItemName.SecretAnsemsReport2,
- ItemName.SecretAnsemsReport3,
- ItemName.SecretAnsemsReport4,
- ItemName.SecretAnsemsReport5,
- ItemName.SecretAnsemsReport6,
- ItemName.SecretAnsemsReport7,
- ItemName.SecretAnsemsReport8,
- ItemName.SecretAnsemsReport9,
- ItemName.SecretAnsemsReport10,
- ItemName.SecretAnsemsReport11,
- ItemName.SecretAnsemsReport12,
- ItemName.SecretAnsemsReport13,
-
+ item_name for keys in [Forms_Table.keys(), Summon_Table.keys(), Reports_Table.keys()] for item_name in keys
},
"Weapons": {
"Keyblades": {
- ItemName.Oathkeeper,
- ItemName.Oblivion,
- ItemName.StarSeeker,
- ItemName.HiddenDragon,
- ItemName.HerosCrest,
- ItemName.Monochrome,
- ItemName.FollowtheWind,
- ItemName.CircleofLife,
- ItemName.PhotonDebugger,
- ItemName.GullWing,
- ItemName.RumblingRose,
- ItemName.GuardianSoul,
- ItemName.WishingLamp,
- ItemName.DecisivePumpkin,
- ItemName.SleepingLion,
- ItemName.SweetMemories,
- ItemName.MysteriousAbyss,
- ItemName.TwoBecomeOne,
- ItemName.FatalCrest,
- ItemName.BondofFlame,
- ItemName.Fenrir,
- ItemName.UltimaWeapon,
- ItemName.WinnersProof,
- ItemName.Pureblood,
+ keyblade for keyblade in Keyblade_Table.keys()
},
"Staffs": {
- ItemName.Centurion2,
- ItemName.MeteorStaff,
- ItemName.NobodyLance,
- ItemName.PreciousMushroom,
- ItemName.PreciousMushroom2,
- ItemName.PremiumMushroom,
- ItemName.RisingDragon,
- ItemName.SaveTheQueen2,
- ItemName.ShamansRelic,
+ staff for staff in Staffs_Table.keys()
},
"Shields": {
- ItemName.AkashicRecord,
- ItemName.FrozenPride2,
- ItemName.GenjiShield,
- ItemName.MajesticMushroom,
- ItemName.MajesticMushroom2,
- ItemName.NobodyGuard,
- ItemName.OgreShield,
- ItemName.SaveTheKing2,
- ItemName.UltimateMushroom,
+ shield for shield in Shields_Table.keys()
}
},
"Equipment": {
"Accessories": {
- ItemName.AbilityRing,
- ItemName.EngineersRing,
- ItemName.TechniciansRing,
- ItemName.SkillRing,
- ItemName.SkillfulRing,
- ItemName.ExpertsRing,
- ItemName.MastersRing,
- ItemName.CosmicRing,
- ItemName.ExecutivesRing,
- ItemName.SardonyxRing,
- ItemName.TourmalineRing,
- ItemName.AquamarineRing,
- ItemName.GarnetRing,
- ItemName.DiamondRing,
- ItemName.SilverRing,
- ItemName.GoldRing,
- ItemName.PlatinumRing,
- ItemName.MythrilRing,
- ItemName.OrichalcumRing,
- ItemName.SoldierEarring,
- ItemName.FencerEarring,
- ItemName.MageEarring,
- ItemName.SlayerEarring,
- ItemName.Medal,
- ItemName.MoonAmulet,
- ItemName.StarCharm,
- ItemName.CosmicArts,
- ItemName.ShadowArchive,
- ItemName.ShadowArchive2,
- ItemName.FullBloom,
- ItemName.FullBloom2,
- ItemName.DrawRing,
- ItemName.LuckyRing,
+ accessory for accessory in Accessory_Table.keys()
},
"Armor": {
- ItemName.ElvenBandana,
- ItemName.DivineBandana,
- ItemName.ProtectBelt,
- ItemName.GaiaBelt,
- ItemName.PowerBand,
- ItemName.BusterBand,
- ItemName.CosmicBelt,
- ItemName.FireBangle,
- ItemName.FiraBangle,
- ItemName.FiragaBangle,
- ItemName.FiragunBangle,
- ItemName.BlizzardArmlet,
- ItemName.BlizzaraArmlet,
- ItemName.BlizzagaArmlet,
- ItemName.BlizzagunArmlet,
- ItemName.ThunderTrinket,
- ItemName.ThundaraTrinket,
- ItemName.ThundagaTrinket,
- ItemName.ThundagunTrinket,
- ItemName.ShockCharm,
- ItemName.ShockCharm2,
- ItemName.ShadowAnklet,
- ItemName.DarkAnklet,
- ItemName.MidnightAnklet,
- ItemName.ChaosAnklet,
- ItemName.ChampionBelt,
- ItemName.AbasChain,
- ItemName.AegisChain,
- ItemName.Acrisius,
- ItemName.Acrisius2,
- ItemName.CosmicChain,
- ItemName.PetiteRibbon,
- ItemName.Ribbon,
- ItemName.GrandRibbon,
+ armor for armor in Armor_Table.keys()
}
},
"Stat Increases": {
@@ -549,297 +470,103 @@ class ItemData(typing.NamedTuple):
},
"Abilities": {
"Sora": {
- ItemName.Scan,
+ item_name for keys in [SupportAbility_Table.keys(), ActionAbility_Table.keys(), Movement_Table.keys()] for item_name in keys
+ },
+ "Donald": {
+ donald_ability for donald_ability in DonaldAbility_Table.keys()
+ },
+ "Goofy": {
+ goofy_ability for goofy_ability in GoofyAbility_Table.keys()
+ }
+ },
+}
+progression_set = {
+ # abilities
+ item_name for keys in [
+ Wincon_Table.keys(),
+ Progression_Table.keys(),
+ Forms_Table.keys(),
+ Magic_Table.keys(),
+ Summon_Table.keys(),
+ Movement_Table.keys(),
+ Keyblade_Table.keys(),
+ Staffs_Table.keys(),
+ Shields_Table.keys(),
+ [
ItemName.AerialRecovery,
ItemName.ComboMaster,
ItemName.ComboPlus,
ItemName.AirComboPlus,
- ItemName.ComboBoost,
- ItemName.AirComboBoost,
- ItemName.ReactionBoost,
ItemName.FinishingPlus,
ItemName.NegativeCombo,
ItemName.BerserkCharge,
- ItemName.DamageDrive,
- ItemName.DriveBoost,
ItemName.FormBoost,
- ItemName.SummonBoost,
- ItemName.ExperienceBoost,
- ItemName.Draw,
- ItemName.Jackpot,
- ItemName.LuckyLucky,
- ItemName.DriveConverter,
- ItemName.FireBoost,
- ItemName.BlizzardBoost,
- ItemName.ThunderBoost,
- ItemName.ItemBoost,
- ItemName.MPRage,
- ItemName.MPHaste,
- ItemName.MPHastera,
- ItemName.MPHastega,
- ItemName.Defender,
- ItemName.DamageControl,
- ItemName.NoExperience,
ItemName.LightDarkness,
- ItemName.MagicLock,
- ItemName.LeafBracer,
- ItemName.CombinationBoost,
ItemName.OnceMore,
ItemName.SecondChance,
ItemName.Guard,
- ItemName.UpperSlash,
ItemName.HorizontalSlash,
ItemName.FinishingLeap,
- ItemName.RetaliatingSlash,
ItemName.Slapshot,
- ItemName.DodgeSlash,
ItemName.FlashStep,
ItemName.SlideDash,
- ItemName.VicinityBreak,
ItemName.GuardBreak,
ItemName.Explosion,
ItemName.AerialSweep,
ItemName.AerialDive,
ItemName.AerialSpiral,
ItemName.AerialFinish,
- ItemName.MagnetBurst,
- ItemName.Counterguard,
ItemName.AutoValor,
ItemName.AutoWisdom,
ItemName.AutoLimit,
ItemName.AutoMaster,
ItemName.AutoFinal,
- ItemName.AutoSummon,
ItemName.TrinityLimit,
- ItemName.HighJump,
- ItemName.QuickRun,
- ItemName.DodgeRoll,
- ItemName.AerialDodge,
- ItemName.Glide,
- },
- "Donald": {
- ItemName.DonaldFire,
- ItemName.DonaldBlizzard,
- ItemName.DonaldThunder,
- ItemName.DonaldCure,
- ItemName.Fantasia,
+ ItemName.DriveConverter,
+ # Party Limits
ItemName.FlareForce,
- ItemName.DonaldMPRage,
- ItemName.DonaldJackpot,
- ItemName.DonaldLuckyLucky,
- ItemName.DonaldFireBoost,
- ItemName.DonaldBlizzardBoost,
- ItemName.DonaldThunderBoost,
- ItemName.DonaldMPHaste,
- ItemName.DonaldMPHastera,
- ItemName.DonaldMPHastega,
- ItemName.DonaldAutoLimit,
- ItemName.DonaldHyperHealing,
- ItemName.DonaldAutoHealing,
- ItemName.DonaldItemBoost,
- ItemName.DonaldDamageControl,
- ItemName.DonaldDraw,
- },
- "Goofy": {
- ItemName.GoofyTornado,
- ItemName.GoofyTurbo,
- ItemName.GoofyBash,
- ItemName.TornadoFusion,
+ ItemName.Fantasia,
ItemName.Teamwork,
- ItemName.GoofyDraw,
- ItemName.GoofyJackpot,
- ItemName.GoofyLuckyLucky,
- ItemName.GoofyItemBoost,
- ItemName.GoofyMPRage,
- ItemName.GoofyDefender,
- ItemName.GoofyDamageControl,
- ItemName.GoofyAutoLimit,
- ItemName.GoofySecondChance,
- ItemName.GoofyOnceMore,
- ItemName.GoofyAutoChange,
- ItemName.GoofyHyperHealing,
- ItemName.GoofyAutoHealing,
- ItemName.GoofyMPHaste,
- ItemName.GoofyMPHastera,
- ItemName.GoofyMPHastega,
- ItemName.GoofyProtect,
- ItemName.GoofyProtera,
- ItemName.GoofyProtega,
- }
- },
- "Boosts": {
- ItemName.PowerBoost,
- ItemName.MagicBoost,
- ItemName.DefenseBoost,
- ItemName.APBoost,
- }
+ ItemName.TornadoFusion,
+ ItemName.HadesCupTrophy],
+ Events_Table]
+ for item_name in keys
}
+party_filler_set = {
+ ItemName.GoofyAutoHealing,
+ ItemName.GoofyMPHaste,
+ ItemName.GoofyMPHastera,
+ ItemName.GoofyMPHastega,
+ ItemName.GoofyProtect,
+ ItemName.GoofyProtera,
+ ItemName.GoofyProtega,
+ ItemName.GoofyMPRage,
+ ItemName.GoofyDefender,
+ ItemName.GoofyDamageControl,
-Progression_Dicts = {
- # Items that are classified as progression
- "Progression": {
- # Wincons
- ItemName.Victory,
- ItemName.LuckyEmblem,
- ItemName.Bounty,
- ItemName.ProofofConnection,
- ItemName.ProofofNonexistence,
- ItemName.ProofofPeace,
- ItemName.PromiseCharm,
- # visit locking
- ItemName.NamineSketches,
- # dummy 13
- ItemName.CastleKey,
- ItemName.BattlefieldsofWar,
- ItemName.SwordoftheAncestor,
- ItemName.BeastsClaw,
- ItemName.BoneFist,
- ItemName.ProudFang,
- ItemName.SkillandCrossbones,
- ItemName.Scimitar,
- ItemName.MembershipCard,
- ItemName.IceCream,
- ItemName.WaytotheDawn,
- ItemName.IdentityDisk,
- ItemName.TornPages,
- # forms
- ItemName.ValorForm,
- ItemName.WisdomForm,
- ItemName.LimitForm,
- ItemName.MasterForm,
- ItemName.FinalForm,
- # magic
- ItemName.FireElement,
- ItemName.BlizzardElement,
- ItemName.ThunderElement,
- ItemName.CureElement,
- ItemName.MagnetElement,
- ItemName.ReflectElement,
- ItemName.Genie,
- ItemName.PeterPan,
- ItemName.Stitch,
- ItemName.ChickenLittle,
- # movement
- ItemName.HighJump,
- ItemName.QuickRun,
- ItemName.DodgeRoll,
- ItemName.AerialDodge,
- ItemName.Glide,
- # abilities
- ItemName.Scan,
- ItemName.AerialRecovery,
- ItemName.ComboMaster,
- ItemName.ComboPlus,
- ItemName.AirComboPlus,
- ItemName.ComboBoost,
- ItemName.AirComboBoost,
- ItemName.ReactionBoost,
- ItemName.FinishingPlus,
- ItemName.NegativeCombo,
- ItemName.BerserkCharge,
- ItemName.DamageDrive,
- ItemName.DriveBoost,
- ItemName.FormBoost,
- ItemName.SummonBoost,
- ItemName.ExperienceBoost,
- ItemName.Draw,
- ItemName.Jackpot,
- ItemName.LuckyLucky,
- ItemName.DriveConverter,
- ItemName.FireBoost,
- ItemName.BlizzardBoost,
- ItemName.ThunderBoost,
- ItemName.ItemBoost,
- ItemName.MPRage,
- ItemName.MPHaste,
- ItemName.MPHastera,
- ItemName.MPHastega,
- ItemName.Defender,
- ItemName.DamageControl,
- ItemName.NoExperience,
- ItemName.LightDarkness,
- ItemName.MagicLock,
- ItemName.LeafBracer,
- ItemName.CombinationBoost,
- ItemName.OnceMore,
- ItemName.SecondChance,
- ItemName.Guard,
- ItemName.UpperSlash,
- ItemName.HorizontalSlash,
- ItemName.FinishingLeap,
- ItemName.RetaliatingSlash,
- ItemName.Slapshot,
- ItemName.DodgeSlash,
- ItemName.FlashStep,
- ItemName.SlideDash,
- ItemName.VicinityBreak,
- ItemName.GuardBreak,
- ItemName.Explosion,
- ItemName.AerialSweep,
- ItemName.AerialDive,
- ItemName.AerialSpiral,
- ItemName.AerialFinish,
- ItemName.MagnetBurst,
- ItemName.Counterguard,
- ItemName.AutoValor,
- ItemName.AutoWisdom,
- ItemName.AutoLimit,
- ItemName.AutoMaster,
- ItemName.AutoFinal,
- ItemName.AutoSummon,
- ItemName.TrinityLimit,
- # keyblades
- ItemName.Oathkeeper,
- ItemName.Oblivion,
- ItemName.StarSeeker,
- ItemName.HiddenDragon,
- ItemName.HerosCrest,
- ItemName.Monochrome,
- ItemName.FollowtheWind,
- ItemName.CircleofLife,
- ItemName.PhotonDebugger,
- ItemName.GullWing,
- ItemName.RumblingRose,
- ItemName.GuardianSoul,
- ItemName.WishingLamp,
- ItemName.DecisivePumpkin,
- ItemName.SleepingLion,
- ItemName.SweetMemories,
- ItemName.MysteriousAbyss,
- ItemName.TwoBecomeOne,
- ItemName.FatalCrest,
- ItemName.BondofFlame,
- ItemName.Fenrir,
- ItemName.UltimaWeapon,
- ItemName.WinnersProof,
- ItemName.Pureblood,
- # Staffs
- ItemName.Centurion2,
- ItemName.MeteorStaff,
- ItemName.NobodyLance,
- ItemName.PreciousMushroom,
- ItemName.PreciousMushroom2,
- ItemName.PremiumMushroom,
- ItemName.RisingDragon,
- ItemName.SaveTheQueen2,
- ItemName.ShamansRelic,
- # Shields
- ItemName.AkashicRecord,
- ItemName.FrozenPride2,
- ItemName.GenjiShield,
- ItemName.MajesticMushroom,
- ItemName.MajesticMushroom2,
- ItemName.NobodyGuard,
- ItemName.OgreShield,
- ItemName.SaveTheKing2,
- ItemName.UltimateMushroom,
- # Party Limits
- ItemName.FlareForce,
- ItemName.Fantasia,
- ItemName.Teamwork,
- ItemName.TornadoFusion
- },
- "2VisitLocking": {
+ ItemName.DonaldFireBoost,
+ ItemName.DonaldBlizzardBoost,
+ ItemName.DonaldThunderBoost,
+ ItemName.DonaldMPHaste,
+ ItemName.DonaldMPHastera,
+ ItemName.DonaldMPHastega,
+ ItemName.DonaldAutoHealing,
+ ItemName.DonaldDamageControl,
+ ItemName.DonaldDraw,
+ ItemName.DonaldMPRage,
+}
+useful_set = {item_name for keys in [
+ SupportAbility_Table.keys(),
+ ActionAbility_Table.keys(),
+ DonaldAbility_Table.keys(),
+ GoofyAbility_Table.keys(),
+ Armor_Table.keys(),
+ Usefull_Table.keys(),
+ Accessory_Table.keys()]
+ for item_name in keys if item_name not in progression_set and item_name not in party_filler_set}
+
+visit_locking_dict = {
+ "2VisitLocking": [
ItemName.CastleKey,
ItemName.BattlefieldsofWar,
ItemName.SwordoftheAncestor,
@@ -854,7 +581,7 @@ class ItemData(typing.NamedTuple):
ItemName.IdentityDisk,
ItemName.IceCream,
ItemName.NamineSketches
- },
+ ],
"AllVisitLocking": {
ItemName.CastleKey: 2,
ItemName.BattlefieldsofWar: 2,
@@ -865,84 +592,13 @@ class ItemData(typing.NamedTuple):
ItemName.SkillandCrossbones: 2,
ItemName.Scimitar: 2,
ItemName.MembershipCard: 2,
- ItemName.WaytotheDawn: 1,
+ ItemName.WaytotheDawn: 2,
ItemName.IdentityDisk: 2,
ItemName.IceCream: 3,
ItemName.NamineSketches: 1,
}
}
-
-exclusionItem_table = {
- "Ability": {
- ItemName.Scan,
- ItemName.AerialRecovery,
- ItemName.ComboMaster,
- ItemName.ComboPlus,
- ItemName.AirComboPlus,
- ItemName.ComboBoost,
- ItemName.AirComboBoost,
- ItemName.ReactionBoost,
- ItemName.FinishingPlus,
- ItemName.NegativeCombo,
- ItemName.BerserkCharge,
- ItemName.DamageDrive,
- ItemName.DriveBoost,
- ItemName.FormBoost,
- ItemName.SummonBoost,
- ItemName.ExperienceBoost,
- ItemName.Draw,
- ItemName.Jackpot,
- ItemName.LuckyLucky,
- ItemName.DriveConverter,
- ItemName.FireBoost,
- ItemName.BlizzardBoost,
- ItemName.ThunderBoost,
- ItemName.ItemBoost,
- ItemName.MPRage,
- ItemName.MPHaste,
- ItemName.MPHastera,
- ItemName.MPHastega,
- ItemName.Defender,
- ItemName.DamageControl,
- ItemName.NoExperience,
- ItemName.LightDarkness,
- ItemName.MagicLock,
- ItemName.LeafBracer,
- ItemName.CombinationBoost,
- ItemName.DamageDrive,
- ItemName.OnceMore,
- ItemName.SecondChance,
- ItemName.Guard,
- ItemName.UpperSlash,
- ItemName.HorizontalSlash,
- ItemName.FinishingLeap,
- ItemName.RetaliatingSlash,
- ItemName.Slapshot,
- ItemName.DodgeSlash,
- ItemName.FlashStep,
- ItemName.SlideDash,
- ItemName.VicinityBreak,
- ItemName.GuardBreak,
- ItemName.Explosion,
- ItemName.AerialSweep,
- ItemName.AerialDive,
- ItemName.AerialSpiral,
- ItemName.AerialFinish,
- ItemName.MagnetBurst,
- ItemName.Counterguard,
- ItemName.AutoValor,
- ItemName.AutoWisdom,
- ItemName.AutoLimit,
- ItemName.AutoMaster,
- ItemName.AutoFinal,
- ItemName.AutoSummon,
- ItemName.TrinityLimit,
- ItemName.HighJump,
- ItemName.QuickRun,
- ItemName.DodgeRoll,
- ItemName.AerialDodge,
- ItemName.Glide,
- },
+exclusion_item_table = {
"StatUps": {
ItemName.MaxHPUp,
ItemName.MaxMPUp,
@@ -951,59 +607,64 @@ class ItemData(typing.NamedTuple):
ItemName.AccessorySlotUp,
ItemName.ItemSlotUp,
},
+ "Ability": {
+ item_name for keys in [SupportAbility_Table.keys(), ActionAbility_Table.keys(), Movement_Table.keys()] for item_name in keys
+ }
}
-item_dictionary_table = {**Reports_Table,
- **Progression_Table,
- **Forms_Table,
- **Magic_Table,
- **Armor_Table,
- **Movement_Table,
- **Staffs_Table,
- **Shields_Table,
- **Keyblade_Table,
- **Accessory_Table,
- **Usefull_Table,
- **SupportAbility_Table,
- **ActionAbility_Table,
- **Items_Table,
- **Misc_Table,
- **Items_Table,
- **DonaldAbility_Table,
- **GoofyAbility_Table,
- }
-
-lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_dictionary_table.items() if
- data.code}
-
-item_groups: typing.Dict[str, list] = {"Drive Form": [item_name for item_name in Forms_Table.keys()],
- "Growth": [item_name for item_name in Movement_Table.keys()],
- "Donald Limit": [ItemName.FlareForce, ItemName.Fantasia],
- "Goofy Limit": [ItemName.Teamwork, ItemName.TornadoFusion],
- "Magic": [ItemName.FireElement, ItemName.BlizzardElement,
- ItemName.ThunderElement,
- ItemName.CureElement, ItemName.MagnetElement,
- ItemName.ReflectElement],
- "Summon": [ItemName.ChickenLittle, ItemName.Genie, ItemName.Stitch,
- ItemName.PeterPan],
- "Gap Closer": [ItemName.SlideDash, ItemName.FlashStep],
- "Ground Finisher": [ItemName.GuardBreak, ItemName.Explosion,
- ItemName.FinishingLeap],
- "Visit Lock": [item_name for item_name in
- Progression_Dicts["2VisitLocking"]],
- "Keyblade": [item_name for item_name in Keyblade_Table.keys()],
- "Fire": [ItemName.FireElement],
- "Blizzard": [ItemName.BlizzardElement],
- "Thunder": [ItemName.ThunderElement],
- "Cure": [ItemName.CureElement],
- "Magnet": [ItemName.MagnetElement],
- "Reflect": [ItemName.ReflectElement],
- "Proof": [ItemName.ProofofNonexistence, ItemName.ProofofPeace,
- ItemName.ProofofConnection],
- "Filler": [
- ItemName.PowerBoost, ItemName.MagicBoost,
- ItemName.DefenseBoost, ItemName.APBoost]
- }
-
-# lookup_kh2id_to_name: typing.Dict[int, str] = {data.kh2id: item_name for item_name, data in
-# item_dictionary_table.items() if data.kh2id}
+default_itempool_option = {
+ item_name: ItemData.quantity for dic in [Magic_Table, Progression_Table, Summon_Table, Movement_Table, Forms_Table] for item_name, ItemData in dic.items()
+}
+item_dictionary_table = {
+ **Reports_Table,
+ **Progression_Table,
+ **Forms_Table,
+ **Magic_Table,
+ **Summon_Table,
+ **Armor_Table,
+ **Movement_Table,
+ **Staffs_Table,
+ **Shields_Table,
+ **Keyblade_Table,
+ **Accessory_Table,
+ **Usefull_Table,
+ **SupportAbility_Table,
+ **ActionAbility_Table,
+ **Boosts_Table,
+ **Wincon_Table,
+ **Boosts_Table,
+ **DonaldAbility_Table,
+ **GoofyAbility_Table,
+ **Consumable_Table
+}
+filler_items = [ItemName.PowerBoost, ItemName.MagicBoost, ItemName.DefenseBoost, ItemName.APBoost,
+ ItemName.Potion, ItemName.HiPotion, ItemName.Ether, ItemName.Elixir, ItemName.Megalixir,
+ ItemName.Tent, ItemName.DriveRecovery, ItemName.HighDriveRecovery,
+ ]
+item_groups: typing.Dict[str, list] = {
+ "Drive Form": [item_name for item_name in Forms_Table.keys()],
+ "Growth": [item_name for item_name in Movement_Table.keys()],
+ "Donald Limit": [ItemName.FlareForce, ItemName.Fantasia],
+ "Goofy Limit": [ItemName.Teamwork, ItemName.TornadoFusion],
+ "Magic": [ItemName.FireElement, ItemName.BlizzardElement,
+ ItemName.ThunderElement,
+ ItemName.CureElement, ItemName.MagnetElement,
+ ItemName.ReflectElement],
+ "Summon": [ItemName.ChickenLittle, ItemName.Genie, ItemName.Stitch,
+ ItemName.PeterPan],
+ "Gap Closer": [ItemName.SlideDash, ItemName.FlashStep],
+ "Ground Finisher": [ItemName.GuardBreak, ItemName.Explosion,
+ ItemName.FinishingLeap],
+ "Visit Lock": [item_name for item_name in
+ visit_locking_dict["2VisitLocking"]],
+ "Keyblade": [item_name for item_name in Keyblade_Table.keys()],
+ "Fire": [ItemName.FireElement],
+ "Blizzard": [ItemName.BlizzardElement],
+ "Thunder": [ItemName.ThunderElement],
+ "Cure": [ItemName.CureElement],
+ "Magnet": [ItemName.MagnetElement],
+ "Reflect": [ItemName.ReflectElement],
+ "Proof": [ItemName.ProofofNonexistence, ItemName.ProofofPeace,
+ ItemName.ProofofConnection],
+ "hitlist": [ItemName.Bounty],
+}
diff --git a/worlds/kh2/Locations.py b/worlds/kh2/Locations.py
index 9046dfc67be5..100e971e7dff 100644
--- a/worlds/kh2/Locations.py
+++ b/worlds/kh2/Locations.py
@@ -1,967 +1,1079 @@
import typing
from BaseClasses import Location
-from .Names import LocationName, RegionName, ItemName
-
-
-class KH2Location(Location):
- game: str = "Kingdom Hearts 2"
-
-
-class LocationData(typing.NamedTuple):
- code: typing.Optional[int]
- locid: int
- yml: str
- charName: str = "Sora"
- charNumber: int = 1
-
+from .Names import LocationName, ItemName, RegionName
+from .Subclasses import LocationData
+from .Regions import KH2REGIONS
# data's addrcheck sys3 addr obtained roomid bit index is eventid
LoD_Checks = {
- LocationName.BambooGroveDarkShard: LocationData(0x130000, 245, "Chest"),
- LocationName.BambooGroveEther: LocationData(0x130001, 497, "Chest"),
- LocationName.BambooGroveMythrilShard: LocationData(0x130002, 498, "Chest"),
- LocationName.EncampmentAreaMap: LocationData(0x130003, 350, "Chest"),
- LocationName.Mission3: LocationData(0x130004, 417, "Chest"),
- LocationName.CheckpointHiPotion: LocationData(0x130005, 21, "Chest"),
- LocationName.CheckpointMythrilShard: LocationData(0x130006, 121, "Chest"),
- LocationName.MountainTrailLightningShard: LocationData(0x130007, 22, "Chest"),
- LocationName.MountainTrailRecoveryRecipe: LocationData(0x130008, 23, "Chest"),
- LocationName.MountainTrailEther: LocationData(0x130009, 122, "Chest"),
- LocationName.MountainTrailMythrilShard: LocationData(0x13000A, 123, "Chest"),
- LocationName.VillageCaveAreaMap: LocationData(0x13000B, 495, "Chest"),
- LocationName.VillageCaveDarkShard: LocationData(0x13000C, 125, "Chest"),
- LocationName.VillageCaveAPBoost: LocationData(0x13000D, 124, "Chest"),
- LocationName.VillageCaveBonus: LocationData(0x13000E, 43, "Get Bonus"),
- LocationName.RidgeFrostShard: LocationData(0x13000F, 24, "Chest"),
- LocationName.RidgeAPBoost: LocationData(0x130010, 126, "Chest"),
- LocationName.ShanYu: LocationData(0x130011, 9, "Double Get Bonus"),
- LocationName.ShanYuGetBonus: LocationData(0x130012, 9, "Second Get Bonus"),
- LocationName.HiddenDragon: LocationData(0x130013, 257, "Chest"),
-
-}
-LoD2_Checks = {
- LocationName.ThroneRoomTornPages: LocationData(0x130014, 25, "Chest"),
- LocationName.ThroneRoomPalaceMap: LocationData(0x130015, 127, "Chest"),
- LocationName.ThroneRoomAPBoost: LocationData(0x130016, 26, "Chest"),
- LocationName.ThroneRoomQueenRecipe: LocationData(0x130017, 27, "Chest"),
- LocationName.ThroneRoomAPBoost2: LocationData(0x130018, 128, "Chest"),
- LocationName.ThroneRoomOgreShield: LocationData(0x130019, 129, "Chest"),
- LocationName.ThroneRoomMythrilCrystal: LocationData(0x13001A, 130, "Chest"),
- LocationName.ThroneRoomOrichalcum: LocationData(0x13001B, 131, "Chest"),
- LocationName.StormRider: LocationData(0x13001C, 10, "Get Bonus"),
- LocationName.XigbarDataDefenseBoost: LocationData(0x13001D, 555, "Chest"),
+ LocationName.BambooGroveDarkShard: LocationData(245, "Chest"),
+ LocationName.BambooGroveEther: LocationData(497, "Chest"),
+ LocationName.BambooGroveMythrilShard: LocationData(498, "Chest"),
+ LocationName.EncampmentAreaMap: LocationData(350, "Chest"),
+ LocationName.Mission3: LocationData(417, "Chest"),
+ LocationName.CheckpointHiPotion: LocationData(21, "Chest"),
+ LocationName.CheckpointMythrilShard: LocationData(121, "Chest"),
+ LocationName.MountainTrailLightningShard: LocationData(22, "Chest"),
+ LocationName.MountainTrailRecoveryRecipe: LocationData(23, "Chest"),
+ LocationName.MountainTrailEther: LocationData(122, "Chest"),
+ LocationName.MountainTrailMythrilShard: LocationData(123, "Chest"),
+ LocationName.VillageCaveAreaMap: LocationData(495, "Chest"),
+ LocationName.VillageCaveDarkShard: LocationData(125, "Chest"),
+ LocationName.VillageCaveAPBoost: LocationData(124, "Chest"),
+ LocationName.VillageCaveBonus: LocationData(43, "Get Bonus"),
+ LocationName.RidgeFrostShard: LocationData(24, "Chest"),
+ LocationName.RidgeAPBoost: LocationData(126, "Chest"),
+ LocationName.ShanYu: LocationData(9, "Double Get Bonus"),
+ LocationName.ShanYuGetBonus: LocationData(9, "Second Get Bonus"),
+ LocationName.HiddenDragon: LocationData(257, "Chest"),
+ LocationName.ThroneRoomTornPages: LocationData(25, "Chest"),
+ LocationName.ThroneRoomPalaceMap: LocationData(127, "Chest"),
+ LocationName.ThroneRoomAPBoost: LocationData(26, "Chest"),
+ LocationName.ThroneRoomQueenRecipe: LocationData(27, "Chest"),
+ LocationName.ThroneRoomAPBoost2: LocationData(128, "Chest"),
+ LocationName.ThroneRoomOgreShield: LocationData(129, "Chest"),
+ LocationName.ThroneRoomMythrilCrystal: LocationData(130, "Chest"),
+ LocationName.ThroneRoomOrichalcum: LocationData(131, "Chest"),
+ LocationName.StormRider: LocationData(10, "Get Bonus"),
+ LocationName.XigbarDataDefenseBoost: LocationData(555, "Chest"),
}
AG_Checks = {
- LocationName.AgrabahMap: LocationData(0x13001E, 353, "Chest"),
- LocationName.AgrabahDarkShard: LocationData(0x13001F, 28, "Chest"),
- LocationName.AgrabahMythrilShard: LocationData(0x130020, 29, "Chest"),
- LocationName.AgrabahHiPotion: LocationData(0x130021, 30, "Chest"),
- LocationName.AgrabahAPBoost: LocationData(0x130022, 132, "Chest"),
- LocationName.AgrabahMythrilStone: LocationData(0x130023, 133, "Chest"),
- LocationName.AgrabahMythrilShard2: LocationData(0x130024, 249, "Chest"),
- LocationName.AgrabahSerenityShard: LocationData(0x130025, 501, "Chest"),
- LocationName.BazaarMythrilGem: LocationData(0x130026, 31, "Chest"),
- LocationName.BazaarPowerShard: LocationData(0x130027, 32, "Chest"),
- LocationName.BazaarHiPotion: LocationData(0x130028, 33, "Chest"),
- LocationName.BazaarAPBoost: LocationData(0x130029, 134, "Chest"),
- LocationName.BazaarMythrilShard: LocationData(0x13002A, 135, "Chest"),
- LocationName.PalaceWallsSkillRing: LocationData(0x13002B, 136, "Chest"),
- LocationName.PalaceWallsMythrilStone: LocationData(0x13002C, 520, "Chest"),
- LocationName.CaveEntrancePowerStone: LocationData(0x13002D, 250, "Chest"),
- LocationName.CaveEntranceMythrilShard: LocationData(0x13002E, 251, "Chest"),
- LocationName.ValleyofStoneMythrilStone: LocationData(0x13002F, 35, "Chest"),
- LocationName.ValleyofStoneAPBoost: LocationData(0x130030, 36, "Chest"),
- LocationName.ValleyofStoneMythrilShard: LocationData(0x130031, 137, "Chest"),
- LocationName.ValleyofStoneHiPotion: LocationData(0x130032, 138, "Chest"),
- LocationName.AbuEscort: LocationData(0x130033, 42, "Get Bonus"),
- LocationName.ChasmofChallengesCaveofWondersMap: LocationData(0x130034, 487, "Chest"),
- LocationName.ChasmofChallengesAPBoost: LocationData(0x130035, 37, "Chest"),
- LocationName.TreasureRoom: LocationData(0x130036, 46, "Get Bonus"),
- LocationName.TreasureRoomAPBoost: LocationData(0x130037, 502, "Chest"),
- LocationName.TreasureRoomSerenityGem: LocationData(0x130038, 503, "Chest"),
- LocationName.ElementalLords: LocationData(0x130039, 37, "Get Bonus"),
- LocationName.LampCharm: LocationData(0x13003A, 300, "Chest"),
-
-}
-AG2_Checks = {
- LocationName.RuinedChamberTornPages: LocationData(0x13003B, 34, "Chest"),
- LocationName.RuinedChamberRuinsMap: LocationData(0x13003C, 486, "Chest"),
- LocationName.GenieJafar: LocationData(0x13003D, 15, "Get Bonus"),
- LocationName.WishingLamp: LocationData(0x13003E, 303, "Chest"),
- LocationName.LexaeusBonus: LocationData(0x13003F, 65, "Get Bonus"),
- LocationName.LexaeusASStrengthBeyondStrength: LocationData(0x130040, 545, "Chest"),
- LocationName.LexaeusDataLostIllusion: LocationData(0x130041, 550, "Chest"),
+ LocationName.AgrabahMap: LocationData(353, "Chest"),
+ LocationName.AgrabahDarkShard: LocationData(28, "Chest"),
+ LocationName.AgrabahMythrilShard: LocationData(29, "Chest"),
+ LocationName.AgrabahHiPotion: LocationData(30, "Chest"),
+ LocationName.AgrabahAPBoost: LocationData(132, "Chest"),
+ LocationName.AgrabahMythrilStone: LocationData(133, "Chest"),
+ LocationName.AgrabahMythrilShard2: LocationData(249, "Chest"),
+ LocationName.AgrabahSerenityShard: LocationData(501, "Chest"),
+ LocationName.BazaarMythrilGem: LocationData(31, "Chest"),
+ LocationName.BazaarPowerShard: LocationData(32, "Chest"),
+ LocationName.BazaarHiPotion: LocationData(33, "Chest"),
+ LocationName.BazaarAPBoost: LocationData(134, "Chest"),
+ LocationName.BazaarMythrilShard: LocationData(135, "Chest"),
+ LocationName.PalaceWallsSkillRing: LocationData(136, "Chest"),
+ LocationName.PalaceWallsMythrilStone: LocationData(520, "Chest"),
+ LocationName.CaveEntrancePowerStone: LocationData(250, "Chest"),
+ LocationName.CaveEntranceMythrilShard: LocationData(251, "Chest"),
+ LocationName.ValleyofStoneMythrilStone: LocationData(35, "Chest"),
+ LocationName.ValleyofStoneAPBoost: LocationData(36, "Chest"),
+ LocationName.ValleyofStoneMythrilShard: LocationData(137, "Chest"),
+ LocationName.ValleyofStoneHiPotion: LocationData(138, "Chest"),
+ LocationName.AbuEscort: LocationData(42, "Get Bonus"),
+ LocationName.ChasmofChallengesCaveofWondersMap: LocationData(487, "Chest"),
+ LocationName.ChasmofChallengesAPBoost: LocationData(37, "Chest"),
+ LocationName.TreasureRoom: LocationData(46, "Get Bonus"),
+ LocationName.TreasureRoomAPBoost: LocationData(502, "Chest"),
+ LocationName.TreasureRoomSerenityGem: LocationData(503, "Chest"),
+ LocationName.ElementalLords: LocationData(37, "Get Bonus"),
+ LocationName.LampCharm: LocationData(300, "Chest"),
+ LocationName.RuinedChamberTornPages: LocationData(34, "Chest"),
+ LocationName.RuinedChamberRuinsMap: LocationData(486, "Chest"),
+ LocationName.GenieJafar: LocationData(15, "Get Bonus"),
+ LocationName.WishingLamp: LocationData(303, "Chest"),
+ LocationName.LexaeusBonus: LocationData(65, "Get Bonus"),
+ LocationName.LexaeusASStrengthBeyondStrength: LocationData(545, "Chest"),
+ LocationName.LexaeusDataLostIllusion: LocationData(550, "Chest"),
}
DC_Checks = {
- LocationName.DCCourtyardMythrilShard: LocationData(0x130042, 16, "Chest"),
- LocationName.DCCourtyardStarRecipe: LocationData(0x130043, 17, "Chest"),
- LocationName.DCCourtyardAPBoost: LocationData(0x130044, 18, "Chest"),
- LocationName.DCCourtyardMythrilStone: LocationData(0x130045, 92, "Chest"),
- LocationName.DCCourtyardBlazingStone: LocationData(0x130046, 93, "Chest"),
- LocationName.DCCourtyardBlazingShard: LocationData(0x130047, 247, "Chest"),
- LocationName.DCCourtyardMythrilShard2: LocationData(0x130048, 248, "Chest"),
- LocationName.LibraryTornPages: LocationData(0x130049, 91, "Chest"),
- LocationName.DisneyCastleMap: LocationData(0x13004A, 332, "Chest"),
- LocationName.MinnieEscort: LocationData(0x13004B, 38, "Double Get Bonus"),
- LocationName.MinnieEscortGetBonus: LocationData(0x13004C, 38, "Second Get Bonus"),
+ LocationName.DCCourtyardMythrilShard: LocationData(16, "Chest"),
+ LocationName.DCCourtyardStarRecipe: LocationData(17, "Chest"),
+ LocationName.DCCourtyardAPBoost: LocationData(18, "Chest"),
+ LocationName.DCCourtyardMythrilStone: LocationData(92, "Chest"),
+ LocationName.DCCourtyardBlazingStone: LocationData(93, "Chest"),
+ LocationName.DCCourtyardBlazingShard: LocationData(247, "Chest"),
+ LocationName.DCCourtyardMythrilShard2: LocationData(248, "Chest"),
+ LocationName.LibraryTornPages: LocationData(91, "Chest"),
+ LocationName.DisneyCastleMap: LocationData(332, "Chest"),
+ LocationName.MinnieEscort: LocationData(38, "Double Get Bonus"),
+ LocationName.MinnieEscortGetBonus: LocationData(38, "Second Get Bonus"),
+ LocationName.CornerstoneHillMap: LocationData(79, "Chest"),
+ LocationName.CornerstoneHillFrostShard: LocationData(12, "Chest"),
+ LocationName.PierMythrilShard: LocationData(81, "Chest"),
+ LocationName.PierHiPotion: LocationData(82, "Chest"),
+ LocationName.WaterwayMythrilStone: LocationData(83, "Chest"),
+ LocationName.WaterwayAPBoost: LocationData(84, "Chest"),
+ LocationName.WaterwayFrostStone: LocationData(85, "Chest"),
+ LocationName.WindowofTimeMap: LocationData(368, "Chest"),
+ LocationName.BoatPete: LocationData(16, "Get Bonus"),
+ LocationName.FuturePete: LocationData(17, "Double Get Bonus"),
+ LocationName.FuturePeteGetBonus: LocationData(17, "Second Get Bonus"),
+ LocationName.Monochrome: LocationData(261, "Chest"),
+ LocationName.WisdomForm: LocationData(262, "Chest"),
+ LocationName.MarluxiaGetBonus: LocationData(67, "Get Bonus"),
+ LocationName.MarluxiaASEternalBlossom: LocationData(548, "Chest"),
+ LocationName.MarluxiaDataLostIllusion: LocationData(553, "Chest"),
+ LocationName.LingeringWillBonus: LocationData(70, "Get Bonus"),
+ LocationName.LingeringWillProofofConnection: LocationData(587, "Chest"),
+ LocationName.LingeringWillManifestIllusion: LocationData(591, "Chest"),
}
-TR_Checks = {
- LocationName.CornerstoneHillMap: LocationData(0x13004D, 79, "Chest"),
- LocationName.CornerstoneHillFrostShard: LocationData(0x13004E, 12, "Chest"),
- LocationName.PierMythrilShard: LocationData(0x13004F, 81, "Chest"),
- LocationName.PierHiPotion: LocationData(0x130050, 82, "Chest"),
- LocationName.WaterwayMythrilStone: LocationData(0x130051, 83, "Chest"),
- LocationName.WaterwayAPBoost: LocationData(0x130052, 84, "Chest"),
- LocationName.WaterwayFrostStone: LocationData(0x130053, 85, "Chest"),
- LocationName.WindowofTimeMap: LocationData(0x130054, 368, "Chest"),
- LocationName.BoatPete: LocationData(0x130055, 16, "Get Bonus"),
- LocationName.FuturePete: LocationData(0x130056, 17, "Double Get Bonus"),
- LocationName.FuturePeteGetBonus: LocationData(0x130057, 17, "Second Get Bonus"),
- LocationName.Monochrome: LocationData(0x130058, 261, "Chest"),
- LocationName.WisdomForm: LocationData(0x130059, 262, "Chest"),
- LocationName.MarluxiaGetBonus: LocationData(0x13005A, 67, "Get Bonus"),
- LocationName.MarluxiaASEternalBlossom: LocationData(0x13005B, 548, "Chest"),
- LocationName.MarluxiaDataLostIllusion: LocationData(0x13005C, 553, "Chest"),
- LocationName.LingeringWillBonus: LocationData(0x13005D, 70, "Get Bonus"),
- LocationName.LingeringWillProofofConnection: LocationData(0x13005E, 587, "Chest"),
- LocationName.LingeringWillManifestIllusion: LocationData(0x13005F, 591, "Chest"),
-}
-# the mismatch might be here
-HundredAcre1_Checks = {
- LocationName.PoohsHouse100AcreWoodMap: LocationData(0x130060, 313, "Chest"),
- LocationName.PoohsHouseAPBoost: LocationData(0x130061, 97, "Chest"),
- LocationName.PoohsHouseMythrilStone: LocationData(0x130062, 98, "Chest"),
-}
-HundredAcre2_Checks = {
- LocationName.PigletsHouseDefenseBoost: LocationData(0x130063, 105, "Chest"),
- LocationName.PigletsHouseAPBoost: LocationData(0x130064, 103, "Chest"),
- LocationName.PigletsHouseMythrilGem: LocationData(0x130065, 104, "Chest"),
-}
-HundredAcre3_Checks = {
- LocationName.RabbitsHouseDrawRing: LocationData(0x130066, 314, "Chest"),
- LocationName.RabbitsHouseMythrilCrystal: LocationData(0x130067, 100, "Chest"),
- LocationName.RabbitsHouseAPBoost: LocationData(0x130068, 101, "Chest"),
-}
-HundredAcre4_Checks = {
- LocationName.KangasHouseMagicBoost: LocationData(0x130069, 108, "Chest"),
- LocationName.KangasHouseAPBoost: LocationData(0x13006A, 106, "Chest"),
- LocationName.KangasHouseOrichalcum: LocationData(0x13006B, 107, "Chest"),
-}
-HundredAcre5_Checks = {
- LocationName.SpookyCaveMythrilGem: LocationData(0x13006C, 110, "Chest"),
- LocationName.SpookyCaveAPBoost: LocationData(0x13006D, 111, "Chest"),
- LocationName.SpookyCaveOrichalcum: LocationData(0x13006E, 112, "Chest"),
- LocationName.SpookyCaveGuardRecipe: LocationData(0x13006F, 113, "Chest"),
- LocationName.SpookyCaveMythrilCrystal: LocationData(0x130070, 115, "Chest"),
- LocationName.SpookyCaveAPBoost2: LocationData(0x130071, 116, "Chest"),
- LocationName.SweetMemories: LocationData(0x130072, 284, "Chest"),
- LocationName.SpookyCaveMap: LocationData(0x130073, 485, "Chest"),
-}
-HundredAcre6_Checks = {
- LocationName.StarryHillCosmicRing: LocationData(0x130074, 312, "Chest"),
- LocationName.StarryHillStyleRecipe: LocationData(0x130075, 94, "Chest"),
- LocationName.StarryHillCureElement: LocationData(0x130076, 285, "Chest"),
- LocationName.StarryHillOrichalcumPlus: LocationData(0x130077, 539, "Chest"),
+HundredAcre_Checks = {
+ LocationName.PoohsHouse100AcreWoodMap: LocationData(313, "Chest"),
+ LocationName.PoohsHouseAPBoost: LocationData(97, "Chest"),
+ LocationName.PoohsHouseMythrilStone: LocationData(98, "Chest"),
+ LocationName.PigletsHouseDefenseBoost: LocationData(105, "Chest"),
+ LocationName.PigletsHouseAPBoost: LocationData(103, "Chest"),
+ LocationName.PigletsHouseMythrilGem: LocationData(104, "Chest"),
+ LocationName.RabbitsHouseDrawRing: LocationData(314, "Chest"),
+ LocationName.RabbitsHouseMythrilCrystal: LocationData(100, "Chest"),
+ LocationName.RabbitsHouseAPBoost: LocationData(101, "Chest"),
+ LocationName.KangasHouseMagicBoost: LocationData(108, "Chest"),
+ LocationName.KangasHouseAPBoost: LocationData(106, "Chest"),
+ LocationName.KangasHouseOrichalcum: LocationData(107, "Chest"),
+ LocationName.SpookyCaveMythrilGem: LocationData(110, "Chest"),
+ LocationName.SpookyCaveAPBoost: LocationData(111, "Chest"),
+ LocationName.SpookyCaveOrichalcum: LocationData(112, "Chest"),
+ LocationName.SpookyCaveGuardRecipe: LocationData(113, "Chest"),
+ LocationName.SpookyCaveMythrilCrystal: LocationData(115, "Chest"),
+ LocationName.SpookyCaveAPBoost2: LocationData(116, "Chest"),
+ LocationName.SweetMemories: LocationData(284, "Chest"),
+ LocationName.SpookyCaveMap: LocationData(485, "Chest"),
+ LocationName.StarryHillCosmicRing: LocationData(312, "Chest"),
+ LocationName.StarryHillStyleRecipe: LocationData(94, "Chest"),
+ LocationName.StarryHillCureElement: LocationData(285, "Chest"),
+ LocationName.StarryHillOrichalcumPlus: LocationData(539, "Chest"),
}
Oc_Checks = {
- LocationName.PassageMythrilShard: LocationData(0x130078, 7, "Chest"),
- LocationName.PassageMythrilStone: LocationData(0x130079, 8, "Chest"),
- LocationName.PassageEther: LocationData(0x13007A, 144, "Chest"),
- LocationName.PassageAPBoost: LocationData(0x13007B, 145, "Chest"),
- LocationName.PassageHiPotion: LocationData(0x13007C, 146, "Chest"),
- LocationName.InnerChamberUnderworldMap: LocationData(0x13007D, 2, "Chest"),
- LocationName.InnerChamberMythrilShard: LocationData(0x13007E, 243, "Chest"),
- LocationName.Cerberus: LocationData(0x13007F, 5, "Get Bonus"),
- LocationName.ColiseumMap: LocationData(0x130080, 338, "Chest"),
- LocationName.Urns: LocationData(0x130081, 57, "Get Bonus"),
- LocationName.UnderworldEntrancePowerBoost: LocationData(0x130082, 242, "Chest"),
- LocationName.CavernsEntranceLucidShard: LocationData(0x130083, 3, "Chest"),
- LocationName.CavernsEntranceAPBoost: LocationData(0x130084, 11, "Chest"),
- LocationName.CavernsEntranceMythrilShard: LocationData(0x130085, 504, "Chest"),
- LocationName.TheLostRoadBrightShard: LocationData(0x130086, 9, "Chest"),
- LocationName.TheLostRoadEther: LocationData(0x130087, 10, "Chest"),
- LocationName.TheLostRoadMythrilShard: LocationData(0x130088, 148, "Chest"),
- LocationName.TheLostRoadMythrilStone: LocationData(0x130089, 149, "Chest"),
- LocationName.AtriumLucidStone: LocationData(0x13008A, 150, "Chest"),
- LocationName.AtriumAPBoost: LocationData(0x13008B, 151, "Chest"),
- LocationName.DemyxOC: LocationData(0x13008C, 58, "Get Bonus"),
- LocationName.SecretAnsemReport5: LocationData(0x13008D, 529, "Chest"),
- LocationName.OlympusStone: LocationData(0x13008E, 293, "Chest"),
- LocationName.TheLockCavernsMap: LocationData(0x13008F, 244, "Chest"),
- LocationName.TheLockMythrilShard: LocationData(0x130090, 5, "Chest"),
- LocationName.TheLockAPBoost: LocationData(0x130091, 142, "Chest"),
- LocationName.PeteOC: LocationData(0x130092, 6, "Get Bonus"),
- LocationName.Hydra: LocationData(0x130093, 7, "Double Get Bonus"),
- LocationName.HydraGetBonus: LocationData(0x130094, 7, "Second Get Bonus"),
- LocationName.HerosCrest: LocationData(0x130095, 260, "Chest"),
-
-}
-Oc2_Checks = {
- LocationName.AuronsStatue: LocationData(0x130096, 295, "Chest"),
- LocationName.Hades: LocationData(0x130097, 8, "Double Get Bonus"),
- LocationName.HadesGetBonus: LocationData(0x130098, 8, "Second Get Bonus"),
- LocationName.GuardianSoul: LocationData(0x130099, 272, "Chest"),
- LocationName.ZexionBonus: LocationData(0x13009A, 66, "Get Bonus"),
- LocationName.ZexionASBookofShadows: LocationData(0x13009B, 546, "Chest"),
- LocationName.ZexionDataLostIllusion: LocationData(0x13009C, 551, "Chest"),
-}
-Oc2Cups = {
- LocationName.ProtectBeltPainandPanicCup: LocationData(0x13009D, 513, "Chest"),
- LocationName.SerenityGemPainandPanicCup: LocationData(0x13009E, 540, "Chest"),
- LocationName.RisingDragonCerberusCup: LocationData(0x13009F, 515, "Chest"),
- LocationName.SerenityCrystalCerberusCup: LocationData(0x1300A0, 542, "Chest"),
- LocationName.GenjiShieldTitanCup: LocationData(0x1300A1, 514, "Chest"),
- LocationName.SkillfulRingTitanCup: LocationData(0x1300A2, 541, "Chest"),
- LocationName.FatalCrestGoddessofFateCup: LocationData(0x1300A3, 516, "Chest"),
- LocationName.OrichalcumPlusGoddessofFateCup: LocationData(0x1300A4, 517, "Chest"),
- LocationName.HadesCupTrophyParadoxCups: LocationData(0x1300A5, 518, "Chest"),
+ LocationName.PassageMythrilShard: LocationData(7, "Chest"),
+ LocationName.PassageMythrilStone: LocationData(8, "Chest"),
+ LocationName.PassageEther: LocationData(144, "Chest"),
+ LocationName.PassageAPBoost: LocationData(145, "Chest"),
+ LocationName.PassageHiPotion: LocationData(146, "Chest"),
+ LocationName.InnerChamberUnderworldMap: LocationData(2, "Chest"),
+ LocationName.InnerChamberMythrilShard: LocationData(243, "Chest"),
+ LocationName.Cerberus: LocationData(5, "Get Bonus"),
+ LocationName.ColiseumMap: LocationData(338, "Chest"),
+ LocationName.Urns: LocationData(57, "Get Bonus"),
+ LocationName.UnderworldEntrancePowerBoost: LocationData(242, "Chest"),
+ LocationName.CavernsEntranceLucidShard: LocationData(3, "Chest"),
+ LocationName.CavernsEntranceAPBoost: LocationData(11, "Chest"),
+ LocationName.CavernsEntranceMythrilShard: LocationData(504, "Chest"),
+ LocationName.TheLostRoadBrightShard: LocationData(9, "Chest"),
+ LocationName.TheLostRoadEther: LocationData(10, "Chest"),
+ LocationName.TheLostRoadMythrilShard: LocationData(148, "Chest"),
+ LocationName.TheLostRoadMythrilStone: LocationData(149, "Chest"),
+ LocationName.AtriumLucidStone: LocationData(150, "Chest"),
+ LocationName.AtriumAPBoost: LocationData(151, "Chest"),
+ LocationName.DemyxOC: LocationData(58, "Get Bonus"),
+ LocationName.SecretAnsemReport5: LocationData(529, "Chest"),
+ LocationName.OlympusStone: LocationData(293, "Chest"),
+ LocationName.TheLockCavernsMap: LocationData(244, "Chest"),
+ LocationName.TheLockMythrilShard: LocationData(5, "Chest"),
+ LocationName.TheLockAPBoost: LocationData(142, "Chest"),
+ LocationName.PeteOC: LocationData(6, "Get Bonus"),
+ LocationName.Hydra: LocationData(7, "Double Get Bonus"),
+ LocationName.HydraGetBonus: LocationData(7, "Second Get Bonus"),
+ LocationName.HerosCrest: LocationData(260, "Chest"),
+ LocationName.AuronsStatue: LocationData(295, "Chest"),
+ LocationName.Hades: LocationData(8, "Double Get Bonus"),
+ LocationName.HadesGetBonus: LocationData(8, "Second Get Bonus"),
+ LocationName.GuardianSoul: LocationData(272, "Chest"),
+ LocationName.ZexionBonus: LocationData(66, "Get Bonus"),
+ LocationName.ZexionASBookofShadows: LocationData(546, "Chest"),
+ LocationName.ZexionDataLostIllusion: LocationData(551, "Chest"),
+ LocationName.ProtectBeltPainandPanicCup: LocationData(513, "Chest"),
+ LocationName.SerenityGemPainandPanicCup: LocationData(540, "Chest"),
+ LocationName.RisingDragonCerberusCup: LocationData(515, "Chest"),
+ LocationName.SerenityCrystalCerberusCup: LocationData(542, "Chest"),
+ LocationName.GenjiShieldTitanCup: LocationData(514, "Chest"),
+ LocationName.SkillfulRingTitanCup: LocationData(541, "Chest"),
+ LocationName.FatalCrestGoddessofFateCup: LocationData(516, "Chest"),
+ LocationName.OrichalcumPlusGoddessofFateCup: LocationData(517, "Chest"),
+ LocationName.HadesCupTrophyParadoxCups: LocationData(518, "Chest"),
}
BC_Checks = {
- LocationName.BCCourtyardAPBoost: LocationData(0x1300A6, 39, "Chest"),
- LocationName.BCCourtyardHiPotion: LocationData(0x1300A7, 40, "Chest"),
- LocationName.BCCourtyardMythrilShard: LocationData(0x1300A8, 505, "Chest"),
- LocationName.BellesRoomCastleMap: LocationData(0x1300A9, 46, "Chest"),
- LocationName.BellesRoomMegaRecipe: LocationData(0x1300AA, 240, "Chest"),
- LocationName.TheEastWingMythrilShard: LocationData(0x1300AB, 63, "Chest"),
- LocationName.TheEastWingTent: LocationData(0x1300AC, 155, "Chest"),
- LocationName.TheWestHallHiPotion: LocationData(0x1300AD, 41, "Chest"),
- LocationName.TheWestHallPowerShard: LocationData(0x1300AE, 207, "Chest"),
- LocationName.TheWestHallAPBoostPostDungeon: LocationData(0x1300AF, 158, "Chest"),
- LocationName.TheWestHallBrightStone: LocationData(0x1300B0, 159, "Chest"),
- LocationName.TheWestHallMythrilShard: LocationData(0x1300B1, 206, "Chest"),
- LocationName.Thresholder: LocationData(0x1300B2, 2, "Get Bonus"),
- LocationName.DungeonBasementMap: LocationData(0x1300B3, 239, "Chest"),
- LocationName.DungeonAPBoost: LocationData(0x1300B4, 43, "Chest"),
- LocationName.SecretPassageMythrilShard: LocationData(0x1300B5, 44, "Chest"),
- LocationName.SecretPassageHiPotion: LocationData(0x1300B6, 168, "Chest"),
- LocationName.SecretPassageLucidShard: LocationData(0x1300B7, 45, "Chest"),
- LocationName.TheWestHallMythrilShard2: LocationData(0x1300B8, 208, "Chest"),
- LocationName.TheWestWingMythrilShard: LocationData(0x1300B9, 42, "Chest"),
- LocationName.TheWestWingTent: LocationData(0x1300BA, 164, "Chest"),
- LocationName.Beast: LocationData(0x1300BB, 12, "Get Bonus"),
- LocationName.TheBeastsRoomBlazingShard: LocationData(0x1300BC, 241, "Chest"),
- LocationName.DarkThorn: LocationData(0x1300BD, 3, "Double Get Bonus"),
- LocationName.DarkThornGetBonus: LocationData(0x1300BE, 3, "Second Get Bonus"),
- LocationName.DarkThornCureElement: LocationData(0x1300BF, 299, "Chest"),
-
-}
-BC2_Checks = {
- LocationName.RumblingRose: LocationData(0x1300C0, 270, "Chest"),
- LocationName.CastleWallsMap: LocationData(0x1300C1, 325, "Chest"),
- LocationName.Xaldin: LocationData(0x1300C2, 4, "Double Get Bonus"),
- LocationName.XaldinGetBonus: LocationData(0x1300C3, 4, "Second Get Bonus"),
- LocationName.SecretAnsemReport4: LocationData(0x1300C4, 528, "Chest"),
- LocationName.XaldinDataDefenseBoost: LocationData(0x1300C5, 559, "Chest"),
+ LocationName.BCCourtyardAPBoost: LocationData(39, "Chest"),
+ LocationName.BCCourtyardHiPotion: LocationData(40, "Chest"),
+ LocationName.BCCourtyardMythrilShard: LocationData(505, "Chest"),
+ LocationName.BellesRoomCastleMap: LocationData(46, "Chest"),
+ LocationName.BellesRoomMegaRecipe: LocationData(240, "Chest"),
+ LocationName.TheEastWingMythrilShard: LocationData(63, "Chest"),
+ LocationName.TheEastWingTent: LocationData(155, "Chest"),
+ LocationName.TheWestHallHiPotion: LocationData(41, "Chest"),
+ LocationName.TheWestHallPowerShard: LocationData(207, "Chest"),
+ LocationName.TheWestHallAPBoostPostDungeon: LocationData(158, "Chest"),
+ LocationName.TheWestHallBrightStone: LocationData(159, "Chest"),
+ LocationName.TheWestHallMythrilShard: LocationData(206, "Chest"),
+ LocationName.Thresholder: LocationData(2, "Get Bonus"),
+ LocationName.DungeonBasementMap: LocationData(239, "Chest"),
+ LocationName.DungeonAPBoost: LocationData(43, "Chest"),
+ LocationName.SecretPassageMythrilShard: LocationData(44, "Chest"),
+ LocationName.SecretPassageHiPotion: LocationData(168, "Chest"),
+ LocationName.SecretPassageLucidShard: LocationData(45, "Chest"),
+ LocationName.TheWestHallMythrilShard2: LocationData(208, "Chest"),
+ LocationName.TheWestWingMythrilShard: LocationData(42, "Chest"),
+ LocationName.TheWestWingTent: LocationData(164, "Chest"),
+ LocationName.Beast: LocationData(12, "Get Bonus"),
+ LocationName.TheBeastsRoomBlazingShard: LocationData(241, "Chest"),
+ LocationName.DarkThorn: LocationData(3, "Double Get Bonus"),
+ LocationName.DarkThornGetBonus: LocationData(3, "Second Get Bonus"),
+ LocationName.DarkThornCureElement: LocationData(299, "Chest"),
+ LocationName.RumblingRose: LocationData(270, "Chest"),
+ LocationName.CastleWallsMap: LocationData(325, "Chest"),
+ LocationName.Xaldin: LocationData(4, "Double Get Bonus"),
+ LocationName.XaldinGetBonus: LocationData(4, "Second Get Bonus"),
+ LocationName.SecretAnsemReport4: LocationData(528, "Chest"),
+ LocationName.XaldinDataDefenseBoost: LocationData(559, "Chest"),
}
SP_Checks = {
- LocationName.PitCellAreaMap: LocationData(0x1300C6, 316, "Chest"),
- LocationName.PitCellMythrilCrystal: LocationData(0x1300C7, 64, "Chest"),
- LocationName.CanyonDarkCrystal: LocationData(0x1300C8, 65, "Chest"),
- LocationName.CanyonMythrilStone: LocationData(0x1300C9, 171, "Chest"),
- LocationName.CanyonMythrilGem: LocationData(0x1300CA, 253, "Chest"),
- LocationName.CanyonFrostCrystal: LocationData(0x1300CB, 521, "Chest"),
- LocationName.Screens: LocationData(0x1300CC, 45, "Get Bonus"),
- LocationName.HallwayPowerCrystal: LocationData(0x1300CD, 49, "Chest"),
- LocationName.HallwayAPBoost: LocationData(0x1300CE, 50, "Chest"),
- LocationName.CommunicationsRoomIOTowerMap: LocationData(0x1300CF, 255, "Chest"),
- LocationName.CommunicationsRoomGaiaBelt: LocationData(0x1300D0, 499, "Chest"),
- LocationName.HostileProgram: LocationData(0x1300D1, 31, "Double Get Bonus"),
- LocationName.HostileProgramGetBonus: LocationData(0x1300D2, 31, "Second Get Bonus"),
- LocationName.PhotonDebugger: LocationData(0x1300D3, 267, "Chest"),
-
-}
-SP2_Checks = {
- LocationName.SolarSailer: LocationData(0x1300D4, 61, "Get Bonus"),
- LocationName.CentralComputerCoreAPBoost: LocationData(0x1300D5, 177, "Chest"),
- LocationName.CentralComputerCoreOrichalcumPlus: LocationData(0x1300D6, 178, "Chest"),
- LocationName.CentralComputerCoreCosmicArts: LocationData(0x1300D7, 51, "Chest"),
- LocationName.CentralComputerCoreMap: LocationData(0x1300D8, 488, "Chest"),
- LocationName.MCP: LocationData(0x1300D9, 32, "Double Get Bonus"),
- LocationName.MCPGetBonus: LocationData(0x1300DA, 32, "Second Get Bonus"),
- LocationName.LarxeneBonus: LocationData(0x1300DB, 68, "Get Bonus"),
- LocationName.LarxeneASCloakedThunder: LocationData(0x1300DC, 547, "Chest"),
- LocationName.LarxeneDataLostIllusion: LocationData(0x1300DD, 552, "Chest"),
+ LocationName.PitCellAreaMap: LocationData(316, "Chest"),
+ LocationName.PitCellMythrilCrystal: LocationData(64, "Chest"),
+ LocationName.CanyonDarkCrystal: LocationData(65, "Chest"),
+ LocationName.CanyonMythrilStone: LocationData(171, "Chest"),
+ LocationName.CanyonMythrilGem: LocationData(253, "Chest"),
+ LocationName.CanyonFrostCrystal: LocationData(521, "Chest"),
+ LocationName.Screens: LocationData(45, "Get Bonus"),
+ LocationName.HallwayPowerCrystal: LocationData(49, "Chest"),
+ LocationName.HallwayAPBoost: LocationData(50, "Chest"),
+ LocationName.CommunicationsRoomIOTowerMap: LocationData(255, "Chest"),
+ LocationName.CommunicationsRoomGaiaBelt: LocationData(499, "Chest"),
+ LocationName.HostileProgram: LocationData(31, "Double Get Bonus"),
+ LocationName.HostileProgramGetBonus: LocationData(31, "Second Get Bonus"),
+ LocationName.PhotonDebugger: LocationData(267, "Chest"),
+ LocationName.SolarSailer: LocationData(61, "Get Bonus"),
+ LocationName.CentralComputerCoreAPBoost: LocationData(177, "Chest"),
+ LocationName.CentralComputerCoreOrichalcumPlus: LocationData(178, "Chest"),
+ LocationName.CentralComputerCoreCosmicArts: LocationData(51, "Chest"),
+ LocationName.CentralComputerCoreMap: LocationData(488, "Chest"),
+ LocationName.MCP: LocationData(32, "Double Get Bonus"),
+ LocationName.MCPGetBonus: LocationData(32, "Second Get Bonus"),
+ LocationName.LarxeneBonus: LocationData(68, "Get Bonus"),
+ LocationName.LarxeneASCloakedThunder: LocationData(547, "Chest"),
+ LocationName.LarxeneDataLostIllusion: LocationData(552, "Chest"),
}
HT_Checks = {
- LocationName.GraveyardMythrilShard: LocationData(0x1300DE, 53, "Chest"),
- LocationName.GraveyardSerenityGem: LocationData(0x1300DF, 212, "Chest"),
- LocationName.FinklesteinsLabHalloweenTownMap: LocationData(0x1300E0, 211, "Chest"),
- LocationName.TownSquareMythrilStone: LocationData(0x1300E1, 209, "Chest"),
- LocationName.TownSquareEnergyShard: LocationData(0x1300E2, 210, "Chest"),
- LocationName.HinterlandsLightningShard: LocationData(0x1300E3, 54, "Chest"),
- LocationName.HinterlandsMythrilStone: LocationData(0x1300E4, 213, "Chest"),
- LocationName.HinterlandsAPBoost: LocationData(0x1300E5, 214, "Chest"),
- LocationName.CandyCaneLaneMegaPotion: LocationData(0x1300E6, 55, "Chest"),
- LocationName.CandyCaneLaneMythrilGem: LocationData(0x1300E7, 56, "Chest"),
- LocationName.CandyCaneLaneLightningStone: LocationData(0x1300E8, 216, "Chest"),
- LocationName.CandyCaneLaneMythrilStone: LocationData(0x1300E9, 217, "Chest"),
- LocationName.SantasHouseChristmasTownMap: LocationData(0x1300EA, 57, "Chest"),
- LocationName.SantasHouseAPBoost: LocationData(0x1300EB, 58, "Chest"),
- LocationName.PrisonKeeper: LocationData(0x1300EC, 18, "Get Bonus"),
- LocationName.OogieBoogie: LocationData(0x1300ED, 19, "Get Bonus"),
- LocationName.OogieBoogieMagnetElement: LocationData(0x1300EE, 301, "Chest"),
-}
-HT2_Checks = {
- LocationName.Lock: LocationData(0x1300EF, 40, "Get Bonus"),
- LocationName.Present: LocationData(0x1300F0, 297, "Chest"),
- LocationName.DecoyPresents: LocationData(0x1300F1, 298, "Chest"),
- LocationName.Experiment: LocationData(0x1300F2, 20, "Get Bonus"),
- LocationName.DecisivePumpkin: LocationData(0x1300F3, 275, "Chest"),
- LocationName.VexenBonus: LocationData(0x1300F4, 64, "Get Bonus"),
- LocationName.VexenASRoadtoDiscovery: LocationData(0x1300F5, 544, "Chest"),
- LocationName.VexenDataLostIllusion: LocationData(0x1300F6, 549, "Chest"),
+ LocationName.GraveyardMythrilShard: LocationData(53, "Chest"),
+ LocationName.GraveyardSerenityGem: LocationData(212, "Chest"),
+ LocationName.FinklesteinsLabHalloweenTownMap: LocationData(211, "Chest"),
+ LocationName.TownSquareMythrilStone: LocationData(209, "Chest"),
+ LocationName.TownSquareEnergyShard: LocationData(210, "Chest"),
+ LocationName.HinterlandsLightningShard: LocationData(54, "Chest"),
+ LocationName.HinterlandsMythrilStone: LocationData(213, "Chest"),
+ LocationName.HinterlandsAPBoost: LocationData(214, "Chest"),
+ LocationName.CandyCaneLaneMegaPotion: LocationData(55, "Chest"),
+ LocationName.CandyCaneLaneMythrilGem: LocationData(56, "Chest"),
+ LocationName.CandyCaneLaneLightningStone: LocationData(216, "Chest"),
+ LocationName.CandyCaneLaneMythrilStone: LocationData(217, "Chest"),
+ LocationName.SantasHouseChristmasTownMap: LocationData(57, "Chest"),
+ LocationName.SantasHouseAPBoost: LocationData(58, "Chest"),
+ LocationName.PrisonKeeper: LocationData(18, "Get Bonus"),
+ LocationName.OogieBoogie: LocationData(19, "Get Bonus"),
+ LocationName.OogieBoogieMagnetElement: LocationData(301, "Chest"),
+ LocationName.Lock: LocationData(40, "Get Bonus"),
+ LocationName.Present: LocationData(297, "Chest"),
+ LocationName.DecoyPresents: LocationData(298, "Chest"),
+ LocationName.Experiment: LocationData(20, "Get Bonus"),
+ LocationName.DecisivePumpkin: LocationData(275, "Chest"),
+ LocationName.VexenBonus: LocationData(64, "Get Bonus"),
+ LocationName.VexenASRoadtoDiscovery: LocationData(544, "Chest"),
+ LocationName.VexenDataLostIllusion: LocationData(549, "Chest"),
}
PR_Checks = {
- LocationName.RampartNavalMap: LocationData(0x1300F7, 70, "Chest"),
- LocationName.RampartMythrilStone: LocationData(0x1300F8, 219, "Chest"),
- LocationName.RampartDarkShard: LocationData(0x1300F9, 220, "Chest"),
- LocationName.TownDarkStone: LocationData(0x1300FA, 71, "Chest"),
- LocationName.TownAPBoost: LocationData(0x1300FB, 72, "Chest"),
- LocationName.TownMythrilShard: LocationData(0x1300FC, 73, "Chest"),
- LocationName.TownMythrilGem: LocationData(0x1300FD, 221, "Chest"),
- LocationName.CaveMouthBrightShard: LocationData(0x1300FE, 74, "Chest"),
- LocationName.CaveMouthMythrilShard: LocationData(0x1300FF, 223, "Chest"),
- LocationName.IsladeMuertaMap: LocationData(0x130100, 329, "Chest"),
- LocationName.BoatFight: LocationData(0x130101, 62, "Get Bonus"),
- LocationName.InterceptorBarrels: LocationData(0x130102, 39, "Get Bonus"),
- LocationName.PowderStoreAPBoost1: LocationData(0x130103, 369, "Chest"),
- LocationName.PowderStoreAPBoost2: LocationData(0x130104, 370, "Chest"),
- LocationName.MoonlightNookMythrilShard: LocationData(0x130105, 75, "Chest"),
- LocationName.MoonlightNookSerenityGem: LocationData(0x130106, 224, "Chest"),
- LocationName.MoonlightNookPowerStone: LocationData(0x130107, 371, "Chest"),
- LocationName.Barbossa: LocationData(0x130108, 21, "Double Get Bonus"),
- LocationName.BarbossaGetBonus: LocationData(0x130109, 21, "Second Get Bonus"),
- LocationName.FollowtheWind: LocationData(0x13010A, 263, "Chest"),
-
-}
-PR2_Checks = {
- LocationName.GrimReaper1: LocationData(0x13010B, 59, "Get Bonus"),
- LocationName.InterceptorsHoldFeatherCharm: LocationData(0x13010C, 252, "Chest"),
- LocationName.SeadriftKeepAPBoost: LocationData(0x13010D, 76, "Chest"),
- LocationName.SeadriftKeepOrichalcum: LocationData(0x13010E, 225, "Chest"),
- LocationName.SeadriftKeepMeteorStaff: LocationData(0x13010F, 372, "Chest"),
- LocationName.SeadriftRowSerenityGem: LocationData(0x130110, 77, "Chest"),
- LocationName.SeadriftRowKingRecipe: LocationData(0x130111, 78, "Chest"),
- LocationName.SeadriftRowMythrilCrystal: LocationData(0x130112, 373, "Chest"),
- LocationName.SeadriftRowCursedMedallion: LocationData(0x130113, 296, "Chest"),
- LocationName.SeadriftRowShipGraveyardMap: LocationData(0x130114, 331, "Chest"),
- LocationName.GrimReaper2: LocationData(0x130115, 22, "Get Bonus"),
- LocationName.SecretAnsemReport6: LocationData(0x130116, 530, "Chest"),
- LocationName.LuxordDataAPBoost: LocationData(0x130117, 557, "Chest"),
+ LocationName.RampartNavalMap: LocationData(70, "Chest"),
+ LocationName.RampartMythrilStone: LocationData(219, "Chest"),
+ LocationName.RampartDarkShard: LocationData(220, "Chest"),
+ LocationName.TownDarkStone: LocationData(71, "Chest"),
+ LocationName.TownAPBoost: LocationData(72, "Chest"),
+ LocationName.TownMythrilShard: LocationData(73, "Chest"),
+ LocationName.TownMythrilGem: LocationData(221, "Chest"),
+ LocationName.CaveMouthBrightShard: LocationData(74, "Chest"),
+ LocationName.CaveMouthMythrilShard: LocationData(223, "Chest"),
+ LocationName.IsladeMuertaMap: LocationData(329, "Chest"),
+ LocationName.BoatFight: LocationData(62, "Get Bonus"),
+ LocationName.InterceptorBarrels: LocationData(39, "Get Bonus"),
+ LocationName.PowderStoreAPBoost1: LocationData(369, "Chest"),
+ LocationName.PowderStoreAPBoost2: LocationData(370, "Chest"),
+ LocationName.MoonlightNookMythrilShard: LocationData(75, "Chest"),
+ LocationName.MoonlightNookSerenityGem: LocationData(224, "Chest"),
+ LocationName.MoonlightNookPowerStone: LocationData(371, "Chest"),
+ LocationName.Barbossa: LocationData(21, "Double Get Bonus"),
+ LocationName.BarbossaGetBonus: LocationData(21, "Second Get Bonus"),
+ LocationName.FollowtheWind: LocationData(263, "Chest"),
+ LocationName.GrimReaper1: LocationData(59, "Get Bonus"),
+ LocationName.InterceptorsHoldFeatherCharm: LocationData(252, "Chest"),
+ LocationName.SeadriftKeepAPBoost: LocationData(76, "Chest"),
+ LocationName.SeadriftKeepOrichalcum: LocationData(225, "Chest"),
+ LocationName.SeadriftKeepMeteorStaff: LocationData(372, "Chest"),
+ LocationName.SeadriftRowSerenityGem: LocationData(77, "Chest"),
+ LocationName.SeadriftRowKingRecipe: LocationData(78, "Chest"),
+ LocationName.SeadriftRowMythrilCrystal: LocationData(373, "Chest"),
+ LocationName.SeadriftRowCursedMedallion: LocationData(296, "Chest"),
+ LocationName.SeadriftRowShipGraveyardMap: LocationData(331, "Chest"),
+ LocationName.GrimReaper2: LocationData(22, "Get Bonus"),
+ LocationName.SecretAnsemReport6: LocationData(530, "Chest"),
+ LocationName.LuxordDataAPBoost: LocationData(557, "Chest"),
}
HB_Checks = {
- LocationName.MarketplaceMap: LocationData(0x130118, 362, "Chest"),
- LocationName.BoroughDriveRecovery: LocationData(0x130119, 194, "Chest"),
- LocationName.BoroughAPBoost: LocationData(0x13011A, 195, "Chest"),
- LocationName.BoroughHiPotion: LocationData(0x13011B, 196, "Chest"),
- LocationName.BoroughMythrilShard: LocationData(0x13011C, 305, "Chest"),
- LocationName.BoroughDarkShard: LocationData(0x13011D, 506, "Chest"),
- LocationName.MerlinsHouseMembershipCard: LocationData(0x13011E, 256, "Chest"),
- LocationName.MerlinsHouseBlizzardElement: LocationData(0x13011F, 292, "Chest"),
- LocationName.Bailey: LocationData(0x130120, 47, "Get Bonus"),
- LocationName.BaileySecretAnsemReport7: LocationData(0x130121, 531, "Chest"),
- LocationName.BaseballCharm: LocationData(0x130122, 258, "Chest"),
-}
-HB2_Checks = {
- LocationName.PosternCastlePerimeterMap: LocationData(0x130123, 310, "Chest"),
- LocationName.PosternMythrilGem: LocationData(0x130124, 189, "Chest"),
- LocationName.PosternAPBoost: LocationData(0x130125, 190, "Chest"),
- LocationName.CorridorsMythrilStone: LocationData(0x130126, 200, "Chest"),
- LocationName.CorridorsMythrilCrystal: LocationData(0x130127, 201, "Chest"),
- LocationName.CorridorsDarkCrystal: LocationData(0x130128, 202, "Chest"),
- LocationName.CorridorsAPBoost: LocationData(0x130129, 307, "Chest"),
- LocationName.AnsemsStudyMasterForm: LocationData(0x13012A, 276, "Chest"),
- LocationName.AnsemsStudySleepingLion: LocationData(0x13012B, 266, "Chest"),
- LocationName.AnsemsStudySkillRecipe: LocationData(0x13012C, 184, "Chest"),
- LocationName.AnsemsStudyUkuleleCharm: LocationData(0x13012D, 183, "Chest"),
- LocationName.RestorationSiteMoonRecipe: LocationData(0x13012E, 309, "Chest"),
- LocationName.RestorationSiteAPBoost: LocationData(0x13012F, 507, "Chest"),
- LocationName.DemyxHB: LocationData(0x130130, 28, "Double Get Bonus"),
- LocationName.DemyxHBGetBonus: LocationData(0x130131, 28, "Second Get Bonus"),
- LocationName.FFFightsCureElement: LocationData(0x130132, 361, "Chest"),
- LocationName.CrystalFissureTornPages: LocationData(0x130133, 179, "Chest"),
- LocationName.CrystalFissureTheGreatMawMap: LocationData(0x130134, 489, "Chest"),
- LocationName.CrystalFissureEnergyCrystal: LocationData(0x130135, 180, "Chest"),
- LocationName.CrystalFissureAPBoost: LocationData(0x130136, 181, "Chest"),
- LocationName.ThousandHeartless: LocationData(0x130137, 60, "Get Bonus"),
- LocationName.ThousandHeartlessSecretAnsemReport1: LocationData(0x130138, 525, "Chest"),
- LocationName.ThousandHeartlessIceCream: LocationData(0x130139, 269, "Chest"),
- LocationName.ThousandHeartlessPicture: LocationData(0x13013A, 511, "Chest"),
- LocationName.PosternGullWing: LocationData(0x13013B, 491, "Chest"),
- LocationName.HeartlessManufactoryCosmicChain: LocationData(0x13013C, 311, "Chest"),
- LocationName.SephirothBonus: LocationData(0x13013D, 35, "Get Bonus"),
- LocationName.SephirothFenrir: LocationData(0x13013E, 282, "Chest"),
- LocationName.WinnersProof: LocationData(0x13013F, 588, "Chest"),
- LocationName.ProofofPeace: LocationData(0x130140, 589, "Chest"),
- LocationName.DemyxDataAPBoost: LocationData(0x130141, 560, "Chest"),
- LocationName.CoRDepthsAPBoost: LocationData(0x130142, 562, "Chest"),
- LocationName.CoRDepthsPowerCrystal: LocationData(0x130143, 563, "Chest"),
- LocationName.CoRDepthsFrostCrystal: LocationData(0x130144, 564, "Chest"),
- LocationName.CoRDepthsManifestIllusion: LocationData(0x130145, 565, "Chest"),
- LocationName.CoRDepthsAPBoost2: LocationData(0x130146, 566, "Chest"),
- LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap: LocationData(0x130147, 580, "Chest"),
- LocationName.CoRMineshaftLowerLevelAPBoost: LocationData(0x130148, 578, "Chest"),
-
-}
-CoR_Checks = {
- LocationName.CoRDepthsUpperLevelRemembranceGem: LocationData(0x130149, 567, "Chest"),
- LocationName.CoRMiningAreaSerenityGem: LocationData(0x13014A, 568, "Chest"),
- LocationName.CoRMiningAreaAPBoost: LocationData(0x13014B, 569, "Chest"),
- LocationName.CoRMiningAreaSerenityCrystal: LocationData(0x13014C, 570, "Chest"),
- LocationName.CoRMiningAreaManifestIllusion: LocationData(0x13014D, 571, "Chest"),
- LocationName.CoRMiningAreaSerenityGem2: LocationData(0x13014E, 572, "Chest"),
- LocationName.CoRMiningAreaDarkRemembranceMap: LocationData(0x13014F, 573, "Chest"),
- LocationName.CoRMineshaftMidLevelPowerBoost: LocationData(0x130150, 581, "Chest"),
- LocationName.CoREngineChamberSerenityCrystal: LocationData(0x130151, 574, "Chest"),
- LocationName.CoREngineChamberRemembranceCrystal: LocationData(0x130152, 575, "Chest"),
- LocationName.CoREngineChamberAPBoost: LocationData(0x130153, 576, "Chest"),
- LocationName.CoREngineChamberManifestIllusion: LocationData(0x130154, 577, "Chest"),
- LocationName.CoRMineshaftUpperLevelMagicBoost: LocationData(0x130155, 582, "Chest"),
- LocationName.CoRMineshaftUpperLevelAPBoost: LocationData(0x130156, 579, "Chest"),
- LocationName.TransporttoRemembrance: LocationData(0x130157, 72, "Get Bonus"),
+ LocationName.MarketplaceMap: LocationData(362, "Chest"),
+ LocationName.BoroughDriveRecovery: LocationData(194, "Chest"),
+ LocationName.BoroughAPBoost: LocationData(195, "Chest"),
+ LocationName.BoroughHiPotion: LocationData(196, "Chest"),
+ LocationName.BoroughMythrilShard: LocationData(305, "Chest"),
+ LocationName.BoroughDarkShard: LocationData(506, "Chest"),
+ LocationName.MerlinsHouseMembershipCard: LocationData(256, "Chest"),
+ LocationName.MerlinsHouseBlizzardElement: LocationData(292, "Chest"),
+ LocationName.Bailey: LocationData(47, "Get Bonus"),
+ LocationName.BaileySecretAnsemReport7: LocationData(531, "Chest"),
+ LocationName.BaseballCharm: LocationData(258, "Chest"),
+ LocationName.PosternCastlePerimeterMap: LocationData(310, "Chest"),
+ LocationName.PosternMythrilGem: LocationData(189, "Chest"),
+ LocationName.PosternAPBoost: LocationData(190, "Chest"),
+ LocationName.CorridorsMythrilStone: LocationData(200, "Chest"),
+ LocationName.CorridorsMythrilCrystal: LocationData(201, "Chest"),
+ LocationName.CorridorsDarkCrystal: LocationData(202, "Chest"),
+ LocationName.CorridorsAPBoost: LocationData(307, "Chest"),
+ LocationName.AnsemsStudyMasterForm: LocationData(276, "Chest"),
+ LocationName.AnsemsStudySleepingLion: LocationData(266, "Chest"),
+ LocationName.AnsemsStudySkillRecipe: LocationData(184, "Chest"),
+ LocationName.AnsemsStudyUkuleleCharm: LocationData(183, "Chest"),
+ LocationName.RestorationSiteMoonRecipe: LocationData(309, "Chest"),
+ LocationName.RestorationSiteAPBoost: LocationData(507, "Chest"),
+ LocationName.DemyxHB: LocationData(28, "Double Get Bonus"),
+ LocationName.DemyxHBGetBonus: LocationData(28, "Second Get Bonus"),
+ LocationName.FFFightsCureElement: LocationData(361, "Chest"),
+ LocationName.CrystalFissureTornPages: LocationData(179, "Chest"),
+ LocationName.CrystalFissureTheGreatMawMap: LocationData(489, "Chest"),
+ LocationName.CrystalFissureEnergyCrystal: LocationData(180, "Chest"),
+ LocationName.CrystalFissureAPBoost: LocationData(181, "Chest"),
+ LocationName.ThousandHeartless: LocationData(60, "Get Bonus"),
+ LocationName.ThousandHeartlessSecretAnsemReport1: LocationData(525, "Chest"),
+ LocationName.ThousandHeartlessIceCream: LocationData(269, "Chest"),
+ LocationName.ThousandHeartlessPicture: LocationData(511, "Chest"),
+ LocationName.PosternGullWing: LocationData(491, "Chest"),
+ LocationName.HeartlessManufactoryCosmicChain: LocationData(311, "Chest"),
+ LocationName.SephirothBonus: LocationData(35, "Get Bonus"),
+ LocationName.SephirothFenrir: LocationData(282, "Chest"),
+ LocationName.WinnersProof: LocationData(588, "Chest"),
+ LocationName.ProofofPeace: LocationData(589, "Chest"),
+ LocationName.DemyxDataAPBoost: LocationData(560, "Chest"),
+ LocationName.CoRDepthsAPBoost: LocationData(562, "Chest"),
+ LocationName.CoRDepthsPowerCrystal: LocationData(563, "Chest"),
+ LocationName.CoRDepthsFrostCrystal: LocationData(564, "Chest"),
+ LocationName.CoRDepthsManifestIllusion: LocationData(565, "Chest"),
+ LocationName.CoRDepthsAPBoost2: LocationData(566, "Chest"),
+ LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap: LocationData(580, "Chest"),
+ LocationName.CoRMineshaftLowerLevelAPBoost: LocationData(578, "Chest"),
+ LocationName.CoRDepthsUpperLevelRemembranceGem: LocationData(567, "Chest"),
+ LocationName.CoRMiningAreaSerenityGem: LocationData(568, "Chest"),
+ LocationName.CoRMiningAreaAPBoost: LocationData(569, "Chest"),
+ LocationName.CoRMiningAreaSerenityCrystal: LocationData(570, "Chest"),
+ LocationName.CoRMiningAreaManifestIllusion: LocationData(571, "Chest"),
+ LocationName.CoRMiningAreaSerenityGem2: LocationData(572, "Chest"),
+ LocationName.CoRMiningAreaDarkRemembranceMap: LocationData(573, "Chest"),
+ LocationName.CoRMineshaftMidLevelPowerBoost: LocationData(581, "Chest"),
+ LocationName.CoREngineChamberSerenityCrystal: LocationData(574, "Chest"),
+ LocationName.CoREngineChamberRemembranceCrystal: LocationData(575, "Chest"),
+ LocationName.CoREngineChamberAPBoost: LocationData(576, "Chest"),
+ LocationName.CoREngineChamberManifestIllusion: LocationData(577, "Chest"),
+ LocationName.CoRMineshaftUpperLevelMagicBoost: LocationData(582, "Chest"),
+ LocationName.CoRMineshaftUpperLevelAPBoost: LocationData(579, "Chest"),
+ LocationName.TransporttoRemembrance: LocationData(72, "Get Bonus"),
}
PL_Checks = {
- LocationName.GorgeSavannahMap: LocationData(0x130158, 492, "Chest"),
- LocationName.GorgeDarkGem: LocationData(0x130159, 404, "Chest"),
- LocationName.GorgeMythrilStone: LocationData(0x13015A, 405, "Chest"),
- LocationName.ElephantGraveyardFrostGem: LocationData(0x13015B, 401, "Chest"),
- LocationName.ElephantGraveyardMythrilStone: LocationData(0x13015C, 402, "Chest"),
- LocationName.ElephantGraveyardBrightStone: LocationData(0x13015D, 403, "Chest"),
- LocationName.ElephantGraveyardAPBoost: LocationData(0x13015E, 508, "Chest"),
- LocationName.ElephantGraveyardMythrilShard: LocationData(0x13015F, 509, "Chest"),
- LocationName.PrideRockMap: LocationData(0x130160, 418, "Chest"),
- LocationName.PrideRockMythrilStone: LocationData(0x130161, 392, "Chest"),
- LocationName.PrideRockSerenityCrystal: LocationData(0x130162, 393, "Chest"),
- LocationName.WildebeestValleyEnergyStone: LocationData(0x130163, 396, "Chest"),
- LocationName.WildebeestValleyAPBoost: LocationData(0x130164, 397, "Chest"),
- LocationName.WildebeestValleyMythrilGem: LocationData(0x130165, 398, "Chest"),
- LocationName.WildebeestValleyMythrilStone: LocationData(0x130166, 399, "Chest"),
- LocationName.WildebeestValleyLucidGem: LocationData(0x130167, 400, "Chest"),
- LocationName.WastelandsMythrilShard: LocationData(0x130168, 406, "Chest"),
- LocationName.WastelandsSerenityGem: LocationData(0x130169, 407, "Chest"),
- LocationName.WastelandsMythrilStone: LocationData(0x13016A, 408, "Chest"),
- LocationName.JungleSerenityGem: LocationData(0x13016B, 409, "Chest"),
- LocationName.JungleMythrilStone: LocationData(0x13016C, 410, "Chest"),
- LocationName.JungleSerenityCrystal: LocationData(0x13016D, 411, "Chest"),
- LocationName.OasisMap: LocationData(0x13016E, 412, "Chest"),
- LocationName.OasisTornPages: LocationData(0x13016F, 493, "Chest"),
- LocationName.OasisAPBoost: LocationData(0x130170, 413, "Chest"),
- LocationName.CircleofLife: LocationData(0x130171, 264, "Chest"),
- LocationName.Hyenas1: LocationData(0x130172, 49, "Get Bonus"),
- LocationName.Scar: LocationData(0x130173, 29, "Get Bonus"),
- LocationName.ScarFireElement: LocationData(0x130174, 302, "Chest"),
-
-}
-PL2_Checks = {
- LocationName.Hyenas2: LocationData(0x130175, 50, "Get Bonus"),
- LocationName.Groundshaker: LocationData(0x130176, 30, "Double Get Bonus"),
- LocationName.GroundshakerGetBonus: LocationData(0x130177, 30, "Second Get Bonus"),
- LocationName.SaixDataDefenseBoost: LocationData(0x130178, 556, "Chest"),
+ LocationName.GorgeSavannahMap: LocationData(492, "Chest"),
+ LocationName.GorgeDarkGem: LocationData(404, "Chest"),
+ LocationName.GorgeMythrilStone: LocationData(405, "Chest"),
+ LocationName.ElephantGraveyardFrostGem: LocationData(401, "Chest"),
+ LocationName.ElephantGraveyardMythrilStone: LocationData(402, "Chest"),
+ LocationName.ElephantGraveyardBrightStone: LocationData(403, "Chest"),
+ LocationName.ElephantGraveyardAPBoost: LocationData(508, "Chest"),
+ LocationName.ElephantGraveyardMythrilShard: LocationData(509, "Chest"),
+ LocationName.PrideRockMap: LocationData(418, "Chest"),
+ LocationName.PrideRockMythrilStone: LocationData(392, "Chest"),
+ LocationName.PrideRockSerenityCrystal: LocationData(393, "Chest"),
+ LocationName.WildebeestValleyEnergyStone: LocationData(396, "Chest"),
+ LocationName.WildebeestValleyAPBoost: LocationData(397, "Chest"),
+ LocationName.WildebeestValleyMythrilGem: LocationData(398, "Chest"),
+ LocationName.WildebeestValleyMythrilStone: LocationData(399, "Chest"),
+ LocationName.WildebeestValleyLucidGem: LocationData(400, "Chest"),
+ LocationName.WastelandsMythrilShard: LocationData(406, "Chest"),
+ LocationName.WastelandsSerenityGem: LocationData(407, "Chest"),
+ LocationName.WastelandsMythrilStone: LocationData(408, "Chest"),
+ LocationName.JungleSerenityGem: LocationData(409, "Chest"),
+ LocationName.JungleMythrilStone: LocationData(410, "Chest"),
+ LocationName.JungleSerenityCrystal: LocationData(411, "Chest"),
+ LocationName.OasisMap: LocationData(412, "Chest"),
+ LocationName.OasisTornPages: LocationData(493, "Chest"),
+ LocationName.OasisAPBoost: LocationData(413, "Chest"),
+ LocationName.CircleofLife: LocationData(264, "Chest"),
+ LocationName.Hyenas1: LocationData(49, "Get Bonus"),
+ LocationName.Scar: LocationData(29, "Get Bonus"),
+ LocationName.ScarFireElement: LocationData(302, "Chest"),
+ LocationName.Hyenas2: LocationData(50, "Get Bonus"),
+ LocationName.Groundshaker: LocationData(30, "Double Get Bonus"),
+ LocationName.GroundshakerGetBonus: LocationData(30, "Second Get Bonus"),
+ LocationName.SaixDataDefenseBoost: LocationData(556, "Chest"),
}
STT_Checks = {
- LocationName.TwilightTownMap: LocationData(0x130179, 319, "Chest"),
- LocationName.MunnyPouchOlette: LocationData(0x13017A, 288, "Chest"),
- LocationName.StationDusks: LocationData(0x13017B, 54, "Get Bonus", "Roxas", 14),
- LocationName.StationofSerenityPotion: LocationData(0x13017C, 315, "Chest"),
- LocationName.StationofCallingPotion: LocationData(0x13017D, 472, "Chest"),
- LocationName.TwilightThorn: LocationData(0x13017E, 33, "Get Bonus", "Roxas", 14),
- LocationName.Axel1: LocationData(0x13017F, 73, "Get Bonus", "Roxas", 14),
- LocationName.JunkChampionBelt: LocationData(0x130180, 389, "Chest"),
- LocationName.JunkMedal: LocationData(0x130181, 390, "Chest"),
- LocationName.TheStruggleTrophy: LocationData(0x130182, 519, "Chest"),
- LocationName.CentralStationPotion1: LocationData(0x130183, 428, "Chest"),
- LocationName.STTCentralStationHiPotion: LocationData(0x130184, 429, "Chest"),
- LocationName.CentralStationPotion2: LocationData(0x130185, 430, "Chest"),
- LocationName.SunsetTerraceAbilityRing: LocationData(0x130186, 434, "Chest"),
- LocationName.SunsetTerraceHiPotion: LocationData(0x130187, 435, "Chest"),
- LocationName.SunsetTerracePotion1: LocationData(0x130188, 436, "Chest"),
- LocationName.SunsetTerracePotion2: LocationData(0x130189, 437, "Chest"),
- LocationName.MansionFoyerHiPotion: LocationData(0x13018A, 449, "Chest"),
- LocationName.MansionFoyerPotion1: LocationData(0x13018B, 450, "Chest"),
- LocationName.MansionFoyerPotion2: LocationData(0x13018C, 451, "Chest"),
- LocationName.MansionDiningRoomElvenBandanna: LocationData(0x13018D, 455, "Chest"),
- LocationName.MansionDiningRoomPotion: LocationData(0x13018E, 456, "Chest"),
- LocationName.NaminesSketches: LocationData(0x13018F, 289, "Chest"),
- LocationName.MansionMap: LocationData(0x130190, 483, "Chest"),
- LocationName.MansionLibraryHiPotion: LocationData(0x130191, 459, "Chest"),
- LocationName.Axel2: LocationData(0x130192, 34, "Get Bonus", "Roxas", 14),
- LocationName.MansionBasementCorridorHiPotion: LocationData(0x130193, 463, "Chest"),
- LocationName.RoxasDataMagicBoost: LocationData(0x130194, 558, "Chest"),
+ LocationName.TwilightTownMap: LocationData(319, "Chest"),
+ LocationName.MunnyPouchOlette: LocationData(288, "Chest"),
+ LocationName.StationDusks: LocationData(54, "Get Bonus", "Roxas", 14),
+ LocationName.StationofSerenityPotion: LocationData(315, "Chest"),
+ LocationName.StationofCallingPotion: LocationData(472, "Chest"),
+ LocationName.TwilightThorn: LocationData(33, "Get Bonus", "Roxas", 14),
+ LocationName.Axel1: LocationData(73, "Get Bonus", "Roxas", 14),
+ LocationName.JunkChampionBelt: LocationData(389, "Chest"),
+ LocationName.JunkMedal: LocationData(390, "Chest"),
+ LocationName.TheStruggleTrophy: LocationData(519, "Chest"),
+ LocationName.CentralStationPotion1: LocationData(428, "Chest"),
+ LocationName.STTCentralStationHiPotion: LocationData(429, "Chest"),
+ LocationName.CentralStationPotion2: LocationData(430, "Chest"),
+ LocationName.SunsetTerraceAbilityRing: LocationData(434, "Chest"),
+ LocationName.SunsetTerraceHiPotion: LocationData(435, "Chest"),
+ LocationName.SunsetTerracePotion1: LocationData(436, "Chest"),
+ LocationName.SunsetTerracePotion2: LocationData(437, "Chest"),
+ LocationName.MansionFoyerHiPotion: LocationData(449, "Chest"),
+ LocationName.MansionFoyerPotion1: LocationData(450, "Chest"),
+ LocationName.MansionFoyerPotion2: LocationData(451, "Chest"),
+ LocationName.MansionDiningRoomElvenBandanna: LocationData(455, "Chest"),
+ LocationName.MansionDiningRoomPotion: LocationData(456, "Chest"),
+ LocationName.NaminesSketches: LocationData(289, "Chest"),
+ LocationName.MansionMap: LocationData(483, "Chest"),
+ LocationName.MansionLibraryHiPotion: LocationData(459, "Chest"),
+ LocationName.Axel2: LocationData(34, "Get Bonus", "Roxas", 14),
+ LocationName.MansionBasementCorridorHiPotion: LocationData(463, "Chest"),
+ LocationName.RoxasDataMagicBoost: LocationData(558, "Chest"),
}
TT_Checks = {
- LocationName.OldMansionPotion: LocationData(0x130195, 447, "Chest"),
- LocationName.OldMansionMythrilShard: LocationData(0x130196, 448, "Chest"),
- LocationName.TheWoodsPotion: LocationData(0x130197, 442, "Chest"),
- LocationName.TheWoodsMythrilShard: LocationData(0x130198, 443, "Chest"),
- LocationName.TheWoodsHiPotion: LocationData(0x130199, 444, "Chest"),
- LocationName.TramCommonHiPotion: LocationData(0x13019A, 420, "Chest"),
- LocationName.TramCommonAPBoost: LocationData(0x13019B, 421, "Chest"),
- LocationName.TramCommonTent: LocationData(0x13019C, 422, "Chest"),
- LocationName.TramCommonMythrilShard1: LocationData(0x13019D, 423, "Chest"),
- LocationName.TramCommonPotion1: LocationData(0x13019E, 424, "Chest"),
- LocationName.TramCommonMythrilShard2: LocationData(0x13019F, 425, "Chest"),
- LocationName.TramCommonPotion2: LocationData(0x1301A0, 484, "Chest"),
- LocationName.StationPlazaSecretAnsemReport2: LocationData(0x1301A1, 526, "Chest"),
- LocationName.MunnyPouchMickey: LocationData(0x1301A2, 290, "Chest"),
- LocationName.CrystalOrb: LocationData(0x1301A3, 291, "Chest"),
- LocationName.CentralStationTent: LocationData(0x1301A4, 431, "Chest"),
- LocationName.TTCentralStationHiPotion: LocationData(0x1301A5, 432, "Chest"),
- LocationName.CentralStationMythrilShard: LocationData(0x1301A6, 433, "Chest"),
- LocationName.TheTowerPotion: LocationData(0x1301A7, 465, "Chest"),
- LocationName.TheTowerHiPotion: LocationData(0x1301A8, 466, "Chest"),
- LocationName.TheTowerEther: LocationData(0x1301A9, 522, "Chest"),
- LocationName.TowerEntrywayEther: LocationData(0x1301AA, 467, "Chest"),
- LocationName.TowerEntrywayMythrilShard: LocationData(0x1301AB, 468, "Chest"),
- LocationName.SorcerersLoftTowerMap: LocationData(0x1301AC, 469, "Chest"),
- LocationName.TowerWardrobeMythrilStone: LocationData(0x1301AD, 470, "Chest"),
- LocationName.StarSeeker: LocationData(0x1301AE, 304, "Chest"),
- LocationName.ValorForm: LocationData(0x1301AF, 286, "Chest"),
-
-}
-TT2_Checks = {
- LocationName.SeifersTrophy: LocationData(0x1301B0, 294, "Chest"),
- LocationName.Oathkeeper: LocationData(0x1301B1, 265, "Chest"),
- LocationName.LimitForm: LocationData(0x1301B2, 543, "Chest"),
-}
-TT3_Checks = {
- LocationName.UndergroundConcourseMythrilGem: LocationData(0x1301B3, 479, "Chest"),
- LocationName.UndergroundConcourseAPBoost: LocationData(0x1301B4, 481, "Chest"),
- LocationName.UndergroundConcourseOrichalcum: LocationData(0x1301B5, 480, "Chest"),
- LocationName.UndergroundConcourseMythrilCrystal: LocationData(0x1301B6, 482, "Chest"),
- LocationName.TunnelwayOrichalcum: LocationData(0x1301B7, 477, "Chest"),
- LocationName.TunnelwayMythrilCrystal: LocationData(0x1301B8, 478, "Chest"),
- LocationName.SunsetTerraceOrichalcumPlus: LocationData(0x1301B9, 438, "Chest"),
- LocationName.SunsetTerraceMythrilShard: LocationData(0x1301BA, 439, "Chest"),
- LocationName.SunsetTerraceMythrilCrystal: LocationData(0x1301BB, 440, "Chest"),
- LocationName.SunsetTerraceAPBoost: LocationData(0x1301BC, 441, "Chest"),
- LocationName.MansionNobodies: LocationData(0x1301BD, 56, "Get Bonus"),
- LocationName.MansionFoyerMythrilCrystal: LocationData(0x1301BE, 452, "Chest"),
- LocationName.MansionFoyerMythrilStone: LocationData(0x1301BF, 453, "Chest"),
- LocationName.MansionFoyerSerenityCrystal: LocationData(0x1301C0, 454, "Chest"),
- LocationName.MansionDiningRoomMythrilCrystal: LocationData(0x1301C1, 457, "Chest"),
- LocationName.MansionDiningRoomMythrilStone: LocationData(0x1301C2, 458, "Chest"),
- LocationName.MansionLibraryOrichalcum: LocationData(0x1301C3, 460, "Chest"),
- LocationName.BeamSecretAnsemReport10: LocationData(0x1301C4, 534, "Chest"),
- LocationName.MansionBasementCorridorUltimateRecipe: LocationData(0x1301C5, 464, "Chest"),
- LocationName.BetwixtandBetween: LocationData(0x1301C6, 63, "Get Bonus"),
- LocationName.BetwixtandBetweenBondofFlame: LocationData(0x1301C7, 317, "Chest"),
- LocationName.AxelDataMagicBoost: LocationData(0x1301C8, 561, "Chest"),
+ LocationName.OldMansionPotion: LocationData(447, "Chest"),
+ LocationName.OldMansionMythrilShard: LocationData(448, "Chest"),
+ LocationName.TheWoodsPotion: LocationData(442, "Chest"),
+ LocationName.TheWoodsMythrilShard: LocationData(443, "Chest"),
+ LocationName.TheWoodsHiPotion: LocationData(444, "Chest"),
+ LocationName.TramCommonHiPotion: LocationData(420, "Chest"),
+ LocationName.TramCommonAPBoost: LocationData(421, "Chest"),
+ LocationName.TramCommonTent: LocationData(422, "Chest"),
+ LocationName.TramCommonMythrilShard1: LocationData(423, "Chest"),
+ LocationName.TramCommonPotion1: LocationData(424, "Chest"),
+ LocationName.TramCommonMythrilShard2: LocationData(425, "Chest"),
+ LocationName.TramCommonPotion2: LocationData(484, "Chest"),
+ LocationName.StationPlazaSecretAnsemReport2: LocationData(526, "Chest"),
+ LocationName.MunnyPouchMickey: LocationData(290, "Chest"),
+ LocationName.CrystalOrb: LocationData(291, "Chest"),
+ LocationName.CentralStationTent: LocationData(431, "Chest"),
+ LocationName.TTCentralStationHiPotion: LocationData(432, "Chest"),
+ LocationName.CentralStationMythrilShard: LocationData(433, "Chest"),
+ LocationName.TheTowerPotion: LocationData(465, "Chest"),
+ LocationName.TheTowerHiPotion: LocationData(466, "Chest"),
+ LocationName.TheTowerEther: LocationData(522, "Chest"),
+ LocationName.TowerEntrywayEther: LocationData(467, "Chest"),
+ LocationName.TowerEntrywayMythrilShard: LocationData(468, "Chest"),
+ LocationName.SorcerersLoftTowerMap: LocationData(469, "Chest"),
+ LocationName.TowerWardrobeMythrilStone: LocationData(470, "Chest"),
+ LocationName.StarSeeker: LocationData(304, "Chest"),
+ LocationName.ValorForm: LocationData(286, "Chest"),
+ LocationName.SeifersTrophy: LocationData(294, "Chest"),
+ LocationName.Oathkeeper: LocationData(265, "Chest"),
+ LocationName.LimitForm: LocationData(543, "Chest"),
+ LocationName.UndergroundConcourseMythrilGem: LocationData(479, "Chest"),
+ LocationName.UndergroundConcourseAPBoost: LocationData(481, "Chest"),
+ LocationName.UndergroundConcourseOrichalcum: LocationData(480, "Chest"),
+ LocationName.UndergroundConcourseMythrilCrystal: LocationData(482, "Chest"),
+ LocationName.TunnelwayOrichalcum: LocationData(477, "Chest"),
+ LocationName.TunnelwayMythrilCrystal: LocationData(478, "Chest"),
+ LocationName.SunsetTerraceOrichalcumPlus: LocationData(438, "Chest"),
+ LocationName.SunsetTerraceMythrilShard: LocationData(439, "Chest"),
+ LocationName.SunsetTerraceMythrilCrystal: LocationData(440, "Chest"),
+ LocationName.SunsetTerraceAPBoost: LocationData(441, "Chest"),
+ LocationName.MansionNobodies: LocationData(56, "Get Bonus"),
+ LocationName.MansionFoyerMythrilCrystal: LocationData(452, "Chest"),
+ LocationName.MansionFoyerMythrilStone: LocationData(453, "Chest"),
+ LocationName.MansionFoyerSerenityCrystal: LocationData(454, "Chest"),
+ LocationName.MansionDiningRoomMythrilCrystal: LocationData(457, "Chest"),
+ LocationName.MansionDiningRoomMythrilStone: LocationData(458, "Chest"),
+ LocationName.MansionLibraryOrichalcum: LocationData(460, "Chest"),
+ LocationName.BeamSecretAnsemReport10: LocationData(534, "Chest"),
+ LocationName.MansionBasementCorridorUltimateRecipe: LocationData(464, "Chest"),
+ LocationName.BetwixtandBetween: LocationData(63, "Get Bonus"),
+ LocationName.BetwixtandBetweenBondofFlame: LocationData(317, "Chest"),
+ LocationName.AxelDataMagicBoost: LocationData(561, "Chest"),
}
TWTNW_Checks = {
- LocationName.FragmentCrossingMythrilStone: LocationData(0x1301C9, 374, "Chest"),
- LocationName.FragmentCrossingMythrilCrystal: LocationData(0x1301CA, 375, "Chest"),
- LocationName.FragmentCrossingAPBoost: LocationData(0x1301CB, 376, "Chest"),
- LocationName.FragmentCrossingOrichalcum: LocationData(0x1301CC, 377, "Chest"),
- LocationName.Roxas: LocationData(0x1301CD, 69, "Double Get Bonus"),
- LocationName.RoxasGetBonus: LocationData(0x1301CE, 69, "Second Get Bonus"),
- LocationName.RoxasSecretAnsemReport8: LocationData(0x1301CF, 532, "Chest"),
- LocationName.TwoBecomeOne: LocationData(0x1301D0, 277, "Chest"),
- LocationName.MemorysSkyscaperMythrilCrystal: LocationData(0x1301D1, 391, "Chest"),
- LocationName.MemorysSkyscaperAPBoost: LocationData(0x1301D2, 523, "Chest"),
- LocationName.MemorysSkyscaperMythrilStone: LocationData(0x1301D3, 524, "Chest"),
- LocationName.TheBrinkofDespairDarkCityMap: LocationData(0x1301D4, 335, "Chest"),
- LocationName.TheBrinkofDespairOrichalcumPlus: LocationData(0x1301D5, 500, "Chest"),
- LocationName.NothingsCallMythrilGem: LocationData(0x1301D6, 378, "Chest"),
- LocationName.NothingsCallOrichalcum: LocationData(0x1301D7, 379, "Chest"),
- LocationName.TwilightsViewCosmicBelt: LocationData(0x1301D8, 336, "Chest"),
-}
-TWTNW2_Checks = {
- LocationName.XigbarBonus: LocationData(0x1301D9, 23, "Get Bonus"),
- LocationName.XigbarSecretAnsemReport3: LocationData(0x1301DA, 527, "Chest"),
- LocationName.NaughtsSkywayMythrilGem: LocationData(0x1301DB, 380, "Chest"),
- LocationName.NaughtsSkywayOrichalcum: LocationData(0x1301DC, 381, "Chest"),
- LocationName.NaughtsSkywayMythrilCrystal: LocationData(0x1301DD, 382, "Chest"),
- LocationName.Oblivion: LocationData(0x1301DE, 278, "Chest"),
- LocationName.CastleThatNeverWasMap: LocationData(0x1301DF, 496, "Chest"),
- LocationName.Luxord: LocationData(0x1301E0, 24, "Double Get Bonus"),
- LocationName.LuxordGetBonus: LocationData(0x1301E1, 24, "Second Get Bonus"),
- LocationName.LuxordSecretAnsemReport9: LocationData(0x1301E2, 533, "Chest"),
- LocationName.SaixBonus: LocationData(0x1301E3, 25, "Get Bonus"),
- LocationName.SaixSecretAnsemReport12: LocationData(0x1301E4, 536, "Chest"),
- LocationName.PreXemnas1SecretAnsemReport11: LocationData(0x1301E5, 535, "Chest"),
- LocationName.RuinandCreationsPassageMythrilStone: LocationData(0x1301E6, 385, "Chest"),
- LocationName.RuinandCreationsPassageAPBoost: LocationData(0x1301E7, 386, "Chest"),
- LocationName.RuinandCreationsPassageMythrilCrystal: LocationData(0x1301E8, 387, "Chest"),
- LocationName.RuinandCreationsPassageOrichalcum: LocationData(0x1301E9, 388, "Chest"),
- LocationName.Xemnas1: LocationData(0x1301EA, 26, "Double Get Bonus"),
- LocationName.Xemnas1GetBonus: LocationData(0x1301EB, 26, "Second Get Bonus"),
- LocationName.Xemnas1SecretAnsemReport13: LocationData(0x1301EC, 537, "Chest"),
- LocationName.FinalXemnas: LocationData(0x1301ED, 71, "Get Bonus"),
- LocationName.XemnasDataPowerBoost: LocationData(0x1301EE, 554, "Chest"),
+ LocationName.FragmentCrossingMythrilStone: LocationData(374, "Chest"),
+ LocationName.FragmentCrossingMythrilCrystal: LocationData(375, "Chest"),
+ LocationName.FragmentCrossingAPBoost: LocationData(376, "Chest"),
+ LocationName.FragmentCrossingOrichalcum: LocationData(377, "Chest"),
+ LocationName.Roxas: LocationData(69, "Double Get Bonus"),
+ LocationName.RoxasGetBonus: LocationData(69, "Second Get Bonus"),
+ LocationName.RoxasSecretAnsemReport8: LocationData(532, "Chest"),
+ LocationName.TwoBecomeOne: LocationData(277, "Chest"),
+ LocationName.MemorysSkyscaperMythrilCrystal: LocationData(391, "Chest"),
+ LocationName.MemorysSkyscaperAPBoost: LocationData(523, "Chest"),
+ LocationName.MemorysSkyscaperMythrilStone: LocationData(524, "Chest"),
+ LocationName.TheBrinkofDespairDarkCityMap: LocationData(335, "Chest"),
+ LocationName.TheBrinkofDespairOrichalcumPlus: LocationData(500, "Chest"),
+ LocationName.NothingsCallMythrilGem: LocationData(378, "Chest"),
+ LocationName.NothingsCallOrichalcum: LocationData(379, "Chest"),
+ LocationName.TwilightsViewCosmicBelt: LocationData(336, "Chest"),
+ LocationName.XigbarBonus: LocationData(23, "Get Bonus"),
+ LocationName.XigbarSecretAnsemReport3: LocationData(527, "Chest"),
+ LocationName.NaughtsSkywayMythrilGem: LocationData(380, "Chest"),
+ LocationName.NaughtsSkywayOrichalcum: LocationData(381, "Chest"),
+ LocationName.NaughtsSkywayMythrilCrystal: LocationData(382, "Chest"),
+ LocationName.Oblivion: LocationData(278, "Chest"),
+ LocationName.CastleThatNeverWasMap: LocationData(496, "Chest"),
+ LocationName.Luxord: LocationData(24, "Double Get Bonus"),
+ LocationName.LuxordGetBonus: LocationData(24, "Second Get Bonus"),
+ LocationName.LuxordSecretAnsemReport9: LocationData(533, "Chest"),
+ LocationName.SaixBonus: LocationData(25, "Get Bonus"),
+ LocationName.SaixSecretAnsemReport12: LocationData(536, "Chest"),
+ LocationName.PreXemnas1SecretAnsemReport11: LocationData(535, "Chest"),
+ LocationName.RuinandCreationsPassageMythrilStone: LocationData(385, "Chest"),
+ LocationName.RuinandCreationsPassageAPBoost: LocationData(386, "Chest"),
+ LocationName.RuinandCreationsPassageMythrilCrystal: LocationData(387, "Chest"),
+ LocationName.RuinandCreationsPassageOrichalcum: LocationData(388, "Chest"),
+ LocationName.Xemnas1: LocationData(26, "Double Get Bonus"),
+ LocationName.Xemnas1GetBonus: LocationData(26, "Second Get Bonus"),
+ LocationName.Xemnas1SecretAnsemReport13: LocationData(537, "Chest"),
+ # LocationName.FinalXemnas: LocationData(71, "Get Bonus"),
+ LocationName.XemnasDataPowerBoost: LocationData(554, "Chest"),
}
SoraLevels = {
- LocationName.Lvl1: LocationData(0x1301EF, 1, "Levels"),
- LocationName.Lvl2: LocationData(0x1301F0, 2, "Levels"),
- LocationName.Lvl3: LocationData(0x1301F1, 3, "Levels"),
- LocationName.Lvl4: LocationData(0x1301F2, 4, "Levels"),
- LocationName.Lvl5: LocationData(0x1301F3, 5, "Levels"),
- LocationName.Lvl6: LocationData(0x1301F4, 6, "Levels"),
- LocationName.Lvl7: LocationData(0x1301F5, 7, "Levels"),
- LocationName.Lvl8: LocationData(0x1301F6, 8, "Levels"),
- LocationName.Lvl9: LocationData(0x1301F7, 9, "Levels"),
- LocationName.Lvl10: LocationData(0x1301F8, 10, "Levels"),
- LocationName.Lvl11: LocationData(0x1301F9, 11, "Levels"),
- LocationName.Lvl12: LocationData(0x1301FA, 12, "Levels"),
- LocationName.Lvl13: LocationData(0x1301FB, 13, "Levels"),
- LocationName.Lvl14: LocationData(0x1301FC, 14, "Levels"),
- LocationName.Lvl15: LocationData(0x1301FD, 15, "Levels"),
- LocationName.Lvl16: LocationData(0x1301FE, 16, "Levels"),
- LocationName.Lvl17: LocationData(0x1301FF, 17, "Levels"),
- LocationName.Lvl18: LocationData(0x130200, 18, "Levels"),
- LocationName.Lvl19: LocationData(0x130201, 19, "Levels"),
- LocationName.Lvl20: LocationData(0x130202, 20, "Levels"),
- LocationName.Lvl21: LocationData(0x130203, 21, "Levels"),
- LocationName.Lvl22: LocationData(0x130204, 22, "Levels"),
- LocationName.Lvl23: LocationData(0x130205, 23, "Levels"),
- LocationName.Lvl24: LocationData(0x130206, 24, "Levels"),
- LocationName.Lvl25: LocationData(0x130207, 25, "Levels"),
- LocationName.Lvl26: LocationData(0x130208, 26, "Levels"),
- LocationName.Lvl27: LocationData(0x130209, 27, "Levels"),
- LocationName.Lvl28: LocationData(0x13020A, 28, "Levels"),
- LocationName.Lvl29: LocationData(0x13020B, 29, "Levels"),
- LocationName.Lvl30: LocationData(0x13020C, 30, "Levels"),
- LocationName.Lvl31: LocationData(0x13020D, 31, "Levels"),
- LocationName.Lvl32: LocationData(0x13020E, 32, "Levels"),
- LocationName.Lvl33: LocationData(0x13020F, 33, "Levels"),
- LocationName.Lvl34: LocationData(0x130210, 34, "Levels"),
- LocationName.Lvl35: LocationData(0x130211, 35, "Levels"),
- LocationName.Lvl36: LocationData(0x130212, 36, "Levels"),
- LocationName.Lvl37: LocationData(0x130213, 37, "Levels"),
- LocationName.Lvl38: LocationData(0x130214, 38, "Levels"),
- LocationName.Lvl39: LocationData(0x130215, 39, "Levels"),
- LocationName.Lvl40: LocationData(0x130216, 40, "Levels"),
- LocationName.Lvl41: LocationData(0x130217, 41, "Levels"),
- LocationName.Lvl42: LocationData(0x130218, 42, "Levels"),
- LocationName.Lvl43: LocationData(0x130219, 43, "Levels"),
- LocationName.Lvl44: LocationData(0x13021A, 44, "Levels"),
- LocationName.Lvl45: LocationData(0x13021B, 45, "Levels"),
- LocationName.Lvl46: LocationData(0x13021C, 46, "Levels"),
- LocationName.Lvl47: LocationData(0x13021D, 47, "Levels"),
- LocationName.Lvl48: LocationData(0x13021E, 48, "Levels"),
- LocationName.Lvl49: LocationData(0x13021F, 49, "Levels"),
- LocationName.Lvl50: LocationData(0x130220, 50, "Levels"),
- LocationName.Lvl51: LocationData(0x130221, 51, "Levels"),
- LocationName.Lvl52: LocationData(0x130222, 52, "Levels"),
- LocationName.Lvl53: LocationData(0x130223, 53, "Levels"),
- LocationName.Lvl54: LocationData(0x130224, 54, "Levels"),
- LocationName.Lvl55: LocationData(0x130225, 55, "Levels"),
- LocationName.Lvl56: LocationData(0x130226, 56, "Levels"),
- LocationName.Lvl57: LocationData(0x130227, 57, "Levels"),
- LocationName.Lvl58: LocationData(0x130228, 58, "Levels"),
- LocationName.Lvl59: LocationData(0x130229, 59, "Levels"),
- LocationName.Lvl60: LocationData(0x13022A, 60, "Levels"),
- LocationName.Lvl61: LocationData(0x13022B, 61, "Levels"),
- LocationName.Lvl62: LocationData(0x13022C, 62, "Levels"),
- LocationName.Lvl63: LocationData(0x13022D, 63, "Levels"),
- LocationName.Lvl64: LocationData(0x13022E, 64, "Levels"),
- LocationName.Lvl65: LocationData(0x13022F, 65, "Levels"),
- LocationName.Lvl66: LocationData(0x130230, 66, "Levels"),
- LocationName.Lvl67: LocationData(0x130231, 67, "Levels"),
- LocationName.Lvl68: LocationData(0x130232, 68, "Levels"),
- LocationName.Lvl69: LocationData(0x130233, 69, "Levels"),
- LocationName.Lvl70: LocationData(0x130234, 70, "Levels"),
- LocationName.Lvl71: LocationData(0x130235, 71, "Levels"),
- LocationName.Lvl72: LocationData(0x130236, 72, "Levels"),
- LocationName.Lvl73: LocationData(0x130237, 73, "Levels"),
- LocationName.Lvl74: LocationData(0x130238, 74, "Levels"),
- LocationName.Lvl75: LocationData(0x130239, 75, "Levels"),
- LocationName.Lvl76: LocationData(0x13023A, 76, "Levels"),
- LocationName.Lvl77: LocationData(0x13023B, 77, "Levels"),
- LocationName.Lvl78: LocationData(0x13023C, 78, "Levels"),
- LocationName.Lvl79: LocationData(0x13023D, 79, "Levels"),
- LocationName.Lvl80: LocationData(0x13023E, 80, "Levels"),
- LocationName.Lvl81: LocationData(0x13023F, 81, "Levels"),
- LocationName.Lvl82: LocationData(0x130240, 82, "Levels"),
- LocationName.Lvl83: LocationData(0x130241, 83, "Levels"),
- LocationName.Lvl84: LocationData(0x130242, 84, "Levels"),
- LocationName.Lvl85: LocationData(0x130243, 85, "Levels"),
- LocationName.Lvl86: LocationData(0x130244, 86, "Levels"),
- LocationName.Lvl87: LocationData(0x130245, 87, "Levels"),
- LocationName.Lvl88: LocationData(0x130246, 88, "Levels"),
- LocationName.Lvl89: LocationData(0x130247, 89, "Levels"),
- LocationName.Lvl90: LocationData(0x130248, 90, "Levels"),
- LocationName.Lvl91: LocationData(0x130249, 91, "Levels"),
- LocationName.Lvl92: LocationData(0x13024A, 92, "Levels"),
- LocationName.Lvl93: LocationData(0x13024B, 93, "Levels"),
- LocationName.Lvl94: LocationData(0x13024C, 94, "Levels"),
- LocationName.Lvl95: LocationData(0x13024D, 95, "Levels"),
- LocationName.Lvl96: LocationData(0x13024E, 96, "Levels"),
- LocationName.Lvl97: LocationData(0x13024F, 97, "Levels"),
- LocationName.Lvl98: LocationData(0x130250, 98, "Levels"),
- LocationName.Lvl99: LocationData(0x130251, 99, "Levels"),
+ LocationName.Lvl2: LocationData(2, "Levels"),
+ LocationName.Lvl3: LocationData(3, "Levels"),
+ LocationName.Lvl4: LocationData(4, "Levels"),
+ LocationName.Lvl5: LocationData(5, "Levels"),
+ LocationName.Lvl6: LocationData(6, "Levels"),
+ LocationName.Lvl7: LocationData(7, "Levels"),
+ LocationName.Lvl8: LocationData(8, "Levels"),
+ LocationName.Lvl9: LocationData(9, "Levels"),
+ LocationName.Lvl10: LocationData(10, "Levels"),
+ LocationName.Lvl11: LocationData(11, "Levels"),
+ LocationName.Lvl12: LocationData(12, "Levels"),
+ LocationName.Lvl13: LocationData(13, "Levels"),
+ LocationName.Lvl14: LocationData(14, "Levels"),
+ LocationName.Lvl15: LocationData(15, "Levels"),
+ LocationName.Lvl16: LocationData(16, "Levels"),
+ LocationName.Lvl17: LocationData(17, "Levels"),
+ LocationName.Lvl18: LocationData(18, "Levels"),
+ LocationName.Lvl19: LocationData(19, "Levels"),
+ LocationName.Lvl20: LocationData(20, "Levels"),
+ LocationName.Lvl21: LocationData(21, "Levels"),
+ LocationName.Lvl22: LocationData(22, "Levels"),
+ LocationName.Lvl23: LocationData(23, "Levels"),
+ LocationName.Lvl24: LocationData(24, "Levels"),
+ LocationName.Lvl25: LocationData(25, "Levels"),
+ LocationName.Lvl26: LocationData(26, "Levels"),
+ LocationName.Lvl27: LocationData(27, "Levels"),
+ LocationName.Lvl28: LocationData(28, "Levels"),
+ LocationName.Lvl29: LocationData(29, "Levels"),
+ LocationName.Lvl30: LocationData(30, "Levels"),
+ LocationName.Lvl31: LocationData(31, "Levels"),
+ LocationName.Lvl32: LocationData(32, "Levels"),
+ LocationName.Lvl33: LocationData(33, "Levels"),
+ LocationName.Lvl34: LocationData(34, "Levels"),
+ LocationName.Lvl35: LocationData(35, "Levels"),
+ LocationName.Lvl36: LocationData(36, "Levels"),
+ LocationName.Lvl37: LocationData(37, "Levels"),
+ LocationName.Lvl38: LocationData(38, "Levels"),
+ LocationName.Lvl39: LocationData(39, "Levels"),
+ LocationName.Lvl40: LocationData(40, "Levels"),
+ LocationName.Lvl41: LocationData(41, "Levels"),
+ LocationName.Lvl42: LocationData(42, "Levels"),
+ LocationName.Lvl43: LocationData(43, "Levels"),
+ LocationName.Lvl44: LocationData(44, "Levels"),
+ LocationName.Lvl45: LocationData(45, "Levels"),
+ LocationName.Lvl46: LocationData(46, "Levels"),
+ LocationName.Lvl47: LocationData(47, "Levels"),
+ LocationName.Lvl48: LocationData(48, "Levels"),
+ LocationName.Lvl49: LocationData(49, "Levels"),
+ LocationName.Lvl50: LocationData(50, "Levels"),
+ LocationName.Lvl51: LocationData(51, "Levels"),
+ LocationName.Lvl52: LocationData(52, "Levels"),
+ LocationName.Lvl53: LocationData(53, "Levels"),
+ LocationName.Lvl54: LocationData(54, "Levels"),
+ LocationName.Lvl55: LocationData(55, "Levels"),
+ LocationName.Lvl56: LocationData(56, "Levels"),
+ LocationName.Lvl57: LocationData(57, "Levels"),
+ LocationName.Lvl58: LocationData(58, "Levels"),
+ LocationName.Lvl59: LocationData(59, "Levels"),
+ LocationName.Lvl60: LocationData(60, "Levels"),
+ LocationName.Lvl61: LocationData(61, "Levels"),
+ LocationName.Lvl62: LocationData(62, "Levels"),
+ LocationName.Lvl63: LocationData(63, "Levels"),
+ LocationName.Lvl64: LocationData(64, "Levels"),
+ LocationName.Lvl65: LocationData(65, "Levels"),
+ LocationName.Lvl66: LocationData(66, "Levels"),
+ LocationName.Lvl67: LocationData(67, "Levels"),
+ LocationName.Lvl68: LocationData(68, "Levels"),
+ LocationName.Lvl69: LocationData(69, "Levels"),
+ LocationName.Lvl70: LocationData(70, "Levels"),
+ LocationName.Lvl71: LocationData(71, "Levels"),
+ LocationName.Lvl72: LocationData(72, "Levels"),
+ LocationName.Lvl73: LocationData(73, "Levels"),
+ LocationName.Lvl74: LocationData(74, "Levels"),
+ LocationName.Lvl75: LocationData(75, "Levels"),
+ LocationName.Lvl76: LocationData(76, "Levels"),
+ LocationName.Lvl77: LocationData(77, "Levels"),
+ LocationName.Lvl78: LocationData(78, "Levels"),
+ LocationName.Lvl79: LocationData(79, "Levels"),
+ LocationName.Lvl80: LocationData(80, "Levels"),
+ LocationName.Lvl81: LocationData(81, "Levels"),
+ LocationName.Lvl82: LocationData(82, "Levels"),
+ LocationName.Lvl83: LocationData(83, "Levels"),
+ LocationName.Lvl84: LocationData(84, "Levels"),
+ LocationName.Lvl85: LocationData(85, "Levels"),
+ LocationName.Lvl86: LocationData(86, "Levels"),
+ LocationName.Lvl87: LocationData(87, "Levels"),
+ LocationName.Lvl88: LocationData(88, "Levels"),
+ LocationName.Lvl89: LocationData(89, "Levels"),
+ LocationName.Lvl90: LocationData(90, "Levels"),
+ LocationName.Lvl91: LocationData(91, "Levels"),
+ LocationName.Lvl92: LocationData(92, "Levels"),
+ LocationName.Lvl93: LocationData(93, "Levels"),
+ LocationName.Lvl94: LocationData(94, "Levels"),
+ LocationName.Lvl95: LocationData(95, "Levels"),
+ LocationName.Lvl96: LocationData(96, "Levels"),
+ LocationName.Lvl97: LocationData(97, "Levels"),
+ LocationName.Lvl98: LocationData(98, "Levels"),
+ LocationName.Lvl99: LocationData(99, "Levels"),
}
Form_Checks = {
- LocationName.Valorlvl2: LocationData(0x130253, 2, "Forms", 1),
- LocationName.Valorlvl3: LocationData(0x130254, 3, "Forms", 1),
- LocationName.Valorlvl4: LocationData(0x130255, 4, "Forms", 1),
- LocationName.Valorlvl5: LocationData(0x130256, 5, "Forms", 1),
- LocationName.Valorlvl6: LocationData(0x130257, 6, "Forms", 1),
- LocationName.Valorlvl7: LocationData(0x130258, 7, "Forms", 1),
+ LocationName.Valorlvl2: LocationData(2, "Forms", 1),
+ LocationName.Valorlvl3: LocationData(3, "Forms", 1),
+ LocationName.Valorlvl4: LocationData(4, "Forms", 1),
+ LocationName.Valorlvl5: LocationData(5, "Forms", 1),
+ LocationName.Valorlvl6: LocationData(6, "Forms", 1),
+ LocationName.Valorlvl7: LocationData(7, "Forms", 1),
- LocationName.Wisdomlvl2: LocationData(0x13025A, 2, "Forms", 2),
- LocationName.Wisdomlvl3: LocationData(0x13025B, 3, "Forms", 2),
- LocationName.Wisdomlvl4: LocationData(0x13025C, 4, "Forms", 2),
- LocationName.Wisdomlvl5: LocationData(0x13025D, 5, "Forms", 2),
- LocationName.Wisdomlvl6: LocationData(0x13025E, 6, "Forms", 2),
- LocationName.Wisdomlvl7: LocationData(0x13025F, 7, "Forms", 2),
+ LocationName.Wisdomlvl2: LocationData(2, "Forms", 2),
+ LocationName.Wisdomlvl3: LocationData(3, "Forms", 2),
+ LocationName.Wisdomlvl4: LocationData(4, "Forms", 2),
+ LocationName.Wisdomlvl5: LocationData(5, "Forms", 2),
+ LocationName.Wisdomlvl6: LocationData(6, "Forms", 2),
+ LocationName.Wisdomlvl7: LocationData(7, "Forms", 2),
- LocationName.Limitlvl2: LocationData(0x130261, 2, "Forms", 3),
- LocationName.Limitlvl3: LocationData(0x130262, 3, "Forms", 3),
- LocationName.Limitlvl4: LocationData(0x130263, 4, "Forms", 3),
- LocationName.Limitlvl5: LocationData(0x130264, 5, "Forms", 3),
- LocationName.Limitlvl6: LocationData(0x130265, 6, "Forms", 3),
- LocationName.Limitlvl7: LocationData(0x130266, 7, "Forms", 3),
+ LocationName.Limitlvl2: LocationData(2, "Forms", 3),
+ LocationName.Limitlvl3: LocationData(3, "Forms", 3),
+ LocationName.Limitlvl4: LocationData(4, "Forms", 3),
+ LocationName.Limitlvl5: LocationData(5, "Forms", 3),
+ LocationName.Limitlvl6: LocationData(6, "Forms", 3),
+ LocationName.Limitlvl7: LocationData(7, "Forms", 3),
- LocationName.Masterlvl2: LocationData(0x130268, 2, "Forms", 4),
- LocationName.Masterlvl3: LocationData(0x130269, 3, "Forms", 4),
- LocationName.Masterlvl4: LocationData(0x13026A, 4, "Forms", 4),
- LocationName.Masterlvl5: LocationData(0x13026B, 5, "Forms", 4),
- LocationName.Masterlvl6: LocationData(0x13026C, 6, "Forms", 4),
- LocationName.Masterlvl7: LocationData(0x13026D, 7, "Forms", 4),
+ LocationName.Masterlvl2: LocationData(2, "Forms", 4),
+ LocationName.Masterlvl3: LocationData(3, "Forms", 4),
+ LocationName.Masterlvl4: LocationData(4, "Forms", 4),
+ LocationName.Masterlvl5: LocationData(5, "Forms", 4),
+ LocationName.Masterlvl6: LocationData(6, "Forms", 4),
+ LocationName.Masterlvl7: LocationData(7, "Forms", 4),
- LocationName.Finallvl2: LocationData(0x13026F, 2, "Forms", 5),
- LocationName.Finallvl3: LocationData(0x130270, 3, "Forms", 5),
- LocationName.Finallvl4: LocationData(0x130271, 4, "Forms", 5),
- LocationName.Finallvl5: LocationData(0x130272, 5, "Forms", 5),
- LocationName.Finallvl6: LocationData(0x130273, 6, "Forms", 5),
- LocationName.Finallvl7: LocationData(0x130274, 7, "Forms", 5),
+ LocationName.Finallvl2: LocationData(2, "Forms", 5),
+ LocationName.Finallvl3: LocationData(3, "Forms", 5),
+ LocationName.Finallvl4: LocationData(4, "Forms", 5),
+ LocationName.Finallvl5: LocationData(5, "Forms", 5),
+ LocationName.Finallvl6: LocationData(6, "Forms", 5),
+ LocationName.Finallvl7: LocationData(7, "Forms", 5),
+}
+Summon_Checks = {
+ LocationName.Summonlvl2: LocationData(2, "Summons"),
+ LocationName.Summonlvl3: LocationData(3, "Summons"),
+ LocationName.Summonlvl4: LocationData(4, "Summons"),
+ LocationName.Summonlvl5: LocationData(5, "Summons"),
+ LocationName.Summonlvl6: LocationData(6, "Summons"),
+ LocationName.Summonlvl7: LocationData(7, "Summons"),
}
GoA_Checks = {
- LocationName.GardenofAssemblageMap: LocationData(0x130275, 585, "Chest"),
- LocationName.GoALostIllusion: LocationData(0x130276, 586, "Chest"),
- LocationName.ProofofNonexistence: LocationData(0x130277, 590, "Chest"),
+ LocationName.GardenofAssemblageMap: LocationData(585, "Chest"),
+ LocationName.GoALostIllusion: LocationData(586, "Chest"),
+ LocationName.ProofofNonexistence: LocationData(590, "Chest"),
}
Keyblade_Slots = {
- LocationName.FAKESlot: LocationData(0x130278, 116, "Keyblade"),
- LocationName.DetectionSaberSlot: LocationData(0x130279, 83, "Keyblade"),
- LocationName.EdgeofUltimaSlot: LocationData(0x13027A, 84, "Keyblade"),
- LocationName.KingdomKeySlot: LocationData(0x13027B, 80, "Keyblade"),
- LocationName.OathkeeperSlot: LocationData(0x13027C, 81, "Keyblade"),
- LocationName.OblivionSlot: LocationData(0x13027D, 82, "Keyblade"),
- LocationName.StarSeekerSlot: LocationData(0x13027E, 123, "Keyblade"),
- LocationName.HiddenDragonSlot: LocationData(0x13027F, 124, "Keyblade"),
- LocationName.HerosCrestSlot: LocationData(0x130280, 127, "Keyblade"),
- LocationName.MonochromeSlot: LocationData(0x130281, 128, "Keyblade"),
- LocationName.FollowtheWindSlot: LocationData(0x130282, 129, "Keyblade"),
- LocationName.CircleofLifeSlot: LocationData(0x130283, 130, "Keyblade"),
- LocationName.PhotonDebuggerSlot: LocationData(0x130284, 131, "Keyblade"),
- LocationName.GullWingSlot: LocationData(0x130285, 132, "Keyblade"),
- LocationName.RumblingRoseSlot: LocationData(0x130286, 133, "Keyblade"),
- LocationName.GuardianSoulSlot: LocationData(0x130287, 134, "Keyblade"),
- LocationName.WishingLampSlot: LocationData(0x130288, 135, "Keyblade"),
- LocationName.DecisivePumpkinSlot: LocationData(0x130289, 136, "Keyblade"),
- LocationName.SweetMemoriesSlot: LocationData(0x13028A, 138, "Keyblade"),
- LocationName.MysteriousAbyssSlot: LocationData(0x13028B, 139, "Keyblade"),
- LocationName.SleepingLionSlot: LocationData(0x13028C, 137, "Keyblade"),
- LocationName.BondofFlameSlot: LocationData(0x13028D, 141, "Keyblade"),
- LocationName.TwoBecomeOneSlot: LocationData(0x13028E, 148, "Keyblade"),
- LocationName.FatalCrestSlot: LocationData(0x13028F, 140, "Keyblade"),
- LocationName.FenrirSlot: LocationData(0x130290, 142, "Keyblade"),
- LocationName.UltimaWeaponSlot: LocationData(0x130291, 143, "Keyblade"),
- LocationName.WinnersProofSlot: LocationData(0x130292, 149, "Keyblade"),
- LocationName.PurebloodSlot: LocationData(0x1302DB, 85, "Keyblade"),
-}
-# checks are given when talking to the computer in the GoA
-Critical_Checks = {
- LocationName.Crit_1: LocationData(0x130293, 1, "Critical"),
- LocationName.Crit_2: LocationData(0x130294, 1, "Critical"),
- LocationName.Crit_3: LocationData(0x130295, 1, "Critical"),
- LocationName.Crit_4: LocationData(0x130296, 1, "Critical"),
- LocationName.Crit_5: LocationData(0x130297, 1, "Critical"),
- LocationName.Crit_6: LocationData(0x130298, 1, "Critical"),
- LocationName.Crit_7: LocationData(0x130299, 1, "Critical"),
+ LocationName.FAKESlot: LocationData(116, "Keyblade"),
+ LocationName.DetectionSaberSlot: LocationData(83, "Keyblade"),
+ LocationName.EdgeofUltimaSlot: LocationData(84, "Keyblade"),
+ LocationName.KingdomKeySlot: LocationData(80, "Keyblade"),
+ LocationName.OathkeeperSlot: LocationData(81, "Keyblade"),
+ LocationName.OblivionSlot: LocationData(82, "Keyblade"),
+ LocationName.StarSeekerSlot: LocationData(123, "Keyblade"),
+ LocationName.HiddenDragonSlot: LocationData(124, "Keyblade"),
+ LocationName.HerosCrestSlot: LocationData(127, "Keyblade"),
+ LocationName.MonochromeSlot: LocationData(128, "Keyblade"),
+ LocationName.FollowtheWindSlot: LocationData(129, "Keyblade"),
+ LocationName.CircleofLifeSlot: LocationData(130, "Keyblade"),
+ LocationName.PhotonDebuggerSlot: LocationData(131, "Keyblade"),
+ LocationName.GullWingSlot: LocationData(132, "Keyblade"),
+ LocationName.RumblingRoseSlot: LocationData(133, "Keyblade"),
+ LocationName.GuardianSoulSlot: LocationData(134, "Keyblade"),
+ LocationName.WishingLampSlot: LocationData(135, "Keyblade"),
+ LocationName.DecisivePumpkinSlot: LocationData(136, "Keyblade"),
+ LocationName.SweetMemoriesSlot: LocationData(138, "Keyblade"),
+ LocationName.MysteriousAbyssSlot: LocationData(139, "Keyblade"),
+ LocationName.SleepingLionSlot: LocationData(137, "Keyblade"),
+ LocationName.BondofFlameSlot: LocationData(141, "Keyblade"),
+ LocationName.TwoBecomeOneSlot: LocationData(148, "Keyblade"),
+ LocationName.FatalCrestSlot: LocationData(140, "Keyblade"),
+ LocationName.FenrirSlot: LocationData(142, "Keyblade"),
+ LocationName.UltimaWeaponSlot: LocationData(143, "Keyblade"),
+ LocationName.WinnersProofSlot: LocationData(149, "Keyblade"),
+ LocationName.PurebloodSlot: LocationData(85, "Keyblade"),
}
Donald_Checks = {
- LocationName.DonaldScreens: LocationData(0x13029A, 45, "Get Bonus", "Donald", 2),
- LocationName.DonaldDemyxHBGetBonus: LocationData(0x13029B, 28, "Get Bonus", "Donald", 2),
- LocationName.DonaldDemyxOC: LocationData(0x13029C, 58, "Get Bonus", "Donald", 2),
- LocationName.DonaldBoatPete: LocationData(0x13029D, 16, "Double Get Bonus", "Donald", 2),
- LocationName.DonaldBoatPeteGetBonus: LocationData(0x13029E, 16, "Second Get Bonus", "Donald", 2),
- LocationName.DonaldPrisonKeeper: LocationData(0x13029F, 18, "Get Bonus", "Donald", 2),
- LocationName.DonaldScar: LocationData(0x1302A0, 29, "Get Bonus", "Donald", 2),
- LocationName.DonaldSolarSailer: LocationData(0x1302A1, 61, "Get Bonus", "Donald", 2),
- LocationName.DonaldExperiment: LocationData(0x1302A2, 20, "Get Bonus", "Donald", 2),
- LocationName.DonaldBoatFight: LocationData(0x1302A3, 62, "Get Bonus", "Donald", 2),
- LocationName.DonaldMansionNobodies: LocationData(0x1302A4, 56, "Get Bonus", "Donald", 2),
- LocationName.DonaldThresholder: LocationData(0x1302A5, 2, "Get Bonus", "Donald", 2),
- LocationName.DonaldXaldinGetBonus: LocationData(0x1302A6, 4, "Get Bonus", "Donald", 2),
- LocationName.DonaladGrimReaper2: LocationData(0x1302A7, 22, "Get Bonus", "Donald", 2),
+ LocationName.DonaldScreens: LocationData(45, "Get Bonus", "Donald", 2),
+ LocationName.DonaldDemyxHBGetBonus: LocationData(28, "Get Bonus", "Donald", 2),
+ LocationName.DonaldDemyxOC: LocationData(58, "Get Bonus", "Donald", 2),
+ LocationName.DonaldBoatPete: LocationData(16, "Double Get Bonus", "Donald", 2),
+ LocationName.DonaldBoatPeteGetBonus: LocationData(16, "Second Get Bonus", "Donald", 2),
+ LocationName.DonaldPrisonKeeper: LocationData(18, "Get Bonus", "Donald", 2),
+ LocationName.DonaldScar: LocationData(29, "Get Bonus", "Donald", 2),
+ LocationName.DonaldSolarSailer: LocationData(61, "Get Bonus", "Donald", 2),
+ LocationName.DonaldExperiment: LocationData(20, "Get Bonus", "Donald", 2),
+ LocationName.DonaldBoatFight: LocationData(62, "Get Bonus", "Donald", 2),
+ LocationName.DonaldMansionNobodies: LocationData(56, "Get Bonus", "Donald", 2),
+ LocationName.DonaldThresholder: LocationData(2, "Get Bonus", "Donald", 2),
+ LocationName.DonaldXaldinGetBonus: LocationData(4, "Get Bonus", "Donald", 2),
+ LocationName.DonaladGrimReaper2: LocationData(22, "Get Bonus", "Donald", 2),
- LocationName.CometStaff: LocationData(0x1302A8, 90, "Keyblade", "Donald"),
- LocationName.HammerStaff: LocationData(0x1302A9, 87, "Keyblade", "Donald"),
- LocationName.LordsBroom: LocationData(0x1302AA, 91, "Keyblade", "Donald"),
- LocationName.MagesStaff: LocationData(0x1302AB, 86, "Keyblade", "Donald"),
- LocationName.MeteorStaff: LocationData(0x1302AC, 89, "Keyblade", "Donald"),
- LocationName.NobodyLance: LocationData(0x1302AD, 94, "Keyblade", "Donald"),
- LocationName.PreciousMushroom: LocationData(0x1302AE, 154, "Keyblade", "Donald"),
- LocationName.PreciousMushroom2: LocationData(0x1302AF, 155, "Keyblade", "Donald"),
- LocationName.PremiumMushroom: LocationData(0x1302B0, 156, "Keyblade", "Donald"),
- LocationName.RisingDragon: LocationData(0x1302B1, 93, "Keyblade", "Donald"),
- LocationName.SaveTheQueen2: LocationData(0x1302B2, 146, "Keyblade", "Donald"),
- LocationName.ShamansRelic: LocationData(0x1302B3, 95, "Keyblade", "Donald"),
- LocationName.VictoryBell: LocationData(0x1302B4, 88, "Keyblade", "Donald"),
- LocationName.WisdomWand: LocationData(0x1302B5, 92, "Keyblade", "Donald"),
- LocationName.Centurion2: LocationData(0x1302B6, 151, "Keyblade", "Donald"),
- LocationName.DonaldAbuEscort: LocationData(0x1302B7, 42, "Get Bonus", "Donald", 2),
- LocationName.DonaldStarting1: LocationData(0x1302B8, 2, "Critical", "Donald"),
- LocationName.DonaldStarting2: LocationData(0x1302B9, 2, "Critical", "Donald"),
+ LocationName.CometStaff: LocationData(90, "Keyblade", "Donald"),
+ LocationName.HammerStaff: LocationData(87, "Keyblade", "Donald"),
+ LocationName.LordsBroom: LocationData(91, "Keyblade", "Donald"),
+ LocationName.MagesStaff: LocationData(86, "Keyblade", "Donald"),
+ LocationName.MeteorStaff: LocationData(89, "Keyblade", "Donald"),
+ LocationName.NobodyLance: LocationData(94, "Keyblade", "Donald"),
+ LocationName.PreciousMushroom: LocationData(154, "Keyblade", "Donald"),
+ LocationName.PreciousMushroom2: LocationData(155, "Keyblade", "Donald"),
+ LocationName.PremiumMushroom: LocationData(156, "Keyblade", "Donald"),
+ LocationName.RisingDragon: LocationData(93, "Keyblade", "Donald"),
+ LocationName.SaveTheQueen2: LocationData(146, "Keyblade", "Donald"),
+ LocationName.ShamansRelic: LocationData(95, "Keyblade", "Donald"),
+ LocationName.VictoryBell: LocationData(88, "Keyblade", "Donald"),
+ LocationName.WisdomWand: LocationData(92, "Keyblade", "Donald"),
+ LocationName.Centurion2: LocationData(151, "Keyblade", "Donald"),
+ LocationName.DonaldAbuEscort: LocationData(42, "Get Bonus", "Donald", 2),
+ # LocationName.DonaldStarting1: LocationData(2, "Critical", "Donald"),
+ # LocationName.DonaldStarting2: LocationData(2, "Critical", "Donald"),
}
Goofy_Checks = {
- LocationName.GoofyBarbossa: LocationData(0x1302BA, 21, "Double Get Bonus", "Goofy", 3),
- LocationName.GoofyBarbossaGetBonus: LocationData(0x1302BB, 21, "Second Get Bonus", "Goofy", 3),
- LocationName.GoofyGrimReaper1: LocationData(0x1302BC, 59, "Get Bonus", "Goofy", 3),
- LocationName.GoofyHostileProgram: LocationData(0x1302BD, 31, "Get Bonus", "Goofy", 3),
- LocationName.GoofyHyenas1: LocationData(0x1302BE, 49, "Get Bonus", "Goofy", 3),
- LocationName.GoofyHyenas2: LocationData(0x1302BF, 50, "Get Bonus", "Goofy", 3),
- LocationName.GoofyLock: LocationData(0x1302C0, 40, "Get Bonus", "Goofy", 3),
- LocationName.GoofyOogieBoogie: LocationData(0x1302C1, 19, "Get Bonus", "Goofy", 3),
- LocationName.GoofyPeteOC: LocationData(0x1302C2, 6, "Get Bonus", "Goofy", 3),
- LocationName.GoofyFuturePete: LocationData(0x1302C3, 17, "Get Bonus", "Goofy", 3),
- LocationName.GoofyShanYu: LocationData(0x1302C4, 9, "Get Bonus", "Goofy", 3),
- LocationName.GoofyStormRider: LocationData(0x1302C5, 10, "Get Bonus", "Goofy", 3),
- LocationName.GoofyBeast: LocationData(0x1302C6, 12, "Get Bonus", "Goofy", 3),
- LocationName.GoofyInterceptorBarrels: LocationData(0x1302C7, 39, "Get Bonus", "Goofy", 3),
- LocationName.GoofyTreasureRoom: LocationData(0x1302C8, 46, "Get Bonus", "Goofy", 3),
- LocationName.GoofyZexion: LocationData(0x1302C9, 66, "Get Bonus", "Goofy", 3),
+ LocationName.GoofyBarbossa: LocationData(21, "Double Get Bonus", "Goofy", 3),
+ LocationName.GoofyBarbossaGetBonus: LocationData(21, "Second Get Bonus", "Goofy", 3),
+ LocationName.GoofyGrimReaper1: LocationData(59, "Get Bonus", "Goofy", 3),
+ LocationName.GoofyHostileProgram: LocationData(31, "Get Bonus", "Goofy", 3),
+ LocationName.GoofyHyenas1: LocationData(49, "Get Bonus", "Goofy", 3),
+ LocationName.GoofyHyenas2: LocationData(50, "Get Bonus", "Goofy", 3),
+ LocationName.GoofyLock: LocationData(40, "Get Bonus", "Goofy", 3),
+ LocationName.GoofyOogieBoogie: LocationData(19, "Get Bonus", "Goofy", 3),
+ LocationName.GoofyPeteOC: LocationData(6, "Get Bonus", "Goofy", 3),
+ LocationName.GoofyFuturePete: LocationData(17, "Get Bonus", "Goofy", 3),
+ LocationName.GoofyShanYu: LocationData(9, "Get Bonus", "Goofy", 3),
+ LocationName.GoofyStormRider: LocationData(10, "Get Bonus", "Goofy", 3),
+ LocationName.GoofyBeast: LocationData(12, "Get Bonus", "Goofy", 3),
+ LocationName.GoofyInterceptorBarrels: LocationData(39, "Get Bonus", "Goofy", 3),
+ LocationName.GoofyTreasureRoom: LocationData(46, "Get Bonus", "Goofy", 3),
+ LocationName.GoofyZexion: LocationData(66, "Get Bonus", "Goofy", 3),
+
+ LocationName.AdamantShield: LocationData(100, "Keyblade", "Goofy"),
+ LocationName.AkashicRecord: LocationData(107, "Keyblade", "Goofy"),
+ LocationName.ChainGear: LocationData(101, "Keyblade", "Goofy"),
+ LocationName.DreamCloud: LocationData(104, "Keyblade", "Goofy"),
+ LocationName.FallingStar: LocationData(103, "Keyblade", "Goofy"),
+ LocationName.FrozenPride2: LocationData(158, "Keyblade", "Goofy"),
+ LocationName.GenjiShield: LocationData(106, "Keyblade", "Goofy"),
+ LocationName.KnightDefender: LocationData(105, "Keyblade", "Goofy"),
+ LocationName.KnightsShield: LocationData(99, "Keyblade", "Goofy"),
+ LocationName.MajesticMushroom: LocationData(161, "Keyblade", "Goofy"),
+ LocationName.MajesticMushroom2: LocationData(162, "Keyblade", "Goofy"),
+ LocationName.NobodyGuard: LocationData(108, "Keyblade", "Goofy"),
+ LocationName.OgreShield: LocationData(102, "Keyblade", "Goofy"),
+ LocationName.SaveTheKing2: LocationData(147, "Keyblade", "Goofy"),
+ LocationName.UltimateMushroom: LocationData(163, "Keyblade", "Goofy"),
+ # LocationName.GoofyStarting1: LocationData(3, "Critical", "Goofy"),
+ # LocationName.GoofyStarting2: LocationData(3, "Critical", "Goofy"),
+}
+
+Atlantica_Checks = {
+ LocationName.UnderseaKingdomMap: LocationData(367, "Chest"),
+ LocationName.MysteriousAbyss: LocationData(287, "Chest"), # needs 2 magnets
+ LocationName.MusicalBlizzardElement: LocationData(279, "Chest"), # 2 magnets all thunders
+ LocationName.MusicalOrichalcumPlus: LocationData(538, "Chest"), # 2 magnets all thunders
+}
- LocationName.AdamantShield: LocationData(0x1302CA, 100, "Keyblade", "Goofy"),
- LocationName.AkashicRecord: LocationData(0x1302CB, 107, "Keyblade", "Goofy"),
- LocationName.ChainGear: LocationData(0x1302CC, 101, "Keyblade", "Goofy"),
- LocationName.DreamCloud: LocationData(0x1302CD, 104, "Keyblade", "Goofy"),
- LocationName.FallingStar: LocationData(0x1302CE, 103, "Keyblade", "Goofy"),
- LocationName.FrozenPride2: LocationData(0x1302CF, 158, "Keyblade", "Goofy"),
- LocationName.GenjiShield: LocationData(0x1302D0, 106, "Keyblade", "Goofy"),
- LocationName.KnightDefender: LocationData(0x1302D1, 105, "Keyblade", "Goofy"),
- LocationName.KnightsShield: LocationData(0x1302D2, 99, "Keyblade", "Goofy"),
- LocationName.MajesticMushroom: LocationData(0x1302D3, 161, "Keyblade", "Goofy"),
- LocationName.MajesticMushroom2: LocationData(0x1302D4, 162, "Keyblade", "Goofy"),
- LocationName.NobodyGuard: LocationData(0x1302D5, 108, "Keyblade", "Goofy"),
- LocationName.OgreShield: LocationData(0x1302D6, 102, "Keyblade", "Goofy"),
- LocationName.SaveTheKing2: LocationData(0x1302D7, 147, "Keyblade", "Goofy"),
- LocationName.UltimateMushroom: LocationData(0x1302D8, 163, "Keyblade", "Goofy"),
- LocationName.GoofyStarting1: LocationData(0x1302D9, 3, "Critical", "Goofy"),
- LocationName.GoofyStarting2: LocationData(0x1302DA, 3, "Critical", "Goofy"),
+event_location_to_item = {
+ LocationName.HostileProgramEventLocation: ItemName.HostileProgramEvent,
+ LocationName.McpEventLocation: ItemName.McpEvent,
+ # LocationName.ASLarxeneEventLocation: ItemName.ASLarxeneEvent,
+ LocationName.DataLarxeneEventLocation: ItemName.DataLarxeneEvent,
+ LocationName.BarbosaEventLocation: ItemName.BarbosaEvent,
+ LocationName.GrimReaper1EventLocation: ItemName.GrimReaper1Event,
+ LocationName.GrimReaper2EventLocation: ItemName.GrimReaper2Event,
+ LocationName.DataLuxordEventLocation: ItemName.DataLuxordEvent,
+ LocationName.DataAxelEventLocation: ItemName.DataAxelEvent,
+ LocationName.CerberusEventLocation: ItemName.CerberusEvent,
+ LocationName.OlympusPeteEventLocation: ItemName.OlympusPeteEvent,
+ LocationName.HydraEventLocation: ItemName.HydraEvent,
+ LocationName.OcPainAndPanicCupEventLocation: ItemName.OcPainAndPanicCupEvent,
+ LocationName.OcCerberusCupEventLocation: ItemName.OcCerberusCupEvent,
+ LocationName.HadesEventLocation: ItemName.HadesEvent,
+ # LocationName.ASZexionEventLocation: ItemName.ASZexionEvent,
+ LocationName.DataZexionEventLocation: ItemName.DataZexionEvent,
+ LocationName.Oc2TitanCupEventLocation: ItemName.Oc2TitanCupEvent,
+ LocationName.Oc2GofCupEventLocation: ItemName.Oc2GofCupEvent,
+ # LocationName.Oc2CupsEventLocation: ItemName.Oc2CupsEventLocation,
+ LocationName.HadesCupEventLocations: ItemName.HadesCupEvents,
+ LocationName.PrisonKeeperEventLocation: ItemName.PrisonKeeperEvent,
+ LocationName.OogieBoogieEventLocation: ItemName.OogieBoogieEvent,
+ LocationName.ExperimentEventLocation: ItemName.ExperimentEvent,
+ # LocationName.ASVexenEventLocation: ItemName.ASVexenEvent,
+ LocationName.DataVexenEventLocation: ItemName.DataVexenEvent,
+ LocationName.ShanYuEventLocation: ItemName.ShanYuEvent,
+ LocationName.AnsemRikuEventLocation: ItemName.AnsemRikuEvent,
+ LocationName.StormRiderEventLocation: ItemName.StormRiderEvent,
+ LocationName.DataXigbarEventLocation: ItemName.DataXigbarEvent,
+ LocationName.RoxasEventLocation: ItemName.RoxasEvent,
+ LocationName.XigbarEventLocation: ItemName.XigbarEvent,
+ LocationName.LuxordEventLocation: ItemName.LuxordEvent,
+ LocationName.SaixEventLocation: ItemName.SaixEvent,
+ LocationName.XemnasEventLocation: ItemName.XemnasEvent,
+ LocationName.ArmoredXemnasEventLocation: ItemName.ArmoredXemnasEvent,
+ LocationName.ArmoredXemnas2EventLocation: ItemName.ArmoredXemnas2Event,
+ # LocationName.FinalXemnasEventLocation: ItemName.FinalXemnasEvent,
+ LocationName.DataXemnasEventLocation: ItemName.DataXemnasEvent,
+ LocationName.ThresholderEventLocation: ItemName.ThresholderEvent,
+ LocationName.BeastEventLocation: ItemName.BeastEvent,
+ LocationName.DarkThornEventLocation: ItemName.DarkThornEvent,
+ LocationName.XaldinEventLocation: ItemName.XaldinEvent,
+ LocationName.DataXaldinEventLocation: ItemName.DataXaldinEvent,
+ LocationName.TwinLordsEventLocation: ItemName.TwinLordsEvent,
+ LocationName.GenieJafarEventLocation: ItemName.GenieJafarEvent,
+ # LocationName.ASLexaeusEventLocation: ItemName.ASLexaeusEvent,
+ LocationName.DataLexaeusEventLocation: ItemName.DataLexaeusEvent,
+ LocationName.ScarEventLocation: ItemName.ScarEvent,
+ LocationName.GroundShakerEventLocation: ItemName.GroundShakerEvent,
+ LocationName.DataSaixEventLocation: ItemName.DataSaixEvent,
+ LocationName.HBDemyxEventLocation: ItemName.HBDemyxEvent,
+ LocationName.ThousandHeartlessEventLocation: ItemName.ThousandHeartlessEvent,
+ LocationName.Mushroom13EventLocation: ItemName.Mushroom13Event,
+ LocationName.SephiEventLocation: ItemName.SephiEvent,
+ LocationName.DataDemyxEventLocation: ItemName.DataDemyxEvent,
+ LocationName.CorFirstFightEventLocation: ItemName.CorFirstFightEvent,
+ LocationName.CorSecondFightEventLocation: ItemName.CorSecondFightEvent,
+ LocationName.TransportEventLocation: ItemName.TransportEvent,
+ LocationName.OldPeteEventLocation: ItemName.OldPeteEvent,
+ LocationName.FuturePeteEventLocation: ItemName.FuturePeteEvent,
+ # LocationName.ASMarluxiaEventLocation: ItemName.ASMarluxiaEvent,
+ LocationName.DataMarluxiaEventLocation: ItemName.DataMarluxiaEvent,
+ LocationName.TerraEventLocation: ItemName.TerraEvent,
+ LocationName.TwilightThornEventLocation: ItemName.TwilightThornEvent,
+ LocationName.Axel1EventLocation: ItemName.Axel1Event,
+ LocationName.Axel2EventLocation: ItemName.Axel2Event,
+ LocationName.DataRoxasEventLocation: ItemName.DataRoxasEvent,
+ LocationName.FinalXemnasEventLocation: ItemName.Victory,
+}
+all_weapon_slot = {
+ LocationName.FAKESlot,
+ LocationName.DetectionSaberSlot,
+ LocationName.EdgeofUltimaSlot,
+ LocationName.KingdomKeySlot,
+ LocationName.OathkeeperSlot,
+ LocationName.OblivionSlot,
+ LocationName.StarSeekerSlot,
+ LocationName.HiddenDragonSlot,
+ LocationName.HerosCrestSlot,
+ LocationName.MonochromeSlot,
+ LocationName.FollowtheWindSlot,
+ LocationName.CircleofLifeSlot,
+ LocationName.PhotonDebuggerSlot,
+ LocationName.GullWingSlot,
+ LocationName.RumblingRoseSlot,
+ LocationName.GuardianSoulSlot,
+ LocationName.WishingLampSlot,
+ LocationName.DecisivePumpkinSlot,
+ LocationName.SweetMemoriesSlot,
+ LocationName.MysteriousAbyssSlot,
+ LocationName.SleepingLionSlot,
+ LocationName.BondofFlameSlot,
+ LocationName.TwoBecomeOneSlot,
+ LocationName.FatalCrestSlot,
+ LocationName.FenrirSlot,
+ LocationName.UltimaWeaponSlot,
+ LocationName.WinnersProofSlot,
+ LocationName.PurebloodSlot,
+
+ LocationName.Centurion2,
+ LocationName.CometStaff,
+ LocationName.HammerStaff,
+ LocationName.LordsBroom,
+ LocationName.MagesStaff,
+ LocationName.MeteorStaff,
+ LocationName.NobodyLance,
+ LocationName.PreciousMushroom,
+ LocationName.PreciousMushroom2,
+ LocationName.PremiumMushroom,
+ LocationName.RisingDragon,
+ LocationName.SaveTheQueen2,
+ LocationName.ShamansRelic,
+ LocationName.VictoryBell,
+ LocationName.WisdomWand,
+
+ LocationName.AdamantShield,
+ LocationName.AkashicRecord,
+ LocationName.ChainGear,
+ LocationName.DreamCloud,
+ LocationName.FallingStar,
+ LocationName.FrozenPride2,
+ LocationName.GenjiShield,
+ LocationName.KnightDefender,
+ LocationName.KnightsShield,
+ LocationName.MajesticMushroom,
+ LocationName.MajesticMushroom2,
+ LocationName.NobodyGuard,
+ LocationName.OgreShield,
+ LocationName.SaveTheKing2,
+ LocationName.UltimateMushroom, }
+
+all_locations = {
+ **TWTNW_Checks,
+ **TT_Checks,
+ **STT_Checks,
+ **PL_Checks,
+ **HB_Checks,
+ **HT_Checks,
+ **PR_Checks,
+ **PR_Checks,
+ **SP_Checks,
+ **BC_Checks,
+ **Oc_Checks,
+ **HundredAcre_Checks,
+ **DC_Checks,
+ **AG_Checks,
+ **LoD_Checks,
+ **SoraLevels,
+ **Form_Checks,
+ **GoA_Checks,
+ **Keyblade_Slots,
+ **Donald_Checks,
+ **Goofy_Checks,
+ **Atlantica_Checks,
+ **Summon_Checks,
+}
+
+popups_set = {
+ LocationName.SweetMemories,
+ LocationName.SpookyCaveMap,
+ LocationName.StarryHillCureElement,
+ LocationName.StarryHillOrichalcumPlus,
+ LocationName.AgrabahMap,
+ LocationName.LampCharm,
+ LocationName.WishingLamp,
+ LocationName.DarkThornCureElement,
+ LocationName.RumblingRose,
+ LocationName.CastleWallsMap,
+ LocationName.SecretAnsemReport4,
+ LocationName.DisneyCastleMap,
+ LocationName.WindowofTimeMap,
+ LocationName.Monochrome,
+ LocationName.WisdomForm,
+ LocationName.LingeringWillProofofConnection,
+ LocationName.LingeringWillManifestIllusion,
+ LocationName.OogieBoogieMagnetElement,
+ LocationName.Present,
+ LocationName.DecoyPresents,
+ LocationName.DecisivePumpkin,
+ LocationName.MarketplaceMap,
+ LocationName.MerlinsHouseMembershipCard,
+ LocationName.MerlinsHouseBlizzardElement,
+ LocationName.BaileySecretAnsemReport7,
+ LocationName.BaseballCharm,
+ LocationName.AnsemsStudyMasterForm,
+ LocationName.AnsemsStudySkillRecipe,
+ LocationName.AnsemsStudySleepingLion,
+ LocationName.FFFightsCureElement,
+ LocationName.ThousandHeartlessSecretAnsemReport1,
+ LocationName.ThousandHeartlessIceCream,
+ LocationName.ThousandHeartlessPicture,
+ LocationName.WinnersProof,
+ LocationName.ProofofPeace,
+ LocationName.SephirothFenrir,
+ LocationName.EncampmentAreaMap,
+ LocationName.Mission3,
+ LocationName.VillageCaveAreaMap,
+ LocationName.HiddenDragon,
+ LocationName.ColiseumMap,
+ LocationName.SecretAnsemReport6,
+ LocationName.OlympusStone,
+ LocationName.HerosCrest,
+ LocationName.AuronsStatue,
+ LocationName.GuardianSoul,
+ LocationName.ProtectBeltPainandPanicCup,
+ LocationName.SerenityGemPainandPanicCup,
+ LocationName.RisingDragonCerberusCup,
+ LocationName.SerenityCrystalCerberusCup,
+ LocationName.GenjiShieldTitanCup,
+ LocationName.SkillfulRingTitanCup,
+ LocationName.FatalCrestGoddessofFateCup,
+ LocationName.OrichalcumPlusGoddessofFateCup,
+ LocationName.HadesCupTrophyParadoxCups,
+ LocationName.IsladeMuertaMap,
+ LocationName.FollowtheWind,
+ LocationName.SeadriftRowCursedMedallion,
+ LocationName.SeadriftRowShipGraveyardMap,
+ LocationName.SecretAnsemReport5,
+ LocationName.CircleofLife,
+ LocationName.ScarFireElement,
+ LocationName.TwilightTownMap,
+ LocationName.MunnyPouchOlette,
+ LocationName.JunkChampionBelt,
+ LocationName.JunkMedal,
+ LocationName.TheStruggleTrophy,
+ LocationName.NaminesSketches,
+ LocationName.MansionMap,
+ LocationName.PhotonDebugger,
+ LocationName.StationPlazaSecretAnsemReport2,
+ LocationName.MunnyPouchMickey,
+ LocationName.CrystalOrb,
+ LocationName.StarSeeker,
+ LocationName.ValorForm,
+ LocationName.SeifersTrophy,
+ LocationName.Oathkeeper,
+ LocationName.LimitForm,
+ LocationName.BeamSecretAnsemReport10,
+ LocationName.BetwixtandBetweenBondofFlame,
+ LocationName.TwoBecomeOne,
+ LocationName.RoxasSecretAnsemReport8,
+ LocationName.XigbarSecretAnsemReport3,
+ LocationName.Oblivion,
+ LocationName.CastleThatNeverWasMap,
+ LocationName.LuxordSecretAnsemReport9,
+ LocationName.SaixSecretAnsemReport12,
+ LocationName.PreXemnas1SecretAnsemReport11,
+ LocationName.Xemnas1SecretAnsemReport13,
+ LocationName.XemnasDataPowerBoost,
+ LocationName.AxelDataMagicBoost,
+ LocationName.RoxasDataMagicBoost,
+ LocationName.SaixDataDefenseBoost,
+ LocationName.DemyxDataAPBoost,
+ LocationName.LuxordDataAPBoost,
+ LocationName.VexenDataLostIllusion,
+ LocationName.LarxeneDataLostIllusion,
+ LocationName.XaldinDataDefenseBoost,
+ LocationName.MarluxiaDataLostIllusion,
+ LocationName.LexaeusDataLostIllusion,
+ LocationName.XigbarDataDefenseBoost,
+ LocationName.VexenASRoadtoDiscovery,
+ LocationName.LarxeneASCloakedThunder,
+ LocationName.ZexionASBookofShadows,
+ LocationName.ZexionDataLostIllusion,
+ LocationName.LexaeusASStrengthBeyondStrength,
+ LocationName.MarluxiaASEternalBlossom,
+ LocationName.UnderseaKingdomMap,
+ LocationName.MysteriousAbyss,
+ LocationName.MusicalBlizzardElement,
+ LocationName.MusicalOrichalcumPlus,
}
exclusion_table = {
- "Popups": {
- LocationName.SweetMemories,
- LocationName.SpookyCaveMap,
- LocationName.StarryHillCureElement,
- LocationName.StarryHillOrichalcumPlus,
- LocationName.AgrabahMap,
- LocationName.LampCharm,
- LocationName.WishingLamp,
- LocationName.DarkThornCureElement,
- LocationName.RumblingRose,
- LocationName.CastleWallsMap,
- LocationName.SecretAnsemReport4,
- LocationName.DisneyCastleMap,
- LocationName.WindowofTimeMap,
- LocationName.Monochrome,
- LocationName.WisdomForm,
+ "SuperBosses": {
+ LocationName.LingeringWillBonus,
LocationName.LingeringWillProofofConnection,
LocationName.LingeringWillManifestIllusion,
- LocationName.OogieBoogieMagnetElement,
- LocationName.Present,
- LocationName.DecoyPresents,
- LocationName.DecisivePumpkin,
- LocationName.MarketplaceMap,
- LocationName.MerlinsHouseMembershipCard,
- LocationName.MerlinsHouseBlizzardElement,
- LocationName.BaileySecretAnsemReport7,
- LocationName.BaseballCharm,
- LocationName.AnsemsStudyMasterForm,
- LocationName.AnsemsStudySkillRecipe,
- LocationName.AnsemsStudySleepingLion,
- LocationName.FFFightsCureElement,
- LocationName.ThousandHeartlessSecretAnsemReport1,
- LocationName.ThousandHeartlessIceCream,
- LocationName.ThousandHeartlessPicture,
- LocationName.WinnersProof,
- LocationName.ProofofPeace,
+ LocationName.SephirothBonus,
LocationName.SephirothFenrir,
- LocationName.EncampmentAreaMap,
- LocationName.Mission3,
- LocationName.VillageCaveAreaMap,
- LocationName.HiddenDragon,
- LocationName.ColiseumMap,
- LocationName.SecretAnsemReport6,
- LocationName.OlympusStone,
- LocationName.HerosCrest,
- LocationName.AuronsStatue,
- LocationName.GuardianSoul,
- LocationName.ProtectBeltPainandPanicCup,
- LocationName.SerenityGemPainandPanicCup,
- LocationName.RisingDragonCerberusCup,
- LocationName.SerenityCrystalCerberusCup,
- LocationName.GenjiShieldTitanCup,
- LocationName.SkillfulRingTitanCup,
- LocationName.FatalCrestGoddessofFateCup,
- LocationName.OrichalcumPlusGoddessofFateCup,
- LocationName.HadesCupTrophyParadoxCups,
- LocationName.IsladeMuertaMap,
- LocationName.FollowtheWind,
- LocationName.SeadriftRowCursedMedallion,
- LocationName.SeadriftRowShipGraveyardMap,
- LocationName.SecretAnsemReport5,
- LocationName.CircleofLife,
- LocationName.ScarFireElement,
- LocationName.TwilightTownMap,
- LocationName.MunnyPouchOlette,
- LocationName.JunkChampionBelt,
- LocationName.JunkMedal,
- LocationName.TheStruggleTrophy,
- LocationName.NaminesSketches,
- LocationName.MansionMap,
- LocationName.PhotonDebugger,
- LocationName.StationPlazaSecretAnsemReport2,
- LocationName.MunnyPouchMickey,
- LocationName.CrystalOrb,
- LocationName.StarSeeker,
- LocationName.ValorForm,
- LocationName.SeifersTrophy,
- LocationName.Oathkeeper,
- LocationName.LimitForm,
- LocationName.BeamSecretAnsemReport10,
- LocationName.BetwixtandBetweenBondofFlame,
- LocationName.TwoBecomeOne,
- LocationName.RoxasSecretAnsemReport8,
- LocationName.XigbarSecretAnsemReport3,
- LocationName.Oblivion,
- LocationName.CastleThatNeverWasMap,
- LocationName.LuxordSecretAnsemReport9,
- LocationName.SaixSecretAnsemReport12,
- LocationName.PreXemnas1SecretAnsemReport11,
- LocationName.Xemnas1SecretAnsemReport13,
- LocationName.XemnasDataPowerBoost,
- LocationName.AxelDataMagicBoost,
- LocationName.RoxasDataMagicBoost,
- LocationName.SaixDataDefenseBoost,
- LocationName.DemyxDataAPBoost,
- LocationName.LuxordDataAPBoost,
- LocationName.VexenDataLostIllusion,
- LocationName.LarxeneDataLostIllusion,
- LocationName.XaldinDataDefenseBoost,
- LocationName.MarluxiaDataLostIllusion,
- LocationName.LexaeusDataLostIllusion,
- LocationName.XigbarDataDefenseBoost,
- LocationName.VexenASRoadtoDiscovery,
- LocationName.LarxeneASCloakedThunder,
- LocationName.ZexionASBookofShadows,
- LocationName.ZexionDataLostIllusion,
- LocationName.LexaeusASStrengthBeyondStrength,
- LocationName.MarluxiaASEternalBlossom
- },
- "Datas": {
LocationName.XemnasDataPowerBoost,
LocationName.AxelDataMagicBoost,
LocationName.RoxasDataMagicBoost,
@@ -985,13 +1097,7 @@ class LocationData(typing.NamedTuple):
LocationName.ZexionDataLostIllusion,
LocationName.ZexionBonus,
LocationName.ZexionASBookofShadows,
- },
- "SuperBosses": {
- LocationName.LingeringWillBonus,
- LocationName.LingeringWillProofofConnection,
- LocationName.LingeringWillManifestIllusion,
- LocationName.SephirothBonus,
- LocationName.SephirothFenrir,
+ LocationName.GoofyZexion,
},
# 23 checks spread through 50 levels
@@ -1148,15 +1254,6 @@ class LocationData(typing.NamedTuple):
LocationName.Lvl98,
LocationName.Lvl99,
},
- "Critical": {
- LocationName.Crit_1,
- LocationName.Crit_2,
- LocationName.Crit_3,
- LocationName.Crit_4,
- LocationName.Crit_5,
- LocationName.Crit_6,
- LocationName.Crit_7,
- },
"Hitlist": [
LocationName.XemnasDataPowerBoost,
LocationName.AxelDataMagicBoost,
@@ -1179,9 +1276,11 @@ class LocationData(typing.NamedTuple):
LocationName.Limitlvl7,
LocationName.Masterlvl7,
LocationName.Finallvl7,
+ LocationName.Summonlvl7,
LocationName.TransporttoRemembrance,
LocationName.OrichalcumPlusGoddessofFateCup,
LocationName.HadesCupTrophyParadoxCups,
+ LocationName.MusicalOrichalcumPlus,
],
"Cups": {
LocationName.ProtectBeltPainandPanicCup,
@@ -1194,6 +1293,12 @@ class LocationData(typing.NamedTuple):
LocationName.OrichalcumPlusGoddessofFateCup,
LocationName.HadesCupTrophyParadoxCups,
},
+ "Atlantica": {
+ LocationName.MysteriousAbyss,
+ LocationName.MusicalOrichalcumPlus,
+ LocationName.MusicalBlizzardElement,
+ LocationName.UnderseaKingdomMap,
+ },
"WeaponSlots": {
LocationName.FAKESlot: ItemName.ValorForm,
LocationName.DetectionSaberSlot: ItemName.MasterForm,
@@ -1244,536 +1349,12 @@ class LocationData(typing.NamedTuple):
LocationName.Centurion2: ItemName.Centurion2,
},
"Chests": {
- LocationName.BambooGroveDarkShard,
- LocationName.BambooGroveEther,
- LocationName.BambooGroveMythrilShard,
- LocationName.CheckpointHiPotion,
- LocationName.CheckpointMythrilShard,
- LocationName.MountainTrailLightningShard,
- LocationName.MountainTrailRecoveryRecipe,
- LocationName.MountainTrailEther,
- LocationName.MountainTrailMythrilShard,
- LocationName.VillageCaveAPBoost,
- LocationName.VillageCaveDarkShard,
- LocationName.RidgeFrostShard,
- LocationName.RidgeAPBoost,
- LocationName.ThroneRoomTornPages,
- LocationName.ThroneRoomPalaceMap,
- LocationName.ThroneRoomAPBoost,
- LocationName.ThroneRoomQueenRecipe,
- LocationName.ThroneRoomAPBoost2,
- LocationName.ThroneRoomOgreShield,
- LocationName.ThroneRoomMythrilCrystal,
- LocationName.ThroneRoomOrichalcum,
- LocationName.AgrabahDarkShard,
- LocationName.AgrabahMythrilShard,
- LocationName.AgrabahHiPotion,
- LocationName.AgrabahAPBoost,
- LocationName.AgrabahMythrilStone,
- LocationName.AgrabahMythrilShard2,
- LocationName.AgrabahSerenityShard,
- LocationName.BazaarMythrilGem,
- LocationName.BazaarPowerShard,
- LocationName.BazaarHiPotion,
- LocationName.BazaarAPBoost,
- LocationName.BazaarMythrilShard,
- LocationName.PalaceWallsSkillRing,
- LocationName.PalaceWallsMythrilStone,
- LocationName.CaveEntrancePowerStone,
- LocationName.CaveEntranceMythrilShard,
- LocationName.ValleyofStoneMythrilStone,
- LocationName.ValleyofStoneAPBoost,
- LocationName.ValleyofStoneMythrilShard,
- LocationName.ValleyofStoneHiPotion,
- LocationName.ChasmofChallengesCaveofWondersMap,
- LocationName.ChasmofChallengesAPBoost,
- LocationName.TreasureRoomAPBoost,
- LocationName.TreasureRoomSerenityGem,
- LocationName.RuinedChamberTornPages,
- LocationName.RuinedChamberRuinsMap,
- LocationName.DCCourtyardMythrilShard,
- LocationName.DCCourtyardStarRecipe,
- LocationName.DCCourtyardAPBoost,
- LocationName.DCCourtyardMythrilStone,
- LocationName.DCCourtyardBlazingStone,
- LocationName.DCCourtyardBlazingShard,
- LocationName.DCCourtyardMythrilShard2,
- LocationName.LibraryTornPages,
- LocationName.CornerstoneHillMap,
- LocationName.CornerstoneHillFrostShard,
- LocationName.PierMythrilShard,
- LocationName.PierHiPotion,
- LocationName.WaterwayMythrilStone,
- LocationName.WaterwayAPBoost,
- LocationName.WaterwayFrostStone,
- LocationName.PoohsHouse100AcreWoodMap,
- LocationName.PoohsHouseAPBoost,
- LocationName.PoohsHouseMythrilStone,
- LocationName.PigletsHouseDefenseBoost,
- LocationName.PigletsHouseAPBoost,
- LocationName.PigletsHouseMythrilGem,
- LocationName.RabbitsHouseDrawRing,
- LocationName.RabbitsHouseMythrilCrystal,
- LocationName.RabbitsHouseAPBoost,
- LocationName.KangasHouseMagicBoost,
- LocationName.KangasHouseAPBoost,
- LocationName.KangasHouseOrichalcum,
- LocationName.SpookyCaveMythrilGem,
- LocationName.SpookyCaveAPBoost,
- LocationName.SpookyCaveOrichalcum,
- LocationName.SpookyCaveGuardRecipe,
- LocationName.SpookyCaveMythrilCrystal,
- LocationName.SpookyCaveAPBoost2,
- LocationName.StarryHillCosmicRing,
- LocationName.StarryHillStyleRecipe,
- LocationName.RampartNavalMap,
- LocationName.RampartMythrilStone,
- LocationName.RampartDarkShard,
- LocationName.TownDarkStone,
- LocationName.TownAPBoost,
- LocationName.TownMythrilShard,
- LocationName.TownMythrilGem,
- LocationName.CaveMouthBrightShard,
- LocationName.CaveMouthMythrilShard,
- LocationName.PowderStoreAPBoost1,
- LocationName.PowderStoreAPBoost2,
- LocationName.MoonlightNookMythrilShard,
- LocationName.MoonlightNookSerenityGem,
- LocationName.MoonlightNookPowerStone,
- LocationName.InterceptorsHoldFeatherCharm,
- LocationName.SeadriftKeepAPBoost,
- LocationName.SeadriftKeepOrichalcum,
- LocationName.SeadriftKeepMeteorStaff,
- LocationName.SeadriftRowSerenityGem,
- LocationName.SeadriftRowKingRecipe,
- LocationName.SeadriftRowMythrilCrystal,
- LocationName.PassageMythrilShard,
- LocationName.PassageMythrilStone,
- LocationName.PassageEther,
- LocationName.PassageAPBoost,
- LocationName.PassageHiPotion,
- LocationName.InnerChamberUnderworldMap,
- LocationName.InnerChamberMythrilShard,
- LocationName.UnderworldEntrancePowerBoost,
- LocationName.CavernsEntranceLucidShard,
- LocationName.CavernsEntranceAPBoost,
- LocationName.CavernsEntranceMythrilShard,
- LocationName.TheLostRoadBrightShard,
- LocationName.TheLostRoadEther,
- LocationName.TheLostRoadMythrilShard,
- LocationName.TheLostRoadMythrilStone,
- LocationName.AtriumLucidStone,
- LocationName.AtriumAPBoost,
- LocationName.TheLockCavernsMap,
- LocationName.TheLockMythrilShard,
- LocationName.TheLockAPBoost,
- LocationName.BCCourtyardAPBoost,
- LocationName.BCCourtyardHiPotion,
- LocationName.BCCourtyardMythrilShard,
- LocationName.BellesRoomCastleMap,
- LocationName.BellesRoomMegaRecipe,
- LocationName.TheEastWingMythrilShard,
- LocationName.TheEastWingTent,
- LocationName.TheWestHallHiPotion,
- LocationName.TheWestHallPowerShard,
- LocationName.TheWestHallMythrilShard2,
- LocationName.TheWestHallBrightStone,
- LocationName.TheWestHallMythrilShard,
- LocationName.DungeonBasementMap,
- LocationName.DungeonAPBoost,
- LocationName.SecretPassageMythrilShard,
- LocationName.SecretPassageHiPotion,
- LocationName.SecretPassageLucidShard,
- LocationName.TheWestHallAPBoostPostDungeon,
- LocationName.TheWestWingMythrilShard,
- LocationName.TheWestWingTent,
- LocationName.TheBeastsRoomBlazingShard,
- LocationName.PitCellAreaMap,
- LocationName.PitCellMythrilCrystal,
- LocationName.CanyonDarkCrystal,
- LocationName.CanyonMythrilStone,
- LocationName.CanyonMythrilGem,
- LocationName.CanyonFrostCrystal,
- LocationName.HallwayPowerCrystal,
- LocationName.HallwayAPBoost,
- LocationName.CommunicationsRoomIOTowerMap,
- LocationName.CommunicationsRoomGaiaBelt,
- LocationName.CentralComputerCoreAPBoost,
- LocationName.CentralComputerCoreOrichalcumPlus,
- LocationName.CentralComputerCoreCosmicArts,
- LocationName.CentralComputerCoreMap,
- LocationName.GraveyardMythrilShard,
- LocationName.GraveyardSerenityGem,
- LocationName.FinklesteinsLabHalloweenTownMap,
- LocationName.TownSquareMythrilStone,
- LocationName.TownSquareEnergyShard,
- LocationName.HinterlandsLightningShard,
- LocationName.HinterlandsMythrilStone,
- LocationName.HinterlandsAPBoost,
- LocationName.CandyCaneLaneMegaPotion,
- LocationName.CandyCaneLaneMythrilGem,
- LocationName.CandyCaneLaneLightningStone,
- LocationName.CandyCaneLaneMythrilStone,
- LocationName.SantasHouseChristmasTownMap,
- LocationName.SantasHouseAPBoost,
- LocationName.BoroughDriveRecovery,
- LocationName.BoroughAPBoost,
- LocationName.BoroughHiPotion,
- LocationName.BoroughMythrilShard,
- LocationName.BoroughDarkShard,
- LocationName.PosternCastlePerimeterMap,
- LocationName.PosternMythrilGem,
- LocationName.PosternAPBoost,
- LocationName.CorridorsMythrilStone,
- LocationName.CorridorsMythrilCrystal,
- LocationName.CorridorsDarkCrystal,
- LocationName.CorridorsAPBoost,
- LocationName.AnsemsStudyUkuleleCharm,
- LocationName.RestorationSiteMoonRecipe,
- LocationName.RestorationSiteAPBoost,
- LocationName.CoRDepthsAPBoost,
- LocationName.CoRDepthsPowerCrystal,
- LocationName.CoRDepthsFrostCrystal,
- LocationName.CoRDepthsManifestIllusion,
- LocationName.CoRDepthsAPBoost2,
- LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap,
- LocationName.CoRMineshaftLowerLevelAPBoost,
- LocationName.CrystalFissureTornPages,
- LocationName.CrystalFissureTheGreatMawMap,
- LocationName.CrystalFissureEnergyCrystal,
- LocationName.CrystalFissureAPBoost,
- LocationName.PosternGullWing,
- LocationName.HeartlessManufactoryCosmicChain,
- LocationName.CoRDepthsUpperLevelRemembranceGem,
- LocationName.CoRMiningAreaSerenityGem,
- LocationName.CoRMiningAreaAPBoost,
- LocationName.CoRMiningAreaSerenityCrystal,
- LocationName.CoRMiningAreaManifestIllusion,
- LocationName.CoRMiningAreaSerenityGem2,
- LocationName.CoRMiningAreaDarkRemembranceMap,
- LocationName.CoRMineshaftMidLevelPowerBoost,
- LocationName.CoREngineChamberSerenityCrystal,
- LocationName.CoREngineChamberRemembranceCrystal,
- LocationName.CoREngineChamberAPBoost,
- LocationName.CoREngineChamberManifestIllusion,
- LocationName.CoRMineshaftUpperLevelMagicBoost,
- LocationName.CoRMineshaftUpperLevelAPBoost,
- LocationName.GorgeSavannahMap,
- LocationName.GorgeDarkGem,
- LocationName.GorgeMythrilStone,
- LocationName.ElephantGraveyardFrostGem,
- LocationName.ElephantGraveyardMythrilStone,
- LocationName.ElephantGraveyardBrightStone,
- LocationName.ElephantGraveyardAPBoost,
- LocationName.ElephantGraveyardMythrilShard,
- LocationName.PrideRockMap,
- LocationName.PrideRockMythrilStone,
- LocationName.PrideRockSerenityCrystal,
- LocationName.WildebeestValleyEnergyStone,
- LocationName.WildebeestValleyAPBoost,
- LocationName.WildebeestValleyMythrilGem,
- LocationName.WildebeestValleyMythrilStone,
- LocationName.WildebeestValleyLucidGem,
- LocationName.WastelandsMythrilShard,
- LocationName.WastelandsSerenityGem,
- LocationName.WastelandsMythrilStone,
- LocationName.JungleSerenityGem,
- LocationName.JungleMythrilStone,
- LocationName.JungleSerenityCrystal,
- LocationName.OasisMap,
- LocationName.OasisTornPages,
- LocationName.OasisAPBoost,
- LocationName.StationofCallingPotion,
- LocationName.CentralStationPotion1,
- LocationName.STTCentralStationHiPotion,
- LocationName.CentralStationPotion2,
- LocationName.SunsetTerraceAbilityRing,
- LocationName.SunsetTerraceHiPotion,
- LocationName.SunsetTerracePotion1,
- LocationName.SunsetTerracePotion2,
- LocationName.MansionFoyerHiPotion,
- LocationName.MansionFoyerPotion1,
- LocationName.MansionFoyerPotion2,
- LocationName.MansionDiningRoomElvenBandanna,
- LocationName.MansionDiningRoomPotion,
- LocationName.MansionLibraryHiPotion,
- LocationName.MansionBasementCorridorHiPotion,
- LocationName.OldMansionPotion,
- LocationName.OldMansionMythrilShard,
- LocationName.TheWoodsPotion,
- LocationName.TheWoodsMythrilShard,
- LocationName.TheWoodsHiPotion,
- LocationName.TramCommonHiPotion,
- LocationName.TramCommonAPBoost,
- LocationName.TramCommonTent,
- LocationName.TramCommonMythrilShard1,
- LocationName.TramCommonPotion1,
- LocationName.TramCommonMythrilShard2,
- LocationName.TramCommonPotion2,
- LocationName.CentralStationTent,
- LocationName.TTCentralStationHiPotion,
- LocationName.CentralStationMythrilShard,
- LocationName.TheTowerPotion,
- LocationName.TheTowerHiPotion,
- LocationName.TheTowerEther,
- LocationName.TowerEntrywayEther,
- LocationName.TowerEntrywayMythrilShard,
- LocationName.SorcerersLoftTowerMap,
- LocationName.TowerWardrobeMythrilStone,
- LocationName.UndergroundConcourseMythrilGem,
- LocationName.UndergroundConcourseAPBoost,
- LocationName.UndergroundConcourseMythrilCrystal,
- LocationName.UndergroundConcourseOrichalcum,
- LocationName.TunnelwayOrichalcum,
- LocationName.TunnelwayMythrilCrystal,
- LocationName.SunsetTerraceOrichalcumPlus,
- LocationName.SunsetTerraceMythrilShard,
- LocationName.SunsetTerraceMythrilCrystal,
- LocationName.SunsetTerraceAPBoost,
- LocationName.MansionFoyerMythrilCrystal,
- LocationName.MansionFoyerMythrilStone,
- LocationName.MansionFoyerSerenityCrystal,
- LocationName.MansionDiningRoomMythrilCrystal,
- LocationName.MansionDiningRoomMythrilStone,
- LocationName.MansionLibraryOrichalcum,
- LocationName.MansionBasementCorridorUltimateRecipe,
- LocationName.FragmentCrossingMythrilStone,
- LocationName.FragmentCrossingMythrilCrystal,
- LocationName.FragmentCrossingAPBoost,
- LocationName.FragmentCrossingOrichalcum,
- LocationName.MemorysSkyscaperMythrilCrystal,
- LocationName.MemorysSkyscaperAPBoost,
- LocationName.MemorysSkyscaperMythrilStone,
- LocationName.TheBrinkofDespairDarkCityMap,
- LocationName.TheBrinkofDespairOrichalcumPlus,
- LocationName.NothingsCallMythrilGem,
- LocationName.NothingsCallOrichalcum,
- LocationName.TwilightsViewCosmicBelt,
- LocationName.NaughtsSkywayMythrilGem,
- LocationName.NaughtsSkywayOrichalcum,
- LocationName.NaughtsSkywayMythrilCrystal,
- LocationName.RuinandCreationsPassageMythrilStone,
- LocationName.RuinandCreationsPassageAPBoost,
- LocationName.RuinandCreationsPassageMythrilCrystal,
- LocationName.RuinandCreationsPassageOrichalcum,
- LocationName.GardenofAssemblageMap,
- LocationName.GoALostIllusion,
- LocationName.ProofofNonexistence,
+ location for location, data in all_locations.items() if location not in event_location_to_item.keys() and location not in popups_set and location != LocationName.StationofSerenityPotion and data.yml == "Chest"
}
}
-AllWeaponSlot = {
- LocationName.FAKESlot,
- LocationName.DetectionSaberSlot,
- LocationName.EdgeofUltimaSlot,
- LocationName.KingdomKeySlot,
- LocationName.OathkeeperSlot,
- LocationName.OblivionSlot,
- LocationName.StarSeekerSlot,
- LocationName.HiddenDragonSlot,
- LocationName.HerosCrestSlot,
- LocationName.MonochromeSlot,
- LocationName.FollowtheWindSlot,
- LocationName.CircleofLifeSlot,
- LocationName.PhotonDebuggerSlot,
- LocationName.GullWingSlot,
- LocationName.RumblingRoseSlot,
- LocationName.GuardianSoulSlot,
- LocationName.WishingLampSlot,
- LocationName.DecisivePumpkinSlot,
- LocationName.SweetMemoriesSlot,
- LocationName.MysteriousAbyssSlot,
- LocationName.SleepingLionSlot,
- LocationName.BondofFlameSlot,
- LocationName.TwoBecomeOneSlot,
- LocationName.FatalCrestSlot,
- LocationName.FenrirSlot,
- LocationName.UltimaWeaponSlot,
- LocationName.WinnersProofSlot,
- LocationName.PurebloodSlot,
- LocationName.Centurion2,
- LocationName.CometStaff,
- LocationName.HammerStaff,
- LocationName.LordsBroom,
- LocationName.MagesStaff,
- LocationName.MeteorStaff,
- LocationName.NobodyLance,
- LocationName.PreciousMushroom,
- LocationName.PreciousMushroom2,
- LocationName.PremiumMushroom,
- LocationName.RisingDragon,
- LocationName.SaveTheQueen2,
- LocationName.ShamansRelic,
- LocationName.VictoryBell,
- LocationName.WisdomWand,
-
- LocationName.AdamantShield,
- LocationName.AkashicRecord,
- LocationName.ChainGear,
- LocationName.DreamCloud,
- LocationName.FallingStar,
- LocationName.FrozenPride2,
- LocationName.GenjiShield,
- LocationName.KnightDefender,
- LocationName.KnightsShield,
- LocationName.MajesticMushroom,
- LocationName.MajesticMushroom2,
- LocationName.NobodyGuard,
- LocationName.OgreShield,
- LocationName.SaveTheKing2,
- LocationName.UltimateMushroom, }
-RegionTable = {
- "FirstVisits": {
- RegionName.LoD_Region,
- RegionName.Ag_Region,
- RegionName.Dc_Region,
- RegionName.Pr_Region,
- RegionName.Oc_Region,
- RegionName.Bc_Region,
- RegionName.Sp_Region,
- RegionName.Ht_Region,
- RegionName.Hb_Region,
- RegionName.Pl_Region,
- RegionName.STT_Region,
- RegionName.TT_Region,
- RegionName.Twtnw_Region,
- },
- "SecondVisits": {
- RegionName.LoD2_Region,
- RegionName.Ag2_Region,
- RegionName.Tr_Region,
- RegionName.Pr2_Region,
- RegionName.Oc2_Region,
- RegionName.Bc2_Region,
- RegionName.Sp2_Region,
- RegionName.Ht2_Region,
- RegionName.Hb2_Region,
- RegionName.Pl2_Region,
- RegionName.STT_Region,
- RegionName.Twtnw2_Region,
- },
- "ValorRegion": {
- RegionName.LoD_Region,
- RegionName.Ag_Region,
- RegionName.Dc_Region,
- RegionName.Pr_Region,
- RegionName.Oc_Region,
- RegionName.Bc_Region,
- RegionName.Sp_Region,
- RegionName.Ht_Region,
- RegionName.Hb_Region,
- RegionName.TT_Region,
- RegionName.Twtnw_Region,
- },
- "WisdomRegion": {
- RegionName.LoD_Region,
- RegionName.Ag_Region,
- RegionName.Dc_Region,
- RegionName.Pr_Region,
- RegionName.Oc_Region,
- RegionName.Bc_Region,
- RegionName.Sp_Region,
- RegionName.Ht_Region,
- RegionName.Hb_Region,
- RegionName.TT_Region,
- RegionName.Twtnw_Region,
- },
- "LimitRegion": {
- RegionName.LoD_Region,
- RegionName.Ag_Region,
- RegionName.Dc_Region,
- RegionName.Pr_Region,
- RegionName.Oc_Region,
- RegionName.Bc_Region,
- RegionName.Sp_Region,
- RegionName.Ht_Region,
- RegionName.Hb_Region,
- RegionName.TT_Region,
- RegionName.Twtnw_Region,
- RegionName.STT_Region,
- },
- "MasterRegion": {
- RegionName.LoD_Region,
- RegionName.Ag_Region,
- RegionName.Dc_Region,
- RegionName.Pr_Region,
- RegionName.Oc_Region,
- RegionName.Bc_Region,
- RegionName.Sp_Region,
- RegionName.Ht_Region,
- RegionName.Hb_Region,
- RegionName.TT_Region,
- RegionName.Twtnw_Region,
- }, # could add lod2 and bc2 as an option since those spawns are rng
- "FinalRegion": {
- RegionName.TT3_Region,
- RegionName.Twtnw_PostRoxas,
- RegionName.Twtnw2_Region,
- }
-}
-
-all_locations = {
- **TWTNW_Checks,
- **TWTNW2_Checks,
- **TT_Checks,
- **TT2_Checks,
- **TT3_Checks,
- **STT_Checks,
- **PL_Checks,
- **PL2_Checks,
- **CoR_Checks,
- **HB_Checks,
- **HB2_Checks,
- **HT_Checks,
- **HT2_Checks,
- **PR_Checks,
- **PR2_Checks,
- **PR_Checks,
- **PR2_Checks,
- **SP_Checks,
- **SP2_Checks,
- **BC_Checks,
- **BC2_Checks,
- **Oc_Checks,
- **Oc2_Checks,
- **Oc2Cups,
- **HundredAcre1_Checks,
- **HundredAcre2_Checks,
- **HundredAcre3_Checks,
- **HundredAcre4_Checks,
- **HundredAcre5_Checks,
- **HundredAcre6_Checks,
- **DC_Checks,
- **TR_Checks,
- **AG_Checks,
- **AG2_Checks,
- **LoD_Checks,
- **LoD2_Checks,
- **SoraLevels,
- **Form_Checks,
- **GoA_Checks,
- **Keyblade_Slots,
- **Critical_Checks,
- **Donald_Checks,
- **Goofy_Checks,
+location_groups: typing.Dict[str, list]
+location_groups = {
+ Region_Name: [loc for loc in Region_Locs if "Event" not in loc]
+ for Region_Name, Region_Locs in KH2REGIONS.items() if Region_Locs and "Event" not in Region_Locs[0]
}
-
-location_table = {}
-
-
-def setup_locations():
- totallocation_table = {**TWTNW_Checks, **TWTNW2_Checks, **TT_Checks, **TT2_Checks, **TT3_Checks, **STT_Checks,
- **PL_Checks, **PL2_Checks, **CoR_Checks, **HB_Checks, **HB2_Checks,
- **PR_Checks, **PR2_Checks, **PR_Checks, **PR2_Checks, **SP_Checks, **SP2_Checks, **BC_Checks,
- **BC2_Checks, **HT_Checks, **HT2_Checks,
- **Oc_Checks, **Oc2_Checks, **Oc2Cups, **Critical_Checks, **Donald_Checks, **Goofy_Checks,
- **HundredAcre1_Checks, **HundredAcre2_Checks, **HundredAcre3_Checks, **HundredAcre4_Checks,
- **HundredAcre5_Checks, **HundredAcre6_Checks,
- **DC_Checks, **TR_Checks, **AG_Checks, **AG2_Checks, **LoD_Checks, **LoD2_Checks,
- **SoraLevels,
- **Form_Checks, **GoA_Checks, **Keyblade_Slots}
- return totallocation_table
-
-
-lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in location_table.items() if
- data.code}
diff --git a/worlds/kh2/Logic.py b/worlds/kh2/Logic.py
new file mode 100644
index 000000000000..1e6c403106d1
--- /dev/null
+++ b/worlds/kh2/Logic.py
@@ -0,0 +1,642 @@
+from .Names import ItemName, RegionName, LocationName
+
+# this file contains the dicts,lists and sets used for making rules in rules.py
+base_tools = [
+ ItemName.FinishingPlus,
+ ItemName.Guard,
+ ItemName.AerialRecovery
+]
+gap_closer = [
+ ItemName.SlideDash,
+ ItemName.FlashStep
+]
+defensive_tool = [
+ ItemName.ReflectElement,
+ ItemName.Guard
+]
+form_list = [
+ ItemName.ValorForm,
+ ItemName.WisdomForm,
+ ItemName.LimitForm,
+ ItemName.MasterForm,
+ ItemName.FinalForm
+]
+form_list_without_final = [
+ ItemName.ValorForm,
+ ItemName.WisdomForm,
+ ItemName.LimitForm,
+ ItemName.MasterForm
+]
+ground_finisher = [
+ ItemName.GuardBreak,
+ ItemName.Explosion,
+ ItemName.FinishingLeap
+]
+party_limit = [
+ ItemName.Fantasia,
+ ItemName.FlareForce,
+ ItemName.Teamwork,
+ ItemName.TornadoFusion
+]
+donald_limit = [
+ ItemName.Fantasia,
+ ItemName.FlareForce
+]
+aerial_move = [
+ ItemName.AerialDive,
+ ItemName.AerialSpiral,
+ ItemName.HorizontalSlash,
+ ItemName.AerialSweep,
+ ItemName.AerialFinish
+]
+level_3_form_loc = [
+ LocationName.Valorlvl3,
+ LocationName.Wisdomlvl3,
+ LocationName.Limitlvl3,
+ LocationName.Masterlvl3,
+ LocationName.Finallvl3
+]
+black_magic = [
+ ItemName.FireElement,
+ ItemName.BlizzardElement,
+ ItemName.ThunderElement
+]
+magic = [
+ ItemName.FireElement,
+ ItemName.BlizzardElement,
+ ItemName.ThunderElement,
+ ItemName.ReflectElement,
+ ItemName.CureElement,
+ ItemName.MagnetElement
+]
+summons = [
+ ItemName.ChickenLittle,
+ ItemName.Stitch,
+ ItemName.Genie,
+ ItemName.PeterPan
+]
+three_proofs = [
+ ItemName.ProofofConnection,
+ ItemName.ProofofPeace,
+ ItemName.ProofofNonexistence
+]
+
+auto_form_dict = {
+ ItemName.FinalForm: ItemName.AutoFinal,
+ ItemName.MasterForm: ItemName.AutoMaster,
+ ItemName.LimitForm: ItemName.AutoLimit,
+ ItemName.WisdomForm: ItemName.AutoWisdom,
+ ItemName.ValorForm: ItemName.AutoValor,
+}
+
+# could use comprehension for getting a list of the region objects but eh I like this more
+drive_form_list = [RegionName.Valor, RegionName.Wisdom, RegionName.Limit, RegionName.Master, RegionName.Final, RegionName.Summon]
+
+easy_data_xigbar_tools = {
+ ItemName.FinishingPlus: 1,
+ ItemName.Guard: 1,
+ ItemName.AerialDive: 1,
+ ItemName.HorizontalSlash: 1,
+ ItemName.AirComboPlus: 2,
+ ItemName.FireElement: 3,
+ ItemName.ReflectElement: 3,
+}
+normal_data_xigbar_tools = {
+ ItemName.FinishingPlus: 1,
+ ItemName.Guard: 1,
+ ItemName.HorizontalSlash: 1,
+ ItemName.FireElement: 3,
+ ItemName.ReflectElement: 3,
+}
+
+easy_data_lex_tools = {
+ ItemName.Guard: 1,
+ ItemName.FireElement: 3,
+ ItemName.ReflectElement: 2,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1
+}
+normal_data_lex_tools = {
+ ItemName.Guard: 1,
+ ItemName.FireElement: 3,
+ ItemName.ReflectElement: 1,
+}
+
+easy_data_marluxia_tools = {
+ ItemName.Guard: 1,
+ ItemName.FireElement: 3,
+ ItemName.ReflectElement: 2,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.AerialRecovery: 1,
+}
+normal_data_marluxia_tools = {
+ ItemName.Guard: 1,
+ ItemName.FireElement: 3,
+ ItemName.ReflectElement: 1,
+ ItemName.AerialRecovery: 1,
+}
+easy_terra_tools = {
+ ItemName.SecondChance: 1,
+ ItemName.OnceMore: 1,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.Explosion: 1,
+ ItemName.ComboPlus: 2,
+ ItemName.FireElement: 3,
+ ItemName.Fantasia: 1,
+ ItemName.FlareForce: 1,
+ ItemName.ReflectElement: 1,
+ ItemName.Guard: 1,
+ ItemName.DodgeRoll: 3,
+ ItemName.AerialDodge: 3,
+ ItemName.Glide: 3
+}
+normal_terra_tools = {
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.Explosion: 1,
+ ItemName.ComboPlus: 2,
+ ItemName.Guard: 1,
+ ItemName.DodgeRoll: 2,
+ ItemName.AerialDodge: 2,
+ ItemName.Glide: 2
+}
+hard_terra_tools = {
+ ItemName.Explosion: 1,
+ ItemName.ComboPlus: 2,
+ ItemName.DodgeRoll: 2,
+ ItemName.AerialDodge: 2,
+ ItemName.Glide: 2,
+ ItemName.Guard: 1
+}
+easy_data_luxord_tools = {
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.AerialDodge: 2,
+ ItemName.Glide: 2,
+ ItemName.ReflectElement: 3,
+ ItemName.Guard: 1,
+}
+easy_data_zexion = {
+ ItemName.FireElement: 3,
+ ItemName.SecondChance: 1,
+ ItemName.OnceMore: 1,
+ ItemName.Fantasia: 1,
+ ItemName.FlareForce: 1,
+ ItemName.ReflectElement: 3,
+ ItemName.Guard: 1,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.QuickRun: 3,
+}
+normal_data_zexion = {
+ ItemName.FireElement: 3,
+ ItemName.ReflectElement: 3,
+ ItemName.Guard: 1,
+ ItemName.QuickRun: 3
+}
+hard_data_zexion = {
+ ItemName.FireElement: 2,
+ ItemName.ReflectElement: 1,
+ ItemName.QuickRun: 2,
+}
+easy_data_xaldin = {
+ ItemName.FireElement: 3,
+ ItemName.AirComboPlus: 2,
+ ItemName.FinishingPlus: 1,
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 3,
+ ItemName.FlareForce: 1,
+ ItemName.Fantasia: 1,
+ ItemName.HighJump: 3,
+ ItemName.AerialDodge: 3,
+ ItemName.Glide: 3,
+ ItemName.MagnetElement: 1,
+ ItemName.HorizontalSlash: 1,
+ ItemName.AerialDive: 1,
+ ItemName.AerialSpiral: 1,
+ ItemName.BerserkCharge: 1
+}
+normal_data_xaldin = {
+ ItemName.FireElement: 3,
+ ItemName.FinishingPlus: 1,
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 3,
+ ItemName.FlareForce: 1,
+ ItemName.Fantasia: 1,
+ ItemName.HighJump: 3,
+ ItemName.AerialDodge: 3,
+ ItemName.Glide: 3,
+ ItemName.MagnetElement: 1,
+ ItemName.HorizontalSlash: 1,
+ ItemName.AerialDive: 1,
+ ItemName.AerialSpiral: 1,
+}
+hard_data_xaldin = {
+ ItemName.FireElement: 2,
+ ItemName.FinishingPlus: 1,
+ ItemName.Guard: 1,
+ ItemName.HighJump: 2,
+ ItemName.AerialDodge: 2,
+ ItemName.Glide: 2,
+ ItemName.MagnetElement: 1,
+ ItemName.AerialDive: 1
+}
+easy_data_larxene = {
+ ItemName.FireElement: 3,
+ ItemName.SecondChance: 1,
+ ItemName.OnceMore: 1,
+ ItemName.Fantasia: 1,
+ ItemName.FlareForce: 1,
+ ItemName.ReflectElement: 3,
+ ItemName.Guard: 1,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.AerialDodge: 3,
+ ItemName.Glide: 3,
+ ItemName.GuardBreak: 1,
+ ItemName.Explosion: 1
+}
+normal_data_larxene = {
+ ItemName.FireElement: 3,
+ ItemName.ReflectElement: 3,
+ ItemName.Guard: 1,
+ ItemName.AerialDodge: 3,
+ ItemName.Glide: 3,
+}
+hard_data_larxene = {
+ ItemName.FireElement: 2,
+ ItemName.ReflectElement: 1,
+ ItemName.Guard: 1,
+ ItemName.AerialDodge: 2,
+ ItemName.Glide: 2,
+}
+easy_data_vexen = {
+ ItemName.FireElement: 3,
+ ItemName.SecondChance: 1,
+ ItemName.OnceMore: 1,
+ ItemName.Fantasia: 1,
+ ItemName.FlareForce: 1,
+ ItemName.ReflectElement: 3,
+ ItemName.Guard: 1,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.AerialDodge: 3,
+ ItemName.Glide: 3,
+ ItemName.GuardBreak: 1,
+ ItemName.Explosion: 1,
+ ItemName.DodgeRoll: 3,
+ ItemName.QuickRun: 3,
+}
+normal_data_vexen = {
+ ItemName.FireElement: 3,
+ ItemName.ReflectElement: 3,
+ ItemName.Guard: 1,
+ ItemName.AerialDodge: 3,
+ ItemName.Glide: 3,
+ ItemName.DodgeRoll: 3,
+ ItemName.QuickRun: 3,
+}
+hard_data_vexen = {
+ ItemName.FireElement: 2,
+ ItemName.ReflectElement: 1,
+ ItemName.Guard: 1,
+ ItemName.AerialDodge: 2,
+ ItemName.Glide: 2,
+ ItemName.DodgeRoll: 3,
+ ItemName.QuickRun: 3,
+}
+easy_thousand_heartless_rules = {
+ ItemName.SecondChance: 1,
+ ItemName.OnceMore: 1,
+ ItemName.Guard: 1,
+ ItemName.MagnetElement: 2,
+}
+normal_thousand_heartless_rules = {
+ ItemName.LimitForm: 1,
+ ItemName.Guard: 1,
+}
+easy_data_demyx = {
+ ItemName.FormBoost: 1,
+ ItemName.ReflectElement: 2,
+ ItemName.FireElement: 3,
+ ItemName.FlareForce: 1,
+ ItemName.Guard: 1,
+ ItemName.SecondChance: 1,
+ ItemName.OnceMore: 1,
+ ItemName.FinishingPlus: 1,
+}
+normal_data_demyx = {
+ ItemName.ReflectElement: 2,
+ ItemName.FireElement: 3,
+ ItemName.FlareForce: 1,
+ ItemName.Guard: 1,
+ ItemName.FinishingPlus: 1,
+}
+hard_data_demyx = {
+ ItemName.ReflectElement: 1,
+ ItemName.FireElement: 2,
+ ItemName.FlareForce: 1,
+ ItemName.Guard: 1,
+ ItemName.FinishingPlus: 1,
+}
+easy_sephiroth_tools = {
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 3,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.GuardBreak: 1,
+ ItemName.Explosion: 1,
+ ItemName.DodgeRoll: 3,
+ ItemName.FinishingPlus: 1,
+ ItemName.SecondChance: 1,
+ ItemName.OnceMore: 1,
+}
+normal_sephiroth_tools = {
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 2,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.GuardBreak: 1,
+ ItemName.Explosion: 1,
+ ItemName.DodgeRoll: 3,
+ ItemName.FinishingPlus: 1,
+}
+hard_sephiroth_tools = {
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 1,
+ ItemName.DodgeRoll: 2,
+ ItemName.FinishingPlus: 1,
+}
+
+not_hard_cor_tools_dict = {
+ ItemName.ReflectElement: 3,
+ ItemName.Stitch: 1,
+ ItemName.ChickenLittle: 1,
+ ItemName.MagnetElement: 2,
+ ItemName.Explosion: 1,
+ ItemName.FinishingLeap: 1,
+ ItemName.ThunderElement: 2,
+}
+transport_tools_dict = {
+ ItemName.ReflectElement: 3,
+ ItemName.Stitch: 1,
+ ItemName.ChickenLittle: 1,
+ ItemName.MagnetElement: 2,
+ ItemName.Explosion: 1,
+ ItemName.FinishingLeap: 1,
+ ItemName.ThunderElement: 3,
+ ItemName.Fantasia: 1,
+ ItemName.FlareForce: 1,
+ ItemName.Genie: 1,
+}
+easy_data_saix = {
+ ItemName.Guard: 1,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.ThunderElement: 1,
+ ItemName.BlizzardElement: 1,
+ ItemName.FlareForce: 1,
+ ItemName.Fantasia: 1,
+ ItemName.FireElement: 3,
+ ItemName.ReflectElement: 3,
+ ItemName.GuardBreak: 1,
+ ItemName.Explosion: 1,
+ ItemName.AerialDodge: 3,
+ ItemName.Glide: 3,
+ ItemName.SecondChance: 1,
+ ItemName.OnceMore: 1
+}
+normal_data_saix = {
+ ItemName.Guard: 1,
+ ItemName.ThunderElement: 1,
+ ItemName.BlizzardElement: 1,
+ ItemName.FireElement: 3,
+ ItemName.ReflectElement: 3,
+ ItemName.AerialDodge: 3,
+ ItemName.Glide: 3,
+}
+hard_data_saix = {
+ ItemName.Guard: 1,
+ ItemName.BlizzardElement: 1,
+ ItemName.ReflectElement: 1,
+ ItemName.AerialDodge: 3,
+ ItemName.Glide: 3,
+}
+easy_data_roxas_tools = {
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 3,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.GuardBreak: 1,
+ ItemName.Explosion: 1,
+ ItemName.DodgeRoll: 3,
+ ItemName.FinishingPlus: 1,
+ ItemName.SecondChance: 1,
+ ItemName.OnceMore: 1,
+}
+normal_data_roxas_tools = {
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 2,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.GuardBreak: 1,
+ ItemName.Explosion: 1,
+ ItemName.DodgeRoll: 3,
+ ItemName.FinishingPlus: 1,
+}
+hard_data_roxas_tools = {
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 1,
+ ItemName.DodgeRoll: 2,
+ ItemName.FinishingPlus: 1,
+}
+easy_data_axel_tools = {
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 3,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.GuardBreak: 1,
+ ItemName.Explosion: 1,
+ ItemName.DodgeRoll: 3,
+ ItemName.FinishingPlus: 1,
+ ItemName.SecondChance: 1,
+ ItemName.OnceMore: 1,
+ ItemName.BlizzardElement: 3,
+}
+normal_data_axel_tools = {
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 2,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.GuardBreak: 1,
+ ItemName.Explosion: 1,
+ ItemName.DodgeRoll: 3,
+ ItemName.FinishingPlus: 1,
+ ItemName.BlizzardElement: 3,
+}
+hard_data_axel_tools = {
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 1,
+ ItemName.DodgeRoll: 2,
+ ItemName.FinishingPlus: 1,
+ ItemName.BlizzardElement: 2,
+}
+easy_roxas_tools = {
+ ItemName.AerialDodge: 1,
+ ItemName.Glide: 1,
+ ItemName.LimitForm: 1,
+ ItemName.ThunderElement: 1,
+ ItemName.ReflectElement: 2,
+ ItemName.GuardBreak: 1,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.FinishingPlus: 1,
+ ItemName.BlizzardElement: 1
+}
+normal_roxas_tools = {
+ ItemName.ThunderElement: 1,
+ ItemName.ReflectElement: 2,
+ ItemName.GuardBreak: 1,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.FinishingPlus: 1,
+ ItemName.BlizzardElement: 1
+}
+easy_xigbar_tools = {
+ ItemName.HorizontalSlash: 1,
+ ItemName.FireElement: 2,
+ ItemName.FinishingPlus: 1,
+ ItemName.Glide: 2,
+ ItemName.AerialDodge: 2,
+ ItemName.QuickRun: 2,
+ ItemName.ReflectElement: 1,
+ ItemName.Guard: 1,
+}
+normal_xigbar_tools = {
+ ItemName.FireElement: 2,
+ ItemName.FinishingPlus: 1,
+ ItemName.Glide: 2,
+ ItemName.AerialDodge: 2,
+ ItemName.QuickRun: 2,
+ ItemName.ReflectElement: 1,
+ ItemName.Guard: 1
+}
+easy_luxord_tools = {
+ ItemName.AerialDodge: 1,
+ ItemName.Glide: 1,
+ ItemName.QuickRun: 2,
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 2,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.LimitForm: 1,
+}
+normal_luxord_tools = {
+ ItemName.AerialDodge: 1,
+ ItemName.Glide: 1,
+ ItemName.QuickRun: 2,
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 2,
+}
+easy_saix_tools = {
+ ItemName.AerialDodge: 1,
+ ItemName.Glide: 1,
+ ItemName.QuickRun: 2,
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 2,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.LimitForm: 1,
+}
+normal_saix_tools = {
+ ItemName.AerialDodge: 1,
+ ItemName.Glide: 1,
+ ItemName.QuickRun: 2,
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 2,
+}
+easy_xemnas_tools = {
+ ItemName.AerialDodge: 1,
+ ItemName.Glide: 1,
+ ItemName.QuickRun: 2,
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 2,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.LimitForm: 1,
+}
+normal_xemnas_tools = {
+ ItemName.AerialDodge: 1,
+ ItemName.Glide: 1,
+ ItemName.QuickRun: 2,
+ ItemName.Guard: 1,
+ ItemName.ReflectElement: 2,
+}
+easy_data_xemnas = {
+ ItemName.ComboMaster: 1,
+ ItemName.Slapshot: 1,
+ ItemName.ReflectElement: 3,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.FinishingPlus: 1,
+ ItemName.Guard: 1,
+ ItemName.TrinityLimit: 1,
+ ItemName.SecondChance: 1,
+ ItemName.OnceMore: 1,
+ ItemName.LimitForm: 1,
+}
+normal_data_xemnas = {
+ ItemName.ComboMaster: 1,
+ ItemName.Slapshot: 1,
+ ItemName.ReflectElement: 3,
+ ItemName.SlideDash: 1,
+ ItemName.FlashStep: 1,
+ ItemName.FinishingPlus: 1,
+ ItemName.Guard: 1,
+ ItemName.LimitForm: 1,
+}
+hard_data_xemnas = {
+ ItemName.ComboMaster: 1,
+ ItemName.Slapshot: 1,
+ ItemName.ReflectElement: 2,
+ ItemName.FinishingPlus: 1,
+ ItemName.Guard: 1,
+ ItemName.LimitForm: 1,
+}
+final_leveling_access = {
+ LocationName.RoxasEventLocation,
+ LocationName.GrimReaper2,
+ LocationName.Xaldin,
+ LocationName.StormRider,
+ LocationName.UndergroundConcourseMythrilGem
+}
+
+multi_form_region_access = {
+ ItemName.CastleKey,
+ ItemName.BattlefieldsofWar,
+ ItemName.SwordoftheAncestor,
+ ItemName.BeastsClaw,
+ ItemName.BoneFist,
+ ItemName.SkillandCrossbones,
+ ItemName.Scimitar,
+ ItemName.MembershipCard,
+ ItemName.IceCream,
+ ItemName.WaytotheDawn,
+ ItemName.IdentityDisk,
+}
+limit_form_region_access = {
+ ItemName.CastleKey,
+ ItemName.BattlefieldsofWar,
+ ItemName.SwordoftheAncestor,
+ ItemName.BeastsClaw,
+ ItemName.BoneFist,
+ ItemName.SkillandCrossbones,
+ ItemName.Scimitar,
+ ItemName.MembershipCard,
+ ItemName.IceCream,
+ ItemName.WaytotheDawn,
+ ItemName.IdentityDisk,
+ ItemName.NamineSketches
+}
diff --git a/worlds/kh2/Names/ItemName.py b/worlds/kh2/Names/ItemName.py
index 57cfcbe0606f..d7dbdb0ad30a 100644
--- a/worlds/kh2/Names/ItemName.py
+++ b/worlds/kh2/Names/ItemName.py
@@ -12,8 +12,7 @@
SecretAnsemsReport11 = "Secret Ansem's Report 11"
SecretAnsemsReport12 = "Secret Ansem's Report 12"
SecretAnsemsReport13 = "Secret Ansem's Report 13"
-
-# progression
+# proofs, visit unlocks and forms
ProofofConnection = "Proof of Connection"
ProofofNonexistence = "Proof of Nonexistence"
ProofofPeace = "Proof of Peace"
@@ -32,51 +31,33 @@
NamineSketches = "Namine Sketches"
CastleKey = "Disney Castle Key"
TornPages = "Torn Page"
-TornPages = "Torn Page"
-TornPages = "Torn Page"
-TornPages = "Torn Page"
-TornPages = "Torn Page"
ValorForm = "Valor Form"
WisdomForm = "Wisdom Form"
LimitForm = "Limit Form"
MasterForm = "Master Form"
FinalForm = "Final Form"
-
+AntiForm = "Anti Form"
# magic and summons
-FireElement = "Fire Element"
-
-BlizzardElement = "Blizzard Element"
-
-ThunderElement = "Thunder Element"
-
-CureElement = "Cure Element"
-
-MagnetElement = "Magnet Element"
-
-ReflectElement = "Reflect Element"
+FireElement = "Fire Element"
+BlizzardElement = "Blizzard Element"
+ThunderElement = "Thunder Element"
+CureElement = "Cure Element"
+MagnetElement = "Magnet Element"
+ReflectElement = "Reflect Element"
Genie = "Genie"
PeterPan = "Peter Pan"
Stitch = "Stitch"
ChickenLittle = "Chicken Little"
-#movement
+# movement
HighJump = "High Jump"
-
-
QuickRun = "Quick Run"
-
-
AerialDodge = "Aerial Dodge"
-
-
Glide = "Glide"
-
-
DodgeRoll = "Dodge Roll"
-
-#keyblades
+# keyblades
Oathkeeper = "Oathkeeper"
Oblivion = "Oblivion"
StarSeeker = "Star Seeker"
@@ -109,7 +90,6 @@
MeteorStaff = "Meteor Staff"
CometStaff = "Comet Staff"
Centurion2 = "Centurion+"
-MeteorStaff = "Meteor Staff"
NobodyLance = "Nobody Lance"
PreciousMushroom = "Precious Mushroom"
PreciousMushroom2 = "Precious Mushroom+"
@@ -203,7 +183,7 @@
GrandRibbon = "Grand Ribbon"
# usefull and stat incre
-MickyMunnyPouch = "Mickey Munny Pouch"
+MickeyMunnyPouch = "Mickey Munny Pouch"
OletteMunnyPouch = "Olette Munny Pouch"
HadesCupTrophy = "Hades Cup Trophy"
UnknownDisk = "Unknown Disk"
@@ -253,7 +233,6 @@
MagicLock = "Magic Lock-On"
LeafBracer = "Leaf Bracer"
CombinationBoost = "Combination Boost"
-DamageDrive = "Damage Drive"
OnceMore = "Once More"
SecondChance = "Second Chance"
@@ -313,10 +292,6 @@
DonaldFireBoost = "Donald Fire Boost"
DonaldBlizzardBoost = "Donald Blizzard Boost"
DonaldThunderBoost = "Donald Thunder Boost"
-DonaldFireBoost = "Donald Fire Boost"
-DonaldBlizzardBoost = "Donald Blizzard Boost"
-DonaldThunderBoost = "Donald Thunder Boost"
-DonaldMPRage = "Donald MP Rage"
DonaldMPHastera = "Donald MP Hastera"
DonaldAutoLimit = "Donald Auto Limit"
DonaldHyperHealing = "Donald Hyper Healing"
@@ -324,14 +299,7 @@
DonaldMPHastega = "Donald MP Hastega"
DonaldItemBoost = "Donald Item Boost"
DonaldDamageControl = "Donald Damage Control"
-DonaldHyperHealing = "Donald Hyper Healing"
-DonaldMPRage = "Donald MP Rage"
DonaldMPHaste = "Donald MP Haste"
-DonaldMPHastera = "Donald MP Hastera"
-DonaldMPHastega = "Donald MP Hastega"
-DonaldMPHaste = "Donald MP Haste"
-DonaldDamageControl = "Donald Damage Control"
-DonaldMPHastera = "Donald MP Hastera"
DonaldDraw = "Donald Draw"
# goofy abili
@@ -353,27 +321,18 @@
GoofyAutoChange = "Goofy Auto Change"
GoofyHyperHealing = "Goofy Hyper Healing"
GoofyAutoHealing = "Goofy Auto Healing"
-GoofyDefender = "Goofy Defender"
-GoofyHyperHealing = "Goofy Hyper Healing"
GoofyMPHaste = "Goofy MP Haste"
GoofyMPHastera = "Goofy MP Hastera"
-GoofyMPRage = "Goofy MP Rage"
GoofyMPHastega = "Goofy MP Hastega"
-GoofyItemBoost = "Goofy Item Boost"
-GoofyDamageControl = "Goofy Damage Control"
-GoofyProtect = "Goofy Protect"
-GoofyProtera = "Goofy Protera"
-GoofyProtega = "Goofy Protega"
-GoofyDamageControl = "Goofy Damage Control"
GoofyProtect = "Goofy Protect"
GoofyProtera = "Goofy Protera"
GoofyProtega = "Goofy Protega"
Victory = "Victory"
LuckyEmblem = "Lucky Emblem"
-Bounty="Bounty"
+Bounty = "Bounty"
-UniversalKey="Universal Key"
+# UniversalKey = "Universal Key"
# Keyblade Slots
FAKESlot = "FAKE (Slot)"
DetectionSaberSlot = "Detection Saber (Slot)"
@@ -402,3 +361,73 @@
FenrirSlot = "Fenrir (Slot)"
UltimaWeaponSlot = "Ultima Weapon (Slot)"
WinnersProofSlot = "Winner's Proof (Slot)"
+
+# events
+HostileProgramEvent = "Hostile Program Event"
+McpEvent = "Master Control Program Event"
+ASLarxeneEvent = "AS Larxene Event"
+DataLarxeneEvent = "Data Larxene Event"
+BarbosaEvent = "Barbosa Event"
+GrimReaper1Event = "Grim Reaper 1 Event"
+GrimReaper2Event = "Grim Reaper 2 Event"
+DataLuxordEvent = "Data Luxord Event"
+DataAxelEvent = "Data Axel Event"
+CerberusEvent = "Cerberus Event"
+OlympusPeteEvent = "Olympus Pete Event"
+HydraEvent = "Hydra Event"
+OcPainAndPanicCupEvent = "Pain and Panic Cup Event"
+OcCerberusCupEvent = "Cerberus Cup Event"
+HadesEvent = "Hades Event"
+ASZexionEvent = "AS Zexion Event"
+DataZexionEvent = "Data Zexion Event"
+Oc2TitanCupEvent = "Titan Cup Event"
+Oc2GofCupEvent = "Goddess of Fate Cup Event"
+Oc2CupsEvent = "Olympus Coliseum Cups Event"
+HadesCupEvents = "Olympus Coliseum Hade's Paradox Event"
+PrisonKeeperEvent = "Prison Keeper Event"
+OogieBoogieEvent = "Oogie Boogie Event"
+ExperimentEvent = "The Experiment Event"
+ASVexenEvent = "AS Vexen Event"
+DataVexenEvent = "Data Vexen Event"
+ShanYuEvent = "Shan Yu Event"
+AnsemRikuEvent = "Ansem Riku Event"
+StormRiderEvent = "Storm Rider Event"
+DataXigbarEvent = "Data Xigbar Event"
+RoxasEvent = "Roxas Event"
+XigbarEvent = "Xigbar Event"
+LuxordEvent = "Luxord Event"
+SaixEvent = "Saix Event"
+XemnasEvent = "Xemnas Event"
+ArmoredXemnasEvent = "Armored Xemnas Event"
+ArmoredXemnas2Event = "Armored Xemnas 2 Event"
+FinalXemnasEvent = "Final Xemnas Event"
+DataXemnasEvent = "Data Xemnas Event"
+ThresholderEvent = "Thresholder Event"
+BeastEvent = "Beast Event"
+DarkThornEvent = "Dark Thorn Event"
+XaldinEvent = "Xaldin Event"
+DataXaldinEvent = "Data Xaldin Event"
+TwinLordsEvent = "Twin Lords Event"
+GenieJafarEvent = "Genie Jafar Event"
+ASLexaeusEvent = "AS Lexaeus Event"
+DataLexaeusEvent = "Data Lexaeus Event"
+ScarEvent = "Scar Event"
+GroundShakerEvent = "Groundshaker Event"
+DataSaixEvent = "Data Saix Event"
+HBDemyxEvent = "Hollow Bastion Demyx Event"
+ThousandHeartlessEvent = "Thousand Heartless Event"
+Mushroom13Event = "Mushroom 13 Event"
+SephiEvent = "Sephiroth Event"
+DataDemyxEvent = "Data Demyx Event"
+CorFirstFightEvent = "Cavern of Rememberance:Fight 1 Event"
+CorSecondFightEvent = "Cavern of Rememberance:Fight 2 Event"
+TransportEvent = "Transport to Rememberance Event"
+OldPeteEvent = "Old Pete Event"
+FuturePeteEvent = "Future Pete Event"
+ASMarluxiaEvent = "AS Marluxia Event"
+DataMarluxiaEvent = "Data Marluxia Event"
+TerraEvent = "Terra Event"
+TwilightThornEvent = "Twilight Thorn Event"
+Axel1Event = "Axel 1 Event"
+Axel2Event = "Axel 2 Event"
+DataRoxasEvent = "Data Roxas Event"
diff --git a/worlds/kh2/Names/LocationName.py b/worlds/kh2/Names/LocationName.py
index 1a6c4d07fbdd..bcaf66455846 100644
--- a/worlds/kh2/Names/LocationName.py
+++ b/worlds/kh2/Names/LocationName.py
@@ -27,7 +27,7 @@
ThroneRoomMythrilCrystal = "(LoD2) Throne Room Mythril Crystal"
ThroneRoomOrichalcum = "(LoD2) Throne Room Orichalcum"
StormRider = "(LoD2) Storm Rider Bonus: Sora Slot 1"
-XigbarDataDefenseBoost = "Data Xigbar"
+XigbarDataDefenseBoost = "(Post LoD2: Summit) Data Xigbar"
AgrabahMap = "(AG) Agrabah Map"
AgrabahDarkShard = "(AG) Agrabah Dark Shard"
@@ -62,9 +62,10 @@
RuinedChamberRuinsMap = "(AG2) Ruined Chamber Ruins Map"
GenieJafar = "(AG2) Genie Jafar"
WishingLamp = "(AG2) Wishing Lamp"
-LexaeusBonus = "Lexaeus Bonus: Sora Slot 1"
-LexaeusASStrengthBeyondStrength = "AS Lexaeus"
-LexaeusDataLostIllusion = "Data Lexaeus"
+LexaeusBonus = "(Post AG2: Peddler's Shop) Lexaeus Bonus: Sora Slot 1"
+LexaeusASStrengthBeyondStrength = "(Post AG2: Peddler's Shop) AS Lexaeus"
+LexaeusDataLostIllusion = "(Post AG2: Peddler's Shop) Data Lexaeus"
+
DCCourtyardMythrilShard = "(DC) Courtyard Mythril Shard"
DCCourtyardStarRecipe = "(DC) Courtyard Star Recipe"
DCCourtyardAPBoost = "(DC) Courtyard AP Boost"
@@ -89,12 +90,15 @@
FuturePeteGetBonus = "(TR) Future Pete Bonus: Sora Slot 2"
Monochrome = "(TR) Monochrome"
WisdomForm = "(TR) Wisdom Form"
-MarluxiaGetBonus = "Marluxia Bonus: Sora Slot 1"
-MarluxiaASEternalBlossom = "AS Marluxia"
-MarluxiaDataLostIllusion = "Data Marluxia"
-LingeringWillBonus = "Lingering Will Bonus: Sora Slot 1"
-LingeringWillProofofConnection = "Lingering Will Proof of Connection"
-LingeringWillManifestIllusion = "Lingering Will Manifest Illusion"
+
+MarluxiaGetBonus = "(Post TR:Hall of the Cornerstone) Marluxia Bonus: Sora Slot 1"
+MarluxiaASEternalBlossom = "(Post TR:Hall of the Cornerstone) AS Marluxia"
+MarluxiaDataLostIllusion = "(Post TR:Hall of the Cornerstone) Data Marluxia"
+
+LingeringWillBonus = "(Post TR:Hall of the Cornerstone) Lingering Will Bonus: Sora Slot 1"
+LingeringWillProofofConnection = "(Post TR:Hall of the Cornerstone) Lingering Will Proof of Connection"
+LingeringWillManifestIllusion = "(Post TR:Hall of the Cornerstone) Lingering Will Manifest Illusion"
+
PoohsHouse100AcreWoodMap = "(100Acre) Pooh's House 100 Acre Wood Map"
PoohsHouseAPBoost = "(100Acre) Pooh's House AP Boost"
PoohsHouseMythrilStone = "(100Acre) Pooh's House Mythril Stone"
@@ -119,6 +123,7 @@
StarryHillStyleRecipe = "(100Acre) Starry Hill Style Recipe"
StarryHillCureElement = "(100Acre) Starry Hill Cure Element"
StarryHillOrichalcumPlus = "(100Acre) Starry Hill Orichalcum+"
+
PassageMythrilShard = "(OC) Passage Mythril Shard"
PassageMythrilStone = "(OC) Passage Mythril Stone"
PassageEther = "(OC) Passage Ether"
@@ -162,9 +167,9 @@
FatalCrestGoddessofFateCup = "Fatal Crest Goddess of Fate Cup"
OrichalcumPlusGoddessofFateCup = "Orichalcum+ Goddess of Fate Cup"
HadesCupTrophyParadoxCups = "Hades Cup Trophy Paradox Cups"
-ZexionBonus = "Zexion Bonus: Sora Slot 1"
-ZexionASBookofShadows = "AS Zexion"
-ZexionDataLostIllusion = "Data Zexion"
+ZexionBonus = "(Post OC2: Cave of the Dead Inner Chamber) Zexion Bonus: Sora Slot 1"
+ZexionASBookofShadows = "(Post OC2: Cave of the Dead Inner Chamber) AS Zexion"
+ZexionDataLostIllusion = "(Post OC2: Cave of the Dead Inner Chamber) Data Zexion"
BCCourtyardAPBoost = "(BC) Courtyard AP Boost"
@@ -198,7 +203,7 @@
Xaldin = "(BC2) Xaldin Bonus: Sora Slot 1"
XaldinGetBonus = "(BC2) Xaldin Bonus: Sora Slot 2"
SecretAnsemReport4 = "(BC2) Secret Ansem Report 4 (Xaldin)"
-XaldinDataDefenseBoost = "Data Xaldin"
+XaldinDataDefenseBoost = "(Post BC2: Ballroom) Data Xaldin"
@@ -223,9 +228,9 @@
CentralComputerCoreMap = "(SP2) Central Computer Core Map"
MCP = "(SP2) MCP Bonus: Sora Slot 1"
MCPGetBonus = "(SP2) MCP Bonus: Sora Slot 2"
-LarxeneBonus = "Larxene Bonus: Sora Slot 1"
-LarxeneASCloakedThunder = "AS Larxene"
-LarxeneDataLostIllusion = "Data Larxene"
+LarxeneBonus = "(Post SP2: Central Computer Core) Larxene Bonus: Sora Slot 1"
+LarxeneASCloakedThunder = "(Post SP2: Central Computer Core) AS Larxene"
+LarxeneDataLostIllusion = "(Post SP2: Central Computer Core) Data Larxene"
GraveyardMythrilShard = "(HT) Graveyard Mythril Shard"
GraveyardSerenityGem = "(HT) Graveyard Serenity Gem"
@@ -249,9 +254,9 @@
DecoyPresents = "(HT2) Decoy Presents"
Experiment = "(HT2) Experiment Bonus: Sora Slot 1"
DecisivePumpkin = "(HT2) Decisive Pumpkin"
-VexenBonus = "Vexen Bonus: Sora Slot 1"
-VexenASRoadtoDiscovery = "AS Vexen"
-VexenDataLostIllusion = "Data Vexen"
+VexenBonus = "(Post HT2: Yuletide Hill) Vexen Bonus: Sora Slot 1"
+VexenASRoadtoDiscovery = "(Post HT2: Yuletide Hill) AS Vexen"
+VexenDataLostIllusion = "(Post HT2: Yuletide Hill) Data Vexen"
RampartNavalMap = "(PR) Rampart Naval Map"
RampartMythrilStone = "(PR) Rampart Mythril Stone"
@@ -286,7 +291,7 @@
GrimReaper2 = "(PR2) Grim Reaper 2 Bonus: Sora Slot 1"
SecretAnsemReport6 = "(PR2) Secret Ansem Report 6 (Grim Reaper 2)"
-LuxordDataAPBoost = "Data Luxord"
+LuxordDataAPBoost = "(Post PR2: Treasure Heap) Data Luxord"
MarketplaceMap = "(HB) Marketplace Map"
BoroughDriveRecovery = "(HB) Borough Drive Recovery"
@@ -329,7 +334,7 @@
SephirothFenrir = "Sephiroth Fenrir"
WinnersProof = "(HB2) Winner's Proof"
ProofofPeace = "(HB2) Proof of Peace"
-DemyxDataAPBoost = "Data Demyx"
+DemyxDataAPBoost = "(Post HB2: Restoration Site) Data Demyx"
CoRDepthsAPBoost = "(CoR) Depths AP Boost"
CoRDepthsPowerCrystal = "(CoR) Depths Power Crystal"
@@ -386,7 +391,7 @@
Hyenas2 = "(PL2) Hyenas 2 Bonus: Sora Slot 1"
Groundshaker = "(PL2) Groundshaker Bonus: Sora Slot 1"
GroundshakerGetBonus = "(PL2) Groundshaker Bonus: Sora Slot 2"
-SaixDataDefenseBoost = "Data Saix"
+SaixDataDefenseBoost = "(Post PL2: Peak) Data Saix"
TwilightTownMap = "(STT) Twilight Town Map"
MunnyPouchOlette = "(STT) Munny Pouch Olette"
@@ -415,7 +420,7 @@
MansionLibraryHiPotion = "(STT) Mansion Library Hi-Potion"
Axel2 = "(STT) Axel 2"
MansionBasementCorridorHiPotion = "(STT) Mansion Basement Corridor Hi-Potion"
-RoxasDataMagicBoost = "Data Roxas"
+RoxasDataMagicBoost = "(Post STT: Mansion Pod Room) Data Roxas"
OldMansionPotion = "(TT) Old Mansion Potion"
OldMansionMythrilShard = "(TT) Old Mansion Mythril Shard"
@@ -468,46 +473,46 @@
MansionBasementCorridorUltimateRecipe = "(TT3) Mansion Basement Corridor Ultimate Recipe"
BetwixtandBetween = "(TT3) Betwixt and Between"
BetwixtandBetweenBondofFlame = "(TT3) Betwixt and Between Bond of Flame"
-AxelDataMagicBoost = "Data Axel"
+AxelDataMagicBoost = "(Post TT3: Betwixt and Between) Data Axel"
FragmentCrossingMythrilStone = "(TWTNW) Fragment Crossing Mythril Stone"
FragmentCrossingMythrilCrystal = "(TWTNW) Fragment Crossing Mythril Crystal"
FragmentCrossingAPBoost = "(TWTNW) Fragment Crossing AP Boost"
FragmentCrossingOrichalcum = "(TWTNW) Fragment Crossing Orichalcum"
-Roxas = "(TWTNW) Roxas Bonus: Sora Slot 1"
-RoxasGetBonus = "(TWTNW) Roxas Bonus: Sora Slot 2"
-RoxasSecretAnsemReport8 = "(TWTNW) Roxas Secret Ansem Report 8"
-TwoBecomeOne = "(TWTNW) Two Become One"
-MemorysSkyscaperMythrilCrystal = "(TWTNW) Memory's Skyscaper Mythril Crystal"
-MemorysSkyscaperAPBoost = "(TWTNW) Memory's Skyscaper AP Boost"
-MemorysSkyscaperMythrilStone = "(TWTNW) Memory's Skyscaper Mythril Stone"
-TheBrinkofDespairDarkCityMap = "(TWTNW) The Brink of Despair Dark City Map"
-TheBrinkofDespairOrichalcumPlus = "(TWTNW) The Brink of Despair Orichalcum+"
-NothingsCallMythrilGem = "(TWTNW) Nothing's Call Mythril Gem"
-NothingsCallOrichalcum = "(TWTNW) Nothing's Call Orichalcum"
-TwilightsViewCosmicBelt = "(TWTNW) Twilight's View Cosmic Belt"
-XigbarBonus = "(TWTNW) Xigbar Bonus: Sora Slot 1"
-XigbarSecretAnsemReport3 = "(TWTNW) Xigbar Secret Ansem Report 3"
-NaughtsSkywayMythrilGem = "(TWTNW) Naught's Skyway Mythril Gem"
-NaughtsSkywayOrichalcum = "(TWTNW) Naught's Skyway Orichalcum"
-NaughtsSkywayMythrilCrystal = "(TWTNW) Naught's Skyway Mythril Crystal"
-Oblivion = "(TWTNW) Oblivion"
-CastleThatNeverWasMap = "(TWTNW) Castle That Never Was Map"
-Luxord = "(TWTNW) Luxord"
-LuxordGetBonus = "(TWTNW) Luxord Bonus: Sora Slot 1"
-LuxordSecretAnsemReport9 = "(TWTNW) Luxord Secret Ansem Report 9"
-SaixBonus = "(TWTNW) Saix Bonus: Sora Slot 1"
-SaixSecretAnsemReport12 = "(TWTNW) Saix Secret Ansem Report 12"
-PreXemnas1SecretAnsemReport11 = "(TWTNW) Secret Ansem Report 11 (Pre-Xemnas 1)"
-RuinandCreationsPassageMythrilStone = "(TWTNW) Ruin and Creation's Passage Mythril Stone"
-RuinandCreationsPassageAPBoost = "(TWTNW) Ruin and Creation's Passage AP Boost"
-RuinandCreationsPassageMythrilCrystal = "(TWTNW) Ruin and Creation's Passage Mythril Crystal"
-RuinandCreationsPassageOrichalcum = "(TWTNW) Ruin and Creation's Passage Orichalcum"
-Xemnas1 = "(TWTNW) Xemnas 1 Bonus: Sora Slot 1"
-Xemnas1GetBonus = "(TWTNW) Xemnas 1 Bonus: Sora Slot 2"
-Xemnas1SecretAnsemReport13 = "(TWTNW) Xemnas 1 Secret Ansem Report 13"
+Roxas = "(TWTNW2) Roxas Bonus: Sora Slot 1"
+RoxasGetBonus = "(TWTNW2) Roxas Bonus: Sora Slot 2"
+RoxasSecretAnsemReport8 = "(TWTNW2) Roxas Secret Ansem Report 8"
+TwoBecomeOne = "(TWTNW2) Two Become One"
+MemorysSkyscaperMythrilCrystal = "(TWTNW2) Memory's Skyscaper Mythril Crystal"
+MemorysSkyscaperAPBoost = "(TWTNW2) Memory's Skyscaper AP Boost"
+MemorysSkyscaperMythrilStone = "(TWTNW2) Memory's Skyscaper Mythril Stone"
+TheBrinkofDespairDarkCityMap = "(TWTNW2) The Brink of Despair Dark City Map"
+TheBrinkofDespairOrichalcumPlus = "(TWTNW2) The Brink of Despair Orichalcum+"
+NothingsCallMythrilGem = "(TWTNW2) Nothing's Call Mythril Gem"
+NothingsCallOrichalcum = "(TWTNW2) Nothing's Call Orichalcum"
+TwilightsViewCosmicBelt = "(TWTNW2) Twilight's View Cosmic Belt"
+XigbarBonus = "(TWTNW2) Xigbar Bonus: Sora Slot 1"
+XigbarSecretAnsemReport3 = "(TWTNW2) Xigbar Secret Ansem Report 3"
+NaughtsSkywayMythrilGem = "(TWTNW2) Naught's Skyway Mythril Gem"
+NaughtsSkywayOrichalcum = "(TWTNW2) Naught's Skyway Orichalcum"
+NaughtsSkywayMythrilCrystal = "(TWTNW2) Naught's Skyway Mythril Crystal"
+Oblivion = "(TWTNW2) Oblivion"
+CastleThatNeverWasMap = "(TWTNW2) Castle That Never Was Map"
+Luxord = "(TWTNW2) Luxord Bonus: Sora Slot 2"
+LuxordGetBonus = "(TWTNW2) Luxord Bonus: Sora Slot 1"
+LuxordSecretAnsemReport9 = "(TWTNW2) Luxord Secret Ansem Report 9"
+SaixBonus = "(TWTNW2) Saix Bonus: Sora Slot 1"
+SaixSecretAnsemReport12 = "(TWTNW2) Saix Secret Ansem Report 12"
+PreXemnas1SecretAnsemReport11 = "(TWTNW3) Secret Ansem Report 11 (Pre-Xemnas 1)"
+RuinandCreationsPassageMythrilStone = "(TWTNW3) Ruin and Creation's Passage Mythril Stone"
+RuinandCreationsPassageAPBoost = "(TWTNW3) Ruin and Creation's Passage AP Boost"
+RuinandCreationsPassageMythrilCrystal = "(TWTNW3) Ruin and Creation's Passage Mythril Crystal"
+RuinandCreationsPassageOrichalcum = "(TWTNW3) Ruin and Creation's Passage Orichalcum"
+Xemnas1 = "(TWTNW3) Xemnas 1 Bonus: Sora Slot 1"
+Xemnas1GetBonus = "(TWTNW3) Xemnas 1 Bonus: Sora Slot 2"
+Xemnas1SecretAnsemReport13 = "(TWTNW3) Xemnas 1 Secret Ansem Report 13"
FinalXemnas = "Final Xemnas"
-XemnasDataPowerBoost = "Data Xemnas"
+XemnasDataPowerBoost = "(Post TWTNW3: The Altar of Naught) Data Xemnas"
Lvl1 ="Level 01"
Lvl2 ="Level 02"
Lvl3 ="Level 03"
@@ -605,7 +610,7 @@
Lvl95 ="Level 95"
Lvl96 ="Level 96"
Lvl97 ="Level 97"
-Lvl98 ="Level 98"
+Lvl98 ="Level 98"
Lvl99 ="Level 99"
Valorlvl1 ="Valor level 1"
Valorlvl2 ="Valor level 2"
@@ -643,13 +648,28 @@
Finallvl6 ="Final level 6"
Finallvl7 ="Final level 7"
+Summonlvl2="Summon level 2"
+Summonlvl3="Summon level 3"
+Summonlvl4="Summon level 4"
+Summonlvl5="Summon level 5"
+Summonlvl6="Summon level 6"
+Summonlvl7="Summon level 7"
+
+
GardenofAssemblageMap ="Garden of Assemblage Map"
GoALostIllusion ="GoA Lost Illusion"
ProofofNonexistence ="Proof of Nonexistence Location"
-test= "test"
-
+UnderseaKingdomMap ="(AT) Undersea Kingdom Map"
+MysteriousAbyss ="(AT) Mysterious Abyss"
+MusicalBlizzardElement ="(AT) Musical Blizzard Element"
+MusicalOrichalcumPlus ="(AT) Musical Orichalcum+"
+DonaldStarting1 ="Donald Starting Item 1"
+DonaldStarting2 ="Donald Starting Item 2"
+GoofyStarting1 ="Goofy Starting Item 1"
+GoofyStarting2 ="Goofy Starting Item 2"
+# TODO: remove in 4.3
Crit_1 ="Critical Starting Ability 1"
Crit_2 ="Critical Starting Ability 2"
Crit_3 ="Critical Starting Ability 3"
@@ -657,14 +677,9 @@
Crit_5 ="Critical Starting Ability 5"
Crit_6 ="Critical Starting Ability 6"
Crit_7 ="Critical Starting Ability 7"
-DonaldStarting1 ="Donald Starting Item 1"
-DonaldStarting2 ="Donald Starting Item 2"
-GoofyStarting1 ="Goofy Starting Item 1"
-GoofyStarting2 ="Goofy Starting Item 2"
-
DonaldScreens ="(SP) Screens Bonus: Donald Slot 1"
-DonaldDemyxHBGetBonus ="(HB) Demyx Bonus: Donald Slot 1"
+DonaldDemyxHBGetBonus ="(HB2) Demyx Bonus: Donald Slot 1"
DonaldDemyxOC ="(OC) Demyx Bonus: Donald Slot 1"
DonaldBoatPete ="(TR) Boat Pete Bonus: Donald Slot 1"
DonaldBoatPeteGetBonus ="(TR) Boat Pete Bonus: Donald Slot 2"
@@ -694,7 +709,7 @@
GoofyBeast ="(BC) Beast Bonus: Goofy Slot 1"
GoofyInterceptorBarrels ="(PR) Interceptor Barrels Bonus: Goofy Slot 1"
GoofyTreasureRoom ="(AG) Treasure Room Heartless Bonus: Goofy Slot 1"
-GoofyZexion ="Zexion Bonus: Goofy Slot 1"
+GoofyZexion ="(Post OC2: Cave of the Dead Inner Chamber) Zexion Bonus: Goofy Slot 1"
AdamantShield ="Adamant Shield Slot"
@@ -760,4 +775,86 @@
WinnersProofSlot ="Winner's Proof Slot"
PurebloodSlot ="Pureblood Slot"
-#Final_Region ="Final Form"
+Mushroom13_1 = "(Post TWTNW3: Memory's Skyscraper) Mushroom XIII No. 1"
+Mushroom13_2 = "(Post HT2: Christmas Tree Plaza) Mushroom XIII No. 2"
+Mushroom13_3 = "(Post BC2: Bridge) Mushroom XIII No. 3"
+Mushroom13_4 = "(Post LOD2: Palace Gates) Mushroom XIII No. 4"
+Mushroom13_5 = "(Post AG2: Treasure Room) Mushroom XIII No. 5"
+Mushroom13_6 = "(Post OC2: Atrium) Mushroom XIII No. 6"
+Mushroom13_7 = "(Post TT3: Tunnel way) Mushroom XIII No. 7"
+Mushroom13_8 = "(Post TT3: Tower) Mushroom XIII No. 8"
+Mushroom13_9 = "(Post HB2: Castle Gates) Mushroom XIII No. 9"
+Mushroom13_10 = "(Post PR2: Moonlight Nook) Mushroom XIII No. 10"
+Mushroom13_11 = "(Post TR: Waterway) Mushroom XIII No. 11"
+Mushroom13_12 = "(Post TT3: Old Mansion) Mushroom XIII No. 12"
+
+
+HostileProgramEventLocation = "Hostile Program Event Location"
+McpEventLocation = "Master Control Program Event Location"
+ASLarxeneEventLocation = "AS Larxene Event Location"
+DataLarxeneEventLocation = "Data Larxene Event Location"
+BarbosaEventLocation = "Barbosa Event Location"
+GrimReaper1EventLocation = "Grim Reaper 1 Event Location"
+GrimReaper2EventLocation = "Grim Reaper 2 Event Location"
+DataLuxordEventLocation = "Data Luxord Event Location"
+DataAxelEventLocation = "Data Axel Event Location"
+CerberusEventLocation = "Cerberus Event Location"
+OlympusPeteEventLocation = "Olympus Pete Event Location"
+HydraEventLocation = "Hydra Event Location"
+OcPainAndPanicCupEventLocation = "Pain and Panic Cup Event Location"
+OcCerberusCupEventLocation = "Cerberus Cup Event Location"
+HadesEventLocation = "Hades Event Location"
+ASZexionEventLocation = "AS Zexion Event Location"
+DataZexionEventLocation = "Data Zexion Event Location"
+Oc2TitanCupEventLocation = "Titan Cup Event Location"
+Oc2GofCupEventLocation = "Goddess of Fate Cup Event Location"
+Oc2CupsEventLocation = "Olympus Coliseum Cups Event Location"
+HadesCupEventLocations = "Olympus Coliseum Hade's Paradox Event Location"
+PrisonKeeperEventLocation = "Prison Keeper Event Location"
+OogieBoogieEventLocation = "Oogie Boogie Event Location"
+ExperimentEventLocation = "The Experiment Event Location"
+ASVexenEventLocation = "AS Vexen Event Location"
+DataVexenEventLocation = "Data Vexen Event Location"
+ShanYuEventLocation = "Shan Yu Event Location"
+AnsemRikuEventLocation = "Ansem Riku Event Location"
+StormRiderEventLocation = "Storm Rider Event Location"
+DataXigbarEventLocation = "Data Xigbar Event Location"
+RoxasEventLocation = "Roxas Event Location"
+XigbarEventLocation = "Xigbar Event Location"
+LuxordEventLocation = "Luxord Event Location"
+SaixEventLocation = "Saix Event Location"
+XemnasEventLocation = "Xemnas Event Location"
+ArmoredXemnasEventLocation = "Armored Xemnas Event Location"
+ArmoredXemnas2EventLocation = "Armored Xemnas 2 Event Location"
+FinalXemnasEventLocation = "Final Xemnas Event Location"
+DataXemnasEventLocation = "Data Xemnas Event Location"
+ThresholderEventLocation = "Thresholder Event Location"
+BeastEventLocation = "Beast Event Location"
+DarkThornEventLocation = "Dark Thorn Event Location"
+XaldinEventLocation = "Xaldin Event Location"
+DataXaldinEventLocation = "Data Xaldin Event Location"
+TwinLordsEventLocation = "Twin Lords Event Location"
+GenieJafarEventLocation = "Genie Jafar Event Location"
+ASLexaeusEventLocation = "AS Lexaeus Event Location"
+DataLexaeusEventLocation = "Data Lexaeus Event Location"
+ScarEventLocation = "Scar Event Location"
+GroundShakerEventLocation = "Groundshaker Event Location"
+DataSaixEventLocation = "Data Saix Event Location"
+HBDemyxEventLocation = "Hollow Bastion Demyx Event Location"
+ThousandHeartlessEventLocation = "Thousand Heartless Event Location"
+Mushroom13EventLocation = "Mushroom 13 Event Location"
+SephiEventLocation = "Sephiroth Event Location"
+DataDemyxEventLocation = "Data Demyx Event Location"
+CorFirstFightEventLocation = "Cavern of Rememberance:Fight 1 Event Location"
+CorSecondFightEventLocation = "Cavern of Rememberance:Fight 2 Event Location"
+TransportEventLocation = "Transport to Rememberance Event Location"
+OldPeteEventLocation = "Old Pete Event Location"
+FuturePeteEventLocation = "Future Pete Event Location"
+ASMarluxiaEventLocation = "AS Marluxia Event Location"
+DataMarluxiaEventLocation = "Data Marluxia Event Location"
+TerraEventLocation = "Terra Event Location"
+TwilightThornEventLocation = "Twilight Thorn Event Location"
+Axel1EventLocation = "Axel 1 Event Location"
+Axel2EventLocation = "Axel 2 Event Location"
+DataRoxasEventLocation = "Data Roxas Event Location"
+
diff --git a/worlds/kh2/Names/RegionName.py b/worlds/kh2/Names/RegionName.py
index d07b5d3de367..63ba6acdb878 100644
--- a/worlds/kh2/Names/RegionName.py
+++ b/worlds/kh2/Names/RegionName.py
@@ -1,90 +1,156 @@
-LoD_Region ="Land of Dragons"
-LoD2_Region ="Land of Dragons 2"
-
-Ag_Region ="Agrabah"
-Ag2_Region ="Agrabah 2"
-
-Dc_Region ="Disney Castle"
-Tr_Region ="Timeless River"
-
-HundredAcre1_Region ="Pooh's House"
-HundredAcre2_Region ="Piglet's House"
-HundredAcre3_Region ="Rabbit's House"
-HundredAcre4_Region ="Roo's House"
-HundredAcre5_Region ="Spookey Cave"
-HundredAcre6_Region ="Starry Hill"
-
-Pr_Region ="Port Royal"
-Pr2_Region ="Port Royal 2"
-Gr2_Region ="Grim Reaper 2"
-
-Oc_Region ="Olympus Coliseum"
-Oc2_Region ="Olympus Coliseum 2"
-Oc2_pain_and_panic_Region ="Pain and Panic Cup"
-Oc2_titan_Region ="Titan Cup"
-Oc2_cerberus_Region ="Cerberus Cup"
-Oc2_gof_Region ="Goddest of Fate Cup"
-Oc2Cups_Region ="Olympus Coliseum Cups"
-HadesCups_Region ="Olympus Coliseum Hade's Paradox"
-
-Bc_Region ="Beast's Castle"
-Bc2_Region ="Beast's Castle 2"
-Xaldin_Region ="Xaldin"
-
-Sp_Region ="Space Paranoids"
-Sp2_Region ="Space Paranoids 2"
-Mcp_Region ="Master Control Program"
-
-Ht_Region ="Holloween Town"
-Ht2_Region ="Holloween Town 2"
-
-Hb_Region ="Hollow Bastion"
-Hb2_Region ="Hollow Bastion 2"
-ThousandHeartless_Region ="Thousand Hearless"
-Mushroom13_Region ="Mushroom 13"
-CoR_Region ="Cavern of Rememberance"
-Transport_Region ="Transport to Rememberance"
-
-Pl_Region ="Pride Lands"
-Pl2_Region ="Pride Lands 2"
-
-STT_Region ="Simulated Twilight Town"
-
-TT_Region ="Twlight Town"
-TT2_Region ="Twlight Town 2"
-TT3_Region ="Twlight Town 3"
-
-Twtnw_Region ="The World That Never Was (First Visit)"
-Twtnw_PostRoxas ="The World That Never Was (Post Roxas)"
-Twtnw_PostXigbar ="The World That Never Was (Post Xigbar)"
-Twtnw2_Region ="The World That Never Was (Second Visit)" #before riku transformation
-
-SoraLevels_Region ="Sora's Levels"
-GoA_Region ="Garden Of Assemblage"
-Keyblade_Region ="Keyblade Slots"
-
-Valor_Region ="Valor Form"
-Wisdom_Region ="Wisdom Form"
-Limit_Region ="Limit Form"
-Master_Region ="Master Form"
-Final_Region ="Final Form"
-
-Terra_Region ="Lingering Will"
-Sephi_Region ="Sephiroth"
-Marluxia_Region ="Marluxia"
-Larxene_Region ="Larxene"
-Vexen_Region ="Vexen"
-Lexaeus_Region ="Lexaeus"
-Zexion_Region ="Zexion"
-
-LevelsVS1 ="Levels Region (1 Visit Locking Item)"
-LevelsVS3 ="Levels Region (3 Visit Locking Items)"
-LevelsVS6 ="Levels Region (6 Visit Locking Items)"
-LevelsVS9 ="Levels Region (9 Visit Locking Items)"
-LevelsVS12 ="Levels Region (12 Visit Locking Items)"
-LevelsVS15 ="Levels Region (15 Visit Locking Items)"
-LevelsVS18 ="Levels Region (18 Visit Locking Items)"
-LevelsVS21 ="Levels Region (21 Visit Locking Items)"
-LevelsVS24 ="Levels Region (24 Visit Locking Items)"
-LevelsVS26 ="Levels Region (26 Visit Locking Items)"
-
+Ha1 = "Pooh's House"
+Ha2 = "Piglet's House"
+Ha3 = "Rabbit's House"
+Ha4 = "Roo's House"
+Ha5 = "Spooky Cave"
+Ha6 = "Starry Hill"
+
+SoraLevels = "Sora's Levels"
+GoA = "Garden Of Assemblage"
+Keyblade = "Weapon Slots"
+
+Valor = "Valor Form"
+Wisdom = "Wisdom Form"
+Limit = "Limit Form"
+Master = "Master Form"
+Final = "Final Form"
+Summon = "Summons"
+# sp
+Sp = "Space Paranoids"
+HostileProgram = "Hostile Program"
+Sp2 = "Space Paranoids 2"
+Mcp = "Master Control Program"
+ASLarxene = "AS Larxene"
+DataLarxene = "Data Larxene"
+
+# pr
+Pr = "Port Royal"
+Barbosa = "Barbosa"
+Pr2 = "Port Royal 2"
+GrimReaper1 = "Grim Reaper 1"
+GrimReaper2 = "Grim Reaper 2"
+DataLuxord = "Data Luxord"
+
+# tt
+Tt = "Twilight Town"
+Tt2 = "Twilight Town 2"
+Tt3 = "Twilight Town 3"
+DataAxel = "Data Axel"
+
+# oc
+Oc = "Olympus Coliseum"
+Cerberus = "Cerberus"
+OlympusPete = "Olympus Pete"
+Hydra = "Hydra"
+OcPainAndPanicCup = "Pain and Panic Cup"
+OcCerberusCup = "Cerberus Cup"
+Oc2 = "Olympus Coliseum 2"
+Hades = "Hades"
+ASZexion = "AS Zexion"
+DataZexion = "Data Zexion"
+Oc2TitanCup = "Titan Cup"
+Oc2GofCup = "Goddess of Fate Cup"
+Oc2Cups = "Olympus Coliseum Cups"
+HadesCups = "Olympus Coliseum Hade's Paradox"
+
+# ht
+Ht = "Holloween Town"
+PrisonKeeper = "Prison Keeper"
+OogieBoogie = "Oogie Boogie"
+Ht2 = "Holloween Town 2"
+Experiment = "The Experiment"
+ASVexen = "AS Vexen"
+DataVexen = "Data Vexen"
+
+# lod
+LoD = "Land of Dragons"
+ShanYu = "Shan Yu"
+LoD2 = "Land of Dragons 2"
+AnsemRiku = "Ansem Riku"
+StormRider = "Storm Rider"
+DataXigbar = "Data Xigbar"
+
+# twtnw
+Twtnw = "The World That Never Was (Pre Roxas)"
+Roxas = "Roxas"
+Xigbar = "Xigbar"
+Luxord = "Luxord"
+Saix = "Saix"
+Twtnw2 = "The World That Never Was (Second Visit)" # Post riku transformation
+Xemnas = "Xemnas"
+ArmoredXemnas = "Armored Xemnas"
+ArmoredXemnas2 = "Armored Xemnas 2"
+FinalXemnas = "Final Xemnas"
+DataXemnas = "Data Xemnas"
+
+# bc
+Bc = "Beast's Castle"
+Thresholder = "Thresholder"
+Beast = "Beast"
+DarkThorn = "Dark Thorn"
+Bc2 = "Beast's Castle 2"
+Xaldin = "Xaldin"
+DataXaldin = "Data Xaldin"
+
+# ag
+Ag = "Agrabah"
+TwinLords = "Twin Lords"
+Ag2 = "Agrabah 2"
+GenieJafar = "Genie Jafar"
+ASLexaeus = "AS Lexaeus"
+DataLexaeus = "Data Lexaeus"
+
+# pl
+Pl = "Pride Lands"
+Scar = "Scar"
+Pl2 = "Pride Lands 2"
+GroundShaker = "Groundshaker"
+DataSaix = "Data Saix"
+
+# hb
+Hb = "Hollow Bastion"
+Hb2 = "Hollow Bastion 2"
+HBDemyx = "Hollow Bastion Demyx"
+ThousandHeartless = "Thousand Heartless"
+Mushroom13 = "Mushroom 13"
+Sephi = "Sephiroth"
+DataDemyx = "Data Demyx"
+
+# CoR
+CoR = "Cavern of Rememberance"
+CorFirstFight = "Cavern of Rememberance:Fight 1"
+CorSecondFight = "Cavern of Rememberance:Fight 2"
+Transport = "Transport to Rememberance"
+
+# dc
+Dc = "Disney Castle"
+Tr = "Timeless River"
+OldPete = "Old Pete"
+FuturePete = "Future Pete"
+ASMarluxia = "AS Marluxia"
+DataMarluxia = "Data Marluxia"
+Terra = "Terra"
+
+# stt
+Stt = "Simulated Twilight Town"
+TwilightThorn = "Twilight Thorn"
+Axel1 = "Axel 1"
+Axel2 = "Axel 2"
+DataRoxas = "Data Roxas"
+
+AtlanticaSongOne = "Atlantica First Song"
+AtlanticaSongTwo = "Atlantica Second Song"
+AtlanticaSongThree = "Atlantica Third Song"
+AtlanticaSongFour = "Atlantica Fourth Song"
+
+
+LevelsVS1 = "Levels Region (1 Visit Locking Item)"
+LevelsVS3 = "Levels Region (3 Visit Locking Items)"
+LevelsVS6 = "Levels Region (6 Visit Locking Items)"
+LevelsVS9 = "Levels Region (9 Visit Locking Items)"
+LevelsVS12 = "Levels Region (12 Visit Locking Items)"
+LevelsVS15 = "Levels Region (15 Visit Locking Items)"
+LevelsVS18 = "Levels Region (18 Visit Locking Items)"
+LevelsVS21 = "Levels Region (21 Visit Locking Items)"
+LevelsVS24 = "Levels Region (24 Visit Locking Items)"
+LevelsVS26 = "Levels Region (26 Visit Locking Items)"
diff --git a/worlds/kh2/OpenKH.py b/worlds/kh2/OpenKH.py
index c3334dbb9949..17d7f84e8cfd 100644
--- a/worlds/kh2/OpenKH.py
+++ b/worlds/kh2/OpenKH.py
@@ -5,7 +5,7 @@
import Utils
import zipfile
-from .Items import item_dictionary_table, CheckDupingItems
+from .Items import item_dictionary_table
from .Locations import all_locations, SoraLevels, exclusion_table
from .XPValues import lvlStats, formExp, soraExp
from worlds.Files import APContainer
@@ -15,7 +15,7 @@ class KH2Container(APContainer):
game: str = 'Kingdom Hearts 2'
def __init__(self, patch_data: dict, base_path: str, output_directory: str,
- player=None, player_name: str = "", server: str = ""):
+ player=None, player_name: str = "", server: str = ""):
self.patch_data = patch_data
self.file_path = base_path
container_path = os.path.join(output_directory, base_path + ".zip")
@@ -24,12 +24,6 @@ def __init__(self, patch_data: dict, base_path: str, output_directory: str,
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
for filename, yml in self.patch_data.items():
opened_zipfile.writestr(filename, yml)
- for root, dirs, files in os.walk(os.path.join(os.path.dirname(__file__), "mod_template")):
- for file in files:
- opened_zipfile.write(os.path.join(root, file),
- os.path.relpath(os.path.join(root, file),
- os.path.join(os.path.dirname(__file__), "mod_template")))
- # opened_zipfile.writestr(self.zpf_path, self.patch_data)
super().write_contents(opened_zipfile)
@@ -59,44 +53,43 @@ def increaseStat(i):
formexp = None
formName = None
levelsetting = list()
- slotDataDuping = set()
- for values in CheckDupingItems.values():
- if isinstance(values, set):
- slotDataDuping = slotDataDuping.union(values)
- else:
- for inner_values in values.values():
- slotDataDuping = slotDataDuping.union(inner_values)
- if self.multiworld.Keyblade_Minimum[self.player].value > self.multiworld.Keyblade_Maximum[self.player].value:
+ if self.options.Keyblade_Minimum.value > self.options.Keyblade_Maximum.value:
logging.info(
f"{self.multiworld.get_file_safe_player_name(self.player)} has Keyblade Minimum greater than Keyblade Maximum")
- keyblademin = self.multiworld.Keyblade_Maximum[self.player].value
- keyblademax = self.multiworld.Keyblade_Minimum[self.player].value
+ keyblademin = self.options.Keyblade_Maximum.value
+ keyblademax = self.options.Keyblade_Minimum.value
else:
- keyblademin = self.multiworld.Keyblade_Minimum[self.player].value
- keyblademax = self.multiworld.Keyblade_Maximum[self.player].value
+ keyblademin = self.options.Keyblade_Minimum.value
+ keyblademax = self.options.Keyblade_Maximum.value
- if self.multiworld.LevelDepth[self.player] == "level_50":
+ if self.options.LevelDepth == "level_50":
levelsetting.extend(exclusion_table["Level50"])
- elif self.multiworld.LevelDepth[self.player] == "level_99":
+ elif self.options.LevelDepth == "level_99":
levelsetting.extend(exclusion_table["Level99"])
- elif self.multiworld.LevelDepth[self.player] != "level_1":
+ elif self.options.LevelDepth != "level_1":
levelsetting.extend(exclusion_table["Level50Sanity"])
- if self.multiworld.LevelDepth[self.player] == "level_99_sanity":
+ if self.options.LevelDepth == "level_99_sanity":
levelsetting.extend(exclusion_table["Level99Sanity"])
mod_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.get_file_safe_player_name(self.player)}"
+ all_valid_locations = {location for location, data in all_locations.items()}
for location in self.multiworld.get_filled_locations(self.player):
-
- data = all_locations[location.name]
- if location.item.player == self.player:
- itemcode = item_dictionary_table[location.item.name].kh2id
+ if location.name in all_valid_locations:
+ data = all_locations[location.name]
+ else:
+ continue
+ if location.item:
+ if location.item.player == self.player:
+ itemcode = item_dictionary_table[location.item.name].kh2id
+ else:
+ itemcode = 90 # castle map
else:
- itemcode = 90 # castle map
+ itemcode = 90
if data.yml == "Chest":
self.formattedTrsr[data.locid] = {"ItemId": itemcode}
@@ -129,8 +122,8 @@ def increaseStat(i):
elif data.yml == "Keyblade":
self.formattedItem["Stats"].append({
"Id": data.locid,
- "Attack": self.multiworld.per_slot_randoms[self.player].randint(keyblademin, keyblademax),
- "Magic": self.multiworld.per_slot_randoms[self.player].randint(keyblademin, keyblademax),
+ "Attack": self.random.randint(keyblademin, keyblademax),
+ "Magic": self.random.randint(keyblademin, keyblademax),
"Defense": 0,
"Ability": itemcode,
"AbilityPoints": 0,
@@ -150,11 +143,12 @@ def increaseStat(i):
if data.locid == 2:
formDict = {1: "Valor", 2: "Wisdom", 3: "Limit", 4: "Master", 5: "Final"}
formDictExp = {
- 1: self.multiworld.Valor_Form_EXP[self.player].value,
- 2: self.multiworld.Wisdom_Form_EXP[self.player].value,
- 3: self.multiworld.Limit_Form_EXP[self.player].value,
- 4: self.multiworld.Master_Form_EXP[self.player].value,
- 5: self.multiworld.Final_Form_EXP[self.player].value}
+ 1: self.options.Valor_Form_EXP.value,
+ 2: self.options.Wisdom_Form_EXP.value,
+ 3: self.options.Limit_Form_EXP.value,
+ 4: self.options.Master_Form_EXP.value,
+ 5: self.options.Final_Form_EXP.value
+ }
formexp = formDictExp[data.charName]
formName = formDict[data.charName]
self.formattedFmlv[formName] = []
@@ -174,31 +168,32 @@ def increaseStat(i):
"GrowthAbilityLevel": 0,
})
- # Summons have no checks on them so done fully locally
+ # Summons have no actual locations so done down here.
self.formattedFmlv["Summon"] = []
for x in range(1, 7):
self.formattedFmlv["Summon"].append({
"Ability": 123,
- "Experience": int(formExp[0][x] / self.multiworld.Summon_EXP[self.player].value),
+ "Experience": int(formExp[0][x] / self.options.Summon_EXP.value),
"FormId": 0,
"FormLevel": x,
"GrowthAbilityLevel": 0,
})
# levels done down here because of optional settings that can take locations out of the pool.
- self.i = 1
+ self.i = 2
for location in SoraLevels:
- increaseStat(self.multiworld.per_slot_randoms[self.player].randint(0, 3))
+ increaseStat(self.random.randint(0, 3))
if location in levelsetting:
data = self.multiworld.get_location(location, self.player)
- if data.item.player == self.player:
- itemcode = item_dictionary_table[data.item.name].kh2id
- else:
- itemcode = 90 # castle map
+ if data.item:
+ if data.item.player == self.player:
+ itemcode = item_dictionary_table[data.item.name].kh2id
+ else:
+ itemcode = 90 # castle map
else:
- increaseStat(self.multiworld.per_slot_randoms[self.player].randint(0, 3))
+ increaseStat(self.random.randint(0, 3))
itemcode = 0
self.formattedLvup["Sora"][self.i] = {
- "Exp": int(soraExp[self.i] / self.multiworld.Sora_Level_EXP[self.player].value),
+ "Exp": int(soraExp[self.i] / self.options.Sora_Level_EXP.value),
"Strength": self.strength,
"Magic": self.magic,
"Defense": self.defense,
@@ -229,16 +224,208 @@ def increaseStat(i):
"GeneralResistance": 100,
"Unknown": 0
})
+ self.formattedLvup["Sora"][1] = {
+ "Exp": int(soraExp[1] / self.options.Sora_Level_EXP.value),
+ "Strength": 2,
+ "Magic": 6,
+ "Defense": 2,
+ "Ap": 0,
+ "SwordAbility": 0,
+ "ShieldAbility": 0,
+ "StaffAbility": 0,
+ "Padding": 0,
+ "Character": "Sora",
+ "Level": 1
+ }
+ self.mod_yml = {
+ "assets": [
+ {
+ 'method': 'binarc',
+ 'name': '00battle.bin',
+ 'source': [
+ {
+ 'method': 'listpatch',
+ 'name': 'fmlv',
+ 'source': [
+ {
+ 'name': 'FmlvList.yml',
+ 'type': 'fmlv'
+ }
+ ],
+ 'type': 'List'
+ },
+ {
+ 'method': 'listpatch',
+ 'name': 'lvup',
+ 'source': [
+ {
+ 'name': 'LvupList.yml',
+ 'type': 'lvup'
+ }
+ ],
+ 'type': 'List'
+ },
+ {
+ 'method': 'listpatch',
+ 'name': 'bons',
+ 'source': [
+ {
+ 'name': 'BonsList.yml',
+ 'type': 'bons'
+ }
+ ],
+ 'type': 'List'
+ }
+ ]
+ },
+ {
+ 'method': 'binarc',
+ 'name': '03system.bin',
+ 'source': [
+ {
+ 'method': 'listpatch',
+ 'name': 'trsr',
+ 'source': [
+ {
+ 'name': 'TrsrList.yml',
+ 'type': 'trsr'
+ }
+ ],
+ 'type': 'List'
+ },
+ {
+ 'method': 'listpatch',
+ 'name': 'item',
+ 'source': [
+ {
+ 'name': 'ItemList.yml',
+ 'type': 'item'
+ }
+ ],
+ 'type': 'List'
+ }
+ ]
+ },
+ {
+ 'name': 'msg/us/po.bar',
+ 'multi': [
+ {
+ 'name': 'msg/fr/po.bar'
+ },
+ {
+ 'name': 'msg/gr/po.bar'
+ },
+ {
+ 'name': 'msg/it/po.bar'
+ },
+ {
+ 'name': 'msg/sp/po.bar'
+ }
+ ],
+ 'method': 'binarc',
+ 'source': [
+ {
+ 'name': 'po',
+ 'type': 'list',
+ 'method': 'kh2msg',
+ 'source': [
+ {
+ 'name': 'po.yml',
+ 'language': 'en'
+ }
+ ]
+ }
+ ]
+ },
+ {
+ 'name': 'msg/us/sys.bar',
+ 'multi': [
+ {
+ 'name': 'msg/fr/sys.bar'
+ },
+ {
+ 'name': 'msg/gr/sys.bar'
+ },
+ {
+ 'name': 'msg/it/sys.bar'
+ },
+ {
+ 'name': 'msg/sp/sys.bar'
+ }
+ ],
+ 'method': 'binarc',
+ 'source': [
+ {
+ 'name': 'sys',
+ 'type': 'list',
+ 'method': 'kh2msg',
+ 'source': [
+ {
+ 'name': 'sys.yml',
+ 'language': 'en'
+ }
+ ]
+ }
+ ]
+ },
+ ],
+ 'title': 'Randomizer Seed'
+ }
+
+ goal_to_text = {
+ 0: "Three Proofs",
+ 1: "Lucky Emblem",
+ 2: "Hitlist",
+ 3: "Lucky Emblem and Hitlist",
+ }
+ lucky_emblem_text = {
+ 0: "Your Goal is not Lucky Emblem. It is Hitlist or Three Proofs.",
+ 1: f"Lucky Emblem Required: {self.options.LuckyEmblemsRequired} out of {self.options.LuckyEmblemsAmount}",
+ 2: "Your Goal is not Lucky Emblem. It is Hitlist or Three Proofs.",
+ 3: f"Lucky Emblem Required: {self.options.LuckyEmblemsRequired} out of {self.options.LuckyEmblemsAmount}"
+ }
+ hitlist_text = {
+ 0: "Your Goal is not Hitlist. It is Lucky Emblem or Three Proofs",
+ 1: "Your Goal is not Hitlist. It is Lucky Emblem or Three Proofs",
+ 2: f"Bounties Required: {self.options.BountyRequired} out of {self.options.BountyAmount}",
+ 3: f"Bounties Required: {self.options.BountyRequired} out of {self.options.BountyAmount}",
+ }
+
+ self.pooh_text = [
+ {
+ 'id': 18326,
+ 'en': f"Your goal is {goal_to_text[self.options.Goal.value]}"
+ },
+ {
+ 'id': 18327,
+ 'en': lucky_emblem_text[self.options.Goal.value]
+ },
+ {
+ 'id': 18328,
+ 'en': hitlist_text[self.options.Goal.value]
+ }
+ ]
+ self.level_depth_text = [
+ {
+ 'id': 0x3BF1,
+ 'en': f"Your Level Depth is {self.options.LevelDepth.current_option_name}"
+ }
+ ]
mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__)
+ self.mod_yml["title"] = f"Randomizer Seed {mod_name}"
+
openkhmod = {
"TrsrList.yml": yaml.dump(self.formattedTrsr, line_break="\n"),
"LvupList.yml": yaml.dump(self.formattedLvup, line_break="\n"),
"BonsList.yml": yaml.dump(self.formattedBons, line_break="\n"),
"ItemList.yml": yaml.dump(self.formattedItem, line_break="\n"),
"FmlvList.yml": yaml.dump(self.formattedFmlv, line_break="\n"),
+ "mod.yml": yaml.dump(self.mod_yml, line_break="\n"),
+ "po.yml": yaml.dump(self.pooh_text, line_break="\n"),
+ "sys.yml": yaml.dump(self.level_depth_text, line_break="\n"),
}
mod = KH2Container(openkhmod, mod_dir, output_directory, self.player,
- self.multiworld.get_file_safe_player_name(self.player))
+ self.multiworld.get_file_safe_player_name(self.player))
mod.write()
diff --git a/worlds/kh2/Options.py b/worlds/kh2/Options.py
index 7a6f106aa9b8..ddaf36ebcbf9 100644
--- a/worlds/kh2/Options.py
+++ b/worlds/kh2/Options.py
@@ -1,7 +1,8 @@
-from Options import Choice, Option, Range, Toggle, OptionSet
-import typing
+from dataclasses import dataclass
-from worlds.kh2 import SupportAbility_Table, ActionAbility_Table
+from Options import Choice, Range, Toggle, ItemDict, PerGameCommonOptions, StartInventoryPool
+
+from . import default_itempool_option
class SoraEXP(Range):
@@ -107,23 +108,61 @@ class Visitlocking(Choice):
First and Second Visit Locking: One item for First Visit Two For Second Visit"""
display_name = "Visit locking"
option_no_visit_locking = 0 # starts with 25 visit locking
- option_second_visit_locking = 1 # starts with 13 (no icecream/picture)
+ option_second_visit_locking = 1 # starts with 12 visit locking
option_first_and_second_visit_locking = 2 # starts with nothing
default = 2
+class FightLogic(Choice):
+ """
+ The level of logic to use when determining what fights in each KH2 world are beatable.
+
+ Easy: For Players not very comfortable doing things without a lot of tools.
+
+ Normal: For Players somewhat comfortable doing fights with some of the tools.
+
+ Hard: For Players comfortable doing fights with almost no tools.
+ """
+ display_name = "Fight Logic"
+ option_easy = 0
+ option_normal = 1
+ option_hard = 2
+ default = 1
+
+
+class FinalFormLogic(Choice):
+ """Determines forcing final form logic
+
+ No Light and Darkness: Light and Darkness is not in logic.
+ Light And Darkness: Final Forcing with light and darkness is in logic.
+ Just a Form: All that requires final forcing is another form.
+ """
+ display_name = "Final Form Logic"
+ option_no_light_and_darkness = 0
+ option_light_and_darkness = 1
+ option_just_a_form = 2
+ default = 1
+
+
+class AutoFormLogic(Toggle):
+ """ Have Auto Forms levels in logic.
+ """
+ display_name = "Auto Form Logic"
+ default = False
+
+
class RandomVisitLockingItem(Range):
"""Start with random amount of visit locking items."""
display_name = "Random Visit Locking Item"
range_start = 0
range_end = 25
- default = 3
+ default = 0
class SuperBosses(Toggle):
- """Terra, Sephiroth and Data Fights Toggle."""
+ """Terra Sephiroth and Data Fights Toggle."""
display_name = "Super Bosses"
- default = False
+ default = True
class Cups(Choice):
@@ -135,7 +174,7 @@ class Cups(Choice):
option_no_cups = 0
option_cups = 1
option_cups_and_hades_paradox = 2
- default = 1
+ default = 0
class LevelDepth(Choice):
@@ -157,67 +196,71 @@ class LevelDepth(Choice):
default = 0
-class PromiseCharm(Toggle):
- """Add Promise Charm to the Pool"""
- display_name = "Promise Charm"
- default = False
+class DonaldGoofyStatsanity(Toggle):
+ """Toggles if on Donald and Goofy's Get Bonus locations can be any item"""
+ display_name = "Donald & Goofy Statsanity"
+ default = True
-class KeybladeAbilities(Choice):
- """
- Action: Action Abilities in the Keyblade Slot Pool.
+class AtlanticaToggle(Toggle):
+ """Atlantica Toggle"""
+ display_name = "Atlantica Toggle"
+ default = False
- Support: Support Abilities in the Keyblade Slot Pool.
- Both: Action and Support Abilities in the Keyblade Slot Pool."""
- display_name = "Keyblade Abilities"
- option_support = 0
- option_action = 1
- option_both = 2
- default = 0
+class PromiseCharm(Toggle):
+ """Add Promise Charm to the pool"""
+ display_name = "Promise Charm"
+ default = False
-class BlacklistKeyblade(OptionSet):
- """Black List these Abilities on Keyblades"""
- display_name = "Blacklist Keyblade Abilities"
- valid_keys = set(SupportAbility_Table.keys()).union(ActionAbility_Table.keys())
+class AntiForm(Toggle):
+ """Add Anti Form to the pool"""
+ display_name = "Anti Form"
+ default = False
class Goal(Choice):
"""Win Condition
- Three Proofs: Get a Gold Crown on Sora's Head.
+ Three Proofs: Find the 3 Proofs to unlock the final door.
+
+ Lucky Emblem Hunt: Find required amount of Lucky Emblems.
- Lucky Emblem Hunt: Find Required Amount of Lucky Emblems .
+ Hitlist (Bounty Hunt): Find required amount of Bounties.
- Hitlist (Bounty Hunt): Find Required Amount of Bounties"""
+ Lucky Emblem and Hitlist: Find the required amount of Lucky Emblems and Bounties."""
display_name = "Goal"
option_three_proofs = 0
option_lucky_emblem_hunt = 1
option_hitlist = 2
- default = 0
+ option_hitlist_and_lucky_emblem = 3
+ default = 1
class FinalXemnas(Toggle):
"""Kill Final Xemnas to Beat the Game.
- This is in addition to your Goal. I.E. get three proofs+kill final Xemnas"""
+
+ This is in addition to your Goal.
+
+ I.E. get three proofs+kill final Xemnas"""
display_name = "Final Xemnas"
default = True
class LuckyEmblemsRequired(Range):
- """Number of Lucky Emblems to collect to Win/Unlock Final Xemnas Door.
+ """Number of Lucky Emblems to collect to Win/Unlock Final Xemnas' Door.
- If Goal is not Lucky Emblem Hunt this does nothing."""
+ If Goal is not Lucky Emblem Hunt or Lucky Emblem and Hitlist this does nothing."""
display_name = "Lucky Emblems Required"
range_start = 1
range_end = 60
- default = 30
+ default = 35
class LuckyEmblemsAmount(Range):
"""Number of Lucky Emblems that are in the pool.
- If Goal is not Lucky Emblem Hunt this does nothing."""
+ If Goal is not Lucky Emblem Hunt or Lucky Emblem and Hitlist this does nothing."""
display_name = "Lucky Emblems Available"
range_start = 1
range_end = 60
@@ -227,48 +270,103 @@ class LuckyEmblemsAmount(Range):
class BountyRequired(Range):
"""Number of Bounties to collect to Win/Unlock Final Xemnas Door.
- If Goal is not Hitlist this does nothing."""
+ If Goal is not Hitlist or Lucky Emblem and Hitlist this does nothing."""
display_name = "Bounties Required"
range_start = 1
- range_end = 24
+ range_end = 26
default = 7
class BountyAmount(Range):
"""Number of Bounties that are in the pool.
- If Goal is not Hitlist this does nothing."""
+ If Goal is not Hitlist or Lucky Emblem and Hitlist this does nothing."""
display_name = "Bounties Available"
range_start = 1
- range_end = 24
- default = 13
-
-
-KH2_Options: typing.Dict[str, type(Option)] = {
- "LevelDepth": LevelDepth,
- "Sora_Level_EXP": SoraEXP,
- "Valor_Form_EXP": ValorEXP,
- "Wisdom_Form_EXP": WisdomEXP,
- "Limit_Form_EXP": LimitEXP,
- "Master_Form_EXP": MasterEXP,
- "Final_Form_EXP": FinalEXP,
- "Summon_EXP": SummonEXP,
- "Schmovement": Schmovement,
- "RandomGrowth": RandomGrowth,
- "Promise_Charm": PromiseCharm,
- "Goal": Goal,
- "FinalXemnas": FinalXemnas,
- "LuckyEmblemsAmount": LuckyEmblemsAmount,
- "LuckyEmblemsRequired": LuckyEmblemsRequired,
- "BountyAmount": BountyAmount,
- "BountyRequired": BountyRequired,
- "Keyblade_Minimum": KeybladeMin,
- "Keyblade_Maximum": KeybladeMax,
- "Visitlocking": Visitlocking,
- "RandomVisitLockingItem": RandomVisitLockingItem,
- "SuperBosses": SuperBosses,
- "KeybladeAbilities": KeybladeAbilities,
- "BlacklistKeyblade": BlacklistKeyblade,
- "Cups": Cups,
-
-}
+ range_end = 26
+ default = 10
+
+
+class BountyStartHint(Toggle):
+ """Start with Bounties Hinted"""
+ display_name = "Start with Bounties Hinted"
+ default = False
+
+
+class WeaponSlotStartHint(Toggle):
+ """Start with Weapon Slots' Hinted"""
+ display_name = "Start with Weapon Slots Hinted"
+ default = False
+
+
+class CorSkipToggle(Toggle):
+ """Toggle for Cor skip.
+
+ Tools depend on which difficulty was chosen on Fight Difficulty.
+
+ Toggle does not negate fight logic but is an alternative.
+
+ Full Cor Skip is also affected by this Toggle.
+ """
+ display_name = "CoR Skip Toggle"
+ default = False
+
+
+class CustomItemPoolQuantity(ItemDict):
+ """Add more of an item into the itempool. Note: You cannot take out items from the pool."""
+ display_name = "Custom Item Pool"
+ valid_keys = default_itempool_option.keys()
+ default = default_itempool_option
+
+
+class FillerItemsLocal(Toggle):
+ """Make all dynamic filler classified items local. Recommended when playing with games with fewer locations than kh2"""
+ display_name = "Local Filler Items"
+ default = True
+
+
+class SummonLevelLocationToggle(Toggle):
+ """Toggle Summon levels to have locations."""
+ display_name = "Summon Level Locations"
+ default = False
+
+
+# shamelessly stolen from the messanger
+@dataclass
+class KingdomHearts2Options(PerGameCommonOptions):
+ start_inventory: StartInventoryPool
+ LevelDepth: LevelDepth
+ Sora_Level_EXP: SoraEXP
+ Valor_Form_EXP: ValorEXP
+ Wisdom_Form_EXP: WisdomEXP
+ Limit_Form_EXP: LimitEXP
+ Master_Form_EXP: MasterEXP
+ Final_Form_EXP: FinalEXP
+ Summon_EXP: SummonEXP
+ Schmovement: Schmovement
+ RandomGrowth: RandomGrowth
+ AntiForm: AntiForm
+ Promise_Charm: PromiseCharm
+ Goal: Goal
+ FinalXemnas: FinalXemnas
+ LuckyEmblemsAmount: LuckyEmblemsAmount
+ LuckyEmblemsRequired: LuckyEmblemsRequired
+ BountyAmount: BountyAmount
+ BountyRequired: BountyRequired
+ BountyStartingHintToggle: BountyStartHint
+ Keyblade_Minimum: KeybladeMin
+ Keyblade_Maximum: KeybladeMax
+ WeaponSlotStartHint: WeaponSlotStartHint
+ FightLogic: FightLogic
+ FinalFormLogic: FinalFormLogic
+ AutoFormLogic: AutoFormLogic
+ DonaldGoofyStatsanity: DonaldGoofyStatsanity
+ FillerItemsLocal: FillerItemsLocal
+ Visitlocking: Visitlocking
+ RandomVisitLockingItem: RandomVisitLockingItem
+ SuperBosses: SuperBosses
+ Cups: Cups
+ SummonLevelLocationToggle: SummonLevelLocationToggle
+ AtlanticaToggle: AtlanticaToggle
+ CorSkipToggle: CorSkipToggle
+ CustomItemPoolQuantity: CustomItemPoolQuantity
diff --git a/worlds/kh2/Regions.py b/worlds/kh2/Regions.py
index 36fc0c046b5c..7fc2ad8a873f 100644
--- a/worlds/kh2/Regions.py
+++ b/worlds/kh2/Regions.py
@@ -1,35 +1,24 @@
import typing
-from BaseClasses import MultiWorld, Region, Entrance
+from BaseClasses import MultiWorld, Region
+from . import Locations
-from .Locations import KH2Location, RegionTable
-from .Names import LocationName, ItemName, RegionName
+from .Subclasses import KH2Location
+from .Names import LocationName, RegionName
+from .Items import Events_Table
-
-def create_regions(world, player: int, active_locations):
- menu_region = create_region(world, player, active_locations, 'Menu', None)
-
- goa_region_locations = [
- LocationName.Crit_1,
- LocationName.Crit_2,
- LocationName.Crit_3,
- LocationName.Crit_4,
- LocationName.Crit_5,
- LocationName.Crit_6,
- LocationName.Crit_7,
+KH2REGIONS: typing.Dict[str, typing.List[str]] = {
+ "Menu": [],
+ RegionName.GoA: [
LocationName.GardenofAssemblageMap,
LocationName.GoALostIllusion,
LocationName.ProofofNonexistence,
- LocationName.DonaldStarting1,
- LocationName.DonaldStarting2,
- LocationName.GoofyStarting1,
- LocationName.GoofyStarting2,
- ]
-
- goa_region = create_region(world, player, active_locations, RegionName.GoA_Region,
- goa_region_locations)
-
- lod_Region_locations = [
+ # LocationName.DonaldStarting1,
+ # LocationName.DonaldStarting2,
+ # LocationName.GoofyStarting1,
+ # LocationName.GoofyStarting2
+ ],
+ RegionName.LoD: [
LocationName.BambooGroveDarkShard,
LocationName.BambooGroveEther,
LocationName.BambooGroveMythrilShard,
@@ -47,14 +36,16 @@ def create_regions(world, player: int, active_locations):
LocationName.VillageCaveBonus,
LocationName.RidgeFrostShard,
LocationName.RidgeAPBoost,
+ ],
+ RegionName.ShanYu: [
LocationName.ShanYu,
LocationName.ShanYuGetBonus,
LocationName.HiddenDragon,
LocationName.GoofyShanYu,
- ]
- lod_Region = create_region(world, player, active_locations, RegionName.LoD_Region,
- lod_Region_locations)
- lod2_Region_locations = [
+ LocationName.ShanYuEventLocation
+ ],
+ RegionName.LoD2: [],
+ RegionName.AnsemRiku: [
LocationName.ThroneRoomTornPages,
LocationName.ThroneRoomPalaceMap,
LocationName.ThroneRoomAPBoost,
@@ -63,13 +54,18 @@ def create_regions(world, player: int, active_locations):
LocationName.ThroneRoomOgreShield,
LocationName.ThroneRoomMythrilCrystal,
LocationName.ThroneRoomOrichalcum,
+ LocationName.AnsemRikuEventLocation,
+ ],
+ RegionName.StormRider: [
LocationName.StormRider,
- LocationName.XigbarDataDefenseBoost,
LocationName.GoofyStormRider,
- ]
- lod2_Region = create_region(world, player, active_locations, RegionName.LoD2_Region,
- lod2_Region_locations)
- ag_region_locations = [
+ LocationName.StormRiderEventLocation
+ ],
+ RegionName.DataXigbar: [
+ LocationName.XigbarDataDefenseBoost,
+ LocationName.DataXigbarEventLocation
+ ],
+ RegionName.Ag: [
LocationName.AgrabahMap,
LocationName.AgrabahDarkShard,
LocationName.AgrabahMythrilShard,
@@ -97,30 +93,30 @@ def create_regions(world, player: int, active_locations):
LocationName.TreasureRoom,
LocationName.TreasureRoomAPBoost,
LocationName.TreasureRoomSerenityGem,
+ LocationName.GoofyTreasureRoom,
+ LocationName.DonaldAbuEscort
+ ],
+ RegionName.TwinLords: [
LocationName.ElementalLords,
LocationName.LampCharm,
- LocationName.GoofyTreasureRoom,
- LocationName.DonaldAbuEscort,
- ]
- ag_region = create_region(world, player, active_locations, RegionName.Ag_Region,
- ag_region_locations)
- ag2_region_locations = [
+ LocationName.TwinLordsEventLocation
+ ],
+ RegionName.Ag2: [
LocationName.RuinedChamberTornPages,
LocationName.RuinedChamberRuinsMap,
+ ],
+ RegionName.GenieJafar: [
LocationName.GenieJafar,
LocationName.WishingLamp,
- ]
- ag2_region = create_region(world, player, active_locations, RegionName.Ag2_Region,
- ag2_region_locations)
- lexaeus_region_locations = [
+ LocationName.GenieJafarEventLocation,
+ ],
+ RegionName.DataLexaeus: [
LocationName.LexaeusBonus,
LocationName.LexaeusASStrengthBeyondStrength,
LocationName.LexaeusDataLostIllusion,
- ]
- lexaeus_region = create_region(world, player, active_locations, RegionName.Lexaeus_Region,
- lexaeus_region_locations)
-
- dc_region_locations = [
+ LocationName.DataLexaeusEventLocation
+ ],
+ RegionName.Dc: [
LocationName.DCCourtyardMythrilShard,
LocationName.DCCourtyardStarRecipe,
LocationName.DCCourtyardAPBoost,
@@ -131,74 +127,65 @@ def create_regions(world, player: int, active_locations):
LocationName.LibraryTornPages,
LocationName.DisneyCastleMap,
LocationName.MinnieEscort,
- LocationName.MinnieEscortGetBonus,
- ]
- dc_region = create_region(world, player, active_locations, RegionName.Dc_Region,
- dc_region_locations)
- tr_region_locations = [
+ LocationName.MinnieEscortGetBonus
+ ],
+ RegionName.Tr: [
LocationName.CornerstoneHillMap,
LocationName.CornerstoneHillFrostShard,
LocationName.PierMythrilShard,
LocationName.PierHiPotion,
+ ],
+ RegionName.OldPete: [
LocationName.WaterwayMythrilStone,
LocationName.WaterwayAPBoost,
LocationName.WaterwayFrostStone,
LocationName.WindowofTimeMap,
LocationName.BoatPete,
+ LocationName.DonaldBoatPete,
+ LocationName.DonaldBoatPeteGetBonus,
+ LocationName.OldPeteEventLocation,
+ ],
+ RegionName.FuturePete: [
LocationName.FuturePete,
LocationName.FuturePeteGetBonus,
LocationName.Monochrome,
LocationName.WisdomForm,
- LocationName.DonaldBoatPete,
- LocationName.DonaldBoatPeteGetBonus,
LocationName.GoofyFuturePete,
- ]
- tr_region = create_region(world, player, active_locations, RegionName.Tr_Region,
- tr_region_locations)
- marluxia_region_locations = [
+ LocationName.FuturePeteEventLocation
+ ],
+ RegionName.DataMarluxia: [
LocationName.MarluxiaGetBonus,
LocationName.MarluxiaASEternalBlossom,
LocationName.MarluxiaDataLostIllusion,
- ]
- marluxia_region = create_region(world, player, active_locations, RegionName.Marluxia_Region,
- marluxia_region_locations)
- terra_region_locations = [
+ LocationName.DataMarluxiaEventLocation
+ ],
+ RegionName.Terra: [
LocationName.LingeringWillBonus,
LocationName.LingeringWillProofofConnection,
LocationName.LingeringWillManifestIllusion,
- ]
- terra_region = create_region(world, player, active_locations, RegionName.Terra_Region,
- terra_region_locations)
-
- hundred_acre1_region_locations = [
+ LocationName.TerraEventLocation
+ ],
+ RegionName.Ha1: [
LocationName.PoohsHouse100AcreWoodMap,
LocationName.PoohsHouseAPBoost,
- LocationName.PoohsHouseMythrilStone,
- ]
- hundred_acre1_region = create_region(world, player, active_locations, RegionName.HundredAcre1_Region,
- hundred_acre1_region_locations)
- hundred_acre2_region_locations = [
+ LocationName.PoohsHouseMythrilStone
+ ],
+ RegionName.Ha2: [
LocationName.PigletsHouseDefenseBoost,
LocationName.PigletsHouseAPBoost,
- LocationName.PigletsHouseMythrilGem,
- ]
- hundred_acre2_region = create_region(world, player, active_locations, RegionName.HundredAcre2_Region,
- hundred_acre2_region_locations)
- hundred_acre3_region_locations = [
+ LocationName.PigletsHouseMythrilGem
+ ],
+ RegionName.Ha3: [
LocationName.RabbitsHouseDrawRing,
LocationName.RabbitsHouseMythrilCrystal,
LocationName.RabbitsHouseAPBoost,
- ]
- hundred_acre3_region = create_region(world, player, active_locations, RegionName.HundredAcre3_Region,
- hundred_acre3_region_locations)
- hundred_acre4_region_locations = [
+ ],
+ RegionName.Ha4: [
LocationName.KangasHouseMagicBoost,
LocationName.KangasHouseAPBoost,
LocationName.KangasHouseOrichalcum,
- ]
- hundred_acre4_region = create_region(world, player, active_locations, RegionName.HundredAcre4_Region,
- hundred_acre4_region_locations)
- hundred_acre5_region_locations = [
+ ],
+ RegionName.Ha5: [
LocationName.SpookyCaveMythrilGem,
LocationName.SpookyCaveAPBoost,
LocationName.SpookyCaveOrichalcum,
@@ -206,19 +193,15 @@ def create_regions(world, player: int, active_locations):
LocationName.SpookyCaveMythrilCrystal,
LocationName.SpookyCaveAPBoost2,
LocationName.SweetMemories,
- LocationName.SpookyCaveMap,
- ]
- hundred_acre5_region = create_region(world, player, active_locations, RegionName.HundredAcre5_Region,
- hundred_acre5_region_locations)
- hundred_acre6_region_locations = [
+ LocationName.SpookyCaveMap
+ ],
+ RegionName.Ha6: [
LocationName.StarryHillCosmicRing,
LocationName.StarryHillStyleRecipe,
LocationName.StarryHillCureElement,
- LocationName.StarryHillOrichalcumPlus,
- ]
- hundred_acre6_region = create_region(world, player, active_locations, RegionName.HundredAcre6_Region,
- hundred_acre6_region_locations)
- pr_region_locations = [
+ LocationName.StarryHillOrichalcumPlus
+ ],
+ RegionName.Pr: [
LocationName.RampartNavalMap,
LocationName.RampartMythrilStone,
LocationName.RampartDarkShard,
@@ -236,17 +219,20 @@ def create_regions(world, player: int, active_locations):
LocationName.MoonlightNookMythrilShard,
LocationName.MoonlightNookSerenityGem,
LocationName.MoonlightNookPowerStone,
+ LocationName.DonaldBoatFight,
+ LocationName.GoofyInterceptorBarrels,
+
+ ],
+ RegionName.Barbosa: [
LocationName.Barbossa,
LocationName.BarbossaGetBonus,
LocationName.FollowtheWind,
- LocationName.DonaldBoatFight,
LocationName.GoofyBarbossa,
LocationName.GoofyBarbossaGetBonus,
- LocationName.GoofyInterceptorBarrels,
- ]
- pr_region = create_region(world, player, active_locations, RegionName.Pr_Region,
- pr_region_locations)
- pr2_region_locations = [
+ LocationName.BarbosaEventLocation,
+ ],
+ RegionName.Pr2: [],
+ RegionName.GrimReaper1: [
LocationName.GrimReaper1,
LocationName.InterceptorsHoldFeatherCharm,
LocationName.SeadriftKeepAPBoost,
@@ -258,19 +244,19 @@ def create_regions(world, player: int, active_locations):
LocationName.SeadriftRowCursedMedallion,
LocationName.SeadriftRowShipGraveyardMap,
LocationName.GoofyGrimReaper1,
-
- ]
- pr2_region = create_region(world, player, active_locations, RegionName.Pr2_Region,
- pr2_region_locations)
- gr2_region_locations = [
+ LocationName.GrimReaper1EventLocation,
+ ],
+ RegionName.GrimReaper2: [
LocationName.DonaladGrimReaper2,
LocationName.GrimReaper2,
LocationName.SecretAnsemReport6,
+ LocationName.GrimReaper2EventLocation,
+ ],
+ RegionName.DataLuxord: [
LocationName.LuxordDataAPBoost,
- ]
- gr2_region = create_region(world, player, active_locations, RegionName.Gr2_Region,
- gr2_region_locations)
- oc_region_locations = [
+ LocationName.DataLuxordEventLocation
+ ],
+ RegionName.Oc: [
LocationName.PassageMythrilShard,
LocationName.PassageMythrilStone,
LocationName.PassageEther,
@@ -278,6 +264,8 @@ def create_regions(world, player: int, active_locations):
LocationName.PassageHiPotion,
LocationName.InnerChamberUnderworldMap,
LocationName.InnerChamberMythrilShard,
+ ],
+ RegionName.Cerberus: [
LocationName.Cerberus,
LocationName.ColiseumMap,
LocationName.Urns,
@@ -297,56 +285,61 @@ def create_regions(world, player: int, active_locations):
LocationName.TheLockCavernsMap,
LocationName.TheLockMythrilShard,
LocationName.TheLockAPBoost,
+ LocationName.CerberusEventLocation
+ ],
+ RegionName.OlympusPete: [
LocationName.PeteOC,
+ LocationName.DonaldDemyxOC,
+ LocationName.GoofyPeteOC,
+ LocationName.OlympusPeteEventLocation
+ ],
+ RegionName.Hydra: [
LocationName.Hydra,
LocationName.HydraGetBonus,
LocationName.HerosCrest,
- LocationName.DonaldDemyxOC,
- LocationName.GoofyPeteOC,
- ]
- oc_region = create_region(world, player, active_locations, RegionName.Oc_Region,
- oc_region_locations)
- oc2_region_locations = [
+ LocationName.HydraEventLocation
+ ],
+ RegionName.Oc2: [
LocationName.AuronsStatue,
+ ],
+ RegionName.Hades: [
LocationName.Hades,
LocationName.HadesGetBonus,
LocationName.GuardianSoul,
-
- ]
- oc2_region = create_region(world, player, active_locations, RegionName.Oc2_Region,
- oc2_region_locations)
- oc2_pain_and_panic_locations = [
+ LocationName.HadesEventLocation
+ ],
+ RegionName.OcPainAndPanicCup: [
LocationName.ProtectBeltPainandPanicCup,
LocationName.SerenityGemPainandPanicCup,
- ]
- oc2_titan_locations = [
- LocationName.GenjiShieldTitanCup,
- LocationName.SkillfulRingTitanCup,
- ]
- oc2_cerberus_locations = [
+ LocationName.OcPainAndPanicCupEventLocation
+ ],
+ RegionName.OcCerberusCup: [
LocationName.RisingDragonCerberusCup,
LocationName.SerenityCrystalCerberusCup,
- ]
- oc2_gof_cup_locations = [
+ LocationName.OcCerberusCupEventLocation
+ ],
+ RegionName.Oc2TitanCup: [
+ LocationName.GenjiShieldTitanCup,
+ LocationName.SkillfulRingTitanCup,
+ LocationName.Oc2TitanCupEventLocation
+ ],
+ RegionName.Oc2GofCup: [
LocationName.FatalCrestGoddessofFateCup,
LocationName.OrichalcumPlusGoddessofFateCup,
+ LocationName.Oc2GofCupEventLocation,
+ ],
+ RegionName.HadesCups: [
LocationName.HadesCupTrophyParadoxCups,
- ]
- zexion_region_locations = [
+ LocationName.HadesCupEventLocations
+ ],
+ RegionName.DataZexion: [
LocationName.ZexionBonus,
LocationName.ZexionASBookofShadows,
LocationName.ZexionDataLostIllusion,
LocationName.GoofyZexion,
- ]
- oc2_pain_and_panic_cup = create_region(world, player, active_locations, RegionName.Oc2_pain_and_panic_Region,
- oc2_pain_and_panic_locations)
- oc2_titan_cup = create_region(world, player, active_locations, RegionName.Oc2_titan_Region, oc2_titan_locations)
- oc2_cerberus_cup = create_region(world, player, active_locations, RegionName.Oc2_cerberus_Region,
- oc2_cerberus_locations)
- oc2_gof_cup = create_region(world, player, active_locations, RegionName.Oc2_gof_Region, oc2_gof_cup_locations)
- zexion_region = create_region(world, player, active_locations, RegionName.Zexion_Region, zexion_region_locations)
-
- bc_region_locations = [
+ LocationName.DataZexionEventLocation
+ ],
+ RegionName.Bc: [
LocationName.BCCourtyardAPBoost,
LocationName.BCCourtyardHiPotion,
LocationName.BCCourtyardMythrilShard,
@@ -359,6 +352,8 @@ def create_regions(world, player: int, active_locations):
LocationName.TheWestHallMythrilShard2,
LocationName.TheWestHallBrightStone,
LocationName.TheWestHallMythrilShard,
+ ],
+ RegionName.Thresholder: [
LocationName.Thresholder,
LocationName.DungeonBasementMap,
LocationName.DungeonAPBoost,
@@ -368,33 +363,37 @@ def create_regions(world, player: int, active_locations):
LocationName.TheWestHallAPBoostPostDungeon,
LocationName.TheWestWingMythrilShard,
LocationName.TheWestWingTent,
+ LocationName.DonaldThresholder,
+ LocationName.ThresholderEventLocation
+ ],
+ RegionName.Beast: [
LocationName.Beast,
LocationName.TheBeastsRoomBlazingShard,
+ LocationName.GoofyBeast,
+ LocationName.BeastEventLocation
+ ],
+ RegionName.DarkThorn: [
LocationName.DarkThorn,
LocationName.DarkThornGetBonus,
LocationName.DarkThornCureElement,
- LocationName.DonaldThresholder,
- LocationName.GoofyBeast,
- ]
- bc_region = create_region(world, player, active_locations, RegionName.Bc_Region,
- bc_region_locations)
- bc2_region_locations = [
+ LocationName.DarkThornEventLocation,
+ ],
+ RegionName.Bc2: [
LocationName.RumblingRose,
- LocationName.CastleWallsMap,
-
- ]
- bc2_region = create_region(world, player, active_locations, RegionName.Bc2_Region,
- bc2_region_locations)
- xaldin_region_locations = [
+ LocationName.CastleWallsMap
+ ],
+ RegionName.Xaldin: [
LocationName.Xaldin,
LocationName.XaldinGetBonus,
LocationName.DonaldXaldinGetBonus,
LocationName.SecretAnsemReport4,
+ LocationName.XaldinEventLocation
+ ],
+ RegionName.DataXaldin: [
LocationName.XaldinDataDefenseBoost,
- ]
- xaldin_region = create_region(world, player, active_locations, RegionName.Xaldin_Region,
- xaldin_region_locations)
- sp_region_locations = [
+ LocationName.DataXaldinEventLocation
+ ],
+ RegionName.Sp: [
LocationName.PitCellAreaMap,
LocationName.PitCellMythrilCrystal,
LocationName.CanyonDarkCrystal,
@@ -406,41 +405,35 @@ def create_regions(world, player: int, active_locations):
LocationName.HallwayAPBoost,
LocationName.CommunicationsRoomIOTowerMap,
LocationName.CommunicationsRoomGaiaBelt,
+ LocationName.DonaldScreens,
+ ],
+ RegionName.HostileProgram: [
LocationName.HostileProgram,
LocationName.HostileProgramGetBonus,
LocationName.PhotonDebugger,
- LocationName.DonaldScreens,
LocationName.GoofyHostileProgram,
-
- ]
- sp_region = create_region(world, player, active_locations, RegionName.Sp_Region,
- sp_region_locations)
- sp2_region_locations = [
+ LocationName.HostileProgramEventLocation
+ ],
+ RegionName.Sp2: [
LocationName.SolarSailer,
LocationName.CentralComputerCoreAPBoost,
LocationName.CentralComputerCoreOrichalcumPlus,
LocationName.CentralComputerCoreCosmicArts,
LocationName.CentralComputerCoreMap,
-
- LocationName.DonaldSolarSailer,
- ]
-
- sp2_region = create_region(world, player, active_locations, RegionName.Sp2_Region,
- sp2_region_locations)
- mcp_region_locations = [
+ LocationName.DonaldSolarSailer
+ ],
+ RegionName.Mcp: [
LocationName.MCP,
LocationName.MCPGetBonus,
- ]
- mcp_region = create_region(world, player, active_locations, RegionName.Mcp_Region,
- mcp_region_locations)
- larxene_region_locations = [
+ LocationName.McpEventLocation
+ ],
+ RegionName.DataLarxene: [
LocationName.LarxeneBonus,
LocationName.LarxeneASCloakedThunder,
LocationName.LarxeneDataLostIllusion,
- ]
- larxene_region = create_region(world, player, active_locations, RegionName.Larxene_Region,
- larxene_region_locations)
- ht_region_locations = [
+ LocationName.DataLarxeneEventLocation
+ ],
+ RegionName.Ht: [
LocationName.GraveyardMythrilShard,
LocationName.GraveyardSerenityGem,
LocationName.FinklesteinsLabHalloweenTownMap,
@@ -455,34 +448,37 @@ def create_regions(world, player: int, active_locations):
LocationName.CandyCaneLaneMythrilStone,
LocationName.SantasHouseChristmasTownMap,
LocationName.SantasHouseAPBoost,
+ ],
+ RegionName.PrisonKeeper: [
LocationName.PrisonKeeper,
+ LocationName.DonaldPrisonKeeper,
+ LocationName.PrisonKeeperEventLocation,
+ ],
+ RegionName.OogieBoogie: [
LocationName.OogieBoogie,
LocationName.OogieBoogieMagnetElement,
- LocationName.DonaldPrisonKeeper,
LocationName.GoofyOogieBoogie,
- ]
- ht_region = create_region(world, player, active_locations, RegionName.Ht_Region,
- ht_region_locations)
- ht2_region_locations = [
+ LocationName.OogieBoogieEventLocation
+ ],
+ RegionName.Ht2: [
LocationName.Lock,
LocationName.Present,
LocationName.DecoyPresents,
+ LocationName.GoofyLock
+ ],
+ RegionName.Experiment: [
LocationName.Experiment,
LocationName.DecisivePumpkin,
-
LocationName.DonaldExperiment,
- LocationName.GoofyLock,
- ]
- ht2_region = create_region(world, player, active_locations, RegionName.Ht2_Region,
- ht2_region_locations)
- vexen_region_locations = [
+ LocationName.ExperimentEventLocation,
+ ],
+ RegionName.DataVexen: [
LocationName.VexenBonus,
LocationName.VexenASRoadtoDiscovery,
LocationName.VexenDataLostIllusion,
- ]
- vexen_region = create_region(world, player, active_locations, RegionName.Vexen_Region,
- vexen_region_locations)
- hb_region_locations = [
+ LocationName.DataVexenEventLocation
+ ],
+ RegionName.Hb: [
LocationName.MarketplaceMap,
LocationName.BoroughDriveRecovery,
LocationName.BoroughAPBoost,
@@ -493,11 +489,9 @@ def create_regions(world, player: int, active_locations):
LocationName.MerlinsHouseBlizzardElement,
LocationName.Bailey,
LocationName.BaileySecretAnsemReport7,
- LocationName.BaseballCharm,
- ]
- hb_region = create_region(world, player, active_locations, RegionName.Hb_Region,
- hb_region_locations)
- hb2_region_locations = [
+ LocationName.BaseballCharm
+ ],
+ RegionName.Hb2: [
LocationName.PosternCastlePerimeterMap,
LocationName.PosternMythrilGem,
LocationName.PosternAPBoost,
@@ -511,18 +505,9 @@ def create_regions(world, player: int, active_locations):
LocationName.AnsemsStudyUkuleleCharm,
LocationName.RestorationSiteMoonRecipe,
LocationName.RestorationSiteAPBoost,
- LocationName.CoRDepthsAPBoost,
- LocationName.CoRDepthsPowerCrystal,
- LocationName.CoRDepthsFrostCrystal,
- LocationName.CoRDepthsManifestIllusion,
- LocationName.CoRDepthsAPBoost2,
- LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap,
- LocationName.CoRMineshaftLowerLevelAPBoost,
+ ],
+ RegionName.HBDemyx: [
LocationName.DonaldDemyxHBGetBonus,
- ]
- hb2_region = create_region(world, player, active_locations, RegionName.Hb2_Region,
- hb2_region_locations)
- onek_region_locations = [
LocationName.DemyxHB,
LocationName.DemyxHBGetBonus,
LocationName.FFFightsCureElement,
@@ -530,30 +515,41 @@ def create_regions(world, player: int, active_locations):
LocationName.CrystalFissureTheGreatMawMap,
LocationName.CrystalFissureEnergyCrystal,
LocationName.CrystalFissureAPBoost,
+ LocationName.HBDemyxEventLocation,
+ ],
+ RegionName.ThousandHeartless: [
LocationName.ThousandHeartless,
LocationName.ThousandHeartlessSecretAnsemReport1,
LocationName.ThousandHeartlessIceCream,
LocationName.ThousandHeartlessPicture,
LocationName.PosternGullWing,
LocationName.HeartlessManufactoryCosmicChain,
+ LocationName.ThousandHeartlessEventLocation,
+ ],
+ RegionName.DataDemyx: [
LocationName.DemyxDataAPBoost,
- ]
- onek_region = create_region(world, player, active_locations, RegionName.ThousandHeartless_Region,
- onek_region_locations)
- mushroom_region_locations = [
+ LocationName.DataDemyxEventLocation,
+ ],
+ RegionName.Mushroom13: [
LocationName.WinnersProof,
LocationName.ProofofPeace,
- ]
- mushroom_region = create_region(world, player, active_locations, RegionName.Mushroom13_Region,
- mushroom_region_locations)
- sephi_region_locations = [
+ LocationName.Mushroom13EventLocation,
+ ],
+ RegionName.Sephi: [
LocationName.SephirothBonus,
LocationName.SephirothFenrir,
- ]
- sephi_region = create_region(world, player, active_locations, RegionName.Sephi_Region,
- sephi_region_locations)
-
- cor_region_locations = [
+ LocationName.SephiEventLocation
+ ],
+ RegionName.CoR: [
+ LocationName.CoRDepthsAPBoost,
+ LocationName.CoRDepthsPowerCrystal,
+ LocationName.CoRDepthsFrostCrystal,
+ LocationName.CoRDepthsManifestIllusion,
+ LocationName.CoRDepthsAPBoost2,
+ LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap,
+ LocationName.CoRMineshaftLowerLevelAPBoost,
+ ],
+ RegionName.CorFirstFight: [
LocationName.CoRDepthsUpperLevelRemembranceGem,
LocationName.CoRMiningAreaSerenityGem,
LocationName.CoRMiningAreaAPBoost,
@@ -561,22 +557,23 @@ def create_regions(world, player: int, active_locations):
LocationName.CoRMiningAreaManifestIllusion,
LocationName.CoRMiningAreaSerenityGem2,
LocationName.CoRMiningAreaDarkRemembranceMap,
+ LocationName.CorFirstFightEventLocation,
+ ],
+ RegionName.CorSecondFight: [
LocationName.CoRMineshaftMidLevelPowerBoost,
LocationName.CoREngineChamberSerenityCrystal,
LocationName.CoREngineChamberRemembranceCrystal,
LocationName.CoREngineChamberAPBoost,
LocationName.CoREngineChamberManifestIllusion,
LocationName.CoRMineshaftUpperLevelMagicBoost,
- ]
- cor_region = create_region(world, player, active_locations, RegionName.CoR_Region,
- cor_region_locations)
- transport_region_locations = [
- LocationName.CoRMineshaftUpperLevelAPBoost,
+ LocationName.CorSecondFightEventLocation,
+ ],
+ RegionName.Transport: [
+ LocationName.CoRMineshaftUpperLevelAPBoost, # last chest
LocationName.TransporttoRemembrance,
- ]
- transport_region = create_region(world, player, active_locations, RegionName.Transport_Region,
- transport_region_locations)
- pl_region_locations = [
+ LocationName.TransportEventLocation,
+ ],
+ RegionName.Pl: [
LocationName.GorgeSavannahMap,
LocationName.GorgeDarkGem,
LocationName.GorgeMythrilStone,
@@ -604,31 +601,40 @@ def create_regions(world, player: int, active_locations):
LocationName.OasisAPBoost,
LocationName.CircleofLife,
LocationName.Hyenas1,
+
+ LocationName.GoofyHyenas1
+ ],
+ RegionName.Scar: [
LocationName.Scar,
LocationName.ScarFireElement,
LocationName.DonaldScar,
- LocationName.GoofyHyenas1,
-
- ]
- pl_region = create_region(world, player, active_locations, RegionName.Pl_Region,
- pl_region_locations)
- pl2_region_locations = [
+ LocationName.ScarEventLocation,
+ ],
+ RegionName.Pl2: [
LocationName.Hyenas2,
+ LocationName.GoofyHyenas2
+ ],
+ RegionName.GroundShaker: [
LocationName.Groundshaker,
LocationName.GroundshakerGetBonus,
+ LocationName.GroundShakerEventLocation,
+ ],
+ RegionName.DataSaix: [
LocationName.SaixDataDefenseBoost,
- LocationName.GoofyHyenas2,
- ]
- pl2_region = create_region(world, player, active_locations, RegionName.Pl2_Region,
- pl2_region_locations)
-
- stt_region_locations = [
+ LocationName.DataSaixEventLocation
+ ],
+ RegionName.Stt: [
LocationName.TwilightTownMap,
LocationName.MunnyPouchOlette,
LocationName.StationDusks,
LocationName.StationofSerenityPotion,
LocationName.StationofCallingPotion,
+ ],
+ RegionName.TwilightThorn: [
LocationName.TwilightThorn,
+ LocationName.TwilightThornEventLocation
+ ],
+ RegionName.Axel1: [
LocationName.Axel1,
LocationName.JunkChampionBelt,
LocationName.JunkMedal,
@@ -648,14 +654,18 @@ def create_regions(world, player: int, active_locations):
LocationName.NaminesSketches,
LocationName.MansionMap,
LocationName.MansionLibraryHiPotion,
+ LocationName.Axel1EventLocation
+ ],
+ RegionName.Axel2: [
LocationName.Axel2,
LocationName.MansionBasementCorridorHiPotion,
+ LocationName.Axel2EventLocation
+ ],
+ RegionName.DataRoxas: [
LocationName.RoxasDataMagicBoost,
- ]
- stt_region = create_region(world, player, active_locations, RegionName.STT_Region,
- stt_region_locations)
-
- tt_region_locations = [
+ LocationName.DataRoxasEventLocation
+ ],
+ RegionName.Tt: [
LocationName.OldMansionPotion,
LocationName.OldMansionMythrilShard,
LocationName.TheWoodsPotion,
@@ -682,18 +692,14 @@ def create_regions(world, player: int, active_locations):
LocationName.SorcerersLoftTowerMap,
LocationName.TowerWardrobeMythrilStone,
LocationName.StarSeeker,
- LocationName.ValorForm,
- ]
- tt_region = create_region(world, player, active_locations, RegionName.TT_Region,
- tt_region_locations)
- tt2_region_locations = [
+ LocationName.ValorForm
+ ],
+ RegionName.Tt2: [
LocationName.SeifersTrophy,
LocationName.Oathkeeper,
- LocationName.LimitForm,
- ]
- tt2_region = create_region(world, player, active_locations, RegionName.TT2_Region,
- tt2_region_locations)
- tt3_region_locations = [
+ LocationName.LimitForm
+ ],
+ RegionName.Tt3: [
LocationName.UndergroundConcourseMythrilGem,
LocationName.UndergroundConcourseAPBoost,
LocationName.UndergroundConcourseMythrilCrystal,
@@ -715,22 +721,19 @@ def create_regions(world, player: int, active_locations):
LocationName.MansionBasementCorridorUltimateRecipe,
LocationName.BetwixtandBetween,
LocationName.BetwixtandBetweenBondofFlame,
+ LocationName.DonaldMansionNobodies
+ ],
+ RegionName.DataAxel: [
LocationName.AxelDataMagicBoost,
- LocationName.DonaldMansionNobodies,
- ]
- tt3_region = create_region(world, player, active_locations, RegionName.TT3_Region,
- tt3_region_locations)
-
- twtnw_region_locations = [
+ LocationName.DataAxelEventLocation,
+ ],
+ RegionName.Twtnw: [
LocationName.FragmentCrossingMythrilStone,
LocationName.FragmentCrossingMythrilCrystal,
LocationName.FragmentCrossingAPBoost,
- LocationName.FragmentCrossingOrichalcum,
- ]
-
- twtnw_region = create_region(world, player, active_locations, RegionName.Twtnw_Region,
- twtnw_region_locations)
- twtnw_postroxas_region_locations = [
+ LocationName.FragmentCrossingOrichalcum
+ ],
+ RegionName.Roxas: [
LocationName.Roxas,
LocationName.RoxasGetBonus,
LocationName.RoxasSecretAnsemReport8,
@@ -743,11 +746,9 @@ def create_regions(world, player: int, active_locations):
LocationName.NothingsCallMythrilGem,
LocationName.NothingsCallOrichalcum,
LocationName.TwilightsViewCosmicBelt,
-
- ]
- twtnw_postroxas_region = create_region(world, player, active_locations, RegionName.Twtnw_PostRoxas,
- twtnw_postroxas_region_locations)
- twtnw_postxigbar_region_locations = [
+ LocationName.RoxasEventLocation
+ ],
+ RegionName.Xigbar: [
LocationName.XigbarBonus,
LocationName.XigbarSecretAnsemReport3,
LocationName.NaughtsSkywayMythrilGem,
@@ -755,80 +756,100 @@ def create_regions(world, player: int, active_locations):
LocationName.NaughtsSkywayMythrilCrystal,
LocationName.Oblivion,
LocationName.CastleThatNeverWasMap,
+ LocationName.XigbarEventLocation,
+ ],
+ RegionName.Luxord: [
LocationName.Luxord,
LocationName.LuxordGetBonus,
LocationName.LuxordSecretAnsemReport9,
- ]
- twtnw_postxigbar_region = create_region(world, player, active_locations, RegionName.Twtnw_PostXigbar,
- twtnw_postxigbar_region_locations)
- twtnw2_region_locations = [
+ LocationName.LuxordEventLocation,
+ ],
+ RegionName.Saix: [
LocationName.SaixBonus,
LocationName.SaixSecretAnsemReport12,
+ LocationName.SaixEventLocation,
+ ],
+ RegionName.Twtnw2: [
LocationName.PreXemnas1SecretAnsemReport11,
LocationName.RuinandCreationsPassageMythrilStone,
LocationName.RuinandCreationsPassageAPBoost,
LocationName.RuinandCreationsPassageMythrilCrystal,
- LocationName.RuinandCreationsPassageOrichalcum,
+ LocationName.RuinandCreationsPassageOrichalcum
+ ],
+ RegionName.Xemnas: [
LocationName.Xemnas1,
LocationName.Xemnas1GetBonus,
LocationName.Xemnas1SecretAnsemReport13,
- LocationName.FinalXemnas,
+ LocationName.XemnasEventLocation
+
+ ],
+ RegionName.ArmoredXemnas: [
+ LocationName.ArmoredXemnasEventLocation
+ ],
+ RegionName.ArmoredXemnas2: [
+ LocationName.ArmoredXemnas2EventLocation
+ ],
+ RegionName.FinalXemnas: [
+ LocationName.FinalXemnasEventLocation
+ ],
+ RegionName.DataXemnas: [
LocationName.XemnasDataPowerBoost,
- ]
- twtnw2_region = create_region(world, player, active_locations, RegionName.Twtnw2_Region,
- twtnw2_region_locations)
+ LocationName.DataXemnasEventLocation
+ ],
+ RegionName.AtlanticaSongOne: [
+ LocationName.UnderseaKingdomMap
+ ],
+ RegionName.AtlanticaSongTwo: [
- valor_region_locations = [
+ ],
+ RegionName.AtlanticaSongThree: [
+ LocationName.MysteriousAbyss
+ ],
+ RegionName.AtlanticaSongFour: [
+ LocationName.MusicalBlizzardElement,
+ LocationName.MusicalOrichalcumPlus
+ ],
+ RegionName.Valor: [
LocationName.Valorlvl2,
LocationName.Valorlvl3,
LocationName.Valorlvl4,
LocationName.Valorlvl5,
LocationName.Valorlvl6,
- LocationName.Valorlvl7,
- ]
- valor_region = create_region(world, player, active_locations, RegionName.Valor_Region,
- valor_region_locations)
- wisdom_region_locations = [
+ LocationName.Valorlvl7
+ ],
+ RegionName.Wisdom: [
LocationName.Wisdomlvl2,
LocationName.Wisdomlvl3,
LocationName.Wisdomlvl4,
LocationName.Wisdomlvl5,
LocationName.Wisdomlvl6,
- LocationName.Wisdomlvl7,
- ]
- wisdom_region = create_region(world, player, active_locations, RegionName.Wisdom_Region,
- wisdom_region_locations)
- limit_region_locations = [
+ LocationName.Wisdomlvl7
+ ],
+ RegionName.Limit: [
LocationName.Limitlvl2,
LocationName.Limitlvl3,
LocationName.Limitlvl4,
LocationName.Limitlvl5,
LocationName.Limitlvl6,
- LocationName.Limitlvl7,
- ]
- limit_region = create_region(world, player, active_locations, RegionName.Limit_Region,
- limit_region_locations)
- master_region_locations = [
+ LocationName.Limitlvl7
+ ],
+ RegionName.Master: [
LocationName.Masterlvl2,
LocationName.Masterlvl3,
LocationName.Masterlvl4,
LocationName.Masterlvl5,
LocationName.Masterlvl6,
- LocationName.Masterlvl7,
- ]
- master_region = create_region(world, player, active_locations, RegionName.Master_Region,
- master_region_locations)
- final_region_locations = [
+ LocationName.Masterlvl7
+ ],
+ RegionName.Final: [
LocationName.Finallvl2,
LocationName.Finallvl3,
LocationName.Finallvl4,
LocationName.Finallvl5,
LocationName.Finallvl6,
- LocationName.Finallvl7,
- ]
- final_region = create_region(world, player, active_locations, RegionName.Final_Region,
- final_region_locations)
- keyblade_region_locations = [
+ LocationName.Finallvl7
+ ],
+ RegionName.Keyblade: [
LocationName.FAKESlot,
LocationName.DetectionSaberSlot,
LocationName.EdgeofUltimaSlot,
@@ -887,356 +908,256 @@ def create_regions(world, player: int, active_locations):
LocationName.NobodyGuard,
LocationName.OgreShield,
LocationName.SaveTheKing2,
- LocationName.UltimateMushroom,
- ]
- keyblade_region = create_region(world, player, active_locations, RegionName.Keyblade_Region,
- keyblade_region_locations)
+ LocationName.UltimateMushroom
+ ],
+}
+level_region_list = [
+ RegionName.LevelsVS1,
+ RegionName.LevelsVS3,
+ RegionName.LevelsVS6,
+ RegionName.LevelsVS9,
+ RegionName.LevelsVS12,
+ RegionName.LevelsVS15,
+ RegionName.LevelsVS18,
+ RegionName.LevelsVS21,
+ RegionName.LevelsVS24,
+ RegionName.LevelsVS26,
+]
+
- world.regions += [
- lod_Region,
- lod2_Region,
- ag_region,
- ag2_region,
- lexaeus_region,
- dc_region,
- tr_region,
- terra_region,
- marluxia_region,
- hundred_acre1_region,
- hundred_acre2_region,
- hundred_acre3_region,
- hundred_acre4_region,
- hundred_acre5_region,
- hundred_acre6_region,
- pr_region,
- pr2_region,
- gr2_region,
- oc_region,
- oc2_region,
- oc2_pain_and_panic_cup,
- oc2_titan_cup,
- oc2_cerberus_cup,
- oc2_gof_cup,
- zexion_region,
- bc_region,
- bc2_region,
- xaldin_region,
- sp_region,
- sp2_region,
- mcp_region,
- larxene_region,
- ht_region,
- ht2_region,
- vexen_region,
- hb_region,
- hb2_region,
- onek_region,
- mushroom_region,
- sephi_region,
- cor_region,
- transport_region,
- pl_region,
- pl2_region,
- stt_region,
- tt_region,
- tt2_region,
- tt3_region,
- twtnw_region,
- twtnw_postroxas_region,
- twtnw_postxigbar_region,
- twtnw2_region,
- goa_region,
- menu_region,
- valor_region,
- wisdom_region,
- limit_region,
- master_region,
- final_region,
- keyblade_region,
- ]
+def create_regions(self):
# Level region depends on level depth.
# for every 5 levels there should be +3 visit locking
- levelVL1 = []
- levelVL3 = []
- levelVL6 = []
- levelVL9 = []
- levelVL12 = []
- levelVL15 = []
- levelVL18 = []
- levelVL21 = []
- levelVL24 = []
- levelVL26 = []
# level 50
- if world.LevelDepth[player] == "level_50":
- levelVL1 = [LocationName.Lvl2, LocationName.Lvl4, LocationName.Lvl7, LocationName.Lvl9, LocationName.Lvl10]
- levelVL3 = [LocationName.Lvl12, LocationName.Lvl14, LocationName.Lvl15, LocationName.Lvl17,
- LocationName.Lvl20, ]
- levelVL6 = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28, LocationName.Lvl30]
- levelVL9 = [LocationName.Lvl32, LocationName.Lvl34, LocationName.Lvl36, LocationName.Lvl39, LocationName.Lvl41]
- levelVL12 = [LocationName.Lvl44, LocationName.Lvl46, LocationName.Lvl48]
- levelVL15 = [LocationName.Lvl50]
+ multiworld = self.multiworld
+ player = self.player
+ active_locations = self.location_name_to_id
+
+ for level_region_name in level_region_list:
+ KH2REGIONS[level_region_name] = []
+ if self.options.LevelDepth == "level_50":
+ KH2REGIONS[RegionName.LevelsVS1] = [LocationName.Lvl2, LocationName.Lvl4, LocationName.Lvl7, LocationName.Lvl9,
+ LocationName.Lvl10]
+ KH2REGIONS[RegionName.LevelsVS3] = [LocationName.Lvl12, LocationName.Lvl14, LocationName.Lvl15,
+ LocationName.Lvl17,
+ LocationName.Lvl20]
+ KH2REGIONS[RegionName.LevelsVS6] = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28,
+ LocationName.Lvl30]
+ KH2REGIONS[RegionName.LevelsVS9] = [LocationName.Lvl32, LocationName.Lvl34, LocationName.Lvl36,
+ LocationName.Lvl39, LocationName.Lvl41]
+ KH2REGIONS[RegionName.LevelsVS12] = [LocationName.Lvl44, LocationName.Lvl46, LocationName.Lvl48]
+ KH2REGIONS[RegionName.LevelsVS15] = [LocationName.Lvl50]
+
# level 99
- elif world.LevelDepth[player] == "level_99":
- levelVL1 = [LocationName.Lvl7, LocationName.Lvl9, ]
- levelVL3 = [LocationName.Lvl12, LocationName.Lvl15, LocationName.Lvl17, LocationName.Lvl20]
- levelVL6 = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28]
- levelVL9 = [LocationName.Lvl31, LocationName.Lvl33, LocationName.Lvl36, LocationName.Lvl39]
- levelVL12 = [LocationName.Lvl41, LocationName.Lvl44, LocationName.Lvl47, LocationName.Lvl49]
- levelVL15 = [LocationName.Lvl53, LocationName.Lvl59]
- levelVL18 = [LocationName.Lvl65]
- levelVL21 = [LocationName.Lvl73]
- levelVL24 = [LocationName.Lvl85]
- levelVL26 = [LocationName.Lvl99]
+ elif self.options.LevelDepth == "level_99":
+ KH2REGIONS[RegionName.LevelsVS1] = [LocationName.Lvl7, LocationName.Lvl9]
+ KH2REGIONS[RegionName.LevelsVS3] = [LocationName.Lvl12, LocationName.Lvl15, LocationName.Lvl17,
+ LocationName.Lvl20]
+ KH2REGIONS[RegionName.LevelsVS6] = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28]
+ KH2REGIONS[RegionName.LevelsVS9] = [LocationName.Lvl31, LocationName.Lvl33, LocationName.Lvl36,
+ LocationName.Lvl39]
+ KH2REGIONS[RegionName.LevelsVS12] = [LocationName.Lvl41, LocationName.Lvl44, LocationName.Lvl47,
+ LocationName.Lvl49]
+ KH2REGIONS[RegionName.LevelsVS15] = [LocationName.Lvl53, LocationName.Lvl59]
+ KH2REGIONS[RegionName.LevelsVS18] = [LocationName.Lvl65]
+ KH2REGIONS[RegionName.LevelsVS21] = [LocationName.Lvl73]
+ KH2REGIONS[RegionName.LevelsVS24] = [LocationName.Lvl85]
+ KH2REGIONS[RegionName.LevelsVS26] = [LocationName.Lvl99]
# level sanity
# has to be [] instead of {} for in
- elif world.LevelDepth[player] in ["level_50_sanity", "level_99_sanity"]:
- levelVL1 = [LocationName.Lvl2, LocationName.Lvl3, LocationName.Lvl4, LocationName.Lvl5, LocationName.Lvl6,
- LocationName.Lvl7, LocationName.Lvl8, LocationName.Lvl9, LocationName.Lvl10]
- levelVL3 = [LocationName.Lvl11, LocationName.Lvl12, LocationName.Lvl13, LocationName.Lvl14, LocationName.Lvl15,
- LocationName.Lvl16, LocationName.Lvl17, LocationName.Lvl18, LocationName.Lvl19, LocationName.Lvl20]
- levelVL6 = [LocationName.Lvl21, LocationName.Lvl22, LocationName.Lvl23, LocationName.Lvl24, LocationName.Lvl25,
- LocationName.Lvl26, LocationName.Lvl27, LocationName.Lvl28, LocationName.Lvl29, LocationName.Lvl30]
- levelVL9 = [LocationName.Lvl31, LocationName.Lvl32, LocationName.Lvl33, LocationName.Lvl34, LocationName.Lvl35,
- LocationName.Lvl36, LocationName.Lvl37, LocationName.Lvl38, LocationName.Lvl39, LocationName.Lvl40]
- levelVL12 = [LocationName.Lvl41, LocationName.Lvl42, LocationName.Lvl43, LocationName.Lvl44, LocationName.Lvl45,
- LocationName.Lvl46, LocationName.Lvl47, LocationName.Lvl48, LocationName.Lvl49, LocationName.Lvl50]
+ elif self.options.LevelDepth in ["level_50_sanity", "level_99_sanity"]:
+ KH2REGIONS[RegionName.LevelsVS1] = [LocationName.Lvl2, LocationName.Lvl3, LocationName.Lvl4, LocationName.Lvl5,
+ LocationName.Lvl6,
+ LocationName.Lvl7, LocationName.Lvl8, LocationName.Lvl9, LocationName.Lvl10]
+ KH2REGIONS[RegionName.LevelsVS3] = [LocationName.Lvl11, LocationName.Lvl12, LocationName.Lvl13,
+ LocationName.Lvl14, LocationName.Lvl15,
+ LocationName.Lvl16, LocationName.Lvl17, LocationName.Lvl18,
+ LocationName.Lvl19, LocationName.Lvl20]
+ KH2REGIONS[RegionName.LevelsVS6] = [LocationName.Lvl21, LocationName.Lvl22, LocationName.Lvl23,
+ LocationName.Lvl24, LocationName.Lvl25,
+ LocationName.Lvl26, LocationName.Lvl27, LocationName.Lvl28,
+ LocationName.Lvl29, LocationName.Lvl30]
+ KH2REGIONS[RegionName.LevelsVS9] = [LocationName.Lvl31, LocationName.Lvl32, LocationName.Lvl33,
+ LocationName.Lvl34, LocationName.Lvl35,
+ LocationName.Lvl36, LocationName.Lvl37, LocationName.Lvl38,
+ LocationName.Lvl39, LocationName.Lvl40]
+ KH2REGIONS[RegionName.LevelsVS12] = [LocationName.Lvl41, LocationName.Lvl42, LocationName.Lvl43,
+ LocationName.Lvl44, LocationName.Lvl45,
+ LocationName.Lvl46, LocationName.Lvl47, LocationName.Lvl48,
+ LocationName.Lvl49, LocationName.Lvl50]
# level 99 sanity
- if world.LevelDepth[player] == "level_99_sanity":
- levelVL15 = [LocationName.Lvl51, LocationName.Lvl52, LocationName.Lvl53, LocationName.Lvl54,
- LocationName.Lvl55, LocationName.Lvl56, LocationName.Lvl57, LocationName.Lvl58,
- LocationName.Lvl59, LocationName.Lvl60]
- levelVL18 = [LocationName.Lvl61, LocationName.Lvl62, LocationName.Lvl63, LocationName.Lvl64,
- LocationName.Lvl65, LocationName.Lvl66, LocationName.Lvl67, LocationName.Lvl68,
- LocationName.Lvl69, LocationName.Lvl70]
- levelVL21 = [LocationName.Lvl71, LocationName.Lvl72, LocationName.Lvl73, LocationName.Lvl74,
- LocationName.Lvl75, LocationName.Lvl76, LocationName.Lvl77, LocationName.Lvl78,
- LocationName.Lvl79, LocationName.Lvl80]
- levelVL24 = [LocationName.Lvl81, LocationName.Lvl82, LocationName.Lvl83, LocationName.Lvl84,
- LocationName.Lvl85, LocationName.Lvl86, LocationName.Lvl87, LocationName.Lvl88,
- LocationName.Lvl89, LocationName.Lvl90]
- levelVL26 = [LocationName.Lvl91, LocationName.Lvl92, LocationName.Lvl93, LocationName.Lvl94,
- LocationName.Lvl95, LocationName.Lvl96, LocationName.Lvl97, LocationName.Lvl98,
- LocationName.Lvl99]
+ if self.options.LevelDepth == "level_99_sanity":
+ KH2REGIONS[RegionName.LevelsVS15] = [LocationName.Lvl51, LocationName.Lvl52, LocationName.Lvl53,
+ LocationName.Lvl54,
+ LocationName.Lvl55, LocationName.Lvl56, LocationName.Lvl57,
+ LocationName.Lvl58,
+ LocationName.Lvl59, LocationName.Lvl60]
+ KH2REGIONS[RegionName.LevelsVS18] = [LocationName.Lvl61, LocationName.Lvl62, LocationName.Lvl63,
+ LocationName.Lvl64,
+ LocationName.Lvl65, LocationName.Lvl66, LocationName.Lvl67,
+ LocationName.Lvl68,
+ LocationName.Lvl69, LocationName.Lvl70]
+ KH2REGIONS[RegionName.LevelsVS21] = [LocationName.Lvl71, LocationName.Lvl72, LocationName.Lvl73,
+ LocationName.Lvl74,
+ LocationName.Lvl75, LocationName.Lvl76, LocationName.Lvl77,
+ LocationName.Lvl78,
+ LocationName.Lvl79, LocationName.Lvl80]
+ KH2REGIONS[RegionName.LevelsVS24] = [LocationName.Lvl81, LocationName.Lvl82, LocationName.Lvl83,
+ LocationName.Lvl84,
+ LocationName.Lvl85, LocationName.Lvl86, LocationName.Lvl87,
+ LocationName.Lvl88,
+ LocationName.Lvl89, LocationName.Lvl90]
+ KH2REGIONS[RegionName.LevelsVS26] = [LocationName.Lvl91, LocationName.Lvl92, LocationName.Lvl93,
+ LocationName.Lvl94,
+ LocationName.Lvl95, LocationName.Lvl96, LocationName.Lvl97,
+ LocationName.Lvl98, LocationName.Lvl99]
+ KH2REGIONS[RegionName.Summon] = []
+ if self.options.SummonLevelLocationToggle:
+ KH2REGIONS[RegionName.Summon] = [LocationName.Summonlvl2,
+ LocationName.Summonlvl3,
+ LocationName.Summonlvl4,
+ LocationName.Summonlvl5,
+ LocationName.Summonlvl6,
+ LocationName.Summonlvl7]
+ multiworld.regions += [create_region(multiworld, player, active_locations, region, locations) for region, locations in
+ KH2REGIONS.items()]
+ # fill the event locations with events
- level_regionVL1 = create_region(world, player, active_locations, RegionName.LevelsVS1,
- levelVL1)
- level_regionVL3 = create_region(world, player, active_locations, RegionName.LevelsVS3,
- levelVL3)
- level_regionVL6 = create_region(world, player, active_locations, RegionName.LevelsVS6,
- levelVL6)
- level_regionVL9 = create_region(world, player, active_locations, RegionName.LevelsVS9,
- levelVL9)
- level_regionVL12 = create_region(world, player, active_locations, RegionName.LevelsVS12,
- levelVL12)
- level_regionVL15 = create_region(world, player, active_locations, RegionName.LevelsVS15,
- levelVL15)
- level_regionVL18 = create_region(world, player, active_locations, RegionName.LevelsVS18,
- levelVL18)
- level_regionVL21 = create_region(world, player, active_locations, RegionName.LevelsVS21,
- levelVL21)
- level_regionVL24 = create_region(world, player, active_locations, RegionName.LevelsVS24,
- levelVL24)
- level_regionVL26 = create_region(world, player, active_locations, RegionName.LevelsVS26,
- levelVL26)
- world.regions += [level_regionVL1, level_regionVL3, level_regionVL6, level_regionVL9, level_regionVL12,
- level_regionVL15, level_regionVL18, level_regionVL21, level_regionVL24, level_regionVL26]
+ for location, item in Locations.event_location_to_item.items():
+ multiworld.get_location(location, player).place_locked_item(
+ multiworld.worlds[player].create_event_item(item))
-def connect_regions(world: MultiWorld, player: int):
+def connect_regions(self):
+ multiworld = self.multiworld
+ player = self.player
# connecting every first visit to the GoA
+ KH2RegionConnections: typing.Dict[str, typing.Set[str]] = {
+ "Menu": {RegionName.GoA},
+ RegionName.GoA: {RegionName.Sp, RegionName.Pr, RegionName.Tt, RegionName.Oc, RegionName.Ht,
+ RegionName.LoD,
+ RegionName.Twtnw, RegionName.Bc, RegionName.Ag, RegionName.Pl, RegionName.Hb,
+ RegionName.Dc, RegionName.Stt,
+ RegionName.Ha1, RegionName.Keyblade, RegionName.LevelsVS1,
+ RegionName.Valor, RegionName.Wisdom, RegionName.Limit, RegionName.Master,
+ RegionName.Final, RegionName.Summon, RegionName.AtlanticaSongOne},
+ RegionName.LoD: {RegionName.ShanYu},
+ RegionName.ShanYu: {RegionName.LoD2},
+ RegionName.LoD2: {RegionName.AnsemRiku},
+ RegionName.AnsemRiku: {RegionName.StormRider},
+ RegionName.StormRider: {RegionName.DataXigbar},
+ RegionName.Ag: {RegionName.TwinLords},
+ RegionName.TwinLords: {RegionName.Ag2},
+ RegionName.Ag2: {RegionName.GenieJafar},
+ RegionName.GenieJafar: {RegionName.DataLexaeus},
+ RegionName.Dc: {RegionName.Tr},
+ RegionName.Tr: {RegionName.OldPete},
+ RegionName.OldPete: {RegionName.FuturePete},
+ RegionName.FuturePete: {RegionName.Terra, RegionName.DataMarluxia},
+ RegionName.Ha1: {RegionName.Ha2},
+ RegionName.Ha2: {RegionName.Ha3},
+ RegionName.Ha3: {RegionName.Ha4},
+ RegionName.Ha4: {RegionName.Ha5},
+ RegionName.Ha5: {RegionName.Ha6},
+ RegionName.Pr: {RegionName.Barbosa},
+ RegionName.Barbosa: {RegionName.Pr2},
+ RegionName.Pr2: {RegionName.GrimReaper1},
+ RegionName.GrimReaper1: {RegionName.GrimReaper2},
+ RegionName.GrimReaper2: {RegionName.DataLuxord},
+ RegionName.Oc: {RegionName.Cerberus},
+ RegionName.Cerberus: {RegionName.OlympusPete},
+ RegionName.OlympusPete: {RegionName.Hydra},
+ RegionName.Hydra: {RegionName.OcPainAndPanicCup, RegionName.OcCerberusCup, RegionName.Oc2},
+ RegionName.Oc2: {RegionName.Hades},
+ RegionName.Hades: {RegionName.Oc2TitanCup, RegionName.Oc2GofCup, RegionName.DataZexion},
+ RegionName.Oc2GofCup: {RegionName.HadesCups},
+ RegionName.Bc: {RegionName.Thresholder},
+ RegionName.Thresholder: {RegionName.Beast},
+ RegionName.Beast: {RegionName.DarkThorn},
+ RegionName.DarkThorn: {RegionName.Bc2},
+ RegionName.Bc2: {RegionName.Xaldin},
+ RegionName.Xaldin: {RegionName.DataXaldin},
+ RegionName.Sp: {RegionName.HostileProgram},
+ RegionName.HostileProgram: {RegionName.Sp2},
+ RegionName.Sp2: {RegionName.Mcp},
+ RegionName.Mcp: {RegionName.DataLarxene},
+ RegionName.Ht: {RegionName.PrisonKeeper},
+ RegionName.PrisonKeeper: {RegionName.OogieBoogie},
+ RegionName.OogieBoogie: {RegionName.Ht2},
+ RegionName.Ht2: {RegionName.Experiment},
+ RegionName.Experiment: {RegionName.DataVexen},
+ RegionName.Hb: {RegionName.Hb2},
+ RegionName.Hb2: {RegionName.CoR, RegionName.HBDemyx},
+ RegionName.HBDemyx: {RegionName.ThousandHeartless},
+ RegionName.ThousandHeartless: {RegionName.Mushroom13, RegionName.DataDemyx, RegionName.Sephi},
+ RegionName.CoR: {RegionName.CorFirstFight},
+ RegionName.CorFirstFight: {RegionName.CorSecondFight},
+ RegionName.CorSecondFight: {RegionName.Transport},
+ RegionName.Pl: {RegionName.Scar},
+ RegionName.Scar: {RegionName.Pl2},
+ RegionName.Pl2: {RegionName.GroundShaker},
+ RegionName.GroundShaker: {RegionName.DataSaix},
+ RegionName.Stt: {RegionName.TwilightThorn},
+ RegionName.TwilightThorn: {RegionName.Axel1},
+ RegionName.Axel1: {RegionName.Axel2},
+ RegionName.Axel2: {RegionName.DataRoxas},
+ RegionName.Tt: {RegionName.Tt2},
+ RegionName.Tt2: {RegionName.Tt3},
+ RegionName.Tt3: {RegionName.DataAxel},
+ RegionName.Twtnw: {RegionName.Roxas},
+ RegionName.Roxas: {RegionName.Xigbar},
+ RegionName.Xigbar: {RegionName.Luxord},
+ RegionName.Luxord: {RegionName.Saix},
+ RegionName.Saix: {RegionName.Twtnw2},
+ RegionName.Twtnw2: {RegionName.Xemnas},
+ RegionName.Xemnas: {RegionName.ArmoredXemnas, RegionName.DataXemnas},
+ RegionName.ArmoredXemnas: {RegionName.ArmoredXemnas2},
+ RegionName.ArmoredXemnas2: {RegionName.FinalXemnas},
+ RegionName.LevelsVS1: {RegionName.LevelsVS3},
+ RegionName.LevelsVS3: {RegionName.LevelsVS6},
+ RegionName.LevelsVS6: {RegionName.LevelsVS9},
+ RegionName.LevelsVS9: {RegionName.LevelsVS12},
+ RegionName.LevelsVS12: {RegionName.LevelsVS15},
+ RegionName.LevelsVS15: {RegionName.LevelsVS18},
+ RegionName.LevelsVS18: {RegionName.LevelsVS21},
+ RegionName.LevelsVS21: {RegionName.LevelsVS24},
+ RegionName.LevelsVS24: {RegionName.LevelsVS26},
+ RegionName.AtlanticaSongOne: {RegionName.AtlanticaSongTwo},
+ RegionName.AtlanticaSongTwo: {RegionName.AtlanticaSongThree},
+ RegionName.AtlanticaSongThree: {RegionName.AtlanticaSongFour},
+ }
- names: typing.Dict[str, int] = {}
-
- connect(world, player, names, "Menu", RegionName.Keyblade_Region)
- connect(world, player, names, "Menu", RegionName.GoA_Region)
-
- connect(world, player, names, RegionName.GoA_Region, RegionName.LoD_Region,
- lambda state: state.kh_lod_unlocked(player, 1))
- connect(world, player, names, RegionName.LoD_Region, RegionName.LoD2_Region,
- lambda state: state.kh_lod_unlocked(player, 2))
-
- connect(world, player, names, RegionName.GoA_Region, RegionName.Oc_Region,
- lambda state: state.kh_oc_unlocked(player, 1))
- connect(world, player, names, RegionName.Oc_Region, RegionName.Oc2_Region,
- lambda state: state.kh_oc_unlocked(player, 2))
- connect(world, player, names, RegionName.Oc2_Region, RegionName.Zexion_Region,
- lambda state: state.kh_datazexion(player))
-
- connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_pain_and_panic_Region,
- lambda state: state.kh_painandpanic(player))
- connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_cerberus_Region,
- lambda state: state.kh_cerberuscup(player))
- connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_titan_Region,
- lambda state: state.kh_titan(player))
- connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_gof_Region,
- lambda state: state.kh_gof(player))
-
- connect(world, player, names, RegionName.GoA_Region, RegionName.Ag_Region,
- lambda state: state.kh_ag_unlocked(player, 1))
- connect(world, player, names, RegionName.Ag_Region, RegionName.Ag2_Region,
- lambda state: state.kh_ag_unlocked(player, 2)
- and (state.has(ItemName.FireElement, player)
- and state.has(ItemName.BlizzardElement, player)
- and state.has(ItemName.ThunderElement, player)))
- connect(world, player, names, RegionName.Ag2_Region, RegionName.Lexaeus_Region,
- lambda state: state.kh_datalexaeus(player))
-
- connect(world, player, names, RegionName.GoA_Region, RegionName.Dc_Region,
- lambda state: state.kh_dc_unlocked(player, 1))
- connect(world, player, names, RegionName.Dc_Region, RegionName.Tr_Region,
- lambda state: state.kh_dc_unlocked(player, 2))
- connect(world, player, names, RegionName.Tr_Region, RegionName.Marluxia_Region,
- lambda state: state.kh_datamarluxia(player))
- connect(world, player, names, RegionName.Tr_Region, RegionName.Terra_Region, lambda state: state.kh_terra(player))
-
- connect(world, player, names, RegionName.GoA_Region, RegionName.Pr_Region,
- lambda state: state.kh_pr_unlocked(player, 1))
- connect(world, player, names, RegionName.Pr_Region, RegionName.Pr2_Region,
- lambda state: state.kh_pr_unlocked(player, 2))
- connect(world, player, names, RegionName.Pr2_Region, RegionName.Gr2_Region,
- lambda state: state.kh_gr2(player))
-
- connect(world, player, names, RegionName.GoA_Region, RegionName.Bc_Region,
- lambda state: state.kh_bc_unlocked(player, 1))
- connect(world, player, names, RegionName.Bc_Region, RegionName.Bc2_Region,
- lambda state: state.kh_bc_unlocked(player, 2))
- connect(world, player, names, RegionName.Bc2_Region, RegionName.Xaldin_Region,
- lambda state: state.kh_xaldin(player))
-
- connect(world, player, names, RegionName.GoA_Region, RegionName.Sp_Region,
- lambda state: state.kh_sp_unlocked(player, 1))
- connect(world, player, names, RegionName.Sp_Region, RegionName.Sp2_Region,
- lambda state: state.kh_sp_unlocked(player, 2))
- connect(world, player, names, RegionName.Sp2_Region, RegionName.Mcp_Region,
- lambda state: state.kh_mcp(player))
- connect(world, player, names, RegionName.Mcp_Region, RegionName.Larxene_Region,
- lambda state: state.kh_datalarxene(player))
-
- connect(world, player, names, RegionName.GoA_Region, RegionName.Ht_Region,
- lambda state: state.kh_ht_unlocked(player, 1))
- connect(world, player, names, RegionName.Ht_Region, RegionName.Ht2_Region,
- lambda state: state.kh_ht_unlocked(player, 2))
- connect(world, player, names, RegionName.Ht2_Region, RegionName.Vexen_Region,
- lambda state: state.kh_datavexen(player))
-
- connect(world, player, names, RegionName.GoA_Region, RegionName.Hb_Region,
- lambda state: state.kh_hb_unlocked(player, 1))
- connect(world, player, names, RegionName.Hb_Region, RegionName.Hb2_Region,
- lambda state: state.kh_hb_unlocked(player, 2))
- connect(world, player, names, RegionName.Hb2_Region, RegionName.ThousandHeartless_Region,
- lambda state: state.kh_onek(player))
- connect(world, player, names, RegionName.ThousandHeartless_Region, RegionName.Mushroom13_Region,
- lambda state: state.has(ItemName.ProofofPeace, player))
- connect(world, player, names, RegionName.ThousandHeartless_Region, RegionName.Sephi_Region,
- lambda state: state.kh_sephi(player))
-
- connect(world, player, names, RegionName.Hb2_Region, RegionName.CoR_Region, lambda state: state.kh_cor(player))
- connect(world, player, names, RegionName.CoR_Region, RegionName.Transport_Region, lambda state:
- state.has(ItemName.HighJump, player, 3)
- and state.has(ItemName.AerialDodge, player, 3)
- and state.has(ItemName.Glide, player, 3))
-
- connect(world, player, names, RegionName.GoA_Region, RegionName.Pl_Region,
- lambda state: state.kh_pl_unlocked(player, 1))
- connect(world, player, names, RegionName.Pl_Region, RegionName.Pl2_Region,
- lambda state: state.kh_pl_unlocked(player, 2) and (
- state.has(ItemName.BerserkCharge, player) or state.kh_reflect(player)))
-
- connect(world, player, names, RegionName.GoA_Region, RegionName.STT_Region,
- lambda state: state.kh_stt_unlocked(player, 1))
-
- connect(world, player, names, RegionName.GoA_Region, RegionName.TT_Region,
- lambda state: state.kh_tt_unlocked(player, 1))
- connect(world, player, names, RegionName.TT_Region, RegionName.TT2_Region,
- lambda state: state.kh_tt_unlocked(player, 2))
- connect(world, player, names, RegionName.TT2_Region, RegionName.TT3_Region,
- lambda state: state.kh_tt_unlocked(player, 3))
-
- connect(world, player, names, RegionName.GoA_Region, RegionName.Twtnw_Region,
- lambda state: state.kh_twtnw_unlocked(player, 0))
- connect(world, player, names, RegionName.Twtnw_Region, RegionName.Twtnw_PostRoxas,
- lambda state: state.kh_roxastools(player))
- connect(world, player, names, RegionName.Twtnw_PostRoxas, RegionName.Twtnw_PostXigbar,
- lambda state: state.kh_basetools(player) and (state.kh_donaldlimit(player) or (
- state.has(ItemName.FinalForm, player) and state.has(ItemName.FireElement, player))))
- connect(world, player, names, RegionName.Twtnw_PostRoxas, RegionName.Twtnw2_Region,
- lambda state: state.kh_twtnw_unlocked(player, 1))
-
- hundredacrevisits = {RegionName.HundredAcre1_Region: 0, RegionName.HundredAcre2_Region: 1,
- RegionName.HundredAcre3_Region: 2,
- RegionName.HundredAcre4_Region: 3, RegionName.HundredAcre5_Region: 4,
- RegionName.HundredAcre6_Region: 5}
- for visit, tornpage in hundredacrevisits.items():
- connect(world, player, names, RegionName.GoA_Region, visit,
- lambda state: (state.has(ItemName.TornPages, player, tornpage)))
-
- connect(world, player, names, RegionName.GoA_Region, RegionName.LevelsVS1,
- lambda state: state.kh_visit_locking_amount(player, 1))
- connect(world, player, names, RegionName.LevelsVS1, RegionName.LevelsVS3,
- lambda state: state.kh_visit_locking_amount(player, 3))
- connect(world, player, names, RegionName.LevelsVS3, RegionName.LevelsVS6,
- lambda state: state.kh_visit_locking_amount(player, 6))
- connect(world, player, names, RegionName.LevelsVS6, RegionName.LevelsVS9,
- lambda state: state.kh_visit_locking_amount(player, 9))
- connect(world, player, names, RegionName.LevelsVS9, RegionName.LevelsVS12,
- lambda state: state.kh_visit_locking_amount(player, 12))
- connect(world, player, names, RegionName.LevelsVS12, RegionName.LevelsVS15,
- lambda state: state.kh_visit_locking_amount(player, 15))
- connect(world, player, names, RegionName.LevelsVS15, RegionName.LevelsVS18,
- lambda state: state.kh_visit_locking_amount(player, 18))
- connect(world, player, names, RegionName.LevelsVS18, RegionName.LevelsVS21,
- lambda state: state.kh_visit_locking_amount(player, 21))
- connect(world, player, names, RegionName.LevelsVS21, RegionName.LevelsVS24,
- lambda state: state.kh_visit_locking_amount(player, 24))
- connect(world, player, names, RegionName.LevelsVS24, RegionName.LevelsVS26,
- lambda state: state.kh_visit_locking_amount(player, 25)) # 25 because of goa twtnw bugs with visit locking.
-
- for region in RegionTable["ValorRegion"]:
- connect(world, player, names, region, RegionName.Valor_Region,
- lambda state: state.has(ItemName.ValorForm, player))
- for region in RegionTable["WisdomRegion"]:
- connect(world, player, names, region, RegionName.Wisdom_Region,
- lambda state: state.has(ItemName.WisdomForm, player))
- for region in RegionTable["LimitRegion"]:
- connect(world, player, names, region, RegionName.Limit_Region,
- lambda state: state.has(ItemName.LimitForm, player))
- for region in RegionTable["MasterRegion"]:
- connect(world, player, names, region, RegionName.Master_Region,
- lambda state: state.has(ItemName.MasterForm, player) and state.has(ItemName.DriveConverter, player))
- for region in RegionTable["FinalRegion"]:
- connect(world, player, names, region, RegionName.Final_Region,
- lambda state: state.has(ItemName.FinalForm, player))
-
-
-# shamelessly stolen from the sa2b
-def connect(world: MultiWorld, player: int, used_names: typing.Dict[str, int], source: str, target: str,
- rule: typing.Optional[typing.Callable] = None):
- source_region = world.get_region(source, player)
- target_region = world.get_region(target, player)
-
- if target not in used_names:
- used_names[target] = 1
- name = target
- else:
- used_names[target] += 1
- name = target + (' ' * used_names[target])
-
- connection = Entrance(player, name, source_region)
-
- if rule:
- connection.access_rule = rule
+ for source, target in KH2RegionConnections.items():
+ source_region = multiworld.get_region(source, player)
+ source_region.add_exits(target)
- source_region.exits.append(connection)
- connection.connect(target_region)
+# cave fight:fire/guard
+# hades escape logic:fire,blizzard,slide dash, base tools
+# windows:chicken little.fire element,base tools
+# chasm of challenges:reflect, blizzard, trinity limit,chicken little
+# living bones: magnet
+# some things for barbosa(PR), chicken little
+# hyneas(magnet,reflect)
+# tt2: reflect,chicken,form, guard,aerial recovery,finising plus,
+# corridors,dancers:chicken little or stitch +demyx tools
+# 1k: guard,once more,limit form,
+# snipers +before: stitch, magnet, finishing leap, base tools, reflect
+# dragoons:stitch, magnet, base tools, reflect
+# oc2 tournament thing: stitch, magnet, base tools, reflera
+# lock,shock and barrel: reflect, base tools
+# carpet section: magnera, reflect, base tools,
+# sp2: reflera, stitch, basse tools, reflera, thundara, fantasia/duck flare,once more.
+# tt3: stitch/chicken little, magnera,reflera,base tools,finishing leap,limit form
+# cor
-def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None):
- ret = Region(name, player, world)
+def create_region(multiworld, player: int, active_locations, name: str, locations=None):
+ ret = Region(name, player, multiworld)
if locations:
- for location in locations:
- loc_id = active_locations.get(location, 0)
- if loc_id:
- location = KH2Location(player, location, loc_id.code, ret)
- ret.locations.append(location)
+ loc_to_id = {loc: active_locations.get(loc, 0) for loc in locations if active_locations.get(loc, None)}
+ ret.add_locations(loc_to_id, KH2Location)
+ loc_to_event = {loc: active_locations.get(loc, None) for loc in locations if
+ not active_locations.get(loc, None)}
+ ret.add_locations(loc_to_event, KH2Location)
return ret
diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py
index b86ae4a2db4f..4370ad36b540 100644
--- a/worlds/kh2/Rules.py
+++ b/worlds/kh2/Rules.py
@@ -1,96 +1,1167 @@
+from typing import Dict, Callable, TYPE_CHECKING
-from BaseClasses import MultiWorld
-
-from .Items import exclusionItem_table
-from .Locations import STT_Checks, exclusion_table
-from .Names import LocationName, ItemName
-from ..generic.Rules import add_rule, forbid_items, set_rule
-
-
-def set_rules(world: MultiWorld, player: int):
-
- add_rule(world.get_location(LocationName.RoxasDataMagicBoost, player),
- lambda state: state.kh_dataroxas(player))
- add_rule(world.get_location(LocationName.DemyxDataAPBoost, player),
- lambda state: state.kh_datademyx(player))
- add_rule(world.get_location(LocationName.SaixDataDefenseBoost, player),
- lambda state: state.kh_datasaix(player))
- add_rule(world.get_location(LocationName.XaldinDataDefenseBoost, player),
- lambda state: state.kh_dataxaldin(player))
- add_rule(world.get_location(LocationName.XemnasDataPowerBoost, player),
- lambda state: state.kh_dataxemnas(player))
- add_rule(world.get_location(LocationName.XigbarDataDefenseBoost, player),
- lambda state: state.kh_dataxigbar(player))
- add_rule(world.get_location(LocationName.VexenDataLostIllusion, player),
- lambda state: state.kh_dataaxel(player))
- add_rule(world.get_location(LocationName.LuxordDataAPBoost, player),
- lambda state: state.kh_dataluxord(player))
-
- for slot, weapon in exclusion_table["WeaponSlots"].items():
- add_rule(world.get_location(slot, player), lambda state: state.has(weapon, player))
- formLogicTable = {
- ItemName.ValorForm: [LocationName.Valorlvl4,
- LocationName.Valorlvl5,
- LocationName.Valorlvl6,
- LocationName.Valorlvl7],
- ItemName.WisdomForm: [LocationName.Wisdomlvl4,
- LocationName.Wisdomlvl5,
- LocationName.Wisdomlvl6,
- LocationName.Wisdomlvl7],
- ItemName.LimitForm: [LocationName.Limitlvl4,
- LocationName.Limitlvl5,
- LocationName.Limitlvl6,
- LocationName.Limitlvl7],
- ItemName.MasterForm: [LocationName.Masterlvl4,
- LocationName.Masterlvl5,
- LocationName.Masterlvl6,
- LocationName.Masterlvl7],
- ItemName.FinalForm: [LocationName.Finallvl4,
- LocationName.Finallvl5,
- LocationName.Finallvl6,
- LocationName.Finallvl7]
- }
-
- for form in formLogicTable:
- for i in range(4):
- location = world.get_location(formLogicTable[form][i], player)
- set_rule(location, lambda state, i=i + 1, form=form: state.kh_amount_of_forms(player, i, form))
-
- if world.Goal[player] == "three_proofs":
- add_rule(world.get_location(LocationName.FinalXemnas, player),
- lambda state: state.kh_three_proof_unlocked(player))
- if world.FinalXemnas[player]:
- world.completion_condition[player] = lambda state: state.kh_victory(player)
- else:
- world.completion_condition[player] = lambda state: state.kh_three_proof_unlocked(player)
- # lucky emblem hunt
- elif world.Goal[player] == "lucky_emblem_hunt":
- add_rule(world.get_location(LocationName.FinalXemnas, player),
- lambda state: state.kh_lucky_emblem_unlocked(player, world.LuckyEmblemsRequired[player].value))
- if world.FinalXemnas[player]:
- world.completion_condition[player] = lambda state: state.kh_victory(player)
- else:
- world.completion_condition[player] = lambda state: state.kh_lucky_emblem_unlocked(player, world.LuckyEmblemsRequired[player].value)
- # hitlist if == 2
- else:
- add_rule(world.get_location(LocationName.FinalXemnas, player),
- lambda state: state.kh_hitlist(player, world.BountyRequired[player].value))
- if world.FinalXemnas[player]:
- world.completion_condition[player] = lambda state: state.kh_victory(player)
+from BaseClasses import CollectionState
+from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table, SupportAbility_Table
+from .Locations import exclusion_table, popups_set, Goofy_Checks, Donald_Checks
+from .Names import LocationName, ItemName, RegionName
+from worlds.generic.Rules import add_rule, forbid_items, add_item_rule
+from .Logic import *
+
+# I don't know what is going on here, but it works.
+if TYPE_CHECKING:
+ from . import KH2World
+else:
+ KH2World = object
+
+
+# Shamelessly Stolen from Messanger
+
+
+class KH2Rules:
+ player: int
+ world: KH2World
+ # World Rules: Rules for the visit locks
+ # Location Rules: Deterministic of player settings.
+ # Form Rules: Rules for Drive Forms and Summon levels. These Are Locations
+ # Fight Rules: Rules for fights. These are regions in the worlds.
+ world_rules: Dict[str, Callable[[CollectionState], bool]]
+ location_rules: Dict[str, Callable[[CollectionState], bool]]
+
+ fight_rules: Dict[str, Callable[[CollectionState], bool]]
+
+ def __init__(self, world: KH2World) -> None:
+ self.player = world.player
+ self.world = world
+ self.multiworld = world.multiworld
+
+ def lod_unlocked(self, state: CollectionState, Amount) -> bool:
+ return state.has(ItemName.SwordoftheAncestor, self.player, Amount)
+
+ def oc_unlocked(self, state: CollectionState, Amount) -> bool:
+ return state.has(ItemName.BattlefieldsofWar, self.player, Amount)
+
+ def twtnw_unlocked(self, state: CollectionState, Amount) -> bool:
+ return state.has(ItemName.WaytotheDawn, self.player, Amount)
+
+ def ht_unlocked(self, state: CollectionState, Amount) -> bool:
+ return state.has(ItemName.BoneFist, self.player, Amount)
+
+ def tt_unlocked(self, state: CollectionState, Amount) -> bool:
+ return state.has(ItemName.IceCream, self.player, Amount)
+
+ def pr_unlocked(self, state: CollectionState, Amount) -> bool:
+ return state.has(ItemName.SkillandCrossbones, self.player, Amount)
+
+ def sp_unlocked(self, state: CollectionState, Amount) -> bool:
+ return state.has(ItemName.IdentityDisk, self.player, Amount)
+
+ def stt_unlocked(self, state: CollectionState, Amount) -> bool:
+ return state.has(ItemName.NamineSketches, self.player, Amount)
+
+ def dc_unlocked(self, state: CollectionState, Amount) -> bool:
+ return state.has(ItemName.CastleKey, self.player, Amount) # Using Dummy 13 for this
+
+ def hb_unlocked(self, state: CollectionState, Amount) -> bool:
+ return state.has(ItemName.MembershipCard, self.player, Amount)
+
+ def pl_unlocked(self, state: CollectionState, Amount) -> bool:
+ return state.has(ItemName.ProudFang, self.player, Amount)
+
+ def ag_unlocked(self, state: CollectionState, Amount) -> bool:
+ return state.has(ItemName.Scimitar, self.player, Amount)
+
+ def bc_unlocked(self, state: CollectionState, Amount) -> bool:
+ return state.has(ItemName.BeastsClaw, self.player, Amount)
+
+ def at_three_unlocked(self, state: CollectionState) -> bool:
+ return state.has(ItemName.MagnetElement, self.player, 2)
+
+ def at_four_unlocked(self, state: CollectionState) -> bool:
+ return state.has(ItemName.ThunderElement, self.player, 3)
+
+ def hundred_acre_unlocked(self, state: CollectionState, amount) -> bool:
+ return state.has(ItemName.TornPages, self.player, amount)
+
+ def level_locking_unlock(self, state: CollectionState, amount):
+ if self.world.options.Promise_Charm and state.has(ItemName.PromiseCharm, self.player):
+ return True
+ return amount <= sum([state.count(item_name, self.player) for item_name in visit_locking_dict["2VisitLocking"]])
+
+ def summon_levels_unlocked(self, state: CollectionState, amount) -> bool:
+ return amount <= sum([state.count(item_name, self.player) for item_name in summons])
+
+ def kh2_list_count_sum(self, item_name_set: list, state: CollectionState) -> int:
+ """
+ Returns the sum of state.count() for each item in the list.
+ """
+ return sum(
+ [state.count(item_name, self.player) for item_name in item_name_set]
+ )
+
+ def kh2_list_any_sum(self, list_of_item_name_list: list, state: CollectionState) -> int:
+ """
+ Returns sum that increments by 1 if state.has_any
+ """
+ return sum(
+ [1 for item_list in list_of_item_name_list if
+ state.has_any(set(item_list), self.player)]
+ )
+
+ def kh2_dict_count(self, item_name_to_count: dict, state: CollectionState) -> bool:
+ """
+ simplifies count to a dictionary.
+ """
+ return all(
+ [state.count(item_name, self.player) >= item_amount for item_name, item_amount in
+ item_name_to_count.items()]
+ )
+
+ def kh2_dict_one_count(self, item_name_to_count: dict, state: CollectionState) -> int:
+ """
+ simplifies count to a dictionary.
+ """
+ return sum(
+ [1 for item_name, item_amount in
+ item_name_to_count.items() if state.count(item_name, self.player) >= item_amount]
+ )
+
+ def kh2_can_reach_any(self, loc_set: list, state: CollectionState):
+ """
+ Can reach any locations in the set.
+ """
+ return any(
+ [self.kh2_can_reach(location, state) for location in
+ loc_set]
+ )
+
+ def kh2_can_reach_all(self, loc_list: list, state: CollectionState):
+ """
+ Can reach all locations in the set.
+ """
+ return all(
+ [self.kh2_can_reach(location, state) for location in loc_list]
+ )
+
+ def kh2_can_reach(self, loc: str, state: CollectionState):
+ """
+ Returns bool instead of collection state.
+ """
+ return state.can_reach(self.multiworld.get_location(loc, self.player), "location", self.player)
+
+ def kh2_has_all(self, items: list, state: CollectionState):
+ """If state has at least one of all."""
+ return state.has_all(set(items), self.player)
+
+ def kh2_has_any(self, items: list, state: CollectionState):
+ return state.has_any(set(items), self.player)
+
+ def form_list_unlock(self, state: CollectionState, parent_form_list, level_required, fight_logic=False) -> bool:
+ form_access = {parent_form_list}
+ if self.world.options.AutoFormLogic and state.has(ItemName.SecondChance, self.player) and not fight_logic:
+ if parent_form_list == ItemName.MasterForm:
+ if state.has(ItemName.DriveConverter, self.player):
+ form_access.add(auto_form_dict[parent_form_list])
+ else:
+ form_access.add(auto_form_dict[parent_form_list])
+ return state.has_any(form_access, self.player) \
+ and self.get_form_level_requirement(state, level_required)
+
+ def get_form_level_requirement(self, state, amount):
+ forms_available = 0
+ form_list = [ItemName.ValorForm, ItemName.WisdomForm, ItemName.LimitForm, ItemName.MasterForm,
+ ItemName.FinalForm]
+ if self.world.options.FinalFormLogic != "no_light_and_darkness":
+ if self.world.options.FinalFormLogic == "light_and_darkness":
+ if state.has(ItemName.LightDarkness, self.player) and state.has_any(set(form_list), self.player):
+ forms_available += 1
+ form_list.remove(ItemName.FinalForm)
+ else: # self.multiworld.FinalFormLogic=="just a form"
+ form_list.remove(ItemName.FinalForm)
+ if state.has_any(form_list, self.player):
+ forms_available += 1
+ forms_available += sum([1 for form in form_list if state.has(form, self.player)])
+ return forms_available >= amount
+
+
+class KH2WorldRules(KH2Rules):
+ def __init__(self, kh2world: KH2World) -> None:
+ # These Rules are Always in effect
+ super().__init__(kh2world)
+ self.region_rules = {
+ RegionName.LoD: lambda state: self.lod_unlocked(state, 1),
+ RegionName.LoD2: lambda state: self.lod_unlocked(state, 2),
+
+ RegionName.Oc: lambda state: self.oc_unlocked(state, 1),
+ RegionName.Oc2: lambda state: self.oc_unlocked(state, 2),
+
+ RegionName.Twtnw2: lambda state: self.twtnw_unlocked(state, 2),
+ # These will be swapped and First Visit lock for twtnw is in development.
+ # RegionName.Twtnw1: lambda state: self.lod_unlocked(state, 2),
+
+ RegionName.Ht: lambda state: self.ht_unlocked(state, 1),
+ RegionName.Ht2: lambda state: self.ht_unlocked(state, 2),
+
+ RegionName.Tt: lambda state: self.tt_unlocked(state, 1),
+ RegionName.Tt2: lambda state: self.tt_unlocked(state, 2),
+ RegionName.Tt3: lambda state: self.tt_unlocked(state, 3),
+
+ RegionName.Pr: lambda state: self.pr_unlocked(state, 1),
+ RegionName.Pr2: lambda state: self.pr_unlocked(state, 2),
+
+ RegionName.Sp: lambda state: self.sp_unlocked(state, 1),
+ RegionName.Sp2: lambda state: self.sp_unlocked(state, 2),
+
+ RegionName.Stt: lambda state: self.stt_unlocked(state, 1),
+
+ RegionName.Dc: lambda state: self.dc_unlocked(state, 1),
+ RegionName.Tr: lambda state: self.dc_unlocked(state, 2),
+ # Terra is a fight and can have more than just this requirement.
+ # RegionName.Terra: lambda state:state.has(ItemName.ProofofConnection,self.player),
+
+ RegionName.Hb: lambda state: self.hb_unlocked(state, 1),
+ RegionName.Hb2: lambda state: self.hb_unlocked(state, 2),
+ RegionName.Mushroom13: lambda state: state.has(ItemName.ProofofPeace, self.player),
+
+ RegionName.Pl: lambda state: self.pl_unlocked(state, 1),
+ RegionName.Pl2: lambda state: self.pl_unlocked(state, 2),
+
+ RegionName.Ag: lambda state: self.ag_unlocked(state, 1),
+ RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement, ItemName.BlizzardElement, ItemName.ThunderElement], state),
+
+ RegionName.Bc: lambda state: self.bc_unlocked(state, 1),
+ RegionName.Bc2: lambda state: self.bc_unlocked(state, 2),
+
+ RegionName.AtlanticaSongThree: lambda state: self.at_three_unlocked(state),
+ RegionName.AtlanticaSongFour: lambda state: self.at_four_unlocked(state),
+
+ RegionName.Ha1: lambda state: True,
+ RegionName.Ha2: lambda state: self.hundred_acre_unlocked(state, 1),
+ RegionName.Ha3: lambda state: self.hundred_acre_unlocked(state, 2),
+ RegionName.Ha4: lambda state: self.hundred_acre_unlocked(state, 3),
+ RegionName.Ha5: lambda state: self.hundred_acre_unlocked(state, 4),
+ RegionName.Ha6: lambda state: self.hundred_acre_unlocked(state, 5),
+
+ RegionName.LevelsVS1: lambda state: self.level_locking_unlock(state, 1),
+ RegionName.LevelsVS3: lambda state: self.level_locking_unlock(state, 3),
+ RegionName.LevelsVS6: lambda state: self.level_locking_unlock(state, 6),
+ RegionName.LevelsVS9: lambda state: self.level_locking_unlock(state, 9),
+ RegionName.LevelsVS12: lambda state: self.level_locking_unlock(state, 12),
+ RegionName.LevelsVS15: lambda state: self.level_locking_unlock(state, 15),
+ RegionName.LevelsVS18: lambda state: self.level_locking_unlock(state, 18),
+ RegionName.LevelsVS21: lambda state: self.level_locking_unlock(state, 21),
+ RegionName.LevelsVS24: lambda state: self.level_locking_unlock(state, 24),
+ RegionName.LevelsVS26: lambda state: self.level_locking_unlock(state, 26),
+ }
+
+ def set_kh2_rules(self) -> None:
+ for region_name, rules in self.region_rules.items():
+ region = self.multiworld.get_region(region_name, self.player)
+ for entrance in region.entrances:
+ entrance.access_rule = rules
+
+ self.set_kh2_goal()
+
+ weapon_region = self.multiworld.get_region(RegionName.Keyblade, self.player)
+ for location in weapon_region.locations:
+ add_rule(location, lambda state: state.has(exclusion_table["WeaponSlots"][location.name], self.player))
+ if location.name in Goofy_Checks:
+ add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys())
+ elif location.name in Donald_Checks:
+ add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys())
+ else:
+ add_item_rule(location, lambda item: item.player == self.player and item.name in SupportAbility_Table.keys())
+
+ def set_kh2_goal(self):
+ final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnasEventLocation, self.player)
+ if self.world.options.Goal == "three_proofs":
+ final_xemnas_location.access_rule = lambda state: self.kh2_has_all(three_proofs, state)
+ if self.world.options.FinalXemnas:
+ self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1)
+ else:
+ self.multiworld.completion_condition[self.player] = lambda state: self.kh2_has_all(three_proofs, state)
+ # lucky emblem hunt
+ elif self.world.options.Goal == "lucky_emblem_hunt":
+ final_xemnas_location.access_rule = lambda state: state.has(ItemName.LuckyEmblem, self.player, self.world.options.LuckyEmblemsRequired.value)
+ if self.world.options.FinalXemnas:
+ self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1)
+ else:
+ self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.LuckyEmblem, self.player, self.world.options.LuckyEmblemsRequired.value)
+ # hitlist if == 2
+ elif self.world.options.Goal == "hitlist":
+ final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.world.options.BountyRequired.value)
+ if self.world.options.FinalXemnas:
+ self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1)
+ else:
+ self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Bounty, self.player, self.world.options.BountyRequired.value)
else:
- world.completion_condition[player] = lambda state: state.kh_hitlist(player, world.BountyRequired[player].value)
+
+ final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.world.options.BountyRequired.value) and \
+ state.has(ItemName.LuckyEmblem, self.player, self.world.options.LuckyEmblemsRequired.value)
+ if self.world.options.FinalXemnas:
+ self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1)
+ else:
+ self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Bounty, self.player, self.world.options.BountyRequired.value) and \
+ state.has(ItemName.LuckyEmblem, self.player, self.world.options.LuckyEmblemsRequired.value)
+
+
+class KH2FormRules(KH2Rules):
+ #: Dict[str, Callable[[CollectionState], bool]]
+ def __init__(self, world: KH2World) -> None:
+ super().__init__(world)
+ # access rules on where you can level a form.
+
+ self.form_rules = {
+ LocationName.Valorlvl2: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 0),
+ LocationName.Valorlvl3: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 1),
+ LocationName.Valorlvl4: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 2),
+ LocationName.Valorlvl5: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 3),
+ LocationName.Valorlvl6: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 4),
+ LocationName.Valorlvl7: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 5),
+ LocationName.Wisdomlvl2: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 0),
+ LocationName.Wisdomlvl3: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 1),
+ LocationName.Wisdomlvl4: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 2),
+ LocationName.Wisdomlvl5: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 3),
+ LocationName.Wisdomlvl6: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 4),
+ LocationName.Wisdomlvl7: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 5),
+ LocationName.Limitlvl2: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 0),
+ LocationName.Limitlvl3: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 1),
+ LocationName.Limitlvl4: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 2),
+ LocationName.Limitlvl5: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 3),
+ LocationName.Limitlvl6: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 4),
+ LocationName.Limitlvl7: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 5),
+ LocationName.Masterlvl2: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 0),
+ LocationName.Masterlvl3: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 1),
+ LocationName.Masterlvl4: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 2),
+ LocationName.Masterlvl5: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 3),
+ LocationName.Masterlvl6: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 4),
+ LocationName.Masterlvl7: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 5),
+ LocationName.Finallvl2: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 0),
+ LocationName.Finallvl3: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 1),
+ LocationName.Finallvl4: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 2),
+ LocationName.Finallvl5: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 3),
+ LocationName.Finallvl6: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 4),
+ LocationName.Finallvl7: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 5),
+ LocationName.Summonlvl2: lambda state: self.summon_levels_unlocked(state, 1),
+ LocationName.Summonlvl3: lambda state: self.summon_levels_unlocked(state, 1),
+ LocationName.Summonlvl4: lambda state: self.summon_levels_unlocked(state, 2),
+ LocationName.Summonlvl5: lambda state: self.summon_levels_unlocked(state, 3),
+ LocationName.Summonlvl6: lambda state: self.summon_levels_unlocked(state, 4),
+ LocationName.Summonlvl7: lambda state: self.summon_levels_unlocked(state, 4),
+ }
+ self.form_region_rules = {
+ RegionName.Valor: lambda state: self.multi_form_region_access(),
+ RegionName.Wisdom: lambda state: self.multi_form_region_access(),
+ RegionName.Limit: lambda state: self.limit_form_region_access(),
+ RegionName.Master: lambda state: self.multi_form_region_access(),
+ RegionName.Final: lambda state: self.final_form_region_access(state)
+ }
+
+ def final_form_region_access(self, state: CollectionState) -> bool:
+ """
+ Can reach one of TT3,Twtnw post Roxas, BC2, LoD2 or PR2
+ """
+ # tt3 start, can beat roxas, can beat gr2, can beat xaldin, can beat storm rider.
+
+ return any(
+ self.multiworld.get_location(location, self.player).can_reach(state) for location in
+ final_leveling_access
+ )
+
+ @staticmethod
+ def limit_form_region_access() -> bool:
+ """
+ returns true since twtnw always is open and has enemies
+ """
+ return True
+
+ @staticmethod
+ def multi_form_region_access() -> bool:
+ """
+ returns true since twtnw always is open and has enemies
+ Valor, Wisdom and Master Form region access.
+ Note: This does not account for having the drive form. See form_list_unlock
+ """
+ # todo: if boss enemy start the player with oc stone because of cerb
+ return True
+
+ def set_kh2_form_rules(self):
+ for region_name in drive_form_list:
+ if region_name == RegionName.Summon and not self.world.options.SummonLevelLocationToggle:
+ continue
+ # could get the location of each of these, but I feel like that would be less optimal
+ region = self.multiworld.get_region(region_name, self.player)
+ # if region_name in form_region_rules
+ if region_name != RegionName.Summon:
+ for entrance in region.entrances:
+ entrance.access_rule = self.form_region_rules[region_name]
+ for loc in region.locations:
+ loc.access_rule = self.form_rules[loc.name]
+
+
+class KH2FightRules(KH2Rules):
+ player: int
+ world: KH2World
+ region_rules: Dict[str, Callable[[CollectionState], bool]]
+ location_rules: Dict[str, Callable[[CollectionState], bool]]
+
+ # cor logic
+ # have 3 things for the logic
+ # region:movement_rules and (fight_rules or skip rules)
+ # if skip rules are of return false
+ def __init__(self, world: KH2World) -> None:
+ super().__init__(world)
+ self.fight_logic = world.options.FightLogic.current_key
+
+ self.fight_region_rules = {
+ RegionName.ShanYu: lambda state: self.get_shan_yu_rules(state),
+ RegionName.AnsemRiku: lambda state: self.get_ansem_riku_rules(state),
+ RegionName.StormRider: lambda state: self.get_storm_rider_rules(state),
+ RegionName.DataXigbar: lambda state: self.get_data_xigbar_rules(state),
+ RegionName.TwinLords: lambda state: self.get_fire_lord_rules(state) and self.get_blizzard_lord_rules(state),
+ RegionName.GenieJafar: lambda state: self.get_genie_jafar_rules(state),
+ RegionName.DataLexaeus: lambda state: self.get_data_lexaeus_rules(state),
+ RegionName.OldPete: lambda state: self.get_old_pete_rules(),
+ RegionName.FuturePete: lambda state: self.get_future_pete_rules(state),
+ RegionName.Terra: lambda state: self.get_terra_rules(state) and state.has(ItemName.ProofofConnection, self.player),
+ RegionName.DataMarluxia: lambda state: self.get_data_marluxia_rules(state),
+ RegionName.Barbosa: lambda state: self.get_barbosa_rules(state),
+ RegionName.GrimReaper1: lambda state: self.get_grim_reaper1_rules(),
+ RegionName.GrimReaper2: lambda state: self.get_grim_reaper2_rules(state),
+ RegionName.DataLuxord: lambda state: self.get_data_luxord_rules(state),
+ RegionName.Cerberus: lambda state: self.get_cerberus_rules(state),
+ RegionName.OlympusPete: lambda state: self.get_olympus_pete_rules(state),
+ RegionName.Hydra: lambda state: self.get_hydra_rules(state),
+ RegionName.Hades: lambda state: self.get_hades_rules(state),
+ RegionName.DataZexion: lambda state: self.get_data_zexion_rules(state),
+ RegionName.OcPainAndPanicCup: lambda state: self.get_pain_and_panic_cup_rules(state),
+ RegionName.OcCerberusCup: lambda state: self.get_cerberus_cup_rules(state),
+ RegionName.Oc2TitanCup: lambda state: self.get_titan_cup_rules(state),
+ RegionName.Oc2GofCup: lambda state: self.get_goddess_of_fate_cup_rules(state),
+ RegionName.HadesCups: lambda state: self.get_hades_cup_rules(state),
+ RegionName.Thresholder: lambda state: self.get_thresholder_rules(state),
+ RegionName.Beast: lambda state: self.get_beast_rules(),
+ RegionName.DarkThorn: lambda state: self.get_dark_thorn_rules(state),
+ RegionName.Xaldin: lambda state: self.get_xaldin_rules(state),
+ RegionName.DataXaldin: lambda state: self.get_data_xaldin_rules(state),
+ RegionName.HostileProgram: lambda state: self.get_hostile_program_rules(state),
+ RegionName.Mcp: lambda state: self.get_mcp_rules(state),
+ RegionName.DataLarxene: lambda state: self.get_data_larxene_rules(state),
+ RegionName.PrisonKeeper: lambda state: self.get_prison_keeper_rules(state),
+ RegionName.OogieBoogie: lambda state: self.get_oogie_rules(),
+ RegionName.Experiment: lambda state: self.get_experiment_rules(state),
+ RegionName.DataVexen: lambda state: self.get_data_vexen_rules(state),
+ RegionName.HBDemyx: lambda state: self.get_demyx_rules(state),
+ RegionName.ThousandHeartless: lambda state: self.get_thousand_heartless_rules(state),
+ RegionName.DataDemyx: lambda state: self.get_data_demyx_rules(state),
+ RegionName.Sephi: lambda state: self.get_sephiroth_rules(state),
+ RegionName.CorFirstFight: lambda state: self.get_cor_first_fight_movement_rules(state) and (self.get_cor_first_fight_rules(state) or self.get_cor_skip_first_rules(state)),
+ RegionName.CorSecondFight: lambda state: self.get_cor_second_fight_movement_rules(state),
+ RegionName.Transport: lambda state: self.get_transport_movement_rules(state),
+ RegionName.Scar: lambda state: self.get_scar_rules(state),
+ RegionName.GroundShaker: lambda state: self.get_groundshaker_rules(state),
+ RegionName.DataSaix: lambda state: self.get_data_saix_rules(state),
+ RegionName.TwilightThorn: lambda state: self.get_twilight_thorn_rules(),
+ RegionName.Axel1: lambda state: self.get_axel_one_rules(),
+ RegionName.Axel2: lambda state: self.get_axel_two_rules(),
+ RegionName.DataRoxas: lambda state: self.get_data_roxas_rules(state),
+ RegionName.DataAxel: lambda state: self.get_data_axel_rules(state),
+ RegionName.Roxas: lambda state: self.get_roxas_rules(state) and self.twtnw_unlocked(state, 1),
+ RegionName.Xigbar: lambda state: self.get_xigbar_rules(state),
+ RegionName.Luxord: lambda state: self.get_luxord_rules(state),
+ RegionName.Saix: lambda state: self.get_saix_rules(state),
+ RegionName.Xemnas: lambda state: self.get_xemnas_rules(state),
+ RegionName.ArmoredXemnas: lambda state: self.get_armored_xemnas_one_rules(state),
+ RegionName.ArmoredXemnas2: lambda state: self.get_armored_xemnas_two_rules(state),
+ RegionName.FinalXemnas: lambda state: self.get_final_xemnas_rules(state),
+ RegionName.DataXemnas: lambda state: self.get_data_xemnas_rules(state),
+ }
+
+ def set_kh2_fight_rules(self) -> None:
+ for region_name, rules in self.fight_region_rules.items():
+ region = self.multiworld.get_region(region_name, self.player)
+ for entrance in region.entrances:
+ entrance.access_rule = rules
+
+ for loc_name in [LocationName.TransportEventLocation, LocationName.TransporttoRemembrance]:
+ location = self.multiworld.get_location(loc_name, self.player)
+ add_rule(location, lambda state: self.get_transport_fight_rules(state))
+
+ def get_shan_yu_rules(self, state: CollectionState) -> bool:
+ # easy: gap closer, defensive tool,drive form
+ # normal: 2 out of easy
+ # hard: defensive tool or drive form
+ shan_yu_rules = {
+ "easy": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 3,
+ "normal": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 2,
+ "hard": self.kh2_list_any_sum([defensive_tool, form_list], state) >= 1
+ }
+ return shan_yu_rules[self.fight_logic]
+
+ def get_ansem_riku_rules(self, state: CollectionState) -> bool:
+ # easy: gap closer,defensive tool,ground finisher/limit form
+ # normal: defensive tool and (gap closer/ground finisher/limit form)
+ # hard: defensive tool or limit form
+ ansem_riku_rules = {
+ "easy": self.kh2_list_any_sum([gap_closer, defensive_tool, [ItemName.LimitForm], ground_finisher], state) >= 3,
+ "normal": self.kh2_list_any_sum([gap_closer, defensive_tool, [ItemName.LimitForm], ground_finisher], state) >= 2,
+ "hard": self.kh2_has_any([ItemName.ReflectElement, ItemName.Guard, ItemName.LimitForm], state),
+ }
+ return ansem_riku_rules[self.fight_logic]
+
+ def get_storm_rider_rules(self, state: CollectionState) -> bool:
+ # easy: has defensive tool,drive form, party limit,aerial move
+ # normal: has 3 of those things
+ # hard: has 2 of those things
+ storm_rider_rules = {
+ "easy": self.kh2_list_any_sum([defensive_tool, party_limit, aerial_move, form_list], state) >= 4,
+ "normal": self.kh2_list_any_sum([defensive_tool, party_limit, aerial_move, form_list], state) >= 3,
+ "hard": self.kh2_list_any_sum([defensive_tool, party_limit, aerial_move, form_list], state) >= 2,
+ }
+ return storm_rider_rules[self.fight_logic]
+
+ def get_data_xigbar_rules(self, state: CollectionState) -> bool:
+ # easy:final 7,firaga,2 air combo plus,air gap closer, finishing plus,guard,reflega,horizontal slash,donald limit
+ # normal:final 7,firaga,finishing plus,guard,reflect horizontal slash,donald limit
+ # hard:((final 5, fira) or donald limit), finishing plus,guard/reflect
+ data_xigbar_rules = {
+ "easy": self.kh2_dict_count(easy_data_xigbar_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_has_any(donald_limit, state),
+ "normal": self.kh2_dict_count(normal_data_xigbar_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_has_any(donald_limit, state),
+ "hard": ((self.form_list_unlock(state, ItemName.FinalForm, 3, True) and state.has(ItemName.FireElement, self.player, 2)) or self.kh2_has_any(donald_limit, state))
+ and state.has(ItemName.FinishingPlus, self.player) and self.kh2_has_any(defensive_tool, state)
+ }
+ return data_xigbar_rules[self.fight_logic]
+
+ def get_fire_lord_rules(self, state: CollectionState) -> bool:
+ # easy: drive form,defensive tool,one black magic,party limit
+ # normal: 3 of those things
+ # hard:2 of those things
+ # duplicate of the other because in boss rando there will be to bosses in arena and these bosses can be split.
+ fire_lords_rules = {
+ "easy": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 4,
+ "normal": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 3,
+ "hard": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 2,
+ }
+ return fire_lords_rules[self.fight_logic]
+
+ def get_blizzard_lord_rules(self, state: CollectionState) -> bool:
+ # easy: drive form,defensive tool,one black magic,party limit
+ # normal: 3 of those things
+ # hard:2 of those things
+ # duplicate of the other because in boss rando there will be to bosses in arena and these bosses can be split.
+ blizzard_lords_rules = {
+ "easy": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 4,
+ "normal": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 3,
+ "hard": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 2,
+ }
+ return blizzard_lords_rules[self.fight_logic]
+
+ def get_genie_jafar_rules(self, state: CollectionState) -> bool:
+ # easy: defensive tool,black magic,ground finisher,finishing plus
+ # normal: defensive tool, ground finisher,finishing plus
+ # hard: defensive tool,finishing plus
+ genie_jafar_rules = {
+ "easy": self.kh2_list_any_sum([defensive_tool, black_magic, ground_finisher, {ItemName.FinishingPlus}], state) >= 4,
+ "normal": self.kh2_list_any_sum([defensive_tool, ground_finisher, {ItemName.FinishingPlus}], state) >= 3,
+ "hard": self.kh2_list_any_sum([defensive_tool, {ItemName.FinishingPlus}], state) >= 2,
+ }
+ return genie_jafar_rules[self.fight_logic]
+
+ def get_data_lexaeus_rules(self, state: CollectionState) -> bool:
+ # easy:both gap closers,final 7,firaga,reflera,donald limit, guard
+ # normal:one gap closer,final 5,fira,reflect, donald limit,guard
+ # hard:defensive tool,gap closer
+ data_lexaues_rules = {
+ "easy": self.kh2_dict_count(easy_data_lex_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_list_any_sum([donald_limit], state) >= 1,
+ "normal": self.kh2_dict_count(normal_data_lex_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 3, True) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2,
+ "hard": self.kh2_list_any_sum([defensive_tool, gap_closer], state) >= 2,
+ }
+ return data_lexaues_rules[self.fight_logic]
+
+ @staticmethod
+ def get_old_pete_rules():
+ # fight is free.
+ return True
+
+ def get_future_pete_rules(self, state: CollectionState) -> bool:
+ # easy:defensive option,gap closer,drive form
+ # norma:2 of those things
+ # hard 1 of those things
+ future_pete_rules = {
+ "easy": self.kh2_list_any_sum([defensive_tool, gap_closer, form_list], state) >= 3,
+ "normal": self.kh2_list_any_sum([defensive_tool, gap_closer, form_list], state) >= 2,
+ "hard": self.kh2_list_any_sum([defensive_tool, gap_closer, form_list], state) >= 1,
+ }
+ return future_pete_rules[self.fight_logic]
+
+ def get_data_marluxia_rules(self, state: CollectionState) -> bool:
+ # easy:both gap closers,final 7,firaga,reflera,donald limit, guard
+ # normal:one gap closer,final 5,fira,reflect, donald limit,guard
+ # hard:defensive tool,gap closer
+ data_marluxia_rules = {
+ "easy": self.kh2_dict_count(easy_data_marluxia_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_list_any_sum([donald_limit], state) >= 1,
+ "normal": self.kh2_dict_count(normal_data_marluxia_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 3, True) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2,
+ "hard": self.kh2_list_any_sum([defensive_tool, gap_closer, [ItemName.AerialRecovery]], state) >= 3,
+ }
+ return data_marluxia_rules[self.fight_logic]
+
+ def get_terra_rules(self, state: CollectionState) -> bool:
+ # easy:scom,gap closers,explosion,2 combo pluses,final 7,firaga, donald limits,reflect,guard,3 dodge roll,3 aerial dodge and 3glide
+ # normal:gap closers,explosion,2 combo pluses,2 dodge roll,2 aerial dodge and lvl 2glide,guard,donald limit, guard
+ # hard:1 gap closer,explosion,2 combo pluses,2 dodge roll,2 aerial dodge and lvl 2glide,guard
+ terra_rules = {
+ "easy": self.kh2_dict_count(easy_terra_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True),
+ "normal": self.kh2_dict_count(normal_terra_tools, state) and self.kh2_list_any_sum([donald_limit], state) >= 1,
+ "hard": self.kh2_dict_count(hard_terra_tools, state) and self.kh2_list_any_sum([gap_closer], state) >= 1,
+ }
+ return terra_rules[self.fight_logic]
+
+ def get_barbosa_rules(self, state: CollectionState) -> bool:
+ # easy:blizzara and thundara or one of each,defensive tool
+ # normal:(blizzard or thunder) and defensive tool
+ # hard: defensive tool
+ barbosa_rules = {
+ "easy": self.kh2_list_count_sum([ItemName.BlizzardElement, ItemName.ThunderElement], state) >= 2 and self.kh2_list_any_sum([defensive_tool], state) >= 1,
+ "normal": self.kh2_list_any_sum([defensive_tool, {ItemName.BlizzardElement, ItemName.ThunderElement}], state) >= 2,
+ "hard": self.kh2_list_any_sum([defensive_tool], state) >= 1,
+ }
+ return barbosa_rules[self.fight_logic]
+
+ @staticmethod
+ def get_grim_reaper1_rules():
+ # fight is free.
+ return True
+
+ def get_grim_reaper2_rules(self, state: CollectionState) -> bool:
+ # easy:master form,thunder,defensive option
+ # normal:master form/stitch,thunder,defensive option
+ # hard:any black magic,defensive option.
+ gr2_rules = {
+ "easy": self.kh2_list_any_sum([defensive_tool, {ItemName.MasterForm, ItemName.ThunderElement}], state) >= 2,
+ "normal": self.kh2_list_any_sum([defensive_tool, {ItemName.MasterForm, ItemName.Stitch}, {ItemName.ThunderElement}], state) >= 3,
+ "hard": self.kh2_list_any_sum([black_magic, defensive_tool], state) >= 2
+ }
+ return gr2_rules[self.fight_logic]
+
+ def get_data_luxord_rules(self, state: CollectionState) -> bool:
+ # easy:gap closers,reflega,aerial dodge lvl 2,glide lvl 2,guard
+ # normal:1 gap closer,reflect,aerial dodge lvl 1,glide lvl 1,guard
+ # hard:quick run,defensive option
+ data_luxord_rules = {
+ "easy": self.kh2_dict_count(easy_data_luxord_tools, state),
+ "normal": self.kh2_has_all([ItemName.ReflectElement, ItemName.AerialDodge, ItemName.Glide, ItemName.Guard], state) and self.kh2_has_any(defensive_tool, state),
+ "hard": self.kh2_list_any_sum([{ItemName.QuickRun}, defensive_tool], state)
+ }
+ return data_luxord_rules[self.fight_logic]
+
+ def get_cerberus_rules(self, state: CollectionState) -> bool:
+ # easy,normal:defensive option, offensive magic
+ # hard:defensive option
+ cerberus_rules = {
+ "easy": self.kh2_list_any_sum([defensive_tool, black_magic], state) >= 2,
+ "normal": self.kh2_list_any_sum([defensive_tool, black_magic], state) >= 2,
+ "hard": self.kh2_has_any(defensive_tool, state),
+ }
+ return cerberus_rules[self.fight_logic]
+
+ def get_pain_and_panic_cup_rules(self, state: CollectionState) -> bool:
+ # easy:2 party limit,reflect
+ # normal:1 party limit,reflect
+ # hard:reflect
+ pain_and_panic_rules = {
+ "easy": self.kh2_list_count_sum(party_limit, state) >= 2 and state.has(ItemName.ReflectElement, self.player),
+ "normal": self.kh2_list_count_sum(party_limit, state) >= 1 and state.has(ItemName.ReflectElement, self.player),
+ "hard": state.has(ItemName.ReflectElement, self.player)
+ }
+ return pain_and_panic_rules[self.fight_logic] and (self.kh2_has_all([ItemName.FuturePeteEvent], state) or state.has(ItemName.HadesCupTrophy, self.player))
+
+ def get_cerberus_cup_rules(self, state: CollectionState) -> bool:
+ # easy:3 drive forms,reflect
+ # normal:2 drive forms,reflect
+ # hard:reflect
+ cerberus_cup_rules = {
+ "easy": self.kh2_can_reach_any([LocationName.Valorlvl5, LocationName.Wisdomlvl5, LocationName.Limitlvl5, LocationName.Masterlvl5, LocationName.Finallvl5], state) and state.has(ItemName.ReflectElement, self.player),
+ "normal": self.kh2_can_reach_any([LocationName.Valorlvl4, LocationName.Wisdomlvl4, LocationName.Limitlvl4, LocationName.Masterlvl4, LocationName.Finallvl4], state) and state.has(ItemName.ReflectElement, self.player),
+ "hard": state.has(ItemName.ReflectElement, self.player)
+ }
+ return cerberus_cup_rules[self.fight_logic] and (self.kh2_has_all([ItemName.ScarEvent, ItemName.OogieBoogieEvent, ItemName.TwinLordsEvent], state) or state.has(ItemName.HadesCupTrophy, self.player))
+
+ def get_titan_cup_rules(self, state: CollectionState) -> bool:
+ # easy:4 summons,reflera
+ # normal:4 summons,reflera
+ # hard:2 summons,reflera
+ titan_cup_rules = {
+ "easy": self.kh2_list_count_sum(summons, state) >= 4 and state.has(ItemName.ReflectElement, self.player, 2),
+ "normal": self.kh2_list_count_sum(summons, state) >= 3 and state.has(ItemName.ReflectElement, self.player, 2),
+ "hard": self.kh2_list_count_sum(summons, state) >= 2 and state.has(ItemName.ReflectElement, self.player, 2),
+ }
+ return titan_cup_rules[self.fight_logic] and (state.has(ItemName.HadesEvent, self.player) or state.has(ItemName.HadesCupTrophy, self.player))
+
+ def get_goddess_of_fate_cup_rules(self, state: CollectionState) -> bool:
+ # can beat all the other cups+xemnas 1
+ return self.kh2_has_all([ItemName.OcPainAndPanicCupEvent, ItemName.OcCerberusCupEvent, ItemName.Oc2TitanCupEvent], state)
+
+ def get_hades_cup_rules(self, state: CollectionState) -> bool:
+ # can beat goddess of fate cup
+ return state.has(ItemName.Oc2GofCupEvent, self.player)
+
+ def get_olympus_pete_rules(self, state: CollectionState) -> bool:
+ # easy:gap closer,defensive option,drive form
+ # normal:2 of those things
+ # hard:1 of those things
+ olympus_pete_rules = {
+ "easy": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 3,
+ "normal": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 2,
+ "hard": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 1,
+ }
+ return olympus_pete_rules[self.fight_logic]
+
+ def get_hydra_rules(self, state: CollectionState) -> bool:
+ # easy:drive form,defensive option,offensive magic
+ # normal 2 of those things
+ # hard: one of those things
+ hydra_rules = {
+ "easy": self.kh2_list_any_sum([black_magic, defensive_tool, form_list], state) >= 3,
+ "normal": self.kh2_list_any_sum([black_magic, defensive_tool, form_list], state) >= 2,
+ "hard": self.kh2_list_any_sum([black_magic, defensive_tool, form_list], state) >= 1,
+ }
+ return hydra_rules[self.fight_logic]
+
+ def get_hades_rules(self, state: CollectionState) -> bool:
+ # easy:drive form,summon,gap closer,defensive option
+ # normal:3 of those things
+ # hard:2 of those things
+ hades_rules = {
+ "easy": self.kh2_list_any_sum([gap_closer, summons, defensive_tool, form_list], state) >= 4,
+ "normal": self.kh2_list_any_sum([gap_closer, summons, defensive_tool, form_list], state) >= 3,
+ "hard": self.kh2_list_any_sum([gap_closer, summons, defensive_tool, form_list], state) >= 2,
+ }
+ return hades_rules[self.fight_logic]
+
+ def get_data_zexion_rules(self, state: CollectionState) -> bool:
+ # easy: final 7,firaga,scom,both donald limits, Reflega ,guard,2 gap closers,quick run level 3
+ # normal:final 7,firaga, donald limit, Reflega ,guard,1 gap closers,quick run level 3
+ # hard:final 5,fira, donald limit, reflect,gap closer,quick run level 2
+ data_zexion_rules = {
+ "easy": self.kh2_dict_count(easy_data_zexion, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True),
+ "normal": self.kh2_dict_count(normal_data_zexion, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2,
+ "hard": self.kh2_dict_count(hard_data_zexion, state) and self.form_list_unlock(state, ItemName.FinalForm, 3, True) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2,
+ }
+ return data_zexion_rules[self.fight_logic]
+
+ def get_thresholder_rules(self, state: CollectionState) -> bool:
+ # easy:drive form,black magic,defensive tool
+ # normal:2 of those things
+ # hard:defensive tool or drive form
+ thresholder_rules = {
+ "easy": self.kh2_list_any_sum([form_list, black_magic, defensive_tool], state) >= 3,
+ "normal": self.kh2_list_any_sum([form_list, black_magic, defensive_tool], state) >= 2,
+ "hard": self.kh2_list_any_sum([form_list, defensive_tool], state) >= 1,
+ }
+ return thresholder_rules[self.fight_logic]
+
+ @staticmethod
+ def get_beast_rules():
+ # fight is free
+ return True
+
+ def get_dark_thorn_rules(self, state: CollectionState) -> bool:
+ # easy:drive form,defensive tool,gap closer
+ # normal:drive form,defensive tool
+ # hard:defensive tool
+ dark_thorn_rules = {
+ "easy": self.kh2_list_any_sum([form_list, gap_closer, defensive_tool], state) >= 3,
+ "normal": self.kh2_list_any_sum([form_list, defensive_tool], state) >= 2,
+ "hard": self.kh2_list_any_sum([defensive_tool], state) >= 1,
+ }
+ return dark_thorn_rules[self.fight_logic]
+
+ def get_xaldin_rules(self, state: CollectionState) -> bool:
+ # easy:guard,2 aerial modifier,valor/master/final
+ # normal:guard,1 aerial modifier
+ # hard:guard
+ xaldin_rules = {
+ "easy": self.kh2_list_any_sum([[ItemName.Guard], [ItemName.ValorForm, ItemName.MasterForm, ItemName.FinalForm]], state) >= 2 and self.kh2_list_count_sum(aerial_move, state) >= 2,
+ "normal": self.kh2_list_any_sum([aerial_move], state) >= 1 and state.has(ItemName.Guard, self.player),
+ "hard": state.has(ItemName.Guard, self.player),
+ }
+ return xaldin_rules[self.fight_logic]
+
+ def get_data_xaldin_rules(self, state: CollectionState) -> bool:
+ # easy:final 7,firaga,2 air combo plus, finishing plus,guard,reflega,donald limit,high jump aerial dodge glide lvl 3,magnet,aerial dive,aerial spiral,hori slash,berserk charge
+ # normal:final 7,firaga, finishing plus,guard,reflega,donald limit,high jump aerial dodge glide lvl 3,magnet,aerial dive,aerial spiral,hori slash
+ # hard:final 5, fira, party limit, finishing plus,guard,high jump aerial dodge glide lvl 2,magnet,aerial dive
+ data_xaldin_rules = {
+ "easy": self.kh2_dict_count(easy_data_xaldin, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True),
+ "normal": self.kh2_dict_count(normal_data_xaldin, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True),
+ "hard": self.kh2_dict_count(hard_data_xaldin, state) and self.form_list_unlock(state, ItemName.FinalForm, 3, True) and self.kh2_has_any(party_limit, state),
+ }
+ return data_xaldin_rules[self.fight_logic]
+
+ def get_hostile_program_rules(self, state: CollectionState) -> bool:
+ # easy:donald limit,reflect,drive form,summon
+ # normal:3 of those things
+ # hard: 2 of those things
+ hostile_program_rules = {
+ "easy": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 4,
+ "normal": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 3,
+ "hard": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 2,
+ }
+ return hostile_program_rules[self.fight_logic]
+
+ def get_mcp_rules(self, state: CollectionState) -> bool:
+ # easy:donald limit,reflect,drive form,summon
+ # normal:3 of those things
+ # hard: 2 of those things
+ mcp_rules = {
+ "easy": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 4,
+ "normal": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 3,
+ "hard": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 2,
+ }
+ return mcp_rules[self.fight_logic]
+
+ def get_data_larxene_rules(self, state: CollectionState) -> bool:
+ # easy: final 7,firaga,scom,both donald limits, Reflega,guard,2 gap closers,2 ground finishers,aerial dodge 3,glide 3
+ # normal:final 7,firaga, donald limit, Reflega ,guard,1 gap closers,1 ground finisher,aerial dodge 3,glide 3
+ # hard:final 5,fira, donald limit, reflect,gap closer,aerial dodge 2,glide 2
+ data_larxene_rules = {
+ "easy": self.kh2_dict_count(easy_data_larxene, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True),
+ "normal": self.kh2_dict_count(normal_data_larxene, state) and self.kh2_list_any_sum([gap_closer, ground_finisher, donald_limit], state) >= 3 and self.form_list_unlock(state, ItemName.FinalForm, 5, True),
+ "hard": self.kh2_dict_count(hard_data_larxene, state) and self.kh2_list_any_sum([gap_closer, donald_limit], state) >= 2 and self.form_list_unlock(state, ItemName.FinalForm, 3, True),
+ }
+ return data_larxene_rules[self.fight_logic]
+
+ def get_prison_keeper_rules(self, state: CollectionState) -> bool:
+ # easy:defensive tool,drive form, party limit
+ # normal:two of those things
+ # hard:one of those things
+ prison_keeper_rules = {
+ "easy": self.kh2_list_any_sum([defensive_tool, form_list, party_limit], state) >= 3,
+ "normal": self.kh2_list_any_sum([defensive_tool, form_list, party_limit], state) >= 2,
+ "hard": self.kh2_list_any_sum([defensive_tool, form_list, party_limit], state) >= 1,
+ }
+ return prison_keeper_rules[self.fight_logic]
+
+ @staticmethod
+ def get_oogie_rules():
+ # fight is free
+ return True
+
+ def get_experiment_rules(self, state: CollectionState) -> bool:
+ # easy:drive form,defensive tool,summon,party limit
+ # normal:3 of those things
+ # hard 2 of those things
+ experiment_rules = {
+ "easy": self.kh2_list_any_sum([form_list, defensive_tool, party_limit, summons], state) >= 4,
+ "normal": self.kh2_list_any_sum([form_list, defensive_tool, party_limit, summons], state) >= 3,
+ "hard": self.kh2_list_any_sum([form_list, defensive_tool, party_limit, summons], state) >= 2,
+ }
+ return experiment_rules[self.fight_logic]
+
+ def get_data_vexen_rules(self, state: CollectionState) -> bool:
+ # easy: final 7,firaga,scom,both donald limits, Reflega,guard,2 gap closers,2 ground finishers,aerial dodge 3,glide 3,dodge roll 3,quick run 3
+ # normal:final 7,firaga, donald limit, Reflega,guard,1 gap closers,1 ground finisher,aerial dodge 3,glide 3,dodge roll 3,quick run 3
+ # hard:final 5,fira, donald limit, reflect,gap closer,aerial dodge 2,glide 2,dodge roll 2,quick run 2
+ data_vexen_rules = {
+ "easy": self.kh2_dict_count(easy_data_vexen, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True),
+ "normal": self.kh2_dict_count(normal_data_vexen, state) and self.kh2_list_any_sum([gap_closer, ground_finisher, donald_limit], state) >= 3 and self.form_list_unlock(state, ItemName.FinalForm, 5, True),
+ "hard": self.kh2_dict_count(hard_data_vexen, state) and self.kh2_list_any_sum([gap_closer, donald_limit], state) >= 2 and self.form_list_unlock(state, ItemName.FinalForm, 3, True),
+ }
+ return data_vexen_rules[self.fight_logic]
+
+ def get_demyx_rules(self, state: CollectionState) -> bool:
+ # defensive option,drive form,party limit
+ # defensive option,drive form
+ # defensive option
+ demyx_rules = {
+ "easy": self.kh2_list_any_sum([defensive_tool, form_list, party_limit], state) >= 3,
+ "normal": self.kh2_list_any_sum([defensive_tool, form_list], state) >= 2,
+ "hard": self.kh2_list_any_sum([defensive_tool], state) >= 1,
+ }
+ return demyx_rules[self.fight_logic]
+
+ def get_thousand_heartless_rules(self, state: CollectionState) -> bool:
+ # easy:scom,limit form,guard,magnera
+ # normal:limit form, guard
+ # hard:guard
+ thousand_heartless_rules = {
+ "easy": self.kh2_dict_count(easy_thousand_heartless_rules, state),
+ "normal": self.kh2_dict_count(normal_thousand_heartless_rules, state),
+ "hard": state.has(ItemName.Guard, self.player),
+ }
+ return thousand_heartless_rules[self.fight_logic]
+
+ def get_data_demyx_rules(self, state: CollectionState) -> bool:
+ # easy:wisdom 7,1 form boosts,reflera,firaga,duck flare,guard,scom,finishing plus
+ # normal:remove form boost and scom
+ # hard:wisdom 6,reflect,guard,duck flare,fira,finishing plus
+ data_demyx_rules = {
+ "easy": self.kh2_dict_count(easy_data_demyx, state) and self.form_list_unlock(state, ItemName.WisdomForm, 5, True),
+ "normal": self.kh2_dict_count(normal_data_demyx, state) and self.form_list_unlock(state, ItemName.WisdomForm, 5, True),
+ "hard": self.kh2_dict_count(hard_data_demyx, state) and self.form_list_unlock(state, ItemName.WisdomForm, 4, True),
+ }
+ return data_demyx_rules[self.fight_logic]
+
+ def get_sephiroth_rules(self, state: CollectionState) -> bool:
+ # easy:both gap closers,limit 5,reflega,guard,both 2 ground finishers,3 dodge roll,finishing plus,scom
+ # normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus
+ # hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus
+ sephiroth_rules = {
+ "easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1,
+ "normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2,
+ "hard": self.kh2_dict_count(hard_sephiroth_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2,
+ }
+ return sephiroth_rules[self.fight_logic]
+
+ def get_cor_first_fight_movement_rules(self, state: CollectionState) -> bool:
+ # easy: quick run 3 or wisdom 5 (wisdom has qr 3)
+ # normal: quick run 2 and aerial dodge 1 or wisdom 5 (wisdom has qr 3)
+ # hard: (quick run 1, aerial dodge 1) or (wisdom form and aerial dodge 1)
+ cor_first_fight_movement_rules = {
+ "easy": state.has(ItemName.QuickRun, self.player, 3) or self.form_list_unlock(state, ItemName.WisdomForm, 3, True),
+ "normal": self.kh2_dict_count({ItemName.QuickRun: 2, ItemName.AerialDodge: 1}, state) or self.form_list_unlock(state, ItemName.WisdomForm, 3, True),
+ "hard": self.kh2_has_all([ItemName.AerialDodge, ItemName.QuickRun], state) or self.kh2_has_all([ItemName.AerialDodge, ItemName.WisdomForm], state),
+ }
+ return cor_first_fight_movement_rules[self.fight_logic]
+
+ def get_cor_first_fight_rules(self, state: CollectionState) -> bool:
+ # easy:have 5 of these things (reflega,stitch and chicken,final form,magnera,explosion,thundara)
+ # normal:have 3 of these things (reflega,stitch and chicken,final form,magnera,explosion,thundara)
+ # hard: reflect,stitch or chicken,final form
+ cor_first_fight_rules = {
+ "easy": self.kh2_dict_one_count(not_hard_cor_tools_dict, state) >= 5 or self.kh2_dict_one_count(not_hard_cor_tools_dict, state) >= 4 and self.form_list_unlock(state, ItemName.FinalForm, 1, True),
+ "normal": self.kh2_dict_one_count(not_hard_cor_tools_dict, state) >= 3 or self.kh2_dict_one_count(not_hard_cor_tools_dict, state) >= 2 and self.form_list_unlock(state, ItemName.FinalForm, 1, True),
+ "hard": state.has(ItemName.ReflectElement, self.player) and self.kh2_has_any([ItemName.Stitch, ItemName.ChickenLittle], state) and self.form_list_unlock(state, ItemName.FinalForm, 1, True),
+ }
+ return cor_first_fight_rules[self.fight_logic]
+
+ def get_cor_skip_first_rules(self, state: CollectionState) -> bool:
+ # if option is not allow skips return false else run rules
+ if not self.world.options.CorSkipToggle:
+ return False
+ # easy: aerial dodge 3,master form,fire
+ # normal: aerial dodge 2, master form,fire
+ # hard:void cross(quick run 3,aerial dodge 1)
+ # or (quick run 2,aerial dodge 2 and magic)
+ # or (final form and (magic or combo master))
+ # or (master form and (reflect or fire or thunder or combo master)
+ # wall rise(aerial dodge 1 and (final form lvl 3 or glide 2) or (master form and (1 of black magic or combo master)
+ void_cross_rules = {
+ "easy": state.has(ItemName.AerialDodge, self.player, 3) and self.kh2_has_all([ItemName.MasterForm, ItemName.FireElement], state),
+ "normal": state.has(ItemName.AerialDodge, self.player, 2) and self.kh2_has_all([ItemName.MasterForm, ItemName.FireElement], state),
+ "hard": (self.kh2_dict_count({ItemName.QuickRun: 3, ItemName.AerialDodge: 1}, state)) \
+ or (self.kh2_dict_count({ItemName.QuickRun: 2, ItemName.AerialDodge: 2}, state) and self.kh2_has_any(magic, state)) \
+ or (state.has(ItemName.FinalForm, self.player) and (self.kh2_has_any(magic, state) or state.has(ItemName.ComboMaster, self.player))) \
+ or (state.has(ItemName.MasterForm, self.player) and (self.kh2_has_any([ItemName.ReflectElement, ItemName.FireElement, ItemName.ComboMaster], state)))
+ }
+ wall_rise_rules = {
+ "easy": True,
+ "normal": True,
+ "hard": state.has(ItemName.AerialDodge, self.player) and (self.form_list_unlock(state, ItemName.FinalForm, 1, True) or state.has(ItemName.Glide, self.player, 2))
+ }
+ return void_cross_rules[self.fight_logic] and wall_rise_rules[self.fight_logic]
+
+ def get_cor_second_fight_movement_rules(self, state: CollectionState) -> bool:
+ # easy: quick run 2, aerial dodge 3 or master form 5
+ # normal: quick run 2, aerial dodge 2 or master 5
+ # hard: (glide 1,aerial dodge 1 any magic) or (master 3 any magic) or glide 1 and aerial dodge 2
+
+ cor_second_fight_movement_rules = {
+ "easy": self.kh2_dict_count({ItemName.QuickRun: 2, ItemName.AerialDodge: 3}, state) or self.form_list_unlock(state, ItemName.MasterForm, 3, True),
+ "normal": self.kh2_dict_count({ItemName.QuickRun: 2, ItemName.AerialDodge: 2}, state) or self.form_list_unlock(state, ItemName.MasterForm, 3, True),
+ "hard": (self.kh2_has_all([ItemName.Glide, ItemName.AerialDodge], state) and self.kh2_has_any(magic, state)) \
+ or (state.has(ItemName.MasterForm, self.player) and self.kh2_has_any(magic, state)) \
+ or (state.has(ItemName.Glide, self.player) and state.has(ItemName.AerialDodge, self.player, 2)),
+ }
+ return cor_second_fight_movement_rules[self.fight_logic]
+
+ def get_transport_fight_rules(self, state: CollectionState) -> bool:
+ # easy: reflega,stitch and chicken,final form,magnera,explosion,finishing leap,thundaga,2 donald limits
+ # normal: 7 of those things
+ # hard: 5 of those things
+ transport_fight_rules = {
+ "easy": self.kh2_dict_count(transport_tools_dict, state),
+ "normal": self.kh2_dict_one_count(transport_tools_dict, state) >= 7,
+ "hard": self.kh2_dict_one_count(transport_tools_dict, state) >= 5,
+ }
+ return transport_fight_rules[self.fight_logic]
+
+ def get_transport_movement_rules(self, state: CollectionState) -> bool:
+ # easy:high jump 3,aerial dodge 3,glide 3
+ # normal: high jump 2,glide 3,aerial dodge 2
+ # hard: (hj 2,glide 2,ad 1,any magic) or hj 1,glide 2,ad 3 any magic or (any magic master form,ad) or hj lvl 1,glide 3,ad 1
+ transport_movement_rules = {
+ "easy": self.kh2_dict_count({ItemName.HighJump: 3, ItemName.AerialDodge: 3, ItemName.Glide: 3}, state),
+ "normal": self.kh2_dict_count({ItemName.HighJump: 2, ItemName.AerialDodge: 2, ItemName.Glide: 3}, state),
+ "hard": (self.kh2_dict_count({ItemName.HighJump: 2, ItemName.AerialDodge: 1, ItemName.Glide: 2}, state) and self.kh2_has_any(magic, state)) \
+ or (self.kh2_dict_count({ItemName.HighJump: 1, ItemName.Glide: 2, ItemName.AerialDodge: 3}, state) and self.kh2_has_any(magic, state)) \
+ or (self.kh2_dict_count({ItemName.HighJump: 1, ItemName.Glide: 3, ItemName.AerialDodge: 1}, state)) \
+ or (self.kh2_has_all([ItemName.MasterForm, ItemName.AerialDodge], state) and self.kh2_has_any(magic, state)),
+ }
+ return transport_movement_rules[self.fight_logic]
+
+ def get_scar_rules(self, state: CollectionState) -> bool:
+ # easy: reflect,thunder,fire
+ # normal:reflect,fire
+ # hard:reflect
+ scar_rules = {
+ "easy": self.kh2_has_all([ItemName.ReflectElement, ItemName.ThunderElement, ItemName.FireElement], state),
+ "normal": self.kh2_has_all([ItemName.ReflectElement, ItemName.FireElement], state),
+ "hard": state.has(ItemName.ReflectElement, self.player),
+ }
+ return scar_rules[self.fight_logic]
+
+ def get_groundshaker_rules(self, state: CollectionState) -> bool:
+ # easy:berserk charge,cure,2 air combo plus,reflect
+ # normal:berserk charge,reflect,cure
+ # hard:berserk charge or 2 air combo plus. reflect
+ groundshaker_rules = {
+ "easy": state.has(ItemName.AirComboPlus, self.player, 2) and self.kh2_has_all([ItemName.BerserkCharge, ItemName.CureElement, ItemName.ReflectElement], state),
+ "normal": self.kh2_has_all([ItemName.BerserkCharge, ItemName.ReflectElement, ItemName.CureElement], state),
+ "hard": (state.has(ItemName.BerserkCharge, self.player) or state.has(ItemName.AirComboPlus, self.player, 2)) and state.has(ItemName.ReflectElement, self.player),
+ }
+ return groundshaker_rules[self.fight_logic]
+
+ def get_data_saix_rules(self, state: CollectionState) -> bool:
+ # easy:guard,2 gap closers,thunder,blizzard,2 donald limit,reflega,2 ground finisher,aerial dodge 3,glide 3,final 7,firaga,scom
+ # normal:guard,1 gap closers,thunder,blizzard,1 donald limit,reflega,1 ground finisher,aerial dodge 3,glide 3,final 7,firaga
+ # hard:aerial dodge 3,glide 3,guard,reflect,blizzard,1 gap closer,1 ground finisher
+ easy_data_rules = {
+ "easy": self.kh2_dict_count(easy_data_saix, state) and self.form_list_unlock(state, ItemName.FinalForm, 5),
+ "normal": self.kh2_dict_count(normal_data_saix, state) and self.kh2_list_any_sum([gap_closer, ground_finisher, donald_limit], state) >= 3 and self.form_list_unlock(state, ItemName.FinalForm, 5),
+ "hard": self.kh2_dict_count(hard_data_saix, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2
+ }
+ return easy_data_rules[self.fight_logic]
+
+ @staticmethod
+ def get_twilight_thorn_rules() -> bool:
+ return True
+
+ @staticmethod
+ def get_axel_one_rules() -> bool:
+ return True
+
+ @staticmethod
+ def get_axel_two_rules() -> bool:
+ return True
+
+ def get_data_roxas_rules(self, state: CollectionState) -> bool:
+ # easy:both gap closers,limit 5,reflega,guard,both 2 ground finishers,3 dodge roll,finishing plus,scom
+ # normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus
+ # hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus
+ data_roxas_rules = {
+ "easy": self.kh2_dict_count(easy_data_roxas_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1,
+ "normal": self.kh2_dict_count(normal_data_roxas_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2,
+ "hard": self.kh2_dict_count(hard_data_roxas_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2
+ }
+ return data_roxas_rules[self.fight_logic]
+
+ def get_data_axel_rules(self, state: CollectionState) -> bool:
+ # easy:both gap closers,limit 5,reflega,guard,both 2 ground finishers,3 dodge roll,finishing plus,scom,blizzaga
+ # normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus,blizzaga
+ # hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus,blizzara
+ data_axel_rules = {
+ "easy": self.kh2_dict_count(easy_data_axel_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1,
+ "normal": self.kh2_dict_count(normal_data_axel_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2,
+ "hard": self.kh2_dict_count(hard_data_axel_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2
+ }
+ return data_axel_rules[self.fight_logic]
+
+ def get_roxas_rules(self, state: CollectionState) -> bool:
+ # easy:aerial dodge 1,glide 1, limit form,thunder,reflera,guard break,2 gap closers,finishing plus,blizzard
+ # normal:thunder,reflera,guard break,2 gap closers,finishing plus,blizzard
+ # hard:guard
+ roxas_rules = {
+ "easy": self.kh2_dict_count(easy_roxas_tools, state),
+ "normal": self.kh2_dict_count(normal_roxas_tools, state),
+ "hard": state.has(ItemName.Guard, self.player),
+ }
+ return roxas_rules[self.fight_logic]
+
+ def get_xigbar_rules(self, state: CollectionState) -> bool:
+ # easy:final 4,horizontal slash,fira,finishing plus,glide 2,aerial dodge 2,quick run 2,guard,reflect
+ # normal:final 4,fira,finishing plus,glide 2,aerial dodge 2,quick run 2,guard,reflect
+ # hard:guard,quick run,finishing plus
+ xigbar_rules = {
+ "easy": self.kh2_dict_count(easy_xigbar_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 1) and self.kh2_has_any([ItemName.LightDarkness, ItemName.FinalForm], state),
+ "normal": self.kh2_dict_count(normal_xigbar_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 1),
+ "hard": self.kh2_has_all([ItemName.Guard, ItemName.QuickRun, ItemName.FinishingPlus], state),
+ }
+ return xigbar_rules[self.fight_logic]
+
+ def get_luxord_rules(self, state: CollectionState) -> bool:
+ # easy:aerial dodge 1,glide 1,quickrun 2,guard,reflera,2 gap closers,ground finisher,limit form
+ # normal:aerial dodge 1,glide 1,quickrun 2,guard,reflera,1 gap closers,ground finisher
+ # hard:quick run,guard
+ luxord_rules = {
+ "easy": self.kh2_dict_count(easy_luxord_tools, state) and self.kh2_has_any(ground_finisher, state),
+ "normal": self.kh2_dict_count(normal_luxord_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2,
+ "hard": self.kh2_has_all([ItemName.Guard, ItemName.QuickRun], state)
+ }
+ return luxord_rules[self.fight_logic]
+
+ def get_saix_rules(self, state: CollectionState) -> bool:
+ # easy:aerial dodge 1,glide 1,quickrun 2,guard,reflera,2 gap closers,ground finisher,limit form
+ # normal:aerial dodge 1,glide 1,quickrun 2,guard,reflera,1 gap closers,ground finisher
+ # hard:,guard
+
+ saix_rules = {
+ "easy": self.kh2_dict_count(easy_saix_tools, state) and self.kh2_has_any(ground_finisher, state),
+ "normal": self.kh2_dict_count(normal_saix_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2,
+ "hard": self.kh2_has_all([ItemName.Guard], state)
+ }
+ return saix_rules[self.fight_logic]
+
+ def get_xemnas_rules(self, state: CollectionState) -> bool:
+ # easy:aerial dodge 1,glide 1,quickrun 2,guard,reflera,2 gap closers,ground finisher,limit form
+ # normal:aerial dodge 1,glide 1,quickrun 2,guard,reflera,1 gap closers,ground finisher
+ # hard:,guard
+ xemnas_rules = {
+ "easy": self.kh2_dict_count(easy_xemnas_tools, state) and self.kh2_has_any(ground_finisher, state),
+ "normal": self.kh2_dict_count(normal_xemnas_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2,
+ "hard": self.kh2_has_all([ItemName.Guard], state)
+ }
+ return xemnas_rules[self.fight_logic]
- # Forbid Abilities on popups due to game limitations
- for location in exclusion_table["Popups"]:
- forbid_items(world.get_location(location, player), exclusionItem_table["Ability"])
- forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"])
+ def get_armored_xemnas_one_rules(self, state: CollectionState) -> bool:
+ # easy:donald limit,reflect,1 gap closer,ground finisher
+ # normal:reflect,gap closer,ground finisher
+ # hard:reflect
+ armored_xemnas_one_rules = {
+ "easy": self.kh2_list_any_sum([donald_limit, gap_closer, ground_finisher, {ItemName.ReflectElement}], state) >= 4,
+ "normal": self.kh2_list_any_sum([gap_closer, ground_finisher, {ItemName.ReflectElement}], state) >= 3,
+ "hard": state.has(ItemName.ReflectElement, self.player),
+ }
+ return armored_xemnas_one_rules[self.fight_logic]
- for location in STT_Checks:
- forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"])
+ def get_armored_xemnas_two_rules(self, state: CollectionState) -> bool:
+ # easy:donald limit,reflect,1 gap closer,ground finisher
+ # normal:reflect,gap closer,ground finisher
+ # hard:reflect
+ armored_xemnas_two_rules = {
+ "easy": self.kh2_list_any_sum([gap_closer, ground_finisher, {ItemName.ReflectElement}, {ItemName.ThunderElement}], state) >= 4,
+ "normal": self.kh2_list_any_sum([gap_closer, ground_finisher, {ItemName.ReflectElement}], state) >= 3,
+ "hard": state.has(ItemName.ReflectElement, self.player),
+ }
+ return armored_xemnas_two_rules[self.fight_logic]
- # Santa's house also breaks with stat ups
- for location in {LocationName.SantasHouseChristmasTownMap, LocationName.SantasHouseAPBoost}:
- forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"])
+ def get_final_xemnas_rules(self, state: CollectionState) -> bool:
+ # easy:reflera,limit form,finishing plus,gap closer,guard
+ # normal:reflect,finishing plus,guard
+ # hard:guard
+ final_xemnas_rules = {
+ "easy": self.kh2_has_all([ItemName.LimitForm, ItemName.FinishingPlus, ItemName.Guard], state) and state.has(ItemName.ReflectElement, self.player, 2) and self.kh2_has_any(gap_closer, state),
+ "normal": self.kh2_has_all([ItemName.ReflectElement, ItemName.FinishingPlus, ItemName.Guard], state),
+ "hard": state.has(ItemName.Guard, self.player),
+ }
+ return final_xemnas_rules[self.fight_logic]
- add_rule(world.get_location(LocationName.TransporttoRemembrance, player),
- lambda state: state.kh_transport(player))
+ def get_data_xemnas_rules(self, state: CollectionState) -> bool:
+ # easy:combo master,slapshot,reflega,2 ground finishers,both gap closers,finishing plus,guard,limit 5,scom,trinity limit
+ # normal:combo master,slapshot,reflega,2 ground finishers,both gap closers,finishing plus,guard,limit 5,
+ # hard:combo master,slapshot,reflera,1 ground finishers,1 gap closers,finishing plus,guard,limit form
+ data_xemnas_rules = {
+ "easy": self.kh2_dict_count(easy_data_xemnas, state) and self.kh2_list_count_sum(ground_finisher, state) >= 2 and self.kh2_can_reach(LocationName.Limitlvl5, state),
+ "normal": self.kh2_dict_count(normal_data_xemnas, state) and self.kh2_list_count_sum(ground_finisher, state) >= 2 and self.kh2_can_reach(LocationName.Limitlvl5, state),
+ "hard": self.kh2_dict_count(hard_data_xemnas, state) and self.kh2_list_any_sum([ground_finisher, gap_closer], state) >= 2
+ }
+ return data_xemnas_rules[self.fight_logic]
diff --git a/worlds/kh2/Subclasses.py b/worlds/kh2/Subclasses.py
new file mode 100644
index 000000000000..79f52c41c02a
--- /dev/null
+++ b/worlds/kh2/Subclasses.py
@@ -0,0 +1,29 @@
+import typing
+
+from BaseClasses import Location, Item
+
+
+class KH2Location(Location):
+ game: str = "Kingdom Hearts 2"
+
+
+class LocationData(typing.NamedTuple):
+ locid: int
+ yml: str
+ charName: str = "Sora"
+ charNumber: int = 1
+
+
+class KH2Item(Item):
+ game: str = "Kingdom Hearts 2"
+
+
+class ItemData(typing.NamedTuple):
+ quantity: int = 0
+ kh2id: int = 0
+ # Save+ mem addr
+ memaddr: int = 0
+ # some items have bitmasks. if bitmask>0 bitor to give item else
+ bitmask: int = 0
+ # if ability then
+ ability: bool = False
diff --git a/worlds/kh2/WorldLocations.py b/worlds/kh2/WorldLocations.py
index 172874c2b71a..6df18fc800e3 100644
--- a/worlds/kh2/WorldLocations.py
+++ b/worlds/kh2/WorldLocations.py
@@ -96,6 +96,10 @@ class WorldLocationData(typing.NamedTuple):
LocationName.LingeringWillBonus: WorldLocationData(0x370C, 6),
LocationName.LingeringWillProofofConnection: WorldLocationData(0x370C, 6),
LocationName.LingeringWillManifestIllusion: WorldLocationData(0x370C, 6),
+
+ 'Lingering Will Bonus: Sora Slot 1': WorldLocationData(14092, 6),
+ 'Lingering Will Proof of Connection': WorldLocationData(14092, 6),
+ 'Lingering Will Manifest Illusion': WorldLocationData(14092, 6),
}
TR_Checks = {
LocationName.CornerstoneHillMap: WorldLocationData(0x23B2, 0),
@@ -226,6 +230,8 @@ class WorldLocationData(typing.NamedTuple):
LocationName.DonaldXaldinGetBonus: WorldLocationData(0x3704, 4),
LocationName.SecretAnsemReport4: WorldLocationData(0x1D31, 2),
LocationName.XaldinDataDefenseBoost: WorldLocationData(0x1D34, 7),
+
+ 'Data Xaldin': WorldLocationData(7476, 7),
}
SP_Checks = {
LocationName.PitCellAreaMap: WorldLocationData(0x23CA, 2),
@@ -351,6 +357,7 @@ class WorldLocationData(typing.NamedTuple):
LocationName.RestorationSiteMoonRecipe: WorldLocationData(0x23C9, 3),
LocationName.RestorationSiteAPBoost: WorldLocationData(0x23DB, 2),
LocationName.DemyxHB: WorldLocationData(0x3707, 4),
+ '(HB) Demyx Bonus: Donald Slot 1': WorldLocationData(14087, 4),
LocationName.DemyxHBGetBonus: WorldLocationData(0x3707, 4),
LocationName.DonaldDemyxHBGetBonus: WorldLocationData(0x3707, 4),
LocationName.FFFightsCureElement: WorldLocationData(0x1D14, 6),
@@ -409,6 +416,25 @@ class WorldLocationData(typing.NamedTuple):
LocationName.VexenASRoadtoDiscovery: WorldLocationData(0x370C, 0),
LocationName.VexenDataLostIllusion: WorldLocationData(0x370C, 0), #
LocationName.DemyxDataAPBoost: WorldLocationData(0x1D26, 5),
+
+ 'Lexaeus Bonus: Sora Slot 1': WorldLocationData(14092, 1),
+ 'AS Lexaeus': WorldLocationData(14092, 1),
+ 'Data Lexaeus': WorldLocationData(14092, 1),
+ 'Marluxia Bonus: Sora Slot 1': WorldLocationData(14092, 3),
+ 'AS Marluxia': WorldLocationData(14092, 3),
+ 'Data Marluxia': WorldLocationData(14092, 3),
+ 'Zexion Bonus: Sora Slot 1': WorldLocationData(14092, 2),
+ 'Zexion Bonus: Goofy Slot 1': WorldLocationData(14092, 2),
+ 'AS Zexion': WorldLocationData(14092, 2),
+ 'Data Zexion': WorldLocationData(14092, 2),
+ 'Larxene Bonus: Sora Slot 1': WorldLocationData(14092, 4),
+ 'AS Larxene': WorldLocationData(14092, 4),
+ 'Data Larxene': WorldLocationData(14092, 4),
+ 'Vexen Bonus: Sora Slot 1': WorldLocationData(14092, 0),
+ 'AS Vexen': WorldLocationData(14092, 0),
+ 'Data Vexen': WorldLocationData(14092, 0),
+ 'Data Demyx': WorldLocationData(7462, 5),
+
LocationName.GardenofAssemblageMap: WorldLocationData(0x23DF, 1),
LocationName.GoALostIllusion: WorldLocationData(0x23DF, 2),
LocationName.ProofofNonexistence: WorldLocationData(0x23DF, 3),
@@ -549,50 +575,97 @@ class WorldLocationData(typing.NamedTuple):
LocationName.BetwixtandBetween: WorldLocationData(0x370B, 7),
LocationName.BetwixtandBetweenBondofFlame: WorldLocationData(0x1CE9, 1),
LocationName.AxelDataMagicBoost: WorldLocationData(0x1CEB, 4),
+
+ 'Data Axel': WorldLocationData(7403, 4),
}
TWTNW_Checks = {
- LocationName.FragmentCrossingMythrilStone: WorldLocationData(0x23CB, 4),
- LocationName.FragmentCrossingMythrilCrystal: WorldLocationData(0x23CB, 5),
- LocationName.FragmentCrossingAPBoost: WorldLocationData(0x23CB, 6),
- LocationName.FragmentCrossingOrichalcum: WorldLocationData(0x23CB, 7),
- LocationName.Roxas: WorldLocationData(0x370C, 5),
- LocationName.RoxasGetBonus: WorldLocationData(0x370C, 5),
- LocationName.RoxasSecretAnsemReport8: WorldLocationData(0x1ED1, 1),
- LocationName.TwoBecomeOne: WorldLocationData(0x1ED1, 1),
- LocationName.MemorysSkyscaperMythrilCrystal: WorldLocationData(0x23CD, 3),
- LocationName.MemorysSkyscaperAPBoost: WorldLocationData(0x23DC, 0),
- LocationName.MemorysSkyscaperMythrilStone: WorldLocationData(0x23DC, 1),
- LocationName.TheBrinkofDespairDarkCityMap: WorldLocationData(0x23CA, 5),
- LocationName.TheBrinkofDespairOrichalcumPlus: WorldLocationData(0x23DA, 2),
- LocationName.NothingsCallMythrilGem: WorldLocationData(0x23CC, 0),
- LocationName.NothingsCallOrichalcum: WorldLocationData(0x23CC, 1),
- LocationName.TwilightsViewCosmicBelt: WorldLocationData(0x23CA, 6),
- LocationName.XigbarBonus: WorldLocationData(0x3706, 7),
- LocationName.XigbarSecretAnsemReport3: WorldLocationData(0x1ED2, 2),
- LocationName.NaughtsSkywayMythrilGem: WorldLocationData(0x23CC, 2),
- LocationName.NaughtsSkywayOrichalcum: WorldLocationData(0x23CC, 3),
- LocationName.NaughtsSkywayMythrilCrystal: WorldLocationData(0x23CC, 4),
- LocationName.Oblivion: WorldLocationData(0x1ED2, 4),
- LocationName.CastleThatNeverWasMap: WorldLocationData(0x1ED2, 4),
- LocationName.Luxord: WorldLocationData(0x3707, 0),
- LocationName.LuxordGetBonus: WorldLocationData(0x3707, 0),
- LocationName.LuxordSecretAnsemReport9: WorldLocationData(0x1ED2, 7),
- LocationName.SaixBonus: WorldLocationData(0x3707, 1),
- LocationName.SaixSecretAnsemReport12: WorldLocationData(0x1ED3, 2),
- LocationName.PreXemnas1SecretAnsemReport11: WorldLocationData(0x1ED3, 6),
- LocationName.RuinandCreationsPassageMythrilStone: WorldLocationData(0x23CC, 7),
- LocationName.RuinandCreationsPassageAPBoost: WorldLocationData(0x23CD, 0),
- LocationName.RuinandCreationsPassageMythrilCrystal: WorldLocationData(0x23CD, 1),
- LocationName.RuinandCreationsPassageOrichalcum: WorldLocationData(0x23CD, 2),
- LocationName.Xemnas1: WorldLocationData(0x3707, 2),
- LocationName.Xemnas1GetBonus: WorldLocationData(0x3707, 2),
- LocationName.Xemnas1SecretAnsemReport13: WorldLocationData(0x1ED4, 5),
- LocationName.FinalXemnas: WorldLocationData(0x1ED8, 1),
- LocationName.XemnasDataPowerBoost: WorldLocationData(0x1EDA, 2),
- LocationName.XigbarDataDefenseBoost: WorldLocationData(0x1ED9, 7),
- LocationName.SaixDataDefenseBoost: WorldLocationData(0x1EDA, 0),
- LocationName.LuxordDataAPBoost: WorldLocationData(0x1EDA, 1),
- LocationName.RoxasDataMagicBoost: WorldLocationData(0x1ED9, 6),
+ LocationName.FragmentCrossingMythrilStone: WorldLocationData(0x23CB, 4),
+ LocationName.FragmentCrossingMythrilCrystal: WorldLocationData(0x23CB, 5),
+ LocationName.FragmentCrossingAPBoost: WorldLocationData(0x23CB, 6),
+ LocationName.FragmentCrossingOrichalcum: WorldLocationData(0x23CB, 7),
+ LocationName.Roxas: WorldLocationData(0x370C, 5),
+ LocationName.RoxasGetBonus: WorldLocationData(0x370C, 5),
+ LocationName.RoxasSecretAnsemReport8: WorldLocationData(0x1ED1, 1),
+ LocationName.TwoBecomeOne: WorldLocationData(0x1ED1, 1),
+ LocationName.MemorysSkyscaperMythrilCrystal: WorldLocationData(0x23CD, 3),
+ LocationName.MemorysSkyscaperAPBoost: WorldLocationData(0x23DC, 0),
+ LocationName.MemorysSkyscaperMythrilStone: WorldLocationData(0x23DC, 1),
+ LocationName.TheBrinkofDespairDarkCityMap: WorldLocationData(0x23CA, 5),
+ LocationName.TheBrinkofDespairOrichalcumPlus: WorldLocationData(0x23DA, 2),
+ LocationName.NothingsCallMythrilGem: WorldLocationData(0x23CC, 0),
+ LocationName.NothingsCallOrichalcum: WorldLocationData(0x23CC, 1),
+ LocationName.TwilightsViewCosmicBelt: WorldLocationData(0x23CA, 6),
+ LocationName.XigbarBonus: WorldLocationData(0x3706, 7),
+ LocationName.XigbarSecretAnsemReport3: WorldLocationData(0x1ED2, 2),
+ LocationName.NaughtsSkywayMythrilGem: WorldLocationData(0x23CC, 2),
+ LocationName.NaughtsSkywayOrichalcum: WorldLocationData(0x23CC, 3),
+ LocationName.NaughtsSkywayMythrilCrystal: WorldLocationData(0x23CC, 4),
+ LocationName.Oblivion: WorldLocationData(0x1ED2, 4),
+ LocationName.CastleThatNeverWasMap: WorldLocationData(0x1ED2, 4),
+ LocationName.Luxord: WorldLocationData(0x3707, 0),
+ LocationName.LuxordGetBonus: WorldLocationData(0x3707, 0),
+ LocationName.LuxordSecretAnsemReport9: WorldLocationData(0x1ED2, 7),
+ LocationName.SaixBonus: WorldLocationData(0x3707, 1),
+ LocationName.SaixSecretAnsemReport12: WorldLocationData(0x1ED3, 2),
+ LocationName.PreXemnas1SecretAnsemReport11: WorldLocationData(0x1ED3, 6),
+ LocationName.RuinandCreationsPassageMythrilStone: WorldLocationData(0x23CC, 7),
+ LocationName.RuinandCreationsPassageAPBoost: WorldLocationData(0x23CD, 0),
+ LocationName.RuinandCreationsPassageMythrilCrystal: WorldLocationData(0x23CD, 1),
+ LocationName.RuinandCreationsPassageOrichalcum: WorldLocationData(0x23CD, 2),
+ LocationName.Xemnas1: WorldLocationData(0x3707, 2),
+ LocationName.Xemnas1GetBonus: WorldLocationData(0x3707, 2),
+ LocationName.Xemnas1SecretAnsemReport13: WorldLocationData(0x1ED4, 5),
+ LocationName.FinalXemnas: WorldLocationData(0x1ED8, 1),
+ LocationName.XemnasDataPowerBoost: WorldLocationData(0x1EDA, 2),
+ LocationName.XigbarDataDefenseBoost: WorldLocationData(0x1ED9, 7),
+ LocationName.SaixDataDefenseBoost: WorldLocationData(0x1EDA, 0),
+ LocationName.LuxordDataAPBoost: WorldLocationData(0x1EDA, 1),
+ LocationName.RoxasDataMagicBoost: WorldLocationData(0x1ED9, 6),
+
+ "(TWTNW) Roxas Bonus: Sora Slot 1": WorldLocationData(14092, 5),
+ "(TWTNW) Roxas Bonus: Sora Slot 2": WorldLocationData(14092, 5),
+ "(TWTNW) Roxas Secret Ansem Report 8": WorldLocationData(7889, 1),
+ "(TWTNW) Two Become One": WorldLocationData(7889, 1),
+ "(TWTNW) Memory's Skyscaper Mythril Crystal": WorldLocationData(9165, 3),
+ "(TWTNW) Memory's Skyscaper AP Boost": WorldLocationData(9180, 0),
+ "(TWTNW) Memory's Skyscaper Mythril Stone": WorldLocationData(9180, 1),
+ "(TWTNW) The Brink of Despair Dark City Map": WorldLocationData(9162, 5),
+ "(TWTNW) The Brink of Despair Orichalcum+": WorldLocationData(9178, 2),
+ "(TWTNW) Nothing's Call Mythril Gem": WorldLocationData(9164, 0),
+ "(TWTNW) Nothing's Call Orichalcum": WorldLocationData(9164, 1),
+ "(TWTNW) Twilight's View Cosmic Belt": WorldLocationData(9162, 6),
+ "(TWTNW) Xigbar Bonus: Sora Slot 1": WorldLocationData(14086, 7),
+ "(TWTNW) Xigbar Secret Ansem Report 3": WorldLocationData(7890, 2),
+ "(TWTNW) Naught's Skyway Mythril Gem": WorldLocationData(9164, 2),
+ "(TWTNW) Naught's Skyway Orichalcum": WorldLocationData(9164, 3),
+ "(TWTNW) Naught's Skyway Mythril Crystal": WorldLocationData(9164, 4),
+ "(TWTNW) Oblivion": WorldLocationData(7890, 4),
+ "(TWTNW) Castle That Never Was Map": WorldLocationData(7890, 4),
+ "(TWTNW) Luxord": WorldLocationData(14087, 0),
+ "(TWTNW) Luxord Bonus: Sora Slot 1": WorldLocationData(14087, 0),
+ "(TWTNW) Luxord Secret Ansem Report 9": WorldLocationData(7890, 7),
+ "(TWTNW) Saix Bonus: Sora Slot 1": WorldLocationData(14087, 1),
+ "(TWTNW) Saix Secret Ansem Report 12": WorldLocationData(7891, 2),
+ "(TWTNW) Secret Ansem Report 11 (Pre-Xemnas 1)": WorldLocationData(7891, 6),
+ "(TWTNW) Ruin and Creation's Passage Mythril Stone": WorldLocationData(9164, 7),
+ "(TWTNW) Ruin and Creation's Passage AP Boost": WorldLocationData(9165, 0),
+ "(TWTNW) Ruin and Creation's Passage Mythril Crystal": WorldLocationData(9165, 1),
+ "(TWTNW) Ruin and Creation's Passage Orichalcum": WorldLocationData(9165, 2),
+ "(TWTNW) Xemnas 1 Bonus: Sora Slot 1": WorldLocationData(14087, 2),
+ "(TWTNW) Xemnas 1 Bonus: Sora Slot 2": WorldLocationData(14087, 2),
+ "(TWTNW) Xemnas 1 Secret Ansem Report 13": WorldLocationData(7892, 5),
+ "Data Xemnas": WorldLocationData(7898, 2),
+ "Data Xigbar": WorldLocationData(7897, 7),
+ "Data Saix": WorldLocationData(7898, 0),
+ "Data Luxord": WorldLocationData(7898, 1),
+ "Data Roxas": WorldLocationData(7897, 6),
+
+}
+Atlantica_Checks = {
+ LocationName.UnderseaKingdomMap: WorldLocationData(0x1DF4, 2),
+ LocationName.MysteriousAbyss: WorldLocationData(0x1DF5, 3),
+ LocationName.MusicalOrichalcumPlus: WorldLocationData(0x1DF4, 1),
+ LocationName.MusicalBlizzardElement: WorldLocationData(0x1DF4, 1)
}
SoraLevels = {
# LocationName.Lvl1: WorldLocationData(0xFFFF,1),
@@ -743,6 +816,15 @@ class WorldLocationData(typing.NamedTuple):
LocationName.Finallvl6: WorldLocationData(0x33D6, 6),
LocationName.Finallvl7: WorldLocationData(0x33D6, 7),
+}
+SummonLevels = {
+ LocationName.Summonlvl2: WorldLocationData(0x3526, 2),
+ LocationName.Summonlvl3: WorldLocationData(0x3526, 3),
+ LocationName.Summonlvl4: WorldLocationData(0x3526, 4),
+ LocationName.Summonlvl5: WorldLocationData(0x3526, 5),
+ LocationName.Summonlvl6: WorldLocationData(0x3526, 6),
+ LocationName.Summonlvl7: WorldLocationData(0x3526, 7),
+
}
weaponSlots = {
LocationName.AdamantShield: WorldLocationData(0x35E6, 1),
@@ -817,7 +899,6 @@ class WorldLocationData(typing.NamedTuple):
all_world_locations = {
**TWTNW_Checks,
**TT_Checks,
- **TT_Checks,
**HB_Checks,
**BC_Checks,
**Oc_Checks,
@@ -828,11 +909,9 @@ class WorldLocationData(typing.NamedTuple):
**DC_Checks,
**TR_Checks,
**HT_Checks,
- **HB_Checks,
**PR_Checks,
**SP_Checks,
- **TWTNW_Checks,
- **HB_Checks,
+ **Atlantica_Checks,
}
levels_locations = {
diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py
index 23075a2084df..faf0bed88567 100644
--- a/worlds/kh2/__init__.py
+++ b/worlds/kh2/__init__.py
@@ -1,15 +1,26 @@
-from BaseClasses import Tutorial, ItemClassification
import logging
+from typing import List
+from BaseClasses import Tutorial, ItemClassification
+from Fill import fill_restrictive
+from worlds.LauncherComponents import Component, components, Type, launch_subprocess
+from worlds.AutoWorld import World, WebWorld
from .Items import *
-from .Locations import all_locations, setup_locations, exclusion_table, AllWeaponSlot
-from .Names import ItemName, LocationName
+from .Locations import *
+from .Names import ItemName, LocationName, RegionName
from .OpenKH import patch_kh2
-from .Options import KH2_Options
+from .Options import KingdomHearts2Options
from .Regions import create_regions, connect_regions
-from .Rules import set_rules
-from ..AutoWorld import World, WebWorld
-from .logic import KH2Logic
+from .Rules import *
+from .Subclasses import KH2Item
+
+
+def launch_client():
+ from .Client import launch
+ launch_subprocess(launch, name="KH2Client")
+
+
+components.append(Component("KH2 Client", "KH2Client", func=launch_client, component_type=Type.CLIENT))
class KingdomHearts2Web(WebWorld):
@@ -23,99 +34,125 @@ class KingdomHearts2Web(WebWorld):
)]
-# noinspection PyUnresolvedReferences
class KH2World(World):
"""
Kingdom Hearts II is an action role-playing game developed and published by Square Enix and released in 2005.
It is the sequel to Kingdom Hearts and Kingdom Hearts: Chain of Memories, and like the two previous games,
focuses on Sora and his friends' continued battle against the Darkness.
"""
- game: str = "Kingdom Hearts 2"
+ game = "Kingdom Hearts 2"
web = KingdomHearts2Web()
- data_version = 1
- required_client_version = (0, 4, 0)
- option_definitions = KH2_Options
- item_name_to_id = {name: data.code for name, data in item_dictionary_table.items()}
- location_name_to_id = {item_name: data.code for item_name, data in all_locations.items() if data.code}
+
+ required_client_version = (0, 4, 4)
+ options_dataclass = KingdomHearts2Options
+ options: KingdomHearts2Options
+ item_name_to_id = {item: item_id
+ for item_id, item in enumerate(item_dictionary_table.keys(), 0x130000)}
+ location_name_to_id = {item: location
+ for location, item in enumerate(all_locations.keys(), 0x130000)}
+
item_name_groups = item_groups
+ location_name_groups = location_groups
+
+ visitlocking_dict: Dict[str, int]
+ plando_locations: Dict[str, str]
+ lucky_emblem_amount: int
+ lucky_emblem_required: int
+ bounties_required: int
+ bounties_amount: int
+ filler_items: List[str]
+ item_quantity_dict: Dict[str, int]
+ local_items: Dict[int, int]
+ sora_ability_dict: Dict[str, int]
+ goofy_ability_dict: Dict[str, int]
+ donald_ability_dict: Dict[str, int]
+ total_locations: int
+
+ # growth_list: list[str]
def __init__(self, multiworld: "MultiWorld", player: int):
super().__init__(multiworld, player)
- self.valid_abilities = None
- self.visitlocking_dict = None
- self.plando_locations = None
- self.luckyemblemamount = None
- self.luckyemblemrequired = None
- self.BountiesRequired = None
- self.BountiesAmount = None
- self.hitlist = None
- self.LocalItems = {}
- self.RandomSuperBoss = list()
- self.filler_items = list()
- self.item_quantity_dict = {}
- self.donald_ability_pool = list()
- self.goofy_ability_pool = list()
- self.sora_keyblade_ability_pool = list()
- self.keyblade_slot_copy = list(Locations.Keyblade_Slots.keys())
- self.keyblade_slot_copy.remove(LocationName.KingdomKeySlot)
- self.totalLocations = len(all_locations.items())
+ # random_super_boss_list List[str]
+ # has to be in __init__ or else other players affect each other's bounties
+ self.random_super_boss_list = list()
self.growth_list = list()
- for x in range(4):
- self.growth_list.extend(Movement_Table.keys())
- self.slotDataDuping = set()
- self.localItems = dict()
+ # lists of KH2Item
+ self.keyblade_ability_pool = list()
+
+ self.goofy_get_bonus_abilities = list()
+ self.goofy_weapon_abilities = list()
+ self.donald_get_bonus_abilities = list()
+ self.donald_weapon_abilities = list()
+
+ self.slot_data_goofy_weapon = dict()
+ self.slot_data_sora_weapon = dict()
+ self.slot_data_donald_weapon = dict()
def fill_slot_data(self) -> dict:
- for values in CheckDupingItems.values():
- if isinstance(values, set):
- self.slotDataDuping = self.slotDataDuping.union(values)
- else:
- for inner_values in values.values():
- self.slotDataDuping = self.slotDataDuping.union(inner_values)
- self.LocalItems = {location.address: item_dictionary_table[location.item.name].code
- for location in self.multiworld.get_filled_locations(self.player)
- if location.item.player == self.player
- and location.item.name in self.slotDataDuping
- and location.name not in AllWeaponSlot}
-
- return {"hitlist": self.hitlist,
- "LocalItems": self.LocalItems,
- "Goal": self.multiworld.Goal[self.player].value,
- "FinalXemnas": self.multiworld.FinalXemnas[self.player].value,
- "LuckyEmblemsRequired": self.multiworld.LuckyEmblemsRequired[self.player].value,
- "BountyRequired": self.multiworld.BountyRequired[self.player].value}
-
- def create_item(self, name: str, ) -> Item:
- data = item_dictionary_table[name]
- if name in Progression_Dicts["Progression"]:
+ for ability in self.slot_data_sora_weapon:
+ if ability in self.sora_ability_dict and self.sora_ability_dict[ability] >= 1:
+ self.sora_ability_dict[ability] -= 1
+ self.donald_ability_dict = {k: v.quantity for k, v in DonaldAbility_Table.items()}
+ for ability in self.slot_data_donald_weapon:
+ if ability in self.donald_ability_dict and self.donald_ability_dict[ability] >= 1:
+ self.donald_ability_dict[ability] -= 1
+ self.goofy_ability_dict = {k: v.quantity for k, v in GoofyAbility_Table.items()}
+ for ability in self.slot_data_goofy_weapon:
+ if ability in self.goofy_ability_dict and self.goofy_ability_dict[ability] >= 1:
+ self.goofy_ability_dict[ability] -= 1
+
+ slot_data = self.options.as_dict("Goal", "FinalXemnas", "LuckyEmblemsRequired", "BountyRequired")
+ slot_data.update({
+ "hitlist": [], # remove this after next update
+ "PoptrackerVersionCheck": 4.3,
+ "KeybladeAbilities": self.sora_ability_dict,
+ "StaffAbilities": self.donald_ability_dict,
+ "ShieldAbilities": self.goofy_ability_dict,
+ })
+ return slot_data
+
+ def create_item(self, name: str) -> Item:
+ """
+ Returns created KH2Item
+ """
+ # data = item_dictionary_table[name]
+ if name in progression_set:
item_classification = ItemClassification.progression
+ elif name in useful_set:
+ item_classification = ItemClassification.useful
else:
item_classification = ItemClassification.filler
+ created_item = KH2Item(name, item_classification, self.item_name_to_id[name], self.player)
- created_item = KH2Item(name, item_classification, data.code, self.player)
+ return created_item
+ def create_event_item(self, name: str) -> Item:
+ item_classification = ItemClassification.progression
+ created_item = KH2Item(name, item_classification, None, self.player)
return created_item
def create_items(self) -> None:
- self.visitlocking_dict = Progression_Dicts["AllVisitLocking"].copy()
- if self.multiworld.Schmovement[self.player] != "level_0":
- for _ in range(self.multiworld.Schmovement[self.player].value):
- for name in {ItemName.HighJump, ItemName.QuickRun, ItemName.DodgeRoll, ItemName.AerialDodge,
- ItemName.Glide}:
+ """
+ Fills ItemPool and manages schmovement, random growth, visit locking and random starting visit locking.
+ """
+ self.visitlocking_dict = visit_locking_dict["AllVisitLocking"].copy()
+ if self.options.Schmovement != "level_0":
+ for _ in range(self.options.Schmovement.value):
+ for name in Movement_Table.keys():
self.item_quantity_dict[name] -= 1
self.growth_list.remove(name)
self.multiworld.push_precollected(self.create_item(name))
- if self.multiworld.RandomGrowth[self.player] != 0:
- max_growth = min(self.multiworld.RandomGrowth[self.player].value, len(self.growth_list))
+ if self.options.RandomGrowth:
+ max_growth = min(self.options.RandomGrowth.value, len(self.growth_list))
for _ in range(max_growth):
- random_growth = self.multiworld.per_slot_randoms[self.player].choice(self.growth_list)
+ random_growth = self.random.choice(self.growth_list)
self.item_quantity_dict[random_growth] -= 1
self.growth_list.remove(random_growth)
self.multiworld.push_precollected(self.create_item(random_growth))
- if self.multiworld.Visitlocking[self.player] == "no_visit_locking":
- for item, amount in Progression_Dicts["AllVisitLocking"].items():
+ if self.options.Visitlocking == "no_visit_locking":
+ for item, amount in visit_locking_dict["AllVisitLocking"].items():
for _ in range(amount):
self.multiworld.push_precollected(self.create_item(item))
self.item_quantity_dict[item] -= 1
@@ -123,19 +160,19 @@ def create_items(self) -> None:
if self.visitlocking_dict[item] == 0:
self.visitlocking_dict.pop(item)
- elif self.multiworld.Visitlocking[self.player] == "second_visit_locking":
- for item in Progression_Dicts["2VisitLocking"]:
+ elif self.options.Visitlocking == "second_visit_locking":
+ for item in visit_locking_dict["2VisitLocking"]:
self.item_quantity_dict[item] -= 1
self.visitlocking_dict[item] -= 1
if self.visitlocking_dict[item] == 0:
self.visitlocking_dict.pop(item)
self.multiworld.push_precollected(self.create_item(item))
- for _ in range(self.multiworld.RandomVisitLockingItem[self.player].value):
+ for _ in range(self.options.RandomVisitLockingItem.value):
if sum(self.visitlocking_dict.values()) <= 0:
break
visitlocking_set = list(self.visitlocking_dict.keys())
- item = self.multiworld.per_slot_randoms[self.player].choice(visitlocking_set)
+ item = self.random.choice(visitlocking_set)
self.item_quantity_dict[item] -= 1
self.visitlocking_dict[item] -= 1
if self.visitlocking_dict[item] == 0:
@@ -145,257 +182,359 @@ def create_items(self) -> None:
itempool = [self.create_item(item) for item, data in self.item_quantity_dict.items() for _ in range(data)]
# Creating filler for unfilled locations
- itempool += [self.create_filler()
- for _ in range(self.totalLocations - len(itempool))]
+ itempool += [self.create_filler() for _ in range(self.total_locations - len(itempool))]
+
self.multiworld.itempool += itempool
def generate_early(self) -> None:
- # Item Quantity dict because Abilities can be a problem for KH2's Software.
+ """
+ Determines the quantity of items and maps plando locations to items.
+ """
+ # Item: Quantity Map
+ # Example. Quick Run: 4
+ self.total_locations = len(all_locations.keys())
+ for x in range(4):
+ self.growth_list.extend(Movement_Table.keys())
+
self.item_quantity_dict = {item: data.quantity for item, data in item_dictionary_table.items()}
+ self.sora_ability_dict = {k: v.quantity for dic in [SupportAbility_Table, ActionAbility_Table] for k, v in
+ dic.items()}
# Dictionary to mark locations with their plandoed item
# Example. Final Xemnas: Victory
+ # 3 random support abilities because there are left over slots
+ support_abilities = list(SupportAbility_Table.keys())
+ for _ in range(6):
+ random_support_ability = self.random.choice(support_abilities)
+ self.item_quantity_dict[random_support_ability] += 1
+ self.sora_ability_dict[random_support_ability] += 1
+
self.plando_locations = dict()
- self.hitlist = []
self.starting_invo_verify()
+
+ for k, v in self.options.CustomItemPoolQuantity.value.items():
+ # kh2's items cannot hold more than a byte
+ if 255 > v > self.item_quantity_dict[k] and k in default_itempool_option.keys():
+ self.item_quantity_dict[k] = v
+ elif 255 <= v:
+ logging.warning(
+ f"{self.player} has too many {k} in their CustomItemPool setting. Setting to default quantity")
# Option to turn off Promise Charm Item
- if not self.multiworld.Promise_Charm[self.player]:
- self.item_quantity_dict[ItemName.PromiseCharm] = 0
+ if not self.options.Promise_Charm:
+ del self.item_quantity_dict[ItemName.PromiseCharm]
+
+ if not self.options.AntiForm:
+ del self.item_quantity_dict[ItemName.AntiForm]
self.set_excluded_locations()
- if self.multiworld.Goal[self.player] == "lucky_emblem_hunt":
- self.luckyemblemamount = self.multiworld.LuckyEmblemsAmount[self.player].value
- self.luckyemblemrequired = self.multiworld.LuckyEmblemsRequired[self.player].value
+ if self.options.Goal not in ["hitlist", "three_proofs"]:
+ self.lucky_emblem_amount = self.options.LuckyEmblemsAmount.value
+ self.lucky_emblem_required = self.options.LuckyEmblemsRequired.value
self.emblem_verify()
# hitlist
- elif self.multiworld.Goal[self.player] == "hitlist":
- self.RandomSuperBoss.extend(exclusion_table["Hitlist"])
- self.BountiesAmount = self.multiworld.BountyAmount[self.player].value
- self.BountiesRequired = self.multiworld.BountyRequired[self.player].value
+ if self.options.Goal not in ["lucky_emblem_hunt", "three_proofs"]:
+ self.random_super_boss_list.extend(exclusion_table["Hitlist"])
+ self.bounties_amount = self.options.BountyAmount.value
+ self.bounties_required = self.options.BountyRequired.value
self.hitlist_verify()
- for bounty in range(self.BountiesAmount):
- randomBoss = self.multiworld.per_slot_randoms[self.player].choice(self.RandomSuperBoss)
- self.plando_locations[randomBoss] = ItemName.Bounty
- self.hitlist.append(self.location_name_to_id[randomBoss])
- self.RandomSuperBoss.remove(randomBoss)
- self.totalLocations -= 1
-
- self.donald_fill()
- self.goofy_fill()
- self.keyblade_fill()
-
- if self.multiworld.FinalXemnas[self.player]:
- self.plando_locations[LocationName.FinalXemnas] = ItemName.Victory
- else:
- self.plando_locations[LocationName.FinalXemnas] = self.create_filler().name
-
- # same item placed because you can only get one of these 2 locations
- # they are both under the same flag so the player gets both locations just one of the two items
- random_stt_item = self.create_filler().name
- for location in {LocationName.JunkMedal, LocationName.JunkMedal}:
- self.plando_locations[location] = random_stt_item
- self.level_subtraction()
- # subtraction from final xemnas and stt
- self.totalLocations -= 3
+ prio_hitlist = [location for location in self.options.priority_locations.value if
+ location in self.random_super_boss_list]
+ for bounty in range(self.options.BountyAmount.value):
+ if prio_hitlist:
+ random_boss = self.random.choice(prio_hitlist)
+ prio_hitlist.remove(random_boss)
+ else:
+ random_boss = self.random.choice(self.random_super_boss_list)
+ self.plando_locations[random_boss] = ItemName.Bounty
+ self.random_super_boss_list.remove(random_boss)
+ self.total_locations -= 1
+
+ self.donald_gen_early()
+ self.goofy_gen_early()
+ self.keyblade_gen_early()
+
+ # final xemnas isn't a location anymore
+ # self.total_locations -= 1
+
+ if self.options.WeaponSlotStartHint:
+ for location in all_weapon_slot:
+ self.options.start_location_hints.value.add(location)
+
+ if self.options.FillerItemsLocal:
+ for item in filler_items:
+ self.options.local_items.value.add(item)
+ # By imitating remote this doesn't have to be plandoded filler anymore
+ # for location in {LocationName.JunkMedal, LocationName.JunkMedal}:
+ # self.plando_locations[location] = random_stt_item
+ if not self.options.SummonLevelLocationToggle:
+ self.total_locations -= 6
+
+ self.total_locations -= self.level_subtraction()
def pre_fill(self):
+ """
+ Plandoing Events and Fill_Restrictive for donald,goofy and sora
+ """
+ self.donald_pre_fill()
+ self.goofy_pre_fill()
+ self.keyblade_pre_fill()
+
for location, item in self.plando_locations.items():
self.multiworld.get_location(location, self.player).place_locked_item(
self.create_item(item))
def create_regions(self):
- location_table = setup_locations()
- create_regions(self.multiworld, self.player, location_table)
- connect_regions(self.multiworld, self.player)
+ """
+ Creates the Regions and Connects them.
+ """
+ create_regions(self)
+ connect_regions(self)
def set_rules(self):
- set_rules(self.multiworld, self.player)
+ """
+ Sets the Logic for the Regions and Locations.
+ """
+ universal_logic = Rules.KH2WorldRules(self)
+ form_logic = Rules.KH2FormRules(self)
+ fight_rules = Rules.KH2FightRules(self)
+ fight_rules.set_kh2_fight_rules()
+ universal_logic.set_kh2_rules()
+ form_logic.set_kh2_form_rules()
def generate_output(self, output_directory: str):
+ """
+ Generates the .zip for OpenKH (The KH Mod Manager)
+ """
patch_kh2(self, output_directory)
- def donald_fill(self):
- for item in DonaldAbility_Table:
- data = self.item_quantity_dict[item]
- for _ in range(data):
- self.donald_ability_pool.append(item)
- self.item_quantity_dict[item] = 0
- # 32 is the amount of donald abilities
- while len(self.donald_ability_pool) < 32:
- self.donald_ability_pool.append(
- self.multiworld.per_slot_randoms[self.player].choice(self.donald_ability_pool))
- # Placing Donald Abilities on donald locations
- for donaldLocation in Locations.Donald_Checks.keys():
- random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.donald_ability_pool)
- self.plando_locations[donaldLocation] = random_ability
- self.totalLocations -= 1
- self.donald_ability_pool.remove(random_ability)
-
- def goofy_fill(self):
- for item in GoofyAbility_Table.keys():
- data = self.item_quantity_dict[item]
- for _ in range(data):
- self.goofy_ability_pool.append(item)
- self.item_quantity_dict[item] = 0
- # 32 is the amount of goofy abilities
- while len(self.goofy_ability_pool) < 33:
- self.goofy_ability_pool.append(
- self.multiworld.per_slot_randoms[self.player].choice(self.goofy_ability_pool))
- # Placing Goofy Abilities on goofy locations
- for goofyLocation in Locations.Goofy_Checks.keys():
- random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.goofy_ability_pool)
- self.plando_locations[goofyLocation] = random_ability
- self.totalLocations -= 1
- self.goofy_ability_pool.remove(random_ability)
-
- def keyblade_fill(self):
- if self.multiworld.KeybladeAbilities[self.player] == "support":
- self.sora_keyblade_ability_pool = {
- **{item: data for item, data in self.item_quantity_dict.items() if item in SupportAbility_Table},
- **{ItemName.NegativeCombo: 1, ItemName.AirComboPlus: 1, ItemName.ComboPlus: 1,
- ItemName.FinishingPlus: 1}}
-
- elif self.multiworld.KeybladeAbilities[self.player] == "action":
- self.sora_keyblade_ability_pool = {item: data for item, data in self.item_quantity_dict.items() if
- item in ActionAbility_Table}
- # there are too little action abilities so 2 random support abilities are placed
- for _ in range(3):
- randomSupportAbility = self.multiworld.per_slot_randoms[self.player].choice(
- list(SupportAbility_Table.keys()))
- while randomSupportAbility in self.sora_keyblade_ability_pool:
- randomSupportAbility = self.multiworld.per_slot_randoms[self.player].choice(
- list(SupportAbility_Table.keys()))
- self.sora_keyblade_ability_pool[randomSupportAbility] = 1
- else:
- # both action and support on keyblades.
- # TODO: make option to just exclude scom
- self.sora_keyblade_ability_pool = {
- **{item: data for item, data in self.item_quantity_dict.items() if item in SupportAbility_Table},
- **{item: data for item, data in self.item_quantity_dict.items() if item in ActionAbility_Table},
- **{ItemName.NegativeCombo: 1, ItemName.AirComboPlus: 1, ItemName.ComboPlus: 1,
- ItemName.FinishingPlus: 1}}
-
- for ability in self.multiworld.BlacklistKeyblade[self.player].value:
- if ability in self.sora_keyblade_ability_pool:
- self.sora_keyblade_ability_pool.pop(ability)
-
- # magic number for amount of keyblades
- if sum(self.sora_keyblade_ability_pool.values()) < 28:
- raise Exception(
- f"{self.multiworld.get_file_safe_player_name(self.player)} has too little Keyblade Abilities in the Keyblade Pool")
-
- self.valid_abilities = list(self.sora_keyblade_ability_pool.keys())
- # Kingdom Key cannot have No Experience so plandoed here instead of checking 26 times if its kingdom key
- random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.valid_abilities)
- while random_ability == ItemName.NoExperience:
- random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.valid_abilities)
- self.plando_locations[LocationName.KingdomKeySlot] = random_ability
- self.item_quantity_dict[random_ability] -= 1
- self.sora_keyblade_ability_pool[random_ability] -= 1
- if self.sora_keyblade_ability_pool[random_ability] == 0:
- self.valid_abilities.remove(random_ability)
- self.sora_keyblade_ability_pool.pop(random_ability)
-
- # plando keyblades because they can only have abilities
- for keyblade in self.keyblade_slot_copy:
- random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.valid_abilities)
- self.plando_locations[keyblade] = random_ability
+ def donald_gen_early(self):
+ random_prog_ability = self.random.choice([ItemName.Fantasia, ItemName.FlareForce])
+ donald_master_ability = [donald_ability for donald_ability in DonaldAbility_Table.keys() for _ in
+ range(self.item_quantity_dict[donald_ability]) if
+ donald_ability != random_prog_ability]
+ self.donald_weapon_abilities = []
+ self.donald_get_bonus_abilities = []
+ # fill goofy weapons first
+ for _ in range(15):
+ random_ability = self.random.choice(donald_master_ability)
+ donald_master_ability.remove(random_ability)
+ self.donald_weapon_abilities += [self.create_item(random_ability)]
+ self.item_quantity_dict[random_ability] -= 1
+ self.total_locations -= 1
+ self.slot_data_donald_weapon = [item_name.name for item_name in self.donald_weapon_abilities]
+ if not self.options.DonaldGoofyStatsanity:
+ # pre plando donald get bonuses
+ self.donald_get_bonus_abilities += [self.create_item(random_prog_ability)]
+ self.total_locations -= 1
+ for item_name in donald_master_ability:
+ self.donald_get_bonus_abilities += [self.create_item(item_name)]
+ self.item_quantity_dict[item_name] -= 1
+ self.total_locations -= 1
+
+ def goofy_gen_early(self):
+ random_prog_ability = self.random.choice([ItemName.Teamwork, ItemName.TornadoFusion])
+ goofy_master_ability = [goofy_ability for goofy_ability in GoofyAbility_Table.keys() for _ in
+ range(self.item_quantity_dict[goofy_ability]) if goofy_ability != random_prog_ability]
+ self.goofy_weapon_abilities = []
+ self.goofy_get_bonus_abilities = []
+ # fill goofy weapons first
+ for _ in range(15):
+ random_ability = self.random.choice(goofy_master_ability)
+ goofy_master_ability.remove(random_ability)
+ self.goofy_weapon_abilities += [self.create_item(random_ability)]
+ self.item_quantity_dict[random_ability] -= 1
+ self.total_locations -= 1
+
+ self.slot_data_goofy_weapon = [item_name.name for item_name in self.goofy_weapon_abilities]
+
+ if not self.options.DonaldGoofyStatsanity:
+ # pre plando goofy get bonuses
+ self.goofy_get_bonus_abilities += [self.create_item(random_prog_ability)]
+ self.total_locations -= 1
+ for item_name in goofy_master_ability:
+ self.goofy_get_bonus_abilities += [self.create_item(item_name)]
+ self.item_quantity_dict[item_name] -= 1
+ self.total_locations -= 1
+
+ def keyblade_gen_early(self):
+ keyblade_master_ability = [ability for ability in SupportAbility_Table.keys() if ability not in progression_set
+ for _ in range(self.item_quantity_dict[ability])]
+ self.keyblade_ability_pool = []
+
+ for _ in range(len(Keyblade_Slots)):
+ random_ability = self.random.choice(keyblade_master_ability)
+ keyblade_master_ability.remove(random_ability)
+ self.keyblade_ability_pool += [self.create_item(random_ability)]
self.item_quantity_dict[random_ability] -= 1
- self.sora_keyblade_ability_pool[random_ability] -= 1
- if self.sora_keyblade_ability_pool[random_ability] == 0:
- self.valid_abilities.remove(random_ability)
- self.sora_keyblade_ability_pool.pop(random_ability)
- self.totalLocations -= 1
+ self.total_locations -= 1
+ self.slot_data_sora_weapon = [item_name.name for item_name in self.keyblade_ability_pool]
+
+ def goofy_pre_fill(self):
+ """
+ Removes donald locations from the location pool maps random donald items to be plandoded.
+ """
+ goofy_weapon_location_list = [self.multiworld.get_location(location, self.player) for location in
+ Goofy_Checks.keys() if Goofy_Checks[location].yml == "Keyblade"]
+ # take one of the 2 out
+ # randomize the list with only
+ for location in goofy_weapon_location_list:
+ random_ability = self.random.choice(self.goofy_weapon_abilities)
+ location.place_locked_item(random_ability)
+ self.goofy_weapon_abilities.remove(random_ability)
+
+ if not self.options.DonaldGoofyStatsanity:
+ # plando goofy get bonuses
+ goofy_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in
+ Goofy_Checks.keys() if Goofy_Checks[location].yml != "Keyblade"]
+ for location in goofy_get_bonus_location_pool:
+ self.random.choice(self.goofy_get_bonus_abilities)
+ random_ability = self.random.choice(self.goofy_get_bonus_abilities)
+ location.place_locked_item(random_ability)
+ self.goofy_get_bonus_abilities.remove(random_ability)
+
+ def donald_pre_fill(self):
+ donald_weapon_location_list = [self.multiworld.get_location(location, self.player) for location in
+ Donald_Checks.keys() if Donald_Checks[location].yml == "Keyblade"]
+
+ # take one of the 2 out
+ # randomize the list with only
+ for location in donald_weapon_location_list:
+ random_ability = self.random.choice(self.donald_weapon_abilities)
+ location.place_locked_item(random_ability)
+ self.donald_weapon_abilities.remove(random_ability)
+
+ if not self.options.DonaldGoofyStatsanity:
+ # plando goofy get bonuses
+ donald_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in
+ Donald_Checks.keys() if Donald_Checks[location].yml != "Keyblade"]
+ for location in donald_get_bonus_location_pool:
+ random_ability = self.random.choice(self.donald_get_bonus_abilities)
+ location.place_locked_item(random_ability)
+ self.donald_get_bonus_abilities.remove(random_ability)
+
+ def keyblade_pre_fill(self):
+ """
+ Fills keyblade slots with abilities determined on player's setting
+ """
+ keyblade_locations = [self.multiworld.get_location(location, self.player) for location in Keyblade_Slots.keys()]
+ state = self.multiworld.get_all_state(False)
+ keyblade_ability_pool_copy = self.keyblade_ability_pool.copy()
+ fill_restrictive(self.multiworld, state, keyblade_locations, keyblade_ability_pool_copy, True, True, allow_excluded=True)
def starting_invo_verify(self):
- for item, value in self.multiworld.start_inventory[self.player].value.items():
+ """
+ Making sure the player doesn't put too many abilities in their starting inventory.
+ """
+ for item, value in self.options.start_inventory.value.items():
if item in ActionAbility_Table \
- or item in SupportAbility_Table or exclusionItem_table["StatUps"] \
+ or item in SupportAbility_Table or item in exclusion_item_table["StatUps"] \
or item in DonaldAbility_Table or item in GoofyAbility_Table:
# cannot have more than the quantity for abilties
if value > item_dictionary_table[item].quantity:
logging.info(
- f"{self.multiworld.get_file_safe_player_name(self.player)} cannot have more than {item_dictionary_table[item].quantity} of {item}"
- f"Changing the amount to the max amount")
+ f"{self.multiworld.get_file_safe_player_name(self.player)} cannot have more than {item_dictionary_table[item].quantity} of {item}."
+ f" Changing the amount to the max amount")
value = item_dictionary_table[item].quantity
self.item_quantity_dict[item] -= value
def emblem_verify(self):
- if self.luckyemblemamount < self.luckyemblemrequired:
+ """
+ Making sure lucky emblems have amount>=required.
+ """
+ if self.lucky_emblem_amount < self.lucky_emblem_required:
logging.info(
- f"Lucky Emblem Amount {self.multiworld.LuckyEmblemsAmount[self.player].value} is less than required "
- f"{self.multiworld.LuckyEmblemsRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}."
- f" Setting amount to {self.multiworld.LuckyEmblemsRequired[self.player].value}")
- luckyemblemamount = max(self.luckyemblemamount, self.luckyemblemrequired)
- self.multiworld.LuckyEmblemsAmount[self.player].value = luckyemblemamount
+ f"Lucky Emblem Amount {self.options.LuckyEmblemsAmount.value} is less than required "
+ f"{self.options.LuckyEmblemsRequired.value} for player {self.multiworld.get_file_safe_player_name(self.player)}."
+ f" Setting amount to {self.options.LuckyEmblemsRequired.value}")
+ luckyemblemamount = max(self.lucky_emblem_amount, self.lucky_emblem_required)
+ self.options.LuckyEmblemsAmount.value = luckyemblemamount
- self.item_quantity_dict[ItemName.LuckyEmblem] = self.multiworld.LuckyEmblemsAmount[self.player].value
+ self.item_quantity_dict[ItemName.LuckyEmblem] = self.options.LuckyEmblemsAmount.value
# give this proof to unlock the final door once the player has the amount of lucky emblem required
- self.item_quantity_dict[ItemName.ProofofNonexistence] = 0
+ if ItemName.ProofofNonexistence in self.item_quantity_dict:
+ del self.item_quantity_dict[ItemName.ProofofNonexistence]
def hitlist_verify(self):
- for location in self.multiworld.exclude_locations[self.player].value:
- if location in self.RandomSuperBoss:
- self.RandomSuperBoss.remove(location)
+ """
+ Making sure hitlist have amount>=required.
+ """
+ for location in self.options.exclude_locations.value:
+ if location in self.random_super_boss_list:
+ self.random_super_boss_list.remove(location)
+
+ if not self.options.SummonLevelLocationToggle and LocationName.Summonlvl7 in self.random_super_boss_list:
+ self.random_super_boss_list.remove(LocationName.Summonlvl7)
# Testing if the player has the right amount of Bounties for Completion.
- if len(self.RandomSuperBoss) < self.BountiesAmount:
+ if len(self.random_super_boss_list) < self.bounties_amount:
logging.info(
f"{self.multiworld.get_file_safe_player_name(self.player)} has more bounties than bosses."
- f" Setting total bounties to {len(self.RandomSuperBoss)}")
- self.BountiesAmount = len(self.RandomSuperBoss)
- self.multiworld.BountyAmount[self.player].value = self.BountiesAmount
+ f" Setting total bounties to {len(self.random_super_boss_list)}")
+ self.bounties_amount = len(self.random_super_boss_list)
+ self.options.BountyAmount.value = self.bounties_amount
- if len(self.RandomSuperBoss) < self.BountiesRequired:
+ if len(self.random_super_boss_list) < self.bounties_required:
logging.info(f"{self.multiworld.get_file_safe_player_name(self.player)} has too many required bounties."
- f" Setting required bounties to {len(self.RandomSuperBoss)}")
- self.BountiesRequired = len(self.RandomSuperBoss)
- self.multiworld.BountyRequired[self.player].value = self.BountiesRequired
+ f" Setting required bounties to {len(self.random_super_boss_list)}")
+ self.bounties_required = len(self.random_super_boss_list)
+ self.options.BountyRequired.value = self.bounties_required
+
+ if self.bounties_amount < self.bounties_required:
+ logging.info(
+ f"Bounties Amount is less than required for player {self.multiworld.get_file_safe_player_name(self.player)}."
+ f" Swapping Amount and Required")
+ temp = self.options.BountyRequired.value
+ self.options.BountyRequired.value = self.options.BountyAmount.value
+ self.options.BountyAmount.value = temp
- if self.BountiesAmount < self.BountiesRequired:
- logging.info(f"Bounties Amount {self.multiworld.BountyAmount[self.player].value} is less than required "
- f"{self.multiworld.BountyRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}."
- f" Setting amount to {self.multiworld.BountyRequired[self.player].value}")
- self.BountiesAmount = max(self.BountiesAmount, self.BountiesRequired)
- self.multiworld.BountyAmount[self.player].value = self.BountiesAmount
+ if self.options.BountyStartingHintToggle:
+ self.options.start_hints.value.add(ItemName.Bounty)
- self.multiworld.start_hints[self.player].value.add(ItemName.Bounty)
- self.item_quantity_dict[ItemName.ProofofNonexistence] = 0
+ if ItemName.ProofofNonexistence in self.item_quantity_dict:
+ del self.item_quantity_dict[ItemName.ProofofNonexistence]
def set_excluded_locations(self):
+ """
+ Fills excluded_locations from player's settings.
+ """
# Option to turn off all superbosses. Can do this individually but its like 20+ checks
- if not self.multiworld.SuperBosses[self.player] and not self.multiworld.Goal[self.player] == "hitlist":
- for superboss in exclusion_table["Datas"]:
- self.multiworld.exclude_locations[self.player].value.add(superboss)
+ if not self.options.SuperBosses:
for superboss in exclusion_table["SuperBosses"]:
- self.multiworld.exclude_locations[self.player].value.add(superboss)
+ self.options.exclude_locations.value.add(superboss)
# Option to turn off Olympus Colosseum Cups.
- if self.multiworld.Cups[self.player] == "no_cups":
+ if self.options.Cups == "no_cups":
for cup in exclusion_table["Cups"]:
- self.multiworld.exclude_locations[self.player].value.add(cup)
+ self.options.exclude_locations.value.add(cup)
# exclude only hades paradox. If cups and hades paradox then nothing is excluded
- elif self.multiworld.Cups[self.player] == "cups":
- self.multiworld.exclude_locations[self.player].value.add(LocationName.HadesCupTrophyParadoxCups)
+ elif self.options.Cups == "cups":
+ self.options.exclude_locations.value.add(LocationName.HadesCupTrophyParadoxCups)
+
+ if not self.options.AtlanticaToggle:
+ for loc in exclusion_table["Atlantica"]:
+ self.options.exclude_locations.value.add(loc)
def level_subtraction(self):
- # there are levels but level 1 is there for the yamls
- if self.multiworld.LevelDepth[self.player] == "level_99_sanity":
- # level 99 sanity
- self.totalLocations -= 1
- elif self.multiworld.LevelDepth[self.player] == "level_50_sanity":
+ """
+ Determine how many locations are on sora's levels.
+ """
+ if self.options.LevelDepth == "level_50_sanity":
# level 50 sanity
- self.totalLocations -= 50
- elif self.multiworld.LevelDepth[self.player] == "level_1":
+ return 49
+ elif self.options.LevelDepth == "level_1":
# level 1. No checks on levels
- self.totalLocations -= 99
+ return 98
+ elif self.options.LevelDepth in ["level_50", "level_99"]:
+ # could be if leveldepth!= 99 sanity but this reads better imo
+ return 75
else:
- # level 50/99 since they contain the same amount of levels
- self.totalLocations -= 76
+ return 0
def get_filler_item_name(self) -> str:
- return self.multiworld.random.choice(
- [ItemName.PowerBoost, ItemName.MagicBoost, ItemName.DefenseBoost, ItemName.APBoost])
+ """
+ Returns random filler item name.
+ """
+ return self.random.choice(filler_items)
diff --git a/worlds/kh2/docs/en_Kingdom Hearts 2.md b/worlds/kh2/docs/en_Kingdom Hearts 2.md
index 8258a099cc95..5aae7ad3a70e 100644
--- a/worlds/kh2/docs/en_Kingdom Hearts 2.md
+++ b/worlds/kh2/docs/en_Kingdom Hearts 2.md
@@ -4,9 +4,9 @@
This randomizer creates a more dynamic play experience by randomizing the locations of most items in Kingdom Hearts 2. Currently all items within Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels are randomized. This allows abilities that Sora would normally have to be placed on Keyblades with random stats. Additionally, there are several options for ways to finish the game, allowing for different goals beyond beating the final boss.
-Where is the settings page
+Where is the options page
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
+The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
What is randomized in this game?
@@ -16,6 +16,7 @@ The [player settings page for this game](../player-settings) contains all the op
- Popups
- Get Bonuses
- Form Levels
+- Summon Levels
- Sora's Levels
- Keyblade Stats
- Keyblade Abilities
@@ -23,7 +24,7 @@ The [player settings page for this game](../player-settings) contains all the op
What Kingdom Hearts 2 items can appear in other players' worlds?
-Every item in the game except for party members' abilities.
+Every item in the game except for abilities on weapons.
What is The Garden of Assemblage "GoA"?
@@ -63,6 +64,24 @@ For example, if you are fighting Roxas, receive Reflect Element, then die mid-fi
- Customize the amount and level of progressive movement (Growth Abilities) you start with.
- Customize start inventory, i.e., begin every run with certain items or spells of your choice.
+What are Lucky Emblems?
+Lucky Emblems are items that are required to beat the game if your goal is "Lucky Emblem Hunt".
+You can think of these as requiring X number of Proofs of Nonexistence to open the final door.
+
+What is Hitlist/Bounties?
+The Hitlist goal adds "bounty" items to select late-game fights and locations, and you need to collect X number of them to win.
+The list of possible locations that can contain a bounty:
+
+- Each of the 13 Data Fights
+- Max level (7) for each Drive Form
+- Max level (7) of Summons
+- Last song of Atlantica
+- Sephiroth
+- Lingering Will
+- Starry Hill
+- Transport to Remembrance
+- Godess of Fate cup and Hades Paradox cup
+
Quality of life:
@@ -71,6 +90,7 @@ With the help of Shananas, Num, and ZakTheRobot we have many QoL features such a
- Faster Wardrobe.
- Faster Water Jafar Chase.
+- Faster Bulky Vendors
- Carpet Skip.
- Start with Lion Dash.
- Faster Urns.
diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md
index 17235042e1fa..c6fdb020b8a4 100644
--- a/worlds/kh2/docs/setup_en.md
+++ b/worlds/kh2/docs/setup_en.md
@@ -2,20 +2,21 @@
Quick Links
- [Game Info Page](../../../../games/Kingdom%20Hearts%202/info/en)
-- [Player Settings Page](../../../../games/Kingdom%20Hearts%202/player-settings)
+- [Player Options Page](../../../../games/Kingdom%20Hearts%202/player-options)
Required Software:
`Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts)
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
- 1. `3.0.0 OpenKH Mod Manager with Panacea`
- 2. `Install mod from KH2FM-Mods-Num/GoA-ROM-Edition`
- 3. `Setup Lua Backend From the 3.0.0 KH2Randomizer.exe per the setup guide linked above`
+ 1. `3.2.0 OpenKH Mod Manager with Panacea`
+ 2. `Lua Backend from the OpenKH Mod Manager`
+ 3. `Install the mod KH2FM-Mods-Num/GoA-ROM-Edition using OpenKH Mod Manager`
- Needed for Archipelago
1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases)
- 2. `Install mod from JaredWeakStrike/APCompanion`
- 3. `Install mod from KH2FM-Mods-equations19/auto-save`
- 4. `AP Randomizer Seed`
+ 2. `Install the Archipelago Companion mod from JaredWeakStrike/APCompanion using OpenKH Mod Manager`
+ 3. `Install the Archipelago Quality Of Life mod from JaredWeakStrike/AP_QOL using OpenKH Mod Manager`
+ 4. `Install the mod from KH2FM-Mods-equations19/auto-save using OpenKH Mod Manager`
+ 5. `AP Randomizer Seed`
Required: Archipelago Companion Mod
Load this mod just like the GoA ROM you did during the KH2 Rando setup. `JaredWeakStrike/APCompanion`
@@ -23,7 +24,7 @@ Have this mod second-highest priority below the .zip seed.
This mod is based upon Num's Garden of Assemblege Mod and requires it to work. Without Num this could not be possible.
Required: Auto Save Mod
-Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` Location doesn't matter, required in case of crashes.
+Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` Location doesn't matter, required in case of crashes. See [Best Practices](en#best-practices) on how to load the auto save
Installing A Seed
@@ -32,7 +33,7 @@ Make sure the seed is on the top of the list (Highest Priority)
After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot is a unique mod to install and will be needed be repatched for different slots/rooms.
What the Mod Manager Should Look Like.
-![image](https://i.imgur.com/QgRfjP1.png)
+![image](https://i.imgur.com/Si4oZ8w.png)
Using the KH2 Client
@@ -60,36 +61,40 @@ Enter `The room's port number` into the top box where the x's are and pr
- To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps.
-Best Practices
+Best Practices
- Make a save at the start of the GoA before opening anything. This will be the file to select when loading an autosave if/when your game crashes.
- If you don't want to have a save in the GoA. Disconnect the client, load the auto save, and then reconnect the client after it loads the auto save.
- Set fps limit to 60fps.
- Run the game in windows/borderless windowed mode. Fullscreen is stable but the game can crash if you alt-tab out.
+- Make sure to save in a different save slot when playing in an async or disconnecting from the server to play a different seed
-Requirement/logic sheet
-Have any questions on what's in logic? This spreadsheet has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1Embae0t7pIrbzvX-NRywk7bTHHEvuFzzQBUUpSUL7Ak/edit?usp=sharing)
+Logic Sheet
+Have any questions on what's in logic? This spreadsheet made by Bulcon has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1nNi8ohEs1fv-sDQQRaP45o6NoRcMlLJsGckBonweDMY/edit?usp=sharing)
F.A.Q.
-
+- Why is my Client giving me a "Cannot Open Process: " error?
+ - Due to how the client reads kingdom hearts 2 memory some people's computer flags it as a virus. Run the client as admin.
+- Why is my HP/MP continuously increasing without stopping?
+ - You do not have `JaredWeakStrike/APCompanion` set up correctly. Make sure it is above the `GoA ROM Mod` in the mod manager.
+- Why is my HP/MP continuously increasing without stopping when I have the APCompanion Mod?
+ - You have a leftover GOA lua script in your `Documents\KINGDOM HEARTS HD 1.5+2.5 ReMIX\scripts\KH2`.
+- Why am I missing worlds/portals in the GoA?
+ - You are missing the required visit-locking item to access the world/portal.
+- Why did I not load into the correct visit?
+ - You need to trigger a cutscene or visit The World That Never Was for it to register that you have received the item.
+- What versions of Kingdom Hearts 2 are supported?
+ - Currently `only` the most up to date version on the Epic Game Store is supported: version `1.0.0.8_WW`.
- Why am I getting wallpapered while going into a world for the first time?
- Your `Lua Backend` was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide.
- Why am I not getting magic?
- If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
-- Why am I missing worlds/portals in the GoA?
- - You are missing the required visit locking item to access the world/portal.
-- What versions of Kingdom Hearts 2 are supported?
- - Currently `only` the most up to date version on the Epic Game Store is supported `1.0.0.8_WW`. Emulator may be added in the future.
- Why did I crash?
- The port of Kingdom Hearts 2 can and will randomly crash, this is the fault of the game not the randomizer or the archipelago client.
- If you have a continuous/constant crash (in the same area/event every time) you will want to reverify your installed files. This can be done by doing the following: Open Epic Game Store --> Library --> Click Triple Dots --> Manage --> Verify
- Why am I getting dummy items or letters?
- You will need to get the `JaredWeakStrike/APCompanion` (you can find how to get this if you scroll up)
-- Why is my HP/MP continuously increasing without stopping?
- - You do not have `JaredWeakStrike/APCompanion` setup correctly. Make Sure it is above the GOA in the mod manager.
- Why am I not sending or receiving items?
- Make sure you are connected to the KH2 client and the correct room (for more information scroll up)
-- Why did I not load in to the correct visit
- - You need to trigger a cutscene or visit The World That Never Was for it to update you have recevied the item.
- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save`?
- Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress.
- How do I load an auto save?
diff --git a/worlds/kh2/logic.py b/worlds/kh2/logic.py
deleted file mode 100644
index 1c5883f5ce8a..000000000000
--- a/worlds/kh2/logic.py
+++ /dev/null
@@ -1,312 +0,0 @@
-from .Names import ItemName
-from ..AutoWorld import LogicMixin
-
-
-class KH2Logic(LogicMixin):
- def kh_lod_unlocked(self, player, amount):
- return self.has(ItemName.SwordoftheAncestor, player, amount)
-
- def kh_oc_unlocked(self, player, amount):
- return self.has(ItemName.BattlefieldsofWar, player, amount)
-
- def kh_twtnw_unlocked(self, player, amount):
- return self.has(ItemName.WaytotheDawn, player, amount)
-
- def kh_ht_unlocked(self, player, amount):
- return self.has(ItemName.BoneFist, player, amount)
-
- def kh_tt_unlocked(self, player, amount):
- return self.has(ItemName.IceCream, player, amount)
-
- def kh_pr_unlocked(self, player, amount):
- return self.has(ItemName.SkillandCrossbones, player, amount)
-
- def kh_sp_unlocked(self, player, amount):
- return self.has(ItemName.IdentityDisk, player, amount)
-
- def kh_stt_unlocked(self, player: int, amount):
- return self.has(ItemName.NamineSketches, player, amount)
-
- # Using Dummy 13 for this
- def kh_dc_unlocked(self, player: int, amount):
- return self.has(ItemName.CastleKey, player, amount)
-
- def kh_hb_unlocked(self, player, amount):
- return self.has(ItemName.MembershipCard, player, amount)
-
- def kh_pl_unlocked(self, player, amount):
- return self.has(ItemName.ProudFang, player, amount)
-
- def kh_ag_unlocked(self, player, amount):
- return self.has(ItemName.Scimitar, player, amount)
-
- def kh_bc_unlocked(self, player, amount):
- return self.has(ItemName.BeastsClaw, player, amount)
-
- def kh_amount_of_forms(self, player, amount, requiredform="None"):
- level = 0
- formList = [ItemName.ValorForm, ItemName.WisdomForm, ItemName.LimitForm, ItemName.MasterForm,
- ItemName.FinalForm]
- # required form is in the logic for region connections
- if requiredform != "None":
- formList.remove(requiredform)
- for form in formList:
- if self.has(form, player):
- level += 1
- return level >= amount
-
- def kh_visit_locking_amount(self, player, amount):
- visit = 0
- # torn pages are not added since you cannot get exp from that world
- for item in {ItemName.CastleKey, ItemName.BattlefieldsofWar, ItemName.SwordoftheAncestor, ItemName.BeastsClaw,
- ItemName.BoneFist, ItemName.ProudFang, ItemName.SkillandCrossbones, ItemName.Scimitar,
- ItemName.MembershipCard,
- ItemName.IceCream, ItemName.WaytotheDawn,
- ItemName.IdentityDisk, ItemName.NamineSketches}:
- visit += self.item_count(item, player)
- return visit >= amount
-
- def kh_three_proof_unlocked(self, player):
- return self.has(ItemName.ProofofConnection, player, 1) \
- and self.has(ItemName.ProofofNonexistence, player, 1) \
- and self.has(ItemName.ProofofPeace, player, 1)
-
- def kh_hitlist(self, player, amount):
- return self.has(ItemName.Bounty, player, amount)
-
- def kh_lucky_emblem_unlocked(self, player, amount):
- return self.has(ItemName.LuckyEmblem, player, amount)
-
- def kh_victory(self, player):
- return self.has(ItemName.Victory, player, 1)
-
- def kh_summon(self, player, amount):
- summonlevel = 0
- for summon in {ItemName.Genie, ItemName.ChickenLittle, ItemName.Stitch, ItemName.PeterPan}:
- if self.has(summon, player):
- summonlevel += 1
- return summonlevel >= amount
-
- # magic progression
- def kh_fire(self, player):
- return self.has(ItemName.FireElement, player, 1)
-
- def kh_fira(self, player):
- return self.has(ItemName.FireElement, player, 2)
-
- def kh_firaga(self, player):
- return self.has(ItemName.FireElement, player, 3)
-
- def kh_blizzard(self, player):
- return self.has(ItemName.BlizzardElement, player, 1)
-
- def kh_blizzara(self, player):
- return self.has(ItemName.BlizzardElement, player, 2)
-
- def kh_blizzaga(self, player):
- return self.has(ItemName.BlizzardElement, player, 3)
-
- def kh_thunder(self, player):
- return self.has(ItemName.ThunderElement, player, 1)
-
- def kh_thundara(self, player):
- return self.has(ItemName.ThunderElement, player, 2)
-
- def kh_thundaga(self, player):
- return self.has(ItemName.ThunderElement, player, 3)
-
- def kh_magnet(self, player):
- return self.has(ItemName.MagnetElement, player, 1)
-
- def kh_magnera(self, player):
- return self.has(ItemName.MagnetElement, player, 2)
-
- def kh_magnega(self, player):
- return self.has(ItemName.MagnetElement, player, 3)
-
- def kh_reflect(self, player):
- return self.has(ItemName.ReflectElement, player, 1)
-
- def kh_reflera(self, player):
- return self.has(ItemName.ReflectElement, player, 2)
-
- def kh_reflega(self, player):
- return self.has(ItemName.ReflectElement, player, 3)
-
- def kh_highjump(self, player, amount):
- return self.has(ItemName.HighJump, player, amount)
-
- def kh_quickrun(self, player, amount):
- return self.has(ItemName.QuickRun, player, amount)
-
- def kh_dodgeroll(self, player, amount):
- return self.has(ItemName.DodgeRoll, player, amount)
-
- def kh_aerialdodge(self, player, amount):
- return self.has(ItemName.AerialDodge, player, amount)
-
- def kh_glide(self, player, amount):
- return self.has(ItemName.Glide, player, amount)
-
- def kh_comboplus(self, player, amount):
- return self.has(ItemName.ComboPlus, player, amount)
-
- def kh_aircomboplus(self, player, amount):
- return self.has(ItemName.AirComboPlus, player, amount)
-
- def kh_valorgenie(self, player):
- return self.has(ItemName.Genie, player) and self.has(ItemName.ValorForm, player)
-
- def kh_wisdomgenie(self, player):
- return self.has(ItemName.Genie, player) and self.has(ItemName.WisdomForm, player)
-
- def kh_mastergenie(self, player):
- return self.has(ItemName.Genie, player) and self.has(ItemName.MasterForm, player)
-
- def kh_finalgenie(self, player):
- return self.has(ItemName.Genie, player) and self.has(ItemName.FinalForm, player)
-
- def kh_rsr(self, player):
- return self.has(ItemName.Slapshot, player, 1) and self.has(ItemName.ComboMaster, player) and self.kh_reflect(
- player)
-
- def kh_gapcloser(self, player):
- return self.has(ItemName.FlashStep, player, 1) or self.has(ItemName.SlideDash, player)
-
- # Crowd Control and Berserk Hori will be used when I add hard logic.
-
- def kh_crowdcontrol(self, player):
- return self.kh_magnera(player) and self.has(ItemName.ChickenLittle, player) \
- or self.kh_magnega(player) and self.kh_mastergenie(player)
-
- def kh_berserkhori(self, player):
- return self.has(ItemName.HorizontalSlash, player, 1) and self.has(ItemName.BerserkCharge, player)
-
- def kh_donaldlimit(self, player):
- return self.has(ItemName.FlareForce, player, 1) or self.has(ItemName.Fantasia, player)
-
- def kh_goofylimit(self, player):
- return self.has(ItemName.TornadoFusion, player, 1) or self.has(ItemName.Teamwork, player)
-
- def kh_basetools(self, player):
- # TODO: if option is easy then add reflect,gap closer and second chance&once more. #option east scom option normal adds gap closer or combo master #hard is what is right now
- return self.has(ItemName.Guard, player, 1) and self.has(ItemName.AerialRecovery, player, 1) \
- and self.has(ItemName.FinishingPlus, player, 1)
-
- def kh_roxastools(self, player):
- return self.kh_basetools(player) and (
- self.has(ItemName.QuickRun, player) or self.has(ItemName.NegativeCombo, player, 2))
-
- def kh_painandpanic(self, player):
- return (self.kh_goofylimit(player) or self.kh_donaldlimit(player)) and self.kh_dc_unlocked(player, 2)
-
- def kh_cerberuscup(self, player):
- return self.kh_amount_of_forms(player, 2) and self.kh_thundara(player) \
- and self.kh_ag_unlocked(player, 1) and self.kh_ht_unlocked(player, 1) \
- and self.kh_pl_unlocked(player, 1)
-
- def kh_titan(self, player: int):
- return self.kh_summon(player, 2) and (self.kh_thundara(player) or self.kh_magnera(player)) \
- and self.kh_oc_unlocked(player, 2)
-
- def kh_gof(self, player):
- return self.kh_titan(player) and self.kh_cerberuscup(player) \
- and self.kh_painandpanic(player) and self.kh_twtnw_unlocked(player, 1)
-
- def kh_dataroxas(self, player):
- return self.kh_basetools(player) and \
- ((self.has(ItemName.LimitForm, player) and self.kh_amount_of_forms(player, 3) and self.has(
- ItemName.TrinityLimit, player) and self.kh_gapcloser(player))
- or (self.has(ItemName.NegativeCombo, player, 2) or self.kh_quickrun(player, 2)))
-
- def kh_datamarluxia(self, player):
- return self.kh_basetools(player) and self.kh_reflera(player) \
- and ((self.kh_amount_of_forms(player, 3) and self.has(ItemName.FinalForm, player) and self.kh_fira(
- player)) or self.has(ItemName.NegativeCombo, player, 2) or self.kh_donaldlimit(player))
-
- def kh_datademyx(self, player):
- return self.kh_basetools(player) and self.kh_amount_of_forms(player, 5) and self.kh_firaga(player) \
- and (self.kh_donaldlimit(player) or self.kh_blizzard(player))
-
- def kh_datalexaeus(self, player):
- return self.kh_basetools(player) and self.kh_amount_of_forms(player, 3) and self.kh_reflera(player) \
- and (self.has(ItemName.NegativeCombo, player, 2) or self.kh_donaldlimit(player))
-
- def kh_datasaix(self, player):
- return self.kh_basetools(player) and (self.kh_thunder(player) or self.kh_blizzard(player)) \
- and self.kh_highjump(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player, 2) and self.kh_amount_of_forms(player, 3) \
- and (self.kh_rsr(player) or self.has(ItemName.NegativeCombo, player, 2) or self.has(ItemName.PeterPan,
- player))
-
- def kh_dataxaldin(self, player):
- return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.kh_goofylimit(player) \
- and self.kh_highjump(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player,
- 2) and self.kh_magnet(
- player)
- # and (self.kh_form_level_unlocked(player, 3) or self.kh_berserkhori(player))
-
- def kh_dataxemnas(self, player):
- return self.kh_basetools(player) and self.kh_rsr(player) and self.kh_gapcloser(player) \
- and (self.has(ItemName.LimitForm, player) or self.has(ItemName.TrinityLimit, player))
-
- def kh_dataxigbar(self, player):
- return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \
- and self.kh_amount_of_forms(player, 3) and self.kh_reflera(player)
-
- def kh_datavexen(self, player):
- return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \
- and self.kh_amount_of_forms(player, 4) and self.kh_reflera(player) and self.kh_fira(player)
-
- def kh_datazexion(self, player):
- return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \
- and self.kh_amount_of_forms(player, 3) \
- and self.kh_reflera(player) and self.kh_fira(player)
-
- def kh_dataaxel(self, player):
- return self.kh_basetools(player) \
- and ((self.kh_reflera(player) and self.kh_blizzara(player)) or self.has(ItemName.NegativeCombo, player, 2))
-
- def kh_dataluxord(self, player):
- return self.kh_basetools(player) and self.kh_reflect(player)
-
- def kh_datalarxene(self, player):
- return self.kh_basetools(player) and self.kh_reflera(player) \
- and ((self.has(ItemName.FinalForm, player) and self.kh_amount_of_forms(player, 4) and self.kh_fire(
- player))
- or (self.kh_donaldlimit(player) and self.kh_amount_of_forms(player, 2)))
-
- def kh_sephi(self, player):
- return self.kh_dataxemnas(player)
-
- def kh_onek(self, player):
- return self.kh_reflect(player) or self.has(ItemName.Guard, player)
-
- def kh_terra(self, player):
- return self.has(ItemName.ProofofConnection, player) and self.kh_basetools(player) \
- and self.kh_dodgeroll(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player, 3) \
- and ((self.kh_comboplus(player, 2) and self.has(ItemName.Explosion, player)) or self.has(
- ItemName.NegativeCombo, player, 2))
-
- def kh_cor(self, player):
- return self.kh_reflect(player) \
- and self.kh_highjump(player, 2) and self.kh_quickrun(player, 2) and self.kh_aerialdodge(player, 2) \
- and (self.has(ItemName.MasterForm, player) and self.kh_fire(player)
- or (self.has(ItemName.ChickenLittle, player) and self.kh_donaldlimit(player) and self.kh_glide(player,
- 2)))
-
- def kh_transport(self, player):
- return self.kh_basetools(player) and self.kh_reflera(player) \
- and ((self.kh_mastergenie(player) and self.kh_magnera(player) and self.kh_donaldlimit(player))
- or (self.has(ItemName.FinalForm, player) and self.kh_amount_of_forms(player, 4) and self.kh_fira(
- player)))
-
- def kh_gr2(self, player):
- return (self.has(ItemName.MasterForm, player) or self.has(ItemName.Stitch, player)) \
- and (self.kh_fire(player) or self.kh_blizzard(player) or self.kh_thunder(player))
-
- def kh_xaldin(self, player):
- return self.kh_basetools(player) and (self.kh_donaldlimit(player) or self.kh_amount_of_forms(player, 1))
-
- def kh_mcp(self, player):
- return self.kh_reflect(player) and (
- self.has(ItemName.MasterForm, player) or self.has(ItemName.FinalForm, player))
diff --git a/worlds/kh2/mod_template/mod.yml b/worlds/kh2/mod_template/mod.yml
deleted file mode 100644
index 4246132c2641..000000000000
--- a/worlds/kh2/mod_template/mod.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-assets:
-- method: binarc
- name: 00battle.bin
- source:
- - method: listpatch
- name: fmlv
- source:
- - name: FmlvList.yml
- type: fmlv
- type: List
- - method: listpatch
- name: lvup
- source:
- - name: LvupList.yml
- type: lvup
- type: List
- - method: listpatch
- name: bons
- source:
- - name: BonsList.yml
- type: bons
- type: List
-- method: binarc
- name: 03system.bin
- source:
- - method: listpatch
- name: trsr
- source:
- - name: TrsrList.yml
- type: trsr
- type: List
- - method: listpatch
- name: item
- source:
- - name: ItemList.yml
- type: item
- type: List
-title: Randomizer Seed
diff --git a/worlds/kh2/test/TestGoal.py b/worlds/kh2/test/TestGoal.py
deleted file mode 100644
index 97874da2d090..000000000000
--- a/worlds/kh2/test/TestGoal.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from . import KH2TestBase
-from ..Names import ItemName
-
-
-class TestDefault(KH2TestBase):
- options = {}
-
- def testEverything(self):
- self.collect_all_but([ItemName.Victory])
- self.assertBeatable(True)
-
-
-class TestLuckyEmblem(KH2TestBase):
- options = {
- "Goal": 1,
- }
-
- def testEverything(self):
- self.collect_all_but([ItemName.LuckyEmblem])
- self.assertBeatable(True)
-
-
-class TestHitList(KH2TestBase):
- options = {
- "Goal": 2,
- }
-
- def testEverything(self):
- self.collect_all_but([ItemName.Bounty])
- self.assertBeatable(True)
diff --git a/worlds/kh2/test/TestSlotData.py b/worlds/kh2/test/TestSlotData.py
deleted file mode 100644
index 656cd48d5a6f..000000000000
--- a/worlds/kh2/test/TestSlotData.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import unittest
-
-from test.general import setup_solo_multiworld
-from . import KH2TestBase
-from .. import KH2World, all_locations, item_dictionary_table, CheckDupingItems, AllWeaponSlot, KH2Item
-from ..Names import ItemName
-from ... import AutoWorldRegister
-from ...AutoWorld import call_all
-
-
-class TestLocalItems(KH2TestBase):
-
- def testSlotData(self):
- gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
- multiworld = setup_solo_multiworld(KH2World, gen_steps)
- for location in multiworld.get_locations():
- if location.item is None:
- location.place_locked_item(multiworld.worlds[1].create_item(ItemName.NoExperience))
- call_all(multiworld, "fill_slot_data")
- slotdata = multiworld.worlds[1].fill_slot_data()
- assert len(slotdata["LocalItems"]) > 0, f"{slotdata['LocalItems']} is empty"
diff --git a/worlds/kh2/test/__init__.py b/worlds/kh2/test/__init__.py
index dfef22762745..6cefe6e79197 100644
--- a/worlds/kh2/test/__init__.py
+++ b/worlds/kh2/test/__init__.py
@@ -1,4 +1,4 @@
-from test.TestBase import WorldTestBase
+from test.bases import WorldTestBase
class KH2TestBase(WorldTestBase):
diff --git a/worlds/kh2/test/test_fight_logic.py b/worlds/kh2/test/test_fight_logic.py
new file mode 100644
index 000000000000..0c47d132f0a0
--- /dev/null
+++ b/worlds/kh2/test/test_fight_logic.py
@@ -0,0 +1,19 @@
+from . import KH2TestBase
+
+
+class TestEasy(KH2TestBase):
+ options = {
+ "FightLogic": 0
+ }
+
+
+class TestNormal(KH2TestBase):
+ options = {
+ "FightLogic": 1
+ }
+
+
+class TestHard(KH2TestBase):
+ options = {
+ "FightLogic": 2
+ }
diff --git a/worlds/kh2/test/test_form_logic.py b/worlds/kh2/test/test_form_logic.py
new file mode 100644
index 000000000000..1cd850a985dd
--- /dev/null
+++ b/worlds/kh2/test/test_form_logic.py
@@ -0,0 +1,214 @@
+from . import KH2TestBase
+from ..Names import ItemName, LocationName
+
+global_all_possible_forms = [ItemName.ValorForm, ItemName.WisdomForm, ItemName.LimitForm, ItemName.MasterForm, ItemName.FinalForm] + [ItemName.AutoValor, ItemName.AutoWisdom, ItemName.AutoLimit, ItemName.AutoMaster, ItemName.AutoFinal]
+
+
+class KH2TestFormBase(KH2TestBase):
+ allForms = [ItemName.ValorForm, ItemName.WisdomForm, ItemName.LimitForm, ItemName.MasterForm, ItemName.FinalForm]
+ autoForms = [ItemName.AutoValor, ItemName.AutoWisdom, ItemName.AutoLimit, ItemName.AutoMaster, ItemName.AutoFinal]
+ allLevel2 = [LocationName.Valorlvl2, LocationName.Wisdomlvl2, LocationName.Limitlvl2, LocationName.Masterlvl2,
+ LocationName.Finallvl2]
+ allLevel3 = [LocationName.Valorlvl3, LocationName.Wisdomlvl3, LocationName.Limitlvl3, LocationName.Masterlvl3,
+ LocationName.Finallvl3]
+ allLevel4 = [LocationName.Valorlvl4, LocationName.Wisdomlvl4, LocationName.Limitlvl4, LocationName.Masterlvl4,
+ LocationName.Finallvl4]
+ allLevel5 = [LocationName.Valorlvl5, LocationName.Wisdomlvl5, LocationName.Limitlvl5, LocationName.Masterlvl5,
+ LocationName.Finallvl5]
+ allLevel6 = [LocationName.Valorlvl6, LocationName.Wisdomlvl6, LocationName.Limitlvl6, LocationName.Masterlvl6,
+ LocationName.Finallvl6]
+ allLevel7 = [LocationName.Valorlvl7, LocationName.Wisdomlvl7, LocationName.Limitlvl7, LocationName.Masterlvl7,
+ LocationName.Finallvl7]
+ driveToAuto = {
+ ItemName.FinalForm: ItemName.AutoFinal,
+ ItemName.MasterForm: ItemName.AutoMaster,
+ ItemName.LimitForm: ItemName.AutoLimit,
+ ItemName.WisdomForm: ItemName.AutoWisdom,
+ ItemName.ValorForm: ItemName.AutoValor,
+ }
+ AutoToDrive = {Auto: Drive for Drive, Auto in driveToAuto.items()}
+ driveFormMap = {
+ ItemName.ValorForm: [LocationName.Valorlvl2,
+ LocationName.Valorlvl3,
+ LocationName.Valorlvl4,
+ LocationName.Valorlvl5,
+ LocationName.Valorlvl6,
+ LocationName.Valorlvl7],
+ ItemName.WisdomForm: [LocationName.Wisdomlvl2,
+ LocationName.Wisdomlvl3,
+ LocationName.Wisdomlvl4,
+ LocationName.Wisdomlvl5,
+ LocationName.Wisdomlvl6,
+ LocationName.Wisdomlvl7],
+ ItemName.LimitForm: [LocationName.Limitlvl2,
+ LocationName.Limitlvl3,
+ LocationName.Limitlvl4,
+ LocationName.Limitlvl5,
+ LocationName.Limitlvl6,
+ LocationName.Limitlvl7],
+ ItemName.MasterForm: [LocationName.Masterlvl2,
+ LocationName.Masterlvl3,
+ LocationName.Masterlvl4,
+ LocationName.Masterlvl5,
+ LocationName.Masterlvl6,
+ LocationName.Masterlvl7],
+ ItemName.FinalForm: [LocationName.Finallvl2,
+ LocationName.Finallvl3,
+ LocationName.Finallvl4,
+ LocationName.Finallvl5,
+ LocationName.Finallvl6,
+ LocationName.Finallvl7],
+ }
+ # global_all_possible_forms = allForms + autoForms
+
+
+class TestDefaultForms(KH2TestFormBase):
+ """
+ Test default form access rules.
+ """
+ options = {
+ "AutoFormLogic": False,
+ "FinalFormLogic": "light_and_darkness"
+ }
+
+ def test_default_Auto_Form_Logic(self):
+ allPossibleForms = global_all_possible_forms
+ # this tests with a light and darkness in the inventory.
+ self.collect_all_but(allPossibleForms)
+ for form in self.allForms:
+ self.assertFalse((self.can_reach_location(self.driveFormMap[form][0])), form)
+ self.collect(self.get_item_by_name(self.driveToAuto[form]))
+ self.assertFalse((self.can_reach_location(self.driveFormMap[form][0])), form)
+
+ def test_default_Final_Form(self):
+ allPossibleForms = global_all_possible_forms
+ self.collect_all_but(allPossibleForms)
+ self.collect_by_name(ItemName.FinalForm)
+ self.assertTrue((self.can_reach_location(LocationName.Finallvl2)))
+ self.assertTrue((self.can_reach_location(LocationName.Finallvl3)))
+ self.assertFalse((self.can_reach_location(LocationName.Finallvl4)))
+
+ def test_default_without_LnD(self):
+ allPossibleForms = self.allForms
+ self.collect_all_but(allPossibleForms)
+ for form, levels in self.driveFormMap.items():
+ # final form is unique and breaks using this test. Tested above.
+ if levels[0] == LocationName.Finallvl2:
+ continue
+ for driveForm in self.allForms:
+ if self.count(driveForm) >= 1:
+ for _ in range(self.count(driveForm)):
+ self.remove(self.get_item_by_name(driveForm))
+ allFormsCopy = self.allForms.copy()
+ allFormsCopy.remove(form)
+ self.collect(self.get_item_by_name(form))
+ for _ in range(self.count(ItemName.LightDarkness)):
+ self.remove(self.get_item_by_name(ItemName.LightDarkness))
+ self.assertTrue((self.can_reach_location(levels[0])), levels[0])
+ self.assertTrue((self.can_reach_location(levels[1])), levels[1])
+ self.assertFalse((self.can_reach_location(levels[2])), levels[2])
+ for i in range(3):
+ self.collect(self.get_item_by_name(allFormsCopy[i]))
+ # for some reason after collecting a form it can pick up light and darkness
+ for _ in range(self.count(ItemName.LightDarkness)):
+ self.remove(self.get_item_by_name(ItemName.LightDarkness))
+
+ self.assertTrue((self.can_reach_location(levels[2 + i])))
+ if i < 2:
+ self.assertFalse((self.can_reach_location(levels[3 + i])))
+ else:
+ self.collect(self.get_item_by_name(allFormsCopy[i + 1]))
+ for _ in range(self.count(ItemName.LightDarkness)):
+ self.remove(self.get_item_by_name(ItemName.LightDarkness))
+ self.assertTrue((self.can_reach_location(levels[3 + i])))
+
+ def test_default_with_lnd(self):
+ allPossibleForms = self.allForms
+ self.collect_all_but(allPossibleForms)
+ for form, levels in self.driveFormMap.items():
+ if form != ItemName.FinalForm:
+ for driveForm in self.allForms:
+ for _ in range(self.count(driveForm)):
+ self.remove(self.get_item_by_name(driveForm))
+ allFormsCopy = self.allForms.copy()
+ allFormsCopy.remove(form)
+ self.collect(self.get_item_by_name(ItemName.LightDarkness))
+ self.assertFalse((self.can_reach_location(levels[0])))
+ self.collect(self.get_item_by_name(form))
+
+ self.assertTrue((self.can_reach_location(levels[0])))
+ self.assertTrue((self.can_reach_location(levels[1])))
+ self.assertTrue((self.can_reach_location(levels[2])))
+ self.assertFalse((self.can_reach_location(levels[3])))
+ for i in range(2):
+ self.collect(self.get_item_by_name(allFormsCopy[i]))
+ self.assertTrue((self.can_reach_location(levels[i + 3])))
+ if i <= 2:
+ self.assertFalse((self.can_reach_location(levels[i + 4])))
+
+
+class TestJustAForm(KH2TestFormBase):
+ # this test checks if you can unlock final form with just a form.
+ options = {
+ "AutoFormLogic": False,
+ "FinalFormLogic": "just_a_form"
+ }
+
+ def test_just_a_form_connections(self):
+ allPossibleForms = self.allForms
+ self.collect_all_but(allPossibleForms)
+ allPossibleForms.remove(ItemName.FinalForm)
+ for form, levels in self.driveFormMap.items():
+ for driveForm in self.allForms:
+ for _ in range(self.count(driveForm)):
+ self.remove(self.get_item_by_name(driveForm))
+ if form != ItemName.FinalForm:
+ # reset the forms
+ allFormsCopy = self.allForms.copy()
+ allFormsCopy.remove(form)
+ self.assertFalse((self.can_reach_location(levels[0])))
+ self.collect(self.get_item_by_name(form))
+ self.assertTrue((self.can_reach_location(levels[0])))
+ self.assertTrue((self.can_reach_location(levels[1])))
+ self.assertTrue((self.can_reach_location(levels[2])))
+
+ # level 4 of a form. This tests if the player can unlock final form.
+ self.assertFalse((self.can_reach_location(levels[3])))
+ # amount of forms left in the pool are 3. 1 already collected and one is final form.
+ for i in range(3):
+ allFormsCopy.remove(allFormsCopy[0])
+ # so we don't accidentally collect another form like light and darkness in the above tests.
+ self.collect_all_but(allFormsCopy)
+ self.assertTrue((self.can_reach_location(levels[3 + i])), levels[3 + i])
+ if i < 2:
+ self.assertFalse((self.can_reach_location(levels[4 + i])), levels[4 + i])
+
+
+class TestAutoForms(KH2TestFormBase):
+ options = {
+ "AutoFormLogic": True,
+ "FinalFormLogic": "light_and_darkness"
+ }
+
+ def test_Nothing(self):
+ KH2TestBase()
+
+ def test_auto_forms_level_progression(self):
+ allPossibleForms = self.allForms + [ItemName.LightDarkness]
+ # state has all auto forms
+ self.collect_all_but(allPossibleForms)
+ allPossibleFormsCopy = allPossibleForms.copy()
+ collectedDrives = []
+ i = 0
+ for form in allPossibleForms:
+ currentDriveForm = form
+ collectedDrives += [currentDriveForm]
+ allPossibleFormsCopy.remove(currentDriveForm)
+ self.collect_all_but(allPossibleFormsCopy)
+ for driveForm in self.allForms:
+ # +1 every iteration.
+ self.assertTrue((self.can_reach_location(self.driveFormMap[driveForm][i])), driveForm)
+ # making sure having the form still gives an extra drive level to its own form.
+ if driveForm in collectedDrives and i < 5:
+ self.assertTrue((self.can_reach_location(self.driveFormMap[driveForm][i + 1])), driveForm)
+ i += 1
diff --git a/worlds/kh2/test/test_goal.py b/worlds/kh2/test/test_goal.py
new file mode 100644
index 000000000000..1a481ad3d91f
--- /dev/null
+++ b/worlds/kh2/test/test_goal.py
@@ -0,0 +1,59 @@
+from . import KH2TestBase
+from ..Names import ItemName
+
+
+class TestDefault(KH2TestBase):
+ options = {}
+
+
+class TestThreeProofs(KH2TestBase):
+ options = {
+ "Goal": 0,
+ }
+
+
+class TestLuckyEmblem(KH2TestBase):
+ options = {
+ "Goal": 1,
+ }
+
+
+class TestHitList(KH2TestBase):
+ options = {
+ "Goal": 2,
+ }
+
+
+class TestLuckyEmblemHitlist(KH2TestBase):
+ options = {
+ "Goal": 3,
+ }
+
+
+class TestThreeProofsNoXemnas(KH2TestBase):
+ options = {
+ "Goal": 0,
+ "FinalXemnas": False,
+ }
+
+
+class TestLuckyEmblemNoXemnas(KH2TestBase):
+ options = {
+ "Goal": 1,
+ "FinalXemnas": False,
+ }
+
+
+class TestHitListNoXemnas(KH2TestBase):
+ options = {
+ "Goal": 2,
+ "FinalXemnas": False,
+ }
+
+
+class TestLuckyEmblemHitlistNoXemnas(KH2TestBase):
+ options = {
+ "Goal": 3,
+ "FinalXemnas": False,
+ }
+
diff --git a/worlds/ladx/LADXR/assembler.py b/worlds/ladx/LADXR/assembler.py
index 6c35fac4b3a8..c95d4dd9912c 100644
--- a/worlds/ladx/LADXR/assembler.py
+++ b/worlds/ladx/LADXR/assembler.py
@@ -757,7 +757,7 @@ def getLabels(self) -> ItemsView[str, int]:
def const(name: str, value: int) -> None:
name = name.upper()
- assert name not in CONST_MAP
+ assert name not in CONST_MAP or CONST_MAP[name] == value
CONST_MAP[name] = value
diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py
index 733ab314da81..e6f608a92180 100644
--- a/worlds/ladx/LADXR/generator.py
+++ b/worlds/ladx/LADXR/generator.py
@@ -3,6 +3,8 @@
import importlib.machinery
import os
import pkgutil
+from collections import defaultdict
+from typing import TYPE_CHECKING
from .romTables import ROMWithTables
from . import assembler
@@ -55,18 +57,25 @@
from . import hints
from .patches import bank34
+from .utils import formatText
+from ..Options import TrendyGame, Palette
+from .roomEditor import RoomEditor, Object
from .patches.aesthetics import rgb_to_bin, bin_to_rgb
from .locations.keyLocation import KeyLocation
from BaseClasses import ItemClassification
from ..Locations import LinksAwakeningLocation
-from ..Options import TrendyGame, Palette, MusicChangeCondition
+from ..Options import TrendyGame, Palette, MusicChangeCondition, BootsControls
+
+if TYPE_CHECKING:
+ from .. import LinksAwakeningWorld
# Function to generate a final rom, this patches the rom with all required patches
-def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, multiworld=None, player_name=None, player_names=[], player_id = 0):
+def generateRom(args, world: "LinksAwakeningWorld"):
rom_patches = []
+ player_names = list(world.multiworld.player_name.values())
rom = ROMWithTables(args.input_filename, rom_patches)
rom.player_names = player_names
@@ -80,10 +89,10 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
for pymod in pymods:
pymod.prePatch(rom)
- if settings.gfxmod:
- patches.aesthetics.gfxMod(rom, os.path.join("data", "sprites", "ladx", settings.gfxmod))
+ if world.ladxr_settings.gfxmod:
+ patches.aesthetics.gfxMod(rom, os.path.join("data", "sprites", "ladx", world.ladxr_settings.gfxmod))
- item_list = [item for item in logic.iteminfo_list if not isinstance(item, KeyLocation)]
+ item_list = [item for item in world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)]
assembler.resetConsts()
assembler.const("INV_SIZE", 16)
@@ -93,7 +102,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
assembler.const("wTradeSequenceItem2", 0xDB7F) # Normally used to store that we have exchanged the trade item, we use it to store flags of which trade items we have
assembler.const("wSeashellsCount", 0xDB41)
assembler.const("wGoldenLeaves", 0xDB42) # New memory location where to store the golden leaf counter
- assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available
+ assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available (and boots)
assembler.const("wCustomMessage", 0xC0A0)
# We store the link info in unused color dungeon flags, so it gets preserved in the savegame.
@@ -112,7 +121,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
assembler.const("wLinkSpawnDelay", 0xDE13)
#assembler.const("HARDWARE_LINK", 1)
- assembler.const("HARD_MODE", 1 if settings.hardmode != "none" else 0)
+ assembler.const("HARD_MODE", 1 if world.ladxr_settings.hardmode != "none" else 0)
patches.core.cleanup(rom)
patches.save.singleSaveSlot(rom)
@@ -126,7 +135,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
patches.core.easyColorDungeonAccess(rom)
patches.owl.removeOwlEvents(rom)
patches.enemies.fixArmosKnightAsMiniboss(rom)
- patches.bank3e.addBank3E(rom, auth, player_id, player_names)
+ patches.bank3e.addBank3E(rom, world.multi_key, world.player, player_names)
patches.bank3f.addBank3F(rom)
patches.bank34.addBank34(rom, item_list)
patches.core.removeGhost(rom)
@@ -134,13 +143,14 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
patches.core.fixWrongWarp(rom)
patches.core.alwaysAllowSecretBook(rom)
patches.core.injectMainLoop(rom)
-
+
from ..Options import ShuffleSmallKeys, ShuffleNightmareKeys
- if ap_settings["shuffle_small_keys"] != ShuffleSmallKeys.option_original_dungeon or ap_settings["shuffle_nightmare_keys"] != ShuffleNightmareKeys.option_original_dungeon:
+ if world.options.shuffle_small_keys != ShuffleSmallKeys.option_original_dungeon or\
+ world.options.shuffle_nightmare_keys != ShuffleNightmareKeys.option_original_dungeon:
patches.inventory.advancedInventorySubscreen(rom)
patches.inventory.moreSlots(rom)
- if settings.witch:
+ if world.ladxr_settings.witch:
patches.witch.updateWitch(rom)
patches.softlock.fixAll(rom)
patches.maptweaks.tweakMap(rom)
@@ -154,9 +164,9 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
patches.tarin.updateTarin(rom)
patches.fishingMinigame.updateFinishingMinigame(rom)
patches.health.upgradeHealthContainers(rom)
- if settings.owlstatues in ("dungeon", "both"):
+ if world.ladxr_settings.owlstatues in ("dungeon", "both"):
patches.owl.upgradeDungeonOwlStatues(rom)
- if settings.owlstatues in ("overworld", "both"):
+ if world.ladxr_settings.owlstatues in ("overworld", "both"):
patches.owl.upgradeOverworldOwlStatues(rom)
patches.goldenLeaf.fixGoldenLeaf(rom)
patches.heartPiece.fixHeartPiece(rom)
@@ -166,112 +176,119 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
patches.songs.upgradeMarin(rom)
patches.songs.upgradeManbo(rom)
patches.songs.upgradeMamu(rom)
- if settings.tradequest:
- patches.tradeSequence.patchTradeSequence(rom, settings.boomerang)
+ if world.ladxr_settings.tradequest:
+ patches.tradeSequence.patchTradeSequence(rom, world.ladxr_settings.boomerang)
else:
# Monkey bridge patch, always have the bridge there.
rom.patch(0x00, 0x333D, assembler.ASM("bit 4, e\njr Z, $05"), b"", fill_nop=True)
- patches.bowwow.fixBowwow(rom, everywhere=settings.bowwow != 'normal')
- if settings.bowwow != 'normal':
+ patches.bowwow.fixBowwow(rom, everywhere=world.ladxr_settings.bowwow != 'normal')
+ if world.ladxr_settings.bowwow != 'normal':
patches.bowwow.bowwowMapPatches(rom)
patches.desert.desertAccess(rom)
- if settings.overworld == 'dungeondive':
+ if world.ladxr_settings.overworld == 'dungeondive':
patches.overworld.patchOverworldTilesets(rom)
patches.overworld.createDungeonOnlyOverworld(rom)
- elif settings.overworld == 'nodungeons':
+ elif world.ladxr_settings.overworld == 'nodungeons':
patches.dungeon.patchNoDungeons(rom)
- elif settings.overworld == 'random':
+ elif world.ladxr_settings.overworld == 'random':
patches.overworld.patchOverworldTilesets(rom)
- mapgen.store_map(rom, logic.world.map)
+ mapgen.store_map(rom, world.ladxr_logic.world.map)
#if settings.dungeon_items == 'keysy':
# patches.dungeon.removeKeyDoors(rom)
# patches.reduceRNG.slowdownThreeOfAKind(rom)
patches.reduceRNG.fixHorseHeads(rom)
patches.bomb.onlyDropBombsWhenHaveBombs(rom)
- if ap_settings['music_change_condition'] == MusicChangeCondition.option_always:
+ if world.options.music_change_condition == MusicChangeCondition.option_always:
patches.aesthetics.noSwordMusic(rom)
- patches.aesthetics.reduceMessageLengths(rom, rnd)
+ patches.aesthetics.reduceMessageLengths(rom, world.random)
patches.aesthetics.allowColorDungeonSpritesEverywhere(rom)
- if settings.music == 'random':
- patches.music.randomizeMusic(rom, rnd)
- elif settings.music == 'off':
+ if world.ladxr_settings.music == 'random':
+ patches.music.randomizeMusic(rom, world.random)
+ elif world.ladxr_settings.music == 'off':
patches.music.noMusic(rom)
- if settings.noflash:
+ if world.ladxr_settings.noflash:
patches.aesthetics.removeFlashingLights(rom)
- if settings.hardmode == "oracle":
+ if world.ladxr_settings.hardmode == "oracle":
patches.hardMode.oracleMode(rom)
- elif settings.hardmode == "hero":
+ elif world.ladxr_settings.hardmode == "hero":
patches.hardMode.heroMode(rom)
- elif settings.hardmode == "ohko":
+ elif world.ladxr_settings.hardmode == "ohko":
patches.hardMode.oneHitKO(rom)
- if settings.superweapons:
+ if world.ladxr_settings.superweapons:
patches.weapons.patchSuperWeapons(rom)
- if settings.textmode == 'fast':
+ if world.ladxr_settings.textmode == 'fast':
patches.aesthetics.fastText(rom)
- if settings.textmode == 'none':
+ if world.ladxr_settings.textmode == 'none':
patches.aesthetics.fastText(rom)
patches.aesthetics.noText(rom)
- if not settings.nagmessages:
+ if not world.ladxr_settings.nagmessages:
patches.aesthetics.removeNagMessages(rom)
- if settings.lowhpbeep == 'slow':
+ if world.ladxr_settings.lowhpbeep == 'slow':
patches.aesthetics.slowLowHPBeep(rom)
- if settings.lowhpbeep == 'none':
+ if world.ladxr_settings.lowhpbeep == 'none':
patches.aesthetics.removeLowHPBeep(rom)
- if 0 <= int(settings.linkspalette):
- patches.aesthetics.forceLinksPalette(rom, int(settings.linkspalette))
+ if 0 <= int(world.ladxr_settings.linkspalette):
+ patches.aesthetics.forceLinksPalette(rom, int(world.ladxr_settings.linkspalette))
if args.romdebugmode:
# The default rom has this build in, just need to set a flag and we get this save.
rom.patch(0, 0x0003, "00", "01")
# Patch the sword check on the shopkeeper turning around.
- if settings.steal == 'never':
+ if world.ladxr_settings.steal == 'never':
rom.patch(4, 0x36F9, "FA4EDB", "3E0000")
- elif settings.steal == 'always':
+ elif world.ladxr_settings.steal == 'always':
rom.patch(4, 0x36F9, "FA4EDB", "3E0100")
- if settings.hpmode == 'inverted':
+ if world.ladxr_settings.hpmode == 'inverted':
patches.health.setStartHealth(rom, 9)
- elif settings.hpmode == '1':
+ elif world.ladxr_settings.hpmode == '1':
patches.health.setStartHealth(rom, 1)
patches.inventory.songSelectAfterOcarinaSelect(rom)
- if settings.quickswap == 'a':
+ if world.ladxr_settings.quickswap == 'a':
patches.core.quickswap(rom, 1)
- elif settings.quickswap == 'b':
+ elif world.ladxr_settings.quickswap == 'b':
patches.core.quickswap(rom, 0)
- world_setup = logic.world_setup
+ patches.core.addBootsControls(rom, world.options.boots_controls)
+
+
+ world_setup = world.ladxr_logic.world_setup
JUNK_HINT = 0.33
RANDOM_HINT= 0.66
# USEFUL_HINT = 1.0
# TODO: filter events, filter unshuffled keys
- all_items = multiworld.get_items()
- our_items = [item for item in all_items if item.player == player_id and item.location and item.code is not None and item.location.show_in_spoiler]
+ all_items = world.multiworld.get_items()
+ our_items = [item for item in all_items
+ if item.player == world.player
+ and item.location
+ and item.code is not None
+ and item.location.show_in_spoiler]
our_useful_items = [item for item in our_items if ItemClassification.progression in item.classification]
def gen_hint():
- chance = rnd.uniform(0, 1)
+ chance = world.random.uniform(0, 1)
if chance < JUNK_HINT:
return None
elif chance < RANDOM_HINT:
- location = rnd.choice(our_items).location
+ location = world.random.choice(our_items).location
else: # USEFUL_HINT
- location = rnd.choice(our_useful_items).location
+ location = world.random.choice(our_useful_items).location
- if location.item.player == player_id:
+ if location.item.player == world.player:
name = "Your"
else:
- name = f"{multiworld.player_name[location.item.player]}'s"
-
+ name = f"{world.multiworld.player_name[location.item.player]}'s"
+
if isinstance(location, LinksAwakeningLocation):
location_name = location.ladxr_item.metadata.name
else:
location_name = location.name
hint = f"{name} {location.item} is at {location_name}"
- if location.player != player_id:
- hint += f" in {multiworld.player_name[location.player]}'s world"
+ if location.player != world.player:
+ hint += f" in {world.multiworld.player_name[location.player]}'s world"
# Cap hint size at 85
# Realistically we could go bigger but let's be safe instead
@@ -279,7 +296,7 @@ def gen_hint():
return hint
- hints.addHints(rom, rnd, gen_hint)
+ hints.addHints(rom, world.random, gen_hint)
if world_setup.goal == "raft":
patches.goal.setRaftGoal(rom)
@@ -292,7 +309,7 @@ def gen_hint():
# Patch the generated logic into the rom
patches.chest.setMultiChest(rom, world_setup.multichest)
- if settings.overworld not in {"dungeondive", "random"}:
+ if world.ladxr_settings.overworld not in {"dungeondive", "random"}:
patches.entrances.changeEntrances(rom, world_setup.entrance_mapping)
for spot in item_list:
if spot.item and spot.item.startswith("*"):
@@ -311,22 +328,39 @@ def gen_hint():
patches.core.addFrameCounter(rom, len(item_list))
patches.core.warpHome(rom) # Needs to be done after setting the start location.
- patches.titleScreen.setRomInfo(rom, auth, seed_name, settings, player_name, player_id)
- if ap_settings["ap_title_screen"]:
+ patches.titleScreen.setRomInfo(rom, world.multi_key, world.multiworld.seed_name, world.ladxr_settings,
+ world.player_name, world.player)
+ if world.options.ap_title_screen:
patches.titleScreen.setTitleGraphics(rom)
patches.endscreen.updateEndScreen(rom)
patches.aesthetics.updateSpriteData(rom)
if args.doubletrouble:
patches.enemies.doubleTrouble(rom)
- if ap_settings["trendy_game"] != TrendyGame.option_normal:
+ if world.options.text_shuffle:
+ buckets = defaultdict(list)
+ # For each ROM bank, shuffle text within the bank
+ for n, data in enumerate(rom.texts._PointerTable__data):
+ # Don't muck up which text boxes are questions and which are statements
+ if type(data) != int and data and data != b'\xFF':
+ buckets[(rom.texts._PointerTable__banks[n], data[len(data) - 1] == 0xfe)].append((n, data))
+ for bucket in buckets.values():
+ # For each bucket, make a copy and shuffle
+ shuffled = bucket.copy()
+ world.random.shuffle(shuffled)
+ # Then put new text in
+ for bucket_idx, (orig_idx, data) in enumerate(bucket):
+ rom.texts[shuffled[bucket_idx][0]] = data
+
+
+ if world.options.trendy_game != TrendyGame.option_normal:
# TODO: if 0 or 4, 5, remove inaccurate conveyor tiles
- from .roomEditor import RoomEditor, Object
+
room_editor = RoomEditor(rom, 0x2A0)
- if ap_settings["trendy_game"] == TrendyGame.option_easy:
+ if world.options.trendy_game == TrendyGame.option_easy:
# Set physics flag on all objects
for i in range(0, 6):
rom.banks[0x4][0x6F1E + i -0x4000] = 0x4
@@ -337,7 +371,7 @@ def gen_hint():
# Add new conveyor to "push" yoshi (it's only a visual)
room_editor.objects.append(Object(5, 3, 0xD0))
- if int(ap_settings["trendy_game"]) >= TrendyGame.option_harder:
+ if world.options.trendy_game >= TrendyGame.option_harder:
"""
Data_004_76A0::
db $FC, $00, $04, $00, $00
@@ -351,13 +385,13 @@ def gen_hint():
TrendyGame.option_impossible: (3, 16),
}
def speed():
- return rnd.randint(*speeds[ap_settings["trendy_game"]])
- rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed()
+ return world.random.randint(*speeds[world.options.trendy_game])
+ rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed()
rom.banks[0x4][0x76A2-0x4000] = speed()
rom.banks[0x4][0x76A6-0x4000] = speed()
rom.banks[0x4][0x76A8-0x4000] = 0xFF - speed()
- if int(ap_settings["trendy_game"]) >= TrendyGame.option_hardest:
- rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed()
+ if world.options.trendy_game >= TrendyGame.option_hardest:
+ rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed()
rom.banks[0x4][0x76A3-0x4000] = speed()
rom.banks[0x4][0x76A5-0x4000] = speed()
rom.banks[0x4][0x76A7-0x4000] = 0xFF - speed()
@@ -374,14 +408,16 @@ def speed():
[0x0f, 0x38, 0x0f],
[0x30, 0x62, 0x30],
[0x8b, 0xac, 0x0f],
- [0x9b, 0xbc, 0x0f],
+ [0x9b, 0xbc, 0x0f],
]
for color in gb_colors:
for channel in range(3):
color[channel] = color[channel] * 31 // 0xbc
-
- palette = ap_settings["palette"]
+ if world.options.warp_improvements:
+ patches.core.addWarpImprovements(rom, world.options.additional_warp_points)
+
+ palette = world.options.palette
if palette != Palette.option_normal:
ranges = {
# Object palettes
@@ -410,7 +446,7 @@ def clamp(x, min, max):
for address in range(start, end, 2):
packed = (rom.banks[bank][address + 1] << 8) | rom.banks[bank][address]
r,g,b = bin_to_rgb(packed)
-
+
# 1 bit
if palette == Palette.option_1bit:
r &= 0b10000
@@ -447,8 +483,8 @@ def clamp(x, min, max):
SEED_LOCATION = 0x0134
# Patch over the title
- assert(len(auth) == 12)
- rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(auth))
+ assert(len(world.multi_key) == 12)
+ rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(world.multi_key))
for pymod in pymods:
pymod.postPatch(rom)
diff --git a/worlds/ladx/LADXR/locations/droppedKey.py b/worlds/ladx/LADXR/locations/droppedKey.py
index baa093bb3892..d7998633ce65 100644
--- a/worlds/ladx/LADXR/locations/droppedKey.py
+++ b/worlds/ladx/LADXR/locations/droppedKey.py
@@ -19,7 +19,7 @@ def __init__(self, room=None):
extra = 0x01F8
super().__init__(room, extra)
def patch(self, rom, option, *, multiworld=None):
- if (option.startswith(MAP) and option != MAP) or (option.startswith(COMPASS) and option != COMPASS) or option.startswith(STONE_BEAK) or (option.startswith(NIGHTMARE_KEY) and option != NIGHTMARE_KEY )or (option.startswith(KEY) and option != KEY):
+ if (option.startswith(MAP) and option != MAP) or (option.startswith(COMPASS) and option != COMPASS) or (option.startswith(STONE_BEAK) and option != STONE_BEAK) or (option.startswith(NIGHTMARE_KEY) and option != NIGHTMARE_KEY) or (option.startswith(KEY) and option != KEY):
if option[-1] == 'P':
print(option)
if self._location.dungeon == int(option[-1]) and multiworld is None and self.room not in {0x166, 0x223}:
diff --git a/worlds/ladx/LADXR/locations/startItem.py b/worlds/ladx/LADXR/locations/startItem.py
index 95dd6ba54abd..0421c1d6d865 100644
--- a/worlds/ladx/LADXR/locations/startItem.py
+++ b/worlds/ladx/LADXR/locations/startItem.py
@@ -10,7 +10,6 @@ class StartItem(DroppedKey):
# We need to give something here that we can use to progress.
# FEATHER
OPTIONS = [SWORD, SHIELD, POWER_BRACELET, OCARINA, BOOMERANG, MAGIC_ROD, TAIL_KEY, SHOVEL, HOOKSHOT, PEGASUS_BOOTS, MAGIC_POWDER, BOMB]
-
MULTIWORLD = False
def __init__(self):
diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm b/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm
index b19e879dc30e..57771c17b3ca 100644
--- a/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm
+++ b/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm
@@ -51,7 +51,7 @@ GiveItemFromChest:
dw ChestBow ; CHEST_BOW
dw ChestWithItem ; CHEST_HOOKSHOT
dw ChestWithItem ; CHEST_MAGIC_ROD
- dw ChestWithItem ; CHEST_PEGASUS_BOOTS
+ dw Boots ; CHEST_PEGASUS_BOOTS
dw ChestWithItem ; CHEST_OCARINA
dw ChestWithItem ; CHEST_FEATHER
dw ChestWithItem ; CHEST_SHOVEL
@@ -273,6 +273,13 @@ ChestMagicPowder:
ld [$DB4C], a
jp ChestWithItem
+Boots:
+ ; We use DB6D to store which tunics we have available
+ ; ...and the boots
+ ld a, [wCollectedTunics]
+ or $04
+ ld [wCollectedTunics], a
+ jp ChestWithItem
Flippers:
ld a, $01
diff --git a/worlds/ladx/LADXR/patches/core.py b/worlds/ladx/LADXR/patches/core.py
index a202e661f945..f4752c82e3da 100644
--- a/worlds/ladx/LADXR/patches/core.py
+++ b/worlds/ladx/LADXR/patches/core.py
@@ -1,9 +1,11 @@
+from .. import assembler
from ..assembler import ASM
from ..entranceInfo import ENTRANCE_INFO
from ..roomEditor import RoomEditor, ObjectWarp, ObjectHorizontal
from ..backgroundEditor import BackgroundEditor
from .. import utils
+from ...Options import BootsControls
def bugfixWrittingWrongRoomStatus(rom):
# The normal rom contains a pretty nasty bug where door closing triggers in D7/D8 can effect doors in
@@ -255,8 +257,9 @@ def warpHome(rom):
""" % (type, map, room, x, y)), fill_nop=True)
# Patch the RAM clear not to delete our custom dialog when we screen transition
+ # This is kind of horrible as it relies on bank 1 being loaded, lol
rom.patch(0x01, 0x042C, "C629", "6B7E")
- rom.patch(0x01, 0x3E6B, 0x3FFF, ASM("""
+ rom.patch(0x01, 0x3E6B, 0x3E7B, ASM("""
ld bc, $A0
call $29DC
ld bc, $1200
@@ -390,7 +393,7 @@ def addFrameCounter(rom, check_count):
db $20, $20, $20, $00 ;I
db $20, $28, $28, $00 ;M
db $20, $30, $18, $00 ;E
-
+
db $20, $70, $16, $00 ;D
db $20, $78, $18, $00 ;E
db $20, $80, $10, $00 ;A
@@ -407,7 +410,7 @@ def addFrameCounter(rom, check_count):
db $68, $38, $%02x, $00 ;0
db $68, $40, $%02x, $00 ;0
db $68, $48, $%02x, $00 ;0
-
+
""" % ((((check_count // 100) % 10) * 2) | 0x40, (((check_count // 10) % 10) * 2) | 0x40, ((check_count % 10) * 2) | 0x40), 0x469D), fill_nop=True)
# Lower line of credits roll into XX XX XX
rom.patch(0x17, 0x0784, 0x082D, ASM("""
@@ -424,7 +427,7 @@ def addFrameCounter(rom, check_count):
call updateOAM
ld a, [$B001] ; seconds
call updateOAM
-
+
ld a, [$DB58] ; death count high
call updateOAM
ld a, [$DB57] ; death count low
@@ -472,7 +475,7 @@ def addFrameCounter(rom, check_count):
db $68, $18, $40, $00 ;0
db $68, $20, $40, $00 ;0
db $68, $28, $40, $00 ;0
-
+
""", 0x4784), fill_nop=True)
# Grab the "mostly" complete A-Z font
@@ -537,3 +540,295 @@ def addFrameCounter(rom, check_count):
gfx_low = "\n".join([line.split(" ")[n] for line in tile_graphics.split("\n")[8:]])
rom.banks[0x38][0x1400+n*0x20:0x1410+n*0x20] = utils.createTileData(gfx_high)
rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low)
+
+def addBootsControls(rom, boots_controls: BootsControls):
+ if boots_controls == BootsControls.option_vanilla:
+ return
+ consts = {
+ "INVENTORY_PEGASUS_BOOTS": 0x8,
+ "INVENTORY_POWER_BRACELET": 0x3,
+ "UsePegasusBoots": 0x1705,
+ "J_A": (1 << 4),
+ "J_B": (1 << 5),
+ "wAButtonSlot": 0xDB01,
+ "wBButtonSlot": 0xDB00,
+ "wPegasusBootsChargeMeter": 0xC14B,
+ "hPressedButtonsMask": 0xCB
+ }
+ for c,v in consts.items():
+ assembler.const(c, v)
+
+ BOOTS_START_ADDR = 0x11E8
+ condition = {
+ BootsControls.option_bracelet: """
+ ld a, [hl]
+ ; Check if we are using the bracelet
+ cp INVENTORY_POWER_BRACELET
+ jr z, .yesBoots
+ """,
+ BootsControls.option_press_a: """
+ ; Check if we are using the A slot
+ cp J_A
+ jr z, .yesBoots
+ ld a, [hl]
+ """,
+ BootsControls.option_press_b: """
+ ; Check if we are using the B slot
+ cp J_B
+ jr z, .yesBoots
+ ld a, [hl]
+ """
+ }[boots_controls.value]
+
+ # The new code fits exactly within Nintendo's poorly space optimzied code while having more features
+ boots_code = assembler.ASM("""
+CheckBoots:
+ ; check if we own boots
+ ld a, [wCollectedTunics]
+ and $04
+ ; if not, move on to the next inventory item (shield)
+ jr z, .out
+
+ ; Check the B button
+ ld hl, wBButtonSlot
+ ld d, J_B
+ call .maybeBoots
+
+ ; Check the A button
+ inc l ; l = wAButtonSlot - done this way to save a byte or two
+ ld d, J_A
+ call .maybeBoots
+
+ ; If neither, reset charge meter and bail
+ xor a
+ ld [wPegasusBootsChargeMeter], a
+ jr .out
+
+.maybeBoots:
+ ; Check if we are holding this button even
+ ldh a, [hPressedButtonsMask]
+ and d
+ ret z
+ """
+ # Check the special condition (also loads the current item for button into a)
+ + condition +
+ """
+ ; Check if we are just using boots regularly
+ cp INVENTORY_PEGASUS_BOOTS
+ ret nz
+.yesBoots:
+ ; We're using boots! Do so.
+ call UsePegasusBoots
+ ; If we return now we will go back into CheckBoots, we don't want that
+ ; We instead want to move onto the next item
+ ; but if we don't cleanup, the next "ret" will take us back there again
+ ; So we pop the return address off of the stack
+ pop af
+.out:
+ """, BOOTS_START_ADDR)
+
+
+
+ original_code = 'fa00dbfe08200ff0cbe6202805cd05171804afea4bc1fa01dbfe08200ff0cbe6102805cd05171804afea4bc1'
+ rom.patch(0, BOOTS_START_ADDR, original_code, boots_code, fill_nop=True)
+
+def addWarpImprovements(rom, extra_warps):
+ # Patch in a warp icon
+ tile = utils.createTileData( \
+"""11111111
+10000000
+10200320
+10323200
+10033300
+10023230
+10230020
+10000000""", key="0231")
+ MINIMAP_BASE = 0x3800
+
+ # This is replacing a junk tile never used on the minimap
+ rom.banks[0x2C][MINIMAP_BASE + len(tile) * 0x65 : MINIMAP_BASE + len(tile) * 0x66] = tile
+
+ # Allow using ENTITY_WARP for finding which map sections are warps
+ # Interesting - 3CA0 should be free, but something has pushed all the code forward a byte
+ rom.patch(0x02, 0x3CA1, None, ASM("""
+ ld e, $0F
+ ld d, $00
+ warp_search_loop:
+ ; Warp search loop
+ ld hl, $C3A0
+ add hl, de ; $5FE1: $19
+ ld a, [hl] ; $5FE2: $7E
+ cp $61 ; ENTITY_WARP
+ jr nz, search_continue ; if it's not a warp, check the next one
+ ld hl, $C280
+ add hl, de
+ ld a, [hl]
+ and a
+ jr z, search_continue ; if this is despawned, check the next one
+ found:
+ jp $511B ; found
+ search_continue:
+ dec e
+ ld a, e
+ cp $FF
+ jr nz, warp_search_loop
+
+ not_found:
+ jp $512B
+
+ """))
+
+ # Insert redirect to above code
+ rom.patch(0x02, 0x1109, ASM("""
+ ldh a, [$F6]
+ cp 1
+
+ """), ASM("""
+ jp $7CA1
+ nop
+ """))
+ # Leaves some extra bytes behind, if we need more space in 0x02
+
+ # On warp hole, open map instead
+ rom.patch(0x19, 0x1DB9, None, ASM("""
+ ld a, 7 ; Set GAMEPLAY_MAP
+ ld [$DB95], a
+ ld a, 0 ; reset subtype
+ ld [$DB96], a
+ ld a, 1 ; Set flag for using teleport
+ ld [$FFDD], a
+
+ ret
+ """), fill_nop=True)
+
+ # Patch over some instructions that decided if we are in debug mode holding some
+ # buttons with instead checking for FFDD (why FFDD? It appears to be never used anywhere, so we repurpose it for "is in teleport mode")
+ rom.banks[0x01][0x17B8] = 0xDD
+ rom.banks[0x01][0x17B9] = 0xFF
+ rom.banks[0x01][0x17FD] = 0xDD
+ rom.banks[0x01][0x17FE] = 0xFF
+
+ # If in warp mode, don't allow manual exit
+ rom.patch(0x01, 0x1800, "20021E60", ASM("jp nz, $5818"), fill_nop=True)
+
+ # Allow warp with just B
+ rom.banks[0x01][0x17C0] = 0x20
+
+ # Allow cursor to move over black squares
+ # This allows warping to undiscovered areas - a fine cheat, but needs a check for wOverworldRoomStatus in the warp code
+ CHEAT_WARP_ANYWHERE = False
+ if CHEAT_WARP_ANYWHERE:
+ rom.patch(0x01, 0x1AE8, None, ASM("jp $5AF5"))
+
+ # This disables the arrows around the selection bubble
+ #rom.patch(0x01, 0x1B6F, None, ASM("ret"), fill_nop=True)
+
+ # Fix lag when moving the cursor
+ # One option - just disable the delay code
+ #rom.patch(0x01, 0x1A76, 0x1A76+3, ASM("xor a"), fill_nop=True)
+ #rom.banks[0x01][0x1A7C] = 0
+ # Another option - just remove the animation
+ rom.banks[0x01][0x1B20] = 0
+ rom.banks[0x01][0x1B3B] = 0
+
+ # Patch the icon for all teleports
+ all_warps = [0x01, 0x95, 0x2C, 0xEC]
+
+
+ if extra_warps:
+ # mamu
+ all_warps.append(0x45)
+ # Tweak the flute location
+ rom.banks[0x14][0x0E95] += 0x10
+ rom.banks[0x14][0x0EA3] += 0x01
+
+ mamu_pond = RoomEditor(rom, 0x45)
+ # Remove some tall grass so we can add a warp instead
+ mamu_pond.changeObject(1, 6, 0xE8)
+ mamu_pond.moveObject(1, 6, 3, 5)
+ mamu_pond.addEntity(3, 5, 0x61)
+
+ mamu_pond.store(rom)
+
+ # eagle
+ all_warps.append(0x0F)
+ room = RoomEditor(rom, 0x0F)
+ # Move one cliff edge and change it into a pit
+ room.changeObject(7, 6, 0xE8)
+ room.moveObject(7, 6, 6, 4)
+
+ # Add the warp
+ room.addEntity(6, 4, 0x61)
+ # move the two corners
+ room.moveObject(6, 7, 7, 7)
+ room.moveObject(6, 6, 7, 6)
+ for object in room.objects:
+ # Extend the lower wall
+ if ((object.x == 0 and object.y == 7)
+ # Extend the lower floor
+ or (object.x == 0 and object.y == 6)):
+ room.overlay[object.x + object.count + object.y * 10] = object.type_id
+ object.count += 1
+ room.store(rom)
+
+ for warp in all_warps:
+ # Set icon
+ rom.banks[0x20][0x168B + warp] = 0x55
+ # Set text
+ if not rom.banks[0x01][0x1959 + warp]:
+ rom.banks[0x01][0x1959 + warp] = 0x42
+ # Set palette
+ # rom.banks[0x20][0x178B + 0x95] = 0x1
+
+ # Setup [?!] icon on map and associated text
+ rom.banks[0x01][0x1909 + 0x42] = 0x2B
+ rom.texts[0x02B] = utils.formatText('Warp')
+
+ # call warp function (why not just jmp?!)
+ rom.patch(0x01, 0x17C3, None, ASM("""
+ call $7E7B
+ ret
+ """))
+
+ # Build a switch statement by hand
+ warp_jump = "".join(f"cp ${hex(warp)[2:]}\njr z, success\n" for warp in all_warps)
+
+ rom.patch(0x01, 0x3E7B, None, ASM(f"""
+TeleportHandler:
+
+ ld a, [$DBB4] ; Load the current selected tile
+ ; TODO: check if actually revealed so we can have free movement
+ ; Check cursor against different tiles to see if we are selecting a warp
+ {warp_jump}
+ jr exit
+
+success:
+ ld a, $0B
+ ld [$DB95], a ; Gameplay type
+ xor a
+ ld [$D401], a ; wWarp0MapCategory
+ ldh [$DD], a ; unset teleport flag(!!!)
+ ld [$D402], a ; wWarp0Map
+ ld a, [$DBB4] ; wDBB4
+ ld [$D403], a ; wWarp0Room
+
+ ld a, $68
+ ld [$D404], a ; wWarp0DestinationX
+ ldh [$98], a ; LinkPositionY
+ ld [$D475], a
+ ld a, $70
+ ld [$D405], a ; wWarp0DestinationY
+ ldh [$99], a ; LinkPositionX
+ ld a, $66
+ ld [$D416], a ; wWarp0PositionTileIndex
+ ld a, $07
+ ld [$DB96], a ; wGameplaySubtype
+ ldh a, [$A2]
+ ld [$DBC8], a
+ call $0C83 ; ApplyMapFadeOutTransition
+ xor a ; $5DF3: $AF
+ ld [$C167], a ; $5DF4: $EA $67 $C1
+
+exit:
+ ret
+ """))
diff --git a/worlds/ladx/LADXR/patches/owl.py b/worlds/ladx/LADXR/patches/owl.py
index b22386a6cb8f..47e575191a31 100644
--- a/worlds/ladx/LADXR/patches/owl.py
+++ b/worlds/ladx/LADXR/patches/owl.py
@@ -11,15 +11,17 @@ def removeOwlEvents(rom):
re.removeEntities(0x41)
re.store(rom)
# Clear texts used by the owl. Potentially reused somewhere o else.
- rom.texts[0x0D9] = b'\xff' # used by boomerang
# 1 Used by empty chest (master stalfos message)
# 8 unused (0x0C0-0x0C7)
# 1 used by bowwow in chest
# 1 used by item for other player message
# 2 used by arrow chest messages
# 2 used by tunics
- for idx in range(0x0BE, 0x0CE):
- rom.texts[idx] = b'\xff'
+
+ # Undoing this, we use it for text shuffle now
+ #rom.texts[0x0D9] = b'\xff' # used by boomerang
+ # for idx in range(0x0BE, 0x0CE):
+ # rom.texts[idx] = b'\xff'
# Patch the owl entity into a ghost to allow refill of powder/bombs/arrows
diff --git a/worlds/ladx/LADXR/patches/phone.py b/worlds/ladx/LADXR/patches/phone.py
index f38745606c38..a2f3939a08a1 100644
--- a/worlds/ladx/LADXR/patches/phone.py
+++ b/worlds/ladx/LADXR/patches/phone.py
@@ -2,34 +2,35 @@
def patchPhone(rom):
- rom.texts[0x141] = b""
- rom.texts[0x142] = b""
- rom.texts[0x143] = b""
- rom.texts[0x144] = b""
- rom.texts[0x145] = b""
- rom.texts[0x146] = b""
- rom.texts[0x147] = b""
- rom.texts[0x148] = b""
- rom.texts[0x149] = b""
- rom.texts[0x14A] = b""
- rom.texts[0x14B] = b""
- rom.texts[0x14C] = b""
- rom.texts[0x14D] = b""
- rom.texts[0x14E] = b""
- rom.texts[0x14F] = b""
- rom.texts[0x16E] = b""
- rom.texts[0x1FD] = b""
- rom.texts[0x228] = b""
- rom.texts[0x229] = b""
- rom.texts[0x22A] = b""
- rom.texts[0x240] = b""
- rom.texts[0x241] = b""
- rom.texts[0x242] = b""
- rom.texts[0x243] = b""
- rom.texts[0x244] = b""
- rom.texts[0x245] = b""
- rom.texts[0x247] = b""
- rom.texts[0x248] = b""
+ # reenabled for text shuffle
+# rom.texts[0x141] = b""
+# rom.texts[0x142] = b""
+# rom.texts[0x143] = b""
+# rom.texts[0x144] = b""
+# rom.texts[0x145] = b""
+# rom.texts[0x146] = b""
+# rom.texts[0x147] = b""
+# rom.texts[0x148] = b""
+# rom.texts[0x149] = b""
+# rom.texts[0x14A] = b""
+# rom.texts[0x14B] = b""
+# rom.texts[0x14C] = b""
+# rom.texts[0x14D] = b""
+# rom.texts[0x14E] = b""
+# rom.texts[0x14F] = b""
+# rom.texts[0x16E] = b""
+# rom.texts[0x1FD] = b""
+# rom.texts[0x228] = b""
+# rom.texts[0x229] = b""
+# rom.texts[0x22A] = b""
+# rom.texts[0x240] = b""
+# rom.texts[0x241] = b""
+# rom.texts[0x242] = b""
+# rom.texts[0x243] = b""
+# rom.texts[0x244] = b""
+# rom.texts[0x245] = b""
+# rom.texts[0x247] = b""
+# rom.texts[0x248] = b""
rom.patch(0x06, 0x2A8F, 0x2BBC, ASM("""
; We use $DB6D to store which tunics we have. This is normally the Dungeon9 instrument, which does not exist.
ld a, [$DC0F]
diff --git a/worlds/ladx/LADXR/pointerTable.py b/worlds/ladx/LADXR/pointerTable.py
index 9b8d49466c02..a1a92ba1780b 100644
--- a/worlds/ladx/LADXR/pointerTable.py
+++ b/worlds/ladx/LADXR/pointerTable.py
@@ -116,7 +116,10 @@ def store(self, rom):
rom.banks[ptr_bank][ptr_addr] = pointer & 0xFF
rom.banks[ptr_bank][ptr_addr + 1] = (pointer >> 8) | 0x40
- for n, s in enumerate(self.__data):
+ data = list(enumerate(self.__data))
+ data.sort(key=lambda t: type(t[1]) == int or -len(t[1]))
+
+ for n, s in data:
if isinstance(s, int):
pointer = s
else:
diff --git a/worlds/ladx/LADXR/utils.py b/worlds/ladx/LADXR/utils.py
index fcf1d2bb56e7..5f8b2685550d 100644
--- a/worlds/ladx/LADXR/utils.py
+++ b/worlds/ladx/LADXR/utils.py
@@ -146,7 +146,7 @@ def setReplacementName(key: str, value: str) -> None:
def formatText(instr: str, *, center: bool = False, ask: Optional[str] = None) -> bytes:
instr = instr.format(**_NAMES)
- s = instr.encode("ascii")
+ s = instr.encode("ascii", errors="replace")
s = s.replace(b"'", b"^")
def padLine(line: bytes) -> bytes:
@@ -169,7 +169,7 @@ def padLine(line: bytes) -> bytes:
if result_line:
result += padLine(result_line)
if ask is not None:
- askbytes = ask.encode("ascii")
+ askbytes = ask.encode("ascii", errors="replace")
result = result.rstrip()
while len(result) % 32 != 16:
result += b' '
diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py
index 6c89db3891b5..f29355f2ba86 100644
--- a/worlds/ladx/Locations.py
+++ b/worlds/ladx/Locations.py
@@ -60,13 +60,11 @@ class LinksAwakeningLocation(Location):
def __init__(self, player: int, region, ladxr_item):
name = meta_to_name(ladxr_item.metadata)
+ address = None
- self.event = ladxr_item.event is not None
- if self.event:
+ if ladxr_item.event is not None:
name = ladxr_item.event
-
- address = None
- if not self.event:
+ else:
address = locations_to_id[name]
super().__init__(player, name, address)
self.parent_region = region
@@ -124,13 +122,13 @@ def get(self, item, default):
# Don't allow any money usage if you can't get back wasted rupees
if item == "RUPEES":
if can_farm_rupees(self.state, self.player):
- return self.state.prog_items["RUPEES", self.player]
+ return self.state.prog_items[self.player]["RUPEES"]
return 0
elif item.endswith("_USED"):
return 0
else:
item = ladxr_item_to_la_item_name[item]
- return self.state.prog_items.get((item, self.player), default)
+ return self.state.prog_items[self.player].get(item, default)
class LinksAwakeningEntrance(Entrance):
@@ -219,7 +217,7 @@ def print_items(n):
r = LinksAwakeningRegion(
name=name, ladxr_region=l, hint="", player=player, world=multiworld)
- r.locations = [LinksAwakeningLocation(player, r, i) for i in l.items]
+ r.locations += [LinksAwakeningLocation(player, r, i) for i in l.items]
regions[l] = r
for ladxr_location in logic.location_list:
diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py
index 4c85e5c3d0d9..c5dcc080537c 100644
--- a/worlds/ladx/Options.py
+++ b/worlds/ladx/Options.py
@@ -1,7 +1,9 @@
+from dataclasses import dataclass
+
import os.path
import typing
import logging
-from Options import Choice, Option, Toggle, DefaultOnToggle, Range, FreeText
+from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions, OptionGroup
from collections import defaultdict
import Utils
@@ -14,7 +16,7 @@ class LADXROption:
def to_ladxr_option(self, all_options):
if not self.ladxr_name:
return None, None
-
+
return (self.ladxr_name, self.name_lookup[self.value].replace("_", ""))
@@ -32,33 +34,48 @@ class Logic(Choice, LADXROption):
option_hard = 2
option_glitched = 3
option_hell = 4
-
+
default = option_normal
+
class TradeQuest(DefaultOffToggle, LADXROption):
"""
[On] adds the trade items to the pool (the trade locations will always be local items)
[Off] (default) doesn't add them
"""
+ display_name = "Trade Quest"
ladxr_name = "tradequest"
+
+class TextShuffle(DefaultOffToggle):
+ """
+ [On] Shuffles all the text in the game
+ [Off] (default) doesn't shuffle them.
+ """
+ display_name = "Text Shuffle"
+
+
class Rooster(DefaultOnToggle, LADXROption):
"""
[On] Adds the rooster to the item pool.
[Off] The rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.
"""
+ display_name = "Rooster"
ladxr_name = "rooster"
+
class Boomerang(Choice):
"""
[Normal] requires Magnifying Lens to get the boomerang.
[Gift] The boomerang salesman will give you a random item, and the boomerang is shuffled.
"""
-
+ display_name = "Boomerang"
+
normal = 0
gift = 1
default = gift
+
class EntranceShuffle(Choice, LADXROption):
"""
[WARNING] Experimental, may fail to fill
@@ -67,32 +84,38 @@ class EntranceShuffle(Choice, LADXROption):
If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the non-connector entrance pool.
Note, some entrances can lead into water, use the warp-to-home from the save&quit menu to escape this."""
- #[Advanced] Simple, but two-way connector caves are shuffled in their own pool as well.
- #[Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool.
- #[Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool.
+ # [Advanced] Simple, but two-way connector caves are shuffled in their own pool as well.
+ # [Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool.
+ # [Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool.
option_none = 0
option_simple = 1
- #option_advanced = 2
- #option_expert = 3
- #option_insanity = 4
+ # option_advanced = 2
+ # option_expert = 3
+ # option_insanity = 4
default = option_none
+ display_name = "Experimental Entrance Shuffle"
ladxr_name = "entranceshuffle"
+
class DungeonShuffle(DefaultOffToggle, LADXROption):
"""
[WARNING] Experimental, may fail to fill
Randomizes dungeon entrances within eachother
"""
+ display_name = "Experimental Dungeon Shuffle"
ladxr_name = "dungeonshuffle"
+
class APTitleScreen(DefaultOnToggle):
"""
Enables AP specific title screen and disables the intro cutscene
"""
-
+ display_name = "AP Title Screen"
+
class BossShuffle(Choice):
+ display_name = "Boss Shuffle"
none = 0
shuffle = 1
random = 2
@@ -100,15 +123,18 @@ class BossShuffle(Choice):
class DungeonItemShuffle(Choice):
+ display_name = "Dungeon Item Shuffle"
option_original_dungeon = 0
option_own_dungeons = 1
option_own_world = 2
option_any_world = 3
option_different_world = 4
- #option_delete = 5
- #option_start_with = 6
+ # option_delete = 5
+ # option_start_with = 6
alias_true = 3
alias_false = 0
+ ladxr_item: str
+
class ShuffleNightmareKeys(DungeonItemShuffle):
"""
@@ -119,8 +145,10 @@ class ShuffleNightmareKeys(DungeonItemShuffle):
[Any World] The item could be anywhere
[Different World] The item will be somewhere in another world
"""
+ display_name = "Shuffle Nightmare Keys"
ladxr_item = "NIGHTMARE_KEY"
+
class ShuffleSmallKeys(DungeonItemShuffle):
"""
Shuffle Small Keys
@@ -130,7 +158,10 @@ class ShuffleSmallKeys(DungeonItemShuffle):
[Any World] The item could be anywhere
[Different World] The item will be somewhere in another world
"""
+ display_name = "Shuffle Small Keys"
ladxr_item = "KEY"
+
+
class ShuffleMaps(DungeonItemShuffle):
"""
Shuffle Dungeon Maps
@@ -140,8 +171,10 @@ class ShuffleMaps(DungeonItemShuffle):
[Any World] The item could be anywhere
[Different World] The item will be somewhere in another world
"""
+ display_name = "Shuffle Maps"
ladxr_item = "MAP"
+
class ShuffleCompasses(DungeonItemShuffle):
"""
Shuffle Dungeon Compasses
@@ -151,8 +184,10 @@ class ShuffleCompasses(DungeonItemShuffle):
[Any World] The item could be anywhere
[Different World] The item will be somewhere in another world
"""
+ display_name = "Shuffle Compasses"
ladxr_item = "COMPASS"
+
class ShuffleStoneBeaks(DungeonItemShuffle):
"""
Shuffle Owl Beaks
@@ -162,8 +197,27 @@ class ShuffleStoneBeaks(DungeonItemShuffle):
[Any World] The item could be anywhere
[Different World] The item will be somewhere in another world
"""
+ display_name = "Shuffle Stone Beaks"
ladxr_item = "STONE_BEAK"
+
+class ShuffleInstruments(DungeonItemShuffle):
+ """
+ Shuffle Instruments
+ [Original Dungeon] The item will be within its original dungeon
+ [Own Dungeons] The item will be within a dungeon in your world
+ [Own World] The item will be somewhere in your world
+ [Any World] The item could be anywhere
+ [Different World] The item will be somewhere in another world
+ [Vanilla] The item will be in its vanilla location in your world
+ """
+ display_name = "Shuffle Instruments"
+ ladxr_item = "INSTRUMENT"
+ default = 100
+ option_vanilla = 100
+ alias_false = 100
+
+
class Goal(Choice, LADXROption):
"""
The Goal of the game
@@ -176,7 +230,7 @@ class Goal(Choice, LADXROption):
option_instruments = 1
option_seashells = 2
option_open = 3
-
+
default = option_instruments
def to_ladxr_option(self, all_options):
@@ -185,31 +239,38 @@ def to_ladxr_option(self, all_options):
else:
return LADXROption.to_ladxr_option(self, all_options)
+
class InstrumentCount(Range, LADXROption):
"""
Sets the number of instruments required to open the Egg
"""
+ display_name = "Instrument Count"
ladxr_name = None
range_start = 0
range_end = 8
default = 8
+
class NagMessages(DefaultOffToggle, LADXROption):
"""
Controls if nag messages are shown when rocks and crystals are touched. Useful for glitches, annoying for everyone else.
"""
-
+ display_name = "Nag Messages"
ladxr_name = "nagmessages"
+
class MusicChangeCondition(Choice):
"""
Controls how the music changes.
[Sword] When you pick up a sword, the music changes
[Always] You always have the post-sword music
"""
+ display_name = "Music Change Condition"
option_sword = 0
option_always = 1
default = option_always
+
+
# Setting('hpmode', 'Gameplay', 'm', 'Health mode', options=[('default', '', 'Normal'), ('inverted', 'i', 'Inverted'), ('1', '1', 'Start with 1 heart'), ('low', 'l', 'Low max')], default='default',
# description="""
# [Normal} health works as you would expect.
@@ -234,10 +295,12 @@ class Bowwow(Choice):
[Normal] BowWow is in the item pool, but can be logically expected as a damage source.
[Swordless] The progressive swords are removed from the item pool.
"""
+ display_name = "BowWow"
normal = 0
swordless = 1
default = normal
+
class Overworld(Choice, LADXROption):
"""
[Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld.
@@ -251,9 +314,10 @@ class Overworld(Choice, LADXROption):
# option_shuffled = 3
default = option_normal
-#Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False,
+
+# Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False,
# description='All items will be more powerful, faster, harder, bigger stronger. You name it.'),
-#Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none',
+# Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none',
# description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.',
# aesthetic=True),
# Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast',
@@ -283,12 +347,27 @@ class Overworld(Choice, LADXROption):
# [Disable] no music in the whole game""",
# aesthetic=True),
+class BootsControls(Choice):
+ """
+ Adds additional button to activate Pegasus Boots (does nothing if you haven't picked up your boots!)
+ [Vanilla] Nothing changes, you have to equip the boots to use them
+ [Bracelet] Holding down the button for the bracelet also activates boots (somewhat like Link to the Past)
+ [Press A] Holding down A activates boots
+ [Press B] Holding down B activates boots
+ """
+ display_name = "Boots Controls"
+ option_vanilla = 0
+ option_bracelet = 1
+ option_press_a = 2
+ option_press_b = 3
+
+
class LinkPalette(Choice, LADXROption):
"""
Sets link's palette
A-D are color palettes usually used during the damage animation and can change based on where you are.
"""
- display_name = "Links Palette"
+ display_name = "Link's Palette"
ladxr_name = "linkspalette"
option_normal = -1
option_green = 0
@@ -304,6 +383,7 @@ class LinkPalette(Choice, LADXROption):
def to_ladxr_option(self, all_options):
return self.ladxr_name, str(self.value)
+
class TrendyGame(Choice):
"""
[Easy] All of the items hold still for you
@@ -313,6 +393,7 @@ class TrendyGame(Choice):
[Hardest] The items move diagonally
[Impossible] The items move impossibly fast, may scroll on and off the screen
"""
+ display_name = "Trendy Game"
option_easy = 0
option_normal = 1
option_hard = 2
@@ -321,6 +402,7 @@ class TrendyGame(Choice):
option_impossible = 5
default = option_normal
+
class GfxMod(FreeText, LADXROption):
"""
Sets the sprite for link, among other things
@@ -331,24 +413,24 @@ class GfxMod(FreeText, LADXROption):
normal = ''
default = 'Link'
+ __spriteDir: str = Utils.local_path(os.path.join('data', 'sprites', 'ladx'))
__spriteFiles: typing.DefaultDict[str, typing.List[str]] = defaultdict(list)
- __spriteDir: str = None
extensions = [".bin", ".bdiff", ".png", ".bmp"]
+
+ for file in os.listdir(__spriteDir):
+ name, extension = os.path.splitext(file)
+ if extension in extensions:
+ __spriteFiles[name].append(file)
+
def __init__(self, value: str):
super().__init__(value)
- if not GfxMod.__spriteDir:
- GfxMod.__spriteDir = Utils.local_path(os.path.join('data', 'sprites','ladx'))
- for file in os.listdir(GfxMod.__spriteDir):
- name, extension = os.path.splitext(file)
- if extension in self.extensions:
- GfxMod.__spriteFiles[name].append(file)
-
+
def verify(self, world, player_name: str, plando_options) -> None:
if self.value == "Link" or self.value in GfxMod.__spriteFiles:
return
- raise Exception(f"LADX Sprite '{self.value}' not found. Possible sprites are: {['Link'] + list(GfxMod.__spriteFiles.keys())}")
-
+ raise Exception(
+ f"LADX Sprite '{self.value}' not found. Possible sprites are: {['Link'] + list(GfxMod.__spriteFiles.keys())}")
def to_ladxr_option(self, all_options):
if self.value == -1 or self.value == "Link":
@@ -357,10 +439,12 @@ def to_ladxr_option(self, all_options):
assert self.value in GfxMod.__spriteFiles
if len(GfxMod.__spriteFiles[self.value]) > 1:
- logger.warning(f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}")
+ logger.warning(
+ f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}")
return self.ladxr_name, self.__spriteDir + "/" + GfxMod.__spriteFiles[self.value][0]
+
class Palette(Choice):
"""
Sets the palette for the game.
@@ -372,43 +456,126 @@ class Palette(Choice):
[Pink] Aesthetic
[Inverted] Inverted
"""
+ display_name = "Palette"
option_normal = 0
option_1bit = 1
option_2bit = 2
option_greyscale = 3
option_pink = 4
option_inverted = 5
-
-links_awakening_options: typing.Dict[str, typing.Type[Option]] = {
- 'logic': Logic,
+
+
+class Music(Choice, LADXROption):
+ """
+ [Vanilla] Regular Music
+ [Shuffled] Shuffled Music
+ [Off] No music
+ """
+ display_name = "Music"
+ ladxr_name = "music"
+ option_vanilla = 0
+ option_shuffled = 1
+ option_off = 2
+
+ def to_ladxr_option(self, all_options):
+ s = ""
+ if self.value == self.option_shuffled:
+ s = "random"
+ elif self.value == self.option_off:
+ s = "off"
+ return self.ladxr_name, s
+
+
+class WarpImprovements(DefaultOffToggle):
+ """
+ [On] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select.
+ [Off] No change
+ """
+ display_name = "Warp Improvements"
+
+
+class AdditionalWarpPoints(DefaultOffToggle):
+ """
+ [On] (requires warp improvements) Adds a warp point at Crazy Tracy's house (the Mambo teleport spot) and Eagle's Tower
+ [Off] No change
+ """
+ display_name = "Additional Warp Points"
+
+ladx_option_groups = [
+ OptionGroup("Goal Options", [
+ Goal,
+ InstrumentCount,
+ ]),
+ OptionGroup("Shuffles", [
+ ShuffleInstruments,
+ ShuffleNightmareKeys,
+ ShuffleSmallKeys,
+ ShuffleMaps,
+ ShuffleCompasses,
+ ShuffleStoneBeaks
+ ]),
+ OptionGroup("Warp Points", [
+ WarpImprovements,
+ AdditionalWarpPoints,
+ ]),
+ OptionGroup("Miscellaneous", [
+ TradeQuest,
+ Rooster,
+ TrendyGame,
+ NagMessages,
+ BootsControls
+ ]),
+ OptionGroup("Experimental", [
+ DungeonShuffle,
+ EntranceShuffle
+ ]),
+ OptionGroup("Visuals & Sound", [
+ LinkPalette,
+ Palette,
+ TextShuffle,
+ APTitleScreen,
+ GfxMod,
+ Music,
+ MusicChangeCondition
+ ])
+]
+
+@dataclass
+class LinksAwakeningOptions(PerGameCommonOptions):
+ logic: Logic
# 'heartpiece': DefaultOnToggle, # description='Includes heart pieces in the item pool'),
# 'seashells': DefaultOnToggle, # description='Randomizes the secret sea shells hiding in the ground/trees. (chest are always randomized)'),
# 'heartcontainers': DefaultOnToggle, # description='Includes boss heart container drops in the item pool'),
# 'instruments': DefaultOffToggle, # description='Instruments are placed on random locations, dungeon goal will just contain a random item.'),
- 'tradequest': TradeQuest, # description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'),
+ tradequest: TradeQuest # description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'),
# 'witch': DefaultOnToggle, # description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'),
- 'rooster': Rooster, # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'),
+ rooster: Rooster # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'),
# 'boomerang': Boomerang,
# 'randomstartlocation': DefaultOffToggle, # 'Randomize where your starting house is located'),
- 'experimental_dungeon_shuffle': DungeonShuffle, # 'Randomizes the dungeon that each dungeon entrance leads to'),
- 'experimental_entrance_shuffle': EntranceShuffle,
+ experimental_dungeon_shuffle: DungeonShuffle # 'Randomizes the dungeon that each dungeon entrance leads to'),
+ experimental_entrance_shuffle: EntranceShuffle
# 'bossshuffle': BossShuffle,
# 'minibossshuffle': BossShuffle,
- 'goal': Goal,
- 'instrument_count': InstrumentCount,
+ goal: Goal
+ instrument_count: InstrumentCount
# 'itempool': ItemPool,
# 'bowwow': Bowwow,
# 'overworld': Overworld,
- 'link_palette': LinkPalette,
- 'trendy_game': TrendyGame,
- 'gfxmod': GfxMod,
- 'palette': Palette,
- 'shuffle_nightmare_keys': ShuffleNightmareKeys,
- 'shuffle_small_keys': ShuffleSmallKeys,
- 'shuffle_maps': ShuffleMaps,
- 'shuffle_compasses': ShuffleCompasses,
- 'shuffle_stone_beaks': ShuffleStoneBeaks,
- 'music_change_condition': MusicChangeCondition,
- 'nag_messages': NagMessages,
- 'ap_title_screen': APTitleScreen,
-}
+ link_palette: LinkPalette
+ warp_improvements: WarpImprovements
+ additional_warp_points: AdditionalWarpPoints
+ trendy_game: TrendyGame
+ gfxmod: GfxMod
+ palette: Palette
+ text_shuffle: TextShuffle
+ shuffle_nightmare_keys: ShuffleNightmareKeys
+ shuffle_small_keys: ShuffleSmallKeys
+ shuffle_maps: ShuffleMaps
+ shuffle_compasses: ShuffleCompasses
+ shuffle_stone_beaks: ShuffleStoneBeaks
+ music: Music
+ shuffle_instruments: ShuffleInstruments
+ music_change_condition: MusicChangeCondition
+ nag_messages: NagMessages
+ ap_title_screen: APTitleScreen
+ boots_controls: BootsControls
diff --git a/worlds/ladx/Rom.py b/worlds/ladx/Rom.py
index eb573fe5b2cb..8ae1fac0fa31 100644
--- a/worlds/ladx/Rom.py
+++ b/worlds/ladx/Rom.py
@@ -1,4 +1,4 @@
-
+import settings
import worlds.Files
import hashlib
import Utils
@@ -32,7 +32,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
def get_base_rom_path(file_name: str = "") -> str:
- options = Utils.get_options()
+ options = settings.get_settings()
if not file_name:
file_name = options["ladx_options"]["rom_file"]
if not os.path.exists(file_name):
diff --git a/worlds/ladx/Tracker.py b/worlds/ladx/Tracker.py
index 29dce401b895..851fca164453 100644
--- a/worlds/ladx/Tracker.py
+++ b/worlds/ladx/Tracker.py
@@ -1,4 +1,4 @@
-from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
+from .LADXR.checkMetadata import checkMetadataTable
import json
import logging
import websockets
diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py
index 1d6c85dd6449..c958ef212fe4 100644
--- a/worlds/ladx/__init__.py
+++ b/worlds/ladx/__init__.py
@@ -1,33 +1,31 @@
import binascii
-import bsdiff4
+import dataclasses
import os
import pkgutil
-import settings
-import typing
import tempfile
+import typing
+import bsdiff4
-from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial
+import settings
+from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial, MultiWorld
from Fill import fill_restrictive
from worlds.AutoWorld import WebWorld, World
-
from .Common import *
-from .Items import (DungeonItemData, DungeonItemType, LinksAwakeningItem, TradeItemData,
- ladxr_item_to_la_item_name, links_awakening_items,
- links_awakening_items_by_name, ItemName)
+from .Items import (DungeonItemData, DungeonItemType, ItemName, LinksAwakeningItem, TradeItemData,
+ ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name)
from .LADXR import generator
from .LADXR.itempool import ItemPool as LADXRItemPool
-from .LADXR.logic import Logic as LAXDRLogic
+from .LADXR.locations.constants import CHEST_ITEMS
+from .LADXR.locations.instrument import Instrument
+from .LADXR.logic import Logic as LADXRLogic
from .LADXR.main import get_parser
from .LADXR.settings import Settings as LADXRSettings
from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup
-from .LADXR.locations.instrument import Instrument
-from .LADXR.locations.constants import CHEST_ITEMS
from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion,
create_regions_from_ladxr, get_locations_to_id)
-from .Options import links_awakening_options, DungeonItemShuffle
-
-from .Rom import LADXDeltaPatch
+from .Options import DungeonItemShuffle, ShuffleInstruments, LinksAwakeningOptions, ladx_option_groups
+from .Rom import LADXDeltaPatch, get_base_rom_path
DEVELOPER_MODE = False
@@ -67,7 +65,7 @@ class LinksAwakeningWebWorld(WebWorld):
["zig"]
)]
theme = "dirt"
-
+ option_groups = ladx_option_groups
class LinksAwakeningWorld(World):
"""
@@ -76,16 +74,12 @@ class LinksAwakeningWorld(World):
"""
game = LINKS_AWAKENING # name of the game/world
web = LinksAwakeningWebWorld()
-
- option_definitions = links_awakening_options # options the player can set
+
+ options_dataclass = LinksAwakeningOptions
+ options: LinksAwakeningOptions
settings: typing.ClassVar[LinksAwakeningSettings]
topology_present = True # show path to required location checks in spoiler
- # data_version is used to signal that items, locations or their names
- # changed. Set this to 0 during development so other games' clients do not
- # cache any texts, then increase by 1 for each release that makes changes.
- data_version = 1
-
# ID of first item and location, could be hard-coded but code may be easier
# to read with this as a propery.
base_id = BASE_ID
@@ -104,13 +98,20 @@ class LinksAwakeningWorld(World):
# Items can be grouped using their names to allow easy checking if any item
# from that group has been collected. Group names can also be used for !hint
- #item_name_groups = {
- # "weapons": {"sword", "lance"}
- #}
+ item_name_groups = {
+ "Instruments": {
+ "Full Moon Cello", "Conch Horn", "Sea Lily's Bell", "Surf Harp",
+ "Wind Marimba", "Coral Triangle", "Organ of Evening Calm", "Thunder Drum"
+ },
+ }
prefill_dungeon_items = None
- player_options = None
+ ladxr_settings: LADXRSettings
+ ladxr_logic: LADXRLogic
+ ladxr_itempool: LADXRItemPool
+
+ multi_key: bytearray
rupees = {
ItemName.RUPEES_20: 20,
@@ -121,17 +122,13 @@ class LinksAwakeningWorld(World):
}
def convert_ap_options_to_ladxr_logic(self):
- self.player_options = {
- option: getattr(self.multiworld, option)[self.player] for option in self.option_definitions
- }
+ self.ladxr_settings = LADXRSettings(dataclasses.asdict(self.options))
- self.laxdr_options = LADXRSettings(self.player_options)
-
- self.laxdr_options.validate()
+ self.ladxr_settings.validate()
world_setup = LADXRWorldSetup()
- world_setup.randomize(self.laxdr_options, self.multiworld.random)
- self.ladxr_logic = LAXDRLogic(configuration_options=self.laxdr_options, world_setup=world_setup)
- self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict()
+ world_setup.randomize(self.ladxr_settings, self.random)
+ self.ladxr_logic = LADXRLogic(configuration_options=self.ladxr_settings, world_setup=world_setup)
+ self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.ladxr_settings, self.random).toDict()
def create_regions(self) -> None:
# Initialize
@@ -157,7 +154,7 @@ def create_regions(self) -> None:
# Place RAFT, other access events
for region in regions:
for loc in region.locations:
- if loc.event:
+ if loc.address is None:
loc.place_locked_item(self.create_event(loc.ladxr_item.event))
# Connect Windfish -> Victory
@@ -187,19 +184,22 @@ def create_items(self) -> None:
self.pre_fill_items = []
# For any and different world, set item rule instead
- for option in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks"]:
- option = "shuffle_" + option
- option = self.player_options[option]
+ for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]:
+ option_name = "shuffle_" + dungeon_item_type
+ option: DungeonItemShuffle = getattr(self.options, option_name)
dungeon_item_types[option.ladxr_item] = option.value
+ # The color dungeon does not contain an instrument
+ num_items = 8 if dungeon_item_type == "instruments" else 9
+
if option.value == DungeonItemShuffle.option_own_world:
- self.multiworld.local_items[self.player].value |= {
- ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, 10)
+ self.options.local_items.value |= {
+ ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
}
elif option.value == DungeonItemShuffle.option_different_world:
- self.multiworld.non_local_items[self.player].value |= {
- ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, 10)
+ self.options.non_local_items.value |= {
+ ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
}
# option_original_dungeon = 0
# option_own_dungeons = 1
@@ -220,20 +220,21 @@ def create_items(self) -> None:
else:
item = self.create_item(item_name)
- if not self.multiworld.tradequest[self.player] and isinstance(item.item_data, TradeItemData):
+ if not self.options.tradequest and isinstance(item.item_data, TradeItemData):
location = self.multiworld.get_location(item.item_data.vanilla_location, self.player)
location.place_locked_item(item)
location.show_in_spoiler = False
continue
if isinstance(item.item_data, DungeonItemData):
- if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT:
+ item_type = item.item_data.ladxr_id[:-1]
+ shuffle_type = dungeon_item_types[item_type]
+
+ if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT and shuffle_type == ShuffleInstruments.option_vanilla:
# Find instrument, lock
# TODO: we should be able to pinpoint the region we want, save a lookup table please
found = False
- for r in self.multiworld.get_regions():
- if r.player != self.player:
- continue
+ for r in self.multiworld.get_regions(self.player):
if r.dungeon_index != item.item_data.dungeon_index:
continue
for loc in r.locations:
@@ -245,10 +246,8 @@ def create_items(self) -> None:
found = True
break
if found:
- break
+ break
else:
- item_type = item.item_data.ladxr_id[:-1]
- shuffle_type = dungeon_item_types[item_type]
if shuffle_type == DungeonItemShuffle.option_original_dungeon:
self.prefill_original_dungeon[item.item_data.dungeon_index - 1].append(item)
self.pre_fill_items.append(item)
@@ -269,10 +268,7 @@ def create_items(self) -> None:
event_location.place_locked_item(self.create_event("Can Play Trendy Game"))
self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []]
- for r in self.multiworld.get_regions():
- if r.player != self.player:
- continue
-
+ for r in self.multiworld.get_regions(self.player):
# Set aside dungeon locations
if r.dungeon_index:
self.dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations
@@ -284,6 +280,11 @@ def create_items(self) -> None:
# Properly fill locations within dungeon
location.dungeon = r.dungeon_index
+ # For now, special case first item
+ FORCE_START_ITEM = True
+ if FORCE_START_ITEM:
+ self.force_start_item()
+
def force_start_item(self):
start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player)
if not start_loc.item:
@@ -291,21 +292,16 @@ def force_start_item(self):
if item.player == self.player
and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS and not item.location]
if possible_start_items:
- index = self.multiworld.random.choice(possible_start_items)
+ index = self.random.choice(possible_start_items)
start_item = self.multiworld.itempool.pop(index)
start_loc.place_locked_item(start_item)
-
def get_pre_fill_items(self):
return self.pre_fill_items
def pre_fill(self) -> None:
allowed_locations_by_item = {}
- # For now, special case first item
- FORCE_START_ITEM = True
- if FORCE_START_ITEM:
- self.force_start_item()
# Set up filter rules
@@ -345,7 +341,7 @@ def pre_fill(self) -> None:
# Get the list of locations and shuffle
all_dungeon_locs_to_fill = sorted(all_dungeon_locs)
- self.multiworld.random.shuffle(all_dungeon_locs_to_fill)
+ self.random.shuffle(all_dungeon_locs_to_fill)
# Get the list of items and sort by priority
def priority(item):
@@ -442,6 +438,12 @@ def guess_icon_for_other_world(self, other):
return "TRADING_ITEM_LETTER"
+ @classmethod
+ def stage_assert_generate(cls, multiworld: MultiWorld):
+ rom_file = get_base_rom_path()
+ if not os.path.exists(rom_file):
+ raise FileNotFoundError(rom_file)
+
def generate_output(self, output_directory: str):
# copy items back to locations
for r in self.multiworld.get_regions(self.player):
@@ -468,34 +470,19 @@ def generate_output(self, output_directory: str):
loc.ladxr_item.location_owner = self.player
rom_name = Rom.get_base_rom_path()
- out_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.player_name[self.player]}.gbc"
+ out_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.player_name}.gbc"
out_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.gbc")
parser = get_parser()
args = parser.parse_args([rom_name, "-o", out_name, "--dump"])
- name_for_rom = self.multiworld.player_name[self.player]
-
- all_names = [self.multiworld.player_name[i + 1] for i in range(len(self.multiworld.player_name))]
-
- rom = generator.generateRom(
- args,
- self.laxdr_options,
- self.player_options,
- self.multi_key,
- self.multiworld.seed_name,
- self.ladxr_logic,
- rnd=self.multiworld.per_slot_randoms[self.player],
- player_name=name_for_rom,
- player_names=all_names,
- player_id = self.player,
- multiworld=self.multiworld)
+ rom = generator.generateRom(args, self)
with open(out_path, "wb") as handle:
rom.save(handle, name="LADXR")
# Write title screen after everything else is done - full gfxmods may stomp over the egg tiles
- if self.player_options["ap_title_screen"]:
+ if self.options.ap_title_screen:
with tempfile.NamedTemporaryFile(delete=False) as title_patch:
title_patch.write(pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4"))
@@ -503,29 +490,25 @@ def generate_output(self, output_directory: str):
os.unlink(title_patch.name)
patch = LADXDeltaPatch(os.path.splitext(out_path)[0]+LADXDeltaPatch.patch_file_ending, player=self.player,
- player_name=self.multiworld.player_name[self.player], patched_path=out_path)
+ player_name=self.player_name, patched_path=out_path)
patch.write()
if not DEVELOPER_MODE:
os.unlink(out_path)
def generate_multi_key(self):
- return bytearray(self.multiworld.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big')
+ return bytearray(self.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big')
def modify_multidata(self, multidata: dict):
- multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.multiworld.player_name[self.player]]
+ multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.player_name]
def collect(self, state, item: Item) -> bool:
change = super().collect(state, item)
- if change:
- rupees = self.rupees.get(item.name, 0)
- state.prog_items["RUPEES", item.player] += rupees
-
+ if change and item.name in self.rupees:
+ state.prog_items[self.player]["RUPEES"] += self.rupees[item.name]
return change
def remove(self, state, item: Item) -> bool:
change = super().remove(state, item)
- if change:
- rupees = self.rupees.get(item.name, 0)
- state.prog_items["RUPEES", item.player] -= rupees
-
+ if change and item.name in self.rupees:
+ state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name]
return change
diff --git a/worlds/ladx/docs/en_Links Awakening DX.md b/worlds/ladx/docs/en_Links Awakening DX.md
index bceda4bc89c1..91a34107c169 100644
--- a/worlds/ladx/docs/en_Links Awakening DX.md
+++ b/worlds/ladx/docs/en_Links Awakening DX.md
@@ -1,8 +1,8 @@
# Links Awakening DX
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -85,7 +85,7 @@ Title screen graphics by toomanyteeth✨ (https://instagram.com/toomanyyyteeth)
The walrus is moved a bit, so that you can access the desert without taking Marin on a date.
Logic
-Depending on your settings, you can only steal after you find the sword, always, or never.
+Depending on your options, you can only steal after you find the sword, always, or never.
Do not forget that there are two items in the rafting ride. You can access this with just Hookshot or Flippers.
Killing enemies with bombs is in normal logic. You can switch to casual logic if you do not want this.
D7 confuses some people, but by dropping down pits on the 2nd floor you can access almost all of this dungeon, even without feather and power bracelet.
diff --git a/worlds/ladx/docs/setup_en.md b/worlds/ladx/docs/setup_en.md
index 538d70d45e4a..d12f9b8b3b84 100644
--- a/worlds/ladx/docs/setup_en.md
+++ b/worlds/ladx/docs/setup_en.md
@@ -2,7 +2,7 @@
## Required Software
-- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `Links Awakening DX`
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- Software capable of loading and playing GBC ROM files
- [RetroArch](https://retroarch.com?page=platforms) 1.10.3 or newer.
- [BizHawk](https://tasvideos.org/BizHawk) 2.8 or newer.
@@ -10,11 +10,12 @@
## Installation Procedures
-1. Download and install LinksAwakeningClient from the link above, making sure to install the most recent version.
- **The installer file is located in the assets section at the bottom of the version information**.
- - During setup, you will be asked to locate your base ROM file. This is your Links Awakening DX ROM file.
+1. Download and install [Archipelago](). **The installer
+ file is located in the assets section at the bottom of the version information.**
+2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
+ This is your Links Awakening DX ROM file. This only needs to be done once..
-2. You should assign your emulator as your default program for launching ROM
+3. You should assign your emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
2. Right-click on a ROM file and select **Open with...**
@@ -34,17 +35,17 @@ options.
### Where do I get a config file?
-The [Player Settings](/games/Links%20Awakening%20DX/player-settings) page on the website allows you to configure
-your personal settings and export a config file from them.
+The [Player Options](/games/Links%20Awakening%20DX/player-options) page on the website allows you to configure
+your personal options 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
-1. Navigate to the [Player Settings](/games/Links%20Awakening%20DX/player-settings) page, configure your options,
+1. Navigate to the [Player Options](/games/Links%20Awakening%20DX/player-options) page, configure your options,
and click the "Generate Game" button.
2. You will be presented with a "Seed Info" page.
3. Click the "Create New Room" link.
diff --git a/worlds/ladx/test/testShop.py b/worlds/ladx/test/testShop.py
new file mode 100644
index 000000000000..91d504d521b4
--- /dev/null
+++ b/worlds/ladx/test/testShop.py
@@ -0,0 +1,38 @@
+from typing import Optional
+
+from Fill import distribute_planned
+from test.general import setup_solo_multiworld
+from worlds.AutoWorld import call_all
+from . import LADXTestBase
+from .. import LinksAwakeningWorld
+
+
+class PlandoTest(LADXTestBase):
+ options = {
+ "plando_items": [{
+ "items": {
+ "Progressive Sword": 2,
+ },
+ "locations": [
+ "Shop 200 Item (Mabe Village)",
+ "Shop 980 Item (Mabe Village)",
+ ],
+ }],
+ }
+
+ def world_setup(self, seed: Optional[int] = None) -> None:
+ self.multiworld = setup_solo_multiworld(
+ LinksAwakeningWorld,
+ ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic")
+ )
+ self.multiworld.plando_items[1] = self.options["plando_items"]
+ distribute_planned(self.multiworld)
+ call_all(self.multiworld, "pre_fill")
+
+ def test_planned(self):
+ """Tests plandoing swords in the shop."""
+ location_names = ["Shop 200 Item (Mabe Village)", "Shop 980 Item (Mabe Village)"]
+ locations = [self.multiworld.get_location(loc, 1) for loc in location_names]
+ for loc in locations:
+ self.assertEqual("Progressive Sword", loc.item.name)
+ self.assertFalse(loc.can_reach(self.multiworld.state))
diff --git a/worlds/landstalker/Hints.py b/worlds/landstalker/Hints.py
new file mode 100644
index 000000000000..5309e85032ea
--- /dev/null
+++ b/worlds/landstalker/Hints.py
@@ -0,0 +1,143 @@
+from typing import TYPE_CHECKING
+
+from BaseClasses import Location
+from .data.hint_source import HINT_SOURCES_JSON
+
+if TYPE_CHECKING:
+ from random import Random
+ from . import LandstalkerWorld
+
+
+def generate_blurry_location_hint(location: Location, random: "Random"):
+ cleaned_location_name = location.hint_text.lower().translate({ord(c): None for c in "(),:"})
+ cleaned_location_name.replace("-", " ")
+ cleaned_location_name.replace("/", " ")
+ cleaned_location_name.replace(".", " ")
+ location_name_words = [w for w in cleaned_location_name.split(" ") if len(w) > 3]
+
+ random_word_1 = "mysterious"
+ random_word_2 = "place"
+ if location_name_words:
+ random_word_1 = random.choice(location_name_words)
+ location_name_words.remove(random_word_1)
+ if location_name_words:
+ random_word_2 = random.choice(location_name_words)
+ return [random_word_1, random_word_2]
+
+
+def generate_lithograph_hint(world: "LandstalkerWorld"):
+ hint_text = "It's barely readable:\n"
+ jewel_items = world.jewel_items
+
+ for item in jewel_items:
+ if item.location is None:
+ continue
+
+ # Jewel hints are composed of 4 'words' shuffled randomly:
+ # - the name of the player whose world contains said jewel (if not ours)
+ # - the color of the jewel (if relevant)
+ # - two random words from the location name
+ words = generate_blurry_location_hint(item.location, world.random)
+ words[0] = words[0].upper()
+ words[1] = words[1].upper()
+ if len(jewel_items) < 6:
+ # Add jewel color if we are not using generic jewels because jewel count is 6 or more
+ words.append(item.name.split(" ")[0].upper())
+ if item.location.player != world.player:
+ # Add player name if it's not in our own world
+ player_name = world.multiworld.get_player_name(world.player)
+ words.append(player_name.upper())
+ world.random.shuffle(words)
+ hint_text += " ".join(words) + "\n"
+ return hint_text.rstrip("\n")
+
+
+def generate_random_hints(world: "LandstalkerWorld"):
+ hints = {}
+ hint_texts = []
+ random = world.random
+ multiworld = world.multiworld
+ this_player = world.player
+
+ # Exclude Life Stock from the hints as some of them are considered as progression for Fahl, but isn't really
+ # exciting when hinted
+ excluded_items = ["Life Stock", "EkeEke"]
+
+ progression_items = [item for item in multiworld.itempool if item.advancement and
+ item.name not in excluded_items and item.location is not None]
+
+ local_own_progression_items = [item for item in progression_items if item.player == this_player
+ and item.location.player == this_player]
+ remote_own_progression_items = [item for item in progression_items if item.player == this_player
+ and item.location.player != this_player]
+ local_unowned_progression_items = [item for item in progression_items if item.player != this_player
+ and item.location.player == this_player]
+ remote_unowned_progression_items = [item for item in progression_items if item.player != this_player
+ and item.location.player != this_player]
+
+ # Hint-type #1: Own progression item in own world
+ for item in local_own_progression_items:
+ region_hint = item.location.parent_region.hint_text
+ hint_texts.append(f"I can sense {item.name} {region_hint}.")
+
+ # Hint-type #2: Remote progression item in own world
+ for item in local_unowned_progression_items:
+ other_player = multiworld.get_player_name(item.player)
+ own_local_region = item.location.parent_region.hint_text
+ hint_texts.append(f"You might find something useful for {other_player} {own_local_region}. "
+ f"It is a {item.name}, to be precise.")
+
+ # Hint-type #3: Own progression item in remote location
+ for item in remote_own_progression_items:
+ other_player = multiworld.get_player_name(item.location.player)
+ if item.location.game == "Landstalker - The Treasures of King Nole":
+ region_hint_name = item.location.parent_region.hint_text
+ hint_texts.append(f"If you need {item.name}, tell {other_player} to look {region_hint_name}.")
+ else:
+ [word_1, word_2] = generate_blurry_location_hint(item.location, random)
+ if word_1 == "mysterious" and word_2 == "place":
+ continue
+ hint_texts.append(f"Looking for {item.name}? I read something about {other_player}'s world... "
+ f"Does \"{word_1} {word_2}\" remind you anything?")
+
+ # Hint-type #4: Remote progression item in remote location
+ for item in remote_unowned_progression_items:
+ owner_name = multiworld.get_player_name(item.player)
+ if item.location.player == item.player:
+ world_name = "their own world"
+ else:
+ world_name = f"{multiworld.get_player_name(item.location.player)}'s world"
+ [word_1, word_2] = generate_blurry_location_hint(item.location, random)
+ if word_1 == "mysterious" and word_2 == "place":
+ continue
+ hint_texts.append(f"I once found {owner_name}'s {item.name} in {world_name}. "
+ f"I remember \"{word_1} {word_2}\"... Does that make any sense?")
+
+ # Hint-type #5: Jokes
+ other_player_names = [multiworld.get_player_name(player) for player in multiworld.player_ids if
+ player != this_player]
+ if other_player_names:
+ random_player_name = random.choice(other_player_names)
+ hint_texts.append(f"{random_player_name}'s world is objectively better than yours.")
+
+ hint_texts.append(f"Have you found all of the {len(multiworld.itempool)} items in this universe?")
+
+ local_progression_item_count = len(local_own_progression_items) + len(local_unowned_progression_items)
+ remote_progression_item_count = len(remote_own_progression_items) + len(remote_unowned_progression_items)
+ percent = (local_progression_item_count / (local_progression_item_count + remote_progression_item_count)) * 100
+ hint_texts.append(f"Did you know that your world contains {int(percent)} percent of all progression items?")
+
+ # Shuffle hint texts and hint source names, and pair the two of those together
+ hint_texts = list(set(hint_texts))
+ random.shuffle(hint_texts)
+
+ hint_count = world.options.hint_count.value
+ del hint_texts[hint_count:]
+
+ hint_source_names = [source["description"] for source in HINT_SOURCES_JSON if
+ source["description"].startswith("Foxy")]
+ random.shuffle(hint_source_names)
+
+ for i in range(hint_count):
+ hints[hint_source_names[i]] = hint_texts[i]
+ return hints
diff --git a/worlds/landstalker/Items.py b/worlds/landstalker/Items.py
new file mode 100644
index 000000000000..ad7efa1cb27a
--- /dev/null
+++ b/worlds/landstalker/Items.py
@@ -0,0 +1,105 @@
+from typing import Dict, List, NamedTuple
+
+from BaseClasses import Item, ItemClassification
+
+BASE_ITEM_ID = 4000
+
+
+class LandstalkerItem(Item):
+ game: str = "Landstalker - The Treasures of King Nole"
+ price_in_shops: int
+
+
+class LandstalkerItemData(NamedTuple):
+ id: int
+ classification: ItemClassification
+ price_in_shops: int
+ quantity: int = 1
+
+
+item_table: Dict[str, LandstalkerItemData] = {
+ "EkeEke": LandstalkerItemData(0, ItemClassification.filler, 20, 0), # Variable amount
+ "Magic Sword": LandstalkerItemData(1, ItemClassification.useful, 300),
+ "Sword of Ice": LandstalkerItemData(2, ItemClassification.useful, 300),
+ "Thunder Sword": LandstalkerItemData(3, ItemClassification.useful, 500),
+ "Sword of Gaia": LandstalkerItemData(4, ItemClassification.progression, 300),
+ "Fireproof": LandstalkerItemData(5, ItemClassification.progression, 150),
+ "Iron Boots": LandstalkerItemData(6, ItemClassification.progression, 150),
+ "Healing Boots": LandstalkerItemData(7, ItemClassification.useful, 300),
+ "Snow Spikes": LandstalkerItemData(8, ItemClassification.progression, 400),
+ "Steel Breast": LandstalkerItemData(9, ItemClassification.useful, 200),
+ "Chrome Breast": LandstalkerItemData(10, ItemClassification.useful, 350),
+ "Shell Breast": LandstalkerItemData(11, ItemClassification.useful, 500),
+ "Hyper Breast": LandstalkerItemData(12, ItemClassification.useful, 700),
+ "Mars Stone": LandstalkerItemData(13, ItemClassification.useful, 150),
+ "Moon Stone": LandstalkerItemData(14, ItemClassification.useful, 150),
+ "Saturn Stone": LandstalkerItemData(15, ItemClassification.useful, 200),
+ "Venus Stone": LandstalkerItemData(16, ItemClassification.useful, 300),
+ # Awakening Book: 17
+ "Detox Grass": LandstalkerItemData(18, ItemClassification.filler, 25, 9),
+ "Statue of Gaia": LandstalkerItemData(19, ItemClassification.filler, 75, 12),
+ "Golden Statue": LandstalkerItemData(20, ItemClassification.filler, 150, 10),
+ "Mind Repair": LandstalkerItemData(21, ItemClassification.filler, 25, 7),
+ "Casino Ticket": LandstalkerItemData(22, ItemClassification.progression, 50),
+ "Axe Magic": LandstalkerItemData(23, ItemClassification.progression, 400),
+ "Blue Ribbon": LandstalkerItemData(24, ItemClassification.filler, 50),
+ "Buyer Card": LandstalkerItemData(25, ItemClassification.progression, 150),
+ "Lantern": LandstalkerItemData(26, ItemClassification.progression, 200),
+ "Garlic": LandstalkerItemData(27, ItemClassification.progression, 150, 2),
+ "Anti Paralyze": LandstalkerItemData(28, ItemClassification.filler, 20, 7),
+ "Statue of Jypta": LandstalkerItemData(29, ItemClassification.useful, 250),
+ "Sun Stone": LandstalkerItemData(30, ItemClassification.progression, 300),
+ "Armlet": LandstalkerItemData(31, ItemClassification.progression, 300),
+ "Einstein Whistle": LandstalkerItemData(32, ItemClassification.progression, 200),
+ "Blue Jewel": LandstalkerItemData(33, ItemClassification.progression, 500, 0), # Detox Book in base game
+ "Yellow Jewel": LandstalkerItemData(34, ItemClassification.progression, 500, 0), # AntiCurse Book in base game
+ # Record Book: 35
+ # Spell Book: 36
+ # Hotel Register: 37
+ # Island Map: 38
+ "Lithograph": LandstalkerItemData(39, ItemClassification.progression, 250),
+ "Red Jewel": LandstalkerItemData(40, ItemClassification.progression, 500, 0),
+ "Pawn Ticket": LandstalkerItemData(41, ItemClassification.useful, 200, 4),
+ "Purple Jewel": LandstalkerItemData(42, ItemClassification.progression, 500, 0),
+ "Gola's Eye": LandstalkerItemData(43, ItemClassification.progression, 400),
+ "Death Statue": LandstalkerItemData(44, ItemClassification.filler, 150),
+ "Dahl": LandstalkerItemData(45, ItemClassification.filler, 100, 18),
+ "Restoration": LandstalkerItemData(46, ItemClassification.filler, 40, 9),
+ "Logs": LandstalkerItemData(47, ItemClassification.progression, 100, 2),
+ "Oracle Stone": LandstalkerItemData(48, ItemClassification.progression, 250),
+ "Idol Stone": LandstalkerItemData(49, ItemClassification.progression, 200),
+ "Key": LandstalkerItemData(50, ItemClassification.progression, 150),
+ "Safety Pass": LandstalkerItemData(51, ItemClassification.progression, 250),
+ "Green Jewel": LandstalkerItemData(52, ItemClassification.progression, 500, 0), # No52 in base game
+ "Bell": LandstalkerItemData(53, ItemClassification.useful, 200),
+ "Short Cake": LandstalkerItemData(54, ItemClassification.useful, 250),
+ "Gola's Nail": LandstalkerItemData(55, ItemClassification.progression, 800),
+ "Gola's Horn": LandstalkerItemData(56, ItemClassification.progression, 800),
+ "Gola's Fang": LandstalkerItemData(57, ItemClassification.progression, 800),
+ # Broad Sword: 58
+ # Leather Breast: 59
+ # Leather Boots: 60
+ # No Ring: 61
+ "Life Stock": LandstalkerItemData(62, ItemClassification.filler, 250, 0), # Variable amount
+ "No Item": LandstalkerItemData(63, ItemClassification.filler, 0, 0),
+ "1 Gold": LandstalkerItemData(64, ItemClassification.filler, 1),
+ "20 Golds": LandstalkerItemData(65, ItemClassification.filler, 20, 15),
+ "50 Golds": LandstalkerItemData(66, ItemClassification.filler, 50, 7),
+ "100 Golds": LandstalkerItemData(67, ItemClassification.filler, 100, 5),
+ "200 Golds": LandstalkerItemData(68, ItemClassification.useful, 200, 2),
+
+ "Progressive Armor": LandstalkerItemData(69, ItemClassification.useful, 250, 0),
+ "Kazalt Jewel": LandstalkerItemData(70, ItemClassification.progression, 500, 0)
+}
+
+
+def get_weighted_filler_item_names():
+ weighted_item_names: List[str] = []
+ for name, data in item_table.items():
+ if data.classification == ItemClassification.filler:
+ weighted_item_names += [name for _ in range(data.quantity)]
+ return weighted_item_names
+
+
+def build_item_name_to_id_table():
+ return {name: data.id + BASE_ITEM_ID for name, data in item_table.items()}
diff --git a/worlds/landstalker/Locations.py b/worlds/landstalker/Locations.py
new file mode 100644
index 000000000000..b0148269eab3
--- /dev/null
+++ b/worlds/landstalker/Locations.py
@@ -0,0 +1,66 @@
+from typing import Dict, Optional
+
+from BaseClasses import Location, ItemClassification, Item
+from .Regions import LandstalkerRegion
+from .data.item_source import ITEM_SOURCES_JSON
+from .data.world_path import WORLD_PATHS_JSON
+
+BASE_LOCATION_ID = 4000
+BASE_GROUND_LOCATION_ID = BASE_LOCATION_ID + 256
+BASE_SHOP_LOCATION_ID = BASE_GROUND_LOCATION_ID + 30
+BASE_REWARD_LOCATION_ID = BASE_SHOP_LOCATION_ID + 50
+
+
+class LandstalkerLocation(Location):
+ game: str = "Landstalker - The Treasures of King Nole"
+ type_string: str
+ price: int = 0
+
+ def __init__(self, player: int, name: str, location_id: Optional[int], region: LandstalkerRegion, type_string: str):
+ super().__init__(player, name, location_id, region)
+ self.type_string = type_string
+
+
+def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion], name_to_id_table: Dict[str, int]):
+ # Create real locations from the data inside the corresponding JSON file
+ for data in ITEM_SOURCES_JSON:
+ region_id = data["nodeId"]
+ region = regions_table[region_id]
+ new_location = LandstalkerLocation(player, data["name"], name_to_id_table[data["name"]], region, data["type"])
+ region.locations.append(new_location)
+
+ # Create fake event locations that will be used to determine if some key regions has been visited
+ regions_with_entrance_checks = []
+ for data in WORLD_PATHS_JSON:
+ if "requiredNodes" in data:
+ regions_with_entrance_checks.extend([region_id for region_id in data["requiredNodes"]])
+ regions_with_entrance_checks = list(set(regions_with_entrance_checks))
+ for region_id in regions_with_entrance_checks:
+ region = regions_table[region_id]
+ location = LandstalkerLocation(player, 'event_visited_' + region_id, None, region, "event")
+ location.place_locked_item(Item("event_visited_" + region_id, ItemClassification.progression, None, player))
+ region.locations.append(location)
+
+ # Create a specific end location that will contain a fake win-condition item
+ end_location = LandstalkerLocation(player, "End", None, regions_table["end"], "reward")
+ regions_table["end"].locations.append(end_location)
+
+
+def build_location_name_to_id_table():
+ location_name_to_id_table = {}
+
+ for data in ITEM_SOURCES_JSON:
+ if data["type"] == "chest":
+ location_id = BASE_LOCATION_ID + int(data["chestId"])
+ elif data["type"] == "ground":
+ location_id = BASE_GROUND_LOCATION_ID + int(data["groundItemId"])
+ elif data["type"] == "shop":
+ location_id = BASE_SHOP_LOCATION_ID + int(data["shopItemId"])
+ else: # if data["type"] == "reward":
+ location_id = BASE_REWARD_LOCATION_ID + int(data["rewardId"])
+ location_name_to_id_table[data["name"]] = location_id
+
+ # Win condition location ID
+ location_name_to_id_table["Gola"] = BASE_REWARD_LOCATION_ID + 10
+
+ return location_name_to_id_table
diff --git a/worlds/landstalker/Options.py b/worlds/landstalker/Options.py
new file mode 100644
index 000000000000..65ffd2c1f31e
--- /dev/null
+++ b/worlds/landstalker/Options.py
@@ -0,0 +1,228 @@
+from dataclasses import dataclass
+
+from Options import Choice, DeathLink, DefaultOnToggle, PerGameCommonOptions, Range, Toggle
+
+
+class LandstalkerGoal(Choice):
+ """
+ The goal to accomplish in order to complete the seed.
+ - Beat Gola: beat the usual final boss (same as vanilla)
+ - Reach Kazalt: find the jewels and take the teleporter to Kazalt
+ - Beat Dark Nole: the door to King Nole's fight brings you into a final dungeon with an absurdly hard boss you have
+ to beat to win the game
+ """
+ display_name = "Goal"
+
+ option_beat_gola = 0
+ option_reach_kazalt = 1
+ option_beat_dark_nole = 2
+
+ default = 0
+
+
+class JewelCount(Range):
+ """
+ Determines the number of jewels to find to be able to reach Kazalt.
+ """
+ display_name = "Jewel Count"
+ range_start = 0
+ range_end = 9
+ default = 5
+
+
+class ProgressiveArmors(DefaultOnToggle):
+ """
+ When obtaining an armor, you get the next armor tier instead of getting the specific armor tier that was
+ placed here by randomization. Enabling this provides a smoother progression.
+ """
+ display_name = "Progressive Armors"
+
+
+class UseRecordBook(DefaultOnToggle):
+ """
+ Gives a Record Book item in starting inventory, allowing to save the game anywhere.
+ This makes the game significantly less frustrating and enables interesting save-scumming strategies in some places.
+ """
+ display_name = "Use Record Book"
+
+
+class UseSpellBook(DefaultOnToggle):
+ """
+ Gives a Spell Book item in starting inventory, allowing to warp back to the starting location at any time.
+ This prevents any kind of softlock and makes the world easier to explore.
+ """
+ display_name = "Use Spell Book"
+
+
+class EnsureEkeEkeInShops(DefaultOnToggle):
+ """
+ Ensures an EkeEke will always be for sale in one shop per region in the game.
+ Disabling this can lead to frustrating situations where you cannot refill your health items and might get locked.
+ """
+ display_name = "Ensure EkeEke in Shops"
+
+
+class RemoveGumiBoulder(Toggle):
+ """
+ Removes the boulder between Gumi and Ryuma, which is usually a one-way path.
+ This makes the vanilla early game (Massan, Gumi...) more easily accessible when starting outside it.
+ """
+ display_name = "Remove Boulder After Gumi"
+
+
+class EnemyJumpingInLogic(Toggle):
+ """
+ Adds jumping on enemies' heads as a logical rule.
+ This gives access to Mountainous Area from Lake Shrine sector and to the cliff chest behind a magic tree near Mir Tower.
+ These tricks not being easy, you should leave this disabled until practiced.
+ """
+ display_name = "Enemy Jumping in Logic"
+
+
+class TreeCuttingGlitchInLogic(Toggle):
+ """
+ Adds tree-cutting glitch as a logical rule, enabling access to both chests behind magic trees in Mir Tower Sector
+ without having Axe Magic.
+ """
+ display_name = "Tree-cutting Glitch in Logic"
+
+
+class DamageBoostingInLogic(Toggle):
+ """
+ Adds damage boosting as a logical rule, removing any requirements involving Iron Boots or Fireproof Boots.
+ Who doesn't like walking on spikes and lava?
+ """
+ display_name = "Damage Boosting in Logic"
+
+
+class WhistleUsageBehindTrees(DefaultOnToggle):
+ """
+ In Greenmaze, Einstein Whistle can only be used to call Cutter from the intended side by default.
+ Enabling this allows using Einstein Whistle from both sides of the magic trees.
+ This is only useful in seeds starting in the "waterfall" spawn region or where teleportation trees are made open from the start.
+ """
+ display_name = "Allow Using Einstein Whistle Behind Trees"
+
+
+class SpawnRegion(Choice):
+ """
+ List of spawn locations that can be picked by the randomizer.
+ It is advised to keep Massan as your spawn location for your first few seeds.
+ Picking a late-game location can make the seed significantly harder, both for logic and combat.
+ """
+ display_name = "Starting Region"
+
+ option_massan = 0
+ option_gumi = 1
+ option_kado = 2
+ option_waterfall = 3
+ option_ryuma = 4
+ option_mercator = 5
+ option_verla = 6
+ option_greenmaze = 7
+ option_destel = 8
+
+ default = 0
+
+
+class TeleportTreeRequirements(Choice):
+ """
+ Determines the requirements to be able to use a teleport tree pair.
+ - None: All teleport trees are available right from the start
+ - Clear Tibor: Tibor needs to be cleared before unlocking any tree
+ - Visit Trees: Both trees from a tree pair need to be visited to teleport between them
+ Vanilla behavior is "Clear Tibor And Visit Trees"
+ """
+ display_name = "Teleportation Trees Requirements"
+
+ option_none = 0
+ option_clear_tibor = 1
+ option_visit_trees = 2
+ option_clear_tibor_and_visit_trees = 3
+
+ default = 3
+
+
+class ShuffleTrees(Toggle):
+ """
+ If enabled, all teleportation trees will be shuffled into new pairs.
+ """
+ display_name = "Shuffle Teleportation Trees"
+
+
+class ReviveUsingEkeeke(DefaultOnToggle):
+ """
+ In the vanilla game, when you die, you are automatically revived by Friday using an EkeEke.
+ This setting allows disabling this feature, making the game extremely harder.
+ USE WITH CAUTION!
+ """
+ display_name = "Revive Using EkeEke"
+
+
+class ShopPricesFactor(Range):
+ """
+ Applies a percentage factor on all prices in shops. Having higher prices can lead to a bit of gold farming, which
+ can make seeds longer but also sometimes more frustrating.
+ """
+ display_name = "Shop Prices Factor (%)"
+ range_start = 50
+ range_end = 200
+ default = 100
+
+
+class CombatDifficulty(Choice):
+ """
+ Determines the overall combat difficulty in the game by modifying both monsters HP & damage.
+ - Peaceful: 50% HP & damage
+ - Easy: 75% HP & damage
+ - Normal: 100% HP & damage
+ - Hard: 140% HP & damage
+ - Insane: 200% HP & damage
+ """
+ display_name = "Combat Difficulty"
+
+ option_peaceful = 0
+ option_easy = 1
+ option_normal = 2
+ option_hard = 3
+ option_insane = 4
+
+ default = 2
+
+
+class HintCount(Range):
+ """
+ Determines the number of Foxy NPCs that will be scattered across the world, giving various types of hints
+ """
+ display_name = "Hint Count"
+ range_start = 0
+ range_end = 25
+ default = 12
+
+
+@dataclass
+class LandstalkerOptions(PerGameCommonOptions):
+ goal: LandstalkerGoal
+ spawn_region: SpawnRegion
+ jewel_count: JewelCount
+ progressive_armors: ProgressiveArmors
+ use_record_book: UseRecordBook
+ use_spell_book: UseSpellBook
+
+ shop_prices_factor: ShopPricesFactor
+ combat_difficulty: CombatDifficulty
+
+ teleport_tree_requirements: TeleportTreeRequirements
+ shuffle_trees: ShuffleTrees
+
+ ensure_ekeeke_in_shops: EnsureEkeEkeInShops
+ remove_gumi_boulder: RemoveGumiBoulder
+ allow_whistle_usage_behind_trees: WhistleUsageBehindTrees
+ handle_damage_boosting_in_logic: DamageBoostingInLogic
+ handle_enemy_jumping_in_logic: EnemyJumpingInLogic
+ handle_tree_cutting_glitch_in_logic: TreeCuttingGlitchInLogic
+
+ hint_count: HintCount
+
+ revive_using_ekeeke: ReviveUsingEkeeke
+ death_link: DeathLink
diff --git a/worlds/landstalker/Regions.py b/worlds/landstalker/Regions.py
new file mode 100644
index 000000000000..27e5e2e993d0
--- /dev/null
+++ b/worlds/landstalker/Regions.py
@@ -0,0 +1,118 @@
+from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING
+
+from BaseClasses import MultiWorld, Region
+from .data.world_node import WORLD_NODES_JSON
+from .data.world_path import WORLD_PATHS_JSON
+from .data.world_region import WORLD_REGIONS_JSON
+from .data.world_teleport_tree import WORLD_TELEPORT_TREES_JSON
+
+if TYPE_CHECKING:
+ from . import LandstalkerWorld
+
+
+class LandstalkerRegion(Region):
+ code: str
+
+ def __init__(self, code: str, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
+ super().__init__(name, player, multiworld, hint)
+ self.code = code
+
+
+class LandstalkerRegionData(NamedTuple):
+ locations: Optional[List[str]]
+ region_exits: Optional[List[str]]
+
+
+def create_regions(world: "LandstalkerWorld"):
+ regions_table: Dict[str, LandstalkerRegion] = {}
+ multiworld = world.multiworld
+ player = world.player
+
+ # Create the hardcoded starting "Menu" region
+ menu_region = LandstalkerRegion("menu", "Menu", player, multiworld)
+ regions_table["menu"] = menu_region
+ multiworld.regions.append(menu_region)
+
+ # Create regions from world_nodes
+ for code, region_data in WORLD_NODES_JSON.items():
+ random_hint_name = None
+ if "hints" in region_data:
+ random_hint_name = world.random.choice(region_data["hints"])
+ region = LandstalkerRegion(code, region_data["name"], player, multiworld, random_hint_name)
+ regions_table[code] = region
+ multiworld.regions.append(region)
+
+ # Create exits/entrances from world_paths
+ for data in WORLD_PATHS_JSON:
+ two_way = data["twoWay"] if "twoWay" in data else False
+ create_entrance(data["fromId"], data["toId"], two_way, regions_table)
+
+ # Create a path between the fake Menu location and the starting location
+ starting_region = get_starting_region(world, regions_table)
+ menu_region.connect(starting_region, f"menu -> {starting_region.code}")
+
+ add_specific_paths(world, regions_table)
+
+ return regions_table
+
+
+def add_specific_paths(world: "LandstalkerWorld", regions_table: Dict[str, LandstalkerRegion]):
+ # If Gumi boulder is removed, add a path from "route_gumi_ryuma" to "gumi"
+ if world.options.remove_gumi_boulder == 1:
+ create_entrance("route_gumi_ryuma", "gumi", False, regions_table)
+
+ # If enemy jumping is in logic, Mountainous Area can be reached from route to Lake Shrine by doing a "ghost jump"
+ # at crossroads map
+ if world.options.handle_enemy_jumping_in_logic == 1:
+ create_entrance("route_lake_shrine", "route_lake_shrine_cliff", False, regions_table)
+
+ # If using Einstein Whistle behind trees is allowed, add a new logic path there to reflect that change
+ if world.options.allow_whistle_usage_behind_trees == 1:
+ create_entrance("greenmaze_post_whistle", "greenmaze_pre_whistle", False, regions_table)
+
+
+def create_entrance(from_id: str, to_id: str, two_way: bool, regions_table: Dict[str, LandstalkerRegion]):
+ created_entrances = []
+
+ name = from_id + " -> " + to_id
+ from_region = regions_table[from_id]
+ to_region = regions_table[to_id]
+
+ created_entrances.append(from_region.connect(to_region, name))
+
+ if two_way:
+ reverse_name = to_id + " -> " + from_id
+ created_entrances.append(to_region.connect(from_region, reverse_name))
+
+ return created_entrances
+
+
+def get_starting_region(world: "LandstalkerWorld", regions_table: Dict[str, LandstalkerRegion]):
+ # Most spawn locations have the same name as the region they are bound to, but a few vary.
+ spawn_id = world.options.spawn_region.current_key
+ if spawn_id == "waterfall":
+ return regions_table["greenmaze_post_whistle"]
+ elif spawn_id == "kado":
+ return regions_table["route_gumi_ryuma"]
+ elif spawn_id == "greenmaze":
+ return regions_table["greenmaze_pre_whistle"]
+ return regions_table[spawn_id]
+
+
+def get_darkenable_regions():
+ return {data["name"]: data["nodeIds"] for data in WORLD_REGIONS_JSON if "darkMapIds" in data}
+
+
+def load_teleport_trees():
+ pairs = []
+ for pair in WORLD_TELEPORT_TREES_JSON:
+ first_tree = {
+ 'name': pair[0]["name"],
+ 'region': pair[0]["nodeId"]
+ }
+ second_tree = {
+ 'name': pair[1]["name"],
+ 'region': pair[1]["nodeId"]
+ }
+ pairs.append([first_tree, second_tree])
+ return pairs
diff --git a/worlds/landstalker/Rules.py b/worlds/landstalker/Rules.py
new file mode 100644
index 000000000000..94171944d7b2
--- /dev/null
+++ b/worlds/landstalker/Rules.py
@@ -0,0 +1,134 @@
+from typing import List, TYPE_CHECKING
+
+from BaseClasses import CollectionState
+from .data.world_path import WORLD_PATHS_JSON
+from .Locations import LandstalkerLocation
+from .Regions import LandstalkerRegion
+
+if TYPE_CHECKING:
+ from . import LandstalkerWorld
+
+
+def _landstalker_has_visited_regions(state: CollectionState, player: int, regions):
+ return all(state.has("event_visited_" + region.code, player) for region in regions)
+
+
+def _landstalker_has_health(state: CollectionState, player: int, health):
+ return state.has("Life Stock", player, health)
+
+
+# multiworld: MultiWorld, player: int, regions_table: Dict[str, Region], dark_region_ids: List[str]
+def create_rules(world: "LandstalkerWorld"):
+ # Item & exploration requirements to take paths
+ add_path_requirements(world)
+ add_specific_path_requirements(world)
+
+ # Location rules to forbid some item types depending on location types
+ add_location_rules(world)
+
+ # Win condition
+ world.multiworld.completion_condition[world.player] = lambda state: state.has("King Nole's Treasure", world.player)
+
+
+# multiworld: MultiWorld, player: int, regions_table: Dict[str, Region],
+# dark_region_ids: List[str]
+def add_path_requirements(world: "LandstalkerWorld"):
+ for data in WORLD_PATHS_JSON:
+ name = data["fromId"] + " -> " + data["toId"]
+
+ # Determine required items to reach this region
+ required_items = data["requiredItems"] if "requiredItems" in data else []
+ if "itemsPlacedWhenCrossing" in data:
+ required_items += data["itemsPlacedWhenCrossing"]
+
+ if data["toId"] in world.dark_region_ids:
+ # Make Lantern required to reach the randomly selected dark regions
+ required_items.append("Lantern")
+ if world.options.handle_damage_boosting_in_logic:
+ # If damage boosting is handled in logic, remove all iron boots & fireproof requirements
+ required_items = [item for item in required_items if item != "Iron Boots" and item != "Fireproof"]
+
+ # Determine required other visited regions to reach this region
+ required_region_ids = data["requiredNodes"] if "requiredNodes" in data else []
+ required_regions = [world.regions_table[region_id] for region_id in required_region_ids]
+
+ if not (required_items or required_regions):
+ continue
+
+ # Create the rule lambda using those requirements
+ access_rule = make_path_requirement_lambda(world.player, required_items, required_regions)
+ world.multiworld.get_entrance(name, world.player).access_rule = access_rule
+
+ # If two-way, also apply the rule to the opposite path
+ if "twoWay" in data and data["twoWay"] is True:
+ reverse_name = data["toId"] + " -> " + data["fromId"]
+ world.multiworld.get_entrance(reverse_name, world.player).access_rule = access_rule
+
+
+def add_specific_path_requirements(world: "LandstalkerWorld"):
+ multiworld = world.multiworld
+ player = world.player
+
+ # Make the jewels required to reach Kazalt
+ jewel_count = world.options.jewel_count.value
+ path_to_kazalt = multiworld.get_entrance("king_nole_cave -> kazalt", player)
+ if jewel_count < 6:
+ # 5- jewels => the player needs to find as many uniquely named jewel items
+ required_jewels = ["Red Jewel", "Purple Jewel", "Green Jewel", "Blue Jewel", "Yellow Jewel"]
+ del required_jewels[jewel_count:]
+ path_to_kazalt.access_rule = make_path_requirement_lambda(player, required_jewels, [])
+ else:
+ # 6+ jewels => the player needs to find as many "Kazalt Jewel" items
+ path_to_kazalt.access_rule = lambda state: state.has("Kazalt Jewel", player, jewel_count)
+
+ # If enemy jumping is enabled, Mir Tower sector first tree can be bypassed to reach the elevated ledge
+ if world.options.handle_enemy_jumping_in_logic == 1:
+ remove_requirements_for(world, "mir_tower_sector -> mir_tower_sector_tree_ledge")
+
+ # Both trees in Mir Tower sector can be abused using tree cutting glitch
+ if world.options.handle_tree_cutting_glitch_in_logic == 1:
+ remove_requirements_for(world, "mir_tower_sector -> mir_tower_sector_tree_ledge")
+ remove_requirements_for(world, "mir_tower_sector -> mir_tower_sector_tree_coast")
+
+ # If Whistle can be used from behind the trees, it adds a new path that requires the whistle as well
+ if world.options.allow_whistle_usage_behind_trees == 1:
+ entrance = multiworld.get_entrance("greenmaze_post_whistle -> greenmaze_pre_whistle", player)
+ entrance.access_rule = make_path_requirement_lambda(player, ["Einstein Whistle"], [])
+
+
+def make_path_requirement_lambda(player: int, required_items: List[str], required_regions: List[LandstalkerRegion]):
+ """
+ Lambdas are created in a for loop, so values need to be captured
+ """
+ return lambda state: \
+ state.has_all(set(required_items), player) and _landstalker_has_visited_regions(state, player, required_regions)
+
+
+def make_shop_location_requirement_lambda(player: int, location: LandstalkerLocation):
+ """
+ Lambdas are created in a for loop, so values need to be captured
+ """
+ # Prevent local golds in shops, as well as duplicates
+ other_locations_in_shop = [loc for loc in location.parent_region.locations if loc != location]
+ return lambda item: \
+ item.player != player \
+ or (" Gold" not in item.name
+ and item.name not in [loc.item.name for loc in other_locations_in_shop if loc.item is not None])
+
+
+def remove_requirements_for(world: "LandstalkerWorld", entrance_name: str):
+ entrance = world.multiworld.get_entrance(entrance_name, world.player)
+ entrance.access_rule = lambda state: True
+
+
+def add_location_rules(world: "LandstalkerWorld"):
+ location: LandstalkerLocation
+ for location in world.multiworld.get_locations(world.player):
+ if location.type_string == "ground":
+ location.item_rule = lambda item: not (item.player == world.player and " Gold" in item.name)
+ elif location.type_string == "shop":
+ location.item_rule = make_shop_location_requirement_lambda(world.player, location)
+
+ # Add a special rule for Fahl
+ fahl_location = world.multiworld.get_location("Mercator: Fahl's dojo challenge reward", world.player)
+ fahl_location.access_rule = lambda state: _landstalker_has_health(state, world.player, 15)
diff --git a/worlds/landstalker/__init__.py b/worlds/landstalker/__init__.py
new file mode 100644
index 000000000000..2b3dc41239c3
--- /dev/null
+++ b/worlds/landstalker/__init__.py
@@ -0,0 +1,265 @@
+from typing import ClassVar, Set
+
+from BaseClasses import LocationProgressType, Tutorial
+from worlds.AutoWorld import WebWorld, World
+from .Hints import *
+from .Items import *
+from .Locations import *
+from .Options import JewelCount, LandstalkerGoal, LandstalkerOptions, ProgressiveArmors, TeleportTreeRequirements
+from .Regions import *
+from .Rules import *
+
+
+class LandstalkerWeb(WebWorld):
+ theme = "grass"
+ tutorials = [Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to setting up the Landstalker Randomizer software on your computer.",
+ "English",
+ "landstalker_setup_en.md",
+ "landstalker_setup/en",
+ ["Dinopony"]
+ )]
+
+
+class LandstalkerWorld(World):
+ """
+ Landstalker: The Treasures of King Nole is a classic Action-RPG with an isometric view (also known as "2.5D").
+ You play Nigel, a treasure hunter exploring the island of Mercator trying to find the legendary treasure.
+ Roam freely on the island, get stronger to beat dungeons and gather the required key items in order to reach the
+ hidden palace and claim the treasure.
+ """
+ game = "Landstalker - The Treasures of King Nole"
+ options_dataclass = LandstalkerOptions
+ options: LandstalkerOptions
+ required_client_version = (0, 4, 4)
+ web = LandstalkerWeb()
+
+ item_name_to_id = build_item_name_to_id_table()
+ location_name_to_id = build_location_name_to_id_table()
+
+ cached_spheres: ClassVar[List[Set[Location]]]
+
+ def __init__(self, multiworld, player):
+ super().__init__(multiworld, player)
+ self.regions_table: Dict[str, LandstalkerRegion] = {}
+ self.dark_dungeon_id = "None"
+ self.dark_region_ids = []
+ self.teleport_tree_pairs = []
+ self.jewel_items = []
+
+ def fill_slot_data(self) -> dict:
+ # Generate hints.
+ self.adjust_shop_prices()
+ hints = Hints.generate_random_hints(self)
+ hints["Lithograph"] = Hints.generate_lithograph_hint(self)
+ hints["Oracle Stone"] = f"It shows {self.dark_dungeon_id}\nenshrouded in darkness."
+
+ # Put options, locations' contents and some additional data inside slot data
+ options = [
+ "goal", "jewel_count", "progressive_armors", "use_record_book", "use_spell_book", "shop_prices_factor",
+ "combat_difficulty", "teleport_tree_requirements", "shuffle_trees", "ensure_ekeeke_in_shops",
+ "remove_gumi_boulder", "allow_whistle_usage_behind_trees", "handle_damage_boosting_in_logic",
+ "handle_enemy_jumping_in_logic", "handle_tree_cutting_glitch_in_logic", "hint_count", "death_link",
+ "revive_using_ekeeke",
+ ]
+
+ slot_data = self.options.as_dict(*options)
+ slot_data["spawn_region"] = self.options.spawn_region.current_key
+ slot_data["seed"] = self.random.randint(0, 2 ** 32 - 1)
+ slot_data["dark_region"] = self.dark_dungeon_id
+ slot_data["hints"] = hints
+ slot_data["teleport_tree_pairs"] = [[pair[0]["name"], pair[1]["name"]] for pair in self.teleport_tree_pairs]
+
+ # Type hinting for location.
+ location: LandstalkerLocation
+ slot_data["location_prices"] = {
+ location.name: location.price for location in self.multiworld.get_locations(self.player) if location.price}
+
+ return slot_data
+
+ def generate_early(self):
+ # Randomly pick a set of dark regions where Lantern is needed
+ darkenable_regions = get_darkenable_regions()
+ self.dark_dungeon_id = self.random.choice(list(darkenable_regions))
+ self.dark_region_ids = darkenable_regions[self.dark_dungeon_id]
+
+ def create_regions(self):
+ self.regions_table = Regions.create_regions(self)
+ Locations.create_locations(self.player, self.regions_table, self.location_name_to_id)
+ self.create_teleportation_trees()
+
+ def create_item(self, name: str, classification_override: Optional[ItemClassification] = None) -> LandstalkerItem:
+ data = item_table[name]
+ classification = classification_override or data.classification
+ item = LandstalkerItem(name, classification, BASE_ITEM_ID + data.id, self.player)
+ item.price_in_shops = data.price_in_shops
+ return item
+
+ def create_event(self, name: str) -> LandstalkerItem:
+ return LandstalkerItem(name, ItemClassification.progression, None, self.player)
+
+ def get_filler_item_name(self) -> str:
+ return "EkeEke"
+
+ def create_items(self):
+ item_pool: List[LandstalkerItem] = []
+ for name, data in item_table.items():
+ # If item is an armor and progressive armors are enabled, transform it into a progressive armor item
+ if self.options.progressive_armors and "Breast" in name:
+ name = "Progressive Armor"
+ item_pool += [self.create_item(name) for _ in range(data.quantity)]
+
+ # If the appropriate setting is on, place one EkeEke in one shop in every town in the game
+ if self.options.ensure_ekeeke_in_shops:
+ shops_to_fill = [
+ "Massan: Shop item #1",
+ "Gumi: Inn item #1",
+ "Ryuma: Inn item",
+ "Mercator: Shop item #1",
+ "Verla: Shop item #1",
+ "Destel: Inn item",
+ "Route to Lake Shrine: Greedly's shop item #1",
+ "Kazalt: Shop item #1"
+ ]
+ for location_name in shops_to_fill:
+ self.multiworld.get_location(location_name, self.player).place_locked_item(self.create_item("EkeEke"))
+
+ # Add a fixed amount of progression Life Stock for a specific requirement (Fahl)
+ fahl_lifestock_req = 15
+ item_pool += [self.create_item("Life Stock", ItemClassification.progression) for _ in range(fahl_lifestock_req)]
+ # Add a unique progression EkeEke for a specific requirement (Cutter)
+ item_pool.append(self.create_item("EkeEke", ItemClassification.progression))
+
+ # Add a variable amount of "useful" Life Stock to the pool, depending on the amount of starting Life Stock
+ # (i.e. on the starting location)
+ starting_lifestocks = self.get_starting_health() - 4
+ lifestock_count = 80 - starting_lifestocks - fahl_lifestock_req
+ item_pool += [self.create_item("Life Stock") for _ in range(lifestock_count)]
+
+ # Add jewels to the item pool depending on the number of jewels set in generation settings
+ self.jewel_items = [self.create_item(name) for name in self.get_jewel_names(self.options.jewel_count)]
+ item_pool += self.jewel_items
+
+ # Add a pre-placed fake win condition item
+ self.multiworld.get_location("End", self.player).place_locked_item(self.create_event("King Nole's Treasure"))
+
+ # Fill the rest of the item pool with EkeEke
+ remaining_items = len(self.multiworld.get_unfilled_locations(self.player)) - len(item_pool)
+ item_pool += [self.create_item(self.get_filler_item_name()) for _ in range(remaining_items)]
+
+ self.multiworld.itempool += item_pool
+
+ def create_teleportation_trees(self):
+ self.teleport_tree_pairs = load_teleport_trees()
+
+ def pairwise(iterable):
+ """Yields pairs of elements from the given list -> [0,1], [2,3]..."""
+ a = iter(iterable)
+ return zip(a, a)
+
+ # Shuffle teleport tree pairs if the matching setting is on
+ if self.options.shuffle_trees:
+ all_trees = [item for pair in self.teleport_tree_pairs for item in pair]
+ self.random.shuffle(all_trees)
+ self.teleport_tree_pairs = [[x, y] for x, y in pairwise(all_trees)]
+
+ # If a specific setting is set, teleport trees are potentially active without visiting both sides.
+ # This means we need to add those as explorable paths for the generation algorithm.
+ teleport_trees_mode = self.options.teleport_tree_requirements.value
+ created_entrances = []
+ if teleport_trees_mode in [TeleportTreeRequirements.option_none, TeleportTreeRequirements.option_clear_tibor]:
+ for pair in self.teleport_tree_pairs:
+ entrances = create_entrance(pair[0]["region"], pair[1]["region"], True, self.regions_table)
+ created_entrances += entrances
+
+ # Teleport trees are open but require access to Tibor to work
+ if teleport_trees_mode == TeleportTreeRequirements.option_clear_tibor:
+ for entrance in created_entrances:
+ entrance.access_rule = make_path_requirement_lambda(self.player, [], [self.regions_table["tibor"]])
+
+ def set_rules(self):
+ Rules.create_rules(self)
+
+ # In "Reach Kazalt" goal, player doesn't have access to Kazalt, King Nole's Labyrinth & King Nole's Palace.
+ # As a consequence, all locations inside those regions must be excluded, and the teleporter from
+ # King Nole's Cave to Kazalt must go to the end region instead.
+ if self.options.goal == LandstalkerGoal.option_reach_kazalt:
+ kazalt_tp = self.multiworld.get_entrance("king_nole_cave -> kazalt", self.player)
+ kazalt_tp.connected_region = self.regions_table["end"]
+
+ excluded_regions = [
+ "kazalt",
+ "king_nole_labyrinth_pre_door",
+ "king_nole_labyrinth_post_door",
+ "king_nole_labyrinth_exterior",
+ "king_nole_labyrinth_fall_from_exterior",
+ "king_nole_labyrinth_raft_entrance",
+ "king_nole_labyrinth_raft",
+ "king_nole_labyrinth_sacred_tree",
+ "king_nole_labyrinth_path_to_palace",
+ "king_nole_palace"
+ ]
+
+ for location in self.multiworld.get_locations(self.player):
+ if location.parent_region.name in excluded_regions:
+ location.progress_type = LocationProgressType.EXCLUDED
+ # We need to make that event non-progression since it would crash generation in reach_kazalt goal
+ if location.item is not None and location.item.name == "event_visited_king_nole_labyrinth_raft_entrance":
+ location.item.classification = ItemClassification.filler
+
+ def get_starting_health(self):
+ spawn_id = self.options.spawn_region.current_key
+ if spawn_id == "destel":
+ return 20
+ elif spawn_id == "verla":
+ return 16
+ elif spawn_id in ["waterfall", "mercator", "greenmaze"]:
+ return 10
+ else:
+ return 4
+
+ @classmethod
+ def stage_post_fill(cls, multiworld):
+ # Cache spheres for hint calculation after fill completes.
+ cls.cached_spheres = list(multiworld.get_spheres())
+
+ @classmethod
+ def stage_modify_multidata(cls, *_):
+ # Clean up all references in cached spheres after generation completes.
+ del cls.cached_spheres
+
+ def adjust_shop_prices(self):
+ # Calculate prices for items in shops once all items have their final position
+ unknown_items_price = 250
+ earlygame_price_factor = 1.0
+ endgame_price_factor = 2.0
+ factor_diff = endgame_price_factor - earlygame_price_factor
+
+ global_price_factor = self.options.shop_prices_factor / 100.0
+
+ spheres = self.cached_spheres
+ sphere_count = len(spheres)
+ for sphere_id, sphere in enumerate(spheres):
+ location: LandstalkerLocation # after conditional, we guarantee it's this kind of location.
+ for location in sphere:
+ if location.player != self.player or location.type_string != "shop":
+ continue
+
+ current_playthrough_progression = sphere_id / sphere_count
+ progression_price_factor = earlygame_price_factor + (current_playthrough_progression * factor_diff)
+
+ price = location.item.price_in_shops \
+ if location.item.game == "Landstalker - The Treasures of King Nole" else unknown_items_price
+ price *= progression_price_factor
+ price *= global_price_factor
+ price -= price % 5
+ price = max(price, 5)
+ location.price = int(price)
+
+ @staticmethod
+ def get_jewel_names(count: JewelCount):
+ if count < 6:
+ return ["Red Jewel", "Purple Jewel", "Green Jewel", "Blue Jewel", "Yellow Jewel"][:count]
+
+ return ["Kazalt Jewel"] * count
diff --git a/worlds/landstalker/data/hint_source.py b/worlds/landstalker/data/hint_source.py
new file mode 100644
index 000000000000..4f22cac4bdd6
--- /dev/null
+++ b/worlds/landstalker/data/hint_source.py
@@ -0,0 +1,1989 @@
+HINT_SOURCES_JSON = [
+ {
+ "description": "Lithograph",
+ "smallTextbox": True
+ },
+ {
+ "description": "Oracle Stone",
+ "smallTextbox": True
+ },
+ {
+ "description": "Mercator fortune teller",
+ "textIds": [
+ 654
+ ]
+ },
+ {
+ "description": "King Nole's Cave sign",
+ "textIds": [
+ 253
+ ]
+ },
+ {
+ "description": "Foxy (next to Ryuma's mayor house)",
+ "entity": {
+ "mapId": 611,
+ "position": {
+ "x": 47,
+ "y": 25,
+ "z": 3
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "ryuma"
+ },
+ {
+ "description": "Foxy (behind trees in Gumi)",
+ "entity": {
+ "mapId": [602, 603],
+ "position": {
+ "x": 24,
+ "y": 35,
+ "z": 6
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "gumi"
+ },
+ {
+ "description": "Foxy (next to Mercator gates)",
+ "entity": {
+ "mapId": 454,
+ "position": {
+ "x": 18,
+ "y": 46,
+ "z": 0
+ },
+ "orientation": "se"
+ },
+ "nodeId": "route_gumi_ryuma"
+ },
+ {
+ "description": "Foxy (near basin behind Mercator)",
+ "entity": {
+ "mapId": 636,
+ "position": {
+ "x": 18,
+ "y": 27,
+ "z": 1
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "mercator"
+ },
+ {
+ "description": "Foxy (near cabin on Verla Shore)",
+ "entity": {
+ "mapId": 468,
+ "position": {
+ "x": 24,
+ "y": 45,
+ "z": 1
+ },
+ "orientation": "se"
+ },
+ "nodeId": "verla_shore"
+ },
+ {
+ "description": "Foxy (outside Verla Mines entrance)",
+ "entity": {
+ "mapId": 470,
+ "position": {
+ "x": 24,
+ "y": 29,
+ "z": 5
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "verla_shore"
+ },
+ {
+ "description": "Foxy (room below Thieves Hideout summit)",
+ "entity": {
+ "mapId": 221,
+ "position": {
+ "x": 29,
+ "y": 19,
+ "z": 2
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "thieves_hideout_post_key"
+ },
+ {
+ "description": "Foxy (near waterfall in Mountainous Area)",
+ "entity": {
+ "mapId": 485,
+ "position": {
+ "x": 42,
+ "y": 62,
+ "z": 2
+ },
+ "orientation": "nw",
+ "highPalette": True
+ },
+ "nodeId": "mountainous_area"
+ },
+ {
+ "description": "Foxy (in Mercator Castle left court)",
+ "entity": {
+ "mapId": 32,
+ "position": {
+ "x": 36,
+ "y": 38,
+ "z": 2
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "mercator"
+ },
+ {
+ "description": "Foxy (on Mercator inn balcony)",
+ "entity": {
+ "mapId": 632,
+ "position": {
+ "x": 19,
+ "y": 35,
+ "z": 4
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mercator"
+ },
+ {
+ "description": "Foxy (on a beach between Ryuma and Mercator)",
+ "entity": {
+ "mapId": 450,
+ "position": {
+ "x": 18,
+ "y": 28,
+ "z": 0
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "route_gumi_ryuma"
+ },
+ {
+ "description": "Foxy (atop Ryuma's lighthouse)",
+ "entity": {
+ "mapId": [628, 629],
+ "position": {
+ "x": 26,
+ "y": 21,
+ "z": 1
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "ryuma"
+ },
+ {
+ "description": "Foxy (looking at dead man in Thieves Hideout)",
+ "entity": {
+ "mapId": 210,
+ "position": {
+ "x": 25,
+ "y": 20,
+ "z": 2
+ },
+ "orientation": "se"
+ },
+ "nodeId": "thieves_hideout_pre_key"
+ },
+ {
+ "description": "Foxy (contemplating water near goddess statue in Thieves Hideout)",
+ "entity": {
+ "mapId": [219, 220],
+ "position": {
+ "x": 36,
+ "y": 31,
+ "z": 2
+ },
+ "orientation": "se"
+ },
+ "nodeId": "thieves_hideout_pre_key"
+ },
+ {
+ "description": "Foxy (after timed trial in Thieves Hideout)",
+ "entity": {
+ "mapId": 196,
+ "position": {
+ "x": 49,
+ "y": 24,
+ "z": 10
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "thieves_hideout_post_key"
+ },
+ {
+ "description": "Foxy (inside Mercator Castle armory tower)",
+ "entity": {
+ "mapId": 106,
+ "position": {
+ "x": 31,
+ "y": 30,
+ "z": 4
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "mercator"
+ },
+ {
+ "description": "Foxy (near Mercator Castle kitchen)",
+ "entity": {
+ "mapId": 71,
+ "position": {
+ "x": 15,
+ "y": 19,
+ "z": 1
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "mercator"
+ },
+ {
+ "description": "Foxy (in Mercator Castle library)",
+ "entity": {
+ "mapId": 73,
+ "position": {
+ "x": 18,
+ "y": 29,
+ "z": 0
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "mercator"
+ },
+ {
+ "description": "Foxy (in Mercator Dungeon main room)",
+ "entity": {
+ "mapId": 38,
+ "position": {
+ "x": 24,
+ "y": 35,
+ "z": 3
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mercator_dungeon"
+ },
+ {
+ "description": "Foxy (in hallway before tower in Mercator Dungeon)",
+ "entity": {
+ "mapId": 46,
+ "position": {
+ "x": 24,
+ "y": 13,
+ "z": 0
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "mercator_dungeon"
+ },
+ {
+ "description": "Foxy (atop Mercator Dungeon tower)",
+ "entity": {
+ "mapId": 35,
+ "position": {
+ "x": 31,
+ "y": 31,
+ "z": 12
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "mercator_dungeon"
+ },
+ {
+ "description": "Foxy (inside Mercator Crypt)",
+ "entity": {
+ "mapId": 647,
+ "position": {
+ "x": 30,
+ "y": 21,
+ "z": 2
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "crypt"
+ },
+ {
+ "description": "Foxy (on Verla beach)",
+ "entity": {
+ "mapId": 474,
+ "position": {
+ "x": 43,
+ "y": 30,
+ "z": 0
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "verla_shore"
+ },
+ {
+ "description": "Foxy (spying on house in Verla)",
+ "entity": {
+ "mapId": [711, 712],
+ "position": {
+ "x": 48,
+ "y": 29,
+ "z": 5
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "verla"
+ },
+ {
+ "description": "Foxy (on upper Verla shore, reachable from Dex exit)",
+ "entity": {
+ "mapId": 530,
+ "position": {
+ "x": 18,
+ "y": 29,
+ "z": 1
+ },
+ "orientation": "se"
+ },
+ "nodeId": "verla_mines"
+ },
+ {
+ "description": "Foxy (in Verla Mines jar staircase room)",
+ "entity": {
+ "mapId": 235,
+ "position": {
+ "x": 42,
+ "y": 22,
+ "z": 6
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "verla_mines"
+ },
+ {
+ "description": "Foxy (in Verla Mines lizards and crates room)",
+ "entity": {
+ "mapId": 239,
+ "position": {
+ "x": 32,
+ "y": 31,
+ "z": 3,
+ "halfX": True,
+ "halfY": True
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "verla_mines"
+ },
+ {
+ "description": "Foxy (in Verla Mines lava room in Slasher sector)",
+ "entity": {
+ "mapId": 252,
+ "position": {
+ "x": 16,
+ "y": 13,
+ "z": 1,
+ "halfX": True,
+ "halfY": True
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "verla_mines"
+ },
+ {
+ "description": "Foxy (in Verla Mines room behind lava)",
+ "entity": {
+ "mapId": 265,
+ "position": {
+ "x": 13,
+ "y": 16,
+ "z": 0
+ },
+ "orientation": "se"
+ },
+ "nodeId": "verla_mines"
+ },
+ {
+ "description": "Foxy (in Verla Mines lava room in Marley sector)",
+ "entity": {
+ "mapId": 264,
+ "position": {
+ "x": 18,
+ "y": 19,
+ "z": 6,
+ "halfX": True,
+ "halfY": True
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "verla_mines"
+ },
+ {
+ "description": "Foxy (on small rocky ledge in elevator map near Kelketo shop)",
+ "entity": {
+ "mapId": 473,
+ "position": {
+ "x": 35,
+ "y": 25,
+ "z": 8
+ },
+ "orientation": "se"
+ },
+ "nodeId": "route_verla_destel"
+ },
+ {
+ "description": "Foxy (contemplating fast currents below Kelketo shop)",
+ "entity": {
+ "mapId": 481,
+ "position": {
+ "x": 40,
+ "y": 48,
+ "z": 1
+ },
+ "orientation": "se"
+ },
+ "nodeId": "route_verla_destel"
+ },
+ {
+ "description": "Foxy (in Destel)",
+ "entity": {
+ "mapId": 726,
+ "position": {
+ "x": 48,
+ "y": 55,
+ "z": 5
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "destel"
+ },
+ {
+ "description": "Foxy (contemplating water near boatmaker house in route after Destel)",
+ "entity": {
+ "mapId": 489,
+ "position": {
+ "x": 23,
+ "y": 20,
+ "z": 1
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "route_after_destel"
+ },
+ {
+ "description": "Foxy (looking at Lake Shrine from elevated viewpoint)",
+ "entity": {
+ "mapId": 525,
+ "position": {
+ "x": 53,
+ "y": 45,
+ "z": 5
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "route_after_destel"
+ },
+ {
+ "description": "Foxy (on small floating block in Destel Well)",
+ "entity": {
+ "mapId": 275,
+ "position": {
+ "x": 27,
+ "y": 36,
+ "z": 5
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "destel_well"
+ },
+ {
+ "description": "Foxy (in Destel Well watery hub room)",
+ "entity": {
+ "mapId": 283,
+ "position": {
+ "x": 34,
+ "y": 41,
+ "z": 2
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "destel_well"
+ },
+ {
+ "description": "Foxy (in Destel Well watery room before boss)",
+ "entity": {
+ "mapId": 287,
+ "position": {
+ "x": 50,
+ "y": 46,
+ "z": 8,
+ "halfX": True,
+ "halfY": True
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "destel_well"
+ },
+ {
+ "description": "Foxy (at Destel Well exit on Lake Shrine side)",
+ "entity": {
+ "mapId": 545,
+ "position": {
+ "x": 58,
+ "y": 18,
+ "z": 0
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "route_lake_shrine"
+ },
+ {
+ "description": "Foxy (at crossroads on route to Lake Shrine)",
+ "entity": {
+ "mapId": 515,
+ "position": {
+ "x": 30,
+ "y": 20,
+ "z": 4
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "route_lake_shrine"
+ },
+ {
+ "description": "Foxy (on mountainous path to Lake Shrine)",
+ "entity": {
+ "mapId": 514,
+ "position": {
+ "x": 57,
+ "y": 24,
+ "z": 1
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "route_lake_shrine"
+ },
+ {
+ "description": "Foxy (in volcano to Lake Shrine)",
+ "entity": {
+ "mapId": 522,
+ "position": {
+ "x": 50,
+ "y": 39,
+ "z": 6,
+ "halfX": True,
+ "halfY": True
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "route_lake_shrine"
+ },
+ {
+ "description": "Foxy (next to Lake Shrine door)",
+ "entity": {
+ "mapId": 524,
+ "position": {
+ "x": 24,
+ "y": 51,
+ "z": 2,
+ "halfX": True
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "lake_shrine"
+ },
+ {
+ "description": "Foxy (above Greedly's shop)",
+ "entity": {
+ "mapId": 503,
+ "position": {
+ "x": 23,
+ "y": 35,
+ "z": 8
+ },
+ "orientation": "se"
+ },
+ "nodeId": "route_lake_shrine"
+ },
+ {
+ "description": "Foxy (contemplating water near Greedly's teleport tree)",
+ "entity": {
+ "mapId": 501,
+ "position": {
+ "x": 30,
+ "y": 26,
+ "z": 5
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "route_lake_shrine"
+ },
+ {
+ "description": "Foxy (in room after golem hops riddle in Lake Shrine)",
+ "entity": {
+ "mapId": 298,
+ "position": {
+ "x": 21,
+ "y": 19,
+ "z": 2
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "lake_shrine"
+ },
+ {
+ "description": "Foxy (in room next to green golem roundabout in Lake Shrine)",
+ "entity": {
+ "mapId": 293,
+ "position": {
+ "x": 19,
+ "y": 18,
+ "z": 2
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "lake_shrine"
+ },
+ {
+ "description": "Foxy (in Lake Shrine 'throne room')",
+ "entity": {
+ "mapId": 327,
+ "position": {
+ "x": 31,
+ "y": 31,
+ "z": 2
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "lake_shrine"
+ },
+ {
+ "description": "Foxy (in room next to golden golems roundabout in Lake Shrine)",
+ "entity": {
+ "mapId": 353,
+ "position": {
+ "x": 31,
+ "y": 20,
+ "z": 4
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "lake_shrine"
+ },
+ {
+ "description": "Foxy (in room near white golems roundabout in Lake Shrine)",
+ "entity": {
+ "mapId": 329,
+ "position": {
+ "x": 25,
+ "y": 25,
+ "z": 2,
+ "halfY": True
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "lake_shrine"
+ },
+ {
+ "description": "Foxy (next to Mir Tower)",
+ "entity": {
+ "mapId": 475,
+ "position": {
+ "x": 34,
+ "y": 17,
+ "z": 1
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mir_tower_sector"
+ },
+ {
+ "description": "Foxy (on the way to Mir Tower)",
+ "entity": {
+ "mapId": 464,
+ "position": {
+ "x": 22,
+ "y": 40,
+ "z": 1
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mir_tower_sector"
+ },
+ {
+ "description": "Foxy (near Twinkle Village)",
+ "entity": {
+ "mapId": 461,
+ "position": {
+ "x": 20,
+ "y": 21,
+ "z": 1
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mir_tower_sector"
+ },
+ {
+ "description": "Foxy (inside Tibor)",
+ "entity": {
+ "mapId": 813,
+ "position": {
+ "x": 19,
+ "y": 32,
+ "z": 2
+ },
+ "orientation": "se"
+ },
+ "nodeId": "tibor"
+ },
+ {
+ "description": "Foxy (inside Tibor spikeballs room)",
+ "entity": {
+ "mapId": 810,
+ "position": {
+ "x": 21,
+ "y": 33,
+ "z": 2,
+ "halfX": True,
+ "halfY": True
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "tibor"
+ },
+ {
+ "description": "Foxy (near Kado's house)",
+ "entity": {
+ "mapId": 430,
+ "position": {
+ "x": 24,
+ "y": 27,
+ "z": 11
+ },
+ "orientation": "se"
+ },
+ "nodeId": "route_gumi_ryuma"
+ },
+ {
+ "description": "Foxy (in Gumi boulder map)",
+ "entity": {
+ "mapId": 449,
+ "position": {
+ "x": 48,
+ "y": 20,
+ "z": 1,
+ "halfX": True
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "route_gumi_ryuma"
+ },
+ {
+ "description": "Foxy (at Waterfall Shrine crossroads)",
+ "entity": {
+ "mapId": 425,
+ "position": {
+ "x": 22,
+ "y": 56,
+ "z": 0,
+ "halfX": True
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "route_massan_gumi"
+ },
+ {
+ "description": "Foxy (in upstairs room inside Waterfall Shrine)",
+ "entity": {
+ "mapId": 182,
+ "position": {
+ "x": 29,
+ "y": 19,
+ "z": 4
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "waterfall_shrine"
+ },
+ {
+ "description": "Foxy (inside Waterfall Shrine pit)",
+ "entity": {
+ "mapId": 174,
+ "position": {
+ "x": 32,
+ "y": 29,
+ "z": 1
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "waterfall_shrine"
+ },
+ {
+ "description": "Foxy (in Massan)",
+ "entity": {
+ "mapId": 592,
+ "position": {
+ "x": 24,
+ "y": 46,
+ "z": 0,
+ "halfY": True
+ },
+ "orientation": "se"
+ },
+ "nodeId": "massan"
+ },
+ {
+ "description": "Foxy (in room at the bottom of ladders in Massan Cave)",
+ "entity": {
+ "mapId": 805,
+ "position": {
+ "x": 34,
+ "y": 30,
+ "z": 2,
+ "halfY": True
+ },
+ "orientation": "se"
+ },
+ "nodeId": "massan_cave"
+ },
+ {
+ "description": "Foxy (in treasure room of Massan Cave)",
+ "entity": {
+ "mapId": 807,
+ "position": {
+ "x": 28,
+ "y": 22,
+ "z": 1
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "massan_cave"
+ },
+ {
+ "description": "Foxy (bathing in the swamp next to Swamp Shrine entrance)",
+ "entity": {
+ "mapId": 433,
+ "position": {
+ "x": 39,
+ "y": 20,
+ "z": 0
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "massan_cave"
+ },
+ {
+ "description": "Foxy (in side room of Swamp Shrine accessible without Idol Stone)",
+ "entity": {
+ "mapId": 10,
+ "position": {
+ "x": 25,
+ "y": 27,
+ "z": 2,
+ "halfX": True
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "route_massan_gumi"
+ },
+ {
+ "description": "Foxy (in wooden room with falling EkeEke chest in Swamp Shrine)",
+ "entity": {
+ "mapId": 7,
+ "position": {
+ "x": 29,
+ "y": 25,
+ "z": 1,
+ "halfY": True
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "swamp_shrine"
+ },
+ {
+ "description": "Foxy (in Swamp Shrine carpet room)",
+ "entity": {
+ "mapId": 2,
+ "position": {
+ "x": 19,
+ "y": 33,
+ "z": 4
+ },
+ "orientation": "se"
+ },
+ "nodeId": "swamp_shrine"
+ },
+ {
+ "description": "Foxy (in Swamp Shrine spikeball storage room)",
+ "entity": {
+ "mapId": 16,
+ "position": {
+ "x": 25,
+ "y": 24,
+ "z": 2
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "swamp_shrine"
+ },
+ {
+ "description": "Foxy (in Swamp Shrine spiked floor room)",
+ "entity": {
+ "mapId": 21,
+ "position": {
+ "x": 27,
+ "y": 17,
+ "z": 4
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "swamp_shrine"
+ },
+ {
+ "description": "Foxy (in Mercator Castle backdoor court)",
+ "entity": {
+ "mapId": 639,
+ "position": {
+ "x": 23,
+ "y": 15,
+ "z": 0
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "mercator"
+ },
+ {
+ "description": "Foxy (on Greenmaze / Mountainous Area crossroad)",
+ "entity": {
+ "mapId": 460,
+ "position": {
+ "x": 16,
+ "y": 27,
+ "z": 4
+ },
+ "orientation": "se"
+ },
+ "nodeId": "greenmaze_pre_whistle"
+ },
+ {
+ "description": "Foxy (below Mountainous Area bridge)",
+ "entity": {
+ "mapId": 486,
+ "position": {
+ "x": 52,
+ "y": 45,
+ "z": 5
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mountainous_area"
+ },
+ {
+ "description": "Foxy (in Mountainous Area isolated cave)",
+ "entity": {
+ "mapId": 553,
+ "position": {
+ "x": 23,
+ "y": 21,
+ "z": 3
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "mountainous_area"
+ },
+ {
+ "description": "Foxy (in access to Zak arena inside Mountainous Area)",
+ "entity": {
+ "mapId": 487,
+ "position": {
+ "x": 44,
+ "y": 51,
+ "z": 3
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mountainous_area"
+ },
+ {
+ "description": "Foxy (in Zak arena inside Mountainous Area)",
+ "entity": {
+ "mapId": 492,
+ "position": {
+ "x": 27,
+ "y": 55,
+ "z": 9
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mountainous_area"
+ },
+ {
+ "description": "Foxy (in empty secret room inside Mountainous Area cave)",
+ "entity": {
+ "mapId": 552,
+ "position": {
+ "x": 24,
+ "y": 27,
+ "z": 0,
+ "halfX": True
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "mountainous_area"
+ },
+ {
+ "description": "Foxy (in empty visible room inside Mountainous Area cave)",
+ "entity": {
+ "mapId": 547,
+ "position": {
+ "x": 23,
+ "y": 23,
+ "z": 0,
+ "halfY": True
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mountainous_area"
+ },
+ {
+ "description": "Foxy (in waterfall entrance of Mountainous Area cave)",
+ "entity": {
+ "mapId": 549,
+ "position": {
+ "x": 27,
+ "y": 40,
+ "z": 0
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mountainous_area"
+ },
+ {
+ "description": "Foxy (on Mir Tower sector crossroads)",
+ "entity": {
+ "mapId": 458,
+ "position": {
+ "x": 21,
+ "y": 21,
+ "z": 1
+ },
+ "orientation": "se",
+ "highPalette": True
+ },
+ "nodeId": "mir_tower_sector"
+ },
+ {
+ "description": "Foxy (near Mountainous Area teleport tree)",
+ "entity": {
+ "mapId": 484,
+ "position": {
+ "x": 38,
+ "y": 57,
+ "z": 0
+ },
+ "orientation": "sw",
+ "highPalette": True
+ },
+ "nodeId": "mountainous_area"
+ },
+ {
+ "description": "Foxy (on route to Mountainous Area, in rocky arch map)",
+ "entity": {
+ "mapId": 500,
+ "position": {
+ "x": 19,
+ "y": 19,
+ "z": 7
+ },
+ "orientation": "sw",
+ "highPalette": True
+ },
+ "nodeId": "mountainous_area"
+ },
+ {
+ "description": "Foxy (on route to Mountainous Area, in L-shaped turn map)",
+ "entity": {
+ "mapId": 540,
+ "position": {
+ "x": 16,
+ "y": 23,
+ "z": 3
+ },
+ "orientation": "se",
+ "halfY": True,
+ "highPalette": True
+ },
+ "nodeId": "mountainous_area"
+ },
+ {
+ "description": "Foxy (in map next to Mountainous Area goddess statue)",
+ "entity": {
+ "mapId": 518,
+ "position": {
+ "x": 38,
+ "y": 33,
+ "z": 12
+ },
+ "orientation": "sw",
+ "highPalette": True
+ },
+ "nodeId": "mountainous_area"
+ },
+ {
+ "description": "Foxy (in King Nole's Cave isolated chest room)",
+ "entity": {
+ "mapId": 156,
+ "position": {
+ "x": 21,
+ "y": 27,
+ "z": 0,
+ "halfX": True
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "king_nole_cave"
+ },
+ {
+ "description": "Foxy (in King Nole's Cave crate stairway room)",
+ "entity": {
+ "mapId": 158,
+ "position": {
+ "x": 29,
+ "y": 26,
+ "z": 6
+ },
+ "orientation": "sw",
+ "highPalette": True
+ },
+ "nodeId": "king_nole_cave"
+ },
+ {
+ "description": "Foxy (in room before boulder hallway inside King Nole's Cave)",
+ "entity": {
+ "mapId": 147,
+ "position": {
+ "x": 26,
+ "y": 23,
+ "z": 2
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "king_nole_cave"
+ },
+ {
+ "description": "Foxy (in empty isolated room inside King Nole's Cave)",
+ "entity": {
+ "mapId": 162,
+ "position": {
+ "x": 26,
+ "y": 17,
+ "z": 0,
+ "halfX": True
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "king_nole_cave"
+ },
+ {
+ "description": "Foxy (looking at the waterfall in King Nole's Cave)",
+ "entity": {
+ "mapId": 164,
+ "position": {
+ "x": 22,
+ "y": 48,
+ "z": 1
+ },
+ "orientation": "sw",
+ "highPalette": True
+ },
+ "nodeId": "king_nole_cave"
+ },
+ {
+ "description": "Foxy (in King Nole's Cave teleporter to Kazalt)",
+ "entity": {
+ "mapId": 170,
+ "position": {
+ "x": 22,
+ "y": 27,
+ "z": 1
+ },
+ "orientation": "se"
+ },
+ "nodeId": "king_nole_cave"
+ },
+ {
+ "description": "Foxy (in access to Kazalt)",
+ "entity": {
+ "mapId": 739,
+ "position": {
+ "x": 17,
+ "y": 28,
+ "z": 1
+ },
+ "orientation": "se"
+ },
+ "nodeId": "kazalt"
+ },
+ {
+ "description": "Foxy (on Kazalt bridge)",
+ "entity": {
+ "mapId": 737,
+ "position": {
+ "x": 46,
+ "y": 34,
+ "z": 7
+ },
+ "orientation": "se"
+ },
+ "nodeId": "kazalt"
+ },
+ {
+ "description": "Foxy (in Mir Tower 0F isolated chest room)",
+ "entity": {
+ "mapId": 757,
+ "position": {
+ "x": 19,
+ "y": 24,
+ "z": 0
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mir_tower_pre_garlic"
+ },
+ {
+ "description": "Foxy (in Mir Tower activatable bridge room)",
+ "entity": {
+ "mapId": [752, 753],
+ "position": {
+ "x": 29,
+ "y": 34,
+ "z": 3,
+ "halfX": True
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "mir_tower_pre_garlic"
+ },
+ {
+ "description": "Foxy (in Garlic trial room inside Mir Tower)",
+ "entity": {
+ "mapId": 750,
+ "position": {
+ "x": 22,
+ "y": 21,
+ "z": 4
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "mir_tower_pre_garlic"
+ },
+ {
+ "description": "Foxy (in Mir Tower library)",
+ "entity": {
+ "mapId": 759,
+ "position": {
+ "x": 38,
+ "y": 29,
+ "z": 4
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "mir_tower_post_garlic"
+ },
+ {
+ "description": "Foxy (in Mir Tower priest room)",
+ "entity": {
+ "mapId": 775,
+ "position": {
+ "x": 23,
+ "y": 22,
+ "z": 1,
+ "halfX": True
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "mir_tower_post_garlic"
+ },
+ {
+ "description": "Foxy (right after making Miro flee with Garlic in Mir Tower)",
+ "entity": {
+ "mapId": 758,
+ "position": {
+ "x": 14,
+ "y": 34,
+ "z": 1
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mir_tower_post_garlic"
+ },
+ {
+ "description": "Foxy (in falling spikeballs room inside Mir Tower)",
+ "entity": {
+ "mapId": 761,
+ "position": {
+ "x": 14,
+ "y": 24,
+ "z": 1
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mir_tower_post_garlic"
+ },
+ {
+ "description": "Foxy (in first room of Mir Tower teleporter maze)",
+ "entity": {
+ "mapId": 767,
+ "position": {
+ "x": 18,
+ "y": 18,
+ "z": 2
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mir_tower_post_garlic"
+ },
+ {
+ "description": "Foxy (in small spikeballs room of Mir Tower teleporter maze)",
+ "entity": {
+ "mapId": 771,
+ "position": {
+ "x": 18,
+ "y": 18,
+ "z": 2
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "mir_tower_post_garlic"
+ },
+ {
+ "description": "Foxy (in wooden elevators room after Mir Tower teleporter maze)",
+ "entity": {
+ "mapId": 779,
+ "position": {
+ "x": 32,
+ "y": 20,
+ "z": 7,
+ "halfY": True
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "mir_tower_post_garlic"
+ },
+ {
+ "description": "Foxy (in room before Mir Tower boss room)",
+ "entity": {
+ "mapId": 783,
+ "position": {
+ "x": 32,
+ "y": 19,
+ "z": 2
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mir_tower_post_garlic"
+ },
+ {
+ "description": "Foxy (in Mir Tower treasure room)",
+ "entity": {
+ "mapId": 781,
+ "position": {
+ "x": 53,
+ "y": 26,
+ "z": 1
+ },
+ "orientation": "se"
+ },
+ "nodeId": "mir_tower_post_garlic"
+ },
+ {
+ "description": "Foxy (next to Waterfall Shrine entrance)",
+ "entity": {
+ "mapId": 426,
+ "position": {
+ "x": 46,
+ "y": 31,
+ "z": 0,
+ "halfX": True,
+ "halfY": True
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "route_massan_gumi"
+ },
+ {
+ "description": "Foxy (looking at river next to Massan teleport tree)",
+ "entity": {
+ "mapId": 424,
+ "position": {
+ "x": 44,
+ "y": 35,
+ "z": 0
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "route_massan_gumi"
+ },
+ {
+ "description": "Foxy (looking at bush at Swamp Shrine crossroads)",
+ "entity": {
+ "mapId": 440,
+ "position": {
+ "x": 25,
+ "y": 42,
+ "z": 4
+ },
+ "orientation": "nw",
+ "highPalette": True
+ },
+ "nodeId": "route_massan_gumi"
+ },
+ {
+ "description": "Foxy (at Helga's Hut crossroads)",
+ "entity": {
+ "mapId": 447,
+ "position": {
+ "x": 24,
+ "y": 17,
+ "z": 1
+ },
+ "orientation": "se",
+ "highPalette": True
+ },
+ "nodeId": "route_gumi_ryuma"
+ },
+ {
+ "description": "Foxy (near Helga's Hut)",
+ "entity": {
+ "mapId": 444,
+ "position": {
+ "x": 25,
+ "y": 26,
+ "z": 7
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "route_gumi_ryuma"
+ },
+ {
+ "description": "Foxy (in reapers room at Greenmaze entrance)",
+ "entity": {
+ "mapId": 571,
+ "position": {
+ "x": 31,
+ "y": 20,
+ "z": 6
+ },
+ "orientation": "nw",
+ "highPalette": True
+ },
+ "nodeId": "greenmaze_pre_whistle"
+ },
+ {
+ "description": "Foxy (near Greenmaze swamp)",
+ "entity": {
+ "mapId": 566,
+ "position": {
+ "x": 53,
+ "y": 51,
+ "z": 1
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "greenmaze_pre_whistle"
+ },
+ {
+ "description": "Foxy (spying on Cutter in Greenmaze)",
+ "entity": {
+ "mapId": 560,
+ "position": {
+ "x": 31,
+ "y": 52,
+ "z": 9
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "greenmaze_pre_whistle"
+ },
+ {
+ "description": "Foxy (in sector with red orcs making an elevator appear in Greenmaze)",
+ "entity": {
+ "mapId": 565,
+ "position": {
+ "x": 50,
+ "y": 30,
+ "z": 1
+ },
+ "orientation": "se"
+ },
+ "nodeId": "greenmaze_pre_whistle"
+ },
+ {
+ "description": "Foxy (in center of Greenmaze)",
+ "entity": {
+ "mapId": 576,
+ "position": {
+ "x": 32,
+ "y": 38,
+ "z": 5,
+ "halfY": True
+ },
+ "orientation": "se"
+ },
+ "nodeId": "greenmaze_pre_whistle"
+ },
+ {
+ "description": "Foxy (in waterfall sector of Greenmaze)",
+ "entity": {
+ "mapId": 568,
+ "position": {
+ "x": 29,
+ "y": 41,
+ "z": 7,
+ "halfX": True
+ },
+ "orientation": "ne",
+ "highPalette": True
+ },
+ "nodeId": "greenmaze_pre_whistle"
+ },
+ {
+ "description": "Foxy (in ropes sector of Greenmaze)",
+ "entity": {
+ "mapId": 567,
+ "position": {
+ "x": 38,
+ "y": 28,
+ "z": 0
+ },
+ "orientation": "se"
+ },
+ "nodeId": "greenmaze_pre_whistle"
+ },
+ {
+ "description": "Foxy (in Sun Stone sector of Greenmaze)",
+ "entity": {
+ "mapId": 564,
+ "position": {
+ "x": 30,
+ "y": 35,
+ "z": 1
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "greenmaze_pre_whistle"
+ },
+ {
+ "description": "Foxy (in first chest map of Greenmaze after cutting trees)",
+ "entity": {
+ "mapId": 570,
+ "position": {
+ "x": 26,
+ "y": 15,
+ "z": 1
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "greenmaze_post_whistle"
+ },
+ {
+ "description": "Foxy (near shortcut cavern entrance in Greenmaze after cutting trees)",
+ "entity": {
+ "mapId": 569,
+ "position": {
+ "x": 20,
+ "y": 24,
+ "z": 6,
+ "halfY": True
+ },
+ "orientation": "se"
+ },
+ "nodeId": "greenmaze_post_whistle"
+ },
+ {
+ "description": "Foxy (in room next to spiked floor and keydoor room in King Nole's Labyrinth)",
+ "entity": {
+ "mapId": 380,
+ "position": {
+ "x": 17,
+ "y": 18,
+ "z": 0
+ },
+ "orientation": "se"
+ },
+ "nodeId": "king_nole_labyrinth_pre_door"
+ },
+ {
+ "description": "Foxy (in ice shortcut room in King Nole's Labyrinth)",
+ "entity": {
+ "mapId": 390,
+ "position": {
+ "x": 19,
+ "y": 41,
+ "z": 2
+ },
+ "orientation": "se"
+ },
+ "nodeId": "king_nole_labyrinth_pre_door"
+ },
+ {
+ "description": "Foxy (in exterior room of King Nole's Labyrinth)",
+ "entity": {
+ "mapId": 362,
+ "position": {
+ "x": 35,
+ "y": 21,
+ "z": 2
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "king_nole_labyrinth_pre_door"
+ },
+ {
+ "description": "Foxy (in room above Iron Boots in King Nole's Labyrinth)",
+ "entity": {
+ "mapId": 373,
+ "position": {
+ "x": 26,
+ "y": 30,
+ "z": 2
+ },
+ "orientation": "se"
+ },
+ "nodeId": "king_nole_labyrinth_post_door"
+ },
+ {
+ "description": "Foxy (next to raft starting point in King Nole's Labyrinth)",
+ "entity": {
+ "mapId": 406,
+ "position": {
+ "x": 46,
+ "y": 40,
+ "z": 7
+ },
+ "orientation": "nw",
+ "highPalette": True
+ },
+ "nodeId": "king_nole_labyrinth_raft_entrance"
+ },
+ {
+ "description": "Foxy (in fast boulder room in King Nole's Labyrinth)",
+ "entity": {
+ "mapId": 382,
+ "position": {
+ "x": 30,
+ "y": 30,
+ "z": 7,
+ "halfX": True
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "king_nole_labyrinth_post_door"
+ },
+ {
+ "description": "Foxy (in first maze room inside King Nole's Labyrinth)",
+ "entity": {
+ "mapId": 367,
+ "position": {
+ "x": 43,
+ "y": 38,
+ "z": 1
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "king_nole_labyrinth_post_door"
+ },
+ {
+ "description": "Foxy (in lava sector of King Nole's Labyrinth)",
+ "entity": {
+ "mapId": 399,
+ "position": {
+ "x": 23,
+ "y": 19,
+ "z": 2,
+ "halfY": True
+ },
+ "orientation": "se"
+ },
+ "nodeId": "king_nole_labyrinth_post_door"
+ },
+ {
+ "description": "Foxy (in hands room inside King Nole's Labyrinth)",
+ "entity": {
+ "mapId": 418,
+ "position": {
+ "x": 41,
+ "y": 31,
+ "z": 7
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "king_nole_labyrinth_post_door"
+ },
+ {
+ "description": "Foxy (next to King Nole's Palace entrance)",
+ "entity": {
+ "mapId": 422,
+ "position": {
+ "x": 27,
+ "y": 25,
+ "z": 2
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "king_nole_labyrinth_path_to_palace"
+ },
+ {
+ "description": "Foxy (in King Nole's Palace entrance room)",
+ "entity": {
+ "mapId": 122,
+ "position": {
+ "x": 30,
+ "y": 35,
+ "z": 8
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "king_nole_palace"
+ },
+ {
+ "description": "Foxy (in King Nole's Palace jar and moving platforms room)",
+ "entity": {
+ "mapId": 126,
+ "position": {
+ "x": 27,
+ "y": 37,
+ "z": 6
+ },
+ "orientation": "se"
+ },
+ "nodeId": "king_nole_palace"
+ },
+ {
+ "description": "Foxy (in King Nole's Palace last chest room)",
+ "entity": {
+ "mapId": 125,
+ "position": {
+ "x": 25,
+ "y": 39,
+ "z": 2
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "king_nole_palace"
+ },
+ {
+ "description": "Foxy (in Mercator casino)",
+ "entity": {
+ "mapId": 663,
+ "position": {
+ "x": 16,
+ "y": 58,
+ "z": 0,
+ "halfX": True,
+ "halfY": True
+ },
+ "orientation": "ne"
+ },
+ "nodeId": "mercator_casino"
+ },
+ {
+ "description": "Foxy (in Helga's hut basement)",
+ "entity": {
+ "mapId": 479,
+ "position": {
+ "x": 20,
+ "y": 33,
+ "z": 0,
+ "halfX": True
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "helga_hut"
+ },
+ {
+ "description": "Foxy (in Helga's hut dungeon deepest room)",
+ "entity": {
+ "mapId": 802,
+ "position": {
+ "x": 28,
+ "y": 19,
+ "z": 2
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "helga_hut"
+ },
+ {
+ "description": "Foxy (in Helga's hut dungeon topmost room)",
+ "entity": {
+ "mapId": 786,
+ "position": {
+ "x": 25,
+ "y": 23,
+ "z": 2,
+ "halfY": True
+ },
+ "orientation": "se"
+ },
+ "nodeId": "helga_hut"
+ },
+ {
+ "description": "Foxy (in Swamp Shrine right aisle room)",
+ "entity": {
+ "mapId": 1,
+ "position": {
+ "x": 34,
+ "y": 20,
+ "z": 2
+ },
+ "orientation": "se",
+ "highPalette": True
+ },
+ "nodeId": "swamp_shrine"
+ },
+ {
+ "description": "Foxy (upstairs in Swamp Shrine main hall)",
+ "entity": {
+ "mapId": [5, 15],
+ "position": {
+ "x": 45,
+ "y": 24,
+ "z": 8,
+ "halfY": True
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "swamp_shrine"
+ },
+ {
+ "description": "Foxy (in room before boss inside Swamp Shrine)",
+ "entity": {
+ "mapId": 30,
+ "position": {
+ "x": 19,
+ "y": 25,
+ "z": 2,
+ "halfY": True
+ },
+ "orientation": "se"
+ },
+ "nodeId": "swamp_shrine"
+ },
+ {
+ "description": "Foxy (in Thieves Hideout entrance room)",
+ "entity": {
+ "mapId": [185, 186],
+ "position": {
+ "x": 40,
+ "y": 35,
+ "z": 2
+ },
+ "orientation": "se",
+ "highPalette": True
+ },
+ "nodeId": "thieves_hideout_pre_key"
+ },
+ {
+ "description": "Foxy (in Thieves Hideout room with hidden door behind waterfall)",
+ "entity": {
+ "mapId": [192, 193],
+ "position": {
+ "x": 30,
+ "y": 34,
+ "z": 1
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "thieves_hideout_pre_key"
+ },
+ {
+ "description": "Foxy (in Thieves Hideout double chest room before goddess statue)",
+ "entity": {
+ "mapId": 215,
+ "position": {
+ "x": 17,
+ "y": 17,
+ "z": 0
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "thieves_hideout_pre_key"
+ },
+ {
+ "description": "Foxy (in hub room after Thieves Hideout keydoor)",
+ "entity": {
+ "mapId": 199,
+ "position": {
+ "x": 24,
+ "y": 52,
+ "z": 2
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "thieves_hideout_post_key"
+ },
+ {
+ "description": "Foxy (in reward room after Thieves Hideout moving balls riddle)",
+ "entity": {
+ "mapId": 205,
+ "position": {
+ "x": 32,
+ "y": 24,
+ "z": 0
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "thieves_hideout_post_key"
+ },
+ {
+ "description": "Foxy (in Lake Shrine main hallway)",
+ "entity": {
+ "mapId": 302,
+ "position": {
+ "x": 20,
+ "y": 19,
+ "z": 0
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "lake_shrine"
+ },
+ {
+ "description": "Foxy (in triple chest room in Slasher sector of Verla Mines)",
+ "entity": {
+ "mapId": 256,
+ "position": {
+ "x": 23,
+ "y": 23,
+ "z": 0
+ },
+ "orientation": "sw"
+ },
+ "nodeId": "verla_mines"
+ },
+ {
+ "description": "Foxy (near teleport tree after Destel)",
+ "entity": {
+ "mapId": 488,
+ "position": {
+ "x": 28,
+ "y": 53,
+ "z": 0
+ },
+ "orientation": "se"
+ },
+ "nodeId": "route_after_destel"
+ },
+ {
+ "description": "Foxy (in lower half of mimics room in King Nole's Labyrinth)",
+ "entity": {
+ "mapId": 383,
+ "position": {
+ "x": 26,
+ "y": 26,
+ "z": 2
+ },
+ "orientation": "nw"
+ },
+ "nodeId": "king_nole_labyrinth_pre_door"
+ }
+]
diff --git a/worlds/landstalker/data/item_source.py b/worlds/landstalker/data/item_source.py
new file mode 100644
index 000000000000..e0a2d701f4bf
--- /dev/null
+++ b/worlds/landstalker/data/item_source.py
@@ -0,0 +1,2017 @@
+ITEM_SOURCES_JSON = [
+ {
+ "name": "Swamp Shrine (0F): chest in room to the right",
+ "type": "chest",
+ "nodeId": "swamp_shrine",
+ "chestId": 0
+ },
+ {
+ "name": "Swamp Shrine (0F): chest in carpet room",
+ "type": "chest",
+ "nodeId": "swamp_shrine",
+ "chestId": 1
+ },
+ {
+ "name": "Swamp Shrine (0F): chest in left hallway (accessed by falling from upstairs)",
+ "type": "chest",
+ "nodeId": "swamp_shrine",
+ "chestId": 2
+ },
+ {
+ "name": "Swamp Shrine (0F): falling chest after beating orc",
+ "type": "chest",
+ "nodeId": "swamp_shrine",
+ "chestId": 3
+ },
+ {
+ "name": "Swamp Shrine (0F): chest in room visible from second entrance",
+ "type": "chest",
+ "nodeId": "swamp_shrine",
+ "chestId": 4
+ },
+ {
+ "name": "Swamp Shrine (1F): lower chest in wooden bridges room",
+ "type": "chest",
+ "nodeId": "swamp_shrine",
+ "chestId": 5
+ },
+ {
+ "name": "Swamp Shrine (2F): upper chest in wooden bridges room",
+ "type": "chest",
+ "nodeId": "swamp_shrine",
+ "chestId": 6
+ },
+ {
+ "name": "Swamp Shrine (2F): chest on spiked floor room balcony",
+ "type": "chest",
+ "nodeId": "swamp_shrine",
+ "chestId": 7
+ },
+ {
+ "name": "Swamp Shrine (3F): chest in boss arena",
+ "type": "chest",
+ "nodeId": "swamp_shrine",
+ "chestId": 8
+ },
+ {
+ "name": "Mercator Dungeon (-1F): chest on elevated path near entrance",
+ "type": "chest",
+ "nodeId": "mercator_dungeon",
+ "hints": [
+ "hidden in the depths of Mercator"
+ ],
+ "chestId": 9
+ },
+ {
+ "name": "Mercator Dungeon (-1F): chest in Moralis's cell",
+ "type": "chest",
+ "nodeId": "mercator_dungeon",
+ "hints": [
+ "hidden in the depths of Mercator"
+ ],
+ "chestId": 10
+ },
+ {
+ "name": "Mercator Dungeon (-1F): left chest in undeground double chest room",
+ "type": "chest",
+ "nodeId": "mercator_dungeon",
+ "hints": [
+ "hidden in the depths of Mercator"
+ ],
+ "chestId": 11
+ },
+ {
+ "name": "Mercator Dungeon (-1F): right chest in undeground double chest room",
+ "type": "chest",
+ "nodeId": "mercator_dungeon",
+ "hints": [
+ "hidden in the depths of Mercator"
+ ],
+ "chestId": 12
+ },
+ {
+ "name": "Mercator: castle kitchen chest",
+ "type": "chest",
+ "nodeId": "mercator",
+ "chestId": 13
+ },
+ {
+ "name": "Mercator: chest in special shop backroom",
+ "type": "chest",
+ "nodeId": "mercator",
+ "chestId": 14
+ },
+ {
+ "name": "Mercator Dungeon (1F): left chest in tower double chest room",
+ "type": "chest",
+ "nodeId": "mercator_dungeon",
+ "hints": [
+ "inside a tower"
+ ],
+ "chestId": 15
+ },
+ {
+ "name": "Mercator Dungeon (1F): right chest in tower double chest room",
+ "type": "chest",
+ "nodeId": "mercator_dungeon",
+ "hints": [
+ "inside a tower"
+ ],
+ "chestId": 16
+ },
+ {
+ "name": "Mercator: chest in castle tower (ladder revealed by slashing armor)",
+ "type": "chest",
+ "nodeId": "mercator",
+ "hints": [
+ "inside a tower"
+ ],
+ "chestId": 17
+ },
+ {
+ "name": "Mercator Dungeon (4F): chest in topmost tower room",
+ "type": "chest",
+ "nodeId": "mercator_dungeon",
+ "hints": [
+ "inside a tower"
+ ],
+ "chestId": 18
+ },
+ {
+ "name": "King Nole's Palace: chest at entrance",
+ "type": "chest",
+ "nodeId": "king_nole_palace",
+ "chestId": 19
+ },
+ {
+ "name": "King Nole's Palace: chest along central pit",
+ "type": "chest",
+ "nodeId": "king_nole_palace",
+ "chestId": 20
+ },
+ {
+ "name": "King Nole's Palace: chest in floating button room",
+ "type": "chest",
+ "nodeId": "king_nole_palace",
+ "chestId": 21
+ },
+ {
+ "name": "King Nole's Cave: chest in second room",
+ "type": "chest",
+ "nodeId": "king_nole_cave",
+ "chestId": 22
+ },
+ {
+ "name": "King Nole's Cave: first chest in third room",
+ "type": "chest",
+ "nodeId": "king_nole_cave",
+ "chestId": 24
+ },
+ {
+ "name": "King Nole's Cave: second chest in third room",
+ "type": "chest",
+ "nodeId": "king_nole_cave",
+ "chestId": 25
+ },
+ {
+ "name": "King Nole's Cave: chest in isolated room",
+ "type": "chest",
+ "nodeId": "king_nole_cave",
+ "chestId": 28
+ },
+ {
+ "name": "King Nole's Cave: chest in crate room",
+ "type": "chest",
+ "nodeId": "king_nole_cave",
+ "chestId": 29
+ },
+ {
+ "name": "King Nole's Cave: boulder chase hallway chest",
+ "type": "chest",
+ "nodeId": "king_nole_cave",
+ "chestId": 31
+ },
+ {
+ "name": "Waterfall Shrine: chest under entrance hallway",
+ "type": "chest",
+ "nodeId": "waterfall_shrine",
+ "chestId": 33
+ },
+ {
+ "name": "Waterfall Shrine: chest near Prospero",
+ "type": "chest",
+ "nodeId": "waterfall_shrine",
+ "chestId": 34
+ },
+ {
+ "name": "Waterfall Shrine: chest on right branch of biggest room",
+ "type": "chest",
+ "nodeId": "waterfall_shrine",
+ "chestId": 35
+ },
+ {
+ "name": "Waterfall Shrine: upstairs chest",
+ "type": "chest",
+ "nodeId": "waterfall_shrine",
+ "chestId": 36
+ },
+ {
+ "name": "Thieves Hideout: chest under water in entrance room",
+ "type": "chest",
+ "nodeId": "thieves_hideout_pre_key",
+ "chestId": 38
+ },
+ {
+ "name": "Thieves Hideout (back): right chest after teal knight mini-boss",
+ "type": "chest",
+ "nodeId": "thieves_hideout_post_key",
+ "chestId": 41
+ },
+ {
+ "name": "Thieves Hideout (back): left chest after teal knight mini-boss",
+ "type": "chest",
+ "nodeId": "thieves_hideout_post_key",
+ "chestId": 42
+ },
+ {
+ "name": "Thieves Hideout: left chest in Pockets cell",
+ "type": "chest",
+ "nodeId": "thieves_hideout_pre_key",
+ "chestId": 43
+ },
+ {
+ "name": "Thieves Hideout: right chest in Pockets cell",
+ "type": "chest",
+ "nodeId": "thieves_hideout_pre_key",
+ "chestId": 44
+ },
+ {
+ "name": "Thieves Hideout (back): second chest in hallway after quick climb trial",
+ "type": "chest",
+ "nodeId": "thieves_hideout_post_key",
+ "chestId": 45
+ },
+ {
+ "name": "Thieves Hideout (back): first chest in hallway after quick climb trial",
+ "type": "chest",
+ "nodeId": "thieves_hideout_post_key",
+ "chestId": 46
+ },
+ {
+ "name": "Thieves Hideout (back): chest in moving platforms room",
+ "type": "chest",
+ "nodeId": "thieves_hideout_post_key",
+ "chestId": 47
+ },
+ {
+ "name": "Thieves Hideout (back): chest in falling platforms room",
+ "type": "chest",
+ "nodeId": "thieves_hideout_post_key",
+ "chestId": 48
+ },
+ {
+ "name": "Thieves Hideout (back): reward chest after moving balls room",
+ "type": "chest",
+ "nodeId": "thieves_hideout_post_key",
+ "chestId": 49
+ },
+ {
+ "name": "Thieves Hideout: rolling boulder chest near entrance",
+ "type": "chest",
+ "nodeId": "thieves_hideout_pre_key",
+ "chestId": 50
+ },
+ {
+ "name": "Thieves Hideout: left chest in room on the way to goddess statue",
+ "type": "chest",
+ "nodeId": "thieves_hideout_pre_key",
+ "chestId": 52
+ },
+ {
+ "name": "Thieves Hideout: right chest in room on the way to goddess statue",
+ "type": "chest",
+ "nodeId": "thieves_hideout_pre_key",
+ "chestId": 53
+ },
+ {
+ "name": "Thieves Hideout (back): left chest in room before boss",
+ "type": "chest",
+ "nodeId": "thieves_hideout_post_key",
+ "chestId": 54
+ },
+ {
+ "name": "Thieves Hideout (back): right chest in room before boss",
+ "type": "chest",
+ "nodeId": "thieves_hideout_post_key",
+ "chestId": 55
+ },
+ {
+ "name": "Thieves Hideout (back): chest #1 in boss reward room",
+ "type": "chest",
+ "nodeId": "thieves_hideout_post_key",
+ "chestId": 56
+ },
+ {
+ "name": "Thieves Hideout (back): chest #2 in boss reward room",
+ "type": "chest",
+ "nodeId": "thieves_hideout_post_key",
+ "chestId": 57
+ },
+ {
+ "name": "Thieves Hideout (back): chest #3 in boss reward room",
+ "type": "chest",
+ "nodeId": "thieves_hideout_post_key",
+ "chestId": 58
+ },
+ {
+ "name": "Thieves Hideout (back): chest #4 in boss reward room",
+ "type": "chest",
+ "nodeId": "thieves_hideout_post_key",
+ "chestId": 59
+ },
+ {
+ "name": "Thieves Hideout (back): chest #5 in boss reward room",
+ "type": "chest",
+ "nodeId": "thieves_hideout_post_key",
+ "chestId": 60
+ },
+ {
+ "name": "Verla Mines: right chest in double chest room near entrance",
+ "type": "chest",
+ "nodeId": "verla_mines",
+ "chestId": 66
+ },
+ {
+ "name": "Verla Mines: left chest in double chest room near entrance",
+ "type": "chest",
+ "nodeId": "verla_mines",
+ "chestId": 67
+ },
+ {
+ "name": "Verla Mines: chest on jar staircase room balcony",
+ "type": "chest",
+ "nodeId": "verla_mines",
+ "chestId": 68
+ },
+ {
+ "name": "Verla Mines: Dex reward chest",
+ "type": "chest",
+ "nodeId": "verla_mines",
+ "chestId": 69
+ },
+ {
+ "name": "Verla Mines: Slasher reward chest",
+ "type": "chest",
+ "nodeId": "verla_mines",
+ "hints": [
+ "kept by a threatening guardian"
+ ],
+ "chestId": 70
+ },
+ {
+ "name": "Verla Mines: left chest in 3-chests room near Slasher",
+ "type": "chest",
+ "nodeId": "verla_mines",
+ "chestId": 71
+ },
+ {
+ "name": "Verla Mines: middle chest in 3-chests room near Slasher",
+ "type": "chest",
+ "nodeId": "verla_mines",
+ "chestId": 72
+ },
+ {
+ "name": "Verla Mines: right chest in 3-chests room near Slasher",
+ "type": "chest",
+ "nodeId": "verla_mines",
+ "chestId": 73
+ },
+ {
+ "name": "Verla Mines: right chest in button room near elevator shaft leading to Marley",
+ "type": "chest",
+ "nodeId": "verla_mines",
+ "chestId": 74
+ },
+ {
+ "name": "Verla Mines: left chest in button room near elevator shaft leading to Marley",
+ "type": "chest",
+ "nodeId": "verla_mines",
+ "chestId": 75
+ },
+ {
+ "name": "Verla Mines: chest in hidden room accessed by walking on lava",
+ "type": "chest",
+ "nodeId": "verla_mines_behind_lava",
+ "hints": [
+ "in a very hot place"
+ ],
+ "chestId": 76
+ },
+ {
+ "name": "Destel Well (0F): 4 crates puzzle room chest",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 77
+ },
+ {
+ "name": "Destel Well (1F): chest on small stairs",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 78
+ },
+ {
+ "name": "Destel Well (1F): chest on narrow floating ground",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 79
+ },
+ {
+ "name": "Destel Well (1F): chest in spiky hallway",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 80
+ },
+ {
+ "name": "Destel Well (2F): chest in ghosts room",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 81
+ },
+ {
+ "name": "Destel Well (2F): chest in falling platforms room",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 82
+ },
+ {
+ "name": "Destel Well (2F): right chest in Pockets room",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 83
+ },
+ {
+ "name": "Destel Well (2F): left chest in Pockets room",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 84
+ },
+ {
+ "name": "Destel Well (3F): chest in first trapped arena",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 85
+ },
+ {
+ "name": "Destel Well (3F): chest in trapped giants room",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 86
+ },
+ {
+ "name": "Destel Well (3F): chest in second trapped arena",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 87
+ },
+ {
+ "name": "Destel Well (4F): top chest in room before boss",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 88
+ },
+ {
+ "name": "Destel Well (4F): left chest in room before boss",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 89
+ },
+ {
+ "name": "Destel Well (4F): bottom chest in room before boss",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 90
+ },
+ {
+ "name": "Destel Well (4F): right chest in room before boss",
+ "type": "chest",
+ "nodeId": "destel_well",
+ "chestId": 91
+ },
+ {
+ "name": "Lake Shrine (-1F): chest in crate room near green golem spinner",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "chestId": 92
+ },
+ {
+ "name": "Lake Shrine (-1F): chest in hallway with hole leading downstairs",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "chestId": 93
+ },
+ {
+ "name": "Lake Shrine (-1F): chest in spikeballs hallway near green golem spinner",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "chestId": 94
+ },
+ {
+ "name": "Lake Shrine (-1F): reward chest for golem hopping puzzle",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "chestId": 95
+ },
+ {
+ "name": "Lake Shrine (-2F): chest on room corner accessed by falling from above",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "chestId": 96
+ },
+ {
+ "name": "Lake Shrine (-2F): lower chest in throne room",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "chestId": 97
+ },
+ {
+ "name": "Lake Shrine (-2F): upper chest in throne room",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "chestId": 98
+ },
+ {
+ "name": "Lake Shrine (-3F): chest on floating platform in white golems room",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "chestId": 99
+ },
+ {
+ "name": "Lake Shrine (-3F): chest near Sword of Ice",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "chestId": 100
+ },
+ {
+ "name": "Lake Shrine (-3F): chest in snake trapping puzzle room",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "chestId": 101
+ },
+ {
+ "name": "Lake Shrine (-3F): chest on cube accessed by falling from upstairs",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "chestId": 102
+ },
+ {
+ "name": "Lake Shrine (-3F): chest in watery archway room",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "chestId": 103
+ },
+ {
+ "name": "Lake Shrine (-3F): left reward chest in boss room",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "hints": [
+ "kept by a threatening guardian"
+ ],
+ "chestId": 104
+ },
+ {
+ "name": "Lake Shrine (-3F): middle reward chest in boss room",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "hints": [
+ "kept by a threatening guardian"
+ ],
+ "chestId": 105
+ },
+ {
+ "name": "Lake Shrine (-3F): right reward chest in boss room",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "hints": [
+ "kept by a threatening guardian"
+ ],
+ "chestId": 106
+ },
+ {
+ "name": "Lake Shrine (-3F): chest near golden golems spinner",
+ "type": "chest",
+ "nodeId": "lake_shrine",
+ "chestId": 107
+ },
+ {
+ "name": "King Nole's Labyrinth (0F): chest in exterior room",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_exterior",
+ "chestId": 108
+ },
+ {
+ "name": "King Nole's Labyrinth (0F): left chest in room after key door",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "chestId": 109
+ },
+ {
+ "name": "King Nole's Labyrinth (0F): right chest in room after key door",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "chestId": 110
+ },
+ {
+ "name": "King Nole's Labyrinth (-1F): chest in maze room with healing tile",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "chestId": 111
+ },
+ {
+ "name": "King Nole's Labyrinth (0F): chest in spike balls room",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_pre_door",
+ "chestId": 112
+ },
+ {
+ "name": "King Nole's Labyrinth (-1F): right chest in 3-chest dark room (left side)",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "chestId": 113
+ },
+ {
+ "name": "King Nole's Labyrinth (-1F): chest in 3-chest dark room (right side)",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_pre_door",
+ "chestId": 114
+ },
+ {
+ "name": "King Nole's Labyrinth (-1F): left chest in 3-chest dark room (left side)",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "chestId": 115
+ },
+ {
+ "name": "King Nole's Labyrinth (-1F): chest in maze room with two buttons",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "chestId": 116
+ },
+ {
+ "name": "King Nole's Labyrinth (-1F): upper chest in lantern room",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_pre_door",
+ "chestId": 117
+ },
+ {
+ "name": "King Nole's Labyrinth (-1F): lower chest in lantern room",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_pre_door",
+ "chestId": 118
+ },
+ {
+ "name": "King Nole's Labyrinth (-1F): chest in ice shortcut room",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_pre_door",
+ "chestId": 119
+ },
+ {
+ "name": "King Nole's Labyrinth (-2F): chest in save room",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "chestId": 120
+ },
+ {
+ "name": "King Nole's Labyrinth (-1F): chest in room with button and crates stairway",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "chestId": 121
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): first chest before Firedemon",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "hints": [
+ "in a very hot place"
+ ],
+ "chestId": 122
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): second chest before Firedemon",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "hints": [
+ "in a very hot place"
+ ],
+ "chestId": 123
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): reward chest for beating Firedemon",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "hints": [
+ "kept by a threatening guardian",
+ "in a very hot place"
+ ],
+ "chestId": 124
+ },
+ {
+ "name": "King Nole's Labyrinth (-2F): chest in four buttons room",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "chestId": 125
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): first chest after falling from raft",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_raft",
+ "chestId": 126
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): left chest in room before Spinner",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_raft",
+ "chestId": 127
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): right chest in room before Spinner",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_raft",
+ "chestId": 128
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): reward chest for beating Spinner",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_raft",
+ "hints": [
+ "kept by a threatening guardian"
+ ],
+ "chestId": 129
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): chest in room after Spinner",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_raft",
+ "hints": [
+ "kept by a threatening guardian"
+ ],
+ "chestId": 130
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): chest in room before Miro",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_path_to_palace",
+ "hints": [
+ "close to a waterfall"
+ ],
+ "chestId": 131
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): reward chest for beating Miro",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_path_to_palace",
+ "hints": [
+ "kept by a threatening guardian"
+ ],
+ "chestId": 132
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): chest in hands room",
+ "type": "chest",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "chestId": 133
+ },
+ {
+ "name": "Route between Gumi and Ryuma: chest on the way to Swordsman Kado",
+ "type": "chest",
+ "nodeId": "route_gumi_ryuma",
+ "chestId": 134
+ },
+ {
+ "name": "Route between Massan and Gumi: chest on cliff",
+ "type": "chest",
+ "nodeId": "route_massan_gumi",
+ "hints": [
+ "near a swamp"
+ ],
+ "chestId": 135
+ },
+ {
+ "name": "Route between Mercator and Verla: chest on cliff next to tree",
+ "type": "chest",
+ "nodeId": "mir_tower_sector",
+ "chestId": 136
+ },
+ {
+ "name": "Route between Mercator and Verla: chest on cliff next to blocked cave",
+ "type": "chest",
+ "nodeId": "mir_tower_sector",
+ "chestId": 137
+ },
+ {
+ "name": "Route between Mercator and Verla: chest near Twinkle village",
+ "type": "chest",
+ "nodeId": "mir_tower_sector",
+ "chestId": 138
+ },
+ {
+ "name": "Verla Shore: chest on corner cliff after Verla tunnel",
+ "type": "chest",
+ "nodeId": "verla_shore",
+ "chestId": 139
+ },
+ {
+ "name": "Verla Shore: chest on highest cliff after Verla tunnel (accessible through Verla mines)",
+ "type": "chest",
+ "nodeId": "verla_shore_cliff",
+ "chestId": 140
+ },
+ {
+ "name": "Route to Mir Tower: chest on cliff accessed by pressing hidden switch",
+ "type": "chest",
+ "nodeId": "mir_tower_sector",
+ "chestId": 141
+ },
+ {
+ "name": "Route to Mir Tower: chest behind first sacred tree",
+ "type": "chest",
+ "nodeId": "mir_tower_sector_tree_ledge",
+ "chestId": 142
+ },
+ {
+ "name": "Verla Shore: chest behind cabin",
+ "type": "chest",
+ "nodeId": "verla_shore",
+ "hints": [
+ "in a well-hidden chest"
+ ],
+ "chestId": 143
+ },
+ {
+ "name": "Route to Destel: chest in map right after Verla mines exit",
+ "type": "chest",
+ "nodeId": "route_verla_destel",
+ "chestId": 144
+ },
+ {
+ "name": "Route to Destel: chest in small platform elevator map",
+ "type": "chest",
+ "nodeId": "route_verla_destel",
+ "chestId": 145
+ },
+ {
+ "name": "Route to Mir Tower: chest behind second sacred tree",
+ "type": "chest",
+ "nodeId": "mir_tower_sector_tree_coast",
+ "chestId": 146
+ },
+ {
+ "name": "Route to Destel: hidden chest in map right before Destel",
+ "type": "chest",
+ "nodeId": "route_verla_destel",
+ "hints": [
+ "in a well-hidden chest"
+ ],
+ "chestId": 147
+ },
+ {
+ "name": "Mountainous Area: chest near teleport tree",
+ "type": "chest",
+ "nodeId": "mountainous_area",
+ "chestId": 148
+ },
+ {
+ "name": "Mountainous Area: chest on right side of map before the bridge",
+ "type": "chest",
+ "nodeId": "mountainous_area",
+ "chestId": 149
+ },
+ {
+ "name": "Mountainous Area: hidden chest in L-shaped path",
+ "type": "chest",
+ "nodeId": "mountainous_area",
+ "hints": [
+ "in a well-hidden chest"
+ ],
+ "chestId": 150
+ },
+ {
+ "name": "Mountainous Area: hidden chest in uppermost path",
+ "type": "chest",
+ "nodeId": "mountainous_area",
+ "hints": [
+ "in a well-hidden chest"
+ ],
+ "chestId": 151
+ },
+ {
+ "name": "Mountainous Area: isolated chest on cliff in bridge map",
+ "type": "chest",
+ "nodeId": "mountainous_area",
+ "chestId": 152
+ },
+ {
+ "name": "Mountainous Area: left chest on wall in bridge map",
+ "type": "chest",
+ "nodeId": "mountainous_area",
+ "chestId": 153
+ },
+ {
+ "name": "Mountainous Area: right chest on wall in bridge map",
+ "type": "chest",
+ "nodeId": "mountainous_area",
+ "chestId": 154
+ },
+ {
+ "name": "Mountainous Area: right chest in map before Zak arena",
+ "type": "chest",
+ "nodeId": "mountainous_area",
+ "chestId": 155
+ },
+ {
+ "name": "Mountainous Area: left chest in map before Zak arena",
+ "type": "chest",
+ "nodeId": "mountainous_area",
+ "chestId": 156
+ },
+ {
+ "name": "Route after Destel: chest on tiny cliff",
+ "type": "chest",
+ "nodeId": "route_after_destel",
+ "chestId": 157
+ },
+ {
+ "name": "Route after Destel: hidden chest in map after seeing Duke raft",
+ "type": "chest",
+ "nodeId": "route_after_destel",
+ "hints": [
+ "in a well-hidden chest"
+ ],
+ "chestId": 158
+ },
+ {
+ "name": "Route after Destel: visible chest in map after seeing Duke raft",
+ "type": "chest",
+ "nodeId": "route_after_destel",
+ "chestId": 159
+ },
+ {
+ "name": "Mountainous Area: chest hidden under rocky arch",
+ "type": "chest",
+ "nodeId": "mountainous_area",
+ "hints": [
+ "in a well-hidden chest"
+ ],
+ "chestId": 160
+ },
+ {
+ "name": "Route to Lake Shrine: chest on long cliff in crossroads map",
+ "type": "chest",
+ "nodeId": "route_lake_shrine",
+ "chestId": 161
+ },
+ {
+ "name": "Route to Lake Shrine: chest on middle cliff in crossroads map (reached from Mountainous Area)",
+ "type": "chest",
+ "nodeId": "route_lake_shrine_cliff",
+ "chestId": 162
+ },
+ {
+ "name": "Mountainous Area: chest in map in front of bridge statue",
+ "type": "chest",
+ "nodeId": "mountainous_area",
+ "chestId": 163
+ },
+ {
+ "name": "Route to Lake Shrine: right chest in volcano",
+ "type": "chest",
+ "nodeId": "route_lake_shrine",
+ "chestId": 164
+ },
+ {
+ "name": "Route to Lake Shrine: left chest in volcano",
+ "type": "chest",
+ "nodeId": "route_lake_shrine",
+ "chestId": 165
+ },
+ {
+ "name": "Mountainous Area Cave: chest in small hidden room",
+ "type": "chest",
+ "nodeId": "mountainous_area",
+ "hints": [
+ "in a small cave",
+ "in a well-hidden chest",
+ "in a cave in the mountains"
+ ],
+ "chestId": 166
+ },
+ {
+ "name": "Mountainous Area Cave: chest in small visible room",
+ "type": "chest",
+ "nodeId": "mountainous_area",
+ "hints": [
+ "in a small cave",
+ "in a cave in the mountains"
+ ],
+ "chestId": 167
+ },
+ {
+ "name": "Greenmaze: chest on path to Cutter",
+ "type": "chest",
+ "nodeId": "greenmaze_cutter",
+ "chestId": 168
+ },
+ {
+ "name": "Greenmaze: chest on cliff near the swamp",
+ "type": "chest",
+ "nodeId": "greenmaze_pre_whistle",
+ "chestId": 169
+ },
+ {
+ "name": "Greenmaze: chest between Sunstone and Massan shortcut",
+ "type": "chest",
+ "nodeId": "greenmaze_post_whistle",
+ "chestId": 170
+ },
+ {
+ "name": "Greenmaze: chest in mages room",
+ "type": "chest",
+ "nodeId": "greenmaze_pre_whistle",
+ "chestId": 171
+ },
+ {
+ "name": "Greenmaze: left chest in elbow cave",
+ "type": "chest",
+ "nodeId": "greenmaze_pre_whistle",
+ "chestId": 172
+ },
+ {
+ "name": "Greenmaze: right chest in elbow cave",
+ "type": "chest",
+ "nodeId": "greenmaze_pre_whistle",
+ "chestId": 173
+ },
+ {
+ "name": "Greenmaze: chest in waterfall cave",
+ "type": "chest",
+ "nodeId": "greenmaze_pre_whistle",
+ "hints": [
+ "close to a waterfall"
+ ],
+ "chestId": 174
+ },
+ {
+ "name": "Greenmaze: left chest in hidden room behind waterfall",
+ "type": "chest",
+ "nodeId": "greenmaze_pre_whistle",
+ "hints": [
+ "close to a waterfall"
+ ],
+ "chestId": 175
+ },
+ {
+ "name": "Greenmaze: right chest in hidden room behind waterfall",
+ "type": "chest",
+ "nodeId": "greenmaze_pre_whistle",
+ "hints": [
+ "close to a waterfall"
+ ],
+ "chestId": 176
+ },
+ {
+ "name": "Massan: chest triggered by dog statue",
+ "type": "chest",
+ "nodeId": "massan",
+ "hints": [
+ "in a well-hidden chest"
+ ],
+ "chestId": 177
+ },
+ {
+ "name": "Massan: chest in house nearest to elder house",
+ "type": "chest",
+ "nodeId": "massan",
+ "chestId": 178
+ },
+ {
+ "name": "Massan: chest in middle house",
+ "type": "chest",
+ "nodeId": "massan",
+ "chestId": 179
+ },
+ {
+ "name": "Massan: chest in house farthest from elder house",
+ "type": "chest",
+ "nodeId": "massan",
+ "chestId": 180
+ },
+ {
+ "name": "Gumi: chest on top of bed in house",
+ "type": "chest",
+ "nodeId": "gumi",
+ "chestId": 181
+ },
+ {
+ "name": "Gumi: chest in elder house after saving Fara",
+ "type": "chest",
+ "nodeId": "gumi_after_swamp_shrine",
+ "chestId": 182
+ },
+ {
+ "name": "Ryuma: chest in mayor's house",
+ "type": "chest",
+ "nodeId": "ryuma",
+ "chestId": 183
+ },
+ {
+ "name": "Ryuma: chest in repaired lighthouse",
+ "type": "chest",
+ "nodeId": "ryuma_lighthouse_repaired",
+ "chestId": 184
+ },
+ {
+ "name": "Crypt: chest in main room",
+ "type": "chest",
+ "nodeId": "crypt",
+ "chestId": 185
+ },
+ {
+ "name": "Crypt: reward chest",
+ "type": "chest",
+ "nodeId": "crypt",
+ "chestId": 186
+ },
+ {
+ "name": "Mercator: hidden casino chest",
+ "type": "chest",
+ "nodeId": "mercator_casino",
+ "hints": [
+ "hidden in the depths of Mercator",
+ "in a well-hidden chest"
+ ],
+ "chestId": 191
+ },
+ {
+ "name": "Mercator: chest in Greenpea's house",
+ "type": "chest",
+ "nodeId": "mercator",
+ "chestId": 192
+ },
+ {
+ "name": "Mercator: chest in grandma's house (pot shelving trial)",
+ "type": "chest",
+ "nodeId": "mercator",
+ "chestId": 193
+ },
+ {
+ "name": "Verla: chest in well after beating Marley",
+ "type": "chest",
+ "nodeId": "verla_after_mines",
+ "hints": [
+ "in a well-hidden chest"
+ ],
+ "chestId": 194
+ },
+ {
+ "name": "Destel: chest in inn next to innkeeper",
+ "type": "chest",
+ "nodeId": "destel",
+ "chestId": 196
+ },
+ {
+ "name": "Mir Tower: timed jump trial chest",
+ "type": "chest",
+ "nodeId": "mir_tower_pre_garlic",
+ "chestId": 197
+ },
+ {
+ "name": "Mir Tower: chest after mimic room",
+ "type": "chest",
+ "nodeId": "mir_tower_pre_garlic",
+ "chestId": 198
+ },
+ {
+ "name": "Mir Tower: mimic room chest #1",
+ "type": "chest",
+ "nodeId": "mir_tower_pre_garlic",
+ "chestId": 199
+ },
+ {
+ "name": "Mir Tower: mimic room chest #2",
+ "type": "chest",
+ "nodeId": "mir_tower_pre_garlic",
+ "chestId": 200
+ },
+ {
+ "name": "Mir Tower: mimic room chest #3",
+ "type": "chest",
+ "nodeId": "mir_tower_pre_garlic",
+ "chestId": 201
+ },
+ {
+ "name": "Mir Tower: mimic room chest #4",
+ "type": "chest",
+ "nodeId": "mir_tower_pre_garlic",
+ "chestId": 202
+ },
+ {
+ "name": "Mir Tower: chest in mushroom pit room",
+ "type": "chest",
+ "nodeId": "mir_tower_pre_garlic",
+ "chestId": 203
+ },
+ {
+ "name": "Mir Tower: chest in room next to mummy switch room",
+ "type": "chest",
+ "nodeId": "mir_tower_pre_garlic",
+ "chestId": 204
+ },
+ {
+ "name": "Mir Tower: chest in library accessible from teleporter maze",
+ "type": "chest",
+ "nodeId": "mir_tower_post_garlic",
+ "chestId": 205
+ },
+ {
+ "name": "Mir Tower: hidden chest in room before library",
+ "type": "chest",
+ "nodeId": "mir_tower_post_garlic",
+ "hints": [
+ "in a well-hidden chest"
+ ],
+ "chestId": 206
+ },
+ {
+ "name": "Mir Tower: chest in falling spikeballs room",
+ "type": "chest",
+ "nodeId": "mir_tower_post_garlic",
+ "chestId": 207
+ },
+ {
+ "name": "Mir Tower: chest in timed challenge room",
+ "type": "chest",
+ "nodeId": "mir_tower_post_garlic",
+ "chestId": 208
+ },
+ {
+ "name": "Mir Tower: chest in room where Miro closes the door",
+ "type": "chest",
+ "nodeId": "mir_tower_post_garlic",
+ "chestId": 209
+ },
+ {
+ "name": "Mir Tower: chest after room where Miro closes the door",
+ "type": "chest",
+ "nodeId": "mir_tower_post_garlic",
+ "chestId": 210
+ },
+ {
+ "name": "Mir Tower: reward chest",
+ "type": "chest",
+ "nodeId": "mir_tower_post_garlic",
+ "hints": [
+ "kept by a threatening guardian"
+ ],
+ "chestId": 211
+ },
+ {
+ "name": "Mir Tower: right chest in reward room",
+ "type": "chest",
+ "nodeId": "mir_tower_post_garlic",
+ "hints": [
+ "kept by a threatening guardian"
+ ],
+ "chestId": 212
+ },
+ {
+ "name": "Mir Tower: left chest in reward room",
+ "type": "chest",
+ "nodeId": "mir_tower_post_garlic",
+ "hints": [
+ "kept by a threatening guardian"
+ ],
+ "chestId": 213
+ },
+ {
+ "name": "Mir Tower: chest behind wall accessible after beating Mir",
+ "type": "chest",
+ "nodeId": "mir_tower_post_garlic",
+ "hints": [
+ "kept by a threatening guardian"
+ ],
+ "chestId": 214
+ },
+ {
+ "name": "Witch Helga's Hut: end chest",
+ "type": "chest",
+ "nodeId": "helga_hut",
+ "chestId": 215
+ },
+ {
+ "name": "Massan Cave: right chest",
+ "type": "chest",
+ "nodeId": "massan_cave",
+ "chestId": 216
+ },
+ {
+ "name": "Massan Cave: left chest",
+ "type": "chest",
+ "nodeId": "massan_cave",
+ "chestId": 217
+ },
+ {
+ "name": "Tibor: reward chest after boss",
+ "type": "chest",
+ "nodeId": "tibor",
+ "chestId": 218
+ },
+ {
+ "name": "Tibor: chest in spike balls room",
+ "type": "chest",
+ "nodeId": "tibor",
+ "chestId": 219
+ },
+ {
+ "name": "Tibor: left chest on 2 chest group",
+ "type": "chest",
+ "nodeId": "tibor",
+ "chestId": 220
+ },
+ {
+ "name": "Tibor: right chest on 2 chest group",
+ "type": "chest",
+ "nodeId": "tibor",
+ "chestId": 221
+ },
+ {
+ "name": "Gumi: item on furniture in elder's house",
+ "type": "ground",
+ "nodeId": "gumi",
+ "entity": {"mapId": 605, "entityId": 2},
+ "groundItemId": 1
+ },
+ {
+ "name": "Greenmaze: item behind trees requiring Cutter",
+ "type": "ground",
+ "nodeId": "greenmaze_post_whistle",
+ "entity": {"mapId": 564, "entityId": 0},
+ "groundItemId": 2
+ },
+ {
+ "name": "Verla Mines: item in the corner of lava filled room",
+ "type": "ground",
+ "nodeId": "verla_mines",
+ "hints": [
+ "in a very hot place"
+ ],
+ "entity": {"mapId": 263, "entityId": 7},
+ "groundItemId": 3
+ },
+ {
+ "name": "Lake Shrine (-3F): item on ground at the SE exit of the golden golems roundabout",
+ "type": "ground",
+ "nodeId": "lake_shrine",
+ "entity": {"mapId": 333, "entityId": 0},
+ "groundItemId": 4
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): item on ground behind waterfall after beating Spinner",
+ "type": "ground",
+ "nodeId": "king_nole_labyrinth_raft",
+ "hints": [
+ "kept by a threatening guardian"
+ ],
+ "entity": {"mapId": 411, "entityId": 0},
+ "groundItemId": 5
+ },
+ {
+ "name": "Destel Well: item on platform revealed after beating Quake",
+ "type": "ground",
+ "nodeId": "destel_well",
+ "hints": [
+ "kept by a threatening guardian"
+ ],
+ "entity": {"mapId": 288, "entityId": 0},
+ "groundItemId": 6
+ },
+ {
+ "name": "King Nole's Labyrinth (-1F): item on ground in ninjas room",
+ "type": "ground",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "entity": {"mapId": 374, "entityId": 3},
+ "groundItemId": 7
+ },
+ {
+ "name": "Massan Cave: item on ground in treasure room",
+ "type": "ground",
+ "nodeId": "massan_cave",
+ "entity": {"mapId": 807, "entityId": 4},
+ "groundItemId": 8
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): item on floating hands",
+ "type": "ground",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "entity": {"mapId": 418, "entityId": 0},
+ "groundItemId": 9
+ },
+ {
+ "name": "Lake Shrine (-3F): isolated item on ground requiring raised platform to reach",
+ "type": "ground",
+ "nodeId": "lake_shrine",
+ "entities": [
+ {"mapId": 344, "entityId": 0},
+ {"mapId": 345, "entityId": 0}
+ ],
+ "groundItemId": 10
+ },
+ {
+ "name": "King Nole's Labyrinth (-2F): item on ground after falling from exterior room",
+ "type": "ground",
+ "nodeId": "king_nole_labyrinth_fall_from_exterior",
+ "entity": {"mapId": 363, "entityId": 0},
+ "groundItemId": 11
+ },
+ {
+ "name": "Route after Destel: item on ground on the cliff",
+ "type": "ground",
+ "nodeId": "route_after_destel",
+ "entity": {"mapId": 483, "entityId": 0},
+ "groundItemId": 12
+ },
+ {
+ "name": "Mountainous Area cave: item on ground behind hidden path",
+ "type": "ground",
+ "nodeId": "mountainous_area",
+ "hints": [
+ "in a small cave",
+ "in a cave in the mountains"
+ ],
+ "entity": {"mapId": 553, "entityId": 0},
+ "groundItemId": 13
+ },
+ {
+ "name": "Witch Helga's Hut: item on furniture",
+ "type": "ground",
+ "nodeId": "helga_hut",
+ "entity": {"mapId": 479, "entityId": 1},
+ "groundItemId": 14
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): item on ground climbing back from Firedemon",
+ "type": "ground",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "hints": [
+ "in a very hot place"
+ ],
+ "entity": {"mapId": 399, "entityId": 0},
+ "groundItemId": 15
+ },
+ {
+ "name": "Mercator: falling item in castle court",
+ "type": "ground",
+ "nodeId": "mercator",
+ "entity": {"mapId": 32, "entityId": 2},
+ "groundItemId": 16
+ },
+ {
+ "name": "Lake Shrine (-2F): north item on ground in quadruple items room",
+ "type": "ground",
+ "nodeId": "lake_shrine",
+ "entity": {"mapId": 318, "entityId": 0},
+ "groundItemId": 17
+ },
+ {
+ "name": "Lake Shrine (-2F): south item on ground in quadruple items room",
+ "type": "ground",
+ "nodeId": "lake_shrine",
+ "entity": {"mapId": 318, "entityId": 1},
+ "groundItemId": 18
+ },
+ {
+ "name": "Lake Shrine (-2F): west item on ground in quadruple items room",
+ "type": "ground",
+ "nodeId": "lake_shrine",
+ "entity": {"mapId": 318, "entityId": 2},
+ "groundItemId": 19
+ },
+ {
+ "name": "Lake Shrine (-2F): east item on ground in quadruple items room",
+ "type": "ground",
+ "nodeId": "lake_shrine",
+ "entity": {"mapId": 318, "entityId": 3},
+ "groundItemId": 20
+ },
+ {
+ "name": "Twinkle Village: first item on ground",
+ "type": "ground",
+ "nodeId": "twinkle_village",
+ "entity": {"mapId": 462, "entityId": 5},
+ "groundItemId": 21
+ },
+ {
+ "name": "Twinkle Village: second item on ground",
+ "type": "ground",
+ "nodeId": "twinkle_village",
+ "entity": {"mapId": 462, "entityId": 4},
+ "groundItemId": 22
+ },
+ {
+ "name": "Twinkle Village: third item on ground",
+ "type": "ground",
+ "nodeId": "twinkle_village",
+ "entity": {"mapId": 462, "entityId": 3},
+ "groundItemId": 23
+ },
+ {
+ "name": "Mir Tower: Priest room item #1",
+ "type": "ground",
+ "nodeId": "mir_tower_post_garlic",
+ "entity": {"mapId": 775, "entityId": 7},
+ "groundItemId": 24
+ },
+ {
+ "name": "Mir Tower: Priest room item #2",
+ "type": "ground",
+ "nodeId": "mir_tower_post_garlic",
+ "entity": {"mapId": 775, "entityId": 6},
+ "groundItemId": 25
+ },
+ {
+ "name": "Mir Tower: Priest room item #3",
+ "type": "ground",
+ "nodeId": "mir_tower_post_garlic",
+ "entity": {"mapId": 775, "entityId": 1},
+ "groundItemId": 26
+ },
+ {
+ "name": "King Nole's Labyrinth (-2F): Left item dropped by sacred tree",
+ "type": "ground",
+ "nodeId": "king_nole_labyrinth_sacred_tree",
+ "entity": {"mapId": 415, "entityId": 2},
+ "groundItemId": 27
+ },
+ {
+ "name": "King Nole's Labyrinth (-2F): Right item dropped by sacred tree",
+ "type": "ground",
+ "nodeId": "king_nole_labyrinth_sacred_tree",
+ "entity": {"mapId": 415, "entityId": 1},
+ "groundItemId": 28
+ },
+ {
+ "name": "King Nole's Labyrinth (-3F): First item on ground before Firedemon",
+ "type": "ground",
+ "nodeId": "king_nole_labyrinth_post_door",
+ "hints": [
+ "in a very hot place"
+ ],
+ "entity": {"mapId": 400, "entityId": 0},
+ "groundItemId": 29
+ },
+ {
+ "name": "Massan: Shop item #1",
+ "type": "shop",
+ "nodeId": "massan",
+ "entity": {"mapId": 596, "entityId": 1},
+ "shopItemId": 1
+ },
+ {
+ "name": "Massan: Shop item #2",
+ "type": "shop",
+ "nodeId": "massan",
+ "entity": {"mapId": 596, "entityId": 2},
+ "shopItemId": 2
+ },
+ {
+ "name": "Massan: Shop item #3",
+ "type": "shop",
+ "nodeId": "massan",
+ "entity": {"mapId": 596, "entityId": 3},
+ "shopItemId": 3
+ },
+ {
+ "name": "Gumi: Inn item #1",
+ "type": "shop",
+ "nodeId": "gumi",
+ "entity": {"mapId": 608, "entityId": 4},
+ "shopItemId": 4
+ },
+ {
+ "name": "Gumi: Inn item #2",
+ "type": "shop",
+ "nodeId": "gumi",
+ "entity": {"mapId": 608, "entityId": 2},
+ "shopItemId": 5
+ },
+ {
+ "name": "Ryuma: Shop item #1",
+ "type": "shop",
+ "nodeId": "ryuma_after_thieves_hideout",
+ "entity": {"mapId": 615, "entityId": 2},
+ "shopItemId": 6
+ },
+ {
+ "name": "Ryuma: Shop item #2",
+ "type": "shop",
+ "nodeId": "ryuma_after_thieves_hideout",
+ "entity": {"mapId": 615, "entityId": 3},
+ "shopItemId": 7
+ },
+ {
+ "name": "Ryuma: Shop item #3",
+ "type": "shop",
+ "nodeId": "ryuma_after_thieves_hideout",
+ "entity": {"mapId": 615, "entityId": 4},
+ "shopItemId": 8
+ },
+ {
+ "name": "Ryuma: Shop item #4",
+ "type": "shop",
+ "nodeId": "ryuma_after_thieves_hideout",
+ "entity": {"mapId": 615, "entityId": 5},
+ "shopItemId": 9
+ },
+ {
+ "name": "Ryuma: Shop item #5",
+ "type": "shop",
+ "nodeId": "ryuma_after_thieves_hideout",
+ "entity": {"mapId": 615, "entityId": 6},
+ "shopItemId": 10
+ },
+ {
+ "name": "Ryuma: Inn item",
+ "type": "shop",
+ "nodeId": "ryuma",
+ "entity": {"mapId": 624, "entityId": 3},
+ "shopItemId": 11
+ },
+ {
+ "name": "Mercator: Shop item #1",
+ "type": "shop",
+ "nodeId": "mercator",
+ "entity": {"mapId": 679, "entityId": 1},
+ "shopItemId": 12
+ },
+ {
+ "name": "Mercator: Shop item #2",
+ "type": "shop",
+ "nodeId": "mercator",
+ "entity": {"mapId": 679, "entityId": 2},
+ "shopItemId": 13
+ },
+ {
+ "name": "Mercator: Shop item #3",
+ "type": "shop",
+ "nodeId": "mercator",
+ "entity": {"mapId": 679, "entityId": 3},
+ "shopItemId": 14
+ },
+ {
+ "name": "Mercator: Shop item #4",
+ "type": "shop",
+ "nodeId": "mercator",
+ "entity": {"mapId": 679, "entityId": 4},
+ "shopItemId": 15
+ },
+ {
+ "name": "Mercator: Shop item #5",
+ "type": "shop",
+ "nodeId": "mercator",
+ "entity": {"mapId": 679, "entityId": 5},
+ "shopItemId": 16
+ },
+ {
+ "name": "Mercator: Shop item #6",
+ "type": "shop",
+ "nodeId": "mercator",
+ "entity": {"mapId": 679, "entityId": 6},
+ "shopItemId": 17
+ },
+ {
+ "name": "Mercator: Special shop item #1",
+ "type": "shop",
+ "nodeId": "mercator_special_shop",
+ "entity": {"mapId": 696, "entityId": 1},
+ "shopItemId": 18
+ },
+ {
+ "name": "Mercator: Special shop item #2",
+ "type": "shop",
+ "nodeId": "mercator_special_shop",
+ "entity": {"mapId": 696, "entityId": 2},
+ "shopItemId": 19
+ },
+ {
+ "name": "Mercator: Special shop item #3",
+ "type": "shop",
+ "nodeId": "mercator_special_shop",
+ "entity": {"mapId": 696, "entityId": 3},
+ "shopItemId": 20
+ },
+ {
+ "name": "Mercator: Special shop item #4",
+ "type": "shop",
+ "nodeId": "mercator_special_shop",
+ "entity": {"mapId": 696, "entityId": 4},
+ "shopItemId": 21
+ },
+ {
+ "name": "Mercator: Docks shop item #1",
+ "type": "shop",
+ "nodeId": "mercator_repaired_docks",
+ "entities": [
+ {"mapId": 644, "entityId": 3},
+ {"mapId": 643, "entityId": 9}
+ ],
+ "shopItemId": 22
+ },
+ {
+ "name": "Mercator: Docks shop item #2",
+ "type": "shop",
+ "nodeId": "mercator_repaired_docks",
+ "entities": [
+ {"mapId": 644, "entityId": 4},
+ {"mapId": 643, "entityId": 10}
+ ],
+ "shopItemId": 23
+ },
+ {
+ "name": "Mercator: Docks shop item #3",
+ "type": "shop",
+ "nodeId": "mercator_repaired_docks",
+ "entities": [
+ {"mapId": 644, "entityId": 5},
+ {"mapId": 643, "entityId": 11}
+ ],
+ "shopItemId": 24
+ },
+ {
+ "name": "Verla: Shop item #1",
+ "type": "shop",
+ "nodeId": "verla",
+ "entities": [
+ {"mapId": 719, "entityId": 0},
+ {"mapId": 720, "entityId": 1}
+ ],
+ "shopItemId": 25
+ },
+ {
+ "name": "Verla: Shop item #2",
+ "type": "shop",
+ "nodeId": "verla",
+ "entities": [
+ {"mapId": 719, "entityId": 1},
+ {"mapId": 720, "entityId": 2}
+ ],
+ "shopItemId": 26
+ },
+ {
+ "name": "Verla: Shop item #3",
+ "type": "shop",
+ "nodeId": "verla",
+ "entities": [
+ {"mapId": 719, "entityId": 2},
+ {"mapId": 720, "entityId": 3}
+ ],
+ "shopItemId": 27
+ },
+ {
+ "name": "Verla: Shop item #4",
+ "type": "shop",
+ "nodeId": "verla",
+ "entities": [
+ {"mapId": 719, "entityId": 4},
+ {"mapId": 720, "entityId": 4}
+ ],
+ "shopItemId": 28
+ },
+ {
+ "name": "Verla: Shop item #5 (extra item after saving town)",
+ "type": "shop",
+ "nodeId": "verla_after_mines",
+ "entity": {"mapId": 720, "entityId": 5},
+ "shopItemId": 29
+ },
+ {
+ "name": "Route from Verla to Destel: Kelketo shop item #1",
+ "type": "shop",
+ "nodeId": "route_verla_destel",
+ "entity": {"mapId": 517, "entityId": 1},
+ "shopItemId": 30
+ },
+ {
+ "name": "Route from Verla to Destel: Kelketo shop item #2",
+ "type": "shop",
+ "nodeId": "route_verla_destel",
+ "entity": {"mapId": 517, "entityId": 2},
+ "shopItemId": 31
+ },
+ {
+ "name": "Route from Verla to Destel: Kelketo shop item #3",
+ "type": "shop",
+ "nodeId": "route_verla_destel",
+ "entity": {"mapId": 517, "entityId": 3},
+ "shopItemId": 32
+ },
+ {
+ "name": "Route from Verla to Destel: Kelketo shop item #4",
+ "type": "shop",
+ "nodeId": "route_verla_destel",
+ "entity": {"mapId": 517, "entityId": 4},
+ "shopItemId": 33
+ },
+ {
+ "name": "Route from Verla to Destel: Kelketo shop item #5",
+ "type": "shop",
+ "nodeId": "route_verla_destel",
+ "entity": {"mapId": 517, "entityId": 5},
+ "shopItemId": 34
+ },
+ {
+ "name": "Destel: Inn item",
+ "type": "shop",
+ "nodeId": "destel",
+ "entity": {"mapId": 729, "entityId": 2},
+ "shopItemId": 35
+ },
+ {
+ "name": "Destel: Shop item #1",
+ "type": "shop",
+ "nodeId": "destel",
+ "entity": {"mapId": 733, "entityId": 1},
+ "shopItemId": 36
+ },
+ {
+ "name": "Destel: Shop item #2",
+ "type": "shop",
+ "nodeId": "destel",
+ "entity": {"mapId": 733, "entityId": 2},
+ "shopItemId": 37
+ },
+ {
+ "name": "Destel: Shop item #3",
+ "type": "shop",
+ "nodeId": "destel",
+ "entity": {"mapId": 733, "entityId": 3},
+ "shopItemId": 38
+ },
+ {
+ "name": "Destel: Shop item #4",
+ "type": "shop",
+ "nodeId": "destel",
+ "entity": {"mapId": 733, "entityId": 4},
+ "shopItemId": 39
+ },
+ {
+ "name": "Destel: Shop item #5",
+ "type": "shop",
+ "nodeId": "destel",
+ "entity": {"mapId": 733, "entityId": 5},
+ "shopItemId": 40
+ },
+ {
+ "name": "Route to Lake Shrine: Greedly's shop item #1",
+ "type": "shop",
+ "nodeId": "route_lake_shrine",
+ "entity": {"mapId": 526, "entityId": 0},
+ "shopItemId": 41
+ },
+ {
+ "name": "Route to Lake Shrine: Greedly's shop item #2",
+ "type": "shop",
+ "nodeId": "route_lake_shrine",
+ "entity": {"mapId": 526, "entityId": 2},
+ "shopItemId": 42
+ },
+ {
+ "name": "Route to Lake Shrine: Greedly's shop item #3",
+ "type": "shop",
+ "nodeId": "route_lake_shrine",
+ "entity": {"mapId": 526, "entityId": 3},
+ "shopItemId": 43
+ },
+ {
+ "name": "Route to Lake Shrine: Greedly's shop item #4",
+ "type": "shop",
+ "nodeId": "route_lake_shrine",
+ "entity": {"mapId": 526, "entityId": 4},
+ "shopItemId": 44
+ },
+ {
+ "name": "Kazalt: Shop item #1",
+ "type": "shop",
+ "nodeId": "kazalt",
+ "entity": {"mapId": 747, "entityId": 0},
+ "shopItemId": 45
+ },
+ {
+ "name": "Kazalt: Shop item #2",
+ "type": "shop",
+ "nodeId": "kazalt",
+ "entity": {"mapId": 747, "entityId": 2},
+ "shopItemId": 46
+ },
+ {
+ "name": "Kazalt: Shop item #3",
+ "type": "shop",
+ "nodeId": "kazalt",
+ "entity": {"mapId": 747, "entityId": 3},
+ "shopItemId": 47
+ },
+ {
+ "name": "Kazalt: Shop item #4",
+ "type": "shop",
+ "nodeId": "kazalt",
+ "entity": {"mapId": 747, "entityId": 4},
+ "shopItemId": 48
+ },
+ {
+ "name": "Kazalt: Shop item #5",
+ "type": "shop",
+ "nodeId": "kazalt",
+ "entity": {"mapId": 747, "entityId": 5},
+ "shopItemId": 49
+ },
+ {
+ "name": "Massan: Elder reward after freeing Fara in Swamp Shrine",
+ "type": "reward",
+ "nodeId": "massan_after_swamp_shrine",
+ "address": 162337,
+ "flag": {"byte": "0x1004", "bit": 2},
+ "rewardId": 0
+ },
+ {
+ "name": "Lake Shrine: Mir reward after beating Duke",
+ "type": "reward",
+ "nodeId": "lake_shrine",
+ "address": 166463,
+ "flag": {"byte": "0x1003", "bit": 0},
+ "rewardId": 1
+ },
+ {
+ "name": "Greenmaze: Cutter reward for saving Einstein",
+ "type": "reward",
+ "nodeId": "greenmaze_cutter",
+ "address": 166021,
+ "flag": {"byte": "0x1024", "bit": 4},
+ "rewardId": 2
+ },
+ {
+ "name": "Mountainous Area: Zak reward after fighting",
+ "type": "reward",
+ "nodeId": "mountainous_area",
+ "hints": [
+ "kept by a threatening guardian"
+ ],
+ "address": 166515,
+ "flag": {"byte": "0x1027", "bit": 0},
+ "rewardId": 3
+ },
+ {
+ "name": "Route between Gumi and Ryuma: Swordsman Kado reward",
+ "type": "reward",
+ "nodeId": "route_gumi_ryuma",
+ "address": 166219,
+ "flag": {"byte": "0x101B", "bit": 7},
+ "rewardId": 4
+ },
+ {
+ "name": "Greenmaze: dwarf hidden in the trees",
+ "type": "reward",
+ "nodeId": "greenmaze_pre_whistle",
+ "address": 166111,
+ "flag": {"byte": "0x1022", "bit": 7},
+ "rewardId": 5
+ },
+ {
+ "name": "Mercator: Arthur reward (in castle throne room)",
+ "type": "reward",
+ "nodeId": "mercator",
+ "address": 164191,
+ "flag": {"byte": "0x101B", "bit": 6},
+ "rewardId": 6
+ },
+ {
+ "name": "Mercator: Fahl's dojo challenge reward",
+ "type": "reward",
+ "nodeId": "mercator",
+ "address": 165029,
+ "flag": {"byte": "0x101C", "bit": 4},
+ "rewardId": 7
+ },
+ {
+ "name": "Ryuma: Mayor's first reward",
+ "type": "reward",
+ "nodeId": "ryuma_after_thieves_hideout",
+ "address": 164731,
+ "flag": {"byte": "0x1004", "bit": 3},
+ "rewardId": 8
+ },
+ {
+ "name": "Ryuma: Mayor's second reward",
+ "type": "reward",
+ "nodeId": "ryuma_after_thieves_hideout",
+ "address": 164735,
+ "flag": {"byte": "0x1004", "bit": 3},
+ "rewardId": 9
+ }
+]
diff --git a/worlds/landstalker/data/world_node.py b/worlds/landstalker/data/world_node.py
new file mode 100644
index 000000000000..f786f9613fba
--- /dev/null
+++ b/worlds/landstalker/data/world_node.py
@@ -0,0 +1,411 @@
+WORLD_NODES_JSON = {
+ "massan": {
+ "name": "Massan",
+ "hints": [
+ "in a village",
+ "in a region inhabited by bears",
+ "in the village of Massan"
+ ]
+ },
+ "massan_cave": {
+ "name": "Massan Cave",
+ "hints": [
+ "in a large cave",
+ "in a region inhabited by bears",
+ "in Massan cave"
+ ]
+ },
+ "route_massan_gumi": {
+ "name": "Route between Massan and Gumi",
+ "hints": [
+ "on a route",
+ "in a region inhabited by bears",
+ "between Massan and Gumi"
+ ]
+ },
+ "waterfall_shrine": {
+ "name": "Waterfall Shrine",
+ "hints": [
+ "in a shrine",
+ "close to a waterfall",
+ "in a region inhabited by bears",
+ "in Waterfall Shrine"
+ ]
+ },
+ "swamp_shrine": {
+ "name": "Swamp Shrine",
+ "hints": [
+ "in a shrine",
+ "near a swamp",
+ "in a region inhabited by bears",
+ "in Swamp Shrine"
+ ]
+ },
+ "massan_after_swamp_shrine": {
+ "name": "Massan (after Swamp Shrine)",
+ "hints": [
+ "in a village",
+ "in a region inhabited by bears",
+ "in the village of Massan"
+ ]
+ },
+ "gumi_after_swamp_shrine": {
+ "name": "Gumi (after Swamp Shrine)",
+ "hints": [
+ "in a village",
+ "in a region inhabited by bears",
+ "in the village of Gumi"
+ ]
+ },
+ "gumi": {
+ "name": "Gumi",
+ "hints": [
+ "in a village",
+ "in a region inhabited by bears",
+ "in the village of Gumi"
+ ]
+ },
+ "route_gumi_ryuma": {
+ "name": "Route from Gumi to Ryuma",
+ "hints": [
+ "on a route",
+ "in a region inhabited by bears",
+ "between Gumi and Ryuma"
+ ]
+ },
+ "tibor": {
+ "name": "Tibor",
+ "hints": [
+ "among the trees",
+ "inside the elder tree called Tibor"
+ ]
+ },
+ "ryuma": {
+ "name": "Ryuma",
+ "hints": [
+ "in a town",
+ "in the town of Ryuma"
+ ]
+ },
+ "ryuma_after_thieves_hideout": {
+ "name": "Ryuma (after Thieves Hideout)",
+ "hints": [
+ "in a town",
+ "in the town of Ryuma"
+ ]
+ },
+ "ryuma_lighthouse_repaired": {
+ "name": "Ryuma (repaired lighthouse)",
+ "hints": [
+ "in a town",
+ "in the town of Ryuma"
+ ]
+ },
+ "thieves_hideout_pre_key": {
+ "name": "Thieves Hideout (before keydoor)",
+ "hints": [
+ "close to a waterfall",
+ "in a large cave",
+ "in the Thieves' Hideout"
+ ]
+ },
+ "thieves_hideout_post_key": {
+ "name": "Thieves Hideout (after keydoor)",
+ "hints": [
+ "close to a waterfall",
+ "in a large cave",
+ "in the Thieves' Hideout"
+ ]
+ },
+ "helga_hut": {
+ "name": "Witch Helga's Hut",
+ "hints": [
+ "near a swamp",
+ "in the hut of a witch called Helga"
+ ]
+ },
+ "mercator": {
+ "name": "Mercator",
+ "hints": [
+ "in a town",
+ "in the town of Mercator"
+ ]
+ },
+ "mercator_repaired_docks": {
+ "name": "Mercator (docks with repaired lighthouse)",
+ "hints": [
+ "in a town",
+ "in the town of Mercator"
+ ]
+ },
+ "mercator_casino": {
+ "name": "Mercator casino"
+ },
+ "mercator_dungeon": {
+ "name": "Mercator Dungeon"
+ },
+ "crypt": {
+ "name": "Crypt",
+ "hints": [
+ "hidden in the depths of Mercator",
+ "in Mercator crypt"
+ ]
+ },
+ "mercator_special_shop": {
+ "name": "Mercator special shop",
+ "hints": [
+ "in a town",
+ "in the town of Mercator"
+ ]
+ },
+ "mir_tower_sector": {
+ "name": "Mir Tower sector",
+ "hints": [
+ "on a route",
+ "near Mir Tower"
+ ]
+ },
+ "mir_tower_sector_tree_ledge": {
+ "name": "Mir Tower sector (ledge behind sacred tree)",
+ "hints": [
+ "on a route",
+ "among the trees",
+ "near Mir Tower"
+ ]
+ },
+ "mir_tower_sector_tree_coast": {
+ "name": "Mir Tower sector (coast behind sacred tree)",
+ "hints": [
+ "on a route",
+ "among the trees",
+ "near Mir Tower"
+ ]
+ },
+ "twinkle_village": {
+ "name": "Twinkle village",
+ "hints": [
+ "in a village",
+ "in Twinkle village"
+ ]
+ },
+ "mir_tower_pre_garlic": {
+ "name": "Mir Tower (pre-garlic)",
+ "hints": [
+ "inside a tower",
+ "in Mir Tower"
+ ]
+ },
+ "mir_tower_post_garlic": {
+ "name": "Mir Tower (post-garlic)",
+ "hints": [
+ "inside a tower",
+ "in Mir Tower"
+ ]
+ },
+ "greenmaze_pre_whistle": {
+ "name": "Greenmaze (pre-whistle)",
+ "hints": [
+ "among the trees",
+ "in the infamous Greenmaze"
+ ]
+ },
+ "greenmaze_cutter": {
+ "name": "Greenmaze (Cutter hidden sector)",
+ "hints": [
+ "among the trees",
+ "in the infamous Greenmaze"
+ ]
+ },
+ "greenmaze_post_whistle": {
+ "name": "Greenmaze (post-whistle)",
+ "hints": [
+ "among the trees",
+ "in the infamous Greenmaze"
+ ]
+ },
+ "verla_shore": {
+ "name": "Verla shore",
+ "hints": [
+ "on a route",
+ "near the town of Verla"
+ ]
+ },
+ "verla_shore_cliff": {
+ "name": "Verla shore cliff (accessible from Verla Mines)",
+ "hints": [
+ "on a route",
+ "near the town of Verla"
+ ]
+ },
+ "verla": {
+ "name": "Verla",
+ "hints": [
+ "in a town",
+ "in the town of Verla"
+ ]
+ },
+ "verla_after_mines": {
+ "name": "Verla (after mines)",
+ "hints": [
+ "in a town",
+ "in the town of Verla"
+ ]
+ },
+ "verla_mines": {
+ "name": "Verla Mines",
+ "hints": [
+ "in Verla Mines"
+ ]
+ },
+ "verla_mines_behind_lava": {
+ "name": "Verla Mines (behind lava)",
+ "hints": [
+ "in Verla Mines"
+ ]
+ },
+ "route_verla_destel": {
+ "name": "Route between Verla and Destel",
+ "hints": [
+ "on a route",
+ "in Destel region",
+ "between Verla and Destel"
+ ]
+ },
+ "destel": {
+ "name": "Destel",
+ "hints": [
+ "in a village",
+ "in Destel region",
+ "in the village of Destel"
+ ]
+ },
+ "route_after_destel": {
+ "name": "Route after Destel",
+ "hints": [
+ "on a route",
+ "near a lake",
+ "in Destel region",
+ "on the route to the lake after Destel"
+ ]
+ },
+ "destel_well": {
+ "name": "Destel Well",
+ "hints": [
+ "in Destel region",
+ "in a large cave",
+ "in Destel Well"
+ ]
+ },
+ "route_lake_shrine": {
+ "name": "Route to Lake Shrine",
+ "hints": [
+ "on a route",
+ "near a lake",
+ "on the mountainous path to Lake Shrine"
+ ]
+ },
+ "route_lake_shrine_cliff": {
+ "name": "Route to Lake Shrine cliff",
+ "hints": [
+ "on a route",
+ "near a lake",
+ "on the mountainous path to Lake Shrine"
+ ]
+ },
+ "lake_shrine": {
+ "name": "Lake Shrine",
+ "hints": [
+ "in a shrine",
+ "near a lake",
+ "in Lake Shrine"
+ ]
+ },
+ "mountainous_area": {
+ "name": "Mountainous Area",
+ "hints": [
+ "in a mountainous area"
+ ]
+ },
+ "king_nole_cave": {
+ "name": "King Nole's Cave",
+ "hints": [
+ "in a large cave",
+ "in King Nole's cave"
+ ]
+ },
+ "kazalt": {
+ "name": "Kazalt",
+ "hints": [
+ "in King Nole's domain",
+ "in Kazalt"
+ ]
+ },
+ "king_nole_labyrinth_pre_door": {
+ "name": "King Nole's Labyrinth (before door)",
+ "hints": [
+ "in King Nole's domain",
+ "in King Nole's labyrinth"
+ ]
+ },
+ "king_nole_labyrinth_post_door": {
+ "name": "King Nole's Labyrinth (after door)",
+ "hints": [
+ "in King Nole's domain",
+ "in King Nole's labyrinth"
+ ]
+ },
+ "king_nole_labyrinth_exterior": {
+ "name": "King Nole's Labyrinth (exterior)",
+ "hints": [
+ "in King Nole's domain",
+ "in King Nole's labyrinth"
+ ]
+ },
+ "king_nole_labyrinth_fall_from_exterior": {
+ "name": "King Nole's Labyrinth (fall from exterior)",
+ "hints": [
+ "in King Nole's domain",
+ "in King Nole's labyrinth"
+ ]
+ },
+ "king_nole_labyrinth_raft_entrance": {
+ "name": "King Nole's Labyrinth (raft entrance)",
+ "hints": [
+ "in King Nole's domain",
+ "in King Nole's labyrinth"
+ ]
+ },
+ "king_nole_labyrinth_raft": {
+ "name": "King Nole's Labyrinth (raft)",
+ "hints": [
+ "close to a waterfall",
+ "in King Nole's domain",
+ "in King Nole's labyrinth"
+ ]
+ },
+ "king_nole_labyrinth_sacred_tree": {
+ "name": "King Nole's Labyrinth (sacred tree)",
+ "hints": [
+ "among the trees",
+ "in King Nole's domain",
+ "in King Nole's labyrinth"
+ ]
+ },
+ "king_nole_labyrinth_path_to_palace": {
+ "name": "King Nole's Labyrinth (path to palace)",
+ "hints": [
+ "in King Nole's domain",
+ "in King Nole's labyrinth"
+ ]
+ },
+ "king_nole_palace": {
+ "name": "King Nole's Palace",
+ "hints": [
+ "in King Nole's domain",
+ "in King Nole's palace"
+ ]
+ },
+ "end": {
+ "name": "The End"
+ }
+}
diff --git a/worlds/landstalker/data/world_path.py b/worlds/landstalker/data/world_path.py
new file mode 100644
index 000000000000..f7baba358a48
--- /dev/null
+++ b/worlds/landstalker/data/world_path.py
@@ -0,0 +1,446 @@
+WORLD_PATHS_JSON = [
+ {
+ "fromId": "massan",
+ "toId": "massan_cave",
+ "twoWay": True,
+ "requiredItems": [
+ "Axe Magic"
+ ]
+ },
+ {
+ "fromId": "massan",
+ "toId": "massan_after_swamp_shrine",
+ "requiredNodes": [
+ "swamp_shrine"
+ ]
+ },
+ {
+ "fromId": "massan",
+ "toId": "route_massan_gumi",
+ "twoWay": True
+ },
+ {
+ "fromId": "route_massan_gumi",
+ "toId": "waterfall_shrine",
+ "twoWay": True
+ },
+ {
+ "fromId": "route_massan_gumi",
+ "toId": "swamp_shrine",
+ "twoWay": True,
+ "weight": 2,
+ "requiredItems": [
+ "Idol Stone"
+ ]
+ },
+ {
+ "fromId": "route_massan_gumi",
+ "toId": "gumi",
+ "twoWay": True
+ },
+ {
+ "fromId": "gumi",
+ "toId": "gumi_after_swamp_shrine",
+ "requiredNodes": [
+ "swamp_shrine"
+ ]
+ },
+ {
+ "fromId": "gumi",
+ "toId": "route_gumi_ryuma"
+ },
+ {
+ "fromId": "route_gumi_ryuma",
+ "toId": "ryuma",
+ "twoWay": True
+ },
+ {
+ "fromId": "ryuma",
+ "toId": "ryuma_after_thieves_hideout",
+ "requiredNodes": [
+ "thieves_hideout_post_key"
+ ]
+ },
+ {
+ "fromId": "ryuma",
+ "toId": "ryuma_lighthouse_repaired",
+ "twoWay": True,
+ "requiredItems": [
+ "Sun Stone"
+ ]
+ },
+ {
+ "fromId": "ryuma",
+ "toId": "thieves_hideout_pre_key",
+ "twoWay": True
+ },
+ {
+ "fromId": "thieves_hideout_pre_key",
+ "toId": "thieves_hideout_post_key",
+ "requiredItems": [
+ "Key"
+ ]
+ },
+ {
+ "fromId": "thieves_hideout_post_key",
+ "toId": "thieves_hideout_pre_key"
+ },
+ {
+ "fromId": "route_gumi_ryuma",
+ "toId": "tibor",
+ "twoWay": True
+ },
+ {
+ "fromId": "route_gumi_ryuma",
+ "toId": "helga_hut",
+ "twoWay": True,
+ "requiredItems": [
+ "Einstein Whistle"
+ ],
+ "requiredNodes": [
+ "massan"
+ ]
+ },
+ {
+ "fromId": "route_gumi_ryuma",
+ "toId": "mercator",
+ "twoWay": True,
+ "weight": 2,
+ "requiredItems": [
+ "Safety Pass"
+ ]
+ },
+ {
+ "fromId": "mercator",
+ "toId": "mercator_dungeon",
+ "twoWay": True
+ },
+ {
+ "fromId": "mercator",
+ "toId": "crypt",
+ "twoWay": True
+ },
+ {
+ "fromId": "mercator",
+ "toId": "mercator_special_shop",
+ "twoWay": True,
+ "requiredItems": [
+ "Buyer Card"
+ ]
+ },
+ {
+ "fromId": "mercator",
+ "toId": "mercator_casino",
+ "twoWay": True,
+ "requiredItems": [
+ "Casino Ticket"
+ ]
+ },
+ {
+ "fromId": "mercator",
+ "toId": "mir_tower_sector",
+ "twoWay": True
+ },
+ {
+ "fromId": "mir_tower_sector",
+ "toId": "twinkle_village",
+ "twoWay": True
+ },
+ {
+ "fromId": "mir_tower_sector",
+ "toId": "mir_tower_sector_tree_ledge",
+ "twoWay": True,
+ "requiredItems": [
+ "Axe Magic"
+ ]
+ },
+ {
+ "fromId": "mir_tower_sector",
+ "toId": "mir_tower_sector_tree_coast",
+ "twoWay": True,
+ "requiredItems": [
+ "Axe Magic"
+ ]
+ },
+ {
+ "fromId": "mir_tower_sector",
+ "toId": "mir_tower_pre_garlic",
+ "requiredItems": [
+ "Armlet"
+ ]
+ },
+ {
+ "fromId": "mir_tower_pre_garlic",
+ "toId": "mir_tower_sector"
+ },
+ {
+ "fromId": "mir_tower_pre_garlic",
+ "toId": "mir_tower_post_garlic",
+ "requiredItems": [
+ "Garlic"
+ ]
+ },
+ {
+ "fromId": "mir_tower_post_garlic",
+ "toId": "mir_tower_pre_garlic"
+ },
+ {
+ "fromId": "mir_tower_post_garlic",
+ "toId": "mir_tower_sector"
+ },
+ {
+ "fromId": "mercator",
+ "toId": "greenmaze_pre_whistle",
+ "weight": 2,
+ "requiredItems": [
+ "Key"
+ ]
+ },
+ {
+ "fromId": "greenmaze_pre_whistle",
+ "toId": "greenmaze_post_whistle",
+ "requiredItems": [
+ "Einstein Whistle"
+ ]
+ },
+ {
+ "fromId": "greenmaze_pre_whistle",
+ "toId": "greenmaze_cutter",
+ "requiredItems": [
+ "EkeEke"
+ ],
+ "twoWay": True
+ },
+ {
+ "fromId": "greenmaze_post_whistle",
+ "toId": "route_massan_gumi"
+ },
+ {
+ "fromId": "mercator",
+ "toId": "mercator_repaired_docks",
+ "requiredNodes": [
+ "ryuma_lighthouse_repaired"
+ ]
+ },
+ {
+ "fromId": "mercator_repaired_docks",
+ "toId": "verla_shore"
+ },
+ {
+ "fromId": "verla_shore",
+ "toId": "verla",
+ "twoWay": True
+ },
+ {
+ "fromId": "verla",
+ "toId": "verla_after_mines",
+ "requiredNodes": [
+ "verla_mines"
+ ],
+ "twoWay": True
+ },
+ {
+ "fromId": "verla_shore",
+ "toId": "verla_mines",
+ "twoWay": True
+ },
+ {
+ "fromId": "verla_mines",
+ "toId": "verla_shore_cliff",
+ "twoWay": True
+ },
+ {
+ "fromId": "verla_shore_cliff",
+ "toId": "verla_shore"
+ },
+ {
+ "fromId": "verla_shore",
+ "toId": "mir_tower_sector",
+ "requiredNodes": [
+ "verla_mines"
+ ],
+ "twoWay": True
+ },
+ {
+ "fromId": "verla_mines",
+ "toId": "route_verla_destel"
+ },
+ {
+ "fromId": "verla_mines",
+ "toId": "verla_mines_behind_lava",
+ "twoWay": True,
+ "requiredItems": [
+ "Fireproof"
+ ]
+ },
+ {
+ "fromId": "route_verla_destel",
+ "toId": "destel",
+ "twoWay": True
+ },
+ {
+ "fromId": "destel",
+ "toId": "route_after_destel",
+ "twoWay": True
+ },
+ {
+ "fromId": "destel",
+ "toId": "destel_well",
+ "twoWay": True
+ },
+ {
+ "fromId": "destel_well",
+ "toId": "route_lake_shrine",
+ "twoWay": True
+ },
+ {
+ "fromId": "route_lake_shrine",
+ "toId": "lake_shrine",
+ "itemsPlacedWhenCrossing": [
+ "Sword of Gaia"
+ ]
+ },
+ {
+ "fromId": "lake_shrine",
+ "toId": "route_lake_shrine"
+ },
+ {
+ "fromId": "lake_shrine",
+ "toId": "mir_tower_sector"
+ },
+ {
+ "fromId": "greenmaze_pre_whistle",
+ "toId": "mountainous_area",
+ "twoWay": True,
+ "requiredItems": [
+ "Axe Magic"
+ ]
+ },
+ {
+ "fromId": "mountainous_area",
+ "toId": "route_lake_shrine_cliff",
+ "twoWay": True,
+ "requiredItems": [
+ "Axe Magic"
+ ]
+ },
+ {
+ "fromId": "route_lake_shrine_cliff",
+ "toId": "route_lake_shrine"
+ },
+ {
+ "fromId": "mountainous_area",
+ "toId": "king_nole_cave",
+ "twoWay": True,
+ "weight": 2,
+ "requiredItems": [
+ "Gola's Eye"
+ ]
+ },
+ {
+ "fromId": "king_nole_cave",
+ "toId": "mercator"
+ },
+ {
+ "fromId": "king_nole_cave",
+ "toId": "kazalt",
+ "itemsPlacedWhenCrossing": [
+ "Lithograph"
+ ]
+ },
+ {
+ "fromId": "kazalt",
+ "toId": "king_nole_cave"
+ },
+ {
+ "fromId": "kazalt",
+ "toId": "king_nole_labyrinth_pre_door",
+ "twoWay": True
+ },
+ {
+ "fromId": "king_nole_labyrinth_pre_door",
+ "toId": "king_nole_labyrinth_post_door",
+ "requiredItems": [
+ "Key"
+ ]
+ },
+ {
+ "fromId": "king_nole_labyrinth_post_door",
+ "toId": "king_nole_labyrinth_pre_door"
+ },
+ {
+ "fromId": "king_nole_labyrinth_pre_door",
+ "toId": "king_nole_labyrinth_exterior",
+ "requiredItems": [
+ "Iron Boots"
+ ]
+ },
+ {
+ "fromId": "king_nole_labyrinth_exterior",
+ "toId": "king_nole_labyrinth_fall_from_exterior",
+ "requiredItems": [
+ "Axe Magic"
+ ]
+ },
+ {
+ "fromId": "king_nole_labyrinth_fall_from_exterior",
+ "toId": "king_nole_labyrinth_pre_door"
+ },
+ {
+ "fromId": "king_nole_labyrinth_post_door",
+ "toId": "king_nole_labyrinth_raft_entrance",
+ "requiredItems": [
+ "Snow Spikes"
+ ]
+ },
+ {
+ "fromId": "king_nole_labyrinth_raft_entrance",
+ "toId": "king_nole_labyrinth_post_door"
+ },
+ {
+ "fromId": "king_nole_labyrinth_raft_entrance",
+ "toId": "king_nole_labyrinth_raft",
+ "requiredItems": [
+ "Logs"
+ ]
+ },
+ {
+ "fromId": "king_nole_labyrinth_raft",
+ "toId": "king_nole_labyrinth_raft_entrance"
+ },
+ {
+ "fromId": "king_nole_labyrinth_post_door",
+ "toId": "king_nole_labyrinth_path_to_palace",
+ "requiredItems": [
+ "Snow Spikes"
+ ]
+ },
+ {
+ "fromId": "king_nole_labyrinth_path_to_palace",
+ "toId": "king_nole_labyrinth_post_door"
+ },
+ {
+ "fromId": "king_nole_labyrinth_post_door",
+ "toId": "king_nole_labyrinth_sacred_tree",
+ "requiredItems": [
+ "Axe Magic"
+ ],
+ "requiredNodes": [
+ "king_nole_labyrinth_raft_entrance"
+ ]
+ },
+ {
+ "fromId": "king_nole_labyrinth_path_to_palace",
+ "toId": "king_nole_palace",
+ "twoWay": True
+ },
+ {
+ "fromId": "king_nole_palace",
+ "toId": "end",
+ "requiredItems": [
+ "Gola's Fang",
+ "Gola's Horn",
+ "Gola's Nail"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/worlds/landstalker/data/world_region.py b/worlds/landstalker/data/world_region.py
new file mode 100644
index 000000000000..3365a9dfa9e2
--- /dev/null
+++ b/worlds/landstalker/data/world_region.py
@@ -0,0 +1,299 @@
+WORLD_REGIONS_JSON = [
+ {
+ "name": "Massan",
+ "hintName": "in the village of Massan",
+ "nodeIds": [
+ "massan",
+ "massan_after_swamp_shrine"
+ ]
+ },
+ {
+ "name": "Massan Cave",
+ "hintName": "in the cave near Massan",
+ "nodeIds": [
+ "massan_cave"
+ ],
+ "darkMapIds": [
+ 803, 804, 805, 806, 807
+ ]
+ },
+ {
+ "name": "Route between Massan and Gumi",
+ "canBeHintedAsRequired": False,
+ "nodeIds": [
+ "route_massan_gumi"
+ ]
+ },
+ {
+ "name": "Waterfall Shrine",
+ "hintName": "in the waterfall shrine",
+ "nodeIds": [
+ "waterfall_shrine"
+ ],
+ "darkMapIds": [
+ 174, 175, 176, 177, 178, 179, 180, 181, 182
+ ]
+ },
+ {
+ "name": "Swamp Shrine",
+ "hintName": "in the swamp shrine",
+ "canBeHintedAsRequired": False,
+ "nodeIds": [
+ "swamp_shrine"
+ ],
+ "darkMapIds": [
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 30
+ ]
+ },
+ {
+ "name": "Gumi",
+ "hintName": "in the village of Gumi",
+ "nodeIds": [
+ "gumi",
+ "gumi_after_swamp_shrine"
+ ]
+ },
+ {
+ "name": "Route between Gumi and Ryuma",
+ "canBeHintedAsRequired": False,
+ "nodeIds": [
+ "route_gumi_ryuma"
+ ]
+ },
+ {
+ "name": "Tibor",
+ "hintName": "inside Tibor",
+ "nodeIds": [
+ "tibor"
+ ],
+ "darkMapIds": [
+ 808, 809, 810, 811, 812, 813, 814, 815
+ ]
+ },
+ {
+ "name": "Ryuma",
+ "hintName": "in the town of Ryuma",
+ "nodeIds": [
+ "ryuma",
+ "ryuma_after_thieves_hideout",
+ "ryuma_lighthouse_repaired"
+ ]
+ },
+ {
+ "name": "Thieves Hideout",
+ "hintName": "in the thieves' hideout",
+ "nodeIds": [
+ "thieves_hideout_pre_key",
+ "thieves_hideout_post_key"
+ ],
+ "darkMapIds": [
+ 185, 186, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202,
+ 203, 204, 205, 206, 207, 208, 210, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222
+ ]
+ },
+ {
+ "name": "Witch Helga's Hut",
+ "hintName": "in witch Helga's hut",
+ "nodeIds": [
+ "helga_hut"
+ ]
+ },
+ {
+ "name": "Mercator",
+ "hintName": "in the town of Mercator",
+ "nodeIds": [
+ "mercator",
+ "mercator_repaired_docks",
+ "mercator_casino",
+ "mercator_special_shop"
+ ]
+ },
+ {
+ "name": "Crypt",
+ "hintName": "in the crypt of Mercator",
+ "nodeIds": [
+ "crypt"
+ ],
+ "darkMapIds": [
+ 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659
+ ]
+ },
+ {
+ "name": "Mercator Dungeon",
+ "hintName": "in the dungeon of Mercator",
+ "nodeIds": [
+ "mercator_dungeon"
+ ],
+ "darkMapIds": [
+ 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 76, 80, 81, 82, 91, 92
+ ]
+ },
+ {
+ "name": "Mir Tower sector",
+ "hintName": "near Mir Tower",
+ "canBeHintedAsRequired": False,
+ "nodeIds": [
+ "mir_tower_sector",
+ "mir_tower_sector_tree_ledge",
+ "mir_tower_sector_tree_coast",
+ "twinkle_village"
+ ]
+ },
+ {
+ "name": "Mir Tower",
+ "hintName": "inside Mir Tower",
+ "canBeHintedAsRequired": False,
+ "nodeIds": [
+ "mir_tower_pre_garlic",
+ "mir_tower_post_garlic"
+ ],
+ "darkMapIds": [
+ 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766,
+ 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784
+ ]
+ },
+ {
+ "name": "Greenmaze",
+ "hintName": "in Greenmaze",
+ "nodeIds": [
+ "greenmaze_pre_whistle",
+ "greenmaze_post_whistle"
+ ]
+ },
+ {
+ "name": "Verla Shore",
+ "canBeHintedAsRequired": False,
+ "nodeIds": [
+ "verla_shore",
+ "verla_shore_cliff"
+ ]
+ },
+ {
+ "name": "Verla",
+ "hintName": "in the town of Verla",
+ "nodeIds": [
+ "verla",
+ "verla_after_mines"
+ ]
+ },
+ {
+ "name": "Verla Mines",
+ "hintName": "in the mines near Verla",
+ "nodeIds": [
+ "verla_mines",
+ "verla_mines_behind_lava"
+ ],
+ "darkMapIds": [
+ 227, 228, 229, 230, 231, 232, 233, 234, 235, 237, 239, 240, 241, 242, 243, 244, 246,
+ 247, 248, 250, 253, 254, 255, 256, 258, 259, 266, 268, 269, 471
+ ]
+ },
+ {
+ "name": "Route between Verla and Destel",
+ "canBeHintedAsRequired": False,
+ "nodeIds": [
+ "route_verla_destel"
+ ]
+ },
+ {
+ "name": "Destel",
+ "hintName": "in the village of Destel",
+ "nodeIds": [
+ "destel"
+ ]
+ },
+ {
+ "name": "Route after Destel",
+ "canBeHintedAsRequired": False,
+ "nodeIds": [
+ "route_after_destel"
+ ]
+ },
+ {
+ "name": "Destel Well",
+ "hintName": "in Destel well",
+ "nodeIds": [
+ "destel_well"
+ ],
+ "darkMapIds": [
+ 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290
+ ]
+ },
+ {
+ "name": "Route to Lake Shrine",
+ "canBeHintedAsRequired": False,
+ "nodeIds": [
+ "route_lake_shrine",
+ "route_lake_shrine_cliff"
+ ]
+ },
+ {
+ "name": "Lake Shrine",
+ "hintName": "in the lake shrine",
+ "nodeIds": [
+ "lake_shrine"
+ ],
+ "darkMapIds": [
+ 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306,
+ 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322,
+ 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338,
+ 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354
+ ]
+ },
+ {
+ "name": "Mountainous Area",
+ "hintName": "in the mountainous area",
+ "nodeIds": [
+ "mountainous_area"
+ ]
+ },
+ {
+ "name": "King Nole's Cave",
+ "hintName": "in King Nole's cave",
+ "nodeIds": [
+ "king_nole_cave"
+ ],
+ "darkMapIds": [
+ 145, 147, 150, 152, 154, 155, 156, 158, 160, 161, 162, 164, 166, 170, 171, 172
+ ]
+ },
+ {
+ "name": "Kazalt",
+ "hintName": "in the hidden town of Kazalt",
+ "nodeIds": [
+ "kazalt"
+ ]
+ },
+ {
+ "name": "King Nole's Labyrinth",
+ "hintName": "in King Nole's labyrinth",
+ "nodeIds": [
+ "king_nole_labyrinth_pre_door",
+ "king_nole_labyrinth_post_door",
+ "king_nole_labyrinth_exterior",
+ "king_nole_labyrinth_fall_from_exterior",
+ "king_nole_labyrinth_path_to_palace",
+ "king_nole_labyrinth_raft_entrance",
+ "king_nole_labyrinth_raft",
+ "king_nole_labyrinth_sacred_tree"
+ ],
+ "darkMapIds": [
+ 355, 356, 357, 358, 359, 360, 361, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372,
+ 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389,
+ 390, 391, 392, 393, 394, 395, 396, 397, 398, 405, 406, 408, 409, 410, 411, 412, 413,
+ 414, 415, 416, 417, 418, 419, 420, 422, 423
+ ]
+ },
+ {
+ "name": "King Nole's Palace",
+ "hintName": "in King Nole's palace",
+ "nodeIds": [
+ "king_nole_palace",
+ "end"
+ ],
+ "darkMapIds": [
+ 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130,
+ 131, 132, 133, 134, 135, 136, 137, 138
+ ]
+ }
+]
\ No newline at end of file
diff --git a/worlds/landstalker/data/world_teleport_tree.py b/worlds/landstalker/data/world_teleport_tree.py
new file mode 100644
index 000000000000..830f5547201e
--- /dev/null
+++ b/worlds/landstalker/data/world_teleport_tree.py
@@ -0,0 +1,62 @@
+WORLD_TELEPORT_TREES_JSON = [
+ [
+ {
+ "name": "Massan tree",
+ "treeMapId": 512,
+ "nodeId": "route_massan_gumi"
+ },
+ {
+ "name": "Tibor tree",
+ "treeMapId": 534,
+ "nodeId": "route_gumi_ryuma"
+ }
+ ],
+ [
+ {
+ "name": "Mercator front gate tree",
+ "treeMapId": 539,
+ "nodeId": "route_gumi_ryuma"
+ },
+ {
+ "name": "Verla shore tree",
+ "treeMapId": 537,
+ "nodeId": "verla_shore"
+ }
+ ],
+ [
+ {
+ "name": "Destel sector tree",
+ "treeMapId": 536,
+ "nodeId": "route_after_destel"
+ },
+ {
+ "name": "Lake Shrine sector tree",
+ "treeMapId": 513,
+ "nodeId": "route_lake_shrine"
+ }
+ ],
+ [
+ {
+ "name": "Mir Tower sector tree",
+ "treeMapId": 538,
+ "nodeId": "mir_tower_sector"
+ },
+ {
+ "name": "Mountainous area tree",
+ "treeMapId": 535,
+ "nodeId": "mountainous_area"
+ }
+ ],
+ [
+ {
+ "name": "Greenmaze entrance tree",
+ "treeMapId": 510,
+ "nodeId": "greenmaze_pre_whistle"
+ },
+ {
+ "name": "Greenmaze end tree",
+ "treeMapId": 511,
+ "nodeId": "greenmaze_post_whistle"
+ }
+ ]
+]
\ No newline at end of file
diff --git a/worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md b/worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md
new file mode 100644
index 000000000000..9239f741b436
--- /dev/null
+++ b/worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md
@@ -0,0 +1,60 @@
+# Landstalker: The Treasures of King Nole
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains most of the options you need to
+configure and export a config file.
+
+## What does randomization do to this game?
+
+All items are shuffled while keeping a logic to make every seed completable.
+
+Some key items could be obtained in a very different order compared to the vanilla game, leading to very unusual situations.
+
+The world is made as open as possible while keeping the original locks behind the same items & triggers as vanilla
+when that makes sense logic-wise. This puts the emphasis on exploration and gameplay by removing all the scenario
+and story-related triggers, giving a wide open world to explore.
+
+## What items and locations get shuffled?
+
+All items and locations are shuffled. This includes **chests**, items on **ground**, in **shops**, and given by **NPCs**.
+
+It's also worth noting that all of these items are shuffled among all worlds, meaning every item can be sent to you
+by other players.
+
+## What are the main differences compared to the vanilla game?
+
+The **Key** is now a unique item and can open several doors without being consumed, making it a standard progression item.
+All key doors are gone, except three of them :
+ - the Mercator castle backdoor (giving access to Greenmaze sector)
+ - Thieves Hideout middle door (cutting the level in half)
+ - King Nole's Labyrinth door near entrance
+
+---
+
+The secondary shop of Mercator requiring to do the traders sidequest in the original game is now unlocked by having
+**Buyer Card** in your inventory.
+
+You will need as many **jewels** as specified in the options to use the teleporter to go to Kazalt and the final dungeon.
+If you find and use the **Lithograph**, it will tell you in which world are each one of your jewels.
+
+Each seed, there is a random dungeon which is chosen to be the "dark dungeon" where you won't see anything unless you
+have the **Lantern** in your inventory. Unlike vanilla, King Nole's Labyrinth no longer has the few dark rooms the lantern
+was originally intended for.
+
+The **Statue of Jypta** is introduced as a real item (instead of just being an intro gimmick) and gives you gold over
+time while you're walking, the same way Healing Boots heal you when you walk.
+
+
+## What do I need to know for my first seed?
+
+It's advised you keep Massan as your starting region for your first seed, since taking another starting region might
+be significantly harder, both combat-wise and logic-wise.
+
+Having fully open & shuffled teleportation trees is an interesting way to play, but is discouraged for beginners
+as well since it can force you to go in late-game zones with few Life Stocks.
+
+Overall, the default options are good for a beginner-friendly seed, and if you don't feel too confident, you can also
+lower the combat difficulty to make it more forgiving.
+
+*Have fun on your adventure!*
diff --git a/worlds/landstalker/docs/landstalker_setup_en.md b/worlds/landstalker/docs/landstalker_setup_en.md
new file mode 100644
index 000000000000..30f85dd8f19b
--- /dev/null
+++ b/worlds/landstalker/docs/landstalker_setup_en.md
@@ -0,0 +1,121 @@
+# Landstalker Setup Guide
+
+## Required Software
+
+- [Landstalker Archipelago Client](https://github.com/Dinopony/randstalker-archipelago/releases) (only available on Windows)
+- A compatible emulator to run the game
+ - [RetroArch](https://retroarch.com?page=platforms) with the Genesis Plus GX core
+ - [Bizhawk 2.9.1 (x64)](https://tasvideos.org/BizHawk/ReleaseHistory) with the Genesis Plus GX core
+- Your legally obtained Landstalker US ROM file (which can be acquired on [Steam](https://store.steampowered.com/app/71118/Landstalker_The_Treasures_of_King_Nole/))
+
+## Installation Instructions
+
+- Unzip the Landstalker Archipelago Client archive into its own folder
+- Put your Landstalker ROM (`LandStalker_USA.SGD` on the Steam release) inside this folder
+- To launch the client, launch `randstalker_archipelago.exe` inside that folder
+
+Be aware that you might get antivirus warnings about the client program because one of its main features is to spy
+on another process's memory (your emulator). This is something antiviruses obviously dislike, and sometimes mistake
+for malicious software.
+
+If you're not trusting the program, you can check its [source code](https://github.com/Dinopony/randstalker-archipelago/)
+or test it on a service like Virustotal.
+
+## Create a Config (.yaml) File
+
+### What is a config file and why do I need one?
+
+See the guide on setting up a basic YAML at the Archipelago setup
+guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
+
+### Where do I get a config file?
+
+
+The [Player Options Page](/games/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/player-options) on the website allows
+you to easily configure your personal options.
+
+
+## How-to-play
+
+### Connecting to the Archipelago Server
+
+Once the game has been created, you need to connect to the server using the Landstalker Archipelago Client.
+
+To do so, run `randstalker_archipelago.exe` inside the folder you created while installing the software.
+
+A window will open with a few settings to enter:
+- **Host**: Put the server address and port in this field (e.g. `archipelago.gg:12345`)
+- **Slot name**: Put the player name you specified in your YAML config file in this field.
+- **Password**: If the server has a password, put it there.
+
+![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_ap.png)
+
+Once all those fields were filled appropriately, click on the `Connect to Archipelago` button below to try connecting to
+the Archipelago server.
+
+If this didn't work, double-check your credentials. An error message should be displayed on the console log to the
+right that might help you find the cause of the issue.
+
+### ROM Generation
+
+When you connected to the Archipelago server, the client fetched all the required data from the server to be able to
+build a randomized ROM.
+
+You should see a window with settings to fill:
+- **Input ROM file**: This is the path to your original ROM file for the game. If you are using the Steam release ROM
+ and placed it inside the client's folder as mentioned above, you don't need to change anything.
+- **Output ROM directory**: This is where the randomized ROMs will be put. No need to change this unless you want them
+ to be created in a very specific folder.
+
+![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_rom.png)
+
+There also a few cosmetic options you can fill before clicking the `Build ROM` button which should create your
+randomized seed if everything went right.
+
+If it didn't, double-check your `Input ROM file` and `Output ROM path`, then retry building the ROM by clicking
+the same button again.
+
+### Connecting to the emulator
+
+Now that you're connected to the Archipelago server and have a randomized ROM, all we need is to get the client
+connected to the emulator. This way, the client will be able to see what's happening while you play and give you in-game
+the items you have received from other players.
+
+You should see the following window:
+
+![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_emu.png)
+
+As written, you have to open the newly generated ROM inside either Retroarch or Bizhawk using the Genesis Plus GX core.
+Be careful to select that core, because any other core (e.g. BlastEm) won't work.
+
+The easiest way to do so is to:
+- open the emu of your choice
+- if you're using Retroarch and it's your first time, download the Genesis Plus GX core through Retroarch user interface
+- click the `Show ROM file in explorer` button
+- drag-and-drop the shown ROM file on the emulator window
+- press Start to reach file select screen (to ensure game RAM is properly set-up)
+
+Then, you can click on the `Connect to emulator` button below and it should work.
+
+If this didn't work, try the following:
+- ensure you have loaded your ROM and reached the save select screen
+- ensure you are using Genesis Plus GX and not another core (e.g. BlastEm will not work)
+- try launching the client in Administrator Mode (right-click on `randstalker_archipelago.exe`, then
+ `Run as administrator`)
+- if all else fails, try using one of those specific emulator versions:
+ - RetroArch 1.9.0 and Genesis Plus GX 1.7.4
+ - Bizhawk 2.9.1 (x64)
+
+### Play the game
+
+If all indicators are green and show "Connected," you're good to go! Play the game and enjoy the wonders of isometric
+perspective.
+
+The client is packaged with both an **automatic item tracker** and an **automatic map tracker** for your comfort.
+
+If you don't know all checks in the game, don't be afraid: you can click the `Where is it?` button that will show
+you a screenshot of where the location actually is.
+
+![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_client.png)
+
+Have fun!
\ No newline at end of file
diff --git a/worlds/landstalker/docs/ls_guide_ap.png b/worlds/landstalker/docs/ls_guide_ap.png
new file mode 100644
index 000000000000..674938ce6707
Binary files /dev/null and b/worlds/landstalker/docs/ls_guide_ap.png differ
diff --git a/worlds/landstalker/docs/ls_guide_client.png b/worlds/landstalker/docs/ls_guide_client.png
new file mode 100644
index 000000000000..a4e0f1ccf3d8
Binary files /dev/null and b/worlds/landstalker/docs/ls_guide_client.png differ
diff --git a/worlds/landstalker/docs/ls_guide_emu.png b/worlds/landstalker/docs/ls_guide_emu.png
new file mode 100644
index 000000000000..ff9218de12ba
Binary files /dev/null and b/worlds/landstalker/docs/ls_guide_emu.png differ
diff --git a/worlds/landstalker/docs/ls_guide_rom.png b/worlds/landstalker/docs/ls_guide_rom.png
new file mode 100644
index 000000000000..c57554ab43d8
Binary files /dev/null and b/worlds/landstalker/docs/ls_guide_rom.png differ
diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py
new file mode 100644
index 000000000000..9853be73fa9b
--- /dev/null
+++ b/worlds/lingo/__init__.py
@@ -0,0 +1,192 @@
+"""
+Archipelago init file for Lingo
+"""
+from logging import warning
+
+from BaseClasses import CollectionState, Item, ItemClassification, Tutorial
+from Options import OptionError
+from worlds.AutoWorld import WebWorld, World
+from .datatypes import Room, RoomEntrance
+from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, TRAP_ITEMS, LingoItem
+from .locations import ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP
+from .options import LingoOptions, lingo_option_groups, SunwarpAccess, VictoryCondition
+from .player_logic import LingoPlayerLogic
+from .regions import create_regions
+
+
+class LingoWebWorld(WebWorld):
+ option_groups = lingo_option_groups
+ rich_text_options_doc = True
+ theme = "grass"
+ tutorials = [Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to playing Lingo with Archipelago.",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["hatkirby"]
+ )]
+
+
+class LingoWorld(World):
+ """
+ Lingo is a first person indie puzzle game in the vein of The Witness. You find yourself in a mazelike, non-Euclidean
+ world filled with 800 word puzzles that use a variety of different mechanics.
+ """
+ game = "Lingo"
+ web = LingoWebWorld()
+
+ base_id = 444400
+ topology_present = True
+
+ options_dataclass = LingoOptions
+ options: LingoOptions
+
+ item_name_to_id = {
+ name: data.code for name, data in ALL_ITEM_TABLE.items()
+ }
+ location_name_to_id = {
+ name: data.code for name, data in ALL_LOCATION_TABLE.items()
+ }
+ item_name_groups = ITEMS_BY_GROUP
+ location_name_groups = LOCATIONS_BY_GROUP
+
+ player_logic: LingoPlayerLogic
+
+ def generate_early(self):
+ if not (self.options.shuffle_doors or self.options.shuffle_colors or
+ (self.options.sunwarp_access >= SunwarpAccess.option_unlock and
+ self.options.victory_condition == VictoryCondition.option_pilgrimage)):
+ if self.multiworld.players == 1:
+ warning(f"{self.player_name}'s Lingo world doesn't have any progression items. Please turn on Door"
+ f" Shuffle or Color Shuffle, or use item-blocked sunwarps with the Pilgrimage victory condition"
+ f" if that doesn't seem right.")
+ else:
+ raise OptionError(f"{self.player_name}'s Lingo world doesn't have any progression items. Please turn on"
+ f" Door Shuffle or Color Shuffle, or use item-blocked sunwarps with the Pilgrimage"
+ f" victory condition.")
+
+ self.player_logic = LingoPlayerLogic(self)
+
+ def create_regions(self):
+ create_regions(self)
+
+ if not self.options.shuffle_postgame:
+ state = CollectionState(self.multiworld)
+ state.collect(LingoItem("Prevent Victory", ItemClassification.progression, None, self.player), True)
+
+ # Note: relies on the assumption that real_items is a definitive list of real progression items in this
+ # world, and is not modified after being created.
+ for item in self.player_logic.real_items:
+ state.collect(self.create_item(item), True)
+
+ # Exception to the above: a forced good item is not considered a "real item", but needs to be here anyway.
+ if self.player_logic.forced_good_item != "":
+ state.collect(self.create_item(self.player_logic.forced_good_item), True)
+
+ all_locations = self.multiworld.get_locations(self.player)
+ state.sweep_for_events(locations=all_locations)
+
+ unreachable_locations = [location for location in all_locations
+ if not state.can_reach_location(location.name, self.player)]
+
+ for location in unreachable_locations:
+ if location.name in self.player_logic.event_loc_to_item.keys():
+ continue
+
+ self.player_logic.real_locations.remove(location.name)
+ location.parent_region.locations.remove(location)
+
+ if len(self.player_logic.real_items) > len(self.player_logic.real_locations):
+ raise OptionError(f"{self.player_name}'s Lingo world does not have enough locations to fit the number"
+ f" of required items without shuffling the postgame. Either enable postgame"
+ f" shuffling, or choose different options.")
+
+ def create_items(self):
+ pool = [self.create_item(name) for name in self.player_logic.real_items]
+
+ if self.player_logic.forced_good_item != "":
+ new_item = self.create_item(self.player_logic.forced_good_item)
+ location_obj = self.multiworld.get_location("Second Room - Good Luck", self.player)
+ location_obj.place_locked_item(new_item)
+
+ item_difference = len(self.player_logic.real_locations) - len(pool)
+ if item_difference:
+ trap_percentage = self.options.trap_percentage
+ traps = int(item_difference * trap_percentage / 100.0)
+ non_traps = item_difference - traps
+
+ if non_traps:
+ skip_percentage = self.options.puzzle_skip_percentage
+ skips = int(non_traps * skip_percentage / 100.0)
+ non_skips = non_traps - skips
+
+ for i in range(0, non_skips):
+ pool.append(self.create_item(self.get_filler_item_name()))
+
+ for i in range(0, skips):
+ pool.append(self.create_item("Puzzle Skip"))
+
+ if traps:
+ total_weight = sum(self.options.trap_weights.values())
+
+ if total_weight == 0:
+ raise OptionError("Sum of trap weights must be at least one.")
+
+ trap_counts = {name: int(weight * traps / total_weight)
+ for name, weight in self.options.trap_weights.items()}
+
+ trap_difference = traps - sum(trap_counts.values())
+ if trap_difference > 0:
+ allowed_traps = [name for name in TRAP_ITEMS if self.options.trap_weights[name] > 0]
+ for i in range(0, trap_difference):
+ trap_counts[allowed_traps[i % len(allowed_traps)]] += 1
+
+ for name, count in trap_counts.items():
+ for i in range(0, count):
+ pool.append(self.create_item(name))
+
+ self.multiworld.itempool += pool
+
+ def create_item(self, name: str) -> Item:
+ item = ALL_ITEM_TABLE[name]
+
+ classification = item.classification
+ if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0 \
+ and not item.has_doors and all(painting_id not in self.player_logic.painting_mapping
+ for painting_id in item.painting_ids) \
+ and "pilgrim_painting2" not in item.painting_ids:
+ # If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings
+ # go nowhere, then this item should not be progression. The Pilgrim Room painting is special and needs to be
+ # excluded from this.
+ classification = ItemClassification.filler
+
+ return LingoItem(name, classification, item.code, self.player)
+
+ def set_rules(self):
+ self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
+
+ def fill_slot_data(self):
+ slot_options = [
+ "death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels",
+ "enable_pilgrimage", "sunwarp_access", "mastery_achievements", "level_2_requirement", "location_checks",
+ "early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps",
+ "group_doors"
+ ]
+
+ slot_data = {
+ "seed": self.random.randint(0, 1000000),
+ **self.options.as_dict(*slot_options),
+ }
+
+ if self.options.shuffle_paintings:
+ slot_data["painting_entrance_to_exit"] = self.player_logic.painting_mapping
+
+ if self.options.shuffle_sunwarps:
+ slot_data["sunwarp_permutation"] = self.player_logic.sunwarp_mapping
+
+ return slot_data
+
+ def get_filler_item_name(self) -> str:
+ filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"]
+ return self.random.choice(filler_list)
diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml
new file mode 100644
index 000000000000..950fd326743a
--- /dev/null
+++ b/worlds/lingo/data/LL1.yaml
@@ -0,0 +1,8631 @@
+---
+ # This file is an associative array where the keys are region names. Rooms
+ # have a number of properties:
+ # - entrances
+ # - panels
+ # - doors
+ # - panel_doors
+ # - paintings
+ # - progression
+ # - sunwarps
+ #
+ # entrances is an array of regions from which this room can be accessed. The
+ # key of each entry is the room that can access this one. The value is a list
+ # of OR'd requirements for being able to access this room from the other one,
+ # although the list can be elided if there is only one requirement, and the
+ # value True can be used if there are no requirements (i.e. you always have
+ # access to this room if you have access to the other). Each requirement
+ # describes a door that must be opened in order to access this room from the
+ # other. The door is described by both the door's name and the name of the
+ # room that the door is in. The room name may be omitted if the door is
+ # located in the current room.
+ #
+ # panels is a named array of panels in the room. The key of the array is an
+ # arbitrary name for the panel. Panels can have the following fields:
+ # - id: The internal ID of the panel in the LINGO map
+ # - required_room: In addition to having access to this room, the player must
+ # also have access to this other room in order to solve this
+ # panel.
+ # - required_door: In addition to having access to this room, the player must
+ # also have this door opened in order to solve this panel.
+ # - required_panel: In addition to having access to this room, the player must
+ # also be able to access this other panel in order to solve
+ # this panel.
+ # - colors: A list of colors that are required to be unlocked in order
+ # to solve this panel
+ # - check: A location check will be created for this individual panel.
+ # - exclude_reduce: Panel checks are assumed to be INCLUDED when reduce checks
+ # is on. This option excludes the check anyway.
+ # - tag: Label that describes how panel randomization should be
+ # done. In reorder mode, panels with the same tag can be
+ # shuffled amongst themselves. "forbid" is a special value
+ # meaning that no randomization should be done. This field is
+ # mandatory.
+ # - link: Panels with the same link label are randomized as a group.
+ # - subtag: Used to identify the separate parts of a linked group.
+ # - copy_to_sign: When randomizing this panel, the hint should be copied to
+ # the specified sign(s).
+ # - achievement: The name of the achievement that is received upon solving
+ # this panel.
+ # - non_counting: If True, this panel does not contribute to the total needed
+ # to unlock Level 2.
+ # - hunt: If True, the tracker will show this panel even when it is
+ # not a check. Used for hunts like the Number Hunt.
+ #
+ # doors is a named array of doors associated with this room. When door
+ # randomization is enabled, each of these is an item. The key is a name that
+ # will be displayed as part of the item's name. Doors can have the following
+ # fields:
+ # - id: A string or list of internal door IDs from the LINGO map.
+ # In door shuffle mode, collecting the item generated for
+ # this door will open the doors listed here.
+ # - painting_id: An internal ID of a painting that should be moved upon
+ # receiving this door.
+ # - warp_id: An internal ID or IDs of warps that should be disabled
+ # until receiving this door.
+ # - panels: These are the panels that canonically open this door. If
+ # there is only one panel for the door, then that panel is a
+ # check. If there is more than one panel, then that entire
+ # set of panels must be solved for a check. Panels can
+ # either be a string (representing a panel in this room) or
+ # a dict containing "room" and "panel".
+ # - item_name: Overrides the name of the item generated for this door.
+ # If not specified, the item name will be generated from
+ # the room name and the door name.
+ # - item_group: If set, this item will be in the specified item group.
+ # - location_name: Overrides the name of the location generated for this
+ # door. If not specified, the location name will be
+ # generated using the names of the panels.
+ # - skip_location: If true, no location is generated for this door.
+ # - skip_item: If true, no item is generated for this door.
+ # - door_group: When simple doors is used, all doors with the same group
+ # will be covered by a single item.
+ # - include_reduce: Door checks are assumed to be EXCLUDED when reduce checks
+ # is on. This option includes the check anyway.
+ # - event: Denotes that the door is event only. This is similar to
+ # setting both skip_location and skip_item.
+ #
+ # panel_doors is a named array of "panel doors" associated with this room.
+ # When panel door shuffle is enabled, each of these becomes an item, and those
+ # items block access to the listed panels. The key is a name for internal
+ # reference only. Panel doors can have the following fields:
+ # - panels: Required. This is the set of panels that are blocked by this
+ # panel door.
+ # - item_name: Overrides the name of the item generated for this panel
+ # door. If not specified, the item name will be generated from
+ # the room name and the name(s) of the panel(s).
+ # - panel_group: When region grouping is enabled, all panel doors with the
+ # same group will be covered by a single item.
+ #
+ # paintings is an array of paintings in the room. This is used for painting
+ # shuffling.
+ # - id: The internal painting ID from the LINGO map.
+ # - enter_only: If true, painting shuffling will not place a warp exit on
+ # this painting.
+ # - exit_only: If true, painting shuffling will not place a warp entrance
+ # on this painting.
+ # - orientation: One of north/south/east/west. This is the direction that
+ # the player is facing when they are interacting with it,
+ # not the orientation of the painting itself. "North" is
+ # the direction the player faces at a new game, with the
+ # positive X axis to the right.
+ # - required_door: This door must be open for the painting to be usable as an
+ # entrance. If required_door is set, enter_only must be
+ # True.
+ # - required: Marks a painting as being the only entrance for a room,
+ # and thus it is required to be an exit when randomized.
+ # Use "required_when_no_doors" instead if it would be
+ # possible to enter the room without the painting in door
+ # shuffle mode.
+ # - req_blocked: Marks that a painting cannot be an entrance leading to a
+ # required painting. Paintings within a room that has a
+ # required painting are automatically req blocked.
+ # Use "req_blocked_when_no_doors" instead if it would be
+ # fine in door shuffle mode.
+ # - move: Denotes that the painting is able to move.
+ #
+ # progression is a named array of items that define an ordered set of items.
+ # progression items do not have any true connection to the rooms that they
+ # are defined in, but it is best to place them in a thematically appropriate
+ # room. The key for a progression entry is the name of the item that will be
+ # created. A progression entry is a dictionary with one or both of a "doors"
+ # key and a "panel_doors" key. These fields should be lists of doors or
+ # panel doors that will be contained in this progressive item.
+ #
+ # sunwarps is an array of sunwarps in the room. This is used for sunwarp
+ # shuffling.
+ # - dots: The number of dots on this sunwarp.
+ # - direction: "enter" or "exit"
+ # - entrance_indicator_pos: Coordinates for where the entrance indicator
+ # should be placed if this becomes an entrance.
+ # - orientation: One of north/south/east/west.
+ Starting Room:
+ entrances:
+ Menu:
+ warp: True
+ Outside The Wise:
+ painting: True
+ Rhyme Room (Circle):
+ painting: True
+ Rhyme Room (Target):
+ painting: True
+ Wondrous Lobby:
+ painting: True
+ Orange Tower Third Floor:
+ painting: True
+ Color Hunt:
+ painting: True
+ Owl Hallway:
+ painting: True
+ The Wondrous:
+ room: The Wondrous
+ door: Exit
+ painting: True
+ Orange Tower Sixth Floor:
+ painting: True
+ Orange Tower Basement:
+ painting: True
+ The Colorful:
+ painting: True
+ Welcome Back Area:
+ room: Welcome Back Area
+ door: Shortcut to Starting Room
+ Second Room:
+ door: Main Door
+ Hidden Room:
+ door: Back Right Door
+ Rhyme Room (Looped Square):
+ door: Rhyme Room Entrance
+ panels:
+ HI:
+ id: Entry Room/Panel_hi_hi
+ tag: midwhite
+ check: True
+ HIDDEN:
+ id: Entry Room/Panel_hidden_hidden
+ tag: midwhite
+ TYPE:
+ id: Entry Room/Panel_type_type
+ tag: midwhite
+ THIS:
+ id: Entry Room/Panel_this_this
+ tag: midwhite
+ WRITE:
+ id: Entry Room/Panel_write_write
+ tag: midwhite
+ SAME:
+ id: Entry Room/Panel_same_same
+ tag: midwhite
+ doors:
+ Main Door:
+ event: True
+ panels:
+ - HI
+ Back Right Door:
+ id: Entry Room Area Doors/Door_hidden_hidden
+ include_reduce: True
+ panels:
+ - HIDDEN
+ Rhyme Room Entrance:
+ id:
+ - Palindrome Room Area Doors/Door_level_level_2
+ - Palindrome Room Area Doors/Door_racecar_racecar_2
+ - Palindrome Room Area Doors/Door_solos_solos_2
+ skip_location: True
+ door_group: Rhyme Room Doors
+ panels:
+ - room: The Tenacious
+ panel: LEVEL (Black)
+ - room: The Tenacious
+ panel: RACECAR (Black)
+ - room: The Tenacious
+ panel: SOLOS (Black)
+ panel_doors:
+ HIDDEN:
+ panels:
+ - HIDDEN
+ paintings:
+ - id: arrows_painting
+ exit_only: True
+ orientation: south
+ - id: arrows_painting2
+ disable: True
+ move: True
+ - id: arrows_painting3
+ disable: True
+ move: True
+ - id: garden_painting_tower2
+ enter_only: True
+ orientation: north
+ move: True
+ required_door:
+ room: Hedge Maze
+ door: Painting Shortcut
+ - id: flower_painting_8
+ enter_only: True
+ orientation: north
+ move: True
+ required_door:
+ room: Courtyard
+ door: Painting Shortcut
+ - id: symmetry_painting_a_starter
+ enter_only: True
+ orientation: west
+ move: True
+ required_door:
+ room: The Wondrous (Doorknob)
+ door: Painting Shortcut
+ - id: pencil_painting6
+ enter_only: True
+ orientation: east
+ move: True
+ required_door:
+ room: Outside The Bold
+ door: Painting Shortcut
+ - id: blueman_painting_3
+ enter_only: True
+ orientation: east
+ move: True
+ required_door:
+ room: Outside The Undeterred
+ door: Painting Shortcut
+ - id: eyes_yellow_painting2
+ enter_only: True
+ orientation: west
+ move: True
+ required_door:
+ room: Outside The Agreeable
+ door: Painting Shortcut
+ Hidden Room:
+ entrances:
+ Starting Room:
+ room: Starting Room
+ door: Back Right Door
+ The Seeker:
+ door: Seeker Entrance
+ Dead End Area:
+ door: Dead End Door
+ Knight Night (Outer Ring):
+ door: Knight Night Entrance
+ panels:
+ DEAD END:
+ id: Appendix Room/Panel_deadend_deadened
+ check: True
+ exclude_reduce: True
+ tag: topwhite
+ OPEN:
+ id: Heteronym Room/Panel_entrance_entrance
+ tag: midwhite
+ LIES:
+ id: Appendix Room/Panel_lies_lies
+ tag: midwhite
+ doors:
+ Dead End Door:
+ id: Appendix Room Area Doors/Door_rat_tar_2
+ skip_location: true
+ door_group: Dead End Area Access
+ panels:
+ - room: Hub Room
+ panel: RAT
+ Knight Night Entrance:
+ id: Appendix Room Area Doors/Door_rat_tar_4
+ skip_location: true
+ panels:
+ - room: Hub Room
+ panel: RAT
+ Seeker Entrance:
+ id: Entry Room Area Doors/Door_entrance_entrance
+ item_name: The Seeker - Entrance
+ item_group: Achievement Room Entrances
+ panels:
+ - OPEN
+ Rhyme Room Entrance:
+ id:
+ - Appendix Room Area Doors/Door_rat_tar_3
+ - Double Room Area Doors/Door_room_entry_stairs
+ skip_location: True
+ door_group: Rhyme Room Doors
+ panels:
+ - room: The Tenacious
+ panel: LEVEL (Black)
+ - room: The Tenacious
+ panel: RACECAR (Black)
+ - room: The Tenacious
+ panel: SOLOS (Black)
+ - room: Hub Room
+ panel: RAT
+ panel_doors:
+ OPEN:
+ panels:
+ - OPEN
+ paintings:
+ - id: owl_painting
+ orientation: north
+ The Seeker:
+ entrances:
+ Hidden Room:
+ room: Hidden Room
+ door: Seeker Entrance
+ Pilgrim Room:
+ room: Pilgrim Room
+ door: Shortcut to The Seeker
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_seeker_seeker
+ # The Seeker uniquely has the property that 1) it can be entered (through the Pilgrim Room) without opening the
+ # front door in panels mode door shuffle, and 2) the front door panel is part of the CDP. This necessitates this
+ # required_panel clause, because the entrance panel needs to be solvable for the achievement even if an
+ # alternate entrance to the room is used.
+ required_panel:
+ room: Hidden Room
+ panel: OPEN
+ tag: forbid
+ check: True
+ achievement: The Seeker
+ BEAR (1):
+ id: Heteronym Room/Panel_bear_bear
+ tag: midwhite
+ MINE (1):
+ id: Heteronym Room/Panel_mine_mine
+ tag: double midwhite
+ subtag: left
+ link: exact MINE
+ MINE (2):
+ id: Heteronym Room/Panel_mine_mine_2
+ tag: double midwhite
+ subtag: right
+ link: exact MINE
+ BOW:
+ id: Heteronym Room/Panel_bow_bow
+ tag: midwhite
+ DOES:
+ id: Heteronym Room/Panel_does_does
+ tag: midwhite
+ MOBILE (1):
+ id: Heteronym Room/Panel_mobile_mobile
+ tag: double midwhite
+ subtag: left
+ link: exact MOBILE
+ MOBILE (2):
+ id: Heteronym Room/Panel_mobile_mobile_2
+ tag: double midwhite
+ subtag: right
+ link: exact MOBILE
+ DESERT:
+ id: Heteronym Room/Panel_desert_desert
+ tag: topmid white stack
+ subtag: mid
+ link: topmid DESERT
+ DESSERT:
+ id: Heteronym Room/Panel_desert_dessert
+ tag: topmid white stack
+ subtag: top
+ link: topmid DESERT
+ SOW:
+ id: Heteronym Room/Panel_sow_sow
+ tag: topmid white stack
+ subtag: mid
+ link: topmid SOW
+ SEW:
+ id: Heteronym Room/Panel_sow_so
+ tag: topmid white stack
+ subtag: top
+ link: topmid SOW
+ TO:
+ id: Heteronym Room/Panel_two_to
+ tag: double topwhite
+ subtag: left
+ link: hp TWO
+ TOO:
+ id: Heteronym Room/Panel_two_too
+ tag: double topwhite
+ subtag: right
+ link: hp TWO
+ WRITE:
+ id: Heteronym Room/Panel_write_right
+ tag: topwhite
+ EWE:
+ id: Heteronym Room/Panel_you_ewe
+ tag: topwhite
+ KNOT:
+ id: Heteronym Room/Panel_not_knot
+ tag: double topwhite
+ subtag: left
+ link: hp NOT
+ NAUGHT:
+ id: Heteronym Room/Panel_not_naught
+ tag: double topwhite
+ subtag: right
+ link: hp NOT
+ BEAR (2):
+ id: Heteronym Room/Panel_bear_bare
+ tag: topwhite
+ Second Room:
+ entrances:
+ Starting Room:
+ room: Starting Room
+ door: Main Door
+ Hub Room:
+ door: Exit Door
+ panels:
+ HI:
+ id: Entry Room/Panel_hi_high
+ tag: topwhite
+ LOW:
+ id: Entry Room/Panel_low_low
+ tag: forbid # This is a midwhite pretending to be a botwhite
+ ANOTHER TRY:
+ id: Entry Room/Panel_advance
+ tag: topwhite
+ non_counting: True # This is a counting panel in-game, but it can never count towards the LEVEL 2 panel hunt.
+ LEVEL 2:
+ # We will set up special rules for this in code.
+ id: EndPanel/Panel_level_2
+ tag: forbid
+ non_counting: True
+ check: True
+ doors:
+ Exit Door:
+ id: Entry Room Area Doors/Door_hi_high
+ location_name: Second Room - Good Luck
+ include_reduce: True
+ panels:
+ - HI
+ - LOW
+ Hub Room:
+ entrances:
+ Second Room:
+ room: Second Room
+ door: Exit Door
+ Dead End Area:
+ door: Near RAT Door
+ Crossroads:
+ door: Crossroads Entrance
+ The Tenacious:
+ door: Tenacious Entrance
+ Near Far Area: True
+ Hedge Maze:
+ door: Shortcut to Hedge Maze
+ Orange Tower First Floor:
+ room: Orange Tower First Floor
+ door: Shortcut to Hub Room
+ Owl Hallway:
+ painting: True
+ Outside The Initiated:
+ room: Outside The Initiated
+ door: Shortcut to Hub Room
+ The Traveled:
+ door: Traveled Entrance
+ Roof: True # through the sunwarp
+ Outside The Undeterred:
+ room: Outside The Undeterred
+ door: Green Painting
+ painting: True
+ panels:
+ ORDER:
+ id: Shuffle Room/Panel_order_chaos
+ colors: black
+ tag: botblack
+ SLAUGHTER:
+ id: Palindrome Room/Panel_slaughter_laughter
+ colors: red
+ tag: midred
+ TRACE:
+ id: Maze Room/Panel_trace_trace
+ tag: midwhite
+ RAT:
+ id: Appendix Room/Panel_rat_tar
+ colors: black
+ check: True
+ exclude_reduce: True
+ tag: midblack
+ OPEN:
+ id: Synonym Room/Panel_open_open
+ tag: midwhite
+ FOUR:
+ id: Backside Room/Panel_four_four_3
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Outside The Undeterred
+ door: Fours
+ LOST:
+ id: Shuffle Room/Panel_lost_found
+ colors: black
+ tag: botblack
+ check: True
+ FORWARD:
+ id: Entry Room/Panel_forward_forward
+ tag: midwhite
+ BETWEEN:
+ id: Entry Room/Panel_between_between
+ tag: midwhite
+ BACKWARD:
+ id: Entry Room/Panel_backward_backward
+ tag: midwhite
+ doors:
+ Crossroads Entrance:
+ id:
+ - Shuffle Room Area Doors/Door_chaos
+ - Shuffle Room Area Doors/Door_swap
+ - Shuffle Room Area Doors/Door_swap2
+ - Shuffle Room Area Doors/Door_swap3
+ - Shuffle Room Area Doors/Door_swap4
+ panels:
+ - ORDER
+ Tenacious Entrance:
+ id: Palindrome Room Area Doors/Door_slaughter_laughter
+ door_group: Entrances to The Tenacious
+ item_group: Achievement Room Entrances
+ panels:
+ - SLAUGHTER
+ Shortcut to Hedge Maze:
+ id: Maze Area Doors/Door_trace_trace
+ door_group: Hedge Maze Doors
+ panels:
+ - TRACE
+ Near RAT Door:
+ id: Appendix Room Area Doors/Door_deadend_deadened
+ skip_location: True
+ door_group: Dead End Area Access
+ panels:
+ - room: Hidden Room
+ panel: DEAD END
+ Traveled Entrance:
+ id: Appendix Room Area Doors/Door_open_open
+ item_name: The Traveled - Entrance
+ door_group: Entrance to The Traveled
+ item_group: Achievement Room Entrances
+ panels:
+ - OPEN
+ panel_doors:
+ ORDER:
+ panels:
+ - ORDER
+ SLAUGHTER:
+ panel_group: Tenacious Entrance Panels
+ panels:
+ - SLAUGHTER
+ TRACE:
+ panels:
+ - TRACE
+ RAT:
+ panels:
+ - RAT
+ OPEN:
+ panels:
+ - OPEN
+ paintings:
+ - id: maze_painting
+ orientation: west
+ sunwarps:
+ - dots: 1
+ direction: enter
+ entrance_indicator_pos: [18, 2.5, -17.01]
+ orientation: north
+ Dead End Area:
+ entrances:
+ Hidden Room:
+ room: Hidden Room
+ door: Dead End Door
+ Hub Room:
+ room: Hub Room
+ door: Near RAT Door
+ panels:
+ FOUR:
+ id: Backside Room/Panel_four_four_2
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Outside The Undeterred
+ door: Fours
+ EIGHT:
+ id: Backside Room/Panel_eight_eight_8
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Eights
+ paintings:
+ - id: smile_painting_6
+ orientation: north
+ Sunwarps:
+ # This is a special, meta-ish room.
+ entrances:
+ Menu: True
+ doors:
+ 1 Sunwarp:
+ warp_id: Teleporter Warps/Sunwarp_enter_1
+ door_group: Sunwarps
+ skip_location: True
+ item_name: "1 Sunwarp"
+ 2 Sunwarp:
+ warp_id: Teleporter Warps/Sunwarp_enter_2
+ door_group: Sunwarps
+ skip_location: True
+ item_name: 2 Sunwarp
+ 3 Sunwarp:
+ warp_id: Teleporter Warps/Sunwarp_enter_3
+ door_group: Sunwarps
+ skip_location: True
+ item_name: "3 Sunwarp"
+ 4 Sunwarp:
+ warp_id: Teleporter Warps/Sunwarp_enter_4
+ door_group: Sunwarps
+ skip_location: True
+ item_name: 4 Sunwarp
+ 5 Sunwarp:
+ warp_id: Teleporter Warps/Sunwarp_enter_5
+ door_group: Sunwarps
+ skip_location: True
+ item_name: "5 Sunwarp"
+ 6 Sunwarp:
+ warp_id: Teleporter Warps/Sunwarp_enter_6
+ door_group: Sunwarps
+ skip_location: True
+ item_name: "6 Sunwarp"
+ progression:
+ Progressive Pilgrimage:
+ doors:
+ - 1 Sunwarp
+ - 2 Sunwarp
+ - 3 Sunwarp
+ - 4 Sunwarp
+ - 5 Sunwarp
+ - 6 Sunwarp
+ Pilgrim Antechamber:
+ # The entrances to this room are special. When pilgrimage is enabled, we use a special access rule to determine
+ # whether a pilgrimage can succeed. When pilgrimage is disabled, the sun painting will be added to the pool.
+ panels:
+ HOT CRUST:
+ id: Lingo Room/Panel_shortcut
+ colors: yellow
+ tag: midyellow
+ PILGRIM:
+ id: Lingo Room/Panel_pilgrim
+ colors: blue
+ tag: midblue
+ check: True
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery14
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ doors:
+ Sun Painting:
+ item_name: Pilgrim Room - Sun Painting
+ item_group: Paintings
+ location_name: Pilgrim Room - HOT CRUST
+ painting_id: pilgrim_painting2
+ panels:
+ - HOT CRUST
+ Exit:
+ event: True
+ panels:
+ - PILGRIM
+ Pilgrim Room:
+ entrances:
+ The Seeker:
+ door: Shortcut to The Seeker
+ Pilgrim Antechamber:
+ room: Pilgrim Antechamber
+ door: Exit
+ panels:
+ THIS:
+ id: Lingo Room/Panel_lingo_9
+ colors: gray
+ tag: forbid
+ TIME ROOM:
+ id: Lingo Room/Panel_lingo_1
+ colors: purple
+ tag: toppurp
+ SCIENCE ROOM:
+ id: Lingo Room/Panel_lingo_2
+ tag: botwhite
+ SHINY ROCK ROOM:
+ id: Lingo Room/Panel_lingo_3
+ tag: botwhite
+ ANGRY POWER:
+ id: Lingo Room/Panel_lingo_4
+ colors:
+ - purple
+ tag: forbid
+ MICRO LEGION:
+ id: Lingo Room/Panel_lingo_5
+ colors: yellow
+ tag: midyellow
+ LOSERS RELAX:
+ id: Lingo Room/Panel_lingo_6
+ colors:
+ - black
+ tag: forbid
+ "906234":
+ id: Lingo Room/Panel_lingo_7
+ colors:
+ - orange
+ - blue
+ tag: forbid
+ MOOR EMORDNILAP:
+ id: Lingo Room/Panel_lingo_8
+ colors: black
+ tag: midblack
+ HALL ROOMMATE:
+ id: Lingo Room/Panel_lingo_10
+ colors:
+ - red
+ - blue
+ tag: forbid
+ ALL GREY:
+ id: Lingo Room/Panel_lingo_11
+ colors: yellow
+ tag: midyellow
+ PLUNDER ISLAND:
+ id: Lingo Room/Panel_lingo_12
+ colors:
+ - purple
+ - red
+ tag: forbid
+ FLOSS PATHS:
+ id: Lingo Room/Panel_lingo_13
+ colors:
+ - purple
+ - brown
+ tag: forbid
+ doors:
+ Shortcut to The Seeker:
+ id: Master Room Doors/Door_pilgrim_shortcut
+ include_reduce: True
+ panels:
+ - THIS
+ Crossroads:
+ entrances:
+ Hub Room:
+ - room: Sunwarps
+ door: 1 Sunwarp
+ sunwarp: True
+ - room: Hub Room
+ door: Crossroads Entrance
+ Color Hallways:
+ warp: True
+ The Tenacious:
+ door: Tenacious Entrance
+ Orange Tower Fourth Floor:
+ - warp: True # through IRK HORN
+ - door: Tower Entrance
+ Amen Name Area:
+ room: Lost Area
+ door: Exit
+ Roof: True # through the sunwarp
+ panels:
+ DECAY:
+ id: Palindrome Room/Panel_decay_day
+ colors: red
+ tag: midred
+ NOPE:
+ id: Sun Room/Panel_nope_open
+ colors: yellow
+ tag: midyellow
+ EIGHT:
+ id: Backside Room/Panel_eight_eight_5
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Eights
+ WE ROT:
+ id: Shuffle Room/Panel_tower
+ colors: yellow
+ tag: midyellow
+ WORDS:
+ id: Shuffle Room/Panel_words_sword
+ colors: yellow
+ tag: midyellow
+ SWORD:
+ id: Shuffle Room/Panel_sword_words
+ colors: yellow
+ tag: midyellow
+ TURN:
+ id: Shuffle Room/Panel_turn_runt
+ colors: yellow
+ tag: midyellow
+ BEND HI:
+ id: Shuffle Room/Panel_behind
+ colors: yellow
+ tag: midyellow
+ THE EYES:
+ id: Shuffle Room/Panel_eyes_see_shuffle
+ colors: yellow
+ check: True
+ exclude_reduce: True
+ required_door:
+ door: Hollow Hallway
+ tag: midyellow
+ CORNER:
+ id: Shuffle Room/Panel_corner_corner
+ required_door:
+ door: Hollow Hallway
+ tag: midwhite
+ HOLLOW:
+ id: Shuffle Room/Panel_hollow_hollow
+ required_door:
+ door: Hollow Hallway
+ tag: midwhite
+ SWAP:
+ # In vanilla doors, solving this panel will open the way to Hub Room. This does not impact logic at all because
+ # Hub Room is always sphere 1 in vanilla doors.
+ id: Shuffle Room/Panel_swap_wasp
+ colors: yellow
+ tag: midyellow
+ GEL:
+ id: Shuffle Room/Panel_gel
+ colors: yellow
+ tag: topyellow
+ required_door:
+ door: Tower Entrance
+ THOUGH:
+ id: Shuffle Room/Panel_though
+ colors: yellow
+ tag: topyellow
+ required_door:
+ door: Tower Entrance
+ CROSSROADS:
+ id: Shuffle Room/Panel_crossroads_crossroads
+ tag: midwhite
+ doors:
+ Tenacious Entrance:
+ id: Palindrome Room Area Doors/Door_decay_day
+ door_group: Entrances to The Tenacious
+ item_group: Achievement Room Entrances
+ panels:
+ - DECAY
+ Discerning Entrance:
+ id: Shuffle Room Area Doors/Door_nope_open
+ item_name: The Discerning - Entrance
+ item_group: Achievement Room Entrances
+ panels:
+ - NOPE
+ Tower Entrance:
+ id:
+ - Shuffle Room Area Doors/Door_tower
+ - Shuffle Room Area Doors/Door_tower2
+ - Shuffle Room Area Doors/Door_tower3
+ - Shuffle Room Area Doors/Door_tower4
+ door_group: Crossroads - Tower Entrances
+ panels:
+ - WE ROT
+ Tower Back Entrance:
+ id: Shuffle Room Area Doors/Door_runt
+ location_name: Crossroads - TURN/RUNT
+ door_group: Crossroads - Tower Entrances
+ panels:
+ - TURN
+ - room: Orange Tower Fourth Floor
+ panel: RUNT (1)
+ Words Sword Door:
+ id:
+ - Shuffle Room Area Doors/Door_words_shuffle_3
+ - Shuffle Room Area Doors/Door_words_shuffle_4
+ door_group: Crossroads Doors
+ panels:
+ - WORDS
+ - SWORD
+ Eye Wall:
+ id: Shuffle Room Area Doors/Door_behind
+ door_group: Crossroads Doors
+ panels:
+ - BEND HI
+ Hollow Hallway:
+ id: Shuffle Room Area Doors/Door_crossroads6
+ skip_location: True
+ door_group: Crossroads Doors
+ panels:
+ - BEND HI
+ Roof Access:
+ id: Tower Room Area Doors/Door_level_6_2
+ skip_location: True
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ - room: Outside The Undeterred
+ panel: ART + ART
+ - room: Orange Tower Third Floor
+ panel: DEER + WREN
+ - room: Orange Tower Fourth Floor
+ panel: LEARNS + UNSEW
+ - room: Orange Tower Fifth Floor
+ panel: DRAWL + RUNS
+ - room: Owl Hallway
+ panel: READS + RUST
+ - room: Ending Area
+ panel: THE END
+ panel_doors:
+ DECAY:
+ panel_group: Tenacious Entrance Panels
+ panels:
+ - DECAY
+ NOPE:
+ panels:
+ - NOPE
+ WE ROT:
+ panels:
+ - WE ROT
+ WORDS SWORD:
+ panels:
+ - WORDS
+ - SWORD
+ BEND HI:
+ panels:
+ - BEND HI
+ paintings:
+ - id: eye_painting
+ disable: True
+ orientation: east
+ move: True
+ required_door:
+ door: Eye Wall
+ - id: smile_painting_4
+ orientation: south
+ sunwarps:
+ - dots: 1
+ direction: exit
+ entrance_indicator_pos: [ -17, 2.5, -41.01 ]
+ orientation: north
+ progression:
+ Progressive Suits Area:
+ panel_doors:
+ - WORDS SWORD
+ - room: Lost Area
+ panel_door: LOST
+ - room: Amen Name Area
+ panel_door: AMEN NAME
+ Lost Area:
+ entrances:
+ Outside The Agreeable:
+ door: Exit
+ Crossroads:
+ room: Crossroads
+ door: Words Sword Door
+ panels:
+ LOST (1):
+ id: Shuffle Room/Panel_lost_lots
+ colors: yellow
+ tag: midyellow
+ LOST (2):
+ id: Shuffle Room/Panel_lost_slot
+ colors: yellow
+ tag: midyellow
+ doors:
+ Exit:
+ id:
+ - Shuffle Room Area Doors/Door_lost_shuffle_1
+ - Shuffle Room Area Doors/Door_lost_shuffle_2
+ location_name: Crossroads - LOST Pair
+ panels:
+ - LOST (1)
+ - LOST (2)
+ panel_doors:
+ LOST:
+ panels:
+ - LOST (1)
+ - LOST (2)
+ Amen Name Area:
+ entrances:
+ Crossroads:
+ room: Lost Area
+ door: Exit
+ Suits Area:
+ door: Exit
+ panels:
+ AMEN:
+ id: Shuffle Room/Panel_amen_mean
+ colors: yellow
+ tag: double midyellow
+ subtag: left
+ link: ana MEAN
+ NAME:
+ id: Shuffle Room/Panel_name_mean
+ colors: yellow
+ tag: double midyellow
+ subtag: right
+ link: ana MEAN
+ NINE:
+ id: Backside Room/Panel_nine_nine_3
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Nines
+ doors:
+ Exit:
+ id: Shuffle Room Area Doors/Door_mean
+ panels:
+ - AMEN
+ - NAME
+ panel_doors:
+ AMEN NAME:
+ panels:
+ - AMEN
+ - NAME
+ Suits Area:
+ entrances:
+ Amen Name Area:
+ room: Amen Name Area
+ door: Exit
+ Roof: True
+ panels:
+ SPADES:
+ id: Cross Room/Panel_spades_spades
+ tag: midwhite
+ CLUBS:
+ id: Cross Room/Panel_clubs_clubs
+ tag: midwhite
+ HEARTS:
+ id: Cross Room/Panel_hearts_hearts
+ tag: midwhite
+ paintings:
+ - id: west_afar
+ orientation: south
+ The Tenacious:
+ entrances:
+ Hub Room:
+ - room: Hub Room
+ door: Tenacious Entrance
+ - door: Shortcut to Hub Room
+ Crossroads:
+ room: Crossroads
+ door: Tenacious Entrance
+ Outside The Agreeable:
+ room: Outside The Agreeable
+ door: Tenacious Entrance
+ Dread Hallway:
+ room: Dread Hallway
+ door: Tenacious Entrance
+ panels:
+ LEVEL (Black):
+ id: Palindrome Room/Panel_level_level
+ colors: black
+ tag: midblack
+ RACECAR (Black):
+ id: Palindrome Room/Panel_racecar_racecar
+ colors: black
+ tag: palindrome
+ copy_to_sign: sign4
+ SOLOS (Black):
+ id: Palindrome Room/Panel_solos_solos
+ colors: black
+ tag: palindrome
+ copy_to_sign:
+ - sign5
+ - sign6
+ LEVEL (White):
+ id: Palindrome Room/Panel_level_level_2
+ tag: midwhite
+ RACECAR (White):
+ id: Palindrome Room/Panel_racecar_racecar_2
+ tag: midwhite
+ copy_to_sign: sign3
+ SOLOS (White):
+ id: Palindrome Room/Panel_solos_solos_2
+ tag: midwhite
+ copy_to_sign:
+ - sign1
+ - sign2
+ Achievement:
+ id: Countdown Panels/Panel_tenacious_tenacious
+ check: True
+ tag: forbid
+ required_panel:
+ - panel: LEVEL (Black)
+ - panel: RACECAR (Black)
+ - panel: SOLOS (Black)
+ - panel: LEVEL (White)
+ - panel: RACECAR (White)
+ - panel: SOLOS (White)
+ - room: Hub Room
+ panel: SLAUGHTER
+ - room: Crossroads
+ panel: DECAY
+ - room: Outside The Agreeable
+ panel: MASSACRED
+ - room: Dread Hallway
+ panel: DREAD
+ achievement: The Tenacious
+ doors:
+ Shortcut to Hub Room:
+ id:
+ - Palindrome Room Area Doors/Door_level_level_1
+ - Palindrome Room Area Doors/Door_racecar_racecar_1
+ - Palindrome Room Area Doors/Door_solos_solos_1
+ location_name: The Tenacious - Palindromes
+ door_group: Entrances to The Tenacious
+ panels:
+ - LEVEL (Black)
+ - RACECAR (Black)
+ - SOLOS (Black)
+ White Palindromes:
+ location_name: The Tenacious - White Palindromes
+ skip_item: True
+ panels:
+ - LEVEL (White)
+ - RACECAR (White)
+ - SOLOS (White)
+ panel_doors:
+ Black Palindromes:
+ item_name: The Tenacious - Black Palindromes (Panels)
+ panels:
+ - LEVEL (Black)
+ - RACECAR (Black)
+ - SOLOS (Black)
+ Near Far Area:
+ entrances:
+ Hub Room: True
+ Warts Straw Area:
+ door: Door
+ panels:
+ NEAR:
+ id: Symmetry Room/Panel_near_far
+ colors: black
+ tag: botblack
+ FAR:
+ id: Symmetry Room/Panel_far_near
+ colors: black
+ tag: botblack
+ doors:
+ Door:
+ id:
+ - Symmetry Room Area Doors/Door_near_far
+ - Symmetry Room Area Doors/Door_far_near
+ door_group: Symmetry Doors
+ item_name: Symmetry Room - Near Far Door
+ location_name: Symmetry Room - NEAR, FAR
+ panels:
+ - NEAR
+ - FAR
+ panel_doors:
+ NEAR FAR:
+ item_name: Symmetry Room - NEAR, FAR (Panels)
+ panel_group: Symmetry Room Panels
+ panels:
+ - NEAR
+ - FAR
+ progression:
+ Progressive Symmetry Room:
+ panel_doors:
+ - NEAR FAR
+ - room: Warts Straw Area
+ panel_door: WARTS STRAW
+ - room: Leaf Feel Area
+ panel_door: LEAF FEEL
+ Warts Straw Area:
+ entrances:
+ Near Far Area:
+ room: Near Far Area
+ door: Door
+ Leaf Feel Area:
+ door: Door
+ panels:
+ WARTS:
+ id: Symmetry Room/Panel_warts_straw
+ colors: black
+ tag: midblack
+ STRAW:
+ id: Symmetry Room/Panel_straw_warts
+ colors: black
+ tag: midblack
+ doors:
+ Door:
+ id:
+ - Symmetry Room Area Doors/Door_warts_straw
+ - Symmetry Room Area Doors/Door_straw_warts
+ door_group: Symmetry Doors
+ item_name: Symmetry Room - Warts Straw Door
+ location_name: Symmetry Room - WARTS, STRAW
+ panels:
+ - WARTS
+ - STRAW
+ panel_doors:
+ WARTS STRAW:
+ item_name: Symmetry Room - WARTS, STRAW (Panels)
+ panel_group: Symmetry Room Panels
+ panels:
+ - WARTS
+ - STRAW
+ Leaf Feel Area:
+ entrances:
+ Warts Straw Area:
+ room: Warts Straw Area
+ door: Door
+ Outside The Agreeable:
+ door: Door
+ panels:
+ LEAF:
+ id: Symmetry Room/Panel_leaf_feel
+ colors: black
+ tag: topblack
+ FEEL:
+ id: Symmetry Room/Panel_feel_leaf
+ colors: black
+ tag: topblack
+ doors:
+ Door:
+ id:
+ - Symmetry Room Area Doors/Door_leaf_feel
+ - Symmetry Room Area Doors/Door_feel_leaf
+ door_group: Symmetry Doors
+ item_name: Symmetry Room - Leaf Feel Door
+ location_name: Symmetry Room - LEAF, FEEL
+ panels:
+ - LEAF
+ - FEEL
+ panel_doors:
+ LEAF FEEL:
+ item_name: Symmetry Room - LEAF, FEEL (Panels)
+ panel_group: Symmetry Room Panels
+ panels:
+ - LEAF
+ - FEEL
+ Outside The Agreeable:
+ entrances:
+ Crossroads:
+ warp: True
+ Lost Area:
+ room: Lost Area
+ door: Exit
+ The Tenacious:
+ door: Tenacious Entrance
+ The Agreeable:
+ door: Agreeable Entrance
+ Dread Hallway:
+ door: Black Door
+ Leaf Feel Area:
+ room: Leaf Feel Area
+ door: Door
+ Starting Room:
+ door: Painting Shortcut
+ painting: True
+ Hallway Room (1):
+ warp: True
+ Hedge Maze: True # through the door to the sectioned-off part of the hedge maze
+ Compass Room:
+ warp: True
+ panels:
+ MASSACRED:
+ id: Palindrome Room/Panel_massacred_sacred
+ colors: red
+ tag: midred
+ BLACK:
+ id: Symmetry Room/Panel_black_white
+ colors: black
+ tag: botblack
+ CLOSE:
+ id: Antonym Room/Panel_close_open
+ colors: black
+ tag: botblack
+ LEFT:
+ id: Symmetry Room/Panel_left_right
+ colors: black
+ tag: botblack
+ LEFT (2):
+ id: Symmetry Room/Panel_left_wrong
+ colors: black
+ tag: bot black black
+ RIGHT:
+ id: Symmetry Room/Panel_right_left
+ colors: black
+ tag: botblack
+ PURPLE:
+ id: Color Arrow Room/Panel_purple_afar
+ tag: midwhite
+ hunt: True
+ required_door:
+ door: Purple Barrier
+ FIVE (1):
+ id: Backside Room/Panel_five_five_5
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Outside The Undeterred
+ door: Fives
+ FIVE (2):
+ id: Backside Room/Panel_five_five_4
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Outside The Undeterred
+ door: Fives
+ HIDE:
+ id: Maze Room/Panel_hide_seek_4
+ colors: black
+ tag: botblack
+ DAZE:
+ id: Maze Room/Panel_daze_maze
+ colors: purple
+ tag: midpurp
+ doors:
+ Tenacious Entrance:
+ id: Palindrome Room Area Doors/Door_massacred_sacred
+ door_group: Entrances to The Tenacious
+ item_group: Achievement Room Entrances
+ panels:
+ - MASSACRED
+ Black Door:
+ id: Symmetry Room Area Doors/Door_black_white
+ door_group: Entrances to The Tenacious
+ panels:
+ - BLACK
+ Agreeable Entrance:
+ id: Symmetry Room Area Doors/Door_close_open
+ item_name: The Agreeable - Entrance
+ item_group: Achievement Room Entrances
+ panels:
+ - CLOSE
+ Painting Shortcut:
+ item_name: Starting Room - Street Painting
+ item_group: Paintings
+ painting_id: eyes_yellow_painting2
+ panels:
+ - RIGHT
+ Purple Barrier:
+ id: Color Arrow Room Doors/Door_purple_3
+ door_group: Color Hunt Barriers
+ skip_location: True
+ panels:
+ - room: Color Hunt
+ panel: PURPLE
+ panel_doors:
+ MASSACRED:
+ panel_group: Tenacious Entrance Panels
+ panels:
+ - MASSACRED
+ BLACK:
+ panels:
+ - BLACK
+ CLOSE:
+ panels:
+ - CLOSE
+ RIGHT:
+ panels:
+ - RIGHT
+ paintings:
+ - id: eyes_yellow_painting
+ orientation: east
+ sunwarps:
+ - dots: 6
+ direction: enter
+ entrance_indicator_pos: [ 3, 2.5, -55.01 ]
+ orientation: north
+ Compass Room:
+ entrances:
+ Outside The Agreeable:
+ warp: True
+ Cellar:
+ door: Lookout Entrance
+ warp: True
+ panels:
+ NORTH:
+ id: Cross Room/Panel_north_missing
+ colors: green
+ tag: forbid
+ required_panel:
+ - room: Outside The Bold
+ panel: SOUND
+ - room: Outside The Bold
+ panel: YEAST
+ - room: Outside The Bold
+ panel: WET
+ DIAMONDS:
+ id: Cross Room/Panel_diamonds_missing
+ colors: green
+ tag: forbid
+ required_room: Suits Area
+ FIRE:
+ id: Cross Room/Panel_fire_missing
+ colors: green
+ tag: forbid
+ required_room: Elements Area
+ WINTER:
+ id: Cross Room/Panel_winter_missing
+ colors: green
+ tag: forbid
+ required_room: Orange Tower Fifth Floor
+ doors:
+ Lookout Entrance:
+ id: Cross Room Doors/Door_missing
+ location_name: Outside The Agreeable - Lookout Panels
+ panels:
+ - NORTH
+ - WINTER
+ - DIAMONDS
+ - FIRE
+ panel_doors:
+ Lookout:
+ item_name: Compass Room Panels
+ panels:
+ - NORTH
+ - WINTER
+ - DIAMONDS
+ - FIRE
+ paintings:
+ - id: pencil_painting7
+ orientation: north
+ Dread Hallway:
+ entrances:
+ Outside The Agreeable:
+ room: Outside The Agreeable
+ door: Black Door
+ The Tenacious:
+ door: Tenacious Entrance
+ panels:
+ DREAD:
+ id: Palindrome Room/Panel_dread_dead
+ colors: red
+ tag: midred
+ doors:
+ Tenacious Entrance:
+ id: Palindrome Room Area Doors/Door_dread_dead
+ door_group: Entrances to The Tenacious
+ item_group: Achievement Room Entrances
+ panels:
+ - DREAD
+ The Agreeable:
+ entrances:
+ Outside The Agreeable:
+ room: Outside The Agreeable
+ door: Agreeable Entrance
+ Hedge Maze:
+ door: Shortcut to Hedge Maze
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_disagreeable_agreeable
+ colors: black
+ tag: forbid
+ required_room: Outside The Agreeable
+ check: True
+ achievement: The Agreeable
+ BYE:
+ id: Antonym Room/Panel_bye_hi
+ colors: black
+ tag: botblack
+ RETOOL:
+ id: Antonym Room/Panel_retool_looter
+ colors: black
+ tag: midblack
+ DRAWER:
+ id: Antonym Room/Panel_drawer_reward
+ colors: black
+ tag: midblack
+ READ:
+ id: Antonym Room/Panel_read_write
+ colors: black
+ tag: botblack
+ DIFFERENT:
+ id: Antonym Room/Panel_different_same
+ colors: black
+ tag: botblack
+ LOW:
+ id: Antonym Room/Panel_low_high
+ colors: black
+ tag: botblack
+ ALIVE:
+ id: Antonym Room/Panel_alive_dead
+ colors: black
+ tag: botblack
+ THAT:
+ id: Antonym Room/Panel_that_this
+ colors: black
+ tag: botblack
+ STRESSED:
+ id: Antonym Room/Panel_stressed_desserts
+ colors: black
+ tag: midblack
+ STAR:
+ id: Antonym Room/Panel_star_rats
+ colors: black
+ tag: midblack
+ TUBE:
+ id: Antonym Room/Panel_tame_mate
+ colors: black
+ tag: topblack
+ CAT:
+ id: Antonym Room/Panel_cat_tack
+ colors: black
+ tag: topblack
+ doors:
+ Shortcut to Hedge Maze:
+ id: Symmetry Room Area Doors/Door_bye_hi
+ item_group: Achievement Room Entrances
+ door_group: Hedge Maze Doors
+ panels:
+ - BYE
+ Hedge Maze:
+ entrances:
+ Hub Room:
+ room: Hub Room
+ door: Shortcut to Hedge Maze
+ Color Hallways:
+ warp: True
+ The Agreeable:
+ room: The Agreeable
+ door: Shortcut to Hedge Maze
+ The Perceptive: True
+ The Observant:
+ door: Observant Entrance
+ Owl Hallway:
+ room: Owl Hallway
+ door: Shortcut to Hedge Maze
+ Roof: True
+ The Incomparable:
+ door: Observant Entrance
+ panels:
+ DOWN:
+ id: Maze Room/Panel_down_up
+ colors: black
+ tag: botblack
+ HIDE (1):
+ id: Maze Room/Panel_hide_seek
+ colors: black
+ tag: botblack
+ HIDE (2):
+ id: Maze Room/Panel_hide_seek_2
+ colors: black
+ tag: botblack
+ HIDE (3):
+ id: Maze Room/Panel_hide_seek_3
+ colors: black
+ tag: botblack
+ MASTERY (1):
+ id: Master Room/Panel_mastery_mastery5
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ MASTERY (2):
+ id: Master Room/Panel_mastery_mastery9
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ PATH (1):
+ id: Maze Room/Panel_path_lock
+ colors: green
+ tag: forbid
+ PATH (2):
+ id: Maze Room/Panel_path_knot
+ colors: green
+ tag: forbid
+ PATH (3):
+ id: Maze Room/Panel_path_lost
+ colors: green
+ tag: forbid
+ PATH (4):
+ id: Maze Room/Panel_path_open
+ colors: green
+ tag: forbid
+ PATH (5):
+ id: Maze Room/Panel_path_help
+ colors: green
+ tag: forbid
+ PATH (6):
+ id: Maze Room/Panel_path_hunt
+ colors: green
+ tag: forbid
+ PATH (7):
+ id: Maze Room/Panel_path_nest
+ colors: green
+ tag: forbid
+ PATH (8):
+ id: Maze Room/Panel_path_look
+ colors: green
+ tag: forbid
+ REFLOW:
+ id: Maze Room/Panel_reflow_flower
+ colors: yellow
+ tag: midyellow
+ LEAP:
+ id: Maze Room/Panel_leap_jump
+ tag: botwhite
+ doors:
+ Perceptive Entrance:
+ id: Maze Area Doors/Door_maze_maze
+ item_name: The Perceptive - Entrance
+ door_group: Hedge Maze Doors
+ item_group: Achievement Room Entrances
+ panels:
+ - DOWN
+ Painting Shortcut:
+ painting_id: garden_painting_tower2
+ item_name: Starting Room - Hedge Maze Painting
+ item_group: Paintings
+ skip_location: True
+ panels:
+ - DOWN
+ Observant Entrance:
+ id:
+ - Maze Area Doors/Door_look_room_1
+ - Maze Area Doors/Door_look_room_2
+ - Maze Area Doors/Door_look_room_3
+ skip_location: True
+ item_name: The Observant - Entrance
+ door_group: Observant Doors
+ item_group: Achievement Room Entrances
+ panels:
+ - room: The Perceptive
+ panel: GAZE
+ Hide and Seek:
+ skip_item: True
+ location_name: Hedge Maze - Hide and Seek
+ include_reduce: True
+ panels:
+ - HIDE (1)
+ - HIDE (2)
+ - HIDE (3)
+ - room: Outside The Agreeable
+ panel: HIDE
+ panel_doors:
+ DOWN:
+ panels:
+ - DOWN
+ The Perceptive:
+ entrances:
+ Starting Room:
+ room: Hedge Maze
+ door: Painting Shortcut
+ painting: True
+ Hedge Maze:
+ room: Hedge Maze
+ door: Perceptive Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_perceptive_perceptive
+ colors: green
+ tag: forbid
+ check: True
+ achievement: The Perceptive
+ GAZE:
+ id: Maze Room/Panel_look_look
+ check: True
+ exclude_reduce: True
+ tag: botwhite
+ panel_doors:
+ GAZE:
+ panels:
+ - GAZE
+ paintings:
+ - id: garden_painting_tower
+ orientation: north
+ The Fearless (First Floor):
+ entrances:
+ The Perceptive:
+ warp: True
+ panels:
+ SPAN:
+ id: Naps Room/Panel_naps_span
+ colors: black
+ tag: midblack
+ TEAM:
+ id: Naps Room/Panel_team_meet
+ colors: black
+ tag: topblack
+ TEEM:
+ id: Naps Room/Panel_teem_meat
+ colors: black
+ tag: topblack
+ IMPATIENT:
+ id: Naps Room/Panel_impatient_doctor
+ colors: black
+ tag: bot black black
+ EAT:
+ id: Naps Room/Panel_eat_tea
+ colors: black
+ tag: topblack
+ doors:
+ Second Floor:
+ id: Naps Room Doors/Door_hider_5
+ location_name: The Fearless - First Floor Puzzles
+ door_group: Fearless Doors
+ panels:
+ - SPAN
+ - TEAM
+ - TEEM
+ - IMPATIENT
+ - EAT
+ progression:
+ Progressive Fearless:
+ doors:
+ - Second Floor
+ - room: The Fearless (Second Floor)
+ door: Third Floor
+ The Fearless (Second Floor):
+ entrances:
+ The Fearless (First Floor):
+ room: The Fearless (First Floor)
+ door: Second Floor
+ warp: True
+ panels:
+ NONE:
+ id: Naps Room/Panel_one_many
+ colors: black
+ tag: bot black top white
+ SUM:
+ id: Naps Room/Panel_one_none
+ colors: black
+ tag: top white bot black
+ FUNNY:
+ id: Naps Room/Panel_funny_enough
+ colors: black
+ tag: topblack
+ MIGHT:
+ id: Naps Room/Panel_might_time
+ colors: black
+ tag: topblack
+ SAFE:
+ id: Naps Room/Panel_safe_face
+ colors: black
+ tag: topblack
+ SAME:
+ id: Naps Room/Panel_same_mace
+ colors: black
+ tag: topblack
+ CAME:
+ id: Naps Room/Panel_came_make
+ colors: black
+ tag: topblack
+ doors:
+ Third Floor:
+ id:
+ - Naps Room Doors/Door_hider_1b2
+ - Naps Room Doors/Door_hider_new1
+ location_name: The Fearless - Second Floor Puzzles
+ door_group: Fearless Doors
+ panels:
+ - NONE
+ - SUM
+ - FUNNY
+ - MIGHT
+ - SAFE
+ - SAME
+ - CAME
+ The Fearless:
+ entrances:
+ The Fearless (First Floor):
+ room: The Fearless (Second Floor)
+ door: Third Floor
+ warp: True
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_fearless_fearless
+ colors: black
+ tag: forbid
+ check: True
+ achievement: The Fearless
+ EASY:
+ id: Naps Room/Panel_easy_soft
+ colors: black
+ tag: bot black black
+ SOMETIMES:
+ id: Naps Room/Panel_sometimes_always
+ colors: black
+ tag: bot black black
+ DARK:
+ id: Naps Room/Panel_dark_extinguish
+ colors: black
+ tag: bot black black
+ EVEN:
+ id: Naps Room/Panel_even_ordinary
+ colors: black
+ tag: bot black black
+ The Observant:
+ entrances:
+ Hedge Maze:
+ room: Hedge Maze
+ door: Observant Entrance
+ The Incomparable:
+ warp: True
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_observant_observant
+ colors: green
+ check: True
+ tag: forbid
+ required_door:
+ door: Stairs
+ required_panel:
+ - panel: FOUR (1)
+ - panel: FOUR (2)
+ - panel: SIX
+ achievement: The Observant
+ FOUR (1):
+ id: Look Room/Panel_four_back
+ colors: green
+ tag: forbid
+ FOUR (2):
+ id: Look Room/Panel_four_side
+ colors: green
+ tag: forbid
+ BACKSIDE:
+ id: Backside Room/Panel_backside_2
+ tag: midwhite
+ hunt: True
+ required_door:
+ door: Backside Door
+ SIX:
+ id: Look Room/Panel_six_stairs
+ colors: green
+ tag: forbid
+ FOUR (3):
+ id: Look Room/Panel_four_ways
+ colors: green
+ tag: forbid
+ TWO (1):
+ id: Look Room/Panel_two_on
+ colors: green
+ tag: forbid
+ TWO (2):
+ id: Look Room/Panel_two_up
+ colors: green
+ tag: forbid
+ FIVE:
+ id: Look Room/Panel_five_swims
+ colors: green
+ tag: forbid
+ BELOW (1):
+ id: Look Room/Panel_eight_upstairs
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ BLUE:
+ id: Look Room/Panel_blue_toil
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ BELOW (2):
+ id: Look Room/Panel_four_stop
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ MINT (1):
+ id: Look Room/Panel_aqua_top
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ ESACREWOL:
+ id: Look Room/Panel_blue_hi
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ EULB:
+ id: Look Room/Panel_blue_hi2
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ NUMBERS (1):
+ id: Look Room/Panel_numbers_31
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ NUMBERS (2):
+ id: Look Room/Panel_numbers_52
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ MINT (2):
+ id: Look Room/Panel_aqua_oil
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ GREEN (1):
+ id: Look Room/Panel_eight_backside
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ GREEN (2):
+ id: Look Room/Panel_eight_sideways
+ colors: green
+ tag: forbid
+ required_door:
+ door: Stairs
+ doors:
+ Backside Door:
+ id: Maze Area Doors/Door_backside
+ door_group: Backside Doors
+ panels:
+ - FOUR (1)
+ - FOUR (2)
+ Stairs:
+ id: Maze Area Doors/Door_stairs
+ door_group: Observant Doors
+ panels:
+ - SIX
+ panel_doors:
+ BACKSIDE:
+ item_name: The Observant - Backside Entrance Panels
+ panel_group: Backside Entrance Panels
+ panels:
+ - FOUR (1)
+ - FOUR (2)
+ STAIRS:
+ panels:
+ - SIX
+ The Incomparable:
+ entrances:
+ The Observant:
+ warp: True
+ Eight Room: True
+ Eight Alcove:
+ door: Eight Door
+ Orange Tower Sixth Floor:
+ painting: True
+ Hedge Maze:
+ room: Hedge Maze
+ door: Observant Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_incomparable_incomparable
+ colors: blue
+ check: True
+ tag: forbid
+ required_room:
+ - Eight Room
+ required_panel:
+ - room: Courtyard
+ panel: I
+ - room: Elements Area
+ panel: A
+ achievement: The Incomparable
+ A (One):
+ id: Strand Room/Panel_blank_a
+ colors: blue
+ tag: forbid
+ A (Two):
+ id: Strand Room/Panel_a_an
+ colors: blue
+ tag: forbid
+ A (Three):
+ id: Strand Room/Panel_a_and
+ colors: blue
+ tag: forbid
+ A (Four):
+ id: Strand Room/Panel_a_sand
+ colors: blue
+ tag: forbid
+ A (Five):
+ id: Strand Room/Panel_a_stand
+ colors: blue
+ tag: forbid
+ A (Six):
+ id: Strand Room/Panel_a_strand
+ colors: blue
+ tag: forbid
+ I (One):
+ id: Strand Room/Panel_blank_i
+ colors: blue
+ tag: forbid
+ I (Two):
+ id: Strand Room/Panel_i_in
+ colors: blue
+ tag: forbid
+ I (Three):
+ id: Strand Room/Panel_i_sin
+ colors: blue
+ tag: forbid
+ I (Four):
+ id: Strand Room/Panel_i_sing
+ colors: blue
+ tag: forbid
+ I (Five):
+ id: Strand Room/Panel_i_sting
+ colors: blue
+ tag: forbid
+ I (Six):
+ id: Strand Room/Panel_i_string
+ colors: blue
+ tag: forbid
+ I (Seven):
+ id: Strand Room/Panel_i_strings
+ colors: blue
+ tag: forbid
+ doors:
+ Eight Door:
+ id: Red Blue Purple Room Area Doors/Door_a_strands
+ location_name: Giant Sevens
+ door_group: Observant Doors
+ panels:
+ - I (Seven)
+ - room: Courtyard
+ panel: I
+ - room: Elements Area
+ panel: A
+ panel_doors:
+ Giant Sevens:
+ item_name: Giant Seven Panels
+ panels:
+ - I (Seven)
+ - room: Courtyard
+ panel: I
+ - room: Elements Area
+ panel: A
+ paintings:
+ - id: crown_painting
+ orientation: east
+ Eight Alcove:
+ entrances:
+ The Incomparable:
+ room: The Incomparable
+ door: Eight Door
+ Outside The Initiated:
+ room: Outside The Initiated
+ door: Eight Door
+ paintings:
+ - id: eight_painting2
+ orientation: north
+ Eight Room:
+ entrances:
+ Eight Alcove:
+ painting: True
+ panels:
+ Eight Back:
+ id: Strand Room/Panel_i_starling
+ colors: blue
+ tag: forbid
+ Eight Front:
+ id: Strand Room/Panel_i_starting
+ colors: blue
+ tag: forbid
+ Nine:
+ id: Strand Room/Panel_i_startling
+ colors: blue
+ tag: forbid
+ paintings:
+ - id: eight_painting
+ orientation: south
+ exit_only: True
+ required: True
+ Orange Tower:
+ # This is a special, meta-ish room.
+ entrances:
+ Menu: True
+ doors:
+ Second Floor:
+ id: Tower Room Area Doors/Door_level_1
+ skip_location: True
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ Third Floor:
+ id: Tower Room Area Doors/Door_level_2
+ skip_location: True
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ - room: Outside The Undeterred
+ panel: ART + ART
+ Fourth Floor:
+ id: Tower Room Area Doors/Door_level_3
+ skip_location: True
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ - room: Outside The Undeterred
+ panel: ART + ART
+ - room: Orange Tower Third Floor
+ panel: DEER + WREN
+ Fifth Floor:
+ id: Tower Room Area Doors/Door_level_4
+ skip_location: True
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ - room: Outside The Undeterred
+ panel: ART + ART
+ - room: Orange Tower Third Floor
+ panel: DEER + WREN
+ - room: Orange Tower Fourth Floor
+ panel: LEARNS + UNSEW
+ Sixth Floor:
+ id: Tower Room Area Doors/Door_level_5
+ skip_location: True
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ - room: Outside The Undeterred
+ panel: ART + ART
+ - room: Orange Tower Third Floor
+ panel: DEER + WREN
+ - room: Orange Tower Fourth Floor
+ panel: LEARNS + UNSEW
+ - room: Orange Tower Fifth Floor
+ panel: DRAWL + RUNS
+ Seventh Floor:
+ id: Tower Room Area Doors/Door_level_6
+ skip_location: True
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ - room: Outside The Undeterred
+ panel: ART + ART
+ - room: Orange Tower Third Floor
+ panel: DEER + WREN
+ - room: Orange Tower Fourth Floor
+ panel: LEARNS + UNSEW
+ - room: Orange Tower Fifth Floor
+ panel: DRAWL + RUNS
+ - room: Owl Hallway
+ panel: READS + RUST
+ panel_doors:
+ Access:
+ item_name: Orange Tower Panels
+ panels:
+ - room: Orange Tower First Floor
+ panel: DADS + ALE
+ - room: Outside The Undeterred
+ panel: ART + ART
+ - room: Orange Tower Third Floor
+ panel: DEER + WREN
+ - room: Orange Tower Fourth Floor
+ panel: LEARNS + UNSEW
+ - room: Orange Tower Fifth Floor
+ panel: DRAWL + RUNS
+ - room: Owl Hallway
+ panel: READS + RUST
+ progression:
+ Progressive Orange Tower:
+ doors:
+ - Second Floor
+ - Third Floor
+ - Fourth Floor
+ - Fifth Floor
+ - Sixth Floor
+ - Seventh Floor
+ Orange Tower First Floor:
+ entrances:
+ Hub Room:
+ door: Shortcut to Hub Room
+ Outside The Wanderer:
+ room: Outside The Wanderer
+ door: Tower Entrance
+ warp: True
+ Orange Tower Second Floor:
+ room: Orange Tower
+ door: Second Floor
+ warp: True
+ Directional Gallery:
+ door: Salt Pepper Door
+ Roof: True # through the sunwarp
+ panels:
+ SECRET:
+ id: Shuffle Room/Panel_secret_secret
+ tag: midwhite
+ DADS + ALE:
+ id: Tower Room/Panel_dads_ale_dead_1
+ colors: orange
+ check: True
+ tag: midorange
+ SALT:
+ id: Backside Room/Panel_salt_pepper
+ colors: black
+ tag: botblack
+ doors:
+ Shortcut to Hub Room:
+ id: Shuffle Room Area Doors/Door_secret_secret
+ door_group: Orange Tower First Floor - Shortcuts
+ panels:
+ - SECRET
+ Salt Pepper Door:
+ id: Count Up Room Area Doors/Door_salt_pepper
+ location_name: Orange Tower First Floor - Salt Pepper Door
+ door_group: Orange Tower First Floor - Shortcuts
+ panels:
+ - SALT
+ - room: Directional Gallery
+ panel: PEPPER
+ panel_doors:
+ SECRET:
+ panels:
+ - SECRET
+ sunwarps:
+ - dots: 4
+ direction: enter
+ entrance_indicator_pos: [ -32, 2.5, -14.99 ]
+ orientation: south
+ Orange Tower Second Floor:
+ entrances:
+ Orange Tower First Floor:
+ room: Orange Tower
+ door: Second Floor
+ warp: True
+ Orange Tower Third Floor:
+ room: Orange Tower
+ door: Third Floor
+ warp: True
+ Outside The Undeterred:
+ warp: True
+ Orange Tower Third Floor:
+ entrances:
+ Knight Night Exit:
+ room: Knight Night (Final)
+ door: Exit
+ Orange Tower Second Floor:
+ room: Orange Tower
+ door: Third Floor
+ warp: True
+ Orange Tower Fourth Floor:
+ room: Orange Tower
+ door: Fourth Floor
+ warp: True
+ Hot Crusts Area:
+ room: Sunwarps
+ door: 2 Sunwarp
+ sunwarp: True
+ Bearer Side Area:
+ room: Bearer Side Area
+ door: Shortcut to Tower
+ Rhyme Room (Smiley):
+ door: Rhyme Room Entrance
+ Art Gallery:
+ warp: True
+ Roof: True # by parkouring through the Bearer shortcut
+ panels:
+ RED:
+ id: Color Arrow Room/Panel_red_afar
+ tag: midwhite
+ hunt: True
+ required_door:
+ door: Red Barrier
+ DEER + WREN:
+ id: Tower Room/Panel_deer_wren_rats_3
+ colors: orange
+ check: True
+ tag: midorange
+ doors:
+ Red Barrier:
+ id: Color Arrow Room Doors/Door_red_6
+ door_group: Color Hunt Barriers
+ skip_location: True
+ panels:
+ - room: Color Hunt
+ panel: RED
+ Rhyme Room Entrance:
+ id: Double Room Area Doors/Door_room_entry_stairs2
+ skip_location: True
+ door_group: Rhyme Room Doors
+ panels:
+ - room: The Tenacious
+ panel: LEVEL (Black)
+ - room: The Tenacious
+ panel: RACECAR (Black)
+ - room: The Tenacious
+ panel: SOLOS (Black)
+ Orange Barrier: # see note in Outside The Initiated
+ id:
+ - Color Arrow Room Doors/Door_orange_hider_1
+ - Color Arrow Room Doors/Door_orange_hider_2
+ - Color Arrow Room Doors/Door_orange_hider_3
+ location_name: Color Barriers - RED and YELLOW
+ door_group: Color Hunt Barriers
+ item_name: Color Hunt - Orange Barrier
+ panels:
+ - RED
+ - room: Directional Gallery
+ panel: YELLOW
+ paintings:
+ - id: arrows_painting_6
+ orientation: east
+ - id: flower_painting_5
+ orientation: south
+ sunwarps:
+ - dots: 2
+ direction: exit
+ entrance_indicator_pos: [ 24.01, 2.5, 38 ]
+ orientation: west
+ - dots: 3
+ direction: enter
+ entrance_indicator_pos: [ 28.01, 2.5, 29 ]
+ orientation: west
+ Orange Tower Fourth Floor:
+ entrances:
+ Orange Tower Third Floor:
+ room: Orange Tower
+ door: Fourth Floor
+ warp: True
+ Orange Tower Fifth Floor:
+ room: Orange Tower
+ door: Fifth Floor
+ warp: True
+ Hot Crusts Area:
+ door: Hot Crusts Door
+ Crossroads:
+ - room: Crossroads
+ door: Tower Entrance
+ - room: Crossroads
+ door: Tower Back Entrance
+ Courtyard:
+ - warp: True
+ - room: Crossroads
+ door: Tower Entrance
+ Roof: True # through the sunwarp
+ panels:
+ RUNT (1):
+ id: Shuffle Room/Panel_turn_runt2
+ colors: yellow
+ tag: midyellow
+ RUNT (2):
+ id: Shuffle Room/Panel_runt3
+ colors:
+ - yellow
+ - blue
+ tag: mid yellow blue
+ LEARNS + UNSEW:
+ id: Tower Room/Panel_learns_unsew_unrest_4
+ colors: orange
+ check: True
+ tag: midorange
+ HOT CRUSTS:
+ id: Shuffle Room/Panel_shortcuts
+ colors: yellow
+ tag: midyellow
+ IRK HORN:
+ id: Shuffle Room/Panel_corner
+ colors: yellow
+ check: True
+ exclude_reduce: True
+ tag: topyellow
+ doors:
+ Hot Crusts Door:
+ id: Shuffle Room Area Doors/Door_hotcrust_shortcuts
+ panels:
+ - HOT CRUSTS
+ panel_doors:
+ HOT CRUSTS:
+ panels:
+ - HOT CRUSTS
+ sunwarps:
+ - dots: 5
+ direction: enter
+ entrance_indicator_pos: [ -20, 3, -64.01 ]
+ orientation: north
+ Hot Crusts Area:
+ entrances:
+ Orange Tower Fourth Floor:
+ room: Orange Tower Fourth Floor
+ door: Hot Crusts Door
+ Roof: True # through the sunwarp
+ panels:
+ EIGHT:
+ id: Backside Room/Panel_eight_eight_3
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Eights
+ paintings:
+ - id: smile_painting_8
+ orientation: north
+ sunwarps:
+ - dots: 2
+ direction: enter
+ entrance_indicator_pos: [ -26, 3.5, -80.01 ]
+ orientation: north
+ Orange Tower Fifth Floor:
+ entrances:
+ Orange Tower Fourth Floor:
+ room: Orange Tower
+ door: Fifth Floor
+ warp: True
+ Orange Tower Sixth Floor:
+ room: Orange Tower
+ door: Sixth Floor
+ warp: True
+ Cellar:
+ room: Room Room
+ door: Cellar Exit
+ warp: True
+ Welcome Back Area:
+ door: Welcome Back
+ Outside The Initiated:
+ room: Art Gallery
+ door: Exit
+ warp: True
+ panels:
+ SIZE (Small):
+ id: Entry Room/Panel_size_small
+ colors: gray
+ tag: forbid
+ SIZE (Big):
+ id: Entry Room/Panel_size_big
+ colors: gray
+ tag: forbid
+ DRAWL + RUNS:
+ id: Tower Room/Panel_drawl_runs_enter_5
+ colors: orange
+ check: True
+ tag: midorange
+ NINE:
+ id: Backside Room/Panel_nine_nine_2
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Nines
+ SUMMER:
+ id: Entry Room/Panel_summer_summer
+ tag: midwhite
+ AUTUMN:
+ id: Entry Room/Panel_autumn_autumn
+ tag: midwhite
+ SPRING:
+ id: Entry Room/Panel_spring_spring
+ tag: midwhite
+ PAINTING (1):
+ id: Panel Room/Panel_painting_flower
+ colors: green
+ tag: forbid
+ required_room: Cellar
+ PAINTING (2):
+ id: Panel Room/Panel_painting_eye
+ colors: green
+ tag: forbid
+ required_room: Cellar
+ PAINTING (3):
+ id: Panel Room/Panel_painting_snowman
+ colors: green
+ tag: forbid
+ required_room: Cellar
+ PAINTING (4):
+ id: Panel Room/Panel_painting_owl
+ colors: green
+ tag: forbid
+ required_room: Cellar
+ PAINTING (5):
+ id: Panel Room/Panel_painting_panda
+ colors: green
+ tag: forbid
+ required_room: Cellar
+ ROOM:
+ id: Panel Room/Panel_room_stairs
+ colors: gray
+ tag: forbid
+ required_room: Cellar
+ doors:
+ Welcome Back:
+ id: Entry Room Area Doors/Door_sizes
+ door_group: Welcome Back Doors
+ panels:
+ - SIZE (Small)
+ - SIZE (Big)
+ panel_doors:
+ SIZE:
+ item_name: Orange Tower Fifth Floor - SIZE Panels
+ panels:
+ - SIZE (Small)
+ - SIZE (Big)
+ paintings:
+ - id: hi_solved_painting3
+ orientation: south
+ - id: hi_solved_painting2
+ orientation: south
+ - id: east_afar
+ orientation: north
+ Orange Tower Sixth Floor:
+ entrances:
+ Orange Tower Fifth Floor:
+ room: Orange Tower
+ door: Sixth Floor
+ warp: True
+ The Scientific:
+ painting: True
+ paintings:
+ - id: arrows_painting_10
+ orientation: east
+ - id: owl_painting_3
+ orientation: north
+ - id: clock_painting
+ orientation: west
+ - id: scenery_painting_5d_2
+ orientation: south
+ - id: symmetry_painting_b_7
+ orientation: north
+ - id: panda_painting_2
+ orientation: south
+ - id: crown_painting2
+ orientation: north
+ - id: colors_painting2
+ orientation: south
+ - id: cherry_painting2
+ orientation: east
+ - id: hi_solved_painting
+ orientation: west
+ Ending Area:
+ entrances:
+ Orange Tower Sixth Floor:
+ room: Orange Tower
+ door: Seventh Floor
+ warp: True
+ panels:
+ THE END:
+ id: EndPanel/Panel_end_end
+ check: True
+ tag: forbid
+ non_counting: True
+ location_name: Orange Tower Seventh Floor - THE END
+ doors:
+ End:
+ event: True
+ panels:
+ - THE END
+ Orange Tower Seventh Floor:
+ entrances:
+ Ending Area:
+ room: Ending Area
+ door: End
+ panels:
+ THE MASTER:
+ # We will set up special rules for this in code.
+ id: Countdown Panels/Panel_master_master
+ check: True
+ tag: forbid
+ MASTERY:
+ # This is the MASTERY on the other side of THE FEARLESS. It can only be
+ # accessed by jumping from the top of the tower.
+ id: Master Room/Panel_mastery_mastery8
+ location_name: The Fearless - MASTERY
+ tag: midwhite
+ hunt: True
+ required_door:
+ door: Mastery
+ doors:
+ Mastery:
+ id:
+ - Master Room Doors/Door_tower_down
+ - Master Room Doors/Door_master_master
+ - Master Room Doors/Door_master_master_2
+ - Master Room Doors/Door_master_master_3
+ - Master Room Doors/Door_master_master_4
+ - Master Room Doors/Door_master_master_5
+ - Master Room Doors/Door_master_master_6
+ - Master Room Doors/Door_master_master_10
+ - Master Room Doors/Door_master_master_11
+ - Master Room Doors/Door_master_master_12
+ - Master Room Doors/Door_master_master_13
+ - Master Room Doors/Door_master_master_14
+ - Master Room Doors/Door_master_master_15
+ - Master Room Doors/Door_master_down
+ - Master Room Doors/Door_master_down2
+ skip_location: True
+ item_name: Mastery
+ panels:
+ - THE MASTER
+ Mastery Panels:
+ skip_item: True
+ location_name: Mastery Panels
+ panels:
+ - room: Room Room
+ panel: MASTERY
+ - room: The Steady (Topaz)
+ panel: MASTERY
+ - room: Orange Tower Basement
+ panel: MASTERY
+ - room: Arrow Garden
+ panel: MASTERY
+ - room: Hedge Maze
+ panel: MASTERY (1)
+ - room: Behind A Smile
+ panel: MASTERY
+ - room: Sixteen Colorful Squares
+ panel: MASTERY
+ - MASTERY
+ - room: Hedge Maze
+ panel: MASTERY (2)
+ - room: Among Treetops
+ panel: MASTERY
+ - room: Horizon's Edge
+ panel: MASTERY
+ - room: Beneath The Lookout
+ panel: MASTERY
+ - room: Elements Area
+ panel: MASTERY
+ - room: Pilgrim Antechamber
+ panel: MASTERY
+ - room: Rooftop Staircase
+ panel: MASTERY
+ paintings:
+ - id: map_painting2
+ orientation: north
+ enter_only: True # otherwise you might just skip the whole game!
+ req_blocked_when_no_doors: True # owl hallway in vanilla doors
+ Roof:
+ entrances:
+ Orange Tower Seventh Floor: True
+ Crossroads:
+ room: Crossroads
+ door: Roof Access
+ Behind A Smile:
+ entrances:
+ Roof: True
+ panels:
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery6
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ STAIRCASE:
+ id: Open Areas/Panel_staircase
+ tag: midwhite
+ Sixteen Colorful Squares:
+ entrances:
+ Roof: True
+ panels:
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery7
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ Among Treetops:
+ entrances:
+ Roof: True
+ panels:
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery10
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ Horizon's Edge:
+ entrances:
+ Roof: True
+ panels:
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery11
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ Beneath The Lookout:
+ entrances:
+ Roof: True
+ panels:
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery12
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ Rooftop Staircase:
+ entrances:
+ Roof: True
+ panels:
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery15
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ Orange Tower Basement:
+ entrances:
+ Orange Tower Sixth Floor:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ panels:
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery3
+ tag: midwhite
+ hunt: True
+ THE LIBRARY:
+ id: EndPanel/Panel_library
+ check: True
+ tag: forbid
+ non_counting: True
+ paintings:
+ - id: arrows_painting_11
+ orientation: east
+ req_blocked_when_no_doors: True # owl hallway in vanilla doors
+ Courtyard:
+ entrances:
+ Roof: True
+ Orange Tower Fourth Floor:
+ - warp: True
+ - room: Crossroads
+ door: Tower Entrance
+ Arrow Garden:
+ painting: True
+ Starting Room:
+ door: Painting Shortcut
+ painting: True
+ Yellow Backside Area:
+ room: First Second Third Fourth
+ door: Backside Door
+ The Colorful (White): True
+ panels:
+ I:
+ id: Strand Room/Panel_i_staring
+ colors: blue
+ tag: forbid
+ hunt: True
+ GREEN:
+ id: Color Arrow Room/Panel_green_afar
+ tag: midwhite
+ hunt: True
+ required_door:
+ door: Green Barrier
+ PINECONE:
+ id: Shuffle Room/Panel_pinecone_pine
+ colors: brown
+ tag: botbrown
+ ACORN:
+ id: Shuffle Room/Panel_acorn_oak
+ colors: brown
+ tag: botbrown
+ doors:
+ Painting Shortcut:
+ painting_id: flower_painting_8
+ item_name: Starting Room - Flower Painting
+ item_group: Paintings
+ skip_location: True
+ panels:
+ - room: First Second Third Fourth
+ panel: FIRST
+ - room: First Second Third Fourth
+ panel: SECOND
+ - room: First Second Third Fourth
+ panel: THIRD
+ - room: First Second Third Fourth
+ panel: FOURTH
+ Green Barrier:
+ id: Color Arrow Room Doors/Door_green_5
+ door_group: Color Hunt Barriers
+ skip_location: True
+ panels:
+ - room: Color Hunt
+ panel: GREEN
+ paintings:
+ - id: flower_painting_7
+ orientation: north
+ Yellow Backside Area:
+ entrances:
+ Courtyard:
+ room: First Second Third Fourth
+ door: Backside Door
+ Roof: True
+ panels:
+ BACKSIDE:
+ id: Backside Room/Panel_backside_3
+ tag: midwhite
+ hunt: True
+ NINE:
+ id: Backside Room/Panel_nine_nine_8
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Nines
+ paintings:
+ - id: blueman_painting
+ orientation: east
+ First Second Third Fourth:
+ # We are separating this door + its panels into its own room because they
+ # are accessible from two distinct regions (Courtyard and Yellow Backside
+ # Area). We need to do this because painting shuffle makes it possible to
+ # have access to Yellow Backside Area without having access to Courtyard,
+ # and we want it to still be in logic to solve these panels.
+ entrances:
+ Courtyard: True
+ Yellow Backside Area: True
+ panels:
+ FIRST:
+ id: Backside Room/Panel_first_first
+ tag: midwhite
+ SECOND:
+ id: Backside Room/Panel_second_second
+ tag: midwhite
+ THIRD:
+ id: Backside Room/Panel_third_third
+ tag: midwhite
+ FOURTH:
+ id: Backside Room/Panel_fourth_fourth
+ tag: midwhite
+ doors:
+ Backside Door:
+ id: Count Up Room Area Doors/Door_yellow_backside
+ door_group: Backside Doors
+ location_name: Courtyard - FIRST, SECOND, THIRD, FOURTH
+ item_name: Courtyard - Backside Door
+ panels:
+ - FIRST
+ - SECOND
+ - THIRD
+ - FOURTH
+ panel_doors:
+ FIRST SECOND THIRD FOURTH:
+ item_name: Courtyard - Ordinal Panels
+ panel_group: Backside Entrance Panels
+ panels:
+ - FIRST
+ - SECOND
+ - THIRD
+ - FOURTH
+ The Colorful (White):
+ entrances:
+ Courtyard: True
+ The Colorful (Black):
+ door: Progress Door
+ panels:
+ BEGIN:
+ id: Doorways Room/Panel_begin_start
+ tag: botwhite
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_white
+ item_name: The Colorful - White Door
+ door_group: Colorful Doors
+ location_name: The Colorful - White
+ panels:
+ - BEGIN
+ panel_doors:
+ BEGIN:
+ item_name: The Colorful - BEGIN (Panel)
+ panel_group: Colorful Panels
+ panels:
+ - BEGIN
+ The Colorful (Black):
+ entrances:
+ The Colorful (White):
+ room: The Colorful (White)
+ door: Progress Door
+ The Colorful (Red):
+ door: Progress Door
+ panels:
+ FOUND:
+ id: Doorways Room/Panel_found_lost
+ colors: black
+ tag: botblack
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_black
+ item_name: The Colorful - Black Door
+ location_name: The Colorful - Black
+ door_group: Colorful Doors
+ panels:
+ - FOUND
+ panel_doors:
+ FOUND:
+ item_name: The Colorful - FOUND (Panel)
+ panel_group: Colorful Panels
+ panels:
+ - FOUND
+ The Colorful (Red):
+ entrances:
+ The Colorful (Black):
+ room: The Colorful (Black)
+ door: Progress Door
+ The Colorful (Yellow):
+ door: Progress Door
+ panels:
+ LOAF:
+ id: Doorways Room/Panel_loaf_crust
+ colors: red
+ tag: botred
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_red
+ item_name: The Colorful - Red Door
+ location_name: The Colorful - Red
+ door_group: Colorful Doors
+ panels:
+ - LOAF
+ panel_doors:
+ LOAF:
+ item_name: The Colorful - LOAF (Panel)
+ panel_group: Colorful Panels
+ panels:
+ - LOAF
+ The Colorful (Yellow):
+ entrances:
+ The Colorful (Red):
+ room: The Colorful (Red)
+ door: Progress Door
+ The Colorful (Blue):
+ door: Progress Door
+ panels:
+ CREAM:
+ id: Doorways Room/Panel_eggs_breakfast
+ colors: yellow
+ tag: botyellow
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_yellow
+ item_name: The Colorful - Yellow Door
+ location_name: The Colorful - Yellow
+ door_group: Colorful Doors
+ panels:
+ - CREAM
+ panel_doors:
+ CREAM:
+ item_name: The Colorful - CREAM (Panel)
+ panel_group: Colorful Panels
+ panels:
+ - CREAM
+ The Colorful (Blue):
+ entrances:
+ The Colorful (Yellow):
+ room: The Colorful (Yellow)
+ door: Progress Door
+ The Colorful (Purple):
+ door: Progress Door
+ panels:
+ SUN:
+ id: Doorways Room/Panel_sun_sky
+ colors: blue
+ tag: botblue
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_blue
+ item_name: The Colorful - Blue Door
+ location_name: The Colorful - Blue
+ door_group: Colorful Doors
+ panels:
+ - SUN
+ panel_doors:
+ SUN:
+ item_name: The Colorful - SUN (Panel)
+ panel_group: Colorful Panels
+ panels:
+ - SUN
+ The Colorful (Purple):
+ entrances:
+ The Colorful (Blue):
+ room: The Colorful (Blue)
+ door: Progress Door
+ The Colorful (Orange):
+ door: Progress Door
+ panels:
+ SPOON:
+ id: Doorways Room/Panel_teacher_substitute
+ colors: purple
+ tag: botpurple
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_purple
+ item_name: The Colorful - Purple Door
+ location_name: The Colorful - Purple
+ door_group: Colorful Doors
+ panels:
+ - SPOON
+ panel_doors:
+ SPOON:
+ item_name: The Colorful - SPOON (Panel)
+ panel_group: Colorful Panels
+ panels:
+ - SPOON
+ The Colorful (Orange):
+ entrances:
+ The Colorful (Purple):
+ room: The Colorful (Purple)
+ door: Progress Door
+ The Colorful (Green):
+ door: Progress Door
+ panels:
+ LETTERS:
+ id: Doorways Room/Panel_walnuts_orange
+ colors: orange
+ tag: botorange
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_orange
+ item_name: The Colorful - Orange Door
+ location_name: The Colorful - Orange
+ door_group: Colorful Doors
+ panels:
+ - LETTERS
+ panel_doors:
+ LETTERS:
+ item_name: The Colorful - LETTERS (Panel)
+ panel_group: Colorful Panels
+ panels:
+ - LETTERS
+ The Colorful (Green):
+ entrances:
+ The Colorful (Orange):
+ room: The Colorful (Orange)
+ door: Progress Door
+ The Colorful (Brown):
+ door: Progress Door
+ panels:
+ WALLS:
+ id: Doorways Room/Panel_path_i
+ colors: green
+ tag: forbid
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_green
+ item_name: The Colorful - Green Door
+ location_name: The Colorful - Green
+ door_group: Colorful Doors
+ panels:
+ - WALLS
+ panel_doors:
+ WALLS:
+ item_name: The Colorful - WALLS (Panel)
+ panel_group: Colorful Panels
+ panels:
+ - WALLS
+ The Colorful (Brown):
+ entrances:
+ The Colorful (Green):
+ room: The Colorful (Green)
+ door: Progress Door
+ The Colorful (Gray):
+ door: Progress Door
+ panels:
+ IRON:
+ id: Doorways Room/Panel_iron_rust
+ colors: brown
+ tag: botbrown
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_brown
+ item_name: The Colorful - Brown Door
+ location_name: The Colorful - Brown
+ door_group: Colorful Doors
+ panels:
+ - IRON
+ panel_doors:
+ IRON:
+ item_name: The Colorful - IRON (Panel)
+ panel_group: Colorful Panels
+ panels:
+ - IRON
+ The Colorful (Gray):
+ entrances:
+ The Colorful (Brown):
+ room: The Colorful (Brown)
+ door: Progress Door
+ The Colorful:
+ door: Progress Door
+ panels:
+ OBSTACLE:
+ id: Doorways Room/Panel_obstacle_door
+ colors: gray
+ tag: forbid
+ doors:
+ Progress Door:
+ id: Doorway Room Doors/Door_gray
+ item_name: The Colorful - Gray Door
+ location_name: The Colorful - Gray
+ door_group: Colorful Doors
+ panels:
+ - OBSTACLE
+ panel_doors:
+ OBSTACLE:
+ item_name: The Colorful - OBSTACLE (Panel)
+ panel_group: Colorful Panels
+ panels:
+ - OBSTACLE
+ The Colorful:
+ entrances:
+ The Colorful (Gray):
+ room: The Colorful (Gray)
+ door: Progress Door
+ Roof: True
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_colorful_colorful
+ check: True
+ tag: forbid
+ required_panel:
+ - room: The Colorful (White)
+ panel: BEGIN
+ - room: The Colorful (Black)
+ panel: FOUND
+ - room: The Colorful (Red)
+ panel: LOAF
+ - room: The Colorful (Yellow)
+ panel: CREAM
+ - room: The Colorful (Blue)
+ panel: SUN
+ - room: The Colorful (Purple)
+ panel: SPOON
+ - room: The Colorful (Orange)
+ panel: LETTERS
+ - room: The Colorful (Green)
+ panel: WALLS
+ - room: The Colorful (Brown)
+ panel: IRON
+ - room: The Colorful (Gray)
+ panel: OBSTACLE
+ achievement: The Colorful
+ paintings:
+ - id: arrows_painting_12
+ orientation: north
+ progression:
+ Progressive Colorful:
+ doors:
+ - room: The Colorful (White)
+ door: Progress Door
+ - room: The Colorful (Black)
+ door: Progress Door
+ - room: The Colorful (Red)
+ door: Progress Door
+ - room: The Colorful (Yellow)
+ door: Progress Door
+ - room: The Colorful (Blue)
+ door: Progress Door
+ - room: The Colorful (Purple)
+ door: Progress Door
+ - room: The Colorful (Orange)
+ door: Progress Door
+ - room: The Colorful (Green)
+ door: Progress Door
+ - room: The Colorful (Brown)
+ door: Progress Door
+ - room: The Colorful (Gray)
+ door: Progress Door
+ panel_doors:
+ - room: The Colorful (White)
+ panel_door: BEGIN
+ - room: The Colorful (Black)
+ panel_door: FOUND
+ - room: The Colorful (Red)
+ panel_door: LOAF
+ - room: The Colorful (Yellow)
+ panel_door: CREAM
+ - room: The Colorful (Blue)
+ panel_door: SUN
+ - room: The Colorful (Purple)
+ panel_door: SPOON
+ - room: The Colorful (Orange)
+ panel_door: LETTERS
+ - room: The Colorful (Green)
+ panel_door: WALLS
+ - room: The Colorful (Brown)
+ panel_door: IRON
+ - room: The Colorful (Gray)
+ panel_door: OBSTACLE
+ Welcome Back Area:
+ entrances:
+ Starting Room:
+ door: Shortcut to Starting Room
+ Hub Room:
+ warp: True
+ Outside The Wondrous:
+ warp: True
+ Outside The Undeterred:
+ warp: True
+ Outside The Agreeable:
+ warp: True
+ Outside The Wanderer:
+ warp: True
+ The Observant:
+ warp: True
+ Art Gallery:
+ warp: True
+ The Scientific:
+ warp: True
+ Cellar:
+ warp: True
+ Orange Tower Fifth Floor:
+ room: Orange Tower Fifth Floor
+ door: Welcome Back
+ Challenge Room:
+ room: Challenge Room
+ door: Welcome Door
+ panels:
+ WELCOME BACK:
+ id: Entry Room/Panel_return_return
+ tag: midwhite
+ SECRET:
+ id: Entry Room/Panel_secret_secret
+ tag: midwhite
+ CLOCKWISE:
+ id: Shuffle Room/Panel_clockwise_counterclockwise
+ colors: black
+ check: True
+ exclude_reduce: True
+ tag: botblack
+ doors:
+ Shortcut to Starting Room:
+ id: Entry Room Area Doors/Door_return_return
+ door_group: Welcome Back Doors
+ include_reduce: True
+ panels:
+ - WELCOME BACK
+ Owl Hallway:
+ entrances:
+ Hidden Room:
+ painting: True
+ Hedge Maze:
+ door: Shortcut to Hedge Maze
+ Orange Tower Sixth Floor:
+ painting: True
+ panels:
+ STRAYS:
+ id: Maze Room/Panel_strays_maze
+ colors: purple
+ tag: toppurp
+ READS + RUST:
+ id: Tower Room/Panel_reads_rust_lawns_6
+ colors: orange
+ check: True
+ tag: midorange
+ doors:
+ Shortcut to Hedge Maze:
+ id: Maze Area Doors/Door_strays_maze
+ door_group: Hedge Maze Doors
+ panels:
+ - STRAYS
+ panel_doors:
+ STRAYS:
+ panels:
+ - STRAYS
+ paintings:
+ - id: arrows_painting_8
+ orientation: south
+ - id: maze_painting_2
+ orientation: north
+ - id: owl_painting_2
+ orientation: south
+ required_when_no_doors: True
+ - id: clock_painting_4
+ orientation: north
+ Outside The Initiated:
+ entrances:
+ Hub Room:
+ door: Shortcut to Hub Room
+ Knight Night Exit:
+ room: Knight Night (Final)
+ door: Exit
+ Orange Tower Third Floor:
+ room: Sunwarps
+ door: 3 Sunwarp
+ sunwarp: True
+ Orange Tower Fifth Floor:
+ room: Art Gallery
+ door: Exit
+ warp: True
+ Art Gallery:
+ room: Art Gallery
+ door: Exit
+ warp: True
+ The Bearer:
+ room: Art Gallery
+ door: Exit
+ Eight Alcove:
+ door: Eight Door
+ The Optimistic: True
+ panels:
+ SEVEN (1):
+ id: Backside Room/Panel_seven_seven_5
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Sevens
+ SEVEN (2):
+ id: Backside Room/Panel_seven_seven_6
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Sevens
+ EIGHT:
+ id: Backside Room/Panel_eight_eight_7
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Eights
+ NINE:
+ id: Backside Room/Panel_nine_nine_4
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Nines
+ BLUE:
+ id: Color Arrow Room/Panel_blue_afar
+ tag: midwhite
+ hunt: True
+ required_door:
+ door: Blue Barrier
+ ORANGE:
+ id: Color Arrow Room/Panel_orange_afar
+ tag: midwhite
+ hunt: True
+ required_door:
+ door: Orange Barrier
+ UNCOVER:
+ id: Appendix Room/Panel_discover_recover
+ colors: purple
+ tag: midpurp
+ OXEN:
+ id: Rhyme Room/Panel_locked_knocked
+ colors: purple
+ tag: midpurp
+ PAST (1):
+ id: Shuffle Room/Panel_past_present
+ colors: brown
+ tag: botbrown
+ FUTURE (1):
+ id: Shuffle Room/Panel_future_present
+ colors:
+ - brown
+ - black
+ tag: bot brown black
+ FUTURE (2):
+ id: Shuffle Room/Panel_future_past
+ colors: black
+ tag: botblack
+ PAST (2):
+ id: Shuffle Room/Panel_past_future
+ colors: black
+ tag: botblack
+ PRESENT:
+ id: Shuffle Room/Panel_past_past
+ colors:
+ - brown
+ - black
+ tag: bot brown black
+ SMILE:
+ id: Open Areas/Panel_smile_smile
+ tag: midwhite
+ ANGERED:
+ id: Open Areas/Panel_angered_enraged
+ colors:
+ - yellow
+ tag: syn anagram
+ copy_to_sign: sign18
+ VOTE:
+ id: Open Areas/Panel_vote_veto
+ colors:
+ - yellow
+ - black
+ tag: ant anagram
+ copy_to_sign: sign17
+ doors:
+ Shortcut to Hub Room:
+ id: Appendix Room Area Doors/Door_recover_discover
+ panels:
+ - UNCOVER
+ Blue Barrier:
+ id: Color Arrow Room Doors/Door_blue_3
+ door_group: Color Hunt Barriers
+ skip_location: True
+ panels:
+ - room: Color Hunt
+ panel: BLUE
+ Orange Barrier:
+ id: Color Arrow Room Doors/Door_orange_3
+ door_group: Color Hunt Barriers
+ skip_location: True
+ panels:
+ - room: Color Hunt
+ panel: ORANGE
+ Initiated Entrance:
+ id: Red Blue Purple Room Area Doors/Door_locked_knocked
+ item_name: The Initiated - Entrance
+ item_group: Achievement Room Entrances
+ panels:
+ - OXEN
+ # These would be more appropriate in Champion's Rest, but as currently
+ # implemented, locations need to include at least one panel from the
+ # containing region.
+ Green Barrier:
+ id: Color Arrow Room Doors/Door_green_hider_1
+ location_name: Color Barriers - BLUE and YELLOW
+ item_name: Color Hunt - Green Barrier
+ door_group: Color Hunt Barriers
+ panels:
+ - BLUE
+ - room: Directional Gallery
+ panel: YELLOW
+ Purple Barrier:
+ id:
+ - Color Arrow Room Doors/Door_purple_hider_1
+ - Color Arrow Room Doors/Door_purple_hider_2
+ - Color Arrow Room Doors/Door_purple_hider_3
+ location_name: Color Barriers - RED and BLUE
+ item_name: Color Hunt - Purple Barrier
+ door_group: Color Hunt Barriers
+ panels:
+ - BLUE
+ - room: Orange Tower Third Floor
+ panel: RED
+ Entrance:
+ id:
+ - Color Arrow Room Doors/Door_all_hider_1
+ - Color Arrow Room Doors/Door_all_hider_2
+ - Color Arrow Room Doors/Door_all_hider_3
+ location_name: Color Barriers - GREEN, ORANGE and PURPLE
+ item_name: Champion's Rest - Entrance
+ panels:
+ - ORANGE
+ - room: Courtyard
+ panel: GREEN
+ - room: Outside The Agreeable
+ panel: PURPLE
+ Eight Door:
+ id: Red Blue Purple Room Area Doors/Door_a_strands2
+ item_group: Achievement Room Entrances
+ skip_location: True
+ panels:
+ - room: The Incomparable
+ panel: I (Seven)
+ - room: Courtyard
+ panel: I
+ - room: Elements Area
+ panel: A
+ panel_doors:
+ UNCOVER:
+ panels:
+ - UNCOVER
+ OXEN:
+ panels:
+ - OXEN
+ paintings:
+ - id: clock_painting_5
+ orientation: east
+ - id: smile_painting_1
+ orientation: north
+ sunwarps:
+ - dots: 3
+ direction: exit
+ entrance_indicator_pos: [ 89.99, 2.5, 1 ]
+ orientation: east
+ The Initiated:
+ entrances:
+ Outside The Initiated:
+ room: Outside The Initiated
+ door: Initiated Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_illuminated_initiated
+ colors: purple
+ tag: forbid
+ check: True
+ achievement: The Initiated
+ DAUGHTER:
+ id: Rhyme Room/Panel_daughter_laughter
+ colors: purple
+ tag: midpurp
+ START:
+ id: Rhyme Room/Panel_move_love
+ colors: purple
+ tag: double midpurp
+ subtag: left
+ link: change STARS
+ STARE:
+ id: Rhyme Room/Panel_stove_love
+ colors: purple
+ tag: double midpurp
+ subtag: right
+ link: change STARS
+ HYPE:
+ id: Rhyme Room/Panel_scope_type
+ colors: purple
+ tag: midpurp and rhyme
+ copy_to_sign: sign16
+ ABYSS:
+ id: Rhyme Room/Panel_abyss_this
+ colors: purple
+ tag: toppurp
+ SWEAT:
+ id: Rhyme Room/Panel_sweat_great
+ colors: purple
+ tag: double midpurp
+ subtag: left
+ link: change GREAT
+ BEAT:
+ id: Rhyme Room/Panel_beat_great
+ colors: purple
+ tag: double midpurp
+ subtag: right
+ link: change GREAT
+ ALUMNI:
+ id: Rhyme Room/Panel_alumni_hi
+ colors: purple
+ tag: midpurp and rhyme
+ copy_to_sign: sign14
+ PATS:
+ id: Rhyme Room/Panel_wrath_path
+ colors: purple
+ tag: forbid
+ KNIGHT:
+ id: Rhyme Room/Panel_knight_write
+ colors: purple
+ tag: double toppurp
+ subtag: left
+ link: change WRITE
+ BYTE:
+ id: Rhyme Room/Panel_byte_write
+ colors: purple
+ tag: double toppurp
+ subtag: right
+ link: change WRITE
+ MAIM:
+ id: Rhyme Room/Panel_maim_same
+ colors: purple
+ tag: toppurp
+ MORGUE:
+ id: Rhyme Room/Panel_chair_bear
+ colors: purple
+ tag: purple rhyme change stack
+ subtag: top
+ link: prcs CYBORG
+ CHAIR:
+ id: Rhyme Room/Panel_bare_bear
+ colors: purple
+ tag: toppurp
+ HUMAN:
+ id: Rhyme Room/Panel_cost_most
+ colors: purple
+ tag: purple rhyme change stack
+ subtag: bot
+ link: prcs CYBORG
+ BED:
+ id: Rhyme Room/Panel_bed_dead
+ colors: purple
+ tag: toppurp
+ The Optimistic:
+ entrances:
+ Outside The Initiated: True
+ panels:
+ BACKSIDE:
+ id: Backside Room/Panel_backside_1
+ tag: midwhite
+ Achievement:
+ id: Countdown Panels/Panel_optimistic_optimistic
+ check: True
+ tag: forbid
+ required_panel:
+ - panel: BACKSIDE
+ - room: The Observant
+ panel: BACKSIDE
+ - room: Yellow Backside Area
+ panel: BACKSIDE
+ - room: Directional Gallery
+ panel: BACKSIDE
+ - room: The Bearer
+ panel: BACKSIDE
+ achievement: The Optimistic
+ The Traveled:
+ entrances:
+ Hub Room:
+ room: Hub Room
+ door: Traveled Entrance
+ Color Hallways:
+ door: Color Hallways Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_traveled_traveled
+ required_room: Hub Room
+ tag: forbid
+ check: True
+ achievement: The Traveled
+ CLOSE:
+ id: Synonym Room/Panel_close_near
+ tag: botwhite
+ COMPOSE:
+ id: Synonym Room/Panel_compose_write
+ tag: double botwhite
+ subtag: left
+ link: syn WRITE
+ RECORD:
+ id: Synonym Room/Panel_record_write
+ tag: double botwhite
+ subtag: right
+ link: syn WRITE
+ CATEGORY:
+ id: Synonym Room/Panel_category_type
+ tag: botwhite
+ HELLO:
+ id: Synonym Room/Panel_hello_hi
+ tag: botwhite
+ DUPLICATE:
+ id: Synonym Room/Panel_duplicate_same
+ tag: double botwhite
+ subtag: left
+ link: syn SAME
+ IDENTICAL:
+ id: Synonym Room/Panel_identical_same
+ tag: double botwhite
+ subtag: right
+ link: syn SAME
+ DISTANT:
+ id: Synonym Room/Panel_distant_far
+ tag: botwhite
+ HAY:
+ id: Synonym Room/Panel_hay_straw
+ tag: botwhite
+ GIGGLE:
+ id: Synonym Room/Panel_giggle_laugh
+ tag: double botwhite
+ subtag: left
+ link: syn LAUGH
+ CHUCKLE:
+ id: Synonym Room/Panel_chuckle_laugh
+ tag: double botwhite
+ subtag: right
+ link: syn LAUGH
+ SNITCH:
+ id: Synonym Room/Panel_snitch_rat
+ tag: botwhite
+ CONCEALED:
+ id: Synonym Room/Panel_concealed_hidden
+ tag: botwhite
+ PLUNGE:
+ id: Synonym Room/Panel_plunge_fall
+ tag: double botwhite
+ subtag: left
+ link: syn FALL
+ AUTUMN:
+ id: Synonym Room/Panel_autumn_fall
+ tag: double botwhite
+ subtag: right
+ link: syn FALL
+ ROAD:
+ id: Synonym Room/Panel_growths_warts
+ tag: botwhite
+ FOUR:
+ id: Backside Room/Panel_four_four_4
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Outside The Undeterred
+ door: Fours
+ doors:
+ Color Hallways Entrance:
+ id: Appendix Room Area Doors/Door_hello_hi
+ door_group: Entrance to The Traveled
+ item_group: Achievement Room Entrances
+ panels:
+ - HELLO
+ Color Hallways:
+ entrances:
+ The Traveled:
+ room: The Traveled
+ door: Color Hallways Entrance
+ Outside The Bold:
+ warp: True
+ Outside The Undeterred:
+ warp: True
+ Crossroads:
+ warp: True
+ Hedge Maze:
+ warp: True
+ The Optimistic:
+ warp: True # backside
+ Directional Gallery:
+ warp: True # backside
+ Yellow Backside Area:
+ warp: True
+ The Bearer:
+ room: The Bearer
+ door: Backside Door
+ warp: True
+ The Observant:
+ room: The Observant
+ door: Backside Door
+ warp: True
+ Outside The Bold:
+ entrances:
+ Color Hallways:
+ warp: True
+ Color Hunt:
+ room: Color Hunt
+ door: Shortcut to The Steady
+ The Bearer:
+ room: The Bearer
+ door: Entrance
+ Directional Gallery:
+ # There is a painting warp here from the Directional Gallery, but it
+ # only appears when the sixes are revealed. It could be its own item if
+ # we wanted.
+ room: Number Hunt
+ door: Sixes
+ painting: True
+ Starting Room:
+ door: Painting Shortcut
+ painting: True
+ Room Room: True # trapdoor
+ Compass Room:
+ painting: True
+ panels:
+ UNOPEN:
+ id: Truncate Room/Panel_unopened_open
+ colors: red
+ tag: midred
+ BEGIN:
+ id: Rock Room/Panel_begin_begin
+ tag: midwhite
+ SIX:
+ id: Backside Room/Panel_six_six_4
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Sixes
+ NINE:
+ id: Backside Room/Panel_nine_nine_5
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Nines
+ LEFT:
+ id: Shuffle Room/Panel_left_left_2
+ tag: midwhite
+ RIGHT:
+ id: Shuffle Room/Panel_right_right_2
+ tag: midwhite
+ RISE (Horizon):
+ id: Open Areas/Panel_rise_horizon
+ colors: blue
+ tag: double topblue
+ subtag: left
+ link: expand HORIZON
+ RISE (Sunrise):
+ id: Open Areas/Panel_rise_sunrise
+ colors: blue
+ tag: double topblue
+ subtag: left
+ link: expand SUNRISE
+ ZEN:
+ id: Open Areas/Panel_son_horizon
+ colors: blue
+ tag: double topblue
+ subtag: right
+ link: expand HORIZON
+ SON:
+ id: Open Areas/Panel_son_sunrise
+ colors: blue
+ tag: double topblue
+ subtag: right
+ link: expand SUNRISE
+ STARGAZER:
+ id: Open Areas/Panel_stargazer_stargazer
+ tag: midwhite
+ required_door:
+ door: Stargazer Door
+ SOUND:
+ id: Cross Room/Panel_mouth_south
+ colors: purple
+ tag: midpurp
+ YEAST:
+ id: Cross Room/Panel_yeast_east
+ colors: red
+ tag: midred
+ WET:
+ id: Cross Room/Panel_wet_west
+ colors: blue
+ tag: midblue
+ doors:
+ Bold Entrance:
+ id: Red Blue Purple Room Area Doors/Door_unopened_open
+ item_name: The Bold - Entrance
+ item_group: Achievement Room Entrances
+ panels:
+ - UNOPEN
+ Painting Shortcut:
+ painting_id: pencil_painting6
+ skip_location: True
+ item_name: Starting Room - Pencil Painting
+ item_group: Paintings
+ panels:
+ - UNOPEN
+ Steady Entrance:
+ id: Rock Room Doors/Door_2
+ item_name: The Steady - Entrance
+ item_group: Achievement Room Entrances
+ panels:
+ - BEGIN
+ Lilac Entrance:
+ event: True
+ panels:
+ - room: The Steady (Rose)
+ panel: SOAR
+ Stargazer Door:
+ event: True
+ panels:
+ - RISE (Horizon)
+ - RISE (Sunrise)
+ - ZEN
+ - SON
+ panel_doors:
+ UNOPEN:
+ panels:
+ - UNOPEN
+ BEGIN:
+ panels:
+ - BEGIN
+ paintings:
+ - id: pencil_painting2
+ orientation: west
+ - id: north_missing2
+ orientation: north
+ The Bold:
+ entrances:
+ Outside The Bold:
+ room: Outside The Bold
+ door: Bold Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_emboldened_bold
+ colors: red
+ tag: forbid
+ check: True
+ achievement: The Bold
+ FOOT:
+ id: Truncate Room/Panel_foot_toe
+ colors: red
+ tag: botred
+ NEEDLE:
+ id: Truncate Room/Panel_needle_eye
+ colors: red
+ tag: double botred
+ subtag: left
+ link: mero EYE
+ FACE:
+ id: Truncate Room/Panel_face_eye
+ colors: red
+ tag: double botred
+ subtag: right
+ link: mero EYE
+ SIGN:
+ id: Truncate Room/Panel_sign_sigh
+ colors: red
+ tag: topred
+ HEARTBREAK:
+ id: Truncate Room/Panel_heartbreak_brake
+ colors: red
+ tag: topred
+ UNDEAD:
+ id: Truncate Room/Panel_undead_dead
+ colors: red
+ tag: double midred
+ subtag: left
+ link: trunc DEAD
+ DEADLINE:
+ id: Truncate Room/Panel_deadline_dead
+ colors: red
+ tag: double midred
+ subtag: right
+ link: trunc DEAD
+ SUSHI:
+ id: Truncate Room/Panel_sushi_hi
+ colors: red
+ tag: midred
+ THISTLE:
+ id: Truncate Room/Panel_thistle_this
+ colors: red
+ tag: midred
+ LANDMASS:
+ id: Truncate Room/Panel_landmass_mass
+ colors: red
+ tag: double midred
+ subtag: left
+ link: trunc MASS
+ MASSACRED:
+ id: Truncate Room/Panel_massacred_mass
+ colors: red
+ tag: double midred
+ subtag: right
+ link: trunc MASS
+ AIRPLANE:
+ id: Truncate Room/Panel_airplane_plain
+ colors: red
+ tag: topred
+ NIGHTMARE:
+ id: Truncate Room/Panel_nightmare_knight
+ colors: red
+ tag: topred
+ MOUTH:
+ id: Truncate Room/Panel_mouth_teeth
+ colors: red
+ tag: double botred
+ subtag: left
+ link: mero TEETH
+ SAW:
+ id: Truncate Room/Panel_saw_teeth
+ colors: red
+ tag: double botred
+ subtag: right
+ link: mero TEETH
+ HAND:
+ id: Truncate Room/Panel_hand_finger
+ colors: red
+ tag: botred
+ Outside The Undeterred:
+ entrances:
+ Color Hallways:
+ warp: True
+ Orange Tower First Floor:
+ room: Sunwarps
+ door: 4 Sunwarp
+ sunwarp: True
+ Orange Tower Second Floor:
+ warp: True
+ The Artistic (Smiley):
+ warp: True
+ The Artistic (Panda):
+ warp: True
+ The Artistic (Apple):
+ warp: True
+ The Artistic (Lattice):
+ warp: True
+ Yellow Backside Area:
+ painting: True
+ Number Hunt:
+ door: Number Hunt
+ Directional Gallery:
+ room: Directional Gallery
+ door: Shortcut to The Undeterred
+ Starting Room:
+ door: Painting Shortcut
+ painting: True
+ panels:
+ HOLLOW:
+ id: Hallway Room/Panel_hollow_hollow
+ tag: midwhite
+ ART + ART:
+ id: Tower Room/Panel_art_art_eat_2
+ colors: orange
+ check: True
+ tag: midorange
+ PEN:
+ id: Blue Room/Panel_pen_open
+ colors: blue
+ tag: midblue
+ HUSTLING:
+ id: Open Areas/Panel_hustling_sunlight
+ colors: yellow
+ tag: midyellow
+ SUNLIGHT:
+ id: Open Areas/Panel_sunlight_light
+ colors: red
+ tag: midred
+ required_panel:
+ panel: HUSTLING
+ LIGHT:
+ id: Open Areas/Panel_light_bright
+ colors: purple
+ tag: midpurp
+ required_panel:
+ panel: SUNLIGHT
+ BRIGHT:
+ id: Open Areas/Panel_bright_sunny
+ tag: botwhite
+ required_panel:
+ panel: LIGHT
+ SUNNY:
+ id: Open Areas/Panel_sunny_rainy
+ colors: black
+ tag: botblack
+ required_panel:
+ panel: BRIGHT
+ RAINY:
+ id: Open Areas/Panel_rainy_rainbow
+ colors: brown
+ tag: botbrown
+ required_panel:
+ panel: SUNNY
+ check: True
+ ZERO:
+ id: Backside Room/Panel_zero_zero
+ tag: midwhite
+ required_door:
+ room: Number Hunt
+ door: Zero Door
+ ONE:
+ id: Backside Room/Panel_one_one
+ tag: midwhite
+ TWO (1):
+ id: Backside Room/Panel_two_two
+ tag: midwhite
+ required_door:
+ door: Twos
+ TWO (2):
+ id: Backside Room/Panel_two_two_2
+ tag: midwhite
+ required_door:
+ door: Twos
+ THREE (1):
+ id: Backside Room/Panel_three_three
+ tag: midwhite
+ required_door:
+ door: Threes
+ THREE (2):
+ id: Backside Room/Panel_three_three_2
+ tag: midwhite
+ required_door:
+ door: Threes
+ THREE (3):
+ id: Backside Room/Panel_three_three_3
+ tag: midwhite
+ required_door:
+ door: Threes
+ FOUR:
+ id: Backside Room/Panel_four_four
+ tag: midwhite
+ required_door:
+ door: Fours
+ doors:
+ Undeterred Entrance:
+ id: Red Blue Purple Room Area Doors/Door_pen_open
+ item_name: The Undeterred - Entrance
+ item_group: Achievement Room Entrances
+ panels:
+ - PEN
+ Painting Shortcut:
+ painting_id:
+ - blueman_painting_3
+ - arrows_painting3
+ skip_location: True
+ item_name: Starting Room - Blue Painting
+ item_group: Paintings
+ panels:
+ - PEN
+ Green Painting:
+ painting_id: maze_painting_3
+ skip_location: True
+ item_group: Paintings
+ panels:
+ - FOUR
+ Twos:
+ id:
+ - Count Up Room Area Doors/Door_two_hider
+ - Count Up Room Area Doors/Door_two_hider_2
+ include_reduce: True
+ item_group: Numbers
+ panels:
+ - ONE
+ Threes:
+ id:
+ - Count Up Room Area Doors/Door_three_hider
+ - Count Up Room Area Doors/Door_three_hider_2
+ - Count Up Room Area Doors/Door_three_hider_3
+ location_name: Twos
+ include_reduce: True
+ item_group: Numbers
+ panels:
+ - TWO (1)
+ - TWO (2)
+ Number Hunt:
+ id: Count Up Room Area Doors/Door_three_unlocked
+ location_name: Threes
+ include_reduce: True
+ panels:
+ - THREE (1)
+ - THREE (2)
+ - THREE (3)
+ Fours:
+ id:
+ - Count Up Room Area Doors/Door_four_hider
+ - Count Up Room Area Doors/Door_four_hider_2
+ - Count Up Room Area Doors/Door_four_hider_3
+ - Count Up Room Area Doors/Door_four_hider_4
+ skip_location: True
+ item_group: Numbers
+ panels:
+ - THREE (1)
+ - THREE (2)
+ - THREE (3)
+ Fives:
+ id:
+ - Count Up Room Area Doors/Door_five_hider
+ - Count Up Room Area Doors/Door_five_hider_4
+ - Count Up Room Area Doors/Door_five_hider_5
+ location_name: Fours
+ item_name: Number Hunt - Fives
+ item_group: Numbers
+ include_reduce: True
+ panels:
+ - FOUR
+ - room: Hub Room
+ panel: FOUR
+ - room: Dead End Area
+ panel: FOUR
+ - room: The Traveled
+ panel: FOUR
+ Challenge Entrance:
+ id: Count Up Room Area Doors/Door_zero_unlocked
+ item_name: Number Hunt - Challenge Entrance
+ item_group: Achievement Room Entrances
+ panels:
+ - ZERO
+ panel_doors:
+ ZERO:
+ panels:
+ - ZERO
+ PEN:
+ panels:
+ - PEN
+ TWO:
+ item_name: Two Panels
+ panels:
+ - TWO (1)
+ - TWO (2)
+ THREE:
+ item_name: Three Panels
+ panels:
+ - THREE (1)
+ - THREE (2)
+ - THREE (3)
+ FOUR:
+ item_name: Four Panels
+ panels:
+ - FOUR
+ - room: Hub Room
+ panel: FOUR
+ - room: Dead End Area
+ panel: FOUR
+ - room: The Traveled
+ panel: FOUR
+ paintings:
+ - id: maze_painting_3
+ enter_only: True
+ orientation: north
+ move: True
+ required_door:
+ door: Green Painting
+ - id: blueman_painting_2
+ orientation: east
+ sunwarps:
+ - dots: 4
+ direction: exit
+ entrance_indicator_pos: [ -89.01, 2.5, 4 ]
+ orientation: east
+ The Undeterred:
+ entrances:
+ Outside The Undeterred:
+ room: Outside The Undeterred
+ door: Undeterred Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_deterred_undeterred
+ colors: blue
+ tag: forbid
+ check: True
+ achievement: The Undeterred
+ BONE:
+ id: Blue Room/Panel_bone_skeleton
+ colors: blue
+ tag: botblue
+ EYE (1):
+ id: Blue Room/Panel_mouth_face
+ colors: blue
+ tag: double botblue
+ subtag: left
+ link: holo FACE
+ MOUTH:
+ id: Blue Room/Panel_eye_face
+ colors: blue
+ tag: double botblue
+ subtag: right
+ link: holo FACE
+ IRIS:
+ id: Blue Room/Panel_toucan_bird
+ colors: blue
+ tag: botblue
+ EYE (2):
+ id: Blue Room/Panel_two_toucan
+ colors: blue
+ tag: topblue
+ ICE:
+ id: Blue Room/Panel_ice_eyesight
+ colors: blue
+ tag: double topblue
+ subtag: left
+ link: hex EYESIGHT
+ HEIGHT:
+ id: Blue Room/Panel_height_eyesight
+ colors: blue
+ tag: double topblue
+ subtag: right
+ link: hex EYESIGHT
+ EYE (3):
+ id: Blue Room/Panel_eye_hi
+ colors: blue
+ tag: topblue
+ NOT:
+ id: Blue Room/Panel_not_notice
+ colors: blue
+ tag: midblue
+ JUST:
+ id: Blue Room/Panel_just_readjust
+ colors: blue
+ tag: double midblue
+ subtag: left
+ link: exp READJUST
+ READ:
+ id: Blue Room/Panel_read_readjust
+ colors: blue
+ tag: double midblue
+ subtag: right
+ link: exp READJUST
+ FATHER:
+ id: Blue Room/Panel_ate_primate
+ colors: blue
+ tag: midblue
+ FEATHER:
+ id: Blue Room/Panel_primate_mammal
+ colors: blue
+ tag: botblue
+ CONTINENT:
+ id: Blue Room/Panel_continent_planet
+ colors: blue
+ tag: double botblue
+ subtag: left
+ link: holo PLANET
+ OCEAN:
+ id: Blue Room/Panel_ocean_planet
+ colors: blue
+ tag: double botblue
+ subtag: right
+ link: holo PLANET
+ WALL:
+ id: Blue Room/Panel_wall_room
+ colors: blue
+ tag: botblue
+ Number Hunt:
+ # This works a little differently than in the base game. The door to the
+ # initial number in each set opens at the same time as the rest of the doors
+ # in that set.
+ entrances:
+ Outside The Undeterred:
+ room: Outside The Undeterred
+ door: Number Hunt
+ Directional Gallery:
+ door: Door to Directional Gallery
+ Challenge Room:
+ room: Outside The Undeterred
+ door: Challenge Entrance
+ panels:
+ FIVE:
+ id: Backside Room/Panel_five_five
+ tag: midwhite
+ required_door:
+ room: Outside The Undeterred
+ door: Fives
+ SIX:
+ id: Backside Room/Panel_six_six
+ tag: midwhite
+ required_door:
+ door: Sixes
+ SEVEN:
+ id: Backside Room/Panel_seven_seven
+ tag: midwhite
+ required_door:
+ door: Sevens
+ EIGHT:
+ id: Backside Room/Panel_eight_eight
+ tag: midwhite
+ required_door:
+ door: Eights
+ NINE:
+ id: Backside Room/Panel_nine_nine
+ tag: midwhite
+ required_door:
+ door: Nines
+ doors:
+ Door to Directional Gallery:
+ id: Count Up Room Area Doors/Door_five_unlocked
+ door_group: Directional Gallery Doors
+ skip_location: True
+ panels:
+ - FIVE
+ Sixes:
+ id:
+ - Count Up Room Area Doors/Door_six_hider
+ - Count Up Room Area Doors/Door_six_hider_2
+ - Count Up Room Area Doors/Door_six_hider_3
+ - Count Up Room Area Doors/Door_six_hider_4
+ - Count Up Room Area Doors/Door_six_hider_5
+ - Count Up Room Area Doors/Door_six_hider_6
+ painting_id: pencil_painting3 # See note in Outside The Bold
+ location_name: Fives
+ item_group: Numbers
+ include_reduce: True
+ panels:
+ - FIVE
+ - room: Outside The Agreeable
+ panel: FIVE (1)
+ - room: Outside The Agreeable
+ panel: FIVE (2)
+ - room: Directional Gallery
+ panel: FIVE (1)
+ - room: Directional Gallery
+ panel: FIVE (2)
+ First Six:
+ event: True
+ panels:
+ - SIX
+ Sevens:
+ id:
+ - Count Up Room Area Doors/Door_seven_hider
+ - Count Up Room Area Doors/Door_seven_unlocked
+ - Count Up Room Area Doors/Door_seven_hider_2
+ - Count Up Room Area Doors/Door_seven_hider_3
+ - Count Up Room Area Doors/Door_seven_hider_4
+ - Count Up Room Area Doors/Door_seven_hider_5
+ - Count Up Room Area Doors/Door_seven_hider_6
+ - Count Up Room Area Doors/Door_seven_hider_7
+ location_name: Sixes
+ item_group: Numbers
+ include_reduce: True
+ panels:
+ - SIX
+ - room: Outside The Bold
+ panel: SIX
+ - room: Directional Gallery
+ panel: SIX (1)
+ - room: Directional Gallery
+ panel: SIX (2)
+ - room: The Bearer (East)
+ panel: SIX
+ - room: The Bearer (South)
+ panel: SIX
+ Eights:
+ id:
+ - Count Up Room Area Doors/Door_eight_hider
+ - Count Up Room Area Doors/Door_eight_unlocked
+ - Count Up Room Area Doors/Door_eight_hider_2
+ - Count Up Room Area Doors/Door_eight_hider_3
+ - Count Up Room Area Doors/Door_eight_hider_4
+ - Count Up Room Area Doors/Door_eight_hider_5
+ - Count Up Room Area Doors/Door_eight_hider_6
+ - Count Up Room Area Doors/Door_eight_hider_7
+ - Count Up Room Area Doors/Door_eight_hider_8
+ location_name: Sevens
+ item_group: Numbers
+ include_reduce: True
+ panels:
+ - SEVEN
+ - room: Directional Gallery
+ panel: SEVEN
+ - room: Knight Night Exit
+ panel: SEVEN (1)
+ - room: Knight Night Exit
+ panel: SEVEN (2)
+ - room: Knight Night Exit
+ panel: SEVEN (3)
+ - room: Outside The Initiated
+ panel: SEVEN (1)
+ - room: Outside The Initiated
+ panel: SEVEN (2)
+ Nines:
+ id:
+ - Count Up Room Area Doors/Door_nine_hider
+ - Count Up Room Area Doors/Door_nine_hider_2
+ - Count Up Room Area Doors/Door_nine_hider_3
+ - Count Up Room Area Doors/Door_nine_hider_4
+ - Count Up Room Area Doors/Door_nine_hider_5
+ - Count Up Room Area Doors/Door_nine_hider_6
+ - Count Up Room Area Doors/Door_nine_hider_7
+ - Count Up Room Area Doors/Door_nine_hider_8
+ - Count Up Room Area Doors/Door_nine_hider_9
+ location_name: Eights
+ item_group: Numbers
+ include_reduce: True
+ panels:
+ - EIGHT
+ - room: Directional Gallery
+ panel: EIGHT
+ - room: The Eyes They See
+ panel: EIGHT
+ - room: Dead End Area
+ panel: EIGHT
+ - room: Crossroads
+ panel: EIGHT
+ - room: Hot Crusts Area
+ panel: EIGHT
+ - room: Art Gallery
+ panel: EIGHT
+ - room: Outside The Initiated
+ panel: EIGHT
+ Zero Door:
+ # The black wall isn't a door, so we can't ever hide it.
+ id: Count Up Room Area Doors/Door_zero_hider_2
+ location_name: Nines
+ item_name: Outside The Undeterred - Zero Door
+ item_group: Numbers
+ include_reduce: True
+ panels:
+ - NINE
+ - room: Directional Gallery
+ panel: NINE
+ - room: Amen Name Area
+ panel: NINE
+ - room: Yellow Backside Area
+ panel: NINE
+ - room: Outside The Initiated
+ panel: NINE
+ - room: Outside The Bold
+ panel: NINE
+ - room: Rhyme Room (Cross)
+ panel: NINE
+ - room: Orange Tower Fifth Floor
+ panel: NINE
+ - room: Elements Area
+ panel: NINE
+ panel_doors:
+ FIVE:
+ item_name: Five Panels
+ panels:
+ - FIVE
+ - room: Outside The Agreeable
+ panel: FIVE (1)
+ - room: Outside The Agreeable
+ panel: FIVE (2)
+ - room: Directional Gallery
+ panel: FIVE (1)
+ - room: Directional Gallery
+ panel: FIVE (2)
+ SIX:
+ item_name: Six Panels
+ panels:
+ - SIX
+ - room: Outside The Bold
+ panel: SIX
+ - room: Directional Gallery
+ panel: SIX (1)
+ - room: Directional Gallery
+ panel: SIX (2)
+ - room: The Bearer (East)
+ panel: SIX
+ - room: The Bearer (South)
+ panel: SIX
+ SEVEN:
+ item_name: Seven Panels
+ panels:
+ - SEVEN
+ - room: Directional Gallery
+ panel: SEVEN
+ - room: Knight Night Exit
+ panel: SEVEN (1)
+ - room: Knight Night Exit
+ panel: SEVEN (2)
+ - room: Knight Night Exit
+ panel: SEVEN (3)
+ - room: Outside The Initiated
+ panel: SEVEN (1)
+ - room: Outside The Initiated
+ panel: SEVEN (2)
+ EIGHT:
+ item_name: Eight Panels
+ panels:
+ - EIGHT
+ - room: Directional Gallery
+ panel: EIGHT
+ - room: The Eyes They See
+ panel: EIGHT
+ - room: Dead End Area
+ panel: EIGHT
+ - room: Crossroads
+ panel: EIGHT
+ - room: Hot Crusts Area
+ panel: EIGHT
+ - room: Art Gallery
+ panel: EIGHT
+ - room: Outside The Initiated
+ panel: EIGHT
+ NINE:
+ item_name: Nine Panels
+ panels:
+ - NINE
+ - room: Directional Gallery
+ panel: NINE
+ - room: Amen Name Area
+ panel: NINE
+ - room: Yellow Backside Area
+ panel: NINE
+ - room: Outside The Initiated
+ panel: NINE
+ - room: Outside The Bold
+ panel: NINE
+ - room: Rhyme Room (Cross)
+ panel: NINE
+ - room: Orange Tower Fifth Floor
+ panel: NINE
+ - room: Elements Area
+ panel: NINE
+ paintings:
+ - id: smile_painting_5
+ enter_only: True
+ orientation: east
+ required_door:
+ door: Eights
+ progression:
+ Progressive Number Hunt:
+ panel_doors:
+ - room: Outside The Undeterred
+ panel_door: TWO
+ - room: Outside The Undeterred
+ panel_door: THREE
+ - room: Outside The Undeterred
+ panel_door: FOUR
+ - FIVE
+ - SIX
+ - SEVEN
+ - EIGHT
+ - NINE
+ - room: Outside The Undeterred
+ panel_door: ZERO
+ Directional Gallery:
+ entrances:
+ Outside The Agreeable:
+ room: Sunwarps
+ door: 6 Sunwarp
+ sunwarp: True
+ Orange Tower First Floor:
+ room: Orange Tower First Floor
+ door: Salt Pepper Door
+ Outside The Undeterred:
+ door: Shortcut to The Undeterred
+ Number Hunt:
+ room: Number Hunt
+ door: Door to Directional Gallery
+ Roof: True # through ceiling of sunwarp
+ panels:
+ PEPPER:
+ id: Backside Room/Panel_pepper_salt
+ colors: black
+ tag: botblack
+ TURN:
+ id: Backside Room/Panel_turn_return
+ colors: blue
+ tag: midblue
+ LEARN:
+ id: Backside Room/Panel_learn_return
+ colors: purple
+ tag: midpurp
+ FIVE (1):
+ id: Backside Room/Panel_five_five_3
+ tag: midwhite
+ hunt: True
+ required_panel:
+ panel: LIGHT
+ FIVE (2):
+ id: Backside Room/Panel_five_five_2
+ tag: midwhite
+ hunt: True
+ required_panel:
+ panel: WARD
+ SIX (1):
+ id: Backside Room/Panel_six_six_3
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Sixes
+ SIX (2):
+ id: Backside Room/Panel_six_six_2
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Sixes
+ SEVEN:
+ id: Backside Room/Panel_seven_seven_2
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Sevens
+ EIGHT:
+ id: Backside Room/Panel_eight_eight_2
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Eights
+ NINE:
+ id: Backside Room/Panel_nine_nine_6
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Nines
+ BACKSIDE:
+ id: Backside Room/Panel_backside_4
+ tag: midwhite
+ hunt: True
+ "834283054":
+ id: Tower Room/Panel_834283054_undaunted
+ colors: orange
+ check: True
+ exclude_reduce: True
+ tag: midorange
+ required_door:
+ room: Number Hunt
+ door: First Six
+ PARANOID:
+ id: Backside Room/Panel_paranoid_paranoid
+ tag: midwhite
+ check: True
+ exclude_reduce: True
+ required_door:
+ room: Number Hunt
+ door: First Six
+ YELLOW:
+ id: Color Arrow Room/Panel_yellow_afar
+ tag: midwhite
+ hunt: True
+ required_door:
+ door: Yellow Barrier
+ WADED + WEE:
+ id: Tower Room/Panel_waded_wee_warts_7
+ colors: orange
+ check: True
+ exclude_reduce: True
+ tag: midorange
+ THE EYES:
+ id: Shuffle Room/Panel_theeyes_theeyes
+ tag: midwhite
+ LEFT:
+ id: Shuffle Room/Panel_left_left
+ tag: midwhite
+ RIGHT:
+ id: Shuffle Room/Panel_right_right
+ tag: midwhite
+ MIDDLE:
+ id: Shuffle Room/Panel_middle_middle
+ tag: midwhite
+ WARD:
+ id: Backside Room/Panel_ward_forward
+ colors: blue
+ tag: midblue
+ HIND:
+ id: Backside Room/Panel_hind_behind
+ colors: blue
+ tag: midblue
+ RIG:
+ id: Backside Room/Panel_rig_right
+ colors: blue
+ tag: midblue
+ WINDWARD:
+ id: Backside Room/Panel_windward_forward
+ colors: purple
+ tag: midpurp
+ LIGHT:
+ id: Backside Room/Panel_light_right
+ colors: purple
+ tag: midpurp
+ REWIND:
+ id: Backside Room/Panel_rewind_behind
+ colors: purple
+ tag: midpurp
+ doors:
+ Shortcut to The Undeterred:
+ id: Count Up Room Area Doors/Door_return_double
+ door_group: Directional Gallery Doors
+ panels:
+ - TURN
+ - LEARN
+ Yellow Barrier:
+ id: Color Arrow Room Doors/Door_yellow_4
+ door_group: Color Hunt Barriers
+ skip_location: True
+ panels:
+ - room: Color Hunt
+ panel: YELLOW
+ panel_doors:
+ TURN LEARN:
+ panels:
+ - TURN
+ - LEARN
+ paintings:
+ - id: smile_painting_7
+ orientation: south
+ - id: flower_painting_4
+ orientation: south
+ - id: pencil_painting3
+ enter_only: True
+ orientation: east
+ move: True
+ required_door:
+ room: Number Hunt
+ door: First Six
+ - id: boxes_painting
+ orientation: south
+ - id: cherry_painting
+ orientation: east
+ sunwarps:
+ - dots: 6
+ direction: exit
+ entrance_indicator_pos: [ -39, 2.5, -7.01 ]
+ orientation: north
+ Color Hunt:
+ entrances:
+ Outside The Bold:
+ door: Shortcut to The Steady
+ Orange Tower Fourth Floor:
+ room: Sunwarps
+ door: 5 Sunwarp
+ sunwarp: True
+ Roof: True # through ceiling of sunwarp
+ Champion's Rest:
+ room: Outside The Initiated
+ door: Entrance
+ panels:
+ EXIT:
+ id: Rock Room/Panel_red_red
+ tag: midwhite
+ HUES:
+ id: Color Arrow Room/Panel_hues_colors
+ tag: botwhite
+ RED:
+ id: Color Arrow Room/Panel_red_near
+ check: True
+ tag: midwhite
+ BLUE:
+ id: Color Arrow Room/Panel_blue_near
+ check: True
+ tag: midwhite
+ YELLOW:
+ id: Color Arrow Room/Panel_yellow_near
+ check: True
+ tag: midwhite
+ GREEN:
+ id: Color Arrow Room/Panel_green_near
+ check: True
+ tag: midwhite
+ required_door:
+ room: Outside The Initiated
+ door: Green Barrier
+ PURPLE:
+ id: Color Arrow Room/Panel_purple_near
+ check: True
+ tag: midwhite
+ required_door:
+ room: Outside The Initiated
+ door: Purple Barrier
+ ORANGE:
+ id: Color Arrow Room/Panel_orange_near
+ check: True
+ tag: midwhite
+ required_door:
+ room: Orange Tower Third Floor
+ door: Orange Barrier
+ doors:
+ Shortcut to The Steady:
+ id: Rock Room Doors/Door_hint
+ panels:
+ - EXIT
+ panel_doors:
+ EXIT:
+ panels:
+ - EXIT
+ RED:
+ panel_group: Color Hunt Panels
+ panels:
+ - RED
+ BLUE:
+ panel_group: Color Hunt Panels
+ panels:
+ - BLUE
+ YELLOW:
+ panel_group: Color Hunt Panels
+ panels:
+ - YELLOW
+ ORANGE:
+ panel_group: Color Hunt Panels
+ panels:
+ - ORANGE
+ PURPLE:
+ panel_group: Color Hunt Panels
+ panels:
+ - PURPLE
+ GREEN:
+ panel_group: Color Hunt Panels
+ panels:
+ - GREEN
+ paintings:
+ - id: arrows_painting_7
+ orientation: east
+ - id: fruitbowl_painting3
+ orientation: west
+ enter_only: True
+ required_door:
+ room: Outside The Initiated
+ door: Entrance
+ sunwarps:
+ - dots: 5
+ direction: exit
+ entrance_indicator_pos: [ 54, 2.5, 69.99 ]
+ orientation: north
+ Champion's Rest:
+ entrances:
+ Color Hunt:
+ room: Outside The Initiated
+ door: Entrance
+ panels:
+ YOU:
+ id: Color Arrow Room/Panel_you
+ check: True
+ colors: gray
+ tag: forbid
+ ME:
+ id: Color Arrow Room/Panel_me
+ colors: gray
+ tag: forbid
+ SECRET BLUE:
+ # Pretend this and the other two are white, because they are snipes.
+ # TODO: Extract them and randomize them?
+ id: Color Arrow Room/Panel_secret_blue
+ tag: forbid
+ SECRET YELLOW:
+ id: Color Arrow Room/Panel_secret_yellow
+ tag: forbid
+ SECRET RED:
+ id: Color Arrow Room/Panel_secret_red
+ tag: forbid
+ paintings:
+ - id: colors_painting
+ orientation: south
+ The Bearer:
+ entrances:
+ Outside The Bold:
+ door: Entrance
+ Outside The Initiated:
+ room: Art Gallery
+ door: Exit
+ The Bearer (East): True
+ The Bearer (North): True
+ The Bearer (South): True
+ The Bearer (West): True
+ Roof: True
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_bearer_bearer
+ check: True
+ tag: forbid
+ required_panel:
+ - panel: PART
+ - panel: HEART
+ - room: Cross Tower (East)
+ panel: WINTER
+ - room: The Bearer (East)
+ panel: PEACE
+ - room: Cross Tower (North)
+ panel: NORTH
+ - room: The Bearer (North)
+ panel: SILENT (1)
+ - room: The Bearer (North)
+ panel: SILENT (2)
+ - room: The Bearer (North)
+ panel: SPACE
+ - room: The Bearer (North)
+ panel: WARTS
+ - room: Cross Tower (South)
+ panel: FIRE
+ - room: The Bearer (South)
+ panel: TENT
+ - room: The Bearer (South)
+ panel: BOWL
+ - room: Cross Tower (West)
+ panel: DIAMONDS
+ - room: The Bearer (West)
+ panel: SNOW
+ - room: The Bearer (West)
+ panel: SMILE
+ - room: Bearer Side Area
+ panel: SHORTCUT
+ - room: Bearer Side Area
+ panel: POTS
+ achievement: The Bearer
+ MIDDLE:
+ id: Shuffle Room/Panel_middle_middle_2
+ tag: midwhite
+ FARTHER:
+ id: Backside Room/Panel_farther_far
+ colors: red
+ tag: midred
+ BACKSIDE:
+ id: Backside Room/Panel_backside_5
+ tag: midwhite
+ hunt: True
+ required_door:
+ door: Backside Door
+ PART:
+ id: Cross Room/Panel_part_rap
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ required_panel:
+ room: The Bearer (East)
+ panel: PEACE
+ HEART:
+ id: Cross Room/Panel_heart_tar
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ doors:
+ Entrance:
+ id: Red Blue Purple Room Area Doors/Door_middle_middle
+ item_group: Achievement Room Entrances
+ panels:
+ - MIDDLE
+ Backside Door:
+ id: Red Blue Purple Room Area Doors/Door_locked_knocked2 # yeah...
+ door_group: Backside Doors
+ panels:
+ - FARTHER
+ East Entrance:
+ event: True
+ panels:
+ - HEART
+ panel_doors:
+ FARTHER:
+ panel_group: Backside Entrance Panels
+ panels:
+ - FARTHER
+ MIDDLE:
+ panels:
+ - MIDDLE
+ The Bearer (East):
+ entrances:
+ Cross Tower (East): True
+ Bearer Side Area:
+ door: Side Area Access
+ Roof: True
+ panels:
+ SIX:
+ id: Backside Room/Panel_six_six_5
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Sixes
+ PEACE:
+ id: Cross Room/Panel_peace_ape
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ doors:
+ North Entrance:
+ event: True
+ panels:
+ - room: The Bearer
+ panel: PART
+ Side Area Access:
+ event: True
+ panels:
+ - room: The Bearer (North)
+ panel: SPACE
+ The Bearer (North):
+ entrances:
+ Cross Tower (East): True
+ Roof: True
+ panels:
+ SILENT (1):
+ id: Cross Room/Panel_silent_list
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ required_panel:
+ room: The Bearer (West)
+ panel: SMILE
+ SILENT (2):
+ id: Cross Room/Panel_silent_list_2
+ colors:
+ - red
+ - yellow
+ tag: mid yellow red
+ required_panel:
+ room: The Bearer (West)
+ panel: SMILE
+ SPACE:
+ id: Cross Room/Panel_space_cape
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ WARTS:
+ id: Cross Room/Panel_warts_star
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ required_panel:
+ room: The Bearer (West)
+ panel: SNOW
+ doors:
+ South Entrance:
+ event: True
+ panels:
+ - room: Bearer Side Area
+ panel: POTS
+ The Bearer (South):
+ entrances:
+ Cross Tower (North): True
+ Bearer Side Area:
+ door: Side Area Shortcut
+ Roof: True
+ panels:
+ SIX:
+ id: Backside Room/Panel_six_six_6
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Sixes
+ TENT:
+ id: Cross Room/Panel_tent_net
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ BOWL:
+ id: Cross Room/Panel_bowl_low
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ required_panel:
+ panel: TENT
+ doors:
+ Side Area Shortcut:
+ event: True
+ panels:
+ - room: The Bearer (North)
+ panel: SILENT (1)
+ The Bearer (West):
+ entrances:
+ Cross Tower (West): True
+ Bearer Side Area:
+ door: Side Area Shortcut
+ Roof: True
+ panels:
+ SMILE:
+ id: Cross Room/Panel_smile_lime
+ colors:
+ - red
+ - yellow
+ tag: mid yellow red
+ required_panel:
+ room: The Bearer (North)
+ panel: WARTS
+ SNOW:
+ id: Cross Room/Panel_snow_won
+ colors:
+ - red
+ - yellow
+ tag: mid red yellow
+ doors:
+ Side Area Shortcut:
+ event: True
+ panels:
+ - room: Cross Tower (East)
+ panel: WINTER
+ - room: Cross Tower (North)
+ panel: NORTH
+ - room: Cross Tower (South)
+ panel: FIRE
+ - room: Cross Tower (West)
+ panel: DIAMONDS
+ Bearer Side Area:
+ entrances:
+ The Bearer (East):
+ room: The Bearer (East)
+ door: Side Area Access
+ The Bearer (South):
+ room: The Bearer (South)
+ door: Side Area Shortcut
+ The Bearer (West):
+ room: The Bearer (West)
+ door: Side Area Shortcut
+ Orange Tower Third Floor:
+ door: Shortcut to Tower
+ Roof: True
+ panels:
+ SHORTCUT:
+ id: Cross Room/Panel_shortcut_shortcut
+ tag: midwhite
+ POTS:
+ id: Cross Room/Panel_pots_top
+ colors:
+ - red
+ - yellow
+ tag: mid yellow red
+ doors:
+ Shortcut to Tower:
+ id: Cross Room Doors/Door_shortcut
+ item_name: The Bearer - Shortcut to Tower
+ location_name: The Bearer - SHORTCUT
+ panels:
+ - SHORTCUT
+ West Entrance:
+ event: True
+ panels:
+ - room: The Bearer (South)
+ panel: BOWL
+ Cross Tower (East):
+ entrances:
+ The Bearer:
+ room: The Bearer
+ door: East Entrance
+ Roof: True
+ panels:
+ WINTER:
+ id: Cross Room/Panel_winter_winter
+ colors: blue
+ tag: forbid
+ required_panel:
+ room: The Bearer (North)
+ panel: SPACE
+ required_room: Orange Tower Fifth Floor
+ Cross Tower (North):
+ entrances:
+ The Bearer (East):
+ room: The Bearer (East)
+ door: North Entrance
+ Roof: True
+ panels:
+ NORTH:
+ id: Cross Room/Panel_north_north
+ colors: blue
+ tag: forbid
+ required_panel:
+ - room: The Bearer (West)
+ panel: SMILE
+ - room: Outside The Bold
+ panel: SOUND
+ - room: Outside The Bold
+ panel: YEAST
+ - room: Outside The Bold
+ panel: WET
+ Cross Tower (South):
+ entrances: # No roof access
+ The Bearer (North):
+ room: The Bearer (North)
+ door: South Entrance
+ panels:
+ FIRE:
+ id: Cross Room/Panel_fire_fire
+ colors: blue
+ tag: forbid
+ required_panel:
+ room: The Bearer (North)
+ panel: SILENT (1)
+ required_room: Elements Area
+ Cross Tower (West):
+ entrances:
+ Bearer Side Area:
+ room: Bearer Side Area
+ door: West Entrance
+ Roof: True
+ panels:
+ DIAMONDS:
+ id: Cross Room/Panel_diamonds_diamonds
+ colors: blue
+ tag: forbid
+ required_panel:
+ room: The Bearer (North)
+ panel: WARTS
+ required_room: Suits Area
+ The Steady (Rose):
+ entrances:
+ Outside The Bold:
+ room: Outside The Bold
+ door: Steady Entrance
+ The Steady (Lilac):
+ room: The Steady
+ door: Reveal
+ The Steady (Ruby):
+ door: Forward Exit
+ The Steady (Carnation):
+ door: Right Exit
+ panels:
+ SOAR:
+ id: Rock Room/Panel_soar_rose
+ colors: black
+ tag: topblack
+ doors:
+ Forward Exit:
+ event: True
+ panels:
+ - SOAR
+ Right Exit:
+ event: True
+ panels:
+ - room: The Steady (Lilac)
+ panel: LIE LACK
+ The Steady (Ruby):
+ entrances:
+ The Steady (Rose):
+ room: The Steady (Rose)
+ door: Forward Exit
+ The Steady (Amethyst):
+ room: The Steady
+ door: Reveal
+ The Steady (Cherry):
+ door: Forward Exit
+ The Steady (Amber):
+ door: Right Exit
+ panels:
+ BURY:
+ id: Rock Room/Panel_bury_ruby
+ colors: yellow
+ tag: midyellow
+ doors:
+ Forward Exit:
+ event: True
+ panels:
+ - room: The Steady (Lime)
+ panel: LIMELIGHT
+ Right Exit:
+ event: True
+ panels:
+ - room: The Steady (Carnation)
+ panel: INCARNATION
+ The Steady (Carnation):
+ entrances:
+ The Steady (Rose):
+ room: The Steady (Rose)
+ door: Right Exit
+ Outside The Bold:
+ room: The Steady
+ door: Reveal
+ The Steady (Amber):
+ room: The Steady
+ door: Reveal
+ The Steady (Sunflower):
+ door: Right Exit
+ panels:
+ INCARNATION:
+ id: Rock Room/Panel_incarnation_carnation
+ colors: red
+ tag: midred
+ doors:
+ Right Exit:
+ event: True
+ panels:
+ - room: The Steady (Amethyst)
+ panel: PACIFIST
+ The Steady (Sunflower):
+ entrances:
+ The Steady (Carnation):
+ room: The Steady (Carnation)
+ door: Right Exit
+ The Steady (Topaz):
+ room: The Steady (Topaz)
+ door: Back Exit
+ panels:
+ SUN:
+ id: Rock Room/Panel_sun_sunflower
+ colors: blue
+ tag: midblue
+ doors:
+ Back Exit:
+ event: True
+ panels:
+ - SUN
+ The Steady (Plum):
+ entrances:
+ The Steady (Amethyst):
+ room: The Steady
+ door: Reveal
+ The Steady (Blueberry):
+ room: The Steady
+ door: Reveal
+ The Steady (Cherry):
+ room: The Steady (Cherry)
+ door: Left Exit
+ panels:
+ LUMP:
+ id: Rock Room/Panel_lump_plum
+ colors: yellow
+ tag: midyellow
+ The Steady (Lime):
+ entrances:
+ The Steady (Sunflower):
+ warp: True
+ The Steady (Emerald):
+ room: The Steady
+ door: Reveal
+ The Steady (Blueberry):
+ door: Right Exit
+ panels:
+ LIMELIGHT:
+ id: Rock Room/Panel_limelight_lime
+ colors: red
+ tag: midred
+ doors:
+ Right Exit:
+ event: True
+ panels:
+ - room: The Steady (Amber)
+ panel: ANTECHAMBER
+ paintings:
+ - id: pencil_painting5
+ orientation: south
+ The Steady (Lemon):
+ entrances:
+ The Steady (Emerald):
+ warp: True
+ The Steady (Orange):
+ room: The Steady
+ door: Reveal
+ The Steady (Topaz):
+ door: Back Exit
+ panels:
+ MELON:
+ id: Rock Room/Panel_melon_lemon
+ colors: yellow
+ tag: midyellow
+ doors:
+ Back Exit:
+ event: True
+ panels:
+ - MELON
+ paintings:
+ - id: pencil_painting4
+ orientation: south
+ The Steady (Topaz):
+ entrances:
+ The Steady (Lemon):
+ room: The Steady (Lemon)
+ door: Back Exit
+ The Steady (Amber):
+ room: The Steady
+ door: Reveal
+ The Steady (Sunflower):
+ door: Back Exit
+ panels:
+ TOP:
+ id: Rock Room/Panel_top_topaz
+ colors: blue
+ tag: midblue
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery2
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ doors:
+ Back Exit:
+ event: True
+ panels:
+ - TOP
+ The Steady (Orange):
+ entrances:
+ The Steady (Cherry):
+ room: The Steady
+ door: Reveal
+ The Steady (Lemon):
+ room: The Steady
+ door: Reveal
+ The Steady (Amber):
+ room: The Steady (Amber)
+ door: Forward Exit
+ panels:
+ BLUE:
+ id: Rock Room/Panel_blue_orange
+ colors: black
+ tag: botblack
+ The Steady (Sapphire):
+ entrances:
+ The Steady (Emerald):
+ door: Left Exit
+ The Steady (Blueberry):
+ room: The Steady
+ door: Reveal
+ The Steady (Amethyst):
+ room: The Steady (Amethyst)
+ door: Left Exit
+ panels:
+ SAP:
+ id: Rock Room/Panel_sap_sapphire
+ colors: blue
+ tag: midblue
+ doors:
+ Left Exit:
+ event: True
+ panels:
+ - room: The Steady (Plum)
+ panel: LUMP
+ - room: The Steady (Orange)
+ panel: BLUE
+ The Steady (Blueberry):
+ entrances:
+ The Steady (Lime):
+ room: The Steady (Lime)
+ door: Right Exit
+ The Steady (Sapphire):
+ room: The Steady
+ door: Reveal
+ The Steady (Plum):
+ room: The Steady
+ door: Reveal
+ panels:
+ BLUE:
+ id: Rock Room/Panel_blue_blueberry
+ colors: blue
+ tag: midblue
+ The Steady (Amber):
+ entrances:
+ The Steady (Ruby):
+ room: The Steady (Ruby)
+ door: Right Exit
+ The Steady (Carnation):
+ room: The Steady
+ door: Reveal
+ The Steady (Orange):
+ door: Forward Exit
+ The Steady (Topaz):
+ room: The Steady
+ door: Reveal
+ panels:
+ ANTECHAMBER:
+ id: Rock Room/Panel_antechamber_amber
+ colors: red
+ tag: midred
+ doors:
+ Forward Exit:
+ event: True
+ panels:
+ - room: The Steady (Blueberry)
+ panel: BLUE
+ The Steady (Emerald):
+ entrances:
+ The Steady (Sapphire):
+ room: The Steady (Sapphire)
+ door: Left Exit
+ The Steady (Lime):
+ room: The Steady
+ door: Reveal
+ panels:
+ HERALD:
+ id: Rock Room/Panel_herald_emerald
+ colors: purple
+ tag: midpurp
+ The Steady (Amethyst):
+ entrances:
+ The Steady (Lilac):
+ room: The Steady (Lilac)
+ door: Forward Exit
+ The Steady (Sapphire):
+ door: Left Exit
+ The Steady (Plum):
+ room: The Steady
+ door: Reveal
+ The Steady (Ruby):
+ room: The Steady
+ door: Reveal
+ panels:
+ PACIFIST:
+ id: Rock Room/Panel_thistle_amethyst
+ colors: purple
+ tag: toppurp
+ doors:
+ Left Exit:
+ event: True
+ panels:
+ - room: The Steady (Sunflower)
+ panel: SUN
+ The Steady (Lilac):
+ entrances:
+ Outside The Bold:
+ room: Outside The Bold
+ door: Lilac Entrance
+ The Steady (Amethyst):
+ door: Forward Exit
+ The Steady (Rose):
+ room: The Steady
+ door: Reveal
+ panels:
+ LIE LACK:
+ id: Rock Room/Panel_lielack_lilac
+ tag: topwhite
+ doors:
+ Forward Exit:
+ event: True
+ panels:
+ - room: The Steady (Ruby)
+ panel: BURY
+ The Steady (Cherry):
+ entrances:
+ The Steady (Plum):
+ door: Left Exit
+ The Steady (Orange):
+ room: The Steady
+ door: Reveal
+ The Steady (Ruby):
+ room: The Steady (Ruby)
+ door: Forward Exit
+ panels:
+ HAIRY:
+ id: Rock Room/Panel_hairy_cherry
+ colors: blue
+ tag: topblue
+ doors:
+ Left Exit:
+ event: True
+ panels:
+ - room: The Steady (Sapphire)
+ panel: SAP
+ The Steady:
+ entrances:
+ The Steady (Sunflower):
+ room: The Steady (Sunflower)
+ door: Back Exit
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_steady_steady
+ required_panel:
+ - room: The Steady (Rose)
+ panel: SOAR
+ - room: The Steady (Carnation)
+ panel: INCARNATION
+ - room: The Steady (Sunflower)
+ panel: SUN
+ - room: The Steady (Ruby)
+ panel: BURY
+ - room: The Steady (Plum)
+ panel: LUMP
+ - room: The Steady (Lime)
+ panel: LIMELIGHT
+ - room: The Steady (Lemon)
+ panel: MELON
+ - room: The Steady (Topaz)
+ panel: TOP
+ - room: The Steady (Orange)
+ panel: BLUE
+ - room: The Steady (Sapphire)
+ panel: SAP
+ - room: The Steady (Blueberry)
+ panel: BLUE
+ - room: The Steady (Amber)
+ panel: ANTECHAMBER
+ - room: The Steady (Emerald)
+ panel: HERALD
+ - room: The Steady (Amethyst)
+ panel: PACIFIST
+ - room: The Steady (Lilac)
+ panel: LIE LACK
+ - room: The Steady (Cherry)
+ panel: HAIRY
+ tag: forbid
+ check: True
+ achievement: The Steady
+ doors:
+ Reveal:
+ event: True
+ panels:
+ - Achievement
+ Knight Night (Outer Ring):
+ entrances:
+ Hidden Room:
+ room: Hidden Room
+ door: Knight Night Entrance
+ Knight Night Exit: True
+ panels:
+ NIGHT:
+ id: Appendix Room/Panel_night_knight
+ colors: blue
+ tag: homophone midblue
+ copy_to_sign: sign7
+ KNIGHT:
+ id: Appendix Room/Panel_knight_night
+ colors: red
+ tag: homophone midred
+ copy_to_sign: sign8
+ BEE:
+ id: Appendix Room/Panel_bee_be
+ colors: red
+ tag: homophone midred
+ copy_to_sign: sign9
+ NEW:
+ id: Appendix Room/Panel_new_knew
+ colors: blue
+ tag: homophone midblue
+ copy_to_sign: sign11
+ FORE:
+ id: Appendix Room/Panel_fore_for
+ colors: red
+ tag: homophone midred
+ copy_to_sign: sign10
+ TRUSTED (1):
+ id: Appendix Room/Panel_trusted_trust
+ colors: red
+ tag: midred
+ required_panel:
+ room: Knight Night (Right Lower Segment)
+ panel: BEFORE
+ TRUSTED (2):
+ id: Appendix Room/Panel_trusted_rusted
+ colors: red
+ tag: midred
+ required_panel:
+ room: Knight Night (Right Lower Segment)
+ panel: BEFORE
+ ENCRUSTED:
+ id: Appendix Room/Panel_encrusted_rust
+ colors: red
+ tag: midred
+ required_panel:
+ - panel: TRUSTED (1)
+ - panel: TRUSTED (2)
+ ADJUST (1):
+ id: Appendix Room/Panel_adjust_readjust
+ colors: blue
+ tag: midblue and phone
+ required_panel:
+ room: Knight Night (Right Lower Segment)
+ panel: BE
+ ADJUST (2):
+ id: Appendix Room/Panel_adjust_adjusted
+ colors: blue
+ tag: midblue and phone
+ required_panel:
+ room: Knight Night (Right Lower Segment)
+ panel: BE
+ RIGHT:
+ id: Appendix Room/Panel_right_right
+ tag: midwhite
+ required_panel:
+ room: Knight Night (Right Lower Segment)
+ panel: ADJUST
+ TRUST:
+ id: Appendix Room/Panel_trust_crust
+ colors:
+ - red
+ - blue
+ tag: chain mid red blue
+ required_panel:
+ - room: Knight Night (Right Lower Segment)
+ panel: ADJUST
+ - room: Knight Night (Right Lower Segment)
+ panel: LEFT
+ doors:
+ Fore Door:
+ event: True
+ panels:
+ - FORE
+ New Door:
+ event: True
+ panels:
+ - NEW
+ To End:
+ event: True
+ panels:
+ - RIGHT
+ - room: Knight Night (Right Lower Segment)
+ panel: LEFT
+ Knight Night (Right Upper Segment):
+ entrances:
+ Knight Night Exit: True
+ Knight Night (Outer Ring):
+ room: Knight Night (Outer Ring)
+ door: Fore Door
+ warp: True
+ Knight Night (Right Lower Segment):
+ door: Segment Door
+ warp: True
+ panels:
+ RUST (1):
+ id: Appendix Room/Panel_rust_trust
+ colors: blue
+ tag: midblue
+ required_panel:
+ room: Knight Night (Outer Ring)
+ panel: BEE
+ RUST (2):
+ id: Appendix Room/Panel_rust_crust
+ colors: blue
+ tag: midblue
+ required_panel:
+ room: Knight Night (Outer Ring)
+ panel: BEE
+ doors:
+ Segment Door:
+ event: True
+ panels:
+ - RUST (2)
+ - room: Knight Night (Right Lower Segment)
+ panel: BEFORE
+ Knight Night (Right Lower Segment):
+ entrances:
+ Knight Night Exit: True
+ Knight Night (Right Upper Segment):
+ room: Knight Night (Right Upper Segment)
+ door: Segment Door
+ warp: True
+ Knight Night (Outer Ring):
+ room: Knight Night (Outer Ring)
+ door: New Door
+ warp: True
+ panels:
+ ADJUST:
+ id: Appendix Room/Panel_adjust_readjusted
+ colors: blue
+ tag: midblue
+ required_panel:
+ - room: Knight Night (Outer Ring)
+ panel: ADJUST (1)
+ - room: Knight Night (Outer Ring)
+ panel: ADJUST (2)
+ BEFORE:
+ id: Appendix Room/Panel_before_fore
+ colors: red
+ tag: midred and phone
+ required_panel:
+ room: Knight Night (Right Upper Segment)
+ panel: RUST (1)
+ BE:
+ id: Appendix Room/Panel_be_before
+ colors: blue
+ tag: midblue and phone
+ required_panel:
+ room: Knight Night (Right Upper Segment)
+ panel: RUST (1)
+ LEFT:
+ id: Appendix Room/Panel_left_left
+ tag: midwhite
+ required_panel:
+ room: Knight Night (Outer Ring)
+ panel: ENCRUSTED
+ TRUST:
+ id: Appendix Room/Panel_trust_crust_2
+ colors: purple
+ tag: midpurp
+ required_panel:
+ - room: Knight Night (Outer Ring)
+ panel: ENCRUSTED
+ - room: Knight Night (Outer Ring)
+ panel: RIGHT
+ Knight Night (Final):
+ entrances:
+ Knight Night Exit: True
+ Knight Night (Outer Ring):
+ room: Knight Night (Outer Ring)
+ door: To End
+ warp: True
+ Knight Night (Right Upper Segment):
+ room: Knight Night (Outer Ring)
+ door: To End
+ warp: True
+ panels:
+ TRUSTED:
+ id: Appendix Room/Panel_trusted_readjusted
+ colors: purple
+ tag: midpurp
+ doors:
+ Exit:
+ id:
+ - Appendix Room Area Doors/Door_trusted_readjusted
+ - Appendix Room Area Doors/Door_trusted_readjusted2
+ - Appendix Room Area Doors/Door_trusted_readjusted3
+ - Appendix Room Area Doors/Door_trusted_readjusted4
+ - Appendix Room Area Doors/Door_trusted_readjusted5
+ - Appendix Room Area Doors/Door_trusted_readjusted6
+ - Appendix Room Area Doors/Door_trusted_readjusted7
+ - Appendix Room Area Doors/Door_trusted_readjusted8
+ - Appendix Room Area Doors/Door_trusted_readjusted9
+ - Appendix Room Area Doors/Door_trusted_readjusted10
+ - Appendix Room Area Doors/Door_trusted_readjusted11
+ - Appendix Room Area Doors/Door_trusted_readjusted12
+ - Appendix Room Area Doors/Door_trusted_readjusted13
+ include_reduce: True
+ location_name: Knight Night Room - TRUSTED
+ item_name: Knight Night Room - Exit
+ panels:
+ - TRUSTED
+ panel_doors:
+ TRUSTED:
+ item_name: Knight Night Room - TRUSTED (Panel)
+ panels:
+ - TRUSTED
+ Knight Night Exit:
+ entrances:
+ Knight Night (Outer Ring):
+ room: Knight Night (Final)
+ door: Exit
+ Orange Tower Third Floor:
+ room: Knight Night (Final)
+ door: Exit
+ Outside The Initiated:
+ room: Knight Night (Final)
+ door: Exit
+ panels:
+ SEVEN (1):
+ id: Backside Room/Panel_seven_seven_7
+ tag: midwhite
+ hunt: True
+ required_door:
+ - room: Number Hunt
+ door: Sevens
+ SEVEN (2):
+ id: Backside Room/Panel_seven_seven_3
+ tag: midwhite
+ hunt: True
+ required_door:
+ - room: Number Hunt
+ door: Sevens
+ SEVEN (3):
+ id: Backside Room/Panel_seven_seven_4
+ tag: midwhite
+ hunt: True
+ required_door:
+ - room: Number Hunt
+ door: Sevens
+ DEAD END:
+ id: Appendix Room/Panel_deadend_deadend
+ tag: midwhite
+ WARNER:
+ id: Appendix Room/Panel_warner_corner
+ colors: purple
+ tag: toppurp
+ The Artistic (Smiley):
+ entrances:
+ Dead End Area:
+ painting: True
+ Crossroads:
+ painting: True
+ Hot Crusts Area:
+ painting: True
+ Outside The Initiated:
+ painting: True
+ Directional Gallery:
+ painting: True
+ Number Hunt:
+ room: Number Hunt
+ door: Eights
+ painting: True
+ Art Gallery:
+ painting: True
+ The Eyes They See:
+ painting: True
+ The Artistic (Panda):
+ door: Door to Panda
+ The Artistic (Apple):
+ room: The Artistic (Apple)
+ door: Door to Smiley
+ Elements Area:
+ room: Hallway Room (4)
+ door: Exit
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_artistic_artistic
+ colors:
+ - red
+ - black
+ - yellow
+ - blue
+ tag: forbid
+ required_room:
+ - The Artistic (Panda)
+ - The Artistic (Apple)
+ - The Artistic (Lattice)
+ check: True
+ location_name: The Artistic - Achievement
+ achievement: The Artistic
+ FINE:
+ id: Ceiling Room/Panel_yellow_top_5
+ colors:
+ - yellow
+ - blue
+ tag: yellow top blue bot
+ subtag: top
+ link: yxu KNIFE
+ BLADE:
+ id: Ceiling Room/Panel_blue_bot_5
+ colors:
+ - blue
+ - yellow
+ tag: yellow top blue bot
+ subtag: bot
+ link: yxu KNIFE
+ RED:
+ id: Ceiling Room/Panel_blue_top_6
+ colors:
+ - blue
+ - yellow
+ tag: blue top yellow mid
+ subtag: top
+ link: uyx BREAD
+ BEARD:
+ id: Ceiling Room/Panel_yellow_mid_6
+ colors:
+ - yellow
+ - blue
+ tag: blue top yellow mid
+ subtag: mid
+ link: uyx BREAD
+ ICE:
+ id: Ceiling Room/Panel_blue_mid_7
+ colors:
+ - blue
+ - yellow
+ tag: blue mid yellow bot
+ subtag: mid
+ link: xuy SPICE
+ ROOT:
+ id: Ceiling Room/Panel_yellow_bot_7
+ colors:
+ - yellow
+ - blue
+ tag: blue mid yellow bot
+ subtag: bot
+ link: xuy SPICE
+ doors:
+ Door to Panda:
+ id:
+ - Ceiling Room Doors/Door_blue
+ - Ceiling Room Doors/Door_blue2
+ location_name: The Artistic - Smiley and Panda
+ door_group: Artistic Doors
+ panels:
+ - FINE
+ - BLADE
+ - RED
+ - BEARD
+ - ICE
+ - ROOT
+ - room: The Artistic (Panda)
+ panel: EYE (Top)
+ - room: The Artistic (Panda)
+ panel: EYE (Bottom)
+ - room: The Artistic (Panda)
+ panel: LADYLIKE
+ - room: The Artistic (Panda)
+ panel: WATER
+ - room: The Artistic (Panda)
+ panel: OURS
+ - room: The Artistic (Panda)
+ panel: DAYS
+ - room: The Artistic (Panda)
+ panel: NIGHTTIME
+ - room: The Artistic (Panda)
+ panel: NIGHT
+ paintings:
+ - id: smile_painting_9
+ orientation: north
+ exit_only: True
+ The Artistic (Panda):
+ entrances:
+ Orange Tower Sixth Floor:
+ painting: True
+ Hallway Room (1):
+ painting: True
+ The Artistic (Smiley):
+ room: The Artistic (Smiley)
+ door: Door to Panda
+ The Artistic (Lattice):
+ door: Door to Lattice
+ panels:
+ EYE (Top):
+ id: Ceiling Room/Panel_blue_top_1
+ colors:
+ - blue
+ - red
+ tag: blue top red bot
+ subtag: top
+ link: uxr IRIS
+ EYE (Bottom):
+ id: Ceiling Room/Panel_red_bot_1
+ colors:
+ - red
+ - blue
+ tag: blue top red bot
+ subtag: bot
+ link: uxr IRIS
+ LADYLIKE:
+ id: Ceiling Room/Panel_red_mid_2
+ colors:
+ - red
+ - blue
+ tag: red mid blue bot
+ subtag: mid
+ link: xru LAKE
+ WATER:
+ id: Ceiling Room/Panel_blue_bot_2
+ colors:
+ - blue
+ - red
+ tag: red mid blue bot
+ subtag: bot
+ link: xru LAKE
+ OURS:
+ id: Ceiling Room/Panel_blue_mid_3
+ colors:
+ - blue
+ - red
+ tag: blue mid red bot
+ subtag: mid
+ link: xur HOURS
+ DAYS:
+ id: Ceiling Room/Panel_red_bot_3
+ colors:
+ - red
+ - blue
+ tag: blue mid red bot
+ subtag: bot
+ link: xur HOURS
+ NIGHTTIME:
+ id: Ceiling Room/Panel_red_top_4
+ colors:
+ - red
+ - blue
+ tag: red top mid blue
+ subtag: top
+ link: rux KNIGHT
+ NIGHT:
+ id: Ceiling Room/Panel_blue_mid_4
+ colors:
+ - blue
+ - red
+ tag: red top mid blue
+ subtag: mid
+ link: rux KNIGHT
+ doors:
+ Door to Lattice:
+ id:
+ - Ceiling Room Doors/Door_red
+ - Ceiling Room Doors/Door_red2
+ location_name: The Artistic - Panda and Lattice
+ door_group: Artistic Doors
+ panels:
+ - EYE (Top)
+ - EYE (Bottom)
+ - LADYLIKE
+ - WATER
+ - OURS
+ - DAYS
+ - NIGHTTIME
+ - NIGHT
+ - room: The Artistic (Lattice)
+ panel: POSH
+ - room: The Artistic (Lattice)
+ panel: MALL
+ - room: The Artistic (Lattice)
+ panel: DEICIDE
+ - room: The Artistic (Lattice)
+ panel: WAVER
+ - room: The Artistic (Lattice)
+ panel: REPAID
+ - room: The Artistic (Lattice)
+ panel: BABY
+ - room: The Artistic (Lattice)
+ panel: LOBE
+ - room: The Artistic (Lattice)
+ panel: BOWELS
+ paintings:
+ - id: panda_painting_3
+ exit_only: True
+ orientation: south
+ required_when_no_doors: True
+ The Artistic (Lattice):
+ entrances:
+ Directional Gallery:
+ painting: True
+ The Artistic (Panda):
+ room: The Artistic (Panda)
+ door: Door to Lattice
+ The Artistic (Apple):
+ door: Door to Apple
+ panels:
+ POSH:
+ id: Ceiling Room/Panel_black_top_12
+ colors:
+ - black
+ - red
+ tag: black top red bot
+ subtag: top
+ link: bxr SHOP
+ MALL:
+ id: Ceiling Room/Panel_red_bot_12
+ colors:
+ - red
+ - black
+ tag: black top red bot
+ subtag: bot
+ link: bxr SHOP
+ DEICIDE:
+ id: Ceiling Room/Panel_red_top_13
+ colors:
+ - red
+ - black
+ tag: red top black bot
+ subtag: top
+ link: rxb DECIDE
+ WAVER:
+ id: Ceiling Room/Panel_black_bot_13
+ colors:
+ - black
+ - red
+ tag: red top black bot
+ subtag: bot
+ link: rxb DECIDE
+ REPAID:
+ id: Ceiling Room/Panel_black_mid_14
+ colors:
+ - black
+ - red
+ tag: black mid red bot
+ subtag: mid
+ link: xbr DIAPER
+ BABY:
+ id: Ceiling Room/Panel_red_bot_14
+ colors:
+ - red
+ - black
+ tag: black mid red bot
+ subtag: bot
+ link: xbr DIAPER
+ LOBE:
+ id: Ceiling Room/Panel_black_top_15
+ colors:
+ - black
+ - red
+ tag: black top red mid
+ subtag: top
+ link: brx BOWL
+ BOWELS:
+ id: Ceiling Room/Panel_red_mid_15
+ colors:
+ - red
+ - black
+ tag: black top red mid
+ subtag: mid
+ link: brx BOWL
+ doors:
+ Door to Apple:
+ id:
+ - Ceiling Room Doors/Door_black
+ - Ceiling Room Doors/Door_black2
+ location_name: The Artistic - Lattice and Apple
+ door_group: Artistic Doors
+ panels:
+ - POSH
+ - MALL
+ - DEICIDE
+ - WAVER
+ - REPAID
+ - BABY
+ - LOBE
+ - BOWELS
+ - room: The Artistic (Apple)
+ panel: SPRIG
+ - room: The Artistic (Apple)
+ panel: RELEASES
+ - room: The Artistic (Apple)
+ panel: MUCH
+ - room: The Artistic (Apple)
+ panel: FISH
+ - room: The Artistic (Apple)
+ panel: MASK
+ - room: The Artistic (Apple)
+ panel: HILL
+ - room: The Artistic (Apple)
+ panel: TINE
+ - room: The Artistic (Apple)
+ panel: THING
+ paintings:
+ - id: boxes_painting2
+ orientation: south
+ exit_only: True
+ required_when_no_doors: True
+ The Artistic (Apple):
+ entrances:
+ Orange Tower Sixth Floor:
+ painting: True
+ Directional Gallery:
+ painting: True
+ The Artistic (Lattice):
+ room: The Artistic (Lattice)
+ door: Door to Apple
+ The Artistic (Smiley):
+ door: Door to Smiley
+ panels:
+ SPRIG:
+ id: Ceiling Room/Panel_yellow_mid_8
+ colors:
+ - yellow
+ - black
+ tag: yellow mid black bot
+ subtag: mid
+ link: xyb GRIPS
+ RELEASES:
+ id: Ceiling Room/Panel_black_bot_8
+ colors:
+ - black
+ - yellow
+ tag: yellow mid black bot
+ subtag: bot
+ link: xyb GRIPS
+ MUCH:
+ id: Ceiling Room/Panel_black_top_9
+ colors:
+ - black
+ - yellow
+ tag: black top yellow bot
+ subtag: top
+ link: bxy CHUM
+ FISH:
+ id: Ceiling Room/Panel_yellow_bot_9
+ colors:
+ - yellow
+ - black
+ tag: black top yellow bot
+ subtag: bot
+ link: bxy CHUM
+ MASK:
+ id: Ceiling Room/Panel_yellow_top_10
+ colors:
+ - yellow
+ - black
+ tag: yellow top black bot
+ subtag: top
+ link: yxb CHASM
+ HILL:
+ id: Ceiling Room/Panel_black_bot_10
+ colors:
+ - black
+ - yellow
+ tag: yellow top black bot
+ subtag: bot
+ link: yxb CHASM
+ TINE:
+ id: Ceiling Room/Panel_black_top_11
+ colors:
+ - black
+ - yellow
+ tag: black top yellow mid
+ subtag: top
+ link: byx NIGHT
+ THING:
+ id: Ceiling Room/Panel_yellow_mid_11
+ colors:
+ - yellow
+ - black
+ tag: black top yellow mid
+ subtag: mid
+ link: byx NIGHT
+ doors:
+ Door to Smiley:
+ id:
+ - Ceiling Room Doors/Door_yellow
+ - Ceiling Room Doors/Door_yellow2
+ location_name: The Artistic - Apple and Smiley
+ door_group: Artistic Doors
+ panels:
+ - SPRIG
+ - RELEASES
+ - MUCH
+ - FISH
+ - MASK
+ - HILL
+ - TINE
+ - THING
+ - room: The Artistic (Smiley)
+ panel: FINE
+ - room: The Artistic (Smiley)
+ panel: BLADE
+ - room: The Artistic (Smiley)
+ panel: RED
+ - room: The Artistic (Smiley)
+ panel: BEARD
+ - room: The Artistic (Smiley)
+ panel: ICE
+ - room: The Artistic (Smiley)
+ panel: ROOT
+ paintings:
+ - id: cherry_painting3
+ orientation: north
+ exit_only: True
+ required_when_no_doors: True
+ The Artistic (Hint Room):
+ entrances:
+ The Artistic (Lattice):
+ room: The Artistic (Lattice)
+ door: Door to Apple
+ panels:
+ THEME:
+ id: Ceiling Room/Panel_answer_1
+ colors: red
+ tag: midred
+ PAINTS:
+ id: Ceiling Room/Panel_answer_2
+ colors: yellow
+ tag: botyellow
+ I:
+ id: Ceiling Room/Panel_answer_3
+ colors: blue
+ tag: midblue
+ KIT:
+ id: Ceiling Room/Panel_answer_4
+ colors: black
+ tag: topblack
+ The Discerning:
+ entrances:
+ Crossroads:
+ room: Crossroads
+ door: Discerning Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_discerning_scramble
+ colors: yellow
+ tag: forbid
+ check: True
+ achievement: The Discerning
+ HITS:
+ id: Sun Room/Panel_hits_this
+ colors: yellow
+ tag: midyellow
+ WARRED:
+ id: Sun Room/Panel_warred_drawer
+ colors: yellow
+ tag: double midyellow
+ subtag: left
+ link: ana DRAWER
+ REDRAW:
+ id: Sun Room/Panel_redraw_drawer
+ colors: yellow
+ tag: double midyellow
+ subtag: right
+ link: ana DRAWER
+ ADDER:
+ id: Sun Room/Panel_adder_dread
+ colors: yellow
+ tag: midyellow
+ LAUGHTERS:
+ id: Sun Room/Panel_laughters_slaughter
+ colors: yellow
+ tag: midyellow
+ STONE:
+ id: Sun Room/Panel_stone_notes
+ colors: yellow
+ tag: double midyellow
+ subtag: left
+ link: ana NOTES
+ ONSET:
+ id: Sun Room/Panel_onset_notes
+ colors: yellow
+ tag: double midyellow
+ subtag: right
+ link: ana NOTES
+ RAT:
+ id: Sun Room/Panel_rat_art
+ colors: yellow
+ tag: midyellow
+ DUSTY:
+ id: Sun Room/Panel_dusty_study
+ colors: yellow
+ tag: midyellow
+ ARTS:
+ id: Sun Room/Panel_arts_star
+ colors: yellow
+ tag: double midyellow
+ subtag: left
+ link: ana STAR
+ TSAR:
+ id: Sun Room/Panel_tsar_star
+ colors: yellow
+ tag: double midyellow
+ subtag: right
+ link: ana STAR
+ STATE:
+ id: Sun Room/Panel_state_taste
+ colors: yellow
+ tag: midyellow
+ REACT:
+ id: Sun Room/Panel_react_trace
+ colors: yellow
+ tag: midyellow
+ DEAR:
+ id: Sun Room/Panel_dear_read
+ colors: yellow
+ tag: double midyellow
+ subtag: left
+ link: ana READ
+ DARE:
+ id: Sun Room/Panel_dare_read
+ colors: yellow
+ tag: double midyellow
+ subtag: right
+ link: ana READ
+ SEAM:
+ id: Sun Room/Panel_seam_same
+ colors: yellow
+ tag: midyellow
+ The Eyes They See:
+ entrances:
+ Crossroads:
+ room: Crossroads
+ door: Eye Wall
+ painting: True
+ Wondrous Lobby:
+ door: Exit
+ Directional Gallery:
+ warp: True
+ panels:
+ NEAR:
+ id: Shuffle Room/Panel_near_near
+ tag: midwhite
+ EIGHT:
+ id: Backside Room/Panel_eight_eight_4
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Eights
+ doors:
+ Exit:
+ id: Count Up Room Area Doors/Door_near_near
+ door_group: Crossroads Doors
+ panels:
+ - NEAR
+ paintings:
+ - id: eye_painting_2
+ orientation: west
+ - id: smile_painting_2
+ orientation: north
+ Far Window:
+ entrances:
+ Crossroads:
+ room: Crossroads
+ door: Eye Wall
+ The Eyes They See: True
+ panels:
+ FAR:
+ id: Shuffle Room/Panel_far_far
+ tag: midwhite
+ Wondrous Lobby:
+ entrances:
+ Directional Gallery:
+ warp: True
+ The Eyes They See:
+ room: The Eyes They See
+ door: Exit
+ paintings:
+ - id: arrows_painting_5
+ orientation: east
+ Outside The Wondrous:
+ entrances:
+ Wondrous Lobby:
+ warp: True
+ The Wondrous (Doorknob):
+ door: Wondrous Entrance
+ The Wondrous (Window):
+ warp: True
+ panels:
+ SHRINK:
+ id: Wonderland Room/Panel_shrink_shrink
+ tag: midwhite
+ doors:
+ Wondrous Entrance:
+ id: Red Blue Purple Room Area Doors/Door_wonderland
+ item_name: The Wondrous - Entrance
+ item_group: Achievement Room Entrances
+ panels:
+ - SHRINK
+ panel_doors:
+ SHRINK:
+ panels:
+ - SHRINK
+ The Wondrous (Doorknob):
+ entrances:
+ Outside The Wondrous:
+ room: Outside The Wondrous
+ door: Wondrous Entrance
+ Starting Room:
+ door: Painting Shortcut
+ painting: True
+ The Wondrous (Chandelier):
+ painting: True
+ The Wondrous (Table):
+ - painting: True
+ - warp: True
+ doors:
+ Painting Shortcut:
+ painting_id:
+ - symmetry_painting_a_starter
+ - arrows_painting2
+ skip_location: True
+ item_name: Starting Room - Symmetry Painting
+ item_group: Paintings
+ panels:
+ - room: Outside The Wondrous
+ panel: SHRINK
+ paintings:
+ - id: symmetry_painting_a_1
+ orientation: east
+ exit_only: True
+ - id: symmetry_painting_b_1
+ orientation: south
+ The Wondrous (Bookcase):
+ entrances:
+ The Wondrous (Doorknob): True
+ panels:
+ CASE:
+ id: Wonderland Room/Panel_case_bookcase
+ colors: blue
+ tag: midblue
+ paintings:
+ - id: symmetry_painting_a_3
+ orientation: west
+ exit_only: True
+ - id: symmetry_painting_b_3
+ disable: True
+ The Wondrous (Chandelier):
+ entrances:
+ The Wondrous (Bookcase): True
+ panels:
+ CANDLE HEIR:
+ id: Wonderland Room/Panel_candleheir_chandelier
+ colors: yellow
+ tag: midyellow
+ paintings:
+ - id: symmetry_painting_a_5
+ orientation: east
+ - id: symmetry_painting_b_5
+ disable: True
+ The Wondrous (Window):
+ entrances:
+ The Wondrous (Bookcase): True
+ panels:
+ GLASS:
+ id: Wonderland Room/Panel_glass_window
+ colors: brown
+ tag: botbrown
+ paintings:
+ - id: symmetry_painting_b_4
+ orientation: north
+ exit_only: True
+ - id: symmetry_painting_a_4
+ disable: True
+ The Wondrous (Table):
+ entrances:
+ The Wondrous (Doorknob):
+ painting: True
+ The Wondrous:
+ painting: True
+ panels:
+ WOOD:
+ id: Wonderland Room/Panel_wood_table
+ colors: brown
+ tag: botbrown
+ BROOK NOD:
+ # This panel, while physically being in the first room, is facing upward
+ # and is only really solvable while standing on the windowsill, which is
+ # a location you can only get to from Table.
+ id: Wonderland Room/Panel_brooknod_doorknob
+ colors: yellow
+ tag: midyellow
+ paintings:
+ - id: symmetry_painting_a_2
+ orientation: west
+ - id: symmetry_painting_b_2
+ orientation: south
+ exit_only: True
+ required: True
+ The Wondrous:
+ entrances:
+ The Wondrous (Table):
+ warp: True
+ Arrow Garden:
+ door: Exit
+ panels:
+ FIREPLACE:
+ id: Wonderland Room/Panel_fireplace_fire
+ colors: red
+ tag: midred
+ Achievement:
+ id: Countdown Panels/Panel_wondrous_wondrous
+ required_panel:
+ - panel: FIREPLACE
+ - room: The Wondrous (Table)
+ panel: BROOK NOD
+ - room: The Wondrous (Bookcase)
+ panel: CASE
+ - room: The Wondrous (Chandelier)
+ panel: CANDLE HEIR
+ - room: The Wondrous (Window)
+ panel: GLASS
+ - room: The Wondrous (Table)
+ panel: WOOD
+ tag: forbid
+ achievement: The Wondrous
+ doors:
+ Exit:
+ id: Red Blue Purple Room Area Doors/Door_wonderland_exit
+ item_group: Paintings
+ painting_id: arrows_painting_9
+ include_reduce: True
+ panels:
+ - Achievement
+ paintings:
+ - id: arrows_painting_9
+ enter_only: True
+ orientation: south
+ move: True
+ required_door:
+ door: Exit
+ req_blocked_when_no_doors: True # the wondrous (table) in vanilla doors
+ - id: symmetry_painting_a_6
+ orientation: west
+ exit_only: True
+ - id: symmetry_painting_b_6
+ orientation: north
+ req_blocked_when_no_doors: True # the wondrous (table) in vanilla doors
+ Arrow Garden:
+ entrances:
+ The Wondrous:
+ room: The Wondrous
+ door: Exit
+ Roof: True
+ panels:
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery4
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ SHARP:
+ id: Open Areas/Panel_rainy_rainbow2
+ tag: midwhite
+ paintings:
+ - id: flower_painting_6
+ orientation: south
+ Hallway Room (1):
+ entrances:
+ Outside The Agreeable:
+ warp: True
+ Hallway Room (2):
+ warp: True
+ Hallway Room (3):
+ warp: True
+ Hallway Room (4):
+ warp: True
+ panels:
+ OUT:
+ id: Hallway Room/Panel_out_out
+ check: True
+ exclude_reduce: True
+ tag: midwhite
+ WALL:
+ id: Hallway Room/Panel_castle_1
+ colors: blue
+ tag: quad bot blue
+ link: qbb CASTLE
+ KEEP:
+ id: Hallway Room/Panel_castle_2
+ colors: blue
+ tag: quad bot blue
+ link: qbb CASTLE
+ BAILEY:
+ id: Hallway Room/Panel_castle_3
+ colors: blue
+ tag: quad bot blue
+ link: qbb CASTLE
+ TOWER:
+ id: Hallway Room/Panel_castle_4
+ colors: blue
+ tag: quad bot blue
+ link: qbb CASTLE
+ doors:
+ Exit:
+ id: Red Blue Purple Room Area Doors/Door_room_2
+ door_group: Hallway Room Doors
+ location_name: Hallway Room - First Room
+ panels:
+ - WALL
+ - KEEP
+ - BAILEY
+ - TOWER
+ panel_doors:
+ CASTLE:
+ item_name: Hallway Room - First Room Panels
+ panel_group: Hallway Room Panels
+ panels:
+ - WALL
+ - KEEP
+ - BAILEY
+ - TOWER
+ paintings:
+ - id: panda_painting
+ orientation: south
+ progression:
+ Progressive Hallway Room:
+ doors:
+ - Exit
+ - room: Hallway Room (2)
+ door: Exit
+ - room: Hallway Room (3)
+ door: Exit
+ - room: Hallway Room (4)
+ door: Exit
+ panel_doors:
+ - CASTLE
+ - room: Hallway Room (2)
+ panel_door: COUNTERCLOCKWISE
+ - room: Hallway Room (3)
+ panel_door: TRANSFORMATION
+ - room: Hallway Room (4)
+ panel_door: WHEELBARROW
+ Hallway Room (2):
+ entrances:
+ Hallway Room (1):
+ room: Hallway Room (1)
+ door: Exit
+ warp: True
+ Elements Area: True
+ panels:
+ WISE:
+ id: Hallway Room/Panel_counterclockwise_1
+ colors: blue
+ tag: quad mid blue
+ link: qmb COUNTERCLOCKWISE
+ CLOCK:
+ id: Hallway Room/Panel_counterclockwise_2
+ colors: blue
+ tag: quad mid blue
+ link: qmb COUNTERCLOCKWISE
+ ER:
+ id: Hallway Room/Panel_counterclockwise_3
+ colors: blue
+ tag: quad mid blue
+ link: qmb COUNTERCLOCKWISE
+ COUNT:
+ id: Hallway Room/Panel_counterclockwise_4
+ colors: blue
+ tag: quad mid blue
+ link: qmb COUNTERCLOCKWISE
+ doors:
+ Exit:
+ id: Red Blue Purple Room Area Doors/Door_room_3
+ location_name: Hallway Room - Second Room
+ door_group: Hallway Room Doors
+ panels:
+ - WISE
+ - CLOCK
+ - ER
+ - COUNT
+ panel_doors:
+ COUNTERCLOCKWISE:
+ item_name: Hallway Room - Second Room Panels
+ panel_group: Hallway Room Panels
+ panels:
+ - WISE
+ - CLOCK
+ - ER
+ - COUNT
+ Hallway Room (3):
+ entrances:
+ Hallway Room (2):
+ room: Hallway Room (2)
+ door: Exit
+ warp: True
+ # No entrance from Elements Area. The winding hallway does not connect.
+ panels:
+ TRANCE:
+ id: Hallway Room/Panel_transformation_1
+ colors: blue
+ tag: quad top blue
+ link: qtb TRANSFORMATION
+ FORM:
+ id: Hallway Room/Panel_transformation_2
+ colors: blue
+ tag: quad top blue
+ link: qtb TRANSFORMATION
+ A:
+ id: Hallway Room/Panel_transformation_3
+ colors: blue
+ tag: quad top blue
+ link: qtb TRANSFORMATION
+ SHUN:
+ id: Hallway Room/Panel_transformation_4
+ colors: blue
+ tag: quad top blue
+ link: qtb TRANSFORMATION
+ doors:
+ Exit:
+ id: Red Blue Purple Room Area Doors/Door_room_4
+ location_name: Hallway Room - Third Room
+ door_group: Hallway Room Doors
+ panels:
+ - TRANCE
+ - FORM
+ - A
+ - SHUN
+ panel_doors:
+ TRANSFORMATION:
+ item_name: Hallway Room - Third Room Panels
+ panel_group: Hallway Room Panels
+ panels:
+ - TRANCE
+ - FORM
+ - A
+ - SHUN
+ Hallway Room (4):
+ entrances:
+ Hallway Room (3):
+ room: Hallway Room (3)
+ door: Exit
+ warp: True
+ Elements Area: True
+ panels:
+ WHEEL:
+ id: Hallway Room/Panel_room_5
+ colors: blue
+ tag: full stack blue
+ doors:
+ Exit:
+ id:
+ - Red Blue Purple Room Area Doors/Door_room_5
+ - Red Blue Purple Room Area Doors/Door_room_6 # this is the connection to The Artistic
+ door_group: Hallway Room Doors
+ location_name: Hallway Room - Fourth Room
+ panels:
+ - WHEEL
+ include_reduce: True
+ panel_doors:
+ WHEELBARROW:
+ item_name: Hallway Room - WHEEL
+ panel_group: Hallway Room Panels
+ panels:
+ - WHEEL
+ Elements Area:
+ entrances:
+ Roof: True
+ Hallway Room (4):
+ room: Hallway Room (4)
+ door: Exit
+ # If this door is open, then a non-warp entrance from the first hallway room is available
+ The Artistic (Smiley):
+ room: Hallway Room (4)
+ door: Exit
+ panels:
+ A:
+ id: Strand Room/Panel_a_strands
+ colors: blue
+ tag: forbid
+ hunt: True
+ NINE:
+ id: Backside Room/Panel_nine_nine_7
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Nines
+ UNDISTRACTED:
+ id: Open Areas/Panel_undistracted
+ check: True
+ exclude_reduce: True
+ tag: midwhite
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery13
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ EARTH:
+ id: Cross Room/Panel_earth_earth
+ tag: midwhite
+ WATER:
+ id: Cross Room/Panel_water_water
+ tag: midwhite
+ AIR:
+ id: Cross Room/Panel_air_air
+ tag: midwhite
+ paintings:
+ - id: south_afar
+ orientation: south
+ Outside The Wanderer:
+ entrances:
+ Orange Tower First Floor:
+ door: Tower Entrance
+ warp: True
+ Rhyme Room (Cross):
+ room: Rhyme Room (Cross)
+ door: Exit
+ Roof: True
+ panels:
+ WANDERLUST:
+ id: Tower Room/Panel_wanderlust_1234567890
+ colors: orange
+ tag: midorange
+ doors:
+ Wanderer Entrance:
+ id: Tower Room Area Doors/Door_wanderer_entrance
+ item_name: The Wanderer - Entrance
+ item_group: Achievement Room Entrances
+ panels:
+ - WANDERLUST
+ Tower Entrance:
+ id: Tower Room Area Doors/Door_wanderlust_start
+ skip_location: True
+ panels:
+ - room: The Wanderer
+ panel: Achievement
+ panel_doors:
+ WANDERLUST:
+ panels:
+ - WANDERLUST
+ The Wanderer:
+ entrances:
+ Outside The Wanderer:
+ room: Outside The Wanderer
+ door: Wanderer Entrance
+ warp: True
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_1234567890_wanderlust
+ colors: orange
+ check: True
+ tag: forbid
+ achievement: The Wanderer
+ "7890":
+ id: Orange Room/Panel_lust
+ colors: orange
+ tag: midorange
+ "6524":
+ id: Orange Room/Panel_read
+ colors: orange
+ tag: midorange
+ "951":
+ id: Orange Room/Panel_sew
+ colors: orange
+ tag: midorange
+ "4524":
+ id: Orange Room/Panel_dead
+ colors: orange
+ tag: midorange
+ LEARN:
+ id: Orange Room/Panel_learn
+ colors: orange
+ tag: midorange
+ DUST:
+ id: Orange Room/Panel_dust
+ colors: orange
+ tag: midorange
+ STAR:
+ id: Orange Room/Panel_star
+ colors: orange
+ tag: midorange
+ WANDER:
+ id: Orange Room/Panel_wander
+ colors: orange
+ tag: midorange
+ Art Gallery:
+ entrances:
+ Orange Tower Third Floor:
+ warp: True
+ Art Gallery (Second Floor):
+ warp: True
+ Art Gallery (Third Floor):
+ warp: True
+ Art Gallery (Fourth Floor):
+ warp: True
+ Outside The Initiated:
+ door: Exit
+ warp: True
+ panels:
+ EIGHT:
+ id: Backside Room/Panel_eight_eight_6
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Eights
+ EON:
+ id: Painting Room/Panel_eon_one
+ colors: yellow
+ tag: midyellow
+ TRUSTWORTHY:
+ id: Painting Room/Panel_to_two
+ colors: red
+ tag: midred
+ FREE:
+ id: Painting Room/Panel_free_three
+ colors: purple
+ tag: midpurp
+ OUR:
+ id: Painting Room/Panel_our_four
+ colors: blue
+ tag: midblue
+ ORDER:
+ id: Painting Room/Panel_order_onepathmanyturns
+ tag: forbid
+ colors:
+ - yellow
+ - blue
+ - gray
+ - brown
+ - orange
+ required_door:
+ door: Fifth Floor
+ doors:
+ Second Floor:
+ painting_id:
+ - scenery_painting_2b
+ - scenery_painting_2c
+ skip_location: True
+ panels:
+ - EON
+ First Floor Puzzles:
+ skip_item: True
+ location_name: Art Gallery - First Floor Puzzles
+ panels:
+ - EON
+ - TRUSTWORTHY
+ - FREE
+ - OUR
+ Third Floor:
+ painting_id:
+ - scenery_painting_3b
+ - scenery_painting_3c
+ skip_location: True
+ panels:
+ - room: Art Gallery (Second Floor)
+ panel: PATH
+ Fourth Floor:
+ painting_id:
+ - scenery_painting_4b
+ - scenery_painting_4c
+ skip_location: True
+ panels:
+ - room: Art Gallery (Third Floor)
+ panel: ANY
+ Fifth Floor:
+ id: Tower Room Area Doors/Door_painting_backroom
+ painting_id:
+ - scenery_painting_5b
+ - scenery_painting_5c
+ skip_location: True
+ panels:
+ - room: Art Gallery (Fourth Floor)
+ panel: SEND - USE
+ Exit:
+ id: Tower Room Area Doors/Door_painting_exit
+ include_reduce: True
+ item_name: Orange Tower Fifth Floor - Quadruple Intersection
+ item_group: Achievement Room Entrances
+ panels:
+ - ORDER
+ panel_doors:
+ ORDER:
+ panels:
+ - ORDER
+ paintings:
+ - id: smile_painting_3
+ orientation: west
+ - id: flower_painting_2
+ orientation: east
+ - id: scenery_painting_0a
+ orientation: north
+ - id: map_painting
+ orientation: east
+ - id: fruitbowl_painting4
+ orientation: south
+ progression:
+ Progressive Art Gallery:
+ doors:
+ - Second Floor
+ - Third Floor
+ - Fourth Floor
+ - Fifth Floor
+ Art Gallery (Second Floor):
+ entrances:
+ Art Gallery:
+ room: Art Gallery
+ door: Second Floor
+ panels:
+ HOUSE:
+ id: Painting Room/Panel_house_neighborhood
+ colors: blue
+ tag: botblue
+ PATH:
+ id: Painting Room/Panel_path_road
+ colors: brown
+ tag: botbrown
+ PARK:
+ id: Painting Room/Panel_park_drive
+ colors: black
+ tag: botblack
+ CARRIAGE:
+ id: Painting Room/Panel_carriage_horse
+ colors: red
+ tag: botred
+ doors:
+ Puzzles:
+ skip_item: True
+ location_name: Art Gallery - Second Floor Puzzles
+ panels:
+ - HOUSE
+ - PATH
+ - PARK
+ - CARRIAGE
+ Art Gallery (Third Floor):
+ entrances:
+ Art Gallery:
+ room: Art Gallery
+ door: Third Floor
+ panels:
+ AN:
+ id: Painting Room/Panel_an_many
+ colors: blue
+ tag: midblue
+ MAY:
+ id: Painting Room/Panel_may_many
+ colors: blue
+ tag: midblue
+ ANY:
+ id: Painting Room/Panel_any_many
+ colors: blue
+ tag: midblue
+ MAN:
+ id: Painting Room/Panel_man_many
+ colors: blue
+ tag: midblue
+ doors:
+ Puzzles:
+ skip_item: True
+ location_name: Art Gallery - Third Floor Puzzles
+ panels:
+ - AN
+ - MAY
+ - ANY
+ - MAN
+ Art Gallery (Fourth Floor):
+ entrances:
+ Art Gallery:
+ room: Art Gallery
+ door: Fourth Floor
+ panels:
+ URNS:
+ id: Painting Room/Panel_urns_turns
+ colors: blue
+ tag: midblue
+ LEARNS:
+ id: Painting Room/Panel_learns_turns
+ colors: purple
+ tag: midpurp
+ RUNTS:
+ id: Painting Room/Panel_runts_turns
+ colors: yellow
+ tag: midyellow
+ SEND - USE:
+ id: Painting Room/Panel_send_use_turns
+ colors: orange
+ tag: midorange
+ TRUST:
+ id: Painting Room/Panel_trust_06890
+ colors: orange
+ tag: midorange
+ "062459":
+ id: Painting Room/Panel_06890_trust
+ colors: orange
+ tag: midorange
+ doors:
+ Puzzles:
+ skip_item: True
+ location_name: Art Gallery - Fourth Floor Puzzles
+ panels:
+ - URNS
+ - LEARNS
+ - RUNTS
+ - SEND - USE
+ - TRUST
+ - "062459"
+ Rhyme Room (Smiley):
+ entrances:
+ Orange Tower Third Floor:
+ room: Orange Tower Third Floor
+ door: Rhyme Room Entrance
+ Rhyme Room (Circle):
+ room: Rhyme Room (Circle)
+ door: Door to Smiley
+ Rhyme Room (Cross): True # one-way
+ panels:
+ LOANS:
+ id: Double Room/Panel_bones_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme BONES
+ SKELETON:
+ id: Double Room/Panel_bones_syn
+ tag: syn rhyme
+ colors: purple
+ subtag: bot
+ link: rhyme BONES
+ REPENTANCE:
+ id: Double Room/Panel_sentence_rhyme
+ colors:
+ - purple
+ - blue
+ tag: whole rhyme
+ subtag: top
+ link: rhyme SENTENCE
+ WORD:
+ id: Double Room/Panel_sentence_whole
+ colors:
+ - purple
+ - blue
+ tag: whole rhyme
+ subtag: bot
+ link: rhyme SENTENCE
+ SCHEME:
+ id: Double Room/Panel_dream_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme DREAM
+ FANTASY:
+ id: Double Room/Panel_dream_syn
+ colors: purple
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme DREAM
+ HISTORY:
+ id: Double Room/Panel_mystery_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme MYSTERY
+ SECRET:
+ id: Double Room/Panel_mystery_syn
+ colors: purple
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme MYSTERY
+ doors:
+ # This is complicated. I want the location in here to just be the four
+ # panels against the wall toward Target. But in vanilla, you also need to
+ # solve the panels in Circle that are against the Smiley wall. Logic needs
+ # to know this so that it can handle no door shuffle properly. So we split
+ # the item and location up.
+ Door to Target:
+ id:
+ - Double Room Area Doors/Door_room_3a
+ - Double Room Area Doors/Door_room_3bc
+ skip_location: True
+ door_group: Rhyme Room Doors
+ panels:
+ - SCHEME
+ - FANTASY
+ - HISTORY
+ - SECRET
+ - room: Rhyme Room (Circle)
+ panel: BIRD
+ - room: Rhyme Room (Circle)
+ panel: LETTER
+ - room: Rhyme Room (Circle)
+ panel: VIOLENT
+ - room: Rhyme Room (Circle)
+ panel: MUTE
+ Door to Target (Location):
+ location_name: Rhyme Room (Smiley) - Puzzles Toward Target
+ skip_item: True
+ panels:
+ - SCHEME
+ - FANTASY
+ - HISTORY
+ - SECRET
+ Rhyme Room (Cross):
+ entrances:
+ Rhyme Room (Target): # one-way
+ room: Rhyme Room (Target)
+ door: Door to Cross
+ Rhyme Room (Looped Square):
+ room: Rhyme Room (Looped Square)
+ door: Door to Cross
+ panels:
+ NINE:
+ id: Backside Room/Panel_nine_nine_9
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Number Hunt
+ door: Nines
+ FERN:
+ id: Double Room/Panel_return_rhyme
+ colors:
+ - purple
+ - black
+ tag: ant rhyme
+ subtag: top
+ link: rhyme RETURN
+ STAY:
+ id: Double Room/Panel_return_ant
+ colors:
+ - purple
+ - black
+ tag: ant rhyme
+ subtag: bot
+ link: rhyme RETURN
+ FRIEND:
+ id: Double Room/Panel_descend_rhyme
+ colors:
+ - purple
+ - black
+ tag: ant rhyme
+ subtag: top
+ link: rhyme DESCEND
+ RISE:
+ id: Double Room/Panel_descend_ant
+ colors:
+ - purple
+ - black
+ tag: ant rhyme
+ subtag: bot
+ link: rhyme DESCEND
+ PLUMP:
+ id: Double Room/Panel_jump_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme JUMP
+ BOUNCE:
+ id: Double Room/Panel_jump_syn
+ colors: purple
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme JUMP
+ SCRAWL:
+ id: Double Room/Panel_fall_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme FALL
+ PLUNGE:
+ id: Double Room/Panel_fall_syn
+ colors: purple
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme FALL
+ doors:
+ Exit:
+ id: Double Room Area Doors/Door_room_exit
+ location_name: Rhyme Room (Cross) - Exit Puzzles
+ door_group: Rhyme Room Doors
+ panels:
+ - PLUMP
+ - BOUNCE
+ - SCRAWL
+ - PLUNGE
+ Rhyme Room (Circle):
+ entrances:
+ Rhyme Room (Looped Square):
+ room: Rhyme Room (Looped Square)
+ door: Door to Circle
+ Hidden Room:
+ room: Hidden Room
+ door: Rhyme Room Entrance
+ Rhyme Room (Smiley):
+ door: Door to Smiley
+ panels:
+ BIRD:
+ id: Double Room/Panel_word_rhyme
+ colors:
+ - purple
+ - blue
+ tag: whole rhyme
+ subtag: top
+ link: rhyme WORD
+ LETTER:
+ id: Double Room/Panel_word_whole
+ colors:
+ - purple
+ - blue
+ tag: whole rhyme
+ subtag: bot
+ link: rhyme WORD
+ FORBIDDEN:
+ id: Double Room/Panel_hidden_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme HIDDEN
+ CONCEALED:
+ id: Double Room/Panel_hidden_syn
+ colors: purple
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme HIDDEN
+ VIOLENT:
+ id: Double Room/Panel_silent_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme SILENT
+ MUTE:
+ id: Double Room/Panel_silent_syn
+ colors: purple
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme SILENT
+ doors:
+ Door to Smiley:
+ id:
+ - Double Room Area Doors/Door_room_2b
+ - Double Room Area Doors/Door_room_3b
+ location_name: Rhyme Room - Circle/Smiley Wall
+ door_group: Rhyme Room Doors
+ panels:
+ - BIRD
+ - LETTER
+ - VIOLENT
+ - MUTE
+ - room: Rhyme Room (Smiley)
+ panel: LOANS
+ - room: Rhyme Room (Smiley)
+ panel: SKELETON
+ - room: Rhyme Room (Smiley)
+ panel: REPENTANCE
+ - room: Rhyme Room (Smiley)
+ panel: WORD
+ paintings:
+ - id: arrows_painting_3
+ orientation: north
+ Rhyme Room (Looped Square):
+ entrances:
+ Starting Room:
+ room: Starting Room
+ door: Rhyme Room Entrance
+ Rhyme Room (Circle):
+ door: Door to Circle
+ Rhyme Room (Cross):
+ door: Door to Cross
+ Rhyme Room (Target):
+ door: Door to Target
+ panels:
+ WALKED:
+ id: Double Room/Panel_blocked_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme BLOCKED
+ OBSTRUCTED:
+ id: Double Room/Panel_blocked_syn
+ colors: purple
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme BLOCKED
+ SKIES:
+ id: Double Room/Panel_rise_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme RISE
+ SWELL:
+ id: Double Room/Panel_rise_syn
+ colors: purple
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme RISE
+ PENNED:
+ id: Double Room/Panel_ascend_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme ASCEND
+ CLIMB:
+ id: Double Room/Panel_ascend_syn
+ colors: purple
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme ASCEND
+ TROUBLE:
+ id: Double Room/Panel_double_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme DOUBLE
+ DUPLICATE:
+ id: Double Room/Panel_double_syn
+ colors: purple
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme DOUBLE
+ doors:
+ Door to Circle:
+ id:
+ - Double Room Area Doors/Door_room_2a
+ - Double Room Area Doors/Door_room_1c
+ location_name: Rhyme Room - Circle/Looped Square Wall
+ door_group: Rhyme Room Doors
+ panels:
+ - WALKED
+ - OBSTRUCTED
+ - SKIES
+ - SWELL
+ - room: Rhyme Room (Circle)
+ panel: BIRD
+ - room: Rhyme Room (Circle)
+ panel: LETTER
+ - room: Rhyme Room (Circle)
+ panel: FORBIDDEN
+ - room: Rhyme Room (Circle)
+ panel: CONCEALED
+ Door to Cross:
+ id:
+ - Double Room Area Doors/Door_room_1a
+ - Double Room Area Doors/Door_room_5a
+ location_name: Rhyme Room - Cross/Looped Square Wall
+ door_group: Rhyme Room Doors
+ panels:
+ - SKIES
+ - SWELL
+ - PENNED
+ - CLIMB
+ - room: Rhyme Room (Cross)
+ panel: FERN
+ - room: Rhyme Room (Cross)
+ panel: STAY
+ - room: Rhyme Room (Cross)
+ panel: FRIEND
+ - room: Rhyme Room (Cross)
+ panel: RISE
+ Door to Target:
+ id:
+ - Double Room Area Doors/Door_room_1b
+ - Double Room Area Doors/Door_room_4b
+ location_name: Rhyme Room - Target/Looped Square Wall
+ door_group: Rhyme Room Doors
+ panels:
+ - PENNED
+ - CLIMB
+ - TROUBLE
+ - DUPLICATE
+ - room: Rhyme Room (Target)
+ panel: WILD
+ - room: Rhyme Room (Target)
+ panel: KID
+ - room: Rhyme Room (Target)
+ panel: PISTOL
+ - room: Rhyme Room (Target)
+ panel: GEM
+ Rhyme Room (Target):
+ entrances:
+ Rhyme Room (Smiley): # one-way
+ room: Rhyme Room (Smiley)
+ door: Door to Target
+ warp: True
+ Rhyme Room (Looped Square):
+ room: Rhyme Room (Looped Square)
+ door: Door to Target
+ panels:
+ WILD:
+ id: Double Room/Panel_child_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme CHILD
+ KID:
+ id: Double Room/Panel_child_syn
+ colors: purple
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme CHILD
+ PISTOL:
+ id: Double Room/Panel_crystal_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme CRYSTAL
+ GEM:
+ id: Double Room/Panel_crystal_syn
+ colors: purple
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme CRYSTAL
+ INNOVATIVE (Top):
+ id: Double Room/Panel_creative_rhyme
+ colors: purple
+ tag: syn rhyme
+ subtag: top
+ link: rhyme CREATIVE
+ INNOVATIVE (Bottom):
+ id: Double Room/Panel_creative_syn
+ colors: purple
+ tag: syn rhyme
+ subtag: bot
+ link: rhyme CREATIVE
+ LEAP:
+ id: Double Room/Panel_leap_leap
+ tag: midwhite
+ required_door:
+ door: Door to Cross
+ doors:
+ Door to Cross:
+ id: Double Room Area Doors/Door_room_4a
+ location_name: Rhyme Room (Target) - Puzzles Toward Cross
+ door_group: Rhyme Room Doors
+ panels:
+ - PISTOL
+ - GEM
+ - INNOVATIVE (Top)
+ - INNOVATIVE (Bottom)
+ paintings:
+ - id: arrows_painting_4
+ orientation: north
+ Room Room:
+ # This is a bit of a weird room. You can't really get to it from the roof.
+ # And even if you were to go through the shortcut on the fifth floor into
+ # the basement and up the stairs, you'd be blocked by the backsides of the
+ # ROOM panels, which isn't ideal. So we will, at least for now, say that
+ # this room is vanilla.
+ #
+ # For pretty much the same reason, I don't want to shuffle the paintings in
+ # here.
+ entrances:
+ Orange Tower Fourth Floor:
+ warp: True
+ panels:
+ DOOR (1):
+ id: Panel Room/Panel_room_door_1
+ colors: gray
+ tag: forbid
+ DOOR (2):
+ id: Panel Room/Panel_room_door_2
+ colors: gray
+ tag: forbid
+ WINDOW:
+ id: Panel Room/Panel_room_window_1
+ colors: gray
+ tag: forbid
+ STAIRS:
+ id: Panel Room/Panel_room_stairs_1
+ colors: gray
+ tag: forbid
+ PAINTING:
+ id: Panel Room/Panel_room_painting_1
+ colors: gray
+ tag: forbid
+ FLOOR (1):
+ id: Panel Room/Panel_room_floor_1
+ colors: gray
+ tag: forbid
+ FLOOR (2):
+ id: Panel Room/Panel_room_floor_2
+ colors: gray
+ tag: forbid
+ FLOOR (3):
+ id: Panel Room/Panel_room_floor_3
+ colors: gray
+ tag: forbid
+ FLOOR (4):
+ id: Panel Room/Panel_room_floor_4
+ colors: gray
+ tag: forbid
+ FLOOR (5):
+ id: Panel Room/Panel_room_floor_5
+ colors: gray
+ tag: forbid
+ FLOOR (6):
+ id: Panel Room/Panel_room_floor_7
+ colors: gray
+ tag: forbid
+ FLOOR (7):
+ id: Panel Room/Panel_room_floor_8
+ colors: gray
+ tag: forbid
+ FLOOR (8):
+ id: Panel Room/Panel_room_floor_9
+ colors: gray
+ tag: forbid
+ CEILING (1):
+ id: Panel Room/Panel_room_ceiling_1
+ colors: gray
+ tag: forbid
+ CEILING (2):
+ id: Panel Room/Panel_room_ceiling_2
+ colors: gray
+ tag: forbid
+ CEILING (3):
+ id: Panel Room/Panel_room_ceiling_3
+ colors: gray
+ tag: forbid
+ CEILING (4):
+ id: Panel Room/Panel_room_ceiling_4
+ colors: gray
+ tag: forbid
+ CEILING (5):
+ id: Panel Room/Panel_room_ceiling_5
+ colors: gray
+ tag: forbid
+ CEILING (6):
+ id: Panel Room/Panel_room_floor_10
+ colors: gray
+ tag: forbid
+ WALL (1):
+ id: Panel Room/Panel_room_wall_1
+ colors: gray
+ tag: forbid
+ WALL (2):
+ id: Panel Room/Panel_room_wall_2
+ colors: gray
+ tag: forbid
+ WALL (3):
+ id: Panel Room/Panel_room_wall_3
+ colors: gray
+ tag: forbid
+ WALL (4):
+ id: Panel Room/Panel_room_wall_4
+ colors: gray
+ tag: forbid
+ WALL (5):
+ id: Panel Room/Panel_room_wall_5
+ colors: gray
+ tag: forbid
+ WALL (6):
+ id: Panel Room/Panel_room_wall_6
+ colors: gray
+ tag: forbid
+ WALL (7):
+ id: Panel Room/Panel_room_wall_7
+ colors: gray
+ tag: forbid
+ WALL (8):
+ id: Panel Room/Panel_room_wall_8
+ colors: gray
+ tag: forbid
+ WALL (9):
+ id: Panel Room/Panel_room_wall_9
+ colors: gray
+ tag: forbid
+ WALL (10):
+ id: Panel Room/Panel_room_wall_10
+ colors: gray
+ tag: forbid
+ WALL (11):
+ id: Panel Room/Panel_room_wall_11
+ colors: gray
+ tag: forbid
+ WALL (12):
+ id: Panel Room/Panel_room_wall_12
+ colors: gray
+ tag: forbid
+ WALL (13):
+ id: Panel Room/Panel_room_wall_13
+ colors: gray
+ tag: forbid
+ WALL (14):
+ id: Panel Room/Panel_room_wall_14
+ colors: gray
+ tag: forbid
+ WALL (15):
+ id: Panel Room/Panel_room_wall_15
+ colors: gray
+ tag: forbid
+ WALL (16):
+ id: Panel Room/Panel_room_wall_16
+ colors: gray
+ tag: forbid
+ WALL (17):
+ id: Panel Room/Panel_room_wall_17
+ colors: gray
+ tag: forbid
+ WALL (18):
+ id: Panel Room/Panel_room_wall_18
+ colors: gray
+ tag: forbid
+ WALL (19):
+ id: Panel Room/Panel_room_wall_19
+ colors: gray
+ tag: forbid
+ WALL (20):
+ id: Panel Room/Panel_room_wall_20
+ colors: gray
+ tag: forbid
+ WALL (21):
+ id: Panel Room/Panel_room_wall_21
+ colors: gray
+ tag: forbid
+ BROOMED:
+ id: Panel Room/Panel_broomed_bedroom
+ colors: yellow
+ tag: midyellow
+ required_panel:
+ panel: WALL (1)
+ LAYS:
+ id: Panel Room/Panel_lays_maze
+ colors: purple
+ tag: toppurp
+ required_panel:
+ panel: BROOMED
+ BASE:
+ id: Panel Room/Panel_base_basement
+ colors: blue
+ tag: midblue
+ required_panel:
+ panel: LAYS
+ MASTERY:
+ id: Master Room/Panel_mastery_mastery
+ tag: midwhite
+ hunt: True
+ required_door:
+ room: Orange Tower Seventh Floor
+ door: Mastery
+ required_panel:
+ room: Room Room
+ panel: WALL (2)
+ doors:
+ Excavation:
+ event: True
+ panels:
+ - STAIRS
+ Cellar Exit:
+ id:
+ - Tower Room Area Doors/Door_panel_basement
+ - Tower Room Area Doors/Door_panel_basement2
+ panels:
+ - BASE
+ panel_doors:
+ STAIRS:
+ panel_group: Room Room Panels
+ panels:
+ - STAIRS
+ Colors:
+ panel_group: Room Room Panels
+ panels:
+ - BROOMED
+ - LAYS
+ - BASE
+ Cellar:
+ entrances:
+ Room Room:
+ room: Room Room
+ door: Excavation
+ Orange Tower Fifth Floor:
+ room: Room Room
+ door: Cellar Exit
+ warp: True
+ Compass Room:
+ room: Compass Room
+ door: Lookout Entrance
+ warp: True
+ Outside The Wise:
+ entrances:
+ Orange Tower Sixth Floor:
+ painting: True
+ Outside The Initiated:
+ painting: True
+ panels:
+ KITTEN:
+ id: Clock Room/Panel_kitten_cat
+ colors: brown
+ tag: botbrown
+ CAT:
+ id: Clock Room/Panel_cat_kitten
+ tag: bot brown black
+ colors:
+ - brown
+ - black
+ doors:
+ Wise Entrance:
+ id: Clock Room Area Doors/Door_time_start
+ item_name: The Wise - Entrance
+ item_group: Achievement Room Entrances
+ panels:
+ - KITTEN
+ - CAT
+ panel_doors:
+ KITTEN CAT:
+ panels:
+ - KITTEN
+ - CAT
+ paintings:
+ - id: arrows_painting_2
+ orientation: east
+ - id: clock_painting_2
+ orientation: east
+ exit_only: True
+ required: True
+ The Wise:
+ entrances:
+ Outside The Wise:
+ room: Outside The Wise
+ door: Wise Entrance
+ warp: True # The Wise is so full of warps
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_intelligent_wise
+ colors:
+ - brown
+ - black
+ tag: forbid
+ check: True
+ achievement: The Wise
+ PUPPY:
+ id: Clock Room/Panel_puppy_dog
+ colors: brown
+ tag: botbrown
+ ADULT:
+ id: Clock Room/Panel_adult_child
+ colors:
+ - brown
+ - black
+ tag: bot brown black
+ BREAD:
+ id: Clock Room/Panel_bread_mold
+ colors: brown
+ tag: botbrown
+ DINOSAUR:
+ id: Clock Room/Panel_dinosaur_fossil
+ colors: brown
+ tag: botbrown
+ OAK:
+ id: Clock Room/Panel_oak_acorn
+ colors:
+ - brown
+ - black
+ tag: bot brown black
+ CORPSE:
+ id: Clock Room/Panel_corpse_skeleton
+ colors: brown
+ tag: botbrown
+ BEFORE:
+ id: Clock Room/Panel_before_ere
+ colors:
+ - brown
+ - black
+ tag: mid brown black
+ YOUR:
+ id: Clock Room/Panel_your_thy
+ colors:
+ - brown
+ - black
+ tag: mid brown black
+ BETWIXT:
+ id: Clock Room/Panel_betwixt_between
+ colors: brown
+ tag: midbrown
+ NIGH:
+ id: Clock Room/Panel_nigh_near
+ colors: brown
+ tag: midbrown
+ CONNEXION:
+ id: Clock Room/Panel_connexion_connection
+ colors: brown
+ tag: midbrown
+ THOU:
+ id: Clock Room/Panel_thou_you
+ colors: brown
+ tag: midbrown
+ paintings:
+ - id: clock_painting_3
+ orientation: east
+ req_blocked: True # outside the wise (with or without door shuffle)
+ The Red:
+ entrances:
+ Roof: True
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_grandfathered_red
+ colors: red
+ tag: forbid
+ check: True
+ achievement: The Red
+ PANDEMIC (1):
+ id: Hangry Room/Panel_red_top_1
+ colors: red
+ tag: topred
+ TRINITY:
+ id: Hangry Room/Panel_red_top_2
+ colors: red
+ tag: topred
+ CHEMISTRY:
+ id: Hangry Room/Panel_red_top_3
+ colors: red
+ tag: topred
+ FLUSTERED:
+ id: Hangry Room/Panel_red_top_4
+ colors: red
+ tag: topred
+ PANDEMIC (2):
+ id: Hangry Room/Panel_red_mid_1
+ colors: red
+ tag: midred
+ COUNTERCLOCKWISE:
+ id: Hangry Room/Panel_red_mid_2
+ colors: red
+ tag: red top red mid black bot
+ FEARLESS:
+ id: Hangry Room/Panel_red_mid_3
+ colors: red
+ tag: midred
+ DEFORESTATION:
+ id: Hangry Room/Panel_red_mid_4
+ colors: red
+ tag: red mid bot
+ subtag: mid
+ link: rmb FORE
+ CRAFTSMANSHIP:
+ id: Hangry Room/Panel_red_mid_5
+ colors: red
+ tag: red mid bot
+ subtag: mid
+ link: rmb AFT
+ CAMEL:
+ id: Hangry Room/Panel_red_bot_1
+ colors: red
+ tag: botred
+ LION:
+ id: Hangry Room/Panel_red_bot_2
+ colors: red
+ tag: botred
+ TIGER:
+ id: Hangry Room/Panel_red_bot_3
+ colors: red
+ tag: botred
+ SHIP (1):
+ id: Hangry Room/Panel_red_bot_4
+ colors: red
+ tag: red mid bot
+ subtag: bot
+ link: rmb FORE
+ SHIP (2):
+ id: Hangry Room/Panel_red_bot_5
+ colors: red
+ tag: red mid bot
+ subtag: bot
+ link: rmb AFT
+ GIRAFFE:
+ id: Hangry Room/Panel_red_bot_6
+ colors: red
+ tag: botred
+ The Ecstatic:
+ entrances:
+ Roof: True
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_ecstatic_ecstatic
+ colors: yellow
+ tag: forbid
+ check: True
+ achievement: The Ecstatic
+ FORM (1):
+ id: Smiley Room/Panel_soundgram_1
+ colors: yellow
+ tag: yellow top bot
+ subtag: bottom
+ link: ytb FORM
+ WIND:
+ id: Smiley Room/Panel_soundgram_2
+ colors: yellow
+ tag: botyellow
+ EGGS:
+ id: Smiley Room/Panel_scrambled_1
+ colors: yellow
+ tag: botyellow
+ VEGETABLES:
+ id: Smiley Room/Panel_scrambled_2
+ colors: yellow
+ tag: botyellow
+ WATER:
+ id: Smiley Room/Panel_anagram_6_1
+ colors: yellow
+ tag: botyellow
+ FRUITS:
+ id: Smiley Room/Panel_anagram_6_2
+ colors: yellow
+ tag: botyellow
+ LEAVES:
+ id: Smiley Room/Panel_anagram_7_1
+ colors: yellow
+ tag: topyellow
+ VINES:
+ id: Smiley Room/Panel_anagram_7_2
+ colors: yellow
+ tag: topyellow
+ ICE:
+ id: Smiley Room/Panel_anagram_7_3
+ colors: yellow
+ tag: topyellow
+ STYLE:
+ id: Smiley Room/Panel_anagram_7_4
+ colors: yellow
+ tag: topyellow
+ FIR:
+ id: Smiley Room/Panel_anagram_8_1
+ colors: yellow
+ tag: topyellow
+ REEF:
+ id: Smiley Room/Panel_anagram_8_2
+ colors: yellow
+ tag: topyellow
+ ROTS:
+ id: Smiley Room/Panel_anagram_8_3
+ colors: yellow
+ tag: topyellow
+ FORM (2):
+ id: Smiley Room/Panel_anagram_9_1
+ colors: yellow
+ tag: yellow top bot
+ subtag: top
+ link: ytb FORM
+ Outside The Scientific:
+ entrances:
+ Roof: True
+ The Scientific:
+ door: Scientific Entrance
+ panels:
+ OPEN:
+ id: Chemistry Room/Panel_open
+ tag: midwhite
+ CLOSE:
+ id: Chemistry Room/Panel_close
+ colors: black
+ tag: botblack
+ AHEAD:
+ id: Chemistry Room/Panel_ahead
+ colors: black
+ tag: botblack
+ doors:
+ Scientific Entrance:
+ id: Red Blue Purple Room Area Doors/Door_chemistry_lab
+ item_name: The Scientific - Entrance
+ item_group: Achievement Room Entrances
+ panels:
+ - OPEN
+ panel_doors:
+ OPEN:
+ panels:
+ - OPEN
+ The Scientific:
+ entrances:
+ Outside The Scientific:
+ room: Outside The Scientific
+ door: Scientific Entrance
+ panels:
+ Achievement:
+ id: Countdown Panels/Panel_scientific_scientific
+ colors:
+ - yellow
+ - red
+ - blue
+ - brown
+ - black
+ - purple
+ tag: forbid
+ check: True
+ achievement: The Scientific
+ HYDROGEN (1):
+ id: Chemistry Room/Panel_blue_bot_3
+ colors: blue
+ tag: tri botblue
+ link: tbb WATER
+ OXYGEN:
+ id: Chemistry Room/Panel_blue_bot_2
+ colors: blue
+ tag: tri botblue
+ link: tbb WATER
+ HYDROGEN (2):
+ id: Chemistry Room/Panel_blue_bot_4
+ colors: blue
+ tag: tri botblue
+ link: tbb WATER
+ SUGAR (1):
+ id: Chemistry Room/Panel_sugar_1
+ colors: red
+ tag: botred
+ SUGAR (2):
+ id: Chemistry Room/Panel_sugar_2
+ colors: red
+ tag: botred
+ SUGAR (3):
+ id: Chemistry Room/Panel_sugar_3
+ colors: red
+ tag: botred
+ CHLORINE:
+ id: Chemistry Room/Panel_blue_bot_5
+ colors: blue
+ tag: double botblue
+ subtag: left
+ link: holo SALT
+ SODIUM:
+ id: Chemistry Room/Panel_blue_bot_6
+ colors: blue
+ tag: double botblue
+ subtag: right
+ link: holo SALT
+ FOREST:
+ id: Chemistry Room/Panel_long_bot_1
+ colors:
+ - red
+ - blue
+ tag: chain red bot blue top
+ POUND:
+ id: Chemistry Room/Panel_long_top_1
+ colors:
+ - red
+ - blue
+ tag: chain blue mid red bot
+ ICE:
+ id: Chemistry Room/Panel_brown_bot_1
+ colors: brown
+ tag: botbrown
+ FISSION:
+ id: Chemistry Room/Panel_black_bot_1
+ colors: black
+ tag: botblack
+ FUSION:
+ id: Chemistry Room/Panel_black_bot_2
+ colors: black
+ tag: botblack
+ MISS:
+ id: Chemistry Room/Panel_blue_top_1
+ colors: blue
+ tag: double topblue
+ subtag: left
+ link: exp CHEMISTRY
+ TREE (1):
+ id: Chemistry Room/Panel_blue_top_2
+ colors: blue
+ tag: double topblue
+ subtag: right
+ link: exp CHEMISTRY
+ BIOGRAPHY:
+ id: Chemistry Room/Panel_biology_9
+ colors: purple
+ tag: midpurp
+ CACTUS:
+ id: Chemistry Room/Panel_biology_4
+ colors: red
+ tag: double botred
+ subtag: right
+ link: mero SPINE
+ VERTEBRATE:
+ id: Chemistry Room/Panel_biology_8
+ colors: red
+ tag: double botred
+ subtag: left
+ link: mero SPINE
+ ROSE:
+ id: Chemistry Room/Panel_biology_2
+ colors: red
+ tag: botred
+ TREE (2):
+ id: Chemistry Room/Panel_biology_3
+ colors: red
+ tag: botred
+ FRUIT:
+ id: Chemistry Room/Panel_biology_1
+ colors: red
+ tag: botred
+ MAMMAL:
+ id: Chemistry Room/Panel_biology_5
+ colors: red
+ tag: botred
+ BIRD:
+ id: Chemistry Room/Panel_biology_6
+ colors: red
+ tag: botred
+ FISH:
+ id: Chemistry Room/Panel_biology_7
+ colors: red
+ tag: botred
+ GRAVELY:
+ id: Chemistry Room/Panel_physics_9
+ colors: purple
+ tag: double midpurp
+ subtag: left
+ link: change GRAVITY
+ BREVITY:
+ id: Chemistry Room/Panel_biology_10
+ colors: purple
+ tag: double midpurp
+ subtag: right
+ link: change GRAVITY
+ PART:
+ id: Chemistry Room/Panel_physics_2
+ colors:
+ - blue
+ - red
+ tag: blue mid red bot
+ subtag: mid
+ link: xur PARTICLE
+ MATTER:
+ id: Chemistry Room/Panel_physics_1
+ colors:
+ - blue
+ - red
+ tag: blue mid red bot
+ subtag: bot
+ link: xur PARTICLE
+ ELECTRIC:
+ id: Chemistry Room/Panel_physics_6
+ colors:
+ - purple
+ - red
+ tag: purple mid red bot
+ subtag: mid
+ link: xpr ELECTRON
+ ATOM (1):
+ id: Chemistry Room/Panel_physics_3
+ colors:
+ - purple
+ - red
+ tag: purple mid red bot
+ subtag: bot
+ link: xpr ELECTRON
+ NEUTRAL:
+ id: Chemistry Room/Panel_physics_7
+ colors:
+ - purple
+ - red
+ tag: purple mid red bot
+ subtag: mid
+ link: xpr NEUTRON
+ ATOM (2):
+ id: Chemistry Room/Panel_physics_4
+ colors:
+ - purple
+ - red
+ tag: purple mid red bot
+ subtag: bot
+ link: xpr NEUTRON
+ PROPEL:
+ id: Chemistry Room/Panel_physics_8
+ colors:
+ - purple
+ - red
+ tag: purple mid red bot
+ subtag: mid
+ link: xpr PROTON
+ ATOM (3):
+ id: Chemistry Room/Panel_physics_5
+ colors:
+ - purple
+ - red
+ tag: purple mid red bot
+ subtag: bot
+ link: xpr PROTON
+ ORDER:
+ id: Chemistry Room/Panel_physics_11
+ colors: brown
+ tag: botbrown
+ OPTICS:
+ id: Chemistry Room/Panel_physics_10
+ colors: yellow
+ tag: midyellow
+ GRAPHITE:
+ id: Chemistry Room/Panel_yellow_bot_1
+ colors: yellow
+ tag: botyellow
+ HOT RYE:
+ id: Chemistry Room/Panel_anagram_1
+ colors: yellow
+ tag: midyellow
+ SIT SHY HOPE:
+ id: Chemistry Room/Panel_anagram_2
+ colors: yellow
+ tag: midyellow
+ ME NEXT PIER:
+ id: Chemistry Room/Panel_anagram_3
+ colors: yellow
+ tag: midyellow
+ RUT LESS:
+ id: Chemistry Room/Panel_anagram_4
+ colors: yellow
+ tag: midyellow
+ SON COUNCIL:
+ id: Chemistry Room/Panel_anagram_5
+ colors: yellow
+ tag: midyellow
+ doors:
+ Chemistry Puzzles:
+ skip_item: True
+ location_name: The Scientific - Chemistry Puzzles
+ panels:
+ - HYDROGEN (1)
+ - OXYGEN
+ - HYDROGEN (2)
+ - SUGAR (1)
+ - SUGAR (2)
+ - SUGAR (3)
+ - CHLORINE
+ - SODIUM
+ - FOREST
+ - POUND
+ - ICE
+ - FISSION
+ - FUSION
+ - MISS
+ - TREE (1)
+ Biology Puzzles:
+ skip_item: True
+ location_name: The Scientific - Biology Puzzles
+ panels:
+ - BIOGRAPHY
+ - CACTUS
+ - VERTEBRATE
+ - ROSE
+ - TREE (2)
+ - FRUIT
+ - MAMMAL
+ - BIRD
+ - FISH
+ Physics Puzzles:
+ skip_item: True
+ location_name: The Scientific - Physics Puzzles
+ panels:
+ - GRAVELY
+ - BREVITY
+ - PART
+ - MATTER
+ - ELECTRIC
+ - ATOM (1)
+ - NEUTRAL
+ - ATOM (2)
+ - PROPEL
+ - ATOM (3)
+ - ORDER
+ - OPTICS
+ paintings:
+ - id: hi_solved_painting4
+ orientation: south
+ req_blocked_when_no_doors: True # owl hallway in vanilla doors
+ Challenge Room:
+ entrances:
+ Welcome Back Area:
+ door: Welcome Door
+ Number Hunt:
+ room: Outside The Undeterred
+ door: Challenge Entrance
+ panels:
+ WELCOME:
+ id: Challenge Room/Panel_welcome_welcome
+ tag: midwhite
+ CHALLENGE:
+ id: Challenge Room/Panel_challenge_challenge
+ tag: midwhite
+ Achievement:
+ id: Countdown Panels/Panel_challenged_unchallenged
+ check: True
+ colors:
+ - black
+ - gray
+ - red
+ - blue
+ - yellow
+ - purple
+ - brown
+ - orange
+ tag: forbid
+ achievement: The Unchallenged
+ OPEN:
+ id: Challenge Room/Panel_open_nepotism
+ colors:
+ - black
+ - blue
+ tag: chain mid black !!! blue
+ SINGED:
+ id: Challenge Room/Panel_singed_singsong
+ colors:
+ - red
+ - blue
+ tag: chain mid red blue
+ NEVER TRUSTED:
+ id: Challenge Room/Panel_nevertrusted_maladjusted
+ colors: purple
+ tag: midpurp
+ CORNER:
+ id: Challenge Room/Panel_corner_corn
+ colors: red
+ tag: midred
+ STRAWBERRIES:
+ id: Challenge Room/Panel_strawberries_mold
+ colors: brown
+ tag: double botbrown
+ subtag: left
+ link: time MOLD
+ GRUB:
+ id: Challenge Room/Panel_grub_burger
+ colors:
+ - black
+ - blue
+ tag: chain mid black blue
+ CHEESE:
+ id: Challenge Room/Panel_bread_mold
+ colors: brown
+ tag: double botbrown
+ subtag: right
+ link: time MOLD
+ COLOR:
+ id: Challenge Room/Panel_color_gray
+ colors: gray
+ tag: forbid
+ WRITER:
+ id: Challenge Room/Panel_writer_songwriter
+ colors: blue
+ tag: midblue
+ "02759":
+ id: Challenge Room/Panel_tales_stale
+ colors:
+ - orange
+ - yellow
+ tag: chain mid orange yellow
+ REAL EYES:
+ id: Challenge Room/Panel_realeyes_realize
+ tag: topwhite
+ LOBS:
+ id: Challenge Room/Panel_lobs_lobster
+ colors: blue
+ tag: midblue
+ PEST ALLY:
+ id: Challenge Room/Panel_double_anagram_1
+ colors: yellow
+ tag: midyellow
+ GENIAL HALO:
+ id: Challenge Room/Panel_double_anagram_2
+ colors: yellow
+ tag: midyellow
+ DUCK LOGO:
+ id: Challenge Room/Panel_double_anagram_3
+ colors: yellow
+ tag: midyellow
+ AVIAN GREEN:
+ id: Challenge Room/Panel_double_anagram_4
+ colors: yellow
+ tag: midyellow
+ FEVER TEAR:
+ id: Challenge Room/Panel_double_anagram_5
+ colors: yellow
+ tag: midyellow
+ FACTS (Chain):
+ id: Challenge Room/Panel_facts
+ colors:
+ - red
+ - blue
+ tag: forbid
+ FACTS (1):
+ id: Challenge Room/Panel_facts2
+ colors: red
+ tag: forbid
+ FACTS (2):
+ id: Challenge Room/Panel_facts3
+ tag: forbid
+ FACTS (3):
+ id: Challenge Room/Panel_facts4
+ colors: blue
+ tag: forbid
+ FACTS (4):
+ id: Challenge Room/Panel_facts5
+ colors: blue
+ tag: forbid
+ FACTS (5):
+ id: Challenge Room/Panel_facts6
+ colors: blue
+ tag: forbid
+ LAPEL SHEEP:
+ id: Challenge Room/Panel_double_anagram_6
+ colors: yellow
+ tag: midyellow
+ doors:
+ Welcome Door:
+ id: Entry Room Area Doors/Door_challenge_challenge
+ item_group: Achievement Room Entrances
+ panels:
+ - WELCOME
diff --git a/worlds/lingo/data/README.md b/worlds/lingo/data/README.md
new file mode 100644
index 000000000000..fe834cef0d47
--- /dev/null
+++ b/worlds/lingo/data/README.md
@@ -0,0 +1,5 @@
+# lingo data
+
+The source of truth for the Lingo randomizer is (currently) the LL1.yaml and ids.yaml files located here. These files are used by the generator, the game client, and the tracker, in order to have logic that is consistent across them all.
+
+The generator does not actually read in the yaml files. Instead, a compiled datafile called generated.dat is also located in this directory. If you update LL1.yaml and/or ids.yaml, you must also regenerate the datafile using `python worlds/lingo/utils/pickle_static_data.py`. A unit test will fail if you don't.
diff --git a/worlds/lingo/data/__init__.py b/worlds/lingo/data/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat
new file mode 100644
index 000000000000..9a49d3d9d4b9
Binary files /dev/null and b/worlds/lingo/data/generated.dat differ
diff --git a/worlds/lingo/data/ids.yaml b/worlds/lingo/data/ids.yaml
new file mode 100644
index 000000000000..b46f1d36ec1a
--- /dev/null
+++ b/worlds/lingo/data/ids.yaml
@@ -0,0 +1,1622 @@
+---
+special_items:
+ Black: 444400
+ Red: 444401
+ Blue: 444402
+ Yellow: 444403
+ Green: 444404
+ Orange: 444405
+ Gray: 444406
+ Brown: 444407
+ Purple: 444408
+ ":)": 444409
+ The Feeling of Being Lost: 444575
+ Wanderlust: 444576
+ Empty White Hallways: 444577
+ Slowness Trap: 444410
+ Iceland Trap: 444411
+ Atbash Trap: 444412
+ Puzzle Skip: 444413
+panels:
+ Starting Room:
+ HI: 444400
+ HIDDEN: 444401
+ TYPE: 444402
+ THIS: 444403
+ WRITE: 444404
+ SAME: 444405
+ Hidden Room:
+ DEAD END: 444406
+ OPEN: 444407
+ LIES: 444408
+ The Seeker:
+ Achievement: 444409
+ BEAR (1): 444410
+ MINE (1): 444411
+ MINE (2): 444412
+ BOW: 444413
+ DOES: 444414
+ MOBILE (1): 444415
+ MOBILE (2): 444416
+ DESERT: 444417
+ DESSERT: 444418
+ SOW: 444419
+ SEW: 444420
+ TO: 444421
+ TOO: 444422
+ WRITE: 444423
+ EWE: 444424
+ KNOT: 444425
+ NAUGHT: 444426
+ BEAR (2): 444427
+ Second Room:
+ HI: 444428
+ LOW: 444429
+ ANOTHER TRY: 444430
+ LEVEL 2: 444431
+ Hub Room:
+ ORDER: 444432
+ SLAUGHTER: 444433
+ TRACE: 444436
+ RAT: 444437
+ OPEN: 444438
+ FOUR: 444439
+ LOST: 444440
+ FORWARD: 444441
+ BETWEEN: 444442
+ BACKWARD: 444443
+ Dead End Area:
+ FOUR: 444444
+ EIGHT: 444445
+ Pilgrim Antechamber:
+ HOT CRUST: 444446
+ PILGRIM: 444447
+ MASTERY: 444448
+ Pilgrim Room:
+ THIS: 444449
+ TIME ROOM: 444450
+ SCIENCE ROOM: 444451
+ SHINY ROCK ROOM: 444452
+ ANGRY POWER: 444453
+ MICRO LEGION: 444454
+ LOSERS RELAX: 444455
+ '906234': 444456
+ MOOR EMORDNILAP: 444457
+ HALL ROOMMATE: 444458
+ ALL GREY: 444459
+ PLUNDER ISLAND: 444460
+ FLOSS PATHS: 444461
+ Crossroads:
+ DECAY: 444462
+ NOPE: 444463
+ EIGHT: 444464
+ WE ROT: 444465
+ WORDS: 444466
+ SWORD: 444467
+ TURN: 444468
+ BEND HI: 444469
+ THE EYES: 444470
+ CORNER: 444471
+ HOLLOW: 444472
+ SWAP: 444473
+ GEL: 444474
+ THOUGH: 444475
+ CROSSROADS: 444476
+ Lost Area:
+ LOST (1): 444477
+ LOST (2): 444478
+ Amen Name Area:
+ AMEN: 444479
+ NAME: 444480
+ NINE: 444481
+ Suits Area:
+ SPADES: 444482
+ CLUBS: 444483
+ HEARTS: 444484
+ The Tenacious:
+ LEVEL (Black): 444485
+ RACECAR (Black): 444486
+ SOLOS (Black): 444487
+ LEVEL (White): 444488
+ RACECAR (White): 444489
+ SOLOS (White): 444490
+ Achievement: 444491
+ Near Far Area:
+ NEAR: 444434
+ FAR: 444435
+ Warts Straw Area:
+ WARTS: 444492
+ STRAW: 444493
+ Leaf Feel Area:
+ LEAF: 444494
+ FEEL: 444495
+ Outside The Agreeable:
+ MASSACRED: 444496
+ BLACK: 444497
+ CLOSE: 444498
+ LEFT: 444499
+ LEFT (2): 444500
+ RIGHT: 444501
+ PURPLE: 444502
+ FIVE (1): 444503
+ FIVE (2): 444504
+ HIDE: 444506
+ DAZE: 444507
+ Compass Room:
+ NORTH: 444512
+ DIAMONDS: 444513
+ FIRE: 444514
+ WINTER: 444515
+ Dread Hallway:
+ DREAD: 444516
+ The Agreeable:
+ Achievement: 444517
+ BYE: 444518
+ RETOOL: 444519
+ DRAWER: 444520
+ READ: 444521
+ DIFFERENT: 444522
+ LOW: 444523
+ ALIVE: 444524
+ THAT: 444525
+ STRESSED: 444526
+ STAR: 444527
+ TUBE: 444528
+ CAT: 444529
+ Hedge Maze:
+ DOWN: 444530
+ HIDE (1): 444531
+ HIDE (2): 444532
+ HIDE (3): 444533
+ MASTERY (1): 444534
+ MASTERY (2): 444535
+ PATH (1): 444536
+ PATH (2): 444537
+ PATH (3): 444538
+ PATH (4): 444539
+ PATH (5): 444540
+ PATH (6): 444541
+ PATH (7): 444542
+ PATH (8): 444543
+ REFLOW: 444544
+ LEAP: 444545
+ The Perceptive:
+ Achievement: 444546
+ GAZE: 444547
+ The Fearless (First Floor):
+ SPAN: 444548
+ TEAM: 444549
+ TEEM: 444550
+ IMPATIENT: 444551
+ EAT: 444552
+ The Fearless (Second Floor):
+ NONE: 444553
+ SUM: 444554
+ FUNNY: 444555
+ MIGHT: 444556
+ SAFE: 444557
+ SAME: 444558
+ CAME: 444559
+ The Fearless:
+ Achievement: 444560
+ EASY: 444561
+ SOMETIMES: 444562
+ DARK: 444563
+ EVEN: 444564
+ The Observant:
+ Achievement: 444565
+ FOUR (1): 444566
+ FOUR (2): 444567
+ BACKSIDE: 444568
+ SIX: 444569
+ FOUR (3): 444570
+ TWO (1): 444571
+ TWO (2): 444572
+ FIVE: 444573
+ BELOW (1): 444574
+ BLUE: 444575
+ BELOW (2): 444576
+ MINT (1): 444577
+ ESACREWOL: 444578
+ EULB: 444579
+ NUMBERS (1): 444580
+ NUMBERS (2): 444581
+ MINT (2): 444582
+ GREEN (1): 444583
+ GREEN (2): 444584
+ The Incomparable:
+ Achievement: 444585
+ A (One): 444586
+ A (Two): 444587
+ A (Three): 444588
+ A (Four): 444589
+ A (Five): 444590
+ A (Six): 444591
+ I (One): 444592
+ I (Two): 444593
+ I (Three): 444594
+ I (Four): 444595
+ I (Five): 444596
+ I (Six): 444597
+ I (Seven): 444598
+ Eight Room:
+ Eight Back: 444599
+ Eight Front: 444600
+ Nine: 444601
+ Orange Tower First Floor:
+ SECRET: 444602
+ DADS + ALE: 444603
+ SALT: 444604
+ Orange Tower Third Floor:
+ RED: 444605
+ DEER + WREN: 444606
+ Orange Tower Fourth Floor:
+ RUNT (1): 444607
+ RUNT (2): 444608
+ LEARNS + UNSEW: 444609
+ HOT CRUSTS: 444610
+ IRK HORN: 444611
+ Hot Crusts Area:
+ EIGHT: 444612
+ Orange Tower Fifth Floor:
+ SIZE (Small): 444613
+ SIZE (Big): 444614
+ DRAWL + RUNS: 444615
+ NINE: 444616
+ SUMMER: 444617
+ AUTUMN: 444618
+ SPRING: 444619
+ PAINTING (1): 445078
+ PAINTING (2): 445079
+ PAINTING (3): 445080
+ PAINTING (4): 445081
+ PAINTING (5): 445082
+ ROOM: 445083
+ Ending Area:
+ THE END: 444620
+ Orange Tower Seventh Floor:
+ THE MASTER: 444621
+ MASTERY: 444622
+ Behind A Smile:
+ MASTERY: 444623
+ STAIRCASE: 444629
+ Sixteen Colorful Squares:
+ MASTERY: 444624
+ Among Treetops:
+ MASTERY: 444625
+ Horizon's Edge:
+ MASTERY: 444626
+ Beneath The Lookout:
+ MASTERY: 444627
+ Rooftop Staircase:
+ MASTERY: 444628
+ Orange Tower Basement:
+ MASTERY: 444630
+ THE LIBRARY: 444631
+ Courtyard:
+ I: 444632
+ GREEN: 444633
+ PINECONE: 444634
+ ACORN: 444635
+ Yellow Backside Area:
+ BACKSIDE: 444636
+ NINE: 444637
+ First Second Third Fourth:
+ FIRST: 444638
+ SECOND: 444639
+ THIRD: 444640
+ FOURTH: 444641
+ The Colorful (White):
+ BEGIN: 444642
+ The Colorful (Black):
+ FOUND: 444643
+ The Colorful (Red):
+ LOAF: 444644
+ The Colorful (Yellow):
+ CREAM: 444645
+ The Colorful (Blue):
+ SUN: 444646
+ The Colorful (Purple):
+ SPOON: 444647
+ The Colorful (Orange):
+ LETTERS: 444648
+ The Colorful (Green):
+ WALLS: 444649
+ The Colorful (Brown):
+ IRON: 444650
+ The Colorful (Gray):
+ OBSTACLE: 444651
+ The Colorful:
+ Achievement: 444652
+ Welcome Back Area:
+ WELCOME BACK: 444653
+ SECRET: 444654
+ CLOCKWISE: 444655
+ Owl Hallway:
+ STRAYS: 444656
+ READS + RUST: 444657
+ Outside The Initiated:
+ SEVEN (1): 444658
+ SEVEN (2): 444659
+ EIGHT: 444660
+ NINE: 444661
+ BLUE: 444662
+ ORANGE: 444663
+ UNCOVER: 444664
+ OXEN: 444665
+ PAST (1): 444668
+ FUTURE (1): 444669
+ FUTURE (2): 444670
+ PAST (2): 444671
+ PRESENT: 444672
+ SMILE: 444673
+ ANGERED: 444674
+ VOTE: 444675
+ The Optimistic:
+ BACKSIDE: 444666
+ Achievement: 444667
+ The Initiated:
+ Achievement: 444676
+ DAUGHTER: 444677
+ START: 444678
+ STARE: 444679
+ HYPE: 444680
+ ABYSS: 444681
+ SWEAT: 444682
+ BEAT: 444683
+ ALUMNI: 444684
+ PATS: 444685
+ KNIGHT: 444686
+ BYTE: 444687
+ MAIM: 444688
+ MORGUE: 444689
+ CHAIR: 444690
+ HUMAN: 444691
+ BED: 444692
+ The Traveled:
+ Achievement: 444693
+ CLOSE: 444694
+ COMPOSE: 444695
+ RECORD: 444696
+ CATEGORY: 444697
+ HELLO: 444698
+ DUPLICATE: 444699
+ IDENTICAL: 444700
+ DISTANT: 444701
+ HAY: 444702
+ GIGGLE: 444703
+ CHUCKLE: 444704
+ SNITCH: 444705
+ CONCEALED: 444706
+ PLUNGE: 444707
+ AUTUMN: 444708
+ ROAD: 444709
+ FOUR: 444710
+ Outside The Bold:
+ UNOPEN: 444711
+ BEGIN: 444712
+ SIX: 444713
+ NINE: 444714
+ LEFT: 444715
+ RIGHT: 444716
+ RISE (Horizon): 444717
+ RISE (Sunrise): 444718
+ ZEN: 444719
+ SON: 444720
+ STARGAZER: 444721
+ SOUND: 444722
+ YEAST: 444723
+ WET: 444724
+ The Bold:
+ Achievement: 444725
+ FOOT: 444726
+ NEEDLE: 444727
+ FACE: 444728
+ SIGN: 444729
+ HEARTBREAK: 444730
+ UNDEAD: 444731
+ DEADLINE: 444732
+ SUSHI: 444733
+ THISTLE: 444734
+ LANDMASS: 444735
+ MASSACRED: 444736
+ AIRPLANE: 444737
+ NIGHTMARE: 444738
+ MOUTH: 444739
+ SAW: 444740
+ HAND: 444741
+ Outside The Undeterred:
+ HOLLOW: 444742
+ ART + ART: 444743
+ PEN: 444744
+ HUSTLING: 444745
+ SUNLIGHT: 444746
+ LIGHT: 444747
+ BRIGHT: 444748
+ SUNNY: 444749
+ RAINY: 444750
+ ZERO: 444751
+ ONE: 444752
+ TWO (1): 444753
+ TWO (2): 444754
+ THREE (1): 444755
+ THREE (2): 444756
+ THREE (3): 444757
+ FOUR: 444758
+ The Undeterred:
+ Achievement: 444759
+ BONE: 444760
+ EYE (1): 444761
+ MOUTH: 444762
+ IRIS: 444763
+ EYE (2): 444764
+ ICE: 444765
+ HEIGHT: 444766
+ EYE (3): 444767
+ NOT: 444768
+ JUST: 444769
+ READ: 444770
+ FATHER: 444771
+ FEATHER: 444772
+ CONTINENT: 444773
+ OCEAN: 444774
+ WALL: 444775
+ Number Hunt:
+ FIVE: 444776
+ SIX: 444777
+ SEVEN: 444778
+ EIGHT: 444779
+ NINE: 444780
+ Directional Gallery:
+ PEPPER: 444781
+ TURN: 444782
+ LEARN: 444783
+ FIVE (1): 444784
+ FIVE (2): 444785
+ SIX (1): 444786
+ SIX (2): 444787
+ SEVEN: 444788
+ EIGHT: 444789
+ NINE: 444790
+ BACKSIDE: 444791
+ '834283054': 444792
+ PARANOID: 444793
+ YELLOW: 444794
+ WADED + WEE: 444795
+ THE EYES: 444796
+ LEFT: 444797
+ RIGHT: 444798
+ MIDDLE: 444799
+ WARD: 444800
+ HIND: 444801
+ RIG: 444802
+ WINDWARD: 444803
+ LIGHT: 444804
+ REWIND: 444805
+ Color Hunt:
+ EXIT: 444806
+ HUES: 444807
+ RED: 444808
+ BLUE: 444809
+ YELLOW: 444810
+ GREEN: 444811
+ PURPLE: 444812
+ ORANGE: 444813
+ Champion's Rest:
+ YOU: 444814
+ ME: 444815
+ SECRET BLUE: 444816
+ SECRET YELLOW: 444817
+ SECRET RED: 444818
+ The Bearer:
+ Achievement: 444819
+ MIDDLE: 444820
+ FARTHER: 444821
+ BACKSIDE: 444822
+ PART: 444823
+ HEART: 444824
+ The Bearer (East):
+ SIX: 444825
+ PEACE: 444826
+ The Bearer (North):
+ SILENT (1): 444827
+ SILENT (2): 444828
+ SPACE: 444829
+ WARTS: 444830
+ The Bearer (South):
+ SIX: 444831
+ TENT: 444832
+ BOWL: 444833
+ The Bearer (West):
+ SMILE: 444834
+ SNOW: 444835
+ Bearer Side Area:
+ SHORTCUT: 444836
+ POTS: 444837
+ Cross Tower (East):
+ WINTER: 444838
+ Cross Tower (North):
+ NORTH: 444839
+ Cross Tower (South):
+ FIRE: 444840
+ Cross Tower (West):
+ DIAMONDS: 444841
+ The Steady (Rose):
+ SOAR: 444842
+ The Steady (Ruby):
+ BURY: 444843
+ The Steady (Carnation):
+ INCARNATION: 444844
+ The Steady (Sunflower):
+ SUN: 444845
+ The Steady (Plum):
+ LUMP: 444846
+ The Steady (Lime):
+ LIMELIGHT: 444847
+ The Steady (Lemon):
+ MELON: 444848
+ The Steady (Topaz):
+ TOP: 444849
+ MASTERY: 444850
+ The Steady (Orange):
+ BLUE: 444851
+ The Steady (Sapphire):
+ SAP: 444852
+ The Steady (Blueberry):
+ BLUE: 444853
+ The Steady (Amber):
+ ANTECHAMBER: 444854
+ The Steady (Emerald):
+ HERALD: 444855
+ The Steady (Amethyst):
+ PACIFIST: 444856
+ The Steady (Lilac):
+ LIE LACK: 444857
+ The Steady (Cherry):
+ HAIRY: 444858
+ The Steady:
+ Achievement: 444859
+ Knight Night (Outer Ring):
+ NIGHT: 444860
+ KNIGHT: 444861
+ BEE: 444862
+ NEW: 444863
+ FORE: 444864
+ TRUSTED (1): 444865
+ TRUSTED (2): 444866
+ ENCRUSTED: 444867
+ ADJUST (1): 444868
+ ADJUST (2): 444869
+ RIGHT: 444870
+ TRUST: 444871
+ Knight Night (Right Upper Segment):
+ RUST (1): 444872
+ RUST (2): 444873
+ Knight Night (Right Lower Segment):
+ ADJUST: 444874
+ BEFORE: 444875
+ BE: 444876
+ LEFT: 444877
+ TRUST: 444878
+ Knight Night (Final):
+ TRUSTED: 444879
+ Knight Night Exit:
+ SEVEN (1): 444880
+ SEVEN (2): 444881
+ SEVEN (3): 444882
+ DEAD END: 444883
+ WARNER: 444884
+ The Artistic (Smiley):
+ Achievement: 444885
+ FINE: 444886
+ BLADE: 444887
+ RED: 444888
+ BEARD: 444889
+ ICE: 444890
+ ROOT: 444891
+ The Artistic (Panda):
+ EYE (Top): 444892
+ EYE (Bottom): 444893
+ LADYLIKE: 444894
+ WATER: 444895
+ OURS: 444896
+ DAYS: 444897
+ NIGHTTIME: 444898
+ NIGHT: 444899
+ The Artistic (Lattice):
+ POSH: 444900
+ MALL: 444901
+ DEICIDE: 444902
+ WAVER: 444903
+ REPAID: 444904
+ BABY: 444905
+ LOBE: 444906
+ BOWELS: 444907
+ The Artistic (Apple):
+ SPRIG: 444908
+ RELEASES: 444909
+ MUCH: 444910
+ FISH: 444911
+ MASK: 444912
+ HILL: 444913
+ TINE: 444914
+ THING: 444915
+ The Artistic (Hint Room):
+ THEME: 444916
+ PAINTS: 444917
+ I: 444918
+ KIT: 444919
+ The Discerning:
+ Achievement: 444920
+ HITS: 444921
+ WARRED: 444922
+ REDRAW: 444923
+ ADDER: 444924
+ LAUGHTERS: 444925
+ STONE: 444926
+ ONSET: 444927
+ RAT: 444928
+ DUSTY: 444929
+ ARTS: 444930
+ TSAR: 444931
+ STATE: 444932
+ REACT: 444933
+ DEAR: 444934
+ DARE: 444935
+ SEAM: 444936
+ The Eyes They See:
+ NEAR: 444937
+ EIGHT: 444938
+ Far Window:
+ FAR: 444939
+ Outside The Wondrous:
+ SHRINK: 444940
+ The Wondrous (Bookcase):
+ CASE: 444941
+ The Wondrous (Chandelier):
+ CANDLE HEIR: 444942
+ The Wondrous (Window):
+ GLASS: 444943
+ The Wondrous (Table):
+ WOOD: 444944
+ BROOK NOD: 444945
+ The Wondrous:
+ FIREPLACE: 444946
+ Achievement: 444947
+ Arrow Garden:
+ MASTERY: 444948
+ SHARP: 444949
+ Hallway Room (1):
+ OUT: 444505
+ WALL: 444508
+ KEEP: 444509
+ BAILEY: 444510
+ TOWER: 444511
+ Hallway Room (2):
+ WISE: 444950
+ CLOCK: 444951
+ ER: 444952
+ COUNT: 444953
+ Hallway Room (3):
+ TRANCE: 444954
+ FORM: 444955
+ A: 444956
+ SHUN: 444957
+ Hallway Room (4):
+ WHEEL: 444958
+ Elements Area:
+ A: 444959
+ NINE: 444960
+ UNDISTRACTED: 444961
+ MASTERY: 444962
+ EARTH: 444963
+ WATER: 444964
+ AIR: 444965
+ Outside The Wanderer:
+ WANDERLUST: 444966
+ The Wanderer:
+ Achievement: 444967
+ '7890': 444968
+ '6524': 444969
+ '951': 444970
+ '4524': 444971
+ LEARN: 444972
+ DUST: 444973
+ STAR: 444974
+ WANDER: 444975
+ Art Gallery:
+ EIGHT: 444976
+ EON: 444977
+ TRUSTWORTHY: 444978
+ FREE: 444979
+ OUR: 444980
+ ORDER: 444981
+ Art Gallery (Second Floor):
+ HOUSE: 444982
+ PATH: 444983
+ PARK: 444984
+ CARRIAGE: 444985
+ Art Gallery (Third Floor):
+ AN: 444986
+ MAY: 444987
+ ANY: 444988
+ MAN: 444989
+ Art Gallery (Fourth Floor):
+ URNS: 444990
+ LEARNS: 444991
+ RUNTS: 444992
+ SEND - USE: 444993
+ TRUST: 444994
+ '062459': 444995
+ Rhyme Room (Smiley):
+ LOANS: 444996
+ SKELETON: 444997
+ REPENTANCE: 444998
+ WORD: 444999
+ SCHEME: 445000
+ FANTASY: 445001
+ HISTORY: 445002
+ SECRET: 445003
+ Rhyme Room (Cross):
+ NINE: 445004
+ FERN: 445005
+ STAY: 445006
+ FRIEND: 445007
+ RISE: 445008
+ PLUMP: 445009
+ BOUNCE: 445010
+ SCRAWL: 445011
+ PLUNGE: 445012
+ Rhyme Room (Circle):
+ BIRD: 445014
+ LETTER: 445015
+ FORBIDDEN: 445016
+ CONCEALED: 445017
+ VIOLENT: 445018
+ MUTE: 445019
+ Rhyme Room (Looped Square):
+ WALKED: 445020
+ OBSTRUCTED: 445021
+ SKIES: 445022
+ SWELL: 445023
+ PENNED: 445024
+ CLIMB: 445025
+ TROUBLE: 445026
+ DUPLICATE: 445027
+ Rhyme Room (Target):
+ WILD: 445028
+ KID: 445029
+ PISTOL: 445030
+ GEM: 445031
+ INNOVATIVE (Top): 445032
+ INNOVATIVE (Bottom): 445033
+ LEAP: 445013
+ Room Room:
+ DOOR (1): 445034
+ DOOR (2): 445035
+ WINDOW: 445036
+ STAIRS: 445037
+ PAINTING: 445038
+ FLOOR (1): 445039
+ FLOOR (2): 445040
+ FLOOR (3): 445041
+ FLOOR (4): 445042
+ FLOOR (5): 445043
+ FLOOR (6): 445044
+ FLOOR (7): 445045
+ FLOOR (8): 445046
+ CEILING (1): 445048
+ CEILING (2): 445049
+ CEILING (3): 445050
+ CEILING (4): 445051
+ CEILING (5): 445052
+ CEILING (6): 445047
+ WALL (1): 445053
+ WALL (2): 445054
+ WALL (3): 445055
+ WALL (4): 445056
+ WALL (5): 445057
+ WALL (6): 445058
+ WALL (7): 445059
+ WALL (8): 445060
+ WALL (9): 445061
+ WALL (10): 445062
+ WALL (11): 445063
+ WALL (12): 445064
+ WALL (13): 445065
+ WALL (14): 445066
+ WALL (15): 445067
+ WALL (16): 445068
+ WALL (17): 445069
+ WALL (18): 445070
+ WALL (19): 445071
+ WALL (20): 445072
+ WALL (21): 445073
+ BROOMED: 445074
+ LAYS: 445075
+ BASE: 445076
+ MASTERY: 445077
+ Outside The Wise:
+ KITTEN: 445084
+ CAT: 445085
+ The Wise:
+ Achievement: 445086
+ PUPPY: 445087
+ ADULT: 445088
+ BREAD: 445089
+ DINOSAUR: 445090
+ OAK: 445091
+ CORPSE: 445092
+ BEFORE: 445093
+ YOUR: 445094
+ BETWIXT: 445095
+ NIGH: 445096
+ CONNEXION: 445097
+ THOU: 445098
+ The Red:
+ Achievement: 445099
+ PANDEMIC (1): 445100
+ TRINITY: 445101
+ CHEMISTRY: 445102
+ FLUSTERED: 445103
+ PANDEMIC (2): 445104
+ COUNTERCLOCKWISE: 445105
+ FEARLESS: 445106
+ DEFORESTATION: 445107
+ CRAFTSMANSHIP: 445108
+ CAMEL: 445109
+ LION: 445110
+ TIGER: 445111
+ SHIP (1): 445112
+ SHIP (2): 445113
+ GIRAFFE: 445114
+ The Ecstatic:
+ Achievement: 445115
+ FORM (1): 445116
+ WIND: 445117
+ EGGS: 445118
+ VEGETABLES: 445119
+ WATER: 445120
+ FRUITS: 445121
+ LEAVES: 445122
+ VINES: 445123
+ ICE: 445124
+ STYLE: 445125
+ FIR: 445126
+ REEF: 445127
+ ROTS: 445128
+ FORM (2): 445129
+ Outside The Scientific:
+ OPEN: 445130
+ CLOSE: 445131
+ AHEAD: 445132
+ The Scientific:
+ Achievement: 445133
+ HYDROGEN (1): 445134
+ OXYGEN: 445135
+ HYDROGEN (2): 445136
+ SUGAR (1): 445137
+ SUGAR (2): 445138
+ SUGAR (3): 445139
+ CHLORINE: 445140
+ SODIUM: 445141
+ FOREST: 445142
+ POUND: 445143
+ ICE: 445144
+ FISSION: 445145
+ FUSION: 445146
+ MISS: 445147
+ TREE (1): 445148
+ BIOGRAPHY: 445149
+ CACTUS: 445150
+ VERTEBRATE: 445151
+ ROSE: 445152
+ TREE (2): 445153
+ FRUIT: 445154
+ MAMMAL: 445155
+ BIRD: 445156
+ FISH: 445157
+ GRAVELY: 445158
+ BREVITY: 445159
+ PART: 445160
+ MATTER: 445161
+ ELECTRIC: 445162
+ ATOM (1): 445163
+ NEUTRAL: 445164
+ ATOM (2): 445165
+ PROPEL: 445166
+ ATOM (3): 445167
+ ORDER: 445168
+ OPTICS: 445169
+ GRAPHITE: 445170
+ HOT RYE: 445171
+ SIT SHY HOPE: 445172
+ ME NEXT PIER: 445173
+ RUT LESS: 445174
+ SON COUNCIL: 445175
+ Challenge Room:
+ WELCOME: 445176
+ CHALLENGE: 445177
+ Achievement: 445178
+ OPEN: 445179
+ SINGED: 445180
+ NEVER TRUSTED: 445181
+ CORNER: 445182
+ STRAWBERRIES: 445183
+ GRUB: 445184
+ CHEESE: 445185
+ COLOR: 445186
+ WRITER: 445187
+ '02759': 445188
+ REAL EYES: 445189
+ LOBS: 445190
+ PEST ALLY: 445191
+ GENIAL HALO: 445192
+ DUCK LOGO: 445193
+ AVIAN GREEN: 445194
+ FEVER TEAR: 445195
+ FACTS (Chain): 445196
+ FACTS (1): 445197
+ FACTS (2): 445198
+ FACTS (3): 445199
+ FACTS (4): 445200
+ FACTS (5): 445201
+ LAPEL SHEEP: 445202
+doors:
+ Starting Room:
+ Back Right Door:
+ item: 444416
+ location: 444401
+ Rhyme Room Entrance:
+ item: 444417
+ Hidden Room:
+ Dead End Door:
+ item: 444419
+ Knight Night Entrance:
+ item: 444421
+ Seeker Entrance:
+ item: 444422
+ location: 444407
+ Rhyme Room Entrance:
+ item: 444423
+ Second Room:
+ Exit Door:
+ item: 444424
+ location: 445203
+ Hub Room:
+ Crossroads Entrance:
+ item: 444425
+ location: 444432
+ Tenacious Entrance:
+ item: 444426
+ location: 444433
+ Shortcut to Hedge Maze:
+ item: 444430
+ location: 444436
+ Near RAT Door:
+ item: 444432
+ Traveled Entrance:
+ item: 444433
+ location: 444438
+ Sunwarps:
+ 1 Sunwarp:
+ item: 444581
+ 2 Sunwarp:
+ item: 444588
+ 3 Sunwarp:
+ item: 444586
+ 4 Sunwarp:
+ item: 444585
+ 5 Sunwarp:
+ item: 444587
+ 6 Sunwarp:
+ item: 444584
+ Pilgrim Antechamber:
+ Sun Painting:
+ item: 444436
+ location: 445205
+ Pilgrim Room:
+ Shortcut to The Seeker:
+ item: 444437
+ location: 444449
+ Crossroads:
+ Tenacious Entrance:
+ item: 444438
+ location: 444462
+ Discerning Entrance:
+ item: 444439
+ location: 444463
+ Tower Entrance:
+ item: 444440
+ location: 444465
+ Tower Back Entrance:
+ item: 444442
+ location: 445206
+ Words Sword Door:
+ item: 444443
+ location: 445207
+ Eye Wall:
+ item: 444445
+ location: 444469
+ Hollow Hallway:
+ item: 444446
+ Roof Access:
+ item: 444447
+ Lost Area:
+ Exit:
+ item: 444448
+ location: 445208
+ Amen Name Area:
+ Exit:
+ item: 444449
+ location: 445209
+ The Tenacious:
+ Shortcut to Hub Room:
+ item: 444450
+ location: 445210
+ White Palindromes:
+ location: 445211
+ Near Far Area:
+ Door:
+ item: 444428
+ location: 445204
+ Warts Straw Area:
+ Door:
+ item: 444451
+ location: 445212
+ Leaf Feel Area:
+ Door:
+ item: 444452
+ location: 445213
+ Outside The Agreeable:
+ Tenacious Entrance:
+ item: 444453
+ location: 444496
+ Black Door:
+ item: 444454
+ location: 444497
+ Agreeable Entrance:
+ item: 444455
+ location: 444498
+ Painting Shortcut:
+ item: 444456
+ location: 444501
+ Purple Barrier:
+ item: 444457
+ Compass Room:
+ Lookout Entrance:
+ item: 444579
+ location: 445271
+ Dread Hallway:
+ Tenacious Entrance:
+ item: 444462
+ location: 444516
+ The Agreeable:
+ Shortcut to Hedge Maze:
+ item: 444463
+ location: 444518
+ Hedge Maze:
+ Perceptive Entrance:
+ item: 444464
+ location: 444530
+ Painting Shortcut:
+ item: 444465
+ Observant Entrance:
+ item: 444466
+ Hide and Seek:
+ location: 445215
+ The Fearless (First Floor):
+ Second Floor:
+ item: 444468
+ location: 445216
+ The Fearless (Second Floor):
+ Third Floor:
+ item: 444471
+ location: 445217
+ The Observant:
+ Backside Door:
+ item: 444472
+ location: 445218
+ Stairs:
+ item: 444474
+ location: 444569
+ The Incomparable:
+ Eight Door:
+ item: 444475
+ location: 445219
+ Orange Tower:
+ Second Floor:
+ item: 444476
+ Third Floor:
+ item: 444477
+ Fourth Floor:
+ item: 444478
+ Fifth Floor:
+ item: 444479
+ Sixth Floor:
+ item: 444480
+ Seventh Floor:
+ item: 444481
+ Orange Tower First Floor:
+ Shortcut to Hub Room:
+ item: 444483
+ location: 444602
+ Salt Pepper Door:
+ item: 444485
+ location: 445220
+ Orange Tower Third Floor:
+ Red Barrier:
+ item: 444486
+ Rhyme Room Entrance:
+ item: 444487
+ Orange Barrier:
+ item: 444488
+ location: 445221
+ Orange Tower Fourth Floor:
+ Hot Crusts Door:
+ item: 444490
+ location: 444610
+ Orange Tower Fifth Floor:
+ Welcome Back:
+ item: 444491
+ location: 445222
+ Orange Tower Seventh Floor:
+ Mastery:
+ item: 444493
+ Mastery Panels:
+ location: 445223
+ Courtyard:
+ Painting Shortcut:
+ item: 444494
+ Green Barrier:
+ item: 444495
+ First Second Third Fourth:
+ Backside Door:
+ item: 444496
+ location: 445224
+ The Colorful (White):
+ Progress Door:
+ item: 444497
+ location: 445225
+ The Colorful (Black):
+ Progress Door:
+ item: 444499
+ location: 445226
+ The Colorful (Red):
+ Progress Door:
+ item: 444500
+ location: 445227
+ The Colorful (Yellow):
+ Progress Door:
+ item: 444501
+ location: 445228
+ The Colorful (Blue):
+ Progress Door:
+ item: 444502
+ location: 445229
+ The Colorful (Purple):
+ Progress Door:
+ item: 444503
+ location: 445230
+ The Colorful (Orange):
+ Progress Door:
+ item: 444504
+ location: 445231
+ The Colorful (Green):
+ Progress Door:
+ item: 444505
+ location: 445232
+ The Colorful (Brown):
+ Progress Door:
+ item: 444506
+ location: 445233
+ The Colorful (Gray):
+ Progress Door:
+ item: 444507
+ location: 445234
+ Welcome Back Area:
+ Shortcut to Starting Room:
+ item: 444508
+ location: 444653
+ Owl Hallway:
+ Shortcut to Hedge Maze:
+ item: 444509
+ location: 444656
+ Outside The Initiated:
+ Shortcut to Hub Room:
+ item: 444510
+ location: 444664
+ Blue Barrier:
+ item: 444511
+ Orange Barrier:
+ item: 444512
+ Initiated Entrance:
+ item: 444513
+ location: 444665
+ Green Barrier:
+ item: 444514
+ location: 445235
+ Purple Barrier:
+ item: 444515
+ location: 445236
+ Entrance:
+ item: 444516
+ location: 445237
+ Eight Door:
+ item: 444578
+ The Traveled:
+ Color Hallways Entrance:
+ item: 444517
+ location: 444698
+ Outside The Bold:
+ Bold Entrance:
+ item: 444518
+ location: 444711
+ Painting Shortcut:
+ item: 444519
+ Steady Entrance:
+ item: 444520
+ location: 444712
+ Outside The Undeterred:
+ Undeterred Entrance:
+ item: 444521
+ location: 444744
+ Painting Shortcut:
+ item: 444522
+ Green Painting:
+ item: 444523
+ Twos:
+ item: 444524
+ location: 444752
+ Threes:
+ item: 444525
+ location: 445238
+ Number Hunt:
+ item: 444526
+ location: 445239
+ Fours:
+ item: 444527
+ Fives:
+ item: 444528
+ location: 445240
+ Challenge Entrance:
+ item: 444529
+ location: 444751
+ Number Hunt:
+ Door to Directional Gallery:
+ item: 444530
+ Sixes:
+ item: 444532
+ location: 445241
+ Sevens:
+ item: 444533
+ location: 445242
+ Eights:
+ item: 444534
+ location: 445243
+ Nines:
+ item: 444535
+ location: 445244
+ Zero Door:
+ item: 444536
+ location: 445245
+ Directional Gallery:
+ Shortcut to The Undeterred:
+ item: 444537
+ location: 445246
+ Yellow Barrier:
+ item: 444538
+ Color Hunt:
+ Shortcut to The Steady:
+ item: 444539
+ location: 444806
+ The Bearer:
+ Entrance:
+ item: 444540
+ location: 444820
+ Backside Door:
+ item: 444541
+ location: 444821
+ Bearer Side Area:
+ Shortcut to Tower:
+ item: 444542
+ location: 445247
+ Knight Night (Final):
+ Exit:
+ item: 444543
+ location: 445248
+ The Artistic (Smiley):
+ Door to Panda:
+ item: 444544
+ location: 445249
+ The Artistic (Panda):
+ Door to Lattice:
+ item: 444546
+ location: 445250
+ The Artistic (Lattice):
+ Door to Apple:
+ item: 444547
+ location: 445251
+ The Artistic (Apple):
+ Door to Smiley:
+ item: 444548
+ location: 445252
+ The Eyes They See:
+ Exit:
+ item: 444549
+ location: 444937
+ Outside The Wondrous:
+ Wondrous Entrance:
+ item: 444550
+ location: 444940
+ The Wondrous (Doorknob):
+ Painting Shortcut:
+ item: 444551
+ The Wondrous:
+ Exit:
+ item: 444552
+ location: 444947
+ Hallway Room (1):
+ Exit:
+ item: 444459
+ location: 445214
+ Hallway Room (2):
+ Exit:
+ item: 444553
+ location: 445253
+ Hallway Room (3):
+ Exit:
+ item: 444554
+ location: 445254
+ Hallway Room (4):
+ Exit:
+ item: 444555
+ location: 445255
+ Outside The Wanderer:
+ Wanderer Entrance:
+ item: 444556
+ location: 444966
+ Tower Entrance:
+ item: 444557
+ Art Gallery:
+ Second Floor:
+ item: 444558
+ First Floor Puzzles:
+ location: 445256
+ Third Floor:
+ item: 444559
+ Fourth Floor:
+ item: 444560
+ Fifth Floor:
+ item: 444561
+ Exit:
+ item: 444562
+ location: 444981
+ Art Gallery (Second Floor):
+ Puzzles:
+ location: 445257
+ Art Gallery (Third Floor):
+ Puzzles:
+ location: 445258
+ Art Gallery (Fourth Floor):
+ Puzzles:
+ location: 445259
+ Rhyme Room (Smiley):
+ Door to Target:
+ item: 444564
+ Door to Target (Location):
+ location: 445260
+ Rhyme Room (Cross):
+ Exit:
+ item: 444565
+ location: 445261
+ Rhyme Room (Circle):
+ Door to Smiley:
+ item: 444566
+ location: 445262
+ Rhyme Room (Looped Square):
+ Door to Circle:
+ item: 444567
+ location: 445263
+ Door to Cross:
+ item: 444568
+ location: 445264
+ Door to Target:
+ item: 444569
+ location: 445265
+ Rhyme Room (Target):
+ Door to Cross:
+ item: 444570
+ location: 445266
+ Room Room:
+ Cellar Exit:
+ item: 444571
+ location: 445076
+ Outside The Wise:
+ Wise Entrance:
+ item: 444572
+ location: 445267
+ Outside The Scientific:
+ Scientific Entrance:
+ item: 444573
+ location: 445130
+ The Scientific:
+ Chemistry Puzzles:
+ location: 445268
+ Biology Puzzles:
+ location: 445269
+ Physics Puzzles:
+ location: 445270
+ Challenge Room:
+ Welcome Door:
+ item: 444574
+ location: 445176
+door_groups:
+ Rhyme Room Doors: 444418
+ Dead End Area Access: 444420
+ Entrances to The Tenacious: 444427
+ Symmetry Doors: 444429
+ Hedge Maze Doors: 444431
+ Entrance to The Traveled: 444434
+ Crossroads - Tower Entrances: 444441
+ Crossroads Doors: 444444
+ Color Hunt Barriers: 444458
+ Hallway Room Doors: 444460
+ Observant Doors: 444467
+ Fearless Doors: 444469
+ Backside Doors: 444473
+ Orange Tower First Floor - Shortcuts: 444484
+ Welcome Back Doors: 444492
+ Colorful Doors: 444498
+ Directional Gallery Doors: 444531
+ Artistic Doors: 444545
+ Sunwarps: 444582
+progression:
+ Progressive Hallway Room: 444461
+ Progressive Fearless: 444470
+ Progressive Orange Tower: 444482
+ Progressive Art Gallery: 444563
+ Progressive Colorful: 444580
+ Progressive Pilgrimage: 444583
+ Progressive Suits Area: 444602
+ Progressive Symmetry Room: 444608
+ Progressive Number Hunt: 444654
+panel_doors:
+ Starting Room:
+ HIDDEN: 444589
+ Hidden Room:
+ OPEN: 444590
+ Hub Room:
+ ORDER: 444591
+ SLAUGHTER: 444592
+ TRACE: 444594
+ RAT: 444595
+ OPEN: 444596
+ Crossroads:
+ DECAY: 444597
+ NOPE: 444598
+ WE ROT: 444599
+ WORDS SWORD: 444600
+ BEND HI: 444601
+ Lost Area:
+ LOST: 444603
+ Amen Name Area:
+ AMEN NAME: 444604
+ The Tenacious:
+ Black Palindromes: 444605
+ Near Far Area:
+ NEAR FAR: 444606
+ Warts Straw Area:
+ WARTS STRAW: 444609
+ Leaf Feel Area:
+ LEAF FEEL: 444610
+ Outside The Agreeable:
+ MASSACRED: 444611
+ BLACK: 444612
+ CLOSE: 444613
+ RIGHT: 444614
+ Compass Room:
+ Lookout: 444615
+ Hedge Maze:
+ DOWN: 444617
+ The Perceptive:
+ GAZE: 444618
+ The Observant:
+ BACKSIDE: 444619
+ STAIRS: 444621
+ The Incomparable:
+ Giant Sevens: 444622
+ Orange Tower:
+ Access: 444623
+ Orange Tower First Floor:
+ SECRET: 444624
+ Orange Tower Fourth Floor:
+ HOT CRUSTS: 444625
+ Orange Tower Fifth Floor:
+ SIZE: 444626
+ First Second Third Fourth:
+ FIRST SECOND THIRD FOURTH: 444627
+ The Colorful (White):
+ BEGIN: 444628
+ The Colorful (Black):
+ FOUND: 444630
+ The Colorful (Red):
+ LOAF: 444631
+ The Colorful (Yellow):
+ CREAM: 444632
+ The Colorful (Blue):
+ SUN: 444633
+ The Colorful (Purple):
+ SPOON: 444634
+ The Colorful (Orange):
+ LETTERS: 444635
+ The Colorful (Green):
+ WALLS: 444636
+ The Colorful (Brown):
+ IRON: 444637
+ The Colorful (Gray):
+ OBSTACLE: 444638
+ Owl Hallway:
+ STRAYS: 444639
+ Outside The Initiated:
+ UNCOVER: 444640
+ OXEN: 444641
+ Outside The Bold:
+ UNOPEN: 444642
+ BEGIN: 444643
+ Outside The Undeterred:
+ ZERO: 444644
+ PEN: 444645
+ TWO: 444646
+ THREE: 444647
+ FOUR: 444648
+ Number Hunt:
+ FIVE: 444649
+ SIX: 444650
+ SEVEN: 444651
+ EIGHT: 444652
+ NINE: 444653
+ Color Hunt:
+ EXIT: 444655
+ RED: 444656
+ BLUE: 444658
+ YELLOW: 444659
+ ORANGE: 444660
+ PURPLE: 444661
+ GREEN: 444662
+ The Bearer:
+ FARTHER: 444663
+ MIDDLE: 444664
+ Knight Night (Final):
+ TRUSTED: 444665
+ Outside The Wondrous:
+ SHRINK: 444666
+ Hallway Room (1):
+ CASTLE: 444667
+ Hallway Room (2):
+ COUNTERCLOCKWISE: 444669
+ Hallway Room (3):
+ TRANSFORMATION: 444670
+ Hallway Room (4):
+ WHEELBARROW: 444671
+ Outside The Wanderer:
+ WANDERLUST: 444672
+ Art Gallery:
+ ORDER: 444673
+ Room Room:
+ STAIRS: 444674
+ Colors: 444676
+ Outside The Wise:
+ KITTEN CAT: 444677
+ Outside The Scientific:
+ OPEN: 444678
+ Directional Gallery:
+ TURN LEARN: 444679
+panel_groups:
+ Tenacious Entrance Panels: 444593
+ Symmetry Room Panels: 444607
+ Backside Entrance Panels: 444620
+ Colorful Panels: 444629
+ Color Hunt Panels: 444657
+ Hallway Room Panels: 444668
+ Room Room Panels: 444675
diff --git a/worlds/lingo/datatypes.py b/worlds/lingo/datatypes.py
new file mode 100644
index 000000000000..9521422ab154
--- /dev/null
+++ b/worlds/lingo/datatypes.py
@@ -0,0 +1,95 @@
+from enum import Enum, Flag, auto
+from typing import List, NamedTuple, Optional
+
+
+class RoomAndDoor(NamedTuple):
+ room: Optional[str]
+ door: str
+
+
+class RoomAndPanel(NamedTuple):
+ room: Optional[str]
+ panel: str
+
+
+class RoomAndPanelDoor(NamedTuple):
+ room: Optional[str]
+ panel_door: str
+
+
+class EntranceType(Flag):
+ NORMAL = auto()
+ PAINTING = auto()
+ SUNWARP = auto()
+ WARP = auto()
+ CROSSROADS_ROOF_ACCESS = auto()
+
+
+class RoomEntrance(NamedTuple):
+ room: str # source room
+ door: Optional[RoomAndDoor]
+ type: EntranceType
+
+
+class Room(NamedTuple):
+ name: str
+ entrances: List[RoomEntrance]
+
+
+class DoorType(Enum):
+ NORMAL = 1
+ SUNWARP = 2
+ SUN_PAINTING = 3
+
+
+class Door(NamedTuple):
+ name: str
+ item_name: str
+ location_name: Optional[str]
+ panels: Optional[List[RoomAndPanel]]
+ skip_location: bool
+ skip_item: bool
+ has_doors: bool
+ painting_ids: List[str]
+ event: bool
+ door_group: Optional[str]
+ include_reduce: bool
+ type: DoorType
+ item_group: Optional[str]
+
+
+class Panel(NamedTuple):
+ required_rooms: List[str]
+ required_doors: List[RoomAndDoor]
+ required_panels: List[RoomAndPanel]
+ colors: List[str]
+ check: bool
+ event: bool
+ exclude_reduce: bool
+ achievement: bool
+ non_counting: bool
+ panel_door: Optional[RoomAndPanelDoor] # This will always be fully specified.
+ location_name: Optional[str]
+
+
+class PanelDoor(NamedTuple):
+ item_name: str
+ panel_group: Optional[str]
+
+
+class Painting(NamedTuple):
+ id: str
+ room: str
+ enter_only: bool
+ exit_only: bool
+ required: bool
+ required_when_no_doors: bool
+ required_door: Optional[RoomAndDoor]
+ disable: bool
+ req_blocked: bool
+ req_blocked_when_no_doors: bool
+
+
+class Progression(NamedTuple):
+ item_name: str
+ index: int
diff --git a/worlds/lingo/docs/en_Lingo.md b/worlds/lingo/docs/en_Lingo.md
new file mode 100644
index 000000000000..c7e1bfc8192e
--- /dev/null
+++ b/worlds/lingo/docs/en_Lingo.md
@@ -0,0 +1,42 @@
+# Lingo
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
+config file.
+
+## What does randomization do to this game?
+
+There are a couple of modes of randomization currently available, and you can pick and choose which ones you would like
+to use.
+
+* **Door shuffle**: There are many doors in the game, which are opened by completing a set of panels. With door shuffle
+ on, the doors become items and only open up once you receive the corresponding item. The panel sets that would
+ ordinarily open the doors become locations.
+
+* **Color shuffle**: There are ten different colors of puzzle in the game, each representing a different mechanic. With
+ color shuffle on, you would start with only access to white puzzles. Puzzles of other colors will require you to
+ receive an item in order to solve them (e.g. you can't solve any red puzzles until you receive the "Red" item).
+
+* **Panel shuffle**: Panel shuffling replaces the puzzles on each panel with different ones. So far, the only mode of
+ panel shuffling is "rearrange" mode, which simply shuffles the already-existing puzzles from the base game onto
+ different panels.
+
+* **Painting shuffle**: This randomizes the appearance of the paintings in the game, as well as which of them are warps,
+ and the locations that they warp you to. It is the equivalent of an entrance randomizer in another game.
+
+## What is a "check" in this game?
+
+Most panels / panel sets that open a door are now location checks, even if door shuffle is not enabled. Various other
+puzzles are also location checks, including the achievement panels for each area.
+
+## What about wall snipes?
+
+"Wall sniping" refers to the fact that you are able to solve puzzles on the other side of opaque walls. This randomizer
+does not change how wall snipes work, but it will never require the use of them. There are three puzzles from the base
+game that you would ordinarily be expected to wall snipe. The randomizer moves these panels out of the wall or otherwise
+reveals them so that a snipe is not necessary.
+
+Because of this, all wall snipes are considered out of logic. This includes sniping The Bearer's MIDDLE while standing
+outside The Bold, sniping The Colorful without opening all of the color doors, and sniping WELCOME from next to WELCOME
+BACK.
diff --git a/worlds/lingo/docs/setup_en.md b/worlds/lingo/docs/setup_en.md
new file mode 100644
index 000000000000..0e68c7ed45c4
--- /dev/null
+++ b/worlds/lingo/docs/setup_en.md
@@ -0,0 +1,43 @@
+# Lingo Randomizer Setup
+
+## Required Software
+
+- [Lingo](https://store.steampowered.com/app/1814170/Lingo/)
+- [Lingo Archipelago Randomizer](https://steamcommunity.com/sharedfiles/filedetails/?id=3092505110)
+
+## Optional Software
+
+- [Archipelago Text Client](https://github.com/ArchipelagoMW/Archipelago/releases)
+- [Lingo AP Tracker](https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md)
+
+## Installation
+
+You can use the above Steam Workshop link to subscribe to the Lingo Archipelago Randomizer. This will automatically
+download the client, as well as update it whenever an update is available.
+
+If you don't want to use Steam Workshop, you can also
+[download the randomizer manually](https://code.fourisland.com/lingo-archipelago/about/) using the instructions on the
+linked page.
+
+## Joining a Multiworld game
+
+1. Launch Lingo
+2. Click on Settings, and then Level. Choose Archipelago from the list.
+3. Start a new game. Leave the name field blank (anything you type in will be
+ ignored).
+4. Enter the Archipelago address, slot name, and password into the fields.
+5. Press Connect.
+6. Enjoy!
+
+To continue an earlier game, you can perform the exact same steps as above. You
+do not have to re-select Archipelago in the level selection screen if you were
+using Archipelago the last time you launched the game.
+
+In order to play the base game again, simply return to the level selection
+screen and choose Level 1 (or whatever else you want to play). The randomizer
+will not affect gameplay unless you launch it by starting a new game while it is
+selected in the level selection screen, so it is safe to play the game normally
+while the client is installed.
+
+**Note**: Running the randomizer modifies the game's memory. If you want to play
+the base game after playing the randomizer, you need to restart Lingo first.
diff --git a/worlds/lingo/items.py b/worlds/lingo/items.py
new file mode 100644
index 000000000000..78b288e7c2df
--- /dev/null
+++ b/worlds/lingo/items.py
@@ -0,0 +1,107 @@
+from enum import Enum
+from typing import Dict, List, NamedTuple, Set
+
+from BaseClasses import Item, ItemClassification
+from .static_logic import DOORS_BY_ROOM, PROGRESSIVE_ITEMS, get_door_group_item_id, get_door_item_id, \
+ get_progressive_item_id, get_special_item_id, PANEL_DOORS_BY_ROOM, get_panel_door_item_id, get_panel_group_item_id
+
+
+class ItemType(Enum):
+ NORMAL = 1
+ COLOR = 2
+
+
+class ItemData(NamedTuple):
+ """
+ ItemData for an item in Lingo
+ """
+ code: int
+ classification: ItemClassification
+ type: ItemType
+ has_doors: bool
+ painting_ids: List[str]
+
+
+class LingoItem(Item):
+ """
+ Item from the game Lingo
+ """
+ game: str = "Lingo"
+
+
+ALL_ITEM_TABLE: Dict[str, ItemData] = {}
+ITEMS_BY_GROUP: Dict[str, List[str]] = {}
+
+TRAP_ITEMS: List[str] = ["Slowness Trap", "Iceland Trap", "Atbash Trap"]
+
+
+def load_item_data():
+ global ALL_ITEM_TABLE, ITEMS_BY_GROUP
+
+ for color in ["Black", "Red", "Blue", "Yellow", "Green", "Orange", "Gray", "Brown", "Purple"]:
+ ALL_ITEM_TABLE[color] = ItemData(get_special_item_id(color), ItemClassification.progression,
+ ItemType.COLOR, False, [])
+ ITEMS_BY_GROUP.setdefault("Colors", []).append(color)
+
+ door_groups: Set[str] = set()
+ for room_name, doors in DOORS_BY_ROOM.items():
+ for door_name, door in doors.items():
+ if door.skip_item is True or door.event is True:
+ continue
+
+ if door.door_group is not None:
+ door_groups.add(door.door_group)
+
+ ALL_ITEM_TABLE[door.item_name] = \
+ ItemData(get_door_item_id(room_name, door_name), ItemClassification.progression, ItemType.NORMAL,
+ door.has_doors, door.painting_ids)
+ ITEMS_BY_GROUP.setdefault("Doors", []).append(door.item_name)
+
+ if door.item_group is not None:
+ ITEMS_BY_GROUP.setdefault(door.item_group, []).append(door.item_name)
+
+ for group in door_groups:
+ ALL_ITEM_TABLE[group] = ItemData(get_door_group_item_id(group),
+ ItemClassification.progression, ItemType.NORMAL, True, [])
+ ITEMS_BY_GROUP.setdefault("Doors", []).append(group)
+
+ panel_groups: Set[str] = set()
+ for room_name, panel_doors in PANEL_DOORS_BY_ROOM.items():
+ for panel_door_name, panel_door in panel_doors.items():
+ if panel_door.panel_group is not None:
+ panel_groups.add(panel_door.panel_group)
+
+ ALL_ITEM_TABLE[panel_door.item_name] = ItemData(get_panel_door_item_id(room_name, panel_door_name),
+ ItemClassification.progression, ItemType.NORMAL, False, [])
+ ITEMS_BY_GROUP.setdefault("Panels", []).append(panel_door.item_name)
+
+ for group in panel_groups:
+ ALL_ITEM_TABLE[group] = ItemData(get_panel_group_item_id(group), ItemClassification.progression,
+ ItemType.NORMAL, False, [])
+ ITEMS_BY_GROUP.setdefault("Panels", []).append(group)
+
+ special_items: Dict[str, ItemClassification] = {
+ ":)": ItemClassification.filler,
+ "The Feeling of Being Lost": ItemClassification.filler,
+ "Wanderlust": ItemClassification.filler,
+ "Empty White Hallways": ItemClassification.filler,
+ **{trap_name: ItemClassification.trap for trap_name in TRAP_ITEMS},
+ "Puzzle Skip": ItemClassification.useful,
+ }
+
+ for item_name, classification in special_items.items():
+ ALL_ITEM_TABLE[item_name] = ItemData(get_special_item_id(item_name), classification,
+ ItemType.NORMAL, False, [])
+
+ if classification == ItemClassification.filler:
+ ITEMS_BY_GROUP.setdefault("Junk", []).append(item_name)
+ elif classification == ItemClassification.trap:
+ ITEMS_BY_GROUP.setdefault("Traps", []).append(item_name)
+
+ for item_name in PROGRESSIVE_ITEMS:
+ ALL_ITEM_TABLE[item_name] = ItemData(get_progressive_item_id(item_name),
+ ItemClassification.progression, ItemType.NORMAL, False, [])
+
+
+# Initialize the item data at module scope.
+load_item_data()
diff --git a/worlds/lingo/locations.py b/worlds/lingo/locations.py
new file mode 100644
index 000000000000..c527e522fb06
--- /dev/null
+++ b/worlds/lingo/locations.py
@@ -0,0 +1,81 @@
+from enum import Flag, auto
+from typing import Dict, List, NamedTuple
+
+from BaseClasses import Location
+from .datatypes import RoomAndPanel
+from .static_logic import DOORS_BY_ROOM, PANELS_BY_ROOM, get_door_location_id, get_panel_location_id
+
+
+class LocationClassification(Flag):
+ normal = auto()
+ reduced = auto()
+ insanity = auto()
+ small_sphere_one = auto()
+
+
+class LocationData(NamedTuple):
+ """
+ LocationData for a location in Lingo
+ """
+ code: int
+ room: str
+ panels: List[RoomAndPanel]
+ classification: LocationClassification
+
+
+class LingoLocation(Location):
+ """
+ Location from the game Lingo
+ """
+ game: str = "Lingo"
+
+
+ALL_LOCATION_TABLE: Dict[str, LocationData] = {}
+LOCATIONS_BY_GROUP: Dict[str, List[str]] = {}
+
+
+def load_location_data():
+ global ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP
+
+ for room_name, panels in PANELS_BY_ROOM.items():
+ for panel_name, panel in panels.items():
+ location_name = f"{room_name} - {panel_name}" if panel.location_name is None else panel.location_name
+
+ classification = LocationClassification.insanity
+ if panel.check:
+ classification |= LocationClassification.normal
+
+ if not panel.exclude_reduce:
+ classification |= LocationClassification.reduced
+
+ if room_name == "Starting Room":
+ classification |= LocationClassification.small_sphere_one
+
+ ALL_LOCATION_TABLE[location_name] = \
+ LocationData(get_panel_location_id(room_name, panel_name), room_name,
+ [RoomAndPanel(None, panel_name)], classification)
+
+ if panel.achievement:
+ LOCATIONS_BY_GROUP.setdefault("Achievements", []).append(location_name)
+
+ for room_name, doors in DOORS_BY_ROOM.items():
+ for door_name, door in doors.items():
+ if door.skip_location or door.event or not door.panels:
+ continue
+
+ location_name = door.location_name
+ classification = LocationClassification.normal
+ if door.include_reduce:
+ classification |= LocationClassification.reduced
+
+ if location_name in ALL_LOCATION_TABLE:
+ new_id = ALL_LOCATION_TABLE[location_name].code
+ classification |= ALL_LOCATION_TABLE[location_name].classification
+ else:
+ new_id = get_door_location_id(room_name, door_name)
+
+ ALL_LOCATION_TABLE[location_name] = LocationData(new_id, room_name, door.panels, classification)
+
+
+# Initialize location data on the module scope.
+load_location_data()
diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py
new file mode 100644
index 000000000000..2fd57ff5ede3
--- /dev/null
+++ b/worlds/lingo/options.py
@@ -0,0 +1,287 @@
+from dataclasses import dataclass
+
+from schema import And, Schema
+
+from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool, OptionDict, \
+ OptionGroup
+from .items import TRAP_ITEMS
+
+
+class ShuffleDoors(Choice):
+ """This option specifies how doors open.
+
+ - **None:** Doors in the game will open the way they do in vanilla.
+ - **Panels:** Doors still open as in vanilla, but the panels that open the
+ doors will be locked, and an item will be required to unlock the panels.
+ - **Doors:** the doors themselves are locked behind items, and will open
+ automatically without needing to solve a panel once the key is obtained.
+ """
+ display_name = "Shuffle Doors"
+ option_none = 0
+ option_panels = 1
+ option_doors = 2
+ alias_simple = 2
+ alias_complex = 2
+
+
+class GroupDoors(Toggle):
+ """By default, door shuffle in either panels or doors mode will create individual keys for every panel or door to be locked.
+
+ When group doors is on, some panels and doors are sorted into logical groups, which are opened together by receiving an item."""
+ display_name = "Group Doors"
+
+
+class ProgressiveOrangeTower(DefaultOnToggle):
+ """When "Shuffle Doors" is on doors mode, this setting governs the manner in which the Orange Tower floors open up.
+
+ - **Off:** There is an item for each floor of the tower, and each floor's
+ item is the only one needed to access that floor.
+ - **On:** There are six progressive items, which open up the tower from the
+ bottom floor upward.
+ """
+ display_name = "Progressive Orange Tower"
+
+
+class ProgressiveColorful(DefaultOnToggle):
+ """When "Shuffle Doors" is on either panels or doors mode and "Group Doors" is off, this setting governs the manner in which The Colorful opens up.
+
+ - **Off:** There is an item for each room of The Colorful, meaning that
+ random rooms in the middle of the sequence can open up without giving you
+ access to them.
+ - **On:** There are ten progressive items, which open up the sequence from
+ White forward.
+ """
+ display_name = "Progressive Colorful"
+
+
+class LocationChecks(Choice):
+ """Determines what locations are available.
+
+ - **Normal:** There will be a location check for each panel set that would
+ ordinarily open a door, as well as for achievement panels and a small
+ handful of other panels.
+ - **Reduced:** Many of the locations that are associated with opening doors
+ are removed.
+ - **Insanity:** Every individual panel in the game is a location check.
+ """
+ display_name = "Location Checks"
+ option_normal = 0
+ option_reduced = 1
+ option_insanity = 2
+
+
+class ShuffleColors(DefaultOnToggle):
+ """If on, an item is added to the pool for every puzzle color (besides White).
+
+ You will need to unlock the requisite colors in order to be able to solve
+ puzzles of that color.
+ """
+ display_name = "Shuffle Colors"
+
+
+class ShufflePanels(Choice):
+ """If on, the puzzles on each panel are randomized.
+
+ On "rearrange", the puzzles are the same as the ones in the base game, but
+ are placed in different areas.
+ """
+ display_name = "Shuffle Panels"
+ option_none = 0
+ option_rearrange = 1
+
+
+class ShufflePaintings(Toggle):
+ """If on, the destination, location, and appearance of the painting warps in the game will be randomized."""
+ display_name = "Shuffle Paintings"
+
+
+class EnablePilgrimage(Toggle):
+ """Determines how the pilgrimage works.
+
+ - **On:** You are required to complete a pilgrimage in order to access the
+ Pilgrim Antechamber.
+ - **Off:** The pilgrimage will be deactivated, and the sun painting will be
+ added to the pool, even if door shuffle is off.
+ """
+ display_name = "Enable Pilgrimage"
+
+
+class PilgrimageAllowsRoofAccess(DefaultOnToggle):
+ """If on, you may use the Crossroads roof access during a pilgrimage (and you may be expected to do so).
+
+ Otherwise, pilgrimage will be deactivated when going up the stairs.
+ """
+ display_name = "Allow Roof Access for Pilgrimage"
+
+
+class PilgrimageAllowsPaintings(DefaultOnToggle):
+ """If on, you may use paintings during a pilgrimage (and you may be expected to do so).
+
+ Otherwise, pilgrimage will be deactivated when going through a painting.
+ """
+ display_name = "Allow Paintings for Pilgrimage"
+
+
+class SunwarpAccess(Choice):
+ """Determines how access to sunwarps works.
+
+ - **Normal:** All sunwarps are enabled from the start.
+ - **Disabled:** All sunwarps are disabled. Pilgrimage must be disabled when
+ this is used.
+ - **Unlock:** Sunwarps start off disabled, and all six activate once you
+ receive an item.
+ - **Individual:** Sunwarps start off disabled, and each has a corresponding
+ item that unlocks it.
+ - **Progressive:** Sunwarps start off disabled, and they unlock in order
+ using a progressive item.
+ """
+ display_name = "Sunwarp Access"
+ option_normal = 0
+ option_disabled = 1
+ option_unlock = 2
+ option_individual = 3
+ option_progressive = 4
+
+
+class ShuffleSunwarps(Toggle):
+ """If on, the pairing and ordering of the sunwarps in the game will be randomized."""
+ display_name = "Shuffle Sunwarps"
+
+
+class VictoryCondition(Choice):
+ """Change the victory condition.
+
+ - **The End:** the goal is to solve THE END at the top of the tower.
+ - **The Master:** The goal is to solve THE MASTER at the top of the tower,
+ after getting the number of achievements specified in the Mastery
+ Achievements option.
+ - **Level 2:** The goal is to solve LEVEL 2 in the second room, after
+ solving the number of panels specified in the Level 2 Requirement option.
+ - **Pilgrimage:** The goal is to solve PILGRIM in the Pilgrim Antechamber,
+ typically after performing a Pilgrimage.
+ """
+ display_name = "Victory Condition"
+ option_the_end = 0
+ option_the_master = 1
+ option_level_2 = 2
+ option_pilgrimage = 3
+
+
+class MasteryAchievements(Range):
+ """The number of achievements required to unlock THE MASTER.
+
+ - In the base game, 21 achievements are needed.
+ - If you include The Scientific and The Unchallenged, which are in the base
+ game but are not counted for mastery, 23 would be required.
+ - If you include the custom achievement (The Wanderer), 24 would be
+ required.
+ """
+ display_name = "Mastery Achievements"
+ range_start = 1
+ range_end = 24
+ default = 21
+
+
+class Level2Requirement(Range):
+ """The number of panel solves required to unlock LEVEL 2.
+
+ In the base game, 223 are needed. Note that this count includes ANOTHER TRY.
+ When set to 1, the panel hunt is disabled, and you can access LEVEL 2 for
+ free.
+ """
+ display_name = "Level 2 Requirement"
+ range_start = 1
+ range_end = 800
+ default = 223
+
+
+class EarlyColorHallways(Toggle):
+ """When on, a painting warp to the color hallways area will appear in the starting room.
+
+ This lets you avoid being trapped in the starting room for long periods of
+ time when door shuffle is on.
+ """
+ display_name = "Early Color Hallways"
+
+
+class ShufflePostgame(Toggle):
+ """When off, locations that could not be reached without also reaching your victory condition are removed."""
+ display_name = "Shuffle Postgame"
+
+
+class TrapPercentage(Range):
+ """Replaces junk items with traps, at the specified rate."""
+ display_name = "Trap Percentage"
+ range_start = 0
+ range_end = 100
+ default = 20
+
+
+class TrapWeights(OptionDict):
+ """Specify the distribution of traps that should be placed into the pool.
+
+ If you don't want a specific type of trap, set the weight to zero.
+ """
+ display_name = "Trap Weights"
+ schema = Schema({trap_name: And(int, lambda n: n >= 0) for trap_name in TRAP_ITEMS})
+ default = {trap_name: 1 for trap_name in TRAP_ITEMS}
+
+
+class PuzzleSkipPercentage(Range):
+ """Replaces junk items with puzzle skips, at the specified rate."""
+ display_name = "Puzzle Skip Percentage"
+ range_start = 0
+ range_end = 100
+ default = 20
+
+
+class DeathLink(Toggle):
+ """If on: Whenever another player on death link dies, you will be returned to the starting room."""
+ display_name = "Death Link"
+
+
+lingo_option_groups = [
+ OptionGroup("Pilgrimage", [
+ EnablePilgrimage,
+ PilgrimageAllowsRoofAccess,
+ PilgrimageAllowsPaintings,
+ SunwarpAccess,
+ ShuffleSunwarps,
+ ]),
+ OptionGroup("Fine-tuning", [
+ ProgressiveOrangeTower,
+ ProgressiveColorful,
+ MasteryAchievements,
+ Level2Requirement,
+ TrapPercentage,
+ TrapWeights,
+ PuzzleSkipPercentage,
+ ])
+]
+
+
+@dataclass
+class LingoOptions(PerGameCommonOptions):
+ shuffle_doors: ShuffleDoors
+ group_doors: GroupDoors
+ progressive_orange_tower: ProgressiveOrangeTower
+ progressive_colorful: ProgressiveColorful
+ location_checks: LocationChecks
+ shuffle_colors: ShuffleColors
+ shuffle_panels: ShufflePanels
+ shuffle_paintings: ShufflePaintings
+ enable_pilgrimage: EnablePilgrimage
+ pilgrimage_allows_roof_access: PilgrimageAllowsRoofAccess
+ pilgrimage_allows_paintings: PilgrimageAllowsPaintings
+ sunwarp_access: SunwarpAccess
+ shuffle_sunwarps: ShuffleSunwarps
+ victory_condition: VictoryCondition
+ mastery_achievements: MasteryAchievements
+ level_2_requirement: Level2Requirement
+ early_color_hallways: EarlyColorHallways
+ shuffle_postgame: ShufflePostgame
+ trap_percentage: TrapPercentage
+ trap_weights: TrapWeights
+ puzzle_skip_percentage: PuzzleSkipPercentage
+ death_link: DeathLink
+ start_inventory_from_pool: StartInventoryPool
diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py
new file mode 100644
index 000000000000..b21735c1f533
--- /dev/null
+++ b/worlds/lingo/player_logic.py
@@ -0,0 +1,611 @@
+from enum import Enum
+from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING
+
+from Options import OptionError
+from .datatypes import Door, DoorType, Painting, RoomAndDoor, RoomAndPanel
+from .items import ALL_ITEM_TABLE, ItemType
+from .locations import ALL_LOCATION_TABLE, LocationClassification
+from .options import LocationChecks, ShuffleDoors, SunwarpAccess, VictoryCondition
+from .static_logic import DOORS_BY_ROOM, PAINTINGS, PAINTING_ENTRANCES, PAINTING_EXITS, \
+ PANELS_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, PROGRESSIVE_DOORS_BY_ROOM, \
+ PANEL_DOORS_BY_ROOM, PROGRESSIVE_PANELS_BY_ROOM, SUNWARP_ENTRANCES, SUNWARP_EXITS
+
+if TYPE_CHECKING:
+ from . import LingoWorld
+
+
+class AccessRequirements:
+ rooms: Set[str]
+ doors: Set[RoomAndDoor]
+ colors: Set[str]
+ items: Set[str]
+ progression: Dict[str, int]
+ the_master: bool
+ postgame: bool
+
+ def __init__(self):
+ self.rooms = set()
+ self.doors = set()
+ self.colors = set()
+ self.items = set()
+ self.progression = dict()
+ self.the_master = False
+ self.postgame = False
+
+ def merge(self, other: "AccessRequirements"):
+ self.rooms |= other.rooms
+ self.doors |= other.doors
+ self.colors |= other.colors
+ self.items |= other.items
+ self.the_master |= other.the_master
+ self.postgame |= other.postgame
+
+ for progression, index in other.progression.items():
+ if progression not in self.progression or index > self.progression[progression]:
+ self.progression[progression] = index
+
+ def __str__(self):
+ return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors}, items={self.items}," \
+ f" progression={self.progression}), the_master={self.the_master}, postgame={self.postgame}"
+
+
+class PlayerLocation(NamedTuple):
+ name: str
+ code: Optional[int]
+ access: AccessRequirements
+
+
+class ProgressiveItemBehavior(Enum):
+ DISABLE = 1
+ SPLIT = 2
+ PROGRESSIVE = 3
+
+
+def should_split_progression(progression_name: str, world: "LingoWorld") -> ProgressiveItemBehavior:
+ if progression_name == "Progressive Orange Tower":
+ if world.options.progressive_orange_tower:
+ return ProgressiveItemBehavior.PROGRESSIVE
+ else:
+ return ProgressiveItemBehavior.SPLIT
+ elif progression_name == "Progressive Colorful":
+ if world.options.progressive_colorful:
+ return ProgressiveItemBehavior.PROGRESSIVE
+ else:
+ return ProgressiveItemBehavior.SPLIT
+
+ return ProgressiveItemBehavior.PROGRESSIVE
+
+
+class LingoPlayerLogic:
+ """
+ Defines logic after a player's options have been applied
+ """
+
+ item_by_door: Dict[str, Dict[str, str]]
+
+ locations_by_room: Dict[str, List[PlayerLocation]]
+ real_locations: List[str]
+
+ event_loc_to_item: Dict[str, str]
+ real_items: List[str]
+
+ victory_condition: str
+ mastery_location: str
+ level_2_location: str
+
+ painting_mapping: Dict[str, str]
+
+ forced_good_item: str
+
+ panel_reqs: Dict[str, Dict[str, AccessRequirements]]
+ door_reqs: Dict[str, Dict[str, AccessRequirements]]
+ mastery_reqs: List[AccessRequirements]
+ counting_panel_reqs: Dict[str, List[Tuple[AccessRequirements, int]]]
+
+ sunwarp_mapping: List[int]
+ sunwarp_entrances: List[str]
+ sunwarp_exits: List[str]
+
+ def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel], world: "LingoWorld"):
+ """
+ Creates a location. This function determines the access requirements for the location by combining and
+ flattening the requirements for each of the given panels.
+ """
+ access_reqs = AccessRequirements()
+ for panel in panels:
+ if panel.room is not None and panel.room != room:
+ access_reqs.rooms.add(panel.room)
+
+ panel_room = room if panel.room is None else panel.room
+ sub_access_reqs = self.calculate_panel_requirements(panel_room, panel.panel, world)
+ access_reqs.merge(sub_access_reqs)
+
+ self.locations_by_room.setdefault(room, []).append(PlayerLocation(name, code, access_reqs))
+
+ def set_door_item(self, room: str, door: str, item: str):
+ self.item_by_door.setdefault(room, {})[door] = item
+
+ def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"):
+ if room_name in PROGRESSIVE_DOORS_BY_ROOM and door_data.name in PROGRESSIVE_DOORS_BY_ROOM[room_name]:
+ progression_name = PROGRESSIVE_DOORS_BY_ROOM[room_name][door_data.name].item_name
+ progression_handling = should_split_progression(progression_name, world)
+
+ if progression_handling == ProgressiveItemBehavior.SPLIT:
+ self.set_door_item(room_name, door_data.name, door_data.item_name)
+ self.real_items.append(door_data.item_name)
+ elif progression_handling == ProgressiveItemBehavior.PROGRESSIVE:
+ progressive_item_name = PROGRESSIVE_DOORS_BY_ROOM[room_name][door_data.name].item_name
+ self.set_door_item(room_name, door_data.name, progressive_item_name)
+ self.real_items.append(progressive_item_name)
+ else:
+ self.set_door_item(room_name, door_data.name, door_data.item_name)
+ self.real_items.append(door_data.item_name)
+
+ def __init__(self, world: "LingoWorld"):
+ self.item_by_door = {}
+ self.locations_by_room = {}
+ self.real_locations = []
+ self.event_loc_to_item = {}
+ self.real_items = []
+ self.victory_condition = ""
+ self.mastery_location = ""
+ self.level_2_location = ""
+ self.painting_mapping = {}
+ self.forced_good_item = ""
+ self.panel_reqs = {}
+ self.door_reqs = {}
+ self.mastery_reqs = []
+ self.counting_panel_reqs = {}
+ self.sunwarp_mapping = []
+
+ door_shuffle = world.options.shuffle_doors
+ color_shuffle = world.options.shuffle_colors
+ painting_shuffle = world.options.shuffle_paintings
+ location_checks = world.options.location_checks
+ victory_condition = world.options.victory_condition
+ early_color_hallways = world.options.early_color_hallways
+
+ if location_checks == LocationChecks.option_reduced:
+ if door_shuffle == ShuffleDoors.option_doors:
+ raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks when door shuffle"
+ f" is on, because there would not be enough locations for all of the door items.")
+ if door_shuffle == ShuffleDoors.option_panels:
+ if not world.options.group_doors:
+ raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks when ungrouped"
+ f" panels mode door shuffle is on, because there would not be enough locations for"
+ f" all of the panel items.")
+ if color_shuffle:
+ raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks with both"
+ f" panels mode door shuffle and color shuffle because there would not be enough"
+ f" locations for all of the items.")
+ if world.options.sunwarp_access >= SunwarpAccess.option_individual:
+ raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks with both"
+ f" panels mode door shuffle and individual or progressive sunwarp access because"
+ f" there would not be enough locations for all of the items.")
+
+ # Create door items, where needed.
+ door_groups: Set[str] = set()
+ for room_name, room_data in DOORS_BY_ROOM.items():
+ for door_name, door_data in room_data.items():
+ if door_data.skip_item is False and door_data.event is False:
+ if door_data.type == DoorType.NORMAL and door_shuffle == ShuffleDoors.option_doors:
+ if door_data.door_group is not None and world.options.group_doors:
+ # Grouped doors are handled differently if shuffle doors is on simple.
+ self.set_door_item(room_name, door_name, door_data.door_group)
+ door_groups.add(door_data.door_group)
+ else:
+ self.handle_non_grouped_door(room_name, door_data, world)
+ elif door_data.type == DoorType.SUNWARP:
+ if world.options.sunwarp_access == SunwarpAccess.option_unlock:
+ self.set_door_item(room_name, door_name, "Sunwarps")
+ door_groups.add("Sunwarps")
+ elif world.options.sunwarp_access == SunwarpAccess.option_individual:
+ self.set_door_item(room_name, door_name, door_data.item_name)
+ self.real_items.append(door_data.item_name)
+ elif world.options.sunwarp_access == SunwarpAccess.option_progressive:
+ self.set_door_item(room_name, door_name, "Progressive Pilgrimage")
+ self.real_items.append("Progressive Pilgrimage")
+ elif door_data.type == DoorType.SUN_PAINTING:
+ if not world.options.enable_pilgrimage:
+ self.set_door_item(room_name, door_name, door_data.item_name)
+ self.real_items.append(door_data.item_name)
+
+ self.real_items += door_groups
+
+ # Create panel items, where needed.
+ if world.options.shuffle_doors == ShuffleDoors.option_panels:
+ panel_groups: Set[str] = set()
+
+ for room_name, room_data in PANEL_DOORS_BY_ROOM.items():
+ for panel_door_name, panel_door_data in room_data.items():
+ if panel_door_data.panel_group is not None and world.options.group_doors:
+ panel_groups.add(panel_door_data.panel_group)
+ elif room_name in PROGRESSIVE_PANELS_BY_ROOM \
+ and panel_door_name in PROGRESSIVE_PANELS_BY_ROOM[room_name]:
+ progression_obj = PROGRESSIVE_PANELS_BY_ROOM[room_name][panel_door_name]
+ progression_handling = should_split_progression(progression_obj.item_name, world)
+
+ if progression_handling == ProgressiveItemBehavior.SPLIT:
+ self.real_items.append(panel_door_data.item_name)
+ elif progression_handling == ProgressiveItemBehavior.PROGRESSIVE:
+ self.real_items.append(progression_obj.item_name)
+ else:
+ self.real_items.append(panel_door_data.item_name)
+
+ self.real_items += panel_groups
+
+ # Create color items, if needed.
+ if color_shuffle:
+ self.real_items += [name for name, item in ALL_ITEM_TABLE.items() if item.type == ItemType.COLOR]
+
+ # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need
+ # to prevent the actual victory condition from becoming a check.
+ self.mastery_location = "Orange Tower Seventh Floor - THE MASTER"
+ self.level_2_location = "Second Room - LEVEL 2"
+
+ if victory_condition == VictoryCondition.option_the_end:
+ self.victory_condition = "Orange Tower Seventh Floor - THE END"
+ self.add_location("Ending Area", "The End (Solved)", None, [], world)
+ self.event_loc_to_item["The End (Solved)"] = "Victory"
+ elif victory_condition == VictoryCondition.option_the_master:
+ self.victory_condition = "Orange Tower Seventh Floor - THE MASTER"
+ self.mastery_location = "Orange Tower Seventh Floor - Mastery Achievements"
+
+ self.add_location("Orange Tower Seventh Floor", self.mastery_location, None, [], world)
+ self.event_loc_to_item[self.mastery_location] = "Victory"
+ elif victory_condition == VictoryCondition.option_level_2:
+ self.victory_condition = "Second Room - LEVEL 2"
+ self.level_2_location = "Second Room - Unlock Level 2"
+
+ self.add_location("Second Room", self.level_2_location, None, [RoomAndPanel("Second Room", "LEVEL 2")],
+ world)
+ self.event_loc_to_item[self.level_2_location] = "Victory"
+
+ if world.options.level_2_requirement == 1:
+ raise OptionError("The Level 2 requirement must be at least 2 when LEVEL 2 is the victory condition.")
+ elif victory_condition == VictoryCondition.option_pilgrimage:
+ self.victory_condition = "Pilgrim Antechamber - PILGRIM"
+ self.add_location("Pilgrim Antechamber", "PILGRIM (Solved)", None,
+ [RoomAndPanel("Pilgrim Antechamber", "PILGRIM")], world)
+ self.event_loc_to_item["PILGRIM (Solved)"] = "Victory"
+
+ # Create events for each achievement panel, so that we can determine when THE MASTER is accessible.
+ for room_name, room_data in PANELS_BY_ROOM.items():
+ for panel_name, panel_data in room_data.items():
+ if panel_data.achievement:
+ access_req = AccessRequirements()
+ access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world))
+ access_req.rooms.add(room_name)
+
+ self.mastery_reqs.append(access_req)
+
+ # Create groups of counting panel access requirements for the LEVEL 2 check.
+ self.create_panel_hunt_events(world)
+
+ # Instantiate all real locations.
+ location_classification = LocationClassification.normal
+ if location_checks == LocationChecks.option_reduced:
+ location_classification = LocationClassification.reduced
+ elif location_checks == LocationChecks.option_insanity:
+ location_classification = LocationClassification.insanity
+
+ if door_shuffle == ShuffleDoors.option_doors and not early_color_hallways:
+ location_classification |= LocationClassification.small_sphere_one
+
+ for location_name, location_data in ALL_LOCATION_TABLE.items():
+ if location_name != self.victory_condition:
+ if not (location_classification & location_data.classification):
+ continue
+
+ self.add_location(location_data.room, location_name, location_data.code, location_data.panels, world)
+ self.real_locations.append(location_name)
+
+ if world.options.enable_pilgrimage and world.options.sunwarp_access == SunwarpAccess.option_disabled:
+ raise OptionError("Sunwarps cannot be disabled when pilgrimage is enabled.")
+
+ if world.options.shuffle_sunwarps:
+ if world.options.sunwarp_access == SunwarpAccess.option_disabled:
+ raise OptionError("Sunwarps cannot be shuffled if they are disabled.")
+
+ self.sunwarp_mapping = list(range(0, 12))
+ world.random.shuffle(self.sunwarp_mapping)
+
+ sunwarp_rooms = SUNWARP_ENTRANCES + SUNWARP_EXITS
+ self.sunwarp_entrances = [sunwarp_rooms[i] for i in self.sunwarp_mapping[0:6]]
+ self.sunwarp_exits = [sunwarp_rooms[i] for i in self.sunwarp_mapping[6:12]]
+ else:
+ self.sunwarp_entrances = SUNWARP_ENTRANCES
+ self.sunwarp_exits = SUNWARP_EXITS
+
+ # Create the paintings mapping, if painting shuffle is on.
+ if painting_shuffle:
+ # Shuffle paintings until we get something workable.
+ workable_paintings = False
+ for i in range(0, 20):
+ workable_paintings = self.randomize_paintings(world)
+ if workable_paintings:
+ break
+
+ if not workable_paintings:
+ raise Exception("This Lingo world was unable to generate a workable painting mapping after 20 "
+ "iterations. This is very unlikely to happen on its own, and probably indicates some "
+ "kind of logic error.")
+
+ if door_shuffle == ShuffleDoors.option_doors and location_checks != LocationChecks.option_insanity \
+ and not early_color_hallways and world.multiworld.players > 1:
+ # Under the combination of door shuffle, normal location checks, and no early color hallways, sphere 1 is
+ # only three checks. In a multiplayer situation, this can be frustrating for the player because they are
+ # more likely to be stuck in the starting room for a long time. To remedy this, we will force a useful item
+ # onto the GOOD LUCK check under these circumstances. The goal is to expand sphere 1 to at least four
+ # checks (and likely more than that).
+ #
+ # Note: A very low LEVEL 2 requirement would naturally expand sphere 1 to four checks, but this is a very
+ # uncommon configuration, so we will ignore it and force a good item anyway.
+
+ # Starting Room - Back Right Door gives access to OPEN and DEAD END.
+ # Starting Room - Exit Door gives access to OPEN and TRACE.
+ good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"]
+
+ if not color_shuffle:
+ if not world.options.enable_pilgrimage:
+ # HOT CRUST and THIS.
+ good_item_options.append("Pilgrim Room - Sun Painting")
+
+ if world.options.group_doors:
+ # WELCOME BACK, CLOCKWISE, and DRAWL + RUNS.
+ good_item_options.append("Welcome Back Doors")
+ else:
+ # WELCOME BACK and CLOCKWISE.
+ good_item_options.append("Welcome Back Area - Shortcut to Starting Room")
+
+ if world.options.group_doors:
+ # Color hallways access (NOTE: reconsider when sunwarp shuffling exists).
+ good_item_options.append("Rhyme Room Doors")
+
+ # When painting shuffle is off, most Starting Room paintings give color hallways access. The Wondrous's
+ # painting does not, but it gives access to SHRINK and WELCOME BACK.
+ for painting_obj in PAINTINGS.values():
+ if not painting_obj.enter_only or painting_obj.required_door is None\
+ or painting_obj.room != "Starting Room":
+ continue
+
+ # If painting shuffle is on, we only want to consider paintings that actually go somewhere.
+ #
+ # NOTE: This does not guarantee that there will be any checks on the other side.
+ if painting_shuffle and painting_obj.id not in self.painting_mapping.keys():
+ continue
+
+ pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door]
+ good_item_options.append(pdoor.item_name)
+
+ # Copied from The Witness -- remove any plandoed items from the possible good items set.
+ for v in world.multiworld.plando_items[world.player]:
+ if v.get("from_pool", True):
+ for item_key in {"item", "items"}:
+ if item_key in v:
+ if type(v[item_key]) is str:
+ if v[item_key] in good_item_options:
+ good_item_options.remove(v[item_key])
+ elif type(v[item_key]) is dict:
+ for item, weight in v[item_key].items():
+ if weight and item in good_item_options:
+ good_item_options.remove(item)
+ else:
+ # Other type of iterable
+ for item in v[item_key]:
+ if item in good_item_options:
+ good_item_options.remove(item)
+
+ if len(good_item_options) > 0:
+ self.forced_good_item = world.random.choice(good_item_options)
+ self.real_items.remove(self.forced_good_item)
+ self.real_locations.remove("Second Room - Good Luck")
+
+ def randomize_paintings(self, world: "LingoWorld") -> bool:
+ self.painting_mapping.clear()
+
+ # First, assign mappings to the required-exit paintings. We ensure that req-blocked paintings do not lead to
+ # required paintings.
+ req_exits = []
+ required_painting_rooms = REQUIRED_PAINTING_ROOMS
+ if world.options.shuffle_doors != ShuffleDoors.option_doors:
+ required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
+ req_exits = [painting_id for painting_id, painting in PAINTINGS.items() if painting.required_when_no_doors]
+
+ def is_req_enterable(painting_id: str, painting: Painting) -> bool:
+ if painting.exit_only or painting.disable or painting.req_blocked\
+ or painting.room in required_painting_rooms:
+ return False
+
+ if world.options.shuffle_doors == ShuffleDoors.option_none:
+ if painting.req_blocked_when_no_doors:
+ return False
+
+ # Special case for the paintings in Color Hunt and Champion's Rest. These are req blocked when not on
+ # doors mode, and when sunwarps are disabled or sunwarp shuffle is on and the Color Hunt sunwarp is not
+ # an exit. This is because these two rooms would then be inaccessible without roof access, and we can't
+ # hide the Owl Hallway entrance behind roof access.
+ if painting.room in ["Color Hunt", "Champion's Rest"]:
+ if world.options.sunwarp_access == SunwarpAccess.option_disabled\
+ or (world.options.shuffle_sunwarps and "Color Hunt" not in self.sunwarp_exits):
+ return False
+
+ return True
+
+ req_enterable = [painting_id for painting_id, painting in PAINTINGS.items()
+ if is_req_enterable(painting_id, painting)]
+ req_exits += [painting_id for painting_id, painting in PAINTINGS.items()
+ if painting.exit_only and painting.required]
+ req_entrances = world.random.sample(req_enterable, len(req_exits))
+
+ self.painting_mapping = dict(zip(req_entrances, req_exits))
+
+ # Next, determine the rest of the exit paintings.
+ exitable = [painting_id for painting_id, painting in PAINTINGS.items()
+ if not painting.enter_only and not painting.disable and painting_id not in req_exits and
+ painting_id not in req_entrances]
+ nonreq_exits = world.random.sample(exitable, PAINTING_EXITS - len(req_exits))
+ chosen_exits = req_exits + nonreq_exits
+
+ # Determine the rest of the entrance paintings.
+ enterable = [painting_id for painting_id, painting in PAINTINGS.items()
+ if not painting.exit_only and not painting.disable and painting_id not in chosen_exits and
+ painting_id not in req_entrances]
+ chosen_entrances = world.random.sample(enterable, PAINTING_ENTRANCES - len(req_entrances))
+
+ # Assign one entrance to each non-required exit, to ensure that the total number of exits is achieved.
+ for warp_exit in nonreq_exits:
+ warp_enter = world.random.choice(chosen_entrances)
+ chosen_entrances.remove(warp_enter)
+ self.painting_mapping[warp_enter] = warp_exit
+
+ # Assign each of the remaining entrances to any required or non-required exit.
+ for warp_enter in chosen_entrances:
+ warp_exit = world.random.choice(chosen_exits)
+ self.painting_mapping[warp_enter] = warp_exit
+
+ # The Eye Wall painting is unique in that it is both double-sided and also enter only (because it moves).
+ # There is only one eligible double-sided exit painting, which is the vanilla exit for this warp. If the
+ # exit painting is an entrance in the shuffle, we will disable the Eye Wall painting. Otherwise, Eye Wall
+ # is forced to point to the vanilla exit.
+ if "eye_painting_2" not in self.painting_mapping.keys():
+ self.painting_mapping["eye_painting"] = "eye_painting_2"
+
+ # Just for sanity's sake, ensure that all required painting rooms are accessed.
+ for painting_id, painting in PAINTINGS.items():
+ if painting_id not in self.painting_mapping.values() \
+ and (painting.required or (painting.required_when_no_doors and
+ world.options.shuffle_doors != ShuffleDoors.option_doors)):
+ return False
+
+ return True
+
+ def calculate_panel_requirements(self, room: str, panel: str, world: "LingoWorld"):
+ """
+ Calculate and return the access requirements for solving a given panel. The goal is to eliminate recursion in
+ the access rule function by collecting the rooms, doors, and colors needed by this panel and any panel required
+ by this panel. Memoization is used so that no panel is evaluated more than once.
+ """
+ if panel not in self.panel_reqs.setdefault(room, {}):
+ access_reqs = AccessRequirements()
+ panel_object = PANELS_BY_ROOM[room][panel]
+
+ if world.options.shuffle_doors == ShuffleDoors.option_panels and panel_object.panel_door is not None:
+ panel_door_room = panel_object.panel_door.room
+ panel_door_name = panel_object.panel_door.panel_door
+ panel_door = PANEL_DOORS_BY_ROOM[panel_door_room][panel_door_name]
+
+ if panel_door.panel_group is not None and world.options.group_doors:
+ access_reqs.items.add(panel_door.panel_group)
+ elif panel_door_room in PROGRESSIVE_PANELS_BY_ROOM\
+ and panel_door_name in PROGRESSIVE_PANELS_BY_ROOM[panel_door_room]:
+ progression_obj = PROGRESSIVE_PANELS_BY_ROOM[panel_door_room][panel_door_name]
+ progression_handling = should_split_progression(progression_obj.item_name, world)
+
+ if progression_handling == ProgressiveItemBehavior.SPLIT:
+ access_reqs.items.add(panel_door.item_name)
+ elif progression_handling == ProgressiveItemBehavior.PROGRESSIVE:
+ access_reqs.progression[progression_obj.item_name] = progression_obj.index
+ else:
+ access_reqs.items.add(panel_door.item_name)
+
+ for req_room in panel_object.required_rooms:
+ access_reqs.rooms.add(req_room)
+
+ for req_door in panel_object.required_doors:
+ door_object = DOORS_BY_ROOM[room if req_door.room is None else req_door.room][req_door.door]
+ if door_object.event or world.options.shuffle_doors != ShuffleDoors.option_doors:
+ sub_access_reqs = self.calculate_door_requirements(
+ room if req_door.room is None else req_door.room, req_door.door, world)
+ access_reqs.merge(sub_access_reqs)
+ else:
+ access_reqs.doors.add(RoomAndDoor(room if req_door.room is None else req_door.room, req_door.door))
+
+ for color in panel_object.colors:
+ access_reqs.colors.add(color)
+
+ for req_panel in panel_object.required_panels:
+ if req_panel.room is not None and req_panel.room != room:
+ access_reqs.rooms.add(req_panel.room)
+
+ sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room,
+ req_panel.panel, world)
+ access_reqs.merge(sub_access_reqs)
+
+ if panel == "THE MASTER":
+ access_reqs.the_master = True
+
+ # Evil python magic (so sayeth NewSoupVi): this checks victory_condition against the panel's location name
+ # override if it exists, or the auto-generated location name if it's None.
+ if self.victory_condition == (panel_object.location_name or f"{room} - {panel}"):
+ access_reqs.postgame = True
+
+ self.panel_reqs[room][panel] = access_reqs
+
+ return self.panel_reqs[room][panel]
+
+ def calculate_door_requirements(self, room: str, door: str, world: "LingoWorld"):
+ """
+ Similar to calculate_panel_requirements, but for event doors.
+ """
+ if door not in self.door_reqs.setdefault(room, {}):
+ access_reqs = AccessRequirements()
+ door_object = DOORS_BY_ROOM[room][door]
+
+ for req_panel in door_object.panels:
+ panel_room = room if req_panel.room is None else req_panel.room
+ access_reqs.rooms.add(panel_room)
+ sub_access_reqs = self.calculate_panel_requirements(panel_room, req_panel.panel, world)
+ access_reqs.merge(sub_access_reqs)
+
+ self.door_reqs[room][door] = access_reqs
+
+ return self.door_reqs[room][door]
+
+ def create_panel_hunt_events(self, world: "LingoWorld"):
+ """
+ Creates the event locations/items used for determining access to the LEVEL 2 panel. Instead of creating an event
+ for every single counting panel in the game, we try to coalesce panels with identical access rules into the same
+ event. Right now, this means the following:
+
+ When color shuffle is off, panels in a room with no extra access requirements (room, door, or other panel) are
+ all coalesced into one event.
+
+ When color shuffle is on, single-colored panels (including white) in a room are combined into one event per
+ color. Multicolored panels and panels with any extra access requirements are not coalesced, and will each
+ receive their own event.
+ """
+ for room_name, room_data in PANELS_BY_ROOM.items():
+ unhindered_panels_by_color: dict[Optional[str], int] = {}
+
+ for panel_name, panel_data in room_data.items():
+ # We won't count non-counting panels.
+ if panel_data.non_counting:
+ continue
+
+ # We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will
+ # only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. Panel door locked
+ # puzzles will be separate if panels mode is on. THE MASTER has special access rules and is handled
+ # separately.
+ if len(panel_data.required_panels) > 0 or len(panel_data.required_doors) > 0\
+ or len(panel_data.required_rooms) > 0\
+ or (world.options.shuffle_colors and len(panel_data.colors) > 1)\
+ or (world.options.shuffle_doors == ShuffleDoors.option_panels
+ and panel_data.panel_door is not None)\
+ or panel_name == "THE MASTER":
+ self.counting_panel_reqs.setdefault(room_name, []).append(
+ (self.calculate_panel_requirements(room_name, panel_name, world), 1))
+ else:
+ if len(panel_data.colors) == 0 or not world.options.shuffle_colors:
+ color = None
+ else:
+ color = panel_data.colors[0]
+
+ unhindered_panels_by_color[color] = unhindered_panels_by_color.get(color, 0) + 1
+
+ for color, panel_count in unhindered_panels_by_color.items():
+ access_reqs = AccessRequirements()
+ if color is not None:
+ access_reqs.colors.add(color)
+
+ self.counting_panel_reqs.setdefault(room_name, []).append((access_reqs, panel_count))
diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py
new file mode 100644
index 000000000000..9773f22d9196
--- /dev/null
+++ b/worlds/lingo/regions.py
@@ -0,0 +1,183 @@
+from typing import Dict, Optional, TYPE_CHECKING
+
+from BaseClasses import Entrance, ItemClassification, Region
+from .datatypes import EntranceType, Room, RoomAndDoor
+from .items import LingoItem
+from .locations import LingoLocation
+from .options import SunwarpAccess
+from .rules import lingo_can_do_pilgrimage, lingo_can_use_entrance, make_location_lambda
+from .static_logic import ALL_ROOMS, PAINTINGS
+
+if TYPE_CHECKING:
+ from . import LingoWorld
+
+
+def create_region(room: Room, world: "LingoWorld") -> Region:
+ new_region = Region(room.name, world.player, world.multiworld)
+ for location in world.player_logic.locations_by_room.get(room.name, {}):
+ new_location = LingoLocation(world.player, location.name, location.code, new_region)
+ new_location.access_rule = make_location_lambda(location, world)
+ new_region.locations.append(new_location)
+ if location.name in world.player_logic.event_loc_to_item:
+ event_name = world.player_logic.event_loc_to_item[location.name]
+ event_item = LingoItem(event_name, ItemClassification.progression, None, world.player)
+ new_location.place_locked_item(event_item)
+
+ return new_region
+
+
+def is_acceptable_pilgrimage_entrance(entrance_type: EntranceType, world: "LingoWorld") -> bool:
+ allowed_entrance_types = EntranceType.NORMAL
+
+ if world.options.pilgrimage_allows_paintings:
+ allowed_entrance_types |= EntranceType.PAINTING
+
+ if world.options.pilgrimage_allows_roof_access:
+ allowed_entrance_types |= EntranceType.CROSSROADS_ROOF_ACCESS
+
+ return bool(entrance_type & allowed_entrance_types)
+
+
+def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str,
+ door: Optional[RoomAndDoor], entrance_type: EntranceType, pilgrimage: bool, world: "LingoWorld"):
+ connection = Entrance(world.player, description, source_region)
+ connection.access_rule = lambda state: lingo_can_use_entrance(state, target_region.name, door, world)
+
+ source_region.exits.append(connection)
+ connection.connect(target_region)
+
+ if door is not None:
+ effective_room = target_region.name if door.room is None else door.room
+ if door.door not in world.player_logic.item_by_door.get(effective_room, {}):
+ access_reqs = world.player_logic.calculate_door_requirements(effective_room, door.door, world)
+ for region in access_reqs.rooms:
+ world.multiworld.register_indirect_condition(regions[region], connection)
+
+ # This pretty much only applies to Orange Tower Sixth Floor -> Orange Tower Basement.
+ if access_reqs.the_master:
+ for mastery_req in world.player_logic.mastery_reqs:
+ for region in mastery_req.rooms:
+ world.multiworld.register_indirect_condition(regions[region], connection)
+
+ if not pilgrimage and world.options.enable_pilgrimage and is_acceptable_pilgrimage_entrance(entrance_type, world)\
+ and source_region.name != "Menu":
+ for part in range(1, 6):
+ pilgrimage_descriptor = f" (Pilgrimage Part {part})"
+ pilgrim_source_region = regions[f"{source_region.name}{pilgrimage_descriptor}"]
+ pilgrim_target_region = regions[f"{target_region.name}{pilgrimage_descriptor}"]
+
+ effective_door = door
+ if effective_door is not None:
+ effective_room = target_region.name if door.room is None else door.room
+ effective_door = RoomAndDoor(effective_room, door.door)
+
+ connect_entrance(regions, pilgrim_source_region, pilgrim_target_region,
+ f"{description}{pilgrimage_descriptor}", effective_door, entrance_type, True, world)
+
+
+def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld") -> None:
+ source_painting = PAINTINGS[warp_enter]
+ target_painting = PAINTINGS[warp_exit]
+
+ target_region = regions[target_painting.room]
+ source_region = regions[source_painting.room]
+
+ entrance_name = f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)"
+ connect_entrance(regions, source_region, target_region, entrance_name, source_painting.required_door,
+ EntranceType.PAINTING, False, world)
+
+
+def create_regions(world: "LingoWorld") -> None:
+ regions = {
+ "Menu": Region("Menu", world.player, world.multiworld)
+ }
+
+ painting_shuffle = world.options.shuffle_paintings
+ early_color_hallways = world.options.early_color_hallways
+
+ # Instantiate all rooms as regions with their locations first.
+ for room in ALL_ROOMS:
+ regions[room.name] = create_region(room, world)
+
+ if world.options.enable_pilgrimage:
+ for part in range(1, 6):
+ pilgrimage_region_name = f"{room.name} (Pilgrimage Part {part})"
+ regions[pilgrimage_region_name] = Region(pilgrimage_region_name, world.player, world.multiworld)
+
+ # Connect all created regions now that they exist.
+ allowed_entrance_types = EntranceType.NORMAL | EntranceType.WARP | EntranceType.CROSSROADS_ROOF_ACCESS
+
+ if not painting_shuffle:
+ # Don't use the vanilla painting connections if we are shuffling paintings.
+ allowed_entrance_types |= EntranceType.PAINTING
+
+ if world.options.sunwarp_access != SunwarpAccess.option_disabled and not world.options.shuffle_sunwarps:
+ # Don't connect sunwarps if sunwarps are disabled or if we're shuffling sunwarps.
+ allowed_entrance_types |= EntranceType.SUNWARP
+
+ for room in ALL_ROOMS:
+ for entrance in room.entrances:
+ effective_entrance_type = entrance.type & allowed_entrance_types
+ if not effective_entrance_type:
+ continue
+
+ entrance_name = f"{entrance.room} to {room.name}"
+ if entrance.door is not None:
+ if entrance.door.room is not None:
+ entrance_name += f" (through {entrance.door.room} - {entrance.door.door})"
+ else:
+ entrance_name += f" (through {room.name} - {entrance.door.door})"
+
+ effective_door = entrance.door
+ if entrance.type == EntranceType.SUNWARP and world.options.sunwarp_access == SunwarpAccess.option_normal:
+ effective_door = None
+
+ connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, effective_door,
+ effective_entrance_type, False, world)
+
+ if world.options.enable_pilgrimage:
+ # Connect the start of the pilgrimage. We check for all sunwarp items here.
+ pilgrim_start_from = regions[world.player_logic.sunwarp_entrances[0]]
+ pilgrim_start_to = regions[f"{world.player_logic.sunwarp_exits[0]} (Pilgrimage Part 1)"]
+
+ if world.options.sunwarp_access >= SunwarpAccess.option_unlock:
+ pilgrim_start_from.connect(pilgrim_start_to, f"Pilgrimage Part 1",
+ lambda state: lingo_can_do_pilgrimage(state, world))
+ else:
+ pilgrim_start_from.connect(pilgrim_start_to, f"Pilgrimage Part 1")
+
+ # Create connections between each segment of the pilgrimage.
+ for i in range(1, 6):
+ from_room = f"{world.player_logic.sunwarp_entrances[i]} (Pilgrimage Part {i})"
+ to_room = f"{world.player_logic.sunwarp_exits[i]} (Pilgrimage Part {i+1})"
+ if i == 5:
+ to_room = "Pilgrim Antechamber"
+
+ regions[from_room].connect(regions[to_room], f"Pilgrimage Part {i+1}")
+ else:
+ connect_entrance(regions, regions["Starting Room"], regions["Pilgrim Antechamber"], "Sun Painting",
+ RoomAndDoor("Pilgrim Antechamber", "Sun Painting"), EntranceType.PAINTING, False, world)
+
+ if early_color_hallways:
+ connect_entrance(regions, regions["Starting Room"], regions["Color Hallways"], "Early Color Hallways",
+ None, EntranceType.PAINTING, False, world)
+
+ if painting_shuffle:
+ for warp_enter, warp_exit in world.player_logic.painting_mapping.items():
+ connect_painting(regions, warp_enter, warp_exit, world)
+
+ if world.options.shuffle_sunwarps:
+ for i in range(0, 6):
+ if world.options.sunwarp_access == SunwarpAccess.option_normal:
+ effective_door = None
+ else:
+ effective_door = RoomAndDoor("Sunwarps", f"{i + 1} Sunwarp")
+
+ source_region = regions[world.player_logic.sunwarp_entrances[i]]
+ target_region = regions[world.player_logic.sunwarp_exits[i]]
+
+ entrance_name = f"{source_region.name} to {target_region.name} ({i + 1} Sunwarp)"
+ connect_entrance(regions, source_region, target_region, entrance_name, effective_door, EntranceType.SUNWARP,
+ False, world)
+
+ world.multiworld.regions += regions.values()
diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py
new file mode 100644
index 000000000000..e0bb08fa1f7a
--- /dev/null
+++ b/worlds/lingo/rules.py
@@ -0,0 +1,100 @@
+from typing import TYPE_CHECKING
+
+from BaseClasses import CollectionState
+from .datatypes import RoomAndDoor
+from .player_logic import AccessRequirements, PlayerLocation
+from .static_logic import PROGRESSIVE_DOORS_BY_ROOM, PROGRESSIVE_ITEMS
+
+if TYPE_CHECKING:
+ from . import LingoWorld
+
+
+def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, world: "LingoWorld"):
+ if door is None:
+ return True
+
+ effective_room = room if door.room is None else door.room
+ return _lingo_can_open_door(state, effective_room, door.door, world)
+
+
+def lingo_can_do_pilgrimage(state: CollectionState, world: "LingoWorld"):
+ return all(_lingo_can_open_door(state, "Sunwarps", f"{i} Sunwarp", world) for i in range(1, 7))
+
+
+def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld"):
+ return _lingo_can_satisfy_requirements(state, location.access, world)
+
+
+def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"):
+ satisfied_count = 0
+ for access_req in world.player_logic.mastery_reqs:
+ if _lingo_can_satisfy_requirements(state, access_req, world):
+ satisfied_count += 1
+ return satisfied_count >= world.options.mastery_achievements.value
+
+
+def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"):
+ counted_panels = 0
+ state.update_reachable_regions(world.player)
+ for region in state.reachable_regions[world.player]:
+ for access_req, panel_count in world.player_logic.counting_panel_reqs.get(region.name, []):
+ if _lingo_can_satisfy_requirements(state, access_req, world):
+ counted_panels += panel_count
+ if counted_panels >= world.options.level_2_requirement.value - 1:
+ return True
+ return False
+
+
+def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld"):
+ for req_room in access.rooms:
+ if not state.can_reach(req_room, "Region", world.player):
+ return False
+
+ for req_door in access.doors:
+ if not _lingo_can_open_door(state, req_door.room, req_door.door, world):
+ return False
+
+ if len(access.colors) > 0 and world.options.shuffle_colors:
+ for color in access.colors:
+ if not state.has(color.capitalize(), world.player):
+ return False
+
+ if not all(state.has(item, world.player) for item in access.items):
+ return False
+
+ if not all(state.has(item, world.player, index) for item, index in access.progression.items()):
+ return False
+
+ if access.the_master and not lingo_can_use_mastery_location(state, world):
+ return False
+
+ if access.postgame and state.has("Prevent Victory", world.player):
+ return False
+
+ return True
+
+
+def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "LingoWorld"):
+ """
+ Determines whether a door can be opened
+ """
+ if door not in world.player_logic.item_by_door.get(room, {}):
+ return _lingo_can_satisfy_requirements(state, world.player_logic.door_reqs[room][door], world)
+
+ item_name = world.player_logic.item_by_door[room][door]
+ if item_name in PROGRESSIVE_ITEMS:
+ progression = PROGRESSIVE_DOORS_BY_ROOM[room][door]
+ return state.has(item_name, world.player, progression.index)
+
+ return state.has(item_name, world.player)
+
+
+def make_location_lambda(location: PlayerLocation, world: "LingoWorld"):
+ if location.name == world.player_logic.mastery_location:
+ return lambda state: lingo_can_use_mastery_location(state, world)
+
+ if world.options.level_2_requirement > 1\
+ and (location.name == "Second Room - ANOTHER TRY" or location.name == world.player_logic.level_2_location):
+ return lambda state: lingo_can_use_level_2_location(state, world)
+
+ return lambda state: lingo_can_use_location(state, location, world)
diff --git a/worlds/lingo/static_logic.py b/worlds/lingo/static_logic.py
new file mode 100644
index 000000000000..74eea449f228
--- /dev/null
+++ b/worlds/lingo/static_logic.py
@@ -0,0 +1,140 @@
+import os
+import pkgutil
+import pickle
+from io import BytesIO
+from typing import Dict, List, Set
+
+from .datatypes import Door, Painting, Panel, PanelDoor, Progression, Room
+
+ALL_ROOMS: List[Room] = []
+DOORS_BY_ROOM: Dict[str, Dict[str, Door]] = {}
+PANELS_BY_ROOM: Dict[str, Dict[str, Panel]] = {}
+PANEL_DOORS_BY_ROOM: Dict[str, Dict[str, PanelDoor]] = {}
+PAINTINGS: Dict[str, Painting] = {}
+
+PROGRESSIVE_ITEMS: Set[str] = set()
+PROGRESSIVE_DOORS_BY_ROOM: Dict[str, Dict[str, Progression]] = {}
+PROGRESSIVE_PANELS_BY_ROOM: Dict[str, Dict[str, Progression]] = {}
+
+PAINTING_ENTRANCES: int = 0
+PAINTING_EXIT_ROOMS: Set[str] = set()
+PAINTING_EXITS: int = 0
+REQUIRED_PAINTING_ROOMS: List[str] = []
+REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS: List[str] = []
+
+SUNWARP_ENTRANCES: List[str] = []
+SUNWARP_EXITS: List[str] = []
+
+SPECIAL_ITEM_IDS: Dict[str, int] = {}
+PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
+DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
+DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {}
+DOOR_GROUP_ITEM_IDS: Dict[str, int] = {}
+PANEL_DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {}
+PANEL_GROUP_ITEM_IDS: Dict[str, int] = {}
+PROGRESSIVE_ITEM_IDS: Dict[str, int] = {}
+
+HASHES: Dict[str, str] = {}
+
+
+def get_special_item_id(name: str):
+ if name not in SPECIAL_ITEM_IDS:
+ raise Exception(f"Item ID for special item {name} not found in ids.yaml.")
+
+ return SPECIAL_ITEM_IDS[name]
+
+
+def get_panel_location_id(room: str, name: str):
+ if room not in PANEL_LOCATION_IDS or name not in PANEL_LOCATION_IDS[room]:
+ raise Exception(f"Location ID for panel {room} - {name} not found in ids.yaml.")
+
+ return PANEL_LOCATION_IDS[room][name]
+
+
+def get_door_location_id(room: str, name: str):
+ if room not in DOOR_LOCATION_IDS or name not in DOOR_LOCATION_IDS[room]:
+ raise Exception(f"Location ID for door {room} - {name} not found in ids.yaml.")
+
+ return DOOR_LOCATION_IDS[room][name]
+
+
+def get_door_item_id(room: str, name: str):
+ if room not in DOOR_ITEM_IDS or name not in DOOR_ITEM_IDS[room]:
+ raise Exception(f"Item ID for door {room} - {name} not found in ids.yaml.")
+
+ return DOOR_ITEM_IDS[room][name]
+
+
+def get_door_group_item_id(name: str):
+ if name not in DOOR_GROUP_ITEM_IDS:
+ raise Exception(f"Item ID for door group {name} not found in ids.yaml.")
+
+ return DOOR_GROUP_ITEM_IDS[name]
+
+
+def get_panel_door_item_id(room: str, name: str):
+ if room not in PANEL_DOOR_ITEM_IDS or name not in PANEL_DOOR_ITEM_IDS[room]:
+ raise Exception(f"Item ID for panel door {room} - {name} not found in ids.yaml.")
+
+ return PANEL_DOOR_ITEM_IDS[room][name]
+
+
+def get_panel_group_item_id(name: str):
+ if name not in PANEL_GROUP_ITEM_IDS:
+ raise Exception(f"Item ID for panel group {name} not found in ids.yaml.")
+
+ return PANEL_GROUP_ITEM_IDS[name]
+
+
+def get_progressive_item_id(name: str):
+ if name not in PROGRESSIVE_ITEM_IDS:
+ raise Exception(f"Item ID for progressive item {name} not found in ids.yaml.")
+
+ return PROGRESSIVE_ITEM_IDS[name]
+
+
+def load_static_data_from_file():
+ global PAINTING_ENTRANCES, PAINTING_EXITS
+
+ from . import datatypes
+ from Utils import safe_builtins
+
+ class RenameUnpickler(pickle.Unpickler):
+ def find_class(self, module, name):
+ if module in ("worlds.lingo.datatypes", "datatypes"):
+ return getattr(datatypes, name)
+ elif module == "builtins" and name in safe_builtins:
+ return getattr(safe_builtins, name)
+ raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
+
+ file = pkgutil.get_data(__name__, os.path.join("data", "generated.dat"))
+ pickdata = RenameUnpickler(BytesIO(file)).load()
+
+ HASHES.update(pickdata["HASHES"])
+ PAINTINGS.update(pickdata["PAINTINGS"])
+ ALL_ROOMS.extend(pickdata["ALL_ROOMS"])
+ DOORS_BY_ROOM.update(pickdata["DOORS_BY_ROOM"])
+ PANELS_BY_ROOM.update(pickdata["PANELS_BY_ROOM"])
+ PANEL_DOORS_BY_ROOM.update(pickdata["PANEL_DOORS_BY_ROOM"])
+ PROGRESSIVE_ITEMS.update(pickdata["PROGRESSIVE_ITEMS"])
+ PROGRESSIVE_DOORS_BY_ROOM.update(pickdata["PROGRESSIVE_DOORS_BY_ROOM"])
+ PROGRESSIVE_PANELS_BY_ROOM.update(pickdata["PROGRESSIVE_PANELS_BY_ROOM"])
+ PAINTING_ENTRANCES = pickdata["PAINTING_ENTRANCES"]
+ PAINTING_EXIT_ROOMS.update(pickdata["PAINTING_EXIT_ROOMS"])
+ PAINTING_EXITS = pickdata["PAINTING_EXITS"]
+ REQUIRED_PAINTING_ROOMS.extend(pickdata["REQUIRED_PAINTING_ROOMS"])
+ REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS.extend(pickdata["REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS"])
+ SUNWARP_ENTRANCES.extend(pickdata["SUNWARP_ENTRANCES"])
+ SUNWARP_EXITS.extend(pickdata["SUNWARP_EXITS"])
+ SPECIAL_ITEM_IDS.update(pickdata["SPECIAL_ITEM_IDS"])
+ PANEL_LOCATION_IDS.update(pickdata["PANEL_LOCATION_IDS"])
+ DOOR_LOCATION_IDS.update(pickdata["DOOR_LOCATION_IDS"])
+ DOOR_ITEM_IDS.update(pickdata["DOOR_ITEM_IDS"])
+ DOOR_GROUP_ITEM_IDS.update(pickdata["DOOR_GROUP_ITEM_IDS"])
+ PANEL_DOOR_ITEM_IDS.update(pickdata["PANEL_DOOR_ITEM_IDS"])
+ PANEL_GROUP_ITEM_IDS.update(pickdata["PANEL_GROUP_ITEM_IDS"])
+ PROGRESSIVE_ITEM_IDS.update(pickdata["PROGRESSIVE_ITEM_IDS"])
+
+
+# Initialize the static data at module scope.
+load_static_data_from_file()
diff --git a/worlds/lingo/test/TestDatafile.py b/worlds/lingo/test/TestDatafile.py
new file mode 100644
index 000000000000..60acb3e85e96
--- /dev/null
+++ b/worlds/lingo/test/TestDatafile.py
@@ -0,0 +1,16 @@
+import os
+import unittest
+
+from ..static_logic import HASHES
+from ..utils.pickle_static_data import hash_file
+
+
+class TestDatafile(unittest.TestCase):
+ def test_check_hashes(self) -> None:
+ ll1_file_hash = hash_file(os.path.join(os.path.dirname(__file__), "..", "data", "LL1.yaml"))
+ ids_file_hash = hash_file(os.path.join(os.path.dirname(__file__), "..", "data", "ids.yaml"))
+
+ self.assertEqual(ll1_file_hash, HASHES["LL1.yaml"],
+ "LL1.yaml hash does not match generated.dat. Please regenerate using 'python worlds/lingo/utils/pickle_static_data.py'")
+ self.assertEqual(ids_file_hash, HASHES["ids.yaml"],
+ "ids.yaml hash does not match generated.dat. Please regenerate using 'python worlds/lingo/utils/pickle_static_data.py'")
diff --git a/worlds/lingo/test/TestDoors.py b/worlds/lingo/test/TestDoors.py
new file mode 100644
index 000000000000..cfbd7f30278c
--- /dev/null
+++ b/worlds/lingo/test/TestDoors.py
@@ -0,0 +1,142 @@
+from . import LingoTestBase
+
+
+class TestRequiredRoomLogic(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "shuffle_colors": "false",
+ }
+
+ def test_pilgrim_first(self) -> None:
+ self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
+ self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
+
+ self.collect_by_name("Pilgrim Room - Sun Painting")
+ self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
+ self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
+
+ self.collect_by_name("Pilgrim Room - Shortcut to The Seeker")
+ self.assertTrue(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
+ self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
+
+ self.collect_by_name("Starting Room - Back Right Door")
+ self.assertTrue(self.can_reach_location("The Seeker - Achievement"))
+
+ def test_hidden_first(self) -> None:
+ self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
+ self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
+
+ self.collect_by_name("Starting Room - Back Right Door")
+ self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
+ self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
+
+ self.collect_by_name("Pilgrim Room - Shortcut to The Seeker")
+ self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
+ self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
+
+ self.collect_by_name("Pilgrim Room - Sun Painting")
+ self.assertTrue(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
+ self.assertTrue(self.can_reach_location("The Seeker - Achievement"))
+
+
+class TestRequiredDoorLogic(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "shuffle_colors": "false",
+ }
+
+ def test_through_rhyme(self) -> None:
+ self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
+
+ self.collect_by_name("Starting Room - Rhyme Room Entrance")
+ self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
+
+ self.collect_by_name("Rhyme Room (Looped Square) - Door to Circle")
+ self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
+
+ def test_through_hidden(self) -> None:
+ self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
+
+ self.collect_by_name("Starting Room - Rhyme Room Entrance")
+ self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
+
+ self.collect_by_name("Starting Room - Back Right Door")
+ self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
+
+ self.collect_by_name("Hidden Room - Rhyme Room Entrance")
+ self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
+
+
+class TestSimpleDoors(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "group_doors": "true",
+ "shuffle_colors": "false",
+ }
+
+ def test_requirement(self):
+ self.assertFalse(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+
+ self.collect_by_name("Rhyme Room Doors")
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+
+
+class TestPanels(LingoTestBase):
+ options = {
+ "shuffle_doors": "panels"
+ }
+
+ def test_requirement(self):
+ self.assertFalse(self.can_reach_location("Starting Room - HIDDEN"))
+ self.assertFalse(self.can_reach_location("Hidden Room - OPEN"))
+ self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
+
+ self.collect_by_name("Starting Room - HIDDEN (Panel)")
+ self.assertTrue(self.can_reach_location("Starting Room - HIDDEN"))
+ self.assertFalse(self.can_reach_location("Hidden Room - OPEN"))
+ self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
+
+ self.collect_by_name("Hidden Room - OPEN (Panel)")
+ self.assertTrue(self.can_reach_location("Starting Room - HIDDEN"))
+ self.assertTrue(self.can_reach_location("Hidden Room - OPEN"))
+ self.assertTrue(self.can_reach_location("The Seeker - Achievement"))
+
+
+class TestGroupedPanels(LingoTestBase):
+ options = {
+ "shuffle_doors": "panels",
+ "group_doors": "true",
+ "shuffle_colors": "false",
+ }
+
+ def test_requirement(self):
+ self.assertFalse(self.can_reach_location("Hub Room - SLAUGHTER"))
+ self.assertFalse(self.can_reach_location("Dread Hallway - DREAD"))
+ self.assertFalse(self.can_reach_location("The Tenacious - Achievement"))
+
+ self.collect_by_name("Tenacious Entrance Panels")
+ self.assertTrue(self.can_reach_location("Hub Room - SLAUGHTER"))
+ self.assertFalse(self.can_reach_location("Dread Hallway - DREAD"))
+ self.assertFalse(self.can_reach_location("The Tenacious - Achievement"))
+
+ self.collect_by_name("Outside The Agreeable - BLACK (Panel)")
+ self.assertTrue(self.can_reach_location("Hub Room - SLAUGHTER"))
+ self.assertTrue(self.can_reach_location("Dread Hallway - DREAD"))
+ self.assertFalse(self.can_reach_location("The Tenacious - Achievement"))
+
+ self.collect_by_name("The Tenacious - Black Palindromes (Panels)")
+ self.assertTrue(self.can_reach_location("Hub Room - SLAUGHTER"))
+ self.assertTrue(self.can_reach_location("Dread Hallway - DREAD"))
+ self.assertTrue(self.can_reach_location("The Tenacious - Achievement"))
+
diff --git a/worlds/lingo/test/TestMastery.py b/worlds/lingo/test/TestMastery.py
new file mode 100644
index 000000000000..c9c79a9d0658
--- /dev/null
+++ b/worlds/lingo/test/TestMastery.py
@@ -0,0 +1,58 @@
+from . import LingoTestBase
+
+
+class TestMasteryWhenVictoryIsTheEnd(LingoTestBase):
+ options = {
+ "mastery_achievements": "22",
+ "victory_condition": "the_end",
+ "shuffle_colors": "true",
+ "shuffle_postgame": "true",
+ }
+
+ def test_requirement(self):
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect_by_name(["Red", "Blue", "Black", "Purple", "Orange"])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+ self.assertTrue(self.can_reach_location("The End (Solved)"))
+ self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - THE MASTER"))
+
+ self.collect_by_name(["Green", "Brown", "Yellow"])
+ self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - THE MASTER"))
+
+
+class TestMasteryWhenVictoryIsTheMaster(LingoTestBase):
+ options = {
+ "mastery_achievements": "24",
+ "victory_condition": "the_master",
+ "shuffle_colors": "true"
+ }
+
+ def test_requirement(self):
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect_by_name(["Red", "Blue", "Black", "Purple", "Orange"])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+ self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - THE END"))
+ self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements"))
+
+ self.collect_by_name(["Green", "Gray", "Brown", "Yellow"])
+ self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements"))
+
+
+class TestMasteryBlocksDependents(LingoTestBase):
+ options = {
+ "mastery_achievements": "24",
+ "shuffle_colors": "true",
+ "location_checks": "insanity",
+ "victory_condition": "level_2",
+ }
+
+ def test_requirement(self):
+ self.collect_all_but("Gray")
+ self.assertFalse(self.can_reach_location("Orange Tower Basement - THE LIBRARY"))
+ self.assertFalse(self.can_reach_location("The Fearless - MASTERY"))
+
+ self.collect_by_name("Gray")
+ self.assertTrue(self.can_reach_location("Orange Tower Basement - THE LIBRARY"))
+ self.assertTrue(self.can_reach_location("The Fearless - MASTERY"))
diff --git a/worlds/lingo/test/TestOptions.py b/worlds/lingo/test/TestOptions.py
new file mode 100644
index 000000000000..bd8ed81d7a12
--- /dev/null
+++ b/worlds/lingo/test/TestOptions.py
@@ -0,0 +1,62 @@
+from . import LingoTestBase
+
+
+class TestMultiShuffleOptions(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "progressive_orange_tower": "true",
+ "shuffle_colors": "true",
+ "shuffle_paintings": "true",
+ "early_color_hallways": "true"
+ }
+
+
+class TestPanelsanity(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "progressive_orange_tower": "true",
+ "location_checks": "insanity",
+ "shuffle_colors": "true"
+ }
+
+
+class TestAllPanelHunt(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "progressive_orange_tower": "true",
+ "shuffle_colors": "true",
+ "victory_condition": "level_2",
+ "level_2_requirement": "800",
+ "early_color_hallways": "true"
+ }
+
+
+class TestAllPanelHuntPanelsMode(LingoTestBase):
+ options = {
+ "shuffle_doors": "panels",
+ "progressive_orange_tower": "true",
+ "shuffle_colors": "true",
+ "victory_condition": "level_2",
+ "level_2_requirement": "800",
+ "early_color_hallways": "true"
+ }
+
+
+class TestShuffleSunwarps(LingoTestBase):
+ options = {
+ "shuffle_doors": "none",
+ "shuffle_colors": "false",
+ "victory_condition": "pilgrimage",
+ "shuffle_sunwarps": "true",
+ "sunwarp_access": "normal"
+ }
+
+
+class TestShuffleSunwarpsAccess(LingoTestBase):
+ options = {
+ "shuffle_doors": "none",
+ "shuffle_colors": "false",
+ "victory_condition": "pilgrimage",
+ "shuffle_sunwarps": "true",
+ "sunwarp_access": "individual"
+ }
\ No newline at end of file
diff --git a/worlds/lingo/test/TestOrangeTower.py b/worlds/lingo/test/TestOrangeTower.py
new file mode 100644
index 000000000000..444264a58964
--- /dev/null
+++ b/worlds/lingo/test/TestOrangeTower.py
@@ -0,0 +1,175 @@
+from . import LingoTestBase
+
+
+class TestProgressiveOrangeTower(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "progressive_orange_tower": "true"
+ }
+
+ def test_from_welcome_back(self) -> None:
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect_by_name("Welcome Back Area - Shortcut to Starting Room")
+ self.collect_by_name("Orange Tower Fifth Floor - Welcome Back")
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ progressive_tower = self.get_items_by_name("Progressive Orange Tower")
+
+ self.collect(progressive_tower[0])
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[1])
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[2])
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[3])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[4])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[5])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ def test_from_hub_room(self) -> None:
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect_by_name("Second Room - Exit Door")
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect_by_name("Orange Tower First Floor - Shortcut to Hub Room")
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ progressive_tower = self.get_items_by_name("Progressive Orange Tower")
+
+ self.collect(progressive_tower[0])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.remove(self.get_item_by_name("Orange Tower First Floor - Shortcut to Hub Room"))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[1])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[2])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[3])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[4])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
+
+ self.collect(progressive_tower[5])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
diff --git a/worlds/lingo/test/TestPanelsanity.py b/worlds/lingo/test/TestPanelsanity.py
new file mode 100644
index 000000000000..f8330ae78206
--- /dev/null
+++ b/worlds/lingo/test/TestPanelsanity.py
@@ -0,0 +1,19 @@
+from . import LingoTestBase
+
+
+class TestPanelHunt(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "location_checks": "insanity",
+ "victory_condition": "level_2",
+ "level_2_requirement": "15"
+ }
+
+ def test_another_try(self) -> None:
+ self.collect_by_name("The Traveled - Entrance") # idk why this is needed
+ self.assertFalse(self.can_reach_location("Second Room - ANOTHER TRY"))
+ self.assertFalse(self.can_reach_location("Second Room - Unlock Level 2"))
+
+ self.collect_by_name("Second Room - Exit Door")
+ self.assertTrue(self.can_reach_location("Second Room - ANOTHER TRY"))
+ self.assertTrue(self.can_reach_location("Second Room - Unlock Level 2"))
diff --git a/worlds/lingo/test/TestPilgrimage.py b/worlds/lingo/test/TestPilgrimage.py
new file mode 100644
index 000000000000..328156da2d17
--- /dev/null
+++ b/worlds/lingo/test/TestPilgrimage.py
@@ -0,0 +1,138 @@
+from . import LingoTestBase
+
+
+class TestDisabledPilgrimage(LingoTestBase):
+ options = {
+ "enable_pilgrimage": "false",
+ "shuffle_colors": "false"
+ }
+
+ def test_access(self):
+ self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+
+ self.collect_by_name("Pilgrim Room - Sun Painting")
+ self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+
+
+class TestPilgrimageWithRoofAndPaintings(LingoTestBase):
+ options = {
+ "enable_pilgrimage": "true",
+ "shuffle_colors": "false",
+ "shuffle_doors": "doors",
+ "pilgrimage_allows_roof_access": "true",
+ "pilgrimage_allows_paintings": "true",
+ "early_color_hallways": "false"
+ }
+
+ def test_access(self):
+ doors = ["Second Room - Exit Door", "Crossroads - Roof Access", "Hub Room - Crossroads Entrance",
+ "Outside The Undeterred - Green Painting"]
+
+ for door in doors:
+ self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+ self.collect_by_name(door)
+
+ self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+
+
+class TestPilgrimageNoRoofYesPaintings(LingoTestBase):
+ options = {
+ "enable_pilgrimage": "true",
+ "shuffle_colors": "false",
+ "shuffle_doors": "doors",
+ "pilgrimage_allows_roof_access": "false",
+ "pilgrimage_allows_paintings": "true",
+ "early_color_hallways": "false"
+ }
+
+ def test_access(self):
+ doors = ["Second Room - Exit Door", "Crossroads - Roof Access", "Hub Room - Crossroads Entrance",
+ "Outside The Undeterred - Green Painting", "Crossroads - Tower Entrance",
+ "Orange Tower Fourth Floor - Hot Crusts Door", "Orange Tower First Floor - Shortcut to Hub Room",
+ "Starting Room - Street Painting"]
+
+ for door in doors:
+ self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+ self.collect_by_name(door)
+
+ self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+
+
+class TestPilgrimageNoRoofNoPaintings(LingoTestBase):
+ options = {
+ "enable_pilgrimage": "true",
+ "shuffle_colors": "false",
+ "shuffle_doors": "doors",
+ "pilgrimage_allows_roof_access": "false",
+ "pilgrimage_allows_paintings": "false",
+ "early_color_hallways": "false"
+ }
+
+ def test_access(self):
+ doors = ["Second Room - Exit Door", "Crossroads - Roof Access", "Hub Room - Crossroads Entrance",
+ "Outside The Undeterred - Green Painting", "Orange Tower First Floor - Shortcut to Hub Room",
+ "Starting Room - Street Painting", "Outside The Initiated - Shortcut to Hub Room",
+ "Directional Gallery - Shortcut to The Undeterred", "Orange Tower First Floor - Salt Pepper Door",
+ "Color Hunt - Shortcut to The Steady", "The Bearer - Entrance",
+ "Orange Tower Fifth Floor - Quadruple Intersection", "The Tenacious - Shortcut to Hub Room",
+ "Outside The Agreeable - Tenacious Entrance", "Crossroads - Tower Entrance",
+ "Orange Tower Fourth Floor - Hot Crusts Door"]
+
+ for door in doors:
+ self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+ self.collect_by_name(door)
+
+ self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+
+
+class TestPilgrimageRequireStartingRoom(LingoTestBase):
+ options = {
+ "enable_pilgrimage": "true",
+ "shuffle_colors": "false",
+ "shuffle_doors": "complex",
+ "pilgrimage_allows_roof_access": "false",
+ "pilgrimage_allows_paintings": "false",
+ "early_color_hallways": "false"
+ }
+
+ def test_access(self):
+ doors = ["Second Room - Exit Door", "Crossroads - Roof Access", "Hub Room - Crossroads Entrance",
+ "Outside The Undeterred - Green Painting", "Outside The Undeterred - Number Hunt",
+ "Starting Room - Street Painting", "Outside The Initiated - Shortcut to Hub Room",
+ "Directional Gallery - Shortcut to The Undeterred", "Orange Tower First Floor - Salt Pepper Door",
+ "Color Hunt - Shortcut to The Steady", "The Bearer - Entrance",
+ "Orange Tower Fifth Floor - Quadruple Intersection", "The Tenacious - Shortcut to Hub Room",
+ "Outside The Agreeable - Tenacious Entrance", "Crossroads - Tower Entrance",
+ "Orange Tower Fourth Floor - Hot Crusts Door", "Challenge Room - Welcome Door",
+ "Number Hunt - Challenge Entrance", "Welcome Back Area - Shortcut to Starting Room"]
+
+ for door in doors:
+ self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+ self.collect_by_name(door)
+
+ self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+
+
+class TestPilgrimageYesRoofNoPaintings(LingoTestBase):
+ options = {
+ "enable_pilgrimage": "true",
+ "shuffle_colors": "false",
+ "shuffle_doors": "doors",
+ "pilgrimage_allows_roof_access": "true",
+ "pilgrimage_allows_paintings": "false",
+ "early_color_hallways": "false"
+ }
+
+ def test_access(self):
+ doors = ["Second Room - Exit Door", "Crossroads - Roof Access", "Hub Room - Crossroads Entrance",
+ "Outside The Undeterred - Green Painting", "Orange Tower First Floor - Shortcut to Hub Room",
+ "Starting Room - Street Painting", "Outside The Initiated - Shortcut to Hub Room",
+ "Directional Gallery - Shortcut to The Undeterred", "Orange Tower First Floor - Salt Pepper Door",
+ "Color Hunt - Shortcut to The Steady", "The Bearer - Entrance",
+ "Orange Tower Fifth Floor - Quadruple Intersection"]
+
+ for door in doors:
+ self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+ self.collect_by_name(door)
+
+ self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
diff --git a/worlds/lingo/test/TestPostgame.py b/worlds/lingo/test/TestPostgame.py
new file mode 100644
index 000000000000..d2e2232ff769
--- /dev/null
+++ b/worlds/lingo/test/TestPostgame.py
@@ -0,0 +1,62 @@
+from . import LingoTestBase
+
+
+class TestPostgameVanillaTheEnd(LingoTestBase):
+ options = {
+ "shuffle_doors": "none",
+ "victory_condition": "the_end",
+ "shuffle_postgame": "false",
+ }
+
+ def test_requirement(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+
+ self.assertTrue("The End (Solved)" in location_names)
+ self.assertTrue("Champion's Rest - YOU" in location_names)
+ self.assertFalse("Orange Tower Seventh Floor - THE MASTER" in location_names)
+ self.assertFalse("The Red - Achievement" in location_names)
+
+
+class TestPostgameComplexDoorsTheEnd(LingoTestBase):
+ options = {
+ "shuffle_doors": "complex",
+ "victory_condition": "the_end",
+ "shuffle_postgame": "false",
+ }
+
+ def test_requirement(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+
+ self.assertTrue("The End (Solved)" in location_names)
+ self.assertFalse("Orange Tower Seventh Floor - THE MASTER" in location_names)
+ self.assertTrue("The Red - Achievement" in location_names)
+
+
+class TestPostgameLateColorHunt(LingoTestBase):
+ options = {
+ "shuffle_doors": "none",
+ "victory_condition": "the_end",
+ "sunwarp_access": "disabled",
+ "shuffle_postgame": "false",
+ }
+
+ def test_requirement(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+
+ self.assertFalse("Champion's Rest - YOU" in location_names)
+
+
+class TestPostgameVanillaTheMaster(LingoTestBase):
+ options = {
+ "shuffle_doors": "none",
+ "victory_condition": "the_master",
+ "shuffle_postgame": "false",
+ }
+
+ def test_requirement(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+
+ self.assertTrue("Orange Tower Seventh Floor - THE END" in location_names)
+ self.assertTrue("Orange Tower Seventh Floor - Mastery Achievements" in location_names)
+ self.assertTrue("The Red - Achievement" in location_names)
+ self.assertFalse("Mastery Panels" in location_names)
diff --git a/worlds/lingo/test/TestProgressive.py b/worlds/lingo/test/TestProgressive.py
new file mode 100644
index 000000000000..2c837f53f34d
--- /dev/null
+++ b/worlds/lingo/test/TestProgressive.py
@@ -0,0 +1,193 @@
+from . import LingoTestBase
+
+
+class TestComplexProgressiveHallwayRoom(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors"
+ }
+
+ def test_item(self):
+ self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+ self.collect_by_name(["Second Room - Exit Door", "The Tenacious - Shortcut to Hub Room",
+ "Outside The Agreeable - Tenacious Entrance"])
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+ progressive_hallway_room = self.get_items_by_name("Progressive Hallway Room")
+
+ self.collect(progressive_hallway_room[0])
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+ self.collect(progressive_hallway_room[1])
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+ self.collect(progressive_hallway_room[2])
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+ self.collect(progressive_hallway_room[3])
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+
+class TestSimpleHallwayRoom(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "group_doors": "true",
+ }
+
+ def test_item(self):
+ self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+ self.collect_by_name(["Second Room - Exit Door", "Entrances to The Tenacious"])
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+ self.collect_by_name("Hallway Room Doors")
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
+
+
+class TestProgressiveArtGallery(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "shuffle_colors": "false",
+ }
+
+ def test_item(self):
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ORDER"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect_by_name(["Second Room - Exit Door", "Crossroads - Tower Entrance",
+ "Orange Tower Fourth Floor - Hot Crusts Door"])
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ORDER"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ progressive_gallery_room = self.get_items_by_name("Progressive Art Gallery")
+
+ self.collect(progressive_gallery_room[0])
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ORDER"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect(progressive_gallery_room[1])
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ORDER"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect(progressive_gallery_room[2])
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ORDER"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect(progressive_gallery_room[3])
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertTrue(self.can_reach_location("Art Gallery - ORDER"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect_by_name("Orange Tower Fifth Floor - Quadruple Intersection")
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertTrue(self.can_reach_location("Art Gallery - ORDER"))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+
+class TestNoDoorsArtGallery(LingoTestBase):
+ options = {
+ "shuffle_doors": "none",
+ "shuffle_colors": "true"
+ }
+
+ def test_item(self):
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ORDER"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect_by_name("Yellow")
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ORDER"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect_by_name("Brown")
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ORDER"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect_by_name("Blue")
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertFalse(self.can_reach_location("Art Gallery - ORDER"))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
+
+ self.collect_by_name(["Orange", "Gray"])
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
+ self.assertTrue(self.can_reach_location("Art Gallery - ORDER"))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
diff --git a/worlds/lingo/test/TestSunwarps.py b/worlds/lingo/test/TestSunwarps.py
new file mode 100644
index 000000000000..66ba3afd6e90
--- /dev/null
+++ b/worlds/lingo/test/TestSunwarps.py
@@ -0,0 +1,220 @@
+from . import LingoTestBase
+
+
+class TestVanillaDoorsNormalSunwarps(LingoTestBase):
+ options = {
+ "shuffle_doors": "none",
+ "shuffle_colors": "true",
+ "sunwarp_access": "normal"
+ }
+
+ def test_access(self):
+ self.assertTrue(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+
+ self.collect_by_name("Yellow")
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
+
+
+class TestSimpleDoorsNormalSunwarps(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "group_doors": "true",
+ "sunwarp_access": "normal"
+ }
+
+ def test_access(self):
+ self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+
+ self.collect_by_name("Second Room - Exit Door")
+ self.assertTrue(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+
+ self.collect_by_name(["Crossroads - Tower Entrances", "Orange Tower Fourth Floor - Hot Crusts Door"])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
+
+
+class TestSimpleDoorsDisabledSunwarps(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "group_doors": "true",
+ "sunwarp_access": "disabled"
+ }
+
+ def test_access(self):
+ self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+
+ self.collect_by_name("Second Room - Exit Door")
+ self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+
+ self.collect_by_name(["Hub Room - Crossroads Entrance", "Crossroads - Tower Entrancse",
+ "Orange Tower Fourth Floor - Hot Crusts Door"])
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
+
+
+class TestSimpleDoorsUnlockSunwarps(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "group_doors": "true",
+ "sunwarp_access": "unlock"
+ }
+
+ def test_access(self):
+ self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+
+ self.collect_by_name("Second Room - Exit Door")
+ self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+
+ self.collect_by_name(["Crossroads - Tower Entrances", "Orange Tower Fourth Floor - Hot Crusts Door"])
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
+
+ self.collect_by_name("Sunwarps")
+ self.assertTrue(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
+
+
+class TestComplexDoorsNormalSunwarps(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "group_doors": "false",
+ "sunwarp_access": "normal"
+ }
+
+ def test_access(self):
+ self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+
+ self.collect_by_name("Second Room - Exit Door")
+ self.assertTrue(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+
+ self.collect_by_name(["Crossroads - Tower Entrance", "Orange Tower Fourth Floor - Hot Crusts Door"])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
+
+
+class TestComplexDoorsDisabledSunwarps(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "group_doors": "false",
+ "sunwarp_access": "disabled"
+ }
+
+ def test_access(self):
+ self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+
+ self.collect_by_name("Second Room - Exit Door")
+ self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+
+ self.collect_by_name(["Hub Room - Crossroads Entrance", "Crossroads - Tower Entrance",
+ "Orange Tower Fourth Floor - Hot Crusts Door"])
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
+
+
+class TestComplexDoorsIndividualSunwarps(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "group_doors": "false",
+ "sunwarp_access": "individual"
+ }
+
+ def test_access(self):
+ self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+
+ self.collect_by_name("Second Room - Exit Door")
+ self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+
+ self.collect_by_name("1 Sunwarp")
+ self.assertTrue(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+
+ self.collect_by_name(["Crossroads - Tower Entrance", "Orange Tower Fourth Floor - Hot Crusts Door"])
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
+
+ self.collect_by_name("2 Sunwarp")
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
+
+ self.collect_by_name("3 Sunwarp")
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
+
+
+class TestComplexDoorsProgressiveSunwarps(LingoTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "group_doors": "false",
+ "sunwarp_access": "progressive"
+ }
+
+ def test_access(self):
+ self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+
+ self.collect_by_name("Second Room - Exit Door")
+ self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+
+ progressive_pilgrimage = self.get_items_by_name("Progressive Pilgrimage")
+ self.collect(progressive_pilgrimage[0])
+ self.assertTrue(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
+
+ self.collect_by_name(["Crossroads - Tower Entrance", "Orange Tower Fourth Floor - Hot Crusts Door"])
+ self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
+
+ self.collect(progressive_pilgrimage[1])
+ self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
+
+ self.collect(progressive_pilgrimage[2])
+ self.assertTrue(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
+
+
+class TestUnlockSunwarpPilgrimage(LingoTestBase):
+ options = {
+ "sunwarp_access": "unlock",
+ "shuffle_colors": "false",
+ "enable_pilgrimage": "true"
+ }
+
+ def test_access(self):
+ self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+
+ self.collect_by_name("Sunwarps")
+
+ self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+
+
+class TestIndividualSunwarpPilgrimage(LingoTestBase):
+ options = {
+ "sunwarp_access": "individual",
+ "shuffle_colors": "false",
+ "enable_pilgrimage": "true"
+ }
+
+ def test_access(self):
+ for i in range(1, 7):
+ self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+ self.collect_by_name(f"{i} Sunwarp")
+
+ self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+
+
+class TestProgressiveSunwarpPilgrimage(LingoTestBase):
+ options = {
+ "sunwarp_access": "progressive",
+ "shuffle_colors": "false",
+ "enable_pilgrimage": "true"
+ }
+
+ def test_access(self):
+ for item in self.get_items_by_name("Progressive Pilgrimage"):
+ self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
+ self.collect(item)
+
+ self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
diff --git a/worlds/lingo/test/__init__.py b/worlds/lingo/test/__init__.py
new file mode 100644
index 000000000000..a4196de110db
--- /dev/null
+++ b/worlds/lingo/test/__init__.py
@@ -0,0 +1,8 @@
+from typing import ClassVar
+
+from test.bases import WorldTestBase
+
+
+class LingoTestBase(WorldTestBase):
+ game = "Lingo"
+ player: ClassVar[int] = 1
diff --git a/worlds/lingo/utils/__init__.py b/worlds/lingo/utils/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/lingo/utils/assign_ids.rb b/worlds/lingo/utils/assign_ids.rb
new file mode 100644
index 000000000000..f7de3d03f582
--- /dev/null
+++ b/worlds/lingo/utils/assign_ids.rb
@@ -0,0 +1,218 @@
+# This utility goes through the provided Lingo config and assigns item and
+# location IDs to entities that require them (such as doors and panels). These
+# IDs are output in a separate yaml file. If the output file already exists,
+# then it will be updated with any newly assigned IDs rather than overwritten.
+# In this event, all new IDs will be greater than any already existing IDs,
+# even if there are gaps in the ID space; this is to prevent collision when IDs
+# are retired.
+#
+# This utility should be run whenever logically new items or locations are
+# required. If an item or location is created that is logically equivalent to
+# one that used to exist, this utility should not be used, and instead the ID
+# file should be manually edited so that the old ID can be reused.
+
+require 'set'
+require 'yaml'
+
+configpath = ARGV[0]
+outputpath = ARGV[1]
+
+next_item_id = 444400
+next_location_id = 444400
+
+location_id_by_name = {}
+
+old_generated = YAML.load_file(outputpath)
+File.write(outputpath + ".old", old_generated.to_yaml)
+
+if old_generated.include? "special_items" then
+ old_generated["special_items"].each do |name, id|
+ if id >= next_item_id then
+ next_item_id = id + 1
+ end
+ end
+end
+if old_generated.include? "special_locations" then
+ old_generated["special_locations"].each do |name, id|
+ if id >= next_location_id then
+ next_location_id = id + 1
+ end
+ end
+end
+if old_generated.include? "panels" then
+ old_generated["panels"].each do |room, panels|
+ panels.each do |name, id|
+ if id >= next_location_id then
+ next_location_id = id + 1
+ end
+ location_name = "#{room} - #{name}"
+ location_id_by_name[location_name] = id
+ end
+ end
+end
+if old_generated.include? "doors" then
+ old_generated["doors"].each do |room, doors|
+ doors.each do |name, ids|
+ if ids.include? "location" then
+ if ids["location"] >= next_location_id then
+ next_location_id = ids["location"] + 1
+ end
+ end
+ if ids.include? "item" then
+ if ids["item"] >= next_item_id then
+ next_item_id = ids["item"] + 1
+ end
+ end
+ end
+ end
+end
+if old_generated.include? "door_groups" then
+ old_generated["door_groups"].each do |name, id|
+ if id >= next_item_id then
+ next_item_id = id + 1
+ end
+ end
+end
+if old_generated.include? "panel_doors" then
+ old_generated["panel_doors"].each do |room, panel_doors|
+ panel_doors.each do |name, id|
+ if id >= next_item_id then
+ next_item_id = id + 1
+ end
+ end
+ end
+end
+if old_generated.include? "panel_groups" then
+ old_generated["panel_groups"].each do |name, id|
+ if id >= next_item_id then
+ next_item_id = id + 1
+ end
+ end
+end
+if old_generated.include? "progression" then
+ old_generated["progression"].each do |name, id|
+ if id >= next_item_id then
+ next_item_id = id + 1
+ end
+ end
+end
+
+door_groups = Set[]
+panel_groups = Set[]
+
+config = YAML.load_file(configpath)
+config.each do |room_name, room_data|
+ if room_data.include? "panels"
+ room_data["panels"].each do |panel_name, panel|
+ unless old_generated.include? "panels" and old_generated["panels"].include? room_name and old_generated["panels"][room_name].include? panel_name then
+ old_generated["panels"] ||= {}
+ old_generated["panels"][room_name] ||= {}
+ old_generated["panels"][room_name][panel_name] = next_location_id
+
+ location_name = "#{room_name} - #{panel_name}"
+ location_id_by_name[location_name] = next_location_id
+
+ next_location_id += 1
+ end
+ end
+ end
+end
+
+config.each do |room_name, room_data|
+ if room_data.include? "doors"
+ room_data["doors"].each do |door_name, door|
+ if door.include? "event" and door["event"] then
+ next
+ end
+
+ unless door.include? "skip_item" and door["skip_item"] then
+ unless old_generated.include? "doors" and old_generated["doors"].include? room_name and old_generated["doors"][room_name].include? door_name and old_generated["doors"][room_name][door_name].include? "item" then
+ old_generated["doors"] ||= {}
+ old_generated["doors"][room_name] ||= {}
+ old_generated["doors"][room_name][door_name] ||= {}
+ old_generated["doors"][room_name][door_name]["item"] = next_item_id
+
+ next_item_id += 1
+ end
+
+ if door.include? "group" and not door_groups.include? door["group"] then
+ door_groups.add(door["group"])
+
+ unless old_generated.include? "door_groups" and old_generated["door_groups"].include? door["group"] then
+ old_generated["door_groups"] ||= {}
+ old_generated["door_groups"][door["group"]] = next_item_id
+
+ next_item_id += 1
+ end
+ end
+ end
+
+ unless door.include? "skip_location" and door["skip_location"] then
+ location_name = ""
+ if door.include? "location_name" then
+ location_name = door["location_name"]
+ elsif door.include? "panels" then
+ location_name = door["panels"].map do |panel|
+ if panel.kind_of? Hash then
+ panel
+ else
+ {"room" => room_name, "panel" => panel}
+ end
+ end.sort_by {|panel| panel["room"]}.chunk {|panel| panel["room"]}.map do |room_panels|
+ room_panels[0] + " - " + room_panels[1].map{|panel| panel["panel"]}.join(", ")
+ end.join(" and ")
+ end
+
+ if location_id_by_name.has_key? location_name then
+ old_generated["doors"] ||= {}
+ old_generated["doors"][room_name] ||= {}
+ old_generated["doors"][room_name][door_name] ||= {}
+ old_generated["doors"][room_name][door_name]["location"] = location_id_by_name[location_name]
+ elsif not (old_generated.include? "doors" and old_generated["doors"].include? room_name and old_generated["doors"][room_name].include? door_name and old_generated["doors"][room_name][door_name].include? "location") then
+ old_generated["doors"] ||= {}
+ old_generated["doors"][room_name] ||= {}
+ old_generated["doors"][room_name][door_name] ||= {}
+ old_generated["doors"][room_name][door_name]["location"] = next_location_id
+
+ next_location_id += 1
+ end
+ end
+ end
+ end
+
+ if room_data.include? "panel_doors"
+ room_data["panel_doors"].each do |panel_door_name, panel_door|
+ unless old_generated.include? "panel_doors" and old_generated["panel_doors"].include? room_name and old_generated["panel_doors"][room_name].include? panel_door_name then
+ old_generated["panel_doors"] ||= {}
+ old_generated["panel_doors"][room_name] ||= {}
+ old_generated["panel_doors"][room_name][panel_door_name] = next_item_id
+
+ next_item_id += 1
+ end
+
+ if panel_door.include? "panel_group" and not panel_groups.include? panel_door["panel_group"] then
+ panel_groups.add(panel_door["panel_group"])
+
+ unless old_generated.include? "panel_groups" and old_generated["panel_groups"].include? panel_door["panel_group"] then
+ old_generated["panel_groups"] ||= {}
+ old_generated["panel_groups"][panel_door["panel_group"]] = next_item_id
+
+ next_item_id += 1
+ end
+ end
+ end
+ end
+
+ if room_data.include? "progression"
+ room_data["progression"].each do |progression_name, pdata|
+ unless old_generated.include? "progression" and old_generated["progression"].include? progression_name then
+ old_generated["progression"] ||= {}
+ old_generated["progression"][progression_name] = next_item_id
+
+ next_item_id += 1
+ end
+ end
+ end
+end
+
+File.write(outputpath, old_generated.to_yaml)
diff --git a/worlds/lingo/utils/pickle_static_data.py b/worlds/lingo/utils/pickle_static_data.py
new file mode 100644
index 000000000000..92bcb7a859ea
--- /dev/null
+++ b/worlds/lingo/utils/pickle_static_data.py
@@ -0,0 +1,609 @@
+from typing import Dict, List, Set, Optional
+
+import os
+import sys
+
+sys.path.append(os.path.join("worlds", "lingo"))
+sys.path.append(".")
+sys.path.append("..")
+from datatypes import Door, DoorType, EntranceType, Painting, Panel, PanelDoor, Progression, Room, RoomAndDoor,\
+ RoomAndPanel, RoomAndPanelDoor, RoomEntrance
+
+import hashlib
+import pickle
+import sys
+import Utils
+
+
+ALL_ROOMS: List[Room] = []
+DOORS_BY_ROOM: Dict[str, Dict[str, Door]] = {}
+PANELS_BY_ROOM: Dict[str, Dict[str, Panel]] = {}
+PANEL_DOORS_BY_ROOM: Dict[str, Dict[str, PanelDoor]] = {}
+PAINTINGS: Dict[str, Painting] = {}
+
+PROGRESSIVE_ITEMS: Set[str] = set()
+PROGRESSIVE_DOORS_BY_ROOM: Dict[str, Dict[str, Progression]] = {}
+PROGRESSIVE_PANELS_BY_ROOM: Dict[str, Dict[str, Progression]] = {}
+
+PAINTING_ENTRANCES: int = 0
+PAINTING_EXIT_ROOMS: Set[str] = set()
+PAINTING_EXITS: int = 0
+REQUIRED_PAINTING_ROOMS: List[str] = []
+REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS: List[str] = []
+
+SUNWARP_ENTRANCES: List[str] = ["", "", "", "", "", ""]
+SUNWARP_EXITS: List[str] = ["", "", "", "", "", ""]
+
+SPECIAL_ITEM_IDS: Dict[str, int] = {}
+PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
+DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
+DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {}
+DOOR_GROUP_ITEM_IDS: Dict[str, int] = {}
+PANEL_DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {}
+PANEL_GROUP_ITEM_IDS: Dict[str, int] = {}
+PROGRESSIVE_ITEM_IDS: Dict[str, int] = {}
+
+# This doesn't need to be stored in the datafile.
+PANEL_DOOR_BY_PANEL_BY_ROOM: Dict[str, Dict[str, str]] = {}
+
+
+def hash_file(path):
+ md5 = hashlib.md5()
+
+ with open(path, 'rb') as f:
+ content = f.read()
+ content = content.replace(b'\r\n', b'\n')
+ md5.update(content)
+
+ return md5.hexdigest()
+
+
+def load_static_data(ll1_path, ids_path):
+ global PAINTING_EXITS, SPECIAL_ITEM_IDS, PANEL_LOCATION_IDS, DOOR_LOCATION_IDS, DOOR_ITEM_IDS, \
+ DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS, PANEL_DOOR_ITEM_IDS, PANEL_GROUP_ITEM_IDS
+
+ # Load in all item and location IDs. These are broken up into groups based on the type of item/location.
+ with open(ids_path, "r") as file:
+ config = Utils.parse_yaml(file)
+
+ if "special_items" in config:
+ for item_name, item_id in config["special_items"].items():
+ SPECIAL_ITEM_IDS[item_name] = item_id
+
+ if "panels" in config:
+ for room_name in config["panels"].keys():
+ PANEL_LOCATION_IDS[room_name] = {}
+
+ for panel_name, location_id in config["panels"][room_name].items():
+ PANEL_LOCATION_IDS[room_name][panel_name] = location_id
+
+ if "doors" in config:
+ for room_name in config["doors"].keys():
+ DOOR_LOCATION_IDS[room_name] = {}
+ DOOR_ITEM_IDS[room_name] = {}
+
+ for door_name, door_data in config["doors"][room_name].items():
+ if "location" in door_data:
+ DOOR_LOCATION_IDS[room_name][door_name] = door_data["location"]
+
+ if "item" in door_data:
+ DOOR_ITEM_IDS[room_name][door_name] = door_data["item"]
+
+ if "door_groups" in config:
+ for item_name, item_id in config["door_groups"].items():
+ DOOR_GROUP_ITEM_IDS[item_name] = item_id
+
+ if "panel_doors" in config:
+ for room_name, panel_doors in config["panel_doors"].items():
+ PANEL_DOOR_ITEM_IDS[room_name] = {}
+
+ for panel_door, item_id in panel_doors.items():
+ PANEL_DOOR_ITEM_IDS[room_name][panel_door] = item_id
+
+ if "panel_groups" in config:
+ for item_name, item_id in config["panel_groups"].items():
+ PANEL_GROUP_ITEM_IDS[item_name] = item_id
+
+ if "progression" in config:
+ for item_name, item_id in config["progression"].items():
+ PROGRESSIVE_ITEM_IDS[item_name] = item_id
+
+ # Process the main world file.
+ with open(ll1_path, "r") as file:
+ config = Utils.parse_yaml(file)
+
+ for room_name, room_data in config.items():
+ process_room(room_name, room_data)
+
+ PAINTING_EXITS = len(PAINTING_EXIT_ROOMS)
+
+
+def process_single_entrance(source_room: str, room_name: str, door_obj) -> RoomEntrance:
+ global PAINTING_ENTRANCES, PAINTING_EXIT_ROOMS
+
+ entrance_type = EntranceType.NORMAL
+ if "painting" in door_obj and door_obj["painting"]:
+ entrance_type = EntranceType.PAINTING
+ elif "sunwarp" in door_obj and door_obj["sunwarp"]:
+ entrance_type = EntranceType.SUNWARP
+ elif "warp" in door_obj and door_obj["warp"]:
+ entrance_type = EntranceType.WARP
+ elif source_room == "Crossroads" and room_name == "Roof":
+ entrance_type = EntranceType.CROSSROADS_ROOF_ACCESS
+
+ if "painting" in door_obj and door_obj["painting"]:
+ PAINTING_EXIT_ROOMS.add(room_name)
+ PAINTING_ENTRANCES += 1
+
+ if "door" in door_obj:
+ return RoomEntrance(source_room, RoomAndDoor(
+ door_obj["room"] if "room" in door_obj else None,
+ door_obj["door"]
+ ), entrance_type)
+ else:
+ return RoomEntrance(source_room, None, entrance_type)
+
+
+def process_entrance(source_room, doors, room_obj):
+ # If the value of an entrance is just True, that means that the entrance is always accessible.
+ if doors is True:
+ room_obj.entrances.append(RoomEntrance(source_room, None, EntranceType.NORMAL))
+ elif isinstance(doors, dict):
+ # If the value of an entrance is a dictionary, that means the entrance requires a door to be accessible, is a
+ # painting-based entrance, or both.
+ room_obj.entrances.append(process_single_entrance(source_room, room_obj.name, doors))
+ else:
+ # If the value of an entrance is a list, then there are multiple possible doors that can give access to the
+ # entrance. If there are multiple connections with the same door (or lack of door) that differ only by entrance
+ # type, coalesce them into one entrance.
+ entrances: Dict[Optional[RoomAndDoor], EntranceType] = {}
+ for door in doors:
+ entrance = process_single_entrance(source_room, room_obj.name, door)
+ entrances[entrance.door] = entrances.get(entrance.door, EntranceType(0)) | entrance.type
+
+ for door, entrance_type in entrances.items():
+ room_obj.entrances.append(RoomEntrance(source_room, door, entrance_type))
+
+
+def process_panel_door(room_name, panel_door_name, panel_door_data):
+ global PANEL_DOORS_BY_ROOM, PANEL_DOOR_BY_PANEL_BY_ROOM
+
+ panels: List[RoomAndPanel] = list()
+ for panel in panel_door_data["panels"]:
+ if isinstance(panel, dict):
+ panels.append(RoomAndPanel(panel["room"], panel["panel"]))
+ else:
+ panels.append(RoomAndPanel(room_name, panel))
+
+ for panel in panels:
+ PANEL_DOOR_BY_PANEL_BY_ROOM.setdefault(panel.room, {})[panel.panel] = RoomAndPanelDoor(room_name,
+ panel_door_name)
+
+ if "item_name" in panel_door_data:
+ item_name = panel_door_data["item_name"]
+ else:
+ panel_per_room = dict()
+ for panel in panels:
+ panel_room_name = room_name if panel.room is None else panel.room
+ panel_per_room.setdefault(panel_room_name, []).append(panel.panel)
+
+ room_strs = list()
+ for door_room_str, door_panels_str in panel_per_room.items():
+ room_strs.append(door_room_str + " - " + ", ".join(door_panels_str))
+
+ if len(panels) == 1:
+ item_name = f"{room_strs[0]} (Panel)"
+ else:
+ item_name = " and ".join(room_strs) + " (Panels)"
+
+ if "panel_group" in panel_door_data:
+ panel_group = panel_door_data["panel_group"]
+ else:
+ panel_group = None
+
+ panel_door_obj = PanelDoor(item_name, panel_group)
+ PANEL_DOORS_BY_ROOM[room_name][panel_door_name] = panel_door_obj
+
+
+def process_panel(room_name, panel_name, panel_data):
+ global PANELS_BY_ROOM
+
+ # required_room can either be a single room or a list of rooms.
+ if "required_room" in panel_data:
+ if isinstance(panel_data["required_room"], list):
+ required_rooms = panel_data["required_room"]
+ else:
+ required_rooms = [panel_data["required_room"]]
+ else:
+ required_rooms = []
+
+ # required_door can either be a single door or a list of doors. For convenience, the room key for each door does not
+ # need to be specified if the door is in this room.
+ required_doors = list()
+ if "required_door" in panel_data:
+ if isinstance(panel_data["required_door"], dict):
+ door = panel_data["required_door"]
+ required_doors.append(RoomAndDoor(
+ door["room"] if "room" in door else None,
+ door["door"]
+ ))
+ else:
+ for door in panel_data["required_door"]:
+ required_doors.append(RoomAndDoor(
+ door["room"] if "room" in door else None,
+ door["door"]
+ ))
+
+ # required_panel can either be a single panel or a list of panels. For convenience, the room key for each panel does
+ # not need to be specified if the panel is in this room.
+ required_panels = list()
+ if "required_panel" in panel_data:
+ if isinstance(panel_data["required_panel"], dict):
+ other_panel = panel_data["required_panel"]
+ required_panels.append(RoomAndPanel(
+ other_panel["room"] if "room" in other_panel else None,
+ other_panel["panel"]
+ ))
+ else:
+ for other_panel in panel_data["required_panel"]:
+ required_panels.append(RoomAndPanel(
+ other_panel["room"] if "room" in other_panel else None,
+ other_panel["panel"]
+ ))
+
+ # colors can either be a single color or a list of colors.
+ if "colors" in panel_data:
+ if isinstance(panel_data["colors"], list):
+ colors = panel_data["colors"]
+ else:
+ colors = [panel_data["colors"]]
+ else:
+ colors = []
+
+ if "check" in panel_data:
+ check = panel_data["check"]
+ else:
+ check = False
+
+ if "event" in panel_data:
+ event = panel_data["event"]
+ else:
+ event = False
+
+ if "achievement" in panel_data:
+ achievement = True
+ else:
+ achievement = False
+
+ if "exclude_reduce" in panel_data:
+ exclude_reduce = panel_data["exclude_reduce"]
+ else:
+ exclude_reduce = False
+
+ if "non_counting" in panel_data:
+ non_counting = panel_data["non_counting"]
+ else:
+ non_counting = False
+
+ if room_name in PANEL_DOOR_BY_PANEL_BY_ROOM and panel_name in PANEL_DOOR_BY_PANEL_BY_ROOM[room_name]:
+ panel_door = PANEL_DOOR_BY_PANEL_BY_ROOM[room_name][panel_name]
+ else:
+ panel_door = None
+
+ if "location_name" in panel_data:
+ location_name = panel_data["location_name"]
+ else:
+ location_name = None
+
+ panel_obj = Panel(required_rooms, required_doors, required_panels, colors, check, event, exclude_reduce,
+ achievement, non_counting, panel_door, location_name)
+ PANELS_BY_ROOM[room_name][panel_name] = panel_obj
+
+
+def process_door(room_name, door_name, door_data):
+ global DOORS_BY_ROOM
+
+ # The item name associated with a door can be explicitly specified in the configuration. If it is not, it is
+ # generated from the room and door name.
+ if "item_name" in door_data:
+ item_name = door_data["item_name"]
+ else:
+ item_name = f"{room_name} - {door_name}"
+
+ if "skip_location" in door_data:
+ skip_location = door_data["skip_location"]
+ else:
+ skip_location = False
+
+ if "skip_item" in door_data:
+ skip_item = door_data["skip_item"]
+ else:
+ skip_item = False
+
+ if "event" in door_data:
+ event = door_data["event"]
+ else:
+ event = False
+
+ if "include_reduce" in door_data:
+ include_reduce = door_data["include_reduce"]
+ else:
+ include_reduce = False
+
+ if "door_group" in door_data:
+ door_group = door_data["door_group"]
+ else:
+ door_group = None
+
+ if "item_group" in door_data:
+ item_group = door_data["item_group"]
+ else:
+ item_group = None
+
+ # panels is a list of panels. Each panel can either be a simple string (the name of a panel in the current room) or
+ # a dictionary specifying a panel in a different room.
+ if "panels" in door_data:
+ panels = list()
+ for panel in door_data["panels"]:
+ if isinstance(panel, dict):
+ panels.append(RoomAndPanel(panel["room"], panel["panel"]))
+ else:
+ panels.append(RoomAndPanel(None, panel))
+ else:
+ skip_location = True
+ panels = []
+
+ # The location name associated with a door can be explicitly specified in the configuration. If it is not, then the
+ # name is generated using a combination of all of the panels that would ordinarily open the door. This can get quite
+ # messy if there are a lot of panels, especially if panels from multiple rooms are involved, so in these cases it
+ # would be better to specify a name.
+ if "location_name" in door_data:
+ location_name = door_data["location_name"]
+ elif skip_location is False:
+ panel_per_room = dict()
+ for panel in panels:
+ panel_room_name = room_name if panel.room is None else panel.room
+ panel_per_room.setdefault(panel_room_name, []).append(panel.panel)
+
+ room_strs = list()
+ for door_room_str, door_panels_str in panel_per_room.items():
+ room_strs.append(door_room_str + " - " + ", ".join(door_panels_str))
+
+ location_name = " and ".join(room_strs)
+ else:
+ location_name = None
+
+ # The id field can be a single item, or a list of door IDs, in the event that the item for this logical door should
+ # open more than one actual in-game door.
+ has_doors = "id" in door_data
+
+ # The painting_id field can be a single item, or a list of painting IDs, in the event that the item for this logical
+ # door should move more than one actual in-game painting.
+ if "painting_id" in door_data:
+ if isinstance(door_data["painting_id"], list):
+ painting_ids = door_data["painting_id"]
+ else:
+ painting_ids = [door_data["painting_id"]]
+ else:
+ painting_ids = []
+
+ door_type = DoorType.NORMAL
+ if room_name == "Sunwarps":
+ door_type = DoorType.SUNWARP
+ elif room_name == "Pilgrim Antechamber" and door_name == "Sun Painting":
+ door_type = DoorType.SUN_PAINTING
+
+ door_obj = Door(door_name, item_name, location_name, panels, skip_location, skip_item, has_doors,
+ painting_ids, event, door_group, include_reduce, door_type, item_group)
+
+ DOORS_BY_ROOM[room_name][door_name] = door_obj
+
+
+def process_painting(room_name, painting_data):
+ global PAINTINGS, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
+
+ # Read in information about this painting and store it in an object.
+ painting_id = painting_data["id"]
+
+ if "disable" in painting_data:
+ disable_painting = painting_data["disable"]
+ else:
+ disable_painting = False
+
+ if "required" in painting_data:
+ required_painting = painting_data["required"]
+ if required_painting:
+ REQUIRED_PAINTING_ROOMS.append(room_name)
+ else:
+ required_painting = False
+
+ if "required_when_no_doors" in painting_data:
+ rwnd = painting_data["required_when_no_doors"]
+ if rwnd:
+ REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS.append(room_name)
+ else:
+ rwnd = False
+
+ if "exit_only" in painting_data:
+ exit_only = painting_data["exit_only"]
+ else:
+ exit_only = False
+
+ if "enter_only" in painting_data:
+ enter_only = painting_data["enter_only"]
+ else:
+ enter_only = False
+
+ if "req_blocked" in painting_data:
+ req_blocked = painting_data["req_blocked"]
+ else:
+ req_blocked = False
+
+ if "req_blocked_when_no_doors" in painting_data:
+ req_blocked_when_no_doors = painting_data["req_blocked_when_no_doors"]
+ else:
+ req_blocked_when_no_doors = False
+
+ required_door = None
+ if "required_door" in painting_data:
+ door = painting_data["required_door"]
+ required_door = RoomAndDoor(
+ door["room"] if "room" in door else room_name,
+ door["door"]
+ )
+
+ painting_obj = Painting(painting_id, room_name, enter_only, exit_only,
+ required_painting, rwnd, required_door, disable_painting, req_blocked,
+ req_blocked_when_no_doors)
+ PAINTINGS[painting_id] = painting_obj
+
+
+def process_sunwarp(room_name, sunwarp_data):
+ global SUNWARP_ENTRANCES, SUNWARP_EXITS
+
+ if sunwarp_data["direction"] == "enter":
+ SUNWARP_ENTRANCES[sunwarp_data["dots"] - 1] = room_name
+ else:
+ SUNWARP_EXITS[sunwarp_data["dots"] - 1] = room_name
+
+
+def process_progressive_door(room_name, progression_name, progression_doors):
+ global PROGRESSIVE_ITEMS, PROGRESSIVE_DOORS_BY_ROOM
+
+ # Progressive items are configured as a list of doors.
+ PROGRESSIVE_ITEMS.add(progression_name)
+
+ progression_index = 1
+ for door in progression_doors:
+ if isinstance(door, Dict):
+ door_room = door["room"]
+ door_door = door["door"]
+ else:
+ door_room = room_name
+ door_door = door
+
+ room_progressions = PROGRESSIVE_DOORS_BY_ROOM.setdefault(door_room, {})
+ room_progressions[door_door] = Progression(progression_name, progression_index)
+ progression_index += 1
+
+
+def process_progressive_panel(room_name, progression_name, progression_panel_doors):
+ global PROGRESSIVE_ITEMS, PROGRESSIVE_PANELS_BY_ROOM
+
+ # Progressive items are configured as a list of panel doors.
+ PROGRESSIVE_ITEMS.add(progression_name)
+
+ progression_index = 1
+ for panel_door in progression_panel_doors:
+ if isinstance(panel_door, Dict):
+ panel_door_room = panel_door["room"]
+ panel_door_door = panel_door["panel_door"]
+ else:
+ panel_door_room = room_name
+ panel_door_door = panel_door
+
+ room_progressions = PROGRESSIVE_PANELS_BY_ROOM.setdefault(panel_door_room, {})
+ room_progressions[panel_door_door] = Progression(progression_name, progression_index)
+ progression_index += 1
+
+
+def process_room(room_name, room_data):
+ global ALL_ROOMS
+
+ room_obj = Room(room_name, [])
+
+ if "entrances" in room_data:
+ for source_room, doors in room_data["entrances"].items():
+ process_entrance(source_room, doors, room_obj)
+
+ if "panel_doors" in room_data:
+ PANEL_DOORS_BY_ROOM[room_name] = dict()
+
+ for panel_door_name, panel_door_data in room_data["panel_doors"].items():
+ process_panel_door(room_name, panel_door_name, panel_door_data)
+
+ if "panels" in room_data:
+ PANELS_BY_ROOM[room_name] = dict()
+
+ for panel_name, panel_data in room_data["panels"].items():
+ process_panel(room_name, panel_name, panel_data)
+
+ if "doors" in room_data:
+ DOORS_BY_ROOM[room_name] = dict()
+
+ for door_name, door_data in room_data["doors"].items():
+ process_door(room_name, door_name, door_data)
+
+ if "paintings" in room_data:
+ for painting_data in room_data["paintings"]:
+ process_painting(room_name, painting_data)
+
+ if "sunwarps" in room_data:
+ for sunwarp_data in room_data["sunwarps"]:
+ process_sunwarp(room_name, sunwarp_data)
+
+ if "progression" in room_data:
+ for progression_name, pdata in room_data["progression"].items():
+ if "doors" in pdata:
+ process_progressive_door(room_name, progression_name, pdata["doors"])
+ if "panel_doors" in pdata:
+ process_progressive_panel(room_name, progression_name, pdata["panel_doors"])
+
+ ALL_ROOMS.append(room_obj)
+
+
+if __name__ == '__main__':
+ if len(sys.argv) == 1:
+ ll1_path = os.path.join("worlds", "lingo", "data", "LL1.yaml")
+ ids_path = os.path.join("worlds", "lingo", "data", "ids.yaml")
+ output_path = os.path.join("worlds", "lingo", "data", "generated.dat")
+ elif len(sys.argv) != 4:
+ print("")
+ print("Usage: python worlds/lingo/utils/pickle_static_data.py [args]")
+ print("Arguments:")
+ print(" - Path to LL1.yaml")
+ print(" - Path to ids.yaml")
+ print(" - Path to output file")
+
+ exit()
+ else:
+ ll1_path = sys.argv[1]
+ ids_path = sys.argv[2]
+ output_path = sys.argv[3]
+
+ load_static_data(ll1_path, ids_path)
+
+ hashes = {
+ "LL1.yaml": hash_file(ll1_path),
+ "ids.yaml": hash_file(ids_path),
+ }
+
+ pickdata = {
+ "HASHES": hashes,
+ "PAINTINGS": PAINTINGS,
+ "ALL_ROOMS": ALL_ROOMS,
+ "DOORS_BY_ROOM": DOORS_BY_ROOM,
+ "PANELS_BY_ROOM": PANELS_BY_ROOM,
+ "PANEL_DOORS_BY_ROOM": PANEL_DOORS_BY_ROOM,
+ "PROGRESSIVE_ITEMS": PROGRESSIVE_ITEMS,
+ "PROGRESSIVE_DOORS_BY_ROOM": PROGRESSIVE_DOORS_BY_ROOM,
+ "PROGRESSIVE_PANELS_BY_ROOM": PROGRESSIVE_PANELS_BY_ROOM,
+ "PAINTING_ENTRANCES": PAINTING_ENTRANCES,
+ "PAINTING_EXIT_ROOMS": PAINTING_EXIT_ROOMS,
+ "PAINTING_EXITS": PAINTING_EXITS,
+ "REQUIRED_PAINTING_ROOMS": REQUIRED_PAINTING_ROOMS,
+ "REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS": REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS,
+ "SUNWARP_ENTRANCES": SUNWARP_ENTRANCES,
+ "SUNWARP_EXITS": SUNWARP_EXITS,
+ "SPECIAL_ITEM_IDS": SPECIAL_ITEM_IDS,
+ "PANEL_LOCATION_IDS": PANEL_LOCATION_IDS,
+ "DOOR_LOCATION_IDS": DOOR_LOCATION_IDS,
+ "DOOR_ITEM_IDS": DOOR_ITEM_IDS,
+ "DOOR_GROUP_ITEM_IDS": DOOR_GROUP_ITEM_IDS,
+ "PANEL_DOOR_ITEM_IDS": PANEL_DOOR_ITEM_IDS,
+ "PANEL_GROUP_ITEM_IDS": PANEL_GROUP_ITEM_IDS,
+ "PROGRESSIVE_ITEM_IDS": PROGRESSIVE_ITEM_IDS,
+ }
+
+ with open(output_path, "wb") as file:
+ pickle.dump(pickdata, file)
diff --git a/worlds/lingo/utils/validate_config.rb b/worlds/lingo/utils/validate_config.rb
new file mode 100644
index 000000000000..70f7fc2cf659
--- /dev/null
+++ b/worlds/lingo/utils/validate_config.rb
@@ -0,0 +1,459 @@
+# Script to validate a level config file. This checks that the names used within
+# the file are consistent. It also checks that the panel and door IDs mentioned
+# all exist in the map file.
+#
+# Usage: validate_config.rb [config file] [map file]
+
+require 'set'
+require 'yaml'
+
+configpath = ARGV[0]
+idspath = ARGV[1]
+mappath = ARGV[2]
+
+panels = Set["Countdown Panels/Panel_1234567890_wanderlust"]
+doors = Set["Naps Room Doors/Door_hider_new1", "Tower Room Area Doors/Door_wanderer_entrance"]
+paintings = Set[]
+
+File.readlines(mappath).each do |line|
+ line.match(/node name=\"(.*)\" parent=\"Panels\/(.*)\" instance/) do |m|
+ panels.add(m[2] + "/" + m[1])
+ end
+ line.match(/node name=\"(.*)\" parent=\"Doors\/(.*)\" instance/) do |m|
+ doors.add(m[2] + "/" + m[1])
+ end
+ line.match(/node name=\"(.*)\" parent=\"Decorations\/Paintings\" instance/) do |m|
+ paintings.add(m[1])
+ end
+ line.match(/node name=\"(.*)\" parent=\"Decorations\/EndPanel\" instance/) do |m|
+ panels.add("EndPanel/" + m[1])
+ end
+end
+
+configured_rooms = Set["Menu"]
+configured_doors = Set[]
+configured_panels = Set[]
+configured_panel_doors = Set[]
+
+mentioned_rooms = Set[]
+mentioned_doors = Set[]
+mentioned_panels = Set[]
+mentioned_panel_doors = Set[]
+mentioned_sunwarp_entrances = Set[]
+mentioned_sunwarp_exits = Set[]
+mentioned_paintings = Set[]
+
+door_groups = {}
+panel_groups = {}
+
+directives = Set["entrances", "panels", "doors", "panel_doors", "paintings", "sunwarps", "progression"]
+panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt", "location_name"]
+door_directives = Set["id", "painting_id", "panels", "item_name", "item_group", "location_name", "skip_location", "skip_item", "door_group", "include_reduce", "event", "warp_id"]
+panel_door_directives = Set["panels", "item_name", "panel_group"]
+painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"]
+
+non_counting = 0
+
+ids = YAML.load_file(idspath)
+
+config = YAML.load_file(configpath)
+config.each do |room_name, room|
+ configured_rooms.add(room_name)
+
+ used_directives = Set[]
+ room.each_key do |key|
+ used_directives.add(key)
+ end
+ diff_directives = used_directives - directives
+ unless diff_directives.empty? then
+ puts("#{room_name} has the following invalid top-level directives: #{diff_directives.to_s}")
+ end
+
+ (room["entrances"] || {}).each do |source_room, entrance|
+ mentioned_rooms.add(source_room)
+
+ entrances = []
+ if entrance.kind_of? Hash
+ entrances = [entrance]
+ elsif entrance.kind_of? Array
+ entrances = entrance
+ end
+
+ entrances.each do |e|
+ if e.include?("door") then
+ entrance_room = e.include?("room") ? e["room"] : room_name
+ mentioned_rooms.add(entrance_room)
+ mentioned_doors.add(entrance_room + " - " + e["door"])
+ end
+ end
+ end
+
+ (room["panels"] || {}).each do |panel_name, panel|
+ unless panel_name.kind_of? String then
+ puts "#{room_name} has an invalid panel name"
+ end
+
+ configured_panels.add(room_name + " - " + panel_name)
+
+ if panel.include?("id")
+ panel_ids = []
+ if panel["id"].kind_of? Array
+ panel_ids = panel["id"]
+ else
+ panel_ids = [panel["id"]]
+ end
+
+ panel_ids.each do |panel_id|
+ unless panels.include? panel_id then
+ puts "#{room_name} - #{panel_name} :::: Invalid Panel ID #{panel_id}"
+ end
+ end
+ else
+ puts "#{room_name} - #{panel_name} :::: Panel is missing an ID"
+ end
+
+ if panel.include?("required_room")
+ required_rooms = []
+ if panel["required_room"].kind_of? Array
+ required_rooms = panel["required_room"]
+ else
+ required_rooms = [panel["required_room"]]
+ end
+
+ required_rooms.each do |required_room|
+ mentioned_rooms.add(required_room)
+ end
+ end
+
+ if panel.include?("required_door")
+ required_doors = []
+ if panel["required_door"].kind_of? Array
+ required_doors = panel["required_door"]
+ else
+ required_doors = [panel["required_door"]]
+ end
+
+ required_doors.each do |required_door|
+ other_room = required_door.include?("room") ? required_door["room"] : room_name
+ mentioned_rooms.add(other_room)
+ mentioned_doors.add("#{other_room} - #{required_door["door"]}")
+ end
+ end
+
+ if panel.include?("required_panel")
+ required_panels = []
+ if panel["required_panel"].kind_of? Array
+ required_panels = panel["required_panel"]
+ else
+ required_panels = [panel["required_panel"]]
+ end
+
+ required_panels.each do |required_panel|
+ other_room = required_panel.include?("room") ? required_panel["room"] : room_name
+ mentioned_rooms.add(other_room)
+ mentioned_panels.add("#{other_room} - #{required_panel["panel"]}")
+ end
+ end
+
+ unless panel.include?("tag") then
+ puts "#{room_name} - #{panel_name} :::: Panel is missing a tag"
+ end
+
+ if panel.include?("non_counting") then
+ non_counting += 1
+ end
+
+ bad_subdirectives = []
+ panel.keys.each do |key|
+ unless panel_directives.include?(key) then
+ bad_subdirectives << key
+ end
+ end
+ unless bad_subdirectives.empty? then
+ puts "#{room_name} - #{panel_name} :::: Panel has the following invalid subdirectives: #{bad_subdirectives.join(", ")}"
+ end
+
+ unless ids.include?("panels") and ids["panels"].include?(room_name) and ids["panels"][room_name].include?(panel_name)
+ puts "#{room_name} - #{panel_name} :::: Panel is missing a location ID"
+ end
+ end
+
+ (room["doors"] || {}).each do |door_name, door|
+ configured_doors.add("#{room_name} - #{door_name}")
+
+ if door.include?("id")
+ door_ids = []
+ if door["id"].kind_of? Array
+ door_ids = door["id"]
+ else
+ door_ids = [door["id"]]
+ end
+
+ door_ids.each do |door_id|
+ unless doors.include? door_id then
+ puts "#{room_name} - #{door_name} :::: Invalid Door ID #{door_id}"
+ end
+ end
+ end
+
+ if door.include?("painting_id")
+ painting_ids = []
+ if door["painting_id"].kind_of? Array
+ painting_ids = door["painting_id"]
+ else
+ painting_ids = [door["painting_id"]]
+ end
+
+ painting_ids.each do |painting_id|
+ unless paintings.include? painting_id then
+ puts "#{room_name} - #{door_name} :::: Invalid Painting ID #{painting_id}"
+ end
+ end
+ end
+
+ if not door.include?("id") and not door.include?("painting_id") and not door.include?("warp_id") and not door["skip_item"] and not door["event"] then
+ puts "#{room_name} - #{door_name} :::: Should be marked skip_item or event if there are no doors, paintings, or warps"
+ end
+
+ if door.include?("panels")
+ door["panels"].each do |panel|
+ if panel.kind_of? Hash then
+ other_room = panel.include?("room") ? panel["room"] : room_name
+ mentioned_panels.add("#{other_room} - #{panel["panel"]}")
+ else
+ other_room = panel.include?("room") ? panel["room"] : room_name
+ mentioned_panels.add("#{room_name} - #{panel}")
+ end
+ end
+ elsif not door["skip_location"]
+ puts "#{room_name} - #{door_name} :::: Should be marked skip_location if there are no panels"
+ end
+
+ if door.include?("group")
+ door_groups[door["group"]] ||= 0
+ door_groups[door["group"]] += 1
+ end
+
+ bad_subdirectives = []
+ door.keys.each do |key|
+ unless door_directives.include?(key) then
+ bad_subdirectives << key
+ end
+ end
+ unless bad_subdirectives.empty? then
+ puts "#{room_name} - #{door_name} :::: Door has the following invalid subdirectives: #{bad_subdirectives.join(", ")}"
+ end
+
+ unless door["skip_item"] or door["event"]
+ unless ids.include?("doors") and ids["doors"].include?(room_name) and ids["doors"][room_name].include?(door_name) and ids["doors"][room_name][door_name].include?("item")
+ puts "#{room_name} - #{door_name} :::: Door is missing an item ID"
+ end
+ end
+
+ unless door["skip_location"] or door["event"]
+ unless ids.include?("doors") and ids["doors"].include?(room_name) and ids["doors"][room_name].include?(door_name) and ids["doors"][room_name][door_name].include?("location")
+ puts "#{room_name} - #{door_name} :::: Door is missing a location ID"
+ end
+ end
+ end
+
+ (room["panel_doors"] || {}).each do |panel_door_name, panel_door|
+ configured_panel_doors.add("#{room_name} - #{panel_door_name}")
+
+ if panel_door.include?("panels")
+ panel_door["panels"].each do |panel|
+ if panel.kind_of? Hash then
+ other_room = panel.include?("room") ? panel["room"] : room_name
+ mentioned_panels.add("#{other_room} - #{panel["panel"]}")
+ else
+ other_room = panel.include?("room") ? panel["room"] : room_name
+ mentioned_panels.add("#{room_name} - #{panel}")
+ end
+ end
+ else
+ puts "#{room_name} - #{panel_door_name} :::: Missing panels field"
+ end
+
+ if panel_door.include?("panel_group")
+ panel_groups[panel_door["panel_group"]] ||= 0
+ panel_groups[panel_door["panel_group"]] += 1
+ end
+
+ bad_subdirectives = []
+ panel_door.keys.each do |key|
+ unless panel_door_directives.include?(key) then
+ bad_subdirectives << key
+ end
+ end
+ unless bad_subdirectives.empty? then
+ puts "#{room_name} - #{panel_door_name} :::: Panel door has the following invalid subdirectives: #{bad_subdirectives.join(", ")}"
+ end
+
+ unless ids.include?("panel_doors") and ids["panel_doors"].include?(room_name) and ids["panel_doors"][room_name].include?(panel_door_name)
+ puts "#{room_name} - #{panel_door_name} :::: Panel door is missing an item ID"
+ end
+ end
+
+ (room["paintings"] || []).each do |painting|
+ if painting.include?("id") and painting["id"].kind_of? String then
+ unless paintings.include? painting["id"] then
+ puts "#{room_name} :::: Invalid Painting ID #{painting["id"]}"
+ end
+
+ if mentioned_paintings.include?(painting["id"]) then
+ puts "Painting #{painting["id"]} is mentioned more than once"
+ else
+ mentioned_paintings.add(painting["id"])
+ end
+ else
+ puts "#{room_name} :::: Painting is missing an ID"
+ end
+
+ if painting["disable"] then
+ # We're good.
+ next
+ end
+
+ if painting.include?("orientation") then
+ unless ["north", "south", "east", "west"].include? painting["orientation"] then
+ puts "#{room_name} - #{painting["id"] || "painting"} :::: Invalid orientation #{painting["orientation"]}"
+ end
+ else
+ puts "#{room_name} :::: Painting is missing an orientation"
+ end
+
+ if painting.include?("required_door")
+ other_room = painting["required_door"].include?("room") ? painting["required_door"]["room"] : room_name
+ mentioned_doors.add("#{other_room} - #{painting["required_door"]["door"]}")
+
+ unless painting["enter_only"] then
+ puts "#{room_name} - #{painting["id"] || "painting"} :::: Should be marked enter_only if there is a required_door"
+ end
+ end
+
+ bad_subdirectives = []
+ painting.keys.each do |key|
+ unless painting_directives.include?(key) then
+ bad_subdirectives << key
+ end
+ end
+ unless bad_subdirectives.empty? then
+ puts "#{room_name} - #{painting["id"] || "painting"} :::: Painting has the following invalid subdirectives: #{bad_subdirectives.join(", ")}"
+ end
+ end
+
+ (room["sunwarps"] || []).each do |sunwarp|
+ if sunwarp.include? "dots" and sunwarp.include? "direction" then
+ if sunwarp["dots"] < 1 or sunwarp["dots"] > 6 then
+ puts "#{room_name} :::: Contains a sunwarp with an invalid dots value"
+ end
+
+ if sunwarp["direction"] == "enter" then
+ if mentioned_sunwarp_entrances.include? sunwarp["dots"] then
+ puts "Multiple #{sunwarp["dots"]} sunwarp entrances were found"
+ else
+ mentioned_sunwarp_entrances.add(sunwarp["dots"])
+ end
+ elsif sunwarp["direction"] == "exit" then
+ if mentioned_sunwarp_exits.include? sunwarp["dots"] then
+ puts "Multiple #{sunwarp["dots"]} sunwarp exits were found"
+ else
+ mentioned_sunwarp_exits.add(sunwarp["dots"])
+ end
+ else
+ puts "#{room_name} :::: Contains a sunwarp with an invalid direction value"
+ end
+ else
+ puts "#{room_name} :::: Contains a sunwarp without a dots and direction"
+ end
+ end
+
+ (room["progression"] || {}).each do |progression_name, pdata|
+ if pdata.include? "doors" then
+ pdata["doors"].each do |door|
+ if door.kind_of? Hash then
+ mentioned_doors.add("#{door["room"]} - #{door["door"]}")
+ else
+ mentioned_doors.add("#{room_name} - #{door}")
+ end
+ end
+ end
+
+ if pdata.include? "panel_doors" then
+ pdata["panel_doors"].each do |panel_door|
+ if panel_door.kind_of? Hash then
+ mentioned_panel_doors.add("#{panel_door["room"]} - #{panel_door["panel_door"]}")
+ else
+ mentioned_panel_doors.add("#{room_name} - #{panel_door}")
+ end
+ end
+ end
+
+ unless ids.include?("progression") and ids["progression"].include?(progression_name)
+ puts "#{room_name} - #{progression_name} :::: Progression is missing an item ID"
+ end
+ end
+end
+
+errored_rooms = mentioned_rooms - configured_rooms
+unless errored_rooms.empty? then
+ puts "The following rooms are mentioned but do not exist: " + errored_rooms.to_s
+end
+
+errored_panels = mentioned_panels - configured_panels
+unless errored_panels.empty? then
+ puts "The following panels are mentioned but do not exist: " + errored_panels.to_s
+end
+
+errored_doors = mentioned_doors - configured_doors
+unless errored_doors.empty? then
+ puts "The following doors are mentioned but do not exist: " + errored_doors.to_s
+end
+
+errored_panel_doors = mentioned_panel_doors - configured_panel_doors
+unless errored_panel_doors.empty? then
+ puts "The following panel doors are mentioned but do not exist: " + errored_panel_doors.to_s
+end
+
+door_groups.each do |group,num|
+ if num == 1 then
+ puts "Door group \"#{group}\" only has one door in it"
+ end
+
+ unless ids.include?("door_groups") and ids["door_groups"].include?(group)
+ puts "#{group} :::: Door group is missing an item ID"
+ end
+end
+
+panel_groups.each do |group,num|
+ if num == 1 then
+ puts "Panel group \"#{group}\" only has one panel in it"
+ end
+
+ unless ids.include?("panel_groups") and ids["panel_groups"].include?(group)
+ puts "#{group} :::: Panel group is missing an item ID"
+ end
+end
+
+slashed_rooms = configured_rooms.select do |room|
+ room.include? "/"
+end
+unless slashed_rooms.empty? then
+ puts "The following rooms have slashes in their names: " + slashed_rooms.to_s
+end
+
+slashed_panels = configured_panels.select do |panel|
+ panel.include? "/"
+end
+unless slashed_panels.empty? then
+ puts "The following panels have slashes in their names: " + slashed_panels.to_s
+end
+
+slashed_doors = configured_doors.select do |door|
+ door.include? "/"
+end
+unless slashed_doors.empty? then
+ puts "The following doors have slashes in their names: " + slashed_doors.to_s
+end
+
+puts "#{configured_panels.size} panels (#{non_counting} non counting)"
diff --git a/worlds/lufia2ac/Client.py b/worlds/lufia2ac/Client.py
index bc0cb6a7d8dd..633bd93ef0c9 100644
--- a/worlds/lufia2ac/Client.py
+++ b/worlds/lufia2ac/Client.py
@@ -30,6 +30,7 @@
class L2ACSNIClient(SNIClient):
game: str = "Lufia II Ancient Cave"
+ patch_suffix = ".apl2ac"
async def validate_rom(self, ctx: SNIContext) -> bool:
from SNIClient import snes_read
@@ -113,14 +114,15 @@ async def game_watcher(self, ctx: SNIContext) -> None:
}],
}])
- total_blue_chests_checked: int = min(sum(blue_chests_checked.values()), BlueChestCount.range_end)
+ total_blue_chests_checked: int = min(sum(blue_chests_checked.values()), BlueChestCount.overall_max)
snes_buffered_write(ctx, L2AC_TX_ADDR + 8, total_blue_chests_checked.to_bytes(2, "little"))
location_ids: List[int] = [locations_start_id + i for i in range(total_blue_chests_checked)]
- loc_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR + 32, snes_other_locations_checked * 2)
- if loc_data is not None:
- location_ids.extend(locations_start_id + int.from_bytes(loc_data[2 * i:2 * i + 2], "little")
- for i in range(snes_other_locations_checked))
+ if snes_other_locations_checked:
+ loc_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR + 32, snes_other_locations_checked * 2)
+ if loc_data is not None:
+ location_ids.extend(locations_start_id + int.from_bytes(loc_data[2 * i:2 * i + 2], "little")
+ for i in range(snes_other_locations_checked))
if new_location_ids := [loc_id for loc_id in location_ids if loc_id not in ctx.locations_checked]:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": new_location_ids}])
@@ -145,9 +147,9 @@ async def game_watcher(self, ctx: SNIContext) -> None:
snes_items_received += 1
snes_logger.info("Received %s from %s (%s) (%d/%d in list)" % (
- ctx.item_names[item.item],
+ ctx.item_names.lookup_in_game(item.item),
ctx.player_names[item.player],
- ctx.location_names[item.location],
+ ctx.location_names.lookup_in_slot(item.location, item.player),
snes_items_received, len(ctx.items_received)))
snes_buffered_write(ctx, L2AC_RX_ADDR + 2 * (snes_items_received + 1), item_code.to_bytes(2, "little"))
snes_buffered_write(ctx, L2AC_RX_ADDR, snes_items_received.to_bytes(2, "little"))
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/Locations.py b/worlds/lufia2ac/Locations.py
index 2f433f72e2ae..510ecbbbf7f4 100644
--- a/worlds/lufia2ac/Locations.py
+++ b/worlds/lufia2ac/Locations.py
@@ -6,7 +6,7 @@
start_id: int = 0xAC0000
l2ac_location_name_to_id: Dict[str, int] = {
- **{f"Blue chest {i + 1}": (start_id + i) for i in range(BlueChestCount.range_end + 7 + 6)},
+ **{f"Blue chest {i + 1}": (start_id + i) for i in range(BlueChestCount.overall_max)},
**{f"Iris treasure {i + 1}": (start_id + 0x039C + i) for i in range(9)},
"Boss": start_id + 0x01C2,
}
diff --git a/worlds/lufia2ac/Options.py b/worlds/lufia2ac/Options.py
index c3bbadc9ba00..1b3a39ddeb5f 100644
--- a/worlds/lufia2ac/Options.py
+++ b/worlds/lufia2ac/Options.py
@@ -1,12 +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, Range, SpecialRange, TextChoice, Toggle
+from Options import AssembleOptions, Choice, DeathLink, ItemDict, NamedRange, OptionDict, PerGameCommonOptions, Range, \
+ TextChoice, Toggle
from .Enemies import enemy_name_to_sprite
+from .Items import ItemType, l2ac_item_table
if TYPE_CHECKING:
from BaseClasses import PlandoOptions
@@ -117,6 +121,7 @@ class BlueChestCount(Range):
range_start = 10
range_end = 100
default = 25
+ overall_max = range_end + 7 + 6 # Have to account for capsule monster and party member items
class Boss(RandomGroupsChoice):
@@ -250,7 +255,7 @@ class CapsuleCravingsJPStyle(Toggle):
display_name = "Capsule cravings JP style"
-class CapsuleStartingForm(SpecialRange):
+class CapsuleStartingForm(NamedRange):
"""The starting form of your capsule monsters.
Supported values: 1 – 4, m
@@ -261,7 +266,6 @@ class CapsuleStartingForm(SpecialRange):
range_start = 1
range_end = 5
default = 1
- special_range_cutoff = 1
special_range_names = {
"default": 1,
"m": 5,
@@ -275,7 +279,7 @@ def unlock(self) -> int:
return self.value - 1
-class CapsuleStartingLevel(LevelMixin, SpecialRange):
+class CapsuleStartingLevel(LevelMixin, NamedRange):
"""The starting level of your capsule monsters.
Can be set to the special value party_starting_level to make it the same value as the party_starting_level option.
@@ -284,10 +288,9 @@ class CapsuleStartingLevel(LevelMixin, SpecialRange):
"""
display_name = "Capsule monster starting level"
- range_start = 0
+ range_start = 1
range_end = 99
default = 1
- special_range_cutoff = 1
special_range_names = {
"default": 1,
"party_starting_level": 0,
@@ -557,6 +560,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.
@@ -571,6 +593,20 @@ class HealingFloorChance(Range):
default = 16
+class InactiveExpGain(Choice):
+ """The rate at which characters not currently in the active party gain EXP.
+
+ Supported values: disabled, half, full
+ Default value: disabled (same as in an unmodified game)
+ """
+
+ display_name = "Inactive character EXP gain"
+ option_disabled = 0
+ option_half = 50
+ option_full = 100
+ default = option_disabled
+
+
class InitialFloor(Range):
"""The initial floor, where you begin your journey.
@@ -661,6 +697,104 @@ class RunSpeed(Choice):
default = option_disabled
+class ShopInterval(NamedRange):
+ """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 = 1
+ range_end = 10
+ default = 0
+ 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.
@@ -685,7 +819,7 @@ class ShufflePartyMembers(Toggle):
false — all 6 optional party members are present in the cafe and can be recruited right away
true — only Maxim is available from the start; 6 new "items" are added to your pool and shuffled into the
multiworld; when one of these items is found, the corresponding party member is unlocked for you to use.
- While cave diving, you can add newly unlocked ones to your party by using the character items from the inventory
+ While cave diving, you can add or remove unlocked party members by using the character items from the inventory
Default value: false (same as in an unmodified game)
"""
@@ -697,7 +831,7 @@ def unlock(self) -> int:
@dataclass
-class L2ACOptions:
+class L2ACOptions(PerGameCommonOptions):
blue_chest_chance: BlueChestChance
blue_chest_count: BlueChestCount
boss: Boss
@@ -716,12 +850,16 @@ class L2ACOptions:
final_floor: FinalFloor
gear_variety_after_b9: GearVarietyAfterB9
goal: Goal
+ gold_modifier: GoldModifier
healing_floor_chance: HealingFloorChance
+ inactive_exp_gain: InactiveExpGain
initial_floor: InitialFloor
iris_floor_chance: IrisFloorChance
iris_treasures_required: IrisTreasuresRequired
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/Rom.py b/worlds/lufia2ac/Rom.py
index 1da8d235a691..446668d39243 100644
--- a/worlds/lufia2ac/Rom.py
+++ b/worlds/lufia2ac/Rom.py
@@ -3,7 +3,7 @@
from typing import Optional
import Utils
-from Utils import OptionsType
+from settings import get_settings
from worlds.Files import APDeltaPatch
L2USHASH: str = "6efc477d6203ed2b3b9133c1cd9e9c5d"
@@ -35,9 +35,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
def get_base_rom_path(file_name: str = "") -> str:
- options: OptionsType = Utils.get_options()
if not file_name:
- file_name = options["lufia2ac_options"]["rom_file"]
+ file_name = get_settings()["lufia2ac_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.user_path(file_name)
return file_name
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 f14048a2c432..6433452cefea 100644
--- a/worlds/lufia2ac/__init__.py
+++ b/worlds/lufia2ac/__init__.py
@@ -2,21 +2,21 @@
import itertools
import os
from enum import IntFlag
-from typing import Any, ClassVar, Dict, get_type_hints, Iterator, List, Set, Tuple
+from typing import Any, ClassVar, Dict, Iterator, List, Set, Tuple, Type
import settings
from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region, Tutorial
-from Options import AssembleOptions
+from Options import PerGameCommonOptions
from Utils import __version__
from worlds.AutoWorld import WebWorld, World
-from worlds.generic.Rules import add_rule, set_rule
+from worlds.generic.Rules import add_rule, CollectionRule, set_rule
from .Client import L2ACSNIClient # noqa: F401
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
@@ -54,7 +54,8 @@ class L2ACWorld(World):
game: ClassVar[str] = "Lufia II Ancient Cave"
web: ClassVar[WebWorld] = L2ACWeb()
- option_definitions: ClassVar[Dict[str, AssembleOptions]] = get_type_hints(L2ACOptions)
+ options_dataclass: ClassVar[Type[PerGameCommonOptions]] = L2ACOptions
+ options: L2ACOptions
settings: ClassVar[L2ACSettings]
item_name_to_id: ClassVar[Dict[str, int]] = l2ac_item_name_to_id
location_name_to_id: ClassVar[Dict[str, int]] = l2ac_location_name_to_id
@@ -64,8 +65,7 @@ class L2ACWorld(World):
"Iris treasures": {name for name, data in l2ac_item_table.items() if data.type is ItemType.IRIS_TREASURE},
"Party members": {name for name, data in l2ac_item_table.items() if data.type is ItemType.PARTY_MEMBER},
}
- data_version: ClassVar[int] = 2
- required_client_version: Tuple[int, int, int] = (0, 4, 2)
+ required_client_version: Tuple[int, int, int] = (0, 4, 4)
# L2ACWorld specific properties
rom_name: bytearray
@@ -87,7 +87,7 @@ def generate_early(self) -> None:
bytearray(f"L2AC{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21]
self.rom_name.extend([0] * (21 - len(self.rom_name)))
- self.o = L2ACOptions(**{opt: getattr(self.multiworld, opt)[self.player] for opt in self.option_definitions})
+ self.o = self.options
if self.o.blue_chest_count < self.o.custom_item_pool.count:
raise ValueError(f"Number of items in custom_item_pool ({self.o.custom_item_pool.count}) is "
@@ -116,6 +116,7 @@ def create_regions(self) -> None:
L2ACLocation(self.player, f"Chest access {i + 1}-{i + CHESTS_PER_SPHERE}", None, ancient_dungeon)
chest_access.place_locked_item(
L2ACItem("Progressive chest access", ItemClassification.progression, None, self.player))
+ chest_access.show_in_spoiler = False
ancient_dungeon.locations.append(chest_access)
for iris in self.item_name_groups["Iris treasures"]:
treasure_name: str = f"Iris treasure {self.item_name_to_id[iris] - self.item_name_to_id['Iris sword'] + 1}"
@@ -152,23 +153,23 @@ def create_items(self) -> None:
self.multiworld.itempool.append(self.create_item(item_name))
def set_rules(self) -> None:
- for i in range(1, self.o.blue_chest_count):
- if i % CHESTS_PER_SPHERE == 0:
- set_rule(self.multiworld.get_location(f"Blue chest {i + 1}", self.player),
- lambda state, j=i: state.has("Progressive chest access", self.player, j // CHESTS_PER_SPHERE))
- set_rule(self.multiworld.get_location(f"Chest access {i + 1}-{i + CHESTS_PER_SPHERE}", self.player),
- lambda state, j=i: state.can_reach(f"Blue chest {j}", "Location", self.player))
- else:
- set_rule(self.multiworld.get_location(f"Blue chest {i + 1}", self.player),
- lambda state, j=i: state.can_reach(f"Blue chest {j}", "Location", self.player))
-
- set_rule(self.multiworld.get_entrance("FinalFloorEntrance", self.player),
- lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player))
+ max_sphere: int = (self.o.blue_chest_count - 1) // CHESTS_PER_SPHERE + 1
+ rule_for_sphere: Dict[int, CollectionRule] = \
+ {sphere: lambda state, s=sphere: state.has("Progressive chest access", self.player, s - 1)
+ for sphere in range(2, max_sphere + 1)}
+
+ for i in range(CHESTS_PER_SPHERE * 2, self.o.blue_chest_count, CHESTS_PER_SPHERE):
+ set_rule(self.multiworld.get_location(f"Chest access {i + 1}-{i + CHESTS_PER_SPHERE}", self.player),
+ rule_for_sphere[i // CHESTS_PER_SPHERE])
+ for i in range(CHESTS_PER_SPHERE, self.o.blue_chest_count):
+ set_rule(self.multiworld.get_location(f"Blue chest {i + 1}", self.player),
+ rule_for_sphere[i // CHESTS_PER_SPHERE + 1])
+
+ set_rule(self.multiworld.get_entrance("FinalFloorEntrance", self.player), rule_for_sphere[max_sphere])
for i in range(9):
- set_rule(self.multiworld.get_location(f"Iris treasure {i + 1}", self.player),
- lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player))
- set_rule(self.multiworld.get_location("Boss", self.player),
- lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player))
+ set_rule(self.multiworld.get_location(f"Iris treasure {i + 1}", self.player), rule_for_sphere[max_sphere])
+ set_rule(self.multiworld.get_location("Boss", self.player), rule_for_sphere[max_sphere])
+
if self.o.shuffle_capsule_monsters:
add_rule(self.multiworld.get_location("Boss", self.player), lambda state: state.has("DARBI", self.player))
if self.o.shuffle_party_members:
@@ -221,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")
@@ -228,6 +230,8 @@ 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[0x28001B:0x28001B + 1] = self.o.inactive_exp_gain.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()
@@ -356,7 +360,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
@@ -365,6 +369,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:
@@ -382,6 +387,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..77809cce6f4c 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
@@ -140,7 +145,7 @@ TX:
BEQ +
JSR ReportLocationCheck
SEP #$20
- JML $8EC331 ; skip item get process
+ JML $8EC2DC ; skip item get process; consider chest emptied
+: BIT.w #$4200 ; test for blue chest flag
BEQ +
LDA $F02048 ; load total blue chests checked
@@ -150,7 +155,7 @@ TX:
INC ; increment check counter
STA $F02040 ; store check counter
SEP #$20
- JML $8EC331 ; skip item get process
+ JML $8EC2DC ; skip item get process; consider chest emptied
+: SEP #$20
JML $8EC1EF ; continue item get process
@@ -165,6 +170,9 @@ pullpc
ScriptTX:
STA $7FD4F1 ; (overwritten instruction)
+ LDA $05AC ; load map number
+ CMP.b #$F1 ; check if ancient cave final floor
+ BNE +
REP #$20
LDA $7FD4EF ; read script item id
CMP.w #$01C2 ; test for ancient key
@@ -256,6 +264,9 @@ SpecialItemGet:
BRA ++
+: CMP.w #$01C2 ; ancient key
BNE +
+ LDA.w #$0008
+ ORA $0796
+ STA $0796 ; set ancient key EV flag ($C3)
LDA.w #$0200
ORA $0797
STA $0797 ; set boss item EV flag ($D1)
@@ -298,6 +309,12 @@ org $8EFD2E ; unused region at the end of bank $8E
DB $1E,$0B,$01,$2B,$05,$1A,$05,$00 ; add dekar
DB $1E,$0B,$01,$2B,$04,$1A,$06,$00 ; add tia
DB $1E,$0B,$01,$2B,$06,$1A,$07,$00 ; add lexis
+ DB $1F,$0B,$01,$2C,$01,$1B,$02,$00 ; remove selan
+ DB $1F,$0B,$01,$2C,$02,$1B,$03,$00 ; remove guy
+ DB $1F,$0B,$01,$2C,$03,$1B,$04,$00 ; remove arty
+ DB $1F,$0B,$01,$2C,$05,$1B,$05,$00 ; remove dekar
+ DB $1F,$0B,$01,$2C,$04,$1B,$06,$00 ; remove tia
+ DB $1F,$0B,$01,$2C,$06,$1B,$07,$00 ; remove lexis
pullpc
SpecialItemUse:
@@ -317,11 +334,15 @@ SpecialItemUse:
SEP #$20
LDA $8ED8C7,X ; load predefined bitmask with a single bit set
BIT $077E ; check against EV flags $02 to $07 (party member flags)
- BNE + ; abort if character already present
- LDA $07A9 ; load EV register $11 (party counter)
+ BEQ ++
+ LDA.b #$30 ; character already present; modify pointer to point to L2SASM leave script
+ ADC $09B7
+ STA $09B7
+ BRA +++
+++: LDA $07A9 ; character not present; load EV register $0B (party counter)
CMP.b #$03
BPL + ; abort if party full
- LDA.b #$8E
++++ LDA.b #$8E
STA $09B9
PHK
PEA ++
@@ -329,7 +350,6 @@ SpecialItemUse:
JML $83BB76 ; initialize parser variables
++: NOP
JSL $809CB8 ; call L2SASM parser
- JSL $81F034 ; consume the item
TSX
INX #13
TXS
@@ -479,6 +499,73 @@ pullpc
+; allow inactive characters to gain exp
+pushpc
+org $81DADD
+ ; DB=$81, x=0, m=1
+ NOP ; overwrites BNE $81DAE2 : JMP $DBED
+ JML HandleActiveExp
+AwardExp:
+ ; isolate exp distribution into a subroutine, to be reused for both active party members and inactive characters
+org $81DAE9
+ NOP #2 ; overwrites JMP $DBBD
+ RTL
+org $81DB42
+ NOP #2 ; overwrites JMP $DBBD
+ RTL
+org $81DD11
+ ; DB=$81, x=0, m=1
+ JSL HandleInactiveExp ; overwrites LDA $0A8A : CLC
+pullpc
+
+HandleActiveExp:
+ BNE + ; (overwritten instruction; modified) check if statblock not empty
+ JML $81DBED ; (overwritten instruction; modified) abort
++: JSL AwardExp ; award exp (X=statblock pointer, Y=position in battle order, $00=position in menu order)
+ JML $81DBBD ; (overwritten instruction; modified) continue to next level text
+
+HandleInactiveExp:
+ LDA $F0201B ; load inactive exp gain rate
+ BEQ + ; zero gain; skip everything
+ CMP.b #$64
+ BCS ++ ; full gain
+ LSR $1607
+ ROR $1606 ; half gain
+ ROR $1605
+++: LDY.w #$0000 ; start looping through all characters
+-: TDC
+ TYA
+ LDX.w #$0003 ; start looping through active party
+--: CMP $0A7B,X
+ BEQ ++ ; skip if character in active party
+ DEX
+ BPL -- ; continue looping through active party
+ STA $153D ; inactive character detected; overwrite character index of 1st slot in party battle order
+ ASL
+ TAX
+ REP #$20
+ LDA $859EBA,X ; convert character index to statblock pointer
+ SEP #$20
+ TAX
+ PHY ; stash character loop index
+ LDY $0A80
+ PHY ; stash 1st (in menu order) party member statblock pointer
+ STX $0A80 ; overwrite 1st (in menu order) party member statblock pointer
+ LDY.w #$0000 ; set to use 1st position (in battle order)
+ STY $00 ; set to use 1st position (in menu order)
+ JSL AwardExp ; award exp (X=statblock pointer, Y=position in battle order, $00=position in menu order)
+ PLY ; restore 1st (in menu order) party member statblock pointer
+ STY $0A80
+ PLY ; restore character loop index
+++: INY
+ CPY.w #$0007
+ BCC - ; continue looping through all characters
++: LDA $0A8A ; (overwritten instruction) load current gold
+ CLC ; (overwritten instruction)
+ RTL
+
+
+
; receive death link
pushpc
org $83BC91
@@ -825,6 +912,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 (eventually causes return by reaching existing PLP : PLB : RTL at $809DB0)
++: 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
@@ -1009,6 +1209,53 @@ pullpc
+; door stairs fix
+pushpc
+org $839453
+ ; DB=$7F, x=0, m=1
+ JSL DoorStairsFix ; overwrites JSR $9B18 : JSR $9D11
+ NOP #2
+pullpc
+
+DoorStairsFix:
+ CLC
+ LDY.w #$0000
+--: LDX.w #$00FF ; loop through floor layout starting from the bottom right
+-: LDA $EA00,X ; read node contents
+ BEQ + ; always skip empty nodes
+ BCC ++ ; 1st pass: skip all blocked nodes (would cause door stairs or rare stairs)
+ LDA $E9F0,X ; 2nd pass: skip only if the one above is also blocked (would cause door stairs)
+++: BMI +
+ INY ; count usable nodes
++: DEX
+ BPL -
+ TYA
+ BNE ++ ; all nodes blocked?
+ SEC ; set up 2nd, less restrictive pass
+ BRA --
+++: JSL $8082C7 ; advance RNG
+ STA $00211B
+ TDC
+ STA $00211B ; M7A; first factor = random number from 0 to 255
+ TYA
+ STA $00211C ; M7B; second factor = number of possible stair positions
+ LDA $002135 ; MPYM; calculate random number from 0 to number of possible stair positions - 1
+ TAY
+ LDX.w #$00FF ; loop through floor layout starting from the bottom right
+-: LDA $EA00,X ; read node contents
+ BEQ + ; always skip empty nodes
+ BCC ++ ; if 1st pass was sufficient: skip all blocked nodes (prevent door stairs and rare stairs)
+ LDA $E9F0,X ; if 2nd pass was needed: skip only if the one above is also blocked (prevent door stairs)
+++: BMI +
+ DEY ; count down to locate the (Y+1)th usable node
+ BMI ++
++: DEX
+ BPL -
+++: TXA ; return selected stair node coordinate
+ RTL
+
+
+
; equipment text fix
pushpc
org $81F2E3
@@ -1054,6 +1301,8 @@ pullpc
; $F02017 1 iris treasures required
; $F02018 1 party members available
; $F02019 1 capsule monsters available
+; $F0201A 1 shop interval
+; $F0201B 1 inactive exp gain rate
; $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..b261f9d1ab97 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..4b5bf3f318fa 100644
--- a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md
+++ b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md
@@ -1,8 +1,8 @@
# Lufia II - Rise of the Sinistrals (Ancient Cave)
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -49,11 +49,13 @@ 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
+ find them in order to unlock them for you to use. While cave diving, you can add or remove unlocked party members by
+ using the character items from your inventory. There's also an option to allow inactive characters to gain some EXP,
+ so that new party members added during a run don't have to start off at a low level
###### Quality of life:
@@ -75,7 +77,7 @@ Your Party Leader will hold up the item they received when not in a fight or in
###### Bug fixes:
-- Vanilla game bugs that could result in softlocks or save file corruption have been fixed
+- Vanilla game bugs that could result in anomalous floors, softlocks, or save file corruption have been fixed
- (optional) Bugfix for the algorithm that determines the item pool for red chest gear. Enabling this allows the cave to
generate shields, headgear, rings, and jewels in red chests even after floor B9
- (optional) Bugfix for the outlandish cravings of capsule monsters in the US version. Enabling this makes feeding work
diff --git a/worlds/lufia2ac/docs/setup_en.md b/worlds/lufia2ac/docs/setup_en.md
index f9e0d3725cfb..d82853d4fddf 100644
--- a/worlds/lufia2ac/docs/setup_en.md
+++ b/worlds/lufia2ac/docs/setup_en.md
@@ -2,7 +2,7 @@
## Required Software
-- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Lufia II Ancient Cave Patch Setup`
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of connecting to SNI
([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
@@ -14,11 +14,12 @@ modded SNES minis are currently not supported by SNI**
## Installation Procedures
-1. Download and install SNIClient from the link above, making sure to install the most recent version.
- **The installer file is located in the assets section at the bottom of the version information**.
- - During setup, you will be asked to locate your base ROM file. This is your American Lufia II - Rise of the Sinistrals ROM file.
+1. Download and install [Archipelago](). **The installer
+ file is located in the assets section at the bottom of the version information.**
+2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
+ This is your American Lufia II - Rise of the Sinistrals ROM file. This only needs to be done once.
-2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
+3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
2. Right-click on a ROM file and select **Open with...**
@@ -38,17 +39,17 @@ options.
### Where do I get a config file?
-The [Player Settings](/games/Lufia%20II%20Ancient%20Cave/player-settings) page on the website allows you to configure
-your personal settings and export a config file from them.
+The [Player Options](/games/Lufia%20II%20Ancient%20Cave/player-options) page on the website allows you to configure
+your personal options 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
-1. Navigate to the [Player Settings](/games/Lufia%20II%20Ancient%20Cave/player-settings) page, configure your options,
+1. Navigate to the [Player Options](/games/Lufia%20II%20Ancient%20Cave/player-options) page, configure your options,
and click the "Generate Game" button.
2. You will be presented with a "Seed Info" page.
3. Click the "Create New Room" link.
@@ -82,8 +83,7 @@ first time launching, you may be prompted to allow it to communicate through the
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Select the connector lua file included with your client
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
@@ -94,9 +94,8 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas
- (≥ 2.9) `Config` 〉 `Preferred Cores` 〉 `SNES` 〉 `BSNESv115+`
2. Load your ROM file if it hasn't already been loaded.
If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R).
-3. Drag+drop the `Connector.lua` file that you downloaded above onto the main EmuHawk window.
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
+3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
- You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua`
with the file picker.
diff --git a/worlds/meritous/Locations.py b/worlds/meritous/Locations.py
index 1893b8520e48..690c757efff8 100644
--- a/worlds/meritous/Locations.py
+++ b/worlds/meritous/Locations.py
@@ -9,11 +9,6 @@
class MeritousLocation(Location):
game: str = "Meritous"
- def __init__(self, player: int, name: str = '', address: int = None, parent=None):
- super(MeritousLocation, self).__init__(player, name, address, parent)
- if "Wervyn Anixil" in name or "Defeat" in name:
- self.event = True
-
offset = 593_000
diff --git a/worlds/meritous/Options.py b/worlds/meritous/Options.py
index 6b3ea5885824..fb51bbfba120 100644
--- a/worlds/meritous/Options.py
+++ b/worlds/meritous/Options.py
@@ -3,8 +3,10 @@
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
+from dataclasses import dataclass
+
import typing
-from Options import Option, DeathLink, Toggle, DefaultOnToggle, Choice
+from Options import Option, DeathLink, Toggle, DefaultOnToggle, Choice, PerGameCommonOptions
cost_scales = {
@@ -51,10 +53,10 @@ class ItemCacheCost(Choice):
default = 0
-meritous_options: typing.Dict[str, type(Option)] = {
- "goal": Goal,
- "include_psi_keys": IncludePSIKeys,
- "include_evolution_traps": IncludeEvolutionTraps,
- "item_cache_cost": ItemCacheCost,
- "death_link": DeathLink
-}
+@dataclass
+class MeritousOptions(PerGameCommonOptions):
+ goal: Goal
+ include_psi_keys: IncludePSIKeys
+ include_evolution_traps: IncludeEvolutionTraps
+ item_cache_cost: ItemCacheCost
+ death_link: DeathLink
diff --git a/worlds/meritous/Regions.py b/worlds/meritous/Regions.py
index 2c66a024ca69..acf560eb8adf 100644
--- a/worlds/meritous/Regions.py
+++ b/worlds/meritous/Regions.py
@@ -13,7 +13,7 @@ def _generate_entrances(player: int, entrance_list: [str], parent: Region):
return [Entrance(player, entrance, parent) for entrance in entrance_list]
-def create_regions(world: MultiWorld, player: int):
+def create_regions(multiworld: MultiWorld, player: int):
regions = ["First", "Second", "Third", "Last"]
bosses = ["Meridian", "Ataraxia", "Merodach"]
@@ -23,7 +23,7 @@ def create_regions(world: MultiWorld, player: int):
if x == 0:
insidename = "Menu"
- region = Region(insidename, player, world)
+ region = Region(insidename, player, multiworld)
for store in ["Alpha Cache", "Beta Cache", "Gamma Cache", "Reward Chest"]:
for y in range(1, 7):
loc_name = f"{store} {(x * 6) + y}"
@@ -42,26 +42,26 @@ def create_regions(world: MultiWorld, player: int):
"Back to the entrance with the Knife"],
region)
- world.regions += [region]
+ multiworld.regions += [region]
for x, boss in enumerate(bosses):
- boss_region = Region(boss, player, world)
+ boss_region = Region(boss, player, multiworld)
boss_region.locations += [
MeritousLocation(player, boss, location_table[boss], boss_region),
MeritousLocation(player, f"{boss} Defeat", None, boss_region)
]
boss_region.exits = _generate_entrances(player, [f"To {regions[x + 1]} Quarter"], boss_region)
- world.regions.append(boss_region)
+ multiworld.regions.append(boss_region)
- region_final_boss = Region("Final Boss", player, world)
- region_final_boss.locations = [MeritousLocation(
+ region_final_boss = Region("Final Boss", player, multiworld)
+ region_final_boss.locations += [MeritousLocation(
player, "Wervyn Anixil", None, region_final_boss)]
- world.regions.append(region_final_boss)
+ multiworld.regions.append(region_final_boss)
- region_tfb = Region("True Final Boss", player, world)
- region_tfb.locations = [MeritousLocation(
+ region_tfb = Region("True Final Boss", player, multiworld)
+ region_tfb.locations += [MeritousLocation(
player, "Wervyn Anixil?", None, region_tfb)]
- world.regions.append(region_tfb)
+ multiworld.regions.append(region_tfb)
entrance_map = {
"To Meridian": {
@@ -103,6 +103,6 @@ def create_regions(world: MultiWorld, player: int):
for entrance in entrance_map:
connection_data = entrance_map[entrance]
- connection = world.get_entrance(entrance, player)
+ connection = multiworld.get_entrance(entrance, player)
connection.access_rule = connection_data["rule"]
- connection.connect(world.get_region(connection_data["to"], player))
+ connection.connect(multiworld.get_region(connection_data["to"], player))
diff --git a/worlds/meritous/__init__.py b/worlds/meritous/__init__.py
index 1bf1bfc0f2e6..7a21b19ef247 100644
--- a/worlds/meritous/__init__.py
+++ b/worlds/meritous/__init__.py
@@ -7,7 +7,7 @@
from Fill import fill_restrictive
from .Items import item_table, item_groups, MeritousItem
from .Locations import location_table, MeritousLocation
-from .Options import meritous_options, cost_scales
+from .Options import MeritousOptions, cost_scales
from .Regions import create_regions
from .Rules import set_rules
from ..AutoWorld import World, WebWorld
@@ -17,7 +17,7 @@
class MeritousWeb(WebWorld):
tutorials = [Tutorial(
- "Meritous Setup Tutorial",
+ "Meritous Setup Guide",
"A guide to setting up the Archipelago Meritous software on your computer.",
"English",
"setup_en.md",
@@ -44,15 +44,14 @@ class MeritousWorld(World):
location_name_to_id = location_table
item_name_groups = item_groups
- data_version = 2
-
# NOTE: Remember to change this before this game goes live
required_client_version = (0, 2, 4)
- option_definitions = meritous_options
+ options: MeritousOptions
+ options_dataclass = MeritousOptions
- def __init__(self, world: MultiWorld, player: int):
- super(MeritousWorld, self).__init__(world, player)
+ def __init__(self, multiworld: MultiWorld, player: int):
+ super(MeritousWorld, self).__init__(multiworld, player)
self.goal = 0
self.include_evolution_traps = False
self.include_psi_keys = False
@@ -97,11 +96,11 @@ def get_filler_item_name(self) -> str:
return "Crystals x2000"
def generate_early(self):
- self.goal = self.multiworld.goal[self.player].value
- self.include_evolution_traps = self.multiworld.include_evolution_traps[self.player].value
- self.include_psi_keys = self.multiworld.include_psi_keys[self.player].value
- self.item_cache_cost = self.multiworld.item_cache_cost[self.player].value
- self.death_link = self.multiworld.death_link[self.player].value
+ self.goal = self.options.goal.value
+ self.include_evolution_traps = self.options.include_evolution_traps.value
+ self.include_psi_keys = self.options.include_psi_keys.value
+ self.item_cache_cost = self.options.item_cache_cost.value
+ self.death_link = self.options.death_link.value
def create_regions(self):
create_regions(self.multiworld, self.player)
diff --git a/worlds/meritous/docs/en_Meritous.md b/worlds/meritous/docs/en_Meritous.md
index bceae7d9ae2b..d119c3634c58 100644
--- a/worlds/meritous/docs/en_Meritous.md
+++ b/worlds/meritous/docs/en_Meritous.md
@@ -1,7 +1,7 @@
# Meritous
-## Where is the settings page?
-The [player settings page for Meritous](../player-settings) contains all the options you need to configure and export a config file.
+## Where is the options page?
+The [player options page for Meritous](../player-options) contains all the options you need to configure and export a config file.
## What does randomization do to this game?
The PSI Enhancement Tiles have become general-purpose Item Caches, and all upgrades and artifacts are added to the multiworld item pool. Optionally, the progression-critical PSI Keys can also be added to the pool, as well as monster evolution traps which (in vanilla) trigger when bosses are defeated.
diff --git a/worlds/meritous/docs/setup_en.md b/worlds/meritous/docs/setup_en.md
index 63f8657b63bb..9b91f12106de 100644
--- a/worlds/meritous/docs/setup_en.md
+++ b/worlds/meritous/docs/setup_en.md
@@ -40,9 +40,9 @@ Eventually, this process will be moved to in-game menus for better ease of use.
## Finishing the Game
-Your initial goal is to find all three PSI Keys. Depending on your YAML settings, these may be located on pedestals in special rooms in the Atlas Dome, or they may be scattered across other players' worlds. These PSI Keys are then brought to their respective locations in the Dome, where you will be subjected to a boss battle. Once all three bosses are defeated, this unlocks the Cursed Seal, hidden in the farthest-away location from the Entrance. The Compass tiles can help you find your way to these locations.
+Your initial goal is to find all three PSI Keys. Depending on your YAML options, these may be located on pedestals in special rooms in the Atlas Dome, or they may be scattered across other players' worlds. These PSI Keys are then brought to their respective locations in the Dome, where you will be subjected to a boss battle. Once all three bosses are defeated, this unlocks the Cursed Seal, hidden in the farthest-away location from the Entrance. The Compass tiles can help you find your way to these locations.
-At minimum, every seed will require you to find the Cursed Seal and bring it back to the Entrance. The goal can then vary based on your `goal` YAML setting:
+At minimum, every seed will require you to find the Cursed Seal and bring it back to the Entrance. The goal can then vary based on your `goal` YAML option:
- `return_the_cursed_seal`: You will fight the final boss, but win or lose, a victory will be posted.
- `any_ending`: You must defeat the final boss.
diff --git a/worlds/messenger/Constants.py b/worlds/messenger/Constants.py
deleted file mode 100644
index 121584da0555..000000000000
--- a/worlds/messenger/Constants.py
+++ /dev/null
@@ -1,113 +0,0 @@
-# items
-# listing individual groups first for easy lookup
-from .Shop import SHOP_ITEMS, FIGURINES
-
-NOTES = [
- "Key of Hope",
- "Key of Chaos",
- "Key of Courage",
- "Key of Love",
- "Key of Strength",
- "Key of Symbiosis",
-]
-
-PROG_ITEMS = [
- "Wingsuit",
- "Rope Dart",
- "Lightfoot Tabi",
- "Power Thistle",
- "Demon King Crown",
- "Ruxxtin's Amulet",
- "Magic Firefly",
- "Sun Crest",
- "Moon Crest",
- # "Astral Seed",
- # "Astral Tea Leaves",
- "Money Wrench",
-]
-
-PHOBEKINS = [
- "Necro",
- "Pyro",
- "Claustro",
- "Acro",
-]
-
-USEFUL_ITEMS = [
- "Windmill Shuriken",
-]
-
-FILLER = {
- "Time Shard": 5,
- "Time Shard (10)": 10,
- "Time Shard (50)": 20,
- "Time Shard (100)": 20,
- "Time Shard (300)": 10,
- "Time Shard (500)": 5,
-}
-
-# item_name_to_id needs to be deterministic and match upstream
-ALL_ITEMS = [
- *NOTES,
- "Windmill Shuriken",
- "Wingsuit",
- "Rope Dart",
- "Lightfoot Tabi",
- # "Astral Seed",
- # "Astral Tea Leaves",
- "Candle",
- "Seashell",
- "Power Thistle",
- "Demon King Crown",
- "Ruxxtin's Amulet",
- "Magic Firefly",
- "Sun Crest",
- "Moon Crest",
- *PHOBEKINS,
- "Power Seal",
- *FILLER,
- *SHOP_ITEMS,
- *FIGURINES,
- "Money Wrench",
-]
-
-# locations
-# the names of these don't actually matter, but using the upstream's names for now
-# order must be exactly the same as upstream
-ALWAYS_LOCATIONS = [
- # notes
- "Sunken Shrine - Key of Love",
- "Corrupted Future - Key of Courage",
- "Underworld - Key of Chaos",
- "Elemental Skylands - Key of Symbiosis",
- "Searing Crags - Key of Strength",
- "Autumn Hills - Key of Hope",
- # upgrades
- "Howling Grotto - Wingsuit",
- "Searing Crags - Rope Dart",
- "Sunken Shrine - Lightfoot Tabi",
- "Autumn Hills - Climbing Claws",
- # quest items
- "Ninja Village - Astral Seed",
- "Searing Crags - Astral Tea Leaves",
- "Ninja Village - Candle",
- "Quillshroom Marsh - Seashell",
- "Searing Crags - Power Thistle",
- "Forlorn Temple - Demon King",
- "Catacombs - Ruxxtin's Amulet",
- "Riviere Turquoise - Butterfly Matriarch",
- "Sunken Shrine - Sun Crest",
- "Sunken Shrine - Moon Crest",
- # phobekins
- "Catacombs - Necro",
- "Searing Crags - Pyro",
- "Bamboo Creek - Claustro",
- "Cloud Ruins - Acro",
-]
-
-BOSS_LOCATIONS = [
- "Autumn Hills - Leaf Golem",
- "Catacombs - Ruxxtin",
- "Howling Grotto - Emerald Golem",
- "Quillshroom Marsh - Queen of Quills",
-]
diff --git a/worlds/messenger/Options.py b/worlds/messenger/Options.py
deleted file mode 100644
index 8e8b61a2049c..000000000000
--- a/worlds/messenger/Options.py
+++ /dev/null
@@ -1,146 +0,0 @@
-from typing import Dict
-from schema import Schema, Or, And, Optional
-
-from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice, Toggle, OptionDict, StartInventoryPool
-
-
-class MessengerAccessibility(Accessibility):
- default = Accessibility.option_locations
- # defaulting to locations accessibility since items makes certain items self-locking
- __doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}")
-
-
-class Logic(Choice):
- """
- The level of logic to use when determining what locations in your world are accessible.
-
- Normal: can require damage boosts, but otherwise approachable for someone who has beaten the game.
- Hard: has leashing, normal clips, time warps and turtle boosting in logic.
- OoB: places everything with the minimum amount of rules possible. Expect to do OoB. Not guaranteed completable.
- """
- display_name = "Logic Level"
- option_normal = 0
- option_hard = 1
- option_oob = 2
- alias_challenging = 1
-
-
-class PowerSeals(DefaultOnToggle):
- """Whether power seal locations should be randomized."""
- display_name = "Shuffle Seals"
-
-
-class MegaShards(Toggle):
- """Whether mega shards should be item locations."""
- display_name = "Shuffle Mega Time Shards"
-
-
-class Goal(Choice):
- """Requirement to finish the game. Power Seal Hunt will force power seal locations to be shuffled."""
- display_name = "Goal"
- option_open_music_box = 0
- option_power_seal_hunt = 1
-
-
-class MusicBox(DefaultOnToggle):
- """Whether the music box gauntlet needs to be done."""
- display_name = "Music Box Gauntlet"
-
-
-class NotesNeeded(Range):
- """How many notes are needed to access the Music Box."""
- display_name = "Notes Needed"
- range_start = 1
- range_end = 6
- default = range_end
-
-
-class AmountSeals(Range):
- """Number of power seals that exist in the item pool when power seal hunt is the goal."""
- display_name = "Total Power Seals"
- range_start = 1
- range_end = 85
- default = 45
-
-
-class RequiredSeals(Range):
- """Percentage of total seals required to open the shop chest."""
- display_name = "Percent Seals Required"
- range_start = 10
- range_end = 100
- default = range_end
-
-
-class ShopPrices(Range):
- """Percentage modifier for shuffled item prices in shops"""
- display_name = "Shop Prices Modifier"
- range_start = 25
- range_end = 400
- default = 100
-
-
-def planned_price(location: str) -> Dict[Optional, Or]:
- return {
- Optional(location): Or(
- And(int, lambda n: n >= 0),
- {
- Optional(And(int, lambda n: n >= 0)): And(int, lambda n: n >= 0)
- }
- )
- }
-
-
-class PlannedShopPrices(OptionDict):
- """Plan specific prices on shop slots. Supports weighting"""
- display_name = "Shop Price Plando"
- schema = Schema({
- **planned_price("Karuta Plates"),
- **planned_price("Serendipitous Bodies"),
- **planned_price("Path of Resilience"),
- **planned_price("Kusari Jacket"),
- **planned_price("Energy Shuriken"),
- **planned_price("Serendipitous Minds"),
- **planned_price("Prepared Mind"),
- **planned_price("Meditation"),
- **planned_price("Rejuvenative Spirit"),
- **planned_price("Centered Mind"),
- **planned_price("Strike of the Ninja"),
- **planned_price("Second Wind"),
- **planned_price("Currents Master"),
- **planned_price("Aerobatics Warrior"),
- **planned_price("Demon's Bane"),
- **planned_price("Devil's Due"),
- **planned_price("Time Sense"),
- **planned_price("Power Sense"),
- **planned_price("Focused Power Sense"),
- **planned_price("Green Kappa Figurine"),
- **planned_price("Blue Kappa Figurine"),
- **planned_price("Ountarde Figurine"),
- **planned_price("Red Kappa Figurine"),
- **planned_price("Demon King Figurine"),
- **planned_price("Quillshroom Figurine"),
- **planned_price("Jumping Quillshroom Figurine"),
- **planned_price("Scurubu Figurine"),
- **planned_price("Jumping Scurubu Figurine"),
- **planned_price("Wallaxer Figurine"),
- **planned_price("Barmath'azel Figurine"),
- **planned_price("Queen of Quills Figurine"),
- **planned_price("Demon Hive Figurine"),
- })
-
-
-messenger_options = {
- "accessibility": MessengerAccessibility,
- "start_inventory": StartInventoryPool,
- "logic_level": Logic,
- "shuffle_seals": PowerSeals,
- "shuffle_shards": MegaShards,
- "goal": Goal,
- "music_box": MusicBox,
- "notes_needed": NotesNeeded,
- "total_seals": AmountSeals,
- "percent_seals_required": RequiredSeals,
- "shop_price": ShopPrices,
- "shop_price_plan": PlannedShopPrices,
- "death_link": DeathLink,
-}
diff --git a/worlds/messenger/Regions.py b/worlds/messenger/Regions.py
deleted file mode 100644
index 88579a9704fa..000000000000
--- a/worlds/messenger/Regions.py
+++ /dev/null
@@ -1,107 +0,0 @@
-from typing import Dict, Set, List
-
-REGIONS: Dict[str, List[str]] = {
- "Menu": [],
- "Tower HQ": [],
- "The Shop": [],
- "Tower of Time": [],
- "Ninja Village": ["Ninja Village - Candle", "Ninja Village - Astral Seed"],
- "Autumn Hills": ["Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills - Leaf Golem"],
- "Forlorn Temple": ["Forlorn Temple - Demon King"],
- "Catacombs": ["Catacombs - Necro", "Catacombs - Ruxxtin's Amulet", "Catacombs - Ruxxtin"],
- "Bamboo Creek": ["Bamboo Creek - Claustro"],
- "Howling Grotto": ["Howling Grotto - Wingsuit", "Howling Grotto - Emerald Golem"],
- "Quillshroom Marsh": ["Quillshroom Marsh - Seashell", "Quillshroom Marsh - Queen of Quills"],
- "Searing Crags": ["Searing Crags - Rope Dart"],
- "Searing Crags Upper": ["Searing Crags - Power Thistle", "Searing Crags - Key of Strength",
- "Searing Crags - Astral Tea Leaves"],
- "Glacial Peak": [],
- "Cloud Ruins": [],
- "Cloud Ruins Right": ["Cloud Ruins - Acro"],
- "Underworld": ["Searing Crags - Pyro", "Underworld - Key of Chaos"],
- "Dark Cave": [],
- "Riviere Turquoise Entrance": [],
- "Riviere Turquoise": ["Riviere Turquoise - Butterfly Matriarch"],
- "Sunken Shrine": ["Sunken Shrine - Lightfoot Tabi", "Sunken Shrine - Sun Crest", "Sunken Shrine - Moon Crest",
- "Sunken Shrine - Key of Love"],
- "Elemental Skylands": ["Elemental Skylands - Key of Symbiosis"],
- "Corrupted Future": ["Corrupted Future - Key of Courage"],
- "Music Box": ["Rescue Phantom"],
-}
-
-SEALS: Dict[str, List[str]] = {
- "Ninja Village": ["Ninja Village Seal - Tree House"],
- "Autumn Hills": ["Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
- "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts"],
- "Catacombs": ["Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet",
- "Catacombs Seal - Dirty Pond"],
- "Bamboo Creek": ["Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
- "Bamboo Creek Seal - Spike Crushers and Doors v2"],
- "Howling Grotto": ["Howling Grotto Seal - Windy Saws and Balls", "Howling Grotto Seal - Crushing Pits",
- "Howling Grotto Seal - Breezy Crushers"],
- "Quillshroom Marsh": ["Quillshroom Marsh Seal - Spikey Window", "Quillshroom Marsh Seal - Sand Trap",
- "Quillshroom Marsh Seal - Do the Spike Wave"],
- "Searing Crags": ["Searing Crags Seal - Triple Ball Spinner"],
- "Searing Crags Upper": ["Searing Crags Seal - Raining Rocks", "Searing Crags Seal - Rhythm Rocks"],
- "Glacial Peak": ["Glacial Peak Seal - Ice Climbers", "Glacial Peak Seal - Projectile Spike Pit",
- "Glacial Peak Seal - Glacial Air Swag"],
- "Tower of Time": ["Tower of Time Seal - Time Waster", "Tower of Time Seal - Lantern Climb",
- "Tower of Time Seal - Arcane Orbs"],
- "Cloud Ruins Right": ["Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
- "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room"],
- "Underworld": ["Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Spike Wall",
- "Underworld Seal - Fireball Wave", "Underworld Seal - Rising Fanta"],
- "Forlorn Temple": ["Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset"],
- "Sunken Shrine": ["Sunken Shrine Seal - Ultra Lifeguard", "Sunken Shrine Seal - Waterfall Paradise",
- "Sunken Shrine Seal - Tabi Gauntlet"],
- "Riviere Turquoise Entrance": ["Riviere Turquoise Seal - Bounces and Balls"],
- "Riviere Turquoise": ["Riviere Turquoise Seal - Launch of Faith", "Riviere Turquoise Seal - Flower Power"],
- "Elemental Skylands": ["Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water",
- "Elemental Skylands Seal - Fire"]
-}
-
-MEGA_SHARDS: Dict[str, List[str]] = {
- "Autumn Hills": ["Autumn Hills Mega Shard", "Hidden Entrance Mega Shard"],
- "Catacombs": ["Catacombs Mega Shard"],
- "Bamboo Creek": ["Above Entrance Mega Shard", "Abandoned Mega Shard", "Time Loop Mega Shard"],
- "Howling Grotto": ["Bottom Left Mega Shard", "Near Portal Mega Shard", "Pie in the Sky Mega Shard"],
- "Quillshroom Marsh": ["Quillshroom Marsh Mega Shard"],
- "Searing Crags Upper": ["Searing Crags Mega Shard"],
- "Glacial Peak": ["Glacial Peak Mega Shard"],
- "Tower of Time": [],
- "Cloud Ruins": ["Cloud Entrance Mega Shard", "Time Warp Mega Shard"],
- "Cloud Ruins Right": ["Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2"],
- "Underworld": ["Under Entrance Mega Shard", "Hot Tub Mega Shard", "Projectile Pit Mega Shard"],
- "Forlorn Temple": ["Sunny Day Mega Shard", "Down Under Mega Shard"],
- "Sunken Shrine": ["Mega Shard of the Moon", "Beginner's Mega Shard", "Mega Shard of the Stars", "Mega Shard of the Sun"],
- "Riviere Turquoise Entrance": ["Waterfall Mega Shard"],
- "Riviere Turquoise": ["Quick Restock Mega Shard 1", "Quick Restock Mega Shard 2"],
- "Elemental Skylands": ["Earth Mega Shard", "Water Mega Shard"],
-}
-
-
-REGION_CONNECTIONS: Dict[str, Set[str]] = {
- "Menu": {"Tower HQ"},
- "Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time",
- "Riviere Turquoise Entrance", "Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"},
- "Tower of Time": set(),
- "Ninja Village": set(),
- "Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"},
- "Forlorn Temple": {"Catacombs", "Bamboo Creek"},
- "Catacombs": {"Autumn Hills", "Bamboo Creek", "Dark Cave"},
- "Bamboo Creek": {"Catacombs", "Howling Grotto"},
- "Howling Grotto": {"Bamboo Creek", "Quillshroom Marsh", "Sunken Shrine"},
- "Quillshroom Marsh": {"Howling Grotto", "Searing Crags"},
- "Searing Crags": {"Searing Crags Upper", "Quillshroom Marsh", "Underworld"},
- "Searing Crags Upper": {"Searing Crags", "Glacial Peak"},
- "Glacial Peak": {"Searing Crags Upper", "Tower HQ", "Cloud Ruins", "Elemental Skylands"},
- "Cloud Ruins": {"Cloud Ruins Right"},
- "Cloud Ruins Right": {"Underworld"},
- "Underworld": set(),
- "Dark Cave": {"Catacombs", "Riviere Turquoise Entrance"},
- "Riviere Turquoise Entrance": {"Riviere Turquoise"},
- "Riviere Turquoise": set(),
- "Sunken Shrine": {"Howling Grotto"},
- "Elemental Skylands": set(),
-}
-"""Vanilla layout mapping with all Tower HQ portals open. from -> to"""
diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py
deleted file mode 100644
index b72d454a7e0f..000000000000
--- a/worlds/messenger/Rules.py
+++ /dev/null
@@ -1,268 +0,0 @@
-from typing import Dict, Callable, TYPE_CHECKING
-
-from BaseClasses import CollectionState, MultiWorld
-from worlds.generic.Rules import set_rule, allow_self_locking_items, add_rule
-from .Options import MessengerAccessibility, Goal
-from .Constants import NOTES, PHOBEKINS
-from .SubClasses import MessengerShopLocation
-
-if TYPE_CHECKING:
- from . import MessengerWorld
-else:
- MessengerWorld = object
-
-
-class MessengerRules:
- player: int
- world: MessengerWorld
- region_rules: Dict[str, Callable[[CollectionState], bool]]
- location_rules: Dict[str, Callable[[CollectionState], bool]]
-
- def __init__(self, world: MessengerWorld) -> None:
- self.player = world.player
- self.world = world
-
- self.region_rules = {
- "Ninja Village": self.has_wingsuit,
- "Autumn Hills": self.has_wingsuit,
- "Catacombs": self.has_wingsuit,
- "Bamboo Creek": self.has_wingsuit,
- "Searing Crags Upper": self.has_vertical,
- "Cloud Ruins": lambda state: self.has_vertical(state) and state.has("Ruxxtin's Amulet", self.player),
- "Cloud Ruins Right": lambda state: self.has_wingsuit(state) and
- (self.has_dart(state) or self.can_dboost(state)),
- "Underworld": self.has_tabi,
- "Riviere Turquoise": lambda state: self.has_dart(state) or
- (self.has_wingsuit(state) and self.can_destroy_projectiles(state)),
- "Forlorn Temple": lambda state: state.has_all({"Wingsuit", *PHOBEKINS}, self.player) and self.can_dboost(state),
- "Glacial Peak": self.has_vertical,
- "Elemental Skylands": lambda state: state.has("Magic Firefly", self.player) and self.has_wingsuit(state),
- "Music Box": lambda state: state.has_all(set(NOTES), self.player) and self.has_dart(state),
- }
-
- self.location_rules = {
- # ninja village
- "Ninja Village Seal - Tree House": self.has_dart,
- # autumn hills
- "Autumn Hills - Key of Hope": self.has_dart,
- "Autumn Hills Seal - Spike Ball Darts": self.is_aerobatic,
- # bamboo creek
- "Bamboo Creek - Claustro": lambda state: self.has_dart(state) or self.can_dboost(state),
- # howling grotto
- "Howling Grotto Seal - Windy Saws and Balls": self.has_wingsuit,
- "Howling Grotto Seal - Crushing Pits": lambda state: self.has_wingsuit(state) and self.has_dart(state),
- "Howling Grotto - Emerald Golem": self.has_wingsuit,
- # searing crags
- "Searing Crags Seal - Triple Ball Spinner": self.has_vertical,
- "Searing Crags - Astral Tea Leaves":
- lambda state: state.can_reach("Ninja Village - Astral Seed", "Location", self.player),
- "Searing Crags - Key of Strength": lambda state: state.has("Power Thistle", self.player),
- # glacial peak
- "Glacial Peak Seal - Ice Climbers": self.has_dart,
- "Glacial Peak Seal - Projectile Spike Pit": self.can_destroy_projectiles,
- # cloud ruins
- "Cloud Ruins Seal - Ghost Pit": self.has_dart,
- # tower of time
- "Tower of Time Seal - Time Waster": self.has_dart,
- "Tower of Time Seal - Lantern Climb": lambda state: self.has_wingsuit(state) and self.has_dart(state),
- "Tower of Time Seal - Arcane Orbs": lambda state: self.has_wingsuit(state) and self.has_dart(state),
- # underworld
- "Underworld Seal - Sharp and Windy Climb": self.has_wingsuit,
- "Underworld Seal - Fireball Wave": self.is_aerobatic,
- "Underworld Seal - Rising Fanta": self.has_dart,
- # sunken shrine
- "Sunken Shrine - Sun Crest": self.has_tabi,
- "Sunken Shrine - Moon Crest": self.has_tabi,
- "Sunken Shrine - Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player),
- "Sunken Shrine Seal - Waterfall Paradise": self.has_tabi,
- "Sunken Shrine Seal - Tabi Gauntlet": self.has_tabi,
- "Mega Shard of the Moon": self.has_tabi,
- "Mega Shard of the Sun": self.has_tabi,
- # riviere turquoise
- "Riviere Turquoise Seal - Bounces and Balls": self.can_dboost,
- "Riviere Turquoise Seal - Launch of Faith": lambda state: self.can_dboost(state) or self.has_dart(state),
- # elemental skylands
- "Elemental Skylands - Key of Symbiosis": self.has_dart,
- "Elemental Skylands Seal - Air": self.has_wingsuit,
- "Elemental Skylands Seal - Water": lambda state: self.has_dart(state) and
- state.has("Currents Master", self.player),
- "Elemental Skylands Seal - Fire": lambda state: self.has_dart(state) and self.can_destroy_projectiles(state),
- "Earth Mega Shard": self.has_dart,
- "Water Mega Shard": self.has_dart,
- # corrupted future
- "Corrupted Future - Key of Courage": lambda state: state.has_all({"Demon King Crown", "Magic Firefly"},
- self.player),
- # the shop
- "Shop Chest": self.has_enough_seals,
- # tower hq
- "Money Wrench": self.can_shop,
- }
-
- def has_wingsuit(self, state: CollectionState) -> bool:
- return state.has("Wingsuit", self.player)
-
- def has_dart(self, state: CollectionState) -> bool:
- return state.has("Rope Dart", self.player)
-
- def has_tabi(self, state: CollectionState) -> bool:
- return state.has("Lightfoot Tabi", self.player)
-
- def has_vertical(self, state: CollectionState) -> bool:
- return self.has_wingsuit(state) or self.has_dart(state)
-
- def has_enough_seals(self, state: CollectionState) -> bool:
- return not self.world.required_seals or state.has("Power Seal", self.player, self.world.required_seals)
-
- def can_destroy_projectiles(self, state: CollectionState) -> bool:
- return state.has("Strike of the Ninja", self.player)
-
- def can_dboost(self, state: CollectionState) -> bool:
- return state.has_any({"Path of Resilience", "Meditation"}, self.player) and \
- state.has("Second Wind", self.player)
-
- def is_aerobatic(self, state: CollectionState) -> bool:
- return self.has_wingsuit(state) and state.has("Aerobatics Warrior", self.player)
-
- def true(self, state: CollectionState) -> bool:
- """I know this is stupid, but it's easier to read in the dicts."""
- return True
-
- def can_shop(self, state: CollectionState) -> bool:
- prices = self.world.shop_prices
- most_expensive_loc = max(prices, key=prices.get)
- return state.can_reach(f"The Shop - {most_expensive_loc}", "Location", self.player)
-
- def set_messenger_rules(self) -> None:
- multiworld = self.world.multiworld
-
- for region in multiworld.get_regions(self.player):
- if region.name in self.region_rules:
- for entrance in region.entrances:
- entrance.access_rule = self.region_rules[region.name]
- for loc in region.locations:
- if loc.name in self.location_rules:
- loc.access_rule = self.location_rules[loc.name]
- if region.name == "The Shop":
- for loc in [location for location in region.locations if isinstance(location, MessengerShopLocation)]:
- loc.access_rule = loc.can_afford
- if multiworld.goal[self.player] == Goal.option_power_seal_hunt:
- set_rule(multiworld.get_entrance("Tower HQ -> Music Box", self.player),
- lambda state: state.has("Shop Chest", self.player))
-
- multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player)
- if multiworld.accessibility[self.player] > MessengerAccessibility.option_locations:
- set_self_locking_items(multiworld, self.player)
-
-
-class MessengerHardRules(MessengerRules):
- extra_rules: Dict[str, Callable[[CollectionState], bool]]
-
- def __init__(self, world: MessengerWorld) -> None:
- super().__init__(world)
-
- self.region_rules.update({
- "Ninja Village": self.has_vertical,
- "Autumn Hills": self.has_vertical,
- "Catacombs": self.has_vertical,
- "Bamboo Creek": self.has_vertical,
- "Riviere Turquoise": self.true,
- "Forlorn Temple": lambda state: self.has_vertical(state) and state.has_all(set(PHOBEKINS), self.player),
- "Searing Crags Upper": lambda state: self.can_destroy_projectiles(state) or self.has_windmill(state)
- or self.has_vertical(state),
- "Glacial Peak": lambda state: self.can_destroy_projectiles(state) or self.has_windmill(state)
- or self.has_vertical(state),
- "Elemental Skylands": lambda state: state.has("Magic Firefly", self.player) or
- self.has_windmill(state) or
- self.has_dart(state),
- })
-
- self.location_rules.update({
- "Howling Grotto Seal - Windy Saws and Balls": self.true,
- "Searing Crags Seal - Triple Ball Spinner": self.true,
- "Searing Crags Seal - Raining Rocks": lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state),
- "Searing Crags Seal - Rhythm Rocks": lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state),
- "Searing Crags - Power Thistle": lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state),
- "Glacial Peak Seal - Ice Climbers": self.has_vertical,
- "Glacial Peak Seal - Projectile Spike Pit": self.true,
- "Cloud Ruins Seal - Ghost Pit": self.true,
- "Bamboo Creek - Claustro": self.has_wingsuit,
- "Tower of Time Seal - Lantern Climb": self.has_wingsuit,
- "Elemental Skylands Seal - Water": lambda state: self.has_dart(state) or self.can_dboost(state)
- or self.has_windmill(state),
- "Elemental Skylands Seal - Fire": lambda state: (self.has_dart(state) or self.can_dboost(state)
- or self.has_windmill(state)) and
- self.can_destroy_projectiles(state),
- "Earth Mega Shard": lambda state: self.has_dart(state) or self.can_dboost(state) or self.has_windmill(state),
- "Water Mega Shard": lambda state: self.has_dart(state) or self.can_dboost(state) or self.has_windmill(state),
- })
-
- self.extra_rules = {
- "Searing Crags - Key of Strength": lambda state: self.has_dart(state) or self.has_windmill(state),
- "Elemental Skylands - Key of Symbiosis": lambda state: self.has_windmill(state) or self.can_dboost(state),
- "Autumn Hills Seal - Spike Ball Darts": lambda state: (self.has_dart(state) and self.has_windmill(state))
- or self.has_wingsuit(state),
- "Glacial Peak Seal - Glacial Air Swag": self.has_windmill,
- "Glacial Peak Seal - Ice Climbers": lambda state: self.has_wingsuit(state) or self.can_dboost(state),
- "Underworld Seal - Fireball Wave": lambda state: state.has_all({"Lightfoot Tabi", "Windmill Shuriken"},
- self.player),
- }
-
- def has_windmill(self, state: CollectionState) -> bool:
- return state.has("Windmill Shuriken", self.player)
-
- def set_messenger_rules(self) -> None:
- super().set_messenger_rules()
- for loc, rule in self.extra_rules.items():
- if not self.world.multiworld.shuffle_seals[self.player] and "Seal" in loc:
- continue
- if not self.world.multiworld.shuffle_shards[self.player] and "Shard" in loc:
- continue
- add_rule(self.world.multiworld.get_location(loc, self.player), rule, "or")
-
-
-class MessengerOOBRules(MessengerRules):
- def __init__(self, world: MessengerWorld) -> None:
- self.world = world
- self.player = world.player
-
- self.region_rules = {
- "Elemental Skylands":
- lambda state: state.has_any({"Windmill Shuriken", "Wingsuit", "Rope Dart", "Magic Firefly"}, self.player),
- "Music Box": lambda state: state.has_all(set(NOTES), self.player)
- }
-
- self.location_rules = {
- "Bamboo Creek - Claustro": self.has_wingsuit,
- "Searing Crags - Key of Strength": self.has_wingsuit,
- "Sunken Shrine - Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player),
- "Searing Crags - Pyro": self.has_tabi,
- "Underworld - Key of Chaos": self.has_tabi,
- "Corrupted Future - Key of Courage":
- lambda state: state.has_all({"Demon King Crown", "Magic Firefly"}, self.player),
- "Autumn Hills Seal - Spike Ball Darts": self.has_dart,
- "Ninja Village Seal - Tree House": self.has_dart,
- "Underworld Seal - Fireball Wave": lambda state: state.has_any({"Wingsuit", "Windmill Shuriken"},
- self.player),
- "Tower of Time Seal - Time Waster": self.has_dart,
- "Shop Chest": self.has_enough_seals
- }
-
- def set_messenger_rules(self) -> None:
- super().set_messenger_rules()
- self.world.multiworld.completion_condition[self.player] = lambda state: True
- self.world.multiworld.accessibility[self.player].value = MessengerAccessibility.option_minimal
-
-
-def set_self_locking_items(multiworld: MultiWorld, player: int) -> None:
- # do the ones for seal shuffle on and off first
- allow_self_locking_items(multiworld.get_location("Searing Crags - Key of Strength", player), "Power Thistle")
- allow_self_locking_items(multiworld.get_location("Sunken Shrine - Key of Love", player), "Sun Crest", "Moon Crest")
- allow_self_locking_items(multiworld.get_location("Corrupted Future - Key of Courage", player), "Demon King Crown")
-
- # add these locations when seals are shuffled
- if multiworld.shuffle_seals[player]:
- allow_self_locking_items(multiworld.get_location("Elemental Skylands Seal - Water", player), "Currents Master")
- # add these locations when seals and shards aren't shuffled
- elif not multiworld.shuffle_shards[player]:
- allow_self_locking_items(multiworld.get_region("Cloud Ruins Right", player), "Ruxxtin's Amulet")
- allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS)
diff --git a/worlds/messenger/Shop.py b/worlds/messenger/Shop.py
deleted file mode 100644
index f0915f5d02cb..000000000000
--- a/worlds/messenger/Shop.py
+++ /dev/null
@@ -1,100 +0,0 @@
-from typing import Dict, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Union
-
-if TYPE_CHECKING:
- from . import MessengerWorld
-else:
- MessengerWorld = object
-
-PROG_SHOP_ITEMS: List[str] = [
- "Path of Resilience",
- "Meditation",
- "Strike of the Ninja",
- "Second Wind",
- "Currents Master",
- "Aerobatics Warrior",
-]
-
-USEFUL_SHOP_ITEMS: List[str] = [
- "Karuta Plates",
- "Serendipitous Bodies",
- "Kusari Jacket",
- "Energy Shuriken",
- "Serendipitous Minds",
- "Rejuvenate Spirit",
- "Demon's Bane",
-]
-
-
-class ShopData(NamedTuple):
- internal_name: str
- min_price: int
- max_price: int
- prerequisite: Optional[Union[str, Set[str]]] = None
-
-
-SHOP_ITEMS: Dict[str, ShopData] = {
- "Karuta Plates": ShopData("HP_UPGRADE_1", 20, 200),
- "Serendipitous Bodies": ShopData("ENEMY_DROP_HP", 20, 300, "The Shop - Karuta Plates"),
- "Path of Resilience": ShopData("DAMAGE_REDUCTION", 100, 500, "The Shop - Serendipitous Bodies"),
- "Kusari Jacket": ShopData("HP_UPGRADE_2", 100, 500, "The Shop - Serendipitous Bodies"),
- "Energy Shuriken": ShopData("SHURIKEN", 20, 200),
- "Serendipitous Minds": ShopData("ENEMY_DROP_MANA", 20, 300, "The Shop - Energy Shuriken"),
- "Prepared Mind": ShopData("SHURIKEN_UPGRADE_1", 100, 600, "The Shop - Serendipitous Minds"),
- "Meditation": ShopData("CHECKPOINT_FULL", 100, 600,
- {"The Shop - Prepared Mind", "The Shop - Kusari Jacket"}),
- "Rejuvenative Spirit": ShopData("POTION_FULL_HEAL_AND_HP", 300, 800, "The Shop - Meditation"),
- "Centered Mind": ShopData("SHURIKEN_UPGRADE_2", 300, 800, "The Shop - Meditation"),
- "Strike of the Ninja": ShopData("ATTACK_PROJECTILE", 20, 200),
- "Second Wind": ShopData("AIR_RECOVER", 20, 350, "The Shop - Strike of the Ninja"),
- "Currents Master": ShopData("SWIM_DASH", 100, 600, "The Shop - Second Wind"),
- "Aerobatics Warrior": ShopData("GLIDE_ATTACK", 300, 800, "The Shop - Currents Master"),
- "Demon's Bane": ShopData("CHARGED_ATTACK", 400, 1000,
- {"The Shop - Rejuvenative Spirit", "The Shop - Aerobatics Warrior"}),
- "Devil's Due": ShopData("QUARBLE_DISCOUNT_50", 20, 200),
- "Time Sense": ShopData("TIME_WARP", 20, 300),
- "Power Sense": ShopData("POWER_SEAL", 100, 800, "The Shop - Time Sense"),
- "Focused Power Sense": ShopData("POWER_SEAL_WORLD_MAP", 300, 600, "The Shop - Power Sense"),
-}
-
-FIGURINES: Dict[str, ShopData] = {
- "Green Kappa Figurine": ShopData("GREEN_KAPPA", 100, 500),
- "Blue Kappa Figurine": ShopData("BLUE_KAPPA", 100, 500),
- "Ountarde Figurine": ShopData("OUNTARDE", 100, 500),
- "Red Kappa Figurine": ShopData("RED_KAPPA", 100, 500),
- "Demon King Figurine": ShopData("DEMON_KING", 600, 2000),
- "Quillshroom Figurine": ShopData("QUILLSHROOM", 100, 500),
- "Jumping Quillshroom Figurine": ShopData("JUMPING_QUILLSHROOM", 100, 500),
- "Scurubu Figurine": ShopData("SCURUBU", 100, 500),
- "Jumping Scurubu Figurine": ShopData("JUMPING_SCURUBU", 100, 500),
- "Wallaxer Figurine": ShopData("WALLAXER", 100, 500),
- "Barmath'azel Figurine": ShopData("BARMATHAZEL", 600, 2000),
- "Queen of Quills Figurine": ShopData("QUEEN_OF_QUILLS", 400, 1000),
- "Demon Hive Figurine": ShopData("DEMON_HIVE", 100, 500),
-}
-
-
-def shuffle_shop_prices(world: MessengerWorld) -> Tuple[Dict[str, int], Dict[str, int]]:
- shop_price_mod = world.multiworld.shop_price[world.player].value
- shop_price_planned = world.multiworld.shop_price_plan[world.player]
-
- shop_prices: Dict[str, int] = {}
- figurine_prices: Dict[str, int] = {}
- for item, price in shop_price_planned.value.items():
- if not isinstance(price, int):
- price = world.random.choices(list(price.keys()), weights=list(price.values()))[0]
- if "Figurine" in item:
- figurine_prices[item] = price
- else:
- shop_prices[item] = price
-
- remaining_slots = [item for item in [*SHOP_ITEMS, *FIGURINES] if item not in shop_price_planned.value]
- for shop_item in remaining_slots:
- shop_data = SHOP_ITEMS.get(shop_item, FIGURINES.get(shop_item))
- price = world.random.randint(shop_data.min_price, shop_data.max_price)
- adjusted_price = min(int(price * shop_price_mod / 100), 5000)
- if "Figurine" in shop_item:
- figurine_prices[shop_item] = adjusted_price
- else:
- shop_prices[shop_item] = adjusted_price
-
- return shop_prices, figurine_prices
diff --git a/worlds/messenger/SubClasses.py b/worlds/messenger/SubClasses.py
deleted file mode 100644
index 717b38987870..000000000000
--- a/worlds/messenger/SubClasses.py
+++ /dev/null
@@ -1,95 +0,0 @@
-from functools import cached_property
-from typing import Optional, TYPE_CHECKING, cast
-
-from BaseClasses import CollectionState, Item, ItemClassification, Location, Region
-from .Constants import NOTES, PHOBEKINS, PROG_ITEMS, USEFUL_ITEMS
-from .Options import Goal
-from .Regions import MEGA_SHARDS, REGIONS, SEALS
-from .Shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS
-
-if TYPE_CHECKING:
- from . import MessengerWorld
-else:
- MessengerWorld = object
-
-
-class MessengerRegion(Region):
- def __init__(self, name: str, world: MessengerWorld) -> None:
- super().__init__(name, world.player, world.multiworld)
- locations = [loc for loc in REGIONS[self.name]]
- if self.name == "The Shop":
- if self.multiworld.goal[self.player] > Goal.option_open_music_box:
- locations.append("Shop Chest")
- shop_locations = {f"The Shop - {shop_loc}": world.location_name_to_id[f"The Shop - {shop_loc}"]
- for shop_loc in SHOP_ITEMS}
- shop_locations.update(**{figurine: world.location_name_to_id[figurine] for figurine in FIGURINES})
- self.add_locations(shop_locations, MessengerShopLocation)
- elif self.name == "Tower HQ":
- locations.append("Money Wrench")
- if self.multiworld.shuffle_seals[self.player] and self.name in SEALS:
- locations += [seal_loc for seal_loc in SEALS[self.name]]
- if self.multiworld.shuffle_shards[self.player] and self.name in MEGA_SHARDS:
- locations += [shard for shard in MEGA_SHARDS[self.name]]
- loc_dict = {loc: world.location_name_to_id[loc] if loc in world.location_name_to_id else None
- for loc in locations}
- self.add_locations(loc_dict, MessengerLocation)
- world.multiworld.regions.append(self)
-
-
-class MessengerLocation(Location):
- game = "The Messenger"
-
- def __init__(self, player: int, name: str, loc_id: Optional[int], parent: MessengerRegion) -> None:
- super().__init__(player, name, loc_id, parent)
- if loc_id is None:
- self.place_locked_item(MessengerItem(name, parent.player, None))
-
-
-class MessengerShopLocation(MessengerLocation):
- @cached_property
- def cost(self) -> int:
- name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped
- world: MessengerWorld = self.parent_region.multiworld.worlds[self.player]
- # short circuit figurines which all require demon's bane be purchased, but nothing else
- if "Figurine" in name:
- return world.figurine_prices[name] +\
- cast(MessengerShopLocation, world.multiworld.get_location("The Shop - Demon's Bane", self.player)).cost
- shop_data = SHOP_ITEMS[name]
- if shop_data.prerequisite:
- prereq_cost = 0
- if isinstance(shop_data.prerequisite, set):
- for prereq in shop_data.prerequisite:
- prereq_cost +=\
- cast(MessengerShopLocation,
- world.multiworld.get_location(prereq, self.player)).cost
- else:
- prereq_cost +=\
- cast(MessengerShopLocation,
- world.multiworld.get_location(shop_data.prerequisite, self.player)).cost
- return world.shop_prices[name] + prereq_cost
- return world.shop_prices[name]
-
- def can_afford(self, state: CollectionState) -> bool:
- world: MessengerWorld = state.multiworld.worlds[self.player]
- cost = self.cost
- can_afford = state.has("Shards", self.player, min(cost, world.total_shards))
- if "Figurine" in self.name:
- can_afford = state.has("Money Wrench", self.player) and can_afford\
- and state.can_reach("Money Wrench", "Location", self.player)
- return can_afford
-
-
-class MessengerItem(Item):
- game = "The Messenger"
-
- def __init__(self, name: str, player: int, item_id: Optional[int] = None, override_progression: bool = False,
- count: int = 0) -> None:
- if count:
- item_class = ItemClassification.progression_skip_balancing
- elif item_id is None or override_progression or name in {*NOTES, *PROG_ITEMS, *PHOBEKINS, *PROG_SHOP_ITEMS}:
- item_class = ItemClassification.progression
- elif name in {*USEFUL_ITEMS, *USEFUL_SHOP_ITEMS}:
- item_class = ItemClassification.useful
- else:
- item_class = ItemClassification.filler
- super().__init__(name, item_class, item_id, player)
diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py
index b37f23749df5..a03c33c2f7b6 100644
--- a/worlds/messenger/__init__.py
+++ b/worlds/messenger/__init__.py
@@ -1,14 +1,34 @@
import logging
-from typing import Dict, Any, List, Optional
+from typing import Any, ClassVar, Dict, List, Optional, Set, TextIO
-from BaseClasses import Tutorial, ItemClassification, CollectionState, Item, MultiWorld
-from worlds.AutoWorld import World, WebWorld
-from .Constants import NOTES, PHOBEKINS, ALL_ITEMS, ALWAYS_LOCATIONS, BOSS_LOCATIONS, FILLER
-from .Options import messenger_options, NotesNeeded, Goal, PowerSeals, Logic
-from .Regions import REGIONS, REGION_CONNECTIONS, SEALS, MEGA_SHARDS
-from .Shop import SHOP_ITEMS, shuffle_shop_prices, FIGURINES
-from .SubClasses import MessengerRegion, MessengerItem
-from . import Rules
+from BaseClasses import CollectionState, Entrance, Item, ItemClassification, MultiWorld, Tutorial
+from Options import Accessibility
+from Utils import output_path
+from settings import FilePath, Group
+from worlds.AutoWorld import WebWorld, World
+from worlds.LauncherComponents import Component, Type, components
+from .client_setup import launch_game
+from .connections import CONNECTIONS, RANDOMIZED_CONNECTIONS, TRANSITIONS
+from .constants import ALL_ITEMS, ALWAYS_LOCATIONS, BOSS_LOCATIONS, FILLER, NOTES, PHOBEKINS, PROG_ITEMS, TRAPS, \
+ USEFUL_ITEMS
+from .options import AvailablePortals, Goal, Logic, MessengerOptions, NotesNeeded, ShuffleTransitions
+from .portals import PORTALS, add_closed_portal_reqs, disconnect_portals, shuffle_portals, validate_portals
+from .regions import LEVELS, MEGA_SHARDS, LOCATIONS, REGION_CONNECTIONS
+from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules
+from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shuffle_shop_prices
+from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation
+
+components.append(
+ Component("The Messenger", component_type=Type.CLIENT, func=launch_game)#, game_name="The Messenger", supports_uri=True)
+)
+
+
+class MessengerSettings(Group):
+ class GamePath(FilePath):
+ description = "The Messenger game executable"
+ is_exe = True
+
+ game_path: GamePath = GamePath("TheMessenger.exe")
class MessengerWeb(WebWorld):
@@ -17,7 +37,7 @@ class MessengerWeb(WebWorld):
bug_report_page = "https://github.com/alwaysintreble/TheMessengerRandomizerModAP/issues"
tut_en = Tutorial(
- "Multiworld Setup Tutorial",
+ "Multiworld Setup Guide",
"A guide to setting up The Messenger randomizer on your computer.",
"English",
"setup_en.md",
@@ -35,16 +55,10 @@ class MessengerWorld(World):
adventure full of thrills, surprises, and humor.
"""
game = "The Messenger"
-
- item_name_groups = {
- "Notes": set(NOTES),
- "Keys": set(NOTES),
- "Crest": {"Sun Crest", "Moon Crest"},
- "Phobe": set(PHOBEKINS),
- "Phobekin": set(PHOBEKINS),
- }
-
- option_definitions = messenger_options
+ options_dataclass = MessengerOptions
+ options: MessengerOptions
+ settings_key = "messenger_settings"
+ settings: ClassVar[MessengerSettings]
base_offset = 0xADD_000
item_name_to_id = {item: item_id
@@ -53,145 +67,329 @@ class MessengerWorld(World):
for location_id, location in
enumerate([
*ALWAYS_LOCATIONS,
- *[seal for seals in SEALS.values() for seal in seals],
*[shard for shards in MEGA_SHARDS.values() for shard in shards],
*BOSS_LOCATIONS,
*[f"The Shop - {shop_loc}" for shop_loc in SHOP_ITEMS],
*FIGURINES,
"Money Wrench",
], base_offset)}
+ item_name_groups = {
+ "Notes": set(NOTES),
+ "Keys": set(NOTES),
+ "Crest": {"Sun Crest", "Moon Crest"},
+ "Phobe": set(PHOBEKINS),
+ "Phobekin": set(PHOBEKINS),
+ }
+ location_name_groups = {
+ "Notes": {
+ "Autumn Hills - Key of Hope",
+ "Searing Crags - Key of Strength",
+ "Underworld - Key of Chaos",
+ "Sunken Shrine - Key of Love",
+ "Elemental Skylands - Key of Symbiosis",
+ "Corrupted Future - Key of Courage",
+ },
+ "Keys": {
+ "Autumn Hills - Key of Hope",
+ "Searing Crags - Key of Strength",
+ "Underworld - Key of Chaos",
+ "Sunken Shrine - Key of Love",
+ "Elemental Skylands - Key of Symbiosis",
+ "Corrupted Future - Key of Courage",
+ },
+ "Phobe": {
+ "Catacombs - Necro",
+ "Bamboo Creek - Claustro",
+ "Searing Crags - Pyro",
+ "Cloud Ruins - Acro",
+ },
+ "Phobekin": {
+ "Catacombs - Necro",
+ "Bamboo Creek - Claustro",
+ "Searing Crags - Pyro",
+ "Cloud Ruins - Acro",
+ },
+ }
- data_version = 3
- required_client_version = (0, 4, 0)
+ required_client_version = (0, 4, 4)
web = MessengerWeb()
total_seals: int = 0
required_seals: int = 0
- total_shards: int
+ created_seals: int = 0
+ total_shards: int = 0
shop_prices: Dict[str, int]
figurine_prices: Dict[str, int]
_filler_items: List[str]
-
- def __init__(self, multiworld: MultiWorld, player: int):
- super().__init__(multiworld, player)
- self.total_shards = 0
+ starting_portals: List[str]
+ plando_portals: List[str]
+ spoiler_portal_mapping: Dict[str, str]
+ portal_mapping: List[int]
+ transitions: List[Entrance]
+ reachable_locs: int = 0
+ filler: Dict[str, int]
def generate_early(self) -> None:
- if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
- self.multiworld.shuffle_seals[self.player].value = PowerSeals.option_true
- self.total_seals = self.multiworld.total_seals[self.player].value
+ if self.options.goal == Goal.option_power_seal_hunt:
+ self.total_seals = self.options.total_seals.value
+
+ if self.options.limited_movement:
+ self.options.accessibility.value = Accessibility.option_minimal
+ if self.options.logic_level < Logic.option_hard:
+ self.options.logic_level.value = Logic.option_hard
+
+ if self.options.early_meditation:
+ self.multiworld.early_items[self.player]["Meditation"] = 1
self.shop_prices, self.figurine_prices = shuffle_shop_prices(self)
+ starting_portals = ["Autumn Hills", "Howling Grotto", "Glacial Peak", "Riviere Turquoise", "Sunken Shrine", "Searing Crags"]
+ self.starting_portals = [f"{portal} Portal"
+ for portal in starting_portals[:3] +
+ self.random.sample(starting_portals[3:], k=self.options.available_portals - 3)]
+
+ # super complicated method for adding searing crags to starting portals if it wasn't chosen
+ # TODO add a check for transition shuffle when that gets added back in
+ if not self.options.shuffle_portals and "Searing Crags Portal" not in self.starting_portals:
+ self.starting_portals.append("Searing Crags Portal")
+ portals_to_strip = [portal for portal in ["Riviere Turquoise Portal", "Sunken Shrine Portal"]
+ if portal in self.starting_portals]
+ if portals_to_strip:
+ self.starting_portals.remove(self.random.choice(portals_to_strip))
+
+ self.filler = FILLER.copy()
+ if self.options.traps:
+ self.filler.update(TRAPS)
+
+ self.plando_portals = []
+ self.portal_mapping = []
+ self.spoiler_portal_mapping = {}
+ self.transitions = []
+
def create_regions(self) -> None:
- for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]:
- if region.name in REGION_CONNECTIONS:
- region.add_exits(REGION_CONNECTIONS[region.name])
+ # MessengerRegion adds itself to the multiworld
+ # create simple regions
+ simple_regions = [MessengerRegion(level, self) for level in LEVELS]
+ # create complex regions that have sub-regions
+ complex_regions = [MessengerRegion(f"{parent} - {reg_name}", self, parent)
+ for parent, sub_region in CONNECTIONS.items()
+ for reg_name in sub_region]
+
+ for region in complex_regions:
+ region_name = region.name.replace(f"{region.parent} - ", "")
+ connection_data = CONNECTIONS[region.parent][region_name]
+ for exit_region in connection_data:
+ region.connect(self.multiworld.get_region(exit_region, self.player))
+
+ # all regions need to be created before i can do these connections so we create and connect the complex first
+ for region in [level for level in simple_regions if level.name in REGION_CONNECTIONS]:
+ region.add_exits(REGION_CONNECTIONS[region.name])
def create_items(self) -> None:
# create items that are always in the item pool
- itempool = [
+ main_movement_items = ["Rope Dart", "Wingsuit"]
+ precollected_names = [item.name for item in self.multiworld.precollected_items[self.player]]
+ itempool: List[MessengerItem] = [
self.create_item(item)
for item in self.item_name_to_id
- if item not in
- {
- "Power Seal", *NOTES, *FIGURINES,
- *{collected_item.name for collected_item in self.multiworld.precollected_items[self.player]},
- } and "Time Shard" not in item
+ if item not in {
+ "Power Seal", *NOTES, *FIGURINES, *main_movement_items,
+ *precollected_names, *FILLER, *TRAPS,
+ }
]
- if self.multiworld.goal[self.player] == Goal.option_open_music_box:
+ if self.options.limited_movement:
+ itempool.append(self.create_item(self.random.choice(main_movement_items)))
+ else:
+ itempool += [self.create_item(move_item) for move_item in main_movement_items]
+
+ if self.options.goal == Goal.option_open_music_box:
# make a list of all notes except those in the player's defined starting inventory, and adjust the
# amount we need to put in the itempool and precollect based on that
- notes = [note for note in NOTES if note not in self.multiworld.precollected_items[self.player]]
+ notes = [note for note in NOTES if note not in precollected_names]
self.random.shuffle(notes)
precollected_notes_amount = NotesNeeded.range_end - \
- self.multiworld.notes_needed[self.player] - \
- (len(NOTES) - len(notes))
+ self.options.notes_needed - \
+ (len(NOTES) - len(notes))
if precollected_notes_amount:
for note in notes[:precollected_notes_amount]:
self.multiworld.push_precollected(self.create_item(note))
notes = notes[precollected_notes_amount:]
itempool += [self.create_item(note) for note in notes]
- elif self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
+ elif self.options.goal == Goal.option_power_seal_hunt:
total_seals = min(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool),
- self.multiworld.total_seals[self.player].value)
+ self.options.total_seals.value)
if total_seals < self.total_seals:
- logging.warning(f"Not enough locations for total seals setting "
- f"({self.multiworld.total_seals[self.player].value}). Adjusting to {total_seals}")
+ logging.warning(
+ f"Not enough locations for total seals setting "
+ f"({self.options.total_seals}). Adjusting to {total_seals}"
+ )
self.total_seals = total_seals
- self.required_seals =\
- int(self.multiworld.percent_seals_required[self.player].value / 100 * self.total_seals)
+ self.required_seals = int(self.options.percent_seals_required.value / 100 * self.total_seals)
seals = [self.create_item("Power Seal") for _ in range(self.total_seals)]
- for i in range(self.required_seals):
- seals[i].classification = ItemClassification.progression_skip_balancing
itempool += seals
+ self.multiworld.itempool += itempool
remaining_fill = len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool)
if remaining_fill < 10:
self._filler_items = self.random.choices(
- list(FILLER)[2:],
- weights=list(FILLER.values())[2:],
- k=remaining_fill
+ list(self.filler)[2:],
+ weights=list(self.filler.values())[2:],
+ k=remaining_fill
)
- itempool += [self.create_filler() for _ in range(remaining_fill)]
+ filler = [self.create_filler() for _ in range(remaining_fill)]
- self.multiworld.itempool += itempool
+ self.multiworld.itempool += filler
def set_rules(self) -> None:
- logic = self.multiworld.logic_level[self.player]
+ logic = self.options.logic_level
if logic == Logic.option_normal:
- Rules.MessengerRules(self).set_messenger_rules()
+ MessengerRules(self).set_messenger_rules()
elif logic == Logic.option_hard:
- Rules.MessengerHardRules(self).set_messenger_rules()
+ MessengerHardRules(self).set_messenger_rules()
else:
- Rules.MessengerOOBRules(self).set_messenger_rules()
+ raise ValueError(f"Somehow you have a logic option that's currently invalid."
+ f" {logic} for {self.multiworld.get_player_name(self.player)}")
+ # MessengerOOBRules(self).set_messenger_rules()
- def fill_slot_data(self) -> Dict[str, Any]:
- shop_prices = {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()}
- figure_prices = {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}
+ add_closed_portal_reqs(self)
+ # i need portal shuffle to happen after rules exist so i can validate it
+ attempts = 5
+ if self.options.shuffle_portals:
+ self.portal_mapping = []
+ self.spoiler_portal_mapping = {}
+ for _ in range(attempts):
+ disconnect_portals(self)
+ shuffle_portals(self)
+ if validate_portals(self):
+ break
+ # failsafe mostly for invalid plandoed portals with no transition shuffle
+ else:
+ raise RuntimeError("Unable to generate valid portal output.")
- return {
- "deathlink": self.multiworld.death_link[self.player].value,
- "goal": self.multiworld.goal[self.player].current_key,
- "music_box": self.multiworld.music_box[self.player].value,
- "required_seals": self.required_seals,
- "mega_shards": self.multiworld.shuffle_shards[self.player].value,
- "logic": self.multiworld.logic_level[self.player].current_key,
- "shop": shop_prices,
- "figures": figure_prices,
+ def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
+ if self.options.available_portals < 6:
+ spoiler_handle.write(f"\nStarting Portals:\n\n")
+ for portal in self.starting_portals:
+ spoiler_handle.write(f"{portal}\n")
+
+ spoiler = self.multiworld.spoiler
+
+ if self.options.shuffle_portals:
+ # sort the portals as they appear left to right in-game
+ portal_info = sorted(
+ self.spoiler_portal_mapping.items(),
+ key=lambda portal:
+ ["Autumn Hills", "Riviere Turquoise",
+ "Howling Grotto", "Sunken Shrine",
+ "Searing Crags", "Glacial Peak"].index(portal[0]))
+ for portal, output in portal_info:
+ spoiler.set_entrance(f"{portal} Portal", output, "I can write anything I want here lmao", self.player)
+
+ def fill_slot_data(self) -> Dict[str, Any]:
+ slot_data = {
+ "shop": {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()},
+ "figures": {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()},
"max_price": self.total_shards,
+ "required_seals": self.required_seals,
+ "starting_portals": self.starting_portals,
+ "portal_exits": self.portal_mapping,
+ "transitions": [[TRANSITIONS.index("Corrupted Future") if transition.name == "Artificer's Portal"
+ else TRANSITIONS.index(RANDOMIZED_CONNECTIONS[transition.parent_region.name]),
+ TRANSITIONS.index(transition.connected_region.name)]
+ for transition in self.transitions],
+ **self.options.as_dict("music_box", "death_link", "logic_level"),
}
+ return slot_data
def get_filler_item_name(self) -> str:
if not getattr(self, "_filler_items", None):
self._filler_items = [name for name in self.random.choices(
- list(FILLER),
- weights=list(FILLER.values()),
+ list(self.filler),
+ weights=list(self.filler.values()),
k=20
)]
return self._filler_items.pop(0)
def create_item(self, name: str) -> MessengerItem:
item_id: Optional[int] = self.item_name_to_id.get(name, None)
- override_prog = getattr(self, "multiworld") is not None and \
- name in {"Windmill Shuriken"} and \
- self.multiworld.logic_level[self.player] > Logic.option_normal
- count = 0
+ return MessengerItem(
+ name,
+ ItemClassification.progression if item_id is None else self.get_item_classification(name),
+ item_id,
+ self.player
+ )
+
+ def get_item_classification(self, name: str) -> ItemClassification:
if "Time Shard " in name:
count = int(name.strip("Time Shard ()"))
count = count if count >= 100 else 0
self.total_shards += count
- return MessengerItem(name, self.player, item_id, override_prog, count)
+ return ItemClassification.progression_skip_balancing if count else ItemClassification.filler
+
+ if name == "Windmill Shuriken" and getattr(self, "multiworld", None) is not None:
+ return ItemClassification.progression if self.options.logic_level else ItemClassification.filler
+
+ if name == "Power Seal":
+ self.created_seals += 1
+ return ItemClassification.progression_skip_balancing \
+ if self.required_seals >= self.created_seals else ItemClassification.filler
- def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]:
- if item.advancement and "Time Shard" in item.name:
- shard_count = int(item.name.strip("Time Shard ()"))
- if remove:
- shard_count = -shard_count
- state.prog_items["Shards", self.player] += shard_count
+ if name in {*NOTES, *PROG_ITEMS, *PHOBEKINS, *PROG_SHOP_ITEMS}:
+ return ItemClassification.progression
- return super().collect_item(state, item, remove)
+ if name in {*USEFUL_ITEMS, *USEFUL_SHOP_ITEMS}:
+ return ItemClassification.useful
+
+ if name in TRAPS:
+ return ItemClassification.trap
+
+ return ItemClassification.filler
+
+ @classmethod
+ def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World:
+ group = super().create_group(multiworld, new_player_id, players)
+ assert isinstance(group, MessengerWorld)
+
+ group.filler = FILLER.copy()
+ group.options.traps.value = all(multiworld.worlds[player].options.traps for player in players)
+ if group.options.traps:
+ group.filler.update(TRAPS)
+ return group
+
+ def collect(self, state: "CollectionState", item: "Item") -> bool:
+ change = super().collect(state, item)
+ if change and "Time Shard" in item.name:
+ state.prog_items[self.player]["Shards"] += int(item.name.strip("Time Shard ()"))
+ return change
+
+ def remove(self, state: "CollectionState", item: "Item") -> bool:
+ change = super().remove(state, item)
+ if change and "Time Shard" in item.name:
+ state.prog_items[self.player]["Shards"] -= int(item.name.strip("Time Shard ()"))
+ return change
+
+ @classmethod
+ def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str) -> None:
+ # using stage_generate_output because it doesn't increase the logged player count for players without output
+ # only generate output if there's a single player
+ if multiworld.players > 1:
+ return
+ # the messenger client calls into AP with specific args, so check the out path matches what the client sends
+ out_path = output_path(multiworld.get_out_file_name_base(1) + ".aptm")
+ if "The Messenger\\Archipelago\\output" not in out_path:
+ return
+ import orjson
+ data = {
+ "name": multiworld.get_player_name(1),
+ "slot_data": multiworld.worlds[1].fill_slot_data(),
+ "loc_data": {loc.address: {loc.item.name: [loc.item.code, loc.item.flags]}
+ for loc in multiworld.get_filled_locations() if loc.address},
+ }
+
+ output = orjson.dumps(data, option=orjson.OPT_NON_STR_KEYS)
+ with open(out_path, "wb") as f:
+ f.write(output)
diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py
new file mode 100644
index 000000000000..9fd08e52d899
--- /dev/null
+++ b/worlds/messenger/client_setup.py
@@ -0,0 +1,164 @@
+import io
+import logging
+import os.path
+import subprocess
+import urllib.request
+from shutil import which
+from tkinter.messagebox import askyesnocancel
+from typing import Any, Optional
+from zipfile import ZipFile
+from Utils import open_file
+
+import requests
+
+from Utils import is_windows, messagebox, tuplize_version
+
+
+MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest"
+
+
+def launch_game(url: Optional[str] = None) -> None:
+ """Check the game installation, then launch it"""
+ def courier_installed() -> bool:
+ """Check if Courier is installed"""
+ return os.path.exists(os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.Courier.mm.dll"))
+
+ def mod_installed() -> bool:
+ """Check if the mod is installed"""
+ return os.path.exists(os.path.join(game_folder, "Mods", "TheMessengerRandomizerAP", "courier.toml"))
+
+ def request_data(request_url: str) -> Any:
+ """Fetches json response from given url"""
+ logging.info(f"requesting {request_url}")
+ response = requests.get(request_url)
+ if response.status_code == 200: # success
+ try:
+ data = response.json()
+ except requests.exceptions.JSONDecodeError:
+ raise RuntimeError(f"Unable to fetch data. (status code {response.status_code})")
+ else:
+ raise RuntimeError(f"Unable to fetch data. (status code {response.status_code})")
+ return data
+
+ def install_courier() -> None:
+ """Installs latest version of Courier"""
+ # can't use latest since courier uses pre-release tags
+ courier_url = "https://api.github.com/repos/Brokemia/Courier/releases"
+ latest_download = request_data(courier_url)[0]["assets"][-1]["browser_download_url"]
+
+ with urllib.request.urlopen(latest_download) as download:
+ with ZipFile(io.BytesIO(download.read()), "r") as zf:
+ for member in zf.infolist():
+ zf.extract(member, path=game_folder)
+
+ os.chdir(game_folder)
+ # linux and mac handling
+ if not is_windows:
+ mono_exe = which("mono")
+ if not mono_exe:
+ # steam deck support but doesn't currently work
+ messagebox("Failure", "Failed to install Courier", True)
+ raise RuntimeError("Failed to install Courier")
+ # # download and use mono kickstart
+ # # this allows steam deck support
+ # mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/refs/heads/master.zip"
+ # target = os.path.join(folder, "monoKickstart")
+ # os.makedirs(target, exist_ok=True)
+ # with urllib.request.urlopen(mono_kick_url) as download:
+ # with ZipFile(io.BytesIO(download.read()), "r") as zf:
+ # for member in zf.infolist():
+ # zf.extract(member, path=target)
+ # installer = subprocess.Popen([os.path.join(target, "precompiled"),
+ # os.path.join(folder, "MiniInstaller.exe")], shell=False)
+ # os.remove(target)
+ else:
+ installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=False)
+ else:
+ installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=False)
+
+ failure = installer.wait()
+ if failure:
+ messagebox("Failure", "Failed to install Courier", True)
+ os.chdir(working_directory)
+ raise RuntimeError("Failed to install Courier")
+ os.chdir(working_directory)
+
+ if courier_installed():
+ messagebox("Success!", "Courier successfully installed!")
+ return
+ messagebox("Failure", "Failed to install Courier", True)
+ raise RuntimeError("Failed to install Courier")
+
+ def install_mod() -> None:
+ """Installs latest version of the mod"""
+ assets = request_data(MOD_URL)["assets"]
+ if len(assets) == 1:
+ release_url = assets[0]["browser_download_url"]
+ else:
+ for asset in assets:
+ if "TheMessengerRandomizerAP" in asset["name"]:
+ release_url = asset["browser_download_url"]
+ break
+ else:
+ messagebox("Failure", "Failed to find latest mod download", True)
+ raise RuntimeError("Failed to install Mod")
+
+ mod_folder = os.path.join(game_folder, "Mods")
+ os.makedirs(mod_folder, exist_ok=True)
+ with urllib.request.urlopen(release_url) as download:
+ with ZipFile(io.BytesIO(download.read()), "r") as zf:
+ for member in zf.infolist():
+ zf.extract(member, path=mod_folder)
+
+ messagebox("Success!", "Latest mod successfully installed!")
+
+ def available_mod_update(latest_version: str) -> bool:
+ """Check if there's an available update"""
+ latest_version = latest_version.lstrip("v")
+ toml_path = os.path.join(game_folder, "Mods", "TheMessengerRandomizerAP", "courier.toml")
+ with open(toml_path, "r") as f:
+ installed_version = f.read().splitlines()[1].strip("version = \"")
+
+ logging.info(f"Installed version: {installed_version}. Latest version: {latest_version}")
+ # one of the alpha builds
+ return "alpha" in latest_version or tuplize_version(latest_version) > tuplize_version(installed_version)
+
+ from . import MessengerWorld
+ game_folder = os.path.dirname(MessengerWorld.settings.game_path)
+ working_directory = os.getcwd()
+ if not courier_installed():
+ should_install = askyesnocancel("Install Courier",
+ "No Courier installation detected. Would you like to install now?")
+ if not should_install:
+ return
+ logging.info("Installing Courier")
+ install_courier()
+ if not mod_installed():
+ should_install = askyesnocancel("Install Mod",
+ "No randomizer mod detected. Would you like to install now?")
+ if not should_install:
+ return
+ logging.info("Installing Mod")
+ install_mod()
+ else:
+ latest = request_data(MOD_URL)["tag_name"]
+ if available_mod_update(latest):
+ should_update = askyesnocancel("Update Mod",
+ f"New mod version detected. Would you like to update to {latest} now?")
+ if should_update:
+ logging.info("Updating mod")
+ install_mod()
+ elif should_update is None:
+ return
+ if not is_windows:
+ if url:
+ open_file(f"steam://rungameid/764790//{url}/")
+ else:
+ open_file("steam://rungameid/764790")
+ else:
+ os.chdir(game_folder)
+ if url:
+ subprocess.Popen([MessengerWorld.settings.game_path, str(url)])
+ else:
+ subprocess.Popen(MessengerWorld.settings.game_path)
+ os.chdir(working_directory)
diff --git a/worlds/messenger/connections.py b/worlds/messenger/connections.py
new file mode 100644
index 000000000000..978917c555e1
--- /dev/null
+++ b/worlds/messenger/connections.py
@@ -0,0 +1,725 @@
+from typing import Dict, List
+
+CONNECTIONS: Dict[str, Dict[str, List[str]]] = {
+ "Ninja Village": {
+ "Right": [
+ "Autumn Hills - Left",
+ "Ninja Village - Nest",
+ ],
+ "Nest": [
+ "Ninja Village - Right",
+ ],
+ },
+ "Autumn Hills": {
+ "Left": [
+ "Ninja Village - Right",
+ "Autumn Hills - Climbing Claws Shop",
+ ],
+ "Right": [
+ "Forlorn Temple - Left",
+ "Autumn Hills - Leaf Golem Shop",
+ ],
+ "Bottom": [
+ "Catacombs - Bottom Left",
+ "Autumn Hills - Double Swing Checkpoint",
+ ],
+ "Portal": [
+ "Tower HQ",
+ "Autumn Hills - Dimension Climb Shop",
+ ],
+ "Climbing Claws Shop": [
+ "Autumn Hills - Left",
+ "Autumn Hills - Hope Path Shop",
+ "Autumn Hills - Lakeside Checkpoint",
+ "Autumn Hills - Key of Hope Checkpoint",
+ ],
+ "Hope Path Shop": [
+ "Autumn Hills - Climbing Claws Shop",
+ "Autumn Hills - Hope Latch Checkpoint",
+ "Autumn Hills - Lakeside Checkpoint",
+ ],
+ "Dimension Climb Shop": [
+ "Autumn Hills - Lakeside Checkpoint",
+ "Autumn Hills - Portal",
+ "Autumn Hills - Double Swing Checkpoint",
+ ],
+ "Leaf Golem Shop": [
+ "Autumn Hills - Spike Ball Swing Checkpoint",
+ "Autumn Hills - Right",
+ ],
+ "Hope Latch Checkpoint": [
+ "Autumn Hills - Hope Path Shop",
+ "Autumn Hills - Key of Hope Checkpoint",
+ ],
+ "Key of Hope Checkpoint": [
+ "Autumn Hills - Hope Latch Checkpoint",
+ "Autumn Hills - Lakeside Checkpoint",
+ ],
+ "Lakeside Checkpoint": [
+ "Autumn Hills - Climbing Claws Shop",
+ "Autumn Hills - Dimension Climb Shop",
+ ],
+ "Double Swing Checkpoint": [
+ "Autumn Hills - Dimension Climb Shop",
+ "Autumn Hills - Spike Ball Swing Checkpoint",
+ "Autumn Hills - Bottom",
+ ],
+ "Spike Ball Swing Checkpoint": [
+ "Autumn Hills - Double Swing Checkpoint",
+ "Autumn Hills - Leaf Golem Shop",
+ ],
+ },
+ "Forlorn Temple": {
+ "Left": [
+ "Autumn Hills - Right",
+ "Forlorn Temple - Outside Shop",
+ ],
+ "Right": [
+ "Bamboo Creek - Top Left",
+ "Forlorn Temple - Demon King Shop",
+ ],
+ "Bottom": [
+ "Catacombs - Top Left",
+ "Forlorn Temple - Outside Shop",
+ ],
+ "Outside Shop": [
+ "Forlorn Temple - Left",
+ "Forlorn Temple - Bottom",
+ "Forlorn Temple - Entrance Shop",
+ ],
+ "Entrance Shop": [
+ "Forlorn Temple - Outside Shop",
+ "Forlorn Temple - Sunny Day Checkpoint",
+ ],
+ "Climb Shop": [
+ "Forlorn Temple - Rocket Maze Checkpoint",
+ "Forlorn Temple - Rocket Sunset Shop",
+ ],
+ "Rocket Sunset Shop": [
+ "Forlorn Temple - Climb Shop",
+ "Forlorn Temple - Descent Shop",
+ ],
+ "Descent Shop": [
+ "Forlorn Temple - Rocket Sunset Shop",
+ "Forlorn Temple - Saw Gauntlet Shop",
+ ],
+ "Saw Gauntlet Shop": [
+ "Forlorn Temple - Demon King Shop",
+ ],
+ "Demon King Shop": [
+ "Forlorn Temple - Saw Gauntlet Shop",
+ "Forlorn Temple - Right",
+ ],
+ "Sunny Day Checkpoint": [
+ "Forlorn Temple - Rocket Maze Checkpoint",
+ ],
+ "Rocket Maze Checkpoint": [
+ "Forlorn Temple - Sunny Day Checkpoint",
+ "Forlorn Temple - Climb Shop",
+ ],
+ },
+ "Catacombs": {
+ "Top Left": [
+ "Forlorn Temple - Bottom",
+ "Catacombs - Triple Spike Crushers Shop",
+ ],
+ "Bottom Left": [
+ "Autumn Hills - Bottom",
+ "Catacombs - Triple Spike Crushers Shop",
+ "Catacombs - Death Trap Checkpoint",
+ ],
+ "Bottom": [
+ "Dark Cave - Right",
+ "Catacombs - Dirty Pond Checkpoint",
+ ],
+ "Right": [
+ "Bamboo Creek - Bottom Left",
+ "Catacombs - Ruxxtin Shop",
+ ],
+ "Triple Spike Crushers Shop": [
+ "Catacombs - Bottom Left",
+ "Catacombs - Death Trap Checkpoint",
+ ],
+ "Ruxxtin Shop": [
+ "Catacombs - Right",
+ "Catacombs - Dirty Pond Checkpoint",
+ ],
+ "Death Trap Checkpoint": [
+ "Catacombs - Triple Spike Crushers Shop",
+ "Catacombs - Bottom Left",
+ "Catacombs - Dirty Pond Checkpoint",
+ ],
+ "Crusher Gauntlet Checkpoint": [
+ "Catacombs - Dirty Pond Checkpoint",
+ ],
+ "Dirty Pond Checkpoint": [
+ "Catacombs - Bottom",
+ "Catacombs - Death Trap Checkpoint",
+ "Catacombs - Crusher Gauntlet Checkpoint",
+ "Catacombs - Ruxxtin Shop",
+ ],
+ },
+ "Bamboo Creek": {
+ "Bottom Left": [
+ "Catacombs - Right",
+ "Bamboo Creek - Spike Crushers Shop",
+ ],
+ "Top Left": [
+ "Bamboo Creek - Abandoned Shop",
+ "Forlorn Temple - Right",
+ ],
+ "Right": [
+ "Howling Grotto - Left",
+ "Bamboo Creek - Time Loop Shop",
+ ],
+ "Spike Crushers Shop": [
+ "Bamboo Creek - Bottom Left",
+ "Bamboo Creek - Abandoned Shop",
+ ],
+ "Abandoned Shop": [
+ "Bamboo Creek - Spike Crushers Shop",
+ "Bamboo Creek - Spike Doors Checkpoint",
+ ],
+ "Time Loop Shop": [
+ "Bamboo Creek - Right",
+ "Bamboo Creek - Spike Doors Checkpoint",
+ ],
+ "Spike Ball Pits Checkpoint": [
+ "Bamboo Creek - Spike Doors Checkpoint",
+ ],
+ "Spike Doors Checkpoint": [
+ "Bamboo Creek - Abandoned Shop",
+ "Bamboo Creek - Spike Ball Pits Checkpoint",
+ "Bamboo Creek - Time Loop Shop",
+ ],
+ },
+ "Howling Grotto": {
+ "Left": [
+ "Bamboo Creek - Right",
+ "Howling Grotto - Wingsuit Shop",
+ ],
+ "Top": [
+ "Howling Grotto - Crushing Pits Shop",
+ "Quillshroom Marsh - Bottom Left",
+ ],
+ "Right": [
+ "Howling Grotto - Emerald Golem Shop",
+ "Quillshroom Marsh - Top Left",
+ ],
+ "Bottom": [
+ "Howling Grotto - Lost Woods Checkpoint",
+ "Sunken Shrine - Left",
+ ],
+ "Portal": [
+ "Howling Grotto - Crushing Pits Shop",
+ "Tower HQ",
+ ],
+ "Wingsuit Shop": [
+ "Howling Grotto - Left",
+ "Howling Grotto - Lost Woods Checkpoint",
+ ],
+ "Crushing Pits Shop": [
+ "Howling Grotto - Lost Woods Checkpoint",
+ "Howling Grotto - Portal",
+ "Howling Grotto - Breezy Crushers Checkpoint",
+ "Howling Grotto - Top",
+ ],
+ "Emerald Golem Shop": [
+ "Howling Grotto - Breezy Crushers Checkpoint",
+ "Howling Grotto - Right",
+ ],
+ "Lost Woods Checkpoint": [
+ "Howling Grotto - Wingsuit Shop",
+ "Howling Grotto - Crushing Pits Shop",
+ "Howling Grotto - Bottom",
+ ],
+ "Breezy Crushers Checkpoint": [
+ "Howling Grotto - Crushing Pits Shop",
+ "Howling Grotto - Emerald Golem Shop",
+ ],
+ },
+ "Quillshroom Marsh": {
+ "Top Left": [
+ "Howling Grotto - Right",
+ "Quillshroom Marsh - Seashell Checkpoint",
+ "Quillshroom Marsh - Spikey Window Shop",
+ ],
+ "Bottom Left": [
+ "Howling Grotto - Top",
+ "Quillshroom Marsh - Sand Trap Shop",
+ "Quillshroom Marsh - Bottom Right",
+ ],
+ "Top Right": [
+ "Quillshroom Marsh - Queen of Quills Shop",
+ "Searing Crags - Left",
+ ],
+ "Bottom Right": [
+ "Quillshroom Marsh - Bottom Left",
+ "Quillshroom Marsh - Sand Trap Shop",
+ "Searing Crags - Bottom",
+ ],
+ "Spikey Window Shop": [
+ "Quillshroom Marsh - Top Left",
+ "Quillshroom Marsh - Seashell Checkpoint",
+ "Quillshroom Marsh - Quicksand Checkpoint",
+ ],
+ "Sand Trap Shop": [
+ "Quillshroom Marsh - Quicksand Checkpoint",
+ "Quillshroom Marsh - Bottom Left",
+ "Quillshroom Marsh - Bottom Right",
+ "Quillshroom Marsh - Spike Wave Checkpoint",
+ ],
+ "Queen of Quills Shop": [
+ "Quillshroom Marsh - Spike Wave Checkpoint",
+ "Quillshroom Marsh - Top Right",
+ ],
+ "Seashell Checkpoint": [
+ "Quillshroom Marsh - Top Left",
+ "Quillshroom Marsh - Spikey Window Shop",
+ ],
+ "Quicksand Checkpoint": [
+ "Quillshroom Marsh - Spikey Window Shop",
+ "Quillshroom Marsh - Sand Trap Shop",
+ ],
+ "Spike Wave Checkpoint": [
+ "Quillshroom Marsh - Sand Trap Shop",
+ "Quillshroom Marsh - Queen of Quills Shop",
+ ],
+ },
+ "Searing Crags": {
+ "Left": [
+ "Quillshroom Marsh - Top Right",
+ "Searing Crags - Rope Dart Shop",
+ ],
+ "Top": [
+ "Searing Crags - Colossuses Shop",
+ "Glacial Peak - Bottom",
+ ],
+ "Bottom": [
+ "Searing Crags - Portal",
+ "Quillshroom Marsh - Bottom Right",
+ ],
+ "Right": [
+ "Searing Crags - Portal",
+ "Underworld - Left",
+ ],
+ "Portal": [
+ "Searing Crags - Bottom",
+ "Searing Crags - Right",
+ "Searing Crags - Before Final Climb Shop",
+ "Searing Crags - Colossuses Shop",
+ "Tower HQ",
+ ],
+ "Rope Dart Shop": [
+ "Searing Crags - Left",
+ "Searing Crags - Triple Ball Spinner Checkpoint",
+ ],
+ "Falling Rocks Shop": [
+ "Searing Crags - Triple Ball Spinner Checkpoint",
+ "Searing Crags - Searing Mega Shard Shop",
+ ],
+ "Searing Mega Shard Shop": [
+ "Searing Crags - Falling Rocks Shop",
+ "Searing Crags - Before Final Climb Shop",
+ "Searing Crags - Key of Strength Shop",
+ ],
+ "Before Final Climb Shop": [
+ "Searing Crags - Raining Rocks Checkpoint",
+ "Searing Crags - Portal",
+ "Searing Crags - Colossuses Shop",
+ ],
+ "Colossuses Shop": [
+ "Searing Crags - Before Final Climb Shop",
+ "Searing Crags - Key of Strength Shop",
+ "Searing Crags - Portal",
+ "Searing Crags - Top",
+ ],
+ "Key of Strength Shop": [
+ "Searing Crags - Searing Mega Shard Shop",
+ ],
+ "Triple Ball Spinner Checkpoint": [
+ "Searing Crags - Rope Dart Shop",
+ "Searing Crags - Falling Rocks Shop",
+ ],
+ "Raining Rocks Checkpoint": [
+ "Searing Crags - Searing Mega Shard Shop",
+ "Searing Crags - Before Final Climb Shop",
+ ],
+ },
+ "Glacial Peak": {
+ "Bottom": [
+ "Searing Crags - Top",
+ "Glacial Peak - Ice Climbers' Shop",
+ ],
+ "Left": [
+ "Elemental Skylands - Air Shmup",
+ "Glacial Peak - Projectile Spike Pit Checkpoint",
+ "Glacial Peak - Glacial Mega Shard Shop",
+ ],
+ "Top": [
+ "Glacial Peak - Tower Entrance Shop",
+ "Cloud Ruins - Left",
+ ],
+ "Portal": [
+ "Glacial Peak - Tower Entrance Shop",
+ "Tower HQ",
+ ],
+ "Ice Climbers' Shop": [
+ "Glacial Peak - Bottom",
+ "Glacial Peak - Projectile Spike Pit Checkpoint",
+ ],
+ "Glacial Mega Shard Shop": [
+ "Glacial Peak - Left",
+ "Glacial Peak - Air Swag Checkpoint",
+ ],
+ "Tower Entrance Shop": [
+ "Glacial Peak - Top",
+ "Glacial Peak - Free Climbing Checkpoint",
+ "Glacial Peak - Portal",
+ ],
+ "Projectile Spike Pit Checkpoint": [
+ "Glacial Peak - Ice Climbers' Shop",
+ "Glacial Peak - Left",
+ ],
+ "Air Swag Checkpoint": [
+ "Glacial Peak - Glacial Mega Shard Shop",
+ "Glacial Peak - Free Climbing Checkpoint",
+ ],
+ "Free Climbing Checkpoint": [
+ "Glacial Peak - Air Swag Checkpoint",
+ "Glacial Peak - Tower Entrance Shop",
+ ],
+ },
+ "Tower of Time": {
+ "Left": [
+ "Tower of Time - Final Chance Shop",
+ ],
+ "Final Chance Shop": [
+ "Tower of Time - First Checkpoint",
+ ],
+ "Arcane Golem Shop": [
+ "Tower of Time - Sixth Checkpoint",
+ ],
+ "First Checkpoint": [
+ "Tower of Time - Second Checkpoint",
+ ],
+ "Second Checkpoint": [
+ "Tower of Time - Third Checkpoint",
+ ],
+ "Third Checkpoint": [
+ "Tower of Time - Fourth Checkpoint",
+ ],
+ "Fourth Checkpoint": [
+ "Tower of Time - Fifth Checkpoint",
+ ],
+ "Fifth Checkpoint": [
+ "Tower of Time - Sixth Checkpoint",
+ ],
+ "Sixth Checkpoint": [
+ "Tower of Time - Arcane Golem Shop",
+ ],
+ },
+ "Cloud Ruins": {
+ "Left": [
+ "Glacial Peak - Top",
+ "Cloud Ruins - Cloud Entrance Shop",
+ ],
+ "Cloud Entrance Shop": [
+ "Cloud Ruins - Left",
+ "Cloud Ruins - Spike Float Checkpoint",
+ ],
+ "Pillar Glide Shop": [
+ "Cloud Ruins - Spike Float Checkpoint",
+ "Cloud Ruins - Ghost Pit Checkpoint",
+ "Cloud Ruins - Crushers' Descent Shop",
+ ],
+ "Crushers' Descent Shop": [
+ "Cloud Ruins - Pillar Glide Shop",
+ "Cloud Ruins - Toothbrush Alley Checkpoint",
+ ],
+ "Seeing Spikes Shop": [
+ "Cloud Ruins - Toothbrush Alley Checkpoint",
+ "Cloud Ruins - Sliding Spikes Shop",
+ ],
+ "Sliding Spikes Shop": [
+ "Cloud Ruins - Seeing Spikes Shop",
+ "Cloud Ruins - Saw Pit Checkpoint",
+ ],
+ "Final Flight Shop": [
+ "Cloud Ruins - Saw Pit Checkpoint",
+ "Cloud Ruins - Manfred's Shop",
+ ],
+ "Manfred's Shop": [
+ "Cloud Ruins - Final Flight Shop",
+ ],
+ "Spike Float Checkpoint": [
+ "Cloud Ruins - Cloud Entrance Shop",
+ "Cloud Ruins - Pillar Glide Shop",
+ ],
+ "Ghost Pit Checkpoint": [
+ "Cloud Ruins - Pillar Glide Shop",
+ ],
+ "Toothbrush Alley Checkpoint": [
+ "Cloud Ruins - Crushers' Descent Shop",
+ "Cloud Ruins - Seeing Spikes Shop",
+ ],
+ "Saw Pit Checkpoint": [
+ "Cloud Ruins - Sliding Spikes Shop",
+ "Cloud Ruins - Final Flight Shop",
+ ],
+ },
+ "Underworld": {
+ "Left": [
+ "Underworld - Left Shop",
+ "Searing Crags - Right",
+ ],
+ "Left Shop": [
+ "Underworld - Left",
+ "Underworld - Hot Dip Checkpoint",
+ ],
+ "Fireball Wave Shop": [
+ "Underworld - Hot Dip Checkpoint",
+ "Underworld - Long Climb Shop",
+ ],
+ "Long Climb Shop": [
+ "Underworld - Fireball Wave Shop",
+ "Underworld - Hot Tub Checkpoint",
+ ],
+ "Barm'athaziel Shop": [
+ "Underworld - Hot Tub Checkpoint",
+ ],
+ "Key of Chaos Shop": [
+ ],
+ "Hot Dip Checkpoint": [
+ "Underworld - Left Shop",
+ "Underworld - Fireball Wave Shop",
+ "Underworld - Lava Run Checkpoint",
+ ],
+ "Hot Tub Checkpoint": [
+ "Underworld - Long Climb Shop",
+ "Underworld - Barm'athaziel Shop",
+ ],
+ "Lava Run Checkpoint": [
+ "Underworld - Hot Dip Checkpoint",
+ "Underworld - Key of Chaos Shop",
+ ],
+ },
+ "Dark Cave": {
+ "Right": [
+ "Catacombs - Bottom",
+ "Dark Cave - Left",
+ ],
+ "Left": [
+ "Riviere Turquoise - Right",
+ ],
+ },
+ "Riviere Turquoise": {
+ "Right": [
+ "Riviere Turquoise - Portal",
+ ],
+ "Portal": [
+ "Riviere Turquoise - Waterfall Shop",
+ "Tower HQ",
+ ],
+ "Waterfall Shop": [
+ "Riviere Turquoise - Portal",
+ "Riviere Turquoise - Flower Flight Checkpoint",
+ ],
+ "Launch of Faith Shop": [
+ "Riviere Turquoise - Flower Flight Checkpoint",
+ "Riviere Turquoise - Log Flume Shop",
+ ],
+ "Log Flume Shop": [
+ "Riviere Turquoise - Log Climb Shop",
+ ],
+ "Log Climb Shop": [
+ "Riviere Turquoise - Restock Shop",
+ ],
+ "Restock Shop": [
+ "Riviere Turquoise - Butterfly Matriarch Shop",
+ ],
+ "Butterfly Matriarch Shop": [
+ ],
+ "Flower Flight Checkpoint": [
+ "Riviere Turquoise - Waterfall Shop",
+ "Riviere Turquoise - Launch of Faith Shop",
+ ],
+ },
+ "Elemental Skylands": {
+ "Air Shmup": [
+ "Elemental Skylands - Air Intro Shop",
+ ],
+ "Air Intro Shop": [
+ "Elemental Skylands - Air Seal Checkpoint",
+ "Elemental Skylands - Air Generator Shop",
+ ],
+ "Air Seal Checkpoint": [
+ "Elemental Skylands - Air Intro Shop",
+ "Elemental Skylands - Air Generator Shop",
+ ],
+ "Air Generator Shop": [
+ "Elemental Skylands - Earth Shmup",
+ ],
+ "Earth Shmup": [
+ "Elemental Skylands - Earth Intro Shop",
+ ],
+ "Earth Intro Shop": [
+ "Elemental Skylands - Earth Generator Shop",
+ ],
+ "Earth Generator Shop": [
+ "Elemental Skylands - Water Shmup",
+ ],
+ "Water Shmup": [
+ "Elemental Skylands - Water Intro Shop",
+ ],
+ "Water Intro Shop": [
+ "Elemental Skylands - Water Generator Shop",
+ ],
+ "Water Generator Shop": [
+ "Elemental Skylands - Fire Shmup",
+ ],
+ "Fire Shmup": [
+ "Elemental Skylands - Fire Intro Shop",
+ ],
+ "Fire Intro Shop": [
+ "Elemental Skylands - Fire Generator Shop",
+ ],
+ "Fire Generator Shop": [
+ "Elemental Skylands - Right",
+ ],
+ "Right": [
+ "Glacial Peak - Left",
+ ],
+ },
+ "Sunken Shrine": {
+ "Left": [
+ "Howling Grotto - Bottom",
+ "Sunken Shrine - Portal",
+ ],
+ "Portal": [
+ "Sunken Shrine - Left",
+ "Sunken Shrine - Above Portal Shop",
+ "Sunken Shrine - Sun Path Shop",
+ "Sunken Shrine - Moon Path Shop",
+ "Tower HQ",
+ ],
+ "Above Portal Shop": [
+ "Sunken Shrine - Portal",
+ "Sunken Shrine - Lifeguard Shop",
+ ],
+ "Lifeguard Shop": [
+ "Sunken Shrine - Above Portal Shop",
+ "Sunken Shrine - Lightfoot Tabi Checkpoint",
+ ],
+ "Sun Path Shop": [
+ "Sunken Shrine - Portal",
+ "Sunken Shrine - Tabi Gauntlet Shop",
+ ],
+ "Tabi Gauntlet Shop": [
+ "Sunken Shrine - Sun Path Shop",
+ "Sunken Shrine - Sun Crest Checkpoint",
+ ],
+ "Moon Path Shop": [
+ "Sunken Shrine - Portal",
+ "Sunken Shrine - Waterfall Paradise Checkpoint",
+ ],
+ "Lightfoot Tabi Checkpoint": [
+ "Sunken Shrine - Portal",
+ ],
+ "Sun Crest Checkpoint": [
+ "Sunken Shrine - Tabi Gauntlet Shop",
+ "Sunken Shrine - Portal",
+ ],
+ "Waterfall Paradise Checkpoint": [
+ "Sunken Shrine - Moon Path Shop",
+ "Sunken Shrine - Moon Crest Checkpoint",
+ ],
+ "Moon Crest Checkpoint": [
+ "Sunken Shrine - Waterfall Paradise Checkpoint",
+ "Sunken Shrine - Portal",
+ ],
+ },
+}
+
+RANDOMIZED_CONNECTIONS: Dict[str, str] = {
+ "Ninja Village - Right": "Autumn Hills - Left",
+ "Autumn Hills - Left": "Ninja Village - Right",
+ "Autumn Hills - Right": "Forlorn Temple - Left",
+ "Autumn Hills - Bottom": "Catacombs - Bottom Left",
+ "Forlorn Temple - Left": "Autumn Hills - Right",
+ "Forlorn Temple - Right": "Bamboo Creek - Top Left",
+ "Forlorn Temple - Bottom": "Catacombs - Top Left",
+ "Catacombs - Top Left": "Forlorn Temple - Bottom",
+ "Catacombs - Bottom Left": "Autumn Hills - Bottom",
+ "Catacombs - Bottom": "Dark Cave - Right",
+ "Catacombs - Right": "Bamboo Creek - Bottom Left",
+ "Bamboo Creek - Bottom Left": "Catacombs - Right",
+ "Bamboo Creek - Right": "Howling Grotto - Left",
+ "Bamboo Creek - Top Left": "Forlorn Temple - Right",
+ "Howling Grotto - Left": "Bamboo Creek - Right",
+ "Howling Grotto - Top": "Quillshroom Marsh - Bottom Left",
+ "Howling Grotto - Right": "Quillshroom Marsh - Top Left",
+ "Howling Grotto - Bottom": "Sunken Shrine - Left",
+ "Quillshroom Marsh - Top Left": "Howling Grotto - Right",
+ "Quillshroom Marsh - Bottom Left": "Howling Grotto - Top",
+ "Quillshroom Marsh - Top Right": "Searing Crags - Left",
+ "Quillshroom Marsh - Bottom Right": "Searing Crags - Bottom",
+ "Searing Crags - Left": "Quillshroom Marsh - Top Right",
+ "Searing Crags - Top": "Glacial Peak - Bottom",
+ "Searing Crags - Bottom": "Quillshroom Marsh - Bottom Right",
+ "Searing Crags - Right": "Underworld - Left",
+ "Glacial Peak - Bottom": "Searing Crags - Top",
+ "Glacial Peak - Top": "Cloud Ruins - Left",
+ "Glacial Peak - Left": "Elemental Skylands - Air Shmup",
+ "Cloud Ruins - Left": "Glacial Peak - Top",
+ "Elemental Skylands - Right": "Glacial Peak - Left",
+ "Tower HQ": "Tower of Time - Left",
+ "Artificer": "Corrupted Future",
+ "Underworld - Left": "Searing Crags - Right",
+ "Dark Cave - Right": "Catacombs - Bottom",
+ "Dark Cave - Left": "Riviere Turquoise - Right",
+ "Sunken Shrine - Left": "Howling Grotto - Bottom",
+}
+
+TRANSITIONS: List[str] = [
+ "Ninja Village - Right",
+ "Autumn Hills - Left",
+ "Autumn Hills - Right",
+ "Autumn Hills - Bottom",
+ "Forlorn Temple - Left",
+ "Forlorn Temple - Bottom",
+ "Forlorn Temple - Right",
+ "Catacombs - Top Left",
+ "Catacombs - Right",
+ "Catacombs - Bottom",
+ "Catacombs - Bottom Left",
+ "Dark Cave - Right",
+ "Dark Cave - Left",
+ "Riviere Turquoise - Right",
+ "Howling Grotto - Left",
+ "Howling Grotto - Right",
+ "Howling Grotto - Top",
+ "Howling Grotto - Bottom",
+ "Sunken Shrine - Left",
+ "Bamboo Creek - Top Left",
+ "Bamboo Creek - Bottom Left",
+ "Bamboo Creek - Right",
+ "Quillshroom Marsh - Top Left",
+ "Quillshroom Marsh - Bottom Left",
+ "Quillshroom Marsh - Top Right",
+ "Quillshroom Marsh - Bottom Right",
+ "Searing Crags - Left",
+ "Searing Crags - Bottom",
+ "Searing Crags - Right",
+ "Searing Crags - Top",
+ "Glacial Peak - Bottom",
+ "Glacial Peak - Top",
+ "Glacial Peak - Left",
+ "Elemental Skylands - Air Shmup",
+ "Elemental Skylands - Right",
+ "Tower HQ",
+ "Tower of Time - Left",
+ "Corrupted Future",
+ "Cloud Ruins - Left",
+ "Underworld - Left",
+]
diff --git a/worlds/messenger/constants.py b/worlds/messenger/constants.py
new file mode 100644
index 000000000000..ea15c71068db
--- /dev/null
+++ b/worlds/messenger/constants.py
@@ -0,0 +1,168 @@
+from .shop import FIGURINES, SHOP_ITEMS
+
+# items
+# listing individual groups first for easy lookup
+NOTES = [
+ "Key of Hope",
+ "Key of Chaos",
+ "Key of Courage",
+ "Key of Love",
+ "Key of Strength",
+ "Key of Symbiosis",
+]
+
+PROG_ITEMS = [
+ "Wingsuit",
+ "Rope Dart",
+ "Lightfoot Tabi",
+ "Power Thistle",
+ "Demon King Crown",
+ "Ruxxtin's Amulet",
+ "Magic Firefly",
+ "Sun Crest",
+ "Moon Crest",
+ # "Astral Seed",
+ # "Astral Tea Leaves",
+ "Money Wrench",
+ "Candle",
+ "Seashell",
+]
+
+PHOBEKINS = [
+ "Necro",
+ "Pyro",
+ "Claustro",
+ "Acro",
+]
+
+USEFUL_ITEMS = [
+ "Windmill Shuriken",
+]
+
+FILLER = {
+ "Time Shard": 5,
+ "Time Shard (10)": 10,
+ "Time Shard (50)": 20,
+ "Time Shard (100)": 20,
+ "Time Shard (300)": 10,
+ "Time Shard (500)": 5,
+}
+
+TRAPS = {
+ "Teleport Trap": 5,
+ "Prophecy Trap": 10,
+}
+
+# item_name_to_id needs to be deterministic and match upstream
+ALL_ITEMS = [
+ *NOTES,
+ "Windmill Shuriken",
+ "Wingsuit",
+ "Rope Dart",
+ "Lightfoot Tabi",
+ # "Astral Seed",
+ # "Astral Tea Leaves",
+ "Candle",
+ "Seashell",
+ "Power Thistle",
+ "Demon King Crown",
+ "Ruxxtin's Amulet",
+ "Magic Firefly",
+ "Sun Crest",
+ "Moon Crest",
+ *PHOBEKINS,
+ "Power Seal",
+ *FILLER,
+ *SHOP_ITEMS,
+ *FIGURINES,
+ "Money Wrench",
+ "Teleport Trap",
+ "Prophecy Trap",
+]
+
+# locations
+# the names of these don't actually matter, but using the upstream's names for now
+# order must be exactly the same as upstream
+ALWAYS_LOCATIONS = [
+ # notes
+ "Sunken Shrine - Key of Love",
+ "Corrupted Future - Key of Courage",
+ "Underworld - Key of Chaos",
+ "Elemental Skylands - Key of Symbiosis",
+ "Searing Crags - Key of Strength",
+ "Autumn Hills - Key of Hope",
+ # upgrades
+ "Howling Grotto - Wingsuit",
+ "Searing Crags - Rope Dart",
+ "Sunken Shrine - Lightfoot Tabi",
+ "Autumn Hills - Climbing Claws",
+ # quest items
+ "Ninja Village - Astral Seed",
+ "Searing Crags - Astral Tea Leaves",
+ "Ninja Village - Candle",
+ "Quillshroom Marsh - Seashell",
+ "Searing Crags - Power Thistle",
+ "Forlorn Temple - Demon King",
+ "Catacombs - Ruxxtin's Amulet",
+ "Riviere Turquoise - Butterfly Matriarch",
+ "Sunken Shrine - Sun Crest",
+ "Sunken Shrine - Moon Crest",
+ # phobekins
+ "Catacombs - Necro",
+ "Searing Crags - Pyro",
+ "Bamboo Creek - Claustro",
+ "Cloud Ruins - Acro",
+ # seals
+ "Ninja Village Seal - Tree House",
+ "Autumn Hills Seal - Trip Saws",
+ "Autumn Hills Seal - Double Swing Saws",
+ "Autumn Hills Seal - Spike Ball Swing",
+ "Autumn Hills Seal - Spike Ball Darts",
+ "Catacombs Seal - Triple Spike Crushers",
+ "Catacombs Seal - Crusher Gauntlet",
+ "Catacombs Seal - Dirty Pond",
+ "Bamboo Creek Seal - Spike Crushers and Doors",
+ "Bamboo Creek Seal - Spike Ball Pits",
+ "Bamboo Creek Seal - Spike Crushers and Doors v2",
+ "Howling Grotto Seal - Windy Saws and Balls",
+ "Howling Grotto Seal - Crushing Pits",
+ "Howling Grotto Seal - Breezy Crushers",
+ "Quillshroom Marsh Seal - Spikey Window",
+ "Quillshroom Marsh Seal - Sand Trap",
+ "Quillshroom Marsh Seal - Do the Spike Wave",
+ "Searing Crags Seal - Triple Ball Spinner",
+ "Searing Crags Seal - Raining Rocks",
+ "Searing Crags Seal - Rhythm Rocks",
+ "Glacial Peak Seal - Ice Climbers",
+ "Glacial Peak Seal - Projectile Spike Pit",
+ "Glacial Peak Seal - Glacial Air Swag",
+ "Tower of Time Seal - Time Waster",
+ "Tower of Time Seal - Lantern Climb",
+ "Tower of Time Seal - Arcane Orbs",
+ "Cloud Ruins Seal - Ghost Pit",
+ "Cloud Ruins Seal - Toothbrush Alley",
+ "Cloud Ruins Seal - Saw Pit",
+ "Cloud Ruins Seal - Money Farm Room",
+ "Underworld Seal - Sharp and Windy Climb",
+ "Underworld Seal - Spike Wall",
+ "Underworld Seal - Fireball Wave",
+ "Underworld Seal - Rising Fanta",
+ "Forlorn Temple Seal - Rocket Maze",
+ "Forlorn Temple Seal - Rocket Sunset",
+ "Sunken Shrine Seal - Ultra Lifeguard",
+ "Sunken Shrine Seal - Waterfall Paradise",
+ "Sunken Shrine Seal - Tabi Gauntlet",
+ "Riviere Turquoise Seal - Bounces and Balls",
+ "Riviere Turquoise Seal - Launch of Faith",
+ "Riviere Turquoise Seal - Flower Power",
+ "Elemental Skylands Seal - Air",
+ "Elemental Skylands Seal - Water",
+ "Elemental Skylands Seal - Fire",
+]
+
+BOSS_LOCATIONS = [
+ "Autumn Hills - Leaf Golem",
+ "Catacombs - Ruxxtin",
+ "Howling Grotto - Emerald Golem",
+ "Quillshroom Marsh - Queen of Quills",
+]
diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md
index 4ffe04183073..8248a4755d3f 100644
--- a/worlds/messenger/docs/en_The Messenger.md
+++ b/worlds/messenger/docs/en_The Messenger.md
@@ -1,12 +1,10 @@
# The Messenger
## Quick Links
-- [Setup](../../../../tutorial/The%20Messenger/setup/en)
-- [Settings Page](../../../../games/The%20Messenger/player-settings)
+- [Setup](/tutorial/The%20Messenger/setup/en)
+- [Options Page](/games/The%20Messenger/player-options)
- [Courier Github](https://github.com/Brokemia/Courier)
-- [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod)
- [The Messenger Randomizer AP Github](https://github.com/alwaysintreble/TheMessengerRandomizerModAP)
-- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker)
- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack)
## What does randomization do in this game?
@@ -51,30 +49,29 @@ for it. The groups you can use for The Messenger are:
## Other changes
* The player can return to the Tower of Time HQ at any point by selecting the button from the options menu
- * This can cause issues if used at specific times. Current known:
- * During Boss fights
- * After Courage Note collection (Corrupted Future chase)
- * This is currently an expected action in logic. If you do need to teleport during this chase sequence, it
- is recommended to quit to title and reload the save
+ * This can cause issues if used at specific times. If used in any of these known problematic areas, immediately
+quit to title and reload the save. The currently known areas include:
+ * During Boss fights
+ * After Courage Note collection (Corrupted Future chase)
* After reaching ninja village a teleport option is added to the menu to reach it quickly
* Toggle Windmill Shuriken button is added to option menu once the item is received
-* The mod option menu will also have a hint item button, as well as a release and collect button that are all placed when
- the player fulfills the necessary conditions.
+* The mod option menu will also have a hint item button, as well as a release and collect button that are all placed
+when the player fulfills the necessary conditions.
* After running the game with the mod, a config file (APConfig.toml) will be generated in your game folder that can be
- used to modify certain settings such as text size and color. This can also be used to specify a player name that can't
- be entered in game.
+used to modify certain settings such as text size and color. This can also be used to specify a player name that can't
+be entered in game.
## Known issues
* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item
* If you receive the Magic Firefly while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit
- to Searing Crags and re-enter to get it to play correctly.
-* Sometimes upon teleporting back to HQ, Ninja will run left and enter a different portal than the one entered by the
- player. This may also cause a softlock.
+to Searing Crags and re-enter to get it to play correctly.
+* Teleporting back to HQ, then returning to the same level you just left through a Portal can cause Ninja to run left
+and enter a different portal than the one entered by the player or lead to other incorrect inputs, causing a soft lock
* Text entry menus don't accept controller input
-* Opening the shop chest in power seal hunt mode from the tower of time HQ will softlock the game.
-* If you are unable to reset file slots, load into a save slot, let the game save, and close it.
+* In power seal hunt mode, the chest must be opened by entering the shop from a level. Teleporting to HQ and opening the
+chest will not work.
## What do I do if I have a problem?
-If you believe something happened that isn't intended, please get the `log.txt` from the folder of your game installation
-and send a bug report either on GitHub or the [Archipelago Discord Server](http://archipelago.gg/discord)
+If you believe something happened that isn't intended, please get the `log.txt` from the folder of your game
+installation and send a bug report either on GitHub or the [Archipelago Discord Server](http://archipelago.gg/discord)
diff --git a/worlds/messenger/docs/setup_en.md b/worlds/messenger/docs/setup_en.md
index d93d13b27483..c1770e747442 100644
--- a/worlds/messenger/docs/setup_en.md
+++ b/worlds/messenger/docs/setup_en.md
@@ -1,25 +1,34 @@
# The Messenger Randomizer Setup Guide
## Quick Links
-- [Game Info](../../../../games/The%20Messenger/info/en)
-- [Settings Page](../../../../games/The%20Messenger/player-settings)
+- [Game Info](/games/The%20Messenger/info/en)
+- [Options Page](/games/The%20Messenger/player-options)
- [Courier Github](https://github.com/Brokemia/Courier)
- [The Messenger Randomizer AP Github](https://github.com/alwaysintreble/TheMessengerRandomizerModAP)
-- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker)
- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack)
## Installation
-1. Read the [Game Info Page](../../../../games/The%20Messenger/info/en) for how the game works, caveats and known issues
-2. Download and install Courier Mod Loader using the instructions on the release page
+Read changes to the base game on the [Game Info Page](/games/The%20Messenger/info/en)
+
+### Automated Installation
+
+1. Download and install the latest [Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
+2. Launch the Archipelago Launcher (ArchipelagoLauncher.exe)
+3. Click on "The Messenger"
+4. Follow the prompts
+
+These steps can also be followed to launch the game and check for mod updates after the initial setup.
+
+### Manual Installation
+
+1. Download and install Courier Mod Loader using the instructions on the release page
* [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases)
-3. Download and install the randomizer mod
+2. Download and install the randomizer mod
1. Download the latest TheMessengerRandomizerAP.zip from
- [The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases)
+[The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases)
2. Extract the zip file to `TheMessenger/Mods/` of your game's install location
- * You cannot have both the non-AP randomizer and the AP randomizer installed at the same time. The AP randomizer
- is backwards compatible, so the non-AP mod can be safely removed, and you can still play seeds generated from the
- non-AP randomizer.
+ * You cannot have both the non-AP randomizer and the AP randomizer installed at the same time
3. Optionally, Backup your save game
* On Windows
1. Press `Windows Key + R` to open run
@@ -33,19 +42,17 @@
## Joining a MultiWorld Game
1. Launch the game
-2. Navigate to `Options > Third Party Mod Options`
-3. Select `Reset Randomizer File Slots`
- * This will set up all of your save slots with new randomizer save files. You can have up to 3 randomizer files at a
- time, but must do this step again to start new runs afterward.
-4. Enter connection info using the relevant option buttons
+2. Navigate to `Options > Archipelago Options`
+3. Enter connection info using the relevant option buttons
* **The game is limited to alphanumerical characters, `.`, and `-`.**
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
- website.
+website.
* If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game
- directory. When using this, all connection information must be entered in the file.
-5. Select the `Connect to Archipelago` button
-6. Navigate to save file selection
-7. Select a new valid randomizer save
+directory. When using this, all connection information must be entered in the file.
+4. Select the `Connect to Archipelago` button
+5. Navigate to save file selection
+6. Start a new game
+ * If you're already connected, deleting an existing save will not disconnect you and is completely safe.
## Continuing a MultiWorld Game
@@ -53,6 +60,5 @@ At any point while playing, it is completely safe to quit. Returning to the titl
disconnect you from the server. To reconnect to an in progress MultiWorld, simply load the correct save file for that
MultiWorld.
-If the reconnection fails, the message on screen will state you are disconnected. If this happens, you can return to the
-main menu and connect to the server as in [Joining a Multiworld Game](#joining-a-multiworld-game), then load the correct
-save file.
+If the reconnection fails, the message on screen will state you are disconnected. If this happens, the game will attempt
+to reconnect in the background. An option will also be added to the in game menu to change the port, if necessary.
diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py
new file mode 100644
index 000000000000..59e694cd3963
--- /dev/null
+++ b/worlds/messenger/options.py
@@ -0,0 +1,240 @@
+from dataclasses import dataclass
+from typing import Dict
+
+from schema import And, Optional, Or, Schema
+
+from Options import Choice, DeathLinkMixin, DefaultOnToggle, ItemsAccessibility, OptionDict, PerGameCommonOptions, \
+ PlandoConnections, Range, StartInventoryPool, Toggle, Visibility
+from .portals import CHECKPOINTS, PORTALS, SHOP_POINTS
+
+
+class MessengerAccessibility(ItemsAccessibility):
+ # defaulting to locations accessibility since items makes certain items self-locking
+ default = ItemsAccessibility.option_full
+ __doc__ = ItemsAccessibility.__doc__
+
+
+class PortalPlando(PlandoConnections):
+ """
+ Plando connections to be used with portal shuffle. Direction is ignored.
+ List of valid connections can be found here: https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/portals.py#L12.
+ The entering Portal should *not* have "Portal" appended.
+ For the exits, those in checkpoints and shops should just be the name of the spot, while portals should have " Portal" at the end.
+ Example:
+ - entrance: Riviere Turquoise
+ exit: Wingsuit
+ - entrance: Sunken Shrine
+ exit: Sunny Day
+ - entrance: Searing Crags
+ exit: Glacial Peak Portal
+ """
+ portals = [f"{portal} Portal" for portal in PORTALS]
+ shop_points = [point for points in SHOP_POINTS.values() for point in points]
+ checkpoints = [point for points in CHECKPOINTS.values() for point in points]
+ portal_entrances = PORTALS
+ portal_exits = portals + shop_points + checkpoints
+ entrances = portal_entrances
+ exits = portal_exits
+
+
+# for back compatibility. To later be replaced with transition plando
+class HiddenPortalPlando(PortalPlando):
+ visibility = Visibility.none
+ entrances = PortalPlando.entrances
+ exits = PortalPlando.exits
+
+
+class Logic(Choice):
+ """
+ The level of logic to use when determining what locations in your world are accessible.
+
+ Normal: Can require damage boosts, but otherwise approachable for someone who has beaten the game.
+ Hard: Expects more knowledge and tighter execution. Has leashing, normal clips and much tighter d-boosting in logic.
+ """
+ display_name = "Logic Level"
+ option_normal = 0
+ option_hard = 1
+ alias_oob = 1
+ alias_challenging = 1
+
+
+class MegaShards(Toggle):
+ """Whether mega shards should be item locations."""
+ display_name = "Shuffle Mega Time Shards"
+
+
+class LimitedMovement(Toggle):
+ """
+ Removes either rope dart or wingsuit from the itempool. Forces logic to at least hard and accessibility to minimal.
+ """
+ display_name = "Limited Movement"
+
+
+class EarlyMed(Toggle):
+ """Guarantees meditation will be found early"""
+ display_name = "Early Meditation"
+
+
+class AvailablePortals(Range):
+ """Number of portals that are available from the start. Autumn Hills, Howling Grotto, and Glacial Peak are always available. If portal outputs are not randomized, Searing Crags will also be available."""
+ display_name = "Available Starting Portals"
+ range_start = 3
+ range_end = 6
+ default = 6
+
+
+class ShufflePortals(Choice):
+ """
+ Whether the portals lead to random places.
+ Entering a portal from its vanilla area will always lead to HQ, and will unlock it if relevant.
+ Supports plando.
+
+ None: Portals will take you where they're supposed to.
+ Shops: Portals can lead to any area except Music Box and Elemental Skylands, with each portal output guaranteed to not overlap with another portal's. Will only put you at a portal or a shop.
+ Checkpoints: Like Shops except checkpoints without shops are also valid drop points.
+ Anywhere: Like Checkpoints except it's possible for multiple portals to output to the same map.
+ """
+ display_name = "Shuffle Portal Outputs"
+ option_none = 0
+ alias_off = 0
+ option_shops = 1
+ option_checkpoints = 2
+ option_anywhere = 3
+
+
+class ShuffleTransitions(Choice):
+ """
+ Whether the transitions between the levels should be randomized.
+ Supports plando.
+
+ None: Level transitions lead where they should.
+ Coupled: Returning through a transition will take you from whence you came.
+ Decoupled: Any level transition can take you to any other level transition.
+ """
+ display_name = "Shuffle Level Transitions"
+ option_none = 0
+ alias_off = 0
+ option_coupled = 1
+ option_decoupled = 2
+
+
+class Goal(Choice):
+ """Requirement to finish the game. To win with the power seal hunt goal, you must enter the Music Box through the shop chest."""
+ display_name = "Goal"
+ option_open_music_box = 0
+ option_power_seal_hunt = 1
+
+
+class MusicBox(DefaultOnToggle):
+ """Whether the music box gauntlet needs to be done."""
+ display_name = "Music Box Gauntlet"
+
+
+class NotesNeeded(Range):
+ """How many notes are needed to access the Music Box."""
+ display_name = "Notes Needed"
+ range_start = 1
+ range_end = 6
+ default = range_end
+
+
+class AmountSeals(Range):
+ """Number of power seals that exist in the item pool when power seal hunt is the goal."""
+ display_name = "Total Power Seals"
+ range_start = 1
+ range_end = 85
+ default = 45
+
+
+class RequiredSeals(Range):
+ """Percentage of total seals required to open the shop chest."""
+ display_name = "Percent Seals Required"
+ range_start = 10
+ range_end = 100
+ default = range_end
+
+
+class Traps(Toggle):
+ """Whether traps should be included in the itempool."""
+ display_name = "Include Traps"
+
+
+class ShopPrices(Range):
+ """Percentage modifier for shuffled item prices in shops"""
+ display_name = "Shop Prices Modifier"
+ range_start = 25
+ range_end = 400
+ default = 100
+
+
+def planned_price(location: str) -> Dict[Optional, Or]:
+ return {
+ Optional(location): Or(
+ And(int, lambda n: n >= 0),
+ {
+ Optional(And(int, lambda n: n >= 0)): And(int, lambda n: n >= 0)
+ }
+ )
+ }
+
+
+class PlannedShopPrices(OptionDict):
+ """Plan specific prices on shop slots. Supports weighting"""
+ display_name = "Shop Price Plando"
+ schema = Schema({
+ **planned_price("Karuta Plates"),
+ **planned_price("Serendipitous Bodies"),
+ **planned_price("Path of Resilience"),
+ **planned_price("Kusari Jacket"),
+ **planned_price("Energy Shuriken"),
+ **planned_price("Serendipitous Minds"),
+ **planned_price("Prepared Mind"),
+ **planned_price("Meditation"),
+ **planned_price("Rejuvenative Spirit"),
+ **planned_price("Centered Mind"),
+ **planned_price("Strike of the Ninja"),
+ **planned_price("Second Wind"),
+ **planned_price("Currents Master"),
+ **planned_price("Aerobatics Warrior"),
+ **planned_price("Demon's Bane"),
+ **planned_price("Devil's Due"),
+ **planned_price("Time Sense"),
+ **planned_price("Power Sense"),
+ **planned_price("Focused Power Sense"),
+ **planned_price("Green Kappa Figurine"),
+ **planned_price("Blue Kappa Figurine"),
+ **planned_price("Ountarde Figurine"),
+ **planned_price("Red Kappa Figurine"),
+ **planned_price("Demon King Figurine"),
+ **planned_price("Quillshroom Figurine"),
+ **planned_price("Jumping Quillshroom Figurine"),
+ **planned_price("Scurubu Figurine"),
+ **planned_price("Jumping Scurubu Figurine"),
+ **planned_price("Wallaxer Figurine"),
+ **planned_price("Barmath'azel Figurine"),
+ **planned_price("Queen of Quills Figurine"),
+ **planned_price("Demon Hive Figurine"),
+ })
+
+
+@dataclass
+class MessengerOptions(DeathLinkMixin, PerGameCommonOptions):
+ accessibility: MessengerAccessibility
+ start_inventory: StartInventoryPool
+ logic_level: Logic
+ shuffle_shards: MegaShards
+ limited_movement: LimitedMovement
+ early_meditation: EarlyMed
+ available_portals: AvailablePortals
+ shuffle_portals: ShufflePortals
+ # shuffle_transitions: ShuffleTransitions
+ goal: Goal
+ music_box: MusicBox
+ notes_needed: NotesNeeded
+ total_seals: AmountSeals
+ percent_seals_required: RequiredSeals
+ traps: Traps
+ shop_price: ShopPrices
+ shop_price_plan: PlannedShopPrices
+ portal_plando: PortalPlando
+ plando_connections: HiddenPortalPlando
diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py
new file mode 100644
index 000000000000..1da210cb23ff
--- /dev/null
+++ b/worlds/messenger/portals.py
@@ -0,0 +1,300 @@
+from copy import deepcopy
+from typing import List, TYPE_CHECKING
+
+from BaseClasses import CollectionState, PlandoOptions
+from Options import PlandoConnection
+
+if TYPE_CHECKING:
+ from . import MessengerWorld
+
+
+PORTALS = [
+ "Autumn Hills",
+ "Riviere Turquoise",
+ "Howling Grotto",
+ "Sunken Shrine",
+ "Searing Crags",
+ "Glacial Peak",
+]
+
+
+SHOP_POINTS = {
+ "Autumn Hills": [
+ "Climbing Claws",
+ "Hope Path",
+ "Dimension Climb",
+ "Leaf Golem",
+ ],
+ "Forlorn Temple": [
+ "Outside",
+ "Entrance",
+ "Climb",
+ "Rocket Sunset",
+ "Descent",
+ "Saw Gauntlet",
+ "Demon King",
+ ],
+ "Catacombs": [
+ "Triple Spike Crushers",
+ "Ruxxtin",
+ ],
+ "Bamboo Creek": [
+ "Spike Crushers",
+ "Abandoned",
+ "Time Loop",
+ ],
+ "Howling Grotto": [
+ "Wingsuit",
+ "Crushing Pits",
+ "Emerald Golem",
+ ],
+ "Quillshroom Marsh": [
+ "Spikey Window",
+ "Sand Trap",
+ "Queen of Quills",
+ ],
+ "Searing Crags": [
+ "Rope Dart",
+ "Falling Rocks",
+ "Searing Mega Shard",
+ "Before Final Climb",
+ "Colossuses",
+ "Key of Strength",
+ ],
+ "Glacial Peak": [
+ "Ice Climbers'",
+ "Glacial Mega Shard",
+ "Tower Entrance",
+ ],
+ "Tower of Time": [
+ "Final Chance",
+ "Arcane Golem",
+ ],
+ "Cloud Ruins": [
+ "Cloud Entrance",
+ "Pillar Glide",
+ "Crushers' Descent",
+ "Seeing Spikes",
+ "Final Flight",
+ "Manfred's",
+ ],
+ "Underworld": [
+ "Left",
+ "Fireball Wave",
+ "Long Climb",
+ # "Barm'athaziel", # not currently valid
+ "Key of Chaos",
+ ],
+ "Riviere Turquoise": [
+ "Waterfall",
+ "Launch of Faith",
+ "Log Flume",
+ "Log Climb",
+ "Restock",
+ "Butterfly Matriarch",
+ ],
+ "Elemental Skylands": [
+ "Air Intro",
+ "Air Generator",
+ "Earth Intro",
+ "Earth Generator",
+ "Fire Intro",
+ "Fire Generator",
+ "Water Intro",
+ "Water Generator",
+ ],
+ "Sunken Shrine": [
+ "Above Portal",
+ "Lifeguard",
+ "Sun Path",
+ "Tabi Gauntlet",
+ "Moon Path",
+ ]
+}
+
+
+CHECKPOINTS = {
+ "Autumn Hills": [
+ "Hope Latch",
+ "Key of Hope",
+ "Lakeside",
+ "Double Swing",
+ "Spike Ball Swing",
+ ],
+ "Forlorn Temple": [
+ "Sunny Day",
+ "Rocket Maze",
+ ],
+ "Catacombs": [
+ "Death Trap",
+ "Crusher Gauntlet",
+ "Dirty Pond",
+ ],
+ "Bamboo Creek": [
+ "Spike Ball Pits",
+ "Spike Doors",
+ ],
+ "Howling Grotto": [
+ "Lost Woods",
+ "Breezy Crushers",
+ ],
+ "Quillshroom Marsh": [
+ "Seashell",
+ "Quicksand",
+ "Spike Wave",
+ ],
+ "Searing Crags": [
+ "Triple Ball Spinner",
+ "Raining Rocks",
+ ],
+ "Glacial Peak": [
+ "Projectile Spike Pit",
+ "Air Swag",
+ "Free Climbing",
+ ],
+ "Tower of Time": [
+ "First",
+ "Second",
+ "Third",
+ "Fourth",
+ "Fifth",
+ "Sixth",
+ ],
+ "Cloud Ruins": [
+ "Spike Float",
+ "Ghost Pit",
+ "Toothbrush Alley",
+ "Saw Pit",
+ ],
+ "Underworld": [
+ "Hot Dip",
+ "Hot Tub",
+ "Lava Run",
+ ],
+ "Riviere Turquoise": [
+ "Flower Flight",
+ ],
+ "Elemental Skylands": [
+ "Air Seal",
+ ],
+ "Sunken Shrine": [
+ "Lightfoot Tabi",
+ "Sun Crest",
+ "Waterfall Paradise",
+ "Moon Crest",
+ ]
+}
+
+
+REGION_ORDER = [
+ "Autumn Hills",
+ "Forlorn Temple",
+ "Catacombs",
+ "Bamboo Creek",
+ "Howling Grotto",
+ "Quillshroom Marsh",
+ "Searing Crags",
+ "Glacial Peak",
+ "Tower of Time",
+ "Cloud Ruins",
+ "Underworld",
+ "Riviere Turquoise",
+ "Elemental Skylands",
+ "Sunken Shrine",
+]
+
+
+def shuffle_portals(world: "MessengerWorld") -> None:
+ """shuffles the output of the portals from the main hub"""
+ from .options import ShufflePortals
+
+ def create_mapping(in_portal: str, warp: str) -> str:
+ """assigns the chosen output to the input"""
+ parent = out_to_parent[warp]
+ exit_string = f"{parent.strip(' ')} - "
+
+ if "Portal" in warp:
+ exit_string += "Portal"
+ world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}00"))
+ elif warp in SHOP_POINTS[parent]:
+ exit_string += f"{warp} Shop"
+ world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}"))
+ else:
+ exit_string += f"{warp} Checkpoint"
+ world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}"))
+
+ world.spoiler_portal_mapping[in_portal] = exit_string
+ connect_portal(world, in_portal, exit_string)
+
+ return parent
+
+ def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None:
+ """checks the provided plando connections for portals and connects them"""
+ for connection in plando_connections:
+ if connection.entrance not in PORTALS:
+ continue
+ # let it crash here if input is invalid
+ create_mapping(connection.entrance, connection.exit)
+ world.plando_portals.append(connection.entrance)
+
+ shuffle_type = world.options.shuffle_portals
+ shop_points = deepcopy(SHOP_POINTS)
+ for portal in PORTALS:
+ shop_points[portal].append(f"{portal} Portal")
+ if shuffle_type > ShufflePortals.option_shops:
+ for area, points in CHECKPOINTS.items():
+ shop_points[area] += points
+ out_to_parent = {checkpoint: parent for parent, checkpoints in shop_points.items() for checkpoint in checkpoints}
+ available_portals = [val for zone in shop_points.values() for val in zone]
+ world.random.shuffle(available_portals)
+
+ plando = world.options.portal_plando.value
+ if not plando:
+ plando = world.options.plando_connections.value
+ if plando and world.multiworld.plando_options & PlandoOptions.connections:
+ handle_planned_portals(plando)
+
+ for portal in PORTALS:
+ if portal in world.plando_portals:
+ continue
+ warp_point = available_portals.pop()
+ parent = create_mapping(portal, warp_point)
+ if shuffle_type < ShufflePortals.option_anywhere:
+ available_portals = [port for port in available_portals if port not in shop_points[parent]]
+ world.random.shuffle(available_portals)
+
+
+def connect_portal(world: "MessengerWorld", portal: str, out_region: str) -> None:
+ entrance = world.multiworld.get_entrance(f"ToTHQ {portal} Portal", world.player)
+ entrance.connect(world.multiworld.get_region(out_region, world.player))
+
+
+def disconnect_portals(world: "MessengerWorld") -> None:
+ for portal in [port for port in PORTALS if port not in world.plando_portals]:
+ entrance = world.multiworld.get_entrance(f"ToTHQ {portal} Portal", world.player)
+ entrance.connected_region.entrances.remove(entrance)
+ entrance.connected_region = None
+ if portal in world.spoiler_portal_mapping:
+ del world.spoiler_portal_mapping[portal]
+ if len(world.portal_mapping) > len(world.spoiler_portal_mapping):
+ world.portal_mapping = world.portal_mapping[:len(world.spoiler_portal_mapping)]
+
+
+def validate_portals(world: "MessengerWorld") -> bool:
+ # if world.options.shuffle_transitions:
+ # return True
+ new_state = CollectionState(world.multiworld)
+ new_state.update_reachable_regions(world.player)
+ reachable_locs = 0
+ for loc in world.multiworld.get_locations(world.player):
+ reachable_locs += loc.can_reach(new_state)
+ if reachable_locs > 5:
+ return True
+ return False
+
+
+def add_closed_portal_reqs(world: "MessengerWorld") -> None:
+ closed_portals = [entrance for entrance in PORTALS if f"{entrance} Portal" not in world.starting_portals]
+ for portal in closed_portals:
+ tower_exit = world.multiworld.get_entrance(f"ToTHQ {portal} Portal", world.player)
+ tower_exit.access_rule = lambda state: state.has(portal, world.player)
diff --git a/worlds/messenger/regions.py b/worlds/messenger/regions.py
new file mode 100644
index 000000000000..153f8510f1bd
--- /dev/null
+++ b/worlds/messenger/regions.py
@@ -0,0 +1,446 @@
+from typing import Dict, List
+
+
+LOCATIONS: Dict[str, List[str]] = {
+ "Ninja Village - Nest": [
+ "Ninja Village - Candle",
+ "Ninja Village - Astral Seed",
+ "Ninja Village Seal - Tree House",
+ ],
+ "Autumn Hills - Climbing Claws Shop": [
+ "Autumn Hills - Climbing Claws",
+ "Autumn Hills Seal - Trip Saws",
+ ],
+ "Autumn Hills - Key of Hope Checkpoint": [
+ "Autumn Hills - Key of Hope",
+ ],
+ "Autumn Hills - Double Swing Checkpoint": [
+ "Autumn Hills Seal - Double Swing Saws",
+ ],
+ "Autumn Hills - Spike Ball Swing Checkpoint": [
+ "Autumn Hills Seal - Spike Ball Swing",
+ "Autumn Hills Seal - Spike Ball Darts",
+ ],
+ "Autumn Hills - Leaf Golem Shop": [
+ "Autumn Hills - Leaf Golem",
+ ],
+ "Forlorn Temple - Rocket Maze Checkpoint": [
+ "Forlorn Temple Seal - Rocket Maze",
+ ],
+ "Forlorn Temple - Rocket Sunset Shop": [
+ "Forlorn Temple Seal - Rocket Sunset",
+ ],
+ "Forlorn Temple - Demon King Shop": [
+ "Forlorn Temple - Demon King",
+ ],
+ "Catacombs - Top Left": [
+ "Catacombs - Necro",
+ ],
+ "Catacombs - Triple Spike Crushers Shop": [
+ "Catacombs Seal - Triple Spike Crushers",
+ ],
+ "Catacombs - Dirty Pond Checkpoint": [
+ "Catacombs Seal - Crusher Gauntlet",
+ "Catacombs Seal - Dirty Pond",
+ ],
+ "Catacombs - Ruxxtin Shop": [
+ "Catacombs - Ruxxtin's Amulet",
+ "Catacombs - Ruxxtin",
+ ],
+ "Bamboo Creek - Spike Crushers Shop": [
+ "Bamboo Creek Seal - Spike Crushers and Doors",
+ ],
+ "Bamboo Creek - Spike Ball Pits Checkpoint": [
+ "Bamboo Creek Seal - Spike Ball Pits",
+ ],
+ "Bamboo Creek - Time Loop Shop": [
+ "Bamboo Creek Seal - Spike Crushers and Doors v2",
+ "Bamboo Creek - Claustro",
+ ],
+ "Howling Grotto - Wingsuit Shop": [
+ "Howling Grotto - Wingsuit",
+ "Howling Grotto Seal - Windy Saws and Balls",
+ ],
+ "Howling Grotto - Crushing Pits Shop": [
+ "Howling Grotto Seal - Crushing Pits",
+ ],
+ "Howling Grotto - Breezy Crushers Checkpoint": [
+ "Howling Grotto Seal - Breezy Crushers",
+ ],
+ "Howling Grotto - Emerald Golem Shop": [
+ "Howling Grotto - Emerald Golem",
+ ],
+ "Quillshroom Marsh - Seashell Checkpoint": [
+ "Quillshroom Marsh - Seashell",
+ ],
+ "Quillshroom Marsh - Spikey Window Shop": [
+ "Quillshroom Marsh Seal - Spikey Window",
+ ],
+ "Quillshroom Marsh - Sand Trap Shop": [
+ "Quillshroom Marsh Seal - Sand Trap",
+ ],
+ "Quillshroom Marsh - Spike Wave Checkpoint": [
+ "Quillshroom Marsh Seal - Do the Spike Wave",
+ ],
+ "Quillshroom Marsh - Queen of Quills Shop": [
+ "Quillshroom Marsh - Queen of Quills",
+ ],
+ "Searing Crags - Rope Dart Shop": [
+ "Searing Crags - Rope Dart",
+ ],
+ "Searing Crags - Triple Ball Spinner Checkpoint": [
+ "Searing Crags Seal - Triple Ball Spinner",
+ ],
+ "Searing Crags - Raining Rocks Checkpoint": [
+ "Searing Crags Seal - Raining Rocks",
+ ],
+ "Searing Crags - Colossuses Shop": [
+ "Searing Crags Seal - Rhythm Rocks",
+ "Searing Crags - Power Thistle",
+ "Searing Crags - Astral Tea Leaves",
+ ],
+ "Searing Crags - Key of Strength Shop": [
+ "Searing Crags - Key of Strength",
+ ],
+ "Searing Crags - Portal": [
+ "Searing Crags - Pyro",
+ ],
+ "Glacial Peak - Ice Climbers' Shop": [
+ "Glacial Peak Seal - Ice Climbers",
+ ],
+ "Glacial Peak - Projectile Spike Pit Checkpoint": [
+ "Glacial Peak Seal - Projectile Spike Pit",
+ ],
+ "Glacial Peak - Air Swag Checkpoint": [
+ "Glacial Peak Seal - Glacial Air Swag",
+ ],
+ "Tower of Time - First Checkpoint": [
+ "Tower of Time Seal - Time Waster",
+ ],
+ "Tower of Time - Fourth Checkpoint": [
+ "Tower of Time Seal - Lantern Climb",
+ ],
+ "Tower of Time - Fifth Checkpoint": [
+ "Tower of Time Seal - Arcane Orbs",
+ ],
+ "Cloud Ruins - Ghost Pit Checkpoint": [
+ "Cloud Ruins Seal - Ghost Pit",
+ ],
+ "Cloud Ruins - Toothbrush Alley Checkpoint": [
+ "Cloud Ruins Seal - Toothbrush Alley",
+ ],
+ "Cloud Ruins - Saw Pit Checkpoint": [
+ "Cloud Ruins Seal - Saw Pit",
+ ],
+ "Cloud Ruins - Final Flight Shop": [
+ "Cloud Ruins - Acro",
+ ],
+ "Cloud Ruins - Manfred's Shop": [
+ "Cloud Ruins Seal - Money Farm Room",
+ ],
+ "Underworld - Left Shop": [
+ "Underworld Seal - Sharp and Windy Climb",
+ ],
+ "Underworld - Fireball Wave Shop": [
+ "Underworld Seal - Spike Wall",
+ "Underworld Seal - Fireball Wave",
+ ],
+ "Underworld - Hot Tub Checkpoint": [
+ "Underworld Seal - Rising Fanta",
+ ],
+ "Underworld - Key of Chaos Shop": [
+ "Underworld - Key of Chaos",
+ ],
+ "Riviere Turquoise - Waterfall Shop": [
+ "Riviere Turquoise Seal - Bounces and Balls",
+ ],
+ "Riviere Turquoise - Launch of Faith Shop": [
+ "Riviere Turquoise Seal - Launch of Faith",
+ ],
+ "Riviere Turquoise - Restock Shop": [
+ "Riviere Turquoise Seal - Flower Power",
+ ],
+ "Riviere Turquoise - Butterfly Matriarch Shop": [
+ "Riviere Turquoise - Butterfly Matriarch",
+ ],
+ "Sunken Shrine - Lifeguard Shop": [
+ "Sunken Shrine Seal - Ultra Lifeguard",
+ ],
+ "Sunken Shrine - Lightfoot Tabi Checkpoint": [
+ "Sunken Shrine - Lightfoot Tabi",
+ ],
+ "Sunken Shrine - Portal": [
+ "Sunken Shrine - Key of Love",
+ ],
+ "Sunken Shrine - Tabi Gauntlet Shop": [
+ "Sunken Shrine Seal - Tabi Gauntlet",
+ ],
+ "Sunken Shrine - Sun Crest Checkpoint": [
+ "Sunken Shrine - Sun Crest",
+ ],
+ "Sunken Shrine - Waterfall Paradise Checkpoint": [
+ "Sunken Shrine Seal - Waterfall Paradise",
+ ],
+ "Sunken Shrine - Moon Crest Checkpoint": [
+ "Sunken Shrine - Moon Crest",
+ ],
+ "Elemental Skylands - Air Seal Checkpoint": [
+ "Elemental Skylands Seal - Air",
+ ],
+ "Elemental Skylands - Water Intro Shop": [
+ "Elemental Skylands Seal - Water",
+ ],
+ "Elemental Skylands - Fire Intro Shop": [
+ "Elemental Skylands Seal - Fire",
+ ],
+ "Elemental Skylands - Right": [
+ "Elemental Skylands - Key of Symbiosis",
+ ],
+ "Corrupted Future": ["Corrupted Future - Key of Courage"],
+ "Music Box": ["Rescue Phantom"],
+}
+
+
+SUB_REGIONS: Dict[str, List[str]] = {
+ "Ninja Village": [
+ "Right",
+ ],
+ "Autumn Hills": [
+ "Left",
+ "Right",
+ "Bottom",
+ "Portal",
+ "Climbing Claws Shop",
+ "Hope Path Shop",
+ "Dimension Climb Shop",
+ "Leaf Golem Shop",
+ "Hope Path Checkpoint",
+ "Key of Hope Checkpoint",
+ "Lakeside Checkpoint",
+ "Double Swing Checkpoint",
+ "Spike Ball Swing Checkpoint",
+ ],
+ "Forlorn Temple": [
+ "Left",
+ "Right",
+ "Bottom",
+ "Outside Shop",
+ "Entrance Shop",
+ "Climb Shop",
+ "Rocket Sunset Shop",
+ "Descent Shop",
+ "Final Fall Shop",
+ "Demon King Shop",
+ "Sunny Day Checkpoint",
+ "Rocket Maze Checkpoint",
+ ],
+ "Catacombs": [
+ "Top Left",
+ "Bottom Left",
+ "Bottom",
+ "Right",
+ "Triple Spike Crushers Shop",
+ "Ruxxtin Shop",
+ "Death Trap Checkpoint",
+ "Crusher Gauntlet Checkpoint",
+ "Dirty Pond Checkpoint",
+ ],
+ "Bamboo Creek": [
+ "Bottom Left",
+ "Top Left",
+ "Right",
+ "Spike Crushers Shop",
+ "Abandoned Shop",
+ "Time Loop Shop",
+ "Spike Ball Pits Checkpoint",
+ "Spike Doors Checkpoint",
+ ],
+ "Howling Grotto": [
+ "Left",
+ "Top",
+ "Right",
+ "Bottom",
+ "Portal",
+ "Wingsuit Shop",
+ "Crushing Pits Shop",
+ "Emerald Golem Shop",
+ "Lost Woods Checkpoint",
+ "Breezy Crushers Checkpoint",
+ ],
+ "Quillshroom Marsh": [
+ "Top Left",
+ "Bottom Left",
+ "Top Right",
+ "Bottom Right",
+ "Spikey Window Shop",
+ "Sand Trap Shop",
+ "Queen of Quills Shop",
+ "Seashell Checkpoint",
+ "Quicksand Checkpoint",
+ "Spike Wave Checkpoint",
+ ],
+ "Searing Crags": [
+ "Left",
+ "Top",
+ "Bottom",
+ "Right",
+ "Portal",
+ "Rope Dart Shop",
+ "Falling Rocks Shop",
+ "Searing Mega Shard Shop",
+ "Before Final Climb Shop",
+ "Colossuses Shop",
+ "Key of Strength Shop",
+ "Triple Ball Spinner Checkpoint",
+ "Raining Rocks Checkpoint",
+ ],
+ "Glacial Peak": [
+ "Bottom",
+ "Top",
+ "Portal",
+ "Ice Climbers' Shop",
+ "Glacial Mega Shard Shop",
+ "Tower Entrance Shop",
+ "Projectile Spike Pit Checkpoint",
+ "Air Swag Checkpoint",
+ "Free Climbing Checkpoint",
+ ],
+ "Tower of Time": [
+ "Left",
+ "Entrance Shop",
+ "Arcane Golem Shop",
+ "First Checkpoint",
+ "Second Checkpoint",
+ "Third Checkpoint",
+ "Fourth Checkpoint",
+ "Fifth Checkpoint",
+ "Sixth Checkpoint",
+ ],
+ "Cloud Ruins": [
+ "Left",
+ "Entrance Shop",
+ "Pillar Glide Shop",
+ "Crushers' Descent Shop",
+ "Seeing Spikes Shop",
+ "Sliding Spikes Shop",
+ "Final Flight Shop",
+ "Manfred's Shop",
+ "Spike Float Checkpoint",
+ "Ghost Pit Checkpoint",
+ "Toothbrush Alley Checkpoint",
+ "Saw Pit Checkpoint",
+ ],
+ "Underworld": [
+ "Left",
+ "Entrance Shop",
+ "Fireball Wave Shop",
+ "Long Climb Shop",
+ "Barm'athaziel Shop",
+ "Key of Chaos Shop",
+ "Hot Dip Checkpoint",
+ "Hot Tub Checkpoint",
+ "Lava Run Checkpoint",
+ ],
+ "Riviere Turquoise": [
+ "Right",
+ "Portal",
+ "Waterfall Shop",
+ "Launch of Faith Shop",
+ "Log Flume Shop",
+ "Log Climb Shop",
+ "Restock Shop",
+ "Butterfly Matriarch Shop",
+ "Flower Flight Checkpoint",
+ ],
+ "Elemental Skylands": [
+ "Air Shmup",
+ "Air Intro Shop",
+ "Air Seal Checkpoint",
+ "Air Generator Shop",
+ "Earth Shmup",
+ "Earth Intro Shop",
+ "Earth Generator Shop",
+ "Fire Shmup",
+ "Fire Intro Shop",
+ "Fire Generator Shop",
+ "Water Shmup",
+ "Water Intro Shop",
+ "Water Generator Shop",
+ "Right",
+ ],
+ "Sunken Shrine": [
+ "Left",
+ "Portal",
+ "Entrance Shop",
+ "Lifeguard Shop",
+ "Sun Path Shop",
+ "Tabi Gauntlet Shop",
+ "Moon Path Shop",
+ "Ninja Tabi Checkpoint",
+ "Sun Crest Checkpoint",
+ "Waterfall Paradise Checkpoint",
+ "Moon Crest Checkpoint",
+ ],
+}
+
+
+# order is slightly funky here for back compat
+MEGA_SHARDS: Dict[str, List[str]] = {
+ "Autumn Hills - Lakeside Checkpoint": ["Autumn Hills Mega Shard"],
+ "Forlorn Temple - Outside Shop": ["Hidden Entrance Mega Shard"],
+ "Catacombs - Top Left": ["Catacombs Mega Shard"],
+ "Bamboo Creek - Spike Crushers Shop": ["Above Entrance Mega Shard"],
+ "Bamboo Creek - Abandoned Shop": ["Abandoned Mega Shard"],
+ "Bamboo Creek - Time Loop Shop": ["Time Loop Mega Shard"],
+ "Howling Grotto - Lost Woods Checkpoint": ["Bottom Left Mega Shard"],
+ "Howling Grotto - Breezy Crushers Checkpoint": ["Near Portal Mega Shard", "Pie in the Sky Mega Shard"],
+ "Quillshroom Marsh - Spikey Window Shop": ["Quillshroom Marsh Mega Shard"],
+ "Searing Crags - Searing Mega Shard Shop": ["Searing Crags Mega Shard"],
+ "Glacial Peak - Glacial Mega Shard Shop": ["Glacial Peak Mega Shard"],
+ "Cloud Ruins - Cloud Entrance Shop": ["Cloud Entrance Mega Shard", "Time Warp Mega Shard"],
+ "Cloud Ruins - Manfred's Shop": ["Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2"],
+ "Underworld - Left Shop": ["Under Entrance Mega Shard"],
+ "Underworld - Hot Tub Checkpoint": ["Hot Tub Mega Shard", "Projectile Pit Mega Shard"],
+ "Forlorn Temple - Sunny Day Checkpoint": ["Sunny Day Mega Shard"],
+ "Forlorn Temple - Demon King Shop": ["Down Under Mega Shard"],
+ "Sunken Shrine - Waterfall Paradise Checkpoint": ["Mega Shard of the Moon"],
+ "Sunken Shrine - Portal": ["Beginner's Mega Shard"],
+ "Sunken Shrine - Above Portal Shop": ["Mega Shard of the Stars"],
+ "Sunken Shrine - Sun Crest Checkpoint": ["Mega Shard of the Sun"],
+ "Riviere Turquoise - Waterfall Shop": ["Waterfall Mega Shard"],
+ "Riviere Turquoise - Restock Shop": ["Quick Restock Mega Shard 1", "Quick Restock Mega Shard 2"],
+ "Elemental Skylands - Earth Intro Shop": ["Earth Mega Shard"],
+ "Elemental Skylands - Water Generator Shop": ["Water Mega Shard"],
+}
+
+
+REGION_CONNECTIONS: Dict[str, Dict[str, str]] = {
+ "Menu": {"Tower HQ": "Start Game"},
+ "Tower HQ": {
+ "Autumn Hills - Portal": "ToTHQ Autumn Hills Portal",
+ "Howling Grotto - Portal": "ToTHQ Howling Grotto Portal",
+ "Searing Crags - Portal": "ToTHQ Searing Crags Portal",
+ "Glacial Peak - Portal": "ToTHQ Glacial Peak Portal",
+ "Tower of Time - Left": "Artificer's Challenge",
+ "Riviere Turquoise - Portal": "ToTHQ Riviere Turquoise Portal",
+ "Sunken Shrine - Portal": "ToTHQ Sunken Shrine Portal",
+ "Corrupted Future": "Artificer's Portal",
+ "The Shop": "Home",
+ "Music Box": "Shrink Down",
+ },
+ "The Shop": {
+ "The Craftsman's Corner": "Money Sink",
+ },
+}
+"""Vanilla layout mapping with all Tower HQ portals open. format is source[exit_region][entrance_name]"""
+
+
+# regions that don't have sub-regions
+LEVELS: List[str] = [
+ "Menu",
+ "Tower HQ",
+ "The Shop",
+ "The Craftsman's Corner",
+ "Corrupted Future",
+ "Music Box",
+]
diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py
new file mode 100644
index 000000000000..85b73dec4147
--- /dev/null
+++ b/worlds/messenger/rules.py
@@ -0,0 +1,533 @@
+from typing import Dict, TYPE_CHECKING
+
+from BaseClasses import CollectionState
+from worlds.generic.Rules import CollectionRule, add_rule, allow_self_locking_items
+from .constants import NOTES, PHOBEKINS
+from .options import MessengerAccessibility
+
+if TYPE_CHECKING:
+ from . import MessengerWorld
+
+
+class MessengerRules:
+ player: int
+ world: "MessengerWorld"
+ connection_rules: Dict[str, CollectionRule]
+ region_rules: Dict[str, CollectionRule]
+ location_rules: Dict[str, CollectionRule]
+ maximum_price: int
+ required_seals: int
+
+ def __init__(self, world: "MessengerWorld") -> None:
+ self.player = world.player
+ self.world = world
+
+ # these locations are at the top of the shop tree, and the entire shop tree needs to be purchased
+ maximum_price = (world.multiworld.get_location("The Shop - Demon's Bane", self.player).cost +
+ world.multiworld.get_location("The Shop - Focused Power Sense", self.player).cost)
+ self.maximum_price = min(maximum_price, world.total_shards)
+ self.required_seals = max(1, world.required_seals)
+
+ # dict of connection names and requirements to traverse the exit
+ self.connection_rules = {
+ # from ToTHQ
+ "Artificer's Portal":
+ lambda state: state.has_all({"Demon King Crown", "Magic Firefly"}, self.player),
+ "Shrink Down":
+ lambda state: state.has_all(NOTES, self.player) or self.has_enough_seals(state),
+ # the shop
+ "Money Sink":
+ lambda state: state.has("Money Wrench", self.player) and self.can_shop(state),
+ # Autumn Hills
+ "Autumn Hills - Portal -> Autumn Hills - Dimension Climb Shop":
+ lambda state: self.has_wingsuit(state) and self.has_dart(state),
+ "Autumn Hills - Dimension Climb Shop -> Autumn Hills - Portal":
+ self.has_vertical,
+ "Autumn Hills - Climbing Claws Shop -> Autumn Hills - Hope Path Shop":
+ self.has_dart,
+ "Autumn Hills - Climbing Claws Shop -> Autumn Hills - Key of Hope Checkpoint":
+ self.false, # hard logic only
+ "Autumn Hills - Hope Path Shop -> Autumn Hills - Hope Latch Checkpoint":
+ self.has_dart,
+ "Autumn Hills - Hope Path Shop -> Autumn Hills - Climbing Claws Shop":
+ lambda state: self.has_dart(state) and self.can_dboost(state),
+ "Autumn Hills - Hope Path Shop -> Autumn Hills - Lakeside Checkpoint":
+ lambda state: self.has_dart(state) and self.can_dboost(state),
+ "Autumn Hills - Hope Latch Checkpoint -> Autumn Hills - Hope Path Shop":
+ self.can_dboost,
+ "Autumn Hills - Hope Latch Checkpoint -> Autumn Hills - Key of Hope Checkpoint":
+ lambda state: self.has_dart(state) and self.has_wingsuit(state),
+ # Forlorn Temple
+ "Forlorn Temple - Outside Shop -> Forlorn Temple - Entrance Shop":
+ lambda state: state.has_all(PHOBEKINS, self.player),
+ "Forlorn Temple - Entrance Shop -> Forlorn Temple - Outside Shop":
+ lambda state: state.has_all(PHOBEKINS, self.player),
+ "Forlorn Temple - Entrance Shop -> Forlorn Temple - Sunny Day Checkpoint":
+ lambda state: self.has_vertical(state) and self.can_dboost(state),
+ "Forlorn Temple - Sunny Day Checkpoint -> Forlorn Temple - Rocket Maze Checkpoint":
+ self.has_vertical,
+ "Forlorn Temple - Rocket Sunset Shop -> Forlorn Temple - Descent Shop":
+ lambda state: self.has_dart(state) and (self.can_dboost(state) or self.has_wingsuit(state)),
+ "Forlorn Temple - Saw Gauntlet Shop -> Forlorn Temple - Demon King Shop":
+ self.has_vertical,
+ "Forlorn Temple - Demon King Shop -> Forlorn Temple - Saw Gauntlet Shop":
+ self.has_vertical,
+ # Howling Grotto
+ "Howling Grotto - Portal -> Howling Grotto - Crushing Pits Shop":
+ self.has_wingsuit,
+ "Howling Grotto - Wingsuit Shop -> Howling Grotto - Left":
+ self.has_wingsuit,
+ "Howling Grotto - Wingsuit Shop -> Howling Grotto - Lost Woods Checkpoint":
+ self.has_wingsuit,
+ "Howling Grotto - Lost Woods Checkpoint -> Howling Grotto - Bottom":
+ lambda state: state.has("Seashell", self.player),
+ "Howling Grotto - Crushing Pits Shop -> Howling Grotto - Portal":
+ lambda state: self.has_wingsuit(state) or self.can_dboost(state),
+ "Howling Grotto - Breezy Crushers Checkpoint -> Howling Grotto - Emerald Golem Shop":
+ self.has_wingsuit,
+ "Howling Grotto - Breezy Crushers Checkpoint -> Howling Grotto - Crushing Pits Shop":
+ lambda state: (self.has_wingsuit(state) or self.can_dboost(
+ state
+ ) or self.can_destroy_projectiles(state))
+ and state.multiworld.get_region(
+ "Howling Grotto - Emerald Golem Shop", self.player
+ ).can_reach(state),
+ "Howling Grotto - Emerald Golem Shop -> Howling Grotto - Right":
+ self.has_wingsuit,
+ # Searing Crags
+ "Searing Crags - Rope Dart Shop -> Searing Crags - Triple Ball Spinner Checkpoint":
+ self.has_vertical,
+ "Searing Crags - Portal -> Searing Crags - Right":
+ self.has_tabi,
+ "Searing Crags - Portal -> Searing Crags - Before Final Climb Shop":
+ self.has_wingsuit,
+ "Searing Crags - Portal -> Searing Crags - Colossuses Shop":
+ self.has_wingsuit,
+ "Searing Crags - Bottom -> Searing Crags - Portal":
+ self.has_wingsuit,
+ "Searing Crags - Right -> Searing Crags - Portal":
+ lambda state: self.has_tabi(state) and self.has_wingsuit(state),
+ "Searing Crags - Colossuses Shop -> Searing Crags - Key of Strength Shop":
+ lambda state: state.has("Power Thistle", self.player)
+ and (self.has_dart(state)
+ or (self.has_wingsuit(state)
+ and self.can_destroy_projectiles(state))),
+ "Searing Crags - Falling Rocks Shop -> Searing Crags - Searing Mega Shard Shop":
+ self.has_dart,
+ "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Before Final Climb Shop":
+ lambda state: self.has_dart(state) or self.can_destroy_projectiles(state),
+ "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Falling Rocks Shop":
+ self.has_dart,
+ "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Shop":
+ self.false,
+ "Searing Crags - Before Final Climb Shop -> Searing Crags - Colossuses Shop":
+ self.has_dart,
+ # Glacial Peak
+ "Glacial Peak - Portal -> Glacial Peak - Tower Entrance Shop":
+ self.has_vertical,
+ "Glacial Peak - Left -> Elemental Skylands - Air Shmup":
+ lambda state: state.has("Magic Firefly", self.player)
+ and state.multiworld.get_location("Quillshroom Marsh - Queen of Quills", self.player)
+ .can_reach(state),
+ "Glacial Peak - Tower Entrance Shop -> Glacial Peak - Top":
+ lambda state: state.has("Ruxxtin's Amulet", self.player),
+ "Glacial Peak - Projectile Spike Pit Checkpoint -> Glacial Peak - Left":
+ lambda state: self.has_dart(state) or (self.can_dboost(state) and self.has_wingsuit(state)),
+ # Tower of Time
+ "Tower of Time - Left -> Tower of Time - Final Chance Shop":
+ self.has_dart,
+ "Tower of Time - Second Checkpoint -> Tower of Time - Third Checkpoint":
+ lambda state: self.has_wingsuit(state) and (self.has_dart(state) or self.can_dboost(state)),
+ "Tower of Time - Third Checkpoint -> Tower of Time - Fourth Checkpoint":
+ lambda state: self.has_wingsuit(state) or self.can_dboost(state),
+ "Tower of Time - Fourth Checkpoint -> Tower of Time - Fifth Checkpoint":
+ lambda state: self.has_wingsuit(state) and self.has_dart(state),
+ "Tower of Time - Fifth Checkpoint -> Tower of Time - Sixth Checkpoint":
+ self.has_wingsuit,
+ # Cloud Ruins
+ "Cloud Ruins - Cloud Entrance Shop -> Cloud Ruins - Spike Float Checkpoint":
+ self.has_wingsuit,
+ "Cloud Ruins - Spike Float Checkpoint -> Cloud Ruins - Cloud Entrance Shop":
+ lambda state: self.has_vertical(state) or self.can_dboost(state),
+ "Cloud Ruins - Spike Float Checkpoint -> Cloud Ruins - Pillar Glide Shop":
+ lambda state: self.has_vertical(state) or self.can_dboost(state),
+ "Cloud Ruins - Pillar Glide Shop -> Cloud Ruins - Spike Float Checkpoint":
+ lambda state: self.has_vertical(state) and self.can_double_dboost(state),
+ "Cloud Ruins - Pillar Glide Shop -> Cloud Ruins - Ghost Pit Checkpoint":
+ lambda state: self.has_dart(state) and self.has_wingsuit(state),
+ "Cloud Ruins - Pillar Glide Shop -> Cloud Ruins - Crushers' Descent Shop":
+ lambda state: self.has_wingsuit(state) and (self.has_dart(state) or self.can_dboost(state)),
+ "Cloud Ruins - Toothbrush Alley Checkpoint -> Cloud Ruins - Seeing Spikes Shop":
+ self.has_vertical,
+ "Cloud Ruins - Seeing Spikes Shop -> Cloud Ruins - Sliding Spikes Shop":
+ self.has_wingsuit,
+ "Cloud Ruins - Sliding Spikes Shop -> Cloud Ruins - Seeing Spikes Shop":
+ self.has_wingsuit,
+ "Cloud Ruins - Sliding Spikes Shop -> Cloud Ruins - Saw Pit Checkpoint":
+ self.has_vertical,
+ "Cloud Ruins - Final Flight Shop -> Cloud Ruins - Manfred's Shop":
+ lambda state: self.has_wingsuit(state) and self.has_dart(state),
+ "Cloud Ruins - Manfred's Shop -> Cloud Ruins - Final Flight Shop":
+ lambda state: self.has_wingsuit(state) and self.can_dboost(state),
+ # Underworld
+ "Underworld - Left -> Underworld - Left Shop":
+ self.has_tabi,
+ "Underworld - Left Shop -> Underworld - Left":
+ self.has_tabi,
+ "Underworld - Hot Dip Checkpoint -> Underworld - Lava Run Checkpoint":
+ self.has_tabi,
+ "Underworld - Fireball Wave Shop -> Underworld - Long Climb Shop":
+ lambda state: self.can_destroy_projectiles(state) or self.has_tabi(state) or self.has_vertical(state),
+ "Underworld - Long Climb Shop -> Underworld - Hot Tub Checkpoint":
+ lambda state: self.has_tabi(state)
+ and (self.can_destroy_projectiles(state)
+ or self.has_wingsuit(state))
+ or (self.has_wingsuit(state)
+ and (self.has_dart(state)
+ or self.can_dboost(state)
+ or self.can_destroy_projectiles(state))),
+ "Underworld - Hot Tub Checkpoint -> Underworld - Long Climb Shop":
+ lambda state: self.has_tabi(state)
+ or self.can_destroy_projectiles(state)
+ or (self.has_dart(state) and self.has_wingsuit(state)),
+ # Dark Cave
+ "Dark Cave - Right -> Dark Cave - Left":
+ lambda state: state.has("Candle", self.player) and self.has_dart(state),
+ # Riviere Turquoise
+ "Riviere Turquoise - Waterfall Shop -> Riviere Turquoise - Flower Flight Checkpoint":
+ lambda state: self.has_dart(state) or (
+ self.has_wingsuit(state) and self.can_destroy_projectiles(state)),
+ "Riviere Turquoise - Launch of Faith Shop -> Riviere Turquoise - Flower Flight Checkpoint":
+ lambda state: self.has_dart(state) and self.can_dboost(state),
+ "Riviere Turquoise - Flower Flight Checkpoint -> Riviere Turquoise - Waterfall Shop":
+ lambda state: False,
+ # Elemental Skylands
+ "Elemental Skylands - Air Intro Shop -> Elemental Skylands - Air Seal Checkpoint":
+ self.has_wingsuit,
+ "Elemental Skylands - Air Intro Shop -> Elemental Skylands - Air Generator Shop":
+ self.has_wingsuit,
+ # Sunken Shrine
+ "Sunken Shrine - Portal -> Sunken Shrine - Sun Path Shop":
+ self.has_tabi,
+ "Sunken Shrine - Portal -> Sunken Shrine - Moon Path Shop":
+ self.has_tabi,
+ "Sunken Shrine - Moon Path Shop -> Sunken Shrine - Waterfall Paradise Checkpoint":
+ self.has_tabi,
+ "Sunken Shrine - Waterfall Paradise Checkpoint -> Sunken Shrine - Moon Path Shop":
+ self.has_tabi,
+ "Sunken Shrine - Tabi Gauntlet Shop -> Sunken Shrine - Sun Path Shop":
+ lambda state: self.can_dboost(state) or self.has_dart(state),
+ }
+
+ self.location_rules = {
+ # ninja village
+ "Ninja Village Seal - Tree House":
+ self.has_dart,
+ "Ninja Village - Candle":
+ lambda state: state.multiworld.get_location("Searing Crags - Astral Tea Leaves", self.player).can_reach(
+ state),
+ # autumn hills
+ "Autumn Hills Seal - Spike Ball Darts":
+ self.is_aerobatic,
+ "Autumn Hills Seal - Trip Saws":
+ self.has_wingsuit,
+ "Autumn Hills Seal - Double Swing Saws":
+ self.has_vertical,
+ # forlorn temple
+ "Forlorn Temple Seal - Rocket Maze":
+ self.has_vertical,
+ # bamboo creek
+ "Bamboo Creek - Claustro":
+ lambda state: self.has_wingsuit(state) and (self.has_dart(state) or self.can_dboost(state)),
+ "Above Entrance Mega Shard":
+ lambda state: self.has_dart(state) or self.can_dboost(state),
+ "Bamboo Creek Seal - Spike Ball Pits":
+ self.has_wingsuit,
+ # howling grotto
+ "Howling Grotto Seal - Windy Saws and Balls":
+ self.has_wingsuit,
+ "Howling Grotto Seal - Crushing Pits":
+ lambda state: self.has_wingsuit(state) and self.has_dart(state),
+ "Howling Grotto - Emerald Golem":
+ self.has_wingsuit,
+ # searing crags
+ "Searing Crags - Astral Tea Leaves":
+ lambda state: state.multiworld.get_location("Ninja Village - Astral Seed", self.player).can_reach(state),
+ "Searing Crags Seal - Triple Ball Spinner":
+ self.can_dboost,
+ "Searing Crags - Pyro":
+ self.has_tabi,
+ # glacial peak
+ "Glacial Peak Seal - Ice Climbers":
+ self.has_dart,
+ "Glacial Peak Seal - Projectile Spike Pit":
+ self.can_destroy_projectiles,
+ # tower of time
+ "Tower of Time Seal - Time Waster":
+ self.has_dart,
+ # cloud ruins
+ "Time Warp Mega Shard":
+ lambda state: self.has_vertical(state) or self.can_dboost(state),
+ "Cloud Ruins Seal - Ghost Pit":
+ self.has_vertical,
+ "Cloud Ruins Seal - Toothbrush Alley":
+ self.has_dart,
+ "Cloud Ruins Seal - Saw Pit":
+ self.has_vertical,
+ # underworld
+ "Underworld Seal - Sharp and Windy Climb":
+ self.has_wingsuit,
+ "Underworld Seal - Fireball Wave":
+ self.is_aerobatic,
+ "Underworld Seal - Rising Fanta":
+ self.has_dart,
+ "Hot Tub Mega Shard":
+ lambda state: self.has_tabi(state) or self.has_dart(state),
+ # sunken shrine
+ "Sunken Shrine - Key of Love":
+ lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player),
+ "Sunken Shrine Seal - Waterfall Paradise":
+ self.has_tabi,
+ "Sunken Shrine Seal - Tabi Gauntlet":
+ self.has_tabi,
+ "Mega Shard of the Sun":
+ self.has_tabi,
+ # riviere turquoise
+ "Riviere Turquoise Seal - Bounces and Balls":
+ self.can_dboost,
+ "Riviere Turquoise Seal - Launch of Faith":
+ lambda state: self.has_vertical(state),
+ # elemental skylands
+ "Elemental Skylands - Key of Symbiosis":
+ self.has_dart,
+ "Elemental Skylands Seal - Air":
+ self.has_wingsuit,
+ "Elemental Skylands Seal - Water":
+ lambda state: self.has_dart(state) and state.has("Currents Master", self.player),
+ "Elemental Skylands Seal - Fire":
+ lambda state: self.has_dart(state) and self.can_destroy_projectiles(state) and self.is_aerobatic(state),
+ "Earth Mega Shard":
+ self.has_dart,
+ "Water Mega Shard":
+ self.has_dart,
+ }
+
+ def has_wingsuit(self, state: CollectionState) -> bool:
+ return state.has("Wingsuit", self.player)
+
+ def has_dart(self, state: CollectionState) -> bool:
+ return state.has("Rope Dart", self.player)
+
+ def has_tabi(self, state: CollectionState) -> bool:
+ return state.has("Lightfoot Tabi", self.player)
+
+ def has_vertical(self, state: CollectionState) -> bool:
+ return self.has_wingsuit(state) or self.has_dart(state)
+
+ def has_enough_seals(self, state: CollectionState) -> bool:
+ return state.has("Power Seal", self.player, self.required_seals)
+
+ def can_destroy_projectiles(self, state: CollectionState) -> bool:
+ return state.has("Strike of the Ninja", self.player)
+
+ def can_dboost(self, state: CollectionState) -> bool:
+ return state.has_any({"Path of Resilience", "Meditation"}, self.player) and \
+ state.has("Second Wind", self.player)
+
+ def can_double_dboost(self, state: CollectionState) -> bool:
+ return state.has_all({"Path of Resilience", "Meditation", "Second Wind"}, self.player)
+
+ def is_aerobatic(self, state: CollectionState) -> bool:
+ return self.has_wingsuit(state) and state.has("Aerobatics Warrior", self.player)
+
+ def true(self, state: CollectionState) -> bool:
+ """I know this is stupid, but it's easier to read in the dicts."""
+ return True
+
+ def false(self, state: CollectionState) -> bool:
+ """It's a bit easier to just always create the connections that are only possible in hard or higher logic."""
+ return False
+
+ def can_shop(self, state: CollectionState) -> bool:
+ return state.has("Shards", self.player, self.maximum_price)
+
+ def set_messenger_rules(self) -> None:
+ multiworld = self.world.multiworld
+
+ for entrance_name, rule in self.connection_rules.items():
+ entrance = multiworld.get_entrance(entrance_name, self.player)
+ entrance.access_rule = rule
+ for loc in multiworld.get_locations(self.player):
+ if loc.name in self.location_rules:
+ loc.access_rule = self.location_rules[loc.name]
+
+ if self.world.options.music_box and not self.world.options.limited_movement:
+ add_rule(multiworld.get_entrance("Shrink Down", self.player), self.has_dart)
+ multiworld.completion_condition[self.player] = lambda state: state.has("Do the Thing!", self.player)
+ if self.world.options.accessibility: # not locations accessibility
+ set_self_locking_items(self.world, self.player)
+
+
+class MessengerHardRules(MessengerRules):
+ def __init__(self, world: "MessengerWorld") -> None:
+ super().__init__(world)
+
+ self.connection_rules.update(
+ {
+ # Autumn Hills
+ "Autumn Hills - Portal -> Autumn Hills - Dimension Climb Shop":
+ self.has_dart,
+ "Autumn Hills - Climbing Claws Shop -> Autumn Hills - Key of Hope Checkpoint":
+ self.true, # super easy normal clip - also possible with moderately difficult cloud stepping
+ # Howling Grotto
+ "Howling Grotto - Portal -> Howling Grotto - Crushing Pits Shop":
+ self.true,
+ "Howling Grotto - Lost Woods Checkpoint -> Howling Grotto - Bottom":
+ self.true, # just memorize the pattern :)
+ "Howling Grotto - Crushing Pits Shop -> Howling Grotto - Portal":
+ self.true,
+ "Howling Grotto - Breezy Crushers Checkpoint -> Howling Grotto - Emerald Golem Shop":
+ lambda state: self.has_wingsuit(state) or # there's a very easy normal clip here but it's 16-bit only
+ "Howling Grotto - Breezy Crushers Checkpoint" in self.world.spoiler_portal_mapping.values(),
+ # Searing Crags
+ "Searing Crags - Rope Dart Shop -> Searing Crags - Triple Ball Spinner Checkpoint":
+ lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state),
+ # it's doable without anything but one jump is pretty hard and time warping is no longer reliable
+ "Searing Crags - Falling Rocks Shop -> Searing Crags - Searing Mega Shard Shop":
+ lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state),
+ "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Falling Rocks Shop":
+ lambda state: self.has_dart(state) or
+ (self.can_destroy_projectiles(state) and
+ (self.has_wingsuit(state) or self.can_dboost(state))),
+ "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Shop":
+ lambda state: self.can_leash(state) or self.has_windmill(state),
+ "Searing Crags - Before Final Climb Shop -> Searing Crags - Colossuses Shop":
+ self.true,
+ # Glacial Peak
+ "Glacial Peak - Left -> Elemental Skylands - Air Shmup":
+ lambda state: self.has_windmill(state) or
+ (state.has("Magic Firefly", self.player) and
+ state.multiworld.get_location(
+ "Quillshroom Marsh - Queen of Quills", self.player).can_reach(state)) or
+ (self.has_dart(state) and self.can_dboost(state)),
+ "Glacial Peak - Projectile Spike Pit Checkpoint -> Glacial Peak - Left":
+ lambda state: self.has_vertical(state) or self.has_windmill(state),
+ # Cloud Ruins
+ "Cloud Ruins - Sliding Spikes Shop -> Cloud Ruins - Saw Pit Checkpoint":
+ self.true,
+ # Elemental Skylands
+ "Elemental Skylands - Air Intro Shop -> Elemental Skylands - Air Generator Shop":
+ self.true,
+ # Riviere Turquoise
+ "Riviere Turquoise - Waterfall Shop -> Riviere Turquoise - Flower Flight Checkpoint":
+ self.true,
+ "Riviere Turquoise - Launch of Faith Shop -> Riviere Turquoise - Flower Flight Checkpoint":
+ self.can_dboost,
+ "Riviere Turquoise - Flower Flight Checkpoint -> Riviere Turquoise - Waterfall Shop":
+ self.can_double_dboost,
+ }
+ )
+
+ self.location_rules.update(
+ {
+ "Autumn Hills Seal - Spike Ball Darts":
+ lambda state: self.has_vertical(state) and self.has_windmill(state) or self.is_aerobatic(state),
+ "Autumn Hills Seal - Double Swing Saws":
+ lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state),
+ "Bamboo Creek - Claustro":
+ self.has_wingsuit,
+ "Bamboo Creek Seal - Spike Ball Pits":
+ self.true,
+ "Howling Grotto Seal - Windy Saws and Balls":
+ self.true,
+ "Searing Crags Seal - Triple Ball Spinner":
+ self.true,
+ "Glacial Peak Seal - Ice Climbers":
+ lambda state: self.has_vertical(state) or self.can_dboost(state),
+ "Glacial Peak Seal - Projectile Spike Pit":
+ lambda state: self.can_dboost(state) or self.can_destroy_projectiles(state),
+ "Glacial Peak Seal - Glacial Air Swag":
+ lambda state: self.has_windmill(state) or self.has_vertical(state),
+ "Glacial Peak Mega Shard":
+ lambda state: self.has_windmill(state) or self.has_vertical(state),
+ "Cloud Ruins Seal - Ghost Pit":
+ self.true,
+ "Cloud Ruins Seal - Toothbrush Alley":
+ self.true,
+ "Cloud Ruins Seal - Saw Pit":
+ self.true,
+ "Underworld Seal - Fireball Wave":
+ lambda state: self.is_aerobatic(state) or self.has_windmill(state),
+ "Riviere Turquoise Seal - Bounces and Balls":
+ self.true,
+ "Riviere Turquoise Seal - Launch of Faith":
+ lambda state: self.can_dboost(state) or self.has_vertical(state),
+ "Elemental Skylands - Key of Symbiosis":
+ lambda state: self.has_dart(state) or self.can_dboost(state) or self.has_windmill(state),
+ "Elemental Skylands Seal - Water":
+ lambda state: self.has_dart(state) or self.can_dboost(state) or self.has_windmill(state),
+ "Elemental Skylands Seal - Fire":
+ lambda state: (self.has_dart(state) or self.can_dboost(state) or self.has_windmill(state))
+ and self.can_destroy_projectiles(state),
+ "Earth Mega Shard":
+ lambda state: self.has_dart(state) or self.can_dboost(state) or self.has_windmill(state),
+ "Water Mega Shard":
+ lambda state: self.has_dart(state) or self.can_dboost(state) or self.has_windmill(state),
+ }
+ )
+
+ def has_windmill(self, state: CollectionState) -> bool:
+ return state.has("Windmill Shuriken", self.player)
+
+ def can_dboost(self, state: CollectionState) -> bool:
+ return state.has("Second Wind", self.player) # who really needs meditation
+
+ def can_destroy_projectiles(self, state: CollectionState) -> bool:
+ return super().can_destroy_projectiles(state) or self.has_windmill(state)
+
+ def can_leash(self, state: CollectionState) -> bool:
+ return self.has_dart(state) and self.can_dboost(state)
+
+
+class MessengerOOBRules(MessengerRules):
+ def __init__(self, world: "MessengerWorld") -> None:
+ self.world = world
+ self.player = world.player
+
+ self.required_seals = max(1, world.required_seals)
+ self.region_rules = {
+ "Elemental Skylands":
+ lambda state: state.has_any(
+ {"Windmill Shuriken", "Wingsuit", "Rope Dart", "Magic Firefly"}, self.player
+ ),
+ "Music Box": lambda state: state.has_all(set(NOTES), self.player) or self.has_enough_seals(state),
+ }
+
+ self.location_rules = {
+ "Bamboo Creek - Claustro": self.has_wingsuit,
+ "Searing Crags - Key of Strength": self.has_wingsuit,
+ "Sunken Shrine - Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player),
+ "Searing Crags - Pyro": self.has_tabi,
+ "Underworld - Key of Chaos": self.has_tabi,
+ "Corrupted Future - Key of Courage":
+ lambda state: state.has_all({"Demon King Crown", "Magic Firefly"}, self.player),
+ "Autumn Hills Seal - Spike Ball Darts": self.has_dart,
+ "Ninja Village Seal - Tree House": self.has_dart,
+ "Underworld Seal - Fireball Wave": lambda state: state.has_any(
+ {"Wingsuit", "Windmill Shuriken"},
+ self.player
+ ),
+ "Tower of Time Seal - Time Waster": self.has_dart,
+ }
+
+ def set_messenger_rules(self) -> None:
+ super().set_messenger_rules()
+ self.world.options.accessibility.value = MessengerAccessibility.option_minimal
+
+
+def set_self_locking_items(world: "MessengerWorld", player: int) -> None:
+ # locations where these placements are always valid
+ allow_self_locking_items(world.get_location("Searing Crags - Key of Strength").parent_region, "Power Thistle")
+ allow_self_locking_items(world.get_location("Sunken Shrine - Key of Love"), "Sun Crest", "Moon Crest")
+ allow_self_locking_items(world.get_location("Corrupted Future - Key of Courage").parent_region, "Demon King Crown")
+ allow_self_locking_items(world.get_location("Elemental Skylands Seal - Water"), "Currents Master")
diff --git a/worlds/messenger/shop.py b/worlds/messenger/shop.py
new file mode 100644
index 000000000000..3c8c7bf6f21e
--- /dev/null
+++ b/worlds/messenger/shop.py
@@ -0,0 +1,100 @@
+from typing import Dict, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Union
+
+if TYPE_CHECKING:
+ from . import MessengerWorld
+else:
+ MessengerWorld = object
+
+PROG_SHOP_ITEMS: List[str] = [
+ "Path of Resilience",
+ "Meditation",
+ "Strike of the Ninja",
+ "Second Wind",
+ "Currents Master",
+ "Aerobatics Warrior",
+]
+
+USEFUL_SHOP_ITEMS: List[str] = [
+ "Karuta Plates",
+ "Serendipitous Bodies",
+ "Kusari Jacket",
+ "Energy Shuriken",
+ "Serendipitous Minds",
+ "Rejuvenate Spirit",
+ "Demon's Bane",
+]
+
+
+class ShopData(NamedTuple):
+ internal_name: str
+ min_price: int
+ max_price: int
+ prerequisite: Optional[Union[str, Set[str]]] = None
+
+
+SHOP_ITEMS: Dict[str, ShopData] = {
+ "Karuta Plates": ShopData("HP_UPGRADE_1", 20, 200),
+ "Serendipitous Bodies": ShopData("ENEMY_DROP_HP", 20, 300, "The Shop - Karuta Plates"),
+ "Path of Resilience": ShopData("DAMAGE_REDUCTION", 100, 500, "The Shop - Serendipitous Bodies"),
+ "Kusari Jacket": ShopData("HP_UPGRADE_2", 100, 500, "The Shop - Serendipitous Bodies"),
+ "Energy Shuriken": ShopData("SHURIKEN", 20, 200),
+ "Serendipitous Minds": ShopData("ENEMY_DROP_MANA", 20, 300, "The Shop - Energy Shuriken"),
+ "Prepared Mind": ShopData("SHURIKEN_UPGRADE_1", 100, 600, "The Shop - Serendipitous Minds"),
+ "Meditation": ShopData("CHECKPOINT_FULL", 100, 600,
+ {"The Shop - Prepared Mind", "The Shop - Kusari Jacket"}),
+ "Rejuvenative Spirit": ShopData("POTION_FULL_HEAL_AND_HP", 300, 800, "The Shop - Meditation"),
+ "Centered Mind": ShopData("SHURIKEN_UPGRADE_2", 300, 800, "The Shop - Meditation"),
+ "Strike of the Ninja": ShopData("ATTACK_PROJECTILE", 20, 200),
+ "Second Wind": ShopData("AIR_RECOVER", 20, 350, "The Shop - Strike of the Ninja"),
+ "Currents Master": ShopData("SWIM_DASH", 100, 600, "The Shop - Second Wind"),
+ "Aerobatics Warrior": ShopData("GLIDE_ATTACK", 300, 800, "The Shop - Currents Master"),
+ "Demon's Bane": ShopData("CHARGED_ATTACK", 400, 1000,
+ {"The Shop - Rejuvenative Spirit", "The Shop - Aerobatics Warrior"}),
+ "Devil's Due": ShopData("QUARBLE_DISCOUNT_50", 20, 200),
+ "Time Sense": ShopData("TIME_WARP", 20, 300),
+ "Power Sense": ShopData("POWER_SEAL", 100, 800, "The Shop - Time Sense"),
+ "Focused Power Sense": ShopData("POWER_SEAL_WORLD_MAP", 300, 600, "The Shop - Power Sense"),
+}
+
+FIGURINES: Dict[str, ShopData] = {
+ "Green Kappa Figurine": ShopData("GREEN_KAPPA", 100, 500),
+ "Blue Kappa Figurine": ShopData("BLUE_KAPPA", 100, 500),
+ "Ountarde Figurine": ShopData("OUNTARDE", 100, 500),
+ "Red Kappa Figurine": ShopData("RED_KAPPA", 100, 500),
+ "Demon King Figurine": ShopData("DEMON_KING", 600, 2000),
+ "Quillshroom Figurine": ShopData("QUILLSHROOM", 100, 500),
+ "Jumping Quillshroom Figurine": ShopData("JUMPING_QUILLSHROOM", 100, 500),
+ "Scurubu Figurine": ShopData("SCURUBU", 100, 500),
+ "Jumping Scurubu Figurine": ShopData("JUMPING_SCURUBU", 100, 500),
+ "Wallaxer Figurine": ShopData("WALLAXER", 100, 500),
+ "Barmath'azel Figurine": ShopData("BARMATHAZEL", 600, 2000),
+ "Queen of Quills Figurine": ShopData("QUEEN_OF_QUILLS", 400, 1000),
+ "Demon Hive Figurine": ShopData("DEMON_HIVE", 100, 500),
+}
+
+
+def shuffle_shop_prices(world: MessengerWorld) -> Tuple[Dict[str, int], Dict[str, int]]:
+ shop_price_mod = world.options.shop_price.value
+ shop_price_planned = world.options.shop_price_plan
+
+ shop_prices: Dict[str, int] = {}
+ figurine_prices: Dict[str, int] = {}
+ for item, price in shop_price_planned.value.items():
+ if not isinstance(price, int):
+ price = world.random.choices(list(price.keys()), weights=list(price.values()))[0]
+ if "Figurine" in item:
+ figurine_prices[item] = price
+ else:
+ shop_prices[item] = price
+
+ remaining_slots = [item for item in [*SHOP_ITEMS, *FIGURINES] if item not in shop_price_planned.value]
+ for shop_item in remaining_slots:
+ shop_data = SHOP_ITEMS.get(shop_item, FIGURINES.get(shop_item))
+ price = world.random.randint(shop_data.min_price, shop_data.max_price)
+ adjusted_price = min(int(price * shop_price_mod / 100), 5000)
+ if "Figurine" in shop_item:
+ figurine_prices[shop_item] = adjusted_price
+ else:
+ shop_prices[shop_item] = adjusted_price
+
+ return shop_prices, figurine_prices
diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py
new file mode 100644
index 000000000000..b60aeb179feb
--- /dev/null
+++ b/worlds/messenger/subclasses.py
@@ -0,0 +1,86 @@
+from functools import cached_property
+from typing import Optional, TYPE_CHECKING
+
+from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region
+from .regions import LOCATIONS, MEGA_SHARDS
+from .shop import FIGURINES, SHOP_ITEMS
+
+if TYPE_CHECKING:
+ from . import MessengerWorld
+
+
+class MessengerEntrance(Entrance):
+ world: Optional["MessengerWorld"] = None
+
+
+class MessengerRegion(Region):
+ parent: str
+ entrance_type = MessengerEntrance
+
+ def __init__(self, name: str, world: "MessengerWorld", parent: Optional[str] = None) -> None:
+ super().__init__(name, world.player, world.multiworld)
+ self.parent = parent
+ locations = []
+ if name in LOCATIONS:
+ locations = [loc for loc in LOCATIONS[name]]
+ # portal event locations since portals can be opened from their exit regions
+ if name.endswith("Portal"):
+ locations.append(name.replace(" -", ""))
+
+ if name == "The Shop":
+ shop_locations = {f"The Shop - {shop_loc}": world.location_name_to_id[f"The Shop - {shop_loc}"]
+ for shop_loc in SHOP_ITEMS}
+ self.add_locations(shop_locations, MessengerShopLocation)
+ elif name == "The Craftsman's Corner":
+ self.add_locations({figurine: world.location_name_to_id[figurine] for figurine in FIGURINES},
+ MessengerLocation)
+ elif name == "Tower HQ":
+ locations.append("Money Wrench")
+
+ if world.options.shuffle_shards and name in MEGA_SHARDS:
+ locations += MEGA_SHARDS[name]
+ loc_dict = {loc: world.location_name_to_id.get(loc, None) for loc in locations}
+ self.add_locations(loc_dict, MessengerLocation)
+
+ self.multiworld.regions.append(self)
+
+
+class MessengerLocation(Location):
+ game = "The Messenger"
+
+ def __init__(self, player: int, name: str, loc_id: Optional[int], parent: MessengerRegion) -> None:
+ super().__init__(player, name, loc_id, parent)
+ if loc_id is None:
+ if name == "Rescue Phantom":
+ name = "Do the Thing!"
+ self.place_locked_item(MessengerItem(name, ItemClassification.progression, None, parent.player))
+
+
+class MessengerShopLocation(MessengerLocation):
+ @cached_property
+ def cost(self) -> int:
+ name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped
+ world = self.parent_region.multiworld.worlds[self.player]
+ shop_data = SHOP_ITEMS[name]
+ if shop_data.prerequisite:
+ prereq_cost = 0
+ if isinstance(shop_data.prerequisite, set):
+ for prereq in shop_data.prerequisite:
+ loc = world.multiworld.get_location(prereq, self.player)
+ assert isinstance(loc, MessengerShopLocation)
+ prereq_cost += loc.cost
+ else:
+ loc = world.multiworld.get_location(shop_data.prerequisite, self.player)
+ assert isinstance(loc, MessengerShopLocation)
+ prereq_cost += loc.cost
+ return world.shop_prices[name] + prereq_cost
+ return world.shop_prices[name]
+
+ def access_rule(self, state: CollectionState) -> bool:
+ world = state.multiworld.worlds[self.player]
+ can_afford = state.has("Shards", self.player, min(self.cost, world.total_shards))
+ return can_afford
+
+
+class MessengerItem(Item):
+ game = "The Messenger"
diff --git a/worlds/messenger/test/TestAccess.py b/worlds/messenger/test/TestAccess.py
deleted file mode 100644
index 452ed1189f44..000000000000
--- a/worlds/messenger/test/TestAccess.py
+++ /dev/null
@@ -1,170 +0,0 @@
-from . import MessengerTestBase
-from ..Constants import NOTES, PHOBEKINS
-
-
-class AccessTest(MessengerTestBase):
- options = {
- "shuffle_shards": "true",
- }
-
- def testTabi(self) -> None:
- """locations that hard require the Lightfoot Tabi"""
- locations = [
- "Searing Crags - Pyro", "Underworld - Key of Chaos", "Underworld Seal - Sharp and Windy Climb",
- "Underworld Seal - Spike Wall", "Underworld Seal - Fireball Wave", "Underworld Seal - Rising Fanta",
- "Sunken Shrine - Sun Crest", "Sunken Shrine - Moon Crest", "Sunken Shrine Seal - Waterfall Paradise",
- "Sunken Shrine Seal - Tabi Gauntlet", "Mega Shard of the Moon", "Mega Shard of the Sun",
- "Under Entrance Mega Shard", "Hot Tub Mega Shard", "Projectile Pit Mega Shard"
- ]
- items = [["Lightfoot Tabi"]]
- self.assertAccessDependency(locations, items)
-
- def testDart(self) -> None:
- """locations that hard require the Rope Dart"""
- locations = [
- "Ninja Village Seal - Tree House", "Autumn Hills - Key of Hope", "Howling Grotto Seal - Crushing Pits",
- "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster", "Tower of Time Seal - Lantern Climb",
- "Tower of Time Seal - Arcane Orbs", "Cloud Ruins Seal - Ghost Pit", "Underworld Seal - Rising Fanta",
- "Elemental Skylands - Key of Symbiosis", "Elemental Skylands Seal - Water",
- "Elemental Skylands Seal - Fire", "Earth Mega Shard", "Water Mega Shard", "Rescue Phantom",
- ]
- items = [["Rope Dart"]]
- self.assertAccessDependency(locations, items)
-
- def testWingsuit(self) -> None:
- """locations that hard require the Wingsuit"""
- locations = [
- "Ninja Village - Candle", "Ninja Village Seal - Tree House", "Autumn Hills - Climbing Claws",
- "Autumn Hills - Key of Hope", "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
- "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", "Catacombs - Necro",
- "Catacombs - Ruxxtin's Amulet", "Catacombs Seal - Triple Spike Crushers",
- "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond", "Bamboo Creek - Claustro",
- "Cloud Ruins - Acro", "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
- "Bamboo Creek Seal - Spike Crushers and Doors v2", "Howling Grotto Seal - Crushing Pits",
- "Howling Grotto Seal - Windy Saws and Balls", "Tower of Time Seal - Lantern Climb",
- "Forlorn Temple - Demon King", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
- "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", "Tower of Time Seal - Lantern Climb",
- "Tower of Time Seal - Arcane Orbs", "Underworld Seal - Sharp and Windy Climb",
- "Underworld Seal - Fireball Wave", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water",
- "Elemental Skylands Seal - Fire", "Elemental Skylands - Key of Symbiosis",
- "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset", "Ninja Village - Astral Seed",
- "Searing Crags - Astral Tea Leaves", "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard",
- "Sunny Day Mega Shard", "Down Under Mega Shard", "Catacombs Mega Shard", "Above Entrance Mega Shard",
- "Abandoned Mega Shard", "Time Loop Mega Shard", "Earth Mega Shard", "Water Mega Shard",
- "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2",
- "Autumn Hills - Leaf Golem", "Catacombs - Ruxxtin", "Howling Grotto - Emerald Golem"
- ]
- items = [["Wingsuit"]]
- self.assertAccessDependency(locations, items)
-
- def testVertical(self) -> None:
- """locations that require either the Rope Dart or the Wingsuit"""
- locations = [
- "Ninja Village Seal - Tree House", "Howling Grotto Seal - Crushing Pits",
- "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster",
- "Underworld Seal - Rising Fanta", "Elemental Skylands - Key of Symbiosis",
- "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "Ninja Village - Candle",
- "Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills Seal - Trip Saws",
- "Autumn Hills Seal - Double Swing Saws", "Autumn Hills Seal - Spike Ball Swing",
- "Autumn Hills Seal - Spike Ball Darts", "Catacombs - Necro", "Catacombs - Ruxxtin's Amulet",
- "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet",
- "Catacombs Seal - Dirty Pond", "Bamboo Creek - Claustro", "Cloud Ruins - Acro",
- "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
- "Bamboo Creek Seal - Spike Crushers and Doors v2", "Howling Grotto Seal - Crushing Pits",
- "Howling Grotto Seal - Windy Saws and Balls", "Forlorn Temple - Demon King", "Cloud Ruins Seal - Ghost Pit",
- "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
- "Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs",
- "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave",
- "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
- "Searing Crags - Power Thistle", "Searing Crags - Key of Strength",
- "Glacial Peak Seal - Projectile Spike Pit", "Glacial Peak Seal - Glacial Air Swag",
- "Riviere Turquoise - Butterfly Matriarch", "Riviere Turquoise Seal - Flower Power",
- "Riviere Turquoise Seal - Launch of Faith",
- "Searing Crags Seal - Triple Ball Spinner", "Searing Crags Seal - Raining Rocks",
- "Searing Crags Seal - Rhythm Rocks", "Ninja Village - Astral Seed", "Searing Crags - Astral Tea Leaves",
- "Rescue Phantom", "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard", "Sunny Day Mega Shard",
- "Down Under Mega Shard", "Catacombs Mega Shard", "Above Entrance Mega Shard", "Abandoned Mega Shard",
- "Time Loop Mega Shard", "Searing Crags Mega Shard", "Glacial Peak Mega Shard", "Cloud Entrance Mega Shard",
- "Time Warp Mega Shard", "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2",
- "Quick Restock Mega Shard 1", "Quick Restock Mega Shard 2", "Earth Mega Shard", "Water Mega Shard",
- "Autumn Hills - Leaf Golem", "Catacombs - Ruxxtin", "Howling Grotto - Emerald Golem"
- ]
- items = [["Wingsuit", "Rope Dart"]]
- self.assertAccessDependency(locations, items)
-
- def testAmulet(self) -> None:
- """Locations that require Ruxxtin's Amulet"""
- locations = [
- "Cloud Ruins - Acro", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
- "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", "Cloud Entrance Mega Shard",
- "Time Warp Mega Shard", "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2"
- ]
- # Cloud Ruins requires Ruxxtin's Amulet
- items = [["Ruxxtin's Amulet"]]
- self.assertAccessDependency(locations, items)
-
- def testFirefly(self) -> None:
- """Elemental Skylands and Corrupted Future require the Magic Firefly"""
- locations = [
- "Elemental Skylands - Key of Symbiosis", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Fire",
- "Elemental Skylands Seal - Water", "Corrupted Future - Key of Courage", "Earth Mega Shard",
- "Water Mega Shard"
- ]
- items = [["Magic Firefly"]]
- self.assertAccessDependency(locations, items)
-
- def testCrests(self) -> None:
- """Test Key of Love nonsense"""
- locations = ["Sunken Shrine - Key of Love"]
- items = [["Sun Crest", "Moon Crest"]]
- self.assertAccessDependency(locations, items)
- self.collect_all_but("Sun Crest")
- self.assertEqual(self.can_reach_location("Sunken Shrine - Key of Love"), False)
- self.remove(self.get_item_by_name("Moon Crest"))
- self.collect_by_name("Sun Crest")
- self.assertEqual(self.can_reach_location("Sunken Shrine - Key of Love"), False)
-
- def testThistle(self) -> None:
- """I'm a chuckster!"""
- locations = ["Searing Crags - Key of Strength"]
- items = [["Power Thistle"]]
- self.assertAccessDependency(locations, items)
-
- def testCrown(self) -> None:
- """Crocomire but not"""
- locations = ["Corrupted Future - Key of Courage"]
- items = [["Demon King Crown"]]
- self.assertAccessDependency(locations, items)
-
- def testGoal(self) -> None:
- """Test some different states to verify goal requires the correct items"""
- self.collect_all_but([*NOTES, "Rescue Phantom"])
- self.assertEqual(self.can_reach_location("Rescue Phantom"), False)
- self.collect_all_but(["Key of Love", "Rescue Phantom"])
- self.assertBeatable(False)
- self.collect_by_name(["Key of Love"])
- self.assertEqual(self.can_reach_location("Rescue Phantom"), True)
- self.assertBeatable(True)
-
-
-class ItemsAccessTest(MessengerTestBase):
- options = {
- "shuffle_seals": "false",
- "accessibility": "items",
- }
-
- def testSelfLockingItems(self) -> None:
- """Force items that can be self locked to ensure it's valid placement."""
- location_lock_pairs = {
- "Searing Crags - Key of Strength": ["Power Thistle"],
- "Sunken Shrine - Key of Love": ["Sun Crest", "Moon Crest"],
- "Corrupted Future - Key of Courage": ["Demon King Crown"],
- "Cloud Ruins - Acro": ["Ruxxtin's Amulet"],
- "Forlorn Temple - Demon King": PHOBEKINS
- }
-
- for loc in location_lock_pairs:
- for item_name in location_lock_pairs[loc]:
- item = self.get_item_by_name(item_name)
- with self.subTest("Fulfills Accessibility", location=loc, item=item_name):
- self.assertTrue(self.multiworld.get_location(loc, self.player).can_fill(self.multiworld.state, item, True))
diff --git a/worlds/messenger/test/TestLocations.py b/worlds/messenger/test/TestLocations.py
deleted file mode 100644
index ccb358568ccf..000000000000
--- a/worlds/messenger/test/TestLocations.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from . import MessengerTestBase
-from ..SubClasses import MessengerLocation
-
-
-class LocationsTest(MessengerTestBase):
- options = {
- "shuffle_shards": "true",
- }
-
- @property
- def run_default_tests(self) -> bool:
- return False
-
- def testLocationsExist(self):
- for location in self.multiworld.worlds[1].location_name_to_id:
- self.assertIsInstance(self.multiworld.get_location(location, self.player), MessengerLocation)
diff --git a/worlds/messenger/test/TestLogic.py b/worlds/messenger/test/TestLogic.py
deleted file mode 100644
index 45b0d0dab629..000000000000
--- a/worlds/messenger/test/TestLogic.py
+++ /dev/null
@@ -1,96 +0,0 @@
-from BaseClasses import ItemClassification
-from . import MessengerTestBase
-
-
-class HardLogicTest(MessengerTestBase):
- options = {
- "logic_level": "hard",
- }
-
- def testVertical(self) -> None:
- """Test the locations that still require wingsuit or rope dart."""
- locations = [
- # tower of time
- "Tower of Time Seal - Time Waster", "Tower of Time Seal - Lantern Climb",
- "Tower of Time Seal - Arcane Orbs",
- # ninja village
- "Ninja Village - Candle", "Ninja Village - Astral Seed", "Ninja Village Seal - Tree House",
- # autumn hills
- "Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills - Leaf Golem",
- "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
- "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts",
- # forlorn temple
- "Forlorn Temple - Demon King",
- "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
- # catacombs
- "Catacombs - Necro", "Catacombs - Ruxxtin's Amulet", "Catacombs - Ruxxtin",
- "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond",
- # bamboo creek
- "Bamboo Creek - Claustro",
- "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
- "Bamboo Creek Seal - Spike Crushers and Doors v2",
- # howling grotto
- "Howling Grotto - Emerald Golem", "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Crushing Pits",
- # searing crags
- "Searing Crags - Astral Tea Leaves",
- # cloud ruins
- "Cloud Ruins - Acro", "Cloud Ruins Seal - Ghost Pit",
- "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
- # underworld
- "Underworld Seal - Rising Fanta", "Underworld Seal - Sharp and Windy Climb",
- # elemental skylands
- "Elemental Skylands Seal - Air",
- # phantom
- "Rescue Phantom",
- ]
- items = [["Wingsuit", "Rope Dart"]]
- self.assertAccessDependency(locations, items)
-
- def testWindmill(self) -> None:
- """Windmill Shuriken isn't progression on normal difficulty, so test it's marked correctly and required."""
- self.assertEqual(ItemClassification.progression, self.get_item_by_name("Windmill Shuriken").classification)
- windmill_locs = [
- "Searing Crags - Key of Strength",
- "Elemental Skylands - Key of Symbiosis",
- "Underworld Seal - Fireball Wave",
- ]
- for loc in windmill_locs:
- with self.subTest("can't reach location with nothing", location=loc):
- self.assertFalse(self.can_reach_location(loc))
-
- items = self.get_items_by_name(["Windmill Shuriken", "Lightfoot Tabi", "Magic Firefly"])
- self.collect(items)
- for loc in windmill_locs:
- with self.subTest("can reach with Windmill", location=loc):
- self.assertTrue(self.can_reach_location(loc))
-
- special_loc = "Autumn Hills Seal - Spike Ball Darts"
- item = self.get_item_by_name("Wingsuit")
- self.collect(item)
- self.assertTrue(self.can_reach_location(special_loc))
- self.remove(item)
-
- item = self.get_item_by_name("Rope Dart")
- self.collect(item)
- self.assertTrue(self.can_reach_location(special_loc))
-
-
-class NoLogicTest(MessengerTestBase):
- options = {
- "logic_level": "oob",
- }
-
- def testAccess(self) -> None:
- """Test the locations with rules still require things."""
- all_locations = [
- "Bamboo Creek - Claustro", "Searing Crags - Key of Strength", "Elemental Skylands - Key of Symbiosis",
- "Sunken Shrine - Key of Love", "Searing Crags - Pyro", "Underworld - Key of Chaos",
- "Corrupted Future - Key of Courage", "Autumn Hills Seal - Spike Ball Darts",
- "Ninja Village Seal - Tree House", "Underworld Seal - Fireball Wave", "Tower of Time Seal - Time Waster",
- "Rescue Phantom", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water",
- "Elemental Skylands Seal - Fire",
- ]
- for loc in all_locations:
- with self.subTest("Default unreachables", location=loc):
- self.assertFalse(self.can_reach_location(loc))
- self.assertBeatable(True)
diff --git a/worlds/messenger/test/TestNotes.py b/worlds/messenger/test/TestNotes.py
deleted file mode 100644
index c4292e4900d8..000000000000
--- a/worlds/messenger/test/TestNotes.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from . import MessengerTestBase
-from ..Constants import NOTES
-
-
-class TwoNoteGoalTest(MessengerTestBase):
- options = {
- "notes_needed": 2,
- }
-
- def testPrecollectedNotes(self) -> None:
- self.assertEqual(self.multiworld.state.count_group("Notes", self.player), 4)
-
-
-class FourNoteGoalTest(MessengerTestBase):
- options = {
- "notes_needed": 4,
- }
-
- def testPrecollectedNotes(self) -> None:
- self.assertEqual(self.multiworld.state.count_group("Notes", self.player), 2)
-
-
-class DefaultGoalTest(MessengerTestBase):
- def testPrecollectedNotes(self) -> None:
- self.assertEqual(self.multiworld.state.count_group("Notes", self.player), 0)
-
- def testGoal(self) -> None:
- self.assertBeatable(False)
- self.collect_by_name(NOTES)
- rope_dart = self.get_item_by_name("Rope Dart")
- self.collect(rope_dart)
- self.assertBeatable(True)
- self.remove(rope_dart)
- self.collect_by_name("Wingsuit")
- self.assertBeatable(True)
diff --git a/worlds/messenger/test/TestShop.py b/worlds/messenger/test/TestShop.py
deleted file mode 100644
index 89ea93624ddd..000000000000
--- a/worlds/messenger/test/TestShop.py
+++ /dev/null
@@ -1,111 +0,0 @@
-from typing import Dict
-
-from . import MessengerTestBase
-from ..Shop import SHOP_ITEMS, FIGURINES
-
-
-class ShopCostTest(MessengerTestBase):
- options = {
- "shop_price": "random",
- "shuffle_shards": "true",
- }
-
- def testShopRules(self) -> None:
- for loc in SHOP_ITEMS:
- loc = f"The Shop - {loc}"
- with self.subTest("has cost", loc=loc):
- self.assertFalse(self.can_reach_location(loc))
-
- def testShopPrices(self) -> None:
- prices: Dict[str, int] = self.multiworld.worlds[self.player].shop_prices
- for loc, price in prices.items():
- with self.subTest("prices", loc=loc):
- self.assertLessEqual(price, self.multiworld.get_location(f"The Shop - {loc}", self.player).cost)
- self.assertTrue(loc in SHOP_ITEMS)
- self.assertEqual(len(prices), len(SHOP_ITEMS))
-
- def testDBoost(self) -> None:
- locations = [
- "Riviere Turquoise Seal - Bounces and Balls",
- "Forlorn Temple - Demon King", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
- "Sunny Day Mega Shard", "Down Under Mega Shard",
- ]
- items = [["Path of Resilience", "Meditation", "Second Wind"]]
- self.assertAccessDependency(locations, items)
-
- def testCurrents(self) -> None:
- self.assertAccessDependency(["Elemental Skylands Seal - Water"], [["Currents Master"]])
-
- def testStrike(self) -> None:
- locations = [
- "Glacial Peak Seal - Projectile Spike Pit", "Elemental Skylands Seal - Fire",
- ]
- items = [["Strike of the Ninja"]]
- self.assertAccessDependency(locations, items)
-
-
-class ShopCostMinTest(ShopCostTest):
- options = {
- "shop_price": "random",
- "shuffle_seals": "false",
- }
-
- def testShopRules(self) -> None:
- if self.multiworld.worlds[self.player].total_shards:
- super().testShopRules()
- else:
- for loc in SHOP_ITEMS:
- loc = f"The Shop - {loc}"
- with self.subTest("has cost", loc=loc):
- self.assertTrue(self.can_reach_location(loc))
-
- def testDBoost(self) -> None:
- pass
-
- def testCurrents(self) -> None:
- pass
-
- def testStrike(self) -> None:
- pass
-
-
-class PlandoTest(MessengerTestBase):
- options = {
- "shop_price_plan": {
- "Karuta Plates": 50,
- "Serendipitous Bodies": {100: 1, 200: 1, 300: 1},
- "Barmath'azel Figurine": 500,
- "Demon Hive Figurine": {100: 1, 200: 2, 300: 1},
- },
- }
-
- def testCosts(self) -> None:
- for loc in SHOP_ITEMS:
- loc = f"The Shop - {loc}"
- with self.subTest("has cost", loc=loc):
- self.assertFalse(self.can_reach_location(loc))
-
- prices = self.multiworld.worlds[self.player].shop_prices
- for loc, price in prices.items():
- with self.subTest("prices", loc=loc):
- if loc == "Karuta Plates":
- self.assertEqual(self.options["shop_price_plan"]["Karuta Plates"], price)
- elif loc == "Serendipitous Bodies":
- self.assertIn(price, self.options["shop_price_plan"]["Serendipitous Bodies"])
-
- loc = f"The Shop - {loc}"
- self.assertLessEqual(price, self.multiworld.get_location(loc, self.player).cost)
- self.assertTrue(loc.replace("The Shop - ", "") in SHOP_ITEMS)
- self.assertEqual(len(prices), len(SHOP_ITEMS))
-
- figures = self.multiworld.worlds[self.player].figurine_prices
- for loc, price in figures.items():
- with self.subTest("figure prices", loc=loc):
- if loc == "Barmath'azel Figurine":
- self.assertEqual(self.options["shop_price_plan"]["Barmath'azel Figurine"], price)
- elif loc == "Demon Hive Figurine":
- self.assertIn(price, self.options["shop_price_plan"]["Demon Hive Figurine"])
-
- self.assertLessEqual(price, self.multiworld.get_location(loc, self.player).cost)
- self.assertTrue(loc in FIGURINES)
- self.assertEqual(len(figures), len(FIGURINES))
diff --git a/worlds/messenger/test/TestShopChest.py b/worlds/messenger/test/TestShopChest.py
deleted file mode 100644
index ad4178fbd718..000000000000
--- a/worlds/messenger/test/TestShopChest.py
+++ /dev/null
@@ -1,100 +0,0 @@
-from BaseClasses import ItemClassification, CollectionState
-from . import MessengerTestBase
-
-
-class AllSealsRequired(MessengerTestBase):
- options = {
- "shuffle_seals": "false",
- "goal": "power_seal_hunt",
- }
-
- def testSealsShuffled(self) -> None:
- """Shuffle seals should be forced on when shop chest is the goal so test it."""
- self.assertTrue(self.multiworld.shuffle_seals[self.player])
-
- def testChestAccess(self) -> None:
- """Defaults to a total of 45 power seals in the pool and required."""
- with self.subTest("Access Dependency"):
- self.assertEqual(len([seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]),
- self.multiworld.total_seals[self.player])
- locations = ["Shop Chest"]
- items = [["Power Seal"]]
- self.assertAccessDependency(locations, items)
- self.multiworld.state = CollectionState(self.multiworld)
-
- self.assertEqual(self.can_reach_location("Shop Chest"), False)
- self.assertBeatable(False)
- self.collect_all_but(["Power Seal", "Shop Chest", "Rescue Phantom"])
- self.assertEqual(self.can_reach_location("Shop Chest"), False)
- self.assertBeatable(False)
- self.collect_by_name("Power Seal")
- self.assertEqual(self.can_reach_location("Shop Chest"), True)
- self.assertBeatable(True)
-
-
-class HalfSealsRequired(MessengerTestBase):
- options = {
- "goal": "power_seal_hunt",
- "percent_seals_required": 50,
- }
-
- def testSealsAmount(self) -> None:
- """Should have 45 power seals in the item pool and half that required"""
- self.assertEqual(self.multiworld.total_seals[self.player], 45)
- self.assertEqual(self.multiworld.worlds[self.player].total_seals, 45)
- self.assertEqual(self.multiworld.worlds[self.player].required_seals, 22)
- total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]
- required_seals = [seal for seal in total_seals
- if seal.classification == ItemClassification.progression_skip_balancing]
- self.assertEqual(len(total_seals), 45)
- self.assertEqual(len(required_seals), 22)
-
-
-class ThirtyThirtySeals(MessengerTestBase):
- options = {
- "goal": "power_seal_hunt",
- "total_seals": 30,
- "percent_seals_required": 34,
- }
-
- def testSealsAmount(self) -> None:
- """Should have 30 power seals in the pool and 33 percent of that required."""
- self.assertEqual(self.multiworld.total_seals[self.player], 30)
- self.assertEqual(self.multiworld.worlds[self.player].total_seals, 30)
- self.assertEqual(self.multiworld.worlds[self.player].required_seals, 10)
- total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]
- required_seals = [seal for seal in total_seals
- if seal.classification == ItemClassification.progression_skip_balancing]
- self.assertEqual(len(total_seals), 30)
- self.assertEqual(len(required_seals), 10)
-
-
-class MaxSealsNoShards(MessengerTestBase):
- options = {
- "goal": "power_seal_hunt",
- "total_seals": 85,
- }
-
- def testSealsAmount(self) -> None:
- """Should set total seals to 70 since shards aren't shuffled."""
- self.assertEqual(self.multiworld.total_seals[self.player], 85)
- self.assertEqual(self.multiworld.worlds[self.player].total_seals, 70)
-
-
-class MaxSealsWithShards(MessengerTestBase):
- options = {
- "goal": "power_seal_hunt",
- "total_seals": 85,
- "shuffle_shards": "true",
- }
-
- def testSealsAmount(self) -> None:
- """Should have 85 seals in the pool with all required and be a valid seed."""
- self.assertEqual(self.multiworld.total_seals[self.player], 85)
- self.assertEqual(self.multiworld.worlds[self.player].total_seals, 85)
- self.assertEqual(self.multiworld.worlds[self.player].required_seals, 85)
- total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]
- required_seals = [seal for seal in total_seals
- if seal.classification == ItemClassification.progression_skip_balancing]
- self.assertEqual(len(total_seals), 85)
- self.assertEqual(len(required_seals), 85)
diff --git a/worlds/messenger/test/__init__.py b/worlds/messenger/test/__init__.py
index 7ab1e11781da..83bb248d6483 100644
--- a/worlds/messenger/test/__init__.py
+++ b/worlds/messenger/test/__init__.py
@@ -1,6 +1,7 @@
-from test.TestBase import WorldTestBase
+from test.bases import WorldTestBase
+from .. import MessengerWorld
class MessengerTestBase(WorldTestBase):
game = "The Messenger"
- player: int = 1
+ world: MessengerWorld
diff --git a/worlds/messenger/test/test_access.py b/worlds/messenger/test/test_access.py
new file mode 100644
index 000000000000..ad2265ffa0a6
--- /dev/null
+++ b/worlds/messenger/test/test_access.py
@@ -0,0 +1,217 @@
+import typing
+
+from . import MessengerTestBase
+from ..constants import NOTES, PHOBEKINS
+
+
+class AccessTest(MessengerTestBase):
+ options = {
+ "shuffle_shards": "true",
+ }
+
+ def test_tabi(self) -> None:
+ """locations that hard require the Lightfoot Tabi"""
+ locations = [
+ "Searing Crags - Pyro", "Underworld - Key of Chaos", "Underworld Seal - Sharp and Windy Climb",
+ "Underworld Seal - Spike Wall", "Underworld Seal - Fireball Wave", "Underworld Seal - Rising Fanta",
+ "Sunken Shrine - Sun Crest", "Sunken Shrine - Moon Crest", "Sunken Shrine Seal - Waterfall Paradise",
+ "Sunken Shrine Seal - Tabi Gauntlet", "Mega Shard of the Moon", "Mega Shard of the Sun",
+ "Under Entrance Mega Shard", "Hot Tub Mega Shard", "Projectile Pit Mega Shard"
+ ]
+ items = [["Lightfoot Tabi"]]
+ self.assertAccessDependency(locations, items)
+
+ def test_dart(self) -> None:
+ """locations that hard require the Rope Dart"""
+ locations = [
+ "Ninja Village Seal - Tree House",
+ "Autumn Hills - Key of Hope",
+ "Forlorn Temple - Demon King",
+ "Down Under Mega Shard",
+ "Howling Grotto Seal - Crushing Pits",
+ "Glacial Peak Seal - Ice Climbers",
+ "Tower of Time Seal - Time Waster",
+ "Tower of Time Seal - Lantern Climb",
+ "Tower of Time Seal - Arcane Orbs",
+ "Cloud Ruins Seal - Ghost Pit",
+ "Cloud Ruins Seal - Money Farm Room",
+ "Cloud Ruins Seal - Toothbrush Alley",
+ "Money Farm Room Mega Shard 1",
+ "Money Farm Room Mega Shard 2",
+ "Underworld Seal - Rising Fanta",
+ "Elemental Skylands - Key of Symbiosis",
+ "Elemental Skylands Seal - Water",
+ "Elemental Skylands Seal - Fire",
+ "Earth Mega Shard",
+ "Water Mega Shard",
+ "Rescue Phantom",
+ ]
+ items = [["Rope Dart"]]
+ self.assertAccessDependency(locations, items)
+
+ def test_wingsuit(self) -> None:
+ """locations that hard require the Wingsuit"""
+ locations = [
+ "Ninja Village - Candle", "Ninja Village Seal - Tree House", "Autumn Hills - Climbing Claws",
+ "Autumn Hills - Key of Hope", "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
+ "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", "Catacombs - Necro",
+ "Catacombs - Ruxxtin's Amulet", "Catacombs Seal - Triple Spike Crushers",
+ "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond", "Bamboo Creek - Claustro",
+ "Cloud Ruins - Acro", "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
+ "Bamboo Creek Seal - Spike Crushers and Doors v2", "Howling Grotto Seal - Crushing Pits",
+ "Howling Grotto Seal - Windy Saws and Balls", "Tower of Time Seal - Lantern Climb",
+ "Forlorn Temple - Demon King", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
+ "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", "Tower of Time Seal - Lantern Climb",
+ "Tower of Time Seal - Arcane Orbs", "Underworld Seal - Sharp and Windy Climb",
+ "Underworld Seal - Fireball Wave", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water",
+ "Elemental Skylands Seal - Fire", "Elemental Skylands - Key of Symbiosis",
+ "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset", "Ninja Village - Astral Seed",
+ "Searing Crags - Astral Tea Leaves", "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard",
+ "Sunny Day Mega Shard", "Down Under Mega Shard", "Catacombs Mega Shard", "Above Entrance Mega Shard",
+ "Abandoned Mega Shard", "Time Loop Mega Shard", "Earth Mega Shard", "Water Mega Shard",
+ "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2",
+ "Autumn Hills - Leaf Golem", "Catacombs - Ruxxtin", "Howling Grotto - Emerald Golem"
+ ]
+ items = [["Wingsuit"]]
+ self.assertAccessDependency(locations, items)
+
+ def test_vertical(self) -> None:
+ """locations that require either the Rope Dart or the Wingsuit"""
+ locations = [
+ "Ninja Village Seal - Tree House", "Howling Grotto Seal - Crushing Pits",
+ "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster",
+ "Underworld Seal - Rising Fanta", "Elemental Skylands - Key of Symbiosis",
+ "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "Ninja Village - Candle",
+ "Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills Seal - Trip Saws",
+ "Autumn Hills Seal - Double Swing Saws", "Autumn Hills Seal - Spike Ball Swing",
+ "Autumn Hills Seal - Spike Ball Darts", "Catacombs - Necro", "Catacombs - Ruxxtin's Amulet",
+ "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet",
+ "Catacombs Seal - Dirty Pond", "Bamboo Creek - Claustro", "Cloud Ruins - Acro",
+ "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
+ "Bamboo Creek Seal - Spike Crushers and Doors v2", "Howling Grotto Seal - Crushing Pits",
+ "Howling Grotto Seal - Windy Saws and Balls", "Forlorn Temple - Demon King", "Cloud Ruins Seal - Ghost Pit",
+ "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
+ "Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs",
+ "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave",
+ "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
+ "Searing Crags - Power Thistle", "Searing Crags - Key of Strength",
+ "Glacial Peak Seal - Projectile Spike Pit", "Glacial Peak Seal - Glacial Air Swag",
+ "Riviere Turquoise - Butterfly Matriarch", "Riviere Turquoise Seal - Flower Power",
+ "Riviere Turquoise Seal - Launch of Faith",
+ "Searing Crags Seal - Triple Ball Spinner", "Searing Crags Seal - Raining Rocks",
+ "Searing Crags Seal - Rhythm Rocks", "Ninja Village - Astral Seed", "Searing Crags - Astral Tea Leaves",
+ "Rescue Phantom", "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard", "Sunny Day Mega Shard",
+ "Down Under Mega Shard", "Catacombs Mega Shard", "Above Entrance Mega Shard", "Abandoned Mega Shard",
+ "Time Loop Mega Shard", "Searing Crags Mega Shard", "Glacial Peak Mega Shard", "Cloud Entrance Mega Shard",
+ "Time Warp Mega Shard", "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2",
+ "Quick Restock Mega Shard 1", "Quick Restock Mega Shard 2", "Earth Mega Shard", "Water Mega Shard",
+ "Autumn Hills - Leaf Golem", "Catacombs - Ruxxtin", "Howling Grotto - Emerald Golem"
+ ]
+ items = [["Wingsuit", "Rope Dart"]]
+ self.assertAccessDependency(locations, items)
+
+ def test_amulet(self) -> None:
+ """Locations that require Ruxxtin's Amulet"""
+ locations = [
+ "Cloud Ruins - Acro", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
+ "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", "Cloud Entrance Mega Shard",
+ "Time Warp Mega Shard", "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2"
+ ]
+ # Cloud Ruins requires Ruxxtin's Amulet
+ items = [["Ruxxtin's Amulet"]]
+ self.assertAccessDependency(locations, items)
+
+ def test_firefly(self) -> None:
+ """Elemental Skylands and Corrupted Future require the Magic Firefly"""
+ locations = [
+ "Elemental Skylands - Key of Symbiosis", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Fire",
+ "Elemental Skylands Seal - Water", "Corrupted Future - Key of Courage", "Earth Mega Shard",
+ "Water Mega Shard"
+ ]
+ items = [["Magic Firefly"]]
+ self.assertAccessDependency(locations, items)
+
+ def test_crests(self) -> None:
+ """Test Key of Love nonsense"""
+ locations = ["Sunken Shrine - Key of Love"]
+ items = [["Sun Crest", "Moon Crest"]]
+ self.assertAccessDependency(locations, items)
+ self.collect_all_but("Sun Crest")
+ self.assertEqual(self.can_reach_location("Sunken Shrine - Key of Love"), False)
+ self.remove(self.get_item_by_name("Moon Crest"))
+ self.collect_by_name("Sun Crest")
+ self.assertEqual(self.can_reach_location("Sunken Shrine - Key of Love"), False)
+
+ def test_thistle(self) -> None:
+ """I'm a chuckster!"""
+ locations = ["Searing Crags - Key of Strength"]
+ items = [["Power Thistle"]]
+ self.assertAccessDependency(locations, items)
+
+ def test_crown(self) -> None:
+ """Crocomire but not"""
+ locations = ["Corrupted Future - Key of Courage"]
+ items = [["Demon King Crown"]]
+ self.assertAccessDependency(locations, items)
+
+ def test_dboost(self) -> None:
+ """
+ short for damage boosting, d-boosting is a technique in video games where the player intentionally or
+ unintentionally takes damage and uses the several following frames of invincibility to defeat or get past an
+ enemy or obstacle, most commonly used in platformers such as the Super Mario games
+ """
+ locations = [
+ "Riviere Turquoise Seal - Bounces and Balls", "Searing Crags Seal - Triple Ball Spinner",
+ "Forlorn Temple - Demon King", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
+ "Sunny Day Mega Shard", "Down Under Mega Shard",
+ ]
+ items = [["Path of Resilience", "Meditation", "Second Wind"]]
+ self.assertAccessDependency(locations, items)
+
+ def test_currents(self) -> None:
+ """there's one of these but oh man look at it go"""
+ self.assertAccessDependency(["Elemental Skylands Seal - Water"], [["Currents Master"]])
+
+ def test_strike(self) -> None:
+ """strike is pretty cool but it doesn't block much"""
+ locations = [
+ "Glacial Peak Seal - Projectile Spike Pit", "Elemental Skylands Seal - Fire",
+ ]
+ items = [["Strike of the Ninja"]]
+ self.assertAccessDependency(locations, items)
+
+ def test_goal(self) -> None:
+ """Test some different states to verify goal requires the correct items"""
+ self.collect_all_but([*NOTES, "Do the Thing!"])
+ self.assertEqual(self.can_reach_location("Rescue Phantom"), False)
+ self.collect_all_but(["Key of Love", "Do the Thing!"])
+ self.assertBeatable(False)
+ self.collect_by_name(["Key of Love"])
+ self.assertEqual(self.can_reach_location("Rescue Phantom"), True)
+ self.assertBeatable(True)
+
+
+class ItemsAccessTest(MessengerTestBase):
+ options = {
+ "shuffle_seals": "false",
+ "accessibility": "items",
+ }
+
+ def test_self_locking_items(self) -> None:
+ """Force items that can be self locked to ensure it's valid placement."""
+ location_lock_pairs = {
+ "Searing Crags - Key of Strength": ["Power Thistle"],
+ "Sunken Shrine - Key of Love": ["Sun Crest", "Moon Crest"],
+ "Corrupted Future - Key of Courage": ["Demon King Crown"],
+ }
+
+ self.collect_all_but([item for items in location_lock_pairs.values() for item in items])
+ for loc in location_lock_pairs:
+ for item_name in location_lock_pairs[loc]:
+ item = self.get_item_by_name(item_name)
+ with self.subTest("Fulfills Accessibility", location=loc, item=item_name):
+ location = self.multiworld.get_location(loc, self.player)
+ self.assertTrue(location.can_fill(self.multiworld.state, item, True))
+ location.item = item
+ self.multiworld.state.update_reachable_regions(self.player)
+ self.assertTrue(self.can_reach_location(loc))
diff --git a/worlds/messenger/test/test_locations.py b/worlds/messenger/test/test_locations.py
new file mode 100644
index 000000000000..627d58c29061
--- /dev/null
+++ b/worlds/messenger/test/test_locations.py
@@ -0,0 +1,16 @@
+from . import MessengerTestBase
+from ..subclasses import MessengerLocation
+
+
+class LocationsTest(MessengerTestBase):
+ options = {
+ "shuffle_shards": "true",
+ }
+
+ @property
+ def run_default_tests(self) -> bool:
+ return False
+
+ def test_locations_exist(self) -> None:
+ for location in self.world.location_name_to_id:
+ self.assertIsInstance(self.multiworld.get_location(location, self.player), MessengerLocation)
diff --git a/worlds/messenger/test/test_logic.py b/worlds/messenger/test/test_logic.py
new file mode 100644
index 000000000000..c13bd5c5a008
--- /dev/null
+++ b/worlds/messenger/test/test_logic.py
@@ -0,0 +1,101 @@
+from BaseClasses import ItemClassification
+from . import MessengerTestBase
+
+
+class HardLogicTest(MessengerTestBase):
+ options = {
+ "logic_level": "hard",
+ "shuffle_shards": "true",
+ }
+
+ def test_vertical(self) -> None:
+ """Test the locations that still require wingsuit or rope dart."""
+ locations = [
+ # tower of time
+ "Tower of Time Seal - Time Waster", "Tower of Time Seal - Lantern Climb",
+ "Tower of Time Seal - Arcane Orbs",
+ # ninja village
+ "Ninja Village - Candle", "Ninja Village - Astral Seed", "Ninja Village Seal - Tree House",
+ # autumn hills
+ "Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills - Leaf Golem",
+ "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
+ "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts",
+ "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard",
+ # forlorn temple
+ "Forlorn Temple - Demon King",
+ "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
+ "Sunny Day Mega Shard", "Down Under Mega Shard",
+ # catacombs
+ "Catacombs - Necro", "Catacombs - Ruxxtin's Amulet", "Catacombs - Ruxxtin",
+ "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond",
+ "Catacombs Mega Shard",
+ # bamboo creek
+ "Bamboo Creek - Claustro",
+ "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
+ "Bamboo Creek Seal - Spike Crushers and Doors v2",
+ "Above Entrance Mega Shard", "Abandoned Mega Shard", "Time Loop Mega Shard",
+ # howling grotto
+ "Howling Grotto - Emerald Golem", "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Crushing Pits",
+ # searing crags
+ "Searing Crags - Astral Tea Leaves",
+ # cloud ruins
+ "Cloud Ruins - Acro", "Cloud Ruins Seal - Ghost Pit",
+ "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
+ "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2",
+ # underworld
+ "Underworld Seal - Rising Fanta", "Underworld Seal - Sharp and Windy Climb",
+ # elemental skylands
+ "Elemental Skylands Seal - Air",
+ # phantom
+ "Rescue Phantom",
+ ]
+ items = [["Wingsuit", "Rope Dart"]]
+ self.assertAccessDependency(locations, items)
+
+ def test_windmill(self) -> None:
+ """Windmill Shuriken isn't progression on normal difficulty, so test it's marked correctly and required."""
+ self.assertEqual(ItemClassification.progression, self.get_item_by_name("Windmill Shuriken").classification)
+ windmill_locs = [
+ "Searing Crags - Key of Strength",
+ "Elemental Skylands - Key of Symbiosis",
+ "Underworld Seal - Fireball Wave",
+ ]
+ for loc in windmill_locs:
+ with self.subTest("can't reach location with nothing", location=loc):
+ self.assertFalse(self.can_reach_location(loc))
+
+ items = self.get_items_by_name(["Windmill Shuriken", "Lightfoot Tabi", "Magic Firefly"])
+ self.collect(items)
+ for loc in windmill_locs:
+ with self.subTest("can reach with Windmill", location=loc):
+ self.assertTrue(self.can_reach_location(loc))
+
+ special_loc = "Autumn Hills Seal - Spike Ball Darts"
+ item = self.get_item_by_name("Wingsuit")
+ self.collect(item)
+ self.assertTrue(self.can_reach_location(special_loc))
+ self.remove(item)
+
+ item = self.get_item_by_name("Rope Dart")
+ self.collect(item)
+ self.assertTrue(self.can_reach_location(special_loc))
+
+
+class NoLogicTest(MessengerTestBase):
+ options = {
+ "logic_level": "oob",
+ }
+
+ def test_access(self) -> None:
+ """Test the locations with rules still require things."""
+ all_locations = [
+ "Bamboo Creek - Claustro", "Searing Crags - Key of Strength", "Elemental Skylands - Key of Symbiosis",
+ "Sunken Shrine - Key of Love", "Searing Crags - Pyro", "Underworld - Key of Chaos",
+ "Corrupted Future - Key of Courage", "Autumn Hills Seal - Spike Ball Darts",
+ "Ninja Village Seal - Tree House", "Underworld Seal - Fireball Wave", "Tower of Time Seal - Time Waster",
+ "Rescue Phantom", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water",
+ "Elemental Skylands Seal - Fire",
+ ]
+ for loc in all_locations:
+ with self.subTest("Default unreachables", location=loc):
+ self.assertFalse(self.can_reach_location(loc))
diff --git a/worlds/messenger/test/test_notes.py b/worlds/messenger/test/test_notes.py
new file mode 100644
index 000000000000..fdb1cef1dfbe
--- /dev/null
+++ b/worlds/messenger/test/test_notes.py
@@ -0,0 +1,39 @@
+from . import MessengerTestBase
+from ..constants import NOTES
+
+
+class PrecollectedNotesTestBase(MessengerTestBase):
+ starting_notes: int = 0
+
+ @property
+ def run_default_tests(self) -> bool:
+ return False
+
+ def test_precollected_notes(self) -> None:
+ self.assertEqual(self.multiworld.state.count_group("Notes", self.player), self.starting_notes)
+
+ def test_goal(self) -> None:
+ if self.__class__ is not PrecollectedNotesTestBase:
+ return
+ self.assertBeatable(False)
+ self.collect_by_name(NOTES)
+ rope_dart = self.get_item_by_name("Rope Dart")
+ self.collect(rope_dart)
+ self.assertBeatable(True)
+ self.remove(rope_dart)
+ self.collect_by_name("Wingsuit")
+ self.assertBeatable(True)
+
+
+class TwoNoteGoalTest(PrecollectedNotesTestBase):
+ options = {
+ "notes_needed": 2,
+ }
+ starting_notes = 4
+
+
+class FourNoteGoalTest(PrecollectedNotesTestBase):
+ options = {
+ "notes_needed": 4,
+ }
+ starting_notes = 2
diff --git a/worlds/messenger/test/test_options.py b/worlds/messenger/test/test_options.py
new file mode 100644
index 000000000000..ea84af80388f
--- /dev/null
+++ b/worlds/messenger/test/test_options.py
@@ -0,0 +1,35 @@
+from BaseClasses import CollectionState
+from Fill import distribute_items_restrictive
+from . import MessengerTestBase
+from .. import MessengerWorld
+from ..options import Logic
+
+
+class LimitedMovementTest(MessengerTestBase):
+ options = {
+ "limited_movement": "true",
+ "shuffle_shards": "true",
+ }
+
+ @property
+ def run_default_tests(self) -> bool:
+ # This test base fails reachability tests. Not sure if the core tests should change to support that
+ return False
+
+ def test_options(self) -> None:
+ """Tests that options were correctly changed."""
+ assert isinstance(self.multiworld.worlds[self.player], MessengerWorld)
+ self.assertEqual(Logic.option_hard, self.world.options.logic_level)
+
+
+class EarlyMeditationTest(MessengerTestBase):
+ options = {
+ "early_meditation": "true",
+ }
+
+ def test_option(self) -> None:
+ """Checks that Meditation gets placed early"""
+ distribute_items_restrictive(self.multiworld)
+ sphere1 = self.multiworld.get_reachable_locations(CollectionState(self.multiworld))
+ items = [loc.item.name for loc in sphere1]
+ self.assertIn("Meditation", items)
diff --git a/worlds/messenger/test/test_portals.py b/worlds/messenger/test/test_portals.py
new file mode 100644
index 000000000000..b1875ac0b3ab
--- /dev/null
+++ b/worlds/messenger/test/test_portals.py
@@ -0,0 +1,37 @@
+from BaseClasses import CollectionState
+from . import MessengerTestBase
+from ..portals import PORTALS
+
+
+class PortalTestBase(MessengerTestBase):
+ options = {
+ "available_portals": 3,
+ }
+
+ def test_portal_reqs(self) -> None:
+ """tests the paths to open a portal if only that portal is closed with vanilla connections."""
+ # portal and requirements to reach it if it's the only closed portal
+ portal_requirements = {
+ "Autumn Hills Portal": [["Wingsuit"]], # grotto -> bamboo -> catacombs -> hills
+ "Riviere Turquoise Portal": [["Candle", "Wingsuit", "Rope Dart"]], # hills -> catacombs -> dark cave -> riviere
+ "Howling Grotto Portal": [["Wingsuit"], ["Meditation", "Second Wind"]], # crags -> quillshroom -> grotto
+ "Sunken Shrine Portal": [["Seashell"]], # crags -> quillshroom -> grotto -> shrine
+ "Searing Crags Portal": [["Wingsuit"], ["Rope Dart"]], # grotto -> quillshroom -> crags there's two separate paths
+ "Glacial Peak Portal": [["Wingsuit", "Second Wind", "Meditation"], ["Rope Dart"]], # grotto -> quillshroom -> crags -> peak or crags -> peak
+ }
+
+ for portal in PORTALS:
+ name = f"{portal} Portal"
+ entrance_name = f"ToTHQ {name}"
+ with self.subTest(portal=name, entrance_name=entrance_name):
+ entrance = self.multiworld.get_entrance(entrance_name, self.player)
+ # this emulates the portal being initially closed
+ entrance.access_rule = lambda state: state.has(name, self.player)
+ for grouping in portal_requirements[name]:
+ test_state = CollectionState(self.multiworld)
+ self.assertFalse(entrance.can_reach(test_state), "reachable with nothing")
+ items = self.get_items_by_name(grouping)
+ for item in items:
+ test_state.collect(item)
+ self.assertTrue(entrance.can_reach(test_state), grouping)
+ entrance.access_rule = lambda state: True
diff --git a/worlds/messenger/test/test_shop.py b/worlds/messenger/test/test_shop.py
new file mode 100644
index 000000000000..971ff1763b47
--- /dev/null
+++ b/worlds/messenger/test/test_shop.py
@@ -0,0 +1,91 @@
+from typing import Dict
+
+from . import MessengerTestBase
+from ..shop import SHOP_ITEMS, FIGURINES
+
+
+class ShopCostTest(MessengerTestBase):
+ options = {
+ "shop_price": "random",
+ "shuffle_shards": "true",
+ }
+
+ def test_shop_rules(self) -> None:
+ for loc in SHOP_ITEMS:
+ loc = f"The Shop - {loc}"
+ with self.subTest("has cost", loc=loc):
+ self.assertFalse(self.can_reach_location(loc))
+
+ def test_shop_prices(self) -> None:
+ prices: Dict[str, int] = self.world.shop_prices
+ for loc, price in prices.items():
+ with self.subTest("prices", loc=loc):
+ self.assertLessEqual(price, self.multiworld.get_location(f"The Shop - {loc}", self.player).cost)
+ self.assertTrue(loc in SHOP_ITEMS)
+ self.assertEqual(len(prices), len(SHOP_ITEMS))
+
+
+class ShopCostMinTest(ShopCostTest):
+ options = {
+ "shop_price": "random",
+ "shuffle_seals": "false",
+ }
+
+ def test_shop_rules(self) -> None:
+ if self.world.total_shards:
+ super().test_shop_rules()
+ else:
+ for loc in SHOP_ITEMS:
+ loc = f"The Shop - {loc}"
+ with self.subTest("has cost", loc=loc):
+ self.assertTrue(self.can_reach_location(loc))
+
+ def test_dboost(self) -> None:
+ pass
+
+ def test_currents(self) -> None:
+ pass
+
+ def test_strike(self) -> None:
+ pass
+
+
+class PlandoTest(MessengerTestBase):
+ options = {
+ "shop_price_plan": {
+ "Karuta Plates": 50,
+ "Serendipitous Bodies": {100: 1, 200: 1, 300: 1},
+ "Barmath'azel Figurine": 500,
+ "Demon Hive Figurine": {100: 1, 200: 2, 300: 1},
+ },
+ }
+
+ def test_costs(self) -> None:
+ for loc in SHOP_ITEMS:
+ loc = f"The Shop - {loc}"
+ with self.subTest("has cost", loc=loc):
+ self.assertFalse(self.can_reach_location(loc))
+
+ prices = self.world.shop_prices
+ for loc, price in prices.items():
+ with self.subTest("prices", loc=loc):
+ if loc == "Karuta Plates":
+ self.assertEqual(self.options["shop_price_plan"]["Karuta Plates"], price)
+ elif loc == "Serendipitous Bodies":
+ self.assertIn(price, self.options["shop_price_plan"]["Serendipitous Bodies"])
+
+ loc = f"The Shop - {loc}"
+ self.assertLessEqual(price, self.multiworld.get_location(loc, self.player).cost)
+ self.assertTrue(loc.replace("The Shop - ", "") in SHOP_ITEMS)
+ self.assertEqual(len(prices), len(SHOP_ITEMS))
+
+ figures = self.world.figurine_prices
+ for loc, price in figures.items():
+ with self.subTest("figure prices", loc=loc):
+ if loc == "Barmath'azel Figurine":
+ self.assertEqual(self.options["shop_price_plan"]["Barmath'azel Figurine"], price)
+ elif loc == "Demon Hive Figurine":
+ self.assertIn(price, self.options["shop_price_plan"]["Demon Hive Figurine"])
+
+ self.assertTrue(loc in FIGURINES)
+ self.assertEqual(len(figures), len(FIGURINES))
diff --git a/worlds/messenger/test/test_shop_chest.py b/worlds/messenger/test/test_shop_chest.py
new file mode 100644
index 000000000000..2ac306972614
--- /dev/null
+++ b/worlds/messenger/test/test_shop_chest.py
@@ -0,0 +1,95 @@
+from BaseClasses import ItemClassification, CollectionState
+from . import MessengerTestBase
+
+
+class AllSealsRequired(MessengerTestBase):
+ options = {
+ "goal": "power_seal_hunt",
+ }
+
+ def test_chest_access(self) -> None:
+ """Defaults to a total of 45 power seals in the pool and required."""
+ with self.subTest("Access Dependency"):
+ self.assertEqual(len([seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]),
+ self.world.options.total_seals)
+ locations = ["Rescue Phantom"]
+ items = [["Power Seal"]]
+ self.assertAccessDependency(locations, items)
+ self.multiworld.state = CollectionState(self.multiworld)
+
+ self.assertEqual(self.can_reach_location("Rescue Phantom"), False)
+ self.assertBeatable(False)
+ self.collect_all_but(["Power Seal", "Do the Thing!"])
+ self.assertEqual(self.can_reach_location("Rescue Phantom"), False)
+ self.assertBeatable(False)
+ self.collect_by_name("Power Seal")
+ self.assertEqual(self.can_reach_location("Rescue Phantom"), True)
+ self.assertBeatable(True)
+
+
+class HalfSealsRequired(MessengerTestBase):
+ options = {
+ "goal": "power_seal_hunt",
+ "percent_seals_required": 50,
+ }
+
+ def test_seals_amount(self) -> None:
+ """Should have 45 power seals in the item pool and half that required"""
+ self.assertEqual(self.world.options.total_seals, 45)
+ self.assertEqual(self.world.total_seals, 45)
+ self.assertEqual(self.world.required_seals, 22)
+ total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]
+ required_seals = [seal for seal in total_seals
+ if seal.classification == ItemClassification.progression_skip_balancing]
+ self.assertEqual(len(total_seals), 45)
+ self.assertEqual(len(required_seals), 22)
+
+
+class ThirtyThirtySeals(MessengerTestBase):
+ options = {
+ "goal": "power_seal_hunt",
+ "total_seals": 30,
+ "percent_seals_required": 34,
+ }
+
+ def test_seals_amount(self) -> None:
+ """Should have 30 power seals in the pool and 33 percent of that required."""
+ self.assertEqual(self.world.options.total_seals, 30)
+ self.assertEqual(self.world.total_seals, 30)
+ self.assertEqual(self.world.required_seals, 10)
+ total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]
+ required_seals = [seal for seal in total_seals
+ if seal.classification == ItemClassification.progression_skip_balancing]
+ self.assertEqual(len(total_seals), 30)
+ self.assertEqual(len(required_seals), 10)
+
+
+class MaxSealsNoShards(MessengerTestBase):
+ options = {
+ "goal": "power_seal_hunt",
+ "total_seals": 85,
+ }
+
+ def test_seals_amount(self) -> None:
+ """Should set total seals to 70 since shards aren't shuffled."""
+ self.assertEqual(self.world.options.total_seals, 85)
+ self.assertEqual(self.world.total_seals, 70)
+
+
+class MaxSealsWithShards(MessengerTestBase):
+ options = {
+ "goal": "power_seal_hunt",
+ "total_seals": 85,
+ "shuffle_shards": "true",
+ }
+
+ def test_seals_amount(self) -> None:
+ """Should have 85 seals in the pool with all required and be a valid seed."""
+ self.assertEqual(self.world.options.total_seals, 85)
+ self.assertEqual(self.world.total_seals, 85)
+ self.assertEqual(self.world.required_seals, 85)
+ total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]
+ required_seals = [seal for seal in total_seals
+ if seal.classification == ItemClassification.progression_skip_balancing]
+ self.assertEqual(len(total_seals), 85)
+ self.assertEqual(len(required_seals), 85)
diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py
index 084a611e4454..9407097b4638 100644
--- a/worlds/minecraft/Options.py
+++ b/worlds/minecraft/Options.py
@@ -1,5 +1,6 @@
import typing
-from Options import Choice, Option, Toggle, DefaultOnToggle, Range, OptionList, DeathLink
+from Options import Choice, Option, Toggle, DefaultOnToggle, Range, OptionList, DeathLink, PlandoConnections
+from .Constants import region_info
class AdvancementGoal(Range):
@@ -14,7 +15,7 @@ class EggShardsRequired(Range):
"""Number of dragon egg shards to collect to spawn bosses."""
display_name = "Egg Shards Required"
range_start = 0
- range_end = 74
+ range_end = 50
default = 0
@@ -22,7 +23,7 @@ class EggShardsAvailable(Range):
"""Number of dragon egg shards available to collect."""
display_name = "Egg Shards Available"
range_start = 0
- range_end = 74
+ range_end = 50
default = 0
@@ -97,7 +98,19 @@ class StartingItems(OptionList):
display_name = "Starting Items"
+class MCPlandoConnections(PlandoConnections):
+ entrances = set(connection[0] for connection in region_info["default_connections"])
+ exits = set(connection[1] for connection in region_info["default_connections"])
+
+ @classmethod
+ def can_connect(cls, entrance, exit):
+ if exit in region_info["illegal_connections"] and entrance in region_info["illegal_connections"][exit]:
+ return False
+ return True
+
+
minecraft_options: typing.Dict[str, type(Option)] = {
+ "plando_connections": MCPlandoConnections,
"advancement_goal": AdvancementGoal,
"egg_shards_required": EggShardsRequired,
"egg_shards_available": EggShardsAvailable,
diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py
index fa992e1e1146..75e043d0cbaf 100644
--- a/worlds/minecraft/__init__.py
+++ b/worlds/minecraft/__init__.py
@@ -37,7 +37,7 @@ class MinecraftWebWorld(WebWorld):
bug_report_page = "https://github.com/KonoTyran/Minecraft_AP_Randomizer/issues/new?assignees=&labels=bug&template=bug_report.yaml&title=%5BBug%5D%3A+Brief+Description+of+bug+here"
setup = Tutorial(
- "Multiworld Setup Tutorial",
+ "Multiworld Setup Guide",
"A guide to setting up the Archipelago Minecraft software on your computer. This guide covers"
"single-player, multiworld, and related software.",
"English",
@@ -92,8 +92,6 @@ class MinecraftWorld(World):
item_name_to_id = Constants.item_name_to_id
location_name_to_id = Constants.location_name_to_id
- data_version = 7
-
def _get_mc_data(self) -> Dict[str, Any]:
exits = [connection[0] for connection in Constants.region_info["default_connections"]]
return {
@@ -173,7 +171,7 @@ def create_items(self) -> None:
def generate_output(self, output_directory: str) -> None:
data = self._get_mc_data()
- filename = f"AP_{self.multiworld.get_out_file_name_base(self.player)}.apmc"
+ filename = f"{self.multiworld.get_out_file_name_base(self.player)}.apmc"
with open(os.path.join(output_directory, filename), 'wb') as f:
f.write(b64encode(bytes(json.dumps(data), 'utf-8')))
diff --git a/worlds/minecraft/docs/en_Minecraft.md b/worlds/minecraft/docs/en_Minecraft.md
index 1ef347983bc4..3a69a7f59a22 100644
--- a/worlds/minecraft/docs/en_Minecraft.md
+++ b/worlds/minecraft/docs/en_Minecraft.md
@@ -1,8 +1,8 @@
# Minecraft
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -11,9 +11,9 @@ Some recipes are locked from being able to be crafted and shuffled into the item
structures appear in each dimension. Crafting recipes are re-learned when they are received from other players as item
checks, and occasionally when completing your own achievements. See below for which recipes are shuffled.
-## What is considered a location check in minecraft?
+## What is considered a location check in Minecraft?
-Location checks in are completed when the player completes various Minecraft achievements. Opening the advancements menu
+Location checks are completed when the player completes various Minecraft achievements. Opening the advancements menu
in-game by pressing "L" will display outstanding achievements.
## When the player receives an item, what happens?
@@ -24,7 +24,7 @@ inventory directly.
## What is the victory condition?
Victory is achieved when the player kills the Ender Dragon, enters the portal in The End, and completes the credits
-sequence either by skipping it or watching hit play out.
+sequence either by skipping it or watching it play out.
## Which recipes are locked?
@@ -64,12 +64,15 @@ sequence either by skipping it or watching hit play out.
* Diamond Axe
* Progessive Tools
* Tier I
+ * Stone Pickaxe
* Stone Shovel
* Stone Hoe
* Tier II
+ * Iron Pickaxe
* Iron Shovel
* Iron Hoe
* Tier III
+ * Diamond Pickaxe
* Diamond Shovel
* Diamond Hoe
* Netherite Ingot
diff --git a/worlds/minecraft/docs/minecraft_en.md b/worlds/minecraft/docs/minecraft_en.md
index e8b1a3642ee0..e0b5ae3b98b5 100644
--- a/worlds/minecraft/docs/minecraft_en.md
+++ b/worlds/minecraft/docs/minecraft_en.md
@@ -5,7 +5,6 @@
- Minecraft Java Edition from
the [Minecraft Java Edition Store Page](https://www.minecraft.net/en-us/store/minecraft-java-edition)
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- - (select `Minecraft Client` during installation.)
## Configuring your YAML file
@@ -16,7 +15,7 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a YAML file?
-You can customize your settings by visiting the [Minecraft Player Settings Page](/games/Minecraft/player-settings)
+You can customize your options by visiting the [Minecraft Player Options Page](/games/Minecraft/player-options)
## Joining a MultiWorld Game
diff --git a/worlds/minecraft/docs/minecraft_es.md b/worlds/minecraft/docs/minecraft_es.md
index 3f2df6e7ba76..4f4899212240 100644
--- a/worlds/minecraft/docs/minecraft_es.md
+++ b/worlds/minecraft/docs/minecraft_es.md
@@ -29,7 +29,7 @@ name: TuNombre
game: Minecraft
# Opciones compartidas por todos los juegos:
-accessibility: locations
+accessibility: full
progression_balancing: 50
# Opciones Especficicas para Minecraft
diff --git a/worlds/minecraft/docs/minecraft_fr.md b/worlds/minecraft/docs/minecraft_fr.md
index e25febba42f9..31c48151f491 100644
--- a/worlds/minecraft/docs/minecraft_fr.md
+++ b/worlds/minecraft/docs/minecraft_fr.md
@@ -16,7 +16,7 @@ guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/se
### Où puis-je obtenir un fichier YAML ?
-Vous pouvez personnaliser vos paramètres Minecraft en allant sur la [page des paramètres de joueur](/games/Minecraft/player-settings)
+Vous pouvez personnaliser vos paramètres Minecraft en allant sur la [page des paramètres de joueur](/games/Minecraft/player-options)
## Rejoindre une partie MultiWorld
@@ -71,4 +71,4 @@ les liens suivants sont les versions des logiciels que nous utilisons.
- [Page des versions du mod Minecraft Archipelago Randomizer] (https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
- **NE PAS INSTALLER CECI SUR VOTRE CLIENT**
- [Amazon Corretto](https://docs.aws.amazon.com/corretto/)
- - choisissez la version correspondante et sélectionnez "Téléchargements" sur la gauche
\ No newline at end of file
+ - choisissez la version correspondante et sélectionnez "Téléchargements" sur la gauche
diff --git a/worlds/minecraft/docs/minecraft_sv.md b/worlds/minecraft/docs/minecraft_sv.md
index e86d29393925..ab8c1b5d8ea7 100644
--- a/worlds/minecraft/docs/minecraft_sv.md
+++ b/worlds/minecraft/docs/minecraft_sv.md
@@ -79,7 +79,7 @@ description: Template Name
# Ditt spelnamn. Mellanslag kommer bli omplacerad med understräck och det är en 16-karaktärsgräns.
name: YourName
game: Minecraft
-accessibility: locations
+accessibility: full
progression_balancing: 0
advancement_goal:
few: 0
@@ -103,8 +103,6 @@ shuffle_structures:
off: 0
```
-För mer detaljer om vad varje inställning gör, kolla standardinställningen `PlayerSettings.yaml` som kommer med
-Archipelago-installationen.
## Gå med i ett Multivärld-spel
diff --git a/worlds/mlss/Client.py b/worlds/mlss/Client.py
new file mode 100644
index 000000000000..1f08b85610d6
--- /dev/null
+++ b/worlds/mlss/Client.py
@@ -0,0 +1,284 @@
+from typing import TYPE_CHECKING, Optional, Set, List, Dict
+import struct
+
+from NetUtils import ClientStatus
+from .Locations import roomCount, nonBlock, beanstones, roomException, shop, badge, pants, eReward
+from .Items import items_by_id
+
+import asyncio
+
+import worlds._bizhawk as bizhawk
+from worlds._bizhawk.client import BizHawkClient
+
+if TYPE_CHECKING:
+ from worlds._bizhawk.context import BizHawkClientContext
+
+ROOM_ARRAY_POINTER = 0x51FA00
+
+
+class MLSSClient(BizHawkClient):
+ game = "Mario & Luigi Superstar Saga"
+ system = "GBA"
+ patch_suffix = ".apmlss"
+ local_checked_locations: Set[int]
+ goal_flag: int
+ rom_slot_name: Optional[str]
+ eUsed: List[int]
+ room: int
+ local_events: List[int]
+ player_name: Optional[str]
+ checked_flags: Dict[int, list] = {}
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.local_checked_locations = set()
+ self.local_set_events = {}
+ self.local_found_key_items = {}
+ self.rom_slot_name = None
+ self.seed_verify = False
+ self.eUsed = []
+ self.room = 0
+ self.local_events = []
+
+ async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
+ from CommonClient import logger
+
+ try:
+ # Check ROM name/patch version
+ rom_name_bytes = await bizhawk.read(ctx.bizhawk_ctx, [(0xA0, 14, "ROM")])
+ rom_name = bytes([byte for byte in rom_name_bytes[0] if byte != 0]).decode("UTF-8")
+ if not rom_name.startswith("MARIO&LUIGIUA8"):
+ return False
+ except UnicodeDecodeError:
+ return False
+ except bizhawk.RequestFailedError:
+ return False # Should verify on the next pass
+
+ ctx.game = self.game
+ ctx.items_handling = 0b101
+ ctx.want_slot_data = True
+ ctx.watcher_timeout = 0.125
+ self.rom_slot_name = rom_name
+ self.seed_verify = False
+ name_bytes = (await bizhawk.read(ctx.bizhawk_ctx, [(0xDF0000, 16, "ROM")]))[0]
+ name = bytes([byte for byte in name_bytes if byte != 0]).decode("UTF-8")
+ self.player_name = name
+
+ for i in range(59):
+ self.checked_flags[i] = []
+
+ return True
+
+ async def set_auth(self, ctx: "BizHawkClientContext") -> None:
+ ctx.auth = self.player_name
+
+ def on_package(self, ctx, cmd, args) -> None:
+ if cmd == "RoomInfo":
+ ctx.seed_name = args["seed_name"]
+
+ async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
+ from CommonClient import logger
+
+ try:
+ if ctx.seed_name is None:
+ return
+ if not self.seed_verify:
+ seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.seed_name), "ROM")])
+ seed = seed[0].decode("UTF-8")
+ if seed != ctx.seed_name:
+ logger.info(
+ "ERROR: The ROM you loaded is for a different game of AP. "
+ "Please make sure the host has sent you the correct patch file,"
+ "and that you have opened the correct ROM."
+ )
+ raise bizhawk.ConnectorError("Loaded ROM is for Incorrect lobby.")
+ self.seed_verify = True
+
+ read_state = await bizhawk.read(
+ ctx.bizhawk_ctx,
+ [
+ (0x4564, 59, "EWRAM"),
+ (0x2330, 2, "IWRAM"),
+ (0x3FE0, 1, "IWRAM"),
+ (0x304A, 1, "EWRAM"),
+ (0x304B, 1, "EWRAM"),
+ (0x304C, 4, "EWRAM"),
+ (0x3060, 6, "EWRAM"),
+ (0x4808, 2, "EWRAM"),
+ (0x4407, 1, "EWRAM"),
+ (0x2339, 1, "IWRAM"),
+ ]
+ )
+ flags = read_state[0]
+ current_room = int.from_bytes(read_state[1], "little")
+ shop_init = read_state[2][0]
+ shop_scroll = read_state[3][0] & 0x1F
+ is_buy = read_state[4][0] != 0
+ shop_address = (struct.unpack("= 0xDA0000 and location not in self.local_events:
+ self.local_events += [location]
+ await ctx.send_msgs(
+ [
+ {
+ "cmd": "Set",
+ "key": f"mlss_flag_{ctx.team}_{ctx.slot}",
+ "default": 0,
+ "want_reply": False,
+ "operations": [{"operation": "or", "value": 1 << (location - 0xDA0000)}],
+ }
+ ]
+ )
+ continue
+ if location in roomException:
+ if current_room not in roomException[location]:
+ exception = True
+ else:
+ exception = False
+ else:
+ exception = True
+
+ if location in eReward:
+ if location not in self.eUsed:
+ self.eUsed += [location]
+ location = eReward[len(self.eUsed) - 1]
+ else:
+ continue
+ if (location in ctx.server_locations) and exception:
+ locs_to_send.add(location)
+
+ # Check for set location flags.
+ for byte_i, byte in enumerate(bytearray(flags)):
+ for j in range(8):
+ if j in self.checked_flags[byte_i]:
+ continue
+ and_value = 1 << j
+ if byte & and_value != 0:
+ flag_id = byte_i * 8 + (j + 1)
+ room, item = find_key(roomCount, flag_id)
+ pointer_arr = await bizhawk.read(
+ ctx.bizhawk_ctx, [(ROOM_ARRAY_POINTER + ((room - 1) * 4), 4, "ROM")]
+ )
+ pointer = struct.unpack(" value:
+ leftover -= value
+ else:
+ return key, leftover
+
+
+def id_to_RAM(id_: int):
+ code = id_
+ if 0x1C <= code <= 0x1F:
+ code += 0xE
+ if 0x20 <= code <= 0x26:
+ code -= 0x4
+ return code
diff --git a/worlds/mlss/Data.py b/worlds/mlss/Data.py
new file mode 100644
index 000000000000..749e63bcf24d
--- /dev/null
+++ b/worlds/mlss/Data.py
@@ -0,0 +1,5705 @@
+flying = [
+ 0x14,
+ 0x1D,
+ 0x4C
+]
+
+pestnut = [
+ 0x20,
+ 0x34
+]
+
+enemies = [
+ 0x5030EC,
+ 0x50310C,
+ 0x50312C,
+ 0x50314C,
+ 0x50316C,
+ 0x50318C,
+ 0x5031AC,
+ 0x5031CC,
+ 0x5031EC,
+ 0x50328C,
+ 0x5032AC,
+ 0x5032CC,
+ 0x5032EC,
+ 0x50330C,
+ 0x50332C,
+ 0x50334C,
+ 0x50336C,
+ 0x50338C,
+ 0x5033AC,
+ 0x5033CC,
+ 0x50340C,
+ 0x50342C,
+ 0x50344C,
+ 0x50346C,
+ 0x50348C,
+ 0x5034AC,
+ 0x5034CC,
+ 0x5034EC,
+ 0x50350C,
+ 0x50356C,
+ 0x50358C,
+ 0x5035AC,
+ 0x5035CC,
+ 0x5035EC,
+ 0x50366C,
+ 0x50368C,
+ 0x5036AC,
+ 0x5036CC,
+ 0x5036EC,
+ 0x50370C,
+ 0x50372C,
+ 0x50374C,
+ 0x50376C,
+ 0x50378C,
+ 0x5037EC,
+ 0x50380C,
+ 0x50382C,
+ 0x50384C,
+ 0x50386C,
+ 0x50388C,
+ 0x5038AC,
+ 0x5038CC,
+ 0x5038EC,
+ 0x50390C,
+ 0x50392C,
+ 0x50394C,
+ 0x50398C,
+ 0x5039AC,
+ 0x5039CC,
+ 0x5039EC,
+ 0x503A0C,
+ 0x503A2C,
+ 0x503A4C,
+ 0x503A6C,
+ 0x503AAC,
+ 0x503ACC,
+ 0x503AEC,
+ 0x503B0C,
+ 0x503B2C,
+ 0x503B4C,
+ 0x503B6C,
+ 0x503B8C,
+ 0x503BAC,
+ 0x503BCC,
+ 0x503BEC,
+ 0x503C0C,
+ 0x503C2C,
+ 0x503C4C,
+ 0x503C6C,
+ 0x503C8C,
+ 0x503CAC,
+ 0x503CCC,
+ 0x503CEC,
+ 0x503D0C,
+ 0x503D2C,
+ 0x503D4C,
+ 0x503D8C,
+ 0x503DAC,
+ 0x503DCC,
+ 0x503DEC,
+ 0x503E0C,
+ 0x503E2C,
+ 0x503E4C,
+ 0x503E6C,
+ 0x503E8C,
+ 0x503EAC,
+ 0x503ECC,
+ 0x503EEC,
+ 0x503F2C,
+ 0x503F4C,
+ 0x503F6C,
+ 0x503F8C,
+ 0x503FAC,
+ 0x503FCC,
+ 0x503FEC,
+ 0x50400C,
+ 0x50404C,
+ 0x50406C,
+ 0x50408C,
+ 0x5040AC,
+ 0x5040CC,
+ 0x5040EC,
+ 0x50410C,
+ 0x50412C,
+ 0x50414C,
+ 0x50416C,
+ 0x50418C,
+ 0x5041AC,
+ 0x5041CC,
+ 0x5041EC,
+ 0x50420C,
+ 0x50422C,
+ 0x50436C,
+ 0x5043AC,
+ 0x5043CC,
+ 0x5043EC,
+ 0x50440C,
+ 0x50442C,
+ 0x50444C,
+ 0x50446C,
+ 0x50448C,
+ 0x5044AC,
+ 0x5044CC,
+ 0x5044EC,
+ 0x50450C,
+ 0x50452C,
+ 0x50454C,
+ 0x50456C,
+ 0x50458C,
+ 0x5045AC,
+ 0x50468C,
+ 0x5046CC,
+ 0x5046EC,
+ 0x50470C
+]
+
+bosses = [
+ 0x5030CC,
+ 0x50320C,
+ 0x50322C,
+ 0x50352C,
+ 0x50360C,
+ 0x5037AC,
+ 0x5037CC,
+ 0x503A8C,
+ 0x503D6C,
+ 0x503F0C,
+ 0x50424C,
+ 0x50426C,
+ 0x50428C,
+ 0x5042AC,
+ 0x5042CC,
+ 0x5042EC,
+ 0x50430C,
+ 0x50438C,
+ 0x5045CC,
+ 0x5045EC,
+ 0x50460C
+]
+
+bowsers = [
+ 0x50404C,
+ 0x50406C,
+ 0x50408C,
+ 0x5040AC,
+ 0x5040CC,
+ 0x5040EC,
+ 0x50410C,
+ 0x50412C,
+ 0x50414C,
+ 0x50416C,
+ 0x50418C,
+ 0x5041AC,
+ 0x5041CC,
+ 0x5041EC,
+ 0x50420C,
+ 0x50422C,
+ 0x50424C,
+ 0x50426C,
+ 0x50428C,
+ 0x5042AC,
+ 0x5042CC,
+ 0x5042EC,
+ 0x50430C,
+]
+
+sounds = [
+ 0x1da51c,
+ 0x1da478,
+ 0x1da430,
+ 0x1da420,
+ 0x1da404,
+ 0x1da3e8,
+ 0x1da330,
+ 0x1da1e0,
+ 0x1da1b4,
+ 0x1da13c,
+ 0x1da0c8,
+ 0x1d9fec,
+ 0x1d9f10,
+ 0x1d9e78,
+ 0x1d9cec,
+ 0x1d9cdc,
+ 0x1d9cd0,
+ 0x1d9cc4,
+ 0x1d9cb8,
+ 0x1d9cac,
+ 0x1d9ca0,
+ 0x1d9c94,
+ 0x1d9c88,
+ 0x1d9c1c,
+ 0x1d93a4,
+ 0x1d926c,
+ 0x1d91d4,
+ 0x1d9134,
+ 0x1d9108,
+ 0x1d9094,
+ 0x1d9020,
+ 0x1d8f48,
+ 0x1d8f3c,
+ 0x1d8f30,
+ 0x1d8f14,
+ 0x1d8f08,
+ 0x1d8efc,
+ 0x1d8ef0,
+ 0x1bc330,
+ 0x1bc324,
+ 0x1bc318,
+ 0x1bc30c,
+ 0x1bc300,
+ 0x1bc2f4,
+ 0x1bc2a4,
+ 0x1bc210,
+ 0x1bc18c,
+ 0x1bc124,
+ 0x1bc088,
+ 0x1bc06c,
+ 0x1bbffc,
+ 0x1bbf8c,
+ 0x1bbeb8,
+ 0x1bbde4,
+ 0x1bbd34,
+ 0x1bbd14,
+ 0x1bbc14,
+ 0x1bbbd8,
+ 0x1bbb60,
+ 0x1bbb0c,
+ 0x1bba5c,
+ 0x1bb9b8,
+ 0x1bb9a0,
+ 0x1bb8e4,
+ 0x1bb89c,
+ 0x1bb880,
+ 0x1bb704,
+ 0x1bb6e8,
+ 0x1bb6dc,
+ 0x1bb6d0,
+ 0x1bb6c4,
+ 0x1bb6b8,
+ 0x1bb6ac,
+ 0x1bb6a0,
+ 0x1bb694,
+ 0x1bb688,
+ 0x1bb67c,
+ 0x1bb66c,
+ 0x1bb660,
+ 0x1bb654,
+ 0x1bb648,
+ 0x1bb63c,
+ 0x1bb630,
+ 0x1bb624,
+ 0x1bb618,
+ 0x1bb60c,
+ 0x1bb600,
+ 0x1bb5f4,
+ 0x1bb5e8,
+ 0x1bb5dc,
+ 0x1bb5d0,
+ 0x1bb5c4,
+ 0x1bb5b8,
+ 0x1bb5ac,
+ 0x1bb580,
+ 0x1bb574,
+ 0x1bb558,
+ 0x1bb54c,
+ 0x1bb540,
+ 0x1bb534,
+ 0x1bb528,
+ 0x1bb51c,
+ 0x1bb510,
+ 0x1bb504,
+ 0x1bb4f8,
+ 0x1bb4ec,
+ 0x1bb4e0,
+ 0x1bb4d4,
+ 0x1bb4c8,
+ 0x1bb4bc,
+ 0x1bb4b0,
+ 0x1bb4a4,
+ 0x1bb408,
+ 0x1bb398,
+ 0x1bb2e0,
+ 0x1bb270,
+ 0x1bb14c,
+ 0x1bb140,
+ 0x1bb134,
+ 0x1bb128,
+ 0x1bb11c,
+ 0x1bb110,
+ 0x1baf84,
+ 0x1bac0c,
+ 0x1bab3c,
+ 0x1bab24,
+ 0x1bab18,
+ 0x1bab0c,
+ 0x1bab00,
+ 0x1baaf4,
+ 0x1baad8,
+ 0x1ba988,
+ 0x1ba97c,
+ 0x1ba970,
+ 0x1ba964,
+ 0x1ba958,
+ 0x1ba94c,
+ 0x1ba940,
+ 0x1ba918,
+ 0x1ba580,
+ 0x1ba4c0,
+ 0x1ba498,
+ 0x1ae0c8,
+ 0x1ae060,
+ 0x1ae048,
+ 0x1abcec,
+ 0x1abc48,
+ 0x1abb8c,
+ 0x1abb30,
+ 0x1ababc,
+ 0x1ab644,
+ 0x1ab5a8,
+ 0x1ab58c,
+ 0x1ab50c,
+ 0x1ab488,
+ 0x1ab3f0,
+ 0x1ab38c,
+ 0x1ab348,
+ 0x1ab31c,
+ 0x1ab300,
+ 0x1ab290,
+ 0x1ab220,
+ 0x1ab084,
+ 0x1ab04c,
+ 0x1aafc8,
+ 0x1aaf64,
+ 0x1a8d14,
+ 0x1a8c30,
+ 0x1a8290,
+ 0x1a8284,
+ 0x1a8168,
+ 0x1a8014,
+ 0x1a7f10,
+ 0x1a7eb8,
+ 0x1a7dc8,
+ 0x1a7d5c,
+ 0x1a7bf0,
+ 0x1a7b70,
+ 0x1a7b3c,
+ 0x1a7b0c,
+ 0x1a7a1c,
+ 0x1a7980,
+ 0x1a7910,
+ 0x1a783c,
+ 0x1a77a0,
+ 0x1a7718,
+ 0x1a769c,
+ 0x1a7620,
+ 0x1a75dc,
+ 0x1a75bc,
+ 0x1a7518,
+ 0x1a7500,
+ 0x1a74c0,
+ 0x1a7440,
+ 0x1a7434,
+ 0x1a7428,
+ 0x1a311c,
+ 0x1a3010,
+ 0x1a2ff8,
+ 0x1a2fa0,
+ 0x1a2f4c,
+ 0x1a2f18,
+ 0x1a2eec,
+ 0x1a2edc,
+ 0x1a2e98,
+ 0x1a2df8,
+ 0x1a2d00,
+ 0x1a2b80,
+ 0x1a2b58,
+ 0x1a2b30,
+ 0x1a2b04,
+ 0x1a2a80,
+ 0x1a2a20,
+ 0x1a29e0,
+ 0x1a29c0,
+ 0x1a2990,
+ 0x1a296c,
+ 0x1a1d70,
+ 0x1a1d48,
+ 0x1a1d38,
+ 0x1a1d18,
+ 0x1a1c9c,
+ 0x1a1c50,
+ 0x1a1c10,
+ 0x1a1be8,
+ 0x1a1bbc,
+ 0x1a1b74,
+ 0x1a1af8,
+ 0x1a1a58,
+ 0x1a1968,
+ 0x1a18d8,
+ 0x1a186c,
+ 0x1a1850,
+ 0x1a1804,
+ 0x1a17dc,
+ 0x1a1768,
+ 0x1a173c,
+ 0x1a1714,
+ 0x1a16d0,
+ 0x1a16a4,
+ 0x1a1680,
+ 0x1a1620,
+ 0x1a15b8,
+ 0x1a156c,
+ 0x1a1524,
+ 0x1a1494,
+ 0x1a1464,
+ 0x1a1440,
+ 0x1a1430,
+ 0x1a1414,
+ 0x1a1350,
+ 0x1a12c4,
+ 0x1a126c,
+ 0x1a1250,
+ 0x1a11f8,
+ 0x1a11b4,
+ 0x1a11a4,
+ 0x1a110c,
+ 0x1a10f8,
+ 0x1a1078,
+ 0x1a1018,
+ 0x1a0fd4,
+ 0x1a0fb0,
+ 0x1a0fa4,
+ 0x1a0f98,
+ 0x1a0f74,
+ 0x1a0f68,
+ 0x1a0f5c,
+ 0x1a0f50,
+ 0x1a0f44,
+ 0x1a0f38,
+ 0x1a0f2c,
+ 0x1a0f20,
+ 0x1a0f14,
+ 0x1a0f08,
+ 0x1a0efc,
+ 0x1a0ef0,
+ 0x1a0ee4,
+ 0x1a0ed8,
+ 0x1a0ec8,
+ 0x1a0eb8,
+ 0x1a0ea8,
+ 0x1a0e9c,
+ 0x1a0e90,
+ 0x1a0e84,
+ 0x1a0e78,
+ 0x1a0e6c,
+ 0x1a0e60,
+ 0x1a0e54,
+ 0x1a0e48,
+ 0x1a0e3c,
+ 0x1a0e30,
+ 0x1a0e24,
+ 0x1a0e18,
+ 0x1a0e0c,
+ 0x1a0e00,
+ 0x1a0df4,
+ 0x1a0de8,
+ 0x1a0ddc,
+ 0x1a0dd0,
+ 0x1a0da4,
+]
+
+vanilla = []
+
+azureHat = [
+[0x3CC884, 0xED, 0x7F, 0],
+[0x3CC886, 0x8B, 0x73, 0],
+[0x3CC8C4, 0xED, 0x7F, 1],
+[0x3CC8C6, 0x8B, 0x73, 1],
+[0x4F4CDC, 0xED, 0x7F, 0],
+[0x4F4CDE, 0x8B, 0x73, 0],
+[0x4F4D1C, 0xED, 0x7F, 1],
+[0x4F4D1E, 0x8B, 0x73, 1],
+[0x4F51D8, 0xED, 0x7F, 1],
+[0x4F51DC, 0xED, 0x7F, 0],
+[0x4F51E8, 0xD6, 0x7B, 0],
+[0x4F51EA, 0xD6, 0x7B, 1],
+[0x4F51DA, 0xFA, 0x7F, 1],
+[0x4F51DE, 0xFA, 0x7F, 0],
+[0x4FB686, 0xED, 0x7F, 0],
+[0x4FB6A6, 0xED, 0x7F, 1],
+[0x4FB684, 0x8E, 0x52, 0],
+[0x4FB6A4, 0x8E, 0x52, 1],
+[0x4FB688, 0xFA, 0x7F, 0],
+[0x4FB6A8, 0xFA, 0x7F, 1],
+[0x4FB786, 0x8B, 0x73, 0],
+[0x4FB788, 0xED, 0x7F, 0],
+[0x4FB78A, 0xFA, 0x7F, 0],
+[0x4FB7A6, 0x8B, 0x73, 1],
+[0x4FB7A8, 0xED, 0x7F, 1],
+[0x4FB7AA, 0xFA, 0x7F, 1],
+[0x51901C, 0xED, 0x7F, 0],
+[0x519018, 0xC9, 0x5A, 0],
+[0x51901A, 0x8B, 0x77, 0],
+[0x51903C, 0xED, 0x7F, 1],
+[0x519038, 0xC9, 0x5A, 1],
+[0x51903A, 0x8B, 0x77, 1],
+[0x5193EA, 0x8B, 0x73, 0],
+[0x5193E8, 0xED, 0x7F, 0],
+[0x51940A, 0x8B, 0x73, 1],
+[0x519408, 0xED, 0x7F, 1],
+[0x3A72AE, 0x60, 0x6F, 1],
+[0x3A7244, 0x20, 0x67, 0],
+[0x3A724C, 0x60, 0x6F, 1],
+[0x3A7290, 0x40, 0x6B, 0],
+[0x3A72B2, 0xA0, 0x77, 1],
+[0x3A7270, 0xC0, 0x5A, 1],
+[0x3A7288, 0xC0, 0x5A, 0],
+[0x3A7296, 0x80, 0x73, 0],
+[0x3A7274, 0x80, 0x73, 1],
+[0x3A7294, 0x60, 0x6F, 0],
+[0x3A724A, 0xA0, 0x77, 0],
+[0x3A7278, 0xA0, 0x77, 1],
+[0x3A724E, 0xC0, 0x7B, 0],
+[0x3A727A, 0xC0, 0x7B, 1],
+[0x3A7252, 0xE0, 0x7F, 0],
+[0x3A727C, 0xE0, 0x7F, 1],
+[0x3A72BC, 0xE0, 0x7F, 1],
+[0x3A726C, 0x80, 0x52, 1],
+[0x3A7286, 0x80, 0x52, 0],
+[0x3A728C, 0x00, 0x63, 0],
+[0x3A7272, 0x00, 0x63, 1],
+[0x3A7254, 0xF3, 0x7F, 1],
+[0x3A7256, 0xF3, 0x7F, 0],
+[0x3A7264, 0xF3, 0x7F, 1],
+[0x9F9A58, 0x56, 0x6B, 0],
+[0x9F9A5E, 0x56, 0x6B, 1],
+[0x9F9A2A, 0xED, 0x7F, 0],
+[0x9F9A30, 0xED, 0x7F, 1],
+[0x9F9A36, 0xCC, 0x7B, 0],
+[0x9F9A3C, 0xCC, 0x7B, 1],
+[0x9F9A4A, 0xCC, 0x7B, 0],
+[0x9F9A50, 0xCC, 0x7B, 1],
+[0x9F9A56, 0x0A, 0x63, 0],
+[0x9F9A5C, 0x0A, 0x63, 1],
+[0x9F9A28, 0x8E, 0x52, 0],
+[0x9F9A2E, 0x8E, 0x52, 1],
+[0x9F9A48, 0x8E, 0x52, 0],
+[0x9F9A4E, 0x8E, 0x52, 1],
+[0x9F9A34, 0x8B, 0x73, 0],
+[0x9F9A3A, 0x8B, 0x73, 1],
+[0x9F9A2C, 0xFA, 0x7F, 0],
+[0x9F9A32, 0xFA, 0x7F, 1],
+[0x9F9A38, 0xFA, 0x7F, 0],
+[0x9F9A3E, 0xFA, 0x7F, 1],
+[0x9F9A4C, 0xFA, 0x7F, 0],
+[0x9F9A52, 0xFA, 0x7F, 1],
+[0xA5DD46, 0xAC, 0x56, 0],
+[0xA5DD44, 0x2A, 0x46, 0],
+[0xA5DD4A, 0x90, 0x73, 0],
+[0xA5DD3E, 0x80, 0x52, 0],
+[0xA5DD40, 0xF2, 0x7F, 0],
+[0xA5DD42, 0xFC, 0x7F, 0],
+[0xA5DD48, 0x2F, 0x67, 0],
+[0xA5DD4E, 0xFF, 0x7F, 0],
+[0xA5DD66, 0xAC, 0x56, 1],
+[0xA5DD64, 0x2A, 0x46, 1],
+[0xA5DD6A, 0x90, 0x73, 1],
+[0xA5DD5E, 0x80, 0x52, 1],
+[0xA5DD60, 0xF2, 0x7F, 1],
+[0xA5DD62, 0xFC, 0x7F, 1],
+[0xA5DD68, 0x2F, 0x67, 1],
+[0x3D0E0C, 0xED, 0x7F, 1],
+[0x3D0E18, 0xED, 0x7F, 0],
+[0x3D0E0A, 0x8B, 0x73, 1],
+[0x3D0E1A, 0x8B, 0x73, 0],
+[0x3D0E08, 0xFA, 0x7F, 1],
+[0x3D0E16, 0xFA, 0x7F, 0],
+[0x3CC9C4, 0xED, 0x7F, 0],
+[0x3CC9C6, 0x8B, 0x73, 0],
+[0x3CDE7C, 0xED, 0x7F, 1],
+[0x3CDE7E, 0x8B, 0x73, 1],
+[0x51AD2C, 0xED, 0x7F, 0],
+[0x51AD2E, 0x8B, 0x73, 0],
+[0x51AD4C, 0xED, 0x7F, 1],
+[0x51AD4E, 0x8B, 0x73, 1],
+[0x51A4AE, 0x8B, 0x73, 0],
+[0x51A4AC, 0xED, 0x7F, 0],
+[0x51A4CC, 0xED, 0x7F, 1],
+[0x51A4CE, 0x8B, 0x73, 1],
+[0x51A98C, 0xED, 0x7F, 0],
+[0x51A98E, 0x8B, 0x73, 0],
+[0x51A96C, 0xED, 0x7F, 1],
+[0x51A96E, 0x8B, 0x73, 1],
+[0x51AA00, 0x8B, 0x73, 0],
+[0x51AA10, 0x8B, 0x73, 1],
+[0x51AA02, 0xA8, 0x56, 0],
+[0x51AA12, 0xA8, 0x56, 1],
+[0x51A9FE, 0xED, 0x7F, 0],
+[0x51AA0E, 0xED, 0x7F, 1],
+[0x51AA0A, 0xF3, 0x7F, 0],
+[0x3CC984, 0xED, 0x7F, 0],
+[0x3CC986, 0x8B, 0x73, 0],
+[0x3D4B52, 0xAB, 0x77, 0],
+[0x3D4B5C, 0x09, 0x63, 0],
+[0x3D4B54, 0xED, 0x7F, 0],
+[0x3D4B56, 0x8B, 0x73, 0],
+[0x3D4B50, 0xFA, 0x7F, 0],
+[0x3CCA44, 0xED, 0x7F, 1],
+[0x3CCA46, 0x8B, 0x73, 1],
+[0x3CFB3C, 0xED, 0x7F, 0],
+[0x3CFB3E, 0x8B, 0x73, 0],
+[0x3CFB7C, 0xED, 0x7F, 1],
+[0x3CFB7E, 0x8B, 0x73, 1],
+[0x3D504C, 0xAC, 0x77, 0],
+[0x3D504A, 0xED, 0x7F, 0],
+[0x3D504E, 0x8B, 0x73, 0],
+[0x3D508C, 0xAC, 0x77, 1],
+[0x3D508A, 0xED, 0x7F, 1],
+[0x3D508E, 0x8B, 0x73, 1],
+[0xA5DDA2, 0xAC, 0x56, 0],
+[0xA5DDC2, 0xAC, 0x56, 1],
+[0xA5DDA6, 0x90, 0x73, 0],
+[0xA5DDC6, 0x90, 0x73, 1],
+[0xA5DDA4, 0x2F, 0x67, 0],
+[0xA5DDC4, 0x2F, 0x67, 1],
+[0xA5DDA8, 0x97, 0x73, 0],
+[0xA5DDC8, 0x97, 0x73, 1],
+[0x3D3E0C, 0xED, 0x7F, 1],
+[0x3D3E10, 0xED, 0x7F, 0],
+[0x3D3E0E, 0x8B, 0x73, 1],
+[0x3D3E12, 0x8B, 0x73, 0],
+[0x3CF1C0, 0xED, 0x7F, 0],
+[0x3CF200, 0xED, 0x7F, 1],
+[0x3CF1C2, 0x8B, 0x73, 0],
+[0x3CF202, 0x8B, 0x73, 1],
+[0x3D360E, 0x60, 0x6F, 1],
+[0x3D3614, 0x60, 0x6F, 0],
+[0x3D360C, 0xFA, 0x7F, 1],
+[0x3D3612, 0xFA, 0x7F, 0],
+[0x3D3604, 0x2F, 0x67, 0],
+[0x3D3606, 0x2F, 0x67, 1],
+[0x3D360A, 0xFA, 0x7F, 1],
+[0x3D3610, 0xFA, 0x7F, 0],
+[0x3D1A48, 0x00, 0x63, 0],
+[0x3D1A46, 0xFA, 0x7F, 0],
+[0x3D1A44, 0xFA, 0x7F, 0],
+[0x3D1A4A, 0x2F, 0x67, 0],
+[0x3D1A88, 0x00, 0x63, 1],
+[0x3D1A8A, 0xBB, 0xB8, 1],
+[0x3D1A86, 0xFA, 0x7F, 1],
+[0x3D1A84, 0xFA, 0x7F, 1],
+[0x3CE282, 0x8B, 0x73, 0],
+[0x3CE2C2, 0x8B, 0x73, 1],
+[0x3CE280, 0xFA, 0x7F, 0],
+[0x3CE2C0, 0xFA, 0x7F, 1],
+[0x4FA29E, 0x8B, 0x73, 0],
+[0x4FA2DE, 0x8B, 0x73, 1],
+[0x4FA29C, 0xFA, 0x7F, 0],
+[0x4FA2DC, 0xFA, 0x7F, 1],
+[0x3D4786, 0xF3, 0x7F, 1],
+[0x3D478C, 0xF3, 0x7F, 0],
+[0x3D478E, 0x40, 0x6B, 0],
+[0x3D4788, 0x40, 0x6B, 1],
+[0x3D4790, 0x40, 0x6B, 0],
+[0x3D478A, 0xAC, 0x77, 1],
+[0x3D4794, 0x2F, 0x67, 0],
+[0x3D4792, 0xAC, 0x77, 0],
+[0x3D4784, 0xFE, 0x33, 0],
+[0x3C9E3A, 0xFA, 0x7F, 1],
+[0x3C9E40, 0xFA, 0x7F, 0],
+[0x3C9E38, 0xFA, 0x7F, 1],
+[0x3C9E3E, 0xFA, 0x7F, 0],
+[0x3C9E36, 0xFC, 0x7F, 1],
+[0x3C9E3C, 0xFC, 0x7F, 0],
+[0x4F4D5C, 0xED, 0x7F, 0],
+[0x4F4D5E, 0x8B, 0x73, 0],
+[0x3C9320, 0xED, 0x7F, 0],
+[0x3C9322, 0x8B, 0x73, 0],
+[0x9F9CF6, 0xED, 0x7F, 0],
+[0x9F9CF8, 0x8B, 0x73, 0],
+[0x4F4E1C, 0xED, 0x7F, 1],
+[0x4F4E1E, 0x8B, 0x73, 1],
+[0x3C9450, 0xED, 0x7F, 1],
+[0x3C9452, 0x8B, 0x73, 1],
+[0x9F9D74, 0xED, 0x7F, 1],
+[0x9F9D76, 0x8B, 0x73, 1],
+[0xA6202E, 0xAC, 0x56, 0],
+[0xA62032, 0x90, 0x73, 0],
+[0xA62030, 0x2F, 0x67, 0],
+[0xA62034, 0x97, 0x73, 0],
+[0xA6204E, 0xAC, 0x56, 1],
+[0xA62052, 0x90, 0x73, 1],
+[0xA62050, 0x2F, 0x67, 1],
+[0xA62054, 0x97, 0x73, 1],
+[0x3D4812, 0x60, 0x6F, 0],
+[0x3D480E, 0x60, 0x6F, 1],
+[0x3D4810, 0xFA, 0x7F, 0],
+[0x3D480C, 0xFA, 0x7F, 1],
+[0x3CC9FE, 0xED, 0x7F, 0],
+[0x3CCA0A, 0x8B, 0x73, 0],
+[0x8CBE20, 0x00, 0x63, 0],
+[0x8CBE22, 0x8B, 0x73, 0],
+[0x8CBE1E, 0xF3, 0x7F, 0],
+[0x8CBE40, 0x00, 0x63, 1],
+[0x8CBE42, 0x8B, 0x73, 1],
+[0x8CBE3E, 0xF3, 0x7F, 1],
+[0x8CBEE0, 0xED, 0x7F, 0],
+[0x8CBEDE, 0xFC, 0x7F, 0],
+[0x8CBEE2, 0x8B, 0x73, 0],
+[0x3B8F38, 0x00, 0x63, 1],
+[0x3B8F3A, 0xFC, 0x7F, 1],
+[0x3B8F40, 0x00, 0x63, 0],
+[0x3B8F42, 0xFC, 0x7F, 0],
+[0x3D1094, 0x40, 0x6B, 0],
+[0x3D109A, 0x2A, 0x46, 0],
+[0x3D1098, 0xC9, 0x5A, 0],
+[0x3D1096, 0x8B, 0x77, 0],
+[0x3D1092, 0xFC, 0x7F, 0],
+[0x3D1090, 0xFA, 0x7F, 0],
+[0x3D10D4, 0x40, 0x6B, 1],
+[0x3D10DA, 0x2A, 0x46, 1],
+[0x3D10D8, 0xC9, 0x5A, 1],
+[0x3D10D6, 0x8B, 0x77, 1],
+[0x3D10D2, 0xFC, 0x7F, 1],
+[0x3D10D0, 0xFA, 0x7F, 1],
+[0x3D14D0, 0xED, 0x7F, 0],
+[0x3D14D2, 0x8B, 0x73, 0],
+[0x3D14CE, 0xFA, 0x7F, 0],
+[0x3D14CC, 0xD6, 0x7B, 0],
+[0x3D1510, 0x60, 0x6F, 1],
+[0x3D1512, 0x8B, 0x73, 1],
+[0x3D150E, 0xFA, 0x7F, 1],
+[0x3D150C, 0xD6, 0x7B, 1],
+[0x3CE0F4, 0xAC, 0x56, 1],
+[0x3CE0F2, 0x8B, 0x73, 1],
+[0x3CE0F0, 0xD6, 0x7B, 1],
+[0x3CE0D4, 0xAC, 0x56, 0],
+[0x3CE0D2, 0x8B, 0x73, 0],
+[0x3CE0D0, 0xD6, 0x7B, 0]]
+
+blackHat = [
+[0x3CC884, 0x84, 0x10, 0],
+[0x3CC886, 0x63, 0x0C, 0],
+[0x3CC8C4, 0x84, 0x10, 1],
+[0x3CC8C6, 0x63, 0x0C, 1],
+[0x4F4CDC, 0x84, 0x10, 0],
+[0x4F4CDE, 0x63, 0x0C, 0],
+[0x4F4D1C, 0x84, 0x10, 1],
+[0x4F4D1E, 0x63, 0x0C, 1],
+[0x4F51D8, 0x84, 0x10, 1],
+[0x4F51DC, 0x84, 0x10, 0],
+[0x4F51E8, 0x08, 0x21, 0],
+[0x4F51EA, 0x08, 0x21, 1],
+[0x4F51DA, 0x4A, 0x29, 1],
+[0x4F51DE, 0x4A, 0x29, 0],
+[0x4FB686, 0x84, 0x10, 0],
+[0x4FB6A6, 0x84, 0x10, 1],
+[0x4FB684, 0x21, 0x04, 0],
+[0x4FB6A4, 0x21, 0x04, 1],
+[0x4FB688, 0x4A, 0x29, 0],
+[0x4FB6A8, 0x4A, 0x29, 1],
+[0x4FB786, 0x84, 0x10, 0],
+[0x4FB788, 0x4A, 0x29, 0],
+[0x4FB78A, 0x73, 0x4E, 0],
+[0x4FB7A6, 0x84, 0x10, 1],
+[0x4FB7A8, 0x4A, 0x29, 1],
+[0x4FB7AA, 0x73, 0x4E, 1],
+[0x51901C, 0x84, 0x10, 0],
+[0x519018, 0x42, 0x08, 0],
+[0x51901A, 0x63, 0x0C, 0],
+[0x51903C, 0x84, 0x10, 1],
+[0x519038, 0x42, 0x08, 1],
+[0x51903A, 0x63, 0x0C, 1],
+[0x5193EA, 0x63, 0x0C, 0],
+[0x5193E8, 0x84, 0x10, 0],
+[0x51940A, 0x63, 0x0C, 1],
+[0x519408, 0x84, 0x10, 1],
+[0x3A72AE, 0x84, 0x10, 1],
+[0x3A7244, 0xA5, 0x14, 0],
+[0x3A724C, 0x08, 0x21, 1],
+[0x3A7290, 0xC6, 0x18, 0],
+[0x3A72B2, 0xA5, 0x14, 1],
+[0x3A7270, 0x63, 0x0C, 1],
+[0x3A7288, 0x63, 0x0C, 0],
+[0x3A7296, 0x29, 0x25, 0],
+[0x3A7274, 0xA5, 0x14, 1],
+[0x3A7294, 0x08, 0x21, 0],
+[0x3A724A, 0x6B, 0x2D, 0],
+[0x3A7278, 0xE7, 0x1C, 1],
+[0x3A724E, 0xCE, 0x39, 0],
+[0x3A727A, 0x29, 0x25, 1],
+[0x3A7252, 0x10, 0x42, 0],
+[0x3A727C, 0xAD, 0x35, 1],
+[0x3A72BC, 0xAD, 0x35, 1],
+[0x3A726C, 0x21, 0x04, 1],
+[0x3A7286, 0x21, 0x04, 0],
+[0x3A728C, 0x84, 0x10, 0],
+[0x3A7272, 0x84, 0x10, 1],
+[0x3A7254, 0x31, 0x46, 1],
+[0x3A7256, 0x52, 0x4A, 0],
+[0x3A7264, 0x31, 0x46, 1],
+[0x9F9A58, 0x84, 0x10, 0],
+[0x9F9A5E, 0x84, 0x10, 1],
+[0x9F9A2A, 0x84, 0x10, 0],
+[0x9F9A30, 0x84, 0x10, 1],
+[0x9F9A36, 0x84, 0x10, 0],
+[0x9F9A3C, 0x84, 0x10, 1],
+[0x9F9A4A, 0x84, 0x10, 0],
+[0x9F9A50, 0x84, 0x10, 1],
+[0x9F9A56, 0x63, 0x0C, 0],
+[0x9F9A5C, 0x63, 0x0C, 1],
+[0x9F9A28, 0x21, 0x04, 0],
+[0x9F9A2E, 0x21, 0x04, 1],
+[0x9F9A48, 0x21, 0x04, 0],
+[0x9F9A4E, 0x21, 0x04, 1],
+[0x9F9A34, 0x63, 0x0C, 0],
+[0x9F9A3A, 0x63, 0x0C, 1],
+[0x9F9A2C, 0x4A, 0x29, 0],
+[0x9F9A32, 0x4A, 0x29, 1],
+[0x9F9A38, 0x4A, 0x29, 0],
+[0x9F9A3E, 0x4A, 0x29, 1],
+[0x9F9A4C, 0x4A, 0x29, 0],
+[0x9F9A52, 0x4A, 0x29, 1],
+[0xA5DD46, 0x63, 0x0C, 0],
+[0xA5DD44, 0x00, 0x00, 0],
+[0xA5DD4A, 0x6B, 0x2D, 0],
+[0xA5DD3E, 0x21, 0x04, 0],
+[0xA5DD40, 0xEF, 0x3D, 0],
+[0xA5DD42, 0x52, 0x4A, 0],
+[0xA5DD48, 0xE7, 0x1C, 0],
+[0xA5DD4E, 0xFF, 0x7F, 0],
+[0xA5DD66, 0x63, 0x0C, 1],
+[0xA5DD64, 0x00, 0x00, 1],
+[0xA5DD6A, 0x6B, 0x2D, 1],
+[0xA5DD5E, 0x21, 0x04, 1],
+[0xA5DD60, 0xEF, 0x3D, 1],
+[0xA5DD62, 0x52, 0x4A, 1],
+[0xA5DD68, 0xE7, 0x1C, 1],
+[0x3D0E0C, 0x84, 0x10, 1],
+[0x3D0E18, 0x84, 0x10, 0],
+[0x3D0E0A, 0x63, 0x0C, 1],
+[0x3D0E1A, 0x63, 0x0C, 0],
+[0x3D0E08, 0x4A, 0x29, 1],
+[0x3D0E16, 0x4A, 0x29, 0],
+[0x3CC9C4, 0x84, 0x10, 0],
+[0x3CC9C6, 0x63, 0x0C, 0],
+[0x3CDE7C, 0x84, 0x10, 1],
+[0x3CDE7E, 0x63, 0x0C, 1],
+[0x51AD2C, 0x84, 0x10, 0],
+[0x51AD2E, 0x63, 0x0C, 0],
+[0x51AD4C, 0x84, 0x10, 1],
+[0x51AD4E, 0x63, 0x0C, 1],
+[0x51A4AE, 0x63, 0x0C, 0],
+[0x51A4AC, 0x84, 0x10, 0],
+[0x51A4CC, 0x84, 0x10, 1],
+[0x51A4CE, 0x63, 0x0C, 1],
+[0x51A98C, 0x84, 0x10, 0],
+[0x51A98E, 0x63, 0x0C, 0],
+[0x51A96C, 0x84, 0x10, 1],
+[0x51A96E, 0x63, 0x0C, 1],
+[0x51AA00, 0x63, 0x0C, 0],
+[0x51AA10, 0x63, 0x0C, 1],
+[0x51AA02, 0x42, 0x08, 0],
+[0x51AA12, 0x63, 0x0C, 1],
+[0x51A9FE, 0x84, 0x10, 0],
+[0x51AA0E, 0x84, 0x10, 1],
+[0x51AA0A, 0x31, 0x46, 0],
+[0x3CC984, 0x84, 0x10, 0],
+[0x3CC986, 0x63, 0x0C, 0],
+[0x3D4B52, 0x84, 0x10, 0],
+[0x3D4B5C, 0x00, 0x00, 0],
+[0x3D4B54, 0x84, 0x10, 0],
+[0x3D4B56, 0x63, 0x0C, 0],
+[0x3D4B50, 0x4A, 0x29, 0],
+[0x3CCA44, 0x84, 0x10, 1],
+[0x3CCA46, 0x63, 0x0C, 1],
+[0x3CFB3C, 0x84, 0x10, 0],
+[0x3CFB3E, 0x63, 0x0C, 0],
+[0x3CFB7C, 0x84, 0x10, 1],
+[0x3CFB7E, 0x63, 0x0C, 1],
+[0x3D504C, 0xE7, 0x1C, 0],
+[0x3D504A, 0x6B, 0x2D, 0],
+[0x3D504E, 0x63, 0x0C, 0],
+[0x3D508C, 0xE7, 0x1C, 1],
+[0x3D508A, 0x6B, 0x2D, 1],
+[0x3D508E, 0x63, 0x0C, 1],
+[0xA5DDA2, 0x63, 0x0C, 0],
+[0xA5DDC2, 0x63, 0x0C, 1],
+[0xA5DDA6, 0x6B, 0x2D, 0],
+[0xA5DDC6, 0x6B, 0x2D, 1],
+[0xA5DDA4, 0xE7, 0x1C, 0],
+[0xA5DDC4, 0xE7, 0x1C, 1],
+[0xA5DDA8, 0xEF, 0x3D, 0],
+[0xA5DDC8, 0xEF, 0x3D, 1],
+[0x3D3E0C, 0x84, 0x10, 1],
+[0x3D3E10, 0x84, 0x10, 0],
+[0x3D3E0E, 0x63, 0x0C, 1],
+[0x3D3E12, 0x63, 0x0C, 0],
+[0x3CF1C0, 0x84, 0x10, 0],
+[0x3CF200, 0x84, 0x10, 1],
+[0x3CF1C2, 0x63, 0x0C, 0],
+[0x3CF202, 0x63, 0x0C, 1],
+[0x3D360E, 0x08, 0x21, 1],
+[0x3D3614, 0x08, 0x21, 0],
+[0x3D360C, 0x4A, 0x29, 1],
+[0x3D3612, 0x4A, 0x29, 0],
+[0x3D3604, 0xE7, 0x1C, 0],
+[0x3D3606, 0xE7, 0x1C, 1],
+[0x3D360A, 0x4A, 0x29, 1],
+[0x3D3610, 0x4A, 0x29, 0],
+[0x3D1A48, 0x84, 0x10, 0],
+[0x3D1A46, 0x4A, 0x29, 0],
+[0x3D1A44, 0x4A, 0x29, 0],
+[0x3D1A4A, 0xE7, 0x1C, 0],
+[0x3D1A88, 0x84, 0x10, 1],
+[0x3D1A8A, 0xEE, 0xE4, 1],
+[0x3D1A86, 0x4A, 0x29, 1],
+[0x3D1A84, 0x4A, 0x29, 1],
+[0x3CE282, 0x63, 0x0C, 0],
+[0x3CE2C2, 0x63, 0x0C, 1],
+[0x3CE280, 0x4A, 0x29, 0],
+[0x3CE2C0, 0x4A, 0x29, 1],
+[0x4FA29E, 0x63, 0x0C, 0],
+[0x4FA2DE, 0x63, 0x0C, 1],
+[0x4FA29C, 0x4A, 0x29, 0],
+[0x4FA2DC, 0x4A, 0x29, 1],
+[0x3D4786, 0x31, 0x46, 1],
+[0x3D478C, 0x31, 0x46, 0],
+[0x3D478E, 0xC6, 0x18, 0],
+[0x3D4788, 0xC6, 0x18, 1],
+[0x3D4790, 0xC6, 0x18, 0],
+[0x3D478A, 0xE7, 0x1C, 1],
+[0x3D4794, 0xE7, 0x1C, 0],
+[0x3D4792, 0xE7, 0x1C, 0],
+[0x3D4784, 0xFE, 0x33, 0],
+[0x3C9E3A, 0x73, 0x4E, 1],
+[0x3C9E40, 0x73, 0x4E, 0],
+[0x3C9E38, 0x73, 0x4E, 1],
+[0x3C9E3E, 0x73, 0x4E, 0],
+[0x3C9E36, 0xFC, 0x7F, 1],
+[0x3C9E3C, 0xFC, 0x7F, 0],
+[0x4F4D5C, 0x84, 0x10, 0],
+[0x4F4D5E, 0x63, 0x0C, 0],
+[0x3C9320, 0x84, 0x10, 0],
+[0x3C9322, 0x63, 0x0C, 0],
+[0x9F9CF6, 0x84, 0x10, 0],
+[0x9F9CF8, 0x63, 0x0C, 0],
+[0x4F4E1C, 0x84, 0x10, 1],
+[0x4F4E1E, 0x63, 0x0C, 1],
+[0x3C9450, 0x84, 0x10, 1],
+[0x3C9452, 0x63, 0x0C, 1],
+[0x9F9D74, 0x84, 0x10, 1],
+[0x9F9D76, 0x63, 0x0C, 1],
+[0xA6202E, 0x63, 0x0C, 0],
+[0xA62032, 0x6B, 0x2D, 0],
+[0xA62030, 0xE7, 0x1C, 0],
+[0xA62034, 0xEF, 0x3D, 0],
+[0xA6204E, 0x63, 0x0C, 1],
+[0xA62052, 0x6B, 0x2D, 1],
+[0xA62050, 0xE7, 0x1C, 1],
+[0xA62054, 0xEF, 0x3D, 1],
+[0x3D4812, 0x08, 0x21, 0],
+[0x3D480E, 0x08, 0x21, 1],
+[0x3D4810, 0x4A, 0x29, 0],
+[0x3D480C, 0x4A, 0x29, 1],
+[0x3CC9FE, 0x84, 0x10, 0],
+[0x3CCA0A, 0x63, 0x0C, 0],
+[0x8CBE20, 0x84, 0x10, 0],
+[0x8CBE22, 0x63, 0x0C, 0],
+[0x8CBE1E, 0x31, 0x46, 0],
+[0x8CBE40, 0x84, 0x10, 1],
+[0x8CBE42, 0x63, 0x0C, 1],
+[0x8CBE3E, 0x31, 0x46, 1],
+[0x8CBEE0, 0x84, 0x10, 0],
+[0x8CBEDE, 0x52, 0x4A, 0],
+[0x8CBEE2, 0x63, 0x0C, 0],
+[0x3B8F38, 0x84, 0x10, 1],
+[0x3B8F3A, 0x52, 0x4A, 1],
+[0x3B8F40, 0x84, 0x10, 0],
+[0x3B8F42, 0x52, 0x4A, 0],
+[0x3D1094, 0xC6, 0x18, 0],
+[0x3D109A, 0x00, 0x00, 0],
+[0x3D1098, 0x42, 0x08, 0],
+[0x3D1096, 0x63, 0x0C, 0],
+[0x3D1092, 0x52, 0x4A, 0],
+[0x3D1090, 0x4A, 0x29, 0],
+[0x3D10D4, 0xC6, 0x18, 1],
+[0x3D10DA, 0x00, 0x00, 1],
+[0x3D10D8, 0x42, 0x08, 1],
+[0x3D10D6, 0x63, 0x0C, 1],
+[0x3D10D2, 0x52, 0x4A, 1],
+[0x3D10D0, 0x4A, 0x29, 1],
+[0x3D14D0, 0x84, 0x10, 0],
+[0x3D14D2, 0x63, 0x0C, 0],
+[0x3D14CE, 0x4A, 0x29, 0],
+[0x3D14CC, 0x08, 0x21, 0],
+[0x3D1510, 0x08, 0x21, 1],
+[0x3D1512, 0x63, 0x0C, 1],
+[0x3D150E, 0x4A, 0x29, 1],
+[0x3D150C, 0x08, 0x21, 1],
+[0x3CE0F4, 0x63, 0x0C, 1],
+[0x3CE0F2, 0x63, 0x0C, 1],
+[0x3CE0F0, 0x08, 0x21, 1],
+[0x3CE0D4, 0x63, 0x0C, 0],
+[0x3CE0D2, 0x63, 0x0C, 0],
+[0x3CE0D0, 0x08, 0x21, 0]]
+
+blueHat = [
+[0x3CC884, 0x00, 0x7C, 0],
+[0x3CC886, 0x00, 0x5C, 0],
+[0x3CC8C4, 0x00, 0x7C, 1],
+[0x3CC8C6, 0x00, 0x5C, 1],
+[0x4F4CDC, 0x00, 0x7C, 0],
+[0x4F4CDE, 0x00, 0x5C, 0],
+[0x4F4D1C, 0x00, 0x7C, 1],
+[0x4F4D1E, 0x00, 0x5C, 1],
+[0x4F51D8, 0x00, 0x7C, 1],
+[0x4F51DC, 0x00, 0x7C, 0],
+[0x4F51E8, 0x73, 0x72, 0],
+[0x4F51EA, 0x73, 0x72, 1],
+[0x4F51DA, 0xB5, 0x7E, 1],
+[0x4F51DE, 0xB5, 0x7E, 0],
+[0x4FB686, 0x00, 0x7C, 0],
+[0x4FB6A6, 0x00, 0x7C, 1],
+[0x4FB684, 0xA5, 0x38, 0],
+[0x4FB6A4, 0xA5, 0x38, 1],
+[0x4FB688, 0xB5, 0x7E, 0],
+[0x4FB6A8, 0xB5, 0x7E, 1],
+[0x4FB786, 0x00, 0x7C, 0],
+[0x4FB788, 0xB5, 0x7E, 0],
+[0x4FB78A, 0x9C, 0x7F, 0],
+[0x4FB7A6, 0x00, 0x7C, 1],
+[0x4FB7A8, 0xB5, 0x7E, 1],
+[0x4FB7AA, 0x9C, 0x7F, 1],
+[0x51901C, 0x00, 0x7C, 0],
+[0x519018, 0x00, 0x50, 0],
+[0x51901A, 0x00, 0x5C, 0],
+[0x51903C, 0x00, 0x7C, 1],
+[0x519038, 0x00, 0x50, 1],
+[0x51903A, 0x00, 0x5C, 1],
+[0x5193EA, 0x00, 0x5C, 0],
+[0x5193E8, 0x00, 0x7C, 0],
+[0x51940A, 0x00, 0x5C, 1],
+[0x519408, 0x00, 0x7C, 1],
+[0x3A72AE, 0xC5, 0x4C, 1],
+[0x3A7244, 0xC5, 0x4C, 0],
+[0x3A724C, 0x07, 0x5D, 1],
+[0x3A7290, 0xE6, 0x54, 0],
+[0x3A72B2, 0xE6, 0x54, 1],
+[0x3A7270, 0x42, 0x3C, 1],
+[0x3A7288, 0x42, 0x3C, 0],
+[0x3A7296, 0x07, 0x65, 0],
+[0x3A7274, 0x07, 0x65, 1],
+[0x3A7294, 0x07, 0x5D, 0],
+[0x3A724A, 0x28, 0x71, 0],
+[0x3A7278, 0x28, 0x71, 1],
+[0x3A724E, 0x49, 0x7D, 0],
+[0x3A727A, 0x49, 0x7D, 1],
+[0x3A7252, 0xAC, 0x7D, 0],
+[0x3A727C, 0xAC, 0x7D, 1],
+[0x3A72BC, 0xAC, 0x7D, 1],
+[0x3A726C, 0x00, 0x38, 1],
+[0x3A7286, 0x00, 0x38, 0],
+[0x3A728C, 0x84, 0x44, 0],
+[0x3A7272, 0x84, 0x44, 1],
+[0x3A7254, 0x0F, 0x7E, 1],
+[0x3A7256, 0x0F, 0x7E, 0],
+[0x3A7264, 0x0F, 0x7E, 1],
+[0x9F9A58, 0xAD, 0x4D, 0],
+[0x9F9A5E, 0xAD, 0x4D, 1],
+[0x9F9A2A, 0x00, 0x7C, 0],
+[0x9F9A30, 0x00, 0x7C, 1],
+[0x9F9A36, 0x00, 0x7C, 0],
+[0x9F9A3C, 0x00, 0x7C, 1],
+[0x9F9A4A, 0x00, 0x7C, 0],
+[0x9F9A50, 0x00, 0x7C, 1],
+[0x9F9A56, 0x00, 0x54, 0],
+[0x9F9A5C, 0x00, 0x54, 1],
+[0x9F9A28, 0xA5, 0x38, 0],
+[0x9F9A2E, 0xA5, 0x38, 1],
+[0x9F9A48, 0xA5, 0x38, 0],
+[0x9F9A4E, 0xA5, 0x38, 1],
+[0x9F9A34, 0x00, 0x5C, 0],
+[0x9F9A3A, 0x00, 0x5C, 1],
+[0x9F9A2C, 0xB5, 0x7E, 0],
+[0x9F9A32, 0xB5, 0x7E, 1],
+[0x9F9A38, 0xB5, 0x7E, 0],
+[0x9F9A3E, 0xB5, 0x7E, 1],
+[0x9F9A4C, 0xB5, 0x7E, 0],
+[0x9F9A52, 0xB5, 0x7E, 1],
+[0xA5DD46, 0x8C, 0x5D, 0],
+[0xA5DD44, 0x4A, 0x4D, 0],
+[0xA5DD4A, 0x31, 0x7E, 0],
+[0xA5DD3E, 0xAD, 0x4D, 0],
+[0xA5DD40, 0xD6, 0x7E, 0],
+[0xA5DD42, 0x7B, 0x7F, 0],
+[0xA5DD48, 0xEF, 0x71, 0],
+[0xA5DD4E, 0xFF, 0x7F, 0],
+[0xA5DD66, 0x8C, 0x5D, 1],
+[0xA5DD64, 0x4A, 0x4D, 1],
+[0xA5DD6A, 0x31, 0x7E, 1],
+[0xA5DD5E, 0xAD, 0x4D, 1],
+[0xA5DD60, 0xD6, 0x7E, 1],
+[0xA5DD62, 0x7B, 0x7F, 1],
+[0xA5DD68, 0xEF, 0x71, 1],
+[0x3D0E0C, 0x00, 0x7C, 1],
+[0x3D0E18, 0x00, 0x7C, 0],
+[0x3D0E0A, 0x00, 0x5C, 1],
+[0x3D0E1A, 0x00, 0x5C, 0],
+[0x3D0E08, 0xB5, 0x7E, 1],
+[0x3D0E16, 0xB5, 0x7E, 0],
+[0x3CC9C4, 0x00, 0x7C, 0],
+[0x3CC9C6, 0x00, 0x5C, 0],
+[0x3CDE7C, 0x00, 0x7C, 1],
+[0x3CDE7E, 0x00, 0x5C, 1],
+[0x51AD2C, 0x00, 0x7C, 0],
+[0x51AD2E, 0x00, 0x5C, 0],
+[0x51AD4C, 0x00, 0x7C, 1],
+[0x51AD4E, 0x00, 0x5C, 1],
+[0x51A4AE, 0x00, 0x5C, 0],
+[0x51A4AC, 0x00, 0x7C, 0],
+[0x51A4CC, 0x00, 0x7C, 1],
+[0x51A4CE, 0x00, 0x5C, 1],
+[0x51A98C, 0x00, 0x7C, 0],
+[0x51A98E, 0x00, 0x5C, 0],
+[0x51A96C, 0x00, 0x7C, 1],
+[0x51A96E, 0x00, 0x5C, 1],
+[0x51AA00, 0x00, 0x70, 0],
+[0x51AA10, 0x00, 0x70, 1],
+[0x51AA02, 0x00, 0x5C, 0],
+[0x51AA12, 0x00, 0x5C, 1],
+[0x51A9FE, 0x00, 0x7C, 0],
+[0x51AA0E, 0x00, 0x7C, 1],
+[0x51AA0A, 0x0F, 0x7E, 0],
+[0x3CC984, 0x00, 0x7C, 0],
+[0x3CC986, 0x00, 0x5C, 0],
+[0x3D4B52, 0x00, 0x70, 0],
+[0x3D4B5C, 0x00, 0x50, 0],
+[0x3D4B54, 0x00, 0x7C, 0],
+[0x3D4B56, 0x00, 0x5C, 0],
+[0x3D4B50, 0xB5, 0x7E, 0],
+[0x3CCA44, 0x00, 0x7C, 1],
+[0x3CCA46, 0x00, 0x5C, 1],
+[0x3CFB3C, 0x00, 0x7C, 0],
+[0x3CFB3E, 0x00, 0x5C, 0],
+[0x3CFB7C, 0x00, 0x7C, 1],
+[0x3CFB7E, 0x00, 0x5C, 1],
+[0x3D504C, 0x00, 0x6C, 0],
+[0x3D504A, 0x00, 0x7C, 0],
+[0x3D504E, 0x00, 0x5C, 0],
+[0x3D508C, 0x00, 0x6C, 1],
+[0x3D508A, 0x00, 0x7C, 1],
+[0x3D508E, 0x00, 0x5C, 1],
+[0xA5DDA2, 0x8C, 0x5D, 0],
+[0xA5DDC2, 0x8C, 0x5D, 1],
+[0xA5DDA6, 0x31, 0x7E, 0],
+[0xA5DDC6, 0x31, 0x7E, 1],
+[0xA5DDA4, 0xEF, 0x71, 0],
+[0xA5DDC4, 0xEF, 0x71, 1],
+[0xA5DDA8, 0xB5, 0x7E, 0],
+[0xA5DDC8, 0xB5, 0x7E, 1],
+[0x3D3E0C, 0x00, 0x7C, 1],
+[0x3D3E10, 0x00, 0x7C, 0],
+[0x3D3E0E, 0x00, 0x5C, 1],
+[0x3D3E12, 0x00, 0x5C, 0],
+[0x3CF1C0, 0x00, 0x7C, 0],
+[0x3CF200, 0x00, 0x7C, 1],
+[0x3CF1C2, 0x00, 0x5C, 0],
+[0x3CF202, 0x00, 0x5C, 1],
+[0x3D360E, 0x07, 0x5D, 1],
+[0x3D3614, 0x07, 0x5D, 0],
+[0x3D360C, 0xB5, 0x7E, 1],
+[0x3D3612, 0xB5, 0x7E, 0],
+[0x3D3604, 0xEF, 0x71, 0],
+[0x3D3606, 0xEF, 0x71, 1],
+[0x3D360A, 0xB5, 0x7E, 1],
+[0x3D3610, 0xB5, 0x7E, 0],
+[0x3D1A48, 0x84, 0x44, 0],
+[0x3D1A46, 0xB5, 0x7E, 0],
+[0x3D1A44, 0xB5, 0x7E, 0],
+[0x3D1A4A, 0xEF, 0x71, 0],
+[0x3D1A88, 0x84, 0x44, 1],
+[0x3D1A8A, 0xCC, 0xC4, 1],
+[0x3D1A86, 0xB5, 0x7E, 1],
+[0x3D1A84, 0xB5, 0x7E, 1],
+[0x3CE282, 0x00, 0x5C, 0],
+[0x3CE2C2, 0x00, 0x5C, 1],
+[0x3CE280, 0xB5, 0x7E, 0],
+[0x3CE2C0, 0xB5, 0x7E, 1],
+[0x4FA29E, 0x00, 0x5C, 0],
+[0x4FA2DE, 0x00, 0x5C, 1],
+[0x4FA29C, 0xB5, 0x7E, 0],
+[0x4FA2DC, 0xB5, 0x7E, 1],
+[0x3D4786, 0x0F, 0x7E, 1],
+[0x3D478C, 0x0F, 0x7E, 0],
+[0x3D478E, 0xE6, 0x54, 0],
+[0x3D4788, 0xE6, 0x54, 1],
+[0x3D4790, 0xE6, 0x54, 0],
+[0x3D478A, 0x00, 0x6C, 1],
+[0x3D4794, 0xEF, 0x71, 0],
+[0x3D4792, 0x00, 0x6C, 0],
+[0x3D4784, 0xFE, 0x33, 0],
+[0x3C9E3A, 0x9C, 0x7F, 1],
+[0x3C9E40, 0x9C, 0x7F, 0],
+[0x3C9E38, 0x9C, 0x7F, 1],
+[0x3C9E3E, 0x9C, 0x7F, 0],
+[0x3C9E36, 0xFC, 0x7F, 1],
+[0x3C9E3C, 0xFC, 0x7F, 0],
+[0x4F4D5C, 0x00, 0x7C, 0],
+[0x4F4D5E, 0x00, 0x5C, 0],
+[0x3C9320, 0x00, 0x7C, 0],
+[0x3C9322, 0x00, 0x5C, 0],
+[0x9F9CF6, 0x00, 0x7C, 0],
+[0x9F9CF8, 0x00, 0x5C, 0],
+[0x4F4E1C, 0x00, 0x7C, 1],
+[0x4F4E1E, 0x00, 0x5C, 1],
+[0x3C9450, 0x00, 0x7C, 1],
+[0x3C9452, 0x00, 0x5C, 1],
+[0x9F9D74, 0x00, 0x7C, 1],
+[0x9F9D76, 0x00, 0x5C, 1],
+[0xA6202E, 0x8C, 0x5D, 0],
+[0xA62032, 0x31, 0x7E, 0],
+[0xA62030, 0xEF, 0x71, 0],
+[0xA62034, 0xB5, 0x7E, 0],
+[0xA6204E, 0x8C, 0x5D, 1],
+[0xA62052, 0x31, 0x7E, 1],
+[0xA62050, 0xEF, 0x71, 1],
+[0xA62054, 0xB5, 0x7E, 1],
+[0x3D4812, 0x07, 0x5D, 0],
+[0x3D480E, 0x07, 0x5D, 1],
+[0x3D4810, 0xB5, 0x7E, 0],
+[0x3D480C, 0xB5, 0x7E, 1],
+[0x3CC9FE, 0x00, 0x7C, 0],
+[0x3CCA0A, 0x00, 0x5C, 0],
+[0x8CBE20, 0x84, 0x44, 0],
+[0x8CBE22, 0x00, 0x5C, 0],
+[0x8CBE1E, 0x0F, 0x7E, 0],
+[0x8CBE40, 0x84, 0x44, 1],
+[0x8CBE42, 0x00, 0x5C, 1],
+[0x8CBE3E, 0x0F, 0x7E, 1],
+[0x8CBEE0, 0x00, 0x7C, 0],
+[0x8CBEDE, 0x7B, 0x7F, 0],
+[0x8CBEE2, 0x00, 0x5C, 0],
+[0x3B8F38, 0x84, 0x44, 1],
+[0x3B8F3A, 0x7B, 0x7F, 1],
+[0x3B8F40, 0x84, 0x44, 0],
+[0x3B8F42, 0x7B, 0x7F, 0],
+[0x3D1094, 0xE6, 0x54, 0],
+[0x3D109A, 0x4A, 0x4D, 0],
+[0x3D1098, 0x00, 0x50, 0],
+[0x3D1096, 0x00, 0x5C, 0],
+[0x3D1092, 0x7B, 0x7F, 0],
+[0x3D1090, 0xB5, 0x7E, 0],
+[0x3D10D4, 0xE6, 0x54, 1],
+[0x3D10DA, 0x4A, 0x4D, 1],
+[0x3D10D8, 0x00, 0x50, 1],
+[0x3D10D6, 0x00, 0x5C, 1],
+[0x3D10D2, 0x7B, 0x7F, 1],
+[0x3D10D0, 0xB5, 0x7E, 1],
+[0x3D14D0, 0x00, 0x7C, 0],
+[0x3D14D2, 0x00, 0x5C, 0],
+[0x3D14CE, 0xB5, 0x7E, 0],
+[0x3D14CC, 0x73, 0x72, 0],
+[0x3D1510, 0x07, 0x5D, 1],
+[0x3D1512, 0x00, 0x5C, 1],
+[0x3D150E, 0xB5, 0x7E, 1],
+[0x3D150C, 0x73, 0x72, 1],
+[0x3CE0F4, 0x8C, 0x5D, 1],
+[0x3CE0F2, 0x00, 0x70, 1],
+[0x3CE0F0, 0x73, 0x72, 1],
+[0x3CE0D4, 0x8C, 0x5D, 0],
+[0x3CE0D2, 0x00, 0x70, 0],
+[0x3CE0D0, 0x73, 0x72, 0]]
+
+chaosHat = [
+[0x3CC884, 0],
+[0x3CC886, 0],
+[0x3CC984, 0],
+[0x3CC986, 0],
+[0x3D0E16, 0],
+[0x3D0E18, 0],
+[0x3D0E1A, 0],
+[0x3D4B50, 0],
+[0x3D4B52, 0],
+[0x3D4B54, 0],
+[0x3D4B56, 0],
+[0x4F4CDC, 0],
+[0x4F4CDE, 0],
+[0x4F51DC, 0],
+[0x4F51DE, 0],
+[0x4F51E8, 0],
+[0x4fb684, 0],
+[0x4fb686, 0],
+[0x4fb688, 0],
+[0x4FB786, 0],
+[0x4fb788, 0],
+[0x4fb78a, 0],
+[0x519018, 0],
+[0x51901a, 0],
+[0x51901c, 0],
+[0x5193E8, 0],
+[0x5193EA, 0],
+[0x51A4AC, 0],
+[0x51A4AE, 0],
+[0x51A98C, 0],
+[0x51A98E, 0],
+[0x51AD2C, 0],
+[0x51AD2E, 0],
+[0x9F9A28, 0],
+[0x9F9A2A, 0],
+[0x9F9A2C, 0],
+[0x9F9A34, 0],
+[0x9F9A36, 0],
+[0x9F9A38, 0],
+[0x9F9A48, 0],
+[0x9F9A4A, 0],
+[0x9F9A4C, 0],
+[0x9F9A56, 0],
+[0x9F9A58, 0],
+[0xA5DD3E, 0],
+[0xA5DD40, 0],
+[0xA5DD42, 0],
+[0xA5DD44, 0],
+[0xA5DD46, 0],
+[0xA5DD48, 0],
+[0xA5DD4A, 0],
+[0x3CC8C4, 1],
+[0x3CC8C6, 1],
+[0x3D0E08, 1],
+[0x3D0E0A, 1],
+[0x3D0E0C, 1],
+[0x4F4D1C, 1],
+[0x4F4D1E, 1],
+[0x4F51D8, 1],
+[0x4F51DA, 1],
+[0x4F51EA, 1],
+[0x4fb6a4, 1],
+[0x4fb6a6, 1],
+[0x4fb6a8, 1],
+[0x4FB7A6, 1],
+[0x4FB7A8, 1],
+[0x4FB7AA, 1],
+[0x519038, 1],
+[0x51903A, 1],
+[0x51903c, 1],
+[0x519408, 1],
+[0x51940a, 1],
+[0x51A4CC, 1],
+[0x51A4CE, 1],
+[0x51A96C, 1],
+[0x51A96E, 1],
+[0x51AD4C, 1],
+[0x51AD4E, 1],
+[0x9F9A2E, 1],
+[0x9F9A30, 1],
+[0x9F9A32, 1],
+[0x9F9A3A, 1],
+[0x9F9A3C, 1],
+[0x9F9A3E, 1],
+[0x9F9A4E, 1],
+[0x9F9A50, 1],
+[0x9F9A52, 1],
+[0x9F9A5C, 1],
+[0x9F9A5E, 1],
+[0xA5DD5E, 1],
+[0xA5DD60, 1],
+[0xA5DD62, 1],
+[0xA5DD64, 1],
+[0xA5DD66, 1],
+[0xA5DD68, 1],
+[0xA5DD6A, 1],
+[0x3A7256, 0],
+[0x3A7252, 0],
+[0x3A724E, 0],
+[0x3A724A, 0],
+[0x3A7296, 0],
+[0x3A7294, 0],
+[0x3A7290, 0],
+[0x3A7244, 0],
+[0x3A728C, 0],
+[0x3A7288, 0],
+[0x3A7286, 0],
+[0x3A7254, 1],
+[0x3A7264, 1],
+[0x3A727C, 1],
+[0x3A72BC, 1],
+[0x3A727A, 1],
+[0x3A7278, 1],
+[0x3A7274, 1],
+[0x3A724C, 1],
+[0x3A72B2, 1],
+[0x3A72AE, 1],
+[0x3A7272, 1],
+[0x3A7270, 1],
+[0x3A726C, 1],
+[0xA5DDA2, 0],
+[0xA5DDA4, 0],
+[0xA5DDA6, 0],
+[0xA5DDA8, 0],
+[0x3D3E10, 0],
+[0x3D3E12, 0],
+[0x3D504A, 0],
+[0x3D504C, 0],
+[0x3D504E, 0],
+[0x3D4B5C, 0],
+[0xA5DDC2, 1],
+[0xA5DDC4, 1],
+[0xA5DDC6, 1],
+[0xA5DDC8, 1],
+[0x3D3E0C, 1],
+[0x3D3E0E, 1],
+[0x3D508A, 1],
+[0x3D508C, 1],
+[0x3D508E, 1],
+[0x51A9FE, 0],
+[0x51AA00, 0],
+[0x51AA02, 0],
+[0x51AA0A, 0],
+[0x51AA0E, 1],
+[0x51AA10, 1],
+[0x51AA12, 1],
+[0x3CFB3C, 0],
+[0x3CFB3E, 0],
+[0x3CC9C4, 0],
+[0x3CC9C6, 0],
+[0x3CFB7C, 1],
+[0x3CFB7E, 1],
+[0x3CCA44, 1],
+[0x3CCA46, 1],
+[0x3CDE7C, 1],
+[0x3CDE7E, 1]]
+
+greenHat = [
+[0x3CC884, 0xA3, 0x03, 0],
+[0x3CC886, 0xE0, 0x02, 0],
+[0x3CC8C4, 0xA3, 0x03, 1],
+[0x3CC8C6, 0xE0, 0x02, 1],
+[0x4F4CDC, 0xA3, 0x03, 0],
+[0x4F4CDE, 0xE0, 0x02, 0],
+[0x4F4D1C, 0xA3, 0x03, 1],
+[0x4F4D1E, 0xE0, 0x02, 1],
+[0x4F51D8, 0xA3, 0x03, 1],
+[0x4F51DC, 0xA3, 0x03, 0],
+[0x4F51E8, 0xB1, 0x3B, 0],
+[0x4F51EA, 0xB1, 0x3B, 1],
+[0x4F51DA, 0xF3, 0x43, 1],
+[0x4F51DE, 0xF3, 0x43, 0],
+[0x4FB686, 0xA3, 0x03, 0],
+[0x4FB6A6, 0xA3, 0x03, 1],
+[0x4FB684, 0xC2, 0x01, 0],
+[0x4FB6A4, 0xC2, 0x01, 1],
+[0x4FB688, 0xF6, 0x5B, 0],
+[0x4FB6A8, 0xF6, 0x5B, 1],
+[0x4FB786, 0xA3, 0x03, 0],
+[0x4FB788, 0xF6, 0x5B, 0],
+[0x4FB78A, 0xFB, 0x6F, 0],
+[0x4FB7A6, 0xA3, 0x03, 1],
+[0x4FB7A8, 0xF6, 0x5B, 1],
+[0x4FB7AA, 0xFB, 0x6F, 1],
+[0x51901C, 0xA3, 0x03, 0],
+[0x519018, 0xE0, 0x01, 0],
+[0x51901A, 0xE0, 0x02, 0],
+[0x51903C, 0xA3, 0x03, 1],
+[0x519038, 0xE0, 0x01, 1],
+[0x51903A, 0xE0, 0x02, 1],
+[0x5193EA, 0xA0, 0x02, 0],
+[0x5193E8, 0xA3, 0x03, 0],
+[0x51940A, 0xA0, 0x02, 1],
+[0x519408, 0xA3, 0x03, 1],
+[0x3A72AE, 0x26, 0x43, 1],
+[0x3A7244, 0x26, 0x43, 0],
+[0x3A724C, 0x34, 0x2F, 1],
+[0x3A7290, 0x4C, 0x3F, 0],
+[0x3A72B2, 0x4C, 0x3F, 1],
+[0x3A7270, 0x4E, 0x2E, 1],
+[0x3A7288, 0x4E, 0x2E, 0],
+[0x3A7296, 0x6D, 0x3F, 0],
+[0x3A7274, 0x6D, 0x3F, 1],
+[0x3A7294, 0x70, 0x3B, 0],
+[0x3A724A, 0x8F, 0x3B, 0],
+[0x3A7278, 0x8F, 0x3B, 1],
+[0x3A724E, 0xB4, 0x33, 0],
+[0x3A727A, 0xB4, 0x33, 1],
+[0x3A7252, 0xD7, 0x2B, 0],
+[0x3A727C, 0xD7, 0x2B, 1],
+[0x3A72BC, 0xD7, 0x2B, 1],
+[0x3A726C, 0xEC, 0x29, 1],
+[0x3A7286, 0xEC, 0x29, 0],
+[0x3A728C, 0xF0, 0x36, 0],
+[0x3A7272, 0xF0, 0x36, 1],
+[0x3A7254, 0xF9, 0x33, 1],
+[0x3A7256, 0xF9, 0x33, 0],
+[0x3A7264, 0xF9, 0x33, 1],
+[0x9F9A58, 0x0C, 0x32, 0],
+[0x9F9A5E, 0x0C, 0x32, 1],
+[0x9F9A2A, 0xA3, 0x03, 0],
+[0x9F9A30, 0xA3, 0x03, 1],
+[0x9F9A36, 0xA3, 0x03, 0],
+[0x9F9A3C, 0xA3, 0x03, 1],
+[0x9F9A4A, 0xA3, 0x03, 0],
+[0x9F9A50, 0xA3, 0x03, 1],
+[0x9F9A56, 0xC1, 0x01, 0],
+[0x9F9A5C, 0xC1, 0x01, 1],
+[0x9F9A28, 0xC2, 0x01, 0],
+[0x9F9A2E, 0xC2, 0x01, 1],
+[0x9F9A48, 0xC2, 0x01, 0],
+[0x9F9A4E, 0xC2, 0x01, 1],
+[0x9F9A34, 0xE2, 0x02, 0],
+[0x9F9A3A, 0xE2, 0x02, 1],
+[0x9F9A2C, 0xF6, 0x5B, 0],
+[0x9F9A32, 0xF6, 0x5B, 1],
+[0x9F9A38, 0xF6, 0x5B, 0],
+[0x9F9A3E, 0xF6, 0x5B, 1],
+[0x9F9A4C, 0xF6, 0x5B, 0],
+[0x9F9A52, 0xF6, 0x5B, 1],
+[0xA5DD46, 0x24, 0x1E, 0],
+[0xA5DD44, 0x82, 0x19, 0],
+[0xA5DD4A, 0x89, 0x3F, 0],
+[0xA5DD3E, 0xA9, 0x2D, 0],
+[0xA5DD40, 0xB0, 0x57, 0],
+[0xA5DD42, 0xD5, 0x5F, 0],
+[0xA5DD48, 0xE7, 0x32, 0],
+[0xA5DD4E, 0xFF, 0x7F, 0],
+[0xA5DD66, 0x24, 0x1E, 1],
+[0xA5DD64, 0x82, 0x19, 1],
+[0xA5DD6A, 0x89, 0x3F, 1],
+[0xA5DD5E, 0xA9, 0x2D, 1],
+[0xA5DD60, 0xB0, 0x57, 1],
+[0xA5DD62, 0xD5, 0x5F, 1],
+[0xA5DD68, 0xE7, 0x32, 1],
+[0x3D0E0C, 0xA3, 0x03, 1],
+[0x3D0E18, 0xA3, 0x03, 0],
+[0x3D0E0A, 0xE0, 0x02, 1],
+[0x3D0E1A, 0xE0, 0x02, 0],
+[0x3D0E08, 0xF6, 0x5B, 1],
+[0x3D0E16, 0xF6, 0x5B, 0],
+[0x3CC9C4, 0xA3, 0x03, 0],
+[0x3CC9C6, 0xE0, 0x02, 0],
+[0x3CDE7C, 0xA3, 0x03, 1],
+[0x3CDE7E, 0xE0, 0x02, 1],
+[0x51AD2C, 0xA3, 0x03, 0],
+[0x51AD2E, 0xE0, 0x02, 0],
+[0x51AD4C, 0xA3, 0x03, 1],
+[0x51AD4E, 0xE0, 0x02, 1],
+[0x51A4AE, 0xA0, 0x02, 0],
+[0x51A4AC, 0xA3, 0x03, 0],
+[0x51A4CC, 0xA3, 0x03, 1],
+[0x51A4CE, 0xE0, 0x02, 1],
+[0x51A98C, 0xA3, 0x03, 0],
+[0x51A98E, 0xE0, 0x02, 0],
+[0x51A96C, 0xA3, 0x03, 1],
+[0x51A96E, 0xE0, 0x02, 1],
+[0x51AA00, 0x60, 0x02, 0],
+[0x51AA10, 0x60, 0x02, 1],
+[0x51AA02, 0x61, 0x01, 0],
+[0x51AA12, 0x61, 0x01, 1],
+[0x51A9FE, 0xA3, 0x03, 0],
+[0x51AA0E, 0xA3, 0x03, 1],
+[0x51AA0A, 0xF0, 0x3B, 0],
+[0x3CC984, 0xA3, 0x03, 0],
+[0x3CC986, 0xE0, 0x02, 0],
+[0x3D4B52, 0x43, 0x03, 0],
+[0x3D4B5C, 0x60, 0x02, 0],
+[0x3D4B54, 0xA3, 0x03, 0],
+[0x3D4B56, 0xE0, 0x02, 0],
+[0x3D4B50, 0xF6, 0x5B, 0],
+[0x3CCA44, 0xA3, 0x03, 1],
+[0x3CCA46, 0xE0, 0x02, 1],
+[0x3CFB3C, 0xA3, 0x03, 0],
+[0x3CFB3E, 0xE0, 0x02, 0],
+[0x3CFB7C, 0xA3, 0x03, 1],
+[0x3CFB7E, 0xE0, 0x02, 1],
+[0x3D504C, 0x41, 0x03, 0],
+[0x3D504A, 0xA3, 0x03, 0],
+[0x3D504E, 0xE0, 0x02, 0],
+[0x3D508C, 0x41, 0x03, 1],
+[0x3D508A, 0xA3, 0x03, 1],
+[0x3D508E, 0xE0, 0x02, 1],
+[0xA5DDA2, 0x24, 0x1E, 0],
+[0xA5DDC2, 0x24, 0x1E, 1],
+[0xA5DDA6, 0x89, 0x3F, 0],
+[0xA5DDC6, 0x89, 0x3F, 1],
+[0xA5DDA4, 0xE7, 0x32, 0],
+[0xA5DDC4, 0xE7, 0x32, 1],
+[0xA5DDA8, 0xF6, 0x63, 0],
+[0xA5DDC8, 0xF6, 0x63, 1],
+[0x3D3E0C, 0xA3, 0x03, 1],
+[0x3D3E10, 0xA3, 0x03, 0],
+[0x3D3E0E, 0xE0, 0x02, 1],
+[0x3D3E12, 0xE0, 0x02, 0],
+[0x3CF1C0, 0xA3, 0x03, 0],
+[0x3CF200, 0xA3, 0x03, 1],
+[0x3CF1C2, 0xE0, 0x02, 0],
+[0x3CF202, 0xE0, 0x02, 1],
+[0x3D360E, 0x00, 0x43, 1],
+[0x3D3614, 0x00, 0x43, 0],
+[0x3D360C, 0x80, 0x4F, 1],
+[0x3D3612, 0x80, 0x4F, 0],
+[0x3D3604, 0xC1, 0x01, 0],
+[0x3D3606, 0xC1, 0x01, 1],
+[0x3D360A, 0xE3, 0x63, 1],
+[0x3D3610, 0xE3, 0x63, 0],
+[0x3D1A48, 0x28, 0x1B, 0],
+[0x3D1A46, 0xF6, 0x5B, 0],
+[0x3D1A44, 0xFC, 0x5F, 0],
+[0x3D1A4A, 0xE7, 0x32, 0],
+[0x3D1A88, 0x28, 0x1B, 1],
+[0x3D1A8A, 0x81, 0x02, 1],
+[0x3D1A86, 0xF6, 0x5B, 1],
+[0x3D1A84, 0xFC, 0x5F, 1],
+[0x3CE282, 0xA0, 0x36, 0],
+[0x3CE2C2, 0xA0, 0x36, 1],
+[0x3CE280, 0xE0, 0x4F, 0],
+[0x3CE2C0, 0xE0, 0x4F, 1],
+[0x4FA29E, 0xA0, 0x36, 0],
+[0x4FA2DE, 0xA0, 0x36, 1],
+[0x4FA29C, 0xE0, 0x4F, 0],
+[0x4FA2DC, 0xE0, 0x4F, 1],
+[0x3D4786, 0xA0, 0x07, 1],
+[0x3D478C, 0xA0, 0x07, 0],
+[0x3D478E, 0xC0, 0x02, 0],
+[0x3D4788, 0xC0, 0x02, 1],
+[0x3D4790, 0xC0, 0x02, 0],
+[0x3D478A, 0xE0, 0x01, 1],
+[0x3D4794, 0xE7, 0x32, 0],
+[0x3D4792, 0xE7, 0x32, 0],
+[0x3D4784, 0xFE, 0x33, 0],
+[0x3C9E3A, 0xF2, 0x73, 1],
+[0x3C9E40, 0xF2, 0x73, 0],
+[0x3C9E38, 0xF6, 0x77, 1],
+[0x3C9E3E, 0xF6, 0x77, 0],
+[0x3C9E36, 0xFC, 0x7F, 1],
+[0x3C9E3C, 0xFC, 0x7F, 0],
+[0x4F4D5C, 0xA3, 0x03, 0],
+[0x4F4D5E, 0xE0, 0x02, 0],
+[0x3C9320, 0xA3, 0x03, 0],
+[0x3C9322, 0xE0, 0x02, 0],
+[0x9F9CF6, 0xA3, 0x03, 0],
+[0x9F9CF8, 0xE0, 0x02, 0],
+[0x4F4E1C, 0xA3, 0x03, 1],
+[0x4F4E1E, 0xE0, 0x02, 1],
+[0x3C9450, 0xA3, 0x03, 1],
+[0x3C9452, 0xE0, 0x02, 1],
+[0x9F9D74, 0xA3, 0x03, 1],
+[0x9F9D76, 0xE0, 0x02, 1],
+[0xA6202E, 0x24, 0x1E, 0],
+[0xA62032, 0x89, 0x3F, 0],
+[0xA62030, 0xE7, 0x32, 0],
+[0xA62034, 0xF6, 0x63, 0],
+[0xA6204E, 0x24, 0x1E, 1],
+[0xA62052, 0x89, 0x3F, 1],
+[0xA62050, 0xE7, 0x32, 1],
+[0xA62054, 0xF6, 0x63, 1],
+[0x3D4812, 0x64, 0x3B, 0],
+[0x3D480E, 0x64, 0x3B, 1],
+[0x3D4810, 0xEF, 0x57, 0],
+[0x3D480C, 0xEF, 0x57, 1],
+[0x3CC9FE, 0xA3, 0x03, 0],
+[0x3CCA0A, 0xE0, 0x02, 0],
+[0x8CBE20, 0x4D, 0x33, 0],
+[0x8CBE22, 0xA7, 0x2A, 0],
+[0x8CBE1E, 0xD3, 0x3F, 0],
+[0x8CBE40, 0x4D, 0x33, 1],
+[0x8CBE42, 0xA7, 0x2A, 1],
+[0x8CBE3E, 0xD3, 0x3F, 1],
+[0x8CBEE0, 0xA3, 0x03, 0],
+[0x8CBEDE, 0xD5, 0x5F, 0],
+[0x8CBEE2, 0xE0, 0x02, 0],
+[0x3B8F38, 0x20, 0x17, 1],
+[0x3B8F3A, 0xD3, 0x63, 1],
+[0x3B8F40, 0x20, 0x17, 0],
+[0x3B8F42, 0xD3, 0x63, 0],
+[0x3D1094, 0x46, 0x1B, 0],
+[0x3D109A, 0x80, 0x01, 0],
+[0x3D1098, 0xE0, 0x01, 0],
+[0x3D1096, 0xE0, 0x02, 0],
+[0x3D1092, 0xED, 0x33, 0],
+[0x3D1090, 0xFA, 0x67, 0],
+[0x3D10D4, 0x46, 0x1B, 1],
+[0x3D10DA, 0x80, 0x01, 1],
+[0x3D10D8, 0xE0, 0x01, 1],
+[0x3D10D6, 0xE0, 0x02, 1],
+[0x3D10D2, 0xED, 0x33, 1],
+[0x3D10D0, 0xFA, 0x67, 1],
+[0x3D14D0, 0xA3, 0x03, 0],
+[0x3D14D2, 0xE0, 0x02, 0],
+[0x3D14CE, 0xF6, 0x5B, 0],
+[0x3D14CC, 0xF2, 0x4B, 0],
+[0x3D1510, 0x40, 0x03, 1],
+[0x3D1512, 0xA0, 0x02, 1],
+[0x3D150E, 0xF6, 0x5B, 1],
+[0x3D150C, 0xF2, 0x4B, 1],
+[0x3CE0F4, 0x40, 0x2A, 1],
+[0x3CE0F2, 0x42, 0x2B, 1],
+[0x3CE0F0, 0xF0, 0x3F, 1],
+[0x3CE0D4, 0x40, 0x2A, 0],
+[0x3CE0D2, 0x42, 0x2B, 0],
+[0x3CE0D0, 0xF0, 0x3F, 0]]
+
+orangeHat = [
+[0x3CC884, 0x7F, 0x02, 0],
+[0x3CC886, 0x3C, 0x02, 0],
+[0x3CC8C4, 0x7F, 0x02, 1],
+[0x3CC8C6, 0x3C, 0x02, 1],
+[0x4F4CDC, 0x7F, 0x02, 0],
+[0x4F4CDE, 0x3C, 0x02, 0],
+[0x4F4D1C, 0x7F, 0x02, 1],
+[0x4F4D1E, 0x3C, 0x02, 1],
+[0x4F51D8, 0x7F, 0x02, 1],
+[0x4F51DC, 0x7F, 0x02, 0],
+[0x4F51E8, 0xFE, 0x42, 0],
+[0x4F51EA, 0xFE, 0x42, 1],
+[0x4F51DA, 0x3E, 0x43, 1],
+[0x4F51DE, 0x3E, 0x43, 0],
+[0x4FB686, 0x7F, 0x02, 0],
+[0x4FB6A6, 0x7F, 0x02, 1],
+[0x4FB684, 0x95, 0x01, 0],
+[0x4FB6A4, 0x95, 0x01, 1],
+[0x4FB688, 0x5E, 0x53, 0],
+[0x4FB6A8, 0x5E, 0x53, 1],
+[0x4FB786, 0x7F, 0x02, 0],
+[0x4FB788, 0x5E, 0x53, 0],
+[0x4FB78A, 0x9D, 0x6B, 0],
+[0x4FB7A6, 0x7F, 0x02, 1],
+[0x4FB7A8, 0x5E, 0x53, 1],
+[0x4FB7AA, 0x9D, 0x6B, 1],
+[0x51901C, 0x7F, 0x02, 0],
+[0x519018, 0x3C, 0x01, 0],
+[0x51901A, 0x3C, 0x02, 0],
+[0x51903C, 0x7F, 0x02, 1],
+[0x519038, 0x3C, 0x01, 1],
+[0x51903A, 0x3C, 0x02, 1],
+[0x5193EA, 0x3C, 0x02, 0],
+[0x5193E8, 0x7F, 0x02, 0],
+[0x51940A, 0x3C, 0x02, 1],
+[0x519408, 0x7F, 0x02, 1],
+[0x3A72AE, 0x56, 0x02, 1],
+[0x3A7244, 0x56, 0x02, 0],
+[0x3A724C, 0xBA, 0x02, 1],
+[0x3A7290, 0x98, 0x02, 0],
+[0x3A72B2, 0x98, 0x02, 1],
+[0x3A7270, 0x14, 0x02, 1],
+[0x3A7288, 0x14, 0x02, 0],
+[0x3A7296, 0xB9, 0x02, 0],
+[0x3A7274, 0xB9, 0x02, 1],
+[0x3A7294, 0xBA, 0x02, 0],
+[0x3A724A, 0xDA, 0x02, 0],
+[0x3A7278, 0xDA, 0x02, 1],
+[0x3A724E, 0xFC, 0x02, 0],
+[0x3A727A, 0xFC, 0x02, 1],
+[0x3A7252, 0x3E, 0x03, 0],
+[0x3A727C, 0x3E, 0x03, 1],
+[0x3A72BC, 0x3E, 0x03, 1],
+[0x3A726C, 0xD1, 0x01, 1],
+[0x3A7286, 0xD1, 0x01, 0],
+[0x3A728C, 0x35, 0x02, 0],
+[0x3A7272, 0x35, 0x02, 1],
+[0x3A7254, 0x5F, 0x03, 1],
+[0x3A7256, 0x5F, 0x03, 0],
+[0x3A7264, 0x5F, 0x03, 1],
+[0x9F9A58, 0x76, 0x3E, 0],
+[0x9F9A5E, 0x76, 0x3E, 1],
+[0x9F9A2A, 0x7F, 0x02, 0],
+[0x9F9A30, 0x7F, 0x02, 1],
+[0x9F9A36, 0x7F, 0x02, 0],
+[0x9F9A3C, 0x7F, 0x02, 1],
+[0x9F9A4A, 0x7F, 0x02, 0],
+[0x9F9A50, 0x7F, 0x02, 1],
+[0x9F9A56, 0xB6, 0x01, 0],
+[0x9F9A5C, 0xB6, 0x01, 1],
+[0x9F9A28, 0x95, 0x01, 0],
+[0x9F9A2E, 0x95, 0x01, 1],
+[0x9F9A48, 0x95, 0x01, 0],
+[0x9F9A4E, 0x95, 0x01, 1],
+[0x9F9A34, 0x3C, 0x02, 0],
+[0x9F9A3A, 0x3C, 0x02, 1],
+[0x9F9A2C, 0x5E, 0x53, 0],
+[0x9F9A32, 0x5E, 0x53, 1],
+[0x9F9A38, 0x5E, 0x53, 0],
+[0x9F9A3E, 0x5E, 0x53, 1],
+[0x9F9A4C, 0x5E, 0x53, 0],
+[0x9F9A52, 0x5E, 0x53, 1],
+[0xA5DD46, 0x36, 0x26, 0],
+[0xA5DD44, 0x8F, 0x19, 0],
+[0xA5DD4A, 0x1F, 0x37, 0],
+[0xA5DD3E, 0x6D, 0x1D, 0],
+[0xA5DD40, 0xBF, 0x5B, 0],
+[0xA5DD42, 0xDF, 0x67, 0],
+[0xA5DD48, 0xBB, 0x2E, 0],
+[0xA5DD4E, 0xFF, 0x7F, 0],
+[0xA5DD66, 0x36, 0x26, 1],
+[0xA5DD64, 0x8F, 0x19, 1],
+[0xA5DD6A, 0x1F, 0x37, 1],
+[0xA5DD5E, 0x6D, 0x1D, 1],
+[0xA5DD60, 0xBF, 0x5B, 1],
+[0xA5DD62, 0xDF, 0x67, 1],
+[0xA5DD68, 0xBB, 0x2E, 1],
+[0x3D0E0C, 0x7F, 0x02, 1],
+[0x3D0E18, 0x7F, 0x02, 0],
+[0x3D0E0A, 0x3C, 0x02, 1],
+[0x3D0E1A, 0x3C, 0x02, 0],
+[0x3D0E08, 0x5E, 0x53, 1],
+[0x3D0E16, 0x5E, 0x53, 0],
+[0x3CC9C4, 0x7F, 0x02, 0],
+[0x3CC9C6, 0x3C, 0x02, 0],
+[0x3CDE7C, 0x7F, 0x02, 1],
+[0x3CDE7E, 0x3C, 0x02, 1],
+[0x51AD2C, 0x7F, 0x02, 0],
+[0x51AD2E, 0x3C, 0x02, 0],
+[0x51AD4C, 0x7F, 0x02, 1],
+[0x51AD4E, 0x3C, 0x02, 1],
+[0x51A4AE, 0x3C, 0x02, 0],
+[0x51A4AC, 0x7F, 0x02, 0],
+[0x51A4CC, 0x7F, 0x02, 1],
+[0x51A4CE, 0x3C, 0x02, 1],
+[0x51A98C, 0x7F, 0x02, 0],
+[0x51A98E, 0x3C, 0x02, 0],
+[0x51A96C, 0x7F, 0x02, 1],
+[0x51A96E, 0x3C, 0x02, 1],
+[0x51AA00, 0x3D, 0x02, 0],
+[0x51AA10, 0x3D, 0x02, 1],
+[0x51AA02, 0xB5, 0x01, 0],
+[0x51AA12, 0xB5, 0x01, 1],
+[0x51A9FE, 0x7F, 0x02, 0],
+[0x51AA0E, 0x7F, 0x02, 1],
+[0x51AA0A, 0x5E, 0x4F, 0],
+[0x3CC984, 0x7F, 0x02, 0],
+[0x3CC986, 0x3C, 0x02, 0],
+[0x3D4B52, 0x3D, 0x02, 0],
+[0x3D4B5C, 0xFA, 0x01, 0],
+[0x3D4B54, 0x7F, 0x02, 0],
+[0x3D4B56, 0x3C, 0x02, 0],
+[0x3D4B50, 0x5E, 0x53, 0],
+[0x3CCA44, 0x7F, 0x02, 1],
+[0x3CCA46, 0x3C, 0x02, 1],
+[0x3CFB3C, 0x7F, 0x02, 0],
+[0x3CFB3E, 0x3C, 0x02, 0],
+[0x3CFB7C, 0x7F, 0x02, 1],
+[0x3CFB7E, 0x3C, 0x02, 1],
+[0x3D504C, 0x3D, 0x02, 0],
+[0x3D504A, 0x7F, 0x02, 0],
+[0x3D504E, 0x3C, 0x02, 0],
+[0x3D508C, 0x3D, 0x02, 1],
+[0x3D508A, 0x7F, 0x02, 1],
+[0x3D508E, 0x3C, 0x02, 1],
+[0xA5DDA2, 0x36, 0x26, 0],
+[0xA5DDC2, 0x36, 0x26, 1],
+[0xA5DDA6, 0x1F, 0x37, 0],
+[0xA5DDC6, 0x1F, 0x37, 1],
+[0xA5DDA4, 0xBB, 0x2E, 0],
+[0xA5DDC4, 0xBB, 0x2E, 1],
+[0xA5DDA8, 0x5E, 0x53, 0],
+[0xA5DDC8, 0x5E, 0x53, 1],
+[0x3D3E0C, 0x7F, 0x02, 1],
+[0x3D3E10, 0x7F, 0x02, 0],
+[0x3D3E0E, 0x3C, 0x02, 1],
+[0x3D3E12, 0x3C, 0x02, 0],
+[0x3CF1C0, 0x7F, 0x02, 0],
+[0x3CF200, 0x7F, 0x02, 1],
+[0x3CF1C2, 0x3C, 0x02, 0],
+[0x3CF202, 0x3C, 0x02, 1],
+[0x3D360E, 0xBA, 0x02, 1],
+[0x3D3614, 0xBA, 0x02, 0],
+[0x3D360C, 0x3E, 0x43, 1],
+[0x3D3612, 0x3E, 0x43, 0],
+[0x3D3604, 0xBB, 0x2E, 0],
+[0x3D3606, 0xBB, 0x2E, 1],
+[0x3D360A, 0x5E, 0x53, 1],
+[0x3D3610, 0x5E, 0x53, 0],
+[0x3D1A48, 0x35, 0x02, 0],
+[0x3D1A46, 0x5E, 0x53, 0],
+[0x3D1A44, 0x5E, 0x53, 0],
+[0x3D1A4A, 0xBB, 0x2E, 0],
+[0x3D1A88, 0x35, 0x02, 1],
+[0x3D1A8A, 0xDD, 0xD4, 1],
+[0x3D1A86, 0x5E, 0x53, 1],
+[0x3D1A84, 0x5E, 0x53, 1],
+[0x3CE282, 0x3C, 0x02, 0],
+[0x3CE2C2, 0x3C, 0x02, 1],
+[0x3CE280, 0x5E, 0x53, 0],
+[0x3CE2C0, 0x5E, 0x53, 1],
+[0x4FA29E, 0x3C, 0x02, 0],
+[0x4FA2DE, 0x3C, 0x02, 1],
+[0x4FA29C, 0x5E, 0x53, 0],
+[0x4FA2DC, 0x5E, 0x53, 1],
+[0x3D4786, 0x5F, 0x03, 1],
+[0x3D478C, 0x5F, 0x03, 0],
+[0x3D478E, 0x98, 0x02, 0],
+[0x3D4788, 0x98, 0x02, 1],
+[0x3D4790, 0x98, 0x02, 0],
+[0x3D478A, 0x3D, 0x02, 1],
+[0x3D4794, 0xBB, 0x2E, 0],
+[0x3D4792, 0x3D, 0x02, 0],
+[0x3D4784, 0xFE, 0x33, 0],
+[0x3C9E3A, 0x9D, 0x6B, 1],
+[0x3C9E40, 0x9D, 0x6B, 0],
+[0x3C9E38, 0x9D, 0x6B, 1],
+[0x3C9E3E, 0x9D, 0x6B, 0],
+[0x3C9E36, 0xFC, 0x7F, 1],
+[0x3C9E3C, 0xFC, 0x7F, 0],
+[0x4F4D5C, 0x7F, 0x02, 0],
+[0x4F4D5E, 0x3C, 0x02, 0],
+[0x3C9320, 0x7F, 0x02, 0],
+[0x3C9322, 0x3C, 0x02, 0],
+[0x9F9CF6, 0x7F, 0x02, 0],
+[0x9F9CF8, 0x3C, 0x02, 0],
+[0x4F4E1C, 0x7F, 0x02, 1],
+[0x4F4E1E, 0x3C, 0x02, 1],
+[0x3C9450, 0x7F, 0x02, 1],
+[0x3C9452, 0x3C, 0x02, 1],
+[0x9F9D74, 0x7F, 0x02, 1],
+[0x9F9D76, 0x3C, 0x02, 1],
+[0xA6202E, 0x36, 0x26, 0],
+[0xA62032, 0x1F, 0x37, 0],
+[0xA62030, 0xBB, 0x2E, 0],
+[0xA62034, 0x5E, 0x53, 0],
+[0xA6204E, 0x36, 0x26, 1],
+[0xA62052, 0x1F, 0x37, 1],
+[0xA62050, 0xBB, 0x2E, 1],
+[0xA62054, 0x5E, 0x53, 1],
+[0x3D4812, 0xBA, 0x02, 0],
+[0x3D480E, 0xBA, 0x02, 1],
+[0x3D4810, 0x5E, 0x53, 0],
+[0x3D480C, 0x5E, 0x53, 1],
+[0x3CC9FE, 0x7F, 0x02, 0],
+[0x3CCA0A, 0x3C, 0x02, 0],
+[0x8CBE20, 0x35, 0x02, 0],
+[0x8CBE22, 0x3C, 0x02, 0],
+[0x8CBE1E, 0x5E, 0x4F, 0],
+[0x8CBE40, 0x35, 0x02, 1],
+[0x8CBE42, 0x3C, 0x02, 1],
+[0x8CBE3E, 0x5E, 0x4F, 1],
+[0x8CBEE0, 0x7F, 0x02, 0],
+[0x8CBEDE, 0xDF, 0x67, 0],
+[0x8CBEE2, 0x3C, 0x02, 0],
+[0x3B8F38, 0x35, 0x02, 1],
+[0x3B8F3A, 0xDF, 0x67, 1],
+[0x3B8F40, 0x35, 0x02, 0],
+[0x3B8F42, 0xDF, 0x67, 0],
+[0x3D1094, 0x98, 0x02, 0],
+[0x3D109A, 0x8F, 0x19, 0],
+[0x3D1098, 0x3C, 0x01, 0],
+[0x3D1096, 0x3C, 0x02, 0],
+[0x3D1092, 0xDF, 0x67, 0],
+[0x3D1090, 0x5E, 0x53, 0],
+[0x3D10D4, 0x98, 0x02, 1],
+[0x3D10DA, 0x8F, 0x19, 1],
+[0x3D10D8, 0x3C, 0x01, 1],
+[0x3D10D6, 0x3C, 0x02, 1],
+[0x3D10D2, 0xDF, 0x67, 1],
+[0x3D10D0, 0x5E, 0x53, 1],
+[0x3D14D0, 0x7F, 0x02, 0],
+[0x3D14D2, 0x3C, 0x02, 0],
+[0x3D14CE, 0x5E, 0x53, 0],
+[0x3D14CC, 0xFE, 0x42, 0],
+[0x3D1510, 0xBA, 0x02, 1],
+[0x3D1512, 0x3C, 0x02, 1],
+[0x3D150E, 0x5E, 0x53, 1],
+[0x3D150C, 0xFE, 0x42, 1],
+[0x3CE0F4, 0x36, 0x26, 1],
+[0x3CE0F2, 0x3D, 0x02, 1],
+[0x3CE0F0, 0xFE, 0x42, 1],
+[0x3CE0D4, 0x36, 0x26, 0],
+[0x3CE0D2, 0x3D, 0x02, 0],
+[0x3CE0D0, 0xFE, 0x42, 0]]
+
+pinkHat = [
+[0x3CC884, 0xDF, 0x6E, 0],
+[0x3CC886, 0xFF, 0x5D, 0],
+[0x3CC8C4, 0xDF, 0x6E, 1],
+[0x3CC8C6, 0xFF, 0x5D, 1],
+[0x4F4CDC, 0xDF, 0x6E, 0],
+[0x4F4CDE, 0xFF, 0x5D, 0],
+[0x4F4D1C, 0xDF, 0x6E, 1],
+[0x4F4D1E, 0xFF, 0x5D, 1],
+[0x4F51D8, 0xDF, 0x6E, 1],
+[0x4F51DC, 0xDF, 0x6E, 0],
+[0x4F51E8, 0x3C, 0x6B, 0],
+[0x4F51EA, 0x3C, 0x6B, 1],
+[0x4F51DA, 0x7F, 0x77, 1],
+[0x4F51DE, 0x7F, 0x77, 0],
+[0x4FB686, 0xDF, 0x6E, 0],
+[0x4FB6A6, 0xDF, 0x6E, 1],
+[0x4FB684, 0xD4, 0x45, 0],
+[0x4FB6A4, 0xD4, 0x45, 1],
+[0x4FB688, 0x7F, 0x77, 0],
+[0x4FB6A8, 0x7F, 0x77, 1],
+[0x4FB786, 0xDF, 0x6E, 0],
+[0x4FB788, 0x7F, 0x77, 0],
+[0x4FB78A, 0x9E, 0x7F, 0],
+[0x4FB7A6, 0xDF, 0x6E, 1],
+[0x4FB7A8, 0x7F, 0x77, 1],
+[0x4FB7AA, 0x9E, 0x7F, 1],
+[0x51901C, 0xDF, 0x6E, 0],
+[0x519018, 0x3C, 0x51, 0],
+[0x51901A, 0xDD, 0x59, 0],
+[0x51903C, 0xDF, 0x6E, 1],
+[0x519038, 0x3C, 0x51, 1],
+[0x51903A, 0xDD, 0x59, 1],
+[0x5193EA, 0xFF, 0x5D, 0],
+[0x5193E8, 0xDF, 0x6E, 0],
+[0x51940A, 0xFF, 0x5D, 1],
+[0x519408, 0xDF, 0x6E, 1],
+[0x3A72AE, 0x3C, 0x5E, 1],
+[0x3A7244, 0x3C, 0x5E, 0],
+[0x3A724C, 0x5E, 0x66, 1],
+[0x3A7290, 0x3D, 0x62, 0],
+[0x3A72B2, 0x3D, 0x62, 1],
+[0x3A7270, 0xFA, 0x55, 1],
+[0x3A7288, 0xFA, 0x55, 0],
+[0x3A7296, 0x9E, 0x6A, 0],
+[0x3A7274, 0x9E, 0x6A, 1],
+[0x3A7294, 0x5E, 0x66, 0],
+[0x3A724A, 0xDE, 0x6A, 0],
+[0x3A7278, 0xDE, 0x6A, 1],
+[0x3A724E, 0xFE, 0x6E, 0],
+[0x3A727A, 0xFE, 0x6E, 1],
+[0x3A7252, 0x3E, 0x73, 0],
+[0x3A727C, 0x3E, 0x73, 1],
+[0x3A72BC, 0x3E, 0x73, 1],
+[0x3A726C, 0xD8, 0x51, 1],
+[0x3A7286, 0xD8, 0x51, 0],
+[0x3A728C, 0x1B, 0x5A, 0],
+[0x3A7272, 0x1B, 0x5A, 1],
+[0x3A7254, 0x9E, 0x77, 1],
+[0x3A7256, 0x9E, 0x77, 0],
+[0x3A7264, 0x9E, 0x77, 1],
+[0x9F9A58, 0x75, 0x52, 0],
+[0x9F9A5E, 0x75, 0x52, 1],
+[0x9F9A2A, 0xDF, 0x6E, 0],
+[0x9F9A30, 0xDF, 0x6E, 1],
+[0x9F9A36, 0xDF, 0x6E, 0],
+[0x9F9A3C, 0xDF, 0x6E, 1],
+[0x9F9A4A, 0xDF, 0x6E, 0],
+[0x9F9A50, 0xDF, 0x6E, 1],
+[0x9F9A56, 0xD3, 0x45, 0],
+[0x9F9A5C, 0xD3, 0x45, 1],
+[0x9F9A28, 0xD4, 0x45, 0],
+[0x9F9A2E, 0xD4, 0x45, 1],
+[0x9F9A48, 0xD4, 0x45, 0],
+[0x9F9A4E, 0xD4, 0x45, 1],
+[0x9F9A34, 0xFF, 0x5D, 0],
+[0x9F9A3A, 0xFF, 0x5D, 1],
+[0x9F9A2C, 0x7F, 0x77, 0],
+[0x9F9A32, 0x7F, 0x77, 1],
+[0x9F9A38, 0x7F, 0x77, 0],
+[0x9F9A3E, 0x7F, 0x77, 1],
+[0x9F9A4C, 0x7F, 0x77, 0],
+[0x9F9A52, 0x7F, 0x77, 1],
+[0xA5DD46, 0xD5, 0x49, 0],
+[0xA5DD44, 0x91, 0x3D, 0],
+[0xA5DD4A, 0xBD, 0x6A, 0],
+[0xA5DD3E, 0x31, 0x39, 0],
+[0xA5DD40, 0x3D, 0x73, 0],
+[0xA5DD42, 0x5F, 0x77, 0],
+[0xA5DD48, 0x59, 0x5A, 0],
+[0xA5DD4E, 0xFF, 0x7F, 0],
+[0xA5DD66, 0xD5, 0x49, 1],
+[0xA5DD64, 0x91, 0x3D, 1],
+[0xA5DD6A, 0xBD, 0x6A, 1],
+[0xA5DD5E, 0x31, 0x39, 1],
+[0xA5DD60, 0x3D, 0x73, 1],
+[0xA5DD62, 0x5F, 0x77, 1],
+[0xA5DD68, 0x59, 0x5A, 1],
+[0x3D0E0C, 0xDF, 0x6E, 1],
+[0x3D0E18, 0xDF, 0x6E, 0],
+[0x3D0E0A, 0xFF, 0x5D, 1],
+[0x3D0E1A, 0xFF, 0x5D, 0],
+[0x3D0E08, 0x7F, 0x77, 1],
+[0x3D0E16, 0x7F, 0x77, 0],
+[0x3CC9C4, 0xDF, 0x6E, 0],
+[0x3CC9C6, 0xFF, 0x5D, 0],
+[0x3CDE7C, 0xDF, 0x6E, 1],
+[0x3CDE7E, 0xFF, 0x5D, 1],
+[0x51AD2C, 0xDF, 0x6E, 0],
+[0x51AD2E, 0xFF, 0x5D, 0],
+[0x51AD4C, 0xDF, 0x6E, 1],
+[0x51AD4E, 0xFF, 0x5D, 1],
+[0x51A4AE, 0xFF, 0x5D, 0],
+[0x51A4AC, 0xDF, 0x6E, 0],
+[0x51A4CC, 0xDF, 0x6E, 1],
+[0x51A4CE, 0xFF, 0x5D, 1],
+[0x51A98C, 0xDF, 0x6E, 0],
+[0x51A98E, 0xFF, 0x5D, 0],
+[0x51A96C, 0xDF, 0x6E, 1],
+[0x51A96E, 0xFF, 0x5D, 1],
+[0x51AA00, 0xBD, 0x6A, 0],
+[0x51AA10, 0xBD, 0x6A, 1],
+[0x51AA02, 0xFF, 0x5D, 0],
+[0x51AA12, 0xFF, 0x5D, 1],
+[0x51A9FE, 0xDF, 0x6E, 0],
+[0x51AA0E, 0xDF, 0x6E, 1],
+[0x51AA0A, 0x5E, 0x77, 0],
+[0x3CC984, 0xDF, 0x6E, 0],
+[0x3CC986, 0xFF, 0x5D, 0],
+[0x3D4B52, 0x7F, 0x66, 0],
+[0x3D4B5C, 0xBC, 0x55, 0],
+[0x3D4B54, 0xDF, 0x6E, 0],
+[0x3D4B56, 0xFF, 0x5D, 0],
+[0x3D4B50, 0x7F, 0x77, 0],
+[0x3CCA44, 0xDF, 0x6E, 1],
+[0x3CCA46, 0xFF, 0x5D, 1],
+[0x3CFB3C, 0xDF, 0x6E, 0],
+[0x3CFB3E, 0xFF, 0x5D, 0],
+[0x3CFB7C, 0xDF, 0x6E, 1],
+[0x3CFB7E, 0xFF, 0x5D, 1],
+[0x3D504C, 0x9C, 0x62, 0],
+[0x3D504A, 0xDF, 0x6E, 0],
+[0x3D504E, 0xFF, 0x5D, 0],
+[0x3D508C, 0x9C, 0x62, 1],
+[0x3D508A, 0xDF, 0x6E, 1],
+[0x3D508E, 0xFF, 0x5D, 1],
+[0xA5DDA2, 0xD5, 0x49, 0],
+[0xA5DDC2, 0xD5, 0x49, 1],
+[0xA5DDA6, 0xBD, 0x6A, 0],
+[0xA5DDC6, 0xBD, 0x6A, 1],
+[0xA5DDA4, 0x59, 0x5A, 0],
+[0xA5DDC4, 0x59, 0x5A, 1],
+[0xA5DDA8, 0x3D, 0x6F, 0],
+[0xA5DDC8, 0x3D, 0x6F, 1],
+[0x3D3E0C, 0xDF, 0x6E, 1],
+[0x3D3E10, 0xDF, 0x6E, 0],
+[0x3D3E0E, 0xFF, 0x5D, 1],
+[0x3D3E12, 0xFF, 0x5D, 0],
+[0x3CF1C0, 0xDF, 0x6E, 0],
+[0x3CF200, 0xDF, 0x6E, 1],
+[0x3CF1C2, 0xFF, 0x5D, 0],
+[0x3CF202, 0xFF, 0x5D, 1],
+[0x3D360E, 0x5E, 0x66, 1],
+[0x3D3614, 0x5E, 0x66, 0],
+[0x3D360C, 0x7F, 0x77, 1],
+[0x3D3612, 0x7F, 0x77, 0],
+[0x3D3604, 0x59, 0x5A, 0],
+[0x3D3606, 0x59, 0x5A, 1],
+[0x3D360A, 0x7F, 0x77, 1],
+[0x3D3610, 0x7F, 0x77, 0],
+[0x3D1A48, 0x1B, 0x5A, 0],
+[0x3D1A46, 0x7F, 0x77, 0],
+[0x3D1A44, 0x7F, 0x77, 0],
+[0x3D1A4A, 0x59, 0x5A, 0],
+[0x3D1A88, 0x1B, 0x5A, 1],
+[0x3D1A8A, 0xCC, 0xC8, 1],
+[0x3D1A86, 0x7F, 0x77, 1],
+[0x3D1A84, 0x7F, 0x77, 1],
+[0x3CE282, 0xFF, 0x5D, 0],
+[0x3CE2C2, 0xFF, 0x5D, 1],
+[0x3CE280, 0x7F, 0x77, 0],
+[0x3CE2C0, 0x7F, 0x77, 1],
+[0x4FA29E, 0xFF, 0x5D, 0],
+[0x4FA2DE, 0xFF, 0x5D, 1],
+[0x4FA29C, 0x7F, 0x77, 0],
+[0x4FA2DC, 0x7F, 0x77, 1],
+[0x3D4786, 0x9E, 0x77, 1],
+[0x3D478C, 0x9E, 0x77, 0],
+[0x3D478E, 0x3D, 0x62, 0],
+[0x3D4788, 0x3D, 0x62, 1],
+[0x3D4790, 0x3D, 0x62, 0],
+[0x3D478A, 0x9C, 0x62, 1],
+[0x3D4794, 0x59, 0x5A, 0],
+[0x3D4792, 0x9C, 0x62, 0],
+[0x3D4784, 0xFE, 0x33, 0],
+[0x3C9E3A, 0x9E, 0x7F, 1],
+[0x3C9E40, 0x9E, 0x7F, 0],
+[0x3C9E38, 0x9E, 0x7F, 1],
+[0x3C9E3E, 0x9E, 0x7F, 0],
+[0x3C9E36, 0xFC, 0x7F, 1],
+[0x3C9E3C, 0xFC, 0x7F, 0],
+[0x4F4D5C, 0xDF, 0x6E, 0],
+[0x4F4D5E, 0xFF, 0x5D, 0],
+[0x3C9320, 0xDF, 0x6E, 0],
+[0x3C9322, 0xFF, 0x5D, 0],
+[0x9F9CF6, 0xDF, 0x6E, 0],
+[0x9F9CF8, 0xFF, 0x5D, 0],
+[0x4F4E1C, 0xDF, 0x6E, 1],
+[0x4F4E1E, 0xFF, 0x5D, 1],
+[0x3C9450, 0xDF, 0x6E, 1],
+[0x3C9452, 0xFF, 0x5D, 1],
+[0x9F9D74, 0xDF, 0x6E, 1],
+[0x9F9D76, 0xFF, 0x5D, 1],
+[0xA6202E, 0xD5, 0x49, 0],
+[0xA62032, 0xBD, 0x6A, 0],
+[0xA62030, 0x59, 0x5A, 0],
+[0xA62034, 0x3D, 0x6F, 0],
+[0xA6204E, 0xD5, 0x49, 1],
+[0xA62052, 0xBD, 0x6A, 1],
+[0xA62050, 0x59, 0x5A, 1],
+[0xA62054, 0x3D, 0x6F, 1],
+[0x3D4812, 0x5E, 0x66, 0],
+[0x3D480E, 0x5E, 0x66, 1],
+[0x3D4810, 0x7F, 0x77, 0],
+[0x3D480C, 0x7F, 0x77, 1],
+[0x3CC9FE, 0xDF, 0x6E, 0],
+[0x3CCA0A, 0xFF, 0x5D, 0],
+[0x8CBE20, 0x1B, 0x5A, 0],
+[0x8CBE22, 0xFF, 0x5D, 0],
+[0x8CBE1E, 0x5E, 0x77, 0],
+[0x8CBE40, 0x1B, 0x5A, 1],
+[0x8CBE42, 0xFF, 0x5D, 1],
+[0x8CBE3E, 0x5E, 0x77, 1],
+[0x8CBEE0, 0xDF, 0x6E, 0],
+[0x8CBEDE, 0x5F, 0x77, 0],
+[0x8CBEE2, 0xFF, 0x5D, 0],
+[0x3B8F38, 0x1B, 0x5A, 1],
+[0x3B8F3A, 0x5F, 0x77, 1],
+[0x3B8F40, 0x1B, 0x5A, 0],
+[0x3B8F42, 0x5F, 0x77, 0],
+[0x3D1094, 0x3D, 0x62, 0],
+[0x3D109A, 0x91, 0x3D, 0],
+[0x3D1098, 0x3C, 0x51, 0],
+[0x3D1096, 0xDD, 0x59, 0],
+[0x3D1092, 0x5F, 0x77, 0],
+[0x3D1090, 0x7F, 0x77, 0],
+[0x3D10D4, 0x3D, 0x62, 1],
+[0x3D10DA, 0x91, 0x3D, 1],
+[0x3D10D8, 0x3C, 0x51, 1],
+[0x3D10D6, 0xDD, 0x59, 1],
+[0x3D10D2, 0x5F, 0x77, 1],
+[0x3D10D0, 0x7F, 0x77, 1],
+[0x3D14D0, 0xDF, 0x6E, 0],
+[0x3D14D2, 0xFF, 0x5D, 0],
+[0x3D14CE, 0x7F, 0x77, 0],
+[0x3D14CC, 0x3C, 0x6B, 0],
+[0x3D1510, 0x5E, 0x66, 1],
+[0x3D1512, 0xFF, 0x5D, 1],
+[0x3D150E, 0x7F, 0x77, 1],
+[0x3D150C, 0x3C, 0x6B, 1],
+[0x3CE0F4, 0xD5, 0x49, 1],
+[0x3CE0F2, 0xBD, 0x6A, 1],
+[0x3CE0F0, 0x3C, 0x6B, 1],
+[0x3CE0D4, 0xD5, 0x49, 0],
+[0x3CE0D2, 0xBD, 0x6A, 0],
+[0x3CE0D0, 0x3C, 0x6B, 0]]
+
+purpleHat = [
+[0x3CC884, 0x10, 0x7C, 0],
+[0x3CC886, 0x0B, 0x5C, 0],
+[0x3CC8C4, 0x10, 0x7C, 1],
+[0x3CC8C6, 0x0B, 0x5C, 1],
+[0x4F4CDC, 0x10, 0x7C, 0],
+[0x4F4CDE, 0x0B, 0x5C, 0],
+[0x4F4D1C, 0x10, 0x7C, 1],
+[0x4F4D1E, 0x0B, 0x5C, 1],
+[0x4F51D8, 0x10, 0x7C, 1],
+[0x4F51DC, 0x10, 0x7C, 0],
+[0x4F51E8, 0x98, 0x6E, 0],
+[0x4F51EA, 0x98, 0x6E, 1],
+[0x4F51DA, 0xFB, 0x7A, 1],
+[0x4F51DE, 0xFB, 0x7A, 0],
+[0x4FB686, 0x10, 0x7C, 0],
+[0x4FB6A6, 0x10, 0x7C, 1],
+[0x4FB684, 0x05, 0x28, 0],
+[0x4FB6A4, 0x05, 0x28, 1],
+[0x4FB688, 0xFB, 0x7A, 0],
+[0x4FB6A8, 0xFB, 0x7A, 1],
+[0x4FB786, 0x10, 0x7C, 0],
+[0x4FB788, 0xFB, 0x7A, 0],
+[0x4FB78A, 0x9E, 0x7F, 0],
+[0x4FB7A6, 0x10, 0x7C, 1],
+[0x4FB7A8, 0xFB, 0x7A, 1],
+[0x4FB7AA, 0x9E, 0x7F, 1],
+[0x51901C, 0x10, 0x7C, 0],
+[0x519018, 0x05, 0x39, 0],
+[0x51901A, 0x0F, 0x5C, 0],
+[0x51903C, 0x10, 0x7C, 1],
+[0x519038, 0x05, 0x39, 1],
+[0x51903A, 0x0F, 0x5C, 1],
+[0x5193EA, 0x0B, 0x5C, 0],
+[0x5193E8, 0x10, 0x7C, 0],
+[0x51940A, 0x0B, 0x5C, 1],
+[0x519408, 0x10, 0x7C, 1],
+[0x3A72AE, 0xB4, 0x51, 1],
+[0x3A7244, 0xB4, 0x51, 0],
+[0x3A724C, 0xF6, 0x59, 1],
+[0x3A7290, 0xF6, 0x59, 0],
+[0x3A72B2, 0x36, 0x5A, 1],
+[0x3A7270, 0xF4, 0x50, 1],
+[0x3A7288, 0xF4, 0x50, 0],
+[0x3A7296, 0x79, 0x66, 0],
+[0x3A7274, 0x79, 0x66, 1],
+[0x3A7294, 0x36, 0x5A, 0],
+[0x3A724A, 0x99, 0x66, 0],
+[0x3A7278, 0x99, 0x66, 1],
+[0x3A724E, 0xB9, 0x66, 0],
+[0x3A727A, 0xB9, 0x66, 1],
+[0x3A7252, 0x19, 0x67, 0],
+[0x3A727C, 0x19, 0x67, 1],
+[0x3A72BC, 0x19, 0x67, 1],
+[0x3A726C, 0x54, 0x50, 1],
+[0x3A7286, 0x54, 0x50, 0],
+[0x3A728C, 0x74, 0x51, 0],
+[0x3A7272, 0x74, 0x51, 1],
+[0x3A7254, 0x5C, 0x73, 1],
+[0x3A7256, 0x5C, 0x73, 0],
+[0x3A7264, 0x5C, 0x73, 1],
+[0x9F9A58, 0xF2, 0x51, 0],
+[0x9F9A5E, 0xF2, 0x51, 1],
+[0x9F9A2A, 0x10, 0x7C, 0],
+[0x9F9A30, 0x10, 0x7C, 1],
+[0x9F9A36, 0x10, 0x7C, 0],
+[0x9F9A3C, 0x10, 0x7C, 1],
+[0x9F9A4A, 0x10, 0x7C, 0],
+[0x9F9A50, 0x10, 0x7C, 1],
+[0x9F9A56, 0x06, 0x34, 0],
+[0x9F9A5C, 0x06, 0x34, 1],
+[0x9F9A28, 0x05, 0x28, 0],
+[0x9F9A2E, 0x05, 0x28, 1],
+[0x9F9A48, 0x05, 0x28, 0],
+[0x9F9A4E, 0x05, 0x28, 1],
+[0x9F9A34, 0x0F, 0x5C, 0],
+[0x9F9A3A, 0x0F, 0x5C, 1],
+[0x9F9A2C, 0xFB, 0x7A, 0],
+[0x9F9A32, 0xFB, 0x7A, 1],
+[0x9F9A38, 0xFB, 0x7A, 0],
+[0x9F9A3E, 0xFB, 0x7A, 1],
+[0x9F9A4C, 0xFB, 0x7A, 0],
+[0x9F9A52, 0xFB, 0x7A, 1],
+[0xA5DD46, 0x8A, 0x3C, 0],
+[0xA5DD44, 0x66, 0x28, 0],
+[0xA5DD4A, 0x32, 0x71, 0],
+[0xA5DD3E, 0x4C, 0x39, 0],
+[0xA5DD40, 0x34, 0x62, 0],
+[0xA5DD42, 0x77, 0x72, 0],
+[0xA5DD48, 0xED, 0x50, 0],
+[0xA5DD4E, 0xFF, 0x7F, 0],
+[0xA5DD66, 0x8A, 0x3C, 1],
+[0xA5DD64, 0x66, 0x28, 1],
+[0xA5DD6A, 0x32, 0x71, 1],
+[0xA5DD5E, 0x4C, 0x39, 1],
+[0xA5DD60, 0x34, 0x62, 1],
+[0xA5DD62, 0x77, 0x72, 1],
+[0xA5DD68, 0xED, 0x50, 1],
+[0x3D0E0C, 0x10, 0x7C, 1],
+[0x3D0E18, 0x10, 0x7C, 0],
+[0x3D0E0A, 0x0B, 0x5C, 1],
+[0x3D0E1A, 0x0B, 0x5C, 0],
+[0x3D0E08, 0xFB, 0x7A, 1],
+[0x3D0E16, 0xFB, 0x7A, 0],
+[0x3CC9C4, 0x10, 0x7C, 0],
+[0x3CC9C6, 0x0B, 0x5C, 0],
+[0x3CDE7C, 0x10, 0x7C, 1],
+[0x3CDE7E, 0x0B, 0x5C, 1],
+[0x51AD2C, 0x10, 0x7C, 0],
+[0x51AD2E, 0x0B, 0x5C, 0],
+[0x51AD4C, 0x10, 0x7C, 1],
+[0x51AD4E, 0x0B, 0x5C, 1],
+[0x51A4AE, 0x0B, 0x5C, 0],
+[0x51A4AC, 0x10, 0x7C, 0],
+[0x51A4CC, 0x10, 0x7C, 1],
+[0x51A4CE, 0x0B, 0x5C, 1],
+[0x51A98C, 0x10, 0x7C, 0],
+[0x51A98E, 0x0B, 0x5C, 0],
+[0x51A96C, 0x10, 0x7C, 1],
+[0x51A96E, 0x0B, 0x5C, 1],
+[0x51AA00, 0x0B, 0x64, 0],
+[0x51AA10, 0x0B, 0x64, 1],
+[0x51AA02, 0x0B, 0x5C, 0],
+[0x51AA12, 0x0B, 0x5C, 1],
+[0x51A9FE, 0x10, 0x7C, 0],
+[0x51AA0E, 0x10, 0x7C, 1],
+[0x51AA0A, 0xF7, 0x79, 0],
+[0x3CC984, 0x10, 0x7C, 0],
+[0x3CC986, 0x0B, 0x5C, 0],
+[0x3D4B52, 0x0E, 0x70, 0],
+[0x3D4B5C, 0x09, 0x50, 0],
+[0x3D4B54, 0x10, 0x7C, 0],
+[0x3D4B56, 0x0B, 0x5C, 0],
+[0x3D4B50, 0xFB, 0x7A, 0],
+[0x3CCA44, 0x10, 0x7C, 1],
+[0x3CCA46, 0x0B, 0x5C, 1],
+[0x3CFB3C, 0x10, 0x7C, 0],
+[0x3CFB3E, 0x0B, 0x5C, 0],
+[0x3CFB7C, 0x10, 0x7C, 1],
+[0x3CFB7E, 0x0B, 0x5C, 1],
+[0x3D504C, 0x0C, 0x64, 0],
+[0x3D504A, 0x10, 0x7C, 0],
+[0x3D504E, 0x0B, 0x5C, 0],
+[0x3D508C, 0x0C, 0x64, 1],
+[0x3D508A, 0x10, 0x7C, 1],
+[0x3D508E, 0x0B, 0x5C, 1],
+[0xA5DDA2, 0x8A, 0x3C, 0],
+[0xA5DDC2, 0x8A, 0x3C, 1],
+[0xA5DDA6, 0x32, 0x71, 0],
+[0xA5DDC6, 0x32, 0x71, 1],
+[0xA5DDA4, 0xED, 0x50, 0],
+[0xA5DDC4, 0xED, 0x50, 1],
+[0xA5DDA8, 0xFB, 0x7A, 0],
+[0xA5DDC8, 0xFB, 0x7A, 1],
+[0x3D3E0C, 0x10, 0x7C, 1],
+[0x3D3E10, 0x10, 0x7C, 0],
+[0x3D3E0E, 0x0B, 0x5C, 1],
+[0x3D3E12, 0x0B, 0x5C, 0],
+[0x3CF1C0, 0x10, 0x7C, 0],
+[0x3CF200, 0x10, 0x7C, 1],
+[0x3CF1C2, 0x0B, 0x5C, 0],
+[0x3CF202, 0x0B, 0x5C, 1],
+[0x3D360E, 0xF6, 0x59, 1],
+[0x3D3614, 0xF6, 0x59, 0],
+[0x3D360C, 0xFB, 0x7A, 1],
+[0x3D3612, 0xFB, 0x7A, 0],
+[0x3D3604, 0xED, 0x50, 0],
+[0x3D3606, 0xED, 0x50, 1],
+[0x3D360A, 0xFB, 0x7A, 1],
+[0x3D3610, 0xFB, 0x7A, 0],
+[0x3D1A48, 0x74, 0x51, 0],
+[0x3D1A46, 0xFB, 0x7A, 0],
+[0x3D1A44, 0xFB, 0x7A, 0],
+[0x3D1A4A, 0xED, 0x50, 0],
+[0x3D1A88, 0x74, 0x51, 1],
+[0x3D1A8A, 0xAA, 0xA4, 1],
+[0x3D1A86, 0xFB, 0x7A, 1],
+[0x3D1A84, 0xFB, 0x7A, 1],
+[0x3CE282, 0x0B, 0x5C, 0],
+[0x3CE2C2, 0x0B, 0x5C, 1],
+[0x3CE280, 0xFB, 0x7A, 0],
+[0x3CE2C0, 0xFB, 0x7A, 1],
+[0x4FA29E, 0x0B, 0x5C, 0],
+[0x4FA2DE, 0x0B, 0x5C, 1],
+[0x4FA29C, 0xFB, 0x7A, 0],
+[0x4FA2DC, 0xFB, 0x7A, 1],
+[0x3D4786, 0x5C, 0x73, 1],
+[0x3D478C, 0x5C, 0x73, 0],
+[0x3D478E, 0xF6, 0x59, 0],
+[0x3D4788, 0xF6, 0x59, 1],
+[0x3D4790, 0xF6, 0x59, 0],
+[0x3D478A, 0x0C, 0x64, 1],
+[0x3D4794, 0xED, 0x50, 0],
+[0x3D4792, 0x0C, 0x64, 0],
+[0x3D4784, 0xFE, 0x33, 0],
+[0x3C9E3A, 0x9E, 0x7F, 1],
+[0x3C9E40, 0x9E, 0x7F, 0],
+[0x3C9E38, 0x9E, 0x7F, 1],
+[0x3C9E3E, 0x9E, 0x7F, 0],
+[0x3C9E36, 0xFC, 0x7F, 1],
+[0x3C9E3C, 0xFC, 0x7F, 0],
+[0x4F4D5C, 0x10, 0x7C, 0],
+[0x4F4D5E, 0x0B, 0x5C, 0],
+[0x3C9320, 0x10, 0x7C, 0],
+[0x3C9322, 0x0B, 0x5C, 0],
+[0x9F9CF6, 0x10, 0x7C, 0],
+[0x9F9CF8, 0x0B, 0x5C, 0],
+[0x4F4E1C, 0x10, 0x7C, 1],
+[0x4F4E1E, 0x0B, 0x5C, 1],
+[0x3C9450, 0x10, 0x7C, 1],
+[0x3C9452, 0x0B, 0x5C, 1],
+[0x9F9D74, 0x10, 0x7C, 1],
+[0x9F9D76, 0x0B, 0x5C, 1],
+[0xA6202E, 0x8A, 0x3C, 0],
+[0xA62032, 0x32, 0x71, 0],
+[0xA62030, 0xED, 0x50, 0],
+[0xA62034, 0xFB, 0x7A, 0],
+[0xA6204E, 0x8A, 0x3C, 1],
+[0xA62052, 0x32, 0x71, 1],
+[0xA62050, 0xED, 0x50, 1],
+[0xA62054, 0xFB, 0x7A, 1],
+[0x3D4812, 0xF6, 0x59, 0],
+[0x3D480E, 0xF6, 0x59, 1],
+[0x3D4810, 0xFB, 0x7A, 0],
+[0x3D480C, 0xFB, 0x7A, 1],
+[0x3CC9FE, 0x10, 0x7C, 0],
+[0x3CCA0A, 0x0B, 0x5C, 0],
+[0x8CBE20, 0x74, 0x51, 0],
+[0x8CBE22, 0x0B, 0x5C, 0],
+[0x8CBE1E, 0xF7, 0x79, 0],
+[0x8CBE40, 0x74, 0x51, 1],
+[0x8CBE42, 0x0B, 0x5C, 1],
+[0x8CBE3E, 0xF7, 0x79, 1],
+[0x8CBEE0, 0x10, 0x7C, 0],
+[0x8CBEDE, 0x77, 0x72, 0],
+[0x8CBEE2, 0x0B, 0x5C, 0],
+[0x3B8F38, 0x74, 0x51, 1],
+[0x3B8F3A, 0x77, 0x72, 1],
+[0x3B8F40, 0x74, 0x51, 0],
+[0x3B8F42, 0x77, 0x72, 0],
+[0x3D1094, 0xF6, 0x59, 0],
+[0x3D109A, 0x66, 0x28, 0],
+[0x3D1098, 0x05, 0x39, 0],
+[0x3D1096, 0x0F, 0x5C, 0],
+[0x3D1092, 0x77, 0x72, 0],
+[0x3D1090, 0xFB, 0x7A, 0],
+[0x3D10D4, 0xF6, 0x59, 1],
+[0x3D10DA, 0x66, 0x28, 1],
+[0x3D10D8, 0x05, 0x39, 1],
+[0x3D10D6, 0x0F, 0x5C, 1],
+[0x3D10D2, 0x77, 0x72, 1],
+[0x3D10D0, 0xFB, 0x7A, 1],
+[0x3D14D0, 0x10, 0x7C, 0],
+[0x3D14D2, 0x0B, 0x5C, 0],
+[0x3D14CE, 0xFB, 0x7A, 0],
+[0x3D14CC, 0x98, 0x6E, 0],
+[0x3D1510, 0xF6, 0x59, 1],
+[0x3D1512, 0x0B, 0x5C, 1],
+[0x3D150E, 0xFB, 0x7A, 1],
+[0x3D150C, 0x98, 0x6E, 1],
+[0x3CE0F4, 0x8A, 0x3C, 1],
+[0x3CE0F2, 0x0B, 0x64, 1],
+[0x3CE0F0, 0x98, 0x6E, 1],
+[0x3CE0D4, 0x8A, 0x3C, 0],
+[0x3CE0D2, 0x0B, 0x64, 0],
+[0x3CE0D0, 0x98, 0x6E, 0]]
+
+redHat = [
+[0x3CC884, 0x3F, 0x04, 0],
+[0x3CC886, 0x17, 0x00, 0],
+[0x3CC8C4, 0x3F, 0x04, 1],
+[0x3CC8C6, 0x17, 0x00, 1],
+[0x4F4CDC, 0x3F, 0x04, 0],
+[0x4F4CDE, 0x17, 0x00, 0],
+[0x4F4D1C, 0x3F, 0x04, 1],
+[0x4F4D1E, 0x17, 0x00, 1],
+[0x4F51D8, 0x3F, 0x04, 1],
+[0x4F51DC, 0x3F, 0x04, 0],
+[0x4F51E8, 0x1D, 0x3A, 0],
+[0x4F51EA, 0x1D, 0x3A, 1],
+[0x4F51DA, 0x5F, 0x42, 1],
+[0x4F51DE, 0x5F, 0x42, 0],
+[0x4FB686, 0x3F, 0x04, 0],
+[0x4FB6A6, 0x3F, 0x04, 1],
+[0x4FB684, 0x0F, 0x00, 0],
+[0x4FB6A4, 0x0F, 0x00, 1],
+[0x4FB688, 0x5F, 0x42, 0],
+[0x4FB6A8, 0x5F, 0x42, 1],
+[0x4FB786, 0x3F, 0x04, 0],
+[0x4FB788, 0x5F, 0x42, 0],
+[0x4FB78A, 0xDF, 0x52, 0],
+[0x4FB7A6, 0x3F, 0x04, 1],
+[0x4FB7A8, 0x5F, 0x42, 1],
+[0x4FB7AA, 0xDF, 0x52, 1],
+[0x51901C, 0x3F, 0x04, 0],
+[0x519018, 0xD0, 0x00, 0],
+[0x51901A, 0x19, 0x00, 0],
+[0x51903C, 0x3F, 0x04, 1],
+[0x519038, 0xD0, 0x00, 1],
+[0x51903A, 0x19, 0x00, 1],
+[0x5193EA, 0x17, 0x00, 0],
+[0x5193E8, 0x3F, 0x04, 0],
+[0x51940A, 0x17, 0x00, 1],
+[0x519408, 0x3F, 0x04, 1],
+[0x3A72AE, 0x7D, 0x21, 1],
+[0x3A7244, 0x9A, 0x29, 0],
+[0x3A724C, 0xDE, 0x21, 1],
+[0x3A7290, 0x7D, 0x21, 0],
+[0x3A72B2, 0xDE, 0x21, 1],
+[0x3A7270, 0x16, 0x1D, 1],
+[0x3A7288, 0x16, 0x1D, 0],
+[0x3A7296, 0x3F, 0x22, 0],
+[0x3A7274, 0xDE, 0x21, 1],
+[0x3A7294, 0xDE, 0x21, 0],
+[0x3A724A, 0x5F, 0x22, 0],
+[0x3A7278, 0x5F, 0x22, 1],
+[0x3A724E, 0xDF, 0x22, 0],
+[0x3A727A, 0xDF, 0x22, 1],
+[0x3A7252, 0x3F, 0x1F, 0],
+[0x3A727C, 0x3F, 0x1F, 1],
+[0x3A72BC, 0x3F, 0x1F, 1],
+[0x3A726C, 0x10, 0x1D, 1],
+[0x3A7286, 0x10, 0x1D, 0],
+[0x3A728C, 0x3C, 0x1D, 0],
+[0x3A7272, 0x9A, 0x29, 1],
+[0x3A7254, 0x9F, 0x23, 1],
+[0x3A7256, 0x9F, 0x23, 0],
+[0x3A7264, 0x9F, 0x23, 1],
+[0x9F9A58, 0x71, 0x29, 0],
+[0x9F9A5E, 0x71, 0x29, 1],
+[0x9F9A2A, 0x3F, 0x04, 0],
+[0x9F9A30, 0x3F, 0x04, 1],
+[0x9F9A36, 0x3F, 0x04, 0],
+[0x9F9A3C, 0x3F, 0x04, 1],
+[0x9F9A4A, 0x3F, 0x04, 0],
+[0x9F9A50, 0x3F, 0x04, 1],
+[0x9F9A56, 0x30, 0x04, 0],
+[0x9F9A5C, 0x30, 0x04, 1],
+[0x9F9A28, 0x0F, 0x00, 0],
+[0x9F9A2E, 0x0F, 0x00, 1],
+[0x9F9A48, 0x0F, 0x00, 0],
+[0x9F9A4E, 0x0F, 0x00, 1],
+[0x9F9A34, 0x17, 0x00, 0],
+[0x9F9A3A, 0x17, 0x00, 1],
+[0x9F9A2C, 0x5F, 0x42, 0],
+[0x9F9A32, 0x5F, 0x42, 1],
+[0x9F9A38, 0x5F, 0x42, 0],
+[0x9F9A3E, 0x5F, 0x42, 1],
+[0x9F9A4C, 0x5F, 0x42, 0],
+[0x9F9A52, 0x5F, 0x42, 1],
+[0xA5DD46, 0x13, 0x00, 0],
+[0xA5DD44, 0x0F, 0x00, 0],
+[0xA5DD4A, 0x7F, 0x0C, 0],
+[0xA5DD3E, 0x30, 0x21, 0],
+[0xA5DD40, 0xDF, 0x35, 0],
+[0xA5DD42, 0x9F, 0x4A, 0],
+[0xA5DD48, 0x59, 0x08, 0],
+[0xA5DD4E, 0xFF, 0x7F, 0],
+[0xA5DD66, 0x13, 0x00, 1],
+[0xA5DD64, 0x0F, 0x00, 1],
+[0xA5DD6A, 0x7F, 0x0C, 1],
+[0xA5DD5E, 0x30, 0x21, 1],
+[0xA5DD60, 0xDF, 0x35, 1],
+[0xA5DD62, 0x9F, 0x4A, 1],
+[0xA5DD68, 0x59, 0x08, 1],
+[0x3D0E0C, 0x3F, 0x04, 1],
+[0x3D0E18, 0x3F, 0x04, 0],
+[0x3D0E0A, 0x17, 0x00, 1],
+[0x3D0E1A, 0x17, 0x00, 0],
+[0x3D0E08, 0x5F, 0x42, 1],
+[0x3D0E16, 0x5F, 0x42, 0],
+[0x3CC9C4, 0x3F, 0x04, 0],
+[0x3CC9C6, 0x17, 0x00, 0],
+[0x3CDE7C, 0x3F, 0x04, 1],
+[0x3CDE7E, 0x17, 0x00, 1],
+[0x51AD2C, 0x3F, 0x04, 0],
+[0x51AD2E, 0x17, 0x00, 0],
+[0x51AD4C, 0x3F, 0x04, 1],
+[0x51AD4E, 0x17, 0x00, 1],
+[0x51A4AE, 0x17, 0x00, 0],
+[0x51A4AC, 0x3F, 0x04, 0],
+[0x51A4CC, 0x3F, 0x04, 1],
+[0x51A4CE, 0x17, 0x00, 1],
+[0x51A98C, 0x3F, 0x04, 0],
+[0x51A98E, 0x17, 0x00, 0],
+[0x51A96C, 0x3F, 0x04, 1],
+[0x51A96E, 0x17, 0x00, 1],
+[0x51AA00, 0x3A, 0x04, 0],
+[0x51AA10, 0x3A, 0x04, 1],
+[0x51AA02, 0x12, 0x00, 0],
+[0x51AA12, 0x12, 0x00, 1],
+[0x51A9FE, 0x1F, 0x0D, 0],
+[0x51AA0E, 0x1F, 0x0D, 1],
+[0x51AA0A, 0xFF, 0x26, 0],
+[0x3CC984, 0x3F, 0x04, 0],
+[0x3CC986, 0x17, 0x00, 0],
+[0x3D4B52, 0x1D, 0x00, 0],
+[0x3D4B5C, 0x13, 0x00, 0],
+[0x3D4B54, 0x3F, 0x04, 0],
+[0x3D4B56, 0x17, 0x00, 0],
+[0x3D4B50, 0xBF, 0x56, 0],
+[0x3CCA44, 0x3F, 0x04, 1],
+[0x3CCA46, 0x17, 0x00, 1],
+[0x3CFB3C, 0x3F, 0x04, 0],
+[0x3CFB3E, 0x17, 0x00, 0],
+[0x3CFB7C, 0x3F, 0x04, 1],
+[0x3CFB7E, 0x17, 0x00, 1],
+[0x3D504C, 0x1B, 0x00, 0],
+[0x3D504A, 0x3F, 0x04, 0],
+[0x3D504E, 0x17, 0x00, 0],
+[0x3D508C, 0x1B, 0x00, 1],
+[0x3D508A, 0x3F, 0x04, 1],
+[0x3D508E, 0x17, 0x00, 1],
+[0xA5DDA2, 0x13, 0x00, 0],
+[0xA5DDC2, 0x13, 0x00, 1],
+[0xA5DDA6, 0x7F, 0x0C, 0],
+[0xA5DDC6, 0x7F, 0x0C, 1],
+[0xA5DDA4, 0x59, 0x08, 0],
+[0xA5DDC4, 0x59, 0x08, 1],
+[0xA5DDA8, 0x3F, 0x5B, 0],
+[0xA5DDC8, 0x3F, 0x5B, 1],
+[0x3D3E0C, 0x3F, 0x04, 1],
+[0x3D3E10, 0x3F, 0x04, 0],
+[0x3D3E0E, 0x17, 0x00, 1],
+[0x3D3E12, 0x17, 0x00, 0],
+[0x3CF1C0, 0x3F, 0x04, 0],
+[0x3CF200, 0x3F, 0x04, 1],
+[0x3CF1C2, 0x17, 0x00, 0],
+[0x3CF202, 0x17, 0x00, 1],
+[0x3D360E, 0xDD, 0x10, 1],
+[0x3D3614, 0xDD, 0x10, 0],
+[0x3D360C, 0x3F, 0x46, 1],
+[0x3D3612, 0x3F, 0x46, 0],
+[0x3D3604, 0x76, 0x00, 0],
+[0x3D3606, 0x76, 0x00, 1],
+[0x3D360A, 0x5F, 0x6B, 1],
+[0x3D3610, 0x5F, 0x6B, 0],
+[0x3D1A48, 0x9D, 0x31, 0],
+[0x3D1A46, 0xBF, 0x56, 0],
+[0x3D1A44, 0x7F, 0x6F, 0],
+[0x3D1A4A, 0xFA, 0x1C, 0],
+[0x3D1A88, 0x9D, 0x31, 1],
+[0x3D1A8A, 0xFA, 0x1C, 1],
+[0x3D1A86, 0xBF, 0x56, 1],
+[0x3D1A84, 0x7F, 0x6F, 1],
+[0x3CE282, 0x18, 0x00, 0],
+[0x3CE2C2, 0x18, 0x00, 1],
+[0x3CE280, 0x1F, 0x52, 0],
+[0x3CE2C0, 0x1F, 0x52, 1],
+[0x4FA29E, 0x18, 0x00, 0],
+[0x4FA2DE, 0x18, 0x00, 1],
+[0x4FA29C, 0x1F, 0x52, 0],
+[0x4FA2DC, 0x1F, 0x52, 1],
+[0x3D4786, 0x1F, 0x03, 1],
+[0x3D478C, 0x9F, 0x0B, 0],
+[0x3D478E, 0x1F, 0x03, 0],
+[0x3D4788, 0x3F, 0x25, 1],
+[0x3D4790, 0x3F, 0x25, 0],
+[0x3D478A, 0x1A, 0x00, 1],
+[0x3D4794, 0x50, 0x08, 0],
+[0x3D4792, 0x1A, 0x00, 0],
+[0x3D4784, 0xFE, 0x33, 0],
+[0x3C9E3A, 0xDF, 0x5A, 1],
+[0x3C9E40, 0xDF, 0x5A, 0],
+[0x3C9E38, 0x5F, 0x6B, 1],
+[0x3C9E3E, 0x5F, 0x6B, 0],
+[0x3C9E36, 0xDF, 0x7B, 1],
+[0x3C9E3C, 0xDF, 0x7B, 0],
+[0x4F4D5C, 0x3F, 0x04, 0],
+[0x4F4D5E, 0x17, 0x00, 0],
+[0x3C9320, 0x3F, 0x04, 0],
+[0x3C9322, 0x17, 0x00, 0],
+[0x9F9CF6, 0x3F, 0x04, 0],
+[0x9F9CF8, 0x17, 0x00, 0],
+[0x4F4E1C, 0x3F, 0x04, 1],
+[0x4F4E1E, 0x17, 0x00, 1],
+[0x3C9450, 0x3F, 0x04, 1],
+[0x3C9452, 0x17, 0x00, 1],
+[0x9F9D74, 0x3F, 0x04, 1],
+[0x9F9D76, 0x17, 0x00, 1],
+[0xA6202E, 0x13, 0x00, 0],
+[0xA62032, 0x7F, 0x0C, 0],
+[0xA62030, 0x59, 0x08, 0],
+[0xA62034, 0x3F, 0x5B, 0],
+[0xA6204E, 0x13, 0x00, 1],
+[0xA62052, 0x7F, 0x0C, 1],
+[0xA62050, 0x59, 0x08, 1],
+[0xA62054, 0x3F, 0x5B, 1],
+[0x3D4812, 0xBD, 0x14, 0],
+[0x3D480E, 0xBD, 0x14, 1],
+[0x3D4810, 0xBF, 0x35, 0],
+[0x3D480C, 0xBF, 0x35, 1],
+[0x3CC9FE, 0x3F, 0x04, 0],
+[0x3CCA0A, 0xF0, 0x00, 0],
+[0x8CBE20, 0x9C, 0x35, 0],
+[0x8CBE22, 0xF9, 0x28, 0],
+[0x8CBE1E, 0xFF, 0x3D, 0],
+[0x8CBE40, 0x9C, 0x35, 1],
+[0x8CBE42, 0xF9, 0x28, 1],
+[0x8CBE3E, 0xFF, 0x3D, 1],
+[0x8CBEE0, 0x3F, 0x04, 0],
+[0x8CBEDE, 0x7F, 0x4E, 0],
+[0x8CBEE2, 0x17, 0x00, 0],
+[0x3B8F38, 0x1F, 0x20, 1],
+[0x3B8F3A, 0x1F, 0x5A, 1],
+[0x3B8F40, 0x1F, 0x20, 0],
+[0x3B8F42, 0x1F, 0x5A, 0],
+[0x3D1094, 0x7C, 0x2D, 0],
+[0x3D109A, 0x2F, 0x00, 0],
+[0x3D1098, 0x36, 0x04, 0],
+[0x3D1096, 0x7A, 0x0C, 0],
+[0x3D1092, 0x7F, 0x4E, 0],
+[0x3D1090, 0x9F, 0x6F, 0],
+[0x3D10D4, 0x7C, 0x2D, 1],
+[0x3D10DA, 0x2F, 0x00, 1],
+[0x3D10D8, 0x36, 0x04, 1],
+[0x3D10D6, 0x7A, 0x0C, 1],
+[0x3D10D2, 0x7F, 0x4E, 1],
+[0x3D10D0, 0x9F, 0x6F, 1],
+[0x3D14D0, 0x3F, 0x04, 0],
+[0x3D14D2, 0x17, 0x00, 0],
+[0x3D14CE, 0x1F, 0x3E, 0],
+[0x3D14CC, 0x1F, 0x57, 0],
+[0x3D1510, 0xDE, 0x21, 1],
+[0x3D1512, 0x17, 0x00, 1],
+[0x3D150E, 0x1F, 0x3E, 1],
+[0x3D150C, 0x1F, 0x57, 1],
+[0x3CE0F4, 0x34, 0x04, 1],
+[0x3CE0F2, 0x3A, 0x04, 1],
+[0x3CE0F0, 0x1B, 0x3E, 1],
+[0x3CE0D4, 0x34, 0x04, 0],
+[0x3CE0D2, 0x3A, 0x04, 0],
+[0x3CE0D0, 0x1B, 0x3E, 0]]
+
+silhouetteHat = [
+[0x3CC878, 0xFF, 0x7F, 0],
+[0x3CC87A, 0xFF, 0x7F, 0],
+[0x3CC87C, 0xFF, 0x7F, 0],
+[0x3CC87E, 0xFF, 0x7F, 0],
+[0x3CC880, 0xFF, 0x7F, 0],
+[0x3CC882, 0xFF, 0x7F, 0],
+[0x3CC884, 0xFF, 0x7F, 0],
+[0x3CC886, 0xFF, 0x7F, 0],
+[0x3CC888, 0xFF, 0x7F, 0],
+[0x3CC88A, 0xFF, 0x7F, 0],
+[0x3CC88C, 0xFF, 0x7F, 0],
+[0x3CC88E, 0xFF, 0x7F, 0],
+[0x3CC890, 0xFF, 0x7F, 0],
+[0x3CC892, 0xFF, 0x7F, 0],
+[0x3CC8B8, 0xFF, 0x7F, 1],
+[0x3CC8BA, 0xFF, 0x7F, 1],
+[0x3CC8BC, 0xFF, 0x7F, 1],
+[0x3CC8BE, 0xFF, 0x7F, 1],
+[0x3CC8C0, 0xFF, 0x7F, 1],
+[0x3CC8C2, 0xFF, 0x7F, 1],
+[0x3CC8C4, 0xFF, 0x7F, 1],
+[0x3CC8C6, 0xFF, 0x7F, 1],
+[0x3CC8C8, 0xFF, 0x7F, 1],
+[0x3CC8CA, 0xFF, 0x7F, 1],
+[0x3CC8CC, 0xFF, 0x7F, 1],
+[0x3CC8CE, 0xFF, 0x7F, 1],
+[0x3CC8D0, 0xFF, 0x7F, 1],
+[0x3CC8D2, 0xFF, 0x7F, 1],
+[0x4F4CD0, 0xFF, 0x7F, 0],
+[0x4F4CD2, 0xFF, 0x7F, 0],
+[0x4F4CD4, 0xFF, 0x7F, 0],
+[0x4F4CD6, 0xFF, 0x7F, 0],
+[0x4F4CD8, 0xFF, 0x7F, 0],
+[0x4F4CDA, 0xFF, 0x7F, 0],
+[0x4F4CDC, 0xFF, 0x7F, 0],
+[0x4F4CDE, 0xFF, 0x7F, 0],
+[0x4F4CE0, 0xFF, 0x7F, 0],
+[0x4F4CE2, 0xFF, 0x7F, 0],
+[0x4F4CE4, 0xFF, 0x7F, 0],
+[0x4F4CE6, 0xFF, 0x7F, 0],
+[0x4F4CE8, 0xFF, 0x7F, 0],
+[0x4F4CEA, 0xFF, 0x7F, 0],
+[0x4F4D10, 0xFF, 0x7F, 1],
+[0x4F4D12, 0xFF, 0x7F, 1],
+[0x4F4D14, 0xFF, 0x7F, 1],
+[0x4F4D16, 0xFF, 0x7F, 1],
+[0x4F4D18, 0xFF, 0x7F, 1],
+[0x4F4D1A, 0xFF, 0x7F, 1],
+[0x4F4D1C, 0xFF, 0x7F, 1],
+[0x4F4D1E, 0xFF, 0x7F, 1],
+[0x4F4D20, 0xFF, 0x7F, 1],
+[0x4F4D22, 0xFF, 0x7F, 1],
+[0x4F4D24, 0xFF, 0x7F, 1],
+[0x4F4D26, 0xFF, 0x7F, 1],
+[0x4F4D28, 0xFF, 0x7F, 1],
+[0x4F4D2A, 0xFF, 0x7F, 1],
+[0x4F51D8, 0xFF, 0x7F, 1],
+[0x4F51DA, 0xFF, 0x7F, 1],
+[0x4F51DC, 0xFF, 0x7F, 0],
+[0x4F51DE, 0xFF, 0x7F, 0],
+[0x4F51E8, 0xFF, 0x7F, 0],
+[0x4F51EA, 0xFF, 0x7F, 1],
+[0x4FB684, 0xFF, 0x7F, 0],
+[0x4FB686, 0xFF, 0x7F, 0],
+[0x4FB688, 0xFF, 0x7F, 0],
+[0x4FB6A4, 0xFF, 0x7F, 1],
+[0x4FB6A6, 0xFF, 0x7F, 1],
+[0x4FB6A8, 0xFF, 0x7F, 1],
+[0x4FB692, 0xFF, 0x7F, 0],
+[0x4FB6B2, 0xFF, 0x7F, 1],
+[0x4FB786, 0xFF, 0x7F, 0],
+[0x4FB788, 0xFF, 0x7F, 0],
+[0x4FB78A, 0xFF, 0x7F, 0],
+[0x4FB7A6, 0xFF, 0x7F, 1],
+[0x4FB7A8, 0xFF, 0x7F, 1],
+[0x4FB7AA, 0xFF, 0x7F, 1],
+[0x519006, 0xFF, 0x7F, 0],
+[0x519008, 0xFF, 0x7F, 0],
+[0x51900C, 0xFF, 0x7F, 0],
+[0x51900E, 0xFF, 0x7F, 0],
+[0x519010, 0xFF, 0x7F, 0],
+[0x519012, 0xFF, 0x7F, 0],
+[0x519014, 0xFF, 0x7F, 0],
+[0x519016, 0xFF, 0x7F, 0],
+[0x519018, 0xFF, 0x7F, 0],
+[0x51901A, 0xFF, 0x7F, 0],
+[0x51901C, 0xFF, 0x7F, 0],
+[0x51901E, 0xFF, 0x7F, 0],
+[0x519020, 0xFF, 0x7F, 0],
+[0x519022, 0xFF, 0x7F, 0],
+[0x519026, 0xFF, 0x7F, 1],
+[0x519028, 0xFF, 0x7F, 1],
+[0x51902C, 0xFF, 0x7F, 1],
+[0x51902E, 0xFF, 0x7F, 1],
+[0x519030, 0xFF, 0x7F, 1],
+[0x519032, 0xFF, 0x7F, 1],
+[0x519034, 0xFF, 0x7F, 1],
+[0x519036, 0xFF, 0x7F, 1],
+[0x519038, 0xFF, 0x7F, 1],
+[0x51903A, 0xFF, 0x7F, 1],
+[0x51903C, 0xFF, 0x7F, 1],
+[0x51903E, 0xFF, 0x7F, 1],
+[0x519040, 0xFF, 0x7F, 1],
+[0x519042, 0xFF, 0x7F, 1],
+[0x5193D2, 0xFF, 0x7F, 0],
+[0x5193D4, 0xFF, 0x7F, 0],
+[0x5193D6, 0xFF, 0x7F, 0],
+[0x5193D8, 0xFF, 0x7F, 0],
+[0x5193DA, 0xFF, 0x7F, 0],
+[0x5193DC, 0xFF, 0x7F, 0],
+[0x5193DE, 0xFF, 0x7F, 0],
+[0x5193E0, 0xFF, 0x7F, 0],
+[0x5193E2, 0xFF, 0x7F, 0],
+[0x5193E4, 0xFF, 0x7F, 0],
+[0x5193E6, 0xFF, 0x7F, 0],
+[0x5193E8, 0xFF, 0x7F, 0],
+[0x5193EA, 0xFF, 0x7F, 0],
+[0x5193EC, 0xFF, 0x7F, 0],
+[0x5193F2, 0xFF, 0x7F, 1],
+[0x5193F4, 0xFF, 0x7F, 1],
+[0x5193F6, 0xFF, 0x7F, 1],
+[0x5193F8, 0xFF, 0x7F, 1],
+[0x5193FA, 0xFF, 0x7F, 1],
+[0x5193FC, 0xFF, 0x7F, 1],
+[0x5193FE, 0xFF, 0x7F, 1],
+[0x519400, 0xFF, 0x7F, 1],
+[0x519402, 0xFF, 0x7F, 1],
+[0x519404, 0xFF, 0x7F, 1],
+[0x519406, 0xFF, 0x7F, 1],
+[0x519408, 0xFF, 0x7F, 1],
+[0x51940A, 0xFF, 0x7F, 1],
+[0x51940C, 0xFF, 0x7F, 1],
+[0x3A7244, 0xFF, 0x7F, 0],
+[0x3A724A, 0xFF, 0x7F, 0],
+[0x3A724C, 0xFF, 0x7F, 1],
+[0x3A724E, 0xFF, 0x7F, 0],
+[0x3A7252, 0xFF, 0x7F, 0],
+[0x3A7254, 0xFF, 0x7F, 1],
+[0x3A7256, 0xFF, 0x7F, 0],
+[0x3A7264, 0xFF, 0x7F, 1],
+[0x3A726C, 0xFF, 0x7F, 1],
+[0x3A7270, 0xFF, 0x7F, 1],
+[0x3A7272, 0xFF, 0x7F, 1],
+[0x3A7274, 0xFF, 0x7F, 1],
+[0x3A7278, 0xFF, 0x7F, 1],
+[0x3A727A, 0xFF, 0x7F, 1],
+[0x3A727C, 0xFF, 0x7F, 1],
+[0x3A7286, 0xFF, 0x7F, 0],
+[0x3A7288, 0xFF, 0x7F, 0],
+[0x3A728C, 0xFF, 0x7F, 0],
+[0x3A7290, 0xFF, 0x7F, 0],
+[0x3A7294, 0xFF, 0x7F, 0],
+[0x3A7296, 0xFF, 0x7F, 0],
+[0x3A72AE, 0xFF, 0x7F, 1],
+[0x3A72B2, 0xFF, 0x7F, 1],
+[0x3A72BC, 0xFF, 0x7F, 1],
+[0x9F9A28, 0xFF, 0x7F, 0],
+[0x9F9A2A, 0xFF, 0x7F, 0],
+[0x9F9A2C, 0xFF, 0x7F, 0],
+[0x9F9A2E, 0xFF, 0x7F, 1],
+[0x9F9A30, 0xFF, 0x7F, 1],
+[0x9F9A32, 0xFF, 0x7F, 1],
+[0x9F9A34, 0xFF, 0x7F, 0],
+[0x9F9A36, 0xFF, 0x7F, 0],
+[0x9F9A38, 0xFF, 0x7F, 0],
+[0x9F9A3A, 0xFF, 0x7F, 1],
+[0x9F9A3C, 0xFF, 0x7F, 1],
+[0x9F9A3E, 0xFF, 0x7F, 1],
+[0x9F9A48, 0xFF, 0x7F, 0],
+[0x9F9A4A, 0xFF, 0x7F, 0],
+[0x9F9A4C, 0xFF, 0x7F, 0],
+[0x9F9A4E, 0xFF, 0x7F, 1],
+[0x9F9A50, 0xFF, 0x7F, 1],
+[0x9F9A52, 0xFF, 0x7F, 1],
+[0x9F9A56, 0xFF, 0x7F, 0],
+[0x9F9A58, 0xFF, 0x7F, 0],
+[0x9F9A5C, 0xFF, 0x7F, 1],
+[0x9F9A5E, 0xFF, 0x7F, 1],
+[0x9F9A26, 0xFF, 0x7F, 0],
+[0x9F9A46, 0xFF, 0x7F, 1],
+[0xA5DD32, 0xFF, 0x7F, 0],
+[0xA5DD34, 0xFF, 0x7F, 0],
+[0xA5DD38, 0xFF, 0x7F, 0],
+[0xA5DD3A, 0xFF, 0x7F, 0],
+[0xA5DD3C, 0xFF, 0x7F, 0],
+[0xA5DD3E, 0xFF, 0x7F, 0],
+[0xA5DD40, 0xFF, 0x7F, 0],
+[0xA5DD42, 0xFF, 0x7F, 0],
+[0xA5DD44, 0xFF, 0x7F, 0],
+[0xA5DD46, 0xFF, 0x7F, 0],
+[0xA5DD48, 0xFF, 0x7F, 0],
+[0xA5DD4A, 0xFF, 0x7F, 0],
+[0xA5DD4C, 0xFF, 0x7F, 0],
+[0xA5DD4E, 0x00, 0x00, 0],
+[0xA5DD52, 0xFF, 0x7F, 1],
+[0xA5DD54, 0xFF, 0x7F, 1],
+[0xA5DD58, 0xFF, 0x7F, 1],
+[0xA5DD5A, 0xFF, 0x7F, 1],
+[0xA5DD5C, 0xFF, 0x7F, 1],
+[0xA5DD5E, 0xFF, 0x7F, 1],
+[0xA5DD60, 0xFF, 0x7F, 1],
+[0xA5DD62, 0xFF, 0x7F, 1],
+[0xA5DD64, 0xFF, 0x7F, 1],
+[0xA5DD66, 0xFF, 0x7F, 1],
+[0xA5DD68, 0xFF, 0x7F, 1],
+[0xA5DD6A, 0xFF, 0x7F, 1],
+[0xA5DD6C, 0xFF, 0x7F, 1],
+[0xA5DD6E, 0x00, 0x00, 1],
+[0x3D0E08, 0xFF, 0x7F, 1],
+[0x3D0E0A, 0xFF, 0x7F, 1],
+[0x3D0E0C, 0xFF, 0x7F, 1],
+[0x3D0E16, 0xFF, 0x7F, 0],
+[0x3D0E18, 0xFF, 0x7F, 0],
+[0x3D0E1A, 0xFF, 0x7F, 0],
+[0x3CC9B8, 0xFF, 0x7F, 0],
+[0x3CC9BA, 0xFF, 0x7F, 0],
+[0x3CC9BC, 0xFF, 0x7F, 0],
+[0x3CC9BE, 0xFF, 0x7F, 0],
+[0x3CC9C0, 0xFF, 0x7F, 0],
+[0x3CC9C2, 0xFF, 0x7F, 0],
+[0x3CC9C4, 0xFF, 0x7F, 0],
+[0x3CC9C6, 0xFF, 0x7F, 0],
+[0x3CC9C8, 0xFF, 0x7F, 0],
+[0x3CC9CA, 0xFF, 0x7F, 0],
+[0x3CC9CC, 0xFF, 0x7F, 0],
+[0x3CC9CE, 0xFF, 0x7F, 0],
+[0x3CC9D0, 0xFF, 0x7F, 0],
+[0x3CC9D2, 0xFF, 0x7F, 0],
+[0x3CDE6E, 0xFF, 0x7F, 1],
+[0x3CDE70, 0xFF, 0x7F, 1],
+[0x3CDE72, 0xFF, 0x7F, 1],
+[0x3CDE74, 0xFF, 0x7F, 1],
+[0x3CDE7C, 0xFF, 0x7F, 1],
+[0x3CDE7E, 0xFF, 0x7F, 1],
+[0x3CDE80, 0xFF, 0x7F, 1],
+[0x3CDE82, 0xFF, 0x7F, 1],
+[0x3CDE84, 0xFF, 0x7F, 1],
+[0x3CDE86, 0xFF, 0x7F, 1],
+[0x3CDE88, 0xFF, 0x7F, 1],
+[0x3CDE8A, 0xFF, 0x7F, 1],
+[0x51AD1E, 0xFF, 0x7F, 0],
+[0x51AD20, 0xFF, 0x7F, 0],
+[0x51AD22, 0xFF, 0x7F, 0],
+[0x51AD24, 0xFF, 0x7F, 0],
+[0x51AD26, 0xFF, 0x7F, 0],
+[0x51AD28, 0xFF, 0x7F, 0],
+[0x51AD2A, 0xFF, 0x7F, 0],
+[0x51AD2C, 0xFF, 0x7F, 0],
+[0x51AD2E, 0xFF, 0x7F, 0],
+[0x51AD30, 0xFF, 0x7F, 0],
+[0x51AD32, 0xFF, 0x7F, 0],
+[0x51AD34, 0xFF, 0x7F, 0],
+[0x51AD36, 0xFF, 0x7F, 0],
+[0x51AD38, 0xFF, 0x7F, 0],
+[0x51AD3A, 0xFF, 0x7F, 0],
+[0x51AD3E, 0xFF, 0x7F, 1],
+[0x51AD40, 0xFF, 0x7F, 1],
+[0x51AD42, 0xFF, 0x7F, 1],
+[0x51AD44, 0xFF, 0x7F, 1],
+[0x51AD46, 0xFF, 0x7F, 1],
+[0x51AD48, 0xFF, 0x7F, 1],
+[0x51AD4A, 0xFF, 0x7F, 1],
+[0x51AD4C, 0xFF, 0x7F, 1],
+[0x51AD4E, 0xFF, 0x7F, 1],
+[0x51AD50, 0xFF, 0x7F, 1],
+[0x51AD52, 0xFF, 0x7F, 1],
+[0x51AD54, 0xFF, 0x7F, 1],
+[0x51AD56, 0xFF, 0x7F, 1],
+[0x51AD58, 0xFF, 0x7F, 1],
+[0x51AD5A, 0xFF, 0x7F, 1],
+[0x51A49E, 0xFF, 0x7F, 0],
+[0x51A4A0, 0xFF, 0x7F, 0],
+[0x51A4A2, 0xFF, 0x7F, 0],
+[0x51A4A4, 0xFF, 0x7F, 0],
+[0x51A4A6, 0xFF, 0x7F, 0],
+[0x51A4A8, 0xFF, 0x7F, 0],
+[0x51A4AA, 0xFF, 0x7F, 0],
+[0x51A4AC, 0xFF, 0x7F, 0],
+[0x51A4AE, 0xFF, 0x7F, 0],
+[0x51A4B0, 0xFF, 0x7F, 0],
+[0x51A4B2, 0xFF, 0x7F, 0],
+[0x51A4B4, 0xFF, 0x7F, 0],
+[0x51A4B6, 0xFF, 0x7F, 0],
+[0x51A4B8, 0xFF, 0x7F, 0],
+[0x51A4BA, 0xFF, 0x7F, 0],
+[0x51A4BE, 0xFF, 0x7F, 1],
+[0x51A4C0, 0xFF, 0x7F, 1],
+[0x51A4C2, 0xFF, 0x7F, 1],
+[0x51A4C4, 0xFF, 0x7F, 1],
+[0x51A4C6, 0xFF, 0x7F, 1],
+[0x51A4C8, 0xFF, 0x7F, 1],
+[0x51A4CA, 0xFF, 0x7F, 1],
+[0x51A4CC, 0xFF, 0x7F, 1],
+[0x51A4CE, 0xFF, 0x7F, 1],
+[0x51A4D0, 0xFF, 0x7F, 1],
+[0x51A4D2, 0xFF, 0x7F, 1],
+[0x51A4D4, 0xFF, 0x7F, 1],
+[0x51A4D6, 0xFF, 0x7F, 1],
+[0x51A4D8, 0xFF, 0x7F, 1],
+[0x51A4DA, 0xFF, 0x7F, 1],
+[0x51A97E, 0xFF, 0x7F, 0],
+[0x51A980, 0xFF, 0x7F, 0],
+[0x51A982, 0xFF, 0x7F, 0],
+[0x51A984, 0xFF, 0x7F, 0],
+[0x51A986, 0xFF, 0x7F, 0],
+[0x51A988, 0xFF, 0x7F, 0],
+[0x51A98A, 0xFF, 0x7F, 0],
+[0x51A98C, 0xFF, 0x7F, 0],
+[0x51A98E, 0xFF, 0x7F, 0],
+[0x51A990, 0xFF, 0x7F, 0],
+[0x51A992, 0xFF, 0x7F, 0],
+[0x51A994, 0xFF, 0x7F, 0],
+[0x51A996, 0xFF, 0x7F, 0],
+[0x51A998, 0xFF, 0x7F, 0],
+[0x51A99A, 0xFF, 0x7F, 0],
+[0x51A95E, 0xFF, 0x7F, 1],
+[0x51A960, 0xFF, 0x7F, 1],
+[0x51A962, 0xFF, 0x7F, 1],
+[0x51A964, 0xFF, 0x7F, 1],
+[0x51A966, 0xFF, 0x7F, 1],
+[0x51A968, 0xFF, 0x7F, 1],
+[0x51A96A, 0xFF, 0x7F, 1],
+[0x51A96C, 0xFF, 0x7F, 1],
+[0x51A96E, 0xFF, 0x7F, 1],
+[0x51A970, 0xFF, 0x7F, 1],
+[0x51A972, 0xFF, 0x7F, 1],
+[0x51A974, 0xFF, 0x7F, 1],
+[0x51A976, 0xFF, 0x7F, 1],
+[0x51A978, 0xFF, 0x7F, 1],
+[0x51A97A, 0xFF, 0x7F, 1],
+[0x51A9FE, 0xFF, 0x7F, 0],
+[0x51AA00, 0xFF, 0x7F, 0],
+[0x51AA02, 0xFF, 0x7F, 0],
+[0x51AA0A, 0xFF, 0x7F, 0],
+[0x51AA0E, 0xFF, 0x7F, 1],
+[0x51AA10, 0xFF, 0x7F, 1],
+[0x51AA12, 0xFF, 0x7F, 1],
+[0x3CC976, 0x7F, 0x4F, 0],
+[0x3CC978, 0xFF, 0x7F, 0],
+[0x3CC97A, 0xFF, 0x7F, 0],
+[0x3CC97C, 0xFF, 0x7F, 0],
+[0x3CC97E, 0xFF, 0x7F, 0],
+[0x3CC980, 0xFF, 0x7F, 0],
+[0x3CC982, 0xFF, 0x7F, 0],
+[0x3CC984, 0xFF, 0x7F, 0],
+[0x3CC986, 0xFF, 0x7F, 0],
+[0x3CC988, 0xFF, 0x7F, 0],
+[0x3CC98A, 0xFF, 0x7F, 0],
+[0x3CC98C, 0xFF, 0x7F, 0],
+[0x3CC98E, 0xFF, 0x7F, 0],
+[0x3CC990, 0xFF, 0x7F, 0],
+[0x3CC992, 0xFF, 0x7F, 0],
+[0x3D4B44, 0xFF, 0x7F, 0],
+[0x3D4B46, 0xFF, 0x7F, 0],
+[0x3D4B48, 0xFF, 0x7F, 0],
+[0x3D4B4A, 0xFF, 0x7F, 0],
+[0x3D4B4C, 0xFF, 0x7F, 0],
+[0x3D4B4E, 0xFF, 0x7F, 0],
+[0x3D4B50, 0xFF, 0x7F, 0],
+[0x3D4B52, 0xFF, 0x7F, 0],
+[0x3D4B54, 0xFF, 0x7F, 0],
+[0x3D4B56, 0xFF, 0x7F, 0],
+[0x3D4B58, 0xFF, 0x7F, 0],
+[0x3D4B5A, 0x00, 0x00, 0],
+[0x3D4B5C, 0xFF, 0x7F, 0],
+[0x3D4B5E, 0xFF, 0x7F, 0],
+[0x3D4B42, 0xFF, 0x7F, 0],
+[0x3CCA38, 0xFF, 0x7F, 1],
+[0x3CCA3A, 0xFF, 0x7F, 1],
+[0x3CCA3C, 0xFF, 0x7F, 1],
+[0x3CCA3E, 0xFF, 0x7F, 1],
+[0x3CCA40, 0xFF, 0x7F, 1],
+[0x3CCA42, 0xFF, 0x7F, 1],
+[0x3CCA44, 0xFF, 0x7F, 1],
+[0x3CCA46, 0xFF, 0x7F, 1],
+[0x3CCA48, 0xFF, 0x7F, 1],
+[0x3CCA4A, 0xFF, 0x7F, 1],
+[0x3CCA4C, 0xFF, 0x7F, 1],
+[0x3CCA4E, 0xFF, 0x7F, 1],
+[0x3CCA50, 0xFF, 0x7F, 1],
+[0x3CCA52, 0xFF, 0x7F, 1],
+[0x3CCA36, 0xFF, 0x7F, 1],
+[0x3CFB30, 0xFF, 0x7F, 0],
+[0x3CFB32, 0xFF, 0x7F, 0],
+[0x3CFB34, 0xFF, 0x7F, 0],
+[0x3CFB36, 0xFF, 0x7F, 0],
+[0x3CFB38, 0xFF, 0x7F, 0],
+[0x3CFB3A, 0xFF, 0x7F, 0],
+[0x3CFB3C, 0xFF, 0x7F, 0],
+[0x3CFB3E, 0xFF, 0x7F, 0],
+[0x3CFB40, 0xFF, 0x7F, 0],
+[0x3CFB42, 0xFF, 0x7F, 0],
+[0x3CFB44, 0xFF, 0x7F, 0],
+[0x3CFB46, 0xFF, 0x7F, 0],
+[0x3CFB48, 0xFF, 0x7F, 0],
+[0x3CFB4A, 0xFF, 0x7F, 0],
+[0x3CFB70, 0xFF, 0x7F, 1],
+[0x3CFB72, 0xFF, 0x7F, 1],
+[0x3CFB74, 0xFF, 0x7F, 1],
+[0x3CFB76, 0xFF, 0x7F, 1],
+[0x3CFB78, 0xFF, 0x7F, 1],
+[0x3CFB7A, 0xFF, 0x7F, 1],
+[0x3CFB7C, 0xFF, 0x7F, 1],
+[0x3CFB7E, 0xFF, 0x7F, 1],
+[0x3CFB80, 0xFF, 0x7F, 1],
+[0x3CFB82, 0xFF, 0x7F, 1],
+[0x3CFB84, 0xFF, 0x7F, 1],
+[0x3CFB86, 0xFF, 0x7F, 1],
+[0x3CFB88, 0xFF, 0x7F, 1],
+[0x3CFB8A, 0xFF, 0x7F, 1],
+[0x3D5044, 0xFF, 0x7F, 0],
+[0x3D5046, 0xFF, 0x7F, 0],
+[0x3D5048, 0xFF, 0x7F, 0],
+[0x3D504A, 0xFF, 0x7F, 0],
+[0x3D504C, 0xFF, 0x7F, 0],
+[0x3D504E, 0xFF, 0x7F, 0],
+[0x3D5050, 0xFF, 0x7F, 0],
+[0x3D5052, 0xFF, 0x7F, 0],
+[0x3D5054, 0xFF, 0x7F, 0],
+[0x3D5056, 0xFF, 0x7F, 0],
+[0x3D505C, 0xFF, 0x7F, 0],
+[0x3D505E, 0xFF, 0x7F, 0],
+[0x3D5084, 0xFF, 0x7F, 1],
+[0x3D5086, 0xFF, 0x7F, 1],
+[0x3D5088, 0xFF, 0x7F, 1],
+[0x3D508A, 0xFF, 0x7F, 1],
+[0x3D508C, 0xFF, 0x7F, 1],
+[0x3D508E, 0xFF, 0x7F, 1],
+[0x3D5090, 0xFF, 0x7F, 1],
+[0x3D5092, 0xFF, 0x7F, 1],
+[0x3D5094, 0xFF, 0x7F, 1],
+[0x3D5096, 0xFF, 0x7F, 1],
+[0x3D509C, 0xFF, 0x7F, 1],
+[0x3D509E, 0xFF, 0x7F, 1],
+[0xA5DDA2, 0xFF, 0x7F, 0],
+[0xA5DDA4, 0x00, 0x00, 0],
+[0xA5DDA6, 0xFF, 0x7F, 0],
+[0xA5DDA8, 0xFF, 0x7F, 0],
+[0xA5DDAE, 0x00, 0x00, 0],
+[0xA5DDC2, 0xFF, 0x7F, 1],
+[0xA5DDC4, 0x00, 0x00, 1],
+[0xA5DDC6, 0xFF, 0x7F, 1],
+[0xA5DDC8, 0xFF, 0x7F, 1],
+[0xA5DDCE, 0x00, 0x00, 1],
+[0x3D3E0C, 0xFF, 0x7F, 1],
+[0x3D3E0E, 0xFF, 0x7F, 1],
+[0x3D3E10, 0xFF, 0x7F, 0],
+[0x3D3E12, 0xFF, 0x7F, 0],
+[0x3CF1C0, 0xFF, 0x7F, 0],
+[0x3CF1C2, 0xFF, 0x7F, 0],
+[0x3CF200, 0xFF, 0x7F, 1],
+[0x3CF202, 0xFF, 0x7F, 1],
+[0x3D3604, 0xFF, 0x7F, 0],
+[0x3D3606, 0xFF, 0x7F, 1],
+[0x3D360A, 0xFF, 0x7F, 1],
+[0x3D360C, 0xFF, 0x7F, 1],
+[0x3D360E, 0xFF, 0x7F, 1],
+[0x3D3610, 0xFF, 0x7F, 0],
+[0x3D3612, 0xFF, 0x7F, 0],
+[0x3D3614, 0xFF, 0x7F, 0],
+[0x3D1A42, 0xFF, 0x7F, 0],
+[0x3D1A44, 0xFF, 0x7F, 0],
+[0x3D1A46, 0xFF, 0x7F, 0],
+[0x3D1A48, 0xFF, 0x7F, 0],
+[0x3D1A4A, 0xFF, 0x7F, 0],
+[0x3D1A82, 0xFF, 0x7F, 1],
+[0x3D1A84, 0xFF, 0x7F, 1],
+[0x3D1A86, 0xFF, 0x7F, 1],
+[0x3D1A88, 0xFF, 0x7F, 1],
+[0x3D1A8A, 0xFF, 0x7F, 1],
+[0x3CE280, 0xFF, 0x7F, 0],
+[0x3CE282, 0xFF, 0x7F, 0],
+[0x3CE2C0, 0xFF, 0x7F, 1],
+[0x3CE2C2, 0xFF, 0x7F, 1],
+[0x4FA29C, 0xFF, 0x7F, 0],
+[0x4FA29E, 0xFF, 0x7F, 0],
+[0x4FA2DC, 0xFF, 0x7F, 1],
+[0x4FA2DE, 0xFF, 0x7F, 1],
+[0x8D3C5E, 0xFF, 0x7F, 0],
+[0x8D3C60, 0xFF, 0x7F, 0],
+[0x8D3C62, 0xFF, 0x7F, 0],
+[0x8D3C64, 0xFF, 0x7F, 0],
+[0x8D3B9C, 0xAE, 0x7E, 0],
+[0x8D3BA0, 0x20, 0x4E, 0],
+[0x8D3B9E, 0x6C, 0x7E, 0],
+[0x3D4C02, 0xFF, 0x7F, 0],
+[0x3D4C0C, 0xFF, 0x7F, 0],
+[0x3D4C0E, 0xFF, 0x7F, 0],
+[0x3D4C10, 0xFF, 0x7F, 0],
+[0x3D4C12, 0xFF, 0x7F, 0],
+[0x3D4C14, 0xFF, 0x7F, 0],
+[0x3D2D96, 0xFF, 0x7F, 0],
+[0x3D2D98, 0xFF, 0x7F, 0],
+[0x3D2D9A, 0xFF, 0x7F, 0],
+[0x3D2D9C, 0xFF, 0x7F, 0],
+[0x8CC272, 0xFF, 0x7F, 1],
+[0x8CC274, 0xFF, 0x7F, 1],
+[0x8CC276, 0xFF, 0x7F, 1],
+[0x8CC278, 0x00, 0x00, 1],
+[0x8CC27A, 0x00, 0x00, 1],
+[0x8CC27C, 0x00, 0x00, 1],
+[0x3D4784, 0xFF, 0x7F, 0],
+[0x3D4786, 0xFF, 0x7F, 1],
+[0x3D4788, 0xFF, 0x7F, 1],
+[0x3D478A, 0x00, 0x00, 1],
+[0x3D478C, 0xFF, 0x7F, 0],
+[0x3D478E, 0xFF, 0x7F, 0],
+[0x3D4790, 0xFF, 0x7F, 0],
+[0x3D4792, 0xFF, 0x7F, 0],
+[0x3D4794, 0x00, 0x00, 0],
+[0x8CF4B2, 0xFF, 0x7F, 1],
+[0x8CF4B4, 0xFF, 0x7F, 1],
+[0x8CF4B6, 0xFF, 0x7F, 1],
+[0x8CF4B8, 0x00, 0x00, 1],
+[0x8CF4BA, 0x00, 0x00, 1],
+[0x8CF4BC, 0x00, 0x00, 1],
+[0x8CF7B2, 0xFF, 0x7F, 1],
+[0x8CF7B4, 0xFF, 0x7F, 1],
+[0x8CF7B6, 0xFF, 0x7F, 1],
+[0x8CF7B8, 0x00, 0x00, 1],
+[0x8CF7BA, 0x00, 0x00, 1],
+[0x8CF7BC, 0x00, 0x00, 1],
+[0x8D2D52, 0xFF, 0x7F, 1],
+[0x8D2D54, 0xFF, 0x7F, 1],
+[0x8D2D56, 0xFF, 0x7F, 1],
+[0x8D2D58, 0x00, 0x00, 1],
+[0x8D2D5A, 0x00, 0x00, 1],
+[0x8D2D5C, 0x00, 0x00, 1],
+[0x8D0096, 0xFF, 0x7F, 1],
+[0x8D0A16, 0xFF, 0x7F, 1],
+[0x8D006E, 0xFF, 0x7F, 1],
+[0x8D0070, 0xFF, 0x7F, 1],
+[0x8D09AE, 0xFF, 0x7F, 1],
+[0x8D09B0, 0xFF, 0x7F, 1],
+[0x3CE8F6, 0xFF, 0x7F, 1],
+[0x3CE8F8, 0xFF, 0x7F, 1],
+[0x3CE8FA, 0xFF, 0x7F, 1],
+[0x8D0952, 0xFF, 0x7F, 1],
+[0x8D0954, 0xFF, 0x7F, 1],
+[0x8D0956, 0x00, 0x00, 1],
+[0x8D0958, 0xFF, 0x7F, 1],
+[0x8D095A, 0x00, 0x00, 1],
+[0x8D095C, 0x00, 0x00, 1],
+[0x3CE976, 0xFF, 0x7F, 1],
+[0x3CE978, 0xFF, 0x7F, 1],
+[0x3CE97A, 0xFF, 0x7F, 1],
+[0x4F8912, 0xFF, 0x7F, 1],
+[0x4F8914, 0xFF, 0x7F, 1],
+[0x4F8916, 0xFF, 0x7F, 1],
+[0x8D0456, 0xFF, 0x7F, 0],
+[0x8D09F6, 0xFF, 0x7F, 0],
+[0x8D042E, 0xFF, 0x7F, 0],
+[0x8D0430, 0xFF, 0x7F, 0],
+[0x8D09CE, 0xFF, 0x7F, 0],
+[0x8D09D0, 0xFF, 0x7F, 0],
+[0x3CE8B6, 0xFF, 0x7F, 0],
+[0x3CE8B8, 0xFF, 0x7F, 0],
+[0x3CE8BA, 0xFF, 0x7F, 0],
+[0x8D0890, 0xFF, 0x7F, 0],
+[0x8D0892, 0xFF, 0x7F, 0],
+[0x8D0894, 0xFF, 0x7F, 0],
+[0x8D0896, 0xFF, 0x7F, 0],
+[0x3CE932, 0xFF, 0x7F, 0],
+[0x3CE936, 0xFF, 0x7F, 0],
+[0x3CE938, 0xFF, 0x7F, 0],
+[0x3CE93A, 0xFF, 0x7F, 0],
+[0x3CE940, 0xFF, 0x7F, 0],
+[0x4F88CE, 0xFF, 0x7F, 0],
+[0x4F88D2, 0xFF, 0x7F, 0],
+[0x4F88D4, 0xFF, 0x7F, 0],
+[0x4F88D6, 0xFF, 0x7F, 0],
+[0x4F88DC, 0xFF, 0x7F, 0],
+[0x8D095E, 0xFF, 0x7F, 0],
+[0x8D0960, 0xFF, 0x7F, 0],
+[0x8D0962, 0xFF, 0x7F, 0],
+[0x8D0964, 0xFF, 0x7F, 0],
+[0x3D2104, 0xFF, 0x7F, 0],
+[0x3D210E, 0xFF, 0x7F, 0],
+[0x3D2110, 0xFF, 0x7F, 0],
+[0x3D2118, 0xFF, 0x7F, 0],
+[0x3D211A, 0xFF, 0x7F, 0],
+[0x8D0052, 0xFF, 0x7F, 1],
+[0x8D0054, 0xFF, 0x7F, 1],
+[0x8D0056, 0xFF, 0x7F, 1],
+[0x8D0058, 0x00, 0x00, 1],
+[0x8D005A, 0x00, 0x00, 1],
+[0x8D005C, 0x00, 0x00, 1],
+[0x8D005E, 0xFF, 0x7F, 0],
+[0x8D0060, 0xFF, 0x7F, 0],
+[0x8D0062, 0xFF, 0x7F, 0],
+[0x8D0064, 0xFF, 0x7F, 0],
+[0x3C9E36, 0xFF, 0x7F, 1],
+[0x3C9E38, 0xFF, 0x7F, 1],
+[0x3C9E3A, 0xFF, 0x7F, 1],
+[0x3C9E3C, 0xFF, 0x7F, 0],
+[0x3C9E3E, 0xFF, 0x7F, 0],
+[0x3C9E40, 0xFF, 0x7F, 0],
+[0x4F4D50, 0xFF, 0x7F, 0],
+[0x4F4D52, 0xFF, 0x7F, 0],
+[0x4F4D54, 0xFF, 0x7F, 0],
+[0x4F4D56, 0xFF, 0x7F, 0],
+[0x4F4D58, 0xFF, 0x7F, 0],
+[0x4F4D5A, 0xFF, 0x7F, 0],
+[0x4F4D5C, 0xFF, 0x7F, 0],
+[0x4F4D5E, 0xFF, 0x7F, 0],
+[0x4F4D60, 0xFF, 0x7F, 0],
+[0x4F4D62, 0xFF, 0x7F, 0],
+[0x4F4D64, 0xFF, 0x7F, 0],
+[0x4F4D66, 0xFF, 0x7F, 0],
+[0x4F4D68, 0xFF, 0x7F, 0],
+[0x4F4D6A, 0xFF, 0x7F, 0],
+[0x3C9314, 0xFF, 0x7F, 0],
+[0x3C9316, 0xBD, 0x7B, 0],
+[0x3C9318, 0x39, 0x6F, 0],
+[0x3C931A, 0xB5, 0x62, 0],
+[0x3C931C, 0xB5, 0x56, 0],
+[0x3C931E, 0x31, 0x46, 0],
+[0x3C9320, 0xDE, 0x7F, 0],
+[0x3C9322, 0x5A, 0x6F, 0],
+[0x3C9324, 0x89, 0x52, 0],
+[0x3C9326, 0x08, 0x46, 0],
+[0x3C9328, 0x39, 0x67, 0],
+[0x3C932A, 0x73, 0x4E, 0],
+[0x3C932C, 0x10, 0x42, 0],
+[0x3C932E, 0xAD, 0x35, 0],
+[0x9F9CEA, 0xFF, 0x7F, 0],
+[0x9F9CEC, 0xBD, 0x7B, 0],
+[0x9F9CEE, 0x39, 0x6F, 0],
+[0x9F9CF0, 0xB5, 0x62, 0],
+[0x9F9CF2, 0xB5, 0x56, 0],
+[0x9F9CF4, 0x31, 0x46, 0],
+[0x9F9CF6, 0xDE, 0x7F, 0],
+[0x9F9CF8, 0x5A, 0x6F, 0],
+[0x9F9CFA, 0x89, 0x52, 0],
+[0x9F9CFC, 0x08, 0x46, 0],
+[0x9F9CFE, 0x39, 0x67, 0],
+[0x9F9D00, 0x73, 0x4E, 0],
+[0x9F9D02, 0x10, 0x42, 0],
+[0x9F9D04, 0xAD, 0x35, 0],
+[0x4F4E10, 0xFF, 0x7F, 1],
+[0x4F4E12, 0xFF, 0x7F, 1],
+[0x4F4E14, 0xFF, 0x7F, 1],
+[0x4F4E16, 0xFF, 0x7F, 1],
+[0x4F4E18, 0xFF, 0x7F, 1],
+[0x4F4E1A, 0xFF, 0x7F, 1],
+[0x4F4E1C, 0xFF, 0x7F, 1],
+[0x4F4E1E, 0xFF, 0x7F, 1],
+[0x4F4E20, 0xFF, 0x7F, 1],
+[0x4F4E22, 0xFF, 0x7F, 1],
+[0x4F4E24, 0xFF, 0x7F, 1],
+[0x4F4E26, 0xFF, 0x7F, 1],
+[0x4F4E28, 0xFF, 0x7F, 1],
+[0x4F4E2A, 0xFF, 0x7F, 1],
+[0x3C9444, 0xFF, 0x7F, 1],
+[0x3C9446, 0xBD, 0x7B, 1],
+[0x3C9448, 0x39, 0x6F, 1],
+[0x3C944A, 0xB5, 0x62, 1],
+[0x3C944C, 0xB5, 0x56, 1],
+[0x3C944E, 0x31, 0x46, 1],
+[0x3C9450, 0xDE, 0x7F, 1],
+[0x3C9452, 0x5A, 0x6F, 1],
+[0x3C9454, 0x89, 0x52, 1],
+[0x3C9456, 0x08, 0x46, 1],
+[0x3C9458, 0x39, 0x67, 1],
+[0x3C945A, 0x73, 0x4E, 1],
+[0x3C945C, 0x10, 0x42, 1],
+[0x3C945E, 0xAD, 0x35, 1],
+[0x9F9D68, 0xFF, 0x7F, 1],
+[0x9F9D6A, 0xBD, 0x7B, 1],
+[0x9F9D6C, 0x39, 0x6F, 1],
+[0x9F9D6E, 0xB5, 0x62, 1],
+[0x9F9D70, 0xB5, 0x56, 1],
+[0x9F9D72, 0x31, 0x46, 1],
+[0x9F9D74, 0xDE, 0x7F, 1],
+[0x9F9D76, 0x5A, 0x6F, 1],
+[0x9F9D78, 0x89, 0x52, 1],
+[0x9F9D7A, 0x08, 0x46, 1],
+[0x9F9D7C, 0x39, 0x67, 1],
+[0x9F9D7E, 0x73, 0x4E, 1],
+[0x9F9D80, 0x10, 0x42, 1],
+[0x9F9D82, 0xAD, 0x35, 1],
+[0x518E12, 0xFF, 0x7F, 0],
+[0x518E10, 0xFF, 0x7F, 0],
+[0x518E1A, 0xFF, 0x7F, 0],
+[0x518E18, 0xFF, 0x7F, 0],
+[0xA6202E, 0xFF, 0x7F, 0],
+[0xA62030, 0xFF, 0x7F, 0],
+[0xA62032, 0xFF, 0x7F, 0],
+[0xA62034, 0xFF, 0x7F, 0],
+[0xA6203A, 0x00, 0x00, 0],
+[0xA6204E, 0xFF, 0x7F, 1],
+[0xA62050, 0xFF, 0x7F, 1],
+[0xA62052, 0xFF, 0x7F, 1],
+[0xA62054, 0xFF, 0x7F, 1],
+[0xA6205A, 0x00, 0x00, 1],
+[0x3D4804, 0xFF, 0x7F, 0],
+[0x3D4806, 0xFF, 0x7F, 0],
+[0x3D4808, 0xFF, 0x7F, 0],
+[0x3D480A, 0xFF, 0x7F, 0],
+[0x3D4810, 0xFF, 0x7F, 0],
+[0x3D4812, 0xFF, 0x7F, 0],
+[0x3D480C, 0xFF, 0x7F, 1],
+[0x3D480E, 0xFF, 0x7F, 1],
+[0x8D049E, 0xFF, 0x7F, 0],
+[0x8D04A0, 0xFF, 0x7F, 0],
+[0x8D04A2, 0xFF, 0x7F, 0],
+[0x8D0592, 0xFF, 0x7F, 1],
+[0x8D0594, 0xFF, 0x7F, 1],
+[0x8D0596, 0xFF, 0x7F, 1],
+[0x8D0598, 0x00, 0x00, 1],
+[0x8D059A, 0x00, 0x00, 1],
+[0x8D059C, 0x00, 0x00, 1],
+[0x4F5010, 0xFF, 0x7F, 0],
+[0x4F5012, 0xFF, 0x7F, 0],
+[0x4F5014, 0xFF, 0x7F, 0],
+[0x4F5016, 0xFF, 0x7F, 0],
+[0x4F4ECE, 0xFF, 0x7F, 0],
+[0x4F4ED0, 0xFF, 0x7F, 0],
+[0x4F4ED2, 0xFF, 0x7F, 0],
+[0x4F4ED4, 0xFF, 0x7F, 0],
+[0x4F4ED6, 0xFF, 0x7F, 0],
+[0x4F4ED8, 0xFF, 0x7F, 0],
+[0x4F4EDA, 0xFF, 0x7F, 1],
+[0x4F4EDC, 0xFF, 0x7F, 1],
+[0x4F4EDE, 0xFF, 0x7F, 1],
+[0x4F4EE0, 0xFF, 0x7F, 1],
+[0x4F4EE2, 0xFF, 0x7F, 1],
+[0x3CC9F8, 0xFF, 0x7F, 0],
+[0x3CC9FA, 0xFF, 0x7F, 0],
+[0x3CC9FC, 0xFF, 0x7F, 0],
+[0x3CC9FE, 0xFF, 0x7F, 0],
+[0x3CCA00, 0xFF, 0x7F, 0],
+[0x3CCA02, 0xFF, 0x7F, 0],
+[0x3CCA0A, 0xFF, 0x7F, 0],
+[0x3CCA0C, 0xFF, 0x7F, 0],
+[0x3CCA0E, 0xFF, 0x7F, 0],
+[0x3CCA10, 0xFF, 0x7F, 0],
+[0x3CCA12, 0xFF, 0x7F, 0],
+[0x8CBE1E, 0xFF, 0x7F, 0],
+[0x8CBE20, 0xFF, 0x7F, 0],
+[0x8CBE22, 0xFF, 0x7F, 0],
+[0x8CBE3E, 0xFF, 0x7F, 1],
+[0x8CBE40, 0xFF, 0x7F, 1],
+[0x8CBE42, 0xFF, 0x7F, 1],
+[0x8CBEDE, 0xFF, 0x7F, 0],
+[0x8CBEE0, 0xFF, 0x7F, 0],
+[0x8CBED4, 0xFF, 0x7F, 0],
+[0x8CBEE2, 0xFF, 0x7F, 0],
+[0x8CBECA, 0xFF, 0x7F, 0],
+[0x3B8F38, 0xFF, 0x7F, 1],
+[0x3B8F3A, 0xFF, 0x7F, 1],
+[0x3B8F40, 0xFF, 0x7F, 0],
+[0x3B8F42, 0xFF, 0x7F, 0],
+[0x3D1090, 0xFF, 0x7F, 0],
+[0x3D1092, 0xFF, 0x7F, 0],
+[0x3D1094, 0xFF, 0x7F, 0],
+[0x3D1096, 0xFF, 0x7F, 0],
+[0x3D1098, 0xFF, 0x7F, 0],
+[0x3D109A, 0x00, 0x00, 0],
+[0x3D109C, 0xFF, 0x7F, 0],
+[0x3D109E, 0xFF, 0x7F, 0],
+[0x3D1082, 0xFF, 0x7F, 0],
+[0x3D1084, 0xFF, 0x7F, 0],
+[0x3D1086, 0xFF, 0x7F, 0],
+[0x3D10C2, 0xFF, 0x7F, 1],
+[0x3D10C4, 0xFF, 0x7F, 1],
+[0x3D10C6, 0xFF, 0x7F, 1],
+[0x3D10D0, 0xFF, 0x7F, 1],
+[0x3D10D2, 0xFF, 0x7F, 1],
+[0x3D10D4, 0xFF, 0x7F, 1],
+[0x3D10D6, 0xFF, 0x7F, 1],
+[0x3D10D8, 0xFF, 0x7F, 1],
+[0x3D10DA, 0x00, 0x00, 1],
+[0x3D10DC, 0xFF, 0x7F, 1],
+[0x3D10DE, 0xFF, 0x7F, 1],
+[0x3D14C2, 0xFF, 0x7F, 0],
+[0x3D14C4, 0xFF, 0x7F, 0],
+[0x3D14C6, 0xFF, 0x7F, 0],
+[0x3D14C8, 0xFF, 0x7F, 0],
+[0x3D14CA, 0xFF, 0x7F, 0],
+[0x3D14CC, 0xFF, 0x7F, 0],
+[0x3D14CE, 0xFF, 0x7F, 0],
+[0x3D14D0, 0xFF, 0x7F, 0],
+[0x3D14D2, 0xFF, 0x7F, 0],
+[0x3D14D4, 0xFF, 0x7F, 0],
+[0x3D14D6, 0xFF, 0x7F, 0],
+[0x3D1502, 0xFF, 0x7F, 1],
+[0x3D1504, 0xFF, 0x7F, 1],
+[0x3D1506, 0xFF, 0x7F, 1],
+[0x3D1508, 0xFF, 0x7F, 1],
+[0x3D150A, 0xFF, 0x7F, 1],
+[0x3D150C, 0xFF, 0x7F, 1],
+[0x3D150E, 0xFF, 0x7F, 1],
+[0x3D1510, 0xFF, 0x7F, 1],
+[0x3D1512, 0xFF, 0x7F, 1],
+[0x3D1514, 0xFF, 0x7F, 1],
+[0x3D1516, 0xFF, 0x7F, 1],
+[0x3CE0F0, 0xFF, 0x7F, 1],
+[0x3CE0F2, 0xFF, 0x7F, 1],
+[0x3CE0F4, 0xFF, 0x7F, 1],
+[0x3CE0D0, 0xFF, 0x7F, 0],
+[0x3CE0D2, 0xFF, 0x7F, 0],
+[0x3CE0D4, 0xFF, 0x7F, 0],
+[0x8D3DAC, 0xFF, 0x7F, 0],
+[0x8D3DBC, 0xFF, 0x7F, 0],
+[0x8D3DB4, 0xFF, 0x7F, 0],
+[0x8D3D86, 0xFF, 0x7F, 0],
+[0x8D3DBA, 0xFF, 0x7F, 0],
+[0x8D3DB8, 0xFF, 0x7F, 0]]
+
+truechaosHat = [
+[0x3CC878, 0],
+[0x3CC87A, 0],
+[0x3CC87C, 0],
+[0x3CC87E, 0],
+[0x3CC880, 0],
+[0x3CC882, 0],
+[0x3CC884, 0],
+[0x3CC886, 0],
+[0x3CC888, 0],
+[0x3CC88A, 0],
+[0x3CC88C, 0],
+[0x3CC88E, 0],
+[0x3CC890, 0],
+[0x3CC892, 0],
+[0x3CC8B8, 1],
+[0x3CC8BA, 1],
+[0x3CC8BC, 1],
+[0x3CC8BE, 1],
+[0x3CC8C0, 1],
+[0x3CC8C2, 1],
+[0x3CC8C4, 1],
+[0x3CC8C6, 1],
+[0x3CC8C8, 1],
+[0x3CC8CA, 1],
+[0x3CC8CC, 1],
+[0x3CC8CE, 1],
+[0x3CC8D0, 1],
+[0x3CC8D2, 1],
+[0x4F4CD0, 0],
+[0x4F4CD2, 0],
+[0x4F4CD4, 0],
+[0x4F4CD6, 0],
+[0x4F4CD8, 0],
+[0x4F4CDA, 0],
+[0x4F4CDC, 0],
+[0x4F4CDE, 0],
+[0x4F4CE0, 0],
+[0x4F4CE2, 0],
+[0x4F4CE4, 0],
+[0x4F4CE6, 0],
+[0x4F4CE8, 0],
+[0x4F4CEA, 0],
+[0x4F4D10, 1],
+[0x4F4D12, 1],
+[0x4F4D14, 1],
+[0x4F4D16, 1],
+[0x4F4D18, 1],
+[0x4F4D1A, 1],
+[0x4F4D1C, 1],
+[0x4F4D1E, 1],
+[0x4F4D20, 1],
+[0x4F4D22, 1],
+[0x4F4D24, 1],
+[0x4F4D26, 1],
+[0x4F4D28, 1],
+[0x4F4D2A, 1],
+[0x4F51D8, 1],
+[0x4F51DA, 1],
+[0x4F51DC, 0],
+[0x4F51DE, 0],
+[0x4F51E8, 0],
+[0x4F51EA, 1],
+[0x4FB684, 0],
+[0x4FB686, 0],
+[0x4FB688, 0],
+[0x4FB6A4, 1],
+[0x4FB6A6, 1],
+[0x4FB6A8, 1],
+[0x4FB692, 0],
+[0x4FB6B2, 1],
+[0x4FB786, 0],
+[0x4FB788, 0],
+[0x4FB78A, 0],
+[0x4FB7A6, 1],
+[0x4FB7A8, 1],
+[0x4FB7AA, 1],
+[0x519006, 0],
+[0x519008, 0],
+[0x51900C, 0],
+[0x51900E, 0],
+[0x519010, 0],
+[0x519012, 0],
+[0x519014, 0],
+[0x519016, 0],
+[0x519018, 0],
+[0x51901A, 0],
+[0x51901C, 0],
+[0x51901E, 0],
+[0x519020, 0],
+[0x519022, 0],
+[0x519026, 1],
+[0x519028, 1],
+[0x51902C, 1],
+[0x51902E, 1],
+[0x519030, 1],
+[0x519032, 1],
+[0x519034, 1],
+[0x519036, 1],
+[0x519038, 1],
+[0x51903A, 1],
+[0x51903C, 1],
+[0x51903E, 1],
+[0x519040, 1],
+[0x519042, 1],
+[0x5193D2, 0],
+[0x5193D4, 0],
+[0x5193D6, 0],
+[0x5193D8, 0],
+[0x5193DA, 0],
+[0x5193DC, 0],
+[0x5193DE, 0],
+[0x5193E0, 0],
+[0x5193E2, 0],
+[0x5193E4, 0],
+[0x5193E6, 0],
+[0x5193E8, 0],
+[0x5193EA, 0],
+[0x5193EC, 0],
+[0x5193F2, 1],
+[0x5193F4, 1],
+[0x5193F6, 1],
+[0x5193F8, 1],
+[0x5193FA, 1],
+[0x5193FC, 1],
+[0x5193FE, 1],
+[0x519400, 1],
+[0x519402, 1],
+[0x519404, 1],
+[0x519406, 1],
+[0x519408, 1],
+[0x51940A, 1],
+[0x51940C, 1],
+[0x3A7244, 0],
+[0x3A724A, 0],
+[0x3A724C, 1],
+[0x3A724E, 0],
+[0x3A7252, 0],
+[0x3A7254, 1],
+[0x3A7256, 0],
+[0x3A7264, 1],
+[0x3A726C, 1],
+[0x3A7270, 1],
+[0x3A7272, 1],
+[0x3A7274, 1],
+[0x3A7278, 1],
+[0x3A727A, 1],
+[0x3A727C, 1],
+[0x3A7286, 0],
+[0x3A7288, 0],
+[0x3A728C, 0],
+[0x3A7290, 0],
+[0x3A7294, 0],
+[0x3A7296, 0],
+[0x3A72AE, 1],
+[0x3A72B2, 1],
+[0x3A72BC, 1],
+[0x9F9A28, 0],
+[0x9F9A2A, 0],
+[0x9F9A2C, 0],
+[0x9F9A2E, 1],
+[0x9F9A30, 1],
+[0x9F9A32, 1],
+[0x9F9A34, 0],
+[0x9F9A36, 0],
+[0x9F9A38, 0],
+[0x9F9A3A, 1],
+[0x9F9A3C, 1],
+[0x9F9A3E, 1],
+[0x9F9A48, 0],
+[0x9F9A4A, 0],
+[0x9F9A4C, 0],
+[0x9F9A4E, 1],
+[0x9F9A50, 1],
+[0x9F9A52, 1],
+[0x9F9A56, 0],
+[0x9F9A58, 0],
+[0x9F9A5C, 1],
+[0x9F9A5E, 1],
+[0x9F9A26, 0],
+[0x9F9A46, 1],
+[0xA5DD32, 0],
+[0xA5DD34, 0],
+[0xA5DD38, 0],
+[0xA5DD3A, 0],
+[0xA5DD3C, 0],
+[0xA5DD3E, 0],
+[0xA5DD40, 0],
+[0xA5DD42, 0],
+[0xA5DD44, 0],
+[0xA5DD46, 0],
+[0xA5DD48, 0],
+[0xA5DD4A, 0],
+[0xA5DD4C, 0],
+[0xA5DD4E, 0],
+[0xA5DD52, 1],
+[0xA5DD54, 1],
+[0xA5DD58, 1],
+[0xA5DD5A, 1],
+[0xA5DD5C, 1],
+[0xA5DD5E, 1],
+[0xA5DD60, 1],
+[0xA5DD62, 1],
+[0xA5DD64, 1],
+[0xA5DD66, 1],
+[0xA5DD68, 1],
+[0xA5DD6A, 1],
+[0xA5DD6C, 1],
+[0xA5DD6E, 1],
+[0x3D0E08, 1],
+[0x3D0E0A, 1],
+[0x3D0E0C, 1],
+[0x3D0E16, 0],
+[0x3D0E18, 0],
+[0x3D0E1A, 0],
+[0x3CC9B8, 0],
+[0x3CC9BA, 0],
+[0x3CC9BC, 0],
+[0x3CC9BE, 0],
+[0x3CC9C0, 0],
+[0x3CC9C2, 0],
+[0x3CC9C4, 0],
+[0x3CC9C6, 0],
+[0x3CC9C8, 0],
+[0x3CC9CA, 0],
+[0x3CC9CC, 0],
+[0x3CC9CE, 0],
+[0x3CC9D0, 0],
+[0x3CC9D2, 0],
+[0x3CDE6E, 1],
+[0x3CDE70, 1],
+[0x3CDE72, 1],
+[0x3CDE74, 1],
+[0x3CDE76, 2],
+[0x3CDE78, 2],
+[0x3CDE7A, 2],
+[0x3CDE7C, 1],
+[0x3CDE7E, 1],
+[0x3CDE80, 1],
+[0x3CDE82, 1],
+[0x3CDE84, 1],
+[0x3CDE86, 1],
+[0x3CDE88, 1],
+[0x3CDE8A, 1],
+[0x51AD1E, 0],
+[0x51AD20, 0],
+[0x51AD22, 0],
+[0x51AD24, 0],
+[0x51AD26, 0],
+[0x51AD28, 0],
+[0x51AD2A, 0],
+[0x51AD2C, 0],
+[0x51AD2E, 0],
+[0x51AD30, 0],
+[0x51AD32, 0],
+[0x51AD34, 0],
+[0x51AD36, 0],
+[0x51AD38, 0],
+[0x51AD3A, 0],
+[0x51AD3E, 1],
+[0x51AD40, 1],
+[0x51AD42, 1],
+[0x51AD44, 1],
+[0x51AD46, 1],
+[0x51AD48, 1],
+[0x51AD4A, 1],
+[0x51AD4C, 1],
+[0x51AD4E, 1],
+[0x51AD50, 1],
+[0x51AD52, 1],
+[0x51AD54, 1],
+[0x51AD56, 1],
+[0x51AD58, 1],
+[0x51AD5A, 1],
+[0x51A49E, 0],
+[0x51A4A0, 0],
+[0x51A4A2, 0],
+[0x51A4A4, 0],
+[0x51A4A6, 0],
+[0x51A4A8, 0],
+[0x51A4AA, 0],
+[0x51A4AC, 0],
+[0x51A4AE, 0],
+[0x51A4B0, 0],
+[0x51A4B2, 0],
+[0x51A4B4, 0],
+[0x51A4B6, 0],
+[0x51A4B8, 0],
+[0x51A4BA, 0],
+[0x51A4BE, 1],
+[0x51A4C0, 1],
+[0x51A4C2, 1],
+[0x51A4C4, 1],
+[0x51A4C6, 1],
+[0x51A4C8, 1],
+[0x51A4CA, 1],
+[0x51A4CC, 1],
+[0x51A4CE, 1],
+[0x51A4D0, 1],
+[0x51A4D2, 1],
+[0x51A4D4, 1],
+[0x51A4D6, 1],
+[0x51A4D8, 1],
+[0x51A4DA, 1],
+[0x51A97E, 0],
+[0x51A980, 0],
+[0x51A982, 0],
+[0x51A984, 0],
+[0x51A986, 0],
+[0x51A988, 0],
+[0x51A98A, 0],
+[0x51A98C, 0],
+[0x51A98E, 0],
+[0x51A990, 0],
+[0x51A992, 0],
+[0x51A994, 0],
+[0x51A996, 0],
+[0x51A998, 0],
+[0x51A99A, 0],
+[0x51A95E, 1],
+[0x51A960, 1],
+[0x51A962, 1],
+[0x51A964, 1],
+[0x51A966, 1],
+[0x51A968, 1],
+[0x51A96A, 1],
+[0x51A96C, 1],
+[0x51A96E, 1],
+[0x51A970, 1],
+[0x51A972, 1],
+[0x51A974, 1],
+[0x51A976, 1],
+[0x51A978, 1],
+[0x51A97A, 1],
+[0x51A9FE, 0],
+[0x51AA00, 0],
+[0x51AA02, 0],
+[0x51AA0A, 0],
+[0x51AA0E, 1],
+[0x51AA10, 1],
+[0x51AA12, 1],
+[0x3CC976, 0],
+[0x3CC978, 0],
+[0x3CC97A, 0],
+[0x3CC97C, 0],
+[0x3CC97E, 0],
+[0x3CC980, 0],
+[0x3CC982, 0],
+[0x3CC984, 0],
+[0x3CC986, 0],
+[0x3CC988, 0],
+[0x3CC98A, 0],
+[0x3CC98C, 0],
+[0x3CC98E, 0],
+[0x3CC990, 0],
+[0x3CC992, 0],
+[0x3D4B44, 0],
+[0x3D4B46, 0],
+[0x3D4B48, 0],
+[0x3D4B4A, 0],
+[0x3D4B4C, 0],
+[0x3D4B4E, 0],
+[0x3D4B50, 0],
+[0x3D4B52, 0],
+[0x3D4B54, 0],
+[0x3D4B56, 0],
+[0x3D4B58, 0],
+[0x3D4B5A, 0],
+[0x3D4B5C, 0],
+[0x3D4B5E, 0],
+[0x3D4B42, 0],
+[0x3CCA38, 1],
+[0x3CCA3A, 1],
+[0x3CCA3C, 1],
+[0x3CCA3E, 1],
+[0x3CCA40, 1],
+[0x3CCA42, 1],
+[0x3CCA44, 1],
+[0x3CCA46, 1],
+[0x3CCA48, 1],
+[0x3CCA4A, 1],
+[0x3CCA4C, 1],
+[0x3CCA4E, 1],
+[0x3CCA50, 1],
+[0x3CCA52, 1],
+[0x3CCA36, 1],
+[0x3CFB30, 0],
+[0x3CFB32, 0],
+[0x3CFB34, 0],
+[0x3CFB36, 0],
+[0x3CFB38, 0],
+[0x3CFB3A, 0],
+[0x3CFB3C, 0],
+[0x3CFB3E, 0],
+[0x3CFB40, 0],
+[0x3CFB42, 0],
+[0x3CFB44, 0],
+[0x3CFB46, 0],
+[0x3CFB48, 0],
+[0x3CFB4A, 0],
+[0x3CFB70, 1],
+[0x3CFB72, 1],
+[0x3CFB74, 1],
+[0x3CFB76, 1],
+[0x3CFB78, 1],
+[0x3CFB7A, 1],
+[0x3CFB7C, 1],
+[0x3CFB7E, 1],
+[0x3CFB80, 1],
+[0x3CFB82, 1],
+[0x3CFB84, 1],
+[0x3CFB86, 1],
+[0x3CFB88, 1],
+[0x3CFB8A, 1],
+[0x3D5044, 0],
+[0x3D5046, 0],
+[0x3D5048, 0],
+[0x3D504A, 0],
+[0x3D504C, 0],
+[0x3D504E, 0],
+[0x3D5050, 0],
+[0x3D5052, 0],
+[0x3D5054, 0],
+[0x3D5056, 0],
+[0x3D505C, 0],
+[0x3D505E, 0],
+[0x3D5084, 1],
+[0x3D5086, 1],
+[0x3D5088, 1],
+[0x3D508A, 1],
+[0x3D508C, 1],
+[0x3D508E, 1],
+[0x3D5090, 1],
+[0x3D5092, 1],
+[0x3D5094, 1],
+[0x3D5096, 1],
+[0x3D509C, 1],
+[0x3D509E, 1],
+[0xA5DDA2, 0],
+[0xA5DDA4, 0],
+[0xA5DDA6, 0],
+[0xA5DDA8, 0],
+[0xA5DDAE, 0],
+[0xA5DDC2, 1],
+[0xA5DDC4, 1],
+[0xA5DDC6, 1],
+[0xA5DDC8, 1],
+[0xA5DDCE, 1],
+[0x3D3E0C, 1],
+[0x3D3E0E, 1],
+[0x3D3E10, 0],
+[0x3D3E12, 0],
+[0x3CF1C0, 0],
+[0x3CF1C2, 0],
+[0x3CF200, 1],
+[0x3CF202, 1],
+[0x3D3604, 0],
+[0x3D3606, 1],
+[0x3D360A, 1],
+[0x3D360C, 1],
+[0x3D360E, 1],
+[0x3D3610, 0],
+[0x3D3612, 0],
+[0x3D3614, 0],
+[0x3D1A42, 0],
+[0x3D1A44, 0],
+[0x3D1A46, 0],
+[0x3D1A48, 0],
+[0x3D1A4A, 0],
+[0x3D1A82, 1],
+[0x3D1A84, 1],
+[0x3D1A86, 1],
+[0x3D1A88, 1],
+[0x3D1A8A, 1],
+[0x3CE280, 0],
+[0x3CE282, 0],
+[0x3CE2C0, 1],
+[0x3CE2C2, 1],
+[0x4FA29C, 0],
+[0x4FA29E, 0],
+[0x4FA2DC, 1],
+[0x4FA2DE, 1],
+[0x8D3C5E, 0],
+[0x8D3C60, 0],
+[0x8D3C62, 0],
+[0x8D3C64, 0],
+[0x8D3B9C, 0],
+[0x8D3BA0, 0],
+[0x8D3B9E, 0],
+[0x3D4C02, 0],
+[0x3D4C0C, 0],
+[0x3D4C0E, 0],
+[0x3D4C10, 0],
+[0x3D4C12, 0],
+[0x3D4C14, 0],
+[0x3D2D96, 0],
+[0x3D2D98, 0],
+[0x3D2D9A, 0],
+[0x3D2D9C, 0],
+[0x8CC272, 1],
+[0x8CC274, 1],
+[0x8CC276, 1],
+[0x8CC278, 1],
+[0x8CC27A, 1],
+[0x8CC27C, 1],
+[0x3D4784, 0],
+[0x3D4786, 1],
+[0x3D4788, 1],
+[0x3D478A, 1],
+[0x3D478C, 0],
+[0x3D478E, 0],
+[0x3D4790, 0],
+[0x3D4792, 0],
+[0x3D4794, 0],
+[0x8CF4B2, 1],
+[0x8CF4B4, 1],
+[0x8CF4B6, 1],
+[0x8CF4B8, 1],
+[0x8CF4BA, 1],
+[0x8CF4BC, 1],
+[0x8CF7B2, 1],
+[0x8CF7B4, 1],
+[0x8CF7B6, 1],
+[0x8CF7B8, 1],
+[0x8CF7BA, 1],
+[0x8CF7BC, 1],
+[0x8D2D52, 1],
+[0x8D2D54, 1],
+[0x8D2D56, 1],
+[0x8D2D58, 1],
+[0x8D2D5A, 1],
+[0x8D2D5C, 1],
+[0x8D0096, 1],
+[0x8D0A16, 1],
+[0x8D006E, 1],
+[0x8D0070, 1],
+[0x8D09AE, 1],
+[0x8D09B0, 1],
+[0x3CE8F6, 1],
+[0x3CE8F8, 1],
+[0x3CE8FA, 1],
+[0x8D0952, 1],
+[0x8D0954, 1],
+[0x8D0956, 1],
+[0x8D0958, 1],
+[0x8D095A, 1],
+[0x8D095C, 1],
+[0x3CE976, 1],
+[0x3CE978, 1],
+[0x3CE97A, 1],
+[0x4F8912, 1],
+[0x4F8914, 1],
+[0x4F8916, 1],
+[0x8D0456, 0],
+[0x8D09F6, 0],
+[0x8D042E, 0],
+[0x8D0430, 0],
+[0x8D09CE, 0],
+[0x8D09D0, 0],
+[0x3CE8B6, 0],
+[0x3CE8B8, 0],
+[0x3CE8BA, 0],
+[0x8D0890, 0],
+[0x8D0892, 0],
+[0x8D0894, 0],
+[0x8D0896, 0],
+[0x3CE932, 0],
+[0x3CE936, 0],
+[0x3CE938, 0],
+[0x3CE93A, 0],
+[0x3CE940, 0],
+[0x4F88CE, 0],
+[0x4F88D2, 0],
+[0x4F88D4, 0],
+[0x4F88D6, 0],
+[0x4F88DC, 0],
+[0x8D095E, 0],
+[0x8D0960, 0],
+[0x8D0962, 0],
+[0x8D0964, 0],
+[0x3D2104, 0],
+[0x3D210E, 0],
+[0x3D2110, 0],
+[0x3D2118, 0],
+[0x3D211A, 0],
+[0x8D0052, 1],
+[0x8D0054, 1],
+[0x8D0056, 1],
+[0x8D0058, 1],
+[0x8D005A, 1],
+[0x8D005C, 1],
+[0x8D005E, 0],
+[0x8D0060, 0],
+[0x8D0062, 0],
+[0x8D0064, 0],
+[0x3C9E36, 1],
+[0x3C9E38, 1],
+[0x3C9E3A, 1],
+[0x3C9E3C, 0],
+[0x3C9E3E, 0],
+[0x3C9E40, 0],
+[0x4F4D50, 0],
+[0x4F4D52, 0],
+[0x4F4D54, 0],
+[0x4F4D56, 0],
+[0x4F4D58, 0],
+[0x4F4D5A, 0],
+[0x4F4D5C, 0],
+[0x4F4D5E, 0],
+[0x4F4D60, 0],
+[0x4F4D62, 0],
+[0x4F4D64, 0],
+[0x4F4D66, 0],
+[0x4F4D68, 0],
+[0x4F4D6A, 0],
+[0x3C9314, 0],
+[0x3C9316, 0],
+[0x3C9318, 0],
+[0x3C931A, 0],
+[0x3C931C, 0],
+[0x3C931E, 0],
+[0x3C9320, 0],
+[0x3C9322, 0],
+[0x3C9324, 0],
+[0x3C9326, 0],
+[0x3C9328, 0],
+[0x3C932A, 0],
+[0x3C932C, 0],
+[0x3C932E, 0],
+[0x9F9CEA, 0],
+[0x9F9CEC, 0],
+[0x9F9CEE, 0],
+[0x9F9CF0, 0],
+[0x9F9CF2, 0],
+[0x9F9CF4, 0],
+[0x9F9CF6, 0],
+[0x9F9CF8, 0],
+[0x9F9CFA, 0],
+[0x9F9CFC, 0],
+[0x9F9CFE, 0],
+[0x9F9D00, 0],
+[0x9F9D02, 0],
+[0x9F9D04, 0],
+[0x4F4E10, 1],
+[0x4F4E12, 1],
+[0x4F4E14, 1],
+[0x4F4E16, 1],
+[0x4F4E18, 1],
+[0x4F4E1A, 1],
+[0x4F4E1C, 1],
+[0x4F4E1E, 1],
+[0x4F4E20, 1],
+[0x4F4E22, 1],
+[0x4F4E24, 1],
+[0x4F4E26, 1],
+[0x4F4E28, 1],
+[0x4F4E2A, 1],
+[0x3C9444, 1],
+[0x3C9446, 1],
+[0x3C9448, 1],
+[0x3C944A, 1],
+[0x3C944C, 1],
+[0x3C944E, 1],
+[0x3C9450, 1],
+[0x3C9452, 1],
+[0x3C9454, 1],
+[0x3C9456, 1],
+[0x3C9458, 1],
+[0x3C945A, 1],
+[0x3C945C, 1],
+[0x3C945E, 1],
+[0x9F9D68, 1],
+[0x9F9D6A, 1],
+[0x9F9D6C, 1],
+[0x9F9D6E, 1],
+[0x9F9D70, 1],
+[0x9F9D72, 1],
+[0x9F9D74, 1],
+[0x9F9D76, 1],
+[0x9F9D78, 1],
+[0x9F9D7A, 1],
+[0x9F9D7C, 1],
+[0x9F9D7E, 1],
+[0x9F9D80, 1],
+[0x9F9D82, 1],
+[0x518E12, 0],
+[0x518E10, 0],
+[0x518E1A, 0],
+[0x518E18, 0],
+[0xA6202E, 0],
+[0xA62030, 0],
+[0xA62032, 0],
+[0xA62034, 0],
+[0xA6203A, 0],
+[0xA6204E, 1],
+[0xA62050, 1],
+[0xA62052, 1],
+[0xA62054, 1],
+[0xA6205A, 1],
+[0x3D4804, 0],
+[0x3D4806, 0],
+[0x3D4808, 0],
+[0x3D480A, 0],
+[0x3D4810, 0],
+[0x3D4812, 0],
+[0x3D480C, 1],
+[0x3D480E, 1],
+[0x8D049E, 0],
+[0x8D04A0, 0],
+[0x8D04A2, 0],
+[0x8D0592, 1],
+[0x8D0594, 1],
+[0x8D0596, 1],
+[0x8D0598, 1],
+[0x8D059A, 1],
+[0x8D059C, 1],
+[0x4F5010, 0],
+[0x4F5012, 0],
+[0x4F5014, 0],
+[0x4F5016, 0],
+[0x4F4ECE, 0],
+[0x4F4ED0, 0],
+[0x4F4ED2, 0],
+[0x4F4ED4, 0],
+[0x4F4ED6, 0],
+[0x4F4ED8, 0],
+[0x4F4EDA, 1],
+[0x4F4EDC, 1],
+[0x4F4EDE, 1],
+[0x4F4EE0, 1],
+[0x4F4EE2, 1],
+[0x3CC9F8, 0],
+[0x3CC9FA, 0],
+[0x3CC9FC, 0],
+[0x3CC9FE, 0],
+[0x3CCA00, 0],
+[0x3CCA02, 0],
+[0x3CCA0A, 0],
+[0x3CCA0C, 0],
+[0x3CCA0E, 0],
+[0x3CCA10, 0],
+[0x3CCA12, 0],
+[0x8CBE1E, 0],
+[0x8CBE20, 0],
+[0x8CBE22, 0],
+[0x8CBE3E, 1],
+[0x8CBE40, 1],
+[0x8CBE42, 1],
+[0x8CBEDE, 0],
+[0x8CBEE0, 0],
+[0x8CBED4, 0],
+[0x8CBEE2, 0],
+[0x8CBECA, 0],
+[0x3B8F38, 1],
+[0x3B8F3A, 1],
+[0x3B8F40, 0],
+[0x3B8F42, 0],
+[0x3D1090, 0],
+[0x3D1092, 0],
+[0x3D1094, 0],
+[0x3D1096, 0],
+[0x3D1098, 0],
+[0x3D109A, 0],
+[0x3D109C, 0],
+[0x3D109E, 0],
+[0x3D1082, 0],
+[0x3D1084, 0],
+[0x3D1086, 0],
+[0x3D10C2, 1],
+[0x3D10C4, 1],
+[0x3D10C6, 1],
+[0x3D10D0, 1],
+[0x3D10D2, 1],
+[0x3D10D4, 1],
+[0x3D10D6, 1],
+[0x3D10D8, 1],
+[0x3D10DA, 1],
+[0x3D10DC, 1],
+[0x3D10DE, 1],
+[0x3D14C2, 0],
+[0x3D14C4, 0],
+[0x3D14C6, 0],
+[0x3D14C8, 0],
+[0x3D14CA, 0],
+[0x3D14CC, 0],
+[0x3D14CE, 0],
+[0x3D14D0, 0],
+[0x3D14D2, 0],
+[0x3D14D4, 0],
+[0x3D14D6, 0],
+[0x3D1502, 1],
+[0x3D1504, 1],
+[0x3D1506, 1],
+[0x3D1508, 1],
+[0x3D150A, 1],
+[0x3D150C, 1],
+[0x3D150E, 1],
+[0x3D1510, 1],
+[0x3D1512, 1],
+[0x3D1514, 1],
+[0x3D1516, 1],
+[0x3CE0F0, 1],
+[0x3CE0F2, 1],
+[0x3CE0F4, 1],
+[0x3CE0D0, 0],
+[0x3CE0D2, 0],
+[0x3CE0D4, 0],
+[0x8D3DAC, 0],
+[0x8D3DBC, 0],
+[0x8D3DB4, 0],
+[0x8D3D86, 0],
+[0x8D3DBA, 0],
+[0x8D3DB8, 0]]
+
+whiteHat = [
+[0x3CC884, 0xFF, 0x7F, 0],
+[0x3CC886, 0x7B, 0x6F, 0],
+[0x3CC8C4, 0xFF, 0x7F, 1],
+[0x3CC8C6, 0x7B, 0x6F, 1],
+[0x4F4CDC, 0xFF, 0x7F, 0],
+[0x4F4CDE, 0x7B, 0x6F, 0],
+[0x4F4D1C, 0xFF, 0x7F, 1],
+[0x4F4D1E, 0x7B, 0x6F, 1],
+[0x4F51D8, 0xFF, 0x7F, 1],
+[0x4F51DC, 0xFF, 0x7F, 0],
+[0x4F51E8, 0x7B, 0x6F, 0],
+[0x4F51EA, 0x7B, 0x6F, 1],
+[0x4F51DA, 0x9C, 0x73, 1],
+[0x4F51DE, 0x9C, 0x73, 0],
+[0x4FB686, 0xFF, 0x7F, 0],
+[0x4FB6A6, 0xFF, 0x7F, 1],
+[0x4FB684, 0x73, 0x4E, 0],
+[0x4FB6A4, 0x73, 0x4E, 1],
+[0x4FB688, 0x7B, 0x6F, 0],
+[0x4FB6A8, 0x7B, 0x6F, 1],
+[0x4FB786, 0x73, 0x4E, 0],
+[0x4FB788, 0xFF, 0x7F, 0],
+[0x4FB78A, 0xFF, 0x7F, 0],
+[0x4FB7A6, 0x73, 0x4E, 1],
+[0x4FB7A8, 0xFF, 0x7F, 1],
+[0x4FB7AA, 0xFF, 0x7F, 1],
+[0x51901C, 0xFF, 0x7F, 0],
+[0x519018, 0x73, 0x4E, 0],
+[0x51901A, 0x7B, 0x6F, 0],
+[0x51903C, 0xFF, 0x7F, 1],
+[0x519038, 0x73, 0x4E, 1],
+[0x51903A, 0x7B, 0x6F, 1],
+[0x5193EA, 0x7B, 0x6F, 0],
+[0x5193E8, 0xFF, 0x7F, 0],
+[0x51940A, 0x7B, 0x6F, 1],
+[0x519408, 0xFF, 0x7F, 1],
+[0x3A72AE, 0xF7, 0x5E, 1],
+[0x3A7244, 0xF7, 0x5E, 0],
+[0x3A724C, 0x39, 0x67, 1],
+[0x3A7290, 0x18, 0x63, 0],
+[0x3A72B2, 0x18, 0x63, 1],
+[0x3A7270, 0xB5, 0x56, 1],
+[0x3A7288, 0xB5, 0x56, 0],
+[0x3A7296, 0x5A, 0x6B, 0],
+[0x3A7274, 0x5A, 0x6B, 1],
+[0x3A7294, 0x39, 0x67, 0],
+[0x3A724A, 0x7B, 0x6F, 0],
+[0x3A7278, 0x7B, 0x6F, 1],
+[0x3A724E, 0x9C, 0x73, 0],
+[0x3A727A, 0x9C, 0x73, 1],
+[0x3A7252, 0xBD, 0x77, 0],
+[0x3A727C, 0xBD, 0x77, 1],
+[0x3A72BC, 0xBD, 0x77, 1],
+[0x3A726C, 0x94, 0x52, 1],
+[0x3A7286, 0x94, 0x52, 0],
+[0x3A728C, 0xD6, 0x5A, 0],
+[0x3A7272, 0xD6, 0x5A, 1],
+[0x3A7254, 0xFF, 0x7F, 1],
+[0x3A7256, 0xFF, 0x7F, 0],
+[0x3A7264, 0xFF, 0x7F, 1],
+[0x9F9A58, 0x7B, 0x6F, 0],
+[0x9F9A5E, 0x7B, 0x6F, 1],
+[0x9F9A2A, 0x9C, 0x73, 0],
+[0x9F9A30, 0x9C, 0x73, 1],
+[0x9F9A36, 0x9C, 0x73, 0],
+[0x9F9A3C, 0x9C, 0x73, 1],
+[0x9F9A4A, 0x9C, 0x73, 0],
+[0x9F9A50, 0x9C, 0x73, 1],
+[0x9F9A56, 0xF7, 0x5E, 0],
+[0x9F9A5C, 0xF7, 0x5E, 1],
+[0x9F9A28, 0x73, 0x4E, 0],
+[0x9F9A2E, 0x73, 0x4E, 1],
+[0x9F9A48, 0x73, 0x4E, 0],
+[0x9F9A4E, 0x73, 0x4E, 1],
+[0x9F9A34, 0x73, 0x4E, 0],
+[0x9F9A3A, 0x9C, 0x73, 1],
+[0x9F9A2C, 0xFF, 0x7F, 0],
+[0x9F9A32, 0xFF, 0x7F, 1],
+[0x9F9A38, 0xFF, 0x7F, 0],
+[0x9F9A3E, 0xFF, 0x7F, 1],
+[0x9F9A4C, 0xFF, 0x7F, 0],
+[0x9F9A52, 0xFF, 0x7F, 1],
+[0xA5DD46, 0xF7, 0x5E, 0],
+[0xA5DD44, 0x94, 0x52, 0],
+[0xA5DD4A, 0x9C, 0x73, 0],
+[0xA5DD3E, 0x31, 0x46, 0],
+[0xA5DD40, 0xDE, 0x7B, 0],
+[0xA5DD42, 0xFF, 0x7F, 0],
+[0xA5DD48, 0x5A, 0x6B, 0],
+[0xA5DD4E, 0xFF, 0x7F, 0],
+[0xA5DD66, 0xF7, 0x5E, 1],
+[0xA5DD64, 0x94, 0x52, 1],
+[0xA5DD6A, 0x9C, 0x73, 1],
+[0xA5DD5E, 0x31, 0x46, 1],
+[0xA5DD60, 0xDE, 0x7B, 1],
+[0xA5DD62, 0xFF, 0x7F, 1],
+[0xA5DD68, 0x5A, 0x6B, 1],
+[0x3D0E0C, 0xFF, 0x7F, 1],
+[0x3D0E18, 0xFF, 0x7F, 0],
+[0x3D0E0A, 0x7B, 0x6F, 1],
+[0x3D0E1A, 0x7B, 0x6F, 0],
+[0x3D0E08, 0xFF, 0x7F, 1],
+[0x3D0E16, 0xFF, 0x7F, 0],
+[0x3CC9C4, 0xFF, 0x7F, 0],
+[0x3CC9C6, 0x7B, 0x6F, 0],
+[0x3CDE7C, 0xFF, 0x7F, 1],
+[0x3CDE7E, 0x7B, 0x6F, 1],
+[0x51AD2C, 0xFF, 0x7F, 0],
+[0x51AD2E, 0x7B, 0x6F, 0],
+[0x51AD4C, 0xFF, 0x7F, 1],
+[0x51AD4E, 0x7B, 0x6F, 1],
+[0x51A4AE, 0x7B, 0x6F, 0],
+[0x51A4AC, 0xFF, 0x7F, 0],
+[0x51A4CC, 0xFF, 0x7F, 1],
+[0x51A4CE, 0x7B, 0x6F, 1],
+[0x51A98C, 0xFF, 0x7F, 0],
+[0x51A98E, 0x7B, 0x6F, 0],
+[0x51A96C, 0xFF, 0x7F, 1],
+[0x51A96E, 0x7B, 0x6F, 1],
+[0x51AA00, 0x9C, 0x73, 0],
+[0x51AA10, 0x9C, 0x73, 1],
+[0x51AA02, 0xD6, 0x5A, 0],
+[0x51AA12, 0xD6, 0x5A, 1],
+[0x51A9FE, 0xFF, 0x7F, 0],
+[0x51AA0E, 0xFF, 0x7F, 1],
+[0x51AA0A, 0xFF, 0x7F, 0],
+[0x3CC984, 0xFF, 0x7F, 0],
+[0x3CC986, 0x7B, 0x6F, 0],
+[0x3D4B52, 0xFF, 0x7F, 0],
+[0x3D4B5C, 0xD6, 0x5A, 0],
+[0x3D4B54, 0xFF, 0x7F, 0],
+[0x3D4B56, 0x7B, 0x6F, 0],
+[0x3D4B50, 0xFF, 0x7F, 0],
+[0x3CCA44, 0xFF, 0x7F, 1],
+[0x3CCA46, 0x7B, 0x6F, 1],
+[0x3CFB3C, 0xFF, 0x7F, 0],
+[0x3CFB3E, 0x7B, 0x6F, 0],
+[0x3CFB7C, 0xFF, 0x7F, 1],
+[0x3CFB7E, 0x7B, 0x6F, 1],
+[0x3D504C, 0xBD, 0x77, 0],
+[0x3D504A, 0xFF, 0x7F, 0],
+[0x3D504E, 0x7B, 0x6F, 0],
+[0x3D508C, 0xBD, 0x77, 1],
+[0x3D508A, 0xFF, 0x7F, 1],
+[0x3D508E, 0x7B, 0x6F, 1],
+[0xA5DDA2, 0xF7, 0x5E, 0],
+[0xA5DDC2, 0xF7, 0x5E, 1],
+[0xA5DDA6, 0x9C, 0x73, 0],
+[0xA5DDC6, 0x9C, 0x73, 1],
+[0xA5DDA4, 0x5A, 0x6B, 0],
+[0xA5DDC4, 0x5A, 0x6B, 1],
+[0xA5DDA8, 0xFF, 0x7F, 0],
+[0xA5DDC8, 0xFF, 0x7F, 1],
+[0x3D3E0C, 0xFF, 0x7F, 1],
+[0x3D3E10, 0xFF, 0x7F, 0],
+[0x3D3E0E, 0x7B, 0x6F, 1],
+[0x3D3E12, 0x7B, 0x6F, 0],
+[0x3CF1C0, 0xFF, 0x7F, 0],
+[0x3CF200, 0xFF, 0x7F, 1],
+[0x3CF1C2, 0x7B, 0x6F, 0],
+[0x3CF202, 0x7B, 0x6F, 1],
+[0x3D360E, 0x39, 0x67, 1],
+[0x3D3614, 0x39, 0x67, 0],
+[0x3D360C, 0x9C, 0x73, 1],
+[0x3D3612, 0x9C, 0x73, 0],
+[0x3D3604, 0x5A, 0x6B, 0],
+[0x3D3606, 0x5A, 0x6B, 1],
+[0x3D360A, 0xFF, 0x7F, 1],
+[0x3D3610, 0xFF, 0x7F, 0],
+[0x3D1A48, 0xD6, 0x5A, 0],
+[0x3D1A46, 0xFF, 0x7F, 0],
+[0x3D1A44, 0xFF, 0x7F, 0],
+[0x3D1A4A, 0x5A, 0x6B, 0],
+[0x3D1A88, 0xD6, 0x5A, 1],
+[0x3D1A8A, 0xFF, 0xF4, 1],
+[0x3D1A86, 0xFF, 0x7F, 1],
+[0x3D1A84, 0xFF, 0x7F, 1],
+[0x3CE282, 0x7B, 0x6F, 0],
+[0x3CE2C2, 0x7B, 0x6F, 1],
+[0x3CE280, 0xFF, 0x7F, 0],
+[0x3CE2C0, 0xFF, 0x7F, 1],
+[0x4FA29E, 0x7B, 0x6F, 0],
+[0x4FA2DE, 0x7B, 0x6F, 1],
+[0x4FA29C, 0xFF, 0x7F, 0],
+[0x4FA2DC, 0xFF, 0x7F, 1],
+[0x3D4786, 0xFF, 0x7F, 1],
+[0x3D478C, 0xFF, 0x7F, 0],
+[0x3D478E, 0x18, 0x63, 0],
+[0x3D4788, 0x18, 0x63, 1],
+[0x3D4790, 0x18, 0x63, 0],
+[0x3D478A, 0xBD, 0x77, 1],
+[0x3D4794, 0x5A, 0x6B, 0],
+[0x3D4792, 0xBD, 0x77, 0],
+[0x3D4784, 0xFE, 0x33, 0],
+[0x3C9E3A, 0xFF, 0x7F, 1],
+[0x3C9E40, 0xFF, 0x7F, 0],
+[0x3C9E38, 0xFF, 0x7F, 1],
+[0x3C9E3E, 0xFF, 0x7F, 0],
+[0x3C9E36, 0xFC, 0x7F, 1],
+[0x3C9E3C, 0xFC, 0x7F, 0],
+[0x4F4D5C, 0xFF, 0x7F, 0],
+[0x4F4D5E, 0x7B, 0x6F, 0],
+[0x3C9320, 0xFF, 0x7F, 0],
+[0x3C9322, 0x7B, 0x6F, 0],
+[0x9F9CF6, 0xFF, 0x7F, 0],
+[0x9F9CF8, 0x7B, 0x6F, 0],
+[0x4F4E1C, 0xFF, 0x7F, 1],
+[0x4F4E1E, 0x7B, 0x6F, 1],
+[0x3C9450, 0xFF, 0x7F, 1],
+[0x3C9452, 0x7B, 0x6F, 1],
+[0x9F9D74, 0xFF, 0x7F, 1],
+[0x9F9D76, 0x7B, 0x6F, 1],
+[0xA6202E, 0xF7, 0x5E, 0],
+[0xA62032, 0x9C, 0x73, 0],
+[0xA62030, 0x5A, 0x6B, 0],
+[0xA62034, 0xFF, 0x7F, 0],
+[0xA6204E, 0xF7, 0x5E, 1],
+[0xA62052, 0x9C, 0x73, 1],
+[0xA62050, 0x5A, 0x6B, 1],
+[0xA62054, 0xFF, 0x7F, 1],
+[0x3D4812, 0x39, 0x67, 0],
+[0x3D480E, 0x39, 0x67, 1],
+[0x3D4810, 0xFF, 0x7F, 0],
+[0x3D480C, 0xFF, 0x7F, 1],
+[0x3CC9FE, 0xFF, 0x7F, 0],
+[0x3CCA0A, 0x7B, 0x6F, 0],
+[0x8CBE20, 0xD6, 0x5A, 0],
+[0x8CBE22, 0x7B, 0x6F, 0],
+[0x8CBE1E, 0xFF, 0x7F, 0],
+[0x8CBE40, 0xD6, 0x5A, 1],
+[0x8CBE42, 0x7B, 0x6F, 1],
+[0x8CBE3E, 0xFF, 0x7F, 1],
+[0x8CBEE0, 0xFF, 0x7F, 0],
+[0x8CBEDE, 0xFF, 0x7F, 0],
+[0x8CBEE2, 0x7B, 0x6F, 0],
+[0x3B8F38, 0xD6, 0x5A, 1],
+[0x3B8F3A, 0xFF, 0x7F, 1],
+[0x3B8F40, 0xD6, 0x5A, 0],
+[0x3B8F42, 0xFF, 0x7F, 0],
+[0x3D1094, 0x18, 0x63, 0],
+[0x3D109A, 0x94, 0x52, 0],
+[0x3D1098, 0x73, 0x4E, 0],
+[0x3D1096, 0x7B, 0x6F, 0],
+[0x3D1092, 0xFF, 0x7F, 0],
+[0x3D1090, 0xFF, 0x7F, 0],
+[0x3D10D4, 0x18, 0x63, 1],
+[0x3D10DA, 0x94, 0x52, 1],
+[0x3D10D8, 0x73, 0x4E, 1],
+[0x3D10D6, 0x7B, 0x6F, 1],
+[0x3D10D2, 0xFF, 0x7F, 1],
+[0x3D10D0, 0xFF, 0x7F, 1],
+[0x3D14D0, 0xFF, 0x7F, 0],
+[0x3D14D2, 0x7B, 0x6F, 0],
+[0x3D14CE, 0xFF, 0x7F, 0],
+[0x3D14CC, 0x7B, 0x6F, 0],
+[0x3D1510, 0x39, 0x67, 1],
+[0x3D1512, 0x7B, 0x6F, 1],
+[0x3D150E, 0xFF, 0x7F, 1],
+[0x3D150C, 0x7B, 0x6F, 1],
+[0x3CE0F4, 0xF7, 0x5E, 1],
+[0x3CE0F2, 0x9C, 0x73, 1],
+[0x3CE0F0, 0x7B, 0x6F, 1],
+[0x3CE0D4, 0xF7, 0x5E, 0],
+[0x3CE0D2, 0x9C, 0x73, 0],
+[0x3CE0D0, 0x7B, 0x6F, 0]]
+
+yellowHat = [
+[0x3CC884, 0xFF, 0x07, 0],
+[0x3CC886, 0xF7, 0x02, 0],
+[0x3CC8C4, 0xFF, 0x07, 1],
+[0x3CC8C6, 0xF7, 0x02, 1],
+[0x4F4CDC, 0xFF, 0x07, 0],
+[0x4F4CDE, 0xF7, 0x02, 0],
+[0x4F4D1C, 0xFF, 0x07, 1],
+[0x4F4D1E, 0xF7, 0x02, 1],
+[0x4F51D8, 0xFF, 0x07, 1],
+[0x4F51DC, 0xFF, 0x07, 0],
+[0x4F51E8, 0xDE, 0x27, 0],
+[0x4F51EA, 0xDE, 0x27, 1],
+[0x4F51DA, 0xFF, 0x4F, 1],
+[0x4F51DE, 0xFF, 0x4F, 0],
+[0x4FB686, 0xFF, 0x07, 0],
+[0x4FB6A6, 0xFF, 0x07, 1],
+[0x4FB684, 0xCE, 0x01, 0],
+[0x4FB6A4, 0xCE, 0x01, 1],
+[0x4FB688, 0xFF, 0x4F, 0],
+[0x4FB6A8, 0xFF, 0x4F, 1],
+[0x4FB786, 0x39, 0x03, 0],
+[0x4FB788, 0xFF, 0x07, 0],
+[0x4FB78A, 0xFF, 0x63, 0],
+[0x4FB7A6, 0x39, 0x03, 1],
+[0x4FB7A8, 0xFF, 0x07, 1],
+[0x4FB7AA, 0xFF, 0x63, 1],
+[0x51901C, 0xFF, 0x07, 0],
+[0x519018, 0xF7, 0x01, 0],
+[0x51901A, 0xF7, 0x02, 0],
+[0x51903C, 0xFF, 0x07, 1],
+[0x519038, 0xF7, 0x01, 1],
+[0x51903A, 0xF7, 0x02, 1],
+[0x5193EA, 0xF7, 0x02, 0],
+[0x5193E8, 0xFF, 0x07, 0],
+[0x51940A, 0xF7, 0x02, 1],
+[0x519408, 0xFF, 0x07, 1],
+[0x3A72AE, 0x39, 0x13, 1],
+[0x3A7244, 0x39, 0x13, 0],
+[0x3A724C, 0x5A, 0x17, 1],
+[0x3A7290, 0x5A, 0x17, 0],
+[0x3A72B2, 0x7B, 0x13, 1],
+[0x3A7270, 0x73, 0x0E, 1],
+[0x3A7288, 0x73, 0x0E, 0],
+[0x3A7296, 0x9C, 0x17, 0],
+[0x3A7274, 0x9C, 0x17, 1],
+[0x3A7294, 0x7B, 0x13, 0],
+[0x3A724A, 0xBD, 0x17, 0],
+[0x3A7278, 0xBD, 0x17, 1],
+[0x3A724E, 0xDE, 0x17, 0],
+[0x3A727A, 0xDE, 0x17, 1],
+[0x3A7252, 0xFF, 0x1B, 0],
+[0x3A727C, 0xFF, 0x1B, 1],
+[0x3A72BC, 0xFF, 0x1B, 1],
+[0x3A726C, 0x31, 0x02, 1],
+[0x3A7286, 0x31, 0x02, 0],
+[0x3A728C, 0x18, 0x13, 0],
+[0x3A7272, 0xB5, 0x12, 1],
+[0x3A7254, 0xFF, 0x3B, 1],
+[0x3A7256, 0xFF, 0x3B, 0],
+[0x3A7264, 0xFF, 0x3B, 1],
+[0x9F9A58, 0xD6, 0x3A, 0],
+[0x9F9A5E, 0xD6, 0x3A, 1],
+[0x9F9A2A, 0xFF, 0x07, 0],
+[0x9F9A30, 0xFF, 0x07, 1],
+[0x9F9A36, 0xFF, 0x07, 0],
+[0x9F9A3C, 0xFF, 0x07, 1],
+[0x9F9A4A, 0xFF, 0x07, 0],
+[0x9F9A50, 0xFF, 0x07, 1],
+[0x9F9A56, 0x94, 0x02, 0],
+[0x9F9A5C, 0x94, 0x02, 1],
+[0x9F9A28, 0xCE, 0x01, 0],
+[0x9F9A2E, 0xCE, 0x01, 1],
+[0x9F9A48, 0xCE, 0x01, 0],
+[0x9F9A4E, 0xCE, 0x01, 1],
+[0x9F9A34, 0x70, 0x07, 0],
+[0x9F9A3A, 0x70, 0x07, 1],
+[0x9F9A2C, 0xFF, 0x4F, 0],
+[0x9F9A32, 0xFF, 0x4F, 1],
+[0x9F9A38, 0xFF, 0x4F, 0],
+[0x9F9A3E, 0xFF, 0x4F, 1],
+[0x9F9A4C, 0xFF, 0x4F, 0],
+[0x9F9A52, 0xFF, 0x4F, 1],
+[0xA5DD46, 0x94, 0x12, 0],
+[0xA5DD44, 0xEF, 0x0D, 0],
+[0xA5DD4A, 0xBD, 0x1B, 0],
+[0xA5DD3E, 0xF7, 0x32, 0],
+[0xA5DD40, 0xBD, 0x3F, 0],
+[0xA5DD42, 0xFF, 0x57, 0],
+[0xA5DD48, 0x39, 0x1B, 0],
+[0xA5DD4E, 0xFF, 0x7F, 0],
+[0xA5DD66, 0x94, 0x12, 1],
+[0xA5DD64, 0xEF, 0x0D, 1],
+[0xA5DD6A, 0xBD, 0x1B, 1],
+[0xA5DD5E, 0xF7, 0x32, 1],
+[0xA5DD60, 0xFF, 0x43, 1],
+[0xA5DD62, 0xFF, 0x57, 1],
+[0xA5DD68, 0x39, 0x1B, 1],
+[0x3D0E0C, 0xFF, 0x07, 1],
+[0x3D0E18, 0xFF, 0x07, 0],
+[0x3D0E0A, 0xF7, 0x02, 1],
+[0x3D0E1A, 0xF7, 0x02, 0],
+[0x3D0E08, 0xFF, 0x4F, 1],
+[0x3D0E16, 0xFF, 0x4F, 0],
+[0x3CC9C4, 0xFF, 0x07, 0],
+[0x3CC9C6, 0xF7, 0x02, 0],
+[0x3CDE7C, 0xFF, 0x07, 1],
+[0x3CDE7E, 0xF7, 0x02, 1],
+[0x51AD2C, 0xFF, 0x07, 0],
+[0x51AD2E, 0xF7, 0x02, 0],
+[0x51AD4C, 0xFF, 0x07, 1],
+[0x51AD4E, 0xF7, 0x02, 1],
+[0x51A4AE, 0xF7, 0x02, 0],
+[0x51A4AC, 0xFF, 0x07, 0],
+[0x51A4CC, 0xFF, 0x07, 1],
+[0x51A4CE, 0xF7, 0x02, 1],
+[0x51A98C, 0xFF, 0x07, 0],
+[0x51A98E, 0xF7, 0x02, 0],
+[0x51A96C, 0xFF, 0x07, 1],
+[0x51A96E, 0xF7, 0x02, 1],
+[0x51AA00, 0x9C, 0x07, 0],
+[0x51AA10, 0x9C, 0x07, 1],
+[0x51AA02, 0xF7, 0x02, 0],
+[0x51AA12, 0xF7, 0x02, 1],
+[0x51A9FE, 0xFF, 0x07, 0],
+[0x51AA0E, 0xFF, 0x07, 1],
+[0x51AA0A, 0xFF, 0x3B, 0],
+[0x3CC984, 0xFF, 0x07, 0],
+[0x3CC986, 0xF7, 0x02, 0],
+[0x3D4B52, 0x7B, 0x07, 0],
+[0x3D4B5C, 0x94, 0x02, 0],
+[0x3D4B54, 0xFF, 0x07, 0],
+[0x3D4B56, 0xF7, 0x02, 0],
+[0x3D4B50, 0xFF, 0x4F, 0],
+[0x3CCA44, 0xFF, 0x07, 1],
+[0x3CCA46, 0xF7, 0x02, 1],
+[0x3CFB3C, 0xFF, 0x07, 0],
+[0x3CFB3E, 0xF7, 0x02, 0],
+[0x3CFB7C, 0xFF, 0x07, 1],
+[0x3CFB7E, 0xF7, 0x02, 1],
+[0x3D504C, 0x7B, 0x07, 0],
+[0x3D504A, 0xFF, 0x07, 0],
+[0x3D504E, 0xF7, 0x02, 0],
+[0x3D508C, 0x7B, 0x07, 1],
+[0x3D508A, 0xFF, 0x07, 1],
+[0x3D508E, 0xF7, 0x02, 1],
+[0xA5DDA2, 0x94, 0x12, 0],
+[0xA5DDC2, 0x94, 0x12, 1],
+[0xA5DDA6, 0xBD, 0x1B, 0],
+[0xA5DDC6, 0xBD, 0x1B, 1],
+[0xA5DDA4, 0x39, 0x1B, 0],
+[0xA5DDC4, 0x39, 0x1B, 1],
+[0xA5DDA8, 0xFF, 0x4F, 0],
+[0xA5DDC8, 0xFF, 0x4F, 1],
+[0x3D3E0C, 0xFF, 0x07, 1],
+[0x3D3E10, 0xFF, 0x07, 0],
+[0x3D3E0E, 0xF7, 0x02, 1],
+[0x3D3E12, 0xF7, 0x02, 0],
+[0x3CF1C0, 0xFF, 0x07, 0],
+[0x3CF200, 0xFF, 0x07, 1],
+[0x3CF1C2, 0xF7, 0x02, 0],
+[0x3CF202, 0xF7, 0x02, 1],
+[0x3D360E, 0x5A, 0x17, 1],
+[0x3D3614, 0x5A, 0x17, 0],
+[0x3D360C, 0xFF, 0x4F, 1],
+[0x3D3612, 0xFF, 0x4F, 0],
+[0x3D3604, 0x39, 0x1B, 0],
+[0x3D3606, 0x39, 0x1B, 1],
+[0x3D360A, 0xFF, 0x4F, 1],
+[0x3D3610, 0xFF, 0x4F, 0],
+[0x3D1A48, 0xB5, 0x12, 0],
+[0x3D1A46, 0xFF, 0x4F, 0],
+[0x3D1A44, 0xFF, 0x4F, 0],
+[0x3D1A4A, 0x39, 0x1B, 0],
+[0x3D1A88, 0xB5, 0x12, 1],
+[0x3D1A8A, 0xBB, 0xB4, 1],
+[0x3D1A86, 0xFF, 0x4F, 1],
+[0x3D1A84, 0xFF, 0x4F, 1],
+[0x3CE282, 0xF7, 0x02, 0],
+[0x3CE2C2, 0xF7, 0x02, 1],
+[0x3CE280, 0xFF, 0x4F, 0],
+[0x3CE2C0, 0xFF, 0x4F, 1],
+[0x4FA29E, 0xF7, 0x02, 0],
+[0x4FA2DE, 0xF7, 0x02, 1],
+[0x4FA29C, 0xFF, 0x4F, 0],
+[0x4FA2DC, 0xFF, 0x4F, 1],
+[0x3D4786, 0xFF, 0x3B, 1],
+[0x3D478C, 0xFF, 0x3B, 0],
+[0x3D478E, 0x5A, 0x17, 0],
+[0x3D4788, 0x5A, 0x17, 1],
+[0x3D4790, 0x5A, 0x17, 0],
+[0x3D478A, 0x7B, 0x07, 1],
+[0x3D4794, 0x39, 0x1B, 0],
+[0x3D4792, 0x7B, 0x07, 0],
+[0x3D4784, 0xFE, 0x33, 0],
+[0x3C9E3A, 0xFF, 0x63, 1],
+[0x3C9E40, 0xFF, 0x63, 0],
+[0x3C9E38, 0xFF, 0x63, 1],
+[0x3C9E3E, 0xFF, 0x63, 0],
+[0x3C9E36, 0xFC, 0x7F, 1],
+[0x3C9E3C, 0xFC, 0x7F, 0],
+[0x4F4D5C, 0xFF, 0x07, 0],
+[0x4F4D5E, 0xF7, 0x02, 0],
+[0x3C9320, 0xFF, 0x07, 0],
+[0x3C9322, 0xF7, 0x02, 0],
+[0x9F9CF6, 0xFF, 0x07, 0],
+[0x9F9CF8, 0xF7, 0x02, 0],
+[0x4F4E1C, 0xFF, 0x07, 1],
+[0x4F4E1E, 0xF7, 0x02, 1],
+[0x3C9450, 0xFF, 0x07, 1],
+[0x3C9452, 0xF7, 0x02, 1],
+[0x9F9D74, 0xFF, 0x07, 1],
+[0x9F9D76, 0xF7, 0x02, 1],
+[0xA6202E, 0x94, 0x12, 0],
+[0xA62032, 0xBD, 0x1B, 0],
+[0xA62030, 0x39, 0x1B, 0],
+[0xA62034, 0xFF, 0x4F, 0],
+[0xA6204E, 0x94, 0x12, 1],
+[0xA62052, 0xBD, 0x1B, 1],
+[0xA62050, 0x39, 0x1B, 1],
+[0xA62054, 0xFF, 0x4F, 1],
+[0x3D4812, 0x5A, 0x17, 0],
+[0x3D480E, 0x5A, 0x17, 1],
+[0x3D4810, 0xFF, 0x4F, 0],
+[0x3D480C, 0xFF, 0x4F, 1],
+[0x3CC9FE, 0xFF, 0x07, 0],
+[0x3CCA0A, 0xF7, 0x02, 0],
+[0x8CBE20, 0xB5, 0x12, 0],
+[0x8CBE22, 0xF7, 0x02, 0],
+[0x8CBE1E, 0xFF, 0x3B, 0],
+[0x8CBE40, 0xB5, 0x12, 1],
+[0x8CBE42, 0xF7, 0x02, 1],
+[0x8CBE3E, 0xFF, 0x3B, 1],
+[0x8CBEE0, 0xFF, 0x07, 0],
+[0x8CBEDE, 0xFF, 0x57, 0],
+[0x8CBEE2, 0xF7, 0x02, 0],
+[0x3B8F38, 0xB5, 0x12, 1],
+[0x3B8F3A, 0xFF, 0x57, 1],
+[0x3B8F40, 0xB5, 0x12, 0],
+[0x3B8F42, 0xFF, 0x57, 0],
+[0x3D1094, 0x5A, 0x17, 0],
+[0x3D109A, 0xEF, 0x0D, 0],
+[0x3D1098, 0xF7, 0x01, 0],
+[0x3D1096, 0xF7, 0x02, 0],
+[0x3D1092, 0xFF, 0x57, 0],
+[0x3D1090, 0xFF, 0x4F, 0],
+[0x3D10D4, 0x5A, 0x17, 1],
+[0x3D10DA, 0xEF, 0x0D, 1],
+[0x3D10D8, 0xF7, 0x01, 1],
+[0x3D10D6, 0xF7, 0x02, 1],
+[0x3D10D2, 0xFF, 0x57, 1],
+[0x3D10D0, 0xFF, 0x4F, 1],
+[0x3D14D0, 0xFF, 0x07, 0],
+[0x3D14D2, 0xF7, 0x02, 0],
+[0x3D14CE, 0xFF, 0x4F, 0],
+[0x3D14CC, 0xDE, 0x27, 0],
+[0x3D1510, 0x5A, 0x17, 1],
+[0x3D1512, 0xF7, 0x02, 1],
+[0x3D150E, 0xFF, 0x4F, 1],
+[0x3D150C, 0xDE, 0x27, 1],
+[0x3CE0F4, 0x94, 0x12, 1],
+[0x3CE0F2, 0x9C, 0x07, 1],
+[0x3CE0F0, 0xDE, 0x27, 1],
+[0x3CE0D4, 0x94, 0x12, 0],
+[0x3CE0D2, 0x9C, 0x07, 0],
+[0x3CE0D0, 0xDE, 0x27, 0]]
+
+azurePants = [
+[0x3CC882, 0x8B, 0x73, 0],
+[0x3CC880, 0xED, 0x7F, 0],
+[0x3CC8C2, 0x8B, 0x73, 1],
+[0x3CC8C0, 0xED, 0x7F, 1],
+[0x4F4CDA, 0x8B, 0x73, 0],
+[0x4F4CD8, 0xED, 0x7F, 0],
+[0x4F4D1A, 0x8B, 0x73, 1],
+[0x4F4D18, 0xED, 0x7F, 1],
+[0x519014, 0x8B, 0x73, 0],
+[0x519016, 0xED, 0x7F, 0],
+[0x519034, 0x8B, 0x73, 1],
+[0x519036, 0xED, 0x7F, 1],
+[0x5193E6, 0x8B, 0x73, 0],
+[0x5193E4, 0xED, 0x7F, 0],
+[0x519406, 0x8B, 0x73, 1],
+[0x519404, 0xED, 0x7F, 1],
+[0x3CC9C2, 0x8B, 0x73, 0],
+[0x3CC9C0, 0xED, 0x7F, 0],
+[0x3CDE84, 0x8B, 0x73, 1],
+[0x3CDE74, 0x8B, 0x73, 1],
+[0x51AD2A, 0x8B, 0x73, 0],
+[0x51AD26, 0xFA, 0x7F, 0],
+[0x51AD28, 0xED, 0x7F, 0],
+[0x51AD4A, 0x8B, 0x73, 1],
+[0x51AD48, 0xED, 0x7F, 1],
+[0x51AD46, 0xFA, 0x7F, 1],
+[0x51A4AA, 0x8B, 0x73, 0],
+[0x51A4A6, 0xFA, 0x7F, 0],
+[0x51A4A8, 0xED, 0x7F, 0],
+[0x51A4CA, 0x8B, 0x73, 1],
+[0x51A4C8, 0xED, 0x7F, 1],
+[0x51A4C6, 0xFA, 0x7F, 1],
+[0x51A98A, 0x8B, 0x73, 0],
+[0x51A986, 0xFA, 0x7F, 0],
+[0x51A988, 0xED, 0x7F, 0],
+[0x51A96A, 0x8B, 0x73, 1],
+[0x51A968, 0xED, 0x7F, 1],
+[0x51A966, 0xFA, 0x7F, 1],
+[0x3CC982, 0x8B, 0x73, 0],
+[0x3CC980, 0xED, 0x7F, 0],
+[0x3D4B4E, 0xD6, 0x7B, 0],
+[0x3D4B4C, 0xFA, 0x7F, 0],
+[0x3CCA42, 0x8B, 0x73, 1],
+[0x3CCA40, 0xED, 0x7F, 1],
+[0x3CFB3A, 0x8B, 0x73, 0],
+[0x3CFB38, 0xED, 0x7F, 0],
+[0x3CFB7A, 0x8B, 0x73, 1],
+[0x3CFB78, 0xED, 0x7F, 1],
+[0x4F4D5A, 0x8B, 0x73, 0],
+[0x4F4D58, 0xED, 0x7F, 0],
+[0x3C931E, 0x8B, 0x73, 0],
+[0x3C931C, 0xED, 0x7F, 0],
+[0x9F9CF4, 0x8B, 0x73, 0],
+[0x9F9CF2, 0xED, 0x7F, 0],
+[0x4F4E1A, 0x8B, 0x73, 1],
+[0x4F4E18, 0xED, 0x7F, 1],
+[0x3C944E, 0x8B, 0x73, 1],
+[0x3C944C, 0xED, 0x7F, 1],
+[0x9F9D72, 0x8B, 0x73, 1],
+[0x9F9D70, 0xED, 0x7F, 1],
+[0x518E10, 0x8B, 0x73, 0],
+[0x518E12, 0xFA, 0x7F, 0],
+[0x3D4806, 0x8B, 0x73, 0],
+[0x3D4804, 0xED, 0x7F, 0],
+[0x3CCA02, 0x8B, 0x73, 0],
+[0x3CCA00, 0xED, 0x7F, 0],
+[0x3D14C6, 0xED, 0x7F, 0],
+[0x3D1506, 0xED, 0x7F, 1]]
+
+blackPants = [
+[0x3CC882, 0x63, 0x0C, 0],
+[0x3CC880, 0x84, 0x10, 0],
+[0x3CC8C2, 0x63, 0x0C, 1],
+[0x3CC8C0, 0x84, 0x10, 1],
+[0x4F4CDA, 0x63, 0x0C, 0],
+[0x4F4CD8, 0x84, 0x10, 0],
+[0x4F4D1A, 0x63, 0x0C, 1],
+[0x4F4D18, 0x84, 0x10, 1],
+[0x519014, 0x63, 0x0C, 0],
+[0x519016, 0x84, 0x10, 0],
+[0x519034, 0x63, 0x0C, 1],
+[0x519036, 0x84, 0x10, 1],
+[0x5193E6, 0x63, 0x0C, 0],
+[0x5193E4, 0x84, 0x10, 0],
+[0x519406, 0x63, 0x0C, 1],
+[0x519404, 0x84, 0x10, 1],
+[0x3CC9C2, 0x63, 0x0C, 0],
+[0x3CC9C0, 0x84, 0x10, 0],
+[0x3CDE84, 0x63, 0x0C, 1],
+[0x3CDE74, 0x63, 0x0C, 1],
+[0x51AD2A, 0x63, 0x0C, 0],
+[0x51AD26, 0x4A, 0x29, 0],
+[0x51AD28, 0x84, 0x10, 0],
+[0x51AD4A, 0x63, 0x0C, 1],
+[0x51AD48, 0x84, 0x10, 1],
+[0x51AD46, 0x4A, 0x29, 1],
+[0x51A4AA, 0x63, 0x0C, 0],
+[0x51A4A6, 0x4A, 0x29, 0],
+[0x51A4A8, 0x84, 0x10, 0],
+[0x51A4CA, 0x63, 0x0C, 1],
+[0x51A4C8, 0x84, 0x10, 1],
+[0x51A4C6, 0x4A, 0x29, 1],
+[0x51A98A, 0x63, 0x0C, 0],
+[0x51A986, 0x4A, 0x29, 0],
+[0x51A988, 0x84, 0x10, 0],
+[0x51A96A, 0x63, 0x0C, 1],
+[0x51A968, 0x84, 0x10, 1],
+[0x51A966, 0x4A, 0x29, 1],
+[0x3CC982, 0x63, 0x0C, 0],
+[0x3CC980, 0x84, 0x10, 0],
+[0x3D4B4E, 0x08, 0x21, 0],
+[0x3D4B4C, 0x4A, 0x29, 0],
+[0x3CCA42, 0x63, 0x0C, 1],
+[0x3CCA40, 0x84, 0x10, 1],
+[0x3CFB3A, 0x63, 0x0C, 0],
+[0x3CFB38, 0x84, 0x10, 0],
+[0x3CFB7A, 0x63, 0x0C, 1],
+[0x3CFB78, 0x84, 0x10, 1],
+[0x4F4D5A, 0x63, 0x0C, 0],
+[0x4F4D58, 0x84, 0x10, 0],
+[0x3C931E, 0x63, 0x0C, 0],
+[0x3C931C, 0x84, 0x10, 0],
+[0x9F9CF4, 0x63, 0x0C, 0],
+[0x9F9CF2, 0x84, 0x10, 0],
+[0x4F4E1A, 0x63, 0x0C, 1],
+[0x4F4E18, 0x84, 0x10, 1],
+[0x3C944E, 0x63, 0x0C, 1],
+[0x3C944C, 0x84, 0x10, 1],
+[0x9F9D72, 0x63, 0x0C, 1],
+[0x9F9D70, 0x84, 0x10, 1],
+[0x518E10, 0x63, 0x0C, 0],
+[0x518E12, 0x4A, 0x29, 0],
+[0x3D4806, 0x63, 0x0C, 0],
+[0x3D4804, 0x84, 0x10, 0],
+[0x3CCA02, 0x63, 0x0C, 0],
+[0x3CCA00, 0x84, 0x10, 0],
+[0x3D14C6, 0x84, 0x10, 0],
+[0x3D1506, 0x84, 0x10, 1]]
+
+bluePants = [
+[0x3CC882, 0x00, 0x5C, 0],
+[0x3CC880, 0x00, 0x7C, 0],
+[0x3CC8C2, 0x00, 0x5C, 1],
+[0x3CC8C0, 0x00, 0x7C, 1],
+[0x4F4CDA, 0x00, 0x5C, 0],
+[0x4F4CD8, 0x00, 0x7C, 0],
+[0x4F4D1A, 0x00, 0x5C, 1],
+[0x4F4D18, 0x00, 0x7C, 1],
+[0x519014, 0x00, 0x5C, 0],
+[0x519016, 0x00, 0x7C, 0],
+[0x519034, 0x00, 0x5C, 1],
+[0x519036, 0x00, 0x7C, 1],
+[0x5193E6, 0x00, 0x5C, 0],
+[0x5193E4, 0x00, 0x7C, 0],
+[0x519406, 0x00, 0x5C, 1],
+[0x519404, 0x00, 0x7C, 1],
+[0x3CC9C2, 0x00, 0x5C, 0],
+[0x3CC9C0, 0x00, 0x7C, 0],
+[0x3CDE84, 0x00, 0x5C, 1],
+[0x3CDE74, 0x00, 0x5C, 1],
+[0x51AD2A, 0x00, 0x5C, 0],
+[0x51AD26, 0xB5, 0x7E, 0],
+[0x51AD28, 0x00, 0x7C, 0],
+[0x51AD4A, 0x00, 0x5C, 1],
+[0x51AD48, 0x00, 0x7C, 1],
+[0x51AD46, 0xB5, 0x7E, 1],
+[0x51A4AA, 0x00, 0x5C, 0],
+[0x51A4A6, 0xB5, 0x7E, 0],
+[0x51A4A8, 0x00, 0x7C, 0],
+[0x51A4CA, 0x00, 0x5C, 1],
+[0x51A4C8, 0x00, 0x7C, 1],
+[0x51A4C6, 0xB5, 0x7E, 1],
+[0x51A98A, 0x00, 0x5C, 0],
+[0x51A986, 0xB5, 0x7E, 0],
+[0x51A988, 0x00, 0x7C, 0],
+[0x51A96A, 0x00, 0x5C, 1],
+[0x51A968, 0x00, 0x7C, 1],
+[0x51A966, 0xB5, 0x7E, 1],
+[0x3CC982, 0x00, 0x5C, 0],
+[0x3CC980, 0x00, 0x7C, 0],
+[0x3D4B4E, 0x73, 0x72, 0],
+[0x3D4B4C, 0xB5, 0x7E, 0],
+[0x3CCA42, 0x00, 0x5C, 1],
+[0x3CCA40, 0x00, 0x7C, 1],
+[0x3CFB3A, 0x00, 0x5C, 0],
+[0x3CFB38, 0x00, 0x7C, 0],
+[0x3CFB7A, 0x00, 0x5C, 1],
+[0x3CFB78, 0x00, 0x7C, 1],
+[0x4F4D5A, 0x00, 0x5C, 0],
+[0x4F4D58, 0x00, 0x7C, 0],
+[0x3C931E, 0x00, 0x5C, 0],
+[0x3C931C, 0x00, 0x7C, 0],
+[0x9F9CF4, 0x00, 0x5C, 0],
+[0x9F9CF2, 0x00, 0x7C, 0],
+[0x4F4E1A, 0x00, 0x5C, 1],
+[0x4F4E18, 0x00, 0x7C, 1],
+[0x3C944E, 0x00, 0x5C, 1],
+[0x3C944C, 0x00, 0x7C, 1],
+[0x9F9D72, 0x00, 0x5C, 1],
+[0x9F9D70, 0x00, 0x7C, 1],
+[0x518E10, 0x00, 0x5C, 0],
+[0x518E12, 0xB5, 0x7E, 0],
+[0x3D4806, 0x00, 0x5C, 0],
+[0x3D4804, 0x00, 0x7C, 0],
+[0x3CCA02, 0x00, 0x5C, 0],
+[0x3CCA00, 0x00, 0x7C, 0],
+[0x3D14C6, 0x00, 0x7C, 0],
+[0x3D1506, 0x00, 0x7C, 1]]
+
+chaosPants = [
+[0x3CC880, 0],
+[0x3CC882, 0],
+[0x3CC8C0, 1],
+[0x3CC8C2, 1],
+[0x3CC980, 0],
+[0x3CC982, 0],
+[0x4f4cd8, 0],
+[0x4F4CDA, 0],
+[0x4F4D18, 1],
+[0x4F4D1A, 1],
+[0x519014, 0],
+[0x519016, 0],
+[0x519034, 1],
+[0x519036, 1],
+[0x5193E4, 0],
+[0x5193E6, 0],
+[0x519404, 1],
+[0x519406, 1],
+[0x51A988, 0],
+[0x51A98A, 0],
+[0x51A968, 1],
+[0x51A96A, 1],
+[0x51ad28, 0],
+[0x51AD2A, 0],
+[0x51AD48, 1],
+[0x51AD4A, 1],
+[0x3CFB38, 0],
+[0x3CFB3A, 0],
+[0x3CFB78, 1],
+[0x3CFB7A, 1],
+[0x3CC9C0, 0],
+[0x3CC9C2, 0],
+[0x3CCA40, 1],
+[0x3CCA42, 1],
+[0x3CDE74, 1],
+[0x3D4B4C, 0],
+[0x3D4B4E, 0]]
+
+greenPants = [
+[0x3CC882, 0xE0, 0x02, 0],
+[0x3CC880, 0xA3, 0x03, 0],
+[0x3CC8C2, 0xE0, 0x02, 1],
+[0x3CC8C0, 0xA3, 0x03, 1],
+[0x4F4CDA, 0xE0, 0x02, 0],
+[0x4F4CD8, 0xA3, 0x03, 0],
+[0x4F4D1A, 0xE0, 0x02, 1],
+[0x4F4D18, 0xA3, 0x03, 1],
+[0x519014, 0xE0, 0x02, 0],
+[0x519016, 0xA3, 0x03, 0],
+[0x519034, 0xE0, 0x02, 1],
+[0x519036, 0xA3, 0x03, 1],
+[0x5193E6, 0xE0, 0x02, 0],
+[0x5193E4, 0xA3, 0x03, 0],
+[0x519406, 0xE0, 0x02, 1],
+[0x519404, 0xA3, 0x03, 1],
+[0x3CC9C2, 0xE0, 0x02, 0],
+[0x3CC9C0, 0xA3, 0x03, 0],
+[0x3CDE84, 0xE0, 0x02, 1],
+[0x3CDE74, 0xE0, 0x02, 1],
+[0x51AD2A, 0xE0, 0x02, 0],
+[0x51AD26, 0xF6, 0x5B, 0],
+[0x51AD28, 0xA3, 0x03, 0],
+[0x51AD4A, 0xE0, 0x02, 1],
+[0x51AD48, 0xA3, 0x03, 1],
+[0x51AD46, 0xF6, 0x5B, 1],
+[0x51A4AA, 0xE0, 0x02, 0],
+[0x51A4A6, 0xF6, 0x5B, 0],
+[0x51A4A8, 0xA3, 0x03, 0],
+[0x51A4CA, 0xE0, 0x02, 1],
+[0x51A4C8, 0xA3, 0x03, 1],
+[0x51A4C6, 0xF6, 0x5B, 1],
+[0x51A98A, 0xE0, 0x02, 0],
+[0x51A986, 0xF6, 0x5B, 0],
+[0x51A988, 0xA3, 0x03, 0],
+[0x51A96A, 0xE0, 0x02, 1],
+[0x51A968, 0xA3, 0x03, 1],
+[0x51A966, 0xF6, 0x5B, 1],
+[0x3CC982, 0xE0, 0x02, 0],
+[0x3CC980, 0xA3, 0x03, 0],
+[0x3D4B4E, 0xB1, 0x3B, 0],
+[0x3D4B4C, 0xF6, 0x5B, 0],
+[0x3CCA42, 0xE0, 0x02, 1],
+[0x3CCA40, 0xA3, 0x03, 1],
+[0x3CFB3A, 0xE0, 0x02, 0],
+[0x3CFB38, 0xA3, 0x03, 0],
+[0x3CFB7A, 0xE0, 0x02, 1],
+[0x3CFB78, 0xA3, 0x03, 1],
+[0x4F4D5A, 0xE0, 0x02, 0],
+[0x4F4D58, 0xA3, 0x03, 0],
+[0x3C931E, 0xE0, 0x02, 0],
+[0x3C931C, 0xA3, 0x03, 0],
+[0x9F9CF4, 0xE0, 0x02, 0],
+[0x9F9CF2, 0xA3, 0x03, 0],
+[0x4F4E1A, 0xE0, 0x02, 1],
+[0x4F4E18, 0xA3, 0x03, 1],
+[0x3C944E, 0xE0, 0x02, 1],
+[0x3C944C, 0xA3, 0x03, 1],
+[0x9F9D72, 0xE0, 0x02, 1],
+[0x9F9D70, 0xA3, 0x03, 1],
+[0x518E10, 0xE0, 0x02, 0],
+[0x518E12, 0xF6, 0x5B, 0],
+[0x3D4806, 0xE0, 0x02, 0],
+[0x3D4804, 0xA3, 0x03, 0],
+[0x3CCA02, 0xE0, 0x02, 0],
+[0x3CCA00, 0xA3, 0x03, 0],
+[0x3D14C6, 0xA3, 0x03, 0],
+[0x3D1506, 0xA3, 0x03, 1]]
+
+orangePants = [
+[0x3CC882, 0x3C, 0x02, 0],
+[0x3CC880, 0x7F, 0x02, 0],
+[0x3CC8C2, 0x3C, 0x02, 1],
+[0x3CC8C0, 0x7F, 0x02, 1],
+[0x4F4CDA, 0x3C, 0x02, 0],
+[0x4F4CD8, 0x7F, 0x02, 0],
+[0x4F4D1A, 0x3C, 0x02, 1],
+[0x4F4D18, 0x7F, 0x02, 1],
+[0x519014, 0x3C, 0x02, 0],
+[0x519016, 0x7F, 0x02, 0],
+[0x519034, 0x3C, 0x02, 1],
+[0x519036, 0x7F, 0x02, 1],
+[0x5193E6, 0x3C, 0x02, 0],
+[0x5193E4, 0x7F, 0x02, 0],
+[0x519406, 0x3C, 0x02, 1],
+[0x519404, 0x7F, 0x02, 1],
+[0x3CC9C2, 0x3C, 0x02, 0],
+[0x3CC9C0, 0x7F, 0x02, 0],
+[0x3CDE84, 0x3C, 0x02, 1],
+[0x3CDE74, 0x3C, 0x02, 1],
+[0x51AD2A, 0x3C, 0x02, 0],
+[0x51AD26, 0x5E, 0x53, 0],
+[0x51AD28, 0x7F, 0x02, 0],
+[0x51AD4A, 0x3C, 0x02, 1],
+[0x51AD48, 0x7F, 0x02, 1],
+[0x51AD46, 0x5E, 0x53, 1],
+[0x51A4AA, 0x3C, 0x02, 0],
+[0x51A4A6, 0x5E, 0x53, 0],
+[0x51A4A8, 0x7F, 0x02, 0],
+[0x51A4CA, 0x3C, 0x02, 1],
+[0x51A4C8, 0x7F, 0x02, 1],
+[0x51A4C6, 0x5E, 0x53, 1],
+[0x51A98A, 0x3C, 0x02, 0],
+[0x51A986, 0x5E, 0x53, 0],
+[0x51A988, 0x7F, 0x02, 0],
+[0x51A96A, 0x3C, 0x02, 1],
+[0x51A968, 0x7F, 0x02, 1],
+[0x51A966, 0x5E, 0x53, 1],
+[0x3CC982, 0x3C, 0x02, 0],
+[0x3CC980, 0x7F, 0x02, 0],
+[0x3D4B4E, 0xFE, 0x42, 0],
+[0x3D4B4C, 0x5E, 0x53, 0],
+[0x3CCA42, 0x3C, 0x02, 1],
+[0x3CCA40, 0x7F, 0x02, 1],
+[0x3CFB3A, 0x3C, 0x02, 0],
+[0x3CFB38, 0x7F, 0x02, 0],
+[0x3CFB7A, 0x3C, 0x02, 1],
+[0x3CFB78, 0x7F, 0x02, 1],
+[0x4F4D5A, 0x3C, 0x02, 0],
+[0x4F4D58, 0x7F, 0x02, 0],
+[0x3C931E, 0x3C, 0x02, 0],
+[0x3C931C, 0x7F, 0x02, 0],
+[0x9F9CF4, 0x3C, 0x02, 0],
+[0x9F9CF2, 0x7F, 0x02, 0],
+[0x4F4E1A, 0x3C, 0x02, 1],
+[0x4F4E18, 0x7F, 0x02, 1],
+[0x3C944E, 0x3C, 0x02, 1],
+[0x3C944C, 0x7F, 0x02, 1],
+[0x9F9D72, 0x3C, 0x02, 1],
+[0x9F9D70, 0x7F, 0x02, 1],
+[0x518E10, 0x3C, 0x02, 0],
+[0x518E12, 0x5E, 0x53, 0],
+[0x3D4806, 0x3C, 0x02, 0],
+[0x3D4804, 0x7F, 0x02, 0],
+[0x3CCA02, 0x3C, 0x02, 0],
+[0x3CCA00, 0x7F, 0x02, 0],
+[0x3D14C6, 0x7F, 0x02, 0],
+[0x3D1506, 0x7F, 0x02, 1]]
+
+pinkPants = [
+[0x3CC882, 0xFF, 0x5D, 0],
+[0x3CC880, 0xDF, 0x6E, 0],
+[0x3CC8C2, 0xFF, 0x5D, 1],
+[0x3CC8C0, 0xDF, 0x6E, 1],
+[0x4F4CDA, 0xFF, 0x5D, 0],
+[0x4F4CD8, 0xDF, 0x6E, 0],
+[0x4F4D1A, 0xFF, 0x5D, 1],
+[0x4F4D18, 0xDF, 0x6E, 1],
+[0x519014, 0xFF, 0x5D, 0],
+[0x519016, 0xDF, 0x6E, 0],
+[0x519034, 0xFF, 0x5D, 1],
+[0x519036, 0xDF, 0x6E, 1],
+[0x5193E6, 0xFF, 0x5D, 0],
+[0x5193E4, 0xDF, 0x6E, 0],
+[0x519406, 0xFF, 0x5D, 1],
+[0x519404, 0xDF, 0x6E, 1],
+[0x3CC9C2, 0xFF, 0x5D, 0],
+[0x3CC9C0, 0xDF, 0x6E, 0],
+[0x3CDE84, 0xFF, 0x5D, 1],
+[0x3CDE74, 0xFF, 0x5D, 1],
+[0x51AD2A, 0xFF, 0x5D, 0],
+[0x51AD26, 0x7F, 0x77, 0],
+[0x51AD28, 0xDF, 0x6E, 0],
+[0x51AD4A, 0xFF, 0x5D, 1],
+[0x51AD48, 0xDF, 0x6E, 1],
+[0x51AD46, 0x7F, 0x77, 1],
+[0x51A4AA, 0xFF, 0x5D, 0],
+[0x51A4A6, 0x7F, 0x77, 0],
+[0x51A4A8, 0xDF, 0x6E, 0],
+[0x51A4CA, 0xFF, 0x5D, 1],
+[0x51A4C8, 0xDF, 0x6E, 1],
+[0x51A4C6, 0x7F, 0x77, 1],
+[0x51A98A, 0xFF, 0x5D, 0],
+[0x51A986, 0x7F, 0x77, 0],
+[0x51A988, 0xDF, 0x6E, 0],
+[0x51A96A, 0xFF, 0x5D, 1],
+[0x51A968, 0xDF, 0x6E, 1],
+[0x51A966, 0x7F, 0x77, 1],
+[0x3CC982, 0xFF, 0x5D, 0],
+[0x3CC980, 0xDF, 0x6E, 0],
+[0x3D4B4E, 0x3C, 0x6B, 0],
+[0x3D4B4C, 0x7F, 0x77, 0],
+[0x3CCA42, 0xFF, 0x5D, 1],
+[0x3CCA40, 0xDF, 0x6E, 1],
+[0x3CFB3A, 0xFF, 0x5D, 0],
+[0x3CFB38, 0xDF, 0x6E, 0],
+[0x3CFB7A, 0xFF, 0x5D, 1],
+[0x3CFB78, 0xDF, 0x6E, 1],
+[0x4F4D5A, 0xFF, 0x5D, 0],
+[0x4F4D58, 0xDF, 0x6E, 0],
+[0x3C931E, 0xFF, 0x5D, 0],
+[0x3C931C, 0xDF, 0x6E, 0],
+[0x9F9CF4, 0xFF, 0x5D, 0],
+[0x9F9CF2, 0xDF, 0x6E, 0],
+[0x4F4E1A, 0xFF, 0x5D, 1],
+[0x4F4E18, 0xDF, 0x6E, 1],
+[0x3C944E, 0xFF, 0x5D, 1],
+[0x3C944C, 0xDF, 0x6E, 1],
+[0x9F9D72, 0xFF, 0x5D, 1],
+[0x9F9D70, 0xDF, 0x6E, 1],
+[0x518E10, 0xFF, 0x5D, 0],
+[0x518E12, 0x7F, 0x77, 0],
+[0x3D4806, 0xFF, 0x5D, 0],
+[0x3D4804, 0xDF, 0x6E, 0],
+[0x3CCA02, 0xFF, 0x5D, 0],
+[0x3CCA00, 0xDF, 0x6E, 0],
+[0x3D14C6, 0xDF, 0x6E, 0],
+[0x3D1506, 0xDF, 0x6E, 1]]
+
+purplePants = [
+[0x3CC882, 0x0B, 0x5C, 0],
+[0x3CC880, 0x10, 0x7C, 0],
+[0x3CC8C2, 0x0B, 0x5C, 1],
+[0x3CC8C0, 0x10, 0x7C, 1],
+[0x4F4CDA, 0x0B, 0x5C, 0],
+[0x4F4CD8, 0x10, 0x7C, 0],
+[0x4F4D1A, 0x0B, 0x5C, 1],
+[0x4F4D18, 0x10, 0x7C, 1],
+[0x519014, 0x0B, 0x5C, 0],
+[0x519016, 0x10, 0x7C, 0],
+[0x519034, 0x0B, 0x5C, 1],
+[0x519036, 0x10, 0x7C, 1],
+[0x5193E6, 0x0B, 0x5C, 0],
+[0x5193E4, 0x10, 0x7C, 0],
+[0x519406, 0x0B, 0x5C, 1],
+[0x519404, 0x10, 0x7C, 1],
+[0x3CC9C2, 0x0B, 0x5C, 0],
+[0x3CC9C0, 0x10, 0x7C, 0],
+[0x3CDE84, 0x0B, 0x5C, 1],
+[0x3CDE74, 0x0B, 0x5C, 1],
+[0x51AD2A, 0x0B, 0x5C, 0],
+[0x51AD26, 0xFB, 0x7A, 0],
+[0x51AD28, 0x10, 0x7C, 0],
+[0x51AD4A, 0x0B, 0x5C, 1],
+[0x51AD48, 0x10, 0x7C, 1],
+[0x51AD46, 0xFB, 0x7A, 1],
+[0x51A4AA, 0x0B, 0x5C, 0],
+[0x51A4A6, 0xFB, 0x7A, 0],
+[0x51A4A8, 0x10, 0x7C, 0],
+[0x51A4CA, 0x0B, 0x5C, 1],
+[0x51A4C8, 0x10, 0x7C, 1],
+[0x51A4C6, 0xFB, 0x7A, 1],
+[0x51A98A, 0x0B, 0x5C, 0],
+[0x51A986, 0xFB, 0x7A, 0],
+[0x51A988, 0x10, 0x7C, 0],
+[0x51A96A, 0x0B, 0x5C, 1],
+[0x51A968, 0x10, 0x7C, 1],
+[0x51A966, 0xFB, 0x7A, 1],
+[0x3CC982, 0x0B, 0x5C, 0],
+[0x3CC980, 0x10, 0x7C, 0],
+[0x3D4B4E, 0x98, 0x6E, 0],
+[0x3D4B4C, 0xFB, 0x7A, 0],
+[0x3CCA42, 0x0B, 0x5C, 1],
+[0x3CCA40, 0x10, 0x7C, 1],
+[0x3CFB3A, 0x0B, 0x5C, 0],
+[0x3CFB38, 0x10, 0x7C, 0],
+[0x3CFB7A, 0x0B, 0x5C, 1],
+[0x3CFB78, 0x10, 0x7C, 1],
+[0x4F4D5A, 0x0B, 0x5C, 0],
+[0x4F4D58, 0x10, 0x7C, 0],
+[0x3C931E, 0x0B, 0x5C, 0],
+[0x3C931C, 0x10, 0x7C, 0],
+[0x9F9CF4, 0x0B, 0x5C, 0],
+[0x9F9CF2, 0x10, 0x7C, 0],
+[0x4F4E1A, 0x0B, 0x5C, 1],
+[0x4F4E18, 0x10, 0x7C, 1],
+[0x3C944E, 0x0B, 0x5C, 1],
+[0x3C944C, 0x10, 0x7C, 1],
+[0x9F9D72, 0x0B, 0x5C, 1],
+[0x9F9D70, 0x10, 0x7C, 1],
+[0x518E10, 0x0B, 0x5C, 0],
+[0x518E12, 0xFB, 0x7A, 0],
+[0x3D4806, 0x0B, 0x5C, 0],
+[0x3D4804, 0x10, 0x7C, 0],
+[0x3CCA02, 0x0B, 0x5C, 0],
+[0x3CCA00, 0x10, 0x7C, 0],
+[0x3D14C6, 0x10, 0x7C, 0],
+[0x3D1506, 0x10, 0x7C, 1]]
+
+redPants = [
+[0x3CC882, 0x17, 0x00, 0],
+[0x3CC880, 0x3F, 0x04, 0],
+[0x3CC8C2, 0x17, 0x00, 1],
+[0x3CC8C0, 0x3F, 0x04, 1],
+[0x4F4CDA, 0x17, 0x00, 0],
+[0x4F4CD8, 0x3F, 0x04, 0],
+[0x4F4D1A, 0x17, 0x00, 1],
+[0x4F4D18, 0x3F, 0x04, 1],
+[0x519014, 0x17, 0x00, 0],
+[0x519016, 0x3F, 0x04, 0],
+[0x519034, 0x17, 0x00, 1],
+[0x519036, 0x3F, 0x04, 1],
+[0x5193E6, 0x17, 0x00, 0],
+[0x5193E4, 0x3F, 0x04, 0],
+[0x519406, 0x17, 0x00, 1],
+[0x519404, 0x3F, 0x04, 1],
+[0x3CC9C2, 0x17, 0x00, 0],
+[0x3CC9C0, 0x3F, 0x04, 0],
+[0x3CDE84, 0x17, 0x00, 1],
+[0x3CDE74, 0x3F, 0x04, 1],
+[0x51AD2A, 0x17, 0x00, 0],
+[0x51AD26, 0xBF, 0x56, 0],
+[0x51AD28, 0x3F, 0x04, 0],
+[0x51AD4A, 0x17, 0x00, 1],
+[0x51AD48, 0x3F, 0x04, 1],
+[0x51AD46, 0xBF, 0x56, 1],
+[0x51A4AA, 0x17, 0x00, 0],
+[0x51A4A6, 0xBF, 0x56, 0],
+[0x51A4A8, 0x3F, 0x04, 0],
+[0x51A4CA, 0x17, 0x00, 1],
+[0x51A4C8, 0x3F, 0x04, 1],
+[0x51A4C6, 0xBF, 0x56, 1],
+[0x51A98A, 0x17, 0x00, 0],
+[0x51A986, 0xBF, 0x56, 0],
+[0x51A988, 0x3F, 0x04, 0],
+[0x51A96A, 0x17, 0x00, 1],
+[0x51A968, 0x3F, 0x04, 1],
+[0x51A966, 0xBF, 0x56, 1],
+[0x3CC982, 0x17, 0x00, 0],
+[0x3CC980, 0x3F, 0x04, 0],
+[0x3D4B4E, 0x1D, 0x3A, 0],
+[0x3D4B4C, 0xBF, 0x56, 0],
+[0x3CCA42, 0x17, 0x00, 1],
+[0x3CCA40, 0x3F, 0x04, 1],
+[0x3CFB3A, 0x17, 0x00, 0],
+[0x3CFB38, 0x3F, 0x04, 0],
+[0x3CFB7A, 0x17, 0x00, 1],
+[0x3CFB78, 0x3F, 0x04, 1],
+[0x4F4D5A, 0x17, 0x00, 0],
+[0x4F4D58, 0x3F, 0x04, 0],
+[0x3C931E, 0x17, 0x00, 0],
+[0x3C931C, 0x3F, 0x04, 0],
+[0x9F9CF4, 0x17, 0x00, 0],
+[0x9F9CF2, 0x3F, 0x04, 0],
+[0x4F4E1A, 0x17, 0x00, 1],
+[0x4F4E18, 0x3F, 0x04, 1],
+[0x3C944E, 0x17, 0x00, 1],
+[0x3C944C, 0x3F, 0x04, 1],
+[0x9F9D72, 0x17, 0x00, 1],
+[0x9F9D70, 0x3F, 0x04, 1],
+[0x518E10, 0x17, 0x00, 0],
+[0x518E12, 0xBF, 0x56, 0],
+[0x3D4806, 0x17, 0x00, 0],
+[0x3D4804, 0x3F, 0x04, 0],
+[0x3CCA02, 0x17, 0x00, 0],
+[0x3CCA00, 0x3F, 0x04, 0],
+[0x3D14C6, 0x3F, 0x04, 0],
+[0x3D1506, 0x3F, 0x04, 1]]
+
+whitePants = [
+[0x3CC882, 0x7B, 0x6F, 0],
+[0x3CC880, 0xFF, 0x7F, 0],
+[0x3CC8C2, 0x7B, 0x6F, 1],
+[0x3CC8C0, 0xFF, 0x7F, 1],
+[0x4F4CDA, 0x7B, 0x6F, 0],
+[0x4F4CD8, 0xFF, 0x7F, 0],
+[0x4F4D1A, 0x7B, 0x6F, 1],
+[0x4F4D18, 0xFF, 0x7F, 1],
+[0x519014, 0x7B, 0x6F, 0],
+[0x519016, 0xFF, 0x7F, 0],
+[0x519034, 0x7B, 0x6F, 1],
+[0x519036, 0xFF, 0x7F, 1],
+[0x5193E6, 0x7B, 0x6F, 0],
+[0x5193E4, 0xFF, 0x7F, 0],
+[0x519406, 0x7B, 0x6F, 1],
+[0x519404, 0xFF, 0x7F, 1],
+[0x3CC9C2, 0x7B, 0x6F, 0],
+[0x3CC9C0, 0xFF, 0x7F, 0],
+[0x3CDE84, 0x7B, 0x6F, 1],
+[0x3CDE74, 0x7B, 0x6F, 1],
+[0x51AD2A, 0x7B, 0x6F, 0],
+[0x51AD26, 0xFF, 0x7F, 0],
+[0x51AD28, 0xFF, 0x7F, 0],
+[0x51AD4A, 0x7B, 0x6F, 1],
+[0x51AD48, 0xFF, 0x7F, 1],
+[0x51AD46, 0xFF, 0x7F, 1],
+[0x51A4AA, 0x7B, 0x6F, 0],
+[0x51A4A6, 0xFF, 0x7F, 0],
+[0x51A4A8, 0xFF, 0x7F, 0],
+[0x51A4CA, 0x7B, 0x6F, 1],
+[0x51A4C8, 0xFF, 0x7F, 1],
+[0x51A4C6, 0xFF, 0x7F, 1],
+[0x51A98A, 0x7B, 0x6F, 0],
+[0x51A986, 0xFF, 0x7F, 0],
+[0x51A988, 0xFF, 0x7F, 0],
+[0x51A96A, 0x7B, 0x6F, 1],
+[0x51A968, 0xFF, 0x7F, 1],
+[0x51A966, 0xFF, 0x7F, 1],
+[0x3CC982, 0x7B, 0x6F, 0],
+[0x3CC980, 0xFF, 0x7F, 0],
+[0x3D4B4E, 0x7B, 0x6F, 0],
+[0x3D4B4C, 0xFF, 0x7F, 0],
+[0x3CCA42, 0x7B, 0x6F, 1],
+[0x3CCA40, 0xFF, 0x7F, 1],
+[0x3CFB3A, 0x7B, 0x6F, 0],
+[0x3CFB38, 0xFF, 0x7F, 0],
+[0x3CFB7A, 0x7B, 0x6F, 1],
+[0x3CFB78, 0xFF, 0x7F, 1],
+[0x4F4D5A, 0x7B, 0x6F, 0],
+[0x4F4D58, 0xFF, 0x7F, 0],
+[0x3C931E, 0x7B, 0x6F, 0],
+[0x3C931C, 0xFF, 0x7F, 0],
+[0x9F9CF4, 0x7B, 0x6F, 0],
+[0x9F9CF2, 0xFF, 0x7F, 0],
+[0x4F4E1A, 0x7B, 0x6F, 1],
+[0x4F4E18, 0xFF, 0x7F, 1],
+[0x3C944E, 0x7B, 0x6F, 1],
+[0x3C944C, 0xFF, 0x7F, 1],
+[0x9F9D72, 0x7B, 0x6F, 1],
+[0x9F9D70, 0xFF, 0x7F, 1],
+[0x518E10, 0x7B, 0x6F, 0],
+[0x518E12, 0xFF, 0x7F, 0],
+[0x3D4806, 0x7B, 0x6F, 0],
+[0x3D4804, 0xFF, 0x7F, 0],
+[0x3CCA02, 0x7B, 0x6F, 0],
+[0x3CCA00, 0xFF, 0x7F, 0],
+[0x3D14C6, 0xFF, 0x7F, 0],
+[0x3D1506, 0xFF, 0x7F, 1]]
+
+yellowPants = [
+[0x3CC882, 0xF7, 0x02, 0],
+[0x3CC880, 0xFF, 0x07, 0],
+[0x3CC8C2, 0xF7, 0x02, 1],
+[0x3CC8C0, 0xFF, 0x07, 1],
+[0x4F4CDA, 0xF7, 0x02, 0],
+[0x4F4CD8, 0xFF, 0x07, 0],
+[0x4F4D1A, 0xF7, 0x02, 1],
+[0x4F4D18, 0xFF, 0x07, 1],
+[0x519014, 0xF7, 0x02, 0],
+[0x519016, 0xFF, 0x07, 0],
+[0x519034, 0xF7, 0x02, 1],
+[0x519036, 0xFF, 0x07, 1],
+[0x5193E6, 0xF7, 0x02, 0],
+[0x5193E4, 0xFF, 0x07, 0],
+[0x519406, 0xF7, 0x02, 1],
+[0x519404, 0xFF, 0x07, 1],
+[0x3CC9C2, 0xF7, 0x02, 0],
+[0x3CC9C0, 0xFF, 0x07, 0],
+[0x3CDE84, 0xF7, 0x02, 1],
+[0x3CDE74, 0xF7, 0x02, 1],
+[0x51AD2A, 0xF7, 0x02, 0],
+[0x51AD26, 0xFF, 0x4F, 0],
+[0x51AD28, 0xFF, 0x07, 0],
+[0x51AD4A, 0xF7, 0x02, 1],
+[0x51AD48, 0xFF, 0x07, 1],
+[0x51AD46, 0xFF, 0x4F, 1],
+[0x51A4AA, 0xF7, 0x02, 0],
+[0x51A4A6, 0xFF, 0x4F, 0],
+[0x51A4A8, 0xFF, 0x07, 0],
+[0x51A4CA, 0xF7, 0x02, 1],
+[0x51A4C8, 0xFF, 0x07, 1],
+[0x51A4C6, 0xFF, 0x4F, 1],
+[0x51A98A, 0xF7, 0x02, 0],
+[0x51A986, 0xFF, 0x4F, 0],
+[0x51A988, 0xFF, 0x07, 0],
+[0x51A96A, 0xF7, 0x02, 1],
+[0x51A968, 0xFF, 0x07, 1],
+[0x51A966, 0xFF, 0x4F, 1],
+[0x3CC982, 0xF7, 0x02, 0],
+[0x3CC980, 0xFF, 0x07, 0],
+[0x3D4B4E, 0xDE, 0x27, 0],
+[0x3D4B4C, 0xFF, 0x4F, 0],
+[0x3CCA42, 0xF7, 0x02, 1],
+[0x3CCA40, 0xFF, 0x07, 1],
+[0x3CFB3A, 0xF7, 0x02, 0],
+[0x3CFB38, 0xFF, 0x07, 0],
+[0x3CFB7A, 0xF7, 0x02, 1],
+[0x3CFB78, 0xFF, 0x07, 1],
+[0x4F4D5A, 0xF7, 0x02, 0],
+[0x4F4D58, 0xFF, 0x07, 0],
+[0x3C931E, 0xF7, 0x02, 0],
+[0x3C931C, 0xFF, 0x07, 0],
+[0x9F9CF4, 0xF7, 0x02, 0],
+[0x9F9CF2, 0xFF, 0x07, 0],
+[0x4F4E1A, 0xF7, 0x02, 1],
+[0x4F4E18, 0xFF, 0x07, 1],
+[0x3C944E, 0xF7, 0x02, 1],
+[0x3C944C, 0xFF, 0x07, 1],
+[0x9F9D72, 0xF7, 0x02, 1],
+[0x9F9D70, 0xFF, 0x07, 1],
+[0x518E10, 0xF7, 0x02, 0],
+[0x518E12, 0xFF, 0x4F, 0],
+[0x3D4806, 0xF7, 0x02, 0],
+[0x3D4804, 0xFF, 0x07, 0],
+[0x3CCA02, 0xF7, 0x02, 0],
+[0x3CCA00, 0xFF, 0x07, 0],
+[0x3D14C6, 0xFF, 0x07, 0],
+[0x3D1506, 0xFF, 0x07, 1]]
diff --git a/worlds/mlss/Items.py b/worlds/mlss/Items.py
new file mode 100644
index 000000000000..b95f1a0bc0a8
--- /dev/null
+++ b/worlds/mlss/Items.py
@@ -0,0 +1,190 @@
+import typing
+
+from BaseClasses import Item, ItemClassification
+
+
+class ItemData(typing.NamedTuple):
+ code: int
+ itemName: str
+ classification: ItemClassification
+ itemID: int
+
+
+class MLSSItem(Item):
+ game: str = "Mario & Luigi Superstar Saga"
+
+
+itemList: typing.List[ItemData] = [
+ ItemData(77771000, "5 Coins", ItemClassification.filler, 0x4),
+ ItemData(77771001, "Mushroom", ItemClassification.filler, 0xA),
+ ItemData(77771002, "Super Mushroom", ItemClassification.filler, 0xB),
+ ItemData(77771003, "Ultra Mushroom", ItemClassification.filler, 0xC),
+ ItemData(77771004, "Max Mushroom", ItemClassification.filler, 0xD),
+ ItemData(77771005, "Nuts", ItemClassification.filler, 0xE),
+ ItemData(77771006, "Super Nuts", ItemClassification.filler, 0xF),
+ ItemData(77771007, "Ultra Nuts", ItemClassification.useful, 0x10),
+ ItemData(77771008, "Max Nuts", ItemClassification.useful, 0x11),
+ ItemData(77771009, "Syrup", ItemClassification.filler, 0x12),
+ ItemData(77771010, "Super Syrup", ItemClassification.filler, 0x13),
+ ItemData(77771011, "Ultra Syrup", ItemClassification.useful, 0x14),
+ ItemData(77771012, "Max Syrup", ItemClassification.useful, 0x15),
+ ItemData(77771013, "1-UP Mushroom", ItemClassification.useful, 0x16),
+ ItemData(77771014, "1-UP Super", ItemClassification.useful, 0x17),
+ ItemData(77771015, "Golden Mushroom", ItemClassification.useful, 0x18),
+ ItemData(77771016, "Refreshing Herb", ItemClassification.filler, 0x19),
+ ItemData(77771017, "Red Pepper", ItemClassification.useful, 0x1A),
+ ItemData(77771018, "Green Pepper", ItemClassification.useful, 0x1B),
+ ItemData(77771019, "Hoo Bean", ItemClassification.filler, 0x1D),
+ ItemData(77771020, "Chuckle Bean", ItemClassification.filler, 0x1E),
+ ItemData(77771021, "Woohoo Blend", ItemClassification.useful, 0x20),
+ ItemData(77771022, "Hoohoo Blend", ItemClassification.useful, 0x21),
+ ItemData(77771023, "Chuckle Blend", ItemClassification.useful, 0x22),
+ ItemData(77771024, "Teehee Blend", ItemClassification.useful, 0x23),
+ ItemData(77771025, "Hoolumbian", ItemClassification.useful, 0x24),
+ ItemData(77771026, "Chuckoccino", ItemClassification.useful, 0x25),
+ ItemData(77771027, "Teeheespresso", ItemClassification.useful, 0x26),
+ ItemData(77771028, "Peasley's Rose", ItemClassification.progression, 0x31),
+ ItemData(77771029, "Beanbean Brooch", ItemClassification.progression, 0x32),
+ ItemData(77771030, "Red Goblet", ItemClassification.progression, 0x33),
+ ItemData(77771031, "Green Goblet", ItemClassification.progression, 0x34),
+ ItemData(77771032, "Red Chuckola Fruit", ItemClassification.progression, 0x35),
+ ItemData(77771033, "White Chuckola Fruit", ItemClassification.progression, 0x36),
+ ItemData(77771034, "Purple Chuckola Fruit", ItemClassification.progression, 0x37),
+ ItemData(77771035, "Hammers", ItemClassification.progression, 0x38),
+ ItemData(77771036, "Firebrand", ItemClassification.progression, 0x39),
+ ItemData(77771037, "Thunderhand", ItemClassification.progression, 0x3A),
+ ItemData(77771038, "Membership Card", ItemClassification.progression, 0x40),
+ ItemData(77771039, "Winkle Card", ItemClassification.progression, 0x41),
+ ItemData(77771040, "Peach's Extra Dress", ItemClassification.progression, 0x42),
+ ItemData(77771041, "Fake Beanstar", ItemClassification.progression, 0x43),
+ ItemData(77771042, "Red Pearl Bean", ItemClassification.progression, 0x45),
+ ItemData(77771043, "Green Pearl Bean", ItemClassification.progression, 0x46),
+ ItemData(77771044, "Bean Fruit 1", ItemClassification.progression_skip_balancing, 0x47),
+ ItemData(77771045, "Bean Fruit 2", ItemClassification.progression_skip_balancing, 0x50),
+ ItemData(77771046, "Bean Fruit 3", ItemClassification.progression_skip_balancing, 0x51),
+ ItemData(77771047, "Bean Fruit 4", ItemClassification.progression_skip_balancing, 0x52),
+ ItemData(77771048, "Bean Fruit 5", ItemClassification.progression_skip_balancing, 0x53),
+ ItemData(77771049, "Bean Fruit 6", ItemClassification.progression_skip_balancing, 0x54),
+ ItemData(77771050, "Bean Fruit 7", ItemClassification.progression_skip_balancing, 0x55),
+ ItemData(77771051, "Blue Neon Egg", ItemClassification.progression, 0x56),
+ ItemData(77771052, "Red Neon Egg", ItemClassification.progression, 0x57),
+ ItemData(77771053, "Green Neon Egg", ItemClassification.progression, 0x60),
+ ItemData(77771054, "Yellow Neon Egg", ItemClassification.progression, 0x61),
+ ItemData(77771055, "Purple Neon Egg", ItemClassification.progression, 0x62),
+ ItemData(77771056, "Orange Neon Egg", ItemClassification.progression, 0x63),
+ ItemData(77771057, "Azure Neon Egg", ItemClassification.progression, 0x64),
+ ItemData(77771058, "Beanstar Piece 1", ItemClassification.progression, 0x65),
+ ItemData(77771059, "Beanstar Piece 2", ItemClassification.progression, 0x66),
+ ItemData(77771060, "Beanstar Piece 3", ItemClassification.progression, 0x67),
+ ItemData(77771061, "Beanstar Piece 4", ItemClassification.progression, 0x70),
+ ItemData(77771062, "Spangle", ItemClassification.progression, 0x72),
+ ItemData(77771063, "Beanlet 1", ItemClassification.filler, 0x73),
+ ItemData(77771064, "Beanlet 2", ItemClassification.filler, 0x74),
+ ItemData(77771065, "Beanlet 3", ItemClassification.filler, 0x75),
+ ItemData(77771066, "Beanlet 4", ItemClassification.filler, 0x76),
+ ItemData(77771067, "Beanlet 5", ItemClassification.filler, 0x77),
+ ItemData(77771068, "Beanstone 1", ItemClassification.filler, 0x80),
+ ItemData(77771069, "Beanstone 2", ItemClassification.filler, 0x81),
+ ItemData(77771070, "Beanstone 3", ItemClassification.filler, 0x82),
+ ItemData(77771071, "Beanstone 4", ItemClassification.filler, 0x83),
+ ItemData(77771072, "Beanstone 5", ItemClassification.filler, 0x84),
+ ItemData(77771073, "Beanstone 6", ItemClassification.filler, 0x85),
+ ItemData(77771074, "Beanstone 7", ItemClassification.filler, 0x86),
+ ItemData(77771075, "Beanstone 8", ItemClassification.filler, 0x87),
+ ItemData(77771076, "Beanstone 9", ItemClassification.filler, 0x90),
+ ItemData(77771077, "Beanstone 10", ItemClassification.filler, 0x91),
+ ItemData(77771078, "Secret Scroll 1", ItemClassification.useful, 0x92),
+ ItemData(77771079, "Secret Scroll 2", ItemClassification.useful, 0x93),
+ ItemData(77771080, "Castle Badge", ItemClassification.useful, 0x9F),
+ ItemData(77771081, "Pea Badge", ItemClassification.useful, 0xA0),
+ ItemData(77771082, "Bean B. Badge", ItemClassification.useful, 0xA1),
+ ItemData(77771083, "Counter Badge", ItemClassification.useful, 0xA2),
+ ItemData(77771084, "Charity Badge", ItemClassification.useful, 0xA3),
+ ItemData(77771085, "Bros. Badge", ItemClassification.useful, 0xA4),
+ ItemData(77771086, "Miracle Badge", ItemClassification.useful, 0xA5),
+ ItemData(77771087, "Ohoracle Badge", ItemClassification.useful, 0xA6),
+ ItemData(77771088, "Mush Badge", ItemClassification.useful, 0xA7),
+ ItemData(77771089, "Mari-Lui Badge", ItemClassification.useful, 0xA8),
+ ItemData(77771090, "Muscle Badge", ItemClassification.useful, 0xA9),
+ ItemData(77771091, "Spiny Badge AA", ItemClassification.useful, 0xAA),
+ ItemData(77771092, "Mush Badge A", ItemClassification.useful, 0xAB),
+ ItemData(77771093, "Grab Badge", ItemClassification.useful, 0xAC),
+ ItemData(77771094, "Mush Badge AA", ItemClassification.useful, 0xAD),
+ ItemData(77771095, "Power Badge", ItemClassification.useful, 0xAE),
+ ItemData(77771096, "Wonder Badge", ItemClassification.useful, 0xAF),
+ ItemData(77771097, "Beauty Badge", ItemClassification.useful, 0xB0),
+ ItemData(77771098, "Salvage Badge", ItemClassification.useful, 0xB1),
+ ItemData(77771099, "Oh-Pah Badge", ItemClassification.useful, 0xB2),
+ ItemData(77771100, "Brilliant Badge", ItemClassification.useful, 0xB3),
+ ItemData(77771101, "Sarge Badge", ItemClassification.useful, 0xB4),
+ ItemData(77771102, "General Badge", ItemClassification.useful, 0xB5),
+ ItemData(77771103, "Tank Badge", ItemClassification.useful, 0xB6),
+ ItemData(77771104, "Bros. Rock", ItemClassification.useful, 0xBD),
+ ItemData(77771105, "Soulful Bros.", ItemClassification.useful, 0xC0),
+ ItemData(77771106, "High-End Badge", ItemClassification.useful, 0xC1),
+ ItemData(77771107, "Bean Pants", ItemClassification.useful, 0xCC),
+ ItemData(77771108, "Bean Trousers", ItemClassification.useful, 0xCD),
+ ItemData(77771109, "Blue Jeans", ItemClassification.useful, 0xCE),
+ ItemData(77771110, "Parasol Pants", ItemClassification.useful, 0xCF),
+ ItemData(77771111, "Hard Pants", ItemClassification.useful, 0xD0),
+ ItemData(77771112, "Heart Jeans", ItemClassification.useful, 0xD1),
+ ItemData(77771113, "Plaid Trousers", ItemClassification.useful, 0xD2),
+ ItemData(77771114, "#1 Trousers", ItemClassification.useful, 0xD3),
+ ItemData(77771115, "Safety Slacks", ItemClassification.useful, 0xD4),
+ ItemData(77771116, "Shroom Pants", ItemClassification.useful, 0xD5),
+ ItemData(77771117, "Shroom Bells", ItemClassification.useful, 0xD6),
+ ItemData(77771118, "Shroom Slacks", ItemClassification.useful, 0xD7),
+ ItemData(77771119, "Peachy Jeans", ItemClassification.useful, 0xD8),
+ ItemData(77771120, "Mushwin Pants", ItemClassification.useful, 0xD9),
+ ItemData(77771121, "Mushluck Pants", ItemClassification.useful, 0xDA),
+ ItemData(77771122, "Scandal Jeans", ItemClassification.useful, 0xDB),
+ ItemData(77771123, "Street Jeans", ItemClassification.useful, 0xDC),
+ ItemData(77771124, "Tropic Slacks", ItemClassification.useful, 0xDD),
+ ItemData(77771125, "Hermetic Pants", ItemClassification.useful, 0xDE),
+ ItemData(77771126, "Beanstar Pants", ItemClassification.useful, 0xDF),
+ ItemData(77771127, "Peasley Slacks", ItemClassification.useful, 0xE0),
+ ItemData(77771128, "Queen B. Jeans", ItemClassification.useful, 0xE1),
+ ItemData(77771129, "B. Brand Jeans", ItemClassification.useful, 0xE2),
+ ItemData(77771130, "Heart Slacks", ItemClassification.useful, 0xE3),
+ ItemData(77771131, "Casual Slacks", ItemClassification.useful, 0xE4),
+ ItemData(77771132, "Casual Coral", ItemClassification.useful, 0xEB),
+ ItemData(77771133, "Harhall's Pants", ItemClassification.useful, 0xF1),
+ ItemData(77771134, "Wool Trousers", ItemClassification.useful, 0xF3),
+ ItemData(77771135, "Iron Pants", ItemClassification.useful, 0xF7),
+ ItemData(77771136, "Greed Wallet", ItemClassification.useful, 0xF8),
+ ItemData(77771137, "Bonus Ring", ItemClassification.useful, 0xF9),
+ ItemData(77771138, "Excite Spring", ItemClassification.useful, 0xFA),
+ ItemData(77771139, "Great Force", ItemClassification.useful, 0xFB),
+ ItemData(77771140, "Power Grip", ItemClassification.useful, 0xFC),
+ ItemData(77771141, "Cobalt Necktie", ItemClassification.useful, 0xFD),
+ ItemData(77771142, "Game Boy Horror SP", ItemClassification.useful, 0xFE),
+ ItemData(77771143, "Woo Bean", ItemClassification.skip_balancing, 0x1C),
+ ItemData(77771144, "Hee Bean", ItemClassification.skip_balancing, 0x1F),
+]
+
+item_frequencies: typing.Dict[str, int] = {
+ "5 Coins": 40,
+ "Mushroom": 55,
+ "Super Mushroom": 15,
+ "Ultra Mushroom": 12,
+ "Nuts": 10,
+ "Super Nuts": 5,
+ "Ultra Nuts": 5,
+ "Max Nuts": 2,
+ "Syrup": 28,
+ "Super Syrup": 10,
+ "Ultra Syrup": 10,
+ "Max Syrup": 2,
+ "1-Up Mushroom": 15,
+ "1-Up Super": 5,
+ "Golden Mushroom": 3,
+ "Refreshing Herb": 9,
+ "Red Pepper": 2,
+ "Green Pepper": 2,
+ "Hoo Bean": 100,
+ "Chuckle Bean": 200,
+ "Hammers": 3,
+}
+
+item_table: typing.Dict[str, ItemData] = {item.itemName: item for item in itemList}
+items_by_id: typing.Dict[int, ItemData] = {item.code: item for item in itemList}
diff --git a/worlds/mlss/Locations.py b/worlds/mlss/Locations.py
new file mode 100644
index 000000000000..8c00432a8f06
--- /dev/null
+++ b/worlds/mlss/Locations.py
@@ -0,0 +1,1185 @@
+import typing
+
+from BaseClasses import Location
+
+
+class LocationData:
+ name: str = ""
+ id: int = 0x00
+
+ def __init__(self, name, id_, itemType):
+ self.name = name
+ self.itemType = itemType
+ self.id = id_
+
+
+class MLSSLocation(Location):
+ game: str = "Mario & Luigi Superstar Saga"
+
+
+hidden = [
+ 0x39D8C5,
+ 0x39D90F,
+ 0x39D9E9,
+ 0x39DB02,
+ 0x39DAB5,
+ 0x39DB0F,
+ 0x39DB2A,
+ 0x39DB32,
+ 0x39DBBC,
+ 0x39DBE1,
+ 0x39DC65,
+ 0x39DC5D,
+ 0x39DC82,
+ 0x39DCC4,
+ 0x39DCE1,
+ 0x39DD13,
+ 0x39DDF6,
+ 0x39DEA8,
+ 0x39DED7,
+ 0x39DF63,
+ 0x39E077,
+ 0x39E092,
+ 0x39E0CD,
+ 0x39E0FA,
+ 0x39E102,
+ 0x39E187,
+ 0x39E1BC,
+ 0x39E1C9,
+ 0x39E1E3,
+ 0x39E21D,
+ 0x39E232,
+ 0x39E2DC,
+ 0x39E2E9,
+ 0x39E316,
+ 0x39E343,
+ 0x39E370,
+ 0x39E396,
+ 0x39E3D1,
+ 0x39E3F3,
+ 0x39E462,
+ 0x39E477,
+ 0x39E51E,
+ 0x39E5B5,
+ 0x39E5C8,
+ 0x39E5D0,
+ 0x39E5F0,
+ 0x39E5FD,
+ 0x39E6C2,
+ 0x39E6CF,
+ 0x39E702,
+ 0x39E857,
+ 0x39E8A3,
+ 0x39E91A,
+ 0x39E944,
+ 0x39E959,
+ 0x39E983,
+ 0x39E9A0,
+ 0x39EC40,
+ 0x39EC4D,
+]
+
+
+mainArea: typing.List[LocationData] = [
+ LocationData("Stardust Fields Room 1 Block 1", 0x39D65D, 0),
+ LocationData("Stardust Fields Room 1 Block 2", 0x39D665, 0),
+ LocationData("Stardust Fields Room 2 Block", 0x39D678, 0),
+ LocationData("Stardust Fields Room 3 Block", 0x39D6AD, 0),
+ LocationData("Stardust Fields Room 4 Block 1", 0x39D6CA, 0),
+ LocationData("Stardust Fields Room 4 Block 2", 0x39D6C2, 0),
+ LocationData("Stardust Fields Room 4 Block 3", 0x39D6BA, 0),
+ LocationData("Stardust Fields Room 5 Block", 0x39D713, 0),
+ LocationData("Hoohoo Village Hammer House Block", 0x39D731, 0),
+ LocationData("Hoohoo Mountain Below Summit Block 1", 0x39D873, 0),
+ LocationData("Hoohoo Mountain Below Summit Block 2", 0x39D87B, 0),
+ LocationData("Hoohoo Mountain Below Summit Block 3", 0x39D883, 0),
+ LocationData("Hoohoo Mountain After Hoohooros Block 1", 0x39D890, 0),
+ LocationData("Hoohoo Mountain After Hoohooros Block 2", 0x39D8A0, 0),
+ LocationData("Hoohoo Mountain Hoohooros Room Block 1", 0x39D8AD, 0),
+ LocationData("Hoohoo Mountain Hoohooros Room Block 2", 0x39D8B5, 0),
+ LocationData("Hoohoo Mountain Before Hoohooros Block", 0x39D8D2, 0),
+ LocationData("Hoohoo Mountain Fountain Room Block 1", 0x39D8F2, 0),
+ LocationData("Hoohoo Mountain Fountain Room Block 2", 0x39D8FA, 0),
+ LocationData("Hoohoo Mountain Room 1 Block 1", 0x39D91C, 0),
+ LocationData("Hoohoo Mountain Room 1 Block 2", 0x39D924, 0),
+ LocationData("Hoohoo Mountain Room 1 Block 3", 0x39D92C, 0),
+ LocationData("Hoohoo Mountain Base Room 1 Block", 0x39D939, 0),
+ LocationData("Hoohoo Village Right Side Block", 0x39D957, 0),
+ LocationData("Hoohoo Village Bridge Room Block 1", 0x39D96F, 0),
+ LocationData("Hoohoo Village Bridge Room Block 2", 0x39D97F, 0),
+ LocationData("Hoohoo Village Bridge Room Block 3", 0x39D98F, 0),
+ LocationData("Hoohoo Mountain Base Bridge Room Block 1", 0x39D99C, 0),
+ LocationData("Hoohoo Mountain Base Bridge Room Block 2", 0x39D9A4, 0),
+ LocationData("Hoohoo Mountain Base Bridge Room Block 3", 0x39D9AC, 0),
+ LocationData("Hoohoo Mountain Base Bridge Room Block 4", 0x39D9B4, 0),
+ LocationData("Hoohoo Mountain Base Bridge Room Digspot", 0x39D9BC, 0),
+ LocationData("Hoohoo Mountain Base Boostatue Room Block 1", 0x39D9C9, 0),
+ LocationData("Hoohoo Mountain Base Boostatue Room Block 2", 0x39D9D1, 0),
+ LocationData("Hoohoo Mountain Base Boostatue Room Digspot 1", 0x39D9D9, 0),
+ LocationData("Hoohoo Mountain Base Boostatue Room Digspot 2", 0x39D9E1, 0),
+ LocationData("Hoohoo Mountain Base Grassy Area Block 1", 0x39D9FE, 0),
+ LocationData("Hoohoo Mountain Base Grassy Area Block 2", 0x39D9F6, 0),
+ LocationData("Hoohoo Mountain Base After Minecart Minigame Block 1", 0x39DA35, 0),
+ LocationData("Hoohoo Mountain Base After Minecart Minigame Block 2", 0x39DA2D, 0),
+ LocationData("Cave Connecting Stardust Fields and Hoohoo Village Block 1", 0x39DA77, 0),
+ LocationData("Cave Connecting Stardust Fields and Hoohoo Village Block 2", 0x39DA7F, 0),
+ LocationData("Hoohoo Village South Cave Block", 0x39DACD, 0),
+ LocationData("Hoohoo Village North Cave Room 1 Block", 0x39DA98, 0),
+ LocationData("Hoohoo Village North Cave Room 2 Block", 0x39DAAD, 0),
+ LocationData("Beanbean Outskirts Surf Beach Block", 0x39DD03, 0),
+ LocationData("Woohoo Hooniversity Star Room Block 1", 0x39E13D, 0),
+ LocationData("Woohoo Hooniversity Star Room Block 2", 0x39E145, 0),
+ LocationData("Woohoo Hooniversity Star Room Block 3", 0x39E14D, 0),
+ LocationData("Woohoo Hooniversity Sun Door Block 1", 0x39E15A, 0),
+ LocationData("Woohoo Hooniversity Sun Door Block 2", 0x39E162, 0),
+ LocationData("Woohoo Hooniversity West of Star Room 4 Block 1", 0x39E1F0, 0),
+ LocationData("Woohoo Hooniversity West of Star Room 4 Block 2", 0x39E1F8, 0),
+ LocationData("Woohoo Hooniversity West of Star Room 4 Block 3", 0x39E200, 0),
+ LocationData("Hoohoo Mountain Fountain Room 2 Block", 0x39E8F5, 0),
+ LocationData("Hoohoo Mountain Past Hoohooros Connector Room Block", 0x39E912, 0),
+ LocationData("Outside Woohoo Hooniversity Block", 0x39E9B5, 0),
+ LocationData("Shop Starting Flag 1", 0x3C05F0, 3),
+ LocationData("Shop Starting Flag 2", 0x3C05F2, 3),
+ LocationData("Shop Starting Flag 3", 0x3C05F4, 3),
+ LocationData("Hoohoo Mountain Summit Digspot", 0x39D85E, 0),
+ LocationData("Hoohoo Mountain Below Summit Digspot", 0x39D86B, 0),
+ LocationData("Hoohoo Mountain After Hoohooros Digspot", 0x39D898, 0),
+ LocationData("Hoohoo Mountain Hoohooros Room Digspot 1", 0x39D8BD, 0),
+ LocationData("Hoohoo Mountain Hoohooros Room Digspot 2", 0x39D8C5, 0),
+ LocationData("Hoohoo Mountain Before Hoohooros Digspot", 0x39D8E2, 0),
+ LocationData("Hoohoo Mountain Room 2 Digspot 1", 0x39D907, 0),
+ LocationData("Hoohoo Mountain Room 2 Digspot 2", 0x39D90F, 0),
+ LocationData("Hoohoo Mountain Base Room 1 Digspot", 0x39D941, 0),
+ LocationData("Hoohoo Village Right Side Digspot", 0x39D95F, 0),
+ LocationData("Hoohoo Village Super Hammer Cave Digspot", 0x39DB02, 0),
+ LocationData("Hoohoo Village Super Hammer Cave Block", 0x39DAEA, 0),
+ LocationData("Hoohoo Village North Cave Room 2 Digspot", 0x39DAB5, 0),
+ LocationData("Hoohoo Mountain Base Minecart Cave Digspot", 0x39DB0F, 0),
+ LocationData("Beanbean Outskirts Farm Room Digspot 1", 0x39DB22, 0),
+ LocationData("Beanbean Outskirts Farm Room Digspot 2", 0x39DB2A, 0),
+ LocationData("Beanbean Outskirts Farm Room Digspot 3", 0x39DB32, 0),
+ LocationData("Beanbean Outskirts NW Block", 0x39DB87, 0),
+ LocationData("Beanbean Outskirts NW Digspot", 0x39DB97, 0),
+ LocationData("Beanbean Outskirts W Digspot 1", 0x39DBAC, 0),
+ LocationData("Beanbean Outskirts W Digspot 2", 0x39DBB4, 0),
+ LocationData("Beanbean Outskirts W Digspot 3", 0x39DBBC, 0),
+ LocationData("Beanbean Outskirts SW Digspot 1", 0x39DBC9, 0),
+ LocationData("Beanbean Outskirts SW Digspot 2", 0x39DBD9, 0),
+ LocationData("Beanbean Outskirts SW Digspot 3", 0x39DBE1, 0),
+ LocationData("Beanbean Outskirts N Room 1 Digspot", 0x39DBEE, 0),
+ LocationData("Beanbean Outskirts N Room 2 Digspot", 0x39DBFB, 0),
+ LocationData("Beanbean Outskirts S Room 1 Digspot 1", 0x39DC08, 0),
+ LocationData("Beanbean Outskirts S Room 1 Block", 0x39DC20, 0),
+ LocationData("Beanbean Outskirts S Room 1 Digspot 2", 0x39DC28, 0),
+ LocationData("Beanbean Outskirts S Room 2 Block 1", 0x39DC4D, 0),
+ LocationData("Beanbean Outskirts NE Digspot 1", 0x39DC7A, 0),
+ LocationData("Beanbean Outskirts NE Digspot 2", 0x39DC82, 0),
+ LocationData("Beanbean Outskirts E Digspot 1", 0x39DC8F, 0),
+ LocationData("Beanbean Outskirts E Digspot 2", 0x39DC97, 0),
+ LocationData("Beanbean Outskirts E Digspot 3", 0x39DC9F, 0),
+ LocationData("Beanbean Outskirts SE Digspot 1", 0x39DCAC, 0),
+ LocationData("Beanbean Outskirts SE Digspot 2", 0x39DCBC, 0),
+ LocationData("Beanbean Outskirts SE Digspot 3", 0x39DCC4, 0),
+ LocationData("Beanbean Outskirts North Beach Digspot 1", 0x39DCD1, 0),
+ LocationData("Beanbean Outskirts North Beach Digspot 2", 0x39DCE1, 0),
+ LocationData("Beanbean Outskirts North Beach Digspot 3", 0x39DCD9, 0),
+ LocationData("Beanbean Outskirts South Beach Digspot", 0x39DCEE, 0),
+ LocationData("Woohoo Hooniversity West of Star Room Digspot 1", 0x39E17F, 0),
+ LocationData("Woohoo Hooniversity West of Star Room Digspot 2", 0x39E187, 0),
+ LocationData("Woohoo Hooniversity West of Star Room 2 Digspot", 0x39E1D6, 0),
+ LocationData("Woohoo Hooniversity West of Star Room 3 Digspot", 0x39E1E3, 0),
+ LocationData("Woohoo Hooniversity West of Star Room 4 Digspot 1", 0x39E208, 0),
+ LocationData("Woohoo Hooniversity West of Star Room 4 Digspot 2", 0x39E210, 0),
+ LocationData("Woohoo Hooniversity West of Star Room 5 Digspot", 0x39E21D, 0),
+ LocationData("Woohoo Hooniversity Entrance to Mini Mario Room Digspot 1", 0x39E22A, 0),
+ LocationData("Woohoo Hooniversity Entrance to Mini Mario Room Digspot 2", 0x39E232, 0),
+ LocationData("Woohoo Hooniversity Entrance to Mini Mario Room 2 Digspot", 0x39E23F, 0),
+ LocationData("Woohoo Hooniversity Mini Mario Puzzle Block", 0x39E24C, 0),
+ LocationData("Woohoo Hooniversity Mini Mario Puzzle Digspot", 0x39E254, 0),
+ LocationData("Woohoo Hooniversity Mini Mario Puzzle Secret Area Block 1", 0x39E261, 0),
+ LocationData("Woohoo Hooniversity Mini Mario Puzzle Secret Area Block 2", 0x39E269, 0),
+ LocationData("Woohoo Hooniversity Mini Mario Puzzle Secret Area Block 3", 0x39E271, 0),
+ LocationData("Woohoo Hooniversity Mini Mario Puzzle Secret Area Block 4", 0x39E279, 0),
+ LocationData("Hoohoo Mountain Fountain Room 2 Digspot", 0x39E8FD, 0),
+ LocationData("Hoohoo Mountain Past Hoohooros Connector Room Digspot 1", 0x39E90A, 0),
+ LocationData("Hoohoo Mountain Past Hoohooros Connector Room Digspot 2", 0x39E91A, 0),
+ LocationData("Beanbean Outskirts Secret Scroll 1", 0x1E9411, 2),
+ LocationData("Beanbean Outskirts Secret Scroll 2", 0x1E9412, 2),
+ LocationData("Beanbean Outskirts Bean Fruit 1", 0x229345, 1),
+ LocationData("Beanbean Outskirts Bean Fruit 2", 0x22954D, 1),
+ LocationData("Beanbean Outskirts Bean Fruit 3", 0x228A17, 1),
+ LocationData("Beanbean Outskirts Bean Fruit 4", 0x22913A, 1),
+ LocationData("Beanbean Outskirts Bean Fruit 5", 0x22890E, 1),
+ LocationData("Beanbean Outskirts Bean Fruit 6", 0x228775, 1),
+ LocationData("Beanbean Outskirts Bean Fruit 7", 0x1E9431, 2),
+ LocationData("Hoohoo Village Mole Behind Turtle", 0x277AB2, 1),
+ LocationData("Beanbean Outskirts Thunderhand Mole", 0x2779C8, 1),
+ LocationData("Hoohoo Mountain Peasley's Rose", 0x1E9430, 2),
+ LocationData("Beanbean Outskirts Super Hammer Upgrade", 0x1E9404, 2),
+ LocationData("Beanbean Outskirts Ultra Hammer Upgrade", 0x1E9405, 2),
+ LocationData("Beanbean Outskirts NE Solo Mario Mole 1", 0x1E9435, 2),
+ LocationData("Beanbean Outskirts NE Solo Mario Mole 2", 0x1E9436, 2),
+ LocationData("Hoohoo Village Hammers", 0x1E9403, 2),
+ LocationData("Beanbean Outskirts Solo Luigi Cave Mole", 0x242888, 1),
+ LocationData("Beanbean Outskirts Farm Room Mole Reward 1", 0x243844, 1),
+ LocationData("Beanbean Outskirts Farm Room Mole Reward 2", 0x24387D, 1),
+ LocationData("Beanbean Outskirts South of Hooniversity Guards Digspot 1", 0x39E990, 0),
+ LocationData("Beanbean Outskirts South of Hooniversity Guards Digspot 2", 0x39E998, 0),
+ LocationData("Beanbean Outskirts South of Hooniversity Guards Digspot 3", 0x39E9A0, 0),
+ LocationData("Beanbean Outskirts Entrance to Hoohoo Mountain Base Digspot 1", 0x39EB5A, 0),
+ LocationData("Beanbean Outskirts Entrance to Hoohoo Mountain Base Digspot 2", 0x39EB62, 0),
+ LocationData("Beanbean Outskirts Pipe 2 Room Digspot", 0x39EC40, 0),
+ LocationData("Beanbean Outskirts Pipe 4 Room Digspot", 0x39EC4D, 0),
+ LocationData("Beanbean Castle Town Mini Mario Block 1", 0x39D813, 0),
+ LocationData("Beanbean Castle Town Mini Mario Block 2", 0x39D81B, 0),
+ LocationData("Beanbean Castle Town Mini Mario Block 3", 0x39D823, 0),
+ LocationData("Beanbean Castle Town Mini Mario Block 4", 0x39D82B, 0),
+ LocationData("Beanbean Castle Town Mini Mario Block 5", 0x39D833, 0),
+]
+
+coins: typing.List[LocationData] = [
+ LocationData("Stardust Fields Room 2 Coin Block 1", 0x39D680, 0),
+ LocationData("Stardust Fields Room 2 Coin Block 2", 0x39D688, 0),
+ LocationData("Stardust Fields Room 2 Coin Block 3", 0x39D690, 0),
+ LocationData("Stardust Fields Room 3 Coin Block 1", 0x39D69D, 0),
+ LocationData("Stardust Fields Room 3 Coin Block 2", 0x39D6A5, 0),
+ LocationData("Stardust Fields Room 5 Coin Block 1", 0x39D6D7, 0),
+ LocationData("Stardust Fields Room 5 Coin Block 2", 0x39D6DF, 0),
+ LocationData("Stardust Fields Room 7 Coin Block 1", 0x39D70B, 0),
+ LocationData("Stardust Fields Room 7 Coin Block 2", 0x39D71B, 0),
+ LocationData("Beanbean Castle Town Passport Photo Room Coin Block", 0x39D803, 0),
+ LocationData("Hoohoo Mountain Before Hoohooros Coin Block", 0x39D8DA, 0),
+ LocationData("Hoohoo Village Bridge Room Coin Block 1", 0x39D977, 0),
+ LocationData("Hoohoo Village Bridge Room Coin Block 2", 0x39D987, 0),
+ LocationData("Hoohoo Village North Cave Room 1 Coin Block", 0x39DAA0, 0),
+ LocationData("Hoohoo Village South Cave Coin Block 1", 0x39DAC5, 0),
+ LocationData("Hoohoo Village South Cave Coin Block 2", 0x39DAD5, 0),
+ LocationData("Hoohoo Mountain Base Boo Statue Cave Coin Block 1", 0x39DAE2, 0),
+ LocationData("Hoohoo Mountain Base Boo Statue Cave Coin Block 2", 0x39DAF2, 0),
+ LocationData("Hoohoo Mountain Base Boo Statue Cave Coin Block 3", 0x39DAFA, 0),
+ LocationData("Beanbean Outskirts NW Coin Block", 0x39DB8F, 0),
+ LocationData("Beanbean Outskirts S Room 1 Coin Block", 0x39DC18, 0),
+ LocationData("Beanbean Outskirts S Room 2 Coin Block", 0x39DC3D, 0),
+ LocationData("Chateau Popple Room Coin Block 1", 0x39DD30, 0),
+ LocationData("Chateau Popple Room Coin Block 2", 0x39DD40, 0),
+ LocationData("Chucklehuck Woods Cave Room 1 Coin Block", 0x39DD7A, 0),
+ LocationData("Chucklehuck Woods Cave Room 2 Coin Block", 0x39DD97, 0),
+ LocationData("Chucklehuck Woods Cave Room 3 Coin Block", 0x39DDB4, 0),
+ LocationData("Chucklehuck Woods Pipe 5 Room Coin Block", 0x39DDE6, 0),
+ LocationData("Chucklehuck Woods Room 7 Coin Block", 0x39DE31, 0),
+ LocationData("Chucklehuck Woods After Chuckleroot Coin Block", 0x39DF14, 0),
+ LocationData("Chucklehuck Woods Koopa Room Coin Block", 0x39DF53, 0),
+ LocationData("Chucklehuck Woods Winkle Area Cave Coin Block", 0x39DF80, 0),
+ LocationData("Sewers Prison Room Coin Block", 0x39E01E, 0),
+ LocationData("Gwarhar Lagoon First Underwater Area Room 2 Coin Block", 0x39E455, 0),
+ LocationData("Teehee Valley Past Ultra Hammer Rocks Coin Block", 0x39E588, 0),
+ LocationData("S.S. Chuckola Storage Room Coin Block 1", 0x39E618, 0),
+ LocationData("S.S. Chuckola Storage Room Coin Block 2", 0x39E620, 0),
+ LocationData("Joke's End Second Floor West Room Coin Block", 0x39E771, 0),
+ LocationData("Joke's End North of Bridge Room Coin Block", 0x39E836, 0),
+ LocationData("Outside Woohoo Hooniversity Coin Block 1", 0x39E9AD, 0),
+ LocationData("Outside Woohoo Hooniversity Coin Block 2", 0x39E9BD, 0),
+ LocationData("Outside Woohoo Hooniversity Coin Block 3", 0x39E9C5, 0),
+]
+
+baseUltraRocks: typing.List[LocationData] = [
+ LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 1", 0x39DA42, 0),
+ LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 2", 0x39DA4A, 0),
+ LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 3", 0x39DA52, 0),
+ LocationData("Hoohoo Mountain Base Boostatue Room Digspot 3 (Rightside)", 0x39D9E9, 0),
+ LocationData("Hoohoo Mountain Base Mole Near Teehee Valley", 0x277A45, 1),
+ LocationData("Teehee Valley Entrance To Hoohoo Mountain Digspot", 0x39E5B5, 0),
+ LocationData("Teehee Valley Solo Luigi Maze Room 2 Digspot 1", 0x39E5C8, 0),
+ LocationData("Teehee Valley Solo Luigi Maze Room 2 Digspot 2", 0x39E5D0, 0),
+ LocationData("Hoohoo Mountain Base Guffawha Ruins Entrance Digspot", 0x39DA0B, 0),
+ LocationData("Hoohoo Mountain Base Teehee Valley Entrance Digspot", 0x39DA20, 0),
+ LocationData("Hoohoo Mountain Base Teehee Valley Entrance Block", 0x39DA18, 0),
+]
+
+booStatue: typing.List[LocationData] = [
+ LocationData("Beanbean Outskirts Before Harhall Digspot 1", 0x39E951, 0),
+ LocationData("Beanbean Outskirts Before Harhall Digspot 2", 0x39E959, 0),
+ LocationData("Beanstar Piece Harhall", 0x1E9441, 2),
+ LocationData("Beanbean Outskirts Boo Statue Mole", 0x1E9434, 2),
+ LocationData("Harhall's Pants", 0x1E9444, 2),
+ LocationData("Beanbean Outskirts S Room 2 Digspot 1", 0x39DC65, 0),
+ LocationData("Beanbean Outskirts S Room 2 Digspot 2", 0x39DC5D, 0),
+ LocationData("Beanbean Outskirts S Room 2 Block 2", 0x39DC45, 0),
+ LocationData("Beanbean Outskirts S Room 2 Digspot 3", 0x39DC35, 0),
+]
+
+chucklehuck: typing.List[LocationData] = [
+ LocationData("Chateau Room 1 Digspot", 0x39DD20, 0),
+ LocationData("Chateau Popple Fight Room Block 1", 0x39DD38, 0),
+ LocationData("Chateau Popple Fight Room Block 2", 0x39DD48, 0),
+ LocationData("Chateau Popple Fight Room Digspot", 0x39DD50, 0),
+ LocationData("Chateau Barrel Room Digspot", 0x39DD5D, 0),
+ LocationData("Chateau Goblet Room Digspot", 0x39DD6D, 0),
+ LocationData("Chucklehuck Woods Cave Room 1 Block 1", 0x39DD82, 0),
+ LocationData("Chucklehuck Woods Cave Room 1 Block 2", 0x39DD8A, 0),
+ LocationData("Chucklehuck Woods Cave Room 2 Block", 0x39DD9F, 0),
+ LocationData("Chucklehuck Woods Cave Room 3 Block", 0x39DDAC, 0),
+ LocationData("Chucklehuck Woods Room 2 Block", 0x39DDC1, 0),
+ LocationData("Chucklehuck Woods Room 2 Digspot", 0x39DDC9, 0),
+ LocationData("Chucklehuck Woods Pipe Room Block 1", 0x39DDD6, 0),
+ LocationData("Chucklehuck Woods Pipe Room Block 2", 0x39DDDE, 0),
+ LocationData("Chucklehuck Woods Pipe Room Digspot 1", 0x39DDEE, 0),
+ LocationData("Chucklehuck Woods Pipe Room Digspot 2", 0x39DDF6, 0),
+ LocationData("Chucklehuck Woods Room 4 Block 1", 0x39DE06, 0),
+ LocationData("Chucklehuck Woods Room 4 Block 2", 0x39DE0E, 0),
+ LocationData("Chucklehuck Woods Room 4 Block 3", 0x39DE16, 0),
+ LocationData("Chucklehuck Woods Room 7 Block 1", 0x39DE29, 0),
+ LocationData("Chucklehuck Woods Room 7 Block 2", 0x39DE39, 0),
+ LocationData("Chucklehuck Woods Room 7 Digspot 1", 0x39DE41, 0),
+ LocationData("Chucklehuck Woods Room 7 Digspot 2", 0x39DE49, 0),
+ LocationData("Chucklehuck Woods Room 8 Digspot", 0x39DE56, 0),
+ LocationData("Chucklehuck Woods East of Chuckleroot Digspot", 0x39DE66, 0),
+ LocationData("Chucklehuck Woods Northeast of Chuckleroot Digspot 1", 0x39DE73, 0),
+ LocationData("Chucklehuck Woods Northeast of Chuckleroot Digspot 2", 0x39DE7B, 0),
+ LocationData("Chucklehuck Woods Northeast of Chuckleroot Digspot 3", 0x39DE83, 0),
+ LocationData("Chucklehuck Woods Northeast of Chuckleroot Digspot 4", 0x39DE8B, 0),
+ LocationData("Chucklehuck Woods White Fruit Room Digspot 1", 0x39DE98, 0),
+ LocationData("Chucklehuck Woods White Fruit Room Digspot 2", 0x39DEA0, 0),
+ LocationData("Chucklehuck Woods White Fruit Room Digspot 3", 0x39DEA8, 0),
+ LocationData("Chucklehuck Woods West of Chuckleroot Block", 0x39DEB5, 0),
+ LocationData("Chucklehuck Woods Southwest of Chuckleroot Block", 0x39DEC2, 0),
+ LocationData("Chucklehuck Woods Wiggler room Digspot 1", 0x39DECF, 0),
+ LocationData("Chucklehuck Woods Wiggler room Digspot 2", 0x39DED7, 0),
+ LocationData("Chucklehuck Woods After Chuckleroot Block 1", 0x39DEE4, 0),
+ LocationData("Chucklehuck Woods After Chuckleroot Block 2", 0x39DEEC, 0),
+ LocationData("Chucklehuck Woods After Chuckleroot Block 3", 0x39DEF4, 0),
+ LocationData("Chucklehuck Woods After Chuckleroot Block 4", 0x39DEFC, 0),
+ LocationData("Chucklehuck Woods After Chuckleroot Block 5", 0x39DF04, 0),
+ LocationData("Chucklehuck Woods After Chuckleroot Block 6", 0x39DF0C, 0),
+ LocationData("Chucklehuck Woods Koopa Room Block 1", 0x39DF4B, 0),
+ LocationData("Chucklehuck Woods Koopa Room Block 2", 0x39DF5B, 0),
+ LocationData("Chucklehuck Woods Koopa Room Digspot", 0x39DF63, 0),
+ LocationData("Chucklehuck Woods Room 1 Digspot", 0x39E1C9, 0),
+ LocationData("Beanbean Outskirts Brooch Guards Room Digspot 1", 0x39E966, 0),
+ LocationData("Beanbean Outskirts Brooch Guards Room Digspot 2", 0x39E96E, 0),
+ LocationData("Beanbean Outskirts Chateau Entrance Digspot 1", 0x39E97B, 0),
+ LocationData("Beanbean Outskirts Chateau Entrance Digspot 2", 0x39E983, 0),
+ LocationData("Chateau Green Goblet", 0x24E628, 1),
+ LocationData("Chateau Red Goblet", 0x1E943E, 2),
+ LocationData("Chucklehuck Woods Red Chuckola Fruit", 0x250621, 2),
+ LocationData("Chucklehuck Woods White Chuckola Fruit", 0x24FF18, 2),
+ LocationData("Chucklehuck Woods Purple Chuckola Fruit", 0x24ED74, 1),
+]
+
+castleTown: typing.List[LocationData] = [
+ LocationData("Beanbean Castle Town Left Side House Block 1", 0x39D7A4, 0),
+ LocationData("Beanbean Castle Town Left Side House Block 2", 0x39D7AC, 0),
+ LocationData("Beanbean Castle Town Left Side House Block 3", 0x39D7B4, 0),
+ LocationData("Beanbean Castle Town Left Side House Block 4", 0x39D7BC, 0),
+ LocationData("Beanbean Castle Town Right Side House Block 1", 0x39D7D8, 0),
+ LocationData("Beanbean Castle Town Right Side House Block 2", 0x39D7E0, 0),
+ LocationData("Beanbean Castle Town Right Side House Block 3", 0x39D7E8, 0),
+ LocationData("Beanbean Castle Town Right Side House Block 4", 0x39D7F0, 0),
+ LocationData("Beanbean Castle Peach's Extra Dress", 0x1E9433, 2),
+ LocationData("Beanbean Castle Fake Beanstar", 0x1E9432, 2),
+ LocationData("Beanbean Castle Town Beanlet 1", 0x251347, 1),
+ LocationData("Beanbean Castle Town Beanlet 2", 0x2513FB, 1),
+ LocationData("Beanbean Castle Town Beanlet 3", 0x2513A1, 1),
+ LocationData("Beanbean Castle Town Beanlet 4", 0x251988, 1),
+ LocationData("Beanbean Castle Town Beanlet 5", 0x25192E, 1),
+ LocationData("Beanbean Castle Town Beanstone 1", 0x25117D, 1),
+ LocationData("Beanbean Castle Town Beanstone 2", 0x2511D6, 1),
+ LocationData("Beanbean Castle Town Beanstone 3", 0x25122F, 1),
+ LocationData("Beanbean Castle Town Beanstone 4", 0x251288, 1),
+ LocationData("Beanbean Castle Town Beanstone 5", 0x2512E1, 1),
+ LocationData("Beanbean Castle Town Beanstone 6", 0x25170B, 1),
+ LocationData("Beanbean Castle Town Beanstone 7", 0x251767, 1),
+ LocationData("Beanbean Castle Town Beanstone 8", 0x2517C3, 1),
+ LocationData("Beanbean Castle Town Beanstone 9", 0x25181F, 1),
+ LocationData("Beanbean Castle Town Beanstone 10", 0x25187B, 1),
+ LocationData("Coffee Shop Brew Reward 1", 0x253515, 1),
+ LocationData("Coffee Shop Brew Reward 2", 0x253776, 1),
+ LocationData("Coffee Shop Brew Reward 3", 0x253C70, 1),
+ LocationData("Coffee Shop Brew Reward 4", 0x254324, 1),
+ LocationData("Coffee Shop Brew Reward 5", 0x254718, 1),
+ LocationData("Coffee Shop Brew Reward 6", 0x254A34, 1),
+ LocationData("Coffee Shop Brew Reward 7", 0x254E24, 1),
+ LocationData("Coffee Shop Woohoo Blend", 0x252D07, 1),
+ LocationData("Coffee Shop Hoohoo Blend", 0x252D28, 1),
+ LocationData("Coffee Shop Chuckle Blend", 0x252D49, 1),
+ LocationData("Coffee Shop Teehee Blend", 0x252D6A, 1),
+ LocationData("Coffee Shop Hoolumbian", 0x252D8B, 1),
+ LocationData("Coffee Shop Chuckoccino", 0x252DAC, 1),
+ LocationData("Coffee Shop Teeheespresso", 0x252DCD, 1),
+ LocationData("Beanbean Castle Town Beanstone Reward", 0x251071, 1),
+ LocationData("Beanbean Castle Town Beanlet Reward", 0x2515EB, 1),
+]
+
+eReward: typing.List[int] = [0x253515, 0x253776, 0x253C70, 0x254324, 0x254718, 0x254A34, 0x254E24]
+
+startingFlag: typing.List[LocationData] = [
+ LocationData("Badge Shop Starting Flag 1", 0x3C0618, 2),
+ LocationData("Badge Shop Starting Flag 2", 0x3C061A, 2),
+ LocationData("Pants Shop Starting Flag 1", 0x3C061C, 2),
+ LocationData("Pants Shop Starting Flag 2", 0x3C061E, 2),
+ LocationData("Pants Shop Starting Flag 3", 0x3C0620, 2),
+]
+
+chuckolatorFlag: typing.List[LocationData] = [
+ LocationData("Shop Chuckolator Flag", 0x3C05F8, 3),
+ LocationData("Pants Shop Chuckolator Flag 1", 0x3C062A, 2),
+ LocationData("Pants Shop Chuckolator Flag 2", 0x3C062C, 2),
+ LocationData("Pants Shop Chuckolator Flag 3", 0x3C062E, 2),
+ LocationData("Badge Shop Chuckolator Flag 1", 0x3C0624, 2),
+ LocationData("Badge Shop Chuckolator Flag 2", 0x3C0626, 2),
+ LocationData("Badge Shop Chuckolator Flag 3", 0x3C0628, 2),
+]
+
+piranhaFlag: typing.List[LocationData] = [
+ LocationData("Shop Mom Piranha Flag 1", 0x3C05FC, 3),
+ LocationData("Shop Mom Piranha Flag 2", 0x3C05FE, 3),
+ LocationData("Shop Mom Piranha Flag 3", 0x3C0600, 3),
+ LocationData("Shop Mom Piranha Flag 4", 0x3C0602, 3),
+ LocationData("Pants Shop Mom Piranha Flag 1", 0x3C0638, 2),
+ LocationData("Pants Shop Mom Piranha Flag 2", 0x3C063A, 2),
+ LocationData("Pants Shop Mom Piranha Flag 3", 0x3C063C, 2),
+ LocationData("Badge Shop Mom Piranha Flag 1", 0x3C0632, 2),
+ LocationData("Badge Shop Mom Piranha Flag 2", 0x3C0634, 2),
+ LocationData("Badge Shop Mom Piranha Flag 3", 0x3C0636, 2),
+]
+
+kidnappedFlag: typing.List[LocationData] = [
+ LocationData("Badge Shop Enter Fungitown Flag 1", 0x3C0640, 2),
+ LocationData("Badge Shop Enter Fungitown Flag 2", 0x3C0642, 2),
+ LocationData("Badge Shop Enter Fungitown Flag 3", 0x3C0644, 2),
+ LocationData("Pants Shop Enter Fungitown Flag 1", 0x3C0646, 2),
+ LocationData("Pants Shop Enter Fungitown Flag 2", 0x3C0648, 2),
+ LocationData("Pants Shop Enter Fungitown Flag 3", 0x3C064A, 2),
+ LocationData("Shop Enter Fungitown Flag 1", 0x3C0606, 3),
+ LocationData("Shop Enter Fungitown Flag 2", 0x3C0608, 3),
+]
+
+beanstarFlag: typing.List[LocationData] = [
+ LocationData("Badge Shop Beanstar Complete Flag 1", 0x3C064E, 2),
+ LocationData("Badge Shop Beanstar Complete Flag 2", 0x3C0650, 2),
+ LocationData("Badge Shop Beanstar Complete Flag 3", 0x3C0652, 2),
+ LocationData("Pants Shop Beanstar Complete Flag 1", 0x3C0654, 2),
+ LocationData("Pants Shop Beanstar Complete Flag 2", 0x3C0656, 2),
+ LocationData("Pants Shop Beanstar Complete Flag 3", 0x3C0658, 2),
+ LocationData("Shop Beanstar Complete Flag 1", 0x3C060C, 3),
+ LocationData("Shop Beanstar Complete Flag 2", 0x3C060E, 3),
+ LocationData("Shop Beanstar Complete Flag 3", 0x3C0610, 3),
+]
+
+birdoFlag: typing.List[LocationData] = [
+ LocationData("Badge Shop Birdo Flag 1", 0x3C065C, 2),
+ LocationData("Badge Shop Birdo Flag 2", 0x3C065E, 2),
+ LocationData("Badge Shop Birdo Flag 3", 0x3C0660, 2),
+ LocationData("Pants Shop Birdo Flag 1", 0x3C0662, 2),
+ LocationData("Pants Shop Birdo Flag 2", 0x3C0664, 2),
+ LocationData("Pants Shop Birdo Flag 3", 0x3C0666, 2),
+ LocationData("Shop Birdo Flag", 0x3C0614, 3),
+]
+
+winkle: typing.List[LocationData] = [
+ LocationData("Chucklehuck Woods Winkle Cave Block 1", 0x39DF70, 0),
+ LocationData("Chucklehuck Woods Winkle Cave Block 2", 0x39DF78, 0),
+ LocationData("Winkle Area Beanstar Room Block", 0x39DF21, 0),
+ LocationData("Winkle Area Digspot", 0x39DF2E, 0),
+ LocationData("Winkle Area Outside Colloseum Block", 0x39DF3B, 0),
+ LocationData("Winkle Area Colloseum Digspot", 0x39E8A3, 0),
+ LocationData("Beanstar Piece Winkle Area", 0x1E9440, 2),
+ LocationData("Winkle Area Winkle Card", 0x261658, 1),
+]
+
+sewers: typing.List[LocationData] = [
+ LocationData("Sewers Room 3 Block 1", 0x39DFE6, 0),
+ LocationData("Sewers Room 3 Block 2", 0x39DFEE, 0),
+ LocationData("Sewers Room 3 Block 3", 0x39DFF6, 0),
+ LocationData("Sewers Room 5 Block 1", 0x39E006, 0),
+ LocationData("Sewers Room 5 Block 2", 0x39E00E, 0),
+ LocationData("Sewers Prison Room Block 1", 0x39E026, 0),
+ LocationData("Sewers Prison Room Block 2", 0x39E02E, 0),
+ LocationData("Sewers Prison Room Block 3", 0x39E036, 0),
+ LocationData("Sewers Prison Room Block 4", 0x39E03E, 0),
+ LocationData("Beanbean Castle Beanbean Brooch", 0x2578E7, 1),
+]
+
+hooniversity: typing.List[LocationData] = [
+ LocationData("Woohoo Hooniversity South Of Star Room Block", 0x39E16F, 0),
+ LocationData("Woohoo Hooniversity Barrel Puzzle Entrance Digspot 1", 0x39E194, 0),
+ LocationData("Woohoo Hooniversity Barrel Puzzle Entrance Block 1", 0x39E19C, 0),
+ LocationData("Woohoo Hooniversity Barrel Puzzle Entrance Block 2", 0x39E1A4, 0),
+ LocationData("Woohoo Hooniversity Barrel Puzzle Entrance Block 3", 0x39E1AC, 0),
+ LocationData("Woohoo Hooniversity Barrel Puzzle Entrance Block 4", 0x39E1B4, 0),
+ LocationData("Woohoo Hooniversity Barrel Puzzle Entrance Digspot 2", 0x39E1BC, 0),
+ LocationData("Woohoo Hooniversity Past Sun Door Block 1", 0x39E28C, 0),
+ LocationData("Woohoo Hooniversity Past Sun Door Block 2", 0x39E294, 0),
+ LocationData("Woohoo Hooniversity Past Sun Door Block 3", 0x39E29C, 0),
+ LocationData("Woohoo Hooniversity Past Cackletta Room 1 Block", 0x39E2AC, 0),
+ LocationData("Woohoo Hooniversity Past Cackletta Room 2 Block 1", 0x39E2BF, 0),
+ LocationData("Woohoo Hooniversity Past Cackletta Room 2 Block 2", 0x39E2C7, 0),
+ LocationData("Woohoo Hooniversity Past Cackletta Room 2 Digspot", 0x39E2CF, 0),
+ LocationData("Woohoo Hooniversity Basement Room 1 Digspot", 0x39E4C6, 0),
+ LocationData("Woohoo Hooniversity Basement Room 2 Digspot", 0x39E4D3, 0),
+ LocationData("Woohoo Hooniversity Basement Room 3 Block", 0x39E4E0, 0),
+ LocationData("Woohoo Hooniversity Basement Room 4 Block", 0x39E4ED, 0),
+ LocationData("Woohoo Hooniversity Popple Room Digspot 1", 0x39E4FA, 0),
+ LocationData("Woohoo Hooniversity Popple Room Digspot 2", 0x39E502, 0),
+ LocationData("Woohoo Hooniversity Solo Mario Barrel Area Block 1", 0x39EC05, 0),
+ LocationData("Woohoo Hooniversity Solo Mario Barrel Area Block 2", 0x39EC0D, 0),
+ LocationData("Woohoo Hooniversity Solo Mario Barrel Area Block 3", 0x39EC15, 0),
+]
+
+surfable: typing.List[LocationData] = [
+ LocationData("Oho Ocean North Whirlpool Block 1", 0x39E0A5, 0),
+ LocationData("Oho Ocean North Whirlpool Block 2", 0x39E0AD, 0),
+ LocationData("Oho Ocean North Whirlpool Block 3", 0x39E0B5, 0),
+ LocationData("Oho Ocean North Whirlpool Block 4", 0x39E0BD, 0),
+ LocationData("Oho Ocean North Whirlpool Digspot 1", 0x39E0C5, 0),
+ LocationData("Oho Ocean North Whirlpool Digspot 2", 0x39E0CD, 0),
+ LocationData("Oho Ocean Fire Puzzle Room Digspot", 0x39E057, 0),
+ LocationData("Oho Ocean South Whirlpool Digspot 1", 0x39E0DA, 0),
+ LocationData("Oho Ocean South Whirlpool Digspot 2", 0x39E0E2, 0),
+ LocationData("Oho Ocean South Whirlpool Digspot 3", 0x39E0EA, 0),
+ LocationData("Oho Ocean South Whirlpool Digspot 4", 0x39E0F2, 0),
+ LocationData("Oho Ocean South Whirlpool Digspot 5", 0x39E0FA, 0),
+ LocationData("Oho Ocean South Whirlpool Digspot 6", 0x39E102, 0),
+ LocationData("Oho Ocean South Whirlpool Room 2 Digspot", 0x39E10F, 0),
+ LocationData("Joke's End Pipe Digspot", 0x39E6C2, 0),
+ LocationData("Joke's End Staircase Digspot", 0x39E6CF, 0),
+ LocationData("Surf Minigame", 0x2753EA, 1),
+ LocationData("North Ocean Whirlpool Mole", 0x277956, 1),
+ LocationData("Beanbean Outskirts Surf Beach Digspot 1", 0x39DCFB, 0),
+ LocationData("Beanbean Outskirts Surf Beach Digspot 2", 0x39DD0B, 0),
+ LocationData("Beanbean Outskirts Surf Beach Digspot 3", 0x39DD13, 0),
+]
+
+airport: typing.List[LocationData] = [
+ LocationData("Airport Entrance Digspot", 0x39E2DC, 0),
+ LocationData("Airport Lobby Digspot", 0x39E2E9, 0),
+ LocationData("Airport Leftside Digspot 1", 0x39E2F6, 0),
+ LocationData("Airport Leftside Digspot 2", 0x39E2FE, 0),
+ LocationData("Airport Leftside Digspot 3", 0x39E306, 0),
+ LocationData("Airport Leftside Digspot 4", 0x39E30E, 0),
+ LocationData("Airport Leftside Digspot 5", 0x39E316, 0),
+ LocationData("Airport Center Digspot 1", 0x39E323, 0),
+ LocationData("Airport Center Digspot 2", 0x39E32B, 0),
+ LocationData("Airport Center Digspot 3", 0x39E333, 0),
+ LocationData("Airport Center Digspot 4", 0x39E33B, 0),
+ LocationData("Airport Center Digspot 5", 0x39E343, 0),
+ LocationData("Airport Rightside Digspot 1", 0x39E350, 0),
+ LocationData("Airport Rightside Digspot 2", 0x39E358, 0),
+ LocationData("Airport Rightside Digspot 3", 0x39E360, 0),
+ LocationData("Airport Rightside Digspot 4", 0x39E368, 0),
+ LocationData("Airport Rightside Digspot 5", 0x39E370, 0),
+]
+
+gwarharEntrance: typing.List[LocationData] = [
+ LocationData("Gwarhar Lagoon Pipe Room Digspot", 0x39E37D, 0),
+ LocationData("Gwarhar Lagoon Massage Parlor Entrance Digspot", 0x39E396, 0),
+ LocationData("Gwarhar Lagoon First Underwater Area Room 1 Block", 0x39E438, 0),
+ LocationData("Gwarhar Lagoon First Underwater Area Room 2 Block 1", 0x39E445, 0),
+ LocationData("Gwarhar Lagoon First Underwater Area Room 2 Block 2", 0x39E44D, 0),
+ LocationData("Gwarhar Lagoon Red Pearl Bean", 0x235C1C, 1),
+ LocationData("Gwarhar Lagoon Green Pearl Bean", 0x235A5B, 1),
+ LocationData("Oho Ocean South Room 1 Block", 0x39E06A, 0),
+ LocationData("Oho Ocean South Room 2 Digspot", 0x39E077, 0),
+]
+
+gwarharMain: typing.List[LocationData] = [
+ LocationData("Gwarhar Lagoon Past Hermie Digspot", 0x39E3A6, 0),
+ LocationData("Gwarhar Lagoon East of Stone Bridge Block", 0x39E403, 0),
+ LocationData("Gwarhar Lagoon North of Spangle Room Digspot", 0x39E40B, 0),
+ LocationData("Gwarhar Lagoon West of Spangle Room Digspot", 0x39E41B, 0),
+ LocationData("Gwarhar Lagoon Second Underwater Area Room 4 Digspot", 0x39E462, 0),
+ LocationData("Gwarhar Lagoon Second Underwater Area Room 2 Digspot 1", 0x39E46F, 0),
+ LocationData("Gwarhar Lagoon Second Underwater Area Room 2 Digspot 2", 0x39E477, 0),
+ LocationData("Gwarhar Lagoon Second Underwater Area Room 3 Block 1", 0x39E484, 0),
+ LocationData("Gwarhar Lagoon Second Underwater Area Room 3 Block 2", 0x39E48C, 0),
+ LocationData("Gwarhar Lagoon Second Underwater Area Room 3 Block 3", 0x39E494, 0),
+ LocationData("Gwarhar Lagoon Second Underwater Area Room 1 Block", 0x39E4A1, 0),
+ LocationData("Gwarhar Lagoon Entrance to West Underwater Area Digspot", 0x39E3BC, 0),
+ LocationData("Gwarhar Lagoon Fire Dash Puzzle Room 1 Digspot 1", 0x39E3C9, 0),
+ LocationData("Gwarhar Lagoon Fire Dash Puzzle Room 1 Digspot 2", 0x39E3D1, 0),
+ LocationData("Gwarhar Lagoon Fire Dash Puzzle Room 2 Digspot", 0x39E3DE, 0),
+ LocationData("Gwarhar Lagoon Fire Dash Puzzle Room 3 Digspot 1", 0x39E3EB, 0),
+ LocationData("Gwarhar Lagoon Fire Dash Puzzle Room 3 Digspot 2", 0x39E3F3, 0),
+ LocationData("Gwarhar Lagoon Spangle Room Block", 0x39E428, 0),
+ LocationData("Gwarhar Lagoon Spangle Reward", 0x236E73, 1),
+ LocationData("Beanstar Piece Hermie", 0x1E9443, 2),
+ LocationData("Gwarhar Lagoon Spangle", 0x1E9437, 2),
+]
+
+teeheeValley: typing.List[LocationData] = [
+ LocationData("Teehee Valley Room 1 Digspot 1", 0x39E51E, 0),
+ LocationData("Teehee Valley Room 1 Digspot 2", 0x39E526, 0),
+ LocationData("Teehee Valley Room 1 Digspot 3", 0x39E52E, 0),
+ LocationData("Teehee Valley Room 2 Digspot 1", 0x39E53B, 0),
+ LocationData("Teehee Valley Room 2 Digspot 2", 0x39E543, 0),
+ LocationData("Teehee Valley Room 2 Digspot 3", 0x39E54B, 0),
+ LocationData("Teehee Valley Past Ultra Hammer Rock Block 1", 0x39E580, 0),
+ LocationData("Teehee Valley Past Ultra Hammer Rock Block 2", 0x39E590, 0),
+ LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 1", 0x39E598, 0),
+ LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 3", 0x39E5A8, 0),
+ LocationData("Teehee Valley Solo Luigi Maze Room 1 Block", 0x39E5E0, 0),
+ LocationData("Teehee Valley Before Trunkle Digspot", 0x39E5F0, 0),
+ LocationData("S.S. Chuckola Storage Room Block 1", 0x39E610, 0),
+ LocationData("S.S. Chuckola Storage Room Block 2", 0x39E628, 0),
+ LocationData("S.S. Chuckola Membership Card", 0x260637, 1),
+]
+
+fungitown: typing.List[LocationData] = [
+ LocationData("Teehee Valley Trunkle Room Digspot", 0x39E5FD, 0),
+ LocationData("Fungitown Embassy Room Block", 0x39E66B, 0),
+ LocationData("Fungitown Entrance Room Block", 0x39E67E, 0),
+ LocationData("Fungitown Badge Shop Starting Flag 1", 0x3C0684, 2),
+ LocationData("Fungitown Badge Shop Starting Flag 2", 0x3C0686, 2),
+ LocationData("Fungitown Badge Shop Starting Flag 3", 0x3C0688, 2),
+ LocationData("Fungitown Shop Starting Flag 1", 0x3C066A, 3),
+ LocationData("Fungitown Shop Starting Flag 2", 0x3C066C, 3),
+ LocationData("Fungitown Shop Starting Flag 3", 0x3C066E, 3),
+ LocationData("Fungitown Shop Starting Flag 4", 0x3C0670, 3),
+ LocationData("Fungitown Shop Starting Flag 5", 0x3C0672, 3),
+ LocationData("Fungitown Shop Starting Flag 6", 0x3C0674, 3),
+ LocationData("Fungitown Shop Starting Flag 7", 0x3C0676, 3),
+ LocationData("Fungitown Shop Starting Flag 8", 0x3C0678, 3),
+ LocationData("Fungitown Pants Shop Starting Flag 1", 0x3C068A, 2),
+ LocationData("Fungitown Pants Shop Starting Flag 2", 0x3C068C, 2),
+ LocationData("Fungitown Pants Shop Starting Flag 3", 0x3C068E, 2),
+]
+
+fungitownBeanstar: typing.List[LocationData] = [
+ LocationData("Fungitown Badge Shop Beanstar Complete Flag 1", 0x3C0692, 2),
+ LocationData("Fungitown Badge Shop Beanstar Complete Flag 2", 0x3C0694, 2),
+ LocationData("Fungitown Pants Shop Beanstar Complete Flag 1", 0x3C0696, 2),
+ LocationData("Fungitown Pants Shop Beanstar Complete Flag 2", 0x3C0698, 2),
+ LocationData("Fungitown Shop Beanstar Complete Flag", 0x3C067C, 3),
+]
+
+fungitownBirdo: typing.List[LocationData] = [
+ LocationData("Fungitown Shop Birdo Flag", 0x3C0680, 3),
+ LocationData("Fungitown Pants Shop Birdo Flag 1", 0x3C06A0, 2),
+ LocationData("Fungitown Pants Shop Birdo Flag 2", 0x3C06A2, 2),
+ LocationData("Fungitown Badge Shop Birdo Flag 1", 0x3C069C, 2),
+ LocationData("Fungitown Badge Shop Birdo Flag 2", 0x3C069E, 2),
+]
+
+bowsers: typing.List[LocationData] = [
+ LocationData("Bowser's Castle Entrance Block 1", 0x39E9D2, 0),
+ LocationData("Bowser's Castle Entrance Block 2", 0x39E9DA, 0),
+ LocationData("Bowser's Castle Entrance Digspot", 0x39E9E2, 0),
+ LocationData("Bowser's Castle Iggy & Morton Hallway Block 1", 0x39E9EF, 0),
+ LocationData("Bowser's Castle Iggy & Morton Hallway Block 2", 0x39E9F7, 0),
+ LocationData("Bowser's Castle Iggy & Morton Hallway Digspot", 0x39E9FF, 0),
+ LocationData("Bowser's Castle After Morton Block", 0x39EA0C, 0),
+ LocationData("Bowser's Castle Morton Room 1 Digspot", 0x39EA89, 0),
+ LocationData("Bowser's Castle Lemmy Room 1 Block", 0x39EA9C, 0),
+ LocationData("Bowser's Castle Lemmy Room 1 Digspot", 0x39EAA4, 0),
+ LocationData("Bowser's Castle Ludwig Room 1 Block", 0x39EABA, 0),
+ LocationData("Bowser's Castle Lemmy Room Mole", 0x277B1F, 1),
+]
+
+bowsersMini: typing.List[LocationData] = [
+ LocationData("Bowser's Castle Ludwig & Roy Hallway Block 1", 0x39EA1C, 0),
+ LocationData("Bowser's Castle Ludwig & Roy Hallway Block 2", 0x39EA24, 0),
+ LocationData("Bowser's Castle Roy Corridor Block 1", 0x39EA31, 0),
+ LocationData("Bowser's Castle Roy Corridor Block 2", 0x39EA39, 0),
+ LocationData("Bowser's Castle Mini Mario Sidescroller Block 1", 0x39EAD6, 0),
+ LocationData("Bowser's Castle Mini Mario Sidescroller Block 2", 0x39EADE, 0),
+ LocationData("Bowser's Castle Mini Mario Maze Block 1", 0x39EAEB, 0),
+ LocationData("Bowser's Castle Mini Mario Maze Block 2", 0x39EAF3, 0),
+ LocationData("Bowser's Castle Before Wendy Fight Block 1", 0x39EB12, 0),
+ LocationData("Bowser's Castle Before Wendy Fight Block 2", 0x39EB1A, 0),
+ LocationData("Bowser's Castle Larry Room Block", 0x39EBB6, 0),
+ LocationData("Bowser's Castle Wendy & Larry Hallway Digspot", 0x39EA46, 0),
+ LocationData("Bowser's Castle Before Fawful Fight Block 1", 0x39EA56, 0),
+ LocationData("Bowser's Castle Before Fawful Fight Block 2", 0x39EA5E, 0),
+ LocationData("Bowser's Castle Great Door Block 1", 0x39EA6B, 0),
+ LocationData("Bowser's Castle Great Door Block 2", 0x39EA73, 0),
+]
+
+jokesEntrance: typing.List[LocationData] = [
+ LocationData("Joke's End West of First Boiler Room Block 1", 0x39E6E5, 0),
+ LocationData("Joke's End West of First Boiler Room Block 2", 0x39E6ED, 0),
+ LocationData("Joke's End First Boiler Room Digspot 1", 0x39E6FA, 0),
+ LocationData("Joke's End First Boiler Room Digspot 2", 0x39E702, 0),
+ LocationData("Joke's End Second Floor West Room Block 1", 0x39E761, 0),
+ LocationData("Joke's End Second Floor West Room Block 2", 0x39E769, 0),
+ LocationData("Joke's End Second Floor West Room Block 3", 0x39E779, 0),
+ LocationData("Joke's End Second Floor West Room Block 4", 0x39E781, 0),
+ LocationData("Joke's End Mole Reward 1", 0x27788E, 1),
+ LocationData("Joke's End Mole Reward 2", 0x2778D2, 1),
+]
+
+jokesMain: typing.List[LocationData] = [
+ LocationData("Joke's End Furnace Room 1 Block 1", 0x39E70F, 0),
+ LocationData("Joke's End Furnace Room 1 Block 2", 0x39E717, 0),
+ LocationData("Joke's End Furnace Room 1 Block 3", 0x39E71F, 0),
+ LocationData("Joke's End Northeast of Boiler Room 1 Block", 0x39E732, 0),
+ LocationData("Joke's End Northeast of Boiler Room 3 Digspot", 0x39E73F, 0),
+ LocationData("Joke's End Northeast of Boiler Room 2 Block", 0x39E74C, 0),
+ LocationData("Joke's End Northeast of Boiler Room 2 Digspot", 0x39E754, 0),
+ LocationData("Joke's End Second Floor East Room Digspot", 0x39E794, 0),
+ LocationData("Joke's End Final Split up Room Digspot", 0x39E7A7, 0),
+ LocationData("Joke's End South of Bridge Room Block", 0x39E7B4, 0),
+ LocationData("Joke's End Solo Luigi Room 1 Block", 0x39E7C4, 0),
+ LocationData("Joke's End Solo Luigi Room 1 Digspot", 0x39E7CC, 0),
+ LocationData("Joke's End Solo Mario Final Room Block 1", 0x39E7D9, 0),
+ LocationData("Joke's End Solo Mario Final Room Block 2", 0x39E7E1, 0),
+ LocationData("Joke's End Solo Mario Final Room Block 3", 0x39E7E9, 0),
+ LocationData("Joke's End Solo Luigi Room 2 Digspot", 0x39E7FC, 0),
+ LocationData("Joke's End Solo Mario Room 1 Digspot", 0x39E809, 0),
+ LocationData("Joke's End Solo Mario Room 2 Block 1", 0x39E819, 0),
+ LocationData("Joke's End Solo Mario Room 2 Block 2", 0x39E821, 0),
+ LocationData("Joke's End Solo Mario Room 2 Block 3", 0x39E829, 0),
+ LocationData("Joke's End Second Boiler Room Digspot 1", 0x39E84F, 0),
+ LocationData("Joke's End Second Boiler Room Digspot 2", 0x39E857, 0),
+ LocationData("Joke's End North of Second Boiler Room Block 1", 0x39E864, 0),
+ LocationData("Joke's End North of Second Boiler Room Block 2", 0x39E86C, 0),
+ LocationData("Joke's End Before Jojora Room Block 1", 0x39E927, 0),
+ LocationData("Joke's End Before Jojora Room Block 2", 0x39E92F, 0),
+ LocationData("Joke's End Before Jojora Room Digspot", 0x39E937, 0),
+ LocationData("Joke's End Jojora Room Digspot", 0x39E944, 0),
+]
+
+postJokes: typing.List[LocationData] = [
+ LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 2 (Post-Birdo)", 0x39E5A0, 0),
+ LocationData("Teehee Valley Before Popple Digspot 1", 0x39E55B, 0),
+ LocationData("Teehee Valley Before Popple Digspot 2", 0x39E563, 0),
+ LocationData("Teehee Valley Before Popple Digspot 3", 0x39E56B, 0),
+ LocationData("Teehee Valley Before Popple Digspot 4", 0x39E573, 0),
+]
+
+theater: typing.List[LocationData] = [
+ LocationData("Yoshi Theater Blue Yoshi", 0x241155, 1),
+ LocationData("Yoshi Theater Red Yoshi", 0x240EBE, 1),
+ LocationData("Yoshi Theater Green Yoshi", 0x241AFA, 1),
+ LocationData("Yoshi Theater Yellow Yoshi", 0x241C3C, 1),
+ LocationData("Yoshi Theater Purple Yoshi", 0x241297, 1),
+ LocationData("Yoshi Theater Orange Yoshi", 0x241000, 1),
+ LocationData("Yoshi Theater Azure Yoshi", 0x241D7E, 1),
+ LocationData("Beanstar Piece Yoshi Theater", 0x1E9442, 2),
+]
+
+oasis: typing.List[LocationData] = [
+ LocationData("Oho Oasis West Digspot", 0x39DF9F, 0),
+ LocationData("Oho Oasis Fire Palace Block", 0x39DFBE, 0),
+ LocationData("Oho Ocean Spike Room Digspot 1", 0x39E08A, 0),
+ LocationData("Oho Ocean Spike Room Digspot 2", 0x39E092, 0),
+ LocationData("Oho Oasis Firebrand", 0x1E9408, 2),
+ LocationData("Oho Oasis Thunderhand", 0x1E9409, 2),
+]
+
+nonBlock = [
+ (0x434B, 0x1, 0x243844), # Farm Mole 1
+ (0x434B, 0x1, 0x24387D), # Farm Mole 2
+ (0x4373, 0x8, 0x2779C8), # Simulblock Mole
+ (0x42F9, 0x4, 0x1E9403), # Hammers
+ (0x434B, 0x10, 0x1E9435), # Solo Mario Mole 1
+ (0x434B, 0x20, 0x1E9436), # Solo Mario Mole 2
+ (0x4359, 0x20, 0x1E9404), # Super Hammers
+ (0x4359, 0x40, 0x1E9405), # Ultra Hammers
+ (0x42F9, 0x2, 0x1E9430), # Rose
+ (0x434B, 0x4, 0x242888), # Solo Luigi Cave Mole
+ (0x4373, 0x20, 0x277AB2), # Hoohoo Village Turtle Mole
+ (0x432D, 0x20, 0x1E9431), # Piranha Bean
+ (0x434E, 0x2, 0x1E9411), # Secret Scroll 1
+ (0x434E, 0x4, 0x1E9412), # Secret Scroll 2
+ (0x4375, 0x8, 0x260637), # Membership Card
+ (0x4373, 0x10, 0x277A45), # Teehee Valley Mole
+ (0x434D, 0x8, 0x1E9444), # Harhall's Pants
+ (0x432E, 0x10, 0x1E9441), # Harhall Beanstar Piece
+ (0x434B, 0x8, 0x1E9434), # Outskirts Boo Statue Mole
+ (0x42FE, 0x2, 0x1E943E), # Red Goblet
+ (0x42FE, 0x4, 0x24E628), # Green Goblet
+ (0x4301, 0x10, 0x250621), # Red Chuckola Fruit
+ (0x42FE, 0x80, 0x24ED74), # Purple Chuckola Fruit
+ (0x4302, 0x4, 0x24FF18), # White Chuckola Fruit
+ (0x42FF, 0x8, 0x251347), # Beanlet 1
+ (0x42FF, 0x20, 0x2513FB), # Beanlet 2
+ (0x42FF, 0x10, 0x2513A1), # Beanlet 3
+ (0x42FF, 0x4, 0x251988), # Beanlet 4
+ (0x42FF, 0x2, 0x25192E), # Beanlet 5
+ (0x42FF, 0x1, 0x2515EB), # Beanlet Reward
+ (0x4371, 0x40, 0x253515), # Espresso 1
+ (0x4371, 0x80, 0x253776), # Espresso 2
+ (0x4372, 0x1, 0x253C70), # Espresso 3
+ (0x4372, 0x2, 0x254324), # Espresso 4
+ (0x4372, 0x4, 0x254718), # Espresso 5
+ (0x4372, 0x8, 0x254A34), # Espresso 6
+ (0x4372, 0x10, 0x254E24), # Espresso 7
+ (0x472F, 0x1, 0x252D07), # Woohoo Blend
+ (0x472F, 0x2, 0x252D28), # Hoohoo Blend
+ (0x472F, 0x4, 0x252D49), # Chuckle Blend
+ (0x472F, 0x8, 0x252D6A), # Teehee Blend
+ (0x472F, 0x10, 0x252D8B), # Hoolumbian
+ (0x472F, 0x20, 0x252DAC), # Chuckoccino
+ (0x472F, 0x40, 0x252DCD), # Teeheespresso
+ (0x430B, 0x10, 0x1E9433), # Extra Dress
+ (0x430B, 0x10, 0x1E9432), # Fake Beanstar
+ (0x430F, 0x1, 0x1E9440), # Popple Beanstar Piece
+ (0x467E, 0xFF, 0x261658), # Winkle Card
+ (0x4300, 0x40, 0x2578E7), # Brooch
+ (0x4375, 0x2, 0x2753EA), # Surf Minigame
+ (0x4373, 0x1, 0x277956), # North Whirlpool Mole
+ (0x4346, 0x40, 0x235A5B), # Green Pearl Bean
+ (0x4346, 0x80, 0x235C1C), # Red Pearl Bean
+ (0x4340, 0x20, 0x1E9443), # Hermie Beanstar Piece
+ (0x434A, 0x40, 0x1E9437), # Spangle
+ (0x434A, 0x80, 0x236E73), # Spangle Reward
+ (0x4373, 0x40, 0x277B1F), # Bowser's Castle Mole
+ (0x4372, 0x80, 0x27788E), # Jokes end Mole 1
+ (0x4372, 0x80, 0x2778D2), # Jokes end Mole 2
+ (0x434C, 0x80, 0x241000), # Orange Neon Egg
+ (0x434D, 0x1, 0x240EBE), # Red Neon Egg
+ (0x434C, 0x40, 0x241155), # Blue Neon Egg
+ (0x434D, 0x2, 0x241297), # Purple Neon Egg
+ (0x434C, 0x8, 0x241AFA), # Green Neon Egg
+ (0x434C, 0x10, 0x241D7E), # Azure Neon Egg
+ (0x434C, 0x20, 0x241C3C), # Yellow Neon Egg
+ (0x4406, 0x8, 0x1E9442), # Theater Beanstar Piece
+ (0x4345, 0x8, 0x1E9408), # Firebrand
+ (0x4345, 0x4, 0x1E9409), # Thunder Hand
+ (0x42FF, 0x80, 0x251071), # Beanstone Reward
+ (0x42F9, 0x2, 0xDA0000), # Dragohoho
+ (0x433D, 0x1, 0xDA0001), # Chuckolator
+ (0x43FC, 0x80, 0xDA0002), # Popple 2
+ (0x433D, 0x2, 0xDA0003), # Mom Piranha
+ (0x4342, 0x10, 0xDA0004), # Fungitowm
+ (0x433D, 0x8, 0xDA0005), # Beanstar
+ (0x430F, 0x40, 0xDA0006), # Jojora
+ (0x433D, 0x10, 0xDA0007), # Birdo
+]
+
+roomException = {
+ 0x1E9437: [0xFE, 0xFF, 0x100],
+ 0x24ED74: [0x94, 0x95, 0x96, 0x99],
+ 0x250621: [0x94, 0x95, 0x96, 0x99],
+ 0x24FF18: [0x94, 0x95, 0x96, 0x99],
+ 0x260637: [0x135],
+ 0x1E9403: [0x4D],
+ 0xDA0001: [0x79, 0x192, 0x193],
+ 0x2578E7: [0x79, 0x192, 0x193],
+}
+
+beanstones = {
+ 0x229345: 0x39DC72, # Bean fruit 1 - 6
+ 0x22954D: 0x39DCB4,
+ 0x228A17: 0x39DBD1,
+ 0x22913A: 0x39DC10,
+ 0x22890E: 0x39DBA4,
+ 0x228775: 0x39DB7F,
+ 0x251288: 0x39D73E, # Beanstone 1 - 10
+ 0x2512E1: 0x39D746,
+ 0x25122F: 0x39D74E,
+ 0x25117D: 0x39D756,
+ 0x2511D6: 0x39D75E,
+ 0x25187B: 0x39D76B,
+ 0x25170B: 0x39D773,
+ 0x251767: 0x39D77B,
+ 0x2517C3: 0x39D783,
+ 0x25181F: 0x39D78B,
+}
+
+roomCount = {
+ 0x15: 2,
+ 0x18: 4,
+ 0x19: 3,
+ 0x1A: 3,
+ 0x1B: 2,
+ 0x1E: 1,
+ 0x23: 3,
+ 0x27: 1,
+ 0x28: 5,
+ 0x29: 5,
+ 0x2E: 4,
+ 0x34: 4,
+ 0x37: 1,
+ 0x39: 5,
+ 0x44: 1,
+ 0x45: 4,
+ 0x46: 3,
+ 0x47: 4,
+ 0x48: 3,
+ 0x4A: 2,
+ 0x4B: 2,
+ 0x4C: 3,
+ 0x4D: 2,
+ 0x51: 2,
+ 0x53: 5,
+ 0x54: 5,
+ 0x55: 5,
+ 0x56: 2,
+ 0x57: 1,
+ 0x58: 2,
+ 0x59: 2,
+ 0x5A: 3,
+ 0x63: 2,
+ 0x68: 2,
+ 0x69: 2,
+ 0x6B: 3,
+ 0x6C: 5,
+ 0x6D: 1,
+ 0x70: 3,
+ 0x74: 2,
+ 0x75: 2,
+ 0x76: 1,
+ 0x77: 4,
+ 0x78: 4,
+ 0x79: 4,
+ 0x7A: 1,
+ 0x7B: 1,
+ 0x7C: 5,
+ 0x7D: 7,
+ 0x7E: 3,
+ 0x7F: 3,
+ 0x80: 4,
+ 0x81: 3,
+ 0x82: 1,
+ 0x83: 4,
+ 0x84: 1,
+ 0x86: 5,
+ 0x87: 1,
+ 0x89: 1,
+ 0x8A: 3,
+ 0x8B: 2,
+ 0x8C: 2,
+ 0x8D: 2,
+ 0x8E: 5,
+ 0x90: 3,
+ 0x93: 5,
+ 0x94: 1,
+ 0x96: 1,
+ 0x97: 4,
+ 0x98: 3,
+ 0x99: 1,
+ 0x9A: 1,
+ 0x9B: 2,
+ 0x9C: 7,
+ 0x9D: 1,
+ 0x9E: 1,
+ 0x9F: 1,
+ 0xA1: 4,
+ 0xA2: 3,
+ 0xA9: 1,
+ 0xB0: 1,
+ 0xBA: 3,
+ 0xBC: 2,
+ 0xBE: 5,
+ 0xC3: 1,
+ 0xC6: 1,
+ 0xC7: 1,
+ 0xCA: 2,
+ 0xCD: 6,
+ 0xCE: 6,
+ 0xCF: 1,
+ 0xDB: 3,
+ 0xDC: 2,
+ 0xDD: 1,
+ 0xDF: 2,
+ 0xE0: 6,
+ 0xE1: 1,
+ 0xE2: 1,
+ 0xE3: 1,
+ 0xE4: 5,
+ 0xE5: 1,
+ 0xE6: 2,
+ 0xE7: 1,
+ 0xE8: 2,
+ 0xE9: 4,
+ 0xEC: 3,
+ 0xEE: 1,
+ 0xF1: 3,
+ 0xF2: 1,
+ 0xF3: 1,
+ 0xF4: 5,
+ 0xF5: 5,
+ 0xF6: 5,
+ 0xF7: 1,
+ 0xFC: 1,
+ 0xFE: 1,
+ 0x102: 1,
+ 0x103: 2,
+ 0x104: 1,
+ 0x105: 2,
+ 0x107: 2,
+ 0x109: 1,
+ 0x10A: 1,
+ 0x10C: 1,
+ 0x10D: 3,
+ 0x10E: 1,
+ 0x10F: 2,
+ 0x110: 3,
+ 0x111: 1,
+ 0x112: 2,
+ 0x114: 1,
+ 0x115: 1,
+ 0x116: 1,
+ 0x117: 1,
+ 0x118: 2,
+ 0x11E: 3,
+ 0x11F: 3,
+ 0x121: 4,
+ 0x122: 6,
+ 0x123: 1,
+ 0x126: 2,
+ 0x128: 1,
+ 0x12A: 1,
+ 0x12B: 1,
+ 0x12E: 4,
+ 0x139: 2,
+ 0x13B: 1,
+ 0x13E: 1,
+ 0x147: 1,
+ 0x14E: 1,
+ 0x14F: 1,
+ 0x153: 2,
+ 0x154: 2,
+ 0x155: 3,
+ 0x158: 1,
+ 0x159: 1,
+ 0x15A: 2,
+ 0x15B: 5,
+ 0x15E: 1,
+ 0x161: 1,
+ 0x162: 1,
+ 0x164: 2,
+ 0x165: 3,
+ 0x168: 1,
+ 0x169: 1,
+ 0x16B: 3,
+ 0x16C: 1,
+ 0x171: 2,
+ 0x172: 2,
+ 0x181: 1,
+ 0x186: 3,
+ 0x187: 1,
+ 0x18D: 2,
+ 0x18E: 3,
+ 0x18F: 3,
+ 0x190: 1,
+ 0x191: 2,
+ 0x192: 2,
+ 0x193: 2,
+ 0x194: 3,
+ 0x195: 4,
+ 0x196: 3,
+ 0x197: 3,
+ 0x198: 1,
+ 0x19A: 2,
+ 0x19B: 2,
+ 0x19C: 1,
+ 0x19E: 2,
+ 0x19F: 2,
+ 0x1A3: 1,
+ 0x1A6: 2,
+ 0x1AA: 1,
+ 0x1B0: 2,
+ 0x1B1: 2,
+ 0x1B8: 2,
+ 0x1CA: 2,
+ 0x1D1: 2,
+ 0x1D2: 3,
+ 0x1D4: 1,
+ 0x1EB: 3,
+ 0x1F6: 1,
+ 0x1F7: 1,
+}
+
+shop = {
+ 0x3C05F0: [
+ 0x3C05F0,
+ 0x3C05F2,
+ 0x3C05F4,
+ 0x3C05F8,
+ 0x3C05FC,
+ 0x3C05FE,
+ 0x3C0600,
+ 0x3C0602,
+ 0x3C0606,
+ 0x3C0608,
+ 0x3C060C,
+ 0x3C060E,
+ 0x3C0610,
+ 0x3C0614,
+ ],
+ 0x3C066A: [0x3C066A, 0x3C066C, 0x3C066E, 0x3C0670, 0x3C0672, 0x3C0674, 0x3C0676, 0x3C0678, 0x3C067C, 0x3C0680],
+}
+
+badge = {
+ 0x3C0618: [
+ 0x3C0618,
+ 0x3C061A,
+ 0x3C0624,
+ 0x3C0626,
+ 0x3C0628,
+ 0x3C0632,
+ 0x3C0634,
+ 0x3C0636,
+ 0x3C0640,
+ 0x3C0642,
+ 0x3C0644,
+ 0x3C064E,
+ 0x3C0650,
+ 0x3C0652,
+ 0x3C065C,
+ 0x3C065E,
+ 0x3C0660,
+ ],
+ 0x3C0684: [0x3C0684, 0x3C0686, 0x3C0688, 0x3C0692, 0x3C0694, 0x3C069C, 0x3C069E],
+}
+
+pants = {
+ 0x3C0618: [
+ 0x3C061C,
+ 0x3C061E,
+ 0x3C0620,
+ 0x3C062A,
+ 0x3C062C,
+ 0x3C062E,
+ 0x3C0638,
+ 0x3C063A,
+ 0x3C063C,
+ 0x3C0646,
+ 0x3C0648,
+ 0x3C064A,
+ 0x3C0654,
+ 0x3C0656,
+ 0x3C0658,
+ 0x3C0662,
+ 0x3C0664,
+ 0x3C0666,
+ ],
+ 0x3C0684: [0x3C068A, 0x3C068C, 0x3C068E, 0x3C0696, 0x3C0698, 0x3C06A0, 0x3C06A2],
+}
+
+all_locations: typing.List[LocationData] = (
+ mainArea
+ + booStatue
+ + chucklehuck
+ + castleTown
+ + startingFlag
+ + chuckolatorFlag
+ + piranhaFlag
+ + kidnappedFlag
+ + beanstarFlag
+ + birdoFlag
+ + winkle
+ + sewers
+ + hooniversity
+ + surfable
+ + airport
+ + gwarharEntrance
+ + teeheeValley
+ + fungitown
+ + fungitownBeanstar
+ + fungitownBirdo
+ + bowsers
+ + jokesEntrance
+ + jokesMain
+ + postJokes
+ + theater
+ + oasis
+ + gwarharMain
+ + bowsersMini
+ + baseUltraRocks
+ + coins
+)
+
+location_table: typing.Dict[str, int] = {locData.name: locData.id for locData in all_locations}
diff --git a/worlds/mlss/Names/LocationName.py b/worlds/mlss/Names/LocationName.py
new file mode 100644
index 000000000000..7cbc2e4f31f8
--- /dev/null
+++ b/worlds/mlss/Names/LocationName.py
@@ -0,0 +1,559 @@
+class LocationName:
+ StardustFields1Block1 = "Stardust Fields Room 1 Block 1"
+ StardustFields1Block2 = "Stardust Fields Room 1 Block 2"
+ StardustFields2Block = "Stardust Fields Room 2 Block"
+ StardustFields3Block = "Stardust Fields Room 3 Block"
+ StardustFields4Block1 = "Stardust Fields Room 4 Block 1"
+ StardustFields4Block2 = "Stardust Fields Room 4 Block 2"
+ StardustFields4Block3 = "Stardust Fields Room 4 Block 3"
+ StardustFields5Block = "Stardust Fields Room 5 Block"
+ HoohooVillageHammerHouseBlock = "Hoohoo Village Hammer House Block"
+ BeanbeanCastleTownLeftSideHouseBlock1 = "Beanbean Castle Town Left Side House Block 1"
+ BeanbeanCastleTownLeftSideHouseBlock2 = "Beanbean Castle Town Left Side House Block 2"
+ BeanbeanCastleTownLeftSideHouseBlock3 = "Beanbean Castle Town Left Side House Block 3"
+ BeanbeanCastleTownLeftSideHouseBlock4 = "Beanbean Castle Town Left Side House Block 4"
+ BeanbeanCastleTownRightSideHouseBlock1 = "Beanbean Castle Town Right Side House Block 1"
+ BeanbeanCastleTownRightSideHouseBlock2 = "Beanbean Castle Town Right Side House Block 2"
+ BeanbeanCastleTownRightSideHouseBlock3 = "Beanbean Castle Town Right Side House Block 3"
+ BeanbeanCastleTownRightSideHouseBlock4 = "Beanbean Castle Town Right Side House Block 4"
+ BeanbeanCastleTownMiniMarioBlock1 = "Beanbean Castle Town Mini Mario Block 1"
+ BeanbeanCastleTownMiniMarioBlock2 = "Beanbean Castle Town Mini Mario Block 2"
+ BeanbeanCastleTownMiniMarioBlock3 = "Beanbean Castle Town Mini Mario Block 3"
+ BeanbeanCastleTownMiniMarioBlock4 = "Beanbean Castle Town Mini Mario Block 4"
+ BeanbeanCastleTownMiniMarioBlock5 = "Beanbean Castle Town Mini Mario Block 5"
+ HoohooMountainSummitDigspot = "Hoohoo Mountain Summit Digspot"
+ HoohooMountainBelowSummitDigspot = "Hoohoo Mountain Below Summit Digspot"
+ HoohooMountainBelowSummitBlock1 = "Hoohoo Mountain Below Summit Block 1"
+ HoohooMountainBelowSummitBlock2 = "Hoohoo Mountain Below Summit Block 2"
+ HoohooMountainBelowSummitBlock3 = "Hoohoo Mountain Below Summit Block 3"
+ HoohooMountainAfterHoohoorosBlock1 = "Hoohoo Mountain After Hoohooros Block 1"
+ HoohooMountainAfterHoohoorosDigspot = "Hoohoo Mountain After Hoohooros Digspot"
+ HoohooMountainAfterHoohoorosBlock2 = "Hoohoo Mountain After Hoohooros Block 2"
+ HoohooMountainHoohoorosRoomBlock1 = "Hoohoo Mountain Hoohooros Room Block 1"
+ HoohooMountainHoohoorosRoomBlock2 = "Hoohoo Mountain Hoohooros Room Block 2"
+ HoohooMountainHoohoorosRoomDigspot1 = "Hoohoo Mountain Hoohooros Room Digspot 1"
+ HoohooMountainHoohoorosRoomDigspot2 = "Hoohoo Mountain Hoohooros Room Digspot 2"
+ HoohooMountainBeforeHoohoorosBlock = "Hoohoo Mountain Before Hoohooros Block"
+ HoohooMountainBeforeHoohoorosDigspot = "Hoohoo Mountain Before Hoohooros Digspot"
+ HoohooMountainFountainRoomBlock1 = "Hoohoo Mountain Fountain Room Block 1"
+ HoohooMountainFountainRoomBlock2 = "Hoohoo Mountain Fountain Room Block 2"
+ HoohooMountainRoom2Digspot1 = "Hoohoo Mountain Room 2 Digspot 1"
+ HoohooMountainRoom2Digspot2 = "Hoohoo Mountain Room 2 Digspot 2"
+ HoohooMountainRoom1Block1 = "Hoohoo Mountain Room 1 Block 1"
+ HoohooMountainRoom1Block2 = "Hoohoo Mountain Room 1 Block 2"
+ HoohooMountainRoom1Block3 = "Hoohoo Mountain Room 1 Block 3"
+ HoohooMountainBaseRoom1Block = "Hoohoo Mountain Base Room 1 Block"
+ HoohooMountainBaseRoom1Digspot = "Hoohoo Mountain Base Room 1 Digspot"
+ HoohooVillageRightSideBlock = "Hoohoo Village Right Side Block"
+ HoohooVillageRightSideDigspot = "Hoohoo Village Right Side Digspot"
+ HoohooVillageBridgeRoomBlock1 = "Hoohoo Village Bridge Room Block 1"
+ HoohooVillageBridgeRoomBlock2 = "Hoohoo Village Bridge Room Block 2"
+ HoohooVillageBridgeRoomBlock3 = "Hoohoo Village Bridge Room Block 3"
+ HoohooMountainBaseBridgeRoomBlock1 = "Hoohoo Mountain Base Bridge Room Block 1"
+ HoohooMountainBaseBridgeRoomBlock2 = "Hoohoo Mountain Base Bridge Room Block 2"
+ HoohooMountainBaseBridgeRoomBlock3 = "Hoohoo Mountain Base Bridge Room Block 3"
+ HoohooMountainBaseBridgeRoomBlock4 = "Hoohoo Mountain Base Bridge Room Block 4"
+ HoohooMountainBaseBridgeRoomDigspot = "Hoohoo Mountain Base Bridge Room Digspot"
+ HoohooMountainBaseBoostatueRoomBlock1 = "Hoohoo Mountain Base Boostatue Room Block 1"
+ HoohooMountainBaseBoostatueRoomBlock2 = "Hoohoo Mountain Base Boostatue Room Block 2"
+ HoohooMountainBaseBoostatueRoomDigspot1 = "Hoohoo Mountain Base Boostatue Room Digspot 1"
+ HoohooMountainBaseBoostatueRoomDigspot2 = "Hoohoo Mountain Base Boostatue Room Digspot 2"
+ HoohooMountainBaseBoostatueRoomDigspot3 = "Hoohoo Mountain Base Boostatue Room Digspot 3"
+ BeanbeanOutskirtsBooStatueMole = "Beanbean Outskirts Boo Statue Mole"
+ HoohooMountainBaseGrassyAreaBlock1 = "Hoohoo Mountain Base Grassy Area Block 1"
+ HoohooMountainBaseGrassyAreaBlock2 = "Hoohoo Mountain Base Grassy Area Block 2"
+ HoohooMountainBaseGuffawhaRuinsEntranceDigspot = "Hoohoo Mountain Base Guffawha Ruins Entrance Digspot"
+ HoohooMountainBaseTeeheeValleyEntranceDigspot = "Hoohoo Mountain Base Teehee Valley Entrance Digspot"
+ HoohooMountainBaseTeeheeValleyEntranceBlock = "Hoohoo Mountain Base Teehee Valley Entrance Block"
+ HoohooMountainBaseAfterMinecartMinigameBlock1 = "Hoohoo Mountain Base After Minecart Minigame Block 1"
+ HoohooMountainBaseAfterMinecartMinigameBlock2 = "Hoohoo Mountain Base After Minecart Minigame Block 2"
+ HoohooMountainBasePastUltraHammerRocksBlock1 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 1"
+ HoohooMountainBasePastUltraHammerRocksBlock2 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 2"
+ HoohooMountainBasePastUltraHammerRocksBlock3 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 3"
+ CaveConnectingStardustFieldsAndHoohooVillageBlock1 = "Cave Connecting Stardust Fields and Hoohoo Village Block 1"
+ CaveConnectingStardustFieldsAndHoohooVillageBlock2 = "Cave Connecting Stardust Fields and Hoohoo Village Block 2"
+ HoohooVillageSouthCaveBlock = "Hoohoo Village South Cave Block"
+ HoohooVillageSuperHammerCaveDigspot = "Hoohoo Village Super Hammer Cave Digspot"
+ HoohooVillageSuperHammerCaveBlock = "Hoohoo Village Super Hammer Cave Block"
+ HoohooVillageNorthCaveRoom1Block = "Hoohoo Village North Cave Room 1 Block"
+ HoohooVillageNorthCaveRoom2Block = "Hoohoo Village North Cave Room 2 Block"
+ HoohooVillageNorthCaveRoom2Digspot = "Hoohoo Village North Cave Room 2 Digspot"
+ HoohooMountainBaseMinecartCaveDigspot = "Hoohoo Mountain Base Minecart Cave Digspot"
+ BeanbeanOutskirtsFarmRoomDigspot1 = "Beanbean Outskirts Farm Room Digspot 1"
+ BeanbeanOutskirtsFarmRoomDigspot2 = "Beanbean Outskirts Farm Room Digspot 2"
+ BeanbeanOutskirtsFarmRoomDigspot3 = "Beanbean Outskirts Farm Room Digspot 3"
+ BeanbeanOutskirtsNWBlock = "Beanbean Outskirts NW Block"
+ BeanbeanOutskirtsNWDigspot = "Beanbean Outskirts NW Digspot"
+ BeanbeanOutskirtsWDigspot1 = "Beanbean Outskirts W Digspot 1"
+ BeanbeanOutskirtsWDigspot2 = "Beanbean Outskirts W"
+ BeanbeanOutskirtsNRoom1Digspot = "Beanbean Outskirts N Room 1 Digspot"
+ BeanbeanOutskirtsNRoom2Digspot = "Beanbean Outskirts N Room 2 Digspot"
+ BeanbeanOutskirtsSRoom1Digspot1 = "Beanbean Outskirts S Room 1 Digspot 1"
+ BeanbeanOutskirtsSRoom1Block = "Beanbean Outskirts S Room 1 Block"
+ BeanbeanOutskirtsSRoom1Digspot2 = "Beanbean Outskirts S Room 1 Digspot 2"
+ BeanbeanOutskirtsSRoom2Block1 = "Beanbean Outskirts S Room 2 Block 1"
+ BeanbeanOutskirtsSRoom2Digspot1 = "Beanbean Outskirts S Room 2 Digspot 1"
+ BeanbeanOutskirtsSRoom2Digspot2 = "Beanbean Outskirts S Room 2 Digspot 2"
+ BeanbeanOutskirtsSRoom2Block2 = "Beanbean Outskirts S Room 2 Block 2"
+ BeanbeanOutskirtsSRoom2Digspot3 = "Beanbean Outskirts S Room 2 Digspot 3"
+ BeanbeanOutskirtsNEDigspot1 = "Beanbean Outskirts NE Digspot 1"
+ BeanbeanOutskirtsNEDigspot2 = "Beanbean Outskirts NE Digspot 2"
+ BeanbeanOutskirtsEDigspot1 = "Beanbean Outskirts E Digspot 1"
+ BeanbeanOutskirtsEDigspot2 = "Beanbean Outskirts E Digspot 2"
+ BeanbeanOutskirtsEDigspot3 = "Beanbean Outskirts E Digspot 3"
+ BeanbeanOutskirtsSEDigspot1 = "Beanbean Outskirts SE Digspot 1"
+ BeanbeanOutskirtsSEDigspot2 = "Beanbean Outskirts SE Digspot 2"
+ BeanbeanOutskirtsSEDigspot3 = "Beanbean Outskirts SE Digspot 3"
+ BeanbeanOutskirtsNorthBeachDigspot1 = "Beanbean Outskirts North Beach Digspot 1"
+ BeanbeanOutskirtsNorthBeachDigspot2 = "Beanbean Outskirts North Beach Digspot 2"
+ BeanbeanOutskirtsNorthBeachDigspot3 = "Beanbean Outskirts North Beach Digspot 3"
+ BeanbeanOutskirtsSouthBeachDigspot = "Beanbean Outskirts South Beach Digspot"
+ BeanbeanOutskirtsSurfBeachDigspot1 = "Beanbean Outskirts Surf Beach Digspot 1"
+ BeanbeanOutskirtsSurfBeachBlock = "Beanbean Outskirts Surf Beach Block"
+ BeanbeanOutskirtsSurfBeachDigspot2 = "Beanbean Outskirts Surf Beach Digspot 2"
+ BeanbeanOutskirtsSurfBeachDigspot3 = "Beanbean Outskirts Surf Beach Digspot 3"
+ ChateauRoom1Digspot = "Chateau Room 1 Digspot"
+ ChateauPoppleFightRoomBlock1 = "Chateau Popple Fight Room Block 1"
+ ChateauPoppleFightRoomBlock2 = "Chateau Popple Fight Room Block 2"
+ ChateauPoppleFightRoomDigspot = "Chateau Popple Fight Room Digspot"
+ ChateauBarrelRoomDigspot = "Chateau Barrel Room Digspot"
+ ChateauGobletRoomDigspot = "Chateau Goblet Room Digspot"
+ ChucklehuckWoodsCaveRoom1Block1 = "Chucklehuck Woods Cave Room 1 Block 1"
+ ChucklehuckWoodsCaveRoom1Block2 = "Chucklehuck Woods Cave Room 1 Block 2"
+ ChucklehuckWoodsCaveRoom2Block = "Chucklehuck Woods Cave Room 2 Block"
+ ChucklehuckWoodsCaveRoom3Block = "Chucklehuck Woods Cave Room 3 Block"
+ ChucklehuckWoodsRoom2Block = "Chucklehuck Woods Room 2 Block"
+ ChucklehuckWoodsRoom2Digspot = "Chucklehuck Woods Room 2 Digspot"
+ ChucklehuckWoodsPipeRoomBlock1 = "Chucklehuck Woods Pipe Room Block 1"
+ ChucklehuckWoodsPipeRoomBlock2 = "Chucklehuck Woods Pipe Room Block 2"
+ ChucklehuckWoodsPipeRoomDigspot1 = "Chucklehuck Woods Pipe Room Digspot 1"
+ ChucklehuckWoodsPipeRoomDigspot2 = "Chucklehuck Woods Pipe Room Digspot 2"
+ ChucklehuckWoodsRoom4Block1 = "Chucklehuck Woods Room 4 Block 1"
+ ChucklehuckWoodsRoom4Block2 = "Chucklehuck Woods Room 4 Block 2"
+ ChucklehuckWoodsRoom4Block3 = "Chucklehuck Woods Room 4 Block 3"
+ ChucklehuckWoodsRoom7Block1 = "Chucklehuck Woods Room 7 Block 1"
+ ChucklehuckWoodsRoom7Block2 = "Chucklehuck Woods Room 7 Block 2"
+ ChucklehuckWoodsRoom7Digspot1 = "Chucklehuck Woods Room 7 Digspot 1"
+ ChucklehuckWoodsRoom7Digspot2 = "Chucklehuck Woods Room 7 Digspot 2"
+ ChucklehuckWoodsRoom8Digspot = "Chucklehuck Woods Room 8 Digspot"
+ ChucklehuckWoodsEastOfChucklerootDigspot = "Chucklehuck Woods East of Chuckleroot Digspot"
+ ChucklehuckWoodsNortheastOfChucklerootDigspot1 = "Chucklehuck Woods Northeast of Chuckleroot Digspot 1"
+ ChucklehuckWoodsNortheastOfChucklerootDigspot2 = "Chucklehuck Woods Northeast of Chuckleroot Digspot 2"
+ ChucklehuckWoodsNortheastOfChucklerootDigspot3 = "Chucklehuck Woods Northeast of Chuckleroot Digspot 3"
+ ChucklehuckWoodsNortheastOfChucklerootDigspot4 = "Chucklehuck Woods Northeast of Chuckleroot Digspot 4"
+ ChucklehuckWoodsWhiteFruitRoomDigspot1 = "Chucklehuck Woods White Fruit Room Digspot 1"
+ ChucklehuckWoodsWhiteFruitRoomDigspot2 = "Chucklehuck Woods White Fruit Room Digspot 2"
+ ChucklehuckWoodsWhiteFruitRoomDigspot3 = "Chucklehuck Woods White Fruit Room Digspot 3"
+ ChucklehuckWoodsWestOfChucklerootBlock = "Chucklehuck Woods West of Chuckleroot Block"
+ ChucklehuckWoodsSouthwestOfChucklerootBlock = "Chucklehuck Woods Southwest of Chuckleroot Block"
+ ChucklehuckWoodsWigglerRoomDigspot1 = "Chucklehuck Woods Wiggler Room Digspot 1"
+ ChucklehuckWoodsWigglerRoomDigspot2 = "Chucklehuck Woods Wiggler Room Digspot 2"
+ ChucklehuckWoodsAfterChucklerootBlock1 = "Chucklehuck Woods After Chuckleroot Block 1"
+ ChucklehuckWoodsAfterChucklerootBlock2 = "Chucklehuck Woods After Chuckleroot Block 2"
+ ChucklehuckWoodsAfterChucklerootBlock3 = "Chucklehuck Woods After Chuckleroot Block 3"
+ ChucklehuckWoodsAfterChucklerootBlock4 = "Chucklehuck Woods After Chuckleroot Block 4"
+ ChucklehuckWoodsAfterChucklerootBlock5 = "Chucklehuck Woods After Chuckleroot Block 5"
+ ChucklehuckWoodsAfterChucklerootBlock6 = "Chucklehuck Woods After Chuckleroot Block 6"
+ WinkleAreaBeanstarRoomBlock = "Winkle Area Beanstar Room Block"
+ WinkleAreaDigspot = "Winkle Area Digspot"
+ WinkleAreaOutsideColosseumBlock = "Winkle Area Outside Colosseum Block"
+ ChucklehuckWoodsKoopaRoomBlock1 = "Chucklehuck Woods Koopa Room Block 1"
+ ChucklehuckWoodsKoopaRoomBlock2 = "Chucklehuck Woods Koopa Room Block 2"
+ ChucklehuckWoodsKoopaRoomDigspot = "Chucklehuck Woods Koopa Room Digspot"
+ ChucklehuckWoodsWinkleCaveBlock1 = "Chucklehuck Woods Winkle Cave Block 1"
+ ChucklehuckWoodsWinkleCaveBlock2 = "Chucklehuck Woods Winkle Cave Block 2"
+ OhoOasisWestDigspot = "Oho Oasis West Digspot"
+ OhoOasisFirePalaceBlock = "Oho Oasis Fire Palace Block"
+ SewersRoom3Block1 = "Sewers Room 3 Block 1"
+ SewersRoom3Block2 = "Sewers Room 3 Block 2"
+ SewersRoom3Block3 = "Sewers Room 3 Block 3"
+ SewersRoom5Block1 = "Sewers Room 5 Block 1"
+ SewersRoom5Block2 = "Sewers Room 5 Block 2"
+ SewersPrisonRoomBlock1 = "Sewers Prison Room Block 1"
+ SewersPrisonRoomBlock2 = "Sewers Prison Room Block 2"
+ SewersPrisonRoomBlock3 = "Sewers Prison Room Block 3"
+ SewersPrisonRoomBlock4 = "Sewers Prison Room Block 4"
+ OhoOceanFirePuzzleRoomDigspot = "Oho Ocean Fire Puzzle Room Digspot"
+ OhoOceanSouthRoom1Block = "Oho Ocean South Room 1 Block"
+ OhoOceanSouthRoom2Digspot = "Oho Ocean South Room 2 Digspot"
+ OhoOceanSpikeRoomDigspot1 = "Oho Ocean Spike Room Digspot 1"
+ OhoOceanSpikeRoomDigspot2 = "Oho Ocean Spike Room Digspot 2"
+ OceanNorthWhirlpoolBlock1 = "Oho Ocean North Whirlpool Block 1"
+ OceanNorthWhirlpoolBlock2 = "Oho Ocean North Whirlpool Block 2"
+ OceanNorthWhirlpoolBlock3 = "Oho Ocean North Whirlpool Block 3"
+ OceanNorthWhirlpoolBlock4 = "Oho Ocean North Whirlpool Block 4"
+ OceanNorthWhirlpoolDigspot1 = "Oho Ocean North Whirlpool Digspot 1"
+ OceanNorthWhirlpoolDigspot2 = "Oho Ocean North Whirlpool Digspot 2"
+ OceanSouthWhirlpoolDigspot1 = "Oho Ocean South Whirlpool Digspot 1"
+ OceanSouthWhirlpoolDigspot2 = "Oho Ocean South Whirlpool Digspot 2"
+ OceanSouthWhirlpoolDigspot3 = "Oho Ocean South Whirlpool Digspot 3"
+ OceanSouthWhirlpoolDigspot4 = "Oho Ocean South Whirlpool Digspot 4"
+ OceanSouthWhirlpoolDigspot5 = "Oho Ocean South Whirlpool Digspot 5"
+ OceanSouthWhirlpoolDigspot6 = "Oho Ocean South Whirlpool Digspot 6"
+ OceanSouthWhirlpoolRoom2Digspot = "Oho Ocean South Whirlpool Room 2 Digspot"
+ WoohooHooniversityStarRoomBlock1 = "Woohoo Hooniversity Star Room Block 1"
+ WoohooHooniversityStarRoomBlock2 = "Woohoo Hooniversity Star Room Block 2"
+ WoohooHooniversityStarRoomBlock3 = "Woohoo Hooniversity Star Room Block 3"
+ WoohooHooniversitySunDoorBlock1 = "Woohoo Hooniversity Sun Door Block 1"
+ WoohooHooniversitySunDoorBlock2 = "Woohoo Hooniversity Sun Door Block 2"
+ WoohooHooniversitySouthOfStarRoomBlock = "Woohoo Hooniversity South Of Star Room Block"
+ WoohooHooniversityWestOfStarRoomDigspot1 = "Woohoo Hooniversity West Of Star Room Digspot 1"
+ WoohooHooniversityWestOfStarRoomDigspot2 = "Woohoo Hooniversity West Of Star Room Digspot 2"
+ WoohooHooniversityBarrelPuzzleEntranceDigspot1 = "Woohoo Hooniversity Barrel Puzzle Entrance Digspot 1"
+ WoohooHooniversityBarrelPuzzleEntranceBlock1 = "Woohoo Hooniversity Barrel Puzzle Entrance Block 1"
+ WoohooHooniversityBarrelPuzzleEntranceBlock2 = "Woohoo Hooniversity Barrel Puzzle Entrance Block 2"
+ WoohooHooniversityBarrelPuzzleEntranceBlock3 = "Woohoo Hooniversity Barrel Puzzle Entrance Block 3"
+ WoohooHooniversityBarrelPuzzleEntranceBlock4 = "Woohoo Hooniversity Barrel Puzzle Entrance Block 4"
+ WoohooHooniversityBarrelPuzzleEntranceDigspot2 = "Woohoo Hooniversity Barrel Puzzle Entrance Digspot 2"
+ ChucklehuckWoodsRoom1Digspot = "Chucklehuck Woods Room 1 Digspot"
+ WoohooHooniversityWestOfStarRoom2Digspot = "Woohoo Hooniversity West of Star Room 2 Digspot"
+ WoohooHooniversityWestOfStarRoom3Digspot = "Woohoo Hooniversity West of Star Room 3 Digspot"
+ WoohooHooniversityWestOfStarRoom4Block1 = "Woohoo Hooniversity West of Star Room 4 Block 1"
+ WoohooHooniversityWestOfStarRoom4Block2 = "Woohoo Hooniversity West of Star Room 4 Block 2"
+ WoohooHooniversityWestOfStarRoom4Block3 = "Woohoo Hooniversity West of Star Room 4 Block 3"
+ WoohooHooniversityWestOfStarRoom4Digspot1 = "Woohoo Hooniversity West of Star Room 4 Digspot 1"
+ WoohooHooniversityWestOfStarRoom4Digspot2 = "Woohoo Hooniversity West of Star Room 4 Digspot 2"
+ WoohooHooniversityWestOfStarRoom5Digspot = "Woohoo Hooniversity West of Star Room 5 Digspot"
+ WoohooHooniversityEntranceToMiniMarioRoomDigspot1 = "Woohoo Hooniversity Entrance to Mini Mario Room Digspot 1"
+ WoohooHooniversityEntranceToMiniMarioRoomDigspot2 = "Woohoo Hooniversity Entrance to Mini Mario Room Digspot 2"
+ WoohooHooniversityEntranceToMiniMarioRoom2Digspot = "Woohoo Hooniversity Entrance to Mini Mario Room 2 Digspot"
+ WoohooHooniversityMiniMarioPuzzleBlock = "Woohoo Hooniversity Mini Mario Puzzle Block"
+ WoohooHooniversityMiniMarioPuzzleDigspot = "Woohoo Hooniversity Mini Mario Puzzle Digspot"
+ WoohooHooniversityMiniMarioPuzzleSecretAreaBlock1 = "Woohoo Hooniversity Mini Mario Puzzle Secret Area Block 1"
+ WoohooHooniversityMiniMarioPuzzleSecretAreaBlock2 = "Woohoo Hooniversity Mini Mario Puzzle Secret Area Block 2"
+ WoohooHooniversityMiniMarioPuzzleSecretAreaBlock3 = "Woohoo Hooniversity Mini Mario Puzzle Secret Area Block 3"
+ WoohooHooniversityMiniMarioPuzzleSecretAreaBlock4 = "Woohoo Hooniversity Mini Mario Puzzle Secret Area Block 4"
+ WoohooHooniversityPastSunDoorBlock1 = "Woohoo Hooniversity Past Sun Door Block 1"
+ WoohooHooniversityPastSunDoorBlock2 = "Woohoo Hooniversity Past Sun Door Block 2"
+ WoohooHooniversityPastSunDoorBlock3 = "Woohoo Hooniversity Past Sun Door Block 3"
+ WoohooHooniversityPastCacklettaRoom1Block = "Woohoo Hooniversity Past Cackletta Room 1 Block"
+ WoohooHooniversityPastCacklettaRoom2Block1 = "Woohoo Hooniversity Past Cackletta Room 2 Block 1"
+ WoohooHooniversityPastCacklettaRoom2Block2 = "Woohoo Hooniversity Past Cackletta Room 2 Block 2"
+ WoohooHooniversityPastCacklettaRoom2Digspot = "Woohoo Hooniversity Past Cackletta Room 2 Digspot"
+ AirportEntranceDigspot = "Airport Entrance Digspot"
+ AirportLobbyDigspot = "Airport Lobby Digspot"
+ AirportLeftsideDigspot1 = "Airport Leftside Digspot 1"
+ AirportLeftsideDigspot2 = "Airport Leftside Digspot 2"
+ AirportLeftsideDigspot3 = "Airport Leftside Digspot 3"
+ AirportLeftsideDigspot4 = "Airport Leftside Digspot 4"
+ AirportLeftsideDigspot5 = "Airport Leftside Digspot 5"
+ AirportCenterDigspot1 = "Airport Center Digspot 1"
+ AirportCenterDigspot2 = "Airport Center Digspot 2"
+ AirportCenterDigspot3 = "Airport Center Digspot 3"
+ AirportCenterDigspot4 = "Airport Center Digspot 4"
+ AirportCenterDigspot5 = "Airport Center Digspot 5"
+ AirportRightsideDigspot1 = "Airport Rightside Digspot 1"
+ AirportRightsideDigspot2 = "Airport Rightside Digspot 2"
+ AirportRightsideDigspot3 = "Airport Rightside Digspot 3"
+ AirportRightsideDigspot4 = "Airport Rightside Digspot 4"
+ AirportRightsideDigspot5 = "Airport Rightside Digspot 5"
+ GwarharLagoonPipeRoomDigspot = "Gwarhar Lagoon Pipe Room Digspot"
+ GwarharLagoonMassageParlorEntranceDigspot = "Gwarhar Lagoon Massage Parlor Entrance Digspot"
+ GwarharLagoonPastHermieDigspot = "Gwarhar Lagoon Past Hermie Digspot"
+ GwarharLagoonEntranceToWestUnderwaterAreaDigspot = "Gwarhar Lagoon Entrance to West Underwater Area Digspot"
+ GwarharLagoonFireDashPuzzleRoom1Digspot1 = "Gwarhar Lagoon Fire Dash Puzzle Room 1 Digspot 1"
+ GwarharLagoonFireDashPuzzleRoom1Digspot2 = "Gwarhar Lagoon Fire Dash Puzzle Room 1 Digspot 2"
+ GwarharLagoonFireDashPuzzleRoom2Digspot = "Gwarhar Lagoon Fire Dash Puzzle Room 2 Digspot"
+ GwarharLagoonFireDashPuzzleRoom3Digspot1 = "Gwarhar Lagoon Fire Dash Puzzle Room 3 Digspot 1"
+ GwarharLagoonFireDashPuzzleRoom3Digspot2 = "Gwarhar Lagoon Fire Dash Puzzle Room 3 Digspot 2"
+ GwarharLagoonEastOfStoneBridgeBlock = "Gwarhar Lagoon East of Stone Bridge Block"
+ GwarharLagoonNorthOfSpangleRoomDigspot = "Gwarhar Lagoon North of Spangle Room Digspot"
+ GwarharLagoonWestOfSpangleRoomDigspot = "Gwarhar Lagoon West of Spangle Room Digspot"
+ GwarharLagoonSpangleRoomBlock = "Gwarhar Lagoon Spangle Room Block"
+ GwarharLagoonFirstUnderwaterAreaRoom1Block = "Gwarhar Lagoon First Underwater Area Room 1 Block"
+ GwarharLagoonFirstUnderwaterAreaRoom2Block1 = "Gwarhar Lagoon First Underwater Area Room 2 Block 1"
+ GwarharLagoonFirstUnderwaterAreaRoom2Block2 = "Gwarhar Lagoon First Underwater Area Room 2 Block 2"
+ GwarharLagoonSecondUnderwaterAreaRoom4Digspot = "Gwarhar Lagoon Second Underwater Area Room 4 Digspot"
+ GwarharLagoonSecondUnderwaterAreaRoom2Digspot1 = "Gwarhar Lagoon Second Underwater Area Room 2 Digspot 1"
+ GwarharLagoonSecondUnderwaterAreaRoom2Digspot2 = "Gwarhar Lagoon Second Underwater Area Room 2 Digspot 2"
+ GwarharLagoonSecondUnderwaterAreaRoom3Block1 = "Gwarhar Lagoon Second Underwater Area Room 3 Block 1"
+ GwarharLagoonSecondUnderwaterAreaRoom3Block2 = "Gwarhar Lagoon Second Underwater Area Room 3 Block 2"
+ GwarharLagoonSecondUnderwaterAreaRoom3Block3 = "Gwarhar Lagoon Second Underwater Area Room 3 Block 3"
+ GwarharLagoonSecondUnderwaterAreaRoom1Digspot = "Gwarhar Lagoon Second Underwater Area Room 1 Digspot"
+ WoohooHooniversityBasementRoom1Digspot = "Woohoo Hooniversity Basement Room 1 Digspot"
+ WoohooHooniversityBasementRoom2Digspot = "Woohoo Hooniversity Basement Room 2 Digspot"
+ WoohooHooniversityBasementRoom3Block = "Woohoo Hooniversity Basement Room 3 Block"
+ WoohooHooniversityBasementRoom4Block = "Woohoo Hooniversity Basement Room 4 Block"
+ WoohooHooniversityPoppleRoomDigspot1 = "Woohoo Hooniversity Popple Room Digspot 1"
+ WoohooHooniversityPoppleRoomDigspot2 = "Woohoo Hooniversity Popple Room Digspot 2"
+ TeeheeValleyBeforePoppleDigspot1 = "Teehee Valley Before Popple Digspot 1"
+ TeeheeValleyBeforePoppleDigspot2 = "Teehee Valley Before Popple Digspot 2"
+ TeeheeValleyBeforePoppleDigspot3 = "Teehee Valley Before Popple Digspot 3"
+ TeeheeValleyBeforePoppleDigspot4 = "Teehee Valley Before Popple Digspot 4"
+ TeeheeValleyRoom1Digspot1 = "Teehee Valley Room 1 Digspot 1"
+ TeeheeValleyRoom1Digspot2 = "Teehee Valley Room 1 Digspot 2"
+ TeeheeValleyRoom1Digspot3 = "Teehee Valley Room 1 Digspot 3"
+ TeeheeValleyEastRoomDigspot1 = "Teehee Valley East Room Digspot 1"
+ TeeheeValleyEastRoomDigspot2 = "Teehee Valley East Room Digspot 2"
+ TeeheeValleyEastRoomDigspot3 = "Teehee Valley East Room Digspot 3"
+ TeeheeValleySoloMarioRoomDigspot1 = "Teehee Valley Solo Mario Room Digspot 1"
+ TeeheeValleySoloMarioRoomDigspot2 = "Teehee Valley Solo Mario Room Digspot 2"
+ TeeheeValleySoloMarioRoomDigspot3 = "Teehee Valley Solo Mario Room Digspot 3"
+ TeeheeValleySoloMarioRoomDigspot4 = "Teehee Valley Solo Mario Room Digspot 4"
+ TeeheeValleyPastUltraHammersBlock1 = "Teehee Valley Past Ultra Hammer Rock Block 1"
+ TeeheeValleyPastUltraHammersBlock2 = "Teehee Valley Past Ultra Hammer Rock Block 2"
+ TeeheeValleyPastUltraHammersDigspot1 = "Teehee Valley Past Ultra Hammer Rock Digspot 1"
+ TeeheeValleyPastUltraHammersDigspot2 = "Teehee Valley Past Ultra Hammer Rock Digspot 2 (Post-Birdo)"
+ TeeheeValleyPastUltraHammersDigspot3 = "Teehee Valley Past Ultra Hammer Rock Digspot 3"
+ TeeheeValleyEntranceToHoohooMountainDigspot = "Teehee Valley Entrance To Hoohoo Mountain Digspot"
+ TeeheeValleySoloLuigiMazeRoom2Digspot1 = "Teehee Valley Solo Luigi Maze Room 2 Digspot 1"
+ TeeheeValleySoloLuigiMazeRoom2Digspot2 = "Teehee Valley Solo Luigi Maze Room 2 Digspot 2"
+ TeeheeValleySoloLuigiMazeRoom1Block = "Teehee Valley Solo Luigi Maze Room 1 Block"
+ TeeheeValleyBeforeTrunkleDigspot = "Teehee Valley Before Trunkle Digspot"
+ TeeheeValleyTrunkleRoomDigspot = "Teehee Valley Trunkle Room Digspot"
+ SSChuckolaStorageRoomBlock1 = "S.S. Chuckola Storage Room Block 1"
+ SSChuckolaStorageRoomBlock2 = "S.S. Chuckola Storage Room Block 2"
+ LittleFungitownEmbassyRoomBlock = "Little Fungitown Embassy Room Block"
+ LittleFungitownEntranceRoomBlock = "Little Fungitown Entrance Room Block"
+ JokesEndPipeDigspot = "Joke's End Pipe Digspot"
+ JokesEndStaircaseDigspot = "Joke's End Staircase Digspot"
+ JokesEndWestOfFirstBoilerRoomBlock1 = "Joke's End West Of First Boiler Room Block 1"
+ JokesEndWestOfFirstBoilerRoomBlock2 = "Joke's End West Of First Boiler Room Block 2"
+ JokesEndFirstBoilerRoomDigspot1 = "Joke's End First Boiler Room Digspot 1"
+ JokesEndFirstBoilerRoomDigspot2 = "Joke's End First Boiler Room Digspot 2"
+ JokesEndFurnaceRoom1Block1 = "Joke's End Furnace Room 1 Block 1"
+ JokesEndFurnaceRoom1Block2 = "Joke's End Furnace Room 1 Block 2"
+ JokesEndFurnaceRoom1Block3 = "Joke's End Furnace Room 1 Block 3"
+ JokesEndNortheastOfBoilerRoom1Block = "Joke's End Northeast Of Boiler Room 1 Block"
+ JokesEndNortheastOfBoilerRoom3Digspot = "Joke's End Northeast Of Boiler Room 3 Digspot"
+ JokesEndNortheastOfBoilerRoom2Block1 = "Joke's End Northeast Of Boiler Room 2 Block"
+ JokesEndNortheastOfBoilerRoom2Block2 = "Joke's End Northeast Of Boiler Room 2 Digspot"
+ JokesEndSecondFloorWestRoomBlock1 = "Joke's End Second Floor West Room Block 1"
+ JokesEndSecondFloorWestRoomBlock2 = "Joke's End Second Floor West Room Block 2"
+ JokesEndSecondFloorWestRoomBlock3 = "Joke's End Second Floor West Room Block 3"
+ JokesEndSecondFloorWestRoomBlock4 = "Joke's End Second Floor West Room Block 4"
+ JokesEndSecondFloorEastRoomDigspot = "Joke's End Second Floor East Room Digspot"
+ JokesEndFinalSplitUpRoomDigspot = "Joke's End Final Split Up Room Digspot"
+ JokesEndSouthOfBridgeRoomBlock = "Joke's End South Of Bridge Room Block"
+ JokesEndSoloLuigiRoom1Block = "Joke's End Solo Luigi Room 1 Block"
+ JokesEndSoloLuigiRoom1Digspot = "Joke's End Solo Luigi Room 1 Digspot"
+ JokesEndSoloMarioFinalRoomBlock1 = "Joke's End Solo Mario Final Room Block 1"
+ JokesEndSoloMarioFinalRoomBlock2 = "Joke's End Solo Mario Final Room Block 2"
+ JokesEndSoloMarioFinalRoomBlock3 = "Joke's End Solo Mario Final Room Block 3"
+ JokesEndSoloLuigiRoom2Digspot = "Joke's End Solo Luigi Room 2 Digspot"
+ JokesEndSoloMarioRoom1Digspot = "Joke's End Solo Mario Room 1 Digspot"
+ JokesEndSoloMarioRoom2Block1 = "Joke's End Solo Mario Room 2 Block 1"
+ JokesEndSoloMarioRoom2Block2 = "Joke's End Solo Mario Room 2 Block 2"
+ JokesEndSoloMarioRoom2Block3 = "Joke's End Solo Mario Room 2 Block 3"
+ JokesEndSecondBoilerRoomDigspot1 = "Joke's End Second Boiler Room Digspot 1"
+ JokesEndSecondBoilerRoomDigspot2 = "Joke's End Second Boiler Room Digspot 2"
+ JokesEndNorthOfSecondBoilerRoomBlock1 = "Joke's End North Of Second Boiler Room Block 1"
+ JokesEndNorthOfSecondBoilerRoomBlock2 = "Joke's End North Of Second Boiler Room Block 2"
+ WinkleAreaColloseumDigspot = "Winkle Area Colloseum Digspot"
+ HoohooMountainFountainRoom2Block = "Hoohoo Mountain Fountain Room 2 Block"
+ HoohooMountainFountainRoom2Digspot = "Hoohoo Mountain Fountain Room 2 Digspot"
+ HoohooMountainPastHoohoorosConnectorRoomDigspot1 = "Hoohoo Mountain Past Hoohooros Connector Room Digspot 1"
+ HoohooMountainPastHoohoorosConnectorRoomBlock = "Hoohoo Mountain Past Hoohooros Connector Room Block"
+ HoohooMountainPastHoohoorosConnectorRoomDigspot2 = "Hoohoo Mountain Past Hoohooros Connector Room Digspot 2"
+ JokesEndBeforeJojoraRoomBlock1 = "Joke's End Before Jojora Room Block 1"
+ JokesEndBeforeJojoraRoomBlock2 = "Joke's End Before Jojora Room Block 2"
+ JokesEndBeforeJojoraRoomDigspot = "Joke's End Before Jojora Room Digspot"
+ JokesEndJojoraRoomDigspot = "Joke's End Jojora Room Digspot"
+ BeanbeanOutskirtsBeforeHarhallDigspot1 = "Beanbean Outskirts Before Harhall Digspot 1"
+ BeanbeanOutskirtsBeforeHarhallDigspot2 = "Beanbean Outskirts Before Harhall Digspot 2"
+ BeanbeanOutskirtsBroochGuardsRoomDigspot1 = "Beanbean Outskirts Brooch Guards Room Digspot 1"
+ BeanbeanOutskirtsBroochGuardsRoomDigspot2 = "Beanbean Outskirts Brooch Guards Room Digspot 2"
+ BeanbeanOutskirtsChateauEntranceDigspot1 = "Beanbean Outskirts Chateau Entrance Digspot 1"
+ BeanbeanOutskirtsChateauEntranceDigspot2 = "Beanbean Outskirts Chateau Entrance Digspot 2"
+ BeanbeanOutskirtsSouthOfHooniversityGuardsDigspot1 = "Beanbean Outskirts South of Hooniversity Guards Digspot 1"
+ BeanbeanOutskirtsSouthOfHooniversityGuardsDigspot2 = "Beanbean Outskirts South of Hooniversity Guards Digspot 2"
+ BeanbeanOutskirtsSouthOfHooniversityGuardsDigspot3 = "Beanbean Outskirts South of Hooniversity Guards Digspot 3"
+ OutsideWoohooHooniversityBlock = "Outside Woohoo Hooniversity Block"
+ BeanbeanOutskirtsEntranceToHoohooMountainBaseDigspot1 = (
+ "Beanbean Outskirts Entrance to Hoohoo Mountain Base Digspot 1"
+ )
+ BeanbeanOutskirtsEntranceToHoohooMountainBaseDigspot2 = (
+ "Beanbean Outskirts Entrance to Hoohoo Mountain Base Digspot 2"
+ )
+ WoohooHooniversitySoloMarioBarrelAreaBlock1 = "Woohoo Hooniversity Solo Mario Barrel Area Block 1"
+ WoohooHooniversitySoloMarioBarrelAreaBlock2 = "Woohoo Hooniversity Solo Mario Barrel Area Block 2"
+ WoohooHooniversitySoloMarioBarrelAreaBlock3 = "Woohoo Hooniversity Solo Mario Barrel Area Block 3"
+ BeanbeanOutskirtsPipe2RoomDigspot = "Beanbean Outskirts Pipe 2 Room Digspot"
+ BeanbeanOutskirtsPipe4RoomDigspot = "Beanbean Outskirts Pipe 4 Room Digspot"
+ BeanbeanCastleTownBeanletReward = "Beanbean Castle Town Beanlet Reward"
+ HoohooVillageMoleBehindTurtle = "Hoohoo Village Mole Behind Turtle"
+ HoohooMountainBaseMoleNearTeeheeValley = "Hoohoo Mountain Base Mole Near Teehee Valley"
+ BeanbeanOutskirtsSoloLuigiCaveMole = "Beanbean Outskirts Solo Luigi Cave Mole"
+ BeanbeanOutskirtsFarmRoomMoleReward1 = "Beanbean Outskirts Farm Room Mole Reward 1"
+ BeanbeanOutskirtsFarmRoomMoleReward2 = "Beanbean Outskirts Farm Room Mole Reward 2"
+ JokesEndMoleReward1 = "Joke's End Mole Reward 1"
+ JokesEndMoleReward2 = "Joke's End Mole Reward 2"
+ NorthOceanWhirlpoolMole = "North Ocean Whirlpool Mole"
+ BeanbeanOutskirtsNESoloMarioMole1 = "Beanbean Outskirts NE Solo Mario Mole 1"
+ HoohooVillageHammers = "Hoohoo Village Hammers"
+ BeanbeanOutskirtsSuperHammerUpgrade = "Beanbean Outskirts Super Hammer Upgrade"
+ BeanbeanOutskirtsUltraHammerUpgrade = "Beanbean Outskirts Ultra Hammer Upgrade"
+ OhoOasisFirebrand = "Oho Oasis Firebrand"
+ OhoOasisThunderhand = "Oho Oasis Thunderhand"
+ ChucklehuckWoodsRedChuckolaFruit = "Chucklehuck Woods Red Chuckola Fruit"
+ ChucklehuckWoodsWhiteChuckolaFruit = "Chucklehuck Woods White Chuckola Fruit"
+ ChucklehuckWoodsPurpleChuckolaFruit = "Chucklehuck Woods Purple Chuckola Fruit"
+ SSChuckolaMembershipCard = "S.S. Chuckola Membership Card"
+ WinkleAreaWinkleCard = "Winkle Area Winkle Card"
+ BeanbeanCastlePeachsExtraDress = "Beanbean Castle Peach's Extra Dress"
+ BeanbeanCastleFakeBeastar = "Beanbean Castle Fake Beanstar"
+ BeanbeanCastleTownBeanlet1 = "Beanbean Castle Town Beanlet 1"
+ BeanbeanCastleTownBeanlet2 = "Beanbean Castle Town Beanlet 2"
+ BeanbeanCastleTownBeanlet3 = "Beanbean Castle Town Beanlet 3"
+ BeanbeanCastleTownBeanlet4 = "Beanbean Castle Town Beanlet 4"
+ BeanbeanCastleTownBeanlet5 = "Beanbean Castle Town Beanlet 5"
+ BeanbeanCastleTownBeanstone1 = "Beanbean Castle Town Beanstone 1"
+ BeanbeanCastleTownBeanstone2 = "Beanbean Castle Town Beanstone 2"
+ BeanbeanCastleTownBeanstone3 = "Beanbean Castle Town Beanstone 3"
+ BeanbeanCastleTownBeanstone4 = "Beanbean Castle Town Beanstone 4"
+ BeanbeanCastleTownBeanstone5 = "Beanbean Castle Town Beanstone 5"
+ BeanbeanCastleTownBeanstone6 = "Beanbean Castle Town Beanstone 6"
+ BeanbeanCastleTownBeanstone7 = "Beanbean Castle Town Beanstone 7"
+ BeanbeanCastleTownBeanstone8 = "Beanbean Castle Town Beanstone 8"
+ BeanbeanCastleTownBeanstone9 = "Beanbean Castle Town Beanstone 9"
+ BeanbeanCastleTownBeanstone10 = "Beanbean Castle Town Beanstone 10"
+ YoshiTheaterBlueYoshi = "Yoshi Theater Blue Yoshi"
+ YoshiTheaterRedYoshi = "Yoshi Theater Red Yoshi"
+ YoshiTheaterGreenYoshi = "Yoshi Theater Green Yoshi"
+ YoshiTheaterYellowYoshi = "Yoshi Theater Yellow Yoshi"
+ YoshiTheaterPurpleYoshi = "Yoshi Theater Purple Yoshi"
+ YoshiTheaterOrangeYoshi = "Yoshi Theater Orange Yoshi"
+ YoshiTheaterAzureYoshi = "Yoshi Theater Azure Yoshi"
+ BeanbeanCastleBeanbeanBrooch = "Beanbean Castle Beanbean Brooch"
+ BeanbeanOutskirtsSecretScroll1 = "Beanbean Outskirts Secret Scroll 1"
+ BeanbeanOutskirtsSecretScroll2 = "Beanbean Outskirts Secret Scroll 2"
+ BeanbeanOutskirtsBeanFruit1 = "Beanbean Outskirts Bean Fruit 1"
+ BeanbeanOutskirtsBeanFruit2 = "Beanbean Outskirts Bean Fruit 2"
+ BeanbeanOutskirtsBeanFruit3 = "Beanbean Outskirts Bean Fruit 3"
+ BeanbeanOutskirtsBeanFruit4 = "Beanbean Outskirts Bean Fruit 4"
+ BeanbeanOutskirtsBeanFruit5 = "Beanbean Outskirts Bean Fruit 5"
+ BeanbeanOutskirtsBeanFruit6 = "Beanbean Outskirts Bean Fruit 6"
+ BeanbeanOutskirtsBeanFruit7 = "Beanbean Outskirts Bean Fruit 7"
+ HoohooMountainPeasleysRose = "Hoohoo Mountain Peasley's Rose"
+ ChateauGreenGoblet = "Chateau Green Goblet"
+ ChateauRedGoblet = "Chateau Red Goblet"
+ GwarharLagoonRedPearlBean = "Gwarhar Lagoon Red Pearl Bean"
+ GwarharLagoonGreenPearlBean = "Gwarhar Lagoon Green Pearl Bean"
+ GwarharLagoonSpangle = "Gwarhar Lagoon Spangle"
+ BeanstarPieceWinkleArea = "Beanstar Piece Winkle Area"
+ BeanstarPieceHarhall = "Beanstar Piece Harhall"
+ BeanstarPieceYoshiTheater = "Beanstar Piece Yoshi Theater"
+ BeanstarPieceHermie = "Beanstar Piece Hermie"
+ ShopStartingFlag1 = "Shop Starting Flag 1"
+ ShopStartingFlag2 = "Shop Starting Flag 2"
+ ShopStartingFlag3 = "Shop Starting Flag 3"
+ ShopChuckolatorFlag = "Shop Chuckolator Flag"
+ ShopMomPiranhaFlag1 = "Shop Mom Piranha Flag 1"
+ ShopMomPiranhaFlag2 = "Shop Mom Piranha Flag 2"
+ ShopMomPiranhaFlag3 = "Shop Mom Piranha Flag 3"
+ ShopMomPiranhaFlag4 = "Shop Mom Piranha Flag 4"
+ ShopPeachKidnappedFlag1 = "Shop Enter Fungitown Flag 1"
+ ShopPeachKidnappedFlag2 = "Shop Enter Fungitown Flag 2"
+ FungitownShopStartingFlag1 = "Fungitown Shop Starting Flag 1"
+ FungitownShopStartingFlag2 = "Fungitown Shop Starting Flag 2"
+ FungitownShopStartingFlag3 = "Fungitown Shop Starting Flag 3"
+ FungitownShopStartingFlag4 = "Fungitown Shop Starting Flag 4"
+ FungitownShopStartingFlag5 = "Fungitown Shop Starting Flag 5"
+ FungitownShopStartingFlag6 = "Fungitown Shop Starting Flag 6"
+ FungitownShopStartingFlag7 = "Fungitown Shop Starting Flag 7"
+ FungitownShopStartingFlag8 = "Fungitown Shop Starting Flag 8"
+ ShopBeanstarCompleteFlag1 = "Shop Beanstar Complete Flag 1"
+ ShopBeanstarCompleteFlag2 = "Shop Beanstar Complete Flag 2"
+ ShopBeanstarCompleteFlag3 = "Shop Beanstar Complete Flag 3"
+ FungitownShopBeanstarCompleteFlag = "Fungitown Shop Beanstar Complete Flag"
+ ShopBirdoFlag = "Shop Birdo Flag"
+ FungitownShopBirdoFlag = "Fungitown Shop Birdo Flag"
+ CoffeeShopBrewReward1 = "Coffee Shop Brew Reward 1"
+ CoffeeShopBrewReward2 = "Coffee Shop Brew Reward 2"
+ CoffeeShopBrewReward3 = "Coffee Shop Brew Reward 3"
+ CoffeeShopBrewReward4 = "Coffee Shop Brew Reward 4"
+ CoffeeShopBrewReward5 = "Coffee Shop Brew Reward 5"
+ CoffeeShopBrewReward6 = "Coffee Shop Brew Reward 6"
+ CoffeeShopBrewReward7 = "Coffee Shop Brew Reward 7"
+ CoffeeShopWoohooBlend = "Coffee Shop Woohoo Blend"
+ CoffeeShopHoohooBlend = "Coffee Shop Hoohoo Blend"
+ CoffeeShopChuckleBlend = "Coffee Shop Chuckle Blend"
+ CoffeeShopTeeheeBlend = "Coffee Shop Teehee Blend"
+ CoffeeShopHoolumbian = "Coffee Shop Hoolumbian"
+ CoffeeShopChuckoccino = "Coffee Shop Chuckoccino"
+ CoffeeShopTeeheespresso = "Coffee Shop Teeheespresso"
+ PantsShopStartingFlag1 = "Pants Shop Starting Flag 1"
+ PantsShopStartingFlag2 = "Pants Shop Starting Flag 2"
+ PantsShopStartingFlag3 = "Pants Shop Starting Flag 3"
+ PantsShopChuckolatorFlag1 = "Pants Shop Chuckolator Flag 1"
+ PantsShopChuckolatorFlag2 = "Pants Shop Chuckolator Flag 2"
+ PantsShopChuckolatorFlag3 = "Pants Shop Chuckolator Flag 3"
+ PantsShopMomPiranhaFlag1 = "Pants Shop Mom Piranha Flag 1"
+ PantsShopMomPiranhaFlag2 = "Pants Shop Mom Piranha Flag 2"
+ PantsShopMomPiranhaFlag3 = "Pants Shop Mom Piranha Flag 3"
+ PantsShopPeachKidnappedFlag1 = "Pants Shop Enter Fungitown Flag 1"
+ PantsShopPeachKidnappedFlag2 = "Pants Shop Enter Fungitown Flag 2"
+ PantsShopPeachKidnappedFlag3 = "Pants Shop Enter Fungitown Flag 3"
+ PantsShopBeanstarCompleteFlag1 = "Pants Shop Beanstar Complete Flag 1"
+ PantsShopBeanstarCompleteFlag2 = "Pants Shop Beanstar Complete Flag 2"
+ PantsShopBeanstarCompleteFlag3 = "Pants Shop Beanstar Complete Flag 3"
+ PantsShopBirdoFlag1 = "Pants Shop Birdo Flag 1"
+ PantsShopBirdoFlag2 = "Pants Shop Birdo Flag 2"
+ PantsShopBirdoFlag3 = "Pants Shop Birdo Flag 3"
+ FungitownPantsShopStartingFlag1 = "Fungitown Pants Shop Starting Flag 1"
+ FungitownPantsShopStartingFlag2 = "Fungitown Pants Shop Starting Flag 2"
+ FungitownPantsShopStartingFlag3 = "Fungitown Pants Shop Starting Flag 3"
+ FungitownPantsShopBeanstarCompleteFlag1 = "Fungitown Pants Shop Beanstar Complete Flag 1"
+ FungitownPantsShopBeanstarCompleteFlag2 = "Fungitown Pants Shop Beanstar Complete Flag 2"
+ FungitownPantsShopBirdoFlag1 = "Fungitown Pants Shop Birdo Flag 1"
+ FungitownPantsShopBirdoFlag2 = "Fungitown Pants Shop Birdo Flag 2"
+ BeanbeanOutskirtsNESoloMarioMole2 = "Beanbean Outskirts NE Solo Mario Mole 2"
+ GwarharLagoonSpangleReward = "Gwarhar Lagoon Spangle Reward"
+ BowsersCastleEntranceBlock1 = "Bowser's Castle Entrance Block 1"
+ BowsersCastleEntranceBlock2 = "Bowser's Castle Entrance Block 2"
+ BowsersCastleEntranceDigspot = "Bowser's Castle Entrance Digspot"
+ BowsersCastleIggyMortonHallwayBlock1 = "Bowser's Castle Iggy & Morton Hallway Block 1"
+ BowsersCastleIggyMortonHallwayBlock2 = "Bowser's Castle Iggy & Morton Hallway Block 2"
+ BowsersCastleIggyMortonHallwayDigspot = "Bowser's Castle Iggy & Morton Hallway Digspot"
+ BowsersCastleAfterMortonBlock = "Bowser's Castle After Morton Block"
+ BowsersCastleLudwigRoyHallwayBlock1 = "Bowser's Castle Ludwig & Roy Hallway Block 1"
+ BowsersCastleLudwigRoyHallwayBlock2 = "Bowser's Castle Ludwig & Roy Hallway Block 2"
+ BowsersCastleRoyCorridorBlock1 = "Bowser's Castle Roy Corridor Block 1"
+ BowsersCastleRoyCorridorBlock2 = "Bowser's Castle Roy Corridor Block 2"
+ BowsersCastleWendyLarryHallwayDigspot = "Bowser's Castle Wendy & Larry Hallway Digspot"
+ BowsersCastleBeforeFawfulFightBlock1 = "Bowser's Castle Before Fawful Fight Block 1"
+ BowsersCastleBeforeFawfulFightBlock2 = "Bowser's Castle Before Fawful Fight Block 2"
+ BowsersCastleGreatDoorBlock1 = "Bowser's Castle Great Door Block 1"
+ BowsersCastleGreatDoorBlock2 = "Bowser's Castle Great Door Block 2"
+ BowsersCastleMortonRoom1Digspot = "Bowser's Castle Morton Room 1 Digspot"
+ BowsersCastleLemmyRoom1Block = "Bowser's Castle Lemmy Room 1 Block"
+ BowsersCastleLemmyRoom1Digspot = "Bowser's Castle Lemmy Room 1 Digspot"
+ BowsersCastleLudwigRoom1Block = "Bowser's Castle Ludwig Room 1 Block"
+ BowsersCastleMiniMarioSidescrollerBlock1 = "Bowser's Castle Mini Mario Sidescroller Block 1"
+ BowsersCastleMiniMarioSidescrollerBlock2 = "Bowser's Castle Mini Mario Sidescroller Block 2"
+ BowsersCastleMiniMarioMazeBlock1 = "Bowser's Castle Mini Mario Maze Block 1"
+ BowsersCastleMiniMarioMazeBlock2 = "Bowser's Castle Mini Mario Maze Block 2"
+ BowsersCastleBeforeWendyFightBlock1 = "Bowser's Castle Before Wendy Fight Block 1"
+ BowsersCastleBeforeWendyFightBlock2 = "Bowser's Castle Before Wendy Fight Block 2"
+ BowsersCastleLarryRoomBlock = "Bowser's Castle Larry Room Block"
+ BowsersCastleLemmyRoomMole = "Bowser's Castle Lemmy Room Mole"
+ SurfMinigame = "Surf Minigame"
+ BeanbeanOutskirtsThunderHandMole = "Beanbean Outskirts Thunderhand Mole"
+ BadgeShopMomPiranhaFlag1 = "Badge Shop Mom Piranha Flag 1"
+ BadgeShopMomPiranhaFlag2 = "Badge Shop Mom Piranha Flag 2"
+ BadgeShopMomPiranhaFlag3 = "Badge Shop Mom Piranha Flag 3"
+ HarhallsPants = "Harhall's Pants"
+ HoohooMountainBaseBooStatueCaveCoinBlock1 = "Hoohoo Mountain Base Boo Statue Cave Coin Block 1"
+ HoohooMountainBaseBooStatueCaveCoinBlock2 = "Hoohoo Mountain Base Boo Statue Cave Coin Block 2"
+ HoohooMountainBaseBooStatueCaveCoinBlock3 = "Hoohoo Mountain Base Boo Statue Cave Coin Block 3"
+ BeanbeanOutskirtsNWCoinBlock = "Beanbean Outskirts NW Coin Block"
+ BeanbeanOutskirtsSRoom1CoinBlock = "Beanbean Outskirts S Room 1 Coin Block"
+ BeanbeanOutskirtsSRoom2CoinBlock = "Beanbean Outskirts S Room 2 Coin Block"
+ ChateauPoppleRoomCoinBlock1 = "Chateau Popple Room Coin Block 1"
+ ChateauPoppleRoomCoinBlock2 = "Chateau Popple Room Coin Block 2"
+ ChucklehuckWoodsCaveRoom1CoinBlock = "Chucklehuck Woods Cave Room 1 Coin Block"
+ ChucklehuckWoodsCaveRoom2CoinBlock = "Chucklehuck Woods Cave Room 2 Coin Block"
+ ChucklehuckWoodsCaveRoom3CoinBlock = "Chucklehuck Woods Cave Room 3 Coin Block"
+ ChucklehuckWoodsPipe5RoomCoinBlock = "Chucklehuck Woods Pipe 5 Room Coin Block"
+ ChucklehuckWoodsRoom7CoinBlock = "Chucklehuck Woods Room 7 Coin Block"
+ ChucklehuckWoodsAfterChucklerootCoinBlock = "Chucklehuck Woods After Chuckleroot Coin Block"
+ ChucklehuckWoodsKoopaRoomCoinBlock = "Chucklehuck Woods Koopa Room Coin Block"
+ ChucklehuckWoodsWinkleAreaCaveCoinBlock = "Chucklehuck Woods Winkle Area Cave Coin Block"
+ SewersPrisonRoomCoinBlock = "Sewers Prison Room Coin Block"
+ TeeheeValleyPastUltraHammerRocksCoinBlock = "Teehee Valley Past Ultra Hammer Rocks Coin Block"
+ SSChuckolaStorageRoomCoinBlock1 = "S.S. Chuckola Storage Room Coin Block 1"
+ SSChuckolaStorageRoomCoinBlock2 = "S.S. Chuckola Storage Room Coin Block 2"
+ GwarharLagoonFirstUnderwaterAreaRoom2CoinBlock = "Gwarhar Lagoon First Underwater Area Room 2 Coin Block"
+ JokesEndSecondFloorWestRoomCoinBlock = "Joke's End Second Floor West Room Coin Block"
+ JokesEndNorthofBridgeRoomCoinBlock = "Joke's End North of Bridge Room Coin Block"
+
diff --git a/worlds/mlss/Options.py b/worlds/mlss/Options.py
new file mode 100644
index 000000000000..14c1ef3a7d5a
--- /dev/null
+++ b/worlds/mlss/Options.py
@@ -0,0 +1,299 @@
+from Options import Choice, Toggle, StartInventoryPool, PerGameCommonOptions, Range
+from dataclasses import dataclass
+
+
+class BowsersCastleSkip(Toggle):
+ """
+ Skip straight from the entrance hall to Bowletta in Bowser's Castle.
+ All Bowser's Castle locations will be removed from the location pool.
+ """
+
+ display_name = "Bowser's Castle Skip"
+
+
+class ExtraPipes(Toggle):
+ """
+ Gives the player access to pipes 1, 3, 4, and 6 from the start.
+ """
+
+ display_name = "Start With Extra Pipes"
+
+
+class SkipMinecart(Toggle):
+ """
+ Skip the minecart minigame that leads you through Hoohoo Mountain Base.
+ This will remove the 1 location in the minecart cave from the location pool.
+ """
+
+ display_name = "Skip Minecart Minigame"
+
+
+class DisableSurf(Toggle):
+ """
+ Remove the surf minigame location from the location pool.
+ """
+
+ display_name = "Disable Surf Minigame"
+
+
+class MusicOptions(Choice):
+ """
+ Choose if you want to randomize or disable music.
+ default: Music will be untouched.
+ randomize: Music will be randomized.
+ disable: All music will be disabled. No music will play throughout the entire game.
+ """
+
+ display_name = "Music Options"
+ option_default = 0
+ option_randomize = 1
+ option_disable = 2
+ default = 0
+
+
+class RandomSounds(Toggle):
+ """
+ Randomizes every sound in the game, minus a select few that can softlock the game.
+ """
+
+ display_name = "Randomize Sounds"
+
+
+class MarioColor(Choice):
+ """
+ This changes the color of Mario's hat, as well as some key colors that are red including UI etc.
+ """
+
+ display_name = "Mario's Color"
+ option_red = 0
+ option_green = 1
+ option_blue = 2
+ option_cyan = 3
+ option_yellow = 4
+ option_orange = 5
+ option_purple = 6
+ option_pink = 7
+ option_black = 8
+ option_white = 9
+ option_silhouette = 10
+ option_chaos = 11
+ option_true_chaos = 12
+ default = 0
+
+
+class LuigiColor(Choice):
+ """
+ This changes the color of Luigi's hat, as well as some key colors that are green including UI etc.
+ """
+
+ display_name = "Luigi's Color"
+ option_red = 0
+ option_green = 1
+ option_blue = 2
+ option_cyan = 3
+ option_yellow = 4
+ option_orange = 5
+ option_purple = 6
+ option_pink = 7
+ option_black = 8
+ option_white = 9
+ option_silhouette = 10
+ option_chaos = 11
+ option_true_chaos = 12
+ default = 1
+
+
+class MarioPants(Choice):
+ """
+ This changes the color of Mario's trousers.
+ """
+
+ display_name = "Mario's Pants Color"
+ option_vanilla = 0
+ option_red = 1
+ option_green = 2
+ option_blue = 3
+ option_cyan = 4
+ option_yellow = 5
+ option_orange = 6
+ option_purple = 7
+ option_pink = 8
+ option_black = 9
+ option_white = 10
+ option_chaos = 11
+ default = 0
+
+
+class LuigiPants(Choice):
+ """
+ This changes the color of Luigi's trousers.
+ """
+
+ display_name = "Luigi's Pants Color"
+ option_vanilla = 0
+ option_red = 1
+ option_green = 2
+ option_blue = 3
+ option_cyan = 4
+ option_yellow = 5
+ option_orange = 6
+ option_purple = 7
+ option_pink = 8
+ option_black = 9
+ option_white = 10
+ option_chaos = 11
+ default = 0
+
+
+class RandomizeEnemies(Choice):
+ """
+ Randomize all normal enemy encounters in the game.
+ If Bowser's castle skip is enabled, then enemies from Bowser's Castle will not be included.
+ Disabled: Enemies will not be randomized.
+ Vanilla Groups: Vanilla enemy groups will be shuffled with each other. Custom enemy groups will not be made.
+ Custom Groups: Custom enemy groups will be made and shuffled. Some enemy groups will only be semi-random,
+ including groups with flying enemies or pestnuts in them.
+ """
+
+ display_name = "Randomize Enemies"
+ option_disabled = 0
+ option_vanilla_groups = 1
+ option_custom_groups = 2
+ default = 0
+
+
+class RandomizeBosses(Choice):
+ """
+ Randomize all boss encounters in the game.
+ If Bowser's castle skip is enabled then bosses from Bowser's Castle will not be included.
+ Some bosses are not randomized due to flags, and story (such as the final boss).
+ Boss Only: Bosses will only be swapped with another boss.
+ Boss Normal: Bosses can be swapped with normal enemy encounters.
+ """
+
+ display_name = "Randomize Bosses"
+ option_disabled = 0
+ option_boss_only = 1
+ option_boss_normal = 2
+ default = 0
+
+
+class ScaleStats(Toggle):
+ """
+ This scales enemy HP, POW, DEF, and XP to vanilla values.
+ This setting is intended for use with the Enemy Randomizer and is Recommended to turn on.
+ If you are not using the Enemy Randomizer the effects will be minimal.
+ """
+
+ display_name = "Scale Enemy Stats"
+
+
+class XPMultiplier(Range):
+ """
+ This will multiply any XP you receive in battle by the chosen multiplier.
+ """
+
+ display_name = "XP Multiplier"
+ range_start = 0
+ range_end = 4
+ default = 1
+
+
+class TattleHp(Toggle):
+ """
+ This will display the enemies' current and max health while in battle.
+ """
+
+ display_name = "Tattle HP"
+
+
+class RandomizeBackgrounds(Toggle):
+ """
+ This randomizes the background image in battles.
+ """
+
+ display_name = "Randomize Battle Backgrounds"
+
+
+class HiddenVisible(Choice):
+ """
+ This makes any hidden blocks in the game into regular item blocks and vice versa.
+ Disabled: Hidden blocks will remain invisible.
+ Hidden Visible: Hidden blocks will turn visible to the player.
+ Blocks Invisible: All item blocks will turn invisible. Hidden blocks will also remain invisible.
+ """
+
+ display_name = "Item Block Visibility"
+ option_disabled = 0
+ option_hidden_visible = 1
+ option_blocks_invisible = 2
+ default = 0
+
+
+class Coins(Toggle):
+ """
+ Add all coin blocks in the game to the location pool.
+ """
+
+ display_name = "Coin Blocks"
+
+
+class HarhallsPants(Toggle):
+ """
+ This will remove the Harhall's Pants check from the pool.
+ """
+
+ display_name = "Remove Harhall's Pants"
+
+
+class DifficultLogic(Toggle):
+ """
+ This adjusts the logic to be more difficult in a few areas,
+ allowing for the logic to account for players getting to certain areas in unintended ways.
+ Enable at your own risk, this is not an option made for beginners.
+ """
+
+ display_name = "Difficult Logic"
+
+
+class ChuckleBeans(Choice):
+ """
+ Choose how you want chuckle bean digspots to be randomized.
+ An amount of chuckle beans will be removed from the item pool,
+ equal to the amount of locations removed by the setting that you choose.
+ None: No chuckle bean digspots will be added into the location pool.
+ Only Visible: Only chuckle bean digspots clearly marked with an X will be added into the location pool.
+ All: All chuckle bean digspots will be added into the location pool.
+ """
+
+ display_name = "Chuckle Beans"
+ option_none = 0
+ option_only_visible = 1
+ option_all = 2
+ default = 2
+
+
+@dataclass
+class MLSSOptions(PerGameCommonOptions):
+ start_inventory_from_pool: StartInventoryPool
+ coins: Coins
+ difficult_logic: DifficultLogic
+ castle_skip: BowsersCastleSkip
+ extra_pipes: ExtraPipes
+ skip_minecart: SkipMinecart
+ disable_surf: DisableSurf
+ harhalls_pants: HarhallsPants
+ block_visibility: HiddenVisible
+ chuckle_beans: ChuckleBeans
+ music_options: MusicOptions
+ randomize_sounds: RandomSounds
+ randomize_enemies: RandomizeEnemies
+ randomize_bosses: RandomizeBosses
+ randomize_backgrounds: RandomizeBackgrounds
+ scale_stats: ScaleStats
+ xp_multiplier: XPMultiplier
+ tattle_hp: TattleHp
+ mario_color: MarioColor
+ luigi_color: LuigiColor
+ mario_pants: MarioPants
+ luigi_pants: LuigiPants
diff --git a/worlds/mlss/Regions.py b/worlds/mlss/Regions.py
new file mode 100644
index 000000000000..992e99e2c7f7
--- /dev/null
+++ b/worlds/mlss/Regions.py
@@ -0,0 +1,320 @@
+import typing
+
+from BaseClasses import Region, Entrance
+from .Locations import (
+ MLSSLocation,
+ mainArea,
+ chucklehuck,
+ castleTown,
+ startingFlag,
+ chuckolatorFlag,
+ piranhaFlag,
+ kidnappedFlag,
+ beanstarFlag,
+ birdoFlag,
+ surfable,
+ hooniversity,
+ gwarharEntrance,
+ gwarharMain,
+ fungitown,
+ fungitownBeanstar,
+ fungitownBirdo,
+ teeheeValley,
+ winkle,
+ sewers,
+ airport,
+ bowsers,
+ bowsersMini,
+ jokesEntrance,
+ jokesMain,
+ theater,
+ booStatue,
+ oasis,
+ postJokes,
+ baseUltraRocks,
+ coins,
+)
+from . import StateLogic
+
+if typing.TYPE_CHECKING:
+ from . import MLSSWorld
+
+
+def create_regions(world: "MLSSWorld", excluded: typing.List[str]):
+ menu_region = Region("Menu", world.player, world.multiworld)
+ world.multiworld.regions.append(menu_region)
+
+ create_region(world, "Main Area", mainArea, excluded)
+ create_region(world, "Chucklehuck Woods", chucklehuck, excluded)
+ create_region(world, "Beanbean Castle Town", castleTown, excluded)
+ create_region(world, "Shop Starting Flag", startingFlag, excluded)
+ create_region(world, "Shop Chuckolator Flag", chuckolatorFlag, excluded)
+ create_region(world, "Shop Mom Piranha Flag", piranhaFlag, excluded)
+ create_region(world, "Shop Enter Fungitown Flag", kidnappedFlag, excluded)
+ create_region(world, "Shop Beanstar Complete Flag", beanstarFlag, excluded)
+ create_region(world, "Shop Birdo Flag", birdoFlag, excluded)
+ create_region(world, "Surfable", surfable, excluded)
+ create_region(world, "Hooniversity", hooniversity, excluded)
+ create_region(world, "GwarharEntrance", gwarharEntrance, excluded)
+ create_region(world, "GwarharMain", gwarharMain, excluded)
+ create_region(world, "TeeheeValley", teeheeValley, excluded)
+ create_region(world, "Winkle", winkle, excluded)
+ create_region(world, "Sewers", sewers, excluded)
+ create_region(world, "Airport", airport, excluded)
+ create_region(world, "JokesEntrance", jokesEntrance, excluded)
+ create_region(world, "JokesMain", jokesMain, excluded)
+ create_region(world, "PostJokes", postJokes, excluded)
+ create_region(world, "Theater", theater, excluded)
+ create_region(world, "Fungitown", fungitown, excluded)
+ create_region(world, "Fungitown Shop Beanstar Complete Flag", fungitownBeanstar, excluded)
+ create_region(world, "Fungitown Shop Birdo Flag", fungitownBirdo, excluded)
+ create_region(world, "BooStatue", booStatue, excluded)
+ create_region(world, "Oasis", oasis, excluded)
+ create_region(world, "BaseUltraRocks", baseUltraRocks, excluded)
+
+ if world.options.coins:
+ create_region(world, "Coins", coins, excluded)
+
+ if not world.options.castle_skip:
+ create_region(world, "Bowser's Castle", bowsers, excluded)
+ create_region(world, "Bowser's Castle Mini", bowsersMini, excluded)
+
+
+def connect_regions(world: "MLSSWorld"):
+ names: typing.Dict[str, int] = {}
+
+ connect(world, names, "Menu", "Main Area")
+ if world.options.coins:
+ connect(world, names, "Main Area", "Coins")
+ connect(world, names, "Main Area", "BaseUltraRocks", lambda state: StateLogic.ultra(state, world.player))
+ connect(world, names, "Main Area", "Chucklehuck Woods", lambda state: StateLogic.brooch(state, world.player))
+ connect(world, names, "Main Area", "BooStatue", lambda state: StateLogic.canCrash(state, world.player))
+ connect(
+ world,
+ names,
+ "Main Area",
+ "Hooniversity",
+ lambda state: StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player),
+ )
+ connect(world, names, "Hooniversity", "Oasis")
+ connect(
+ world,
+ names,
+ "Main Area",
+ "TeeheeValley",
+ lambda state: StateLogic.super(state, world.player) or StateLogic.canDash(state, world.player),
+ )
+ connect(
+ world,
+ names,
+ "TeeheeValley",
+ "GwarharEntrance",
+ lambda state: StateLogic.membership(state, world.player) and StateLogic.fire(state, world.player),
+ )
+ connect(
+ world,
+ names,
+ "TeeheeValley",
+ "Oasis",
+ lambda state: StateLogic.membership(state, world.player) and StateLogic.fire(state, world.player),
+ )
+ connect(
+ world,
+ names,
+ "TeeheeValley",
+ "Fungitown",
+ lambda state: StateLogic.thunder(state, world.player)
+ and StateLogic.castleTown(state, world.player)
+ and StateLogic.rose(state, world.player),
+ )
+ connection = connect(
+ world,
+ names,
+ "Fungitown",
+ "Fungitown Shop Beanstar Complete Flag",
+ lambda state: StateLogic.pieces(state, world.player) or StateLogic.fungitown_birdo_shop(state, world.player),
+ True,
+ )
+ world.multiworld.register_indirect_condition(world.get_region("Fungitown Shop Birdo Flag"), connection)
+ connect(world, names, "Main Area", "Shop Starting Flag")
+ connection = connect(
+ world,
+ names,
+ "Shop Starting Flag",
+ "Shop Chuckolator Flag",
+ lambda state: (
+ StateLogic.brooch(state, world.player)
+ and StateLogic.fruits(state, world.player)
+ and (
+ StateLogic.thunder(state, world.player)
+ or StateLogic.fire(state, world.player)
+ or StateLogic.hammers(state, world.player)
+ )
+ )
+ or (
+ StateLogic.piranha_shop(state, world.player)
+ or StateLogic.fungitown_shop(state, world.player)
+ or StateLogic.star_shop(state, world.player)
+ or StateLogic.birdo_shop(state, world.player)
+ ),
+ True,
+ )
+ world.multiworld.register_indirect_condition(world.get_region("Shop Mom Piranha Flag"), connection)
+ world.multiworld.register_indirect_condition(world.get_region("Shop Enter Fungitown Flag"), connection)
+ world.multiworld.register_indirect_condition(world.get_region("Shop Beanstar Complete Flag"), connection)
+ world.multiworld.register_indirect_condition(world.get_region("Shop Birdo Flag"), connection)
+ connection = connect(
+ world,
+ names,
+ "Shop Starting Flag",
+ "Shop Mom Piranha Flag",
+ lambda state: StateLogic.thunder(state, world.player)
+ or (
+ StateLogic.fungitown_shop(state, world.player)
+ or StateLogic.star_shop(state, world.player)
+ or StateLogic.birdo_shop(state, world.player)
+ ),
+ True,
+ )
+ world.multiworld.register_indirect_condition(world.get_region("Shop Enter Fungitown Flag"), connection)
+ world.multiworld.register_indirect_condition(world.get_region("Shop Beanstar Complete Flag"), connection)
+ world.multiworld.register_indirect_condition(world.get_region("Shop Birdo Flag"), connection)
+ connection = connect(
+ world,
+ names,
+ "Shop Starting Flag",
+ "Shop Enter Fungitown Flag",
+ lambda state: StateLogic.fungitown(state, world.player)
+ or (StateLogic.star_shop(state, world.player) or StateLogic.birdo_shop(state, world.player)),
+ True,
+ )
+ world.multiworld.register_indirect_condition(world.get_region("Shop Beanstar Complete Flag"), connection)
+ world.multiworld.register_indirect_condition(world.get_region("Shop Birdo Flag"), connection)
+ connection = connect(
+ world,
+ names,
+ "Shop Starting Flag",
+ "Shop Beanstar Complete Flag",
+ lambda state: (
+ StateLogic.castleTown(state, world.player)
+ and StateLogic.pieces(state, world.player)
+ and StateLogic.rose(state, world.player)
+ )
+ or StateLogic.birdo_shop(state, world.player),
+ True,
+ )
+ world.multiworld.register_indirect_condition(world.get_region("Shop Birdo Flag"), connection)
+ connect(world, names, "Main Area", "Sewers", lambda state: StateLogic.rose(state, world.player))
+ connect(world, names, "Main Area", "Airport", lambda state: StateLogic.thunder(state, world.player))
+ connect(world, names, "Main Area", "Theater", lambda state: StateLogic.canDash(state, world.player))
+ connect(world, names, "Main Area", "Surfable", lambda state: StateLogic.surfable(state, world.player))
+ connect(world, names, "Surfable", "GwarharEntrance")
+ connect(world, names, "Surfable", "Oasis")
+ connect(world, names, "Surfable", "JokesEntrance", lambda state: StateLogic.fire(state, world.player))
+ connect(world, names, "JokesMain", "PostJokes", lambda state: StateLogic.postJokes(state, world.player))
+ if not world.options.castle_skip:
+ connect(world, names, "PostJokes", "Bowser's Castle")
+ connect(
+ world,
+ names,
+ "Bowser's Castle",
+ "Bowser's Castle Mini",
+ lambda state: StateLogic.canMini(state, world.player) and StateLogic.thunder(state, world.player),
+ )
+ connect(world, names, "Chucklehuck Woods", "Winkle", lambda state: StateLogic.canDash(state, world.player))
+ connect(
+ world,
+ names,
+ "Chucklehuck Woods",
+ "Beanbean Castle Town",
+ lambda state: StateLogic.fruits(state, world.player)
+ and (
+ StateLogic.hammers(state, world.player)
+ or StateLogic.fire(state, world.player)
+ or StateLogic.thunder(state, world.player)
+ ),
+ )
+ if world.options.difficult_logic:
+ connect(world, names, "GwarharEntrance", "GwarharMain", lambda state: StateLogic.canDash(state, world.player))
+ connect(world, names, "JokesEntrance", "JokesMain", lambda state: StateLogic.canDig(state, world.player))
+ connect(
+ world,
+ names,
+ "Shop Starting Flag",
+ "Shop Birdo Flag",
+ lambda state: StateLogic.postJokes(state, world.player),
+ )
+ connect(
+ world,
+ names,
+ "Fungitown",
+ "Fungitown Shop Birdo Flag",
+ lambda state: StateLogic.postJokes(state, world.player),
+ )
+ else:
+ connect(
+ world,
+ names,
+ "GwarharEntrance",
+ "GwarharMain",
+ lambda state: StateLogic.canDash(state, world.player) and StateLogic.canCrash(state, world.player),
+ )
+ connect(
+ world,
+ names,
+ "JokesEntrance",
+ "JokesMain",
+ lambda state: StateLogic.canCrash(state, world.player) and StateLogic.canDig(state, world.player),
+ )
+ connect(
+ world,
+ names,
+ "Shop Starting Flag",
+ "Shop Birdo Flag",
+ lambda state: StateLogic.canCrash(state, world.player) and StateLogic.postJokes(state, world.player),
+ )
+ connect(
+ world,
+ names,
+ "Fungitown",
+ "Fungitown Shop Birdo Flag",
+ lambda state: StateLogic.canCrash(state, world.player) and StateLogic.postJokes(state, world.player),
+ )
+
+
+def create_region(world: "MLSSWorld", name, locations, excluded):
+ ret = Region(name, world.player, world.multiworld)
+ for location in locations:
+ loc = MLSSLocation(world.player, location.name, location.id, ret)
+ if location.name in excluded:
+ continue
+ ret.locations.append(loc)
+ world.multiworld.regions.append(ret)
+
+
+def connect(
+ world: "MLSSWorld",
+ used_names: typing.Dict[str, int],
+ source: str,
+ target: str,
+ rule: typing.Optional[typing.Callable] = None,
+ reach: typing.Optional[bool] = False,
+) -> typing.Optional[Entrance]:
+ source_region = world.multiworld.get_region(source, world.player)
+ target_region = world.multiworld.get_region(target, world.player)
+
+ if target not in used_names:
+ used_names[target] = 1
+ name = target
+ else:
+ used_names[target] += 1
+ name = target + (" " * used_names[target])
+
+ connection = Entrance(world.player, name, source_region)
+
+ if rule:
+ connection.access_rule = rule
+
+ source_region.exits.append(connection)
+ connection.connect(target_region)
+ return connection if reach else None
diff --git a/worlds/mlss/Rom.py b/worlds/mlss/Rom.py
new file mode 100644
index 000000000000..7cbbe8875195
--- /dev/null
+++ b/worlds/mlss/Rom.py
@@ -0,0 +1,433 @@
+import io
+import json
+import random
+
+from . import Data
+from typing import TYPE_CHECKING, Optional
+from BaseClasses import Item, Location
+from settings import get_settings
+from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension
+from .Items import item_table
+from .Locations import shop, badge, pants, location_table, hidden, all_locations
+
+if TYPE_CHECKING:
+ from . import MLSSWorld
+
+colors = [
+ Data.redHat,
+ Data.greenHat,
+ Data.blueHat,
+ Data.azureHat,
+ Data.yellowHat,
+ Data.orangeHat,
+ Data.purpleHat,
+ Data.pinkHat,
+ Data.blackHat,
+ Data.whiteHat,
+ Data.silhouetteHat,
+ Data.chaosHat,
+ Data.truechaosHat
+]
+
+cpants = [
+ Data.vanilla,
+ Data.redPants,
+ Data.greenPants,
+ Data.bluePants,
+ Data.azurePants,
+ Data.yellowPants,
+ Data.orangePants,
+ Data.purplePants,
+ Data.pinkPants,
+ Data.blackPants,
+ Data.whitePants,
+ Data.chaosPants
+]
+
+
+def get_base_rom_as_bytes() -> bytes:
+ with open(get_settings().mlss_options.rom_file, "rb") as infile:
+ base_rom_bytes = bytes(infile.read())
+ return base_rom_bytes
+
+
+class MLSSPatchExtension(APPatchExtension):
+ game = "Mario & Luigi Superstar Saga"
+
+ @staticmethod
+ def randomize_music(caller: APProcedurePatch, rom: bytes):
+ options = json.loads(caller.get_file("options.json").decode("UTF-8"))
+ if options["music_options"] != 1:
+ return rom
+ stream = io.BytesIO(rom)
+ random.seed(options["seed"] + options["player"])
+
+ songs = []
+ stream.seek(0x21CB74)
+ for _ in range(50):
+ if stream.tell() == 0x21CBD8:
+ stream.seek(4, 1)
+ continue
+ temp = stream.read(4)
+ songs.append(temp)
+
+ random.shuffle(songs)
+ stream.seek(0x21CB74)
+ for _ in range(50):
+ if stream.tell() == 0x21CBD8:
+ stream.seek(4, 1)
+ continue
+ stream.write(songs.pop())
+
+ return stream.getvalue()
+
+ @staticmethod
+ def hidden_visible(caller: APProcedurePatch, rom: bytes):
+ options = json.loads(caller.get_file("options.json").decode("UTF-8"))
+ if options["block_visibility"] == 0:
+ return rom
+ stream = io.BytesIO(rom)
+
+ for location in all_locations:
+ stream.seek(location.id - 6)
+ b = stream.read(1)
+ if b[0] == 0x10 and options["block_visibility"] == 1:
+ stream.seek(location.id - 6)
+ stream.write(bytes([0x0]))
+ if b[0] == 0x0 and options["block_visibility"] == 2:
+ stream.seek(location.id - 6)
+ stream.write(bytes([0x10]))
+
+ return stream.getvalue()
+
+ @staticmethod
+ def randomize_sounds(caller: APProcedurePatch, rom: bytes):
+ options = json.loads(caller.get_file("options.json").decode("UTF-8"))
+ if options["randomize_sounds"] != 1:
+ return rom
+ stream = io.BytesIO(rom)
+ random.seed(options["seed"] + options["player"])
+ fresh_pointers = Data.sounds
+ pointers = Data.sounds
+
+ random.shuffle(pointers)
+ stream.seek(0x21CC44, 0)
+ for i in range(354):
+ current_position = stream.tell()
+ value = int.from_bytes(stream.read(3), "little")
+ if value in fresh_pointers:
+ stream.seek(current_position)
+ stream.write(pointers.pop().to_bytes(3, "little"))
+ stream.seek(1, 1)
+
+ return stream.getvalue()
+
+ @staticmethod
+ def enemy_randomize(caller: APProcedurePatch, rom: bytes):
+ options = json.loads(caller.get_file("options.json").decode("UTF-8"))
+ if options["randomize_bosses"] == 0 and options["randomize_enemies"] == 0:
+ return rom
+
+ enemies = [pos for pos in Data.enemies if pos not in Data.bowsers] if options["castle_skip"] else Data.enemies
+ bosses = [pos for pos in Data.bosses if pos not in Data.bowsers] if options["castle_skip"] else Data.bosses
+ stream = io.BytesIO(rom)
+ random.seed(options["seed"] + options["player"])
+
+ if options["randomize_bosses"] == 1 or (options["randomize_bosses"] == 2) and options["randomize_enemies"] == 0:
+ raw = []
+ for pos in bosses:
+ stream.seek(pos + 1)
+ raw += [stream.read(0x1F)]
+ random.shuffle(raw)
+ for pos in bosses:
+ stream.seek(pos + 1)
+ stream.write(raw.pop())
+
+ if options["randomize_enemies"] == 1:
+ raw = []
+ for pos in enemies:
+ stream.seek(pos + 1)
+ raw += [stream.read(0x1F)]
+ if options["randomize_bosses"] == 2:
+ for pos in bosses:
+ stream.seek(pos + 1)
+ raw += [stream.read(0x1F)]
+ random.shuffle(raw)
+ for pos in enemies:
+ stream.seek(pos + 1)
+ stream.write(raw.pop())
+ if options["randomize_bosses"] == 2:
+ for pos in bosses:
+ stream.seek(pos + 1)
+ stream.write(raw.pop())
+ return stream.getvalue()
+
+ enemies_raw = []
+ groups = []
+
+ if options["randomize_enemies"] == 0:
+ return stream.getvalue()
+
+ if options["randomize_bosses"] == 2:
+ for pos in bosses:
+ stream.seek(pos + 1)
+ groups += [stream.read(0x1F)]
+
+ for pos in enemies:
+ stream.seek(pos + 8)
+ for _ in range(6):
+ enemy = int.from_bytes(stream.read(1))
+ if enemy > 0:
+ stream.seek(1, 1)
+ flag = int.from_bytes(stream.read(1))
+ if flag == 0x7:
+ break
+ if flag in [0x0, 0x2, 0x4]:
+ if enemy not in Data.pestnut and enemy not in Data.flying:
+ enemies_raw += [enemy]
+ stream.seek(1, 1)
+ else:
+ stream.seek(3, 1)
+
+ random.shuffle(enemies_raw)
+ chomp = False
+ for pos in enemies:
+ stream.seek(pos + 8)
+
+ for _ in range(6):
+ enemy = int.from_bytes(stream.read(1))
+ if enemy > 0 and enemy not in Data.flying and enemy not in Data.pestnut:
+ if enemy == 0x52:
+ chomp = True
+ stream.seek(1, 1)
+ flag = int.from_bytes(stream.read(1))
+ if flag not in [0x0, 0x2, 0x4]:
+ stream.seek(1, 1)
+ continue
+ stream.seek(-3, 1)
+ stream.write(bytes([enemies_raw.pop()]))
+ stream.seek(1, 1)
+ stream.write(bytes([0x6]))
+ stream.seek(1, 1)
+ else:
+ stream.seek(3, 1)
+
+ stream.seek(pos + 1)
+ raw = stream.read(0x1F)
+ if chomp:
+ raw = raw[0:3] + bytes([0x67, 0xAB, 0x28, 0x08]) + raw[7:]
+ else:
+ raw = raw[0:3] + bytes([0xEE, 0x2C, 0x28, 0x08]) + raw[7:]
+ groups += [raw]
+ chomp = False
+
+ random.shuffle(groups)
+ arr = enemies
+ if options["randomize_bosses"] == 2:
+ arr += bosses
+
+ for pos in arr:
+ stream.seek(pos + 1)
+ stream.write(groups.pop())
+
+ return stream.getvalue()
+
+
+class MLSSProcedurePatch(APProcedurePatch, APTokenMixin):
+ game = "Mario & Luigi Superstar Saga"
+ hash = "4b1a5897d89d9e74ec7f630eefdfd435"
+ patch_file_ending = ".apmlss"
+ result_file_ending = ".gba"
+
+ procedure = [
+ ("apply_bsdiff4", ["base_patch.bsdiff4"]),
+ ("apply_tokens", ["token_data.bin"]),
+ ("enemy_randomize", []),
+ ("hidden_visible", []),
+ ("randomize_sounds", []),
+ ("randomize_music", []),
+ ]
+
+ @classmethod
+ def get_source_data(cls) -> bytes:
+ return get_base_rom_as_bytes()
+
+
+def write_tokens(world: "MLSSWorld", patch: MLSSProcedurePatch) -> None:
+ options_dict = {
+ "randomize_enemies": world.options.randomize_enemies.value,
+ "randomize_bosses": world.options.randomize_bosses.value,
+ "castle_skip": world.options.castle_skip.value,
+ "randomize_sounds": world.options.randomize_sounds.value,
+ "music_options": world.options.music_options.value,
+ "block_visibility": world.options.block_visibility.value,
+ "seed": world.multiworld.seed,
+ "player": world.player,
+ }
+ patch.write_file("options.json", json.dumps(options_dict).encode("UTF-8"))
+
+ # Bake player name into ROM
+ patch.write_token(APTokenTypes.WRITE, 0xDF0000, world.multiworld.player_name[world.player].encode("UTF-8"))
+
+ # Bake seed name into ROM
+ patch.write_token(APTokenTypes.WRITE, 0xDF00A0, world.multiworld.seed_name.encode("UTF-8"))
+
+ # Intro Skip
+ patch.write_token(
+ APTokenTypes.WRITE,
+ 0x244D08,
+ bytes([0x88, 0x0, 0x19, 0x91, 0x1, 0x20, 0x58, 0x1, 0xF, 0xA0, 0x3, 0x15, 0x27, 0x8]),
+ )
+
+ # Patch S.S Chuckola Loading Zones
+ patch.write_token(APTokenTypes.WRITE, 0x25FD4E, bytes([0x48, 0x30, 0x80, 0x60, 0x50, 0x2, 0xF]))
+
+ patch.write_token(APTokenTypes.WRITE, 0x25FD83, bytes([0x48, 0x30, 0x80, 0x60, 0xC0, 0x2, 0xF]))
+
+ patch.write_token(APTokenTypes.WRITE, 0x25FDB8, bytes([0x48, 0x30, 0x05, 0x80, 0xE4, 0x0, 0xF]))
+
+ patch.write_token(APTokenTypes.WRITE, 0x25FDED, bytes([0x48, 0x30, 0x06, 0x80, 0xE4, 0x0, 0xF]))
+
+ patch.write_token(APTokenTypes.WRITE, 0x25FE22, bytes([0x48, 0x30, 0x07, 0x80, 0xE4, 0x0, 0xF]))
+
+ patch.write_token(APTokenTypes.WRITE, 0x25FE57, bytes([0x48, 0x30, 0x08, 0x80, 0xE4, 0x0, 0xF]))
+
+ if world.options.extra_pipes:
+ patch.write_token(APTokenTypes.WRITE, 0xD00001, bytes([0x1]))
+
+ if world.options.castle_skip:
+ patch.write_token(APTokenTypes.WRITE, 0x3AEAB0, bytes([0xC1, 0x67, 0x0, 0x6, 0x1C, 0x08, 0x3]))
+ patch.write_token(APTokenTypes.WRITE, 0x3AEC18, bytes([0x89, 0x65, 0x0, 0xE, 0xA, 0x08, 0x1]))
+
+ if world.options.skip_minecart:
+ patch.write_token(APTokenTypes.WRITE, 0x3AC728, bytes([0x89, 0x13, 0x0, 0x10, 0xF, 0x08, 0x1]))
+ patch.write_token(APTokenTypes.WRITE, 0x3AC56C, bytes([0x49, 0x16, 0x0, 0x8, 0x8, 0x08, 0x1]))
+
+ if world.options.scale_stats:
+ patch.write_token(APTokenTypes.WRITE, 0xD00002, bytes([0x1]))
+
+ patch.write_token(APTokenTypes.WRITE, 0xD00003, bytes([world.options.xp_multiplier.value]))
+
+ if world.options.tattle_hp:
+ patch.write_token(APTokenTypes.WRITE, 0xD00000, bytes([0x1]))
+
+ if world.options.music_options == 2:
+ patch.write_token(APTokenTypes.WRITE, 0x19B118, bytes([0x0, 0x25]))
+
+ if world.options.randomize_backgrounds:
+ all_enemies = Data.enemies + Data.bosses
+ for address in all_enemies:
+ patch.write_token(APTokenTypes.WRITE, address + 3, bytes([world.random.randint(0x0, 0x26)]))
+
+ for location_name in location_table.keys():
+ if (
+ (world.options.skip_minecart and "Minecart" in location_name and "After" not in location_name)
+ or (world.options.castle_skip and "Bowser" in location_name)
+ or (world.options.disable_surf and "Surf Minigame" in location_name)
+ or (world.options.harhalls_pants and "Harhall's" in location_name)
+ ):
+ continue
+ if (world.options.chuckle_beans == 0 and "Digspot" in location_name) or (
+ world.options.chuckle_beans == 1 and location_table[location_name] in hidden
+ ):
+ continue
+ if not world.options.coins and "Coin" in location_name:
+ continue
+ location = world.multiworld.get_location(location_name, world.player)
+ item = location.item
+ address = [address for address in all_locations if address.name == location.name]
+ item_inject(world, patch, location.address, address[0].itemType, item)
+ if "Shop" in location_name and "Coffee" not in location_name and item.player != world.player:
+ desc_inject(world, patch, location, item)
+
+ swap_colors(world, patch, world.options.mario_pants.value, 0, True)
+ swap_colors(world, patch, world.options.luigi_pants.value, 1, True)
+ swap_colors(world, patch, world.options.mario_color.value, 0)
+ swap_colors(world, patch, world.options.luigi_color.value, 1)
+
+ patch.write_file("token_data.bin", patch.get_token_binary())
+
+
+def swap_colors(world: "MLSSWorld", patch: MLSSProcedurePatch, color: int, bro: int,
+ pants_option: Optional[bool] = False):
+ if not pants_option and color == bro:
+ return
+ chaos = False
+ if not pants_option and color == 11 or color == 12:
+ chaos = True
+ if pants_option and color == 11:
+ chaos = True
+ for c in [c for c in (cpants[color] if pants_option else colors[color])
+ if (c[3] == bro if not chaos else c[1] == bro)]:
+ if chaos:
+ patch.write_token(APTokenTypes.WRITE, c[0],
+ bytes([world.random.randint(0, 255), world.random.randint(0, 127)]))
+ else:
+ patch.write_token(APTokenTypes.WRITE, c[0], bytes([c[1], c[2]]))
+
+
+def item_inject(world: "MLSSWorld", patch: MLSSProcedurePatch, location: int, item_type: int, item: Item):
+ if item.player == world.player:
+ code = item_table[item.name].itemID
+ else:
+ code = 0x3F
+ if item_type == 0:
+ patch.write_token(APTokenTypes.WRITE, location, bytes([code]))
+ elif item_type == 1:
+ if code == 0x1D or code == 0x1E:
+ code += 0xE
+ if 0x20 <= code <= 0x26:
+ code -= 0x4
+ insert = int(code)
+ insert2 = insert % 0x10
+ insert2 *= 0x10
+ insert //= 0x10
+ insert += 0x20
+ patch.write_token(APTokenTypes.WRITE, location, bytes([insert, insert2]))
+ elif item_type == 2:
+ if code == 0x1D or code == 0x1E:
+ code += 0xE
+ if 0x20 <= code <= 0x26:
+ code -= 0x4
+ patch.write_token(APTokenTypes.WRITE, location, bytes([code]))
+ elif item_type == 3:
+ if code == 0x1D or code == 0x1E:
+ code += 0xE
+ if code < 0x1D:
+ code -= 0xA
+ if 0x20 <= code <= 0x26:
+ code -= 0xE
+ patch.write_token(APTokenTypes.WRITE, location, bytes([code]))
+ else:
+ patch.write_token(APTokenTypes.WRITE, location, bytes([0x18]))
+
+
+def desc_inject(world: "MLSSWorld", patch: MLSSProcedurePatch, location: Location, item: Item):
+ index = -1
+ for key, value in shop.items():
+ if location.address in value:
+ if key == 0x3C05F0:
+ index = value.index(location.address)
+ else:
+ index = value.index(location.address) + 14
+
+ for key, value in badge.items():
+ if index != -1:
+ break
+ if location.address in value:
+ if key == 0x3C0618:
+ index = value.index(location.address) + 24
+ else:
+ index = value.index(location.address) + 41
+
+ for key, value in pants.items():
+ if index != -1:
+ break
+ if location.address in value:
+ if key == 0x3C0618:
+ index = value.index(location.address) + 48
+ else:
+ index = value.index(location.address) + 66
+
+ dstring = f"{world.multiworld.player_name[item.player]}: {item.name}"
+ patch.write_token(APTokenTypes.WRITE, 0xD11000 + (index * 0x40), dstring.encode("UTF8"))
diff --git a/worlds/mlss/Rules.py b/worlds/mlss/Rules.py
new file mode 100644
index 000000000000..13627eafc479
--- /dev/null
+++ b/worlds/mlss/Rules.py
@@ -0,0 +1,571 @@
+import typing
+
+from worlds.generic.Rules import add_rule, forbid_item
+from .Names.LocationName import LocationName
+from .Locations import all_locations, hidden
+from . import StateLogic
+
+if typing.TYPE_CHECKING:
+ from . import MLSSWorld
+
+
+def set_rules(world: "MLSSWorld", excluded):
+ for location in all_locations:
+ if "Digspot" in location.name:
+ if (world.options.skip_minecart and "Minecart" in location.name) or (
+ world.options.castle_skip and "Bowser" in location.name
+ ):
+ continue
+ if world.options.chuckle_beans == 0 or world.options.chuckle_beans == 1 and location.id in hidden:
+ continue
+ add_rule(
+ world.get_location(location.name),
+ lambda state: StateLogic.canDig(state, world.player),
+ )
+ if "Beanstone" in location.name:
+ add_rule(
+ world.get_location(location.name),
+ lambda state: StateLogic.canDig(state, world.player),
+ )
+ if "Shop" in location.name and "Coffee" not in location.name and location.name not in excluded:
+ forbid_item(world.get_location(location.name), "Hammers", world.player)
+ if "Badge" in location.name or "Pants" in location.name:
+ add_rule(
+ world.get_location(location.name),
+ lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
+ )
+ if location.itemType != 0 and location.name not in excluded:
+ if "Bowser" in location.name and world.options.castle_skip:
+ continue
+ forbid_item(world.get_location(location.name), "5 Coins", world.player)
+
+ if world.options.chuckle_beans == 2:
+ add_rule(
+ world.get_location(LocationName.HoohooVillageSuperHammerCaveDigspot),
+ lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsFarmRoomDigspot2),
+ lambda state: StateLogic.thunder(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsFarmRoomDigspot3),
+ lambda state: StateLogic.thunder(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsWhiteFruitRoomDigspot3),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.JokesEndJojoraRoomDigspot),
+ lambda state: StateLogic.canDash(state, world.player),
+ )
+
+ if world.options.chuckle_beans != 0:
+ add_rule(
+ world.get_location(LocationName.HoohooMountainBaseBoostatueRoomDigspot2),
+ lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsFarmRoomDigspot1),
+ lambda state: StateLogic.thunder(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsWhiteFruitRoomDigspot2),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.TeeheeValleyPastUltraHammersDigspot1),
+ lambda state: StateLogic.ultra(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.TeeheeValleyPastUltraHammersDigspot3),
+ lambda state: StateLogic.ultra(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsNorthBeachDigspot3),
+ lambda state: StateLogic.canDash(state, world.player) or StateLogic.super(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsEDigspot2),
+ lambda state: StateLogic.thunder(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsNEDigspot1),
+ lambda state: StateLogic.thunder(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsSRoom1Digspot2),
+ lambda state: StateLogic.ultra(state, world.player) and StateLogic.thunder(state, world.player),
+ )
+
+ forbid_item(
+ world.get_location(LocationName.SSChuckolaMembershipCard), "Nuts", world.player
+ ) # Bandaid Fix
+
+ add_rule(
+ world.get_location(LocationName.HoohooVillageHammerHouseBlock),
+ lambda state: StateLogic.hammers(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.HoohooMountainBaseBoostatueRoomBlock2),
+ lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsBooStatueMole),
+ lambda state: StateLogic.canMini(state, world.player) and StateLogic.canDig(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.HoohooVillageSuperHammerCaveBlock),
+ lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsFarmRoomMoleReward1),
+ lambda state: StateLogic.thunder(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsFarmRoomMoleReward2),
+ lambda state: StateLogic.thunder(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsThunderHandMole),
+ lambda state: StateLogic.thunder(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsNWBlock),
+ lambda state: StateLogic.super(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsBeanFruit1),
+ lambda state: StateLogic.canDig(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsBeanFruit2),
+ lambda state: StateLogic.canDig(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsBeanFruit3),
+ lambda state: StateLogic.canDig(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsBeanFruit4),
+ lambda state: StateLogic.super(state, world.player) and StateLogic.canDig(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsBeanFruit5),
+ lambda state: StateLogic.super(state, world.player) and StateLogic.canDig(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsBeanFruit6),
+ lambda state: StateLogic.canDig(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsBeanFruit7),
+ lambda state: StateLogic.teehee(state, world.player) and StateLogic.canDig(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsSRoom1Block),
+ lambda state: StateLogic.ultra(state, world.player) and StateLogic.thunder(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsSRoom2Block1),
+ lambda state: StateLogic.canDig(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.WoohooHooniversityMiniMarioPuzzleSecretAreaBlock1),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.WoohooHooniversityMiniMarioPuzzleSecretAreaBlock2),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.WoohooHooniversityMiniMarioPuzzleSecretAreaBlock3),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.WoohooHooniversityMiniMarioPuzzleSecretAreaBlock4),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.WoohooHooniversityMiniMarioPuzzleBlock),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsSecretScroll1),
+ lambda state: StateLogic.thunder(state, world.player) and StateLogic.super(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsSecretScroll2),
+ lambda state: StateLogic.thunder(state, world.player) and StateLogic.ultra(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.HoohooVillageMoleBehindTurtle),
+ lambda state: StateLogic.canDash(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsNESoloMarioMole1),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsNESoloMarioMole2),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsSuperHammerUpgrade),
+ lambda state: StateLogic.thunder(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsUltraHammerUpgrade),
+ lambda state: StateLogic.thunder(state, world.player)
+ and StateLogic.pieces(state, world.player)
+ and StateLogic.castleTown(state, world.player)
+ and StateLogic.rose(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsSoloLuigiCaveMole),
+ lambda state: StateLogic.canDig(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsRedChuckolaFruit),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsWhiteChuckolaFruit),
+ lambda state: StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock1),
+ lambda state: StateLogic.fruits(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock2),
+ lambda state: StateLogic.fruits(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock3),
+ lambda state: StateLogic.fruits(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock4),
+ lambda state: StateLogic.fruits(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock5),
+ lambda state: StateLogic.fruits(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock6),
+ lambda state: StateLogic.fruits(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsRoom7Block1),
+ lambda state: StateLogic.hammers(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsRoom7Block2),
+ lambda state: StateLogic.hammers(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsRoom4Block1),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsRoom4Block2),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsRoom4Block3),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsPipeRoomBlock1),
+ lambda state: StateLogic.hammers(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsPipeRoomBlock2),
+ lambda state: StateLogic.hammers(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanCastleTownMiniMarioBlock1),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanCastleTownMiniMarioBlock2),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanCastleTownMiniMarioBlock3),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanCastleTownMiniMarioBlock4),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanCastleTownMiniMarioBlock5),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanCastleFakeBeastar),
+ lambda state: StateLogic.pieces(state, world.player) and StateLogic.rose(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanCastlePeachsExtraDress),
+ lambda state: StateLogic.pieces(state, world.player) and StateLogic.rose(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.SewersRoom5Block1),
+ lambda state: StateLogic.hammers(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.SewersRoom5Block2),
+ lambda state: StateLogic.hammers(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.GwarharLagoonFirstUnderwaterAreaRoom1Block),
+ lambda state: StateLogic.canDash(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.GwarharLagoonFirstUnderwaterAreaRoom2Block1),
+ lambda state: StateLogic.canDash(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.GwarharLagoonFirstUnderwaterAreaRoom2Block2),
+ lambda state: StateLogic.canDash(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.GwarharLagoonRedPearlBean),
+ lambda state: StateLogic.fire(state, world.player) and StateLogic.thunder(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.GwarharLagoonGreenPearlBean),
+ lambda state: StateLogic.fire(state, world.player) and StateLogic.thunder(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.TeeheeValleyPastUltraHammersBlock1),
+ lambda state: StateLogic.ultra(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.TeeheeValleyPastUltraHammersBlock2),
+ lambda state: StateLogic.ultra(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.TeeheeValleySoloLuigiMazeRoom1Block),
+ lambda state: StateLogic.ultra(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.OhoOasisFirebrand),
+ lambda state: StateLogic.canMini(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.OhoOasisThunderhand),
+ lambda state: StateLogic.canDig(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanstarPieceYoshiTheater),
+ lambda state: StateLogic.neon(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.YoshiTheaterAzureYoshi),
+ lambda state: StateLogic.beanFruit(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.YoshiTheaterBlueYoshi),
+ lambda state: StateLogic.beanFruit(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.YoshiTheaterGreenYoshi),
+ lambda state: StateLogic.beanFruit(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.YoshiTheaterOrangeYoshi),
+ lambda state: StateLogic.beanFruit(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.YoshiTheaterPurpleYoshi),
+ lambda state: StateLogic.beanFruit(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.YoshiTheaterRedYoshi),
+ lambda state: StateLogic.beanFruit(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.YoshiTheaterYellowYoshi),
+ lambda state: StateLogic.beanFruit(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.WinkleAreaBeanstarRoomBlock),
+ lambda state: StateLogic.winkle(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanstarPieceWinkleArea),
+ lambda state: StateLogic.winkle(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.GwarharLagoonSpangleReward),
+ lambda state: StateLogic.spangle(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.PantsShopMomPiranhaFlag1),
+ lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.PantsShopMomPiranhaFlag2),
+ lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.PantsShopMomPiranhaFlag3),
+ lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BadgeShopMomPiranhaFlag1),
+ lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BadgeShopMomPiranhaFlag2),
+ lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BadgeShopMomPiranhaFlag3),
+ lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChateauGreenGoblet),
+ lambda state: StateLogic.brooch(state, world.player) and StateLogic.canDig(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChateauRedGoblet),
+ lambda state: StateLogic.brooch(state, world.player) and StateLogic.canMini(state, world.player),
+ )
+
+ add_rule(
+ world.get_location(LocationName.GwarharLagoonSpangle),
+ lambda state: StateLogic.ultra(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.GwarharLagoonSpangleRoomBlock),
+ lambda state: StateLogic.ultra(state, world.player),
+ )
+ if world.options.difficult_logic:
+ add_rule(
+ world.get_location(LocationName.GwarharLagoonSpangleReward),
+ lambda state: StateLogic.canCrash(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanstarPieceHermie),
+ lambda state: StateLogic.canCrash(state, world.player),
+ )
+ if world.options.chuckle_beans != 0:
+ add_rule(
+ world.get_location(LocationName.GwarharLagoonPastHermieDigspot),
+ lambda state: StateLogic.canCrash(state, world.player),
+ )
+
+ if world.options.coins:
+ add_rule(
+ world.get_location(LocationName.HoohooMountainBaseBooStatueCaveCoinBlock1),
+ lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.HoohooMountainBaseBooStatueCaveCoinBlock2),
+ lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.HoohooMountainBaseBooStatueCaveCoinBlock3),
+ lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsNWCoinBlock),
+ lambda state: StateLogic.super(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsSRoom1CoinBlock),
+ lambda state: StateLogic.ultra(state, world.player) and StateLogic.thunder(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.BeanbeanOutskirtsSRoom2CoinBlock),
+ lambda state: StateLogic.canCrash(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChateauPoppleRoomCoinBlock1),
+ lambda state: StateLogic.brooch(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChateauPoppleRoomCoinBlock2),
+ lambda state: StateLogic.brooch(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsCaveRoom1CoinBlock),
+ lambda state: StateLogic.brooch(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsCaveRoom2CoinBlock),
+ lambda state: StateLogic.brooch(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsCaveRoom3CoinBlock),
+ lambda state: StateLogic.brooch(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsPipe5RoomCoinBlock),
+ lambda state: StateLogic.brooch(state, world.player) and StateLogic.hammers(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsRoom7CoinBlock),
+ lambda state: StateLogic.brooch(state, world.player) and StateLogic.hammers(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootCoinBlock),
+ lambda state: StateLogic.brooch(state, world.player) and StateLogic.fruits(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsKoopaRoomCoinBlock),
+ lambda state: StateLogic.brooch(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.ChucklehuckWoodsWinkleAreaCaveCoinBlock),
+ lambda state: StateLogic.brooch(state, world.player) and StateLogic.canDash(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.SewersPrisonRoomCoinBlock),
+ lambda state: StateLogic.rose(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.TeeheeValleyPastUltraHammerRocksCoinBlock),
+ lambda state: StateLogic.ultra(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.SSChuckolaStorageRoomCoinBlock1),
+ lambda state: StateLogic.super(state, world.player) or StateLogic.canDash(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.SSChuckolaStorageRoomCoinBlock2),
+ lambda state: StateLogic.super(state, world.player) or StateLogic.canDash(state, world.player),
+ )
+ add_rule(
+ world.get_location(LocationName.GwarharLagoonFirstUnderwaterAreaRoom2CoinBlock),
+ lambda state: StateLogic.canDash(state, world.player)
+ and (StateLogic.membership(state, world.player) or StateLogic.surfable(state, world.player)),
+ )
+ add_rule(
+ world.get_location(LocationName.JokesEndSecondFloorWestRoomCoinBlock),
+ lambda state: StateLogic.ultra(state, world.player)
+ and StateLogic.fire(state, world.player)
+ and (
+ StateLogic.membership(state, world.player)
+ or (StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player))
+ ),
+ )
+ add_rule(
+ world.get_location(LocationName.JokesEndNorthofBridgeRoomCoinBlock),
+ lambda state: StateLogic.ultra(state, world.player)
+ and StateLogic.fire(state, world.player)
+ and StateLogic.canDig(state, world.player)
+ and (StateLogic.membership(state, world.player) or StateLogic.canMini(state, world.player)),
+ )
+ if not world.options.difficult_logic:
+ add_rule(
+ world.get_location(LocationName.JokesEndNorthofBridgeRoomCoinBlock),
+ lambda state: StateLogic.canCrash(state, world.player),
+ )
diff --git a/worlds/mlss/StateLogic.py b/worlds/mlss/StateLogic.py
new file mode 100644
index 000000000000..39f08e169ef5
--- /dev/null
+++ b/worlds/mlss/StateLogic.py
@@ -0,0 +1,155 @@
+def canDig(state, player):
+ return state.has("Green Goblet", player) and state.has("Hammers", player)
+
+
+def canMini(state, player):
+ return state.has("Red Goblet", player) and state.has("Hammers", player)
+
+
+def canDash(state, player):
+ return state.has("Red Pearl Bean", player) and state.has("Firebrand", player)
+
+
+def canCrash(state, player):
+ return state.has("Green Pearl Bean", player) and state.has("Thunderhand", player)
+
+
+def hammers(state, player):
+ return state.has("Hammers", player)
+
+
+def super(state, player):
+ return state.has("Hammers", player, 2)
+
+
+def ultra(state, player):
+ return state.has("Hammers", player, 3)
+
+
+def fruits(state, player):
+ return (
+ state.has("Red Chuckola Fruit", player)
+ and state.has("Purple Chuckola Fruit", player)
+ and state.has("White Chuckola Fruit", player)
+ )
+
+
+def pieces(state, player):
+ return (
+ state.has("Beanstar Piece 1", player)
+ and state.has("Beanstar Piece 2", player)
+ and state.has("Beanstar Piece 3", player)
+ and state.has("Beanstar Piece 4", player)
+ )
+
+
+def neon(state, player):
+ return (
+ state.has("Blue Neon Egg", player)
+ and state.has("Red Neon Egg", player)
+ and state.has("Green Neon Egg", player)
+ and state.has("Yellow Neon Egg", player)
+ and state.has("Purple Neon Egg", player)
+ and state.has("Orange Neon Egg", player)
+ and state.has("Azure Neon Egg", player)
+ )
+
+
+def spangle(state, player):
+ return state.has("Spangle", player)
+
+
+def rose(state, player):
+ return state.has("Peasley's Rose", player)
+
+
+def brooch(state, player):
+ return state.has("Beanbean Brooch", player)
+
+
+def thunder(state, player):
+ return state.has("Thunderhand", player)
+
+
+def fire(state, player):
+ return state.has("Firebrand", player)
+
+
+def dressBeanstar(state, player):
+ return state.has("Peach's Extra Dress", player) and state.has("Fake Beanstar", player)
+
+
+def membership(state, player):
+ return state.has("Membership Card", player)
+
+
+def winkle(state, player):
+ return state.has("Winkle Card", player)
+
+
+def beanFruit(state, player):
+ return (
+ state.has("Bean Fruit 1", player)
+ and state.has("Bean Fruit 2", player)
+ and state.has("Bean Fruit 3", player)
+ and state.has("Bean Fruit 4", player)
+ and state.has("Bean Fruit 5", player)
+ and state.has("Bean Fruit 6", player)
+ and state.has("Bean Fruit 7", player)
+ )
+
+
+def surfable(state, player):
+ return ultra(state, player) and (
+ (canDig(state, player) and canMini(state, player)) or (membership(state, player) and fire(state, player))
+ )
+
+
+def postJokes(state, player):
+ return (
+ surfable(state, player)
+ and canDig(state, player)
+ and dressBeanstar(state, player)
+ and pieces(state, player)
+ and fruits(state, player)
+ and brooch(state, player)
+ and rose(state, player)
+ and canDash(state, player)
+ )
+
+
+def teehee(state, player):
+ return super(state, player) or canDash(state, player)
+
+
+def castleTown(state, player):
+ return fruits(state, player) and brooch(state, player)
+
+
+def fungitown(state, player):
+ return (
+ castleTown(state, player)
+ and thunder(state, player)
+ and rose(state, player)
+ and (super(state, player) or canDash(state, player))
+ )
+
+
+def piranha_shop(state, player):
+ return state.can_reach("Shop Mom Piranha Flag", "Region", player)
+
+
+def fungitown_shop(state, player):
+ return state.can_reach("Shop Enter Fungitown Flag", "Region", player)
+
+
+def star_shop(state, player):
+ return state.can_reach("Shop Beanstar Complete Flag", "Region", player)
+
+
+def birdo_shop(state, player):
+ return state.can_reach("Shop Birdo Flag", "Region", player)
+
+
+def fungitown_birdo_shop(state, player):
+ return state.can_reach("Fungitown Shop Birdo Flag", "Region", player)
diff --git a/worlds/mlss/__init__.py b/worlds/mlss/__init__.py
new file mode 100644
index 000000000000..f44343c230d0
--- /dev/null
+++ b/worlds/mlss/__init__.py
@@ -0,0 +1,183 @@
+import os
+import pkgutil
+import typing
+import settings
+from BaseClasses import Tutorial, ItemClassification
+from worlds.AutoWorld import WebWorld, World
+from typing import List, Dict, Any
+from .Locations import all_locations, location_table, bowsers, bowsersMini, hidden, coins
+from .Options import MLSSOptions
+from .Items import MLSSItem, itemList, item_frequencies, item_table
+from .Names.LocationName import LocationName
+from .Client import MLSSClient
+from .Regions import create_regions, connect_regions
+from .Rom import MLSSProcedurePatch, write_tokens
+from .Rules import set_rules
+
+
+class MLSSWebWorld(WebWorld):
+ theme = "partyTime"
+ bug_report_page = "https://github.com/jamesbrq/ArchipelagoMLSS/issues"
+ tutorials = [
+ Tutorial(
+ tutorial_name="Setup Guide",
+ description="A guide to setting up Mario & Luigi: Superstar Saga for Archipelago.",
+ language="English",
+ file_name="setup_en.md",
+ link="setup/en",
+ authors=["jamesbrq"],
+ )
+ ]
+
+
+class MLSSSettings(settings.Group):
+ class RomFile(settings.UserFilePath):
+ """File name of the MLSS US rom"""
+
+ copy_to = "Mario & Luigi - Superstar Saga (U).gba"
+ description = "MLSS ROM File"
+ md5s = ["4b1a5897d89d9e74ec7f630eefdfd435"]
+
+ rom_file: RomFile = RomFile(RomFile.copy_to)
+ rom_start: bool = True
+
+
+class MLSSWorld(World):
+ """
+ Adventure with Mario and Luigi together in the Beanbean Kingdom
+ to stop the evil Cackletta and retrieve the Beanstar.
+ """
+
+ game = "Mario & Luigi Superstar Saga"
+ web = MLSSWebWorld()
+ options_dataclass = MLSSOptions
+ options: MLSSOptions
+ settings: typing.ClassVar[MLSSSettings]
+ item_name_to_id = {name: data.code for name, data in item_table.items()}
+ location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations}
+ required_client_version = (0, 4, 5)
+
+ disabled_locations: List[str]
+
+ def generate_early(self) -> None:
+ self.disabled_locations = []
+ if self.options.chuckle_beans == 0:
+ self.disabled_locations += [location.name for location in all_locations if "Digspot" in location.name]
+ if self.options.castle_skip:
+ self.disabled_locations += [location.name for location in all_locations if "Bowser" in location.name]
+ if self.options.chuckle_beans == 1:
+ self.disabled_locations = [location.name for location in all_locations if location.id in hidden]
+ if self.options.skip_minecart:
+ self.disabled_locations += [LocationName.HoohooMountainBaseMinecartCaveDigspot]
+ if self.options.disable_surf:
+ self.disabled_locations += [LocationName.SurfMinigame]
+ if self.options.harhalls_pants:
+ self.disabled_locations += [LocationName.HarhallsPants]
+ if not self.options.coins:
+ self.disabled_locations += [location.name for location in all_locations if location in coins]
+
+ def create_regions(self) -> None:
+ create_regions(self, self.disabled_locations)
+ connect_regions(self)
+
+ item = self.create_item("Mushroom")
+ self.get_location(LocationName.ShopStartingFlag1).place_locked_item(item)
+ item = self.create_item("Syrup")
+ self.get_location(LocationName.ShopStartingFlag2).place_locked_item(item)
+ item = self.create_item("1-UP Mushroom")
+ self.get_location(LocationName.ShopStartingFlag3).place_locked_item(item)
+ item = self.create_item("Hoo Bean")
+ self.get_location(LocationName.PantsShopStartingFlag1).place_locked_item(item)
+ item = self.create_item("Chuckle Bean")
+ self.get_location(LocationName.PantsShopStartingFlag2).place_locked_item(item)
+
+ def fill_slot_data(self) -> Dict[str, Any]:
+ return {
+ "CastleSkip": self.options.castle_skip.value,
+ "SkipMinecart": self.options.skip_minecart.value,
+ "DisableSurf": self.options.disable_surf.value,
+ "HarhallsPants": self.options.harhalls_pants.value,
+ "ChuckleBeans": self.options.chuckle_beans.value,
+ "DifficultLogic": self.options.difficult_logic.value,
+ "Coins": self.options.coins.value,
+ }
+
+ def create_items(self) -> None:
+ # First add in all progression and useful items
+ required_items = []
+ precollected = [item for item in itemList if item in self.multiworld.precollected_items]
+ for item in itemList:
+ if item.classification != ItemClassification.filler and item.classification != ItemClassification.skip_balancing:
+ freq = item_frequencies.get(item.itemName, 1)
+ if item in precollected:
+ freq = max(freq - precollected.count(item), 0)
+ if self.options.harhalls_pants and "Harhall's" in item.itemName:
+ continue
+ required_items += [item.itemName for _ in range(freq)]
+
+ for itemName in required_items:
+ self.multiworld.itempool.append(self.create_item(itemName))
+
+ # Then, create our list of filler items
+ filler_items = []
+ for item in itemList:
+ if item.classification != ItemClassification.filler:
+ continue
+ if item.itemName == "5 Coins" and not self.options.coins:
+ continue
+ freq = item_frequencies.get(item.itemName, 1)
+ if self.options.chuckle_beans == 0:
+ if item.itemName == "Chuckle Bean":
+ continue
+ if self.options.chuckle_beans == 1:
+ if item.itemName == "Chuckle Bean":
+ freq -= 59
+ filler_items += [item.itemName for _ in range(freq)]
+
+ # And finally take as many fillers as we need to have the same amount of items and locations.
+ remaining = len(all_locations) - len(required_items) - 5
+ if self.options.castle_skip:
+ remaining -= len(bowsers) + len(bowsersMini) - (5 if self.options.chuckle_beans == 0 else 0)
+ if self.options.skip_minecart and self.options.chuckle_beans == 2:
+ remaining -= 1
+ if self.options.disable_surf:
+ remaining -= 1
+ if self.options.harhalls_pants:
+ remaining -= 1
+ if self.options.chuckle_beans == 0:
+ remaining -= 192
+ if self.options.chuckle_beans == 1:
+ remaining -= 59
+ if not self.options.coins:
+ remaining -= len(coins)
+
+ self.multiworld.itempool += [
+ self.create_item(filler_item_name) for filler_item_name in self.random.sample(filler_items, remaining)
+ ]
+
+ def set_rules(self) -> None:
+ set_rules(self, self.disabled_locations)
+ if self.options.castle_skip:
+ self.multiworld.completion_condition[self.player] = lambda state: state.can_reach(
+ "PostJokes", "Region", self.player
+ )
+ else:
+ self.multiworld.completion_condition[self.player] = lambda state: state.can_reach(
+ "Bowser's Castle Mini", "Region", self.player
+ )
+
+ def create_item(self, name: str) -> MLSSItem:
+ item = item_table[name]
+ return MLSSItem(item.itemName, item.classification, item.code, self.player)
+
+ def get_filler_item_name(self) -> str:
+ return self.random.choice(list(filter(lambda item: item.classification == ItemClassification.filler, itemList)))
+
+ def generate_output(self, output_directory: str) -> None:
+ patch = MLSSProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player])
+ patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, "data/basepatch.bsdiff"))
+ write_tokens(self, patch)
+ rom_path = os.path.join(
+ output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}" f"{patch.patch_file_ending}"
+ )
+ patch.write(rom_path)
diff --git a/worlds/mlss/data/basepatch.bsdiff b/worlds/mlss/data/basepatch.bsdiff
new file mode 100644
index 000000000000..8f9324995ec4
Binary files /dev/null and b/worlds/mlss/data/basepatch.bsdiff differ
diff --git a/worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md b/worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md
new file mode 100644
index 000000000000..4f83427d0955
--- /dev/null
+++ b/worlds/mlss/docs/en_Mario & Luigi Superstar Saga.md
@@ -0,0 +1,66 @@
+# Mario & Luigi: Superstar Saga
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains all the options you need to configure and
+export a config file.
+
+## What does randomization do to this game?
+
+Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is
+always able to be completed, but because of the item shuffle, the player may need to access certain areas before they
+would in the vanilla game.
+
+The game has been changed to an open-world style as opposed to the linear style the vanilla game has.
+
+Other Features such as Turbo through textboxes (Hold L/R+A) and Pipe Warping from any room (Hold L+R+SELECT) have been added for convenience.
+
+Enemies and Bosses can be randomized, and their stats can be scaled to feel more like the vanilla game's stats.
+
+Other aspects of the game can be randomized as well such as music, sounds, battle backgrounds, Mario and Luigi's Colors, and more.
+
+## What is the goal of Mario & Luigi: Superstar Saga when randomized?
+
+Defeat Cackletta's Soul in Bowser's Castle. This requires you to collect all 4 Beanstar Pieces, restore the Beanstar, and bring Peach's Extra Dress and the Fake Beanstar to Fawful at the end of Jokes End.
+
+In total, this requires:
+- 4 Beanstar Pieces
+- Peach's Extra Dress
+- Fake Beanstar
+- Ultra Hammers
+- Fire Hand
+- Thunder Hand
+- Red Pearl Bean
+- Green Pearl Bean
+- Green Goblet
+- Peasley's Rose
+- Beanbean Brooch
+- All 3 Chuckola Fruits
+- Membership Card OR Red Goblet
+
+## What items and locations can get shuffled?
+
+Locations in which items can be found:
+- All Item Blocks and Coin Blocks
+- All Chuckle Bean Digspots
+- All Shop items
+- All Pants and Badge shop items
+- All Espresso brews and rewards
+- All Minigame Rewards
+- All Event based items
+
+Items that can be shuffled:
+- All consumable items (Mushrooms, Nuts, Syrups etc.)
+- All Hoo Beans and Chuckle Beans
+- All Badges and Pants
+- All key items (Beanfruits, Beanbean Brooch, Hammers etc.)
+- All Extra Gears (Great Force, Gameboy Horror SP etc.)
+
+## What does another world's item look like in Mario & Luigi: Superstar Saga?
+
+Items will show up as a Golden Mushroom from boxes and Digspots and "AP Item" in all textboxes.
+Items in a shop from another player's world will display the player name and item name in addition to being displayed as an AP Item.
+
+## When the player receives an item, what happens?
+
+Items will be placed directly into the players inventory after a few seconds. Sometimes for certain events and cutscenes to be properly triggered right after you received an item, you may have to leave and re-enter the room to properly load everything required for the respective event or cutscene.
diff --git a/worlds/mlss/docs/setup_en.md b/worlds/mlss/docs/setup_en.md
new file mode 100644
index 000000000000..e74c942a0e74
--- /dev/null
+++ b/worlds/mlss/docs/setup_en.md
@@ -0,0 +1,53 @@
+# Setup Guide for Mario & Luigi: Superstar Saga Archipelago
+
+## Important
+
+As we are using Bizhawk, this guide is only applicable to Windows and Linux systems.
+
+## Required Software
+
+- Bizhawk: [Bizhawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
+ - Version 2.9.1 is recommended.
+ - Detailed installation instructions for Bizhawk can be found at the above link.
+ - Windows users must run the prerequisite installer first, which can also be found at the above link.
+- The built-in Bizhawk client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
+- A US copy of Mario & Luigi: Superstar Saga
+
+## Optional Software
+
+- [Poptracker](https://github.com/black-sliver/PopTracker/releases)
+ - [MLSS Autotracker](https://github.com/seto10987/MLSS-PopTracker/releases)
+
+## Configuring your YAML file
+
+### What is a YAML file and why do I need one?
+
+Your YAML file contains a set of configuration options which provide the generator with information about how it should
+generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
+an experience customized for their taste, and different players in the same multiworld can all have different options.
+
+### Where do I get a YAML file?
+
+You can customize your options by visiting the
+[Mario & Luigi Superstar Saga Options Page](/games/Mario%20&%20Luigi%20Superstar%20Saga/player-options)
+
+## Joining a MultiWorld Game
+
+### Obtain your GBA patch file
+
+When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
+the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
+files. Your data file should have a `.apmlss` extension.
+
+Double-click on your `.apmlss` file to start your client and start the ROM patch process. Once the process is finished, the client and the emulator will be started automatically (if you associated the extension
+to the emulator as recommended).
+
+### Connect to the Multiserver
+
+Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools"
+menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script.
+
+Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
+
+To connect the client to the multiserver simply put `:` on the textfield on top and press enter (if the
+server uses password, type in the bottom textfield `/connect : [password]`)
diff --git a/worlds/mmbn3/Locations.py b/worlds/mmbn3/Locations.py
index fc5910334055..0e2a1c51d11b 100644
--- a/worlds/mmbn3/Locations.py
+++ b/worlds/mmbn3/Locations.py
@@ -208,7 +208,7 @@ class MMBN3Location(Location):
LocationData(LocationName.ACDC_Class_5B_Bookshelf, 0xb3109e, 0x200024c, 0x40, 0x737634, 235, [5, 6]),
LocationData(LocationName.SciLab_Garbage_Can, 0xb3109f, 0x200024c, 0x8, 0x73AC20, 222, [4, 5]),
LocationData(LocationName.Yoka_Inn_Jars, 0xb310a0, 0x200024c, 0x80, 0x747B1C, 237, [4, 5]),
- LocationData(LocationName.Yoka_Zoo_Garbage, 0xb310a1, 0x200024d, 0x8, 0x749444, 226, [4]),
+ LocationData(LocationName.Yoka_Zoo_Garbage, 0xb310a1, 0x200024d, 0x8, 0x749444, 226, [5]),
LocationData(LocationName.Beach_Department_Store, 0xb310a2, 0x2000161, 0x40, 0x74C27C, 196, [0, 1]),
LocationData(LocationName.Beach_Hospital_Plaque, 0xb310a3, 0x200024c, 0x4, 0x754394, 220, [3, 4]),
LocationData(LocationName.Beach_Hospital_Pink_Door, 0xb310a4, 0x200024d, 0x4, 0x754D00, 220, [4]),
diff --git a/worlds/mmbn3/Options.py b/worlds/mmbn3/Options.py
index 96a01290a5c7..4ed64e3d9dbf 100644
--- a/worlds/mmbn3/Options.py
+++ b/worlds/mmbn3/Options.py
@@ -1,4 +1,5 @@
-from Options import Choice, Range, DefaultOnToggle
+from dataclasses import dataclass
+from Options import Choice, Range, DefaultOnToggle, PerGameCommonOptions
class ExtraRanks(Range):
@@ -41,8 +42,9 @@ class TradeQuestHinting(Choice):
default = 2
-MMBN3Options = {
- "extra_ranks": ExtraRanks,
- "include_jobs": IncludeJobs,
- "trade_quest_hinting": TradeQuestHinting,
-}
+@dataclass
+class MMBN3Options(PerGameCommonOptions):
+ extra_ranks: ExtraRanks
+ include_jobs: IncludeJobs
+ trade_quest_hinting: TradeQuestHinting
+
\ No newline at end of file
diff --git a/worlds/mmbn3/__init__.py b/worlds/mmbn3/__init__.py
index ec68825c2d2c..97725e728bae 100644
--- a/worlds/mmbn3/__init__.py
+++ b/worlds/mmbn3/__init__.py
@@ -7,6 +7,7 @@
LocationProgressType
from worlds.AutoWorld import WebWorld, World
+
from .Rom import MMBN3DeltaPatch, LocalRom, get_base_rom_path
from .Items import MMBN3Item, ItemData, item_table, all_items, item_frequencies, items_by_id, ItemType
from .Locations import Location, MMBN3Location, all_locations, location_table, location_data_table, \
@@ -15,6 +16,7 @@
from .Regions import regions, RegionName
from .Names.ItemName import ItemName
from .Names.LocationName import LocationName
+from worlds.generic.Rules import add_item_rule
class MMBN3Settings(settings.Group):
@@ -50,12 +52,11 @@ class MMBN3World(World):
threat the Internet has ever faced!
"""
game = "MegaMan Battle Network 3"
- option_definitions = MMBN3Options
+ options_dataclass = MMBN3Options
+ options: MMBN3Options
settings: typing.ClassVar[MMBN3Settings]
topology_present = False
- data_version = 1
-
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations}
@@ -70,10 +71,10 @@ def generate_early(self) -> None:
Already has access to player options and RNG.
"""
self.item_frequencies = item_frequencies.copy()
- if self.multiworld.extra_ranks[self.player] > 0:
- self.item_frequencies[ItemName.Progressive_Undernet_Rank] = 8 + self.multiworld.extra_ranks[self.player]
+ if self.options.extra_ranks > 0:
+ self.item_frequencies[ItemName.Progressive_Undernet_Rank] = 8 + self.options.extra_ranks
- if not self.multiworld.include_jobs[self.player]:
+ if not self.options.include_jobs:
self.excluded_locations = always_excluded_locations + [job.name for job in jobs]
else:
self.excluded_locations = always_excluded_locations
@@ -91,14 +92,15 @@ def create_regions(self) -> None:
loc = MMBN3Location(self.player, location, self.location_name_to_id.get(location, None), region)
if location in self.excluded_locations:
loc.progress_type = LocationProgressType.EXCLUDED
+ # Do not place any progression items on WWW Island
+ if region_info.name == RegionName.WWW_Island:
+ add_item_rule(loc, lambda item: not item.advancement)
region.locations.append(loc)
self.multiworld.regions.append(region)
for region_info in regions:
region = name_to_region[region_info.name]
for connection in region_info.connections:
- connection_region = name_to_region[connection]
- entrance = Entrance(self.player, connection, region)
- entrance.connect(connection_region)
+ entrance = region.connect(name_to_region[connection])
# ACDC Pending with Start Randomizer
# if connection == RegionName.ACDC_Overworld:
@@ -137,7 +139,6 @@ def create_regions(self) -> None:
if connection == RegionName.WWW_Island:
entrance.access_rule = lambda state:\
state.has(ItemName.Progressive_Undernet_Rank, self.player, 8)
- region.exits.append(entrance)
def create_items(self) -> None:
# First add in all progression and useful items
@@ -159,7 +160,7 @@ def create_items(self) -> None:
remaining = len(all_locations) - len(required_items)
for i in range(remaining):
- filler_item_name = self.multiworld.random.choice(filler_items)
+ filler_item_name = self.random.choice(filler_items)
item = self.create_item(filler_item_name)
self.multiworld.itempool.append(item)
filler_items.remove(filler_item_name)
@@ -410,10 +411,10 @@ def generate_output(self, output_directory: str) -> None:
long_item_text = ""
# No item hinting
- if self.multiworld.trade_quest_hinting[self.player] == 0:
+ if self.options.trade_quest_hinting == 0:
item_name_text = "Check"
# Partial item hinting
- elif self.multiworld.trade_quest_hinting[self.player] == 1:
+ elif self.options.trade_quest_hinting == 1:
if item.progression == ItemClassification.progression \
or item.progression == ItemClassification.progression_skip_balancing:
item_name_text = "Progress"
@@ -465,7 +466,7 @@ def create_event(self, event: str):
return MMBN3Item(event, ItemClassification.progression, None, self.player)
def fill_slot_data(self):
- return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions}
+ return self.options.as_dict("extra_ranks", "include_jobs", "trade_quest_hinting")
def explore_score(self, state):
diff --git a/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md b/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md
index 854034d5a8f1..bb9d2c15af2c 100644
--- a/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md
+++ b/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md
@@ -1,8 +1,8 @@
# MegaMan Battle Network 3
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and
+The [player options page for this game](../player-options) contains all the options you need to configure and
export a config file.
## What does randomization do to this game?
@@ -72,3 +72,10 @@ what item and what player is receiving the item
Whenever you have an item pending, the next time you are not in a battle, menu, or dialog box, you will receive a
message on screen notifying you of the item and sender, and the item will be added directly to your inventory.
+
+## Unique Local Commands
+
+The following commands are only available when using the MMBN3Client to play with Archipelago.
+
+- `/gba` Check GBA Connection State
+- `/debug` Toggle the Debug Text overlay in ROM
diff --git a/worlds/mmbn3/docs/setup_en.md b/worlds/mmbn3/docs/setup_en.md
index 309c07f5cfc4..b26403f78bb9 100644
--- a/worlds/mmbn3/docs/setup_en.md
+++ b/worlds/mmbn3/docs/setup_en.md
@@ -10,19 +10,20 @@ As we are using Bizhawk, this guide is only applicable to Windows and Linux syst
- Version 2.7.0 and later are supported.
- 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)
- (select `MegaMan Battle Network 3 Client` during installation).
-- A US MegaMan Battle Network 3 Blue Rom
+- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
+- A US MegaMan Battle Network 3 Blue Rom. If you have the [MegaMan Battle Network Legacy Collection Vol. 1](https://store.steampowered.com/app/1798010/Mega_Man_Battle_Network_Legacy_Collection_Vol_1/)
+on Steam, you can obtain a copy of this ROM from the game's files, see instructions below.
## Configuring Bizhawk
Once Bizhawk has been installed, open Bizhawk and change the following settings:
-- Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
- "Lua+LuaInterface". 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 Bizhawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load**
- **"NLua+KopiLua" until this step is done.**
+- **If you are using a version of BizHawk older than 2.9**, you will need to modify the Lua Core.
+ Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
+ "Lua+LuaInterface". 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 Bizhawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load
+ "NLua+KopiLua" until this step is done.
- Under Config > Customize > Advanced, make sure the box for AutoSaveRAM is checked, and click the 5s button.
This reduces the possibility of losing save data in emulator crashes.
- Under Config > Customize, check the "Run in background" and "Accept background input" boxes. This will allow you to
@@ -35,6 +36,14 @@ To do so, we simply have to search any GBA rom we happened to own, right click a
the list that appears and select the bottom option "Look for another application", then browse to the Bizhawk folder
and select EmuHawk.exe.
+## Extracting a ROM from the Legacy Collection
+
+The Steam version of the Battle Network Legacy Collection contains unmodified GBA ROMs in its files. You can extract these for use with Archipelago.
+
+1. Open the Legacy Collection Vol. 1's Game Files (Right click on the game in your Library, then open Properties -> Installed Files -> Browse)
+2. Open the file `exe/data/exe3b.dat` in a zip-extracting program such as 7-Zip or WinRAR.
+3. Extract the file `rom_b_e.srl` somewhere and rename it to `Mega Man Battle Network 3 - Blue Version (USA).gba`
+
## Configuring your YAML file
### What is a YAML file and why do I need one?
@@ -45,8 +54,8 @@ an experience customized for their taste, and different players in the same mult
### Where do I get a YAML file?
-You can customize your settings by visiting the
-[MegaMan Battle Network 3 Player Settings Page](/games/MegaMan%20Battle%20Network%203/player-settings)
+You can customize your options by visiting the
+[MegaMan Battle Network 3 Player Options Page](/games/MegaMan%20Battle%20Network%203/player-options)
## Joining a MultiWorld Game
@@ -65,7 +74,9 @@ to the emulator as recommended).
Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools"
menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script.
-Navigate to your Archipelago install folder and open `data/lua/connector_mmbn3.lua`.
+Navigate to your Archipelago install folder and open `data/lua/connector_mmbn3.lua`.
+**NOTE:** The MMBN3 Lua file depends on other shared Lua files inside of the `data` directory in the Archipelago
+installation. Do not move this Lua file from its default location or you may run into issues connecting.
To connect the client to the multiserver simply put `:` on the textfield on top and press enter (if the
server uses password, type in the bottom textfield `/connect : [password]`)
@@ -76,4 +87,4 @@ Don't forget to start manipulating RNG early by shouting during generation:
JACK IN!
[Your name]!
EXECUTE!
-```
\ No newline at end of file
+```
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..576a106df7cc 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,17 +15,37 @@ 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"
+
+ # Ordering matters for webhost. Order goes: Muse Plus, Time Limited Muse Plus Dlcs, Paid Dlcs
+ DLC: List[str] = [
+ MUSE_PLUS_DLC,
+ "CHUNITHM COURSE MUSE", # Part of Muse Plus. Goes away 22nd May 2027.
+ "maimai DX Limited-time Suite", # Part of Muse Plus. Goes away 31st Jan 2026.
+ "MSR Anthology", # Now no longer available.
+ "Miku in Museland", # Paid DLC not included in Muse Plus
+ "Rin Len's Mirrorland", # Paid DLC not included in Muse Plus
+ ]
+
+ DIFF_OVERRIDES: List[str] = [
"MuseDash ka nanika hi",
"Rush-Hour",
"Find this Month's Featured Playlist",
"PeroPero in the Universe",
+ "umpopoff",
+ "P E R O P E R O Brother Dance",
+ ]
+
+ REMOVED_SONGS = [
+ "CHAOS Glitch",
+ "FM 17314 SUGAR RADIO",
+ "Yume Ou Mono Yo Secret",
]
album_items: Dict[str, AlbumData] = {}
@@ -33,23 +53,38 @@ class MuseDashCollections:
song_items: Dict[str, SongData] = {}
song_locations: Dict[str, int] = {}
- vfx_trap_items: Dict[str, int] = {
+ trap_items: Dict[str, int] = {
"Bad Apple Trap": STARTING_CODE + 1,
"Pixelate Trap": STARTING_CODE + 2,
- "Random Wave Trap": STARTING_CODE + 3,
- "Shadow Edge Trap": STARTING_CODE + 4,
+ "Ripple Trap": STARTING_CODE + 3,
+ "Vignette Trap": STARTING_CODE + 4,
"Chromatic Aberration Trap": STARTING_CODE + 5,
"Background Freeze Trap": STARTING_CODE + 6,
"Gray Scale Trap": STARTING_CODE + 7,
- }
-
- sfx_trap_items: Dict[str, int] = {
"Nyaa SFX Trap": STARTING_CODE + 8,
"Error SFX Trap": STARTING_CODE + 9,
+ "Focus Line Trap": STARTING_CODE + 10,
+ }
+
+ sfx_trap_items: List[str] = [
+ "Nyaa SFX Trap",
+ "Error SFX Trap",
+ ]
+
+ filler_items: Dict[str, int] = {
+ "Great To Perfect (10 Pack)": STARTING_CODE + 30,
+ "Miss To Great (5 Pack)": STARTING_CODE + 31,
+ "Extra Life": STARTING_CODE + 32,
+ }
+
+ filler_item_weights: Dict[str, int] = {
+ "Great To Perfect (10 Pack)": 10,
+ "Miss To Great (5 Pack)": 3,
+ "Extra Life": 1,
}
- 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({}, filler_items, 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,21 +105,31 @@ 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:
- # Note: These difficulties may not actually be representative of these songs.
- # The game does not provide these difficulties so they have to be filled in.
- diff_of_easy = 4
- diff_of_hard = 7
- diff_of_master = 10
+ # These songs use non-standard difficulty values. Which are being overriden with standard values.
+ # But also avoid filling any missing difficulties (i.e. 0s) with a difficulty value.
+ if sections[4] != '0':
+ diff_of_easy = 4
+ else:
+ diff_of_easy = None
+
+ if sections[5] != '0':
+ diff_of_hard = 7
+ else:
+ diff_of_hard = None
+
+ if sections[6] != '0':
+ diff_of_master = 10
+ else:
+ diff_of_master = None
else:
diff_of_easy = self.parse_song_difficulty(sections[4])
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 +147,16 @@ 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 songKey in self.REMOVED_SONGS:
continue
if streamer_mode_active and not songData.streamer_mode:
@@ -128,6 +176,22 @@ def get_songs_with_settings(self, dlc_songs: bool, streamer_mode_active: bool,
return filtered_list
+ def filter_songs_to_dlc(self, song_list: List[str], dlc_songs: Set[str]) -> List[str]:
+ return [song for song in song_list if self.song_matches_dlc_filter(self.song_items[song], dlc_songs)]
+
+ 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..6f48d6af9fdd 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|
@@ -119,7 +119,7 @@ Prestige and Vestige|56-4|Give Up TREATMENT Vol.11|True|6|8|11|
Tiny Fate|56-5|Give Up TREATMENT Vol.11|False|7|9|11|
Tsuki ni Murakumo Hana ni Kaze|55-0|Touhou Mugakudan -2-|False|3|5|7|
Patchouli's - Best Hit GSK|55-1|Touhou Mugakudan -2-|False|3|5|8|
-Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7|
+Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7|11
Kakoinaki Yo wa Ichigo no Tsukikage|55-3|Touhou Mugakudan -2-|False|3|6|8|
Psychedelic Kizakura Doumei|55-4|Touhou Mugakudan -2-|False|4|7|10|
Mischievous Sensation|55-5|Touhou Mugakudan -2-|False|5|7|9|
@@ -404,7 +404,7 @@ trippers feeling!|8-4|Give Up TREATMENT Vol.3|True|5|7|9|11
Lilith ambivalence lovers|8-5|Give Up TREATMENT Vol.3|False|5|8|10|
Brave My Soul|7-0|Give Up TREATMENT Vol.2|False|4|6|8|
Halcyon|7-1|Give Up TREATMENT Vol.2|False|4|7|10|
-Crimson Nightingle|7-2|Give Up TREATMENT Vol.2|True|4|7|10|
+Crimson Nightingale|7-2|Give Up TREATMENT Vol.2|True|4|7|10|
Invader|7-3|Give Up TREATMENT Vol.2|True|3|7|11|
Lyrith|7-4|Give Up TREATMENT Vol.2|False|5|7|10|
GOODBOUNCE|7-5|Give Up TREATMENT Vol.2|False|4|6|9|
@@ -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,105 @@ 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|11
+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|
+NICE TYPE feat. monii|43-41|MD Plus Project|True|3|6|8|
+Rainy Angel|67-0|Happy Otaku Pack Vol.18|True|4|6|9|11
+Gullinkambi|67-1|Happy Otaku Pack Vol.18|True|4|7|10|
+RakiRaki Rebuilders!!!|67-2|Happy Otaku Pack Vol.18|True|5|7|10|
+Laniakea|67-3|Happy Otaku Pack Vol.18|False|5|8|10|
+OTTAMA GAZER|67-4|Happy Otaku Pack Vol.18|True|5|8|10|
+Sleep Tight feat.Macoto|67-5|Happy Otaku Pack Vol.18|True|3|5|8|
+New York Back Raise|68-0|Gambler's Tricks|True|6|8|10|
+slic.hertz|68-1|Gambler's Tricks|True|5|7|9|
+Fuzzy-Navel|68-2|Gambler's Tricks|True|6|8|10|11
+Swing Edge|68-3|Gambler's Tricks|True|4|8|10|
+Twisted Escape|68-4|Gambler's Tricks|True|5|8|10|11
+Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10|
+Sanyousei SAY YA!!!|43-42|MD Plus Project|False|4|6|8|
+YUKEMURI TAMAONSEN II|43-43|MD Plus Project|False|3|6|9|
+Samayoi no mei Amatsu|69-0|Touhou Mugakudan -3-|False|4|6|9|
+INTERNET SURVIVOR|69-1|Touhou Mugakudan -3-|False|5|8|10|
+Shuki*RaiRai|69-2|Touhou Mugakudan -3-|False|5|7|9|
+HELLOHELL|69-3|Touhou Mugakudan -3-|False|4|7|10|
+Calamity Fortune|69-4|Touhou Mugakudan -3-|True|6|8|10|11
+Tsurupettan|69-5|Touhou Mugakudan -3-|True|2|5|8|
+Twilight Poems|43-44|MD Plus Project|True|3|6|8|
+All My Friends feat. RANASOL|43-45|MD Plus Project|True|4|7|9|
+Heartache|43-46|MD Plus Project|True|5|7|10|
+Blue Lemonade|43-47|MD Plus Project|True|3|6|8|
+Haunted Dance|43-48|MD Plus Project|False|6|9|11|
+Hey Vincent.|43-49|MD Plus Project|True|6|8|10|
+Meteor feat. TEA|43-50|MD Plus Project|True|3|6|9|
+Narcissism Angel|43-51|MD Plus Project|True|1|3|6|
+AlterLuna|43-52|MD Plus Project|True|6|8|11|12
+Niki Tousen|43-53|MD Plus Project|True|6|8|10|12
+Rettou Joutou|70-0|Rin Len's Mirrorland|False|4|7|9|
+Telecaster B-Boy|70-1|Rin Len's Mirrorland|False|5|7|10|
+Iya Iya Iya|70-2|Rin Len's Mirrorland|False|2|4|7|
+Nee Nee Nee|70-3|Rin Len's Mirrorland|False|4|6|8|
+Chaotic Love Revolution|70-4|Rin Len's Mirrorland|False|4|6|8|
+Dance of the Corpses|70-5|Rin Len's Mirrorland|False|2|5|8|
+Bitter Choco Decoration|70-6|Rin Len's Mirrorland|False|3|6|9|
+Dance Robot Dance|70-7|Rin Len's Mirrorland|False|4|7|10|
+Sweet Devil|70-8|Rin Len's Mirrorland|False|5|7|9|
+Someday'z Coming|70-9|Rin Len's Mirrorland|False|5|7|9|
+Yume Ou Mono Yo Secret|0-53|Default Music|True|6|8|10|
+Yume Ou Mono Yo|0-54|Default Music|True|1|4|0|
+Sweet Dream VIVINOS|71-0|Valentine Stage|False|1|4|7|
+Ruler Of My Heart VIVINOS|71-1|Valentine Stage|False|2|4|6|
+Reality Show|71-2|Valentine Stage|False|5|7|10|
+SIG feat.Tobokegao|71-3|Valentine Stage|True|3|6|8|
+Rose Love|71-4|Valentine Stage|True|2|4|7|
+Euphoria|71-5|Valentine Stage|True|1|3|6|
+P E R O P E R O Brother Dance|72-0|Legends of Muse Warriors|True|0|?|0|
+PA PPA PANIC|72-1|Legends of Muse Warriors|False|4|8|10|
+How To Make Music Game Song!|72-2|Legends of Muse Warriors|True|6|8|10|11
+Re Re|72-3|Legends of Muse Warriors|True|7|9|11|12
+Marmalade Twins|72-4|Legends of Muse Warriors|True|5|8|10|
+DOMINATOR|72-5|Legends of Muse Warriors|True|7|9|11|
+Teshikani TESHiKANi|72-6|Legends of Muse Warriors|True|5|7|9|
+Urban Magic|73-0|Happy Otaku Pack Vol.19|True|3|5|7|
+Maid's Prank|73-1|Happy Otaku Pack Vol.19|True|5|7|10|
+Dance Dance Good Night Dance|73-2|Happy Otaku Pack Vol.19|True|2|4|7|
+Ops Limone|73-3|Happy Otaku Pack Vol.19|True|5|8|11|
+NOVA|73-4|Happy Otaku Pack Vol.19|True|6|8|10|
+Heaven's Gradius|73-5|Happy Otaku Pack Vol.19|True|6|8|10|
+Ray Tuning|74-0|CHUNITHM COURSE MUSE|True|6|8|10|
+World Vanquisher|74-1|CHUNITHM COURSE MUSE|True|6|8|10|11
+Tsukuyomi Ni Naru|74-2|CHUNITHM COURSE MUSE|False|5|7|9|
+The wheel to the right|74-3|CHUNITHM COURSE MUSE|True|5|7|9|11
+Climax|74-4|CHUNITHM COURSE MUSE|True|4|8|11|11
+Spider's Thread|74-5|CHUNITHM COURSE MUSE|True|5|8|10|12
+HIT ME UP|43-54|MD Plus Project|True|4|6|8|
+Test Me feat. Uyeon|43-55|MD Plus Project|True|3|5|9|
+Assault TAXI|43-56|MD Plus Project|True|4|7|10|
+No|43-57|MD Plus Project|False|4|6|9|
+Pop it|43-58|MD Plus Project|True|1|3|6|
+HEARTBEAT! KyunKyun!|43-59|MD Plus Project|True|4|6|9|
+SUPERHERO|75-0|Novice Rider Pack|False|2|4|7|
+Highway_Summer|75-1|Novice Rider Pack|True|2|4|6|
+Mx. Black Box|75-2|Novice Rider Pack|True|5|7|9|
+Sweet Encounter|75-3|Novice Rider Pack|True|2|4|7|
diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py
index cc9f5d705684..7164aa3e1362 100644
--- a/worlds/musedash/Options.py
+++ b/worlds/musedash/Options.py
@@ -1,15 +1,26 @@
-from typing import Dict
-from Options import Toggle, Option, Range, Choice, DeathLink, ItemSet
+from Options import Toggle, Range, Choice, DeathLink, ItemSet, OptionSet, PerGameCommonOptions, OptionGroup, Removed
+from dataclasses import dataclass
+from .MuseDashCollection import MuseDashCollections
-class AllowJustAsPlannedDLCSongs(Toggle):
- """Whether 'Just as Planned DLC' songs, and all the DLCs along with it, will be included in the randomizer."""
- display_name = "Allow Just As Planned DLC Songs"
+
+class DLCMusicPacks(OptionSet):
+ """
+ Choose which DLC Packs will be included in the pool of chooseable songs.
+
+ Note: The [Just As Planned] DLC contains all [Muse Plus] songs.
+ """
+ display_name = "DLC Packs"
+ default = {}
+ valid_keys = [dlc for dlc in MuseDashCollections.DLC]
class StreamerModeEnabled(Toggle):
- """In Muse Dash, an option named 'Streamer Mode' removes songs which may trigger copyright issues when streaming.
- If this is enabled, only songs available under Streamer Mode will be available for randomization."""
+ """
+ In Muse Dash, an option named 'Streamer Mode' removes songs which may trigger copyright issues when streaming.
+
+ If this is enabled, only songs available under Streamer Mode will be available for randomization.
+ """
display_name = "Streamer Mode Only Songs"
@@ -22,18 +33,20 @@ class StartingSongs(Range):
class AdditionalSongs(Range):
- """The total number of songs that will be placed in the randomization pool.
+ """
+ The total number of songs that will be placed in the randomization pool.
- This does not count any starting songs or the goal song.
- The final song count may be lower due to other settings.
"""
range_start = 15
- range_end = 500 # Note will probably not reach this high if any other settings are done.
+ range_end = 534 # Note will probably not reach this high if any other settings are done.
default = 40
display_name = "Additional Song Count"
class DifficultyMode(Choice):
- """Ensures that at any chosen song has at least 1 value falling within these values.
+ """
+ Ensures that at any chosen song has at least 1 value falling within these values.
- Any: All songs are available
- Easy: 1, 2 or 3
- Medium: 4, 5
@@ -55,8 +68,11 @@ class DifficultyMode(Choice):
# Todo: Investigate options to make this non randomizable
class DifficultyModeOverrideMin(Range):
- """Ensures that 1 difficulty has at least 1 this value or higher per song.
- - Difficulty Mode must be set to Manual."""
+ """
+ Ensures that 1 difficulty has at least 1 this value or higher per song.
+
+ Note: Difficulty Mode must be set to Manual.
+ """
display_name = "Manual Difficulty Min"
range_start = 1
range_end = 11
@@ -65,8 +81,11 @@ class DifficultyModeOverrideMin(Range):
# Todo: Investigate options to make this non randomizable
class DifficultyModeOverrideMax(Range):
- """Ensures that 1 difficulty has at least 1 this value or lower per song.
- - Difficulty Mode must be set to Manual."""
+ """
+ Ensures that 1 difficulty has at least 1 this value or lower per song.
+
+ Note: Difficulty Mode must be set to Manual.
+ """
display_name = "Manual Difficulty Max"
range_start = 1
range_end = 11
@@ -74,7 +93,8 @@ class DifficultyModeOverrideMax(Range):
class GradeNeeded(Choice):
- """Completing a song will require a grade of this value or higher in order to unlock items.
+ """
+ Completing a song will require a grade of this value or higher in order to unlock items.
The grades are as follows:
- Silver S (SS): >= 95% accuracy
- Pink S (S): >= 90% accuracy
@@ -92,20 +112,12 @@ class GradeNeeded(Choice):
default = 0
-class AdditionalItemPercentage(Range):
- """The percentage of songs that will have 2 items instead of 1 when completing them.
- - Starting Songs will always have 2 items.
- - Locations will be filled with duplicate songs if there are not enough items.
- """
- display_name = "Additional Item %"
- range_start = 50
- default = 80
- range_end = 100
-
-
class MusicSheetCountPercentage(Range):
- """Collecting enough Music Sheets will unlock the goal song needed for completion.
- This option controls how many are in the item pool, based on the total number of songs."""
+ """
+ Controls how many music sheets are added to the pool based on the number of songs, including starting songs.
+
+ Higher numbers leads to more consistent game lengths, but will cause individual music sheets to be less important.
+ """
range_start = 10
range_end = 40
default = 20
@@ -120,19 +132,18 @@ class MusicSheetWinCountPercentage(Range):
display_name = "Music Sheets Needed to Win"
-class TrapTypes(Choice):
- """This controls the types of traps that can be added to the pool.
+class ChosenTraps(OptionSet):
+ """
+ This controls the types of traps that can be added to the pool.
+ - Traps last the length of a song, or until you die.
- VFX Traps consist of visual effects that play over the song. (i.e. Grayscale.)
- SFX Traps consist of changing your sfx setting to one possibly more annoying sfx.
- Traps last the length of a song, or until you die.
- Note: SFX traps are only available with Just As Planned dlc songs.
+
+ Note: SFX traps are only available if [Just as Planned] DLC songs are enabled.
"""
- display_name = "Available Trap Types"
- option_None = 0
- option_VFX = 1
- option_SFX = 2
- option_All = 3
- default = 3
+ display_name = "Chosen Traps"
+ default = {}
+ valid_keys = {trap for trap in MuseDashCollections.trap_items.keys()}
class TrapCountPercentage(Range):
@@ -144,36 +155,65 @@ class TrapCountPercentage(Range):
class IncludeSongs(ItemSet):
- """Any song listed here will be guaranteed to be included as part of the seed.
- - Difficulty options will be skipped for these songs.
- - If there being too many included songs, songs will be randomly chosen without regard for difficulty.
- - If you want these songs immediately, use start_inventory instead.
+ """
+ These songs will be guaranteed to show up within the seed.
+ - You must have the DLC enabled to play these songs.
+ - Difficulty options will not affect these songs.
+ - If there are too many included songs, this will act as a whitelist ignoring song difficulty.
"""
verify_item_name = True
display_name = "Include Songs"
class ExcludeSongs(ItemSet):
- """Any song listed here will be excluded from being a part of the seed."""
+ """
+ These songs will be guaranteed to not show up within the seed.
+
+ Note: Does not affect songs within the "Include Songs" list.
+ """
verify_item_name = True
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
-}
+md_option_groups = [
+ OptionGroup("Song Choice", [
+ DLCMusicPacks,
+ StreamerModeEnabled,
+ IncludeSongs,
+ ExcludeSongs,
+ ]),
+ OptionGroup("Difficulty", [
+ GradeNeeded,
+ DifficultyMode,
+ DifficultyModeOverrideMin,
+ DifficultyModeOverrideMax,
+ DeathLink,
+ ]),
+ OptionGroup("Traps", [
+ ChosenTraps,
+ TrapCountPercentage,
+ ]),
+]
+
+
+@dataclass
+class MuseDashOptions(PerGameCommonOptions):
+ dlc_packs: DLCMusicPacks
+ streamer_mode_enabled: StreamerModeEnabled
+ starting_song_count: StartingSongs
+ additional_song_count: AdditionalSongs
+ 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
+ chosen_traps: ChosenTraps
+ trap_count_percentage: TrapCountPercentage
+ death_link: DeathLink
+ include_songs: IncludeSongs
+ exclude_songs: ExcludeSongs
+
+ # Removed
+ allow_just_as_planned_dlc_songs: Removed
+ available_trap_types: Removed
diff --git a/worlds/musedash/Presets.py b/worlds/musedash/Presets.py
new file mode 100644
index 000000000000..fe314edbc9b5
--- /dev/null
+++ b/worlds/musedash/Presets.py
@@ -0,0 +1,28 @@
+from typing import Any, Dict
+
+MuseDashPresets: Dict[str, Dict[str, Any]] = {
+ # An option to support Short Sync games. 40 songs.
+ "No DLC - Short": {
+ "dlc_packs": [],
+ "starting_song_count": 5,
+ "additional_song_count": 34,
+ "music_sheet_count_percentage": 20,
+ "music_sheet_win_count_percentage": 90,
+ },
+ # An option to support Short Sync games but adds variety. 40 songs.
+ "DLC - Short": {
+ "dlc_packs": ["Muse Plus"],
+ "starting_song_count": 5,
+ "additional_song_count": 34,
+ "music_sheet_count_percentage": 20,
+ "music_sheet_win_count_percentage": 90,
+ },
+ # An option to support Longer Sync/Async games. 100 songs.
+ "DLC - Long": {
+ "dlc_packs": ["Muse Plus"],
+ "starting_song_count": 8,
+ "additional_song_count": 91,
+ "music_sheet_count_percentage": 20,
+ "music_sheet_win_count_percentage": 90,
+ },
+}
diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py
index 78b9c253d593..ab3a4819fc48 100644
--- a/worlds/musedash/__init__.py
+++ b/worlds/musedash/__init__.py
@@ -1,13 +1,14 @@
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 BaseClasses import Region, Item, ItemClassification, Tutorial
+from typing import List, ClassVar, Type, Set
from math import floor
+from Options import PerGameCommonOptions
-from .Options import musedash_options
+from .Options import MuseDashOptions, md_option_groups
from .Items import MuseDashSongItem, MuseDashFixedItem
from .Locations import MuseDashLocation
from .MuseDashCollection import MuseDashCollections
+from .Presets import MuseDashPresets
class MuseDashWebWorld(WebWorld):
@@ -23,7 +24,18 @@ class MuseDashWebWorld(WebWorld):
["DeamonHunter"]
)
- tutorials = [setup_en]
+ setup_es = Tutorial(
+ setup_en.tutorial_name,
+ setup_en.description,
+ "Español",
+ "setup_es.md",
+ "setup/es",
+ ["Shiny"]
+ )
+
+ tutorials = [setup_en, setup_es]
+ options_presets = MuseDashPresets
+ option_groups = md_option_groups
class MuseDashWorld(World):
@@ -38,13 +50,16 @@ class MuseDashWorld(World):
# World Options
game = "Muse Dash"
- option_definitions = musedash_options
+ options_dataclass: ClassVar[Type[PerGameCommonOptions]] = MuseDashOptions
+ options: MuseDashOptions
+
topology_present = False
- data_version = 9
web = MuseDashWebWorld()
# Necessary Data
md_collection = MuseDashCollections()
+ filler_item_names = list(md_collection.filler_item_weights.keys())
+ filler_item_weights = list(md_collection.filler_item_weights.values())
item_name_to_id = {name: code for name, code in md_collection.item_names_to_id.items()}
location_name_to_id = {name: code for name, code in md_collection.location_names_to_id.items()}
@@ -57,21 +72,22 @@ 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}
+
+ 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
available_song_keys = self.md_collection.get_songs_with_settings(
- dlc_songs, streamer_mode, lower_diff_threshold, higher_diff_threshold)
+ dlc_songs, bool(streamer_mode.value), lower_diff_threshold, higher_diff_threshold)
- available_song_keys = self.handle_plando(available_song_keys)
+ available_song_keys = self.handle_plando(available_song_keys, dlc_songs)
count_needed_for_start = max(0, starter_song_count - len(self.starting_songs))
if len(available_song_keys) + len(self.included_songs) >= count_needed_for_start + 11:
@@ -92,22 +108,24 @@ def generate_early(self):
for song in self.starting_songs:
self.multiworld.push_precollected(self.create_item(song))
- def handle_plando(self, available_song_keys: List[str]) -> List[str]:
+ def handle_plando(self, available_song_keys: List[str], dlc_songs: Set[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.starting_songs = self.md_collection.filter_songs_to_dlc(self.starting_songs, dlc_songs)
self.included_songs = [s for s in include_songs if s in song_items and s not in self.starting_songs]
+ self.included_songs = self.md_collection.filter_songs_to_dlc(self.included_songs, dlc_songs)
return [s for s in available_song_keys if s not in start_items
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)
@@ -131,7 +149,7 @@ def create_song_pool(self, available_song_keys: List[str]):
self.victory_song_name = available_song_keys[chosen_song - included_song_count]
del available_song_keys[chosen_song - included_song_count]
- # Next, make sure the starting songs are fufilled
+ # Next, make sure the starting songs are fulfilled
if len(self.starting_songs) < starting_song_count:
for _ in range(len(self.starting_songs), starting_song_count):
if len(available_song_keys) > 0:
@@ -139,31 +157,25 @@ def create_song_pool(self, available_song_keys: List[str]):
else:
self.starting_songs.append(self.included_songs.pop())
- # Then attempt to fufill any remaining songs for interim songs
+ # Then attempt to fulfill 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())
- self.location_count = len(self.starting_songs) + len(self.included_songs)
- location_multiplier = 1 + (self.get_additional_item_percentage() / 100.0)
- self.location_count = floor(self.location_count * location_multiplier)
-
- minimum_location_count = len(self.included_songs) + self.get_music_sheet_count()
- if self.location_count < minimum_location_count:
- self.location_count = minimum_location_count
+ self.location_count = 2 * (len(self.starting_songs) + len(self.included_songs))
def create_item(self, name: str) -> Item:
if name == self.md_collection.MUSIC_SHEET_NAME:
return MuseDashFixedItem(name, ItemClassification.progression_skip_balancing,
self.md_collection.MUSIC_SHEET_CODE, self.player)
- trap = self.md_collection.vfx_trap_items.get(name)
- if trap:
- return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player)
+ filler = self.md_collection.filler_items.get(name)
+ if filler:
+ return MuseDashFixedItem(name, ItemClassification.filler, filler, self.player)
- trap = self.md_collection.sfx_trap_items.get(name)
+ trap = self.md_collection.trap_items.get(name)
if trap:
return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player)
@@ -174,6 +186,9 @@ def create_item(self, name: str) -> Item:
song = self.md_collection.song_items.get(name)
return MuseDashSongItem(name, self.player, song)
+ def get_filler_item_name(self) -> str:
+ return self.random.choices(self.filler_item_names, self.filler_item_weights)[0]
+
def create_items(self) -> None:
song_keys_in_pool = self.included_songs.copy()
@@ -184,8 +199,13 @@ def create_items(self) -> None:
for _ in range(0, item_count):
self.multiworld.itempool.append(self.create_item(self.md_collection.MUSIC_SHEET_NAME))
- # Then add all traps
- trap_count = self.get_trap_count()
+ # Then add 1 copy of every song
+ item_count += len(self.included_songs)
+ for song in self.included_songs:
+ self.multiworld.itempool.append(self.create_item(song))
+
+ # Then add all traps, making sure we don't over fill
+ trap_count = min(self.location_count - item_count, self.get_trap_count())
trap_list = self.get_available_traps()
if len(trap_list) > 0 and trap_count > 0:
for _ in range(0, trap_count):
@@ -194,29 +214,42 @@ def create_items(self) -> None:
item_count += trap_count
- # Next fill all remaining slots with song items
- needed_item_count = self.location_count
- while item_count < needed_item_count:
- # If we have more items needed than keys, just iterate the list and add them all
- if len(song_keys_in_pool) <= needed_item_count - item_count:
- for key in song_keys_in_pool:
- self.multiworld.itempool.append(self.create_item(key))
+ # At this point, if a player is using traps, it's possible that they have filled all locations
+ items_left = self.location_count - item_count
+ if items_left <= 0:
+ return
+
+ # When it comes to filling remaining spaces, we have 2 options. A useless filler or additional songs.
+ # First fill 50% with the filler. The rest is to be duplicate songs.
+ filler_count = floor(0.5 * items_left)
+ items_left -= filler_count
+
+ for _ in range(0, filler_count):
+ self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
+
+ # All remaining spots are filled with duplicate songs. Duplicates are set to useful instead of progression
+ # to cut down on the number of progression items that Muse Dash puts into the pool.
- item_count += len(song_keys_in_pool)
- continue
+ # This is for the extraordinary case of needing to fill a lot of items.
+ while items_left > len(song_keys_in_pool):
+ for key in song_keys_in_pool:
+ item = self.create_item(key)
+ item.classification = ItemClassification.useful
+ self.multiworld.itempool.append(item)
- # Otherwise add a random assortment of songs
- self.random.shuffle(song_keys_in_pool)
- for i in range(0, needed_item_count - item_count):
- self.multiworld.itempool.append(self.create_item(song_keys_in_pool[i]))
+ items_left -= len(song_keys_in_pool)
+ continue
- item_count = needed_item_count
+ # Otherwise add a random assortment of songs
+ self.random.shuffle(song_keys_in_pool)
+ for i in range(0, items_left):
+ item = self.create_item(song_keys_in_pool[i])
+ item.classification = ItemClassification.useful
+ self.multiworld.itempool.append(item)
def create_regions(self) -> None:
menu_region = Region("Menu", self.player, self.multiworld)
- song_select_region = Region("Song Select", self.player, self.multiworld)
- self.multiworld.regions += [menu_region, song_select_region]
- menu_region.connect(song_select_region)
+ self.multiworld.regions += [menu_region]
# Make a collection of all songs available for this rando.
# 1. All starting songs
@@ -230,59 +263,45 @@ def create_regions(self) -> None:
self.random.shuffle(included_song_copy)
all_selected_locations.extend(included_song_copy)
- two_item_location_count = self.location_count - len(all_selected_locations)
-
- # Make a region per song/album, then adds 1-2 item locations to them
+ # Adds 2 item locations per song/album to the menu region.
for i in range(0, len(all_selected_locations)):
name = all_selected_locations[i]
- region = Region(name, self.player, self.multiworld)
- self.multiworld.regions.append(region)
- song_select_region.connect(region, name, lambda state, place=name: state.has(place, self.player))
+ loc1 = MuseDashLocation(self.player, name + "-0", self.md_collection.song_locations[name + "-0"], menu_region)
+ loc1.access_rule = lambda state, place=name: state.has(place, self.player)
+ menu_region.locations.append(loc1)
- # Up to 2 Locations are defined per song
- region.add_locations({name + "-0": self.md_collection.song_locations[name + "-0"]}, MuseDashLocation)
- if i < two_item_location_count:
- region.add_locations({name + "-1": self.md_collection.song_locations[name + "-1"]}, MuseDashLocation)
+ loc2 = MuseDashLocation(self.player, name + "-1", self.md_collection.song_locations[name + "-1"], menu_region)
+ loc2.access_rule = lambda state, place=name: state.has(place, self.player)
+ menu_region.locations.append(loc2)
def set_rules(self) -> None:
self.multiworld.completion_condition[self.player] = lambda state: \
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]
-
- trap_list = []
- if self.multiworld.available_trap_types[self.player].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:
- trap_list += self.md_collection.sfx_trap_items.keys()
-
- return trap_list
+ full_trap_list = self.md_collection.trap_items.keys()
+ if self.md_collection.MUSE_PLUS_DLC not in self.options.dlc_packs.value:
+ full_trap_list = [trap for trap in full_trap_list if trap not in self.md_collection.sfx_trap_items]
- 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)
+ return [trap for trap in full_trap_list if trap in self.options.chosen_traps.value]
def get_trap_count(self) -> int:
- multiplier = self.multiworld.trap_count_percentage[self.player].value / 100.0
- trap_count = (len(self.starting_songs) * 2) + len(self.included_songs)
+ multiplier = self.options.trap_count_percentage.value / 100.0
+ trap_count = len(self.starting_songs) + 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
- song_count = (len(self.starting_songs) * 2) + len(self.included_songs)
+ multiplier = self.options.music_sheet_count_percentage.value / 100.0
+ song_count = len(self.starting_songs) + 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]
@@ -300,8 +319,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)
@@ -311,7 +330,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/docs/en_Muse Dash.md b/worlds/musedash/docs/en_Muse Dash.md
index 5f4673d256a8..29d1465ed098 100644
--- a/worlds/musedash/docs/en_Muse Dash.md
+++ b/worlds/musedash/docs/en_Muse Dash.md
@@ -2,10 +2,10 @@
## Quick Links
- [Setup Guide](../../../tutorial/Muse%20Dash/setup/en)
-- [Settings Page](../player-settings)
+- [Options Page](../player-options)
## What Does Randomization do to this Game?
-- You will be given a number of starting songs. The number of which depends on your settings.
+- You will be given a number of starting songs. The number of which depends on your options.
- Completing any song will give you 1 or 2 rewards.
- The rewards for completing songs will range from songs to traps and **Music Sheets**.
@@ -17,7 +17,7 @@ The goal of Muse Dash is to collect a number of **Music Sheets**. Once you've co
Only the base Muse Dash game is required in order to play this game.
-However, the **Just as Planned DLC** is recommended as the number of possible songs increases from 60+ to 400+ songs, which adds to the variety and increases replayability.
+However, the **[Just as Planned]**/**[Muse Plus]** DLC is recommended, as it increases the number of possible songs from ~60 to 400+ songs, which adds to the variety and increases replayability.
## What Other Adjustments have been made to the Base Game?
- Several song select filters have been added to make finding songs to play easy.
diff --git a/worlds/musedash/docs/setup_en.md b/worlds/musedash/docs/setup_en.md
index 7ad701829735..a3c2f43a91f0 100644
--- a/worlds/musedash/docs/setup_en.md
+++ b/worlds/musedash/docs/setup_en.md
@@ -2,32 +2,33 @@
## Quick Links
- [Main Page](../../../../games/Muse%20Dash/info/en)
-- [Settings Page](../../../../games/Muse%20Dash/player-settings)
+- [Options Page](../../../../games/Muse%20Dash/player-options)
## Required Software
- Windows 8 or Newer.
- Muse Dash: [Available on Steam](https://store.steampowered.com/app/774171/Muse_Dash/)
- - \[Optional\] Just As Planned DLC: [Also Available on Steam](https://store.steampowered.com/app/1055810/Muse_Dash__Just_as_planned/)
+ - \[Optional\] [Muse Plus] DLC: [Also Available on Steam](https://store.steampowered.com/app/2593750/Muse_Dash__Muse_Plus/)
- Melon Loader: [GitHub](https://github.com/LavaGang/MelonLoader/releases/latest)
- .Net Framework 4.8 may be needed for the installer: [Download](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net48)
-- .Net 6.0 (If not already installed): [Download](https://dotnet.microsoft.com/en-us/download/dotnet/6.0#runtime-6.0.15)
+- .NET Desktop Runtime 6.0.XX (If not already installed): [Download](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
- Muse Dash Archipelago Mod: [GitHub](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest)
## Installing the Archipelago mod to Muse Dash
1. Download [MelonLoader.Installer.exe](https://github.com/LavaGang/MelonLoader/releases/latest) and run it.
-2. Choose the automated tab, click the select button and browse to `MuseDash.exe`. Then click install.
+2. Choose the automated tab, click the select button and browse to `MuseDash.exe`.
- You can find the folder in steam by finding the game in your library, right clicking it and choosing *Manage→Browse Local Files*.
- If you click the bar at the top telling you your current folder, this will give you a path you can copy. If you paste that into the window popped up by **MelonLoader**, it will automatically go to the same folder.
-3. Run the game once, and wait until you get to the Muse Dash start screen before exiting.
-4. Download the latest [Muse Dash Archipelago Mod](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) and then extract that into the newly created `/Mods/` folder in MuseDash's install location.
+3. Uncheck "Latest" and select v0.6.1. Then click install.
+4. Run the game once, and wait until you get to the Muse Dash start screen before exiting.
+5. Download the latest [Muse Dash Archipelago Mod](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) and then extract that into the newly created `/Mods/` folder in MuseDash's install location.
- All files must be under the `/Mods/` folder and not within a sub folder inside of `/Mods/`
If you've successfully installed everything, a button will appear in the bottom right which will allow you to log into an Archipelago server.
## Generating a MultiWorld Game
-1. Visit the [Player Settings](/games/Muse%20Dash/player-settings) page and configure the game-specific settings to your taste.
+1. Visit the [Player Options](/games/Muse%20Dash/player-options) page and configure the game-specific options to your taste.
2. Export your yaml file and use it to generate a new randomized game
- (For instructions on how to generate an Archipelago game, refer to the [Archipelago Web Guide](/tutorial/Archipelago/setup/en))
diff --git a/worlds/musedash/docs/setup_es.md b/worlds/musedash/docs/setup_es.md
new file mode 100644
index 000000000000..fe5358921def
--- /dev/null
+++ b/worlds/musedash/docs/setup_es.md
@@ -0,0 +1,49 @@
+# GuÃa de instalación para Muse Dash: Archipelago
+
+## Enlaces rápidos
+- [Página Principal](../../../../games/Muse%20Dash/info/en)
+- [Página de Configuraciones](../../../../games/Muse%20Dash/player-options)
+
+## Software Requerido
+
+- Windows 8 o más reciente.
+- Muse Dash: [Disponible en Steam](https://store.steampowered.com/app/774171/Muse_Dash/)
+ - \[Opcional\] [Muse Plus] DLC: [tambien disponible on Steam](https://store.steampowered.com/app/2593750/Muse_Dash__Muse_Plus/)
+- Melon Loader: [GitHub](https://github.com/LavaGang/MelonLoader/releases/latest)
+ - .Net Framework 4.8 podrÃa ser necesario para el instalador: [Descarga](https://dotnet.microsoft.com/es-es/download/dotnet-framework/net48)
+- Entorno de ejecución de escritorio de .NET 6.0.XX (si aún no está instalado): [Descarga](https://dotnet.microsoft.com/es-es/download/dotnet/6.0)
+- Muse Dash Archipelago Mod: [GitHub](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest)
+
+## Instalar el mod de Archipelago en Muse Dash
+
+1. Descarga [MelonLoader.Installer.exe](https://github.com/LavaGang/MelonLoader/releases/latest) y ejecutalo.
+2. Elije la pestaña "automated", haz clic en el botón "select" y busca tu `MuseDash.exe`.
+ - Puedes encontrar la carpeta en Steam buscando el juego en tu biblioteca, haciendo clic derecho sobre el y elegir *Administrar→Ver archivos locales*.
+ - Si haces clic en la barra superior que te indica la carpeta en la que estas, te dará la dirección de ésta para que puedas copiarla. Al pegar esa dirección en la ventana que **MelonLoader** abre, irá automaticamente a esa carpeta.
+3. Desmarca "Latest" y selecciona v0.6.1. Luego haz clic en "install".
+4. Ejecuta el juego una vez, y espera hasta que aparezca la pantalla de inicio de Muse Dash antes de cerrarlo.
+5. Descarga la última version de [Muse Dash Archipelago Mod](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) y extraelo en la nueva carpeta creada llamada `/Mods/`, localizada en la carpeta de instalación de Muse Dash.
+ - Todos los archivos deben ir directamente en la carpeta `/Mods/`, y NO en una subcarpeta dentro de la carpeta `/Mods/`
+
+Si todo fue instalado correctamente, un botón aparecerá en la parte inferior derecha del juego una vez abierto, que te permitirá conectarte al servidor de Archipelago.
+
+## Generar un juego MultiWorld
+1. Entra a la página de [configuraciones de jugador](/games/Muse%20Dash/player-options) y configura las opciones del juego a tu gusto.
+2. Genera tu archivo YAML y úsalo para generar un juego nuevo en el radomizer
+ - (Instrucciones sobre como generar un juego en Archipelago disponibles en la [guÃa web de Archipelago en Inglés](/tutorial/Archipelago/setup/en))
+
+## Unirse a un juego MultiWorld
+
+1. Ejecuta Muse Dash y pasa por la pantalla de introducción. Haz clic en el botón de la esquina inferior derecha.
+2. Ingresa los detalles de la sesión de archipelago, como la dirección del servidor con el puerto (por ejemplo, archipelago.gg:38381), nombre de usuario y contraseña.
+3. Si todo se ingresó correctamente, el pop-up deberÃa desaparecer y se mostrará el menú principal habitual. Al ingresar a la selección de canciones, deberÃas ver una cantidad limitada de canciones.
+
+## Solución de problemas
+
+### No Support Module Loaded
+
+Este error ocurre cuando Melon Loader no puede encontrar los archivos necesarios para ejecutar mods. Generalmente, hay dos razones principales de este error: una falla al generar los archivos cuando el juego se ejecutó por primera vez con Melon Loader, o un antivirus que elimina los archivos después de la generación.
+
+Para solucionar este problema, primero debes eliminar Melon Loader de Muse Dash. Puedes hacer esto eliminando la carpeta Melon Loader dentro de la carpeta de Muse Dash. Luego, seguir los pasos de instalación nuevamente.
+
+Si continúas teniendo problemas y estás utilizando un antivirus, es posible que tengas que desactivarlo temporalmente cuando se ejecute Muse Dash por primera vez, o excluir la carpeta Muse Dash de ser escaneada.
diff --git a/worlds/musedash/test/TestCollection.py b/worlds/musedash/test/TestCollection.py
index 23348af104b5..c8c2b39acb4d 100644
--- a/worlds/musedash/test/TestCollection.py
+++ b/worlds/musedash/test/TestCollection.py
@@ -3,47 +3,56 @@
class CollectionsTest(unittest.TestCase):
- REMOVED_SONGS = [
- "CHAOS Glitch",
- "FM 17314 SUGAR RADIO",
- ]
-
def test_all_names_are_ascii(self) -> None:
bad_names = list()
collection = MuseDashCollections()
for name in collection.song_items.keys():
for c in name:
# This is taken directly from OoT. Represents the generally excepted characters.
- if (0x20 <= ord(c) < 0x7e):
+ if 0x20 <= ord(c) < 0x7e:
continue
bad_names.append(name)
break
- self.assertEqual(len(bad_names), 0, f"Muse Dash has {len(bad_names)} songs with non-ASCII characters.\n{bad_names}")
+ self.assertEqual(len(bad_names), 0,
+ f"Muse Dash has {len(bad_names)} songs with non-ASCII characters.\n{bad_names}")
def test_ids_dont_change(self) -> None:
collection = MuseDashCollections()
- itemsBefore = {name: code for name, code in collection.item_names_to_id.items()}
- locationsBefore = {name: code for name, code in collection.location_names_to_id.items()}
+ items_before = {name: code for name, code in collection.item_names_to_id.items()}
+ locations_before = {name: code for name, code in collection.location_names_to_id.items()}
collection.__init__()
- itemsAfter = {name: code for name, code in collection.item_names_to_id.items()}
- locationsAfter = {name: code for name, code in collection.location_names_to_id.items()}
+ items_after = {name: code for name, code in collection.item_names_to_id.items()}
+ locations_after = {name: code for name, code in collection.location_names_to_id.items()}
- self.assertDictEqual(itemsBefore, itemsAfter, "Item ID changed after secondary init.")
- self.assertDictEqual(locationsBefore, locationsAfter, "Location ID changed after secondary init.")
+ self.assertDictEqual(items_before, items_after, "Item ID changed after secondary init.")
+ self.assertDictEqual(locations_before, locations_after, "Location ID changed after secondary init.")
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:
+ for song_name in collection.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..a9c36985afae 100644
--- a/worlds/musedash/test/TestDifficultyRanges.py
+++ b/worlds/musedash/test/TestDifficultyRanges.py
@@ -3,30 +3,31 @@
class DifficultyRanges(MuseDashTestBase):
def test_all_difficulty_ranges(self) -> None:
- muse_dash_world = self.multiworld.worlds[1]
- difficulty_choice = self.multiworld.song_difficulty_mode[1]
- difficulty_min = self.multiworld.song_difficulty_min[1]
- difficulty_max = self.multiworld.song_difficulty_max[1]
+ muse_dash_world = self.get_world()
+ dlc_set = {x for x in muse_dash_world.md_collection.DLC}
+ difficulty_choice = muse_dash_world.options.song_difficulty_mode
+ difficulty_min = muse_dash_world.options.song_difficulty_min
+ difficulty_max = muse_dash_world.options.song_difficulty_max
- def test_range(inputRange, lower, upper):
- self.assertEqual(inputRange[0], lower)
- self.assertEqual(inputRange[1], upper)
+ def test_range(input_range, lower, upper):
+ self.assertEqual(input_range[0], lower)
+ self.assertEqual(input_range[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, input_range[0], input_range[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]):
+ if song.easy is not None and input_range[0] <= song.easy <= input_range[1]:
continue
- if (song.hard is not None and inputRange[0] <= song.hard <= inputRange[1]):
+ if song.hard is not None and input_range[0] <= song.hard <= input_range[1]:
continue
- if (song.master is not None and inputRange[0] <= song.master <= inputRange[1]):
+ if song.master is not None and input_range[0] <= song.master <= input_range[1]:
continue
- self.fail(f"Invalid song '{songKey}' was given for range '{inputRange[0]} to {inputRange[1]}'")
+ self.fail(f"Invalid song '{songKey}' was given for range '{input_range[0]} to {input_range[1]}'")
- #auto ranges
+ # auto ranges
difficulty_choice.value = 0
test_range(muse_dash_world.get_difficulty_range(), 0, 12)
difficulty_choice.value = 1
@@ -60,10 +61,16 @@ def test_range(inputRange, lower, upper):
test_range(muse_dash_world.get_difficulty_range(), 4, 6)
def test_songs_have_difficulty(self) -> None:
- muse_dash_world = self.multiworld.worlds[1]
+ muse_dash_world = self.get_world()
for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES:
song = muse_dash_world.md_collection.song_items[song_name]
- self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None,
- f"Song '{song_name}' difficulty not set when it should be.")
+ # Some songs are weird and have less than the usual 3 difficulties.
+ # So this override is to avoid failing on these songs.
+ if song_name in ("umpopoff", "P E R O P E R O Brother Dance"):
+ self.assertTrue(song.easy is None and song.hard is not None and song.master is None,
+ f"Song '{song_name}' difficulty not set when it should be.")
+ else:
+ self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None,
+ f"Song '{song_name}' difficulty not set when it should be.")
diff --git a/worlds/musedash/test/TestPlandoSettings.py b/worlds/musedash/test/TestPlandoSettings.py
index 4b23a4afa90a..2617b7a4e02c 100644
--- a/worlds/musedash/test/TestPlandoSettings.py
+++ b/worlds/musedash/test/TestPlandoSettings.py
@@ -4,7 +4,32 @@
class TestPlandoSettings(MuseDashTestBase):
options = {
"additional_song_count": 15,
- "allow_just_as_planned_dlc_songs": True,
+ "dlc_packs": {"Muse Plus"},
+ "include_songs": [
+ "Lunatic",
+ "Out of Sense",
+ "Magic Knight Girl",
+ ]
+ }
+
+ def test_included_songs_didnt_grow_item_count(self) -> None:
+ muse_dash_world = self.get_world()
+ self.assertEqual(len(muse_dash_world.included_songs), 15, "Logical songs size grew when it shouldn't.")
+
+ def test_included_songs_plando(self) -> None:
+ muse_dash_world = self.get_world()
+ songs = muse_dash_world.included_songs.copy()
+ songs.append(muse_dash_world.victory_song_name)
+
+ self.assertIn("Lunatic", songs, "Logical songs is missing a plando song: Lunatic")
+ self.assertIn("Out of Sense", songs, "Logical songs is missing a plando song: Out of Sense")
+ self.assertIn("Magic Knight Girl", songs, "Logical songs is missing a plando song: Magic Knight Girl")
+
+
+class TestFilteredPlandoSettings(MuseDashTestBase):
+ options = {
+ "additional_song_count": 15,
+ "dlc_packs": {"MSR Anthology"},
"include_songs": [
"Operation Blade",
"Autumn Moods",
@@ -13,15 +38,15 @@ class TestPlandoSettings(MuseDashTestBase):
}
def test_included_songs_didnt_grow_item_count(self) -> None:
- muse_dash_world = self.multiworld.worlds[1]
- self.assertEqual(len(muse_dash_world.included_songs), 15,
- f"Logical songs size grew when it shouldn't. Expected 15. Got {len(muse_dash_world.included_songs)}")
+ muse_dash_world = self.get_world()
+ self.assertEqual(len(muse_dash_world.included_songs), 15, "Logical songs size grew when it shouldn't.")
- def test_included_songs_plando(self) -> None:
- muse_dash_world = self.multiworld.worlds[1]
+ # Tests for excluding included songs when the right dlc isn't enabled
+ def test_filtered_included_songs_plando(self) -> None:
+ muse_dash_world = self.get_world()
songs = muse_dash_world.included_songs.copy()
songs.append(muse_dash_world.victory_song_name)
self.assertIn("Operation Blade", songs, "Logical songs is missing a plando song: Operation Blade")
self.assertIn("Autumn Moods", songs, "Logical songs is missing a plando song: Autumn Moods")
- self.assertIn("Fireflies", songs, "Logical songs is missing a plando song: Fireflies")
\ No newline at end of file
+ self.assertNotIn("Fireflies", songs, "Logical songs has added a filtered a plando song: Fireflies")
diff --git a/worlds/musedash/test/TestTrapOption.py b/worlds/musedash/test/TestTrapOption.py
new file mode 100644
index 000000000000..ca0579c1f66c
--- /dev/null
+++ b/worlds/musedash/test/TestTrapOption.py
@@ -0,0 +1,33 @@
+from . import MuseDashTestBase
+
+
+class TestNoTraps(MuseDashTestBase):
+ def test_no_traps(self) -> None:
+ md_world = self.get_world()
+ md_world.options.chosen_traps.value.clear()
+ self.assertEqual(len(md_world.get_available_traps()), 0, "Got an available trap when we expected none.")
+
+ def test_all_traps(self) -> None:
+ md_world = self.get_world()
+ md_world.options.dlc_packs.value.add(md_world.md_collection.MUSE_PLUS_DLC)
+
+ for trap in md_world.md_collection.trap_items.keys():
+ md_world.options.chosen_traps.value.add(trap)
+
+ trap_count = len(md_world.get_available_traps())
+ true_count = len(md_world.md_collection.trap_items.keys())
+
+ self.assertEqual(trap_count, true_count, "Got a different amount of traps than what was expected.")
+
+ def test_exclude_sfx_traps(self) -> None:
+ md_world = self.get_world()
+ if "Muse Plus" in md_world.options.dlc_packs.value:
+ md_world.options.dlc_packs.value.remove("Muse Plus")
+
+ for trap in md_world.md_collection.trap_items.keys():
+ md_world.options.chosen_traps.value.add(trap)
+
+ trap_count = len(md_world.get_available_traps())
+ true_count = len(md_world.md_collection.trap_items.keys()) - len(md_world.md_collection.sfx_trap_items)
+
+ self.assertEqual(trap_count, true_count, "Got a different amount of traps than what was expected.")
diff --git a/worlds/musedash/test/TestWorstCaseSettings.py b/worlds/musedash/test/TestWorstCaseSettings.py
index eeedfa5c3a5f..fd39651d1203 100644
--- a/worlds/musedash/test/TestWorstCaseSettings.py
+++ b/worlds/musedash/test/TestWorstCaseSettings.py
@@ -4,30 +4,33 @@
# This ends up with only 25 valid songs that can be chosen.
# These tests ensure that this won't fail generation
+
class TestWorstCaseHighDifficulty(MuseDashTestBase):
options = {
"starting_song_count": 10,
- "allow_just_as_planned_dlc_songs": False,
+ "dlc_packs": [],
"streamer_mode_enabled": True,
"song_difficulty_mode": 6,
"song_difficulty_min": 11,
"song_difficulty_max": 11,
}
+
class TestWorstCaseMidDifficulty(MuseDashTestBase):
options = {
"starting_song_count": 10,
- "allow_just_as_planned_dlc_songs": False,
+ "dlc_packs": [],
"streamer_mode_enabled": True,
"song_difficulty_mode": 6,
"song_difficulty_min": 6,
"song_difficulty_max": 6,
}
+
class TestWorstCaseLowDifficulty(MuseDashTestBase):
options = {
"starting_song_count": 10,
- "allow_just_as_planned_dlc_songs": False,
+ "dlc_packs": [],
"streamer_mode_enabled": True,
"song_difficulty_mode": 6,
"song_difficulty_min": 1,
diff --git a/worlds/musedash/test/__init__.py b/worlds/musedash/test/__init__.py
index 818fd357cd97..ff9d988c65c2 100644
--- a/worlds/musedash/test/__init__.py
+++ b/worlds/musedash/test/__init__.py
@@ -1,5 +1,10 @@
-from test.TestBase import WorldTestBase
-
+from test.bases import WorldTestBase
+from .. import MuseDashWorld
+from typing import cast
class MuseDashTestBase(WorldTestBase):
game = "Muse Dash"
+
+ def get_world(self) -> MuseDashWorld:
+ return cast(MuseDashWorld, self.multiworld.worlds[1])
+
diff --git a/worlds/noita/Events.py b/worlds/noita/Events.py
deleted file mode 100644
index e759d38c6c7a..000000000000
--- a/worlds/noita/Events.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from typing import Dict
-
-from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region
-from . import Items, Locations
-
-
-def create_event(player: int, name: str) -> Item:
- return Items.NoitaItem(name, ItemClassification.progression, None, player)
-
-
-def create_location(player: int, name: str, region: Region) -> Location:
- return Locations.NoitaLocation(player, name, None, region)
-
-
-def create_locked_location_event(multiworld: MultiWorld, player: int, region_name: str, item: str) -> Location:
- region = multiworld.get_region(region_name, player)
-
- new_location = create_location(player, item, region)
- new_location.place_locked_item(create_event(player, item))
-
- region.locations.append(new_location)
- return new_location
-
-
-def create_all_events(multiworld: MultiWorld, player: int) -> None:
- for region, event in event_locks.items():
- create_locked_location_event(multiworld, player, region, event)
-
- multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
-
-
-# Maps region names to event names
-event_locks: Dict[str, str] = {
- "The Work": "Victory",
- "Mines": "Portal to Holy Mountain 1",
- "Coal Pits": "Portal to Holy Mountain 2",
- "Snowy Depths": "Portal to Holy Mountain 3",
- "Hiisi Base": "Portal to Holy Mountain 4",
- "Underground Jungle": "Portal to Holy Mountain 5",
- "The Vault": "Portal to Holy Mountain 6",
- "Temple of the Art": "Portal to Holy Mountain 7",
-}
diff --git a/worlds/noita/Items.py b/worlds/noita/Items.py
deleted file mode 100644
index ca53c9623387..000000000000
--- a/worlds/noita/Items.py
+++ /dev/null
@@ -1,156 +0,0 @@
-import itertools
-from collections import Counter
-from typing import Dict, List, NamedTuple, Set
-
-from BaseClasses import Item, ItemClassification, MultiWorld
-from .Options import BossesAsChecks, VictoryCondition, ExtraOrbs
-
-
-class ItemData(NamedTuple):
- code: int
- group: str
- classification: ItemClassification = ItemClassification.progression
- required_num: int = 0
-
-
-class NoitaItem(Item):
- game: str = "Noita"
-
-
-def create_item(player: int, name: str) -> Item:
- item_data = item_table[name]
- return NoitaItem(name, item_data.classification, item_data.code, player)
-
-
-def create_fixed_item_pool() -> List[str]:
- required_items: Dict[str, int] = {name: data.required_num for name, data in item_table.items()}
- return list(Counter(required_items).elements())
-
-
-def create_orb_items(victory_condition: VictoryCondition, extra_orbs: ExtraOrbs) -> List[str]:
- orb_count = extra_orbs.value
- if victory_condition == VictoryCondition.option_pure_ending:
- orb_count = orb_count + 11
- elif victory_condition == VictoryCondition.option_peaceful_ending:
- orb_count = orb_count + 33
- return ["Orb" for _ in range(orb_count)]
-
-
-def create_spatial_awareness_item(bosses_as_checks: BossesAsChecks) -> List[str]:
- return ["Spatial Awareness Perk"] if bosses_as_checks.value >= BossesAsChecks.option_all_bosses else []
-
-
-def create_kantele(victory_condition: VictoryCondition) -> List[str]:
- return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else []
-
-
-def create_random_items(multiworld: MultiWorld, player: int, random_count: int) -> List[str]:
- filler_pool = filler_weights.copy()
- if multiworld.bad_effects[player].value == 0:
- del filler_pool["Trap"]
-
- return multiworld.random.choices(
- population=list(filler_pool.keys()),
- weights=list(filler_pool.values()),
- k=random_count
- )
-
-
-def create_all_items(multiworld: MultiWorld, player: int) -> None:
- sum_locations = len(multiworld.get_unfilled_locations(player))
-
- itempool = (
- create_fixed_item_pool()
- + create_orb_items(multiworld.victory_condition[player], multiworld.extra_orbs[player])
- + create_spatial_awareness_item(multiworld.bosses_as_checks[player])
- + create_kantele(multiworld.victory_condition[player])
- )
-
- random_count = sum_locations - len(itempool)
- itempool += create_random_items(multiworld, player, random_count)
-
- multiworld.itempool += [create_item(player, name) for name in itempool]
-
-
-# 110000 - 110032
-item_table: Dict[str, ItemData] = {
- "Trap": ItemData(110000, "Traps", ItemClassification.trap),
- "Extra Max HP": ItemData(110001, "Pickups", ItemClassification.useful),
- "Spell Refresher": ItemData(110002, "Pickups", ItemClassification.filler),
- "Potion": ItemData(110003, "Items", ItemClassification.filler),
- "Gold (200)": ItemData(110004, "Gold", ItemClassification.filler),
- "Gold (1000)": ItemData(110005, "Gold", ItemClassification.filler),
- "Wand (Tier 1)": ItemData(110006, "Wands", ItemClassification.useful),
- "Wand (Tier 2)": ItemData(110007, "Wands", ItemClassification.useful),
- "Wand (Tier 3)": ItemData(110008, "Wands", ItemClassification.useful),
- "Wand (Tier 4)": ItemData(110009, "Wands", ItemClassification.useful),
- "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful),
- "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful),
- "Kantele": ItemData(110012, "Wands", ItemClassification.useful),
- "Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression, 1),
- "Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression, 1),
- "Explosion Immunity Perk": ItemData(110015, "Perks", ItemClassification.progression, 1),
- "Melee Immunity Perk": ItemData(110016, "Perks", ItemClassification.progression, 1),
- "Electricity Immunity Perk": ItemData(110017, "Perks", ItemClassification.progression, 1),
- "Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression, 1),
- "All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression, 1),
- "Spatial Awareness Perk": ItemData(110020, "Perks", ItemClassification.progression),
- "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful),
- "Orb": ItemData(110022, "Orbs", ItemClassification.progression_skip_balancing),
- "Random Potion": ItemData(110023, "Items", ItemClassification.filler),
- "Secret Potion": ItemData(110024, "Items", ItemClassification.filler),
- "Powder Pouch": ItemData(110025, "Items", ItemClassification.filler),
- "Chaos Die": ItemData(110026, "Items", ItemClassification.filler),
- "Greed Die": ItemData(110027, "Items", ItemClassification.filler),
- "Kammi": ItemData(110028, "Items", ItemClassification.filler),
- "Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler),
- "Sädekivi": ItemData(110030, "Items", ItemClassification.filler),
- "Broken Wand": ItemData(110031, "Items", ItemClassification.filler),
-
-}
-
-filler_weights: Dict[str, int] = {
- "Trap": 15,
- "Extra Max HP": 25,
- "Spell Refresher": 20,
- "Potion": 40,
- "Gold (200)": 15,
- "Gold (1000)": 6,
- "Wand (Tier 1)": 10,
- "Wand (Tier 2)": 8,
- "Wand (Tier 3)": 7,
- "Wand (Tier 4)": 6,
- "Wand (Tier 5)": 5,
- "Wand (Tier 6)": 4,
- "Extra Life Perk": 10,
- "Random Potion": 9,
- "Secret Potion": 10,
- "Powder Pouch": 10,
- "Chaos Die": 4,
- "Greed Die": 4,
- "Kammi": 4,
- "Refreshing Gourd": 4,
- "Sädekivi": 3,
- "Broken Wand": 10,
-}
-
-
-# These helper functions make the comprehensions below more readable
-def get_item_group(item_name: str) -> str:
- return item_table[item_name].group
-
-
-def item_is_filler(item_name: str) -> bool:
- return item_table[item_name].classification == ItemClassification.filler
-
-
-def item_is_perk(item_name: str) -> bool:
- return item_table[item_name].group == "Perks"
-
-
-filler_items: List[str] = list(filter(item_is_filler, item_table.keys()))
-item_name_to_id: Dict[str, int] = {name: data.code for name, data in item_table.items()}
-
-item_name_groups: Dict[str, Set[str]] = {
- group: set(item_names) for group, item_names in itertools.groupby(item_table, get_item_group)
-}
diff --git a/worlds/noita/Locations.py b/worlds/noita/Locations.py
deleted file mode 100644
index 7c27d699ccba..000000000000
--- a/worlds/noita/Locations.py
+++ /dev/null
@@ -1,223 +0,0 @@
-# Locations are specific points that you would obtain an item at.
-from enum import IntEnum
-from typing import Dict, NamedTuple, Optional, Set
-
-from BaseClasses import Location
-
-
-class NoitaLocation(Location):
- game: str = "Noita"
-
-
-class LocationData(NamedTuple):
- id: int
- flag: int = 0
- ltype: Optional[str] = "shop"
-
-
-class LocationFlag(IntEnum):
- none = 0
- main_path = 1
- side_path = 2
- main_world = 3
- parallel_worlds = 4
-
-
-# Mapping of items in each region.
-# Only the first Hidden Chest and Pedestal are mapped here, the others are created in Regions.
-# ltype key: "chest" = Hidden Chests, "pedestal" = Pedestals, "boss" = Boss, "orb" = Orb.
-# 110000-110649
-location_region_mapping: Dict[str, Dict[str, LocationData]] = {
- "Coal Pits Holy Mountain": {
- "Coal Pits Holy Mountain Shop Item 1": LocationData(110000),
- "Coal Pits Holy Mountain Shop Item 2": LocationData(110001),
- "Coal Pits Holy Mountain Shop Item 3": LocationData(110002),
- "Coal Pits Holy Mountain Shop Item 4": LocationData(110003),
- "Coal Pits Holy Mountain Shop Item 5": LocationData(110004),
- "Coal Pits Holy Mountain Spell Refresh": LocationData(110005),
- },
- "Snowy Depths Holy Mountain": {
- "Snowy Depths Holy Mountain Shop Item 1": LocationData(110006),
- "Snowy Depths Holy Mountain Shop Item 2": LocationData(110007),
- "Snowy Depths Holy Mountain Shop Item 3": LocationData(110008),
- "Snowy Depths Holy Mountain Shop Item 4": LocationData(110009),
- "Snowy Depths Holy Mountain Shop Item 5": LocationData(110010),
- "Snowy Depths Holy Mountain Spell Refresh": LocationData(110011),
- },
- "Hiisi Base Holy Mountain": {
- "Hiisi Base Holy Mountain Shop Item 1": LocationData(110012),
- "Hiisi Base Holy Mountain Shop Item 2": LocationData(110013),
- "Hiisi Base Holy Mountain Shop Item 3": LocationData(110014),
- "Hiisi Base Holy Mountain Shop Item 4": LocationData(110015),
- "Hiisi Base Holy Mountain Shop Item 5": LocationData(110016),
- "Hiisi Base Holy Mountain Spell Refresh": LocationData(110017),
- },
- "Underground Jungle Holy Mountain": {
- "Underground Jungle Holy Mountain Shop Item 1": LocationData(110018),
- "Underground Jungle Holy Mountain Shop Item 2": LocationData(110019),
- "Underground Jungle Holy Mountain Shop Item 3": LocationData(110020),
- "Underground Jungle Holy Mountain Shop Item 4": LocationData(110021),
- "Underground Jungle Holy Mountain Shop Item 5": LocationData(110022),
- "Underground Jungle Holy Mountain Spell Refresh": LocationData(110023),
- },
- "Vault Holy Mountain": {
- "Vault Holy Mountain Shop Item 1": LocationData(110024),
- "Vault Holy Mountain Shop Item 2": LocationData(110025),
- "Vault Holy Mountain Shop Item 3": LocationData(110026),
- "Vault Holy Mountain Shop Item 4": LocationData(110027),
- "Vault Holy Mountain Shop Item 5": LocationData(110028),
- "Vault Holy Mountain Spell Refresh": LocationData(110029),
- },
- "Temple of the Art Holy Mountain": {
- "Temple of the Art Holy Mountain Shop Item 1": LocationData(110030),
- "Temple of the Art Holy Mountain Shop Item 2": LocationData(110031),
- "Temple of the Art Holy Mountain Shop Item 3": LocationData(110032),
- "Temple of the Art Holy Mountain Shop Item 4": LocationData(110033),
- "Temple of the Art Holy Mountain Shop Item 5": LocationData(110034),
- "Temple of the Art Holy Mountain Spell Refresh": LocationData(110035),
- },
- "Laboratory Holy Mountain": {
- "Laboratory Holy Mountain Shop Item 1": LocationData(110036),
- "Laboratory Holy Mountain Shop Item 2": LocationData(110037),
- "Laboratory Holy Mountain Shop Item 3": LocationData(110038),
- "Laboratory Holy Mountain Shop Item 4": LocationData(110039),
- "Laboratory Holy Mountain Shop Item 5": LocationData(110040),
- "Laboratory Holy Mountain Spell Refresh": LocationData(110041),
- },
- "Secret Shop": {
- "Secret Shop Item 1": LocationData(110042),
- "Secret Shop Item 2": LocationData(110043),
- "Secret Shop Item 3": LocationData(110044),
- "Secret Shop Item 4": LocationData(110045),
- },
- "Floating Island": {
- "Floating Island Orb": LocationData(110658, LocationFlag.main_path, "orb"),
- },
- "Pyramid": {
- "Kolmisilmän Koipi": LocationData(110649, LocationFlag.main_world, "boss"),
- "Pyramid Orb": LocationData(110659, LocationFlag.main_world, "orb"),
- "Sandcave Orb": LocationData(110662, LocationFlag.main_world, "orb"),
- },
- "Overgrown Cavern": {
- "Overgrown Cavern Chest": LocationData(110526, LocationFlag.main_world, "chest"),
- "Overgrown Cavern Pedestal": LocationData(110546, LocationFlag.main_world, "pedestal"),
- },
- "Lake": {
- "Syväolento": LocationData(110651, LocationFlag.main_world, "boss"),
- },
- "Frozen Vault": {
- "Frozen Vault Orb": LocationData(110660, LocationFlag.main_world, "orb"),
- "Frozen Vault Chest": LocationData(110566, LocationFlag.main_world, "chest"),
- "Frozen Vault Pedestal": LocationData(110586, LocationFlag.main_world, "pedestal"),
- },
- "Mines": {
- "Mines Chest": LocationData(110046, LocationFlag.main_path, "chest"),
- "Mines Pedestal": LocationData(110066, LocationFlag.main_path, "pedestal"),
- },
- # Collapsed Mines is a very small area, combining it with the Mines. Leaving this here in case we change our minds.
- # "Collapsed Mines": {
- # "Collapsed Mines Chest": LocationData(110086, LocationFlag.main_path, "chest"),
- # "Collapsed Mines Pedestal": LocationData(110106, LocationFlag.main_path, "pedestal"),
- # },
- "Ancient Laboratory": {
- "Ylialkemisti": LocationData(110656, LocationFlag.side_path, "boss"),
- },
- "Abyss Orb Room": {
- "Sauvojen Tuntija": LocationData(110650, LocationFlag.side_path, "boss"),
- "Abyss Orb": LocationData(110665, LocationFlag.main_path, "orb"),
- },
- "Below Lava Lake": {
- "Lava Lake Orb": LocationData(110661, LocationFlag.side_path, "orb"),
- },
- "Coal Pits": {
- "Coal Pits Chest": LocationData(110126, LocationFlag.main_path, "chest"),
- "Coal Pits Pedestal": LocationData(110146, LocationFlag.main_path, "pedestal"),
- },
- "Fungal Caverns": {
- "Fungal Caverns Chest": LocationData(110166, LocationFlag.side_path, "chest"),
- "Fungal Caverns Pedestal": LocationData(110186, LocationFlag.side_path, "pedestal"),
- },
- "Snowy Depths": {
- "Snowy Depths Chest": LocationData(110206, LocationFlag.main_path, "chest"),
- "Snowy Depths Pedestal": LocationData(110226, LocationFlag.main_path, "pedestal"),
- },
- "Magical Temple": {
- "Magical Temple Orb": LocationData(110663, LocationFlag.side_path, "orb"),
- },
- "Hiisi Base": {
- "Hiisi Base Chest": LocationData(110246, LocationFlag.main_path, "chest"),
- "Hiisi Base Pedestal": LocationData(110266, LocationFlag.main_path, "pedestal"),
- },
- "Underground Jungle": {
- "Suomuhauki": LocationData(110648, LocationFlag.main_path, "boss"),
- "Underground Jungle Chest": LocationData(110286, LocationFlag.main_path, "chest"),
- "Underground Jungle Pedestal": LocationData(110306, LocationFlag.main_path, "pedestal"),
- },
- "Lukki Lair": {
- "Lukki Lair Orb": LocationData(110664, LocationFlag.side_path, "orb"),
- "Lukki Lair Chest": LocationData(110326, LocationFlag.side_path, "chest"),
- "Lukki Lair Pedestal": LocationData(110346, LocationFlag.side_path, "pedestal"),
- },
- "The Vault": {
- "The Vault Chest": LocationData(110366, LocationFlag.main_path, "chest"),
- "The Vault Pedestal": LocationData(110386, LocationFlag.main_path, "pedestal"),
- },
- "Temple of the Art": {
- "Gate Guardian": LocationData(110652, LocationFlag.main_path, "boss"),
- "Temple of the Art Chest": LocationData(110406, LocationFlag.main_path, "chest"),
- "Temple of the Art Pedestal": LocationData(110426, LocationFlag.main_path, "pedestal"),
- },
- "The Tower": {
- "The Tower Chest": LocationData(110606, LocationFlag.main_world, "chest"),
- "The Tower Pedestal": LocationData(110626, LocationFlag.main_world, "pedestal"),
- },
- "Wizards' Den": {
- "Mestarien Mestari": LocationData(110655, LocationFlag.main_world, "boss"),
- "Wizards' Den Orb": LocationData(110668, LocationFlag.main_world, "orb"),
- "Wizards' Den Chest": LocationData(110446, LocationFlag.main_world, "chest"),
- "Wizards' Den Pedestal": LocationData(110466, LocationFlag.main_world, "pedestal"),
- },
- "Powerplant": {
- "Kolmisilmän silmä": LocationData(110657, LocationFlag.main_world, "boss"),
- "Power Plant Chest": LocationData(110486, LocationFlag.main_world, "chest"),
- "Power Plant Pedestal": LocationData(110506, LocationFlag.main_world, "pedestal"),
- },
- "Snow Chasm": {
- "Unohdettu": LocationData(110653, LocationFlag.main_world, "boss"),
- "Snow Chasm Orb": LocationData(110667, LocationFlag.main_world, "orb"),
- },
- "Deep Underground": {
- "Limatoukka": LocationData(110647, LocationFlag.main_world, "boss"),
- },
- "The Laboratory": {
- "Kolmisilmä": LocationData(110646, LocationFlag.main_path, "boss"),
- },
- "Friend Cave": {
- "Toveri": LocationData(110654, LocationFlag.main_world, "boss"),
- },
- "The Work (Hell)": {
- "The Work (Hell) Orb": LocationData(110666, LocationFlag.main_world, "orb"),
- },
-}
-
-
-# Iterating the hidden chest and pedestal locations here to avoid clutter above
-def generate_location_entries(locname: str, locinfo: LocationData) -> Dict[str, int]:
- if locinfo.ltype in ["chest", "pedestal"]:
- return {f"{locname} {i + 1}": locinfo.id + i for i in range(20)}
- return {locname: locinfo.id}
-
-
-location_name_groups: Dict[str, Set[str]] = {"shop": set(), "orb": set(), "boss": set(), "chest": set(),
- "pedestal": set()}
-location_name_to_id: Dict[str, int] = {}
-
-
-for location_group in location_region_mapping.values():
- for locname, locinfo in location_group.items():
- location_name_to_id.update(generate_location_entries(locname, locinfo))
- if locinfo.ltype in ["chest", "pedestal"]:
- for i in range(20):
- location_name_groups[locinfo.ltype].add(f"{locname} {i + 1}")
- else:
- location_name_groups[locinfo.ltype].add(locname)
diff --git a/worlds/noita/Options.py b/worlds/noita/Options.py
deleted file mode 100644
index 0b54597f364d..000000000000
--- a/worlds/noita/Options.py
+++ /dev/null
@@ -1,114 +0,0 @@
-from typing import Dict
-from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool
-
-
-class PathOption(Choice):
- """Choose where you would like Hidden Chest and Pedestal checks to be placed.
- Main Path includes the main 7 biomes you typically go through to get to the final boss.
- Side Path includes the Lukki Lair and Fungal Caverns. 9 biomes total.
- Main World includes the full world (excluding parallel worlds). 14 biomes total.
- Note: The Collapsed Mines have been combined into the Mines as the biome is tiny."""
- display_name = "Path Option"
- option_main_path = 1
- option_side_path = 2
- option_main_world = 3
- default = 1
-
-
-class HiddenChests(Range):
- """Number of hidden chest checks added to the applicable biomes."""
- display_name = "Hidden Chests per Biome"
- range_start = 0
- range_end = 20
- default = 3
-
-
-class PedestalChecks(Range):
- """Number of checks that will spawn on pedestals in the applicable biomes."""
- display_name = "Pedestal Checks per Biome"
- range_start = 0
- range_end = 20
- default = 6
-
-
-class Traps(DefaultOnToggle):
- """Whether negative effects on the Noita world are added to the item pool."""
- display_name = "Traps"
-
-
-class OrbsAsChecks(Choice):
- """Decides whether finding the orbs that naturally spawn in the world count as checks.
- The Main Path option includes only the Floating Island and Abyss Orb Room orbs.
- The Side Path option includes the Main Path, Magical Temple, Lukki Lair, and Lava Lake orbs.
- The Main World option includes all 11 orbs."""
- display_name = "Orbs as Location Checks"
- option_no_orbs = 0
- option_main_path = 1
- option_side_path = 2
- option_main_world = 3
- default = 0
-
-
-class BossesAsChecks(Choice):
- """Makes bosses count as location checks. The boss only needs to die, you do not need the kill credit.
- The Main Path option includes Gate Guardian, Suomuhauki, and Kolmisilmä.
- The Side Path option includes the Main Path bosses, Sauvojen Tuntija, and Ylialkemisti.
- The All Bosses option includes all 12 bosses."""
- display_name = "Bosses as Location Checks"
- option_no_bosses = 0
- option_main_path = 1
- option_side_path = 2
- option_all_bosses = 3
- default = 0
-
-
-# Note: the Sampo is an item that is picked up to trigger the boss fight at the normal ending location.
-# The sampo is required for every ending (having orbs and bringing the sampo to a different spot changes the ending).
-class VictoryCondition(Choice):
- """Greed is to get to the bottom, beat the boss, and win the game.
- Pure is to get 11 orbs, grab the sampo, and bring it to the mountain altar.
- Peaceful is to get all 33 orbs, grab the sampo, and bring it to the mountain altar.
- Orbs will be added to the randomizer pool based on which victory condition you chose.
- The base game orbs will not count towards these victory conditions."""
- display_name = "Victory Condition"
- option_greed_ending = 0
- option_pure_ending = 1
- option_peaceful_ending = 2
- default = 0
-
-
-class ExtraOrbs(Range):
- """Add extra orbs to your item pool, to prevent you from needing to wait as long
- for the last orb you need for your victory condition.
- Extra orbs received past your victory condition's amount will be received as hearts instead.
- Can be turned on for the Greed Ending goal, but will only really make it harder."""
- display_name = "Extra Orbs"
- range_start = 0
- range_end = 10
- default = 0
-
-
-class ShopPrice(Choice):
- """Reduce the costs of Archipelago items in shops.
- By default, the price of Archipelago items matches the price of wands at that shop."""
- display_name = "Shop Price Reduction"
- option_full_price = 100
- option_25_percent_off = 75
- option_50_percent_off = 50
- option_75_percent_off = 25
- default = 100
-
-
-noita_options: Dict[str, AssembleOptions] = {
- "start_inventory_from_pool": StartInventoryPool,
- "death_link": DeathLink,
- "bad_effects": Traps,
- "victory_condition": VictoryCondition,
- "path_option": PathOption,
- "hidden_chests": HiddenChests,
- "pedestal_checks": PedestalChecks,
- "orbs_as_checks": OrbsAsChecks,
- "bosses_as_checks": BossesAsChecks,
- "extra_orbs": ExtraOrbs,
- "shop_price": ShopPrice,
-}
diff --git a/worlds/noita/Regions.py b/worlds/noita/Regions.py
deleted file mode 100644
index a239b437d75f..000000000000
--- a/worlds/noita/Regions.py
+++ /dev/null
@@ -1,148 +0,0 @@
-# Regions are areas in your game that you travel to.
-from typing import Dict, Set
-
-from BaseClasses import Entrance, MultiWorld, Region
-from . import Locations
-
-
-def add_location(player: int, loc_name: str, id: int, region: Region) -> None:
- location = Locations.NoitaLocation(player, loc_name, id, region)
- region.locations.append(location)
-
-
-def add_locations(multiworld: MultiWorld, player: int, region: Region) -> None:
- locations = Locations.location_region_mapping.get(region.name, {})
- for location_name, location_data in locations.items():
- location_type = location_data.ltype
- flag = location_data.flag
-
- opt_orbs = multiworld.orbs_as_checks[player].value
- opt_bosses = multiworld.bosses_as_checks[player].value
- opt_paths = multiworld.path_option[player].value
- opt_num_chests = multiworld.hidden_chests[player].value
- opt_num_pedestals = multiworld.pedestal_checks[player].value
-
- is_orb_allowed = location_type == "orb" and flag <= opt_orbs
- is_boss_allowed = location_type == "boss" and flag <= opt_bosses
- if flag == Locations.LocationFlag.none or is_orb_allowed or is_boss_allowed:
- add_location(player, location_name, location_data.id, region)
- elif location_type == "chest" and flag <= opt_paths:
- for i in range(opt_num_chests):
- add_location(player, f"{location_name} {i+1}", location_data.id + i, region)
- elif location_type == "pedestal" and flag <= opt_paths:
- for i in range(opt_num_pedestals):
- add_location(player, f"{location_name} {i+1}", location_data.id + i, region)
-
-
-# Creates a new Region with the locations found in `location_region_mapping` and adds them to the world.
-def create_region(multiworld: MultiWorld, player: int, region_name: str) -> Region:
- new_region = Region(region_name, player, multiworld)
- add_locations(multiworld, player, new_region)
- return new_region
-
-
-def create_regions(multiworld: MultiWorld, player: int) -> Dict[str, Region]:
- return {name: create_region(multiworld, player, name) for name in noita_regions}
-
-
-# An "Entrance" is really just a connection between two regions
-def create_entrance(player: int, source: str, destination: str, regions: Dict[str, Region]):
- entrance = Entrance(player, f"From {source} To {destination}", regions[source])
- entrance.connect(regions[destination])
- return entrance
-
-
-# Creates connections based on our access mapping in `noita_connections`.
-def create_connections(player: int, regions: Dict[str, Region]) -> None:
- for source, destinations in noita_connections.items():
- new_entrances = [create_entrance(player, source, destination, regions) for destination in destinations]
- regions[source].exits = new_entrances
-
-
-# Creates all regions and connections. Called from NoitaWorld.
-def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> None:
- created_regions = create_regions(multiworld, player)
- create_connections(player, created_regions)
-
- multiworld.regions += created_regions.values()
-
-
-# Oh, what a tangled web we weave
-# Notes to create artificial spheres:
-# - Shaft is excluded to disconnect Mines from the Snowy Depths
-# - Lukki Lair is disconnected from The Vault
-# - Overgrown Cavern is connected to the Underground Jungle instead of the Desert due to similar difficulty
-# - Powerplant is disconnected from the Sandcave due to difficulty and sphere creation
-# - Snow Chasm is disconnected from the Snowy Wasteland
-# - Pyramid is connected to the Hiisi Base instead of the Desert due to similar difficulty
-# - Frozen Vault is connected to the Vault instead of the Snowy Wasteland due to similar difficulty
-# - Lake is connected to The Laboratory, since the boss is hard without specific set-ups (which means late game)
-# - Snowy Depths connects to Lava Lake orb since you need digging for it, so fairly early is acceptable
-# - Ancient Laboratory is connected to the Coal Pits, so that Ylialkemisti isn't sphere 1
-noita_connections: Dict[str, Set[str]] = {
- "Menu": {"Forest"},
- "Forest": {"Mines", "Floating Island", "Desert", "Snowy Wasteland"},
- "Snowy Wasteland": {"Forest"},
- "Frozen Vault": {"The Vault"},
- "Lake": {"The Laboratory"},
- "Desert": {"Forest"},
- "Floating Island": {"Forest"},
- "Pyramid": {"Hiisi Base"},
- "Overgrown Cavern": {"Sandcave", "Undeground Jungle"},
- "Sandcave": {"Overgrown Cavern"},
-
- ###
- "Mines": {"Collapsed Mines", "Coal Pits Holy Mountain", "Lava Lake", "Forest"},
- "Collapsed Mines": {"Mines", "Dark Cave"},
- "Lava Lake": {"Mines", "Abyss Orb Room"},
- "Abyss Orb Room": {"Lava Lake"},
- "Below Lava Lake": {"Snowy Depths"},
- "Dark Cave": {"Collapsed Mines"},
- "Ancient Laboratory": {"Coal Pits"},
-
- ###
- "Coal Pits Holy Mountain": {"Coal Pits"},
- "Coal Pits": {"Coal Pits Holy Mountain", "Fungal Caverns", "Snowy Depths Holy Mountain", "Ancient Laboratory"},
- "Fungal Caverns": {"Coal Pits"},
-
- ###
- "Snowy Depths Holy Mountain": {"Snowy Depths"},
- "Snowy Depths": {"Snowy Depths Holy Mountain", "Hiisi Base Holy Mountain", "Magical Temple", "Below Lava Lake"},
- "Magical Temple": {"Snowy Depths"},
-
- ###
- "Hiisi Base Holy Mountain": {"Hiisi Base"},
- "Hiisi Base": {"Hiisi Base Holy Mountain", "Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"},
- "Secret Shop": {"Hiisi Base"},
-
- ###
- "Underground Jungle Holy Mountain": {"Underground Jungle"},
- "Underground Jungle": {"Underground Jungle Holy Mountain", "Dragoncave", "Overgrown Cavern", "Vault Holy Mountain",
- "Lukki Lair"},
- "Dragoncave": {"Underground Jungle"},
- "Lukki Lair": {"Underground Jungle", "Snow Chasm", "Frozen Vault"},
- "Snow Chasm": {},
-
- ###
- "Vault Holy Mountain": {"The Vault"},
- "The Vault": {"Vault Holy Mountain", "Frozen Vault", "Temple of the Art Holy Mountain"},
-
- ###
- "Temple of the Art Holy Mountain": {"Temple of the Art"},
- "Temple of the Art": {"Temple of the Art Holy Mountain", "Laboratory Holy Mountain", "The Tower",
- "Wizards' Den"},
- "Wizards' Den": {"Temple of the Art", "Powerplant"},
- "Powerplant": {"Wizards' Den", "Deep Underground"},
- "The Tower": {"Forest"},
- "Deep Underground": {},
-
- ###
- "Laboratory Holy Mountain": {"The Laboratory"},
- "The Laboratory": {"Laboratory Holy Mountain", "The Work", "Friend Cave", "The Work (Hell)", "Lake"},
- "Friend Cave": {},
- "The Work": {},
- "The Work (Hell)": {},
- ###
-}
-
-noita_regions: Set[str] = set(noita_connections.keys()).union(*noita_connections.values())
diff --git a/worlds/noita/Rules.py b/worlds/noita/Rules.py
deleted file mode 100644
index 3eb6be5a7c6d..000000000000
--- a/worlds/noita/Rules.py
+++ /dev/null
@@ -1,167 +0,0 @@
-from typing import List, NamedTuple, Set
-
-from BaseClasses import CollectionState, MultiWorld
-from . import Items, Locations
-from .Options import BossesAsChecks, VictoryCondition
-from worlds.generic import Rules as GenericRules
-
-
-class EntranceLock(NamedTuple):
- source: str
- destination: str
- event: str
- items_needed: int
-
-
-entrance_locks: List[EntranceLock] = [
- EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1),
- EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2),
- EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3),
- EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4),
- EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5),
- EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6),
- EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7),
-]
-
-
-holy_mountain_regions: List[str] = [
- "Coal Pits Holy Mountain",
- "Snowy Depths Holy Mountain",
- "Hiisi Base Holy Mountain",
- "Underground Jungle Holy Mountain",
- "Vault Holy Mountain",
- "Temple of the Art Holy Mountain",
- "Laboratory Holy Mountain",
-]
-
-
-wand_tiers: List[str] = [
- "Wand (Tier 1)", # Coal Pits
- "Wand (Tier 2)", # Snowy Depths
- "Wand (Tier 3)", # Hiisi Base
- "Wand (Tier 4)", # Underground Jungle
- "Wand (Tier 5)", # The Vault
- "Wand (Tier 6)", # Temple of the Art
-]
-
-
-items_hidden_from_shops: List[str] = ["Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
- "Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand",
- "Powder Pouch"]
-
-
-perk_list: List[str] = list(filter(Items.item_is_perk, Items.item_table.keys()))
-
-
-# ----------------
-# Helper Functions
-# ----------------
-
-
-def has_perk_count(state: CollectionState, player: int, amount: int) -> bool:
- return sum(state.item_count(perk, player) for perk in perk_list) >= amount
-
-
-def has_orb_count(state: CollectionState, player: int, amount: int) -> bool:
- return state.item_count("Orb", player) >= amount
-
-
-def forbid_items_at_location(multiworld: MultiWorld, location_name: str, items: Set[str], player: int):
- location = multiworld.get_location(location_name, player)
- GenericRules.forbid_items_for_player(location, items, player)
-
-
-# ----------------
-# Rule Functions
-# ----------------
-
-
-# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them)
-def ban_items_from_shops(multiworld: MultiWorld, player: int) -> None:
- for location_name in Locations.location_name_to_id.keys():
- if "Shop Item" in location_name:
- forbid_items_at_location(multiworld, location_name, items_hidden_from_shops, player)
-
-
-# Prevent high tier wands from appearing in early Holy Mountain shops
-def ban_early_high_tier_wands(multiworld: MultiWorld, player: int) -> None:
- for i, region_name in enumerate(holy_mountain_regions):
- wands_to_forbid = wand_tiers[i+1:]
-
- locations_in_region = Locations.location_region_mapping[region_name].keys()
- for location_name in locations_in_region:
- forbid_items_at_location(multiworld, location_name, wands_to_forbid, player)
-
- # Prevent high tier wands from appearing in the Secret shop
- wands_to_forbid = wand_tiers[3:]
- locations_in_region = Locations.location_region_mapping["Secret Shop"].keys()
- for location_name in locations_in_region:
- forbid_items_at_location(multiworld, location_name, wands_to_forbid, player)
-
-
-def lock_holy_mountains_into_spheres(multiworld: MultiWorld, player: int) -> None:
- for lock in entrance_locks:
- location = multiworld.get_entrance(f"From {lock.source} To {lock.destination}", player)
- GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, player))
-
-
-def holy_mountain_unlock_conditions(multiworld: MultiWorld, player: int) -> None:
- victory_condition = multiworld.victory_condition[player].value
- for lock in entrance_locks:
- location = multiworld.get_location(lock.event, player)
-
- if victory_condition == VictoryCondition.option_greed_ending:
- location.access_rule = lambda state, items_needed=lock.items_needed: (
- has_perk_count(state, player, items_needed//2)
- )
- elif victory_condition == VictoryCondition.option_pure_ending:
- location.access_rule = lambda state, items_needed=lock.items_needed: (
- has_perk_count(state, player, items_needed//2) and
- has_orb_count(state, player, items_needed)
- )
- elif victory_condition == VictoryCondition.option_peaceful_ending:
- location.access_rule = lambda state, items_needed=lock.items_needed: (
- has_perk_count(state, player, items_needed//2) and
- has_orb_count(state, player, items_needed * 3)
- )
-
-
-def biome_unlock_conditions(multiworld: MultiWorld, player: int):
- lukki_entrances = multiworld.get_region("Lukki Lair", player).entrances
- magical_entrances = multiworld.get_region("Magical Temple", player).entrances
- wizard_entrances = multiworld.get_region("Wizards' Den", player).entrances
- for entrance in lukki_entrances:
- entrance.access_rule = lambda state: state.has("Melee Immunity Perk", player) and\
- state.has("All-Seeing Eye Perk", player)
- for entrance in magical_entrances:
- entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", player)
- for entrance in wizard_entrances:
- entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", player)
-
-
-def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None:
- victory_condition = multiworld.victory_condition[player].value
- victory_location = multiworld.get_location("Victory", player)
-
- if victory_condition == VictoryCondition.option_pure_ending:
- victory_location.access_rule = lambda state: has_orb_count(state, player, 11)
- elif victory_condition == VictoryCondition.option_peaceful_ending:
- victory_location.access_rule = lambda state: has_orb_count(state, player, 33)
-
-
-# ----------------
-# Main Function
-# ----------------
-
-
-def create_all_rules(multiworld: MultiWorld, player: int) -> None:
- ban_items_from_shops(multiworld, player)
- ban_early_high_tier_wands(multiworld, player)
- lock_holy_mountains_into_spheres(multiworld, player)
- holy_mountain_unlock_conditions(multiworld, player)
- biome_unlock_conditions(multiworld, player)
- victory_unlock_conditions(multiworld, player)
-
- # Prevent the Map perk (used to find Toveri) from being on Toveri (boss)
- if multiworld.bosses_as_checks[player].value >= BossesAsChecks.option_all_bosses:
- forbid_items_at_location(multiworld, "Toveri", {"Spatial Awareness Perk"}, player)
diff --git a/worlds/noita/__init__.py b/worlds/noita/__init__.py
index 499d202a64f4..af2921768d6a 100644
--- a/worlds/noita/__init__.py
+++ b/worlds/noita/__init__.py
@@ -1,6 +1,8 @@
from BaseClasses import Item, Tutorial
from worlds.AutoWorld import WebWorld, World
-from . import Events, Items, Locations, Options, Regions, Rules
+from typing import Dict, Any
+from . import events, items, locations, regions, rules
+from .options import NoitaOptions
class NoitaWeb(WebWorld):
@@ -24,33 +26,37 @@ class NoitaWorld(World):
"""
game = "Noita"
- option_definitions = Options.noita_options
+ options: NoitaOptions
+ options_dataclass = NoitaOptions
- item_name_to_id = Items.item_name_to_id
- location_name_to_id = Locations.location_name_to_id
+ item_name_to_id = items.item_name_to_id
+ location_name_to_id = locations.location_name_to_id
- item_name_groups = Items.item_name_groups
- location_name_groups = Locations.location_name_groups
- data_version = 2
+ item_name_groups = items.item_name_groups
+ location_name_groups = locations.location_name_groups
web = NoitaWeb()
+ def generate_early(self) -> None:
+ if not self.multiworld.get_player_name(self.player).isascii():
+ raise Exception("Noita yaml's slot name has invalid character(s).")
+
# Returned items will be sent over to the client
- def fill_slot_data(self):
- return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions}
+ def fill_slot_data(self) -> Dict[str, Any]:
+ return self.options.as_dict("death_link", "victory_condition", "path_option", "hidden_chests",
+ "pedestal_checks", "orbs_as_checks", "bosses_as_checks", "extra_orbs", "shop_price")
def create_regions(self) -> None:
- Regions.create_all_regions_and_connections(self.multiworld, self.player)
- Events.create_all_events(self.multiworld, self.player)
+ regions.create_all_regions_and_connections(self)
def create_item(self, name: str) -> Item:
- return Items.create_item(self.player, name)
+ return items.create_item(self.player, name)
def create_items(self) -> None:
- Items.create_all_items(self.multiworld, self.player)
+ items.create_all_items(self)
def set_rules(self) -> None:
- Rules.create_all_rules(self.multiworld, self.player)
+ rules.create_all_rules(self)
def get_filler_item_name(self) -> str:
- return self.multiworld.random.choice(Items.filler_items)
+ return self.random.choice(items.filler_items)
diff --git a/worlds/noita/docs/en_Noita.md b/worlds/noita/docs/en_Noita.md
index b1480068e96c..1e560cfcb748 100644
--- a/worlds/noita/docs/en_Noita.md
+++ b/worlds/noita/docs/en_Noita.md
@@ -1,15 +1,15 @@
# Noita
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Noita is a procedurally generated action roguelike. During runs in Noita you will find potions, wands, spells, perks,
chests, etc. Shop items, chests/hearts hidden in the environment, and pedestal items will be replaced with location
-checks. Orbs and boss drops can give location checks as well, if they are enabled in the settings.
+checks. Orbs and boss drops can give location checks as well, if their respective options are enabled.
Noita items that can be found in other players' games include specific perks, orbs (optional), wands, hearts, gold,
potions, and other items. If traps are enabled, some randomized negative effects can affect your game when found.
@@ -50,9 +50,9 @@ Traps consist of all "Bad" and "Awful" events from Noita's native stream integra
## How many location checks are there?
-When using the default settings, there are 109 location checks. The number of checks in the game is dependent on the settings that you choose.
-Please check the information boxes next to the settings when setting up your YAML to see how many checks the individual options add.
-There are always 42 Holy Mountain checks and 4 Secret Shop checks in the pool which are not affected by your YAML settings.
+When using the default options, there are 109 location checks. The number of checks in the game is dependent on the options that you choose.
+Please check the information boxes next to the options when setting up your YAML to see how many checks the individual options add.
+There are always 42 Holy Mountain checks and 4 Secret Shop checks in the pool which are not affected by your YAML options.
## What does another world's item look like in Noita?
diff --git a/worlds/noita/docs/setup_en.md b/worlds/noita/docs/setup_en.md
index bd6a151432bd..25c6cbc948bc 100644
--- a/worlds/noita/docs/setup_en.md
+++ b/worlds/noita/docs/setup_en.md
@@ -40,9 +40,11 @@ or try restarting your game.
### What is a YAML and why do I need one?
You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn
about why Archipelago uses YAML files and what they're for.
+Please note that Noita only allows you to type certain characters for your slot name.
+These characters are: `` !#$%&'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~<>|\/``
### Where do I get a YAML?
-You can use the [game settings page for Noita](/games/Noita/player-settings) here on the Archipelago website to
+You can use the [game options page for Noita](/games/Noita/player-options) here on the Archipelago website to
generate a YAML using a graphical interface.
## Poptracker Pack
@@ -54,4 +56,4 @@ Place the unzipped pack in the `packs` folder. Then, open Poptracker and open th
Click on the "AP" symbol at the top, then enter the desired address, slot name, and password.
That's all you need for it. It will provide you with a quick reference to see which checks you've done and
-which checks you still have left.
\ No newline at end of file
+which checks you still have left.
diff --git a/worlds/noita/events.py b/worlds/noita/events.py
new file mode 100644
index 000000000000..4ec04e98b457
--- /dev/null
+++ b/worlds/noita/events.py
@@ -0,0 +1,43 @@
+from typing import Dict, TYPE_CHECKING
+from BaseClasses import Item, ItemClassification, Location, Region
+from . import items, locations
+
+if TYPE_CHECKING:
+ from . import NoitaWorld
+
+
+def create_event(player: int, name: str) -> Item:
+ return items.NoitaItem(name, ItemClassification.progression, None, player)
+
+
+def create_location(player: int, name: str, region: Region) -> Location:
+ return locations.NoitaLocation(player, name, None, region)
+
+
+def create_locked_location_event(player: int, region: Region, item: str) -> Location:
+ new_location = create_location(player, item, region)
+ new_location.place_locked_item(create_event(player, item))
+
+ region.locations.append(new_location)
+ return new_location
+
+
+def create_all_events(world: "NoitaWorld", created_regions: Dict[str, Region]) -> None:
+ for region_name, event in event_locks.items():
+ region = created_regions[region_name]
+ create_locked_location_event(world.player, region, event)
+
+ world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
+
+
+# Maps region names to event names
+event_locks: Dict[str, str] = {
+ "The Work": "Victory",
+ "Mines": "Portal to Holy Mountain 1",
+ "Coal Pits": "Portal to Holy Mountain 2",
+ "Snowy Depths": "Portal to Holy Mountain 3",
+ "Hiisi Base": "Portal to Holy Mountain 4",
+ "Underground Jungle": "Portal to Holy Mountain 5",
+ "The Vault": "Portal to Holy Mountain 6",
+ "Temple of the Art": "Portal to Holy Mountain 7",
+}
diff --git a/worlds/noita/items.py b/worlds/noita/items.py
new file mode 100644
index 000000000000..6b662fbee692
--- /dev/null
+++ b/worlds/noita/items.py
@@ -0,0 +1,172 @@
+import itertools
+from collections import Counter
+from typing import Dict, List, NamedTuple, Set, TYPE_CHECKING
+
+from BaseClasses import Item, ItemClassification
+from .options import BossesAsChecks, VictoryCondition, ExtraOrbs
+
+if TYPE_CHECKING:
+ from . import NoitaWorld
+else:
+ NoitaWorld = object
+
+
+class ItemData(NamedTuple):
+ code: int
+ group: str
+ classification: ItemClassification = ItemClassification.progression
+ required_num: int = 0
+
+
+class NoitaItem(Item):
+ game: str = "Noita"
+
+
+def create_item(player: int, name: str) -> Item:
+ item_data = item_table[name]
+ return NoitaItem(name, item_data.classification, item_data.code, player)
+
+
+def create_fixed_item_pool() -> List[str]:
+ required_items: Dict[str, int] = {name: data.required_num for name, data in item_table.items()}
+ return list(Counter(required_items).elements())
+
+
+def create_orb_items(victory_condition: VictoryCondition, extra_orbs: ExtraOrbs) -> List[str]:
+ orb_count = extra_orbs.value
+ if victory_condition == VictoryCondition.option_pure_ending:
+ orb_count = orb_count + 11
+ elif victory_condition == VictoryCondition.option_peaceful_ending:
+ orb_count = orb_count + 33
+ return ["Orb" for _ in range(orb_count)]
+
+
+def create_spatial_awareness_item(bosses_as_checks: BossesAsChecks) -> List[str]:
+ return ["Spatial Awareness Perk"] if bosses_as_checks.value >= BossesAsChecks.option_all_bosses else []
+
+
+def create_kantele(victory_condition: VictoryCondition) -> List[str]:
+ return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else []
+
+
+def create_random_items(world: NoitaWorld, weights: Dict[str, int], count: int) -> List[str]:
+ filler_pool = weights.copy()
+ if not world.options.bad_effects:
+ del filler_pool["Trap"]
+
+ return world.random.choices(population=list(filler_pool.keys()),
+ weights=list(filler_pool.values()),
+ k=count)
+
+
+def create_all_items(world: NoitaWorld) -> None:
+ player = world.player
+ locations_to_fill = len(world.multiworld.get_unfilled_locations(player))
+
+ itempool = (
+ create_fixed_item_pool()
+ + create_orb_items(world.options.victory_condition, world.options.extra_orbs)
+ + create_spatial_awareness_item(world.options.bosses_as_checks)
+ + create_kantele(world.options.victory_condition)
+ )
+
+ # if there's not enough shop-allowed items in the pool, we can encounter gen issues
+ # 39 is the number of shop-valid items we need to guarantee
+ if len(itempool) < 39:
+ itempool += create_random_items(world, shop_only_filler_weights, 39 - len(itempool))
+ # this is so that it passes tests and gens if you have minimal locations and only one player
+ if world.multiworld.players == 1:
+ for location in world.multiworld.get_unfilled_locations(player):
+ if "Shop Item" in location.name:
+ location.item = create_item(player, itempool.pop())
+ locations_to_fill = len(world.multiworld.get_unfilled_locations(player))
+
+ itempool += create_random_items(world, filler_weights, locations_to_fill - len(itempool))
+ world.multiworld.itempool += [create_item(player, name) for name in itempool]
+
+
+# 110000 - 110032
+item_table: Dict[str, ItemData] = {
+ "Trap": ItemData(110000, "Traps", ItemClassification.trap),
+ "Extra Max HP": ItemData(110001, "Pickups", ItemClassification.useful),
+ "Spell Refresher": ItemData(110002, "Pickups", ItemClassification.filler),
+ "Potion": ItemData(110003, "Items", ItemClassification.filler),
+ "Gold (200)": ItemData(110004, "Gold", ItemClassification.filler),
+ "Gold (1000)": ItemData(110005, "Gold", ItemClassification.filler),
+ "Wand (Tier 1)": ItemData(110006, "Wands", ItemClassification.useful),
+ "Wand (Tier 2)": ItemData(110007, "Wands", ItemClassification.useful),
+ "Wand (Tier 3)": ItemData(110008, "Wands", ItemClassification.useful),
+ "Wand (Tier 4)": ItemData(110009, "Wands", ItemClassification.useful),
+ "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful, 1),
+ "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful, 1),
+ "Kantele": ItemData(110012, "Wands", ItemClassification.useful),
+ "Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression, 1),
+ "Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression, 1),
+ "Explosion Immunity Perk": ItemData(110015, "Perks", ItemClassification.progression, 1),
+ "Melee Immunity Perk": ItemData(110016, "Perks", ItemClassification.progression, 1),
+ "Electricity Immunity Perk": ItemData(110017, "Perks", ItemClassification.progression, 1),
+ "Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression, 1),
+ "All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression, 1),
+ "Spatial Awareness Perk": ItemData(110020, "Perks", ItemClassification.progression),
+ "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful, 1),
+ "Orb": ItemData(110022, "Orbs", ItemClassification.progression_skip_balancing),
+ "Random Potion": ItemData(110023, "Items", ItemClassification.filler),
+ "Secret Potion": ItemData(110024, "Items", ItemClassification.filler),
+ "Powder Pouch": ItemData(110025, "Items", ItemClassification.filler),
+ "Chaos Die": ItemData(110026, "Items", ItemClassification.filler),
+ "Greed Die": ItemData(110027, "Items", ItemClassification.filler),
+ "Kammi": ItemData(110028, "Items", ItemClassification.filler, 1),
+ "Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler, 1),
+ "Sädekivi": ItemData(110030, "Items", ItemClassification.filler),
+ "Broken Wand": ItemData(110031, "Items", ItemClassification.filler),
+}
+
+shop_only_filler_weights: Dict[str, int] = {
+ "Trap": 15,
+ "Extra Max HP": 25,
+ "Spell Refresher": 20,
+ "Wand (Tier 1)": 10,
+ "Wand (Tier 2)": 8,
+ "Wand (Tier 3)": 7,
+ "Wand (Tier 4)": 6,
+ "Wand (Tier 5)": 5,
+ "Wand (Tier 6)": 4,
+ "Extra Life Perk": 10,
+}
+
+filler_weights: Dict[str, int] = {
+ **shop_only_filler_weights,
+ "Gold (200)": 15,
+ "Gold (1000)": 6,
+ "Potion": 40,
+ "Random Potion": 9,
+ "Secret Potion": 10,
+ "Powder Pouch": 10,
+ "Chaos Die": 4,
+ "Greed Die": 4,
+ "Kammi": 4,
+ "Refreshing Gourd": 4,
+ "Sädekivi": 3,
+ "Broken Wand": 10,
+}
+
+
+# These helper functions make the comprehensions below more readable
+def get_item_group(item_name: str) -> str:
+ return item_table[item_name].group
+
+
+def item_is_filler(item_name: str) -> bool:
+ return item_table[item_name].classification == ItemClassification.filler
+
+
+def item_is_perk(item_name: str) -> bool:
+ return item_table[item_name].group == "Perks"
+
+
+filler_items: List[str] = list(filter(item_is_filler, item_table.keys()))
+item_name_to_id: Dict[str, int] = {name: data.code for name, data in item_table.items()}
+
+item_name_groups: Dict[str, Set[str]] = {
+ group: set(item_names) for group, item_names in itertools.groupby(item_table, get_item_group)
+}
diff --git a/worlds/noita/locations.py b/worlds/noita/locations.py
new file mode 100644
index 000000000000..5dd87b5b0387
--- /dev/null
+++ b/worlds/noita/locations.py
@@ -0,0 +1,232 @@
+# Locations are specific points that you would obtain an item at.
+from enum import IntEnum
+from typing import Dict, NamedTuple, Optional, Set
+
+from BaseClasses import Location
+
+
+class NoitaLocation(Location):
+ game: str = "Noita"
+
+
+class LocationData(NamedTuple):
+ id: int
+ flag: int = 0
+ ltype: str = "Shop"
+
+
+class LocationFlag(IntEnum):
+ none = 0
+ main_path = 1
+ side_path = 2
+ main_world = 3
+ parallel_worlds = 4
+
+
+# Mapping of items in each region.
+# Only the first Hidden Chest and Pedestal are mapped here, the others are created in Regions.
+# ltype key: "Chest" = Hidden Chests, "Pedestal" = Pedestals, "Boss" = Boss, "Orb" = Orb.
+# 110000-110671
+location_region_mapping: Dict[str, Dict[str, LocationData]] = {
+ "Coal Pits Holy Mountain": {
+ "Coal Pits Holy Mountain Shop Item 1": LocationData(110000),
+ "Coal Pits Holy Mountain Shop Item 2": LocationData(110001),
+ "Coal Pits Holy Mountain Shop Item 3": LocationData(110002),
+ "Coal Pits Holy Mountain Shop Item 4": LocationData(110003),
+ "Coal Pits Holy Mountain Shop Item 5": LocationData(110004),
+ "Coal Pits Holy Mountain Spell Refresh": LocationData(110005),
+ },
+ "Snowy Depths Holy Mountain": {
+ "Snowy Depths Holy Mountain Shop Item 1": LocationData(110006),
+ "Snowy Depths Holy Mountain Shop Item 2": LocationData(110007),
+ "Snowy Depths Holy Mountain Shop Item 3": LocationData(110008),
+ "Snowy Depths Holy Mountain Shop Item 4": LocationData(110009),
+ "Snowy Depths Holy Mountain Shop Item 5": LocationData(110010),
+ "Snowy Depths Holy Mountain Spell Refresh": LocationData(110011),
+ },
+ "Hiisi Base Holy Mountain": {
+ "Hiisi Base Holy Mountain Shop Item 1": LocationData(110012),
+ "Hiisi Base Holy Mountain Shop Item 2": LocationData(110013),
+ "Hiisi Base Holy Mountain Shop Item 3": LocationData(110014),
+ "Hiisi Base Holy Mountain Shop Item 4": LocationData(110015),
+ "Hiisi Base Holy Mountain Shop Item 5": LocationData(110016),
+ "Hiisi Base Holy Mountain Spell Refresh": LocationData(110017),
+ },
+ "Underground Jungle Holy Mountain": {
+ "Underground Jungle Holy Mountain Shop Item 1": LocationData(110018),
+ "Underground Jungle Holy Mountain Shop Item 2": LocationData(110019),
+ "Underground Jungle Holy Mountain Shop Item 3": LocationData(110020),
+ "Underground Jungle Holy Mountain Shop Item 4": LocationData(110021),
+ "Underground Jungle Holy Mountain Shop Item 5": LocationData(110022),
+ "Underground Jungle Holy Mountain Spell Refresh": LocationData(110023),
+ },
+ "Vault Holy Mountain": {
+ "Vault Holy Mountain Shop Item 1": LocationData(110024),
+ "Vault Holy Mountain Shop Item 2": LocationData(110025),
+ "Vault Holy Mountain Shop Item 3": LocationData(110026),
+ "Vault Holy Mountain Shop Item 4": LocationData(110027),
+ "Vault Holy Mountain Shop Item 5": LocationData(110028),
+ "Vault Holy Mountain Spell Refresh": LocationData(110029),
+ },
+ "Temple of the Art Holy Mountain": {
+ "Temple of the Art Holy Mountain Shop Item 1": LocationData(110030),
+ "Temple of the Art Holy Mountain Shop Item 2": LocationData(110031),
+ "Temple of the Art Holy Mountain Shop Item 3": LocationData(110032),
+ "Temple of the Art Holy Mountain Shop Item 4": LocationData(110033),
+ "Temple of the Art Holy Mountain Shop Item 5": LocationData(110034),
+ "Temple of the Art Holy Mountain Spell Refresh": LocationData(110035),
+ },
+ "Laboratory Holy Mountain": {
+ "Laboratory Holy Mountain Shop Item 1": LocationData(110036),
+ "Laboratory Holy Mountain Shop Item 2": LocationData(110037),
+ "Laboratory Holy Mountain Shop Item 3": LocationData(110038),
+ "Laboratory Holy Mountain Shop Item 4": LocationData(110039),
+ "Laboratory Holy Mountain Shop Item 5": LocationData(110040),
+ "Laboratory Holy Mountain Spell Refresh": LocationData(110041),
+ },
+ "Secret Shop": {
+ "Secret Shop Item 1": LocationData(110042),
+ "Secret Shop Item 2": LocationData(110043),
+ "Secret Shop Item 3": LocationData(110044),
+ "Secret Shop Item 4": LocationData(110045),
+ },
+ "The Sky": {
+ "Kivi": LocationData(110670, LocationFlag.main_world, "Boss"),
+ },
+ "Floating Island": {
+ "Floating Island Orb": LocationData(110658, LocationFlag.main_path, "Orb"),
+ },
+ "Pyramid": {
+ "Kolmisilmän Koipi": LocationData(110649, LocationFlag.main_world, "Boss"),
+ "Pyramid Orb": LocationData(110659, LocationFlag.main_world, "Orb"),
+ "Sandcave Orb": LocationData(110662, LocationFlag.main_world, "Orb"),
+ },
+ "Overgrown Cavern": {
+ "Overgrown Cavern Chest": LocationData(110526, LocationFlag.main_world, "Chest"),
+ "Overgrown Cavern Pedestal": LocationData(110546, LocationFlag.main_world, "Pedestal"),
+ },
+ "Lake": {
+ "Syväolento": LocationData(110651, LocationFlag.main_world, "Boss"),
+ "Tapion vasalli": LocationData(110669, LocationFlag.main_world, "Boss"),
+ },
+ "Frozen Vault": {
+ "Frozen Vault Orb": LocationData(110660, LocationFlag.main_world, "Orb"),
+ "Frozen Vault Chest": LocationData(110566, LocationFlag.main_world, "Chest"),
+ "Frozen Vault Pedestal": LocationData(110586, LocationFlag.main_world, "Pedestal"),
+ },
+ "Mines": {
+ "Mines Chest": LocationData(110046, LocationFlag.main_path, "Chest"),
+ "Mines Pedestal": LocationData(110066, LocationFlag.main_path, "Pedestal"),
+ },
+ # Collapsed Mines is a very small area, combining it with the Mines. Leaving this here as a reminder
+
+ "Ancient Laboratory": {
+ "Ylialkemisti": LocationData(110656, LocationFlag.side_path, "Boss"),
+ },
+ "Abyss Orb Room": {
+ "Sauvojen Tuntija": LocationData(110650, LocationFlag.side_path, "Boss"),
+ "Abyss Orb": LocationData(110665, LocationFlag.main_path, "Orb"),
+ },
+ "Below Lava Lake": {
+ "Lava Lake Orb": LocationData(110661, LocationFlag.side_path, "Orb"),
+ },
+ "Coal Pits": {
+ "Coal Pits Chest": LocationData(110126, LocationFlag.main_path, "Chest"),
+ "Coal Pits Pedestal": LocationData(110146, LocationFlag.main_path, "Pedestal"),
+ },
+ "Fungal Caverns": {
+ "Fungal Caverns Chest": LocationData(110166, LocationFlag.side_path, "Chest"),
+ "Fungal Caverns Pedestal": LocationData(110186, LocationFlag.side_path, "Pedestal"),
+ },
+ "Snowy Depths": {
+ "Snowy Depths Chest": LocationData(110206, LocationFlag.main_path, "Chest"),
+ "Snowy Depths Pedestal": LocationData(110226, LocationFlag.main_path, "Pedestal"),
+ },
+ "Magical Temple": {
+ "Magical Temple Orb": LocationData(110663, LocationFlag.side_path, "Orb"),
+ },
+ "Hiisi Base": {
+ "Hiisi Base Chest": LocationData(110246, LocationFlag.main_path, "Chest"),
+ "Hiisi Base Pedestal": LocationData(110266, LocationFlag.main_path, "Pedestal"),
+ },
+ "Underground Jungle": {
+ "Suomuhauki": LocationData(110648, LocationFlag.main_path, "Boss"),
+ "Underground Jungle Chest": LocationData(110286, LocationFlag.main_path, "Chest"),
+ "Underground Jungle Pedestal": LocationData(110306, LocationFlag.main_path, "Pedestal"),
+ },
+ "Lukki Lair": {
+ "Lukki Lair Orb": LocationData(110664, LocationFlag.side_path, "Orb"),
+ "Lukki Lair Chest": LocationData(110326, LocationFlag.side_path, "Chest"),
+ "Lukki Lair Pedestal": LocationData(110346, LocationFlag.side_path, "Pedestal"),
+ },
+ "The Vault": {
+ "The Vault Chest": LocationData(110366, LocationFlag.main_path, "Chest"),
+ "The Vault Pedestal": LocationData(110386, LocationFlag.main_path, "Pedestal"),
+ },
+ "Temple of the Art": {
+ "Gate Guardian": LocationData(110652, LocationFlag.main_path, "Boss"),
+ "Temple of the Art Chest": LocationData(110406, LocationFlag.main_path, "Chest"),
+ "Temple of the Art Pedestal": LocationData(110426, LocationFlag.main_path, "Pedestal"),
+ },
+ "The Tower": {
+ "The Tower Chest": LocationData(110606, LocationFlag.main_world, "Chest"),
+ "The Tower Pedestal": LocationData(110626, LocationFlag.main_world, "Pedestal"),
+ },
+ "Wizards' Den": {
+ "Mestarien Mestari": LocationData(110655, LocationFlag.main_world, "Boss"),
+ "Wizards' Den Orb": LocationData(110668, LocationFlag.main_world, "Orb"),
+ "Wizards' Den Chest": LocationData(110446, LocationFlag.main_world, "Chest"),
+ "Wizards' Den Pedestal": LocationData(110466, LocationFlag.main_world, "Pedestal"),
+ },
+ "Powerplant": {
+ "Kolmisilmän silmä": LocationData(110657, LocationFlag.main_world, "Boss"),
+ "Power Plant Chest": LocationData(110486, LocationFlag.main_world, "Chest"),
+ "Power Plant Pedestal": LocationData(110506, LocationFlag.main_world, "Pedestal"),
+ },
+ "Snow Chasm": {
+ "Unohdettu": LocationData(110653, LocationFlag.main_world, "Boss"),
+ "Snow Chasm Orb": LocationData(110667, LocationFlag.main_world, "Orb"),
+ },
+ "Meat Realm": {
+ "Meat Realm Chest": LocationData(110086, LocationFlag.main_world, "Chest"),
+ "Meat Realm Pedestal": LocationData(110106, LocationFlag.main_world, "Pedestal"),
+ "Limatoukka": LocationData(110647, LocationFlag.main_world, "Boss"),
+ },
+ "West Meat Realm": {
+ "Kolmisilmän sydän": LocationData(110671, LocationFlag.main_world, "Boss"),
+ },
+ "The Laboratory": {
+ "Kolmisilmä": LocationData(110646, LocationFlag.main_path, "Boss"),
+ },
+ "Friend Cave": {
+ "Toveri": LocationData(110654, LocationFlag.main_world, "Boss"),
+ },
+ "The Work (Hell)": {
+ "The Work (Hell) Orb": LocationData(110666, LocationFlag.main_world, "Orb"),
+ },
+}
+
+
+def make_location_range(location_name: str, base_id: int, amt: int) -> Dict[str, int]:
+ if amt == 1:
+ return {location_name: base_id}
+ return {f"{location_name} {i+1}": base_id + i for i in range(amt)}
+
+
+location_name_groups: Dict[str, Set[str]] = {"Shop": set(), "Orb": set(), "Boss": set(), "Chest": set(),
+ "Pedestal": set()}
+location_name_to_id: Dict[str, int] = {}
+
+
+for region_name, location_group in location_region_mapping.items():
+ location_name_groups[region_name] = set()
+ for locname, locinfo in location_group.items():
+ # Iterating the hidden chest and pedestal locations here to avoid clutter above
+ amount = 20 if locinfo.ltype in ["Chest", "Pedestal"] else 1
+ entries = make_location_range(locname, locinfo.id, amount)
+
+ location_name_to_id.update(entries)
+ location_name_groups[locinfo.ltype].update(entries.keys())
+ location_name_groups[region_name].update(entries.keys())
+
+shop_locations = {name for name in location_name_to_id.keys() if "Shop Item" in name}
diff --git a/worlds/noita/options.py b/worlds/noita/options.py
new file mode 100644
index 000000000000..0fdd62365a5a
--- /dev/null
+++ b/worlds/noita/options.py
@@ -0,0 +1,138 @@
+from Options import Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool, PerGameCommonOptions
+from dataclasses import dataclass
+
+
+class PathOption(Choice):
+ """
+ Choose where you would like Hidden Chest and Pedestal checks to be placed.
+ Main Path includes the main 7 biomes you typically go through to get to the final boss.
+ Side Path includes the Lukki Lair and Fungal Caverns. 9 biomes total.
+ Main World includes the full world (excluding parallel worlds). 15 biomes total.
+ Note: The Collapsed Mines have been combined into the Mines as the biome is tiny.
+ """
+ display_name = "Path Option"
+ option_main_path = 1
+ option_side_path = 2
+ option_main_world = 3
+ default = 1
+
+
+class HiddenChests(Range):
+ """
+ Number of hidden chest checks added to the applicable biomes.
+ """
+ display_name = "Hidden Chests per Biome"
+ range_start = 0
+ range_end = 20
+ default = 3
+
+
+class PedestalChecks(Range):
+ """
+ Number of checks that will spawn on pedestals in the applicable biomes.
+ """
+ display_name = "Pedestal Checks per Biome"
+ range_start = 0
+ range_end = 20
+ default = 6
+
+
+class Traps(DefaultOnToggle):
+ """
+ Whether negative effects on the Noita world are added to the item pool.
+ """
+ display_name = "Traps"
+
+
+class OrbsAsChecks(Choice):
+ """
+ Decides whether finding the orbs that naturally spawn in the world count as checks.
+ The Main Path option includes only the Floating Island and Abyss Orb Room orbs.
+ The Side Path option includes the Main Path, Magical Temple, Lukki Lair, and Lava Lake orbs.
+ The Main World option includes all 11 orbs.
+ """
+ display_name = "Orbs as Location Checks"
+ option_no_orbs = 0
+ option_main_path = 1
+ option_side_path = 2
+ option_main_world = 3
+ default = 0
+
+
+class BossesAsChecks(Choice):
+ """
+ Makes bosses count as location checks. The boss only needs to die, you do not need the kill credit.
+ The Main Path option includes Gate Guardian, Suomuhauki, and Kolmisilmä.
+ The Side Path option includes the Main Path bosses, Sauvojen Tuntija, and Ylialkemisti.
+ The All Bosses option includes all 15 bosses.
+ """
+ display_name = "Bosses as Location Checks"
+ option_no_bosses = 0
+ option_main_path = 1
+ option_side_path = 2
+ option_all_bosses = 3
+ default = 0
+
+
+# Note: the Sampo is an item that is picked up to trigger the boss fight at the normal ending location.
+# The sampo is required for every ending (having orbs and bringing the sampo to a different spot changes the ending).
+class VictoryCondition(Choice):
+ """
+ Greed is to get to the bottom, beat the boss, and win the game.
+ Pure is to get 11 orbs, grab the sampo, and bring it to the mountain altar.
+ Peaceful is to get all 33 orbs, grab the sampo, and bring it to the mountain altar.
+ Orbs will be added to the randomizer pool based on which victory condition you chose.
+ The base game orbs will not count towards these victory conditions.
+ """
+ display_name = "Victory Condition"
+ option_greed_ending = 0
+ option_pure_ending = 1
+ option_peaceful_ending = 2
+ default = 0
+
+
+class ExtraOrbs(Range):
+ """
+ Add extra orbs to your item pool, to prevent you from needing to wait as long for the last orb you need for your victory condition.
+ Extra orbs received past your victory condition's amount will be received as hearts instead.
+ Can be turned on for the Greed Ending goal, but will only really make it harder.
+ """
+ display_name = "Extra Orbs"
+ range_start = 0
+ range_end = 10
+ default = 0
+
+
+class ShopPrice(Choice):
+ """
+ Reduce the costs of Archipelago items in shops.
+ By default, the price of Archipelago items matches the price of wands at that shop.
+ """
+ display_name = "Shop Price Reduction"
+ option_full_price = 100
+ option_25_percent_off = 75
+ option_50_percent_off = 50
+ option_75_percent_off = 25
+ default = 100
+
+
+class NoitaDeathLink(DeathLink):
+ """
+ When you die, everyone dies. Of course, the reverse is true too.
+ You can disable this in the in-game mod options.
+ """
+
+
+@dataclass
+class NoitaOptions(PerGameCommonOptions):
+ start_inventory_from_pool: StartInventoryPool
+ death_link: NoitaDeathLink
+ bad_effects: Traps
+ victory_condition: VictoryCondition
+ path_option: PathOption
+ hidden_chests: HiddenChests
+ pedestal_checks: PedestalChecks
+ orbs_as_checks: OrbsAsChecks
+ bosses_as_checks: BossesAsChecks
+ extra_orbs: ExtraOrbs
+ shop_price: ShopPrice
diff --git a/worlds/noita/regions.py b/worlds/noita/regions.py
new file mode 100644
index 000000000000..184cd96018cf
--- /dev/null
+++ b/worlds/noita/regions.py
@@ -0,0 +1,120 @@
+# Regions are areas in your game that you travel to.
+from typing import Dict, List, TYPE_CHECKING
+
+from BaseClasses import Entrance, Region
+from . import locations
+from .events import create_all_events
+
+if TYPE_CHECKING:
+ from . import NoitaWorld
+
+
+def create_locations(world: "NoitaWorld", region: Region) -> None:
+ locs = locations.location_region_mapping.get(region.name, {})
+ for location_name, location_data in locs.items():
+ location_type = location_data.ltype
+ flag = location_data.flag
+
+ is_orb_allowed = location_type == "Orb" and flag <= world.options.orbs_as_checks
+ is_boss_allowed = location_type == "Boss" and flag <= world.options.bosses_as_checks
+ amount = 0
+ if flag == locations.LocationFlag.none or is_orb_allowed or is_boss_allowed:
+ amount = 1
+ elif location_type == "Chest" and flag <= world.options.path_option:
+ amount = world.options.hidden_chests.value
+ elif location_type == "Pedestal" and flag <= world.options.path_option:
+ amount = world.options.pedestal_checks.value
+
+ region.add_locations(locations.make_location_range(location_name, location_data.id, amount),
+ locations.NoitaLocation)
+
+
+# Creates a new Region with the locations found in `location_region_mapping` and adds them to the world.
+def create_region(world: "NoitaWorld", region_name: str) -> Region:
+ new_region = Region(region_name, world.player, world.multiworld)
+ create_locations(world, new_region)
+ return new_region
+
+
+def create_regions(world: "NoitaWorld") -> Dict[str, Region]:
+ return {name: create_region(world, name) for name in noita_regions}
+
+
+# An "Entrance" is really just a connection between two regions
+def create_entrance(player: int, source: str, destination: str, regions: Dict[str, Region]) -> Entrance:
+ entrance = Entrance(player, f"From {source} To {destination}", regions[source])
+ entrance.connect(regions[destination])
+ return entrance
+
+
+# Creates connections based on our access mapping in `noita_connections`.
+def create_connections(player: int, regions: Dict[str, Region]) -> None:
+ for source, destinations in noita_connections.items():
+ new_entrances = [create_entrance(player, source, destination, regions) for destination in destinations]
+ regions[source].exits = new_entrances
+
+
+# Creates all regions and connections. Called from NoitaWorld.
+def create_all_regions_and_connections(world: "NoitaWorld") -> None:
+ created_regions = create_regions(world)
+ create_connections(world.player, created_regions)
+ create_all_events(world, created_regions)
+
+ world.multiworld.regions += created_regions.values()
+
+
+# Oh, what a tangled web we weave
+# Notes to create artificial spheres:
+# - Shaft is excluded to disconnect Mines from the Snowy Depths
+# - Lukki Lair is disconnected from The Vault
+# - Overgrown Cavern is connected to the Underground Jungle instead of the Desert due to similar difficulty
+# - Powerplant is disconnected from the Sandcave due to difficulty and sphere creation
+# - Snow Chasm is disconnected from the Snowy Wasteland
+# - Pyramid is connected to the Hiisi Base instead of the Desert due to similar difficulty
+# - Frozen Vault is connected to the Vault instead of the Snowy Wasteland due to similar difficulty
+# - Lake is connected to The Laboratory, since the bosses are hard without specific set-ups (which means late game)
+# - Snowy Depths connects to Lava Lake orb since you need digging for it, so fairly early is acceptable
+# - Ancient Laboratory is connected to the Coal Pits, so that Ylialkemisti isn't sphere 1
+noita_connections: Dict[str, List[str]] = {
+ "Menu": ["Forest"],
+ "Forest": ["Mines", "Floating Island", "Desert", "Snowy Wasteland"],
+ "Frozen Vault": ["The Vault"],
+ "Overgrown Cavern": ["Sandcave"],
+
+ ###
+ "Mines": ["Collapsed Mines", "Coal Pits Holy Mountain", "Lava Lake"],
+ "Lava Lake": ["Abyss Orb Room"],
+
+ ###
+ "Coal Pits Holy Mountain": ["Coal Pits"],
+ "Coal Pits": ["Fungal Caverns", "Snowy Depths Holy Mountain", "Ancient Laboratory"],
+
+ ###
+ "Snowy Depths Holy Mountain": ["Snowy Depths"],
+ "Snowy Depths": ["Hiisi Base Holy Mountain", "Magical Temple", "Below Lava Lake"],
+
+ ###
+ "Hiisi Base Holy Mountain": ["Hiisi Base"],
+ "Hiisi Base": ["Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"],
+
+ ###
+ "Underground Jungle Holy Mountain": ["Underground Jungle"],
+ "Underground Jungle": ["Dragoncave", "Overgrown Cavern", "Vault Holy Mountain", "Lukki Lair", "Snow Chasm", "West Meat Realm"],
+
+ ###
+ "Vault Holy Mountain": ["The Vault"],
+ "The Vault": ["Frozen Vault", "Temple of the Art Holy Mountain"],
+
+ ###
+ "Temple of the Art Holy Mountain": ["Temple of the Art"],
+ "Temple of the Art": ["Laboratory Holy Mountain", "The Tower", "Wizards' Den"],
+ "Wizards' Den": ["Powerplant"],
+ "Powerplant": ["Meat Realm"],
+
+ ###
+ "Laboratory Holy Mountain": ["The Laboratory"],
+ "The Laboratory": ["The Work", "Friend Cave", "The Work (Hell)", "Lake", "The Sky"],
+ ###
+}
+
+noita_regions: List[str] = sorted(set(noita_connections.keys()).union(*noita_connections.values()))
diff --git a/worlds/noita/rules.py b/worlds/noita/rules.py
new file mode 100644
index 000000000000..65871a804ea0
--- /dev/null
+++ b/worlds/noita/rules.py
@@ -0,0 +1,172 @@
+from typing import List, NamedTuple, Set, TYPE_CHECKING
+
+from BaseClasses import CollectionState
+from . import items, locations
+from .options import BossesAsChecks, VictoryCondition
+from worlds.generic import Rules as GenericRules
+
+if TYPE_CHECKING:
+ from . import NoitaWorld
+
+
+class EntranceLock(NamedTuple):
+ source: str
+ destination: str
+ event: str
+ items_needed: int
+
+
+entrance_locks: List[EntranceLock] = [
+ EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1),
+ EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2),
+ EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3),
+ EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4),
+ EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5),
+ EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6),
+ EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7),
+]
+
+
+holy_mountain_regions: List[str] = [
+ "Coal Pits Holy Mountain",
+ "Snowy Depths Holy Mountain",
+ "Hiisi Base Holy Mountain",
+ "Underground Jungle Holy Mountain",
+ "Vault Holy Mountain",
+ "Temple of the Art Holy Mountain",
+ "Laboratory Holy Mountain",
+]
+
+
+wand_tiers: List[str] = [
+ "Wand (Tier 1)", # Coal Pits
+ "Wand (Tier 2)", # Snowy Depths
+ "Wand (Tier 3)", # Hiisi Base
+ "Wand (Tier 4)", # Underground Jungle
+ "Wand (Tier 5)", # The Vault
+ "Wand (Tier 6)", # Temple of the Art
+]
+
+
+items_hidden_from_shops: Set[str] = {"Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
+ "Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand",
+ "Powder Pouch"}
+
+perk_list: List[str] = list(filter(items.item_is_perk, items.item_table.keys()))
+
+
+# ----------------
+# Helper Functions
+# ----------------
+
+
+def has_perk_count(state: CollectionState, player: int, amount: int) -> bool:
+ return sum(state.count(perk, player) for perk in perk_list) >= amount
+
+
+def has_orb_count(state: CollectionState, player: int, amount: int) -> bool:
+ return state.count("Orb", player) >= amount
+
+
+def forbid_items_at_locations(world: "NoitaWorld", shop_locations: Set[str], forbidden_items: Set[str]) -> None:
+ for shop_location in shop_locations:
+ location = world.multiworld.get_location(shop_location, world.player)
+ GenericRules.forbid_items_for_player(location, forbidden_items, world.player)
+
+
+# ----------------
+# Rule Functions
+# ----------------
+
+
+# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them)
+# def ban_items_from_shops(world: "NoitaWorld") -> None:
+# for location_name in Locations.location_name_to_id.keys():
+# if "Shop Item" in location_name:
+# forbid_items_at_location(world, location_name, items_hidden_from_shops)
+def ban_items_from_shops(world: "NoitaWorld") -> None:
+ forbid_items_at_locations(world, locations.shop_locations, items_hidden_from_shops)
+
+
+# Prevent high tier wands from appearing in early Holy Mountain shops
+def ban_early_high_tier_wands(world: "NoitaWorld") -> None:
+ for i, region_name in enumerate(holy_mountain_regions):
+ wands_to_forbid = set(wand_tiers[i+1:])
+
+ locations_in_region = set(locations.location_region_mapping[region_name].keys())
+ forbid_items_at_locations(world, locations_in_region, wands_to_forbid)
+
+ # Prevent high tier wands from appearing in the Secret shop
+ wands_to_forbid = set(wand_tiers[3:])
+ locations_in_region = set(locations.location_region_mapping["Secret Shop"].keys())
+ forbid_items_at_locations(world, locations_in_region, wands_to_forbid)
+
+
+def lock_holy_mountains_into_spheres(world: "NoitaWorld") -> None:
+ for lock in entrance_locks:
+ location = world.multiworld.get_entrance(f"From {lock.source} To {lock.destination}", world.player)
+ GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, world.player))
+
+
+def holy_mountain_unlock_conditions(world: "NoitaWorld") -> None:
+ victory_condition = world.options.victory_condition.value
+ for lock in entrance_locks:
+ location = world.multiworld.get_location(lock.event, world.player)
+
+ if victory_condition == VictoryCondition.option_greed_ending:
+ location.access_rule = lambda state, items_needed=lock.items_needed: (
+ has_perk_count(state, world.player, items_needed//2)
+ )
+ elif victory_condition == VictoryCondition.option_pure_ending:
+ location.access_rule = lambda state, items_needed=lock.items_needed: (
+ has_perk_count(state, world.player, items_needed//2) and
+ has_orb_count(state, world.player, items_needed)
+ )
+ elif victory_condition == VictoryCondition.option_peaceful_ending:
+ location.access_rule = lambda state, items_needed=lock.items_needed: (
+ has_perk_count(state, world.player, items_needed//2) and
+ has_orb_count(state, world.player, items_needed * 3)
+ )
+
+
+def biome_unlock_conditions(world: "NoitaWorld") -> None:
+ lukki_entrances = world.multiworld.get_region("Lukki Lair", world.player).entrances
+ magical_entrances = world.multiworld.get_region("Magical Temple", world.player).entrances
+ wizard_entrances = world.multiworld.get_region("Wizards' Den", world.player).entrances
+ for entrance in lukki_entrances:
+ entrance.access_rule = lambda state: state.has("Melee Immunity Perk", world.player) and\
+ state.has("All-Seeing Eye Perk", world.player)
+ for entrance in magical_entrances:
+ entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player)
+ for entrance in wizard_entrances:
+ entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player)
+
+
+def victory_unlock_conditions(world: "NoitaWorld") -> None:
+ victory_condition = world.options.victory_condition.value
+ victory_location = world.multiworld.get_location("Victory", world.player)
+
+ if victory_condition == VictoryCondition.option_pure_ending:
+ victory_location.access_rule = lambda state: has_orb_count(state, world.player, 11)
+ elif victory_condition == VictoryCondition.option_peaceful_ending:
+ victory_location.access_rule = lambda state: has_orb_count(state, world.player, 33)
+
+
+# ----------------
+# Main Function
+# ----------------
+
+
+def create_all_rules(world: "NoitaWorld") -> None:
+ if world.multiworld.players > 1:
+ ban_items_from_shops(world)
+ ban_early_high_tier_wands(world)
+ lock_holy_mountains_into_spheres(world)
+ holy_mountain_unlock_conditions(world)
+ biome_unlock_conditions(world)
+ victory_unlock_conditions(world)
+
+ # Prevent the Map perk (used to find Toveri) from being on Toveri (boss)
+ if world.options.bosses_as_checks.value >= BossesAsChecks.option_all_bosses:
+ toveri = world.multiworld.get_location("Toveri", world.player)
+ GenericRules.forbid_items_for_player(toveri, {"Spatial Awareness Perk"}, world.player)
diff --git a/worlds/oot/Entrance.py b/worlds/oot/Entrance.py
index e480c957a672..6c4b6428f53e 100644
--- a/worlds/oot/Entrance.py
+++ b/worlds/oot/Entrance.py
@@ -1,6 +1,4 @@
-
from BaseClasses import Entrance
-from .Regions import TimeOfDay
class OOTEntrance(Entrance):
game: str = 'Ocarina of Time'
@@ -29,16 +27,16 @@ def disconnect(self):
self.connected_region = None
return previously_connected
- def get_new_target(self):
+ def get_new_target(self, pool_type):
root = self.multiworld.get_region('Root Exits', self.player)
- target_entrance = OOTEntrance(self.player, self.multiworld, 'Root -> ' + self.connected_region.name, root)
+ target_entrance = OOTEntrance(self.player, self.multiworld, f'Root -> ({self.name}) ({pool_type})', root)
target_entrance.connect(self.connected_region)
target_entrance.replaces = self
root.exits.append(target_entrance)
return target_entrance
- def assume_reachable(self):
+ def assume_reachable(self, pool_type):
if self.assumed == None:
- self.assumed = self.get_new_target()
+ self.assumed = self.get_new_target(pool_type)
self.disconnect()
return self.assumed
diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py
index 3c1b2d78c6c9..bbdc30490c18 100644
--- a/worlds/oot/EntranceShuffle.py
+++ b/worlds/oot/EntranceShuffle.py
@@ -2,6 +2,7 @@
import logging
from worlds.generic.Rules import set_rule, add_rule
+from BaseClasses import CollectionState
from .Hints import get_hint_area, HintAreaNotFound
from .Regions import TimeOfDay
@@ -25,12 +26,12 @@ def set_all_entrances_data(world, player):
return_entrance.data['index'] = 0x7FFF
-def assume_entrance_pool(entrance_pool, ootworld):
+def assume_entrance_pool(entrance_pool, ootworld, pool_type):
assumed_pool = []
for entrance in entrance_pool:
- assumed_forward = entrance.assume_reachable()
+ assumed_forward = entrance.assume_reachable(pool_type)
if entrance.reverse != None and not ootworld.decouple_entrances:
- assumed_return = entrance.reverse.assume_reachable()
+ assumed_return = entrance.reverse.assume_reachable(pool_type)
if not (ootworld.mix_entrance_pools != 'off' and (ootworld.shuffle_overworld_entrances or ootworld.shuffle_special_interior_entrances)):
if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \
(entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances):
@@ -41,15 +42,15 @@ def assume_entrance_pool(entrance_pool, ootworld):
return assumed_pool
-def build_one_way_targets(world, types_to_include, exclude=(), target_region_names=()):
+def build_one_way_targets(world, pool, types_to_include, exclude=(), target_region_names=()):
one_way_entrances = []
for pool_type in types_to_include:
one_way_entrances += world.get_shufflable_entrances(type=pool_type)
valid_one_way_entrances = list(filter(lambda entrance: entrance.name not in exclude, one_way_entrances))
if target_region_names:
- return [entrance.get_new_target() for entrance in valid_one_way_entrances
+ return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances
if entrance.connected_region.name in target_region_names]
- return [entrance.get_new_target() for entrance in valid_one_way_entrances]
+ return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances]
# Abbreviations
@@ -423,14 +424,14 @@ def _add_boss_entrances():
}
interior_entrance_bias = {
- 'Kakariko Village -> Kak Potion Shop Front': 4,
- 'Kak Backyard -> Kak Potion Shop Back': 4,
- 'Kakariko Village -> Kak Impas House': 3,
- 'Kak Impas Ledge -> Kak Impas House Back': 3,
- 'Goron City -> GC Shop': 2,
- 'Zoras Domain -> ZD Shop': 2,
+ 'ToT Entrance -> Temple of Time': 4,
+ 'Kakariko Village -> Kak Potion Shop Front': 3,
+ 'Kak Backyard -> Kak Potion Shop Back': 3,
+ 'Kakariko Village -> Kak Impas House': 2,
+ 'Kak Impas Ledge -> Kak Impas House Back': 2,
'Market Entrance -> Market Guard House': 2,
- 'ToT Entrance -> Temple of Time': 1,
+ 'Goron City -> GC Shop': 1,
+ 'Zoras Domain -> ZD Shop': 1,
}
@@ -443,7 +444,8 @@ def shuffle_random_entrances(ootworld):
player = ootworld.player
# Gather locations to keep reachable for validation
- all_state = world.get_all_state(use_cache=True)
+ all_state = ootworld.get_state_with_complete_itempool()
+ all_state.sweep_for_events(locations=ootworld.get_locations())
locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}
# Set entrance data for all entrances
@@ -523,12 +525,12 @@ def shuffle_random_entrances(ootworld):
for pool_type, entrance_pool in one_way_entrance_pools.items():
if pool_type == 'OwlDrop':
valid_target_types = ('WarpSong', 'OwlDrop', 'Overworld', 'Extra')
- one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time'])
+ one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time'])
for target in one_way_target_entrance_pools[pool_type]:
set_rule(target, lambda state: state._oot_reach_as_age(target.parent_region, 'child', player))
elif pool_type in {'Spawn', 'WarpSong'}:
valid_target_types = ('Spawn', 'WarpSong', 'OwlDrop', 'Overworld', 'Interior', 'SpecialInterior', 'Extra')
- one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types)
+ one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types)
# Ensure that the last entrance doesn't assume the rest of the targets are reachable
for target in one_way_target_entrance_pools[pool_type]:
add_rule(target, (lambda entrances=entrance_pool: (lambda state: any(entrance.connected_region == None for entrance in entrances)))())
@@ -538,14 +540,11 @@ def shuffle_random_entrances(ootworld):
target_entrance_pools = {}
for pool_type, entrance_pool in entrance_pools.items():
- target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld)
+ target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld, pool_type)
# Build all_state and none_state
all_state = ootworld.get_state_with_complete_itempool()
- none_state = all_state.copy()
- for item_tuple in none_state.prog_items:
- if item_tuple[1] == player:
- none_state.prog_items[item_tuple] = 0
+ none_state = CollectionState(ootworld.multiworld)
# Plando entrances
if world.plando_connections[player]:
@@ -628,7 +627,7 @@ def shuffle_random_entrances(ootworld):
logging.getLogger('').error(f'Root Exit: {exit} -> {exit.connected_region}')
logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances')
# Game is beatable
- new_all_state = world.get_all_state(use_cache=False)
+ new_all_state = ootworld.get_state_with_complete_itempool()
if not world.has_beaten_game(new_all_state, player):
raise EntranceShuffleError('Cannot beat game')
# Validate world
@@ -700,7 +699,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al
raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}')
-def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20):
+def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=10):
restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances)
@@ -745,7 +744,6 @@ def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollback
def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entrances):
- world = ootworld.multiworld
player = ootworld.player
# Disconnect all root assumed entrances and save original connections
@@ -755,7 +753,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran
if entrance.connected_region:
original_connected_regions[entrance] = entrance.disconnect()
- all_state = world.get_all_state(use_cache=False)
+ all_state = ootworld.get_state_with_complete_itempool()
restrictive_entrances = []
soft_entrances = []
@@ -793,8 +791,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
all_state = all_state_orig.copy()
none_state = none_state_orig.copy()
- all_state.sweep_for_events()
- none_state.sweep_for_events()
+ all_state.sweep_for_events(locations=ootworld.get_locations())
+ none_state.sweep_for_events(locations=ootworld.get_locations())
if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions:
time_travel_state = none_state.copy()
diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py
index 94e1011ddc63..6ca6bc9268a9 100644
--- a/worlds/oot/ItemPool.py
+++ b/worlds/oot/ItemPool.py
@@ -350,7 +350,7 @@ def generate_itempool(ootworld):
ootworld.itempool = [ootworld.create_item(item) for item in pool]
for (location_name, item) in placed_items.items():
location = world.get_location(location_name, player)
- location.place_locked_item(ootworld.create_item(item))
+ location.place_locked_item(ootworld.create_item(item, allow_arbitrary_name=True))
def get_pool_core(world):
@@ -675,7 +675,7 @@ def get_pool_core(world):
world.remove_from_start_inventory.append('Scarecrow Song')
if world.no_epona_race:
- world.multiworld.push_precollected(world.create_item('Epona'))
+ world.multiworld.push_precollected(world.create_item('Epona', allow_arbitrary_name=True))
world.remove_from_start_inventory.append('Epona')
if world.shuffle_smallkeys == 'vanilla':
diff --git a/worlds/oot/Location.py b/worlds/oot/Location.py
index e2b0e52e4dc5..f924dd048da1 100644
--- a/worlds/oot/Location.py
+++ b/worlds/oot/Location.py
@@ -2,6 +2,8 @@
from .LocationList import location_table
from BaseClasses import Location
+non_indexed_location_types = {'Boss', 'Event', 'Drop', 'HintStone', 'Hint'}
+
location_id_offset = 67000
locnames_pre_70 = {
"Gift from Sages",
@@ -18,7 +20,7 @@
else 0)
location_name_to_id = {name: (location_id_offset + index) for (index, name) in enumerate(new_name_order)
- if location_table[name][0] not in {'Boss', 'Event', 'Drop', 'HintStone', 'Hint'}}
+ if location_table[name][0] not in non_indexed_location_types}
class DisableType(Enum):
ENABLED = 0
@@ -42,14 +44,11 @@ def __init__(self, player, name='', code=None, address1=None, address2=None,
self.vanilla_item = vanilla_item
if filter_tags is None:
self.filter_tags = None
- else:
+ else:
self.filter_tags = list(filter_tags)
self.never = False # no idea what this does
self.disabled = DisableType.ENABLED
- if type == 'Event':
- self.event = True
-
@property
def dungeon(self):
return self.parent_region.dungeon
@@ -83,3 +82,57 @@ def LocationFactory(locations, player: int):
return ret
+def build_location_name_groups() -> dict:
+
+ def fix_sing(t) -> tuple:
+ if isinstance(t, str):
+ return (t,)
+ return t
+
+ def rename(d, k1, k2) -> None:
+ d[k2] = d[k1]
+ del d[k1]
+
+ # whoever wrote the location table didn't realize they need to add a comma to mark a singleton as a tuple
+ # so we have to check types unfortunately
+ tags = set()
+ for v in location_table.values():
+ if v[5] is not None:
+ tags.update(fix_sing(v[5]))
+
+ sorted_tags = sorted(list(tags))
+
+ ret = {
+ tag: {k for k, v in location_table.items()
+ if v[5] is not None
+ and tag in fix_sing(v[5])
+ and v[0] not in non_indexed_location_types}
+ for tag in sorted_tags
+ }
+
+ # Delete tags which are a combination of other tags
+ del ret['Death Mountain']
+ del ret['Forest']
+ del ret['Gerudo']
+ del ret['Kakariko']
+ del ret['Market']
+
+ # Delete Vanilla and MQ tags because they are just way too broad
+ del ret['Vanilla']
+ del ret['Master Quest']
+
+ rename(ret, 'Beehive', 'Beehives')
+ rename(ret, 'Cow', 'Cows')
+ rename(ret, 'Crate', 'Crates')
+ rename(ret, 'Deku Scrub', 'Deku Scrubs')
+ rename(ret, 'FlyingPot', 'Flying Pots')
+ rename(ret, 'Freestanding', 'Freestanding Items')
+ rename(ret, 'Pot', 'Pots')
+ rename(ret, 'RupeeTower', 'Rupee Groups')
+ rename(ret, 'SmallCrate', 'Small Crates')
+ rename(ret, 'the Market', 'Market')
+ rename(ret, 'the Graveyard', 'Graveyard')
+ rename(ret, 'the Lost Woods', 'Lost Woods')
+
+ return ret
+
diff --git a/worlds/oot/LocationList.py b/worlds/oot/LocationList.py
index 3f4602c428c1..27ad575699f5 100644
--- a/worlds/oot/LocationList.py
+++ b/worlds/oot/LocationList.py
@@ -238,7 +238,7 @@ def shop_address(shop_id, shelf_id):
("Market Night Green Rupee Crate 1", ("Crate", 0x21, (0,0,24), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
("Market Night Green Rupee Crate 2", ("Crate", 0x21, (0,0,25), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
("Market Night Green Rupee Crate 3", ("Crate", 0x21, (0,0,26), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
- ("Market Dog Lady House Crate", ("Crate", 0x35, (0,0,3), None, 'Rupees (5)', ("Market", "Market", "Crate"))),
+ ("Market Dog Lady House Crate", ("Crate", 0x35, (0,0,3), None, 'Rupees (5)', ("the Market", "Market", "Crate"))),
("Market Guard House Child Crate", ("Crate", 0x4D, (0,0,6), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
("Market Guard House Child Pot 1", ("Pot", 0x4D, (0,0,9), None, 'Rupee (1)', ("the Market", "Market", "Pot"))),
("Market Guard House Child Pot 2", ("Pot", 0x4D, (0,0,10), None, 'Rupee (1)', ("the Market", "Market", "Pot"))),
diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py
index 03f5346ceeed..daf072adb59c 100644
--- a/worlds/oot/Options.py
+++ b/worlds/oot/Options.py
@@ -1,6 +1,7 @@
import typing
import random
-from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink
+from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections
+from .EntranceShuffle import entrance_shuffle_table
from .LogicTricks import normalized_name_tricks
from .ColorSFXOptions import *
@@ -29,8 +30,23 @@ def from_any(cls, data: typing.Any) -> Range:
raise RuntimeError(f"All options specified in \"{cls.display_name}\" are weighted as zero.")
+class OoTPlandoConnections(PlandoConnections):
+ entrances = set([connection[1][0] for connection in entrance_shuffle_table])
+ exits = set([connection[2][0] for connection in entrance_shuffle_table if len(connection) > 2])
+
+
class Logic(Choice):
- """Set the logic used for the generator."""
+ """Set the logic used for the generator.
+ Glitchless: Normal gameplay. Can enable more difficult logical paths using the Logic Tricks option.
+ Glitched: Many powerful glitches expected, such as bomb hovering and clipping.
+ Glitched is incompatible with the following settings:
+ - All forms of entrance randomizer
+ - MQ dungeons
+ - Pot shuffle
+ - Freestanding item shuffle
+ - Crate shuffle
+ - Beehive shuffle
+ No Logic: No logic is used when placing items. Not recommended for most players."""
display_name = "Logic Rules"
option_glitchless = 0
option_glitched = 1
@@ -38,12 +54,16 @@ class Logic(Choice):
class NightTokens(Toggle):
- """Nighttime skulltulas will logically require Sun's Song."""
+ """When enabled, nighttime skulltulas logically require Sun's Song."""
display_name = "Nighttime Skulltulas Expect Sun's Song"
class Forest(Choice):
- """Set the state of Kokiri Forest and the path to Deku Tree."""
+ """Set the state of Kokiri Forest and the path to Deku Tree.
+ Open: Neither the forest exit nor the path to Deku Tree is blocked.
+ Closed Deku: The forest exit is not blocked; the path to Deku Tree requires Kokiri Sword and Deku Shield.
+ Closed: Path to Deku Tree requires sword and shield. The forest exit is blocked until Deku Tree is beaten.
+ Closed forest will force child start, and becomes Closed Deku if interior entrances, overworld entrances, warp songs, or random spawn positions are enabled."""
display_name = "Forest"
option_open = 0
option_closed_deku = 1
@@ -53,7 +73,10 @@ class Forest(Choice):
class Gate(Choice):
- """Set the state of the Kakariko Village gate."""
+ """Set the state of the Kakariko Village gate for child. The gate is always open as adult.
+ Open: The gate starts open. Happy Mask Shop opens upon receiving Zelda's Letter.
+ Zelda: The gate and Mask Shop open upon receiving Zelda's Letter, without needing to show it to the guard.
+ Closed: Vanilla behavior; the gate and Mask Shop open upon showing Zelda's Letter to the gate guard."""
display_name = "Kakariko Gate"
option_open = 0
option_zelda = 1
@@ -61,12 +84,15 @@ class Gate(Choice):
class DoorOfTime(DefaultOnToggle):
- """Open the Door of Time by default, without the Song of Time."""
+ """When enabled, the Door of Time starts opened, without needing Song of Time."""
display_name = "Open Door of Time"
class Fountain(Choice):
- """Set the state of King Zora, blocking the way to Zora's Fountain."""
+ """Set the state of King Zora, blocking the way to Zora's Fountain.
+ Open: King Zora starts moved as both ages. Ruto's Letter is removed.
+ Adult: King Zora must be moved as child, but is always moved for adult.
+ Closed: Vanilla behavior; King Zora must be shown Ruto's Letter as child to move him as both ages."""
display_name = "Zora's Fountain"
option_open = 0
option_adult = 1
@@ -75,7 +101,10 @@ class Fountain(Choice):
class Fortress(Choice):
- """Set the requirements for access to Gerudo Fortress."""
+ """Set the requirements for access to Gerudo Fortress.
+ Normal: Vanilla behavior; all four carpenters must be rescued.
+ Fast: Only one carpenter must be rescued, which is the one in the bottom-left of the fortress.
+ Open: The Gerudo Valley bridge starts repaired. Gerudo Membership Card is given to start if not shuffled."""
display_name = "Gerudo Fortress"
option_normal = 0
option_fast = 1
@@ -84,7 +113,14 @@ class Fortress(Choice):
class Bridge(Choice):
- """Set the requirements for the Rainbow Bridge."""
+ """Set the requirements for the Rainbow Bridge.
+ Open: The bridge is always present.
+ Vanilla: Bridge requires Shadow Medallion, Spirit Medallion, and Light Arrows.
+ Stones: Bridge requires a configurable amount of Spiritual Stones.
+ Medallions: Bridge requires a configurable amount of medallions.
+ Dungeons: Bridge requires a configurable amount of rewards (stones + medallions).
+ Tokens: Bridge requires a configurable amount of gold skulltula tokens.
+ Hearts: Bridge requires a configurable amount of hearts."""
display_name = "Rainbow Bridge Requirement"
option_open = 0
option_vanilla = 1
@@ -122,8 +158,9 @@ class StartingAge(Choice):
class InteriorEntrances(Choice):
- """Shuffles interior entrances. "Simple" shuffles houses and Great Fairies; "All" includes Windmill, Link's House,
- Temple of Time, and Kak potion shop."""
+ """Shuffles interior entrances.
+ Simple: Houses and Great Fairies are shuffled.
+ All: In addition to Simple, includes Windmill, Link's House, Temple of Time, and the Kakariko potion shop."""
display_name = "Shuffle Interior Entrances"
option_off = 0
option_simple = 1
@@ -137,7 +174,9 @@ class GrottoEntrances(Toggle):
class DungeonEntrances(Choice):
- """Shuffles dungeon entrances. Opens Deku, Fire and BotW to both ages. "All" includes Ganon's Castle."""
+ """Shuffles dungeon entrances. When enabled, both ages will have access to Fire Temple, Bottom of the Well, and Deku Tree.
+ Simple: Shuffle dungeon entrances except for Ganon's Castle.
+ All: Include Ganon's Castle as well."""
display_name = "Shuffle Dungeon Entrances"
option_off = 0
option_simple = 1
@@ -146,7 +185,9 @@ class DungeonEntrances(Choice):
class BossEntrances(Choice):
- """Shuffles boss entrances. "Limited" prevents age-mixing of bosses."""
+ """Shuffles boss entrances.
+ Limited: Bosses will be limited to the ages that typically fight them.
+ Full: Bosses may be fought as different ages than usual. Child can defeat Phantom Ganon and Bongo Bongo."""
display_name = "Shuffle Boss Entrances"
option_off = 0
option_limited = 1
@@ -178,19 +219,19 @@ class SpawnPositions(Choice):
alias_true = 3
-class MixEntrancePools(Choice):
- """Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all"
- mixes them in."""
- display_name = "Mix Entrance Pools"
- option_off = 0
- option_indoor = 1
- option_all = 2
+# class MixEntrancePools(Choice):
+# """Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all"
+# mixes them in."""
+# display_name = "Mix Entrance Pools"
+# option_off = 0
+# option_indoor = 1
+# option_all = 2
-class DecoupleEntrances(Toggle):
- """Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if
- overworld is shuffled."""
- display_name = "Decouple Entrances"
+# class DecoupleEntrances(Toggle):
+# """Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if
+# overworld is shuffled."""
+# display_name = "Decouple Entrances"
class TriforceHunt(Toggle):
@@ -216,13 +257,17 @@ class ExtraTriforces(Range):
class LogicalChus(Toggle):
- """Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell
- refills; bombchus open Bombchu Bowling."""
+ """Bombchus are properly considered in logic.
+ The first found pack will always have 20 chus.
+ Kokiri Shop and Bazaar will sell refills at reduced cost.
+ Bombchus open Bombchu Bowling."""
display_name = "Bombchus Considered in Logic"
class DungeonShortcuts(Choice):
- """Shortcuts to dungeon bosses are available without any requirements."""
+ """Shortcuts to dungeon bosses are available without any requirements.
+ If enabled, this will impact the logic of dungeons where shortcuts are available.
+ Choice: Use the option "dungeon_shortcuts_list" to choose shortcuts."""
display_name = "Dungeon Boss Shortcuts Mode"
option_off = 0
option_choice = 1
@@ -246,7 +291,11 @@ class DungeonShortcutsList(OptionSet):
class MQDungeons(Choice):
- """Choose between vanilla and Master Quest dungeon layouts."""
+ """Choose between vanilla and Master Quest dungeon layouts.
+ Vanilla: All layouts are vanilla.
+ MQ: All layouts are Master Quest.
+ Specific: Use the option "mq_dungeons_list" to choose which dungeons are MQ.
+ Count: Use the option "mq_dungeons_count" to choose a number of random dungeons as MQ."""
display_name = "MQ Dungeon Mode"
option_vanilla = 0
option_mq = 1
@@ -255,7 +304,7 @@ class MQDungeons(Choice):
class MQDungeonList(OptionSet):
- """Chosen dungeons to be MQ layout."""
+ """With MQ dungeons as Specific: chosen dungeons to be MQ layout."""
display_name = "MQ Dungeon List"
valid_keys = {
"Deku Tree",
@@ -274,41 +323,41 @@ class MQDungeonList(OptionSet):
class MQDungeonCount(TrackRandomRange):
- """Number of MQ dungeons, chosen randomly."""
+ """With MQ dungeons as Count: number of randomly-selected dungeons to be MQ layout."""
display_name = "MQ Dungeon Count"
range_start = 0
range_end = 12
default = 0
-class EmptyDungeons(Choice):
- """Pre-completed dungeons are barren and rewards are given for free."""
- display_name = "Pre-completed Dungeons Mode"
- option_none = 0
- option_specific = 1
- option_count = 2
+# class EmptyDungeons(Choice):
+# """Pre-completed dungeons are barren and rewards are given for free."""
+# display_name = "Pre-completed Dungeons Mode"
+# option_none = 0
+# option_specific = 1
+# option_count = 2
-class EmptyDungeonList(OptionSet):
- """Chosen dungeons to be pre-completed."""
- display_name = "Pre-completed Dungeon List"
- valid_keys = {
- "Deku Tree",
- "Dodongo's Cavern",
- "Jabu Jabu's Belly",
- "Forest Temple",
- "Fire Temple",
- "Water Temple",
- "Shadow Temple",
- "Spirit Temple",
- }
+# class EmptyDungeonList(OptionSet):
+# """Chosen dungeons to be pre-completed."""
+# display_name = "Pre-completed Dungeon List"
+# valid_keys = {
+# "Deku Tree",
+# "Dodongo's Cavern",
+# "Jabu Jabu's Belly",
+# "Forest Temple",
+# "Fire Temple",
+# "Water Temple",
+# "Shadow Temple",
+# "Spirit Temple",
+# }
-class EmptyDungeonCount(Range):
- display_name = "Pre-completed Dungeon Count"
- range_start = 1
- range_end = 8
- default = 2
+# class EmptyDungeonCount(Range):
+# display_name = "Pre-completed Dungeon Count"
+# range_start = 1
+# range_end = 8
+# default = 2
world_options: typing.Dict[str, type(Option)] = {
@@ -341,59 +390,8 @@ class EmptyDungeonCount(Range):
}
-# class LacsCondition(Choice):
-# """Set the requirements for the Light Arrow Cutscene in the Temple of Time."""
-# display_name = "Light Arrow Cutscene Requirement"
-# option_vanilla = 0
-# option_stones = 1
-# option_medallions = 2
-# option_dungeons = 3
-# option_tokens = 4
-
-
-# class LacsStones(Range):
-# """Set the number of Spiritual Stones required for LACS."""
-# display_name = "Spiritual Stones Required for LACS"
-# range_start = 0
-# range_end = 3
-# default = 3
-
-
-# class LacsMedallions(Range):
-# """Set the number of medallions required for LACS."""
-# display_name = "Medallions Required for LACS"
-# range_start = 0
-# range_end = 6
-# default = 6
-
-
-# class LacsRewards(Range):
-# """Set the number of dungeon rewards required for LACS."""
-# display_name = "Dungeon Rewards Required for LACS"
-# range_start = 0
-# range_end = 9
-# default = 9
-
-
-# class LacsTokens(Range):
-# """Set the number of Gold Skulltula Tokens required for LACS."""
-# display_name = "Tokens Required for LACS"
-# range_start = 0
-# range_end = 100
-# default = 40
-
-
-# lacs_options: typing.Dict[str, type(Option)] = {
-# "lacs_condition": LacsCondition,
-# "lacs_stones": LacsStones,
-# "lacs_medallions": LacsMedallions,
-# "lacs_rewards": LacsRewards,
-# "lacs_tokens": LacsTokens,
-# }
-
-
class BridgeStones(Range):
- """Set the number of Spiritual Stones required for the rainbow bridge."""
+ """With Stones bridge: set the number of Spiritual Stones required."""
display_name = "Spiritual Stones Required for Bridge"
range_start = 0
range_end = 3
@@ -401,7 +399,7 @@ class BridgeStones(Range):
class BridgeMedallions(Range):
- """Set the number of medallions required for the rainbow bridge."""
+ """With Medallions bridge: set the number of medallions required."""
display_name = "Medallions Required for Bridge"
range_start = 0
range_end = 6
@@ -409,7 +407,7 @@ class BridgeMedallions(Range):
class BridgeRewards(Range):
- """Set the number of dungeon rewards required for the rainbow bridge."""
+ """With Dungeons bridge: set the number of dungeon rewards required."""
display_name = "Dungeon Rewards Required for Bridge"
range_start = 0
range_end = 9
@@ -417,7 +415,7 @@ class BridgeRewards(Range):
class BridgeTokens(Range):
- """Set the number of Gold Skulltula Tokens required for the rainbow bridge."""
+ """With Tokens bridge: set the number of Gold Skulltula Tokens required."""
display_name = "Tokens Required for Bridge"
range_start = 0
range_end = 100
@@ -425,7 +423,7 @@ class BridgeTokens(Range):
class BridgeHearts(Range):
- """Set the number of hearts required for the rainbow bridge."""
+ """With Hearts bridge: set the number of hearts required."""
display_name = "Hearts Required for Bridge"
range_start = 4
range_end = 20
@@ -442,7 +440,15 @@ class BridgeHearts(Range):
class SongShuffle(Choice):
- """Set where songs can appear."""
+ """Set where songs can appear.
+ Song: Songs are shuffled into other song locations.
+ Dungeon: Songs are placed into end-of-dungeon locations:
+ - The 8 boss heart containers
+ - Sheik in Ice Cavern
+ - Lens of Truth chest in Bottom of the Well
+ - Ice Arrows chest in Gerudo Training Ground
+ - Impa at Hyrule Castle
+ Any: Songs can appear anywhere in the multiworld."""
display_name = "Shuffle Songs"
option_song = 0
option_dungeon = 1
@@ -450,8 +456,10 @@ class SongShuffle(Choice):
class ShopShuffle(Choice):
- """Randomizes shop contents. "fixed_number" randomizes a specific number of items per shop;
- "random_number" randomizes the value for each shop. """
+ """Randomizes shop contents.
+ Off: Shops are not randomized at all.
+ Fixed Number: Shop contents are shuffled, and a specific number of multiworld locations exist in each shop, controlled by the "shop_slots" option.
+ Random Number: Same as Fixed Number, but the number of locations per shop is random and may differ between shops."""
display_name = "Shopsanity"
option_off = 0
option_fixed_number = 1
@@ -459,15 +467,20 @@ class ShopShuffle(Choice):
class ShopSlots(Range):
- """Number of items per shop to be randomized into the main itempool.
- Only active if Shopsanity is set to "fixed_number." """
+ """With Shopsanity fixed number: quantity of multiworld locations per shop to be randomized."""
display_name = "Shuffled Shop Slots"
range_start = 0
range_end = 4
class ShopPrices(Choice):
- """Controls prices of shop items. "Normal" is a distribution from 0 to 300. "X Wallet" requires that wallet at max. "Affordable" is always 10 rupees."""
+ """Controls prices of shop locations.
+ Normal: Balanced distribution from 0 to 300.
+ Affordable: Every shop location costs 10 rupees.
+ Starting Wallet: Prices capped at 99 rupees.
+ Adult's Wallet: Prices capped at 200 rupees.
+ Giant's Wallet: Prices capped at 500 rupees.
+ Tycoon's Wallet: Prices capped at 999 rupees."""
display_name = "Shopsanity Prices"
option_normal = 0
option_affordable = 1
@@ -478,7 +491,10 @@ class ShopPrices(Choice):
class TokenShuffle(Choice):
- """Token rewards from Gold Skulltulas are shuffled into the pool."""
+ """Token rewards from Gold Skulltulas can be shuffled into the pool.
+ Dungeons: Only skulltulas in dungeons are shuffled.
+ Overworld: Only skulltulas on the overworld (all skulltulas not in dungeons) are shuffled.
+ All: Every skulltula is shuffled."""
display_name = "Tokensanity"
option_off = 0
option_dungeons = 1
@@ -487,7 +503,11 @@ class TokenShuffle(Choice):
class ScrubShuffle(Choice):
- """Shuffle the items sold by Business Scrubs, and set the prices."""
+ """Shuffle the items sold by Business Scrubs, and set the prices.
+ Off: Only the three business scrubs that sell one-time upgrades in vanilla will have items at their vanilla prices.
+ Low/"Affordable": All scrub prices are 10 rupees.
+ Regular/"Expensive": All scrub prices are vanilla.
+ Random Prices: All scrub prices are randomized between 0 and 99 rupees."""
display_name = "Scrub Shuffle"
option_off = 0
option_low = 1
@@ -513,7 +533,11 @@ class ShuffleOcarinas(Toggle):
class ShuffleChildTrade(Choice):
- """Controls the behavior of the start of the child trade quest."""
+ """Controls the behavior of the start of the child trade quest.
+ Vanilla: Malon will give you the Weird Egg at Hyrule Castle.
+ Shuffle: Malon will give you a random item, and the Weird Egg is shuffled.
+ Skip Child Zelda: The game starts with Zelda already met, Zelda's Letter obtained, and the item from Impa obtained.
+ """
display_name = "Shuffle Child Trade Item"
option_vanilla = 0
option_shuffle = 1
@@ -538,30 +562,39 @@ class ShuffleMedigoronCarpet(Toggle):
class ShuffleFreestanding(Choice):
- """Shuffles freestanding rupees, recovery hearts, Shadow Temple Spinning Pots, and Goron Pot."""
+ """Shuffles freestanding rupees, recovery hearts, Shadow Temple Spinning Pots, and Goron Pot drops.
+ Dungeons: Only freestanding items in dungeons are shuffled.
+ Overworld: Only freestanding items in the overworld are shuffled.
+ All: All freestanding items are shuffled."""
display_name = "Shuffle Rupees & Hearts"
option_off = 0
- option_all = 1
+ option_dungeons = 1
option_overworld = 2
- option_dungeons = 3
+ option_all = 3
class ShufflePots(Choice):
- """Shuffles pots and flying pots which normally contain an item."""
+ """Shuffles pots and flying pots which normally contain an item.
+ Dungeons: Only pots in dungeons are shuffled.
+ Overworld: Only pots in the overworld are shuffled.
+ All: All pots are shuffled."""
display_name = "Shuffle Pots"
option_off = 0
- option_all = 1
+ option_dungeons = 1
option_overworld = 2
- option_dungeons = 3
+ option_all = 3
class ShuffleCrates(Choice):
- """Shuffles large and small crates containing an item."""
+ """Shuffles large and small crates containing an item.
+ Dungeons: Only crates in dungeons are shuffled.
+ Overworld: Only crates in the overworld are shuffled.
+ All: All crates are shuffled."""
display_name = "Shuffle Crates"
option_off = 0
- option_all = 1
+ option_dungeons = 1
option_overworld = 2
- option_dungeons = 3
+ option_all = 3
class ShuffleBeehives(Toggle):
@@ -597,72 +630,113 @@ class ShuffleFrogRupees(Toggle):
class ShuffleMapCompass(Choice):
- """Control where to shuffle dungeon maps and compasses."""
+ """Control where to shuffle dungeon maps and compasses.
+ Remove: There will be no maps or compasses in the itempool.
+ Startwith: You start with all maps and compasses.
+ Vanilla: Maps and compasses remain vanilla.
+ Dungeon: Maps and compasses are shuffled within their original dungeon.
+ Regional: Maps and compasses are shuffled only in regions near the original dungeon.
+ Overworld: Maps and compasses are shuffled locally outside of dungeons.
+ Any Dungeon: Maps and compasses are shuffled locally in any dungeon.
+ Keysanity: Maps and compasses can be anywhere in the multiworld."""
display_name = "Maps & Compasses"
option_remove = 0
option_startwith = 1
option_vanilla = 2
option_dungeon = 3
- option_overworld = 4
- option_any_dungeon = 5
- option_keysanity = 6
- option_regional = 7
+ option_regional = 4
+ option_overworld = 5
+ option_any_dungeon = 6
+ option_keysanity = 7
default = 1
- alias_anywhere = 6
+ alias_anywhere = 7
class ShuffleKeys(Choice):
- """Control where to shuffle dungeon small keys."""
+ """Control where to shuffle dungeon small keys.
+ Remove/"Keysy": There will be no small keys in the itempool. All small key doors are automatically unlocked.
+ Vanilla: Small keys remain vanilla. You may start with extra small keys in some dungeons to prevent softlocks.
+ Dungeon: Small keys are shuffled within their original dungeon.
+ Regional: Small keys are shuffled only in regions near the original dungeon.
+ Overworld: Small keys are shuffled locally outside of dungeons.
+ Any Dungeon: Small keys are shuffled locally in any dungeon.
+ Keysanity: Small keys can be anywhere in the multiworld."""
display_name = "Small Keys"
option_remove = 0
option_vanilla = 2
option_dungeon = 3
- option_overworld = 4
- option_any_dungeon = 5
- option_keysanity = 6
- option_regional = 7
+ option_regional = 4
+ option_overworld = 5
+ option_any_dungeon = 6
+ option_keysanity = 7
default = 3
alias_keysy = 0
- alias_anywhere = 6
+ alias_anywhere = 7
class ShuffleGerudoKeys(Choice):
- """Control where to shuffle the Thieves' Hideout small keys."""
+ """Control where to shuffle the Thieves' Hideout small keys.
+ Vanilla: Hideout keys remain vanilla.
+ Regional: Hideout keys are shuffled only in the Gerudo Valley/Desert Colossus area.
+ Overworld: Hideout keys are shuffled locally outside of dungeons.
+ Any Dungeon: Hideout keys are shuffled locally in any dungeon.
+ Keysanity: Hideout keys can be anywhere in the multiworld."""
display_name = "Thieves' Hideout Keys"
option_vanilla = 0
- option_overworld = 1
- option_any_dungeon = 2
- option_keysanity = 3
- option_regional = 4
- alias_anywhere = 3
+ option_regional = 1
+ option_overworld = 2
+ option_any_dungeon = 3
+ option_keysanity = 4
+ alias_anywhere = 4
class ShuffleBossKeys(Choice):
- """Control where to shuffle boss keys, except the Ganon's Castle Boss Key."""
+ """Control where to shuffle boss keys, except the Ganon's Castle Boss Key.
+ Remove/"Keysy": There will be no boss keys in the itempool. All boss key doors are automatically unlocked.
+ Vanilla: Boss keys remain vanilla. You may start with extra small keys in some dungeons to prevent softlocks.
+ Dungeon: Boss keys are shuffled within their original dungeon.
+ Regional: Boss keys are shuffled only in regions near the original dungeon.
+ Overworld: Boss keys are shuffled locally outside of dungeons.
+ Any Dungeon: Boss keys are shuffled locally in any dungeon.
+ Keysanity: Boss keys can be anywhere in the multiworld."""
display_name = "Boss Keys"
option_remove = 0
option_vanilla = 2
option_dungeon = 3
- option_overworld = 4
- option_any_dungeon = 5
- option_keysanity = 6
- option_regional = 7
+ option_regional = 4
+ option_overworld = 5
+ option_any_dungeon = 6
+ option_keysanity = 7
default = 3
alias_keysy = 0
- alias_anywhere = 6
+ alias_anywhere = 7
class ShuffleGanonBK(Choice):
- """Control how to shuffle the Ganon's Castle Boss Key."""
+ """Control how to shuffle the Ganon's Castle Boss Key (GCBK).
+ Remove: GCBK is removed, and the boss key door is automatically unlocked.
+ Vanilla: GCBK remains vanilla.
+ Dungeon: GCBK is shuffled within its original dungeon.
+ Regional: GCBK is shuffled only in Hyrule Field, Market, and Hyrule Castle areas.
+ Overworld: GCBK is shuffled locally outside of dungeons.
+ Any Dungeon: GCBK is shuffled locally in any dungeon.
+ Keysanity: GCBK can be anywhere in the multiworld.
+ On LACS: GCBK is on the Light Arrow Cutscene, which requires Shadow and Spirit Medallions.
+ Stones: GCBK will be awarded when reaching the target number of Spiritual Stones.
+ Medallions: GCBK will be awarded when reaching the target number of medallions.
+ Dungeons: GCBK will be awarded when reaching the target number of dungeon rewards.
+ Tokens: GCBK will be awarded when reaching the target number of Gold Skulltula Tokens.
+ Hearts: GCBK will be awarded when reaching the target number of hearts.
+ """
display_name = "Ganon's Boss Key"
option_remove = 0
option_vanilla = 2
option_dungeon = 3
- option_overworld = 4
- option_any_dungeon = 5
- option_keysanity = 6
- option_on_lacs = 7
- option_regional = 8
+ option_regional = 4
+ option_overworld = 5
+ option_any_dungeon = 6
+ option_keysanity = 7
+ option_on_lacs = 8
option_stones = 9
option_medallions = 10
option_dungeons = 11
@@ -670,7 +744,7 @@ class ShuffleGanonBK(Choice):
option_hearts = 13
default = 0
alias_keysy = 0
- alias_anywhere = 6
+ alias_anywhere = 7
class EnhanceMC(Toggle):
@@ -679,7 +753,7 @@ class EnhanceMC(Toggle):
class GanonBKMedallions(Range):
- """Set how many medallions are required to receive Ganon BK."""
+ """With medallions GCBK: set how many medallions are required to receive GCBK."""
display_name = "Medallions Required for Ganon's BK"
range_start = 1
range_end = 6
@@ -687,7 +761,7 @@ class GanonBKMedallions(Range):
class GanonBKStones(Range):
- """Set how many Spiritual Stones are required to receive Ganon BK."""
+ """With stones GCBK: set how many Spiritual Stones are required to receive GCBK."""
display_name = "Spiritual Stones Required for Ganon's BK"
range_start = 1
range_end = 3
@@ -695,7 +769,7 @@ class GanonBKStones(Range):
class GanonBKRewards(Range):
- """Set how many dungeon rewards are required to receive Ganon BK."""
+ """With dungeons GCBK: set how many dungeon rewards are required to receive GCBK."""
display_name = "Dungeon Rewards Required for Ganon's BK"
range_start = 1
range_end = 9
@@ -703,7 +777,7 @@ class GanonBKRewards(Range):
class GanonBKTokens(Range):
- """Set how many Gold Skulltula Tokens are required to receive Ganon BK."""
+ """With tokens GCBK: set how many Gold Skulltula Tokens are required to receive GCBK."""
display_name = "Tokens Required for Ganon's BK"
range_start = 1
range_end = 100
@@ -711,7 +785,7 @@ class GanonBKTokens(Range):
class GanonBKHearts(Range):
- """Set how many hearts are required to receive Ganon BK."""
+ """With hearts GCBK: set how many hearts are required to receive GCBK."""
display_name = "Hearts Required for Ganon's BK"
range_start = 4
range_end = 20
@@ -719,7 +793,9 @@ class GanonBKHearts(Range):
class KeyRings(Choice):
- """Dungeons have all small keys found at once, rather than individually."""
+ """A key ring grants all dungeon small keys at once, rather than individually.
+ Choose: Use the option "key_rings_list" to choose which dungeons have key rings.
+ All: All dungeons have key rings instead of small keys."""
display_name = "Key Rings Mode"
option_off = 0
option_choose = 1
@@ -728,7 +804,7 @@ class KeyRings(Choice):
class KeyRingList(OptionSet):
- """Select areas with keyrings rather than individual small keys."""
+ """With key rings as Choose: select areas with key rings rather than individual small keys."""
display_name = "Key Ring Areas"
valid_keys = {
"Thieves' Hideout",
@@ -828,7 +904,8 @@ class BigPoeCount(Range):
class FAETorchCount(Range):
- """Number of lit torches required to open Shadow Temple."""
+ """Number of lit torches required to open Shadow Temple.
+ Does not affect logic; use the trick Shadow Temple Entry with Fire Arrows if desired."""
display_name = "Fire Arrow Entry Torch Count"
range_start = 1
range_end = 24
@@ -853,7 +930,11 @@ class FAETorchCount(Range):
class CorrectChestAppearance(Choice):
- """Changes chest textures and/or sizes to match their contents. "Classic" is the old behavior of CSMC."""
+ """Changes chest textures and/or sizes to match their contents.
+ Off: All chests have their vanilla size/appearance.
+ Textures: Chest textures reflect their contents.
+ Both: Like Textures, but progression items and boss keys get big chests, and other items get small chests.
+ Classic: Old behavior of CSMC; textures distinguish keys from non-keys, and size distinguishes importance."""
display_name = "Chest Appearance Matches Contents"
option_off = 0
option_textures = 1
@@ -872,15 +953,24 @@ class InvisibleChests(Toggle):
class CorrectPotCrateAppearance(Choice):
- """Unchecked pots and crates have a different texture; unchecked beehives will wiggle. With textures_content, pots and crates have an appearance based on their contents; with textures_unchecked, all unchecked pots/crates have the same appearance."""
+ """Changes the appearance of pots, crates, and beehives that contain items.
+ Off: Vanilla appearance for all containers.
+ Textures (Content): Unchecked pots and crates have a texture reflecting their contents. Unchecked beehives with progression items will wiggle.
+ Textures (Unchecked): Unchecked pots and crates are golden. Unchecked beehives will wiggle.
+ """
display_name = "Pot, Crate, and Beehive Appearance"
option_off = 0
option_textures_content = 1
option_textures_unchecked = 2
+ default = 2
class Hints(Choice):
- """Gossip Stones can give hints about item locations."""
+ """Gossip Stones can give hints about item locations.
+ None: Gossip Stones do not give hints.
+ Mask: Gossip Stones give hints with Mask of Truth.
+ Agony: Gossip Stones give hints wtih Stone of Agony.
+ Always: Gossip Stones always give hints."""
display_name = "Gossip Stones"
option_none = 0
option_mask = 1
@@ -895,7 +985,9 @@ class MiscHints(DefaultOnToggle):
class HintDistribution(Choice):
- """Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc."""
+ """Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.
+ Detailed documentation on hint distributions can be found on the Archipelago GitHub or OoTRandomizer.com.
+ The Async hint distribution is intended for async multiworlds. It removes Way of the Hero hints to improve generation times, since they are not very useful in asyncs."""
display_name = "Hint Distribution"
option_balanced = 0
option_ddr = 1
@@ -907,10 +999,13 @@ class HintDistribution(Choice):
option_useless = 7
option_very_strong = 8
option_async = 9
+ default = 9
class TextShuffle(Choice):
- """Randomizes text in the game for comedic effect."""
+ """Randomizes text in the game for comedic effect.
+ Except Hints: does not randomize important text such as hints, small/boss key information, and item prices.
+ Complete: randomizes every textbox, including the useful ones."""
display_name = "Text Shuffle"
option_none = 0
option_except_hints = 1
@@ -946,7 +1041,8 @@ class HeroMode(Toggle):
class StartingToD(Choice):
- """Change the starting time of day."""
+ """Change the starting time of day.
+ Daytime starts at Sunrise and ends at Sunset. Default is between Morning and Noon."""
display_name = "Starting Time of Day"
option_default = 0
option_sunrise = 1
@@ -999,7 +1095,11 @@ class RupeeStart(Toggle):
}
class ItemPoolValue(Choice):
- """Changes the number of items available in the game."""
+ """Changes the number of items available in the game.
+ Plentiful: One extra copy of every major item.
+ Balanced: Original item pool.
+ Scarce: Extra copies of major items are removed. Heart containers are removed.
+ Minimal: All major item upgrades not used for locations are removed. All health is removed."""
display_name = "Item Pool"
option_plentiful = 0
option_balanced = 1
@@ -1009,7 +1109,12 @@ class ItemPoolValue(Choice):
class IceTraps(Choice):
- """Adds ice traps to the item pool."""
+ """Adds ice traps to the item pool.
+ Off: All ice traps are removed.
+ Normal: The vanilla quantity of ice traps are placed.
+ On/"Extra": There is a chance for some extra ice traps to be placed.
+ Mayhem: All added junk items are ice traps.
+ Onslaught: All junk items are replaced by ice traps, even those in the base pool."""
display_name = "Ice Traps"
option_off = 0
option_normal = 1
@@ -1021,34 +1126,27 @@ class IceTraps(Choice):
class IceTrapVisual(Choice):
- """Changes the appearance of ice traps as freestanding items."""
- display_name = "Ice Trap Appearance"
+ """Changes the appearance of traps, including other games' traps, as freestanding items."""
+ display_name = "Trap Appearance"
option_major_only = 0
option_junk_only = 1
option_anything = 2
-class AdultTradeStart(OptionSet):
- """Choose the items that can appear to start the adult trade sequence. By default it is Claim Check only."""
- display_name = "Adult Trade Sequence Items"
- default = {"Claim Check"}
- valid_keys = {
- "Pocket Egg",
- "Pocket Cucco",
- "Cojiro",
- "Odd Mushroom",
- "Poachers Saw",
- "Broken Sword",
- "Prescription",
- "Eyeball Frog",
- "Eyedrops",
- "Claim Check",
- }
-
- def __init__(self, value: typing.Iterable[str]):
- if not value:
- value = self.default
- super().__init__(value)
+class AdultTradeStart(Choice):
+ """Choose the item that starts the adult trade sequence."""
+ display_name = "Adult Trade Sequence Start"
+ option_pocket_egg = 0
+ option_pocket_cucco = 1
+ option_cojiro = 2
+ option_odd_mushroom = 3
+ option_poachers_saw = 4
+ option_broken_sword = 5
+ option_prescription = 6
+ option_eyeball_frog = 7
+ option_eyedrops = 8
+ option_claim_check = 9
+ default = 9
itempool_options: typing.Dict[str, type(Option)] = {
@@ -1068,7 +1166,7 @@ class Targeting(Choice):
class DisplayDpad(DefaultOnToggle):
- """Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots)."""
+ """Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots, mask)."""
display_name = "Display D-Pad HUD"
@@ -1179,19 +1277,19 @@ class LogicTricks(OptionList):
https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/LogicTricks.py
"""
display_name = "Logic Tricks"
- valid_keys = frozenset(normalized_name_tricks)
+ valid_keys = tuple(normalized_name_tricks.keys())
valid_keys_casefold = True
# All options assembled into a single dict
oot_options: typing.Dict[str, type(Option)] = {
+ "plando_connections": OoTPlandoConnections,
"logic_rules": Logic,
"logic_no_night_tokens_without_suns_song": NightTokens,
**open_options,
**world_options,
**bridge_options,
**dungeon_items_options,
- # **lacs_options,
**shuffle_options,
**timesavers_options,
**misc_options,
diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py
index ab1e75d1b997..2219d7bb95a8 100644
--- a/worlds/oot/Patches.py
+++ b/worlds/oot/Patches.py
@@ -29,14 +29,14 @@
from .texture_util import ci4_rgba16patch_to_ci8, rgba16_patch
from .Utils import __version__
-from worlds.Files import APContainer
+from worlds.Files import APPatch
from Utils import __version__ as ap_version
AP_PROGRESSION = 0xD4
AP_JUNK = 0xD5
-class OoTContainer(APContainer):
+class OoTContainer(APPatch):
game: str = 'Ocarina of Time'
def __init__(self, patch_data: bytes, base_path: str, output_directory: str,
@@ -2094,10 +2094,14 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name
if not world.dungeon_mq['Ganons Castle']:
chest_name = 'Ganons Castle Light Trial Lullaby Chest'
location = world.get_location(chest_name)
- if location.item.game == 'Ocarina of Time':
- item = read_rom_item(rom, location.item.index)
+ if not location.item.trap:
+ if location.item.game == 'Ocarina of Time':
+ item = read_rom_item(rom, location.item.index)
+ else:
+ item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
else:
- item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
+ looks_like_index = get_override_entry(world, location)[5]
+ item = read_rom_item(rom, looks_like_index)
if item['chest_type'] in (GOLD_CHEST, GILDED_CHEST, SKULL_CHEST_BIG):
rom.write_int16(0x321B176, 0xFC40) # original 0xFC48
@@ -2106,10 +2110,14 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name
chest_name = 'Spirit Temple Compass Chest'
chest_address = 0x2B6B07C
location = world.get_location(chest_name)
- if location.item.game == 'Ocarina of Time':
- item = read_rom_item(rom, location.item.index)
+ if not location.item.trap:
+ if location.item.game == 'Ocarina of Time':
+ item = read_rom_item(rom, location.item.index)
+ else:
+ item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
else:
- item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
+ looks_like_index = get_override_entry(world, location)[5]
+ item = read_rom_item(rom, looks_like_index)
if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL):
rom.write_int16(chest_address + 2, 0x0190) # X pos
rom.write_int16(chest_address + 6, 0xFABC) # Z pos
@@ -2120,10 +2128,14 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name
chest_address_0 = 0x21A02D0 # Address in setup 0
chest_address_2 = 0x21A06E4 # Address in setup 2
location = world.get_location(chest_name)
- if location.item.game == 'Ocarina of Time':
- item = read_rom_item(rom, location.item.index)
+ if not location.item.trap:
+ if location.item.game == 'Ocarina of Time':
+ item = read_rom_item(rom, location.item.index)
+ else:
+ item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
else:
- item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
+ looks_like_index = get_override_entry(world, location)[5]
+ item = read_rom_item(rom, looks_like_index)
if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL):
rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos
rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos
@@ -2170,7 +2182,7 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name
'Shadow Temple': ("the \x05\x45Shadow Temple", 'Bongo Bongo', 0x7f, 0xa3),
}
for dungeon in world.dungeon_mq:
- if dungeon in ['Gerudo Training Ground', 'Ganons Castle']:
+ if dungeon in ['Thieves Hideout', 'Gerudo Training Ground', 'Ganons Castle']:
pass
elif dungeon in ['Bottom of the Well', 'Ice Cavern']:
dungeon_name, boss_name, compass_id, map_id = dungeon_list[dungeon]
diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py
index 1f44cebdcfe2..529411f6fc2c 100644
--- a/worlds/oot/Rules.py
+++ b/worlds/oot/Rules.py
@@ -1,8 +1,12 @@
from collections import deque
import logging
+import typing
from .Regions import TimeOfDay
+from .DungeonList import dungeon_table
+from .Hints import HintArea
from .Items import oot_is_item_of_type
+from .LocationList import dungeon_song_locations
from BaseClasses import CollectionState
from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item
@@ -150,11 +154,16 @@ def set_rules(ootworld):
location = world.get_location('Forest Temple MQ First Room Chest', player)
forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player)
- if ootworld.shuffle_song_items == 'song' and not ootworld.songs_as_items:
+ if ootworld.shuffle_song_items in {'song', 'dungeon'} and not ootworld.songs_as_items:
# Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else.
# This is required if map/compass included, or any_dungeon shuffle.
location = world.get_location('Sheik in Ice Cavern', player)
- add_item_rule(location, lambda item: item.player == player and oot_is_item_of_type(item, 'Song'))
+ add_item_rule(location, lambda item: oot_is_item_of_type(item, 'Song'))
+
+ if ootworld.shuffle_child_trade == 'skip_child_zelda':
+ # Song from Impa must be local
+ location = world.get_location('Song from Impa', player)
+ add_item_rule(location, lambda item: item.player == player)
for name in ootworld.always_hints:
add_rule(world.get_location(name, player), guarantee_hint)
@@ -176,11 +185,6 @@ def required_wallets(price):
return parser.parse_rule('(Progressive_Wallet, %d)' % required_wallets(location.price))
-def limit_to_itemset(location, itemset):
- old_rule = location.item_rule
- location.item_rule = lambda item: item.name in itemset and old_rule(item)
-
-
# This function should be run once after the shop items are placed in the world.
# It should be run before other items are placed in the world so that logic has
# the correct checks for them. This is safe to do since every shop is still
@@ -223,10 +227,8 @@ def set_shop_rules(ootworld):
# The goal is to automatically set item rules based on age requirements in case entrances were shuffled
def set_entrances_based_rules(ootworld):
- if ootworld.multiworld.accessibility == 'beatable':
- return
-
- all_state = ootworld.multiworld.get_all_state(False)
+ all_state = ootworld.get_state_with_complete_itempool()
+ all_state.sweep_for_events(locations=ootworld.get_locations())
for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()):
# If a shop is not reachable as adult, it can't have Goron Tunic or Zora Tunic as child can't buy these
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/oot/__init__.py b/worlds/oot/__init__.py
index 655f4989b243..89f10a5a2da0 100644
--- a/worlds/oot/__init__.py
+++ b/worlds/oot/__init__.py
@@ -10,7 +10,7 @@
logger = logging.getLogger("Ocarina of Time")
-from .Location import OOTLocation, LocationFactory, location_name_to_id
+from .Location import OOTLocation, LocationFactory, location_name_to_id, build_location_name_groups
from .Entrance import OOTEntrance
from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError
from .HintList import getRequiredHints
@@ -32,7 +32,7 @@
from Utils import get_options
from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType
-from Options import Range, Toggle, VerifyKeys
+from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections
from Fill import fill_restrictive, fast_fill, FillError
from worlds.generic.Rules import exclusion_rules, add_item_rule
from ..AutoWorld import World, AutoLogicRegister, WebWorld
@@ -43,14 +43,14 @@
class OOTCollectionState(metaclass=AutoLogicRegister):
def init_mixin(self, parent: MultiWorld):
- all_ids = parent.get_all_ids()
- self.child_reachable_regions = {player: set() for player in all_ids}
- self.adult_reachable_regions = {player: set() for player in all_ids}
- self.child_blocked_connections = {player: set() for player in all_ids}
- self.adult_blocked_connections = {player: set() for player in all_ids}
- self.day_reachable_regions = {player: set() for player in all_ids}
- self.dampe_reachable_regions = {player: set() for player in all_ids}
- self.age = {player: None for player in all_ids}
+ oot_ids = parent.get_game_players(OOTWorld.game) + parent.get_game_groups(OOTWorld.game)
+ self.child_reachable_regions = {player: set() for player in oot_ids}
+ self.adult_reachable_regions = {player: set() for player in oot_ids}
+ self.child_blocked_connections = {player: set() for player in oot_ids}
+ self.adult_blocked_connections = {player: set() for player in oot_ids}
+ self.day_reachable_regions = {player: set() for player in oot_ids}
+ self.dampe_reachable_regions = {player: set() for player in oot_ids}
+ self.age = {player: None for player in oot_ids}
def copy_mixin(self, ret) -> CollectionState:
ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in
@@ -92,7 +92,7 @@ class RomStart(str):
class OOTWeb(WebWorld):
setup = Tutorial(
- "Multiworld Setup Tutorial",
+ "Multiworld Setup Guide",
"A guide to setting up the Archipelago Ocarina of Time software on your computer.",
"English",
"setup_en.md",
@@ -118,7 +118,16 @@ class OOTWeb(WebWorld):
["TheLynk"]
)
- tutorials = [setup, setup_es, setup_fr]
+ setup_de = Tutorial(
+ setup.tutorial_name,
+ setup.description,
+ "Deutsch",
+ "setup_de.md",
+ "setup/de",
+ ["Held_der_Zeit"]
+ )
+
+ tutorials = [setup, setup_es, setup_fr, setup_de]
class OOTWorld(World):
@@ -141,8 +150,6 @@ class OOTWorld(World):
location_name_to_id = location_name_to_id
web = OOTWeb()
- data_version = 3
-
required_client_version = (0, 4, 0)
item_name_groups = {
@@ -163,20 +170,35 @@ class OOTWorld(World):
"Bottle with Big Poe", "Bottle with Red Potion", "Bottle with Green Potion",
"Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish",
"Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"},
- "Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Odd Mushroom",
+ "Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Cojiro", "Odd Mushroom",
"Odd Potion", "Poachers Saw", "Broken Sword", "Prescription",
- "Eyeball Frog", "Eyedrops", "Claim Check"}
+ "Eyeball Frog", "Eyedrops", "Claim Check"},
+ "Keys": {"Small Key (Bottom of the Well)", "Small Key (Fire Temple)", "Small Key (Forest Temple)",
+ "Small Key (Ganons Castle)", "Small Key (Gerudo Training Ground)", "Small Key (Shadow Temple)",
+ "Small Key (Spirit Temple)", "Small Key (Thieves Hideout)", "Small Key (Water Temple)",
+ "Small Key Ring (Bottom of the Well)", "Small Key Ring (Fire Temple)",
+ "Small Key Ring (Forest Temple)", "Small Key Ring (Ganons Castle)",
+ "Small Key Ring (Gerudo Training Ground)", "Small Key Ring (Shadow Temple)",
+ "Small Key Ring (Spirit Temple)", "Small Key Ring (Thieves Hideout)", "Small Key Ring (Water Temple)",
+ "Boss Key (Fire Temple)", "Boss Key (Forest Temple)", "Boss Key (Ganons Castle)",
+ "Boss Key (Shadow Temple)", "Boss Key (Spirit Temple)", "Boss Key (Water Temple)"},
}
+ location_name_groups = build_location_name_groups()
+
+
def __init__(self, world, player):
self.hint_data_available = threading.Event()
self.collectible_flags_available = threading.Event()
super(OOTWorld, self).__init__(world, player)
+
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld):
rom = Rom(file=get_options()['oot_options']['rom_file'])
+
+ # Option parsing, handling incompatible options, building useful-item table
def generate_early(self):
self.parser = Rule_AST_Transformer(self, self.player)
@@ -188,12 +210,16 @@ def generate_early(self):
option_value = bool(result)
elif isinstance(result, VerifyKeys):
option_value = result.value
+ elif isinstance(result, PlandoConnections):
+ option_value = result.value
else:
option_value = result.current_key
setattr(self, option_name, option_value)
+ self.regions = [] # internal caches of regions for this world, used later
+ self._regions_cache = {}
+
self.shop_prices = {}
- self.regions = [] # internal cache of regions for this world, used later
self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory
self.starting_items = Counter()
self.songs_as_items = False
@@ -286,7 +312,7 @@ def generate_early(self):
# No Logic forces all tricks on, prog balancing off and beatable-only
elif self.logic_rules == 'no_logic':
self.multiworld.progression_balancing[self.player].value = False
- self.multiworld.accessibility[self.player] = self.multiworld.accessibility[self.player].from_text("minimal")
+ self.multiworld.accessibility[self.player].value = Accessibility.option_minimal
for trick in normalized_name_tricks.values():
setattr(self, trick['name'], True)
@@ -384,6 +410,7 @@ def generate_early(self):
self.mq_dungeons_mode = 'count'
self.mq_dungeons_count = 0
self.dungeon_mq = {item['name']: (item['name'] in mq_dungeons) for item in dungeon_table}
+ self.dungeon_mq['Thieves Hideout'] = False # fix for bug in SaveContext:287
# Empty dungeon placeholder for the moment
self.empty_dungeons = {name: False for name in self.dungeon_mq}
@@ -409,6 +436,9 @@ def generate_early(self):
self.starting_tod = self.starting_tod.replace('_', '-')
self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '')
+ # Convert adult trade option to expected Set
+ self.adult_trade_start = {self.adult_trade_start.title().replace('_', ' ')}
+
# Get hint distribution
self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json'))
@@ -446,7 +476,7 @@ def generate_early(self):
self.always_hints = [hint.name for hint in getRequiredHints(self)]
# Determine items which are not considered advancement based on settings. They will never be excluded.
- self.nonadvancement_items = {'Double Defense'}
+ self.nonadvancement_items = {'Double Defense', 'Deku Stick Capacity', 'Deku Nut Capacity'}
if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and
self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances):
# nayru's love may be required to prevent forced damage
@@ -483,6 +513,8 @@ def generate_early(self):
# Farore's Wind skippable if not used for this logic trick in Water Temple
self.nonadvancement_items.add('Farores Wind')
+
+ # Reads a group of regions from the given JSON file.
def load_regions_from_json(self, file_path):
region_json = read_json(file_path)
@@ -520,6 +552,10 @@ def load_regions_from_json(self, file_path):
# We still need to fill the location even if ALR is off.
logger.debug('Unreachable location: %s', new_location.name)
new_location.player = self.player
+ # Change some attributes of Drop locations
+ if new_location.type == 'Drop':
+ new_location.name = new_region.name + ' ' + new_location.name
+ new_location.show_in_spoiler = False
new_region.locations.append(new_location)
if 'events' in region:
for event, rule in region['events'].items():
@@ -549,8 +585,10 @@ def load_regions_from_json(self, file_path):
self.multiworld.regions.append(new_region)
self.regions.append(new_region)
- self.multiworld._recache()
+ self._regions_cache[new_region.name] = new_region
+
+ # Sets deku scrub prices
def set_scrub_prices(self):
# Get Deku Scrub Locations
scrub_locations = [location for location in self.get_locations() if location.type in {'Scrub', 'GrottoScrub'}]
@@ -579,6 +617,8 @@ def set_scrub_prices(self):
if location.item is not None:
location.item.price = price
+
+ # Sets prices for shuffled shop locations
def random_shop_prices(self):
shop_item_indexes = ['7', '5', '8', '6']
self.shop_prices = {}
@@ -604,6 +644,8 @@ def random_shop_prices(self):
elif self.shopsanity_prices == 'tycoons_wallet':
self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5)
+
+ # Fill boss prizes
def fill_bosses(self, bossCount=9):
boss_location_names = (
'Queen Gohma',
@@ -616,7 +658,7 @@ def fill_bosses(self, bossCount=9):
'Twinrova',
'Links Pocket'
)
- boss_rewards = [item for item in self.itempool if item.type == 'DungeonReward']
+ boss_rewards = sorted(map(self.create_item, self.item_name_groups['rewards']))
boss_locations = [self.multiworld.get_location(loc, self.player) for loc in boss_location_names]
placed_prizes = [loc.item.name for loc in boss_locations if loc.item is not None]
@@ -630,27 +672,67 @@ def fill_bosses(self, bossCount=9):
item = prizepool.pop()
loc = prize_locs.pop()
loc.place_locked_item(item)
- self.multiworld.itempool.remove(item)
self.hinted_dungeon_reward_locations[item.name] = loc
- def create_item(self, name: str):
+
+ # Separate the result from generate_itempool into main and prefill pools
+ def divide_itempools(self):
+ prefill_item_types = set()
+ if self.shopsanity != 'off':
+ prefill_item_types.add('Shop')
+ if self.shuffle_song_items != 'any':
+ prefill_item_types.add('Song')
+ if self.shuffle_smallkeys != 'keysanity':
+ prefill_item_types.add('SmallKey')
+ if self.shuffle_bosskeys != 'keysanity':
+ prefill_item_types.add('BossKey')
+ if self.shuffle_hideoutkeys != 'keysanity':
+ prefill_item_types.add('HideoutSmallKey')
+ if self.shuffle_ganon_bosskey != 'keysanity':
+ prefill_item_types.add('GanonBossKey')
+ if self.shuffle_mapcompass != 'keysanity':
+ prefill_item_types.update({'Map', 'Compass'})
+
+ main_items = []
+ prefill_items = []
+ for item in self.itempool:
+ if item.type in prefill_item_types:
+ prefill_items.append(item)
+ else:
+ main_items.append(item)
+ return main_items, prefill_items
+
+
+ # only returns proper result after create_items and divide_itempools are run
+ def get_pre_fill_items(self):
+ return self.pre_fill_items
+
+
+ # Note on allow_arbitrary_name:
+ # OoT defines many helper items and event names that are treated indistinguishably from regular items,
+ # but are only defined in the logic files. This means we need to create items for any name.
+ # Allowing any item name to be created is dangerous in case of plando, so this is a middle ground.
+ def create_item(self, name: str, allow_arbitrary_name: bool = False):
if name in item_table:
return OOTItem(name, self.player, item_table[name], False,
(name in self.nonadvancement_items if getattr(self, 'nonadvancement_items',
None) else False))
- return OOTItem(name, self.player, ('Event', True, None, None), True, False)
+ if allow_arbitrary_name:
+ return OOTItem(name, self.player, ('Event', True, None, None), True, False)
+ raise Exception(f"Invalid item name: {name}")
def make_event_item(self, name, location, item=None):
if item is None:
- item = self.create_item(name)
+ item = self.create_item(name, allow_arbitrary_name=True)
self.multiworld.push_item(location, item, collect=False)
location.locked = True
- location.event = True
if name not in item_table:
location.internal = True
return item
- def create_regions(self): # create and link regions
+
+ # Create regions, locations, and entrances
+ def create_regions(self):
if self.logic_rules == 'glitchless' or self.logic_rules == 'no_logic': # enables ER + NL
world_type = 'World'
else:
@@ -663,7 +745,7 @@ def create_regions(self): # create and link regions
self.multiworld.regions.append(menu)
self.load_regions_from_json(overworld_data_path)
self.load_regions_from_json(bosses_data_path)
- start.connect(self.multiworld.get_region('Root', self.player))
+ start.connect(self.get_region('Root'))
create_dungeons(self)
self.parser.create_delayed_rules()
@@ -674,16 +756,13 @@ def create_regions(self): # create and link regions
# Bind entrances to vanilla
for region in self.regions:
for exit in region.exits:
- exit.connect(self.multiworld.get_region(exit.vanilla_connected_region, self.player))
+ exit.connect(self.get_region(exit.vanilla_connected_region))
+
+ # Create items, starting item handling, boss prize fill (before entrance randomizer)
def create_items(self):
- # Uniquely rename drop locations for each region and erase them from the spoiler
- set_drop_location_names(self)
# Generate itempool
generate_itempool(self)
- # Add dungeon rewards
- rewardlist = sorted(list(self.item_name_groups['rewards']))
- self.itempool += map(self.create_item, rewardlist)
junk_pool = get_junk_pool(self)
removed_items = []
@@ -706,12 +785,16 @@ def create_items(self):
if self.start_with_rupees:
self.starting_items['Rupees'] = 999
+ # Divide itempool into prefill and main pools
+ self.itempool, self.pre_fill_items = self.divide_itempools()
+
self.multiworld.itempool += self.itempool
self.remove_from_start_inventory.extend(removed_items)
# Fill boss prizes. needs to happen before entrance shuffle
self.fill_bosses()
+
def set_rules(self):
# This has to run AFTER creating items but BEFORE set_entrances_based_rules
if self.entrance_shuffle:
@@ -749,6 +832,7 @@ def set_rules(self):
set_rules(self)
set_entrances_based_rules(self)
+
def generate_basic(self): # mostly killing locations that shouldn't exist by settings
# Gather items for ice trap appearances
@@ -761,11 +845,12 @@ def generate_basic(self): # mostly killing locations that shouldn't exist by se
# Kill unreachable events that can't be gotten even with all items
# Make sure to only kill actual internal events, not in-game "events"
- all_state = self.multiworld.get_all_state(False)
+ all_state = self.get_state_with_complete_itempool()
all_locations = self.get_locations()
+ all_state.sweep_for_events(locations=all_locations)
reachable = self.multiworld.get_reachable_locations(all_state, self.player)
unreachable = [loc for loc in all_locations if
- (loc.internal or loc.type == 'Drop') and loc.event and loc.locked and loc not in reachable]
+ (loc.internal or loc.type == 'Drop') and loc.address is None and loc.locked and loc not in reachable]
for loc in unreachable:
loc.parent_region.locations.remove(loc)
# Exception: Sell Big Poe is an event which is only reachable if Bottle with Big Poe is in the item pool.
@@ -773,7 +858,6 @@ def generate_basic(self): # mostly killing locations that shouldn't exist by se
bigpoe = self.multiworld.get_location('Sell Big Poe from Market Guard House', self.player)
if not all_state.has('Bottle with Big Poe', self.player) and bigpoe not in reachable:
bigpoe.parent_region.locations.remove(bigpoe)
- self.multiworld.clear_location_cache()
# If fast scarecrow then we need to kill the Pierre location as it will be unreachable
if self.free_scarecrow:
@@ -784,39 +868,69 @@ def generate_basic(self): # mostly killing locations that shouldn't exist by se
loc = self.multiworld.get_location("Deliver Rutos Letter", self.player)
loc.parent_region.locations.remove(loc)
+
def pre_fill(self):
+ def prefill_state(base_state):
+ state = base_state.copy()
+ for item in self.get_pre_fill_items():
+ self.collect(state, item)
+ state.sweep_for_events(locations=self.get_locations())
+ return state
+
+ # Prefill shops, songs, and dungeon items
+ items = self.get_pre_fill_items()
+ locations = list(self.multiworld.get_unfilled_locations(self.player))
+ self.multiworld.random.shuffle(locations)
+
+ # Set up initial state
+ state = CollectionState(self.multiworld)
+ for item in self.itempool:
+ self.collect(state, item)
+ state.sweep_for_events(locations=self.get_locations())
+
# Place dungeon items
special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass']
- world_items = [item for item in self.multiworld.itempool if item.player == self.player]
+ type_to_setting = {
+ 'Map': 'shuffle_mapcompass',
+ 'Compass': 'shuffle_mapcompass',
+ 'SmallKey': 'shuffle_smallkeys',
+ 'BossKey': 'shuffle_bosskeys',
+ 'HideoutSmallKey': 'shuffle_hideoutkeys',
+ 'GanonBossKey': 'shuffle_ganon_bosskey',
+ }
+ special_fill_types.sort(key=lambda x: 0 if getattr(self, type_to_setting[x]) == 'dungeon' else 1)
+
for fill_stage in special_fill_types:
- stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), world_items))
+ stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), self.pre_fill_items))
if not stage_items:
continue
if fill_stage in ['GanonBossKey', 'HideoutSmallKey']:
locations = gather_locations(self.multiworld, fill_stage, self.player)
if isinstance(locations, list):
for item in stage_items:
- self.multiworld.itempool.remove(item)
+ self.pre_fill_items.remove(item)
self.multiworld.random.shuffle(locations)
- fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items,
- single_player_placement=True, lock=True)
+ fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items,
+ single_player_placement=True, lock=True, allow_excluded=True)
else:
for dungeon_info in dungeon_table:
dungeon_name = dungeon_info['name']
+ dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items))
+ if not dungeon_items:
+ continue
locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name)
if isinstance(locations, list):
- dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items))
for item in dungeon_items:
- self.multiworld.itempool.remove(item)
+ self.pre_fill_items.remove(item)
self.multiworld.random.shuffle(locations)
- fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items,
- single_player_placement=True, lock=True)
+ fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items,
+ single_player_placement=True, lock=True, allow_excluded=True)
# Place songs
# 5 built-in retries because this section can fail sometimes
if self.shuffle_song_items != 'any':
- tries = 5
+ tries = 10
if self.shuffle_song_items == 'song':
song_locations = list(filter(lambda location: location.type == 'Song',
self.multiworld.get_unfilled_locations(player=self.player)))
@@ -826,9 +940,9 @@ def pre_fill(self):
else:
raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}")
- songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.multiworld.itempool))
+ songs = list(filter(lambda item: item.type == 'Song', self.pre_fill_items))
for song in songs:
- self.multiworld.itempool.remove(song)
+ self.pre_fill_items.remove(song)
important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or
self.warp_songs or self.spawn_positions)
@@ -851,8 +965,8 @@ def pre_fill(self):
while tries:
try:
self.multiworld.random.shuffle(song_locations)
- fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:],
- True, True)
+ fill_restrictive(self.multiworld, prefill_state(state), song_locations[:], songs[:],
+ single_player_placement=True, lock=True, allow_excluded=True)
logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
except FillError as e:
tries -= 1
@@ -866,17 +980,14 @@ def pre_fill(self):
for location in song_locations:
location.item = None
location.locked = False
- location.event = False
else:
break
# Place shop items
# fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items
if self.shopsanity != 'off':
- shop_prog = list(filter(lambda item: item.player == self.player and item.type == 'Shop'
- and item.advancement, self.multiworld.itempool))
- shop_junk = list(filter(lambda item: item.player == self.player and item.type == 'Shop'
- and not item.advancement, self.multiworld.itempool))
+ shop_prog = list(filter(lambda item: item.type == 'Shop' and item.advancement, self.pre_fill_items))
+ shop_junk = list(filter(lambda item: item.type == 'Shop' and not item.advancement, self.pre_fill_items))
shop_locations = list(
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
self.multiworld.get_unfilled_locations(player=self.player)))
@@ -886,29 +997,14 @@ def pre_fill(self):
'Buy Zora Tunic': 1,
}.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement
self.multiworld.random.shuffle(shop_locations)
- for item in shop_prog + shop_junk:
- self.multiworld.itempool.remove(item)
- fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog, True, True)
+ self.pre_fill_items = [] # all prefill should be done
+ fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog,
+ single_player_placement=True, lock=True, allow_excluded=True)
fast_fill(self.multiworld, shop_junk, shop_locations)
for loc in shop_locations:
loc.locked = True
set_shop_rules(self) # sets wallet requirements on shop items, must be done after they are filled
- # If skip child zelda is active and Song from Impa is unfilled, put a local giveable item into it.
- impa = self.multiworld.get_location("Song from Impa", self.player)
- if self.shuffle_child_trade == 'skip_child_zelda':
- if impa.item is None:
- candidate_items = list(item for item in self.multiworld.itempool if item.player == self.player)
- if candidate_items:
- item_to_place = self.multiworld.random.choice(candidate_items)
- self.multiworld.itempool.remove(item_to_place)
- else:
- item_to_place = self.create_item("Recovery Heart")
- impa.place_locked_item(item_to_place)
- # Give items to startinventory
- self.multiworld.push_precollected(impa.item)
- self.multiworld.push_precollected(self.create_item("Zeldas Letter"))
-
# Exclude locations in Ganon's Castle proportional to the number of items required to make the bridge
# Check for dungeon ER later
if self.logic_rules == 'glitchless':
@@ -943,50 +1039,33 @@ def pre_fill(self):
or (self.shuffle_child_trade == 'skip_child_zelda' and loc.name in ['HC Zeldas Letter', 'Song from Impa'])):
loc.address = None
- # Handle item-linked dungeon items and songs
- @classmethod
- def stage_pre_fill(cls, multiworld: MultiWorld):
- special_fill_types = ['Song', 'GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass']
- for group_id, group in multiworld.groups.items():
- if group['game'] != cls.game:
- continue
- group_items = [item for item in multiworld.itempool if item.player == group_id]
- for fill_stage in special_fill_types:
- group_stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), group_items))
- if not group_stage_items:
- continue
- if fill_stage in ['Song', 'GanonBossKey', 'HideoutSmallKey']:
- # No need to subdivide by dungeon name
- locations = gather_locations(multiworld, fill_stage, group['players'])
- if isinstance(locations, list):
- for item in group_stage_items:
- multiworld.itempool.remove(item)
- multiworld.random.shuffle(locations)
- fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items,
- single_player_placement=False, lock=True)
- if fill_stage == 'Song':
- # We don't want song locations to contain progression unless it's a song
- # or it was marked as priority.
- # We do this manually because we'd otherwise have to either
- # iterate twice or do many function calls.
- for loc in locations:
- if loc.progress_type == LocationProgressType.DEFAULT:
- loc.progress_type = LocationProgressType.EXCLUDED
- add_item_rule(loc, lambda i: not (i.advancement or i.useful))
- else:
- # Perform the fill task once per dungeon
- for dungeon_info in dungeon_table:
- dungeon_name = dungeon_info['name']
- locations = gather_locations(multiworld, fill_stage, group['players'], dungeon=dungeon_name)
- if isinstance(locations, list):
- group_dungeon_items = list(filter(lambda item: dungeon_name in item.name, group_stage_items))
- for item in group_dungeon_items:
- multiworld.itempool.remove(item)
- multiworld.random.shuffle(locations)
- fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items,
- single_player_placement=False, lock=True)
def generate_output(self, output_directory: str):
+
+ # Write entrances to spoiler log
+ all_entrances = self.get_shuffled_entrances()
+ all_entrances.sort(reverse=True, key=lambda x: (x.type, x.name))
+ if not self.decouple_entrances:
+ while all_entrances:
+ loadzone = all_entrances.pop()
+ if loadzone.type != 'Overworld':
+ if loadzone.primary:
+ entrance = loadzone
+ else:
+ entrance = loadzone.reverse
+ if entrance.reverse is not None:
+ self.multiworld.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player)
+ else:
+ self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
+ else:
+ reverse = loadzone.replaces.reverse
+ if reverse in all_entrances:
+ all_entrances.remove(reverse)
+ self.multiworld.spoiler.set_entrance(loadzone, reverse, 'both', self.player)
+ else:
+ for entrance in all_entrances:
+ self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
+
if self.hints != 'none':
self.hint_data_available.wait()
@@ -1021,37 +1100,16 @@ def generate_output(self, output_directory: str):
player_name=self.multiworld.get_player_name(self.player))
apz5.write()
- # Write entrances to spoiler log
- all_entrances = self.get_shuffled_entrances()
- all_entrances.sort(reverse=True, key=lambda x: x.name)
- all_entrances.sort(reverse=True, key=lambda x: x.type)
- if not self.decouple_entrances:
- while all_entrances:
- loadzone = all_entrances.pop()
- if loadzone.type != 'Overworld':
- if loadzone.primary:
- entrance = loadzone
- else:
- entrance = loadzone.reverse
- if entrance.reverse is not None:
- self.multiworld.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player)
- else:
- self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
- else:
- reverse = loadzone.replaces.reverse
- if reverse in all_entrances:
- all_entrances.remove(reverse)
- self.multiworld.spoiler.set_entrance(loadzone, reverse, 'both', self.player)
- else:
- for entrance in all_entrances:
- self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
# Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations.
@classmethod
def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str):
def hint_type_players(hint_type: str) -> set:
return {autoworld.player for autoworld in multiworld.get_game_worlds("Ocarina of Time")
- if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0}
+ if autoworld.hints != 'none'
+ and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0
+ and (autoworld.hint_dist_user['distribution'][hint_type]['fixed'] > 0
+ or autoworld.hint_dist_user['distribution'][hint_type]['weight'] > 0)}
try:
item_hint_players = hint_type_players('item')
@@ -1078,10 +1136,10 @@ def hint_type_players(hint_type: str) -> set:
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or
(oot_is_item_of_type(loc.item, 'Song') or
- (oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or
- (oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys == 'any_dungeon') or
- (oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or
- (oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))):
+ (oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys in ('overworld', 'any_dungeon', 'regional')) or
+ (oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys in ('overworld', 'any_dungeon', 'regional')) or
+ (oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys in ('overworld', 'any_dungeon', 'regional')) or
+ (oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))):
if loc.player in barren_hint_players:
hint_area = get_hint_area(loc)
items_by_region[loc.player][hint_area]['weight'] += 1
@@ -1096,7 +1154,12 @@ def hint_type_players(hint_type: str) -> set:
elif barren_hint_players or woth_hint_players: # Check only relevant oot locations for barren/woth
for player in (barren_hint_players | woth_hint_players):
for loc in multiworld.worlds[player].get_locations():
- if loc.item.code and (not loc.locked or oot_is_item_of_type(loc.item, 'Song')):
+ if loc.item.code and (not loc.locked or
+ (oot_is_item_of_type(loc.item, 'Song') or
+ (oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys in ('overworld', 'any_dungeon', 'regional')) or
+ (oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys in ('overworld', 'any_dungeon', 'regional')) or
+ (oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys in ('overworld', 'any_dungeon', 'regional')) or
+ (oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))):
if player in barren_hint_players:
hint_area = get_hint_area(loc)
items_by_region[player][hint_area]['weight'] += 1
@@ -1116,12 +1179,39 @@ def hint_type_players(hint_type: str) -> set:
for autoworld in multiworld.get_game_worlds("Ocarina of Time"):
autoworld.hint_data_available.set()
+
def fill_slot_data(self):
self.collectible_flags_available.wait()
- return {
+
+ slot_data = {
'collectible_override_flags': self.collectible_override_flags,
'collectible_flag_offsets': self.collectible_flag_offsets
}
+ slot_data.update(self.options.as_dict(
+ "open_forest", "open_kakariko", "open_door_of_time", "zora_fountain", "gerudo_fortress",
+ "bridge", "bridge_stones", "bridge_medallions", "bridge_rewards", "bridge_tokens", "bridge_hearts",
+ "shuffle_ganon_bosskey", "ganon_bosskey_medallions", "ganon_bosskey_stones", "ganon_bosskey_rewards",
+ "ganon_bosskey_tokens", "ganon_bosskey_hearts", "trials",
+ "triforce_hunt", "triforce_goal", "extra_triforce_percentage",
+ "shopsanity", "shop_slots", "shopsanity_prices", "tokensanity",
+ "dungeon_shortcuts", "dungeon_shortcuts_list",
+ "mq_dungeons_mode", "mq_dungeons_list", "mq_dungeons_count",
+ "shuffle_interior_entrances", "shuffle_grotto_entrances", "shuffle_dungeon_entrances",
+ "shuffle_overworld_entrances", "shuffle_bosses", "key_rings", "key_rings_list", "enhance_map_compass",
+ "shuffle_mapcompass", "shuffle_smallkeys", "shuffle_hideoutkeys", "shuffle_bosskeys",
+ "logic_rules", "logic_no_night_tokens_without_suns_song", "logic_tricks",
+ "warp_songs", "shuffle_song_items","shuffle_medigoron_carpet_salesman", "shuffle_frog_song_rupees",
+ "shuffle_scrubs", "shuffle_child_trade", "shuffle_freestanding_items", "shuffle_pots", "shuffle_crates",
+ "shuffle_cows", "shuffle_beehives", "shuffle_kokiri_sword", "shuffle_ocarinas", "shuffle_gerudo_card",
+ "shuffle_beans", "starting_age", "bombchus_in_logic", "spawn_positions", "owl_drops",
+ "no_epona_race", "skip_some_minigame_phases", "complete_mask_quest", "free_scarecrow", "plant_beans",
+ "chicken_count", "big_poe_count", "fae_torch_count", "blue_fire_arrows",
+ "damage_multiplier", "deadly_bonks", "starting_tod", "junk_ice_traps",
+ "start_with_consumables", "adult_trade_start", "plando_connections"
+ )
+ )
+ return slot_data
+
def modify_multidata(self, multidata: dict):
@@ -1137,6 +1227,16 @@ def modify_multidata(self, multidata: dict):
continue
multidata["precollected_items"][self.player].remove(item_id)
+ # If skip child zelda, push item onto autotracker
+ if self.shuffle_child_trade == 'skip_child_zelda':
+ impa_item_id = self.item_name_to_id.get(self.get_location('Song from Impa').item.name, None)
+ zelda_item_id = self.item_name_to_id.get(self.get_location('HC Zeldas Letter').item.name, None)
+ if impa_item_id:
+ multidata["precollected_items"][self.player].append(impa_item_id)
+ if zelda_item_id:
+ multidata["precollected_items"][self.player].append(zelda_item_id)
+
+
def extend_hint_information(self, er_hint_data: dict):
er_hint_data[self.player] = {}
@@ -1183,6 +1283,19 @@ def get_entrance_to_region(region):
er_hint_data[self.player][location.address] = main_entrance.name
logger.debug(f"Set {location.name} hint data to {main_entrance.name}")
+
+ def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
+ required_trials_str = ", ".join(t for t in self.skipped_trials if not self.skipped_trials[t])
+ if required_trials_str == "":
+ required_trials_str = "None"
+ spoiler_handle.write(f"\n\nTrials ({self.multiworld.get_player_name(self.player)}): {required_trials_str}\n")
+
+ if self.shopsanity != 'off':
+ spoiler_handle.write(f"\nShop Prices ({self.multiworld.get_player_name(self.player)}):\n")
+ for k, v in self.shop_prices.items():
+ spoiler_handle.write(f"{k}: {v} Rupees\n")
+
+
# Key ring handling:
# Key rings are multiple items glued together into one, so we need to give
# the appropriate number of keys in the collection state when they are
@@ -1190,16 +1303,16 @@ def get_entrance_to_region(region):
def collect(self, state: CollectionState, item: OOTItem) -> bool:
if item.advancement and item.special and item.special.get('alias', False):
alt_item_name, count = item.special.get('alias')
- state.prog_items[alt_item_name, self.player] += count
+ state.prog_items[self.player][alt_item_name] += count
return True
return super().collect(state, item)
def remove(self, state: CollectionState, item: OOTItem) -> bool:
if item.advancement and item.special and item.special.get('alias', False):
alt_item_name, count = item.special.get('alias')
- state.prog_items[alt_item_name, self.player] -= count
- if state.prog_items[alt_item_name, self.player] < 1:
- del (state.prog_items[alt_item_name, self.player])
+ state.prog_items[self.player][alt_item_name] -= count
+ if state.prog_items[self.player][alt_item_name] < 1:
+ del (state.prog_items[self.player][alt_item_name])
return True
return super().remove(state, item)
@@ -1214,24 +1327,29 @@ def region_has_shortcuts(self, regionname):
return False
def get_shufflable_entrances(self, type=None, only_primary=False):
- return [entrance for entrance in self.multiworld.get_entrances() if (entrance.player == self.player and
- (type == None or entrance.type == type) and
- (not only_primary or entrance.primary))]
+ return [entrance for entrance in self.get_entrances() if ((type == None or entrance.type == type)
+ and (not only_primary or entrance.primary))]
def get_shuffled_entrances(self, type=None, only_primary=False):
return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if
entrance.shuffled]
def get_locations(self):
- for region in self.regions:
- for loc in region.locations:
- yield loc
+ return self.multiworld.get_locations(self.player)
def get_location(self, location):
return self.multiworld.get_location(location, self.player)
- def get_region(self, region):
- return self.multiworld.get_region(region, self.player)
+ def get_region(self, region_name):
+ try:
+ return self._regions_cache[region_name]
+ except KeyError:
+ ret = self.multiworld.get_region(region_name, self.player)
+ self._regions_cache[region_name] = ret
+ return ret
+
+ def get_entrances(self):
+ return self.multiworld.get_entrances(self.player)
def get_entrance(self, entrance):
return self.multiworld.get_entrance(entrance, self.player)
@@ -1265,25 +1383,12 @@ def is_major_item(self, item: OOTItem):
# Specifically ensures that only real items are gotten, not any events.
# In particular, ensures that Time Travel needs to be found.
def get_state_with_complete_itempool(self):
- all_state = self.multiworld.get_all_state(use_cache=False)
- # Remove event progression items
- for item, player in all_state.prog_items:
- if player == self.player and (item not in item_table or item_table[item][2] is None):
- all_state.prog_items[(item, player)] = 0
- # Remove all events and checked locations
- all_state.locations_checked = {loc for loc in all_state.locations_checked if loc.player != self.player}
- all_state.events = {loc for loc in all_state.events if loc.player != self.player}
+ all_state = CollectionState(self.multiworld)
+ for item in self.itempool + self.pre_fill_items:
+ self.multiworld.worlds[item.player].collect(all_state, item)
# If free_scarecrow give Scarecrow Song
if self.free_scarecrow:
all_state.collect(self.create_item("Scarecrow Song"), event=True)
-
- # Invalidate caches
- all_state.child_reachable_regions[self.player] = set()
- all_state.adult_reachable_regions[self.player] = set()
- all_state.child_blocked_connections[self.player] = set()
- all_state.adult_blocked_connections[self.player] = set()
- all_state.day_reachable_regions[self.player] = set()
- all_state.dampe_reachable_regions[self.player] = set()
all_state.stale[self.player] = True
return all_state
@@ -1320,7 +1425,6 @@ def gather_locations(multiworld: MultiWorld,
dungeon: str = ''
) -> Optional[List[OOTLocation]]:
type_to_setting = {
- 'Song': 'shuffle_song_items',
'Map': 'shuffle_mapcompass',
'Compass': 'shuffle_mapcompass',
'SmallKey': 'shuffle_smallkeys',
@@ -1339,21 +1443,12 @@ def gather_locations(multiworld: MultiWorld,
players = {players}
fill_opts = {p: getattr(multiworld.worlds[p], type_to_setting[item_type]) for p in players}
locations = []
- if item_type == 'Song':
- if any(map(lambda v: v == 'any', fill_opts.values())):
- return None
- for player, option in fill_opts.items():
- if option == 'song':
- condition = lambda location: location.type == 'Song'
- elif option == 'dungeon':
- condition = lambda location: location.name in dungeon_song_locations
- locations += filter(condition, multiworld.get_unfilled_locations(player=player))
- else:
- if any(map(lambda v: v in {'keysanity'}, fill_opts.values())):
- return None
- for player, option in fill_opts.items():
- condition = functools.partial(valid_dungeon_item_location,
- multiworld.worlds[player], option, dungeon)
- locations += filter(condition, multiworld.get_unfilled_locations(player=player))
+ if any(map(lambda v: v == 'keysanity', fill_opts.values())):
+ return None
+ for player, option in fill_opts.items():
+ condition = functools.partial(valid_dungeon_item_location,
+ multiworld.worlds[player], option, dungeon)
+ locations += filter(condition, multiworld.get_unfilled_locations(player=player))
return locations
+
diff --git a/worlds/oot/docs/MultiWorld-Room_oot.png b/worlds/oot/docs/MultiWorld-Room_oot.png
new file mode 100644
index 000000000000..f0f224e5e1af
Binary files /dev/null and b/worlds/oot/docs/MultiWorld-Room_oot.png differ
diff --git a/worlds/oot/docs/de_Ocarina of Time.md b/worlds/oot/docs/de_Ocarina of Time.md
new file mode 100644
index 000000000000..4d9fd2ea14bd
--- /dev/null
+++ b/worlds/oot/docs/de_Ocarina of Time.md
@@ -0,0 +1,41 @@
+# The Legend of Zelda: Ocarina of Time
+
+## Wo ist die Seite für die Einstellungen?
+
+Die [Seite für die Spielereinstellungen dieses Spiels](../player-options) enthält alle Optionen die man benötigt um
+eine YAML-Datei zu konfigurieren und zu exportieren.
+
+## Was macht der Randomizer in diesem Spiel?
+
+Items, welche der Spieler für gewöhnlich im Verlauf des Spiels erhalten würde, wurden umhergemischt. Die Logik bleit
+bestehen, damit ist das Spiel immer durchspielbar. Doch weil die Items durch das ganze Spiel gemischt wurden, müssen
+ manche Bereiche früher bescuht werden, als man es in Vanilla tun würde.
+Eine Liste von implementierter Logik, die unoffensichtlich erscheinen kann, kann
+[hier (Englisch)](https://wiki.ootrandomizer.com/index.php?title=Logic) gefunden werden.
+
+## Welche Items und Bereiche werden gemischt?
+
+Alle ausrüstbare und sammelbare Gegenstände, sowie Munition können gemischt werden. Und alle Bereiche, die einen
+dieser Items enthalten könnten, haben (sehr wahrscheinlich) ihren Inhalt verändert. Goldene Skulltulas können ebenfalls
+dazugezählt werden, je nach Wunsch des Spielers.
+
+## Welche Items können in sich in der Welt eines anderen Spielers befinden?
+
+Jedes dieser Items, die gemicht werden können, können in einer Multiworld auch in der Welt eines anderen Spielers
+fallen. Es ist jedoch möglich ausgewählte Items auf deine eigene Welt zu beschränken.
+
+## Wie sieht ein Item einer anderen Welt in OoT aus?
+
+Items, die zu einer anderen Welt gehören, werden repräsentiert durch Zelda's Brief.
+
+## Was passiert, wenn der Spieler ein Item erhält?
+
+Sobald der Spieler ein Item erhält, wird Link das Item über seinen Kopf halten und der ganzen Welt präsentieren.
+Gut für's Geschäft!
+
+## Einzigartige Lokale Befehle
+
+Die folgenden Befehle stehen nur im OoTClient, um mit Archipelago zu spielen, zur Verfügung:
+
+- `/n64` Überprüffe den Verbindungsstatus deiner N64
+- `/deathlink` Schalte den "Deathlink" des Clients um. Ãœberschreibt die zuvor konfigurierten Einstellungen.
diff --git a/worlds/oot/docs/en_Ocarina of Time.md b/worlds/oot/docs/en_Ocarina of Time.md
index b4610878b610..5a480d864124 100644
--- a/worlds/oot/docs/en_Ocarina of Time.md
+++ b/worlds/oot/docs/en_Ocarina of Time.md
@@ -1,8 +1,8 @@
# Ocarina of Time
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -31,3 +31,10 @@ Items belonging to other worlds are represented by the Zelda's Letter item.
When the player receives an item, Link will hold the item above his head and display it to the world. It's good for
business!
+
+## Unique Local Commands
+
+The following commands are only available when using the OoTClient to play with Archipelago.
+
+- `/n64` Check N64 Connection State
+- `/deathlink` Toggle deathlink from client. Overrides default option.
diff --git a/worlds/oot/docs/setup_de.md b/worlds/oot/docs/setup_de.md
new file mode 100644
index 000000000000..92c3150a7d2f
--- /dev/null
+++ b/worlds/oot/docs/setup_de.md
@@ -0,0 +1,108 @@
+# Setup Anleitung für Ocarina of Time: Archipelago Edition
+
+## WICHTIG
+
+Da wir BizHawk benutzen, gilt diese Anleitung nur für Windows und Linux.
+
+## Benötigte Software
+
+- BizHawk: [BizHawk Veröffentlichungen von TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
+ - Version 2.3.1 und später werden unterstützt. Version 2.9 ist empfohlen.
+ - Detailierte Installtionsanweisungen für BizHawk können über den obrigen Link gefunden werden.
+ - Windows-Benutzer müssen die Prerequisiten installiert haben. Diese können ebenfalls über
+ den obrigen Link gefunden werden.
+- Der integrierte Archipelago-Client, welcher [hier](https://github.com/ArchipelagoMW/Archipelago/releases) installiert
+ werden kann.
+- Eine `Ocarina of Time v1.0 US(?) ROM`. (Nicht aus Europa und keine Master-Quest oder Debug-Rom!)
+
+## Konfigurieren von BizHawk
+
+Sobald Bizhawk einmal installiert wurde, öffne **EmuHawk** und ändere die folgenen Einsteluungen:
+
+- (≤ 2.8) Gehe zu `Config > Customize`. Wechlse zu dem `Advanced`-Reiter, wechsle dann den `Lua Core` von "NLua+KopiLua" zu
+ `"Lua+LuaInterface"`. Starte danach EmuHawk neu. Dies ist zwingend notwendig, damit die Lua-Scripts, mit denen man sich mit dem Client verbindet, ordnungsgemäß funktionieren.
+ **ANMERKUNG: Selbst wenn "Lua+LuaInterface" bereits ausgewählt ist, wechsle zwischen den beiden Optionen umher und**
+ **wähle es erneut aus. Neue Installationen oder Versionen von EmuHawk neigen dazu "Lua+LuaInterface" als die**
+ **Standard-Option anzuzeigen, aber laden dennoch "NLua+KopiLua", bis dieser Schritt getan ist.**
+- Unter `Config > Customize > Advanced`, gehe sicher dass der Haken bei `AutoSaveRAM` ausgeählt ist, und klicke dann
+ den 5s-Knopf. Dies verringert die Wahrscheinlichkeit den Speicherfrotschritt zu verlieren, sollte der Emulator mal
+ abstürzen.
+- **(Optional)** Unter `Config > Customize` kannst du die Haken in den "Run in background"
+ (Laufe weiter im Hintergrund) und "Accept background input" (akzeptiere Tastendruck im Hintergrund) Kästchen setzen.
+ Dies erlaubt dir das Spiel im Hintergrund weiter zu spielen, selbst wenn ein anderes Fenster aktiv ist. (Nützlich bei
+ mehreren oder eher großen Bildschrimen/Monitoren.)
+- Unter `Config > Hotkeys` sind viele Hotkeys, die mit oft genuten Tasten belegt worden sind. Es wird empfohlen die
+ meisten (oder alle) Hotkeys zu deaktivieren. Dies kann schnell mit `Esc` erledigt werden.
+- Wird mit einem Kontroller gespielt, bei der Tastenbelegung (bei einem Laufendem Spiel, unter
+ `Config > Controllers...`), deaktiviere "P1 A Up", "P1 A Down", "P1 A Left", and "P1 A Right" und gehe stattdessen in
+ den Reiter `Analog Controls` um den Stick zu belegen, da sonst Probleme beim Zielen auftreten (mit dem Bogen oder
+ ähnliches). Y-Axis ist für Oben und Unten, und die X-Axis ist für Links und Rechts.
+- Unter `N64` setze einen Haken bei "Use Expansion Slot" (Benutze Erweiterungs-Slot). Dies wird benötigt damit
+ savestates/schnellspeichern funktioniert. (Das N64-Menü taucht nur **nach** dem laden einer N64-ROM auf.)
+
+Es wird sehr empfohlen N64 Rom-Erweiterungen (\*.n64, \*.z64) mit dem Emuhawk - welcher zuvor installiert wurde - zu
+verknüpfen.
+Um dies zu tun, muss eine beliebige N64 Rom aufgefunden werden, welche in deinem Besitz ist, diese Rechtsklicken und
+dann auf "Öffnen mit..." gehen. Gehe dann auf "Andere App auswählen" und suche nach deinen BizHawk-Ordner, in der
+sich der Emulator befindet, und wähle dann `EmuHawk.exe` **(NICHT "DiscoHawk.exe"!)** aus.
+
+Eine Alternative BizHawk Setup Anleitung (auf Englisch), sowie weitere Hilfe bei Problemen kann
+[hier](https://wiki.ootrandomizer.com/index.php?title=Bizhawk) gefunden werden.
+
+## Erstelle eine YAML-Datei
+
+### Was ist eine YAML-Datei und Warum brauch ich eine?
+
+Eine YAML-Datie enthält einen Satz an einstellbaren Optionen, die dem Generator mitteilen, wie
+dein Spiel generiert werden soll. In einer Multiworld stellt jeder Spieler eine eigene YAML-Datei zur Verfügung. Dies
+erlaubt jeden Spieler eine personalisierte Erfahrung nach derem Geschmack. Damit kann auch jeder Spieler in einer
+Multiworld (des gleichen Spiels) völlig unterschiedliche Einstellungen haben.
+
+Für weitere Informationen, besuche die allgemeine Anleitung zum Erstellen einer
+YAML-Datei: [Archipelago Setup Anleitung](/tutorial/Archipelago/setup/en)
+
+### Woher bekomme ich eine YAML-Datei?
+
+Die Seite für die Spielereinstellungen auf dieser Website erlaubt es dir deine persönlichen Einstellungen nach
+vorlieben zu konfigurieren und eine YAML-Datei zu exportieren.
+Seite für die Spielereinstellungen:
+[Seite für die Spielereinstellungen von Ocarina of Time](/games/Ocarina%20of%20Time/player-options)
+
+### Überprüfen deiner YAML-Datei
+
+Wenn du deine YAML-Datei überprüfen möchtest, um sicher zu gehen, dass sie funktioniert, kannst du dies auf der
+YAML-Überprüfungsseite tun.
+YAML-Überprüfungsseite: [YAML-Überprüfungsseite](/check)
+
+## Beitreten einer Multiworld
+
+### Erhalte deinen OoT-Patch
+
+(Der folgende Prozess ist bei den meisten ROM-basierenden Spielen sehr ähnlich.)
+
+Wenn du einer Multiworld beitrittst, wirst du gefordert eine YAML-Datei bei dem Host abzugeben. Ist dies getan,
+erhälst du (in der Regel) einen Link vom Host der Multiworld. Dieser führt dich zu einem Raum, in dem alle
+teilnehmenden Spieler (bzw. Welten) aufgelistet sind. Du solltest dich dann auf **deine** Welt konzentrieren
+und klicke dann auf `Download APZ5 File...`.
+![Screenshot of a Multiworld Room with an Ocarina of Time Player](/static/generated/docs/Ocarina%20of%20Time/MultiWorld-room_oot.png)
+
+Führe die `.apz5`-Datei mit einem Doppelklick aus, um deinen Ocarina Of Time-Client zu starten, sowie das patchen
+deiner ROM. Ist dieser Prozess fertig (kann etwas dauern), startet sich der Client und der Emulator automatisch
+(sofern das "Öffnen mit..." ausgewählt wurde).
+
+### Verbinde zum Multiserver
+
+Sind einmal der Client und der Emulator gestartet, müssen sie nur noch miteinander verbunden werden. Gehe dazu in
+deinen Archipelago-Ordner, dann zu `data/lua`, und füge das `connector_oot.lua` Script per Drag&Drop (ziehen und
+fallen lassen) auf das EmuHawk-Fenster. (Alternativ kannst du die Lua-Konsole manuell öffnen, gehe dazu auf
+`Script > Open Script` und durchsuche die Ordner nach `data/lua/connector_oot.lua`.)
+
+Um den Client mit dem Multiserver zu verbinden, füge einfach `:` in das Textfeld ganz oben im
+Client ein und drücke Enter oder "Connect" (verbinden). Wird ein Passwort benötigt, musst du es danach unten in das
+Textfeld (für den Chat und Befehle) eingeben.
+Alternativ kannst du auch in dem unterem Textfeld den folgenden Befehl schreiben:
+`/connect : [Passwort]` (wie die Adresse und der Port lautet steht in dem Raum, oder wird von deinem
+Host an dich weitergegeben.)
+Beispiel: `/connect archipelago.gg:12345 Passw123`
+
+Du bist nun bereit für dein Zeitreise-Abenteuer in Hyrule.
diff --git a/worlds/oot/docs/setup_en.md b/worlds/oot/docs/setup_en.md
index 612c5efd8f99..553f1820c3ea 100644
--- a/worlds/oot/docs/setup_en.md
+++ b/worlds/oot/docs/setup_en.md
@@ -10,8 +10,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
- 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)
- (select `Ocarina of Time Client` during installation).
+- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
- An Ocarina of Time v1.0 ROM.
## Configuring BizHawk
@@ -42,360 +41,22 @@ and select EmuHawk.exe.
An alternative BizHawk setup guide as well as various pieces of troubleshooting advice can be found
[here](https://wiki.ootrandomizer.com/index.php?title=Bizhawk).
-## Configuring your YAML file
+## Create a Config (.yaml) File
-### What is a YAML file and why do I need one?
+### What is a config file and why do I need one?
-Your YAML file contains a set of configuration options which provide the generator with information about how it should
-generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
-an experience customized for their taste, and different players in the same multiworld can all have different options.
+See the guide on setting up a basic YAML at the Archipelago setup
+guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
-### Where do I get a YAML file?
+### Where do I get a config file?
-A basic OoT yaml will look like this. There are lots of cosmetic options that have been removed for the sake of this
-tutorial, if you want to see a complete list, download Archipelago from
-the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) and look for the sample file in
-the "Players" folder.
+The Player Options page on the website allows you to configure your personal options and export a config file from
+them. Player options page: [Ocarina of Time Player Options Page](/games/Ocarina%20of%20Time/player-options)
-```yaml
-description: Default Ocarina of Time Template # Used to describe your yaml. Useful if you have multiple files
-# Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
-name: YourName
-game:
- Ocarina of Time: 1
-requires:
- version: 0.1.7 # Version of Archipelago required for this yaml to work as expected.
-# Shared Options supported by all games:
-accessibility:
- items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
- locations: 50 # Guarantees you will be able to access all locations, and therefore all items
- none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items
-progression_balancing: # A system to reduce BK, as in times during which you can't do anything, by moving your items into an earlier access sphere
- 0: 0 # Choose a lower number if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
- 25: 0
- 50: 50 # Make it likely you have stuff to do.
- 99: 0 # Get important items early, and stay at the front of the progression.
-Ocarina of Time:
- logic_rules: # Set the logic used for the generator.
- glitchless: 50
- glitched: 0
- no_logic: 0
- logic_no_night_tokens_without_suns_song: # Nighttime skulltulas will logically require Sun's Song.
- false: 50
- true: 0
- open_forest: # Set the state of Kokiri Forest and the path to Deku Tree.
- open: 50
- closed_deku: 0
- closed: 0
- open_kakariko: # Set the state of the Kakariko Village gate.
- open: 50
- zelda: 0
- closed: 0
- open_door_of_time: # Open the Door of Time by default, without the Song of Time.
- false: 0
- true: 50
- zora_fountain: # Set the state of King Zora, blocking the way to Zora's Fountain.
- open: 0
- adult: 0
- closed: 50
- gerudo_fortress: # Set the requirements for access to Gerudo Fortress.
- normal: 0
- fast: 50
- open: 0
- bridge: # Set the requirements for the Rainbow Bridge.
- open: 0
- vanilla: 0
- stones: 0
- medallions: 50
- dungeons: 0
- tokens: 0
- trials: # Set the number of required trials in Ganon's Castle.
- # you can add additional values between minimum and maximum
- 0: 50 # minimum value
- 6: 0 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- starting_age: # Choose which age Link will start as.
- child: 50
- adult: 0
- triforce_hunt: # Gather pieces of the Triforce scattered around the world to complete the game.
- false: 50
- true: 0
- triforce_goal: # Number of Triforce pieces required to complete the game. Total number placed determined by the Item Pool setting.
- # you can add additional values between minimum and maximum
- 1: 0 # minimum value
- 50: 0 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- 20: 50
- bombchus_in_logic: # Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell refills; bombchus open Bombchu Bowling.
- false: 50
- true: 0
- bridge_stones: # Set the number of Spiritual Stones required for the rainbow bridge.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 3: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- bridge_medallions: # Set the number of medallions required for the rainbow bridge.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 6: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- bridge_rewards: # Set the number of dungeon rewards required for the rainbow bridge.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 9: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- bridge_tokens: # Set the number of Gold Skulltula Tokens required for the rainbow bridge.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 100: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- shuffle_mapcompass: # Control where to shuffle dungeon maps and compasses.
- remove: 0
- startwith: 50
- vanilla: 0
- dungeon: 0
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- shuffle_smallkeys: # Control where to shuffle dungeon small keys.
- remove: 0
- vanilla: 0
- dungeon: 50
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- shuffle_hideoutkeys: # Control where to shuffle the Gerudo Fortress small keys.
- vanilla: 50
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- shuffle_bosskeys: # Control where to shuffle boss keys, except the Ganon's Castle Boss Key.
- remove: 0
- vanilla: 0
- dungeon: 50
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- shuffle_ganon_bosskey: # Control where to shuffle the Ganon's Castle Boss Key.
- remove: 50
- vanilla: 0
- dungeon: 0
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- on_lacs: 0
- enhance_map_compass: # Map tells if a dungeon is vanilla or MQ. Compass tells what the dungeon reward is.
- false: 50
- true: 0
- lacs_condition: # Set the requirements for the Light Arrow Cutscene in the Temple of Time.
- vanilla: 50
- stones: 0
- medallions: 0
- dungeons: 0
- tokens: 0
- lacs_stones: # Set the number of Spiritual Stones required for LACS.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 3: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- lacs_medallions: # Set the number of medallions required for LACS.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 6: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- lacs_rewards: # Set the number of dungeon rewards required for LACS.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 9: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- lacs_tokens: # Set the number of Gold Skulltula Tokens required for LACS.
- # you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 100: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- shuffle_song_items: # Set where songs can appear.
- song: 50
- dungeon: 0
- any: 0
- shopsanity: # Randomizes shop contents. Set to "off" to not shuffle shops; "0" shuffles shops but does not allow multiworld items in shops.
- 0: 0
- 1: 0
- 2: 0
- 3: 0
- 4: 0
- random_value: 0
- off: 50
- tokensanity: # Token rewards from Gold Skulltulas are shuffled into the pool.
- off: 50
- dungeons: 0
- overworld: 0
- all: 0
- shuffle_scrubs: # Shuffle the items sold by Business Scrubs, and set the prices.
- off: 50
- low: 0
- regular: 0
- random_prices: 0
- shuffle_cows: # Cows give items when Epona's Song is played.
- false: 50
- true: 0
- shuffle_kokiri_sword: # Shuffle Kokiri Sword into the item pool.
- false: 50
- true: 0
- shuffle_ocarinas: # Shuffle the Fairy Ocarina and Ocarina of Time into the item pool.
- false: 50
- true: 0
- shuffle_weird_egg: # Shuffle the Weird Egg from Malon at Hyrule Castle.
- false: 50
- true: 0
- shuffle_gerudo_card: # Shuffle the Gerudo Membership Card into the item pool.
- false: 50
- true: 0
- shuffle_beans: # Adds a pack of 10 beans to the item pool and changes the bean salesman to sell one item for 60 rupees.
- false: 50
- true: 0
- shuffle_medigoron_carpet_salesman: # Shuffle the items sold by Medigoron and the Haunted Wasteland Carpet Salesman.
- false: 50
- true: 0
- skip_child_zelda: # Game starts with Zelda's Letter, the item at Zelda's Lullaby, and the relevant events already completed.
- false: 50
- true: 0
- no_escape_sequence: # Skips the tower collapse sequence between the Ganondorf and Ganon fights.
- false: 0
- true: 50
- no_guard_stealth: # The crawlspace into Hyrule Castle skips straight to Zelda.
- false: 0
- true: 50
- no_epona_race: # Epona can always be summoned with Epona's Song.
- false: 0
- true: 50
- skip_some_minigame_phases: # Dampe Race and Horseback Archery give both rewards if the second condition is met on the first attempt.
- false: 0
- true: 50
- complete_mask_quest: # All masks are immediately available to borrow from the Happy Mask Shop.
- false: 50
- true: 0
- useful_cutscenes: # Reenables the Poe cutscene in Forest Temple, Darunia in Fire Temple, and Twinrova introduction. Mostly useful for glitched.
- false: 50
- true: 0
- fast_chests: # All chest animations are fast. If disabled, major items have a slow animation.
- false: 0
- true: 50
- free_scarecrow: # Pulling out the ocarina near a scarecrow spot spawns Pierre without needing the song.
- false: 50
- true: 0
- fast_bunny_hood: # Bunny Hood lets you move 1.5x faster like in Majora's Mask.
- false: 50
- true: 0
- chicken_count: # Controls the number of Cuccos for Anju to give an item as child.
- \# you can add additional values between minimum and maximum
- 0: 0 # minimum value
- 7: 50 # maximum value
- random: 0
- random-low: 0
- random-high: 0
- hints: # Gossip Stones can give hints about item locations.
- none: 0
- mask: 0
- agony: 0
- always: 50
- hint_dist: # Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.
- balanced: 50
- ddr: 0
- league: 0
- mw2: 0
- scrubs: 0
- strong: 0
- tournament: 0
- useless: 0
- very_strong: 0
- text_shuffle: # Randomizes text in the game for comedic effect.
- none: 50
- except_hints: 0
- complete: 0
- damage_multiplier: # Controls the amount of damage Link takes.
- half: 0
- normal: 50
- double: 0
- quadruple: 0
- ohko: 0
- no_collectible_hearts: # Hearts will not drop from enemies or objects.
- false: 50
- true: 0
- starting_tod: # Change the starting time of day.
- default: 50
- sunrise: 0
- morning: 0
- noon: 0
- afternoon: 0
- sunset: 0
- evening: 0
- midnight: 0
- witching_hour: 0
- start_with_consumables: # Start the game with full Deku Sticks and Deku Nuts.
- false: 50
- true: 0
- start_with_rupees: # Start with a full wallet. Wallet upgrades will also fill your wallet.
- false: 50
- true: 0
- item_pool_value: # Changes the number of items available in the game.
- plentiful: 0
- balanced: 50
- scarce: 0
- minimal: 0
- junk_ice_traps: # Adds ice traps to the item pool.
- off: 0
- normal: 50
- on: 0
- mayhem: 0
- onslaught: 0
- ice_trap_appearance: # Changes the appearance of ice traps as freestanding items.
- major_only: 50
- junk_only: 0
- anything: 0
- logic_earliest_adult_trade: # Earliest item that can appear in the adult trade sequence.
- pocket_egg: 0
- pocket_cucco: 0
- cojiro: 0
- odd_mushroom: 0
- poachers_saw: 0
- broken_sword: 0
- prescription: 50
- eyeball_frog: 0
- eyedrops: 0
- claim_check: 0
- logic_latest_adult_trade: # Latest item that can appear in the adult trade sequence.
- pocket_egg: 0
- pocket_cucco: 0
- cojiro: 0
- odd_mushroom: 0
- poachers_saw: 0
- broken_sword: 0
- prescription: 0
- eyeball_frog: 0
- eyedrops: 0
- claim_check: 50
+### 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)
## Joining a MultiWorld Game
diff --git a/worlds/oot/docs/setup_fr.md b/worlds/oot/docs/setup_fr.md
index 57099cdf2e01..40b0e8f571df 100644
--- a/worlds/oot/docs/setup_fr.md
+++ b/worlds/oot/docs/setup_fr.md
@@ -1,422 +1,70 @@
-# Guide d'installation Archipelago pour Ocarina of Time
+# Guide de configuration pour Ocarina of Time Archipelago
## Important
-Comme nous utilisons BizHawk, ce guide ne s'applique qu'aux systèmes Windows et Linux.
+Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windows et Linux.
## Logiciel requis
-- BizHawk : [BizHawk sort de TASVideos] (https://tasvideos.org/BizHawk/ReleaseHistory)
- - Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour la stabilité.
+- BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
+ - Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour des raisons de stabilité.
- Des instructions d'installation détaillées pour BizHawk peuvent être trouvées sur le lien ci-dessus.
- - Les utilisateurs Windows doivent d'abord exécuter le programme d'installation prereq, qui peut également être trouvé sur le lien ci-dessus.
+ - Les utilisateurs Windows doivent d'abord exécuter le programme d'installation des prérequis, qui peut également être trouvé sur le lien ci-dessus.
- Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases)
- (sélectionnez `Ocarina of Time Client` lors de l'installation).
+ (sélectionnez « Ocarina of Time Client » lors de l'installation).
- Une ROM Ocarina of Time v1.0.
## Configuration de BizHawk
-Une fois BizHawk installé, ouvrez BizHawk et modifiez les paramètres suivants :
-
-- Allez dans Config > Personnaliser. Basculez vers l'onglet Avancé, puis basculez le Lua Core de "NLua+KopiLua" vers
- "Interface Lua+Lua". Redémarrez ensuite BizHawk. Ceci est nécessaire pour que le script Lua fonctionne correctement.
- **REMARQUE : Même si "Lua+LuaInterface" est déjà sélectionné, basculez entre les deux options et resélectionnez-le. Nouvelles installations**
- ** des versions plus récentes de BizHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais se chargent toujours **
- **"NLua+KopiLua" jusqu'à ce que cette étape soit terminée.**
-- Sous Config > Personnaliser > Avancé, assurez-vous que la case pour AutoSaveRAM est cochée et cliquez sur le bouton 5s.
- Cela réduit la possibilité de perdre des données de sauvegarde en cas de plantage de l'émulateur.
-- Sous Config > Personnaliser, cochez les cases "Exécuter en arrière-plan" et "Accepter la saisie en arrière-plan". Cela vous permettra de
- continuer à jouer en arrière-plan, même si une autre fenêtre est sélectionnée.
-- Sous Config> Raccourcis clavier, de nombreux raccourcis clavier sont répertoriés, dont beaucoup sont liés aux touches communes du clavier. Vous voudrez probablement
- désactiver la plupart d'entre eux, ce que vous pouvez faire rapidement en utilisant `Esc`.
-- Si vous jouez avec une manette, lorsque vous liez les commandes, désactivez "P1 A Up", "P1 A Down", "P1 A Left" et "P1 A Right"
- car ceux-ci interfèrent avec la visée s'ils sont liés. Définissez l'entrée directionnelle à l'aide de l'onglet Analogique à la place.
-- Sous N64, activez "Utiliser l'emplacement d'extension". Ceci est nécessaire pour que les sauvegardes fonctionnent.
+Une fois BizHawk installé, ouvrez EmuHawk et modifiez les paramètres suivants :
+
+- (≤ 2,8) Allez dans Config > Personnaliser. Passez à l'onglet Avancé, puis faites passer le Lua Core de "NLua+KopiLua" Ã
+ "Lua+LuaInterface". Puis redémarrez EmuHawk. Ceci est nécessaire pour que le script Lua fonctionne correctement.
+ **REMARQUE : Même si « Lua+LuaInterface » est déjà sélectionné, basculez entre les deux options et resélectionnez-la. Nouvelles installations**
+ **des versions plus récentes d'EmuHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais ce pendant refait l'épate juste au dessus par précautions**
+- Sous Config > Personnaliser > Avancé, assurez-vous que la case AutoSaveRAM est cochée et cliquez sur le bouton 5s.
+ Cela réduit la possibilité de perdre des données de sauvegarde en cas de crash de l'émulateur.
+- Sous Config > Personnaliser, cochez les cases « Exécuter en arrière-plan » et « Accepter la saisie en arrière-plan ». Cela vous permettra continuez à jouer en arrière-plan, même si une autre fenêtre est sélectionnée.
+- Sous Config > Hotkeys, de nombreux raccourcis clavier sont répertoriés, dont beaucoup sont liés aux touches communes du clavier. Vous voudrez probablement pour désactiver la plupart d'entre eux, ce que vous pouvez faire rapidement en utilisant « Esc ».
+- Si vous jouez avec une manette, lorsque vous associez des commandes, désactivez "P1 A Up", "P1 A Down", "P1 A Left" et "P1 A Right".
+ car ceux-ci interfèrent avec la visée s’ils sont liés. Définissez plutôt l'entrée directionnelle à l'aide de l'onglet Analogique.
+- Sous N64, activez "Utiliser le connecteur d'extension". Ceci est nécessaire pour que les états de sauvegarde fonctionnent.
(Le menu N64 n'apparaît qu'après le chargement d'une ROM.)
-Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) au BizHawk que nous venons d'installer.
-Pour ce faire, nous devons simplement rechercher n'importe quelle rom N64 que nous possédons, faire un clic droit et sélectionner "Ouvrir avec ...", dépliez
-la liste qui apparaît et sélectionnez l'option du bas "Rechercher une autre application", puis naviguez jusqu'au dossier BizHawk
-et sélectionnez EmuHawk.exe.
+Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) à l'EmuHawk que nous venons d'installer.
+Pour ce faire, vous devez simplement rechercher n'importe quelle rom N64 que vous possédez, faire un clic droit et sélectionner "Ouvrir avec...", déplier la liste qui apparaît et sélectionnez l'option du bas "Rechercher une autre application", puis accédez au dossier BizHawk et sélectionnez EmuHawk.exe.
-Un guide de configuration BizHawk alternatif ainsi que divers conseils de dépannage peuvent être trouvés
+Un guide de configuration BizHawk alternatif ainsi que divers conseils de dépannage sont disponibles
[ici](https://wiki.ootrandomizer.com/index.php?title=Bizhawk).
-## Configuration de votre fichier YAML
+## Créer un fichier de configuration (.yaml)
-### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ?
+### Qu'est-ce qu'un fichier de configuration et pourquoi en ai-je besoin ?
-Votre fichier YAML contient un ensemble d'options de configuration qui fournissent au générateur des informations sur la façon dont il doit
-générer votre jeu. Chaque joueur d'un multimonde fournira son propre fichier YAML. Cette configuration permet à chaque joueur de profiter
-d'une expérience personnalisée à leur goût, et différents joueurs dans le même multimonde peuvent tous avoir des options différentes.
+Consultez le guide sur la configuration d'un YAML de base lors de la configuration de l'archipel.
+guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/setup/en)
-### Où puis-je obtenir un fichier YAML ?
+### Où puis-je obtenir un fichier de configuration (.yaml) ?
-Un yaml OoT de base ressemblera à ceci. Il y a beaucoup d'options cosmétiques qui ont été supprimées pour le plaisir de ce
-tutoriel, si vous voulez voir une liste complète, téléchargez Archipelago depuis
-la [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) et recherchez l'exemple de fichier dans
-le dossier "Lecteurs".
+La page Paramètres du lecteur sur le site Web vous permet de configurer vos paramètres personnels et d'exporter un fichier de configuration depuis eux. Page des paramètres du joueur : [Page des paramètres du joueur d'Ocarina of Time](/games/Ocarina%20of%20Time/player-options)
-``` yaml
-description: Modèle par défaut d'Ocarina of Time # Utilisé pour décrire votre yaml. Utile si vous avez plusieurs fichiers
-# Votre nom dans le jeu. Les espaces seront remplacés par des underscores et il y a une limite de 16 caractères
-name: VotreNom
-game:
- Ocarina of Time: 1
-requires:
- version: 0.1.7 # Version d'Archipelago requise pour que ce yaml fonctionne comme prévu.
-# Options partagées prises en charge par tous les jeux :
-accessibility:
- items: 0 # Garantit que vous pourrez acquérir tous les articles, mais vous ne pourrez peut-être pas accéder à tous les emplacements
- locations: 50 # Garantit que vous pourrez accéder à tous les emplacements, et donc à tous les articles
- none: 0 # Garantit seulement que le jeu est battable. Vous ne pourrez peut-être pas accéder à tous les emplacements ou acquérir tous les objets
-progression_balancing: # Un système pour réduire le BK, comme dans les périodes où vous ne pouvez rien faire, en déplaçant vos éléments dans une sphère d'accès antérieure
- 0: 0 # Choisissez un nombre inférieur si cela ne vous dérange pas d'avoir un multimonde plus long, ou si vous pouvez glitch / faire du hors logique.
- 25: 0
- 50: 50 # Faites en sorte que vous ayez probablement des choses à faire.
- 99: 0 # Obtenez les éléments importants tôt et restez en tête de la progression.
-Ocarina of Time:
- logic_rules: # définit la logique utilisée pour le générateur.
- glitchless: 50
- glitched: 0
- no_logic: 0
- logic_no_night_tokens_without_suns_song: # Les skulltulas nocturnes nécessiteront logiquement le Chant du soleil.
- false: 50
- true: 0
- open_forest: # Définissez l'état de la forêt de Kokiri et du chemin vers l'arbre Mojo.
- open: 50
- closed_deku: 0
- closed: 0
- open_kakariko: # Définit l'état de la porte du village de Kakariko.
- open: 50
- zelda: 0
- closed: 0
- open_door_of_time: # Ouvre la Porte du Temps par défaut, sans le Chant du Temps.
- false: 0
- true: 50
- zora_fountain: # Définit l'état du roi Zora, bloquant le chemin vers la fontaine de Zora.
- open: 0
- adult: 0
- closed: 50
- gerudo_fortress: # Définit les conditions d'accès à la forteresse Gerudo.
- normal: 0
- fast: 50
- open: 0
- bridge: # Définit les exigences pour le pont arc-en-ciel.
- open: 0
- vanilla: 0
- stones: 0
- medallions: 50
- dungeons: 0
- tokens: 0
- trials: # Définit le nombre d'épreuves requises dans le Château de Ganon.
- # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
- 0: 50 # valeur minimale
- 6: 0 # valeur maximale
- random: 0
- random-low: 0
- random-higt: 0
- starting_age: # Choisissez l'âge auquel Link commencera.
- child: 50
- adult: 0
- triforce_hunt: # Rassemblez des morceaux de la Triforce dispersés dans le monde entier pour terminer le jeu.
- false: 50
- true: 0
- triforce_goal: # Nombre de pièces Triforce nécessaires pour terminer le jeu. Nombre total placé déterminé par le paramètre Item Pool.
- # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
- 1: 0 # valeur minimale
- 50: 0 # valeur maximale
- random: 0
- random-low: 0
- random-higt: 0
- 20: 50
- bombchus_in_logic: # Les Bombchus sont correctement pris en compte dans la logique. Le premier pack trouvé aura 20 chus ; Kokiri Shop et Bazaar vendent des recharges ; bombchus ouvre Bombchu Bowling.
- false: 50
- true: 0
- bridge_stones: # Définissez le nombre de pierres spirituelles requises pour le pont arc-en-ciel.
- # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
- 0: 0 # valeur minimale
- 3: 50 # valeur maximale
- random: 0
- random-low: 0
- random-high: 0
- bridge_medallions: # Définissez le nombre de médaillons requis pour le pont arc-en-ciel.
- # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
- 0: 0 # valeur minimale
- 6: 50 # valeur maximale
- random: 0
- random-low: 0
- random-high: 0
- bridge_rewards: # Définissez le nombre de récompenses de donjon requises pour le pont arc-en-ciel.
- # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
- 0: 0 # valeur minimale
- 9: 50 # valeur maximale
- random: 0
- random-low: 0
- random-high: 0
- bridge_tokens: # Définissez le nombre de jetons Gold Skulltula requis pour le pont arc-en-ciel.
- # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
- 0: 0 # valeur minimale
- 100: 50 # valeur maximale
- random: 0
- random-low: 0
- random-high: 0
- shuffle_mapcompass: # Contrôle où mélanger les cartes et boussoles des donjons.
- remove: 0
- startwith: 50
- vanilla: 0
- dungeon: 0
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- shuffle_smallkeys: # Contrôle où mélanger les petites clés de donjon.
- remove: 0
- vanilla: 0
- dungeon: 50
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- shuffle_hideoutkeys: # Contrôle où mélanger les petites clés de la Forteresse Gerudo.
- vanilla: 50
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- shuffle_bosskeys: # Contrôle où mélanger les clés du boss, à l'exception de la clé du boss du château de Ganon.
- remove: 0
- vanilla: 0
- dungeon: 50
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- shuffle_ganon_bosskey: # Contrôle où mélanger la clé du patron du château de Ganon.
- remove: 50
- vanilla: 0
- dungeon: 0
- overworld: 0
- any_dungeon: 0
- keysanity: 0
- on_lacs: 0
- enhance_map_compass: # La carte indique si un donjon est vanille ou MQ. La boussole indique quelle est la récompense du donjon.
- false: 50
- true: 0
- lacs_condition: # Définissez les exigences pour la cinématique de la Flèche lumineuse dans le Temple du temps.
- vanilla: 50
- stones: 0
- medallions: 0
- dungeons: 0
- tokens: 0
- lacs_stones: # Définissez le nombre de pierres spirituelles requises pour le LACS.
- # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
- 0: 0 # valeur minimale
- 3: 50 # valeur maximale
- random: 0
- random-low: 0
- random-high: 0
- lacs_medallions: # Définissez le nombre de médaillons requis pour LACS.
- # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
- 0: 0 # valeur minimale
- 6: 50 # valeur maximale
- random: 0
- random-low: 0
- random-high: 0
- lacs_rewards: # Définissez le nombre de récompenses de donjon requises pour LACS.
- # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
- 0: 0 # valeur minimale
- 9: 50 # valeur maximale
- random: 0
- random-low: 0
- random-high: 0
- lacs_tokens: # Définissez le nombre de jetons Gold Skulltula requis pour le LACS.
- # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum
- 0: 0 # valeur minimale
- 100: 50 # valeur maximale
- random: 0
- random-low: 0
- random-high: 0
- shuffle_song_items: # Définit où les chansons peuvent apparaître.
- song: 50
- dungeon: 0
- any: 0
- shopsanity: # Randomise le contenu de la boutique. Réglez sur "off" pour ne pas mélanger les magasins ; "0" mélange les magasins mais ne n'autorise pas les articles multimonde dans les magasins.
- 0: 0
- 1: 0
- 2: 0
- 3: 0
- 4: 0
- random_value: 0
- off: 50
- tokensanity : # les récompenses en jetons des Skulltulas dorées sont mélangées dans la réserve.
- off: 50
- dungeons: 0
- overworld: 0
- all: 0
- shuffle_scrubs: # Mélangez les articles vendus par Business Scrubs et fixez les prix.
- off: 50
- low: 0
- regular: 0
- random_prices: 0
- shuffle_cows: # les vaches donnent des objets lorsque la chanson d'Epona est jouée.
- false: 50
- true: 0
- shuffle_kokiri_sword: # Mélangez l'épée Kokiri dans la réserve d'objets.
- false: 50
- true: 0
- shuffle_ocarinas: # Mélangez l'Ocarina des fées et l'Ocarina du temps dans la réserve d'objets.
- false: 50
- true: 0
- shuffle_weird_egg: # Mélangez l'œuf bizarre de Malon au château d'Hyrule.
- false: 50
- true: 0
- shuffle_gerudo_card: # Mélangez la carte de membre Gerudo dans la réserve d'objets.
- false: 50
- true: 0
- shuffle_beans: # Ajoute un paquet de 10 haricots au pool d'objets et change le vendeur de haricots pour qu'il vende un objet pour 60 roupies.
- false: 50
- true: 0
- shuffle_medigoron_carpet_salesman: # Mélangez les objets vendus par Medigoron et le vendeur de tapis Haunted Wasteland.
- false: 50
- true: 0
- skip_child_zelda: # le jeu commence avec la lettre de Zelda, l'objet de la berceuse de Zelda et les événements pertinents déjà terminés.
- false: 50
- true: 0
- no_escape_sequence: # Ignore la séquence d'effondrement de la tour entre les combats de Ganondorf et de Ganon.
- false: 50
- true: 0
- no_guard_stealth: # Le vide sanitaire du château d'Hyrule passe directement à Zelda.
- false: 50
- true: 0
- no_epona_race: # Epona peut toujours être invoquée avec Epona's Song.
- false: 50
- true: 0
- skip_some_minigame_phases: # Dampe Race et Horseback Archery donnent les deux récompenses si la deuxième condition est remplie lors de la première tentative.
- false: 50
- true: 0
- complete_mask_quest: # Tous les masques sont immédiatement disponibles à l'emprunt dans la boutique Happy Mask.
- false: 50
- true: 0
- useful_cutscenes: # Réactive la cinématique Poe dans le Temple de la forêt, Darunia dans le Temple du feu et l'introduction de Twinrova. Surtout utile pour les pépins.
- false: 50
- true: 0
- fast_chests: # Toutes les animations des coffres sont rapides. Si désactivé, les éléments principaux ont une animation lente.
- false: 50
- true: 0
- free_scarecrow: # Sortir l'ocarina près d'un point d'épouvantail fait apparaître Pierre sans avoir besoin de la chanson.
- false: 50
- true: 0
- fast_bunny_hood: # Bunny Hood vous permet de vous déplacer 1,5 fois plus vite comme dans Majora's Mask.
- false: 50
- true: 0
- chicken_count: # Contrôle le nombre de Cuccos pour qu'Anju donne un objet en tant qu'enfant.
- \# vous pouvez ajouter des valeurs supplémentaires entre le minimum et le maximum
- 0: 0 # valeur minimale
- 7: 50 # valeur maximale
- random: 0
- random-low: 0
- random-high: 0
- hints: # les pierres à potins peuvent donner des indices sur l'emplacement des objets.
- none: 0
- mask: 0
- agony: 0
- always: 50
- hint_dist: # Choisissez la distribution d'astuces à utiliser. Affecte la fréquence des indices forts, quels éléments sont toujours indiqués, etc.
- balanced: 50
- ddr: 0
- league: 0
- mw2: 0
- scrubs: 0
- strong: 0
- tournament: 0
- useless: 0
- very_strong: 0
- text_shuffle: # Randomise le texte dans le jeu pour un effet comique.
- none: 50
- except_hints: 0
- complete: 0
- damage_multiplier: # contrôle la quantité de dégâts subis par Link.
- half: 0
- normal: 50
- double: 0
- quadruple: 0
- ohko: 0
- no_collectible_hearts: # les cœurs ne tomberont pas des ennemis ou des objets.
- false: 50
- true: 0
- starting_tod: # Changer l'heure de début de la journée.
- default: 50
- sunrise: 0
- morning: 0
- noon: 0
- afternoon: 0
- sunset: 0
- evening: 0
- midnight: 0
- witching_hour: 0
- start_with_consumables: # Démarrez le jeu avec des Deku Sticks et des Deku Nuts pleins.
- false: 50
- true: 0
- start_with_rupees: # Commencez avec un portefeuille plein. Les mises à niveau de portefeuille rempliront également votre portefeuille.
- false: 50
- true: 0
- item_pool_value: # modifie le nombre d'objets disponibles dans le jeu.
- plentiful: 0
- balanced: 50
- scarce: 0
- minimal: 0
- junk_ice_traps: # Ajoute des pièges à glace au pool d'objets.
- off: 0
- normal: 50
- on: 0
- mayhem: 0
- onslaught: 0
- ice_trap_appearance: # modifie l'apparence des pièges à glace en tant qu'éléments autonomes.
- major_only: 50
- junk_only: 0
- anything: 0
- logic_earliest_adult_trade: # premier élément pouvant apparaître dans la séquence d'échange pour adultes.
- pocket_egg: 0
- pocket_cucco: 0
- cojiro: 0
- odd_mushroom: 0
- poachers_saw: 0
- broken_sword: 0
- prescription: 50
- eyeball_frog: 0
- eyedrops: 0
- claim_check: 0
- logic_latest_adult_trade: # Dernier élément pouvant apparaître dans la séquence d'échange pour adultes.
- pocket_egg: 0
- pocket_cucco: 0
- cojiro: 0
- odd_mushroom: 0
- poachers_saw: 0
- broken_sword: 0
- prescription: 0
- eyeball_frog: 0
- eyedrops: 0
- claim_check: 50
+### Vérification de votre fichier de configuration
-```
+Si vous souhaitez valider votre fichier de configuration pour vous assurer qu'il fonctionne, vous pouvez le faire sur la page YAML Validator.
+YAML page du validateur : [page de validation YAML](/mysterycheck)
-## Rejoindre une partie MultiWorld
+## Rejoindre un jeu multimonde
-### Obtenez votre fichier de correctif OOT
+### Obtenez votre fichier OOT modifié
-Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à l'hébergeur. Une fois que c'est Fini,
-l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun
-des dossiers. Votre fichier de données doit avoir une extension `.apz5`.
+Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à celui qui l'héberge. Une fois cela fait, l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun des dossiers. Votre fichier de données doit avoir une extension « .apz5 ».
-Double-cliquez sur votre fichier `.apz5` pour démarrer votre client et démarrer le processus de patch ROM. Une fois le processus terminé
-(cela peut prendre un certain temps), le client et l'émulateur seront lancés automatiquement (si vous avez associé l'extension
-à l'émulateur comme recommandé).
+Double-cliquez sur votre fichier « .apz5 » pour démarrer votre client et démarrer le processus de correctif ROM. Une fois le processus terminé (cela peut prendre un certain temps), le client et l'émulateur seront automatiquement démarrés (si vous avez associé l'extension à l'émulateur comme recommandé).
### Connectez-vous au multiserveur
-Une fois le client et l'émulateur démarrés, vous devez les connecter. Dans l'émulateur, cliquez sur "Outils"
-menu et sélectionnez "Console Lua". Cliquez sur le bouton du dossier ou appuyez sur Ctrl+O pour ouvrir un script Lua.
-
-Accédez à votre dossier d'installation Archipelago et ouvrez `data/lua/connector_oot.lua`.
+Une fois le client et l'émulateur démarrés, vous devez les connecter. Accédez à votre dossier d'installation Archipelago, puis vers `data/lua`, et faites glisser et déposez le script `connector_oot.lua` sur la fenêtre principale d'EmuHawk. (Vous pourrez plutôt ouvrir depuis la console Lua manuellement, cliquez sur `Script` 〉 `Open Script` et accédez à `connector_oot.lua` avec le sélecteur de fichiers.)
-Pour connecter le client au multiserveur, mettez simplement `:` dans le champ de texte en haut et appuyez sur Entrée (si le
-le serveur utilise un mot de passe, saisissez dans le champ de texte inférieur `/connect : [mot de passe]`)
+Pour connecter le client au multiserveur, mettez simplement `:` dans le champ de texte en haut et appuyez sur Entrée (si le serveur utilise un mot de passe, tapez dans le champ de texte inférieur `/connect : [mot de passe]`)
-Vous êtes maintenant prêt à commencer votre aventure à Hyrule.
+Vous êtes maintenant prêt à commencer votre aventure dans Hyrule.
diff --git a/worlds/overcooked2/Logic.py b/worlds/overcooked2/Logic.py
index d8468cb59af1..20111aa01d66 100644
--- a/worlds/overcooked2/Logic.py
+++ b/worlds/overcooked2/Logic.py
@@ -18,7 +18,7 @@ def has_requirements_for_level_access(state: CollectionState, level_name: str, p
return state.has(level_name, player)
# Must have enough stars to purchase level
- star_count = state.item_count("Star", player) + state.item_count("Bonus Star", player)
+ star_count = state.count("Star", player) + state.count("Bonus Star", player)
if star_count < required_star_count:
return False
@@ -64,7 +64,7 @@ def meets_requirements(state: CollectionState, name: str, stars: int, player: in
total: float = 0.0
for (item_name, weight) in additive_reqs:
- for _ in range(0, state.item_count(item_name, player)):
+ for _ in range(0, state.count(item_name, player)):
total += weight
if total >= 0.99: # be nice to rounding errors :)
return True
diff --git a/worlds/overcooked2/Options.py b/worlds/overcooked2/Options.py
index 9ddcf5e85ff5..18a2c18ed4f1 100644
--- a/worlds/overcooked2/Options.py
+++ b/worlds/overcooked2/Options.py
@@ -1,6 +1,7 @@
+from dataclasses import dataclass
from enum import IntEnum
from typing import TypedDict
-from Options import DefaultOnToggle, Toggle, Range, Choice, OptionSet
+from Options import DefaultOnToggle, PerGameCommonOptions, Toggle, Range, Choice, OptionSet
from .Overcooked2Levels import Overcooked2Dlc
class LocationBalancingMode(IntEnum):
@@ -167,32 +168,30 @@ class StarThresholdScale(Range):
default = 35
-overcooked_options = {
+@dataclass
+class OC2Options(PerGameCommonOptions):
# generator options
- "location_balancing": LocationBalancing,
- "ramp_tricks": RampTricks,
-
+ location_balancing: LocationBalancing
+ ramp_tricks: RampTricks
+
# deathlink
- "deathlink": DeathLink,
-
+ deathlink: DeathLink
+
# randomization options
- "shuffle_level_order": ShuffleLevelOrder,
- "include_dlcs": DLCOptionSet,
- "include_horde_levels": IncludeHordeLevels,
- "prep_levels": PrepLevels,
- "kevin_levels": KevinLevels,
-
+ shuffle_level_order: ShuffleLevelOrder
+ include_dlcs: DLCOptionSet
+ include_horde_levels: IncludeHordeLevels
+ prep_levels: PrepLevels
+ kevin_levels: KevinLevels
+
# quality of life options
- "fix_bugs": FixBugs,
- "shorter_level_duration": ShorterLevelDuration,
- "short_horde_levels": ShortHordeLevels,
- "always_preserve_cooking_progress": AlwaysPreserveCookingProgress,
- "always_serve_oldest_order": AlwaysServeOldestOrder,
- "display_leaderboard_scores": DisplayLeaderboardScores,
-
+ fix_bugs: FixBugs
+ shorter_level_duration: ShorterLevelDuration
+ short_horde_levels: ShortHordeLevels
+ always_preserve_cooking_progress: AlwaysPreserveCookingProgress
+ always_serve_oldest_order: AlwaysServeOldestOrder
+ display_leaderboard_scores: DisplayLeaderboardScores
+
# difficulty settings
- "stars_to_win": StarsToWin,
- "star_threshold_scale": StarThresholdScale,
-}
-
-OC2Options = TypedDict("OC2Options", {option.__name__: option for option in overcooked_options.values()})
+ stars_to_win: StarsToWin
+ star_threshold_scale: StarThresholdScale
diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py
index afffa744fa8a..44227d4becaa 100644
--- a/worlds/overcooked2/__init__.py
+++ b/worlds/overcooked2/__init__.py
@@ -6,7 +6,7 @@
from .Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, Overcooked2GenericLevel
from .Locations import Overcooked2Location, oc2_location_name_to_id, oc2_location_id_to_name
-from .Options import overcooked_options, OC2Options, OC2OnToggle, LocationBalancingMode, DeathLinkMode
+from .Options import OC2Options, OC2OnToggle, LocationBalancingMode, DeathLinkMode
from .Items import item_table, Overcooked2Item, item_name_to_id, item_id_to_name, item_to_unlock_event, item_frequencies, dlc_exclusives
from .Logic import has_requirements_for_level_star, has_requirements_for_level_access, level_shuffle_factory, is_item_progression, is_useful
@@ -16,7 +16,7 @@ class Overcooked2Web(WebWorld):
bug_report_page = "https://github.com/toasterparty/oc2-modding/issues"
setup_en = Tutorial(
- "Multiworld Setup Tutorial",
+ "Multiworld Setup Guide",
"A guide to setting up the Overcooked! 2 randomizer on your computer.",
"English",
"setup_en.md",
@@ -47,9 +47,7 @@ class Overcooked2World(World):
game = "Overcooked! 2"
web = Overcooked2Web()
required_client_version = (0, 3, 8)
- option_definitions = overcooked_options
topology_present: bool = False
- data_version = 3
item_name_to_id = item_name_to_id
item_id_to_name = item_id_to_name
@@ -57,13 +55,14 @@ class Overcooked2World(World):
location_id_to_name = oc2_location_id_to_name
location_name_to_id = oc2_location_name_to_id
- options: Dict[str, Any]
+ options_dataclass = OC2Options
+ options: OC2Options
itempool: List[Overcooked2Item]
# Helper Functions
def is_level_horde(self, level_id: int) -> bool:
- return self.options["IncludeHordeLevels"] and \
+ return self.options.include_horde_levels and \
(self.level_mapping is not None) and \
level_id in self.level_mapping.keys() and \
self.level_mapping[level_id].is_horde
@@ -90,13 +89,7 @@ def add_region(self, region_name: str):
def connect_regions(self, source: str, target: str, rule: Optional[Callable[[CollectionState], bool]] = None):
sourceRegion = self.multiworld.get_region(source, self.player)
targetRegion = self.multiworld.get_region(target, self.player)
-
- connection = Entrance(self.player, '', sourceRegion)
- if rule:
- connection.access_rule = rule
-
- sourceRegion.exits.append(connection)
- connection.connect(targetRegion)
+ sourceRegion.connect(targetRegion, rule=rule)
def add_level_location(
self,
@@ -121,8 +114,6 @@ def add_level_location(
region,
)
- location.event = is_event
-
if priority:
location.progress_type = LocationProgressType.PRIORITY
else:
@@ -145,11 +136,6 @@ def add_level_location(
location
)
- def get_options(self) -> Dict[str, Any]:
- return OC2Options({option.__name__: getattr(self.multiworld, name)[self.player].result
- if issubclass(option, OC2OnToggle) else getattr(self.multiworld, name)[self.player].value
- for name, option in overcooked_options.items()})
-
def get_n_random_locations(self, n: int) -> List[int]:
"""Return a list of n random non-repeating level locations"""
levels = list()
@@ -160,7 +146,7 @@ def get_n_random_locations(self, n: int) -> List[int]:
for level in Overcooked2Level():
if level.level_id == 36:
continue
- elif not self.options["KevinLevels"] and level.level_id > 36:
+ elif not self.options.kevin_levels and level.level_id > 36:
break
levels.append(level.level_id)
@@ -177,7 +163,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
@@ -230,27 +216,24 @@ def get_priority_locations(self) -> List[int]:
# Autoworld Hooks
def generate_early(self):
- self.player_name = self.multiworld.player_name[self.player]
- self.options = self.get_options()
-
# 0.0 to 1.0 where 1.0 is World Record
- self.star_threshold_scale = self.options["StarThresholdScale"] / 100.0
+ self.star_threshold_scale = self.options.star_threshold_scale / 100.0
# Parse DLCOptionSet back into enums
- self.enabled_dlc = {Overcooked2Dlc(x) for x in self.options["DLCOptionSet"]}
+ self.enabled_dlc = {Overcooked2Dlc(x) for x in self.options.include_dlcs.value}
# Generate level unlock requirements such that the levels get harder to unlock
# the further the game has progressed, and levels progress radially rather than linearly
- self.level_unlock_counts = level_unlock_requirement_factory(self.options["StarsToWin"])
+ self.level_unlock_counts = level_unlock_requirement_factory(self.options.stars_to_win.value)
# Assign new kitchens to each spot on the overworld using pure random chance and nothing else
- if self.options["ShuffleLevelOrder"]:
+ if self.options.shuffle_level_order:
self.level_mapping = \
level_shuffle_factory(
self.multiworld.random,
- self.options["PrepLevels"] != PrepLevelMode.excluded,
- self.options["IncludeHordeLevels"],
- self.options["KevinLevels"],
+ self.options.prep_levels != PrepLevelMode.excluded,
+ self.options.include_horde_levels.result,
+ self.options.kevin_levels.result,
self.enabled_dlc,
self.player_name,
)
@@ -277,7 +260,7 @@ def create_regions(self) -> None:
# Create and populate "regions" (a.k.a. levels)
for level in Overcooked2Level():
- if not self.options["KevinLevels"] and level.level_id > 36:
+ if not self.options.kevin_levels and level.level_id > 36:
break
# Create Region (e.g. "1-1")
@@ -336,7 +319,7 @@ def create_regions(self) -> None:
level_access_rule: Callable[[CollectionState], bool] = \
lambda state, level_name=level.level_name, previous_level_completed_event_name=previous_level_completed_event_name, required_star_count=required_star_count: \
- has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.options["RampTricks"], self.player)
+ has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.options.ramp_tricks.result, self.player)
self.connect_regions("Overworld", level.level_name, level_access_rule)
# Level --> Overworld
@@ -369,11 +352,11 @@ def create_items(self):
# Item is always useless with these settings
continue
- if not self.options["IncludeHordeLevels"] and item_name in ["Calmer Unbread", "Coin Purse"]:
+ if not self.options.include_horde_levels and item_name in ["Calmer Unbread", "Coin Purse"]:
# skip horde-specific items if no horde levels
continue
- if not self.options["KevinLevels"]:
+ if not self.options.kevin_levels:
if item_name.startswith("Kevin"):
# skip kevin items if no kevin levels
continue
@@ -382,7 +365,7 @@ def create_items(self):
# skip dark green ramp if there's no Kevin-1 to reveal it
continue
- if is_item_progression(item_name, self.level_mapping, self.options["KevinLevels"]):
+ if is_item_progression(item_name, self.level_mapping, self.options.kevin_levels):
# progression.append(item_name)
classification = ItemClassification.progression
else:
@@ -404,7 +387,7 @@ def create_items(self):
# Fill any free space with filler
pool_count = len(oc2_location_name_to_id)
- if not self.options["KevinLevels"]:
+ if not self.options.kevin_levels:
pool_count -= 8
while len(self.itempool) < pool_count:
@@ -416,7 +399,7 @@ def create_items(self):
def place_events(self):
# Add Events (Star Acquisition)
for level in Overcooked2Level():
- if not self.options["KevinLevels"] and level.level_id > 36:
+ if not self.options.kevin_levels and level.level_id > 36:
break
if level.level_id != 36:
@@ -449,7 +432,7 @@ def fill_json_data(self) -> Dict[str, Any]:
# Serialize Level Order
story_level_order = dict()
- if self.options["ShuffleLevelOrder"]:
+ if self.options.shuffle_level_order:
for level_id in self.level_mapping:
level: Overcooked2GenericLevel = self.level_mapping[level_id]
story_level_order[str(level_id)] = {
@@ -481,7 +464,7 @@ def fill_json_data(self) -> Dict[str, Any]:
level_unlock_requirements[str(level_id)] = level_id - 1
# Set Kevin Unlock Requirements
- if self.options["KevinLevels"]:
+ if self.options.kevin_levels:
def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]:
location = self.multiworld.find_item(f"Kevin-{level_id-36}", self.player)
if location.player != self.player:
@@ -506,7 +489,7 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]:
on_level_completed[level_id] = [item_to_unlock_event(location.item.name)]
# Put it all together
- star_threshold_scale = self.options["StarThresholdScale"] / 100
+ star_threshold_scale = self.options.star_threshold_scale / 100
base_data = {
# Changes Inherent to rando
@@ -528,13 +511,13 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]:
"SaveFolderName": mod_name,
"CustomOrderTimeoutPenalty": 10,
"LevelForceHide": [37, 38, 39, 40, 41, 42, 43, 44],
- "LocalDeathLink": self.options["DeathLink"] != DeathLinkMode.disabled,
- "BurnTriggersDeath": self.options["DeathLink"] == DeathLinkMode.death_and_overcook,
+ "LocalDeathLink": self.options.deathlink != DeathLinkMode.disabled,
+ "BurnTriggersDeath": self.options.deathlink == DeathLinkMode.death_and_overcook,
# Game Modifications
"LevelPurchaseRequirements": level_purchase_requirements,
"Custom66TimerScale": max(0.4, 0.25 + (1.0 - star_threshold_scale)*0.6),
- "ShortHordeLevels": self.options["ShortHordeLevels"],
+ "ShortHordeLevels": self.options.short_horde_levels.result,
"CustomLevelOrder": custom_level_order,
# Items (Starting Inventory)
@@ -580,28 +563,28 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]:
# Set remaining data in the options dict
bugs = ["FixDoubleServing", "FixSinkBug", "FixControlStickThrowBug", "FixEmptyBurnerThrow"]
for bug in bugs:
- self.options[bug] = self.options["FixBugs"]
- self.options["PreserveCookingProgress"] = self.options["AlwaysPreserveCookingProgress"]
- self.options["TimerAlwaysStarts"] = self.options["PrepLevels"] == PrepLevelMode.ayce
- self.options["LevelTimerScale"] = 0.666 if self.options["ShorterLevelDuration"] else 1.0
- self.options["LeaderboardScoreScale"] = {
+ base_data[bug] = self.options.fix_bugs.result
+ base_data["PreserveCookingProgress"] = self.options.always_preserve_cooking_progress.result
+ base_data["TimerAlwaysStarts"] = self.options.prep_levels == PrepLevelMode.ayce
+ base_data["LevelTimerScale"] = 0.666 if self.options.shorter_level_duration else 1.0
+ base_data["LeaderboardScoreScale"] = {
"FourStars": 1.0,
"ThreeStars": star_threshold_scale,
"TwoStars": star_threshold_scale * 0.75,
"OneStar": star_threshold_scale * 0.35,
}
+ base_data["AlwaysServeOldestOrder"] = self.options.always_serve_oldest_order.result
- base_data.update(self.options)
return base_data
def fill_slot_data(self) -> Dict[str, Any]:
return self.fill_json_data()
def write_spoiler(self, spoiler_handle: TextIO) -> None:
- if not self.options["ShuffleLevelOrder"]:
+ if not self.options.shuffle_level_order:
return
- world: Overcooked2World = self.multiworld.worlds[self.player]
+ world: Overcooked2World = self
spoiler_handle.write(f"\n\n{self.player_name}'s Level Order:\n\n")
for overworld_id in world.level_mapping:
overworld_name = Overcooked2GenericLevel(overworld_id).shortname.split("Story ")[1]
diff --git a/worlds/overcooked2/docs/en_Overcooked! 2.md b/worlds/overcooked2/docs/en_Overcooked! 2.md
index 298c33683ce7..d4cb6fba1f9a 100644
--- a/worlds/overcooked2/docs/en_Overcooked! 2.md
+++ b/worlds/overcooked2/docs/en_Overcooked! 2.md
@@ -2,7 +2,7 @@
## Quick Links
- [Setup Guide](../../../../tutorial/Overcooked!%202/setup/en)
-- [Settings Page](../../../../games/Overcooked!%202/player-settings)
+- [Options Page](../../../../games/Overcooked!%202/player-options)
- [OC2-Modding GitHub](https://github.com/toasterparty/oc2-modding)
## How Does Randomizer Work in the Kitchen?
@@ -55,7 +55,7 @@ The following items were invented for Randomizer:
- Ramp Buttons (x7)
- Bonus Star (Filler Item*)
-**Note: Bonus star count varies with settings*
+**Note: Bonus star count varies with options*
## Other Game Modifications
diff --git a/worlds/overcooked2/docs/setup_en.md b/worlds/overcooked2/docs/setup_en.md
index 1b21642cfe03..bb4c4959a7d4 100644
--- a/worlds/overcooked2/docs/setup_en.md
+++ b/worlds/overcooked2/docs/setup_en.md
@@ -2,7 +2,7 @@
## Quick Links
- [Main Page](../../../../games/Overcooked!%202/info/en)
-- [Settings Page](../../../../games/Overcooked!%202/player-settings)
+- [Options Page](../../../../games/Overcooked!%202/player-options)
- [OC2-Modding GitHub](https://github.com/toasterparty/oc2-modding)
## Required Software
@@ -49,9 +49,7 @@ To completely remove *OC2-Modding*, navigate to your game's installation folder
## Generate a MultiWorld Game
-1. Visit the [Player Settings](../../../../games/Overcooked!%202/player-settings) page and configure the game-specific settings to taste
-
-*By default, these settings will only use levels from the base game and the "Seasonal" free DLC updates. If you own any of the paid DLC, you may select individual DLC packs to include/exclude on the [Weighted Settings](../../../../weighted-settings) page*
+1. Visit the [Player Options](../../../../games/Overcooked!%202/player-options) page and configure the game-specific options to taste
2. Export your yaml file and use it to generate a new randomized game
@@ -84,11 +82,11 @@ To completely remove *OC2-Modding*, navigate to your game's installation folder
Since the goal of randomizer isn't necessarily to achieve new personal high scores, players may find themselves waiting for a level timer to expire once they've met their objective. A new feature called *Auto-Complete* has been added to automatically complete levels once a target star count has been achieved.
-To enable *Auto-Complete*, press the **Show** button near the top of your screen to expand the modding controls. Then, repeatedly press the **Auto-Complete** button until it shows the desired setting.
+To enable *Auto-Complete*, press the **Show** button near the top of your screen to expand the modding controls. Then, repeatedly press the **Auto-Complete** button until it shows the desired option.
## Overworld Sequence Breaking
-In the world's settings, there is an option called "Overworld Tricks" which allows the generator to make games which require doing tricks with the food truck to complete. This includes:
+In the world's options, there is an option called "Overworld Tricks" which allows the generator to make games which require doing tricks with the food truck to complete. This includes:
- Dashing across gaps
diff --git a/worlds/overcooked2/test/TestOvercooked2.py b/worlds/overcooked2/test/TestOvercooked2.py
index ee0b44a86e9f..a3c1c3dc0d3e 100644
--- a/worlds/overcooked2/test/TestOvercooked2.py
+++ b/worlds/overcooked2/test/TestOvercooked2.py
@@ -4,10 +4,11 @@
from worlds.AutoWorld import AutoWorldRegister
from test.general import setup_solo_multiworld
-from worlds.overcooked2.Items import *
-from worlds.overcooked2.Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, OverworldRegion, overworld_region_by_level, level_id_to_shortname
-from worlds.overcooked2.Logic import level_logic, overworld_region_logic, level_shuffle_factory
-from worlds.overcooked2.Locations import oc2_location_name_to_id
+from ..Items import *
+from ..Overcooked2Levels import (Overcooked2Dlc, Overcooked2Level, OverworldRegion, overworld_region_by_level,
+ level_id_to_shortname)
+from ..Logic import level_logic, overworld_region_logic, level_shuffle_factory
+from ..Locations import oc2_location_name_to_id
class Overcooked2Test(unittest.TestCase):
diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md
new file mode 100644
index 000000000000..0437c0dae8ff
--- /dev/null
+++ b/worlds/pokemon_emerald/CHANGELOG.md
@@ -0,0 +1,223 @@
+# 2.2.0
+
+### Features
+
+- When you blacklist species from wild encounters and turn on dexsanity, blacklisted species are not added as locations
+and won't show up in the wild. Previously they would be forced to show up exactly once.
+- Added support for some new autotracking events.
+- Updated option descriptions.
+- Added `full` alias for `100` on TM and HM compatibility options.
+
+### Fixes
+
+- The Lilycove Wailmer now logically block you from the east. Actual game behavior is still unchanged for now.
+- Water encounters in Slateport now correctly require Surf.
+- Mirage Tower can no longer be your only logical access to a species in the wild, since it can permanently disappear.
+- Updated the tracker link in the setup guide.
+
+# 2.1.1
+
+### Features
+
+- You no longer need a copy of Pokemon Emerald to generate a game, patch files generate much faster.
+
+# 2.1.0
+
+_Separately released, branching from 2.0.0. Included procedure patch migration, but none of the 2.0.1 fixes._
+
+# 2.0.1
+
+### Fixes
+
+- Changed "Ho-oh" to "Ho-Oh" in options.
+- Temporary fix to alleviate problems with sometimes not receiving certain items just after connecting if `remote_items`
+is `true`.
+- Temporarily disable a possible location for Marine Cave to spawn, as it causes an overflow.
+- Water encounters in Dewford now correctly require Surf.
+
+# 2.0.0
+
+### Features
+- Picking up items for other players will display the actual item name and receiving player in-game instead of
+"ARCHIPELAGO ITEM". (This does have a limit, but you're unlikely to reach it in all but the largest multiworlds.)
+- New goal `legendary_hunt`. Your goal is to catch/defeat some number of legendary encounters. That is, the static
+encounters themselves, whatever species they may be. Legendary species found in the wild don't count.
+ - You can force the goal to require captures with `legendary_hunt_catch`. If you accidentally faint a legendary, you
+ can respawn it by beating the champion.
+ - The number of legendaries needed is controlled by the `legendary_hunt_count` option.
+ - The caves containing Kyogre and Groudon are fixed to one location per seed. You need to go to the weather
+ institute to trigger a permanent weather event at the corresponding locations. Only one weather event can be active
+ at a time.
+ - The move tutor for the move Sleep Talk has been changed to Dig and is unlimited use (for Sealed Chamber).
+ - Relicanth and Wailord are guaranteed to be reachable in the wild (for Sealed Chamber). Interacting with the Sealed
+ Chamber wall will give you dex info for Wailord and Relicanth.
+ - Event legendaries are included for this goal (see below for new ferry behavior and event tickets).
+ - The roamer is included in this count. It will _always_ be Latios no matter what your options are. Otherwise you
+ might not have any way of knowing which species is roaming to be able to track it. In legendary hunt, Latios will
+ never appear as a wild pokemon to make tracking it easier. The television broadcast that creates the roamer will
+ give you dex info for Latios.
+ - You can set which encounters are considered for this goal with the `allowed_legendary_hunt_encounters` option.
+- New option `dexsanity`. Adds pokedex entries as locations.
+ - Added locations contribute either a Poke Ball, Great Ball, or Ultra Ball to the item pool, based on the evolution
+ stage.
+ - Logic uses only wild encounters for now.
+ - Defeating a gym leader awards "seen" info on 1/8th of the pokedex.
+- New option `trainersanity`. Defeating a trainer awards a random item.
+ - Trainers no longer award money upon defeat. Instead they add a sellable item to the item pool.
+ - Missable trainers are prevented from disappearing when this is enabled.
+ - Gym trainers remain active after their leader is defeated.
+ - Does not include trainers in the Trick House.
+- New option `berry_trees`. Adds berry trees as locations.
+ - All soil patches start with a fully grown berry tree that gives one item.
+ - There are 88 berry trees.
+ - Berries cannot be planted in soil with this option enabled.
+ - Soil that doesn't start with a tree on a fresh save contributes a Sitrus Berry to the item pool.
+- New option `death_link`. Forgive me, Figment.
+- Added Artisan Cave locations
+ - Requires Wailmer Pail and the ability to Surf to access.
+- Added Trick House locations. The Trick Master is finally here!
+ - He will make new layouts only if you have the corresponding badge (or beat the game) and have completed the
+ previous layout (all vanilla behavior).
+ - If you neglect to pick up an item in a puzzle before completing it, the Trick Master will give the item to you
+ alongside the prize.
+ - Locations are enabled or disabled with their broader categories (npc gifts, overworld items, etc...)
+- Added daily berry gift locations. There are a half dozen or so NPCs that give you one or two berries per day.
+ - All these locations are considered NPC gifts.
+ - The NPCs have been reworked to give this gift once permanently so they can be added as locations.
+- New option `remote_items`. All randomized items are sent from the server instead of being patched into your game
+(except for start inventory, which remains in the PC)
+ - As a side effect, when you pick up your own item, there will be a gap between the item disappearing from the
+ overworld and your game actually receiving it. It also causes gifts from NPCs which contain your own items to not
+ show up until after their text box closes. It can feel odd, but there should be no danger to it.
+ - If the seed is in race mode, this is forcibly enabled.
+ - Benefits include:
+ - Two players can play the same slot and both receive items that slot picks up for itself (as long as it was
+ randomized)
+ - You receive items you picked up for yourself if you lose progress on your save
+ - Competitive integrity; the patch file no longer has any knowledge of item placement
+- New option `match_trainer_levels`. This is a sort of pseudo level cap for a randomizer context.
+ - When you start a trainer fight, all your pokemon have their levels temporarily set to the highest level in the
+ opponent's party.
+ - During the battle, all earned exp is set to 0 (EVs are still gained during battle as normal). When the outcome of
+ the battle is decided, your pokemon have their levels reset to what they were before the fight and exp is awarded as
+ it would have been without this option. Think of it as holding earned exp in reserve and awarding it at the end
+ instead, even giving it to fainted pokemon if they earned any before fainting.
+ - Exp gain is based on _your_ party's average level to moderate exp over the course of a seed. Wild battles are
+ entirely unchanged by this option.
+- New option `match_trainer_levels_bonus`. A flat bonus to apply to your party's levels when using
+`match_trainer_levels`. In case you want to give yourself a nerf or buff while still approximately matching your
+opponent.
+- New option `force_fully_evolved`. Define a level at which trainers will stop using pokemon that have further evolution
+stages.
+- New option `move_blacklist`. Define a list of moves that should not be given randomly to learnsets or TMs. Move names
+are accurate to Gen 3 except for capitalization.
+- New option `extra_bumpy_slope`. Adds a "bumpy slope" to Route 115 that lets you hop up the ledge with the Acro Bike.
+- New option `modify_118`. Changes Route 118 so that it must be crossed with the Acro Bike, and cannot be crossed by
+surfing.
+- Changed `require_flash` option to a choice between none, only granite cave, only victory road, or both caves.
+- Removed `static_encounters` option.
+- New option `legendary_encounters`. Replaces `static_encounters`, but only concerns legendaries.
+- New option `misc_pokemon`. Replaces `static_encounters`, but only concerns non-legendaries.
+- Removed `fly_without_badge` option. (Don't worry)
+- New option `hm_requirements`. Will eventually be able to give you more control over the badge requirements for all
+HMs. For now, only includes the presets `vanilla` and `fly_without_badge`.
+- Removed `allow_wild_legendaries`, `allow_starter_legendaries`, and `allow_trainer_legendaries` options.
+- New options `wild_encounter_blacklist`, `starter_blacklist`, and `trainer_party_blacklist`.
+ - These take lists of species and prevent them from randomizing into the corresponding categories
+ - If adhering to your blacklist would make it impossible to choose a random species, your blacklist is ignored in
+ that case
+ - All three include a shorthand for excluding legendaries
+- Removed `enable_ferry` option.
+ - The ferry is now always present.
+ - The S.S. Ticket item/location is now part of `key_items`.
+- Added event tickets and islands.
+ - All event tickets are given to the player by Norman after defeating the Champion alongside the S.S. Ticket.
+ - As in vanilla, these tickets are only usable from Lilycove. Not Slateport or the Battle Frontier.
+- New option `event_tickets`. Randomizes the above-mentioned tickets into the item pool.
+- New option `enable_wonder_trading`. You can participate in Wonder Trading by interacting with the center receptionist
+on the second floor of Pokemon Centers.
+ - Why is this an option instead of just being enabled? You might want to disable wonder trading in a meta yaml to
+ make sure certain rules can't be broken. Or you may want to turn it off for yourself to definitively prevent being
+ asked for help if you prefer to keep certain walls up between your game and others. Trades _do_ include items and
+ known moves, which means there is potential for an extra level of cooperation and even ways to go out of logic. But
+ that's not a boundary everyone wants broken down all the time. Please be respectful of someone's choice to not
+ participate if that's their preference.
+ - A lot of time was spent trying to make this all work without having to touch your client. Hopefully it goes
+ smoothly, but there's room for jank. Anything you decide to give to her you should consider gone forever, whether
+ because it was traded away or because something "went wrong in transit" and the pokemon's data got lost after being
+ removed from the server.
+ - Wonder Trading is _not_ resistant to save scumming in either direction. You _could_ abuse it to dupe pokemon,
+ because there's not realistically a way for me to prevent it, but I'd urge you to stick to the spirit of the design
+ unless everyone involved doesn't mind.
+ - The wonder trades you receive are stored in your save data even before you pick them up, so if you save after the
+ client tells you that you received a wonder trade, it's safe. You don't need to retrieve it from a poke center for
+ it to persist. However, if you reset your game to a point in time before your client popped the "Wonder trade
+ received" message, that pokemon is lost forever.
+- New `easter_egg` passphrase system.
+ - All valid easter egg passphrases will be a phrase that it's possible to submit as a trendy phrase in Dewford Town.
+ Changing the trendy phrase does ***not*** trigger easter eggs. Only the phrase you put in your YAML can trigger an
+ easter egg.
+ - There may be other ways to learn more information.
+ - Phrases are case insensitive. Here are a couple examples of possible phrases: `"GET FOE"`,
+ `"HERE GOES GRANDMOTHER"`, `"late eh?"` (None of those do anything, but I'd love to hear what you think they would.)
+- Added three new easter egg effects.
+- Changed the original easter egg phrase to use the new system.
+- Renamed `tm_moves` to `tm_tutor_moves`. Move tutors are also affected by this option (except the new Dig tutor).
+- Renamed `tm_compatibility` to `tm_tutor_compatibility`. Move tutors are also affected by this option.
+- Changed `tm_tutor_compatibility` to be a percent chance instead of a choice. Use `-1` for vanilla.
+- Changed `hm_compatibility` to be a percent chance instead of a choice. Use `-1` for vanilla.
+- New option `music`. Shuffles all looping music. Includes FRLG tracks and possibly some unused stuff.
+- New option `fanfares`. Shuffles all fanfares. Includes FRLG tracks. When this is enabled, pressing B will interrupt
+most fanfares.
+- New option `purge_spinners`. Trainers that change which direction they face will do so predictably, and will no longer
+turn to face you when you run.
+- New option `normalize_encounter_rates`. Sets every encounter slot to (almost) equal probability. Does NOT make every
+species equally likely to appear, but makes rare encounters less rare.
+- Added `Trick House` location group.
+- Removed `Postgame Locations` location group.
+
+### QoL
+
+- Can teach moves over HM moves.
+- Fishing is much less random; pokemon will always bite if there's an encounter there.
+- Mirage Island is now always present.
+- Waking Rayquaza is no longer required. After releasing Kyogre, going to Sootopolis will immediately trigger the
+Rayquaza cutscene.
+- Renamed some locations to be more accurate.
+- Most trainers will no longer ask to be registered in your Pokegear after battle. Also removed most step-based match
+calls.
+- Removed a ledge on Route 123. With careful routing, it's now possible to check every location without having to save
+scum or go back around.
+- Added "GO HOME" button on the start menu where "EXIT" used to be. Will teleport you to Littleroot.
+- Some locations which are directly blocked by completing your goal are automatically excluded.
+ - For example, the S.S. Ticket and a Champion goal, or the Sludge Bomb TM and the Norman goal.
+ - Your particular options might still result in locations that can't be reached until after your goal. For example,
+ setting a Norman goal and setting your E4 requirement to 8 gyms means that post-Champion locations will not be
+ reachable before defeating Norman, but they are NOT excluded by this modification. That's one of the simpler
+ examples. It is extremely tedious to try to detect these sorts of situations, so I'll instead leave it to you to be
+ aware of your own options.
+- Species in the pokedex are searchable by type even if you haven't caught that species yet
+
+### Fixes
+
+- Mt. Pyre summit state no longer changes when you finish the Sootopolis events, which would lock you out of one or two
+locations.
+- Whiting out under certain conditions no longer softlocks you by moving Mr. Briney to an inaccessible area.
+- It's no longer possible to join a room using the wrong patch file, even if the slot names match.
+- NPCs now stop moving while you're receiving an item.
+- Creating a secret base no longer triggers sending the Secret Power TM location.
+- Hopefully fix bug where receiving an item while walking over a trigger can skip that trigger (the Moving
+Truck/Petalburg wrong warp)
+
+## Easter Eggs
+
+There are plenty among you who are capable of ~~cheating~~ finding information about the easter egg phrases by reading
+source code, writing brute force scripts, and inspecting memory for clues and answers. By all means, go ahead, that can
+be your version of this puzzle and I don't intend to stand in your way. **However**, I would ask that any information
+you come up with by doing this, you keep entirely to yourself until the community as a whole has figured out what you
+know. There was not previously a way to reasonably learn about or make guesses at the easter egg, but that has changed.
+There are mechanisms by which solutions can be found or guessed over the course of multiple games by multiple people,
+and I'd rather the fun not get spoiled immediately.
+
+Once a solution has been found I'd _still_ prefer discussion about hints and effects remain behind spoiler tags just in
+case there are people who want to do the hunt on their own. Thank you all, and good luck.
diff --git a/worlds/pokemon_emerald/LICENSE b/worlds/pokemon_emerald/LICENSE
new file mode 100644
index 000000000000..30b4f413fe4c
--- /dev/null
+++ b/worlds/pokemon_emerald/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2023 Zunawe
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py
new file mode 100644
index 000000000000..abdee26f572f
--- /dev/null
+++ b/worlds/pokemon_emerald/__init__.py
@@ -0,0 +1,736 @@
+"""
+Archipelago World definition for Pokemon Emerald Version
+"""
+from collections import Counter
+import copy
+import logging
+import os
+import pkgutil
+from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar, TextIO, Union
+
+from BaseClasses import ItemClassification, MultiWorld, Tutorial, LocationProgressType
+from Fill import FillError, fill_restrictive
+from Options import OptionError, Toggle
+import settings
+from worlds.AutoWorld import WebWorld, World
+
+from .client import PokemonEmeraldClient # Unused, but required to register with BizHawkClient
+from .data import LEGENDARY_POKEMON, MapData, SpeciesData, TrainerData, data as emerald_data
+from .items import (ITEM_GROUPS, PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification,
+ offset_item_value)
+from .locations import (LOCATION_GROUPS, PokemonEmeraldLocation, create_location_label_to_id_map,
+ create_locations_with_tags, set_free_fly, set_legendary_cave_entrances)
+from .opponents import randomize_opponent_parties
+from .options import (Goal, DarkCavesRequireFlash, HmRequirements, ItemPoolType, PokemonEmeraldOptions,
+ RandomizeWildPokemon, RandomizeBadges, RandomizeHms, NormanRequirement)
+from .pokemon import (get_random_move, get_species_id_by_label, randomize_abilities, randomize_learnsets,
+ randomize_legendary_encounters, randomize_misc_pokemon, randomize_starters,
+ randomize_tm_hm_compatibility,randomize_types, randomize_wild_encounters)
+from .rom import PokemonEmeraldProcedurePatch, write_tokens
+
+
+class PokemonEmeraldWebWorld(WebWorld):
+ """
+ Webhost info for Pokemon Emerald
+ """
+ theme = "ocean"
+
+ setup_en = Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to playing Pokémon Emerald with Archipelago.",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["Zunawe"]
+ )
+
+ setup_es = Tutorial(
+ "GuÃa de configuración para Multiworld",
+ "Una guÃa para jugar Pokémon Emerald en Archipelago",
+ "Español",
+ "setup_es.md",
+ "setup/es",
+ ["nachocua"]
+ )
+
+ setup_sv = Tutorial(
+ "Multivärld Installations Guide",
+ "En guide för att kunna spela Pokémon Emerald med Archipelago.",
+ "Svenska",
+ "setup_sv.md",
+ "setup/sv",
+ ["Tsukino"]
+ )
+
+ tutorials = [setup_en, setup_es, setup_sv]
+
+
+class PokemonEmeraldSettings(settings.Group):
+ class PokemonEmeraldRomFile(settings.UserFilePath):
+ """File name of your English Pokemon Emerald ROM"""
+ description = "Pokemon Emerald ROM File"
+ copy_to = "Pokemon - Emerald Version (USA, Europe).gba"
+ md5s = [PokemonEmeraldProcedurePatch.hash]
+
+ rom_file: PokemonEmeraldRomFile = PokemonEmeraldRomFile(PokemonEmeraldRomFile.copy_to)
+
+
+class PokemonEmeraldWorld(World):
+ """
+ Pokémon Emerald is the definitive Gen III Pokémon game and one of the most beloved in the franchise.
+ Catch, train, and battle Pokémon, explore the Hoenn region, thwart the plots
+ of Team Magma and Team Aqua, challenge gyms, and become the Pokémon champion!
+ """
+ game = "Pokemon Emerald"
+ web = PokemonEmeraldWebWorld()
+ topology_present = True
+
+ settings_key = "pokemon_emerald_settings"
+ settings: ClassVar[PokemonEmeraldSettings]
+
+ options_dataclass = PokemonEmeraldOptions
+ options: PokemonEmeraldOptions
+
+ item_name_to_id = create_item_label_to_code_map()
+ location_name_to_id = create_location_label_to_id_map()
+ item_name_groups = ITEM_GROUPS
+ location_name_groups = LOCATION_GROUPS
+
+ required_client_version = (0, 4, 6)
+
+ badge_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]]
+ hm_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]]
+ free_fly_location_id: int
+ blacklisted_moves: Set[int]
+ blacklisted_wilds: Set[int]
+ blacklisted_starters: Set[int]
+ blacklisted_opponent_pokemon: Set[int]
+ hm_requirements: Dict[str, Union[int, List[str]]]
+ auth: bytes
+
+ modified_species: Dict[int, SpeciesData]
+ modified_maps: Dict[str, MapData]
+ modified_tmhm_moves: List[int]
+ modified_legendary_encounters: List[int]
+ modified_starters: Tuple[int, int, int]
+ modified_trainers: List[TrainerData]
+
+ def __init__(self, multiworld, player):
+ super(PokemonEmeraldWorld, self).__init__(multiworld, player)
+ self.badge_shuffle_info = None
+ self.hm_shuffle_info = None
+ self.free_fly_location_id = 0
+ self.blacklisted_moves = set()
+ self.blacklisted_wilds = set()
+ self.blacklisted_starters = set()
+ self.blacklisted_opponent_pokemon = set()
+ self.modified_maps = copy.deepcopy(emerald_data.maps)
+ self.modified_species = copy.deepcopy(emerald_data.species)
+ self.modified_tmhm_moves = []
+ self.modified_starters = emerald_data.starters
+ self.modified_trainers = []
+ self.modified_legendary_encounters = []
+
+ @classmethod
+ def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
+ from .sanity_check import validate_regions
+
+ assert validate_regions()
+
+ def get_filler_item_name(self) -> str:
+ return "Great Ball"
+
+ def generate_early(self) -> None:
+ self.hm_requirements = {
+ "HM01 Cut": ["Stone Badge"],
+ "HM02 Fly": ["Feather Badge"],
+ "HM03 Surf": ["Balance Badge"],
+ "HM04 Strength": ["Heat Badge"],
+ "HM05 Flash": ["Knuckle Badge"],
+ "HM06 Rock Smash": ["Dynamo Badge"],
+ "HM07 Waterfall": ["Rain Badge"],
+ "HM08 Dive": ["Mind Badge"],
+ }
+ if self.options.hm_requirements == HmRequirements.option_fly_without_badge:
+ self.hm_requirements["HM02 Fly"] = 0
+
+ self.blacklisted_moves = {emerald_data.move_labels[label] for label in self.options.move_blacklist.value}
+
+ self.blacklisted_wilds = {
+ get_species_id_by_label(species_name)
+ for species_name in self.options.wild_encounter_blacklist.value
+ if species_name != "_Legendaries"
+ }
+ if "_Legendaries" in self.options.wild_encounter_blacklist.value:
+ self.blacklisted_wilds |= LEGENDARY_POKEMON
+
+ self.blacklisted_starters = {
+ get_species_id_by_label(species_name)
+ for species_name in self.options.starter_blacklist.value
+ if species_name != "_Legendaries"
+ }
+ if "_Legendaries" in self.options.starter_blacklist.value:
+ self.blacklisted_starters |= LEGENDARY_POKEMON
+
+ self.blacklisted_opponent_pokemon = {
+ get_species_id_by_label(species_name)
+ for species_name in self.options.trainer_party_blacklist.value
+ if species_name != "_Legendaries"
+ }
+ if "_Legendaries" in self.options.starter_blacklist.value:
+ self.blacklisted_opponent_pokemon |= LEGENDARY_POKEMON
+
+ # In race mode we don't patch any item location information into the ROM
+ if self.multiworld.is_race and not self.options.remote_items:
+ logging.warning("Pokemon Emerald: Forcing Player %s (%s) to use remote items due to race mode.",
+ self.player, self.player_name)
+ self.options.remote_items.value = Toggle.option_true
+
+ if self.options.goal == Goal.option_legendary_hunt:
+ # Prevent turning off all legendary encounters
+ if len(self.options.allowed_legendary_hunt_encounters.value) == 0:
+ raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.player_name}) needs to allow at "
+ "least one legendary encounter when goal is legendary hunt.")
+
+ # Prevent setting the number of required legendaries higher than the number of enabled legendaries
+ if self.options.legendary_hunt_count.value > len(self.options.allowed_legendary_hunt_encounters.value):
+ logging.warning("Pokemon Emerald: Legendary hunt count for Player %s (%s) higher than number of allowed "
+ "legendary encounters. Reducing to number of allowed encounters.", self.player,
+ self.player_name)
+ self.options.legendary_hunt_count.value = len(self.options.allowed_legendary_hunt_encounters.value)
+
+ # Require random wild encounters if dexsanity is enabled
+ if self.options.dexsanity and self.options.wild_pokemon == RandomizeWildPokemon.option_vanilla:
+ raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.player_name}) must not leave wild "
+ "encounters vanilla if enabling dexsanity.")
+
+ # If badges or HMs are vanilla, Norman locks you from using Surf,
+ # which means you're not guaranteed to be able to reach Fortree Gym,
+ # Mossdeep Gym, or Sootopolis Gym. So we can't require reaching those
+ # gyms to challenge Norman or it creates a circular dependency.
+ #
+ # This is never a problem for completely random badges/hms because the
+ # algo will not place Surf/Balance Badge on Norman on its own. It's
+ # never a problem for shuffled badges/hms because there is no scenario
+ # where Cut or the Stone Badge can be a lynchpin for access to any gyms,
+ # so they can always be put on Norman in a worst case scenario.
+ #
+ # This will also be a problem in warp rando if direct access to Norman's
+ # room requires Surf or if access any gym leader in general requires
+ # Surf. We will probably have to force this to 0 in that case.
+ max_norman_count = 7
+
+ if self.options.badges == RandomizeBadges.option_vanilla:
+ max_norman_count = 4
+
+ if self.options.hms == RandomizeHms.option_vanilla:
+ if self.options.norman_requirement == NormanRequirement.option_badges:
+ if self.options.badges != RandomizeBadges.option_completely_random:
+ max_norman_count = 4
+ if self.options.norman_requirement == NormanRequirement.option_gyms:
+ max_norman_count = 4
+
+ if self.options.norman_count.value > max_norman_count:
+ logging.warning("Pokemon Emerald: Norman requirements for Player %s (%s) are unsafe in combination with "
+ "other settings. Reducing to 4.", self.player, self.player_name)
+ self.options.norman_count.value = max_norman_count
+
+ def create_regions(self) -> None:
+ from .regions import create_regions
+ regions = create_regions(self)
+
+ tags = {"Badge", "HM", "KeyItem", "Rod", "Bike", "EventTicket"} # Tags with progression items always included
+ if self.options.overworld_items:
+ tags.add("OverworldItem")
+ if self.options.hidden_items:
+ tags.add("HiddenItem")
+ if self.options.npc_gifts:
+ tags.add("NpcGift")
+ if self.options.berry_trees:
+ tags.add("BerryTree")
+ if self.options.dexsanity:
+ tags.add("Pokedex")
+ if self.options.trainersanity:
+ tags.add("Trainer")
+ create_locations_with_tags(self, regions, tags)
+
+ self.multiworld.regions.extend(regions.values())
+
+ # Exclude locations which are always locked behind the player's goal
+ def exclude_locations(location_names: List[str]):
+ for location_name in location_names:
+ try:
+ self.multiworld.get_location(location_name,
+ self.player).progress_type = LocationProgressType.EXCLUDED
+ except KeyError:
+ continue # Location not in multiworld
+
+ if self.options.goal == Goal.option_champion:
+ # Always required to beat champion before receiving these
+ exclude_locations([
+ "Littleroot Town - S.S. Ticket from Norman",
+ "Littleroot Town - Aurora Ticket from Norman",
+ "Littleroot Town - Eon Ticket from Norman",
+ "Littleroot Town - Mystic Ticket from Norman",
+ "Littleroot Town - Old Sea Map from Norman",
+ "Ever Grande City - Champion Wallace",
+ "Meteor Falls 1F - Rival Steven",
+ "Trick House Puzzle 8 - Item",
+ ])
+
+ # Construction workers don't move until champion is defeated
+ if "Safari Zone Construction Workers" not in self.options.remove_roadblocks.value:
+ exclude_locations([
+ "Safari Zone NE - Hidden Item North",
+ "Safari Zone NE - Hidden Item East",
+ "Safari Zone NE - Item on Ledge",
+ "Safari Zone SE - Hidden Item in South Grass 1",
+ "Safari Zone SE - Hidden Item in South Grass 2",
+ "Safari Zone SE - Item in Grass",
+ ])
+ elif self.options.goal == Goal.option_steven:
+ exclude_locations([
+ "Meteor Falls 1F - Rival Steven",
+ ])
+ elif self.options.goal == Goal.option_norman:
+ # If the player sets their options such that Surf or the Balance
+ # Badge is vanilla, a very large number of locations become
+ # "post-Norman". Similarly, access to the E4 may require you to
+ # defeat Norman as an event or to get his badge, making postgame
+ # locations inaccessible. Detecting these situations isn't trivial
+ # and excluding all locations requiring Surf would be a bad idea.
+ # So for now we just won't touch it and blame the user for
+ # constructing their options in this way. Players usually expect
+ # to only partially complete their world when playing this goal
+ # anyway.
+
+ # Locations which are directly unlocked by defeating Norman.
+ exclude_locations([
+ "Petalburg Gym - Leader Norman",
+ "Petalburg Gym - Balance Badge",
+ "Petalburg Gym - TM42 from Norman",
+ "Petalburg City - HM03 from Wally's Uncle",
+ "Dewford Town - TM36 from Sludge Bomb Man",
+ "Mauville City - Basement Key from Wattson",
+ "Mauville City - TM24 from Wattson",
+ ])
+
+ def create_items(self) -> None:
+ item_locations: List[PokemonEmeraldLocation] = [
+ location
+ for location in self.multiworld.get_locations(self.player)
+ if location.address is not None
+ ]
+
+ # Filter progression items which shouldn't be shuffled into the itempool.
+ # Their locations will still exist, but event items will be placed and
+ # locked at their vanilla locations instead.
+ filter_tags = set()
+
+ if not self.options.key_items:
+ filter_tags.add("KeyItem")
+ if not self.options.rods:
+ filter_tags.add("Rod")
+ if not self.options.bikes:
+ filter_tags.add("Bike")
+ if not self.options.event_tickets:
+ filter_tags.add("EventTicket")
+
+ if self.options.badges in {RandomizeBadges.option_vanilla, RandomizeBadges.option_shuffle}:
+ filter_tags.add("Badge")
+ if self.options.hms in {RandomizeHms.option_vanilla, RandomizeHms.option_shuffle}:
+ filter_tags.add("HM")
+
+ # If Badges and HMs are set to the `shuffle` option, don't add them to
+ # the normal item pool, but do create their items and save them and
+ # their locations for use in `pre_fill` later.
+ if self.options.badges == RandomizeBadges.option_shuffle:
+ self.badge_shuffle_info = [
+ (location, self.create_item_by_code(location.default_item_code))
+ for location in [l for l in item_locations if "Badge" in l.tags]
+ ]
+ if self.options.hms == RandomizeHms.option_shuffle:
+ self.hm_shuffle_info = [
+ (location, self.create_item_by_code(location.default_item_code))
+ for location in [l for l in item_locations if "HM" in l.tags]
+ ]
+
+ # Filter down locations to actual items that will be filled and create
+ # the itempool.
+ item_locations = [location for location in item_locations if len(filter_tags & location.tags) == 0]
+ default_itempool = [self.create_item_by_code(location.default_item_code) for location in item_locations]
+
+ # Take the itempool as-is
+ if self.options.item_pool_type == ItemPoolType.option_shuffled:
+ self.multiworld.itempool += default_itempool
+
+ # Recreate the itempool from random items
+ elif self.options.item_pool_type in (ItemPoolType.option_diverse, ItemPoolType.option_diverse_balanced):
+ item_categories = ["Ball", "Heal", "Candy", "Vitamin", "EvoStone", "Money", "TM", "Held", "Misc", "Berry"]
+
+ # Count occurrences of types of vanilla items in pool
+ item_category_counter = Counter()
+ for item in default_itempool:
+ if not item.advancement:
+ item_category_counter.update([tag for tag in item.tags if tag in item_categories])
+
+ item_category_weights = [item_category_counter.get(category) for category in item_categories]
+ item_category_weights = [weight if weight is not None else 0 for weight in item_category_weights]
+
+ # Create lists of item codes that can be used to fill
+ fill_item_candidates = emerald_data.items.values()
+
+ fill_item_candidates = [item for item in fill_item_candidates if "Unique" not in item.tags]
+
+ fill_item_candidates_by_category = {category: [] for category in item_categories}
+ for item_data in fill_item_candidates:
+ for category in item_categories:
+ if category in item_data.tags:
+ fill_item_candidates_by_category[category].append(offset_item_value(item_data.item_id))
+
+ for category in fill_item_candidates_by_category:
+ fill_item_candidates_by_category[category].sort()
+
+ # Ignore vanilla occurrences and pick completely randomly
+ if self.options.item_pool_type == ItemPoolType.option_diverse:
+ item_category_weights = [
+ len(category_list)
+ for category_list in fill_item_candidates_by_category.values()
+ ]
+
+ # TMs should not have duplicates until every TM has been used already
+ all_tm_choices = fill_item_candidates_by_category["TM"].copy()
+
+ def refresh_tm_choices() -> None:
+ fill_item_candidates_by_category["TM"] = all_tm_choices.copy()
+ self.random.shuffle(fill_item_candidates_by_category["TM"])
+ refresh_tm_choices()
+
+ # Create items
+ for item in default_itempool:
+ if not item.advancement and "Unique" not in item.tags:
+ category = self.random.choices(item_categories, item_category_weights)[0]
+ if category == "TM":
+ if len(fill_item_candidates_by_category["TM"]) == 0:
+ refresh_tm_choices()
+ item_code = fill_item_candidates_by_category["TM"].pop()
+ else:
+ item_code = self.random.choice(fill_item_candidates_by_category[category])
+ item = self.create_item_by_code(item_code)
+
+ self.multiworld.itempool.append(item)
+
+ def set_rules(self) -> None:
+ from .rules import set_rules
+ set_rules(self)
+
+ def generate_basic(self) -> None:
+ # Create auth
+ # self.auth = self.random.randbytes(16) # Requires >=3.9
+ self.auth = self.random.getrandbits(16 * 8).to_bytes(16, "little")
+
+ randomize_types(self)
+ randomize_wild_encounters(self)
+ set_free_fly(self)
+ set_legendary_cave_entrances(self)
+
+ # Key items which are considered in access rules but not randomized are converted to events and placed
+ # in their vanilla locations so that the player can have them in their inventory for logic.
+ def convert_unrandomized_items_to_events(tag: str) -> None:
+ for location in self.multiworld.get_locations(self.player):
+ if location.tags is not None and tag in location.tags:
+ location.place_locked_item(self.create_event(self.item_id_to_name[location.default_item_code]))
+ location.progress_type = LocationProgressType.DEFAULT
+ location.address = None
+
+ if self.options.badges == RandomizeBadges.option_vanilla:
+ convert_unrandomized_items_to_events("Badge")
+ if self.options.hms == RandomizeHms.option_vanilla:
+ convert_unrandomized_items_to_events("HM")
+ if not self.options.rods:
+ convert_unrandomized_items_to_events("Rod")
+ if not self.options.bikes:
+ convert_unrandomized_items_to_events("Bike")
+ if not self.options.event_tickets:
+ convert_unrandomized_items_to_events("EventTicket")
+ if not self.options.key_items:
+ convert_unrandomized_items_to_events("KeyItem")
+
+ def pre_fill(self) -> None:
+ # Badges and HMs that are set to shuffle need to be placed at
+ # their own subset of locations
+ if self.options.badges == RandomizeBadges.option_shuffle:
+ badge_locations: List[PokemonEmeraldLocation]
+ badge_items: List[PokemonEmeraldItem]
+
+ # Sort order makes `fill_restrictive` try to place important badges later, which
+ # makes it less likely to have to swap at all, and more likely for swaps to work.
+ badge_locations, badge_items = [list(l) for l in zip(*self.badge_shuffle_info)]
+ badge_priority = {
+ "Knuckle Badge": 3,
+ "Balance Badge": 1,
+ "Dynamo Badge": 1,
+ "Mind Badge": 2,
+ "Heat Badge": 2,
+ "Rain Badge": 3,
+ "Stone Badge": 4,
+ "Feather Badge": 5,
+ }
+ # In the case of vanilla HMs, navigating Granite Cave is required to access more than 2 gyms,
+ # so Knuckle Badge deserves highest priority if Flash is logically required.
+ if self.options.hms == RandomizeHms.option_vanilla and \
+ self.options.require_flash in (DarkCavesRequireFlash.option_both, DarkCavesRequireFlash.option_only_granite_cave):
+ badge_priority["Knuckle Badge"] = 0
+ badge_items.sort(key=lambda item: badge_priority.get(item.name, 0))
+
+ # Un-exclude badge locations, since we need to put progression items on them
+ for location in badge_locations:
+ location.progress_type = LocationProgressType.DEFAULT \
+ if location.progress_type == LocationProgressType.EXCLUDED \
+ else location.progress_type
+
+ collection_state = self.multiworld.get_all_state(False)
+
+ # If HM shuffle is on, HMs are not placed and not in the pool, so
+ # `get_all_state` did not contain them. Collect them manually for
+ # this fill. We know that they will be included in all state after
+ # this stage.
+ if self.hm_shuffle_info is not None:
+ for _, item in self.hm_shuffle_info:
+ collection_state.collect(item)
+
+ # In specific very constrained conditions, fill_restrictive may run
+ # out of swaps before it finds a valid solution if it gets unlucky.
+ # This is a band-aid until fill/swap can reliably find those solutions.
+ attempts_remaining = 2
+ while attempts_remaining > 0:
+ attempts_remaining -= 1
+ self.random.shuffle(badge_locations)
+ try:
+ fill_restrictive(self.multiworld, collection_state, badge_locations, badge_items,
+ single_player_placement=True, lock=True, allow_excluded=True)
+ break
+ except FillError as exc:
+ if attempts_remaining == 0:
+ raise exc
+
+ logging.debug(f"Failed to shuffle badges for player {self.player}. Retrying.")
+ continue
+
+ # Badges are guaranteed to be either placed or in the multiworld's itempool now
+ if self.options.hms == RandomizeHms.option_shuffle:
+ hm_locations: List[PokemonEmeraldLocation]
+ hm_items: List[PokemonEmeraldItem]
+
+ # Sort order makes `fill_restrictive` try to place important HMs later, which
+ # makes it less likely to have to swap at all, and more likely for swaps to work.
+ hm_locations, hm_items = [list(l) for l in zip(*self.hm_shuffle_info)]
+ hm_priority = {
+ "HM05 Flash": 3,
+ "HM03 Surf": 1,
+ "HM06 Rock Smash": 1,
+ "HM08 Dive": 2,
+ "HM04 Strength": 2,
+ "HM07 Waterfall": 3,
+ "HM01 Cut": 4,
+ "HM02 Fly": 5,
+ }
+ # In the case of vanilla badges, navigating Granite Cave is required to access more than 2 gyms,
+ # so Flash deserves highest priority if it's logically required.
+ if self.options.badges == RandomizeBadges.option_vanilla and \
+ self.options.require_flash in (DarkCavesRequireFlash.option_both, DarkCavesRequireFlash.option_only_granite_cave):
+ hm_priority["HM05 Flash"] = 0
+ hm_items.sort(key=lambda item: hm_priority.get(item.name, 0))
+
+ # Un-exclude HM locations, since we need to put progression items on them
+ for location in hm_locations:
+ location.progress_type = LocationProgressType.DEFAULT \
+ if location.progress_type == LocationProgressType.EXCLUDED \
+ else location.progress_type
+
+ collection_state = self.multiworld.get_all_state(False)
+
+ # In specific very constrained conditions, fill_restrictive may run
+ # out of swaps before it finds a valid solution if it gets unlucky.
+ # This is a band-aid until fill/swap can reliably find those solutions.
+ attempts_remaining = 2
+ while attempts_remaining > 0:
+ attempts_remaining -= 1
+ self.random.shuffle(hm_locations)
+ try:
+ fill_restrictive(self.multiworld, collection_state, hm_locations, hm_items,
+ single_player_placement=True, lock=True, allow_excluded=True)
+ break
+ except FillError as exc:
+ if attempts_remaining == 0:
+ raise exc
+
+ logging.debug(f"Failed to shuffle HMs for player {self.player}. Retrying.")
+ continue
+
+ def generate_output(self, output_directory: str) -> None:
+ self.modified_trainers = copy.deepcopy(emerald_data.trainers)
+ self.modified_tmhm_moves = copy.deepcopy(emerald_data.tmhm_moves)
+ self.modified_legendary_encounters = copy.deepcopy(emerald_data.legendary_encounters)
+ self.modified_misc_pokemon = copy.deepcopy(emerald_data.misc_pokemon)
+ self.modified_starters = copy.deepcopy(emerald_data.starters)
+
+ # Modify catch rate
+ min_catch_rate = min(self.options.min_catch_rate.value, 255)
+ for species in self.modified_species.values():
+ species.catch_rate = max(species.catch_rate, min_catch_rate)
+
+ # Modify TM moves
+ if self.options.tm_tutor_moves:
+ new_moves: Set[int] = set()
+
+ for i in range(50):
+ new_move = get_random_move(self.random, new_moves | self.blacklisted_moves)
+ new_moves.add(new_move)
+ self.modified_tmhm_moves[i] = new_move
+
+ randomize_abilities(self)
+ randomize_learnsets(self)
+ randomize_tm_hm_compatibility(self)
+ randomize_legendary_encounters(self)
+ randomize_misc_pokemon(self)
+ randomize_opponent_parties(self)
+ randomize_starters(self)
+
+ patch = PokemonEmeraldProcedurePatch(player=self.player, player_name=self.player_name)
+ patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, "data/base_patch.bsdiff4"))
+ write_tokens(self, patch)
+
+ del self.modified_trainers
+ del self.modified_tmhm_moves
+ del self.modified_legendary_encounters
+ del self.modified_misc_pokemon
+ del self.modified_starters
+ del self.modified_species
+
+ # Write Output
+ out_file_name = self.multiworld.get_out_file_name_base(self.player)
+ patch.write(os.path.join(output_directory, f"{out_file_name}{patch.patch_file_ending}"))
+
+ def write_spoiler(self, spoiler_handle: TextIO):
+ if self.options.dexsanity:
+ from collections import defaultdict
+
+ spoiler_handle.write(f"\n\nWild Pokemon ({self.player_name}):\n\n")
+
+ species_maps = defaultdict(set)
+ for map in self.modified_maps.values():
+ if map.land_encounters is not None:
+ for encounter in map.land_encounters.slots:
+ species_maps[encounter].add(map.name[4:])
+
+ if map.water_encounters is not None:
+ for encounter in map.water_encounters.slots:
+ species_maps[encounter].add(map.name[4:])
+
+ if map.fishing_encounters is not None:
+ for encounter in map.fishing_encounters.slots:
+ species_maps[encounter].add(map.name[4:])
+
+ lines = [f"{emerald_data.species[species].label}: {', '.join(maps)}\n"
+ for species, maps in species_maps.items()]
+ lines.sort()
+ for line in lines:
+ spoiler_handle.write(line)
+
+ del self.modified_maps
+
+ def extend_hint_information(self, hint_data):
+ if self.options.dexsanity:
+ from collections import defaultdict
+
+ slot_to_rod = {
+ 0: "_OLD_ROD",
+ 1: "_OLD_ROD",
+ 2: "_GOOD_ROD",
+ 3: "_GOOD_ROD",
+ 4: "_GOOD_ROD",
+ 5: "_SUPER_ROD",
+ 6: "_SUPER_ROD",
+ 7: "_SUPER_ROD",
+ 8: "_SUPER_ROD",
+ 9: "_SUPER_ROD",
+ }
+
+ species_maps = defaultdict(set)
+ for map in self.modified_maps.values():
+ if map.land_encounters is not None:
+ for encounter in map.land_encounters.slots:
+ species_maps[encounter].add(map.name[4:] + "_GRASS")
+
+ if map.water_encounters is not None:
+ for encounter in map.water_encounters.slots:
+ species_maps[encounter].add(map.name[4:] + "_WATER")
+
+ if map.fishing_encounters is not None:
+ for slot, encounter in enumerate(map.fishing_encounters.slots):
+ species_maps[encounter].add(map.name[4:] + slot_to_rod[slot])
+
+ hint_data[self.player] = {
+ self.location_name_to_id[f"Pokedex - {emerald_data.species[species].label}"]: ", ".join(maps)
+ for species, maps in species_maps.items()
+ }
+
+ def modify_multidata(self, multidata: Dict[str, Any]):
+ import base64
+ multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = multidata["connect_names"][self.player_name]
+
+ def fill_slot_data(self) -> Dict[str, Any]:
+ slot_data = self.options.as_dict(
+ "goal",
+ "badges",
+ "hms",
+ "key_items",
+ "bikes",
+ "event_tickets",
+ "rods",
+ "overworld_items",
+ "hidden_items",
+ "npc_gifts",
+ "berry_trees",
+ "require_itemfinder",
+ "require_flash",
+ "elite_four_requirement",
+ "elite_four_count",
+ "norman_requirement",
+ "norman_count",
+ "legendary_hunt_catch",
+ "legendary_hunt_count",
+ "extra_boulders",
+ "remove_roadblocks",
+ "allowed_legendary_hunt_encounters",
+ "extra_bumpy_slope",
+ "free_fly_location",
+ "remote_items",
+ "dexsanity",
+ "trainersanity",
+ "modify_118",
+ "death_link",
+ )
+ slot_data["free_fly_location_id"] = self.free_fly_location_id
+ slot_data["hm_requirements"] = self.hm_requirements
+ return slot_data
+
+ def create_item(self, name: str) -> PokemonEmeraldItem:
+ return self.create_item_by_code(self.item_name_to_id[name])
+
+ def create_item_by_code(self, item_code: int) -> PokemonEmeraldItem:
+ return PokemonEmeraldItem(
+ self.item_id_to_name[item_code],
+ get_item_classification(item_code),
+ item_code,
+ self.player
+ )
+
+ def create_event(self, name: str) -> PokemonEmeraldItem:
+ return PokemonEmeraldItem(
+ name,
+ ItemClassification.progression,
+ None,
+ self.player
+ )
diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py
new file mode 100644
index 000000000000..a830957e9c7e
--- /dev/null
+++ b/worlds/pokemon_emerald/client.py
@@ -0,0 +1,694 @@
+import asyncio
+import copy
+import orjson
+import random
+import time
+from typing import TYPE_CHECKING, Optional, Dict, Set, Tuple
+import uuid
+
+from NetUtils import ClientStatus
+from Options import Toggle
+import Utils
+import worlds._bizhawk as bizhawk
+from worlds._bizhawk.client import BizHawkClient
+
+from .data import BASE_OFFSET, POKEDEX_OFFSET, data
+from .options import Goal, RemoteItems
+from .util import pokemon_data_to_json, json_to_pokemon_data
+
+if TYPE_CHECKING:
+ from worlds._bizhawk.context import BizHawkClientContext
+
+
+EXPECTED_ROM_NAME = "pokemon emerald version / AP 5"
+
+DEFEATED_WALLACE_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_WALLACE"]
+DEFEATED_STEVEN_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_STEVEN"]
+DEFEATED_NORMAN_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_NORMAN_1"]
+
+# These flags are communicated to the tracker as a bitfield using this order.
+# Modifying the order will cause undetectable autotracking issues.
+TRACKER_EVENT_FLAGS = [
+ "FLAG_DEFEATED_RUSTBORO_GYM",
+ "FLAG_DEFEATED_DEWFORD_GYM",
+ "FLAG_DEFEATED_MAUVILLE_GYM",
+ "FLAG_DEFEATED_LAVARIDGE_GYM",
+ "FLAG_DEFEATED_PETALBURG_GYM",
+ "FLAG_DEFEATED_FORTREE_GYM",
+ "FLAG_DEFEATED_MOSSDEEP_GYM",
+ "FLAG_DEFEATED_SOOTOPOLIS_GYM",
+ "FLAG_RECEIVED_POKENAV", # Talk to Mr. Stone
+ "FLAG_DELIVERED_STEVEN_LETTER",
+ "FLAG_DELIVERED_DEVON_GOODS",
+ "FLAG_HIDE_ROUTE_119_TEAM_AQUA_SHELLY", # Clear Weather Institute
+ "FLAG_MET_ARCHIE_METEOR_FALLS", # Magma steals meteorite
+ "FLAG_GROUDON_AWAKENED_MAGMA_HIDEOUT", # Clear Magma Hideout
+ "FLAG_MET_TEAM_AQUA_HARBOR", # Aqua steals submarine
+ "FLAG_TEAM_AQUA_ESCAPED_IN_SUBMARINE", # Clear Aqua Hideout
+ "FLAG_HIDE_MOSSDEEP_CITY_SPACE_CENTER_MAGMA_NOTE", # Clear Space Center
+ "FLAG_KYOGRE_ESCAPED_SEAFLOOR_CAVERN",
+ "FLAG_HIDE_SKY_PILLAR_TOP_RAYQUAZA", # Rayquaza departs for Sootopolis
+ "FLAG_OMIT_DIVE_FROM_STEVEN_LETTER", # Steven gives Dive HM (clears seafloor cavern grunt)
+ "FLAG_IS_CHAMPION",
+ "FLAG_PURCHASED_HARBOR_MAIL",
+ "FLAG_REGI_DOORS_OPENED",
+ "FLAG_RETURNED_DEVON_GOODS",
+ "FLAG_DOCK_REJECTED_DEVON_GOODS",
+ "FLAG_DEFEATED_EVIL_TEAM_MT_CHIMNEY",
+ "FLAG_WINGULL_SENT_ON_ERRAND",
+ "FLAG_WINGULL_DELIVERED_MAIL",
+ "FLAG_MET_PRETTY_PETAL_SHOP_OWNER",
+]
+EVENT_FLAG_MAP = {data.constants[flag_name]: flag_name for flag_name in TRACKER_EVENT_FLAGS}
+
+KEY_LOCATION_FLAGS = [
+ "NPC_GIFT_RECEIVED_HM_CUT",
+ "NPC_GIFT_RECEIVED_HM_FLY",
+ "NPC_GIFT_RECEIVED_HM_SURF",
+ "NPC_GIFT_RECEIVED_HM_STRENGTH",
+ "NPC_GIFT_RECEIVED_HM_FLASH",
+ "NPC_GIFT_RECEIVED_HM_ROCK_SMASH",
+ "NPC_GIFT_RECEIVED_HM_WATERFALL",
+ "NPC_GIFT_RECEIVED_HM_DIVE",
+ "NPC_GIFT_RECEIVED_ACRO_BIKE",
+ "NPC_GIFT_RECEIVED_WAILMER_PAIL",
+ "NPC_GIFT_RECEIVED_DEVON_GOODS_RUSTURF_TUNNEL",
+ "NPC_GIFT_RECEIVED_LETTER",
+ "NPC_GIFT_RECEIVED_METEORITE",
+ "NPC_GIFT_RECEIVED_GO_GOGGLES",
+ "NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON",
+ "NPC_GIFT_RECEIVED_ITEMFINDER",
+ "NPC_GIFT_RECEIVED_DEVON_SCOPE",
+ "NPC_GIFT_RECEIVED_MAGMA_EMBLEM",
+ "NPC_GIFT_RECEIVED_POKEBLOCK_CASE",
+ "NPC_GIFT_RECEIVED_SS_TICKET",
+ "HIDDEN_ITEM_ABANDONED_SHIP_RM_2_KEY",
+ "HIDDEN_ITEM_ABANDONED_SHIP_RM_1_KEY",
+ "HIDDEN_ITEM_ABANDONED_SHIP_RM_4_KEY",
+ "HIDDEN_ITEM_ABANDONED_SHIP_RM_6_KEY",
+ "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_2_SCANNER",
+ "ITEM_ABANDONED_SHIP_CAPTAINS_OFFICE_STORAGE_KEY",
+ "NPC_GIFT_RECEIVED_OLD_ROD",
+ "NPC_GIFT_RECEIVED_GOOD_ROD",
+ "NPC_GIFT_RECEIVED_SUPER_ROD",
+ "NPC_GIFT_RECEIVED_EON_TICKET",
+ "NPC_GIFT_RECEIVED_AURORA_TICKET",
+ "NPC_GIFT_RECEIVED_MYSTIC_TICKET",
+ "NPC_GIFT_RECEIVED_OLD_SEA_MAP",
+]
+KEY_LOCATION_FLAG_MAP = {data.locations[location_name].flag: location_name for location_name in KEY_LOCATION_FLAGS}
+
+# .lower() keys for backward compatibility between 0.4.5 and 0.4.6
+LEGENDARY_NAMES = {k.lower(): v for k, v in {
+ "Groudon": "GROUDON",
+ "Kyogre": "KYOGRE",
+ "Rayquaza": "RAYQUAZA",
+ "Latias": "LATIAS",
+ "Latios": "LATIOS",
+ "Regirock": "REGIROCK",
+ "Regice": "REGICE",
+ "Registeel": "REGISTEEL",
+ "Mew": "MEW",
+ "Deoxys": "DEOXYS",
+ "Ho-Oh": "HO_OH",
+ "Lugia": "LUGIA",
+}.items()}
+
+DEFEATED_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_DEFEATED_{name}"]: name for name in LEGENDARY_NAMES.values()}
+CAUGHT_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_CAUGHT_{name}"]: name for name in LEGENDARY_NAMES.values()}
+
+
+class PokemonEmeraldClient(BizHawkClient):
+ game = "Pokemon Emerald"
+ system = "GBA"
+ patch_suffix = ".apemerald"
+ local_checked_locations: Set[int]
+ local_set_events: Dict[str, bool]
+ local_found_key_items: Dict[str, bool]
+ local_defeated_legendaries: Dict[str, bool]
+ goal_flag: Optional[int]
+
+ wonder_trade_update_event: asyncio.Event
+ latest_wonder_trade_reply: dict
+ wonder_trade_cooldown: int
+ wonder_trade_cooldown_timer: int
+
+ death_counter: Optional[int]
+ previous_death_link: float
+ ignore_next_death_link: bool
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.local_checked_locations = set()
+ self.local_set_events = {}
+ self.local_found_key_items = {}
+ self.local_defeated_legendaries = {}
+ self.goal_flag = None
+ self.wonder_trade_update_event = asyncio.Event()
+ self.wonder_trade_cooldown = 5000
+ self.wonder_trade_cooldown_timer = 0
+ self.death_counter = None
+ self.previous_death_link = 0
+ self.ignore_next_death_link = False
+
+ async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
+ from CommonClient import logger
+
+ try:
+ # Check ROM name/patch version
+ rom_name_bytes = ((await bizhawk.read(ctx.bizhawk_ctx, [(0x108, 32, "ROM")]))[0])
+ rom_name = bytes([byte for byte in rom_name_bytes if byte != 0]).decode("ascii")
+ if not rom_name.startswith("pokemon emerald version"):
+ return False
+ if rom_name == "pokemon emerald version":
+ logger.info("ERROR: You appear to be running an unpatched version of Pokemon Emerald. "
+ "You need to generate a patch file and use it to create a patched ROM.")
+ return False
+ if rom_name != EXPECTED_ROM_NAME:
+ logger.info("ERROR: The patch file used to create this ROM is not compatible with "
+ "this client. Double check your client version against the version being "
+ "used by the generator.")
+ return False
+ except UnicodeDecodeError:
+ return False
+ except bizhawk.RequestFailedError:
+ return False # Should verify on the next pass
+
+ ctx.game = self.game
+ ctx.items_handling = 0b001
+ ctx.want_slot_data = True
+ ctx.watcher_timeout = 0.125
+
+ self.death_counter = None
+ self.previous_death_link = 0
+ self.ignore_next_death_link = False
+
+ return True
+
+ async def set_auth(self, ctx: "BizHawkClientContext") -> None:
+ import base64
+ auth_raw = (await bizhawk.read(ctx.bizhawk_ctx, [(data.rom_addresses["gArchipelagoInfo"], 16, "ROM")]))[0]
+ ctx.auth = base64.b64encode(auth_raw).decode("utf-8")
+
+ async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
+ if ctx.server is None or ctx.server.socket.closed or ctx.slot_data is None:
+ return
+
+ if ctx.slot_data["goal"] == Goal.option_champion:
+ self.goal_flag = DEFEATED_WALLACE_FLAG
+ elif ctx.slot_data["goal"] == Goal.option_steven:
+ self.goal_flag = DEFEATED_STEVEN_FLAG
+ elif ctx.slot_data["goal"] == Goal.option_norman:
+ self.goal_flag = DEFEATED_NORMAN_FLAG
+ elif ctx.slot_data["goal"] == Goal.option_legendary_hunt:
+ self.goal_flag = None
+
+ if ctx.slot_data["remote_items"] == RemoteItems.option_true and not ctx.items_handling & 0b010:
+ ctx.items_handling = 0b011
+ Utils.async_start(ctx.send_msgs([{
+ "cmd": "ConnectUpdate",
+ "items_handling": ctx.items_handling
+ }]))
+
+ # Need to make sure items handling updates and we get the correct list of received items
+ # before continuing. Otherwise we might give some duplicate items and skip others.
+ # Should patch remote_items option value into the ROM in the future to guarantee we get the
+ # right item list before entering this part of the code
+ await asyncio.sleep(0.75)
+ return
+
+ try:
+ guards: Dict[str, Tuple[int, bytes, str]] = {}
+
+ # Checks that the player is in the overworld
+ guards["IN OVERWORLD"] = (
+ data.ram_addresses["gMain"] + 4,
+ (data.ram_addresses["CB2_Overworld"] + 1).to_bytes(4, "little"),
+ "System Bus"
+ )
+
+ # Read save block addresses
+ read_result = await bizhawk.read(
+ ctx.bizhawk_ctx,
+ [
+ (data.ram_addresses["gSaveBlock1Ptr"], 4, "System Bus"),
+ (data.ram_addresses["gSaveBlock2Ptr"], 4, "System Bus"),
+ ]
+ )
+
+ # Checks that the save data hasn't moved
+ guards["SAVE BLOCK 1"] = (data.ram_addresses["gSaveBlock1Ptr"], read_result[0], "System Bus")
+ guards["SAVE BLOCK 2"] = (data.ram_addresses["gSaveBlock2Ptr"], read_result[1], "System Bus")
+
+ sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
+ sb2_address = int.from_bytes(guards["SAVE BLOCK 2"][1], "little")
+
+ await self.handle_death_link(ctx, guards)
+ await self.handle_received_items(ctx, guards)
+ await self.handle_wonder_trade(ctx, guards)
+
+ # Read flags in 2 chunks
+ read_result = await bizhawk.guarded_read(
+ ctx.bizhawk_ctx,
+ [(sb1_address + 0x1450, 0x96, "System Bus")], # Flags
+ [guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]]
+ )
+ if read_result is None: # Not in overworld, or save block moved
+ return
+ flag_bytes = read_result[0]
+
+ read_result = await bizhawk.guarded_read(
+ ctx.bizhawk_ctx,
+ [(sb1_address + 0x14E6, 0x96, "System Bus")], # Flags continued
+ [guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]]
+ )
+ if read_result is not None:
+ flag_bytes += read_result[0]
+
+ # Read pokedex flags
+ pokedex_caught_bytes = bytes(0)
+ if ctx.slot_data["dexsanity"] == Toggle.option_true:
+ # Read pokedex flags
+ read_result = await bizhawk.guarded_read(
+ ctx.bizhawk_ctx,
+ [(sb2_address + 0x28, 0x34, "System Bus")],
+ [guards["IN OVERWORLD"], guards["SAVE BLOCK 2"]]
+ )
+ if read_result is not None:
+ pokedex_caught_bytes = read_result[0]
+
+ game_clear = False
+ local_checked_locations = set()
+ local_set_events = {flag_name: False for flag_name in TRACKER_EVENT_FLAGS}
+ local_found_key_items = {location_name: False for location_name in KEY_LOCATION_FLAGS}
+ defeated_legendaries = {legendary_name: False for legendary_name in LEGENDARY_NAMES.values()}
+ caught_legendaries = {legendary_name: False for legendary_name in LEGENDARY_NAMES.values()}
+
+ # Check set flags
+ for byte_i, byte in enumerate(flag_bytes):
+ for i in range(8):
+ if byte & (1 << i) != 0:
+ flag_id = byte_i * 8 + i
+
+ location_id = flag_id + BASE_OFFSET
+ if location_id in ctx.server_locations:
+ local_checked_locations.add(location_id)
+
+ if flag_id == self.goal_flag:
+ game_clear = True
+
+ if flag_id in DEFEATED_LEGENDARY_FLAG_MAP:
+ defeated_legendaries[DEFEATED_LEGENDARY_FLAG_MAP[flag_id]] = True
+
+ if flag_id in CAUGHT_LEGENDARY_FLAG_MAP:
+ caught_legendaries[CAUGHT_LEGENDARY_FLAG_MAP[flag_id]] = True
+
+ if flag_id in EVENT_FLAG_MAP:
+ local_set_events[EVENT_FLAG_MAP[flag_id]] = True
+
+ if flag_id in KEY_LOCATION_FLAG_MAP:
+ local_found_key_items[KEY_LOCATION_FLAG_MAP[flag_id]] = True
+
+ # Check pokedex
+ if ctx.slot_data["dexsanity"] == Toggle.option_true:
+ for byte_i, byte in enumerate(pokedex_caught_bytes):
+ for i in range(8):
+ if byte & (1 << i) != 0:
+ dex_number = (byte_i * 8 + i) + 1
+
+ location_id = dex_number + BASE_OFFSET + POKEDEX_OFFSET
+ if location_id in ctx.server_locations:
+ local_checked_locations.add(location_id)
+
+ # Count legendary hunt flags
+ if ctx.slot_data["goal"] == Goal.option_legendary_hunt:
+ # If legendary hunt doesn't require catching, add defeated legendaries to caught_legendaries
+ if ctx.slot_data["legendary_hunt_catch"] == Toggle.option_false:
+ for legendary, is_defeated in defeated_legendaries.items():
+ if is_defeated:
+ caught_legendaries[legendary] = True
+
+ num_caught = 0
+ for legendary, is_caught in caught_legendaries.items():
+ if is_caught and legendary in [LEGENDARY_NAMES[name.lower()] for name in ctx.slot_data["allowed_legendary_hunt_encounters"]]:
+ num_caught += 1
+
+ if num_caught >= ctx.slot_data["legendary_hunt_count"]:
+ game_clear = True
+
+ # Send locations
+ if local_checked_locations != self.local_checked_locations:
+ self.local_checked_locations = local_checked_locations
+
+ if local_checked_locations is not None:
+ await ctx.send_msgs([{
+ "cmd": "LocationChecks",
+ "locations": list(local_checked_locations),
+ }])
+
+ # Send game clear
+ if not ctx.finished_game and game_clear:
+ await ctx.send_msgs([{
+ "cmd": "StatusUpdate",
+ "status": ClientStatus.CLIENT_GOAL,
+ }])
+
+ # Send tracker event flags
+ if local_set_events != self.local_set_events and ctx.slot is not None:
+ event_bitfield = 0
+ for i, flag_name in enumerate(TRACKER_EVENT_FLAGS):
+ if local_set_events[flag_name]:
+ event_bitfield |= 1 << i
+
+ await ctx.send_msgs([{
+ "cmd": "Set",
+ "key": f"pokemon_emerald_events_{ctx.team}_{ctx.slot}",
+ "default": 0,
+ "want_reply": False,
+ "operations": [{"operation": "or", "value": event_bitfield}],
+ }])
+ self.local_set_events = local_set_events
+
+ if local_found_key_items != self.local_found_key_items:
+ key_bitfield = 0
+ for i, location_name in enumerate(KEY_LOCATION_FLAGS):
+ if local_found_key_items[location_name]:
+ key_bitfield |= 1 << i
+
+ await ctx.send_msgs([{
+ "cmd": "Set",
+ "key": f"pokemon_emerald_keys_{ctx.team}_{ctx.slot}",
+ "default": 0,
+ "want_reply": False,
+ "operations": [{"operation": "or", "value": key_bitfield}],
+ }])
+ self.local_found_key_items = local_found_key_items
+
+ if ctx.slot_data["goal"] == Goal.option_legendary_hunt:
+ if caught_legendaries != self.local_defeated_legendaries and ctx.slot is not None:
+ legendary_bitfield = 0
+ for i, legendary_name in enumerate(LEGENDARY_NAMES.values()):
+ if caught_legendaries[legendary_name]:
+ legendary_bitfield |= 1 << i
+
+ await ctx.send_msgs([{
+ "cmd": "Set",
+ "key": f"pokemon_emerald_legendaries_{ctx.team}_{ctx.slot}",
+ "default": 0,
+ "want_reply": False,
+ "operations": [{"operation": "or", "value": legendary_bitfield}],
+ }])
+ self.local_defeated_legendaries = caught_legendaries
+ except bizhawk.RequestFailedError:
+ # Exit handler and return to main loop to reconnect
+ pass
+
+ async def handle_death_link(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
+ """
+ Checks whether the player has died while connected and sends a death link if so. Queues a death link in the game
+ if a new one has been received.
+ """
+ if ctx.slot_data.get("death_link", Toggle.option_false) == Toggle.option_true:
+ if "DeathLink" not in ctx.tags:
+ await ctx.update_death_link(True)
+ self.previous_death_link = ctx.last_death_link
+
+ sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
+ sb2_address = int.from_bytes(guards["SAVE BLOCK 2"][1], "little")
+
+ read_result = await bizhawk.guarded_read(
+ ctx.bizhawk_ctx, [
+ (sb1_address + 0x177C + (52 * 4), 4, "System Bus"), # White out stat
+ (sb1_address + 0x177C + (22 * 4), 4, "System Bus"), # Canary stat
+ (sb2_address + 0xAC, 4, "System Bus"), # Encryption key
+ ],
+ [guards["SAVE BLOCK 1"], guards["SAVE BLOCK 2"]]
+ )
+ if read_result is None: # Save block moved
+ return
+
+ encryption_key = int.from_bytes(read_result[2], "little")
+ times_whited_out = int.from_bytes(read_result[0], "little") ^ encryption_key
+
+ # Canary is an unused stat that will always be 0. There is a low chance that we've done this read on
+ # a frame where the user has just entered a battle and the encryption key has been changed, but the data
+ # has not yet been encrypted with the new key. If `canary` is 0, `times_whited_out` is correct.
+ canary = int.from_bytes(read_result[1], "little") ^ encryption_key
+
+ # Skip all deathlink code if save is not yet loaded (encryption key is zero) or white out stat not yet
+ # initialized (starts at 100 as a safety for subtracting values from an unsigned int).
+ if canary == 0 and encryption_key != 0 and times_whited_out >= 100:
+ if self.previous_death_link != ctx.last_death_link:
+ self.previous_death_link = ctx.last_death_link
+ if self.ignore_next_death_link:
+ self.ignore_next_death_link = False
+ else:
+ await bizhawk.write(
+ ctx.bizhawk_ctx,
+ [(data.ram_addresses["gArchipelagoDeathLinkQueued"], [1], "System Bus")]
+ )
+
+ if self.death_counter is None:
+ self.death_counter = times_whited_out
+ elif times_whited_out > self.death_counter:
+ await ctx.send_death(f"{ctx.player_names[ctx.slot]} is out of usable POKéMON! "
+ f"{ctx.player_names[ctx.slot]} whited out!")
+ self.ignore_next_death_link = True
+ self.death_counter = times_whited_out
+
+ async def handle_received_items(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
+ """
+ Checks the index of the most recently received item and whether the item queue is full. Writes the next item
+ into the game if necessary.
+ """
+ received_item_address = data.ram_addresses["gArchipelagoReceivedItem"]
+
+ sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
+
+ read_result = await bizhawk.guarded_read(
+ ctx.bizhawk_ctx,
+ [
+ (sb1_address + 0x3778, 2, "System Bus"), # Number of received items
+ (received_item_address + 4, 1, "System Bus") # Received item struct full?
+ ],
+ [guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]]
+ )
+ if read_result is None: # Not in overworld, or save block moved
+ return
+
+ num_received_items = int.from_bytes(read_result[0], "little")
+ received_item_is_empty = read_result[1][0] == 0
+
+ # If the game hasn't received all items yet and the received item struct doesn't contain an item, then
+ # fill it with the next item
+ if num_received_items < len(ctx.items_received) and received_item_is_empty:
+ next_item = ctx.items_received[num_received_items]
+ should_display = 1 if next_item.flags & 1 or next_item.player == ctx.slot else 0
+ await bizhawk.write(ctx.bizhawk_ctx, [
+ (received_item_address + 0, (next_item.item - BASE_OFFSET).to_bytes(2, "little"), "System Bus"),
+ (received_item_address + 2, (num_received_items + 1).to_bytes(2, "little"), "System Bus"),
+ (received_item_address + 4, [1], "System Bus"),
+ (received_item_address + 5, [should_display], "System Bus"),
+ ])
+
+ async def handle_wonder_trade(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
+ """
+ Read wonder trade status from save data and either send a queued pokemon to data storage or attempt to retrieve
+ one from data storage and write it into the save.
+ """
+ from CommonClient import logger
+
+ sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
+
+ read_result = await bizhawk.guarded_read(
+ ctx.bizhawk_ctx,
+ [
+ (sb1_address + 0x377C, 0x50, "System Bus"), # Wonder trade data
+ (sb1_address + 0x37CC, 1, "System Bus"), # Is wonder trade sent
+ ],
+ [guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]]
+ )
+
+ if read_result is not None:
+ wonder_trade_pokemon_data = read_result[0]
+ trade_is_sent = read_result[1][0]
+
+ if trade_is_sent == 0 and wonder_trade_pokemon_data[19] == 2:
+ # Game has wonder trade data to send. Send it to data storage, remove it from the game's memory,
+ # and mark that the game is waiting on receiving a trade
+ Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data)))
+ await bizhawk.write(ctx.bizhawk_ctx, [
+ (sb1_address + 0x377C, bytes(0x50), "System Bus"),
+ (sb1_address + 0x37CC, [1], "System Bus"),
+ ])
+ elif trade_is_sent != 0 and wonder_trade_pokemon_data[19] != 2:
+ # Game is waiting on receiving a trade. See if there are any available trades that were not
+ # sent by this player, and if so, try to receive one.
+ if self.wonder_trade_cooldown_timer <= 0 and f"pokemon_wonder_trades_{ctx.team}" in ctx.stored_data:
+ if any(item[0] != ctx.slot
+ for key, item in ctx.stored_data.get(f"pokemon_wonder_trades_{ctx.team}", {}).items()
+ if key != "_lock" and orjson.loads(item[1])["species"] <= 386):
+ received_trade = await self.wonder_trade_receive(ctx)
+ if received_trade is None:
+ self.wonder_trade_cooldown_timer = self.wonder_trade_cooldown
+ self.wonder_trade_cooldown *= 2
+ self.wonder_trade_cooldown += random.randrange(0, 500)
+ else:
+ await bizhawk.write(ctx.bizhawk_ctx, [
+ (sb1_address + 0x377C, json_to_pokemon_data(received_trade), "System Bus"),
+ ])
+ logger.info("Wonder trade received!")
+ self.wonder_trade_cooldown = 5000
+
+ else:
+ # Very approximate "time since last loop", but extra delay is fine for this
+ self.wonder_trade_cooldown_timer -= int(ctx.watcher_timeout * 1000)
+
+ async def wonder_trade_acquire(self, ctx: "BizHawkClientContext", keep_trying: bool = False) -> Optional[dict]:
+ """
+ Acquires a lock on the `pokemon_wonder_trades_{ctx.team}` key in
+ datastorage. Locking the key means you have exclusive access
+ to modifying the value until you unlock it or the key expires (5
+ seconds).
+
+ If `keep_trying` is `True`, it will keep trying to acquire the lock
+ until successful. Otherwise it will return `None` if it fails to
+ acquire the lock.
+ """
+ while not ctx.exit_event.is_set():
+ lock = int(time.time_ns() / 1000000)
+ message_uuid = str(uuid.uuid4())
+ await ctx.send_msgs([{
+ "cmd": "Set",
+ "key": f"pokemon_wonder_trades_{ctx.team}",
+ "default": {"_lock": 0},
+ "want_reply": True,
+ "operations": [{"operation": "update", "value": {"_lock": lock}}],
+ "uuid": message_uuid,
+ }])
+
+ self.wonder_trade_update_event.clear()
+ try:
+ await asyncio.wait_for(self.wonder_trade_update_event.wait(), 5)
+ except asyncio.TimeoutError:
+ if not keep_trying:
+ return None
+ continue
+
+ reply = copy.deepcopy(self.latest_wonder_trade_reply)
+
+ # Make sure the most recently received update was triggered by our lock attempt
+ if reply.get("uuid", None) != message_uuid:
+ if not keep_trying:
+ return None
+ await asyncio.sleep(self.wonder_trade_cooldown)
+ continue
+
+ # Make sure the current value of the lock is what we set it to
+ # (I think this should theoretically never run)
+ if reply["value"]["_lock"] != lock:
+ if not keep_trying:
+ return None
+ await asyncio.sleep(self.wonder_trade_cooldown)
+ continue
+
+ # Make sure that the lock value we replaced is at least 5 seconds old
+ # If it was unlocked before our change, its value was 0 and it will look decades old
+ if lock - reply["original_value"]["_lock"] < 5000:
+ # Multiple clients trying to lock the key may get stuck in a loop of checking the lock
+ # by trying to set it, which will extend its expiration. So if we see that the lock was
+ # too new when we replaced it, we should wait for increasingly longer periods so that
+ # eventually the lock will expire and a client will acquire it.
+ self.wonder_trade_cooldown *= 2
+ self.wonder_trade_cooldown += random.randrange(0, 500)
+
+ if not keep_trying:
+ self.wonder_trade_cooldown_timer = self.wonder_trade_cooldown
+ return None
+ await asyncio.sleep(self.wonder_trade_cooldown)
+ continue
+
+ # We have the lock, reset the cooldown and return
+ self.wonder_trade_cooldown = 5000
+ return reply
+
+ async def wonder_trade_send(self, ctx: "BizHawkClientContext", data: str) -> None:
+ """
+ Sends a wonder trade pokemon to data storage
+ """
+ from CommonClient import logger
+
+ reply = await self.wonder_trade_acquire(ctx, True)
+
+ wonder_trade_slot = 0
+ while str(wonder_trade_slot) in reply["value"]:
+ wonder_trade_slot += 1
+
+ await ctx.send_msgs([{
+ "cmd": "Set",
+ "key": f"pokemon_wonder_trades_{ctx.team}",
+ "default": {"_lock": 0},
+ "operations": [{"operation": "update", "value": {
+ "_lock": 0,
+ str(wonder_trade_slot): (ctx.slot, data),
+ }}],
+ }])
+
+ logger.info("Wonder trade sent! We'll notify you here when a trade has been found.")
+
+ async def wonder_trade_receive(self, ctx: "BizHawkClientContext") -> Optional[str]:
+ """
+ Tries to pop a pokemon out of the wonder trades. Returns `None` if
+ for some reason it can't immediately remove a compatible pokemon.
+ """
+ reply = await self.wonder_trade_acquire(ctx)
+
+ if reply is None:
+ return None
+
+ candidate_slots = [
+ int(slot)
+ for slot in reply["value"]
+ if slot != "_lock" \
+ and reply["value"][slot][0] != ctx.slot \
+ and orjson.loads(reply["value"][slot][1])["species"] <= 386
+ ]
+
+ if len(candidate_slots) == 0:
+ await ctx.send_msgs([{
+ "cmd": "Set",
+ "key": f"pokemon_wonder_trades_{ctx.team}",
+ "default": {"_lock": 0},
+ "operations": [{"operation": "update", "value": {"_lock": 0}}],
+ }])
+ return None
+
+ wonder_trade_slot = max(candidate_slots)
+
+ await ctx.send_msgs([{
+ "cmd": "Set",
+ "key": f"pokemon_wonder_trades_{ctx.team}",
+ "default": {"_lock": 0},
+ "operations": [
+ {"operation": "update", "value": {"_lock": 0}},
+ {"operation": "pop", "value": str(wonder_trade_slot)},
+ ]
+ }])
+
+ return reply["value"][str(wonder_trade_slot)][1]
+
+ def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None:
+ if cmd == "Connected":
+ Utils.async_start(ctx.send_msgs([{
+ "cmd": "SetNotify",
+ "keys": [f"pokemon_wonder_trades_{ctx.team}"],
+ }, {
+ "cmd": "Set",
+ "key": f"pokemon_wonder_trades_{ctx.team}",
+ "default": {"_lock": 0},
+ "operations": [{"operation": "default", "value": None}] # value is ignored
+ }]))
+ elif cmd == "SetReply":
+ if args.get("key", "") == f"pokemon_wonder_trades_{ctx.team}":
+ self.latest_wonder_trade_reply = args
+ self.wonder_trade_update_event.set()
diff --git a/worlds/pokemon_emerald/data.py b/worlds/pokemon_emerald/data.py
new file mode 100644
index 000000000000..d89ab5febb33
--- /dev/null
+++ b/worlds/pokemon_emerald/data.py
@@ -0,0 +1,1473 @@
+"""
+Pulls data from JSON files in worlds/pokemon_emerald/data/ into classes.
+This also includes marrying automatically extracted data with manually
+defined data (like location labels or usable pokemon species), some cleanup
+and sorting, and Warp methods.
+"""
+from dataclasses import dataclass
+from enum import IntEnum
+import orjson
+from typing import Dict, List, NamedTuple, Optional, Set, FrozenSet, Tuple, Any, Union
+import pkgutil
+import pkg_resources
+
+from BaseClasses import ItemClassification
+
+
+BASE_OFFSET = 3860000
+POKEDEX_OFFSET = 10000
+
+IGNORABLE_MAPS = {
+ "MAP_ALTERING_CAVE",
+ "MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1",
+ "MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2",
+ "MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3",
+}
+"""These maps exist but don't show up in the rando or are unused, and so should be discarded"""
+
+OUT_OF_LOGIC_MAPS = {
+ "MAP_DESERT_UNDERPASS",
+ "MAP_SAFARI_ZONE_NORTHEAST",
+ "MAP_SAFARI_ZONE_SOUTHEAST",
+ "MAP_METEOR_FALLS_STEVENS_CAVE",
+ "MAP_MIRAGE_TOWER_1F",
+ "MAP_MIRAGE_TOWER_2F",
+ "MAP_MIRAGE_TOWER_3F",
+ "MAP_MIRAGE_TOWER_4F",
+}
+"""
+These maps have encounters and are locked behind beating the champion or are missable.
+Those encounter slots should be ignored for logical access to a species.
+"""
+
+NUM_REAL_SPECIES = 386
+
+
+class Warp:
+ """
+ Represents warp events in the game like doorways or warp pads
+ """
+ is_one_way: bool
+ source_map: str
+ source_ids: List[int]
+ dest_map: str
+ dest_ids: List[int]
+ parent_region: Optional[str]
+
+ def __init__(self, encoded_string: Optional[str] = None, parent_region: Optional[str] = None) -> None:
+ if encoded_string is not None:
+ decoded_warp = Warp.decode(encoded_string)
+ self.is_one_way = decoded_warp.is_one_way
+ self.source_map = decoded_warp.source_map
+ self.source_ids = decoded_warp.source_ids
+ self.dest_map = decoded_warp.dest_map
+ self.dest_ids = decoded_warp.dest_ids
+ self.parent_region = parent_region
+
+ def encode(self) -> str:
+ """
+ Returns a string encoding of this warp
+ """
+ source_ids_string = ""
+ for source_id in self.source_ids:
+ source_ids_string += str(source_id) + ","
+ source_ids_string = source_ids_string[:-1] # Remove last ","
+
+ dest_ids_string = ""
+ for dest_id in self.dest_ids:
+ dest_ids_string += str(dest_id) + ","
+ dest_ids_string = dest_ids_string[:-1] # Remove last ","
+
+ return f"{self.source_map}:{source_ids_string}/{self.dest_map}:{dest_ids_string}{'!' if self.is_one_way else ''}"
+
+ def connects_to(self, other: "Warp") -> bool:
+ """
+ Returns true if this warp sends the player to `other`
+ """
+ return self.dest_map == other.source_map and set(self.dest_ids) <= set(other.source_ids)
+
+ @staticmethod
+ def decode(encoded_string: str) -> "Warp":
+ """
+ Create a Warp object from an encoded string
+ """
+ warp = Warp()
+ warp.is_one_way = encoded_string.endswith("!")
+ if warp.is_one_way:
+ encoded_string = encoded_string[:-1]
+
+ warp_source, warp_dest = encoded_string.split("/")
+ warp_source_map, warp_source_indices = warp_source.split(":")
+ warp_dest_map, warp_dest_indices = warp_dest.split(":")
+
+ warp.source_map = warp_source_map
+ warp.dest_map = warp_dest_map
+
+ warp.source_ids = [int(index) for index in warp_source_indices.split(",")]
+ warp.dest_ids = [int(index) for index in warp_dest_indices.split(",")]
+
+ return warp
+
+
+class ItemData(NamedTuple):
+ label: str
+ item_id: int
+ modern_id: Optional[int]
+ classification: ItemClassification
+ tags: FrozenSet[str]
+
+
+class LocationData(NamedTuple):
+ name: str
+ label: str
+ parent_region: str
+ default_item: int
+ address: Union[int, List[int]]
+ flag: int
+ tags: FrozenSet[str]
+
+
+class EncounterTableData(NamedTuple):
+ slots: List[int]
+ address: int
+
+
+@dataclass
+class MapData:
+ name: str
+ header_address: int
+ land_encounters: Optional[EncounterTableData]
+ water_encounters: Optional[EncounterTableData]
+ fishing_encounters: Optional[EncounterTableData]
+
+
+class EventData(NamedTuple):
+ name: str
+ parent_region: str
+
+
+class RegionData:
+ name: str
+ parent_map: MapData
+ has_grass: bool
+ has_water: bool
+ has_fishing: bool
+ exits: List[str]
+ warps: List[str]
+ locations: List[str]
+ events: List[EventData]
+
+ def __init__(self, name: str, parent_map: MapData, has_grass: bool, has_water: bool, has_fishing: bool):
+ self.name = name
+ self.parent_map = parent_map
+ self.has_grass = has_grass
+ self.has_water = has_water
+ self.has_fishing = has_fishing
+ self.exits = []
+ self.warps = []
+ self.locations = []
+ self.events = []
+
+
+class BaseStats(NamedTuple):
+ hp: int
+ attack: int
+ defense: int
+ speed: int
+ special_attack: int
+ special_defense: int
+
+
+class LearnsetMove(NamedTuple):
+ level: int
+ move_id: int
+
+
+class EvolutionMethodEnum(IntEnum):
+ LEVEL = 0
+ LEVEL_ATK_LT_DEF = 1
+ LEVEL_ATK_EQ_DEF = 2
+ LEVEL_ATK_GT_DEF = 3
+ LEVEL_SILCOON = 4
+ LEVEL_CASCOON = 5
+ LEVEL_NINJASK = 6
+ LEVEL_SHEDINJA = 7
+ ITEM = 8
+ FRIENDSHIP = 9
+ FRIENDSHIP_DAY = 10
+ FRIENDSHIP_NIGHT = 11
+
+
+def _str_to_evolution_method(string: str) -> EvolutionMethodEnum:
+ if string == "LEVEL":
+ return EvolutionMethodEnum.LEVEL
+ if string == "LEVEL_ATK_LT_DEF":
+ return EvolutionMethodEnum.LEVEL_ATK_LT_DEF
+ if string == "LEVEL_ATK_EQ_DEF":
+ return EvolutionMethodEnum.LEVEL_ATK_EQ_DEF
+ if string == "LEVEL_ATK_GT_DEF":
+ return EvolutionMethodEnum.LEVEL_ATK_GT_DEF
+ if string == "LEVEL_SILCOON":
+ return EvolutionMethodEnum.LEVEL_SILCOON
+ if string == "LEVEL_CASCOON":
+ return EvolutionMethodEnum.LEVEL_CASCOON
+ if string == "LEVEL_NINJASK":
+ return EvolutionMethodEnum.LEVEL_NINJASK
+ if string == "LEVEL_SHEDINJA":
+ return EvolutionMethodEnum.LEVEL_SHEDINJA
+ if string == "FRIENDSHIP":
+ return EvolutionMethodEnum.FRIENDSHIP
+ if string == "FRIENDSHIP_DAY":
+ return EvolutionMethodEnum.FRIENDSHIP_DAY
+ if string == "FRIENDSHIP_NIGHT":
+ return EvolutionMethodEnum.FRIENDSHIP_NIGHT
+
+
+class EvolutionData(NamedTuple):
+ method: EvolutionMethodEnum
+ param: int
+ species_id: int
+
+
+class MiscPokemonData(NamedTuple):
+ species_id: int
+ address: int
+
+
+@dataclass
+class SpeciesData:
+ name: str
+ label: str
+ species_id: int
+ national_dex_number: int
+ base_stats: BaseStats
+ types: Tuple[int, int]
+ abilities: Tuple[int, int]
+ evolutions: List[EvolutionData]
+ pre_evolution: Optional[int]
+ catch_rate: int
+ friendship: int
+ learnset: List[LearnsetMove]
+ tm_hm_compatibility: int
+ learnset_address: int
+ address: int
+
+
+class AbilityData(NamedTuple):
+ ability_id: int
+ label: str
+
+
+class TrainerPokemonDataTypeEnum(IntEnum):
+ NO_ITEM_DEFAULT_MOVES = 0
+ ITEM_DEFAULT_MOVES = 1
+ NO_ITEM_CUSTOM_MOVES = 2
+ ITEM_CUSTOM_MOVES = 3
+
+
+def _str_to_pokemon_data_type(string: str) -> TrainerPokemonDataTypeEnum:
+ if string == "NO_ITEM_DEFAULT_MOVES":
+ return TrainerPokemonDataTypeEnum.NO_ITEM_DEFAULT_MOVES
+ if string == "ITEM_DEFAULT_MOVES":
+ return TrainerPokemonDataTypeEnum.ITEM_DEFAULT_MOVES
+ if string == "NO_ITEM_CUSTOM_MOVES":
+ return TrainerPokemonDataTypeEnum.NO_ITEM_CUSTOM_MOVES
+ if string == "ITEM_CUSTOM_MOVES":
+ return TrainerPokemonDataTypeEnum.ITEM_CUSTOM_MOVES
+
+
+@dataclass
+class TrainerPokemonData:
+ species_id: int
+ level: int
+ moves: Optional[Tuple[int, int, int, int]]
+
+
+@dataclass
+class TrainerPartyData:
+ pokemon: List[TrainerPokemonData]
+ pokemon_data_type: TrainerPokemonDataTypeEnum
+ address: int
+
+
+@dataclass
+class TrainerData:
+ trainer_id: int
+ party: TrainerPartyData
+ address: int
+ script_address: int
+ battle_type: int
+
+
+class PokemonEmeraldData:
+ starters: Tuple[int, int, int]
+ constants: Dict[str, int]
+ ram_addresses: Dict[str, int]
+ rom_addresses: Dict[str, int]
+ regions: Dict[str, RegionData]
+ locations: Dict[str, LocationData]
+ items: Dict[int, ItemData]
+ species: Dict[int, SpeciesData]
+ legendary_encounters: List[MiscPokemonData]
+ misc_pokemon: List[MiscPokemonData]
+ tmhm_moves: List[int]
+ abilities: List[AbilityData]
+ move_labels: Dict[str, int]
+ maps: Dict[str, MapData]
+ warps: Dict[str, Warp]
+ warp_map: Dict[str, Optional[str]]
+ trainers: List[TrainerData]
+
+ def __init__(self) -> None:
+ self.starters = (277, 280, 283)
+ self.constants = {}
+ self.ram_addresses = {}
+ self.rom_addresses = {}
+ self.regions = {}
+ self.locations = {}
+ self.items = {}
+ self.species = {}
+ self.legendary_encounters = []
+ self.misc_pokemon = []
+ self.tmhm_moves = []
+ self.abilities = []
+ self.move_labels = {}
+ self.maps = {}
+ self.warps = {}
+ self.warp_map = {}
+ self.trainers = []
+
+
+def load_json_data(data_name: str) -> Union[List[Any], Dict[str, Any]]:
+ return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name).decode("utf-8-sig"))
+
+
+def _init() -> None:
+ extracted_data: Dict[str, Any] = load_json_data("extracted_data.json")
+ data.constants = extracted_data["constants"]
+ data.ram_addresses = extracted_data["misc_ram_addresses"]
+ data.rom_addresses = extracted_data["misc_rom_addresses"]
+
+ location_attributes_json = load_json_data("locations.json")
+
+ # Create map data
+ for map_name, map_json in extracted_data["maps"].items():
+ if map_name in IGNORABLE_MAPS:
+ continue
+
+ land_encounters = None
+ water_encounters = None
+ fishing_encounters = None
+
+ if "land_encounters" in map_json:
+ land_encounters = EncounterTableData(
+ map_json["land_encounters"]["slots"],
+ map_json["land_encounters"]["address"]
+ )
+ if "water_encounters" in map_json:
+ water_encounters = EncounterTableData(
+ map_json["water_encounters"]["slots"],
+ map_json["water_encounters"]["address"]
+ )
+ if "fishing_encounters" in map_json:
+ fishing_encounters = EncounterTableData(
+ map_json["fishing_encounters"]["slots"],
+ map_json["fishing_encounters"]["address"]
+ )
+
+ data.maps[map_name] = MapData(
+ map_name,
+ map_json["header_address"],
+ land_encounters,
+ water_encounters,
+ fishing_encounters
+ )
+
+ # Load/merge region json files
+ region_json_list = []
+ for file in pkg_resources.resource_listdir(__name__, "data/regions"):
+ if not pkg_resources.resource_isdir(__name__, "data/regions/" + file):
+ region_json_list.append(load_json_data("regions/" + file))
+
+ regions_json = {}
+ for region_subset in region_json_list:
+ for region_name, region_json in region_subset.items():
+ if region_name in regions_json:
+ raise AssertionError("Region [{region_name}] was defined multiple times")
+ regions_json[region_name] = region_json
+
+ # Create region data
+ claimed_locations: Set[str] = set()
+ claimed_warps: Set[str] = set()
+
+ data.regions = {}
+ for region_name, region_json in regions_json.items():
+ new_region = RegionData(
+ region_name,
+ data.maps[region_json["parent_map"]],
+ region_json["has_grass"],
+ region_json["has_water"],
+ region_json["has_fishing"]
+ )
+
+ # Locations
+ for location_name in region_json["locations"]:
+ if location_name in claimed_locations:
+ raise AssertionError(f"Location [{location_name}] was claimed by multiple regions")
+
+ location_json = extracted_data["locations"][location_name]
+ if location_name.startswith("TRAINER_BRENDAN_") or location_name.startswith("TRAINER_MAY_"):
+ import re
+ locale = re.match("TRAINER_BRENDAN_([A-Z0-9_]+)_MUDKIP_REWARD", location_name).group(1)
+ alternate_rival_jsons = [extracted_data["locations"][alternate] for alternate in [
+ f"TRAINER_BRENDAN_{locale}_TORCHIC_REWARD",
+ f"TRAINER_BRENDAN_{locale}_TREECKO_REWARD",
+ f"TRAINER_MAY_{locale}_MUDKIP_REWARD",
+ f"TRAINER_MAY_{locale}_TORCHIC_REWARD",
+ f"TRAINER_MAY_{locale}_TREECKO_REWARD",
+ ]]
+ new_location = LocationData(
+ location_name,
+ location_attributes_json[location_name]["label"],
+ region_name,
+ location_json["default_item"],
+ [location_json["address"]] + [j["address"] for j in alternate_rival_jsons],
+ location_json["flag"],
+ frozenset(location_attributes_json[location_name]["tags"])
+ )
+ else:
+ new_location = LocationData(
+ location_name,
+ location_attributes_json[location_name]["label"],
+ region_name,
+ location_json["default_item"],
+ location_json["address"],
+ location_json["flag"],
+ frozenset(location_attributes_json[location_name]["tags"])
+ )
+ new_region.locations.append(location_name)
+ data.locations[location_name] = new_location
+ claimed_locations.add(location_name)
+
+ new_region.locations.sort()
+
+ # Events
+ for event in region_json["events"]:
+ new_region.events.append(EventData(event, region_name))
+
+ # Exits
+ for region_exit in region_json["exits"]:
+ new_region.exits.append(region_exit)
+
+ # Warps
+ for encoded_warp in region_json["warps"]:
+ if encoded_warp in claimed_warps:
+ raise AssertionError(f"Warp [{encoded_warp}] was claimed by multiple regions")
+ new_region.warps.append(encoded_warp)
+ data.warps[encoded_warp] = Warp(encoded_warp, region_name)
+ claimed_warps.add(encoded_warp)
+
+ new_region.warps.sort()
+
+ data.regions[region_name] = new_region
+
+ # Create item data
+ items_json = load_json_data("items.json")
+
+ data.items = {}
+ for item_constant_name, attributes in items_json.items():
+ item_classification = None
+ if attributes["classification"] == "PROGRESSION":
+ item_classification = ItemClassification.progression
+ elif attributes["classification"] == "USEFUL":
+ item_classification = ItemClassification.useful
+ elif attributes["classification"] == "FILLER":
+ item_classification = ItemClassification.filler
+ elif attributes["classification"] == "TRAP":
+ item_classification = ItemClassification.trap
+ else:
+ raise ValueError(f"Unknown classification {attributes['classification']} for item {item_constant_name}")
+
+ data.items[data.constants[item_constant_name]] = ItemData(
+ attributes["label"],
+ data.constants[item_constant_name],
+ attributes["modern_id"],
+ item_classification,
+ frozenset(attributes["tags"])
+ )
+
+ # Create species data
+
+ # Excludes extras like copies of Unown and special species values like SPECIES_EGG.
+ all_species: List[Tuple[str, str, int]] = [
+ ("SPECIES_BULBASAUR", "Bulbasaur", 1),
+ ("SPECIES_IVYSAUR", "Ivysaur", 2),
+ ("SPECIES_VENUSAUR", "Venusaur", 3),
+ ("SPECIES_CHARMANDER", "Charmander", 4),
+ ("SPECIES_CHARMELEON", "Charmeleon", 5),
+ ("SPECIES_CHARIZARD", "Charizard", 6),
+ ("SPECIES_SQUIRTLE", "Squirtle", 7),
+ ("SPECIES_WARTORTLE", "Wartortle", 8),
+ ("SPECIES_BLASTOISE", "Blastoise", 9),
+ ("SPECIES_CATERPIE", "Caterpie", 10),
+ ("SPECIES_METAPOD", "Metapod", 11),
+ ("SPECIES_BUTTERFREE", "Butterfree", 12),
+ ("SPECIES_WEEDLE", "Weedle", 13),
+ ("SPECIES_KAKUNA", "Kakuna", 14),
+ ("SPECIES_BEEDRILL", "Beedrill", 15),
+ ("SPECIES_PIDGEY", "Pidgey", 16),
+ ("SPECIES_PIDGEOTTO", "Pidgeotto", 17),
+ ("SPECIES_PIDGEOT", "Pidgeot", 18),
+ ("SPECIES_RATTATA", "Rattata", 19),
+ ("SPECIES_RATICATE", "Raticate", 20),
+ ("SPECIES_SPEAROW", "Spearow", 21),
+ ("SPECIES_FEAROW", "Fearow", 22),
+ ("SPECIES_EKANS", "Ekans", 23),
+ ("SPECIES_ARBOK", "Arbok", 24),
+ ("SPECIES_PIKACHU", "Pikachu", 25),
+ ("SPECIES_RAICHU", "Raichu", 26),
+ ("SPECIES_SANDSHREW", "Sandshrew", 27),
+ ("SPECIES_SANDSLASH", "Sandslash", 28),
+ ("SPECIES_NIDORAN_F", "Nidoran Female", 29),
+ ("SPECIES_NIDORINA", "Nidorina", 30),
+ ("SPECIES_NIDOQUEEN", "Nidoqueen", 31),
+ ("SPECIES_NIDORAN_M", "Nidoran Male", 32),
+ ("SPECIES_NIDORINO", "Nidorino", 33),
+ ("SPECIES_NIDOKING", "Nidoking", 34),
+ ("SPECIES_CLEFAIRY", "Clefairy", 35),
+ ("SPECIES_CLEFABLE", "Clefable", 36),
+ ("SPECIES_VULPIX", "Vulpix", 37),
+ ("SPECIES_NINETALES", "Ninetales", 38),
+ ("SPECIES_JIGGLYPUFF", "Jigglypuff", 39),
+ ("SPECIES_WIGGLYTUFF", "Wigglytuff", 40),
+ ("SPECIES_ZUBAT", "Zubat", 41),
+ ("SPECIES_GOLBAT", "Golbat", 42),
+ ("SPECIES_ODDISH", "Oddish", 43),
+ ("SPECIES_GLOOM", "Gloom", 44),
+ ("SPECIES_VILEPLUME", "Vileplume", 45),
+ ("SPECIES_PARAS", "Paras", 46),
+ ("SPECIES_PARASECT", "Parasect", 47),
+ ("SPECIES_VENONAT", "Venonat", 48),
+ ("SPECIES_VENOMOTH", "Venomoth", 49),
+ ("SPECIES_DIGLETT", "Diglett", 50),
+ ("SPECIES_DUGTRIO", "Dugtrio", 51),
+ ("SPECIES_MEOWTH", "Meowth", 52),
+ ("SPECIES_PERSIAN", "Persian", 53),
+ ("SPECIES_PSYDUCK", "Psyduck", 54),
+ ("SPECIES_GOLDUCK", "Golduck", 55),
+ ("SPECIES_MANKEY", "Mankey", 56),
+ ("SPECIES_PRIMEAPE", "Primeape", 57),
+ ("SPECIES_GROWLITHE", "Growlithe", 58),
+ ("SPECIES_ARCANINE", "Arcanine", 59),
+ ("SPECIES_POLIWAG", "Poliwag", 60),
+ ("SPECIES_POLIWHIRL", "Poliwhirl", 61),
+ ("SPECIES_POLIWRATH", "Poliwrath", 62),
+ ("SPECIES_ABRA", "Abra", 63),
+ ("SPECIES_KADABRA", "Kadabra", 64),
+ ("SPECIES_ALAKAZAM", "Alakazam", 65),
+ ("SPECIES_MACHOP", "Machop", 66),
+ ("SPECIES_MACHOKE", "Machoke", 67),
+ ("SPECIES_MACHAMP", "Machamp", 68),
+ ("SPECIES_BELLSPROUT", "Bellsprout", 69),
+ ("SPECIES_WEEPINBELL", "Weepinbell", 70),
+ ("SPECIES_VICTREEBEL", "Victreebel", 71),
+ ("SPECIES_TENTACOOL", "Tentacool", 72),
+ ("SPECIES_TENTACRUEL", "Tentacruel", 73),
+ ("SPECIES_GEODUDE", "Geodude", 74),
+ ("SPECIES_GRAVELER", "Graveler", 75),
+ ("SPECIES_GOLEM", "Golem", 76),
+ ("SPECIES_PONYTA", "Ponyta", 77),
+ ("SPECIES_RAPIDASH", "Rapidash", 78),
+ ("SPECIES_SLOWPOKE", "Slowpoke", 79),
+ ("SPECIES_SLOWBRO", "Slowbro", 80),
+ ("SPECIES_MAGNEMITE", "Magnemite", 81),
+ ("SPECIES_MAGNETON", "Magneton", 82),
+ ("SPECIES_FARFETCHD", "Farfetch'd", 83),
+ ("SPECIES_DODUO", "Doduo", 84),
+ ("SPECIES_DODRIO", "Dodrio", 85),
+ ("SPECIES_SEEL", "Seel", 86),
+ ("SPECIES_DEWGONG", "Dewgong", 87),
+ ("SPECIES_GRIMER", "Grimer", 88),
+ ("SPECIES_MUK", "Muk", 89),
+ ("SPECIES_SHELLDER", "Shellder", 90),
+ ("SPECIES_CLOYSTER", "Cloyster", 91),
+ ("SPECIES_GASTLY", "Gastly", 92),
+ ("SPECIES_HAUNTER", "Haunter", 93),
+ ("SPECIES_GENGAR", "Gengar", 94),
+ ("SPECIES_ONIX", "Onix", 95),
+ ("SPECIES_DROWZEE", "Drowzee", 96),
+ ("SPECIES_HYPNO", "Hypno", 97),
+ ("SPECIES_KRABBY", "Krabby", 98),
+ ("SPECIES_KINGLER", "Kingler", 99),
+ ("SPECIES_VOLTORB", "Voltorb", 100),
+ ("SPECIES_ELECTRODE", "Electrode", 101),
+ ("SPECIES_EXEGGCUTE", "Exeggcute", 102),
+ ("SPECIES_EXEGGUTOR", "Exeggutor", 103),
+ ("SPECIES_CUBONE", "Cubone", 104),
+ ("SPECIES_MAROWAK", "Marowak", 105),
+ ("SPECIES_HITMONLEE", "Hitmonlee", 106),
+ ("SPECIES_HITMONCHAN", "Hitmonchan", 107),
+ ("SPECIES_LICKITUNG", "Lickitung", 108),
+ ("SPECIES_KOFFING", "Koffing", 109),
+ ("SPECIES_WEEZING", "Weezing", 110),
+ ("SPECIES_RHYHORN", "Rhyhorn", 111),
+ ("SPECIES_RHYDON", "Rhydon", 112),
+ ("SPECIES_CHANSEY", "Chansey", 113),
+ ("SPECIES_TANGELA", "Tangela", 114),
+ ("SPECIES_KANGASKHAN", "Kangaskhan", 115),
+ ("SPECIES_HORSEA", "Horsea", 116),
+ ("SPECIES_SEADRA", "Seadra", 117),
+ ("SPECIES_GOLDEEN", "Goldeen", 118),
+ ("SPECIES_SEAKING", "Seaking", 119),
+ ("SPECIES_STARYU", "Staryu", 120),
+ ("SPECIES_STARMIE", "Starmie", 121),
+ ("SPECIES_MR_MIME", "Mr. Mime", 122),
+ ("SPECIES_SCYTHER", "Scyther", 123),
+ ("SPECIES_JYNX", "Jynx", 124),
+ ("SPECIES_ELECTABUZZ", "Electabuzz", 125),
+ ("SPECIES_MAGMAR", "Magmar", 126),
+ ("SPECIES_PINSIR", "Pinsir", 127),
+ ("SPECIES_TAUROS", "Tauros", 128),
+ ("SPECIES_MAGIKARP", "Magikarp", 129),
+ ("SPECIES_GYARADOS", "Gyarados", 130),
+ ("SPECIES_LAPRAS", "Lapras", 131),
+ ("SPECIES_DITTO", "Ditto", 132),
+ ("SPECIES_EEVEE", "Eevee", 133),
+ ("SPECIES_VAPOREON", "Vaporeon", 134),
+ ("SPECIES_JOLTEON", "Jolteon", 135),
+ ("SPECIES_FLAREON", "Flareon", 136),
+ ("SPECIES_PORYGON", "Porygon", 137),
+ ("SPECIES_OMANYTE", "Omanyte", 138),
+ ("SPECIES_OMASTAR", "Omastar", 139),
+ ("SPECIES_KABUTO", "Kabuto", 140),
+ ("SPECIES_KABUTOPS", "Kabutops", 141),
+ ("SPECIES_AERODACTYL", "Aerodactyl", 142),
+ ("SPECIES_SNORLAX", "Snorlax", 143),
+ ("SPECIES_ARTICUNO", "Articuno", 144),
+ ("SPECIES_ZAPDOS", "Zapdos", 145),
+ ("SPECIES_MOLTRES", "Moltres", 146),
+ ("SPECIES_DRATINI", "Dratini", 147),
+ ("SPECIES_DRAGONAIR", "Dragonair", 148),
+ ("SPECIES_DRAGONITE", "Dragonite", 149),
+ ("SPECIES_MEWTWO", "Mewtwo", 150),
+ ("SPECIES_MEW", "Mew", 151),
+ ("SPECIES_CHIKORITA", "Chikorita", 152),
+ ("SPECIES_BAYLEEF", "Bayleef", 153),
+ ("SPECIES_MEGANIUM", "Meganium", 154),
+ ("SPECIES_CYNDAQUIL", "Cyndaquil", 155),
+ ("SPECIES_QUILAVA", "Quilava", 156),
+ ("SPECIES_TYPHLOSION", "Typhlosion", 157),
+ ("SPECIES_TOTODILE", "Totodile", 158),
+ ("SPECIES_CROCONAW", "Croconaw", 159),
+ ("SPECIES_FERALIGATR", "Feraligatr", 160),
+ ("SPECIES_SENTRET", "Sentret", 161),
+ ("SPECIES_FURRET", "Furret", 162),
+ ("SPECIES_HOOTHOOT", "Hoothoot", 163),
+ ("SPECIES_NOCTOWL", "Noctowl", 164),
+ ("SPECIES_LEDYBA", "Ledyba", 165),
+ ("SPECIES_LEDIAN", "Ledian", 166),
+ ("SPECIES_SPINARAK", "Spinarak", 167),
+ ("SPECIES_ARIADOS", "Ariados", 168),
+ ("SPECIES_CROBAT", "Crobat", 169),
+ ("SPECIES_CHINCHOU", "Chinchou", 170),
+ ("SPECIES_LANTURN", "Lanturn", 171),
+ ("SPECIES_PICHU", "Pichu", 172),
+ ("SPECIES_CLEFFA", "Cleffa", 173),
+ ("SPECIES_IGGLYBUFF", "Igglybuff", 174),
+ ("SPECIES_TOGEPI", "Togepi", 175),
+ ("SPECIES_TOGETIC", "Togetic", 176),
+ ("SPECIES_NATU", "Natu", 177),
+ ("SPECIES_XATU", "Xatu", 178),
+ ("SPECIES_MAREEP", "Mareep", 179),
+ ("SPECIES_FLAAFFY", "Flaaffy", 180),
+ ("SPECIES_AMPHAROS", "Ampharos", 181),
+ ("SPECIES_BELLOSSOM", "Bellossom", 182),
+ ("SPECIES_MARILL", "Marill", 183),
+ ("SPECIES_AZUMARILL", "Azumarill", 184),
+ ("SPECIES_SUDOWOODO", "Sudowoodo", 185),
+ ("SPECIES_POLITOED", "Politoed", 186),
+ ("SPECIES_HOPPIP", "Hoppip", 187),
+ ("SPECIES_SKIPLOOM", "Skiploom", 188),
+ ("SPECIES_JUMPLUFF", "Jumpluff", 189),
+ ("SPECIES_AIPOM", "Aipom", 190),
+ ("SPECIES_SUNKERN", "Sunkern", 191),
+ ("SPECIES_SUNFLORA", "Sunflora", 192),
+ ("SPECIES_YANMA", "Yanma", 193),
+ ("SPECIES_WOOPER", "Wooper", 194),
+ ("SPECIES_QUAGSIRE", "Quagsire", 195),
+ ("SPECIES_ESPEON", "Espeon", 196),
+ ("SPECIES_UMBREON", "Umbreon", 197),
+ ("SPECIES_MURKROW", "Murkrow", 198),
+ ("SPECIES_SLOWKING", "Slowking", 199),
+ ("SPECIES_MISDREAVUS", "Misdreavus", 200),
+ ("SPECIES_UNOWN", "Unown", 201),
+ ("SPECIES_WOBBUFFET", "Wobbuffet", 202),
+ ("SPECIES_GIRAFARIG", "Girafarig", 203),
+ ("SPECIES_PINECO", "Pineco", 204),
+ ("SPECIES_FORRETRESS", "Forretress", 205),
+ ("SPECIES_DUNSPARCE", "Dunsparce", 206),
+ ("SPECIES_GLIGAR", "Gligar", 207),
+ ("SPECIES_STEELIX", "Steelix", 208),
+ ("SPECIES_SNUBBULL", "Snubbull", 209),
+ ("SPECIES_GRANBULL", "Granbull", 210),
+ ("SPECIES_QWILFISH", "Qwilfish", 211),
+ ("SPECIES_SCIZOR", "Scizor", 212),
+ ("SPECIES_SHUCKLE", "Shuckle", 213),
+ ("SPECIES_HERACROSS", "Heracross", 214),
+ ("SPECIES_SNEASEL", "Sneasel", 215),
+ ("SPECIES_TEDDIURSA", "Teddiursa", 216),
+ ("SPECIES_URSARING", "Ursaring", 217),
+ ("SPECIES_SLUGMA", "Slugma", 218),
+ ("SPECIES_MAGCARGO", "Magcargo", 219),
+ ("SPECIES_SWINUB", "Swinub", 220),
+ ("SPECIES_PILOSWINE", "Piloswine", 221),
+ ("SPECIES_CORSOLA", "Corsola", 222),
+ ("SPECIES_REMORAID", "Remoraid", 223),
+ ("SPECIES_OCTILLERY", "Octillery", 224),
+ ("SPECIES_DELIBIRD", "Delibird", 225),
+ ("SPECIES_MANTINE", "Mantine", 226),
+ ("SPECIES_SKARMORY", "Skarmory", 227),
+ ("SPECIES_HOUNDOUR", "Houndour", 228),
+ ("SPECIES_HOUNDOOM", "Houndoom", 229),
+ ("SPECIES_KINGDRA", "Kingdra", 230),
+ ("SPECIES_PHANPY", "Phanpy", 231),
+ ("SPECIES_DONPHAN", "Donphan", 232),
+ ("SPECIES_PORYGON2", "Porygon2", 233),
+ ("SPECIES_STANTLER", "Stantler", 234),
+ ("SPECIES_SMEARGLE", "Smeargle", 235),
+ ("SPECIES_TYROGUE", "Tyrogue", 236),
+ ("SPECIES_HITMONTOP", "Hitmontop", 237),
+ ("SPECIES_SMOOCHUM", "Smoochum", 238),
+ ("SPECIES_ELEKID", "Elekid", 239),
+ ("SPECIES_MAGBY", "Magby", 240),
+ ("SPECIES_MILTANK", "Miltank", 241),
+ ("SPECIES_BLISSEY", "Blissey", 242),
+ ("SPECIES_RAIKOU", "Raikou", 243),
+ ("SPECIES_ENTEI", "Entei", 244),
+ ("SPECIES_SUICUNE", "Suicune", 245),
+ ("SPECIES_LARVITAR", "Larvitar", 246),
+ ("SPECIES_PUPITAR", "Pupitar", 247),
+ ("SPECIES_TYRANITAR", "Tyranitar", 248),
+ ("SPECIES_LUGIA", "Lugia", 249),
+ ("SPECIES_HO_OH", "Ho-Oh", 250),
+ ("SPECIES_CELEBI", "Celebi", 251),
+ ("SPECIES_TREECKO", "Treecko", 252),
+ ("SPECIES_GROVYLE", "Grovyle", 253),
+ ("SPECIES_SCEPTILE", "Sceptile", 254),
+ ("SPECIES_TORCHIC", "Torchic", 255),
+ ("SPECIES_COMBUSKEN", "Combusken", 256),
+ ("SPECIES_BLAZIKEN", "Blaziken", 257),
+ ("SPECIES_MUDKIP", "Mudkip", 258),
+ ("SPECIES_MARSHTOMP", "Marshtomp", 259),
+ ("SPECIES_SWAMPERT", "Swampert", 260),
+ ("SPECIES_POOCHYENA", "Poochyena", 261),
+ ("SPECIES_MIGHTYENA", "Mightyena", 262),
+ ("SPECIES_ZIGZAGOON", "Zigzagoon", 263),
+ ("SPECIES_LINOONE", "Linoone", 264),
+ ("SPECIES_WURMPLE", "Wurmple", 265),
+ ("SPECIES_SILCOON", "Silcoon", 266),
+ ("SPECIES_BEAUTIFLY", "Beautifly", 267),
+ ("SPECIES_CASCOON", "Cascoon", 268),
+ ("SPECIES_DUSTOX", "Dustox", 269),
+ ("SPECIES_LOTAD", "Lotad", 270),
+ ("SPECIES_LOMBRE", "Lombre", 271),
+ ("SPECIES_LUDICOLO", "Ludicolo", 272),
+ ("SPECIES_SEEDOT", "Seedot", 273),
+ ("SPECIES_NUZLEAF", "Nuzleaf", 274),
+ ("SPECIES_SHIFTRY", "Shiftry", 275),
+ ("SPECIES_NINCADA", "Nincada", 290),
+ ("SPECIES_NINJASK", "Ninjask", 291),
+ ("SPECIES_SHEDINJA", "Shedinja", 292),
+ ("SPECIES_TAILLOW", "Taillow", 276),
+ ("SPECIES_SWELLOW", "Swellow", 277),
+ ("SPECIES_SHROOMISH", "Shroomish", 285),
+ ("SPECIES_BRELOOM", "Breloom", 286),
+ ("SPECIES_SPINDA", "Spinda", 327),
+ ("SPECIES_WINGULL", "Wingull", 278),
+ ("SPECIES_PELIPPER", "Pelipper", 279),
+ ("SPECIES_SURSKIT", "Surskit", 283),
+ ("SPECIES_MASQUERAIN", "Masquerain", 284),
+ ("SPECIES_WAILMER", "Wailmer", 320),
+ ("SPECIES_WAILORD", "Wailord", 321),
+ ("SPECIES_SKITTY", "Skitty", 300),
+ ("SPECIES_DELCATTY", "Delcatty", 301),
+ ("SPECIES_KECLEON", "Kecleon", 352),
+ ("SPECIES_BALTOY", "Baltoy", 343),
+ ("SPECIES_CLAYDOL", "Claydol", 344),
+ ("SPECIES_NOSEPASS", "Nosepass", 299),
+ ("SPECIES_TORKOAL", "Torkoal", 324),
+ ("SPECIES_SABLEYE", "Sableye", 302),
+ ("SPECIES_BARBOACH", "Barboach", 339),
+ ("SPECIES_WHISCASH", "Whiscash", 340),
+ ("SPECIES_LUVDISC", "Luvdisc", 370),
+ ("SPECIES_CORPHISH", "Corphish", 341),
+ ("SPECIES_CRAWDAUNT", "Crawdaunt", 342),
+ ("SPECIES_FEEBAS", "Feebas", 349),
+ ("SPECIES_MILOTIC", "Milotic", 350),
+ ("SPECIES_CARVANHA", "Carvanha", 318),
+ ("SPECIES_SHARPEDO", "Sharpedo", 319),
+ ("SPECIES_TRAPINCH", "Trapinch", 328),
+ ("SPECIES_VIBRAVA", "Vibrava", 329),
+ ("SPECIES_FLYGON", "Flygon", 330),
+ ("SPECIES_MAKUHITA", "Makuhita", 296),
+ ("SPECIES_HARIYAMA", "Hariyama", 297),
+ ("SPECIES_ELECTRIKE", "Electrike", 309),
+ ("SPECIES_MANECTRIC", "Manectric", 310),
+ ("SPECIES_NUMEL", "Numel", 322),
+ ("SPECIES_CAMERUPT", "Camerupt", 323),
+ ("SPECIES_SPHEAL", "Spheal", 363),
+ ("SPECIES_SEALEO", "Sealeo", 364),
+ ("SPECIES_WALREIN", "Walrein", 365),
+ ("SPECIES_CACNEA", "Cacnea", 331),
+ ("SPECIES_CACTURNE", "Cacturne", 332),
+ ("SPECIES_SNORUNT", "Snorunt", 361),
+ ("SPECIES_GLALIE", "Glalie", 362),
+ ("SPECIES_LUNATONE", "Lunatone", 337),
+ ("SPECIES_SOLROCK", "Solrock", 338),
+ ("SPECIES_AZURILL", "Azurill", 298),
+ ("SPECIES_SPOINK", "Spoink", 325),
+ ("SPECIES_GRUMPIG", "Grumpig", 326),
+ ("SPECIES_PLUSLE", "Plusle", 311),
+ ("SPECIES_MINUN", "Minun", 312),
+ ("SPECIES_MAWILE", "Mawile", 303),
+ ("SPECIES_MEDITITE", "Meditite", 307),
+ ("SPECIES_MEDICHAM", "Medicham", 308),
+ ("SPECIES_SWABLU", "Swablu", 333),
+ ("SPECIES_ALTARIA", "Altaria", 334),
+ ("SPECIES_WYNAUT", "Wynaut", 360),
+ ("SPECIES_DUSKULL", "Duskull", 355),
+ ("SPECIES_DUSCLOPS", "Dusclops", 356),
+ ("SPECIES_ROSELIA", "Roselia", 315),
+ ("SPECIES_SLAKOTH", "Slakoth", 287),
+ ("SPECIES_VIGOROTH", "Vigoroth", 288),
+ ("SPECIES_SLAKING", "Slaking", 289),
+ ("SPECIES_GULPIN", "Gulpin", 316),
+ ("SPECIES_SWALOT", "Swalot", 317),
+ ("SPECIES_TROPIUS", "Tropius", 357),
+ ("SPECIES_WHISMUR", "Whismur", 293),
+ ("SPECIES_LOUDRED", "Loudred", 294),
+ ("SPECIES_EXPLOUD", "Exploud", 295),
+ ("SPECIES_CLAMPERL", "Clamperl", 366),
+ ("SPECIES_HUNTAIL", "Huntail", 367),
+ ("SPECIES_GOREBYSS", "Gorebyss", 368),
+ ("SPECIES_ABSOL", "Absol", 359),
+ ("SPECIES_SHUPPET", "Shuppet", 353),
+ ("SPECIES_BANETTE", "Banette", 354),
+ ("SPECIES_SEVIPER", "Seviper", 336),
+ ("SPECIES_ZANGOOSE", "Zangoose", 335),
+ ("SPECIES_RELICANTH", "Relicanth", 369),
+ ("SPECIES_ARON", "Aron", 304),
+ ("SPECIES_LAIRON", "Lairon", 305),
+ ("SPECIES_AGGRON", "Aggron", 306),
+ ("SPECIES_CASTFORM", "Castform", 351),
+ ("SPECIES_VOLBEAT", "Volbeat", 313),
+ ("SPECIES_ILLUMISE", "Illumise", 314),
+ ("SPECIES_LILEEP", "Lileep", 345),
+ ("SPECIES_CRADILY", "Cradily", 346),
+ ("SPECIES_ANORITH", "Anorith", 347),
+ ("SPECIES_ARMALDO", "Armaldo", 348),
+ ("SPECIES_RALTS", "Ralts", 280),
+ ("SPECIES_KIRLIA", "Kirlia", 281),
+ ("SPECIES_GARDEVOIR", "Gardevoir", 282),
+ ("SPECIES_BAGON", "Bagon", 371),
+ ("SPECIES_SHELGON", "Shelgon", 372),
+ ("SPECIES_SALAMENCE", "Salamence", 373),
+ ("SPECIES_BELDUM", "Beldum", 374),
+ ("SPECIES_METANG", "Metang", 375),
+ ("SPECIES_METAGROSS", "Metagross", 376),
+ ("SPECIES_REGIROCK", "Regirock", 377),
+ ("SPECIES_REGICE", "Regice", 378),
+ ("SPECIES_REGISTEEL", "Registeel", 379),
+ ("SPECIES_KYOGRE", "Kyogre", 382),
+ ("SPECIES_GROUDON", "Groudon", 383),
+ ("SPECIES_RAYQUAZA", "Rayquaza", 384),
+ ("SPECIES_LATIAS", "Latias", 380),
+ ("SPECIES_LATIOS", "Latios", 381),
+ ("SPECIES_JIRACHI", "Jirachi", 385),
+ ("SPECIES_DEOXYS", "Deoxys", 386),
+ ("SPECIES_CHIMECHO", "Chimecho", 358),
+ ]
+
+ max_species_id = 0
+ for species_name, species_label, species_dex_number in all_species:
+ species_id = data.constants[species_name]
+ max_species_id = max(species_id, max_species_id)
+ species_data = extracted_data["species"][species_id]
+
+ learnset = [LearnsetMove(item["level"], item["move_id"]) for item in species_data["learnset"]["moves"]]
+
+ data.species[species_id] = SpeciesData(
+ species_name,
+ species_label,
+ species_id,
+ species_dex_number,
+ BaseStats(
+ species_data["base_stats"][0],
+ species_data["base_stats"][1],
+ species_data["base_stats"][2],
+ species_data["base_stats"][3],
+ species_data["base_stats"][4],
+ species_data["base_stats"][5]
+ ),
+ (species_data["types"][0], species_data["types"][1]),
+ (species_data["abilities"][0], species_data["abilities"][1]),
+ [EvolutionData(
+ _str_to_evolution_method(evolution_json["method"]),
+ evolution_json["param"],
+ evolution_json["species"],
+ ) for evolution_json in species_data["evolutions"]],
+ None,
+ species_data["catch_rate"],
+ species_data["friendship"],
+ learnset,
+ int(species_data["tmhm_learnset"], 16),
+ species_data["learnset"]["address"],
+ species_data["address"]
+ )
+
+ for species in data.species.values():
+ for evolution in species.evolutions:
+ data.species[evolution.species_id].pre_evolution = species.species_id
+
+ # Replace default item for dex entry locations based on evo stage of species
+ evo_stage_to_ball_map = {
+ 0: data.constants["ITEM_POKE_BALL"],
+ 1: data.constants["ITEM_GREAT_BALL"],
+ 2: data.constants["ITEM_ULTRA_BALL"],
+ }
+ for species in data.species.values():
+ evo_stage = 0
+ pre_evolution = species.pre_evolution
+ while pre_evolution is not None:
+ evo_stage += 1
+ pre_evolution = data.species[pre_evolution].pre_evolution
+
+ dex_location_name = f"POKEDEX_REWARD_{str(species.national_dex_number).zfill(3)}"
+ data.locations[dex_location_name] = LocationData(
+ data.locations[dex_location_name].name,
+ data.locations[dex_location_name].label,
+ data.locations[dex_location_name].parent_region,
+ evo_stage_to_ball_map[evo_stage],
+ data.locations[dex_location_name].address,
+ data.locations[dex_location_name].flag,
+ data.locations[dex_location_name].tags
+ )
+
+ # Create legendary encounter data
+ for legendary_encounter_json in extracted_data["legendary_encounters"]:
+ data.legendary_encounters.append(MiscPokemonData(
+ legendary_encounter_json["species"],
+ legendary_encounter_json["address"]
+ ))
+
+ for misc_pokemon_json in extracted_data["misc_pokemon"]:
+ data.misc_pokemon.append(MiscPokemonData(
+ misc_pokemon_json["species"],
+ misc_pokemon_json["address"]
+ ))
+
+ # TM moves
+ data.tmhm_moves = extracted_data["tmhm_moves"]
+
+ # Create ability data
+ data.abilities = [AbilityData(data.constants[ability_data[0]], ability_data[1]) for ability_data in [
+ ("ABILITY_STENCH", "Stench"),
+ ("ABILITY_DRIZZLE", "Drizzle"),
+ ("ABILITY_SPEED_BOOST", "Speed Boost"),
+ ("ABILITY_BATTLE_ARMOR", "Battle Armor"),
+ ("ABILITY_STURDY", "Sturdy"),
+ ("ABILITY_DAMP", "Damp"),
+ ("ABILITY_LIMBER", "Limber"),
+ ("ABILITY_SAND_VEIL", "Sand Veil"),
+ ("ABILITY_STATIC", "Static"),
+ ("ABILITY_VOLT_ABSORB", "Volt Absorb"),
+ ("ABILITY_WATER_ABSORB", "Water Absorb"),
+ ("ABILITY_OBLIVIOUS", "Oblivious"),
+ ("ABILITY_CLOUD_NINE", "Cloud Nine"),
+ ("ABILITY_COMPOUND_EYES", "Compoundeyes"),
+ ("ABILITY_INSOMNIA", "Insomnia"),
+ ("ABILITY_COLOR_CHANGE", "Color Change"),
+ ("ABILITY_IMMUNITY", "Immunity"),
+ ("ABILITY_FLASH_FIRE", "Flash Fire"),
+ ("ABILITY_SHIELD_DUST", "Shield Dust"),
+ ("ABILITY_OWN_TEMPO", "Own Tempo"),
+ ("ABILITY_SUCTION_CUPS", "Suction Cups"),
+ ("ABILITY_INTIMIDATE", "Intimidate"),
+ ("ABILITY_SHADOW_TAG", "Shadow Tag"),
+ ("ABILITY_ROUGH_SKIN", "Rough Skin"),
+ ("ABILITY_WONDER_GUARD", "Wonder Guard"),
+ ("ABILITY_LEVITATE", "Levitate"),
+ ("ABILITY_EFFECT_SPORE", "Effect Spore"),
+ ("ABILITY_SYNCHRONIZE", "Synchronize"),
+ ("ABILITY_CLEAR_BODY", "Clear Body"),
+ ("ABILITY_NATURAL_CURE", "Natural Cure"),
+ ("ABILITY_LIGHTNING_ROD", "Lightningrod"),
+ ("ABILITY_SERENE_GRACE", "Serene Grace"),
+ ("ABILITY_SWIFT_SWIM", "Swift Swim"),
+ ("ABILITY_CHLOROPHYLL", "Chlorophyll"),
+ ("ABILITY_ILLUMINATE", "Illuminate"),
+ ("ABILITY_TRACE", "Trace"),
+ ("ABILITY_HUGE_POWER", "Huge Power"),
+ ("ABILITY_POISON_POINT", "Poison Point"),
+ ("ABILITY_INNER_FOCUS", "Inner Focus"),
+ ("ABILITY_MAGMA_ARMOR", "Magma Armor"),
+ ("ABILITY_WATER_VEIL", "Water Veil"),
+ ("ABILITY_MAGNET_PULL", "Magnet Pull"),
+ ("ABILITY_SOUNDPROOF", "Soundproof"),
+ ("ABILITY_RAIN_DISH", "Rain Dish"),
+ ("ABILITY_SAND_STREAM", "Sand Stream"),
+ ("ABILITY_PRESSURE", "Pressure"),
+ ("ABILITY_THICK_FAT", "Thick Fat"),
+ ("ABILITY_EARLY_BIRD", "Early Bird"),
+ ("ABILITY_FLAME_BODY", "Flame Body"),
+ ("ABILITY_RUN_AWAY", "Run Away"),
+ ("ABILITY_KEEN_EYE", "Keen Eye"),
+ ("ABILITY_HYPER_CUTTER", "Hyper Cutter"),
+ ("ABILITY_PICKUP", "Pickup"),
+ ("ABILITY_TRUANT", "Truant"),
+ ("ABILITY_HUSTLE", "Hustle"),
+ ("ABILITY_CUTE_CHARM", "Cute Charm"),
+ ("ABILITY_PLUS", "Plus"),
+ ("ABILITY_MINUS", "Minus"),
+ ("ABILITY_FORECAST", "Forecast"),
+ ("ABILITY_STICKY_HOLD", "Sticky Hold"),
+ ("ABILITY_SHED_SKIN", "Shed Skin"),
+ ("ABILITY_GUTS", "Guts"),
+ ("ABILITY_MARVEL_SCALE", "Marvel Scale"),
+ ("ABILITY_LIQUID_OOZE", "Liquid Ooze"),
+ ("ABILITY_OVERGROW", "Overgrow"),
+ ("ABILITY_BLAZE", "Blaze"),
+ ("ABILITY_TORRENT", "Torrent"),
+ ("ABILITY_SWARM", "Swarm"),
+ ("ABILITY_ROCK_HEAD", "Rock Head"),
+ ("ABILITY_DROUGHT", "Drought"),
+ ("ABILITY_ARENA_TRAP", "Arena Trap"),
+ ("ABILITY_VITAL_SPIRIT", "Vital Spirit"),
+ ("ABILITY_WHITE_SMOKE", "White Smoke"),
+ ("ABILITY_PURE_POWER", "Pure Power"),
+ ("ABILITY_SHELL_ARMOR", "Shell Armor"),
+ ("ABILITY_CACOPHONY", "Cacophony"),
+ ("ABILITY_AIR_LOCK", "Air Lock")
+ ]]
+
+ # Move labels
+ data.move_labels = {r: data.constants[l] for l, r in [
+ ("MOVE_POUND", "Pound"),
+ ("MOVE_KARATE_CHOP", "Karate Chop"),
+ ("MOVE_DOUBLE_SLAP", "Doubleslap"),
+ ("MOVE_COMET_PUNCH", "Comet Punch"),
+ ("MOVE_MEGA_PUNCH", "Mega Punch"),
+ ("MOVE_PAY_DAY", "Pay Day"),
+ ("MOVE_FIRE_PUNCH", "Fire Punch"),
+ ("MOVE_ICE_PUNCH", "Ice Punch"),
+ ("MOVE_THUNDER_PUNCH", "Thunderpunch"),
+ ("MOVE_SCRATCH", "Scratch"),
+ ("MOVE_VICE_GRIP", "Vicegrip"),
+ ("MOVE_GUILLOTINE", "Guillotine"),
+ ("MOVE_RAZOR_WIND", "Razor Wind"),
+ ("MOVE_SWORDS_DANCE", "Swords Dance"),
+ ("MOVE_CUT", "Cut"),
+ ("MOVE_GUST", "Gust"),
+ ("MOVE_WING_ATTACK", "Wing Attack"),
+ ("MOVE_WHIRLWIND", "Whirlwind"),
+ ("MOVE_FLY", "Fly"),
+ ("MOVE_BIND", "Bind"),
+ ("MOVE_SLAM", "Slam"),
+ ("MOVE_VINE_WHIP", "Vine Whip"),
+ ("MOVE_STOMP", "Stomp"),
+ ("MOVE_DOUBLE_KICK", "Double Kick"),
+ ("MOVE_MEGA_KICK", "Mega Kick"),
+ ("MOVE_JUMP_KICK", "Jump Kick"),
+ ("MOVE_ROLLING_KICK", "Rolling Kick"),
+ ("MOVE_SAND_ATTACK", "Sand-Attack"),
+ ("MOVE_HEADBUTT", "Headbutt"),
+ ("MOVE_HORN_ATTACK", "Horn Attack"),
+ ("MOVE_FURY_ATTACK", "Fury Attack"),
+ ("MOVE_HORN_DRILL", "Horn Drill"),
+ ("MOVE_TACKLE", "Tackle"),
+ ("MOVE_BODY_SLAM", "Body Slam"),
+ ("MOVE_WRAP", "Wrap"),
+ ("MOVE_TAKE_DOWN", "Take Down"),
+ ("MOVE_THRASH", "Thrash"),
+ ("MOVE_DOUBLE_EDGE", "Double-Edge"),
+ ("MOVE_TAIL_WHIP", "Tail Whip"),
+ ("MOVE_POISON_STING", "Poison Sting"),
+ ("MOVE_TWINEEDLE", "Twineedle"),
+ ("MOVE_PIN_MISSILE", "Pin Missile"),
+ ("MOVE_LEER", "Leer"),
+ ("MOVE_BITE", "Bite"),
+ ("MOVE_GROWL", "Growl"),
+ ("MOVE_ROAR", "Roar"),
+ ("MOVE_SING", "Sing"),
+ ("MOVE_SUPERSONIC", "Supersonic"),
+ ("MOVE_SONIC_BOOM", "Sonicboom"),
+ ("MOVE_DISABLE", "Disable"),
+ ("MOVE_ACID", "Acid"),
+ ("MOVE_EMBER", "Ember"),
+ ("MOVE_FLAMETHROWER", "Flamethrower"),
+ ("MOVE_MIST", "Mist"),
+ ("MOVE_WATER_GUN", "Water Gun"),
+ ("MOVE_HYDRO_PUMP", "Hydro Pump"),
+ ("MOVE_SURF", "Surf"),
+ ("MOVE_ICE_BEAM", "Ice Beam"),
+ ("MOVE_BLIZZARD", "Blizzard"),
+ ("MOVE_PSYBEAM", "Psybeam"),
+ ("MOVE_BUBBLE_BEAM", "Bubblebeam"),
+ ("MOVE_AURORA_BEAM", "Aurora Beam"),
+ ("MOVE_HYPER_BEAM", "Hyper Beam"),
+ ("MOVE_PECK", "Peck"),
+ ("MOVE_DRILL_PECK", "Drill Peck"),
+ ("MOVE_SUBMISSION", "Submission"),
+ ("MOVE_LOW_KICK", "Low Kick"),
+ ("MOVE_COUNTER", "Counter"),
+ ("MOVE_SEISMIC_TOSS", "Seismic Toss"),
+ ("MOVE_STRENGTH", "Strength"),
+ ("MOVE_ABSORB", "Absorb"),
+ ("MOVE_MEGA_DRAIN", "Mega Drain"),
+ ("MOVE_LEECH_SEED", "Leech Seed"),
+ ("MOVE_GROWTH", "Growth"),
+ ("MOVE_RAZOR_LEAF", "Razor Leaf"),
+ ("MOVE_SOLAR_BEAM", "Solarbeam"),
+ ("MOVE_POISON_POWDER", "Poisonpowder"),
+ ("MOVE_STUN_SPORE", "Stun Spore"),
+ ("MOVE_SLEEP_POWDER", "Sleep Powder"),
+ ("MOVE_PETAL_DANCE", "Petal Dance"),
+ ("MOVE_STRING_SHOT", "String Shot"),
+ ("MOVE_DRAGON_RAGE", "Dragon Rage"),
+ ("MOVE_FIRE_SPIN", "Fire Spin"),
+ ("MOVE_THUNDER_SHOCK", "Thundershock"),
+ ("MOVE_THUNDERBOLT", "Thunderbolt"),
+ ("MOVE_THUNDER_WAVE", "Thunder Wave"),
+ ("MOVE_THUNDER", "Thunder"),
+ ("MOVE_ROCK_THROW", "Rock Throw"),
+ ("MOVE_EARTHQUAKE", "Earthquake"),
+ ("MOVE_FISSURE", "Fissure"),
+ ("MOVE_DIG", "Dig"),
+ ("MOVE_TOXIC", "Toxic"),
+ ("MOVE_CONFUSION", "Confusion"),
+ ("MOVE_PSYCHIC", "Psychic"),
+ ("MOVE_HYPNOSIS", "Hypnosis"),
+ ("MOVE_MEDITATE", "Meditate"),
+ ("MOVE_AGILITY", "Agility"),
+ ("MOVE_QUICK_ATTACK", "Quick Attack"),
+ ("MOVE_RAGE", "Rage"),
+ ("MOVE_TELEPORT", "Teleport"),
+ ("MOVE_NIGHT_SHADE", "Night Shade"),
+ ("MOVE_MIMIC", "Mimic"),
+ ("MOVE_SCREECH", "Screech"),
+ ("MOVE_DOUBLE_TEAM", "Double Team"),
+ ("MOVE_RECOVER", "Recover"),
+ ("MOVE_HARDEN", "Harden"),
+ ("MOVE_MINIMIZE", "Minimize"),
+ ("MOVE_SMOKESCREEN", "Smokescreen"),
+ ("MOVE_CONFUSE_RAY", "Confuse Ray"),
+ ("MOVE_WITHDRAW", "Withdraw"),
+ ("MOVE_DEFENSE_CURL", "Defense Curl"),
+ ("MOVE_BARRIER", "Barrier"),
+ ("MOVE_LIGHT_SCREEN", "Light Screen"),
+ ("MOVE_HAZE", "Haze"),
+ ("MOVE_REFLECT", "Reflect"),
+ ("MOVE_FOCUS_ENERGY", "Focus Energy"),
+ ("MOVE_BIDE", "Bide"),
+ ("MOVE_METRONOME", "Metronome"),
+ ("MOVE_MIRROR_MOVE", "Mirror Move"),
+ ("MOVE_SELF_DESTRUCT", "Selfdestruct"),
+ ("MOVE_EGG_BOMB", "Egg Bomb"),
+ ("MOVE_LICK", "Lick"),
+ ("MOVE_SMOG", "Smog"),
+ ("MOVE_SLUDGE", "Sludge"),
+ ("MOVE_BONE_CLUB", "Bone Club"),
+ ("MOVE_FIRE_BLAST", "Fire Blast"),
+ ("MOVE_WATERFALL", "Waterfall"),
+ ("MOVE_CLAMP", "Clamp"),
+ ("MOVE_SWIFT", "Swift"),
+ ("MOVE_SKULL_BASH", "Skull Bash"),
+ ("MOVE_SPIKE_CANNON", "Spike Cannon"),
+ ("MOVE_CONSTRICT", "Constrict"),
+ ("MOVE_AMNESIA", "Amnesia"),
+ ("MOVE_KINESIS", "Kinesis"),
+ ("MOVE_SOFT_BOILED", "Softboiled"),
+ ("MOVE_HI_JUMP_KICK", "Hi Jump Kick"),
+ ("MOVE_GLARE", "Glare"),
+ ("MOVE_DREAM_EATER", "Dream Eater"),
+ ("MOVE_POISON_GAS", "Poison Gas"),
+ ("MOVE_BARRAGE", "Barrage"),
+ ("MOVE_LEECH_LIFE", "Leech Life"),
+ ("MOVE_LOVELY_KISS", "Lovely Kiss"),
+ ("MOVE_SKY_ATTACK", "Sky Attack"),
+ ("MOVE_TRANSFORM", "Transform"),
+ ("MOVE_BUBBLE", "Bubble"),
+ ("MOVE_DIZZY_PUNCH", "Dizzy Punch"),
+ ("MOVE_SPORE", "Spore"),
+ ("MOVE_FLASH", "Flash"),
+ ("MOVE_PSYWAVE", "Psywave"),
+ ("MOVE_SPLASH", "Splash"),
+ ("MOVE_ACID_ARMOR", "Acid Armor"),
+ ("MOVE_CRABHAMMER", "Crabhammer"),
+ ("MOVE_EXPLOSION", "Explosion"),
+ ("MOVE_FURY_SWIPES", "Fury Swipes"),
+ ("MOVE_BONEMERANG", "Bonemerang"),
+ ("MOVE_REST", "Rest"),
+ ("MOVE_ROCK_SLIDE", "Rock Slide"),
+ ("MOVE_HYPER_FANG", "Hyper Fang"),
+ ("MOVE_SHARPEN", "Sharpen"),
+ ("MOVE_CONVERSION", "Conversion"),
+ ("MOVE_TRI_ATTACK", "Tri Attack"),
+ ("MOVE_SUPER_FANG", "Super Fang"),
+ ("MOVE_SLASH", "Slash"),
+ ("MOVE_SUBSTITUTE", "Substitute"),
+ ("MOVE_SKETCH", "Sketch"),
+ ("MOVE_TRIPLE_KICK", "Triple Kick"),
+ ("MOVE_THIEF", "Thief"),
+ ("MOVE_SPIDER_WEB", "Spider Web"),
+ ("MOVE_MIND_READER", "Mind Reader"),
+ ("MOVE_NIGHTMARE", "Nightmare"),
+ ("MOVE_FLAME_WHEEL", "Flame Wheel"),
+ ("MOVE_SNORE", "Snore"),
+ ("MOVE_CURSE", "Curse"),
+ ("MOVE_FLAIL", "Flail"),
+ ("MOVE_CONVERSION_2", "Conversion 2"),
+ ("MOVE_AEROBLAST", "Aeroblast"),
+ ("MOVE_COTTON_SPORE", "Cotton Spore"),
+ ("MOVE_REVERSAL", "Reversal"),
+ ("MOVE_SPITE", "Spite"),
+ ("MOVE_POWDER_SNOW", "Powder Snow"),
+ ("MOVE_PROTECT", "Protect"),
+ ("MOVE_MACH_PUNCH", "Mach Punch"),
+ ("MOVE_SCARY_FACE", "Scary Face"),
+ ("MOVE_FAINT_ATTACK", "Faint Attack"),
+ ("MOVE_SWEET_KISS", "Sweet Kiss"),
+ ("MOVE_BELLY_DRUM", "Belly Drum"),
+ ("MOVE_SLUDGE_BOMB", "Sludge Bomb"),
+ ("MOVE_MUD_SLAP", "Mud-Slap"),
+ ("MOVE_OCTAZOOKA", "Octazooka"),
+ ("MOVE_SPIKES", "Spikes"),
+ ("MOVE_ZAP_CANNON", "Zap Cannon"),
+ ("MOVE_FORESIGHT", "Foresight"),
+ ("MOVE_DESTINY_BOND", "Destiny Bond"),
+ ("MOVE_PERISH_SONG", "Perish Song"),
+ ("MOVE_ICY_WIND", "Icy Wind"),
+ ("MOVE_DETECT", "Detect"),
+ ("MOVE_BONE_RUSH", "Bone Rush"),
+ ("MOVE_LOCK_ON", "Lock-On"),
+ ("MOVE_OUTRAGE", "Outrage"),
+ ("MOVE_SANDSTORM", "Sandstorm"),
+ ("MOVE_GIGA_DRAIN", "Giga Drain"),
+ ("MOVE_ENDURE", "Endure"),
+ ("MOVE_CHARM", "Charm"),
+ ("MOVE_ROLLOUT", "Rollout"),
+ ("MOVE_FALSE_SWIPE", "False Swipe"),
+ ("MOVE_SWAGGER", "Swagger"),
+ ("MOVE_MILK_DRINK", "Milk Drink"),
+ ("MOVE_SPARK", "Spark"),
+ ("MOVE_FURY_CUTTER", "Fury Cutter"),
+ ("MOVE_STEEL_WING", "Steel Wing"),
+ ("MOVE_MEAN_LOOK", "Mean Look"),
+ ("MOVE_ATTRACT", "Attract"),
+ ("MOVE_SLEEP_TALK", "Sleep Talk"),
+ ("MOVE_HEAL_BELL", "Heal Bell"),
+ ("MOVE_RETURN", "Return"),
+ ("MOVE_PRESENT", "Present"),
+ ("MOVE_FRUSTRATION", "Frustration"),
+ ("MOVE_SAFEGUARD", "Safeguard"),
+ ("MOVE_PAIN_SPLIT", "Pain Split"),
+ ("MOVE_SACRED_FIRE", "Sacred Fire"),
+ ("MOVE_MAGNITUDE", "Magnitude"),
+ ("MOVE_DYNAMIC_PUNCH", "Dynamicpunch"),
+ ("MOVE_MEGAHORN", "Megahorn"),
+ ("MOVE_DRAGON_BREATH", "Dragonbreath"),
+ ("MOVE_BATON_PASS", "Baton Pass"),
+ ("MOVE_ENCORE", "Encore"),
+ ("MOVE_PURSUIT", "Pursuit"),
+ ("MOVE_RAPID_SPIN", "Rapid Spin"),
+ ("MOVE_SWEET_SCENT", "Sweet Scent"),
+ ("MOVE_IRON_TAIL", "Iron Tail"),
+ ("MOVE_METAL_CLAW", "Metal Claw"),
+ ("MOVE_VITAL_THROW", "Vital Throw"),
+ ("MOVE_MORNING_SUN", "Morning Sun"),
+ ("MOVE_SYNTHESIS", "Synthesis"),
+ ("MOVE_MOONLIGHT", "Moonlight"),
+ ("MOVE_HIDDEN_POWER", "Hidden Power"),
+ ("MOVE_CROSS_CHOP", "Cross Chop"),
+ ("MOVE_TWISTER", "Twister"),
+ ("MOVE_RAIN_DANCE", "Rain Dance"),
+ ("MOVE_SUNNY_DAY", "Sunny Day"),
+ ("MOVE_CRUNCH", "Crunch"),
+ ("MOVE_MIRROR_COAT", "Mirror Coat"),
+ ("MOVE_PSYCH_UP", "Psych Up"),
+ ("MOVE_EXTREME_SPEED", "Extremespeed"),
+ ("MOVE_ANCIENT_POWER", "Ancientpower"),
+ ("MOVE_SHADOW_BALL", "Shadow Ball"),
+ ("MOVE_FUTURE_SIGHT", "Future Sight"),
+ ("MOVE_ROCK_SMASH", "Rock Smash"),
+ ("MOVE_WHIRLPOOL", "Whirlpool"),
+ ("MOVE_BEAT_UP", "Beat Up"),
+ ("MOVE_FAKE_OUT", "Fake Out"),
+ ("MOVE_UPROAR", "Uproar"),
+ ("MOVE_STOCKPILE", "Stockpile"),
+ ("MOVE_SPIT_UP", "Spit Up"),
+ ("MOVE_SWALLOW", "Swallow"),
+ ("MOVE_HEAT_WAVE", "Heat Wave"),
+ ("MOVE_HAIL", "Hail"),
+ ("MOVE_TORMENT", "Torment"),
+ ("MOVE_FLATTER", "Flatter"),
+ ("MOVE_WILL_O_WISP", "Will-O-Wisp"),
+ ("MOVE_MEMENTO", "Memento"),
+ ("MOVE_FACADE", "Facade"),
+ ("MOVE_FOCUS_PUNCH", "Focus Punch"),
+ ("MOVE_SMELLING_SALT", "Smellingsalt"),
+ ("MOVE_FOLLOW_ME", "Follow Me"),
+ ("MOVE_NATURE_POWER", "Nature Power"),
+ ("MOVE_CHARGE", "Charge"),
+ ("MOVE_TAUNT", "Taunt"),
+ ("MOVE_HELPING_HAND", "Helping Hand"),
+ ("MOVE_TRICK", "Trick"),
+ ("MOVE_ROLE_PLAY", "Role Play"),
+ ("MOVE_WISH", "Wish"),
+ ("MOVE_ASSIST", "Assist"),
+ ("MOVE_INGRAIN", "Ingrain"),
+ ("MOVE_SUPERPOWER", "Superpower"),
+ ("MOVE_MAGIC_COAT", "Magic Coat"),
+ ("MOVE_RECYCLE", "Recycle"),
+ ("MOVE_REVENGE", "Revenge"),
+ ("MOVE_BRICK_BREAK", "Brick Break"),
+ ("MOVE_YAWN", "Yawn"),
+ ("MOVE_KNOCK_OFF", "Knock Off"),
+ ("MOVE_ENDEAVOR", "Endeavor"),
+ ("MOVE_ERUPTION", "Eruption"),
+ ("MOVE_SKILL_SWAP", "Skill Swap"),
+ ("MOVE_IMPRISON", "Imprison"),
+ ("MOVE_REFRESH", "Refresh"),
+ ("MOVE_GRUDGE", "Grudge"),
+ ("MOVE_SNATCH", "Snatch"),
+ ("MOVE_SECRET_POWER", "Secret Power"),
+ ("MOVE_DIVE", "Dive"),
+ ("MOVE_ARM_THRUST", "Arm Thrust"),
+ ("MOVE_CAMOUFLAGE", "Camouflage"),
+ ("MOVE_TAIL_GLOW", "Tail Glow"),
+ ("MOVE_LUSTER_PURGE", "Luster Purge"),
+ ("MOVE_MIST_BALL", "Mist Ball"),
+ ("MOVE_FEATHER_DANCE", "Featherdance"),
+ ("MOVE_TEETER_DANCE", "Teeter Dance"),
+ ("MOVE_BLAZE_KICK", "Blaze Kick"),
+ ("MOVE_MUD_SPORT", "Mud Sport"),
+ ("MOVE_ICE_BALL", "Ice Ball"),
+ ("MOVE_NEEDLE_ARM", "Needle Arm"),
+ ("MOVE_SLACK_OFF", "Slack Off"),
+ ("MOVE_HYPER_VOICE", "Hyper Voice"),
+ ("MOVE_POISON_FANG", "Poison Fang"),
+ ("MOVE_CRUSH_CLAW", "Crush Claw"),
+ ("MOVE_BLAST_BURN", "Blast Burn"),
+ ("MOVE_HYDRO_CANNON", "Hydro Cannon"),
+ ("MOVE_METEOR_MASH", "Meteor Mash"),
+ ("MOVE_ASTONISH", "Astonish"),
+ ("MOVE_WEATHER_BALL", "Weather Ball"),
+ ("MOVE_AROMATHERAPY", "Aromatherapy"),
+ ("MOVE_FAKE_TEARS", "Fake Tears"),
+ ("MOVE_AIR_CUTTER", "Air Cutter"),
+ ("MOVE_OVERHEAT", "Overheat"),
+ ("MOVE_ODOR_SLEUTH", "Odor Sleuth"),
+ ("MOVE_ROCK_TOMB", "Rock Tomb"),
+ ("MOVE_SILVER_WIND", "Silver Wind"),
+ ("MOVE_METAL_SOUND", "Metal Sound"),
+ ("MOVE_GRASS_WHISTLE", "Grasswhistle"),
+ ("MOVE_TICKLE", "Tickle"),
+ ("MOVE_COSMIC_POWER", "Cosmic Power"),
+ ("MOVE_WATER_SPOUT", "Water Spout"),
+ ("MOVE_SIGNAL_BEAM", "Signal Beam"),
+ ("MOVE_SHADOW_PUNCH", "Shadow Punch"),
+ ("MOVE_EXTRASENSORY", "Extrasensory"),
+ ("MOVE_SKY_UPPERCUT", "Sky Uppercut"),
+ ("MOVE_SAND_TOMB", "Sand Tomb"),
+ ("MOVE_SHEER_COLD", "Sheer Cold"),
+ ("MOVE_MUDDY_WATER", "Muddy Water"),
+ ("MOVE_BULLET_SEED", "Bullet Seed"),
+ ("MOVE_AERIAL_ACE", "Aerial Ace"),
+ ("MOVE_ICICLE_SPEAR", "Icicle Spear"),
+ ("MOVE_IRON_DEFENSE", "Iron Defense"),
+ ("MOVE_BLOCK", "Block"),
+ ("MOVE_HOWL", "Howl"),
+ ("MOVE_DRAGON_CLAW", "Dragon Claw"),
+ ("MOVE_FRENZY_PLANT", "Frenzy Plant"),
+ ("MOVE_BULK_UP", "Bulk Up"),
+ ("MOVE_BOUNCE", "Bounce"),
+ ("MOVE_MUD_SHOT", "Mud Shot"),
+ ("MOVE_POISON_TAIL", "Poison Tail"),
+ ("MOVE_COVET", "Covet"),
+ ("MOVE_VOLT_TACKLE", "Volt Tackle"),
+ ("MOVE_MAGICAL_LEAF", "Magical Leaf"),
+ ("MOVE_WATER_SPORT", "Water Sport"),
+ ("MOVE_CALM_MIND", "Calm Mind"),
+ ("MOVE_LEAF_BLADE", "Leaf Blade"),
+ ("MOVE_DRAGON_DANCE", "Dragon Dance"),
+ ("MOVE_ROCK_BLAST", "Rock Blast"),
+ ("MOVE_SHOCK_WAVE", "Shock Wave"),
+ ("MOVE_WATER_PULSE", "Water Pulse"),
+ ("MOVE_DOOM_DESIRE", "Doom Desire"),
+ ("MOVE_PSYCHO_BOOST", "Psycho Boost"),
+ ]}
+
+ # Create warp map
+ for warp, destination in extracted_data["warps"].items():
+ data.warp_map[warp] = None if destination == "" else destination
+
+ if encoded_warp not in data.warp_map:
+ data.warp_map[encoded_warp] = None
+
+ # Create trainer data
+ for i, trainer_json in enumerate(extracted_data["trainers"]):
+ party_json = trainer_json["party"]
+ pokemon_data_type = _str_to_pokemon_data_type(trainer_json["data_type"])
+ data.trainers.append(TrainerData(
+ i,
+ TrainerPartyData(
+ [TrainerPokemonData(
+ p["species"],
+ p["level"],
+ (p["moves"][0], p["moves"][1], p["moves"][2], p["moves"][3]) if "moves" in p else None
+ ) for p in party_json],
+ pokemon_data_type,
+ trainer_json["party_address"]
+ ),
+ trainer_json["address"],
+ trainer_json["script_address"],
+ trainer_json["battle_type"]
+ ))
+
+
+data = PokemonEmeraldData()
+_init()
+
+LEGENDARY_POKEMON = frozenset([data.constants[species] for species in [
+ "SPECIES_ARTICUNO",
+ "SPECIES_ZAPDOS",
+ "SPECIES_MOLTRES",
+ "SPECIES_MEWTWO",
+ "SPECIES_MEW",
+ "SPECIES_RAIKOU",
+ "SPECIES_ENTEI",
+ "SPECIES_SUICUNE",
+ "SPECIES_LUGIA",
+ "SPECIES_HO_OH",
+ "SPECIES_CELEBI",
+ "SPECIES_REGIROCK",
+ "SPECIES_REGICE",
+ "SPECIES_REGISTEEL",
+ "SPECIES_LATIAS",
+ "SPECIES_LATIOS",
+ "SPECIES_KYOGRE",
+ "SPECIES_GROUDON",
+ "SPECIES_RAYQUAZA",
+ "SPECIES_JIRACHI",
+ "SPECIES_DEOXYS",
+]])
+"""Species IDs of legendary pokemon"""
+
+UNEVOLVED_POKEMON = frozenset({
+ species.species_id
+ for species in data.species.values()
+ if len(species.evolutions) > 0
+})
+"""Species IDs of pokemon which have further evolution stages in the vanilla game"""
+
+NATIONAL_ID_TO_SPECIES_ID = {species.national_dex_number: i for i, species in data.species.items()}
diff --git a/worlds/pokemon_emerald/data/base_patch.bsdiff4 b/worlds/pokemon_emerald/data/base_patch.bsdiff4
new file mode 100644
index 000000000000..0da226f617f6
Binary files /dev/null and b/worlds/pokemon_emerald/data/base_patch.bsdiff4 differ
diff --git a/worlds/pokemon_emerald/data/extracted_data.json b/worlds/pokemon_emerald/data/extracted_data.json
new file mode 100644
index 000000000000..fcc2cf24e7b7
--- /dev/null
+++ b/worlds/pokemon_emerald/data/extracted_data.json
@@ -0,0 +1 @@
+{"_comment":"DO NOT MODIFY. This file was auto-generated. Your changes will likely be overwritten.","_rom_name":"pokemon emerald version / AP 5","constants":{"ABILITIES_COUNT":78,"ABILITY_AIR_LOCK":77,"ABILITY_ARENA_TRAP":71,"ABILITY_BATTLE_ARMOR":4,"ABILITY_BLAZE":66,"ABILITY_CACOPHONY":76,"ABILITY_CHLOROPHYLL":34,"ABILITY_CLEAR_BODY":29,"ABILITY_CLOUD_NINE":13,"ABILITY_COLOR_CHANGE":16,"ABILITY_COMPOUND_EYES":14,"ABILITY_CUTE_CHARM":56,"ABILITY_DAMP":6,"ABILITY_DRIZZLE":2,"ABILITY_DROUGHT":70,"ABILITY_EARLY_BIRD":48,"ABILITY_EFFECT_SPORE":27,"ABILITY_FLAME_BODY":49,"ABILITY_FLASH_FIRE":18,"ABILITY_FORECAST":59,"ABILITY_GUTS":62,"ABILITY_HUGE_POWER":37,"ABILITY_HUSTLE":55,"ABILITY_HYPER_CUTTER":52,"ABILITY_ILLUMINATE":35,"ABILITY_IMMUNITY":17,"ABILITY_INNER_FOCUS":39,"ABILITY_INSOMNIA":15,"ABILITY_INTIMIDATE":22,"ABILITY_KEEN_EYE":51,"ABILITY_LEVITATE":26,"ABILITY_LIGHTNING_ROD":31,"ABILITY_LIMBER":7,"ABILITY_LIQUID_OOZE":64,"ABILITY_MAGMA_ARMOR":40,"ABILITY_MAGNET_PULL":42,"ABILITY_MARVEL_SCALE":63,"ABILITY_MINUS":58,"ABILITY_NATURAL_CURE":30,"ABILITY_NONE":0,"ABILITY_OBLIVIOUS":12,"ABILITY_OVERGROW":65,"ABILITY_OWN_TEMPO":20,"ABILITY_PICKUP":53,"ABILITY_PLUS":57,"ABILITY_POISON_POINT":38,"ABILITY_PRESSURE":46,"ABILITY_PURE_POWER":74,"ABILITY_RAIN_DISH":44,"ABILITY_ROCK_HEAD":69,"ABILITY_ROUGH_SKIN":24,"ABILITY_RUN_AWAY":50,"ABILITY_SAND_STREAM":45,"ABILITY_SAND_VEIL":8,"ABILITY_SERENE_GRACE":32,"ABILITY_SHADOW_TAG":23,"ABILITY_SHED_SKIN":61,"ABILITY_SHELL_ARMOR":75,"ABILITY_SHIELD_DUST":19,"ABILITY_SOUNDPROOF":43,"ABILITY_SPEED_BOOST":3,"ABILITY_STATIC":9,"ABILITY_STENCH":1,"ABILITY_STICKY_HOLD":60,"ABILITY_STURDY":5,"ABILITY_SUCTION_CUPS":21,"ABILITY_SWARM":68,"ABILITY_SWIFT_SWIM":33,"ABILITY_SYNCHRONIZE":28,"ABILITY_THICK_FAT":47,"ABILITY_TORRENT":67,"ABILITY_TRACE":36,"ABILITY_TRUANT":54,"ABILITY_VITAL_SPIRIT":72,"ABILITY_VOLT_ABSORB":10,"ABILITY_WATER_ABSORB":11,"ABILITY_WATER_VEIL":41,"ABILITY_WHITE_SMOKE":73,"ABILITY_WONDER_GUARD":25,"ACRO_BIKE":1,"BAG_ITEM_CAPACITY_DIGITS":2,"BERRY_CAPACITY_DIGITS":3,"BERRY_FIRMNESS_HARD":3,"BERRY_FIRMNESS_SOFT":2,"BERRY_FIRMNESS_SUPER_HARD":5,"BERRY_FIRMNESS_UNKNOWN":0,"BERRY_FIRMNESS_VERY_HARD":4,"BERRY_FIRMNESS_VERY_SOFT":1,"BERRY_NONE":0,"BERRY_STAGE_BERRIES":5,"BERRY_STAGE_FLOWERING":4,"BERRY_STAGE_NO_BERRY":0,"BERRY_STAGE_PLANTED":1,"BERRY_STAGE_SPARKLING":255,"BERRY_STAGE_SPROUTED":2,"BERRY_STAGE_TALLER":3,"BERRY_TREES_COUNT":128,"BERRY_TREE_ROUTE_102_ORAN":2,"BERRY_TREE_ROUTE_102_PECHA":1,"BERRY_TREE_ROUTE_103_CHERI_1":5,"BERRY_TREE_ROUTE_103_CHERI_2":7,"BERRY_TREE_ROUTE_103_LEPPA":6,"BERRY_TREE_ROUTE_104_CHERI_1":8,"BERRY_TREE_ROUTE_104_CHERI_2":76,"BERRY_TREE_ROUTE_104_LEPPA":10,"BERRY_TREE_ROUTE_104_ORAN_1":4,"BERRY_TREE_ROUTE_104_ORAN_2":11,"BERRY_TREE_ROUTE_104_PECHA":13,"BERRY_TREE_ROUTE_104_SOIL_1":3,"BERRY_TREE_ROUTE_104_SOIL_2":9,"BERRY_TREE_ROUTE_104_SOIL_3":12,"BERRY_TREE_ROUTE_104_SOIL_4":75,"BERRY_TREE_ROUTE_110_NANAB_1":16,"BERRY_TREE_ROUTE_110_NANAB_2":17,"BERRY_TREE_ROUTE_110_NANAB_3":18,"BERRY_TREE_ROUTE_111_ORAN_1":80,"BERRY_TREE_ROUTE_111_ORAN_2":81,"BERRY_TREE_ROUTE_111_RAZZ_1":19,"BERRY_TREE_ROUTE_111_RAZZ_2":20,"BERRY_TREE_ROUTE_112_PECHA_1":22,"BERRY_TREE_ROUTE_112_PECHA_2":23,"BERRY_TREE_ROUTE_112_RAWST_1":21,"BERRY_TREE_ROUTE_112_RAWST_2":24,"BERRY_TREE_ROUTE_114_PERSIM_1":68,"BERRY_TREE_ROUTE_114_PERSIM_2":77,"BERRY_TREE_ROUTE_114_PERSIM_3":78,"BERRY_TREE_ROUTE_115_BLUK_1":55,"BERRY_TREE_ROUTE_115_BLUK_2":56,"BERRY_TREE_ROUTE_115_KELPSY_1":69,"BERRY_TREE_ROUTE_115_KELPSY_2":70,"BERRY_TREE_ROUTE_115_KELPSY_3":71,"BERRY_TREE_ROUTE_116_CHESTO_1":26,"BERRY_TREE_ROUTE_116_CHESTO_2":66,"BERRY_TREE_ROUTE_116_PINAP_1":25,"BERRY_TREE_ROUTE_116_PINAP_2":67,"BERRY_TREE_ROUTE_117_WEPEAR_1":27,"BERRY_TREE_ROUTE_117_WEPEAR_2":28,"BERRY_TREE_ROUTE_117_WEPEAR_3":29,"BERRY_TREE_ROUTE_118_SITRUS_1":31,"BERRY_TREE_ROUTE_118_SITRUS_2":33,"BERRY_TREE_ROUTE_118_SOIL":32,"BERRY_TREE_ROUTE_119_HONDEW_1":83,"BERRY_TREE_ROUTE_119_HONDEW_2":84,"BERRY_TREE_ROUTE_119_LEPPA":86,"BERRY_TREE_ROUTE_119_POMEG_1":34,"BERRY_TREE_ROUTE_119_POMEG_2":35,"BERRY_TREE_ROUTE_119_POMEG_3":36,"BERRY_TREE_ROUTE_119_SITRUS":85,"BERRY_TREE_ROUTE_120_ASPEAR_1":37,"BERRY_TREE_ROUTE_120_ASPEAR_2":38,"BERRY_TREE_ROUTE_120_ASPEAR_3":39,"BERRY_TREE_ROUTE_120_NANAB":44,"BERRY_TREE_ROUTE_120_PECHA_1":40,"BERRY_TREE_ROUTE_120_PECHA_2":41,"BERRY_TREE_ROUTE_120_PECHA_3":42,"BERRY_TREE_ROUTE_120_PINAP":45,"BERRY_TREE_ROUTE_120_RAZZ":43,"BERRY_TREE_ROUTE_120_WEPEAR":46,"BERRY_TREE_ROUTE_121_ASPEAR":48,"BERRY_TREE_ROUTE_121_CHESTO":50,"BERRY_TREE_ROUTE_121_NANAB_1":52,"BERRY_TREE_ROUTE_121_NANAB_2":53,"BERRY_TREE_ROUTE_121_PERSIM":47,"BERRY_TREE_ROUTE_121_RAWST":49,"BERRY_TREE_ROUTE_121_SOIL_1":51,"BERRY_TREE_ROUTE_121_SOIL_2":54,"BERRY_TREE_ROUTE_123_GREPA_1":60,"BERRY_TREE_ROUTE_123_GREPA_2":61,"BERRY_TREE_ROUTE_123_GREPA_3":65,"BERRY_TREE_ROUTE_123_GREPA_4":72,"BERRY_TREE_ROUTE_123_LEPPA_1":62,"BERRY_TREE_ROUTE_123_LEPPA_2":64,"BERRY_TREE_ROUTE_123_PECHA":87,"BERRY_TREE_ROUTE_123_POMEG_1":15,"BERRY_TREE_ROUTE_123_POMEG_2":30,"BERRY_TREE_ROUTE_123_POMEG_3":58,"BERRY_TREE_ROUTE_123_POMEG_4":59,"BERRY_TREE_ROUTE_123_QUALOT_1":14,"BERRY_TREE_ROUTE_123_QUALOT_2":73,"BERRY_TREE_ROUTE_123_QUALOT_3":74,"BERRY_TREE_ROUTE_123_QUALOT_4":79,"BERRY_TREE_ROUTE_123_RAWST":57,"BERRY_TREE_ROUTE_123_SITRUS":88,"BERRY_TREE_ROUTE_123_SOIL":63,"BERRY_TREE_ROUTE_130_LIECHI":82,"DAILY_FLAGS_END":2399,"DAILY_FLAGS_START":2336,"FIRST_BALL":1,"FIRST_BERRY_INDEX":133,"FIRST_BERRY_MASTER_BERRY":153,"FIRST_BERRY_MASTER_WIFE_BERRY":133,"FIRST_KIRI_BERRY":153,"FIRST_MAIL_INDEX":121,"FIRST_ROUTE_114_MAN_BERRY":148,"FLAGS_COUNT":2400,"FLAG_ADDED_MATCH_CALL_TO_POKENAV":304,"FLAG_ADVENTURE_STARTED":116,"FLAG_ARRIVED_AT_MARINE_CAVE_EMERGE_SPOT":2265,"FLAG_ARRIVED_AT_NAVEL_ROCK":2273,"FLAG_ARRIVED_AT_TERRA_CAVE_ENTRANCE":2266,"FLAG_ARRIVED_ON_FARAWAY_ISLAND":2264,"FLAG_BADGE01_GET":2151,"FLAG_BADGE02_GET":2152,"FLAG_BADGE03_GET":2153,"FLAG_BADGE04_GET":2154,"FLAG_BADGE05_GET":2155,"FLAG_BADGE06_GET":2156,"FLAG_BADGE07_GET":2157,"FLAG_BADGE08_GET":2158,"FLAG_BATTLE_FRONTIER_TRADE_DONE":156,"FLAG_BEAT_MAGMA_GRUNT_JAGGED_PASS":313,"FLAG_BEAUTY_PAINTING_MADE":161,"FLAG_BERRY_MASTERS_WIFE":1197,"FLAG_BERRY_MASTER_RECEIVED_BERRY_1":1195,"FLAG_BERRY_MASTER_RECEIVED_BERRY_2":1196,"FLAG_BERRY_TREES_START":612,"FLAG_BERRY_TREE_01":612,"FLAG_BERRY_TREE_02":613,"FLAG_BERRY_TREE_03":614,"FLAG_BERRY_TREE_04":615,"FLAG_BERRY_TREE_05":616,"FLAG_BERRY_TREE_06":617,"FLAG_BERRY_TREE_07":618,"FLAG_BERRY_TREE_08":619,"FLAG_BERRY_TREE_09":620,"FLAG_BERRY_TREE_10":621,"FLAG_BERRY_TREE_11":622,"FLAG_BERRY_TREE_12":623,"FLAG_BERRY_TREE_13":624,"FLAG_BERRY_TREE_14":625,"FLAG_BERRY_TREE_15":626,"FLAG_BERRY_TREE_16":627,"FLAG_BERRY_TREE_17":628,"FLAG_BERRY_TREE_18":629,"FLAG_BERRY_TREE_19":630,"FLAG_BERRY_TREE_20":631,"FLAG_BERRY_TREE_21":632,"FLAG_BERRY_TREE_22":633,"FLAG_BERRY_TREE_23":634,"FLAG_BERRY_TREE_24":635,"FLAG_BERRY_TREE_25":636,"FLAG_BERRY_TREE_26":637,"FLAG_BERRY_TREE_27":638,"FLAG_BERRY_TREE_28":639,"FLAG_BERRY_TREE_29":640,"FLAG_BERRY_TREE_30":641,"FLAG_BERRY_TREE_31":642,"FLAG_BERRY_TREE_32":643,"FLAG_BERRY_TREE_33":644,"FLAG_BERRY_TREE_34":645,"FLAG_BERRY_TREE_35":646,"FLAG_BERRY_TREE_36":647,"FLAG_BERRY_TREE_37":648,"FLAG_BERRY_TREE_38":649,"FLAG_BERRY_TREE_39":650,"FLAG_BERRY_TREE_40":651,"FLAG_BERRY_TREE_41":652,"FLAG_BERRY_TREE_42":653,"FLAG_BERRY_TREE_43":654,"FLAG_BERRY_TREE_44":655,"FLAG_BERRY_TREE_45":656,"FLAG_BERRY_TREE_46":657,"FLAG_BERRY_TREE_47":658,"FLAG_BERRY_TREE_48":659,"FLAG_BERRY_TREE_49":660,"FLAG_BERRY_TREE_50":661,"FLAG_BERRY_TREE_51":662,"FLAG_BERRY_TREE_52":663,"FLAG_BERRY_TREE_53":664,"FLAG_BERRY_TREE_54":665,"FLAG_BERRY_TREE_55":666,"FLAG_BERRY_TREE_56":667,"FLAG_BERRY_TREE_57":668,"FLAG_BERRY_TREE_58":669,"FLAG_BERRY_TREE_59":670,"FLAG_BERRY_TREE_60":671,"FLAG_BERRY_TREE_61":672,"FLAG_BERRY_TREE_62":673,"FLAG_BERRY_TREE_63":674,"FLAG_BERRY_TREE_64":675,"FLAG_BERRY_TREE_65":676,"FLAG_BERRY_TREE_66":677,"FLAG_BERRY_TREE_67":678,"FLAG_BERRY_TREE_68":679,"FLAG_BERRY_TREE_69":680,"FLAG_BERRY_TREE_70":681,"FLAG_BERRY_TREE_71":682,"FLAG_BERRY_TREE_72":683,"FLAG_BERRY_TREE_73":684,"FLAG_BERRY_TREE_74":685,"FLAG_BERRY_TREE_75":686,"FLAG_BERRY_TREE_76":687,"FLAG_BERRY_TREE_77":688,"FLAG_BERRY_TREE_78":689,"FLAG_BERRY_TREE_79":690,"FLAG_BERRY_TREE_80":691,"FLAG_BERRY_TREE_81":692,"FLAG_BERRY_TREE_82":693,"FLAG_BERRY_TREE_83":694,"FLAG_BERRY_TREE_84":695,"FLAG_BERRY_TREE_85":696,"FLAG_BERRY_TREE_86":697,"FLAG_BERRY_TREE_87":698,"FLAG_BERRY_TREE_88":699,"FLAG_BETTER_SHOPS_ENABLED":206,"FLAG_BIRCH_AIDE_MET":88,"FLAG_CANCEL_BATTLE_ROOM_CHALLENGE":119,"FLAG_CAUGHT_DEOXYS":429,"FLAG_CAUGHT_GROUDON":480,"FLAG_CAUGHT_HO_OH":146,"FLAG_CAUGHT_KYOGRE":479,"FLAG_CAUGHT_LATIAS":457,"FLAG_CAUGHT_LATIOS":482,"FLAG_CAUGHT_LUGIA":145,"FLAG_CAUGHT_MEW":458,"FLAG_CAUGHT_RAYQUAZA":478,"FLAG_CAUGHT_REGICE":427,"FLAG_CAUGHT_REGIROCK":426,"FLAG_CAUGHT_REGISTEEL":483,"FLAG_CHOSEN_MULTI_BATTLE_NPC_PARTNER":338,"FLAG_CHOSE_CLAW_FOSSIL":336,"FLAG_CHOSE_ROOT_FOSSIL":335,"FLAG_COLLECTED_ALL_GOLD_SYMBOLS":466,"FLAG_COLLECTED_ALL_SILVER_SYMBOLS":92,"FLAG_CONTEST_SKETCH_CREATED":270,"FLAG_COOL_PAINTING_MADE":160,"FLAG_CUTE_PAINTING_MADE":162,"FLAG_DAILY_APPRENTICE_LEAVES":2356,"FLAG_DAILY_BERRY_MASTERS_WIFE":2353,"FLAG_DAILY_BERRY_MASTER_RECEIVED_BERRY":2349,"FLAG_DAILY_CONTEST_LOBBY_RECEIVED_BERRY":2337,"FLAG_DAILY_FLOWER_SHOP_RECEIVED_BERRY":2352,"FLAG_DAILY_LILYCOVE_RECEIVED_BERRY":2351,"FLAG_DAILY_PICKED_LOTO_TICKET":2346,"FLAG_DAILY_ROUTE_111_RECEIVED_BERRY":2348,"FLAG_DAILY_ROUTE_114_RECEIVED_BERRY":2347,"FLAG_DAILY_ROUTE_120_RECEIVED_BERRY":2350,"FLAG_DAILY_SECRET_BASE":2338,"FLAG_DAILY_SOOTOPOLIS_RECEIVED_BERRY":2354,"FLAG_DECLINED_BIKE":89,"FLAG_DECLINED_RIVAL_BATTLE_LILYCOVE":286,"FLAG_DECLINED_WALLY_BATTLE_MAUVILLE":284,"FLAG_DECORATION_1":174,"FLAG_DECORATION_10":183,"FLAG_DECORATION_11":184,"FLAG_DECORATION_12":185,"FLAG_DECORATION_13":186,"FLAG_DECORATION_14":187,"FLAG_DECORATION_2":175,"FLAG_DECORATION_3":176,"FLAG_DECORATION_4":177,"FLAG_DECORATION_5":178,"FLAG_DECORATION_6":179,"FLAG_DECORATION_7":180,"FLAG_DECORATION_8":181,"FLAG_DECORATION_9":182,"FLAG_DEFEATED_DEOXYS":428,"FLAG_DEFEATED_DEWFORD_GYM":1265,"FLAG_DEFEATED_ELECTRODE_1_AQUA_HIDEOUT":452,"FLAG_DEFEATED_ELECTRODE_2_AQUA_HIDEOUT":453,"FLAG_DEFEATED_ELITE_4_DRAKE":1278,"FLAG_DEFEATED_ELITE_4_GLACIA":1277,"FLAG_DEFEATED_ELITE_4_PHOEBE":1276,"FLAG_DEFEATED_ELITE_4_SIDNEY":1275,"FLAG_DEFEATED_EVIL_TEAM_MT_CHIMNEY":139,"FLAG_DEFEATED_FORTREE_GYM":1269,"FLAG_DEFEATED_GROUDON":447,"FLAG_DEFEATED_GRUNT_SPACE_CENTER_1F":191,"FLAG_DEFEATED_HO_OH":476,"FLAG_DEFEATED_KECLEON_1_ROUTE_119":989,"FLAG_DEFEATED_KECLEON_1_ROUTE_120":982,"FLAG_DEFEATED_KECLEON_2_ROUTE_119":990,"FLAG_DEFEATED_KECLEON_2_ROUTE_120":985,"FLAG_DEFEATED_KECLEON_3_ROUTE_120":986,"FLAG_DEFEATED_KECLEON_4_ROUTE_120":987,"FLAG_DEFEATED_KECLEON_5_ROUTE_120":988,"FLAG_DEFEATED_KEKLEON_ROUTE_120_BRIDGE":970,"FLAG_DEFEATED_KYOGRE":446,"FLAG_DEFEATED_LATIAS":456,"FLAG_DEFEATED_LATIOS":481,"FLAG_DEFEATED_LAVARIDGE_GYM":1267,"FLAG_DEFEATED_LUGIA":477,"FLAG_DEFEATED_MAGMA_SPACE_CENTER":117,"FLAG_DEFEATED_MAUVILLE_GYM":1266,"FLAG_DEFEATED_METEOR_FALLS_STEVEN":1272,"FLAG_DEFEATED_MEW":455,"FLAG_DEFEATED_MOSSDEEP_GYM":1270,"FLAG_DEFEATED_PETALBURG_GYM":1268,"FLAG_DEFEATED_RAYQUAZA":448,"FLAG_DEFEATED_REGICE":444,"FLAG_DEFEATED_REGIROCK":443,"FLAG_DEFEATED_REGISTEEL":445,"FLAG_DEFEATED_RIVAL_ROUTE103":130,"FLAG_DEFEATED_RIVAL_ROUTE_104":125,"FLAG_DEFEATED_RIVAL_RUSTBORO":211,"FLAG_DEFEATED_RUSTBORO_GYM":1264,"FLAG_DEFEATED_SEASHORE_HOUSE":141,"FLAG_DEFEATED_SOOTOPOLIS_GYM":1271,"FLAG_DEFEATED_SS_TIDAL_TRAINERS":247,"FLAG_DEFEATED_SUDOWOODO":454,"FLAG_DEFEATED_VOLTORB_1_NEW_MAUVILLE":449,"FLAG_DEFEATED_VOLTORB_2_NEW_MAUVILLE":450,"FLAG_DEFEATED_VOLTORB_3_NEW_MAUVILLE":451,"FLAG_DEFEATED_WALLY_MAUVILLE":190,"FLAG_DEFEATED_WALLY_VICTORY_ROAD":126,"FLAG_DELIVERED_DEVON_GOODS":149,"FLAG_DELIVERED_STEVEN_LETTER":189,"FLAG_DEOXYS_IS_RECOVERING":1258,"FLAG_DEOXYS_ROCK_COMPLETE":2260,"FLAG_DEVON_GOODS_STOLEN":142,"FLAG_DOCK_REJECTED_DEVON_GOODS":148,"FLAG_DONT_TRANSITION_MUSIC":16385,"FLAG_ENABLE_BRAWLY_MATCH_CALL":468,"FLAG_ENABLE_FIRST_WALLY_POKENAV_CALL":136,"FLAG_ENABLE_FLANNERY_MATCH_CALL":470,"FLAG_ENABLE_JUAN_MATCH_CALL":473,"FLAG_ENABLE_MOM_MATCH_CALL":216,"FLAG_ENABLE_MR_STONE_POKENAV":344,"FLAG_ENABLE_MULTI_CORRIDOR_DOOR":16386,"FLAG_ENABLE_NORMAN_MATCH_CALL":306,"FLAG_ENABLE_PROF_BIRCH_MATCH_CALL":281,"FLAG_ENABLE_RIVAL_MATCH_CALL":253,"FLAG_ENABLE_ROXANNE_FIRST_CALL":128,"FLAG_ENABLE_ROXANNE_MATCH_CALL":467,"FLAG_ENABLE_SCOTT_MATCH_CALL":215,"FLAG_ENABLE_SHIP_BIRTH_ISLAND":2261,"FLAG_ENABLE_SHIP_FARAWAY_ISLAND":2262,"FLAG_ENABLE_SHIP_NAVEL_ROCK":2272,"FLAG_ENABLE_SHIP_SOUTHERN_ISLAND":2227,"FLAG_ENABLE_TATE_AND_LIZA_MATCH_CALL":472,"FLAG_ENABLE_WALLY_MATCH_CALL":214,"FLAG_ENABLE_WATTSON_MATCH_CALL":469,"FLAG_ENABLE_WINONA_MATCH_CALL":471,"FLAG_ENTERED_CONTEST":341,"FLAG_ENTERED_ELITE_FOUR":263,"FLAG_ENTERED_MIRAGE_TOWER":2268,"FLAG_EVIL_LEADER_PLEASE_STOP":219,"FLAG_EVIL_TEAM_ESCAPED_STERN_SPOKE":271,"FLAG_EXCHANGED_SCANNER":294,"FLAG_FAN_CLUB_STRENGTH_SHARED":210,"FLAG_FLOWER_SHOP_RECEIVED_BERRY":1207,"FLAG_FORCE_MIRAGE_TOWER_VISIBLE":157,"FLAG_FORTREE_NPC_TRADE_COMPLETED":155,"FLAG_GOOD_LUCK_SAFARI_ZONE":93,"FLAG_GOT_BASEMENT_KEY_FROM_WATTSON":208,"FLAG_GOT_TM_THUNDERBOLT_FROM_WATTSON":209,"FLAG_GROUDON_AWAKENED_MAGMA_HIDEOUT":111,"FLAG_GROUDON_IS_RECOVERING":1274,"FLAG_HAS_MATCH_CALL":303,"FLAG_HIDDEN_ITEMS_START":500,"FLAG_HIDDEN_ITEM_ABANDONED_SHIP_RM_1_KEY":531,"FLAG_HIDDEN_ITEM_ABANDONED_SHIP_RM_2_KEY":532,"FLAG_HIDDEN_ITEM_ABANDONED_SHIP_RM_4_KEY":533,"FLAG_HIDDEN_ITEM_ABANDONED_SHIP_RM_6_KEY":534,"FLAG_HIDDEN_ITEM_ARTISAN_CAVE_B1F_CALCIUM":601,"FLAG_HIDDEN_ITEM_ARTISAN_CAVE_B1F_IRON":604,"FLAG_HIDDEN_ITEM_ARTISAN_CAVE_B1F_PROTEIN":603,"FLAG_HIDDEN_ITEM_ARTISAN_CAVE_B1F_ZINC":602,"FLAG_HIDDEN_ITEM_FALLARBOR_TOWN_NUGGET":528,"FLAG_HIDDEN_ITEM_GRANITE_CAVE_B2F_EVERSTONE_1":548,"FLAG_HIDDEN_ITEM_GRANITE_CAVE_B2F_EVERSTONE_2":549,"FLAG_HIDDEN_ITEM_JAGGED_PASS_FULL_HEAL":577,"FLAG_HIDDEN_ITEM_JAGGED_PASS_GREAT_BALL":576,"FLAG_HIDDEN_ITEM_LAVARIDGE_TOWN_ICE_HEAL":500,"FLAG_HIDDEN_ITEM_LILYCOVE_CITY_HEART_SCALE":527,"FLAG_HIDDEN_ITEM_LILYCOVE_CITY_POKE_BALL":575,"FLAG_HIDDEN_ITEM_LILYCOVE_CITY_PP_UP":543,"FLAG_HIDDEN_ITEM_MT_PYRE_EXTERIOR_MAX_ETHER":578,"FLAG_HIDDEN_ITEM_MT_PYRE_EXTERIOR_ULTRA_BALL":529,"FLAG_HIDDEN_ITEM_MT_PYRE_SUMMIT_RARE_CANDY":580,"FLAG_HIDDEN_ITEM_MT_PYRE_SUMMIT_ZINC":579,"FLAG_HIDDEN_ITEM_NAVEL_ROCK_TOP_SACRED_ASH":609,"FLAG_HIDDEN_ITEM_PETALBURG_CITY_RARE_CANDY":595,"FLAG_HIDDEN_ITEM_PETALBURG_WOODS_POKE_BALL":561,"FLAG_HIDDEN_ITEM_PETALBURG_WOODS_POTION":558,"FLAG_HIDDEN_ITEM_PETALBURG_WOODS_TINY_MUSHROOM_1":559,"FLAG_HIDDEN_ITEM_PETALBURG_WOODS_TINY_MUSHROOM_2":560,"FLAG_HIDDEN_ITEM_ROUTE_104_ANTIDOTE":585,"FLAG_HIDDEN_ITEM_ROUTE_104_HEART_SCALE":588,"FLAG_HIDDEN_ITEM_ROUTE_104_POKE_BALL":562,"FLAG_HIDDEN_ITEM_ROUTE_104_POTION":537,"FLAG_HIDDEN_ITEM_ROUTE_104_SUPER_POTION":544,"FLAG_HIDDEN_ITEM_ROUTE_105_BIG_PEARL":611,"FLAG_HIDDEN_ITEM_ROUTE_105_HEART_SCALE":589,"FLAG_HIDDEN_ITEM_ROUTE_106_HEART_SCALE":547,"FLAG_HIDDEN_ITEM_ROUTE_106_POKE_BALL":563,"FLAG_HIDDEN_ITEM_ROUTE_106_STARDUST":546,"FLAG_HIDDEN_ITEM_ROUTE_108_RARE_CANDY":586,"FLAG_HIDDEN_ITEM_ROUTE_109_ETHER":564,"FLAG_HIDDEN_ITEM_ROUTE_109_GREAT_BALL":551,"FLAG_HIDDEN_ITEM_ROUTE_109_HEART_SCALE_1":552,"FLAG_HIDDEN_ITEM_ROUTE_109_HEART_SCALE_2":590,"FLAG_HIDDEN_ITEM_ROUTE_109_HEART_SCALE_3":591,"FLAG_HIDDEN_ITEM_ROUTE_109_REVIVE":550,"FLAG_HIDDEN_ITEM_ROUTE_110_FULL_HEAL":555,"FLAG_HIDDEN_ITEM_ROUTE_110_GREAT_BALL":553,"FLAG_HIDDEN_ITEM_ROUTE_110_POKE_BALL":565,"FLAG_HIDDEN_ITEM_ROUTE_110_REVIVE":554,"FLAG_HIDDEN_ITEM_ROUTE_111_PROTEIN":556,"FLAG_HIDDEN_ITEM_ROUTE_111_RARE_CANDY":557,"FLAG_HIDDEN_ITEM_ROUTE_111_STARDUST":502,"FLAG_HIDDEN_ITEM_ROUTE_113_ETHER":503,"FLAG_HIDDEN_ITEM_ROUTE_113_NUGGET":598,"FLAG_HIDDEN_ITEM_ROUTE_113_TM_DOUBLE_TEAM":530,"FLAG_HIDDEN_ITEM_ROUTE_114_CARBOS":504,"FLAG_HIDDEN_ITEM_ROUTE_114_REVIVE":542,"FLAG_HIDDEN_ITEM_ROUTE_115_HEART_SCALE":597,"FLAG_HIDDEN_ITEM_ROUTE_116_BLACK_GLASSES":596,"FLAG_HIDDEN_ITEM_ROUTE_116_SUPER_POTION":545,"FLAG_HIDDEN_ITEM_ROUTE_117_REPEL":572,"FLAG_HIDDEN_ITEM_ROUTE_118_HEART_SCALE":566,"FLAG_HIDDEN_ITEM_ROUTE_118_IRON":567,"FLAG_HIDDEN_ITEM_ROUTE_119_CALCIUM":505,"FLAG_HIDDEN_ITEM_ROUTE_119_FULL_HEAL":568,"FLAG_HIDDEN_ITEM_ROUTE_119_MAX_ETHER":587,"FLAG_HIDDEN_ITEM_ROUTE_119_ULTRA_BALL":506,"FLAG_HIDDEN_ITEM_ROUTE_120_RARE_CANDY_1":571,"FLAG_HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2":569,"FLAG_HIDDEN_ITEM_ROUTE_120_REVIVE":584,"FLAG_HIDDEN_ITEM_ROUTE_120_ZINC":570,"FLAG_HIDDEN_ITEM_ROUTE_121_FULL_HEAL":573,"FLAG_HIDDEN_ITEM_ROUTE_121_HP_UP":539,"FLAG_HIDDEN_ITEM_ROUTE_121_MAX_REVIVE":600,"FLAG_HIDDEN_ITEM_ROUTE_121_NUGGET":540,"FLAG_HIDDEN_ITEM_ROUTE_123_HYPER_POTION":574,"FLAG_HIDDEN_ITEM_ROUTE_123_PP_UP":599,"FLAG_HIDDEN_ITEM_ROUTE_123_RARE_CANDY":610,"FLAG_HIDDEN_ITEM_ROUTE_123_REVIVE":541,"FLAG_HIDDEN_ITEM_ROUTE_123_SUPER_REPEL":507,"FLAG_HIDDEN_ITEM_ROUTE_128_HEART_SCALE_1":592,"FLAG_HIDDEN_ITEM_ROUTE_128_HEART_SCALE_2":593,"FLAG_HIDDEN_ITEM_ROUTE_128_HEART_SCALE_3":594,"FLAG_HIDDEN_ITEM_SAFARI_ZONE_NORTH_EAST_RARE_CANDY":606,"FLAG_HIDDEN_ITEM_SAFARI_ZONE_NORTH_EAST_ZINC":607,"FLAG_HIDDEN_ITEM_SAFARI_ZONE_SOUTH_EAST_FULL_RESTORE":605,"FLAG_HIDDEN_ITEM_SAFARI_ZONE_SOUTH_EAST_PP_UP":608,"FLAG_HIDDEN_ITEM_SS_TIDAL_LOWER_DECK_LEFTOVERS":535,"FLAG_HIDDEN_ITEM_TRICK_HOUSE_NUGGET":501,"FLAG_HIDDEN_ITEM_UNDERWATER_124_BIG_PEARL":511,"FLAG_HIDDEN_ITEM_UNDERWATER_124_CALCIUM":536,"FLAG_HIDDEN_ITEM_UNDERWATER_124_CARBOS":508,"FLAG_HIDDEN_ITEM_UNDERWATER_124_GREEN_SHARD":509,"FLAG_HIDDEN_ITEM_UNDERWATER_124_HEART_SCALE_1":513,"FLAG_HIDDEN_ITEM_UNDERWATER_124_HEART_SCALE_2":538,"FLAG_HIDDEN_ITEM_UNDERWATER_124_PEARL":510,"FLAG_HIDDEN_ITEM_UNDERWATER_126_BIG_PEARL":520,"FLAG_HIDDEN_ITEM_UNDERWATER_126_BLUE_SHARD":512,"FLAG_HIDDEN_ITEM_UNDERWATER_126_HEART_SCALE":514,"FLAG_HIDDEN_ITEM_UNDERWATER_126_IRON":519,"FLAG_HIDDEN_ITEM_UNDERWATER_126_PEARL":517,"FLAG_HIDDEN_ITEM_UNDERWATER_126_STARDUST":516,"FLAG_HIDDEN_ITEM_UNDERWATER_126_ULTRA_BALL":515,"FLAG_HIDDEN_ITEM_UNDERWATER_126_YELLOW_SHARD":518,"FLAG_HIDDEN_ITEM_UNDERWATER_127_HEART_SCALE":523,"FLAG_HIDDEN_ITEM_UNDERWATER_127_HP_UP":522,"FLAG_HIDDEN_ITEM_UNDERWATER_127_RED_SHARD":524,"FLAG_HIDDEN_ITEM_UNDERWATER_127_STAR_PIECE":521,"FLAG_HIDDEN_ITEM_UNDERWATER_128_PEARL":526,"FLAG_HIDDEN_ITEM_UNDERWATER_128_PROTEIN":525,"FLAG_HIDDEN_ITEM_VICTORY_ROAD_1F_ULTRA_BALL":581,"FLAG_HIDDEN_ITEM_VICTORY_ROAD_B2F_ELIXIR":582,"FLAG_HIDDEN_ITEM_VICTORY_ROAD_B2F_MAX_REPEL":583,"FLAG_HIDE_APPRENTICE":701,"FLAG_HIDE_AQUA_HIDEOUT_1F_GRUNTS_BLOCKING_ENTRANCE":821,"FLAG_HIDE_AQUA_HIDEOUT_B1F_ELECTRODE_1":977,"FLAG_HIDE_AQUA_HIDEOUT_B1F_ELECTRODE_2":978,"FLAG_HIDE_AQUA_HIDEOUT_B2F_SUBMARINE_SHADOW":943,"FLAG_HIDE_AQUA_HIDEOUT_GRUNTS":924,"FLAG_HIDE_BATTLE_FRONTIER_RECEPTION_GATE_SCOTT":836,"FLAG_HIDE_BATTLE_FRONTIER_SUDOWOODO":842,"FLAG_HIDE_BATTLE_TOWER_MULTI_BATTLE_PARTNER_1":711,"FLAG_HIDE_BATTLE_TOWER_MULTI_BATTLE_PARTNER_2":712,"FLAG_HIDE_BATTLE_TOWER_MULTI_BATTLE_PARTNER_3":713,"FLAG_HIDE_BATTLE_TOWER_MULTI_BATTLE_PARTNER_4":714,"FLAG_HIDE_BATTLE_TOWER_MULTI_BATTLE_PARTNER_5":715,"FLAG_HIDE_BATTLE_TOWER_MULTI_BATTLE_PARTNER_6":716,"FLAG_HIDE_BATTLE_TOWER_MULTI_BATTLE_PARTNER_ALT_1":864,"FLAG_HIDE_BATTLE_TOWER_MULTI_BATTLE_PARTNER_ALT_2":865,"FLAG_HIDE_BATTLE_TOWER_OPPONENT":888,"FLAG_HIDE_BATTLE_TOWER_REPORTER":918,"FLAG_HIDE_BIRTH_ISLAND_DEOXYS_TRIANGLE":764,"FLAG_HIDE_BRINEYS_HOUSE_MR_BRINEY":739,"FLAG_HIDE_BRINEYS_HOUSE_PEEKO":881,"FLAG_HIDE_CAVE_OF_ORIGIN_B1F_WALLACE":820,"FLAG_HIDE_CHAMPIONS_ROOM_BIRCH":921,"FLAG_HIDE_CHAMPIONS_ROOM_RIVAL":920,"FLAG_HIDE_CONTEST_POKE_BALL":86,"FLAG_HIDE_DEOXYS":763,"FLAG_HIDE_DESERT_UNDERPASS_FOSSIL":874,"FLAG_HIDE_DEWFORD_HALL_SLUDGE_BOMB_MAN":940,"FLAG_HIDE_EVER_GRANDE_POKEMON_CENTER_1F_SCOTT":793,"FLAG_HIDE_FALLARBOR_AZURILL":907,"FLAG_HIDE_FALLARBOR_HOUSE_PROF_COZMO":928,"FLAG_HIDE_FALLARBOR_TOWN_BATTLE_TENT_SCOTT":767,"FLAG_HIDE_FALLORBOR_POKEMON_CENTER_LANETTE":871,"FLAG_HIDE_FANCLUB_BOY":790,"FLAG_HIDE_FANCLUB_LADY":792,"FLAG_HIDE_FANCLUB_LITTLE_BOY":791,"FLAG_HIDE_FANCLUB_OLD_LADY":789,"FLAG_HIDE_FORTREE_CITY_HOUSE_4_WINGULL":933,"FLAG_HIDE_FORTREE_CITY_KECLEON":969,"FLAG_HIDE_GRANITE_CAVE_STEVEN":833,"FLAG_HIDE_HO_OH":801,"FLAG_HIDE_JAGGED_PASS_MAGMA_GUARD":847,"FLAG_HIDE_LANETTES_HOUSE_LANETTE":870,"FLAG_HIDE_LAVARIDGE_TOWN_RIVAL":929,"FLAG_HIDE_LAVARIDGE_TOWN_RIVAL_ON_BIKE":930,"FLAG_HIDE_LILYCOVE_CITY_AQUA_GRUNTS":852,"FLAG_HIDE_LILYCOVE_CITY_RIVAL":971,"FLAG_HIDE_LILYCOVE_CITY_WAILMER":729,"FLAG_HIDE_LILYCOVE_CONTEST_HALL_BLEND_MASTER":832,"FLAG_HIDE_LILYCOVE_CONTEST_HALL_BLEND_MASTER_REPLACEMENT":873,"FLAG_HIDE_LILYCOVE_CONTEST_HALL_CONTEST_ATTENDANT_1":774,"FLAG_HIDE_LILYCOVE_CONTEST_HALL_CONTEST_ATTENDANT_2":895,"FLAG_HIDE_LILYCOVE_CONTEST_HALL_REPORTER":802,"FLAG_HIDE_LILYCOVE_DEPARTMENT_STORE_ROOFTOP_SALE_WOMAN":962,"FLAG_HIDE_LILYCOVE_FAN_CLUB_INTERVIEWER":730,"FLAG_HIDE_LILYCOVE_HARBOR_EVENT_TICKET_TAKER":748,"FLAG_HIDE_LILYCOVE_HARBOR_FERRY_ATTENDANT":908,"FLAG_HIDE_LILYCOVE_HARBOR_FERRY_SAILOR":909,"FLAG_HIDE_LILYCOVE_HARBOR_SSTIDAL":861,"FLAG_HIDE_LILYCOVE_MOTEL_GAME_DESIGNERS":925,"FLAG_HIDE_LILYCOVE_MOTEL_SCOTT":787,"FLAG_HIDE_LILYCOVE_MUSEUM_CURATOR":775,"FLAG_HIDE_LILYCOVE_MUSEUM_PATRON_1":776,"FLAG_HIDE_LILYCOVE_MUSEUM_PATRON_2":777,"FLAG_HIDE_LILYCOVE_MUSEUM_PATRON_3":778,"FLAG_HIDE_LILYCOVE_MUSEUM_PATRON_4":779,"FLAG_HIDE_LILYCOVE_MUSEUM_TOURISTS":780,"FLAG_HIDE_LILYCOVE_POKEMON_CENTER_CONTEST_LADY_MON":993,"FLAG_HIDE_LITTLEROOT_TOWN_BIRCH":795,"FLAG_HIDE_LITTLEROOT_TOWN_BIRCHS_LAB_BIRCH":721,"FLAG_HIDE_LITTLEROOT_TOWN_BIRCHS_LAB_POKEBALL_CHIKORITA":838,"FLAG_HIDE_LITTLEROOT_TOWN_BIRCHS_LAB_POKEBALL_CYNDAQUIL":811,"FLAG_HIDE_LITTLEROOT_TOWN_BIRCHS_LAB_POKEBALL_TOTODILE":812,"FLAG_HIDE_LITTLEROOT_TOWN_BIRCHS_LAB_RIVAL":889,"FLAG_HIDE_LITTLEROOT_TOWN_BIRCHS_LAB_UNKNOWN_0x380":896,"FLAG_HIDE_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F_POKE_BALL":817,"FLAG_HIDE_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F_SWABLU_DOLL":815,"FLAG_HIDE_LITTLEROOT_TOWN_BRENDANS_HOUSE_BRENDAN":745,"FLAG_HIDE_LITTLEROOT_TOWN_BRENDANS_HOUSE_MOM":758,"FLAG_HIDE_LITTLEROOT_TOWN_BRENDANS_HOUSE_RIVAL_BEDROOM":760,"FLAG_HIDE_LITTLEROOT_TOWN_BRENDANS_HOUSE_RIVAL_MOM":784,"FLAG_HIDE_LITTLEROOT_TOWN_BRENDANS_HOUSE_RIVAL_SIBLING":735,"FLAG_HIDE_LITTLEROOT_TOWN_BRENDANS_HOUSE_TRUCK":761,"FLAG_HIDE_LITTLEROOT_TOWN_FAT_MAN":868,"FLAG_HIDE_LITTLEROOT_TOWN_MAYS_HOUSE_2F_PICHU_DOLL":849,"FLAG_HIDE_LITTLEROOT_TOWN_MAYS_HOUSE_2F_POKE_BALL":818,"FLAG_HIDE_LITTLEROOT_TOWN_MAYS_HOUSE_MAY":746,"FLAG_HIDE_LITTLEROOT_TOWN_MAYS_HOUSE_MOM":759,"FLAG_HIDE_LITTLEROOT_TOWN_MAYS_HOUSE_RIVAL_BEDROOM":722,"FLAG_HIDE_LITTLEROOT_TOWN_MAYS_HOUSE_RIVAL_MOM":785,"FLAG_HIDE_LITTLEROOT_TOWN_MAYS_HOUSE_RIVAL_SIBLING":736,"FLAG_HIDE_LITTLEROOT_TOWN_MAYS_HOUSE_TRUCK":762,"FLAG_HIDE_LITTLEROOT_TOWN_MOM_OUTSIDE":752,"FLAG_HIDE_LITTLEROOT_TOWN_PLAYERS_BEDROOM_MOM":757,"FLAG_HIDE_LITTLEROOT_TOWN_PLAYERS_HOUSE_VIGOROTH_1":754,"FLAG_HIDE_LITTLEROOT_TOWN_PLAYERS_HOUSE_VIGOROTH_2":755,"FLAG_HIDE_LITTLEROOT_TOWN_RIVAL":794,"FLAG_HIDE_LUGIA":800,"FLAG_HIDE_MAGMA_HIDEOUT_4F_GROUDON":853,"FLAG_HIDE_MAGMA_HIDEOUT_4F_GROUDON_ASLEEP":850,"FLAG_HIDE_MAGMA_HIDEOUT_GRUNTS":857,"FLAG_HIDE_MAGMA_HIDEOUT_MAXIE":867,"FLAG_HIDE_MAP_NAME_POPUP":16384,"FLAG_HIDE_MARINE_CAVE_KYOGRE":782,"FLAG_HIDE_MAUVILLE_CITY_SCOTT":765,"FLAG_HIDE_MAUVILLE_CITY_WALLY":804,"FLAG_HIDE_MAUVILLE_CITY_WALLYS_UNCLE":805,"FLAG_HIDE_MAUVILLE_CITY_WATTSON":912,"FLAG_HIDE_MAUVILLE_GYM_WATTSON":913,"FLAG_HIDE_METEOR_FALLS_1F_1R_COZMO":942,"FLAG_HIDE_METEOR_FALLS_TEAM_AQUA":938,"FLAG_HIDE_METEOR_FALLS_TEAM_MAGMA":939,"FLAG_HIDE_MEW":718,"FLAG_HIDE_MIRAGE_TOWER_CLAW_FOSSIL":964,"FLAG_HIDE_MIRAGE_TOWER_ROOT_FOSSIL":963,"FLAG_HIDE_MOSSDEEP_CITY_HOUSE_2_WINGULL":934,"FLAG_HIDE_MOSSDEEP_CITY_SCOTT":788,"FLAG_HIDE_MOSSDEEP_CITY_SPACE_CENTER_1F_STEVEN":753,"FLAG_HIDE_MOSSDEEP_CITY_SPACE_CENTER_1F_TEAM_MAGMA":756,"FLAG_HIDE_MOSSDEEP_CITY_SPACE_CENTER_2F_STEVEN":863,"FLAG_HIDE_MOSSDEEP_CITY_SPACE_CENTER_2F_TEAM_MAGMA":862,"FLAG_HIDE_MOSSDEEP_CITY_SPACE_CENTER_MAGMA_NOTE":737,"FLAG_HIDE_MOSSDEEP_CITY_STEVENS_HOUSE_BELDUM_POKEBALL":968,"FLAG_HIDE_MOSSDEEP_CITY_STEVENS_HOUSE_INVISIBLE_NINJA_BOY":727,"FLAG_HIDE_MOSSDEEP_CITY_STEVENS_HOUSE_STEVEN":967,"FLAG_HIDE_MOSSDEEP_CITY_TEAM_MAGMA":823,"FLAG_HIDE_MR_BRINEY_BOAT_DEWFORD_TOWN":743,"FLAG_HIDE_MR_BRINEY_DEWFORD_TOWN":740,"FLAG_HIDE_MT_CHIMNEY_LAVA_COOKIE_LADY":994,"FLAG_HIDE_MT_CHIMNEY_TEAM_AQUA":926,"FLAG_HIDE_MT_CHIMNEY_TEAM_MAGMA":927,"FLAG_HIDE_MT_CHIMNEY_TEAM_MAGMA_BATTLEABLE":981,"FLAG_HIDE_MT_CHIMNEY_TRAINERS":877,"FLAG_HIDE_MT_PYRE_SUMMIT_ARCHIE":916,"FLAG_HIDE_MT_PYRE_SUMMIT_MAXIE":856,"FLAG_HIDE_MT_PYRE_SUMMIT_TEAM_AQUA":917,"FLAG_HIDE_NEW_MAUVILLE_VOLTORB_1":974,"FLAG_HIDE_NEW_MAUVILLE_VOLTORB_2":975,"FLAG_HIDE_NEW_MAUVILLE_VOLTORB_3":976,"FLAG_HIDE_OLDALE_TOWN_RIVAL":979,"FLAG_HIDE_PETALBURG_CITY_SCOTT":995,"FLAG_HIDE_PETALBURG_CITY_WALLY":726,"FLAG_HIDE_PETALBURG_CITY_WALLYS_DAD":830,"FLAG_HIDE_PETALBURG_CITY_WALLYS_MOM":728,"FLAG_HIDE_PETALBURG_GYM_GREETER":781,"FLAG_HIDE_PETALBURG_GYM_NORMAN":772,"FLAG_HIDE_PETALBURG_GYM_WALLY":866,"FLAG_HIDE_PETALBURG_GYM_WALLYS_DAD":824,"FLAG_HIDE_PETALBURG_WOODS_AQUA_GRUNT":725,"FLAG_HIDE_PETALBURG_WOODS_DEVON_EMPLOYEE":724,"FLAG_HIDE_PLAYERS_HOUSE_DAD":734,"FLAG_HIDE_POKEMON_CENTER_2F_MYSTERY_GIFT_MAN":702,"FLAG_HIDE_REGICE":936,"FLAG_HIDE_REGIROCK":935,"FLAG_HIDE_REGISTEEL":937,"FLAG_HIDE_ROUTE_101_BIRCH":897,"FLAG_HIDE_ROUTE_101_BIRCH_STARTERS_BAG":700,"FLAG_HIDE_ROUTE_101_BIRCH_ZIGZAGOON_BATTLE":720,"FLAG_HIDE_ROUTE_101_BOY":991,"FLAG_HIDE_ROUTE_101_ZIGZAGOON":750,"FLAG_HIDE_ROUTE_103_BIRCH":898,"FLAG_HIDE_ROUTE_103_RIVAL":723,"FLAG_HIDE_ROUTE_104_MR_BRINEY":738,"FLAG_HIDE_ROUTE_104_MR_BRINEY_BOAT":742,"FLAG_HIDE_ROUTE_104_RIVAL":719,"FLAG_HIDE_ROUTE_104_WHITE_HERB_FLORIST":906,"FLAG_HIDE_ROUTE_109_MR_BRINEY":741,"FLAG_HIDE_ROUTE_109_MR_BRINEY_BOAT":744,"FLAG_HIDE_ROUTE_110_BIRCH":837,"FLAG_HIDE_ROUTE_110_RIVAL":919,"FLAG_HIDE_ROUTE_110_RIVAL_ON_BIKE":922,"FLAG_HIDE_ROUTE_110_TEAM_AQUA":900,"FLAG_HIDE_ROUTE_111_DESERT_FOSSIL":876,"FLAG_HIDE_ROUTE_111_GABBY_AND_TY_1":796,"FLAG_HIDE_ROUTE_111_GABBY_AND_TY_2":903,"FLAG_HIDE_ROUTE_111_GABBY_AND_TY_3":799,"FLAG_HIDE_ROUTE_111_PLAYER_DESCENT":875,"FLAG_HIDE_ROUTE_111_ROCK_SMASH_TIP_GUY":843,"FLAG_HIDE_ROUTE_111_SECRET_POWER_MAN":960,"FLAG_HIDE_ROUTE_111_VICKY_WINSTRATE":771,"FLAG_HIDE_ROUTE_111_VICTORIA_WINSTRATE":769,"FLAG_HIDE_ROUTE_111_VICTOR_WINSTRATE":768,"FLAG_HIDE_ROUTE_111_VIVI_WINSTRATE":770,"FLAG_HIDE_ROUTE_112_TEAM_MAGMA":819,"FLAG_HIDE_ROUTE_115_BOULDERS":825,"FLAG_HIDE_ROUTE_116_DEVON_EMPLOYEE":947,"FLAG_HIDE_ROUTE_116_DROPPED_GLASSES_MAN":813,"FLAG_HIDE_ROUTE_116_MR_BRINEY":891,"FLAG_HIDE_ROUTE_116_WANDAS_BOYFRIEND":894,"FLAG_HIDE_ROUTE_118_GABBY_AND_TY_1":797,"FLAG_HIDE_ROUTE_118_GABBY_AND_TY_2":901,"FLAG_HIDE_ROUTE_118_GABBY_AND_TY_3":904,"FLAG_HIDE_ROUTE_118_STEVEN":966,"FLAG_HIDE_ROUTE_119_RIVAL":851,"FLAG_HIDE_ROUTE_119_RIVAL_ON_BIKE":923,"FLAG_HIDE_ROUTE_119_SCOTT":786,"FLAG_HIDE_ROUTE_119_TEAM_AQUA":890,"FLAG_HIDE_ROUTE_119_TEAM_AQUA_BRIDGE":822,"FLAG_HIDE_ROUTE_119_TEAM_AQUA_SHELLY":915,"FLAG_HIDE_ROUTE_120_GABBY_AND_TY_1":798,"FLAG_HIDE_ROUTE_120_GABBY_AND_TY_2":902,"FLAG_HIDE_ROUTE_120_STEVEN":972,"FLAG_HIDE_ROUTE_121_TEAM_AQUA_GRUNTS":914,"FLAG_HIDE_ROUTE_128_ARCHIE":944,"FLAG_HIDE_ROUTE_128_MAXIE":945,"FLAG_HIDE_ROUTE_128_STEVEN":834,"FLAG_HIDE_RUSTBORO_CITY_AQUA_GRUNT":731,"FLAG_HIDE_RUSTBORO_CITY_DEVON_CORP_3F_EMPLOYEE":949,"FLAG_HIDE_RUSTBORO_CITY_DEVON_EMPLOYEE_1":732,"FLAG_HIDE_RUSTBORO_CITY_POKEMON_SCHOOL_SCOTT":999,"FLAG_HIDE_RUSTBORO_CITY_RIVAL":814,"FLAG_HIDE_RUSTBORO_CITY_SCIENTIST":844,"FLAG_HIDE_RUSTURF_TUNNEL_AQUA_GRUNT":878,"FLAG_HIDE_RUSTURF_TUNNEL_BRINEY":879,"FLAG_HIDE_RUSTURF_TUNNEL_PEEKO":880,"FLAG_HIDE_RUSTURF_TUNNEL_ROCK_1":931,"FLAG_HIDE_RUSTURF_TUNNEL_ROCK_2":932,"FLAG_HIDE_RUSTURF_TUNNEL_WANDA":983,"FLAG_HIDE_RUSTURF_TUNNEL_WANDAS_BOYFRIEND":807,"FLAG_HIDE_SAFARI_ZONE_SOUTH_CONSTRUCTION_WORKERS":717,"FLAG_HIDE_SAFARI_ZONE_SOUTH_EAST_EXPANSION":747,"FLAG_HIDE_SEAFLOOR_CAVERN_AQUA_GRUNTS":946,"FLAG_HIDE_SEAFLOOR_CAVERN_ENTRANCE_AQUA_GRUNT":941,"FLAG_HIDE_SEAFLOOR_CAVERN_ROOM_9_ARCHIE":828,"FLAG_HIDE_SEAFLOOR_CAVERN_ROOM_9_KYOGRE":859,"FLAG_HIDE_SEAFLOOR_CAVERN_ROOM_9_KYOGRE_ASLEEP":733,"FLAG_HIDE_SEAFLOOR_CAVERN_ROOM_9_MAGMA_GRUNTS":831,"FLAG_HIDE_SEAFLOOR_CAVERN_ROOM_9_MAXIE":829,"FLAG_HIDE_SECRET_BASE_TRAINER":173,"FLAG_HIDE_SKY_PILLAR_TOP_RAYQUAZA":773,"FLAG_HIDE_SKY_PILLAR_TOP_RAYQUAZA_STILL":80,"FLAG_HIDE_SKY_PILLAR_WALLACE":855,"FLAG_HIDE_SLATEPORT_CITY_CAPTAIN_STERN":840,"FLAG_HIDE_SLATEPORT_CITY_CONTEST_REPORTER":803,"FLAG_HIDE_SLATEPORT_CITY_GABBY_AND_TY":835,"FLAG_HIDE_SLATEPORT_CITY_HARBOR_AQUA_GRUNT":845,"FLAG_HIDE_SLATEPORT_CITY_HARBOR_ARCHIE":846,"FLAG_HIDE_SLATEPORT_CITY_HARBOR_CAPTAIN_STERN":841,"FLAG_HIDE_SLATEPORT_CITY_HARBOR_PATRONS":905,"FLAG_HIDE_SLATEPORT_CITY_HARBOR_SS_TIDAL":860,"FLAG_HIDE_SLATEPORT_CITY_HARBOR_SUBMARINE_SHADOW":848,"FLAG_HIDE_SLATEPORT_CITY_OCEANIC_MUSEUM_2F_AQUA_GRUNT_1":884,"FLAG_HIDE_SLATEPORT_CITY_OCEANIC_MUSEUM_2F_AQUA_GRUNT_2":885,"FLAG_HIDE_SLATEPORT_CITY_OCEANIC_MUSEUM_2F_ARCHIE":886,"FLAG_HIDE_SLATEPORT_CITY_OCEANIC_MUSEUM_2F_CAPTAIN_STERN":887,"FLAG_HIDE_SLATEPORT_CITY_OCEANIC_MUSEUM_AQUA_GRUNTS":883,"FLAG_HIDE_SLATEPORT_CITY_OCEANIC_MUSEUM_FAMILIAR_AQUA_GRUNT":965,"FLAG_HIDE_SLATEPORT_CITY_SCOTT":749,"FLAG_HIDE_SLATEPORT_CITY_STERNS_SHIPYARD_MR_BRINEY":869,"FLAG_HIDE_SLATEPORT_CITY_TEAM_AQUA":882,"FLAG_HIDE_SLATEPORT_CITY_TM_SALESMAN":948,"FLAG_HIDE_SLATEPORT_MUSEUM_POPULATION":961,"FLAG_HIDE_SOOTOPOLIS_CITY_ARCHIE":826,"FLAG_HIDE_SOOTOPOLIS_CITY_GROUDON":998,"FLAG_HIDE_SOOTOPOLIS_CITY_KYOGRE":997,"FLAG_HIDE_SOOTOPOLIS_CITY_MAN_1":839,"FLAG_HIDE_SOOTOPOLIS_CITY_MAXIE":827,"FLAG_HIDE_SOOTOPOLIS_CITY_RAYQUAZA":996,"FLAG_HIDE_SOOTOPOLIS_CITY_RESIDENTS":854,"FLAG_HIDE_SOOTOPOLIS_CITY_STEVEN":973,"FLAG_HIDE_SOOTOPOLIS_CITY_WALLACE":816,"FLAG_HIDE_SOUTHERN_ISLAND_EON_STONE":910,"FLAG_HIDE_SOUTHERN_ISLAND_UNCHOSEN_EON_DUO_MON":911,"FLAG_HIDE_SS_TIDAL_CORRIDOR_MR_BRINEY":950,"FLAG_HIDE_SS_TIDAL_CORRIDOR_SCOTT":810,"FLAG_HIDE_SS_TIDAL_ROOMS_SNATCH_GIVER":951,"FLAG_HIDE_TERRA_CAVE_GROUDON":783,"FLAG_HIDE_TRICK_HOUSE_END_MAN":899,"FLAG_HIDE_TRICK_HOUSE_ENTRANCE_MAN":872,"FLAG_HIDE_UNDERWATER_SEA_FLOOR_CAVERN_STOLEN_SUBMARINE":980,"FLAG_HIDE_UNION_ROOM_PLAYER_1":703,"FLAG_HIDE_UNION_ROOM_PLAYER_2":704,"FLAG_HIDE_UNION_ROOM_PLAYER_3":705,"FLAG_HIDE_UNION_ROOM_PLAYER_4":706,"FLAG_HIDE_UNION_ROOM_PLAYER_5":707,"FLAG_HIDE_UNION_ROOM_PLAYER_6":708,"FLAG_HIDE_UNION_ROOM_PLAYER_7":709,"FLAG_HIDE_UNION_ROOM_PLAYER_8":710,"FLAG_HIDE_VERDANTURF_TOWN_SCOTT":766,"FLAG_HIDE_VERDANTURF_TOWN_WANDAS_HOUSE_WALLY":806,"FLAG_HIDE_VERDANTURF_TOWN_WANDAS_HOUSE_WALLYS_UNCLE":809,"FLAG_HIDE_VERDANTURF_TOWN_WANDAS_HOUSE_WANDA":984,"FLAG_HIDE_VERDANTURF_TOWN_WANDAS_HOUSE_WANDAS_BOYFRIEND":808,"FLAG_HIDE_VICTORY_ROAD_ENTRANCE_WALLY":858,"FLAG_HIDE_VICTORY_ROAD_EXIT_WALLY":751,"FLAG_HIDE_WEATHER_INSTITUTE_1F_WORKERS":892,"FLAG_HIDE_WEATHER_INSTITUTE_2F_AQUA_GRUNT_M":992,"FLAG_HIDE_WEATHER_INSTITUTE_2F_WORKERS":893,"FLAG_HO_OH_IS_RECOVERING":1256,"FLAG_INTERACTED_WITH_DEVON_EMPLOYEE_GOODS_STOLEN":159,"FLAG_INTERACTED_WITH_STEVEN_SPACE_CENTER":205,"FLAG_IS_CHAMPION":2175,"FLAG_ITEM_ABANDONED_SHIP_CAPTAINS_OFFICE_STORAGE_KEY":1100,"FLAG_ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_1_TM_RAIN_DANCE":1102,"FLAG_ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_2_SCANNER":1078,"FLAG_ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_3_WATER_STONE":1101,"FLAG_ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_6_LUXURY_BALL":1077,"FLAG_ITEM_ABANDONED_SHIP_ROOMS_1F_HARBOR_MAIL":1095,"FLAG_ITEM_ABANDONED_SHIP_ROOMS_2_1F_REVIVE":1099,"FLAG_ITEM_ABANDONED_SHIP_ROOMS_2_B1F_DIVE_BALL":1097,"FLAG_ITEM_ABANDONED_SHIP_ROOMS_B1F_ESCAPE_ROPE":1096,"FLAG_ITEM_ABANDONED_SHIP_ROOMS_B1F_TM_ICE_BEAM":1098,"FLAG_ITEM_AQUA_HIDEOUT_B1F_MASTER_BALL":1124,"FLAG_ITEM_AQUA_HIDEOUT_B1F_MAX_ELIXIR":1071,"FLAG_ITEM_AQUA_HIDEOUT_B1F_NUGGET":1132,"FLAG_ITEM_AQUA_HIDEOUT_B2F_NEST_BALL":1072,"FLAG_ITEM_ARTISAN_CAVE_1F_CARBOS":1163,"FLAG_ITEM_ARTISAN_CAVE_B1F_HP_UP":1162,"FLAG_ITEM_FIERY_PATH_FIRE_STONE":1111,"FLAG_ITEM_FIERY_PATH_TM_TOXIC":1091,"FLAG_ITEM_GRANITE_CAVE_1F_ESCAPE_ROPE":1050,"FLAG_ITEM_GRANITE_CAVE_B1F_POKE_BALL":1051,"FLAG_ITEM_GRANITE_CAVE_B2F_RARE_CANDY":1054,"FLAG_ITEM_GRANITE_CAVE_B2F_REPEL":1053,"FLAG_ITEM_JAGGED_PASS_BURN_HEAL":1070,"FLAG_ITEM_LILYCOVE_CITY_MAX_REPEL":1042,"FLAG_ITEM_MAGMA_HIDEOUT_1F_RARE_CANDY":1151,"FLAG_ITEM_MAGMA_HIDEOUT_2F_2R_FULL_RESTORE":1165,"FLAG_ITEM_MAGMA_HIDEOUT_2F_2R_MAX_ELIXIR":1164,"FLAG_ITEM_MAGMA_HIDEOUT_3F_1R_NUGGET":1166,"FLAG_ITEM_MAGMA_HIDEOUT_3F_2R_PP_MAX":1167,"FLAG_ITEM_MAGMA_HIDEOUT_3F_3R_ECAPE_ROPE":1059,"FLAG_ITEM_MAGMA_HIDEOUT_4F_MAX_REVIVE":1168,"FLAG_ITEM_MAUVILLE_CITY_X_SPEED":1116,"FLAG_ITEM_METEOR_FALLS_1F_1R_FULL_HEAL":1045,"FLAG_ITEM_METEOR_FALLS_1F_1R_MOON_STONE":1046,"FLAG_ITEM_METEOR_FALLS_1F_1R_PP_UP":1047,"FLAG_ITEM_METEOR_FALLS_1F_1R_TM_IRON_TAIL":1044,"FLAG_ITEM_METEOR_FALLS_B1F_2R_TM_DRAGON_CLAW":1080,"FLAG_ITEM_MOSSDEEP_CITY_NET_BALL":1043,"FLAG_ITEM_MOSSDEEP_STEVENS_HOUSE_HM08":1133,"FLAG_ITEM_MT_PYRE_2F_ULTRA_BALL":1129,"FLAG_ITEM_MT_PYRE_3F_SUPER_REPEL":1120,"FLAG_ITEM_MT_PYRE_4F_SEA_INCENSE":1130,"FLAG_ITEM_MT_PYRE_5F_LAX_INCENSE":1052,"FLAG_ITEM_MT_PYRE_6F_TM_SHADOW_BALL":1089,"FLAG_ITEM_MT_PYRE_EXTERIOR_MAX_POTION":1073,"FLAG_ITEM_MT_PYRE_EXTERIOR_TM_SKILL_SWAP":1074,"FLAG_ITEM_NEW_MAUVILLE_ESCAPE_ROPE":1076,"FLAG_ITEM_NEW_MAUVILLE_FULL_HEAL":1122,"FLAG_ITEM_NEW_MAUVILLE_PARALYZE_HEAL":1123,"FLAG_ITEM_NEW_MAUVILLE_THUNDER_STONE":1110,"FLAG_ITEM_NEW_MAUVILLE_ULTRA_BALL":1075,"FLAG_ITEM_OLD_MAGMA_HIDEOUT_B1F_MASTER_BALL":1125,"FLAG_ITEM_OLD_MAGMA_HIDEOUT_B1F_MAX_ELIXIR":1126,"FLAG_ITEM_OLD_MAGMA_HIDEOUT_B2F_NEST_BALL":1127,"FLAG_ITEM_PETALBURG_CITY_ETHER":1040,"FLAG_ITEM_PETALBURG_CITY_MAX_REVIVE":1039,"FLAG_ITEM_PETALBURG_WOODS_ETHER":1058,"FLAG_ITEM_PETALBURG_WOODS_GREAT_BALL":1056,"FLAG_ITEM_PETALBURG_WOODS_PARALYZE_HEAL":1117,"FLAG_ITEM_PETALBURG_WOODS_X_ATTACK":1055,"FLAG_ITEM_ROUTE_102_POTION":1000,"FLAG_ITEM_ROUTE_103_GUARD_SPEC":1114,"FLAG_ITEM_ROUTE_103_PP_UP":1137,"FLAG_ITEM_ROUTE_104_POKE_BALL":1057,"FLAG_ITEM_ROUTE_104_POTION":1135,"FLAG_ITEM_ROUTE_104_PP_UP":1002,"FLAG_ITEM_ROUTE_104_X_ACCURACY":1115,"FLAG_ITEM_ROUTE_105_IRON":1003,"FLAG_ITEM_ROUTE_106_PROTEIN":1004,"FLAG_ITEM_ROUTE_108_STAR_PIECE":1139,"FLAG_ITEM_ROUTE_109_POTION":1140,"FLAG_ITEM_ROUTE_109_PP_UP":1005,"FLAG_ITEM_ROUTE_110_DIRE_HIT":1007,"FLAG_ITEM_ROUTE_110_ELIXIR":1141,"FLAG_ITEM_ROUTE_110_RARE_CANDY":1006,"FLAG_ITEM_ROUTE_111_ELIXIR":1142,"FLAG_ITEM_ROUTE_111_HP_UP":1010,"FLAG_ITEM_ROUTE_111_STARDUST":1009,"FLAG_ITEM_ROUTE_111_TM_SANDSTORM":1008,"FLAG_ITEM_ROUTE_112_NUGGET":1011,"FLAG_ITEM_ROUTE_113_HYPER_POTION":1143,"FLAG_ITEM_ROUTE_113_MAX_ETHER":1012,"FLAG_ITEM_ROUTE_113_SUPER_REPEL":1013,"FLAG_ITEM_ROUTE_114_ENERGY_POWDER":1160,"FLAG_ITEM_ROUTE_114_PROTEIN":1015,"FLAG_ITEM_ROUTE_114_RARE_CANDY":1014,"FLAG_ITEM_ROUTE_115_GREAT_BALL":1118,"FLAG_ITEM_ROUTE_115_HEAL_POWDER":1144,"FLAG_ITEM_ROUTE_115_IRON":1018,"FLAG_ITEM_ROUTE_115_PP_UP":1161,"FLAG_ITEM_ROUTE_115_SUPER_POTION":1016,"FLAG_ITEM_ROUTE_115_TM_FOCUS_PUNCH":1017,"FLAG_ITEM_ROUTE_116_ETHER":1019,"FLAG_ITEM_ROUTE_116_HP_UP":1021,"FLAG_ITEM_ROUTE_116_POTION":1146,"FLAG_ITEM_ROUTE_116_REPEL":1020,"FLAG_ITEM_ROUTE_116_X_SPECIAL":1001,"FLAG_ITEM_ROUTE_117_GREAT_BALL":1022,"FLAG_ITEM_ROUTE_117_REVIVE":1023,"FLAG_ITEM_ROUTE_118_HYPER_POTION":1121,"FLAG_ITEM_ROUTE_119_ELIXIR_1":1026,"FLAG_ITEM_ROUTE_119_ELIXIR_2":1147,"FLAG_ITEM_ROUTE_119_HYPER_POTION_1":1029,"FLAG_ITEM_ROUTE_119_HYPER_POTION_2":1106,"FLAG_ITEM_ROUTE_119_LEAF_STONE":1027,"FLAG_ITEM_ROUTE_119_NUGGET":1134,"FLAG_ITEM_ROUTE_119_RARE_CANDY":1028,"FLAG_ITEM_ROUTE_119_SUPER_REPEL":1024,"FLAG_ITEM_ROUTE_119_ZINC":1025,"FLAG_ITEM_ROUTE_120_FULL_HEAL":1031,"FLAG_ITEM_ROUTE_120_HYPER_POTION":1107,"FLAG_ITEM_ROUTE_120_NEST_BALL":1108,"FLAG_ITEM_ROUTE_120_NUGGET":1030,"FLAG_ITEM_ROUTE_120_REVIVE":1148,"FLAG_ITEM_ROUTE_121_CARBOS":1103,"FLAG_ITEM_ROUTE_121_REVIVE":1149,"FLAG_ITEM_ROUTE_121_ZINC":1150,"FLAG_ITEM_ROUTE_123_CALCIUM":1032,"FLAG_ITEM_ROUTE_123_ELIXIR":1109,"FLAG_ITEM_ROUTE_123_PP_UP":1152,"FLAG_ITEM_ROUTE_123_REVIVAL_HERB":1153,"FLAG_ITEM_ROUTE_123_ULTRA_BALL":1104,"FLAG_ITEM_ROUTE_124_BLUE_SHARD":1093,"FLAG_ITEM_ROUTE_124_RED_SHARD":1092,"FLAG_ITEM_ROUTE_124_YELLOW_SHARD":1066,"FLAG_ITEM_ROUTE_125_BIG_PEARL":1154,"FLAG_ITEM_ROUTE_126_GREEN_SHARD":1105,"FLAG_ITEM_ROUTE_127_CARBOS":1035,"FLAG_ITEM_ROUTE_127_RARE_CANDY":1155,"FLAG_ITEM_ROUTE_127_ZINC":1034,"FLAG_ITEM_ROUTE_132_PROTEIN":1156,"FLAG_ITEM_ROUTE_132_RARE_CANDY":1036,"FLAG_ITEM_ROUTE_133_BIG_PEARL":1037,"FLAG_ITEM_ROUTE_133_MAX_REVIVE":1157,"FLAG_ITEM_ROUTE_133_STAR_PIECE":1038,"FLAG_ITEM_ROUTE_134_CARBOS":1158,"FLAG_ITEM_ROUTE_134_STAR_PIECE":1159,"FLAG_ITEM_RUSTBORO_CITY_X_DEFEND":1041,"FLAG_ITEM_RUSTURF_TUNNEL_MAX_ETHER":1049,"FLAG_ITEM_RUSTURF_TUNNEL_POKE_BALL":1048,"FLAG_ITEM_SAFARI_ZONE_NORTH_CALCIUM":1119,"FLAG_ITEM_SAFARI_ZONE_NORTH_EAST_NUGGET":1169,"FLAG_ITEM_SAFARI_ZONE_NORTH_WEST_TM_SOLAR_BEAM":1094,"FLAG_ITEM_SAFARI_ZONE_SOUTH_EAST_BIG_PEARL":1170,"FLAG_ITEM_SAFARI_ZONE_SOUTH_WEST_MAX_REVIVE":1131,"FLAG_ITEM_SCORCHED_SLAB_TM_SUNNY_DAY":1079,"FLAG_ITEM_SEAFLOOR_CAVERN_ROOM_9_TM_EARTHQUAKE":1090,"FLAG_ITEM_SHOAL_CAVE_ENTRANCE_BIG_PEARL":1081,"FLAG_ITEM_SHOAL_CAVE_ICE_ROOM_NEVER_MELT_ICE":1113,"FLAG_ITEM_SHOAL_CAVE_ICE_ROOM_TM_HAIL":1112,"FLAG_ITEM_SHOAL_CAVE_INNER_ROOM_RARE_CANDY":1082,"FLAG_ITEM_SHOAL_CAVE_STAIRS_ROOM_ICE_HEAL":1083,"FLAG_ITEM_TRICK_HOUSE_PUZZLE_1_ORANGE_MAIL":1060,"FLAG_ITEM_TRICK_HOUSE_PUZZLE_2_HARBOR_MAIL":1061,"FLAG_ITEM_TRICK_HOUSE_PUZZLE_2_WAVE_MAIL":1062,"FLAG_ITEM_TRICK_HOUSE_PUZZLE_3_SHADOW_MAIL":1063,"FLAG_ITEM_TRICK_HOUSE_PUZZLE_3_WOOD_MAIL":1064,"FLAG_ITEM_TRICK_HOUSE_PUZZLE_4_MECH_MAIL":1065,"FLAG_ITEM_TRICK_HOUSE_PUZZLE_6_GLITTER_MAIL":1067,"FLAG_ITEM_TRICK_HOUSE_PUZZLE_7_TROPIC_MAIL":1068,"FLAG_ITEM_TRICK_HOUSE_PUZZLE_8_BEAD_MAIL":1069,"FLAG_ITEM_VICTORY_ROAD_1F_MAX_ELIXIR":1084,"FLAG_ITEM_VICTORY_ROAD_1F_PP_UP":1085,"FLAG_ITEM_VICTORY_ROAD_B1F_FULL_RESTORE":1087,"FLAG_ITEM_VICTORY_ROAD_B1F_TM_PSYCHIC":1086,"FLAG_ITEM_VICTORY_ROAD_B2F_FULL_HEAL":1088,"FLAG_KECLEON_FLED_FORTREE":295,"FLAG_KYOGRE_ESCAPED_SEAFLOOR_CAVERN":129,"FLAG_KYOGRE_IS_RECOVERING":1273,"FLAG_LANDMARK_ABANDONED_SHIP":2206,"FLAG_LANDMARK_ALTERING_CAVE":2269,"FLAG_LANDMARK_ANCIENT_TOMB":2233,"FLAG_LANDMARK_ARTISAN_CAVE":2271,"FLAG_LANDMARK_BATTLE_FRONTIER":2216,"FLAG_LANDMARK_BERRY_MASTERS_HOUSE":2243,"FLAG_LANDMARK_DESERT_RUINS":2230,"FLAG_LANDMARK_DESERT_UNDERPASS":2270,"FLAG_LANDMARK_FIERY_PATH":2218,"FLAG_LANDMARK_FLOWER_SHOP":2204,"FLAG_LANDMARK_FOSSIL_MANIACS_HOUSE":2231,"FLAG_LANDMARK_GLASS_WORKSHOP":2212,"FLAG_LANDMARK_HUNTERS_HOUSE":2235,"FLAG_LANDMARK_ISLAND_CAVE":2229,"FLAG_LANDMARK_LANETTES_HOUSE":2213,"FLAG_LANDMARK_MIRAGE_TOWER":120,"FLAG_LANDMARK_MR_BRINEY_HOUSE":2205,"FLAG_LANDMARK_NEW_MAUVILLE":2208,"FLAG_LANDMARK_OLD_LADY_REST_SHOP":2209,"FLAG_LANDMARK_POKEMON_DAYCARE":2214,"FLAG_LANDMARK_POKEMON_LEAGUE":2228,"FLAG_LANDMARK_SCORCHED_SLAB":2232,"FLAG_LANDMARK_SEAFLOOR_CAVERN":2215,"FLAG_LANDMARK_SEALED_CHAMBER":2236,"FLAG_LANDMARK_SEASHORE_HOUSE":2207,"FLAG_LANDMARK_SKY_PILLAR":2238,"FLAG_LANDMARK_SOUTHERN_ISLAND":2217,"FLAG_LANDMARK_TRAINER_HILL":2274,"FLAG_LANDMARK_TRICK_HOUSE":2210,"FLAG_LANDMARK_TUNNELERS_REST_HOUSE":2234,"FLAG_LANDMARK_WINSTRATE_FAMILY":2211,"FLAG_LATIAS_IS_RECOVERING":1263,"FLAG_LATIOS_IS_RECOVERING":1255,"FLAG_LATIOS_OR_LATIAS_ROAMING":255,"FLAG_LEGENDARIES_IN_SOOTOPOLIS":83,"FLAG_LILYCOVE_RECEIVED_BERRY":1208,"FLAG_LUGIA_IS_RECOVERING":1257,"FLAG_MAP_SCRIPT_CHECKED_DEOXYS":2259,"FLAG_MATCH_CALL_REGISTERED":348,"FLAG_MAUVILLE_GYM_BARRIERS_STATE":99,"FLAG_MET_ARCHIE_METEOR_FALLS":207,"FLAG_MET_ARCHIE_SOOTOPOLIS":308,"FLAG_MET_BATTLE_FRONTIER_BREEDER":339,"FLAG_MET_BATTLE_FRONTIER_GAMBLER":343,"FLAG_MET_BATTLE_FRONTIER_MANIAC":340,"FLAG_MET_DEVON_EMPLOYEE":287,"FLAG_MET_DIVING_TREASURE_HUNTER":217,"FLAG_MET_FANCLUB_YOUNGER_BROTHER":300,"FLAG_MET_FRONTIER_BEAUTY_MOVE_TUTOR":346,"FLAG_MET_FRONTIER_SWIMMER_MOVE_TUTOR":347,"FLAG_MET_HIDDEN_POWER_GIVER":118,"FLAG_MET_MAXIE_SOOTOPOLIS":309,"FLAG_MET_PRETTY_PETAL_SHOP_OWNER":127,"FLAG_MET_PROF_COZMO":244,"FLAG_MET_RIVAL_IN_HOUSE_AFTER_LILYCOVE":293,"FLAG_MET_RIVAL_LILYCOVE":292,"FLAG_MET_RIVAL_MOM":87,"FLAG_MET_RIVAL_RUSTBORO":288,"FLAG_MET_SCOTT_AFTER_OBTAINING_STONE_BADGE":459,"FLAG_MET_SCOTT_IN_EVERGRANDE":463,"FLAG_MET_SCOTT_IN_FALLARBOR":461,"FLAG_MET_SCOTT_IN_LILYCOVE":462,"FLAG_MET_SCOTT_IN_VERDANTURF":460,"FLAG_MET_SCOTT_ON_SS_TIDAL":464,"FLAG_MET_SCOTT_RUSTBORO":310,"FLAG_MET_SLATEPORT_FANCLUB_CHAIRMAN":342,"FLAG_MET_TEAM_AQUA_HARBOR":97,"FLAG_MET_WAILMER_TRAINER":218,"FLAG_MEW_IS_RECOVERING":1259,"FLAG_MIRAGE_TOWER_VISIBLE":334,"FLAG_MOSSDEEP_GYM_SWITCH_1":100,"FLAG_MOSSDEEP_GYM_SWITCH_2":101,"FLAG_MOSSDEEP_GYM_SWITCH_3":102,"FLAG_MOSSDEEP_GYM_SWITCH_4":103,"FLAG_MOVE_TUTOR_TAUGHT_DOUBLE_EDGE":441,"FLAG_MOVE_TUTOR_TAUGHT_DYNAMICPUNCH":440,"FLAG_MOVE_TUTOR_TAUGHT_EXPLOSION":442,"FLAG_MOVE_TUTOR_TAUGHT_FURY_CUTTER":435,"FLAG_MOVE_TUTOR_TAUGHT_METRONOME":437,"FLAG_MOVE_TUTOR_TAUGHT_MIMIC":436,"FLAG_MOVE_TUTOR_TAUGHT_ROLLOUT":434,"FLAG_MOVE_TUTOR_TAUGHT_SLEEP_TALK":438,"FLAG_MOVE_TUTOR_TAUGHT_SUBSTITUTE":439,"FLAG_MOVE_TUTOR_TAUGHT_SWAGGER":433,"FLAG_MR_BRINEY_SAILING_INTRO":147,"FLAG_MYSTERY_GIFT_1":485,"FLAG_MYSTERY_GIFT_10":494,"FLAG_MYSTERY_GIFT_11":495,"FLAG_MYSTERY_GIFT_12":496,"FLAG_MYSTERY_GIFT_13":497,"FLAG_MYSTERY_GIFT_14":498,"FLAG_MYSTERY_GIFT_15":499,"FLAG_MYSTERY_GIFT_2":486,"FLAG_MYSTERY_GIFT_3":487,"FLAG_MYSTERY_GIFT_4":488,"FLAG_MYSTERY_GIFT_5":489,"FLAG_MYSTERY_GIFT_6":490,"FLAG_MYSTERY_GIFT_7":491,"FLAG_MYSTERY_GIFT_8":492,"FLAG_MYSTERY_GIFT_9":493,"FLAG_MYSTERY_GIFT_DONE":484,"FLAG_NEVER_SET_0x0DC":220,"FLAG_NOT_READY_FOR_BATTLE_ROUTE_120":290,"FLAG_NURSE_MENTIONS_GOLD_CARD":345,"FLAG_NURSE_UNION_ROOM_REMINDER":2176,"FLAG_OCEANIC_MUSEUM_MET_REPORTER":105,"FLAG_OMIT_DIVE_FROM_STEVEN_LETTER":302,"FLAG_PACIFIDLOG_NPC_TRADE_COMPLETED":154,"FLAG_PENDING_DAYCARE_EGG":134,"FLAG_PETALBURG_MART_EXPANDED_ITEMS":296,"FLAG_POKERUS_EXPLAINED":273,"FLAG_PURCHASED_HARBOR_MAIL":104,"FLAG_RAYQUAZA_IS_RECOVERING":1279,"FLAG_RECEIVED_20_COINS":225,"FLAG_RECEIVED_6_SODA_POP":140,"FLAG_RECEIVED_ACRO_BIKE":1181,"FLAG_RECEIVED_AMULET_COIN":133,"FLAG_RECEIVED_AURORA_TICKET":314,"FLAG_RECEIVED_BADGE_1":1182,"FLAG_RECEIVED_BADGE_2":1183,"FLAG_RECEIVED_BADGE_3":1184,"FLAG_RECEIVED_BADGE_4":1185,"FLAG_RECEIVED_BADGE_5":1186,"FLAG_RECEIVED_BADGE_6":1187,"FLAG_RECEIVED_BADGE_7":1188,"FLAG_RECEIVED_BADGE_8":1189,"FLAG_RECEIVED_BELDUM":298,"FLAG_RECEIVED_BELUE_BERRY":252,"FLAG_RECEIVED_BIKE":90,"FLAG_RECEIVED_BLUE_SCARF":201,"FLAG_RECEIVED_CASTFORM":151,"FLAG_RECEIVED_CHARCOAL":254,"FLAG_RECEIVED_CHESTO_BERRY_ROUTE_104":246,"FLAG_RECEIVED_CLEANSE_TAG":282,"FLAG_RECEIVED_COIN_CASE":258,"FLAG_RECEIVED_CONTEST_PASS":150,"FLAG_RECEIVED_DEEP_SEA_SCALE":1190,"FLAG_RECEIVED_DEEP_SEA_TOOTH":1191,"FLAG_RECEIVED_DEVON_GOODS_RUSTURF_TUNNEL":1172,"FLAG_RECEIVED_DEVON_SCOPE":285,"FLAG_RECEIVED_DOLL_LANETTE":131,"FLAG_RECEIVED_DURIN_BERRY":251,"FLAG_RECEIVED_EON_TICKET":474,"FLAG_RECEIVED_EXP_SHARE":272,"FLAG_RECEIVED_FANCLUB_TM_THIS_WEEK":299,"FLAG_RECEIVED_FIRST_POKEBALLS":233,"FLAG_RECEIVED_FOCUS_BAND":283,"FLAG_RECEIVED_GLASS_ORNAMENT":236,"FLAG_RECEIVED_GOLD_SHIELD":238,"FLAG_RECEIVED_GOOD_ROD":227,"FLAG_RECEIVED_GO_GOGGLES":221,"FLAG_RECEIVED_GREAT_BALL_PETALBURG_WOODS":1171,"FLAG_RECEIVED_GREAT_BALL_RUSTBORO_CITY":1173,"FLAG_RECEIVED_GREEN_SCARF":203,"FLAG_RECEIVED_HM_CUT":137,"FLAG_RECEIVED_HM_DIVE":123,"FLAG_RECEIVED_HM_FLASH":109,"FLAG_RECEIVED_HM_FLY":110,"FLAG_RECEIVED_HM_ROCK_SMASH":107,"FLAG_RECEIVED_HM_STRENGTH":106,"FLAG_RECEIVED_HM_SURF":122,"FLAG_RECEIVED_HM_WATERFALL":312,"FLAG_RECEIVED_ITEMFINDER":1176,"FLAG_RECEIVED_KINGS_ROCK":276,"FLAG_RECEIVED_LAVARIDGE_EGG":266,"FLAG_RECEIVED_LETTER":1174,"FLAG_RECEIVED_MACHO_BRACE":277,"FLAG_RECEIVED_MACH_BIKE":1180,"FLAG_RECEIVED_MAGMA_EMBLEM":1177,"FLAG_RECEIVED_MENTAL_HERB":223,"FLAG_RECEIVED_METEORITE":115,"FLAG_RECEIVED_MIRACLE_SEED":297,"FLAG_RECEIVED_MYSTIC_TICKET":315,"FLAG_RECEIVED_OLD_ROD":257,"FLAG_RECEIVED_OLD_SEA_MAP":316,"FLAG_RECEIVED_PAMTRE_BERRY":249,"FLAG_RECEIVED_PINK_SCARF":202,"FLAG_RECEIVED_POKEBLOCK_CASE":95,"FLAG_RECEIVED_POKEDEX_FROM_BIRCH":2276,"FLAG_RECEIVED_POKENAV":188,"FLAG_RECEIVED_POTION_OLDALE":132,"FLAG_RECEIVED_POWDER_JAR":337,"FLAG_RECEIVED_PREMIER_BALL_RUSTBORO":213,"FLAG_RECEIVED_QUICK_CLAW":275,"FLAG_RECEIVED_RED_OR_BLUE_ORB":212,"FLAG_RECEIVED_RED_SCARF":200,"FLAG_RECEIVED_REPEAT_BALL":256,"FLAG_RECEIVED_REVIVED_FOSSIL_MON":267,"FLAG_RECEIVED_RUNNING_SHOES":274,"FLAG_RECEIVED_SECRET_POWER":96,"FLAG_RECEIVED_SHOAL_SALT_1":952,"FLAG_RECEIVED_SHOAL_SALT_2":953,"FLAG_RECEIVED_SHOAL_SALT_3":954,"FLAG_RECEIVED_SHOAL_SALT_4":955,"FLAG_RECEIVED_SHOAL_SHELL_1":956,"FLAG_RECEIVED_SHOAL_SHELL_2":957,"FLAG_RECEIVED_SHOAL_SHELL_3":958,"FLAG_RECEIVED_SHOAL_SHELL_4":959,"FLAG_RECEIVED_SILK_SCARF":289,"FLAG_RECEIVED_SILVER_SHIELD":237,"FLAG_RECEIVED_SOFT_SAND":280,"FLAG_RECEIVED_SOOTHE_BELL":278,"FLAG_RECEIVED_SOOT_SACK":1033,"FLAG_RECEIVED_SPECIAL_PHRASE_HINT":85,"FLAG_RECEIVED_SPELON_BERRY":248,"FLAG_RECEIVED_SS_TICKET":291,"FLAG_RECEIVED_STARTER_DOLL":226,"FLAG_RECEIVED_SUN_STONE_MOSSDEEP":192,"FLAG_RECEIVED_SUPER_ROD":152,"FLAG_RECEIVED_TM_AERIAL_ACE":170,"FLAG_RECEIVED_TM_ATTRACT":235,"FLAG_RECEIVED_TM_BRICK_BREAK":121,"FLAG_RECEIVED_TM_BULK_UP":166,"FLAG_RECEIVED_TM_BULLET_SEED":262,"FLAG_RECEIVED_TM_CALM_MIND":171,"FLAG_RECEIVED_TM_DIG":261,"FLAG_RECEIVED_TM_FACADE":169,"FLAG_RECEIVED_TM_FRUSTRATION":1179,"FLAG_RECEIVED_TM_GIGA_DRAIN":232,"FLAG_RECEIVED_TM_HIDDEN_POWER":264,"FLAG_RECEIVED_TM_OVERHEAT":168,"FLAG_RECEIVED_TM_REST":234,"FLAG_RECEIVED_TM_RETURN":229,"FLAG_RECEIVED_TM_RETURN_2":1178,"FLAG_RECEIVED_TM_ROAR":231,"FLAG_RECEIVED_TM_ROCK_TOMB":165,"FLAG_RECEIVED_TM_SHOCK_WAVE":167,"FLAG_RECEIVED_TM_SLUDGE_BOMB":230,"FLAG_RECEIVED_TM_SNATCH":260,"FLAG_RECEIVED_TM_STEEL_WING":1175,"FLAG_RECEIVED_TM_THIEF":269,"FLAG_RECEIVED_TM_TORMENT":265,"FLAG_RECEIVED_TM_WATER_PULSE":172,"FLAG_RECEIVED_TRICK_HOUSE_REWARD_1":1200,"FLAG_RECEIVED_TRICK_HOUSE_REWARD_2":1201,"FLAG_RECEIVED_TRICK_HOUSE_REWARD_3":1202,"FLAG_RECEIVED_TRICK_HOUSE_REWARD_4":1203,"FLAG_RECEIVED_TRICK_HOUSE_REWARD_5":1204,"FLAG_RECEIVED_TRICK_HOUSE_REWARD_6":1205,"FLAG_RECEIVED_TRICK_HOUSE_REWARD_7":1206,"FLAG_RECEIVED_WAILMER_DOLL":245,"FLAG_RECEIVED_WAILMER_PAIL":94,"FLAG_RECEIVED_WATMEL_BERRY":250,"FLAG_RECEIVED_WHITE_HERB":279,"FLAG_RECEIVED_YELLOW_SCARF":204,"FLAG_RECOVERED_DEVON_GOODS":143,"FLAG_REGICE_IS_RECOVERING":1260,"FLAG_REGIROCK_IS_RECOVERING":1261,"FLAG_REGISTEEL_IS_RECOVERING":1262,"FLAG_REGISTERED_STEVEN_POKENAV":305,"FLAG_REGISTER_RIVAL_POKENAV":124,"FLAG_REGI_DOORS_OPENED":228,"FLAG_REMATCH_ABIGAIL":387,"FLAG_REMATCH_AMY_AND_LIV":399,"FLAG_REMATCH_ANDRES":350,"FLAG_REMATCH_ANNA_AND_MEG":378,"FLAG_REMATCH_BENJAMIN":390,"FLAG_REMATCH_BERNIE":369,"FLAG_REMATCH_BRAWLY":415,"FLAG_REMATCH_BROOKE":356,"FLAG_REMATCH_CALVIN":383,"FLAG_REMATCH_CAMERON":373,"FLAG_REMATCH_CATHERINE":406,"FLAG_REMATCH_CINDY":359,"FLAG_REMATCH_CORY":401,"FLAG_REMATCH_CRISTIN":355,"FLAG_REMATCH_CYNDY":395,"FLAG_REMATCH_DALTON":368,"FLAG_REMATCH_DIANA":398,"FLAG_REMATCH_DRAKE":424,"FLAG_REMATCH_DUSTY":351,"FLAG_REMATCH_DYLAN":388,"FLAG_REMATCH_EDWIN":402,"FLAG_REMATCH_ELLIOT":384,"FLAG_REMATCH_ERNEST":400,"FLAG_REMATCH_ETHAN":370,"FLAG_REMATCH_FERNANDO":367,"FLAG_REMATCH_FLANNERY":417,"FLAG_REMATCH_GABRIELLE":405,"FLAG_REMATCH_GLACIA":423,"FLAG_REMATCH_HALEY":408,"FLAG_REMATCH_ISAAC":404,"FLAG_REMATCH_ISABEL":379,"FLAG_REMATCH_ISAIAH":385,"FLAG_REMATCH_JACKI":374,"FLAG_REMATCH_JACKSON":407,"FLAG_REMATCH_JAMES":409,"FLAG_REMATCH_JEFFREY":372,"FLAG_REMATCH_JENNY":397,"FLAG_REMATCH_JERRY":377,"FLAG_REMATCH_JESSICA":361,"FLAG_REMATCH_JOHN_AND_JAY":371,"FLAG_REMATCH_KAREN":376,"FLAG_REMATCH_KATELYN":389,"FLAG_REMATCH_KIRA_AND_DAN":412,"FLAG_REMATCH_KOJI":366,"FLAG_REMATCH_LAO":394,"FLAG_REMATCH_LILA_AND_ROY":354,"FLAG_REMATCH_LOLA":352,"FLAG_REMATCH_LYDIA":403,"FLAG_REMATCH_MADELINE":396,"FLAG_REMATCH_MARIA":386,"FLAG_REMATCH_MIGUEL":380,"FLAG_REMATCH_NICOLAS":392,"FLAG_REMATCH_NOB":365,"FLAG_REMATCH_NORMAN":418,"FLAG_REMATCH_PABLO":391,"FLAG_REMATCH_PHOEBE":422,"FLAG_REMATCH_RICKY":353,"FLAG_REMATCH_ROBERT":393,"FLAG_REMATCH_ROSE":349,"FLAG_REMATCH_ROXANNE":414,"FLAG_REMATCH_SAWYER":411,"FLAG_REMATCH_SHELBY":382,"FLAG_REMATCH_SIDNEY":421,"FLAG_REMATCH_STEVE":363,"FLAG_REMATCH_TATE_AND_LIZA":420,"FLAG_REMATCH_THALIA":360,"FLAG_REMATCH_TIMOTHY":381,"FLAG_REMATCH_TONY":364,"FLAG_REMATCH_TRENT":410,"FLAG_REMATCH_VALERIE":358,"FLAG_REMATCH_WALLACE":425,"FLAG_REMATCH_WALLY":413,"FLAG_REMATCH_WALTER":375,"FLAG_REMATCH_WATTSON":416,"FLAG_REMATCH_WILTON":357,"FLAG_REMATCH_WINONA":419,"FLAG_REMATCH_WINSTON":362,"FLAG_RESCUED_BIRCH":82,"FLAG_RETURNED_DEVON_GOODS":144,"FLAG_RETURNED_RED_OR_BLUE_ORB":259,"FLAG_RIVAL_LEFT_FOR_ROUTE103":301,"FLAG_ROUTE_111_RECEIVED_BERRY":1192,"FLAG_ROUTE_114_RECEIVED_BERRY":1193,"FLAG_ROUTE_120_RECEIVED_BERRY":1194,"FLAG_RUSTBORO_NPC_TRADE_COMPLETED":153,"FLAG_RUSTURF_TUNNEL_OPENED":199,"FLAG_SCOTT_CALL_BATTLE_FRONTIER":114,"FLAG_SCOTT_CALL_FORTREE_GYM":138,"FLAG_SCOTT_GIVES_BATTLE_POINTS":465,"FLAG_SECRET_BASE_REGISTRY_ENABLED":268,"FLAG_SET_WALL_CLOCK":81,"FLAG_SHOWN_AURORA_TICKET":431,"FLAG_SHOWN_BOX_WAS_FULL_MESSAGE":2263,"FLAG_SHOWN_EON_TICKET":430,"FLAG_SHOWN_MYSTIC_TICKET":475,"FLAG_SHOWN_OLD_SEA_MAP":432,"FLAG_SMART_PAINTING_MADE":163,"FLAG_SOOTOPOLIS_ARCHIE_MAXIE_LEAVE":158,"FLAG_SOOTOPOLIS_RECEIVED_BERRY_1":1198,"FLAG_SOOTOPOLIS_RECEIVED_BERRY_2":1199,"FLAG_SPECIAL_FLAG_UNUSED_0x4003":16387,"FLAG_SS_TIDAL_DISABLED":84,"FLAG_STEVEN_GUIDES_TO_CAVE_OF_ORIGIN":307,"FLAG_STORING_ITEMS_IN_PYRAMID_BAG":16388,"FLAG_SYS_ARENA_GOLD":2251,"FLAG_SYS_ARENA_SILVER":2250,"FLAG_SYS_BRAILLE_DIG":2223,"FLAG_SYS_BRAILLE_REGICE_COMPLETED":2225,"FLAG_SYS_B_DASH":2240,"FLAG_SYS_CAVE_BATTLE":2201,"FLAG_SYS_CAVE_SHIP":2199,"FLAG_SYS_CAVE_WONDER":2200,"FLAG_SYS_CHANGED_DEWFORD_TREND":2195,"FLAG_SYS_CHAT_USED":2149,"FLAG_SYS_CLOCK_SET":2197,"FLAG_SYS_CRUISE_MODE":2189,"FLAG_SYS_CTRL_OBJ_DELETE":2241,"FLAG_SYS_CYCLING_ROAD":2187,"FLAG_SYS_DOME_GOLD":2247,"FLAG_SYS_DOME_SILVER":2246,"FLAG_SYS_ENC_DOWN_ITEM":2222,"FLAG_SYS_ENC_UP_ITEM":2221,"FLAG_SYS_FACTORY_GOLD":2253,"FLAG_SYS_FACTORY_SILVER":2252,"FLAG_SYS_FRONTIER_PASS":2258,"FLAG_SYS_GAME_CLEAR":2148,"FLAG_SYS_MIX_RECORD":2196,"FLAG_SYS_MYSTERY_EVENT_ENABLE":2220,"FLAG_SYS_MYSTERY_GIFT_ENABLE":2267,"FLAG_SYS_NATIONAL_DEX":2198,"FLAG_SYS_PALACE_GOLD":2249,"FLAG_SYS_PALACE_SILVER":2248,"FLAG_SYS_PC_LANETTE":2219,"FLAG_SYS_PIKE_GOLD":2255,"FLAG_SYS_PIKE_SILVER":2254,"FLAG_SYS_POKEDEX_GET":2145,"FLAG_SYS_POKEMON_GET":2144,"FLAG_SYS_POKENAV_GET":2146,"FLAG_SYS_PYRAMID_GOLD":2257,"FLAG_SYS_PYRAMID_SILVER":2256,"FLAG_SYS_REGIROCK_PUZZLE_COMPLETED":2224,"FLAG_SYS_REGISTEEL_PUZZLE_COMPLETED":2226,"FLAG_SYS_RESET_RTC_ENABLE":2242,"FLAG_SYS_RIBBON_GET":2203,"FLAG_SYS_SAFARI_MODE":2188,"FLAG_SYS_SHOAL_ITEM":2239,"FLAG_SYS_SHOAL_TIDE":2202,"FLAG_SYS_TOWER_GOLD":2245,"FLAG_SYS_TOWER_SILVER":2244,"FLAG_SYS_TV_HOME":2192,"FLAG_SYS_TV_LATIAS_LATIOS":2237,"FLAG_SYS_TV_START":2194,"FLAG_SYS_TV_WATCH":2193,"FLAG_SYS_USE_FLASH":2184,"FLAG_SYS_USE_STRENGTH":2185,"FLAG_SYS_WEATHER_CTRL":2186,"FLAG_TEAM_AQUA_ESCAPED_IN_SUBMARINE":112,"FLAG_TEMP_1":1,"FLAG_TEMP_10":16,"FLAG_TEMP_11":17,"FLAG_TEMP_12":18,"FLAG_TEMP_13":19,"FLAG_TEMP_14":20,"FLAG_TEMP_15":21,"FLAG_TEMP_16":22,"FLAG_TEMP_17":23,"FLAG_TEMP_18":24,"FLAG_TEMP_19":25,"FLAG_TEMP_1A":26,"FLAG_TEMP_1B":27,"FLAG_TEMP_1C":28,"FLAG_TEMP_1D":29,"FLAG_TEMP_1E":30,"FLAG_TEMP_1F":31,"FLAG_TEMP_2":2,"FLAG_TEMP_3":3,"FLAG_TEMP_4":4,"FLAG_TEMP_5":5,"FLAG_TEMP_6":6,"FLAG_TEMP_7":7,"FLAG_TEMP_8":8,"FLAG_TEMP_9":9,"FLAG_TEMP_A":10,"FLAG_TEMP_B":11,"FLAG_TEMP_C":12,"FLAG_TEMP_D":13,"FLAG_TEMP_E":14,"FLAG_TEMP_F":15,"FLAG_TEMP_HIDE_MIRAGE_ISLAND_BERRY_TREE":17,"FLAG_TEMP_REGICE_PUZZLE_FAILED":3,"FLAG_TEMP_REGICE_PUZZLE_STARTED":2,"FLAG_TEMP_SKIP_GABBY_INTERVIEW":1,"FLAG_THANKED_FOR_PLAYING_WITH_WALLY":135,"FLAG_TOUGH_PAINTING_MADE":164,"FLAG_TRICK_HOUSE_PUZZLE_7_SWITCH_1":194,"FLAG_TRICK_HOUSE_PUZZLE_7_SWITCH_2":195,"FLAG_TRICK_HOUSE_PUZZLE_7_SWITCH_3":196,"FLAG_TRICK_HOUSE_PUZZLE_7_SWITCH_4":197,"FLAG_TRICK_HOUSE_PUZZLE_7_SWITCH_5":198,"FLAG_TV_EXPLAINED":98,"FLAG_UNLOCKED_TRENDY_SAYINGS":2150,"FLAG_USED_ROOM_1_KEY":240,"FLAG_USED_ROOM_2_KEY":241,"FLAG_USED_ROOM_4_KEY":242,"FLAG_USED_ROOM_6_KEY":243,"FLAG_USED_STORAGE_KEY":239,"FLAG_VISITED_DEWFORD_TOWN":2161,"FLAG_VISITED_EVER_GRANDE_CITY":2174,"FLAG_VISITED_FALLARBOR_TOWN":2163,"FLAG_VISITED_FORTREE_CITY":2170,"FLAG_VISITED_LAVARIDGE_TOWN":2162,"FLAG_VISITED_LILYCOVE_CITY":2171,"FLAG_VISITED_LITTLEROOT_TOWN":2159,"FLAG_VISITED_MAUVILLE_CITY":2168,"FLAG_VISITED_MOSSDEEP_CITY":2172,"FLAG_VISITED_OLDALE_TOWN":2160,"FLAG_VISITED_PACIFIDLOG_TOWN":2165,"FLAG_VISITED_PETALBURG_CITY":2166,"FLAG_VISITED_RUSTBORO_CITY":2169,"FLAG_VISITED_SLATEPORT_CITY":2167,"FLAG_VISITED_SOOTOPOLIS_CITY":2173,"FLAG_VISITED_VERDANTURF_TOWN":2164,"FLAG_WALLACE_GOES_TO_SKY_PILLAR":311,"FLAG_WALLY_SPEECH":193,"FLAG_WATTSON_REMATCH_AVAILABLE":91,"FLAG_WHITEOUT_TO_LAVARIDGE":108,"FLAG_WINGULL_DELIVERED_MAIL":224,"FLAG_WINGULL_SENT_ON_ERRAND":222,"FLAG_WONDER_CARD_UNUSED_1":317,"FLAG_WONDER_CARD_UNUSED_10":326,"FLAG_WONDER_CARD_UNUSED_11":327,"FLAG_WONDER_CARD_UNUSED_12":328,"FLAG_WONDER_CARD_UNUSED_13":329,"FLAG_WONDER_CARD_UNUSED_14":330,"FLAG_WONDER_CARD_UNUSED_15":331,"FLAG_WONDER_CARD_UNUSED_16":332,"FLAG_WONDER_CARD_UNUSED_17":333,"FLAG_WONDER_CARD_UNUSED_2":318,"FLAG_WONDER_CARD_UNUSED_3":319,"FLAG_WONDER_CARD_UNUSED_4":320,"FLAG_WONDER_CARD_UNUSED_5":321,"FLAG_WONDER_CARD_UNUSED_6":322,"FLAG_WONDER_CARD_UNUSED_7":323,"FLAG_WONDER_CARD_UNUSED_8":324,"FLAG_WONDER_CARD_UNUSED_9":325,"FLAVOR_BITTER":3,"FLAVOR_COUNT":5,"FLAVOR_DRY":1,"FLAVOR_SOUR":4,"FLAVOR_SPICY":0,"FLAVOR_SWEET":2,"GOOD_ROD":1,"ITEMS_COUNT":377,"ITEM_034":52,"ITEM_035":53,"ITEM_036":54,"ITEM_037":55,"ITEM_038":56,"ITEM_039":57,"ITEM_03A":58,"ITEM_03B":59,"ITEM_03C":60,"ITEM_03D":61,"ITEM_03E":62,"ITEM_048":72,"ITEM_052":82,"ITEM_057":87,"ITEM_058":88,"ITEM_059":89,"ITEM_05A":90,"ITEM_05B":91,"ITEM_05C":92,"ITEM_063":99,"ITEM_064":100,"ITEM_065":101,"ITEM_066":102,"ITEM_069":105,"ITEM_071":113,"ITEM_072":114,"ITEM_073":115,"ITEM_074":116,"ITEM_075":117,"ITEM_076":118,"ITEM_077":119,"ITEM_078":120,"ITEM_0EA":234,"ITEM_0EB":235,"ITEM_0EC":236,"ITEM_0ED":237,"ITEM_0EE":238,"ITEM_0EF":239,"ITEM_0F0":240,"ITEM_0F1":241,"ITEM_0F2":242,"ITEM_0F3":243,"ITEM_0F4":244,"ITEM_0F5":245,"ITEM_0F6":246,"ITEM_0F7":247,"ITEM_0F8":248,"ITEM_0F9":249,"ITEM_0FA":250,"ITEM_0FB":251,"ITEM_0FC":252,"ITEM_0FD":253,"ITEM_10B":267,"ITEM_15B":347,"ITEM_15C":348,"ITEM_ACRO_BIKE":272,"ITEM_AGUAV_BERRY":146,"ITEM_AMULET_COIN":189,"ITEM_ANTIDOTE":14,"ITEM_APICOT_BERRY":172,"ITEM_ARCHIPELAGO_PROGRESSION":112,"ITEM_ASPEAR_BERRY":137,"ITEM_AURORA_TICKET":371,"ITEM_AWAKENING":17,"ITEM_BADGE_1":226,"ITEM_BADGE_2":227,"ITEM_BADGE_3":228,"ITEM_BADGE_4":229,"ITEM_BADGE_5":230,"ITEM_BADGE_6":231,"ITEM_BADGE_7":232,"ITEM_BADGE_8":233,"ITEM_BASEMENT_KEY":271,"ITEM_BEAD_MAIL":127,"ITEM_BELUE_BERRY":167,"ITEM_BERRY_JUICE":44,"ITEM_BERRY_POUCH":365,"ITEM_BICYCLE":360,"ITEM_BIG_MUSHROOM":104,"ITEM_BIG_PEARL":107,"ITEM_BIKE_VOUCHER":352,"ITEM_BLACK_BELT":207,"ITEM_BLACK_FLUTE":42,"ITEM_BLACK_GLASSES":206,"ITEM_BLUE_FLUTE":39,"ITEM_BLUE_ORB":277,"ITEM_BLUE_SCARF":255,"ITEM_BLUE_SHARD":49,"ITEM_BLUK_BERRY":149,"ITEM_BRIGHT_POWDER":179,"ITEM_BURN_HEAL":15,"ITEM_B_USE_MEDICINE":1,"ITEM_B_USE_OTHER":2,"ITEM_CALCIUM":67,"ITEM_CARBOS":66,"ITEM_CARD_KEY":355,"ITEM_CHARCOAL":215,"ITEM_CHERI_BERRY":133,"ITEM_CHESTO_BERRY":134,"ITEM_CHOICE_BAND":186,"ITEM_CLAW_FOSSIL":287,"ITEM_CLEANSE_TAG":190,"ITEM_COIN_CASE":260,"ITEM_CONTEST_PASS":266,"ITEM_CORNN_BERRY":159,"ITEM_DEEP_SEA_SCALE":193,"ITEM_DEEP_SEA_TOOTH":192,"ITEM_DEVON_GOODS":269,"ITEM_DEVON_SCOPE":288,"ITEM_DIRE_HIT":74,"ITEM_DIVE_BALL":7,"ITEM_DOME_FOSSIL":358,"ITEM_DRAGON_FANG":216,"ITEM_DRAGON_SCALE":201,"ITEM_DREAM_MAIL":130,"ITEM_DURIN_BERRY":166,"ITEM_ELIXIR":36,"ITEM_ENERGY_POWDER":30,"ITEM_ENERGY_ROOT":31,"ITEM_ENIGMA_BERRY":175,"ITEM_EON_TICKET":275,"ITEM_ESCAPE_ROPE":85,"ITEM_ETHER":34,"ITEM_EVERSTONE":195,"ITEM_EXP_SHARE":182,"ITEM_FAB_MAIL":131,"ITEM_FAME_CHECKER":363,"ITEM_FIGY_BERRY":143,"ITEM_FIRE_STONE":95,"ITEM_FLUFFY_TAIL":81,"ITEM_FOCUS_BAND":196,"ITEM_FRESH_WATER":26,"ITEM_FULL_HEAL":23,"ITEM_FULL_RESTORE":19,"ITEM_GANLON_BERRY":169,"ITEM_GLITTER_MAIL":123,"ITEM_GOLD_TEETH":353,"ITEM_GOOD_ROD":263,"ITEM_GO_GOGGLES":279,"ITEM_GREAT_BALL":3,"ITEM_GREEN_SCARF":257,"ITEM_GREEN_SHARD":51,"ITEM_GREPA_BERRY":157,"ITEM_GUARD_SPEC":73,"ITEM_HARBOR_MAIL":122,"ITEM_HARD_STONE":204,"ITEM_HEAL_POWDER":32,"ITEM_HEART_SCALE":111,"ITEM_HELIX_FOSSIL":357,"ITEM_HM01":339,"ITEM_HM02":340,"ITEM_HM03":341,"ITEM_HM04":342,"ITEM_HM05":343,"ITEM_HM06":344,"ITEM_HM07":345,"ITEM_HM08":346,"ITEM_HM_CUT":339,"ITEM_HM_DIVE":346,"ITEM_HM_FLASH":343,"ITEM_HM_FLY":340,"ITEM_HM_ROCK_SMASH":344,"ITEM_HM_STRENGTH":342,"ITEM_HM_SURF":341,"ITEM_HM_WATERFALL":345,"ITEM_HONDEW_BERRY":156,"ITEM_HP_UP":63,"ITEM_HYPER_POTION":21,"ITEM_IAPAPA_BERRY":147,"ITEM_ICE_HEAL":16,"ITEM_IRON":65,"ITEM_ITEMFINDER":261,"ITEM_KELPSY_BERRY":154,"ITEM_KINGS_ROCK":187,"ITEM_LANSAT_BERRY":173,"ITEM_LAVA_COOKIE":38,"ITEM_LAX_INCENSE":221,"ITEM_LEAF_STONE":98,"ITEM_LEFTOVERS":200,"ITEM_LEMONADE":28,"ITEM_LEPPA_BERRY":138,"ITEM_LETTER":274,"ITEM_LIECHI_BERRY":168,"ITEM_LIFT_KEY":356,"ITEM_LIGHT_BALL":202,"ITEM_LIST_END":65535,"ITEM_LUCKY_EGG":197,"ITEM_LUCKY_PUNCH":222,"ITEM_LUM_BERRY":141,"ITEM_LUXURY_BALL":11,"ITEM_MACHO_BRACE":181,"ITEM_MACH_BIKE":259,"ITEM_MAGMA_EMBLEM":375,"ITEM_MAGNET":208,"ITEM_MAGOST_BERRY":160,"ITEM_MAGO_BERRY":145,"ITEM_MASTER_BALL":1,"ITEM_MAX_ELIXIR":37,"ITEM_MAX_ETHER":35,"ITEM_MAX_POTION":20,"ITEM_MAX_REPEL":84,"ITEM_MAX_REVIVE":25,"ITEM_MECH_MAIL":124,"ITEM_MENTAL_HERB":185,"ITEM_METAL_COAT":199,"ITEM_METAL_POWDER":223,"ITEM_METEORITE":280,"ITEM_MIRACLE_SEED":205,"ITEM_MOOMOO_MILK":29,"ITEM_MOON_STONE":94,"ITEM_MYSTIC_TICKET":370,"ITEM_MYSTIC_WATER":209,"ITEM_NANAB_BERRY":150,"ITEM_NEST_BALL":8,"ITEM_NET_BALL":6,"ITEM_NEVER_MELT_ICE":212,"ITEM_NOMEL_BERRY":162,"ITEM_NONE":0,"ITEM_NUGGET":110,"ITEM_OAKS_PARCEL":349,"ITEM_OLD_AMBER":354,"ITEM_OLD_ROD":262,"ITEM_OLD_SEA_MAP":376,"ITEM_ORANGE_MAIL":121,"ITEM_ORAN_BERRY":139,"ITEM_PAMTRE_BERRY":164,"ITEM_PARALYZE_HEAL":18,"ITEM_PEARL":106,"ITEM_PECHA_BERRY":135,"ITEM_PERSIM_BERRY":140,"ITEM_PETAYA_BERRY":171,"ITEM_PINAP_BERRY":152,"ITEM_PINK_SCARF":256,"ITEM_POISON_BARB":211,"ITEM_POKEBLOCK_CASE":273,"ITEM_POKE_BALL":4,"ITEM_POKE_DOLL":80,"ITEM_POKE_FLUTE":350,"ITEM_POMEG_BERRY":153,"ITEM_POTION":13,"ITEM_POWDER_JAR":372,"ITEM_PP_MAX":71,"ITEM_PP_UP":69,"ITEM_PREMIER_BALL":12,"ITEM_PROTEIN":64,"ITEM_QUALOT_BERRY":155,"ITEM_QUICK_CLAW":183,"ITEM_RABUTA_BERRY":161,"ITEM_RAINBOW_PASS":368,"ITEM_RARE_CANDY":68,"ITEM_RAWST_BERRY":136,"ITEM_RAZZ_BERRY":148,"ITEM_RED_FLUTE":41,"ITEM_RED_ORB":276,"ITEM_RED_SCARF":254,"ITEM_RED_SHARD":48,"ITEM_REPEAT_BALL":9,"ITEM_REPEL":86,"ITEM_RETRO_MAIL":132,"ITEM_REVIVAL_HERB":33,"ITEM_REVIVE":24,"ITEM_ROOM_1_KEY":281,"ITEM_ROOM_2_KEY":282,"ITEM_ROOM_4_KEY":283,"ITEM_ROOM_6_KEY":284,"ITEM_ROOT_FOSSIL":286,"ITEM_RUBY":373,"ITEM_SACRED_ASH":45,"ITEM_SAFARI_BALL":5,"ITEM_SALAC_BERRY":170,"ITEM_SAPPHIRE":374,"ITEM_SCANNER":278,"ITEM_SCOPE_LENS":198,"ITEM_SEA_INCENSE":220,"ITEM_SECRET_KEY":351,"ITEM_SHADOW_MAIL":128,"ITEM_SHARP_BEAK":210,"ITEM_SHELL_BELL":219,"ITEM_SHOAL_SALT":46,"ITEM_SHOAL_SHELL":47,"ITEM_SILK_SCARF":217,"ITEM_SILPH_SCOPE":359,"ITEM_SILVER_POWDER":188,"ITEM_SITRUS_BERRY":142,"ITEM_SMOKE_BALL":194,"ITEM_SODA_POP":27,"ITEM_SOFT_SAND":203,"ITEM_SOOTHE_BELL":184,"ITEM_SOOT_SACK":270,"ITEM_SOUL_DEW":191,"ITEM_SPELL_TAG":213,"ITEM_SPELON_BERRY":163,"ITEM_SS_TICKET":265,"ITEM_STARDUST":108,"ITEM_STARF_BERRY":174,"ITEM_STAR_PIECE":109,"ITEM_STICK":225,"ITEM_STORAGE_KEY":285,"ITEM_SUN_STONE":93,"ITEM_SUPER_POTION":22,"ITEM_SUPER_REPEL":83,"ITEM_SUPER_ROD":264,"ITEM_TAMATO_BERRY":158,"ITEM_TEA":369,"ITEM_TEACHY_TV":366,"ITEM_THICK_CLUB":224,"ITEM_THUNDER_STONE":96,"ITEM_TIMER_BALL":10,"ITEM_TINY_MUSHROOM":103,"ITEM_TM01":289,"ITEM_TM02":290,"ITEM_TM03":291,"ITEM_TM04":292,"ITEM_TM05":293,"ITEM_TM06":294,"ITEM_TM07":295,"ITEM_TM08":296,"ITEM_TM09":297,"ITEM_TM10":298,"ITEM_TM11":299,"ITEM_TM12":300,"ITEM_TM13":301,"ITEM_TM14":302,"ITEM_TM15":303,"ITEM_TM16":304,"ITEM_TM17":305,"ITEM_TM18":306,"ITEM_TM19":307,"ITEM_TM20":308,"ITEM_TM21":309,"ITEM_TM22":310,"ITEM_TM23":311,"ITEM_TM24":312,"ITEM_TM25":313,"ITEM_TM26":314,"ITEM_TM27":315,"ITEM_TM28":316,"ITEM_TM29":317,"ITEM_TM30":318,"ITEM_TM31":319,"ITEM_TM32":320,"ITEM_TM33":321,"ITEM_TM34":322,"ITEM_TM35":323,"ITEM_TM36":324,"ITEM_TM37":325,"ITEM_TM38":326,"ITEM_TM39":327,"ITEM_TM40":328,"ITEM_TM41":329,"ITEM_TM42":330,"ITEM_TM43":331,"ITEM_TM44":332,"ITEM_TM45":333,"ITEM_TM46":334,"ITEM_TM47":335,"ITEM_TM48":336,"ITEM_TM49":337,"ITEM_TM50":338,"ITEM_TM_AERIAL_ACE":328,"ITEM_TM_ATTRACT":333,"ITEM_TM_BLIZZARD":302,"ITEM_TM_BRICK_BREAK":319,"ITEM_TM_BULK_UP":296,"ITEM_TM_BULLET_SEED":297,"ITEM_TM_CALM_MIND":292,"ITEM_TM_CASE":364,"ITEM_TM_DIG":316,"ITEM_TM_DOUBLE_TEAM":320,"ITEM_TM_DRAGON_CLAW":290,"ITEM_TM_EARTHQUAKE":314,"ITEM_TM_FACADE":330,"ITEM_TM_FIRE_BLAST":326,"ITEM_TM_FLAMETHROWER":323,"ITEM_TM_FOCUS_PUNCH":289,"ITEM_TM_FRUSTRATION":309,"ITEM_TM_GIGA_DRAIN":307,"ITEM_TM_HAIL":295,"ITEM_TM_HIDDEN_POWER":298,"ITEM_TM_HYPER_BEAM":303,"ITEM_TM_ICE_BEAM":301,"ITEM_TM_IRON_TAIL":311,"ITEM_TM_LIGHT_SCREEN":304,"ITEM_TM_OVERHEAT":338,"ITEM_TM_PROTECT":305,"ITEM_TM_PSYCHIC":317,"ITEM_TM_RAIN_DANCE":306,"ITEM_TM_REFLECT":321,"ITEM_TM_REST":332,"ITEM_TM_RETURN":315,"ITEM_TM_ROAR":293,"ITEM_TM_ROCK_TOMB":327,"ITEM_TM_SAFEGUARD":308,"ITEM_TM_SANDSTORM":325,"ITEM_TM_SECRET_POWER":331,"ITEM_TM_SHADOW_BALL":318,"ITEM_TM_SHOCK_WAVE":322,"ITEM_TM_SKILL_SWAP":336,"ITEM_TM_SLUDGE_BOMB":324,"ITEM_TM_SNATCH":337,"ITEM_TM_SOLAR_BEAM":310,"ITEM_TM_STEEL_WING":335,"ITEM_TM_SUNNY_DAY":299,"ITEM_TM_TAUNT":300,"ITEM_TM_THIEF":334,"ITEM_TM_THUNDER":313,"ITEM_TM_THUNDERBOLT":312,"ITEM_TM_TORMENT":329,"ITEM_TM_TOXIC":294,"ITEM_TM_WATER_PULSE":291,"ITEM_TOWN_MAP":361,"ITEM_TRI_PASS":367,"ITEM_TROPIC_MAIL":129,"ITEM_TWISTED_SPOON":214,"ITEM_ULTRA_BALL":2,"ITEM_UNUSED_BERRY_1":176,"ITEM_UNUSED_BERRY_2":177,"ITEM_UNUSED_BERRY_3":178,"ITEM_UP_GRADE":218,"ITEM_USE_BAG_MENU":4,"ITEM_USE_FIELD":2,"ITEM_USE_MAIL":0,"ITEM_USE_PARTY_MENU":1,"ITEM_USE_PBLOCK_CASE":3,"ITEM_VS_SEEKER":362,"ITEM_WAILMER_PAIL":268,"ITEM_WATER_STONE":97,"ITEM_WATMEL_BERRY":165,"ITEM_WAVE_MAIL":126,"ITEM_WEPEAR_BERRY":151,"ITEM_WHITE_FLUTE":43,"ITEM_WHITE_HERB":180,"ITEM_WIKI_BERRY":144,"ITEM_WOOD_MAIL":125,"ITEM_X_ACCURACY":78,"ITEM_X_ATTACK":75,"ITEM_X_DEFEND":76,"ITEM_X_SPECIAL":79,"ITEM_X_SPEED":77,"ITEM_YELLOW_FLUTE":40,"ITEM_YELLOW_SCARF":258,"ITEM_YELLOW_SHARD":50,"ITEM_ZINC":70,"LAST_BALL":12,"LAST_BERRY_INDEX":175,"LAST_BERRY_MASTER_BERRY":162,"LAST_BERRY_MASTER_WIFE_BERRY":142,"LAST_KIRI_BERRY":162,"LAST_ROUTE_114_MAN_BERRY":152,"MACH_BIKE":0,"MAIL_NONE":255,"MAP_ABANDONED_SHIP_CAPTAINS_OFFICE":6207,"MAP_ABANDONED_SHIP_CORRIDORS_1F":6199,"MAP_ABANDONED_SHIP_CORRIDORS_B1F":6201,"MAP_ABANDONED_SHIP_DECK":6198,"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS":6209,"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS":6210,"MAP_ABANDONED_SHIP_ROOMS2_1F":6206,"MAP_ABANDONED_SHIP_ROOMS2_B1F":6203,"MAP_ABANDONED_SHIP_ROOMS_1F":6200,"MAP_ABANDONED_SHIP_ROOMS_B1F":6202,"MAP_ABANDONED_SHIP_ROOM_B1F":6205,"MAP_ABANDONED_SHIP_UNDERWATER1":6204,"MAP_ABANDONED_SHIP_UNDERWATER2":6208,"MAP_ALTERING_CAVE":6250,"MAP_ANCIENT_TOMB":6212,"MAP_AQUA_HIDEOUT_1F":6167,"MAP_AQUA_HIDEOUT_B1F":6168,"MAP_AQUA_HIDEOUT_B2F":6169,"MAP_AQUA_HIDEOUT_UNUSED_RUBY_MAP1":6218,"MAP_AQUA_HIDEOUT_UNUSED_RUBY_MAP2":6219,"MAP_AQUA_HIDEOUT_UNUSED_RUBY_MAP3":6220,"MAP_ARTISAN_CAVE_1F":6244,"MAP_ARTISAN_CAVE_B1F":6243,"MAP_BATTLE_COLOSSEUM_2P":6424,"MAP_BATTLE_COLOSSEUM_4P":6427,"MAP_BATTLE_FRONTIER_BATTLE_ARENA_BATTLE_ROOM":6686,"MAP_BATTLE_FRONTIER_BATTLE_ARENA_CORRIDOR":6685,"MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY":6684,"MAP_BATTLE_FRONTIER_BATTLE_DOME_BATTLE_ROOM":6677,"MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR":6675,"MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY":6674,"MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM":6676,"MAP_BATTLE_FRONTIER_BATTLE_FACTORY_BATTLE_ROOM":6689,"MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY":6687,"MAP_BATTLE_FRONTIER_BATTLE_FACTORY_PRE_BATTLE_ROOM":6688,"MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM":6680,"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR":6679,"MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY":6678,"MAP_BATTLE_FRONTIER_BATTLE_PIKE_CORRIDOR":6691,"MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY":6690,"MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_FINAL":6694,"MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_NORMAL":6693,"MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_WILD_MONS":6695,"MAP_BATTLE_FRONTIER_BATTLE_PIKE_THREE_PATH_ROOM":6692,"MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_FLOOR":6682,"MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY":6681,"MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_TOP":6683,"MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM":6664,"MAP_BATTLE_FRONTIER_BATTLE_TOWER_CORRIDOR":6663,"MAP_BATTLE_FRONTIER_BATTLE_TOWER_ELEVATOR":6662,"MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY":6661,"MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_BATTLE_ROOM":6673,"MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_CORRIDOR":6672,"MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_PARTNER_ROOM":6671,"MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER":6698,"MAP_BATTLE_FRONTIER_LOUNGE1":6697,"MAP_BATTLE_FRONTIER_LOUNGE2":6699,"MAP_BATTLE_FRONTIER_LOUNGE3":6700,"MAP_BATTLE_FRONTIER_LOUNGE4":6701,"MAP_BATTLE_FRONTIER_LOUNGE5":6703,"MAP_BATTLE_FRONTIER_LOUNGE6":6704,"MAP_BATTLE_FRONTIER_LOUNGE7":6705,"MAP_BATTLE_FRONTIER_LOUNGE8":6707,"MAP_BATTLE_FRONTIER_LOUNGE9":6708,"MAP_BATTLE_FRONTIER_MART":6711,"MAP_BATTLE_FRONTIER_OUTSIDE_EAST":6670,"MAP_BATTLE_FRONTIER_OUTSIDE_WEST":6660,"MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F":6709,"MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F":6710,"MAP_BATTLE_FRONTIER_RANKING_HALL":6696,"MAP_BATTLE_FRONTIER_RECEPTION_GATE":6706,"MAP_BATTLE_FRONTIER_SCOTTS_HOUSE":6702,"MAP_BATTLE_PYRAMID_SQUARE01":6444,"MAP_BATTLE_PYRAMID_SQUARE02":6445,"MAP_BATTLE_PYRAMID_SQUARE03":6446,"MAP_BATTLE_PYRAMID_SQUARE04":6447,"MAP_BATTLE_PYRAMID_SQUARE05":6448,"MAP_BATTLE_PYRAMID_SQUARE06":6449,"MAP_BATTLE_PYRAMID_SQUARE07":6450,"MAP_BATTLE_PYRAMID_SQUARE08":6451,"MAP_BATTLE_PYRAMID_SQUARE09":6452,"MAP_BATTLE_PYRAMID_SQUARE10":6453,"MAP_BATTLE_PYRAMID_SQUARE11":6454,"MAP_BATTLE_PYRAMID_SQUARE12":6455,"MAP_BATTLE_PYRAMID_SQUARE13":6456,"MAP_BATTLE_PYRAMID_SQUARE14":6457,"MAP_BATTLE_PYRAMID_SQUARE15":6458,"MAP_BATTLE_PYRAMID_SQUARE16":6459,"MAP_BIRTH_ISLAND_EXTERIOR":6714,"MAP_BIRTH_ISLAND_HARBOR":6715,"MAP_CAVE_OF_ORIGIN_1F":6182,"MAP_CAVE_OF_ORIGIN_B1F":6186,"MAP_CAVE_OF_ORIGIN_ENTRANCE":6181,"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1":6183,"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2":6184,"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3":6185,"MAP_CONTEST_HALL":6428,"MAP_CONTEST_HALL_BEAUTY":6435,"MAP_CONTEST_HALL_COOL":6437,"MAP_CONTEST_HALL_CUTE":6439,"MAP_CONTEST_HALL_SMART":6438,"MAP_CONTEST_HALL_TOUGH":6436,"MAP_DESERT_RUINS":6150,"MAP_DESERT_UNDERPASS":6242,"MAP_DEWFORD_TOWN":11,"MAP_DEWFORD_TOWN_GYM":771,"MAP_DEWFORD_TOWN_HALL":772,"MAP_DEWFORD_TOWN_HOUSE1":768,"MAP_DEWFORD_TOWN_HOUSE2":773,"MAP_DEWFORD_TOWN_POKEMON_CENTER_1F":769,"MAP_DEWFORD_TOWN_POKEMON_CENTER_2F":770,"MAP_EVER_GRANDE_CITY":8,"MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM":4100,"MAP_EVER_GRANDE_CITY_DRAKES_ROOM":4099,"MAP_EVER_GRANDE_CITY_GLACIAS_ROOM":4098,"MAP_EVER_GRANDE_CITY_HALL1":4101,"MAP_EVER_GRANDE_CITY_HALL2":4102,"MAP_EVER_GRANDE_CITY_HALL3":4103,"MAP_EVER_GRANDE_CITY_HALL4":4104,"MAP_EVER_GRANDE_CITY_HALL5":4105,"MAP_EVER_GRANDE_CITY_HALL_OF_FAME":4107,"MAP_EVER_GRANDE_CITY_PHOEBES_ROOM":4097,"MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F":4108,"MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F":4109,"MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F":4106,"MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F":4110,"MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM":4096,"MAP_FALLARBOR_TOWN":13,"MAP_FALLARBOR_TOWN_BATTLE_TENT_BATTLE_ROOM":1283,"MAP_FALLARBOR_TOWN_BATTLE_TENT_CORRIDOR":1282,"MAP_FALLARBOR_TOWN_BATTLE_TENT_LOBBY":1281,"MAP_FALLARBOR_TOWN_COZMOS_HOUSE":1286,"MAP_FALLARBOR_TOWN_MART":1280,"MAP_FALLARBOR_TOWN_MOVE_RELEARNERS_HOUSE":1287,"MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F":1284,"MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F":1285,"MAP_FARAWAY_ISLAND_ENTRANCE":6712,"MAP_FARAWAY_ISLAND_INTERIOR":6713,"MAP_FIERY_PATH":6158,"MAP_FORTREE_CITY":4,"MAP_FORTREE_CITY_DECORATION_SHOP":3081,"MAP_FORTREE_CITY_GYM":3073,"MAP_FORTREE_CITY_HOUSE1":3072,"MAP_FORTREE_CITY_HOUSE2":3077,"MAP_FORTREE_CITY_HOUSE3":3078,"MAP_FORTREE_CITY_HOUSE4":3079,"MAP_FORTREE_CITY_HOUSE5":3080,"MAP_FORTREE_CITY_MART":3076,"MAP_FORTREE_CITY_POKEMON_CENTER_1F":3074,"MAP_FORTREE_CITY_POKEMON_CENTER_2F":3075,"MAP_GRANITE_CAVE_1F":6151,"MAP_GRANITE_CAVE_B1F":6152,"MAP_GRANITE_CAVE_B2F":6153,"MAP_GRANITE_CAVE_STEVENS_ROOM":6154,"MAP_GROUPS_COUNT":34,"MAP_INSIDE_OF_TRUCK":6440,"MAP_ISLAND_CAVE":6211,"MAP_JAGGED_PASS":6157,"MAP_LAVARIDGE_TOWN":12,"MAP_LAVARIDGE_TOWN_GYM_1F":1025,"MAP_LAVARIDGE_TOWN_GYM_B1F":1026,"MAP_LAVARIDGE_TOWN_HERB_SHOP":1024,"MAP_LAVARIDGE_TOWN_HOUSE":1027,"MAP_LAVARIDGE_TOWN_MART":1028,"MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F":1029,"MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F":1030,"MAP_LILYCOVE_CITY":5,"MAP_LILYCOVE_CITY_CONTEST_HALL":3333,"MAP_LILYCOVE_CITY_CONTEST_LOBBY":3332,"MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F":3328,"MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_2F":3329,"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F":3344,"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F":3345,"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F":3346,"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F":3347,"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F":3348,"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR":3350,"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ROOFTOP":3349,"MAP_LILYCOVE_CITY_HARBOR":3338,"MAP_LILYCOVE_CITY_HOUSE1":3340,"MAP_LILYCOVE_CITY_HOUSE2":3341,"MAP_LILYCOVE_CITY_HOUSE3":3342,"MAP_LILYCOVE_CITY_HOUSE4":3343,"MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F":3330,"MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_2F":3331,"MAP_LILYCOVE_CITY_MOVE_DELETERS_HOUSE":3339,"MAP_LILYCOVE_CITY_POKEMON_CENTER_1F":3334,"MAP_LILYCOVE_CITY_POKEMON_CENTER_2F":3335,"MAP_LILYCOVE_CITY_POKEMON_TRAINER_FAN_CLUB":3337,"MAP_LILYCOVE_CITY_UNUSED_MART":3336,"MAP_LITTLEROOT_TOWN":9,"MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F":256,"MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F":257,"MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F":258,"MAP_LITTLEROOT_TOWN_MAYS_HOUSE_2F":259,"MAP_LITTLEROOT_TOWN_PROFESSOR_BIRCHS_LAB":260,"MAP_MAGMA_HIDEOUT_1F":6230,"MAP_MAGMA_HIDEOUT_2F_1R":6231,"MAP_MAGMA_HIDEOUT_2F_2R":6232,"MAP_MAGMA_HIDEOUT_2F_3R":6237,"MAP_MAGMA_HIDEOUT_3F_1R":6233,"MAP_MAGMA_HIDEOUT_3F_2R":6234,"MAP_MAGMA_HIDEOUT_3F_3R":6236,"MAP_MAGMA_HIDEOUT_4F":6235,"MAP_MARINE_CAVE_END":6247,"MAP_MARINE_CAVE_ENTRANCE":6246,"MAP_MAUVILLE_CITY":2,"MAP_MAUVILLE_CITY_BIKE_SHOP":2561,"MAP_MAUVILLE_CITY_GAME_CORNER":2563,"MAP_MAUVILLE_CITY_GYM":2560,"MAP_MAUVILLE_CITY_HOUSE1":2562,"MAP_MAUVILLE_CITY_HOUSE2":2564,"MAP_MAUVILLE_CITY_MART":2567,"MAP_MAUVILLE_CITY_POKEMON_CENTER_1F":2565,"MAP_MAUVILLE_CITY_POKEMON_CENTER_2F":2566,"MAP_METEOR_FALLS_1F_1R":6144,"MAP_METEOR_FALLS_1F_2R":6145,"MAP_METEOR_FALLS_B1F_1R":6146,"MAP_METEOR_FALLS_B1F_2R":6147,"MAP_METEOR_FALLS_STEVENS_CAVE":6251,"MAP_MIRAGE_TOWER_1F":6238,"MAP_MIRAGE_TOWER_2F":6239,"MAP_MIRAGE_TOWER_3F":6240,"MAP_MIRAGE_TOWER_4F":6241,"MAP_MOSSDEEP_CITY":6,"MAP_MOSSDEEP_CITY_GAME_CORNER_1F":3595,"MAP_MOSSDEEP_CITY_GAME_CORNER_B1F":3596,"MAP_MOSSDEEP_CITY_GYM":3584,"MAP_MOSSDEEP_CITY_HOUSE1":3585,"MAP_MOSSDEEP_CITY_HOUSE2":3586,"MAP_MOSSDEEP_CITY_HOUSE3":3590,"MAP_MOSSDEEP_CITY_HOUSE4":3592,"MAP_MOSSDEEP_CITY_MART":3589,"MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F":3587,"MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F":3588,"MAP_MOSSDEEP_CITY_SPACE_CENTER_1F":3593,"MAP_MOSSDEEP_CITY_SPACE_CENTER_2F":3594,"MAP_MOSSDEEP_CITY_STEVENS_HOUSE":3591,"MAP_MT_CHIMNEY":6156,"MAP_MT_CHIMNEY_CABLE_CAR_STATION":4865,"MAP_MT_PYRE_1F":6159,"MAP_MT_PYRE_2F":6160,"MAP_MT_PYRE_3F":6161,"MAP_MT_PYRE_4F":6162,"MAP_MT_PYRE_5F":6163,"MAP_MT_PYRE_6F":6164,"MAP_MT_PYRE_EXTERIOR":6165,"MAP_MT_PYRE_SUMMIT":6166,"MAP_NAVEL_ROCK_B1F":6725,"MAP_NAVEL_ROCK_BOTTOM":6743,"MAP_NAVEL_ROCK_DOWN01":6732,"MAP_NAVEL_ROCK_DOWN02":6733,"MAP_NAVEL_ROCK_DOWN03":6734,"MAP_NAVEL_ROCK_DOWN04":6735,"MAP_NAVEL_ROCK_DOWN05":6736,"MAP_NAVEL_ROCK_DOWN06":6737,"MAP_NAVEL_ROCK_DOWN07":6738,"MAP_NAVEL_ROCK_DOWN08":6739,"MAP_NAVEL_ROCK_DOWN09":6740,"MAP_NAVEL_ROCK_DOWN10":6741,"MAP_NAVEL_ROCK_DOWN11":6742,"MAP_NAVEL_ROCK_ENTRANCE":6724,"MAP_NAVEL_ROCK_EXTERIOR":6722,"MAP_NAVEL_ROCK_FORK":6726,"MAP_NAVEL_ROCK_HARBOR":6723,"MAP_NAVEL_ROCK_TOP":6731,"MAP_NAVEL_ROCK_UP1":6727,"MAP_NAVEL_ROCK_UP2":6728,"MAP_NAVEL_ROCK_UP3":6729,"MAP_NAVEL_ROCK_UP4":6730,"MAP_NEW_MAUVILLE_ENTRANCE":6196,"MAP_NEW_MAUVILLE_INSIDE":6197,"MAP_OLDALE_TOWN":10,"MAP_OLDALE_TOWN_HOUSE1":512,"MAP_OLDALE_TOWN_HOUSE2":513,"MAP_OLDALE_TOWN_MART":516,"MAP_OLDALE_TOWN_POKEMON_CENTER_1F":514,"MAP_OLDALE_TOWN_POKEMON_CENTER_2F":515,"MAP_PACIFIDLOG_TOWN":15,"MAP_PACIFIDLOG_TOWN_HOUSE1":1794,"MAP_PACIFIDLOG_TOWN_HOUSE2":1795,"MAP_PACIFIDLOG_TOWN_HOUSE3":1796,"MAP_PACIFIDLOG_TOWN_HOUSE4":1797,"MAP_PACIFIDLOG_TOWN_HOUSE5":1798,"MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F":1792,"MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F":1793,"MAP_PETALBURG_CITY":0,"MAP_PETALBURG_CITY_GYM":2049,"MAP_PETALBURG_CITY_HOUSE1":2050,"MAP_PETALBURG_CITY_HOUSE2":2051,"MAP_PETALBURG_CITY_MART":2054,"MAP_PETALBURG_CITY_POKEMON_CENTER_1F":2052,"MAP_PETALBURG_CITY_POKEMON_CENTER_2F":2053,"MAP_PETALBURG_CITY_WALLYS_HOUSE":2048,"MAP_PETALBURG_WOODS":6155,"MAP_RECORD_CORNER":6426,"MAP_ROUTE101":16,"MAP_ROUTE102":17,"MAP_ROUTE103":18,"MAP_ROUTE104":19,"MAP_ROUTE104_MR_BRINEYS_HOUSE":4352,"MAP_ROUTE104_PRETTY_PETAL_FLOWER_SHOP":4353,"MAP_ROUTE104_PROTOTYPE":6912,"MAP_ROUTE104_PROTOTYPE_PRETTY_PETAL_FLOWER_SHOP":6913,"MAP_ROUTE105":20,"MAP_ROUTE106":21,"MAP_ROUTE107":22,"MAP_ROUTE108":23,"MAP_ROUTE109":24,"MAP_ROUTE109_SEASHORE_HOUSE":7168,"MAP_ROUTE110":25,"MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE":7435,"MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE":7436,"MAP_ROUTE110_TRICK_HOUSE_CORRIDOR":7426,"MAP_ROUTE110_TRICK_HOUSE_END":7425,"MAP_ROUTE110_TRICK_HOUSE_ENTRANCE":7424,"MAP_ROUTE110_TRICK_HOUSE_PUZZLE1":7427,"MAP_ROUTE110_TRICK_HOUSE_PUZZLE2":7428,"MAP_ROUTE110_TRICK_HOUSE_PUZZLE3":7429,"MAP_ROUTE110_TRICK_HOUSE_PUZZLE4":7430,"MAP_ROUTE110_TRICK_HOUSE_PUZZLE5":7431,"MAP_ROUTE110_TRICK_HOUSE_PUZZLE6":7432,"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7":7433,"MAP_ROUTE110_TRICK_HOUSE_PUZZLE8":7434,"MAP_ROUTE111":26,"MAP_ROUTE111_OLD_LADYS_REST_STOP":4609,"MAP_ROUTE111_WINSTRATE_FAMILYS_HOUSE":4608,"MAP_ROUTE112":27,"MAP_ROUTE112_CABLE_CAR_STATION":4864,"MAP_ROUTE113":28,"MAP_ROUTE113_GLASS_WORKSHOP":7680,"MAP_ROUTE114":29,"MAP_ROUTE114_FOSSIL_MANIACS_HOUSE":5120,"MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL":5121,"MAP_ROUTE114_LANETTES_HOUSE":5122,"MAP_ROUTE115":30,"MAP_ROUTE116":31,"MAP_ROUTE116_TUNNELERS_REST_HOUSE":5376,"MAP_ROUTE117":32,"MAP_ROUTE117_POKEMON_DAY_CARE":5632,"MAP_ROUTE118":33,"MAP_ROUTE119":34,"MAP_ROUTE119_HOUSE":8194,"MAP_ROUTE119_WEATHER_INSTITUTE_1F":8192,"MAP_ROUTE119_WEATHER_INSTITUTE_2F":8193,"MAP_ROUTE120":35,"MAP_ROUTE121":36,"MAP_ROUTE121_SAFARI_ZONE_ENTRANCE":5888,"MAP_ROUTE122":37,"MAP_ROUTE123":38,"MAP_ROUTE123_BERRY_MASTERS_HOUSE":7936,"MAP_ROUTE124":39,"MAP_ROUTE124_DIVING_TREASURE_HUNTERS_HOUSE":8448,"MAP_ROUTE125":40,"MAP_ROUTE126":41,"MAP_ROUTE127":42,"MAP_ROUTE128":43,"MAP_ROUTE129":44,"MAP_ROUTE130":45,"MAP_ROUTE131":46,"MAP_ROUTE132":47,"MAP_ROUTE133":48,"MAP_ROUTE134":49,"MAP_RUSTBORO_CITY":3,"MAP_RUSTBORO_CITY_CUTTERS_HOUSE":2827,"MAP_RUSTBORO_CITY_DEVON_CORP_1F":2816,"MAP_RUSTBORO_CITY_DEVON_CORP_2F":2817,"MAP_RUSTBORO_CITY_DEVON_CORP_3F":2818,"MAP_RUSTBORO_CITY_FLAT1_1F":2824,"MAP_RUSTBORO_CITY_FLAT1_2F":2825,"MAP_RUSTBORO_CITY_FLAT2_1F":2829,"MAP_RUSTBORO_CITY_FLAT2_2F":2830,"MAP_RUSTBORO_CITY_FLAT2_3F":2831,"MAP_RUSTBORO_CITY_GYM":2819,"MAP_RUSTBORO_CITY_HOUSE1":2826,"MAP_RUSTBORO_CITY_HOUSE2":2828,"MAP_RUSTBORO_CITY_HOUSE3":2832,"MAP_RUSTBORO_CITY_MART":2823,"MAP_RUSTBORO_CITY_POKEMON_CENTER_1F":2821,"MAP_RUSTBORO_CITY_POKEMON_CENTER_2F":2822,"MAP_RUSTBORO_CITY_POKEMON_SCHOOL":2820,"MAP_RUSTURF_TUNNEL":6148,"MAP_SAFARI_ZONE_NORTH":6657,"MAP_SAFARI_ZONE_NORTHEAST":6668,"MAP_SAFARI_ZONE_NORTHWEST":6656,"MAP_SAFARI_ZONE_REST_HOUSE":6667,"MAP_SAFARI_ZONE_SOUTH":6659,"MAP_SAFARI_ZONE_SOUTHEAST":6669,"MAP_SAFARI_ZONE_SOUTHWEST":6658,"MAP_SCORCHED_SLAB":6217,"MAP_SEAFLOOR_CAVERN_ENTRANCE":6171,"MAP_SEAFLOOR_CAVERN_ROOM1":6172,"MAP_SEAFLOOR_CAVERN_ROOM2":6173,"MAP_SEAFLOOR_CAVERN_ROOM3":6174,"MAP_SEAFLOOR_CAVERN_ROOM4":6175,"MAP_SEAFLOOR_CAVERN_ROOM5":6176,"MAP_SEAFLOOR_CAVERN_ROOM6":6177,"MAP_SEAFLOOR_CAVERN_ROOM7":6178,"MAP_SEAFLOOR_CAVERN_ROOM8":6179,"MAP_SEAFLOOR_CAVERN_ROOM9":6180,"MAP_SEALED_CHAMBER_INNER_ROOM":6216,"MAP_SEALED_CHAMBER_OUTER_ROOM":6215,"MAP_SECRET_BASE_BLUE_CAVE1":6402,"MAP_SECRET_BASE_BLUE_CAVE2":6408,"MAP_SECRET_BASE_BLUE_CAVE3":6414,"MAP_SECRET_BASE_BLUE_CAVE4":6420,"MAP_SECRET_BASE_BROWN_CAVE1":6401,"MAP_SECRET_BASE_BROWN_CAVE2":6407,"MAP_SECRET_BASE_BROWN_CAVE3":6413,"MAP_SECRET_BASE_BROWN_CAVE4":6419,"MAP_SECRET_BASE_RED_CAVE1":6400,"MAP_SECRET_BASE_RED_CAVE2":6406,"MAP_SECRET_BASE_RED_CAVE3":6412,"MAP_SECRET_BASE_RED_CAVE4":6418,"MAP_SECRET_BASE_SHRUB1":6405,"MAP_SECRET_BASE_SHRUB2":6411,"MAP_SECRET_BASE_SHRUB3":6417,"MAP_SECRET_BASE_SHRUB4":6423,"MAP_SECRET_BASE_TREE1":6404,"MAP_SECRET_BASE_TREE2":6410,"MAP_SECRET_BASE_TREE3":6416,"MAP_SECRET_BASE_TREE4":6422,"MAP_SECRET_BASE_YELLOW_CAVE1":6403,"MAP_SECRET_BASE_YELLOW_CAVE2":6409,"MAP_SECRET_BASE_YELLOW_CAVE3":6415,"MAP_SECRET_BASE_YELLOW_CAVE4":6421,"MAP_SHOAL_CAVE_HIGH_TIDE_ENTRANCE_ROOM":6194,"MAP_SHOAL_CAVE_HIGH_TIDE_INNER_ROOM":6195,"MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM":6190,"MAP_SHOAL_CAVE_LOW_TIDE_ICE_ROOM":6227,"MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM":6191,"MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM":6193,"MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM":6192,"MAP_SKY_PILLAR_1F":6223,"MAP_SKY_PILLAR_2F":6224,"MAP_SKY_PILLAR_3F":6225,"MAP_SKY_PILLAR_4F":6226,"MAP_SKY_PILLAR_5F":6228,"MAP_SKY_PILLAR_ENTRANCE":6221,"MAP_SKY_PILLAR_OUTSIDE":6222,"MAP_SKY_PILLAR_TOP":6229,"MAP_SLATEPORT_CITY":1,"MAP_SLATEPORT_CITY_BATTLE_TENT_BATTLE_ROOM":2308,"MAP_SLATEPORT_CITY_BATTLE_TENT_CORRIDOR":2307,"MAP_SLATEPORT_CITY_BATTLE_TENT_LOBBY":2306,"MAP_SLATEPORT_CITY_HARBOR":2313,"MAP_SLATEPORT_CITY_HOUSE":2314,"MAP_SLATEPORT_CITY_MART":2317,"MAP_SLATEPORT_CITY_NAME_RATERS_HOUSE":2309,"MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F":2311,"MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_2F":2312,"MAP_SLATEPORT_CITY_POKEMON_CENTER_1F":2315,"MAP_SLATEPORT_CITY_POKEMON_CENTER_2F":2316,"MAP_SLATEPORT_CITY_POKEMON_FAN_CLUB":2310,"MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F":2304,"MAP_SLATEPORT_CITY_STERNS_SHIPYARD_2F":2305,"MAP_SOOTOPOLIS_CITY":7,"MAP_SOOTOPOLIS_CITY_GYM_1F":3840,"MAP_SOOTOPOLIS_CITY_GYM_B1F":3841,"MAP_SOOTOPOLIS_CITY_HOUSE1":3845,"MAP_SOOTOPOLIS_CITY_HOUSE2":3846,"MAP_SOOTOPOLIS_CITY_HOUSE3":3847,"MAP_SOOTOPOLIS_CITY_HOUSE4":3848,"MAP_SOOTOPOLIS_CITY_HOUSE5":3849,"MAP_SOOTOPOLIS_CITY_HOUSE6":3850,"MAP_SOOTOPOLIS_CITY_HOUSE7":3851,"MAP_SOOTOPOLIS_CITY_LOTAD_AND_SEEDOT_HOUSE":3852,"MAP_SOOTOPOLIS_CITY_MART":3844,"MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F":3853,"MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_B1F":3854,"MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F":3842,"MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F":3843,"MAP_SOUTHERN_ISLAND_EXTERIOR":6665,"MAP_SOUTHERN_ISLAND_INTERIOR":6666,"MAP_SS_TIDAL_CORRIDOR":6441,"MAP_SS_TIDAL_LOWER_DECK":6442,"MAP_SS_TIDAL_ROOMS":6443,"MAP_TERRA_CAVE_END":6249,"MAP_TERRA_CAVE_ENTRANCE":6248,"MAP_TRADE_CENTER":6425,"MAP_TRAINER_HILL_1F":6717,"MAP_TRAINER_HILL_2F":6718,"MAP_TRAINER_HILL_3F":6719,"MAP_TRAINER_HILL_4F":6720,"MAP_TRAINER_HILL_ELEVATOR":6744,"MAP_TRAINER_HILL_ENTRANCE":6716,"MAP_TRAINER_HILL_ROOF":6721,"MAP_UNDERWATER_MARINE_CAVE":6245,"MAP_UNDERWATER_ROUTE105":55,"MAP_UNDERWATER_ROUTE124":50,"MAP_UNDERWATER_ROUTE125":56,"MAP_UNDERWATER_ROUTE126":51,"MAP_UNDERWATER_ROUTE127":52,"MAP_UNDERWATER_ROUTE128":53,"MAP_UNDERWATER_ROUTE129":54,"MAP_UNDERWATER_ROUTE134":6213,"MAP_UNDERWATER_SEAFLOOR_CAVERN":6170,"MAP_UNDERWATER_SEALED_CHAMBER":6214,"MAP_UNDERWATER_SOOTOPOLIS_CITY":6149,"MAP_UNION_ROOM":6460,"MAP_UNUSED_CONTEST_HALL1":6429,"MAP_UNUSED_CONTEST_HALL2":6430,"MAP_UNUSED_CONTEST_HALL3":6431,"MAP_UNUSED_CONTEST_HALL4":6432,"MAP_UNUSED_CONTEST_HALL5":6433,"MAP_UNUSED_CONTEST_HALL6":6434,"MAP_VERDANTURF_TOWN":14,"MAP_VERDANTURF_TOWN_BATTLE_TENT_BATTLE_ROOM":1538,"MAP_VERDANTURF_TOWN_BATTLE_TENT_CORRIDOR":1537,"MAP_VERDANTURF_TOWN_BATTLE_TENT_LOBBY":1536,"MAP_VERDANTURF_TOWN_FRIENDSHIP_RATERS_HOUSE":1543,"MAP_VERDANTURF_TOWN_HOUSE":1544,"MAP_VERDANTURF_TOWN_MART":1539,"MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F":1540,"MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F":1541,"MAP_VERDANTURF_TOWN_WANDAS_HOUSE":1542,"MAP_VICTORY_ROAD_1F":6187,"MAP_VICTORY_ROAD_B1F":6188,"MAP_VICTORY_ROAD_B2F":6189,"MAX_BAG_ITEM_CAPACITY":99,"MAX_BERRY_CAPACITY":999,"MAX_BERRY_INDEX":178,"MAX_ITEM_DIGITS":3,"MAX_PC_ITEM_CAPACITY":999,"MAX_TRAINERS_COUNT":864,"MOVES_COUNT":355,"MOVE_ABSORB":71,"MOVE_ACID":51,"MOVE_ACID_ARMOR":151,"MOVE_AERIAL_ACE":332,"MOVE_AEROBLAST":177,"MOVE_AGILITY":97,"MOVE_AIR_CUTTER":314,"MOVE_AMNESIA":133,"MOVE_ANCIENT_POWER":246,"MOVE_ARM_THRUST":292,"MOVE_AROMATHERAPY":312,"MOVE_ASSIST":274,"MOVE_ASTONISH":310,"MOVE_ATTRACT":213,"MOVE_AURORA_BEAM":62,"MOVE_BARRAGE":140,"MOVE_BARRIER":112,"MOVE_BATON_PASS":226,"MOVE_BEAT_UP":251,"MOVE_BELLY_DRUM":187,"MOVE_BIDE":117,"MOVE_BIND":20,"MOVE_BITE":44,"MOVE_BLAST_BURN":307,"MOVE_BLAZE_KICK":299,"MOVE_BLIZZARD":59,"MOVE_BLOCK":335,"MOVE_BODY_SLAM":34,"MOVE_BONEMERANG":155,"MOVE_BONE_CLUB":125,"MOVE_BONE_RUSH":198,"MOVE_BOUNCE":340,"MOVE_BRICK_BREAK":280,"MOVE_BUBBLE":145,"MOVE_BUBBLE_BEAM":61,"MOVE_BULK_UP":339,"MOVE_BULLET_SEED":331,"MOVE_CALM_MIND":347,"MOVE_CAMOUFLAGE":293,"MOVE_CHARGE":268,"MOVE_CHARM":204,"MOVE_CLAMP":128,"MOVE_COMET_PUNCH":4,"MOVE_CONFUSE_RAY":109,"MOVE_CONFUSION":93,"MOVE_CONSTRICT":132,"MOVE_CONVERSION":160,"MOVE_CONVERSION_2":176,"MOVE_COSMIC_POWER":322,"MOVE_COTTON_SPORE":178,"MOVE_COUNTER":68,"MOVE_COVET":343,"MOVE_CRABHAMMER":152,"MOVE_CROSS_CHOP":238,"MOVE_CRUNCH":242,"MOVE_CRUSH_CLAW":306,"MOVE_CURSE":174,"MOVE_CUT":15,"MOVE_DEFENSE_CURL":111,"MOVE_DESTINY_BOND":194,"MOVE_DETECT":197,"MOVE_DIG":91,"MOVE_DISABLE":50,"MOVE_DIVE":291,"MOVE_DIZZY_PUNCH":146,"MOVE_DOOM_DESIRE":353,"MOVE_DOUBLE_EDGE":38,"MOVE_DOUBLE_KICK":24,"MOVE_DOUBLE_SLAP":3,"MOVE_DOUBLE_TEAM":104,"MOVE_DRAGON_BREATH":225,"MOVE_DRAGON_CLAW":337,"MOVE_DRAGON_DANCE":349,"MOVE_DRAGON_RAGE":82,"MOVE_DREAM_EATER":138,"MOVE_DRILL_PECK":65,"MOVE_DYNAMIC_PUNCH":223,"MOVE_EARTHQUAKE":89,"MOVE_EGG_BOMB":121,"MOVE_EMBER":52,"MOVE_ENCORE":227,"MOVE_ENDEAVOR":283,"MOVE_ENDURE":203,"MOVE_ERUPTION":284,"MOVE_EXPLOSION":153,"MOVE_EXTRASENSORY":326,"MOVE_EXTREME_SPEED":245,"MOVE_FACADE":263,"MOVE_FAINT_ATTACK":185,"MOVE_FAKE_OUT":252,"MOVE_FAKE_TEARS":313,"MOVE_FALSE_SWIPE":206,"MOVE_FEATHER_DANCE":297,"MOVE_FIRE_BLAST":126,"MOVE_FIRE_PUNCH":7,"MOVE_FIRE_SPIN":83,"MOVE_FISSURE":90,"MOVE_FLAIL":175,"MOVE_FLAMETHROWER":53,"MOVE_FLAME_WHEEL":172,"MOVE_FLASH":148,"MOVE_FLATTER":260,"MOVE_FLY":19,"MOVE_FOCUS_ENERGY":116,"MOVE_FOCUS_PUNCH":264,"MOVE_FOLLOW_ME":266,"MOVE_FORESIGHT":193,"MOVE_FRENZY_PLANT":338,"MOVE_FRUSTRATION":218,"MOVE_FURY_ATTACK":31,"MOVE_FURY_CUTTER":210,"MOVE_FURY_SWIPES":154,"MOVE_FUTURE_SIGHT":248,"MOVE_GIGA_DRAIN":202,"MOVE_GLARE":137,"MOVE_GRASS_WHISTLE":320,"MOVE_GROWL":45,"MOVE_GROWTH":74,"MOVE_GRUDGE":288,"MOVE_GUILLOTINE":12,"MOVE_GUST":16,"MOVE_HAIL":258,"MOVE_HARDEN":106,"MOVE_HAZE":114,"MOVE_HEADBUTT":29,"MOVE_HEAL_BELL":215,"MOVE_HEAT_WAVE":257,"MOVE_HELPING_HAND":270,"MOVE_HIDDEN_POWER":237,"MOVE_HI_JUMP_KICK":136,"MOVE_HORN_ATTACK":30,"MOVE_HORN_DRILL":32,"MOVE_HOWL":336,"MOVE_HYDRO_CANNON":308,"MOVE_HYDRO_PUMP":56,"MOVE_HYPER_BEAM":63,"MOVE_HYPER_FANG":158,"MOVE_HYPER_VOICE":304,"MOVE_HYPNOSIS":95,"MOVE_ICE_BALL":301,"MOVE_ICE_BEAM":58,"MOVE_ICE_PUNCH":8,"MOVE_ICICLE_SPEAR":333,"MOVE_ICY_WIND":196,"MOVE_IMPRISON":286,"MOVE_INGRAIN":275,"MOVE_IRON_DEFENSE":334,"MOVE_IRON_TAIL":231,"MOVE_JUMP_KICK":26,"MOVE_KARATE_CHOP":2,"MOVE_KINESIS":134,"MOVE_KNOCK_OFF":282,"MOVE_LEAF_BLADE":348,"MOVE_LEECH_LIFE":141,"MOVE_LEECH_SEED":73,"MOVE_LEER":43,"MOVE_LICK":122,"MOVE_LIGHT_SCREEN":113,"MOVE_LOCK_ON":199,"MOVE_LOVELY_KISS":142,"MOVE_LOW_KICK":67,"MOVE_LUSTER_PURGE":295,"MOVE_MACH_PUNCH":183,"MOVE_MAGICAL_LEAF":345,"MOVE_MAGIC_COAT":277,"MOVE_MAGNITUDE":222,"MOVE_MEAN_LOOK":212,"MOVE_MEDITATE":96,"MOVE_MEGAHORN":224,"MOVE_MEGA_DRAIN":72,"MOVE_MEGA_KICK":25,"MOVE_MEGA_PUNCH":5,"MOVE_MEMENTO":262,"MOVE_METAL_CLAW":232,"MOVE_METAL_SOUND":319,"MOVE_METEOR_MASH":309,"MOVE_METRONOME":118,"MOVE_MILK_DRINK":208,"MOVE_MIMIC":102,"MOVE_MIND_READER":170,"MOVE_MINIMIZE":107,"MOVE_MIRROR_COAT":243,"MOVE_MIRROR_MOVE":119,"MOVE_MIST":54,"MOVE_MIST_BALL":296,"MOVE_MOONLIGHT":236,"MOVE_MORNING_SUN":234,"MOVE_MUDDY_WATER":330,"MOVE_MUD_SHOT":341,"MOVE_MUD_SLAP":189,"MOVE_MUD_SPORT":300,"MOVE_NATURE_POWER":267,"MOVE_NEEDLE_ARM":302,"MOVE_NIGHTMARE":171,"MOVE_NIGHT_SHADE":101,"MOVE_NONE":0,"MOVE_OCTAZOOKA":190,"MOVE_ODOR_SLEUTH":316,"MOVE_OUTRAGE":200,"MOVE_OVERHEAT":315,"MOVE_PAIN_SPLIT":220,"MOVE_PAY_DAY":6,"MOVE_PECK":64,"MOVE_PERISH_SONG":195,"MOVE_PETAL_DANCE":80,"MOVE_PIN_MISSILE":42,"MOVE_POISON_FANG":305,"MOVE_POISON_GAS":139,"MOVE_POISON_POWDER":77,"MOVE_POISON_STING":40,"MOVE_POISON_TAIL":342,"MOVE_POUND":1,"MOVE_POWDER_SNOW":181,"MOVE_PRESENT":217,"MOVE_PROTECT":182,"MOVE_PSYBEAM":60,"MOVE_PSYCHIC":94,"MOVE_PSYCHO_BOOST":354,"MOVE_PSYCH_UP":244,"MOVE_PSYWAVE":149,"MOVE_PURSUIT":228,"MOVE_QUICK_ATTACK":98,"MOVE_RAGE":99,"MOVE_RAIN_DANCE":240,"MOVE_RAPID_SPIN":229,"MOVE_RAZOR_LEAF":75,"MOVE_RAZOR_WIND":13,"MOVE_RECOVER":105,"MOVE_RECYCLE":278,"MOVE_REFLECT":115,"MOVE_REFRESH":287,"MOVE_REST":156,"MOVE_RETURN":216,"MOVE_REVENGE":279,"MOVE_REVERSAL":179,"MOVE_ROAR":46,"MOVE_ROCK_BLAST":350,"MOVE_ROCK_SLIDE":157,"MOVE_ROCK_SMASH":249,"MOVE_ROCK_THROW":88,"MOVE_ROCK_TOMB":317,"MOVE_ROLE_PLAY":272,"MOVE_ROLLING_KICK":27,"MOVE_ROLLOUT":205,"MOVE_SACRED_FIRE":221,"MOVE_SAFEGUARD":219,"MOVE_SANDSTORM":201,"MOVE_SAND_ATTACK":28,"MOVE_SAND_TOMB":328,"MOVE_SCARY_FACE":184,"MOVE_SCRATCH":10,"MOVE_SCREECH":103,"MOVE_SECRET_POWER":290,"MOVE_SEISMIC_TOSS":69,"MOVE_SELF_DESTRUCT":120,"MOVE_SHADOW_BALL":247,"MOVE_SHADOW_PUNCH":325,"MOVE_SHARPEN":159,"MOVE_SHEER_COLD":329,"MOVE_SHOCK_WAVE":351,"MOVE_SIGNAL_BEAM":324,"MOVE_SILVER_WIND":318,"MOVE_SING":47,"MOVE_SKETCH":166,"MOVE_SKILL_SWAP":285,"MOVE_SKULL_BASH":130,"MOVE_SKY_ATTACK":143,"MOVE_SKY_UPPERCUT":327,"MOVE_SLACK_OFF":303,"MOVE_SLAM":21,"MOVE_SLASH":163,"MOVE_SLEEP_POWDER":79,"MOVE_SLEEP_TALK":214,"MOVE_SLUDGE":124,"MOVE_SLUDGE_BOMB":188,"MOVE_SMELLING_SALT":265,"MOVE_SMOG":123,"MOVE_SMOKESCREEN":108,"MOVE_SNATCH":289,"MOVE_SNORE":173,"MOVE_SOFT_BOILED":135,"MOVE_SOLAR_BEAM":76,"MOVE_SONIC_BOOM":49,"MOVE_SPARK":209,"MOVE_SPIDER_WEB":169,"MOVE_SPIKES":191,"MOVE_SPIKE_CANNON":131,"MOVE_SPITE":180,"MOVE_SPIT_UP":255,"MOVE_SPLASH":150,"MOVE_SPORE":147,"MOVE_STEEL_WING":211,"MOVE_STOCKPILE":254,"MOVE_STOMP":23,"MOVE_STRENGTH":70,"MOVE_STRING_SHOT":81,"MOVE_STRUGGLE":165,"MOVE_STUN_SPORE":78,"MOVE_SUBMISSION":66,"MOVE_SUBSTITUTE":164,"MOVE_SUNNY_DAY":241,"MOVE_SUPERPOWER":276,"MOVE_SUPERSONIC":48,"MOVE_SUPER_FANG":162,"MOVE_SURF":57,"MOVE_SWAGGER":207,"MOVE_SWALLOW":256,"MOVE_SWEET_KISS":186,"MOVE_SWEET_SCENT":230,"MOVE_SWIFT":129,"MOVE_SWORDS_DANCE":14,"MOVE_SYNTHESIS":235,"MOVE_TACKLE":33,"MOVE_TAIL_GLOW":294,"MOVE_TAIL_WHIP":39,"MOVE_TAKE_DOWN":36,"MOVE_TAUNT":269,"MOVE_TEETER_DANCE":298,"MOVE_TELEPORT":100,"MOVE_THIEF":168,"MOVE_THRASH":37,"MOVE_THUNDER":87,"MOVE_THUNDERBOLT":85,"MOVE_THUNDER_PUNCH":9,"MOVE_THUNDER_SHOCK":84,"MOVE_THUNDER_WAVE":86,"MOVE_TICKLE":321,"MOVE_TORMENT":259,"MOVE_TOXIC":92,"MOVE_TRANSFORM":144,"MOVE_TRICK":271,"MOVE_TRIPLE_KICK":167,"MOVE_TRI_ATTACK":161,"MOVE_TWINEEDLE":41,"MOVE_TWISTER":239,"MOVE_UNAVAILABLE":65535,"MOVE_UPROAR":253,"MOVE_VICE_GRIP":11,"MOVE_VINE_WHIP":22,"MOVE_VITAL_THROW":233,"MOVE_VOLT_TACKLE":344,"MOVE_WATERFALL":127,"MOVE_WATER_GUN":55,"MOVE_WATER_PULSE":352,"MOVE_WATER_SPORT":346,"MOVE_WATER_SPOUT":323,"MOVE_WEATHER_BALL":311,"MOVE_WHIRLPOOL":250,"MOVE_WHIRLWIND":18,"MOVE_WILL_O_WISP":261,"MOVE_WING_ATTACK":17,"MOVE_WISH":273,"MOVE_WITHDRAW":110,"MOVE_WRAP":35,"MOVE_YAWN":281,"MOVE_ZAP_CANNON":192,"MUS_ABANDONED_SHIP":381,"MUS_ABNORMAL_WEATHER":443,"MUS_AQUA_MAGMA_HIDEOUT":430,"MUS_AWAKEN_LEGEND":388,"MUS_BIRCH_LAB":383,"MUS_B_ARENA":458,"MUS_B_DOME":467,"MUS_B_DOME_LOBBY":473,"MUS_B_FACTORY":469,"MUS_B_FRONTIER":457,"MUS_B_PALACE":463,"MUS_B_PIKE":468,"MUS_B_PYRAMID":461,"MUS_B_PYRAMID_TOP":462,"MUS_B_TOWER":465,"MUS_B_TOWER_RS":384,"MUS_CABLE_CAR":425,"MUS_CAUGHT":352,"MUS_CAVE_OF_ORIGIN":386,"MUS_CONTEST":440,"MUS_CONTEST_LOBBY":452,"MUS_CONTEST_RESULTS":446,"MUS_CONTEST_WINNER":439,"MUS_CREDITS":455,"MUS_CYCLING":403,"MUS_C_COMM_CENTER":356,"MUS_C_VS_LEGEND_BEAST":358,"MUS_DESERT":409,"MUS_DEWFORD":427,"MUS_DUMMY":0,"MUS_ENCOUNTER_AQUA":419,"MUS_ENCOUNTER_BRENDAN":421,"MUS_ENCOUNTER_CHAMPION":454,"MUS_ENCOUNTER_COOL":417,"MUS_ENCOUNTER_ELITE_FOUR":450,"MUS_ENCOUNTER_FEMALE":407,"MUS_ENCOUNTER_GIRL":379,"MUS_ENCOUNTER_HIKER":451,"MUS_ENCOUNTER_INTENSE":416,"MUS_ENCOUNTER_INTERVIEWER":453,"MUS_ENCOUNTER_MAGMA":441,"MUS_ENCOUNTER_MALE":380,"MUS_ENCOUNTER_MAY":415,"MUS_ENCOUNTER_RICH":397,"MUS_ENCOUNTER_SUSPICIOUS":423,"MUS_ENCOUNTER_SWIMMER":385,"MUS_ENCOUNTER_TWINS":449,"MUS_END":456,"MUS_EVER_GRANDE":422,"MUS_EVOLUTION":377,"MUS_EVOLUTION_INTRO":376,"MUS_EVOLVED":371,"MUS_FALLARBOR":437,"MUS_FOLLOW_ME":420,"MUS_FORTREE":382,"MUS_GAME_CORNER":426,"MUS_GSC_PEWTER":357,"MUS_GSC_ROUTE38":351,"MUS_GYM":364,"MUS_HALL_OF_FAME":436,"MUS_HALL_OF_FAME_ROOM":447,"MUS_HEAL":368,"MUS_HELP":410,"MUS_INTRO":414,"MUS_INTRO_BATTLE":442,"MUS_LEVEL_UP":367,"MUS_LILYCOVE":408,"MUS_LILYCOVE_MUSEUM":373,"MUS_LINK_CONTEST_P1":393,"MUS_LINK_CONTEST_P2":394,"MUS_LINK_CONTEST_P3":395,"MUS_LINK_CONTEST_P4":396,"MUS_LITTLEROOT":405,"MUS_LITTLEROOT_TEST":350,"MUS_MOVE_DELETED":378,"MUS_MT_CHIMNEY":406,"MUS_MT_PYRE":432,"MUS_MT_PYRE_EXTERIOR":434,"MUS_NONE":65535,"MUS_OBTAIN_BADGE":369,"MUS_OBTAIN_BERRY":387,"MUS_OBTAIN_B_POINTS":459,"MUS_OBTAIN_ITEM":370,"MUS_OBTAIN_SYMBOL":466,"MUS_OBTAIN_TMHM":372,"MUS_OCEANIC_MUSEUM":375,"MUS_OLDALE":363,"MUS_PETALBURG":362,"MUS_PETALBURG_WOODS":366,"MUS_POKE_CENTER":400,"MUS_POKE_MART":404,"MUS_RAYQUAZA_APPEARS":464,"MUS_REGISTER_MATCH_CALL":460,"MUS_RG_BERRY_PICK":542,"MUS_RG_CAUGHT":534,"MUS_RG_CAUGHT_INTRO":531,"MUS_RG_CELADON":521,"MUS_RG_CINNABAR":491,"MUS_RG_CREDITS":502,"MUS_RG_CYCLING":494,"MUS_RG_DEX_RATING":529,"MUS_RG_ENCOUNTER_BOY":497,"MUS_RG_ENCOUNTER_DEOXYS":555,"MUS_RG_ENCOUNTER_GIRL":496,"MUS_RG_ENCOUNTER_GYM_LEADER":554,"MUS_RG_ENCOUNTER_RIVAL":527,"MUS_RG_ENCOUNTER_ROCKET":495,"MUS_RG_FOLLOW_ME":484,"MUS_RG_FUCHSIA":520,"MUS_RG_GAME_CORNER":485,"MUS_RG_GAME_FREAK":533,"MUS_RG_GYM":487,"MUS_RG_HALL_OF_FAME":498,"MUS_RG_HEAL":493,"MUS_RG_INTRO_FIGHT":489,"MUS_RG_JIGGLYPUFF":488,"MUS_RG_LAVENDER":492,"MUS_RG_MT_MOON":500,"MUS_RG_MYSTERY_GIFT":541,"MUS_RG_NET_CENTER":540,"MUS_RG_NEW_GAME_EXIT":537,"MUS_RG_NEW_GAME_INSTRUCT":535,"MUS_RG_NEW_GAME_INTRO":536,"MUS_RG_OAK":514,"MUS_RG_OAK_LAB":513,"MUS_RG_OBTAIN_KEY_ITEM":530,"MUS_RG_PALLET":512,"MUS_RG_PEWTER":526,"MUS_RG_PHOTO":532,"MUS_RG_POKE_CENTER":515,"MUS_RG_POKE_FLUTE":550,"MUS_RG_POKE_JUMP":538,"MUS_RG_POKE_MANSION":501,"MUS_RG_POKE_TOWER":518,"MUS_RG_RIVAL_EXIT":528,"MUS_RG_ROCKET_HIDEOUT":486,"MUS_RG_ROUTE1":503,"MUS_RG_ROUTE11":506,"MUS_RG_ROUTE24":504,"MUS_RG_ROUTE3":505,"MUS_RG_SEVII_123":547,"MUS_RG_SEVII_45":548,"MUS_RG_SEVII_67":549,"MUS_RG_SEVII_CAVE":543,"MUS_RG_SEVII_DUNGEON":546,"MUS_RG_SEVII_ROUTE":545,"MUS_RG_SILPH":519,"MUS_RG_SLOW_PALLET":557,"MUS_RG_SS_ANNE":516,"MUS_RG_SURF":517,"MUS_RG_TEACHY_TV_MENU":558,"MUS_RG_TEACHY_TV_SHOW":544,"MUS_RG_TITLE":490,"MUS_RG_TRAINER_TOWER":556,"MUS_RG_UNION_ROOM":539,"MUS_RG_VERMILLION":525,"MUS_RG_VICTORY_GYM_LEADER":524,"MUS_RG_VICTORY_ROAD":507,"MUS_RG_VICTORY_TRAINER":522,"MUS_RG_VICTORY_WILD":523,"MUS_RG_VIRIDIAN_FOREST":499,"MUS_RG_VS_CHAMPION":511,"MUS_RG_VS_DEOXYS":551,"MUS_RG_VS_GYM_LEADER":508,"MUS_RG_VS_LEGEND":553,"MUS_RG_VS_MEWTWO":552,"MUS_RG_VS_TRAINER":509,"MUS_RG_VS_WILD":510,"MUS_ROULETTE":392,"MUS_ROUTE101":359,"MUS_ROUTE104":401,"MUS_ROUTE110":360,"MUS_ROUTE113":418,"MUS_ROUTE118":32767,"MUS_ROUTE119":402,"MUS_ROUTE120":361,"MUS_ROUTE122":374,"MUS_RUSTBORO":399,"MUS_SAFARI_ZONE":428,"MUS_SAILING":431,"MUS_SCHOOL":435,"MUS_SEALED_CHAMBER":438,"MUS_SLATEPORT":433,"MUS_SLOTS_JACKPOT":389,"MUS_SLOTS_WIN":390,"MUS_SOOTOPOLIS":445,"MUS_SURF":365,"MUS_TITLE":413,"MUS_TOO_BAD":391,"MUS_TRICK_HOUSE":448,"MUS_UNDERWATER":411,"MUS_VERDANTURF":398,"MUS_VICTORY_AQUA_MAGMA":424,"MUS_VICTORY_GYM_LEADER":354,"MUS_VICTORY_LEAGUE":355,"MUS_VICTORY_ROAD":429,"MUS_VICTORY_TRAINER":412,"MUS_VICTORY_WILD":353,"MUS_VS_AQUA_MAGMA":475,"MUS_VS_AQUA_MAGMA_LEADER":483,"MUS_VS_CHAMPION":478,"MUS_VS_ELITE_FOUR":482,"MUS_VS_FRONTIER_BRAIN":471,"MUS_VS_GYM_LEADER":477,"MUS_VS_KYOGRE_GROUDON":480,"MUS_VS_MEW":472,"MUS_VS_RAYQUAZA":470,"MUS_VS_REGI":479,"MUS_VS_RIVAL":481,"MUS_VS_TRAINER":476,"MUS_VS_WILD":474,"MUS_WEATHER_GROUDON":444,"NUM_BADGES":8,"NUM_BERRY_MASTER_BERRIES":10,"NUM_BERRY_MASTER_BERRIES_SKIPPED":20,"NUM_BERRY_MASTER_WIFE_BERRIES":10,"NUM_DAILY_FLAGS":64,"NUM_HIDDEN_MACHINES":8,"NUM_KIRI_BERRIES":10,"NUM_KIRI_BERRIES_SKIPPED":20,"NUM_ROUTE_114_MAN_BERRIES":5,"NUM_ROUTE_114_MAN_BERRIES_SKIPPED":15,"NUM_SPECIAL_FLAGS":128,"NUM_SPECIES":412,"NUM_TECHNICAL_MACHINES":50,"NUM_TEMP_FLAGS":32,"NUM_WATER_STAGES":4,"NUM_WONDER_CARD_FLAGS":20,"OLD_ROD":0,"PH_CHOICE_BLEND":589,"PH_CHOICE_HELD":590,"PH_CHOICE_SOLO":591,"PH_CLOTH_BLEND":565,"PH_CLOTH_HELD":566,"PH_CLOTH_SOLO":567,"PH_CURE_BLEND":604,"PH_CURE_HELD":605,"PH_CURE_SOLO":606,"PH_DRESS_BLEND":568,"PH_DRESS_HELD":569,"PH_DRESS_SOLO":570,"PH_FACE_BLEND":562,"PH_FACE_HELD":563,"PH_FACE_SOLO":564,"PH_FLEECE_BLEND":571,"PH_FLEECE_HELD":572,"PH_FLEECE_SOLO":573,"PH_FOOT_BLEND":595,"PH_FOOT_HELD":596,"PH_FOOT_SOLO":597,"PH_GOAT_BLEND":583,"PH_GOAT_HELD":584,"PH_GOAT_SOLO":585,"PH_GOOSE_BLEND":598,"PH_GOOSE_HELD":599,"PH_GOOSE_SOLO":600,"PH_KIT_BLEND":574,"PH_KIT_HELD":575,"PH_KIT_SOLO":576,"PH_LOT_BLEND":580,"PH_LOT_HELD":581,"PH_LOT_SOLO":582,"PH_MOUTH_BLEND":592,"PH_MOUTH_HELD":593,"PH_MOUTH_SOLO":594,"PH_NURSE_BLEND":607,"PH_NURSE_HELD":608,"PH_NURSE_SOLO":609,"PH_PRICE_BLEND":577,"PH_PRICE_HELD":578,"PH_PRICE_SOLO":579,"PH_STRUT_BLEND":601,"PH_STRUT_HELD":602,"PH_STRUT_SOLO":603,"PH_THOUGHT_BLEND":586,"PH_THOUGHT_HELD":587,"PH_THOUGHT_SOLO":588,"PH_TRAP_BLEND":559,"PH_TRAP_HELD":560,"PH_TRAP_SOLO":561,"SE_A":25,"SE_APPLAUSE":105,"SE_ARENA_TIMEUP1":265,"SE_ARENA_TIMEUP2":266,"SE_BALL":23,"SE_BALLOON_BLUE":75,"SE_BALLOON_RED":74,"SE_BALLOON_YELLOW":76,"SE_BALL_BOUNCE_1":56,"SE_BALL_BOUNCE_2":57,"SE_BALL_BOUNCE_3":58,"SE_BALL_BOUNCE_4":59,"SE_BALL_OPEN":15,"SE_BALL_THROW":61,"SE_BALL_TRADE":60,"SE_BALL_TRAY_BALL":115,"SE_BALL_TRAY_ENTER":114,"SE_BALL_TRAY_EXIT":116,"SE_BANG":20,"SE_BERRY_BLENDER":53,"SE_BIKE_BELL":11,"SE_BIKE_HOP":34,"SE_BOO":22,"SE_BREAKABLE_DOOR":77,"SE_BRIDGE_WALK":71,"SE_CARD":54,"SE_CLICK":36,"SE_CONTEST_CONDITION_LOSE":38,"SE_CONTEST_CURTAIN_FALL":98,"SE_CONTEST_CURTAIN_RISE":97,"SE_CONTEST_HEART":96,"SE_CONTEST_ICON_CHANGE":99,"SE_CONTEST_ICON_CLEAR":100,"SE_CONTEST_MONS_TURN":101,"SE_CONTEST_PLACE":24,"SE_DEX_PAGE":109,"SE_DEX_SCROLL":108,"SE_DEX_SEARCH":112,"SE_DING_DONG":73,"SE_DOOR":8,"SE_DOWNPOUR":83,"SE_DOWNPOUR_STOP":84,"SE_E":28,"SE_EFFECTIVE":13,"SE_EGG_HATCH":113,"SE_ELEVATOR":89,"SE_ESCALATOR":80,"SE_EXIT":9,"SE_EXP":33,"SE_EXP_MAX":91,"SE_FAILURE":32,"SE_FAINT":16,"SE_FALL":43,"SE_FIELD_POISON":79,"SE_FLEE":17,"SE_FU_ZAKU":37,"SE_GLASS_FLUTE":117,"SE_I":26,"SE_ICE_BREAK":41,"SE_ICE_CRACK":42,"SE_ICE_STAIRS":40,"SE_INTRO_BLAST":103,"SE_ITEMFINDER":72,"SE_LAVARIDGE_FALL_WARP":39,"SE_LEDGE":10,"SE_LOW_HEALTH":90,"SE_MUD_BALL":78,"SE_MUGSHOT":104,"SE_M_ABSORB":180,"SE_M_ABSORB_2":179,"SE_M_ACID_ARMOR":218,"SE_M_ATTRACT":226,"SE_M_ATTRACT2":227,"SE_M_BARRIER":208,"SE_M_BATON_PASS":224,"SE_M_BELLY_DRUM":185,"SE_M_BIND":170,"SE_M_BITE":161,"SE_M_BLIZZARD":153,"SE_M_BLIZZARD2":154,"SE_M_BONEMERANG":187,"SE_M_BRICK_BREAK":198,"SE_M_BUBBLE":124,"SE_M_BUBBLE2":125,"SE_M_BUBBLE3":126,"SE_M_BUBBLE_BEAM":182,"SE_M_BUBBLE_BEAM2":183,"SE_M_CHARGE":213,"SE_M_CHARM":212,"SE_M_COMET_PUNCH":139,"SE_M_CONFUSE_RAY":196,"SE_M_COSMIC_POWER":243,"SE_M_CRABHAMMER":142,"SE_M_CUT":128,"SE_M_DETECT":209,"SE_M_DIG":175,"SE_M_DIVE":233,"SE_M_DIZZY_PUNCH":176,"SE_M_DOUBLE_SLAP":134,"SE_M_DOUBLE_TEAM":135,"SE_M_DRAGON_RAGE":171,"SE_M_EARTHQUAKE":234,"SE_M_EMBER":151,"SE_M_ENCORE":222,"SE_M_ENCORE2":223,"SE_M_EXPLOSION":178,"SE_M_FAINT_ATTACK":190,"SE_M_FIRE_PUNCH":147,"SE_M_FLAMETHROWER":146,"SE_M_FLAME_WHEEL":144,"SE_M_FLAME_WHEEL2":145,"SE_M_FLATTER":229,"SE_M_FLY":158,"SE_M_GIGA_DRAIN":199,"SE_M_GRASSWHISTLE":231,"SE_M_GUST":132,"SE_M_GUST2":133,"SE_M_HAIL":242,"SE_M_HARDEN":120,"SE_M_HAZE":246,"SE_M_HEADBUTT":162,"SE_M_HEAL_BELL":195,"SE_M_HEAT_WAVE":240,"SE_M_HORN_ATTACK":166,"SE_M_HYDRO_PUMP":164,"SE_M_HYPER_BEAM":215,"SE_M_HYPER_BEAM2":247,"SE_M_ICY_WIND":137,"SE_M_JUMP_KICK":143,"SE_M_LEER":192,"SE_M_LICK":188,"SE_M_LOCK_ON":210,"SE_M_MEGA_KICK":140,"SE_M_MEGA_KICK2":141,"SE_M_METRONOME":186,"SE_M_MILK_DRINK":225,"SE_M_MINIMIZE":204,"SE_M_MIST":168,"SE_M_MOONLIGHT":211,"SE_M_MORNING_SUN":228,"SE_M_NIGHTMARE":121,"SE_M_PAY_DAY":174,"SE_M_PERISH_SONG":173,"SE_M_PETAL_DANCE":202,"SE_M_POISON_POWDER":169,"SE_M_PSYBEAM":189,"SE_M_PSYBEAM2":200,"SE_M_RAIN_DANCE":127,"SE_M_RAZOR_WIND":136,"SE_M_RAZOR_WIND2":160,"SE_M_REFLECT":207,"SE_M_REVERSAL":217,"SE_M_ROCK_THROW":131,"SE_M_SACRED_FIRE":149,"SE_M_SACRED_FIRE2":150,"SE_M_SANDSTORM":219,"SE_M_SAND_ATTACK":159,"SE_M_SAND_TOMB":230,"SE_M_SCRATCH":155,"SE_M_SCREECH":181,"SE_M_SELF_DESTRUCT":177,"SE_M_SING":172,"SE_M_SKETCH":205,"SE_M_SKY_UPPERCUT":238,"SE_M_SNORE":197,"SE_M_SOLAR_BEAM":201,"SE_M_SPIT_UP":232,"SE_M_STAT_DECREASE":245,"SE_M_STAT_INCREASE":239,"SE_M_STRENGTH":214,"SE_M_STRING_SHOT":129,"SE_M_STRING_SHOT2":130,"SE_M_SUPERSONIC":184,"SE_M_SURF":163,"SE_M_SWAGGER":193,"SE_M_SWAGGER2":194,"SE_M_SWEET_SCENT":236,"SE_M_SWIFT":206,"SE_M_SWORDS_DANCE":191,"SE_M_TAIL_WHIP":167,"SE_M_TAKE_DOWN":152,"SE_M_TEETER_DANCE":244,"SE_M_TELEPORT":203,"SE_M_THUNDERBOLT":118,"SE_M_THUNDERBOLT2":119,"SE_M_THUNDER_WAVE":138,"SE_M_TOXIC":148,"SE_M_TRI_ATTACK":220,"SE_M_TRI_ATTACK2":221,"SE_M_TWISTER":235,"SE_M_UPROAR":241,"SE_M_VICEGRIP":156,"SE_M_VITAL_THROW":122,"SE_M_VITAL_THROW2":123,"SE_M_WATERFALL":216,"SE_M_WHIRLPOOL":165,"SE_M_WING_ATTACK":157,"SE_M_YAWN":237,"SE_N":30,"SE_NOTE_A":67,"SE_NOTE_B":68,"SE_NOTE_C":62,"SE_NOTE_C_HIGH":69,"SE_NOTE_D":63,"SE_NOTE_E":64,"SE_NOTE_F":65,"SE_NOTE_G":66,"SE_NOT_EFFECTIVE":12,"SE_O":29,"SE_ORB":107,"SE_PC_LOGIN":2,"SE_PC_OFF":3,"SE_PC_ON":4,"SE_PIKE_CURTAIN_CLOSE":267,"SE_PIKE_CURTAIN_OPEN":268,"SE_PIN":21,"SE_POKENAV_CALL":263,"SE_POKENAV_HANG_UP":264,"SE_POKENAV_OFF":111,"SE_POKENAV_ON":110,"SE_PUDDLE":70,"SE_RAIN":85,"SE_RAIN_STOP":86,"SE_REPEL":47,"SE_RG_BAG_CURSOR":252,"SE_RG_BAG_POCKET":253,"SE_RG_BALL_CLICK":254,"SE_RG_CARD_FLIP":249,"SE_RG_CARD_FLIPPING":250,"SE_RG_CARD_OPEN":251,"SE_RG_DEOXYS_MOVE":260,"SE_RG_DOOR":248,"SE_RG_HELP_CLOSE":258,"SE_RG_HELP_ERROR":259,"SE_RG_HELP_OPEN":257,"SE_RG_POKE_JUMP_FAILURE":262,"SE_RG_POKE_JUMP_SUCCESS":261,"SE_RG_SHOP":255,"SE_RG_SS_ANNE_HORN":256,"SE_ROTATING_GATE":48,"SE_ROULETTE_BALL":92,"SE_ROULETTE_BALL2":93,"SE_SAVE":55,"SE_SELECT":5,"SE_SHINY":102,"SE_SHIP":19,"SE_SHOP":95,"SE_SLIDING_DOOR":18,"SE_SUCCESS":31,"SE_SUDOWOODO_SHAKE":269,"SE_SUPER_EFFECTIVE":14,"SE_SWITCH":35,"SE_TAILLOW_WING_FLAP":94,"SE_THUNDER":87,"SE_THUNDER2":88,"SE_THUNDERSTORM":81,"SE_THUNDERSTORM_STOP":82,"SE_TRUCK_DOOR":52,"SE_TRUCK_MOVE":49,"SE_TRUCK_STOP":50,"SE_TRUCK_UNLOAD":51,"SE_U":27,"SE_UNLOCK":44,"SE_USE_ITEM":1,"SE_VEND":106,"SE_WALL_HIT":7,"SE_WARP_IN":45,"SE_WARP_OUT":46,"SE_WIN_OPEN":6,"SPECIAL_FLAGS_END":16511,"SPECIAL_FLAGS_START":16384,"SPECIES_ABRA":63,"SPECIES_ABSOL":376,"SPECIES_AERODACTYL":142,"SPECIES_AGGRON":384,"SPECIES_AIPOM":190,"SPECIES_ALAKAZAM":65,"SPECIES_ALTARIA":359,"SPECIES_AMPHAROS":181,"SPECIES_ANORITH":390,"SPECIES_ARBOK":24,"SPECIES_ARCANINE":59,"SPECIES_ARIADOS":168,"SPECIES_ARMALDO":391,"SPECIES_ARON":382,"SPECIES_ARTICUNO":144,"SPECIES_AZUMARILL":184,"SPECIES_AZURILL":350,"SPECIES_BAGON":395,"SPECIES_BALTOY":318,"SPECIES_BANETTE":378,"SPECIES_BARBOACH":323,"SPECIES_BAYLEEF":153,"SPECIES_BEAUTIFLY":292,"SPECIES_BEEDRILL":15,"SPECIES_BELDUM":398,"SPECIES_BELLOSSOM":182,"SPECIES_BELLSPROUT":69,"SPECIES_BLASTOISE":9,"SPECIES_BLAZIKEN":282,"SPECIES_BLISSEY":242,"SPECIES_BRELOOM":307,"SPECIES_BULBASAUR":1,"SPECIES_BUTTERFREE":12,"SPECIES_CACNEA":344,"SPECIES_CACTURNE":345,"SPECIES_CAMERUPT":340,"SPECIES_CARVANHA":330,"SPECIES_CASCOON":293,"SPECIES_CASTFORM":385,"SPECIES_CATERPIE":10,"SPECIES_CELEBI":251,"SPECIES_CHANSEY":113,"SPECIES_CHARIZARD":6,"SPECIES_CHARMANDER":4,"SPECIES_CHARMELEON":5,"SPECIES_CHIKORITA":152,"SPECIES_CHIMECHO":411,"SPECIES_CHINCHOU":170,"SPECIES_CLAMPERL":373,"SPECIES_CLAYDOL":319,"SPECIES_CLEFABLE":36,"SPECIES_CLEFAIRY":35,"SPECIES_CLEFFA":173,"SPECIES_CLOYSTER":91,"SPECIES_COMBUSKEN":281,"SPECIES_CORPHISH":326,"SPECIES_CORSOLA":222,"SPECIES_CRADILY":389,"SPECIES_CRAWDAUNT":327,"SPECIES_CROBAT":169,"SPECIES_CROCONAW":159,"SPECIES_CUBONE":104,"SPECIES_CYNDAQUIL":155,"SPECIES_DELCATTY":316,"SPECIES_DELIBIRD":225,"SPECIES_DEOXYS":410,"SPECIES_DEWGONG":87,"SPECIES_DIGLETT":50,"SPECIES_DITTO":132,"SPECIES_DODRIO":85,"SPECIES_DODUO":84,"SPECIES_DONPHAN":232,"SPECIES_DRAGONAIR":148,"SPECIES_DRAGONITE":149,"SPECIES_DRATINI":147,"SPECIES_DROWZEE":96,"SPECIES_DUGTRIO":51,"SPECIES_DUNSPARCE":206,"SPECIES_DUSCLOPS":362,"SPECIES_DUSKULL":361,"SPECIES_DUSTOX":294,"SPECIES_EEVEE":133,"SPECIES_EGG":412,"SPECIES_EKANS":23,"SPECIES_ELECTABUZZ":125,"SPECIES_ELECTRIKE":337,"SPECIES_ELECTRODE":101,"SPECIES_ELEKID":239,"SPECIES_ENTEI":244,"SPECIES_ESPEON":196,"SPECIES_EXEGGCUTE":102,"SPECIES_EXEGGUTOR":103,"SPECIES_EXPLOUD":372,"SPECIES_FARFETCHD":83,"SPECIES_FEAROW":22,"SPECIES_FEEBAS":328,"SPECIES_FERALIGATR":160,"SPECIES_FLAAFFY":180,"SPECIES_FLAREON":136,"SPECIES_FLYGON":334,"SPECIES_FORRETRESS":205,"SPECIES_FURRET":162,"SPECIES_GARDEVOIR":394,"SPECIES_GASTLY":92,"SPECIES_GENGAR":94,"SPECIES_GEODUDE":74,"SPECIES_GIRAFARIG":203,"SPECIES_GLALIE":347,"SPECIES_GLIGAR":207,"SPECIES_GLOOM":44,"SPECIES_GOLBAT":42,"SPECIES_GOLDEEN":118,"SPECIES_GOLDUCK":55,"SPECIES_GOLEM":76,"SPECIES_GOREBYSS":375,"SPECIES_GRANBULL":210,"SPECIES_GRAVELER":75,"SPECIES_GRIMER":88,"SPECIES_GROUDON":405,"SPECIES_GROVYLE":278,"SPECIES_GROWLITHE":58,"SPECIES_GRUMPIG":352,"SPECIES_GULPIN":367,"SPECIES_GYARADOS":130,"SPECIES_HARIYAMA":336,"SPECIES_HAUNTER":93,"SPECIES_HERACROSS":214,"SPECIES_HITMONCHAN":107,"SPECIES_HITMONLEE":106,"SPECIES_HITMONTOP":237,"SPECIES_HOOTHOOT":163,"SPECIES_HOPPIP":187,"SPECIES_HORSEA":116,"SPECIES_HOUNDOOM":229,"SPECIES_HOUNDOUR":228,"SPECIES_HO_OH":250,"SPECIES_HUNTAIL":374,"SPECIES_HYPNO":97,"SPECIES_IGGLYBUFF":174,"SPECIES_ILLUMISE":387,"SPECIES_IVYSAUR":2,"SPECIES_JIGGLYPUFF":39,"SPECIES_JIRACHI":409,"SPECIES_JOLTEON":135,"SPECIES_JUMPLUFF":189,"SPECIES_JYNX":124,"SPECIES_KABUTO":140,"SPECIES_KABUTOPS":141,"SPECIES_KADABRA":64,"SPECIES_KAKUNA":14,"SPECIES_KANGASKHAN":115,"SPECIES_KECLEON":317,"SPECIES_KINGDRA":230,"SPECIES_KINGLER":99,"SPECIES_KIRLIA":393,"SPECIES_KOFFING":109,"SPECIES_KRABBY":98,"SPECIES_KYOGRE":404,"SPECIES_LAIRON":383,"SPECIES_LANTURN":171,"SPECIES_LAPRAS":131,"SPECIES_LARVITAR":246,"SPECIES_LATIAS":407,"SPECIES_LATIOS":408,"SPECIES_LEDIAN":166,"SPECIES_LEDYBA":165,"SPECIES_LICKITUNG":108,"SPECIES_LILEEP":388,"SPECIES_LINOONE":289,"SPECIES_LOMBRE":296,"SPECIES_LOTAD":295,"SPECIES_LOUDRED":371,"SPECIES_LUDICOLO":297,"SPECIES_LUGIA":249,"SPECIES_LUNATONE":348,"SPECIES_LUVDISC":325,"SPECIES_MACHAMP":68,"SPECIES_MACHOKE":67,"SPECIES_MACHOP":66,"SPECIES_MAGBY":240,"SPECIES_MAGCARGO":219,"SPECIES_MAGIKARP":129,"SPECIES_MAGMAR":126,"SPECIES_MAGNEMITE":81,"SPECIES_MAGNETON":82,"SPECIES_MAKUHITA":335,"SPECIES_MANECTRIC":338,"SPECIES_MANKEY":56,"SPECIES_MANTINE":226,"SPECIES_MAREEP":179,"SPECIES_MARILL":183,"SPECIES_MAROWAK":105,"SPECIES_MARSHTOMP":284,"SPECIES_MASQUERAIN":312,"SPECIES_MAWILE":355,"SPECIES_MEDICHAM":357,"SPECIES_MEDITITE":356,"SPECIES_MEGANIUM":154,"SPECIES_MEOWTH":52,"SPECIES_METAGROSS":400,"SPECIES_METANG":399,"SPECIES_METAPOD":11,"SPECIES_MEW":151,"SPECIES_MEWTWO":150,"SPECIES_MIGHTYENA":287,"SPECIES_MILOTIC":329,"SPECIES_MILTANK":241,"SPECIES_MINUN":354,"SPECIES_MISDREAVUS":200,"SPECIES_MOLTRES":146,"SPECIES_MR_MIME":122,"SPECIES_MUDKIP":283,"SPECIES_MUK":89,"SPECIES_MURKROW":198,"SPECIES_NATU":177,"SPECIES_NIDOKING":34,"SPECIES_NIDOQUEEN":31,"SPECIES_NIDORAN_F":29,"SPECIES_NIDORAN_M":32,"SPECIES_NIDORINA":30,"SPECIES_NIDORINO":33,"SPECIES_NINCADA":301,"SPECIES_NINETALES":38,"SPECIES_NINJASK":302,"SPECIES_NOCTOWL":164,"SPECIES_NONE":0,"SPECIES_NOSEPASS":320,"SPECIES_NUMEL":339,"SPECIES_NUZLEAF":299,"SPECIES_OCTILLERY":224,"SPECIES_ODDISH":43,"SPECIES_OLD_UNOWN_B":252,"SPECIES_OLD_UNOWN_C":253,"SPECIES_OLD_UNOWN_D":254,"SPECIES_OLD_UNOWN_E":255,"SPECIES_OLD_UNOWN_F":256,"SPECIES_OLD_UNOWN_G":257,"SPECIES_OLD_UNOWN_H":258,"SPECIES_OLD_UNOWN_I":259,"SPECIES_OLD_UNOWN_J":260,"SPECIES_OLD_UNOWN_K":261,"SPECIES_OLD_UNOWN_L":262,"SPECIES_OLD_UNOWN_M":263,"SPECIES_OLD_UNOWN_N":264,"SPECIES_OLD_UNOWN_O":265,"SPECIES_OLD_UNOWN_P":266,"SPECIES_OLD_UNOWN_Q":267,"SPECIES_OLD_UNOWN_R":268,"SPECIES_OLD_UNOWN_S":269,"SPECIES_OLD_UNOWN_T":270,"SPECIES_OLD_UNOWN_U":271,"SPECIES_OLD_UNOWN_V":272,"SPECIES_OLD_UNOWN_W":273,"SPECIES_OLD_UNOWN_X":274,"SPECIES_OLD_UNOWN_Y":275,"SPECIES_OLD_UNOWN_Z":276,"SPECIES_OMANYTE":138,"SPECIES_OMASTAR":139,"SPECIES_ONIX":95,"SPECIES_PARAS":46,"SPECIES_PARASECT":47,"SPECIES_PELIPPER":310,"SPECIES_PERSIAN":53,"SPECIES_PHANPY":231,"SPECIES_PICHU":172,"SPECIES_PIDGEOT":18,"SPECIES_PIDGEOTTO":17,"SPECIES_PIDGEY":16,"SPECIES_PIKACHU":25,"SPECIES_PILOSWINE":221,"SPECIES_PINECO":204,"SPECIES_PINSIR":127,"SPECIES_PLUSLE":353,"SPECIES_POLITOED":186,"SPECIES_POLIWAG":60,"SPECIES_POLIWHIRL":61,"SPECIES_POLIWRATH":62,"SPECIES_PONYTA":77,"SPECIES_POOCHYENA":286,"SPECIES_PORYGON":137,"SPECIES_PORYGON2":233,"SPECIES_PRIMEAPE":57,"SPECIES_PSYDUCK":54,"SPECIES_PUPITAR":247,"SPECIES_QUAGSIRE":195,"SPECIES_QUILAVA":156,"SPECIES_QWILFISH":211,"SPECIES_RAICHU":26,"SPECIES_RAIKOU":243,"SPECIES_RALTS":392,"SPECIES_RAPIDASH":78,"SPECIES_RATICATE":20,"SPECIES_RATTATA":19,"SPECIES_RAYQUAZA":406,"SPECIES_REGICE":402,"SPECIES_REGIROCK":401,"SPECIES_REGISTEEL":403,"SPECIES_RELICANTH":381,"SPECIES_REMORAID":223,"SPECIES_RHYDON":112,"SPECIES_RHYHORN":111,"SPECIES_ROSELIA":363,"SPECIES_SABLEYE":322,"SPECIES_SALAMENCE":397,"SPECIES_SANDSHREW":27,"SPECIES_SANDSLASH":28,"SPECIES_SCEPTILE":279,"SPECIES_SCIZOR":212,"SPECIES_SCYTHER":123,"SPECIES_SEADRA":117,"SPECIES_SEAKING":119,"SPECIES_SEALEO":342,"SPECIES_SEEDOT":298,"SPECIES_SEEL":86,"SPECIES_SENTRET":161,"SPECIES_SEVIPER":379,"SPECIES_SHARPEDO":331,"SPECIES_SHEDINJA":303,"SPECIES_SHELGON":396,"SPECIES_SHELLDER":90,"SPECIES_SHIFTRY":300,"SPECIES_SHROOMISH":306,"SPECIES_SHUCKLE":213,"SPECIES_SHUPPET":377,"SPECIES_SILCOON":291,"SPECIES_SKARMORY":227,"SPECIES_SKIPLOOM":188,"SPECIES_SKITTY":315,"SPECIES_SLAKING":366,"SPECIES_SLAKOTH":364,"SPECIES_SLOWBRO":80,"SPECIES_SLOWKING":199,"SPECIES_SLOWPOKE":79,"SPECIES_SLUGMA":218,"SPECIES_SMEARGLE":235,"SPECIES_SMOOCHUM":238,"SPECIES_SNEASEL":215,"SPECIES_SNORLAX":143,"SPECIES_SNORUNT":346,"SPECIES_SNUBBULL":209,"SPECIES_SOLROCK":349,"SPECIES_SPEAROW":21,"SPECIES_SPHEAL":341,"SPECIES_SPINARAK":167,"SPECIES_SPINDA":308,"SPECIES_SPOINK":351,"SPECIES_SQUIRTLE":7,"SPECIES_STANTLER":234,"SPECIES_STARMIE":121,"SPECIES_STARYU":120,"SPECIES_STEELIX":208,"SPECIES_SUDOWOODO":185,"SPECIES_SUICUNE":245,"SPECIES_SUNFLORA":192,"SPECIES_SUNKERN":191,"SPECIES_SURSKIT":311,"SPECIES_SWABLU":358,"SPECIES_SWALOT":368,"SPECIES_SWAMPERT":285,"SPECIES_SWELLOW":305,"SPECIES_SWINUB":220,"SPECIES_TAILLOW":304,"SPECIES_TANGELA":114,"SPECIES_TAUROS":128,"SPECIES_TEDDIURSA":216,"SPECIES_TENTACOOL":72,"SPECIES_TENTACRUEL":73,"SPECIES_TOGEPI":175,"SPECIES_TOGETIC":176,"SPECIES_TORCHIC":280,"SPECIES_TORKOAL":321,"SPECIES_TOTODILE":158,"SPECIES_TRAPINCH":332,"SPECIES_TREECKO":277,"SPECIES_TROPIUS":369,"SPECIES_TYPHLOSION":157,"SPECIES_TYRANITAR":248,"SPECIES_TYROGUE":236,"SPECIES_UMBREON":197,"SPECIES_UNOWN":201,"SPECIES_UNOWN_B":413,"SPECIES_UNOWN_C":414,"SPECIES_UNOWN_D":415,"SPECIES_UNOWN_E":416,"SPECIES_UNOWN_EMARK":438,"SPECIES_UNOWN_F":417,"SPECIES_UNOWN_G":418,"SPECIES_UNOWN_H":419,"SPECIES_UNOWN_I":420,"SPECIES_UNOWN_J":421,"SPECIES_UNOWN_K":422,"SPECIES_UNOWN_L":423,"SPECIES_UNOWN_M":424,"SPECIES_UNOWN_N":425,"SPECIES_UNOWN_O":426,"SPECIES_UNOWN_P":427,"SPECIES_UNOWN_Q":428,"SPECIES_UNOWN_QMARK":439,"SPECIES_UNOWN_R":429,"SPECIES_UNOWN_S":430,"SPECIES_UNOWN_T":431,"SPECIES_UNOWN_U":432,"SPECIES_UNOWN_V":433,"SPECIES_UNOWN_W":434,"SPECIES_UNOWN_X":435,"SPECIES_UNOWN_Y":436,"SPECIES_UNOWN_Z":437,"SPECIES_URSARING":217,"SPECIES_VAPOREON":134,"SPECIES_VENOMOTH":49,"SPECIES_VENONAT":48,"SPECIES_VENUSAUR":3,"SPECIES_VIBRAVA":333,"SPECIES_VICTREEBEL":71,"SPECIES_VIGOROTH":365,"SPECIES_VILEPLUME":45,"SPECIES_VOLBEAT":386,"SPECIES_VOLTORB":100,"SPECIES_VULPIX":37,"SPECIES_WAILMER":313,"SPECIES_WAILORD":314,"SPECIES_WALREIN":343,"SPECIES_WARTORTLE":8,"SPECIES_WEEDLE":13,"SPECIES_WEEPINBELL":70,"SPECIES_WEEZING":110,"SPECIES_WHISCASH":324,"SPECIES_WHISMUR":370,"SPECIES_WIGGLYTUFF":40,"SPECIES_WINGULL":309,"SPECIES_WOBBUFFET":202,"SPECIES_WOOPER":194,"SPECIES_WURMPLE":290,"SPECIES_WYNAUT":360,"SPECIES_XATU":178,"SPECIES_YANMA":193,"SPECIES_ZANGOOSE":380,"SPECIES_ZAPDOS":145,"SPECIES_ZIGZAGOON":288,"SPECIES_ZUBAT":41,"SUPER_ROD":2,"SYSTEM_FLAGS":2144,"TEMP_FLAGS_END":31,"TEMP_FLAGS_START":0,"TRAINERS_COUNT":855,"TRAINER_AARON":397,"TRAINER_ABIGAIL_1":358,"TRAINER_ABIGAIL_2":360,"TRAINER_ABIGAIL_3":361,"TRAINER_ABIGAIL_4":362,"TRAINER_ABIGAIL_5":363,"TRAINER_AIDAN":674,"TRAINER_AISHA":757,"TRAINER_ALAN":630,"TRAINER_ALBERT":80,"TRAINER_ALBERTO":12,"TRAINER_ALEX":413,"TRAINER_ALEXA":670,"TRAINER_ALEXIA":90,"TRAINER_ALEXIS":248,"TRAINER_ALICE":448,"TRAINER_ALIX":750,"TRAINER_ALLEN":333,"TRAINER_ALLISON":387,"TRAINER_ALVARO":849,"TRAINER_ALYSSA":701,"TRAINER_AMY_AND_LIV_1":481,"TRAINER_AMY_AND_LIV_2":482,"TRAINER_AMY_AND_LIV_3":485,"TRAINER_AMY_AND_LIV_4":487,"TRAINER_AMY_AND_LIV_5":488,"TRAINER_AMY_AND_LIV_6":489,"TRAINER_ANABEL":805,"TRAINER_ANDREA":613,"TRAINER_ANDRES_1":737,"TRAINER_ANDRES_2":812,"TRAINER_ANDRES_3":813,"TRAINER_ANDRES_4":814,"TRAINER_ANDRES_5":815,"TRAINER_ANDREW":336,"TRAINER_ANGELICA":436,"TRAINER_ANGELINA":712,"TRAINER_ANGELO":802,"TRAINER_ANNA_AND_MEG_1":287,"TRAINER_ANNA_AND_MEG_2":288,"TRAINER_ANNA_AND_MEG_3":289,"TRAINER_ANNA_AND_MEG_4":290,"TRAINER_ANNA_AND_MEG_5":291,"TRAINER_ANNIKA":502,"TRAINER_ANTHONY":352,"TRAINER_ARCHIE":34,"TRAINER_ASHLEY":655,"TRAINER_ATHENA":577,"TRAINER_ATSUSHI":190,"TRAINER_AURON":506,"TRAINER_AUSTINA":58,"TRAINER_AUTUMN":217,"TRAINER_AXLE":203,"TRAINER_BARNY":343,"TRAINER_BARRY":163,"TRAINER_BEAU":212,"TRAINER_BECK":414,"TRAINER_BECKY":470,"TRAINER_BEN":323,"TRAINER_BENJAMIN_1":353,"TRAINER_BENJAMIN_2":354,"TRAINER_BENJAMIN_3":355,"TRAINER_BENJAMIN_4":356,"TRAINER_BENJAMIN_5":357,"TRAINER_BENNY":407,"TRAINER_BERKE":74,"TRAINER_BERNIE_1":206,"TRAINER_BERNIE_2":207,"TRAINER_BERNIE_3":208,"TRAINER_BERNIE_4":209,"TRAINER_BERNIE_5":210,"TRAINER_BETH":445,"TRAINER_BETHANY":301,"TRAINER_BEVERLY":441,"TRAINER_BIANCA":706,"TRAINER_BILLY":319,"TRAINER_BLAKE":235,"TRAINER_BRANDEN":745,"TRAINER_BRANDI":756,"TRAINER_BRANDON":811,"TRAINER_BRAWLY_1":266,"TRAINER_BRAWLY_2":774,"TRAINER_BRAWLY_3":775,"TRAINER_BRAWLY_4":776,"TRAINER_BRAWLY_5":777,"TRAINER_BRAXTON":75,"TRAINER_BRENDA":454,"TRAINER_BRENDAN_LILYCOVE_MUDKIP":661,"TRAINER_BRENDAN_LILYCOVE_TORCHIC":663,"TRAINER_BRENDAN_LILYCOVE_TREECKO":662,"TRAINER_BRENDAN_PLACEHOLDER":853,"TRAINER_BRENDAN_ROUTE_103_MUDKIP":520,"TRAINER_BRENDAN_ROUTE_103_TORCHIC":526,"TRAINER_BRENDAN_ROUTE_103_TREECKO":523,"TRAINER_BRENDAN_ROUTE_110_MUDKIP":521,"TRAINER_BRENDAN_ROUTE_110_TORCHIC":527,"TRAINER_BRENDAN_ROUTE_110_TREECKO":524,"TRAINER_BRENDAN_ROUTE_119_MUDKIP":522,"TRAINER_BRENDAN_ROUTE_119_TORCHIC":528,"TRAINER_BRENDAN_ROUTE_119_TREECKO":525,"TRAINER_BRENDAN_RUSTBORO_MUDKIP":593,"TRAINER_BRENDAN_RUSTBORO_TORCHIC":599,"TRAINER_BRENDAN_RUSTBORO_TREECKO":592,"TRAINER_BRENDEN":572,"TRAINER_BRENT":223,"TRAINER_BRIANNA":118,"TRAINER_BRICE":626,"TRAINER_BRIDGET":129,"TRAINER_BROOKE_1":94,"TRAINER_BROOKE_2":101,"TRAINER_BROOKE_3":102,"TRAINER_BROOKE_4":103,"TRAINER_BROOKE_5":104,"TRAINER_BRYAN":744,"TRAINER_BRYANT":746,"TRAINER_CALE":764,"TRAINER_CALLIE":763,"TRAINER_CALVIN_1":318,"TRAINER_CALVIN_2":328,"TRAINER_CALVIN_3":329,"TRAINER_CALVIN_4":330,"TRAINER_CALVIN_5":331,"TRAINER_CAMDEN":374,"TRAINER_CAMERON_1":238,"TRAINER_CAMERON_2":239,"TRAINER_CAMERON_3":240,"TRAINER_CAMERON_4":241,"TRAINER_CAMERON_5":242,"TRAINER_CAMRON":739,"TRAINER_CARLEE":464,"TRAINER_CAROL":471,"TRAINER_CAROLINA":741,"TRAINER_CAROLINE":99,"TRAINER_CARTER":345,"TRAINER_CATHERINE_1":559,"TRAINER_CATHERINE_2":562,"TRAINER_CATHERINE_3":563,"TRAINER_CATHERINE_4":564,"TRAINER_CATHERINE_5":565,"TRAINER_CEDRIC":475,"TRAINER_CELIA":743,"TRAINER_CELINA":705,"TRAINER_CHAD":174,"TRAINER_CHANDLER":698,"TRAINER_CHARLIE":66,"TRAINER_CHARLOTTE":714,"TRAINER_CHASE":378,"TRAINER_CHESTER":408,"TRAINER_CHIP":45,"TRAINER_CHRIS":693,"TRAINER_CINDY_1":114,"TRAINER_CINDY_2":117,"TRAINER_CINDY_3":120,"TRAINER_CINDY_4":121,"TRAINER_CINDY_5":122,"TRAINER_CINDY_6":123,"TRAINER_CLARENCE":580,"TRAINER_CLARISSA":435,"TRAINER_CLARK":631,"TRAINER_CLAUDE":338,"TRAINER_CLIFFORD":584,"TRAINER_COBY":709,"TRAINER_COLE":201,"TRAINER_COLIN":405,"TRAINER_COLTON":294,"TRAINER_CONNIE":128,"TRAINER_CONOR":511,"TRAINER_CORA":428,"TRAINER_CORY_1":740,"TRAINER_CORY_2":816,"TRAINER_CORY_3":817,"TRAINER_CORY_4":818,"TRAINER_CORY_5":819,"TRAINER_CRISSY":614,"TRAINER_CRISTIAN":574,"TRAINER_CRISTIN_1":767,"TRAINER_CRISTIN_2":828,"TRAINER_CRISTIN_3":829,"TRAINER_CRISTIN_4":830,"TRAINER_CRISTIN_5":831,"TRAINER_CYNDY_1":427,"TRAINER_CYNDY_2":430,"TRAINER_CYNDY_3":431,"TRAINER_CYNDY_4":432,"TRAINER_CYNDY_5":433,"TRAINER_DAISUKE":189,"TRAINER_DAISY":36,"TRAINER_DALE":341,"TRAINER_DALTON_1":196,"TRAINER_DALTON_2":197,"TRAINER_DALTON_3":198,"TRAINER_DALTON_4":199,"TRAINER_DALTON_5":200,"TRAINER_DANA":458,"TRAINER_DANIELLE":650,"TRAINER_DAPHNE":115,"TRAINER_DARCY":733,"TRAINER_DARIAN":696,"TRAINER_DARIUS":803,"TRAINER_DARRIN":154,"TRAINER_DAVID":158,"TRAINER_DAVIS":539,"TRAINER_DAWSON":694,"TRAINER_DAYTON":760,"TRAINER_DEAN":164,"TRAINER_DEANDRE":715,"TRAINER_DEBRA":460,"TRAINER_DECLAN":15,"TRAINER_DEMETRIUS":375,"TRAINER_DENISE":444,"TRAINER_DEREK":227,"TRAINER_DEVAN":753,"TRAINER_DEZ_AND_LUKE":640,"TRAINER_DIANA_1":474,"TRAINER_DIANA_2":477,"TRAINER_DIANA_3":478,"TRAINER_DIANA_4":479,"TRAINER_DIANA_5":480,"TRAINER_DIANNE":417,"TRAINER_DILLON":327,"TRAINER_DOMINIK":152,"TRAINER_DONALD":224,"TRAINER_DONNY":384,"TRAINER_DOUG":618,"TRAINER_DOUGLAS":153,"TRAINER_DRAKE":264,"TRAINER_DREW":211,"TRAINER_DUDLEY":173,"TRAINER_DUNCAN":496,"TRAINER_DUSTY_1":44,"TRAINER_DUSTY_2":47,"TRAINER_DUSTY_3":48,"TRAINER_DUSTY_4":49,"TRAINER_DUSTY_5":50,"TRAINER_DWAYNE":493,"TRAINER_DYLAN_1":364,"TRAINER_DYLAN_2":365,"TRAINER_DYLAN_3":366,"TRAINER_DYLAN_4":367,"TRAINER_DYLAN_5":368,"TRAINER_ED":13,"TRAINER_EDDIE":332,"TRAINER_EDGAR":79,"TRAINER_EDMOND":491,"TRAINER_EDWARD":232,"TRAINER_EDWARDO":404,"TRAINER_EDWIN_1":512,"TRAINER_EDWIN_2":515,"TRAINER_EDWIN_3":516,"TRAINER_EDWIN_4":517,"TRAINER_EDWIN_5":518,"TRAINER_ELI":501,"TRAINER_ELIJAH":742,"TRAINER_ELLIOT_1":339,"TRAINER_ELLIOT_2":346,"TRAINER_ELLIOT_3":347,"TRAINER_ELLIOT_4":348,"TRAINER_ELLIOT_5":349,"TRAINER_ERIC":632,"TRAINER_ERNEST_1":492,"TRAINER_ERNEST_2":497,"TRAINER_ERNEST_3":498,"TRAINER_ERNEST_4":499,"TRAINER_ERNEST_5":500,"TRAINER_ETHAN_1":216,"TRAINER_ETHAN_2":219,"TRAINER_ETHAN_3":220,"TRAINER_ETHAN_4":221,"TRAINER_ETHAN_5":222,"TRAINER_EVERETT":850,"TRAINER_FABIAN":759,"TRAINER_FELIX":38,"TRAINER_FERNANDO_1":195,"TRAINER_FERNANDO_2":832,"TRAINER_FERNANDO_3":833,"TRAINER_FERNANDO_4":834,"TRAINER_FERNANDO_5":835,"TRAINER_FLAGS_END":2143,"TRAINER_FLAGS_START":1280,"TRAINER_FLANNERY_1":268,"TRAINER_FLANNERY_2":782,"TRAINER_FLANNERY_3":783,"TRAINER_FLANNERY_4":784,"TRAINER_FLANNERY_5":785,"TRAINER_FLINT":654,"TRAINER_FOSTER":46,"TRAINER_FRANKLIN":170,"TRAINER_FREDRICK":29,"TRAINER_GABBY_AND_TY_1":51,"TRAINER_GABBY_AND_TY_2":52,"TRAINER_GABBY_AND_TY_3":53,"TRAINER_GABBY_AND_TY_4":54,"TRAINER_GABBY_AND_TY_5":55,"TRAINER_GABBY_AND_TY_6":56,"TRAINER_GABRIELLE_1":9,"TRAINER_GABRIELLE_2":840,"TRAINER_GABRIELLE_3":841,"TRAINER_GABRIELLE_4":842,"TRAINER_GABRIELLE_5":843,"TRAINER_GARRET":138,"TRAINER_GARRISON":547,"TRAINER_GEORGE":73,"TRAINER_GEORGIA":281,"TRAINER_GERALD":648,"TRAINER_GILBERT":169,"TRAINER_GINA_AND_MIA_1":483,"TRAINER_GINA_AND_MIA_2":486,"TRAINER_GLACIA":263,"TRAINER_GRACE":450,"TRAINER_GREG":619,"TRAINER_GRETA":808,"TRAINER_GRUNT_AQUA_HIDEOUT_1":2,"TRAINER_GRUNT_AQUA_HIDEOUT_2":3,"TRAINER_GRUNT_AQUA_HIDEOUT_3":4,"TRAINER_GRUNT_AQUA_HIDEOUT_4":5,"TRAINER_GRUNT_AQUA_HIDEOUT_5":27,"TRAINER_GRUNT_AQUA_HIDEOUT_6":28,"TRAINER_GRUNT_AQUA_HIDEOUT_7":192,"TRAINER_GRUNT_AQUA_HIDEOUT_8":193,"TRAINER_GRUNT_JAGGED_PASS":570,"TRAINER_GRUNT_MAGMA_HIDEOUT_1":716,"TRAINER_GRUNT_MAGMA_HIDEOUT_10":725,"TRAINER_GRUNT_MAGMA_HIDEOUT_11":726,"TRAINER_GRUNT_MAGMA_HIDEOUT_12":727,"TRAINER_GRUNT_MAGMA_HIDEOUT_13":728,"TRAINER_GRUNT_MAGMA_HIDEOUT_14":729,"TRAINER_GRUNT_MAGMA_HIDEOUT_15":730,"TRAINER_GRUNT_MAGMA_HIDEOUT_16":731,"TRAINER_GRUNT_MAGMA_HIDEOUT_2":717,"TRAINER_GRUNT_MAGMA_HIDEOUT_3":718,"TRAINER_GRUNT_MAGMA_HIDEOUT_4":719,"TRAINER_GRUNT_MAGMA_HIDEOUT_5":720,"TRAINER_GRUNT_MAGMA_HIDEOUT_6":721,"TRAINER_GRUNT_MAGMA_HIDEOUT_7":722,"TRAINER_GRUNT_MAGMA_HIDEOUT_8":723,"TRAINER_GRUNT_MAGMA_HIDEOUT_9":724,"TRAINER_GRUNT_MT_CHIMNEY_1":146,"TRAINER_GRUNT_MT_CHIMNEY_2":579,"TRAINER_GRUNT_MT_PYRE_1":23,"TRAINER_GRUNT_MT_PYRE_2":24,"TRAINER_GRUNT_MT_PYRE_3":25,"TRAINER_GRUNT_MT_PYRE_4":569,"TRAINER_GRUNT_MUSEUM_1":20,"TRAINER_GRUNT_MUSEUM_2":21,"TRAINER_GRUNT_PETALBURG_WOODS":10,"TRAINER_GRUNT_RUSTURF_TUNNEL":16,"TRAINER_GRUNT_SEAFLOOR_CAVERN_1":6,"TRAINER_GRUNT_SEAFLOOR_CAVERN_2":7,"TRAINER_GRUNT_SEAFLOOR_CAVERN_3":8,"TRAINER_GRUNT_SEAFLOOR_CAVERN_4":14,"TRAINER_GRUNT_SEAFLOOR_CAVERN_5":567,"TRAINER_GRUNT_SPACE_CENTER_1":22,"TRAINER_GRUNT_SPACE_CENTER_2":116,"TRAINER_GRUNT_SPACE_CENTER_3":586,"TRAINER_GRUNT_SPACE_CENTER_4":587,"TRAINER_GRUNT_SPACE_CENTER_5":588,"TRAINER_GRUNT_SPACE_CENTER_6":589,"TRAINER_GRUNT_SPACE_CENTER_7":590,"TRAINER_GRUNT_UNUSED":568,"TRAINER_GRUNT_WEATHER_INST_1":17,"TRAINER_GRUNT_WEATHER_INST_2":18,"TRAINER_GRUNT_WEATHER_INST_3":19,"TRAINER_GRUNT_WEATHER_INST_4":26,"TRAINER_GRUNT_WEATHER_INST_5":596,"TRAINER_GWEN":59,"TRAINER_HAILEY":697,"TRAINER_HALEY_1":604,"TRAINER_HALEY_2":607,"TRAINER_HALEY_3":608,"TRAINER_HALEY_4":609,"TRAINER_HALEY_5":610,"TRAINER_HALLE":546,"TRAINER_HANNAH":244,"TRAINER_HARRISON":578,"TRAINER_HAYDEN":707,"TRAINER_HECTOR":513,"TRAINER_HEIDI":469,"TRAINER_HELENE":751,"TRAINER_HENRY":668,"TRAINER_HERMAN":167,"TRAINER_HIDEO":651,"TRAINER_HITOSHI":180,"TRAINER_HOPE":96,"TRAINER_HUDSON":510,"TRAINER_HUEY":490,"TRAINER_HUGH":399,"TRAINER_HUMBERTO":402,"TRAINER_IMANI":442,"TRAINER_IRENE":476,"TRAINER_ISAAC_1":538,"TRAINER_ISAAC_2":541,"TRAINER_ISAAC_3":542,"TRAINER_ISAAC_4":543,"TRAINER_ISAAC_5":544,"TRAINER_ISABELLA":595,"TRAINER_ISABELLE":736,"TRAINER_ISABEL_1":302,"TRAINER_ISABEL_2":303,"TRAINER_ISABEL_3":304,"TRAINER_ISABEL_4":305,"TRAINER_ISABEL_5":306,"TRAINER_ISAIAH_1":376,"TRAINER_ISAIAH_2":379,"TRAINER_ISAIAH_3":380,"TRAINER_ISAIAH_4":381,"TRAINER_ISAIAH_5":382,"TRAINER_ISOBEL":383,"TRAINER_IVAN":337,"TRAINER_JACE":204,"TRAINER_JACK":172,"TRAINER_JACKI_1":249,"TRAINER_JACKI_2":250,"TRAINER_JACKI_3":251,"TRAINER_JACKI_4":252,"TRAINER_JACKI_5":253,"TRAINER_JACKSON_1":552,"TRAINER_JACKSON_2":555,"TRAINER_JACKSON_3":556,"TRAINER_JACKSON_4":557,"TRAINER_JACKSON_5":558,"TRAINER_JACLYN":243,"TRAINER_JACOB":351,"TRAINER_JAIDEN":749,"TRAINER_JAMES_1":621,"TRAINER_JAMES_2":622,"TRAINER_JAMES_3":623,"TRAINER_JAMES_4":624,"TRAINER_JAMES_5":625,"TRAINER_JANI":418,"TRAINER_JANICE":605,"TRAINER_JARED":401,"TRAINER_JASMINE":359,"TRAINER_JAYLEN":326,"TRAINER_JAZMYN":503,"TRAINER_JEFF":202,"TRAINER_JEFFREY_1":226,"TRAINER_JEFFREY_2":228,"TRAINER_JEFFREY_3":229,"TRAINER_JEFFREY_4":230,"TRAINER_JEFFREY_5":231,"TRAINER_JENNA":560,"TRAINER_JENNIFER":95,"TRAINER_JENNY_1":449,"TRAINER_JENNY_2":465,"TRAINER_JENNY_3":466,"TRAINER_JENNY_4":467,"TRAINER_JENNY_5":468,"TRAINER_JEROME":156,"TRAINER_JERRY_1":273,"TRAINER_JERRY_2":276,"TRAINER_JERRY_3":277,"TRAINER_JERRY_4":278,"TRAINER_JERRY_5":279,"TRAINER_JESSICA_1":127,"TRAINER_JESSICA_2":132,"TRAINER_JESSICA_3":133,"TRAINER_JESSICA_4":134,"TRAINER_JESSICA_5":135,"TRAINER_JOCELYN":425,"TRAINER_JODY":91,"TRAINER_JOEY":322,"TRAINER_JOHANNA":647,"TRAINER_JOHNSON":754,"TRAINER_JOHN_AND_JAY_1":681,"TRAINER_JOHN_AND_JAY_2":682,"TRAINER_JOHN_AND_JAY_3":683,"TRAINER_JOHN_AND_JAY_4":684,"TRAINER_JOHN_AND_JAY_5":685,"TRAINER_JONAH":667,"TRAINER_JONAS":504,"TRAINER_JONATHAN":598,"TRAINER_JOSE":617,"TRAINER_JOSEPH":700,"TRAINER_JOSH":320,"TRAINER_JOSHUA":237,"TRAINER_JOSUE":738,"TRAINER_JUAN_1":272,"TRAINER_JUAN_2":798,"TRAINER_JUAN_3":799,"TRAINER_JUAN_4":800,"TRAINER_JUAN_5":801,"TRAINER_JULIE":100,"TRAINER_JULIO":566,"TRAINER_JUSTIN":215,"TRAINER_KAI":713,"TRAINER_KALEB":699,"TRAINER_KARA":457,"TRAINER_KAREN_1":280,"TRAINER_KAREN_2":282,"TRAINER_KAREN_3":283,"TRAINER_KAREN_4":284,"TRAINER_KAREN_5":285,"TRAINER_KATELYNN":325,"TRAINER_KATELYN_1":386,"TRAINER_KATELYN_2":388,"TRAINER_KATELYN_3":389,"TRAINER_KATELYN_4":390,"TRAINER_KATELYN_5":391,"TRAINER_KATE_AND_JOY":286,"TRAINER_KATHLEEN":583,"TRAINER_KATIE":455,"TRAINER_KAYLA":247,"TRAINER_KAYLEE":462,"TRAINER_KAYLEY":505,"TRAINER_KEEGAN":205,"TRAINER_KEIGO":652,"TRAINER_KEIRA":93,"TRAINER_KELVIN":507,"TRAINER_KENT":620,"TRAINER_KEVIN":171,"TRAINER_KIM_AND_IRIS":678,"TRAINER_KINDRA":106,"TRAINER_KIRA_AND_DAN_1":642,"TRAINER_KIRA_AND_DAN_2":643,"TRAINER_KIRA_AND_DAN_3":644,"TRAINER_KIRA_AND_DAN_4":645,"TRAINER_KIRA_AND_DAN_5":646,"TRAINER_KIRK":191,"TRAINER_KIYO":181,"TRAINER_KOICHI":182,"TRAINER_KOJI_1":672,"TRAINER_KOJI_2":824,"TRAINER_KOJI_3":825,"TRAINER_KOJI_4":826,"TRAINER_KOJI_5":827,"TRAINER_KYLA":443,"TRAINER_KYRA":748,"TRAINER_LAO_1":419,"TRAINER_LAO_2":421,"TRAINER_LAO_3":422,"TRAINER_LAO_4":423,"TRAINER_LAO_5":424,"TRAINER_LARRY":213,"TRAINER_LAURA":426,"TRAINER_LAUREL":463,"TRAINER_LAWRENCE":710,"TRAINER_LEAF":852,"TRAINER_LEAH":35,"TRAINER_LEA_AND_JED":641,"TRAINER_LENNY":628,"TRAINER_LEONARD":495,"TRAINER_LEONARDO":576,"TRAINER_LEONEL":762,"TRAINER_LEROY":77,"TRAINER_LILA_AND_ROY_1":687,"TRAINER_LILA_AND_ROY_2":688,"TRAINER_LILA_AND_ROY_3":689,"TRAINER_LILA_AND_ROY_4":690,"TRAINER_LILA_AND_ROY_5":691,"TRAINER_LILITH":573,"TRAINER_LINDA":461,"TRAINER_LISA_AND_RAY":692,"TRAINER_LOLA_1":57,"TRAINER_LOLA_2":60,"TRAINER_LOLA_3":61,"TRAINER_LOLA_4":62,"TRAINER_LOLA_5":63,"TRAINER_LORENZO":553,"TRAINER_LUCAS_1":629,"TRAINER_LUCAS_2":633,"TRAINER_LUCY":810,"TRAINER_LUIS":151,"TRAINER_LUNG":420,"TRAINER_LYDIA_1":545,"TRAINER_LYDIA_2":548,"TRAINER_LYDIA_3":549,"TRAINER_LYDIA_4":550,"TRAINER_LYDIA_5":551,"TRAINER_LYLE":616,"TRAINER_MACEY":591,"TRAINER_MADELINE_1":434,"TRAINER_MADELINE_2":437,"TRAINER_MADELINE_3":438,"TRAINER_MADELINE_4":439,"TRAINER_MADELINE_5":440,"TRAINER_MAKAYLA":758,"TRAINER_MARC":571,"TRAINER_MARCEL":11,"TRAINER_MARCOS":702,"TRAINER_MARIA_1":369,"TRAINER_MARIA_2":370,"TRAINER_MARIA_3":371,"TRAINER_MARIA_4":372,"TRAINER_MARIA_5":373,"TRAINER_MARIELA":848,"TRAINER_MARK":145,"TRAINER_MARLENE":752,"TRAINER_MARLEY":508,"TRAINER_MARTHA":473,"TRAINER_MARY":89,"TRAINER_MATT":30,"TRAINER_MATTHEW":157,"TRAINER_MAURA":246,"TRAINER_MAXIE_MAGMA_HIDEOUT":601,"TRAINER_MAXIE_MOSSDEEP":734,"TRAINER_MAXIE_MT_CHIMNEY":602,"TRAINER_MAY_LILYCOVE_MUDKIP":664,"TRAINER_MAY_LILYCOVE_TORCHIC":666,"TRAINER_MAY_LILYCOVE_TREECKO":665,"TRAINER_MAY_PLACEHOLDER":854,"TRAINER_MAY_ROUTE_103_MUDKIP":529,"TRAINER_MAY_ROUTE_103_TORCHIC":535,"TRAINER_MAY_ROUTE_103_TREECKO":532,"TRAINER_MAY_ROUTE_110_MUDKIP":530,"TRAINER_MAY_ROUTE_110_TORCHIC":536,"TRAINER_MAY_ROUTE_110_TREECKO":533,"TRAINER_MAY_ROUTE_119_MUDKIP":531,"TRAINER_MAY_ROUTE_119_TORCHIC":537,"TRAINER_MAY_ROUTE_119_TREECKO":534,"TRAINER_MAY_RUSTBORO_MUDKIP":600,"TRAINER_MAY_RUSTBORO_TORCHIC":769,"TRAINER_MAY_RUSTBORO_TREECKO":768,"TRAINER_MELINA":755,"TRAINER_MELISSA":124,"TRAINER_MEL_AND_PAUL":680,"TRAINER_MICAH":255,"TRAINER_MICHELLE":98,"TRAINER_MIGUEL_1":293,"TRAINER_MIGUEL_2":295,"TRAINER_MIGUEL_3":296,"TRAINER_MIGUEL_4":297,"TRAINER_MIGUEL_5":298,"TRAINER_MIKE_1":634,"TRAINER_MIKE_2":635,"TRAINER_MISSY":447,"TRAINER_MITCHELL":540,"TRAINER_MIU_AND_YUKI":484,"TRAINER_MOLLIE":137,"TRAINER_MYLES":765,"TRAINER_NANCY":472,"TRAINER_NAOMI":119,"TRAINER_NATE":582,"TRAINER_NED":340,"TRAINER_NICHOLAS":585,"TRAINER_NICOLAS_1":392,"TRAINER_NICOLAS_2":393,"TRAINER_NICOLAS_3":394,"TRAINER_NICOLAS_4":395,"TRAINER_NICOLAS_5":396,"TRAINER_NIKKI":453,"TRAINER_NOB_1":183,"TRAINER_NOB_2":184,"TRAINER_NOB_3":185,"TRAINER_NOB_4":186,"TRAINER_NOB_5":187,"TRAINER_NOLAN":342,"TRAINER_NOLAND":809,"TRAINER_NOLEN":161,"TRAINER_NONE":0,"TRAINER_NORMAN_1":269,"TRAINER_NORMAN_2":786,"TRAINER_NORMAN_3":787,"TRAINER_NORMAN_4":788,"TRAINER_NORMAN_5":789,"TRAINER_OLIVIA":130,"TRAINER_OWEN":83,"TRAINER_PABLO_1":377,"TRAINER_PABLO_2":820,"TRAINER_PABLO_3":821,"TRAINER_PABLO_4":822,"TRAINER_PABLO_5":823,"TRAINER_PARKER":72,"TRAINER_PAT":766,"TRAINER_PATRICIA":105,"TRAINER_PAUL":275,"TRAINER_PAULA":429,"TRAINER_PAXTON":594,"TRAINER_PERRY":398,"TRAINER_PETE":735,"TRAINER_PHIL":400,"TRAINER_PHILLIP":494,"TRAINER_PHOEBE":262,"TRAINER_PRESLEY":403,"TRAINER_PRESTON":233,"TRAINER_QUINCY":324,"TRAINER_RACHEL":761,"TRAINER_RANDALL":71,"TRAINER_RED":851,"TRAINER_REED":675,"TRAINER_RELI_AND_IAN":686,"TRAINER_REYNA":509,"TRAINER_RHETT":703,"TRAINER_RICHARD":166,"TRAINER_RICK":615,"TRAINER_RICKY_1":64,"TRAINER_RICKY_2":67,"TRAINER_RICKY_3":68,"TRAINER_RICKY_4":69,"TRAINER_RICKY_5":70,"TRAINER_RILEY":653,"TRAINER_ROBERT_1":406,"TRAINER_ROBERT_2":409,"TRAINER_ROBERT_3":410,"TRAINER_ROBERT_4":411,"TRAINER_ROBERT_5":412,"TRAINER_ROBIN":612,"TRAINER_RODNEY":165,"TRAINER_ROGER":669,"TRAINER_ROLAND":160,"TRAINER_RONALD":350,"TRAINER_ROSE_1":37,"TRAINER_ROSE_2":40,"TRAINER_ROSE_3":41,"TRAINER_ROSE_4":42,"TRAINER_ROSE_5":43,"TRAINER_ROXANNE_1":265,"TRAINER_ROXANNE_2":770,"TRAINER_ROXANNE_3":771,"TRAINER_ROXANNE_4":772,"TRAINER_ROXANNE_5":773,"TRAINER_RUBEN":671,"TRAINER_SALLY":611,"TRAINER_SAMANTHA":245,"TRAINER_SAMUEL":81,"TRAINER_SANTIAGO":168,"TRAINER_SARAH":695,"TRAINER_SAWYER_1":1,"TRAINER_SAWYER_2":836,"TRAINER_SAWYER_3":837,"TRAINER_SAWYER_4":838,"TRAINER_SAWYER_5":839,"TRAINER_SEBASTIAN":554,"TRAINER_SHANE":214,"TRAINER_SHANNON":97,"TRAINER_SHARON":452,"TRAINER_SHAWN":194,"TRAINER_SHAYLA":747,"TRAINER_SHEILA":125,"TRAINER_SHELBY_1":313,"TRAINER_SHELBY_2":314,"TRAINER_SHELBY_3":315,"TRAINER_SHELBY_4":316,"TRAINER_SHELBY_5":317,"TRAINER_SHELLY_SEAFLOOR_CAVERN":33,"TRAINER_SHELLY_WEATHER_INSTITUTE":32,"TRAINER_SHIRLEY":126,"TRAINER_SIDNEY":261,"TRAINER_SIENNA":459,"TRAINER_SIMON":65,"TRAINER_SOPHIA":561,"TRAINER_SOPHIE":708,"TRAINER_SPENCER":159,"TRAINER_SPENSER":807,"TRAINER_STAN":162,"TRAINER_STEVEN":804,"TRAINER_STEVE_1":143,"TRAINER_STEVE_2":147,"TRAINER_STEVE_3":148,"TRAINER_STEVE_4":149,"TRAINER_STEVE_5":150,"TRAINER_SUSIE":456,"TRAINER_SYLVIA":575,"TRAINER_TABITHA_MAGMA_HIDEOUT":732,"TRAINER_TABITHA_MOSSDEEP":514,"TRAINER_TABITHA_MT_CHIMNEY":597,"TRAINER_TAKAO":179,"TRAINER_TAKASHI":416,"TRAINER_TALIA":385,"TRAINER_TAMMY":107,"TRAINER_TANYA":451,"TRAINER_TARA":446,"TRAINER_TASHA":109,"TRAINER_TATE_AND_LIZA_1":271,"TRAINER_TATE_AND_LIZA_2":794,"TRAINER_TATE_AND_LIZA_3":795,"TRAINER_TATE_AND_LIZA_4":796,"TRAINER_TATE_AND_LIZA_5":797,"TRAINER_TAYLOR":225,"TRAINER_TED":274,"TRAINER_TERRY":581,"TRAINER_THALIA_1":144,"TRAINER_THALIA_2":844,"TRAINER_THALIA_3":845,"TRAINER_THALIA_4":846,"TRAINER_THALIA_5":847,"TRAINER_THOMAS":256,"TRAINER_TIANA":603,"TRAINER_TIFFANY":131,"TRAINER_TIMMY":334,"TRAINER_TIMOTHY_1":307,"TRAINER_TIMOTHY_2":308,"TRAINER_TIMOTHY_3":309,"TRAINER_TIMOTHY_4":310,"TRAINER_TIMOTHY_5":311,"TRAINER_TISHA":676,"TRAINER_TOMMY":321,"TRAINER_TONY_1":155,"TRAINER_TONY_2":175,"TRAINER_TONY_3":176,"TRAINER_TONY_4":177,"TRAINER_TONY_5":178,"TRAINER_TORI_AND_TIA":677,"TRAINER_TRAVIS":218,"TRAINER_TRENT_1":627,"TRAINER_TRENT_2":636,"TRAINER_TRENT_3":637,"TRAINER_TRENT_4":638,"TRAINER_TRENT_5":639,"TRAINER_TUCKER":806,"TRAINER_TYRA_AND_IVY":679,"TRAINER_TYRON":704,"TRAINER_VALERIE_1":108,"TRAINER_VALERIE_2":110,"TRAINER_VALERIE_3":111,"TRAINER_VALERIE_4":112,"TRAINER_VALERIE_5":113,"TRAINER_VANESSA":300,"TRAINER_VICKY":312,"TRAINER_VICTOR":292,"TRAINER_VICTORIA":299,"TRAINER_VINCENT":76,"TRAINER_VIOLET":39,"TRAINER_VIRGIL":234,"TRAINER_VITO":82,"TRAINER_VIVI":606,"TRAINER_VIVIAN":649,"TRAINER_WADE":344,"TRAINER_WALLACE":335,"TRAINER_WALLY_MAUVILLE":656,"TRAINER_WALLY_VR_1":519,"TRAINER_WALLY_VR_2":657,"TRAINER_WALLY_VR_3":658,"TRAINER_WALLY_VR_4":659,"TRAINER_WALLY_VR_5":660,"TRAINER_WALTER_1":254,"TRAINER_WALTER_2":257,"TRAINER_WALTER_3":258,"TRAINER_WALTER_4":259,"TRAINER_WALTER_5":260,"TRAINER_WARREN":88,"TRAINER_WATTSON_1":267,"TRAINER_WATTSON_2":778,"TRAINER_WATTSON_3":779,"TRAINER_WATTSON_4":780,"TRAINER_WATTSON_5":781,"TRAINER_WAYNE":673,"TRAINER_WENDY":92,"TRAINER_WILLIAM":236,"TRAINER_WILTON_1":78,"TRAINER_WILTON_2":84,"TRAINER_WILTON_3":85,"TRAINER_WILTON_4":86,"TRAINER_WILTON_5":87,"TRAINER_WINONA_1":270,"TRAINER_WINONA_2":790,"TRAINER_WINONA_3":791,"TRAINER_WINONA_4":792,"TRAINER_WINONA_5":793,"TRAINER_WINSTON_1":136,"TRAINER_WINSTON_2":139,"TRAINER_WINSTON_3":140,"TRAINER_WINSTON_4":141,"TRAINER_WINSTON_5":142,"TRAINER_WYATT":711,"TRAINER_YASU":415,"TRAINER_YUJI":188,"TRAINER_ZANDER":31},"legendary_encounters":[{"address":2538600,"catch_flag":429,"defeat_flag":428,"level":30,"species":410},{"address":2354334,"catch_flag":480,"defeat_flag":447,"level":70,"species":405},{"address":2543160,"catch_flag":146,"defeat_flag":476,"level":70,"species":250},{"address":2354112,"catch_flag":479,"defeat_flag":446,"level":70,"species":404},{"address":2385623,"catch_flag":457,"defeat_flag":456,"level":50,"species":407},{"address":2385687,"catch_flag":482,"defeat_flag":481,"level":50,"species":408},{"address":2543443,"catch_flag":145,"defeat_flag":477,"level":70,"species":249},{"address":2538177,"catch_flag":458,"defeat_flag":455,"level":30,"species":151},{"address":2347488,"catch_flag":478,"defeat_flag":448,"level":70,"species":406},{"address":2345460,"catch_flag":427,"defeat_flag":444,"level":40,"species":402},{"address":2298183,"catch_flag":426,"defeat_flag":443,"level":40,"species":401},{"address":2345731,"catch_flag":483,"defeat_flag":445,"level":40,"species":403}],"locations":{"BADGE_1":{"address":2188036,"default_item":226,"flag":1182},"BADGE_2":{"address":2095131,"default_item":227,"flag":1183},"BADGE_3":{"address":2167252,"default_item":228,"flag":1184},"BADGE_4":{"address":2103246,"default_item":229,"flag":1185},"BADGE_5":{"address":2129781,"default_item":230,"flag":1186},"BADGE_6":{"address":2202122,"default_item":231,"flag":1187},"BADGE_7":{"address":2243964,"default_item":232,"flag":1188},"BADGE_8":{"address":2262314,"default_item":233,"flag":1189},"BERRY_TREE_01":{"address":5843562,"default_item":135,"flag":612},"BERRY_TREE_02":{"address":5843564,"default_item":139,"flag":613},"BERRY_TREE_03":{"address":5843566,"default_item":142,"flag":614},"BERRY_TREE_04":{"address":5843568,"default_item":139,"flag":615},"BERRY_TREE_05":{"address":5843570,"default_item":133,"flag":616},"BERRY_TREE_06":{"address":5843572,"default_item":138,"flag":617},"BERRY_TREE_07":{"address":5843574,"default_item":133,"flag":618},"BERRY_TREE_08":{"address":5843576,"default_item":133,"flag":619},"BERRY_TREE_09":{"address":5843578,"default_item":142,"flag":620},"BERRY_TREE_10":{"address":5843580,"default_item":138,"flag":621},"BERRY_TREE_11":{"address":5843582,"default_item":139,"flag":622},"BERRY_TREE_12":{"address":5843584,"default_item":142,"flag":623},"BERRY_TREE_13":{"address":5843586,"default_item":135,"flag":624},"BERRY_TREE_14":{"address":5843588,"default_item":155,"flag":625},"BERRY_TREE_15":{"address":5843590,"default_item":153,"flag":626},"BERRY_TREE_16":{"address":5843592,"default_item":150,"flag":627},"BERRY_TREE_17":{"address":5843594,"default_item":150,"flag":628},"BERRY_TREE_18":{"address":5843596,"default_item":150,"flag":629},"BERRY_TREE_19":{"address":5843598,"default_item":148,"flag":630},"BERRY_TREE_20":{"address":5843600,"default_item":148,"flag":631},"BERRY_TREE_21":{"address":5843602,"default_item":136,"flag":632},"BERRY_TREE_22":{"address":5843604,"default_item":135,"flag":633},"BERRY_TREE_23":{"address":5843606,"default_item":135,"flag":634},"BERRY_TREE_24":{"address":5843608,"default_item":136,"flag":635},"BERRY_TREE_25":{"address":5843610,"default_item":152,"flag":636},"BERRY_TREE_26":{"address":5843612,"default_item":134,"flag":637},"BERRY_TREE_27":{"address":5843614,"default_item":151,"flag":638},"BERRY_TREE_28":{"address":5843616,"default_item":151,"flag":639},"BERRY_TREE_29":{"address":5843618,"default_item":151,"flag":640},"BERRY_TREE_30":{"address":5843620,"default_item":153,"flag":641},"BERRY_TREE_31":{"address":5843622,"default_item":142,"flag":642},"BERRY_TREE_32":{"address":5843624,"default_item":142,"flag":643},"BERRY_TREE_33":{"address":5843626,"default_item":142,"flag":644},"BERRY_TREE_34":{"address":5843628,"default_item":153,"flag":645},"BERRY_TREE_35":{"address":5843630,"default_item":153,"flag":646},"BERRY_TREE_36":{"address":5843632,"default_item":153,"flag":647},"BERRY_TREE_37":{"address":5843634,"default_item":137,"flag":648},"BERRY_TREE_38":{"address":5843636,"default_item":137,"flag":649},"BERRY_TREE_39":{"address":5843638,"default_item":137,"flag":650},"BERRY_TREE_40":{"address":5843640,"default_item":135,"flag":651},"BERRY_TREE_41":{"address":5843642,"default_item":135,"flag":652},"BERRY_TREE_42":{"address":5843644,"default_item":135,"flag":653},"BERRY_TREE_43":{"address":5843646,"default_item":148,"flag":654},"BERRY_TREE_44":{"address":5843648,"default_item":150,"flag":655},"BERRY_TREE_45":{"address":5843650,"default_item":152,"flag":656},"BERRY_TREE_46":{"address":5843652,"default_item":151,"flag":657},"BERRY_TREE_47":{"address":5843654,"default_item":140,"flag":658},"BERRY_TREE_48":{"address":5843656,"default_item":137,"flag":659},"BERRY_TREE_49":{"address":5843658,"default_item":136,"flag":660},"BERRY_TREE_50":{"address":5843660,"default_item":134,"flag":661},"BERRY_TREE_51":{"address":5843662,"default_item":142,"flag":662},"BERRY_TREE_52":{"address":5843664,"default_item":150,"flag":663},"BERRY_TREE_53":{"address":5843666,"default_item":150,"flag":664},"BERRY_TREE_54":{"address":5843668,"default_item":142,"flag":665},"BERRY_TREE_55":{"address":5843670,"default_item":149,"flag":666},"BERRY_TREE_56":{"address":5843672,"default_item":149,"flag":667},"BERRY_TREE_57":{"address":5843674,"default_item":136,"flag":668},"BERRY_TREE_58":{"address":5843676,"default_item":153,"flag":669},"BERRY_TREE_59":{"address":5843678,"default_item":153,"flag":670},"BERRY_TREE_60":{"address":5843680,"default_item":157,"flag":671},"BERRY_TREE_61":{"address":5843682,"default_item":157,"flag":672},"BERRY_TREE_62":{"address":5843684,"default_item":138,"flag":673},"BERRY_TREE_63":{"address":5843686,"default_item":142,"flag":674},"BERRY_TREE_64":{"address":5843688,"default_item":138,"flag":675},"BERRY_TREE_65":{"address":5843690,"default_item":157,"flag":676},"BERRY_TREE_66":{"address":5843692,"default_item":134,"flag":677},"BERRY_TREE_67":{"address":5843694,"default_item":152,"flag":678},"BERRY_TREE_68":{"address":5843696,"default_item":140,"flag":679},"BERRY_TREE_69":{"address":5843698,"default_item":154,"flag":680},"BERRY_TREE_70":{"address":5843700,"default_item":154,"flag":681},"BERRY_TREE_71":{"address":5843702,"default_item":154,"flag":682},"BERRY_TREE_72":{"address":5843704,"default_item":157,"flag":683},"BERRY_TREE_73":{"address":5843706,"default_item":155,"flag":684},"BERRY_TREE_74":{"address":5843708,"default_item":155,"flag":685},"BERRY_TREE_75":{"address":5843710,"default_item":142,"flag":686},"BERRY_TREE_76":{"address":5843712,"default_item":133,"flag":687},"BERRY_TREE_77":{"address":5843714,"default_item":140,"flag":688},"BERRY_TREE_78":{"address":5843716,"default_item":140,"flag":689},"BERRY_TREE_79":{"address":5843718,"default_item":155,"flag":690},"BERRY_TREE_80":{"address":5843720,"default_item":139,"flag":691},"BERRY_TREE_81":{"address":5843722,"default_item":139,"flag":692},"BERRY_TREE_82":{"address":5843724,"default_item":168,"flag":693},"BERRY_TREE_83":{"address":5843726,"default_item":156,"flag":694},"BERRY_TREE_84":{"address":5843728,"default_item":156,"flag":695},"BERRY_TREE_85":{"address":5843730,"default_item":142,"flag":696},"BERRY_TREE_86":{"address":5843732,"default_item":138,"flag":697},"BERRY_TREE_87":{"address":5843734,"default_item":135,"flag":698},"BERRY_TREE_88":{"address":5843736,"default_item":142,"flag":699},"HIDDEN_ITEM_ABANDONED_SHIP_RM_1_KEY":{"address":5497200,"default_item":281,"flag":531},"HIDDEN_ITEM_ABANDONED_SHIP_RM_2_KEY":{"address":5497212,"default_item":282,"flag":532},"HIDDEN_ITEM_ABANDONED_SHIP_RM_4_KEY":{"address":5497224,"default_item":283,"flag":533},"HIDDEN_ITEM_ABANDONED_SHIP_RM_6_KEY":{"address":5497236,"default_item":284,"flag":534},"HIDDEN_ITEM_ARTISAN_CAVE_B1F_CALCIUM":{"address":5500100,"default_item":67,"flag":601},"HIDDEN_ITEM_ARTISAN_CAVE_B1F_IRON":{"address":5500124,"default_item":65,"flag":604},"HIDDEN_ITEM_ARTISAN_CAVE_B1F_PROTEIN":{"address":5500112,"default_item":64,"flag":603},"HIDDEN_ITEM_ARTISAN_CAVE_B1F_ZINC":{"address":5500088,"default_item":70,"flag":602},"HIDDEN_ITEM_FALLARBOR_TOWN_NUGGET":{"address":5435924,"default_item":110,"flag":528},"HIDDEN_ITEM_GRANITE_CAVE_B2F_EVERSTONE_1":{"address":5487372,"default_item":195,"flag":548},"HIDDEN_ITEM_GRANITE_CAVE_B2F_EVERSTONE_2":{"address":5487384,"default_item":195,"flag":549},"HIDDEN_ITEM_JAGGED_PASS_FULL_HEAL":{"address":5489116,"default_item":23,"flag":577},"HIDDEN_ITEM_JAGGED_PASS_GREAT_BALL":{"address":5489128,"default_item":3,"flag":576},"HIDDEN_ITEM_LAVARIDGE_TOWN_ICE_HEAL":{"address":5435672,"default_item":16,"flag":500},"HIDDEN_ITEM_LILYCOVE_CITY_HEART_SCALE":{"address":5432608,"default_item":111,"flag":527},"HIDDEN_ITEM_LILYCOVE_CITY_POKE_BALL":{"address":5432632,"default_item":4,"flag":575},"HIDDEN_ITEM_LILYCOVE_CITY_PP_UP":{"address":5432620,"default_item":69,"flag":543},"HIDDEN_ITEM_MT_PYRE_EXTERIOR_MAX_ETHER":{"address":5490440,"default_item":35,"flag":578},"HIDDEN_ITEM_MT_PYRE_EXTERIOR_ULTRA_BALL":{"address":5490428,"default_item":2,"flag":529},"HIDDEN_ITEM_MT_PYRE_SUMMIT_RARE_CANDY":{"address":5490796,"default_item":68,"flag":580},"HIDDEN_ITEM_MT_PYRE_SUMMIT_ZINC":{"address":5490784,"default_item":70,"flag":579},"HIDDEN_ITEM_NAVEL_ROCK_TOP_SACRED_ASH":{"address":5525804,"default_item":45,"flag":609},"HIDDEN_ITEM_PETALBURG_CITY_RARE_CANDY":{"address":5428972,"default_item":68,"flag":595},"HIDDEN_ITEM_PETALBURG_WOODS_POKE_BALL":{"address":5487908,"default_item":4,"flag":561},"HIDDEN_ITEM_PETALBURG_WOODS_POTION":{"address":5487872,"default_item":13,"flag":558},"HIDDEN_ITEM_PETALBURG_WOODS_TINY_MUSHROOM_1":{"address":5487884,"default_item":103,"flag":559},"HIDDEN_ITEM_PETALBURG_WOODS_TINY_MUSHROOM_2":{"address":5487896,"default_item":103,"flag":560},"HIDDEN_ITEM_ROUTE_104_ANTIDOTE":{"address":5438492,"default_item":14,"flag":585},"HIDDEN_ITEM_ROUTE_104_HEART_SCALE":{"address":5438504,"default_item":111,"flag":588},"HIDDEN_ITEM_ROUTE_104_POKE_BALL":{"address":5438468,"default_item":4,"flag":562},"HIDDEN_ITEM_ROUTE_104_POTION":{"address":5438480,"default_item":13,"flag":537},"HIDDEN_ITEM_ROUTE_104_SUPER_POTION":{"address":5438456,"default_item":22,"flag":544},"HIDDEN_ITEM_ROUTE_105_BIG_PEARL":{"address":5438748,"default_item":107,"flag":611},"HIDDEN_ITEM_ROUTE_105_HEART_SCALE":{"address":5438736,"default_item":111,"flag":589},"HIDDEN_ITEM_ROUTE_106_HEART_SCALE":{"address":5438932,"default_item":111,"flag":547},"HIDDEN_ITEM_ROUTE_106_POKE_BALL":{"address":5438908,"default_item":4,"flag":563},"HIDDEN_ITEM_ROUTE_106_STARDUST":{"address":5438920,"default_item":108,"flag":546},"HIDDEN_ITEM_ROUTE_108_RARE_CANDY":{"address":5439340,"default_item":68,"flag":586},"HIDDEN_ITEM_ROUTE_109_ETHER":{"address":5440016,"default_item":34,"flag":564},"HIDDEN_ITEM_ROUTE_109_GREAT_BALL":{"address":5440004,"default_item":3,"flag":551},"HIDDEN_ITEM_ROUTE_109_HEART_SCALE_1":{"address":5439992,"default_item":111,"flag":552},"HIDDEN_ITEM_ROUTE_109_HEART_SCALE_2":{"address":5440028,"default_item":111,"flag":590},"HIDDEN_ITEM_ROUTE_109_HEART_SCALE_3":{"address":5440040,"default_item":111,"flag":591},"HIDDEN_ITEM_ROUTE_109_REVIVE":{"address":5439980,"default_item":24,"flag":550},"HIDDEN_ITEM_ROUTE_110_FULL_HEAL":{"address":5441308,"default_item":23,"flag":555},"HIDDEN_ITEM_ROUTE_110_GREAT_BALL":{"address":5441284,"default_item":3,"flag":553},"HIDDEN_ITEM_ROUTE_110_POKE_BALL":{"address":5441296,"default_item":4,"flag":565},"HIDDEN_ITEM_ROUTE_110_REVIVE":{"address":5441272,"default_item":24,"flag":554},"HIDDEN_ITEM_ROUTE_111_PROTEIN":{"address":5443220,"default_item":64,"flag":556},"HIDDEN_ITEM_ROUTE_111_RARE_CANDY":{"address":5443232,"default_item":68,"flag":557},"HIDDEN_ITEM_ROUTE_111_STARDUST":{"address":5443160,"default_item":108,"flag":502},"HIDDEN_ITEM_ROUTE_113_ETHER":{"address":5444488,"default_item":34,"flag":503},"HIDDEN_ITEM_ROUTE_113_NUGGET":{"address":5444512,"default_item":110,"flag":598},"HIDDEN_ITEM_ROUTE_113_TM_DOUBLE_TEAM":{"address":5444500,"default_item":320,"flag":530},"HIDDEN_ITEM_ROUTE_114_CARBOS":{"address":5445340,"default_item":66,"flag":504},"HIDDEN_ITEM_ROUTE_114_REVIVE":{"address":5445364,"default_item":24,"flag":542},"HIDDEN_ITEM_ROUTE_115_HEART_SCALE":{"address":5446176,"default_item":111,"flag":597},"HIDDEN_ITEM_ROUTE_116_BLACK_GLASSES":{"address":5447056,"default_item":206,"flag":596},"HIDDEN_ITEM_ROUTE_116_SUPER_POTION":{"address":5447044,"default_item":22,"flag":545},"HIDDEN_ITEM_ROUTE_117_REPEL":{"address":5447708,"default_item":86,"flag":572},"HIDDEN_ITEM_ROUTE_118_HEART_SCALE":{"address":5448404,"default_item":111,"flag":566},"HIDDEN_ITEM_ROUTE_118_IRON":{"address":5448392,"default_item":65,"flag":567},"HIDDEN_ITEM_ROUTE_119_CALCIUM":{"address":5449972,"default_item":67,"flag":505},"HIDDEN_ITEM_ROUTE_119_FULL_HEAL":{"address":5450056,"default_item":23,"flag":568},"HIDDEN_ITEM_ROUTE_119_MAX_ETHER":{"address":5450068,"default_item":35,"flag":587},"HIDDEN_ITEM_ROUTE_119_ULTRA_BALL":{"address":5449984,"default_item":2,"flag":506},"HIDDEN_ITEM_ROUTE_120_RARE_CANDY_1":{"address":5451596,"default_item":68,"flag":571},"HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2":{"address":5451620,"default_item":68,"flag":569},"HIDDEN_ITEM_ROUTE_120_REVIVE":{"address":5451608,"default_item":24,"flag":584},"HIDDEN_ITEM_ROUTE_120_ZINC":{"address":5451632,"default_item":70,"flag":570},"HIDDEN_ITEM_ROUTE_121_FULL_HEAL":{"address":5452540,"default_item":23,"flag":573},"HIDDEN_ITEM_ROUTE_121_HP_UP":{"address":5452516,"default_item":63,"flag":539},"HIDDEN_ITEM_ROUTE_121_MAX_REVIVE":{"address":5452552,"default_item":25,"flag":600},"HIDDEN_ITEM_ROUTE_121_NUGGET":{"address":5452528,"default_item":110,"flag":540},"HIDDEN_ITEM_ROUTE_123_HYPER_POTION":{"address":5454100,"default_item":21,"flag":574},"HIDDEN_ITEM_ROUTE_123_PP_UP":{"address":5454112,"default_item":69,"flag":599},"HIDDEN_ITEM_ROUTE_123_RARE_CANDY":{"address":5454124,"default_item":68,"flag":610},"HIDDEN_ITEM_ROUTE_123_REVIVE":{"address":5454088,"default_item":24,"flag":541},"HIDDEN_ITEM_ROUTE_123_SUPER_REPEL":{"address":5454052,"default_item":83,"flag":507},"HIDDEN_ITEM_ROUTE_128_HEART_SCALE_1":{"address":5455620,"default_item":111,"flag":592},"HIDDEN_ITEM_ROUTE_128_HEART_SCALE_2":{"address":5455632,"default_item":111,"flag":593},"HIDDEN_ITEM_ROUTE_128_HEART_SCALE_3":{"address":5455644,"default_item":111,"flag":594},"HIDDEN_ITEM_SAFARI_ZONE_NORTH_EAST_RARE_CANDY":{"address":5517256,"default_item":68,"flag":606},"HIDDEN_ITEM_SAFARI_ZONE_NORTH_EAST_ZINC":{"address":5517268,"default_item":70,"flag":607},"HIDDEN_ITEM_SAFARI_ZONE_SOUTH_EAST_FULL_RESTORE":{"address":5517432,"default_item":19,"flag":605},"HIDDEN_ITEM_SAFARI_ZONE_SOUTH_EAST_PP_UP":{"address":5517420,"default_item":69,"flag":608},"HIDDEN_ITEM_SS_TIDAL_LOWER_DECK_LEFTOVERS":{"address":5511292,"default_item":200,"flag":535},"HIDDEN_ITEM_TRICK_HOUSE_NUGGET":{"address":5526716,"default_item":110,"flag":501},"HIDDEN_ITEM_UNDERWATER_124_BIG_PEARL":{"address":5456992,"default_item":107,"flag":511},"HIDDEN_ITEM_UNDERWATER_124_CALCIUM":{"address":5457016,"default_item":67,"flag":536},"HIDDEN_ITEM_UNDERWATER_124_CARBOS":{"address":5456956,"default_item":66,"flag":508},"HIDDEN_ITEM_UNDERWATER_124_GREEN_SHARD":{"address":5456968,"default_item":51,"flag":509},"HIDDEN_ITEM_UNDERWATER_124_HEART_SCALE_1":{"address":5457004,"default_item":111,"flag":513},"HIDDEN_ITEM_UNDERWATER_124_HEART_SCALE_2":{"address":5457028,"default_item":111,"flag":538},"HIDDEN_ITEM_UNDERWATER_124_PEARL":{"address":5456980,"default_item":106,"flag":510},"HIDDEN_ITEM_UNDERWATER_126_BIG_PEARL":{"address":5457140,"default_item":107,"flag":520},"HIDDEN_ITEM_UNDERWATER_126_BLUE_SHARD":{"address":5457152,"default_item":49,"flag":512},"HIDDEN_ITEM_UNDERWATER_126_HEART_SCALE":{"address":5457068,"default_item":111,"flag":514},"HIDDEN_ITEM_UNDERWATER_126_IRON":{"address":5457116,"default_item":65,"flag":519},"HIDDEN_ITEM_UNDERWATER_126_PEARL":{"address":5457104,"default_item":106,"flag":517},"HIDDEN_ITEM_UNDERWATER_126_STARDUST":{"address":5457092,"default_item":108,"flag":516},"HIDDEN_ITEM_UNDERWATER_126_ULTRA_BALL":{"address":5457080,"default_item":2,"flag":515},"HIDDEN_ITEM_UNDERWATER_126_YELLOW_SHARD":{"address":5457128,"default_item":50,"flag":518},"HIDDEN_ITEM_UNDERWATER_127_HEART_SCALE":{"address":5457224,"default_item":111,"flag":523},"HIDDEN_ITEM_UNDERWATER_127_HP_UP":{"address":5457212,"default_item":63,"flag":522},"HIDDEN_ITEM_UNDERWATER_127_RED_SHARD":{"address":5457236,"default_item":48,"flag":524},"HIDDEN_ITEM_UNDERWATER_127_STAR_PIECE":{"address":5457200,"default_item":109,"flag":521},"HIDDEN_ITEM_UNDERWATER_128_PEARL":{"address":5457288,"default_item":106,"flag":526},"HIDDEN_ITEM_UNDERWATER_128_PROTEIN":{"address":5457276,"default_item":64,"flag":525},"HIDDEN_ITEM_VICTORY_ROAD_1F_ULTRA_BALL":{"address":5493932,"default_item":2,"flag":581},"HIDDEN_ITEM_VICTORY_ROAD_B2F_ELIXIR":{"address":5494744,"default_item":36,"flag":582},"HIDDEN_ITEM_VICTORY_ROAD_B2F_MAX_REPEL":{"address":5494756,"default_item":84,"flag":583},"ITEM_ABANDONED_SHIP_CAPTAINS_OFFICE_STORAGE_KEY":{"address":2709805,"default_item":285,"flag":1100},"ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_1_TM_RAIN_DANCE":{"address":2709857,"default_item":306,"flag":1102},"ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_2_SCANNER":{"address":2709831,"default_item":278,"flag":1078},"ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_3_WATER_STONE":{"address":2709844,"default_item":97,"flag":1101},"ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_6_LUXURY_BALL":{"address":2709818,"default_item":11,"flag":1077},"ITEM_ABANDONED_SHIP_ROOMS_1F_HARBOR_MAIL":{"address":2709740,"default_item":122,"flag":1095},"ITEM_ABANDONED_SHIP_ROOMS_2_1F_REVIVE":{"address":2709792,"default_item":24,"flag":1099},"ITEM_ABANDONED_SHIP_ROOMS_2_B1F_DIVE_BALL":{"address":2709766,"default_item":7,"flag":1097},"ITEM_ABANDONED_SHIP_ROOMS_B1F_ESCAPE_ROPE":{"address":2709753,"default_item":85,"flag":1096},"ITEM_ABANDONED_SHIP_ROOMS_B1F_TM_ICE_BEAM":{"address":2709779,"default_item":301,"flag":1098},"ITEM_AQUA_HIDEOUT_B1F_MASTER_BALL":{"address":2710039,"default_item":1,"flag":1124},"ITEM_AQUA_HIDEOUT_B1F_MAX_ELIXIR":{"address":2710065,"default_item":37,"flag":1071},"ITEM_AQUA_HIDEOUT_B1F_NUGGET":{"address":2710052,"default_item":110,"flag":1132},"ITEM_AQUA_HIDEOUT_B2F_NEST_BALL":{"address":2710078,"default_item":8,"flag":1072},"ITEM_ARTISAN_CAVE_1F_CARBOS":{"address":2710416,"default_item":66,"flag":1163},"ITEM_ARTISAN_CAVE_B1F_HP_UP":{"address":2710403,"default_item":63,"flag":1162},"ITEM_FIERY_PATH_FIRE_STONE":{"address":2709584,"default_item":95,"flag":1111},"ITEM_FIERY_PATH_TM_TOXIC":{"address":2709597,"default_item":294,"flag":1091},"ITEM_GRANITE_CAVE_1F_ESCAPE_ROPE":{"address":2709519,"default_item":85,"flag":1050},"ITEM_GRANITE_CAVE_B1F_POKE_BALL":{"address":2709532,"default_item":4,"flag":1051},"ITEM_GRANITE_CAVE_B2F_RARE_CANDY":{"address":2709558,"default_item":68,"flag":1054},"ITEM_GRANITE_CAVE_B2F_REPEL":{"address":2709545,"default_item":86,"flag":1053},"ITEM_JAGGED_PASS_BURN_HEAL":{"address":2709571,"default_item":15,"flag":1070},"ITEM_LILYCOVE_CITY_MAX_REPEL":{"address":2709415,"default_item":84,"flag":1042},"ITEM_MAGMA_HIDEOUT_1F_RARE_CANDY":{"address":2710429,"default_item":68,"flag":1151},"ITEM_MAGMA_HIDEOUT_2F_2R_FULL_RESTORE":{"address":2710455,"default_item":19,"flag":1165},"ITEM_MAGMA_HIDEOUT_2F_2R_MAX_ELIXIR":{"address":2710442,"default_item":37,"flag":1164},"ITEM_MAGMA_HIDEOUT_3F_1R_NUGGET":{"address":2710468,"default_item":110,"flag":1166},"ITEM_MAGMA_HIDEOUT_3F_2R_PP_MAX":{"address":2710481,"default_item":71,"flag":1167},"ITEM_MAGMA_HIDEOUT_3F_3R_ECAPE_ROPE":{"address":2710507,"default_item":85,"flag":1059},"ITEM_MAGMA_HIDEOUT_4F_MAX_REVIVE":{"address":2710494,"default_item":25,"flag":1168},"ITEM_MAUVILLE_CITY_X_SPEED":{"address":2709389,"default_item":77,"flag":1116},"ITEM_METEOR_FALLS_1F_1R_FULL_HEAL":{"address":2709623,"default_item":23,"flag":1045},"ITEM_METEOR_FALLS_1F_1R_MOON_STONE":{"address":2709636,"default_item":94,"flag":1046},"ITEM_METEOR_FALLS_1F_1R_PP_UP":{"address":2709649,"default_item":69,"flag":1047},"ITEM_METEOR_FALLS_1F_1R_TM_IRON_TAIL":{"address":2709610,"default_item":311,"flag":1044},"ITEM_METEOR_FALLS_B1F_2R_TM_DRAGON_CLAW":{"address":2709662,"default_item":290,"flag":1080},"ITEM_MOSSDEEP_CITY_NET_BALL":{"address":2709428,"default_item":6,"flag":1043},"ITEM_MT_PYRE_2F_ULTRA_BALL":{"address":2709948,"default_item":2,"flag":1129},"ITEM_MT_PYRE_3F_SUPER_REPEL":{"address":2709961,"default_item":83,"flag":1120},"ITEM_MT_PYRE_4F_SEA_INCENSE":{"address":2709974,"default_item":220,"flag":1130},"ITEM_MT_PYRE_5F_LAX_INCENSE":{"address":2709987,"default_item":221,"flag":1052},"ITEM_MT_PYRE_6F_TM_SHADOW_BALL":{"address":2710000,"default_item":318,"flag":1089},"ITEM_MT_PYRE_EXTERIOR_MAX_POTION":{"address":2710013,"default_item":20,"flag":1073},"ITEM_MT_PYRE_EXTERIOR_TM_SKILL_SWAP":{"address":2710026,"default_item":336,"flag":1074},"ITEM_NEW_MAUVILLE_ESCAPE_ROPE":{"address":2709688,"default_item":85,"flag":1076},"ITEM_NEW_MAUVILLE_FULL_HEAL":{"address":2709714,"default_item":23,"flag":1122},"ITEM_NEW_MAUVILLE_PARALYZE_HEAL":{"address":2709727,"default_item":18,"flag":1123},"ITEM_NEW_MAUVILLE_THUNDER_STONE":{"address":2709701,"default_item":96,"flag":1110},"ITEM_NEW_MAUVILLE_ULTRA_BALL":{"address":2709675,"default_item":2,"flag":1075},"ITEM_PETALBURG_CITY_ETHER":{"address":2709376,"default_item":34,"flag":1040},"ITEM_PETALBURG_CITY_MAX_REVIVE":{"address":2709363,"default_item":25,"flag":1039},"ITEM_PETALBURG_WOODS_ETHER":{"address":2709467,"default_item":34,"flag":1058},"ITEM_PETALBURG_WOODS_GREAT_BALL":{"address":2709454,"default_item":3,"flag":1056},"ITEM_PETALBURG_WOODS_PARALYZE_HEAL":{"address":2709480,"default_item":18,"flag":1117},"ITEM_PETALBURG_WOODS_X_ATTACK":{"address":2709441,"default_item":75,"flag":1055},"ITEM_ROUTE_102_POTION":{"address":2708375,"default_item":13,"flag":1000},"ITEM_ROUTE_103_GUARD_SPEC":{"address":2708388,"default_item":73,"flag":1114},"ITEM_ROUTE_103_PP_UP":{"address":2708401,"default_item":69,"flag":1137},"ITEM_ROUTE_104_POKE_BALL":{"address":2708427,"default_item":4,"flag":1057},"ITEM_ROUTE_104_POTION":{"address":2708453,"default_item":13,"flag":1135},"ITEM_ROUTE_104_PP_UP":{"address":2708414,"default_item":69,"flag":1002},"ITEM_ROUTE_104_X_ACCURACY":{"address":2708440,"default_item":78,"flag":1115},"ITEM_ROUTE_105_IRON":{"address":2708466,"default_item":65,"flag":1003},"ITEM_ROUTE_106_PROTEIN":{"address":2708479,"default_item":64,"flag":1004},"ITEM_ROUTE_108_STAR_PIECE":{"address":2708492,"default_item":109,"flag":1139},"ITEM_ROUTE_109_POTION":{"address":2708518,"default_item":13,"flag":1140},"ITEM_ROUTE_109_PP_UP":{"address":2708505,"default_item":69,"flag":1005},"ITEM_ROUTE_110_DIRE_HIT":{"address":2708544,"default_item":74,"flag":1007},"ITEM_ROUTE_110_ELIXIR":{"address":2708557,"default_item":36,"flag":1141},"ITEM_ROUTE_110_RARE_CANDY":{"address":2708531,"default_item":68,"flag":1006},"ITEM_ROUTE_111_ELIXIR":{"address":2708609,"default_item":36,"flag":1142},"ITEM_ROUTE_111_HP_UP":{"address":2708596,"default_item":63,"flag":1010},"ITEM_ROUTE_111_STARDUST":{"address":2708583,"default_item":108,"flag":1009},"ITEM_ROUTE_111_TM_SANDSTORM":{"address":2708570,"default_item":325,"flag":1008},"ITEM_ROUTE_112_NUGGET":{"address":2708622,"default_item":110,"flag":1011},"ITEM_ROUTE_113_HYPER_POTION":{"address":2708661,"default_item":21,"flag":1143},"ITEM_ROUTE_113_MAX_ETHER":{"address":2708635,"default_item":35,"flag":1012},"ITEM_ROUTE_113_SUPER_REPEL":{"address":2708648,"default_item":83,"flag":1013},"ITEM_ROUTE_114_ENERGY_POWDER":{"address":2708700,"default_item":30,"flag":1160},"ITEM_ROUTE_114_PROTEIN":{"address":2708687,"default_item":64,"flag":1015},"ITEM_ROUTE_114_RARE_CANDY":{"address":2708674,"default_item":68,"flag":1014},"ITEM_ROUTE_115_GREAT_BALL":{"address":2708752,"default_item":3,"flag":1118},"ITEM_ROUTE_115_HEAL_POWDER":{"address":2708765,"default_item":32,"flag":1144},"ITEM_ROUTE_115_IRON":{"address":2708739,"default_item":65,"flag":1018},"ITEM_ROUTE_115_PP_UP":{"address":2708778,"default_item":69,"flag":1161},"ITEM_ROUTE_115_SUPER_POTION":{"address":2708713,"default_item":22,"flag":1016},"ITEM_ROUTE_115_TM_FOCUS_PUNCH":{"address":2708726,"default_item":289,"flag":1017},"ITEM_ROUTE_116_ETHER":{"address":2708804,"default_item":34,"flag":1019},"ITEM_ROUTE_116_HP_UP":{"address":2708830,"default_item":63,"flag":1021},"ITEM_ROUTE_116_POTION":{"address":2708843,"default_item":13,"flag":1146},"ITEM_ROUTE_116_REPEL":{"address":2708817,"default_item":86,"flag":1020},"ITEM_ROUTE_116_X_SPECIAL":{"address":2708791,"default_item":79,"flag":1001},"ITEM_ROUTE_117_GREAT_BALL":{"address":2708856,"default_item":3,"flag":1022},"ITEM_ROUTE_117_REVIVE":{"address":2708869,"default_item":24,"flag":1023},"ITEM_ROUTE_118_HYPER_POTION":{"address":2708882,"default_item":21,"flag":1121},"ITEM_ROUTE_119_ELIXIR_1":{"address":2708921,"default_item":36,"flag":1026},"ITEM_ROUTE_119_ELIXIR_2":{"address":2708986,"default_item":36,"flag":1147},"ITEM_ROUTE_119_HYPER_POTION_1":{"address":2708960,"default_item":21,"flag":1029},"ITEM_ROUTE_119_HYPER_POTION_2":{"address":2708973,"default_item":21,"flag":1106},"ITEM_ROUTE_119_LEAF_STONE":{"address":2708934,"default_item":98,"flag":1027},"ITEM_ROUTE_119_NUGGET":{"address":2710104,"default_item":110,"flag":1134},"ITEM_ROUTE_119_RARE_CANDY":{"address":2708947,"default_item":68,"flag":1028},"ITEM_ROUTE_119_SUPER_REPEL":{"address":2708895,"default_item":83,"flag":1024},"ITEM_ROUTE_119_ZINC":{"address":2708908,"default_item":70,"flag":1025},"ITEM_ROUTE_120_FULL_HEAL":{"address":2709012,"default_item":23,"flag":1031},"ITEM_ROUTE_120_HYPER_POTION":{"address":2709025,"default_item":21,"flag":1107},"ITEM_ROUTE_120_NEST_BALL":{"address":2709038,"default_item":8,"flag":1108},"ITEM_ROUTE_120_NUGGET":{"address":2708999,"default_item":110,"flag":1030},"ITEM_ROUTE_120_REVIVE":{"address":2709051,"default_item":24,"flag":1148},"ITEM_ROUTE_121_CARBOS":{"address":2709064,"default_item":66,"flag":1103},"ITEM_ROUTE_121_REVIVE":{"address":2709077,"default_item":24,"flag":1149},"ITEM_ROUTE_121_ZINC":{"address":2709090,"default_item":70,"flag":1150},"ITEM_ROUTE_123_CALCIUM":{"address":2709103,"default_item":67,"flag":1032},"ITEM_ROUTE_123_ELIXIR":{"address":2709129,"default_item":36,"flag":1109},"ITEM_ROUTE_123_PP_UP":{"address":2709142,"default_item":69,"flag":1152},"ITEM_ROUTE_123_REVIVAL_HERB":{"address":2709155,"default_item":33,"flag":1153},"ITEM_ROUTE_123_ULTRA_BALL":{"address":2709116,"default_item":2,"flag":1104},"ITEM_ROUTE_124_BLUE_SHARD":{"address":2709181,"default_item":49,"flag":1093},"ITEM_ROUTE_124_RED_SHARD":{"address":2709168,"default_item":48,"flag":1092},"ITEM_ROUTE_124_YELLOW_SHARD":{"address":2709194,"default_item":50,"flag":1066},"ITEM_ROUTE_125_BIG_PEARL":{"address":2709207,"default_item":107,"flag":1154},"ITEM_ROUTE_126_GREEN_SHARD":{"address":2709220,"default_item":51,"flag":1105},"ITEM_ROUTE_127_CARBOS":{"address":2709246,"default_item":66,"flag":1035},"ITEM_ROUTE_127_RARE_CANDY":{"address":2709259,"default_item":68,"flag":1155},"ITEM_ROUTE_127_ZINC":{"address":2709233,"default_item":70,"flag":1034},"ITEM_ROUTE_132_PROTEIN":{"address":2709285,"default_item":64,"flag":1156},"ITEM_ROUTE_132_RARE_CANDY":{"address":2709272,"default_item":68,"flag":1036},"ITEM_ROUTE_133_BIG_PEARL":{"address":2709298,"default_item":107,"flag":1037},"ITEM_ROUTE_133_MAX_REVIVE":{"address":2709324,"default_item":25,"flag":1157},"ITEM_ROUTE_133_STAR_PIECE":{"address":2709311,"default_item":109,"flag":1038},"ITEM_ROUTE_134_CARBOS":{"address":2709337,"default_item":66,"flag":1158},"ITEM_ROUTE_134_STAR_PIECE":{"address":2709350,"default_item":109,"flag":1159},"ITEM_RUSTBORO_CITY_X_DEFEND":{"address":2709402,"default_item":76,"flag":1041},"ITEM_RUSTURF_TUNNEL_MAX_ETHER":{"address":2709506,"default_item":35,"flag":1049},"ITEM_RUSTURF_TUNNEL_POKE_BALL":{"address":2709493,"default_item":4,"flag":1048},"ITEM_SAFARI_ZONE_NORTH_CALCIUM":{"address":2709896,"default_item":67,"flag":1119},"ITEM_SAFARI_ZONE_NORTH_EAST_NUGGET":{"address":2709922,"default_item":110,"flag":1169},"ITEM_SAFARI_ZONE_NORTH_WEST_TM_SOLAR_BEAM":{"address":2709883,"default_item":310,"flag":1094},"ITEM_SAFARI_ZONE_SOUTH_EAST_BIG_PEARL":{"address":2709935,"default_item":107,"flag":1170},"ITEM_SAFARI_ZONE_SOUTH_WEST_MAX_REVIVE":{"address":2709909,"default_item":25,"flag":1131},"ITEM_SCORCHED_SLAB_TM_SUNNY_DAY":{"address":2709870,"default_item":299,"flag":1079},"ITEM_SEAFLOOR_CAVERN_ROOM_9_TM_EARTHQUAKE":{"address":2710208,"default_item":314,"flag":1090},"ITEM_SHOAL_CAVE_ENTRANCE_BIG_PEARL":{"address":2710143,"default_item":107,"flag":1081},"ITEM_SHOAL_CAVE_ICE_ROOM_NEVER_MELT_ICE":{"address":2710195,"default_item":212,"flag":1113},"ITEM_SHOAL_CAVE_ICE_ROOM_TM_HAIL":{"address":2710182,"default_item":295,"flag":1112},"ITEM_SHOAL_CAVE_INNER_ROOM_RARE_CANDY":{"address":2710156,"default_item":68,"flag":1082},"ITEM_SHOAL_CAVE_STAIRS_ROOM_ICE_HEAL":{"address":2710169,"default_item":16,"flag":1083},"ITEM_TRICK_HOUSE_PUZZLE_1_ORANGE_MAIL":{"address":[2710221,2551006],"default_item":121,"flag":1060},"ITEM_TRICK_HOUSE_PUZZLE_2_HARBOR_MAIL":{"address":[2710234,2551032],"default_item":122,"flag":1061},"ITEM_TRICK_HOUSE_PUZZLE_2_WAVE_MAIL":{"address":[2710247,2551058],"default_item":126,"flag":1062},"ITEM_TRICK_HOUSE_PUZZLE_3_SHADOW_MAIL":{"address":[2710260,2551084],"default_item":128,"flag":1063},"ITEM_TRICK_HOUSE_PUZZLE_3_WOOD_MAIL":{"address":[2710273,2551110],"default_item":125,"flag":1064},"ITEM_TRICK_HOUSE_PUZZLE_4_MECH_MAIL":{"address":[2710286,2551136],"default_item":124,"flag":1065},"ITEM_TRICK_HOUSE_PUZZLE_6_GLITTER_MAIL":{"address":[2710299,2551162],"default_item":123,"flag":1067},"ITEM_TRICK_HOUSE_PUZZLE_7_TROPIC_MAIL":{"address":[2710312,2551188],"default_item":129,"flag":1068},"ITEM_TRICK_HOUSE_PUZZLE_8_BEAD_MAIL":{"address":[2710325,2551214],"default_item":127,"flag":1069},"ITEM_VICTORY_ROAD_1F_MAX_ELIXIR":{"address":2710338,"default_item":37,"flag":1084},"ITEM_VICTORY_ROAD_1F_PP_UP":{"address":2710351,"default_item":69,"flag":1085},"ITEM_VICTORY_ROAD_B1F_FULL_RESTORE":{"address":2710377,"default_item":19,"flag":1087},"ITEM_VICTORY_ROAD_B1F_TM_PSYCHIC":{"address":2710364,"default_item":317,"flag":1086},"ITEM_VICTORY_ROAD_B2F_FULL_HEAL":{"address":2710390,"default_item":23,"flag":1088},"NPC_GIFT_BERRY_MASTERS_WIFE":{"address":2570453,"default_item":133,"flag":1197},"NPC_GIFT_BERRY_MASTER_RECEIVED_BERRY_1":{"address":2570263,"default_item":153,"flag":1195},"NPC_GIFT_BERRY_MASTER_RECEIVED_BERRY_2":{"address":2570315,"default_item":154,"flag":1196},"NPC_GIFT_FLOWER_SHOP_RECEIVED_BERRY":{"address":2284375,"default_item":133,"flag":1207},"NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON":{"address":1971718,"default_item":271,"flag":208},"NPC_GIFT_GOT_TM_THUNDERBOLT_FROM_WATTSON":{"address":1971754,"default_item":312,"flag":209},"NPC_GIFT_LILYCOVE_RECEIVED_BERRY":{"address":1985277,"default_item":141,"flag":1208},"NPC_GIFT_RECEIVED_6_SODA_POP":{"address":2543767,"default_item":27,"flag":140},"NPC_GIFT_RECEIVED_ACRO_BIKE":{"address":2170570,"default_item":272,"flag":1181},"NPC_GIFT_RECEIVED_AMULET_COIN":{"address":2716248,"default_item":189,"flag":133},"NPC_GIFT_RECEIVED_AURORA_TICKET":{"address":2716523,"default_item":371,"flag":314},"NPC_GIFT_RECEIVED_CHARCOAL":{"address":2102559,"default_item":215,"flag":254},"NPC_GIFT_RECEIVED_CHESTO_BERRY_ROUTE_104":{"address":2028703,"default_item":134,"flag":246},"NPC_GIFT_RECEIVED_CLEANSE_TAG":{"address":2312109,"default_item":190,"flag":282},"NPC_GIFT_RECEIVED_COIN_CASE":{"address":2179054,"default_item":260,"flag":258},"NPC_GIFT_RECEIVED_DEEP_SEA_SCALE":{"address":2162572,"default_item":193,"flag":1190},"NPC_GIFT_RECEIVED_DEEP_SEA_TOOTH":{"address":2162555,"default_item":192,"flag":1191},"NPC_GIFT_RECEIVED_DEVON_GOODS_RUSTURF_TUNNEL":{"address":2295814,"default_item":269,"flag":1172},"NPC_GIFT_RECEIVED_DEVON_SCOPE":{"address":2065146,"default_item":288,"flag":285},"NPC_GIFT_RECEIVED_EON_TICKET":{"address":2716574,"default_item":275,"flag":474},"NPC_GIFT_RECEIVED_EXP_SHARE":{"address":2185525,"default_item":182,"flag":272},"NPC_GIFT_RECEIVED_FIRST_POKEBALLS":{"address":2085751,"default_item":4,"flag":233},"NPC_GIFT_RECEIVED_FOCUS_BAND":{"address":2337807,"default_item":196,"flag":283},"NPC_GIFT_RECEIVED_GOOD_ROD":{"address":2058408,"default_item":263,"flag":227},"NPC_GIFT_RECEIVED_GO_GOGGLES":{"address":2017746,"default_item":279,"flag":221},"NPC_GIFT_RECEIVED_GREAT_BALL_PETALBURG_WOODS":{"address":2300119,"default_item":3,"flag":1171},"NPC_GIFT_RECEIVED_GREAT_BALL_RUSTBORO_CITY":{"address":1977146,"default_item":3,"flag":1173},"NPC_GIFT_RECEIVED_HM_CUT":{"address":2199532,"default_item":339,"flag":137},"NPC_GIFT_RECEIVED_HM_DIVE":{"address":2252095,"default_item":346,"flag":123},"NPC_GIFT_RECEIVED_HM_FLASH":{"address":2298287,"default_item":343,"flag":109},"NPC_GIFT_RECEIVED_HM_FLY":{"address":2060636,"default_item":340,"flag":110},"NPC_GIFT_RECEIVED_HM_ROCK_SMASH":{"address":2174128,"default_item":344,"flag":107},"NPC_GIFT_RECEIVED_HM_STRENGTH":{"address":2295305,"default_item":342,"flag":106},"NPC_GIFT_RECEIVED_HM_SURF":{"address":2126671,"default_item":341,"flag":122},"NPC_GIFT_RECEIVED_HM_WATERFALL":{"address":1999854,"default_item":345,"flag":312},"NPC_GIFT_RECEIVED_ITEMFINDER":{"address":2039874,"default_item":261,"flag":1176},"NPC_GIFT_RECEIVED_KINGS_ROCK":{"address":1993670,"default_item":187,"flag":276},"NPC_GIFT_RECEIVED_LETTER":{"address":2185301,"default_item":274,"flag":1174},"NPC_GIFT_RECEIVED_MACHO_BRACE":{"address":2284472,"default_item":181,"flag":277},"NPC_GIFT_RECEIVED_MACH_BIKE":{"address":2170553,"default_item":259,"flag":1180},"NPC_GIFT_RECEIVED_MAGMA_EMBLEM":{"address":2316671,"default_item":375,"flag":1177},"NPC_GIFT_RECEIVED_MENTAL_HERB":{"address":2208103,"default_item":185,"flag":223},"NPC_GIFT_RECEIVED_METEORITE":{"address":2304222,"default_item":280,"flag":115},"NPC_GIFT_RECEIVED_MIRACLE_SEED":{"address":2300337,"default_item":205,"flag":297},"NPC_GIFT_RECEIVED_MYSTIC_TICKET":{"address":2716540,"default_item":370,"flag":315},"NPC_GIFT_RECEIVED_OLD_ROD":{"address":2012541,"default_item":262,"flag":257},"NPC_GIFT_RECEIVED_OLD_SEA_MAP":{"address":2716557,"default_item":376,"flag":316},"NPC_GIFT_RECEIVED_POKEBLOCK_CASE":{"address":2614193,"default_item":273,"flag":95},"NPC_GIFT_RECEIVED_POTION_OLDALE":{"address":2010888,"default_item":13,"flag":132},"NPC_GIFT_RECEIVED_POWDER_JAR":{"address":1962504,"default_item":372,"flag":337},"NPC_GIFT_RECEIVED_PREMIER_BALL_RUSTBORO":{"address":2200571,"default_item":12,"flag":213},"NPC_GIFT_RECEIVED_QUICK_CLAW":{"address":2192227,"default_item":183,"flag":275},"NPC_GIFT_RECEIVED_REPEAT_BALL":{"address":2053722,"default_item":9,"flag":256},"NPC_GIFT_RECEIVED_SECRET_POWER":{"address":2598914,"default_item":331,"flag":96},"NPC_GIFT_RECEIVED_SILK_SCARF":{"address":2101830,"default_item":217,"flag":289},"NPC_GIFT_RECEIVED_SOFT_SAND":{"address":2035664,"default_item":203,"flag":280},"NPC_GIFT_RECEIVED_SOOTHE_BELL":{"address":2151278,"default_item":184,"flag":278},"NPC_GIFT_RECEIVED_SOOT_SACK":{"address":2567245,"default_item":270,"flag":1033},"NPC_GIFT_RECEIVED_SS_TICKET":{"address":2716506,"default_item":265,"flag":291},"NPC_GIFT_RECEIVED_SUN_STONE_MOSSDEEP":{"address":2254406,"default_item":93,"flag":192},"NPC_GIFT_RECEIVED_SUPER_ROD":{"address":2251560,"default_item":264,"flag":152},"NPC_GIFT_RECEIVED_TM_AERIAL_ACE":{"address":2202201,"default_item":328,"flag":170},"NPC_GIFT_RECEIVED_TM_ATTRACT":{"address":2116413,"default_item":333,"flag":235},"NPC_GIFT_RECEIVED_TM_BRICK_BREAK":{"address":2269085,"default_item":319,"flag":121},"NPC_GIFT_RECEIVED_TM_BULK_UP":{"address":2095210,"default_item":296,"flag":166},"NPC_GIFT_RECEIVED_TM_BULLET_SEED":{"address":2028910,"default_item":297,"flag":262},"NPC_GIFT_RECEIVED_TM_CALM_MIND":{"address":2244066,"default_item":292,"flag":171},"NPC_GIFT_RECEIVED_TM_DIG":{"address":2286669,"default_item":316,"flag":261},"NPC_GIFT_RECEIVED_TM_FACADE":{"address":2129909,"default_item":330,"flag":169},"NPC_GIFT_RECEIVED_TM_FRUSTRATION":{"address":2124110,"default_item":309,"flag":1179},"NPC_GIFT_RECEIVED_TM_GIGA_DRAIN":{"address":2068012,"default_item":307,"flag":232},"NPC_GIFT_RECEIVED_TM_HIDDEN_POWER":{"address":2206905,"default_item":298,"flag":264},"NPC_GIFT_RECEIVED_TM_OVERHEAT":{"address":2103328,"default_item":338,"flag":168},"NPC_GIFT_RECEIVED_TM_REST":{"address":2236966,"default_item":332,"flag":234},"NPC_GIFT_RECEIVED_TM_RETURN":{"address":2113546,"default_item":315,"flag":229},"NPC_GIFT_RECEIVED_TM_RETURN_2":{"address":2124055,"default_item":315,"flag":1178},"NPC_GIFT_RECEIVED_TM_ROAR":{"address":2051750,"default_item":293,"flag":231},"NPC_GIFT_RECEIVED_TM_ROCK_TOMB":{"address":2188088,"default_item":327,"flag":165},"NPC_GIFT_RECEIVED_TM_SHOCK_WAVE":{"address":2167340,"default_item":322,"flag":167},"NPC_GIFT_RECEIVED_TM_SLUDGE_BOMB":{"address":2099189,"default_item":324,"flag":230},"NPC_GIFT_RECEIVED_TM_SNATCH":{"address":2360766,"default_item":337,"flag":260},"NPC_GIFT_RECEIVED_TM_STEEL_WING":{"address":2298866,"default_item":335,"flag":1175},"NPC_GIFT_RECEIVED_TM_THIEF":{"address":2154698,"default_item":334,"flag":269},"NPC_GIFT_RECEIVED_TM_TORMENT":{"address":2145260,"default_item":329,"flag":265},"NPC_GIFT_RECEIVED_TM_WATER_PULSE":{"address":2262402,"default_item":291,"flag":172},"NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_1":{"address":2550316,"default_item":68,"flag":1200},"NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_2":{"address":2550390,"default_item":10,"flag":1201},"NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_3":{"address":2550473,"default_item":204,"flag":1202},"NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_4":{"address":2550556,"default_item":194,"flag":1203},"NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_5":{"address":2550630,"default_item":300,"flag":1204},"NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_6":{"address":2550695,"default_item":208,"flag":1205},"NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_7":{"address":2550769,"default_item":71,"flag":1206},"NPC_GIFT_RECEIVED_WAILMER_PAIL":{"address":2284320,"default_item":268,"flag":94},"NPC_GIFT_RECEIVED_WHITE_HERB":{"address":2028770,"default_item":180,"flag":279},"NPC_GIFT_ROUTE_111_RECEIVED_BERRY":{"address":2045493,"default_item":148,"flag":1192},"NPC_GIFT_ROUTE_114_RECEIVED_BERRY":{"address":2051680,"default_item":149,"flag":1193},"NPC_GIFT_ROUTE_120_RECEIVED_BERRY":{"address":2064727,"default_item":143,"flag":1194},"NPC_GIFT_SOOTOPOLIS_RECEIVED_BERRY_1":{"address":1998521,"default_item":153,"flag":1198},"NPC_GIFT_SOOTOPOLIS_RECEIVED_BERRY_2":{"address":1998566,"default_item":143,"flag":1199},"POKEDEX_REWARD_001":{"address":5729368,"default_item":3,"flag":0},"POKEDEX_REWARD_002":{"address":5729370,"default_item":3,"flag":0},"POKEDEX_REWARD_003":{"address":5729372,"default_item":3,"flag":0},"POKEDEX_REWARD_004":{"address":5729374,"default_item":3,"flag":0},"POKEDEX_REWARD_005":{"address":5729376,"default_item":3,"flag":0},"POKEDEX_REWARD_006":{"address":5729378,"default_item":3,"flag":0},"POKEDEX_REWARD_007":{"address":5729380,"default_item":3,"flag":0},"POKEDEX_REWARD_008":{"address":5729382,"default_item":3,"flag":0},"POKEDEX_REWARD_009":{"address":5729384,"default_item":3,"flag":0},"POKEDEX_REWARD_010":{"address":5729386,"default_item":3,"flag":0},"POKEDEX_REWARD_011":{"address":5729388,"default_item":3,"flag":0},"POKEDEX_REWARD_012":{"address":5729390,"default_item":3,"flag":0},"POKEDEX_REWARD_013":{"address":5729392,"default_item":3,"flag":0},"POKEDEX_REWARD_014":{"address":5729394,"default_item":3,"flag":0},"POKEDEX_REWARD_015":{"address":5729396,"default_item":3,"flag":0},"POKEDEX_REWARD_016":{"address":5729398,"default_item":3,"flag":0},"POKEDEX_REWARD_017":{"address":5729400,"default_item":3,"flag":0},"POKEDEX_REWARD_018":{"address":5729402,"default_item":3,"flag":0},"POKEDEX_REWARD_019":{"address":5729404,"default_item":3,"flag":0},"POKEDEX_REWARD_020":{"address":5729406,"default_item":3,"flag":0},"POKEDEX_REWARD_021":{"address":5729408,"default_item":3,"flag":0},"POKEDEX_REWARD_022":{"address":5729410,"default_item":3,"flag":0},"POKEDEX_REWARD_023":{"address":5729412,"default_item":3,"flag":0},"POKEDEX_REWARD_024":{"address":5729414,"default_item":3,"flag":0},"POKEDEX_REWARD_025":{"address":5729416,"default_item":3,"flag":0},"POKEDEX_REWARD_026":{"address":5729418,"default_item":3,"flag":0},"POKEDEX_REWARD_027":{"address":5729420,"default_item":3,"flag":0},"POKEDEX_REWARD_028":{"address":5729422,"default_item":3,"flag":0},"POKEDEX_REWARD_029":{"address":5729424,"default_item":3,"flag":0},"POKEDEX_REWARD_030":{"address":5729426,"default_item":3,"flag":0},"POKEDEX_REWARD_031":{"address":5729428,"default_item":3,"flag":0},"POKEDEX_REWARD_032":{"address":5729430,"default_item":3,"flag":0},"POKEDEX_REWARD_033":{"address":5729432,"default_item":3,"flag":0},"POKEDEX_REWARD_034":{"address":5729434,"default_item":3,"flag":0},"POKEDEX_REWARD_035":{"address":5729436,"default_item":3,"flag":0},"POKEDEX_REWARD_036":{"address":5729438,"default_item":3,"flag":0},"POKEDEX_REWARD_037":{"address":5729440,"default_item":3,"flag":0},"POKEDEX_REWARD_038":{"address":5729442,"default_item":3,"flag":0},"POKEDEX_REWARD_039":{"address":5729444,"default_item":3,"flag":0},"POKEDEX_REWARD_040":{"address":5729446,"default_item":3,"flag":0},"POKEDEX_REWARD_041":{"address":5729448,"default_item":3,"flag":0},"POKEDEX_REWARD_042":{"address":5729450,"default_item":3,"flag":0},"POKEDEX_REWARD_043":{"address":5729452,"default_item":3,"flag":0},"POKEDEX_REWARD_044":{"address":5729454,"default_item":3,"flag":0},"POKEDEX_REWARD_045":{"address":5729456,"default_item":3,"flag":0},"POKEDEX_REWARD_046":{"address":5729458,"default_item":3,"flag":0},"POKEDEX_REWARD_047":{"address":5729460,"default_item":3,"flag":0},"POKEDEX_REWARD_048":{"address":5729462,"default_item":3,"flag":0},"POKEDEX_REWARD_049":{"address":5729464,"default_item":3,"flag":0},"POKEDEX_REWARD_050":{"address":5729466,"default_item":3,"flag":0},"POKEDEX_REWARD_051":{"address":5729468,"default_item":3,"flag":0},"POKEDEX_REWARD_052":{"address":5729470,"default_item":3,"flag":0},"POKEDEX_REWARD_053":{"address":5729472,"default_item":3,"flag":0},"POKEDEX_REWARD_054":{"address":5729474,"default_item":3,"flag":0},"POKEDEX_REWARD_055":{"address":5729476,"default_item":3,"flag":0},"POKEDEX_REWARD_056":{"address":5729478,"default_item":3,"flag":0},"POKEDEX_REWARD_057":{"address":5729480,"default_item":3,"flag":0},"POKEDEX_REWARD_058":{"address":5729482,"default_item":3,"flag":0},"POKEDEX_REWARD_059":{"address":5729484,"default_item":3,"flag":0},"POKEDEX_REWARD_060":{"address":5729486,"default_item":3,"flag":0},"POKEDEX_REWARD_061":{"address":5729488,"default_item":3,"flag":0},"POKEDEX_REWARD_062":{"address":5729490,"default_item":3,"flag":0},"POKEDEX_REWARD_063":{"address":5729492,"default_item":3,"flag":0},"POKEDEX_REWARD_064":{"address":5729494,"default_item":3,"flag":0},"POKEDEX_REWARD_065":{"address":5729496,"default_item":3,"flag":0},"POKEDEX_REWARD_066":{"address":5729498,"default_item":3,"flag":0},"POKEDEX_REWARD_067":{"address":5729500,"default_item":3,"flag":0},"POKEDEX_REWARD_068":{"address":5729502,"default_item":3,"flag":0},"POKEDEX_REWARD_069":{"address":5729504,"default_item":3,"flag":0},"POKEDEX_REWARD_070":{"address":5729506,"default_item":3,"flag":0},"POKEDEX_REWARD_071":{"address":5729508,"default_item":3,"flag":0},"POKEDEX_REWARD_072":{"address":5729510,"default_item":3,"flag":0},"POKEDEX_REWARD_073":{"address":5729512,"default_item":3,"flag":0},"POKEDEX_REWARD_074":{"address":5729514,"default_item":3,"flag":0},"POKEDEX_REWARD_075":{"address":5729516,"default_item":3,"flag":0},"POKEDEX_REWARD_076":{"address":5729518,"default_item":3,"flag":0},"POKEDEX_REWARD_077":{"address":5729520,"default_item":3,"flag":0},"POKEDEX_REWARD_078":{"address":5729522,"default_item":3,"flag":0},"POKEDEX_REWARD_079":{"address":5729524,"default_item":3,"flag":0},"POKEDEX_REWARD_080":{"address":5729526,"default_item":3,"flag":0},"POKEDEX_REWARD_081":{"address":5729528,"default_item":3,"flag":0},"POKEDEX_REWARD_082":{"address":5729530,"default_item":3,"flag":0},"POKEDEX_REWARD_083":{"address":5729532,"default_item":3,"flag":0},"POKEDEX_REWARD_084":{"address":5729534,"default_item":3,"flag":0},"POKEDEX_REWARD_085":{"address":5729536,"default_item":3,"flag":0},"POKEDEX_REWARD_086":{"address":5729538,"default_item":3,"flag":0},"POKEDEX_REWARD_087":{"address":5729540,"default_item":3,"flag":0},"POKEDEX_REWARD_088":{"address":5729542,"default_item":3,"flag":0},"POKEDEX_REWARD_089":{"address":5729544,"default_item":3,"flag":0},"POKEDEX_REWARD_090":{"address":5729546,"default_item":3,"flag":0},"POKEDEX_REWARD_091":{"address":5729548,"default_item":3,"flag":0},"POKEDEX_REWARD_092":{"address":5729550,"default_item":3,"flag":0},"POKEDEX_REWARD_093":{"address":5729552,"default_item":3,"flag":0},"POKEDEX_REWARD_094":{"address":5729554,"default_item":3,"flag":0},"POKEDEX_REWARD_095":{"address":5729556,"default_item":3,"flag":0},"POKEDEX_REWARD_096":{"address":5729558,"default_item":3,"flag":0},"POKEDEX_REWARD_097":{"address":5729560,"default_item":3,"flag":0},"POKEDEX_REWARD_098":{"address":5729562,"default_item":3,"flag":0},"POKEDEX_REWARD_099":{"address":5729564,"default_item":3,"flag":0},"POKEDEX_REWARD_100":{"address":5729566,"default_item":3,"flag":0},"POKEDEX_REWARD_101":{"address":5729568,"default_item":3,"flag":0},"POKEDEX_REWARD_102":{"address":5729570,"default_item":3,"flag":0},"POKEDEX_REWARD_103":{"address":5729572,"default_item":3,"flag":0},"POKEDEX_REWARD_104":{"address":5729574,"default_item":3,"flag":0},"POKEDEX_REWARD_105":{"address":5729576,"default_item":3,"flag":0},"POKEDEX_REWARD_106":{"address":5729578,"default_item":3,"flag":0},"POKEDEX_REWARD_107":{"address":5729580,"default_item":3,"flag":0},"POKEDEX_REWARD_108":{"address":5729582,"default_item":3,"flag":0},"POKEDEX_REWARD_109":{"address":5729584,"default_item":3,"flag":0},"POKEDEX_REWARD_110":{"address":5729586,"default_item":3,"flag":0},"POKEDEX_REWARD_111":{"address":5729588,"default_item":3,"flag":0},"POKEDEX_REWARD_112":{"address":5729590,"default_item":3,"flag":0},"POKEDEX_REWARD_113":{"address":5729592,"default_item":3,"flag":0},"POKEDEX_REWARD_114":{"address":5729594,"default_item":3,"flag":0},"POKEDEX_REWARD_115":{"address":5729596,"default_item":3,"flag":0},"POKEDEX_REWARD_116":{"address":5729598,"default_item":3,"flag":0},"POKEDEX_REWARD_117":{"address":5729600,"default_item":3,"flag":0},"POKEDEX_REWARD_118":{"address":5729602,"default_item":3,"flag":0},"POKEDEX_REWARD_119":{"address":5729604,"default_item":3,"flag":0},"POKEDEX_REWARD_120":{"address":5729606,"default_item":3,"flag":0},"POKEDEX_REWARD_121":{"address":5729608,"default_item":3,"flag":0},"POKEDEX_REWARD_122":{"address":5729610,"default_item":3,"flag":0},"POKEDEX_REWARD_123":{"address":5729612,"default_item":3,"flag":0},"POKEDEX_REWARD_124":{"address":5729614,"default_item":3,"flag":0},"POKEDEX_REWARD_125":{"address":5729616,"default_item":3,"flag":0},"POKEDEX_REWARD_126":{"address":5729618,"default_item":3,"flag":0},"POKEDEX_REWARD_127":{"address":5729620,"default_item":3,"flag":0},"POKEDEX_REWARD_128":{"address":5729622,"default_item":3,"flag":0},"POKEDEX_REWARD_129":{"address":5729624,"default_item":3,"flag":0},"POKEDEX_REWARD_130":{"address":5729626,"default_item":3,"flag":0},"POKEDEX_REWARD_131":{"address":5729628,"default_item":3,"flag":0},"POKEDEX_REWARD_132":{"address":5729630,"default_item":3,"flag":0},"POKEDEX_REWARD_133":{"address":5729632,"default_item":3,"flag":0},"POKEDEX_REWARD_134":{"address":5729634,"default_item":3,"flag":0},"POKEDEX_REWARD_135":{"address":5729636,"default_item":3,"flag":0},"POKEDEX_REWARD_136":{"address":5729638,"default_item":3,"flag":0},"POKEDEX_REWARD_137":{"address":5729640,"default_item":3,"flag":0},"POKEDEX_REWARD_138":{"address":5729642,"default_item":3,"flag":0},"POKEDEX_REWARD_139":{"address":5729644,"default_item":3,"flag":0},"POKEDEX_REWARD_140":{"address":5729646,"default_item":3,"flag":0},"POKEDEX_REWARD_141":{"address":5729648,"default_item":3,"flag":0},"POKEDEX_REWARD_142":{"address":5729650,"default_item":3,"flag":0},"POKEDEX_REWARD_143":{"address":5729652,"default_item":3,"flag":0},"POKEDEX_REWARD_144":{"address":5729654,"default_item":3,"flag":0},"POKEDEX_REWARD_145":{"address":5729656,"default_item":3,"flag":0},"POKEDEX_REWARD_146":{"address":5729658,"default_item":3,"flag":0},"POKEDEX_REWARD_147":{"address":5729660,"default_item":3,"flag":0},"POKEDEX_REWARD_148":{"address":5729662,"default_item":3,"flag":0},"POKEDEX_REWARD_149":{"address":5729664,"default_item":3,"flag":0},"POKEDEX_REWARD_150":{"address":5729666,"default_item":3,"flag":0},"POKEDEX_REWARD_151":{"address":5729668,"default_item":3,"flag":0},"POKEDEX_REWARD_152":{"address":5729670,"default_item":3,"flag":0},"POKEDEX_REWARD_153":{"address":5729672,"default_item":3,"flag":0},"POKEDEX_REWARD_154":{"address":5729674,"default_item":3,"flag":0},"POKEDEX_REWARD_155":{"address":5729676,"default_item":3,"flag":0},"POKEDEX_REWARD_156":{"address":5729678,"default_item":3,"flag":0},"POKEDEX_REWARD_157":{"address":5729680,"default_item":3,"flag":0},"POKEDEX_REWARD_158":{"address":5729682,"default_item":3,"flag":0},"POKEDEX_REWARD_159":{"address":5729684,"default_item":3,"flag":0},"POKEDEX_REWARD_160":{"address":5729686,"default_item":3,"flag":0},"POKEDEX_REWARD_161":{"address":5729688,"default_item":3,"flag":0},"POKEDEX_REWARD_162":{"address":5729690,"default_item":3,"flag":0},"POKEDEX_REWARD_163":{"address":5729692,"default_item":3,"flag":0},"POKEDEX_REWARD_164":{"address":5729694,"default_item":3,"flag":0},"POKEDEX_REWARD_165":{"address":5729696,"default_item":3,"flag":0},"POKEDEX_REWARD_166":{"address":5729698,"default_item":3,"flag":0},"POKEDEX_REWARD_167":{"address":5729700,"default_item":3,"flag":0},"POKEDEX_REWARD_168":{"address":5729702,"default_item":3,"flag":0},"POKEDEX_REWARD_169":{"address":5729704,"default_item":3,"flag":0},"POKEDEX_REWARD_170":{"address":5729706,"default_item":3,"flag":0},"POKEDEX_REWARD_171":{"address":5729708,"default_item":3,"flag":0},"POKEDEX_REWARD_172":{"address":5729710,"default_item":3,"flag":0},"POKEDEX_REWARD_173":{"address":5729712,"default_item":3,"flag":0},"POKEDEX_REWARD_174":{"address":5729714,"default_item":3,"flag":0},"POKEDEX_REWARD_175":{"address":5729716,"default_item":3,"flag":0},"POKEDEX_REWARD_176":{"address":5729718,"default_item":3,"flag":0},"POKEDEX_REWARD_177":{"address":5729720,"default_item":3,"flag":0},"POKEDEX_REWARD_178":{"address":5729722,"default_item":3,"flag":0},"POKEDEX_REWARD_179":{"address":5729724,"default_item":3,"flag":0},"POKEDEX_REWARD_180":{"address":5729726,"default_item":3,"flag":0},"POKEDEX_REWARD_181":{"address":5729728,"default_item":3,"flag":0},"POKEDEX_REWARD_182":{"address":5729730,"default_item":3,"flag":0},"POKEDEX_REWARD_183":{"address":5729732,"default_item":3,"flag":0},"POKEDEX_REWARD_184":{"address":5729734,"default_item":3,"flag":0},"POKEDEX_REWARD_185":{"address":5729736,"default_item":3,"flag":0},"POKEDEX_REWARD_186":{"address":5729738,"default_item":3,"flag":0},"POKEDEX_REWARD_187":{"address":5729740,"default_item":3,"flag":0},"POKEDEX_REWARD_188":{"address":5729742,"default_item":3,"flag":0},"POKEDEX_REWARD_189":{"address":5729744,"default_item":3,"flag":0},"POKEDEX_REWARD_190":{"address":5729746,"default_item":3,"flag":0},"POKEDEX_REWARD_191":{"address":5729748,"default_item":3,"flag":0},"POKEDEX_REWARD_192":{"address":5729750,"default_item":3,"flag":0},"POKEDEX_REWARD_193":{"address":5729752,"default_item":3,"flag":0},"POKEDEX_REWARD_194":{"address":5729754,"default_item":3,"flag":0},"POKEDEX_REWARD_195":{"address":5729756,"default_item":3,"flag":0},"POKEDEX_REWARD_196":{"address":5729758,"default_item":3,"flag":0},"POKEDEX_REWARD_197":{"address":5729760,"default_item":3,"flag":0},"POKEDEX_REWARD_198":{"address":5729762,"default_item":3,"flag":0},"POKEDEX_REWARD_199":{"address":5729764,"default_item":3,"flag":0},"POKEDEX_REWARD_200":{"address":5729766,"default_item":3,"flag":0},"POKEDEX_REWARD_201":{"address":5729768,"default_item":3,"flag":0},"POKEDEX_REWARD_202":{"address":5729770,"default_item":3,"flag":0},"POKEDEX_REWARD_203":{"address":5729772,"default_item":3,"flag":0},"POKEDEX_REWARD_204":{"address":5729774,"default_item":3,"flag":0},"POKEDEX_REWARD_205":{"address":5729776,"default_item":3,"flag":0},"POKEDEX_REWARD_206":{"address":5729778,"default_item":3,"flag":0},"POKEDEX_REWARD_207":{"address":5729780,"default_item":3,"flag":0},"POKEDEX_REWARD_208":{"address":5729782,"default_item":3,"flag":0},"POKEDEX_REWARD_209":{"address":5729784,"default_item":3,"flag":0},"POKEDEX_REWARD_210":{"address":5729786,"default_item":3,"flag":0},"POKEDEX_REWARD_211":{"address":5729788,"default_item":3,"flag":0},"POKEDEX_REWARD_212":{"address":5729790,"default_item":3,"flag":0},"POKEDEX_REWARD_213":{"address":5729792,"default_item":3,"flag":0},"POKEDEX_REWARD_214":{"address":5729794,"default_item":3,"flag":0},"POKEDEX_REWARD_215":{"address":5729796,"default_item":3,"flag":0},"POKEDEX_REWARD_216":{"address":5729798,"default_item":3,"flag":0},"POKEDEX_REWARD_217":{"address":5729800,"default_item":3,"flag":0},"POKEDEX_REWARD_218":{"address":5729802,"default_item":3,"flag":0},"POKEDEX_REWARD_219":{"address":5729804,"default_item":3,"flag":0},"POKEDEX_REWARD_220":{"address":5729806,"default_item":3,"flag":0},"POKEDEX_REWARD_221":{"address":5729808,"default_item":3,"flag":0},"POKEDEX_REWARD_222":{"address":5729810,"default_item":3,"flag":0},"POKEDEX_REWARD_223":{"address":5729812,"default_item":3,"flag":0},"POKEDEX_REWARD_224":{"address":5729814,"default_item":3,"flag":0},"POKEDEX_REWARD_225":{"address":5729816,"default_item":3,"flag":0},"POKEDEX_REWARD_226":{"address":5729818,"default_item":3,"flag":0},"POKEDEX_REWARD_227":{"address":5729820,"default_item":3,"flag":0},"POKEDEX_REWARD_228":{"address":5729822,"default_item":3,"flag":0},"POKEDEX_REWARD_229":{"address":5729824,"default_item":3,"flag":0},"POKEDEX_REWARD_230":{"address":5729826,"default_item":3,"flag":0},"POKEDEX_REWARD_231":{"address":5729828,"default_item":3,"flag":0},"POKEDEX_REWARD_232":{"address":5729830,"default_item":3,"flag":0},"POKEDEX_REWARD_233":{"address":5729832,"default_item":3,"flag":0},"POKEDEX_REWARD_234":{"address":5729834,"default_item":3,"flag":0},"POKEDEX_REWARD_235":{"address":5729836,"default_item":3,"flag":0},"POKEDEX_REWARD_236":{"address":5729838,"default_item":3,"flag":0},"POKEDEX_REWARD_237":{"address":5729840,"default_item":3,"flag":0},"POKEDEX_REWARD_238":{"address":5729842,"default_item":3,"flag":0},"POKEDEX_REWARD_239":{"address":5729844,"default_item":3,"flag":0},"POKEDEX_REWARD_240":{"address":5729846,"default_item":3,"flag":0},"POKEDEX_REWARD_241":{"address":5729848,"default_item":3,"flag":0},"POKEDEX_REWARD_242":{"address":5729850,"default_item":3,"flag":0},"POKEDEX_REWARD_243":{"address":5729852,"default_item":3,"flag":0},"POKEDEX_REWARD_244":{"address":5729854,"default_item":3,"flag":0},"POKEDEX_REWARD_245":{"address":5729856,"default_item":3,"flag":0},"POKEDEX_REWARD_246":{"address":5729858,"default_item":3,"flag":0},"POKEDEX_REWARD_247":{"address":5729860,"default_item":3,"flag":0},"POKEDEX_REWARD_248":{"address":5729862,"default_item":3,"flag":0},"POKEDEX_REWARD_249":{"address":5729864,"default_item":3,"flag":0},"POKEDEX_REWARD_250":{"address":5729866,"default_item":3,"flag":0},"POKEDEX_REWARD_251":{"address":5729868,"default_item":3,"flag":0},"POKEDEX_REWARD_252":{"address":5729870,"default_item":3,"flag":0},"POKEDEX_REWARD_253":{"address":5729872,"default_item":3,"flag":0},"POKEDEX_REWARD_254":{"address":5729874,"default_item":3,"flag":0},"POKEDEX_REWARD_255":{"address":5729876,"default_item":3,"flag":0},"POKEDEX_REWARD_256":{"address":5729878,"default_item":3,"flag":0},"POKEDEX_REWARD_257":{"address":5729880,"default_item":3,"flag":0},"POKEDEX_REWARD_258":{"address":5729882,"default_item":3,"flag":0},"POKEDEX_REWARD_259":{"address":5729884,"default_item":3,"flag":0},"POKEDEX_REWARD_260":{"address":5729886,"default_item":3,"flag":0},"POKEDEX_REWARD_261":{"address":5729888,"default_item":3,"flag":0},"POKEDEX_REWARD_262":{"address":5729890,"default_item":3,"flag":0},"POKEDEX_REWARD_263":{"address":5729892,"default_item":3,"flag":0},"POKEDEX_REWARD_264":{"address":5729894,"default_item":3,"flag":0},"POKEDEX_REWARD_265":{"address":5729896,"default_item":3,"flag":0},"POKEDEX_REWARD_266":{"address":5729898,"default_item":3,"flag":0},"POKEDEX_REWARD_267":{"address":5729900,"default_item":3,"flag":0},"POKEDEX_REWARD_268":{"address":5729902,"default_item":3,"flag":0},"POKEDEX_REWARD_269":{"address":5729904,"default_item":3,"flag":0},"POKEDEX_REWARD_270":{"address":5729906,"default_item":3,"flag":0},"POKEDEX_REWARD_271":{"address":5729908,"default_item":3,"flag":0},"POKEDEX_REWARD_272":{"address":5729910,"default_item":3,"flag":0},"POKEDEX_REWARD_273":{"address":5729912,"default_item":3,"flag":0},"POKEDEX_REWARD_274":{"address":5729914,"default_item":3,"flag":0},"POKEDEX_REWARD_275":{"address":5729916,"default_item":3,"flag":0},"POKEDEX_REWARD_276":{"address":5729918,"default_item":3,"flag":0},"POKEDEX_REWARD_277":{"address":5729920,"default_item":3,"flag":0},"POKEDEX_REWARD_278":{"address":5729922,"default_item":3,"flag":0},"POKEDEX_REWARD_279":{"address":5729924,"default_item":3,"flag":0},"POKEDEX_REWARD_280":{"address":5729926,"default_item":3,"flag":0},"POKEDEX_REWARD_281":{"address":5729928,"default_item":3,"flag":0},"POKEDEX_REWARD_282":{"address":5729930,"default_item":3,"flag":0},"POKEDEX_REWARD_283":{"address":5729932,"default_item":3,"flag":0},"POKEDEX_REWARD_284":{"address":5729934,"default_item":3,"flag":0},"POKEDEX_REWARD_285":{"address":5729936,"default_item":3,"flag":0},"POKEDEX_REWARD_286":{"address":5729938,"default_item":3,"flag":0},"POKEDEX_REWARD_287":{"address":5729940,"default_item":3,"flag":0},"POKEDEX_REWARD_288":{"address":5729942,"default_item":3,"flag":0},"POKEDEX_REWARD_289":{"address":5729944,"default_item":3,"flag":0},"POKEDEX_REWARD_290":{"address":5729946,"default_item":3,"flag":0},"POKEDEX_REWARD_291":{"address":5729948,"default_item":3,"flag":0},"POKEDEX_REWARD_292":{"address":5729950,"default_item":3,"flag":0},"POKEDEX_REWARD_293":{"address":5729952,"default_item":3,"flag":0},"POKEDEX_REWARD_294":{"address":5729954,"default_item":3,"flag":0},"POKEDEX_REWARD_295":{"address":5729956,"default_item":3,"flag":0},"POKEDEX_REWARD_296":{"address":5729958,"default_item":3,"flag":0},"POKEDEX_REWARD_297":{"address":5729960,"default_item":3,"flag":0},"POKEDEX_REWARD_298":{"address":5729962,"default_item":3,"flag":0},"POKEDEX_REWARD_299":{"address":5729964,"default_item":3,"flag":0},"POKEDEX_REWARD_300":{"address":5729966,"default_item":3,"flag":0},"POKEDEX_REWARD_301":{"address":5729968,"default_item":3,"flag":0},"POKEDEX_REWARD_302":{"address":5729970,"default_item":3,"flag":0},"POKEDEX_REWARD_303":{"address":5729972,"default_item":3,"flag":0},"POKEDEX_REWARD_304":{"address":5729974,"default_item":3,"flag":0},"POKEDEX_REWARD_305":{"address":5729976,"default_item":3,"flag":0},"POKEDEX_REWARD_306":{"address":5729978,"default_item":3,"flag":0},"POKEDEX_REWARD_307":{"address":5729980,"default_item":3,"flag":0},"POKEDEX_REWARD_308":{"address":5729982,"default_item":3,"flag":0},"POKEDEX_REWARD_309":{"address":5729984,"default_item":3,"flag":0},"POKEDEX_REWARD_310":{"address":5729986,"default_item":3,"flag":0},"POKEDEX_REWARD_311":{"address":5729988,"default_item":3,"flag":0},"POKEDEX_REWARD_312":{"address":5729990,"default_item":3,"flag":0},"POKEDEX_REWARD_313":{"address":5729992,"default_item":3,"flag":0},"POKEDEX_REWARD_314":{"address":5729994,"default_item":3,"flag":0},"POKEDEX_REWARD_315":{"address":5729996,"default_item":3,"flag":0},"POKEDEX_REWARD_316":{"address":5729998,"default_item":3,"flag":0},"POKEDEX_REWARD_317":{"address":5730000,"default_item":3,"flag":0},"POKEDEX_REWARD_318":{"address":5730002,"default_item":3,"flag":0},"POKEDEX_REWARD_319":{"address":5730004,"default_item":3,"flag":0},"POKEDEX_REWARD_320":{"address":5730006,"default_item":3,"flag":0},"POKEDEX_REWARD_321":{"address":5730008,"default_item":3,"flag":0},"POKEDEX_REWARD_322":{"address":5730010,"default_item":3,"flag":0},"POKEDEX_REWARD_323":{"address":5730012,"default_item":3,"flag":0},"POKEDEX_REWARD_324":{"address":5730014,"default_item":3,"flag":0},"POKEDEX_REWARD_325":{"address":5730016,"default_item":3,"flag":0},"POKEDEX_REWARD_326":{"address":5730018,"default_item":3,"flag":0},"POKEDEX_REWARD_327":{"address":5730020,"default_item":3,"flag":0},"POKEDEX_REWARD_328":{"address":5730022,"default_item":3,"flag":0},"POKEDEX_REWARD_329":{"address":5730024,"default_item":3,"flag":0},"POKEDEX_REWARD_330":{"address":5730026,"default_item":3,"flag":0},"POKEDEX_REWARD_331":{"address":5730028,"default_item":3,"flag":0},"POKEDEX_REWARD_332":{"address":5730030,"default_item":3,"flag":0},"POKEDEX_REWARD_333":{"address":5730032,"default_item":3,"flag":0},"POKEDEX_REWARD_334":{"address":5730034,"default_item":3,"flag":0},"POKEDEX_REWARD_335":{"address":5730036,"default_item":3,"flag":0},"POKEDEX_REWARD_336":{"address":5730038,"default_item":3,"flag":0},"POKEDEX_REWARD_337":{"address":5730040,"default_item":3,"flag":0},"POKEDEX_REWARD_338":{"address":5730042,"default_item":3,"flag":0},"POKEDEX_REWARD_339":{"address":5730044,"default_item":3,"flag":0},"POKEDEX_REWARD_340":{"address":5730046,"default_item":3,"flag":0},"POKEDEX_REWARD_341":{"address":5730048,"default_item":3,"flag":0},"POKEDEX_REWARD_342":{"address":5730050,"default_item":3,"flag":0},"POKEDEX_REWARD_343":{"address":5730052,"default_item":3,"flag":0},"POKEDEX_REWARD_344":{"address":5730054,"default_item":3,"flag":0},"POKEDEX_REWARD_345":{"address":5730056,"default_item":3,"flag":0},"POKEDEX_REWARD_346":{"address":5730058,"default_item":3,"flag":0},"POKEDEX_REWARD_347":{"address":5730060,"default_item":3,"flag":0},"POKEDEX_REWARD_348":{"address":5730062,"default_item":3,"flag":0},"POKEDEX_REWARD_349":{"address":5730064,"default_item":3,"flag":0},"POKEDEX_REWARD_350":{"address":5730066,"default_item":3,"flag":0},"POKEDEX_REWARD_351":{"address":5730068,"default_item":3,"flag":0},"POKEDEX_REWARD_352":{"address":5730070,"default_item":3,"flag":0},"POKEDEX_REWARD_353":{"address":5730072,"default_item":3,"flag":0},"POKEDEX_REWARD_354":{"address":5730074,"default_item":3,"flag":0},"POKEDEX_REWARD_355":{"address":5730076,"default_item":3,"flag":0},"POKEDEX_REWARD_356":{"address":5730078,"default_item":3,"flag":0},"POKEDEX_REWARD_357":{"address":5730080,"default_item":3,"flag":0},"POKEDEX_REWARD_358":{"address":5730082,"default_item":3,"flag":0},"POKEDEX_REWARD_359":{"address":5730084,"default_item":3,"flag":0},"POKEDEX_REWARD_360":{"address":5730086,"default_item":3,"flag":0},"POKEDEX_REWARD_361":{"address":5730088,"default_item":3,"flag":0},"POKEDEX_REWARD_362":{"address":5730090,"default_item":3,"flag":0},"POKEDEX_REWARD_363":{"address":5730092,"default_item":3,"flag":0},"POKEDEX_REWARD_364":{"address":5730094,"default_item":3,"flag":0},"POKEDEX_REWARD_365":{"address":5730096,"default_item":3,"flag":0},"POKEDEX_REWARD_366":{"address":5730098,"default_item":3,"flag":0},"POKEDEX_REWARD_367":{"address":5730100,"default_item":3,"flag":0},"POKEDEX_REWARD_368":{"address":5730102,"default_item":3,"flag":0},"POKEDEX_REWARD_369":{"address":5730104,"default_item":3,"flag":0},"POKEDEX_REWARD_370":{"address":5730106,"default_item":3,"flag":0},"POKEDEX_REWARD_371":{"address":5730108,"default_item":3,"flag":0},"POKEDEX_REWARD_372":{"address":5730110,"default_item":3,"flag":0},"POKEDEX_REWARD_373":{"address":5730112,"default_item":3,"flag":0},"POKEDEX_REWARD_374":{"address":5730114,"default_item":3,"flag":0},"POKEDEX_REWARD_375":{"address":5730116,"default_item":3,"flag":0},"POKEDEX_REWARD_376":{"address":5730118,"default_item":3,"flag":0},"POKEDEX_REWARD_377":{"address":5730120,"default_item":3,"flag":0},"POKEDEX_REWARD_378":{"address":5730122,"default_item":3,"flag":0},"POKEDEX_REWARD_379":{"address":5730124,"default_item":3,"flag":0},"POKEDEX_REWARD_380":{"address":5730126,"default_item":3,"flag":0},"POKEDEX_REWARD_381":{"address":5730128,"default_item":3,"flag":0},"POKEDEX_REWARD_382":{"address":5730130,"default_item":3,"flag":0},"POKEDEX_REWARD_383":{"address":5730132,"default_item":3,"flag":0},"POKEDEX_REWARD_384":{"address":5730134,"default_item":3,"flag":0},"POKEDEX_REWARD_385":{"address":5730136,"default_item":3,"flag":0},"POKEDEX_REWARD_386":{"address":5730138,"default_item":3,"flag":0},"TRAINER_AARON_REWARD":{"address":5602878,"default_item":104,"flag":1677},"TRAINER_ABIGAIL_1_REWARD":{"address":5602800,"default_item":106,"flag":1638},"TRAINER_AIDAN_REWARD":{"address":5603432,"default_item":104,"flag":1954},"TRAINER_AISHA_REWARD":{"address":5603598,"default_item":106,"flag":2037},"TRAINER_ALBERTO_REWARD":{"address":5602108,"default_item":108,"flag":1292},"TRAINER_ALBERT_REWARD":{"address":5602244,"default_item":104,"flag":1360},"TRAINER_ALEXA_REWARD":{"address":5603424,"default_item":104,"flag":1950},"TRAINER_ALEXIA_REWARD":{"address":5602264,"default_item":104,"flag":1370},"TRAINER_ALEX_REWARD":{"address":5602910,"default_item":104,"flag":1693},"TRAINER_ALICE_REWARD":{"address":5602980,"default_item":103,"flag":1728},"TRAINER_ALIX_REWARD":{"address":5603584,"default_item":106,"flag":2030},"TRAINER_ALLEN_REWARD":{"address":5602750,"default_item":103,"flag":1613},"TRAINER_ALLISON_REWARD":{"address":5602858,"default_item":104,"flag":1667},"TRAINER_ALYSSA_REWARD":{"address":5603486,"default_item":106,"flag":1981},"TRAINER_AMY_AND_LIV_1_REWARD":{"address":5603046,"default_item":103,"flag":1761},"TRAINER_ANDREA_REWARD":{"address":5603310,"default_item":106,"flag":1893},"TRAINER_ANDRES_1_REWARD":{"address":5603558,"default_item":104,"flag":2017},"TRAINER_ANDREW_REWARD":{"address":5602756,"default_item":106,"flag":1616},"TRAINER_ANGELICA_REWARD":{"address":5602956,"default_item":104,"flag":1716},"TRAINER_ANGELINA_REWARD":{"address":5603508,"default_item":106,"flag":1992},"TRAINER_ANGELO_REWARD":{"address":5603688,"default_item":104,"flag":2082},"TRAINER_ANNA_AND_MEG_1_REWARD":{"address":5602658,"default_item":106,"flag":1567},"TRAINER_ANNIKA_REWARD":{"address":5603088,"default_item":107,"flag":1782},"TRAINER_ANTHONY_REWARD":{"address":5602788,"default_item":106,"flag":1632},"TRAINER_ARCHIE_REWARD":{"address":5602152,"default_item":107,"flag":1314},"TRAINER_ASHLEY_REWARD":{"address":5603394,"default_item":106,"flag":1935},"TRAINER_ATHENA_REWARD":{"address":5603238,"default_item":104,"flag":1857},"TRAINER_ATSUSHI_REWARD":{"address":5602464,"default_item":104,"flag":1470},"TRAINER_AURON_REWARD":{"address":5603096,"default_item":104,"flag":1786},"TRAINER_AUSTINA_REWARD":{"address":5602200,"default_item":103,"flag":1338},"TRAINER_AUTUMN_REWARD":{"address":5602518,"default_item":106,"flag":1497},"TRAINER_AXLE_REWARD":{"address":5602490,"default_item":108,"flag":1483},"TRAINER_BARNY_REWARD":{"address":5602770,"default_item":104,"flag":1623},"TRAINER_BARRY_REWARD":{"address":5602410,"default_item":106,"flag":1443},"TRAINER_BEAU_REWARD":{"address":5602508,"default_item":106,"flag":1492},"TRAINER_BECKY_REWARD":{"address":5603024,"default_item":106,"flag":1750},"TRAINER_BECK_REWARD":{"address":5602912,"default_item":104,"flag":1694},"TRAINER_BENJAMIN_1_REWARD":{"address":5602790,"default_item":106,"flag":1633},"TRAINER_BEN_REWARD":{"address":5602730,"default_item":106,"flag":1603},"TRAINER_BERKE_REWARD":{"address":5602232,"default_item":104,"flag":1354},"TRAINER_BERNIE_1_REWARD":{"address":5602496,"default_item":106,"flag":1486},"TRAINER_BETHANY_REWARD":{"address":5602686,"default_item":107,"flag":1581},"TRAINER_BETH_REWARD":{"address":5602974,"default_item":103,"flag":1725},"TRAINER_BEVERLY_REWARD":{"address":5602966,"default_item":103,"flag":1721},"TRAINER_BIANCA_REWARD":{"address":5603496,"default_item":106,"flag":1986},"TRAINER_BILLY_REWARD":{"address":5602722,"default_item":103,"flag":1599},"TRAINER_BLAKE_REWARD":{"address":5602554,"default_item":108,"flag":1515},"TRAINER_BRANDEN_REWARD":{"address":5603574,"default_item":106,"flag":2025},"TRAINER_BRANDI_REWARD":{"address":5603596,"default_item":106,"flag":2036},"TRAINER_BRAWLY_1_REWARD":{"address":5602616,"default_item":104,"flag":1546},"TRAINER_BRAXTON_REWARD":{"address":5602234,"default_item":104,"flag":1355},"TRAINER_BRENDAN_LILYCOVE_MUDKIP_REWARD":{"address":5603406,"default_item":104,"flag":1941},"TRAINER_BRENDAN_LILYCOVE_TORCHIC_REWARD":{"address":5603410,"default_item":104,"flag":1943},"TRAINER_BRENDAN_LILYCOVE_TREECKO_REWARD":{"address":5603408,"default_item":104,"flag":1942},"TRAINER_BRENDAN_ROUTE_103_MUDKIP_REWARD":{"address":5603124,"default_item":106,"flag":1800},"TRAINER_BRENDAN_ROUTE_103_TORCHIC_REWARD":{"address":5603136,"default_item":106,"flag":1806},"TRAINER_BRENDAN_ROUTE_103_TREECKO_REWARD":{"address":5603130,"default_item":106,"flag":1803},"TRAINER_BRENDAN_ROUTE_110_MUDKIP_REWARD":{"address":5603126,"default_item":104,"flag":1801},"TRAINER_BRENDAN_ROUTE_110_TORCHIC_REWARD":{"address":5603138,"default_item":104,"flag":1807},"TRAINER_BRENDAN_ROUTE_110_TREECKO_REWARD":{"address":5603132,"default_item":104,"flag":1804},"TRAINER_BRENDAN_ROUTE_119_MUDKIP_REWARD":{"address":5603128,"default_item":104,"flag":1802},"TRAINER_BRENDAN_ROUTE_119_TORCHIC_REWARD":{"address":5603140,"default_item":104,"flag":1808},"TRAINER_BRENDAN_ROUTE_119_TREECKO_REWARD":{"address":5603134,"default_item":104,"flag":1805},"TRAINER_BRENDAN_RUSTBORO_MUDKIP_REWARD":{"address":5603270,"default_item":108,"flag":1873},"TRAINER_BRENDAN_RUSTBORO_TORCHIC_REWARD":{"address":5603282,"default_item":108,"flag":1879},"TRAINER_BRENDAN_RUSTBORO_TREECKO_REWARD":{"address":5603268,"default_item":108,"flag":1872},"TRAINER_BRENDA_REWARD":{"address":5602992,"default_item":106,"flag":1734},"TRAINER_BRENDEN_REWARD":{"address":5603228,"default_item":106,"flag":1852},"TRAINER_BRENT_REWARD":{"address":5602530,"default_item":104,"flag":1503},"TRAINER_BRIANNA_REWARD":{"address":5602320,"default_item":110,"flag":1398},"TRAINER_BRICE_REWARD":{"address":5603336,"default_item":106,"flag":1906},"TRAINER_BRIDGET_REWARD":{"address":5602342,"default_item":107,"flag":1409},"TRAINER_BROOKE_1_REWARD":{"address":5602272,"default_item":108,"flag":1374},"TRAINER_BRYANT_REWARD":{"address":5603576,"default_item":106,"flag":2026},"TRAINER_BRYAN_REWARD":{"address":5603572,"default_item":104,"flag":2024},"TRAINER_CALE_REWARD":{"address":5603612,"default_item":104,"flag":2044},"TRAINER_CALLIE_REWARD":{"address":5603610,"default_item":106,"flag":2043},"TRAINER_CALVIN_1_REWARD":{"address":5602720,"default_item":103,"flag":1598},"TRAINER_CAMDEN_REWARD":{"address":5602832,"default_item":104,"flag":1654},"TRAINER_CAMERON_1_REWARD":{"address":5602560,"default_item":108,"flag":1518},"TRAINER_CAMRON_REWARD":{"address":5603562,"default_item":104,"flag":2019},"TRAINER_CARLEE_REWARD":{"address":5603012,"default_item":106,"flag":1744},"TRAINER_CAROLINA_REWARD":{"address":5603566,"default_item":104,"flag":2021},"TRAINER_CAROLINE_REWARD":{"address":5602282,"default_item":104,"flag":1379},"TRAINER_CAROL_REWARD":{"address":5603026,"default_item":106,"flag":1751},"TRAINER_CARTER_REWARD":{"address":5602774,"default_item":104,"flag":1625},"TRAINER_CATHERINE_1_REWARD":{"address":5603202,"default_item":104,"flag":1839},"TRAINER_CEDRIC_REWARD":{"address":5603034,"default_item":108,"flag":1755},"TRAINER_CELIA_REWARD":{"address":5603570,"default_item":106,"flag":2023},"TRAINER_CELINA_REWARD":{"address":5603494,"default_item":108,"flag":1985},"TRAINER_CHAD_REWARD":{"address":5602432,"default_item":106,"flag":1454},"TRAINER_CHANDLER_REWARD":{"address":5603480,"default_item":103,"flag":1978},"TRAINER_CHARLIE_REWARD":{"address":5602216,"default_item":103,"flag":1346},"TRAINER_CHARLOTTE_REWARD":{"address":5603512,"default_item":106,"flag":1994},"TRAINER_CHASE_REWARD":{"address":5602840,"default_item":104,"flag":1658},"TRAINER_CHESTER_REWARD":{"address":5602900,"default_item":108,"flag":1688},"TRAINER_CHIP_REWARD":{"address":5602174,"default_item":104,"flag":1325},"TRAINER_CHRIS_REWARD":{"address":5603470,"default_item":108,"flag":1973},"TRAINER_CINDY_1_REWARD":{"address":5602312,"default_item":104,"flag":1394},"TRAINER_CLARENCE_REWARD":{"address":5603244,"default_item":106,"flag":1860},"TRAINER_CLARISSA_REWARD":{"address":5602954,"default_item":104,"flag":1715},"TRAINER_CLARK_REWARD":{"address":5603346,"default_item":106,"flag":1911},"TRAINER_CLAUDE_REWARD":{"address":5602760,"default_item":108,"flag":1618},"TRAINER_CLIFFORD_REWARD":{"address":5603252,"default_item":107,"flag":1864},"TRAINER_COBY_REWARD":{"address":5603502,"default_item":106,"flag":1989},"TRAINER_COLE_REWARD":{"address":5602486,"default_item":108,"flag":1481},"TRAINER_COLIN_REWARD":{"address":5602894,"default_item":108,"flag":1685},"TRAINER_COLTON_REWARD":{"address":5602672,"default_item":107,"flag":1574},"TRAINER_CONNIE_REWARD":{"address":5602340,"default_item":107,"flag":1408},"TRAINER_CONOR_REWARD":{"address":5603106,"default_item":104,"flag":1791},"TRAINER_CORY_1_REWARD":{"address":5603564,"default_item":108,"flag":2020},"TRAINER_CRISSY_REWARD":{"address":5603312,"default_item":106,"flag":1894},"TRAINER_CRISTIAN_REWARD":{"address":5603232,"default_item":106,"flag":1854},"TRAINER_CRISTIN_1_REWARD":{"address":5603618,"default_item":104,"flag":2047},"TRAINER_CYNDY_1_REWARD":{"address":5602938,"default_item":106,"flag":1707},"TRAINER_DAISUKE_REWARD":{"address":5602462,"default_item":106,"flag":1469},"TRAINER_DAISY_REWARD":{"address":5602156,"default_item":106,"flag":1316},"TRAINER_DALE_REWARD":{"address":5602766,"default_item":106,"flag":1621},"TRAINER_DALTON_1_REWARD":{"address":5602476,"default_item":106,"flag":1476},"TRAINER_DANA_REWARD":{"address":5603000,"default_item":106,"flag":1738},"TRAINER_DANIELLE_REWARD":{"address":5603384,"default_item":106,"flag":1930},"TRAINER_DAPHNE_REWARD":{"address":5602314,"default_item":110,"flag":1395},"TRAINER_DARCY_REWARD":{"address":5603550,"default_item":104,"flag":2013},"TRAINER_DARIAN_REWARD":{"address":5603476,"default_item":106,"flag":1976},"TRAINER_DARIUS_REWARD":{"address":5603690,"default_item":108,"flag":2083},"TRAINER_DARRIN_REWARD":{"address":5602392,"default_item":103,"flag":1434},"TRAINER_DAVID_REWARD":{"address":5602400,"default_item":103,"flag":1438},"TRAINER_DAVIS_REWARD":{"address":5603162,"default_item":106,"flag":1819},"TRAINER_DAWSON_REWARD":{"address":5603472,"default_item":104,"flag":1974},"TRAINER_DAYTON_REWARD":{"address":5603604,"default_item":108,"flag":2040},"TRAINER_DEANDRE_REWARD":{"address":5603514,"default_item":103,"flag":1995},"TRAINER_DEAN_REWARD":{"address":5602412,"default_item":103,"flag":1444},"TRAINER_DEBRA_REWARD":{"address":5603004,"default_item":106,"flag":1740},"TRAINER_DECLAN_REWARD":{"address":5602114,"default_item":106,"flag":1295},"TRAINER_DEMETRIUS_REWARD":{"address":5602834,"default_item":106,"flag":1655},"TRAINER_DENISE_REWARD":{"address":5602972,"default_item":103,"flag":1724},"TRAINER_DEREK_REWARD":{"address":5602538,"default_item":108,"flag":1507},"TRAINER_DEVAN_REWARD":{"address":5603590,"default_item":106,"flag":2033},"TRAINER_DEZ_AND_LUKE_REWARD":{"address":5603364,"default_item":108,"flag":1920},"TRAINER_DIANA_1_REWARD":{"address":5603032,"default_item":106,"flag":1754},"TRAINER_DIANNE_REWARD":{"address":5602918,"default_item":104,"flag":1697},"TRAINER_DILLON_REWARD":{"address":5602738,"default_item":106,"flag":1607},"TRAINER_DOMINIK_REWARD":{"address":5602388,"default_item":103,"flag":1432},"TRAINER_DONALD_REWARD":{"address":5602532,"default_item":104,"flag":1504},"TRAINER_DONNY_REWARD":{"address":5602852,"default_item":104,"flag":1664},"TRAINER_DOUGLAS_REWARD":{"address":5602390,"default_item":103,"flag":1433},"TRAINER_DOUG_REWARD":{"address":5603320,"default_item":106,"flag":1898},"TRAINER_DRAKE_REWARD":{"address":5602612,"default_item":110,"flag":1544},"TRAINER_DREW_REWARD":{"address":5602506,"default_item":106,"flag":1491},"TRAINER_DUNCAN_REWARD":{"address":5603076,"default_item":108,"flag":1776},"TRAINER_DUSTY_1_REWARD":{"address":5602172,"default_item":104,"flag":1324},"TRAINER_DWAYNE_REWARD":{"address":5603070,"default_item":106,"flag":1773},"TRAINER_DYLAN_1_REWARD":{"address":5602812,"default_item":106,"flag":1644},"TRAINER_EDGAR_REWARD":{"address":5602242,"default_item":104,"flag":1359},"TRAINER_EDMOND_REWARD":{"address":5603066,"default_item":106,"flag":1771},"TRAINER_EDWARDO_REWARD":{"address":5602892,"default_item":108,"flag":1684},"TRAINER_EDWARD_REWARD":{"address":5602548,"default_item":106,"flag":1512},"TRAINER_EDWIN_1_REWARD":{"address":5603108,"default_item":108,"flag":1792},"TRAINER_ED_REWARD":{"address":5602110,"default_item":104,"flag":1293},"TRAINER_ELIJAH_REWARD":{"address":5603568,"default_item":108,"flag":2022},"TRAINER_ELI_REWARD":{"address":5603086,"default_item":108,"flag":1781},"TRAINER_ELLIOT_1_REWARD":{"address":5602762,"default_item":106,"flag":1619},"TRAINER_ERIC_REWARD":{"address":5603348,"default_item":108,"flag":1912},"TRAINER_ERNEST_1_REWARD":{"address":5603068,"default_item":104,"flag":1772},"TRAINER_ETHAN_1_REWARD":{"address":5602516,"default_item":106,"flag":1496},"TRAINER_FABIAN_REWARD":{"address":5603602,"default_item":108,"flag":2039},"TRAINER_FELIX_REWARD":{"address":5602160,"default_item":104,"flag":1318},"TRAINER_FERNANDO_1_REWARD":{"address":5602474,"default_item":108,"flag":1475},"TRAINER_FLANNERY_1_REWARD":{"address":5602620,"default_item":107,"flag":1548},"TRAINER_FLINT_REWARD":{"address":5603392,"default_item":106,"flag":1934},"TRAINER_FOSTER_REWARD":{"address":5602176,"default_item":104,"flag":1326},"TRAINER_FRANKLIN_REWARD":{"address":5602424,"default_item":106,"flag":1450},"TRAINER_FREDRICK_REWARD":{"address":5602142,"default_item":104,"flag":1309},"TRAINER_GABRIELLE_1_REWARD":{"address":5602102,"default_item":104,"flag":1289},"TRAINER_GARRET_REWARD":{"address":5602360,"default_item":110,"flag":1418},"TRAINER_GARRISON_REWARD":{"address":5603178,"default_item":104,"flag":1827},"TRAINER_GEORGE_REWARD":{"address":5602230,"default_item":104,"flag":1353},"TRAINER_GERALD_REWARD":{"address":5603380,"default_item":104,"flag":1928},"TRAINER_GILBERT_REWARD":{"address":5602422,"default_item":106,"flag":1449},"TRAINER_GINA_AND_MIA_1_REWARD":{"address":5603050,"default_item":103,"flag":1763},"TRAINER_GLACIA_REWARD":{"address":5602610,"default_item":110,"flag":1543},"TRAINER_GRACE_REWARD":{"address":5602984,"default_item":106,"flag":1730},"TRAINER_GREG_REWARD":{"address":5603322,"default_item":106,"flag":1899},"TRAINER_GRUNT_AQUA_HIDEOUT_1_REWARD":{"address":5602088,"default_item":106,"flag":1282},"TRAINER_GRUNT_AQUA_HIDEOUT_2_REWARD":{"address":5602090,"default_item":106,"flag":1283},"TRAINER_GRUNT_AQUA_HIDEOUT_3_REWARD":{"address":5602092,"default_item":106,"flag":1284},"TRAINER_GRUNT_AQUA_HIDEOUT_4_REWARD":{"address":5602094,"default_item":106,"flag":1285},"TRAINER_GRUNT_AQUA_HIDEOUT_5_REWARD":{"address":5602138,"default_item":106,"flag":1307},"TRAINER_GRUNT_AQUA_HIDEOUT_6_REWARD":{"address":5602140,"default_item":106,"flag":1308},"TRAINER_GRUNT_AQUA_HIDEOUT_7_REWARD":{"address":5602468,"default_item":106,"flag":1472},"TRAINER_GRUNT_AQUA_HIDEOUT_8_REWARD":{"address":5602470,"default_item":106,"flag":1473},"TRAINER_GRUNT_MAGMA_HIDEOUT_10_REWARD":{"address":5603534,"default_item":106,"flag":2005},"TRAINER_GRUNT_MAGMA_HIDEOUT_11_REWARD":{"address":5603536,"default_item":106,"flag":2006},"TRAINER_GRUNT_MAGMA_HIDEOUT_12_REWARD":{"address":5603538,"default_item":106,"flag":2007},"TRAINER_GRUNT_MAGMA_HIDEOUT_13_REWARD":{"address":5603540,"default_item":106,"flag":2008},"TRAINER_GRUNT_MAGMA_HIDEOUT_14_REWARD":{"address":5603542,"default_item":106,"flag":2009},"TRAINER_GRUNT_MAGMA_HIDEOUT_15_REWARD":{"address":5603544,"default_item":106,"flag":2010},"TRAINER_GRUNT_MAGMA_HIDEOUT_16_REWARD":{"address":5603546,"default_item":106,"flag":2011},"TRAINER_GRUNT_MAGMA_HIDEOUT_1_REWARD":{"address":5603516,"default_item":106,"flag":1996},"TRAINER_GRUNT_MAGMA_HIDEOUT_2_REWARD":{"address":5603518,"default_item":106,"flag":1997},"TRAINER_GRUNT_MAGMA_HIDEOUT_3_REWARD":{"address":5603520,"default_item":106,"flag":1998},"TRAINER_GRUNT_MAGMA_HIDEOUT_4_REWARD":{"address":5603522,"default_item":106,"flag":1999},"TRAINER_GRUNT_MAGMA_HIDEOUT_5_REWARD":{"address":5603524,"default_item":106,"flag":2000},"TRAINER_GRUNT_MAGMA_HIDEOUT_6_REWARD":{"address":5603526,"default_item":106,"flag":2001},"TRAINER_GRUNT_MAGMA_HIDEOUT_7_REWARD":{"address":5603528,"default_item":106,"flag":2002},"TRAINER_GRUNT_MAGMA_HIDEOUT_8_REWARD":{"address":5603530,"default_item":106,"flag":2003},"TRAINER_GRUNT_MAGMA_HIDEOUT_9_REWARD":{"address":5603532,"default_item":106,"flag":2004},"TRAINER_GRUNT_MT_CHIMNEY_1_REWARD":{"address":5602376,"default_item":106,"flag":1426},"TRAINER_GRUNT_MT_CHIMNEY_2_REWARD":{"address":5603242,"default_item":106,"flag":1859},"TRAINER_GRUNT_MT_PYRE_1_REWARD":{"address":5602130,"default_item":106,"flag":1303},"TRAINER_GRUNT_MT_PYRE_2_REWARD":{"address":5602132,"default_item":106,"flag":1304},"TRAINER_GRUNT_MT_PYRE_3_REWARD":{"address":5602134,"default_item":106,"flag":1305},"TRAINER_GRUNT_MT_PYRE_4_REWARD":{"address":5603222,"default_item":106,"flag":1849},"TRAINER_GRUNT_MUSEUM_1_REWARD":{"address":5602124,"default_item":106,"flag":1300},"TRAINER_GRUNT_MUSEUM_2_REWARD":{"address":5602126,"default_item":106,"flag":1301},"TRAINER_GRUNT_PETALBURG_WOODS_REWARD":{"address":5602104,"default_item":103,"flag":1290},"TRAINER_GRUNT_RUSTURF_TUNNEL_REWARD":{"address":5602116,"default_item":103,"flag":1296},"TRAINER_GRUNT_SEAFLOOR_CAVERN_1_REWARD":{"address":5602096,"default_item":108,"flag":1286},"TRAINER_GRUNT_SEAFLOOR_CAVERN_2_REWARD":{"address":5602098,"default_item":108,"flag":1287},"TRAINER_GRUNT_SEAFLOOR_CAVERN_3_REWARD":{"address":5602100,"default_item":108,"flag":1288},"TRAINER_GRUNT_SEAFLOOR_CAVERN_4_REWARD":{"address":5602112,"default_item":108,"flag":1294},"TRAINER_GRUNT_SEAFLOOR_CAVERN_5_REWARD":{"address":5603218,"default_item":108,"flag":1847},"TRAINER_GRUNT_SPACE_CENTER_1_REWARD":{"address":5602128,"default_item":106,"flag":1302},"TRAINER_GRUNT_SPACE_CENTER_2_REWARD":{"address":5602316,"default_item":106,"flag":1396},"TRAINER_GRUNT_SPACE_CENTER_3_REWARD":{"address":5603256,"default_item":106,"flag":1866},"TRAINER_GRUNT_SPACE_CENTER_4_REWARD":{"address":5603258,"default_item":106,"flag":1867},"TRAINER_GRUNT_SPACE_CENTER_5_REWARD":{"address":5603260,"default_item":106,"flag":1868},"TRAINER_GRUNT_SPACE_CENTER_6_REWARD":{"address":5603262,"default_item":106,"flag":1869},"TRAINER_GRUNT_SPACE_CENTER_7_REWARD":{"address":5603264,"default_item":106,"flag":1870},"TRAINER_GRUNT_WEATHER_INST_1_REWARD":{"address":5602118,"default_item":106,"flag":1297},"TRAINER_GRUNT_WEATHER_INST_2_REWARD":{"address":5602120,"default_item":106,"flag":1298},"TRAINER_GRUNT_WEATHER_INST_3_REWARD":{"address":5602122,"default_item":106,"flag":1299},"TRAINER_GRUNT_WEATHER_INST_4_REWARD":{"address":5602136,"default_item":106,"flag":1306},"TRAINER_GRUNT_WEATHER_INST_5_REWARD":{"address":5603276,"default_item":106,"flag":1876},"TRAINER_GWEN_REWARD":{"address":5602202,"default_item":103,"flag":1339},"TRAINER_HAILEY_REWARD":{"address":5603478,"default_item":103,"flag":1977},"TRAINER_HALEY_1_REWARD":{"address":5603292,"default_item":103,"flag":1884},"TRAINER_HALLE_REWARD":{"address":5603176,"default_item":104,"flag":1826},"TRAINER_HANNAH_REWARD":{"address":5602572,"default_item":108,"flag":1524},"TRAINER_HARRISON_REWARD":{"address":5603240,"default_item":106,"flag":1858},"TRAINER_HAYDEN_REWARD":{"address":5603498,"default_item":106,"flag":1987},"TRAINER_HECTOR_REWARD":{"address":5603110,"default_item":104,"flag":1793},"TRAINER_HEIDI_REWARD":{"address":5603022,"default_item":106,"flag":1749},"TRAINER_HELENE_REWARD":{"address":5603586,"default_item":106,"flag":2031},"TRAINER_HENRY_REWARD":{"address":5603420,"default_item":104,"flag":1948},"TRAINER_HERMAN_REWARD":{"address":5602418,"default_item":106,"flag":1447},"TRAINER_HIDEO_REWARD":{"address":5603386,"default_item":106,"flag":1931},"TRAINER_HITOSHI_REWARD":{"address":5602444,"default_item":104,"flag":1460},"TRAINER_HOPE_REWARD":{"address":5602276,"default_item":104,"flag":1376},"TRAINER_HUDSON_REWARD":{"address":5603104,"default_item":104,"flag":1790},"TRAINER_HUEY_REWARD":{"address":5603064,"default_item":106,"flag":1770},"TRAINER_HUGH_REWARD":{"address":5602882,"default_item":108,"flag":1679},"TRAINER_HUMBERTO_REWARD":{"address":5602888,"default_item":108,"flag":1682},"TRAINER_IMANI_REWARD":{"address":5602968,"default_item":103,"flag":1722},"TRAINER_IRENE_REWARD":{"address":5603036,"default_item":106,"flag":1756},"TRAINER_ISAAC_1_REWARD":{"address":5603160,"default_item":106,"flag":1818},"TRAINER_ISABELLA_REWARD":{"address":5603274,"default_item":104,"flag":1875},"TRAINER_ISABELLE_REWARD":{"address":5603556,"default_item":103,"flag":2016},"TRAINER_ISABEL_1_REWARD":{"address":5602688,"default_item":104,"flag":1582},"TRAINER_ISAIAH_1_REWARD":{"address":5602836,"default_item":104,"flag":1656},"TRAINER_ISOBEL_REWARD":{"address":5602850,"default_item":104,"flag":1663},"TRAINER_IVAN_REWARD":{"address":5602758,"default_item":106,"flag":1617},"TRAINER_JACE_REWARD":{"address":5602492,"default_item":108,"flag":1484},"TRAINER_JACKI_1_REWARD":{"address":5602582,"default_item":108,"flag":1529},"TRAINER_JACKSON_1_REWARD":{"address":5603188,"default_item":104,"flag":1832},"TRAINER_JACK_REWARD":{"address":5602428,"default_item":106,"flag":1452},"TRAINER_JACLYN_REWARD":{"address":5602570,"default_item":106,"flag":1523},"TRAINER_JACOB_REWARD":{"address":5602786,"default_item":106,"flag":1631},"TRAINER_JAIDEN_REWARD":{"address":5603582,"default_item":106,"flag":2029},"TRAINER_JAMES_1_REWARD":{"address":5603326,"default_item":103,"flag":1901},"TRAINER_JANICE_REWARD":{"address":5603294,"default_item":103,"flag":1885},"TRAINER_JANI_REWARD":{"address":5602920,"default_item":103,"flag":1698},"TRAINER_JARED_REWARD":{"address":5602886,"default_item":108,"flag":1681},"TRAINER_JASMINE_REWARD":{"address":5602802,"default_item":103,"flag":1639},"TRAINER_JAYLEN_REWARD":{"address":5602736,"default_item":106,"flag":1606},"TRAINER_JAZMYN_REWARD":{"address":5603090,"default_item":106,"flag":1783},"TRAINER_JEFFREY_1_REWARD":{"address":5602536,"default_item":104,"flag":1506},"TRAINER_JEFF_REWARD":{"address":5602488,"default_item":108,"flag":1482},"TRAINER_JENNA_REWARD":{"address":5603204,"default_item":104,"flag":1840},"TRAINER_JENNIFER_REWARD":{"address":5602274,"default_item":104,"flag":1375},"TRAINER_JENNY_1_REWARD":{"address":5602982,"default_item":106,"flag":1729},"TRAINER_JEROME_REWARD":{"address":5602396,"default_item":103,"flag":1436},"TRAINER_JERRY_1_REWARD":{"address":5602630,"default_item":103,"flag":1553},"TRAINER_JESSICA_1_REWARD":{"address":5602338,"default_item":104,"flag":1407},"TRAINER_JOCELYN_REWARD":{"address":5602934,"default_item":106,"flag":1705},"TRAINER_JODY_REWARD":{"address":5602266,"default_item":104,"flag":1371},"TRAINER_JOEY_REWARD":{"address":5602728,"default_item":103,"flag":1602},"TRAINER_JOHANNA_REWARD":{"address":5603378,"default_item":104,"flag":1927},"TRAINER_JOHNSON_REWARD":{"address":5603592,"default_item":103,"flag":2034},"TRAINER_JOHN_AND_JAY_1_REWARD":{"address":5603446,"default_item":104,"flag":1961},"TRAINER_JONAH_REWARD":{"address":5603418,"default_item":104,"flag":1947},"TRAINER_JONAS_REWARD":{"address":5603092,"default_item":106,"flag":1784},"TRAINER_JONATHAN_REWARD":{"address":5603280,"default_item":104,"flag":1878},"TRAINER_JOSEPH_REWARD":{"address":5603484,"default_item":106,"flag":1980},"TRAINER_JOSE_REWARD":{"address":5603318,"default_item":103,"flag":1897},"TRAINER_JOSH_REWARD":{"address":5602724,"default_item":103,"flag":1600},"TRAINER_JOSUE_REWARD":{"address":5603560,"default_item":108,"flag":2018},"TRAINER_JUAN_1_REWARD":{"address":5602628,"default_item":109,"flag":1552},"TRAINER_JULIE_REWARD":{"address":5602284,"default_item":104,"flag":1380},"TRAINER_JULIO_REWARD":{"address":5603216,"default_item":108,"flag":1846},"TRAINER_KAI_REWARD":{"address":5603510,"default_item":108,"flag":1993},"TRAINER_KALEB_REWARD":{"address":5603482,"default_item":104,"flag":1979},"TRAINER_KARA_REWARD":{"address":5602998,"default_item":106,"flag":1737},"TRAINER_KAREN_1_REWARD":{"address":5602644,"default_item":103,"flag":1560},"TRAINER_KATELYNN_REWARD":{"address":5602734,"default_item":104,"flag":1605},"TRAINER_KATELYN_1_REWARD":{"address":5602856,"default_item":104,"flag":1666},"TRAINER_KATE_AND_JOY_REWARD":{"address":5602656,"default_item":106,"flag":1566},"TRAINER_KATHLEEN_REWARD":{"address":5603250,"default_item":108,"flag":1863},"TRAINER_KATIE_REWARD":{"address":5602994,"default_item":106,"flag":1735},"TRAINER_KAYLA_REWARD":{"address":5602578,"default_item":106,"flag":1527},"TRAINER_KAYLEY_REWARD":{"address":5603094,"default_item":104,"flag":1785},"TRAINER_KEEGAN_REWARD":{"address":5602494,"default_item":108,"flag":1485},"TRAINER_KEIGO_REWARD":{"address":5603388,"default_item":106,"flag":1932},"TRAINER_KELVIN_REWARD":{"address":5603098,"default_item":104,"flag":1787},"TRAINER_KENT_REWARD":{"address":5603324,"default_item":106,"flag":1900},"TRAINER_KEVIN_REWARD":{"address":5602426,"default_item":106,"flag":1451},"TRAINER_KIM_AND_IRIS_REWARD":{"address":5603440,"default_item":106,"flag":1958},"TRAINER_KINDRA_REWARD":{"address":5602296,"default_item":108,"flag":1386},"TRAINER_KIRA_AND_DAN_1_REWARD":{"address":5603368,"default_item":108,"flag":1922},"TRAINER_KIRK_REWARD":{"address":5602466,"default_item":106,"flag":1471},"TRAINER_KIYO_REWARD":{"address":5602446,"default_item":104,"flag":1461},"TRAINER_KOICHI_REWARD":{"address":5602448,"default_item":108,"flag":1462},"TRAINER_KOJI_1_REWARD":{"address":5603428,"default_item":104,"flag":1952},"TRAINER_KYLA_REWARD":{"address":5602970,"default_item":103,"flag":1723},"TRAINER_KYRA_REWARD":{"address":5603580,"default_item":104,"flag":2028},"TRAINER_LAO_1_REWARD":{"address":5602922,"default_item":103,"flag":1699},"TRAINER_LARRY_REWARD":{"address":5602510,"default_item":106,"flag":1493},"TRAINER_LAURA_REWARD":{"address":5602936,"default_item":106,"flag":1706},"TRAINER_LAUREL_REWARD":{"address":5603010,"default_item":106,"flag":1743},"TRAINER_LAWRENCE_REWARD":{"address":5603504,"default_item":106,"flag":1990},"TRAINER_LEAH_REWARD":{"address":5602154,"default_item":108,"flag":1315},"TRAINER_LEA_AND_JED_REWARD":{"address":5603366,"default_item":104,"flag":1921},"TRAINER_LENNY_REWARD":{"address":5603340,"default_item":108,"flag":1908},"TRAINER_LEONARDO_REWARD":{"address":5603236,"default_item":106,"flag":1856},"TRAINER_LEONARD_REWARD":{"address":5603074,"default_item":104,"flag":1775},"TRAINER_LEONEL_REWARD":{"address":5603608,"default_item":104,"flag":2042},"TRAINER_LILA_AND_ROY_1_REWARD":{"address":5603458,"default_item":106,"flag":1967},"TRAINER_LILITH_REWARD":{"address":5603230,"default_item":106,"flag":1853},"TRAINER_LINDA_REWARD":{"address":5603006,"default_item":106,"flag":1741},"TRAINER_LISA_AND_RAY_REWARD":{"address":5603468,"default_item":106,"flag":1972},"TRAINER_LOLA_1_REWARD":{"address":5602198,"default_item":103,"flag":1337},"TRAINER_LORENZO_REWARD":{"address":5603190,"default_item":104,"flag":1833},"TRAINER_LUCAS_1_REWARD":{"address":5603342,"default_item":108,"flag":1909},"TRAINER_LUIS_REWARD":{"address":5602386,"default_item":103,"flag":1431},"TRAINER_LUNG_REWARD":{"address":5602924,"default_item":103,"flag":1700},"TRAINER_LYDIA_1_REWARD":{"address":5603174,"default_item":106,"flag":1825},"TRAINER_LYLE_REWARD":{"address":5603316,"default_item":103,"flag":1896},"TRAINER_MACEY_REWARD":{"address":5603266,"default_item":108,"flag":1871},"TRAINER_MADELINE_1_REWARD":{"address":5602952,"default_item":108,"flag":1714},"TRAINER_MAKAYLA_REWARD":{"address":5603600,"default_item":104,"flag":2038},"TRAINER_MARCEL_REWARD":{"address":5602106,"default_item":104,"flag":1291},"TRAINER_MARCOS_REWARD":{"address":5603488,"default_item":106,"flag":1982},"TRAINER_MARC_REWARD":{"address":5603226,"default_item":106,"flag":1851},"TRAINER_MARIA_1_REWARD":{"address":5602822,"default_item":106,"flag":1649},"TRAINER_MARK_REWARD":{"address":5602374,"default_item":104,"flag":1425},"TRAINER_MARLENE_REWARD":{"address":5603588,"default_item":106,"flag":2032},"TRAINER_MARLEY_REWARD":{"address":5603100,"default_item":104,"flag":1788},"TRAINER_MARY_REWARD":{"address":5602262,"default_item":104,"flag":1369},"TRAINER_MATTHEW_REWARD":{"address":5602398,"default_item":103,"flag":1437},"TRAINER_MATT_REWARD":{"address":5602144,"default_item":104,"flag":1310},"TRAINER_MAURA_REWARD":{"address":5602576,"default_item":108,"flag":1526},"TRAINER_MAXIE_MAGMA_HIDEOUT_REWARD":{"address":5603286,"default_item":107,"flag":1881},"TRAINER_MAXIE_MT_CHIMNEY_REWARD":{"address":5603288,"default_item":104,"flag":1882},"TRAINER_MAY_LILYCOVE_MUDKIP_REWARD":{"address":5603412,"default_item":104,"flag":1944},"TRAINER_MAY_LILYCOVE_TORCHIC_REWARD":{"address":5603416,"default_item":104,"flag":1946},"TRAINER_MAY_LILYCOVE_TREECKO_REWARD":{"address":5603414,"default_item":104,"flag":1945},"TRAINER_MAY_ROUTE_103_MUDKIP_REWARD":{"address":5603142,"default_item":106,"flag":1809},"TRAINER_MAY_ROUTE_103_TORCHIC_REWARD":{"address":5603154,"default_item":106,"flag":1815},"TRAINER_MAY_ROUTE_103_TREECKO_REWARD":{"address":5603148,"default_item":106,"flag":1812},"TRAINER_MAY_ROUTE_110_MUDKIP_REWARD":{"address":5603144,"default_item":104,"flag":1810},"TRAINER_MAY_ROUTE_110_TORCHIC_REWARD":{"address":5603156,"default_item":104,"flag":1816},"TRAINER_MAY_ROUTE_110_TREECKO_REWARD":{"address":5603150,"default_item":104,"flag":1813},"TRAINER_MAY_ROUTE_119_MUDKIP_REWARD":{"address":5603146,"default_item":104,"flag":1811},"TRAINER_MAY_ROUTE_119_TORCHIC_REWARD":{"address":5603158,"default_item":104,"flag":1817},"TRAINER_MAY_ROUTE_119_TREECKO_REWARD":{"address":5603152,"default_item":104,"flag":1814},"TRAINER_MAY_RUSTBORO_MUDKIP_REWARD":{"address":5603284,"default_item":108,"flag":1880},"TRAINER_MAY_RUSTBORO_TORCHIC_REWARD":{"address":5603622,"default_item":108,"flag":2049},"TRAINER_MAY_RUSTBORO_TREECKO_REWARD":{"address":5603620,"default_item":108,"flag":2048},"TRAINER_MELINA_REWARD":{"address":5603594,"default_item":106,"flag":2035},"TRAINER_MELISSA_REWARD":{"address":5602332,"default_item":104,"flag":1404},"TRAINER_MEL_AND_PAUL_REWARD":{"address":5603444,"default_item":108,"flag":1960},"TRAINER_MICAH_REWARD":{"address":5602594,"default_item":107,"flag":1535},"TRAINER_MICHELLE_REWARD":{"address":5602280,"default_item":104,"flag":1378},"TRAINER_MIGUEL_1_REWARD":{"address":5602670,"default_item":104,"flag":1573},"TRAINER_MIKE_2_REWARD":{"address":5603354,"default_item":106,"flag":1915},"TRAINER_MISSY_REWARD":{"address":5602978,"default_item":103,"flag":1727},"TRAINER_MITCHELL_REWARD":{"address":5603164,"default_item":104,"flag":1820},"TRAINER_MIU_AND_YUKI_REWARD":{"address":5603052,"default_item":106,"flag":1764},"TRAINER_MOLLIE_REWARD":{"address":5602358,"default_item":104,"flag":1417},"TRAINER_MYLES_REWARD":{"address":5603614,"default_item":104,"flag":2045},"TRAINER_NANCY_REWARD":{"address":5603028,"default_item":106,"flag":1752},"TRAINER_NAOMI_REWARD":{"address":5602322,"default_item":110,"flag":1399},"TRAINER_NATE_REWARD":{"address":5603248,"default_item":107,"flag":1862},"TRAINER_NED_REWARD":{"address":5602764,"default_item":106,"flag":1620},"TRAINER_NICHOLAS_REWARD":{"address":5603254,"default_item":108,"flag":1865},"TRAINER_NICOLAS_1_REWARD":{"address":5602868,"default_item":104,"flag":1672},"TRAINER_NIKKI_REWARD":{"address":5602990,"default_item":106,"flag":1733},"TRAINER_NOB_1_REWARD":{"address":5602450,"default_item":106,"flag":1463},"TRAINER_NOLAN_REWARD":{"address":5602768,"default_item":108,"flag":1622},"TRAINER_NOLEN_REWARD":{"address":5602406,"default_item":106,"flag":1441},"TRAINER_NORMAN_1_REWARD":{"address":5602622,"default_item":107,"flag":1549},"TRAINER_OLIVIA_REWARD":{"address":5602344,"default_item":107,"flag":1410},"TRAINER_OWEN_REWARD":{"address":5602250,"default_item":104,"flag":1363},"TRAINER_PABLO_1_REWARD":{"address":5602838,"default_item":104,"flag":1657},"TRAINER_PARKER_REWARD":{"address":5602228,"default_item":104,"flag":1352},"TRAINER_PAT_REWARD":{"address":5603616,"default_item":104,"flag":2046},"TRAINER_PAXTON_REWARD":{"address":5603272,"default_item":104,"flag":1874},"TRAINER_PERRY_REWARD":{"address":5602880,"default_item":108,"flag":1678},"TRAINER_PETE_REWARD":{"address":5603554,"default_item":103,"flag":2015},"TRAINER_PHILLIP_REWARD":{"address":5603072,"default_item":104,"flag":1774},"TRAINER_PHIL_REWARD":{"address":5602884,"default_item":108,"flag":1680},"TRAINER_PHOEBE_REWARD":{"address":5602608,"default_item":110,"flag":1542},"TRAINER_PRESLEY_REWARD":{"address":5602890,"default_item":104,"flag":1683},"TRAINER_PRESTON_REWARD":{"address":5602550,"default_item":108,"flag":1513},"TRAINER_QUINCY_REWARD":{"address":5602732,"default_item":104,"flag":1604},"TRAINER_RACHEL_REWARD":{"address":5603606,"default_item":104,"flag":2041},"TRAINER_RANDALL_REWARD":{"address":5602226,"default_item":104,"flag":1351},"TRAINER_REED_REWARD":{"address":5603434,"default_item":106,"flag":1955},"TRAINER_RELI_AND_IAN_REWARD":{"address":5603456,"default_item":106,"flag":1966},"TRAINER_REYNA_REWARD":{"address":5603102,"default_item":108,"flag":1789},"TRAINER_RHETT_REWARD":{"address":5603490,"default_item":106,"flag":1983},"TRAINER_RICHARD_REWARD":{"address":5602416,"default_item":106,"flag":1446},"TRAINER_RICKY_1_REWARD":{"address":5602212,"default_item":103,"flag":1344},"TRAINER_RICK_REWARD":{"address":5603314,"default_item":103,"flag":1895},"TRAINER_RILEY_REWARD":{"address":5603390,"default_item":106,"flag":1933},"TRAINER_ROBERT_1_REWARD":{"address":5602896,"default_item":108,"flag":1686},"TRAINER_RODNEY_REWARD":{"address":5602414,"default_item":106,"flag":1445},"TRAINER_ROGER_REWARD":{"address":5603422,"default_item":104,"flag":1949},"TRAINER_ROLAND_REWARD":{"address":5602404,"default_item":106,"flag":1440},"TRAINER_RONALD_REWARD":{"address":5602784,"default_item":104,"flag":1630},"TRAINER_ROSE_1_REWARD":{"address":5602158,"default_item":106,"flag":1317},"TRAINER_ROXANNE_1_REWARD":{"address":5602614,"default_item":104,"flag":1545},"TRAINER_RUBEN_REWARD":{"address":5603426,"default_item":104,"flag":1951},"TRAINER_SAMANTHA_REWARD":{"address":5602574,"default_item":108,"flag":1525},"TRAINER_SAMUEL_REWARD":{"address":5602246,"default_item":104,"flag":1361},"TRAINER_SANTIAGO_REWARD":{"address":5602420,"default_item":106,"flag":1448},"TRAINER_SARAH_REWARD":{"address":5603474,"default_item":104,"flag":1975},"TRAINER_SAWYER_1_REWARD":{"address":5602086,"default_item":108,"flag":1281},"TRAINER_SHANE_REWARD":{"address":5602512,"default_item":106,"flag":1494},"TRAINER_SHANNON_REWARD":{"address":5602278,"default_item":104,"flag":1377},"TRAINER_SHARON_REWARD":{"address":5602988,"default_item":106,"flag":1732},"TRAINER_SHAWN_REWARD":{"address":5602472,"default_item":106,"flag":1474},"TRAINER_SHAYLA_REWARD":{"address":5603578,"default_item":108,"flag":2027},"TRAINER_SHEILA_REWARD":{"address":5602334,"default_item":104,"flag":1405},"TRAINER_SHELBY_1_REWARD":{"address":5602710,"default_item":108,"flag":1593},"TRAINER_SHELLY_SEAFLOOR_CAVERN_REWARD":{"address":5602150,"default_item":104,"flag":1313},"TRAINER_SHELLY_WEATHER_INSTITUTE_REWARD":{"address":5602148,"default_item":104,"flag":1312},"TRAINER_SHIRLEY_REWARD":{"address":5602336,"default_item":104,"flag":1406},"TRAINER_SIDNEY_REWARD":{"address":5602606,"default_item":110,"flag":1541},"TRAINER_SIENNA_REWARD":{"address":5603002,"default_item":106,"flag":1739},"TRAINER_SIMON_REWARD":{"address":5602214,"default_item":103,"flag":1345},"TRAINER_SOPHIE_REWARD":{"address":5603500,"default_item":106,"flag":1988},"TRAINER_SPENCER_REWARD":{"address":5602402,"default_item":106,"flag":1439},"TRAINER_STAN_REWARD":{"address":5602408,"default_item":106,"flag":1442},"TRAINER_STEVEN_REWARD":{"address":5603692,"default_item":109,"flag":2084},"TRAINER_STEVE_1_REWARD":{"address":5602370,"default_item":104,"flag":1423},"TRAINER_SUSIE_REWARD":{"address":5602996,"default_item":106,"flag":1736},"TRAINER_SYLVIA_REWARD":{"address":5603234,"default_item":108,"flag":1855},"TRAINER_TABITHA_MAGMA_HIDEOUT_REWARD":{"address":5603548,"default_item":104,"flag":2012},"TRAINER_TABITHA_MT_CHIMNEY_REWARD":{"address":5603278,"default_item":108,"flag":1877},"TRAINER_TAKAO_REWARD":{"address":5602442,"default_item":106,"flag":1459},"TRAINER_TAKASHI_REWARD":{"address":5602916,"default_item":106,"flag":1696},"TRAINER_TALIA_REWARD":{"address":5602854,"default_item":104,"flag":1665},"TRAINER_TAMMY_REWARD":{"address":5602298,"default_item":106,"flag":1387},"TRAINER_TANYA_REWARD":{"address":5602986,"default_item":106,"flag":1731},"TRAINER_TARA_REWARD":{"address":5602976,"default_item":103,"flag":1726},"TRAINER_TASHA_REWARD":{"address":5602302,"default_item":108,"flag":1389},"TRAINER_TATE_AND_LIZA_1_REWARD":{"address":5602626,"default_item":109,"flag":1551},"TRAINER_TAYLOR_REWARD":{"address":5602534,"default_item":104,"flag":1505},"TRAINER_THALIA_1_REWARD":{"address":5602372,"default_item":104,"flag":1424},"TRAINER_THOMAS_REWARD":{"address":5602596,"default_item":107,"flag":1536},"TRAINER_TIANA_REWARD":{"address":5603290,"default_item":103,"flag":1883},"TRAINER_TIFFANY_REWARD":{"address":5602346,"default_item":107,"flag":1411},"TRAINER_TIMMY_REWARD":{"address":5602752,"default_item":103,"flag":1614},"TRAINER_TIMOTHY_1_REWARD":{"address":5602698,"default_item":104,"flag":1587},"TRAINER_TISHA_REWARD":{"address":5603436,"default_item":106,"flag":1956},"TRAINER_TOMMY_REWARD":{"address":5602726,"default_item":103,"flag":1601},"TRAINER_TONY_1_REWARD":{"address":5602394,"default_item":103,"flag":1435},"TRAINER_TORI_AND_TIA_REWARD":{"address":5603438,"default_item":103,"flag":1957},"TRAINER_TRAVIS_REWARD":{"address":5602520,"default_item":106,"flag":1498},"TRAINER_TRENT_1_REWARD":{"address":5603338,"default_item":106,"flag":1907},"TRAINER_TYRA_AND_IVY_REWARD":{"address":5603442,"default_item":106,"flag":1959},"TRAINER_TYRON_REWARD":{"address":5603492,"default_item":106,"flag":1984},"TRAINER_VALERIE_1_REWARD":{"address":5602300,"default_item":108,"flag":1388},"TRAINER_VANESSA_REWARD":{"address":5602684,"default_item":104,"flag":1580},"TRAINER_VICKY_REWARD":{"address":5602708,"default_item":108,"flag":1592},"TRAINER_VICTORIA_REWARD":{"address":5602682,"default_item":106,"flag":1579},"TRAINER_VICTOR_REWARD":{"address":5602668,"default_item":106,"flag":1572},"TRAINER_VIOLET_REWARD":{"address":5602162,"default_item":104,"flag":1319},"TRAINER_VIRGIL_REWARD":{"address":5602552,"default_item":108,"flag":1514},"TRAINER_VITO_REWARD":{"address":5602248,"default_item":104,"flag":1362},"TRAINER_VIVIAN_REWARD":{"address":5603382,"default_item":106,"flag":1929},"TRAINER_VIVI_REWARD":{"address":5603296,"default_item":106,"flag":1886},"TRAINER_WADE_REWARD":{"address":5602772,"default_item":106,"flag":1624},"TRAINER_WALLACE_REWARD":{"address":5602754,"default_item":110,"flag":1615},"TRAINER_WALLY_MAUVILLE_REWARD":{"address":5603396,"default_item":108,"flag":1936},"TRAINER_WALLY_VR_1_REWARD":{"address":5603122,"default_item":107,"flag":1799},"TRAINER_WALTER_1_REWARD":{"address":5602592,"default_item":104,"flag":1534},"TRAINER_WARREN_REWARD":{"address":5602260,"default_item":104,"flag":1368},"TRAINER_WATTSON_1_REWARD":{"address":5602618,"default_item":104,"flag":1547},"TRAINER_WAYNE_REWARD":{"address":5603430,"default_item":104,"flag":1953},"TRAINER_WENDY_REWARD":{"address":5602268,"default_item":104,"flag":1372},"TRAINER_WILLIAM_REWARD":{"address":5602556,"default_item":106,"flag":1516},"TRAINER_WILTON_1_REWARD":{"address":5602240,"default_item":108,"flag":1358},"TRAINER_WINONA_1_REWARD":{"address":5602624,"default_item":107,"flag":1550},"TRAINER_WINSTON_1_REWARD":{"address":5602356,"default_item":104,"flag":1416},"TRAINER_WYATT_REWARD":{"address":5603506,"default_item":104,"flag":1991},"TRAINER_YASU_REWARD":{"address":5602914,"default_item":106,"flag":1695},"TRAINER_ZANDER_REWARD":{"address":5602146,"default_item":108,"flag":1311}},"maps":{"MAP_ABANDONED_SHIP_CAPTAINS_OFFICE":{"header_address":4766420,"warp_table_address":5496844},"MAP_ABANDONED_SHIP_CORRIDORS_1F":{"header_address":4766196,"warp_table_address":5495920},"MAP_ABANDONED_SHIP_CORRIDORS_B1F":{"header_address":4766252,"warp_table_address":5496248},"MAP_ABANDONED_SHIP_DECK":{"header_address":4766168,"warp_table_address":5495812},"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS":{"fishing_encounters":{"address":5609088,"slots":[129,72,129,72,72,72,72,73,73,73]},"header_address":4766476,"warp_table_address":5496908,"water_encounters":{"address":5609060,"slots":[72,72,72,72,73]}},"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS":{"header_address":4766504,"warp_table_address":5497120},"MAP_ABANDONED_SHIP_ROOMS2_1F":{"header_address":4766392,"warp_table_address":5496752},"MAP_ABANDONED_SHIP_ROOMS2_B1F":{"header_address":4766308,"warp_table_address":5496484},"MAP_ABANDONED_SHIP_ROOMS_1F":{"header_address":4766224,"warp_table_address":5496132},"MAP_ABANDONED_SHIP_ROOMS_B1F":{"fishing_encounters":{"address":5606324,"slots":[129,72,129,72,72,72,72,73,73,73]},"header_address":4766280,"warp_table_address":5496392,"water_encounters":{"address":5606296,"slots":[72,72,72,72,73]}},"MAP_ABANDONED_SHIP_ROOM_B1F":{"header_address":4766364,"warp_table_address":5496596},"MAP_ABANDONED_SHIP_UNDERWATER1":{"header_address":4766336,"warp_table_address":5496536},"MAP_ABANDONED_SHIP_UNDERWATER2":{"header_address":4766448,"warp_table_address":5496880},"MAP_ALTERING_CAVE":{"header_address":4767624,"land_encounters":{"address":5613400,"slots":[41,41,41,41,41,41,41,41,41,41,41,41]},"warp_table_address":5500436},"MAP_ANCIENT_TOMB":{"header_address":4766560,"warp_table_address":5497460},"MAP_AQUA_HIDEOUT_1F":{"header_address":4765300,"warp_table_address":5490892},"MAP_AQUA_HIDEOUT_B1F":{"header_address":4765328,"warp_table_address":5491152},"MAP_AQUA_HIDEOUT_B2F":{"header_address":4765356,"warp_table_address":5491516},"MAP_AQUA_HIDEOUT_UNUSED_RUBY_MAP1":{"header_address":4766728,"warp_table_address":4160749568},"MAP_AQUA_HIDEOUT_UNUSED_RUBY_MAP2":{"header_address":4766756,"warp_table_address":4160749568},"MAP_AQUA_HIDEOUT_UNUSED_RUBY_MAP3":{"header_address":4766784,"warp_table_address":4160749568},"MAP_ARTISAN_CAVE_1F":{"header_address":4767456,"land_encounters":{"address":5613344,"slots":[235,235,235,235,235,235,235,235,235,235,235,235]},"warp_table_address":5500172},"MAP_ARTISAN_CAVE_B1F":{"header_address":4767428,"land_encounters":{"address":5613288,"slots":[235,235,235,235,235,235,235,235,235,235,235,235]},"warp_table_address":5500064},"MAP_BATTLE_COLOSSEUM_2P":{"header_address":4768352,"warp_table_address":5509852},"MAP_BATTLE_COLOSSEUM_4P":{"header_address":4768436,"warp_table_address":5510152},"MAP_BATTLE_FRONTIER_BATTLE_ARENA_BATTLE_ROOM":{"header_address":4770228,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_ARENA_CORRIDOR":{"header_address":4770200,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY":{"header_address":4770172,"warp_table_address":5520908},"MAP_BATTLE_FRONTIER_BATTLE_DOME_BATTLE_ROOM":{"header_address":4769976,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR":{"header_address":4769920,"warp_table_address":5519076},"MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY":{"header_address":4769892,"warp_table_address":5518968},"MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM":{"header_address":4769948,"warp_table_address":5519136},"MAP_BATTLE_FRONTIER_BATTLE_FACTORY_BATTLE_ROOM":{"header_address":4770312,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY":{"header_address":4770256,"warp_table_address":5521384},"MAP_BATTLE_FRONTIER_BATTLE_FACTORY_PRE_BATTLE_ROOM":{"header_address":4770284,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM":{"header_address":4770060,"warp_table_address":5520116},"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR":{"header_address":4770032,"warp_table_address":5519944},"MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY":{"header_address":4770004,"warp_table_address":5519696},"MAP_BATTLE_FRONTIER_BATTLE_PIKE_CORRIDOR":{"header_address":4770368,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY":{"header_address":4770340,"warp_table_address":5521808},"MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_FINAL":{"header_address":4770452,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_NORMAL":{"header_address":4770424,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_WILD_MONS":{"header_address":4770480,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_PIKE_THREE_PATH_ROOM":{"header_address":4770396,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_FLOOR":{"header_address":4770116,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY":{"header_address":4770088,"warp_table_address":5520248},"MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_TOP":{"header_address":4770144,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM":{"header_address":4769612,"warp_table_address":5516696},"MAP_BATTLE_FRONTIER_BATTLE_TOWER_CORRIDOR":{"header_address":4769584,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_TOWER_ELEVATOR":{"header_address":4769556,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY":{"header_address":4769528,"warp_table_address":5516432},"MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_BATTLE_ROOM":{"header_address":4769864,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_CORRIDOR":{"header_address":4769836,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_PARTNER_ROOM":{"header_address":4769808,"warp_table_address":4160749568},"MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER":{"header_address":4770564,"warp_table_address":5523056},"MAP_BATTLE_FRONTIER_LOUNGE1":{"header_address":4770536,"warp_table_address":5522812},"MAP_BATTLE_FRONTIER_LOUNGE2":{"header_address":4770592,"warp_table_address":5523220},"MAP_BATTLE_FRONTIER_LOUNGE3":{"header_address":4770620,"warp_table_address":5523376},"MAP_BATTLE_FRONTIER_LOUNGE4":{"header_address":4770648,"warp_table_address":5523476},"MAP_BATTLE_FRONTIER_LOUNGE5":{"header_address":4770704,"warp_table_address":5523660},"MAP_BATTLE_FRONTIER_LOUNGE6":{"header_address":4770732,"warp_table_address":5523720},"MAP_BATTLE_FRONTIER_LOUNGE7":{"header_address":4770760,"warp_table_address":5523844},"MAP_BATTLE_FRONTIER_LOUNGE8":{"header_address":4770816,"warp_table_address":5524100},"MAP_BATTLE_FRONTIER_LOUNGE9":{"header_address":4770844,"warp_table_address":5524152},"MAP_BATTLE_FRONTIER_MART":{"header_address":4770928,"warp_table_address":5524588},"MAP_BATTLE_FRONTIER_OUTSIDE_EAST":{"header_address":4769780,"warp_table_address":5518080},"MAP_BATTLE_FRONTIER_OUTSIDE_WEST":{"header_address":4769500,"warp_table_address":5516048},"MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F":{"header_address":4770872,"warp_table_address":5524308},"MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F":{"header_address":4770900,"warp_table_address":5524448},"MAP_BATTLE_FRONTIER_RANKING_HALL":{"header_address":4770508,"warp_table_address":5522560},"MAP_BATTLE_FRONTIER_RECEPTION_GATE":{"header_address":4770788,"warp_table_address":5523992},"MAP_BATTLE_FRONTIER_SCOTTS_HOUSE":{"header_address":4770676,"warp_table_address":5523528},"MAP_BATTLE_PYRAMID_SQUARE01":{"header_address":4768912,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE02":{"header_address":4768940,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE03":{"header_address":4768968,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE04":{"header_address":4768996,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE05":{"header_address":4769024,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE06":{"header_address":4769052,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE07":{"header_address":4769080,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE08":{"header_address":4769108,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE09":{"header_address":4769136,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE10":{"header_address":4769164,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE11":{"header_address":4769192,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE12":{"header_address":4769220,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE13":{"header_address":4769248,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE14":{"header_address":4769276,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE15":{"header_address":4769304,"warp_table_address":4160749568},"MAP_BATTLE_PYRAMID_SQUARE16":{"header_address":4769332,"warp_table_address":4160749568},"MAP_BIRTH_ISLAND_EXTERIOR":{"header_address":4771012,"warp_table_address":5524876},"MAP_BIRTH_ISLAND_HARBOR":{"header_address":4771040,"warp_table_address":5524952},"MAP_CAVE_OF_ORIGIN_1F":{"header_address":4765720,"land_encounters":{"address":5609868,"slots":[41,41,41,322,322,322,41,41,42,42,42,42]},"warp_table_address":5493440},"MAP_CAVE_OF_ORIGIN_B1F":{"header_address":4765832,"warp_table_address":5493608},"MAP_CAVE_OF_ORIGIN_ENTRANCE":{"header_address":4765692,"land_encounters":{"address":5609812,"slots":[41,41,41,41,41,41,41,41,42,42,42,42]},"warp_table_address":5493404},"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1":{"header_address":4765748,"land_encounters":{"address":5609924,"slots":[41,41,41,322,322,322,41,41,42,42,42,42]},"warp_table_address":5493476},"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2":{"header_address":4765776,"land_encounters":{"address":5609980,"slots":[41,41,41,322,322,322,41,41,42,42,42,42]},"warp_table_address":5493512},"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3":{"header_address":4765804,"land_encounters":{"address":5610036,"slots":[41,41,41,322,322,322,41,41,42,42,42,42]},"warp_table_address":5493548},"MAP_CONTEST_HALL":{"header_address":4768464,"warp_table_address":4160749568},"MAP_CONTEST_HALL_BEAUTY":{"header_address":4768660,"warp_table_address":4160749568},"MAP_CONTEST_HALL_COOL":{"header_address":4768716,"warp_table_address":4160749568},"MAP_CONTEST_HALL_CUTE":{"header_address":4768772,"warp_table_address":4160749568},"MAP_CONTEST_HALL_SMART":{"header_address":4768744,"warp_table_address":4160749568},"MAP_CONTEST_HALL_TOUGH":{"header_address":4768688,"warp_table_address":4160749568},"MAP_DESERT_RUINS":{"header_address":4764824,"warp_table_address":5486828},"MAP_DESERT_UNDERPASS":{"header_address":4767400,"land_encounters":{"address":5613232,"slots":[132,370,132,371,132,370,371,132,370,132,371,132]},"warp_table_address":5500012},"MAP_DEWFORD_TOWN":{"fishing_encounters":{"address":5611588,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4758300,"warp_table_address":5435180,"water_encounters":{"address":5611560,"slots":[72,309,309,310,310]}},"MAP_DEWFORD_TOWN_GYM":{"header_address":4759952,"warp_table_address":5460340},"MAP_DEWFORD_TOWN_HALL":{"header_address":4759980,"warp_table_address":5460640},"MAP_DEWFORD_TOWN_HOUSE1":{"header_address":4759868,"warp_table_address":5459856},"MAP_DEWFORD_TOWN_HOUSE2":{"header_address":4760008,"warp_table_address":5460748},"MAP_DEWFORD_TOWN_POKEMON_CENTER_1F":{"header_address":4759896,"warp_table_address":5459964},"MAP_DEWFORD_TOWN_POKEMON_CENTER_2F":{"header_address":4759924,"warp_table_address":5460104},"MAP_EVER_GRANDE_CITY":{"fishing_encounters":{"address":5611892,"slots":[129,72,129,325,313,325,313,222,313,313]},"header_address":4758216,"warp_table_address":5434048,"water_encounters":{"address":5611864,"slots":[72,309,309,310,310]}},"MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM":{"header_address":4764012,"warp_table_address":5483720},"MAP_EVER_GRANDE_CITY_DRAKES_ROOM":{"header_address":4763984,"warp_table_address":5483612},"MAP_EVER_GRANDE_CITY_GLACIAS_ROOM":{"header_address":4763956,"warp_table_address":5483552},"MAP_EVER_GRANDE_CITY_HALL1":{"header_address":4764040,"warp_table_address":5483756},"MAP_EVER_GRANDE_CITY_HALL2":{"header_address":4764068,"warp_table_address":5483808},"MAP_EVER_GRANDE_CITY_HALL3":{"header_address":4764096,"warp_table_address":5483860},"MAP_EVER_GRANDE_CITY_HALL4":{"header_address":4764124,"warp_table_address":5483912},"MAP_EVER_GRANDE_CITY_HALL5":{"header_address":4764152,"warp_table_address":5483948},"MAP_EVER_GRANDE_CITY_HALL_OF_FAME":{"header_address":4764208,"warp_table_address":5484180},"MAP_EVER_GRANDE_CITY_PHOEBES_ROOM":{"header_address":4763928,"warp_table_address":5483492},"MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F":{"header_address":4764236,"warp_table_address":5484304},"MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F":{"header_address":4764264,"warp_table_address":5484444},"MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F":{"header_address":4764180,"warp_table_address":5484096},"MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F":{"header_address":4764292,"warp_table_address":5484584},"MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM":{"header_address":4763900,"warp_table_address":5483432},"MAP_FALLARBOR_TOWN":{"header_address":4758356,"warp_table_address":5435792},"MAP_FALLARBOR_TOWN_BATTLE_TENT_BATTLE_ROOM":{"header_address":4760316,"warp_table_address":4160749568},"MAP_FALLARBOR_TOWN_BATTLE_TENT_CORRIDOR":{"header_address":4760288,"warp_table_address":4160749568},"MAP_FALLARBOR_TOWN_BATTLE_TENT_LOBBY":{"header_address":4760260,"warp_table_address":5462376},"MAP_FALLARBOR_TOWN_COZMOS_HOUSE":{"header_address":4760400,"warp_table_address":5462888},"MAP_FALLARBOR_TOWN_MART":{"header_address":4760232,"warp_table_address":5462220},"MAP_FALLARBOR_TOWN_MOVE_RELEARNERS_HOUSE":{"header_address":4760428,"warp_table_address":5462948},"MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F":{"header_address":4760344,"warp_table_address":5462656},"MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F":{"header_address":4760372,"warp_table_address":5462796},"MAP_FARAWAY_ISLAND_ENTRANCE":{"header_address":4770956,"warp_table_address":5524672},"MAP_FARAWAY_ISLAND_INTERIOR":{"header_address":4770984,"warp_table_address":5524792},"MAP_FIERY_PATH":{"header_address":4765048,"land_encounters":{"address":5606456,"slots":[339,109,339,66,321,218,109,66,321,321,88,88]},"warp_table_address":5489344},"MAP_FORTREE_CITY":{"header_address":4758104,"warp_table_address":5431676},"MAP_FORTREE_CITY_DECORATION_SHOP":{"header_address":4762444,"warp_table_address":5473936},"MAP_FORTREE_CITY_GYM":{"header_address":4762220,"warp_table_address":5472984},"MAP_FORTREE_CITY_HOUSE1":{"header_address":4762192,"warp_table_address":5472756},"MAP_FORTREE_CITY_HOUSE2":{"header_address":4762332,"warp_table_address":5473504},"MAP_FORTREE_CITY_HOUSE3":{"header_address":4762360,"warp_table_address":5473588},"MAP_FORTREE_CITY_HOUSE4":{"header_address":4762388,"warp_table_address":5473696},"MAP_FORTREE_CITY_HOUSE5":{"header_address":4762416,"warp_table_address":5473804},"MAP_FORTREE_CITY_MART":{"header_address":4762304,"warp_table_address":5473420},"MAP_FORTREE_CITY_POKEMON_CENTER_1F":{"header_address":4762248,"warp_table_address":5473140},"MAP_FORTREE_CITY_POKEMON_CENTER_2F":{"header_address":4762276,"warp_table_address":5473280},"MAP_GRANITE_CAVE_1F":{"header_address":4764852,"land_encounters":{"address":5605988,"slots":[41,335,335,41,335,63,335,335,74,74,74,74]},"warp_table_address":5486956},"MAP_GRANITE_CAVE_B1F":{"header_address":4764880,"land_encounters":{"address":5606044,"slots":[41,382,382,382,41,63,335,335,322,322,322,322]},"warp_table_address":5487032},"MAP_GRANITE_CAVE_B2F":{"header_address":4764908,"land_encounters":{"address":5606372,"slots":[41,382,382,41,382,63,322,322,322,322,322,322]},"warp_table_address":5487324},"MAP_GRANITE_CAVE_STEVENS_ROOM":{"header_address":4764936,"land_encounters":{"address":5608188,"slots":[41,335,335,41,335,63,335,335,382,382,382,382]},"warp_table_address":5487432},"MAP_INSIDE_OF_TRUCK":{"header_address":4768800,"warp_table_address":5510720},"MAP_ISLAND_CAVE":{"header_address":4766532,"warp_table_address":5497356},"MAP_JAGGED_PASS":{"header_address":4765020,"land_encounters":{"address":5606644,"slots":[339,339,66,339,351,66,351,66,339,351,339,351]},"warp_table_address":5488908},"MAP_LAVARIDGE_TOWN":{"header_address":4758328,"warp_table_address":5435516},"MAP_LAVARIDGE_TOWN_GYM_1F":{"header_address":4760064,"warp_table_address":5461036},"MAP_LAVARIDGE_TOWN_GYM_B1F":{"header_address":4760092,"warp_table_address":5461384},"MAP_LAVARIDGE_TOWN_HERB_SHOP":{"header_address":4760036,"warp_table_address":5460856},"MAP_LAVARIDGE_TOWN_HOUSE":{"header_address":4760120,"warp_table_address":5461668},"MAP_LAVARIDGE_TOWN_MART":{"header_address":4760148,"warp_table_address":5461776},"MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F":{"header_address":4760176,"warp_table_address":5461908},"MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F":{"header_address":4760204,"warp_table_address":5462056},"MAP_LILYCOVE_CITY":{"fishing_encounters":{"address":5611512,"slots":[129,72,129,72,313,313,313,120,313,313]},"header_address":4758132,"warp_table_address":5432368,"water_encounters":{"address":5611484,"slots":[72,309,309,310,310]}},"MAP_LILYCOVE_CITY_CONTEST_HALL":{"header_address":4762612,"warp_table_address":5476560},"MAP_LILYCOVE_CITY_CONTEST_LOBBY":{"header_address":4762584,"warp_table_address":5475596},"MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F":{"header_address":4762472,"warp_table_address":5473996},"MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_2F":{"header_address":4762500,"warp_table_address":5474224},"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F":{"header_address":4762920,"warp_table_address":5478044},"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F":{"header_address":4762948,"warp_table_address":5478228},"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F":{"header_address":4762976,"warp_table_address":5478392},"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F":{"header_address":4763004,"warp_table_address":5478556},"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F":{"header_address":4763032,"warp_table_address":5478768},"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR":{"header_address":4763088,"warp_table_address":5478984},"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ROOFTOP":{"header_address":4763060,"warp_table_address":5478908},"MAP_LILYCOVE_CITY_HARBOR":{"header_address":4762752,"warp_table_address":5477396},"MAP_LILYCOVE_CITY_HOUSE1":{"header_address":4762808,"warp_table_address":5477540},"MAP_LILYCOVE_CITY_HOUSE2":{"header_address":4762836,"warp_table_address":5477600},"MAP_LILYCOVE_CITY_HOUSE3":{"header_address":4762864,"warp_table_address":5477780},"MAP_LILYCOVE_CITY_HOUSE4":{"header_address":4762892,"warp_table_address":5477864},"MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F":{"header_address":4762528,"warp_table_address":5474492},"MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_2F":{"header_address":4762556,"warp_table_address":5474824},"MAP_LILYCOVE_CITY_MOVE_DELETERS_HOUSE":{"header_address":4762780,"warp_table_address":5477456},"MAP_LILYCOVE_CITY_POKEMON_CENTER_1F":{"header_address":4762640,"warp_table_address":5476804},"MAP_LILYCOVE_CITY_POKEMON_CENTER_2F":{"header_address":4762668,"warp_table_address":5476944},"MAP_LILYCOVE_CITY_POKEMON_TRAINER_FAN_CLUB":{"header_address":4762724,"warp_table_address":5477240},"MAP_LILYCOVE_CITY_UNUSED_MART":{"header_address":4762696,"warp_table_address":5476988},"MAP_LITTLEROOT_TOWN":{"header_address":4758244,"warp_table_address":5434528},"MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F":{"header_address":4759588,"warp_table_address":5457588},"MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F":{"header_address":4759616,"warp_table_address":5458080},"MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F":{"header_address":4759644,"warp_table_address":5458324},"MAP_LITTLEROOT_TOWN_MAYS_HOUSE_2F":{"header_address":4759672,"warp_table_address":5458816},"MAP_LITTLEROOT_TOWN_PROFESSOR_BIRCHS_LAB":{"header_address":4759700,"warp_table_address":5459036},"MAP_MAGMA_HIDEOUT_1F":{"header_address":4767064,"land_encounters":{"address":5612560,"slots":[74,321,74,321,74,74,74,75,75,75,75,75]},"warp_table_address":5498844},"MAP_MAGMA_HIDEOUT_2F_1R":{"header_address":4767092,"land_encounters":{"address":5612616,"slots":[74,321,74,321,74,74,74,75,75,75,75,75]},"warp_table_address":5498992},"MAP_MAGMA_HIDEOUT_2F_2R":{"header_address":4767120,"land_encounters":{"address":5612672,"slots":[74,321,74,321,74,74,74,75,75,75,75,75]},"warp_table_address":5499180},"MAP_MAGMA_HIDEOUT_2F_3R":{"header_address":4767260,"land_encounters":{"address":5612952,"slots":[74,321,74,321,74,74,74,75,75,75,75,75]},"warp_table_address":5499696},"MAP_MAGMA_HIDEOUT_3F_1R":{"header_address":4767148,"land_encounters":{"address":5612728,"slots":[74,321,74,321,74,74,74,75,75,75,75,75]},"warp_table_address":5499288},"MAP_MAGMA_HIDEOUT_3F_2R":{"header_address":4767176,"land_encounters":{"address":5612784,"slots":[74,321,74,321,74,74,74,75,75,75,75,75]},"warp_table_address":5499380},"MAP_MAGMA_HIDEOUT_3F_3R":{"header_address":4767232,"land_encounters":{"address":5612896,"slots":[74,321,74,321,74,74,74,75,75,75,75,75]},"warp_table_address":5499660},"MAP_MAGMA_HIDEOUT_4F":{"header_address":4767204,"land_encounters":{"address":5612840,"slots":[74,321,74,321,74,74,74,75,75,75,75,75]},"warp_table_address":5499600},"MAP_MARINE_CAVE_END":{"header_address":4767540,"warp_table_address":5500288},"MAP_MARINE_CAVE_ENTRANCE":{"header_address":4767512,"warp_table_address":5500236},"MAP_MAUVILLE_CITY":{"header_address":4758048,"warp_table_address":5430380},"MAP_MAUVILLE_CITY_BIKE_SHOP":{"header_address":4761520,"warp_table_address":5469232},"MAP_MAUVILLE_CITY_GAME_CORNER":{"header_address":4761576,"warp_table_address":5469640},"MAP_MAUVILLE_CITY_GYM":{"header_address":4761492,"warp_table_address":5469060},"MAP_MAUVILLE_CITY_HOUSE1":{"header_address":4761548,"warp_table_address":5469316},"MAP_MAUVILLE_CITY_HOUSE2":{"header_address":4761604,"warp_table_address":5469988},"MAP_MAUVILLE_CITY_MART":{"header_address":4761688,"warp_table_address":5470424},"MAP_MAUVILLE_CITY_POKEMON_CENTER_1F":{"header_address":4761632,"warp_table_address":5470144},"MAP_MAUVILLE_CITY_POKEMON_CENTER_2F":{"header_address":4761660,"warp_table_address":5470308},"MAP_METEOR_FALLS_1F_1R":{"fishing_encounters":{"address":5610796,"slots":[129,118,129,118,323,323,323,323,323,323]},"header_address":4764656,"land_encounters":{"address":5610712,"slots":[41,41,41,41,41,349,349,349,41,41,41,41]},"warp_table_address":5486052,"water_encounters":{"address":5610768,"slots":[41,41,349,349,349]}},"MAP_METEOR_FALLS_1F_2R":{"fishing_encounters":{"address":5610928,"slots":[129,118,129,118,323,323,323,324,324,324]},"header_address":4764684,"land_encounters":{"address":5610844,"slots":[42,42,42,349,349,349,42,349,42,42,42,42]},"warp_table_address":5486220,"water_encounters":{"address":5610900,"slots":[42,42,349,349,349]}},"MAP_METEOR_FALLS_B1F_1R":{"fishing_encounters":{"address":5611060,"slots":[129,118,129,118,323,323,323,324,324,324]},"header_address":4764712,"land_encounters":{"address":5610976,"slots":[42,42,42,349,349,349,42,349,42,42,42,42]},"warp_table_address":5486284,"water_encounters":{"address":5611032,"slots":[42,42,349,349,349]}},"MAP_METEOR_FALLS_B1F_2R":{"fishing_encounters":{"address":5606596,"slots":[129,118,129,118,323,323,323,324,324,324]},"header_address":4764740,"land_encounters":{"address":5606512,"slots":[42,42,395,349,395,349,395,349,42,42,42,42]},"warp_table_address":5486376,"water_encounters":{"address":5606568,"slots":[42,42,349,349,349]}},"MAP_METEOR_FALLS_STEVENS_CAVE":{"header_address":4767652,"land_encounters":{"address":5613904,"slots":[42,42,42,349,349,349,42,349,42,42,42,42]},"warp_table_address":5500488},"MAP_MIRAGE_TOWER_1F":{"header_address":4767288,"land_encounters":{"address":5613008,"slots":[27,332,27,332,27,332,27,332,27,332,27,332]},"warp_table_address":5499732},"MAP_MIRAGE_TOWER_2F":{"header_address":4767316,"land_encounters":{"address":5613064,"slots":[27,332,27,332,27,332,27,332,27,332,27,332]},"warp_table_address":5499768},"MAP_MIRAGE_TOWER_3F":{"header_address":4767344,"land_encounters":{"address":5613120,"slots":[27,332,27,332,27,332,27,332,27,332,27,332]},"warp_table_address":5499852},"MAP_MIRAGE_TOWER_4F":{"header_address":4767372,"land_encounters":{"address":5613176,"slots":[27,332,27,332,27,332,27,332,27,332,27,332]},"warp_table_address":5499960},"MAP_MOSSDEEP_CITY":{"fishing_encounters":{"address":5611740,"slots":[129,72,129,72,313,331,313,313,313,313]},"header_address":4758160,"warp_table_address":5433064,"water_encounters":{"address":5611712,"slots":[72,309,309,310,310]}},"MAP_MOSSDEEP_CITY_GAME_CORNER_1F":{"header_address":4763424,"warp_table_address":5481712},"MAP_MOSSDEEP_CITY_GAME_CORNER_B1F":{"header_address":4763452,"warp_table_address":5481816},"MAP_MOSSDEEP_CITY_GYM":{"header_address":4763116,"warp_table_address":5479884},"MAP_MOSSDEEP_CITY_HOUSE1":{"header_address":4763144,"warp_table_address":5480232},"MAP_MOSSDEEP_CITY_HOUSE2":{"header_address":4763172,"warp_table_address":5480340},"MAP_MOSSDEEP_CITY_HOUSE3":{"header_address":4763284,"warp_table_address":5480812},"MAP_MOSSDEEP_CITY_HOUSE4":{"header_address":4763340,"warp_table_address":5481076},"MAP_MOSSDEEP_CITY_MART":{"header_address":4763256,"warp_table_address":5480752},"MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F":{"header_address":4763200,"warp_table_address":5480448},"MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F":{"header_address":4763228,"warp_table_address":5480612},"MAP_MOSSDEEP_CITY_SPACE_CENTER_1F":{"header_address":4763368,"warp_table_address":5481376},"MAP_MOSSDEEP_CITY_SPACE_CENTER_2F":{"header_address":4763396,"warp_table_address":5481636},"MAP_MOSSDEEP_CITY_STEVENS_HOUSE":{"header_address":4763312,"warp_table_address":5480920},"MAP_MT_CHIMNEY":{"header_address":4764992,"warp_table_address":5488664},"MAP_MT_CHIMNEY_CABLE_CAR_STATION":{"header_address":4764460,"warp_table_address":5485144},"MAP_MT_PYRE_1F":{"header_address":4765076,"land_encounters":{"address":5606100,"slots":[377,377,377,377,377,377,377,377,377,377,377,377]},"warp_table_address":5489452},"MAP_MT_PYRE_2F":{"header_address":4765104,"land_encounters":{"address":5607796,"slots":[377,377,377,377,377,377,377,377,377,377,377,377]},"warp_table_address":5489712},"MAP_MT_PYRE_3F":{"header_address":4765132,"land_encounters":{"address":5607852,"slots":[377,377,377,377,377,377,377,377,377,377,377,377]},"warp_table_address":5489868},"MAP_MT_PYRE_4F":{"header_address":4765160,"land_encounters":{"address":5607908,"slots":[377,377,377,377,377,377,377,377,361,361,361,361]},"warp_table_address":5489984},"MAP_MT_PYRE_5F":{"header_address":4765188,"land_encounters":{"address":5607964,"slots":[377,377,377,377,377,377,377,377,361,361,361,361]},"warp_table_address":5490100},"MAP_MT_PYRE_6F":{"header_address":4765216,"land_encounters":{"address":5608020,"slots":[377,377,377,377,377,377,377,377,361,361,361,361]},"warp_table_address":5490232},"MAP_MT_PYRE_EXTERIOR":{"header_address":4765244,"land_encounters":{"address":5608076,"slots":[377,377,377,377,37,37,37,37,309,309,309,309]},"warp_table_address":5490316},"MAP_MT_PYRE_SUMMIT":{"header_address":4765272,"land_encounters":{"address":5608132,"slots":[377,377,377,377,377,377,377,361,361,361,411,411]},"warp_table_address":5490656},"MAP_NAVEL_ROCK_B1F":{"header_address":4771320,"warp_table_address":5525524},"MAP_NAVEL_ROCK_BOTTOM":{"header_address":4771824,"warp_table_address":5526248},"MAP_NAVEL_ROCK_DOWN01":{"header_address":4771516,"warp_table_address":5525828},"MAP_NAVEL_ROCK_DOWN02":{"header_address":4771544,"warp_table_address":5525864},"MAP_NAVEL_ROCK_DOWN03":{"header_address":4771572,"warp_table_address":5525900},"MAP_NAVEL_ROCK_DOWN04":{"header_address":4771600,"warp_table_address":5525936},"MAP_NAVEL_ROCK_DOWN05":{"header_address":4771628,"warp_table_address":5525972},"MAP_NAVEL_ROCK_DOWN06":{"header_address":4771656,"warp_table_address":5526008},"MAP_NAVEL_ROCK_DOWN07":{"header_address":4771684,"warp_table_address":5526044},"MAP_NAVEL_ROCK_DOWN08":{"header_address":4771712,"warp_table_address":5526080},"MAP_NAVEL_ROCK_DOWN09":{"header_address":4771740,"warp_table_address":5526116},"MAP_NAVEL_ROCK_DOWN10":{"header_address":4771768,"warp_table_address":5526152},"MAP_NAVEL_ROCK_DOWN11":{"header_address":4771796,"warp_table_address":5526188},"MAP_NAVEL_ROCK_ENTRANCE":{"header_address":4771292,"warp_table_address":5525488},"MAP_NAVEL_ROCK_EXTERIOR":{"header_address":4771236,"warp_table_address":5525376},"MAP_NAVEL_ROCK_FORK":{"header_address":4771348,"warp_table_address":5525560},"MAP_NAVEL_ROCK_HARBOR":{"header_address":4771264,"warp_table_address":5525460},"MAP_NAVEL_ROCK_TOP":{"header_address":4771488,"warp_table_address":5525772},"MAP_NAVEL_ROCK_UP1":{"header_address":4771376,"warp_table_address":5525604},"MAP_NAVEL_ROCK_UP2":{"header_address":4771404,"warp_table_address":5525640},"MAP_NAVEL_ROCK_UP3":{"header_address":4771432,"warp_table_address":5525676},"MAP_NAVEL_ROCK_UP4":{"header_address":4771460,"warp_table_address":5525712},"MAP_NEW_MAUVILLE_ENTRANCE":{"header_address":4766112,"land_encounters":{"address":5610092,"slots":[100,81,100,81,100,81,100,81,100,81,100,81]},"warp_table_address":5495284},"MAP_NEW_MAUVILLE_INSIDE":{"header_address":4766140,"land_encounters":{"address":5607136,"slots":[100,81,100,81,100,81,100,81,100,81,101,82]},"warp_table_address":5495528},"MAP_OLDALE_TOWN":{"header_address":4758272,"warp_table_address":5434860},"MAP_OLDALE_TOWN_HOUSE1":{"header_address":4759728,"warp_table_address":5459276},"MAP_OLDALE_TOWN_HOUSE2":{"header_address":4759756,"warp_table_address":5459360},"MAP_OLDALE_TOWN_MART":{"header_address":4759840,"warp_table_address":5459748},"MAP_OLDALE_TOWN_POKEMON_CENTER_1F":{"header_address":4759784,"warp_table_address":5459492},"MAP_OLDALE_TOWN_POKEMON_CENTER_2F":{"header_address":4759812,"warp_table_address":5459632},"MAP_PACIFIDLOG_TOWN":{"fishing_encounters":{"address":5611816,"slots":[129,72,129,72,313,331,313,313,313,313]},"header_address":4758412,"warp_table_address":5436288,"water_encounters":{"address":5611788,"slots":[72,309,309,310,310]}},"MAP_PACIFIDLOG_TOWN_HOUSE1":{"header_address":4760764,"warp_table_address":5464400},"MAP_PACIFIDLOG_TOWN_HOUSE2":{"header_address":4760792,"warp_table_address":5464508},"MAP_PACIFIDLOG_TOWN_HOUSE3":{"header_address":4760820,"warp_table_address":5464592},"MAP_PACIFIDLOG_TOWN_HOUSE4":{"header_address":4760848,"warp_table_address":5464700},"MAP_PACIFIDLOG_TOWN_HOUSE5":{"header_address":4760876,"warp_table_address":5464784},"MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F":{"header_address":4760708,"warp_table_address":5464168},"MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F":{"header_address":4760736,"warp_table_address":5464308},"MAP_PETALBURG_CITY":{"fishing_encounters":{"address":5611968,"slots":[129,118,129,118,326,326,326,326,326,326]},"header_address":4757992,"warp_table_address":5428704,"water_encounters":{"address":5611940,"slots":[183,183,183,183,183]}},"MAP_PETALBURG_CITY_GYM":{"header_address":4760932,"warp_table_address":5465168},"MAP_PETALBURG_CITY_HOUSE1":{"header_address":4760960,"warp_table_address":5465708},"MAP_PETALBURG_CITY_HOUSE2":{"header_address":4760988,"warp_table_address":5465792},"MAP_PETALBURG_CITY_MART":{"header_address":4761072,"warp_table_address":5466228},"MAP_PETALBURG_CITY_POKEMON_CENTER_1F":{"header_address":4761016,"warp_table_address":5465948},"MAP_PETALBURG_CITY_POKEMON_CENTER_2F":{"header_address":4761044,"warp_table_address":5466088},"MAP_PETALBURG_CITY_WALLYS_HOUSE":{"header_address":4760904,"warp_table_address":5464868},"MAP_PETALBURG_WOODS":{"header_address":4764964,"land_encounters":{"address":5605876,"slots":[286,290,306,286,291,293,290,306,304,364,304,364]},"warp_table_address":5487772},"MAP_RECORD_CORNER":{"header_address":4768408,"warp_table_address":5510036},"MAP_ROUTE101":{"header_address":4758440,"land_encounters":{"address":5604388,"slots":[290,286,290,290,286,286,290,286,288,288,288,288]},"warp_table_address":4160749568},"MAP_ROUTE102":{"fishing_encounters":{"address":5604528,"slots":[129,118,129,118,326,326,326,326,326,326]},"header_address":4758468,"land_encounters":{"address":5604444,"slots":[286,290,286,290,295,295,288,288,288,392,288,298]},"warp_table_address":4160749568,"water_encounters":{"address":5604500,"slots":[183,183,183,183,118]}},"MAP_ROUTE103":{"fishing_encounters":{"address":5604660,"slots":[129,72,129,72,313,331,313,313,313,313]},"header_address":4758496,"land_encounters":{"address":5604576,"slots":[286,286,286,286,309,288,288,288,309,309,309,309]},"warp_table_address":5437452,"water_encounters":{"address":5604632,"slots":[72,309,309,310,310]}},"MAP_ROUTE104":{"fishing_encounters":{"address":5604792,"slots":[129,129,129,129,129,129,129,129,129,129]},"header_address":4758524,"land_encounters":{"address":5604708,"slots":[286,290,286,183,183,286,304,304,309,309,309,309]},"warp_table_address":5438308,"water_encounters":{"address":5604764,"slots":[309,309,309,310,310]}},"MAP_ROUTE104_MR_BRINEYS_HOUSE":{"header_address":4764320,"warp_table_address":5484676},"MAP_ROUTE104_PRETTY_PETAL_FLOWER_SHOP":{"header_address":4764348,"warp_table_address":5484784},"MAP_ROUTE104_PROTOTYPE":{"header_address":4771880,"warp_table_address":4160749568},"MAP_ROUTE104_PROTOTYPE_PRETTY_PETAL_FLOWER_SHOP":{"header_address":4771908,"warp_table_address":4160749568},"MAP_ROUTE105":{"fishing_encounters":{"address":5604868,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4758552,"warp_table_address":5438720,"water_encounters":{"address":5604840,"slots":[72,309,309,310,310]}},"MAP_ROUTE106":{"fishing_encounters":{"address":5606728,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4758580,"warp_table_address":5438892,"water_encounters":{"address":5606700,"slots":[72,309,309,310,310]}},"MAP_ROUTE107":{"fishing_encounters":{"address":5606804,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4758608,"warp_table_address":4160749568,"water_encounters":{"address":5606776,"slots":[72,309,309,310,310]}},"MAP_ROUTE108":{"fishing_encounters":{"address":5606880,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4758636,"warp_table_address":5439324,"water_encounters":{"address":5606852,"slots":[72,309,309,310,310]}},"MAP_ROUTE109":{"fishing_encounters":{"address":5606956,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4758664,"warp_table_address":5439940,"water_encounters":{"address":5606928,"slots":[72,309,309,310,310]}},"MAP_ROUTE109_SEASHORE_HOUSE":{"header_address":4771936,"warp_table_address":5526472},"MAP_ROUTE110":{"fishing_encounters":{"address":5605000,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4758692,"land_encounters":{"address":5604916,"slots":[286,337,367,337,354,43,354,367,309,309,353,353]},"warp_table_address":5440928,"water_encounters":{"address":5604972,"slots":[72,309,309,310,310]}},"MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE":{"header_address":4772272,"warp_table_address":5529400},"MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE":{"header_address":4772300,"warp_table_address":5529508},"MAP_ROUTE110_TRICK_HOUSE_CORRIDOR":{"header_address":4772020,"warp_table_address":5526740},"MAP_ROUTE110_TRICK_HOUSE_END":{"header_address":4771992,"warp_table_address":5526676},"MAP_ROUTE110_TRICK_HOUSE_ENTRANCE":{"header_address":4771964,"warp_table_address":5526532},"MAP_ROUTE110_TRICK_HOUSE_PUZZLE1":{"header_address":4772048,"warp_table_address":5527152},"MAP_ROUTE110_TRICK_HOUSE_PUZZLE2":{"header_address":4772076,"warp_table_address":5527328},"MAP_ROUTE110_TRICK_HOUSE_PUZZLE3":{"header_address":4772104,"warp_table_address":5527616},"MAP_ROUTE110_TRICK_HOUSE_PUZZLE4":{"header_address":4772132,"warp_table_address":5528072},"MAP_ROUTE110_TRICK_HOUSE_PUZZLE5":{"header_address":4772160,"warp_table_address":5528248},"MAP_ROUTE110_TRICK_HOUSE_PUZZLE6":{"header_address":4772188,"warp_table_address":5528752},"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7":{"header_address":4772216,"warp_table_address":5529024},"MAP_ROUTE110_TRICK_HOUSE_PUZZLE8":{"header_address":4772244,"warp_table_address":5529320},"MAP_ROUTE111":{"fishing_encounters":{"address":5605160,"slots":[129,118,129,118,323,323,323,323,323,323]},"header_address":4758720,"land_encounters":{"address":5605048,"slots":[27,332,27,332,318,318,27,332,318,344,344,344]},"warp_table_address":5442448,"water_encounters":{"address":5605104,"slots":[183,183,183,183,118]}},"MAP_ROUTE111_OLD_LADYS_REST_STOP":{"header_address":4764404,"warp_table_address":5484976},"MAP_ROUTE111_WINSTRATE_FAMILYS_HOUSE":{"header_address":4764376,"warp_table_address":5484916},"MAP_ROUTE112":{"header_address":4758748,"land_encounters":{"address":5605208,"slots":[339,339,183,339,339,183,339,183,339,339,339,339]},"warp_table_address":5443604},"MAP_ROUTE112_CABLE_CAR_STATION":{"header_address":4764432,"warp_table_address":5485060},"MAP_ROUTE113":{"header_address":4758776,"land_encounters":{"address":5605264,"slots":[308,308,218,308,308,218,308,218,308,227,308,227]},"warp_table_address":5444092},"MAP_ROUTE113_GLASS_WORKSHOP":{"header_address":4772328,"warp_table_address":5529640},"MAP_ROUTE114":{"fishing_encounters":{"address":5605432,"slots":[129,118,129,118,323,323,323,323,323,323]},"header_address":4758804,"land_encounters":{"address":5605320,"slots":[358,295,358,358,295,296,296,296,379,379,379,299]},"warp_table_address":5445184,"water_encounters":{"address":5605376,"slots":[183,183,183,183,118]}},"MAP_ROUTE114_FOSSIL_MANIACS_HOUSE":{"header_address":4764488,"warp_table_address":5485204},"MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL":{"header_address":4764516,"warp_table_address":5485320},"MAP_ROUTE114_LANETTES_HOUSE":{"header_address":4764544,"warp_table_address":5485420},"MAP_ROUTE115":{"fishing_encounters":{"address":5607088,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4758832,"land_encounters":{"address":5607004,"slots":[358,304,358,304,304,305,39,39,309,309,309,309]},"warp_table_address":5445988,"water_encounters":{"address":5607060,"slots":[72,309,309,310,310]}},"MAP_ROUTE116":{"header_address":4758860,"land_encounters":{"address":5605480,"slots":[286,370,301,63,301,304,304,304,286,286,315,315]},"warp_table_address":5446872},"MAP_ROUTE116_TUNNELERS_REST_HOUSE":{"header_address":4764572,"warp_table_address":5485564},"MAP_ROUTE117":{"fishing_encounters":{"address":5605620,"slots":[129,118,129,118,326,326,326,326,326,326]},"header_address":4758888,"land_encounters":{"address":5605536,"slots":[286,43,286,43,183,43,387,387,387,387,386,298]},"warp_table_address":5447656,"water_encounters":{"address":5605592,"slots":[183,183,183,183,118]}},"MAP_ROUTE117_POKEMON_DAY_CARE":{"header_address":4764600,"warp_table_address":5485624},"MAP_ROUTE118":{"fishing_encounters":{"address":5605752,"slots":[129,72,129,72,330,331,330,330,330,330]},"header_address":4758916,"land_encounters":{"address":5605668,"slots":[288,337,288,337,289,338,309,309,309,309,309,317]},"warp_table_address":5448236,"water_encounters":{"address":5605724,"slots":[72,309,309,310,310]}},"MAP_ROUTE119":{"fishing_encounters":{"address":5607276,"slots":[129,72,129,72,330,330,330,330,330,330]},"header_address":4758944,"land_encounters":{"address":5607192,"slots":[288,289,288,43,289,43,43,43,369,369,369,317]},"warp_table_address":5449460,"water_encounters":{"address":5607248,"slots":[72,309,309,310,310]}},"MAP_ROUTE119_HOUSE":{"header_address":4772440,"warp_table_address":5530360},"MAP_ROUTE119_WEATHER_INSTITUTE_1F":{"header_address":4772384,"warp_table_address":5529880},"MAP_ROUTE119_WEATHER_INSTITUTE_2F":{"header_address":4772412,"warp_table_address":5530164},"MAP_ROUTE120":{"fishing_encounters":{"address":5607408,"slots":[129,118,129,118,323,323,323,323,323,323]},"header_address":4758972,"land_encounters":{"address":5607324,"slots":[286,287,287,43,183,43,43,183,376,376,317,298]},"warp_table_address":5451160,"water_encounters":{"address":5607380,"slots":[183,183,183,183,118]}},"MAP_ROUTE121":{"fishing_encounters":{"address":5607540,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4759000,"land_encounters":{"address":5607456,"slots":[286,377,287,377,287,43,43,44,309,309,309,317]},"warp_table_address":5452364,"water_encounters":{"address":5607512,"slots":[72,309,309,310,310]}},"MAP_ROUTE121_SAFARI_ZONE_ENTRANCE":{"header_address":4764628,"warp_table_address":5485732},"MAP_ROUTE122":{"fishing_encounters":{"address":5607616,"slots":[129,72,129,72,313,331,313,313,313,313]},"header_address":4759028,"warp_table_address":5452576,"water_encounters":{"address":5607588,"slots":[72,309,309,310,310]}},"MAP_ROUTE123":{"fishing_encounters":{"address":5607748,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4759056,"land_encounters":{"address":5607664,"slots":[286,377,287,377,287,43,43,44,309,309,309,317]},"warp_table_address":5453636,"water_encounters":{"address":5607720,"slots":[72,309,309,310,310]}},"MAP_ROUTE123_BERRY_MASTERS_HOUSE":{"header_address":4772356,"warp_table_address":5529724},"MAP_ROUTE124":{"fishing_encounters":{"address":5605828,"slots":[129,72,129,72,313,331,313,313,313,313]},"header_address":4759084,"warp_table_address":5454436,"water_encounters":{"address":5605800,"slots":[72,309,309,310,310]}},"MAP_ROUTE124_DIVING_TREASURE_HUNTERS_HOUSE":{"header_address":4772468,"warp_table_address":5530420},"MAP_ROUTE125":{"fishing_encounters":{"address":5608272,"slots":[129,72,129,72,313,331,313,313,313,313]},"header_address":4759112,"warp_table_address":5454716,"water_encounters":{"address":5608244,"slots":[72,309,309,310,310]}},"MAP_ROUTE126":{"fishing_encounters":{"address":5608348,"slots":[129,72,129,72,313,331,313,313,313,313]},"header_address":4759140,"warp_table_address":4160749568,"water_encounters":{"address":5608320,"slots":[72,309,309,310,310]}},"MAP_ROUTE127":{"fishing_encounters":{"address":5608424,"slots":[129,72,129,72,313,331,313,313,313,313]},"header_address":4759168,"warp_table_address":4160749568,"water_encounters":{"address":5608396,"slots":[72,309,309,310,310]}},"MAP_ROUTE128":{"fishing_encounters":{"address":5608500,"slots":[129,72,129,325,313,325,313,222,313,313]},"header_address":4759196,"warp_table_address":4160749568,"water_encounters":{"address":5608472,"slots":[72,309,309,310,310]}},"MAP_ROUTE129":{"fishing_encounters":{"address":5608576,"slots":[129,72,129,72,313,331,313,313,313,313]},"header_address":4759224,"warp_table_address":4160749568,"water_encounters":{"address":5608548,"slots":[72,309,309,310,314]}},"MAP_ROUTE130":{"fishing_encounters":{"address":5608708,"slots":[129,72,129,72,313,331,313,313,313,313]},"header_address":4759252,"land_encounters":{"address":5608624,"slots":[360,360,360,360,360,360,360,360,360,360,360,360]},"warp_table_address":4160749568,"water_encounters":{"address":5608680,"slots":[72,309,309,310,310]}},"MAP_ROUTE131":{"fishing_encounters":{"address":5608784,"slots":[129,72,129,72,313,331,313,313,313,313]},"header_address":4759280,"warp_table_address":5456116,"water_encounters":{"address":5608756,"slots":[72,309,309,310,310]}},"MAP_ROUTE132":{"fishing_encounters":{"address":5608860,"slots":[129,72,129,72,313,331,313,116,313,313]},"header_address":4759308,"warp_table_address":4160749568,"water_encounters":{"address":5608832,"slots":[72,309,309,310,310]}},"MAP_ROUTE133":{"fishing_encounters":{"address":5608936,"slots":[129,72,129,72,313,331,313,116,313,313]},"header_address":4759336,"warp_table_address":4160749568,"water_encounters":{"address":5608908,"slots":[72,309,309,310,310]}},"MAP_ROUTE134":{"fishing_encounters":{"address":5609012,"slots":[129,72,129,72,313,331,313,116,313,313]},"header_address":4759364,"warp_table_address":4160749568,"water_encounters":{"address":5608984,"slots":[72,309,309,310,310]}},"MAP_RUSTBORO_CITY":{"header_address":4758076,"warp_table_address":5430936},"MAP_RUSTBORO_CITY_CUTTERS_HOUSE":{"header_address":4762024,"warp_table_address":5472204},"MAP_RUSTBORO_CITY_DEVON_CORP_1F":{"header_address":4761716,"warp_table_address":5470532},"MAP_RUSTBORO_CITY_DEVON_CORP_2F":{"header_address":4761744,"warp_table_address":5470744},"MAP_RUSTBORO_CITY_DEVON_CORP_3F":{"header_address":4761772,"warp_table_address":5470852},"MAP_RUSTBORO_CITY_FLAT1_1F":{"header_address":4761940,"warp_table_address":5471808},"MAP_RUSTBORO_CITY_FLAT1_2F":{"header_address":4761968,"warp_table_address":5472044},"MAP_RUSTBORO_CITY_FLAT2_1F":{"header_address":4762080,"warp_table_address":5472372},"MAP_RUSTBORO_CITY_FLAT2_2F":{"header_address":4762108,"warp_table_address":5472464},"MAP_RUSTBORO_CITY_FLAT2_3F":{"header_address":4762136,"warp_table_address":5472548},"MAP_RUSTBORO_CITY_GYM":{"header_address":4761800,"warp_table_address":5471024},"MAP_RUSTBORO_CITY_HOUSE1":{"header_address":4761996,"warp_table_address":5472120},"MAP_RUSTBORO_CITY_HOUSE2":{"header_address":4762052,"warp_table_address":5472288},"MAP_RUSTBORO_CITY_HOUSE3":{"header_address":4762164,"warp_table_address":5472648},"MAP_RUSTBORO_CITY_MART":{"header_address":4761912,"warp_table_address":5471724},"MAP_RUSTBORO_CITY_POKEMON_CENTER_1F":{"header_address":4761856,"warp_table_address":5471444},"MAP_RUSTBORO_CITY_POKEMON_CENTER_2F":{"header_address":4761884,"warp_table_address":5471584},"MAP_RUSTBORO_CITY_POKEMON_SCHOOL":{"header_address":4761828,"warp_table_address":5471252},"MAP_RUSTURF_TUNNEL":{"header_address":4764768,"land_encounters":{"address":5605932,"slots":[370,370,370,370,370,370,370,370,370,370,370,370]},"warp_table_address":5486644},"MAP_SAFARI_ZONE_NORTH":{"header_address":4769416,"land_encounters":{"address":5610280,"slots":[231,43,231,43,177,44,44,177,178,214,178,214]},"warp_table_address":4160749568},"MAP_SAFARI_ZONE_NORTHEAST":{"header_address":4769724,"land_encounters":{"address":5612476,"slots":[190,216,190,216,191,165,163,204,228,241,228,241]},"warp_table_address":4160749568},"MAP_SAFARI_ZONE_NORTHWEST":{"fishing_encounters":{"address":5610448,"slots":[129,118,129,118,118,118,118,119,119,119]},"header_address":4769388,"land_encounters":{"address":5610364,"slots":[111,43,111,43,84,44,44,84,85,127,85,127]},"warp_table_address":4160749568,"water_encounters":{"address":5610420,"slots":[54,54,54,55,55]}},"MAP_SAFARI_ZONE_REST_HOUSE":{"header_address":4769696,"warp_table_address":5516996},"MAP_SAFARI_ZONE_SOUTH":{"header_address":4769472,"land_encounters":{"address":5606212,"slots":[43,43,203,203,177,84,44,202,25,202,25,202]},"warp_table_address":5515444},"MAP_SAFARI_ZONE_SOUTHEAST":{"fishing_encounters":{"address":5612428,"slots":[129,118,129,118,223,118,223,223,223,224]},"header_address":4769752,"land_encounters":{"address":5612344,"slots":[191,179,191,179,190,167,163,209,234,207,234,207]},"warp_table_address":4160749568,"water_encounters":{"address":5612400,"slots":[194,183,183,183,195]}},"MAP_SAFARI_ZONE_SOUTHWEST":{"fishing_encounters":{"address":5610232,"slots":[129,118,129,118,118,118,118,119,119,119]},"header_address":4769444,"land_encounters":{"address":5610148,"slots":[43,43,203,203,177,84,44,202,25,202,25,202]},"warp_table_address":5515260,"water_encounters":{"address":5610204,"slots":[54,54,54,54,54]}},"MAP_SCORCHED_SLAB":{"header_address":4766700,"warp_table_address":5498144},"MAP_SEAFLOOR_CAVERN_ENTRANCE":{"fishing_encounters":{"address":5609764,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4765412,"warp_table_address":5491796,"water_encounters":{"address":5609736,"slots":[72,41,41,42,42]}},"MAP_SEAFLOOR_CAVERN_ROOM1":{"header_address":4765440,"land_encounters":{"address":5609136,"slots":[41,41,41,41,41,41,41,41,42,42,42,42]},"warp_table_address":5491952},"MAP_SEAFLOOR_CAVERN_ROOM2":{"header_address":4765468,"land_encounters":{"address":5609192,"slots":[41,41,41,41,41,41,41,41,42,42,42,42]},"warp_table_address":5492188},"MAP_SEAFLOOR_CAVERN_ROOM3":{"header_address":4765496,"land_encounters":{"address":5609248,"slots":[41,41,41,41,41,41,41,41,42,42,42,42]},"warp_table_address":5492456},"MAP_SEAFLOOR_CAVERN_ROOM4":{"header_address":4765524,"land_encounters":{"address":5609304,"slots":[41,41,41,41,41,41,41,41,42,42,42,42]},"warp_table_address":5492548},"MAP_SEAFLOOR_CAVERN_ROOM5":{"header_address":4765552,"land_encounters":{"address":5609360,"slots":[41,41,41,41,41,41,41,41,42,42,42,42]},"warp_table_address":5492744},"MAP_SEAFLOOR_CAVERN_ROOM6":{"fishing_encounters":{"address":5609500,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4765580,"land_encounters":{"address":5609416,"slots":[41,41,41,41,41,41,41,41,42,42,42,42]},"warp_table_address":5492788,"water_encounters":{"address":5609472,"slots":[72,41,41,42,42]}},"MAP_SEAFLOOR_CAVERN_ROOM7":{"fishing_encounters":{"address":5609632,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4765608,"land_encounters":{"address":5609548,"slots":[41,41,41,41,41,41,41,41,42,42,42,42]},"warp_table_address":5492832,"water_encounters":{"address":5609604,"slots":[72,41,41,42,42]}},"MAP_SEAFLOOR_CAVERN_ROOM8":{"header_address":4765636,"land_encounters":{"address":5609680,"slots":[41,41,41,41,41,41,41,41,42,42,42,42]},"warp_table_address":5493156},"MAP_SEAFLOOR_CAVERN_ROOM9":{"header_address":4765664,"warp_table_address":5493360},"MAP_SEALED_CHAMBER_INNER_ROOM":{"header_address":4766672,"warp_table_address":5497984},"MAP_SEALED_CHAMBER_OUTER_ROOM":{"header_address":4766644,"warp_table_address":5497608},"MAP_SECRET_BASE_BLUE_CAVE1":{"header_address":4767736,"warp_table_address":5501652},"MAP_SECRET_BASE_BLUE_CAVE2":{"header_address":4767904,"warp_table_address":5503980},"MAP_SECRET_BASE_BLUE_CAVE3":{"header_address":4768072,"warp_table_address":5506308},"MAP_SECRET_BASE_BLUE_CAVE4":{"header_address":4768240,"warp_table_address":5508636},"MAP_SECRET_BASE_BROWN_CAVE1":{"header_address":4767708,"warp_table_address":5501264},"MAP_SECRET_BASE_BROWN_CAVE2":{"header_address":4767876,"warp_table_address":5503592},"MAP_SECRET_BASE_BROWN_CAVE3":{"header_address":4768044,"warp_table_address":5505920},"MAP_SECRET_BASE_BROWN_CAVE4":{"header_address":4768212,"warp_table_address":5508248},"MAP_SECRET_BASE_RED_CAVE1":{"header_address":4767680,"warp_table_address":5500876},"MAP_SECRET_BASE_RED_CAVE2":{"header_address":4767848,"warp_table_address":5503204},"MAP_SECRET_BASE_RED_CAVE3":{"header_address":4768016,"warp_table_address":5505532},"MAP_SECRET_BASE_RED_CAVE4":{"header_address":4768184,"warp_table_address":5507860},"MAP_SECRET_BASE_SHRUB1":{"header_address":4767820,"warp_table_address":5502816},"MAP_SECRET_BASE_SHRUB2":{"header_address":4767988,"warp_table_address":5505144},"MAP_SECRET_BASE_SHRUB3":{"header_address":4768156,"warp_table_address":5507472},"MAP_SECRET_BASE_SHRUB4":{"header_address":4768324,"warp_table_address":5509800},"MAP_SECRET_BASE_TREE1":{"header_address":4767792,"warp_table_address":5502428},"MAP_SECRET_BASE_TREE2":{"header_address":4767960,"warp_table_address":5504756},"MAP_SECRET_BASE_TREE3":{"header_address":4768128,"warp_table_address":5507084},"MAP_SECRET_BASE_TREE4":{"header_address":4768296,"warp_table_address":5509412},"MAP_SECRET_BASE_YELLOW_CAVE1":{"header_address":4767764,"warp_table_address":5502040},"MAP_SECRET_BASE_YELLOW_CAVE2":{"header_address":4767932,"warp_table_address":5504368},"MAP_SECRET_BASE_YELLOW_CAVE3":{"header_address":4768100,"warp_table_address":5506696},"MAP_SECRET_BASE_YELLOW_CAVE4":{"header_address":4768268,"warp_table_address":5509024},"MAP_SHOAL_CAVE_HIGH_TIDE_ENTRANCE_ROOM":{"header_address":4766056,"warp_table_address":4160749568},"MAP_SHOAL_CAVE_HIGH_TIDE_INNER_ROOM":{"header_address":4766084,"warp_table_address":4160749568},"MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM":{"fishing_encounters":{"address":5611436,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4765944,"land_encounters":{"address":5611352,"slots":[41,341,41,341,41,341,41,341,42,341,42,341]},"warp_table_address":5494828,"water_encounters":{"address":5611408,"slots":[72,41,341,341,341]}},"MAP_SHOAL_CAVE_LOW_TIDE_ICE_ROOM":{"header_address":4766980,"land_encounters":{"address":5612044,"slots":[41,341,41,341,41,341,346,341,42,346,42,346]},"warp_table_address":5498544},"MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM":{"fishing_encounters":{"address":5611304,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4765972,"land_encounters":{"address":5611220,"slots":[41,341,41,341,41,341,41,341,42,341,42,341]},"warp_table_address":5494904,"water_encounters":{"address":5611276,"slots":[72,41,341,341,341]}},"MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM":{"header_address":4766028,"land_encounters":{"address":5611164,"slots":[41,341,41,341,41,341,41,341,42,341,42,341]},"warp_table_address":5495180},"MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM":{"header_address":4766000,"land_encounters":{"address":5611108,"slots":[41,341,41,341,41,341,41,341,42,341,42,341]},"warp_table_address":5495084},"MAP_SKY_PILLAR_1F":{"header_address":4766868,"land_encounters":{"address":5612100,"slots":[322,42,42,322,319,378,378,319,319,319,319,319]},"warp_table_address":5498328},"MAP_SKY_PILLAR_2F":{"header_address":4766896,"warp_table_address":5498372},"MAP_SKY_PILLAR_3F":{"header_address":4766924,"land_encounters":{"address":5612232,"slots":[322,42,42,322,319,378,378,319,319,319,319,319]},"warp_table_address":5498408},"MAP_SKY_PILLAR_4F":{"header_address":4766952,"warp_table_address":5498452},"MAP_SKY_PILLAR_5F":{"header_address":4767008,"land_encounters":{"address":5612288,"slots":[322,42,42,322,319,378,378,319,319,359,359,359]},"warp_table_address":5498572},"MAP_SKY_PILLAR_ENTRANCE":{"header_address":4766812,"warp_table_address":5498232},"MAP_SKY_PILLAR_OUTSIDE":{"header_address":4766840,"warp_table_address":5498292},"MAP_SKY_PILLAR_TOP":{"header_address":4767036,"warp_table_address":5498656},"MAP_SLATEPORT_CITY":{"fishing_encounters":{"address":5611664,"slots":[129,72,129,72,313,313,313,313,313,313]},"header_address":4758020,"warp_table_address":5429836,"water_encounters":{"address":5611636,"slots":[72,309,309,310,310]}},"MAP_SLATEPORT_CITY_BATTLE_TENT_BATTLE_ROOM":{"header_address":4761212,"warp_table_address":4160749568},"MAP_SLATEPORT_CITY_BATTLE_TENT_CORRIDOR":{"header_address":4761184,"warp_table_address":4160749568},"MAP_SLATEPORT_CITY_BATTLE_TENT_LOBBY":{"header_address":4761156,"warp_table_address":5466624},"MAP_SLATEPORT_CITY_HARBOR":{"header_address":4761352,"warp_table_address":5468328},"MAP_SLATEPORT_CITY_HOUSE":{"header_address":4761380,"warp_table_address":5468492},"MAP_SLATEPORT_CITY_MART":{"header_address":4761464,"warp_table_address":5468856},"MAP_SLATEPORT_CITY_NAME_RATERS_HOUSE":{"header_address":4761240,"warp_table_address":5466832},"MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F":{"header_address":4761296,"warp_table_address":5467456},"MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_2F":{"header_address":4761324,"warp_table_address":5467856},"MAP_SLATEPORT_CITY_POKEMON_CENTER_1F":{"header_address":4761408,"warp_table_address":5468600},"MAP_SLATEPORT_CITY_POKEMON_CENTER_2F":{"header_address":4761436,"warp_table_address":5468740},"MAP_SLATEPORT_CITY_POKEMON_FAN_CLUB":{"header_address":4761268,"warp_table_address":5467084},"MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F":{"header_address":4761100,"warp_table_address":5466360},"MAP_SLATEPORT_CITY_STERNS_SHIPYARD_2F":{"header_address":4761128,"warp_table_address":5466476},"MAP_SOOTOPOLIS_CITY":{"fishing_encounters":{"address":5612184,"slots":[129,72,129,129,129,129,129,130,130,130]},"header_address":4758188,"warp_table_address":5433852,"water_encounters":{"address":5612156,"slots":[129,129,129,129,129]}},"MAP_SOOTOPOLIS_CITY_GYM_1F":{"header_address":4763480,"warp_table_address":5481892},"MAP_SOOTOPOLIS_CITY_GYM_B1F":{"header_address":4763508,"warp_table_address":5482200},"MAP_SOOTOPOLIS_CITY_HOUSE1":{"header_address":4763620,"warp_table_address":5482664},"MAP_SOOTOPOLIS_CITY_HOUSE2":{"header_address":4763648,"warp_table_address":5482724},"MAP_SOOTOPOLIS_CITY_HOUSE3":{"header_address":4763676,"warp_table_address":5482808},"MAP_SOOTOPOLIS_CITY_HOUSE4":{"header_address":4763704,"warp_table_address":5482916},"MAP_SOOTOPOLIS_CITY_HOUSE5":{"header_address":4763732,"warp_table_address":5483000},"MAP_SOOTOPOLIS_CITY_HOUSE6":{"header_address":4763760,"warp_table_address":5483060},"MAP_SOOTOPOLIS_CITY_HOUSE7":{"header_address":4763788,"warp_table_address":5483144},"MAP_SOOTOPOLIS_CITY_LOTAD_AND_SEEDOT_HOUSE":{"header_address":4763816,"warp_table_address":5483228},"MAP_SOOTOPOLIS_CITY_MART":{"header_address":4763592,"warp_table_address":5482580},"MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F":{"header_address":4763844,"warp_table_address":5483312},"MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_B1F":{"header_address":4763872,"warp_table_address":5483380},"MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F":{"header_address":4763536,"warp_table_address":5482324},"MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F":{"header_address":4763564,"warp_table_address":5482464},"MAP_SOUTHERN_ISLAND_EXTERIOR":{"header_address":4769640,"warp_table_address":5516780},"MAP_SOUTHERN_ISLAND_INTERIOR":{"header_address":4769668,"warp_table_address":5516876},"MAP_SS_TIDAL_CORRIDOR":{"header_address":4768828,"warp_table_address":5510992},"MAP_SS_TIDAL_LOWER_DECK":{"header_address":4768856,"warp_table_address":5511276},"MAP_SS_TIDAL_ROOMS":{"header_address":4768884,"warp_table_address":5511508},"MAP_TERRA_CAVE_END":{"header_address":4767596,"warp_table_address":5500392},"MAP_TERRA_CAVE_ENTRANCE":{"header_address":4767568,"warp_table_address":5500332},"MAP_TRADE_CENTER":{"header_address":4768380,"warp_table_address":5509944},"MAP_TRAINER_HILL_1F":{"header_address":4771096,"warp_table_address":5525172},"MAP_TRAINER_HILL_2F":{"header_address":4771124,"warp_table_address":5525208},"MAP_TRAINER_HILL_3F":{"header_address":4771152,"warp_table_address":5525244},"MAP_TRAINER_HILL_4F":{"header_address":4771180,"warp_table_address":5525280},"MAP_TRAINER_HILL_ELEVATOR":{"header_address":4771852,"warp_table_address":5526300},"MAP_TRAINER_HILL_ENTRANCE":{"header_address":4771068,"warp_table_address":5525100},"MAP_TRAINER_HILL_ROOF":{"header_address":4771208,"warp_table_address":5525340},"MAP_UNDERWATER_MARINE_CAVE":{"header_address":4767484,"warp_table_address":5500208},"MAP_UNDERWATER_ROUTE105":{"header_address":4759532,"warp_table_address":5457348},"MAP_UNDERWATER_ROUTE124":{"header_address":4759392,"warp_table_address":4160749568,"water_encounters":{"address":5612016,"slots":[373,170,373,381,381]}},"MAP_UNDERWATER_ROUTE125":{"header_address":4759560,"warp_table_address":5457384},"MAP_UNDERWATER_ROUTE126":{"header_address":4759420,"warp_table_address":5457052,"water_encounters":{"address":5606268,"slots":[373,170,373,381,381]}},"MAP_UNDERWATER_ROUTE127":{"header_address":4759448,"warp_table_address":5457176},"MAP_UNDERWATER_ROUTE128":{"header_address":4759476,"warp_table_address":5457260},"MAP_UNDERWATER_ROUTE129":{"header_address":4759504,"warp_table_address":5457312},"MAP_UNDERWATER_ROUTE134":{"header_address":4766588,"warp_table_address":5497540},"MAP_UNDERWATER_SEAFLOOR_CAVERN":{"header_address":4765384,"warp_table_address":5491744},"MAP_UNDERWATER_SEALED_CHAMBER":{"header_address":4766616,"warp_table_address":5497568},"MAP_UNDERWATER_SOOTOPOLIS_CITY":{"header_address":4764796,"warp_table_address":5486768},"MAP_UNION_ROOM":{"header_address":4769360,"warp_table_address":5514872},"MAP_UNUSED_CONTEST_HALL1":{"header_address":4768492,"warp_table_address":4160749568},"MAP_UNUSED_CONTEST_HALL2":{"header_address":4768520,"warp_table_address":4160749568},"MAP_UNUSED_CONTEST_HALL3":{"header_address":4768548,"warp_table_address":4160749568},"MAP_UNUSED_CONTEST_HALL4":{"header_address":4768576,"warp_table_address":4160749568},"MAP_UNUSED_CONTEST_HALL5":{"header_address":4768604,"warp_table_address":4160749568},"MAP_UNUSED_CONTEST_HALL6":{"header_address":4768632,"warp_table_address":4160749568},"MAP_VERDANTURF_TOWN":{"header_address":4758384,"warp_table_address":5436044},"MAP_VERDANTURF_TOWN_BATTLE_TENT_BATTLE_ROOM":{"header_address":4760512,"warp_table_address":4160749568},"MAP_VERDANTURF_TOWN_BATTLE_TENT_CORRIDOR":{"header_address":4760484,"warp_table_address":4160749568},"MAP_VERDANTURF_TOWN_BATTLE_TENT_LOBBY":{"header_address":4760456,"warp_table_address":5463128},"MAP_VERDANTURF_TOWN_FRIENDSHIP_RATERS_HOUSE":{"header_address":4760652,"warp_table_address":5463928},"MAP_VERDANTURF_TOWN_HOUSE":{"header_address":4760680,"warp_table_address":5464012},"MAP_VERDANTURF_TOWN_MART":{"header_address":4760540,"warp_table_address":5463408},"MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F":{"header_address":4760568,"warp_table_address":5463540},"MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F":{"header_address":4760596,"warp_table_address":5463680},"MAP_VERDANTURF_TOWN_WANDAS_HOUSE":{"header_address":4760624,"warp_table_address":5463844},"MAP_VICTORY_ROAD_1F":{"header_address":4765860,"land_encounters":{"address":5606156,"slots":[42,336,383,371,41,335,42,336,382,370,382,370]},"warp_table_address":5493852},"MAP_VICTORY_ROAD_B1F":{"header_address":4765888,"land_encounters":{"address":5610496,"slots":[42,336,383,383,42,336,42,336,383,355,383,355]},"warp_table_address":5494460},"MAP_VICTORY_ROAD_B2F":{"fishing_encounters":{"address":5610664,"slots":[129,118,129,118,323,323,323,324,324,324]},"header_address":4765916,"land_encounters":{"address":5610580,"slots":[42,322,383,383,42,322,42,322,383,355,383,355]},"warp_table_address":5494704,"water_encounters":{"address":5610636,"slots":[42,42,42,42,42]}}},"misc_pokemon":[{"address":2572358,"species":385},{"address":2018148,"species":360},{"address":2323175,"species":101},{"address":2323252,"species":101},{"address":2581669,"species":317},{"address":2581574,"species":317},{"address":2581688,"species":317},{"address":2581593,"species":317},{"address":2581612,"species":317},{"address":2581631,"species":317},{"address":2581650,"species":317},{"address":2065036,"species":317},{"address":2386223,"species":185},{"address":2339323,"species":100},{"address":2339400,"species":100},{"address":2339477,"species":100}],"misc_ram_addresses":{"CB2_Overworld":134768624,"gArchipelagoDeathLinkQueued":33804824,"gArchipelagoReceivedItem":33804776,"gMain":50340544,"gPlayerParty":33703196,"gSaveBlock1Ptr":50355596,"gSaveBlock2Ptr":50355600},"misc_rom_addresses":{"gArchipelagoInfo":5912960,"gArchipelagoItemNames":5896457,"gArchipelagoNameTable":5905457,"gArchipelagoOptions":5895556,"gArchipelagoPlayerNames":5895607,"gBattleMoves":3281380,"gEvolutionTable":3318404,"gLevelUpLearnsets":3334884,"gRandomizedBerryTreeItems":5843560,"gRandomizedSoundTable":10155508,"gSpeciesInfo":3296744,"gTMHMLearnsets":3289780,"gTrainers":3230072,"gTutorMoves":6428060,"sFanfares":5422580,"sNewGamePCItems":6210444,"sStarterMon":6021752,"sTMHMMoves":6432208,"sTutorLearnsets":6428120},"species":[{"abilities":[0,0],"address":3296744,"base_stats":[0,0,0,0,0,0],"catch_rate":0,"evolutions":[],"friendship":0,"id":0,"learnset":{"address":3308280,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":4,"move_id":45},{"level":7,"move_id":73},{"level":10,"move_id":22},{"level":15,"move_id":77},{"level":15,"move_id":79},{"level":20,"move_id":75},{"level":25,"move_id":230},{"level":32,"move_id":74},{"level":39,"move_id":235},{"level":46,"move_id":76}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[65,0],"address":3296772,"base_stats":[45,49,49,45,65,65],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":16,"species":2}],"friendship":70,"id":1,"learnset":{"address":3308280,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":4,"move_id":45},{"level":7,"move_id":73},{"level":10,"move_id":22},{"level":15,"move_id":77},{"level":15,"move_id":79},{"level":20,"move_id":75},{"level":25,"move_id":230},{"level":32,"move_id":74},{"level":39,"move_id":235},{"level":46,"move_id":76}]},"tmhm_learnset":"00E41E0884350720","types":[12,3]},{"abilities":[65,0],"address":3296800,"base_stats":[60,62,63,60,80,80],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":32,"species":3}],"friendship":70,"id":2,"learnset":{"address":3308308,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":1,"move_id":73},{"level":4,"move_id":45},{"level":7,"move_id":73},{"level":10,"move_id":22},{"level":15,"move_id":77},{"level":15,"move_id":79},{"level":22,"move_id":75},{"level":29,"move_id":230},{"level":38,"move_id":74},{"level":47,"move_id":235},{"level":56,"move_id":76}]},"tmhm_learnset":"00E41E0884350720","types":[12,3]},{"abilities":[65,0],"address":3296828,"base_stats":[80,82,83,80,100,100],"catch_rate":45,"evolutions":[],"friendship":70,"id":3,"learnset":{"address":3308338,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":1,"move_id":73},{"level":1,"move_id":22},{"level":4,"move_id":45},{"level":7,"move_id":73},{"level":10,"move_id":22},{"level":15,"move_id":77},{"level":15,"move_id":79},{"level":22,"move_id":75},{"level":29,"move_id":230},{"level":41,"move_id":74},{"level":53,"move_id":235},{"level":65,"move_id":76}]},"tmhm_learnset":"00E41E0886354730","types":[12,3]},{"abilities":[66,0],"address":3296856,"base_stats":[39,52,43,65,60,50],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":16,"species":5}],"friendship":70,"id":4,"learnset":{"address":3308368,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":45},{"level":7,"move_id":52},{"level":13,"move_id":108},{"level":19,"move_id":99},{"level":25,"move_id":184},{"level":31,"move_id":53},{"level":37,"move_id":163},{"level":43,"move_id":82},{"level":49,"move_id":83}]},"tmhm_learnset":"00A61EA4CC510623","types":[10,10]},{"abilities":[66,0],"address":3296884,"base_stats":[58,64,58,80,80,65],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":36,"species":6}],"friendship":70,"id":5,"learnset":{"address":3308394,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":45},{"level":1,"move_id":52},{"level":7,"move_id":52},{"level":13,"move_id":108},{"level":20,"move_id":99},{"level":27,"move_id":184},{"level":34,"move_id":53},{"level":41,"move_id":163},{"level":48,"move_id":82},{"level":55,"move_id":83}]},"tmhm_learnset":"00A61EA4CC510623","types":[10,10]},{"abilities":[66,0],"address":3296912,"base_stats":[78,84,78,100,109,85],"catch_rate":45,"evolutions":[],"friendship":70,"id":6,"learnset":{"address":3308420,"moves":[{"level":1,"move_id":10},{"level":1,"move_id":45},{"level":1,"move_id":52},{"level":1,"move_id":108},{"level":7,"move_id":52},{"level":13,"move_id":108},{"level":20,"move_id":99},{"level":27,"move_id":184},{"level":34,"move_id":53},{"level":36,"move_id":17},{"level":44,"move_id":163},{"level":54,"move_id":82},{"level":64,"move_id":83}]},"tmhm_learnset":"00AE5EA4CE514633","types":[10,2]},{"abilities":[67,0],"address":3296940,"base_stats":[44,48,65,43,50,64],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":16,"species":8}],"friendship":70,"id":7,"learnset":{"address":3308448,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":4,"move_id":39},{"level":7,"move_id":145},{"level":10,"move_id":110},{"level":13,"move_id":55},{"level":18,"move_id":44},{"level":23,"move_id":229},{"level":28,"move_id":182},{"level":33,"move_id":240},{"level":40,"move_id":130},{"level":47,"move_id":56}]},"tmhm_learnset":"03B01E00CC533265","types":[11,11]},{"abilities":[67,0],"address":3296968,"base_stats":[59,63,80,58,65,80],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":36,"species":9}],"friendship":70,"id":8,"learnset":{"address":3308478,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":39},{"level":1,"move_id":145},{"level":4,"move_id":39},{"level":7,"move_id":145},{"level":10,"move_id":110},{"level":13,"move_id":55},{"level":19,"move_id":44},{"level":25,"move_id":229},{"level":31,"move_id":182},{"level":37,"move_id":240},{"level":45,"move_id":130},{"level":53,"move_id":56}]},"tmhm_learnset":"03B01E00CC533265","types":[11,11]},{"abilities":[67,0],"address":3296996,"base_stats":[79,83,100,78,85,105],"catch_rate":45,"evolutions":[],"friendship":70,"id":9,"learnset":{"address":3308508,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":39},{"level":1,"move_id":145},{"level":1,"move_id":110},{"level":4,"move_id":39},{"level":7,"move_id":145},{"level":10,"move_id":110},{"level":13,"move_id":55},{"level":19,"move_id":44},{"level":25,"move_id":229},{"level":31,"move_id":182},{"level":42,"move_id":240},{"level":55,"move_id":130},{"level":68,"move_id":56}]},"tmhm_learnset":"03B01E00CE537275","types":[11,11]},{"abilities":[19,0],"address":3297024,"base_stats":[45,30,35,45,20,20],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":7,"species":11}],"friendship":70,"id":10,"learnset":{"address":3308538,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":81}]},"tmhm_learnset":"0000000000000000","types":[6,6]},{"abilities":[61,0],"address":3297052,"base_stats":[50,20,55,30,25,25],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":10,"species":12}],"friendship":70,"id":11,"learnset":{"address":3308548,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":106},{"level":7,"move_id":106}]},"tmhm_learnset":"0000000000000000","types":[6,6]},{"abilities":[14,0],"address":3297080,"base_stats":[60,45,50,70,80,80],"catch_rate":45,"evolutions":[],"friendship":70,"id":12,"learnset":{"address":3308560,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":93},{"level":10,"move_id":93},{"level":13,"move_id":77},{"level":14,"move_id":78},{"level":15,"move_id":79},{"level":18,"move_id":48},{"level":23,"move_id":18},{"level":28,"move_id":16},{"level":34,"move_id":60},{"level":40,"move_id":219},{"level":47,"move_id":318}]},"tmhm_learnset":"0040BE80B43F4620","types":[6,2]},{"abilities":[19,0],"address":3297108,"base_stats":[40,35,30,50,20,20],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":7,"species":14}],"friendship":70,"id":13,"learnset":{"address":3308590,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":40},{"level":1,"move_id":81}]},"tmhm_learnset":"0000000000000000","types":[6,3]},{"abilities":[61,0],"address":3297136,"base_stats":[45,25,50,35,25,25],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":10,"species":15}],"friendship":70,"id":14,"learnset":{"address":3308600,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":106},{"level":7,"move_id":106}]},"tmhm_learnset":"0000000000000000","types":[6,3]},{"abilities":[68,0],"address":3297164,"base_stats":[65,80,40,75,45,80],"catch_rate":45,"evolutions":[],"friendship":70,"id":15,"learnset":{"address":3308612,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":31},{"level":10,"move_id":31},{"level":15,"move_id":116},{"level":20,"move_id":41},{"level":25,"move_id":99},{"level":30,"move_id":228},{"level":35,"move_id":42},{"level":40,"move_id":97},{"level":45,"move_id":283}]},"tmhm_learnset":"00843E88C4354620","types":[6,3]},{"abilities":[51,0],"address":3297192,"base_stats":[40,45,40,56,35,35],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":18,"species":17}],"friendship":70,"id":16,"learnset":{"address":3308638,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":5,"move_id":28},{"level":9,"move_id":16},{"level":13,"move_id":98},{"level":19,"move_id":18},{"level":25,"move_id":17},{"level":31,"move_id":297},{"level":39,"move_id":97},{"level":47,"move_id":119}]},"tmhm_learnset":"00087E8084130620","types":[0,2]},{"abilities":[51,0],"address":3297220,"base_stats":[63,60,55,71,50,50],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":36,"species":18}],"friendship":70,"id":17,"learnset":{"address":3308664,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":28},{"level":1,"move_id":16},{"level":5,"move_id":28},{"level":9,"move_id":16},{"level":13,"move_id":98},{"level":20,"move_id":18},{"level":27,"move_id":17},{"level":34,"move_id":297},{"level":43,"move_id":97},{"level":52,"move_id":119}]},"tmhm_learnset":"00087E8084130620","types":[0,2]},{"abilities":[51,0],"address":3297248,"base_stats":[83,80,75,91,70,70],"catch_rate":45,"evolutions":[],"friendship":70,"id":18,"learnset":{"address":3308690,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":28},{"level":1,"move_id":16},{"level":1,"move_id":98},{"level":5,"move_id":28},{"level":9,"move_id":16},{"level":13,"move_id":98},{"level":20,"move_id":18},{"level":27,"move_id":17},{"level":34,"move_id":297},{"level":48,"move_id":97},{"level":62,"move_id":119}]},"tmhm_learnset":"00087E8084134620","types":[0,2]},{"abilities":[50,62],"address":3297276,"base_stats":[30,56,35,72,25,35],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":20,"species":20}],"friendship":70,"id":19,"learnset":{"address":3308716,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":39},{"level":7,"move_id":98},{"level":13,"move_id":158},{"level":20,"move_id":116},{"level":27,"move_id":228},{"level":34,"move_id":162},{"level":41,"move_id":283}]},"tmhm_learnset":"00843E02ADD33E20","types":[0,0]},{"abilities":[50,62],"address":3297304,"base_stats":[55,81,60,97,50,70],"catch_rate":127,"evolutions":[],"friendship":70,"id":20,"learnset":{"address":3308738,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":39},{"level":1,"move_id":98},{"level":7,"move_id":98},{"level":13,"move_id":158},{"level":20,"move_id":184},{"level":30,"move_id":228},{"level":40,"move_id":162},{"level":50,"move_id":283}]},"tmhm_learnset":"00A43E02ADD37E30","types":[0,0]},{"abilities":[51,0],"address":3297332,"base_stats":[40,60,30,70,31,31],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":20,"species":22}],"friendship":70,"id":21,"learnset":{"address":3308760,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":64},{"level":1,"move_id":45},{"level":7,"move_id":43},{"level":13,"move_id":31},{"level":19,"move_id":228},{"level":25,"move_id":332},{"level":31,"move_id":119},{"level":37,"move_id":65},{"level":43,"move_id":97}]},"tmhm_learnset":"00087E8084130620","types":[0,2]},{"abilities":[51,0],"address":3297360,"base_stats":[65,90,65,100,61,61],"catch_rate":90,"evolutions":[],"friendship":70,"id":22,"learnset":{"address":3308784,"moves":[{"level":1,"move_id":64},{"level":1,"move_id":45},{"level":1,"move_id":43},{"level":1,"move_id":31},{"level":7,"move_id":43},{"level":13,"move_id":31},{"level":26,"move_id":228},{"level":32,"move_id":119},{"level":40,"move_id":65},{"level":47,"move_id":97}]},"tmhm_learnset":"00087E8084134620","types":[0,2]},{"abilities":[22,61],"address":3297388,"base_stats":[35,60,44,55,40,54],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":22,"species":24}],"friendship":70,"id":23,"learnset":{"address":3308806,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":35},{"level":1,"move_id":43},{"level":8,"move_id":40},{"level":13,"move_id":44},{"level":20,"move_id":137},{"level":25,"move_id":103},{"level":32,"move_id":51},{"level":37,"move_id":254},{"level":37,"move_id":256},{"level":37,"move_id":255},{"level":44,"move_id":114}]},"tmhm_learnset":"00213F088E570620","types":[3,3]},{"abilities":[22,61],"address":3297416,"base_stats":[60,85,69,80,65,79],"catch_rate":90,"evolutions":[],"friendship":70,"id":24,"learnset":{"address":3308834,"moves":[{"level":1,"move_id":35},{"level":1,"move_id":43},{"level":1,"move_id":40},{"level":1,"move_id":44},{"level":8,"move_id":40},{"level":13,"move_id":44},{"level":20,"move_id":137},{"level":28,"move_id":103},{"level":38,"move_id":51},{"level":46,"move_id":254},{"level":46,"move_id":256},{"level":46,"move_id":255},{"level":56,"move_id":114}]},"tmhm_learnset":"00213F088E574620","types":[3,3]},{"abilities":[9,0],"address":3297444,"base_stats":[35,55,30,90,50,40],"catch_rate":190,"evolutions":[{"method":"ITEM","param":96,"species":26}],"friendship":70,"id":25,"learnset":{"address":3308862,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":84},{"level":1,"move_id":45},{"level":6,"move_id":39},{"level":8,"move_id":86},{"level":11,"move_id":98},{"level":15,"move_id":104},{"level":20,"move_id":21},{"level":26,"move_id":85},{"level":33,"move_id":97},{"level":41,"move_id":87},{"level":50,"move_id":113}]},"tmhm_learnset":"00E01E02CDD38221","types":[13,13]},{"abilities":[9,0],"address":3297472,"base_stats":[60,90,55,100,90,80],"catch_rate":75,"evolutions":[],"friendship":70,"id":26,"learnset":{"address":3308890,"moves":[{"level":1,"move_id":84},{"level":1,"move_id":39},{"level":1,"move_id":98},{"level":1,"move_id":85}]},"tmhm_learnset":"00E03E02CDD3C221","types":[13,13]},{"abilities":[8,0],"address":3297500,"base_stats":[50,75,85,40,20,30],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":22,"species":28}],"friendship":70,"id":27,"learnset":{"address":3308900,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":6,"move_id":111},{"level":11,"move_id":28},{"level":17,"move_id":40},{"level":23,"move_id":163},{"level":30,"move_id":129},{"level":37,"move_id":154},{"level":45,"move_id":328},{"level":53,"move_id":201}]},"tmhm_learnset":"00A43ED0CE510621","types":[4,4]},{"abilities":[8,0],"address":3297528,"base_stats":[75,100,110,65,45,55],"catch_rate":90,"evolutions":[],"friendship":70,"id":28,"learnset":{"address":3308926,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":111},{"level":1,"move_id":28},{"level":6,"move_id":111},{"level":11,"move_id":28},{"level":17,"move_id":40},{"level":24,"move_id":163},{"level":33,"move_id":129},{"level":42,"move_id":154},{"level":52,"move_id":328},{"level":62,"move_id":201}]},"tmhm_learnset":"00A43ED0CE514621","types":[4,4]},{"abilities":[38,0],"address":3297556,"base_stats":[55,47,52,41,40,40],"catch_rate":235,"evolutions":[{"method":"LEVEL","param":16,"species":30}],"friendship":70,"id":29,"learnset":{"address":3308952,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":45},{"level":1,"move_id":10},{"level":8,"move_id":39},{"level":12,"move_id":24},{"level":17,"move_id":40},{"level":20,"move_id":44},{"level":23,"move_id":270},{"level":30,"move_id":154},{"level":38,"move_id":260},{"level":47,"move_id":242}]},"tmhm_learnset":"00A43E8A8DD33624","types":[3,3]},{"abilities":[38,0],"address":3297584,"base_stats":[70,62,67,56,55,55],"catch_rate":120,"evolutions":[{"method":"ITEM","param":94,"species":31}],"friendship":70,"id":30,"learnset":{"address":3308978,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":45},{"level":1,"move_id":10},{"level":8,"move_id":39},{"level":12,"move_id":24},{"level":18,"move_id":40},{"level":22,"move_id":44},{"level":26,"move_id":270},{"level":34,"move_id":154},{"level":43,"move_id":260},{"level":53,"move_id":242}]},"tmhm_learnset":"00A43E8A8DD33624","types":[3,3]},{"abilities":[38,0],"address":3297612,"base_stats":[90,82,87,76,75,85],"catch_rate":45,"evolutions":[],"friendship":70,"id":31,"learnset":{"address":3309004,"moves":[{"level":1,"move_id":10},{"level":1,"move_id":39},{"level":1,"move_id":24},{"level":1,"move_id":40},{"level":23,"move_id":34}]},"tmhm_learnset":"00B43FFEEFD37E35","types":[3,4]},{"abilities":[38,0],"address":3297640,"base_stats":[46,57,40,50,40,40],"catch_rate":235,"evolutions":[{"method":"LEVEL","param":16,"species":33}],"friendship":70,"id":32,"learnset":{"address":3309016,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":43},{"level":1,"move_id":64},{"level":8,"move_id":116},{"level":12,"move_id":24},{"level":17,"move_id":40},{"level":20,"move_id":30},{"level":23,"move_id":270},{"level":30,"move_id":31},{"level":38,"move_id":260},{"level":47,"move_id":32}]},"tmhm_learnset":"00A43E0A8DD33624","types":[3,3]},{"abilities":[38,0],"address":3297668,"base_stats":[61,72,57,65,55,55],"catch_rate":120,"evolutions":[{"method":"ITEM","param":94,"species":34}],"friendship":70,"id":33,"learnset":{"address":3309042,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":43},{"level":1,"move_id":64},{"level":8,"move_id":116},{"level":12,"move_id":24},{"level":18,"move_id":40},{"level":22,"move_id":30},{"level":26,"move_id":270},{"level":34,"move_id":31},{"level":43,"move_id":260},{"level":53,"move_id":32}]},"tmhm_learnset":"00A43E0A8DD33624","types":[3,3]},{"abilities":[38,0],"address":3297696,"base_stats":[81,92,77,85,85,75],"catch_rate":45,"evolutions":[],"friendship":70,"id":34,"learnset":{"address":3309068,"moves":[{"level":1,"move_id":64},{"level":1,"move_id":116},{"level":1,"move_id":24},{"level":1,"move_id":40},{"level":23,"move_id":37}]},"tmhm_learnset":"00B43F7EEFD37E35","types":[3,4]},{"abilities":[56,0],"address":3297724,"base_stats":[70,45,48,35,60,65],"catch_rate":150,"evolutions":[{"method":"ITEM","param":94,"species":36}],"friendship":140,"id":35,"learnset":{"address":3309080,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":1},{"level":1,"move_id":45},{"level":5,"move_id":227},{"level":9,"move_id":47},{"level":13,"move_id":3},{"level":17,"move_id":266},{"level":21,"move_id":107},{"level":25,"move_id":111},{"level":29,"move_id":118},{"level":33,"move_id":322},{"level":37,"move_id":236},{"level":41,"move_id":113},{"level":45,"move_id":309}]},"tmhm_learnset":"00611E27FDFBB62D","types":[0,0]},{"abilities":[56,0],"address":3297752,"base_stats":[95,70,73,60,85,90],"catch_rate":25,"evolutions":[],"friendship":140,"id":36,"learnset":{"address":3309112,"moves":[{"level":1,"move_id":47},{"level":1,"move_id":3},{"level":1,"move_id":107},{"level":1,"move_id":118}]},"tmhm_learnset":"00611E27FDFBF62D","types":[0,0]},{"abilities":[18,0],"address":3297780,"base_stats":[38,41,40,65,50,65],"catch_rate":190,"evolutions":[{"method":"ITEM","param":95,"species":38}],"friendship":70,"id":37,"learnset":{"address":3309122,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":52},{"level":5,"move_id":39},{"level":9,"move_id":46},{"level":13,"move_id":98},{"level":17,"move_id":261},{"level":21,"move_id":109},{"level":25,"move_id":286},{"level":29,"move_id":53},{"level":33,"move_id":219},{"level":37,"move_id":288},{"level":41,"move_id":83}]},"tmhm_learnset":"00021E248C590630","types":[10,10]},{"abilities":[18,0],"address":3297808,"base_stats":[73,76,75,100,81,100],"catch_rate":75,"evolutions":[],"friendship":70,"id":38,"learnset":{"address":3309152,"moves":[{"level":1,"move_id":52},{"level":1,"move_id":98},{"level":1,"move_id":109},{"level":1,"move_id":219},{"level":45,"move_id":83}]},"tmhm_learnset":"00021E248C594630","types":[10,10]},{"abilities":[56,0],"address":3297836,"base_stats":[115,45,20,20,45,25],"catch_rate":170,"evolutions":[{"method":"ITEM","param":94,"species":40}],"friendship":70,"id":39,"learnset":{"address":3309164,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":1},{"level":1,"move_id":47},{"level":4,"move_id":111},{"level":9,"move_id":1},{"level":14,"move_id":50},{"level":19,"move_id":205},{"level":24,"move_id":3},{"level":29,"move_id":156},{"level":34,"move_id":34},{"level":39,"move_id":102},{"level":44,"move_id":304},{"level":49,"move_id":38}]},"tmhm_learnset":"00611E27FDBBB625","types":[0,0]},{"abilities":[56,0],"address":3297864,"base_stats":[140,70,45,45,75,50],"catch_rate":50,"evolutions":[],"friendship":70,"id":40,"learnset":{"address":3309194,"moves":[{"level":1,"move_id":47},{"level":1,"move_id":50},{"level":1,"move_id":111},{"level":1,"move_id":3}]},"tmhm_learnset":"00611E27FDBBF625","types":[0,0]},{"abilities":[39,0],"address":3297892,"base_stats":[40,45,35,55,30,40],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":22,"species":42}],"friendship":70,"id":41,"learnset":{"address":3309204,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":141},{"level":6,"move_id":48},{"level":11,"move_id":310},{"level":16,"move_id":44},{"level":21,"move_id":17},{"level":26,"move_id":109},{"level":31,"move_id":314},{"level":36,"move_id":212},{"level":41,"move_id":305},{"level":46,"move_id":114}]},"tmhm_learnset":"00017F88A4170E20","types":[3,2]},{"abilities":[39,0],"address":3297920,"base_stats":[75,80,70,90,65,75],"catch_rate":90,"evolutions":[{"method":"FRIENDSHIP","param":0,"species":169}],"friendship":70,"id":42,"learnset":{"address":3309232,"moves":[{"level":1,"move_id":103},{"level":1,"move_id":141},{"level":1,"move_id":48},{"level":1,"move_id":310},{"level":6,"move_id":48},{"level":11,"move_id":310},{"level":16,"move_id":44},{"level":21,"move_id":17},{"level":28,"move_id":109},{"level":35,"move_id":314},{"level":42,"move_id":212},{"level":49,"move_id":305},{"level":56,"move_id":114}]},"tmhm_learnset":"00017F88A4174E20","types":[3,2]},{"abilities":[34,0],"address":3297948,"base_stats":[45,50,55,30,75,65],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":21,"species":44}],"friendship":70,"id":43,"learnset":{"address":3309260,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":71},{"level":7,"move_id":230},{"level":14,"move_id":77},{"level":16,"move_id":78},{"level":18,"move_id":79},{"level":23,"move_id":51},{"level":32,"move_id":236},{"level":39,"move_id":80}]},"tmhm_learnset":"00441E0884350720","types":[12,3]},{"abilities":[34,0],"address":3297976,"base_stats":[60,65,70,40,85,75],"catch_rate":120,"evolutions":[{"method":"ITEM","param":98,"species":45},{"method":"ITEM","param":93,"species":182}],"friendship":70,"id":44,"learnset":{"address":3309284,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":71},{"level":1,"move_id":230},{"level":1,"move_id":77},{"level":7,"move_id":230},{"level":14,"move_id":77},{"level":16,"move_id":78},{"level":18,"move_id":79},{"level":24,"move_id":51},{"level":35,"move_id":236},{"level":44,"move_id":80}]},"tmhm_learnset":"00441E0884350720","types":[12,3]},{"abilities":[34,0],"address":3298004,"base_stats":[75,80,85,50,100,90],"catch_rate":45,"evolutions":[],"friendship":70,"id":45,"learnset":{"address":3309308,"moves":[{"level":1,"move_id":71},{"level":1,"move_id":312},{"level":1,"move_id":78},{"level":1,"move_id":72},{"level":44,"move_id":80}]},"tmhm_learnset":"00441E0884354720","types":[12,3]},{"abilities":[27,0],"address":3298032,"base_stats":[35,70,55,25,45,55],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":24,"species":47}],"friendship":70,"id":46,"learnset":{"address":3309320,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":7,"move_id":78},{"level":13,"move_id":77},{"level":19,"move_id":141},{"level":25,"move_id":147},{"level":31,"move_id":163},{"level":37,"move_id":74},{"level":43,"move_id":202},{"level":49,"move_id":312}]},"tmhm_learnset":"00C43E888C350720","types":[6,12]},{"abilities":[27,0],"address":3298060,"base_stats":[60,95,80,30,60,80],"catch_rate":75,"evolutions":[],"friendship":70,"id":47,"learnset":{"address":3309346,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":78},{"level":1,"move_id":77},{"level":7,"move_id":78},{"level":13,"move_id":77},{"level":19,"move_id":141},{"level":27,"move_id":147},{"level":35,"move_id":163},{"level":43,"move_id":74},{"level":51,"move_id":202},{"level":59,"move_id":312}]},"tmhm_learnset":"00C43E888C354720","types":[6,12]},{"abilities":[14,0],"address":3298088,"base_stats":[60,55,50,45,40,55],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":31,"species":49}],"friendship":70,"id":48,"learnset":{"address":3309372,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":50},{"level":1,"move_id":193},{"level":9,"move_id":48},{"level":17,"move_id":93},{"level":20,"move_id":77},{"level":25,"move_id":141},{"level":28,"move_id":78},{"level":33,"move_id":60},{"level":36,"move_id":79},{"level":41,"move_id":94}]},"tmhm_learnset":"0040BE0894350620","types":[6,3]},{"abilities":[19,0],"address":3298116,"base_stats":[70,65,60,90,90,75],"catch_rate":75,"evolutions":[],"friendship":70,"id":49,"learnset":{"address":3309398,"moves":[{"level":1,"move_id":318},{"level":1,"move_id":33},{"level":1,"move_id":50},{"level":1,"move_id":193},{"level":1,"move_id":48},{"level":9,"move_id":48},{"level":17,"move_id":93},{"level":20,"move_id":77},{"level":25,"move_id":141},{"level":28,"move_id":78},{"level":31,"move_id":16},{"level":36,"move_id":60},{"level":42,"move_id":79},{"level":52,"move_id":94}]},"tmhm_learnset":"0040BE8894354620","types":[6,3]},{"abilities":[8,71],"address":3298144,"base_stats":[10,55,25,95,35,45],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":26,"species":51}],"friendship":70,"id":50,"learnset":{"address":3309428,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":28},{"level":5,"move_id":45},{"level":9,"move_id":222},{"level":17,"move_id":91},{"level":25,"move_id":189},{"level":33,"move_id":163},{"level":41,"move_id":89},{"level":49,"move_id":90}]},"tmhm_learnset":"00843EC88E110620","types":[4,4]},{"abilities":[8,71],"address":3298172,"base_stats":[35,80,50,120,50,70],"catch_rate":50,"evolutions":[],"friendship":70,"id":51,"learnset":{"address":3309452,"moves":[{"level":1,"move_id":161},{"level":1,"move_id":10},{"level":1,"move_id":28},{"level":1,"move_id":45},{"level":5,"move_id":45},{"level":9,"move_id":222},{"level":17,"move_id":91},{"level":25,"move_id":189},{"level":26,"move_id":328},{"level":38,"move_id":163},{"level":51,"move_id":89},{"level":64,"move_id":90}]},"tmhm_learnset":"00843EC88E114620","types":[4,4]},{"abilities":[53,0],"address":3298200,"base_stats":[40,45,35,90,40,40],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":28,"species":53}],"friendship":70,"id":52,"learnset":{"address":3309478,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":45},{"level":11,"move_id":44},{"level":20,"move_id":6},{"level":28,"move_id":185},{"level":35,"move_id":103},{"level":41,"move_id":154},{"level":46,"move_id":163},{"level":50,"move_id":252}]},"tmhm_learnset":"00453F82ADD30E24","types":[0,0]},{"abilities":[7,0],"address":3298228,"base_stats":[65,70,60,115,65,65],"catch_rate":90,"evolutions":[],"friendship":70,"id":53,"learnset":{"address":3309502,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":45},{"level":1,"move_id":44},{"level":11,"move_id":44},{"level":20,"move_id":6},{"level":29,"move_id":185},{"level":38,"move_id":103},{"level":46,"move_id":154},{"level":53,"move_id":163},{"level":59,"move_id":252}]},"tmhm_learnset":"00453F82ADD34E34","types":[0,0]},{"abilities":[6,13],"address":3298256,"base_stats":[50,52,48,55,65,50],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":33,"species":55}],"friendship":70,"id":54,"learnset":{"address":3309526,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":346},{"level":1,"move_id":10},{"level":5,"move_id":39},{"level":10,"move_id":50},{"level":16,"move_id":93},{"level":23,"move_id":103},{"level":31,"move_id":244},{"level":40,"move_id":154},{"level":50,"move_id":56}]},"tmhm_learnset":"03F01E80CC53326D","types":[11,11]},{"abilities":[6,13],"address":3298284,"base_stats":[80,82,78,85,95,80],"catch_rate":75,"evolutions":[],"friendship":70,"id":55,"learnset":{"address":3309550,"moves":[{"level":1,"move_id":346},{"level":1,"move_id":10},{"level":1,"move_id":39},{"level":1,"move_id":50},{"level":5,"move_id":39},{"level":10,"move_id":50},{"level":16,"move_id":93},{"level":23,"move_id":103},{"level":31,"move_id":244},{"level":44,"move_id":154},{"level":58,"move_id":56}]},"tmhm_learnset":"03F01E80CC53726D","types":[11,11]},{"abilities":[72,0],"address":3298312,"base_stats":[40,80,35,70,35,45],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":28,"species":57}],"friendship":70,"id":56,"learnset":{"address":3309574,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":43},{"level":9,"move_id":67},{"level":15,"move_id":2},{"level":21,"move_id":154},{"level":27,"move_id":116},{"level":33,"move_id":69},{"level":39,"move_id":238},{"level":45,"move_id":103},{"level":51,"move_id":37}]},"tmhm_learnset":"00A23EC0CFD30EA1","types":[1,1]},{"abilities":[72,0],"address":3298340,"base_stats":[65,105,60,95,60,70],"catch_rate":75,"evolutions":[],"friendship":70,"id":57,"learnset":{"address":3309600,"moves":[{"level":1,"move_id":10},{"level":1,"move_id":43},{"level":1,"move_id":67},{"level":1,"move_id":99},{"level":9,"move_id":67},{"level":15,"move_id":2},{"level":21,"move_id":154},{"level":27,"move_id":116},{"level":28,"move_id":99},{"level":36,"move_id":69},{"level":45,"move_id":238},{"level":54,"move_id":103},{"level":63,"move_id":37}]},"tmhm_learnset":"00A23EC0CFD34EA1","types":[1,1]},{"abilities":[22,18],"address":3298368,"base_stats":[55,70,45,60,70,50],"catch_rate":190,"evolutions":[{"method":"ITEM","param":95,"species":59}],"friendship":70,"id":58,"learnset":{"address":3309628,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":44},{"level":1,"move_id":46},{"level":7,"move_id":52},{"level":13,"move_id":43},{"level":19,"move_id":316},{"level":25,"move_id":36},{"level":31,"move_id":172},{"level":37,"move_id":270},{"level":43,"move_id":97},{"level":49,"move_id":53}]},"tmhm_learnset":"00A23EA48C510630","types":[10,10]},{"abilities":[22,18],"address":3298396,"base_stats":[90,110,80,95,100,80],"catch_rate":75,"evolutions":[],"friendship":70,"id":59,"learnset":{"address":3309654,"moves":[{"level":1,"move_id":44},{"level":1,"move_id":46},{"level":1,"move_id":52},{"level":1,"move_id":316},{"level":49,"move_id":245}]},"tmhm_learnset":"00A23EA48C514630","types":[10,10]},{"abilities":[11,6],"address":3298424,"base_stats":[40,50,40,90,40,40],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":25,"species":61}],"friendship":70,"id":60,"learnset":{"address":3309666,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":145},{"level":7,"move_id":95},{"level":13,"move_id":55},{"level":19,"move_id":3},{"level":25,"move_id":240},{"level":31,"move_id":34},{"level":37,"move_id":187},{"level":43,"move_id":56}]},"tmhm_learnset":"03103E009C133264","types":[11,11]},{"abilities":[11,6],"address":3298452,"base_stats":[65,65,65,90,50,50],"catch_rate":120,"evolutions":[{"method":"ITEM","param":97,"species":62},{"method":"ITEM","param":187,"species":186}],"friendship":70,"id":61,"learnset":{"address":3309690,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":145},{"level":1,"move_id":95},{"level":1,"move_id":55},{"level":7,"move_id":95},{"level":13,"move_id":55},{"level":19,"move_id":3},{"level":27,"move_id":240},{"level":35,"move_id":34},{"level":43,"move_id":187},{"level":51,"move_id":56}]},"tmhm_learnset":"03B03E00DE133265","types":[11,11]},{"abilities":[11,6],"address":3298480,"base_stats":[90,85,95,70,70,90],"catch_rate":45,"evolutions":[],"friendship":70,"id":62,"learnset":{"address":3309714,"moves":[{"level":1,"move_id":55},{"level":1,"move_id":95},{"level":1,"move_id":3},{"level":1,"move_id":66},{"level":35,"move_id":66},{"level":51,"move_id":170}]},"tmhm_learnset":"03B03E40DE1372E5","types":[11,1]},{"abilities":[28,39],"address":3298508,"base_stats":[25,20,15,90,105,55],"catch_rate":200,"evolutions":[{"method":"LEVEL","param":16,"species":64}],"friendship":70,"id":63,"learnset":{"address":3309728,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":93},{"level":1,"move_id":100}]},"tmhm_learnset":"0041BF03B45B8E29","types":[14,14]},{"abilities":[28,39],"address":3298536,"base_stats":[40,35,30,105,120,70],"catch_rate":100,"evolutions":[{"method":"LEVEL","param":37,"species":65}],"friendship":70,"id":64,"learnset":{"address":3309738,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":100},{"level":1,"move_id":134},{"level":1,"move_id":93},{"level":16,"move_id":93},{"level":18,"move_id":50},{"level":21,"move_id":60},{"level":23,"move_id":115},{"level":25,"move_id":105},{"level":30,"move_id":248},{"level":33,"move_id":272},{"level":36,"move_id":94},{"level":43,"move_id":271}]},"tmhm_learnset":"0041BF03B45B8E29","types":[14,14]},{"abilities":[28,39],"address":3298564,"base_stats":[55,50,45,120,135,85],"catch_rate":50,"evolutions":[],"friendship":70,"id":65,"learnset":{"address":3309766,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":100},{"level":1,"move_id":134},{"level":1,"move_id":93},{"level":16,"move_id":93},{"level":18,"move_id":50},{"level":21,"move_id":60},{"level":23,"move_id":115},{"level":25,"move_id":105},{"level":30,"move_id":248},{"level":33,"move_id":347},{"level":36,"move_id":94},{"level":43,"move_id":271}]},"tmhm_learnset":"0041BF03B45BCE29","types":[14,14]},{"abilities":[62,0],"address":3298592,"base_stats":[70,80,50,35,35,35],"catch_rate":180,"evolutions":[{"method":"LEVEL","param":28,"species":67}],"friendship":70,"id":66,"learnset":{"address":3309794,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":67},{"level":1,"move_id":43},{"level":7,"move_id":116},{"level":13,"move_id":2},{"level":19,"move_id":69},{"level":22,"move_id":193},{"level":25,"move_id":279},{"level":31,"move_id":233},{"level":37,"move_id":66},{"level":40,"move_id":238},{"level":43,"move_id":184},{"level":49,"move_id":223}]},"tmhm_learnset":"00A03E64CE1306A1","types":[1,1]},{"abilities":[62,0],"address":3298620,"base_stats":[80,100,70,45,50,60],"catch_rate":90,"evolutions":[{"method":"LEVEL","param":37,"species":68}],"friendship":70,"id":67,"learnset":{"address":3309824,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":67},{"level":1,"move_id":43},{"level":1,"move_id":116},{"level":7,"move_id":116},{"level":13,"move_id":2},{"level":19,"move_id":69},{"level":22,"move_id":193},{"level":25,"move_id":279},{"level":33,"move_id":233},{"level":41,"move_id":66},{"level":46,"move_id":238},{"level":51,"move_id":184},{"level":59,"move_id":223}]},"tmhm_learnset":"00A03E64CE1306A1","types":[1,1]},{"abilities":[62,0],"address":3298648,"base_stats":[90,130,80,55,65,85],"catch_rate":45,"evolutions":[],"friendship":70,"id":68,"learnset":{"address":3309854,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":67},{"level":1,"move_id":43},{"level":1,"move_id":116},{"level":7,"move_id":116},{"level":13,"move_id":2},{"level":19,"move_id":69},{"level":22,"move_id":193},{"level":25,"move_id":279},{"level":33,"move_id":233},{"level":41,"move_id":66},{"level":46,"move_id":238},{"level":51,"move_id":184},{"level":59,"move_id":223}]},"tmhm_learnset":"00A03E64CE1346A1","types":[1,1]},{"abilities":[34,0],"address":3298676,"base_stats":[50,75,35,40,70,30],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":21,"species":70}],"friendship":70,"id":69,"learnset":{"address":3309884,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":22},{"level":6,"move_id":74},{"level":11,"move_id":35},{"level":15,"move_id":79},{"level":17,"move_id":77},{"level":19,"move_id":78},{"level":23,"move_id":51},{"level":30,"move_id":230},{"level":37,"move_id":75},{"level":45,"move_id":21}]},"tmhm_learnset":"00443E0884350720","types":[12,3]},{"abilities":[34,0],"address":3298704,"base_stats":[65,90,50,55,85,45],"catch_rate":120,"evolutions":[{"method":"ITEM","param":98,"species":71}],"friendship":70,"id":70,"learnset":{"address":3309912,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":22},{"level":1,"move_id":74},{"level":1,"move_id":35},{"level":6,"move_id":74},{"level":11,"move_id":35},{"level":15,"move_id":79},{"level":17,"move_id":77},{"level":19,"move_id":78},{"level":24,"move_id":51},{"level":33,"move_id":230},{"level":42,"move_id":75},{"level":54,"move_id":21}]},"tmhm_learnset":"00443E0884350720","types":[12,3]},{"abilities":[34,0],"address":3298732,"base_stats":[80,105,65,70,100,60],"catch_rate":45,"evolutions":[],"friendship":70,"id":71,"learnset":{"address":3309940,"moves":[{"level":1,"move_id":22},{"level":1,"move_id":79},{"level":1,"move_id":230},{"level":1,"move_id":75}]},"tmhm_learnset":"00443E0884354720","types":[12,3]},{"abilities":[29,64],"address":3298760,"base_stats":[40,40,35,70,50,100],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":30,"species":73}],"friendship":70,"id":72,"learnset":{"address":3309950,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":40},{"level":6,"move_id":48},{"level":12,"move_id":132},{"level":19,"move_id":51},{"level":25,"move_id":61},{"level":30,"move_id":35},{"level":36,"move_id":112},{"level":43,"move_id":103},{"level":49,"move_id":56}]},"tmhm_learnset":"03143E0884173264","types":[11,3]},{"abilities":[29,64],"address":3298788,"base_stats":[80,70,65,100,80,120],"catch_rate":60,"evolutions":[],"friendship":70,"id":73,"learnset":{"address":3309976,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":40},{"level":1,"move_id":48},{"level":1,"move_id":132},{"level":6,"move_id":48},{"level":12,"move_id":132},{"level":19,"move_id":51},{"level":25,"move_id":61},{"level":30,"move_id":35},{"level":38,"move_id":112},{"level":47,"move_id":103},{"level":55,"move_id":56}]},"tmhm_learnset":"03143E0884177264","types":[11,3]},{"abilities":[69,5],"address":3298816,"base_stats":[40,80,100,20,30,30],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":25,"species":75}],"friendship":70,"id":74,"learnset":{"address":3310002,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":111},{"level":6,"move_id":300},{"level":11,"move_id":88},{"level":16,"move_id":222},{"level":21,"move_id":120},{"level":26,"move_id":205},{"level":31,"move_id":350},{"level":36,"move_id":89},{"level":41,"move_id":153},{"level":46,"move_id":38}]},"tmhm_learnset":"00A01E74CE110621","types":[5,4]},{"abilities":[69,5],"address":3298844,"base_stats":[55,95,115,35,45,45],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":37,"species":76}],"friendship":70,"id":75,"learnset":{"address":3310030,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":111},{"level":1,"move_id":300},{"level":1,"move_id":88},{"level":6,"move_id":300},{"level":11,"move_id":88},{"level":16,"move_id":222},{"level":21,"move_id":120},{"level":29,"move_id":205},{"level":37,"move_id":350},{"level":45,"move_id":89},{"level":53,"move_id":153},{"level":62,"move_id":38}]},"tmhm_learnset":"00A01E74CE110621","types":[5,4]},{"abilities":[69,5],"address":3298872,"base_stats":[80,110,130,45,55,65],"catch_rate":45,"evolutions":[],"friendship":70,"id":76,"learnset":{"address":3310058,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":111},{"level":1,"move_id":300},{"level":1,"move_id":88},{"level":6,"move_id":300},{"level":11,"move_id":88},{"level":16,"move_id":222},{"level":21,"move_id":120},{"level":29,"move_id":205},{"level":37,"move_id":350},{"level":45,"move_id":89},{"level":53,"move_id":153},{"level":62,"move_id":38}]},"tmhm_learnset":"00A01E74CE114631","types":[5,4]},{"abilities":[50,18],"address":3298900,"base_stats":[50,85,55,90,65,65],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":40,"species":78}],"friendship":70,"id":77,"learnset":{"address":3310086,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":5,"move_id":45},{"level":9,"move_id":39},{"level":14,"move_id":52},{"level":19,"move_id":23},{"level":25,"move_id":83},{"level":31,"move_id":36},{"level":38,"move_id":97},{"level":45,"move_id":340},{"level":53,"move_id":126}]},"tmhm_learnset":"00221E2484710620","types":[10,10]},{"abilities":[50,18],"address":3298928,"base_stats":[65,100,70,105,80,80],"catch_rate":60,"evolutions":[],"friendship":70,"id":78,"learnset":{"address":3310114,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":1,"move_id":39},{"level":1,"move_id":52},{"level":5,"move_id":45},{"level":9,"move_id":39},{"level":14,"move_id":52},{"level":19,"move_id":23},{"level":25,"move_id":83},{"level":31,"move_id":36},{"level":38,"move_id":97},{"level":40,"move_id":31},{"level":50,"move_id":340},{"level":63,"move_id":126}]},"tmhm_learnset":"00221E2484714620","types":[10,10]},{"abilities":[12,20],"address":3298956,"base_stats":[90,65,65,15,40,40],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":37,"species":80},{"method":"ITEM","param":187,"species":199}],"friendship":70,"id":79,"learnset":{"address":3310144,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":174},{"level":1,"move_id":281},{"level":1,"move_id":33},{"level":6,"move_id":45},{"level":15,"move_id":55},{"level":20,"move_id":93},{"level":29,"move_id":50},{"level":34,"move_id":29},{"level":43,"move_id":133},{"level":48,"move_id":94}]},"tmhm_learnset":"02709E24BE5B366C","types":[11,14]},{"abilities":[12,20],"address":3298984,"base_stats":[95,75,110,30,100,80],"catch_rate":75,"evolutions":[],"friendship":70,"id":80,"learnset":{"address":3310168,"moves":[{"level":1,"move_id":174},{"level":1,"move_id":281},{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":6,"move_id":45},{"level":15,"move_id":55},{"level":20,"move_id":93},{"level":29,"move_id":50},{"level":34,"move_id":29},{"level":37,"move_id":110},{"level":46,"move_id":133},{"level":54,"move_id":94}]},"tmhm_learnset":"02F09E24FE5B766D","types":[11,14]},{"abilities":[42,5],"address":3299012,"base_stats":[25,35,70,45,95,55],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":30,"species":82}],"friendship":70,"id":81,"learnset":{"address":3310194,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":319},{"level":1,"move_id":33},{"level":6,"move_id":84},{"level":11,"move_id":48},{"level":16,"move_id":49},{"level":21,"move_id":86},{"level":26,"move_id":209},{"level":32,"move_id":199},{"level":38,"move_id":129},{"level":44,"move_id":103},{"level":50,"move_id":192}]},"tmhm_learnset":"00400E0385930620","types":[13,8]},{"abilities":[42,5],"address":3299040,"base_stats":[50,60,95,70,120,70],"catch_rate":60,"evolutions":[],"friendship":70,"id":82,"learnset":{"address":3310222,"moves":[{"level":1,"move_id":319},{"level":1,"move_id":33},{"level":1,"move_id":84},{"level":1,"move_id":48},{"level":6,"move_id":84},{"level":11,"move_id":48},{"level":16,"move_id":49},{"level":21,"move_id":86},{"level":26,"move_id":209},{"level":35,"move_id":199},{"level":44,"move_id":161},{"level":53,"move_id":103},{"level":62,"move_id":192}]},"tmhm_learnset":"00400E0385934620","types":[13,8]},{"abilities":[51,39],"address":3299068,"base_stats":[52,65,55,60,58,62],"catch_rate":45,"evolutions":[],"friendship":70,"id":83,"learnset":{"address":3310250,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":64},{"level":6,"move_id":28},{"level":11,"move_id":43},{"level":16,"move_id":31},{"level":21,"move_id":282},{"level":26,"move_id":210},{"level":31,"move_id":14},{"level":36,"move_id":97},{"level":41,"move_id":163},{"level":46,"move_id":206}]},"tmhm_learnset":"000C7E8084510620","types":[0,2]},{"abilities":[50,48],"address":3299096,"base_stats":[35,85,45,75,35,35],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":31,"species":85}],"friendship":70,"id":84,"learnset":{"address":3310278,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":64},{"level":1,"move_id":45},{"level":9,"move_id":228},{"level":13,"move_id":31},{"level":21,"move_id":161},{"level":25,"move_id":99},{"level":33,"move_id":253},{"level":37,"move_id":65},{"level":45,"move_id":97}]},"tmhm_learnset":"00087E8084110620","types":[0,2]},{"abilities":[50,48],"address":3299124,"base_stats":[60,110,70,100,60,60],"catch_rate":45,"evolutions":[],"friendship":70,"id":85,"learnset":{"address":3310302,"moves":[{"level":1,"move_id":64},{"level":1,"move_id":45},{"level":1,"move_id":228},{"level":1,"move_id":31},{"level":9,"move_id":228},{"level":13,"move_id":31},{"level":21,"move_id":161},{"level":25,"move_id":99},{"level":38,"move_id":253},{"level":47,"move_id":65},{"level":60,"move_id":97}]},"tmhm_learnset":"00087F8084114E20","types":[0,2]},{"abilities":[47,0],"address":3299152,"base_stats":[65,45,55,45,45,70],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":34,"species":87}],"friendship":70,"id":86,"learnset":{"address":3310326,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":29},{"level":9,"move_id":45},{"level":17,"move_id":196},{"level":21,"move_id":62},{"level":29,"move_id":156},{"level":37,"move_id":36},{"level":41,"move_id":58},{"level":49,"move_id":219}]},"tmhm_learnset":"03103E00841B3264","types":[11,11]},{"abilities":[47,0],"address":3299180,"base_stats":[90,70,80,70,70,95],"catch_rate":75,"evolutions":[],"friendship":70,"id":87,"learnset":{"address":3310350,"moves":[{"level":1,"move_id":29},{"level":1,"move_id":45},{"level":1,"move_id":196},{"level":1,"move_id":62},{"level":9,"move_id":45},{"level":17,"move_id":196},{"level":21,"move_id":62},{"level":29,"move_id":156},{"level":34,"move_id":329},{"level":42,"move_id":36},{"level":51,"move_id":58},{"level":64,"move_id":219}]},"tmhm_learnset":"03103E00841B7264","types":[11,15]},{"abilities":[1,60],"address":3299208,"base_stats":[80,80,50,25,40,50],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":38,"species":89}],"friendship":70,"id":88,"learnset":{"address":3310376,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":139},{"level":1,"move_id":1},{"level":4,"move_id":106},{"level":8,"move_id":50},{"level":13,"move_id":124},{"level":19,"move_id":107},{"level":26,"move_id":103},{"level":34,"move_id":151},{"level":43,"move_id":188},{"level":53,"move_id":262}]},"tmhm_learnset":"00003F6E8D970E20","types":[3,3]},{"abilities":[1,60],"address":3299236,"base_stats":[105,105,75,50,65,100],"catch_rate":75,"evolutions":[],"friendship":70,"id":89,"learnset":{"address":3310402,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":139},{"level":1,"move_id":1},{"level":1,"move_id":106},{"level":4,"move_id":106},{"level":8,"move_id":50},{"level":13,"move_id":124},{"level":19,"move_id":107},{"level":26,"move_id":103},{"level":34,"move_id":151},{"level":47,"move_id":188},{"level":61,"move_id":262}]},"tmhm_learnset":"00A03F6ECD974E21","types":[3,3]},{"abilities":[75,0],"address":3299264,"base_stats":[30,65,100,40,45,25],"catch_rate":190,"evolutions":[{"method":"ITEM","param":97,"species":91}],"friendship":70,"id":90,"learnset":{"address":3310428,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":110},{"level":9,"move_id":48},{"level":17,"move_id":62},{"level":25,"move_id":182},{"level":33,"move_id":43},{"level":41,"move_id":128},{"level":49,"move_id":58}]},"tmhm_learnset":"02101E0084133264","types":[11,11]},{"abilities":[75,0],"address":3299292,"base_stats":[50,95,180,70,85,45],"catch_rate":60,"evolutions":[],"friendship":70,"id":91,"learnset":{"address":3310450,"moves":[{"level":1,"move_id":110},{"level":1,"move_id":48},{"level":1,"move_id":62},{"level":1,"move_id":182},{"level":33,"move_id":191},{"level":41,"move_id":131}]},"tmhm_learnset":"02101F0084137264","types":[11,15]},{"abilities":[26,0],"address":3299320,"base_stats":[30,35,30,80,100,35],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":25,"species":93}],"friendship":70,"id":92,"learnset":{"address":3310464,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":95},{"level":1,"move_id":122},{"level":8,"move_id":180},{"level":13,"move_id":212},{"level":16,"move_id":174},{"level":21,"move_id":101},{"level":28,"move_id":109},{"level":33,"move_id":138},{"level":36,"move_id":194}]},"tmhm_learnset":"0001BF08B4970E20","types":[7,3]},{"abilities":[26,0],"address":3299348,"base_stats":[45,50,45,95,115,55],"catch_rate":90,"evolutions":[{"method":"LEVEL","param":37,"species":94}],"friendship":70,"id":93,"learnset":{"address":3310488,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":95},{"level":1,"move_id":122},{"level":1,"move_id":180},{"level":8,"move_id":180},{"level":13,"move_id":212},{"level":16,"move_id":174},{"level":21,"move_id":101},{"level":25,"move_id":325},{"level":31,"move_id":109},{"level":39,"move_id":138},{"level":48,"move_id":194}]},"tmhm_learnset":"0001BF08B4970E20","types":[7,3]},{"abilities":[26,0],"address":3299376,"base_stats":[60,65,60,110,130,75],"catch_rate":45,"evolutions":[],"friendship":70,"id":94,"learnset":{"address":3310514,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":95},{"level":1,"move_id":122},{"level":1,"move_id":180},{"level":8,"move_id":180},{"level":13,"move_id":212},{"level":16,"move_id":174},{"level":21,"move_id":101},{"level":25,"move_id":325},{"level":31,"move_id":109},{"level":39,"move_id":138},{"level":48,"move_id":194}]},"tmhm_learnset":"00A1BF08F5974E21","types":[7,3]},{"abilities":[69,5],"address":3299404,"base_stats":[35,45,160,70,30,45],"catch_rate":45,"evolutions":[{"method":"ITEM","param":199,"species":208}],"friendship":70,"id":95,"learnset":{"address":3310540,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":103},{"level":9,"move_id":20},{"level":13,"move_id":88},{"level":21,"move_id":106},{"level":25,"move_id":99},{"level":33,"move_id":201},{"level":37,"move_id":21},{"level":45,"move_id":231},{"level":49,"move_id":328},{"level":57,"move_id":38}]},"tmhm_learnset":"00A01F508E510E30","types":[5,4]},{"abilities":[15,0],"address":3299432,"base_stats":[60,48,45,42,43,90],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":26,"species":97}],"friendship":70,"id":96,"learnset":{"address":3310568,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":1},{"level":1,"move_id":95},{"level":10,"move_id":50},{"level":18,"move_id":93},{"level":25,"move_id":29},{"level":31,"move_id":139},{"level":36,"move_id":96},{"level":40,"move_id":94},{"level":43,"move_id":244},{"level":45,"move_id":248}]},"tmhm_learnset":"0041BF01F41B8E29","types":[14,14]},{"abilities":[15,0],"address":3299460,"base_stats":[85,73,70,67,73,115],"catch_rate":75,"evolutions":[],"friendship":70,"id":97,"learnset":{"address":3310594,"moves":[{"level":1,"move_id":1},{"level":1,"move_id":95},{"level":1,"move_id":50},{"level":1,"move_id":93},{"level":10,"move_id":50},{"level":18,"move_id":93},{"level":25,"move_id":29},{"level":33,"move_id":139},{"level":40,"move_id":96},{"level":49,"move_id":94},{"level":55,"move_id":244},{"level":60,"move_id":248}]},"tmhm_learnset":"0041BF01F41BCE29","types":[14,14]},{"abilities":[52,75],"address":3299488,"base_stats":[30,105,90,50,25,25],"catch_rate":225,"evolutions":[{"method":"LEVEL","param":28,"species":99}],"friendship":70,"id":98,"learnset":{"address":3310620,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":145},{"level":5,"move_id":43},{"level":12,"move_id":11},{"level":16,"move_id":106},{"level":23,"move_id":341},{"level":27,"move_id":23},{"level":34,"move_id":12},{"level":41,"move_id":182},{"level":45,"move_id":152}]},"tmhm_learnset":"02B43E408C133264","types":[11,11]},{"abilities":[52,75],"address":3299516,"base_stats":[55,130,115,75,50,50],"catch_rate":60,"evolutions":[],"friendship":70,"id":99,"learnset":{"address":3310646,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":145},{"level":1,"move_id":43},{"level":1,"move_id":11},{"level":5,"move_id":43},{"level":12,"move_id":11},{"level":16,"move_id":106},{"level":23,"move_id":341},{"level":27,"move_id":23},{"level":38,"move_id":12},{"level":49,"move_id":182},{"level":57,"move_id":152}]},"tmhm_learnset":"02B43E408C137264","types":[11,11]},{"abilities":[43,9],"address":3299544,"base_stats":[40,30,50,100,55,55],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":30,"species":101}],"friendship":70,"id":100,"learnset":{"address":3310672,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":268},{"level":1,"move_id":33},{"level":8,"move_id":103},{"level":15,"move_id":49},{"level":21,"move_id":209},{"level":27,"move_id":120},{"level":32,"move_id":205},{"level":37,"move_id":113},{"level":42,"move_id":129},{"level":46,"move_id":153},{"level":49,"move_id":243}]},"tmhm_learnset":"00402F0285938A20","types":[13,13]},{"abilities":[43,9],"address":3299572,"base_stats":[60,50,70,140,80,80],"catch_rate":60,"evolutions":[],"friendship":70,"id":101,"learnset":{"address":3310700,"moves":[{"level":1,"move_id":268},{"level":1,"move_id":33},{"level":1,"move_id":103},{"level":1,"move_id":49},{"level":8,"move_id":103},{"level":15,"move_id":49},{"level":21,"move_id":209},{"level":27,"move_id":120},{"level":34,"move_id":205},{"level":41,"move_id":113},{"level":48,"move_id":129},{"level":54,"move_id":153},{"level":59,"move_id":243}]},"tmhm_learnset":"00402F028593CA20","types":[13,13]},{"abilities":[34,0],"address":3299600,"base_stats":[60,40,80,40,60,45],"catch_rate":90,"evolutions":[{"method":"ITEM","param":98,"species":103}],"friendship":70,"id":102,"learnset":{"address":3310728,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":140},{"level":1,"move_id":253},{"level":1,"move_id":95},{"level":7,"move_id":115},{"level":13,"move_id":73},{"level":19,"move_id":93},{"level":25,"move_id":78},{"level":31,"move_id":77},{"level":37,"move_id":79},{"level":43,"move_id":76}]},"tmhm_learnset":"0060BE0994358720","types":[12,14]},{"abilities":[34,0],"address":3299628,"base_stats":[95,95,85,55,125,65],"catch_rate":45,"evolutions":[],"friendship":70,"id":103,"learnset":{"address":3310752,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":140},{"level":1,"move_id":95},{"level":1,"move_id":93},{"level":19,"move_id":23},{"level":31,"move_id":121}]},"tmhm_learnset":"0060BE099435C720","types":[12,14]},{"abilities":[69,31],"address":3299656,"base_stats":[50,50,95,35,40,50],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":28,"species":105}],"friendship":70,"id":104,"learnset":{"address":3310766,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":125},{"level":1,"move_id":45},{"level":5,"move_id":39},{"level":9,"move_id":125},{"level":13,"move_id":29},{"level":17,"move_id":43},{"level":21,"move_id":116},{"level":25,"move_id":155},{"level":29,"move_id":99},{"level":33,"move_id":206},{"level":37,"move_id":37},{"level":41,"move_id":198},{"level":45,"move_id":38}]},"tmhm_learnset":"00A03EF4CE513621","types":[4,4]},{"abilities":[69,31],"address":3299684,"base_stats":[60,80,110,45,50,80],"catch_rate":75,"evolutions":[],"friendship":70,"id":105,"learnset":{"address":3310798,"moves":[{"level":1,"move_id":45},{"level":1,"move_id":39},{"level":1,"move_id":125},{"level":1,"move_id":29},{"level":5,"move_id":39},{"level":9,"move_id":125},{"level":13,"move_id":29},{"level":17,"move_id":43},{"level":21,"move_id":116},{"level":25,"move_id":155},{"level":32,"move_id":99},{"level":39,"move_id":206},{"level":46,"move_id":37},{"level":53,"move_id":198},{"level":61,"move_id":38}]},"tmhm_learnset":"00A03EF4CE517621","types":[4,4]},{"abilities":[7,0],"address":3299712,"base_stats":[50,120,53,87,35,110],"catch_rate":45,"evolutions":[],"friendship":70,"id":106,"learnset":{"address":3310830,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":279},{"level":1,"move_id":24},{"level":6,"move_id":96},{"level":11,"move_id":27},{"level":16,"move_id":26},{"level":20,"move_id":280},{"level":21,"move_id":116},{"level":26,"move_id":136},{"level":31,"move_id":170},{"level":36,"move_id":193},{"level":41,"move_id":203},{"level":46,"move_id":25},{"level":51,"move_id":179}]},"tmhm_learnset":"00A03E40C61306A1","types":[1,1]},{"abilities":[51,0],"address":3299740,"base_stats":[50,105,79,76,35,110],"catch_rate":45,"evolutions":[],"friendship":70,"id":107,"learnset":{"address":3310862,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":279},{"level":1,"move_id":4},{"level":7,"move_id":97},{"level":13,"move_id":228},{"level":20,"move_id":183},{"level":26,"move_id":9},{"level":26,"move_id":8},{"level":26,"move_id":7},{"level":32,"move_id":327},{"level":38,"move_id":5},{"level":44,"move_id":197},{"level":50,"move_id":68}]},"tmhm_learnset":"00A03E40C61306A1","types":[1,1]},{"abilities":[20,12],"address":3299768,"base_stats":[90,55,75,30,60,75],"catch_rate":45,"evolutions":[],"friendship":70,"id":108,"learnset":{"address":3310892,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":122},{"level":7,"move_id":48},{"level":12,"move_id":111},{"level":18,"move_id":282},{"level":23,"move_id":23},{"level":29,"move_id":35},{"level":34,"move_id":50},{"level":40,"move_id":21},{"level":45,"move_id":103},{"level":51,"move_id":287}]},"tmhm_learnset":"00B43E76EFF37625","types":[0,0]},{"abilities":[26,0],"address":3299796,"base_stats":[40,65,95,35,60,45],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":35,"species":110}],"friendship":70,"id":109,"learnset":{"address":3310920,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":139},{"level":1,"move_id":33},{"level":9,"move_id":123},{"level":17,"move_id":120},{"level":21,"move_id":124},{"level":25,"move_id":108},{"level":33,"move_id":114},{"level":41,"move_id":153},{"level":45,"move_id":194},{"level":49,"move_id":262}]},"tmhm_learnset":"00403F2EA5930E20","types":[3,3]},{"abilities":[26,0],"address":3299824,"base_stats":[65,90,120,60,85,70],"catch_rate":60,"evolutions":[],"friendship":70,"id":110,"learnset":{"address":3310946,"moves":[{"level":1,"move_id":139},{"level":1,"move_id":33},{"level":1,"move_id":123},{"level":1,"move_id":120},{"level":9,"move_id":123},{"level":17,"move_id":120},{"level":21,"move_id":124},{"level":25,"move_id":108},{"level":33,"move_id":114},{"level":44,"move_id":153},{"level":51,"move_id":194},{"level":58,"move_id":262}]},"tmhm_learnset":"00403F2EA5934E20","types":[3,3]},{"abilities":[31,69],"address":3299852,"base_stats":[80,85,95,25,30,30],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":42,"species":112}],"friendship":70,"id":111,"learnset":{"address":3310972,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":30},{"level":1,"move_id":39},{"level":10,"move_id":23},{"level":15,"move_id":31},{"level":24,"move_id":184},{"level":29,"move_id":350},{"level":38,"move_id":32},{"level":43,"move_id":36},{"level":52,"move_id":89},{"level":57,"move_id":224}]},"tmhm_learnset":"00A03E768FD33630","types":[4,5]},{"abilities":[31,69],"address":3299880,"base_stats":[105,130,120,40,45,45],"catch_rate":60,"evolutions":[],"friendship":70,"id":112,"learnset":{"address":3310998,"moves":[{"level":1,"move_id":30},{"level":1,"move_id":39},{"level":1,"move_id":23},{"level":1,"move_id":31},{"level":10,"move_id":23},{"level":15,"move_id":31},{"level":24,"move_id":184},{"level":29,"move_id":350},{"level":38,"move_id":32},{"level":46,"move_id":36},{"level":58,"move_id":89},{"level":66,"move_id":224}]},"tmhm_learnset":"00B43E76CFD37631","types":[4,5]},{"abilities":[30,32],"address":3299908,"base_stats":[250,5,5,50,35,105],"catch_rate":30,"evolutions":[{"method":"FRIENDSHIP","param":0,"species":242}],"friendship":140,"id":113,"learnset":{"address":3311024,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":1},{"level":1,"move_id":45},{"level":5,"move_id":39},{"level":9,"move_id":287},{"level":13,"move_id":135},{"level":17,"move_id":3},{"level":23,"move_id":107},{"level":29,"move_id":47},{"level":35,"move_id":121},{"level":41,"move_id":111},{"level":49,"move_id":113},{"level":57,"move_id":38}]},"tmhm_learnset":"00E19E76F7FBF66D","types":[0,0]},{"abilities":[34,0],"address":3299936,"base_stats":[65,55,115,60,100,40],"catch_rate":45,"evolutions":[],"friendship":70,"id":114,"learnset":{"address":3311054,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":275},{"level":1,"move_id":132},{"level":4,"move_id":79},{"level":10,"move_id":71},{"level":13,"move_id":74},{"level":19,"move_id":77},{"level":22,"move_id":22},{"level":28,"move_id":20},{"level":31,"move_id":72},{"level":37,"move_id":78},{"level":40,"move_id":21},{"level":46,"move_id":321}]},"tmhm_learnset":"00C43E0884354720","types":[12,12]},{"abilities":[48,0],"address":3299964,"base_stats":[105,95,80,90,40,80],"catch_rate":45,"evolutions":[],"friendship":70,"id":115,"learnset":{"address":3311084,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":4},{"level":1,"move_id":43},{"level":7,"move_id":44},{"level":13,"move_id":39},{"level":19,"move_id":252},{"level":25,"move_id":5},{"level":31,"move_id":99},{"level":37,"move_id":203},{"level":43,"move_id":146},{"level":49,"move_id":179}]},"tmhm_learnset":"00B43EF6EFF37675","types":[0,0]},{"abilities":[33,0],"address":3299992,"base_stats":[30,40,70,60,70,25],"catch_rate":225,"evolutions":[{"method":"LEVEL","param":32,"species":117}],"friendship":70,"id":116,"learnset":{"address":3311110,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":145},{"level":8,"move_id":108},{"level":15,"move_id":43},{"level":22,"move_id":55},{"level":29,"move_id":239},{"level":36,"move_id":97},{"level":43,"move_id":56},{"level":50,"move_id":349}]},"tmhm_learnset":"03101E0084133264","types":[11,11]},{"abilities":[38,0],"address":3300020,"base_stats":[55,65,95,85,95,45],"catch_rate":75,"evolutions":[{"method":"ITEM","param":201,"species":230}],"friendship":70,"id":117,"learnset":{"address":3311134,"moves":[{"level":1,"move_id":145},{"level":1,"move_id":108},{"level":1,"move_id":43},{"level":1,"move_id":55},{"level":8,"move_id":108},{"level":15,"move_id":43},{"level":22,"move_id":55},{"level":29,"move_id":239},{"level":40,"move_id":97},{"level":51,"move_id":56},{"level":62,"move_id":349}]},"tmhm_learnset":"03101E0084137264","types":[11,11]},{"abilities":[33,41],"address":3300048,"base_stats":[45,67,60,63,35,50],"catch_rate":225,"evolutions":[{"method":"LEVEL","param":33,"species":119}],"friendship":70,"id":118,"learnset":{"address":3311158,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":64},{"level":1,"move_id":39},{"level":1,"move_id":346},{"level":10,"move_id":48},{"level":15,"move_id":30},{"level":24,"move_id":175},{"level":29,"move_id":31},{"level":38,"move_id":127},{"level":43,"move_id":32},{"level":52,"move_id":97}]},"tmhm_learnset":"03101E0084133264","types":[11,11]},{"abilities":[33,41],"address":3300076,"base_stats":[80,92,65,68,65,80],"catch_rate":60,"evolutions":[],"friendship":70,"id":119,"learnset":{"address":3311182,"moves":[{"level":1,"move_id":64},{"level":1,"move_id":39},{"level":1,"move_id":346},{"level":1,"move_id":48},{"level":10,"move_id":48},{"level":15,"move_id":30},{"level":24,"move_id":175},{"level":29,"move_id":31},{"level":41,"move_id":127},{"level":49,"move_id":32},{"level":61,"move_id":97}]},"tmhm_learnset":"03101E0084137264","types":[11,11]},{"abilities":[35,30],"address":3300104,"base_stats":[30,45,55,85,70,55],"catch_rate":225,"evolutions":[{"method":"ITEM","param":97,"species":121}],"friendship":70,"id":120,"learnset":{"address":3311206,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":106},{"level":6,"move_id":55},{"level":10,"move_id":229},{"level":15,"move_id":105},{"level":19,"move_id":293},{"level":24,"move_id":129},{"level":28,"move_id":61},{"level":33,"move_id":107},{"level":37,"move_id":113},{"level":42,"move_id":322},{"level":46,"move_id":56}]},"tmhm_learnset":"03500E019593B264","types":[11,11]},{"abilities":[35,30],"address":3300132,"base_stats":[60,75,85,115,100,85],"catch_rate":60,"evolutions":[],"friendship":70,"id":121,"learnset":{"address":3311236,"moves":[{"level":1,"move_id":55},{"level":1,"move_id":229},{"level":1,"move_id":105},{"level":1,"move_id":129},{"level":33,"move_id":109}]},"tmhm_learnset":"03508E019593F264","types":[11,14]},{"abilities":[43,0],"address":3300160,"base_stats":[40,45,65,90,100,120],"catch_rate":45,"evolutions":[],"friendship":70,"id":122,"learnset":{"address":3311248,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":112},{"level":5,"move_id":93},{"level":9,"move_id":164},{"level":13,"move_id":96},{"level":17,"move_id":3},{"level":21,"move_id":113},{"level":21,"move_id":115},{"level":25,"move_id":227},{"level":29,"move_id":60},{"level":33,"move_id":278},{"level":37,"move_id":271},{"level":41,"move_id":272},{"level":45,"move_id":94},{"level":49,"move_id":226},{"level":53,"move_id":219}]},"tmhm_learnset":"0041BF03F5BBCE29","types":[14,14]},{"abilities":[68,0],"address":3300188,"base_stats":[70,110,80,105,55,80],"catch_rate":45,"evolutions":[{"method":"ITEM","param":199,"species":212}],"friendship":70,"id":123,"learnset":{"address":3311286,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":98},{"level":1,"move_id":43},{"level":6,"move_id":116},{"level":11,"move_id":228},{"level":16,"move_id":206},{"level":21,"move_id":97},{"level":26,"move_id":17},{"level":31,"move_id":163},{"level":36,"move_id":14},{"level":41,"move_id":104},{"level":46,"move_id":210}]},"tmhm_learnset":"00847E8084134620","types":[6,2]},{"abilities":[12,0],"address":3300216,"base_stats":[65,50,35,95,115,95],"catch_rate":45,"evolutions":[],"friendship":70,"id":124,"learnset":{"address":3311314,"moves":[{"level":1,"move_id":1},{"level":1,"move_id":122},{"level":1,"move_id":142},{"level":1,"move_id":181},{"level":9,"move_id":142},{"level":13,"move_id":181},{"level":21,"move_id":3},{"level":25,"move_id":8},{"level":35,"move_id":212},{"level":41,"move_id":313},{"level":51,"move_id":34},{"level":57,"move_id":195},{"level":67,"move_id":59}]},"tmhm_learnset":"0040BF01F413FA6D","types":[15,14]},{"abilities":[9,0],"address":3300244,"base_stats":[65,83,57,105,95,85],"catch_rate":45,"evolutions":[],"friendship":70,"id":125,"learnset":{"address":3311342,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":98},{"level":1,"move_id":43},{"level":1,"move_id":9},{"level":9,"move_id":9},{"level":17,"move_id":113},{"level":25,"move_id":129},{"level":36,"move_id":103},{"level":47,"move_id":85},{"level":58,"move_id":87}]},"tmhm_learnset":"00E03E02D5D3C221","types":[13,13]},{"abilities":[49,0],"address":3300272,"base_stats":[65,95,57,93,100,85],"catch_rate":45,"evolutions":[],"friendship":70,"id":126,"learnset":{"address":3311364,"moves":[{"level":1,"move_id":52},{"level":1,"move_id":43},{"level":1,"move_id":123},{"level":1,"move_id":7},{"level":7,"move_id":43},{"level":13,"move_id":123},{"level":19,"move_id":7},{"level":25,"move_id":108},{"level":33,"move_id":241},{"level":41,"move_id":53},{"level":49,"move_id":109},{"level":57,"move_id":126}]},"tmhm_learnset":"00A03E24D4514621","types":[10,10]},{"abilities":[52,0],"address":3300300,"base_stats":[65,125,100,85,55,70],"catch_rate":45,"evolutions":[],"friendship":70,"id":127,"learnset":{"address":3311390,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":11},{"level":1,"move_id":116},{"level":7,"move_id":20},{"level":13,"move_id":69},{"level":19,"move_id":106},{"level":25,"move_id":279},{"level":31,"move_id":280},{"level":37,"move_id":12},{"level":43,"move_id":66},{"level":49,"move_id":14}]},"tmhm_learnset":"00A43E40CE1346A1","types":[6,6]},{"abilities":[22,0],"address":3300328,"base_stats":[75,100,95,110,40,70],"catch_rate":45,"evolutions":[],"friendship":70,"id":128,"learnset":{"address":3311416,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":4,"move_id":39},{"level":8,"move_id":99},{"level":13,"move_id":30},{"level":19,"move_id":184},{"level":26,"move_id":228},{"level":34,"move_id":156},{"level":43,"move_id":37},{"level":53,"move_id":36}]},"tmhm_learnset":"00B01E7687F37624","types":[0,0]},{"abilities":[33,0],"address":3300356,"base_stats":[20,10,55,80,15,20],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":20,"species":130}],"friendship":70,"id":129,"learnset":{"address":3311442,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":150},{"level":15,"move_id":33},{"level":30,"move_id":175}]},"tmhm_learnset":"0000000000000000","types":[11,11]},{"abilities":[22,0],"address":3300384,"base_stats":[95,125,79,81,60,100],"catch_rate":45,"evolutions":[],"friendship":70,"id":130,"learnset":{"address":3311456,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":37},{"level":20,"move_id":44},{"level":25,"move_id":82},{"level":30,"move_id":43},{"level":35,"move_id":239},{"level":40,"move_id":56},{"level":45,"move_id":240},{"level":50,"move_id":349},{"level":55,"move_id":63}]},"tmhm_learnset":"03B01F3487937A74","types":[11,2]},{"abilities":[11,75],"address":3300412,"base_stats":[130,85,80,60,85,95],"catch_rate":45,"evolutions":[],"friendship":70,"id":131,"learnset":{"address":3311482,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":55},{"level":1,"move_id":45},{"level":1,"move_id":47},{"level":7,"move_id":54},{"level":13,"move_id":34},{"level":19,"move_id":109},{"level":25,"move_id":195},{"level":31,"move_id":58},{"level":37,"move_id":240},{"level":43,"move_id":219},{"level":49,"move_id":56},{"level":55,"move_id":329}]},"tmhm_learnset":"03B01E0295DB7274","types":[11,15]},{"abilities":[7,0],"address":3300440,"base_stats":[48,48,48,48,48,48],"catch_rate":35,"evolutions":[],"friendship":70,"id":132,"learnset":{"address":3311510,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":144}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[50,0],"address":3300468,"base_stats":[55,55,50,55,45,65],"catch_rate":45,"evolutions":[{"method":"ITEM","param":96,"species":135},{"method":"ITEM","param":97,"species":134},{"method":"ITEM","param":95,"species":136},{"method":"FRIENDSHIP_DAY","param":0,"species":196},{"method":"FRIENDSHIP_NIGHT","param":0,"species":197}],"friendship":70,"id":133,"learnset":{"address":3311520,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":39},{"level":1,"move_id":270},{"level":8,"move_id":28},{"level":16,"move_id":45},{"level":23,"move_id":98},{"level":30,"move_id":44},{"level":36,"move_id":226},{"level":42,"move_id":36}]},"tmhm_learnset":"00001E00AC530620","types":[0,0]},{"abilities":[11,0],"address":3300496,"base_stats":[130,65,60,65,110,95],"catch_rate":45,"evolutions":[],"friendship":70,"id":134,"learnset":{"address":3311542,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":39},{"level":1,"move_id":270},{"level":8,"move_id":28},{"level":16,"move_id":55},{"level":23,"move_id":98},{"level":30,"move_id":44},{"level":36,"move_id":62},{"level":42,"move_id":114},{"level":47,"move_id":151},{"level":52,"move_id":56}]},"tmhm_learnset":"03101E00AC537674","types":[11,11]},{"abilities":[10,0],"address":3300524,"base_stats":[65,65,60,130,110,95],"catch_rate":45,"evolutions":[],"friendship":70,"id":135,"learnset":{"address":3311568,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":39},{"level":1,"move_id":270},{"level":8,"move_id":28},{"level":16,"move_id":84},{"level":23,"move_id":98},{"level":30,"move_id":24},{"level":36,"move_id":42},{"level":42,"move_id":86},{"level":47,"move_id":97},{"level":52,"move_id":87}]},"tmhm_learnset":"00401E02ADD34630","types":[13,13]},{"abilities":[18,0],"address":3300552,"base_stats":[65,130,60,65,95,110],"catch_rate":45,"evolutions":[],"friendship":70,"id":136,"learnset":{"address":3311594,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":39},{"level":1,"move_id":270},{"level":8,"move_id":28},{"level":16,"move_id":52},{"level":23,"move_id":98},{"level":30,"move_id":44},{"level":36,"move_id":83},{"level":42,"move_id":123},{"level":47,"move_id":43},{"level":52,"move_id":53}]},"tmhm_learnset":"00021E24AC534630","types":[10,10]},{"abilities":[36,0],"address":3300580,"base_stats":[65,60,70,40,85,75],"catch_rate":45,"evolutions":[{"method":"ITEM","param":218,"species":233}],"friendship":70,"id":137,"learnset":{"address":3311620,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":176},{"level":1,"move_id":33},{"level":1,"move_id":160},{"level":9,"move_id":97},{"level":12,"move_id":60},{"level":20,"move_id":105},{"level":24,"move_id":159},{"level":32,"move_id":199},{"level":36,"move_id":161},{"level":44,"move_id":278},{"level":48,"move_id":192}]},"tmhm_learnset":"00402E82B5F37620","types":[0,0]},{"abilities":[33,75],"address":3300608,"base_stats":[35,40,100,35,90,55],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":40,"species":139}],"friendship":70,"id":138,"learnset":{"address":3311646,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":132},{"level":1,"move_id":110},{"level":13,"move_id":44},{"level":19,"move_id":55},{"level":25,"move_id":341},{"level":31,"move_id":43},{"level":37,"move_id":182},{"level":43,"move_id":321},{"level":49,"move_id":246},{"level":55,"move_id":56}]},"tmhm_learnset":"03903E5084133264","types":[5,11]},{"abilities":[33,75],"address":3300636,"base_stats":[70,60,125,55,115,70],"catch_rate":45,"evolutions":[],"friendship":70,"id":139,"learnset":{"address":3311672,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":132},{"level":1,"move_id":110},{"level":1,"move_id":44},{"level":13,"move_id":44},{"level":19,"move_id":55},{"level":25,"move_id":341},{"level":31,"move_id":43},{"level":37,"move_id":182},{"level":40,"move_id":131},{"level":46,"move_id":321},{"level":55,"move_id":246},{"level":65,"move_id":56}]},"tmhm_learnset":"03903E5084137264","types":[5,11]},{"abilities":[33,4],"address":3300664,"base_stats":[30,80,90,55,55,45],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":40,"species":141}],"friendship":70,"id":140,"learnset":{"address":3311700,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":106},{"level":13,"move_id":71},{"level":19,"move_id":43},{"level":25,"move_id":341},{"level":31,"move_id":28},{"level":37,"move_id":203},{"level":43,"move_id":319},{"level":49,"move_id":72},{"level":55,"move_id":246}]},"tmhm_learnset":"01903ED08C173264","types":[5,11]},{"abilities":[33,4],"address":3300692,"base_stats":[60,115,105,80,65,70],"catch_rate":45,"evolutions":[],"friendship":70,"id":141,"learnset":{"address":3311726,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":106},{"level":1,"move_id":71},{"level":13,"move_id":71},{"level":19,"move_id":43},{"level":25,"move_id":341},{"level":31,"move_id":28},{"level":37,"move_id":203},{"level":40,"move_id":163},{"level":46,"move_id":319},{"level":55,"move_id":72},{"level":65,"move_id":246}]},"tmhm_learnset":"03943ED0CC177264","types":[5,11]},{"abilities":[69,46],"address":3300720,"base_stats":[80,105,65,130,60,75],"catch_rate":45,"evolutions":[],"friendship":70,"id":142,"learnset":{"address":3311754,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":17},{"level":8,"move_id":97},{"level":15,"move_id":44},{"level":22,"move_id":48},{"level":29,"move_id":246},{"level":36,"move_id":184},{"level":43,"move_id":36},{"level":50,"move_id":63}]},"tmhm_learnset":"00A87FF486534E32","types":[5,2]},{"abilities":[17,47],"address":3300748,"base_stats":[160,110,65,30,65,110],"catch_rate":25,"evolutions":[],"friendship":70,"id":143,"learnset":{"address":3311778,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":6,"move_id":133},{"level":10,"move_id":111},{"level":15,"move_id":187},{"level":19,"move_id":29},{"level":24,"move_id":281},{"level":28,"move_id":156},{"level":28,"move_id":173},{"level":33,"move_id":34},{"level":37,"move_id":335},{"level":42,"move_id":343},{"level":46,"move_id":205},{"level":51,"move_id":63}]},"tmhm_learnset":"00301E76F7B37625","types":[0,0]},{"abilities":[46,0],"address":3300776,"base_stats":[90,85,100,85,95,125],"catch_rate":3,"evolutions":[],"friendship":35,"id":144,"learnset":{"address":3311812,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":16},{"level":1,"move_id":181},{"level":13,"move_id":54},{"level":25,"move_id":97},{"level":37,"move_id":170},{"level":49,"move_id":58},{"level":61,"move_id":115},{"level":73,"move_id":59},{"level":85,"move_id":329}]},"tmhm_learnset":"00884E9184137674","types":[15,2]},{"abilities":[46,0],"address":3300804,"base_stats":[90,90,85,100,125,90],"catch_rate":3,"evolutions":[],"friendship":35,"id":145,"learnset":{"address":3311836,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":64},{"level":1,"move_id":84},{"level":13,"move_id":86},{"level":25,"move_id":97},{"level":37,"move_id":197},{"level":49,"move_id":65},{"level":61,"move_id":268},{"level":73,"move_id":113},{"level":85,"move_id":87}]},"tmhm_learnset":"00C84E928593C630","types":[13,2]},{"abilities":[46,0],"address":3300832,"base_stats":[90,100,90,90,125,85],"catch_rate":3,"evolutions":[],"friendship":35,"id":146,"learnset":{"address":3311860,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":17},{"level":1,"move_id":52},{"level":13,"move_id":83},{"level":25,"move_id":97},{"level":37,"move_id":203},{"level":49,"move_id":53},{"level":61,"move_id":219},{"level":73,"move_id":257},{"level":85,"move_id":143}]},"tmhm_learnset":"008A4EB4841B4630","types":[10,2]},{"abilities":[61,0],"address":3300860,"base_stats":[41,64,45,50,50,50],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":30,"species":148}],"friendship":35,"id":147,"learnset":{"address":3311884,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":35},{"level":1,"move_id":43},{"level":8,"move_id":86},{"level":15,"move_id":239},{"level":22,"move_id":82},{"level":29,"move_id":21},{"level":36,"move_id":97},{"level":43,"move_id":219},{"level":50,"move_id":200},{"level":57,"move_id":63}]},"tmhm_learnset":"01101E2685DB7664","types":[16,16]},{"abilities":[61,0],"address":3300888,"base_stats":[61,84,65,70,70,70],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":55,"species":149}],"friendship":35,"id":148,"learnset":{"address":3311910,"moves":[{"level":1,"move_id":35},{"level":1,"move_id":43},{"level":1,"move_id":86},{"level":1,"move_id":239},{"level":8,"move_id":86},{"level":15,"move_id":239},{"level":22,"move_id":82},{"level":29,"move_id":21},{"level":38,"move_id":97},{"level":47,"move_id":219},{"level":56,"move_id":200},{"level":65,"move_id":63}]},"tmhm_learnset":"01101E2685DB7664","types":[16,16]},{"abilities":[39,0],"address":3300916,"base_stats":[91,134,95,80,100,100],"catch_rate":45,"evolutions":[],"friendship":35,"id":149,"learnset":{"address":3311936,"moves":[{"level":1,"move_id":35},{"level":1,"move_id":43},{"level":1,"move_id":86},{"level":1,"move_id":239},{"level":8,"move_id":86},{"level":15,"move_id":239},{"level":22,"move_id":82},{"level":29,"move_id":21},{"level":38,"move_id":97},{"level":47,"move_id":219},{"level":55,"move_id":17},{"level":61,"move_id":200},{"level":75,"move_id":63}]},"tmhm_learnset":"03BC5EF6C7DB7677","types":[16,2]},{"abilities":[46,0],"address":3300944,"base_stats":[106,110,90,130,154,90],"catch_rate":3,"evolutions":[],"friendship":0,"id":150,"learnset":{"address":3311964,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":93},{"level":1,"move_id":50},{"level":11,"move_id":112},{"level":22,"move_id":129},{"level":33,"move_id":244},{"level":44,"move_id":248},{"level":55,"move_id":54},{"level":66,"move_id":94},{"level":77,"move_id":133},{"level":88,"move_id":105},{"level":99,"move_id":219}]},"tmhm_learnset":"00E18FF7F7FBFEED","types":[14,14]},{"abilities":[28,0],"address":3300972,"base_stats":[100,100,100,100,100,100],"catch_rate":45,"evolutions":[],"friendship":100,"id":151,"learnset":{"address":3311992,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":1},{"level":10,"move_id":144},{"level":20,"move_id":5},{"level":30,"move_id":118},{"level":40,"move_id":94},{"level":50,"move_id":246}]},"tmhm_learnset":"03FFFFFFFFFFFFFF","types":[14,14]},{"abilities":[65,0],"address":3301000,"base_stats":[45,49,65,45,49,65],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":16,"species":153}],"friendship":70,"id":152,"learnset":{"address":3312012,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":8,"move_id":75},{"level":12,"move_id":115},{"level":15,"move_id":77},{"level":22,"move_id":235},{"level":29,"move_id":34},{"level":36,"move_id":113},{"level":43,"move_id":219},{"level":50,"move_id":76}]},"tmhm_learnset":"00441E01847D8720","types":[12,12]},{"abilities":[65,0],"address":3301028,"base_stats":[60,62,80,60,63,80],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":32,"species":154}],"friendship":70,"id":153,"learnset":{"address":3312038,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":1,"move_id":75},{"level":1,"move_id":115},{"level":8,"move_id":75},{"level":12,"move_id":115},{"level":15,"move_id":77},{"level":23,"move_id":235},{"level":31,"move_id":34},{"level":39,"move_id":113},{"level":47,"move_id":219},{"level":55,"move_id":76}]},"tmhm_learnset":"00E41E01847D8720","types":[12,12]},{"abilities":[65,0],"address":3301056,"base_stats":[80,82,100,80,83,100],"catch_rate":45,"evolutions":[],"friendship":70,"id":154,"learnset":{"address":3312064,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":1,"move_id":75},{"level":1,"move_id":115},{"level":8,"move_id":75},{"level":12,"move_id":115},{"level":15,"move_id":77},{"level":23,"move_id":235},{"level":31,"move_id":34},{"level":41,"move_id":113},{"level":51,"move_id":219},{"level":61,"move_id":76}]},"tmhm_learnset":"00E41E01867DC720","types":[12,12]},{"abilities":[66,0],"address":3301084,"base_stats":[39,52,43,65,60,50],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":14,"species":156}],"friendship":70,"id":155,"learnset":{"address":3312090,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":43},{"level":6,"move_id":108},{"level":12,"move_id":52},{"level":19,"move_id":98},{"level":27,"move_id":172},{"level":36,"move_id":129},{"level":46,"move_id":53}]},"tmhm_learnset":"00061EA48C110620","types":[10,10]},{"abilities":[66,0],"address":3301112,"base_stats":[58,64,58,80,80,65],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":36,"species":157}],"friendship":70,"id":156,"learnset":{"address":3312112,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":43},{"level":1,"move_id":108},{"level":6,"move_id":108},{"level":12,"move_id":52},{"level":21,"move_id":98},{"level":31,"move_id":172},{"level":42,"move_id":129},{"level":54,"move_id":53}]},"tmhm_learnset":"00A61EA4CC110631","types":[10,10]},{"abilities":[66,0],"address":3301140,"base_stats":[78,84,78,100,109,85],"catch_rate":45,"evolutions":[],"friendship":70,"id":157,"learnset":{"address":3312134,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":43},{"level":1,"move_id":108},{"level":1,"move_id":52},{"level":6,"move_id":108},{"level":12,"move_id":52},{"level":21,"move_id":98},{"level":31,"move_id":172},{"level":45,"move_id":129},{"level":60,"move_id":53}]},"tmhm_learnset":"00A61EA4CE114631","types":[10,10]},{"abilities":[67,0],"address":3301168,"base_stats":[50,65,64,43,44,48],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":18,"species":159}],"friendship":70,"id":158,"learnset":{"address":3312156,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":43},{"level":7,"move_id":99},{"level":13,"move_id":55},{"level":20,"move_id":44},{"level":27,"move_id":184},{"level":35,"move_id":163},{"level":43,"move_id":103},{"level":52,"move_id":56}]},"tmhm_learnset":"03141E80CC533265","types":[11,11]},{"abilities":[67,0],"address":3301196,"base_stats":[65,80,80,58,59,63],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":30,"species":160}],"friendship":70,"id":159,"learnset":{"address":3312180,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":43},{"level":1,"move_id":99},{"level":7,"move_id":99},{"level":13,"move_id":55},{"level":21,"move_id":44},{"level":28,"move_id":184},{"level":37,"move_id":163},{"level":45,"move_id":103},{"level":55,"move_id":56}]},"tmhm_learnset":"03B41E80CC533275","types":[11,11]},{"abilities":[67,0],"address":3301224,"base_stats":[85,105,100,78,79,83],"catch_rate":45,"evolutions":[],"friendship":70,"id":160,"learnset":{"address":3312204,"moves":[{"level":1,"move_id":10},{"level":1,"move_id":43},{"level":1,"move_id":99},{"level":1,"move_id":55},{"level":7,"move_id":99},{"level":13,"move_id":55},{"level":21,"move_id":44},{"level":28,"move_id":184},{"level":38,"move_id":163},{"level":47,"move_id":103},{"level":58,"move_id":56}]},"tmhm_learnset":"03B41E80CE537277","types":[11,11]},{"abilities":[50,51],"address":3301252,"base_stats":[35,46,34,20,35,45],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":15,"species":162}],"friendship":70,"id":161,"learnset":{"address":3312228,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":4,"move_id":111},{"level":7,"move_id":98},{"level":12,"move_id":154},{"level":17,"move_id":270},{"level":24,"move_id":21},{"level":31,"move_id":266},{"level":40,"move_id":156},{"level":49,"move_id":133}]},"tmhm_learnset":"00143E06ECF31625","types":[0,0]},{"abilities":[50,51],"address":3301280,"base_stats":[85,76,64,90,45,55],"catch_rate":90,"evolutions":[],"friendship":70,"id":162,"learnset":{"address":3312254,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":111},{"level":1,"move_id":98},{"level":4,"move_id":111},{"level":7,"move_id":98},{"level":12,"move_id":154},{"level":19,"move_id":270},{"level":28,"move_id":21},{"level":37,"move_id":266},{"level":48,"move_id":156},{"level":59,"move_id":133}]},"tmhm_learnset":"00B43E06EDF37625","types":[0,0]},{"abilities":[15,51],"address":3301308,"base_stats":[60,30,30,50,36,56],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":20,"species":164}],"friendship":70,"id":163,"learnset":{"address":3312280,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":6,"move_id":193},{"level":11,"move_id":64},{"level":16,"move_id":95},{"level":22,"move_id":115},{"level":28,"move_id":36},{"level":34,"move_id":93},{"level":48,"move_id":138}]},"tmhm_learnset":"00487E81B4130620","types":[0,2]},{"abilities":[15,51],"address":3301336,"base_stats":[100,50,50,70,76,96],"catch_rate":90,"evolutions":[],"friendship":70,"id":164,"learnset":{"address":3312304,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":1,"move_id":193},{"level":1,"move_id":64},{"level":6,"move_id":193},{"level":11,"move_id":64},{"level":16,"move_id":95},{"level":25,"move_id":115},{"level":33,"move_id":36},{"level":41,"move_id":93},{"level":57,"move_id":138}]},"tmhm_learnset":"00487E81B4134620","types":[0,2]},{"abilities":[68,48],"address":3301364,"base_stats":[40,20,30,55,40,80],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":18,"species":166}],"friendship":70,"id":165,"learnset":{"address":3312328,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":8,"move_id":48},{"level":15,"move_id":4},{"level":22,"move_id":113},{"level":22,"move_id":115},{"level":22,"move_id":219},{"level":29,"move_id":226},{"level":36,"move_id":129},{"level":43,"move_id":97},{"level":50,"move_id":38}]},"tmhm_learnset":"00403E81CC3D8621","types":[6,2]},{"abilities":[68,48],"address":3301392,"base_stats":[55,35,50,85,55,110],"catch_rate":90,"evolutions":[],"friendship":70,"id":166,"learnset":{"address":3312356,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":48},{"level":8,"move_id":48},{"level":15,"move_id":4},{"level":24,"move_id":113},{"level":24,"move_id":115},{"level":24,"move_id":219},{"level":33,"move_id":226},{"level":42,"move_id":129},{"level":51,"move_id":97},{"level":60,"move_id":38}]},"tmhm_learnset":"00403E81CC3DC621","types":[6,2]},{"abilities":[68,15],"address":3301420,"base_stats":[40,60,40,30,40,40],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":22,"species":168}],"friendship":70,"id":167,"learnset":{"address":3312384,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":40},{"level":1,"move_id":81},{"level":6,"move_id":184},{"level":11,"move_id":132},{"level":17,"move_id":101},{"level":23,"move_id":141},{"level":30,"move_id":154},{"level":37,"move_id":169},{"level":45,"move_id":97},{"level":53,"move_id":94}]},"tmhm_learnset":"00403E089C350620","types":[6,3]},{"abilities":[68,15],"address":3301448,"base_stats":[70,90,70,40,60,60],"catch_rate":90,"evolutions":[],"friendship":70,"id":168,"learnset":{"address":3312410,"moves":[{"level":1,"move_id":40},{"level":1,"move_id":81},{"level":1,"move_id":184},{"level":1,"move_id":132},{"level":6,"move_id":184},{"level":11,"move_id":132},{"level":17,"move_id":101},{"level":25,"move_id":141},{"level":34,"move_id":154},{"level":43,"move_id":169},{"level":53,"move_id":97},{"level":63,"move_id":94}]},"tmhm_learnset":"00403E089C354620","types":[6,3]},{"abilities":[39,0],"address":3301476,"base_stats":[85,90,80,130,70,80],"catch_rate":90,"evolutions":[],"friendship":70,"id":169,"learnset":{"address":3312436,"moves":[{"level":1,"move_id":103},{"level":1,"move_id":141},{"level":1,"move_id":48},{"level":1,"move_id":310},{"level":6,"move_id":48},{"level":11,"move_id":310},{"level":16,"move_id":44},{"level":21,"move_id":17},{"level":28,"move_id":109},{"level":35,"move_id":314},{"level":42,"move_id":212},{"level":49,"move_id":305},{"level":56,"move_id":114}]},"tmhm_learnset":"00097F88A4174E20","types":[3,2]},{"abilities":[10,35],"address":3301504,"base_stats":[75,38,38,67,56,56],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":27,"species":171}],"friendship":70,"id":170,"learnset":{"address":3312464,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":145},{"level":1,"move_id":86},{"level":5,"move_id":48},{"level":13,"move_id":175},{"level":17,"move_id":55},{"level":25,"move_id":209},{"level":29,"move_id":109},{"level":37,"move_id":36},{"level":41,"move_id":56},{"level":49,"move_id":268}]},"tmhm_learnset":"03501E0285933264","types":[11,13]},{"abilities":[10,35],"address":3301532,"base_stats":[125,58,58,67,76,76],"catch_rate":75,"evolutions":[],"friendship":70,"id":171,"learnset":{"address":3312490,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":145},{"level":1,"move_id":86},{"level":1,"move_id":48},{"level":5,"move_id":48},{"level":13,"move_id":175},{"level":17,"move_id":55},{"level":25,"move_id":209},{"level":32,"move_id":109},{"level":43,"move_id":36},{"level":50,"move_id":56},{"level":61,"move_id":268}]},"tmhm_learnset":"03501E0285937264","types":[11,13]},{"abilities":[9,0],"address":3301560,"base_stats":[20,40,15,60,35,35],"catch_rate":190,"evolutions":[{"method":"FRIENDSHIP","param":0,"species":25}],"friendship":70,"id":172,"learnset":{"address":3312516,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":84},{"level":1,"move_id":204},{"level":6,"move_id":39},{"level":8,"move_id":86},{"level":11,"move_id":186}]},"tmhm_learnset":"00401E0285D38220","types":[13,13]},{"abilities":[56,0],"address":3301588,"base_stats":[50,25,28,15,45,55],"catch_rate":150,"evolutions":[{"method":"FRIENDSHIP","param":0,"species":35}],"friendship":140,"id":173,"learnset":{"address":3312532,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":1},{"level":1,"move_id":204},{"level":4,"move_id":227},{"level":8,"move_id":47},{"level":13,"move_id":186}]},"tmhm_learnset":"00401E27BC7B8624","types":[0,0]},{"abilities":[56,0],"address":3301616,"base_stats":[90,30,15,15,40,20],"catch_rate":170,"evolutions":[{"method":"FRIENDSHIP","param":0,"species":39}],"friendship":70,"id":174,"learnset":{"address":3312548,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":1},{"level":1,"move_id":47},{"level":1,"move_id":204},{"level":4,"move_id":111},{"level":9,"move_id":1},{"level":14,"move_id":186}]},"tmhm_learnset":"00401E27BC3B8624","types":[0,0]},{"abilities":[55,32],"address":3301644,"base_stats":[35,20,65,20,40,65],"catch_rate":190,"evolutions":[{"method":"FRIENDSHIP","param":0,"species":176}],"friendship":70,"id":175,"learnset":{"address":3312564,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":118},{"level":1,"move_id":45},{"level":1,"move_id":204},{"level":6,"move_id":118},{"level":11,"move_id":186},{"level":16,"move_id":281},{"level":21,"move_id":227},{"level":26,"move_id":266},{"level":31,"move_id":273},{"level":36,"move_id":219},{"level":41,"move_id":38}]},"tmhm_learnset":"00C01E27B43B8624","types":[0,0]},{"abilities":[55,32],"address":3301672,"base_stats":[55,40,85,40,80,105],"catch_rate":75,"evolutions":[],"friendship":70,"id":176,"learnset":{"address":3312590,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":118},{"level":1,"move_id":45},{"level":1,"move_id":204},{"level":6,"move_id":118},{"level":11,"move_id":186},{"level":16,"move_id":281},{"level":21,"move_id":227},{"level":26,"move_id":266},{"level":31,"move_id":273},{"level":36,"move_id":219},{"level":41,"move_id":38}]},"tmhm_learnset":"00C85EA7F43BC625","types":[0,2]},{"abilities":[28,48],"address":3301700,"base_stats":[40,50,45,70,70,45],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":25,"species":178}],"friendship":70,"id":177,"learnset":{"address":3312616,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":64},{"level":1,"move_id":43},{"level":10,"move_id":101},{"level":20,"move_id":100},{"level":30,"move_id":273},{"level":30,"move_id":248},{"level":40,"move_id":109},{"level":50,"move_id":94}]},"tmhm_learnset":"0040FE81B4378628","types":[14,2]},{"abilities":[28,48],"address":3301728,"base_stats":[65,75,70,95,95,70],"catch_rate":75,"evolutions":[],"friendship":70,"id":178,"learnset":{"address":3312638,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":64},{"level":1,"move_id":43},{"level":10,"move_id":101},{"level":20,"move_id":100},{"level":35,"move_id":273},{"level":35,"move_id":248},{"level":50,"move_id":109},{"level":65,"move_id":94}]},"tmhm_learnset":"0048FE81B437C628","types":[14,2]},{"abilities":[9,0],"address":3301756,"base_stats":[55,40,40,35,65,45],"catch_rate":235,"evolutions":[{"method":"LEVEL","param":15,"species":180}],"friendship":70,"id":179,"learnset":{"address":3312660,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":9,"move_id":84},{"level":16,"move_id":86},{"level":23,"move_id":178},{"level":30,"move_id":113},{"level":37,"move_id":87}]},"tmhm_learnset":"00401E0285D38220","types":[13,13]},{"abilities":[9,0],"address":3301784,"base_stats":[70,55,55,45,80,60],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":30,"species":181}],"friendship":70,"id":180,"learnset":{"address":3312680,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":1,"move_id":84},{"level":9,"move_id":84},{"level":18,"move_id":86},{"level":27,"move_id":178},{"level":36,"move_id":113},{"level":45,"move_id":87}]},"tmhm_learnset":"00E01E02C5D38221","types":[13,13]},{"abilities":[9,0],"address":3301812,"base_stats":[90,75,75,55,115,90],"catch_rate":45,"evolutions":[],"friendship":70,"id":181,"learnset":{"address":3312700,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":1,"move_id":84},{"level":1,"move_id":86},{"level":9,"move_id":84},{"level":18,"move_id":86},{"level":27,"move_id":178},{"level":30,"move_id":9},{"level":42,"move_id":113},{"level":57,"move_id":87}]},"tmhm_learnset":"00E01E02C5D3C221","types":[13,13]},{"abilities":[34,0],"address":3301840,"base_stats":[75,80,85,50,90,100],"catch_rate":45,"evolutions":[],"friendship":70,"id":182,"learnset":{"address":3312722,"moves":[{"level":1,"move_id":71},{"level":1,"move_id":230},{"level":1,"move_id":78},{"level":1,"move_id":345},{"level":44,"move_id":80},{"level":55,"move_id":76}]},"tmhm_learnset":"00441E08843D4720","types":[12,12]},{"abilities":[47,37],"address":3301868,"base_stats":[70,20,50,40,20,50],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":18,"species":184}],"friendship":70,"id":183,"learnset":{"address":3312736,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":3,"move_id":111},{"level":6,"move_id":39},{"level":10,"move_id":55},{"level":15,"move_id":205},{"level":21,"move_id":61},{"level":28,"move_id":38},{"level":36,"move_id":240},{"level":45,"move_id":56}]},"tmhm_learnset":"03B01E00CC533265","types":[11,11]},{"abilities":[47,37],"address":3301896,"base_stats":[100,50,80,50,50,80],"catch_rate":75,"evolutions":[],"friendship":70,"id":184,"learnset":{"address":3312762,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":111},{"level":1,"move_id":39},{"level":1,"move_id":55},{"level":3,"move_id":111},{"level":6,"move_id":39},{"level":10,"move_id":55},{"level":15,"move_id":205},{"level":24,"move_id":61},{"level":34,"move_id":38},{"level":45,"move_id":240},{"level":57,"move_id":56}]},"tmhm_learnset":"03B01E00CC537265","types":[11,11]},{"abilities":[5,69],"address":3301924,"base_stats":[70,100,115,30,30,65],"catch_rate":65,"evolutions":[],"friendship":70,"id":185,"learnset":{"address":3312788,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":88},{"level":1,"move_id":102},{"level":9,"move_id":175},{"level":17,"move_id":67},{"level":25,"move_id":157},{"level":33,"move_id":335},{"level":41,"move_id":185},{"level":49,"move_id":21},{"level":57,"move_id":38}]},"tmhm_learnset":"00A03E50CE110E29","types":[5,5]},{"abilities":[11,6],"address":3301952,"base_stats":[90,75,75,70,90,100],"catch_rate":45,"evolutions":[],"friendship":70,"id":186,"learnset":{"address":3312812,"moves":[{"level":1,"move_id":55},{"level":1,"move_id":95},{"level":1,"move_id":3},{"level":1,"move_id":195},{"level":35,"move_id":195},{"level":51,"move_id":207}]},"tmhm_learnset":"03B03E00DE137265","types":[11,11]},{"abilities":[34,0],"address":3301980,"base_stats":[35,35,40,50,35,55],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":18,"species":188}],"friendship":70,"id":187,"learnset":{"address":3312826,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":150},{"level":5,"move_id":235},{"level":5,"move_id":39},{"level":10,"move_id":33},{"level":13,"move_id":77},{"level":15,"move_id":78},{"level":17,"move_id":79},{"level":20,"move_id":73},{"level":25,"move_id":178},{"level":30,"move_id":72}]},"tmhm_learnset":"00401E8084350720","types":[12,2]},{"abilities":[34,0],"address":3302008,"base_stats":[55,45,50,80,45,65],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":27,"species":189}],"friendship":70,"id":188,"learnset":{"address":3312854,"moves":[{"level":1,"move_id":150},{"level":1,"move_id":235},{"level":1,"move_id":39},{"level":1,"move_id":33},{"level":5,"move_id":235},{"level":5,"move_id":39},{"level":10,"move_id":33},{"level":13,"move_id":77},{"level":15,"move_id":78},{"level":17,"move_id":79},{"level":22,"move_id":73},{"level":29,"move_id":178},{"level":36,"move_id":72}]},"tmhm_learnset":"00401E8084350720","types":[12,2]},{"abilities":[34,0],"address":3302036,"base_stats":[75,55,70,110,55,85],"catch_rate":45,"evolutions":[],"friendship":70,"id":189,"learnset":{"address":3312882,"moves":[{"level":1,"move_id":150},{"level":1,"move_id":235},{"level":1,"move_id":39},{"level":1,"move_id":33},{"level":5,"move_id":235},{"level":5,"move_id":39},{"level":10,"move_id":33},{"level":13,"move_id":77},{"level":15,"move_id":78},{"level":17,"move_id":79},{"level":22,"move_id":73},{"level":33,"move_id":178},{"level":44,"move_id":72}]},"tmhm_learnset":"00401E8084354720","types":[12,2]},{"abilities":[50,53],"address":3302064,"base_stats":[55,70,55,85,40,55],"catch_rate":45,"evolutions":[],"friendship":70,"id":190,"learnset":{"address":3312910,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":39},{"level":6,"move_id":28},{"level":13,"move_id":310},{"level":18,"move_id":226},{"level":25,"move_id":321},{"level":31,"move_id":154},{"level":38,"move_id":129},{"level":43,"move_id":103},{"level":50,"move_id":97}]},"tmhm_learnset":"00A53E82EDF30E25","types":[0,0]},{"abilities":[34,0],"address":3302092,"base_stats":[30,30,30,30,30,30],"catch_rate":235,"evolutions":[{"method":"ITEM","param":93,"species":192}],"friendship":70,"id":191,"learnset":{"address":3312936,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":71},{"level":6,"move_id":74},{"level":13,"move_id":72},{"level":18,"move_id":275},{"level":25,"move_id":283},{"level":30,"move_id":241},{"level":37,"move_id":235},{"level":42,"move_id":202}]},"tmhm_learnset":"00441E08843D8720","types":[12,12]},{"abilities":[34,0],"address":3302120,"base_stats":[75,75,55,30,105,85],"catch_rate":120,"evolutions":[],"friendship":70,"id":192,"learnset":{"address":3312960,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":71},{"level":1,"move_id":1},{"level":6,"move_id":74},{"level":13,"move_id":75},{"level":18,"move_id":275},{"level":25,"move_id":331},{"level":30,"move_id":241},{"level":37,"move_id":80},{"level":42,"move_id":76}]},"tmhm_learnset":"00441E08843DC720","types":[12,12]},{"abilities":[3,14],"address":3302148,"base_stats":[65,65,45,95,75,45],"catch_rate":75,"evolutions":[],"friendship":70,"id":193,"learnset":{"address":3312984,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":193},{"level":7,"move_id":98},{"level":13,"move_id":104},{"level":19,"move_id":49},{"level":25,"move_id":197},{"level":31,"move_id":48},{"level":37,"move_id":253},{"level":43,"move_id":17},{"level":49,"move_id":103}]},"tmhm_learnset":"00407E80B4350620","types":[6,2]},{"abilities":[6,11],"address":3302176,"base_stats":[55,45,45,15,25,25],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":20,"species":195}],"friendship":70,"id":194,"learnset":{"address":3313010,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":55},{"level":1,"move_id":39},{"level":11,"move_id":21},{"level":16,"move_id":341},{"level":21,"move_id":133},{"level":31,"move_id":281},{"level":36,"move_id":89},{"level":41,"move_id":240},{"level":51,"move_id":54},{"level":51,"move_id":114}]},"tmhm_learnset":"03D01E188E533264","types":[11,4]},{"abilities":[6,11],"address":3302204,"base_stats":[95,85,85,35,65,65],"catch_rate":90,"evolutions":[],"friendship":70,"id":195,"learnset":{"address":3313036,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":55},{"level":1,"move_id":39},{"level":11,"move_id":21},{"level":16,"move_id":341},{"level":23,"move_id":133},{"level":35,"move_id":281},{"level":42,"move_id":89},{"level":49,"move_id":240},{"level":61,"move_id":54},{"level":61,"move_id":114}]},"tmhm_learnset":"03F01E58CE537265","types":[11,4]},{"abilities":[28,0],"address":3302232,"base_stats":[65,65,60,110,130,95],"catch_rate":45,"evolutions":[],"friendship":70,"id":196,"learnset":{"address":3313062,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":39},{"level":1,"move_id":270},{"level":8,"move_id":28},{"level":16,"move_id":93},{"level":23,"move_id":98},{"level":30,"move_id":129},{"level":36,"move_id":60},{"level":42,"move_id":244},{"level":47,"move_id":94},{"level":52,"move_id":234}]},"tmhm_learnset":"00449E01BC53C628","types":[14,14]},{"abilities":[28,0],"address":3302260,"base_stats":[95,65,110,65,60,130],"catch_rate":45,"evolutions":[],"friendship":35,"id":197,"learnset":{"address":3313088,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":39},{"level":1,"move_id":270},{"level":8,"move_id":28},{"level":16,"move_id":228},{"level":23,"move_id":98},{"level":30,"move_id":109},{"level":36,"move_id":185},{"level":42,"move_id":212},{"level":47,"move_id":103},{"level":52,"move_id":236}]},"tmhm_learnset":"00451F00BC534E20","types":[17,17]},{"abilities":[15,0],"address":3302288,"base_stats":[60,85,42,91,85,42],"catch_rate":30,"evolutions":[],"friendship":35,"id":198,"learnset":{"address":3313114,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":64},{"level":9,"move_id":310},{"level":14,"move_id":228},{"level":22,"move_id":114},{"level":27,"move_id":101},{"level":35,"move_id":185},{"level":40,"move_id":269},{"level":48,"move_id":212}]},"tmhm_learnset":"00097F80A4130E28","types":[17,2]},{"abilities":[12,20],"address":3302316,"base_stats":[95,75,80,30,100,110],"catch_rate":70,"evolutions":[],"friendship":70,"id":199,"learnset":{"address":3313138,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":174},{"level":1,"move_id":281},{"level":1,"move_id":33},{"level":6,"move_id":45},{"level":15,"move_id":55},{"level":20,"move_id":93},{"level":29,"move_id":50},{"level":34,"move_id":29},{"level":43,"move_id":207},{"level":48,"move_id":94}]},"tmhm_learnset":"02F09E24FE5B766D","types":[11,14]},{"abilities":[26,0],"address":3302344,"base_stats":[60,60,60,85,85,85],"catch_rate":45,"evolutions":[],"friendship":35,"id":200,"learnset":{"address":3313162,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":45},{"level":1,"move_id":149},{"level":6,"move_id":180},{"level":11,"move_id":310},{"level":17,"move_id":109},{"level":23,"move_id":212},{"level":30,"move_id":60},{"level":37,"move_id":220},{"level":45,"move_id":195},{"level":53,"move_id":288}]},"tmhm_learnset":"0041BF82B5930E28","types":[7,7]},{"abilities":[26,0],"address":3302372,"base_stats":[48,72,48,48,72,48],"catch_rate":225,"evolutions":[],"friendship":70,"id":201,"learnset":{"address":3313188,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":237}]},"tmhm_learnset":"0000000000000000","types":[14,14]},{"abilities":[23,0],"address":3302400,"base_stats":[190,33,58,33,33,58],"catch_rate":45,"evolutions":[],"friendship":70,"id":202,"learnset":{"address":3313198,"moves":[{"level":1,"move_id":68},{"level":1,"move_id":243},{"level":1,"move_id":219},{"level":1,"move_id":194}]},"tmhm_learnset":"0000000000000000","types":[14,14]},{"abilities":[39,48],"address":3302428,"base_stats":[70,80,65,85,90,65],"catch_rate":60,"evolutions":[],"friendship":70,"id":203,"learnset":{"address":3313208,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":7,"move_id":310},{"level":13,"move_id":93},{"level":19,"move_id":23},{"level":25,"move_id":316},{"level":31,"move_id":97},{"level":37,"move_id":226},{"level":43,"move_id":60},{"level":49,"move_id":242}]},"tmhm_learnset":"00E0BE03B7D38628","types":[0,14]},{"abilities":[5,0],"address":3302456,"base_stats":[50,65,90,15,35,35],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":31,"species":205}],"friendship":70,"id":204,"learnset":{"address":3313234,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":182},{"level":8,"move_id":120},{"level":15,"move_id":36},{"level":22,"move_id":229},{"level":29,"move_id":117},{"level":36,"move_id":153},{"level":43,"move_id":191},{"level":50,"move_id":38}]},"tmhm_learnset":"00A01E118E358620","types":[6,6]},{"abilities":[5,0],"address":3302484,"base_stats":[75,90,140,40,60,60],"catch_rate":75,"evolutions":[],"friendship":70,"id":205,"learnset":{"address":3313258,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":182},{"level":1,"move_id":120},{"level":8,"move_id":120},{"level":15,"move_id":36},{"level":22,"move_id":229},{"level":29,"move_id":117},{"level":39,"move_id":153},{"level":49,"move_id":191},{"level":59,"move_id":38}]},"tmhm_learnset":"00A01E118E35C620","types":[6,8]},{"abilities":[32,50],"address":3302512,"base_stats":[100,70,70,45,65,65],"catch_rate":190,"evolutions":[],"friendship":70,"id":206,"learnset":{"address":3313282,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":99},{"level":4,"move_id":111},{"level":11,"move_id":281},{"level":14,"move_id":137},{"level":21,"move_id":180},{"level":24,"move_id":228},{"level":31,"move_id":103},{"level":34,"move_id":36},{"level":41,"move_id":283}]},"tmhm_learnset":"00A03E66AFF3362C","types":[0,0]},{"abilities":[52,8],"address":3302540,"base_stats":[65,75,105,85,35,65],"catch_rate":60,"evolutions":[],"friendship":70,"id":207,"learnset":{"address":3313308,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":40},{"level":6,"move_id":28},{"level":13,"move_id":106},{"level":20,"move_id":98},{"level":28,"move_id":185},{"level":36,"move_id":163},{"level":44,"move_id":103},{"level":52,"move_id":12}]},"tmhm_learnset":"00A47ED88E530620","types":[4,2]},{"abilities":[69,5],"address":3302568,"base_stats":[75,85,200,30,55,65],"catch_rate":25,"evolutions":[],"friendship":70,"id":208,"learnset":{"address":3313332,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":103},{"level":9,"move_id":20},{"level":13,"move_id":88},{"level":21,"move_id":106},{"level":25,"move_id":99},{"level":33,"move_id":201},{"level":37,"move_id":21},{"level":45,"move_id":231},{"level":49,"move_id":242},{"level":57,"move_id":38}]},"tmhm_learnset":"00A41F508E514E30","types":[8,4]},{"abilities":[22,50],"address":3302596,"base_stats":[60,80,50,30,40,40],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":23,"species":210}],"friendship":70,"id":209,"learnset":{"address":3313360,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":184},{"level":4,"move_id":39},{"level":8,"move_id":204},{"level":13,"move_id":44},{"level":19,"move_id":122},{"level":26,"move_id":46},{"level":34,"move_id":99},{"level":43,"move_id":36},{"level":53,"move_id":242}]},"tmhm_learnset":"00A23F2EEFB30EB5","types":[0,0]},{"abilities":[22,22],"address":3302624,"base_stats":[90,120,75,45,60,60],"catch_rate":75,"evolutions":[],"friendship":70,"id":210,"learnset":{"address":3313386,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":184},{"level":4,"move_id":39},{"level":8,"move_id":204},{"level":13,"move_id":44},{"level":19,"move_id":122},{"level":28,"move_id":46},{"level":38,"move_id":99},{"level":49,"move_id":36},{"level":61,"move_id":242}]},"tmhm_learnset":"00A23F6EEFF34EB5","types":[0,0]},{"abilities":[38,33],"address":3302652,"base_stats":[65,95,75,85,55,55],"catch_rate":45,"evolutions":[],"friendship":70,"id":211,"learnset":{"address":3313412,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":191},{"level":1,"move_id":33},{"level":1,"move_id":40},{"level":10,"move_id":106},{"level":10,"move_id":107},{"level":19,"move_id":55},{"level":28,"move_id":42},{"level":37,"move_id":36},{"level":46,"move_id":56}]},"tmhm_learnset":"03101E0AA4133264","types":[11,3]},{"abilities":[68,0],"address":3302680,"base_stats":[70,130,100,65,55,80],"catch_rate":25,"evolutions":[],"friendship":70,"id":212,"learnset":{"address":3313434,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":98},{"level":1,"move_id":43},{"level":6,"move_id":116},{"level":11,"move_id":228},{"level":16,"move_id":206},{"level":21,"move_id":97},{"level":26,"move_id":232},{"level":31,"move_id":163},{"level":36,"move_id":14},{"level":41,"move_id":104},{"level":46,"move_id":210}]},"tmhm_learnset":"00A47E9084134620","types":[6,8]},{"abilities":[5,0],"address":3302708,"base_stats":[20,10,230,5,10,230],"catch_rate":190,"evolutions":[],"friendship":70,"id":213,"learnset":{"address":3313462,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":132},{"level":1,"move_id":110},{"level":9,"move_id":35},{"level":14,"move_id":227},{"level":23,"move_id":219},{"level":28,"move_id":117},{"level":37,"move_id":156}]},"tmhm_learnset":"00E01E588E190620","types":[6,5]},{"abilities":[68,62],"address":3302736,"base_stats":[80,125,75,85,40,95],"catch_rate":45,"evolutions":[],"friendship":70,"id":214,"learnset":{"address":3313482,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":43},{"level":6,"move_id":30},{"level":11,"move_id":203},{"level":17,"move_id":31},{"level":23,"move_id":280},{"level":30,"move_id":68},{"level":37,"move_id":36},{"level":45,"move_id":179},{"level":53,"move_id":224}]},"tmhm_learnset":"00A43E40CE1346A1","types":[6,1]},{"abilities":[39,51],"address":3302764,"base_stats":[55,95,55,115,35,75],"catch_rate":60,"evolutions":[],"friendship":35,"id":215,"learnset":{"address":3313508,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":43},{"level":1,"move_id":269},{"level":8,"move_id":98},{"level":15,"move_id":103},{"level":22,"move_id":185},{"level":29,"move_id":154},{"level":36,"move_id":97},{"level":43,"move_id":196},{"level":50,"move_id":163},{"level":57,"move_id":251},{"level":64,"move_id":232}]},"tmhm_learnset":"00B53F80EC533E69","types":[17,15]},{"abilities":[53,0],"address":3302792,"base_stats":[60,80,50,40,50,50],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":30,"species":217}],"friendship":70,"id":216,"learnset":{"address":3313536,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":43},{"level":7,"move_id":122},{"level":13,"move_id":154},{"level":19,"move_id":313},{"level":25,"move_id":185},{"level":31,"move_id":156},{"level":37,"move_id":163},{"level":43,"move_id":173},{"level":49,"move_id":37}]},"tmhm_learnset":"00A43F80CE130EB1","types":[0,0]},{"abilities":[62,0],"address":3302820,"base_stats":[90,130,75,55,75,75],"catch_rate":60,"evolutions":[],"friendship":70,"id":217,"learnset":{"address":3313562,"moves":[{"level":1,"move_id":10},{"level":1,"move_id":43},{"level":1,"move_id":122},{"level":1,"move_id":154},{"level":7,"move_id":122},{"level":13,"move_id":154},{"level":19,"move_id":313},{"level":25,"move_id":185},{"level":31,"move_id":156},{"level":37,"move_id":163},{"level":43,"move_id":173},{"level":49,"move_id":37}]},"tmhm_learnset":"00A43FC0CE134EB1","types":[0,0]},{"abilities":[40,49],"address":3302848,"base_stats":[40,40,40,20,70,40],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":38,"species":219}],"friendship":70,"id":218,"learnset":{"address":3313588,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":281},{"level":1,"move_id":123},{"level":8,"move_id":52},{"level":15,"move_id":88},{"level":22,"move_id":106},{"level":29,"move_id":133},{"level":36,"move_id":53},{"level":43,"move_id":157},{"level":50,"move_id":34}]},"tmhm_learnset":"00821E2584118620","types":[10,10]},{"abilities":[40,49],"address":3302876,"base_stats":[50,50,120,30,80,80],"catch_rate":75,"evolutions":[],"friendship":70,"id":219,"learnset":{"address":3313612,"moves":[{"level":1,"move_id":281},{"level":1,"move_id":123},{"level":1,"move_id":52},{"level":1,"move_id":88},{"level":8,"move_id":52},{"level":15,"move_id":88},{"level":22,"move_id":106},{"level":29,"move_id":133},{"level":36,"move_id":53},{"level":48,"move_id":157},{"level":60,"move_id":34}]},"tmhm_learnset":"00A21E758611C620","types":[10,5]},{"abilities":[12,0],"address":3302904,"base_stats":[50,50,40,50,30,30],"catch_rate":225,"evolutions":[{"method":"LEVEL","param":33,"species":221}],"friendship":70,"id":220,"learnset":{"address":3313636,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":316},{"level":10,"move_id":181},{"level":19,"move_id":203},{"level":28,"move_id":36},{"level":37,"move_id":54},{"level":46,"move_id":59},{"level":55,"move_id":133}]},"tmhm_learnset":"00A01E518E13B270","types":[15,4]},{"abilities":[12,0],"address":3302932,"base_stats":[100,100,80,50,60,60],"catch_rate":75,"evolutions":[],"friendship":70,"id":221,"learnset":{"address":3313658,"moves":[{"level":1,"move_id":30},{"level":1,"move_id":316},{"level":1,"move_id":181},{"level":1,"move_id":203},{"level":10,"move_id":181},{"level":19,"move_id":203},{"level":28,"move_id":36},{"level":33,"move_id":31},{"level":42,"move_id":54},{"level":56,"move_id":59},{"level":70,"move_id":133}]},"tmhm_learnset":"00A01E518E13F270","types":[15,4]},{"abilities":[55,30],"address":3302960,"base_stats":[55,55,85,35,65,85],"catch_rate":60,"evolutions":[],"friendship":70,"id":222,"learnset":{"address":3313682,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":6,"move_id":106},{"level":12,"move_id":145},{"level":17,"move_id":105},{"level":17,"move_id":287},{"level":23,"move_id":61},{"level":28,"move_id":131},{"level":34,"move_id":350},{"level":39,"move_id":243},{"level":45,"move_id":246}]},"tmhm_learnset":"00B01E51BE1BB66C","types":[11,5]},{"abilities":[55,0],"address":3302988,"base_stats":[35,65,35,65,65,35],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":25,"species":224}],"friendship":70,"id":223,"learnset":{"address":3313710,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":55},{"level":11,"move_id":199},{"level":22,"move_id":60},{"level":22,"move_id":62},{"level":22,"move_id":61},{"level":33,"move_id":116},{"level":44,"move_id":58},{"level":55,"move_id":63}]},"tmhm_learnset":"03103E2494137624","types":[11,11]},{"abilities":[21,0],"address":3303016,"base_stats":[75,105,75,45,105,75],"catch_rate":75,"evolutions":[],"friendship":70,"id":224,"learnset":{"address":3313734,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":55},{"level":11,"move_id":132},{"level":22,"move_id":60},{"level":22,"move_id":62},{"level":22,"move_id":61},{"level":25,"move_id":190},{"level":38,"move_id":116},{"level":54,"move_id":58},{"level":70,"move_id":63}]},"tmhm_learnset":"03103E2C94137724","types":[11,11]},{"abilities":[72,55],"address":3303044,"base_stats":[45,55,45,75,65,45],"catch_rate":45,"evolutions":[],"friendship":70,"id":225,"learnset":{"address":3313760,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":217}]},"tmhm_learnset":"00083E8084133265","types":[15,2]},{"abilities":[33,11],"address":3303072,"base_stats":[65,40,70,70,80,140],"catch_rate":25,"evolutions":[],"friendship":70,"id":226,"learnset":{"address":3313770,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":145},{"level":8,"move_id":48},{"level":15,"move_id":61},{"level":22,"move_id":36},{"level":29,"move_id":97},{"level":36,"move_id":17},{"level":43,"move_id":352},{"level":50,"move_id":109}]},"tmhm_learnset":"03101E8086133264","types":[11,2]},{"abilities":[51,5],"address":3303100,"base_stats":[65,80,140,70,40,70],"catch_rate":25,"evolutions":[],"friendship":70,"id":227,"learnset":{"address":3313794,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":43},{"level":1,"move_id":64},{"level":10,"move_id":28},{"level":13,"move_id":129},{"level":16,"move_id":97},{"level":26,"move_id":31},{"level":29,"move_id":314},{"level":32,"move_id":211},{"level":42,"move_id":191},{"level":45,"move_id":319}]},"tmhm_learnset":"008C7F9084110E30","types":[8,2]},{"abilities":[48,18],"address":3303128,"base_stats":[45,60,30,65,80,50],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":24,"species":229}],"friendship":35,"id":228,"learnset":{"address":3313820,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":43},{"level":1,"move_id":52},{"level":7,"move_id":336},{"level":13,"move_id":123},{"level":19,"move_id":46},{"level":25,"move_id":44},{"level":31,"move_id":316},{"level":37,"move_id":185},{"level":43,"move_id":53},{"level":49,"move_id":242}]},"tmhm_learnset":"00833F2CA4710E30","types":[17,10]},{"abilities":[48,18],"address":3303156,"base_stats":[75,90,50,95,110,80],"catch_rate":45,"evolutions":[],"friendship":35,"id":229,"learnset":{"address":3313846,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":43},{"level":1,"move_id":52},{"level":1,"move_id":336},{"level":7,"move_id":336},{"level":13,"move_id":123},{"level":19,"move_id":46},{"level":27,"move_id":44},{"level":35,"move_id":316},{"level":43,"move_id":185},{"level":51,"move_id":53},{"level":59,"move_id":242}]},"tmhm_learnset":"00A33F2CA4714E30","types":[17,10]},{"abilities":[33,0],"address":3303184,"base_stats":[75,95,95,85,95,95],"catch_rate":45,"evolutions":[],"friendship":70,"id":230,"learnset":{"address":3313872,"moves":[{"level":1,"move_id":145},{"level":1,"move_id":108},{"level":1,"move_id":43},{"level":1,"move_id":55},{"level":8,"move_id":108},{"level":15,"move_id":43},{"level":22,"move_id":55},{"level":29,"move_id":239},{"level":40,"move_id":97},{"level":51,"move_id":56},{"level":62,"move_id":349}]},"tmhm_learnset":"03101E0084137264","types":[11,16]},{"abilities":[53,0],"address":3303212,"base_stats":[90,60,60,40,40,40],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":25,"species":232}],"friendship":70,"id":231,"learnset":{"address":3313896,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":316},{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":9,"move_id":111},{"level":17,"move_id":175},{"level":25,"move_id":36},{"level":33,"move_id":205},{"level":41,"move_id":203},{"level":49,"move_id":38}]},"tmhm_learnset":"00A01E5086510630","types":[4,4]},{"abilities":[5,0],"address":3303240,"base_stats":[90,120,120,50,60,60],"catch_rate":60,"evolutions":[],"friendship":70,"id":232,"learnset":{"address":3313918,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":316},{"level":1,"move_id":30},{"level":1,"move_id":45},{"level":9,"move_id":111},{"level":17,"move_id":175},{"level":25,"move_id":31},{"level":33,"move_id":205},{"level":41,"move_id":229},{"level":49,"move_id":89}]},"tmhm_learnset":"00A01E5086514630","types":[4,4]},{"abilities":[36,0],"address":3303268,"base_stats":[85,80,90,60,105,95],"catch_rate":45,"evolutions":[],"friendship":70,"id":233,"learnset":{"address":3313940,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":176},{"level":1,"move_id":33},{"level":1,"move_id":160},{"level":9,"move_id":97},{"level":12,"move_id":60},{"level":20,"move_id":105},{"level":24,"move_id":111},{"level":32,"move_id":199},{"level":36,"move_id":161},{"level":44,"move_id":278},{"level":48,"move_id":192}]},"tmhm_learnset":"00402E82B5F37620","types":[0,0]},{"abilities":[22,0],"address":3303296,"base_stats":[73,95,62,85,85,65],"catch_rate":45,"evolutions":[],"friendship":70,"id":234,"learnset":{"address":3313966,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":7,"move_id":43},{"level":13,"move_id":310},{"level":19,"move_id":95},{"level":25,"move_id":23},{"level":31,"move_id":28},{"level":37,"move_id":36},{"level":43,"move_id":109},{"level":49,"move_id":347}]},"tmhm_learnset":"0040BE03B7F38638","types":[0,0]},{"abilities":[20,0],"address":3303324,"base_stats":[55,20,35,75,20,45],"catch_rate":45,"evolutions":[],"friendship":70,"id":235,"learnset":{"address":3313992,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":166},{"level":11,"move_id":166},{"level":21,"move_id":166},{"level":31,"move_id":166},{"level":41,"move_id":166},{"level":51,"move_id":166},{"level":61,"move_id":166},{"level":71,"move_id":166},{"level":81,"move_id":166},{"level":91,"move_id":166}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[62,0],"address":3303352,"base_stats":[35,35,35,35,35,35],"catch_rate":75,"evolutions":[{"method":"LEVEL_ATK_LT_DEF","param":20,"species":107},{"method":"LEVEL_ATK_GT_DEF","param":20,"species":106},{"method":"LEVEL_ATK_EQ_DEF","param":20,"species":237}],"friendship":70,"id":236,"learnset":{"address":3314020,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"00A03E00C61306A0","types":[1,1]},{"abilities":[22,0],"address":3303380,"base_stats":[50,95,95,70,35,110],"catch_rate":45,"evolutions":[],"friendship":70,"id":237,"learnset":{"address":3314030,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":279},{"level":1,"move_id":27},{"level":7,"move_id":116},{"level":13,"move_id":228},{"level":19,"move_id":98},{"level":20,"move_id":167},{"level":25,"move_id":229},{"level":31,"move_id":68},{"level":37,"move_id":97},{"level":43,"move_id":197},{"level":49,"move_id":283}]},"tmhm_learnset":"00A03E10CE1306A0","types":[1,1]},{"abilities":[12,0],"address":3303408,"base_stats":[45,30,15,65,85,65],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":30,"species":124}],"friendship":70,"id":238,"learnset":{"address":3314058,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":1},{"level":1,"move_id":122},{"level":9,"move_id":186},{"level":13,"move_id":181},{"level":21,"move_id":93},{"level":25,"move_id":47},{"level":33,"move_id":212},{"level":37,"move_id":313},{"level":45,"move_id":94},{"level":49,"move_id":195},{"level":57,"move_id":59}]},"tmhm_learnset":"0040BE01B413B26C","types":[15,14]},{"abilities":[9,0],"address":3303436,"base_stats":[45,63,37,95,65,55],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":30,"species":125}],"friendship":70,"id":239,"learnset":{"address":3314086,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":98},{"level":1,"move_id":43},{"level":9,"move_id":9},{"level":17,"move_id":113},{"level":25,"move_id":129},{"level":33,"move_id":103},{"level":41,"move_id":85},{"level":49,"move_id":87}]},"tmhm_learnset":"00C03E02D5938221","types":[13,13]},{"abilities":[49,0],"address":3303464,"base_stats":[45,75,37,83,70,55],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":30,"species":126}],"friendship":70,"id":240,"learnset":{"address":3314108,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":52},{"level":7,"move_id":43},{"level":13,"move_id":123},{"level":19,"move_id":7},{"level":25,"move_id":108},{"level":31,"move_id":241},{"level":37,"move_id":53},{"level":43,"move_id":109},{"level":49,"move_id":126}]},"tmhm_learnset":"00803E24D4510621","types":[10,10]},{"abilities":[47,0],"address":3303492,"base_stats":[95,80,105,100,40,70],"catch_rate":45,"evolutions":[],"friendship":70,"id":241,"learnset":{"address":3314134,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":4,"move_id":45},{"level":8,"move_id":111},{"level":13,"move_id":23},{"level":19,"move_id":208},{"level":26,"move_id":117},{"level":34,"move_id":205},{"level":43,"move_id":34},{"level":53,"move_id":215}]},"tmhm_learnset":"00B01E52E7F37625","types":[0,0]},{"abilities":[30,32],"address":3303520,"base_stats":[255,10,10,55,75,135],"catch_rate":30,"evolutions":[],"friendship":140,"id":242,"learnset":{"address":3314160,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":1},{"level":1,"move_id":45},{"level":4,"move_id":39},{"level":7,"move_id":287},{"level":10,"move_id":135},{"level":13,"move_id":3},{"level":18,"move_id":107},{"level":23,"move_id":47},{"level":28,"move_id":121},{"level":33,"move_id":111},{"level":40,"move_id":113},{"level":47,"move_id":38}]},"tmhm_learnset":"00E19E76F7FBF66D","types":[0,0]},{"abilities":[46,0],"address":3303548,"base_stats":[90,85,75,115,115,100],"catch_rate":3,"evolutions":[],"friendship":35,"id":243,"learnset":{"address":3314190,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":44},{"level":1,"move_id":43},{"level":11,"move_id":84},{"level":21,"move_id":46},{"level":31,"move_id":98},{"level":41,"move_id":209},{"level":51,"move_id":115},{"level":61,"move_id":242},{"level":71,"move_id":87},{"level":81,"move_id":347}]},"tmhm_learnset":"00E40E138DD34638","types":[13,13]},{"abilities":[46,0],"address":3303576,"base_stats":[115,115,85,100,90,75],"catch_rate":3,"evolutions":[],"friendship":35,"id":244,"learnset":{"address":3314216,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":44},{"level":1,"move_id":43},{"level":11,"move_id":52},{"level":21,"move_id":46},{"level":31,"move_id":83},{"level":41,"move_id":23},{"level":51,"move_id":53},{"level":61,"move_id":207},{"level":71,"move_id":126},{"level":81,"move_id":347}]},"tmhm_learnset":"00E40E358C734638","types":[10,10]},{"abilities":[46,0],"address":3303604,"base_stats":[100,75,115,85,90,115],"catch_rate":3,"evolutions":[],"friendship":35,"id":245,"learnset":{"address":3314242,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":44},{"level":1,"move_id":43},{"level":11,"move_id":61},{"level":21,"move_id":240},{"level":31,"move_id":16},{"level":41,"move_id":62},{"level":51,"move_id":54},{"level":61,"move_id":243},{"level":71,"move_id":56},{"level":81,"move_id":347}]},"tmhm_learnset":"03940E118C53767C","types":[11,11]},{"abilities":[62,0],"address":3303632,"base_stats":[50,64,50,41,45,50],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":30,"species":247}],"friendship":35,"id":246,"learnset":{"address":3314268,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":44},{"level":1,"move_id":43},{"level":8,"move_id":201},{"level":15,"move_id":103},{"level":22,"move_id":157},{"level":29,"move_id":37},{"level":36,"move_id":184},{"level":43,"move_id":242},{"level":50,"move_id":89},{"level":57,"move_id":63}]},"tmhm_learnset":"00801F10CE134E20","types":[5,4]},{"abilities":[61,0],"address":3303660,"base_stats":[70,84,70,51,65,70],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":55,"species":248}],"friendship":35,"id":247,"learnset":{"address":3314294,"moves":[{"level":1,"move_id":44},{"level":1,"move_id":43},{"level":1,"move_id":201},{"level":1,"move_id":103},{"level":8,"move_id":201},{"level":15,"move_id":103},{"level":22,"move_id":157},{"level":29,"move_id":37},{"level":38,"move_id":184},{"level":47,"move_id":242},{"level":56,"move_id":89},{"level":65,"move_id":63}]},"tmhm_learnset":"00801F10CE134E20","types":[5,4]},{"abilities":[45,0],"address":3303688,"base_stats":[100,134,110,61,95,100],"catch_rate":45,"evolutions":[],"friendship":35,"id":248,"learnset":{"address":3314320,"moves":[{"level":1,"move_id":44},{"level":1,"move_id":43},{"level":1,"move_id":201},{"level":1,"move_id":103},{"level":8,"move_id":201},{"level":15,"move_id":103},{"level":22,"move_id":157},{"level":29,"move_id":37},{"level":38,"move_id":184},{"level":47,"move_id":242},{"level":61,"move_id":89},{"level":75,"move_id":63}]},"tmhm_learnset":"00B41FF6CFD37E37","types":[5,17]},{"abilities":[46,0],"address":3303716,"base_stats":[106,90,130,110,90,154],"catch_rate":3,"evolutions":[],"friendship":0,"id":249,"learnset":{"address":3314346,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":16},{"level":1,"move_id":18},{"level":11,"move_id":219},{"level":22,"move_id":16},{"level":33,"move_id":105},{"level":44,"move_id":56},{"level":55,"move_id":240},{"level":66,"move_id":129},{"level":77,"move_id":177},{"level":88,"move_id":246},{"level":99,"move_id":248}]},"tmhm_learnset":"03B8CE93B7DFF67C","types":[14,2]},{"abilities":[46,0],"address":3303744,"base_stats":[106,130,90,90,110,154],"catch_rate":3,"evolutions":[],"friendship":0,"id":250,"learnset":{"address":3314374,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":18},{"level":11,"move_id":219},{"level":22,"move_id":16},{"level":33,"move_id":105},{"level":44,"move_id":126},{"level":55,"move_id":241},{"level":66,"move_id":129},{"level":77,"move_id":221},{"level":88,"move_id":246},{"level":99,"move_id":248}]},"tmhm_learnset":"00EA4EB7B7BFC638","types":[10,2]},{"abilities":[30,0],"address":3303772,"base_stats":[100,100,100,100,100,100],"catch_rate":45,"evolutions":[],"friendship":100,"id":251,"learnset":{"address":3314402,"moves":[{"level":1,"move_id":73},{"level":1,"move_id":93},{"level":1,"move_id":105},{"level":1,"move_id":215},{"level":10,"move_id":219},{"level":20,"move_id":246},{"level":30,"move_id":248},{"level":40,"move_id":226},{"level":50,"move_id":195}]},"tmhm_learnset":"00448E93B43FC62C","types":[14,12]},{"abilities":[0,0],"address":3303800,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":252,"learnset":{"address":3314422,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3303828,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":253,"learnset":{"address":3314432,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3303856,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":254,"learnset":{"address":3314442,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3303884,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":255,"learnset":{"address":3314452,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3303912,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":256,"learnset":{"address":3314462,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3303940,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":257,"learnset":{"address":3314472,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3303968,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":258,"learnset":{"address":3314482,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3303996,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":259,"learnset":{"address":3314492,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304024,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":260,"learnset":{"address":3314502,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304052,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":261,"learnset":{"address":3314512,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304080,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":262,"learnset":{"address":3314522,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304108,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":263,"learnset":{"address":3314532,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304136,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":264,"learnset":{"address":3314542,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304164,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":265,"learnset":{"address":3314552,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304192,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":266,"learnset":{"address":3314562,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304220,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":267,"learnset":{"address":3314572,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304248,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":268,"learnset":{"address":3314582,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304276,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":269,"learnset":{"address":3314592,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304304,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":270,"learnset":{"address":3314602,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304332,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":271,"learnset":{"address":3314612,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304360,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":272,"learnset":{"address":3314622,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304388,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":273,"learnset":{"address":3314632,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304416,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":274,"learnset":{"address":3314642,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304444,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":275,"learnset":{"address":3314652,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[0,0],"address":3304472,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":276,"learnset":{"address":3314662,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33}]},"tmhm_learnset":"0000000000000000","types":[0,0]},{"abilities":[65,0],"address":3304500,"base_stats":[40,45,35,70,65,55],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":16,"species":278}],"friendship":70,"id":277,"learnset":{"address":3314672,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":1},{"level":1,"move_id":43},{"level":6,"move_id":71},{"level":11,"move_id":98},{"level":16,"move_id":228},{"level":21,"move_id":103},{"level":26,"move_id":72},{"level":31,"move_id":97},{"level":36,"move_id":21},{"level":41,"move_id":197},{"level":46,"move_id":202}]},"tmhm_learnset":"00E41EC0CC7D0721","types":[12,12]},{"abilities":[65,0],"address":3304528,"base_stats":[50,65,45,95,85,65],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":36,"species":279}],"friendship":70,"id":278,"learnset":{"address":3314700,"moves":[{"level":1,"move_id":1},{"level":1,"move_id":43},{"level":1,"move_id":71},{"level":1,"move_id":98},{"level":6,"move_id":71},{"level":11,"move_id":98},{"level":16,"move_id":210},{"level":17,"move_id":228},{"level":23,"move_id":103},{"level":29,"move_id":348},{"level":35,"move_id":97},{"level":41,"move_id":21},{"level":47,"move_id":197},{"level":53,"move_id":206}]},"tmhm_learnset":"00E41EC0CC7D0721","types":[12,12]},{"abilities":[65,0],"address":3304556,"base_stats":[70,85,65,120,105,85],"catch_rate":45,"evolutions":[],"friendship":70,"id":279,"learnset":{"address":3314730,"moves":[{"level":1,"move_id":1},{"level":1,"move_id":43},{"level":1,"move_id":71},{"level":1,"move_id":98},{"level":6,"move_id":71},{"level":11,"move_id":98},{"level":16,"move_id":210},{"level":17,"move_id":228},{"level":23,"move_id":103},{"level":29,"move_id":348},{"level":35,"move_id":97},{"level":43,"move_id":21},{"level":51,"move_id":197},{"level":59,"move_id":206}]},"tmhm_learnset":"00E41EC0CE7D4733","types":[12,12]},{"abilities":[66,0],"address":3304584,"base_stats":[45,60,40,45,70,50],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":16,"species":281}],"friendship":70,"id":280,"learnset":{"address":3314760,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":45},{"level":7,"move_id":116},{"level":10,"move_id":52},{"level":16,"move_id":64},{"level":19,"move_id":28},{"level":25,"move_id":83},{"level":28,"move_id":98},{"level":34,"move_id":163},{"level":37,"move_id":119},{"level":43,"move_id":53}]},"tmhm_learnset":"00A61EE48C110620","types":[10,10]},{"abilities":[66,0],"address":3304612,"base_stats":[60,85,60,55,85,60],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":36,"species":282}],"friendship":70,"id":281,"learnset":{"address":3314788,"moves":[{"level":1,"move_id":10},{"level":1,"move_id":45},{"level":1,"move_id":116},{"level":1,"move_id":52},{"level":7,"move_id":116},{"level":13,"move_id":52},{"level":16,"move_id":24},{"level":17,"move_id":64},{"level":21,"move_id":28},{"level":28,"move_id":339},{"level":32,"move_id":98},{"level":39,"move_id":163},{"level":43,"move_id":119},{"level":50,"move_id":327}]},"tmhm_learnset":"00A61EE4CC1106A1","types":[10,1]},{"abilities":[66,0],"address":3304640,"base_stats":[80,120,70,80,110,70],"catch_rate":45,"evolutions":[],"friendship":70,"id":282,"learnset":{"address":3314818,"moves":[{"level":1,"move_id":7},{"level":1,"move_id":10},{"level":1,"move_id":45},{"level":1,"move_id":116},{"level":1,"move_id":52},{"level":7,"move_id":116},{"level":13,"move_id":52},{"level":16,"move_id":24},{"level":17,"move_id":64},{"level":21,"move_id":28},{"level":28,"move_id":339},{"level":32,"move_id":98},{"level":36,"move_id":299},{"level":42,"move_id":163},{"level":49,"move_id":119},{"level":59,"move_id":327}]},"tmhm_learnset":"00A61EE4CE1146B1","types":[10,1]},{"abilities":[67,0],"address":3304668,"base_stats":[50,70,50,40,50,50],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":16,"species":284}],"friendship":70,"id":283,"learnset":{"address":3314852,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":6,"move_id":189},{"level":10,"move_id":55},{"level":15,"move_id":117},{"level":19,"move_id":193},{"level":24,"move_id":300},{"level":28,"move_id":36},{"level":33,"move_id":250},{"level":37,"move_id":182},{"level":42,"move_id":56},{"level":46,"move_id":283}]},"tmhm_learnset":"03B01E408C533264","types":[11,11]},{"abilities":[67,0],"address":3304696,"base_stats":[70,85,70,50,60,70],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":36,"species":285}],"friendship":70,"id":284,"learnset":{"address":3314882,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":1,"move_id":189},{"level":1,"move_id":55},{"level":6,"move_id":189},{"level":10,"move_id":55},{"level":15,"move_id":117},{"level":16,"move_id":341},{"level":20,"move_id":193},{"level":25,"move_id":300},{"level":31,"move_id":36},{"level":37,"move_id":330},{"level":42,"move_id":182},{"level":46,"move_id":89},{"level":53,"move_id":283}]},"tmhm_learnset":"03B01E408E533264","types":[11,4]},{"abilities":[67,0],"address":3304724,"base_stats":[100,110,90,60,85,90],"catch_rate":45,"evolutions":[],"friendship":70,"id":285,"learnset":{"address":3314914,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":1,"move_id":189},{"level":1,"move_id":55},{"level":6,"move_id":189},{"level":10,"move_id":55},{"level":15,"move_id":117},{"level":16,"move_id":341},{"level":20,"move_id":193},{"level":25,"move_id":300},{"level":31,"move_id":36},{"level":39,"move_id":330},{"level":46,"move_id":182},{"level":52,"move_id":89},{"level":61,"move_id":283}]},"tmhm_learnset":"03B01E40CE537275","types":[11,4]},{"abilities":[50,0],"address":3304752,"base_stats":[35,55,35,35,30,30],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":18,"species":287}],"friendship":70,"id":286,"learnset":{"address":3314946,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":5,"move_id":336},{"level":9,"move_id":28},{"level":13,"move_id":44},{"level":17,"move_id":316},{"level":21,"move_id":46},{"level":25,"move_id":207},{"level":29,"move_id":184},{"level":33,"move_id":36},{"level":37,"move_id":269},{"level":41,"move_id":242},{"level":45,"move_id":168}]},"tmhm_learnset":"00813F00AC530E30","types":[17,17]},{"abilities":[22,0],"address":3304780,"base_stats":[70,90,70,70,60,60],"catch_rate":127,"evolutions":[],"friendship":70,"id":287,"learnset":{"address":3314978,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":336},{"level":1,"move_id":28},{"level":1,"move_id":44},{"level":5,"move_id":336},{"level":9,"move_id":28},{"level":13,"move_id":44},{"level":17,"move_id":316},{"level":22,"move_id":46},{"level":27,"move_id":207},{"level":32,"move_id":184},{"level":37,"move_id":36},{"level":42,"move_id":269},{"level":47,"move_id":242},{"level":52,"move_id":168}]},"tmhm_learnset":"00A13F00AC534E30","types":[17,17]},{"abilities":[53,0],"address":3304808,"base_stats":[38,30,41,60,30,41],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":20,"species":289}],"friendship":70,"id":288,"learnset":{"address":3315010,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":5,"move_id":39},{"level":9,"move_id":29},{"level":13,"move_id":28},{"level":17,"move_id":316},{"level":21,"move_id":300},{"level":25,"move_id":42},{"level":29,"move_id":343},{"level":33,"move_id":175},{"level":37,"move_id":156},{"level":41,"move_id":187}]},"tmhm_learnset":"00943E02ADD33624","types":[0,0]},{"abilities":[53,0],"address":3304836,"base_stats":[78,70,61,100,50,61],"catch_rate":90,"evolutions":[],"friendship":70,"id":289,"learnset":{"address":3315040,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":45},{"level":1,"move_id":39},{"level":1,"move_id":29},{"level":5,"move_id":39},{"level":9,"move_id":29},{"level":13,"move_id":28},{"level":17,"move_id":316},{"level":23,"move_id":300},{"level":29,"move_id":154},{"level":35,"move_id":343},{"level":41,"move_id":163},{"level":47,"move_id":156},{"level":53,"move_id":187}]},"tmhm_learnset":"00B43E02ADD37634","types":[0,0]},{"abilities":[19,0],"address":3304864,"base_stats":[45,45,35,20,20,30],"catch_rate":255,"evolutions":[{"method":"LEVEL_SILCOON","param":7,"species":291},{"method":"LEVEL_CASCOON","param":7,"species":293}],"friendship":70,"id":290,"learnset":{"address":3315070,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":81},{"level":5,"move_id":40}]},"tmhm_learnset":"0000000000000000","types":[6,6]},{"abilities":[61,0],"address":3304892,"base_stats":[50,35,55,15,25,25],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":10,"species":292}],"friendship":70,"id":291,"learnset":{"address":3315082,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":106},{"level":7,"move_id":106}]},"tmhm_learnset":"0000000000000000","types":[6,6]},{"abilities":[68,0],"address":3304920,"base_stats":[60,70,50,65,90,50],"catch_rate":45,"evolutions":[],"friendship":70,"id":292,"learnset":{"address":3315094,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":71},{"level":10,"move_id":71},{"level":13,"move_id":16},{"level":17,"move_id":78},{"level":20,"move_id":234},{"level":24,"move_id":72},{"level":27,"move_id":18},{"level":31,"move_id":213},{"level":34,"move_id":318},{"level":38,"move_id":202}]},"tmhm_learnset":"00403E80B43D4620","types":[6,2]},{"abilities":[61,0],"address":3304948,"base_stats":[50,35,55,15,25,25],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":10,"species":294}],"friendship":70,"id":293,"learnset":{"address":3315122,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":106},{"level":7,"move_id":106}]},"tmhm_learnset":"0000000000000000","types":[6,6]},{"abilities":[19,0],"address":3304976,"base_stats":[60,50,70,65,50,90],"catch_rate":45,"evolutions":[],"friendship":70,"id":294,"learnset":{"address":3315134,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":93},{"level":10,"move_id":93},{"level":13,"move_id":16},{"level":17,"move_id":182},{"level":20,"move_id":236},{"level":24,"move_id":60},{"level":27,"move_id":18},{"level":31,"move_id":113},{"level":34,"move_id":318},{"level":38,"move_id":92}]},"tmhm_learnset":"00403E88B435C620","types":[6,3]},{"abilities":[33,44],"address":3305004,"base_stats":[40,30,30,30,40,50],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":14,"species":296}],"friendship":70,"id":295,"learnset":{"address":3315162,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":310},{"level":3,"move_id":45},{"level":7,"move_id":71},{"level":13,"move_id":267},{"level":21,"move_id":54},{"level":31,"move_id":240},{"level":43,"move_id":72}]},"tmhm_learnset":"00503E0084373764","types":[11,12]},{"abilities":[33,44],"address":3305032,"base_stats":[60,50,50,50,60,70],"catch_rate":120,"evolutions":[{"method":"ITEM","param":97,"species":297}],"friendship":70,"id":296,"learnset":{"address":3315184,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":310},{"level":3,"move_id":45},{"level":7,"move_id":71},{"level":13,"move_id":267},{"level":19,"move_id":252},{"level":25,"move_id":154},{"level":31,"move_id":346},{"level":37,"move_id":168},{"level":43,"move_id":253},{"level":49,"move_id":56}]},"tmhm_learnset":"03F03E00C4373764","types":[11,12]},{"abilities":[33,44],"address":3305060,"base_stats":[80,70,70,70,90,100],"catch_rate":45,"evolutions":[],"friendship":70,"id":297,"learnset":{"address":3315212,"moves":[{"level":1,"move_id":310},{"level":1,"move_id":45},{"level":1,"move_id":71},{"level":1,"move_id":267}]},"tmhm_learnset":"03F03E00C4377765","types":[11,12]},{"abilities":[34,48],"address":3305088,"base_stats":[40,40,50,30,30,30],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":14,"species":299}],"friendship":70,"id":298,"learnset":{"address":3315222,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":117},{"level":3,"move_id":106},{"level":7,"move_id":74},{"level":13,"move_id":267},{"level":21,"move_id":235},{"level":31,"move_id":241},{"level":43,"move_id":153}]},"tmhm_learnset":"00C01E00AC350720","types":[12,12]},{"abilities":[34,48],"address":3305116,"base_stats":[70,70,40,60,60,40],"catch_rate":120,"evolutions":[{"method":"ITEM","param":98,"species":300}],"friendship":70,"id":299,"learnset":{"address":3315244,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":1},{"level":3,"move_id":106},{"level":7,"move_id":74},{"level":13,"move_id":267},{"level":19,"move_id":252},{"level":25,"move_id":259},{"level":31,"move_id":185},{"level":37,"move_id":13},{"level":43,"move_id":207},{"level":49,"move_id":326}]},"tmhm_learnset":"00E43F40EC354720","types":[12,17]},{"abilities":[34,48],"address":3305144,"base_stats":[90,100,60,80,90,60],"catch_rate":45,"evolutions":[],"friendship":70,"id":300,"learnset":{"address":3315272,"moves":[{"level":1,"move_id":1},{"level":1,"move_id":106},{"level":1,"move_id":74},{"level":1,"move_id":267}]},"tmhm_learnset":"00E43FC0EC354720","types":[12,17]},{"abilities":[14,0],"address":3305172,"base_stats":[31,45,90,40,30,30],"catch_rate":255,"evolutions":[{"method":"LEVEL_NINJASK","param":20,"species":302},{"method":"LEVEL_SHEDINJA","param":20,"species":303}],"friendship":70,"id":301,"learnset":{"address":3315282,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":106},{"level":5,"move_id":141},{"level":9,"move_id":28},{"level":14,"move_id":154},{"level":19,"move_id":170},{"level":25,"move_id":206},{"level":31,"move_id":189},{"level":38,"move_id":232},{"level":45,"move_id":91}]},"tmhm_learnset":"00440E90AC350620","types":[6,4]},{"abilities":[3,0],"address":3305200,"base_stats":[61,90,45,160,50,50],"catch_rate":120,"evolutions":[],"friendship":70,"id":302,"learnset":{"address":3315308,"moves":[{"level":1,"move_id":10},{"level":1,"move_id":106},{"level":1,"move_id":141},{"level":1,"move_id":28},{"level":5,"move_id":141},{"level":9,"move_id":28},{"level":14,"move_id":154},{"level":19,"move_id":170},{"level":20,"move_id":104},{"level":20,"move_id":210},{"level":20,"move_id":103},{"level":25,"move_id":14},{"level":31,"move_id":163},{"level":38,"move_id":97},{"level":45,"move_id":226}]},"tmhm_learnset":"00443E90AC354620","types":[6,2]},{"abilities":[25,0],"address":3305228,"base_stats":[1,90,45,40,30,30],"catch_rate":45,"evolutions":[],"friendship":70,"id":303,"learnset":{"address":3315340,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":106},{"level":5,"move_id":141},{"level":9,"move_id":28},{"level":14,"move_id":154},{"level":19,"move_id":170},{"level":25,"move_id":180},{"level":31,"move_id":109},{"level":38,"move_id":247},{"level":45,"move_id":288}]},"tmhm_learnset":"00442E90AC354620","types":[6,7]},{"abilities":[62,0],"address":3305256,"base_stats":[40,55,30,85,30,30],"catch_rate":200,"evolutions":[{"method":"LEVEL","param":22,"species":305}],"friendship":70,"id":304,"learnset":{"address":3315366,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":64},{"level":1,"move_id":45},{"level":4,"move_id":116},{"level":8,"move_id":98},{"level":13,"move_id":17},{"level":19,"move_id":104},{"level":26,"move_id":283},{"level":34,"move_id":332},{"level":43,"move_id":97}]},"tmhm_learnset":"00087E8084130620","types":[0,2]},{"abilities":[62,0],"address":3305284,"base_stats":[60,85,60,125,50,50],"catch_rate":45,"evolutions":[],"friendship":70,"id":305,"learnset":{"address":3315390,"moves":[{"level":1,"move_id":64},{"level":1,"move_id":45},{"level":1,"move_id":116},{"level":1,"move_id":98},{"level":4,"move_id":116},{"level":8,"move_id":98},{"level":13,"move_id":17},{"level":19,"move_id":104},{"level":28,"move_id":283},{"level":38,"move_id":332},{"level":49,"move_id":97}]},"tmhm_learnset":"00087E8084134620","types":[0,2]},{"abilities":[27,0],"address":3305312,"base_stats":[60,40,60,35,40,60],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":23,"species":307}],"friendship":70,"id":306,"learnset":{"address":3315414,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":71},{"level":4,"move_id":33},{"level":7,"move_id":78},{"level":10,"move_id":73},{"level":16,"move_id":72},{"level":22,"move_id":29},{"level":28,"move_id":77},{"level":36,"move_id":74},{"level":45,"move_id":202},{"level":54,"move_id":147}]},"tmhm_learnset":"00411E08843D0720","types":[12,12]},{"abilities":[27,0],"address":3305340,"base_stats":[60,130,80,70,60,60],"catch_rate":90,"evolutions":[],"friendship":70,"id":307,"learnset":{"address":3315442,"moves":[{"level":1,"move_id":71},{"level":1,"move_id":33},{"level":1,"move_id":78},{"level":1,"move_id":73},{"level":4,"move_id":33},{"level":7,"move_id":78},{"level":10,"move_id":73},{"level":16,"move_id":72},{"level":22,"move_id":29},{"level":23,"move_id":183},{"level":28,"move_id":68},{"level":36,"move_id":327},{"level":45,"move_id":170},{"level":54,"move_id":223}]},"tmhm_learnset":"00E51E08C47D47A1","types":[12,1]},{"abilities":[20,0],"address":3305368,"base_stats":[60,60,60,60,60,60],"catch_rate":255,"evolutions":[],"friendship":70,"id":308,"learnset":{"address":3315472,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":5,"move_id":253},{"level":12,"move_id":185},{"level":16,"move_id":60},{"level":23,"move_id":95},{"level":27,"move_id":146},{"level":34,"move_id":298},{"level":38,"move_id":244},{"level":45,"move_id":38},{"level":49,"move_id":175},{"level":56,"move_id":37}]},"tmhm_learnset":"00E1BE42FC1B062D","types":[0,0]},{"abilities":[51,0],"address":3305396,"base_stats":[40,30,30,85,55,30],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":25,"species":310}],"friendship":70,"id":309,"learnset":{"address":3315502,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":45},{"level":1,"move_id":55},{"level":7,"move_id":48},{"level":13,"move_id":17},{"level":21,"move_id":54},{"level":31,"move_id":98},{"level":43,"move_id":228},{"level":55,"move_id":97}]},"tmhm_learnset":"00087E8284133264","types":[11,2]},{"abilities":[51,0],"address":3305424,"base_stats":[60,50,100,65,85,70],"catch_rate":45,"evolutions":[],"friendship":70,"id":310,"learnset":{"address":3315524,"moves":[{"level":1,"move_id":45},{"level":1,"move_id":55},{"level":1,"move_id":346},{"level":1,"move_id":17},{"level":3,"move_id":55},{"level":7,"move_id":48},{"level":13,"move_id":17},{"level":21,"move_id":54},{"level":25,"move_id":182},{"level":33,"move_id":254},{"level":33,"move_id":256},{"level":47,"move_id":255},{"level":61,"move_id":56}]},"tmhm_learnset":"00187E8284137264","types":[11,2]},{"abilities":[33,0],"address":3305452,"base_stats":[40,30,32,65,50,52],"catch_rate":200,"evolutions":[{"method":"LEVEL","param":22,"species":312}],"friendship":70,"id":311,"learnset":{"address":3315552,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":145},{"level":7,"move_id":98},{"level":13,"move_id":230},{"level":19,"move_id":346},{"level":25,"move_id":61},{"level":31,"move_id":97},{"level":37,"move_id":54},{"level":37,"move_id":114}]},"tmhm_learnset":"00403E00A4373624","types":[6,11]},{"abilities":[22,0],"address":3305480,"base_stats":[70,60,62,60,80,82],"catch_rate":75,"evolutions":[],"friendship":70,"id":312,"learnset":{"address":3315576,"moves":[{"level":1,"move_id":145},{"level":1,"move_id":98},{"level":1,"move_id":230},{"level":1,"move_id":346},{"level":7,"move_id":98},{"level":13,"move_id":230},{"level":19,"move_id":346},{"level":26,"move_id":16},{"level":33,"move_id":184},{"level":40,"move_id":78},{"level":47,"move_id":318},{"level":53,"move_id":18}]},"tmhm_learnset":"00403E80A4377624","types":[6,2]},{"abilities":[41,12],"address":3305508,"base_stats":[130,70,35,60,70,35],"catch_rate":125,"evolutions":[{"method":"LEVEL","param":40,"species":314}],"friendship":70,"id":313,"learnset":{"address":3315602,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":55},{"level":1,"move_id":150},{"level":5,"move_id":45},{"level":10,"move_id":55},{"level":14,"move_id":205},{"level":19,"move_id":250},{"level":23,"move_id":310},{"level":28,"move_id":352},{"level":32,"move_id":54},{"level":37,"move_id":156},{"level":41,"move_id":323},{"level":46,"move_id":133},{"level":50,"move_id":56}]},"tmhm_learnset":"03B01E4086133274","types":[11,11]},{"abilities":[41,12],"address":3305536,"base_stats":[170,90,45,60,90,45],"catch_rate":60,"evolutions":[],"friendship":70,"id":314,"learnset":{"address":3315634,"moves":[{"level":1,"move_id":150},{"level":1,"move_id":45},{"level":1,"move_id":55},{"level":1,"move_id":205},{"level":5,"move_id":45},{"level":10,"move_id":55},{"level":14,"move_id":205},{"level":19,"move_id":250},{"level":23,"move_id":310},{"level":28,"move_id":352},{"level":32,"move_id":54},{"level":37,"move_id":156},{"level":44,"move_id":323},{"level":52,"move_id":133},{"level":59,"move_id":56}]},"tmhm_learnset":"03B01E4086137274","types":[11,11]},{"abilities":[56,0],"address":3305564,"base_stats":[50,45,45,50,35,35],"catch_rate":255,"evolutions":[{"method":"ITEM","param":94,"species":316}],"friendship":70,"id":315,"learnset":{"address":3315666,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":45},{"level":1,"move_id":33},{"level":3,"move_id":39},{"level":7,"move_id":213},{"level":13,"move_id":47},{"level":15,"move_id":3},{"level":19,"move_id":274},{"level":25,"move_id":204},{"level":27,"move_id":185},{"level":31,"move_id":343},{"level":37,"move_id":215},{"level":39,"move_id":38}]},"tmhm_learnset":"00401E02ADFB362C","types":[0,0]},{"abilities":[56,0],"address":3305592,"base_stats":[70,65,65,70,55,55],"catch_rate":60,"evolutions":[],"friendship":70,"id":316,"learnset":{"address":3315696,"moves":[{"level":1,"move_id":45},{"level":1,"move_id":213},{"level":1,"move_id":47},{"level":1,"move_id":3}]},"tmhm_learnset":"00E01E02ADFB762C","types":[0,0]},{"abilities":[16,0],"address":3305620,"base_stats":[60,90,70,40,60,120],"catch_rate":200,"evolutions":[],"friendship":70,"id":317,"learnset":{"address":3315706,"moves":[{"level":1,"move_id":168},{"level":1,"move_id":39},{"level":1,"move_id":310},{"level":1,"move_id":122},{"level":1,"move_id":10},{"level":4,"move_id":20},{"level":7,"move_id":185},{"level":12,"move_id":154},{"level":17,"move_id":60},{"level":24,"move_id":103},{"level":31,"move_id":163},{"level":40,"move_id":164},{"level":49,"move_id":246}]},"tmhm_learnset":"00E5BEE6EDF33625","types":[0,0]},{"abilities":[26,0],"address":3305648,"base_stats":[40,40,55,55,40,70],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":36,"species":319}],"friendship":70,"id":318,"learnset":{"address":3315734,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":93},{"level":3,"move_id":106},{"level":5,"move_id":229},{"level":7,"move_id":189},{"level":11,"move_id":60},{"level":15,"move_id":317},{"level":19,"move_id":120},{"level":25,"move_id":246},{"level":31,"move_id":201},{"level":37,"move_id":322},{"level":45,"move_id":153}]},"tmhm_learnset":"00408E51BE339620","types":[4,14]},{"abilities":[26,0],"address":3305676,"base_stats":[60,70,105,75,70,120],"catch_rate":90,"evolutions":[],"friendship":70,"id":319,"learnset":{"address":3315764,"moves":[{"level":1,"move_id":100},{"level":1,"move_id":93},{"level":1,"move_id":106},{"level":1,"move_id":229},{"level":3,"move_id":106},{"level":5,"move_id":229},{"level":7,"move_id":189},{"level":11,"move_id":60},{"level":15,"move_id":317},{"level":19,"move_id":120},{"level":25,"move_id":246},{"level":31,"move_id":201},{"level":36,"move_id":63},{"level":42,"move_id":322},{"level":55,"move_id":153}]},"tmhm_learnset":"00E08E51BE33D620","types":[4,14]},{"abilities":[5,42],"address":3305704,"base_stats":[30,45,135,30,45,90],"catch_rate":255,"evolutions":[],"friendship":70,"id":320,"learnset":{"address":3315796,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":7,"move_id":106},{"level":13,"move_id":88},{"level":16,"move_id":335},{"level":22,"move_id":86},{"level":28,"move_id":157},{"level":31,"move_id":201},{"level":37,"move_id":156},{"level":43,"move_id":192},{"level":46,"move_id":199}]},"tmhm_learnset":"00A01F5287910E20","types":[5,5]},{"abilities":[73,0],"address":3305732,"base_stats":[70,85,140,20,85,70],"catch_rate":90,"evolutions":[],"friendship":70,"id":321,"learnset":{"address":3315824,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":52},{"level":4,"move_id":123},{"level":7,"move_id":174},{"level":14,"move_id":108},{"level":17,"move_id":83},{"level":20,"move_id":34},{"level":27,"move_id":182},{"level":30,"move_id":53},{"level":33,"move_id":334},{"level":40,"move_id":133},{"level":43,"move_id":175},{"level":46,"move_id":257}]},"tmhm_learnset":"00A21E2C84510620","types":[10,10]},{"abilities":[51,0],"address":3305760,"base_stats":[50,75,75,50,65,65],"catch_rate":45,"evolutions":[],"friendship":35,"id":322,"learnset":{"address":3315856,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":43},{"level":1,"move_id":10},{"level":5,"move_id":193},{"level":9,"move_id":101},{"level":13,"move_id":310},{"level":17,"move_id":154},{"level":21,"move_id":252},{"level":25,"move_id":197},{"level":29,"move_id":185},{"level":33,"move_id":282},{"level":37,"move_id":109},{"level":41,"move_id":247},{"level":45,"move_id":212}]},"tmhm_learnset":"00C53FC2FC130E2D","types":[17,7]},{"abilities":[12,0],"address":3305788,"base_stats":[50,48,43,60,46,41],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":30,"species":324}],"friendship":70,"id":323,"learnset":{"address":3315888,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":189},{"level":6,"move_id":300},{"level":6,"move_id":346},{"level":11,"move_id":55},{"level":16,"move_id":222},{"level":21,"move_id":133},{"level":26,"move_id":156},{"level":26,"move_id":173},{"level":31,"move_id":89},{"level":36,"move_id":248},{"level":41,"move_id":90}]},"tmhm_learnset":"03101E5086133264","types":[11,4]},{"abilities":[12,0],"address":3305816,"base_stats":[110,78,73,60,76,71],"catch_rate":75,"evolutions":[],"friendship":70,"id":324,"learnset":{"address":3315918,"moves":[{"level":1,"move_id":321},{"level":1,"move_id":189},{"level":1,"move_id":300},{"level":1,"move_id":346},{"level":6,"move_id":300},{"level":6,"move_id":346},{"level":11,"move_id":55},{"level":16,"move_id":222},{"level":21,"move_id":133},{"level":26,"move_id":156},{"level":26,"move_id":173},{"level":36,"move_id":89},{"level":46,"move_id":248},{"level":56,"move_id":90}]},"tmhm_learnset":"03B01E5086137264","types":[11,4]},{"abilities":[33,0],"address":3305844,"base_stats":[43,30,55,97,40,65],"catch_rate":225,"evolutions":[],"friendship":70,"id":325,"learnset":{"address":3315948,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":4,"move_id":204},{"level":12,"move_id":55},{"level":16,"move_id":97},{"level":24,"move_id":36},{"level":28,"move_id":213},{"level":36,"move_id":186},{"level":40,"move_id":175},{"level":48,"move_id":219}]},"tmhm_learnset":"03101E00841B3264","types":[11,11]},{"abilities":[52,75],"address":3305872,"base_stats":[43,80,65,35,50,35],"catch_rate":205,"evolutions":[{"method":"LEVEL","param":30,"species":327}],"friendship":70,"id":326,"learnset":{"address":3315974,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":145},{"level":7,"move_id":106},{"level":10,"move_id":11},{"level":13,"move_id":43},{"level":20,"move_id":61},{"level":23,"move_id":182},{"level":26,"move_id":282},{"level":32,"move_id":269},{"level":35,"move_id":152},{"level":38,"move_id":14},{"level":44,"move_id":12}]},"tmhm_learnset":"01B41EC8CC133A64","types":[11,11]},{"abilities":[52,75],"address":3305900,"base_stats":[63,120,85,55,90,55],"catch_rate":155,"evolutions":[],"friendship":70,"id":327,"learnset":{"address":3316004,"moves":[{"level":1,"move_id":145},{"level":1,"move_id":106},{"level":1,"move_id":11},{"level":1,"move_id":43},{"level":7,"move_id":106},{"level":10,"move_id":11},{"level":13,"move_id":43},{"level":20,"move_id":61},{"level":23,"move_id":182},{"level":26,"move_id":282},{"level":34,"move_id":269},{"level":39,"move_id":152},{"level":44,"move_id":14},{"level":52,"move_id":12}]},"tmhm_learnset":"03B41EC8CC137A64","types":[11,17]},{"abilities":[33,0],"address":3305928,"base_stats":[20,15,20,80,10,55],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":30,"species":329}],"friendship":70,"id":328,"learnset":{"address":3316034,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":150},{"level":15,"move_id":33},{"level":30,"move_id":175}]},"tmhm_learnset":"03101E0084133264","types":[11,11]},{"abilities":[63,0],"address":3305956,"base_stats":[95,60,79,81,100,125],"catch_rate":60,"evolutions":[],"friendship":70,"id":329,"learnset":{"address":3316048,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":55},{"level":5,"move_id":35},{"level":10,"move_id":346},{"level":15,"move_id":287},{"level":20,"move_id":352},{"level":25,"move_id":239},{"level":30,"move_id":105},{"level":35,"move_id":240},{"level":40,"move_id":56},{"level":45,"move_id":213},{"level":50,"move_id":219}]},"tmhm_learnset":"03101E00845B7264","types":[11,11]},{"abilities":[24,0],"address":3305984,"base_stats":[45,90,20,65,65,20],"catch_rate":225,"evolutions":[{"method":"LEVEL","param":30,"species":331}],"friendship":35,"id":330,"learnset":{"address":3316078,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":43},{"level":1,"move_id":44},{"level":7,"move_id":99},{"level":13,"move_id":116},{"level":16,"move_id":184},{"level":22,"move_id":242},{"level":28,"move_id":103},{"level":31,"move_id":36},{"level":37,"move_id":207},{"level":43,"move_id":97}]},"tmhm_learnset":"03103F0084133A64","types":[11,17]},{"abilities":[24,0],"address":3306012,"base_stats":[70,120,40,95,95,40],"catch_rate":60,"evolutions":[],"friendship":35,"id":331,"learnset":{"address":3316104,"moves":[{"level":1,"move_id":43},{"level":1,"move_id":44},{"level":1,"move_id":99},{"level":1,"move_id":116},{"level":7,"move_id":99},{"level":13,"move_id":116},{"level":16,"move_id":184},{"level":22,"move_id":242},{"level":28,"move_id":103},{"level":33,"move_id":163},{"level":38,"move_id":269},{"level":43,"move_id":207},{"level":48,"move_id":130},{"level":53,"move_id":97}]},"tmhm_learnset":"03B03F4086137A74","types":[11,17]},{"abilities":[52,71],"address":3306040,"base_stats":[45,100,45,10,45,45],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":35,"species":333}],"friendship":70,"id":332,"learnset":{"address":3316134,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":44},{"level":9,"move_id":28},{"level":17,"move_id":185},{"level":25,"move_id":328},{"level":33,"move_id":242},{"level":41,"move_id":91},{"level":49,"move_id":201},{"level":57,"move_id":63}]},"tmhm_learnset":"00A01E508E354620","types":[4,4]},{"abilities":[26,26],"address":3306068,"base_stats":[50,70,50,70,50,50],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":45,"species":334}],"friendship":70,"id":333,"learnset":{"address":3316158,"moves":[{"level":1,"move_id":44},{"level":1,"move_id":28},{"level":1,"move_id":185},{"level":1,"move_id":328},{"level":9,"move_id":28},{"level":17,"move_id":185},{"level":25,"move_id":328},{"level":33,"move_id":242},{"level":35,"move_id":225},{"level":41,"move_id":103},{"level":49,"move_id":201},{"level":57,"move_id":63}]},"tmhm_learnset":"00A85E508E354620","types":[4,16]},{"abilities":[26,26],"address":3306096,"base_stats":[80,100,80,100,80,80],"catch_rate":45,"evolutions":[],"friendship":70,"id":334,"learnset":{"address":3316184,"moves":[{"level":1,"move_id":44},{"level":1,"move_id":28},{"level":1,"move_id":185},{"level":1,"move_id":328},{"level":9,"move_id":28},{"level":17,"move_id":185},{"level":25,"move_id":328},{"level":33,"move_id":242},{"level":35,"move_id":225},{"level":41,"move_id":103},{"level":53,"move_id":201},{"level":65,"move_id":63}]},"tmhm_learnset":"00A85E748E754622","types":[4,16]},{"abilities":[47,62],"address":3306124,"base_stats":[72,60,30,25,20,30],"catch_rate":180,"evolutions":[{"method":"LEVEL","param":24,"species":336}],"friendship":70,"id":335,"learnset":{"address":3316210,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":116},{"level":4,"move_id":28},{"level":10,"move_id":292},{"level":13,"move_id":233},{"level":19,"move_id":252},{"level":22,"move_id":18},{"level":28,"move_id":282},{"level":31,"move_id":265},{"level":37,"move_id":187},{"level":40,"move_id":203},{"level":46,"move_id":69},{"level":49,"move_id":179}]},"tmhm_learnset":"00B01E40CE1306A1","types":[1,1]},{"abilities":[47,62],"address":3306152,"base_stats":[144,120,60,50,40,60],"catch_rate":200,"evolutions":[],"friendship":70,"id":336,"learnset":{"address":3316242,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":116},{"level":1,"move_id":28},{"level":1,"move_id":292},{"level":4,"move_id":28},{"level":10,"move_id":292},{"level":13,"move_id":233},{"level":19,"move_id":252},{"level":22,"move_id":18},{"level":29,"move_id":282},{"level":33,"move_id":265},{"level":40,"move_id":187},{"level":44,"move_id":203},{"level":51,"move_id":69},{"level":55,"move_id":179}]},"tmhm_learnset":"00B01E40CE1346A1","types":[1,1]},{"abilities":[9,31],"address":3306180,"base_stats":[40,45,40,65,65,40],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":26,"species":338}],"friendship":70,"id":337,"learnset":{"address":3316274,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":4,"move_id":86},{"level":9,"move_id":43},{"level":12,"move_id":336},{"level":17,"move_id":98},{"level":20,"move_id":209},{"level":25,"move_id":316},{"level":28,"move_id":46},{"level":33,"move_id":44},{"level":36,"move_id":87},{"level":41,"move_id":268}]},"tmhm_learnset":"00603E0285D30230","types":[13,13]},{"abilities":[9,31],"address":3306208,"base_stats":[70,75,60,105,105,60],"catch_rate":45,"evolutions":[],"friendship":70,"id":338,"learnset":{"address":3316304,"moves":[{"level":1,"move_id":86},{"level":1,"move_id":43},{"level":1,"move_id":336},{"level":1,"move_id":33},{"level":4,"move_id":86},{"level":9,"move_id":43},{"level":12,"move_id":336},{"level":17,"move_id":98},{"level":20,"move_id":209},{"level":25,"move_id":316},{"level":31,"move_id":46},{"level":39,"move_id":44},{"level":45,"move_id":87},{"level":53,"move_id":268}]},"tmhm_learnset":"00603E0285D34230","types":[13,13]},{"abilities":[12,0],"address":3306236,"base_stats":[60,60,40,35,65,45],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":33,"species":340}],"friendship":70,"id":339,"learnset":{"address":3316334,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":45},{"level":1,"move_id":33},{"level":11,"move_id":52},{"level":19,"move_id":222},{"level":25,"move_id":116},{"level":29,"move_id":36},{"level":31,"move_id":133},{"level":35,"move_id":89},{"level":41,"move_id":53},{"level":49,"move_id":38}]},"tmhm_learnset":"00A21E748E110620","types":[10,4]},{"abilities":[40,0],"address":3306264,"base_stats":[70,100,70,40,105,75],"catch_rate":150,"evolutions":[],"friendship":70,"id":340,"learnset":{"address":3316360,"moves":[{"level":1,"move_id":45},{"level":1,"move_id":33},{"level":1,"move_id":52},{"level":1,"move_id":222},{"level":11,"move_id":52},{"level":19,"move_id":222},{"level":25,"move_id":116},{"level":29,"move_id":36},{"level":31,"move_id":133},{"level":33,"move_id":157},{"level":37,"move_id":89},{"level":45,"move_id":284},{"level":55,"move_id":90}]},"tmhm_learnset":"00A21E748E114630","types":[10,4]},{"abilities":[47,0],"address":3306292,"base_stats":[70,40,50,25,55,50],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":32,"species":342}],"friendship":70,"id":341,"learnset":{"address":3316388,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":181},{"level":1,"move_id":45},{"level":1,"move_id":55},{"level":7,"move_id":227},{"level":13,"move_id":301},{"level":19,"move_id":34},{"level":25,"move_id":62},{"level":31,"move_id":258},{"level":37,"move_id":156},{"level":37,"move_id":173},{"level":43,"move_id":59},{"level":49,"move_id":329}]},"tmhm_learnset":"03B01E4086533264","types":[15,11]},{"abilities":[47,0],"address":3306320,"base_stats":[90,60,70,45,75,70],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":44,"species":343}],"friendship":70,"id":342,"learnset":{"address":3316416,"moves":[{"level":1,"move_id":181},{"level":1,"move_id":45},{"level":1,"move_id":55},{"level":1,"move_id":227},{"level":7,"move_id":227},{"level":13,"move_id":301},{"level":19,"move_id":34},{"level":25,"move_id":62},{"level":31,"move_id":258},{"level":39,"move_id":156},{"level":39,"move_id":173},{"level":47,"move_id":59},{"level":55,"move_id":329}]},"tmhm_learnset":"03B01E4086533274","types":[15,11]},{"abilities":[47,0],"address":3306348,"base_stats":[110,80,90,65,95,90],"catch_rate":45,"evolutions":[],"friendship":70,"id":343,"learnset":{"address":3316444,"moves":[{"level":1,"move_id":181},{"level":1,"move_id":45},{"level":1,"move_id":55},{"level":1,"move_id":227},{"level":7,"move_id":227},{"level":13,"move_id":301},{"level":19,"move_id":34},{"level":25,"move_id":62},{"level":31,"move_id":258},{"level":39,"move_id":156},{"level":39,"move_id":173},{"level":50,"move_id":59},{"level":61,"move_id":329}]},"tmhm_learnset":"03B01E4086537274","types":[15,11]},{"abilities":[8,0],"address":3306376,"base_stats":[50,85,40,35,85,40],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":32,"species":345}],"friendship":35,"id":344,"learnset":{"address":3316472,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":40},{"level":1,"move_id":43},{"level":5,"move_id":71},{"level":9,"move_id":74},{"level":13,"move_id":73},{"level":17,"move_id":28},{"level":21,"move_id":42},{"level":25,"move_id":275},{"level":29,"move_id":185},{"level":33,"move_id":191},{"level":37,"move_id":302},{"level":41,"move_id":178},{"level":45,"move_id":201}]},"tmhm_learnset":"00441E1084350721","types":[12,12]},{"abilities":[8,0],"address":3306404,"base_stats":[70,115,60,55,115,60],"catch_rate":60,"evolutions":[],"friendship":35,"id":345,"learnset":{"address":3316504,"moves":[{"level":1,"move_id":40},{"level":1,"move_id":43},{"level":1,"move_id":71},{"level":1,"move_id":74},{"level":5,"move_id":71},{"level":9,"move_id":74},{"level":13,"move_id":73},{"level":17,"move_id":28},{"level":21,"move_id":42},{"level":25,"move_id":275},{"level":29,"move_id":185},{"level":35,"move_id":191},{"level":41,"move_id":302},{"level":47,"move_id":178},{"level":53,"move_id":201}]},"tmhm_learnset":"00641E1084354721","types":[12,17]},{"abilities":[39,0],"address":3306432,"base_stats":[50,50,50,50,50,50],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":42,"species":347}],"friendship":70,"id":346,"learnset":{"address":3316536,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":181},{"level":1,"move_id":43},{"level":7,"move_id":104},{"level":10,"move_id":44},{"level":16,"move_id":196},{"level":19,"move_id":29},{"level":25,"move_id":182},{"level":28,"move_id":242},{"level":34,"move_id":58},{"level":37,"move_id":258},{"level":43,"move_id":59}]},"tmhm_learnset":"00401E00A41BB264","types":[15,15]},{"abilities":[39,0],"address":3306460,"base_stats":[80,80,80,80,80,80],"catch_rate":75,"evolutions":[],"friendship":70,"id":347,"learnset":{"address":3316564,"moves":[{"level":1,"move_id":181},{"level":1,"move_id":43},{"level":1,"move_id":104},{"level":1,"move_id":44},{"level":7,"move_id":104},{"level":10,"move_id":44},{"level":16,"move_id":196},{"level":19,"move_id":29},{"level":25,"move_id":182},{"level":28,"move_id":242},{"level":34,"move_id":58},{"level":42,"move_id":258},{"level":53,"move_id":59},{"level":61,"move_id":329}]},"tmhm_learnset":"00401F00A61BFA64","types":[15,15]},{"abilities":[26,0],"address":3306488,"base_stats":[70,55,65,70,95,85],"catch_rate":45,"evolutions":[],"friendship":70,"id":348,"learnset":{"address":3316594,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":106},{"level":7,"move_id":93},{"level":13,"move_id":88},{"level":19,"move_id":95},{"level":25,"move_id":149},{"level":31,"move_id":322},{"level":37,"move_id":94},{"level":43,"move_id":248},{"level":49,"move_id":153}]},"tmhm_learnset":"00408E51B61BD228","types":[5,14]},{"abilities":[26,0],"address":3306516,"base_stats":[70,95,85,70,55,65],"catch_rate":45,"evolutions":[],"friendship":70,"id":349,"learnset":{"address":3316620,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":106},{"level":7,"move_id":93},{"level":13,"move_id":88},{"level":19,"move_id":83},{"level":25,"move_id":149},{"level":31,"move_id":322},{"level":37,"move_id":157},{"level":43,"move_id":76},{"level":49,"move_id":153}]},"tmhm_learnset":"00428E75B639C628","types":[5,14]},{"abilities":[47,37],"address":3306544,"base_stats":[50,20,40,20,20,40],"catch_rate":150,"evolutions":[{"method":"FRIENDSHIP","param":0,"species":183}],"friendship":70,"id":350,"learnset":{"address":3316646,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":145},{"level":1,"move_id":150},{"level":3,"move_id":204},{"level":6,"move_id":39},{"level":10,"move_id":145},{"level":15,"move_id":21},{"level":21,"move_id":55}]},"tmhm_learnset":"01101E0084533264","types":[0,0]},{"abilities":[47,20],"address":3306572,"base_stats":[60,25,35,60,70,80],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":32,"species":352}],"friendship":70,"id":351,"learnset":{"address":3316666,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":149},{"level":1,"move_id":150},{"level":7,"move_id":149},{"level":10,"move_id":316},{"level":16,"move_id":60},{"level":19,"move_id":244},{"level":25,"move_id":109},{"level":28,"move_id":277},{"level":34,"move_id":94},{"level":37,"move_id":156},{"level":37,"move_id":173},{"level":43,"move_id":340}]},"tmhm_learnset":"0041BF03B4538E28","types":[14,14]},{"abilities":[47,20],"address":3306600,"base_stats":[80,45,65,80,90,110],"catch_rate":60,"evolutions":[],"friendship":70,"id":352,"learnset":{"address":3316696,"moves":[{"level":1,"move_id":150},{"level":1,"move_id":149},{"level":1,"move_id":316},{"level":1,"move_id":60},{"level":7,"move_id":149},{"level":10,"move_id":316},{"level":16,"move_id":60},{"level":19,"move_id":244},{"level":25,"move_id":109},{"level":28,"move_id":277},{"level":37,"move_id":94},{"level":43,"move_id":156},{"level":43,"move_id":173},{"level":55,"move_id":340}]},"tmhm_learnset":"0041BF03B453CE29","types":[14,14]},{"abilities":[57,0],"address":3306628,"base_stats":[60,50,40,95,85,75],"catch_rate":200,"evolutions":[],"friendship":70,"id":353,"learnset":{"address":3316726,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":98},{"level":1,"move_id":45},{"level":4,"move_id":86},{"level":10,"move_id":98},{"level":13,"move_id":270},{"level":19,"move_id":209},{"level":22,"move_id":227},{"level":28,"move_id":313},{"level":31,"move_id":268},{"level":37,"move_id":87},{"level":40,"move_id":226},{"level":47,"move_id":97}]},"tmhm_learnset":"00401E0285D38220","types":[13,13]},{"abilities":[58,0],"address":3306656,"base_stats":[60,40,50,95,75,85],"catch_rate":200,"evolutions":[],"friendship":70,"id":354,"learnset":{"address":3316756,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":98},{"level":1,"move_id":45},{"level":4,"move_id":86},{"level":10,"move_id":98},{"level":13,"move_id":270},{"level":19,"move_id":209},{"level":22,"move_id":227},{"level":28,"move_id":204},{"level":31,"move_id":268},{"level":37,"move_id":87},{"level":40,"move_id":226},{"level":47,"move_id":97}]},"tmhm_learnset":"00401E0285D38220","types":[13,13]},{"abilities":[52,22],"address":3306684,"base_stats":[50,85,85,50,55,55],"catch_rate":45,"evolutions":[],"friendship":70,"id":355,"learnset":{"address":3316786,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":310},{"level":6,"move_id":313},{"level":11,"move_id":44},{"level":16,"move_id":230},{"level":21,"move_id":11},{"level":26,"move_id":185},{"level":31,"move_id":226},{"level":36,"move_id":242},{"level":41,"move_id":334},{"level":46,"move_id":254},{"level":46,"move_id":256},{"level":46,"move_id":255}]},"tmhm_learnset":"00A01F7CC4335E21","types":[8,8]},{"abilities":[74,0],"address":3306712,"base_stats":[30,40,55,60,40,55],"catch_rate":180,"evolutions":[{"method":"LEVEL","param":37,"species":357}],"friendship":70,"id":356,"learnset":{"address":3316818,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":117},{"level":4,"move_id":96},{"level":9,"move_id":93},{"level":12,"move_id":197},{"level":18,"move_id":237},{"level":22,"move_id":170},{"level":28,"move_id":347},{"level":32,"move_id":136},{"level":38,"move_id":244},{"level":42,"move_id":179},{"level":48,"move_id":105}]},"tmhm_learnset":"00E01E41F41386A9","types":[1,14]},{"abilities":[74,0],"address":3306740,"base_stats":[60,60,75,80,60,75],"catch_rate":90,"evolutions":[],"friendship":70,"id":357,"learnset":{"address":3316848,"moves":[{"level":1,"move_id":7},{"level":1,"move_id":9},{"level":1,"move_id":8},{"level":1,"move_id":117},{"level":1,"move_id":96},{"level":1,"move_id":93},{"level":1,"move_id":197},{"level":4,"move_id":96},{"level":9,"move_id":93},{"level":12,"move_id":197},{"level":18,"move_id":237},{"level":22,"move_id":170},{"level":28,"move_id":347},{"level":32,"move_id":136},{"level":40,"move_id":244},{"level":46,"move_id":179},{"level":54,"move_id":105}]},"tmhm_learnset":"00E01E41F413C6A9","types":[1,14]},{"abilities":[30,0],"address":3306768,"base_stats":[45,40,60,50,40,75],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":35,"species":359}],"friendship":70,"id":358,"learnset":{"address":3316884,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":64},{"level":1,"move_id":45},{"level":8,"move_id":310},{"level":11,"move_id":47},{"level":18,"move_id":31},{"level":21,"move_id":219},{"level":28,"move_id":54},{"level":31,"move_id":36},{"level":38,"move_id":119},{"level":41,"move_id":287},{"level":48,"move_id":195}]},"tmhm_learnset":"00087E80843B1620","types":[0,2]},{"abilities":[30,0],"address":3306796,"base_stats":[75,70,90,80,70,105],"catch_rate":45,"evolutions":[],"friendship":70,"id":359,"learnset":{"address":3316912,"moves":[{"level":1,"move_id":64},{"level":1,"move_id":45},{"level":1,"move_id":310},{"level":1,"move_id":47},{"level":8,"move_id":310},{"level":11,"move_id":47},{"level":18,"move_id":31},{"level":21,"move_id":219},{"level":28,"move_id":54},{"level":31,"move_id":36},{"level":35,"move_id":225},{"level":40,"move_id":349},{"level":45,"move_id":287},{"level":54,"move_id":195},{"level":59,"move_id":143}]},"tmhm_learnset":"00887EA4867B5632","types":[16,2]},{"abilities":[23,0],"address":3306824,"base_stats":[95,23,48,23,23,48],"catch_rate":125,"evolutions":[{"method":"LEVEL","param":15,"species":202}],"friendship":70,"id":360,"learnset":{"address":3316944,"moves":[{"level":1,"move_id":68},{"level":1,"move_id":150},{"level":1,"move_id":204},{"level":1,"move_id":227},{"level":15,"move_id":68},{"level":15,"move_id":243},{"level":15,"move_id":219},{"level":15,"move_id":194}]},"tmhm_learnset":"0000000000000000","types":[14,14]},{"abilities":[26,0],"address":3306852,"base_stats":[20,40,90,25,30,90],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":37,"species":362}],"friendship":35,"id":361,"learnset":{"address":3316962,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":43},{"level":1,"move_id":101},{"level":5,"move_id":50},{"level":12,"move_id":193},{"level":16,"move_id":310},{"level":23,"move_id":109},{"level":27,"move_id":228},{"level":34,"move_id":174},{"level":38,"move_id":261},{"level":45,"move_id":212},{"level":49,"move_id":248}]},"tmhm_learnset":"0041BF00B4133E28","types":[7,7]},{"abilities":[46,0],"address":3306880,"base_stats":[40,70,130,25,60,130],"catch_rate":90,"evolutions":[],"friendship":35,"id":362,"learnset":{"address":3316990,"moves":[{"level":1,"move_id":20},{"level":1,"move_id":43},{"level":1,"move_id":101},{"level":1,"move_id":50},{"level":5,"move_id":50},{"level":12,"move_id":193},{"level":16,"move_id":310},{"level":23,"move_id":109},{"level":27,"move_id":228},{"level":34,"move_id":174},{"level":37,"move_id":325},{"level":41,"move_id":261},{"level":51,"move_id":212},{"level":58,"move_id":248}]},"tmhm_learnset":"00E1BF40B6137E29","types":[7,7]},{"abilities":[30,38],"address":3306908,"base_stats":[50,60,45,65,100,80],"catch_rate":150,"evolutions":[],"friendship":70,"id":363,"learnset":{"address":3317020,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":71},{"level":5,"move_id":74},{"level":9,"move_id":40},{"level":13,"move_id":78},{"level":17,"move_id":72},{"level":21,"move_id":73},{"level":25,"move_id":345},{"level":29,"move_id":320},{"level":33,"move_id":202},{"level":37,"move_id":230},{"level":41,"move_id":275},{"level":45,"move_id":92},{"level":49,"move_id":80},{"level":53,"move_id":312},{"level":57,"move_id":235}]},"tmhm_learnset":"00441E08A4350720","types":[12,3]},{"abilities":[54,0],"address":3306936,"base_stats":[60,60,60,30,35,35],"catch_rate":255,"evolutions":[{"method":"LEVEL","param":18,"species":365}],"friendship":70,"id":364,"learnset":{"address":3317058,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":1,"move_id":281},{"level":7,"move_id":227},{"level":13,"move_id":303},{"level":19,"move_id":185},{"level":25,"move_id":133},{"level":31,"move_id":343},{"level":37,"move_id":68},{"level":43,"move_id":175}]},"tmhm_learnset":"00A41EA6E5B336A5","types":[0,0]},{"abilities":[72,0],"address":3306964,"base_stats":[80,80,80,90,55,55],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":36,"species":366}],"friendship":70,"id":365,"learnset":{"address":3317082,"moves":[{"level":1,"move_id":10},{"level":1,"move_id":116},{"level":1,"move_id":227},{"level":1,"move_id":253},{"level":7,"move_id":227},{"level":13,"move_id":253},{"level":19,"move_id":154},{"level":25,"move_id":203},{"level":31,"move_id":163},{"level":37,"move_id":68},{"level":43,"move_id":264},{"level":49,"move_id":179}]},"tmhm_learnset":"00A41EA6E7B33EB5","types":[0,0]},{"abilities":[54,0],"address":3306992,"base_stats":[150,160,100,100,95,65],"catch_rate":45,"evolutions":[],"friendship":70,"id":366,"learnset":{"address":3317108,"moves":[{"level":1,"move_id":10},{"level":1,"move_id":281},{"level":1,"move_id":227},{"level":1,"move_id":303},{"level":7,"move_id":227},{"level":13,"move_id":303},{"level":19,"move_id":185},{"level":25,"move_id":133},{"level":31,"move_id":343},{"level":36,"move_id":207},{"level":37,"move_id":68},{"level":43,"move_id":175}]},"tmhm_learnset":"00A41EA6E7B37EB5","types":[0,0]},{"abilities":[64,60],"address":3307020,"base_stats":[70,43,53,40,43,53],"catch_rate":225,"evolutions":[{"method":"LEVEL","param":26,"species":368}],"friendship":70,"id":367,"learnset":{"address":3317134,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":1},{"level":6,"move_id":281},{"level":9,"move_id":139},{"level":14,"move_id":124},{"level":17,"move_id":133},{"level":23,"move_id":227},{"level":28,"move_id":92},{"level":34,"move_id":254},{"level":34,"move_id":255},{"level":34,"move_id":256},{"level":39,"move_id":188}]},"tmhm_learnset":"00A11E0AA4371724","types":[3,3]},{"abilities":[64,60],"address":3307048,"base_stats":[100,73,83,55,73,83],"catch_rate":75,"evolutions":[],"friendship":70,"id":368,"learnset":{"address":3317164,"moves":[{"level":1,"move_id":1},{"level":1,"move_id":281},{"level":1,"move_id":139},{"level":1,"move_id":124},{"level":6,"move_id":281},{"level":9,"move_id":139},{"level":14,"move_id":124},{"level":17,"move_id":133},{"level":23,"move_id":227},{"level":26,"move_id":34},{"level":31,"move_id":92},{"level":40,"move_id":254},{"level":40,"move_id":255},{"level":40,"move_id":256},{"level":48,"move_id":188}]},"tmhm_learnset":"00A11E0AA4375724","types":[3,3]},{"abilities":[34,0],"address":3307076,"base_stats":[99,68,83,51,72,87],"catch_rate":200,"evolutions":[],"friendship":70,"id":369,"learnset":{"address":3317196,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":43},{"level":1,"move_id":16},{"level":7,"move_id":74},{"level":11,"move_id":75},{"level":17,"move_id":23},{"level":21,"move_id":230},{"level":27,"move_id":18},{"level":31,"move_id":345},{"level":37,"move_id":34},{"level":41,"move_id":76},{"level":47,"move_id":235}]},"tmhm_learnset":"00EC5E80863D4730","types":[12,2]},{"abilities":[43,0],"address":3307104,"base_stats":[64,51,23,28,51,23],"catch_rate":190,"evolutions":[{"method":"LEVEL","param":20,"species":371}],"friendship":70,"id":370,"learnset":{"address":3317224,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":1},{"level":5,"move_id":253},{"level":11,"move_id":310},{"level":15,"move_id":336},{"level":21,"move_id":48},{"level":25,"move_id":23},{"level":31,"move_id":103},{"level":35,"move_id":46},{"level":41,"move_id":156},{"level":41,"move_id":214},{"level":45,"move_id":304}]},"tmhm_learnset":"00001E26A4333634","types":[0,0]},{"abilities":[43,0],"address":3307132,"base_stats":[84,71,43,48,71,43],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":40,"species":372}],"friendship":70,"id":371,"learnset":{"address":3317254,"moves":[{"level":1,"move_id":1},{"level":1,"move_id":253},{"level":1,"move_id":310},{"level":1,"move_id":336},{"level":5,"move_id":253},{"level":11,"move_id":310},{"level":15,"move_id":336},{"level":23,"move_id":48},{"level":29,"move_id":23},{"level":37,"move_id":103},{"level":43,"move_id":46},{"level":51,"move_id":156},{"level":51,"move_id":214},{"level":57,"move_id":304}]},"tmhm_learnset":"00A21F26E6333E34","types":[0,0]},{"abilities":[43,0],"address":3307160,"base_stats":[104,91,63,68,91,63],"catch_rate":45,"evolutions":[],"friendship":70,"id":372,"learnset":{"address":3317284,"moves":[{"level":1,"move_id":1},{"level":1,"move_id":253},{"level":1,"move_id":310},{"level":1,"move_id":336},{"level":5,"move_id":253},{"level":11,"move_id":310},{"level":15,"move_id":336},{"level":23,"move_id":48},{"level":29,"move_id":23},{"level":37,"move_id":103},{"level":40,"move_id":63},{"level":45,"move_id":46},{"level":55,"move_id":156},{"level":55,"move_id":214},{"level":63,"move_id":304}]},"tmhm_learnset":"00A21F26E6337E34","types":[0,0]},{"abilities":[75,0],"address":3307188,"base_stats":[35,64,85,32,74,55],"catch_rate":255,"evolutions":[{"method":"ITEM","param":192,"species":374},{"method":"ITEM","param":193,"species":375}],"friendship":70,"id":373,"learnset":{"address":3317316,"moves":[{"level":1,"move_id":128},{"level":1,"move_id":55},{"level":1,"move_id":250},{"level":1,"move_id":334}]},"tmhm_learnset":"03101E0084133264","types":[11,11]},{"abilities":[33,0],"address":3307216,"base_stats":[55,104,105,52,94,75],"catch_rate":60,"evolutions":[],"friendship":70,"id":374,"learnset":{"address":3317326,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":250},{"level":8,"move_id":44},{"level":15,"move_id":103},{"level":22,"move_id":352},{"level":29,"move_id":184},{"level":36,"move_id":242},{"level":43,"move_id":226},{"level":50,"move_id":56}]},"tmhm_learnset":"03111E4084137264","types":[11,11]},{"abilities":[33,0],"address":3307244,"base_stats":[55,84,105,52,114,75],"catch_rate":60,"evolutions":[],"friendship":70,"id":375,"learnset":{"address":3317350,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":250},{"level":8,"move_id":93},{"level":15,"move_id":97},{"level":22,"move_id":352},{"level":29,"move_id":133},{"level":36,"move_id":94},{"level":43,"move_id":226},{"level":50,"move_id":56}]},"tmhm_learnset":"03101E00B41B7264","types":[11,11]},{"abilities":[46,0],"address":3307272,"base_stats":[65,130,60,75,75,60],"catch_rate":30,"evolutions":[],"friendship":35,"id":376,"learnset":{"address":3317374,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":5,"move_id":43},{"level":9,"move_id":269},{"level":13,"move_id":98},{"level":17,"move_id":13},{"level":21,"move_id":44},{"level":26,"move_id":14},{"level":31,"move_id":104},{"level":36,"move_id":163},{"level":41,"move_id":248},{"level":46,"move_id":195}]},"tmhm_learnset":"00E53FB6A5D37E6C","types":[17,17]},{"abilities":[15,0],"address":3307300,"base_stats":[44,75,35,45,63,33],"catch_rate":225,"evolutions":[{"method":"LEVEL","param":37,"species":378}],"friendship":35,"id":377,"learnset":{"address":3317404,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":282},{"level":8,"move_id":103},{"level":13,"move_id":101},{"level":20,"move_id":174},{"level":25,"move_id":180},{"level":32,"move_id":261},{"level":37,"move_id":185},{"level":44,"move_id":247},{"level":49,"move_id":289},{"level":56,"move_id":288}]},"tmhm_learnset":"0041BF02B5930E28","types":[7,7]},{"abilities":[15,0],"address":3307328,"base_stats":[64,115,65,65,83,63],"catch_rate":45,"evolutions":[],"friendship":35,"id":378,"learnset":{"address":3317432,"moves":[{"level":1,"move_id":282},{"level":1,"move_id":103},{"level":1,"move_id":101},{"level":1,"move_id":174},{"level":8,"move_id":103},{"level":13,"move_id":101},{"level":20,"move_id":174},{"level":25,"move_id":180},{"level":32,"move_id":261},{"level":39,"move_id":185},{"level":48,"move_id":247},{"level":55,"move_id":289},{"level":64,"move_id":288}]},"tmhm_learnset":"0041BF02B5934E28","types":[7,7]},{"abilities":[61,0],"address":3307356,"base_stats":[73,100,60,65,100,60],"catch_rate":90,"evolutions":[],"friendship":70,"id":379,"learnset":{"address":3317460,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":35},{"level":7,"move_id":122},{"level":10,"move_id":44},{"level":16,"move_id":342},{"level":19,"move_id":103},{"level":25,"move_id":137},{"level":28,"move_id":242},{"level":34,"move_id":305},{"level":37,"move_id":207},{"level":43,"move_id":114}]},"tmhm_learnset":"00A13E0C8E570E20","types":[3,3]},{"abilities":[17,0],"address":3307384,"base_stats":[73,115,60,90,60,60],"catch_rate":90,"evolutions":[],"friendship":70,"id":380,"learnset":{"address":3317488,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":4,"move_id":43},{"level":7,"move_id":98},{"level":10,"move_id":14},{"level":13,"move_id":210},{"level":19,"move_id":163},{"level":25,"move_id":228},{"level":31,"move_id":306},{"level":37,"move_id":269},{"level":46,"move_id":197},{"level":55,"move_id":206}]},"tmhm_learnset":"00A03EA6EDF73E35","types":[0,0]},{"abilities":[33,69],"address":3307412,"base_stats":[100,90,130,55,45,65],"catch_rate":25,"evolutions":[],"friendship":70,"id":381,"learnset":{"address":3317518,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":1,"move_id":106},{"level":8,"move_id":55},{"level":15,"move_id":317},{"level":22,"move_id":281},{"level":29,"move_id":36},{"level":36,"move_id":300},{"level":43,"move_id":246},{"level":50,"move_id":156},{"level":57,"move_id":38},{"level":64,"move_id":56}]},"tmhm_learnset":"03901E50861B726C","types":[11,5]},{"abilities":[5,69],"address":3307440,"base_stats":[50,70,100,30,40,40],"catch_rate":180,"evolutions":[{"method":"LEVEL","param":32,"species":383}],"friendship":35,"id":382,"learnset":{"address":3317546,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":4,"move_id":106},{"level":7,"move_id":189},{"level":10,"move_id":29},{"level":13,"move_id":232},{"level":17,"move_id":334},{"level":21,"move_id":46},{"level":25,"move_id":36},{"level":29,"move_id":231},{"level":34,"move_id":182},{"level":39,"move_id":319},{"level":44,"move_id":38}]},"tmhm_learnset":"00A41ED28E530634","types":[8,5]},{"abilities":[5,69],"address":3307468,"base_stats":[60,90,140,40,50,50],"catch_rate":90,"evolutions":[{"method":"LEVEL","param":42,"species":384}],"friendship":35,"id":383,"learnset":{"address":3317578,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":106},{"level":1,"move_id":189},{"level":1,"move_id":29},{"level":4,"move_id":106},{"level":7,"move_id":189},{"level":10,"move_id":29},{"level":13,"move_id":232},{"level":17,"move_id":334},{"level":21,"move_id":46},{"level":25,"move_id":36},{"level":29,"move_id":231},{"level":37,"move_id":182},{"level":45,"move_id":319},{"level":53,"move_id":38}]},"tmhm_learnset":"00A41ED28E530634","types":[8,5]},{"abilities":[5,69],"address":3307496,"base_stats":[70,110,180,50,60,60],"catch_rate":45,"evolutions":[],"friendship":35,"id":384,"learnset":{"address":3317610,"moves":[{"level":1,"move_id":33},{"level":1,"move_id":106},{"level":1,"move_id":189},{"level":1,"move_id":29},{"level":4,"move_id":106},{"level":7,"move_id":189},{"level":10,"move_id":29},{"level":13,"move_id":232},{"level":17,"move_id":334},{"level":21,"move_id":46},{"level":25,"move_id":36},{"level":29,"move_id":231},{"level":37,"move_id":182},{"level":50,"move_id":319},{"level":63,"move_id":38}]},"tmhm_learnset":"00B41EF6CFF37E37","types":[8,5]},{"abilities":[59,0],"address":3307524,"base_stats":[70,70,70,70,70,70],"catch_rate":45,"evolutions":[],"friendship":70,"id":385,"learnset":{"address":3317642,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":10,"move_id":55},{"level":10,"move_id":52},{"level":10,"move_id":181},{"level":20,"move_id":240},{"level":20,"move_id":241},{"level":20,"move_id":258},{"level":30,"move_id":311}]},"tmhm_learnset":"00403E36A5B33664","types":[0,0]},{"abilities":[35,68],"address":3307552,"base_stats":[65,73,55,85,47,75],"catch_rate":150,"evolutions":[],"friendship":70,"id":386,"learnset":{"address":3317666,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":5,"move_id":109},{"level":9,"move_id":104},{"level":13,"move_id":236},{"level":17,"move_id":98},{"level":21,"move_id":294},{"level":25,"move_id":324},{"level":29,"move_id":182},{"level":33,"move_id":270},{"level":37,"move_id":38}]},"tmhm_learnset":"00403E82E5B78625","types":[6,6]},{"abilities":[12,0],"address":3307580,"base_stats":[65,47,55,85,73,75],"catch_rate":150,"evolutions":[],"friendship":70,"id":387,"learnset":{"address":3317694,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":33},{"level":5,"move_id":230},{"level":9,"move_id":204},{"level":13,"move_id":236},{"level":17,"move_id":98},{"level":21,"move_id":273},{"level":25,"move_id":227},{"level":29,"move_id":260},{"level":33,"move_id":270},{"level":37,"move_id":343}]},"tmhm_learnset":"00403E82E5B78625","types":[6,6]},{"abilities":[21,0],"address":3307608,"base_stats":[66,41,77,23,61,87],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":40,"species":389}],"friendship":70,"id":388,"learnset":{"address":3317722,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":310},{"level":8,"move_id":132},{"level":15,"move_id":51},{"level":22,"move_id":275},{"level":29,"move_id":109},{"level":36,"move_id":133},{"level":43,"move_id":246},{"level":50,"move_id":254},{"level":50,"move_id":255},{"level":50,"move_id":256}]},"tmhm_learnset":"00001E1884350720","types":[5,12]},{"abilities":[21,0],"address":3307636,"base_stats":[86,81,97,43,81,107],"catch_rate":45,"evolutions":[],"friendship":70,"id":389,"learnset":{"address":3317750,"moves":[{"level":1,"move_id":310},{"level":1,"move_id":132},{"level":1,"move_id":51},{"level":1,"move_id":275},{"level":8,"move_id":132},{"level":15,"move_id":51},{"level":22,"move_id":275},{"level":29,"move_id":109},{"level":36,"move_id":133},{"level":48,"move_id":246},{"level":60,"move_id":254},{"level":60,"move_id":255},{"level":60,"move_id":256}]},"tmhm_learnset":"00A01E5886354720","types":[5,12]},{"abilities":[4,0],"address":3307664,"base_stats":[45,95,50,75,40,50],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":40,"species":391}],"friendship":70,"id":390,"learnset":{"address":3317778,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":10},{"level":7,"move_id":106},{"level":13,"move_id":300},{"level":19,"move_id":55},{"level":25,"move_id":232},{"level":31,"move_id":182},{"level":37,"move_id":246},{"level":43,"move_id":210},{"level":49,"move_id":163},{"level":55,"move_id":350}]},"tmhm_learnset":"00841ED0CC110624","types":[5,6]},{"abilities":[4,0],"address":3307692,"base_stats":[75,125,100,45,70,80],"catch_rate":45,"evolutions":[],"friendship":70,"id":391,"learnset":{"address":3317806,"moves":[{"level":1,"move_id":10},{"level":1,"move_id":106},{"level":1,"move_id":300},{"level":1,"move_id":55},{"level":7,"move_id":106},{"level":13,"move_id":300},{"level":19,"move_id":55},{"level":25,"move_id":232},{"level":31,"move_id":182},{"level":37,"move_id":246},{"level":46,"move_id":210},{"level":55,"move_id":163},{"level":64,"move_id":350}]},"tmhm_learnset":"00A41ED0CE514624","types":[5,6]},{"abilities":[28,36],"address":3307720,"base_stats":[28,25,25,40,45,35],"catch_rate":235,"evolutions":[{"method":"LEVEL","param":20,"species":393}],"friendship":35,"id":392,"learnset":{"address":3317834,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":93},{"level":1,"move_id":45},{"level":6,"move_id":93},{"level":11,"move_id":104},{"level":16,"move_id":100},{"level":21,"move_id":347},{"level":26,"move_id":94},{"level":31,"move_id":286},{"level":36,"move_id":248},{"level":41,"move_id":95},{"level":46,"move_id":138}]},"tmhm_learnset":"0041BF03B49B8E28","types":[14,14]},{"abilities":[28,36],"address":3307748,"base_stats":[38,35,35,50,65,55],"catch_rate":120,"evolutions":[{"method":"LEVEL","param":30,"species":394}],"friendship":35,"id":393,"learnset":{"address":3317862,"moves":[{"level":1,"move_id":45},{"level":1,"move_id":93},{"level":1,"move_id":104},{"level":1,"move_id":100},{"level":6,"move_id":93},{"level":11,"move_id":104},{"level":16,"move_id":100},{"level":21,"move_id":347},{"level":26,"move_id":94},{"level":33,"move_id":286},{"level":40,"move_id":248},{"level":47,"move_id":95},{"level":54,"move_id":138}]},"tmhm_learnset":"0041BF03B49B8E28","types":[14,14]},{"abilities":[28,36],"address":3307776,"base_stats":[68,65,65,80,125,115],"catch_rate":45,"evolutions":[],"friendship":35,"id":394,"learnset":{"address":3317890,"moves":[{"level":1,"move_id":45},{"level":1,"move_id":93},{"level":1,"move_id":104},{"level":1,"move_id":100},{"level":6,"move_id":93},{"level":11,"move_id":104},{"level":16,"move_id":100},{"level":21,"move_id":347},{"level":26,"move_id":94},{"level":33,"move_id":286},{"level":42,"move_id":248},{"level":51,"move_id":95},{"level":60,"move_id":138}]},"tmhm_learnset":"0041BF03B49BCE28","types":[14,14]},{"abilities":[69,0],"address":3307804,"base_stats":[45,75,60,50,40,30],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":30,"species":396}],"friendship":35,"id":395,"learnset":{"address":3317918,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":99},{"level":5,"move_id":44},{"level":9,"move_id":43},{"level":17,"move_id":29},{"level":21,"move_id":116},{"level":25,"move_id":52},{"level":33,"move_id":225},{"level":37,"move_id":184},{"level":41,"move_id":242},{"level":49,"move_id":337},{"level":53,"move_id":38}]},"tmhm_learnset":"00A41EE4C4130632","types":[16,16]},{"abilities":[69,0],"address":3307832,"base_stats":[65,95,100,50,60,50],"catch_rate":45,"evolutions":[{"method":"LEVEL","param":50,"species":397}],"friendship":35,"id":396,"learnset":{"address":3317948,"moves":[{"level":1,"move_id":99},{"level":1,"move_id":44},{"level":1,"move_id":43},{"level":1,"move_id":29},{"level":5,"move_id":44},{"level":9,"move_id":43},{"level":17,"move_id":29},{"level":21,"move_id":116},{"level":25,"move_id":52},{"level":30,"move_id":182},{"level":38,"move_id":225},{"level":47,"move_id":184},{"level":56,"move_id":242},{"level":69,"move_id":337},{"level":78,"move_id":38}]},"tmhm_learnset":"00A41EE4C4130632","types":[16,16]},{"abilities":[22,0],"address":3307860,"base_stats":[95,135,80,100,110,80],"catch_rate":45,"evolutions":[],"friendship":35,"id":397,"learnset":{"address":3317980,"moves":[{"level":1,"move_id":99},{"level":1,"move_id":44},{"level":1,"move_id":43},{"level":1,"move_id":29},{"level":5,"move_id":44},{"level":9,"move_id":43},{"level":17,"move_id":29},{"level":21,"move_id":116},{"level":25,"move_id":52},{"level":30,"move_id":182},{"level":38,"move_id":225},{"level":47,"move_id":184},{"level":50,"move_id":19},{"level":61,"move_id":242},{"level":79,"move_id":337},{"level":93,"move_id":38}]},"tmhm_learnset":"00AC5EE4C6534632","types":[16,2]},{"abilities":[29,0],"address":3307888,"base_stats":[40,55,80,30,35,60],"catch_rate":3,"evolutions":[{"method":"LEVEL","param":20,"species":399}],"friendship":35,"id":398,"learnset":{"address":3318014,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":36}]},"tmhm_learnset":"0000000000000000","types":[8,14]},{"abilities":[29,0],"address":3307916,"base_stats":[60,75,100,50,55,80],"catch_rate":3,"evolutions":[{"method":"LEVEL","param":45,"species":400}],"friendship":35,"id":399,"learnset":{"address":3318024,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":36},{"level":20,"move_id":93},{"level":20,"move_id":232},{"level":26,"move_id":184},{"level":32,"move_id":228},{"level":38,"move_id":94},{"level":44,"move_id":334},{"level":50,"move_id":309},{"level":56,"move_id":97},{"level":62,"move_id":63}]},"tmhm_learnset":"00E40ED9F613C620","types":[8,14]},{"abilities":[29,0],"address":3307944,"base_stats":[80,135,130,70,95,90],"catch_rate":3,"evolutions":[],"friendship":35,"id":400,"learnset":{"address":3318052,"moves":[{"level":1,"move_id":36},{"level":1,"move_id":93},{"level":1,"move_id":232},{"level":1,"move_id":184},{"level":20,"move_id":93},{"level":20,"move_id":232},{"level":26,"move_id":184},{"level":32,"move_id":228},{"level":38,"move_id":94},{"level":44,"move_id":334},{"level":55,"move_id":309},{"level":66,"move_id":97},{"level":77,"move_id":63}]},"tmhm_learnset":"00E40ED9F613C620","types":[8,14]},{"abilities":[29,0],"address":3307972,"base_stats":[80,100,200,50,50,100],"catch_rate":3,"evolutions":[],"friendship":35,"id":401,"learnset":{"address":3318080,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":88},{"level":1,"move_id":153},{"level":9,"move_id":88},{"level":17,"move_id":174},{"level":25,"move_id":276},{"level":33,"move_id":246},{"level":41,"move_id":334},{"level":49,"move_id":192},{"level":57,"move_id":199},{"level":65,"move_id":63}]},"tmhm_learnset":"00A00E52CF994621","types":[5,5]},{"abilities":[29,0],"address":3308000,"base_stats":[80,50,100,50,100,200],"catch_rate":3,"evolutions":[],"friendship":35,"id":402,"learnset":{"address":3318106,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":196},{"level":1,"move_id":153},{"level":9,"move_id":196},{"level":17,"move_id":174},{"level":25,"move_id":276},{"level":33,"move_id":246},{"level":41,"move_id":133},{"level":49,"move_id":192},{"level":57,"move_id":199},{"level":65,"move_id":63}]},"tmhm_learnset":"00A00E02C79B7261","types":[15,15]},{"abilities":[29,0],"address":3308028,"base_stats":[80,75,150,50,75,150],"catch_rate":3,"evolutions":[],"friendship":35,"id":403,"learnset":{"address":3318132,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":232},{"level":1,"move_id":153},{"level":9,"move_id":232},{"level":17,"move_id":174},{"level":25,"move_id":276},{"level":33,"move_id":246},{"level":41,"move_id":334},{"level":41,"move_id":133},{"level":49,"move_id":192},{"level":57,"move_id":199},{"level":65,"move_id":63}]},"tmhm_learnset":"00A00ED2C79B4621","types":[8,8]},{"abilities":[2,0],"address":3308056,"base_stats":[100,100,90,90,150,140],"catch_rate":5,"evolutions":[],"friendship":0,"id":404,"learnset":{"address":3318160,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":352},{"level":5,"move_id":184},{"level":15,"move_id":246},{"level":20,"move_id":34},{"level":30,"move_id":347},{"level":35,"move_id":58},{"level":45,"move_id":56},{"level":50,"move_id":156},{"level":60,"move_id":329},{"level":65,"move_id":38},{"level":75,"move_id":323}]},"tmhm_learnset":"03B00E42C79B727C","types":[11,11]},{"abilities":[70,0],"address":3308084,"base_stats":[100,150,140,90,100,90],"catch_rate":5,"evolutions":[],"friendship":0,"id":405,"learnset":{"address":3318190,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":341},{"level":5,"move_id":184},{"level":15,"move_id":246},{"level":20,"move_id":163},{"level":30,"move_id":339},{"level":35,"move_id":89},{"level":45,"move_id":126},{"level":50,"move_id":156},{"level":60,"move_id":90},{"level":65,"move_id":76},{"level":75,"move_id":284}]},"tmhm_learnset":"00A60EF6CFF946B2","types":[4,4]},{"abilities":[77,0],"address":3308112,"base_stats":[105,150,90,95,150,90],"catch_rate":3,"evolutions":[],"friendship":0,"id":406,"learnset":{"address":3318220,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":239},{"level":5,"move_id":184},{"level":15,"move_id":246},{"level":20,"move_id":337},{"level":30,"move_id":349},{"level":35,"move_id":242},{"level":45,"move_id":19},{"level":50,"move_id":156},{"level":60,"move_id":245},{"level":65,"move_id":200},{"level":75,"move_id":63}]},"tmhm_learnset":"03BA0EB6C7F376B6","types":[16,2]},{"abilities":[26,0],"address":3308140,"base_stats":[80,80,90,110,110,130],"catch_rate":3,"evolutions":[],"friendship":90,"id":407,"learnset":{"address":3318250,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":149},{"level":5,"move_id":273},{"level":10,"move_id":270},{"level":15,"move_id":219},{"level":20,"move_id":225},{"level":25,"move_id":346},{"level":30,"move_id":287},{"level":35,"move_id":296},{"level":40,"move_id":94},{"level":45,"move_id":105},{"level":50,"move_id":204}]},"tmhm_learnset":"035C5E93B7BBD63E","types":[16,14]},{"abilities":[26,0],"address":3308168,"base_stats":[80,90,80,110,130,110],"catch_rate":3,"evolutions":[],"friendship":90,"id":408,"learnset":{"address":3318280,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":149},{"level":5,"move_id":262},{"level":10,"move_id":270},{"level":15,"move_id":219},{"level":20,"move_id":225},{"level":25,"move_id":182},{"level":30,"move_id":287},{"level":35,"move_id":295},{"level":40,"move_id":94},{"level":45,"move_id":105},{"level":50,"move_id":349}]},"tmhm_learnset":"035C5E93B7BBD63E","types":[16,14]},{"abilities":[32,0],"address":3308196,"base_stats":[100,100,100,100,100,100],"catch_rate":3,"evolutions":[],"friendship":100,"id":409,"learnset":{"address":3318310,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":273},{"level":1,"move_id":93},{"level":5,"move_id":156},{"level":10,"move_id":129},{"level":15,"move_id":270},{"level":20,"move_id":94},{"level":25,"move_id":287},{"level":30,"move_id":156},{"level":35,"move_id":38},{"level":40,"move_id":248},{"level":45,"move_id":322},{"level":50,"move_id":353}]},"tmhm_learnset":"00408E93B59BC62C","types":[8,14]},{"abilities":[46,0],"address":3308224,"base_stats":[50,150,50,150,150,50],"catch_rate":3,"evolutions":[],"friendship":0,"id":410,"learnset":{"address":3318340,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":43},{"level":1,"move_id":35},{"level":5,"move_id":101},{"level":10,"move_id":104},{"level":15,"move_id":282},{"level":20,"move_id":228},{"level":25,"move_id":94},{"level":30,"move_id":129},{"level":35,"move_id":97},{"level":40,"move_id":105},{"level":45,"move_id":354},{"level":50,"move_id":245}]},"tmhm_learnset":"00E58FC3F5BBDE2D","types":[14,14]},{"abilities":[26,0],"address":3308252,"base_stats":[65,50,70,65,95,80],"catch_rate":45,"evolutions":[],"friendship":70,"id":411,"learnset":{"address":3318370,"moves":[{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":0},{"level":1,"move_id":35},{"level":6,"move_id":45},{"level":9,"move_id":310},{"level":14,"move_id":93},{"level":17,"move_id":36},{"level":22,"move_id":253},{"level":25,"move_id":281},{"level":30,"move_id":149},{"level":33,"move_id":38},{"level":38,"move_id":215},{"level":41,"move_id":219},{"level":46,"move_id":94}]},"tmhm_learnset":"00419F03B41B8E28","types":[14,14]}],"tmhm_moves":[264,337,352,347,46,92,258,339,331,237,241,269,58,59,63,113,182,240,202,219,218,76,231,85,87,89,216,91,94,247,280,104,115,351,53,188,201,126,317,332,259,263,290,156,213,168,211,285,289,315,15,19,57,70,148,249,127,291],"trainers":[{"address":3230072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[],"party_address":4160749568,"script_address":0},{"address":3230112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":21,"species":74}],"party_address":3211124,"script_address":2304511},{"address":3230152,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":286}],"party_address":3211132,"script_address":2321901},{"address":3230192,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":41},{"level":31,"species":330}],"party_address":3211140,"script_address":2323326},{"address":3230232,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":41}],"party_address":3211156,"script_address":2323373},{"address":3230272,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":330}],"party_address":3211164,"script_address":2324386},{"address":3230312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":286}],"party_address":3211172,"script_address":2326808},{"address":3230352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":330}],"party_address":3211180,"script_address":2326839},{"address":3230392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":41}],"party_address":3211188,"script_address":2328040},{"address":3230432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":315},{"level":26,"species":286},{"level":26,"species":288},{"level":26,"species":295},{"level":26,"species":298},{"level":26,"species":304}],"party_address":3211196,"script_address":2314251},{"address":3230472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":9,"species":286}],"party_address":3211244,"script_address":0},{"address":3230512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":338},{"level":29,"species":300}],"party_address":3211252,"script_address":2067580},{"address":3230552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":310},{"level":30,"species":178}],"party_address":3211268,"script_address":2068523},{"address":3230592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":380},{"level":30,"species":379}],"party_address":3211284,"script_address":2068554},{"address":3230632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":330}],"party_address":3211300,"script_address":2328071},{"address":3230672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":130}],"party_address":3211308,"script_address":2069620},{"address":3230712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":11,"species":286}],"party_address":3211316,"script_address":0},{"address":3230752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":41},{"level":27,"species":286}],"party_address":3211324,"script_address":2570959},{"address":3230792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":286},{"level":27,"species":330}],"party_address":3211340,"script_address":2572093},{"address":3230832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":286},{"level":26,"species":41},{"level":26,"species":330}],"party_address":3211356,"script_address":2572124},{"address":3230872,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":15,"species":330}],"party_address":3211380,"script_address":2157889},{"address":3230912,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":14,"species":41},{"level":14,"species":330}],"party_address":3211388,"script_address":2157948},{"address":3230952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":339}],"party_address":3211404,"script_address":2254636},{"address":3230992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":41}],"party_address":3211412,"script_address":2317522},{"address":3231032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":330}],"party_address":3211420,"script_address":2317553},{"address":3231072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":286},{"level":30,"species":330}],"party_address":3211428,"script_address":2317584},{"address":3231112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":330}],"party_address":3211444,"script_address":2570990},{"address":3231152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":330}],"party_address":3211452,"script_address":2323414},{"address":3231192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":41}],"party_address":3211460,"script_address":2324427},{"address":3231232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":335},{"level":30,"species":67}],"party_address":3211468,"script_address":2068492},{"address":3231272,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":287},{"level":34,"species":42}],"party_address":3211484,"script_address":2324250},{"address":3231312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":336}],"party_address":3211500,"script_address":2312702},{"address":3231352,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":330},{"level":28,"species":287}],"party_address":3211508,"script_address":2572155},{"address":3231392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":331},{"level":37,"species":287}],"party_address":3211524,"script_address":2327156},{"address":3231432,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":287},{"level":41,"species":169},{"level":43,"species":331}],"party_address":3211540,"script_address":2328478},{"address":3231472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":351}],"party_address":3211564,"script_address":2312671},{"address":3231512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":14,"species":306},{"level":14,"species":363}],"party_address":3211572,"script_address":2026085},{"address":3231552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":14,"species":363},{"level":14,"species":306},{"level":14,"species":363}],"party_address":3211588,"script_address":2058784},{"address":3231592,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":43,"moves":[94,0,0,0],"species":357},{"level":43,"moves":[29,89,0,0],"species":319}],"party_address":3211612,"script_address":2335547},{"address":3231632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":363},{"level":26,"species":44}],"party_address":3211644,"script_address":2068148},{"address":3231672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":306},{"level":26,"species":363}],"party_address":3211660,"script_address":0},{"address":3231712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":306},{"level":28,"species":44},{"level":28,"species":363}],"party_address":3211676,"script_address":0},{"address":3231752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":306},{"level":31,"species":44},{"level":31,"species":363}],"party_address":3211700,"script_address":0},{"address":3231792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":307},{"level":34,"species":44},{"level":34,"species":363}],"party_address":3211724,"script_address":0},{"address":3231832,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":23,"moves":[91,163,28,40],"species":28}],"party_address":3211748,"script_address":2046490},{"address":3231872,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":27,"moves":[60,120,201,246],"species":318},{"level":27,"moves":[91,163,28,40],"species":27},{"level":27,"moves":[91,163,28,40],"species":28}],"party_address":3211764,"script_address":2065682},{"address":3231912,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":25,"moves":[91,163,28,40],"species":27},{"level":25,"moves":[91,163,28,40],"species":28}],"party_address":3211812,"script_address":2033540},{"address":3231952,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":27,"moves":[91,163,28,40],"species":28}],"party_address":3211844,"script_address":0},{"address":3231992,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":30,"moves":[91,163,28,40],"species":28}],"party_address":3211860,"script_address":0},{"address":3232032,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":33,"moves":[91,163,28,40],"species":28}],"party_address":3211876,"script_address":0},{"address":3232072,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":36,"moves":[91,163,28,40],"species":28}],"party_address":3211892,"script_address":0},{"address":3232112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":81},{"level":17,"species":370}],"party_address":3211908,"script_address":0},{"address":3232152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":81},{"level":27,"species":371}],"party_address":3211924,"script_address":0},{"address":3232192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":82},{"level":30,"species":371}],"party_address":3211940,"script_address":0},{"address":3232232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":82},{"level":33,"species":371}],"party_address":3211956,"script_address":0},{"address":3232272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":82},{"level":36,"species":371}],"party_address":3211972,"script_address":0},{"address":3232312,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":39,"moves":[49,86,63,85],"species":82},{"level":39,"moves":[54,23,48,48],"species":372}],"party_address":3211988,"script_address":0},{"address":3232352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":12,"species":350},{"level":12,"species":350}],"party_address":3212020,"script_address":2036011},{"address":3232392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":183}],"party_address":3212036,"script_address":2036121},{"address":3232432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":183}],"party_address":3212044,"script_address":2036152},{"address":3232472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":183},{"level":26,"species":183}],"party_address":3212052,"script_address":0},{"address":3232512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":183},{"level":29,"species":183}],"party_address":3212068,"script_address":0},{"address":3232552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":183},{"level":32,"species":183}],"party_address":3212084,"script_address":0},{"address":3232592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":184},{"level":35,"species":184}],"party_address":3212100,"script_address":0},{"address":3232632,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":13,"moves":[28,29,39,57],"species":288}],"party_address":3212116,"script_address":2035901},{"address":3232672,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":12,"species":350},{"level":12,"species":183}],"party_address":3212132,"script_address":2544001},{"address":3232712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":183}],"party_address":3212148,"script_address":2339831},{"address":3232752,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":27,"moves":[28,42,39,57],"species":289}],"party_address":3212156,"script_address":0},{"address":3232792,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":30,"moves":[28,42,39,57],"species":289}],"party_address":3212172,"script_address":0},{"address":3232832,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":33,"moves":[28,42,39,57],"species":289}],"party_address":3212188,"script_address":0},{"address":3232872,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":36,"moves":[28,42,39,57],"species":289}],"party_address":3212204,"script_address":0},{"address":3232912,"battle_type":2,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":26,"moves":[98,97,17,0],"species":305}],"party_address":3212220,"script_address":2131164},{"address":3232952,"battle_type":2,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":26,"moves":[42,146,8,0],"species":308}],"party_address":3212236,"script_address":2131228},{"address":3232992,"battle_type":2,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":26,"moves":[47,68,247,0],"species":364}],"party_address":3212252,"script_address":2131292},{"address":3233032,"battle_type":2,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":26,"moves":[116,163,0,0],"species":365}],"party_address":3212268,"script_address":2131356},{"address":3233072,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":28,"moves":[116,98,17,27],"species":305},{"level":28,"moves":[44,91,185,72],"species":332},{"level":28,"moves":[205,250,54,96],"species":313},{"level":28,"moves":[85,48,86,49],"species":82},{"level":28,"moves":[202,185,104,207],"species":300}],"party_address":3212284,"script_address":2068117},{"address":3233112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":44,"species":322},{"level":44,"species":357},{"level":44,"species":331}],"party_address":3212364,"script_address":2565920},{"address":3233152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":46,"species":355},{"level":46,"species":121}],"party_address":3212388,"script_address":2565982},{"address":3233192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":337},{"level":17,"species":313},{"level":17,"species":335}],"party_address":3212404,"script_address":2046693},{"address":3233232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":43,"species":345},{"level":43,"species":310}],"party_address":3212428,"script_address":2332685},{"address":3233272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":43,"species":82},{"level":43,"species":89}],"party_address":3212444,"script_address":2332716},{"address":3233312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":42,"species":305},{"level":42,"species":355},{"level":42,"species":64}],"party_address":3212460,"script_address":2334375},{"address":3233352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":42,"species":85},{"level":42,"species":64},{"level":42,"species":101},{"level":42,"species":300}],"party_address":3212484,"script_address":2335423},{"address":3233392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":42,"species":317},{"level":42,"species":75},{"level":42,"species":314}],"party_address":3212516,"script_address":2335454},{"address":3233432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":337},{"level":26,"species":313},{"level":26,"species":335}],"party_address":3212540,"script_address":0},{"address":3233472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":338},{"level":29,"species":313},{"level":29,"species":335}],"party_address":3212564,"script_address":0},{"address":3233512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":338},{"level":32,"species":313},{"level":32,"species":335}],"party_address":3212588,"script_address":0},{"address":3233552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":338},{"level":35,"species":313},{"level":35,"species":336}],"party_address":3212612,"script_address":0},{"address":3233592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":75},{"level":33,"species":297}],"party_address":3212636,"script_address":2073950},{"address":3233632,"battle_type":2,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":26,"moves":[185,95,0,0],"species":316}],"party_address":3212652,"script_address":2131420},{"address":3233672,"battle_type":2,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":26,"moves":[111,38,247,0],"species":40}],"party_address":3212668,"script_address":2131484},{"address":3233712,"battle_type":2,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":26,"moves":[14,163,0,0],"species":380}],"party_address":3212684,"script_address":2131548},{"address":3233752,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":29,"moves":[226,185,57,44],"species":355},{"level":29,"moves":[72,89,64,73],"species":363},{"level":29,"moves":[19,55,54,182],"species":310}],"party_address":3212700,"script_address":2068086},{"address":3233792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":45,"species":383},{"level":45,"species":338}],"party_address":3212748,"script_address":2565951},{"address":3233832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":309},{"level":17,"species":339},{"level":17,"species":363}],"party_address":3212764,"script_address":2046803},{"address":3233872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":322}],"party_address":3212788,"script_address":2065651},{"address":3233912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":45,"species":363}],"party_address":3212796,"script_address":2332747},{"address":3233952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":45,"species":319}],"party_address":3212804,"script_address":2334406},{"address":3233992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":42,"species":321},{"level":42,"species":357},{"level":42,"species":297}],"party_address":3212812,"script_address":2334437},{"address":3234032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":43,"species":227},{"level":43,"species":322}],"party_address":3212836,"script_address":2335485},{"address":3234072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":42,"species":28},{"level":42,"species":38},{"level":42,"species":369}],"party_address":3212852,"script_address":2335516},{"address":3234112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":309},{"level":26,"species":339},{"level":26,"species":363}],"party_address":3212876,"script_address":0},{"address":3234152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":310},{"level":29,"species":339},{"level":29,"species":363}],"party_address":3212900,"script_address":0},{"address":3234192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":310},{"level":32,"species":339},{"level":32,"species":363}],"party_address":3212924,"script_address":0},{"address":3234232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":310},{"level":34,"species":340},{"level":34,"species":363}],"party_address":3212948,"script_address":0},{"address":3234272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":378},{"level":41,"species":348}],"party_address":3212972,"script_address":2564729},{"address":3234312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":361},{"level":30,"species":377}],"party_address":3212988,"script_address":2068461},{"address":3234352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":361},{"level":29,"species":377}],"party_address":3213004,"script_address":2067284},{"address":3234392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":322}],"party_address":3213020,"script_address":2315745},{"address":3234432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":377}],"party_address":3213028,"script_address":2315532},{"address":3234472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":322},{"level":31,"species":351}],"party_address":3213036,"script_address":0},{"address":3234512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":351},{"level":35,"species":322}],"party_address":3213052,"script_address":0},{"address":3234552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":40,"species":351},{"level":40,"species":322}],"party_address":3213068,"script_address":0},{"address":3234592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":42,"species":361},{"level":42,"species":322},{"level":42,"species":352}],"party_address":3213084,"script_address":0},{"address":3234632,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":7,"species":288}],"party_address":3213108,"script_address":2030087},{"address":3234672,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":39,"moves":[213,186,175,96],"species":325},{"level":39,"moves":[213,219,36,96],"species":325}],"party_address":3213116,"script_address":2265894},{"address":3234712,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":287},{"level":28,"species":287},{"level":30,"species":339}],"party_address":3213148,"script_address":2254717},{"address":3234752,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":11,"moves":[33,39,0,0],"species":288}],"party_address":3213172,"script_address":0},{"address":3234792,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":40,"species":119}],"party_address":3213188,"script_address":2265677},{"address":3234832,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":45,"species":363}],"party_address":3213196,"script_address":2361019},{"address":3234872,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":27,"species":289}],"party_address":3213204,"script_address":0},{"address":3234912,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":30,"species":289}],"party_address":3213212,"script_address":0},{"address":3234952,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":33,"species":289}],"party_address":3213220,"script_address":0},{"address":3234992,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":36,"moves":[154,44,60,28],"species":289}],"party_address":3213228,"script_address":0},{"address":3235032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":21,"species":183}],"party_address":3213244,"script_address":2304387},{"address":3235072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":21,"species":306}],"party_address":3213252,"script_address":2304418},{"address":3235112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":21,"species":339}],"party_address":3213260,"script_address":2304449},{"address":3235152,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":29,"moves":[20,122,154,185],"species":317},{"level":29,"moves":[86,103,137,242],"species":379}],"party_address":3213268,"script_address":2067377},{"address":3235192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":40,"species":118}],"party_address":3213300,"script_address":2265708},{"address":3235232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":40,"species":184}],"party_address":3213308,"script_address":2265739},{"address":3235272,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":35,"moves":[78,250,240,96],"species":373},{"level":37,"moves":[13,152,96,0],"species":326},{"level":39,"moves":[253,154,252,96],"species":296}],"party_address":3213316,"script_address":2265770},{"address":3235312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":330},{"level":39,"species":331}],"party_address":3213364,"script_address":2265801},{"address":3235352,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":35,"moves":[20,122,154,185],"species":317},{"level":35,"moves":[86,103,137,242],"species":379}],"party_address":3213380,"script_address":0},{"address":3235392,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":38,"moves":[20,122,154,185],"species":317},{"level":38,"moves":[86,103,137,242],"species":379}],"party_address":3213412,"script_address":0},{"address":3235432,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":41,"moves":[20,122,154,185],"species":317},{"level":41,"moves":[86,103,137,242],"species":379}],"party_address":3213444,"script_address":0},{"address":3235472,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":44,"moves":[20,122,154,185],"species":317},{"level":44,"moves":[86,103,137,242],"species":379}],"party_address":3213476,"script_address":0},{"address":3235512,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":7,"species":288}],"party_address":3213508,"script_address":2029901},{"address":3235552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":324},{"level":33,"species":356}],"party_address":3213516,"script_address":2074012},{"address":3235592,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":45,"species":184}],"party_address":3213532,"script_address":2360988},{"address":3235632,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":27,"species":289}],"party_address":3213540,"script_address":0},{"address":3235672,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":30,"species":289}],"party_address":3213548,"script_address":0},{"address":3235712,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":33,"species":289}],"party_address":3213556,"script_address":0},{"address":3235752,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":36,"moves":[154,44,60,28],"species":289}],"party_address":3213564,"script_address":0},{"address":3235792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":19,"species":382}],"party_address":3213580,"script_address":2051965},{"address":3235832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":313},{"level":25,"species":116}],"party_address":3213588,"script_address":2340108},{"address":3235872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":111}],"party_address":3213604,"script_address":2312578},{"address":3235912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":20,"species":339}],"party_address":3213612,"script_address":2304480},{"address":3235952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":383}],"party_address":3213620,"script_address":0},{"address":3235992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":383},{"level":29,"species":111}],"party_address":3213628,"script_address":0},{"address":3236032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":383},{"level":32,"species":111}],"party_address":3213644,"script_address":0},{"address":3236072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":384},{"level":35,"species":112}],"party_address":3213660,"script_address":0},{"address":3236112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":330}],"party_address":3213676,"script_address":2033571},{"address":3236152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":72}],"party_address":3213684,"script_address":2033602},{"address":3236192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":24,"species":72},{"level":24,"species":72}],"party_address":3213692,"script_address":2034185},{"address":3236232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":24,"species":72},{"level":24,"species":309},{"level":24,"species":72}],"party_address":3213708,"script_address":2034479},{"address":3236272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":330}],"party_address":3213732,"script_address":2034510},{"address":3236312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":73}],"party_address":3213740,"script_address":2034776},{"address":3236352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":330}],"party_address":3213748,"script_address":2034807},{"address":3236392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":72},{"level":25,"species":330}],"party_address":3213756,"script_address":2035777},{"address":3236432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":72},{"level":33,"species":309}],"party_address":3213772,"script_address":2069178},{"address":3236472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":330}],"party_address":3213788,"script_address":2069209},{"address":3236512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":73}],"party_address":3213796,"script_address":2069789},{"address":3236552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":116}],"party_address":3213804,"script_address":2069820},{"address":3236592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":130}],"party_address":3213812,"script_address":2070163},{"address":3236632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":330},{"level":31,"species":309},{"level":31,"species":330}],"party_address":3213820,"script_address":2070194},{"address":3236672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":130}],"party_address":3213844,"script_address":2073229},{"address":3236712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":310}],"party_address":3213852,"script_address":2073359},{"address":3236752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":309},{"level":33,"species":73}],"party_address":3213860,"script_address":2073390},{"address":3236792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":73},{"level":33,"species":313}],"party_address":3213876,"script_address":2073291},{"address":3236832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":331}],"party_address":3213892,"script_address":2073608},{"address":3236872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":342}],"party_address":3213900,"script_address":2073857},{"address":3236912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":341}],"party_address":3213908,"script_address":2073576},{"address":3236952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":130}],"party_address":3213916,"script_address":2074089},{"address":3236992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":72},{"level":33,"species":309},{"level":33,"species":73}],"party_address":3213924,"script_address":0},{"address":3237032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":72},{"level":33,"species":313}],"party_address":3213948,"script_address":2069381},{"address":3237072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":331}],"party_address":3213964,"script_address":0},{"address":3237112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":331}],"party_address":3213972,"script_address":0},{"address":3237152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":120},{"level":36,"species":331}],"party_address":3213980,"script_address":0},{"address":3237192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":121},{"level":39,"species":331}],"party_address":3213996,"script_address":0},{"address":3237232,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":66}],"party_address":3214012,"script_address":2095275},{"address":3237272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":66},{"level":32,"species":67}],"party_address":3214020,"script_address":2074213},{"address":3237312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":336}],"party_address":3214036,"script_address":2073701},{"address":3237352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":24,"species":66},{"level":28,"species":67}],"party_address":3214044,"script_address":2052921},{"address":3237392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":19,"species":66}],"party_address":3214060,"script_address":2052952},{"address":3237432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":67}],"party_address":3214068,"script_address":0},{"address":3237472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":66},{"level":29,"species":67}],"party_address":3214076,"script_address":0},{"address":3237512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":66},{"level":31,"species":67},{"level":31,"species":67}],"party_address":3214092,"script_address":0},{"address":3237552,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":33,"species":66},{"level":33,"species":67},{"level":33,"species":67},{"level":33,"species":68}],"party_address":3214116,"script_address":0},{"address":3237592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":335},{"level":26,"species":67}],"party_address":3214148,"script_address":2557758},{"address":3237632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":19,"species":66}],"party_address":3214164,"script_address":2046662},{"address":3237672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":336}],"party_address":3214172,"script_address":2315359},{"address":3237712,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":17,"moves":[98,86,209,43],"species":337},{"level":17,"moves":[12,95,103,0],"species":100}],"party_address":3214180,"script_address":2167608},{"address":3237752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":286},{"level":31,"species":41}],"party_address":3214212,"script_address":2323445},{"address":3237792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":330}],"party_address":3214228,"script_address":2324458},{"address":3237832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":100},{"level":17,"species":81}],"party_address":3214236,"script_address":2167639},{"address":3237872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":337},{"level":30,"species":371}],"party_address":3214252,"script_address":2068709},{"address":3237912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":15,"species":81},{"level":15,"species":370}],"party_address":3214268,"script_address":2058956},{"address":3237952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":81},{"level":25,"species":370},{"level":25,"species":81}],"party_address":3214284,"script_address":0},{"address":3237992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":81},{"level":28,"species":371},{"level":28,"species":81}],"party_address":3214308,"script_address":0},{"address":3238032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":82},{"level":31,"species":371},{"level":31,"species":82}],"party_address":3214332,"script_address":0},{"address":3238072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":82},{"level":34,"species":372},{"level":34,"species":82}],"party_address":3214356,"script_address":0},{"address":3238112,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":23,"species":339}],"party_address":3214380,"script_address":2103394},{"address":3238152,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":22,"species":218},{"level":22,"species":218}],"party_address":3214388,"script_address":2103601},{"address":3238192,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":23,"species":339}],"party_address":3214404,"script_address":2103446},{"address":3238232,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":23,"species":218}],"party_address":3214412,"script_address":2103570},{"address":3238272,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":23,"species":218}],"party_address":3214420,"script_address":2103477},{"address":3238312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":218},{"level":18,"species":309}],"party_address":3214428,"script_address":2052075},{"address":3238352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":218},{"level":26,"species":309}],"party_address":3214444,"script_address":0},{"address":3238392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":218},{"level":29,"species":310}],"party_address":3214460,"script_address":0},{"address":3238432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":218},{"level":32,"species":310}],"party_address":3214476,"script_address":0},{"address":3238472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":219},{"level":35,"species":310}],"party_address":3214492,"script_address":0},{"address":3238512,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":23,"moves":[91,28,40,163],"species":27}],"party_address":3214508,"script_address":2046366},{"address":3238552,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":21,"moves":[229,189,60,61],"species":318},{"level":21,"moves":[40,28,10,91],"species":27},{"level":21,"moves":[229,189,60,61],"species":318}],"party_address":3214524,"script_address":2046428},{"address":3238592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":299}],"party_address":3214572,"script_address":2049829},{"address":3238632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":27},{"level":18,"species":299}],"party_address":3214580,"script_address":2051903},{"address":3238672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":24,"species":317}],"party_address":3214596,"script_address":2557005},{"address":3238712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":20,"species":288},{"level":20,"species":304}],"party_address":3214604,"script_address":2310199},{"address":3238752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":21,"species":306}],"party_address":3214620,"script_address":2310337},{"address":3238792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":27}],"party_address":3214628,"script_address":2046600},{"address":3238832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":288},{"level":26,"species":304}],"party_address":3214636,"script_address":0},{"address":3238872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":289},{"level":29,"species":305}],"party_address":3214652,"script_address":0},{"address":3238912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":27},{"level":31,"species":305},{"level":31,"species":289}],"party_address":3214668,"script_address":0},{"address":3238952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":305},{"level":34,"species":28},{"level":34,"species":289}],"party_address":3214692,"script_address":0},{"address":3238992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":311}],"party_address":3214716,"script_address":2061044},{"address":3239032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":24,"species":290},{"level":24,"species":291},{"level":24,"species":292}],"party_address":3214724,"script_address":2061075},{"address":3239072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":290},{"level":27,"species":293},{"level":27,"species":294}],"party_address":3214748,"script_address":2061106},{"address":3239112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":311},{"level":27,"species":311},{"level":27,"species":311}],"party_address":3214772,"script_address":2065541},{"address":3239152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":16,"species":294},{"level":16,"species":292}],"party_address":3214796,"script_address":2057595},{"address":3239192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":311},{"level":31,"species":311},{"level":31,"species":311}],"party_address":3214812,"script_address":0},{"address":3239232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":311},{"level":34,"species":311},{"level":34,"species":312}],"party_address":3214836,"script_address":0},{"address":3239272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":311},{"level":36,"species":290},{"level":36,"species":311},{"level":36,"species":312}],"party_address":3214860,"script_address":0},{"address":3239312,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":38,"species":311},{"level":38,"species":294},{"level":38,"species":311},{"level":38,"species":312},{"level":38,"species":292}],"party_address":3214892,"script_address":0},{"address":3239352,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":15,"moves":[237,0,0,0],"species":63}],"party_address":3214932,"script_address":2038374},{"address":3239392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":393}],"party_address":3214948,"script_address":2244488},{"address":3239432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":392}],"party_address":3214956,"script_address":2244519},{"address":3239472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":203}],"party_address":3214964,"script_address":2244550},{"address":3239512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":392},{"level":26,"species":392},{"level":26,"species":393}],"party_address":3214972,"script_address":2314189},{"address":3239552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":64},{"level":41,"species":349}],"party_address":3214996,"script_address":2564698},{"address":3239592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":349}],"party_address":3215012,"script_address":2068179},{"address":3239632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":64},{"level":33,"species":349}],"party_address":3215020,"script_address":0},{"address":3239672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":38,"species":64},{"level":38,"species":349}],"party_address":3215036,"script_address":0},{"address":3239712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":64},{"level":41,"species":349}],"party_address":3215052,"script_address":0},{"address":3239752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":45,"species":349},{"level":45,"species":65}],"party_address":3215068,"script_address":0},{"address":3239792,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":16,"moves":[237,0,0,0],"species":63}],"party_address":3215084,"script_address":2038405},{"address":3239832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":393}],"party_address":3215100,"script_address":2244581},{"address":3239872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":178}],"party_address":3215108,"script_address":2244612},{"address":3239912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":64}],"party_address":3215116,"script_address":2244643},{"address":3239952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":202},{"level":26,"species":177},{"level":26,"species":64}],"party_address":3215124,"script_address":2314220},{"address":3239992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":393},{"level":41,"species":178}],"party_address":3215148,"script_address":2564760},{"address":3240032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":64},{"level":30,"species":348}],"party_address":3215164,"script_address":2068289},{"address":3240072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":64},{"level":34,"species":348}],"party_address":3215180,"script_address":0},{"address":3240112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":64},{"level":37,"species":348}],"party_address":3215196,"script_address":0},{"address":3240152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":40,"species":64},{"level":40,"species":348}],"party_address":3215212,"script_address":0},{"address":3240192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":43,"species":348},{"level":43,"species":65}],"party_address":3215228,"script_address":0},{"address":3240232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":338}],"party_address":3215244,"script_address":2067174},{"address":3240272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":44,"species":338},{"level":44,"species":338}],"party_address":3215252,"script_address":2360864},{"address":3240312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":45,"species":380}],"party_address":3215268,"script_address":2360895},{"address":3240352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":338}],"party_address":3215276,"script_address":0},{"address":3240392,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":36,"moves":[29,28,60,154],"species":289},{"level":36,"moves":[98,209,60,46],"species":338}],"party_address":3215284,"script_address":0},{"address":3240432,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":39,"moves":[29,28,60,154],"species":289},{"level":39,"moves":[98,209,60,0],"species":338}],"party_address":3215316,"script_address":0},{"address":3240472,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":41,"moves":[29,28,60,154],"species":289},{"level":41,"moves":[154,50,93,244],"species":55},{"level":41,"moves":[98,209,60,46],"species":338}],"party_address":3215348,"script_address":0},{"address":3240512,"battle_type":3,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":46,"moves":[46,38,28,242],"species":287},{"level":48,"moves":[3,104,207,70],"species":300},{"level":46,"moves":[73,185,46,178],"species":345},{"level":48,"moves":[57,14,70,7],"species":327},{"level":49,"moves":[76,157,14,163],"species":376}],"party_address":3215396,"script_address":2274753},{"address":3240552,"battle_type":3,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":48,"moves":[69,109,174,182],"species":362},{"level":49,"moves":[247,32,5,185],"species":378},{"level":50,"moves":[247,104,101,185],"species":322},{"level":49,"moves":[247,94,85,7],"species":378},{"level":51,"moves":[247,58,157,89],"species":362}],"party_address":3215476,"script_address":2275380},{"address":3240592,"battle_type":3,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":50,"moves":[227,34,2,45],"species":342},{"level":50,"moves":[113,242,196,58],"species":347},{"level":52,"moves":[213,38,2,59],"species":342},{"level":52,"moves":[247,153,2,58],"species":347},{"level":53,"moves":[57,34,58,73],"species":343}],"party_address":3215556,"script_address":2276062},{"address":3240632,"battle_type":3,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":52,"moves":[61,81,182,38],"species":396},{"level":54,"moves":[38,225,93,76],"species":359},{"level":53,"moves":[108,93,57,34],"species":230},{"level":53,"moves":[53,242,225,89],"species":334},{"level":55,"moves":[53,81,157,242],"species":397}],"party_address":3215636,"script_address":2276724},{"address":3240672,"battle_type":1,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":12,"moves":[33,111,88,61],"species":74},{"level":12,"moves":[33,111,88,61],"species":74},{"level":15,"moves":[79,106,33,61],"species":320}],"party_address":3215716,"script_address":2187976},{"address":3240712,"battle_type":1,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":16,"moves":[2,67,69,83],"species":66},{"level":16,"moves":[8,113,115,83],"species":356},{"level":19,"moves":[36,233,179,83],"species":335}],"party_address":3215764,"script_address":2095066},{"address":3240752,"battle_type":1,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":20,"moves":[205,209,120,95],"species":100},{"level":20,"moves":[95,43,98,80],"species":337},{"level":22,"moves":[48,95,86,49],"species":82},{"level":24,"moves":[98,86,95,80],"species":338}],"party_address":3215812,"script_address":2167181},{"address":3240792,"battle_type":1,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":24,"moves":[59,36,222,241],"species":339},{"level":24,"moves":[59,123,113,241],"species":218},{"level":26,"moves":[59,33,241,213],"species":340},{"level":29,"moves":[59,241,34,213],"species":321}],"party_address":3215876,"script_address":2103186},{"address":3240832,"battle_type":3,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":27,"moves":[42,60,7,227],"species":308},{"level":27,"moves":[163,7,227,185],"species":365},{"level":29,"moves":[163,187,7,29],"species":289},{"level":31,"moves":[68,25,7,185],"species":366}],"party_address":3215940,"script_address":2129756},{"address":3240872,"battle_type":1,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":29,"moves":[195,119,219,76],"species":358},{"level":29,"moves":[241,76,76,235],"species":369},{"level":30,"moves":[55,48,182,76],"species":310},{"level":31,"moves":[28,31,211,76],"species":227},{"level":33,"moves":[89,225,93,76],"species":359}],"party_address":3216004,"script_address":2202062},{"address":3240912,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":41,"moves":[89,246,94,113],"species":319},{"level":41,"moves":[94,241,109,91],"species":178},{"level":42,"moves":[113,94,95,91],"species":348},{"level":42,"moves":[241,76,94,53],"species":349}],"party_address":3216084,"script_address":0},{"address":3240952,"battle_type":1,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":41,"moves":[96,213,186,175],"species":325},{"level":41,"moves":[240,96,133,89],"species":324},{"level":43,"moves":[227,34,62,96],"species":342},{"level":43,"moves":[96,152,13,43],"species":327},{"level":46,"moves":[96,104,58,156],"species":230}],"party_address":3216148,"script_address":2262245},{"address":3240992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":9,"species":392}],"party_address":3216228,"script_address":2054242},{"address":3241032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":392}],"party_address":3216236,"script_address":2554598},{"address":3241072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":15,"species":339},{"level":15,"species":43},{"level":15,"species":309}],"party_address":3216244,"script_address":2554629},{"address":3241112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":392},{"level":26,"species":356}],"party_address":3216268,"script_address":0},{"address":3241152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":393},{"level":29,"species":356}],"party_address":3216284,"script_address":0},{"address":3241192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":393},{"level":32,"species":357}],"party_address":3216300,"script_address":0},{"address":3241232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":393},{"level":34,"species":378},{"level":34,"species":357}],"party_address":3216316,"script_address":0},{"address":3241272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":9,"species":306}],"party_address":3216340,"script_address":2054490},{"address":3241312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":16,"species":306},{"level":16,"species":292}],"party_address":3216348,"script_address":2554660},{"address":3241352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":306},{"level":26,"species":370}],"party_address":3216364,"script_address":0},{"address":3241392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":306},{"level":29,"species":371}],"party_address":3216380,"script_address":0},{"address":3241432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":307},{"level":32,"species":371}],"party_address":3216396,"script_address":0},{"address":3241472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":307},{"level":35,"species":372}],"party_address":3216412,"script_address":0},{"address":3241512,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":30,"moves":[95,60,146,42],"species":308},{"level":32,"moves":[8,25,47,185],"species":366}],"party_address":3216428,"script_address":0},{"address":3241552,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":15,"moves":[45,39,29,60],"species":288},{"level":17,"moves":[33,116,36,0],"species":335}],"party_address":3216460,"script_address":0},{"address":3241592,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":28,"moves":[45,39,29,60],"species":288},{"level":30,"moves":[33,116,36,0],"species":335}],"party_address":3216492,"script_address":0},{"address":3241632,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":31,"moves":[45,39,29,60],"species":288},{"level":33,"moves":[33,116,36,0],"species":335}],"party_address":3216524,"script_address":0},{"address":3241672,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":34,"moves":[45,39,29,60],"species":289},{"level":36,"moves":[33,116,36,0],"species":335}],"party_address":3216556,"script_address":0},{"address":3241712,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":36,"moves":[45,39,29,60],"species":289},{"level":38,"moves":[33,116,36,0],"species":336}],"party_address":3216588,"script_address":0},{"address":3241752,"battle_type":3,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":16,"species":304},{"level":16,"species":288}],"party_address":3216620,"script_address":2045785},{"address":3241792,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":15,"species":315}],"party_address":3216636,"script_address":2026353},{"address":3241832,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":22,"moves":[18,204,185,215],"species":315},{"level":36,"moves":[18,204,185,215],"species":315},{"level":40,"moves":[18,204,185,215],"species":315},{"level":12,"moves":[18,204,185,215],"species":315},{"level":30,"moves":[18,204,185,215],"species":315},{"level":42,"moves":[18,204,185,215],"species":316}],"party_address":3216644,"script_address":2360833},{"address":3241872,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":29,"species":315}],"party_address":3216740,"script_address":0},{"address":3241912,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":32,"species":315}],"party_address":3216748,"script_address":0},{"address":3241952,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":35,"species":316}],"party_address":3216756,"script_address":0},{"address":3241992,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":38,"species":316}],"party_address":3216764,"script_address":0},{"address":3242032,"battle_type":3,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":17,"species":363}],"party_address":3216772,"script_address":2045890},{"address":3242072,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":30,"species":25}],"party_address":3216780,"script_address":2067143},{"address":3242112,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":35,"species":350},{"level":37,"species":183},{"level":39,"species":184}],"party_address":3216788,"script_address":2265832},{"address":3242152,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":14,"species":353},{"level":14,"species":354}],"party_address":3216812,"script_address":2038890},{"address":3242192,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":26,"species":353},{"level":26,"species":354}],"party_address":3216828,"script_address":0},{"address":3242232,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":29,"species":353},{"level":29,"species":354}],"party_address":3216844,"script_address":0},{"address":3242272,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":32,"species":353},{"level":32,"species":354}],"party_address":3216860,"script_address":0},{"address":3242312,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":35,"species":353},{"level":35,"species":354}],"party_address":3216876,"script_address":0},{"address":3242352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":336}],"party_address":3216892,"script_address":2052811},{"address":3242392,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":33,"moves":[36,26,28,91],"species":336}],"party_address":3216900,"script_address":0},{"address":3242432,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":36,"moves":[36,26,28,91],"species":336}],"party_address":3216916,"script_address":0},{"address":3242472,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":39,"moves":[36,187,28,91],"species":336}],"party_address":3216932,"script_address":0},{"address":3242512,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":42,"moves":[36,187,28,91],"species":336}],"party_address":3216948,"script_address":0},{"address":3242552,"battle_type":3,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":18,"moves":[136,96,93,197],"species":356}],"party_address":3216964,"script_address":2046100},{"address":3242592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":21,"species":356},{"level":21,"species":335}],"party_address":3216980,"script_address":2304277},{"address":3242632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":356},{"level":30,"species":335}],"party_address":3216996,"script_address":0},{"address":3242672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":357},{"level":33,"species":336}],"party_address":3217012,"script_address":0},{"address":3242712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":357},{"level":36,"species":336}],"party_address":3217028,"script_address":0},{"address":3242752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":357},{"level":39,"species":336}],"party_address":3217044,"script_address":0},{"address":3242792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":286}],"party_address":3217060,"script_address":2024678},{"address":3242832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":288},{"level":7,"species":298}],"party_address":3217068,"script_address":2029684},{"address":3242872,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":10,"moves":[33,0,0,0],"species":74}],"party_address":3217084,"script_address":2188154},{"address":3242912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":8,"species":74},{"level":8,"species":74}],"party_address":3217100,"script_address":2188185},{"address":3242952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":9,"species":66}],"party_address":3217116,"script_address":2054180},{"address":3242992,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":17,"moves":[29,28,45,85],"species":288},{"level":17,"moves":[133,124,25,1],"species":367}],"party_address":3217124,"script_address":2167670},{"address":3243032,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":43,"moves":[213,58,85,53],"species":366},{"level":43,"moves":[29,182,5,92],"species":362}],"party_address":3217156,"script_address":2332778},{"address":3243072,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":43,"moves":[29,94,85,91],"species":394},{"level":43,"moves":[89,247,76,24],"species":366}],"party_address":3217188,"script_address":2332809},{"address":3243112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":19,"species":332}],"party_address":3217220,"script_address":2050594},{"address":3243152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":19,"species":382}],"party_address":3217228,"script_address":2050625},{"address":3243192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":287}],"party_address":3217236,"script_address":0},{"address":3243232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":305},{"level":30,"species":287}],"party_address":3217244,"script_address":0},{"address":3243272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":305},{"level":29,"species":289},{"level":33,"species":287}],"party_address":3217260,"script_address":0},{"address":3243312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":305},{"level":32,"species":289},{"level":36,"species":287}],"party_address":3217284,"script_address":0},{"address":3243352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":14,"species":288},{"level":16,"species":288}],"party_address":3217308,"script_address":2553792},{"address":3243392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":4,"species":288},{"level":3,"species":304}],"party_address":3217324,"script_address":2024926},{"address":3243432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":15,"species":382},{"level":13,"species":337}],"party_address":3217340,"script_address":2039000},{"address":3243472,"battle_type":3,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":57,"moves":[240,67,38,59],"species":314},{"level":55,"moves":[92,56,188,58],"species":73},{"level":56,"moves":[202,57,73,104],"species":297},{"level":56,"moves":[89,57,133,63],"species":324},{"level":56,"moves":[93,89,63,57],"species":130},{"level":58,"moves":[105,57,58,92],"species":329}],"party_address":3217356,"script_address":2277575},{"address":3243512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":129},{"level":10,"species":72},{"level":15,"species":129}],"party_address":3217452,"script_address":2026322},{"address":3243552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":129},{"level":6,"species":129},{"level":7,"species":129}],"party_address":3217476,"script_address":2029653},{"address":3243592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":16,"species":129},{"level":17,"species":118},{"level":18,"species":323}],"party_address":3217500,"script_address":2052185},{"address":3243632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":10,"species":129},{"level":7,"species":72},{"level":10,"species":129}],"party_address":3217524,"script_address":2034247},{"address":3243672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":11,"species":72}],"party_address":3217548,"script_address":2034357},{"address":3243712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":11,"species":72},{"level":14,"species":313},{"level":11,"species":72},{"level":14,"species":313}],"party_address":3217556,"script_address":2038546},{"address":3243752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":19,"species":323}],"party_address":3217588,"script_address":2052216},{"address":3243792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":72},{"level":25,"species":330}],"party_address":3217596,"script_address":2058894},{"address":3243832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":16,"species":72}],"party_address":3217612,"script_address":2058925},{"address":3243872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":313},{"level":25,"species":73}],"party_address":3217620,"script_address":2036183},{"address":3243912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":24,"species":72},{"level":27,"species":130},{"level":27,"species":130}],"party_address":3217636,"script_address":0},{"address":3243952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":130},{"level":26,"species":330},{"level":26,"species":72},{"level":29,"species":130}],"party_address":3217660,"script_address":0},{"address":3243992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":130},{"level":30,"species":330},{"level":30,"species":73},{"level":31,"species":130}],"party_address":3217692,"script_address":0},{"address":3244032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":130},{"level":33,"species":331},{"level":33,"species":130},{"level":35,"species":73}],"party_address":3217724,"script_address":0},{"address":3244072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":19,"species":129},{"level":21,"species":130},{"level":23,"species":130},{"level":26,"species":130},{"level":30,"species":130},{"level":35,"species":130}],"party_address":3217756,"script_address":2073670},{"address":3244112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":6,"species":100},{"level":6,"species":100},{"level":14,"species":81}],"party_address":3217804,"script_address":2038577},{"address":3244152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":14,"species":81},{"level":14,"species":81}],"party_address":3217828,"script_address":2038608},{"address":3244192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":16,"species":81}],"party_address":3217844,"script_address":2038639},{"address":3244232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":81}],"party_address":3217852,"script_address":0},{"address":3244272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":81}],"party_address":3217860,"script_address":0},{"address":3244312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":82}],"party_address":3217868,"script_address":0},{"address":3244352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":82}],"party_address":3217876,"script_address":0},{"address":3244392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":16,"species":81}],"party_address":3217884,"script_address":2038780},{"address":3244432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":14,"species":81},{"level":14,"species":81},{"level":6,"species":100}],"party_address":3217892,"script_address":2038749},{"address":3244472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":81}],"party_address":3217916,"script_address":0},{"address":3244512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":81}],"party_address":3217924,"script_address":0},{"address":3244552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":82}],"party_address":3217932,"script_address":0},{"address":3244592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":82}],"party_address":3217940,"script_address":0},{"address":3244632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":84}],"party_address":3217948,"script_address":2057375},{"address":3244672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":84}],"party_address":3217956,"script_address":0},{"address":3244712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":84}],"party_address":3217964,"script_address":0},{"address":3244752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":85}],"party_address":3217972,"script_address":0},{"address":3244792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":85}],"party_address":3217980,"script_address":0},{"address":3244832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":84}],"party_address":3217988,"script_address":2057485},{"address":3244872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":84}],"party_address":3217996,"script_address":0},{"address":3244912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":84}],"party_address":3218004,"script_address":0},{"address":3244952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":85}],"party_address":3218012,"script_address":0},{"address":3244992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":85}],"party_address":3218020,"script_address":0},{"address":3245032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":120},{"level":33,"species":120}],"party_address":3218028,"script_address":2070582},{"address":3245072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":288},{"level":25,"species":337}],"party_address":3218044,"script_address":2340077},{"address":3245112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":120}],"party_address":3218060,"script_address":2071332},{"address":3245152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":120},{"level":33,"species":120}],"party_address":3218068,"script_address":2070380},{"address":3245192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":309},{"level":34,"species":120}],"party_address":3218084,"script_address":2072978},{"address":3245232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":120}],"party_address":3218100,"script_address":0},{"address":3245272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":42,"species":120}],"party_address":3218108,"script_address":0},{"address":3245312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":45,"species":121}],"party_address":3218116,"script_address":0},{"address":3245352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":48,"species":121}],"party_address":3218124,"script_address":0},{"address":3245392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":120}],"party_address":3218132,"script_address":2070318},{"address":3245432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":309},{"level":34,"species":120}],"party_address":3218140,"script_address":2070613},{"address":3245472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":120}],"party_address":3218156,"script_address":2073545},{"address":3245512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":120}],"party_address":3218164,"script_address":2071442},{"address":3245552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":309},{"level":33,"species":120}],"party_address":3218172,"script_address":2073009},{"address":3245592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":120}],"party_address":3218188,"script_address":0},{"address":3245632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":42,"species":120}],"party_address":3218196,"script_address":0},{"address":3245672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":45,"species":121}],"party_address":3218204,"script_address":0},{"address":3245712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":48,"species":121}],"party_address":3218212,"script_address":0},{"address":3245752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":359},{"level":37,"species":359}],"party_address":3218220,"script_address":2292701},{"address":3245792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":359},{"level":41,"species":359}],"party_address":3218236,"script_address":0},{"address":3245832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":44,"species":359},{"level":44,"species":359}],"party_address":3218252,"script_address":0},{"address":3245872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":46,"species":395},{"level":46,"species":359},{"level":46,"species":359}],"party_address":3218268,"script_address":0},{"address":3245912,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":49,"species":359},{"level":49,"species":359},{"level":49,"species":396}],"party_address":3218292,"script_address":0},{"address":3245952,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":34,"moves":[225,29,116,52],"species":395}],"party_address":3218316,"script_address":2074182},{"address":3245992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":309}],"party_address":3218332,"script_address":2059066},{"address":3246032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":309},{"level":25,"species":369}],"party_address":3218340,"script_address":2061450},{"address":3246072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":305}],"party_address":3218356,"script_address":2061481},{"address":3246112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":84},{"level":27,"species":227},{"level":27,"species":369}],"party_address":3218364,"script_address":2202267},{"address":3246152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":227}],"party_address":3218388,"script_address":2202391},{"address":3246192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":369},{"level":33,"species":178}],"party_address":3218396,"script_address":2070085},{"address":3246232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":84},{"level":29,"species":310}],"party_address":3218412,"script_address":2202298},{"address":3246272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":309},{"level":28,"species":177}],"party_address":3218428,"script_address":2065338},{"address":3246312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":358}],"party_address":3218444,"script_address":2065369},{"address":3246352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":305},{"level":36,"species":310},{"level":36,"species":178}],"party_address":3218452,"script_address":2563257},{"address":3246392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":304},{"level":25,"species":305}],"party_address":3218476,"script_address":2059097},{"address":3246432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":177},{"level":32,"species":358}],"party_address":3218492,"script_address":0},{"address":3246472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":177},{"level":35,"species":359}],"party_address":3218508,"script_address":0},{"address":3246512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":38,"species":177},{"level":38,"species":359}],"party_address":3218524,"script_address":0},{"address":3246552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":359},{"level":41,"species":178}],"party_address":3218540,"script_address":0},{"address":3246592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":177},{"level":33,"species":305}],"party_address":3218556,"script_address":2074151},{"address":3246632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":369}],"party_address":3218572,"script_address":2073981},{"address":3246672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":302}],"party_address":3218580,"script_address":2061512},{"address":3246712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":302},{"level":25,"species":109}],"party_address":3218588,"script_address":2061543},{"address":3246752,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":43,"moves":[29,89,0,0],"species":319},{"level":43,"moves":[85,89,0,0],"species":171}],"party_address":3218604,"script_address":2335578},{"address":3246792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":183}],"party_address":3218636,"script_address":2341860},{"address":3246832,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":17,"moves":[139,33,123,120],"species":109},{"level":17,"moves":[139,33,123,120],"species":109},{"level":17,"moves":[139,33,124,120],"species":109}],"party_address":3218644,"script_address":2050766},{"address":3246872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":109},{"level":18,"species":302}],"party_address":3218692,"script_address":2050876},{"address":3246912,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":24,"moves":[139,33,124,120],"species":109},{"level":24,"moves":[139,33,124,0],"species":109},{"level":24,"moves":[139,33,124,120],"species":109},{"level":26,"moves":[33,124,0,0],"species":109}],"party_address":3218708,"script_address":0},{"address":3246952,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":27,"moves":[139,33,124,120],"species":109},{"level":27,"moves":[139,33,124,120],"species":109},{"level":27,"moves":[139,33,124,0],"species":109},{"level":29,"moves":[33,124,0,0],"species":109}],"party_address":3218772,"script_address":0},{"address":3246992,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":30,"moves":[139,33,124,0],"species":109},{"level":30,"moves":[139,33,124,0],"species":109},{"level":30,"moves":[139,33,124,0],"species":109},{"level":32,"moves":[33,124,0,0],"species":109}],"party_address":3218836,"script_address":0},{"address":3247032,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":33,"moves":[139,33,124,0],"species":109},{"level":33,"moves":[139,33,124,120],"species":109},{"level":33,"moves":[139,33,124,120],"species":109},{"level":35,"moves":[33,124,0,0],"species":110}],"party_address":3218900,"script_address":0},{"address":3247072,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":356}],"party_address":3218964,"script_address":2095313},{"address":3247112,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":356}],"party_address":3218972,"script_address":2095351},{"address":3247152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":356},{"level":18,"species":335}],"party_address":3218980,"script_address":2053062},{"address":3247192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":356}],"party_address":3218996,"script_address":2557727},{"address":3247232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":307}],"party_address":3219004,"script_address":2557789},{"address":3247272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":356},{"level":26,"species":335}],"party_address":3219012,"script_address":0},{"address":3247312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":356},{"level":29,"species":335}],"party_address":3219028,"script_address":0},{"address":3247352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":357},{"level":32,"species":336}],"party_address":3219044,"script_address":0},{"address":3247392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":357},{"level":35,"species":336}],"party_address":3219060,"script_address":0},{"address":3247432,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":19,"moves":[52,33,222,241],"species":339}],"party_address":3219076,"script_address":2050656},{"address":3247472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":363},{"level":28,"species":313}],"party_address":3219092,"script_address":2065713},{"address":3247512,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":30,"moves":[240,55,87,96],"species":385}],"party_address":3219108,"script_address":2065744},{"address":3247552,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":29,"moves":[52,33,222,241],"species":339}],"party_address":3219124,"script_address":0},{"address":3247592,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":32,"moves":[52,36,222,241],"species":339}],"party_address":3219140,"script_address":0},{"address":3247632,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":34,"moves":[73,72,64,241],"species":363},{"level":34,"moves":[53,36,222,241],"species":339}],"party_address":3219156,"script_address":0},{"address":3247672,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":37,"moves":[73,202,76,241],"species":363},{"level":37,"moves":[53,36,89,241],"species":340}],"party_address":3219188,"script_address":0},{"address":3247712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":309},{"level":25,"species":313}],"party_address":3219220,"script_address":2033633},{"address":3247752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":183}],"party_address":3219236,"script_address":2033664},{"address":3247792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":313}],"party_address":3219244,"script_address":2034216},{"address":3247832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":309},{"level":25,"species":118}],"party_address":3219252,"script_address":2034620},{"address":3247872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":118}],"party_address":3219268,"script_address":2034651},{"address":3247912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":116},{"level":25,"species":183}],"party_address":3219276,"script_address":2034838},{"address":3247952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":118}],"party_address":3219292,"script_address":2034869},{"address":3247992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":24,"species":118},{"level":24,"species":309},{"level":24,"species":118}],"party_address":3219300,"script_address":2035808},{"address":3248032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":313}],"party_address":3219324,"script_address":2069240},{"address":3248072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":183}],"party_address":3219332,"script_address":2069350},{"address":3248112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":325}],"party_address":3219340,"script_address":2069851},{"address":3248152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":119}],"party_address":3219348,"script_address":2069882},{"address":3248192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":183},{"level":33,"species":341}],"party_address":3219356,"script_address":2070225},{"address":3248232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":118}],"party_address":3219372,"script_address":2070256},{"address":3248272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":118},{"level":33,"species":341}],"party_address":3219380,"script_address":2073260},{"address":3248312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":325}],"party_address":3219396,"script_address":2073421},{"address":3248352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":119}],"party_address":3219404,"script_address":2073452},{"address":3248392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":184}],"party_address":3219412,"script_address":2073639},{"address":3248432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":325},{"level":33,"species":325}],"party_address":3219420,"script_address":2070349},{"address":3248472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":119}],"party_address":3219436,"script_address":2073888},{"address":3248512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":116},{"level":33,"species":117}],"party_address":3219444,"script_address":2073919},{"address":3248552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":171},{"level":34,"species":310}],"party_address":3219460,"script_address":0},{"address":3248592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":325},{"level":33,"species":325}],"party_address":3219476,"script_address":2074120},{"address":3248632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":119}],"party_address":3219492,"script_address":2071676},{"address":3248672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":38,"species":313}],"party_address":3219500,"script_address":0},{"address":3248712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":313}],"party_address":3219508,"script_address":0},{"address":3248752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":43,"species":120},{"level":43,"species":313}],"party_address":3219516,"script_address":0},{"address":3248792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":45,"species":325},{"level":45,"species":313},{"level":45,"species":121}],"party_address":3219532,"script_address":0},{"address":3248832,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":22,"moves":[91,28,40,163],"species":27},{"level":22,"moves":[229,189,60,61],"species":318}],"party_address":3219556,"script_address":2046397},{"address":3248872,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":22,"moves":[28,40,163,91],"species":27},{"level":22,"moves":[205,61,39,111],"species":183}],"party_address":3219588,"script_address":2046459},{"address":3248912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":304},{"level":17,"species":296}],"party_address":3219620,"script_address":2049860},{"address":3248952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":183},{"level":18,"species":296}],"party_address":3219636,"script_address":2051934},{"address":3248992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":23,"species":315},{"level":23,"species":358}],"party_address":3219652,"script_address":2557036},{"address":3249032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":19,"species":306},{"level":19,"species":43},{"level":19,"species":358}],"party_address":3219668,"script_address":2310092},{"address":3249072,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":32,"moves":[194,219,68,243],"species":202}],"party_address":3219692,"script_address":2315855},{"address":3249112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":306},{"level":17,"species":183}],"party_address":3219708,"script_address":2046631},{"address":3249152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":306},{"level":25,"species":44},{"level":25,"species":358}],"party_address":3219724,"script_address":0},{"address":3249192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":307},{"level":28,"species":44},{"level":28,"species":358}],"party_address":3219748,"script_address":0},{"address":3249232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":307},{"level":31,"species":44},{"level":31,"species":358}],"party_address":3219772,"script_address":0},{"address":3249272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":40,"species":307},{"level":40,"species":45},{"level":40,"species":359}],"party_address":3219796,"script_address":0},{"address":3249312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":15,"species":353},{"level":15,"species":354}],"party_address":3219820,"script_address":0},{"address":3249352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":353},{"level":27,"species":354}],"party_address":3219836,"script_address":0},{"address":3249392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":6,"species":298},{"level":6,"species":295}],"party_address":3219852,"script_address":0},{"address":3249432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":292},{"level":26,"species":294}],"party_address":3219868,"script_address":0},{"address":3249472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":9,"species":353},{"level":9,"species":354}],"party_address":3219884,"script_address":0},{"address":3249512,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":10,"moves":[101,50,0,0],"species":361},{"level":10,"moves":[71,73,0,0],"species":306}],"party_address":3219900,"script_address":0},{"address":3249552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":353},{"level":30,"species":354}],"party_address":3219932,"script_address":0},{"address":3249592,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":33,"moves":[209,12,57,14],"species":353},{"level":33,"moves":[209,12,204,14],"species":354}],"party_address":3219948,"script_address":0},{"address":3249632,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":36,"moves":[87,12,57,14],"species":353},{"level":36,"moves":[87,12,204,14],"species":354}],"party_address":3219980,"script_address":0},{"address":3249672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":12,"species":309},{"level":12,"species":66}],"party_address":3220012,"script_address":2035839},{"address":3249712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":309}],"party_address":3220028,"script_address":2035870},{"address":3249752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":309},{"level":33,"species":67}],"party_address":3220036,"script_address":2069913},{"address":3249792,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":11,"species":309},{"level":11,"species":66},{"level":11,"species":72}],"party_address":3220052,"script_address":2543939},{"address":3249832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":44,"species":73},{"level":44,"species":67}],"party_address":3220076,"script_address":2360255},{"address":3249872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":43,"species":66},{"level":43,"species":310},{"level":43,"species":67}],"party_address":3220092,"script_address":2360286},{"address":3249912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":341},{"level":25,"species":67}],"party_address":3220116,"script_address":2340984},{"address":3249952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":309},{"level":36,"species":72},{"level":36,"species":67}],"party_address":3220132,"script_address":0},{"address":3249992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":310},{"level":39,"species":72},{"level":39,"species":67}],"party_address":3220156,"script_address":0},{"address":3250032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":42,"species":310},{"level":42,"species":72},{"level":42,"species":67}],"party_address":3220180,"script_address":0},{"address":3250072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":45,"species":310},{"level":45,"species":67},{"level":45,"species":73}],"party_address":3220204,"script_address":0},{"address":3250112,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":23,"species":339}],"party_address":3220228,"script_address":2103632},{"address":3250152,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":39,"moves":[175,96,216,213],"species":328},{"level":39,"moves":[175,96,216,213],"species":328}],"party_address":3220236,"script_address":2265863},{"address":3250192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":376}],"party_address":3220268,"script_address":2068647},{"address":3250232,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":31,"moves":[92,87,120,188],"species":109}],"party_address":3220276,"script_address":2068616},{"address":3250272,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":31,"moves":[241,55,53,76],"species":385}],"party_address":3220292,"script_address":2068585},{"address":3250312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":338},{"level":33,"species":68}],"party_address":3220308,"script_address":2070116},{"address":3250352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":67},{"level":33,"species":341}],"party_address":3220324,"script_address":2074337},{"address":3250392,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":34,"moves":[44,46,86,85],"species":338}],"party_address":3220340,"script_address":2074306},{"address":3250432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":356},{"level":33,"species":336}],"party_address":3220356,"script_address":2074275},{"address":3250472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":313}],"party_address":3220372,"script_address":2074244},{"address":3250512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":170},{"level":33,"species":336}],"party_address":3220380,"script_address":2074043},{"address":3250552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":14,"species":296},{"level":14,"species":299}],"party_address":3220396,"script_address":2038436},{"address":3250592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":380},{"level":18,"species":379}],"party_address":3220412,"script_address":2053172},{"address":3250632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":340},{"level":38,"species":287},{"level":40,"species":42}],"party_address":3220428,"script_address":0},{"address":3250672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":296},{"level":26,"species":299}],"party_address":3220452,"script_address":0},{"address":3250712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":296},{"level":29,"species":299}],"party_address":3220468,"script_address":0},{"address":3250752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":296},{"level":32,"species":299}],"party_address":3220484,"script_address":0},{"address":3250792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":297},{"level":35,"species":300}],"party_address":3220500,"script_address":0},{"address":3250832,"battle_type":3,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":44,"moves":[76,219,225,93],"species":359},{"level":43,"moves":[47,18,204,185],"species":316},{"level":44,"moves":[89,73,202,92],"species":363},{"level":41,"moves":[48,85,161,103],"species":82},{"level":45,"moves":[104,91,94,248],"species":394}],"party_address":3220516,"script_address":2332529},{"address":3250872,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":277}],"party_address":3220596,"script_address":2025759},{"address":3250912,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":218},{"level":18,"species":309},{"level":20,"species":278}],"party_address":3220604,"script_address":2039798},{"address":3250952,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":218},{"level":29,"species":310},{"level":31,"species":278}],"party_address":3220628,"script_address":2060578},{"address":3250992,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":280}],"party_address":3220652,"script_address":2025703},{"address":3251032,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":309},{"level":18,"species":296},{"level":20,"species":281}],"party_address":3220660,"script_address":2039742},{"address":3251072,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":310},{"level":29,"species":296},{"level":31,"species":281}],"party_address":3220684,"script_address":2060522},{"address":3251112,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":283}],"party_address":3220708,"script_address":2025731},{"address":3251152,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":296},{"level":18,"species":218},{"level":20,"species":284}],"party_address":3220716,"script_address":2039770},{"address":3251192,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":296},{"level":29,"species":218},{"level":31,"species":284}],"party_address":3220740,"script_address":2060550},{"address":3251232,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":277}],"party_address":3220764,"script_address":2025675},{"address":3251272,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":309},{"level":18,"species":218},{"level":20,"species":278}],"party_address":3220772,"script_address":2039622},{"address":3251312,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":218},{"level":29,"species":296},{"level":31,"species":278}],"party_address":3220796,"script_address":2060420},{"address":3251352,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":280}],"party_address":3220820,"script_address":2025619},{"address":3251392,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":309},{"level":18,"species":296},{"level":20,"species":281}],"party_address":3220828,"script_address":2039566},{"address":3251432,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":310},{"level":29,"species":296},{"level":31,"species":281}],"party_address":3220852,"script_address":2060364},{"address":3251472,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":283}],"party_address":3220876,"script_address":2025647},{"address":3251512,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":296},{"level":18,"species":218},{"level":20,"species":284}],"party_address":3220884,"script_address":2039594},{"address":3251552,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":296},{"level":29,"species":218},{"level":31,"species":284}],"party_address":3220908,"script_address":2060392},{"address":3251592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":11,"species":370},{"level":11,"species":288},{"level":11,"species":382},{"level":11,"species":286},{"level":11,"species":304},{"level":11,"species":335}],"party_address":3220932,"script_address":2057155},{"address":3251632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":127}],"party_address":3220980,"script_address":2068678},{"address":3251672,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":43,"moves":[153,115,113,94],"species":348},{"level":43,"moves":[153,115,113,247],"species":349}],"party_address":3220988,"script_address":2334468},{"address":3251712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":22,"species":371},{"level":22,"species":289},{"level":22,"species":382},{"level":22,"species":287},{"level":22,"species":305},{"level":22,"species":335}],"party_address":3221020,"script_address":0},{"address":3251752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":371},{"level":25,"species":289},{"level":25,"species":382},{"level":25,"species":287},{"level":25,"species":305},{"level":25,"species":336}],"party_address":3221068,"script_address":0},{"address":3251792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":371},{"level":28,"species":289},{"level":28,"species":382},{"level":28,"species":287},{"level":28,"species":305},{"level":28,"species":336}],"party_address":3221116,"script_address":0},{"address":3251832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":371},{"level":31,"species":289},{"level":31,"species":383},{"level":31,"species":287},{"level":31,"species":305},{"level":31,"species":336}],"party_address":3221164,"script_address":0},{"address":3251872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":11,"species":309},{"level":11,"species":306},{"level":11,"species":183},{"level":11,"species":363},{"level":11,"species":315},{"level":11,"species":118}],"party_address":3221212,"script_address":2057265},{"address":3251912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":43,"species":322},{"level":43,"species":376}],"party_address":3221260,"script_address":2334499},{"address":3251952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":28}],"party_address":3221276,"script_address":2341891},{"address":3251992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":22,"species":309},{"level":22,"species":306},{"level":22,"species":183},{"level":22,"species":363},{"level":22,"species":315},{"level":22,"species":118}],"party_address":3221284,"script_address":0},{"address":3252032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":310},{"level":25,"species":307},{"level":25,"species":183},{"level":25,"species":363},{"level":25,"species":316},{"level":25,"species":118}],"party_address":3221332,"script_address":0},{"address":3252072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":310},{"level":28,"species":307},{"level":28,"species":183},{"level":28,"species":363},{"level":28,"species":316},{"level":28,"species":118}],"party_address":3221380,"script_address":0},{"address":3252112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":310},{"level":31,"species":307},{"level":31,"species":184},{"level":31,"species":363},{"level":31,"species":316},{"level":31,"species":119}],"party_address":3221428,"script_address":0},{"address":3252152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":307}],"party_address":3221476,"script_address":2061230},{"address":3252192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":298},{"level":28,"species":299},{"level":28,"species":296}],"party_address":3221484,"script_address":2065479},{"address":3252232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":345}],"party_address":3221508,"script_address":2563288},{"address":3252272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":307}],"party_address":3221516,"script_address":0},{"address":3252312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":307}],"party_address":3221524,"script_address":0},{"address":3252352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":307}],"party_address":3221532,"script_address":0},{"address":3252392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":317},{"level":39,"species":307}],"party_address":3221540,"script_address":0},{"address":3252432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":44},{"level":26,"species":363}],"party_address":3221556,"script_address":2061340},{"address":3252472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":295},{"level":28,"species":296},{"level":28,"species":299}],"party_address":3221572,"script_address":2065510},{"address":3252512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":38,"species":358},{"level":38,"species":363}],"party_address":3221596,"script_address":2563226},{"address":3252552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":44},{"level":30,"species":363}],"party_address":3221612,"script_address":0},{"address":3252592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":44},{"level":33,"species":363}],"party_address":3221628,"script_address":0},{"address":3252632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":44},{"level":36,"species":363}],"party_address":3221644,"script_address":0},{"address":3252672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":182},{"level":39,"species":363}],"party_address":3221660,"script_address":0},{"address":3252712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":21,"species":81}],"party_address":3221676,"script_address":2310306},{"address":3252752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":287},{"level":35,"species":42}],"party_address":3221684,"script_address":2327187},{"address":3252792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":313},{"level":31,"species":41}],"party_address":3221700,"script_address":0},{"address":3252832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":313},{"level":30,"species":41}],"party_address":3221716,"script_address":2317615},{"address":3252872,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":22,"species":286},{"level":22,"species":339}],"party_address":3221732,"script_address":2309993},{"address":3252912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":8,"species":74},{"level":8,"species":74}],"party_address":3221748,"script_address":2188216},{"address":3252952,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":66}],"party_address":3221764,"script_address":2095389},{"address":3252992,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":356}],"party_address":3221772,"script_address":2095465},{"address":3253032,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":335}],"party_address":3221780,"script_address":2095427},{"address":3253072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":356}],"party_address":3221788,"script_address":2244674},{"address":3253112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":330}],"party_address":3221796,"script_address":2070287},{"address":3253152,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":32,"moves":[87,86,98,0],"species":338},{"level":32,"moves":[57,168,0,0],"species":289}],"party_address":3221804,"script_address":2070768},{"address":3253192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":73}],"party_address":3221836,"script_address":2071645},{"address":3253232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":20,"species":41}],"party_address":3221844,"script_address":2304070},{"address":3253272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":331}],"party_address":3221852,"script_address":2073102},{"address":3253312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":203}],"party_address":3221860,"script_address":0},{"address":3253352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":351}],"party_address":3221868,"script_address":2244705},{"address":3253392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":64}],"party_address":3221876,"script_address":2244829},{"address":3253432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":203}],"party_address":3221884,"script_address":2244767},{"address":3253472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":202}],"party_address":3221892,"script_address":2244798},{"address":3253512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":41},{"level":31,"species":286}],"party_address":3221900,"script_address":2254605},{"address":3253552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":318}],"party_address":3221916,"script_address":2254667},{"address":3253592,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":41}],"party_address":3221924,"script_address":2257768},{"address":3253632,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":287}],"party_address":3221932,"script_address":2257818},{"address":3253672,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":318}],"party_address":3221940,"script_address":2257868},{"address":3253712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":177}],"party_address":3221948,"script_address":2244736},{"address":3253752,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":295},{"level":15,"species":280}],"party_address":3221956,"script_address":1978559},{"address":3253792,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":309},{"level":15,"species":277}],"party_address":3221972,"script_address":1978621},{"address":3253832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":305},{"level":33,"species":307}],"party_address":3221988,"script_address":2073732},{"address":3253872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":120}],"party_address":3222004,"script_address":2069651},{"address":3253912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":41},{"level":27,"species":286}],"party_address":3222012,"script_address":2572062},{"address":3253952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":339},{"level":20,"species":286},{"level":22,"species":339},{"level":22,"species":41}],"party_address":3222028,"script_address":2304039},{"address":3253992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":317},{"level":33,"species":371}],"party_address":3222060,"script_address":2073794},{"address":3254032,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":218},{"level":15,"species":283}],"party_address":3222076,"script_address":1978590},{"address":3254072,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":309},{"level":15,"species":277}],"party_address":3222092,"script_address":1978317},{"address":3254112,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":287},{"level":38,"species":169},{"level":39,"species":340}],"party_address":3222108,"script_address":2351441},{"address":3254152,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":24,"species":287},{"level":24,"species":41},{"level":25,"species":340}],"party_address":3222132,"script_address":2303440},{"address":3254192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":4,"species":288},{"level":4,"species":306}],"party_address":3222156,"script_address":2024895},{"address":3254232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":6,"species":295},{"level":6,"species":306}],"party_address":3222172,"script_address":2029715},{"address":3254272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":9,"species":183}],"party_address":3222188,"script_address":2054459},{"address":3254312,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":15,"species":183},{"level":15,"species":306},{"level":15,"species":339}],"party_address":3222196,"script_address":2045995},{"address":3254352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":296},{"level":26,"species":306}],"party_address":3222220,"script_address":0},{"address":3254392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":296},{"level":29,"species":307}],"party_address":3222236,"script_address":0},{"address":3254432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":296},{"level":32,"species":307}],"party_address":3222252,"script_address":0},{"address":3254472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":305},{"level":34,"species":296},{"level":34,"species":307}],"party_address":3222268,"script_address":0},{"address":3254512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":16,"species":43}],"party_address":3222292,"script_address":2553761},{"address":3254552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":14,"species":315},{"level":14,"species":306},{"level":14,"species":183}],"party_address":3222300,"script_address":2553823},{"address":3254592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":40,"species":325}],"party_address":3222324,"script_address":2265615},{"address":3254632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":118},{"level":39,"species":313}],"party_address":3222332,"script_address":2265646},{"address":3254672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":4,"species":290},{"level":4,"species":290}],"party_address":3222348,"script_address":2024864},{"address":3254712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":3,"species":290},{"level":3,"species":290},{"level":3,"species":290},{"level":3,"species":290}],"party_address":3222364,"script_address":2300392},{"address":3254752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":8,"species":290},{"level":8,"species":301}],"party_address":3222396,"script_address":2054211},{"address":3254792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":301},{"level":28,"species":302}],"party_address":3222412,"script_address":2061137},{"address":3254832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":386},{"level":25,"species":387}],"party_address":3222428,"script_address":2061168},{"address":3254872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":302}],"party_address":3222444,"script_address":2061199},{"address":3254912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":6,"species":301},{"level":6,"species":301}],"party_address":3222452,"script_address":2300423},{"address":3254952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":302}],"party_address":3222468,"script_address":0},{"address":3254992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":294},{"level":29,"species":302}],"party_address":3222476,"script_address":0},{"address":3255032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":311},{"level":31,"species":294},{"level":31,"species":302}],"party_address":3222492,"script_address":0},{"address":3255072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":311},{"level":33,"species":302},{"level":33,"species":294},{"level":33,"species":302}],"party_address":3222516,"script_address":0},{"address":3255112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":339},{"level":17,"species":66}],"party_address":3222548,"script_address":2049688},{"address":3255152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":16,"species":74},{"level":17,"species":74},{"level":16,"species":74}],"party_address":3222564,"script_address":2049719},{"address":3255192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":74},{"level":18,"species":66}],"party_address":3222588,"script_address":2051841},{"address":3255232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":74},{"level":18,"species":339}],"party_address":3222604,"script_address":2051872},{"address":3255272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":22,"species":74},{"level":22,"species":320},{"level":22,"species":75}],"party_address":3222620,"script_address":2557067},{"address":3255312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":8,"species":74}],"party_address":3222644,"script_address":2054428},{"address":3255352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":20,"species":74},{"level":20,"species":318}],"party_address":3222652,"script_address":2310061},{"address":3255392,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":9,"moves":[150,55,0,0],"species":313}],"party_address":3222668,"script_address":0},{"address":3255432,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":10,"moves":[16,45,0,0],"species":310},{"level":10,"moves":[44,184,0,0],"species":286}],"party_address":3222684,"script_address":0},{"address":3255472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":16,"species":74},{"level":16,"species":74},{"level":16,"species":66}],"party_address":3222716,"script_address":2296023},{"address":3255512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":24,"species":74},{"level":24,"species":74},{"level":24,"species":74},{"level":24,"species":75}],"party_address":3222740,"script_address":0},{"address":3255552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":74},{"level":27,"species":74},{"level":27,"species":75},{"level":27,"species":75}],"party_address":3222772,"script_address":0},{"address":3255592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":74},{"level":30,"species":75},{"level":30,"species":75},{"level":30,"species":75}],"party_address":3222804,"script_address":0},{"address":3255632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":75},{"level":33,"species":75},{"level":33,"species":75},{"level":33,"species":76}],"party_address":3222836,"script_address":0},{"address":3255672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":316},{"level":31,"species":338}],"party_address":3222868,"script_address":0},{"address":3255712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":45,"species":325},{"level":45,"species":325}],"party_address":3222884,"script_address":0},{"address":3255752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":386},{"level":25,"species":387}],"party_address":3222900,"script_address":0},{"address":3255792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":386},{"level":30,"species":387}],"party_address":3222916,"script_address":0},{"address":3255832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":386},{"level":33,"species":387}],"party_address":3222932,"script_address":0},{"address":3255872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":386},{"level":36,"species":387}],"party_address":3222948,"script_address":0},{"address":3255912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":386},{"level":39,"species":387}],"party_address":3222964,"script_address":0},{"address":3255952,"battle_type":2,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":118}],"party_address":3222980,"script_address":2543970},{"address":3255992,"battle_type":2,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":23,"moves":[53,154,185,20],"species":317}],"party_address":3222988,"script_address":2103539},{"address":3256032,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":17,"moves":[117,197,93,9],"species":356},{"level":17,"moves":[9,197,93,96],"species":356}],"party_address":3223004,"script_address":2167701},{"address":3256072,"battle_type":2,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":23,"moves":[117,197,93,7],"species":356}],"party_address":3223036,"script_address":2103508},{"address":3256112,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":25,"moves":[33,120,124,108],"species":109},{"level":25,"moves":[33,139,124,108],"species":109}],"party_address":3223052,"script_address":2061574},{"address":3256152,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":28,"moves":[139,120,124,108],"species":109},{"level":28,"moves":[28,104,210,14],"species":302}],"party_address":3223084,"script_address":2065775},{"address":3256192,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":28,"moves":[141,154,170,91],"species":301},{"level":28,"moves":[33,120,124,108],"species":109}],"party_address":3223116,"script_address":2065806},{"address":3256232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":305},{"level":29,"species":178}],"party_address":3223148,"script_address":2202329},{"address":3256272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":358},{"level":27,"species":358},{"level":27,"species":358}],"party_address":3223164,"script_address":2202360},{"address":3256312,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":16,"species":392}],"party_address":3223188,"script_address":1971405},{"address":3256352,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":47,"moves":[76,219,225,93],"species":359},{"level":46,"moves":[47,18,204,185],"species":316},{"level":47,"moves":[89,73,202,92],"species":363},{"level":44,"moves":[48,85,161,103],"species":82},{"level":48,"moves":[104,91,94,248],"species":394}],"party_address":3223196,"script_address":2332607},{"address":3256392,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":50,"moves":[76,219,225,93],"species":359},{"level":49,"moves":[47,18,204,185],"species":316},{"level":50,"moves":[89,73,202,92],"species":363},{"level":47,"moves":[48,85,161,103],"species":82},{"level":51,"moves":[104,91,94,248],"species":394}],"party_address":3223276,"script_address":0},{"address":3256432,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":53,"moves":[76,219,225,93],"species":359},{"level":52,"moves":[47,18,204,185],"species":316},{"level":53,"moves":[89,73,202,92],"species":363},{"level":50,"moves":[48,85,161,103],"species":82},{"level":54,"moves":[104,91,94,248],"species":394}],"party_address":3223356,"script_address":0},{"address":3256472,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":56,"moves":[76,219,225,93],"species":359},{"level":55,"moves":[47,18,204,185],"species":316},{"level":56,"moves":[89,73,202,92],"species":363},{"level":53,"moves":[48,85,161,103],"species":82},{"level":57,"moves":[104,91,94,248],"species":394}],"party_address":3223436,"script_address":0},{"address":3256512,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":369},{"level":32,"species":218},{"level":32,"species":310},{"level":34,"species":278}],"party_address":3223516,"script_address":1986165},{"address":3256552,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":369},{"level":32,"species":310},{"level":32,"species":297},{"level":34,"species":281}],"party_address":3223548,"script_address":1986109},{"address":3256592,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":369},{"level":32,"species":297},{"level":32,"species":218},{"level":34,"species":284}],"party_address":3223580,"script_address":1986137},{"address":3256632,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":369},{"level":32,"species":218},{"level":32,"species":310},{"level":34,"species":278}],"party_address":3223612,"script_address":1986081},{"address":3256672,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":369},{"level":32,"species":310},{"level":32,"species":297},{"level":34,"species":281}],"party_address":3223644,"script_address":1986025},{"address":3256712,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":369},{"level":32,"species":297},{"level":32,"species":218},{"level":34,"species":284}],"party_address":3223676,"script_address":1986053},{"address":3256752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":313},{"level":31,"species":72},{"level":32,"species":331}],"party_address":3223708,"script_address":2070644},{"address":3256792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":330},{"level":34,"species":73}],"party_address":3223732,"script_address":2070675},{"address":3256832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":15,"species":129},{"level":25,"species":129},{"level":35,"species":130}],"party_address":3223748,"script_address":2070706},{"address":3256872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":44},{"level":34,"species":184}],"party_address":3223772,"script_address":2071552},{"address":3256912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":300},{"level":34,"species":320}],"party_address":3223788,"script_address":2071583},{"address":3256952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":67}],"party_address":3223804,"script_address":2070799},{"address":3256992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":72},{"level":31,"species":72},{"level":36,"species":313}],"party_address":3223812,"script_address":2071614},{"address":3257032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":305},{"level":32,"species":227}],"party_address":3223836,"script_address":2070737},{"address":3257072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":341},{"level":33,"species":331}],"party_address":3223852,"script_address":2073040},{"address":3257112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":170}],"party_address":3223868,"script_address":2073071},{"address":3257152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":19,"species":308},{"level":19,"species":308}],"party_address":3223876,"script_address":0},{"address":3257192,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":32,"moves":[47,31,219,76],"species":358},{"level":35,"moves":[53,36,156,89],"species":339}],"party_address":3223892,"script_address":0},{"address":3257232,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":18,"moves":[74,78,72,73],"species":363},{"level":20,"moves":[111,205,44,88],"species":75}],"party_address":3223924,"script_address":0},{"address":3257272,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":27,"moves":[16,60,92,182],"species":294},{"level":27,"moves":[16,72,213,78],"species":292}],"party_address":3223956,"script_address":0},{"address":3257312,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":39,"moves":[94,7,244,182],"species":357},{"level":39,"moves":[8,61,156,187],"species":336}],"party_address":3223988,"script_address":0},{"address":3257352,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":43,"moves":[94,7,244,182],"species":357},{"level":43,"moves":[8,61,156,187],"species":336}],"party_address":3224020,"script_address":0},{"address":3257392,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":46,"moves":[94,7,244,182],"species":357},{"level":46,"moves":[8,61,156,187],"species":336}],"party_address":3224052,"script_address":0},{"address":3257432,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":49,"moves":[94,7,244,182],"species":357},{"level":49,"moves":[8,61,156,187],"species":336}],"party_address":3224084,"script_address":0},{"address":3257472,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":52,"moves":[94,7,244,182],"species":357},{"level":52,"moves":[8,61,156,187],"species":336}],"party_address":3224116,"script_address":0},{"address":3257512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":184},{"level":33,"species":309}],"party_address":3224148,"script_address":0},{"address":3257552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":170},{"level":33,"species":330}],"party_address":3224164,"script_address":0},{"address":3257592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":42,"species":170},{"level":40,"species":330}],"party_address":3224180,"script_address":0},{"address":3257632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":45,"species":171},{"level":43,"species":330}],"party_address":3224196,"script_address":0},{"address":3257672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":48,"species":171},{"level":46,"species":331}],"party_address":3224212,"script_address":0},{"address":3257712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":51,"species":171},{"level":49,"species":331}],"party_address":3224228,"script_address":0},{"address":3257752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":27,"species":118},{"level":25,"species":72}],"party_address":3224244,"script_address":0},{"address":3257792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":129},{"level":20,"species":72},{"level":26,"species":328},{"level":23,"species":330}],"party_address":3224260,"script_address":2061605},{"address":3257832,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":8,"species":288},{"level":8,"species":286}],"party_address":3224292,"script_address":2054707},{"address":3257872,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":8,"species":295},{"level":8,"species":288}],"party_address":3224308,"script_address":2054676},{"address":3257912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":9,"species":129}],"party_address":3224324,"script_address":2030343},{"address":3257952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":183}],"party_address":3224332,"script_address":2036307},{"address":3257992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":12,"species":72},{"level":12,"species":72}],"party_address":3224340,"script_address":2036276},{"address":3258032,"battle_type":0,"data_type":"ITEM_DEFAULT_MOVES","party":[{"level":14,"species":354},{"level":14,"species":353}],"party_address":3224356,"script_address":2039032},{"address":3258072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":14,"species":337},{"level":14,"species":100}],"party_address":3224372,"script_address":2039063},{"address":3258112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":15,"species":81}],"party_address":3224388,"script_address":2039094},{"address":3258152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":15,"species":100}],"party_address":3224396,"script_address":2026463},{"address":3258192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":15,"species":335}],"party_address":3224404,"script_address":2026494},{"address":3258232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":19,"species":27}],"party_address":3224412,"script_address":2046975},{"address":3258272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":363}],"party_address":3224420,"script_address":2047006},{"address":3258312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":306}],"party_address":3224428,"script_address":2046944},{"address":3258352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":339}],"party_address":3224436,"script_address":2046913},{"address":3258392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":183},{"level":19,"species":296}],"party_address":3224444,"script_address":2050969},{"address":3258432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":227},{"level":19,"species":305}],"party_address":3224460,"script_address":2051000},{"address":3258472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":318},{"level":18,"species":27}],"party_address":3224476,"script_address":2051031},{"address":3258512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":382},{"level":18,"species":382}],"party_address":3224492,"script_address":2051062},{"address":3258552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":296},{"level":18,"species":183}],"party_address":3224508,"script_address":2052309},{"address":3258592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":19,"species":323}],"party_address":3224524,"script_address":2052371},{"address":3258632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":19,"species":299}],"party_address":3224532,"script_address":2052340},{"address":3258672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":14,"species":288},{"level":14,"species":382},{"level":14,"species":337}],"party_address":3224540,"script_address":2059128},{"address":3258712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":41}],"party_address":3224564,"script_address":2347841},{"address":3258752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":286}],"party_address":3224572,"script_address":2347872},{"address":3258792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":339}],"party_address":3224580,"script_address":2348597},{"address":3258832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":318},{"level":28,"species":41}],"party_address":3224588,"script_address":2348628},{"address":3258872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":318},{"level":28,"species":339}],"party_address":3224604,"script_address":2348659},{"address":3258912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":287}],"party_address":3224620,"script_address":2349324},{"address":3258952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":41}],"party_address":3224628,"script_address":2349355},{"address":3258992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":286}],"party_address":3224636,"script_address":2349386},{"address":3259032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":41}],"party_address":3224644,"script_address":2350264},{"address":3259072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":287}],"party_address":3224652,"script_address":2350826},{"address":3259112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":318}],"party_address":3224660,"script_address":2351566},{"address":3259152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":339}],"party_address":3224668,"script_address":2351597},{"address":3259192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":41}],"party_address":3224676,"script_address":2351628},{"address":3259232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":287}],"party_address":3224684,"script_address":2348566},{"address":3259272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":339}],"party_address":3224692,"script_address":2349293},{"address":3259312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":318}],"party_address":3224700,"script_address":2350295},{"address":3259352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":339},{"level":28,"species":287},{"level":30,"species":41},{"level":33,"species":340}],"party_address":3224708,"script_address":2351659},{"address":3259392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":310},{"level":33,"species":340}],"party_address":3224740,"script_address":2073763},{"address":3259432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":42,"species":287},{"level":43,"species":169},{"level":44,"species":340}],"party_address":3224756,"script_address":0},{"address":3259472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":15,"species":72}],"party_address":3224780,"script_address":2026525},{"address":3259512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":15,"species":183}],"party_address":3224788,"script_address":2026556},{"address":3259552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":27},{"level":25,"species":27}],"party_address":3224796,"script_address":2033726},{"address":3259592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":304},{"level":25,"species":309}],"party_address":3224812,"script_address":2033695},{"address":3259632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":120}],"party_address":3224828,"script_address":2034744},{"address":3259672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":24,"species":309},{"level":24,"species":66},{"level":24,"species":72}],"party_address":3224836,"script_address":2034931},{"address":3259712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":24,"species":338},{"level":24,"species":305},{"level":24,"species":338}],"party_address":3224860,"script_address":2034900},{"address":3259752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":227},{"level":25,"species":227}],"party_address":3224884,"script_address":2036338},{"address":3259792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":22,"species":183},{"level":22,"species":296}],"party_address":3224900,"script_address":2047037},{"address":3259832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":22,"species":27},{"level":22,"species":28}],"party_address":3224916,"script_address":2047068},{"address":3259872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":22,"species":304},{"level":22,"species":299}],"party_address":3224932,"script_address":2047099},{"address":3259912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":339},{"level":18,"species":218}],"party_address":3224948,"script_address":2049891},{"address":3259952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":306},{"level":18,"species":363}],"party_address":3224964,"script_address":2049922},{"address":3259992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":84},{"level":26,"species":85}],"party_address":3224980,"script_address":2053203},{"address":3260032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":302},{"level":26,"species":367}],"party_address":3224996,"script_address":2053234},{"address":3260072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":64},{"level":26,"species":393}],"party_address":3225012,"script_address":2053265},{"address":3260112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":356},{"level":26,"species":335}],"party_address":3225028,"script_address":2053296},{"address":3260152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":18,"species":356},{"level":18,"species":351}],"party_address":3225044,"script_address":2053327},{"address":3260192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":8,"species":74},{"level":8,"species":74}],"party_address":3225060,"script_address":2054738},{"address":3260232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":8,"species":306},{"level":8,"species":295}],"party_address":3225076,"script_address":2054769},{"address":3260272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":84}],"party_address":3225092,"script_address":2057834},{"address":3260312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":392}],"party_address":3225100,"script_address":2057865},{"address":3260352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":17,"species":356}],"party_address":3225108,"script_address":2057896},{"address":3260392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":363},{"level":33,"species":357}],"party_address":3225116,"script_address":2073825},{"address":3260432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":338}],"party_address":3225132,"script_address":2061636},{"address":3260472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":218},{"level":25,"species":339}],"party_address":3225140,"script_address":2061667},{"address":3260512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":118}],"party_address":3225156,"script_address":2061698},{"address":3260552,"battle_type":0,"data_type":"NO_ITEM_CUSTOM_MOVES","party":[{"level":30,"moves":[87,98,86,0],"species":338}],"party_address":3225164,"script_address":2065837},{"address":3260592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":356},{"level":28,"species":335}],"party_address":3225180,"script_address":2065868},{"address":3260632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":294},{"level":29,"species":292}],"party_address":3225196,"script_address":2067487},{"address":3260672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":335},{"level":25,"species":309},{"level":25,"species":369},{"level":25,"species":288},{"level":25,"species":337},{"level":25,"species":339}],"party_address":3225212,"script_address":2067518},{"address":3260712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":25,"species":286},{"level":25,"species":306},{"level":25,"species":337},{"level":25,"species":183},{"level":25,"species":27},{"level":25,"species":367}],"party_address":3225260,"script_address":2067549},{"address":3260752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":29,"species":371},{"level":29,"species":365}],"party_address":3225308,"script_address":2067611},{"address":3260792,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":295},{"level":15,"species":280}],"party_address":3225324,"script_address":1978255},{"address":3260832,"battle_type":3,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":13,"species":321},{"level":15,"species":283}],"party_address":3225340,"script_address":1978286},{"address":3260872,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":32,"moves":[182,205,222,153],"species":76},{"level":35,"moves":[14,58,57,157],"species":140},{"level":35,"moves":[231,153,46,157],"species":95},{"level":37,"moves":[104,153,182,157],"species":320}],"party_address":3225356,"script_address":0},{"address":3260912,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":37,"moves":[182,58,157,57],"species":138},{"level":37,"moves":[182,205,222,153],"species":76},{"level":40,"moves":[14,58,57,157],"species":141},{"level":40,"moves":[231,153,46,157],"species":95},{"level":42,"moves":[104,153,182,157],"species":320}],"party_address":3225420,"script_address":0},{"address":3260952,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":42,"moves":[182,58,157,57],"species":139},{"level":42,"moves":[182,205,89,153],"species":76},{"level":45,"moves":[14,58,57,157],"species":141},{"level":45,"moves":[231,153,46,157],"species":95},{"level":47,"moves":[104,153,182,157],"species":320}],"party_address":3225500,"script_address":0},{"address":3260992,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":47,"moves":[157,63,48,182],"species":142},{"level":47,"moves":[8,205,89,153],"species":76},{"level":47,"moves":[182,58,157,57],"species":139},{"level":50,"moves":[14,58,57,157],"species":141},{"level":50,"moves":[231,153,46,157],"species":208},{"level":52,"moves":[104,153,182,157],"species":320}],"party_address":3225580,"script_address":0},{"address":3261032,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":33,"moves":[2,157,8,83],"species":68},{"level":33,"moves":[94,113,115,8],"species":356},{"level":35,"moves":[228,68,182,167],"species":237},{"level":37,"moves":[252,8,187,89],"species":336}],"party_address":3225676,"script_address":0},{"address":3261072,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":38,"moves":[2,157,8,83],"species":68},{"level":38,"moves":[94,113,115,8],"species":357},{"level":40,"moves":[228,68,182,167],"species":237},{"level":42,"moves":[252,8,187,89],"species":336}],"party_address":3225740,"script_address":0},{"address":3261112,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":40,"moves":[71,182,7,8],"species":107},{"level":43,"moves":[2,157,8,83],"species":68},{"level":43,"moves":[8,113,115,94],"species":357},{"level":45,"moves":[228,68,182,167],"species":237},{"level":47,"moves":[252,8,187,89],"species":336}],"party_address":3225804,"script_address":0},{"address":3261152,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":46,"moves":[25,8,89,83],"species":106},{"level":46,"moves":[71,182,7,8],"species":107},{"level":48,"moves":[238,157,8,83],"species":68},{"level":48,"moves":[8,113,115,94],"species":357},{"level":50,"moves":[228,68,182,167],"species":237},{"level":52,"moves":[252,8,187,89],"species":336}],"party_address":3225884,"script_address":0},{"address":3261192,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":36,"moves":[87,182,86,113],"species":179},{"level":36,"moves":[205,87,153,240],"species":101},{"level":38,"moves":[48,182,87,240],"species":82},{"level":40,"moves":[44,86,87,182],"species":338}],"party_address":3225980,"script_address":0},{"address":3261232,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":39,"moves":[87,21,240,95],"species":25},{"level":41,"moves":[87,182,86,113],"species":180},{"level":41,"moves":[205,87,153,240],"species":101},{"level":43,"moves":[48,182,87,240],"species":82},{"level":45,"moves":[44,86,87,182],"species":338}],"party_address":3226044,"script_address":0},{"address":3261272,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":44,"moves":[87,21,240,182],"species":26},{"level":46,"moves":[87,182,86,113],"species":181},{"level":46,"moves":[205,87,153,240],"species":101},{"level":48,"moves":[48,182,87,240],"species":82},{"level":50,"moves":[44,86,87,182],"species":338}],"party_address":3226124,"script_address":0},{"address":3261312,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":50,"moves":[129,8,9,113],"species":125},{"level":51,"moves":[87,21,240,182],"species":26},{"level":51,"moves":[87,182,86,113],"species":181},{"level":53,"moves":[205,87,153,240],"species":101},{"level":53,"moves":[48,182,87,240],"species":82},{"level":55,"moves":[44,86,87,182],"species":338}],"party_address":3226204,"script_address":0},{"address":3261352,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":38,"moves":[59,213,113,157],"species":219},{"level":36,"moves":[53,213,76,84],"species":77},{"level":38,"moves":[59,241,89,213],"species":340},{"level":40,"moves":[59,241,153,213],"species":321}],"party_address":3226300,"script_address":0},{"address":3261392,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":41,"moves":[14,53,46,241],"species":58},{"level":43,"moves":[59,213,113,157],"species":219},{"level":41,"moves":[53,213,76,84],"species":77},{"level":43,"moves":[59,241,89,213],"species":340},{"level":45,"moves":[59,241,153,213],"species":321}],"party_address":3226364,"script_address":0},{"address":3261432,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":46,"moves":[46,76,13,241],"species":228},{"level":46,"moves":[14,53,241,46],"species":58},{"level":48,"moves":[59,213,113,157],"species":219},{"level":46,"moves":[53,213,76,84],"species":78},{"level":48,"moves":[59,241,89,213],"species":340},{"level":50,"moves":[59,241,153,213],"species":321}],"party_address":3226444,"script_address":0},{"address":3261472,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":51,"moves":[14,53,241,46],"species":59},{"level":53,"moves":[59,213,113,157],"species":219},{"level":51,"moves":[46,76,13,241],"species":229},{"level":51,"moves":[53,213,76,84],"species":78},{"level":53,"moves":[59,241,89,213],"species":340},{"level":55,"moves":[59,241,153,213],"species":321}],"party_address":3226540,"script_address":0},{"address":3261512,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":42,"moves":[113,47,29,8],"species":113},{"level":42,"moves":[59,247,38,126],"species":366},{"level":43,"moves":[42,29,7,95],"species":308},{"level":45,"moves":[63,53,85,247],"species":366}],"party_address":3226636,"script_address":0},{"address":3261552,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":47,"moves":[59,247,38,126],"species":366},{"level":47,"moves":[113,47,29,8],"species":113},{"level":45,"moves":[252,146,203,179],"species":115},{"level":48,"moves":[42,29,7,95],"species":308},{"level":50,"moves":[63,53,85,247],"species":366}],"party_address":3226700,"script_address":0},{"address":3261592,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":52,"moves":[59,247,38,126],"species":366},{"level":52,"moves":[113,47,29,8],"species":242},{"level":50,"moves":[252,146,203,179],"species":115},{"level":53,"moves":[42,29,7,95],"species":308},{"level":55,"moves":[63,53,85,247],"species":366}],"party_address":3226780,"script_address":0},{"address":3261632,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":57,"moves":[59,247,38,126],"species":366},{"level":57,"moves":[182,47,29,8],"species":242},{"level":55,"moves":[252,146,203,179],"species":115},{"level":57,"moves":[36,182,126,89],"species":128},{"level":58,"moves":[42,29,7,95],"species":308},{"level":60,"moves":[63,53,85,247],"species":366}],"party_address":3226860,"script_address":0},{"address":3261672,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":40,"moves":[86,85,182,58],"species":147},{"level":38,"moves":[241,76,76,89],"species":369},{"level":41,"moves":[57,48,182,76],"species":310},{"level":43,"moves":[18,191,211,76],"species":227},{"level":45,"moves":[76,156,93,89],"species":359}],"party_address":3226956,"script_address":0},{"address":3261712,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":43,"moves":[95,94,115,138],"species":163},{"level":43,"moves":[241,76,76,89],"species":369},{"level":45,"moves":[86,85,182,58],"species":148},{"level":46,"moves":[57,48,182,76],"species":310},{"level":48,"moves":[18,191,211,76],"species":227},{"level":50,"moves":[76,156,93,89],"species":359}],"party_address":3227036,"script_address":0},{"address":3261752,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":48,"moves":[95,94,115,138],"species":164},{"level":49,"moves":[241,76,76,89],"species":369},{"level":50,"moves":[86,85,182,58],"species":148},{"level":51,"moves":[57,48,182,76],"species":310},{"level":53,"moves":[18,191,211,76],"species":227},{"level":55,"moves":[76,156,93,89],"species":359}],"party_address":3227132,"script_address":0},{"address":3261792,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":53,"moves":[95,94,115,138],"species":164},{"level":54,"moves":[241,76,76,89],"species":369},{"level":55,"moves":[57,48,182,76],"species":310},{"level":55,"moves":[63,85,89,58],"species":149},{"level":58,"moves":[18,191,211,76],"species":227},{"level":60,"moves":[143,156,93,89],"species":359}],"party_address":3227228,"script_address":0},{"address":3261832,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":48,"moves":[25,94,91,182],"species":79},{"level":49,"moves":[89,246,94,113],"species":319},{"level":49,"moves":[94,156,109,91],"species":178},{"level":50,"moves":[89,94,156,91],"species":348},{"level":50,"moves":[241,76,94,53],"species":349}],"party_address":3227324,"script_address":0},{"address":3261872,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":53,"moves":[95,138,29,182],"species":96},{"level":53,"moves":[25,94,91,182],"species":79},{"level":54,"moves":[89,153,94,113],"species":319},{"level":54,"moves":[94,156,109,91],"species":178},{"level":55,"moves":[89,94,156,91],"species":348},{"level":55,"moves":[241,76,94,53],"species":349}],"party_address":3227404,"script_address":0},{"address":3261912,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":58,"moves":[95,138,29,182],"species":97},{"level":59,"moves":[89,153,94,113],"species":319},{"level":58,"moves":[25,94,91,182],"species":79},{"level":59,"moves":[94,156,109,91],"species":178},{"level":60,"moves":[89,94,156,91],"species":348},{"level":60,"moves":[241,76,94,53],"species":349}],"party_address":3227500,"script_address":0},{"address":3261952,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":63,"moves":[95,138,29,182],"species":97},{"level":64,"moves":[89,153,94,113],"species":319},{"level":63,"moves":[25,94,91,182],"species":199},{"level":64,"moves":[94,156,109,91],"species":178},{"level":65,"moves":[89,94,156,91],"species":348},{"level":65,"moves":[241,76,94,53],"species":349}],"party_address":3227596,"script_address":0},{"address":3261992,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":46,"moves":[95,240,182,56],"species":60},{"level":46,"moves":[240,96,104,90],"species":324},{"level":48,"moves":[96,34,182,58],"species":343},{"level":48,"moves":[156,152,13,104],"species":327},{"level":51,"moves":[96,104,58,156],"species":230}],"party_address":3227692,"script_address":0},{"address":3262032,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":50,"moves":[95,240,182,56],"species":61},{"level":51,"moves":[240,96,104,90],"species":324},{"level":53,"moves":[96,34,182,58],"species":343},{"level":53,"moves":[156,12,13,104],"species":327},{"level":56,"moves":[96,104,58,156],"species":230}],"party_address":3227772,"script_address":0},{"address":3262072,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":56,"moves":[56,195,58,109],"species":131},{"level":58,"moves":[240,96,104,90],"species":324},{"level":56,"moves":[95,240,182,56],"species":61},{"level":58,"moves":[96,34,182,58],"species":343},{"level":58,"moves":[156,12,13,104],"species":327},{"level":61,"moves":[96,104,58,156],"species":230}],"party_address":3227852,"script_address":0},{"address":3262112,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":61,"moves":[56,195,58,109],"species":131},{"level":63,"moves":[240,96,104,90],"species":324},{"level":61,"moves":[95,240,56,195],"species":186},{"level":63,"moves":[96,34,182,73],"species":343},{"level":63,"moves":[156,12,13,104],"species":327},{"level":66,"moves":[96,104,58,156],"species":230}],"party_address":3227948,"script_address":0},{"address":3262152,"battle_type":0,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":17,"moves":[95,98,204,0],"species":387},{"level":17,"moves":[95,98,109,0],"species":386}],"party_address":3228044,"script_address":2167732},{"address":3262192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":369}],"party_address":3228076,"script_address":2202422},{"address":3262232,"battle_type":3,"data_type":"ITEM_CUSTOM_MOVES","party":[{"level":77,"moves":[92,76,191,211],"species":227},{"level":75,"moves":[115,113,246,89],"species":319},{"level":76,"moves":[87,89,76,81],"species":384},{"level":76,"moves":[202,246,19,109],"species":389},{"level":76,"moves":[96,246,76,163],"species":391},{"level":78,"moves":[89,94,53,247],"species":400}],"party_address":3228084,"script_address":2354502},{"address":3262272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":398}],"party_address":3228180,"script_address":0},{"address":3262312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":398}],"party_address":3228188,"script_address":0},{"address":3262352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":398}],"party_address":3228196,"script_address":0},{"address":3262392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":398}],"party_address":3228204,"script_address":0},{"address":3262432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":398}],"party_address":3228212,"script_address":0},{"address":3262472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":398}],"party_address":3228220,"script_address":0},{"address":3262512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":398}],"party_address":3228228,"script_address":0},{"address":3262552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":27},{"level":31,"species":27}],"party_address":3228236,"script_address":0},{"address":3262592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":320},{"level":33,"species":27},{"level":33,"species":27}],"party_address":3228252,"script_address":0},{"address":3262632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":320},{"level":35,"species":27},{"level":35,"species":27}],"party_address":3228276,"script_address":0},{"address":3262672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":320},{"level":37,"species":28},{"level":37,"species":28}],"party_address":3228300,"script_address":0},{"address":3262712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":309},{"level":30,"species":66},{"level":30,"species":72}],"party_address":3228324,"script_address":0},{"address":3262752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":32,"species":310},{"level":32,"species":66},{"level":32,"species":72}],"party_address":3228348,"script_address":0},{"address":3262792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":310},{"level":34,"species":66},{"level":34,"species":73}],"party_address":3228372,"script_address":0},{"address":3262832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":310},{"level":36,"species":67},{"level":36,"species":73}],"party_address":3228396,"script_address":0},{"address":3262872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":120},{"level":37,"species":120}],"party_address":3228420,"script_address":0},{"address":3262912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":309},{"level":39,"species":120},{"level":39,"species":120}],"party_address":3228436,"script_address":0},{"address":3262952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":310},{"level":41,"species":120},{"level":41,"species":120}],"party_address":3228460,"script_address":0},{"address":3262992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":43,"species":310},{"level":43,"species":121},{"level":43,"species":121}],"party_address":3228484,"script_address":0},{"address":3263032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":67},{"level":37,"species":67}],"party_address":3228508,"script_address":0},{"address":3263072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":335},{"level":39,"species":67},{"level":39,"species":67}],"party_address":3228524,"script_address":0},{"address":3263112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":336},{"level":41,"species":67},{"level":41,"species":67}],"party_address":3228548,"script_address":0},{"address":3263152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":43,"species":336},{"level":43,"species":68},{"level":43,"species":68}],"party_address":3228572,"script_address":0},{"address":3263192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":371},{"level":35,"species":365}],"party_address":3228596,"script_address":0},{"address":3263232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":308},{"level":37,"species":371},{"level":37,"species":365}],"party_address":3228612,"script_address":0},{"address":3263272,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":308},{"level":39,"species":371},{"level":39,"species":365}],"party_address":3228636,"script_address":0},{"address":3263312,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":308},{"level":41,"species":372},{"level":41,"species":366}],"party_address":3228660,"script_address":0},{"address":3263352,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":337},{"level":35,"species":337},{"level":35,"species":371}],"party_address":3228684,"script_address":0},{"address":3263392,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":337},{"level":37,"species":338},{"level":37,"species":371}],"party_address":3228708,"script_address":0},{"address":3263432,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":39,"species":338},{"level":39,"species":338},{"level":39,"species":371}],"party_address":3228732,"script_address":0},{"address":3263472,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":338},{"level":41,"species":338},{"level":41,"species":372}],"party_address":3228756,"script_address":0},{"address":3263512,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":26,"species":74},{"level":26,"species":339}],"party_address":3228780,"script_address":0},{"address":3263552,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":28,"species":66},{"level":28,"species":339},{"level":28,"species":75}],"party_address":3228796,"script_address":0},{"address":3263592,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":30,"species":66},{"level":30,"species":339},{"level":30,"species":75}],"party_address":3228820,"script_address":0},{"address":3263632,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":67},{"level":33,"species":340},{"level":33,"species":76}],"party_address":3228844,"script_address":0},{"address":3263672,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":31,"species":315},{"level":31,"species":287},{"level":31,"species":288},{"level":31,"species":295},{"level":31,"species":298},{"level":31,"species":304}],"party_address":3228868,"script_address":0},{"address":3263712,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":33,"species":315},{"level":33,"species":287},{"level":33,"species":289},{"level":33,"species":296},{"level":33,"species":299},{"level":33,"species":304}],"party_address":3228916,"script_address":0},{"address":3263752,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":35,"species":316},{"level":35,"species":287},{"level":35,"species":289},{"level":35,"species":296},{"level":35,"species":299},{"level":35,"species":305}],"party_address":3228964,"script_address":0},{"address":3263792,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":37,"species":316},{"level":37,"species":287},{"level":37,"species":289},{"level":37,"species":297},{"level":37,"species":300},{"level":37,"species":305}],"party_address":3229012,"script_address":0},{"address":3263832,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":34,"species":313},{"level":34,"species":116}],"party_address":3229060,"script_address":0},{"address":3263872,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":36,"species":325},{"level":36,"species":313},{"level":36,"species":117}],"party_address":3229076,"script_address":0},{"address":3263912,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":38,"species":325},{"level":38,"species":313},{"level":38,"species":117}],"party_address":3229100,"script_address":0},{"address":3263952,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":40,"species":325},{"level":40,"species":314},{"level":40,"species":230}],"party_address":3229124,"script_address":0},{"address":3263992,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":411}],"party_address":3229148,"script_address":2564791},{"address":3264032,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":378},{"level":41,"species":64}],"party_address":3229156,"script_address":2564822},{"address":3264072,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":41,"species":202}],"party_address":3229172,"script_address":0},{"address":3264112,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":4}],"party_address":3229180,"script_address":0},{"address":3264152,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":1}],"party_address":3229188,"script_address":0},{"address":3264192,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":405}],"party_address":3229196,"script_address":0},{"address":3264232,"battle_type":0,"data_type":"NO_ITEM_DEFAULT_MOVES","party":[{"level":5,"species":404}],"party_address":3229204,"script_address":0}],"warps":{"MAP_ABANDONED_SHIP_CAPTAINS_OFFICE:0,1/MAP_ABANDONED_SHIP_DECK:4":"MAP_ABANDONED_SHIP_DECK:4/MAP_ABANDONED_SHIP_CAPTAINS_OFFICE:0","MAP_ABANDONED_SHIP_CORRIDORS_1F:0,1/MAP_ABANDONED_SHIP_DECK:2":"MAP_ABANDONED_SHIP_DECK:2/MAP_ABANDONED_SHIP_CORRIDORS_1F:1","MAP_ABANDONED_SHIP_CORRIDORS_1F:10/MAP_ABANDONED_SHIP_CORRIDORS_B1F:6":"MAP_ABANDONED_SHIP_CORRIDORS_B1F:6/MAP_ABANDONED_SHIP_CORRIDORS_1F:10","MAP_ABANDONED_SHIP_CORRIDORS_1F:11/MAP_ABANDONED_SHIP_ROOMS2_1F:2":"MAP_ABANDONED_SHIP_ROOMS2_1F:2/MAP_ABANDONED_SHIP_CORRIDORS_1F:11","MAP_ABANDONED_SHIP_CORRIDORS_1F:2,3/MAP_ABANDONED_SHIP_DECK:3":"MAP_ABANDONED_SHIP_DECK:3/MAP_ABANDONED_SHIP_CORRIDORS_1F:2","MAP_ABANDONED_SHIP_CORRIDORS_1F:4/MAP_ABANDONED_SHIP_ROOMS_1F:0":"MAP_ABANDONED_SHIP_ROOMS_1F:0,1/MAP_ABANDONED_SHIP_CORRIDORS_1F:4","MAP_ABANDONED_SHIP_CORRIDORS_1F:5/MAP_ABANDONED_SHIP_ROOMS_1F:3":"MAP_ABANDONED_SHIP_ROOMS_1F:3,5/MAP_ABANDONED_SHIP_CORRIDORS_1F:5","MAP_ABANDONED_SHIP_CORRIDORS_1F:6/MAP_ABANDONED_SHIP_ROOMS_1F:2":"MAP_ABANDONED_SHIP_ROOMS_1F:2/MAP_ABANDONED_SHIP_CORRIDORS_1F:6","MAP_ABANDONED_SHIP_CORRIDORS_1F:7/MAP_ABANDONED_SHIP_ROOMS_1F:4":"MAP_ABANDONED_SHIP_ROOMS_1F:4/MAP_ABANDONED_SHIP_CORRIDORS_1F:7","MAP_ABANDONED_SHIP_CORRIDORS_1F:8/MAP_ABANDONED_SHIP_ROOMS2_1F:0":"MAP_ABANDONED_SHIP_ROOMS2_1F:0,1/MAP_ABANDONED_SHIP_CORRIDORS_1F:8","MAP_ABANDONED_SHIP_CORRIDORS_1F:9/MAP_ABANDONED_SHIP_CORRIDORS_B1F:7":"MAP_ABANDONED_SHIP_CORRIDORS_B1F:7/MAP_ABANDONED_SHIP_CORRIDORS_1F:9","MAP_ABANDONED_SHIP_CORRIDORS_B1F:0/MAP_ABANDONED_SHIP_ROOMS2_B1F:2":"MAP_ABANDONED_SHIP_ROOMS2_B1F:2,3/MAP_ABANDONED_SHIP_CORRIDORS_B1F:0","MAP_ABANDONED_SHIP_CORRIDORS_B1F:1/MAP_ABANDONED_SHIP_ROOMS2_B1F:0":"MAP_ABANDONED_SHIP_ROOMS2_B1F:0,1/MAP_ABANDONED_SHIP_CORRIDORS_B1F:1","MAP_ABANDONED_SHIP_CORRIDORS_B1F:2/MAP_ABANDONED_SHIP_ROOMS_B1F:0":"MAP_ABANDONED_SHIP_ROOMS_B1F:0/MAP_ABANDONED_SHIP_CORRIDORS_B1F:2","MAP_ABANDONED_SHIP_CORRIDORS_B1F:3/MAP_ABANDONED_SHIP_ROOMS_B1F:1":"MAP_ABANDONED_SHIP_ROOMS_B1F:1/MAP_ABANDONED_SHIP_CORRIDORS_B1F:3","MAP_ABANDONED_SHIP_CORRIDORS_B1F:4/MAP_ABANDONED_SHIP_ROOMS_B1F:2":"MAP_ABANDONED_SHIP_ROOMS_B1F:2/MAP_ABANDONED_SHIP_CORRIDORS_B1F:4","MAP_ABANDONED_SHIP_CORRIDORS_B1F:5/MAP_ABANDONED_SHIP_ROOM_B1F:0":"MAP_ABANDONED_SHIP_ROOM_B1F:0,1/MAP_ABANDONED_SHIP_CORRIDORS_B1F:5","MAP_ABANDONED_SHIP_CORRIDORS_B1F:6/MAP_ABANDONED_SHIP_CORRIDORS_1F:10":"MAP_ABANDONED_SHIP_CORRIDORS_1F:10/MAP_ABANDONED_SHIP_CORRIDORS_B1F:6","MAP_ABANDONED_SHIP_CORRIDORS_B1F:7/MAP_ABANDONED_SHIP_CORRIDORS_1F:9":"MAP_ABANDONED_SHIP_CORRIDORS_1F:9/MAP_ABANDONED_SHIP_CORRIDORS_B1F:7","MAP_ABANDONED_SHIP_DECK:0,1/MAP_ROUTE108:0":"MAP_ROUTE108:0/MAP_ABANDONED_SHIP_DECK:0","MAP_ABANDONED_SHIP_DECK:2/MAP_ABANDONED_SHIP_CORRIDORS_1F:1":"MAP_ABANDONED_SHIP_CORRIDORS_1F:0,1/MAP_ABANDONED_SHIP_DECK:2","MAP_ABANDONED_SHIP_DECK:3/MAP_ABANDONED_SHIP_CORRIDORS_1F:2":"MAP_ABANDONED_SHIP_CORRIDORS_1F:2,3/MAP_ABANDONED_SHIP_DECK:3","MAP_ABANDONED_SHIP_DECK:4/MAP_ABANDONED_SHIP_CAPTAINS_OFFICE:0":"MAP_ABANDONED_SHIP_CAPTAINS_OFFICE:0,1/MAP_ABANDONED_SHIP_DECK:4","MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:0/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:0":"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:0,1/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:0","MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:1/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:2":"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:2,3/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:1","MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:2/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:4":"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:4,5/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:2","MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:3/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:6":"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:6/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:3","MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:4/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:7":"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:7/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:4","MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:5/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:8":"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:8/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:5","MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:0,1/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:0":"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:0/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:0","MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:2,3/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:1":"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:1/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:2","MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:4,5/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:2":"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:2/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:4","MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:6/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:3":"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:3/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:6","MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:7/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:4":"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:4/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:7","MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:8/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:5":"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:5/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:8","MAP_ABANDONED_SHIP_ROOMS2_1F:0,1/MAP_ABANDONED_SHIP_CORRIDORS_1F:8":"MAP_ABANDONED_SHIP_CORRIDORS_1F:8/MAP_ABANDONED_SHIP_ROOMS2_1F:0","MAP_ABANDONED_SHIP_ROOMS2_1F:2/MAP_ABANDONED_SHIP_CORRIDORS_1F:11":"MAP_ABANDONED_SHIP_CORRIDORS_1F:11/MAP_ABANDONED_SHIP_ROOMS2_1F:2","MAP_ABANDONED_SHIP_ROOMS2_B1F:0,1/MAP_ABANDONED_SHIP_CORRIDORS_B1F:1":"MAP_ABANDONED_SHIP_CORRIDORS_B1F:1/MAP_ABANDONED_SHIP_ROOMS2_B1F:0","MAP_ABANDONED_SHIP_ROOMS2_B1F:2,3/MAP_ABANDONED_SHIP_CORRIDORS_B1F:0":"MAP_ABANDONED_SHIP_CORRIDORS_B1F:0/MAP_ABANDONED_SHIP_ROOMS2_B1F:2","MAP_ABANDONED_SHIP_ROOMS_1F:0,1/MAP_ABANDONED_SHIP_CORRIDORS_1F:4":"MAP_ABANDONED_SHIP_CORRIDORS_1F:4/MAP_ABANDONED_SHIP_ROOMS_1F:0","MAP_ABANDONED_SHIP_ROOMS_1F:2/MAP_ABANDONED_SHIP_CORRIDORS_1F:6":"MAP_ABANDONED_SHIP_CORRIDORS_1F:6/MAP_ABANDONED_SHIP_ROOMS_1F:2","MAP_ABANDONED_SHIP_ROOMS_1F:3,5/MAP_ABANDONED_SHIP_CORRIDORS_1F:5":"MAP_ABANDONED_SHIP_CORRIDORS_1F:5/MAP_ABANDONED_SHIP_ROOMS_1F:3","MAP_ABANDONED_SHIP_ROOMS_1F:4/MAP_ABANDONED_SHIP_CORRIDORS_1F:7":"MAP_ABANDONED_SHIP_CORRIDORS_1F:7/MAP_ABANDONED_SHIP_ROOMS_1F:4","MAP_ABANDONED_SHIP_ROOMS_B1F:0/MAP_ABANDONED_SHIP_CORRIDORS_B1F:2":"MAP_ABANDONED_SHIP_CORRIDORS_B1F:2/MAP_ABANDONED_SHIP_ROOMS_B1F:0","MAP_ABANDONED_SHIP_ROOMS_B1F:1/MAP_ABANDONED_SHIP_CORRIDORS_B1F:3":"MAP_ABANDONED_SHIP_CORRIDORS_B1F:3/MAP_ABANDONED_SHIP_ROOMS_B1F:1","MAP_ABANDONED_SHIP_ROOMS_B1F:2/MAP_ABANDONED_SHIP_CORRIDORS_B1F:4":"MAP_ABANDONED_SHIP_CORRIDORS_B1F:4/MAP_ABANDONED_SHIP_ROOMS_B1F:2","MAP_ABANDONED_SHIP_ROOM_B1F:0,1/MAP_ABANDONED_SHIP_CORRIDORS_B1F:5":"MAP_ABANDONED_SHIP_CORRIDORS_B1F:5/MAP_ABANDONED_SHIP_ROOM_B1F:0","MAP_ABANDONED_SHIP_UNDERWATER1:0,1/MAP_ABANDONED_SHIP_UNDERWATER2:0":"MAP_ABANDONED_SHIP_UNDERWATER2:0/MAP_ABANDONED_SHIP_UNDERWATER1:0","MAP_ABANDONED_SHIP_UNDERWATER2:0/MAP_ABANDONED_SHIP_UNDERWATER1:0":"MAP_ABANDONED_SHIP_UNDERWATER1:0,1/MAP_ABANDONED_SHIP_UNDERWATER2:0","MAP_ALTERING_CAVE:0/MAP_ROUTE103:0":"MAP_ROUTE103:0/MAP_ALTERING_CAVE:0","MAP_ANCIENT_TOMB:0/MAP_ROUTE120:0":"MAP_ROUTE120:0/MAP_ANCIENT_TOMB:0","MAP_ANCIENT_TOMB:1/MAP_ANCIENT_TOMB:2":"MAP_ANCIENT_TOMB:2/MAP_ANCIENT_TOMB:1","MAP_ANCIENT_TOMB:2/MAP_ANCIENT_TOMB:1":"MAP_ANCIENT_TOMB:1/MAP_ANCIENT_TOMB:2","MAP_AQUA_HIDEOUT_1F:0,1/MAP_LILYCOVE_CITY:6":"MAP_LILYCOVE_CITY:6/MAP_AQUA_HIDEOUT_1F:0","MAP_AQUA_HIDEOUT_1F:2/MAP_AQUA_HIDEOUT_B1F:0":"MAP_AQUA_HIDEOUT_B1F:0/MAP_AQUA_HIDEOUT_1F:2","MAP_AQUA_HIDEOUT_B1F:0/MAP_AQUA_HIDEOUT_1F:2":"MAP_AQUA_HIDEOUT_1F:2/MAP_AQUA_HIDEOUT_B1F:0","MAP_AQUA_HIDEOUT_B1F:1/MAP_AQUA_HIDEOUT_B2F:0":"MAP_AQUA_HIDEOUT_B2F:0/MAP_AQUA_HIDEOUT_B1F:1","MAP_AQUA_HIDEOUT_B1F:10/MAP_AQUA_HIDEOUT_B1F:6":"MAP_AQUA_HIDEOUT_B1F:6/MAP_AQUA_HIDEOUT_B1F:10","MAP_AQUA_HIDEOUT_B1F:11/MAP_AQUA_HIDEOUT_B1F:22":"MAP_AQUA_HIDEOUT_B1F:22/MAP_AQUA_HIDEOUT_B1F:11","MAP_AQUA_HIDEOUT_B1F:12/MAP_AQUA_HIDEOUT_B1F:9":"MAP_AQUA_HIDEOUT_B1F:9/MAP_AQUA_HIDEOUT_B1F:12","MAP_AQUA_HIDEOUT_B1F:13/MAP_AQUA_HIDEOUT_B1F:18":"MAP_AQUA_HIDEOUT_B1F:18/MAP_AQUA_HIDEOUT_B1F:13","MAP_AQUA_HIDEOUT_B1F:14/MAP_AQUA_HIDEOUT_B1F:12!":"MAP_AQUA_HIDEOUT_B1F:12/MAP_AQUA_HIDEOUT_B1F:9","MAP_AQUA_HIDEOUT_B1F:15/MAP_AQUA_HIDEOUT_B1F:16":"MAP_AQUA_HIDEOUT_B1F:16/MAP_AQUA_HIDEOUT_B1F:15","MAP_AQUA_HIDEOUT_B1F:16/MAP_AQUA_HIDEOUT_B1F:15":"MAP_AQUA_HIDEOUT_B1F:15/MAP_AQUA_HIDEOUT_B1F:16","MAP_AQUA_HIDEOUT_B1F:17/MAP_AQUA_HIDEOUT_B1F:20":"MAP_AQUA_HIDEOUT_B1F:20/MAP_AQUA_HIDEOUT_B1F:17","MAP_AQUA_HIDEOUT_B1F:18/MAP_AQUA_HIDEOUT_B1F:13":"MAP_AQUA_HIDEOUT_B1F:13/MAP_AQUA_HIDEOUT_B1F:18","MAP_AQUA_HIDEOUT_B1F:19/MAP_AQUA_HIDEOUT_B1F:24":"MAP_AQUA_HIDEOUT_B1F:24/MAP_AQUA_HIDEOUT_B1F:19","MAP_AQUA_HIDEOUT_B1F:2/MAP_AQUA_HIDEOUT_B2F:1":"MAP_AQUA_HIDEOUT_B2F:1/MAP_AQUA_HIDEOUT_B1F:2","MAP_AQUA_HIDEOUT_B1F:20/MAP_AQUA_HIDEOUT_B1F:17":"MAP_AQUA_HIDEOUT_B1F:17/MAP_AQUA_HIDEOUT_B1F:20","MAP_AQUA_HIDEOUT_B1F:21/MAP_AQUA_HIDEOUT_B1F:12!":"MAP_AQUA_HIDEOUT_B1F:12/MAP_AQUA_HIDEOUT_B1F:9","MAP_AQUA_HIDEOUT_B1F:22/MAP_AQUA_HIDEOUT_B1F:11":"MAP_AQUA_HIDEOUT_B1F:11/MAP_AQUA_HIDEOUT_B1F:22","MAP_AQUA_HIDEOUT_B1F:23/MAP_AQUA_HIDEOUT_B1F:17!":"MAP_AQUA_HIDEOUT_B1F:17/MAP_AQUA_HIDEOUT_B1F:20","MAP_AQUA_HIDEOUT_B1F:24/MAP_AQUA_HIDEOUT_B1F:19":"MAP_AQUA_HIDEOUT_B1F:19/MAP_AQUA_HIDEOUT_B1F:24","MAP_AQUA_HIDEOUT_B1F:3/MAP_AQUA_HIDEOUT_B2F:2":"MAP_AQUA_HIDEOUT_B2F:2/MAP_AQUA_HIDEOUT_B1F:3","MAP_AQUA_HIDEOUT_B1F:4/MAP_AQUA_HIDEOUT_B1F:7":"MAP_AQUA_HIDEOUT_B1F:7/MAP_AQUA_HIDEOUT_B1F:4","MAP_AQUA_HIDEOUT_B1F:5/MAP_AQUA_HIDEOUT_B1F:8":"MAP_AQUA_HIDEOUT_B1F:8/MAP_AQUA_HIDEOUT_B1F:5","MAP_AQUA_HIDEOUT_B1F:6/MAP_AQUA_HIDEOUT_B1F:10":"MAP_AQUA_HIDEOUT_B1F:10/MAP_AQUA_HIDEOUT_B1F:6","MAP_AQUA_HIDEOUT_B1F:7/MAP_AQUA_HIDEOUT_B1F:4":"MAP_AQUA_HIDEOUT_B1F:4/MAP_AQUA_HIDEOUT_B1F:7","MAP_AQUA_HIDEOUT_B1F:8/MAP_AQUA_HIDEOUT_B1F:5":"MAP_AQUA_HIDEOUT_B1F:5/MAP_AQUA_HIDEOUT_B1F:8","MAP_AQUA_HIDEOUT_B1F:9/MAP_AQUA_HIDEOUT_B1F:12":"MAP_AQUA_HIDEOUT_B1F:12/MAP_AQUA_HIDEOUT_B1F:9","MAP_AQUA_HIDEOUT_B2F:0/MAP_AQUA_HIDEOUT_B1F:1":"MAP_AQUA_HIDEOUT_B1F:1/MAP_AQUA_HIDEOUT_B2F:0","MAP_AQUA_HIDEOUT_B2F:1/MAP_AQUA_HIDEOUT_B1F:2":"MAP_AQUA_HIDEOUT_B1F:2/MAP_AQUA_HIDEOUT_B2F:1","MAP_AQUA_HIDEOUT_B2F:2/MAP_AQUA_HIDEOUT_B1F:3":"MAP_AQUA_HIDEOUT_B1F:3/MAP_AQUA_HIDEOUT_B2F:2","MAP_AQUA_HIDEOUT_B2F:3/MAP_AQUA_HIDEOUT_B2F:5":"MAP_AQUA_HIDEOUT_B2F:5/MAP_AQUA_HIDEOUT_B2F:3","MAP_AQUA_HIDEOUT_B2F:4/MAP_AQUA_HIDEOUT_B2F:8":"MAP_AQUA_HIDEOUT_B2F:8/MAP_AQUA_HIDEOUT_B2F:4","MAP_AQUA_HIDEOUT_B2F:5/MAP_AQUA_HIDEOUT_B2F:3":"MAP_AQUA_HIDEOUT_B2F:3/MAP_AQUA_HIDEOUT_B2F:5","MAP_AQUA_HIDEOUT_B2F:6/MAP_AQUA_HIDEOUT_B2F:7":"MAP_AQUA_HIDEOUT_B2F:7/MAP_AQUA_HIDEOUT_B2F:6","MAP_AQUA_HIDEOUT_B2F:7/MAP_AQUA_HIDEOUT_B2F:6":"MAP_AQUA_HIDEOUT_B2F:6/MAP_AQUA_HIDEOUT_B2F:7","MAP_AQUA_HIDEOUT_B2F:8/MAP_AQUA_HIDEOUT_B2F:4":"MAP_AQUA_HIDEOUT_B2F:4/MAP_AQUA_HIDEOUT_B2F:8","MAP_AQUA_HIDEOUT_B2F:9/MAP_AQUA_HIDEOUT_B1F:4!":"MAP_AQUA_HIDEOUT_B1F:4/MAP_AQUA_HIDEOUT_B1F:7","MAP_ARTISAN_CAVE_1F:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:13":"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:13/MAP_ARTISAN_CAVE_1F:0","MAP_ARTISAN_CAVE_1F:1/MAP_ARTISAN_CAVE_B1F:1":"MAP_ARTISAN_CAVE_B1F:1/MAP_ARTISAN_CAVE_1F:1","MAP_ARTISAN_CAVE_B1F:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:10":"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:10/MAP_ARTISAN_CAVE_B1F:0","MAP_ARTISAN_CAVE_B1F:1/MAP_ARTISAN_CAVE_1F:1":"MAP_ARTISAN_CAVE_1F:1/MAP_ARTISAN_CAVE_B1F:1","MAP_BATTLE_COLOSSEUM_2P:0,1/MAP_DYNAMIC:-1!":"","MAP_BATTLE_COLOSSEUM_4P:0,1,2,3/MAP_DYNAMIC:-1!":"","MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:1":"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:1/MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY:0","MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1!":"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1/MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0","MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1":"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1/MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0","MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1!":"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1/MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0","MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:2":"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:2/MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY:0","MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2":"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0","MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2":"MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0","MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0":"MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2","MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:3/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0!":"MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2","MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2":"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0","MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0":"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2","MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY:0,1,2/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:0":"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:0/MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY:0","MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:3":"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:3/MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY:0","MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:2":"MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:2/MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM:0","MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:0":"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:0/MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:0","MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:2/MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM:0":"MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:2","MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER:0,1,2/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:6":"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:6/MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER:0","MAP_BATTLE_FRONTIER_LOUNGE1:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:5":"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:5/MAP_BATTLE_FRONTIER_LOUNGE1:0","MAP_BATTLE_FRONTIER_LOUNGE2:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:3":"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:3/MAP_BATTLE_FRONTIER_LOUNGE2:0","MAP_BATTLE_FRONTIER_LOUNGE3:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:9":"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:9/MAP_BATTLE_FRONTIER_LOUNGE3:0","MAP_BATTLE_FRONTIER_LOUNGE4:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:6":"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:6/MAP_BATTLE_FRONTIER_LOUNGE4:0","MAP_BATTLE_FRONTIER_LOUNGE5:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:7":"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:7/MAP_BATTLE_FRONTIER_LOUNGE5:0","MAP_BATTLE_FRONTIER_LOUNGE6:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:8":"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:8/MAP_BATTLE_FRONTIER_LOUNGE6:0","MAP_BATTLE_FRONTIER_LOUNGE7:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:7":"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:7/MAP_BATTLE_FRONTIER_LOUNGE7:0","MAP_BATTLE_FRONTIER_LOUNGE8:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:10":"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:10/MAP_BATTLE_FRONTIER_LOUNGE8:0","MAP_BATTLE_FRONTIER_LOUNGE9:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:11":"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:11/MAP_BATTLE_FRONTIER_LOUNGE9:0","MAP_BATTLE_FRONTIER_MART:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:4":"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:4/MAP_BATTLE_FRONTIER_MART:0","MAP_BATTLE_FRONTIER_OUTSIDE_EAST:0/MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:0":"MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:0","MAP_BATTLE_FRONTIER_OUTSIDE_EAST:1/MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY:0":"MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:1","MAP_BATTLE_FRONTIER_OUTSIDE_EAST:10/MAP_BATTLE_FRONTIER_LOUNGE8:0":"MAP_BATTLE_FRONTIER_LOUNGE8:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:10","MAP_BATTLE_FRONTIER_OUTSIDE_EAST:11/MAP_BATTLE_FRONTIER_LOUNGE9:0":"MAP_BATTLE_FRONTIER_LOUNGE9:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:11","MAP_BATTLE_FRONTIER_OUTSIDE_EAST:12/MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:0":"MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:12","MAP_BATTLE_FRONTIER_OUTSIDE_EAST:13/MAP_ARTISAN_CAVE_1F:0":"MAP_ARTISAN_CAVE_1F:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:13","MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0":"MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2","MAP_BATTLE_FRONTIER_OUTSIDE_EAST:3/MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY:0":"MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:3","MAP_BATTLE_FRONTIER_OUTSIDE_EAST:4/MAP_BATTLE_FRONTIER_RANKING_HALL:0":"MAP_BATTLE_FRONTIER_RANKING_HALL:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:4","MAP_BATTLE_FRONTIER_OUTSIDE_EAST:5/MAP_BATTLE_FRONTIER_LOUNGE1:0":"MAP_BATTLE_FRONTIER_LOUNGE1:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:5","MAP_BATTLE_FRONTIER_OUTSIDE_EAST:6/MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER:0":"MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER:0,1,2/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:6","MAP_BATTLE_FRONTIER_OUTSIDE_EAST:7/MAP_BATTLE_FRONTIER_LOUNGE5:0":"MAP_BATTLE_FRONTIER_LOUNGE5:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:7","MAP_BATTLE_FRONTIER_OUTSIDE_EAST:8/MAP_BATTLE_FRONTIER_LOUNGE6:0":"MAP_BATTLE_FRONTIER_LOUNGE6:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:8","MAP_BATTLE_FRONTIER_OUTSIDE_EAST:9/MAP_BATTLE_FRONTIER_LOUNGE3:0":"MAP_BATTLE_FRONTIER_LOUNGE3:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:9","MAP_BATTLE_FRONTIER_OUTSIDE_WEST:0/MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY:0":"MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY:0,1,2/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:0","MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1/MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0":"MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1","MAP_BATTLE_FRONTIER_OUTSIDE_WEST:10/MAP_ARTISAN_CAVE_B1F:0":"MAP_ARTISAN_CAVE_B1F:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:10","MAP_BATTLE_FRONTIER_OUTSIDE_WEST:2/MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY:0":"MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:2","MAP_BATTLE_FRONTIER_OUTSIDE_WEST:3/MAP_BATTLE_FRONTIER_LOUNGE2:0":"MAP_BATTLE_FRONTIER_LOUNGE2:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:3","MAP_BATTLE_FRONTIER_OUTSIDE_WEST:4/MAP_BATTLE_FRONTIER_MART:0":"MAP_BATTLE_FRONTIER_MART:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:4","MAP_BATTLE_FRONTIER_OUTSIDE_WEST:5/MAP_BATTLE_FRONTIER_SCOTTS_HOUSE:0":"MAP_BATTLE_FRONTIER_SCOTTS_HOUSE:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:5","MAP_BATTLE_FRONTIER_OUTSIDE_WEST:6/MAP_BATTLE_FRONTIER_LOUNGE4:0":"MAP_BATTLE_FRONTIER_LOUNGE4:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:6","MAP_BATTLE_FRONTIER_OUTSIDE_WEST:7/MAP_BATTLE_FRONTIER_LOUNGE7:0":"MAP_BATTLE_FRONTIER_LOUNGE7:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:7","MAP_BATTLE_FRONTIER_OUTSIDE_WEST:8/MAP_BATTLE_FRONTIER_RECEPTION_GATE:0":"MAP_BATTLE_FRONTIER_RECEPTION_GATE:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:8","MAP_BATTLE_FRONTIER_OUTSIDE_WEST:9/MAP_BATTLE_FRONTIER_RECEPTION_GATE:1":"MAP_BATTLE_FRONTIER_RECEPTION_GATE:1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:9","MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:12":"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:12/MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:0","MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:2/MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:0":"MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:0/MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:2","MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:0/MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:2":"MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:2/MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:0","MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_BATTLE_FRONTIER_RANKING_HALL:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:4":"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:4/MAP_BATTLE_FRONTIER_RANKING_HALL:0","MAP_BATTLE_FRONTIER_RECEPTION_GATE:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:8":"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:8/MAP_BATTLE_FRONTIER_RECEPTION_GATE:0","MAP_BATTLE_FRONTIER_RECEPTION_GATE:1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:9":"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:9/MAP_BATTLE_FRONTIER_RECEPTION_GATE:1","MAP_BATTLE_FRONTIER_SCOTTS_HOUSE:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:5":"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:5/MAP_BATTLE_FRONTIER_SCOTTS_HOUSE:0","MAP_BIRTH_ISLAND_EXTERIOR:0/MAP_BIRTH_ISLAND_HARBOR:0":"MAP_BIRTH_ISLAND_HARBOR:0/MAP_BIRTH_ISLAND_EXTERIOR:0","MAP_BIRTH_ISLAND_HARBOR:0/MAP_BIRTH_ISLAND_EXTERIOR:0":"MAP_BIRTH_ISLAND_EXTERIOR:0/MAP_BIRTH_ISLAND_HARBOR:0","MAP_CAVE_OF_ORIGIN_1F:0/MAP_CAVE_OF_ORIGIN_ENTRANCE:1":"MAP_CAVE_OF_ORIGIN_ENTRANCE:1/MAP_CAVE_OF_ORIGIN_1F:0","MAP_CAVE_OF_ORIGIN_1F:1/MAP_CAVE_OF_ORIGIN_B1F:0":"MAP_CAVE_OF_ORIGIN_B1F:0/MAP_CAVE_OF_ORIGIN_1F:1","MAP_CAVE_OF_ORIGIN_B1F:0/MAP_CAVE_OF_ORIGIN_1F:1":"MAP_CAVE_OF_ORIGIN_1F:1/MAP_CAVE_OF_ORIGIN_B1F:0","MAP_CAVE_OF_ORIGIN_ENTRANCE:0/MAP_SOOTOPOLIS_CITY:3":"MAP_SOOTOPOLIS_CITY:3/MAP_CAVE_OF_ORIGIN_ENTRANCE:0","MAP_CAVE_OF_ORIGIN_ENTRANCE:1/MAP_CAVE_OF_ORIGIN_1F:0":"MAP_CAVE_OF_ORIGIN_1F:0/MAP_CAVE_OF_ORIGIN_ENTRANCE:1","MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1:0/MAP_CAVE_OF_ORIGIN_1F:1!":"MAP_CAVE_OF_ORIGIN_1F:1/MAP_CAVE_OF_ORIGIN_B1F:0","MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1:1/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:0":"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:0/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1:1","MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:0/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1:1":"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1:1/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:0","MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:1/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3:0":"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3:0/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:1","MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3:0/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:1":"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:1/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3:0","MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3:1/MAP_CAVE_OF_ORIGIN_B1F:0!":"MAP_CAVE_OF_ORIGIN_B1F:0/MAP_CAVE_OF_ORIGIN_1F:1","MAP_DESERT_RUINS:0/MAP_ROUTE111:1":"MAP_ROUTE111:1/MAP_DESERT_RUINS:0","MAP_DESERT_RUINS:1/MAP_DESERT_RUINS:2":"MAP_DESERT_RUINS:2/MAP_DESERT_RUINS:1","MAP_DESERT_RUINS:2/MAP_DESERT_RUINS:1":"MAP_DESERT_RUINS:1/MAP_DESERT_RUINS:2","MAP_DESERT_UNDERPASS:0/MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:2":"MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:2/MAP_DESERT_UNDERPASS:0","MAP_DEWFORD_TOWN:0/MAP_DEWFORD_TOWN_HALL:0":"MAP_DEWFORD_TOWN_HALL:0,1/MAP_DEWFORD_TOWN:0","MAP_DEWFORD_TOWN:1/MAP_DEWFORD_TOWN_POKEMON_CENTER_1F:0":"MAP_DEWFORD_TOWN_POKEMON_CENTER_1F:0,1/MAP_DEWFORD_TOWN:1","MAP_DEWFORD_TOWN:2/MAP_DEWFORD_TOWN_GYM:0":"MAP_DEWFORD_TOWN_GYM:0,1/MAP_DEWFORD_TOWN:2","MAP_DEWFORD_TOWN:3/MAP_DEWFORD_TOWN_HOUSE1:0":"MAP_DEWFORD_TOWN_HOUSE1:0,1/MAP_DEWFORD_TOWN:3","MAP_DEWFORD_TOWN:4/MAP_DEWFORD_TOWN_HOUSE2:0":"MAP_DEWFORD_TOWN_HOUSE2:0,1/MAP_DEWFORD_TOWN:4","MAP_DEWFORD_TOWN_GYM:0,1/MAP_DEWFORD_TOWN:2":"MAP_DEWFORD_TOWN:2/MAP_DEWFORD_TOWN_GYM:0","MAP_DEWFORD_TOWN_HALL:0,1/MAP_DEWFORD_TOWN:0":"MAP_DEWFORD_TOWN:0/MAP_DEWFORD_TOWN_HALL:0","MAP_DEWFORD_TOWN_HOUSE1:0,1/MAP_DEWFORD_TOWN:3":"MAP_DEWFORD_TOWN:3/MAP_DEWFORD_TOWN_HOUSE1:0","MAP_DEWFORD_TOWN_HOUSE2:0,1/MAP_DEWFORD_TOWN:4":"MAP_DEWFORD_TOWN:4/MAP_DEWFORD_TOWN_HOUSE2:0","MAP_DEWFORD_TOWN_POKEMON_CENTER_1F:0,1/MAP_DEWFORD_TOWN:1":"MAP_DEWFORD_TOWN:1/MAP_DEWFORD_TOWN_POKEMON_CENTER_1F:0","MAP_DEWFORD_TOWN_POKEMON_CENTER_1F:2/MAP_DEWFORD_TOWN_POKEMON_CENTER_2F:0":"MAP_DEWFORD_TOWN_POKEMON_CENTER_2F:0/MAP_DEWFORD_TOWN_POKEMON_CENTER_1F:2","MAP_DEWFORD_TOWN_POKEMON_CENTER_2F:0/MAP_DEWFORD_TOWN_POKEMON_CENTER_1F:2":"MAP_DEWFORD_TOWN_POKEMON_CENTER_1F:2/MAP_DEWFORD_TOWN_POKEMON_CENTER_2F:0","MAP_DEWFORD_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_DEWFORD_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_EVER_GRANDE_CITY:0/MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:0":"MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:0,1/MAP_EVER_GRANDE_CITY:0","MAP_EVER_GRANDE_CITY:1/MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F:0":"MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F:0,1/MAP_EVER_GRANDE_CITY:1","MAP_EVER_GRANDE_CITY:2/MAP_VICTORY_ROAD_1F:0":"MAP_VICTORY_ROAD_1F:0/MAP_EVER_GRANDE_CITY:2","MAP_EVER_GRANDE_CITY:3/MAP_VICTORY_ROAD_1F:1":"MAP_VICTORY_ROAD_1F:1/MAP_EVER_GRANDE_CITY:3","MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM:0/MAP_EVER_GRANDE_CITY_HALL4:1":"MAP_EVER_GRANDE_CITY_HALL4:1/MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM:0","MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM:1/MAP_EVER_GRANDE_CITY_HALL_OF_FAME:0":"MAP_EVER_GRANDE_CITY_HALL_OF_FAME:0/MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM:1","MAP_EVER_GRANDE_CITY_DRAKES_ROOM:0/MAP_EVER_GRANDE_CITY_HALL3:1":"MAP_EVER_GRANDE_CITY_HALL3:1/MAP_EVER_GRANDE_CITY_DRAKES_ROOM:0","MAP_EVER_GRANDE_CITY_DRAKES_ROOM:1/MAP_EVER_GRANDE_CITY_HALL4:0":"MAP_EVER_GRANDE_CITY_HALL4:0/MAP_EVER_GRANDE_CITY_DRAKES_ROOM:1","MAP_EVER_GRANDE_CITY_GLACIAS_ROOM:0/MAP_EVER_GRANDE_CITY_HALL2:1":"MAP_EVER_GRANDE_CITY_HALL2:1/MAP_EVER_GRANDE_CITY_GLACIAS_ROOM:0","MAP_EVER_GRANDE_CITY_GLACIAS_ROOM:1/MAP_EVER_GRANDE_CITY_HALL3:0":"MAP_EVER_GRANDE_CITY_HALL3:0,2,3/MAP_EVER_GRANDE_CITY_GLACIAS_ROOM:1","MAP_EVER_GRANDE_CITY_HALL1:0,2,3/MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM:1":"MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM:1/MAP_EVER_GRANDE_CITY_HALL1:0","MAP_EVER_GRANDE_CITY_HALL1:1/MAP_EVER_GRANDE_CITY_PHOEBES_ROOM:0":"MAP_EVER_GRANDE_CITY_PHOEBES_ROOM:0/MAP_EVER_GRANDE_CITY_HALL1:1","MAP_EVER_GRANDE_CITY_HALL2:0,2,3/MAP_EVER_GRANDE_CITY_PHOEBES_ROOM:1":"MAP_EVER_GRANDE_CITY_PHOEBES_ROOM:1/MAP_EVER_GRANDE_CITY_HALL2:0","MAP_EVER_GRANDE_CITY_HALL2:1/MAP_EVER_GRANDE_CITY_GLACIAS_ROOM:0":"MAP_EVER_GRANDE_CITY_GLACIAS_ROOM:0/MAP_EVER_GRANDE_CITY_HALL2:1","MAP_EVER_GRANDE_CITY_HALL3:0,2,3/MAP_EVER_GRANDE_CITY_GLACIAS_ROOM:1":"MAP_EVER_GRANDE_CITY_GLACIAS_ROOM:1/MAP_EVER_GRANDE_CITY_HALL3:0","MAP_EVER_GRANDE_CITY_HALL3:1/MAP_EVER_GRANDE_CITY_DRAKES_ROOM:0":"MAP_EVER_GRANDE_CITY_DRAKES_ROOM:0/MAP_EVER_GRANDE_CITY_HALL3:1","MAP_EVER_GRANDE_CITY_HALL4:0/MAP_EVER_GRANDE_CITY_DRAKES_ROOM:1":"MAP_EVER_GRANDE_CITY_DRAKES_ROOM:1/MAP_EVER_GRANDE_CITY_HALL4:0","MAP_EVER_GRANDE_CITY_HALL4:1/MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM:0":"MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM:0/MAP_EVER_GRANDE_CITY_HALL4:1","MAP_EVER_GRANDE_CITY_HALL5:0,2,3/MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:2":"MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:2,3/MAP_EVER_GRANDE_CITY_HALL5:0","MAP_EVER_GRANDE_CITY_HALL5:1/MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM:0":"MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM:0/MAP_EVER_GRANDE_CITY_HALL5:1","MAP_EVER_GRANDE_CITY_HALL_OF_FAME:0/MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM:1":"MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM:1/MAP_EVER_GRANDE_CITY_HALL_OF_FAME:0","MAP_EVER_GRANDE_CITY_PHOEBES_ROOM:0/MAP_EVER_GRANDE_CITY_HALL1:1":"MAP_EVER_GRANDE_CITY_HALL1:1/MAP_EVER_GRANDE_CITY_PHOEBES_ROOM:0","MAP_EVER_GRANDE_CITY_PHOEBES_ROOM:1/MAP_EVER_GRANDE_CITY_HALL2:0":"MAP_EVER_GRANDE_CITY_HALL2:0,2,3/MAP_EVER_GRANDE_CITY_PHOEBES_ROOM:1","MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F:0,1/MAP_EVER_GRANDE_CITY:1":"MAP_EVER_GRANDE_CITY:1/MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F:0","MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F:2/MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F:0":"MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F:0/MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F:2","MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F:0/MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F:2":"MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F:2/MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F:0","MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:0,1/MAP_EVER_GRANDE_CITY:0":"MAP_EVER_GRANDE_CITY:0/MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:0","MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:2,3/MAP_EVER_GRANDE_CITY_HALL5:0":"MAP_EVER_GRANDE_CITY_HALL5:0,2,3/MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:2","MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:4/MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F:0":"MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F:0/MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:4","MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F:0/MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:4":"MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:4/MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F:0","MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM:0/MAP_EVER_GRANDE_CITY_HALL5:1":"MAP_EVER_GRANDE_CITY_HALL5:1/MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM:0","MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM:1/MAP_EVER_GRANDE_CITY_HALL1:0":"MAP_EVER_GRANDE_CITY_HALL1:0,2,3/MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM:1","MAP_FALLARBOR_TOWN:0/MAP_FALLARBOR_TOWN_MART:0":"MAP_FALLARBOR_TOWN_MART:0,1/MAP_FALLARBOR_TOWN:0","MAP_FALLARBOR_TOWN:1/MAP_FALLARBOR_TOWN_BATTLE_TENT_LOBBY:0":"MAP_FALLARBOR_TOWN_BATTLE_TENT_LOBBY:0,1/MAP_FALLARBOR_TOWN:1","MAP_FALLARBOR_TOWN:2/MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F:0":"MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F:0,1/MAP_FALLARBOR_TOWN:2","MAP_FALLARBOR_TOWN:3/MAP_FALLARBOR_TOWN_COZMOS_HOUSE:0":"MAP_FALLARBOR_TOWN_COZMOS_HOUSE:0,1/MAP_FALLARBOR_TOWN:3","MAP_FALLARBOR_TOWN:4/MAP_FALLARBOR_TOWN_MOVE_RELEARNERS_HOUSE:0":"MAP_FALLARBOR_TOWN_MOVE_RELEARNERS_HOUSE:0,1/MAP_FALLARBOR_TOWN:4","MAP_FALLARBOR_TOWN_BATTLE_TENT_LOBBY:0,1/MAP_FALLARBOR_TOWN:1":"MAP_FALLARBOR_TOWN:1/MAP_FALLARBOR_TOWN_BATTLE_TENT_LOBBY:0","MAP_FALLARBOR_TOWN_COZMOS_HOUSE:0,1/MAP_FALLARBOR_TOWN:3":"MAP_FALLARBOR_TOWN:3/MAP_FALLARBOR_TOWN_COZMOS_HOUSE:0","MAP_FALLARBOR_TOWN_MART:0,1/MAP_FALLARBOR_TOWN:0":"MAP_FALLARBOR_TOWN:0/MAP_FALLARBOR_TOWN_MART:0","MAP_FALLARBOR_TOWN_MOVE_RELEARNERS_HOUSE:0,1/MAP_FALLARBOR_TOWN:4":"MAP_FALLARBOR_TOWN:4/MAP_FALLARBOR_TOWN_MOVE_RELEARNERS_HOUSE:0","MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F:0,1/MAP_FALLARBOR_TOWN:2":"MAP_FALLARBOR_TOWN:2/MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F:0","MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F:2/MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F:0":"MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F:0/MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F:2","MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F:0/MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F:2":"MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F:2/MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F:0","MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_FARAWAY_ISLAND_ENTRANCE:0,1/MAP_FARAWAY_ISLAND_INTERIOR:0,1":"MAP_FARAWAY_ISLAND_INTERIOR:0,1/MAP_FARAWAY_ISLAND_ENTRANCE:0,1","MAP_FARAWAY_ISLAND_INTERIOR:0,1/MAP_FARAWAY_ISLAND_ENTRANCE:0,1":"MAP_FARAWAY_ISLAND_ENTRANCE:0,1/MAP_FARAWAY_ISLAND_INTERIOR:0,1","MAP_FIERY_PATH:0/MAP_ROUTE112:4":"MAP_ROUTE112:4/MAP_FIERY_PATH:0","MAP_FIERY_PATH:1/MAP_ROUTE112:5":"MAP_ROUTE112:5/MAP_FIERY_PATH:1","MAP_FORTREE_CITY:0/MAP_FORTREE_CITY_POKEMON_CENTER_1F:0":"MAP_FORTREE_CITY_POKEMON_CENTER_1F:0,1/MAP_FORTREE_CITY:0","MAP_FORTREE_CITY:1/MAP_FORTREE_CITY_HOUSE1:0":"MAP_FORTREE_CITY_HOUSE1:0,1/MAP_FORTREE_CITY:1","MAP_FORTREE_CITY:2/MAP_FORTREE_CITY_GYM:0":"MAP_FORTREE_CITY_GYM:0,1/MAP_FORTREE_CITY:2","MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0":"MAP_FORTREE_CITY_MART:0,1/MAP_FORTREE_CITY:3","MAP_FORTREE_CITY:4/MAP_FORTREE_CITY_HOUSE2:0":"MAP_FORTREE_CITY_HOUSE2:0,1/MAP_FORTREE_CITY:4","MAP_FORTREE_CITY:5/MAP_FORTREE_CITY_HOUSE3:0":"MAP_FORTREE_CITY_HOUSE3:0,1/MAP_FORTREE_CITY:5","MAP_FORTREE_CITY:6/MAP_FORTREE_CITY_HOUSE4:0":"MAP_FORTREE_CITY_HOUSE4:0,1/MAP_FORTREE_CITY:6","MAP_FORTREE_CITY:7/MAP_FORTREE_CITY_HOUSE5:0":"MAP_FORTREE_CITY_HOUSE5:0,1/MAP_FORTREE_CITY:7","MAP_FORTREE_CITY:8/MAP_FORTREE_CITY_DECORATION_SHOP:0":"MAP_FORTREE_CITY_DECORATION_SHOP:0,1/MAP_FORTREE_CITY:8","MAP_FORTREE_CITY_DECORATION_SHOP:0,1/MAP_FORTREE_CITY:8":"MAP_FORTREE_CITY:8/MAP_FORTREE_CITY_DECORATION_SHOP:0","MAP_FORTREE_CITY_GYM:0,1/MAP_FORTREE_CITY:2":"MAP_FORTREE_CITY:2/MAP_FORTREE_CITY_GYM:0","MAP_FORTREE_CITY_HOUSE1:0,1/MAP_FORTREE_CITY:1":"MAP_FORTREE_CITY:1/MAP_FORTREE_CITY_HOUSE1:0","MAP_FORTREE_CITY_HOUSE2:0,1/MAP_FORTREE_CITY:4":"MAP_FORTREE_CITY:4/MAP_FORTREE_CITY_HOUSE2:0","MAP_FORTREE_CITY_HOUSE3:0,1/MAP_FORTREE_CITY:5":"MAP_FORTREE_CITY:5/MAP_FORTREE_CITY_HOUSE3:0","MAP_FORTREE_CITY_HOUSE4:0,1/MAP_FORTREE_CITY:6":"MAP_FORTREE_CITY:6/MAP_FORTREE_CITY_HOUSE4:0","MAP_FORTREE_CITY_HOUSE5:0,1/MAP_FORTREE_CITY:7":"MAP_FORTREE_CITY:7/MAP_FORTREE_CITY_HOUSE5:0","MAP_FORTREE_CITY_MART:0,1/MAP_FORTREE_CITY:3":"MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0","MAP_FORTREE_CITY_POKEMON_CENTER_1F:0,1/MAP_FORTREE_CITY:0":"MAP_FORTREE_CITY:0/MAP_FORTREE_CITY_POKEMON_CENTER_1F:0","MAP_FORTREE_CITY_POKEMON_CENTER_1F:2/MAP_FORTREE_CITY_POKEMON_CENTER_2F:0":"MAP_FORTREE_CITY_POKEMON_CENTER_2F:0/MAP_FORTREE_CITY_POKEMON_CENTER_1F:2","MAP_FORTREE_CITY_POKEMON_CENTER_2F:0/MAP_FORTREE_CITY_POKEMON_CENTER_1F:2":"MAP_FORTREE_CITY_POKEMON_CENTER_1F:2/MAP_FORTREE_CITY_POKEMON_CENTER_2F:0","MAP_FORTREE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_FORTREE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_GRANITE_CAVE_1F:0/MAP_ROUTE106:0":"MAP_ROUTE106:0/MAP_GRANITE_CAVE_1F:0","MAP_GRANITE_CAVE_1F:1/MAP_GRANITE_CAVE_B1F:0":"MAP_GRANITE_CAVE_B1F:0/MAP_GRANITE_CAVE_1F:1","MAP_GRANITE_CAVE_1F:2/MAP_GRANITE_CAVE_B1F:1":"MAP_GRANITE_CAVE_B1F:1/MAP_GRANITE_CAVE_1F:2","MAP_GRANITE_CAVE_1F:3/MAP_GRANITE_CAVE_STEVENS_ROOM:0":"MAP_GRANITE_CAVE_STEVENS_ROOM:0/MAP_GRANITE_CAVE_1F:3","MAP_GRANITE_CAVE_B1F:0/MAP_GRANITE_CAVE_1F:1":"MAP_GRANITE_CAVE_1F:1/MAP_GRANITE_CAVE_B1F:0","MAP_GRANITE_CAVE_B1F:1/MAP_GRANITE_CAVE_1F:2":"MAP_GRANITE_CAVE_1F:2/MAP_GRANITE_CAVE_B1F:1","MAP_GRANITE_CAVE_B1F:2/MAP_GRANITE_CAVE_B2F:0":"MAP_GRANITE_CAVE_B2F:0/MAP_GRANITE_CAVE_B1F:2","MAP_GRANITE_CAVE_B1F:3/MAP_GRANITE_CAVE_B2F:1":"MAP_GRANITE_CAVE_B2F:1/MAP_GRANITE_CAVE_B1F:3","MAP_GRANITE_CAVE_B1F:4/MAP_GRANITE_CAVE_B2F:2":"MAP_GRANITE_CAVE_B2F:2/MAP_GRANITE_CAVE_B1F:4","MAP_GRANITE_CAVE_B1F:5/MAP_GRANITE_CAVE_B2F:3":"MAP_GRANITE_CAVE_B2F:3/MAP_GRANITE_CAVE_B1F:5","MAP_GRANITE_CAVE_B1F:6/MAP_GRANITE_CAVE_B2F:4":"MAP_GRANITE_CAVE_B2F:4/MAP_GRANITE_CAVE_B1F:6","MAP_GRANITE_CAVE_B2F:0/MAP_GRANITE_CAVE_B1F:2":"MAP_GRANITE_CAVE_B1F:2/MAP_GRANITE_CAVE_B2F:0","MAP_GRANITE_CAVE_B2F:1/MAP_GRANITE_CAVE_B1F:3":"MAP_GRANITE_CAVE_B1F:3/MAP_GRANITE_CAVE_B2F:1","MAP_GRANITE_CAVE_B2F:2/MAP_GRANITE_CAVE_B1F:4":"MAP_GRANITE_CAVE_B1F:4/MAP_GRANITE_CAVE_B2F:2","MAP_GRANITE_CAVE_B2F:3/MAP_GRANITE_CAVE_B1F:5":"MAP_GRANITE_CAVE_B1F:5/MAP_GRANITE_CAVE_B2F:3","MAP_GRANITE_CAVE_B2F:4/MAP_GRANITE_CAVE_B1F:6":"MAP_GRANITE_CAVE_B1F:6/MAP_GRANITE_CAVE_B2F:4","MAP_GRANITE_CAVE_STEVENS_ROOM:0/MAP_GRANITE_CAVE_1F:3":"MAP_GRANITE_CAVE_1F:3/MAP_GRANITE_CAVE_STEVENS_ROOM:0","MAP_INSIDE_OF_TRUCK:0,1,2/MAP_DYNAMIC:-1!":"","MAP_ISLAND_CAVE:0/MAP_ROUTE105:0":"MAP_ROUTE105:0/MAP_ISLAND_CAVE:0","MAP_ISLAND_CAVE:1/MAP_ISLAND_CAVE:2":"MAP_ISLAND_CAVE:2/MAP_ISLAND_CAVE:1","MAP_ISLAND_CAVE:2/MAP_ISLAND_CAVE:1":"MAP_ISLAND_CAVE:1/MAP_ISLAND_CAVE:2","MAP_JAGGED_PASS:0,1/MAP_ROUTE112:2,3":"MAP_ROUTE112:2,3/MAP_JAGGED_PASS:0,1","MAP_JAGGED_PASS:2,3/MAP_MT_CHIMNEY:2,3":"MAP_MT_CHIMNEY:2,3/MAP_JAGGED_PASS:2,3","MAP_JAGGED_PASS:4/MAP_MAGMA_HIDEOUT_1F:0":"MAP_MAGMA_HIDEOUT_1F:0/MAP_JAGGED_PASS:4","MAP_LAVARIDGE_TOWN:0/MAP_LAVARIDGE_TOWN_HERB_SHOP:0":"MAP_LAVARIDGE_TOWN_HERB_SHOP:0,1/MAP_LAVARIDGE_TOWN:0","MAP_LAVARIDGE_TOWN:1/MAP_LAVARIDGE_TOWN_GYM_1F:0":"MAP_LAVARIDGE_TOWN_GYM_1F:0,1/MAP_LAVARIDGE_TOWN:1","MAP_LAVARIDGE_TOWN:2/MAP_LAVARIDGE_TOWN_MART:0":"MAP_LAVARIDGE_TOWN_MART:0,1/MAP_LAVARIDGE_TOWN:2","MAP_LAVARIDGE_TOWN:3/MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:0":"MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:0,1/MAP_LAVARIDGE_TOWN:3","MAP_LAVARIDGE_TOWN:4/MAP_LAVARIDGE_TOWN_HOUSE:0":"MAP_LAVARIDGE_TOWN_HOUSE:0,1/MAP_LAVARIDGE_TOWN:4","MAP_LAVARIDGE_TOWN:5/MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:3":"MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:3/MAP_LAVARIDGE_TOWN:5","MAP_LAVARIDGE_TOWN_GYM_1F:0,1/MAP_LAVARIDGE_TOWN:1":"MAP_LAVARIDGE_TOWN:1/MAP_LAVARIDGE_TOWN_GYM_1F:0","MAP_LAVARIDGE_TOWN_GYM_1F:10/MAP_LAVARIDGE_TOWN_GYM_B1F:8":"MAP_LAVARIDGE_TOWN_GYM_B1F:8/MAP_LAVARIDGE_TOWN_GYM_1F:10","MAP_LAVARIDGE_TOWN_GYM_1F:11/MAP_LAVARIDGE_TOWN_GYM_B1F:9":"MAP_LAVARIDGE_TOWN_GYM_B1F:9/MAP_LAVARIDGE_TOWN_GYM_1F:11","MAP_LAVARIDGE_TOWN_GYM_1F:12/MAP_LAVARIDGE_TOWN_GYM_B1F:10":"MAP_LAVARIDGE_TOWN_GYM_B1F:10/MAP_LAVARIDGE_TOWN_GYM_1F:12","MAP_LAVARIDGE_TOWN_GYM_1F:13/MAP_LAVARIDGE_TOWN_GYM_B1F:11":"MAP_LAVARIDGE_TOWN_GYM_B1F:11/MAP_LAVARIDGE_TOWN_GYM_1F:13","MAP_LAVARIDGE_TOWN_GYM_1F:14/MAP_LAVARIDGE_TOWN_GYM_B1F:12":"MAP_LAVARIDGE_TOWN_GYM_B1F:12/MAP_LAVARIDGE_TOWN_GYM_1F:14","MAP_LAVARIDGE_TOWN_GYM_1F:15/MAP_LAVARIDGE_TOWN_GYM_B1F:13":"MAP_LAVARIDGE_TOWN_GYM_B1F:13/MAP_LAVARIDGE_TOWN_GYM_1F:15","MAP_LAVARIDGE_TOWN_GYM_1F:16/MAP_LAVARIDGE_TOWN_GYM_B1F:14":"MAP_LAVARIDGE_TOWN_GYM_B1F:14/MAP_LAVARIDGE_TOWN_GYM_1F:16","MAP_LAVARIDGE_TOWN_GYM_1F:17/MAP_LAVARIDGE_TOWN_GYM_B1F:15":"MAP_LAVARIDGE_TOWN_GYM_B1F:15/MAP_LAVARIDGE_TOWN_GYM_1F:17","MAP_LAVARIDGE_TOWN_GYM_1F:18/MAP_LAVARIDGE_TOWN_GYM_B1F:16":"MAP_LAVARIDGE_TOWN_GYM_B1F:16/MAP_LAVARIDGE_TOWN_GYM_1F:18","MAP_LAVARIDGE_TOWN_GYM_1F:19/MAP_LAVARIDGE_TOWN_GYM_B1F:17":"MAP_LAVARIDGE_TOWN_GYM_B1F:17/MAP_LAVARIDGE_TOWN_GYM_1F:19","MAP_LAVARIDGE_TOWN_GYM_1F:2/MAP_LAVARIDGE_TOWN_GYM_B1F:0":"MAP_LAVARIDGE_TOWN_GYM_B1F:0/MAP_LAVARIDGE_TOWN_GYM_1F:2","MAP_LAVARIDGE_TOWN_GYM_1F:20/MAP_LAVARIDGE_TOWN_GYM_B1F:18":"MAP_LAVARIDGE_TOWN_GYM_B1F:18/MAP_LAVARIDGE_TOWN_GYM_1F:20","MAP_LAVARIDGE_TOWN_GYM_1F:21/MAP_LAVARIDGE_TOWN_GYM_B1F:20":"MAP_LAVARIDGE_TOWN_GYM_B1F:20/MAP_LAVARIDGE_TOWN_GYM_1F:21","MAP_LAVARIDGE_TOWN_GYM_1F:22/MAP_LAVARIDGE_TOWN_GYM_B1F:19":"MAP_LAVARIDGE_TOWN_GYM_B1F:19/MAP_LAVARIDGE_TOWN_GYM_1F:22","MAP_LAVARIDGE_TOWN_GYM_1F:23/MAP_LAVARIDGE_TOWN_GYM_B1F:21":"MAP_LAVARIDGE_TOWN_GYM_B1F:21/MAP_LAVARIDGE_TOWN_GYM_1F:23","MAP_LAVARIDGE_TOWN_GYM_1F:24/MAP_LAVARIDGE_TOWN_GYM_B1F:22":"MAP_LAVARIDGE_TOWN_GYM_B1F:22/MAP_LAVARIDGE_TOWN_GYM_1F:24","MAP_LAVARIDGE_TOWN_GYM_1F:25/MAP_LAVARIDGE_TOWN_GYM_B1F:23":"MAP_LAVARIDGE_TOWN_GYM_B1F:23/MAP_LAVARIDGE_TOWN_GYM_1F:25","MAP_LAVARIDGE_TOWN_GYM_1F:3/MAP_LAVARIDGE_TOWN_GYM_B1F:2":"MAP_LAVARIDGE_TOWN_GYM_B1F:2/MAP_LAVARIDGE_TOWN_GYM_1F:3","MAP_LAVARIDGE_TOWN_GYM_1F:4/MAP_LAVARIDGE_TOWN_GYM_B1F:4":"MAP_LAVARIDGE_TOWN_GYM_B1F:4/MAP_LAVARIDGE_TOWN_GYM_1F:4","MAP_LAVARIDGE_TOWN_GYM_1F:5/MAP_LAVARIDGE_TOWN_GYM_B1F:3":"MAP_LAVARIDGE_TOWN_GYM_B1F:3/MAP_LAVARIDGE_TOWN_GYM_1F:5","MAP_LAVARIDGE_TOWN_GYM_1F:6/MAP_LAVARIDGE_TOWN_GYM_B1F:1":"MAP_LAVARIDGE_TOWN_GYM_B1F:1/MAP_LAVARIDGE_TOWN_GYM_1F:6","MAP_LAVARIDGE_TOWN_GYM_1F:7/MAP_LAVARIDGE_TOWN_GYM_B1F:5":"MAP_LAVARIDGE_TOWN_GYM_B1F:5/MAP_LAVARIDGE_TOWN_GYM_1F:7","MAP_LAVARIDGE_TOWN_GYM_1F:8/MAP_LAVARIDGE_TOWN_GYM_B1F:6":"MAP_LAVARIDGE_TOWN_GYM_B1F:6/MAP_LAVARIDGE_TOWN_GYM_1F:8","MAP_LAVARIDGE_TOWN_GYM_1F:9/MAP_LAVARIDGE_TOWN_GYM_B1F:7":"MAP_LAVARIDGE_TOWN_GYM_B1F:7/MAP_LAVARIDGE_TOWN_GYM_1F:9","MAP_LAVARIDGE_TOWN_GYM_B1F:0/MAP_LAVARIDGE_TOWN_GYM_1F:2":"MAP_LAVARIDGE_TOWN_GYM_1F:2/MAP_LAVARIDGE_TOWN_GYM_B1F:0","MAP_LAVARIDGE_TOWN_GYM_B1F:1/MAP_LAVARIDGE_TOWN_GYM_1F:6":"MAP_LAVARIDGE_TOWN_GYM_1F:6/MAP_LAVARIDGE_TOWN_GYM_B1F:1","MAP_LAVARIDGE_TOWN_GYM_B1F:10/MAP_LAVARIDGE_TOWN_GYM_1F:12":"MAP_LAVARIDGE_TOWN_GYM_1F:12/MAP_LAVARIDGE_TOWN_GYM_B1F:10","MAP_LAVARIDGE_TOWN_GYM_B1F:11/MAP_LAVARIDGE_TOWN_GYM_1F:13":"MAP_LAVARIDGE_TOWN_GYM_1F:13/MAP_LAVARIDGE_TOWN_GYM_B1F:11","MAP_LAVARIDGE_TOWN_GYM_B1F:12/MAP_LAVARIDGE_TOWN_GYM_1F:14":"MAP_LAVARIDGE_TOWN_GYM_1F:14/MAP_LAVARIDGE_TOWN_GYM_B1F:12","MAP_LAVARIDGE_TOWN_GYM_B1F:13/MAP_LAVARIDGE_TOWN_GYM_1F:15":"MAP_LAVARIDGE_TOWN_GYM_1F:15/MAP_LAVARIDGE_TOWN_GYM_B1F:13","MAP_LAVARIDGE_TOWN_GYM_B1F:14/MAP_LAVARIDGE_TOWN_GYM_1F:16":"MAP_LAVARIDGE_TOWN_GYM_1F:16/MAP_LAVARIDGE_TOWN_GYM_B1F:14","MAP_LAVARIDGE_TOWN_GYM_B1F:15/MAP_LAVARIDGE_TOWN_GYM_1F:17":"MAP_LAVARIDGE_TOWN_GYM_1F:17/MAP_LAVARIDGE_TOWN_GYM_B1F:15","MAP_LAVARIDGE_TOWN_GYM_B1F:16/MAP_LAVARIDGE_TOWN_GYM_1F:18":"MAP_LAVARIDGE_TOWN_GYM_1F:18/MAP_LAVARIDGE_TOWN_GYM_B1F:16","MAP_LAVARIDGE_TOWN_GYM_B1F:17/MAP_LAVARIDGE_TOWN_GYM_1F:19":"MAP_LAVARIDGE_TOWN_GYM_1F:19/MAP_LAVARIDGE_TOWN_GYM_B1F:17","MAP_LAVARIDGE_TOWN_GYM_B1F:18/MAP_LAVARIDGE_TOWN_GYM_1F:20":"MAP_LAVARIDGE_TOWN_GYM_1F:20/MAP_LAVARIDGE_TOWN_GYM_B1F:18","MAP_LAVARIDGE_TOWN_GYM_B1F:19/MAP_LAVARIDGE_TOWN_GYM_1F:22":"MAP_LAVARIDGE_TOWN_GYM_1F:22/MAP_LAVARIDGE_TOWN_GYM_B1F:19","MAP_LAVARIDGE_TOWN_GYM_B1F:2/MAP_LAVARIDGE_TOWN_GYM_1F:3":"MAP_LAVARIDGE_TOWN_GYM_1F:3/MAP_LAVARIDGE_TOWN_GYM_B1F:2","MAP_LAVARIDGE_TOWN_GYM_B1F:20/MAP_LAVARIDGE_TOWN_GYM_1F:21":"MAP_LAVARIDGE_TOWN_GYM_1F:21/MAP_LAVARIDGE_TOWN_GYM_B1F:20","MAP_LAVARIDGE_TOWN_GYM_B1F:21/MAP_LAVARIDGE_TOWN_GYM_1F:23":"MAP_LAVARIDGE_TOWN_GYM_1F:23/MAP_LAVARIDGE_TOWN_GYM_B1F:21","MAP_LAVARIDGE_TOWN_GYM_B1F:22/MAP_LAVARIDGE_TOWN_GYM_1F:24":"MAP_LAVARIDGE_TOWN_GYM_1F:24/MAP_LAVARIDGE_TOWN_GYM_B1F:22","MAP_LAVARIDGE_TOWN_GYM_B1F:23/MAP_LAVARIDGE_TOWN_GYM_1F:25":"MAP_LAVARIDGE_TOWN_GYM_1F:25/MAP_LAVARIDGE_TOWN_GYM_B1F:23","MAP_LAVARIDGE_TOWN_GYM_B1F:3/MAP_LAVARIDGE_TOWN_GYM_1F:5":"MAP_LAVARIDGE_TOWN_GYM_1F:5/MAP_LAVARIDGE_TOWN_GYM_B1F:3","MAP_LAVARIDGE_TOWN_GYM_B1F:4/MAP_LAVARIDGE_TOWN_GYM_1F:4":"MAP_LAVARIDGE_TOWN_GYM_1F:4/MAP_LAVARIDGE_TOWN_GYM_B1F:4","MAP_LAVARIDGE_TOWN_GYM_B1F:5/MAP_LAVARIDGE_TOWN_GYM_1F:7":"MAP_LAVARIDGE_TOWN_GYM_1F:7/MAP_LAVARIDGE_TOWN_GYM_B1F:5","MAP_LAVARIDGE_TOWN_GYM_B1F:6/MAP_LAVARIDGE_TOWN_GYM_1F:8":"MAP_LAVARIDGE_TOWN_GYM_1F:8/MAP_LAVARIDGE_TOWN_GYM_B1F:6","MAP_LAVARIDGE_TOWN_GYM_B1F:7/MAP_LAVARIDGE_TOWN_GYM_1F:9":"MAP_LAVARIDGE_TOWN_GYM_1F:9/MAP_LAVARIDGE_TOWN_GYM_B1F:7","MAP_LAVARIDGE_TOWN_GYM_B1F:8/MAP_LAVARIDGE_TOWN_GYM_1F:10":"MAP_LAVARIDGE_TOWN_GYM_1F:10/MAP_LAVARIDGE_TOWN_GYM_B1F:8","MAP_LAVARIDGE_TOWN_GYM_B1F:9/MAP_LAVARIDGE_TOWN_GYM_1F:11":"MAP_LAVARIDGE_TOWN_GYM_1F:11/MAP_LAVARIDGE_TOWN_GYM_B1F:9","MAP_LAVARIDGE_TOWN_HERB_SHOP:0,1/MAP_LAVARIDGE_TOWN:0":"MAP_LAVARIDGE_TOWN:0/MAP_LAVARIDGE_TOWN_HERB_SHOP:0","MAP_LAVARIDGE_TOWN_HOUSE:0,1/MAP_LAVARIDGE_TOWN:4":"MAP_LAVARIDGE_TOWN:4/MAP_LAVARIDGE_TOWN_HOUSE:0","MAP_LAVARIDGE_TOWN_MART:0,1/MAP_LAVARIDGE_TOWN:2":"MAP_LAVARIDGE_TOWN:2/MAP_LAVARIDGE_TOWN_MART:0","MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:0,1/MAP_LAVARIDGE_TOWN:3":"MAP_LAVARIDGE_TOWN:3/MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:0","MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:2/MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F:0":"MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F:0/MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:2","MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:3/MAP_LAVARIDGE_TOWN:5":"MAP_LAVARIDGE_TOWN:5/MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:3","MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F:0/MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:2":"MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:2/MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F:0","MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_LILYCOVE_CITY:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:0":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:0,1/MAP_LILYCOVE_CITY:0","MAP_LILYCOVE_CITY:1/MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F:0":"MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F:0,1/MAP_LILYCOVE_CITY:1","MAP_LILYCOVE_CITY:10/MAP_LILYCOVE_CITY_HOUSE3:0":"MAP_LILYCOVE_CITY_HOUSE3:0,1/MAP_LILYCOVE_CITY:10","MAP_LILYCOVE_CITY:11/MAP_LILYCOVE_CITY_HOUSE4:0":"MAP_LILYCOVE_CITY_HOUSE4:0,1/MAP_LILYCOVE_CITY:11","MAP_LILYCOVE_CITY:12/MAP_LILYCOVE_CITY_HARBOR:0":"MAP_LILYCOVE_CITY_HARBOR:0,1/MAP_LILYCOVE_CITY:12","MAP_LILYCOVE_CITY:2/MAP_LILYCOVE_CITY_POKEMON_CENTER_1F:0":"MAP_LILYCOVE_CITY_POKEMON_CENTER_1F:0,1/MAP_LILYCOVE_CITY:2","MAP_LILYCOVE_CITY:3,13/MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F:0,1":"MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F:0,1/MAP_LILYCOVE_CITY:3,13","MAP_LILYCOVE_CITY:4/MAP_LILYCOVE_CITY_CONTEST_LOBBY:0":"MAP_LILYCOVE_CITY_CONTEST_LOBBY:0,1/MAP_LILYCOVE_CITY:4","MAP_LILYCOVE_CITY:5/MAP_LILYCOVE_CITY_POKEMON_TRAINER_FAN_CLUB:1":"MAP_LILYCOVE_CITY_POKEMON_TRAINER_FAN_CLUB:0,1/MAP_LILYCOVE_CITY:5","MAP_LILYCOVE_CITY:6/MAP_AQUA_HIDEOUT_1F:0":"MAP_AQUA_HIDEOUT_1F:0,1/MAP_LILYCOVE_CITY:6","MAP_LILYCOVE_CITY:7/MAP_LILYCOVE_CITY_MOVE_DELETERS_HOUSE:0":"MAP_LILYCOVE_CITY_MOVE_DELETERS_HOUSE:0,1/MAP_LILYCOVE_CITY:7","MAP_LILYCOVE_CITY:8/MAP_LILYCOVE_CITY_HOUSE1:0":"MAP_LILYCOVE_CITY_HOUSE1:0,1/MAP_LILYCOVE_CITY:8","MAP_LILYCOVE_CITY:9/MAP_LILYCOVE_CITY_HOUSE2:0":"MAP_LILYCOVE_CITY_HOUSE2:0,1/MAP_LILYCOVE_CITY:9","MAP_LILYCOVE_CITY_CONTEST_HALL:0,2/MAP_LILYCOVE_CITY_CONTEST_LOBBY:2":"MAP_LILYCOVE_CITY_CONTEST_LOBBY:2/MAP_LILYCOVE_CITY_CONTEST_HALL:0","MAP_LILYCOVE_CITY_CONTEST_HALL:1,3/MAP_LILYCOVE_CITY_CONTEST_LOBBY:3":"MAP_LILYCOVE_CITY_CONTEST_LOBBY:3/MAP_LILYCOVE_CITY_CONTEST_HALL:1","MAP_LILYCOVE_CITY_CONTEST_LOBBY:0,1/MAP_LILYCOVE_CITY:4":"MAP_LILYCOVE_CITY:4/MAP_LILYCOVE_CITY_CONTEST_LOBBY:0","MAP_LILYCOVE_CITY_CONTEST_LOBBY:2/MAP_LILYCOVE_CITY_CONTEST_HALL:0":"MAP_LILYCOVE_CITY_CONTEST_HALL:0,2/MAP_LILYCOVE_CITY_CONTEST_LOBBY:2","MAP_LILYCOVE_CITY_CONTEST_LOBBY:3/MAP_LILYCOVE_CITY_CONTEST_HALL:1":"MAP_LILYCOVE_CITY_CONTEST_HALL:1,3/MAP_LILYCOVE_CITY_CONTEST_LOBBY:3","MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F:0,1/MAP_LILYCOVE_CITY:1":"MAP_LILYCOVE_CITY:1/MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F:0","MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F:2/MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_2F:0":"MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_2F:0/MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F:2","MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_2F:0/MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F:2":"MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F:2/MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_2F:0","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:0,1/MAP_LILYCOVE_CITY:0":"MAP_LILYCOVE_CITY:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:0","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:0":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:2","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:3/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0,1/MAP_DYNAMIC:-1!","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:2":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:0","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:1/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:0":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:1","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0,1/MAP_DYNAMIC:-1!","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:1":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:1/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:0","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:1/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:0":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:1","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0,1/MAP_DYNAMIC:-1!","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:1":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:1/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:0","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:1/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:0":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:1","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0,1/MAP_DYNAMIC:-1!","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:1":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:1/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:0","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:1/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0,1/MAP_DYNAMIC:-1!","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ROOFTOP:0":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ROOFTOP:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:2","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0,1/MAP_DYNAMIC:-1!":"","MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ROOFTOP:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:2":"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ROOFTOP:0","MAP_LILYCOVE_CITY_HARBOR:0,1/MAP_LILYCOVE_CITY:12":"MAP_LILYCOVE_CITY:12/MAP_LILYCOVE_CITY_HARBOR:0","MAP_LILYCOVE_CITY_HOUSE1:0,1/MAP_LILYCOVE_CITY:8":"MAP_LILYCOVE_CITY:8/MAP_LILYCOVE_CITY_HOUSE1:0","MAP_LILYCOVE_CITY_HOUSE2:0,1/MAP_LILYCOVE_CITY:9":"MAP_LILYCOVE_CITY:9/MAP_LILYCOVE_CITY_HOUSE2:0","MAP_LILYCOVE_CITY_HOUSE3:0,1/MAP_LILYCOVE_CITY:10":"MAP_LILYCOVE_CITY:10/MAP_LILYCOVE_CITY_HOUSE3:0","MAP_LILYCOVE_CITY_HOUSE4:0,1/MAP_LILYCOVE_CITY:11":"MAP_LILYCOVE_CITY:11/MAP_LILYCOVE_CITY_HOUSE4:0","MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F:0,1/MAP_LILYCOVE_CITY:3,13":"MAP_LILYCOVE_CITY:3,13/MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F:0,1","MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F:2/MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_2F:0":"MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_2F:0/MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F:2","MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_2F:0/MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F:2":"MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F:2/MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_2F:0","MAP_LILYCOVE_CITY_MOVE_DELETERS_HOUSE:0,1/MAP_LILYCOVE_CITY:7":"MAP_LILYCOVE_CITY:7/MAP_LILYCOVE_CITY_MOVE_DELETERS_HOUSE:0","MAP_LILYCOVE_CITY_POKEMON_CENTER_1F:0,1/MAP_LILYCOVE_CITY:2":"MAP_LILYCOVE_CITY:2/MAP_LILYCOVE_CITY_POKEMON_CENTER_1F:0","MAP_LILYCOVE_CITY_POKEMON_CENTER_1F:2/MAP_LILYCOVE_CITY_POKEMON_CENTER_2F:0":"MAP_LILYCOVE_CITY_POKEMON_CENTER_2F:0/MAP_LILYCOVE_CITY_POKEMON_CENTER_1F:2","MAP_LILYCOVE_CITY_POKEMON_CENTER_2F:0/MAP_LILYCOVE_CITY_POKEMON_CENTER_1F:2":"MAP_LILYCOVE_CITY_POKEMON_CENTER_1F:2/MAP_LILYCOVE_CITY_POKEMON_CENTER_2F:0","MAP_LILYCOVE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_LILYCOVE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_LILYCOVE_CITY_POKEMON_TRAINER_FAN_CLUB:0,1/MAP_LILYCOVE_CITY:5":"MAP_LILYCOVE_CITY:5/MAP_LILYCOVE_CITY_POKEMON_TRAINER_FAN_CLUB:1","MAP_LILYCOVE_CITY_UNUSED_MART:0,1/MAP_LILYCOVE_CITY:0!":"MAP_LILYCOVE_CITY:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:0","MAP_LITTLEROOT_TOWN:0/MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F:1":"MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F:0,1/MAP_LITTLEROOT_TOWN:0","MAP_LITTLEROOT_TOWN:1/MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F:1":"MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F:0,1/MAP_LITTLEROOT_TOWN:1","MAP_LITTLEROOT_TOWN:2/MAP_LITTLEROOT_TOWN_PROFESSOR_BIRCHS_LAB:0":"MAP_LITTLEROOT_TOWN_PROFESSOR_BIRCHS_LAB:0,1/MAP_LITTLEROOT_TOWN:2","MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F:0,1/MAP_LITTLEROOT_TOWN:1":"MAP_LITTLEROOT_TOWN:1/MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F:1","MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F:2/MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F:0":"MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F:0/MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F:2","MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F:0/MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F:2":"MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F:2/MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F:0","MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F:0,1/MAP_LITTLEROOT_TOWN:0":"MAP_LITTLEROOT_TOWN:0/MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F:1","MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F:2/MAP_LITTLEROOT_TOWN_MAYS_HOUSE_2F:0":"MAP_LITTLEROOT_TOWN_MAYS_HOUSE_2F:0/MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F:2","MAP_LITTLEROOT_TOWN_MAYS_HOUSE_2F:0/MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F:2":"MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F:2/MAP_LITTLEROOT_TOWN_MAYS_HOUSE_2F:0","MAP_LITTLEROOT_TOWN_PROFESSOR_BIRCHS_LAB:0,1/MAP_LITTLEROOT_TOWN:2":"MAP_LITTLEROOT_TOWN:2/MAP_LITTLEROOT_TOWN_PROFESSOR_BIRCHS_LAB:0","MAP_MAGMA_HIDEOUT_1F:0/MAP_JAGGED_PASS:4":"MAP_JAGGED_PASS:4/MAP_MAGMA_HIDEOUT_1F:0","MAP_MAGMA_HIDEOUT_1F:1/MAP_MAGMA_HIDEOUT_2F_1R:1":"MAP_MAGMA_HIDEOUT_2F_1R:1/MAP_MAGMA_HIDEOUT_1F:1","MAP_MAGMA_HIDEOUT_1F:2/MAP_MAGMA_HIDEOUT_2F_2R:1":"MAP_MAGMA_HIDEOUT_2F_2R:1/MAP_MAGMA_HIDEOUT_1F:2","MAP_MAGMA_HIDEOUT_1F:3/MAP_MAGMA_HIDEOUT_2F_3R:0":"MAP_MAGMA_HIDEOUT_2F_3R:0/MAP_MAGMA_HIDEOUT_1F:3","MAP_MAGMA_HIDEOUT_2F_1R:0/MAP_MAGMA_HIDEOUT_2F_2R:0":"MAP_MAGMA_HIDEOUT_2F_2R:0/MAP_MAGMA_HIDEOUT_2F_1R:0","MAP_MAGMA_HIDEOUT_2F_1R:1/MAP_MAGMA_HIDEOUT_1F:1":"MAP_MAGMA_HIDEOUT_1F:1/MAP_MAGMA_HIDEOUT_2F_1R:1","MAP_MAGMA_HIDEOUT_2F_1R:2/MAP_MAGMA_HIDEOUT_3F_1R:2":"MAP_MAGMA_HIDEOUT_3F_1R:2/MAP_MAGMA_HIDEOUT_2F_1R:2","MAP_MAGMA_HIDEOUT_2F_2R:0/MAP_MAGMA_HIDEOUT_2F_1R:0":"MAP_MAGMA_HIDEOUT_2F_1R:0/MAP_MAGMA_HIDEOUT_2F_2R:0","MAP_MAGMA_HIDEOUT_2F_2R:1/MAP_MAGMA_HIDEOUT_1F:2":"MAP_MAGMA_HIDEOUT_1F:2/MAP_MAGMA_HIDEOUT_2F_2R:1","MAP_MAGMA_HIDEOUT_2F_3R:0/MAP_MAGMA_HIDEOUT_1F:3":"MAP_MAGMA_HIDEOUT_1F:3/MAP_MAGMA_HIDEOUT_2F_3R:0","MAP_MAGMA_HIDEOUT_2F_3R:1/MAP_MAGMA_HIDEOUT_3F_3R:0":"MAP_MAGMA_HIDEOUT_3F_3R:0/MAP_MAGMA_HIDEOUT_2F_3R:1","MAP_MAGMA_HIDEOUT_3F_1R:0/MAP_MAGMA_HIDEOUT_4F:0":"MAP_MAGMA_HIDEOUT_4F:0/MAP_MAGMA_HIDEOUT_3F_1R:0","MAP_MAGMA_HIDEOUT_3F_1R:1/MAP_MAGMA_HIDEOUT_3F_2R:0":"MAP_MAGMA_HIDEOUT_3F_2R:0/MAP_MAGMA_HIDEOUT_3F_1R:1","MAP_MAGMA_HIDEOUT_3F_1R:2/MAP_MAGMA_HIDEOUT_2F_1R:2":"MAP_MAGMA_HIDEOUT_2F_1R:2/MAP_MAGMA_HIDEOUT_3F_1R:2","MAP_MAGMA_HIDEOUT_3F_2R:0/MAP_MAGMA_HIDEOUT_3F_1R:1":"MAP_MAGMA_HIDEOUT_3F_1R:1/MAP_MAGMA_HIDEOUT_3F_2R:0","MAP_MAGMA_HIDEOUT_3F_3R:0/MAP_MAGMA_HIDEOUT_2F_3R:1":"MAP_MAGMA_HIDEOUT_2F_3R:1/MAP_MAGMA_HIDEOUT_3F_3R:0","MAP_MAGMA_HIDEOUT_3F_3R:1/MAP_MAGMA_HIDEOUT_4F:1":"MAP_MAGMA_HIDEOUT_4F:1/MAP_MAGMA_HIDEOUT_3F_3R:1","MAP_MAGMA_HIDEOUT_4F:0/MAP_MAGMA_HIDEOUT_3F_1R:0":"MAP_MAGMA_HIDEOUT_3F_1R:0/MAP_MAGMA_HIDEOUT_4F:0","MAP_MAGMA_HIDEOUT_4F:1/MAP_MAGMA_HIDEOUT_3F_3R:1":"MAP_MAGMA_HIDEOUT_3F_3R:1/MAP_MAGMA_HIDEOUT_4F:1","MAP_MARINE_CAVE_END:0/MAP_MARINE_CAVE_ENTRANCE:0":"MAP_MARINE_CAVE_ENTRANCE:0/MAP_MARINE_CAVE_END:0","MAP_MARINE_CAVE_ENTRANCE:0/MAP_MARINE_CAVE_END:0":"MAP_MARINE_CAVE_END:0/MAP_MARINE_CAVE_ENTRANCE:0","MAP_MAUVILLE_CITY:0/MAP_MAUVILLE_CITY_GYM:0":"MAP_MAUVILLE_CITY_GYM:0,1/MAP_MAUVILLE_CITY:0","MAP_MAUVILLE_CITY:1/MAP_MAUVILLE_CITY_POKEMON_CENTER_1F:0":"MAP_MAUVILLE_CITY_POKEMON_CENTER_1F:0,1/MAP_MAUVILLE_CITY:1","MAP_MAUVILLE_CITY:2/MAP_MAUVILLE_CITY_BIKE_SHOP:0":"MAP_MAUVILLE_CITY_BIKE_SHOP:0,1/MAP_MAUVILLE_CITY:2","MAP_MAUVILLE_CITY:3/MAP_MAUVILLE_CITY_MART:0":"MAP_MAUVILLE_CITY_MART:0,1/MAP_MAUVILLE_CITY:3","MAP_MAUVILLE_CITY:4/MAP_MAUVILLE_CITY_HOUSE1:0":"MAP_MAUVILLE_CITY_HOUSE1:0,1/MAP_MAUVILLE_CITY:4","MAP_MAUVILLE_CITY:5/MAP_MAUVILLE_CITY_GAME_CORNER:0":"MAP_MAUVILLE_CITY_GAME_CORNER:0,1/MAP_MAUVILLE_CITY:5","MAP_MAUVILLE_CITY:6/MAP_MAUVILLE_CITY_HOUSE2:0":"MAP_MAUVILLE_CITY_HOUSE2:0,1/MAP_MAUVILLE_CITY:6","MAP_MAUVILLE_CITY_BIKE_SHOP:0,1/MAP_MAUVILLE_CITY:2":"MAP_MAUVILLE_CITY:2/MAP_MAUVILLE_CITY_BIKE_SHOP:0","MAP_MAUVILLE_CITY_GAME_CORNER:0,1/MAP_MAUVILLE_CITY:5":"MAP_MAUVILLE_CITY:5/MAP_MAUVILLE_CITY_GAME_CORNER:0","MAP_MAUVILLE_CITY_GYM:0,1/MAP_MAUVILLE_CITY:0":"MAP_MAUVILLE_CITY:0/MAP_MAUVILLE_CITY_GYM:0","MAP_MAUVILLE_CITY_HOUSE1:0,1/MAP_MAUVILLE_CITY:4":"MAP_MAUVILLE_CITY:4/MAP_MAUVILLE_CITY_HOUSE1:0","MAP_MAUVILLE_CITY_HOUSE2:0,1/MAP_MAUVILLE_CITY:6":"MAP_MAUVILLE_CITY:6/MAP_MAUVILLE_CITY_HOUSE2:0","MAP_MAUVILLE_CITY_MART:0,1/MAP_MAUVILLE_CITY:3":"MAP_MAUVILLE_CITY:3/MAP_MAUVILLE_CITY_MART:0","MAP_MAUVILLE_CITY_POKEMON_CENTER_1F:0,1/MAP_MAUVILLE_CITY:1":"MAP_MAUVILLE_CITY:1/MAP_MAUVILLE_CITY_POKEMON_CENTER_1F:0","MAP_MAUVILLE_CITY_POKEMON_CENTER_1F:2/MAP_MAUVILLE_CITY_POKEMON_CENTER_2F:0":"MAP_MAUVILLE_CITY_POKEMON_CENTER_2F:0/MAP_MAUVILLE_CITY_POKEMON_CENTER_1F:2","MAP_MAUVILLE_CITY_POKEMON_CENTER_2F:0/MAP_MAUVILLE_CITY_POKEMON_CENTER_1F:2":"MAP_MAUVILLE_CITY_POKEMON_CENTER_1F:2/MAP_MAUVILLE_CITY_POKEMON_CENTER_2F:0","MAP_MAUVILLE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_MAUVILLE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_METEOR_FALLS_1F_1R:0/MAP_ROUTE114:0":"MAP_ROUTE114:0/MAP_METEOR_FALLS_1F_1R:0","MAP_METEOR_FALLS_1F_1R:1/MAP_ROUTE115:0":"MAP_ROUTE115:0/MAP_METEOR_FALLS_1F_1R:1","MAP_METEOR_FALLS_1F_1R:2/MAP_METEOR_FALLS_1F_2R:0":"MAP_METEOR_FALLS_1F_2R:0/MAP_METEOR_FALLS_1F_1R:2","MAP_METEOR_FALLS_1F_1R:3/MAP_METEOR_FALLS_B1F_1R:4":"MAP_METEOR_FALLS_B1F_1R:4/MAP_METEOR_FALLS_1F_1R:3","MAP_METEOR_FALLS_1F_1R:4/MAP_METEOR_FALLS_B1F_1R:5":"MAP_METEOR_FALLS_B1F_1R:5/MAP_METEOR_FALLS_1F_1R:4","MAP_METEOR_FALLS_1F_1R:5/MAP_METEOR_FALLS_STEVENS_CAVE:0":"MAP_METEOR_FALLS_STEVENS_CAVE:0/MAP_METEOR_FALLS_1F_1R:5","MAP_METEOR_FALLS_1F_2R:0/MAP_METEOR_FALLS_1F_1R:2":"MAP_METEOR_FALLS_1F_1R:2/MAP_METEOR_FALLS_1F_2R:0","MAP_METEOR_FALLS_1F_2R:1/MAP_METEOR_FALLS_B1F_1R:0":"MAP_METEOR_FALLS_B1F_1R:0/MAP_METEOR_FALLS_1F_2R:1","MAP_METEOR_FALLS_1F_2R:2/MAP_METEOR_FALLS_B1F_1R:1":"MAP_METEOR_FALLS_B1F_1R:1/MAP_METEOR_FALLS_1F_2R:2","MAP_METEOR_FALLS_1F_2R:3/MAP_METEOR_FALLS_B1F_1R:2":"MAP_METEOR_FALLS_B1F_1R:2/MAP_METEOR_FALLS_1F_2R:3","MAP_METEOR_FALLS_B1F_1R:0/MAP_METEOR_FALLS_1F_2R:1":"MAP_METEOR_FALLS_1F_2R:1/MAP_METEOR_FALLS_B1F_1R:0","MAP_METEOR_FALLS_B1F_1R:1/MAP_METEOR_FALLS_1F_2R:2":"MAP_METEOR_FALLS_1F_2R:2/MAP_METEOR_FALLS_B1F_1R:1","MAP_METEOR_FALLS_B1F_1R:2/MAP_METEOR_FALLS_1F_2R:3":"MAP_METEOR_FALLS_1F_2R:3/MAP_METEOR_FALLS_B1F_1R:2","MAP_METEOR_FALLS_B1F_1R:3/MAP_METEOR_FALLS_B1F_2R:0":"MAP_METEOR_FALLS_B1F_2R:0/MAP_METEOR_FALLS_B1F_1R:3","MAP_METEOR_FALLS_B1F_1R:4/MAP_METEOR_FALLS_1F_1R:3":"MAP_METEOR_FALLS_1F_1R:3/MAP_METEOR_FALLS_B1F_1R:4","MAP_METEOR_FALLS_B1F_1R:5/MAP_METEOR_FALLS_1F_1R:4":"MAP_METEOR_FALLS_1F_1R:4/MAP_METEOR_FALLS_B1F_1R:5","MAP_METEOR_FALLS_B1F_2R:0/MAP_METEOR_FALLS_B1F_1R:3":"MAP_METEOR_FALLS_B1F_1R:3/MAP_METEOR_FALLS_B1F_2R:0","MAP_METEOR_FALLS_STEVENS_CAVE:0/MAP_METEOR_FALLS_1F_1R:5":"MAP_METEOR_FALLS_1F_1R:5/MAP_METEOR_FALLS_STEVENS_CAVE:0","MAP_MIRAGE_TOWER_1F:0/MAP_ROUTE111:3":"MAP_ROUTE111:3/MAP_MIRAGE_TOWER_1F:0","MAP_MIRAGE_TOWER_1F:1/MAP_MIRAGE_TOWER_2F:1":"MAP_MIRAGE_TOWER_2F:1/MAP_MIRAGE_TOWER_1F:1","MAP_MIRAGE_TOWER_2F:0/MAP_MIRAGE_TOWER_3F:0":"MAP_MIRAGE_TOWER_3F:0/MAP_MIRAGE_TOWER_2F:0","MAP_MIRAGE_TOWER_2F:1/MAP_MIRAGE_TOWER_1F:1":"MAP_MIRAGE_TOWER_1F:1/MAP_MIRAGE_TOWER_2F:1","MAP_MIRAGE_TOWER_3F:0/MAP_MIRAGE_TOWER_2F:0":"MAP_MIRAGE_TOWER_2F:0/MAP_MIRAGE_TOWER_3F:0","MAP_MIRAGE_TOWER_3F:1/MAP_MIRAGE_TOWER_4F:0":"MAP_MIRAGE_TOWER_4F:0/MAP_MIRAGE_TOWER_3F:1","MAP_MIRAGE_TOWER_4F:0/MAP_MIRAGE_TOWER_3F:1":"MAP_MIRAGE_TOWER_3F:1/MAP_MIRAGE_TOWER_4F:0","MAP_MOSSDEEP_CITY:0/MAP_MOSSDEEP_CITY_HOUSE1:0":"MAP_MOSSDEEP_CITY_HOUSE1:0,1/MAP_MOSSDEEP_CITY:0","MAP_MOSSDEEP_CITY:1/MAP_MOSSDEEP_CITY_GYM:0":"MAP_MOSSDEEP_CITY_GYM:0,1/MAP_MOSSDEEP_CITY:1","MAP_MOSSDEEP_CITY:2/MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F:0":"MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F:0,1/MAP_MOSSDEEP_CITY:2","MAP_MOSSDEEP_CITY:3/MAP_MOSSDEEP_CITY_HOUSE2:0":"MAP_MOSSDEEP_CITY_HOUSE2:0,1/MAP_MOSSDEEP_CITY:3","MAP_MOSSDEEP_CITY:4/MAP_MOSSDEEP_CITY_MART:0":"MAP_MOSSDEEP_CITY_MART:0,1/MAP_MOSSDEEP_CITY:4","MAP_MOSSDEEP_CITY:5/MAP_MOSSDEEP_CITY_HOUSE3:0":"MAP_MOSSDEEP_CITY_HOUSE3:0,1/MAP_MOSSDEEP_CITY:5","MAP_MOSSDEEP_CITY:6/MAP_MOSSDEEP_CITY_STEVENS_HOUSE:0":"MAP_MOSSDEEP_CITY_STEVENS_HOUSE:0,1/MAP_MOSSDEEP_CITY:6","MAP_MOSSDEEP_CITY:7/MAP_MOSSDEEP_CITY_HOUSE4:1":"MAP_MOSSDEEP_CITY_HOUSE4:0,1/MAP_MOSSDEEP_CITY:7","MAP_MOSSDEEP_CITY:8/MAP_MOSSDEEP_CITY_SPACE_CENTER_1F:0":"MAP_MOSSDEEP_CITY_SPACE_CENTER_1F:0,1/MAP_MOSSDEEP_CITY:8","MAP_MOSSDEEP_CITY:9/MAP_MOSSDEEP_CITY_GAME_CORNER_1F:0":"MAP_MOSSDEEP_CITY_GAME_CORNER_1F:0,1/MAP_MOSSDEEP_CITY:9","MAP_MOSSDEEP_CITY_GAME_CORNER_1F:0,1/MAP_MOSSDEEP_CITY:9":"MAP_MOSSDEEP_CITY:9/MAP_MOSSDEEP_CITY_GAME_CORNER_1F:0","MAP_MOSSDEEP_CITY_GAME_CORNER_1F:2/MAP_MOSSDEEP_CITY_GAME_CORNER_B1F:0":"MAP_MOSSDEEP_CITY_GAME_CORNER_B1F:0/MAP_MOSSDEEP_CITY_GAME_CORNER_1F:2","MAP_MOSSDEEP_CITY_GAME_CORNER_B1F:0/MAP_MOSSDEEP_CITY_GAME_CORNER_1F:2":"MAP_MOSSDEEP_CITY_GAME_CORNER_1F:2/MAP_MOSSDEEP_CITY_GAME_CORNER_B1F:0","MAP_MOSSDEEP_CITY_GYM:0,1/MAP_MOSSDEEP_CITY:1":"MAP_MOSSDEEP_CITY:1/MAP_MOSSDEEP_CITY_GYM:0","MAP_MOSSDEEP_CITY_GYM:10/MAP_MOSSDEEP_CITY_GYM:11":"MAP_MOSSDEEP_CITY_GYM:11/MAP_MOSSDEEP_CITY_GYM:10","MAP_MOSSDEEP_CITY_GYM:11/MAP_MOSSDEEP_CITY_GYM:10":"MAP_MOSSDEEP_CITY_GYM:10/MAP_MOSSDEEP_CITY_GYM:11","MAP_MOSSDEEP_CITY_GYM:12/MAP_MOSSDEEP_CITY_GYM:13":"MAP_MOSSDEEP_CITY_GYM:13/MAP_MOSSDEEP_CITY_GYM:12","MAP_MOSSDEEP_CITY_GYM:13/MAP_MOSSDEEP_CITY_GYM:12":"MAP_MOSSDEEP_CITY_GYM:12/MAP_MOSSDEEP_CITY_GYM:13","MAP_MOSSDEEP_CITY_GYM:2/MAP_MOSSDEEP_CITY_GYM:3":"MAP_MOSSDEEP_CITY_GYM:3/MAP_MOSSDEEP_CITY_GYM:2","MAP_MOSSDEEP_CITY_GYM:3/MAP_MOSSDEEP_CITY_GYM:2":"MAP_MOSSDEEP_CITY_GYM:2/MAP_MOSSDEEP_CITY_GYM:3","MAP_MOSSDEEP_CITY_GYM:4/MAP_MOSSDEEP_CITY_GYM:5":"MAP_MOSSDEEP_CITY_GYM:5/MAP_MOSSDEEP_CITY_GYM:4","MAP_MOSSDEEP_CITY_GYM:5/MAP_MOSSDEEP_CITY_GYM:4":"MAP_MOSSDEEP_CITY_GYM:4/MAP_MOSSDEEP_CITY_GYM:5","MAP_MOSSDEEP_CITY_GYM:6/MAP_MOSSDEEP_CITY_GYM:7":"MAP_MOSSDEEP_CITY_GYM:7/MAP_MOSSDEEP_CITY_GYM:6","MAP_MOSSDEEP_CITY_GYM:7/MAP_MOSSDEEP_CITY_GYM:6":"MAP_MOSSDEEP_CITY_GYM:6/MAP_MOSSDEEP_CITY_GYM:7","MAP_MOSSDEEP_CITY_GYM:8/MAP_MOSSDEEP_CITY_GYM:9":"MAP_MOSSDEEP_CITY_GYM:9/MAP_MOSSDEEP_CITY_GYM:8","MAP_MOSSDEEP_CITY_GYM:9/MAP_MOSSDEEP_CITY_GYM:8":"MAP_MOSSDEEP_CITY_GYM:8/MAP_MOSSDEEP_CITY_GYM:9","MAP_MOSSDEEP_CITY_HOUSE1:0,1/MAP_MOSSDEEP_CITY:0":"MAP_MOSSDEEP_CITY:0/MAP_MOSSDEEP_CITY_HOUSE1:0","MAP_MOSSDEEP_CITY_HOUSE2:0,1/MAP_MOSSDEEP_CITY:3":"MAP_MOSSDEEP_CITY:3/MAP_MOSSDEEP_CITY_HOUSE2:0","MAP_MOSSDEEP_CITY_HOUSE3:0,1/MAP_MOSSDEEP_CITY:5":"MAP_MOSSDEEP_CITY:5/MAP_MOSSDEEP_CITY_HOUSE3:0","MAP_MOSSDEEP_CITY_HOUSE4:0,1/MAP_MOSSDEEP_CITY:7":"MAP_MOSSDEEP_CITY:7/MAP_MOSSDEEP_CITY_HOUSE4:1","MAP_MOSSDEEP_CITY_MART:0,1/MAP_MOSSDEEP_CITY:4":"MAP_MOSSDEEP_CITY:4/MAP_MOSSDEEP_CITY_MART:0","MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F:0,1/MAP_MOSSDEEP_CITY:2":"MAP_MOSSDEEP_CITY:2/MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F:0","MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F:2/MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F:0":"MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F:0/MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F:2","MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F:0/MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F:2":"MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F:2/MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F:0","MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_MOSSDEEP_CITY_SPACE_CENTER_1F:0,1/MAP_MOSSDEEP_CITY:8":"MAP_MOSSDEEP_CITY:8/MAP_MOSSDEEP_CITY_SPACE_CENTER_1F:0","MAP_MOSSDEEP_CITY_SPACE_CENTER_1F:2/MAP_MOSSDEEP_CITY_SPACE_CENTER_2F:0":"MAP_MOSSDEEP_CITY_SPACE_CENTER_2F:0/MAP_MOSSDEEP_CITY_SPACE_CENTER_1F:2","MAP_MOSSDEEP_CITY_SPACE_CENTER_2F:0/MAP_MOSSDEEP_CITY_SPACE_CENTER_1F:2":"MAP_MOSSDEEP_CITY_SPACE_CENTER_1F:2/MAP_MOSSDEEP_CITY_SPACE_CENTER_2F:0","MAP_MOSSDEEP_CITY_STEVENS_HOUSE:0,1/MAP_MOSSDEEP_CITY:6":"MAP_MOSSDEEP_CITY:6/MAP_MOSSDEEP_CITY_STEVENS_HOUSE:0","MAP_MT_CHIMNEY:0,1/MAP_MT_CHIMNEY_CABLE_CAR_STATION:0,1":"MAP_MT_CHIMNEY_CABLE_CAR_STATION:0,1/MAP_MT_CHIMNEY:0,1","MAP_MT_CHIMNEY:2,3/MAP_JAGGED_PASS:2,3":"MAP_JAGGED_PASS:2,3/MAP_MT_CHIMNEY:2,3","MAP_MT_CHIMNEY_CABLE_CAR_STATION:0,1/MAP_MT_CHIMNEY:0,1":"MAP_MT_CHIMNEY:0,1/MAP_MT_CHIMNEY_CABLE_CAR_STATION:0,1","MAP_MT_PYRE_1F:0,2/MAP_ROUTE122:0":"MAP_ROUTE122:0/MAP_MT_PYRE_1F:0","MAP_MT_PYRE_1F:1,3/MAP_MT_PYRE_EXTERIOR:0":"MAP_MT_PYRE_EXTERIOR:0/MAP_MT_PYRE_1F:1","MAP_MT_PYRE_1F:4/MAP_MT_PYRE_2F:0":"MAP_MT_PYRE_2F:0/MAP_MT_PYRE_1F:4","MAP_MT_PYRE_1F:5/MAP_MT_PYRE_2F:4":"MAP_MT_PYRE_2F:4/MAP_MT_PYRE_1F:5","MAP_MT_PYRE_2F:0/MAP_MT_PYRE_1F:4":"MAP_MT_PYRE_1F:4/MAP_MT_PYRE_2F:0","MAP_MT_PYRE_2F:1/MAP_MT_PYRE_3F:0":"MAP_MT_PYRE_3F:0/MAP_MT_PYRE_2F:1","MAP_MT_PYRE_2F:2/MAP_MT_PYRE_3F:4":"MAP_MT_PYRE_3F:4/MAP_MT_PYRE_2F:2","MAP_MT_PYRE_2F:3/MAP_MT_PYRE_3F:5":"MAP_MT_PYRE_3F:5/MAP_MT_PYRE_2F:3","MAP_MT_PYRE_2F:4/MAP_MT_PYRE_1F:5":"MAP_MT_PYRE_1F:5/MAP_MT_PYRE_2F:4","MAP_MT_PYRE_3F:0/MAP_MT_PYRE_2F:1":"MAP_MT_PYRE_2F:1/MAP_MT_PYRE_3F:0","MAP_MT_PYRE_3F:1/MAP_MT_PYRE_4F:1":"MAP_MT_PYRE_4F:1/MAP_MT_PYRE_3F:1","MAP_MT_PYRE_3F:2/MAP_MT_PYRE_4F:4":"MAP_MT_PYRE_4F:4/MAP_MT_PYRE_3F:2","MAP_MT_PYRE_3F:3/MAP_MT_PYRE_4F:5":"MAP_MT_PYRE_4F:5/MAP_MT_PYRE_3F:3","MAP_MT_PYRE_3F:4/MAP_MT_PYRE_2F:2":"MAP_MT_PYRE_2F:2/MAP_MT_PYRE_3F:4","MAP_MT_PYRE_3F:5/MAP_MT_PYRE_2F:3":"MAP_MT_PYRE_2F:3/MAP_MT_PYRE_3F:5","MAP_MT_PYRE_4F:0/MAP_MT_PYRE_5F:1":"MAP_MT_PYRE_5F:1/MAP_MT_PYRE_4F:0","MAP_MT_PYRE_4F:1/MAP_MT_PYRE_3F:1":"MAP_MT_PYRE_3F:1/MAP_MT_PYRE_4F:1","MAP_MT_PYRE_4F:2/MAP_MT_PYRE_5F:3":"MAP_MT_PYRE_5F:3/MAP_MT_PYRE_4F:2","MAP_MT_PYRE_4F:3/MAP_MT_PYRE_5F:4":"MAP_MT_PYRE_5F:4/MAP_MT_PYRE_4F:3","MAP_MT_PYRE_4F:4/MAP_MT_PYRE_3F:2":"MAP_MT_PYRE_3F:2/MAP_MT_PYRE_4F:4","MAP_MT_PYRE_4F:5/MAP_MT_PYRE_3F:3":"MAP_MT_PYRE_3F:3/MAP_MT_PYRE_4F:5","MAP_MT_PYRE_5F:0/MAP_MT_PYRE_6F:0":"MAP_MT_PYRE_6F:0/MAP_MT_PYRE_5F:0","MAP_MT_PYRE_5F:1/MAP_MT_PYRE_4F:0":"MAP_MT_PYRE_4F:0/MAP_MT_PYRE_5F:1","MAP_MT_PYRE_5F:2/MAP_MT_PYRE_6F:1":"MAP_MT_PYRE_6F:1/MAP_MT_PYRE_5F:2","MAP_MT_PYRE_5F:3/MAP_MT_PYRE_4F:2":"MAP_MT_PYRE_4F:2/MAP_MT_PYRE_5F:3","MAP_MT_PYRE_5F:4/MAP_MT_PYRE_4F:3":"MAP_MT_PYRE_4F:3/MAP_MT_PYRE_5F:4","MAP_MT_PYRE_6F:0/MAP_MT_PYRE_5F:0":"MAP_MT_PYRE_5F:0/MAP_MT_PYRE_6F:0","MAP_MT_PYRE_6F:1/MAP_MT_PYRE_5F:2":"MAP_MT_PYRE_5F:2/MAP_MT_PYRE_6F:1","MAP_MT_PYRE_EXTERIOR:0/MAP_MT_PYRE_1F:1":"MAP_MT_PYRE_1F:1,3/MAP_MT_PYRE_EXTERIOR:0","MAP_MT_PYRE_EXTERIOR:1,2/MAP_MT_PYRE_SUMMIT:1":"MAP_MT_PYRE_SUMMIT:0,1,2/MAP_MT_PYRE_EXTERIOR:1","MAP_MT_PYRE_SUMMIT:0,1,2/MAP_MT_PYRE_EXTERIOR:1":"MAP_MT_PYRE_EXTERIOR:1,2/MAP_MT_PYRE_SUMMIT:1","MAP_NAVEL_ROCK_B1F:0/MAP_NAVEL_ROCK_ENTRANCE:0":"MAP_NAVEL_ROCK_ENTRANCE:0/MAP_NAVEL_ROCK_B1F:0","MAP_NAVEL_ROCK_B1F:1/MAP_NAVEL_ROCK_FORK:1":"MAP_NAVEL_ROCK_FORK:1/MAP_NAVEL_ROCK_B1F:1","MAP_NAVEL_ROCK_BOTTOM:0/MAP_NAVEL_ROCK_DOWN11:0":"MAP_NAVEL_ROCK_DOWN11:0/MAP_NAVEL_ROCK_BOTTOM:0","MAP_NAVEL_ROCK_DOWN01:0/MAP_NAVEL_ROCK_FORK:2":"MAP_NAVEL_ROCK_FORK:2/MAP_NAVEL_ROCK_DOWN01:0","MAP_NAVEL_ROCK_DOWN01:1/MAP_NAVEL_ROCK_DOWN02:0":"MAP_NAVEL_ROCK_DOWN02:0/MAP_NAVEL_ROCK_DOWN01:1","MAP_NAVEL_ROCK_DOWN02:0/MAP_NAVEL_ROCK_DOWN01:1":"MAP_NAVEL_ROCK_DOWN01:1/MAP_NAVEL_ROCK_DOWN02:0","MAP_NAVEL_ROCK_DOWN02:1/MAP_NAVEL_ROCK_DOWN03:0":"MAP_NAVEL_ROCK_DOWN03:0/MAP_NAVEL_ROCK_DOWN02:1","MAP_NAVEL_ROCK_DOWN03:0/MAP_NAVEL_ROCK_DOWN02:1":"MAP_NAVEL_ROCK_DOWN02:1/MAP_NAVEL_ROCK_DOWN03:0","MAP_NAVEL_ROCK_DOWN03:1/MAP_NAVEL_ROCK_DOWN04:0":"MAP_NAVEL_ROCK_DOWN04:0/MAP_NAVEL_ROCK_DOWN03:1","MAP_NAVEL_ROCK_DOWN04:0/MAP_NAVEL_ROCK_DOWN03:1":"MAP_NAVEL_ROCK_DOWN03:1/MAP_NAVEL_ROCK_DOWN04:0","MAP_NAVEL_ROCK_DOWN04:1/MAP_NAVEL_ROCK_DOWN05:0":"MAP_NAVEL_ROCK_DOWN05:0/MAP_NAVEL_ROCK_DOWN04:1","MAP_NAVEL_ROCK_DOWN05:0/MAP_NAVEL_ROCK_DOWN04:1":"MAP_NAVEL_ROCK_DOWN04:1/MAP_NAVEL_ROCK_DOWN05:0","MAP_NAVEL_ROCK_DOWN05:1/MAP_NAVEL_ROCK_DOWN06:0":"MAP_NAVEL_ROCK_DOWN06:0/MAP_NAVEL_ROCK_DOWN05:1","MAP_NAVEL_ROCK_DOWN06:0/MAP_NAVEL_ROCK_DOWN05:1":"MAP_NAVEL_ROCK_DOWN05:1/MAP_NAVEL_ROCK_DOWN06:0","MAP_NAVEL_ROCK_DOWN06:1/MAP_NAVEL_ROCK_DOWN07:0":"MAP_NAVEL_ROCK_DOWN07:0/MAP_NAVEL_ROCK_DOWN06:1","MAP_NAVEL_ROCK_DOWN07:0/MAP_NAVEL_ROCK_DOWN06:1":"MAP_NAVEL_ROCK_DOWN06:1/MAP_NAVEL_ROCK_DOWN07:0","MAP_NAVEL_ROCK_DOWN07:1/MAP_NAVEL_ROCK_DOWN08:0":"MAP_NAVEL_ROCK_DOWN08:0/MAP_NAVEL_ROCK_DOWN07:1","MAP_NAVEL_ROCK_DOWN08:0/MAP_NAVEL_ROCK_DOWN07:1":"MAP_NAVEL_ROCK_DOWN07:1/MAP_NAVEL_ROCK_DOWN08:0","MAP_NAVEL_ROCK_DOWN08:1/MAP_NAVEL_ROCK_DOWN09:0":"MAP_NAVEL_ROCK_DOWN09:0/MAP_NAVEL_ROCK_DOWN08:1","MAP_NAVEL_ROCK_DOWN09:0/MAP_NAVEL_ROCK_DOWN08:1":"MAP_NAVEL_ROCK_DOWN08:1/MAP_NAVEL_ROCK_DOWN09:0","MAP_NAVEL_ROCK_DOWN09:1/MAP_NAVEL_ROCK_DOWN10:0":"MAP_NAVEL_ROCK_DOWN10:0/MAP_NAVEL_ROCK_DOWN09:1","MAP_NAVEL_ROCK_DOWN10:0/MAP_NAVEL_ROCK_DOWN09:1":"MAP_NAVEL_ROCK_DOWN09:1/MAP_NAVEL_ROCK_DOWN10:0","MAP_NAVEL_ROCK_DOWN10:1/MAP_NAVEL_ROCK_DOWN11:1":"MAP_NAVEL_ROCK_DOWN11:1/MAP_NAVEL_ROCK_DOWN10:1","MAP_NAVEL_ROCK_DOWN11:0/MAP_NAVEL_ROCK_BOTTOM:0":"MAP_NAVEL_ROCK_BOTTOM:0/MAP_NAVEL_ROCK_DOWN11:0","MAP_NAVEL_ROCK_DOWN11:1/MAP_NAVEL_ROCK_DOWN10:1":"MAP_NAVEL_ROCK_DOWN10:1/MAP_NAVEL_ROCK_DOWN11:1","MAP_NAVEL_ROCK_ENTRANCE:0/MAP_NAVEL_ROCK_B1F:0":"MAP_NAVEL_ROCK_B1F:0/MAP_NAVEL_ROCK_ENTRANCE:0","MAP_NAVEL_ROCK_ENTRANCE:1/MAP_NAVEL_ROCK_EXTERIOR:1":"MAP_NAVEL_ROCK_EXTERIOR:1/MAP_NAVEL_ROCK_ENTRANCE:1","MAP_NAVEL_ROCK_EXTERIOR:0/MAP_NAVEL_ROCK_HARBOR:0":"MAP_NAVEL_ROCK_HARBOR:0/MAP_NAVEL_ROCK_EXTERIOR:0","MAP_NAVEL_ROCK_EXTERIOR:1/MAP_NAVEL_ROCK_ENTRANCE:1":"MAP_NAVEL_ROCK_ENTRANCE:1/MAP_NAVEL_ROCK_EXTERIOR:1","MAP_NAVEL_ROCK_FORK:0/MAP_NAVEL_ROCK_UP1:0":"MAP_NAVEL_ROCK_UP1:0/MAP_NAVEL_ROCK_FORK:0","MAP_NAVEL_ROCK_FORK:1/MAP_NAVEL_ROCK_B1F:1":"MAP_NAVEL_ROCK_B1F:1/MAP_NAVEL_ROCK_FORK:1","MAP_NAVEL_ROCK_FORK:2/MAP_NAVEL_ROCK_DOWN01:0":"MAP_NAVEL_ROCK_DOWN01:0/MAP_NAVEL_ROCK_FORK:2","MAP_NAVEL_ROCK_HARBOR:0/MAP_NAVEL_ROCK_EXTERIOR:0":"MAP_NAVEL_ROCK_EXTERIOR:0/MAP_NAVEL_ROCK_HARBOR:0","MAP_NAVEL_ROCK_TOP:0/MAP_NAVEL_ROCK_UP4:1":"MAP_NAVEL_ROCK_UP4:1/MAP_NAVEL_ROCK_TOP:0","MAP_NAVEL_ROCK_UP1:0/MAP_NAVEL_ROCK_FORK:0":"MAP_NAVEL_ROCK_FORK:0/MAP_NAVEL_ROCK_UP1:0","MAP_NAVEL_ROCK_UP1:1/MAP_NAVEL_ROCK_UP2:0":"MAP_NAVEL_ROCK_UP2:0/MAP_NAVEL_ROCK_UP1:1","MAP_NAVEL_ROCK_UP2:0/MAP_NAVEL_ROCK_UP1:1":"MAP_NAVEL_ROCK_UP1:1/MAP_NAVEL_ROCK_UP2:0","MAP_NAVEL_ROCK_UP2:1/MAP_NAVEL_ROCK_UP3:0":"MAP_NAVEL_ROCK_UP3:0/MAP_NAVEL_ROCK_UP2:1","MAP_NAVEL_ROCK_UP3:0/MAP_NAVEL_ROCK_UP2:1":"MAP_NAVEL_ROCK_UP2:1/MAP_NAVEL_ROCK_UP3:0","MAP_NAVEL_ROCK_UP3:1/MAP_NAVEL_ROCK_UP4:0":"MAP_NAVEL_ROCK_UP4:0/MAP_NAVEL_ROCK_UP3:1","MAP_NAVEL_ROCK_UP4:0/MAP_NAVEL_ROCK_UP3:1":"MAP_NAVEL_ROCK_UP3:1/MAP_NAVEL_ROCK_UP4:0","MAP_NAVEL_ROCK_UP4:1/MAP_NAVEL_ROCK_TOP:0":"MAP_NAVEL_ROCK_TOP:0/MAP_NAVEL_ROCK_UP4:1","MAP_NEW_MAUVILLE_ENTRANCE:0/MAP_ROUTE110:0":"MAP_ROUTE110:0/MAP_NEW_MAUVILLE_ENTRANCE:0","MAP_NEW_MAUVILLE_ENTRANCE:1/MAP_NEW_MAUVILLE_INSIDE:0":"MAP_NEW_MAUVILLE_INSIDE:0/MAP_NEW_MAUVILLE_ENTRANCE:1","MAP_NEW_MAUVILLE_INSIDE:0/MAP_NEW_MAUVILLE_ENTRANCE:1":"MAP_NEW_MAUVILLE_ENTRANCE:1/MAP_NEW_MAUVILLE_INSIDE:0","MAP_OLDALE_TOWN:0/MAP_OLDALE_TOWN_HOUSE1:0":"MAP_OLDALE_TOWN_HOUSE1:0,1/MAP_OLDALE_TOWN:0","MAP_OLDALE_TOWN:1/MAP_OLDALE_TOWN_HOUSE2:0":"MAP_OLDALE_TOWN_HOUSE2:0,1/MAP_OLDALE_TOWN:1","MAP_OLDALE_TOWN:2/MAP_OLDALE_TOWN_POKEMON_CENTER_1F:0":"MAP_OLDALE_TOWN_POKEMON_CENTER_1F:0,1/MAP_OLDALE_TOWN:2","MAP_OLDALE_TOWN:3/MAP_OLDALE_TOWN_MART:0":"MAP_OLDALE_TOWN_MART:0,1/MAP_OLDALE_TOWN:3","MAP_OLDALE_TOWN_HOUSE1:0,1/MAP_OLDALE_TOWN:0":"MAP_OLDALE_TOWN:0/MAP_OLDALE_TOWN_HOUSE1:0","MAP_OLDALE_TOWN_HOUSE2:0,1/MAP_OLDALE_TOWN:1":"MAP_OLDALE_TOWN:1/MAP_OLDALE_TOWN_HOUSE2:0","MAP_OLDALE_TOWN_MART:0,1/MAP_OLDALE_TOWN:3":"MAP_OLDALE_TOWN:3/MAP_OLDALE_TOWN_MART:0","MAP_OLDALE_TOWN_POKEMON_CENTER_1F:0,1/MAP_OLDALE_TOWN:2":"MAP_OLDALE_TOWN:2/MAP_OLDALE_TOWN_POKEMON_CENTER_1F:0","MAP_OLDALE_TOWN_POKEMON_CENTER_1F:2/MAP_OLDALE_TOWN_POKEMON_CENTER_2F:0":"MAP_OLDALE_TOWN_POKEMON_CENTER_2F:0/MAP_OLDALE_TOWN_POKEMON_CENTER_1F:2","MAP_OLDALE_TOWN_POKEMON_CENTER_2F:0/MAP_OLDALE_TOWN_POKEMON_CENTER_1F:2":"MAP_OLDALE_TOWN_POKEMON_CENTER_1F:2/MAP_OLDALE_TOWN_POKEMON_CENTER_2F:0","MAP_OLDALE_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_OLDALE_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_PACIFIDLOG_TOWN:0/MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F:0":"MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F:0,1/MAP_PACIFIDLOG_TOWN:0","MAP_PACIFIDLOG_TOWN:1/MAP_PACIFIDLOG_TOWN_HOUSE1:0":"MAP_PACIFIDLOG_TOWN_HOUSE1:0,1/MAP_PACIFIDLOG_TOWN:1","MAP_PACIFIDLOG_TOWN:2/MAP_PACIFIDLOG_TOWN_HOUSE2:0":"MAP_PACIFIDLOG_TOWN_HOUSE2:0,1/MAP_PACIFIDLOG_TOWN:2","MAP_PACIFIDLOG_TOWN:3/MAP_PACIFIDLOG_TOWN_HOUSE3:0":"MAP_PACIFIDLOG_TOWN_HOUSE3:0,1/MAP_PACIFIDLOG_TOWN:3","MAP_PACIFIDLOG_TOWN:4/MAP_PACIFIDLOG_TOWN_HOUSE4:0":"MAP_PACIFIDLOG_TOWN_HOUSE4:0,1/MAP_PACIFIDLOG_TOWN:4","MAP_PACIFIDLOG_TOWN:5/MAP_PACIFIDLOG_TOWN_HOUSE5:0":"MAP_PACIFIDLOG_TOWN_HOUSE5:0,1/MAP_PACIFIDLOG_TOWN:5","MAP_PACIFIDLOG_TOWN_HOUSE1:0,1/MAP_PACIFIDLOG_TOWN:1":"MAP_PACIFIDLOG_TOWN:1/MAP_PACIFIDLOG_TOWN_HOUSE1:0","MAP_PACIFIDLOG_TOWN_HOUSE2:0,1/MAP_PACIFIDLOG_TOWN:2":"MAP_PACIFIDLOG_TOWN:2/MAP_PACIFIDLOG_TOWN_HOUSE2:0","MAP_PACIFIDLOG_TOWN_HOUSE3:0,1/MAP_PACIFIDLOG_TOWN:3":"MAP_PACIFIDLOG_TOWN:3/MAP_PACIFIDLOG_TOWN_HOUSE3:0","MAP_PACIFIDLOG_TOWN_HOUSE4:0,1/MAP_PACIFIDLOG_TOWN:4":"MAP_PACIFIDLOG_TOWN:4/MAP_PACIFIDLOG_TOWN_HOUSE4:0","MAP_PACIFIDLOG_TOWN_HOUSE5:0,1/MAP_PACIFIDLOG_TOWN:5":"MAP_PACIFIDLOG_TOWN:5/MAP_PACIFIDLOG_TOWN_HOUSE5:0","MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F:0,1/MAP_PACIFIDLOG_TOWN:0":"MAP_PACIFIDLOG_TOWN:0/MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F:0","MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F:2/MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F:0":"MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F:0/MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F:2","MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F:0/MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F:2":"MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F:2/MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F:0","MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_PETALBURG_CITY:0/MAP_PETALBURG_CITY_HOUSE1:0":"MAP_PETALBURG_CITY_HOUSE1:0,1/MAP_PETALBURG_CITY:0","MAP_PETALBURG_CITY:1/MAP_PETALBURG_CITY_WALLYS_HOUSE:0":"MAP_PETALBURG_CITY_WALLYS_HOUSE:0,1/MAP_PETALBURG_CITY:1","MAP_PETALBURG_CITY:2/MAP_PETALBURG_CITY_GYM:0":"MAP_PETALBURG_CITY_GYM:0,1/MAP_PETALBURG_CITY:2","MAP_PETALBURG_CITY:3/MAP_PETALBURG_CITY_POKEMON_CENTER_1F:0":"MAP_PETALBURG_CITY_POKEMON_CENTER_1F:0,1/MAP_PETALBURG_CITY:3","MAP_PETALBURG_CITY:4/MAP_PETALBURG_CITY_HOUSE2:0":"MAP_PETALBURG_CITY_HOUSE2:0,1/MAP_PETALBURG_CITY:4","MAP_PETALBURG_CITY:5/MAP_PETALBURG_CITY_MART:0":"MAP_PETALBURG_CITY_MART:0,1/MAP_PETALBURG_CITY:5","MAP_PETALBURG_CITY_GYM:0,1/MAP_PETALBURG_CITY:2":"MAP_PETALBURG_CITY:2/MAP_PETALBURG_CITY_GYM:0","MAP_PETALBURG_CITY_GYM:10,11/MAP_PETALBURG_CITY_GYM:8":"MAP_PETALBURG_CITY_GYM:8/MAP_PETALBURG_CITY_GYM:10","MAP_PETALBURG_CITY_GYM:12,13/MAP_PETALBURG_CITY_GYM:9":"MAP_PETALBURG_CITY_GYM:9/MAP_PETALBURG_CITY_GYM:12","MAP_PETALBURG_CITY_GYM:14/MAP_PETALBURG_CITY_GYM:16":"MAP_PETALBURG_CITY_GYM:16,17/MAP_PETALBURG_CITY_GYM:14","MAP_PETALBURG_CITY_GYM:15/MAP_PETALBURG_CITY_GYM:18":"MAP_PETALBURG_CITY_GYM:18,19/MAP_PETALBURG_CITY_GYM:15","MAP_PETALBURG_CITY_GYM:16,17/MAP_PETALBURG_CITY_GYM:14":"MAP_PETALBURG_CITY_GYM:14/MAP_PETALBURG_CITY_GYM:16","MAP_PETALBURG_CITY_GYM:18,19/MAP_PETALBURG_CITY_GYM:15":"MAP_PETALBURG_CITY_GYM:15/MAP_PETALBURG_CITY_GYM:18","MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3":"MAP_PETALBURG_CITY_GYM:3,4/MAP_PETALBURG_CITY_GYM:2","MAP_PETALBURG_CITY_GYM:20/MAP_PETALBURG_CITY_GYM:24":"MAP_PETALBURG_CITY_GYM:24,25/MAP_PETALBURG_CITY_GYM:20","MAP_PETALBURG_CITY_GYM:21/MAP_PETALBURG_CITY_GYM:26":"MAP_PETALBURG_CITY_GYM:26,27/MAP_PETALBURG_CITY_GYM:21","MAP_PETALBURG_CITY_GYM:22/MAP_PETALBURG_CITY_GYM:28":"MAP_PETALBURG_CITY_GYM:28,29/MAP_PETALBURG_CITY_GYM:22","MAP_PETALBURG_CITY_GYM:23/MAP_PETALBURG_CITY_GYM:30":"MAP_PETALBURG_CITY_GYM:30,31/MAP_PETALBURG_CITY_GYM:23","MAP_PETALBURG_CITY_GYM:24,25/MAP_PETALBURG_CITY_GYM:20":"MAP_PETALBURG_CITY_GYM:20/MAP_PETALBURG_CITY_GYM:24","MAP_PETALBURG_CITY_GYM:26,27/MAP_PETALBURG_CITY_GYM:21":"MAP_PETALBURG_CITY_GYM:21/MAP_PETALBURG_CITY_GYM:26","MAP_PETALBURG_CITY_GYM:28,29/MAP_PETALBURG_CITY_GYM:22":"MAP_PETALBURG_CITY_GYM:22/MAP_PETALBURG_CITY_GYM:28","MAP_PETALBURG_CITY_GYM:3,4/MAP_PETALBURG_CITY_GYM:2":"MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3","MAP_PETALBURG_CITY_GYM:30,31/MAP_PETALBURG_CITY_GYM:23":"MAP_PETALBURG_CITY_GYM:23/MAP_PETALBURG_CITY_GYM:30","MAP_PETALBURG_CITY_GYM:32/MAP_PETALBURG_CITY_GYM:34":"MAP_PETALBURG_CITY_GYM:34,35/MAP_PETALBURG_CITY_GYM:32","MAP_PETALBURG_CITY_GYM:33/MAP_PETALBURG_CITY_GYM:36":"MAP_PETALBURG_CITY_GYM:36,37/MAP_PETALBURG_CITY_GYM:33","MAP_PETALBURG_CITY_GYM:34,35/MAP_PETALBURG_CITY_GYM:32":"MAP_PETALBURG_CITY_GYM:32/MAP_PETALBURG_CITY_GYM:34","MAP_PETALBURG_CITY_GYM:36,37/MAP_PETALBURG_CITY_GYM:33":"MAP_PETALBURG_CITY_GYM:33/MAP_PETALBURG_CITY_GYM:36","MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6":"MAP_PETALBURG_CITY_GYM:6,7/MAP_PETALBURG_CITY_GYM:5","MAP_PETALBURG_CITY_GYM:6,7/MAP_PETALBURG_CITY_GYM:5":"MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6","MAP_PETALBURG_CITY_GYM:8/MAP_PETALBURG_CITY_GYM:10":"MAP_PETALBURG_CITY_GYM:10,11/MAP_PETALBURG_CITY_GYM:8","MAP_PETALBURG_CITY_GYM:9/MAP_PETALBURG_CITY_GYM:12":"MAP_PETALBURG_CITY_GYM:12,13/MAP_PETALBURG_CITY_GYM:9","MAP_PETALBURG_CITY_HOUSE1:0,1/MAP_PETALBURG_CITY:0":"MAP_PETALBURG_CITY:0/MAP_PETALBURG_CITY_HOUSE1:0","MAP_PETALBURG_CITY_HOUSE2:0,1/MAP_PETALBURG_CITY:4":"MAP_PETALBURG_CITY:4/MAP_PETALBURG_CITY_HOUSE2:0","MAP_PETALBURG_CITY_MART:0,1/MAP_PETALBURG_CITY:5":"MAP_PETALBURG_CITY:5/MAP_PETALBURG_CITY_MART:0","MAP_PETALBURG_CITY_POKEMON_CENTER_1F:0,1/MAP_PETALBURG_CITY:3":"MAP_PETALBURG_CITY:3/MAP_PETALBURG_CITY_POKEMON_CENTER_1F:0","MAP_PETALBURG_CITY_POKEMON_CENTER_1F:2/MAP_PETALBURG_CITY_POKEMON_CENTER_2F:0":"MAP_PETALBURG_CITY_POKEMON_CENTER_2F:0/MAP_PETALBURG_CITY_POKEMON_CENTER_1F:2","MAP_PETALBURG_CITY_POKEMON_CENTER_2F:0/MAP_PETALBURG_CITY_POKEMON_CENTER_1F:2":"MAP_PETALBURG_CITY_POKEMON_CENTER_1F:2/MAP_PETALBURG_CITY_POKEMON_CENTER_2F:0","MAP_PETALBURG_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_PETALBURG_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_PETALBURG_CITY_WALLYS_HOUSE:0,1/MAP_PETALBURG_CITY:1":"MAP_PETALBURG_CITY:1/MAP_PETALBURG_CITY_WALLYS_HOUSE:0","MAP_PETALBURG_WOODS:0,1/MAP_ROUTE104:2,3":"MAP_ROUTE104:2,3/MAP_PETALBURG_WOODS:0,1","MAP_PETALBURG_WOODS:2,3/MAP_ROUTE104:4,5":"MAP_ROUTE104:4,5/MAP_PETALBURG_WOODS:2,3","MAP_PETALBURG_WOODS:4,5/MAP_ROUTE104:6,7":"MAP_ROUTE104:6,7/MAP_PETALBURG_WOODS:4,5","MAP_RECORD_CORNER:0,1,2,3/MAP_DYNAMIC:-1!":"","MAP_ROUTE103:0/MAP_ALTERING_CAVE:0":"MAP_ALTERING_CAVE:0/MAP_ROUTE103:0","MAP_ROUTE104:0/MAP_ROUTE104_MR_BRINEYS_HOUSE:0":"MAP_ROUTE104_MR_BRINEYS_HOUSE:0,1/MAP_ROUTE104:0","MAP_ROUTE104:1/MAP_ROUTE104_PRETTY_PETAL_FLOWER_SHOP:0":"MAP_ROUTE104_PRETTY_PETAL_FLOWER_SHOP:0,1/MAP_ROUTE104:1","MAP_ROUTE104:2,3/MAP_PETALBURG_WOODS:0,1":"MAP_PETALBURG_WOODS:0,1/MAP_ROUTE104:2,3","MAP_ROUTE104:4,5/MAP_PETALBURG_WOODS:2,3":"MAP_PETALBURG_WOODS:2,3/MAP_ROUTE104:4,5","MAP_ROUTE104:6,7/MAP_PETALBURG_WOODS:4,5":"MAP_PETALBURG_WOODS:4,5/MAP_ROUTE104:6,7","MAP_ROUTE104_MR_BRINEYS_HOUSE:0,1/MAP_ROUTE104:0":"MAP_ROUTE104:0/MAP_ROUTE104_MR_BRINEYS_HOUSE:0","MAP_ROUTE104_PRETTY_PETAL_FLOWER_SHOP:0,1/MAP_ROUTE104:1":"MAP_ROUTE104:1/MAP_ROUTE104_PRETTY_PETAL_FLOWER_SHOP:0","MAP_ROUTE105:0/MAP_ISLAND_CAVE:0":"MAP_ISLAND_CAVE:0/MAP_ROUTE105:0","MAP_ROUTE106:0/MAP_GRANITE_CAVE_1F:0":"MAP_GRANITE_CAVE_1F:0/MAP_ROUTE106:0","MAP_ROUTE108:0/MAP_ABANDONED_SHIP_DECK:0":"MAP_ABANDONED_SHIP_DECK:0,1/MAP_ROUTE108:0","MAP_ROUTE109:0/MAP_ROUTE109_SEASHORE_HOUSE:0":"MAP_ROUTE109_SEASHORE_HOUSE:0,1/MAP_ROUTE109:0","MAP_ROUTE109_SEASHORE_HOUSE:0,1/MAP_ROUTE109:0":"MAP_ROUTE109:0/MAP_ROUTE109_SEASHORE_HOUSE:0","MAP_ROUTE110:0/MAP_NEW_MAUVILLE_ENTRANCE:0":"MAP_NEW_MAUVILLE_ENTRANCE:0/MAP_ROUTE110:0","MAP_ROUTE110:1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:0":"MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:0,1/MAP_ROUTE110:1","MAP_ROUTE110:2/MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE:0":"MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE:0,1/MAP_ROUTE110:2","MAP_ROUTE110:3/MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE:2":"MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE:2,3/MAP_ROUTE110:3","MAP_ROUTE110:4/MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE:0":"MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE:0,1/MAP_ROUTE110:4","MAP_ROUTE110:5/MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE:2":"MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE:2,3/MAP_ROUTE110:5","MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE:0,1/MAP_ROUTE110:4":"MAP_ROUTE110:4/MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE:0","MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE:2,3/MAP_ROUTE110:5":"MAP_ROUTE110:5/MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE:2","MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE:0,1/MAP_ROUTE110:2":"MAP_ROUTE110:2/MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE:0","MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE:2,3/MAP_ROUTE110:3":"MAP_ROUTE110:3/MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE:2","MAP_ROUTE110_TRICK_HOUSE_CORRIDOR:0,1/MAP_ROUTE110_TRICK_HOUSE_END:1":"MAP_ROUTE110_TRICK_HOUSE_END:1/MAP_ROUTE110_TRICK_HOUSE_CORRIDOR:0","MAP_ROUTE110_TRICK_HOUSE_CORRIDOR:2,3/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!":"MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:0","MAP_ROUTE110_TRICK_HOUSE_END:0/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:2":"MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:2/MAP_ROUTE110_TRICK_HOUSE_END:0","MAP_ROUTE110_TRICK_HOUSE_END:1/MAP_ROUTE110_TRICK_HOUSE_CORRIDOR:0":"MAP_ROUTE110_TRICK_HOUSE_CORRIDOR:0,1/MAP_ROUTE110_TRICK_HOUSE_END:1","MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:0,1/MAP_ROUTE110:1":"MAP_ROUTE110:1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:0","MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:0":"MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2","MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2":"MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:0","MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:2/MAP_ROUTE110_TRICK_HOUSE_END:0":"MAP_ROUTE110_TRICK_HOUSE_END:0/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:2","MAP_ROUTE110_TRICK_HOUSE_PUZZLE2:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!":"MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:0","MAP_ROUTE110_TRICK_HOUSE_PUZZLE2:2/MAP_ROUTE110_TRICK_HOUSE_END:0!":"MAP_ROUTE110_TRICK_HOUSE_END:0/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:2","MAP_ROUTE110_TRICK_HOUSE_PUZZLE3:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!":"MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:0","MAP_ROUTE110_TRICK_HOUSE_PUZZLE3:2/MAP_ROUTE110_TRICK_HOUSE_END:0!":"MAP_ROUTE110_TRICK_HOUSE_END:0/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:2","MAP_ROUTE110_TRICK_HOUSE_PUZZLE4:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!":"MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:0","MAP_ROUTE110_TRICK_HOUSE_PUZZLE4:2/MAP_ROUTE110_TRICK_HOUSE_END:0!":"MAP_ROUTE110_TRICK_HOUSE_END:0/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:2","MAP_ROUTE110_TRICK_HOUSE_PUZZLE5:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!":"MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:0","MAP_ROUTE110_TRICK_HOUSE_PUZZLE5:2/MAP_ROUTE110_TRICK_HOUSE_END:0!":"MAP_ROUTE110_TRICK_HOUSE_END:0/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:2","MAP_ROUTE110_TRICK_HOUSE_PUZZLE6:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!":"MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:0","MAP_ROUTE110_TRICK_HOUSE_PUZZLE6:2/MAP_ROUTE110_TRICK_HOUSE_END:0!":"MAP_ROUTE110_TRICK_HOUSE_END:0/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:2","MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!":"MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:0","MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:10/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:9":"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:9/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:10","MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:11/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:12":"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:12/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:11","MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:12/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:11":"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:11/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:12","MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:2/MAP_ROUTE110_TRICK_HOUSE_END:0!":"MAP_ROUTE110_TRICK_HOUSE_END:0/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:2","MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:3/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:4":"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:4/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:3","MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:4/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:3":"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:3/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:4","MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:5/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:6":"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:6/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:5","MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:6/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:5":"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:5/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:6","MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:7/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:8":"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:8/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:7","MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:8/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:7":"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:7/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:8","MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:9/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:10":"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:10/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:9","MAP_ROUTE110_TRICK_HOUSE_PUZZLE8:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!":"MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:0","MAP_ROUTE110_TRICK_HOUSE_PUZZLE8:2/MAP_ROUTE110_TRICK_HOUSE_END:0!":"MAP_ROUTE110_TRICK_HOUSE_END:0/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:2","MAP_ROUTE111:0/MAP_ROUTE111_WINSTRATE_FAMILYS_HOUSE:0":"MAP_ROUTE111_WINSTRATE_FAMILYS_HOUSE:0,1/MAP_ROUTE111:0","MAP_ROUTE111:1/MAP_DESERT_RUINS:0":"MAP_DESERT_RUINS:0/MAP_ROUTE111:1","MAP_ROUTE111:2/MAP_ROUTE111_OLD_LADYS_REST_STOP:0":"MAP_ROUTE111_OLD_LADYS_REST_STOP:0,1/MAP_ROUTE111:2","MAP_ROUTE111:3/MAP_MIRAGE_TOWER_1F:0":"MAP_MIRAGE_TOWER_1F:0/MAP_ROUTE111:3","MAP_ROUTE111:4/MAP_TRAINER_HILL_ENTRANCE:0":"MAP_TRAINER_HILL_ENTRANCE:0,1/MAP_ROUTE111:4","MAP_ROUTE111_OLD_LADYS_REST_STOP:0,1/MAP_ROUTE111:2":"MAP_ROUTE111:2/MAP_ROUTE111_OLD_LADYS_REST_STOP:0","MAP_ROUTE111_WINSTRATE_FAMILYS_HOUSE:0,1/MAP_ROUTE111:0":"MAP_ROUTE111:0/MAP_ROUTE111_WINSTRATE_FAMILYS_HOUSE:0","MAP_ROUTE112:0,1/MAP_ROUTE112_CABLE_CAR_STATION:0,1":"MAP_ROUTE112_CABLE_CAR_STATION:0,1/MAP_ROUTE112:0,1","MAP_ROUTE112:2,3/MAP_JAGGED_PASS:0,1":"MAP_JAGGED_PASS:0,1/MAP_ROUTE112:2,3","MAP_ROUTE112:4/MAP_FIERY_PATH:0":"MAP_FIERY_PATH:0/MAP_ROUTE112:4","MAP_ROUTE112:5/MAP_FIERY_PATH:1":"MAP_FIERY_PATH:1/MAP_ROUTE112:5","MAP_ROUTE112_CABLE_CAR_STATION:0,1/MAP_ROUTE112:0,1":"MAP_ROUTE112:0,1/MAP_ROUTE112_CABLE_CAR_STATION:0,1","MAP_ROUTE113:0/MAP_ROUTE113_GLASS_WORKSHOP:0":"MAP_ROUTE113_GLASS_WORKSHOP:0,1/MAP_ROUTE113:0","MAP_ROUTE113:1/MAP_TERRA_CAVE_ENTRANCE:0!":"MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!","MAP_ROUTE113:2/MAP_TERRA_CAVE_ENTRANCE:0!":"MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!","MAP_ROUTE113_GLASS_WORKSHOP:0,1/MAP_ROUTE113:0":"MAP_ROUTE113:0/MAP_ROUTE113_GLASS_WORKSHOP:0","MAP_ROUTE114:0/MAP_METEOR_FALLS_1F_1R:0":"MAP_METEOR_FALLS_1F_1R:0/MAP_ROUTE114:0","MAP_ROUTE114:1/MAP_ROUTE114_FOSSIL_MANIACS_HOUSE:0":"MAP_ROUTE114_FOSSIL_MANIACS_HOUSE:0,1/MAP_ROUTE114:1","MAP_ROUTE114:2/MAP_ROUTE114_LANETTES_HOUSE:0":"MAP_ROUTE114_LANETTES_HOUSE:0,1/MAP_ROUTE114:2","MAP_ROUTE114:3/MAP_TERRA_CAVE_ENTRANCE:0!":"MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!","MAP_ROUTE114:4/MAP_TERRA_CAVE_ENTRANCE:0!":"MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!","MAP_ROUTE114_FOSSIL_MANIACS_HOUSE:0,1/MAP_ROUTE114:1":"MAP_ROUTE114:1/MAP_ROUTE114_FOSSIL_MANIACS_HOUSE:0","MAP_ROUTE114_FOSSIL_MANIACS_HOUSE:2/MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:0":"MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:0,1/MAP_ROUTE114_FOSSIL_MANIACS_HOUSE:2","MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:0,1/MAP_ROUTE114_FOSSIL_MANIACS_HOUSE:2":"MAP_ROUTE114_FOSSIL_MANIACS_HOUSE:2/MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:0","MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:2/MAP_DESERT_UNDERPASS:0":"MAP_DESERT_UNDERPASS:0/MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:2","MAP_ROUTE114_LANETTES_HOUSE:0,1/MAP_ROUTE114:2":"MAP_ROUTE114:2/MAP_ROUTE114_LANETTES_HOUSE:0","MAP_ROUTE115:0/MAP_METEOR_FALLS_1F_1R:1":"MAP_METEOR_FALLS_1F_1R:1/MAP_ROUTE115:0","MAP_ROUTE115:1/MAP_TERRA_CAVE_ENTRANCE:0!":"MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!","MAP_ROUTE115:2/MAP_TERRA_CAVE_ENTRANCE:0!":"MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!","MAP_ROUTE116:0/MAP_RUSTURF_TUNNEL:0":"MAP_RUSTURF_TUNNEL:0/MAP_ROUTE116:0","MAP_ROUTE116:1/MAP_ROUTE116_TUNNELERS_REST_HOUSE:0":"MAP_ROUTE116_TUNNELERS_REST_HOUSE:0,1/MAP_ROUTE116:1","MAP_ROUTE116:2/MAP_RUSTURF_TUNNEL:2":"MAP_RUSTURF_TUNNEL:2/MAP_ROUTE116:2","MAP_ROUTE116:3/MAP_TERRA_CAVE_ENTRANCE:0!":"MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!","MAP_ROUTE116:4/MAP_TERRA_CAVE_ENTRANCE:0!":"MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!","MAP_ROUTE116_TUNNELERS_REST_HOUSE:0,1/MAP_ROUTE116:1":"MAP_ROUTE116:1/MAP_ROUTE116_TUNNELERS_REST_HOUSE:0","MAP_ROUTE117:0/MAP_ROUTE117_POKEMON_DAY_CARE:0":"MAP_ROUTE117_POKEMON_DAY_CARE:0,1/MAP_ROUTE117:0","MAP_ROUTE117_POKEMON_DAY_CARE:0,1/MAP_ROUTE117:0":"MAP_ROUTE117:0/MAP_ROUTE117_POKEMON_DAY_CARE:0","MAP_ROUTE118:0/MAP_TERRA_CAVE_ENTRANCE:0!":"MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!","MAP_ROUTE118:1/MAP_TERRA_CAVE_ENTRANCE:0!":"MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!","MAP_ROUTE119:0/MAP_ROUTE119_WEATHER_INSTITUTE_1F:0":"MAP_ROUTE119_WEATHER_INSTITUTE_1F:0,1/MAP_ROUTE119:0","MAP_ROUTE119:1/MAP_ROUTE119_HOUSE:0":"MAP_ROUTE119_HOUSE:0,1/MAP_ROUTE119:1","MAP_ROUTE119_HOUSE:0,1/MAP_ROUTE119:1":"MAP_ROUTE119:1/MAP_ROUTE119_HOUSE:0","MAP_ROUTE119_WEATHER_INSTITUTE_1F:0,1/MAP_ROUTE119:0":"MAP_ROUTE119:0/MAP_ROUTE119_WEATHER_INSTITUTE_1F:0","MAP_ROUTE119_WEATHER_INSTITUTE_1F:2/MAP_ROUTE119_WEATHER_INSTITUTE_2F:0":"MAP_ROUTE119_WEATHER_INSTITUTE_2F:0/MAP_ROUTE119_WEATHER_INSTITUTE_1F:2","MAP_ROUTE119_WEATHER_INSTITUTE_2F:0/MAP_ROUTE119_WEATHER_INSTITUTE_1F:2":"MAP_ROUTE119_WEATHER_INSTITUTE_1F:2/MAP_ROUTE119_WEATHER_INSTITUTE_2F:0","MAP_ROUTE120:0/MAP_ANCIENT_TOMB:0":"MAP_ANCIENT_TOMB:0/MAP_ROUTE120:0","MAP_ROUTE120:1/MAP_SCORCHED_SLAB:0":"MAP_SCORCHED_SLAB:0/MAP_ROUTE120:1","MAP_ROUTE121:0/MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:2":"MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:2,3/MAP_ROUTE121:0","MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:0,1/MAP_SAFARI_ZONE_SOUTH:0":"MAP_SAFARI_ZONE_SOUTH:0/MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:0","MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:2,3/MAP_ROUTE121:0":"MAP_ROUTE121:0/MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:2","MAP_ROUTE122:0/MAP_MT_PYRE_1F:0":"MAP_MT_PYRE_1F:0,2/MAP_ROUTE122:0","MAP_ROUTE123:0/MAP_ROUTE123_BERRY_MASTERS_HOUSE:0":"MAP_ROUTE123_BERRY_MASTERS_HOUSE:0,1/MAP_ROUTE123:0","MAP_ROUTE123_BERRY_MASTERS_HOUSE:0,1/MAP_ROUTE123:0":"MAP_ROUTE123:0/MAP_ROUTE123_BERRY_MASTERS_HOUSE:0","MAP_ROUTE124:0/MAP_ROUTE124_DIVING_TREASURE_HUNTERS_HOUSE:0":"MAP_ROUTE124_DIVING_TREASURE_HUNTERS_HOUSE:0,1/MAP_ROUTE124:0","MAP_ROUTE124_DIVING_TREASURE_HUNTERS_HOUSE:0,1/MAP_ROUTE124:0":"MAP_ROUTE124:0/MAP_ROUTE124_DIVING_TREASURE_HUNTERS_HOUSE:0","MAP_ROUTE125:0/MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:0":"MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:0/MAP_ROUTE125:0","MAP_ROUTE131:0/MAP_SKY_PILLAR_ENTRANCE:0":"MAP_SKY_PILLAR_ENTRANCE:0/MAP_ROUTE131:0","MAP_RUSTBORO_CITY:0/MAP_RUSTBORO_CITY_GYM:0":"MAP_RUSTBORO_CITY_GYM:0,1/MAP_RUSTBORO_CITY:0","MAP_RUSTBORO_CITY:1/MAP_RUSTBORO_CITY_FLAT1_1F:0":"MAP_RUSTBORO_CITY_FLAT1_1F:0,1/MAP_RUSTBORO_CITY:1","MAP_RUSTBORO_CITY:10/MAP_RUSTBORO_CITY_FLAT2_1F:0":"MAP_RUSTBORO_CITY_FLAT2_1F:0,1/MAP_RUSTBORO_CITY:10","MAP_RUSTBORO_CITY:11/MAP_RUSTBORO_CITY_HOUSE3:0":"MAP_RUSTBORO_CITY_HOUSE3:0,1/MAP_RUSTBORO_CITY:11","MAP_RUSTBORO_CITY:2/MAP_RUSTBORO_CITY_MART:0":"MAP_RUSTBORO_CITY_MART:0,1/MAP_RUSTBORO_CITY:2","MAP_RUSTBORO_CITY:3/MAP_RUSTBORO_CITY_POKEMON_CENTER_1F:0":"MAP_RUSTBORO_CITY_POKEMON_CENTER_1F:0,1/MAP_RUSTBORO_CITY:3","MAP_RUSTBORO_CITY:4/MAP_RUSTBORO_CITY_POKEMON_SCHOOL:0":"MAP_RUSTBORO_CITY_POKEMON_SCHOOL:0,1/MAP_RUSTBORO_CITY:4","MAP_RUSTBORO_CITY:5,6/MAP_RUSTBORO_CITY_DEVON_CORP_1F:0,1":"MAP_RUSTBORO_CITY_DEVON_CORP_1F:0,1/MAP_RUSTBORO_CITY:5,6","MAP_RUSTBORO_CITY:7/MAP_RUSTBORO_CITY_HOUSE1:0":"MAP_RUSTBORO_CITY_HOUSE1:0,1/MAP_RUSTBORO_CITY:7","MAP_RUSTBORO_CITY:8/MAP_RUSTBORO_CITY_CUTTERS_HOUSE:0":"MAP_RUSTBORO_CITY_CUTTERS_HOUSE:0,1/MAP_RUSTBORO_CITY:8","MAP_RUSTBORO_CITY:9/MAP_RUSTBORO_CITY_HOUSE2:0":"MAP_RUSTBORO_CITY_HOUSE2:0,1/MAP_RUSTBORO_CITY:9","MAP_RUSTBORO_CITY_CUTTERS_HOUSE:0,1/MAP_RUSTBORO_CITY:8":"MAP_RUSTBORO_CITY:8/MAP_RUSTBORO_CITY_CUTTERS_HOUSE:0","MAP_RUSTBORO_CITY_DEVON_CORP_1F:0,1/MAP_RUSTBORO_CITY:5,6":"MAP_RUSTBORO_CITY:5,6/MAP_RUSTBORO_CITY_DEVON_CORP_1F:0,1","MAP_RUSTBORO_CITY_DEVON_CORP_1F:2/MAP_RUSTBORO_CITY_DEVON_CORP_2F:0":"MAP_RUSTBORO_CITY_DEVON_CORP_2F:0/MAP_RUSTBORO_CITY_DEVON_CORP_1F:2","MAP_RUSTBORO_CITY_DEVON_CORP_2F:0/MAP_RUSTBORO_CITY_DEVON_CORP_1F:2":"MAP_RUSTBORO_CITY_DEVON_CORP_1F:2/MAP_RUSTBORO_CITY_DEVON_CORP_2F:0","MAP_RUSTBORO_CITY_DEVON_CORP_2F:1/MAP_RUSTBORO_CITY_DEVON_CORP_3F:0":"MAP_RUSTBORO_CITY_DEVON_CORP_3F:0/MAP_RUSTBORO_CITY_DEVON_CORP_2F:1","MAP_RUSTBORO_CITY_DEVON_CORP_3F:0/MAP_RUSTBORO_CITY_DEVON_CORP_2F:1":"MAP_RUSTBORO_CITY_DEVON_CORP_2F:1/MAP_RUSTBORO_CITY_DEVON_CORP_3F:0","MAP_RUSTBORO_CITY_FLAT1_1F:0,1/MAP_RUSTBORO_CITY:1":"MAP_RUSTBORO_CITY:1/MAP_RUSTBORO_CITY_FLAT1_1F:0","MAP_RUSTBORO_CITY_FLAT1_1F:2/MAP_RUSTBORO_CITY_FLAT1_2F:0":"MAP_RUSTBORO_CITY_FLAT1_2F:0/MAP_RUSTBORO_CITY_FLAT1_1F:2","MAP_RUSTBORO_CITY_FLAT1_2F:0/MAP_RUSTBORO_CITY_FLAT1_1F:2":"MAP_RUSTBORO_CITY_FLAT1_1F:2/MAP_RUSTBORO_CITY_FLAT1_2F:0","MAP_RUSTBORO_CITY_FLAT2_1F:0,1/MAP_RUSTBORO_CITY:10":"MAP_RUSTBORO_CITY:10/MAP_RUSTBORO_CITY_FLAT2_1F:0","MAP_RUSTBORO_CITY_FLAT2_1F:2/MAP_RUSTBORO_CITY_FLAT2_2F:0":"MAP_RUSTBORO_CITY_FLAT2_2F:0/MAP_RUSTBORO_CITY_FLAT2_1F:2","MAP_RUSTBORO_CITY_FLAT2_2F:0/MAP_RUSTBORO_CITY_FLAT2_1F:2":"MAP_RUSTBORO_CITY_FLAT2_1F:2/MAP_RUSTBORO_CITY_FLAT2_2F:0","MAP_RUSTBORO_CITY_FLAT2_2F:1/MAP_RUSTBORO_CITY_FLAT2_3F:0":"MAP_RUSTBORO_CITY_FLAT2_3F:0/MAP_RUSTBORO_CITY_FLAT2_2F:1","MAP_RUSTBORO_CITY_FLAT2_3F:0/MAP_RUSTBORO_CITY_FLAT2_2F:1":"MAP_RUSTBORO_CITY_FLAT2_2F:1/MAP_RUSTBORO_CITY_FLAT2_3F:0","MAP_RUSTBORO_CITY_GYM:0,1/MAP_RUSTBORO_CITY:0":"MAP_RUSTBORO_CITY:0/MAP_RUSTBORO_CITY_GYM:0","MAP_RUSTBORO_CITY_HOUSE1:0,1/MAP_RUSTBORO_CITY:7":"MAP_RUSTBORO_CITY:7/MAP_RUSTBORO_CITY_HOUSE1:0","MAP_RUSTBORO_CITY_HOUSE2:0,1/MAP_RUSTBORO_CITY:9":"MAP_RUSTBORO_CITY:9/MAP_RUSTBORO_CITY_HOUSE2:0","MAP_RUSTBORO_CITY_HOUSE3:0,1/MAP_RUSTBORO_CITY:11":"MAP_RUSTBORO_CITY:11/MAP_RUSTBORO_CITY_HOUSE3:0","MAP_RUSTBORO_CITY_MART:0,1/MAP_RUSTBORO_CITY:2":"MAP_RUSTBORO_CITY:2/MAP_RUSTBORO_CITY_MART:0","MAP_RUSTBORO_CITY_POKEMON_CENTER_1F:0,1/MAP_RUSTBORO_CITY:3":"MAP_RUSTBORO_CITY:3/MAP_RUSTBORO_CITY_POKEMON_CENTER_1F:0","MAP_RUSTBORO_CITY_POKEMON_CENTER_1F:2/MAP_RUSTBORO_CITY_POKEMON_CENTER_2F:0":"MAP_RUSTBORO_CITY_POKEMON_CENTER_2F:0/MAP_RUSTBORO_CITY_POKEMON_CENTER_1F:2","MAP_RUSTBORO_CITY_POKEMON_CENTER_2F:0/MAP_RUSTBORO_CITY_POKEMON_CENTER_1F:2":"MAP_RUSTBORO_CITY_POKEMON_CENTER_1F:2/MAP_RUSTBORO_CITY_POKEMON_CENTER_2F:0","MAP_RUSTBORO_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_RUSTBORO_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_RUSTBORO_CITY_POKEMON_SCHOOL:0,1/MAP_RUSTBORO_CITY:4":"MAP_RUSTBORO_CITY:4/MAP_RUSTBORO_CITY_POKEMON_SCHOOL:0","MAP_RUSTURF_TUNNEL:0/MAP_ROUTE116:0":"MAP_ROUTE116:0/MAP_RUSTURF_TUNNEL:0","MAP_RUSTURF_TUNNEL:1/MAP_VERDANTURF_TOWN:4":"MAP_VERDANTURF_TOWN:4/MAP_RUSTURF_TUNNEL:1","MAP_RUSTURF_TUNNEL:2/MAP_ROUTE116:2":"MAP_ROUTE116:2/MAP_RUSTURF_TUNNEL:2","MAP_SAFARI_ZONE_REST_HOUSE:0,1/MAP_SAFARI_ZONE_SOUTHWEST:0":"MAP_SAFARI_ZONE_SOUTHWEST:0/MAP_SAFARI_ZONE_REST_HOUSE:0","MAP_SAFARI_ZONE_SOUTH:0/MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:0":"MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:0,1/MAP_SAFARI_ZONE_SOUTH:0","MAP_SAFARI_ZONE_SOUTHWEST:0/MAP_SAFARI_ZONE_REST_HOUSE:0":"MAP_SAFARI_ZONE_REST_HOUSE:0,1/MAP_SAFARI_ZONE_SOUTHWEST:0","MAP_SCORCHED_SLAB:0/MAP_ROUTE120:1":"MAP_ROUTE120:1/MAP_SCORCHED_SLAB:0","MAP_SEAFLOOR_CAVERN_ENTRANCE:0/MAP_UNDERWATER_ROUTE128:0!":"MAP_UNDERWATER_ROUTE128:0/MAP_UNDERWATER_SEAFLOOR_CAVERN:0","MAP_SEAFLOOR_CAVERN_ENTRANCE:1/MAP_SEAFLOOR_CAVERN_ROOM1:0":"MAP_SEAFLOOR_CAVERN_ROOM1:0/MAP_SEAFLOOR_CAVERN_ENTRANCE:1","MAP_SEAFLOOR_CAVERN_ROOM1:0/MAP_SEAFLOOR_CAVERN_ENTRANCE:1":"MAP_SEAFLOOR_CAVERN_ENTRANCE:1/MAP_SEAFLOOR_CAVERN_ROOM1:0","MAP_SEAFLOOR_CAVERN_ROOM1:1/MAP_SEAFLOOR_CAVERN_ROOM5:0":"MAP_SEAFLOOR_CAVERN_ROOM5:0/MAP_SEAFLOOR_CAVERN_ROOM1:1","MAP_SEAFLOOR_CAVERN_ROOM1:2/MAP_SEAFLOOR_CAVERN_ROOM2:0":"MAP_SEAFLOOR_CAVERN_ROOM2:0/MAP_SEAFLOOR_CAVERN_ROOM1:2","MAP_SEAFLOOR_CAVERN_ROOM2:0/MAP_SEAFLOOR_CAVERN_ROOM1:2":"MAP_SEAFLOOR_CAVERN_ROOM1:2/MAP_SEAFLOOR_CAVERN_ROOM2:0","MAP_SEAFLOOR_CAVERN_ROOM2:1/MAP_SEAFLOOR_CAVERN_ROOM4:0":"MAP_SEAFLOOR_CAVERN_ROOM4:0/MAP_SEAFLOOR_CAVERN_ROOM2:1","MAP_SEAFLOOR_CAVERN_ROOM2:2/MAP_SEAFLOOR_CAVERN_ROOM6:0":"MAP_SEAFLOOR_CAVERN_ROOM6:0/MAP_SEAFLOOR_CAVERN_ROOM2:2","MAP_SEAFLOOR_CAVERN_ROOM2:3/MAP_SEAFLOOR_CAVERN_ROOM7:0":"MAP_SEAFLOOR_CAVERN_ROOM7:0/MAP_SEAFLOOR_CAVERN_ROOM2:3","MAP_SEAFLOOR_CAVERN_ROOM3:0/MAP_SEAFLOOR_CAVERN_ROOM8:1":"MAP_SEAFLOOR_CAVERN_ROOM8:1/MAP_SEAFLOOR_CAVERN_ROOM3:0","MAP_SEAFLOOR_CAVERN_ROOM3:1/MAP_SEAFLOOR_CAVERN_ROOM7:1":"MAP_SEAFLOOR_CAVERN_ROOM7:1/MAP_SEAFLOOR_CAVERN_ROOM3:1","MAP_SEAFLOOR_CAVERN_ROOM3:2/MAP_SEAFLOOR_CAVERN_ROOM6:1":"MAP_SEAFLOOR_CAVERN_ROOM6:1/MAP_SEAFLOOR_CAVERN_ROOM3:2","MAP_SEAFLOOR_CAVERN_ROOM4:0/MAP_SEAFLOOR_CAVERN_ROOM2:1":"MAP_SEAFLOOR_CAVERN_ROOM2:1/MAP_SEAFLOOR_CAVERN_ROOM4:0","MAP_SEAFLOOR_CAVERN_ROOM4:1/MAP_SEAFLOOR_CAVERN_ROOM5:1":"MAP_SEAFLOOR_CAVERN_ROOM5:1/MAP_SEAFLOOR_CAVERN_ROOM4:1","MAP_SEAFLOOR_CAVERN_ROOM4:2/MAP_SEAFLOOR_CAVERN_ROOM5:2":"MAP_SEAFLOOR_CAVERN_ROOM5:2/MAP_SEAFLOOR_CAVERN_ROOM4:2","MAP_SEAFLOOR_CAVERN_ROOM4:3/MAP_SEAFLOOR_CAVERN_ENTRANCE:1!":"MAP_SEAFLOOR_CAVERN_ENTRANCE:1/MAP_SEAFLOOR_CAVERN_ROOM1:0","MAP_SEAFLOOR_CAVERN_ROOM5:0/MAP_SEAFLOOR_CAVERN_ROOM1:1":"MAP_SEAFLOOR_CAVERN_ROOM1:1/MAP_SEAFLOOR_CAVERN_ROOM5:0","MAP_SEAFLOOR_CAVERN_ROOM5:1/MAP_SEAFLOOR_CAVERN_ROOM4:1":"MAP_SEAFLOOR_CAVERN_ROOM4:1/MAP_SEAFLOOR_CAVERN_ROOM5:1","MAP_SEAFLOOR_CAVERN_ROOM5:2/MAP_SEAFLOOR_CAVERN_ROOM4:2":"MAP_SEAFLOOR_CAVERN_ROOM4:2/MAP_SEAFLOOR_CAVERN_ROOM5:2","MAP_SEAFLOOR_CAVERN_ROOM6:0/MAP_SEAFLOOR_CAVERN_ROOM2:2":"MAP_SEAFLOOR_CAVERN_ROOM2:2/MAP_SEAFLOOR_CAVERN_ROOM6:0","MAP_SEAFLOOR_CAVERN_ROOM6:1/MAP_SEAFLOOR_CAVERN_ROOM3:2":"MAP_SEAFLOOR_CAVERN_ROOM3:2/MAP_SEAFLOOR_CAVERN_ROOM6:1","MAP_SEAFLOOR_CAVERN_ROOM6:2/MAP_SEAFLOOR_CAVERN_ENTRANCE:1!":"MAP_SEAFLOOR_CAVERN_ENTRANCE:1/MAP_SEAFLOOR_CAVERN_ROOM1:0","MAP_SEAFLOOR_CAVERN_ROOM7:0/MAP_SEAFLOOR_CAVERN_ROOM2:3":"MAP_SEAFLOOR_CAVERN_ROOM2:3/MAP_SEAFLOOR_CAVERN_ROOM7:0","MAP_SEAFLOOR_CAVERN_ROOM7:1/MAP_SEAFLOOR_CAVERN_ROOM3:1":"MAP_SEAFLOOR_CAVERN_ROOM3:1/MAP_SEAFLOOR_CAVERN_ROOM7:1","MAP_SEAFLOOR_CAVERN_ROOM8:0/MAP_SEAFLOOR_CAVERN_ROOM9:0":"MAP_SEAFLOOR_CAVERN_ROOM9:0/MAP_SEAFLOOR_CAVERN_ROOM8:0","MAP_SEAFLOOR_CAVERN_ROOM8:1/MAP_SEAFLOOR_CAVERN_ROOM3:0":"MAP_SEAFLOOR_CAVERN_ROOM3:0/MAP_SEAFLOOR_CAVERN_ROOM8:1","MAP_SEAFLOOR_CAVERN_ROOM9:0/MAP_SEAFLOOR_CAVERN_ROOM8:0":"MAP_SEAFLOOR_CAVERN_ROOM8:0/MAP_SEAFLOOR_CAVERN_ROOM9:0","MAP_SEALED_CHAMBER_INNER_ROOM:0/MAP_SEALED_CHAMBER_OUTER_ROOM:0":"MAP_SEALED_CHAMBER_OUTER_ROOM:0/MAP_SEALED_CHAMBER_INNER_ROOM:0","MAP_SEALED_CHAMBER_OUTER_ROOM:0/MAP_SEALED_CHAMBER_INNER_ROOM:0":"MAP_SEALED_CHAMBER_INNER_ROOM:0/MAP_SEALED_CHAMBER_OUTER_ROOM:0","MAP_SECRET_BASE_BLUE_CAVE1:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_BLUE_CAVE2:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_BLUE_CAVE3:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_BLUE_CAVE4:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_BROWN_CAVE1:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_BROWN_CAVE2:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_BROWN_CAVE3:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_BROWN_CAVE4:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_RED_CAVE1:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_RED_CAVE2:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_RED_CAVE3:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_RED_CAVE4:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_SHRUB1:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_SHRUB2:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_SHRUB3:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_SHRUB4:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_TREE1:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_TREE2:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_TREE3:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_TREE4:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_YELLOW_CAVE1:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_YELLOW_CAVE2:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_YELLOW_CAVE3:0/MAP_DYNAMIC:-2!":"","MAP_SECRET_BASE_YELLOW_CAVE4:0/MAP_DYNAMIC:-2!":"","MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:0/MAP_ROUTE125:0":"MAP_ROUTE125:0/MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:0","MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:1/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:0":"MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:0/MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:1","MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:2/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:6":"MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:6/MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:2","MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:3/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:7":"MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:7/MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:3","MAP_SHOAL_CAVE_LOW_TIDE_ICE_ROOM:0/MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:3":"MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:3/MAP_SHOAL_CAVE_LOW_TIDE_ICE_ROOM:0","MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:0/MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:1":"MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:1/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:0","MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:1/MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM:0":"MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM:0/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:1","MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:2/MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM:1":"MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM:1/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:2","MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:3/MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:0":"MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:0/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:3","MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:4/MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:1":"MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:1/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:4","MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:5/MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:2":"MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:2/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:5","MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:6/MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:2":"MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:2/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:6","MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:7/MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:3":"MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:3/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:7","MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:0/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:3":"MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:3/MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:0","MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:1/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:4":"MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:4/MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:1","MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:2/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:5":"MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:5/MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:2","MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:3/MAP_SHOAL_CAVE_LOW_TIDE_ICE_ROOM:0":"MAP_SHOAL_CAVE_LOW_TIDE_ICE_ROOM:0/MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:3","MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM:0/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:1":"MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:1/MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM:0","MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM:1/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:2":"MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:2/MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM:1","MAP_SKY_PILLAR_1F:0,1/MAP_SKY_PILLAR_OUTSIDE:1":"MAP_SKY_PILLAR_OUTSIDE:1/MAP_SKY_PILLAR_1F:0","MAP_SKY_PILLAR_1F:2/MAP_SKY_PILLAR_2F:0":"MAP_SKY_PILLAR_2F:0/MAP_SKY_PILLAR_1F:2","MAP_SKY_PILLAR_2F:0/MAP_SKY_PILLAR_1F:2":"MAP_SKY_PILLAR_1F:2/MAP_SKY_PILLAR_2F:0","MAP_SKY_PILLAR_2F:1/MAP_SKY_PILLAR_3F:0":"MAP_SKY_PILLAR_3F:0/MAP_SKY_PILLAR_2F:1","MAP_SKY_PILLAR_3F:0/MAP_SKY_PILLAR_2F:1":"MAP_SKY_PILLAR_2F:1/MAP_SKY_PILLAR_3F:0","MAP_SKY_PILLAR_3F:1/MAP_SKY_PILLAR_4F:0":"MAP_SKY_PILLAR_4F:0/MAP_SKY_PILLAR_3F:1","MAP_SKY_PILLAR_3F:2/MAP_SKY_PILLAR_4F:1":"MAP_SKY_PILLAR_4F:1/MAP_SKY_PILLAR_3F:2","MAP_SKY_PILLAR_4F:0/MAP_SKY_PILLAR_3F:1":"MAP_SKY_PILLAR_3F:1/MAP_SKY_PILLAR_4F:0","MAP_SKY_PILLAR_4F:1/MAP_SKY_PILLAR_3F:2":"MAP_SKY_PILLAR_3F:2/MAP_SKY_PILLAR_4F:1","MAP_SKY_PILLAR_4F:2/MAP_SKY_PILLAR_5F:0":"MAP_SKY_PILLAR_5F:0/MAP_SKY_PILLAR_4F:2","MAP_SKY_PILLAR_5F:0/MAP_SKY_PILLAR_4F:2":"MAP_SKY_PILLAR_4F:2/MAP_SKY_PILLAR_5F:0","MAP_SKY_PILLAR_5F:1/MAP_SKY_PILLAR_TOP:0":"MAP_SKY_PILLAR_TOP:0/MAP_SKY_PILLAR_5F:1","MAP_SKY_PILLAR_ENTRANCE:0/MAP_ROUTE131:0":"MAP_ROUTE131:0/MAP_SKY_PILLAR_ENTRANCE:0","MAP_SKY_PILLAR_ENTRANCE:1/MAP_SKY_PILLAR_OUTSIDE:0":"MAP_SKY_PILLAR_OUTSIDE:0/MAP_SKY_PILLAR_ENTRANCE:1","MAP_SKY_PILLAR_OUTSIDE:0/MAP_SKY_PILLAR_ENTRANCE:1":"MAP_SKY_PILLAR_ENTRANCE:1/MAP_SKY_PILLAR_OUTSIDE:0","MAP_SKY_PILLAR_OUTSIDE:1/MAP_SKY_PILLAR_1F:0":"MAP_SKY_PILLAR_1F:0,1/MAP_SKY_PILLAR_OUTSIDE:1","MAP_SKY_PILLAR_TOP:0/MAP_SKY_PILLAR_5F:1":"MAP_SKY_PILLAR_5F:1/MAP_SKY_PILLAR_TOP:0","MAP_SLATEPORT_CITY:0/MAP_SLATEPORT_CITY_POKEMON_CENTER_1F:0":"MAP_SLATEPORT_CITY_POKEMON_CENTER_1F:0,1/MAP_SLATEPORT_CITY:0","MAP_SLATEPORT_CITY:1/MAP_SLATEPORT_CITY_MART:0":"MAP_SLATEPORT_CITY_MART:0,1/MAP_SLATEPORT_CITY:1","MAP_SLATEPORT_CITY:10/MAP_SLATEPORT_CITY_HOUSE:0":"MAP_SLATEPORT_CITY_HOUSE:0,1/MAP_SLATEPORT_CITY:10","MAP_SLATEPORT_CITY:2/MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F:0":"MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F:0,1/MAP_SLATEPORT_CITY:2","MAP_SLATEPORT_CITY:3/MAP_SLATEPORT_CITY_BATTLE_TENT_LOBBY:0":"MAP_SLATEPORT_CITY_BATTLE_TENT_LOBBY:0,1/MAP_SLATEPORT_CITY:3","MAP_SLATEPORT_CITY:4/MAP_SLATEPORT_CITY_POKEMON_FAN_CLUB:0":"MAP_SLATEPORT_CITY_POKEMON_FAN_CLUB:0,1/MAP_SLATEPORT_CITY:4","MAP_SLATEPORT_CITY:5,7/MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:0,1":"MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:0,1/MAP_SLATEPORT_CITY:5,7","MAP_SLATEPORT_CITY:6/MAP_SLATEPORT_CITY_NAME_RATERS_HOUSE:0":"MAP_SLATEPORT_CITY_NAME_RATERS_HOUSE:0,1/MAP_SLATEPORT_CITY:6","MAP_SLATEPORT_CITY:8/MAP_SLATEPORT_CITY_HARBOR:0":"MAP_SLATEPORT_CITY_HARBOR:0,1/MAP_SLATEPORT_CITY:8","MAP_SLATEPORT_CITY:9/MAP_SLATEPORT_CITY_HARBOR:2":"MAP_SLATEPORT_CITY_HARBOR:2,3/MAP_SLATEPORT_CITY:9","MAP_SLATEPORT_CITY_BATTLE_TENT_LOBBY:0,1/MAP_SLATEPORT_CITY:3":"MAP_SLATEPORT_CITY:3/MAP_SLATEPORT_CITY_BATTLE_TENT_LOBBY:0","MAP_SLATEPORT_CITY_HARBOR:0,1/MAP_SLATEPORT_CITY:8":"MAP_SLATEPORT_CITY:8/MAP_SLATEPORT_CITY_HARBOR:0","MAP_SLATEPORT_CITY_HARBOR:2,3/MAP_SLATEPORT_CITY:9":"MAP_SLATEPORT_CITY:9/MAP_SLATEPORT_CITY_HARBOR:2","MAP_SLATEPORT_CITY_HOUSE:0,1/MAP_SLATEPORT_CITY:10":"MAP_SLATEPORT_CITY:10/MAP_SLATEPORT_CITY_HOUSE:0","MAP_SLATEPORT_CITY_MART:0,1/MAP_SLATEPORT_CITY:1":"MAP_SLATEPORT_CITY:1/MAP_SLATEPORT_CITY_MART:0","MAP_SLATEPORT_CITY_NAME_RATERS_HOUSE:0,1/MAP_SLATEPORT_CITY:6":"MAP_SLATEPORT_CITY:6/MAP_SLATEPORT_CITY_NAME_RATERS_HOUSE:0","MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:0,1/MAP_SLATEPORT_CITY:5,7":"MAP_SLATEPORT_CITY:5,7/MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:0,1","MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:2/MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_2F:0":"MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_2F:0/MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:2","MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_2F:0/MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:2":"MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:2/MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_2F:0","MAP_SLATEPORT_CITY_POKEMON_CENTER_1F:0,1/MAP_SLATEPORT_CITY:0":"MAP_SLATEPORT_CITY:0/MAP_SLATEPORT_CITY_POKEMON_CENTER_1F:0","MAP_SLATEPORT_CITY_POKEMON_CENTER_1F:2/MAP_SLATEPORT_CITY_POKEMON_CENTER_2F:0":"MAP_SLATEPORT_CITY_POKEMON_CENTER_2F:0/MAP_SLATEPORT_CITY_POKEMON_CENTER_1F:2","MAP_SLATEPORT_CITY_POKEMON_CENTER_2F:0/MAP_SLATEPORT_CITY_POKEMON_CENTER_1F:2":"MAP_SLATEPORT_CITY_POKEMON_CENTER_1F:2/MAP_SLATEPORT_CITY_POKEMON_CENTER_2F:0","MAP_SLATEPORT_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_SLATEPORT_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_SLATEPORT_CITY_POKEMON_FAN_CLUB:0,1/MAP_SLATEPORT_CITY:4":"MAP_SLATEPORT_CITY:4/MAP_SLATEPORT_CITY_POKEMON_FAN_CLUB:0","MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F:0,1/MAP_SLATEPORT_CITY:2":"MAP_SLATEPORT_CITY:2/MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F:0","MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F:2/MAP_SLATEPORT_CITY_STERNS_SHIPYARD_2F:0":"MAP_SLATEPORT_CITY_STERNS_SHIPYARD_2F:0/MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F:2","MAP_SLATEPORT_CITY_STERNS_SHIPYARD_2F:0/MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F:2":"MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F:2/MAP_SLATEPORT_CITY_STERNS_SHIPYARD_2F:0","MAP_SOOTOPOLIS_CITY:0/MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F:0":"MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F:0,1/MAP_SOOTOPOLIS_CITY:0","MAP_SOOTOPOLIS_CITY:1/MAP_SOOTOPOLIS_CITY_MART:0":"MAP_SOOTOPOLIS_CITY_MART:0,1/MAP_SOOTOPOLIS_CITY:1","MAP_SOOTOPOLIS_CITY:10/MAP_SOOTOPOLIS_CITY_HOUSE7:0":"MAP_SOOTOPOLIS_CITY_HOUSE7:0,1/MAP_SOOTOPOLIS_CITY:10","MAP_SOOTOPOLIS_CITY:11/MAP_SOOTOPOLIS_CITY_LOTAD_AND_SEEDOT_HOUSE:0":"MAP_SOOTOPOLIS_CITY_LOTAD_AND_SEEDOT_HOUSE:0,1/MAP_SOOTOPOLIS_CITY:11","MAP_SOOTOPOLIS_CITY:12/MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F:0":"MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F:0,1/MAP_SOOTOPOLIS_CITY:12","MAP_SOOTOPOLIS_CITY:2/MAP_SOOTOPOLIS_CITY_GYM_1F:0":"MAP_SOOTOPOLIS_CITY_GYM_1F:0,1/MAP_SOOTOPOLIS_CITY:2","MAP_SOOTOPOLIS_CITY:3/MAP_CAVE_OF_ORIGIN_ENTRANCE:0":"MAP_CAVE_OF_ORIGIN_ENTRANCE:0/MAP_SOOTOPOLIS_CITY:3","MAP_SOOTOPOLIS_CITY:4/MAP_SOOTOPOLIS_CITY_HOUSE1:0":"MAP_SOOTOPOLIS_CITY_HOUSE1:0,1/MAP_SOOTOPOLIS_CITY:4","MAP_SOOTOPOLIS_CITY:5/MAP_SOOTOPOLIS_CITY_HOUSE2:0":"MAP_SOOTOPOLIS_CITY_HOUSE2:0,1/MAP_SOOTOPOLIS_CITY:5","MAP_SOOTOPOLIS_CITY:6/MAP_SOOTOPOLIS_CITY_HOUSE3:0":"MAP_SOOTOPOLIS_CITY_HOUSE3:0,1/MAP_SOOTOPOLIS_CITY:6","MAP_SOOTOPOLIS_CITY:7/MAP_SOOTOPOLIS_CITY_HOUSE4:0":"MAP_SOOTOPOLIS_CITY_HOUSE4:0,1/MAP_SOOTOPOLIS_CITY:7","MAP_SOOTOPOLIS_CITY:8/MAP_SOOTOPOLIS_CITY_HOUSE5:0":"MAP_SOOTOPOLIS_CITY_HOUSE5:0,1/MAP_SOOTOPOLIS_CITY:8","MAP_SOOTOPOLIS_CITY:9/MAP_SOOTOPOLIS_CITY_HOUSE6:0":"MAP_SOOTOPOLIS_CITY_HOUSE6:0,1/MAP_SOOTOPOLIS_CITY:9","MAP_SOOTOPOLIS_CITY_GYM_1F:0,1/MAP_SOOTOPOLIS_CITY:2":"MAP_SOOTOPOLIS_CITY:2/MAP_SOOTOPOLIS_CITY_GYM_1F:0","MAP_SOOTOPOLIS_CITY_GYM_1F:2/MAP_SOOTOPOLIS_CITY_GYM_B1F:0":"MAP_SOOTOPOLIS_CITY_GYM_B1F:0/MAP_SOOTOPOLIS_CITY_GYM_1F:2","MAP_SOOTOPOLIS_CITY_GYM_B1F:0/MAP_SOOTOPOLIS_CITY_GYM_1F:2":"MAP_SOOTOPOLIS_CITY_GYM_1F:2/MAP_SOOTOPOLIS_CITY_GYM_B1F:0","MAP_SOOTOPOLIS_CITY_HOUSE1:0,1/MAP_SOOTOPOLIS_CITY:4":"MAP_SOOTOPOLIS_CITY:4/MAP_SOOTOPOLIS_CITY_HOUSE1:0","MAP_SOOTOPOLIS_CITY_HOUSE2:0,1/MAP_SOOTOPOLIS_CITY:5":"MAP_SOOTOPOLIS_CITY:5/MAP_SOOTOPOLIS_CITY_HOUSE2:0","MAP_SOOTOPOLIS_CITY_HOUSE3:0,1/MAP_SOOTOPOLIS_CITY:6":"MAP_SOOTOPOLIS_CITY:6/MAP_SOOTOPOLIS_CITY_HOUSE3:0","MAP_SOOTOPOLIS_CITY_HOUSE4:0,1/MAP_SOOTOPOLIS_CITY:7":"MAP_SOOTOPOLIS_CITY:7/MAP_SOOTOPOLIS_CITY_HOUSE4:0","MAP_SOOTOPOLIS_CITY_HOUSE5:0,1/MAP_SOOTOPOLIS_CITY:8":"MAP_SOOTOPOLIS_CITY:8/MAP_SOOTOPOLIS_CITY_HOUSE5:0","MAP_SOOTOPOLIS_CITY_HOUSE6:0,1/MAP_SOOTOPOLIS_CITY:9":"MAP_SOOTOPOLIS_CITY:9/MAP_SOOTOPOLIS_CITY_HOUSE6:0","MAP_SOOTOPOLIS_CITY_HOUSE7:0,1/MAP_SOOTOPOLIS_CITY:10":"MAP_SOOTOPOLIS_CITY:10/MAP_SOOTOPOLIS_CITY_HOUSE7:0","MAP_SOOTOPOLIS_CITY_LOTAD_AND_SEEDOT_HOUSE:0,1/MAP_SOOTOPOLIS_CITY:11":"MAP_SOOTOPOLIS_CITY:11/MAP_SOOTOPOLIS_CITY_LOTAD_AND_SEEDOT_HOUSE:0","MAP_SOOTOPOLIS_CITY_MART:0,1/MAP_SOOTOPOLIS_CITY:1":"MAP_SOOTOPOLIS_CITY:1/MAP_SOOTOPOLIS_CITY_MART:0","MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F:0,1/MAP_SOOTOPOLIS_CITY:12":"MAP_SOOTOPOLIS_CITY:12/MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F:0","MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F:2/MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_B1F:0":"MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_B1F:0/MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F:2","MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_B1F:0/MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F:2":"MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F:2/MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_B1F:0","MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F:0,1/MAP_SOOTOPOLIS_CITY:0":"MAP_SOOTOPOLIS_CITY:0/MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F:0","MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F:2/MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F:0":"MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F:0/MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F:2","MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F:0/MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F:2":"MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F:2/MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F:0","MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_SOUTHERN_ISLAND_EXTERIOR:0,1/MAP_SOUTHERN_ISLAND_INTERIOR:0,1":"MAP_SOUTHERN_ISLAND_INTERIOR:0,1/MAP_SOUTHERN_ISLAND_EXTERIOR:0,1","MAP_SOUTHERN_ISLAND_INTERIOR:0,1/MAP_SOUTHERN_ISLAND_EXTERIOR:0,1":"MAP_SOUTHERN_ISLAND_EXTERIOR:0,1/MAP_SOUTHERN_ISLAND_INTERIOR:0,1","MAP_SS_TIDAL_CORRIDOR:0/MAP_SS_TIDAL_ROOMS:0":"MAP_SS_TIDAL_ROOMS:0,1/MAP_SS_TIDAL_CORRIDOR:0","MAP_SS_TIDAL_CORRIDOR:1/MAP_SS_TIDAL_ROOMS:2":"MAP_SS_TIDAL_ROOMS:2,3/MAP_SS_TIDAL_CORRIDOR:1","MAP_SS_TIDAL_CORRIDOR:2/MAP_SS_TIDAL_ROOMS:4":"MAP_SS_TIDAL_ROOMS:4,5/MAP_SS_TIDAL_CORRIDOR:2","MAP_SS_TIDAL_CORRIDOR:3/MAP_SS_TIDAL_ROOMS:6":"MAP_SS_TIDAL_ROOMS:6,7/MAP_SS_TIDAL_CORRIDOR:3","MAP_SS_TIDAL_CORRIDOR:4/MAP_SS_TIDAL_ROOMS:8":"MAP_SS_TIDAL_ROOMS:8/MAP_SS_TIDAL_CORRIDOR:4","MAP_SS_TIDAL_CORRIDOR:5/MAP_SS_TIDAL_ROOMS:9":"MAP_SS_TIDAL_ROOMS:9/MAP_SS_TIDAL_CORRIDOR:5","MAP_SS_TIDAL_CORRIDOR:6/MAP_SS_TIDAL_ROOMS:10":"MAP_SS_TIDAL_ROOMS:10/MAP_SS_TIDAL_CORRIDOR:6","MAP_SS_TIDAL_CORRIDOR:7/MAP_SS_TIDAL_ROOMS:11":"MAP_SS_TIDAL_ROOMS:11/MAP_SS_TIDAL_CORRIDOR:7","MAP_SS_TIDAL_CORRIDOR:8/MAP_SS_TIDAL_LOWER_DECK:0":"MAP_SS_TIDAL_LOWER_DECK:0/MAP_SS_TIDAL_CORRIDOR:8","MAP_SS_TIDAL_LOWER_DECK:0/MAP_SS_TIDAL_CORRIDOR:8":"MAP_SS_TIDAL_CORRIDOR:8/MAP_SS_TIDAL_LOWER_DECK:0","MAP_SS_TIDAL_ROOMS:0,1/MAP_SS_TIDAL_CORRIDOR:0":"MAP_SS_TIDAL_CORRIDOR:0/MAP_SS_TIDAL_ROOMS:0","MAP_SS_TIDAL_ROOMS:10/MAP_SS_TIDAL_CORRIDOR:6":"MAP_SS_TIDAL_CORRIDOR:6/MAP_SS_TIDAL_ROOMS:10","MAP_SS_TIDAL_ROOMS:11/MAP_SS_TIDAL_CORRIDOR:7":"MAP_SS_TIDAL_CORRIDOR:7/MAP_SS_TIDAL_ROOMS:11","MAP_SS_TIDAL_ROOMS:2,3/MAP_SS_TIDAL_CORRIDOR:1":"MAP_SS_TIDAL_CORRIDOR:1/MAP_SS_TIDAL_ROOMS:2","MAP_SS_TIDAL_ROOMS:4,5/MAP_SS_TIDAL_CORRIDOR:2":"MAP_SS_TIDAL_CORRIDOR:2/MAP_SS_TIDAL_ROOMS:4","MAP_SS_TIDAL_ROOMS:6,7/MAP_SS_TIDAL_CORRIDOR:3":"MAP_SS_TIDAL_CORRIDOR:3/MAP_SS_TIDAL_ROOMS:6","MAP_SS_TIDAL_ROOMS:8/MAP_SS_TIDAL_CORRIDOR:4":"MAP_SS_TIDAL_CORRIDOR:4/MAP_SS_TIDAL_ROOMS:8","MAP_SS_TIDAL_ROOMS:9/MAP_SS_TIDAL_CORRIDOR:5":"MAP_SS_TIDAL_CORRIDOR:5/MAP_SS_TIDAL_ROOMS:9","MAP_TERRA_CAVE_END:0/MAP_TERRA_CAVE_ENTRANCE:1":"MAP_TERRA_CAVE_ENTRANCE:1/MAP_TERRA_CAVE_END:0","MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!":"","MAP_TERRA_CAVE_ENTRANCE:1/MAP_TERRA_CAVE_END:0":"MAP_TERRA_CAVE_END:0/MAP_TERRA_CAVE_ENTRANCE:1","MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!":"","MAP_TRAINER_HILL_1F:0/MAP_TRAINER_HILL_ENTRANCE:2":"MAP_TRAINER_HILL_ENTRANCE:2/MAP_TRAINER_HILL_1F:0","MAP_TRAINER_HILL_1F:1/MAP_TRAINER_HILL_2F:0":"MAP_TRAINER_HILL_2F:0/MAP_TRAINER_HILL_1F:1","MAP_TRAINER_HILL_2F:0/MAP_TRAINER_HILL_1F:1":"MAP_TRAINER_HILL_1F:1/MAP_TRAINER_HILL_2F:0","MAP_TRAINER_HILL_2F:1/MAP_TRAINER_HILL_3F:0":"MAP_TRAINER_HILL_3F:0/MAP_TRAINER_HILL_2F:1","MAP_TRAINER_HILL_3F:0/MAP_TRAINER_HILL_2F:1":"MAP_TRAINER_HILL_2F:1/MAP_TRAINER_HILL_3F:0","MAP_TRAINER_HILL_3F:1/MAP_TRAINER_HILL_4F:0":"MAP_TRAINER_HILL_4F:0/MAP_TRAINER_HILL_3F:1","MAP_TRAINER_HILL_4F:0/MAP_TRAINER_HILL_3F:1":"MAP_TRAINER_HILL_3F:1/MAP_TRAINER_HILL_4F:0","MAP_TRAINER_HILL_4F:1/MAP_TRAINER_HILL_ROOF:0":"MAP_TRAINER_HILL_ROOF:0/MAP_TRAINER_HILL_4F:1","MAP_TRAINER_HILL_ELEVATOR:0,1/MAP_TRAINER_HILL_ROOF:1":"MAP_TRAINER_HILL_ROOF:1/MAP_TRAINER_HILL_ELEVATOR:1","MAP_TRAINER_HILL_ENTRANCE:0,1/MAP_ROUTE111:4":"MAP_ROUTE111:4/MAP_TRAINER_HILL_ENTRANCE:0","MAP_TRAINER_HILL_ENTRANCE:2/MAP_TRAINER_HILL_1F:0":"MAP_TRAINER_HILL_1F:0/MAP_TRAINER_HILL_ENTRANCE:2","MAP_TRAINER_HILL_ROOF:0/MAP_TRAINER_HILL_4F:1":"MAP_TRAINER_HILL_4F:1/MAP_TRAINER_HILL_ROOF:0","MAP_TRAINER_HILL_ROOF:1/MAP_TRAINER_HILL_ELEVATOR:1":"MAP_TRAINER_HILL_ELEVATOR:0,1/MAP_TRAINER_HILL_ROOF:1","MAP_UNDERWATER_MARINE_CAVE:0/MAP_DYNAMIC:-1!":"","MAP_UNDERWATER_ROUTE105:0/MAP_UNDERWATER_MARINE_CAVE:0!":"MAP_UNDERWATER_MARINE_CAVE:0/MAP_DYNAMIC:-1!","MAP_UNDERWATER_ROUTE105:1/MAP_UNDERWATER_MARINE_CAVE:0!":"MAP_UNDERWATER_MARINE_CAVE:0/MAP_DYNAMIC:-1!","MAP_UNDERWATER_ROUTE125:0/MAP_UNDERWATER_MARINE_CAVE:0!":"MAP_UNDERWATER_MARINE_CAVE:0/MAP_DYNAMIC:-1!","MAP_UNDERWATER_ROUTE125:1/MAP_UNDERWATER_MARINE_CAVE:0!":"MAP_UNDERWATER_MARINE_CAVE:0/MAP_DYNAMIC:-1!","MAP_UNDERWATER_ROUTE126:0/MAP_UNDERWATER_SOOTOPOLIS_CITY:0":"MAP_UNDERWATER_SOOTOPOLIS_CITY:0,1/MAP_UNDERWATER_ROUTE126:0","MAP_UNDERWATER_ROUTE127:0/MAP_UNDERWATER_MARINE_CAVE:0!":"MAP_UNDERWATER_MARINE_CAVE:0/MAP_DYNAMIC:-1!","MAP_UNDERWATER_ROUTE127:1/MAP_UNDERWATER_MARINE_CAVE:0!":"MAP_UNDERWATER_MARINE_CAVE:0/MAP_DYNAMIC:-1!","MAP_UNDERWATER_ROUTE128:0/MAP_UNDERWATER_SEAFLOOR_CAVERN:0":"MAP_UNDERWATER_SEAFLOOR_CAVERN:0/MAP_UNDERWATER_ROUTE128:0","MAP_UNDERWATER_ROUTE129:0/MAP_UNDERWATER_MARINE_CAVE:0!":"MAP_UNDERWATER_MARINE_CAVE:0/MAP_DYNAMIC:-1!","MAP_UNDERWATER_ROUTE129:1/MAP_UNDERWATER_MARINE_CAVE:0!":"MAP_UNDERWATER_MARINE_CAVE:0/MAP_DYNAMIC:-1!","MAP_UNDERWATER_ROUTE134:0/MAP_UNDERWATER_SEALED_CHAMBER:0":"MAP_UNDERWATER_SEALED_CHAMBER:0/MAP_UNDERWATER_ROUTE134:0","MAP_UNDERWATER_SEAFLOOR_CAVERN:0/MAP_UNDERWATER_ROUTE128:0":"MAP_UNDERWATER_ROUTE128:0/MAP_UNDERWATER_SEAFLOOR_CAVERN:0","MAP_UNDERWATER_SEALED_CHAMBER:0/MAP_UNDERWATER_ROUTE134:0":"MAP_UNDERWATER_ROUTE134:0/MAP_UNDERWATER_SEALED_CHAMBER:0","MAP_UNDERWATER_SOOTOPOLIS_CITY:0,1/MAP_UNDERWATER_ROUTE126:0":"MAP_UNDERWATER_ROUTE126:0/MAP_UNDERWATER_SOOTOPOLIS_CITY:0","MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!":"","MAP_VERDANTURF_TOWN:0/MAP_VERDANTURF_TOWN_BATTLE_TENT_LOBBY:0":"MAP_VERDANTURF_TOWN_BATTLE_TENT_LOBBY:0,1/MAP_VERDANTURF_TOWN:0","MAP_VERDANTURF_TOWN:1/MAP_VERDANTURF_TOWN_MART:0":"MAP_VERDANTURF_TOWN_MART:0,1/MAP_VERDANTURF_TOWN:1","MAP_VERDANTURF_TOWN:2/MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F:0":"MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F:0,1/MAP_VERDANTURF_TOWN:2","MAP_VERDANTURF_TOWN:3/MAP_VERDANTURF_TOWN_WANDAS_HOUSE:0":"MAP_VERDANTURF_TOWN_WANDAS_HOUSE:0,1/MAP_VERDANTURF_TOWN:3","MAP_VERDANTURF_TOWN:4/MAP_RUSTURF_TUNNEL:1":"MAP_RUSTURF_TUNNEL:1/MAP_VERDANTURF_TOWN:4","MAP_VERDANTURF_TOWN:5/MAP_VERDANTURF_TOWN_FRIENDSHIP_RATERS_HOUSE:0":"MAP_VERDANTURF_TOWN_FRIENDSHIP_RATERS_HOUSE:0,1/MAP_VERDANTURF_TOWN:5","MAP_VERDANTURF_TOWN:6/MAP_VERDANTURF_TOWN_HOUSE:0":"MAP_VERDANTURF_TOWN_HOUSE:0,1/MAP_VERDANTURF_TOWN:6","MAP_VERDANTURF_TOWN_BATTLE_TENT_LOBBY:0,1/MAP_VERDANTURF_TOWN:0":"MAP_VERDANTURF_TOWN:0/MAP_VERDANTURF_TOWN_BATTLE_TENT_LOBBY:0","MAP_VERDANTURF_TOWN_FRIENDSHIP_RATERS_HOUSE:0,1/MAP_VERDANTURF_TOWN:5":"MAP_VERDANTURF_TOWN:5/MAP_VERDANTURF_TOWN_FRIENDSHIP_RATERS_HOUSE:0","MAP_VERDANTURF_TOWN_HOUSE:0,1/MAP_VERDANTURF_TOWN:6":"MAP_VERDANTURF_TOWN:6/MAP_VERDANTURF_TOWN_HOUSE:0","MAP_VERDANTURF_TOWN_MART:0,1/MAP_VERDANTURF_TOWN:1":"MAP_VERDANTURF_TOWN:1/MAP_VERDANTURF_TOWN_MART:0","MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F:0,1/MAP_VERDANTURF_TOWN:2":"MAP_VERDANTURF_TOWN:2/MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F:0","MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F:2/MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F:0":"MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F:0/MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F:2","MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F:0/MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F:2":"MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F:2/MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F:0","MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!":"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!","MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!":"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!","MAP_VERDANTURF_TOWN_WANDAS_HOUSE:0,1/MAP_VERDANTURF_TOWN:3":"MAP_VERDANTURF_TOWN:3/MAP_VERDANTURF_TOWN_WANDAS_HOUSE:0","MAP_VICTORY_ROAD_1F:0/MAP_EVER_GRANDE_CITY:2":"MAP_EVER_GRANDE_CITY:2/MAP_VICTORY_ROAD_1F:0","MAP_VICTORY_ROAD_1F:1/MAP_EVER_GRANDE_CITY:3":"MAP_EVER_GRANDE_CITY:3/MAP_VICTORY_ROAD_1F:1","MAP_VICTORY_ROAD_1F:2/MAP_VICTORY_ROAD_B1F:5":"MAP_VICTORY_ROAD_B1F:5/MAP_VICTORY_ROAD_1F:2","MAP_VICTORY_ROAD_1F:3/MAP_VICTORY_ROAD_B1F:2":"MAP_VICTORY_ROAD_B1F:2/MAP_VICTORY_ROAD_1F:3","MAP_VICTORY_ROAD_1F:4/MAP_VICTORY_ROAD_B1F:4":"MAP_VICTORY_ROAD_B1F:4/MAP_VICTORY_ROAD_1F:4","MAP_VICTORY_ROAD_B1F:0/MAP_VICTORY_ROAD_B2F:0":"MAP_VICTORY_ROAD_B2F:0/MAP_VICTORY_ROAD_B1F:0","MAP_VICTORY_ROAD_B1F:1/MAP_VICTORY_ROAD_B2F:2":"MAP_VICTORY_ROAD_B2F:2/MAP_VICTORY_ROAD_B1F:1","MAP_VICTORY_ROAD_B1F:2/MAP_VICTORY_ROAD_1F:3":"MAP_VICTORY_ROAD_1F:3/MAP_VICTORY_ROAD_B1F:2","MAP_VICTORY_ROAD_B1F:3/MAP_VICTORY_ROAD_B2F:1":"MAP_VICTORY_ROAD_B2F:1/MAP_VICTORY_ROAD_B1F:3","MAP_VICTORY_ROAD_B1F:4/MAP_VICTORY_ROAD_1F:4":"MAP_VICTORY_ROAD_1F:4/MAP_VICTORY_ROAD_B1F:4","MAP_VICTORY_ROAD_B1F:5/MAP_VICTORY_ROAD_1F:2":"MAP_VICTORY_ROAD_1F:2/MAP_VICTORY_ROAD_B1F:5","MAP_VICTORY_ROAD_B1F:6/MAP_VICTORY_ROAD_B2F:3":"MAP_VICTORY_ROAD_B2F:3/MAP_VICTORY_ROAD_B1F:6","MAP_VICTORY_ROAD_B2F:0/MAP_VICTORY_ROAD_B1F:0":"MAP_VICTORY_ROAD_B1F:0/MAP_VICTORY_ROAD_B2F:0","MAP_VICTORY_ROAD_B2F:1/MAP_VICTORY_ROAD_B1F:3":"MAP_VICTORY_ROAD_B1F:3/MAP_VICTORY_ROAD_B2F:1","MAP_VICTORY_ROAD_B2F:2/MAP_VICTORY_ROAD_B1F:1":"MAP_VICTORY_ROAD_B1F:1/MAP_VICTORY_ROAD_B2F:2","MAP_VICTORY_ROAD_B2F:3/MAP_VICTORY_ROAD_B1F:6":"MAP_VICTORY_ROAD_B1F:6/MAP_VICTORY_ROAD_B2F:3"}}
diff --git a/worlds/pokemon_emerald/data/items.json b/worlds/pokemon_emerald/data/items.json
new file mode 100644
index 000000000000..139d75aad0ab
--- /dev/null
+++ b/worlds/pokemon_emerald/data/items.json
@@ -0,0 +1,1781 @@
+{
+ "ITEM_BADGE_1": {
+ "label": "Stone Badge",
+ "classification": "PROGRESSION",
+ "tags": ["Badge", "Unique"],
+ "modern_id": null
+ },
+ "ITEM_BADGE_2": {
+ "label": "Knuckle Badge",
+ "classification": "PROGRESSION",
+ "tags": ["Badge", "Unique"],
+ "modern_id": null
+ },
+ "ITEM_BADGE_3": {
+ "label": "Dynamo Badge",
+ "classification": "PROGRESSION",
+ "tags": ["Badge", "Unique"],
+ "modern_id": null
+ },
+ "ITEM_BADGE_4": {
+ "label": "Heat Badge",
+ "classification": "PROGRESSION",
+ "tags": ["Badge", "Unique"],
+ "modern_id": null
+ },
+ "ITEM_BADGE_5": {
+ "label": "Balance Badge",
+ "classification": "PROGRESSION",
+ "tags": ["Badge", "Unique"],
+ "modern_id": null
+ },
+ "ITEM_BADGE_6": {
+ "label": "Feather Badge",
+ "classification": "PROGRESSION",
+ "tags": ["Badge", "Unique"],
+ "modern_id": null
+ },
+ "ITEM_BADGE_7": {
+ "label": "Mind Badge",
+ "classification": "PROGRESSION",
+ "tags": ["Badge", "Unique"],
+ "modern_id": null
+ },
+ "ITEM_BADGE_8": {
+ "label": "Rain Badge",
+ "classification": "PROGRESSION",
+ "tags": ["Badge", "Unique"],
+ "modern_id": null
+ },
+
+
+ "ITEM_HM_CUT": {
+ "label": "HM01 Cut",
+ "classification": "PROGRESSION",
+ "tags": ["HM", "Unique"],
+ "modern_id": 420
+ },
+ "ITEM_HM_FLY": {
+ "label": "HM02 Fly",
+ "classification": "PROGRESSION",
+ "tags": ["HM", "Unique"],
+ "modern_id": 421
+ },
+ "ITEM_HM_SURF": {
+ "label": "HM03 Surf",
+ "classification": "PROGRESSION",
+ "tags": ["HM", "Unique"],
+ "modern_id": 422
+ },
+ "ITEM_HM_STRENGTH": {
+ "label": "HM04 Strength",
+ "classification": "PROGRESSION",
+ "tags": ["HM", "Unique"],
+ "modern_id": 423
+ },
+ "ITEM_HM_FLASH": {
+ "label": "HM05 Flash",
+ "classification": "PROGRESSION",
+ "tags": ["HM", "Unique"],
+ "modern_id": 424
+ },
+ "ITEM_HM_ROCK_SMASH": {
+ "label": "HM06 Rock Smash",
+ "classification": "PROGRESSION",
+ "tags": ["HM", "Unique"],
+ "modern_id": 425
+ },
+ "ITEM_HM_WATERFALL": {
+ "label": "HM07 Waterfall",
+ "classification": "PROGRESSION",
+ "tags": ["HM", "Unique"],
+ "modern_id": 737
+ },
+ "ITEM_HM_DIVE": {
+ "label": "HM08 Dive",
+ "classification": "PROGRESSION",
+ "tags": ["HM", "Unique"],
+ "modern_id": null
+ },
+
+
+ "ITEM_MACH_BIKE": {
+ "label": "Mach Bike",
+ "classification": "PROGRESSION",
+ "tags": ["Bike", "Unique"],
+ "modern_id": 718
+ },
+ "ITEM_ACRO_BIKE": {
+ "label": "Acro Bike",
+ "classification": "PROGRESSION",
+ "tags": ["Bike", "Unique"],
+ "modern_id": 719
+ },
+
+
+ "ITEM_DEVON_GOODS": {
+ "label": "Devon Goods",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 721
+ },
+ "ITEM_LETTER": {
+ "label": "Letter",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 725
+ },
+ "ITEM_ITEMFINDER": {
+ "label": "Itemfinder",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": null
+ },
+ "ITEM_METEORITE": {
+ "label": "Meteorite",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 729
+ },
+ "ITEM_GO_GOGGLES": {
+ "label": "Go Goggles",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 728
+ },
+ "ITEM_ROOM_1_KEY": {
+ "label": "Room 1 Key",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 730
+ },
+ "ITEM_ROOM_2_KEY": {
+ "label": "Room 2 Key",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 731
+ },
+ "ITEM_ROOM_4_KEY": {
+ "label": "Room 4 Key",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 732
+ },
+ "ITEM_ROOM_6_KEY": {
+ "label": "Room 6 Key",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 733
+ },
+ "ITEM_STORAGE_KEY": {
+ "label": "Storage Key",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 734
+ },
+ "ITEM_SCANNER": {
+ "label": "Scanner",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 727
+ },
+ "ITEM_BASEMENT_KEY": {
+ "label": "Basement Key",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 476
+ },
+ "ITEM_DEVON_SCOPE": {
+ "label": "Devon Scope",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 735
+ },
+ "ITEM_MAGMA_EMBLEM": {
+ "label": "Magma Emblem",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": null
+ },
+ "ITEM_POKEBLOCK_CASE": {
+ "label": "Pokeblock Case",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 724
+ },
+ "ITEM_SS_TICKET": {
+ "label": "S.S. Ticket",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 736
+ },
+ "ITEM_WAILMER_PAIL": {
+ "label": "Wailmer Pail",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 720
+ },
+
+
+ "ITEM_POWDER_JAR": {
+ "label": "Powder Jar",
+ "classification": "FILLER",
+ "tags": ["Unique"],
+ "modern_id": null
+ },
+ "ITEM_COIN_CASE": {
+ "label": "Coin Case",
+ "classification": "FILLER",
+ "tags": ["Unique"],
+ "modern_id": 444
+ },
+ "ITEM_CONTEST_PASS": {
+ "label": "Contest Pass",
+ "classification": "FILLER",
+ "tags": ["Unique"],
+ "modern_id": 457
+ },
+ "ITEM_SOOT_SACK": {
+ "label": "Soot Sack",
+ "classification": "FILLER",
+ "tags": ["Unique"],
+ "modern_id": 722
+ },
+ "ITEM_ROOT_FOSSIL": {
+ "label": "Root Fossil",
+ "classification": "FILLER",
+ "tags": ["Unique"],
+ "modern_id": 99
+ },
+ "ITEM_CLAW_FOSSIL": {
+ "label": "Claw Fossil",
+ "classification": "FILLER",
+ "tags": ["Unique"],
+ "modern_id": 100
+ },
+ "ITEM_EON_TICKET": {
+ "label": "Eon Ticket",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": 726
+ },
+ "ITEM_OLD_SEA_MAP": {
+ "label": "Old Sea Map",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": null
+ },
+ "ITEM_MYSTIC_TICKET": {
+ "label": "Mystic Ticket",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": null
+ },
+ "ITEM_AURORA_TICKET": {
+ "label": "Aurora Ticket",
+ "classification": "PROGRESSION",
+ "tags": ["Unique"],
+ "modern_id": null
+ },
+
+ "ITEM_OLD_ROD": {
+ "label": "Old Rod",
+ "classification": "PROGRESSION",
+ "tags": ["Rod", "Unique"],
+ "modern_id": 445
+ },
+ "ITEM_GOOD_ROD": {
+ "label": "Good Rod",
+ "classification": "PROGRESSION",
+ "tags": ["Rod", "Unique"],
+ "modern_id": 446
+ },
+ "ITEM_SUPER_ROD": {
+ "label": "Super Rod",
+ "classification": "PROGRESSION",
+ "tags": ["Rod", "Unique"],
+ "modern_id": 447
+ },
+
+
+ "ITEM_MASTER_BALL": {
+ "label": "Master Ball",
+ "classification": "USEFUL",
+ "tags": ["Ball"],
+ "modern_id": 1
+ },
+ "ITEM_ULTRA_BALL": {
+ "label": "Ultra Ball",
+ "classification": "FILLER",
+ "tags": ["Ball"],
+ "modern_id": 2
+ },
+ "ITEM_GREAT_BALL": {
+ "label": "Great Ball",
+ "classification": "FILLER",
+ "tags": ["Ball"],
+ "modern_id": 3
+ },
+ "ITEM_POKE_BALL": {
+ "label": "Poke Ball",
+ "classification": "FILLER",
+ "tags": ["Ball"],
+ "modern_id": 4
+ },
+ "ITEM_SAFARI_BALL": {
+ "label": "Safari Ball",
+ "classification": "FILLER",
+ "tags": ["Ball"],
+ "modern_id": 5
+ },
+ "ITEM_NET_BALL": {
+ "label": "Net Ball",
+ "classification": "FILLER",
+ "tags": ["Ball"],
+ "modern_id": 6
+ },
+ "ITEM_DIVE_BALL": {
+ "label": "Dive Ball",
+ "classification": "FILLER",
+ "tags": ["Ball"],
+ "modern_id": 7
+ },
+ "ITEM_NEST_BALL": {
+ "label": "Nest Ball",
+ "classification": "FILLER",
+ "tags": ["Ball"],
+ "modern_id": 8
+ },
+ "ITEM_REPEAT_BALL": {
+ "label": "Repeat Ball",
+ "classification": "FILLER",
+ "tags": ["Ball"],
+ "modern_id": 9
+ },
+ "ITEM_TIMER_BALL": {
+ "label": "Timer Ball",
+ "classification": "FILLER",
+ "tags": ["Ball"],
+ "modern_id": 10
+ },
+ "ITEM_LUXURY_BALL": {
+ "label": "Luxury Ball",
+ "classification": "FILLER",
+ "tags": ["Ball"],
+ "modern_id": 11
+ },
+ "ITEM_PREMIER_BALL": {
+ "label": "Premier Ball",
+ "classification": "FILLER",
+ "tags": ["Ball"],
+ "modern_id": 12
+ },
+
+
+ "ITEM_POTION": {
+ "label": "Potion",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 17
+ },
+ "ITEM_ANTIDOTE": {
+ "label": "Antidote",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 18
+ },
+ "ITEM_BURN_HEAL": {
+ "label": "Burn Heal",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 19
+ },
+ "ITEM_ICE_HEAL": {
+ "label": "Ice Heal",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 20
+ },
+ "ITEM_AWAKENING": {
+ "label": "Awakening",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 21
+ },
+ "ITEM_PARALYZE_HEAL": {
+ "label": "Paralyze Heal",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 22
+ },
+ "ITEM_FULL_RESTORE": {
+ "label": "Full Restore",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 23
+ },
+ "ITEM_MAX_POTION": {
+ "label": "Max Potion",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 24
+ },
+ "ITEM_HYPER_POTION": {
+ "label": "Hyper Potion",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 25
+ },
+ "ITEM_SUPER_POTION": {
+ "label": "Super Potion",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 26
+ },
+ "ITEM_FULL_HEAL": {
+ "label": "Full Heal",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 27
+ },
+ "ITEM_REVIVE": {
+ "label": "Revive",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 28
+ },
+ "ITEM_MAX_REVIVE": {
+ "label": "Max Revive",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 29
+ },
+ "ITEM_FRESH_WATER": {
+ "label": "Fresh Water",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 30
+ },
+ "ITEM_SODA_POP": {
+ "label": "Soda Pop",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 31
+ },
+ "ITEM_LEMONADE": {
+ "label": "Lemonade",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 32
+ },
+ "ITEM_MOOMOO_MILK": {
+ "label": "Moomoo Milk",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 33
+ },
+ "ITEM_ENERGY_POWDER": {
+ "label": "Energy Powder",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 34
+ },
+ "ITEM_ENERGY_ROOT": {
+ "label": "Energy Root",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 35
+ },
+ "ITEM_HEAL_POWDER": {
+ "label": "Heal Powder",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 36
+ },
+ "ITEM_REVIVAL_HERB": {
+ "label": "Revival Herb",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 37
+ },
+ "ITEM_ETHER": {
+ "label": "Ether",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 38
+ },
+ "ITEM_MAX_ETHER": {
+ "label": "Max Ether",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 39
+ },
+ "ITEM_ELIXIR": {
+ "label": "Elixir",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 40
+ },
+ "ITEM_MAX_ELIXIR": {
+ "label": "Max Elixir",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 41
+ },
+ "ITEM_LAVA_COOKIE": {
+ "label": "Lava Cookie",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 42
+ },
+ "ITEM_BERRY_JUICE": {
+ "label": "Berry Juice",
+ "classification": "FILLER",
+ "tags": ["Heal"],
+ "modern_id": 43
+ },
+ "ITEM_SACRED_ASH": {
+ "label": "Sacred Ash",
+ "classification": "USEFUL",
+ "tags": ["Heal"],
+ "modern_id": 44
+ },
+
+
+ "ITEM_SHOAL_SALT": {
+ "label": "Shoal Salt",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 70
+ },
+ "ITEM_SHOAL_SHELL": {
+ "label": "Shoal Shell",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 71
+ },
+ "ITEM_RED_SHARD": {
+ "label": "Red Shard",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 72
+ },
+ "ITEM_BLUE_SHARD": {
+ "label": "Blue Shard",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 73
+ },
+ "ITEM_YELLOW_SHARD": {
+ "label": "Yellow Shard",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 74
+ },
+ "ITEM_GREEN_SHARD": {
+ "label": "Green Shard",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 75
+ },
+
+
+ "ITEM_HP_UP": {
+ "label": "HP Up",
+ "classification": "FILLER",
+ "tags": ["Vitamin"],
+ "modern_id": 45
+ },
+ "ITEM_PROTEIN": {
+ "label": "Protein",
+ "classification": "FILLER",
+ "tags": ["Vitamin"],
+ "modern_id": 46
+ },
+ "ITEM_IRON": {
+ "label": "Iron",
+ "classification": "FILLER",
+ "tags": ["Vitamin"],
+ "modern_id": 47
+ },
+ "ITEM_CARBOS": {
+ "label": "Carbos",
+ "classification": "FILLER",
+ "tags": ["Vitamin"],
+ "modern_id": 48
+ },
+ "ITEM_CALCIUM": {
+ "label": "Calcium",
+ "classification": "FILLER",
+ "tags": ["Vitamin"],
+ "modern_id": 49
+ },
+ "ITEM_ZINC": {
+ "label": "Zinc",
+ "classification": "FILLER",
+ "tags": ["Vitamin"],
+ "modern_id": 52
+ },
+ "ITEM_PP_UP": {
+ "label": "PP Up",
+ "classification": "FILLER",
+ "tags": ["Vitamin"],
+ "modern_id": 51
+ },
+ "ITEM_PP_MAX": {
+ "label": "PP Max",
+ "classification": "FILLER",
+ "tags": ["Vitamin"],
+ "modern_id": 53
+ },
+ "ITEM_RARE_CANDY": {
+ "label": "Rare Candy",
+ "classification": "USEFUL",
+ "tags": ["Candy"],
+ "modern_id": 50
+ },
+
+
+ "ITEM_GUARD_SPEC": {
+ "label": "Guard Spec",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 55
+ },
+ "ITEM_DIRE_HIT": {
+ "label": "Dire Hit",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 56
+ },
+ "ITEM_X_ATTACK": {
+ "label": "X Attack",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 57
+ },
+ "ITEM_X_DEFEND": {
+ "label": "X Defend",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 58
+ },
+ "ITEM_X_SPEED": {
+ "label": "X Speed",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 59
+ },
+ "ITEM_X_ACCURACY": {
+ "label": "X Accuracy",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 60
+ },
+ "ITEM_X_SPECIAL": {
+ "label": "X Special",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 61
+ },
+
+
+ "ITEM_REPEL": {
+ "label": "Repel",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 79
+ },
+ "ITEM_SUPER_REPEL": {
+ "label": "Super Repel",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 76
+ },
+ "ITEM_MAX_REPEL": {
+ "label": "Max Repel",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 77
+ },
+ "ITEM_POKE_DOLL": {
+ "label": "Poke Doll",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 63
+ },
+ "ITEM_FLUFFY_TAIL": {
+ "label": "Fluffy Tail",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 64
+ },
+ "ITEM_ESCAPE_ROPE": {
+ "label": "Escape Rope",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 78
+ },
+ "ITEM_BLUE_FLUTE": {
+ "label": "Blue Flute",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 65
+ },
+ "ITEM_YELLOW_FLUTE": {
+ "label": "Yellow Flute",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 66
+ },
+ "ITEM_RED_FLUTE": {
+ "label": "Red Flute",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 67
+ },
+ "ITEM_BLACK_FLUTE": {
+ "label": "Black Flute",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 68
+ },
+ "ITEM_WHITE_FLUTE": {
+ "label": "White Flute",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 69
+ },
+ "ITEM_HEART_SCALE": {
+ "label": "Heart Scale",
+ "classification": "FILLER",
+ "tags": ["Misc"],
+ "modern_id": 93
+ },
+
+
+ "ITEM_SUN_STONE": {
+ "label": "Sun Stone",
+ "classification": "USEFUL",
+ "tags": ["EvoStone"],
+ "modern_id": 80
+ },
+ "ITEM_MOON_STONE": {
+ "label": "Moon Stone",
+ "classification": "USEFUL",
+ "tags": ["EvoStone"],
+ "modern_id": 81
+ },
+ "ITEM_FIRE_STONE": {
+ "label": "Fire Stone",
+ "classification": "USEFUL",
+ "tags": ["EvoStone"],
+ "modern_id": 82
+ },
+ "ITEM_THUNDER_STONE": {
+ "label": "Thunder Stone",
+ "classification": "USEFUL",
+ "tags": ["EvoStone"],
+ "modern_id": 83
+ },
+ "ITEM_WATER_STONE": {
+ "label": "Water Stone",
+ "classification": "USEFUL",
+ "tags": ["EvoStone"],
+ "modern_id": 84
+ },
+ "ITEM_LEAF_STONE": {
+ "label": "Leaf Stone",
+ "classification": "USEFUL",
+ "tags": ["EvoStone"],
+ "modern_id": 85
+ },
+
+
+ "ITEM_TINY_MUSHROOM": {
+ "label": "Tiny Mushroom",
+ "classification": "FILLER",
+ "tags": ["Money"],
+ "modern_id": 86
+ },
+ "ITEM_BIG_MUSHROOM": {
+ "label": "Big Mushroom",
+ "classification": "FILLER",
+ "tags": ["Money"],
+ "modern_id": 87
+ },
+ "ITEM_PEARL": {
+ "label": "Pearl",
+ "classification": "FILLER",
+ "tags": ["Money"],
+ "modern_id": 88
+ },
+ "ITEM_BIG_PEARL": {
+ "label": "Big Pearl",
+ "classification": "FILLER",
+ "tags": ["Money"],
+ "modern_id": 89
+ },
+ "ITEM_STARDUST": {
+ "label": "Stardust",
+ "classification": "FILLER",
+ "tags": ["Money"],
+ "modern_id": 90
+ },
+ "ITEM_STAR_PIECE": {
+ "label": "Star Piece",
+ "classification": "FILLER",
+ "tags": ["Money"],
+ "modern_id": 91
+ },
+ "ITEM_NUGGET": {
+ "label": "Nugget",
+ "classification": "FILLER",
+ "tags": ["Money"],
+ "modern_id": 92
+ },
+
+
+ "ITEM_ORANGE_MAIL": {
+ "label": "Orange Mail",
+ "classification": "FILLER",
+ "tags": ["Mail"],
+ "modern_id": 137
+ },
+ "ITEM_HARBOR_MAIL": {
+ "label": "Harbor Mail",
+ "classification": "FILLER",
+ "tags": ["Mail"],
+ "modern_id": 138
+ },
+ "ITEM_GLITTER_MAIL": {
+ "label": "Glitter Mail",
+ "classification": "FILLER",
+ "tags": ["Mail"],
+ "modern_id": 139
+ },
+ "ITEM_MECH_MAIL": {
+ "label": "Mech Mail",
+ "classification": "FILLER",
+ "tags": ["Mail"],
+ "modern_id": 140
+ },
+ "ITEM_WOOD_MAIL": {
+ "label": "Wood Mail",
+ "classification": "FILLER",
+ "tags": ["Mail"],
+ "modern_id": 141
+ },
+ "ITEM_WAVE_MAIL": {
+ "label": "Wave Mail",
+ "classification": "FILLER",
+ "tags": ["Mail"],
+ "modern_id": 142
+ },
+ "ITEM_BEAD_MAIL": {
+ "label": "Bead Mail",
+ "classification": "FILLER",
+ "tags": ["Mail"],
+ "modern_id": 143
+ },
+ "ITEM_SHADOW_MAIL": {
+ "label": "Shadow Mail",
+ "classification": "FILLER",
+ "tags": ["Mail"],
+ "modern_id": 144
+ },
+ "ITEM_TROPIC_MAIL": {
+ "label": "Tropic Mail",
+ "classification": "FILLER",
+ "tags": ["Mail"],
+ "modern_id": 145
+ },
+ "ITEM_DREAM_MAIL": {
+ "label": "Dream Mail",
+ "classification": "FILLER",
+ "tags": ["Mail"],
+ "modern_id": 146
+ },
+ "ITEM_FAB_MAIL": {
+ "label": "Fab Mail",
+ "classification": "FILLER",
+ "tags": ["Mail"],
+ "modern_id": 147
+ },
+ "ITEM_RETRO_MAIL": {
+ "label": "Retro Mail",
+ "classification": "FILLER",
+ "tags": ["Mail"],
+ "modern_id": 148
+ },
+
+
+ "ITEM_CHERI_BERRY": {
+ "label": "Cheri Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 149
+ },
+ "ITEM_CHESTO_BERRY": {
+ "label": "Chesto Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 150
+ },
+ "ITEM_PECHA_BERRY": {
+ "label": "Pecha Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 151
+ },
+ "ITEM_RAWST_BERRY": {
+ "label": "Rawst Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 152
+ },
+ "ITEM_ASPEAR_BERRY": {
+ "label": "Aspear Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 153
+ },
+ "ITEM_LEPPA_BERRY": {
+ "label": "Leppa Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 154
+ },
+ "ITEM_ORAN_BERRY": {
+ "label": "Oran Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 155
+ },
+ "ITEM_PERSIM_BERRY": {
+ "label": "Persim Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 156
+ },
+ "ITEM_LUM_BERRY": {
+ "label": "Lum Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 157
+ },
+ "ITEM_SITRUS_BERRY": {
+ "label": "Sitrus Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 158
+ },
+ "ITEM_FIGY_BERRY": {
+ "label": "Figy Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 159
+ },
+ "ITEM_WIKI_BERRY": {
+ "label": "Wiki Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 160
+ },
+ "ITEM_MAGO_BERRY": {
+ "label": "Mago Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 161
+ },
+ "ITEM_AGUAV_BERRY": {
+ "label": "Aguav Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 162
+ },
+ "ITEM_IAPAPA_BERRY": {
+ "label": "Iapapa Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 163
+ },
+ "ITEM_RAZZ_BERRY": {
+ "label": "Razz Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 164
+ },
+ "ITEM_BLUK_BERRY": {
+ "label": "Bluk Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 165
+ },
+ "ITEM_NANAB_BERRY": {
+ "label": "Nanab Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 166
+ },
+ "ITEM_WEPEAR_BERRY": {
+ "label": "Wepear Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 167
+ },
+ "ITEM_PINAP_BERRY": {
+ "label": "Pinap Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 168
+ },
+ "ITEM_POMEG_BERRY": {
+ "label": "Pomeg Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 169
+ },
+ "ITEM_KELPSY_BERRY": {
+ "label": "Kelpsy Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 170
+ },
+ "ITEM_QUALOT_BERRY": {
+ "label": "Qualot Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 171
+ },
+ "ITEM_HONDEW_BERRY": {
+ "label": "Hondew Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 172
+ },
+ "ITEM_GREPA_BERRY": {
+ "label": "Grepa Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 173
+ },
+ "ITEM_TAMATO_BERRY": {
+ "label": "Tamato Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 174
+ },
+ "ITEM_CORNN_BERRY": {
+ "label": "Cornn Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 175
+ },
+ "ITEM_MAGOST_BERRY": {
+ "label": "Magost Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 176
+ },
+ "ITEM_RABUTA_BERRY": {
+ "label": "Rabuta Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 177
+ },
+ "ITEM_NOMEL_BERRY": {
+ "label": "Nomel Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 178
+ },
+ "ITEM_SPELON_BERRY": {
+ "label": "Spelon Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 179
+ },
+ "ITEM_PAMTRE_BERRY": {
+ "label": "Pamtre Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 180
+ },
+ "ITEM_WATMEL_BERRY": {
+ "label": "Watmel Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 181
+ },
+ "ITEM_DURIN_BERRY": {
+ "label": "Durin Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 182
+ },
+ "ITEM_BELUE_BERRY": {
+ "label": "Belue Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 183
+ },
+ "ITEM_LIECHI_BERRY": {
+ "label": "Liechi Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 201
+ },
+ "ITEM_GANLON_BERRY": {
+ "label": "Ganlon Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 202
+ },
+ "ITEM_SALAC_BERRY": {
+ "label": "Salac Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 203
+ },
+ "ITEM_PETAYA_BERRY": {
+ "label": "Petaya Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 204
+ },
+ "ITEM_APICOT_BERRY": {
+ "label": "Apicot Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 205
+ },
+ "ITEM_LANSAT_BERRY": {
+ "label": "Lansat Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 206
+ },
+ "ITEM_STARF_BERRY": {
+ "label": "Starf Berry",
+ "classification": "FILLER",
+ "tags": ["Berry"],
+ "modern_id": 207
+ },
+
+
+ "ITEM_BRIGHT_POWDER": {
+ "label": "Bright Powder",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 213
+ },
+ "ITEM_WHITE_HERB": {
+ "label": "White Herb",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 214
+ },
+ "ITEM_MACHO_BRACE": {
+ "label": "Macho Brace",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 215
+ },
+ "ITEM_EXP_SHARE": {
+ "label": "Exp. Share",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 216
+ },
+ "ITEM_QUICK_CLAW": {
+ "label": "Quick Claw",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 217
+ },
+ "ITEM_SOOTHE_BELL": {
+ "label": "Soothe Bell",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 218
+ },
+ "ITEM_MENTAL_HERB": {
+ "label": "Mental Herb",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 219
+ },
+ "ITEM_CHOICE_BAND": {
+ "label": "Choice Band",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 220
+ },
+ "ITEM_KINGS_ROCK": {
+ "label": "King's Rock",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 221
+ },
+ "ITEM_SILVER_POWDER": {
+ "label": "Silver Powder",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 222
+ },
+ "ITEM_AMULET_COIN": {
+ "label": "Amulet Coin",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 223
+ },
+ "ITEM_CLEANSE_TAG": {
+ "label": "Cleanse Tag",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 224
+ },
+ "ITEM_SOUL_DEW": {
+ "label": "Soul Dew",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 225
+ },
+ "ITEM_DEEP_SEA_TOOTH": {
+ "label": "Deep Sea Tooth",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 226
+ },
+ "ITEM_DEEP_SEA_SCALE": {
+ "label": "Deep Sea Scale",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 227
+ },
+ "ITEM_SMOKE_BALL": {
+ "label": "Smoke Ball",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 228
+ },
+ "ITEM_EVERSTONE": {
+ "label": "Everstone",
+ "classification": "FILLER",
+ "tags": ["Held"],
+ "modern_id": 229
+ },
+ "ITEM_FOCUS_BAND": {
+ "label": "Focus Band",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 230
+ },
+ "ITEM_LUCKY_EGG": {
+ "label": "Lucky Egg",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 231
+ },
+ "ITEM_SCOPE_LENS": {
+ "label": "Scope Lens",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 232
+ },
+ "ITEM_METAL_COAT": {
+ "label": "Metal Coat",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 233
+ },
+ "ITEM_LEFTOVERS": {
+ "label": "Leftovers",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 234
+ },
+ "ITEM_DRAGON_SCALE": {
+ "label": "Dragon Scale",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 235
+ },
+ "ITEM_LIGHT_BALL": {
+ "label": "Light Ball",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 236
+ },
+ "ITEM_SOFT_SAND": {
+ "label": "Soft Sand",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 237
+ },
+ "ITEM_HARD_STONE": {
+ "label": "Hard Stone",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 238
+ },
+ "ITEM_MIRACLE_SEED": {
+ "label": "Miracle Seed",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 239
+ },
+ "ITEM_BLACK_GLASSES": {
+ "label": "Black Glasses",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 240
+ },
+ "ITEM_BLACK_BELT": {
+ "label": "Black Belt",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 241
+ },
+ "ITEM_MAGNET": {
+ "label": "Magnet",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 242
+ },
+ "ITEM_MYSTIC_WATER": {
+ "label": "Mystic Water",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 243
+ },
+ "ITEM_SHARP_BEAK": {
+ "label": "Sharp Beak",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 244
+ },
+ "ITEM_POISON_BARB": {
+ "label": "Poison Barb",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 245
+ },
+ "ITEM_NEVER_MELT_ICE": {
+ "label": "Never-Melt Ice",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 246
+ },
+ "ITEM_SPELL_TAG": {
+ "label": "Spell Tag",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 247
+ },
+ "ITEM_TWISTED_SPOON": {
+ "label": "Twisted Spoon",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 248
+ },
+ "ITEM_CHARCOAL": {
+ "label": "Charcoal",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 249
+ },
+ "ITEM_DRAGON_FANG": {
+ "label": "Dragon Fang",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 250
+ },
+ "ITEM_SILK_SCARF": {
+ "label": "Silk Scarf",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 251
+ },
+ "ITEM_UP_GRADE": {
+ "label": "Up-Grade",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 252
+ },
+ "ITEM_SHELL_BELL": {
+ "label": "Shell Bell",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 253
+ },
+ "ITEM_SEA_INCENSE": {
+ "label": "Sea Incense",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 254
+ },
+ "ITEM_LAX_INCENSE": {
+ "label": "Lax Incense",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 255
+ },
+ "ITEM_LUCKY_PUNCH": {
+ "label": "Lucky Punch",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 256
+ },
+ "ITEM_METAL_POWDER": {
+ "label": "Metal Powder",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 257
+ },
+ "ITEM_THICK_CLUB": {
+ "label": "Thick Club",
+ "classification": "USEFUL",
+ "tags": ["Held"],
+ "modern_id": 258
+ },
+ "ITEM_STICK": {
+ "label": "Stick",
+ "classification": "FILLER",
+ "tags": ["Held"],
+ "modern_id": 259
+ },
+ "ITEM_RED_SCARF": {
+ "label": "Red Scarf",
+ "classification": "FILLER",
+ "tags": ["Held"],
+ "modern_id": 260
+ },
+ "ITEM_BLUE_SCARF": {
+ "label": "Blue Scarf",
+ "classification": "FILLER",
+ "tags": ["Held"],
+ "modern_id": 261
+ },
+ "ITEM_PINK_SCARF": {
+ "label": "Pink Scarf",
+ "classification": "FILLER",
+ "tags": ["Held"],
+ "modern_id": 262
+ },
+ "ITEM_GREEN_SCARF": {
+ "label": "Green Scarf",
+ "classification": "FILLER",
+ "tags": ["Held"],
+ "modern_id": 263
+ },
+ "ITEM_YELLOW_SCARF": {
+ "label": "Yellow Scarf",
+ "classification": "FILLER",
+ "tags": ["Held"],
+ "modern_id": 264
+ },
+
+
+ "ITEM_TM_FOCUS_PUNCH": {
+ "label": "TM01",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 328
+ },
+ "ITEM_TM_DRAGON_CLAW": {
+ "label": "TM02",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 329
+ },
+ "ITEM_TM_WATER_PULSE": {
+ "label": "TM03",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 330
+ },
+ "ITEM_TM_CALM_MIND": {
+ "label": "TM04",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 331
+ },
+ "ITEM_TM_ROAR": {
+ "label": "TM05",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 332
+ },
+ "ITEM_TM_TOXIC": {
+ "label": "TM06",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 333
+ },
+ "ITEM_TM_HAIL": {
+ "label": "TM07",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 334
+ },
+ "ITEM_TM_BULK_UP": {
+ "label": "TM08",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 335
+ },
+ "ITEM_TM_BULLET_SEED": {
+ "label": "TM09",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 336
+ },
+ "ITEM_TM_HIDDEN_POWER": {
+ "label": "TM10",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 337
+ },
+ "ITEM_TM_SUNNY_DAY": {
+ "label": "TM11",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 338
+ },
+ "ITEM_TM_TAUNT": {
+ "label": "TM12",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 339
+ },
+ "ITEM_TM_ICE_BEAM": {
+ "label": "TM13",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 340
+ },
+ "ITEM_TM_BLIZZARD": {
+ "label": "TM14",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 341
+ },
+ "ITEM_TM_HYPER_BEAM": {
+ "label": "TM15",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 342
+ },
+ "ITEM_TM_LIGHT_SCREEN": {
+ "label": "TM16",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 343
+ },
+ "ITEM_TM_PROTECT": {
+ "label": "TM17",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 344
+ },
+ "ITEM_TM_RAIN_DANCE": {
+ "label": "TM18",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 345
+ },
+ "ITEM_TM_GIGA_DRAIN": {
+ "label": "TM19",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 346
+ },
+ "ITEM_TM_SAFEGUARD": {
+ "label": "TM20",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 347
+ },
+ "ITEM_TM_FRUSTRATION": {
+ "label": "TM21",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 348
+ },
+ "ITEM_TM_SOLAR_BEAM": {
+ "label": "TM22",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 349
+ },
+ "ITEM_TM_IRON_TAIL": {
+ "label": "TM23",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 350
+ },
+ "ITEM_TM_THUNDERBOLT": {
+ "label": "TM24",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 351
+ },
+ "ITEM_TM_THUNDER": {
+ "label": "TM25",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 352
+ },
+ "ITEM_TM_EARTHQUAKE": {
+ "label": "TM26",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 353
+ },
+ "ITEM_TM_RETURN": {
+ "label": "TM27",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 354
+ },
+ "ITEM_TM_DIG": {
+ "label": "TM28",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 355
+ },
+ "ITEM_TM_PSYCHIC": {
+ "label": "TM29",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 356
+ },
+ "ITEM_TM_SHADOW_BALL": {
+ "label": "TM30",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 357
+ },
+ "ITEM_TM_BRICK_BREAK": {
+ "label": "TM31",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 358
+ },
+ "ITEM_TM_DOUBLE_TEAM": {
+ "label": "TM32",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 359
+ },
+ "ITEM_TM_REFLECT": {
+ "label": "TM33",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 360
+ },
+ "ITEM_TM_SHOCK_WAVE": {
+ "label": "TM34",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 361
+ },
+ "ITEM_TM_FLAMETHROWER": {
+ "label": "TM35",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 362
+ },
+ "ITEM_TM_SLUDGE_BOMB": {
+ "label": "TM36",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 363
+ },
+ "ITEM_TM_SANDSTORM": {
+ "label": "TM37",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 364
+ },
+ "ITEM_TM_FIRE_BLAST": {
+ "label": "TM38",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 365
+ },
+ "ITEM_TM_ROCK_TOMB": {
+ "label": "TM39",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 366
+ },
+ "ITEM_TM_AERIAL_ACE": {
+ "label": "TM40",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 367
+ },
+ "ITEM_TM_TORMENT": {
+ "label": "TM41",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 368
+ },
+ "ITEM_TM_FACADE": {
+ "label": "TM42",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 369
+ },
+ "ITEM_TM_SECRET_POWER": {
+ "label": "TM43",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 370
+ },
+ "ITEM_TM_REST": {
+ "label": "TM44",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 371
+ },
+ "ITEM_TM_ATTRACT": {
+ "label": "TM45",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 372
+ },
+ "ITEM_TM_THIEF": {
+ "label": "TM46",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 373
+ },
+ "ITEM_TM_STEEL_WING": {
+ "label": "TM47",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 374
+ },
+ "ITEM_TM_SKILL_SWAP": {
+ "label": "TM48",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 375
+ },
+ "ITEM_TM_SNATCH": {
+ "label": "TM49",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 376
+ },
+ "ITEM_TM_OVERHEAT": {
+ "label": "TM50",
+ "classification": "USEFUL",
+ "tags": ["TM"],
+ "modern_id": 377
+ }
+}
diff --git a/worlds/pokemon_emerald/data/locations.json b/worlds/pokemon_emerald/data/locations.json
new file mode 100644
index 000000000000..55ef15d871bb
--- /dev/null
+++ b/worlds/pokemon_emerald/data/locations.json
@@ -0,0 +1,5364 @@
+{
+ "BADGE_1": {
+ "label": "Rustboro Gym - Stone Badge",
+ "tags": ["Badge"]
+ },
+ "BADGE_2": {
+ "label": "Dewford Gym - Knuckle Badge",
+ "tags": ["Badge"]
+ },
+ "BADGE_3": {
+ "label": "Mauville Gym - Dynamo Badge",
+ "tags": ["Badge"]
+ },
+ "BADGE_4": {
+ "label": "Lavaridge Gym - Heat Badge",
+ "tags": ["Badge"]
+ },
+ "BADGE_5": {
+ "label": "Petalburg Gym - Balance Badge",
+ "tags": ["Badge"]
+ },
+ "BADGE_6": {
+ "label": "Fortree Gym - Feather Badge",
+ "tags": ["Badge"]
+ },
+ "BADGE_7": {
+ "label": "Mossdeep Gym - Mind Badge",
+ "tags": ["Badge"]
+ },
+ "BADGE_8": {
+ "label": "Sootopolis Gym - Rain Badge",
+ "tags": ["Badge"]
+ },
+
+ "NPC_GIFT_RECEIVED_HM_CUT": {
+ "label": "Rustboro City - HM01 from Cutter's House",
+ "tags": ["HM"]
+ },
+ "NPC_GIFT_RECEIVED_HM_FLY": {
+ "label": "Route 119 - HM02 from Rival Battle",
+ "tags": ["HM"]
+ },
+ "NPC_GIFT_RECEIVED_HM_SURF": {
+ "label": "Petalburg City - HM03 from Wally's Uncle",
+ "tags": ["HM"]
+ },
+ "NPC_GIFT_RECEIVED_HM_STRENGTH": {
+ "label": "Rusturf Tunnel - HM04 from Tunneler",
+ "tags": ["HM"]
+ },
+ "NPC_GIFT_RECEIVED_HM_FLASH": {
+ "label": "Granite Cave 1F - HM05 from Hiker",
+ "tags": ["HM"]
+ },
+ "NPC_GIFT_RECEIVED_HM_ROCK_SMASH": {
+ "label": "Mauville City - HM06 from Rock Smash Guy",
+ "tags": ["HM"]
+ },
+ "NPC_GIFT_RECEIVED_HM_WATERFALL": {
+ "label": "Sootopolis City - HM07 from Wallace",
+ "tags": ["HM"]
+ },
+ "NPC_GIFT_RECEIVED_HM_DIVE": {
+ "label": "Mossdeep City - HM08 from Steven's House",
+ "tags": ["HM"]
+ },
+
+ "NPC_GIFT_RECEIVED_ACRO_BIKE": {
+ "label": "Mauville City - Acro Bike",
+ "tags": ["Bike"]
+ },
+ "NPC_GIFT_RECEIVED_MACH_BIKE": {
+ "label": "Mauville City - Mach Bike",
+ "tags": ["Bike"]
+ },
+
+ "NPC_GIFT_RECEIVED_WAILMER_PAIL": {
+ "label": "Route 104 - Wailmer Pail from Flower Shop Lady",
+ "tags": ["KeyItem"]
+ },
+ "NPC_GIFT_RECEIVED_DEVON_GOODS_RUSTURF_TUNNEL": {
+ "label": "Rusturf Tunnel - Recover Devon Goods",
+ "tags": ["KeyItem"]
+ },
+ "NPC_GIFT_RECEIVED_LETTER": {
+ "label": "Devon Corp 3F - Letter from Mr. Stone",
+ "tags": ["KeyItem"]
+ },
+ "NPC_GIFT_RECEIVED_COIN_CASE": {
+ "label": "Mauville City - Coin Case from Lady in House",
+ "tags": ["KeyItem"]
+ },
+ "NPC_GIFT_RECEIVED_METEORITE": {
+ "label": "Mt Chimney - Meteorite from Machine",
+ "tags": ["KeyItem"]
+ },
+ "NPC_GIFT_RECEIVED_GO_GOGGLES": {
+ "label": "Lavaridge Town - Go Goggles from Rival",
+ "tags": ["KeyItem"]
+ },
+ "NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON": {
+ "label": "Mauville City - Basement Key from Wattson",
+ "tags": ["KeyItem"]
+ },
+ "NPC_GIFT_RECEIVED_ITEMFINDER": {
+ "label": "Route 110 - Itemfinder from Rival",
+ "tags": ["KeyItem"]
+ },
+ "NPC_GIFT_RECEIVED_DEVON_SCOPE": {
+ "label": "Route 120 - Devon Scope from Steven",
+ "tags": ["KeyItem"]
+ },
+ "NPC_GIFT_RECEIVED_MAGMA_EMBLEM": {
+ "label": "Mt Pyre Summit - Magma Emblem from Old Lady",
+ "tags": ["KeyItem"]
+ },
+ "ITEM_ABANDONED_SHIP_CAPTAINS_OFFICE_STORAGE_KEY": {
+ "label": "Abandoned Ship - Captain's Office Key",
+ "tags": ["KeyItem"]
+ },
+ "HIDDEN_ITEM_ABANDONED_SHIP_RM_4_KEY": {
+ "label": "Abandoned Ship HF - Room 4 Key",
+ "tags": ["KeyItem"]
+ },
+ "HIDDEN_ITEM_ABANDONED_SHIP_RM_1_KEY": {
+ "label": "Abandoned Ship HF - Room 1 Key",
+ "tags": ["KeyItem"]
+ },
+ "HIDDEN_ITEM_ABANDONED_SHIP_RM_6_KEY": {
+ "label": "Abandoned Ship HF - Room 6 Key",
+ "tags": ["KeyItem"]
+ },
+ "HIDDEN_ITEM_ABANDONED_SHIP_RM_2_KEY": {
+ "label": "Abandoned Ship HF - Room 2 Key",
+ "tags": ["KeyItem"]
+ },
+ "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_2_SCANNER": {
+ "label": "Abandoned Ship HF - Scanner",
+ "tags": ["KeyItem"]
+ },
+ "NPC_GIFT_RECEIVED_POKEBLOCK_CASE": {
+ "label": "Lilycove City - Pokeblock Case from Contest Hall",
+ "tags": ["KeyItem"]
+ },
+ "NPC_GIFT_RECEIVED_SS_TICKET": {
+ "label": "Littleroot Town - S.S. Ticket from Norman",
+ "tags": ["KeyItem"]
+ },
+ "NPC_GIFT_RECEIVED_AURORA_TICKET": {
+ "label": "Littleroot Town - Aurora Ticket from Norman",
+ "tags": ["EventTicket"]
+ },
+ "NPC_GIFT_RECEIVED_EON_TICKET": {
+ "label": "Littleroot Town - Eon Ticket from Norman",
+ "tags": ["EventTicket"]
+ },
+ "NPC_GIFT_RECEIVED_MYSTIC_TICKET": {
+ "label": "Littleroot Town - Mystic Ticket from Norman",
+ "tags": ["EventTicket"]
+ },
+ "NPC_GIFT_RECEIVED_OLD_SEA_MAP": {
+ "label": "Littleroot Town - Old Sea Map from Norman",
+ "tags": ["EventTicket"]
+ },
+
+ "NPC_GIFT_RECEIVED_OLD_ROD": {
+ "label": "Dewford Town - Old Rod from Fisherman",
+ "tags": ["Rod"]
+ },
+ "NPC_GIFT_RECEIVED_GOOD_ROD": {
+ "label": "Route 118 - Good Rod from Fisherman",
+ "tags": ["Rod"]
+ },
+ "NPC_GIFT_RECEIVED_SUPER_ROD": {
+ "label": "Mossdeep City - Super Rod from Fisherman in House",
+ "tags": ["Rod"]
+ },
+
+ "HIDDEN_ITEM_ARTISAN_CAVE_B1F_CALCIUM": {
+ "label": "Artisan Cave B1F - Hidden Item 1",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ARTISAN_CAVE_B1F_IRON": {
+ "label": "Artisan Cave B1F - Hidden Item 2",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ARTISAN_CAVE_B1F_PROTEIN": {
+ "label": "Artisan Cave B1F - Hidden Item 3",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ARTISAN_CAVE_B1F_ZINC": {
+ "label": "Artisan Cave B1F - Hidden Item 4",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_FALLARBOR_TOWN_NUGGET": {
+ "label": "Fallarbor Town - Hidden Item in Crater",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_GRANITE_CAVE_B2F_EVERSTONE_1": {
+ "label": "Granite Cave B2F - Hidden Item After Crumbling Floor",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_GRANITE_CAVE_B2F_EVERSTONE_2": {
+ "label": "Granite Cave B2F - Hidden Item on Platform",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_JAGGED_PASS_FULL_HEAL": {
+ "label": "Jagged Pass - Hidden Item in Grass",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_JAGGED_PASS_GREAT_BALL": {
+ "label": "Jagged Pass - Hidden Item in Corner",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_LAVARIDGE_TOWN_ICE_HEAL": {
+ "label": "Lavaridge Town - Hidden Item in Springs",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_LILYCOVE_CITY_HEART_SCALE": {
+ "label": "Lilycove City - Hidden Item on Beach West",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_LILYCOVE_CITY_POKE_BALL": {
+ "label": "Lilycove City - Hidden Item on Beach East",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_LILYCOVE_CITY_PP_UP": {
+ "label": "Lilycove City - Hidden Item on Beach North",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_MT_PYRE_EXTERIOR_MAX_ETHER": {
+ "label": "Mt Pyre Exterior - Hidden Item First Grave",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_MT_PYRE_EXTERIOR_ULTRA_BALL": {
+ "label": "Mt Pyre Exterior - Hidden Item Second Grave",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_MT_PYRE_SUMMIT_RARE_CANDY": {
+ "label": "Mt Pyre Summit - Hidden Item in Grass",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_MT_PYRE_SUMMIT_ZINC": {
+ "label": "Mt Pyre Summit - Hidden Item Grave",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_PETALBURG_CITY_RARE_CANDY": {
+ "label": "Petalburg City - Hidden Item Past Pond South",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_PETALBURG_WOODS_POKE_BALL": {
+ "label": "Petalburg Woods - Hidden Item After Grunt",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_PETALBURG_WOODS_POTION": {
+ "label": "Petalburg Woods - Hidden Item Southeast",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_PETALBURG_WOODS_TINY_MUSHROOM_1": {
+ "label": "Petalburg Woods - Hidden Item Past Tree North",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_PETALBURG_WOODS_TINY_MUSHROOM_2": {
+ "label": "Petalburg Woods - Hidden Item Past Tree South",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_104_ANTIDOTE": {
+ "label": "Route 104 - Hidden Item on Beach 1",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_104_HEART_SCALE": {
+ "label": "Route 104 - Hidden Item on Beach 2",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_104_POTION": {
+ "label": "Route 104 - Hidden Item on Beach 3",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_104_POKE_BALL": {
+ "label": "Route 104 - Hidden Item Behind Flower Shop 1",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_104_SUPER_POTION": {
+ "label": "Route 104 - Hidden Item Behind Flower Shop 2",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_105_BIG_PEARL": {
+ "label": "Route 105 - Hidden Item Between Trainers",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_105_HEART_SCALE": {
+ "label": "Route 105 - Hidden Item on Small Island",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_106_HEART_SCALE": {
+ "label": "Route 106 - Hidden Item on Beach 1",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_106_STARDUST": {
+ "label": "Route 106 - Hidden Item on Beach 2",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_106_POKE_BALL": {
+ "label": "Route 106 - Hidden Item on Beach 3",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_108_RARE_CANDY": {
+ "label": "Route 108 - Hidden Item on Rock",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_109_REVIVE": {
+ "label": "Route 109 - Hidden Item on Beach Southwest",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_109_ETHER": {
+ "label": "Route 109 - Hidden Item on Beach Southeast",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_109_HEART_SCALE_2": {
+ "label": "Route 109 - Hidden Item on Beach Under Umbrella",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_109_GREAT_BALL": {
+ "label": "Route 109 - Hidden Item on Beach West",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_109_HEART_SCALE_1": {
+ "label": "Route 109 - Hidden Item on Beach Behind Old Man",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_109_HEART_SCALE_3": {
+ "label": "Route 109 - Hidden Item in Front of Couple",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_110_FULL_HEAL": {
+ "label": "Route 110 - Hidden Item South of Rival",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_110_GREAT_BALL": {
+ "label": "Route 110 - Hidden Item North of Rival",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_110_REVIVE": {
+ "label": "Route 110 - Hidden Item Behind Two Trainers",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_110_POKE_BALL": {
+ "label": "Route 110 - Hidden Item South of Berries",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_111_PROTEIN": {
+ "label": "Route 111 - Hidden Item Desert Behind Tower",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_111_RARE_CANDY": {
+ "label": "Route 111 - Hidden Item Desert on Rock 1",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_111_STARDUST": {
+ "label": "Route 111 - Hidden Item Desert on Rock 2",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_113_ETHER": {
+ "label": "Route 113 - Hidden Item Mound West of Three Trainers",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_113_NUGGET": {
+ "label": "Route 113 - Hidden Item Mound Between Trainers",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_113_TM_DOUBLE_TEAM": {
+ "label": "Route 113 - Hidden Item Mound West of Workshop",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_114_CARBOS": {
+ "label": "Route 114 - Hidden Item Rock in Grass",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_114_REVIVE": {
+ "label": "Route 114 - Hidden Item West of Bridge",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_115_HEART_SCALE": {
+ "label": "Route 115 - Hidden Item Behind Trainer on Beach",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_116_BLACK_GLASSES": {
+ "label": "Route 116 - Hidden Item in East",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_116_SUPER_POTION": {
+ "label": "Route 116 - Hidden Item in Tree Maze",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_117_REPEL": {
+ "label": "Route 117 - Hidden Item Behind Flower Patch",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_118_HEART_SCALE": {
+ "label": "Route 118 - Hidden Item West on Rock",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_118_IRON": {
+ "label": "Route 118 - Hidden Item East on Rock",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_119_FULL_HEAL": {
+ "label": "Route 119 - Hidden Item in South Tall Grass",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_119_CALCIUM": {
+ "label": "Route 119 - Hidden Item Across South Rail",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_119_ULTRA_BALL": {
+ "label": "Route 119 - Hidden Item in East Tall Grass",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_119_MAX_ETHER": {
+ "label": "Route 119 - Hidden Item Next to Waterfall",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_120_RARE_CANDY_1": {
+ "label": "Route 120 - Hidden Item Behind Trees",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_120_REVIVE": {
+ "label": "Route 120 - Hidden Item in North Tall Grass",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_120_ZINC": {
+ "label": "Route 120 - Hidden Item in Tall Grass Maze",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2": {
+ "label": "Route 120 - Hidden Item Behind Southwest Pool",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_121_HP_UP": {
+ "label": "Route 121 - Hidden Item West of Grunts",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_121_FULL_HEAL": {
+ "label": "Route 121 - Hidden Item in Maze 1",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_121_MAX_REVIVE": {
+ "label": "Route 121 - Hidden Item in Maze 2",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_121_NUGGET": {
+ "label": "Route 121 - Hidden Item Behind Tree",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_123_PP_UP": {
+ "label": "Route 123 - Hidden Item East Behind Tree 1",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_123_RARE_CANDY": {
+ "label": "Route 123 - Hidden Item East Behind Tree 2",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_123_HYPER_POTION": {
+ "label": "Route 123 - Hidden Item on Rock Before Ledges",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_123_SUPER_REPEL": {
+ "label": "Route 123 - Hidden Item in North Path Grass",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_123_REVIVE": {
+ "label": "Route 123 - Hidden Item Behind House",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_128_HEART_SCALE_1": {
+ "label": "Route 128 - Hidden Item North Island",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_128_HEART_SCALE_2": {
+ "label": "Route 128 - Hidden Item Center Island",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_ROUTE_128_HEART_SCALE_3": {
+ "label": "Route 128 - Hidden Item Southwest Island",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_SAFARI_ZONE_NORTH_EAST_ZINC": {
+ "label": "Safari Zone NE - Hidden Item North",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_SAFARI_ZONE_NORTH_EAST_RARE_CANDY": {
+ "label": "Safari Zone NE - Hidden Item East",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_SAFARI_ZONE_SOUTH_EAST_FULL_RESTORE": {
+ "label": "Safari Zone SE - Hidden Item in South Grass 1",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_SAFARI_ZONE_SOUTH_EAST_PP_UP": {
+ "label": "Safari Zone SE - Hidden Item in South Grass 2",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_SS_TIDAL_LOWER_DECK_LEFTOVERS": {
+ "label": "SS Tidal - Hidden Item in Lower Deck Trash Can",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_124_GREEN_SHARD": {
+ "label": "Route 124 UW - Hidden Item in Big Area",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_124_CARBOS": {
+ "label": "Route 124 UW - Hidden Item in Tunnel Alcove",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_124_CALCIUM": {
+ "label": "Route 124 UW - Hidden Item in North Tunnel 1",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_124_HEART_SCALE_2": {
+ "label": "Route 124 UW - Hidden Item in North Tunnel 2",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_124_PEARL": {
+ "label": "Route 124 UW - Hidden Item in Small Area North",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_124_BIG_PEARL": {
+ "label": "Route 124 UW - Hidden Item in Small Area Middle",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_124_HEART_SCALE_1": {
+ "label": "Route 124 UW - Hidden Item in Small Area South",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_126_STARDUST": {
+ "label": "Route 126 UW - Hidden Item Northeast",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_126_ULTRA_BALL": {
+ "label": "Route 126 UW - Hidden Item in North Alcove",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_126_BIG_PEARL": {
+ "label": "Route 126 UW - Hidden Item in Southeast",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_126_HEART_SCALE": {
+ "label": "Route 126 UW - Hidden Item in Northwest Alcove",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_126_BLUE_SHARD": {
+ "label": "Route 126 UW - Hidden Item in Southwest Area",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_126_IRON": {
+ "label": "Route 126 UW - Hidden Item in West Area 1",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_126_PEARL": {
+ "label": "Route 126 UW - Hidden Item in West Area 2",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_126_YELLOW_SHARD": {
+ "label": "Route 126 UW - Hidden Item in West Area 3",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_127_STAR_PIECE": {
+ "label": "Route 127 UW - Hidden Item in West Area",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_127_HEART_SCALE": {
+ "label": "Route 127 UW - Hidden Item in Center Area",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_127_HP_UP": {
+ "label": "Route 127 UW - Hidden Item in East Area",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_127_RED_SHARD": {
+ "label": "Route 127 UW - Hidden Item in Northeast Area",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_128_PEARL": {
+ "label": "Route 128 UW - Hidden Item in East Area",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_UNDERWATER_128_PROTEIN": {
+ "label": "Route 128 UW - Hidden Item in Small Area",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_VICTORY_ROAD_1F_ULTRA_BALL": {
+ "label": "Victory Road 1F - Hidden Item on Southeast Ledge",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_VICTORY_ROAD_B2F_ELIXIR": {
+ "label": "Victory Road B2F - Hidden Item Above Waterfall",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_VICTORY_ROAD_B2F_MAX_REPEL": {
+ "label": "Victory Road B2F - Hidden Item in Northeast Corner",
+ "tags": ["HiddenItem"]
+ },
+ "HIDDEN_ITEM_NAVEL_ROCK_TOP_SACRED_ASH": {
+ "label": "Navel Rock Top - Hidden Item Sacred Ash",
+ "tags": ["HiddenItem"]
+ },
+
+ "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_1_TM_RAIN_DANCE": {
+ "label": "Abandoned Ship HF - Item in Room 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_3_WATER_STONE": {
+ "label": "Abandoned Ship HF - Item in Room 3",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_6_LUXURY_BALL": {
+ "label": "Abandoned Ship HF - Item in Room 6",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ABANDONED_SHIP_ROOMS_1F_HARBOR_MAIL": {
+ "label": "Abandoned Ship 1F - Item in East Side Northwest Room",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ABANDONED_SHIP_ROOMS_2_1F_REVIVE": {
+ "label": "Abandoned Ship 1F - Item in West Side North Room",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ABANDONED_SHIP_ROOMS_B1F_ESCAPE_ROPE": {
+ "label": "Abandoned Ship B1F - Item in South Rooms",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ABANDONED_SHIP_ROOMS_B1F_TM_ICE_BEAM": {
+ "label": "Abandoned Ship B1F - Item in Storage Room",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ABANDONED_SHIP_ROOMS_2_B1F_DIVE_BALL": {
+ "label": "Abandoned Ship B1F - Item in North Rooms",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_AQUA_HIDEOUT_B1F_MASTER_BALL": {
+ "label": "Aqua Hideout B1F - Item in Center Room 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_AQUA_HIDEOUT_B1F_NUGGET": {
+ "label": "Aqua Hideout B1F - Item in Center Room 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_AQUA_HIDEOUT_B1F_MAX_ELIXIR": {
+ "label": "Aqua Hideout B1F - Item in East Room",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_AQUA_HIDEOUT_B2F_NEST_BALL": {
+ "label": "Aqua Hideout B2F - Item in Long Hallway",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ARTISAN_CAVE_1F_CARBOS": {
+ "label": "Artisan Cave 1F - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ARTISAN_CAVE_B1F_HP_UP": {
+ "label": "Artisan Cave B1F - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_FIERY_PATH_FIRE_STONE": {
+ "label": "Fiery Path - Item Behind Boulders 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_FIERY_PATH_TM_TOXIC": {
+ "label": "Fiery Path - Item Behind Boulders 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_GRANITE_CAVE_1F_ESCAPE_ROPE": {
+ "label": "Granite Cave 1F - Item Before Ladder",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_GRANITE_CAVE_B1F_POKE_BALL": {
+ "label": "Granite Cave B1F - Item in Alcove",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_GRANITE_CAVE_B2F_RARE_CANDY": {
+ "label": "Granite Cave B2F - Item After Crumbling Floor",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_GRANITE_CAVE_B2F_REPEL": {
+ "label": "Granite Cave B2F - Item After Mud Slope",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_JAGGED_PASS_BURN_HEAL": {
+ "label": "Jagged Pass - Item Below Hideout",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_LILYCOVE_CITY_MAX_REPEL": {
+ "label": "Lilycove City - Item on Peninsula",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MAGMA_HIDEOUT_1F_RARE_CANDY": {
+ "label": "Magma Hideout 1F - Item on Ledge",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MAGMA_HIDEOUT_2F_2R_FULL_RESTORE": {
+ "label": "Magma Hideout 2F - Item on West Platform",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MAGMA_HIDEOUT_2F_2R_MAX_ELIXIR": {
+ "label": "Magma Hideout 2F - Item on East Platform",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MAGMA_HIDEOUT_3F_1R_NUGGET": {
+ "label": "Magma Hideout 3F - Item Before Last Floor",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MAGMA_HIDEOUT_3F_2R_PP_MAX": {
+ "label": "Magma Hideout 3F - Item in Drill Room",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MAGMA_HIDEOUT_3F_3R_ECAPE_ROPE": {
+ "label": "Magma Hideout 3F - Item After Groudon",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MAGMA_HIDEOUT_4F_MAX_REVIVE": {
+ "label": "Magma Hideout 4F - Item Before Groudon",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MAUVILLE_CITY_X_SPEED": {
+ "label": "Mauville City - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_METEOR_FALLS_1F_1R_FULL_HEAL": {
+ "label": "Meteor Falls 1F - Item Northeast",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_METEOR_FALLS_1F_1R_MOON_STONE": {
+ "label": "Meteor Falls 1F - Item West",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_METEOR_FALLS_1F_1R_PP_UP": {
+ "label": "Meteor Falls 1F - Item Below Waterfall",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_METEOR_FALLS_1F_1R_TM_IRON_TAIL": {
+ "label": "Meteor Falls 1F - Item Before Steven's Cave",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_METEOR_FALLS_B1F_2R_TM_DRAGON_CLAW": {
+ "label": "Meteor Falls B1F - Item in North Cave",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MOSSDEEP_CITY_NET_BALL": {
+ "label": "Mossdeep City - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MT_PYRE_2F_ULTRA_BALL": {
+ "label": "Mt Pyre 2F - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MT_PYRE_3F_SUPER_REPEL": {
+ "label": "Mt Pyre 3F - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MT_PYRE_4F_SEA_INCENSE": {
+ "label": "Mt Pyre 4F - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MT_PYRE_5F_LAX_INCENSE": {
+ "label": "Mt Pyre 5F - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MT_PYRE_6F_TM_SHADOW_BALL": {
+ "label": "Mt Pyre 6F - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MT_PYRE_EXTERIOR_TM_SKILL_SWAP": {
+ "label": "Mt Pyre Exterior - Item 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_MT_PYRE_EXTERIOR_MAX_POTION": {
+ "label": "Mt Pyre Exterior - Item 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_NEW_MAUVILLE_ESCAPE_ROPE": {
+ "label": "New Mauville - Item 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_NEW_MAUVILLE_PARALYZE_HEAL": {
+ "label": "New Mauville - Item 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_NEW_MAUVILLE_FULL_HEAL": {
+ "label": "New Mauville - Item 3",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_NEW_MAUVILLE_THUNDER_STONE": {
+ "label": "New Mauville - Item 4",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_NEW_MAUVILLE_ULTRA_BALL": {
+ "label": "New Mauville - Item 5",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_PETALBURG_CITY_ETHER": {
+ "label": "Petalburg City - Item Past Pond South",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_PETALBURG_CITY_MAX_REVIVE": {
+ "label": "Petalburg City - Item Past Pond North",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_PETALBURG_WOODS_ETHER": {
+ "label": "Petalburg Woods - Item Northwest",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_PETALBURG_WOODS_PARALYZE_HEAL": {
+ "label": "Petalburg Woods - Item Southwest",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_PETALBURG_WOODS_GREAT_BALL": {
+ "label": "Petalburg Woods - Item Past Tree Northeast",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_PETALBURG_WOODS_X_ATTACK": {
+ "label": "Petalburg Woods - Item Past Tree South",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_102_POTION": {
+ "label": "Route 102 - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_103_GUARD_SPEC": {
+ "label": "Route 103 - Item Near Berries",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_103_PP_UP": {
+ "label": "Route 103 - Item in Tree Maze",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_104_POKE_BALL": {
+ "label": "Route 104 - Item Near Briney on Ledge",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_104_POTION": {
+ "label": "Route 104 - Item Behind Flower Shop",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_104_X_ACCURACY": {
+ "label": "Route 104 - Item Behind Tree",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_104_PP_UP": {
+ "label": "Route 104 - Item East Past Pond",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_105_IRON": {
+ "label": "Route 105 - Item on Island",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_106_PROTEIN": {
+ "label": "Route 106 - Item on West Beach",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_108_STAR_PIECE": {
+ "label": "Route 108 - Item Between Trainers",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_109_POTION": {
+ "label": "Route 109 - Item on Beach",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_109_PP_UP": {
+ "label": "Route 109 - Item on Island",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_110_DIRE_HIT": {
+ "label": "Route 110 - Item South of Rival",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_110_ELIXIR": {
+ "label": "Route 110 - Item South of Berries",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_110_RARE_CANDY": {
+ "label": "Route 110 - Item on Island",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_111_ELIXIR": {
+ "label": "Route 111 - Item Near Winstrates",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_111_HP_UP": {
+ "label": "Route 111 - Item West of Pond Near Winstrates",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_111_STARDUST": {
+ "label": "Route 111 - Item Desert Near Tower",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_111_TM_SANDSTORM": {
+ "label": "Route 111 - Item Desert South",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_112_NUGGET": {
+ "label": "Route 112 - Item on Ledges",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_113_SUPER_REPEL": {
+ "label": "Route 113 - Item Past Three Trainers",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_113_MAX_ETHER": {
+ "label": "Route 113 - Item on Ledge",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_113_HYPER_POTION": {
+ "label": "Route 113 - Item Near Fallarbor South",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_114_ENERGY_POWDER": {
+ "label": "Route 114 - Item Between Trainers",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_114_PROTEIN": {
+ "label": "Route 114 - Item Behind Smashable Rock",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_114_RARE_CANDY": {
+ "label": "Route 114 - Item Above Waterfall",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_115_SUPER_POTION": {
+ "label": "Route 115 - Item on Beach",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_115_PP_UP": {
+ "label": "Route 115 - Item on Ledge",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_115_GREAT_BALL": {
+ "label": "Route 115 - Item Behind Smashable Rock",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_115_HEAL_POWDER": {
+ "label": "Route 115 - Item North Near Trainers",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_115_TM_FOCUS_PUNCH": {
+ "label": "Route 115 - Item Near Mud Slope",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_115_IRON": {
+ "label": "Route 115 - Item Past Mud Slope",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_116_REPEL": {
+ "label": "Route 116 - Item in Grass",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_116_X_SPECIAL": {
+ "label": "Route 116 - Item Near Tunnel",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_116_POTION": {
+ "label": "Route 116 - Item in Tree Maze 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_116_ETHER": {
+ "label": "Route 116 - Item in Tree Maze 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_116_HP_UP": {
+ "label": "Route 116 - Item in East",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_117_GREAT_BALL": {
+ "label": "Route 117 - Item Behind Flower Patch",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_117_REVIVE": {
+ "label": "Route 117 - Item Behind Tree",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_118_HYPER_POTION": {
+ "label": "Route 118 - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_119_SUPER_REPEL": {
+ "label": "Route 119 - Item in South Tall Grass 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_119_HYPER_POTION_1": {
+ "label": "Route 119 - Item in South Tall Grass 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_119_ZINC": {
+ "label": "Route 119 - Item Across River South",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_119_HYPER_POTION_2": {
+ "label": "Route 119 - Item Near Mud Slope",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_119_ELIXIR_1": {
+ "label": "Route 119 - Item East of Mud Slope",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_119_ELIXIR_2": {
+ "label": "Route 119 - Item on River Bank",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_119_LEAF_STONE": {
+ "label": "Route 119 - Item Near South Waterfall",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_119_NUGGET": {
+ "label": "Route 119 - Item Above North Waterfall 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_119_RARE_CANDY": {
+ "label": "Route 119 - Item Above North Waterfall 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_120_NEST_BALL": {
+ "label": "Route 120 - Item Near North Pond",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_120_REVIVE": {
+ "label": "Route 120 - Item in North Puddles",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_120_NUGGET": {
+ "label": "Route 120 - Item in Tall Grass Maze",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_120_HYPER_POTION": {
+ "label": "Route 120 - Item in Tall Grass South",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_120_FULL_HEAL": {
+ "label": "Route 120 - Item Behind Southwest Pool",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_121_ZINC": {
+ "label": "Route 121 - Item Near Safari Zone",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_121_REVIVE": {
+ "label": "Route 121 - Item in Maze 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_121_CARBOS": {
+ "label": "Route 121 - Item in Maze 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_123_ULTRA_BALL": {
+ "label": "Route 123 - Item Below Ledges",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_123_ELIXIR": {
+ "label": "Route 123 - Item on Ledges 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_123_REVIVAL_HERB": {
+ "label": "Route 123 - Item on Ledges 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_123_PP_UP": {
+ "label": "Route 123 - Item on Ledges 3",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_123_CALCIUM": {
+ "label": "Route 123 - Item on Ledges 4",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_124_RED_SHARD": {
+ "label": "Route 124 - Item in Northwest Area",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_124_YELLOW_SHARD": {
+ "label": "Route 124 - Item in Northeast Area",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_124_BLUE_SHARD": {
+ "label": "Route 124 - Item in Southwest Area",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_125_BIG_PEARL": {
+ "label": "Route 125 - Item Between Trainers",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_126_GREEN_SHARD": {
+ "label": "Route 126 - Item in Separated Area",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_127_ZINC": {
+ "label": "Route 127 - Item North",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_127_CARBOS": {
+ "label": "Route 127 - Item East",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_127_RARE_CANDY": {
+ "label": "Route 127 - Item Between Trainers",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_132_PROTEIN": {
+ "label": "Route 132 - Item 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_132_RARE_CANDY": {
+ "label": "Route 132 - Item 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_133_BIG_PEARL": {
+ "label": "Route 133 - Item 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_133_MAX_REVIVE": {
+ "label": "Route 133 - Item 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_133_STAR_PIECE": {
+ "label": "Route 133 - Item 3",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_134_CARBOS": {
+ "label": "Route 134 - Item 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_ROUTE_134_STAR_PIECE": {
+ "label": "Route 134 - Item 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_RUSTBORO_CITY_X_DEFEND": {
+ "label": "Rustboro City - Item Behind Fences",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_RUSTURF_TUNNEL_POKE_BALL": {
+ "label": "Rusturf Tunnel - Item West",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_RUSTURF_TUNNEL_MAX_ETHER": {
+ "label": "Rusturf Tunnel - Item East",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_SAFARI_ZONE_NORTH_CALCIUM": {
+ "label": "Safari Zone N - Item in Grass",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_SAFARI_ZONE_NORTH_EAST_NUGGET": {
+ "label": "Safari Zone NE - Item on Ledge",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_SAFARI_ZONE_NORTH_WEST_TM_SOLAR_BEAM": {
+ "label": "Safari Zone NW - Item Behind Pond",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_SAFARI_ZONE_SOUTH_EAST_BIG_PEARL": {
+ "label": "Safari Zone SE - Item in Grass",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_SAFARI_ZONE_SOUTH_WEST_MAX_REVIVE": {
+ "label": "Safari Zone SW - Item Behind Pond",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_SCORCHED_SLAB_TM_SUNNY_DAY": {
+ "label": "Scorched Slab - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_SEAFLOOR_CAVERN_ROOM_9_TM_EARTHQUAKE": {
+ "label": "Seafloor Cavern Room 9 - Item Before Kyogre",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_SHOAL_CAVE_ENTRANCE_BIG_PEARL": {
+ "label": "Shoal Cave Entrance - Item on Ledge",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_SHOAL_CAVE_ICE_ROOM_NEVER_MELT_ICE": {
+ "label": "Shoal Cave Ice Room - Item 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_SHOAL_CAVE_ICE_ROOM_TM_HAIL": {
+ "label": "Shoal Cave Ice Room - Item 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_SHOAL_CAVE_INNER_ROOM_RARE_CANDY": {
+ "label": "Shoal Cave Inner Room - Item in Center",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_SHOAL_CAVE_STAIRS_ROOM_ICE_HEAL": {
+ "label": "Shoal Cave Stairs Room - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_TRICK_HOUSE_PUZZLE_1_ORANGE_MAIL": {
+ "label": "Trick House Puzzle 1 - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_TRICK_HOUSE_PUZZLE_2_HARBOR_MAIL": {
+ "label": "Trick House Puzzle 2 - Item 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_TRICK_HOUSE_PUZZLE_2_WAVE_MAIL": {
+ "label": "Trick House Puzzle 2 - Item 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_TRICK_HOUSE_PUZZLE_3_SHADOW_MAIL": {
+ "label": "Trick House Puzzle 3 - Item 1",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_TRICK_HOUSE_PUZZLE_3_WOOD_MAIL": {
+ "label": "Trick House Puzzle 3 - Item 2",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_TRICK_HOUSE_PUZZLE_4_MECH_MAIL": {
+ "label": "Trick House Puzzle 4 - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_TRICK_HOUSE_PUZZLE_6_GLITTER_MAIL": {
+ "label": "Trick House Puzzle 6 - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_TRICK_HOUSE_PUZZLE_7_TROPIC_MAIL": {
+ "label": "Trick House Puzzle 7 - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_TRICK_HOUSE_PUZZLE_8_BEAD_MAIL": {
+ "label": "Trick House Puzzle 8 - Item",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_VICTORY_ROAD_1F_MAX_ELIXIR": {
+ "label": "Victory Road 1F - Item East",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_VICTORY_ROAD_1F_PP_UP": {
+ "label": "Victory Road 1F - Item on Southeast Ledge",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_VICTORY_ROAD_B1F_FULL_RESTORE": {
+ "label": "Victory Road B1F - Item Behind Boulders",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_VICTORY_ROAD_B1F_TM_PSYCHIC": {
+ "label": "Victory Road B1F - Item on Northeast Ledge",
+ "tags": ["OverworldItem"]
+ },
+ "ITEM_VICTORY_ROAD_B2F_FULL_HEAL": {
+ "label": "Victory Road B2F - Item Above Waterfall",
+ "tags": ["OverworldItem"]
+ },
+
+ "NPC_GIFT_GOT_TM_THUNDERBOLT_FROM_WATTSON": {
+ "label": "Mauville City - TM24 from Wattson",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_6_SODA_POP": {
+ "label": "Route 109 - Seashore House Reward",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_AMULET_COIN": {
+ "label": "Littleroot Town - Amulet Coin from Mom",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_CHARCOAL": {
+ "label": "Lavaridge Town Herb Shop - Charcoal from Man",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_CHESTO_BERRY_ROUTE_104": {
+ "label": "Route 104 - Gift from Woman Near Berries",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_CLEANSE_TAG": {
+ "label": "Mt Pyre 1F - Cleanse Tag from Woman in NE Corner",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_EXP_SHARE": {
+ "label": "Devon Corp 3F - Exp. Share from Mr. Stone",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_FOCUS_BAND": {
+ "label": "Shoal Cave Lower Room - Focus Band from Black Belt",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_GREAT_BALL_PETALBURG_WOODS": {
+ "label": "Petalburg Woods - Gift from Devon Employee",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_GREAT_BALL_RUSTBORO_CITY": {
+ "label": "Rustboro City - Gift from Devon Employee",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_KINGS_ROCK": {
+ "label": "Mossdeep City - King's Rock from Boy",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_MACHO_BRACE": {
+ "label": "Route 111 - Winstrate Family Reward",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_MENTAL_HERB": {
+ "label": "Fortree City - Wingull Delivery Reward",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_MIRACLE_SEED": {
+ "label": "Petalburg Woods - Miracle Seed from Lady",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_POTION_OLDALE": {
+ "label": "Oldale Town - Gift from Shop Tutorial",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_POWDER_JAR": {
+ "label": "Slateport City - Powder Jar from Lady in Market",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_PREMIER_BALL_RUSTBORO": {
+ "label": "Rustboro City - Gift from Boy in Apartments",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_QUICK_CLAW": {
+ "label": "Rustboro City - Quick Claw from School Teacher",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_REPEAT_BALL": {
+ "label": "Route 116 - Gift from Devon Researcher",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_SECRET_POWER": {
+ "label": "Route 111 - Secret Power from Man Near Tree",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_SILK_SCARF": {
+ "label": "Dewford Town - Silk Scarf from Man in House",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_SOFT_SAND": {
+ "label": "Route 109 - Soft Sand from Tuber",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_SOOT_SACK": {
+ "label": "Route 113 - Soot Sack from Glass Blower",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_SOOTHE_BELL": {
+ "label": "Slateport City - Soothe Bell from Woman in Fan Club",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_SUN_STONE_MOSSDEEP": {
+ "label": "Space Center - Gift from Man",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_WATER_PULSE": {
+ "label": "Sootopolis Gym - TM03 from Juan",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_CALM_MIND": {
+ "label": "Mossdeep Gym - TM04 from Tate and Liza",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_ROAR": {
+ "label": "Route 114 - TM05 from Roaring Man",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_BULK_UP": {
+ "label": "Dewford Gym - TM08 from Brawly",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_BULLET_SEED": {
+ "label": "Route 104 - TM09 from Boy",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_HIDDEN_POWER": {
+ "label": "Fortree City - TM10 from Hidden Power Lady",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_GIGA_DRAIN": {
+ "label": "Route 123 - TM19 from Girl near Berries",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_FRUSTRATION": {
+ "label": "Pacifidlog Town - TM21 from Man in House",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_RETURN": {
+ "label": "Fallarbor Town - TM27 from Cozmo",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_RETURN_2": {
+ "label": "Pacifidlog Town - TM27 from Man in House",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_DIG": {
+ "label": "Route 114 - TM28 from Fossil Maniac's Brother",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_BRICK_BREAK": {
+ "label": "Sootopolis City - TM31 from Black Belt in House",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_SHOCK_WAVE": {
+ "label": "Mauville Gym - TM34 from Wattson",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_SLUDGE_BOMB": {
+ "label": "Dewford Town - TM36 from Sludge Bomb Man",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_ROCK_TOMB": {
+ "label": "Rustboro Gym - TM39 from Roxanne",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_AERIAL_ACE": {
+ "label": "Fortree Gym - TM40 from Winona",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_TORMENT": {
+ "label": "Slateport City - TM41 from Sailor in Battle Tent",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_FACADE": {
+ "label": "Petalburg Gym - TM42 from Norman",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_REST": {
+ "label": "Lilycove City - TM44 from Man in House",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_ATTRACT": {
+ "label": "Verdanturf Town - TM45 from Woman in Battle Tent",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_THIEF": {
+ "label": "Oceanic Museum - TM46 from Aqua Grunt in Museum",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_STEEL_WING": {
+ "label": "Granite Cave 1F - TM47 from Steven",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_SNATCH": {
+ "label": "SS Tidal - TM49 from Thief",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TM_OVERHEAT": {
+ "label": "Lavaridge Gym - TM50 from Flannery",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_WHITE_HERB": {
+ "label": "Route 104 - White Herb from Lady Near Flower Shop",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_FLOWER_SHOP_RECEIVED_BERRY": {
+ "label": "Route 104 - Berry from Girl in Flower Shop",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_DEEP_SEA_SCALE": {
+ "label": "Slateport City - Deep Sea Scale from Capt. Stern",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_DEEP_SEA_TOOTH": {
+ "label": "Slateport City - Deep Sea Tooth from Capt. Stern",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_1": {
+ "label": "Trick House Puzzle 1 - Reward",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_2": {
+ "label": "Trick House Puzzle 2 - Reward",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_3": {
+ "label": "Trick House Puzzle 3 - Reward",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_4": {
+ "label": "Trick House Puzzle 4 - Reward",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_5": {
+ "label": "Trick House Puzzle 5 - Reward",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_6": {
+ "label": "Trick House Puzzle 6 - Reward",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_7": {
+ "label": "Trick House Puzzle 7 - Reward",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_RECEIVED_FIRST_POKEBALLS": {
+ "label": "Littleroot Town - Pokeballs from Rival",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_SOOTOPOLIS_RECEIVED_BERRY_1": {
+ "label": "Sootopolis City - Berry from Girl on Grass 1",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_SOOTOPOLIS_RECEIVED_BERRY_2": {
+ "label": "Sootopolis City - Berry from Girl on Grass 2",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_ROUTE_111_RECEIVED_BERRY": {
+ "label": "Route 111 - Berry from Girl Near Berry Trees",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_ROUTE_114_RECEIVED_BERRY": {
+ "label": "Route 114 - Berry from Man Near House",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_ROUTE_120_RECEIVED_BERRY": {
+ "label": "Route 120 - Berry from Lady Near Berry Trees",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_BERRY_MASTER_RECEIVED_BERRY_1": {
+ "label": "Route 123 - Berry from Berry Master 1",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_BERRY_MASTER_RECEIVED_BERRY_2": {
+ "label": "Route 123 - Berry from Berry Master 2",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_BERRY_MASTERS_WIFE": {
+ "label": "Route 123 - Berry from Berry Master's Wife",
+ "tags": ["NpcGift"]
+ },
+ "NPC_GIFT_LILYCOVE_RECEIVED_BERRY": {
+ "label": "Lilycove City - Berry from Gentleman Above Ledges",
+ "tags": ["NpcGift"]
+ },
+
+ "BERRY_TREE_01": {
+ "label": "Route 102 - Berry Tree 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_02": {
+ "label": "Route 102 - Berry Tree 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_03": {
+ "label": "Route 104 - Berry Tree Flower Shop 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_04": {
+ "label": "Route 104 - Berry Tree Flower Shop 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_05": {
+ "label": "Route 103 - Berry Tree 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_06": {
+ "label": "Route 103 - Berry Tree 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_07": {
+ "label": "Route 103 - Berry Tree 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_08": {
+ "label": "Route 104 - Berry Tree North 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_09": {
+ "label": "Route 104 - Berry Tree North 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_10": {
+ "label": "Route 104 - Berry Tree North 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_11": {
+ "label": "Route 104 - Berry Tree South 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_12": {
+ "label": "Route 104 - Berry Tree South 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_13": {
+ "label": "Route 104 - Berry Tree South 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_14": {
+ "label": "Route 123 - Berry Tree Berry Master 6",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_15": {
+ "label": "Route 123 - Berry Tree Berry Master 7",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_16": {
+ "label": "Route 110 - Berry Tree 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_17": {
+ "label": "Route 110 - Berry Tree 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_18": {
+ "label": "Route 110 - Berry Tree 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_19": {
+ "label": "Route 111 - Berry Tree 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_20": {
+ "label": "Route 111 - Berry Tree 4",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_21": {
+ "label": "Route 112 - Berry Tree 4",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_22": {
+ "label": "Route 112 - Berry Tree 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_23": {
+ "label": "Route 112 - Berry Tree 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_24": {
+ "label": "Route 112 - Berry Tree 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_25": {
+ "label": "Route 116 - Berry Tree 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_26": {
+ "label": "Route 116 - Berry Tree 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_27": {
+ "label": "Route 117 - Berry Tree 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_28": {
+ "label": "Route 117 - Berry Tree 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_29": {
+ "label": "Route 117 - Berry Tree 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_30": {
+ "label": "Route 123 - Berry Tree Berry Master 8",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_31": {
+ "label": "Route 118 - Berry Tree 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_32": {
+ "label": "Route 118 - Berry Tree 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_33": {
+ "label": "Route 118 - Berry Tree 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_34": {
+ "label": "Route 119 - Berry Tree North 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_35": {
+ "label": "Route 119 - Berry Tree North 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_36": {
+ "label": "Route 119 - Berry Tree North 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_37": {
+ "label": "Route 120 - Berry Tree in Side Area 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_38": {
+ "label": "Route 120 - Berry Tree in Side Area 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_39": {
+ "label": "Route 120 - Berry Tree in Side Area 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_40": {
+ "label": "Route 120 - Berry Tree South 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_41": {
+ "label": "Route 120 - Berry Tree South 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_42": {
+ "label": "Route 120 - Berry Tree South 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_43": {
+ "label": "Route 120 - Berry Tree Pond 4",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_44": {
+ "label": "Route 120 - Berry Tree Pond 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_45": {
+ "label": "Route 120 - Berry Tree Pond 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_46": {
+ "label": "Route 120 - Berry Tree Pond 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_47": {
+ "label": "Route 121 - Berry Tree West 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_48": {
+ "label": "Route 121 - Berry Tree West 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_49": {
+ "label": "Route 121 - Berry Tree West 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_50": {
+ "label": "Route 121 - Berry Tree West 4",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_51": {
+ "label": "Route 121 - Berry Tree East 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_52": {
+ "label": "Route 121 - Berry Tree East 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_53": {
+ "label": "Route 121 - Berry Tree East 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_54": {
+ "label": "Route 121 - Berry Tree East 4",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_55": {
+ "label": "Route 115 - Berry Tree Behind Smashable Rock 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_56": {
+ "label": "Route 115 - Berry Tree Behind Smashable Rock 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_57": {
+ "label": "Route 123 - Berry Tree East 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_58": {
+ "label": "Route 123 - Berry Tree Berry Master 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_59": {
+ "label": "Route 123 - Berry Tree Berry Master 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_60": {
+ "label": "Route 123 - Berry Tree Berry Master 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_61": {
+ "label": "Route 123 - Berry Tree Berry Master 4",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_62": {
+ "label": "Route 123 - Berry Tree East 4",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_63": {
+ "label": "Route 123 - Berry Tree East 5",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_64": {
+ "label": "Route 123 - Berry Tree East 6",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_65": {
+ "label": "Route 123 - Berry Tree Berry Master 9",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_66": {
+ "label": "Route 116 - Berry Tree 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_67": {
+ "label": "Route 116 - Berry Tree 4",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_68": {
+ "label": "Route 114 - Berry Tree 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_69": {
+ "label": "Route 115 - Berry Tree North 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_70": {
+ "label": "Route 115 - Berry Tree North 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_71": {
+ "label": "Route 115 - Berry Tree North 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_72": {
+ "label": "Route 123 - Berry Tree Berry Master 10",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_73": {
+ "label": "Route 123 - Berry Tree Berry Master 11",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_74": {
+ "label": "Route 123 - Berry Tree Berry Master 12",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_75": {
+ "label": "Route 104 - Berry Tree Flower Shop 3",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_76": {
+ "label": "Route 104 - Berry Tree Flower Shop 4",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_77": {
+ "label": "Route 114 - Berry Tree 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_78": {
+ "label": "Route 114 - Berry Tree 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_79": {
+ "label": "Route 123 - Berry Tree Berry Master 5",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_80": {
+ "label": "Route 111 - Berry Tree 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_81": {
+ "label": "Route 111 - Berry Tree 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_82": {
+ "label": "Route 130 - Berry Tree on Mirage Island",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_83": {
+ "label": "Route 119 - Berry Tree Above Waterfall 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_84": {
+ "label": "Route 119 - Berry Tree Above Waterfall 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_85": {
+ "label": "Route 119 - Berry Tree South 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_86": {
+ "label": "Route 119 - Berry Tree South 2",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_87": {
+ "label": "Route 123 - Berry Tree East 1",
+ "tags": ["BerryTree"]
+ },
+ "BERRY_TREE_88": {
+ "label": "Route 123 - Berry Tree East 2",
+ "tags": ["BerryTree"]
+ },
+
+ "POKEDEX_REWARD_001": {
+ "label": "Pokedex - Bulbasaur",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_002": {
+ "label": "Pokedex - Ivysaur",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_003": {
+ "label": "Pokedex - Venusaur",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_004": {
+ "label": "Pokedex - Charmander",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_005": {
+ "label": "Pokedex - Charmeleon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_006": {
+ "label": "Pokedex - Charizard",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_007": {
+ "label": "Pokedex - Squirtle",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_008": {
+ "label": "Pokedex - Wartortle",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_009": {
+ "label": "Pokedex - Blastoise",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_010": {
+ "label": "Pokedex - Caterpie",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_011": {
+ "label": "Pokedex - Metapod",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_012": {
+ "label": "Pokedex - Butterfree",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_013": {
+ "label": "Pokedex - Weedle",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_014": {
+ "label": "Pokedex - Kakuna",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_015": {
+ "label": "Pokedex - Beedrill",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_016": {
+ "label": "Pokedex - Pidgey",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_017": {
+ "label": "Pokedex - Pidgeotto",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_018": {
+ "label": "Pokedex - Pidgeot",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_019": {
+ "label": "Pokedex - Rattata",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_020": {
+ "label": "Pokedex - Raticate",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_021": {
+ "label": "Pokedex - Spearow",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_022": {
+ "label": "Pokedex - Fearow",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_023": {
+ "label": "Pokedex - Ekans",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_024": {
+ "label": "Pokedex - Arbok",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_025": {
+ "label": "Pokedex - Pikachu",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_026": {
+ "label": "Pokedex - Raichu",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_027": {
+ "label": "Pokedex - Sandshrew",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_028": {
+ "label": "Pokedex - Sandslash",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_029": {
+ "label": "Pokedex - Nidoran Female",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_030": {
+ "label": "Pokedex - Nidorina",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_031": {
+ "label": "Pokedex - Nidoqueen",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_032": {
+ "label": "Pokedex - Nidoran Male",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_033": {
+ "label": "Pokedex - Nidorino",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_034": {
+ "label": "Pokedex - Nidoking",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_035": {
+ "label": "Pokedex - Clefairy",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_036": {
+ "label": "Pokedex - Clefable",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_037": {
+ "label": "Pokedex - Vulpix",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_038": {
+ "label": "Pokedex - Ninetales",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_039": {
+ "label": "Pokedex - Jigglypuff",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_040": {
+ "label": "Pokedex - Wigglytuff",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_041": {
+ "label": "Pokedex - Zubat",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_042": {
+ "label": "Pokedex - Golbat",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_043": {
+ "label": "Pokedex - Oddish",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_044": {
+ "label": "Pokedex - Gloom",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_045": {
+ "label": "Pokedex - Vileplume",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_046": {
+ "label": "Pokedex - Paras",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_047": {
+ "label": "Pokedex - Parasect",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_048": {
+ "label": "Pokedex - Venonat",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_049": {
+ "label": "Pokedex - Venomoth",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_050": {
+ "label": "Pokedex - Diglett",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_051": {
+ "label": "Pokedex - Dugtrio",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_052": {
+ "label": "Pokedex - Meowth",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_053": {
+ "label": "Pokedex - Persian",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_054": {
+ "label": "Pokedex - Psyduck",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_055": {
+ "label": "Pokedex - Golduck",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_056": {
+ "label": "Pokedex - Mankey",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_057": {
+ "label": "Pokedex - Primeape",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_058": {
+ "label": "Pokedex - Growlithe",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_059": {
+ "label": "Pokedex - Arcanine",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_060": {
+ "label": "Pokedex - Poliwag",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_061": {
+ "label": "Pokedex - Poliwhirl",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_062": {
+ "label": "Pokedex - Poliwrath",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_063": {
+ "label": "Pokedex - Abra",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_064": {
+ "label": "Pokedex - Kadabra",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_065": {
+ "label": "Pokedex - Alakazam",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_066": {
+ "label": "Pokedex - Machop",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_067": {
+ "label": "Pokedex - Machoke",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_068": {
+ "label": "Pokedex - Machamp",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_069": {
+ "label": "Pokedex - Bellsprout",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_070": {
+ "label": "Pokedex - Weepinbell",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_071": {
+ "label": "Pokedex - Victreebel",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_072": {
+ "label": "Pokedex - Tentacool",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_073": {
+ "label": "Pokedex - Tentacruel",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_074": {
+ "label": "Pokedex - Geodude",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_075": {
+ "label": "Pokedex - Graveler",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_076": {
+ "label": "Pokedex - Golem",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_077": {
+ "label": "Pokedex - Ponyta",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_078": {
+ "label": "Pokedex - Rapidash",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_079": {
+ "label": "Pokedex - Slowpoke",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_080": {
+ "label": "Pokedex - Slowbro",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_081": {
+ "label": "Pokedex - Magnemite",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_082": {
+ "label": "Pokedex - Magneton",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_083": {
+ "label": "Pokedex - Farfetch'd",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_084": {
+ "label": "Pokedex - Doduo",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_085": {
+ "label": "Pokedex - Dodrio",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_086": {
+ "label": "Pokedex - Seel",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_087": {
+ "label": "Pokedex - Dewgong",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_088": {
+ "label": "Pokedex - Grimer",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_089": {
+ "label": "Pokedex - Muk",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_090": {
+ "label": "Pokedex - Shellder",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_091": {
+ "label": "Pokedex - Cloyster",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_092": {
+ "label": "Pokedex - Gastly",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_093": {
+ "label": "Pokedex - Haunter",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_094": {
+ "label": "Pokedex - Gengar",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_095": {
+ "label": "Pokedex - Onix",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_096": {
+ "label": "Pokedex - Drowzee",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_097": {
+ "label": "Pokedex - Hypno",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_098": {
+ "label": "Pokedex - Krabby",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_099": {
+ "label": "Pokedex - Kingler",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_100": {
+ "label": "Pokedex - Voltorb",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_101": {
+ "label": "Pokedex - Electrode",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_102": {
+ "label": "Pokedex - Exeggcute",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_103": {
+ "label": "Pokedex - Exeggutor",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_104": {
+ "label": "Pokedex - Cubone",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_105": {
+ "label": "Pokedex - Marowak",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_106": {
+ "label": "Pokedex - Hitmonlee",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_107": {
+ "label": "Pokedex - Hitmonchan",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_108": {
+ "label": "Pokedex - Lickitung",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_109": {
+ "label": "Pokedex - Koffing",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_110": {
+ "label": "Pokedex - Weezing",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_111": {
+ "label": "Pokedex - Rhyhorn",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_112": {
+ "label": "Pokedex - Rhydon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_113": {
+ "label": "Pokedex - Chansey",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_114": {
+ "label": "Pokedex - Tangela",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_115": {
+ "label": "Pokedex - Kangaskhan",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_116": {
+ "label": "Pokedex - Horsea",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_117": {
+ "label": "Pokedex - Seadra",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_118": {
+ "label": "Pokedex - Goldeen",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_119": {
+ "label": "Pokedex - Seaking",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_120": {
+ "label": "Pokedex - Staryu",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_121": {
+ "label": "Pokedex - Starmie",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_122": {
+ "label": "Pokedex - Mr. Mime",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_123": {
+ "label": "Pokedex - Scyther",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_124": {
+ "label": "Pokedex - Jynx",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_125": {
+ "label": "Pokedex - Electabuzz",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_126": {
+ "label": "Pokedex - Magmar",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_127": {
+ "label": "Pokedex - Pinsir",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_128": {
+ "label": "Pokedex - Tauros",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_129": {
+ "label": "Pokedex - Magikarp",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_130": {
+ "label": "Pokedex - Gyarados",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_131": {
+ "label": "Pokedex - Lapras",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_132": {
+ "label": "Pokedex - Ditto",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_133": {
+ "label": "Pokedex - Eevee",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_134": {
+ "label": "Pokedex - Vaporeon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_135": {
+ "label": "Pokedex - Jolteon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_136": {
+ "label": "Pokedex - Flareon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_137": {
+ "label": "Pokedex - Porygon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_138": {
+ "label": "Pokedex - Omanyte",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_139": {
+ "label": "Pokedex - Omastar",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_140": {
+ "label": "Pokedex - Kabuto",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_141": {
+ "label": "Pokedex - Kabutops",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_142": {
+ "label": "Pokedex - Aerodactyl",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_143": {
+ "label": "Pokedex - Snorlax",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_144": {
+ "label": "Pokedex - Articuno",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_145": {
+ "label": "Pokedex - Zapdos",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_146": {
+ "label": "Pokedex - Moltres",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_147": {
+ "label": "Pokedex - Dratini",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_148": {
+ "label": "Pokedex - Dragonair",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_149": {
+ "label": "Pokedex - Dragonite",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_150": {
+ "label": "Pokedex - Mewtwo",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_151": {
+ "label": "Pokedex - Mew",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_152": {
+ "label": "Pokedex - Chikorita",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_153": {
+ "label": "Pokedex - Bayleef",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_154": {
+ "label": "Pokedex - Meganium",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_155": {
+ "label": "Pokedex - Cyndaquil",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_156": {
+ "label": "Pokedex - Quilava",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_157": {
+ "label": "Pokedex - Typhlosion",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_158": {
+ "label": "Pokedex - Totodile",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_159": {
+ "label": "Pokedex - Croconaw",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_160": {
+ "label": "Pokedex - Feraligatr",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_161": {
+ "label": "Pokedex - Sentret",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_162": {
+ "label": "Pokedex - Furret",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_163": {
+ "label": "Pokedex - Hoothoot",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_164": {
+ "label": "Pokedex - Noctowl",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_165": {
+ "label": "Pokedex - Ledyba",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_166": {
+ "label": "Pokedex - Ledian",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_167": {
+ "label": "Pokedex - Spinarak",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_168": {
+ "label": "Pokedex - Ariados",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_169": {
+ "label": "Pokedex - Crobat",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_170": {
+ "label": "Pokedex - Chinchou",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_171": {
+ "label": "Pokedex - Lanturn",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_172": {
+ "label": "Pokedex - Pichu",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_173": {
+ "label": "Pokedex - Cleffa",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_174": {
+ "label": "Pokedex - Igglybuff",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_175": {
+ "label": "Pokedex - Togepi",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_176": {
+ "label": "Pokedex - Togetic",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_177": {
+ "label": "Pokedex - Natu",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_178": {
+ "label": "Pokedex - Xatu",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_179": {
+ "label": "Pokedex - Mareep",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_180": {
+ "label": "Pokedex - Flaaffy",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_181": {
+ "label": "Pokedex - Ampharos",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_182": {
+ "label": "Pokedex - Bellossom",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_183": {
+ "label": "Pokedex - Marill",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_184": {
+ "label": "Pokedex - Azumarill",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_185": {
+ "label": "Pokedex - Sudowoodo",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_186": {
+ "label": "Pokedex - Politoed",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_187": {
+ "label": "Pokedex - Hoppip",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_188": {
+ "label": "Pokedex - Skiploom",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_189": {
+ "label": "Pokedex - Jumpluff",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_190": {
+ "label": "Pokedex - Aipom",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_191": {
+ "label": "Pokedex - Sunkern",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_192": {
+ "label": "Pokedex - Sunflora",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_193": {
+ "label": "Pokedex - Yanma",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_194": {
+ "label": "Pokedex - Wooper",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_195": {
+ "label": "Pokedex - Quagsire",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_196": {
+ "label": "Pokedex - Espeon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_197": {
+ "label": "Pokedex - Umbreon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_198": {
+ "label": "Pokedex - Murkrow",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_199": {
+ "label": "Pokedex - Slowking",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_200": {
+ "label": "Pokedex - Misdreavus",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_201": {
+ "label": "Pokedex - Unown",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_202": {
+ "label": "Pokedex - Wobbuffet",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_203": {
+ "label": "Pokedex - Girafarig",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_204": {
+ "label": "Pokedex - Pineco",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_205": {
+ "label": "Pokedex - Forretress",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_206": {
+ "label": "Pokedex - Dunsparce",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_207": {
+ "label": "Pokedex - Gligar",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_208": {
+ "label": "Pokedex - Steelix",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_209": {
+ "label": "Pokedex - Snubbull",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_210": {
+ "label": "Pokedex - Granbull",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_211": {
+ "label": "Pokedex - Qwilfish",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_212": {
+ "label": "Pokedex - Scizor",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_213": {
+ "label": "Pokedex - Shuckle",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_214": {
+ "label": "Pokedex - Heracross",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_215": {
+ "label": "Pokedex - Sneasel",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_216": {
+ "label": "Pokedex - Teddiursa",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_217": {
+ "label": "Pokedex - Ursaring",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_218": {
+ "label": "Pokedex - Slugma",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_219": {
+ "label": "Pokedex - Magcargo",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_220": {
+ "label": "Pokedex - Swinub",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_221": {
+ "label": "Pokedex - Piloswine",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_222": {
+ "label": "Pokedex - Corsola",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_223": {
+ "label": "Pokedex - Remoraid",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_224": {
+ "label": "Pokedex - Octillery",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_225": {
+ "label": "Pokedex - Delibird",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_226": {
+ "label": "Pokedex - Mantine",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_227": {
+ "label": "Pokedex - Skarmory",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_228": {
+ "label": "Pokedex - Houndour",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_229": {
+ "label": "Pokedex - Houndoom",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_230": {
+ "label": "Pokedex - Kingdra",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_231": {
+ "label": "Pokedex - Phanpy",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_232": {
+ "label": "Pokedex - Donphan",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_233": {
+ "label": "Pokedex - Porygon2",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_234": {
+ "label": "Pokedex - Stantler",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_235": {
+ "label": "Pokedex - Smeargle",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_236": {
+ "label": "Pokedex - Tyrogue",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_237": {
+ "label": "Pokedex - Hitmontop",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_238": {
+ "label": "Pokedex - Smoochum",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_239": {
+ "label": "Pokedex - Elekid",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_240": {
+ "label": "Pokedex - Magby",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_241": {
+ "label": "Pokedex - Miltank",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_242": {
+ "label": "Pokedex - Blissey",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_243": {
+ "label": "Pokedex - Raikou",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_244": {
+ "label": "Pokedex - Entei",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_245": {
+ "label": "Pokedex - Suicune",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_246": {
+ "label": "Pokedex - Larvitar",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_247": {
+ "label": "Pokedex - Pupitar",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_248": {
+ "label": "Pokedex - Tyranitar",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_249": {
+ "label": "Pokedex - Lugia",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_250": {
+ "label": "Pokedex - Ho-Oh",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_251": {
+ "label": "Pokedex - Celebi",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_252": {
+ "label": "Pokedex - Treecko",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_253": {
+ "label": "Pokedex - Grovyle",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_254": {
+ "label": "Pokedex - Sceptile",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_255": {
+ "label": "Pokedex - Torchic",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_256": {
+ "label": "Pokedex - Combusken",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_257": {
+ "label": "Pokedex - Blaziken",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_258": {
+ "label": "Pokedex - Mudkip",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_259": {
+ "label": "Pokedex - Marshtomp",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_260": {
+ "label": "Pokedex - Swampert",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_261": {
+ "label": "Pokedex - Poochyena",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_262": {
+ "label": "Pokedex - Mightyena",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_263": {
+ "label": "Pokedex - Zigzagoon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_264": {
+ "label": "Pokedex - Linoone",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_265": {
+ "label": "Pokedex - Wurmple",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_266": {
+ "label": "Pokedex - Silcoon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_267": {
+ "label": "Pokedex - Beautifly",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_268": {
+ "label": "Pokedex - Cascoon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_269": {
+ "label": "Pokedex - Dustox",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_270": {
+ "label": "Pokedex - Lotad",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_271": {
+ "label": "Pokedex - Lombre",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_272": {
+ "label": "Pokedex - Ludicolo",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_273": {
+ "label": "Pokedex - Seedot",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_274": {
+ "label": "Pokedex - Nuzleaf",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_275": {
+ "label": "Pokedex - Shiftry",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_276": {
+ "label": "Pokedex - Taillow",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_277": {
+ "label": "Pokedex - Swellow",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_278": {
+ "label": "Pokedex - Wingull",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_279": {
+ "label": "Pokedex - Pelipper",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_280": {
+ "label": "Pokedex - Ralts",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_281": {
+ "label": "Pokedex - Kirlia",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_282": {
+ "label": "Pokedex - Gardevoir",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_283": {
+ "label": "Pokedex - Surskit",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_284": {
+ "label": "Pokedex - Masquerain",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_285": {
+ "label": "Pokedex - Shroomish",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_286": {
+ "label": "Pokedex - Breloom",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_287": {
+ "label": "Pokedex - Slakoth",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_288": {
+ "label": "Pokedex - Vigoroth",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_289": {
+ "label": "Pokedex - Slaking",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_290": {
+ "label": "Pokedex - Nincada",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_291": {
+ "label": "Pokedex - Ninjask",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_292": {
+ "label": "Pokedex - Shedinja",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_293": {
+ "label": "Pokedex - Whismur",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_294": {
+ "label": "Pokedex - Loudred",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_295": {
+ "label": "Pokedex - Exploud",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_296": {
+ "label": "Pokedex - Makuhita",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_297": {
+ "label": "Pokedex - Hariyama",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_298": {
+ "label": "Pokedex - Azurill",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_299": {
+ "label": "Pokedex - Nosepass",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_300": {
+ "label": "Pokedex - Skitty",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_301": {
+ "label": "Pokedex - Delcatty",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_302": {
+ "label": "Pokedex - Sableye",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_303": {
+ "label": "Pokedex - Mawile",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_304": {
+ "label": "Pokedex - Aron",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_305": {
+ "label": "Pokedex - Lairon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_306": {
+ "label": "Pokedex - Aggron",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_307": {
+ "label": "Pokedex - Meditite",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_308": {
+ "label": "Pokedex - Medicham",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_309": {
+ "label": "Pokedex - Electrike",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_310": {
+ "label": "Pokedex - Manectric",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_311": {
+ "label": "Pokedex - Plusle",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_312": {
+ "label": "Pokedex - Minun",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_313": {
+ "label": "Pokedex - Volbeat",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_314": {
+ "label": "Pokedex - Illumise",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_315": {
+ "label": "Pokedex - Roselia",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_316": {
+ "label": "Pokedex - Gulpin",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_317": {
+ "label": "Pokedex - Swalot",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_318": {
+ "label": "Pokedex - Carvanha",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_319": {
+ "label": "Pokedex - Sharpedo",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_320": {
+ "label": "Pokedex - Wailmer",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_321": {
+ "label": "Pokedex - Wailord",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_322": {
+ "label": "Pokedex - Numel",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_323": {
+ "label": "Pokedex - Camerupt",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_324": {
+ "label": "Pokedex - Torkoal",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_325": {
+ "label": "Pokedex - Spoink",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_326": {
+ "label": "Pokedex - Grumpig",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_327": {
+ "label": "Pokedex - Spinda",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_328": {
+ "label": "Pokedex - Trapinch",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_329": {
+ "label": "Pokedex - Vibrava",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_330": {
+ "label": "Pokedex - Flygon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_331": {
+ "label": "Pokedex - Cacnea",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_332": {
+ "label": "Pokedex - Cacturne",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_333": {
+ "label": "Pokedex - Swablu",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_334": {
+ "label": "Pokedex - Altaria",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_335": {
+ "label": "Pokedex - Zangoose",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_336": {
+ "label": "Pokedex - Seviper",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_337": {
+ "label": "Pokedex - Lunatone",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_338": {
+ "label": "Pokedex - Solrock",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_339": {
+ "label": "Pokedex - Barboach",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_340": {
+ "label": "Pokedex - Whiscash",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_341": {
+ "label": "Pokedex - Corphish",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_342": {
+ "label": "Pokedex - Crawdaunt",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_343": {
+ "label": "Pokedex - Baltoy",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_344": {
+ "label": "Pokedex - Claydol",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_345": {
+ "label": "Pokedex - Lileep",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_346": {
+ "label": "Pokedex - Cradily",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_347": {
+ "label": "Pokedex - Anorith",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_348": {
+ "label": "Pokedex - Armaldo",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_349": {
+ "label": "Pokedex - Feebas",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_350": {
+ "label": "Pokedex - Milotic",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_351": {
+ "label": "Pokedex - Castform",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_352": {
+ "label": "Pokedex - Kecleon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_353": {
+ "label": "Pokedex - Shuppet",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_354": {
+ "label": "Pokedex - Banette",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_355": {
+ "label": "Pokedex - Duskull",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_356": {
+ "label": "Pokedex - Dusclops",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_357": {
+ "label": "Pokedex - Tropius",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_358": {
+ "label": "Pokedex - Chimecho",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_359": {
+ "label": "Pokedex - Absol",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_360": {
+ "label": "Pokedex - Wynaut",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_361": {
+ "label": "Pokedex - Snorunt",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_362": {
+ "label": "Pokedex - Glalie",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_363": {
+ "label": "Pokedex - Spheal",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_364": {
+ "label": "Pokedex - Sealeo",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_365": {
+ "label": "Pokedex - Walrein",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_366": {
+ "label": "Pokedex - Clamperl",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_367": {
+ "label": "Pokedex - Huntail",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_368": {
+ "label": "Pokedex - Gorebyss",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_369": {
+ "label": "Pokedex - Relicanth",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_370": {
+ "label": "Pokedex - Luvdisc",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_371": {
+ "label": "Pokedex - Bagon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_372": {
+ "label": "Pokedex - Shelgon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_373": {
+ "label": "Pokedex - Salamence",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_374": {
+ "label": "Pokedex - Beldum",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_375": {
+ "label": "Pokedex - Metang",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_376": {
+ "label": "Pokedex - Metagross",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_377": {
+ "label": "Pokedex - Regirock",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_378": {
+ "label": "Pokedex - Regice",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_379": {
+ "label": "Pokedex - Registeel",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_380": {
+ "label": "Pokedex - Latias",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_381": {
+ "label": "Pokedex - Latios",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_382": {
+ "label": "Pokedex - Kyogre",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_383": {
+ "label": "Pokedex - Groudon",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_384": {
+ "label": "Pokedex - Rayquaza",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_385": {
+ "label": "Pokedex - Jirachi",
+ "tags": ["Pokedex"]
+ },
+ "POKEDEX_REWARD_386": {
+ "label": "Pokedex - Deoxys",
+ "tags": ["Pokedex"]
+ },
+
+ "TRAINER_AARON_REWARD": {
+ "label": "Route 134 - Dragon Tamer Aaron",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ABIGAIL_1_REWARD": {
+ "label": "Route 110 - Triathlete Abigail",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_AIDAN_REWARD": {
+ "label": "Route 127 - Bird Keeper Aidan",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_AISHA_REWARD": {
+ "label": "Route 117 - Battle Girl Aisha",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ALBERTO_REWARD": {
+ "label": "Route 123 - Bird Keeper Alberto",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ALBERT_REWARD": {
+ "label": "Victory Road 1F - Cooltrainer Albert",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ALEXA_REWARD": {
+ "label": "Route 128 - Cooltrainer Alexa",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ALEXIA_REWARD": {
+ "label": "Petalburg Gym - Cooltrainer Alexia",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ALEX_REWARD": {
+ "label": "Route 134 - Bird Keeper Alex",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ALICE_REWARD": {
+ "label": "Route 109 - Swimmer Alice",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ALIX_REWARD": {
+ "label": "Route 115 - Psychic Alix",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ALLEN_REWARD": {
+ "label": "Route 102 - Youngster Allen",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ALLISON_REWARD": {
+ "label": "Route 129 - Triathlete Allison",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ALYSSA_REWARD": {
+ "label": "Route 110 - Triathlete Alyssa",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_AMY_AND_LIV_1_REWARD": {
+ "label": "Route 103 - Twins Amy and Liv",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ANNA_AND_MEG_1_REWARD": {
+ "label": "Route 117 - Sr. and Jr. Anna and Meg",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ANDREA_REWARD": {
+ "label": "Sootopolis Gym - Lass Andrea",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ANDRES_1_REWARD": {
+ "label": "Route 105 - Ruin Maniac Andres",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ANDREW_REWARD": {
+ "label": "Route 103 - Fisherman Andrew",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ANGELICA_REWARD": {
+ "label": "Route 120 - Parasol Lady Angelica",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ANGELINA_REWARD": {
+ "label": "Route 114 - Picnicker Angelina",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ANGELO_REWARD": {
+ "label": "Mauville Gym - Bug Maniac Angelo",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ANNIKA_REWARD": {
+ "label": "Sootopolis Gym - Pokefan Annika",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ANTHONY_REWARD": {
+ "label": "Route 110 - Triathlete Anthony",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ARCHIE_REWARD": {
+ "label": "Seafloor Cavern Room 9 - Aqua Leader Archie",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ASHLEY_REWARD": {
+ "label": "Fortree Gym - Picnicker Ashley",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ATHENA_REWARD": {
+ "label": "Route 127 - Cooltrainer Athena",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ATSUSHI_REWARD": {
+ "label": "Mt Pyre 5F - Black Belt Atsushi",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_AURON_REWARD": {
+ "label": "Route 125 - Expert Auron",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_AUSTINA_REWARD": {
+ "label": "Route 109 - Tuber Austina",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_AUTUMN_REWARD": {
+ "label": "Jagged Pass - Picnicker Autumn",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_AXLE_REWARD": {
+ "label": "Lavaridge Gym - Kindler Axle",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BARNY_REWARD": {
+ "label": "Route 118 - Fisherman Barny",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BARRY_REWARD": {
+ "label": "Route 126 - Swimmer Barry",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BEAU_REWARD": {
+ "label": "Route 111 - Camper Beau",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BECKY_REWARD": {
+ "label": "Route 111 - Picnicker Becky",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BECK_REWARD": {
+ "label": "Route 133 - Bird Keeper Beck",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BENJAMIN_1_REWARD": {
+ "label": "Route 110 - Triathlete Benjamin",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BEN_REWARD": {
+ "label": "Mauville Gym - Youngster Ben",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BERKE_REWARD": {
+ "label": "Petalburg Gym - Cooltrainer Berke",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BERNIE_1_REWARD": {
+ "label": "Route 114 - Kindler Bernie",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BETHANY_REWARD": {
+ "label": "Sootopolis Gym - Pokefan Bethany",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BETH_REWARD": {
+ "label": "Route 107 - Swimmer Beth",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BEVERLY_REWARD": {
+ "label": "Route 105 - Swimmer Beverly",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BIANCA_REWARD": {
+ "label": "Route 111 - Picnicker Bianca",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BILLY_REWARD": {
+ "label": "Route 104 - Youngster Billy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BLAKE_REWARD": {
+ "label": "Mossdeep Gym - Psychic Blake",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRANDEN_REWARD": {
+ "label": "Route 111 - Camper Branden",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRANDI_REWARD": {
+ "label": "Route 117 - Psychic Brandi",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRAWLY_1_REWARD": {
+ "label": "Dewford Gym - Leader Brawly",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRAXTON_REWARD": {
+ "label": "Route 123 - Cooltrainer Braxton",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRENDAN_LILYCOVE_MUDKIP_REWARD": {
+ "label": "Lilycove City - Rival Brendan/May",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRENDAN_ROUTE_103_MUDKIP_REWARD": {
+ "label": "Route 103 - Rival Brendan/May",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRENDAN_ROUTE_110_MUDKIP_REWARD": {
+ "label": "Route 110 - Rival Brendan/May",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRENDAN_ROUTE_119_MUDKIP_REWARD": {
+ "label": "Route 119 - Rival Brendan/May",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRENDAN_RUSTBORO_MUDKIP_REWARD": {
+ "label": "Rustboro City - Rival Brendan/May",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRENDA_REWARD": {
+ "label": "Route 126 - Swimmer Brenda",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRENDEN_REWARD": {
+ "label": "Dewford Gym - Sailor Brenden",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRENT_REWARD": {
+ "label": "Route 119 - Bug Maniac Brent",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRIANNA_REWARD": {
+ "label": "Sootopolis Gym - Lady Brianna",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRICE_REWARD": {
+ "label": "Route 112 - Hiker Brice",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRIDGET_REWARD": {
+ "label": "Sootopolis Gym - Beauty Bridget",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BROOKE_1_REWARD": {
+ "label": "Route 111 - Cooltrainer Brooke",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRYANT_REWARD": {
+ "label": "Route 112 - Kindler Bryant",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_BRYAN_REWARD": {
+ "label": "Route 111 - Ruin Maniac Bryan",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CALE_REWARD": {
+ "label": "Route 121 - Bug Maniac Cale",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CALLIE_REWARD": {
+ "label": "Route 120 - Battle Girl Callie",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CALVIN_1_REWARD": {
+ "label": "Route 102 - Youngster Calvin",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CAMDEN_REWARD": {
+ "label": "Route 127 - Triathlete Camden",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CAMERON_1_REWARD": {
+ "label": "Route 123 - Psychic Cameron",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CAMRON_REWARD": {
+ "label": "Route 107 - Triathlete Camron",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CARLEE_REWARD": {
+ "label": "Route 128 - Swimmer Carlee",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CAROLINA_REWARD": {
+ "label": "Route 108 - Cooltrainer Carolina",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CAROLINE_REWARD": {
+ "label": "Victory Road B2F - Cooltrainer Caroline",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CAROL_REWARD": {
+ "label": "Route 112 - Picnicker Carol",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CARTER_REWARD": {
+ "label": "Route 109 - Fisherman Carter",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CATHERINE_1_REWARD": {
+ "label": "Route 119 - Pokemon Ranger Catherine",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CEDRIC_REWARD": {
+ "label": "Mt Pyre 6F - Psychic Cedric",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CELIA_REWARD": {
+ "label": "Route 111 - Picnicker Celia",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CELINA_REWARD": {
+ "label": "Route 111 - Aroma Lady Celina",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CHAD_REWARD": {
+ "label": "Route 124 - Swimmer Chad",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CHANDLER_REWARD": {
+ "label": "Route 109 - Tuber Chandler",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CHARLIE_REWARD": {
+ "label": "Abandoned Ship 1F - Tuber Charlie",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CHARLOTTE_REWARD": {
+ "label": "Route 114 - Picnicker Charlotte",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CHASE_REWARD": {
+ "label": "Route 129 - Triathlete Chase",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CHESTER_REWARD": {
+ "label": "Route 118 - Bird Keeper Chester",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CHIP_REWARD": {
+ "label": "Route 120 - Ruin Maniac Chip",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CHRIS_REWARD": {
+ "label": "Route 119 - Fisherman Chris",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CINDY_1_REWARD": {
+ "label": "Route 104 - Lady Cindy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CLARENCE_REWARD": {
+ "label": "Route 129 - Swimmer Clarence",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CLARISSA_REWARD": {
+ "label": "Route 120 - Parasol Lady Clarissa",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CLARK_REWARD": {
+ "label": "Route 116 - Hiker Clark",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CLAUDE_REWARD": {
+ "label": "Route 114 - Fisherman Claude",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CLIFFORD_REWARD": {
+ "label": "Mossdeep Gym - Gentleman Clifford",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_COBY_REWARD": {
+ "label": "Route 113 - Bird Keeper Coby",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_COLE_REWARD": {
+ "label": "Lavaridge Gym - Kindler Cole",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_COLIN_REWARD": {
+ "label": "Route 120 - Bird Keeper Colin",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_COLTON_REWARD": {
+ "label": "SS Tidal - Pokefan Colton",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CONNIE_REWARD": {
+ "label": "Sootopolis Gym - Beauty Connie",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CONOR_REWARD": {
+ "label": "Route 133 - Expert Conor",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CORY_1_REWARD": {
+ "label": "Route 108 - Sailor Cory",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CRISSY_REWARD": {
+ "label": "Sootopolis Gym - Lass Crissy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CRISTIAN_REWARD": {
+ "label": "Dewford Gym - Black Belt Cristian",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CRISTIN_1_REWARD": {
+ "label": "Route 121 - Cooltrainer Cristin",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_CYNDY_1_REWARD": {
+ "label": "Route 115 - Battle Girl Cyndy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DAISUKE_REWARD": {
+ "label": "Route 111 - Black Belt Daisuke",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DAISY_REWARD": {
+ "label": "Route 103 - Aroma Lady Daisy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DALE_REWARD": {
+ "label": "Route 110 - Fisherman Dale",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DALTON_1_REWARD": {
+ "label": "Route 118 - Guitarist Dalton",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DANA_REWARD": {
+ "label": "Route 132 - Swimmer Dana",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DANIELLE_REWARD": {
+ "label": "Lavaridge Gym - Battle Girl Danielle",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DAPHNE_REWARD": {
+ "label": "Sootopolis Gym - Lady Daphne",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DARCY_REWARD": {
+ "label": "Route 132 - Cooltrainer Darcy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DARIAN_REWARD": {
+ "label": "Route 104 - Fisherman Darian",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DARIUS_REWARD": {
+ "label": "Fortree Gym - Bird Keeper Darius",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DARRIN_REWARD": {
+ "label": "Route 107 - Swimmer Darrin",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DAVID_REWARD": {
+ "label": "Route 109 - Swimmer David",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DAVIS_REWARD": {
+ "label": "Route 123 - Bug Catcher Davis",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DAWSON_REWARD": {
+ "label": "Route 116 - Rich Boy Dawson",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DAYTON_REWARD": {
+ "label": "Route 119 - Kindler Dayton",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DEANDRE_REWARD": {
+ "label": "Route 118 - Youngster Deandre",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DEAN_REWARD": {
+ "label": "Route 126 - Swimmer Dean",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DEBRA_REWARD": {
+ "label": "Route 133 - Swimmer Debra",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DECLAN_REWARD": {
+ "label": "Route 124 - Swimmer Declan",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DEMETRIUS_REWARD": {
+ "label": "Abandoned Ship 1F - Youngster Demetrius",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DENISE_REWARD": {
+ "label": "Route 107 - Swimmer Denise",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DEREK_REWARD": {
+ "label": "Route 117 - Bug Maniac Derek",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DEVAN_REWARD": {
+ "label": "Route 116 - Hiker Devan",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DIANA_1_REWARD": {
+ "label": "Jagged Pass - Picnicker Diana",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DIANNE_REWARD": {
+ "label": "Victory Road B2F - Cooltrainer Dianne",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DILLON_REWARD": {
+ "label": "Route 113 - Youngster Dillon",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DOMINIK_REWARD": {
+ "label": "Route 105 - Swimmer Dominik",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DONALD_REWARD": {
+ "label": "Route 119 - Bug Maniac Donald",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DONNY_REWARD": {
+ "label": "Route 127 - Triathlete Donny",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DOUGLAS_REWARD": {
+ "label": "Route 106 - Swimmer Douglas",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DOUG_REWARD": {
+ "label": "Route 119 - Bug Catcher Doug",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DRAKE_REWARD": {
+ "label": "Ever Grande City - Elite Four Drake",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DREW_REWARD": {
+ "label": "Route 111 - Camper Drew",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DUNCAN_REWARD": {
+ "label": "Abandoned Ship B1F - Sailor Duncan",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DUSTY_1_REWARD": {
+ "label": "Route 111 - Ruin Maniac Dusty",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DWAYNE_REWARD": {
+ "label": "Route 109 - Sailor Dwayne",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DYLAN_1_REWARD": {
+ "label": "Route 117 - Triathlete Dylan",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_DEZ_AND_LUKE_REWARD": {
+ "label": "Mt Pyre 2F - Young Couple Dez and Luke",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_EDGAR_REWARD": {
+ "label": "Victory Road 1F - Cooltrainer Edgar",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_EDMOND_REWARD": {
+ "label": "Route 109 - Sailor Edmond",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_EDWARDO_REWARD": {
+ "label": "Fortree Gym - Bird Keeper Edwardo",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_EDWARD_REWARD": {
+ "label": "Route 110 - Psychic Edward",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_EDWIN_1_REWARD": {
+ "label": "Route 110 - Collector Edwin",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ED_REWARD": {
+ "label": "Route 123 - Collector Ed",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ELIJAH_REWARD": {
+ "label": "Route 109 - Bird Keeper Elijah",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ELI_REWARD": {
+ "label": "Lavaridge Gym - Hiker Eli",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ELLIOT_1_REWARD": {
+ "label": "Route 106 - Fisherman Elliot",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ERIC_REWARD": {
+ "label": "Jagged Pass - Hiker Eric",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ERNEST_1_REWARD": {
+ "label": "Route 125 - Sailor Ernest",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ETHAN_1_REWARD": {
+ "label": "Jagged Pass - Camper Ethan",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_FABIAN_REWARD": {
+ "label": "Route 119 - Guitarist Fabian",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_FELIX_REWARD": {
+ "label": "Victory Road B2F - Cooltrainer Felix",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_FERNANDO_1_REWARD": {
+ "label": "Route 123 - Guitarist Fernando",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_FLANNERY_1_REWARD": {
+ "label": "Lavaridge Gym - Leader Flannery",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_FLINT_REWARD": {
+ "label": "Fortree Gym - Camper Flint",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_FOSTER_REWARD": {
+ "label": "Route 105 - Ruin Maniac Foster",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_FRANKLIN_REWARD": {
+ "label": "Route 133 - Swimmer Franklin",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_FREDRICK_REWARD": {
+ "label": "Route 123 - Expert Fredrick",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GABRIELLE_1_REWARD": {
+ "label": "Mt Pyre 3F - Pokemon Breeder Gabrielle",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GARRET_REWARD": {
+ "label": "SS Tidal - Rich Boy Garret",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GARRISON_REWARD": {
+ "label": "Abandoned Ship 1F - Ruin Maniac Garrison",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GEORGE_REWARD": {
+ "label": "Petalburg Gym - Cooltrainer George",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GERALD_REWARD": {
+ "label": "Lavaridge Gym - Cooltrainer Gerald",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GILBERT_REWARD": {
+ "label": "Route 132 - Swimmer Gilbert",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GINA_AND_MIA_1_REWARD": {
+ "label": "Route 104 - Twins Gina and Mia",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GLACIA_REWARD": {
+ "label": "Ever Grande City - Elite Four Glacia",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRACE_REWARD": {
+ "label": "Route 124 - Swimmer Grace",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GREG_REWARD": {
+ "label": "Route 119 - Bug Catcher Greg",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_AQUA_HIDEOUT_1_REWARD": {
+ "label": "Aqua Hideout 1F - Team Aqua Grunt",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_AQUA_HIDEOUT_2_REWARD": {
+ "label": "Aqua Hideout B1F - Team Aqua Grunt 2",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_AQUA_HIDEOUT_3_REWARD": {
+ "label": "Aqua Hideout B1F - Team Aqua Grunt 4",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_AQUA_HIDEOUT_4_REWARD": {
+ "label": "Aqua Hideout B2F - Team Aqua Grunt 1",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_AQUA_HIDEOUT_5_REWARD": {
+ "label": "Aqua Hideout B1F - Team Aqua Grunt 1",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_AQUA_HIDEOUT_6_REWARD": {
+ "label": "Aqua Hideout B2F - Team Aqua Grunt 2",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_AQUA_HIDEOUT_7_REWARD": {
+ "label": "Aqua Hideout B1F - Team Aqua Grunt 3",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_AQUA_HIDEOUT_8_REWARD": {
+ "label": "Aqua Hideout B2F - Team Aqua Grunt 3",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_10_REWARD": {
+ "label": "Magma Hideout 3F - Team Magma Grunt 3",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_11_REWARD": {
+ "label": "Magma Hideout 4F - Team Magma Grunt 1",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_12_REWARD": {
+ "label": "Magma Hideout 4F - Team Magma Grunt 2",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_13_REWARD": {
+ "label": "Magma Hideout 4F - Team Magma Grunt 3",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_14_REWARD": {
+ "label": "Magma Hideout 2F - Team Magma Grunt 2",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_15_REWARD": {
+ "label": "Magma Hideout 2F - Team Magma Grunt 5",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_16_REWARD": {
+ "label": "Magma Hideout 3F - Team Magma Grunt 2",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_1_REWARD": {
+ "label": "Magma Hideout 1F - Team Magma Grunt 2",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_2_REWARD": {
+ "label": "Magma Hideout 1F - Team Magma Grunt 1",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_3_REWARD": {
+ "label": "Magma Hideout 2F - Team Magma Grunt 1",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_4_REWARD": {
+ "label": "Magma Hideout 2F - Team Magma Grunt 4",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_5_REWARD": {
+ "label": "Magma Hideout 2F - Team Magma Grunt 3",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_6_REWARD": {
+ "label": "Magma Hideout 2F - Team Magma Grunt 6",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_7_REWARD": {
+ "label": "Magma Hideout 2F - Team Magma Grunt 7",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_8_REWARD": {
+ "label": "Magma Hideout 2F - Team Magma Grunt 8",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_9_REWARD": {
+ "label": "Magma Hideout 3F - Team Magma Grunt 1",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MT_CHIMNEY_1_REWARD": {
+ "label": "Mt Chimney - Team Magma Grunt 1",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MT_CHIMNEY_2_REWARD": {
+ "label": "Mt Chimney - Team Magma Grunt 2",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MT_PYRE_1_REWARD": {
+ "label": "Mt Pyre Summit - Team Aqua Grunt 2",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MT_PYRE_2_REWARD": {
+ "label": "Mt Pyre Summit - Team Aqua Grunt 1",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MT_PYRE_3_REWARD": {
+ "label": "Mt Pyre Summit - Team Aqua Grunt 4",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MT_PYRE_4_REWARD": {
+ "label": "Mt Pyre Summit - Team Aqua Grunt 3",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MUSEUM_1_REWARD": {
+ "label": "Oceanic Museum - Team Aqua Grunt 1",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_MUSEUM_2_REWARD": {
+ "label": "Oceanic Museum - Team Aqua Grunt 2",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_PETALBURG_WOODS_REWARD": {
+ "label": "Petalburg Woods - Team Aqua Grunt",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_RUSTURF_TUNNEL_REWARD": {
+ "label": "Rusturf Tunnel - Team Aqua Grunt",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_SEAFLOOR_CAVERN_1_REWARD": {
+ "label": "Seafloor Cavern Room 1 - Team Aqua Grunt 1",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_SEAFLOOR_CAVERN_2_REWARD": {
+ "label": "Seafloor Cavern Room 1 - Team Aqua Grunt 2",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_SEAFLOOR_CAVERN_3_REWARD": {
+ "label": "Seafloor Cavern Room 4 - Team Aqua Grunt 1",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_SEAFLOOR_CAVERN_4_REWARD": {
+ "label": "Seafloor Cavern Room 4 - Team Aqua Grunt 2",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_SEAFLOOR_CAVERN_5_REWARD": {
+ "label": "Seafloor Cavern Room 3 - Team Aqua Grunt",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_SPACE_CENTER_1_REWARD": {
+ "label": "Space Center - Team Magma Grunt 2",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_SPACE_CENTER_2_REWARD": {
+ "label": "Space Center - Team Magma Grunt 4",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_SPACE_CENTER_3_REWARD": {
+ "label": "Space Center - Team Magma Grunt 1",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_SPACE_CENTER_4_REWARD": {
+ "label": "Space Center - Team Magma Grunt 3",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_SPACE_CENTER_5_REWARD": {
+ "label": "Space Center - Team Magma Grunt 5",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_SPACE_CENTER_6_REWARD": {
+ "label": "Space Center - Team Magma Grunt 6",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_SPACE_CENTER_7_REWARD": {
+ "label": "Space Center - Team Magma Grunt 7",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_WEATHER_INST_1_REWARD": {
+ "label": "Weather Institute 1F - Team Aqua Grunt 2",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_WEATHER_INST_2_REWARD": {
+ "label": "Weather Institute 2F - Team Aqua Grunt 1",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_WEATHER_INST_3_REWARD": {
+ "label": "Weather Institute 2F - Team Aqua Grunt 3",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_WEATHER_INST_4_REWARD": {
+ "label": "Weather Institute 1F - Team Aqua Grunt 1",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GRUNT_WEATHER_INST_5_REWARD": {
+ "label": "Weather Institute 2F - Team Aqua Grunt 2",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_GWEN_REWARD": {
+ "label": "Route 109 - Tuber Gwen",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HAILEY_REWARD": {
+ "label": "Route 109 - Tuber Hailey",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HALEY_1_REWARD": {
+ "label": "Route 104 - Lass Haley",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HALLE_REWARD": {
+ "label": "Victory Road B1F - Cooltrainer Halle",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HANNAH_REWARD": {
+ "label": "Mossdeep Gym - Psychic Hannah",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HARRISON_REWARD": {
+ "label": "Route 128 - Swimmer Harrison",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HAYDEN_REWARD": {
+ "label": "Route 111 - Kindler Hayden",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HECTOR_REWARD": {
+ "label": "Route 115 - Collector Hector",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HEIDI_REWARD": {
+ "label": "Route 111 - Picnicker Heidi",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HELENE_REWARD": {
+ "label": "Route 115 - Battle Girl Helene",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HENRY_REWARD": {
+ "label": "Route 127 - Fisherman Henry",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HERMAN_REWARD": {
+ "label": "Route 131 - Swimmer Herman",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HIDEO_REWARD": {
+ "label": "Route 119 - Ninja Boy Hideo",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HITOSHI_REWARD": {
+ "label": "Route 134 - Black Belt Hitoshi",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HOPE_REWARD": {
+ "label": "Victory Road 1F - Cooltrainer Hope",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HUDSON_REWARD": {
+ "label": "Route 134 - Sailor Hudson",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HUEY_REWARD": {
+ "label": "Route 109 - Sailor Huey",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HUGH_REWARD": {
+ "label": "Route 119 - Bird Keeper Hugh",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_HUMBERTO_REWARD": {
+ "label": "Fortree Gym - Bird Keeper Humberto",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_IMANI_REWARD": {
+ "label": "Route 105 - Swimmer Imani",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_IRENE_REWARD": {
+ "label": "Route 111 - Picnicker Irene",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ISAAC_1_REWARD": {
+ "label": "Route 117 - Pokemon Breeder Isaac",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ISABELLA_REWARD": {
+ "label": "Route 124 - Triathlete Isabella",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ISABELLE_REWARD": {
+ "label": "Route 103 - Swimmer Isabelle",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ISABEL_1_REWARD": {
+ "label": "Route 110 - Pokefan Isabel",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ISAIAH_1_REWARD": {
+ "label": "Route 128 - Triathlete Isaiah",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ISOBEL_REWARD": {
+ "label": "Route 126 - Triathlete Isobel",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_IVAN_REWARD": {
+ "label": "Route 104 - Fisherman Ivan",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JACE_REWARD": {
+ "label": "Lavaridge Gym - Kindler Jace",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JACKI_1_REWARD": {
+ "label": "Route 123 - Psychic Jacki",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JACKSON_1_REWARD": {
+ "label": "Route 119 - Pokemon Ranger Jackson",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JACK_REWARD": {
+ "label": "Route 134 - Swimmer Jack",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JACLYN_REWARD": {
+ "label": "Route 110 - Psychic Jaclyn",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JACOB_REWARD": {
+ "label": "Route 110 - Triathlete Jacob",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JAIDEN_REWARD": {
+ "label": "Route 115 - Ninja Boy Jaiden",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JAMES_1_REWARD": {
+ "label": "Petalburg Woods - Bug Catcher James",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JANICE_REWARD": {
+ "label": "Route 116 - Lass Janice",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JANI_REWARD": {
+ "label": "Abandoned Ship 1F - Tuber Jani",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JARED_REWARD": {
+ "label": "Fortree Gym - Bird Keeper Jared",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JASMINE_REWARD": {
+ "label": "Route 110 - Triathlete Jasmine",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JAYLEN_REWARD": {
+ "label": "Route 113 - Youngster Jaylen",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JAZMYN_REWARD": {
+ "label": "Route 123 - Cooltrainer Jazmyn",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JEFFREY_1_REWARD": {
+ "label": "Route 120 - Bug Maniac Jeffrey",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JEFF_REWARD": {
+ "label": "Lavaridge Gym - Kindler Jeff",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JENNA_REWARD": {
+ "label": "Route 120 - Pokemon Ranger Jenna",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JENNIFER_REWARD": {
+ "label": "Route 120 - Cooltrainer Jennifer",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JENNY_1_REWARD": {
+ "label": "Route 124 - Swimmer Jenny",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JEROME_REWARD": {
+ "label": "Route 108 - Swimmer Jerome",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JERRY_1_REWARD": {
+ "label": "Route 116 - School Kid Jerry",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JESSICA_1_REWARD": {
+ "label": "Route 121 - Beauty Jessica",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JOCELYN_REWARD": {
+ "label": "Dewford Gym - Battle Girl Jocelyn",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JODY_REWARD": {
+ "label": "Petalburg Gym - Cooltrainer Jody",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JOEY_REWARD": {
+ "label": "Route 116 - Youngster Joey",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JOHANNA_REWARD": {
+ "label": "Route 109 - Beauty Johanna",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JOHN_AND_JAY_1_REWARD": {
+ "label": "Meteor Falls 1F - Old Couple John and Jay",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JOHNSON_REWARD": {
+ "label": "Route 116 - Youngster Johnson",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JONAH_REWARD": {
+ "label": "Route 127 - Fisherman Jonah",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JONAS_REWARD": {
+ "label": "Route 123 - Ninja Boy Jonas",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JONATHAN_REWARD": {
+ "label": "Route 132 - Cooltrainer Jonathan",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JOSEPH_REWARD": {
+ "label": "Route 110 - Guitarist Joseph",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JOSE_REWARD": {
+ "label": "Route 116 - Bug Catcher Jose",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JOSH_REWARD": {
+ "label": "Rustboro Gym - Youngster Josh",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JOSUE_REWARD": {
+ "label": "Route 105 - Bird Keeper Josue",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JUAN_1_REWARD": {
+ "label": "Sootopolis Gym - Leader Juan",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JULIE_REWARD": {
+ "label": "Victory Road B2F - Cooltrainer Julie",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_JULIO_REWARD": {
+ "label": "Jagged Pass - Triathlete Julio",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KAI_REWARD": {
+ "label": "Route 114 - Fisherman Kai",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KALEB_REWARD": {
+ "label": "Route 110 - Pokefan Kaleb",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KARA_REWARD": {
+ "label": "Route 131 - Swimmer Kara",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KAREN_1_REWARD": {
+ "label": "Route 116 - School Kid Karen",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KATE_AND_JOY_REWARD": {
+ "label": "Route 121 - Sr. and Jr. Kate and Joy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KATELYNN_REWARD": {
+ "label": "Victory Road 1F - Cooltrainer Katelynn",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KATELYN_1_REWARD": {
+ "label": "Route 128 - Triathlete Katelyn",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KATHLEEN_REWARD": {
+ "label": "Mossdeep Gym - Hex Maniac Kathleen",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KATIE_REWARD": {
+ "label": "Route 130 - Swimmer Katie",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KAYLA_REWARD": {
+ "label": "Mt Pyre 3F - Psychic Kayla",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KAYLEY_REWARD": {
+ "label": "Route 123 - Parasol Lady Kayley",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KEEGAN_REWARD": {
+ "label": "Lavaridge Gym - Kindler Keegan",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KEIGO_REWARD": {
+ "label": "Route 120 - Ninja Boy Keigo",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KELVIN_REWARD": {
+ "label": "Route 134 - Sailor Kelvin",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KENT_REWARD": {
+ "label": "Route 119 - Bug Catcher Kent",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KEVIN_REWARD": {
+ "label": "Route 131 - Swimmer Kevin",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KIM_AND_IRIS_REWARD": {
+ "label": "Route 125 - Sr. and Jr. Kim and Iris",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KINDRA_REWARD": {
+ "label": "Route 123 - Hex Maniac Kindra",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KIRA_AND_DAN_1_REWARD": {
+ "label": "Abandoned Ship 1F - Young Couple Kira and Dan",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KIRK_REWARD": {
+ "label": "Mauville Gym - Guitarist Kirk",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KIYO_REWARD": {
+ "label": "Route 132 - Black Belt Kiyo",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KOICHI_REWARD": {
+ "label": "Route 115 - Black Belt Koichi",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KOJI_1_REWARD": {
+ "label": "Route 127 - Black Belt Koji",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KYLA_REWARD": {
+ "label": "Route 106 - Swimmer Kyla",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_KYRA_REWARD": {
+ "label": "Route 115 - Triathlete Kyra",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LAO_1_REWARD": {
+ "label": "Route 113 - Ninja Boy Lao",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LARRY_REWARD": {
+ "label": "Route 112 - Camper Larry",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LAURA_REWARD": {
+ "label": "Dewford Gym - Battle Girl Laura",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LAUREL_REWARD": {
+ "label": "Route 134 - Swimmer Laurel",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LAWRENCE_REWARD": {
+ "label": "Route 113 - Camper Lawrence",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LEA_AND_JED_REWARD": {
+ "label": "SS Tidal - Young Couple Lea and Jed",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LEAH_REWARD": {
+ "label": "Mt Pyre 2F - Hex Maniac Leah",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LENNY_REWARD": {
+ "label": "Route 114 - Hiker Lenny",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LEONARDO_REWARD": {
+ "label": "Route 126 - Swimmer Leonardo",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LEONARD_REWARD": {
+ "label": "SS Tidal - Sailor Leonard",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LEONEL_REWARD": {
+ "label": "Route 120 - Cooltrainer Leonel",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LILA_AND_ROY_1_REWARD": {
+ "label": "Route 124 - Sis and Bro Lila and Roy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LILITH_REWARD": {
+ "label": "Dewford Gym - Battle Girl Lilith",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LINDA_REWARD": {
+ "label": "Route 133 - Swimmer Linda",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LISA_AND_RAY_REWARD": {
+ "label": "Route 107 - Sis and Bro Lisa and Ray",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LOLA_1_REWARD": {
+ "label": "Route 109 - Tuber Lola",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LORENZO_REWARD": {
+ "label": "Route 120 - Pokemon Ranger Lorenzo",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LUCAS_1_REWARD": {
+ "label": "Route 114 - Hiker Lucas",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LUIS_REWARD": {
+ "label": "Route 105 - Swimmer Luis",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LUNG_REWARD": {
+ "label": "Route 113 - Ninja Boy Lung",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LYDIA_1_REWARD": {
+ "label": "Route 117 - Pokemon Breeder Lydia",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_LYLE_REWARD": {
+ "label": "Petalburg Woods - Bug Catcher Lyle",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MACEY_REWARD": {
+ "label": "Mossdeep Gym - Psychic Macey",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MADELINE_1_REWARD": {
+ "label": "Route 113 - Parasol Lady Madeline",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MAKAYLA_REWARD": {
+ "label": "Route 132 - Expert Makayla",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MARCEL_REWARD": {
+ "label": "Route 121 - Cooltrainer Marcel",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MARCOS_REWARD": {
+ "label": "Route 103 - Guitarist Marcos",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MARC_REWARD": {
+ "label": "Rustboro Gym - Hiker Marc",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MARIA_1_REWARD": {
+ "label": "Route 117 - Triathlete Maria",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MARK_REWARD": {
+ "label": "Mt Pyre 2F - Pokemaniac Mark",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MARLENE_REWARD": {
+ "label": "Route 115 - Psychic Marlene",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MARLEY_REWARD": {
+ "label": "Route 134 - Cooltrainer Marley",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MARY_REWARD": {
+ "label": "Petalburg Gym - Cooltrainer Mary",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MATTHEW_REWARD": {
+ "label": "Route 108 - Swimmer Matthew",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MATT_REWARD": {
+ "label": "Aqua Hideout B2F - Aqua Admin Matt",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MAURA_REWARD": {
+ "label": "Mossdeep Gym - Psychic Maura",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MAXIE_MAGMA_HIDEOUT_REWARD": {
+ "label": "Magma Hideout 4F - Magma Leader Maxie",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MAXIE_MT_CHIMNEY_REWARD": {
+ "label": "Mt Chimney - Magma Leader Maxie",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MEL_AND_PAUL_REWARD": {
+ "label": "Route 109 - Young Couple Mel and Paul",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MELINA_REWARD": {
+ "label": "Route 117 - Triathlete Melina",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MELISSA_REWARD": {
+ "label": "Mt Chimney - Beauty Melissa",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MICAH_REWARD": {
+ "label": "SS Tidal - Gentleman Micah",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MICHELLE_REWARD": {
+ "label": "Victory Road B1F - Cooltrainer Michelle",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MIGUEL_1_REWARD": {
+ "label": "Route 103 - Pokefan Miguel",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MIKE_2_REWARD": {
+ "label": "Rusturf Tunnel - Hiker Mike",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MISSY_REWARD": {
+ "label": "Route 108 - Swimmer Missy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MITCHELL_REWARD": {
+ "label": "Victory Road B1F - Cooltrainer Mitchell",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MIU_AND_YUKI_REWARD": {
+ "label": "Route 123 - Twins Miu and Yuki",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MOLLIE_REWARD": {
+ "label": "Route 133 - Expert Mollie",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_MYLES_REWARD": {
+ "label": "Route 121 - Pokemon Breeder Myles",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_NANCY_REWARD": {
+ "label": "Route 114 - Picnicker Nancy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_NAOMI_REWARD": {
+ "label": "SS Tidal - Lady Naomi",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_NATE_REWARD": {
+ "label": "Mossdeep Gym - Gentleman Nate",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_NED_REWARD": {
+ "label": "Route 106 - Fisherman Ned",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_NICHOLAS_REWARD": {
+ "label": "Mossdeep Gym - Psychic Nicholas",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_NICOLAS_1_REWARD": {
+ "label": "Meteor Falls 1F - Dragon Tamer Nicolas",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_NIKKI_REWARD": {
+ "label": "Route 126 - Swimmer Nikki",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_NOB_1_REWARD": {
+ "label": "Route 115 - Black Belt Nob",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_NOLAN_REWARD": {
+ "label": "Route 114 - Fisherman Nolan",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_NOLEN_REWARD": {
+ "label": "Route 125 - Swimmer Nolen",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_NORMAN_1_REWARD": {
+ "label": "Petalburg Gym - Leader Norman",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_OLIVIA_REWARD": {
+ "label": "Sootopolis Gym - Beauty Olivia",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_OWEN_REWARD": {
+ "label": "Victory Road B2F - Cooltrainer Owen",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_PABLO_1_REWARD": {
+ "label": "Route 126 - Triathlete Pablo",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_PARKER_REWARD": {
+ "label": "Petalburg Gym - Cooltrainer Parker",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_PAT_REWARD": {
+ "label": "Route 121 - Pokemon Breeder Pat",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_PAXTON_REWARD": {
+ "label": "Route 132 - Expert Paxton",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_PERRY_REWARD": {
+ "label": "Route 118 - Bird Keeper Perry",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_PETE_REWARD": {
+ "label": "Route 103 - Swimmer Pete",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_PHILLIP_REWARD": {
+ "label": "SS Tidal - Sailor Phillip",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_PHIL_REWARD": {
+ "label": "Route 119 - Bird Keeper Phil",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_PHOEBE_REWARD": {
+ "label": "Ever Grande City - Elite Four Phoebe",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_PRESLEY_REWARD": {
+ "label": "Route 125 - Bird Keeper Presley",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_PRESTON_REWARD": {
+ "label": "Mossdeep Gym - Psychic Preston",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_QUINCY_REWARD": {
+ "label": "Victory Road 1F - Cooltrainer Quincy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_RACHEL_REWARD": {
+ "label": "Route 119 - Parasol Lady Rachel",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_RANDALL_REWARD": {
+ "label": "Petalburg Gym - Cooltrainer Randall",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_REED_REWARD": {
+ "label": "Route 129 - Swimmer Reed",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_RELI_AND_IAN_REWARD": {
+ "label": "Route 131 - Sis and Bro Reli and Ian",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_REYNA_REWARD": {
+ "label": "Route 134 - Battle Girl Reyna",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_RHETT_REWARD": {
+ "label": "Route 103 - Black Belt Rhett",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_RICHARD_REWARD": {
+ "label": "Route 131 - Swimmer Richard",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_RICKY_1_REWARD": {
+ "label": "Route 109 - Tuber Ricky",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_RICK_REWARD": {
+ "label": "Route 102 - Bug Catcher Rick",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_RILEY_REWARD": {
+ "label": "Route 120 - Ninja Boy Riley",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ROBERT_1_REWARD": {
+ "label": "Route 120 - Bird Keeper Robert",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_RODNEY_REWARD": {
+ "label": "Route 130 - Swimmer Rodney",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ROGER_REWARD": {
+ "label": "Route 127 - Fisherman Roger",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ROLAND_REWARD": {
+ "label": "Route 124 - Swimmer Roland",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_RONALD_REWARD": {
+ "label": "Route 132 - Fisherman Ronald",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ROSE_1_REWARD": {
+ "label": "Route 118 - Aroma Lady Rose",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ROXANNE_1_REWARD": {
+ "label": "Rustboro Gym - Leader Roxanne",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_RUBEN_REWARD": {
+ "label": "Route 128 - Cooltrainer Ruben",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SAMANTHA_REWARD": {
+ "label": "Mossdeep Gym - Psychic Samantha",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SAMUEL_REWARD": {
+ "label": "Victory Road B1F - Cooltrainer Samuel",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SANTIAGO_REWARD": {
+ "label": "Route 130 - Swimmer Santiago",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SARAH_REWARD": {
+ "label": "Route 116 - Lady Sarah",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SAWYER_1_REWARD": {
+ "label": "Mt Chimney - Hiker Sawyer",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SHANE_REWARD": {
+ "label": "Route 114 - Camper Shane",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SHANNON_REWARD": {
+ "label": "Victory Road B1F - Cooltrainer Shannon",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SHARON_REWARD": {
+ "label": "Route 125 - Swimmer Sharon",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SHAWN_REWARD": {
+ "label": "Mauville Gym - Guitarist Shawn",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SHAYLA_REWARD": {
+ "label": "Route 112 - Aroma Lady Shayla",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SHEILA_REWARD": {
+ "label": "Mt Chimney - Beauty Sheila",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SHELBY_1_REWARD": {
+ "label": "Mt Chimney - Expert Shelby",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SHELLY_SEAFLOOR_CAVERN_REWARD": {
+ "label": "Seafloor Cavern Room 3 - Aqua Admin Shelly",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SHELLY_WEATHER_INSTITUTE_REWARD": {
+ "label": "Weather Institute 2F - Aqua Admin Shelly",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SHIRLEY_REWARD": {
+ "label": "Mt Chimney - Beauty Shirley",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SIDNEY_REWARD": {
+ "label": "Ever Grande City - Elite Four Sidney",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SIENNA_REWARD": {
+ "label": "Route 126 - Swimmer Sienna",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SIMON_REWARD": {
+ "label": "Route 109 - Tuber Simon",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SOPHIE_REWARD": {
+ "label": "Route 113 - Picnicker Sophie",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SPENCER_REWARD": {
+ "label": "Route 124 - Swimmer Spencer",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_STAN_REWARD": {
+ "label": "Route 125 - Swimmer Stan",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_STEVEN_REWARD": {
+ "label": "Meteor Falls 1F - Rival Steven",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_STEVE_1_REWARD": {
+ "label": "Route 114 - Pokemaniac Steve",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SUSIE_REWARD": {
+ "label": "Route 131 - Swimmer Susie",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_SYLVIA_REWARD": {
+ "label": "Mossdeep Gym - Hex Maniac Sylvia",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TABITHA_MAGMA_HIDEOUT_REWARD": {
+ "label": "Magma Hideout 4F - Magma Admin Tabitha",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TABITHA_MT_CHIMNEY_REWARD": {
+ "label": "Mt Chimney - Magma Admin Tabitha",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TAKAO_REWARD": {
+ "label": "Dewford Gym - Black Belt Takao",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TAKASHI_REWARD": {
+ "label": "Route 119 - Ninja Boy Takashi",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TALIA_REWARD": {
+ "label": "Route 131 - Triathlete Talia",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TAMMY_REWARD": {
+ "label": "Route 121 - Hex Maniac Tammy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TANYA_REWARD": {
+ "label": "Route 125 - Swimmer Tanya",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TARA_REWARD": {
+ "label": "Route 108 - Swimmer Tara",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TASHA_REWARD": {
+ "label": "Mt Pyre 4F - Hex Maniac Tasha",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TATE_AND_LIZA_1_REWARD": {
+ "label": "Mossdeep Gym - Leader Tate and Liza",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TAYLOR_REWARD": {
+ "label": "Route 119 - Bug Maniac Taylor",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TYRA_AND_IVY_REWARD": {
+ "label": "Route 114 - Sr. and Jr. Tyra and Ivy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_THALIA_1_REWARD": {
+ "label": "Abandoned Ship 1F - Beauty Thalia",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_THOMAS_REWARD": {
+ "label": "SS Tidal - Gentleman Thomas",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TIANA_REWARD": {
+ "label": "Route 102 - Lass Tiana",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TIFFANY_REWARD": {
+ "label": "Sootopolis Gym - Beauty Tiffany",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TIMMY_REWARD": {
+ "label": "Route 110 - Youngster Timmy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TIMOTHY_1_REWARD": {
+ "label": "Route 115 - Expert Timothy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TISHA_REWARD": {
+ "label": "Route 129 - Swimmer Tisha",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TOMMY_REWARD": {
+ "label": "Rustboro Gym - Youngster Tommy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TONY_1_REWARD": {
+ "label": "Route 107 - Swimmer Tony",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TORI_AND_TIA_REWARD": {
+ "label": "Route 113 - Twins Tori and Tia",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TRAVIS_REWARD": {
+ "label": "Route 111 - Camper Travis",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TRENT_1_REWARD": {
+ "label": "Route 112 - Hiker Trent",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_TYRON_REWARD": {
+ "label": "Route 111 - Camper Tyron",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_VALERIE_1_REWARD": {
+ "label": "Mt Pyre 6F - Hex Maniac Valerie",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_VANESSA_REWARD": {
+ "label": "Route 121 - Pokefan Vanessa",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_VICKY_REWARD": {
+ "label": "Route 111 - Winstrate Vicky",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_VICTORIA_REWARD": {
+ "label": "Route 111 - Winstrate Victoria",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_VICTOR_REWARD": {
+ "label": "Route 111 - Winstrate Victor",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_VIOLET_REWARD": {
+ "label": "Route 123 - Aroma Lady Violet",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_VIRGIL_REWARD": {
+ "label": "Mossdeep Gym - Psychic Virgil",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_VITO_REWARD": {
+ "label": "Victory Road B2F - Cooltrainer Vito",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_VIVIAN_REWARD": {
+ "label": "Mauville Gym - Battle Girl Vivian",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_VIVI_REWARD": {
+ "label": "Route 111 - Winstrate Vivi",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_WADE_REWARD": {
+ "label": "Route 118 - Fisherman Wade",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_WALLACE_REWARD": {
+ "label": "Ever Grande City - Champion Wallace",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_WALTER_1_REWARD": {
+ "label": "Route 121 - Gentleman Walter",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_WALLY_MAUVILLE_REWARD": {
+ "label": "Mauville City - Rival Wally",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_WALLY_VR_1_REWARD": {
+ "label": "Victory Road 1F - Rival Wally",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_WATTSON_1_REWARD": {
+ "label": "Mauville Gym - Leader Wattson",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_WARREN_REWARD": {
+ "label": "Route 133 - Cooltrainer Warren",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_WAYNE_REWARD": {
+ "label": "Route 128 - Fisherman Wayne",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_WENDY_REWARD": {
+ "label": "Route 123 - Cooltrainer Wendy",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_WILLIAM_REWARD": {
+ "label": "Mt Pyre 3F - Psychic William",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_WILTON_1_REWARD": {
+ "label": "Route 111 - Cooltrainer Wilton",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_WINONA_1_REWARD": {
+ "label": "Fortree Gym - Leader Winona",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_WINSTON_1_REWARD": {
+ "label": "Route 104 - Rich Boy Winston",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_WYATT_REWARD": {
+ "label": "Route 113 - Pokemaniac Wyatt",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_YASU_REWARD": {
+ "label": "Route 119 - Ninja Boy Yasu",
+ "tags": ["Trainer"]
+ },
+ "TRAINER_ZANDER_REWARD": {
+ "label": "Mt Pyre 2F - Black Belt Zander",
+ "tags": ["Trainer"]
+ }
+}
diff --git a/worlds/pokemon_emerald/data/regions/battle_frontier.json b/worlds/pokemon_emerald/data/regions/battle_frontier.json
new file mode 100644
index 000000000000..a391129bd176
--- /dev/null
+++ b/worlds/pokemon_emerald/data/regions/battle_frontier.json
@@ -0,0 +1,458 @@
+{
+ "REGION_BATTLE_FRONTIER_RECEPTION_GATE/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_RECEPTION_GATE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_RECEPTION_GATE:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:8",
+ "MAP_BATTLE_FRONTIER_RECEPTION_GATE:1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:9"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_OUTSIDE_WEST/DOCK": {
+ "parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_WEST",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SS_TIDAL_CORRIDOR/MAIN"
+ ],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_OUTSIDE_WEST:8/MAP_BATTLE_FRONTIER_RECEPTION_GATE:0"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_OUTSIDE_WEST/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_WEST",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN"
+ ],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_OUTSIDE_WEST:7/MAP_BATTLE_FRONTIER_LOUNGE7:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_WEST:2/MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1/MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_WEST:9/MAP_BATTLE_FRONTIER_RECEPTION_GATE:1",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_WEST:0/MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_WEST:5/MAP_BATTLE_FRONTIER_SCOTTS_HOUSE:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_WEST:3/MAP_BATTLE_FRONTIER_LOUNGE2:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_WEST:4/MAP_BATTLE_FRONTIER_MART:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_WEST:6/MAP_BATTLE_FRONTIER_LOUNGE4:0"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_OUTSIDE_WEST/WATER": {
+ "parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_WEST",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_BATTLE_FRONTIER_OUTSIDE_EAST/WATER",
+ "REGION_BATTLE_FRONTIER_OUTSIDE_WEST/CAVE_ENTRANCE"
+ ],
+ "warps": []
+ },
+ "REGION_BATTLE_FRONTIER_OUTSIDE_WEST/CAVE_ENTRANCE": {
+ "parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_WEST",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_BATTLE_FRONTIER_OUTSIDE_WEST/WATER"
+ ],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_OUTSIDE_WEST:10/MAP_ARTISAN_CAVE_B1F:0"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_EAST",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_BATTLE_FRONTIER_OUTSIDE_WEST/MAIN",
+ "REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL"
+ ],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_OUTSIDE_EAST:12/MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_EAST:5/MAP_BATTLE_FRONTIER_LOUNGE1:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_EAST:8/MAP_BATTLE_FRONTIER_LOUNGE6:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_EAST:6/MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_EAST:10/MAP_BATTLE_FRONTIER_LOUNGE8:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_EAST:0/MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_EAST:11/MAP_BATTLE_FRONTIER_LOUNGE9:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_EAST:7/MAP_BATTLE_FRONTIER_LOUNGE5:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_EAST:4/MAP_BATTLE_FRONTIER_RANKING_HALL:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_EAST:1/MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_EAST:3/MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY:0",
+ "MAP_BATTLE_FRONTIER_OUTSIDE_EAST:9/MAP_BATTLE_FRONTIER_LOUNGE3:0"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_OUTSIDE_EAST/CAVE_ENTRANCE": {
+ "parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_EAST",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN"
+ ],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_OUTSIDE_EAST:13/MAP_ARTISAN_CAVE_1F:0"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL": {
+ "parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_EAST",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN",
+ "REGION_BATTLE_FRONTIER_OUTSIDE_EAST/WATER"
+ ],
+ "warps": []
+ },
+ "REGION_BATTLE_FRONTIER_OUTSIDE_EAST/WATER": {
+ "parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_EAST",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL",
+ "REGION_BATTLE_FRONTIER_OUTSIDE_WEST/WATER"
+ ],
+ "warps": []
+ },
+ "REGION_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:2"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY:0,1,2/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:0"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:1"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:3"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_BATTLE_DOME_LOBBY/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:2/MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM:0",
+ "MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:0"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:2"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER:0,1,2/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:6"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_RANKING_HALL/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_RANKING_HALL",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_RANKING_HALL:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:4"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:2/MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:0",
+ "MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:12"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:0/MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_MART/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_MART",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_MART:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:4"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_SCOTTS_HOUSE/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_SCOTTS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_SCOTTS_HOUSE:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:5"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_LOUNGE1/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_LOUNGE1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_LOUNGE1:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:5"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_LOUNGE2/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_LOUNGE2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_LOUNGE2:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:3"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_LOUNGE3/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_LOUNGE3",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_LOUNGE3:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:9"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_LOUNGE4/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_LOUNGE4",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_LOUNGE4:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:6"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_LOUNGE5/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_LOUNGE5",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_LOUNGE5:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:7"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_LOUNGE6/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_LOUNGE6",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_LOUNGE6:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:8"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_LOUNGE7/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_LOUNGE7",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_LOUNGE7:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:7"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_LOUNGE8/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_LOUNGE8",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_LOUNGE8:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:10"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_LOUNGE9/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_LOUNGE9",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_LOUNGE9:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:11"
+ ]
+ },
+
+ "REGION_ARTISAN_CAVE_1F/MAIN": {
+ "parent_map": "MAP_ARTISAN_CAVE_1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ARTISAN_CAVE_1F_CARBOS"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ARTISAN_CAVE_1F:1/MAP_ARTISAN_CAVE_B1F:1",
+ "MAP_ARTISAN_CAVE_1F:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:13"
+ ]
+ },
+ "REGION_ARTISAN_CAVE_B1F/MAIN": {
+ "parent_map": "MAP_ARTISAN_CAVE_B1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ARTISAN_CAVE_B1F_HP_UP",
+ "HIDDEN_ITEM_ARTISAN_CAVE_B1F_ZINC",
+ "HIDDEN_ITEM_ARTISAN_CAVE_B1F_CALCIUM",
+ "HIDDEN_ITEM_ARTISAN_CAVE_B1F_PROTEIN",
+ "HIDDEN_ITEM_ARTISAN_CAVE_B1F_IRON"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ARTISAN_CAVE_B1F:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:10",
+ "MAP_ARTISAN_CAVE_B1F:1/MAP_ARTISAN_CAVE_1F:1"
+ ]
+ }
+}
diff --git a/worlds/pokemon_emerald/data/regions/cities.json b/worlds/pokemon_emerald/data/regions/cities.json
new file mode 100644
index 000000000000..d0a1f9d29580
--- /dev/null
+++ b/worlds/pokemon_emerald/data/regions/cities.json
@@ -0,0 +1,3877 @@
+{
+ "REGION_POKEDEX": {
+ "parent_map": "MAP_LITTLEROOT_TOWN",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "POKEDEX_REWARD_001",
+ "POKEDEX_REWARD_002",
+ "POKEDEX_REWARD_003",
+ "POKEDEX_REWARD_004",
+ "POKEDEX_REWARD_005",
+ "POKEDEX_REWARD_006",
+ "POKEDEX_REWARD_007",
+ "POKEDEX_REWARD_008",
+ "POKEDEX_REWARD_009",
+ "POKEDEX_REWARD_010",
+ "POKEDEX_REWARD_011",
+ "POKEDEX_REWARD_012",
+ "POKEDEX_REWARD_013",
+ "POKEDEX_REWARD_014",
+ "POKEDEX_REWARD_015",
+ "POKEDEX_REWARD_016",
+ "POKEDEX_REWARD_017",
+ "POKEDEX_REWARD_018",
+ "POKEDEX_REWARD_019",
+ "POKEDEX_REWARD_020",
+ "POKEDEX_REWARD_021",
+ "POKEDEX_REWARD_022",
+ "POKEDEX_REWARD_023",
+ "POKEDEX_REWARD_024",
+ "POKEDEX_REWARD_025",
+ "POKEDEX_REWARD_026",
+ "POKEDEX_REWARD_027",
+ "POKEDEX_REWARD_028",
+ "POKEDEX_REWARD_029",
+ "POKEDEX_REWARD_030",
+ "POKEDEX_REWARD_031",
+ "POKEDEX_REWARD_032",
+ "POKEDEX_REWARD_033",
+ "POKEDEX_REWARD_034",
+ "POKEDEX_REWARD_035",
+ "POKEDEX_REWARD_036",
+ "POKEDEX_REWARD_037",
+ "POKEDEX_REWARD_038",
+ "POKEDEX_REWARD_039",
+ "POKEDEX_REWARD_040",
+ "POKEDEX_REWARD_041",
+ "POKEDEX_REWARD_042",
+ "POKEDEX_REWARD_043",
+ "POKEDEX_REWARD_044",
+ "POKEDEX_REWARD_045",
+ "POKEDEX_REWARD_046",
+ "POKEDEX_REWARD_047",
+ "POKEDEX_REWARD_048",
+ "POKEDEX_REWARD_049",
+ "POKEDEX_REWARD_050",
+ "POKEDEX_REWARD_051",
+ "POKEDEX_REWARD_052",
+ "POKEDEX_REWARD_053",
+ "POKEDEX_REWARD_054",
+ "POKEDEX_REWARD_055",
+ "POKEDEX_REWARD_056",
+ "POKEDEX_REWARD_057",
+ "POKEDEX_REWARD_058",
+ "POKEDEX_REWARD_059",
+ "POKEDEX_REWARD_060",
+ "POKEDEX_REWARD_061",
+ "POKEDEX_REWARD_062",
+ "POKEDEX_REWARD_063",
+ "POKEDEX_REWARD_064",
+ "POKEDEX_REWARD_065",
+ "POKEDEX_REWARD_066",
+ "POKEDEX_REWARD_067",
+ "POKEDEX_REWARD_068",
+ "POKEDEX_REWARD_069",
+ "POKEDEX_REWARD_070",
+ "POKEDEX_REWARD_071",
+ "POKEDEX_REWARD_072",
+ "POKEDEX_REWARD_073",
+ "POKEDEX_REWARD_074",
+ "POKEDEX_REWARD_075",
+ "POKEDEX_REWARD_076",
+ "POKEDEX_REWARD_077",
+ "POKEDEX_REWARD_078",
+ "POKEDEX_REWARD_079",
+ "POKEDEX_REWARD_080",
+ "POKEDEX_REWARD_081",
+ "POKEDEX_REWARD_082",
+ "POKEDEX_REWARD_083",
+ "POKEDEX_REWARD_084",
+ "POKEDEX_REWARD_085",
+ "POKEDEX_REWARD_086",
+ "POKEDEX_REWARD_087",
+ "POKEDEX_REWARD_088",
+ "POKEDEX_REWARD_089",
+ "POKEDEX_REWARD_090",
+ "POKEDEX_REWARD_091",
+ "POKEDEX_REWARD_092",
+ "POKEDEX_REWARD_093",
+ "POKEDEX_REWARD_094",
+ "POKEDEX_REWARD_095",
+ "POKEDEX_REWARD_096",
+ "POKEDEX_REWARD_097",
+ "POKEDEX_REWARD_098",
+ "POKEDEX_REWARD_099",
+ "POKEDEX_REWARD_100",
+ "POKEDEX_REWARD_101",
+ "POKEDEX_REWARD_102",
+ "POKEDEX_REWARD_103",
+ "POKEDEX_REWARD_104",
+ "POKEDEX_REWARD_105",
+ "POKEDEX_REWARD_106",
+ "POKEDEX_REWARD_107",
+ "POKEDEX_REWARD_108",
+ "POKEDEX_REWARD_109",
+ "POKEDEX_REWARD_110",
+ "POKEDEX_REWARD_111",
+ "POKEDEX_REWARD_112",
+ "POKEDEX_REWARD_113",
+ "POKEDEX_REWARD_114",
+ "POKEDEX_REWARD_115",
+ "POKEDEX_REWARD_116",
+ "POKEDEX_REWARD_117",
+ "POKEDEX_REWARD_118",
+ "POKEDEX_REWARD_119",
+ "POKEDEX_REWARD_120",
+ "POKEDEX_REWARD_121",
+ "POKEDEX_REWARD_122",
+ "POKEDEX_REWARD_123",
+ "POKEDEX_REWARD_124",
+ "POKEDEX_REWARD_125",
+ "POKEDEX_REWARD_126",
+ "POKEDEX_REWARD_127",
+ "POKEDEX_REWARD_128",
+ "POKEDEX_REWARD_129",
+ "POKEDEX_REWARD_130",
+ "POKEDEX_REWARD_131",
+ "POKEDEX_REWARD_132",
+ "POKEDEX_REWARD_133",
+ "POKEDEX_REWARD_134",
+ "POKEDEX_REWARD_135",
+ "POKEDEX_REWARD_136",
+ "POKEDEX_REWARD_137",
+ "POKEDEX_REWARD_138",
+ "POKEDEX_REWARD_139",
+ "POKEDEX_REWARD_140",
+ "POKEDEX_REWARD_141",
+ "POKEDEX_REWARD_142",
+ "POKEDEX_REWARD_143",
+ "POKEDEX_REWARD_144",
+ "POKEDEX_REWARD_145",
+ "POKEDEX_REWARD_146",
+ "POKEDEX_REWARD_147",
+ "POKEDEX_REWARD_148",
+ "POKEDEX_REWARD_149",
+ "POKEDEX_REWARD_150",
+ "POKEDEX_REWARD_151",
+ "POKEDEX_REWARD_152",
+ "POKEDEX_REWARD_153",
+ "POKEDEX_REWARD_154",
+ "POKEDEX_REWARD_155",
+ "POKEDEX_REWARD_156",
+ "POKEDEX_REWARD_157",
+ "POKEDEX_REWARD_158",
+ "POKEDEX_REWARD_159",
+ "POKEDEX_REWARD_160",
+ "POKEDEX_REWARD_161",
+ "POKEDEX_REWARD_162",
+ "POKEDEX_REWARD_163",
+ "POKEDEX_REWARD_164",
+ "POKEDEX_REWARD_165",
+ "POKEDEX_REWARD_166",
+ "POKEDEX_REWARD_167",
+ "POKEDEX_REWARD_168",
+ "POKEDEX_REWARD_169",
+ "POKEDEX_REWARD_170",
+ "POKEDEX_REWARD_171",
+ "POKEDEX_REWARD_172",
+ "POKEDEX_REWARD_173",
+ "POKEDEX_REWARD_174",
+ "POKEDEX_REWARD_175",
+ "POKEDEX_REWARD_176",
+ "POKEDEX_REWARD_177",
+ "POKEDEX_REWARD_178",
+ "POKEDEX_REWARD_179",
+ "POKEDEX_REWARD_180",
+ "POKEDEX_REWARD_181",
+ "POKEDEX_REWARD_182",
+ "POKEDEX_REWARD_183",
+ "POKEDEX_REWARD_184",
+ "POKEDEX_REWARD_185",
+ "POKEDEX_REWARD_186",
+ "POKEDEX_REWARD_187",
+ "POKEDEX_REWARD_188",
+ "POKEDEX_REWARD_189",
+ "POKEDEX_REWARD_190",
+ "POKEDEX_REWARD_191",
+ "POKEDEX_REWARD_192",
+ "POKEDEX_REWARD_193",
+ "POKEDEX_REWARD_194",
+ "POKEDEX_REWARD_195",
+ "POKEDEX_REWARD_196",
+ "POKEDEX_REWARD_197",
+ "POKEDEX_REWARD_198",
+ "POKEDEX_REWARD_199",
+ "POKEDEX_REWARD_200",
+ "POKEDEX_REWARD_201",
+ "POKEDEX_REWARD_202",
+ "POKEDEX_REWARD_203",
+ "POKEDEX_REWARD_204",
+ "POKEDEX_REWARD_205",
+ "POKEDEX_REWARD_206",
+ "POKEDEX_REWARD_207",
+ "POKEDEX_REWARD_208",
+ "POKEDEX_REWARD_209",
+ "POKEDEX_REWARD_210",
+ "POKEDEX_REWARD_211",
+ "POKEDEX_REWARD_212",
+ "POKEDEX_REWARD_213",
+ "POKEDEX_REWARD_214",
+ "POKEDEX_REWARD_215",
+ "POKEDEX_REWARD_216",
+ "POKEDEX_REWARD_217",
+ "POKEDEX_REWARD_218",
+ "POKEDEX_REWARD_219",
+ "POKEDEX_REWARD_220",
+ "POKEDEX_REWARD_221",
+ "POKEDEX_REWARD_222",
+ "POKEDEX_REWARD_223",
+ "POKEDEX_REWARD_224",
+ "POKEDEX_REWARD_225",
+ "POKEDEX_REWARD_226",
+ "POKEDEX_REWARD_227",
+ "POKEDEX_REWARD_228",
+ "POKEDEX_REWARD_229",
+ "POKEDEX_REWARD_230",
+ "POKEDEX_REWARD_231",
+ "POKEDEX_REWARD_232",
+ "POKEDEX_REWARD_233",
+ "POKEDEX_REWARD_234",
+ "POKEDEX_REWARD_235",
+ "POKEDEX_REWARD_236",
+ "POKEDEX_REWARD_237",
+ "POKEDEX_REWARD_238",
+ "POKEDEX_REWARD_239",
+ "POKEDEX_REWARD_240",
+ "POKEDEX_REWARD_241",
+ "POKEDEX_REWARD_242",
+ "POKEDEX_REWARD_243",
+ "POKEDEX_REWARD_244",
+ "POKEDEX_REWARD_245",
+ "POKEDEX_REWARD_246",
+ "POKEDEX_REWARD_247",
+ "POKEDEX_REWARD_248",
+ "POKEDEX_REWARD_249",
+ "POKEDEX_REWARD_250",
+ "POKEDEX_REWARD_251",
+ "POKEDEX_REWARD_252",
+ "POKEDEX_REWARD_253",
+ "POKEDEX_REWARD_254",
+ "POKEDEX_REWARD_255",
+ "POKEDEX_REWARD_256",
+ "POKEDEX_REWARD_257",
+ "POKEDEX_REWARD_258",
+ "POKEDEX_REWARD_259",
+ "POKEDEX_REWARD_260",
+ "POKEDEX_REWARD_261",
+ "POKEDEX_REWARD_262",
+ "POKEDEX_REWARD_263",
+ "POKEDEX_REWARD_264",
+ "POKEDEX_REWARD_265",
+ "POKEDEX_REWARD_266",
+ "POKEDEX_REWARD_267",
+ "POKEDEX_REWARD_268",
+ "POKEDEX_REWARD_269",
+ "POKEDEX_REWARD_270",
+ "POKEDEX_REWARD_271",
+ "POKEDEX_REWARD_272",
+ "POKEDEX_REWARD_273",
+ "POKEDEX_REWARD_274",
+ "POKEDEX_REWARD_275",
+ "POKEDEX_REWARD_276",
+ "POKEDEX_REWARD_277",
+ "POKEDEX_REWARD_278",
+ "POKEDEX_REWARD_279",
+ "POKEDEX_REWARD_280",
+ "POKEDEX_REWARD_281",
+ "POKEDEX_REWARD_282",
+ "POKEDEX_REWARD_283",
+ "POKEDEX_REWARD_284",
+ "POKEDEX_REWARD_285",
+ "POKEDEX_REWARD_286",
+ "POKEDEX_REWARD_287",
+ "POKEDEX_REWARD_288",
+ "POKEDEX_REWARD_289",
+ "POKEDEX_REWARD_290",
+ "POKEDEX_REWARD_291",
+ "POKEDEX_REWARD_292",
+ "POKEDEX_REWARD_293",
+ "POKEDEX_REWARD_294",
+ "POKEDEX_REWARD_295",
+ "POKEDEX_REWARD_296",
+ "POKEDEX_REWARD_297",
+ "POKEDEX_REWARD_298",
+ "POKEDEX_REWARD_299",
+ "POKEDEX_REWARD_300",
+ "POKEDEX_REWARD_301",
+ "POKEDEX_REWARD_302",
+ "POKEDEX_REWARD_303",
+ "POKEDEX_REWARD_304",
+ "POKEDEX_REWARD_305",
+ "POKEDEX_REWARD_306",
+ "POKEDEX_REWARD_307",
+ "POKEDEX_REWARD_308",
+ "POKEDEX_REWARD_309",
+ "POKEDEX_REWARD_310",
+ "POKEDEX_REWARD_311",
+ "POKEDEX_REWARD_312",
+ "POKEDEX_REWARD_313",
+ "POKEDEX_REWARD_314",
+ "POKEDEX_REWARD_315",
+ "POKEDEX_REWARD_316",
+ "POKEDEX_REWARD_317",
+ "POKEDEX_REWARD_318",
+ "POKEDEX_REWARD_319",
+ "POKEDEX_REWARD_320",
+ "POKEDEX_REWARD_321",
+ "POKEDEX_REWARD_322",
+ "POKEDEX_REWARD_323",
+ "POKEDEX_REWARD_324",
+ "POKEDEX_REWARD_325",
+ "POKEDEX_REWARD_326",
+ "POKEDEX_REWARD_327",
+ "POKEDEX_REWARD_328",
+ "POKEDEX_REWARD_329",
+ "POKEDEX_REWARD_330",
+ "POKEDEX_REWARD_331",
+ "POKEDEX_REWARD_332",
+ "POKEDEX_REWARD_333",
+ "POKEDEX_REWARD_334",
+ "POKEDEX_REWARD_335",
+ "POKEDEX_REWARD_336",
+ "POKEDEX_REWARD_337",
+ "POKEDEX_REWARD_338",
+ "POKEDEX_REWARD_339",
+ "POKEDEX_REWARD_340",
+ "POKEDEX_REWARD_341",
+ "POKEDEX_REWARD_342",
+ "POKEDEX_REWARD_343",
+ "POKEDEX_REWARD_344",
+ "POKEDEX_REWARD_345",
+ "POKEDEX_REWARD_346",
+ "POKEDEX_REWARD_347",
+ "POKEDEX_REWARD_348",
+ "POKEDEX_REWARD_349",
+ "POKEDEX_REWARD_350",
+ "POKEDEX_REWARD_351",
+ "POKEDEX_REWARD_352",
+ "POKEDEX_REWARD_353",
+ "POKEDEX_REWARD_354",
+ "POKEDEX_REWARD_355",
+ "POKEDEX_REWARD_356",
+ "POKEDEX_REWARD_357",
+ "POKEDEX_REWARD_358",
+ "POKEDEX_REWARD_359",
+ "POKEDEX_REWARD_360",
+ "POKEDEX_REWARD_361",
+ "POKEDEX_REWARD_362",
+ "POKEDEX_REWARD_363",
+ "POKEDEX_REWARD_364",
+ "POKEDEX_REWARD_365",
+ "POKEDEX_REWARD_366",
+ "POKEDEX_REWARD_367",
+ "POKEDEX_REWARD_368",
+ "POKEDEX_REWARD_369",
+ "POKEDEX_REWARD_370",
+ "POKEDEX_REWARD_371",
+ "POKEDEX_REWARD_372",
+ "POKEDEX_REWARD_373",
+ "POKEDEX_REWARD_374",
+ "POKEDEX_REWARD_375",
+ "POKEDEX_REWARD_376",
+ "POKEDEX_REWARD_377",
+ "POKEDEX_REWARD_378",
+ "POKEDEX_REWARD_379",
+ "POKEDEX_REWARD_380",
+ "POKEDEX_REWARD_381",
+ "POKEDEX_REWARD_382",
+ "POKEDEX_REWARD_383",
+ "POKEDEX_REWARD_384",
+ "POKEDEX_REWARD_385",
+ "POKEDEX_REWARD_386"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_SKY": {
+ "parent_map": "MAP_LITTLEROOT_TOWN",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LITTLEROOT_TOWN/MAIN",
+ "REGION_OLDALE_TOWN/MAIN",
+ "REGION_PETALBURG_CITY/MAIN",
+ "REGION_RUSTBORO_CITY/MAIN",
+ "REGION_DEWFORD_TOWN/MAIN",
+ "REGION_SLATEPORT_CITY/MAIN",
+ "REGION_MAUVILLE_CITY/MAIN",
+ "REGION_VERDANTURF_TOWN/MAIN",
+ "REGION_FALLARBOR_TOWN/MAIN",
+ "REGION_LAVARIDGE_TOWN/MAIN",
+ "REGION_FORTREE_CITY/MAIN",
+ "REGION_LILYCOVE_CITY/MAIN",
+ "REGION_MOSSDEEP_CITY/MAIN",
+ "REGION_SOOTOPOLIS_CITY/EAST",
+ "REGION_EVER_GRANDE_CITY/SOUTH"
+ ],
+ "warps": []
+ },
+
+ "REGION_LITTLEROOT_TOWN/MAIN": {
+ "parent_map": "MAP_LITTLEROOT_TOWN",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_VISITED_LITTLEROOT_TOWN",
+ "FREE_FLY_LOCATION",
+ "TERRA_CAVE_LOCATION",
+ "MARINE_CAVE_LOCATION"
+ ],
+ "exits": [
+ "REGION_ROUTE101/MAIN",
+ "REGION_POKEDEX",
+ "REGION_SKY"
+ ],
+ "warps": [
+ "MAP_LITTLEROOT_TOWN:0/MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F:1",
+ "MAP_LITTLEROOT_TOWN:1/MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F:1",
+ "MAP_LITTLEROOT_TOWN:2/MAP_LITTLEROOT_TOWN_PROFESSOR_BIRCHS_LAB:0"
+ ]
+ },
+ "REGION_LITTLEROOT_TOWN_MAYS_HOUSE_1F/MAIN": {
+ "parent_map": "MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F:0,1/MAP_LITTLEROOT_TOWN:0",
+ "MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F:2/MAP_LITTLEROOT_TOWN_MAYS_HOUSE_2F:0"
+ ]
+ },
+ "REGION_LITTLEROOT_TOWN_MAYS_HOUSE_2F/MAIN": {
+ "parent_map": "MAP_LITTLEROOT_TOWN_MAYS_HOUSE_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LITTLEROOT_TOWN_MAYS_HOUSE_2F:0/MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F:2"
+ ]
+ },
+ "REGION_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F/MAIN": {
+ "parent_map": "MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_AMULET_COIN",
+ "NPC_GIFT_RECEIVED_SS_TICKET",
+ "NPC_GIFT_RECEIVED_AURORA_TICKET",
+ "NPC_GIFT_RECEIVED_EON_TICKET",
+ "NPC_GIFT_RECEIVED_MYSTIC_TICKET",
+ "NPC_GIFT_RECEIVED_OLD_SEA_MAP"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F:0,1/MAP_LITTLEROOT_TOWN:1",
+ "MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F:2/MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F:0"
+ ]
+ },
+ "REGION_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F/MAIN": {
+ "parent_map": "MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F:0/MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F:2"
+ ]
+ },
+ "REGION_LITTLEROOT_TOWN_PROFESSOR_BIRCHS_LAB/MAIN": {
+ "parent_map": "MAP_LITTLEROOT_TOWN_PROFESSOR_BIRCHS_LAB",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_FIRST_POKEBALLS"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LITTLEROOT_TOWN_PROFESSOR_BIRCHS_LAB:0,1/MAP_LITTLEROOT_TOWN:2"
+ ]
+ },
+
+ "REGION_OLDALE_TOWN/MAIN": {
+ "parent_map": "MAP_OLDALE_TOWN",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_POTION_OLDALE"
+ ],
+ "events": [
+ "EVENT_VISITED_OLDALE_TOWN"
+ ],
+ "exits": [
+ "REGION_ROUTE101/MAIN",
+ "REGION_ROUTE102/MAIN",
+ "REGION_ROUTE103/WEST"
+ ],
+ "warps": [
+ "MAP_OLDALE_TOWN:0/MAP_OLDALE_TOWN_HOUSE1:0",
+ "MAP_OLDALE_TOWN:1/MAP_OLDALE_TOWN_HOUSE2:0",
+ "MAP_OLDALE_TOWN:2/MAP_OLDALE_TOWN_POKEMON_CENTER_1F:0",
+ "MAP_OLDALE_TOWN:3/MAP_OLDALE_TOWN_MART:0"
+ ]
+ },
+ "REGION_OLDALE_TOWN_HOUSE1/MAIN": {
+ "parent_map": "MAP_OLDALE_TOWN_HOUSE1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_OLDALE_TOWN_HOUSE1:0,1/MAP_OLDALE_TOWN:0"
+ ]
+ },
+ "REGION_OLDALE_TOWN_HOUSE2/MAIN": {
+ "parent_map": "MAP_OLDALE_TOWN_HOUSE2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_OLDALE_TOWN_HOUSE2:0,1/MAP_OLDALE_TOWN:1"
+ ]
+ },
+ "REGION_OLDALE_TOWN_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_OLDALE_TOWN_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_OLDALE_TOWN_POKEMON_CENTER_1F:0,1/MAP_OLDALE_TOWN:2",
+ "MAP_OLDALE_TOWN_POKEMON_CENTER_1F:2/MAP_OLDALE_TOWN_POKEMON_CENTER_2F:0"
+ ]
+ },
+ "REGION_OLDALE_TOWN_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_OLDALE_TOWN_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_OLDALE_TOWN_POKEMON_CENTER_2F:0/MAP_OLDALE_TOWN_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_OLDALE_TOWN_MART/MAIN": {
+ "parent_map": "MAP_OLDALE_TOWN_MART",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_OLDALE_TOWN_MART:0,1/MAP_OLDALE_TOWN:3"
+ ]
+ },
+
+ "REGION_PETALBURG_CITY/MAIN": {
+ "parent_map": "MAP_PETALBURG_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [
+ "EVENT_VISITED_PETALBURG_CITY"
+ ],
+ "exits": [
+ "REGION_PETALBURG_CITY/SOUTH_POND",
+ "REGION_PETALBURG_CITY/NORTH_POND",
+ "REGION_ROUTE102/MAIN",
+ "REGION_ROUTE104/SOUTH"
+ ],
+ "warps": [
+ "MAP_PETALBURG_CITY:0/MAP_PETALBURG_CITY_HOUSE1:0",
+ "MAP_PETALBURG_CITY:1/MAP_PETALBURG_CITY_WALLYS_HOUSE:0",
+ "MAP_PETALBURG_CITY:2/MAP_PETALBURG_CITY_GYM:0",
+ "MAP_PETALBURG_CITY:3/MAP_PETALBURG_CITY_POKEMON_CENTER_1F:0",
+ "MAP_PETALBURG_CITY:4/MAP_PETALBURG_CITY_HOUSE2:0",
+ "MAP_PETALBURG_CITY:5/MAP_PETALBURG_CITY_MART:0"
+ ]
+ },
+ "REGION_PETALBURG_CITY/NORTH_POND": {
+ "parent_map": "MAP_PETALBURG_CITY",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_PETALBURG_CITY_MAX_REVIVE"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_PETALBURG_CITY/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_PETALBURG_CITY/SOUTH_POND": {
+ "parent_map": "MAP_PETALBURG_CITY",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_PETALBURG_CITY_ETHER",
+ "HIDDEN_ITEM_PETALBURG_CITY_RARE_CANDY"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_PETALBURG_CITY/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_PETALBURG_CITY_HOUSE1/MAIN": {
+ "parent_map": "MAP_PETALBURG_CITY_HOUSE1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_HOUSE1:0,1/MAP_PETALBURG_CITY:0"
+ ]
+ },
+ "REGION_PETALBURG_CITY_HOUSE2/MAIN": {
+ "parent_map": "MAP_PETALBURG_CITY_HOUSE2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_HOUSE2:0,1/MAP_PETALBURG_CITY:4"
+ ]
+ },
+ "REGION_PETALBURG_CITY_WALLYS_HOUSE/MAIN": {
+ "parent_map": "MAP_PETALBURG_CITY_WALLYS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_HM_SURF"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_WALLYS_HOUSE:0,1/MAP_PETALBURG_CITY:1"
+ ]
+ },
+ "REGION_PETALBURG_CITY_GYM/ROOM_1": {
+ "parent_map": "MAP_PETALBURG_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_GYM:0,1/MAP_PETALBURG_CITY:2",
+ "MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3",
+ "MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"
+ ]
+ },
+ "REGION_PETALBURG_CITY_GYM/ROOM_2": {
+ "parent_map": "MAP_PETALBURG_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_MARY_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_GYM:6,7/MAP_PETALBURG_CITY_GYM:5",
+ "MAP_PETALBURG_CITY_GYM:14/MAP_PETALBURG_CITY_GYM:16",
+ "MAP_PETALBURG_CITY_GYM:15/MAP_PETALBURG_CITY_GYM:18"
+ ]
+ },
+ "REGION_PETALBURG_CITY_GYM/ROOM_3": {
+ "parent_map": "MAP_PETALBURG_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_RANDALL_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_GYM:3,4/MAP_PETALBURG_CITY_GYM:2",
+ "MAP_PETALBURG_CITY_GYM:8/MAP_PETALBURG_CITY_GYM:10",
+ "MAP_PETALBURG_CITY_GYM:9/MAP_PETALBURG_CITY_GYM:12"
+ ]
+ },
+ "REGION_PETALBURG_CITY_GYM/ROOM_4": {
+ "parent_map": "MAP_PETALBURG_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GEORGE_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_GYM:18,19/MAP_PETALBURG_CITY_GYM:15",
+ "MAP_PETALBURG_CITY_GYM:23/MAP_PETALBURG_CITY_GYM:30"
+ ]
+ },
+ "REGION_PETALBURG_CITY_GYM/ROOM_5": {
+ "parent_map": "MAP_PETALBURG_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_ALEXIA_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_GYM:12,13/MAP_PETALBURG_CITY_GYM:9",
+ "MAP_PETALBURG_CITY_GYM:16,17/MAP_PETALBURG_CITY_GYM:14",
+ "MAP_PETALBURG_CITY_GYM:21/MAP_PETALBURG_CITY_GYM:26",
+ "MAP_PETALBURG_CITY_GYM:22/MAP_PETALBURG_CITY_GYM:28"
+ ]
+ },
+ "REGION_PETALBURG_CITY_GYM/ROOM_6": {
+ "parent_map": "MAP_PETALBURG_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_PARKER_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_GYM:10,11/MAP_PETALBURG_CITY_GYM:8",
+ "MAP_PETALBURG_CITY_GYM:20/MAP_PETALBURG_CITY_GYM:24"
+ ]
+ },
+ "REGION_PETALBURG_CITY_GYM/ROOM_7": {
+ "parent_map": "MAP_PETALBURG_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_BERKE_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_GYM:28,29/MAP_PETALBURG_CITY_GYM:22",
+ "MAP_PETALBURG_CITY_GYM:30,31/MAP_PETALBURG_CITY_GYM:23",
+ "MAP_PETALBURG_CITY_GYM:33/MAP_PETALBURG_CITY_GYM:36"
+ ]
+ },
+ "REGION_PETALBURG_CITY_GYM/ROOM_8": {
+ "parent_map": "MAP_PETALBURG_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_JODY_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_GYM:24,25/MAP_PETALBURG_CITY_GYM:20",
+ "MAP_PETALBURG_CITY_GYM:26,27/MAP_PETALBURG_CITY_GYM:21",
+ "MAP_PETALBURG_CITY_GYM:32/MAP_PETALBURG_CITY_GYM:34"
+ ]
+ },
+ "REGION_PETALBURG_CITY_GYM/ROOM_9": {
+ "parent_map": "MAP_PETALBURG_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_FACADE",
+ "BADGE_5",
+ "TRAINER_NORMAN_1_REWARD"
+ ],
+ "events": [
+ "EVENT_DEFEAT_NORMAN"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_GYM:34,35/MAP_PETALBURG_CITY_GYM:32",
+ "MAP_PETALBURG_CITY_GYM:36,37/MAP_PETALBURG_CITY_GYM:33"
+ ]
+ },
+ "REGION_PETALBURG_CITY_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_PETALBURG_CITY_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_POKEMON_CENTER_1F:0,1/MAP_PETALBURG_CITY:3",
+ "MAP_PETALBURG_CITY_POKEMON_CENTER_1F:2/MAP_PETALBURG_CITY_POKEMON_CENTER_2F:0"
+ ]
+ },
+ "REGION_PETALBURG_CITY_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_PETALBURG_CITY_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_POKEMON_CENTER_2F:0/MAP_PETALBURG_CITY_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_PETALBURG_CITY_MART/MAIN": {
+ "parent_map": "MAP_PETALBURG_CITY_MART",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PETALBURG_CITY_MART:0,1/MAP_PETALBURG_CITY:5"
+ ]
+ },
+
+ "REGION_RUSTBORO_CITY/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_RUSTBORO_CITY_X_DEFEND",
+ "NPC_GIFT_RECEIVED_GREAT_BALL_RUSTBORO_CITY",
+ "TRAINER_BRENDAN_RUSTBORO_MUDKIP_REWARD"
+ ],
+ "events": [
+ "EVENT_RETURN_DEVON_GOODS",
+ "EVENT_VISITED_RUSTBORO_CITY"
+ ],
+ "exits": [
+ "REGION_ROUTE104/NORTH",
+ "REGION_ROUTE115/SOUTH_BELOW_LEDGE",
+ "REGION_ROUTE116/WEST"
+ ],
+ "warps": [
+ "MAP_RUSTBORO_CITY:0/MAP_RUSTBORO_CITY_GYM:0",
+ "MAP_RUSTBORO_CITY:1/MAP_RUSTBORO_CITY_FLAT1_1F:0",
+ "MAP_RUSTBORO_CITY:2/MAP_RUSTBORO_CITY_MART:0",
+ "MAP_RUSTBORO_CITY:3/MAP_RUSTBORO_CITY_POKEMON_CENTER_1F:0",
+ "MAP_RUSTBORO_CITY:4/MAP_RUSTBORO_CITY_POKEMON_SCHOOL:0",
+ "MAP_RUSTBORO_CITY:5,6/MAP_RUSTBORO_CITY_DEVON_CORP_1F:0,1",
+ "MAP_RUSTBORO_CITY:7/MAP_RUSTBORO_CITY_HOUSE1:0",
+ "MAP_RUSTBORO_CITY:8/MAP_RUSTBORO_CITY_CUTTERS_HOUSE:0",
+ "MAP_RUSTBORO_CITY:9/MAP_RUSTBORO_CITY_HOUSE2:0",
+ "MAP_RUSTBORO_CITY:10/MAP_RUSTBORO_CITY_FLAT2_1F:0",
+ "MAP_RUSTBORO_CITY:11/MAP_RUSTBORO_CITY_HOUSE3:0"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_GYM/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_ROCK_TOMB",
+ "BADGE_1",
+ "TRAINER_JOSH_REWARD",
+ "TRAINER_TOMMY_REWARD",
+ "TRAINER_MARC_REWARD",
+ "TRAINER_ROXANNE_1_REWARD"
+ ],
+ "events": [
+ "EVENT_DEFEAT_ROXANNE"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_GYM:0,1/MAP_RUSTBORO_CITY:0"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_MART/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_MART",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_MART:0,1/MAP_RUSTBORO_CITY:2"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_POKEMON_CENTER_1F:0,1/MAP_RUSTBORO_CITY:3",
+ "MAP_RUSTBORO_CITY_POKEMON_CENTER_1F:2/MAP_RUSTBORO_CITY_POKEMON_CENTER_2F:0"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_POKEMON_CENTER_2F:0/MAP_RUSTBORO_CITY_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_POKEMON_SCHOOL/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_POKEMON_SCHOOL",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_QUICK_CLAW"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_POKEMON_SCHOOL:0,1/MAP_RUSTBORO_CITY:4"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_DEVON_CORP_1F/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_DEVON_CORP_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_DEVON_CORP_1F:0,1/MAP_RUSTBORO_CITY:5,6",
+ "MAP_RUSTBORO_CITY_DEVON_CORP_1F:2/MAP_RUSTBORO_CITY_DEVON_CORP_2F:0"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_DEVON_CORP_2F/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_DEVON_CORP_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_DEVON_CORP_2F:0/MAP_RUSTBORO_CITY_DEVON_CORP_1F:2",
+ "MAP_RUSTBORO_CITY_DEVON_CORP_2F:1/MAP_RUSTBORO_CITY_DEVON_CORP_3F:0"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_DEVON_CORP_3F/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_DEVON_CORP_3F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_LETTER",
+ "NPC_GIFT_RECEIVED_EXP_SHARE"
+ ],
+ "events": [
+ "EVENT_TALK_TO_MR_STONE"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_DEVON_CORP_3F:0/MAP_RUSTBORO_CITY_DEVON_CORP_2F:1"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_FLAT1_1F/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_FLAT1_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_FLAT1_1F:0,1/MAP_RUSTBORO_CITY:1",
+ "MAP_RUSTBORO_CITY_FLAT1_1F:2/MAP_RUSTBORO_CITY_FLAT1_2F:0"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_FLAT1_2F/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_FLAT1_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_FLAT1_2F:0/MAP_RUSTBORO_CITY_FLAT1_1F:2"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_FLAT2_1F/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_FLAT2_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_FLAT2_1F:0,1/MAP_RUSTBORO_CITY:10",
+ "MAP_RUSTBORO_CITY_FLAT2_1F:2/MAP_RUSTBORO_CITY_FLAT2_2F:0"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_FLAT2_2F/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_FLAT2_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_PREMIER_BALL_RUSTBORO"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_FLAT2_2F:0/MAP_RUSTBORO_CITY_FLAT2_1F:2",
+ "MAP_RUSTBORO_CITY_FLAT2_2F:1/MAP_RUSTBORO_CITY_FLAT2_3F:0"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_FLAT2_3F/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_FLAT2_3F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_FLAT2_3F:0/MAP_RUSTBORO_CITY_FLAT2_2F:1"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_HOUSE1/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_HOUSE1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_HOUSE1:0,1/MAP_RUSTBORO_CITY:7"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_HOUSE2/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_HOUSE2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_HOUSE2:0,1/MAP_RUSTBORO_CITY:9"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_HOUSE3/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_HOUSE3",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_HOUSE3:0,1/MAP_RUSTBORO_CITY:11"
+ ]
+ },
+ "REGION_RUSTBORO_CITY_CUTTERS_HOUSE/MAIN": {
+ "parent_map": "MAP_RUSTBORO_CITY_CUTTERS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_HM_CUT"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_RUSTBORO_CITY_CUTTERS_HOUSE:0,1/MAP_RUSTBORO_CITY:8"
+ ]
+ },
+
+ "REGION_DEWFORD_TOWN/MAIN": {
+ "parent_map": "MAP_DEWFORD_TOWN",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "NPC_GIFT_RECEIVED_OLD_ROD"
+ ],
+ "events": [
+ "EVENT_VISITED_DEWFORD_TOWN"
+ ],
+ "exits": [
+ "REGION_DEWFORD_TOWN/WATER",
+ "REGION_ROUTE106/EAST",
+ "REGION_ROUTE107/MAIN",
+ "REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN",
+ "REGION_ROUTE109/BEACH"
+ ],
+ "warps": [
+ "MAP_DEWFORD_TOWN:0/MAP_DEWFORD_TOWN_HALL:0",
+ "MAP_DEWFORD_TOWN:1/MAP_DEWFORD_TOWN_POKEMON_CENTER_1F:0",
+ "MAP_DEWFORD_TOWN:2/MAP_DEWFORD_TOWN_GYM:0",
+ "MAP_DEWFORD_TOWN:3/MAP_DEWFORD_TOWN_HOUSE1:0",
+ "MAP_DEWFORD_TOWN:4/MAP_DEWFORD_TOWN_HOUSE2:0"
+ ]
+ },
+ "REGION_DEWFORD_TOWN/WATER": {
+ "parent_map": "MAP_DEWFORD_TOWN",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_DEWFORD_TOWN_HALL/MAIN": {
+ "parent_map": "MAP_DEWFORD_TOWN_HALL",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_SLUDGE_BOMB"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_DEWFORD_TOWN_HALL:0,1/MAP_DEWFORD_TOWN:0"
+ ]
+ },
+ "REGION_DEWFORD_TOWN_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_DEWFORD_TOWN_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_DEWFORD_TOWN_POKEMON_CENTER_1F:0,1/MAP_DEWFORD_TOWN:1",
+ "MAP_DEWFORD_TOWN_POKEMON_CENTER_1F:2/MAP_DEWFORD_TOWN_POKEMON_CENTER_2F:0"
+ ]
+ },
+ "REGION_DEWFORD_TOWN_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_DEWFORD_TOWN_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_DEWFORD_TOWN_POKEMON_CENTER_2F:0/MAP_DEWFORD_TOWN_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_DEWFORD_TOWN_GYM/MAIN": {
+ "parent_map": "MAP_DEWFORD_TOWN_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_BULK_UP",
+ "BADGE_2",
+ "TRAINER_LAURA_REWARD",
+ "TRAINER_LILITH_REWARD",
+ "TRAINER_BRENDEN_REWARD",
+ "TRAINER_TAKAO_REWARD",
+ "TRAINER_JOCELYN_REWARD",
+ "TRAINER_CRISTIAN_REWARD",
+ "TRAINER_BRAWLY_1_REWARD"
+ ],
+ "events": [
+ "EVENT_DEFEAT_BRAWLY"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_DEWFORD_TOWN_GYM:0,1/MAP_DEWFORD_TOWN:2"
+ ]
+ },
+ "REGION_DEWFORD_TOWN_HOUSE1/MAIN": {
+ "parent_map": "MAP_DEWFORD_TOWN_HOUSE1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_DEWFORD_TOWN_HOUSE1:0,1/MAP_DEWFORD_TOWN:3"
+ ]
+ },
+ "REGION_DEWFORD_TOWN_HOUSE2/MAIN": {
+ "parent_map": "MAP_DEWFORD_TOWN_HOUSE2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_SILK_SCARF"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_DEWFORD_TOWN_HOUSE2:0,1/MAP_DEWFORD_TOWN:4"
+ ]
+ },
+
+ "REGION_SLATEPORT_CITY/MAIN": {
+ "parent_map": "MAP_SLATEPORT_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "NPC_GIFT_RECEIVED_POWDER_JAR"
+ ],
+ "events": [
+ "EVENT_AQUA_STEALS_SUBMARINE",
+ "EVENT_VISITED_SLATEPORT_CITY"
+ ],
+ "exits": [
+ "REGION_SLATEPORT_CITY/WATER",
+ "REGION_ROUTE109/BEACH",
+ "REGION_ROUTE110/SOUTH"
+ ],
+ "warps": [
+ "MAP_SLATEPORT_CITY:0/MAP_SLATEPORT_CITY_POKEMON_CENTER_1F:0",
+ "MAP_SLATEPORT_CITY:1/MAP_SLATEPORT_CITY_MART:0",
+ "MAP_SLATEPORT_CITY:2/MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F:0",
+ "MAP_SLATEPORT_CITY:3/MAP_SLATEPORT_CITY_BATTLE_TENT_LOBBY:0",
+ "MAP_SLATEPORT_CITY:4/MAP_SLATEPORT_CITY_POKEMON_FAN_CLUB:0",
+ "MAP_SLATEPORT_CITY:5,7/MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:0,1",
+ "MAP_SLATEPORT_CITY:6/MAP_SLATEPORT_CITY_NAME_RATERS_HOUSE:0",
+ "MAP_SLATEPORT_CITY:8/MAP_SLATEPORT_CITY_HARBOR:0",
+ "MAP_SLATEPORT_CITY:9/MAP_SLATEPORT_CITY_HARBOR:2",
+ "MAP_SLATEPORT_CITY:10/MAP_SLATEPORT_CITY_HOUSE:0"
+ ]
+ },
+ "REGION_SLATEPORT_CITY/WATER": {
+ "parent_map": "MAP_SLATEPORT_CITY",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SLATEPORT_CITY/MAIN",
+ "REGION_ROUTE134/WEST"
+ ],
+ "warps": []
+ },
+ "REGION_SLATEPORT_CITY_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_SLATEPORT_CITY_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SLATEPORT_CITY_POKEMON_CENTER_2F:0/MAP_SLATEPORT_CITY_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_SLATEPORT_CITY_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_SLATEPORT_CITY_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SLATEPORT_CITY_POKEMON_CENTER_1F:0,1/MAP_SLATEPORT_CITY:0",
+ "MAP_SLATEPORT_CITY_POKEMON_CENTER_1F:2/MAP_SLATEPORT_CITY_POKEMON_CENTER_2F:0"
+ ]
+ },
+ "REGION_SLATEPORT_CITY_MART/MAIN": {
+ "parent_map": "MAP_SLATEPORT_CITY_MART",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_BUY_HARBOR_MAIL"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_SLATEPORT_CITY_MART:0,1/MAP_SLATEPORT_CITY:1"
+ ]
+ },
+ "REGION_SLATEPORT_CITY_STERNS_SHIPYARD_1F/MAIN": {
+ "parent_map": "MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_TALK_TO_DOCK"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F:0,1/MAP_SLATEPORT_CITY:2",
+ "MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F:2/MAP_SLATEPORT_CITY_STERNS_SHIPYARD_2F:0"
+ ]
+ },
+ "REGION_SLATEPORT_CITY_STERNS_SHIPYARD_2F/MAIN": {
+ "parent_map": "MAP_SLATEPORT_CITY_STERNS_SHIPYARD_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SLATEPORT_CITY_STERNS_SHIPYARD_2F:0/MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F:2"
+ ]
+ },
+ "REGION_SLATEPORT_CITY_BATTLE_TENT_LOBBY/MAIN": {
+ "parent_map": "MAP_SLATEPORT_CITY_BATTLE_TENT_LOBBY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_TORMENT"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SLATEPORT_CITY_BATTLE_TENT_LOBBY:0,1/MAP_SLATEPORT_CITY:3"
+ ]
+ },
+ "REGION_SLATEPORT_CITY_POKEMON_FAN_CLUB/MAIN": {
+ "parent_map": "MAP_SLATEPORT_CITY_POKEMON_FAN_CLUB",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_SOOTHE_BELL"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SLATEPORT_CITY_POKEMON_FAN_CLUB:0,1/MAP_SLATEPORT_CITY:4"
+ ]
+ },
+ "REGION_SLATEPORT_CITY_OCEANIC_MUSEUM_1F/MAIN": {
+ "parent_map": "MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_THIEF"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:0,1/MAP_SLATEPORT_CITY:5,7",
+ "MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:2/MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_2F:0"
+ ]
+ },
+ "REGION_SLATEPORT_CITY_OCEANIC_MUSEUM_2F/MAIN": {
+ "parent_map": "MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GRUNT_MUSEUM_1_REWARD",
+ "TRAINER_GRUNT_MUSEUM_2_REWARD"
+ ],
+ "events": [
+ "EVENT_RESCUE_CAPT_STERN"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_2F:0/MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:2"
+ ]
+ },
+ "REGION_SLATEPORT_CITY_NAME_RATERS_HOUSE/MAIN": {
+ "parent_map": "MAP_SLATEPORT_CITY_NAME_RATERS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SLATEPORT_CITY_NAME_RATERS_HOUSE:0,1/MAP_SLATEPORT_CITY:6"
+ ]
+ },
+ "REGION_SLATEPORT_CITY_HARBOR/MAIN": {
+ "parent_map": "MAP_SLATEPORT_CITY_HARBOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_DEEP_SEA_TOOTH",
+ "NPC_GIFT_RECEIVED_DEEP_SEA_SCALE"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_SS_TIDAL_CORRIDOR/MAIN"
+ ],
+ "warps": [
+ "MAP_SLATEPORT_CITY_HARBOR:0,1/MAP_SLATEPORT_CITY:8",
+ "MAP_SLATEPORT_CITY_HARBOR:2,3/MAP_SLATEPORT_CITY:9"
+ ]
+ },
+ "REGION_SLATEPORT_CITY_HOUSE/MAIN": {
+ "parent_map": "MAP_SLATEPORT_CITY_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SLATEPORT_CITY_HOUSE:0,1/MAP_SLATEPORT_CITY:10"
+ ]
+ },
+
+ "REGION_MAUVILLE_CITY/MAIN": {
+ "parent_map": "MAP_MAUVILLE_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_MAUVILLE_CITY_X_SPEED",
+ "NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON",
+ "NPC_GIFT_GOT_TM_THUNDERBOLT_FROM_WATTSON",
+ "TRAINER_WALLY_MAUVILLE_REWARD"
+ ],
+ "events": [
+ "EVENT_VISITED_MAUVILLE_CITY"
+ ],
+ "exits": [
+ "REGION_ROUTE111/SOUTH",
+ "REGION_ROUTE117/MAIN",
+ "REGION_ROUTE110/MAIN",
+ "REGION_ROUTE118/WEST"
+ ],
+ "warps": [
+ "MAP_MAUVILLE_CITY:0/MAP_MAUVILLE_CITY_GYM:0",
+ "MAP_MAUVILLE_CITY:1/MAP_MAUVILLE_CITY_POKEMON_CENTER_1F:0",
+ "MAP_MAUVILLE_CITY:2/MAP_MAUVILLE_CITY_BIKE_SHOP:0",
+ "MAP_MAUVILLE_CITY:3/MAP_MAUVILLE_CITY_MART:0",
+ "MAP_MAUVILLE_CITY:4/MAP_MAUVILLE_CITY_HOUSE1:0",
+ "MAP_MAUVILLE_CITY:5/MAP_MAUVILLE_CITY_GAME_CORNER:0",
+ "MAP_MAUVILLE_CITY:6/MAP_MAUVILLE_CITY_HOUSE2:0"
+ ]
+ },
+ "REGION_MAUVILLE_CITY_GYM/MAIN": {
+ "parent_map": "MAP_MAUVILLE_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_SHOCK_WAVE",
+ "BADGE_3",
+ "TRAINER_VIVIAN_REWARD",
+ "TRAINER_KIRK_REWARD",
+ "TRAINER_BEN_REWARD",
+ "TRAINER_ANGELO_REWARD",
+ "TRAINER_SHAWN_REWARD",
+ "TRAINER_WATTSON_1_REWARD"
+ ],
+ "events": [
+ "EVENT_DEFEAT_WATTSON"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_MAUVILLE_CITY_GYM:0,1/MAP_MAUVILLE_CITY:0"
+ ]
+ },
+ "REGION_MAUVILLE_CITY_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_MAUVILLE_CITY_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MAUVILLE_CITY_POKEMON_CENTER_1F:0,1/MAP_MAUVILLE_CITY:1",
+ "MAP_MAUVILLE_CITY_POKEMON_CENTER_1F:2/MAP_MAUVILLE_CITY_POKEMON_CENTER_2F:0"
+ ]
+ },
+ "REGION_MAUVILLE_CITY_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_MAUVILLE_CITY_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MAUVILLE_CITY_POKEMON_CENTER_2F:0/MAP_MAUVILLE_CITY_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_MAUVILLE_CITY_BIKE_SHOP/MAIN": {
+ "parent_map": "MAP_MAUVILLE_CITY_BIKE_SHOP",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_ACRO_BIKE",
+ "NPC_GIFT_RECEIVED_MACH_BIKE"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MAUVILLE_CITY_BIKE_SHOP:0,1/MAP_MAUVILLE_CITY:2"
+ ]
+ },
+ "REGION_MAUVILLE_CITY_MART/MAIN": {
+ "parent_map": "MAP_MAUVILLE_CITY_MART",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MAUVILLE_CITY_MART:0,1/MAP_MAUVILLE_CITY:3"
+ ]
+ },
+ "REGION_MAUVILLE_CITY_HOUSE1/MAIN": {
+ "parent_map": "MAP_MAUVILLE_CITY_HOUSE1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_HM_ROCK_SMASH"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MAUVILLE_CITY_HOUSE1:0,1/MAP_MAUVILLE_CITY:4"
+ ]
+ },
+ "REGION_MAUVILLE_CITY_HOUSE2/MAIN": {
+ "parent_map": "MAP_MAUVILLE_CITY_HOUSE2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_COIN_CASE"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MAUVILLE_CITY_HOUSE2:0,1/MAP_MAUVILLE_CITY:6"
+ ]
+ },
+ "REGION_MAUVILLE_CITY_GAME_CORNER/MAIN": {
+ "parent_map": "MAP_MAUVILLE_CITY_GAME_CORNER",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MAUVILLE_CITY_GAME_CORNER:0,1/MAP_MAUVILLE_CITY:5"
+ ]
+ },
+
+ "REGION_VERDANTURF_TOWN/MAIN": {
+ "parent_map": "MAP_VERDANTURF_TOWN",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_VISITED_VERDANTURF_TOWN"
+ ],
+ "exits": [
+ "REGION_ROUTE117/MAIN"
+ ],
+ "warps": [
+ "MAP_VERDANTURF_TOWN:0/MAP_VERDANTURF_TOWN_BATTLE_TENT_LOBBY:0",
+ "MAP_VERDANTURF_TOWN:1/MAP_VERDANTURF_TOWN_MART:0",
+ "MAP_VERDANTURF_TOWN:2/MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F:0",
+ "MAP_VERDANTURF_TOWN:3/MAP_VERDANTURF_TOWN_WANDAS_HOUSE:0",
+ "MAP_VERDANTURF_TOWN:4/MAP_RUSTURF_TUNNEL:1",
+ "MAP_VERDANTURF_TOWN:5/MAP_VERDANTURF_TOWN_FRIENDSHIP_RATERS_HOUSE:0",
+ "MAP_VERDANTURF_TOWN:6/MAP_VERDANTURF_TOWN_HOUSE:0"
+ ]
+ },
+ "REGION_VERDANTURF_TOWN_BATTLE_TENT_LOBBY/MAIN": {
+ "parent_map": "MAP_VERDANTURF_TOWN_BATTLE_TENT_LOBBY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_ATTRACT"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_VERDANTURF_TOWN_BATTLE_TENT_LOBBY:0,1/MAP_VERDANTURF_TOWN:0"
+ ]
+ },
+ "REGION_VERDANTURF_TOWN_MART/MAIN": {
+ "parent_map": "MAP_VERDANTURF_TOWN_MART",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_VERDANTURF_TOWN_MART:0,1/MAP_VERDANTURF_TOWN:1"
+ ]
+ },
+ "REGION_VERDANTURF_TOWN_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F:0,1/MAP_VERDANTURF_TOWN:2",
+ "MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F:2/MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F:0"
+ ]
+ },
+ "REGION_VERDANTURF_TOWN_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F:0/MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_VERDANTURF_TOWN_WANDAS_HOUSE/MAIN": {
+ "parent_map": "MAP_VERDANTURF_TOWN_WANDAS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_VERDANTURF_TOWN_WANDAS_HOUSE:0,1/MAP_VERDANTURF_TOWN:3"
+ ]
+ },
+ "REGION_VERDANTURF_TOWN_FRIENDSHIP_RATERS_HOUSE/MAIN": {
+ "parent_map": "MAP_VERDANTURF_TOWN_FRIENDSHIP_RATERS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_VERDANTURF_TOWN_FRIENDSHIP_RATERS_HOUSE:0,1/MAP_VERDANTURF_TOWN:5"
+ ]
+ },
+ "REGION_VERDANTURF_TOWN_HOUSE/MAIN": {
+ "parent_map": "MAP_VERDANTURF_TOWN_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_VERDANTURF_TOWN_HOUSE:0,1/MAP_VERDANTURF_TOWN:6"
+ ]
+ },
+
+ "REGION_FALLARBOR_TOWN/MAIN": {
+ "parent_map": "MAP_FALLARBOR_TOWN",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_FALLARBOR_TOWN_NUGGET"
+ ],
+ "events": [
+ "EVENT_VISITED_FALLARBOR_TOWN"
+ ],
+ "exits": [
+ "REGION_ROUTE114/MAIN",
+ "REGION_ROUTE113/MAIN"
+ ],
+ "warps": [
+ "MAP_FALLARBOR_TOWN:0/MAP_FALLARBOR_TOWN_MART:0",
+ "MAP_FALLARBOR_TOWN:1/MAP_FALLARBOR_TOWN_BATTLE_TENT_LOBBY:0",
+ "MAP_FALLARBOR_TOWN:2/MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F:0",
+ "MAP_FALLARBOR_TOWN:3/MAP_FALLARBOR_TOWN_COZMOS_HOUSE:0",
+ "MAP_FALLARBOR_TOWN:4/MAP_FALLARBOR_TOWN_MOVE_RELEARNERS_HOUSE:0"
+ ]
+ },
+ "REGION_FALLARBOR_TOWN_MART/MAIN": {
+ "parent_map": "MAP_FALLARBOR_TOWN_MART",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_FALLARBOR_TOWN_MART:0,1/MAP_FALLARBOR_TOWN:0"
+ ]
+ },
+ "REGION_FALLARBOR_TOWN_BATTLE_TENT_LOBBY/MAIN": {
+ "parent_map": "MAP_FALLARBOR_TOWN_BATTLE_TENT_LOBBY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_FALLARBOR_TOWN_BATTLE_TENT_LOBBY:0,1/MAP_FALLARBOR_TOWN:1"
+ ]
+ },
+ "REGION_FALLARBOR_TOWN_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F:0,1/MAP_FALLARBOR_TOWN:2",
+ "MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F:2/MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F:0"
+ ]
+ },
+ "REGION_FALLARBOR_TOWN_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F:0/MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_FALLARBOR_TOWN_COZMOS_HOUSE/MAIN": {
+ "parent_map": "MAP_FALLARBOR_TOWN_COZMOS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_RETURN"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_FALLARBOR_TOWN_COZMOS_HOUSE:0,1/MAP_FALLARBOR_TOWN:3"
+ ]
+ },
+ "REGION_FALLARBOR_TOWN_MOVE_RELEARNERS_HOUSE/MAIN": {
+ "parent_map": "MAP_FALLARBOR_TOWN_MOVE_RELEARNERS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_FALLARBOR_TOWN_MOVE_RELEARNERS_HOUSE:0,1/MAP_FALLARBOR_TOWN:4"
+ ]
+ },
+
+ "REGION_LAVARIDGE_TOWN/MAIN": {
+ "parent_map": "MAP_LAVARIDGE_TOWN",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_GO_GOGGLES"
+ ],
+ "events": [
+ "EVENT_VISITED_LAVARIDGE_TOWN"
+ ],
+ "exits": [
+ "REGION_ROUTE112/SOUTH_WEST"
+ ],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN:0/MAP_LAVARIDGE_TOWN_HERB_SHOP:0",
+ "MAP_LAVARIDGE_TOWN:1/MAP_LAVARIDGE_TOWN_GYM_1F:0",
+ "MAP_LAVARIDGE_TOWN:2/MAP_LAVARIDGE_TOWN_MART:0",
+ "MAP_LAVARIDGE_TOWN:3/MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:0",
+ "MAP_LAVARIDGE_TOWN:4/MAP_LAVARIDGE_TOWN_HOUSE:0"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN/SPRINGS": {
+ "parent_map": "MAP_LAVARIDGE_TOWN",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_LAVARIDGE_TOWN_ICE_HEAL"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN:5/MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:3"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_HERB_SHOP/MAIN": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_HERB_SHOP",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_CHARCOAL"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_HERB_SHOP:0,1/MAP_LAVARIDGE_TOWN:0"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_1F/ENTRANCE": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_1F:0,1/MAP_LAVARIDGE_TOWN:1",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:2/MAP_LAVARIDGE_TOWN_GYM_B1F:0",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:3/MAP_LAVARIDGE_TOWN_GYM_B1F:2",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:24/MAP_LAVARIDGE_TOWN_GYM_B1F:22"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_1F/BOTTOM_LEFT_LOWER": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_AXLE_REWARD",
+ "TRAINER_GERALD_REWARD",
+ "TRAINER_COLE_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_1F:4/MAP_LAVARIDGE_TOWN_GYM_B1F:4",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:6/MAP_LAVARIDGE_TOWN_GYM_B1F:1",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:8/MAP_LAVARIDGE_TOWN_GYM_B1F:6",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:9/MAP_LAVARIDGE_TOWN_GYM_B1F:7",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:10/MAP_LAVARIDGE_TOWN_GYM_B1F:8",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:11/MAP_LAVARIDGE_TOWN_GYM_B1F:9",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:12/MAP_LAVARIDGE_TOWN_GYM_B1F:10",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:21/MAP_LAVARIDGE_TOWN_GYM_B1F:20"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_1F/BOTTOM_LEFT_UPPER": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LAVARIDGE_TOWN_GYM_1F/BOTTOM_LEFT_LOWER"
+ ],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_1F:5/MAP_LAVARIDGE_TOWN_GYM_B1F:3",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:7/MAP_LAVARIDGE_TOWN_GYM_B1F:5"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_1F/TOP_LEFT": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_DANIELLE_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_1F:13/MAP_LAVARIDGE_TOWN_GYM_B1F:11",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:14/MAP_LAVARIDGE_TOWN_GYM_B1F:12",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:15/MAP_LAVARIDGE_TOWN_GYM_B1F:13",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:16/MAP_LAVARIDGE_TOWN_GYM_B1F:14",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:17/MAP_LAVARIDGE_TOWN_GYM_B1F:15"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_1F/TOP_CENTER": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_1F:18/MAP_LAVARIDGE_TOWN_GYM_B1F:16",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:19/MAP_LAVARIDGE_TOWN_GYM_B1F:17",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:20/MAP_LAVARIDGE_TOWN_GYM_B1F:18"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_1F/TOP_RIGHT": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_1F:22/MAP_LAVARIDGE_TOWN_GYM_B1F:19",
+ "MAP_LAVARIDGE_TOWN_GYM_1F:23/MAP_LAVARIDGE_TOWN_GYM_B1F:21"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_1F/FLANNERY": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_OVERHEAT",
+ "BADGE_4",
+ "TRAINER_FLANNERY_1_REWARD"
+ ],
+ "events": [
+ "EVENT_DEFEAT_FLANNERY"
+ ],
+ "exits": [
+ "REGION_LAVARIDGE_TOWN_GYM_1F/ENTRANCE"
+ ],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_1F:25/MAP_LAVARIDGE_TOWN_GYM_B1F:23"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_B1F/TOP": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:14/MAP_LAVARIDGE_TOWN_GYM_1F:16",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:15/MAP_LAVARIDGE_TOWN_GYM_1F:17",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:16/MAP_LAVARIDGE_TOWN_GYM_1F:18",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:19/MAP_LAVARIDGE_TOWN_GYM_1F:22"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_B1F/BOTTOM_LEFT_LOWER": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_JACE_REWARD",
+ "TRAINER_ELI_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:1/MAP_LAVARIDGE_TOWN_GYM_1F:6",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:2/MAP_LAVARIDGE_TOWN_GYM_1F:3",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:3/MAP_LAVARIDGE_TOWN_GYM_1F:5",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:4/MAP_LAVARIDGE_TOWN_GYM_1F:4",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:6/MAP_LAVARIDGE_TOWN_GYM_1F:8",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:7/MAP_LAVARIDGE_TOWN_GYM_1F:9",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:8/MAP_LAVARIDGE_TOWN_GYM_1F:10",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:17/MAP_LAVARIDGE_TOWN_GYM_1F:19",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:20/MAP_LAVARIDGE_TOWN_GYM_1F:21"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_B1F/BOTTOM_LEFT_UPPER_1": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_KEEGAN_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_LAVARIDGE_TOWN_GYM_B1F/BOTTOM_LEFT_LOWER"
+ ],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:9/MAP_LAVARIDGE_TOWN_GYM_1F:11",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:10/MAP_LAVARIDGE_TOWN_GYM_1F:12",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:11/MAP_LAVARIDGE_TOWN_GYM_1F:13",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:12/MAP_LAVARIDGE_TOWN_GYM_1F:14"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_B1F/BOTTOM_LEFT_UPPER_2": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LAVARIDGE_TOWN_GYM_B1F/BOTTOM_LEFT_LOWER"
+ ],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:5/MAP_LAVARIDGE_TOWN_GYM_1F:7",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:13/MAP_LAVARIDGE_TOWN_GYM_1F:15"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_B1F/BOTTOM_RIGHT_LOWER": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_JEFF_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:0/MAP_LAVARIDGE_TOWN_GYM_1F:2",
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:22/MAP_LAVARIDGE_TOWN_GYM_1F:24"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_B1F/BOTTOM_RIGHT_MIDDLE": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LAVARIDGE_TOWN_GYM_B1F/BOTTOM_RIGHT_LOWER"
+ ],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:23/MAP_LAVARIDGE_TOWN_GYM_1F:25"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_B1F/BOTTOM_RIGHT_UPPER_1": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LAVARIDGE_TOWN_GYM_B1F/BOTTOM_RIGHT_MIDDLE"
+ ],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:18/MAP_LAVARIDGE_TOWN_GYM_1F:20"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_GYM_B1F/BOTTOM_RIGHT_UPPER_2": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_GYM_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LAVARIDGE_TOWN_GYM_B1F/BOTTOM_RIGHT_LOWER"
+ ],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_GYM_B1F:21/MAP_LAVARIDGE_TOWN_GYM_1F:23"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_MART/MAIN": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_MART",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_MART:0,1/MAP_LAVARIDGE_TOWN:2"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:0,1/MAP_LAVARIDGE_TOWN:3",
+ "MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:2/MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F:0",
+ "MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:3/MAP_LAVARIDGE_TOWN:5"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F:0/MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_LAVARIDGE_TOWN_HOUSE/MAIN": {
+ "parent_map": "MAP_LAVARIDGE_TOWN_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LAVARIDGE_TOWN_HOUSE:0,1/MAP_LAVARIDGE_TOWN:4"
+ ]
+ },
+
+ "REGION_FORTREE_CITY/MAIN": {
+ "parent_map": "MAP_FORTREE_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_VISITED_FORTREE_CITY"
+ ],
+ "exits": [
+ "REGION_FORTREE_CITY/BEFORE_GYM",
+ "REGION_ROUTE119/UPPER",
+ "REGION_ROUTE120/NORTH"
+ ],
+ "warps": [
+ "MAP_FORTREE_CITY:0/MAP_FORTREE_CITY_POKEMON_CENTER_1F:0",
+ "MAP_FORTREE_CITY:1/MAP_FORTREE_CITY_HOUSE1:0",
+ "MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0",
+ "MAP_FORTREE_CITY:4/MAP_FORTREE_CITY_HOUSE2:0",
+ "MAP_FORTREE_CITY:5/MAP_FORTREE_CITY_HOUSE3:0",
+ "MAP_FORTREE_CITY:6/MAP_FORTREE_CITY_HOUSE4:0",
+ "MAP_FORTREE_CITY:7/MAP_FORTREE_CITY_HOUSE5:0",
+ "MAP_FORTREE_CITY:8/MAP_FORTREE_CITY_DECORATION_SHOP:0"
+ ]
+ },
+ "REGION_FORTREE_CITY/BEFORE_GYM": {
+ "parent_map": "MAP_FORTREE_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_FORTREE_CITY/MAIN"
+ ],
+ "warps": [
+ "MAP_FORTREE_CITY:2/MAP_FORTREE_CITY_GYM:0"
+ ]
+ },
+ "REGION_FORTREE_CITY_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_FORTREE_CITY_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_FORTREE_CITY_POKEMON_CENTER_1F:0,1/MAP_FORTREE_CITY:0",
+ "MAP_FORTREE_CITY_POKEMON_CENTER_1F:2/MAP_FORTREE_CITY_POKEMON_CENTER_2F:0"
+ ]
+ },
+ "REGION_FORTREE_CITY_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_FORTREE_CITY_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_FORTREE_CITY_POKEMON_CENTER_2F:0/MAP_FORTREE_CITY_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_FORTREE_CITY_HOUSE1/MAIN": {
+ "parent_map": "MAP_FORTREE_CITY_HOUSE1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_FORTREE_CITY_HOUSE1:0,1/MAP_FORTREE_CITY:1"
+ ]
+ },
+ "REGION_FORTREE_CITY_HOUSE2/MAIN": {
+ "parent_map": "MAP_FORTREE_CITY_HOUSE2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_HIDDEN_POWER"
+ ],
+ "events": [
+ "EVENT_MOVE_TUTOR_DIG"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_FORTREE_CITY_HOUSE2:0,1/MAP_FORTREE_CITY:4"
+ ]
+ },
+ "REGION_FORTREE_CITY_HOUSE3/MAIN": {
+ "parent_map": "MAP_FORTREE_CITY_HOUSE3",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_FORTREE_CITY_HOUSE3:0,1/MAP_FORTREE_CITY:5"
+ ]
+ },
+ "REGION_FORTREE_CITY_HOUSE4/MAIN": {
+ "parent_map": "MAP_FORTREE_CITY_HOUSE4",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_MENTAL_HERB"
+ ],
+ "events": [
+ "EVENT_WINGULL_QUEST_1"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_FORTREE_CITY_HOUSE4:0,1/MAP_FORTREE_CITY:6"
+ ]
+ },
+ "REGION_FORTREE_CITY_HOUSE5/MAIN": {
+ "parent_map": "MAP_FORTREE_CITY_HOUSE5",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_FORTREE_CITY_HOUSE5:0,1/MAP_FORTREE_CITY:7"
+ ]
+ },
+ "REGION_FORTREE_CITY_GYM/MAIN": {
+ "parent_map": "MAP_FORTREE_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_AERIAL_ACE",
+ "BADGE_6",
+ "TRAINER_HUMBERTO_REWARD",
+ "TRAINER_ASHLEY_REWARD",
+ "TRAINER_JARED_REWARD",
+ "TRAINER_FLINT_REWARD",
+ "TRAINER_EDWARDO_REWARD",
+ "TRAINER_DARIUS_REWARD",
+ "TRAINER_WINONA_1_REWARD"
+ ],
+ "events": [
+ "EVENT_DEFEAT_WINONA"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_FORTREE_CITY_GYM:0,1/MAP_FORTREE_CITY:2"
+ ]
+ },
+ "REGION_FORTREE_CITY_MART/MAIN": {
+ "parent_map": "MAP_FORTREE_CITY_MART",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_FORTREE_CITY_MART:0,1/MAP_FORTREE_CITY:3"
+ ]
+ },
+ "REGION_FORTREE_CITY_DECORATION_SHOP/MAIN": {
+ "parent_map": "MAP_FORTREE_CITY_DECORATION_SHOP",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_FORTREE_CITY_DECORATION_SHOP:0,1/MAP_FORTREE_CITY:8"
+ ]
+ },
+
+ "REGION_LILYCOVE_CITY/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_LILYCOVE_CITY_MAX_REPEL",
+ "HIDDEN_ITEM_LILYCOVE_CITY_HEART_SCALE",
+ "HIDDEN_ITEM_LILYCOVE_CITY_PP_UP",
+ "HIDDEN_ITEM_LILYCOVE_CITY_POKE_BALL",
+ "NPC_GIFT_LILYCOVE_RECEIVED_BERRY",
+ "TRAINER_BRENDAN_LILYCOVE_MUDKIP_REWARD"
+ ],
+ "events": [
+ "EVENT_VISITED_LILYCOVE_CITY"
+ ],
+ "exits": [
+ "REGION_ROUTE121/EAST",
+ "REGION_LILYCOVE_CITY/SEA"
+ ],
+ "warps": [
+ "MAP_LILYCOVE_CITY:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:0",
+ "MAP_LILYCOVE_CITY:1/MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F:0",
+ "MAP_LILYCOVE_CITY:2/MAP_LILYCOVE_CITY_POKEMON_CENTER_1F:0",
+ "MAP_LILYCOVE_CITY:3,13/MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F:0,1",
+ "MAP_LILYCOVE_CITY:4/MAP_LILYCOVE_CITY_CONTEST_LOBBY:0",
+ "MAP_LILYCOVE_CITY:5/MAP_LILYCOVE_CITY_POKEMON_TRAINER_FAN_CLUB:1",
+ "MAP_LILYCOVE_CITY:7/MAP_LILYCOVE_CITY_MOVE_DELETERS_HOUSE:0",
+ "MAP_LILYCOVE_CITY:8/MAP_LILYCOVE_CITY_HOUSE1:0",
+ "MAP_LILYCOVE_CITY:9/MAP_LILYCOVE_CITY_HOUSE2:0",
+ "MAP_LILYCOVE_CITY:10/MAP_LILYCOVE_CITY_HOUSE3:0",
+ "MAP_LILYCOVE_CITY:11/MAP_LILYCOVE_CITY_HOUSE4:0",
+ "MAP_LILYCOVE_CITY:12/MAP_LILYCOVE_CITY_HARBOR:0"
+ ]
+ },
+ "REGION_LILYCOVE_CITY/SEA": {
+ "parent_map": "MAP_LILYCOVE_CITY",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LILYCOVE_CITY/MAIN",
+ "REGION_ROUTE124/MAIN"
+ ],
+ "warps": [
+ "MAP_LILYCOVE_CITY:6/MAP_AQUA_HIDEOUT_1F:0"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_1F/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR/MAIN"
+ ],
+ "warps": [
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:0,1/MAP_LILYCOVE_CITY:0",
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:0"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_2F/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR/MAIN"
+ ],
+ "warps": [
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:2",
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:1/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:0"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_3F/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR/MAIN"
+ ],
+ "warps": [
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:1",
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:1/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:0"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_4F/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR/MAIN"
+ ],
+ "warps": [
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:1",
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:1/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:0"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_5F/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR/MAIN"
+ ],
+ "warps": [
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:1",
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ROOFTOP:0"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_ROOFTOP/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ROOFTOP",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ROOFTOP:0/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:2"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_1F/MAIN",
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_2F/MAIN",
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_3F/MAIN",
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_4F/MAIN",
+ "REGION_LILYCOVE_CITY_DEPARTMENT_STORE_5F/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_LILYCOVE_CITY_COVE_LILY_MOTEL_1F/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F:0,1/MAP_LILYCOVE_CITY:1",
+ "MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F:2/MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_2F:0"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_COVE_LILY_MOTEL_2F/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_2F:0/MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F:2"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_POKEMON_CENTER_1F:0,1/MAP_LILYCOVE_CITY:2",
+ "MAP_LILYCOVE_CITY_POKEMON_CENTER_1F:2/MAP_LILYCOVE_CITY_POKEMON_CENTER_2F:0"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_POKEMON_CENTER_2F:0/MAP_LILYCOVE_CITY_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F:0,1/MAP_LILYCOVE_CITY:3,13",
+ "MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F:2/MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_2F:0"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_LILYCOVE_MUSEUM_2F/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_2F:0/MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F:2"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_CONTEST_LOBBY/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_CONTEST_LOBBY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_POKEBLOCK_CASE"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_CONTEST_LOBBY:0,1/MAP_LILYCOVE_CITY:4",
+ "MAP_LILYCOVE_CITY_CONTEST_LOBBY:2/MAP_LILYCOVE_CITY_CONTEST_HALL:0",
+ "MAP_LILYCOVE_CITY_CONTEST_LOBBY:3/MAP_LILYCOVE_CITY_CONTEST_HALL:1"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_CONTEST_HALL/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_CONTEST_HALL",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_CONTEST_HALL:0,2/MAP_LILYCOVE_CITY_CONTEST_LOBBY:2",
+ "MAP_LILYCOVE_CITY_CONTEST_HALL:1,3/MAP_LILYCOVE_CITY_CONTEST_LOBBY:3"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_POKEMON_TRAINER_FAN_CLUB/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_POKEMON_TRAINER_FAN_CLUB",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_POKEMON_TRAINER_FAN_CLUB:0,1/MAP_LILYCOVE_CITY:5"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_MOVE_DELETERS_HOUSE/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_MOVE_DELETERS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_MOVE_DELETERS_HOUSE:0,1/MAP_LILYCOVE_CITY:7"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_HOUSE1/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_HOUSE1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_HOUSE1:0,1/MAP_LILYCOVE_CITY:8"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_HOUSE2/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_HOUSE2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_REST"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_HOUSE2:0,1/MAP_LILYCOVE_CITY:9"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_HOUSE3/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_HOUSE3",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_HOUSE3:0,1/MAP_LILYCOVE_CITY:10"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_HOUSE4/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_HOUSE4",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_LILYCOVE_CITY_HOUSE4:0,1/MAP_LILYCOVE_CITY:11"
+ ]
+ },
+ "REGION_LILYCOVE_CITY_HARBOR/MAIN": {
+ "parent_map": "MAP_LILYCOVE_CITY_HARBOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SS_TIDAL_CORRIDOR/MAIN",
+ "REGION_SOUTHERN_ISLAND_EXTERIOR/MAIN",
+ "REGION_FARAWAY_ISLAND_ENTRANCE/MAIN",
+ "REGION_BIRTH_ISLAND_HARBOR/MAIN",
+ "REGION_NAVEL_ROCK_HARBOR/MAIN"
+ ],
+ "warps": [
+ "MAP_LILYCOVE_CITY_HARBOR:0,1/MAP_LILYCOVE_CITY:12"
+ ]
+ },
+ "REGION_SS_TIDAL_CORRIDOR/MAIN": {
+ "parent_map": "MAP_SS_TIDAL_CORRIDOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SLATEPORT_CITY_HARBOR/MAIN",
+ "REGION_LILYCOVE_CITY_HARBOR/MAIN",
+ "REGION_BATTLE_FRONTIER_OUTSIDE_WEST/DOCK"
+ ],
+ "warps": [
+ "MAP_SS_TIDAL_CORRIDOR:0/MAP_SS_TIDAL_ROOMS:0",
+ "MAP_SS_TIDAL_CORRIDOR:4/MAP_SS_TIDAL_ROOMS:8",
+ "MAP_SS_TIDAL_CORRIDOR:1/MAP_SS_TIDAL_ROOMS:2",
+ "MAP_SS_TIDAL_CORRIDOR:5/MAP_SS_TIDAL_ROOMS:9",
+ "MAP_SS_TIDAL_CORRIDOR:2/MAP_SS_TIDAL_ROOMS:4",
+ "MAP_SS_TIDAL_CORRIDOR:6/MAP_SS_TIDAL_ROOMS:10",
+ "MAP_SS_TIDAL_CORRIDOR:3/MAP_SS_TIDAL_ROOMS:6",
+ "MAP_SS_TIDAL_CORRIDOR:7/MAP_SS_TIDAL_ROOMS:11",
+ "MAP_SS_TIDAL_CORRIDOR:8/MAP_SS_TIDAL_LOWER_DECK:0"
+ ]
+ },
+ "REGION_SS_TIDAL_ROOMS/MAIN": {
+ "parent_map": "MAP_SS_TIDAL_ROOMS",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_SNATCH",
+ "TRAINER_COLTON_REWARD",
+ "TRAINER_NAOMI_REWARD",
+ "TRAINER_THOMAS_REWARD",
+ "TRAINER_MICAH_REWARD",
+ "TRAINER_GARRET_REWARD",
+ "TRAINER_LEA_AND_JED_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SS_TIDAL_ROOMS:0,1/MAP_SS_TIDAL_CORRIDOR:0",
+ "MAP_SS_TIDAL_ROOMS:8/MAP_SS_TIDAL_CORRIDOR:4",
+ "MAP_SS_TIDAL_ROOMS:2,3/MAP_SS_TIDAL_CORRIDOR:1",
+ "MAP_SS_TIDAL_ROOMS:9/MAP_SS_TIDAL_CORRIDOR:5",
+ "MAP_SS_TIDAL_ROOMS:4,5/MAP_SS_TIDAL_CORRIDOR:2",
+ "MAP_SS_TIDAL_ROOMS:10/MAP_SS_TIDAL_CORRIDOR:6",
+ "MAP_SS_TIDAL_ROOMS:6,7/MAP_SS_TIDAL_CORRIDOR:3",
+ "MAP_SS_TIDAL_ROOMS:11/MAP_SS_TIDAL_CORRIDOR:7"
+ ]
+ },
+ "REGION_SS_TIDAL_LOWER_DECK/MAIN": {
+ "parent_map": "MAP_SS_TIDAL_LOWER_DECK",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_SS_TIDAL_LOWER_DECK_LEFTOVERS",
+ "TRAINER_LEONARD_REWARD",
+ "TRAINER_PHILLIP_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SS_TIDAL_LOWER_DECK:0/MAP_SS_TIDAL_CORRIDOR:8"
+ ]
+ },
+
+ "REGION_MOSSDEEP_CITY/MAIN": {
+ "parent_map": "MAP_MOSSDEEP_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_MOSSDEEP_CITY_NET_BALL",
+ "NPC_GIFT_RECEIVED_KINGS_ROCK"
+ ],
+ "events": [
+ "EVENT_VISITED_MOSSDEEP_CITY"
+ ],
+ "exits": [
+ "REGION_MOSSDEEP_CITY/WATER",
+ "REGION_ROUTE124/MAIN",
+ "REGION_ROUTE125/SEA",
+ "REGION_ROUTE127/MAIN"
+ ],
+ "warps": [
+ "MAP_MOSSDEEP_CITY:0/MAP_MOSSDEEP_CITY_HOUSE1:0",
+ "MAP_MOSSDEEP_CITY:1/MAP_MOSSDEEP_CITY_GYM:0",
+ "MAP_MOSSDEEP_CITY:2/MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F:0",
+ "MAP_MOSSDEEP_CITY:3/MAP_MOSSDEEP_CITY_HOUSE2:0",
+ "MAP_MOSSDEEP_CITY:4/MAP_MOSSDEEP_CITY_MART:0",
+ "MAP_MOSSDEEP_CITY:5/MAP_MOSSDEEP_CITY_HOUSE3:0",
+ "MAP_MOSSDEEP_CITY:6/MAP_MOSSDEEP_CITY_STEVENS_HOUSE:0",
+ "MAP_MOSSDEEP_CITY:7/MAP_MOSSDEEP_CITY_HOUSE4:1",
+ "MAP_MOSSDEEP_CITY:8/MAP_MOSSDEEP_CITY_SPACE_CENTER_1F:0",
+ "MAP_MOSSDEEP_CITY:9/MAP_MOSSDEEP_CITY_GAME_CORNER_1F:0"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY/WATER": {
+ "parent_map": "MAP_MOSSDEEP_CITY",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_MOSSDEEP_CITY_GYM/ROOM_1": {
+ "parent_map": "MAP_MOSSDEEP_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_MAURA_REWARD",
+ "TRAINER_PRESTON_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_GYM:0,1/MAP_MOSSDEEP_CITY:1",
+ "MAP_MOSSDEEP_CITY_GYM:2/MAP_MOSSDEEP_CITY_GYM:3",
+ "MAP_MOSSDEEP_CITY_GYM:8/MAP_MOSSDEEP_CITY_GYM:9"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_GYM/ROOM_2": {
+ "parent_map": "MAP_MOSSDEEP_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_SAMANTHA_REWARD",
+ "TRAINER_BLAKE_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_GYM:3/MAP_MOSSDEEP_CITY_GYM:2",
+ "MAP_MOSSDEEP_CITY_GYM:4/MAP_MOSSDEEP_CITY_GYM:5",
+ "MAP_MOSSDEEP_CITY_GYM:6/MAP_MOSSDEEP_CITY_GYM:7"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_GYM/ROOM_3": {
+ "parent_map": "MAP_MOSSDEEP_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_VIRGIL_REWARD",
+ "TRAINER_NATE_REWARD",
+ "TRAINER_SYLVIA_REWARD",
+ "TRAINER_HANNAH_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_GYM:5/MAP_MOSSDEEP_CITY_GYM:4",
+ "MAP_MOSSDEEP_CITY_GYM:10/MAP_MOSSDEEP_CITY_GYM:11"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_GYM/ROOM_4": {
+ "parent_map": "MAP_MOSSDEEP_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_GYM:11/MAP_MOSSDEEP_CITY_GYM:10",
+ "MAP_MOSSDEEP_CITY_GYM:12/MAP_MOSSDEEP_CITY_GYM:13"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_GYM/ROOM_5": {
+ "parent_map": "MAP_MOSSDEEP_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_KATHLEEN_REWARD",
+ "TRAINER_NICHOLAS_REWARD",
+ "TRAINER_MACEY_REWARD",
+ "TRAINER_CLIFFORD_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_GYM:7/MAP_MOSSDEEP_CITY_GYM:6",
+ "MAP_MOSSDEEP_CITY_GYM:9/MAP_MOSSDEEP_CITY_GYM:8"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_GYM/ROOM_6": {
+ "parent_map": "MAP_MOSSDEEP_CITY_GYM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_CALM_MIND",
+ "BADGE_7",
+ "TRAINER_TATE_AND_LIZA_1_REWARD"
+ ],
+ "events": [
+ "EVENT_DEFEAT_TATE_AND_LIZA"
+ ],
+ "exits": [
+ "REGION_MOSSDEEP_CITY_GYM/ROOM_1"
+ ],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_GYM:13/MAP_MOSSDEEP_CITY_GYM:12"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F:0,1/MAP_MOSSDEEP_CITY:2",
+ "MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F:2/MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F:0"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_MART/MAIN": {
+ "parent_map": "MAP_MOSSDEEP_CITY_MART",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_MART:0,1/MAP_MOSSDEEP_CITY:4"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F:0/MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_SPACE_CENTER_1F/MAIN": {
+ "parent_map": "MAP_MOSSDEEP_CITY_SPACE_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_SUN_STONE_MOSSDEEP",
+ "TRAINER_GRUNT_SPACE_CENTER_1_REWARD",
+ "TRAINER_GRUNT_SPACE_CENTER_2_REWARD",
+ "TRAINER_GRUNT_SPACE_CENTER_3_REWARD",
+ "TRAINER_GRUNT_SPACE_CENTER_4_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_SPACE_CENTER_1F:0,1/MAP_MOSSDEEP_CITY:8",
+ "MAP_MOSSDEEP_CITY_SPACE_CENTER_1F:2/MAP_MOSSDEEP_CITY_SPACE_CENTER_2F:0"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_SPACE_CENTER_2F/MAIN": {
+ "parent_map": "MAP_MOSSDEEP_CITY_SPACE_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GRUNT_SPACE_CENTER_5_REWARD",
+ "TRAINER_GRUNT_SPACE_CENTER_6_REWARD",
+ "TRAINER_GRUNT_SPACE_CENTER_7_REWARD"
+ ],
+ "events": [
+ "EVENT_DEFEAT_MAXIE_AT_SPACE_STATION"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_SPACE_CENTER_2F:0/MAP_MOSSDEEP_CITY_SPACE_CENTER_1F:2"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_GAME_CORNER_1F/MAIN": {
+ "parent_map": "MAP_MOSSDEEP_CITY_GAME_CORNER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_GAME_CORNER_1F:0,1/MAP_MOSSDEEP_CITY:9",
+ "MAP_MOSSDEEP_CITY_GAME_CORNER_1F:2/MAP_MOSSDEEP_CITY_GAME_CORNER_B1F:0"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_GAME_CORNER_B1F/MAIN": {
+ "parent_map": "MAP_MOSSDEEP_CITY_GAME_CORNER_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_GAME_CORNER_B1F:0/MAP_MOSSDEEP_CITY_GAME_CORNER_1F:2"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_STEVENS_HOUSE/MAIN": {
+ "parent_map": "MAP_MOSSDEEP_CITY_STEVENS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_HM_DIVE"
+ ],
+ "events": [
+ "EVENT_STEVEN_GIVES_DIVE"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_STEVENS_HOUSE:0,1/MAP_MOSSDEEP_CITY:6"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_HOUSE1/MAIN": {
+ "parent_map": "MAP_MOSSDEEP_CITY_HOUSE1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_HOUSE1:0,1/MAP_MOSSDEEP_CITY:0"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_HOUSE2/MAIN": {
+ "parent_map": "MAP_MOSSDEEP_CITY_HOUSE2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_WINGULL_QUEST_2"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_HOUSE2:0,1/MAP_MOSSDEEP_CITY:3"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_HOUSE3/MAIN": {
+ "parent_map": "MAP_MOSSDEEP_CITY_HOUSE3",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_SUPER_ROD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_HOUSE3:0,1/MAP_MOSSDEEP_CITY:5"
+ ]
+ },
+ "REGION_MOSSDEEP_CITY_HOUSE4/MAIN": {
+ "parent_map": "MAP_MOSSDEEP_CITY_HOUSE4",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MOSSDEEP_CITY_HOUSE4:0,1/MAP_MOSSDEEP_CITY:7"
+ ]
+ },
+
+ "REGION_UNDERWATER_SOOTOPOLIS_CITY/MAIN": {
+ "parent_map": "MAP_UNDERWATER_SOOTOPOLIS_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_RAYQUAZA_STOPS_FIGHT"
+ ],
+ "exits": [
+ "REGION_SOOTOPOLIS_CITY/WATER"
+ ],
+ "warps": [
+ "MAP_UNDERWATER_SOOTOPOLIS_CITY:0,1/MAP_UNDERWATER_ROUTE126:0"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY/WATER": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_UNDERWATER_SOOTOPOLIS_CITY/MAIN",
+ "REGION_SOOTOPOLIS_CITY/EAST",
+ "REGION_SOOTOPOLIS_CITY/WEST",
+ "REGION_SOOTOPOLIS_CITY/WEST_GRASS",
+ "REGION_SOOTOPOLIS_CITY/ISLAND"
+ ],
+ "warps": []
+ },
+ "REGION_SOOTOPOLIS_CITY/EAST": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [
+ "EVENT_VISITED_SOOTOPOLIS_CITY"
+ ],
+ "exits": [
+ "REGION_SOOTOPOLIS_CITY/WATER"
+ ],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY:0/MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F:0",
+ "MAP_SOOTOPOLIS_CITY:5/MAP_SOOTOPOLIS_CITY_HOUSE2:0",
+ "MAP_SOOTOPOLIS_CITY:7/MAP_SOOTOPOLIS_CITY_HOUSE4:0",
+ "MAP_SOOTOPOLIS_CITY:9/MAP_SOOTOPOLIS_CITY_HOUSE6:0",
+ "MAP_SOOTOPOLIS_CITY:11/MAP_SOOTOPOLIS_CITY_LOTAD_AND_SEEDOT_HOUSE:0",
+ "MAP_SOOTOPOLIS_CITY:12/MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F:0"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY/WEST": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SOOTOPOLIS_CITY/WATER"
+ ],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY:1/MAP_SOOTOPOLIS_CITY_MART:0",
+ "MAP_SOOTOPOLIS_CITY:6/MAP_SOOTOPOLIS_CITY_HOUSE3:0",
+ "MAP_SOOTOPOLIS_CITY:3/MAP_CAVE_OF_ORIGIN_ENTRANCE:0",
+ "MAP_SOOTOPOLIS_CITY:4/MAP_SOOTOPOLIS_CITY_HOUSE1:0",
+ "MAP_SOOTOPOLIS_CITY:8/MAP_SOOTOPOLIS_CITY_HOUSE5:0",
+ "MAP_SOOTOPOLIS_CITY:10/MAP_SOOTOPOLIS_CITY_HOUSE7:0"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY/WEST_GRASS": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "NPC_GIFT_SOOTOPOLIS_RECEIVED_BERRY_1",
+ "NPC_GIFT_SOOTOPOLIS_RECEIVED_BERRY_2"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_SOOTOPOLIS_CITY/ISLAND": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "NPC_GIFT_RECEIVED_HM_WATERFALL"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_SOOTOPOLIS_CITY/WATER"
+ ],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY:2/MAP_SOOTOPOLIS_CITY_GYM_1F:0"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F:0,1/MAP_SOOTOPOLIS_CITY:0",
+ "MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F:2/MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F:0"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F:0/MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_MART/MAIN": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_MART",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_MART:0,1/MAP_SOOTOPOLIS_CITY:1"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_GYM_1F/ENTRANCE": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_GYM_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SOOTOPOLIS_CITY_GYM_1F/PUZZLE_1"
+ ],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_GYM_1F:0,1/MAP_SOOTOPOLIS_CITY:2",
+ "MAP_SOOTOPOLIS_CITY_GYM_1F:2/MAP_SOOTOPOLIS_CITY_GYM_B1F:0"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_GYM_1F/PUZZLE_1": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_GYM_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SOOTOPOLIS_CITY_GYM_1F/ENTRANCE",
+ "REGION_SOOTOPOLIS_CITY_GYM_1F/PUZZLE_2",
+ "REGION_SOOTOPOLIS_CITY_GYM_B1F/LEVEL_2"
+ ],
+ "warps": []
+ },
+ "REGION_SOOTOPOLIS_CITY_GYM_1F/PUZZLE_2": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_GYM_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SOOTOPOLIS_CITY_GYM_1F/PUZZLE_1",
+ "REGION_SOOTOPOLIS_CITY_GYM_1F/PUZZLE_3",
+ "REGION_SOOTOPOLIS_CITY_GYM_B1F/LEVEL_3"
+ ],
+ "warps": []
+ },
+ "REGION_SOOTOPOLIS_CITY_GYM_1F/PUZZLE_3": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_GYM_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SOOTOPOLIS_CITY_GYM_1F/PUZZLE_2",
+ "REGION_SOOTOPOLIS_CITY_GYM_1F/TOP",
+ "REGION_SOOTOPOLIS_CITY_GYM_B1F/LEVEL_4"
+ ],
+ "warps": []
+ },
+ "REGION_SOOTOPOLIS_CITY_GYM_1F/TOP": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_GYM_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_WATER_PULSE",
+ "BADGE_8",
+ "TRAINER_JUAN_1_REWARD"
+ ],
+ "events": [
+ "EVENT_DEFEAT_JUAN"
+ ],
+ "exits": [
+ "REGION_SOOTOPOLIS_CITY_GYM_1F/PUZZLE_3"
+ ],
+ "warps": []
+ },
+ "REGION_SOOTOPOLIS_CITY_GYM_B1F/LEVEL_1": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_GYM_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_GYM_B1F:0/MAP_SOOTOPOLIS_CITY_GYM_1F:2"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_GYM_B1F/LEVEL_2": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_GYM_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_ANDREA_REWARD",
+ "TRAINER_CONNIE_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_SOOTOPOLIS_CITY_GYM_B1F/LEVEL_1"
+ ],
+ "warps": []
+ },
+ "REGION_SOOTOPOLIS_CITY_GYM_B1F/LEVEL_3": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_GYM_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_ANNIKA_REWARD",
+ "TRAINER_DAPHNE_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_SOOTOPOLIS_CITY_GYM_B1F/LEVEL_2"
+ ],
+ "warps": []
+ },
+ "REGION_SOOTOPOLIS_CITY_GYM_B1F/LEVEL_4": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_GYM_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_CRISSY_REWARD",
+ "TRAINER_TIFFANY_REWARD",
+ "TRAINER_BETHANY_REWARD",
+ "TRAINER_OLIVIA_REWARD",
+ "TRAINER_BRIDGET_REWARD",
+ "TRAINER_BRIANNA_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_SOOTOPOLIS_CITY_GYM_B1F/LEVEL_3"
+ ],
+ "warps": []
+ },
+ "REGION_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F/MAIN": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F:0,1/MAP_SOOTOPOLIS_CITY:12",
+ "MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F:2/MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_B1F:0"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_B1F/MAIN": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_B1F:0/MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F:2"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_LOTAD_AND_SEEDOT_HOUSE/MAIN": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_LOTAD_AND_SEEDOT_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_LOTAD_AND_SEEDOT_HOUSE:0,1/MAP_SOOTOPOLIS_CITY:11"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_HOUSE1/MAIN": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_HOUSE1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_BRICK_BREAK"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_HOUSE1:0,1/MAP_SOOTOPOLIS_CITY:4"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_HOUSE2/MAIN": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_HOUSE2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_HOUSE2:0,1/MAP_SOOTOPOLIS_CITY:5"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_HOUSE3/MAIN": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_HOUSE3",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_HOUSE3:0,1/MAP_SOOTOPOLIS_CITY:6"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_HOUSE4/MAIN": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_HOUSE4",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_HOUSE4:0,1/MAP_SOOTOPOLIS_CITY:7"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_HOUSE5/MAIN": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_HOUSE5",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_HOUSE5:0,1/MAP_SOOTOPOLIS_CITY:8"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_HOUSE6/MAIN": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_HOUSE6",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_HOUSE6:0,1/MAP_SOOTOPOLIS_CITY:9"
+ ]
+ },
+ "REGION_SOOTOPOLIS_CITY_HOUSE7/MAIN": {
+ "parent_map": "MAP_SOOTOPOLIS_CITY_HOUSE7",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SOOTOPOLIS_CITY_HOUSE7:0,1/MAP_SOOTOPOLIS_CITY:10"
+ ]
+ },
+
+ "REGION_PACIFIDLOG_TOWN/MAIN": {
+ "parent_map": "MAP_PACIFIDLOG_TOWN",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [
+ "EVENT_VISITED_PACIFIDLOG_TOWN"
+ ],
+ "exits": [
+ "REGION_PACIFIDLOG_TOWN/WATER",
+ "REGION_ROUTE131/MAIN",
+ "REGION_ROUTE132/EAST"
+ ],
+ "warps": [
+ "MAP_PACIFIDLOG_TOWN:0/MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F:0",
+ "MAP_PACIFIDLOG_TOWN:1/MAP_PACIFIDLOG_TOWN_HOUSE1:0",
+ "MAP_PACIFIDLOG_TOWN:2/MAP_PACIFIDLOG_TOWN_HOUSE2:0",
+ "MAP_PACIFIDLOG_TOWN:3/MAP_PACIFIDLOG_TOWN_HOUSE3:0",
+ "MAP_PACIFIDLOG_TOWN:4/MAP_PACIFIDLOG_TOWN_HOUSE4:0",
+ "MAP_PACIFIDLOG_TOWN:5/MAP_PACIFIDLOG_TOWN_HOUSE5:0"
+ ]
+ },
+ "REGION_PACIFIDLOG_TOWN/WATER": {
+ "parent_map": "MAP_PACIFIDLOG_TOWN",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_PACIFIDLOG_TOWN_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F:0,1/MAP_PACIFIDLOG_TOWN:0",
+ "MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F:2/MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F:0"
+ ]
+ },
+ "REGION_PACIFIDLOG_TOWN_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F:0/MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_PACIFIDLOG_TOWN_HOUSE1/MAIN": {
+ "parent_map": "MAP_PACIFIDLOG_TOWN_HOUSE1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PACIFIDLOG_TOWN_HOUSE1:0,1/MAP_PACIFIDLOG_TOWN:1"
+ ]
+ },
+ "REGION_PACIFIDLOG_TOWN_HOUSE2/MAIN": {
+ "parent_map": "MAP_PACIFIDLOG_TOWN_HOUSE2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_RETURN_2",
+ "NPC_GIFT_RECEIVED_TM_FRUSTRATION"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PACIFIDLOG_TOWN_HOUSE2:0,1/MAP_PACIFIDLOG_TOWN:2"
+ ]
+ },
+ "REGION_PACIFIDLOG_TOWN_HOUSE3/MAIN": {
+ "parent_map": "MAP_PACIFIDLOG_TOWN_HOUSE3",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PACIFIDLOG_TOWN_HOUSE3:0,1/MAP_PACIFIDLOG_TOWN:3"
+ ]
+ },
+ "REGION_PACIFIDLOG_TOWN_HOUSE4/MAIN": {
+ "parent_map": "MAP_PACIFIDLOG_TOWN_HOUSE4",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PACIFIDLOG_TOWN_HOUSE4:0,1/MAP_PACIFIDLOG_TOWN:4"
+ ]
+ },
+ "REGION_PACIFIDLOG_TOWN_HOUSE5/MAIN": {
+ "parent_map": "MAP_PACIFIDLOG_TOWN_HOUSE5",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_PACIFIDLOG_TOWN_HOUSE5:0,1/MAP_PACIFIDLOG_TOWN:5"
+ ]
+ },
+
+ "REGION_EVER_GRANDE_CITY/SEA": {
+ "parent_map": "MAP_EVER_GRANDE_CITY",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_EVER_GRANDE_CITY/SOUTH",
+ "REGION_ROUTE128/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_EVER_GRANDE_CITY/SOUTH": {
+ "parent_map": "MAP_EVER_GRANDE_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [
+ "EVENT_VISITED_EVER_GRANDE_CITY"
+ ],
+ "exits": [
+ "REGION_EVER_GRANDE_CITY/SEA"
+ ],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY:1/MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F:0",
+ "MAP_EVER_GRANDE_CITY:2/MAP_VICTORY_ROAD_1F:0"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY/NORTH": {
+ "parent_map": "MAP_EVER_GRANDE_CITY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY:0/MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:0",
+ "MAP_EVER_GRANDE_CITY:3/MAP_VICTORY_ROAD_1F:1"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_POKEMON_CENTER_1F/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F:0,1/MAP_EVER_GRANDE_CITY:1",
+ "MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F:2/MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F:0"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_POKEMON_CENTER_2F/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F:0/MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F:2"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"
+ ],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:0,1/MAP_EVER_GRANDE_CITY:0",
+ "MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:4/MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F:0"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN"
+ ],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:2,3/MAP_EVER_GRANDE_CITY_HALL5:0"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F:0/MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:4"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_HALL5/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_HALL5",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_HALL5:0,2,3/MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F:2",
+ "MAP_EVER_GRANDE_CITY_HALL5:1/MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM:0"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_SIDNEYS_ROOM/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_SIDNEY_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM:0/MAP_EVER_GRANDE_CITY_HALL5:1",
+ "MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM:1/MAP_EVER_GRANDE_CITY_HALL1:0"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_HALL1/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_HALL1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_HALL1:0,2,3/MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM:1",
+ "MAP_EVER_GRANDE_CITY_HALL1:1/MAP_EVER_GRANDE_CITY_PHOEBES_ROOM:0"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_PHOEBES_ROOM/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_PHOEBES_ROOM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_PHOEBE_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_PHOEBES_ROOM:0/MAP_EVER_GRANDE_CITY_HALL1:1",
+ "MAP_EVER_GRANDE_CITY_PHOEBES_ROOM:1/MAP_EVER_GRANDE_CITY_HALL2:0"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_HALL2/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_HALL2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_HALL2:0,2,3/MAP_EVER_GRANDE_CITY_PHOEBES_ROOM:1",
+ "MAP_EVER_GRANDE_CITY_HALL2:1/MAP_EVER_GRANDE_CITY_GLACIAS_ROOM:0"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_GLACIAS_ROOM/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_GLACIAS_ROOM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GLACIA_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_GLACIAS_ROOM:0/MAP_EVER_GRANDE_CITY_HALL2:1",
+ "MAP_EVER_GRANDE_CITY_GLACIAS_ROOM:1/MAP_EVER_GRANDE_CITY_HALL3:0"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_HALL3/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_HALL3",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_HALL3:0,2,3/MAP_EVER_GRANDE_CITY_GLACIAS_ROOM:1",
+ "MAP_EVER_GRANDE_CITY_HALL3:1/MAP_EVER_GRANDE_CITY_DRAKES_ROOM:0"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_DRAKES_ROOM/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_DRAKES_ROOM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_DRAKE_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_DRAKES_ROOM:0/MAP_EVER_GRANDE_CITY_HALL3:1",
+ "MAP_EVER_GRANDE_CITY_DRAKES_ROOM:1/MAP_EVER_GRANDE_CITY_HALL4:0"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_CHAMPIONS_ROOM/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_WALLACE_REWARD"
+ ],
+ "events": [
+ "EVENT_DEFEAT_CHAMPION"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM:0/MAP_EVER_GRANDE_CITY_HALL4:1",
+ "MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM:1/MAP_EVER_GRANDE_CITY_HALL_OF_FAME:0"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_HALL4/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_HALL4",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_HALL4:0/MAP_EVER_GRANDE_CITY_DRAKES_ROOM:1",
+ "MAP_EVER_GRANDE_CITY_HALL4:1/MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM:0"
+ ]
+ },
+ "REGION_EVER_GRANDE_CITY_HALL_OF_FAME/MAIN": {
+ "parent_map": "MAP_EVER_GRANDE_CITY_HALL_OF_FAME",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_EVER_GRANDE_CITY_HALL_OF_FAME:0/MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM:1"
+ ]
+ }
+}
diff --git a/worlds/pokemon_emerald/data/regions/dungeons.json b/worlds/pokemon_emerald/data/regions/dungeons.json
new file mode 100644
index 000000000000..040f06d8fea6
--- /dev/null
+++ b/worlds/pokemon_emerald/data/regions/dungeons.json
@@ -0,0 +1,3103 @@
+{
+ "REGION_PETALBURG_WOODS/WEST_PATH": {
+ "parent_map": "MAP_PETALBURG_WOODS",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_PETALBURG_WOODS_ETHER",
+ "ITEM_PETALBURG_WOODS_PARALYZE_HEAL",
+ "HIDDEN_ITEM_PETALBURG_WOODS_POTION",
+ "HIDDEN_ITEM_PETALBURG_WOODS_POKE_BALL",
+ "NPC_GIFT_RECEIVED_GREAT_BALL_PETALBURG_WOODS",
+ "TRAINER_LYLE_REWARD",
+ "TRAINER_GRUNT_PETALBURG_WOODS_REWARD",
+ "TRAINER_JAMES_1_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_PETALBURG_WOODS/EAST_PATH"
+ ],
+ "warps": [
+ "MAP_PETALBURG_WOODS:0,1/MAP_ROUTE104:2,3",
+ "MAP_PETALBURG_WOODS:2,3/MAP_ROUTE104:4,5",
+ "MAP_PETALBURG_WOODS:4,5/MAP_ROUTE104:6,7"
+ ]
+ },
+ "REGION_PETALBURG_WOODS/EAST_PATH": {
+ "parent_map": "MAP_PETALBURG_WOODS",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_PETALBURG_WOODS_GREAT_BALL",
+ "ITEM_PETALBURG_WOODS_X_ATTACK",
+ "HIDDEN_ITEM_PETALBURG_WOODS_TINY_MUSHROOM_1",
+ "HIDDEN_ITEM_PETALBURG_WOODS_TINY_MUSHROOM_2",
+ "NPC_GIFT_RECEIVED_MIRACLE_SEED"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_PETALBURG_WOODS/WEST_PATH"
+ ],
+ "warps": []
+ },
+ "REGION_RUSTURF_TUNNEL/WEST": {
+ "parent_map": "MAP_RUSTURF_TUNNEL",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_RUSTURF_TUNNEL_POKE_BALL",
+ "NPC_GIFT_RECEIVED_DEVON_GOODS_RUSTURF_TUNNEL",
+ "TRAINER_GRUNT_RUSTURF_TUNNEL_REWARD"
+ ],
+ "events": [
+ "EVENT_RECOVER_DEVON_GOODS"
+ ],
+ "exits": [
+ "REGION_RUSTURF_TUNNEL/EAST"
+ ],
+ "warps": [
+ "MAP_RUSTURF_TUNNEL:0/MAP_ROUTE116:0"
+ ]
+ },
+ "REGION_RUSTURF_TUNNEL/EAST": {
+ "parent_map": "MAP_RUSTURF_TUNNEL",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_RUSTURF_TUNNEL_MAX_ETHER",
+ "NPC_GIFT_RECEIVED_HM_STRENGTH",
+ "TRAINER_MIKE_2_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_RUSTURF_TUNNEL/WEST"
+ ],
+ "warps": [
+ "MAP_RUSTURF_TUNNEL:1/MAP_VERDANTURF_TOWN:4",
+ "MAP_RUSTURF_TUNNEL:2/MAP_ROUTE116:2"
+ ]
+ },
+ "REGION_GRANITE_CAVE_1F/LOWER": {
+ "parent_map": "MAP_GRANITE_CAVE_1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_GRANITE_CAVE_1F_ESCAPE_ROPE",
+ "NPC_GIFT_RECEIVED_HM_FLASH"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_GRANITE_CAVE_1F:0/MAP_ROUTE106:0",
+ "MAP_GRANITE_CAVE_1F:2/MAP_GRANITE_CAVE_B1F:1"
+ ]
+ },
+ "REGION_GRANITE_CAVE_1F/UPPER": {
+ "parent_map": "MAP_GRANITE_CAVE_1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_GRANITE_CAVE_1F/LOWER"
+ ],
+ "warps": [
+ "MAP_GRANITE_CAVE_1F:1/MAP_GRANITE_CAVE_B1F:0",
+ "MAP_GRANITE_CAVE_1F:3/MAP_GRANITE_CAVE_STEVENS_ROOM:0"
+ ]
+ },
+ "REGION_GRANITE_CAVE_B1F/LOWER": {
+ "parent_map": "MAP_GRANITE_CAVE_B1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_GRANITE_CAVE_B1F_POKE_BALL"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_GRANITE_CAVE_B1F/UPPER"
+ ],
+ "warps": [
+ "MAP_GRANITE_CAVE_B1F:1/MAP_GRANITE_CAVE_1F:2",
+ "MAP_GRANITE_CAVE_B1F:3/MAP_GRANITE_CAVE_B2F:1"
+ ]
+ },
+ "REGION_GRANITE_CAVE_B1F/LOWER_PLATFORM": {
+ "parent_map": "MAP_GRANITE_CAVE_B1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_GRANITE_CAVE_B1F:0/MAP_GRANITE_CAVE_1F:1",
+ "MAP_GRANITE_CAVE_B1F:2/MAP_GRANITE_CAVE_B2F:0"
+ ]
+ },
+ "REGION_GRANITE_CAVE_B1F/UPPER": {
+ "parent_map": "MAP_GRANITE_CAVE_B1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_GRANITE_CAVE_B1F/UPPER"
+ ],
+ "warps": [
+ "MAP_GRANITE_CAVE_B1F:4/MAP_GRANITE_CAVE_B2F:2",
+ "MAP_GRANITE_CAVE_B1F:5/MAP_GRANITE_CAVE_B2F:3",
+ "MAP_GRANITE_CAVE_B1F:6/MAP_GRANITE_CAVE_B2F:4"
+ ]
+ },
+ "REGION_GRANITE_CAVE_B2F/NORTH_LOWER_LANDING": {
+ "parent_map": "MAP_GRANITE_CAVE_B2F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_GRANITE_CAVE_B2F_REPEL"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_GRANITE_CAVE_B2F:2/MAP_GRANITE_CAVE_B1F:4"
+ ]
+ },
+ "REGION_GRANITE_CAVE_B2F/NORTH_UPPER_LANDING": {
+ "parent_map": "MAP_GRANITE_CAVE_B2F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_GRANITE_CAVE_B2F/NORTH_LOWER_LANDING"
+ ],
+ "warps": [
+ "MAP_GRANITE_CAVE_B2F:3/MAP_GRANITE_CAVE_B1F:5"
+ ]
+ },
+ "REGION_GRANITE_CAVE_B2F/NORTH_EAST_ROOM": {
+ "parent_map": "MAP_GRANITE_CAVE_B2F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_GRANITE_CAVE_B2F_RARE_CANDY",
+ "HIDDEN_ITEM_GRANITE_CAVE_B2F_EVERSTONE_1"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_GRANITE_CAVE_B2F:4/MAP_GRANITE_CAVE_B1F:6"
+ ]
+ },
+ "REGION_GRANITE_CAVE_B2F/LOWER": {
+ "parent_map": "MAP_GRANITE_CAVE_B2F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_GRANITE_CAVE_B2F_EVERSTONE_2"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_GRANITE_CAVE_B2F:0/MAP_GRANITE_CAVE_B1F:2",
+ "MAP_GRANITE_CAVE_B2F:1/MAP_GRANITE_CAVE_B1F:3"
+ ]
+ },
+ "REGION_GRANITE_CAVE_STEVENS_ROOM/MAIN": {
+ "parent_map": "MAP_GRANITE_CAVE_STEVENS_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_GRANITE_CAVE_STEVENS_ROOM/LETTER_DELIVERED"
+ ],
+ "warps": [
+ "MAP_GRANITE_CAVE_STEVENS_ROOM:0/MAP_GRANITE_CAVE_1F:3"
+ ]
+ },
+ "REGION_GRANITE_CAVE_STEVENS_ROOM/LETTER_DELIVERED": {
+ "parent_map": "MAP_GRANITE_CAVE_STEVENS_ROOM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_STEEL_WING"
+ ],
+ "events": [
+ "EVENT_DELIVER_LETTER"
+ ],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_TRAINER_HILL_ENTRANCE/MAIN": {
+ "parent_map": "MAP_TRAINER_HILL_ENTRANCE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_TRAINER_HILL_ENTRANCE:0,1/MAP_ROUTE111:4",
+ "MAP_TRAINER_HILL_ENTRANCE:2/MAP_TRAINER_HILL_1F:0"
+ ]
+ },
+ "REGION_TRAINER_HILL_1F/MAIN": {
+ "parent_map": "MAP_TRAINER_HILL_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_TRAINER_HILL_1F:0/MAP_TRAINER_HILL_ENTRANCE:2",
+ "MAP_TRAINER_HILL_1F:1/MAP_TRAINER_HILL_2F:0"
+ ]
+ },
+ "REGION_TRAINER_HILL_2F/MAIN": {
+ "parent_map": "MAP_TRAINER_HILL_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_TRAINER_HILL_2F:0/MAP_TRAINER_HILL_1F:1",
+ "MAP_TRAINER_HILL_2F:1/MAP_TRAINER_HILL_3F:0"
+ ]
+ },
+ "REGION_TRAINER_HILL_3F/MAIN": {
+ "parent_map": "MAP_TRAINER_HILL_3F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_TRAINER_HILL_3F:0/MAP_TRAINER_HILL_2F:1",
+ "MAP_TRAINER_HILL_3F:1/MAP_TRAINER_HILL_4F:0"
+ ]
+ },
+ "REGION_TRAINER_HILL_4F/MAIN": {
+ "parent_map": "MAP_TRAINER_HILL_4F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_TRAINER_HILL_4F:0/MAP_TRAINER_HILL_3F:1",
+ "MAP_TRAINER_HILL_4F:1/MAP_TRAINER_HILL_ROOF:0"
+ ]
+ },
+ "REGION_TRAINER_HILL_ROOF/MAIN": {
+ "parent_map": "MAP_TRAINER_HILL_ROOF",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_TRAINER_HILL_ROOF:0/MAP_TRAINER_HILL_4F:1",
+ "MAP_TRAINER_HILL_ROOF:1/MAP_TRAINER_HILL_ELEVATOR:1"
+ ]
+ },
+ "REGION_TRAINER_HILL_ELEVATOR/MAIN": {
+ "parent_map": "MAP_TRAINER_HILL_ELEVATOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_TRAINER_HILL_ELEVATOR:0,1/MAP_TRAINER_HILL_ROOF:1"
+ ]
+ },
+ "REGION_FIERY_PATH/MAIN": {
+ "parent_map": "MAP_FIERY_PATH",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_FIERY_PATH/BEHIND_BOULDER"
+ ],
+ "warps": [
+ "MAP_FIERY_PATH:0/MAP_ROUTE112:4",
+ "MAP_FIERY_PATH:1/MAP_ROUTE112:5"
+ ]
+ },
+ "REGION_FIERY_PATH/BEHIND_BOULDER": {
+ "parent_map": "MAP_FIERY_PATH",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_FIERY_PATH_TM_TOXIC",
+ "ITEM_FIERY_PATH_FIRE_STONE"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_FIERY_PATH/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_MAGMA_HIDEOUT_1F/MAIN": {
+ "parent_map": "MAP_MAGMA_HIDEOUT_1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_2_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_MAGMA_HIDEOUT_1F/ENTRANCE"
+ ],
+ "warps": [
+ "MAP_MAGMA_HIDEOUT_1F:1/MAP_MAGMA_HIDEOUT_2F_1R:1"
+ ]
+ },
+ "REGION_MAGMA_HIDEOUT_1F/CENTER_EXIT": {
+ "parent_map": "MAP_MAGMA_HIDEOUT_1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_MAGMA_HIDEOUT_1F/MAIN"
+ ],
+ "warps": [
+ "MAP_MAGMA_HIDEOUT_1F:3/MAP_MAGMA_HIDEOUT_2F_3R:0"
+ ]
+ },
+ "REGION_MAGMA_HIDEOUT_1F/LEDGE": {
+ "parent_map": "MAP_MAGMA_HIDEOUT_1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_MAGMA_HIDEOUT_1F_RARE_CANDY",
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_1_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MAGMA_HIDEOUT_1F:2/MAP_MAGMA_HIDEOUT_2F_2R:1"
+ ]
+ },
+ "REGION_MAGMA_HIDEOUT_1F/ENTRANCE": {
+ "parent_map": "MAP_MAGMA_HIDEOUT_1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_MAGMA_HIDEOUT_1F/MAIN"
+ ],
+ "warps": [
+ "MAP_MAGMA_HIDEOUT_1F:0/MAP_JAGGED_PASS:4"
+ ]
+ },
+ "REGION_MAGMA_HIDEOUT_2F_1R/MAIN": {
+ "parent_map": "MAP_MAGMA_HIDEOUT_2F_1R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_3_REWARD",
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_4_REWARD",
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_5_REWARD",
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_14_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MAGMA_HIDEOUT_2F_1R:0/MAP_MAGMA_HIDEOUT_2F_2R:0",
+ "MAP_MAGMA_HIDEOUT_2F_1R:1/MAP_MAGMA_HIDEOUT_1F:1",
+ "MAP_MAGMA_HIDEOUT_2F_1R:2/MAP_MAGMA_HIDEOUT_3F_1R:2"
+ ]
+ },
+ "REGION_MAGMA_HIDEOUT_2F_2R/MAIN": {
+ "parent_map": "MAP_MAGMA_HIDEOUT_2F_2R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_MAGMA_HIDEOUT_2F_2R_MAX_ELIXIR",
+ "ITEM_MAGMA_HIDEOUT_2F_2R_FULL_RESTORE",
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_6_REWARD",
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_7_REWARD",
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_8_REWARD",
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_15_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MAGMA_HIDEOUT_2F_2R:0/MAP_MAGMA_HIDEOUT_2F_1R:0",
+ "MAP_MAGMA_HIDEOUT_2F_2R:1/MAP_MAGMA_HIDEOUT_1F:2"
+ ]
+ },
+ "REGION_MAGMA_HIDEOUT_2F_3R/MAIN": {
+ "parent_map": "MAP_MAGMA_HIDEOUT_2F_3R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MAGMA_HIDEOUT_2F_3R:0/MAP_MAGMA_HIDEOUT_1F:3",
+ "MAP_MAGMA_HIDEOUT_2F_3R:1/MAP_MAGMA_HIDEOUT_3F_3R:0"
+ ]
+ },
+ "REGION_MAGMA_HIDEOUT_3F_1R/MAIN": {
+ "parent_map": "MAP_MAGMA_HIDEOUT_3F_1R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_MAGMA_HIDEOUT_3F_1R_NUGGET",
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_9_REWARD",
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_16_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MAGMA_HIDEOUT_3F_1R:0/MAP_MAGMA_HIDEOUT_4F:0",
+ "MAP_MAGMA_HIDEOUT_3F_1R:1/MAP_MAGMA_HIDEOUT_3F_2R:0",
+ "MAP_MAGMA_HIDEOUT_3F_1R:2/MAP_MAGMA_HIDEOUT_2F_1R:2"
+ ]
+ },
+ "REGION_MAGMA_HIDEOUT_3F_2R/MAIN": {
+ "parent_map": "MAP_MAGMA_HIDEOUT_3F_2R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_MAGMA_HIDEOUT_3F_2R_PP_MAX",
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_10_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MAGMA_HIDEOUT_3F_2R:0/MAP_MAGMA_HIDEOUT_3F_1R:1"
+ ]
+ },
+ "REGION_MAGMA_HIDEOUT_3F_3R/MAIN": {
+ "parent_map": "MAP_MAGMA_HIDEOUT_3F_3R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_MAGMA_HIDEOUT_3F_3R_ECAPE_ROPE"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MAGMA_HIDEOUT_3F_3R:0/MAP_MAGMA_HIDEOUT_2F_3R:1",
+ "MAP_MAGMA_HIDEOUT_3F_3R:1/MAP_MAGMA_HIDEOUT_4F:1"
+ ]
+ },
+ "REGION_MAGMA_HIDEOUT_4F/MAIN": {
+ "parent_map": "MAP_MAGMA_HIDEOUT_4F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_MAGMA_HIDEOUT_4F_MAX_REVIVE",
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_11_REWARD",
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_12_REWARD",
+ "TRAINER_GRUNT_MAGMA_HIDEOUT_13_REWARD",
+ "TRAINER_TABITHA_MAGMA_HIDEOUT_REWARD",
+ "TRAINER_MAXIE_MAGMA_HIDEOUT_REWARD"
+ ],
+ "events": [
+ "EVENT_RELEASE_GROUDON"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_MAGMA_HIDEOUT_4F:0/MAP_MAGMA_HIDEOUT_3F_1R:0",
+ "MAP_MAGMA_HIDEOUT_4F:1/MAP_MAGMA_HIDEOUT_3F_3R:1"
+ ]
+ },
+ "REGION_MIRAGE_TOWER_1F/MAIN": {
+ "parent_map": "MAP_MIRAGE_TOWER_1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MIRAGE_TOWER_1F:0/MAP_ROUTE111:3",
+ "MAP_MIRAGE_TOWER_1F:1/MAP_MIRAGE_TOWER_2F:1"
+ ]
+ },
+ "REGION_MIRAGE_TOWER_2F/TOP": {
+ "parent_map": "MAP_MIRAGE_TOWER_2F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_MIRAGE_TOWER_2F/BOTTOM",
+ "REGION_MIRAGE_TOWER_1F/MAIN"
+ ],
+ "warps": [
+ "MAP_MIRAGE_TOWER_2F:1/MAP_MIRAGE_TOWER_1F:1"
+ ]
+ },
+ "REGION_MIRAGE_TOWER_2F/BOTTOM": {
+ "parent_map": "MAP_MIRAGE_TOWER_2F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_MIRAGE_TOWER_2F/TOP",
+ "REGION_MIRAGE_TOWER_1F/MAIN"
+ ],
+ "warps": [
+ "MAP_MIRAGE_TOWER_2F:0/MAP_MIRAGE_TOWER_3F:0"
+ ]
+ },
+ "REGION_MIRAGE_TOWER_3F/TOP": {
+ "parent_map": "MAP_MIRAGE_TOWER_3F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_MIRAGE_TOWER_3F/BOTTOM"
+ ],
+ "warps": [
+ "MAP_MIRAGE_TOWER_3F:1/MAP_MIRAGE_TOWER_4F:0"
+ ]
+ },
+ "REGION_MIRAGE_TOWER_3F/BOTTOM": {
+ "parent_map": "MAP_MIRAGE_TOWER_3F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_MIRAGE_TOWER_3F/TOP",
+ "REGION_MIRAGE_TOWER_2F/TOP"
+ ],
+ "warps": [
+ "MAP_MIRAGE_TOWER_3F:0/MAP_MIRAGE_TOWER_2F:0"
+ ]
+ },
+ "REGION_MIRAGE_TOWER_4F/MAIN": {
+ "parent_map": "MAP_MIRAGE_TOWER_4F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_MIRAGE_TOWER_4F/FOSSIL_PLATFORM"
+ ],
+ "warps": [
+ "MAP_MIRAGE_TOWER_4F:0/MAP_MIRAGE_TOWER_3F:1"
+ ]
+ },
+ "REGION_MIRAGE_TOWER_4F/FOSSIL_PLATFORM": {
+ "parent_map": "MAP_MIRAGE_TOWER_4F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_DESERT_RUINS/FRONT": {
+ "parent_map": "MAP_DESERT_RUINS",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_DESERT_RUINS:0/MAP_ROUTE111:1",
+ "MAP_DESERT_RUINS:1/MAP_DESERT_RUINS:2"
+ ]
+ },
+ "REGION_DESERT_RUINS/BACK": {
+ "parent_map": "MAP_DESERT_RUINS",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_ENCOUNTER_REGIROCK"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_DESERT_RUINS:2/MAP_DESERT_RUINS:1"
+ ]
+ },
+ "REGION_METEOR_FALLS_1F_1R/MAIN": {
+ "parent_map": "MAP_METEOR_FALLS_1F_1R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_METEOR_FALLS_1F_1R_MOON_STONE",
+ "ITEM_METEOR_FALLS_1F_1R_FULL_HEAL"
+ ],
+ "events": [
+ "EVENT_MAGMA_STEALS_METEORITE"
+ ],
+ "exits": [
+ "REGION_METEOR_FALLS_1F_1R/WATER"
+ ],
+ "warps": [
+ "MAP_METEOR_FALLS_1F_1R:0/MAP_ROUTE114:0",
+ "MAP_METEOR_FALLS_1F_1R:1/MAP_ROUTE115:0"
+ ]
+ },
+ "REGION_METEOR_FALLS_1F_1R/WATER": {
+ "parent_map": "MAP_METEOR_FALLS_1F_1R",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_METEOR_FALLS_1F_1R/MAIN",
+ "REGION_METEOR_FALLS_1F_1R/WATER_ABOVE_WATERFALL"
+ ],
+ "warps": []
+ },
+ "REGION_METEOR_FALLS_1F_1R/WATER_ABOVE_WATERFALL": {
+ "parent_map": "MAP_METEOR_FALLS_1F_1R",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_METEOR_FALLS_1F_1R/WATER",
+ "REGION_METEOR_FALLS_1F_1R/ABOVE_WATERFALL"
+ ],
+ "warps": []
+ },
+ "REGION_METEOR_FALLS_1F_1R/ABOVE_WATERFALL": {
+ "parent_map": "MAP_METEOR_FALLS_1F_1R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_METEOR_FALLS_1F_1R/WATER_ABOVE_WATERFALL"
+ ],
+ "warps": [
+ "MAP_METEOR_FALLS_1F_1R:2/MAP_METEOR_FALLS_1F_2R:0"
+ ]
+ },
+ "REGION_METEOR_FALLS_1F_1R/TOP": {
+ "parent_map": "MAP_METEOR_FALLS_1F_1R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_METEOR_FALLS_1F_1R_TM_IRON_TAIL"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_METEOR_FALLS_1F_1R:3/MAP_METEOR_FALLS_B1F_1R:4",
+ "MAP_METEOR_FALLS_1F_1R:5/MAP_METEOR_FALLS_STEVENS_CAVE:0"
+ ]
+ },
+ "REGION_METEOR_FALLS_1F_1R/BOTTOM": {
+ "parent_map": "MAP_METEOR_FALLS_1F_1R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_METEOR_FALLS_1F_1R_PP_UP"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_METEOR_FALLS_1F_1R:4/MAP_METEOR_FALLS_B1F_1R:5"
+ ]
+ },
+ "REGION_METEOR_FALLS_1F_2R/TOP": {
+ "parent_map": "MAP_METEOR_FALLS_1F_2R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_NICOLAS_1_REWARD",
+ "TRAINER_JOHN_AND_JAY_1_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_METEOR_FALLS_1F_2R/LEFT_SPLIT",
+ "REGION_METEOR_FALLS_1F_2R/RIGHT_SPLIT"
+ ],
+ "warps": [
+ "MAP_METEOR_FALLS_1F_2R:1/MAP_METEOR_FALLS_B1F_1R:0"
+ ]
+ },
+ "REGION_METEOR_FALLS_1F_2R/LEFT_SPLIT": {
+ "parent_map": "MAP_METEOR_FALLS_1F_2R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_METEOR_FALLS_1F_2R/LEFT_SPLIT_WATER"
+ ],
+ "warps": [
+ "MAP_METEOR_FALLS_1F_2R:2/MAP_METEOR_FALLS_B1F_1R:1"
+ ]
+ },
+ "REGION_METEOR_FALLS_1F_2R/LEFT_SPLIT_WATER": {
+ "parent_map": "MAP_METEOR_FALLS_1F_2R",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_METEOR_FALLS_1F_2R/RIGHT_SPLIT": {
+ "parent_map": "MAP_METEOR_FALLS_1F_2R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_METEOR_FALLS_1F_2R/RIGHT_SPLIT_WATER"
+ ],
+ "warps": [
+ "MAP_METEOR_FALLS_1F_2R:0/MAP_METEOR_FALLS_1F_1R:2",
+ "MAP_METEOR_FALLS_1F_2R:3/MAP_METEOR_FALLS_B1F_1R:2"
+ ]
+ },
+ "REGION_METEOR_FALLS_1F_2R/RIGHT_SPLIT_WATER": {
+ "parent_map": "MAP_METEOR_FALLS_1F_2R",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_METEOR_FALLS_B1F_1R/UPPER": {
+ "parent_map": "MAP_METEOR_FALLS_B1F_1R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_METEOR_FALLS_B1F_1R:0/MAP_METEOR_FALLS_1F_2R:1",
+ "MAP_METEOR_FALLS_B1F_1R:2/MAP_METEOR_FALLS_1F_2R:3",
+ "MAP_METEOR_FALLS_B1F_1R:4/MAP_METEOR_FALLS_1F_1R:3"
+ ]
+ },
+ "REGION_METEOR_FALLS_B1F_1R/HIGHEST_LADDER": {
+ "parent_map": "MAP_METEOR_FALLS_B1F_1R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_METEOR_FALLS_B1F_1R/WATER"
+ ],
+ "warps": [
+ "MAP_METEOR_FALLS_B1F_1R:1/MAP_METEOR_FALLS_1F_2R:2"
+ ]
+ },
+ "REGION_METEOR_FALLS_B1F_1R/NORTH_SHORE": {
+ "parent_map": "MAP_METEOR_FALLS_B1F_1R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_METEOR_FALLS_B1F_1R/WATER"
+ ],
+ "warps": [
+ "MAP_METEOR_FALLS_B1F_1R:3/MAP_METEOR_FALLS_B1F_2R:0"
+ ]
+ },
+ "REGION_METEOR_FALLS_B1F_1R/SOUTH_SHORE": {
+ "parent_map": "MAP_METEOR_FALLS_B1F_1R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_METEOR_FALLS_B1F_1R/WATER"
+ ],
+ "warps": [
+ "MAP_METEOR_FALLS_B1F_1R:5/MAP_METEOR_FALLS_1F_1R:4"
+ ]
+ },
+ "REGION_METEOR_FALLS_B1F_1R/WATER": {
+ "parent_map": "MAP_METEOR_FALLS_B1F_1R",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_METEOR_FALLS_B1F_1R/SOUTH_SHORE",
+ "REGION_METEOR_FALLS_B1F_1R/NORTH_SHORE",
+ "REGION_METEOR_FALLS_B1F_1R/HIGHEST_LADDER"
+ ],
+ "warps": []
+ },
+ "REGION_METEOR_FALLS_B1F_2R/ENTRANCE": {
+ "parent_map": "MAP_METEOR_FALLS_B1F_2R",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_METEOR_FALLS_B1F_2R/WATER"
+ ],
+ "warps": [
+ "MAP_METEOR_FALLS_B1F_2R:0/MAP_METEOR_FALLS_B1F_1R:3"
+ ]
+ },
+ "REGION_METEOR_FALLS_B1F_2R/WATER": {
+ "parent_map": "MAP_METEOR_FALLS_B1F_2R",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_METEOR_FALLS_B1F_2R_TM_DRAGON_CLAW"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_METEOR_FALLS_B1F_2R/ENTRANCE"
+ ],
+ "warps": []
+ },
+ "REGION_METEOR_FALLS_STEVENS_CAVE/MAIN": {
+ "parent_map": "MAP_METEOR_FALLS_STEVENS_CAVE",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_STEVEN_REWARD"
+ ],
+ "events": [
+ "EVENT_DEFEAT_STEVEN"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_METEOR_FALLS_STEVENS_CAVE:0/MAP_METEOR_FALLS_1F_1R:5"
+ ]
+ },
+ "REGION_ISLAND_CAVE/FRONT": {
+ "parent_map": "MAP_ISLAND_CAVE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ISLAND_CAVE:0/MAP_ROUTE105:0",
+ "MAP_ISLAND_CAVE:1/MAP_ISLAND_CAVE:2"
+ ]
+ },
+ "REGION_ISLAND_CAVE/BACK": {
+ "parent_map": "MAP_ISLAND_CAVE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_ENCOUNTER_REGICE"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_ISLAND_CAVE:2/MAP_ISLAND_CAVE:1"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_DECK/ENTRANCE": {
+ "parent_map": "MAP_ABANDONED_SHIP_DECK",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_DECK:0,1/MAP_ROUTE108:0",
+ "MAP_ABANDONED_SHIP_DECK:2/MAP_ABANDONED_SHIP_CORRIDORS_1F:1"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_DECK/UPPER": {
+ "parent_map": "MAP_ABANDONED_SHIP_DECK",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_DECK:3/MAP_ABANDONED_SHIP_CORRIDORS_1F:2",
+ "MAP_ABANDONED_SHIP_DECK:4/MAP_ABANDONED_SHIP_CAPTAINS_OFFICE:0"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_CAPTAINS_OFFICE/MAIN": {
+ "parent_map": "MAP_ABANDONED_SHIP_CAPTAINS_OFFICE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ABANDONED_SHIP_CAPTAINS_OFFICE_STORAGE_KEY"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_CAPTAINS_OFFICE:0,1/MAP_ABANDONED_SHIP_DECK:4"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_CORRIDORS_1F/WEST": {
+ "parent_map": "MAP_ABANDONED_SHIP_CORRIDORS_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_CHARLIE_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_CORRIDORS_1F:2,3/MAP_ABANDONED_SHIP_DECK:3",
+ "MAP_ABANDONED_SHIP_CORRIDORS_1F:8/MAP_ABANDONED_SHIP_ROOMS2_1F:0",
+ "MAP_ABANDONED_SHIP_CORRIDORS_1F:10/MAP_ABANDONED_SHIP_CORRIDORS_B1F:6",
+ "MAP_ABANDONED_SHIP_CORRIDORS_1F:11/MAP_ABANDONED_SHIP_ROOMS2_1F:2"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_CORRIDORS_1F/EAST": {
+ "parent_map": "MAP_ABANDONED_SHIP_CORRIDORS_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_CORRIDORS_1F:0,1/MAP_ABANDONED_SHIP_DECK:2",
+ "MAP_ABANDONED_SHIP_CORRIDORS_1F:4/MAP_ABANDONED_SHIP_ROOMS_1F:0",
+ "MAP_ABANDONED_SHIP_CORRIDORS_1F:5/MAP_ABANDONED_SHIP_ROOMS_1F:3",
+ "MAP_ABANDONED_SHIP_CORRIDORS_1F:6/MAP_ABANDONED_SHIP_ROOMS_1F:2",
+ "MAP_ABANDONED_SHIP_CORRIDORS_1F:7/MAP_ABANDONED_SHIP_ROOMS_1F:4",
+ "MAP_ABANDONED_SHIP_CORRIDORS_1F:9/MAP_ABANDONED_SHIP_CORRIDORS_B1F:7"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_ROOMS_1F/MAIN": {
+ "parent_map": "MAP_ABANDONED_SHIP_ROOMS_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_THALIA_1_REWARD",
+ "TRAINER_DEMETRIUS_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_ROOMS_1F:0,1/MAP_ABANDONED_SHIP_CORRIDORS_1F:4",
+ "MAP_ABANDONED_SHIP_ROOMS_1F:3,5/MAP_ABANDONED_SHIP_CORRIDORS_1F:5",
+ "MAP_ABANDONED_SHIP_ROOMS_1F:4/MAP_ABANDONED_SHIP_CORRIDORS_1F:7"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_ROOMS_1F/NORTH_WEST": {
+ "parent_map": "MAP_ABANDONED_SHIP_ROOMS_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ABANDONED_SHIP_ROOMS_1F_HARBOR_MAIL"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_ROOMS_1F:2/MAP_ABANDONED_SHIP_CORRIDORS_1F:6"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_ROOMS2_1F/MAIN": {
+ "parent_map": "MAP_ABANDONED_SHIP_ROOMS2_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ABANDONED_SHIP_ROOMS_2_1F_REVIVE",
+ "TRAINER_JANI_REWARD",
+ "TRAINER_GARRISON_REWARD",
+ "TRAINER_KIRA_AND_DAN_1_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_ROOMS2_1F:0,1/MAP_ABANDONED_SHIP_CORRIDORS_1F:8",
+ "MAP_ABANDONED_SHIP_ROOMS2_1F:2/MAP_ABANDONED_SHIP_CORRIDORS_1F:11"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_CORRIDORS_B1F/MAIN": {
+ "parent_map": "MAP_ABANDONED_SHIP_CORRIDORS_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_DUNCAN_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_CORRIDORS_B1F:0/MAP_ABANDONED_SHIP_ROOMS2_B1F:2",
+ "MAP_ABANDONED_SHIP_CORRIDORS_B1F:1/MAP_ABANDONED_SHIP_ROOMS2_B1F:0",
+ "MAP_ABANDONED_SHIP_CORRIDORS_B1F:2/MAP_ABANDONED_SHIP_ROOMS_B1F:0",
+ "MAP_ABANDONED_SHIP_CORRIDORS_B1F:3/MAP_ABANDONED_SHIP_ROOMS_B1F:1",
+ "MAP_ABANDONED_SHIP_CORRIDORS_B1F:4/MAP_ABANDONED_SHIP_ROOMS_B1F:2",
+ "MAP_ABANDONED_SHIP_CORRIDORS_B1F:5/MAP_ABANDONED_SHIP_ROOM_B1F:0",
+ "MAP_ABANDONED_SHIP_CORRIDORS_B1F:6/MAP_ABANDONED_SHIP_CORRIDORS_1F:10",
+ "MAP_ABANDONED_SHIP_CORRIDORS_B1F:7/MAP_ABANDONED_SHIP_CORRIDORS_1F:9"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_ROOMS_B1F/LEFT": {
+ "parent_map": "MAP_ABANDONED_SHIP_ROOMS_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ABANDONED_SHIP_ROOMS_B1F_ESCAPE_ROPE"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_ROOMS_B1F:0/MAP_ABANDONED_SHIP_CORRIDORS_B1F:2"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_ROOMS_B1F/CENTER": {
+ "parent_map": "MAP_ABANDONED_SHIP_ROOMS_B1F",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ABANDONED_SHIP_UNDERWATER1/MAIN"
+ ],
+ "warps": [
+ "MAP_ABANDONED_SHIP_ROOMS_B1F:1/MAP_ABANDONED_SHIP_CORRIDORS_B1F:3"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_ROOMS_B1F/RIGHT": {
+ "parent_map": "MAP_ABANDONED_SHIP_ROOMS_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_ROOMS_B1F:2/MAP_ABANDONED_SHIP_CORRIDORS_B1F:4"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_ROOMS2_B1F/MAIN": {
+ "parent_map": "MAP_ABANDONED_SHIP_ROOMS2_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ABANDONED_SHIP_ROOMS_2_B1F_DIVE_BALL"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_ROOMS2_B1F:0,1/MAP_ABANDONED_SHIP_CORRIDORS_B1F:1",
+ "MAP_ABANDONED_SHIP_ROOMS2_B1F:2,3/MAP_ABANDONED_SHIP_CORRIDORS_B1F:0"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_ROOM_B1F/MAIN": {
+ "parent_map": "MAP_ABANDONED_SHIP_ROOM_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ABANDONED_SHIP_ROOMS_B1F_TM_ICE_BEAM"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_ROOM_B1F:0,1/MAP_ABANDONED_SHIP_CORRIDORS_B1F:5"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS/MAIN": {
+ "parent_map": "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ABANDONED_SHIP_UNDERWATER2/MAIN"
+ ],
+ "warps": [
+ "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:0/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:0",
+ "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:3/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:6",
+ "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:1/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:2",
+ "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:4/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:7",
+ "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:2/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:4",
+ "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:5/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:8"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS/TOP_LEFT": {
+ "parent_map": "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_ABANDONED_SHIP_RM_6_KEY"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:6/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:3"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS/TOP_CENTER_DOORWAY": {
+ "parent_map": "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:7/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:4"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS/TOP_RIGHT": {
+ "parent_map": "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_6_LUXURY_BALL",
+ "HIDDEN_ITEM_ABANDONED_SHIP_RM_2_KEY"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:8/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:5"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS/BOTTOM_LEFT": {
+ "parent_map": "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_1_TM_RAIN_DANCE",
+ "HIDDEN_ITEM_ABANDONED_SHIP_RM_4_KEY"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:0,1/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:0"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS/BOTTOM_CENTER": {
+ "parent_map": "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_2_SCANNER"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:2,3/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:1"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS/BOTTOM_RIGHT": {
+ "parent_map": "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_3_WATER_STONE",
+ "HIDDEN_ITEM_ABANDONED_SHIP_RM_1_KEY"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:4,5/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:2"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_UNDERWATER1/MAIN": {
+ "parent_map": "MAP_ABANDONED_SHIP_UNDERWATER1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ABANDONED_SHIP_ROOMS_B1F/CENTER"
+ ],
+ "warps": [
+ "MAP_ABANDONED_SHIP_UNDERWATER1:0,1/MAP_ABANDONED_SHIP_UNDERWATER2:0"
+ ]
+ },
+ "REGION_ABANDONED_SHIP_UNDERWATER2/MAIN": {
+ "parent_map": "MAP_ABANDONED_SHIP_UNDERWATER2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS/MAIN"
+ ],
+ "warps": [
+ "MAP_ABANDONED_SHIP_UNDERWATER2:0/MAP_ABANDONED_SHIP_UNDERWATER1:0"
+ ]
+ },
+ "REGION_NEW_MAUVILLE_ENTRANCE/MAIN": {
+ "parent_map": "MAP_NEW_MAUVILLE_ENTRANCE",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NEW_MAUVILLE_ENTRANCE:0/MAP_ROUTE110:0",
+ "MAP_NEW_MAUVILLE_ENTRANCE:1/MAP_NEW_MAUVILLE_INSIDE:0"
+ ]
+ },
+ "REGION_NEW_MAUVILLE_INSIDE/MAIN": {
+ "parent_map": "MAP_NEW_MAUVILLE_INSIDE",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_NEW_MAUVILLE_ULTRA_BALL",
+ "ITEM_NEW_MAUVILLE_ESCAPE_ROPE",
+ "ITEM_NEW_MAUVILLE_THUNDER_STONE",
+ "ITEM_NEW_MAUVILLE_FULL_HEAL",
+ "ITEM_NEW_MAUVILLE_PARALYZE_HEAL"
+ ],
+ "events": [
+ "EVENT_TURN_OFF_GENERATOR"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_NEW_MAUVILLE_INSIDE:0/MAP_NEW_MAUVILLE_ENTRANCE:1"
+ ]
+ },
+ "REGION_SCORCHED_SLAB/MAIN": {
+ "parent_map": "MAP_SCORCHED_SLAB",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_SCORCHED_SLAB_TM_SUNNY_DAY"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SCORCHED_SLAB:0/MAP_ROUTE120:1"
+ ]
+ },
+ "REGION_ANCIENT_TOMB/FRONT": {
+ "parent_map": "MAP_ANCIENT_TOMB",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ANCIENT_TOMB:0/MAP_ROUTE120:0",
+ "MAP_ANCIENT_TOMB:1/MAP_ANCIENT_TOMB:2"
+ ]
+ },
+ "REGION_ANCIENT_TOMB/BACK": {
+ "parent_map": "MAP_ANCIENT_TOMB",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_ENCOUNTER_REGISTEEL"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_ANCIENT_TOMB:2/MAP_ANCIENT_TOMB:1"
+ ]
+ },
+ "REGION_MT_PYRE_1F/MAIN": {
+ "parent_map": "MAP_MT_PYRE_1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_CLEANSE_TAG"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MT_PYRE_1F:0,2/MAP_ROUTE122:0",
+ "MAP_MT_PYRE_1F:1,3/MAP_MT_PYRE_EXTERIOR:0",
+ "MAP_MT_PYRE_1F:4/MAP_MT_PYRE_2F:0",
+ "MAP_MT_PYRE_1F:5/MAP_MT_PYRE_2F:4"
+ ]
+ },
+ "REGION_MT_PYRE_2F/MAIN": {
+ "parent_map": "MAP_MT_PYRE_2F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_MT_PYRE_2F_ULTRA_BALL",
+ "TRAINER_MARK_REWARD",
+ "TRAINER_LEAH_REWARD",
+ "TRAINER_ZANDER_REWARD",
+ "TRAINER_DEZ_AND_LUKE_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MT_PYRE_2F:0/MAP_MT_PYRE_1F:4",
+ "MAP_MT_PYRE_2F:1/MAP_MT_PYRE_3F:0",
+ "MAP_MT_PYRE_2F:2/MAP_MT_PYRE_3F:4",
+ "MAP_MT_PYRE_2F:3/MAP_MT_PYRE_3F:5",
+ "MAP_MT_PYRE_2F:4/MAP_MT_PYRE_1F:5"
+ ]
+ },
+ "REGION_MT_PYRE_3F/MAIN": {
+ "parent_map": "MAP_MT_PYRE_3F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_MT_PYRE_3F_SUPER_REPEL",
+ "TRAINER_WILLIAM_REWARD",
+ "TRAINER_GABRIELLE_1_REWARD",
+ "TRAINER_KAYLA_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MT_PYRE_3F:0/MAP_MT_PYRE_2F:1",
+ "MAP_MT_PYRE_3F:1/MAP_MT_PYRE_4F:1",
+ "MAP_MT_PYRE_3F:2/MAP_MT_PYRE_4F:4",
+ "MAP_MT_PYRE_3F:3/MAP_MT_PYRE_4F:5",
+ "MAP_MT_PYRE_3F:4/MAP_MT_PYRE_2F:2",
+ "MAP_MT_PYRE_3F:5/MAP_MT_PYRE_2F:3"
+ ]
+ },
+ "REGION_MT_PYRE_4F/MAIN": {
+ "parent_map": "MAP_MT_PYRE_4F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_MT_PYRE_4F_SEA_INCENSE",
+ "TRAINER_TASHA_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MT_PYRE_4F:0/MAP_MT_PYRE_5F:1",
+ "MAP_MT_PYRE_4F:1/MAP_MT_PYRE_3F:1",
+ "MAP_MT_PYRE_4F:2/MAP_MT_PYRE_5F:3",
+ "MAP_MT_PYRE_4F:3/MAP_MT_PYRE_5F:4",
+ "MAP_MT_PYRE_4F:4/MAP_MT_PYRE_3F:2",
+ "MAP_MT_PYRE_4F:5/MAP_MT_PYRE_3F:3"
+ ]
+ },
+ "REGION_MT_PYRE_5F/MAIN": {
+ "parent_map": "MAP_MT_PYRE_5F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_MT_PYRE_5F_LAX_INCENSE",
+ "TRAINER_ATSUSHI_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MT_PYRE_5F:0/MAP_MT_PYRE_6F:0",
+ "MAP_MT_PYRE_5F:1/MAP_MT_PYRE_4F:0",
+ "MAP_MT_PYRE_5F:2/MAP_MT_PYRE_6F:1",
+ "MAP_MT_PYRE_5F:3/MAP_MT_PYRE_4F:2",
+ "MAP_MT_PYRE_5F:4/MAP_MT_PYRE_4F:3"
+ ]
+ },
+ "REGION_MT_PYRE_6F/MAIN": {
+ "parent_map": "MAP_MT_PYRE_6F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_MT_PYRE_6F_TM_SHADOW_BALL",
+ "TRAINER_VALERIE_1_REWARD",
+ "TRAINER_CEDRIC_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MT_PYRE_6F:0/MAP_MT_PYRE_5F:0",
+ "MAP_MT_PYRE_6F:1/MAP_MT_PYRE_5F:2"
+ ]
+ },
+ "REGION_MT_PYRE_EXTERIOR/MAIN": {
+ "parent_map": "MAP_MT_PYRE_EXTERIOR",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_MT_PYRE_EXTERIOR_MAX_POTION",
+ "ITEM_MT_PYRE_EXTERIOR_TM_SKILL_SWAP",
+ "HIDDEN_ITEM_MT_PYRE_EXTERIOR_ULTRA_BALL",
+ "HIDDEN_ITEM_MT_PYRE_EXTERIOR_MAX_ETHER"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MT_PYRE_EXTERIOR:0/MAP_MT_PYRE_1F:1",
+ "MAP_MT_PYRE_EXTERIOR:1,2/MAP_MT_PYRE_SUMMIT:1"
+ ]
+ },
+ "REGION_MT_PYRE_SUMMIT/MAIN": {
+ "parent_map": "MAP_MT_PYRE_SUMMIT",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_MT_PYRE_SUMMIT_ZINC",
+ "HIDDEN_ITEM_MT_PYRE_SUMMIT_RARE_CANDY",
+ "NPC_GIFT_RECEIVED_MAGMA_EMBLEM",
+ "TRAINER_GRUNT_MT_PYRE_1_REWARD",
+ "TRAINER_GRUNT_MT_PYRE_2_REWARD",
+ "TRAINER_GRUNT_MT_PYRE_3_REWARD",
+ "TRAINER_GRUNT_MT_PYRE_4_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_MT_PYRE_SUMMIT:0,1,2/MAP_MT_PYRE_EXTERIOR:1"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_1F/MAIN": {
+ "parent_map": "MAP_AQUA_HIDEOUT_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GRUNT_AQUA_HIDEOUT_1_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_AQUA_HIDEOUT_1F/WATER"
+ ],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_1F:2/MAP_AQUA_HIDEOUT_B1F:0"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_1F/WATER": {
+ "parent_map": "MAP_AQUA_HIDEOUT_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_AQUA_HIDEOUT_1F/MAIN"
+ ],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_1F:0,1/MAP_LILYCOVE_CITY:6"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B1F/WEST_BOTTOM": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GRUNT_AQUA_HIDEOUT_2_REWARD",
+ "TRAINER_GRUNT_AQUA_HIDEOUT_5_REWARD",
+ "TRAINER_GRUNT_AQUA_HIDEOUT_7_REWARD"
+
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B1F:8/MAP_AQUA_HIDEOUT_B1F:5",
+ "MAP_AQUA_HIDEOUT_B1F:9/MAP_AQUA_HIDEOUT_B1F:12",
+ "MAP_AQUA_HIDEOUT_B1F:10/MAP_AQUA_HIDEOUT_B1F:6"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B1F/WEST_TOP_LEFT": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GRUNT_AQUA_HIDEOUT_3_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B1F:2/MAP_AQUA_HIDEOUT_B2F:1",
+ "MAP_AQUA_HIDEOUT_B1F:3/MAP_AQUA_HIDEOUT_B2F:2"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B1F/WEST_TOP_CENTER": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B1F:1/MAP_AQUA_HIDEOUT_B2F:0",
+ "MAP_AQUA_HIDEOUT_B1F:6/MAP_AQUA_HIDEOUT_B1F:10"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B1F/WEST_TOP_RIGHT": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B1F:0/MAP_AQUA_HIDEOUT_1F:2",
+ "MAP_AQUA_HIDEOUT_B1F:4/MAP_AQUA_HIDEOUT_B1F:7",
+ "MAP_AQUA_HIDEOUT_B1F:5/MAP_AQUA_HIDEOUT_B1F:8"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B1F/WEST_CENTER_RIGHT": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_AQUA_HIDEOUT_B1F_MAX_ELIXIR"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B1F:7/MAP_AQUA_HIDEOUT_B1F:4"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B1F/WEST_CENTER": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_AQUA_HIDEOUT_B1F_NUGGET",
+ "ITEM_AQUA_HIDEOUT_B1F_MASTER_BALL"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B1F:11/MAP_AQUA_HIDEOUT_B1F:22"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B1F/EAST_TOP": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B1F:12/MAP_AQUA_HIDEOUT_B1F:9",
+ "MAP_AQUA_HIDEOUT_B1F:13/MAP_AQUA_HIDEOUT_B1F:18",
+ "MAP_AQUA_HIDEOUT_B1F:14/MAP_AQUA_HIDEOUT_B1F:12!",
+ "MAP_AQUA_HIDEOUT_B1F:15/MAP_AQUA_HIDEOUT_B1F:16"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B1F/EAST_ROW_1_RIGHT": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_AQUA_HIDEOUT_B1F/EAST_ROW_1_CENTER"
+ ],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B1F:18/MAP_AQUA_HIDEOUT_B1F:13"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B1F/EAST_ROW_1_CENTER": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_AQUA_HIDEOUT_B1F/EAST_ROW_1_LEFT",
+ "REGION_AQUA_HIDEOUT_B1F/EAST_ROW_1_RIGHT"
+ ],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B1F:17/MAP_AQUA_HIDEOUT_B1F:20"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B1F/EAST_ROW_1_LEFT": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_AQUA_HIDEOUT_B1F/EAST_ROW_1_CENTER"
+ ],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B1F:16/MAP_AQUA_HIDEOUT_B1F:15"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B1F/EAST_ROW_2_RIGHT": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_AQUA_HIDEOUT_B1F/EAST_ROW_2_CENTER"
+ ],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B1F:21/MAP_AQUA_HIDEOUT_B1F:12!"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B1F/EAST_ROW_2_CENTER": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_AQUA_HIDEOUT_B1F/EAST_ROW_2_LEFT",
+ "REGION_AQUA_HIDEOUT_B1F/EAST_ROW_2_RIGHT"
+ ],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B1F:20/MAP_AQUA_HIDEOUT_B1F:17"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B1F/EAST_ROW_2_LEFT": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_AQUA_HIDEOUT_B1F/EAST_ROW_2_CENTER"
+ ],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B1F:19/MAP_AQUA_HIDEOUT_B1F:24"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B1F/EAST_BOTTOM": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B1F:22/MAP_AQUA_HIDEOUT_B1F:11",
+ "MAP_AQUA_HIDEOUT_B1F:23/MAP_AQUA_HIDEOUT_B1F:17!",
+ "MAP_AQUA_HIDEOUT_B1F:24/MAP_AQUA_HIDEOUT_B1F:19"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B2F/TOP_LEFT": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B2F:2/MAP_AQUA_HIDEOUT_B1F:3",
+ "MAP_AQUA_HIDEOUT_B2F:5/MAP_AQUA_HIDEOUT_B2F:3"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B2F/TOP_CENTER": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GRUNT_AQUA_HIDEOUT_6_REWARD",
+ "TRAINER_GRUNT_AQUA_HIDEOUT_8_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B2F:1/MAP_AQUA_HIDEOUT_B1F:2",
+ "MAP_AQUA_HIDEOUT_B2F:4/MAP_AQUA_HIDEOUT_B2F:8"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B2F/TOP_RIGHT": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GRUNT_AQUA_HIDEOUT_4_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B2F:0/MAP_AQUA_HIDEOUT_B1F:1",
+ "MAP_AQUA_HIDEOUT_B2F:3/MAP_AQUA_HIDEOUT_B2F:5",
+ "MAP_AQUA_HIDEOUT_B2F:6/MAP_AQUA_HIDEOUT_B2F:7"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B2F/BOTTOM_LEFT": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_AQUA_HIDEOUT_B2F_NEST_BALL"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B2F:7/MAP_AQUA_HIDEOUT_B2F:6"
+ ]
+ },
+ "REGION_AQUA_HIDEOUT_B2F/BOTTOM_RIGHT": {
+ "parent_map": "MAP_AQUA_HIDEOUT_B2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_MATT_REWARD"
+ ],
+ "events": [
+ "EVENT_CLEAR_AQUA_HIDEOUT"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_AQUA_HIDEOUT_B2F:8/MAP_AQUA_HIDEOUT_B2F:4",
+ "MAP_AQUA_HIDEOUT_B2F:9/MAP_AQUA_HIDEOUT_B1F:4!"
+ ]
+ },
+ "REGION_SHOAL_CAVE_ENTRANCE_ROOM/SOUTH": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_ENTRANCE_ROOM/LOW_TIDE_LOWER",
+ "REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"
+ ],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:0/MAP_ROUTE125:0"
+ ]
+ },
+ "REGION_SHOAL_CAVE_ENTRANCE_ROOM/NORTH_WEST_CORNER": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"
+ ],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:2/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:6"
+ ]
+ },
+ "REGION_SHOAL_CAVE_ENTRANCE_ROOM/NORTH_EAST_CORNER": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_SHOAL_CAVE_ENTRANCE_BIG_PEARL"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"
+ ],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:3/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:7"
+ ]
+ },
+ "REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_ENTRANCE_ROOM/SOUTH",
+ "REGION_SHOAL_CAVE_ENTRANCE_ROOM/NORTH_WEST_CORNER"
+ ],
+ "warps": []
+ },
+ "REGION_SHOAL_CAVE_ENTRANCE_ROOM/LOW_TIDE_LOWER": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_ENTRANCE_ROOM/SOUTH"
+ ],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:1/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:0"
+ ]
+ },
+ "REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_CORNER": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND"
+ ],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:7/MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:3"
+ ]
+ },
+ "REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_CORNER",
+ "REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_WATER",
+ "REGION_SHOAL_CAVE_INNER_ROOM/EAST_WATER",
+ "REGION_SHOAL_CAVE_INNER_ROOM/NORTH_WEST_WATER"
+ ],
+ "warps": []
+ },
+ "REGION_SHOAL_CAVE_INNER_ROOM/LOW_TIDE_EAST_MIDDLE_GROUND": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_INNER_ROOM/LOW_TIDE_SOUTH_EAST_LOWER",
+ "REGION_SHOAL_CAVE_INNER_ROOM/LOW_TIDE_EAST_LOWER"
+ ],
+ "warps": []
+ },
+ "REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_WEST_CORNER": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_INNER_ROOM/NORTH_WEST_WATER"
+ ],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:6/MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:2"
+ ]
+ },
+ "REGION_SHOAL_CAVE_INNER_ROOM/BRIDGES": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:2/MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM:1",
+ "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:3/MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:0"
+ ]
+ },
+ "REGION_SHOAL_CAVE_INNER_ROOM/RARE_CANDY_PLATFORM": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_SHOAL_CAVE_INNER_ROOM_RARE_CANDY"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_WATER"
+ ],
+ "warps": []
+ },
+ "REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_WATER": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND",
+ "REGION_SHOAL_CAVE_INNER_ROOM/RARE_CANDY_PLATFORM"
+ ],
+ "warps": []
+ },
+ "REGION_SHOAL_CAVE_INNER_ROOM/EAST_WATER": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND"
+ ],
+ "warps": []
+ },
+ "REGION_SHOAL_CAVE_INNER_ROOM/NORTH_WEST_WATER": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND",
+ "REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_WEST_CORNER"
+ ],
+ "warps": []
+ },
+ "REGION_SHOAL_CAVE_INNER_ROOM/LOW_TIDE_SOUTH_EAST_LOWER": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_INNER_ROOM/LOW_TIDE_EAST_MIDDLE_GROUND"
+ ],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:0/MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:1",
+ "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:5/MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:2"
+ ]
+ },
+ "REGION_SHOAL_CAVE_INNER_ROOM/LOW_TIDE_EAST_LOWER": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_INNER_ROOM/LOW_TIDE_EAST_MIDDLE_GROUND"
+ ],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:1/MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM:0"
+ ]
+ },
+ "REGION_SHOAL_CAVE_INNER_ROOM/LOW_TIDE_NORTH_WEST_LOWER": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:4/MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:1"
+ ]
+ },
+ "REGION_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM/MAIN": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_SHOAL_CAVE_STAIRS_ROOM_ICE_HEAL"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM:0/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:1",
+ "MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM:1/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:2"
+ ]
+ },
+ "REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/NORTH_WEST": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_FOCUS_BAND"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/EAST",
+ "REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/SOUTH"
+ ],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:0/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:3",
+ "MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:1/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:4"
+ ]
+ },
+ "REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/SOUTH": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:2/MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM:5"
+ ]
+ },
+ "REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/EAST": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/NORTH_WEST"
+ ],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:3/MAP_SHOAL_CAVE_LOW_TIDE_ICE_ROOM:0"
+ ]
+ },
+ "REGION_SHOAL_CAVE_LOW_TIDE_ICE_ROOM/MAIN": {
+ "parent_map": "MAP_SHOAL_CAVE_LOW_TIDE_ICE_ROOM",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_SHOAL_CAVE_ICE_ROOM_TM_HAIL",
+ "ITEM_SHOAL_CAVE_ICE_ROOM_NEVER_MELT_ICE"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SHOAL_CAVE_LOW_TIDE_ICE_ROOM:0/MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM:3"
+ ]
+ },
+ "REGION_UNDERWATER_SEAFLOOR_CAVERN/MAIN": {
+ "parent_map": "MAP_UNDERWATER_SEAFLOOR_CAVERN",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ENTRANCE/WATER"
+ ],
+ "warps": [
+ "MAP_UNDERWATER_SEAFLOOR_CAVERN:0/MAP_UNDERWATER_ROUTE128:0"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ENTRANCE/WATER": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ENTRANCE",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_UNDERWATER_SEAFLOOR_CAVERN/MAIN",
+ "REGION_SEAFLOOR_CAVERN_ENTRANCE/MAIN"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ENTRANCE:0/MAP_UNDERWATER_ROUTE128:0!"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ENTRANCE/MAIN": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ENTRANCE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ENTRANCE/WATER"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ENTRANCE:1/MAP_SEAFLOOR_CAVERN_ROOM1:0"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM1/NORTH": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM1",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GRUNT_SEAFLOOR_CAVERN_1_REWARD",
+ "TRAINER_GRUNT_SEAFLOOR_CAVERN_2_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM1/SOUTH"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM1:2/MAP_SEAFLOOR_CAVERN_ROOM2:0",
+ "MAP_SEAFLOOR_CAVERN_ROOM1:1/MAP_SEAFLOOR_CAVERN_ROOM5:0"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM1/SOUTH": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM1",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM1/NORTH"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM1:0/MAP_SEAFLOOR_CAVERN_ENTRANCE:1"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM2",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST",
+ "REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM2:1/MAP_SEAFLOOR_CAVERN_ROOM4:0"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM2",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST",
+ "REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_EAST",
+ "REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM2:2/MAP_SEAFLOOR_CAVERN_ROOM6:0"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM2",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM2:0/MAP_SEAFLOOR_CAVERN_ROOM1:2"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_EAST": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM2",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM2:3/MAP_SEAFLOOR_CAVERN_ROOM7:0"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM3/MAIN": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM3",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GRUNT_SEAFLOOR_CAVERN_5_REWARD",
+ "TRAINER_SHELLY_SEAFLOOR_CAVERN_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM3:2/MAP_SEAFLOOR_CAVERN_ROOM6:1",
+ "MAP_SEAFLOOR_CAVERN_ROOM3:0/MAP_SEAFLOOR_CAVERN_ROOM8:1",
+ "MAP_SEAFLOOR_CAVERN_ROOM3:1/MAP_SEAFLOOR_CAVERN_ROOM7:1"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM4/NORTH_WEST": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM4",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GRUNT_SEAFLOOR_CAVERN_3_REWARD",
+ "TRAINER_GRUNT_SEAFLOOR_CAVERN_4_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM4/EAST"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM4:1/MAP_SEAFLOOR_CAVERN_ROOM5:1"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM4/EAST": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM4",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM4/SOUTH"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM4:2/MAP_SEAFLOOR_CAVERN_ROOM5:2",
+ "MAP_SEAFLOOR_CAVERN_ROOM4:0/MAP_SEAFLOOR_CAVERN_ROOM2:1"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM4/SOUTH": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM4",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM4:3/MAP_SEAFLOOR_CAVERN_ENTRANCE:1!"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM5",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM5/EAST",
+ "REGION_SEAFLOOR_CAVERN_ROOM5/SOUTH_WEST"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM5:0/MAP_SEAFLOOR_CAVERN_ROOM1:1"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM5/EAST": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM5",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM5:1/MAP_SEAFLOOR_CAVERN_ROOM4:1"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM5/SOUTH_WEST": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM5",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM5:2/MAP_SEAFLOOR_CAVERN_ROOM4:2"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM6/NORTH_WEST": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM6",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM6/WATER"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM6:1/MAP_SEAFLOOR_CAVERN_ROOM3:2"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM6/WATER": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM6",
+ "has_grass": true,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM6/NORTH_WEST"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM6:2/MAP_SEAFLOOR_CAVERN_ENTRANCE:1!"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM6/SOUTH": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM6",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM6/WATER"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM6:0/MAP_SEAFLOOR_CAVERN_ROOM2:2"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM7/NORTH": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM7",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM7/WATER"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM7:1/MAP_SEAFLOOR_CAVERN_ROOM3:1"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM7/WATER": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM7",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM7/SOUTH"
+ ],
+ "warps": []
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM7/SOUTH": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM7",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM7/WATER"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM7:0/MAP_SEAFLOOR_CAVERN_ROOM2:3"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM8/NORTH": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM8",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM8/SOUTH"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM8:0/MAP_SEAFLOOR_CAVERN_ROOM9:0"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM8/SOUTH": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM8",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEAFLOOR_CAVERN_ROOM8/NORTH"
+ ],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM8:1/MAP_SEAFLOOR_CAVERN_ROOM3:0"
+ ]
+ },
+ "REGION_SEAFLOOR_CAVERN_ROOM9/MAIN": {
+ "parent_map": "MAP_SEAFLOOR_CAVERN_ROOM9",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_SEAFLOOR_CAVERN_ROOM_9_TM_EARTHQUAKE",
+ "TRAINER_ARCHIE_REWARD"
+ ],
+ "events": [
+ "EVENT_RELEASE_KYOGRE"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_SEAFLOOR_CAVERN_ROOM9:0/MAP_SEAFLOOR_CAVERN_ROOM8:0"
+ ]
+ },
+ "REGION_CAVE_OF_ORIGIN_ENTRANCE/MAIN": {
+ "parent_map": "MAP_CAVE_OF_ORIGIN_ENTRANCE",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_CAVE_OF_ORIGIN_ENTRANCE:0/MAP_SOOTOPOLIS_CITY:3",
+ "MAP_CAVE_OF_ORIGIN_ENTRANCE:1/MAP_CAVE_OF_ORIGIN_1F:0"
+ ]
+ },
+ "REGION_CAVE_OF_ORIGIN_1F/MAIN": {
+ "parent_map": "MAP_CAVE_OF_ORIGIN_1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_CAVE_OF_ORIGIN_1F:0/MAP_CAVE_OF_ORIGIN_ENTRANCE:1",
+ "MAP_CAVE_OF_ORIGIN_1F:1/MAP_CAVE_OF_ORIGIN_B1F:0"
+ ]
+ },
+ "REGION_CAVE_OF_ORIGIN_B1F/MAIN": {
+ "parent_map": "MAP_CAVE_OF_ORIGIN_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_CAVE_OF_ORIGIN_B1F:0/MAP_CAVE_OF_ORIGIN_1F:1"
+ ]
+ },
+ "REGION_SKY_PILLAR_ENTRANCE/MAIN": {
+ "parent_map": "MAP_SKY_PILLAR_ENTRANCE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SKY_PILLAR_ENTRANCE:0/MAP_ROUTE131:0",
+ "MAP_SKY_PILLAR_ENTRANCE:1/MAP_SKY_PILLAR_OUTSIDE:0"
+ ]
+ },
+ "REGION_SKY_PILLAR_OUTSIDE/MAIN": {
+ "parent_map": "MAP_SKY_PILLAR_OUTSIDE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SKY_PILLAR_OUTSIDE:0/MAP_SKY_PILLAR_ENTRANCE:1",
+ "MAP_SKY_PILLAR_OUTSIDE:1/MAP_SKY_PILLAR_1F:0"
+ ]
+ },
+ "REGION_SKY_PILLAR_1F/MAIN": {
+ "parent_map": "MAP_SKY_PILLAR_1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SKY_PILLAR_1F:0,1/MAP_SKY_PILLAR_OUTSIDE:1",
+ "MAP_SKY_PILLAR_1F:2/MAP_SKY_PILLAR_2F:0"
+ ]
+ },
+ "REGION_SKY_PILLAR_2F/LEFT": {
+ "parent_map": "MAP_SKY_PILLAR_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SKY_PILLAR_2F/RIGHT",
+ "REGION_SKY_PILLAR_1F/MAIN"
+ ],
+ "warps": [
+ "MAP_SKY_PILLAR_2F:1/MAP_SKY_PILLAR_3F:0"
+ ]
+ },
+ "REGION_SKY_PILLAR_2F/RIGHT": {
+ "parent_map": "MAP_SKY_PILLAR_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SKY_PILLAR_2F/LEFT",
+ "REGION_SKY_PILLAR_1F/MAIN"
+ ],
+ "warps": [
+ "MAP_SKY_PILLAR_2F:0/MAP_SKY_PILLAR_1F:2"
+ ]
+ },
+ "REGION_SKY_PILLAR_3F/MAIN": {
+ "parent_map": "MAP_SKY_PILLAR_3F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SKY_PILLAR_3F:0/MAP_SKY_PILLAR_2F:1",
+ "MAP_SKY_PILLAR_3F:1/MAP_SKY_PILLAR_4F:0"
+ ]
+ },
+ "REGION_SKY_PILLAR_3F/TOP_CENTER": {
+ "parent_map": "MAP_SKY_PILLAR_3F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SKY_PILLAR_3F:2/MAP_SKY_PILLAR_4F:1"
+ ]
+ },
+ "REGION_SKY_PILLAR_4F/MAIN": {
+ "parent_map": "MAP_SKY_PILLAR_4F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SKY_PILLAR_4F/ABOVE_3F_TOP_CENTER",
+ "REGION_SKY_PILLAR_3F/MAIN"
+ ],
+ "warps": [
+ "MAP_SKY_PILLAR_4F:0/MAP_SKY_PILLAR_3F:1"
+ ]
+ },
+ "REGION_SKY_PILLAR_4F/ABOVE_3F_TOP_CENTER": {
+ "parent_map": "MAP_SKY_PILLAR_4F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SKY_PILLAR_3F/TOP_CENTER"
+ ],
+ "warps": []
+ },
+ "REGION_SKY_PILLAR_4F/TOP_LEFT": {
+ "parent_map": "MAP_SKY_PILLAR_4F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SKY_PILLAR_4F:1/MAP_SKY_PILLAR_3F:2",
+ "MAP_SKY_PILLAR_4F:2/MAP_SKY_PILLAR_5F:0"
+ ]
+ },
+ "REGION_SKY_PILLAR_5F/MAIN": {
+ "parent_map": "MAP_SKY_PILLAR_5F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SKY_PILLAR_5F:0/MAP_SKY_PILLAR_4F:2",
+ "MAP_SKY_PILLAR_5F:1/MAP_SKY_PILLAR_TOP:0"
+ ]
+ },
+ "REGION_SKY_PILLAR_TOP/MAIN": {
+ "parent_map": "MAP_SKY_PILLAR_TOP",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_ENCOUNTER_RAYQUAZA"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_SKY_PILLAR_TOP:0/MAP_SKY_PILLAR_5F:1"
+ ]
+ },
+ "REGION_UNDERWATER_SEALED_CHAMBER/MAIN": {
+ "parent_map": "MAP_UNDERWATER_SEALED_CHAMBER",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEALED_CHAMBER_OUTER_ROOM/MAIN",
+ "REGION_ROUTE134/MAIN"
+ ],
+ "warps": [
+ "MAP_UNDERWATER_SEALED_CHAMBER:0/MAP_UNDERWATER_ROUTE134:0"
+ ]
+ },
+ "REGION_SEALED_CHAMBER_OUTER_ROOM/MAIN": {
+ "parent_map": "MAP_SEALED_CHAMBER_OUTER_ROOM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_UNDERWATER_SEALED_CHAMBER/MAIN",
+ "REGION_SEALED_CHAMBER_OUTER_ROOM/CRUMBLED_WALL"
+ ],
+ "warps": []
+ },
+ "REGION_SEALED_CHAMBER_OUTER_ROOM/CRUMBLED_WALL": {
+ "parent_map": "MAP_SEALED_CHAMBER_OUTER_ROOM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SEALED_CHAMBER_OUTER_ROOM/MAIN"
+ ],
+ "warps": [
+ "MAP_SEALED_CHAMBER_OUTER_ROOM:0/MAP_SEALED_CHAMBER_INNER_ROOM:0"
+ ]
+ },
+ "REGION_SEALED_CHAMBER_INNER_ROOM/MAIN": {
+ "parent_map": "MAP_SEALED_CHAMBER_INNER_ROOM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_UNDO_REGI_SEAL"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_SEALED_CHAMBER_INNER_ROOM:0/MAP_SEALED_CHAMBER_OUTER_ROOM:0"
+ ]
+ },
+ "REGION_VICTORY_ROAD_1F/NORTH_EAST": {
+ "parent_map": "MAP_VICTORY_ROAD_1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_EDGAR_REWARD",
+ "TRAINER_KATELYNN_REWARD",
+ "TRAINER_QUINCY_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_VICTORY_ROAD_1F:1/MAP_EVER_GRANDE_CITY:3",
+ "MAP_VICTORY_ROAD_1F:2/MAP_VICTORY_ROAD_B1F:5"
+ ]
+ },
+ "REGION_VICTORY_ROAD_1F/SOUTH_WEST": {
+ "parent_map": "MAP_VICTORY_ROAD_1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_VICTORY_ROAD_1F_MAX_ELIXIR",
+ "TRAINER_ALBERT_REWARD",
+ "TRAINER_HOPE_REWARD",
+ "TRAINER_WALLY_VR_1_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_VICTORY_ROAD_1F:0/MAP_EVER_GRANDE_CITY:2",
+ "MAP_VICTORY_ROAD_1F:4/MAP_VICTORY_ROAD_B1F:4"
+ ]
+ },
+ "REGION_VICTORY_ROAD_1F/SOUTH_EAST": {
+ "parent_map": "MAP_VICTORY_ROAD_1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_VICTORY_ROAD_1F_PP_UP",
+ "HIDDEN_ITEM_VICTORY_ROAD_1F_ULTRA_BALL"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_VICTORY_ROAD_1F:3/MAP_VICTORY_ROAD_B1F:2"
+ ]
+ },
+ "REGION_VICTORY_ROAD_B1F/NORTH_EAST": {
+ "parent_map": "MAP_VICTORY_ROAD_B1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_VICTORY_ROAD_B1F_TM_PSYCHIC"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_VICTORY_ROAD_B1F:3/MAP_VICTORY_ROAD_B2F:1"
+ ]
+ },
+ "REGION_VICTORY_ROAD_B1F/SOUTH_WEST_MAIN": {
+ "parent_map": "MAP_VICTORY_ROAD_B1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_MICHELLE_REWARD",
+ "TRAINER_MITCHELL_REWARD",
+ "TRAINER_HALLE_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_VICTORY_ROAD_B1F/SOUTH_WEST_LADDER_UP"
+ ],
+ "warps": [
+ "MAP_VICTORY_ROAD_B1F:1/MAP_VICTORY_ROAD_B2F:2",
+ "MAP_VICTORY_ROAD_B1F:6/MAP_VICTORY_ROAD_B2F:3"
+ ]
+ },
+ "REGION_VICTORY_ROAD_B1F/SOUTH_WEST_LADDER_UP": {
+ "parent_map": "MAP_VICTORY_ROAD_B1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_VICTORY_ROAD_B1F/SOUTH_WEST_MAIN"
+ ],
+ "warps": [
+ "MAP_VICTORY_ROAD_B1F:5/MAP_VICTORY_ROAD_1F:2"
+ ]
+ },
+ "REGION_VICTORY_ROAD_B1F/MAIN_UPPER": {
+ "parent_map": "MAP_VICTORY_ROAD_B1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_SHANNON_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST"
+ ],
+ "warps": [
+ "MAP_VICTORY_ROAD_B1F:2/MAP_VICTORY_ROAD_1F:3"
+ ]
+ },
+ "REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST": {
+ "parent_map": "MAP_VICTORY_ROAD_B1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_VICTORY_ROAD_B1F_FULL_RESTORE",
+ "TRAINER_SAMUEL_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST"
+ ],
+ "warps": [
+ "MAP_VICTORY_ROAD_B1F:0/MAP_VICTORY_ROAD_B2F:0"
+ ]
+ },
+ "REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST": {
+ "parent_map": "MAP_VICTORY_ROAD_B1F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_VICTORY_ROAD_B1F/MAIN_UPPER",
+ "REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST"
+ ],
+ "warps": [
+ "MAP_VICTORY_ROAD_B1F:4/MAP_VICTORY_ROAD_1F:4"
+ ]
+ },
+ "REGION_VICTORY_ROAD_B2F/LOWER_WEST": {
+ "parent_map": "MAP_VICTORY_ROAD_B2F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER"
+ ],
+ "warps": [
+ "MAP_VICTORY_ROAD_B2F:3/MAP_VICTORY_ROAD_B1F:6"
+ ]
+ },
+ "REGION_VICTORY_ROAD_B2F/LOWER_WEST_ISLAND": {
+ "parent_map": "MAP_VICTORY_ROAD_B2F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER"
+ ],
+ "warps": [
+ "MAP_VICTORY_ROAD_B2F:2/MAP_VICTORY_ROAD_B1F:1"
+ ]
+ },
+ "REGION_VICTORY_ROAD_B2F/LOWER_EAST": {
+ "parent_map": "MAP_VICTORY_ROAD_B2F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "TRAINER_JULIE_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER"
+ ],
+ "warps": [
+ "MAP_VICTORY_ROAD_B2F:0/MAP_VICTORY_ROAD_B1F:0"
+ ]
+ },
+ "REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER": {
+ "parent_map": "MAP_VICTORY_ROAD_B2F",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_VICTORY_ROAD_B2F/UPPER_WATER",
+ "REGION_VICTORY_ROAD_B2F/LOWER_WEST",
+ "REGION_VICTORY_ROAD_B2F/LOWER_WEST_ISLAND"
+ ],
+ "warps": []
+ },
+ "REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER": {
+ "parent_map": "MAP_VICTORY_ROAD_B2F",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_VICTORY_ROAD_B2F/UPPER_WATER",
+ "REGION_VICTORY_ROAD_B2F/UPPER",
+ "REGION_VICTORY_ROAD_B2F/LOWER_EAST"
+ ],
+ "warps": []
+ },
+ "REGION_VICTORY_ROAD_B2F/UPPER": {
+ "parent_map": "MAP_VICTORY_ROAD_B2F",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "HIDDEN_ITEM_VICTORY_ROAD_B2F_MAX_REPEL",
+ "TRAINER_OWEN_REWARD",
+ "TRAINER_DIANNE_REWARD",
+ "TRAINER_FELIX_REWARD",
+ "TRAINER_CAROLINE_REWARD"
+
+ ],
+ "events": [],
+ "exits": [
+ "REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER",
+ "REGION_VICTORY_ROAD_B2F/LOWER_EAST",
+ "REGION_VICTORY_ROAD_B2F/UPPER_WATER"
+ ],
+ "warps": [
+ "MAP_VICTORY_ROAD_B2F:1/MAP_VICTORY_ROAD_B1F:3"
+ ]
+ },
+ "REGION_VICTORY_ROAD_B2F/UPPER_WATER": {
+ "parent_map": "MAP_VICTORY_ROAD_B2F",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_VICTORY_ROAD_B2F_FULL_HEAL",
+ "HIDDEN_ITEM_VICTORY_ROAD_B2F_ELIXIR",
+ "TRAINER_VITO_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER",
+ "REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER",
+ "REGION_VICTORY_ROAD_B2F/UPPER"
+ ],
+ "warps": []
+ },
+ "REGION_TERRA_CAVE_ENTRANCE/MAIN": {
+ "parent_map": "MAP_TERRA_CAVE_ENTRANCE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_TERRA_CAVE_ENTRANCE:1/MAP_TERRA_CAVE_END:0"
+ ]
+ },
+ "REGION_TERRA_CAVE_END/MAIN": {
+ "parent_map": "MAP_TERRA_CAVE_END",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_ENCOUNTER_GROUDON"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_TERRA_CAVE_END:0/MAP_TERRA_CAVE_ENTRANCE:1"
+ ]
+ },
+ "REGION_UNDERWATER_MARINE_CAVE/MAIN": {
+ "parent_map": "MAP_UNDERWATER_MARINE_CAVE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_MARINE_CAVE_ENTRANCE/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_MARINE_CAVE_ENTRANCE/MAIN": {
+ "parent_map": "MAP_MARINE_CAVE_ENTRANCE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_UNDERWATER_MARINE_CAVE/MAIN"
+ ],
+ "warps": [
+ "MAP_MARINE_CAVE_ENTRANCE:0/MAP_MARINE_CAVE_END:0"
+ ]
+ },
+ "REGION_MARINE_CAVE_END/MAIN": {
+ "parent_map": "MAP_MARINE_CAVE_END",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_ENCOUNTER_KYOGRE"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_MARINE_CAVE_END:0/MAP_MARINE_CAVE_ENTRANCE:0"
+ ]
+ }
+}
diff --git a/worlds/pokemon_emerald/data/regions/islands.json b/worlds/pokemon_emerald/data/regions/islands.json
new file mode 100644
index 000000000000..442672935764
--- /dev/null
+++ b/worlds/pokemon_emerald/data/regions/islands.json
@@ -0,0 +1,378 @@
+{
+ "REGION_SOUTHERN_ISLAND_EXTERIOR/MAIN": {
+ "parent_map": "MAP_SOUTHERN_ISLAND_EXTERIOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LILYCOVE_CITY_HARBOR/MAIN"
+ ],
+ "warps": [
+ "MAP_SOUTHERN_ISLAND_EXTERIOR:0,1/MAP_SOUTHERN_ISLAND_INTERIOR:0,1"
+ ]
+ },
+ "REGION_SOUTHERN_ISLAND_INTERIOR/MAIN": {
+ "parent_map": "MAP_SOUTHERN_ISLAND_INTERIOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_ENCOUNTER_LATIAS"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_SOUTHERN_ISLAND_INTERIOR:0,1/MAP_SOUTHERN_ISLAND_EXTERIOR:0,1"
+ ]
+ },
+ "REGION_FARAWAY_ISLAND_ENTRANCE/MAIN": {
+ "parent_map": "MAP_FARAWAY_ISLAND_ENTRANCE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LILYCOVE_CITY_HARBOR/MAIN"
+ ],
+ "warps": [
+ "MAP_FARAWAY_ISLAND_ENTRANCE:0,1/MAP_FARAWAY_ISLAND_INTERIOR:0,1"
+ ]
+ },
+ "REGION_FARAWAY_ISLAND_INTERIOR/MAIN": {
+ "parent_map": "MAP_FARAWAY_ISLAND_INTERIOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_ENCOUNTER_MEW"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_FARAWAY_ISLAND_INTERIOR:0,1/MAP_FARAWAY_ISLAND_ENTRANCE:0,1"
+ ]
+ },
+ "REGION_BIRTH_ISLAND_HARBOR/MAIN": {
+ "parent_map": "MAP_BIRTH_ISLAND_HARBOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LILYCOVE_CITY_HARBOR/MAIN"
+ ],
+ "warps": [
+ "MAP_BIRTH_ISLAND_HARBOR:0/MAP_BIRTH_ISLAND_EXTERIOR:0"
+ ]
+ },
+ "REGION_BIRTH_ISLAND_EXTERIOR/MAIN": {
+ "parent_map": "MAP_BIRTH_ISLAND_EXTERIOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_ENCOUNTER_DEOXYS"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_BIRTH_ISLAND_EXTERIOR:0/MAP_BIRTH_ISLAND_HARBOR:0"
+ ]
+ },
+ "REGION_NAVEL_ROCK_HARBOR/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_HARBOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LILYCOVE_CITY_HARBOR/MAIN"
+ ],
+ "warps": [
+ "MAP_NAVEL_ROCK_HARBOR:0/MAP_NAVEL_ROCK_EXTERIOR:0"
+ ]
+ },
+ "REGION_NAVEL_ROCK_EXTERIOR/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_EXTERIOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_EXTERIOR:0/MAP_NAVEL_ROCK_HARBOR:0",
+ "MAP_NAVEL_ROCK_EXTERIOR:1/MAP_NAVEL_ROCK_ENTRANCE:1"
+ ]
+ },
+ "REGION_NAVEL_ROCK_ENTRANCE/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_ENTRANCE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_ENTRANCE:0/MAP_NAVEL_ROCK_B1F:0",
+ "MAP_NAVEL_ROCK_ENTRANCE:1/MAP_NAVEL_ROCK_EXTERIOR:1"
+ ]
+ },
+ "REGION_NAVEL_ROCK_B1F/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_B1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_B1F:0/MAP_NAVEL_ROCK_ENTRANCE:0",
+ "MAP_NAVEL_ROCK_B1F:1/MAP_NAVEL_ROCK_FORK:1"
+ ]
+ },
+ "REGION_NAVEL_ROCK_FORK/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_FORK",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_FORK:0/MAP_NAVEL_ROCK_UP1:0",
+ "MAP_NAVEL_ROCK_FORK:1/MAP_NAVEL_ROCK_B1F:1",
+ "MAP_NAVEL_ROCK_FORK:2/MAP_NAVEL_ROCK_DOWN01:0"
+ ]
+ },
+ "REGION_NAVEL_ROCK_DOWN01/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_DOWN01",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_DOWN01:0/MAP_NAVEL_ROCK_FORK:2",
+ "MAP_NAVEL_ROCK_DOWN01:1/MAP_NAVEL_ROCK_DOWN02:0"
+ ]
+ },
+ "REGION_NAVEL_ROCK_DOWN02/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_DOWN02",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_DOWN02:1/MAP_NAVEL_ROCK_DOWN03:0",
+ "MAP_NAVEL_ROCK_DOWN02:0/MAP_NAVEL_ROCK_DOWN01:1"
+ ]
+ },
+ "REGION_NAVEL_ROCK_DOWN03/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_DOWN03",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_DOWN03:0/MAP_NAVEL_ROCK_DOWN02:1",
+ "MAP_NAVEL_ROCK_DOWN03:1/MAP_NAVEL_ROCK_DOWN04:0"
+ ]
+ },
+ "REGION_NAVEL_ROCK_DOWN04/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_DOWN04",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_DOWN04:1/MAP_NAVEL_ROCK_DOWN05:0",
+ "MAP_NAVEL_ROCK_DOWN04:0/MAP_NAVEL_ROCK_DOWN03:1"
+ ]
+ },
+ "REGION_NAVEL_ROCK_DOWN05/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_DOWN05",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_DOWN05:0/MAP_NAVEL_ROCK_DOWN04:1",
+ "MAP_NAVEL_ROCK_DOWN05:1/MAP_NAVEL_ROCK_DOWN06:0"
+ ]
+ },
+ "REGION_NAVEL_ROCK_DOWN06/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_DOWN06",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_DOWN06:1/MAP_NAVEL_ROCK_DOWN07:0",
+ "MAP_NAVEL_ROCK_DOWN06:0/MAP_NAVEL_ROCK_DOWN05:1"
+ ]
+ },
+ "REGION_NAVEL_ROCK_DOWN07/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_DOWN07",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_DOWN07:0/MAP_NAVEL_ROCK_DOWN06:1",
+ "MAP_NAVEL_ROCK_DOWN07:1/MAP_NAVEL_ROCK_DOWN08:0"
+ ]
+ },
+ "REGION_NAVEL_ROCK_DOWN08/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_DOWN08",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_DOWN08:1/MAP_NAVEL_ROCK_DOWN09:0",
+ "MAP_NAVEL_ROCK_DOWN08:0/MAP_NAVEL_ROCK_DOWN07:1"
+ ]
+ },
+ "REGION_NAVEL_ROCK_DOWN09/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_DOWN09",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_DOWN09:0/MAP_NAVEL_ROCK_DOWN08:1",
+ "MAP_NAVEL_ROCK_DOWN09:1/MAP_NAVEL_ROCK_DOWN10:0"
+ ]
+ },
+ "REGION_NAVEL_ROCK_DOWN10/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_DOWN10",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_DOWN10:1/MAP_NAVEL_ROCK_DOWN11:1",
+ "MAP_NAVEL_ROCK_DOWN10:0/MAP_NAVEL_ROCK_DOWN09:1"
+ ]
+ },
+ "REGION_NAVEL_ROCK_DOWN11/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_DOWN11",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_DOWN11:1/MAP_NAVEL_ROCK_DOWN10:1",
+ "MAP_NAVEL_ROCK_DOWN11:0/MAP_NAVEL_ROCK_BOTTOM:0"
+ ]
+ },
+ "REGION_NAVEL_ROCK_BOTTOM/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_BOTTOM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [
+ "EVENT_ENCOUNTER_LUGIA"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_BOTTOM:0/MAP_NAVEL_ROCK_DOWN11:0"
+ ]
+ },
+ "REGION_NAVEL_ROCK_UP1/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_UP1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_UP1:1/MAP_NAVEL_ROCK_UP2:0",
+ "MAP_NAVEL_ROCK_UP1:0/MAP_NAVEL_ROCK_FORK:0"
+ ]
+ },
+ "REGION_NAVEL_ROCK_UP2/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_UP2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_UP2:0/MAP_NAVEL_ROCK_UP1:1",
+ "MAP_NAVEL_ROCK_UP2:1/MAP_NAVEL_ROCK_UP3:0"
+ ]
+ },
+ "REGION_NAVEL_ROCK_UP3/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_UP3",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_UP3:1/MAP_NAVEL_ROCK_UP4:0",
+ "MAP_NAVEL_ROCK_UP3:0/MAP_NAVEL_ROCK_UP2:1"
+ ]
+ },
+ "REGION_NAVEL_ROCK_UP4/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_UP4",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_UP4:0/MAP_NAVEL_ROCK_UP3:1",
+ "MAP_NAVEL_ROCK_UP4:1/MAP_NAVEL_ROCK_TOP:0"
+ ]
+ },
+ "REGION_NAVEL_ROCK_TOP/MAIN": {
+ "parent_map": "MAP_NAVEL_ROCK_TOP",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_NAVEL_ROCK_TOP_SACRED_ASH"
+ ],
+ "events": [
+ "EVENT_ENCOUNTER_HO_OH"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_NAVEL_ROCK_TOP:0/MAP_NAVEL_ROCK_UP4:1"
+ ]
+ }
+}
diff --git a/worlds/pokemon_emerald/data/regions/routes.json b/worlds/pokemon_emerald/data/regions/routes.json
new file mode 100644
index 000000000000..94af91a49f4c
--- /dev/null
+++ b/worlds/pokemon_emerald/data/regions/routes.json
@@ -0,0 +1,3315 @@
+{
+ "REGION_ROUTE101/MAIN": {
+ "parent_map": "MAP_ROUTE101",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_LITTLEROOT_TOWN/MAIN",
+ "REGION_OLDALE_TOWN/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE102/MAIN": {
+ "parent_map": "MAP_ROUTE102",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_102_POTION",
+ "BERRY_TREE_01",
+ "BERRY_TREE_02",
+ "TRAINER_CALVIN_1_REWARD",
+ "TRAINER_RICK_REWARD",
+ "TRAINER_ALLEN_REWARD",
+ "TRAINER_TIANA_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_OLDALE_TOWN/MAIN",
+ "REGION_PETALBURG_CITY/MAIN",
+ "REGION_ROUTE102/POND"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE102/POND": {
+ "parent_map": "MAP_ROUTE102",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE103/WEST": {
+ "parent_map": "MAP_ROUTE103",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "TRAINER_BRENDAN_ROUTE_103_MUDKIP_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE103/WATER",
+ "REGION_OLDALE_TOWN/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE103/WATER": {
+ "parent_map": "MAP_ROUTE103",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "TRAINER_ISABELLE_REWARD",
+ "TRAINER_PETE_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE103/WEST",
+ "REGION_ROUTE103/EAST"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE103/EAST": {
+ "parent_map": "MAP_ROUTE103",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "TRAINER_ANDREW_REWARD",
+ "TRAINER_MIGUEL_1_REWARD",
+ "TRAINER_AMY_AND_LIV_1_REWARD",
+ "TRAINER_DAISY_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE103/WATER",
+ "REGION_ROUTE103/EAST_TREE_MAZE",
+ "REGION_ROUTE110/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE103/EAST_TREE_MAZE": {
+ "parent_map": "MAP_ROUTE103",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ROUTE_103_GUARD_SPEC",
+ "ITEM_ROUTE_103_PP_UP",
+ "BERRY_TREE_05",
+ "BERRY_TREE_06",
+ "BERRY_TREE_07",
+ "TRAINER_MARCOS_REWARD",
+ "TRAINER_RHETT_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE103/EAST"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE104/SOUTH": {
+ "parent_map": "MAP_ROUTE104",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "HIDDEN_ITEM_ROUTE_104_POTION",
+ "HIDDEN_ITEM_ROUTE_104_HEART_SCALE",
+ "HIDDEN_ITEM_ROUTE_104_ANTIDOTE",
+ "BERRY_TREE_11",
+ "BERRY_TREE_12",
+ "BERRY_TREE_13",
+ "TRAINER_BILLY_REWARD",
+ "TRAINER_DARIAN_REWARD",
+ "TRAINER_CINDY_1_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_PETALBURG_CITY/MAIN",
+ "REGION_ROUTE104/SOUTH_WATER"
+ ],
+ "warps": [
+ "MAP_ROUTE104:0/MAP_ROUTE104_MR_BRINEYS_HOUSE:0",
+ "MAP_ROUTE104:4,5/MAP_PETALBURG_WOODS:2,3"
+ ]
+ },
+ "REGION_ROUTE104/SOUTH_WATER": {
+ "parent_map": "MAP_ROUTE104",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE104/SOUTH",
+ "REGION_ROUTE105/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE104/SOUTH_LEDGE": {
+ "parent_map": "MAP_ROUTE104",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ROUTE_104_POKE_BALL"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE104/SOUTH"
+ ],
+ "warps": [
+ "MAP_ROUTE104:6,7/MAP_PETALBURG_WOODS:4,5"
+ ]
+ },
+ "REGION_ROUTE104/NORTH": {
+ "parent_map": "MAP_ROUTE104",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_104_POTION",
+ "HIDDEN_ITEM_ROUTE_104_SUPER_POTION",
+ "HIDDEN_ITEM_ROUTE_104_POKE_BALL",
+ "NPC_GIFT_RECEIVED_TM_BULLET_SEED",
+ "NPC_GIFT_RECEIVED_WHITE_HERB",
+ "NPC_GIFT_RECEIVED_CHESTO_BERRY_ROUTE_104",
+ "BERRY_TREE_03",
+ "BERRY_TREE_04",
+ "BERRY_TREE_08",
+ "BERRY_TREE_09",
+ "BERRY_TREE_10",
+ "BERRY_TREE_75",
+ "BERRY_TREE_76",
+ "TRAINER_WINSTON_1_REWARD",
+ "TRAINER_HALEY_1_REWARD",
+ "TRAINER_IVAN_REWARD",
+ "TRAINER_GINA_AND_MIA_1_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_RUSTBORO_CITY/MAIN",
+ "REGION_ROUTE104/NORTH_POND",
+ "REGION_ROUTE104/TREE_ALCOVE_2"
+ ],
+ "warps": [
+ "MAP_ROUTE104:1/MAP_ROUTE104_PRETTY_PETAL_FLOWER_SHOP:0",
+ "MAP_ROUTE104:2,3/MAP_PETALBURG_WOODS:0,1"
+ ]
+ },
+ "REGION_ROUTE104/NORTH_POND": {
+ "parent_map": "MAP_ROUTE104",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE104/NORTH",
+ "REGION_ROUTE104/TREE_ALCOVE_1",
+ "REGION_ROUTE104/TREE_ALCOVE_2"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE104/TREE_ALCOVE_1": {
+ "parent_map": "MAP_ROUTE104",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_104_PP_UP"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE104/TREE_ALCOVE_2": {
+ "parent_map": "MAP_ROUTE104",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_104_X_ACCURACY"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN": {
+ "parent_map": "MAP_ROUTE104_MR_BRINEYS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_DEWFORD_TOWN/MAIN"
+ ],
+ "warps": [
+ "MAP_ROUTE104_MR_BRINEYS_HOUSE:0,1/MAP_ROUTE104:0"
+ ]
+ },
+ "REGION_ROUTE104_PRETTY_PETAL_FLOWER_SHOP/MAIN": {
+ "parent_map": "MAP_ROUTE104_PRETTY_PETAL_FLOWER_SHOP",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_WAILMER_PAIL",
+ "NPC_GIFT_FLOWER_SHOP_RECEIVED_BERRY"
+ ],
+ "events": [
+ "EVENT_MEET_FLOWER_SHOP_OWNER"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE104_PRETTY_PETAL_FLOWER_SHOP:0,1/MAP_ROUTE104:1"
+ ]
+ },
+ "REGION_ROUTE105/MAIN": {
+ "parent_map": "MAP_ROUTE105",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_105_IRON",
+ "HIDDEN_ITEM_ROUTE_105_HEART_SCALE",
+ "HIDDEN_ITEM_ROUTE_105_BIG_PEARL",
+ "TRAINER_IMANI_REWARD",
+ "TRAINER_DOMINIK_REWARD",
+ "TRAINER_LUIS_REWARD",
+ "TRAINER_FOSTER_REWARD",
+ "TRAINER_JOSUE_REWARD",
+ "TRAINER_ANDRES_1_REWARD",
+ "TRAINER_BEVERLY_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE104/SOUTH",
+ "REGION_ROUTE106/SEA",
+ "REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_1",
+ "REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_2"
+ ],
+ "warps": [
+ "MAP_ROUTE105:0/MAP_ISLAND_CAVE:0"
+ ]
+ },
+ "REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_1": {
+ "parent_map": "MAP_UNDERWATER_ROUTE105",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE105/MAIN",
+ "REGION_UNDERWATER_MARINE_CAVE/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_2": {
+ "parent_map": "MAP_UNDERWATER_ROUTE105",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE105/MAIN",
+ "REGION_UNDERWATER_MARINE_CAVE/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE106/WEST": {
+ "parent_map": "MAP_ROUTE106",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_106_PROTEIN"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE106/SEA"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE106/SEA": {
+ "parent_map": "MAP_ROUTE106",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "TRAINER_DOUGLAS_REWARD",
+ "TRAINER_KYLA_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE105/MAIN",
+ "REGION_ROUTE106/EAST",
+ "REGION_ROUTE106/WEST"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE106/EAST": {
+ "parent_map": "MAP_ROUTE106",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "HIDDEN_ITEM_ROUTE_106_POKE_BALL",
+ "HIDDEN_ITEM_ROUTE_106_STARDUST",
+ "HIDDEN_ITEM_ROUTE_106_HEART_SCALE",
+ "TRAINER_ELLIOT_1_REWARD",
+ "TRAINER_NED_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE106/SEA",
+ "REGION_DEWFORD_TOWN/MAIN"
+ ],
+ "warps": [
+ "MAP_ROUTE106:0/MAP_GRANITE_CAVE_1F:0"
+ ]
+ },
+ "REGION_ROUTE107/MAIN": {
+ "parent_map": "MAP_ROUTE107",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "TRAINER_DENISE_REWARD",
+ "TRAINER_TONY_1_REWARD",
+ "TRAINER_DARRIN_REWARD",
+ "TRAINER_CAMRON_REWARD",
+ "TRAINER_BETH_REWARD",
+ "TRAINER_LISA_AND_RAY_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_DEWFORD_TOWN/MAIN",
+ "REGION_ROUTE108/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE108/MAIN": {
+ "parent_map": "MAP_ROUTE108",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_108_STAR_PIECE",
+ "HIDDEN_ITEM_ROUTE_108_RARE_CANDY",
+ "TRAINER_MISSY_REWARD",
+ "TRAINER_MATTHEW_REWARD",
+ "TRAINER_TARA_REWARD",
+ "TRAINER_CAROLINA_REWARD",
+ "TRAINER_CORY_1_REWARD",
+ "TRAINER_JEROME_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE107/MAIN",
+ "REGION_ROUTE109/SEA"
+ ],
+ "warps": [
+ "MAP_ROUTE108:0/MAP_ABANDONED_SHIP_DECK:0"
+ ]
+ },
+ "REGION_ROUTE109/SEA": {
+ "parent_map": "MAP_ROUTE109",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_109_PP_UP",
+ "HIDDEN_ITEM_ROUTE_109_HEART_SCALE_3",
+ "TRAINER_AUSTINA_REWARD",
+ "TRAINER_GWEN_REWARD",
+ "TRAINER_ELIJAH_REWARD",
+ "TRAINER_CARTER_REWARD",
+ "TRAINER_ALICE_REWARD",
+ "TRAINER_DAVID_REWARD",
+ "TRAINER_MEL_AND_PAUL_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE109/BEACH",
+ "REGION_ROUTE108/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE109/BEACH": {
+ "parent_map": "MAP_ROUTE109",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_109_POTION",
+ "HIDDEN_ITEM_ROUTE_109_REVIVE",
+ "HIDDEN_ITEM_ROUTE_109_HEART_SCALE_1",
+ "HIDDEN_ITEM_ROUTE_109_GREAT_BALL",
+ "HIDDEN_ITEM_ROUTE_109_ETHER",
+ "HIDDEN_ITEM_ROUTE_109_HEART_SCALE_2",
+ "NPC_GIFT_RECEIVED_SOFT_SAND",
+ "TRAINER_HUEY_REWARD",
+ "TRAINER_HAILEY_REWARD",
+ "TRAINER_EDMOND_REWARD",
+ "TRAINER_RICKY_1_REWARD",
+ "TRAINER_LOLA_1_REWARD",
+ "TRAINER_CHANDLER_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE109/SEA",
+ "REGION_SLATEPORT_CITY/MAIN",
+ "REGION_DEWFORD_TOWN/MAIN"
+ ],
+ "warps": [
+ "MAP_ROUTE109:0/MAP_ROUTE109_SEASHORE_HOUSE:0"
+ ]
+ },
+ "REGION_ROUTE109_SEASHORE_HOUSE/MAIN": {
+ "parent_map": "MAP_ROUTE109_SEASHORE_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_6_SODA_POP",
+ "TRAINER_DWAYNE_REWARD",
+ "TRAINER_JOHANNA_REWARD",
+ "TRAINER_SIMON_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE109_SEASHORE_HOUSE:0,1/MAP_ROUTE109:0"
+ ]
+ },
+ "REGION_ROUTE110/MAIN": {
+ "parent_map": "MAP_ROUTE110",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_110_DIRE_HIT",
+ "ITEM_ROUTE_110_ELIXIR",
+ "HIDDEN_ITEM_ROUTE_110_REVIVE",
+ "HIDDEN_ITEM_ROUTE_110_GREAT_BALL",
+ "HIDDEN_ITEM_ROUTE_110_POKE_BALL",
+ "HIDDEN_ITEM_ROUTE_110_FULL_HEAL",
+ "NPC_GIFT_RECEIVED_ITEMFINDER",
+ "BERRY_TREE_16",
+ "BERRY_TREE_17",
+ "BERRY_TREE_18",
+ "TRAINER_KALEB_REWARD",
+ "TRAINER_ISABEL_1_REWARD",
+ "TRAINER_TIMMY_REWARD",
+ "TRAINER_JOSEPH_REWARD",
+ "TRAINER_EDWIN_1_REWARD",
+ "TRAINER_ALYSSA_REWARD",
+ "TRAINER_EDWARD_REWARD",
+ "TRAINER_DALE_REWARD",
+ "TRAINER_BRENDAN_ROUTE_110_MUDKIP_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110/SOUTH",
+ "REGION_ROUTE110/SOUTH_WATER",
+ "REGION_ROUTE110/NORTH_WATER",
+ "REGION_MAUVILLE_CITY/MAIN",
+ "REGION_ROUTE103/EAST"
+ ],
+ "warps": [
+ "MAP_ROUTE110:1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:0",
+ "MAP_ROUTE110:2/MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE:0"
+ ]
+ },
+ "REGION_ROUTE110/SOUTH": {
+ "parent_map": "MAP_ROUTE110",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110/MAIN",
+ "REGION_SLATEPORT_CITY/MAIN"
+ ],
+ "warps": [
+ "MAP_ROUTE110:4/MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE:0"
+ ]
+ },
+ "REGION_ROUTE110/CYCLING_ROAD": {
+ "parent_map": "MAP_ROUTE110",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_JACOB_REWARD",
+ "TRAINER_JASMINE_REWARD",
+ "TRAINER_BENJAMIN_1_REWARD",
+ "TRAINER_ANTHONY_REWARD",
+ "TRAINER_ABIGAIL_1_REWARD",
+ "TRAINER_JACLYN_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE110:3/MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE:2",
+ "MAP_ROUTE110:5/MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE:2"
+ ]
+ },
+ "REGION_ROUTE110/SOUTH_WATER": {
+ "parent_map": "MAP_ROUTE110",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_110_RARE_CANDY"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE110/NORTH_WATER": {
+ "parent_map": "MAP_ROUTE110",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110/MAIN"
+ ],
+ "warps": [
+ "MAP_ROUTE110:0/MAP_NEW_MAUVILLE_ENTRANCE:0"
+ ]
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_ENTRANCE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE2/ENTRANCE",
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/ENTRANCE",
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/ENTRANCE",
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE5/ENTRANCE",
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE6/ENTRANCE",
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE7/ENTRANCE",
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE8/ENTRANCE"
+ ],
+ "warps": [
+ "MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:0,1/MAP_ROUTE110:1",
+ "MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:0"
+ ]
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE1/ENTRANCE": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE1/REWARDS"
+ ],
+ "warps": [
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2"
+ ]
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE1/REWARDS": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE1",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_1",
+ "ITEM_TRICK_HOUSE_PUZZLE_1_ORANGE_MAIL"
+ ],
+ "events": [
+ "EVENT_COMPLETE_TRICK_HOUSE_1"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:2/MAP_ROUTE110_TRICK_HOUSE_END:0"
+ ]
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE2/ENTRANCE": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE2/REWARDS"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE2/REWARDS": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE2",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_2",
+ "ITEM_TRICK_HOUSE_PUZZLE_2_HARBOR_MAIL",
+ "ITEM_TRICK_HOUSE_PUZZLE_2_WAVE_MAIL"
+ ],
+ "events": [
+ "EVENT_COMPLETE_TRICK_HOUSE_2"
+ ],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/ENTRANCE": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE3",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/REWARDS"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/REWARDS": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE3",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_3",
+ "ITEM_TRICK_HOUSE_PUZZLE_3_SHADOW_MAIL",
+ "ITEM_TRICK_HOUSE_PUZZLE_3_WOOD_MAIL"
+ ],
+ "events": [
+ "EVENT_COMPLETE_TRICK_HOUSE_3"
+ ],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/ENTRANCE": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE4",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/REWARDS"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/REWARDS": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE4",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_4",
+ "ITEM_TRICK_HOUSE_PUZZLE_4_MECH_MAIL"
+ ],
+ "events": [
+ "EVENT_COMPLETE_TRICK_HOUSE_4"
+ ],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE5/ENTRANCE": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE5",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE5/REWARDS"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE5/REWARDS": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE5",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_5"
+ ],
+ "events": [
+ "EVENT_COMPLETE_TRICK_HOUSE_5"
+ ],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE6/ENTRANCE": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE6",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE6/REWARDS"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE6/REWARDS": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE6",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_6",
+ "ITEM_TRICK_HOUSE_PUZZLE_6_GLITTER_MAIL"
+ ],
+ "events": [
+ "EVENT_COMPLETE_TRICK_HOUSE_6"
+ ],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE7/ENTRANCE": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE7/REWARDS"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE7/REWARDS": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_7",
+ "ITEM_TRICK_HOUSE_PUZZLE_7_TROPIC_MAIL"
+ ],
+ "events": [
+ "EVENT_COMPLETE_TRICK_HOUSE_7"
+ ],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE8/ENTRANCE": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE8",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE8/REWARDS"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_PUZZLE8/REWARDS": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE8",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_TRICK_HOUSE_PUZZLE_8_BEAD_MAIL"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_END/MAIN": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_END",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE110_TRICK_HOUSE_END:1/MAP_ROUTE110_TRICK_HOUSE_CORRIDOR:0",
+ "MAP_ROUTE110_TRICK_HOUSE_END:0/MAP_ROUTE110_TRICK_HOUSE_PUZZLE1:2"
+ ]
+ },
+ "REGION_ROUTE110_TRICK_HOUSE_CORRIDOR/MAIN": {
+ "parent_map": "MAP_ROUTE110_TRICK_HOUSE_CORRIDOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE110_TRICK_HOUSE_CORRIDOR:2,3/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
+ "MAP_ROUTE110_TRICK_HOUSE_CORRIDOR:0,1/MAP_ROUTE110_TRICK_HOUSE_END:1"
+ ]
+ },
+ "REGION_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE/WEST": {
+ "parent_map": "MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE/EAST"
+ ],
+ "warps": [
+ "MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE:0,1/MAP_ROUTE110:2"
+ ]
+ },
+ "REGION_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE/EAST": {
+ "parent_map": "MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE/WEST"
+ ],
+ "warps": [
+ "MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE:2,3/MAP_ROUTE110:3"
+ ]
+ },
+ "REGION_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE/WEST": {
+ "parent_map": "MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE/EAST"
+ ],
+ "warps": [
+ "MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE:0,1/MAP_ROUTE110:4"
+ ]
+ },
+ "REGION_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE/EAST": {
+ "parent_map": "MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE/WEST"
+ ],
+ "warps": [
+ "MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE:2,3/MAP_ROUTE110:5"
+ ]
+ },
+ "REGION_ROUTE111/MIDDLE": {
+ "parent_map": "MAP_ROUTE111",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_IRENE_REWARD",
+ "TRAINER_TRAVIS_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE111/DESERT",
+ "REGION_ROUTE111/SOUTH",
+ "REGION_ROUTE112/SOUTH_EAST"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE111/SOUTH": {
+ "parent_map": "MAP_ROUTE111",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_111_ELIXIR",
+ "TRAINER_CELINA_REWARD",
+ "TRAINER_TYRON_REWARD",
+ "TRAINER_BIANCA_REWARD",
+ "TRAINER_HAYDEN_REWARD",
+ "TRAINER_VICTOR_REWARD",
+ "TRAINER_VICKY_REWARD",
+ "TRAINER_VICTORIA_REWARD",
+ "TRAINER_VIVI_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE111/SOUTH_POND",
+ "REGION_ROUTE111/MIDDLE",
+ "REGION_MAUVILLE_CITY/MAIN"
+ ],
+ "warps": [
+ "MAP_ROUTE111:0/MAP_ROUTE111_WINSTRATE_FAMILYS_HOUSE:0",
+ "MAP_ROUTE111:4/MAP_TRAINER_HILL_ENTRANCE:0"
+ ]
+ },
+ "REGION_ROUTE111/SOUTH_POND": {
+ "parent_map": "MAP_ROUTE111",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_111_HP_UP"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE111/SOUTH"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE111/DESERT": {
+ "parent_map": "MAP_ROUTE111",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ROUTE_111_TM_SANDSTORM",
+ "ITEM_ROUTE_111_STARDUST",
+ "HIDDEN_ITEM_ROUTE_111_STARDUST",
+ "HIDDEN_ITEM_ROUTE_111_PROTEIN",
+ "HIDDEN_ITEM_ROUTE_111_RARE_CANDY",
+ "TRAINER_CELIA_REWARD",
+ "TRAINER_BRYAN_REWARD",
+ "TRAINER_BRANDEN_REWARD",
+ "TRAINER_DUSTY_1_REWARD",
+ "TRAINER_BECKY_REWARD",
+ "TRAINER_HEIDI_REWARD",
+ "TRAINER_BEAU_REWARD",
+ "TRAINER_DREW_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE111/NORTH",
+ "REGION_ROUTE111/MIDDLE"
+ ],
+ "warps": [
+ "MAP_ROUTE111:1/MAP_DESERT_RUINS:0",
+ "MAP_ROUTE111:3/MAP_MIRAGE_TOWER_1F:0"
+ ]
+ },
+ "REGION_ROUTE111/NORTH": {
+ "parent_map": "MAP_ROUTE111",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_SECRET_POWER",
+ "NPC_GIFT_ROUTE_111_RECEIVED_BERRY",
+ "BERRY_TREE_19",
+ "BERRY_TREE_20",
+ "BERRY_TREE_80",
+ "BERRY_TREE_81",
+ "TRAINER_WILTON_1_REWARD",
+ "TRAINER_BROOKE_1_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE113/MAIN",
+ "REGION_ROUTE112/NORTH",
+ "REGION_ROUTE111/ABOVE_SLOPE",
+ "REGION_ROUTE111/DESERT"
+ ],
+ "warps": [
+ "MAP_ROUTE111:2/MAP_ROUTE111_OLD_LADYS_REST_STOP:0"
+ ]
+ },
+ "REGION_ROUTE111/ABOVE_SLOPE": {
+ "parent_map": "MAP_ROUTE111",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_DAISUKE_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE111/NORTH"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE111_OLD_LADYS_REST_STOP/MAIN": {
+ "parent_map": "MAP_ROUTE111_OLD_LADYS_REST_STOP",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE111_OLD_LADYS_REST_STOP:0,1/MAP_ROUTE111:2"
+ ]
+ },
+ "REGION_ROUTE111_WINSTRATE_FAMILYS_HOUSE/MAIN": {
+ "parent_map": "MAP_ROUTE111_WINSTRATE_FAMILYS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_MACHO_BRACE"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE111_WINSTRATE_FAMILYS_HOUSE:0,1/MAP_ROUTE111:0"
+ ]
+ },
+ "REGION_ROUTE112/SOUTH_EAST": {
+ "parent_map": "MAP_ROUTE112",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_LARRY_REWARD",
+ "TRAINER_CAROL_REWARD",
+ "TRAINER_TRENT_1_REWARD",
+ "TRAINER_BRICE_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE112/CABLE_CAR_STATION_ENTRANCE",
+ "REGION_ROUTE111/MIDDLE"
+ ],
+ "warps": [
+ "MAP_ROUTE112:4/MAP_FIERY_PATH:0"
+ ]
+ },
+ "REGION_ROUTE112/CABLE_CAR_STATION_ENTRANCE": {
+ "parent_map": "MAP_ROUTE112",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE112/SOUTH_EAST"
+ ],
+ "warps": [
+ "MAP_ROUTE112:0,1/MAP_ROUTE112_CABLE_CAR_STATION:0,1"
+ ]
+ },
+ "REGION_ROUTE112/SOUTH_WEST": {
+ "parent_map": "MAP_ROUTE112",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ROUTE_112_NUGGET"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE112/SOUTH_EAST",
+ "REGION_LAVARIDGE_TOWN/MAIN"
+ ],
+ "warps": [
+ "MAP_ROUTE112:2,3/MAP_JAGGED_PASS:0,1"
+ ]
+ },
+ "REGION_ROUTE112/NORTH": {
+ "parent_map": "MAP_ROUTE112",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "BERRY_TREE_21",
+ "BERRY_TREE_22",
+ "BERRY_TREE_23",
+ "BERRY_TREE_24",
+ "TRAINER_SHAYLA_REWARD",
+ "TRAINER_BRYANT_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE111/NORTH"
+ ],
+ "warps": [
+ "MAP_ROUTE112:5/MAP_FIERY_PATH:1"
+ ]
+ },
+ "REGION_ROUTE112_CABLE_CAR_STATION/MAIN": {
+ "parent_map": "MAP_ROUTE112_CABLE_CAR_STATION",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_MT_CHIMNEY_CABLE_CAR_STATION/MAIN"
+ ],
+ "warps": [
+ "MAP_ROUTE112_CABLE_CAR_STATION:0,1/MAP_ROUTE112:0,1"
+ ]
+ },
+ "REGION_MT_CHIMNEY_CABLE_CAR_STATION/MAIN": {
+ "parent_map": "MAP_MT_CHIMNEY_CABLE_CAR_STATION",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE112_CABLE_CAR_STATION/MAIN"
+ ],
+ "warps": [
+ "MAP_MT_CHIMNEY_CABLE_CAR_STATION:0,1/MAP_MT_CHIMNEY:0,1"
+ ]
+ },
+ "REGION_MT_CHIMNEY/MAIN": {
+ "parent_map": "MAP_MT_CHIMNEY",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_METEORITE",
+ "TRAINER_SHIRLEY_REWARD",
+ "TRAINER_SHEILA_REWARD",
+ "TRAINER_SHELBY_1_REWARD",
+ "TRAINER_SAWYER_1_REWARD",
+ "TRAINER_MELISSA_REWARD",
+ "TRAINER_GRUNT_MT_CHIMNEY_1_REWARD",
+ "TRAINER_GRUNT_MT_CHIMNEY_2_REWARD",
+ "TRAINER_TABITHA_MT_CHIMNEY_REWARD",
+ "TRAINER_MAXIE_MT_CHIMNEY_REWARD"
+ ],
+ "events": [
+ "EVENT_RECOVER_METEORITE"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_MT_CHIMNEY:0,1/MAP_MT_CHIMNEY_CABLE_CAR_STATION:0,1",
+ "MAP_MT_CHIMNEY:2,3/MAP_JAGGED_PASS:2,3"
+ ]
+ },
+ "REGION_JAGGED_PASS/TOP": {
+ "parent_map": "MAP_JAGGED_PASS",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_JAGGED_PASS_FULL_HEAL",
+ "TRAINER_ERIC_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_JAGGED_PASS/MIDDLE"
+ ],
+ "warps": [
+ "MAP_JAGGED_PASS:2,3/MAP_MT_CHIMNEY:2,3"
+ ]
+ },
+ "REGION_JAGGED_PASS/MIDDLE": {
+ "parent_map": "MAP_JAGGED_PASS",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_JAGGED_PASS_BURN_HEAL",
+ "HIDDEN_ITEM_JAGGED_PASS_GREAT_BALL",
+ "TRAINER_DIANA_1_REWARD",
+ "TRAINER_AUTUMN_REWARD",
+ "TRAINER_JULIO_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_JAGGED_PASS/TOP",
+ "REGION_JAGGED_PASS/BOTTOM"
+ ],
+ "warps": [
+ "MAP_JAGGED_PASS:4/MAP_MAGMA_HIDEOUT_1F:0"
+ ]
+ },
+ "REGION_JAGGED_PASS/BOTTOM": {
+ "parent_map": "MAP_JAGGED_PASS",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_ETHAN_1_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_JAGGED_PASS/MIDDLE"
+ ],
+ "warps": [
+ "MAP_JAGGED_PASS:0,1/MAP_ROUTE112:2,3"
+ ]
+ },
+ "REGION_ROUTE113/MAIN": {
+ "parent_map": "MAP_ROUTE113",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ROUTE_113_MAX_ETHER",
+ "ITEM_ROUTE_113_SUPER_REPEL",
+ "ITEM_ROUTE_113_HYPER_POTION",
+ "HIDDEN_ITEM_ROUTE_113_ETHER",
+ "HIDDEN_ITEM_ROUTE_113_TM_DOUBLE_TEAM",
+ "HIDDEN_ITEM_ROUTE_113_NUGGET",
+ "TRAINER_WYATT_REWARD",
+ "TRAINER_LAWRENCE_REWARD",
+ "TRAINER_LUNG_REWARD",
+ "TRAINER_JAYLEN_REWARD",
+ "TRAINER_MADELINE_1_REWARD",
+ "TRAINER_LAO_1_REWARD",
+ "TRAINER_TORI_AND_TIA_REWARD",
+ "TRAINER_DILLON_REWARD",
+ "TRAINER_COBY_REWARD",
+ "TRAINER_SOPHIE_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_FALLARBOR_TOWN/MAIN",
+ "REGION_ROUTE111/NORTH"
+ ],
+ "warps": [
+ "MAP_ROUTE113:0/MAP_ROUTE113_GLASS_WORKSHOP:0"
+ ]
+ },
+ "REGION_ROUTE113_GLASS_WORKSHOP/MAIN": {
+ "parent_map": "MAP_ROUTE113_GLASS_WORKSHOP",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_SOOT_SACK"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE113_GLASS_WORKSHOP:0,1/MAP_ROUTE113:0"
+ ]
+ },
+ "REGION_ROUTE114/MAIN": {
+ "parent_map": "MAP_ROUTE114",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_114_PROTEIN",
+ "ITEM_ROUTE_114_ENERGY_POWDER",
+ "HIDDEN_ITEM_ROUTE_114_REVIVE",
+ "HIDDEN_ITEM_ROUTE_114_CARBOS",
+ "NPC_GIFT_RECEIVED_TM_ROAR",
+ "NPC_GIFT_ROUTE_114_RECEIVED_BERRY",
+ "BERRY_TREE_68",
+ "BERRY_TREE_77",
+ "BERRY_TREE_78",
+ "TRAINER_NOLAN_REWARD",
+ "TRAINER_KAI_REWARD",
+ "TRAINER_CHARLOTTE_REWARD",
+ "TRAINER_CLAUDE_REWARD",
+ "TRAINER_NANCY_REWARD",
+ "TRAINER_SHANE_REWARD",
+ "TRAINER_STEVE_1_REWARD",
+ "TRAINER_BERNIE_1_REWARD",
+ "TRAINER_LUCAS_1_REWARD",
+ "TRAINER_ANGELINA_REWARD",
+ "TRAINER_LENNY_REWARD",
+ "TRAINER_TYRA_AND_IVY_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE114/WATER",
+ "REGION_FALLARBOR_TOWN/MAIN",
+ "REGION_TERRA_CAVE_ENTRANCE/MAIN"
+ ],
+ "warps": [
+ "MAP_ROUTE114:0/MAP_METEOR_FALLS_1F_1R:0",
+ "MAP_ROUTE114:1/MAP_ROUTE114_FOSSIL_MANIACS_HOUSE:0",
+ "MAP_ROUTE114:2/MAP_ROUTE114_LANETTES_HOUSE:0"
+ ]
+ },
+ "REGION_ROUTE114/WATER": {
+ "parent_map": "MAP_ROUTE114",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE114/MAIN",
+ "REGION_ROUTE114/ABOVE_WATERFALL"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE114/ABOVE_WATERFALL": {
+ "parent_map": "MAP_ROUTE114",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_114_RARE_CANDY"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE114/WATER",
+ "REGION_TERRA_CAVE_ENTRANCE/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE114_FOSSIL_MANIACS_HOUSE/MAIN": {
+ "parent_map": "MAP_ROUTE114_FOSSIL_MANIACS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_RECEIVED_TM_DIG"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE114_FOSSIL_MANIACS_HOUSE:0,1/MAP_ROUTE114:1",
+ "MAP_ROUTE114_FOSSIL_MANIACS_HOUSE:2/MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:0"
+ ]
+ },
+ "REGION_ROUTE114_FOSSIL_MANIACS_TUNNEL/MAIN": {
+ "parent_map": "MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:0,1/MAP_ROUTE114_FOSSIL_MANIACS_HOUSE:2",
+ "MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:2/MAP_DESERT_UNDERPASS:0"
+ ]
+ },
+ "REGION_DESERT_UNDERPASS/MAIN": {
+ "parent_map": "MAP_DESERT_UNDERPASS",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_DESERT_UNDERPASS:0/MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:2"
+ ]
+ },
+ "REGION_ROUTE114_LANETTES_HOUSE/MAIN": {
+ "parent_map": "MAP_ROUTE114_LANETTES_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE114_LANETTES_HOUSE:0,1/MAP_ROUTE114:2"
+ ]
+ },
+ "REGION_ROUTE115/SOUTH_BELOW_LEDGE": {
+ "parent_map": "MAP_ROUTE115",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_115_SUPER_POTION"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE115/SEA",
+ "REGION_ROUTE115/SOUTH_ABOVE_LEDGE",
+ "REGION_RUSTBORO_CITY/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE": {
+ "parent_map": "MAP_ROUTE115",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "HIDDEN_ITEM_ROUTE_115_HEART_SCALE",
+ "TRAINER_CYNDY_1_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE115/SOUTH_ABOVE_LEDGE",
+ "REGION_ROUTE115/SEA"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE115/SOUTH_ABOVE_LEDGE": {
+ "parent_map": "MAP_ROUTE115",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ROUTE_115_PP_UP",
+ "TRAINER_NOB_1_REWARD",
+ "TRAINER_MARLENE_REWARD",
+ "TRAINER_HECTOR_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE",
+ "REGION_ROUTE115/SOUTH_BELOW_LEDGE",
+ "REGION_ROUTE115/SOUTH_BEHIND_ROCK"
+ ],
+ "warps": [
+ "MAP_ROUTE115:0/MAP_METEOR_FALLS_1F_1R:1"
+ ]
+ },
+ "REGION_ROUTE115/SOUTH_BEHIND_ROCK": {
+ "parent_map": "MAP_ROUTE115",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ROUTE_115_GREAT_BALL",
+ "BERRY_TREE_55",
+ "BERRY_TREE_56"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE115/SOUTH_ABOVE_LEDGE"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE115/NORTH_BELOW_SLOPE": {
+ "parent_map": "MAP_ROUTE115",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_115_HEAL_POWDER",
+ "ITEM_ROUTE_115_TM_FOCUS_PUNCH",
+ "BERRY_TREE_69",
+ "BERRY_TREE_70",
+ "BERRY_TREE_71",
+ "TRAINER_TIMOTHY_1_REWARD",
+ "TRAINER_KYRA_REWARD",
+ "TRAINER_KOICHI_REWARD",
+ "TRAINER_JAIDEN_REWARD",
+ "TRAINER_ALIX_REWARD",
+ "TRAINER_HELENE_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE115/NORTH_ABOVE_SLOPE",
+ "REGION_ROUTE115/SEA",
+ "REGION_TERRA_CAVE_ENTRANCE/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE115/NORTH_ABOVE_SLOPE": {
+ "parent_map": "MAP_ROUTE115",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ROUTE_115_IRON"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE115/NORTH_BELOW_SLOPE",
+ "REGION_TERRA_CAVE_ENTRANCE/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE115/SEA": {
+ "parent_map": "MAP_ROUTE115",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE115/SOUTH_BELOW_LEDGE",
+ "REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE",
+ "REGION_ROUTE115/NORTH_BELOW_SLOPE"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE116/WEST": {
+ "parent_map": "MAP_ROUTE116",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ROUTE_116_REPEL",
+ "ITEM_ROUTE_116_X_SPECIAL",
+ "NPC_GIFT_RECEIVED_REPEAT_BALL",
+ "TRAINER_JOSE_REWARD",
+ "TRAINER_JOEY_REWARD",
+ "TRAINER_KAREN_1_REWARD",
+ "TRAINER_CLARK_REWARD",
+ "TRAINER_JOHNSON_REWARD",
+ "TRAINER_DEVAN_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE116/WEST_ABOVE_LEDGE",
+ "REGION_RUSTBORO_CITY/MAIN",
+ "REGION_TERRA_CAVE_ENTRANCE/MAIN"
+ ],
+ "warps": [
+ "MAP_ROUTE116:0/MAP_RUSTURF_TUNNEL:0",
+ "MAP_ROUTE116:1/MAP_ROUTE116_TUNNELERS_REST_HOUSE:0"
+ ]
+ },
+ "REGION_ROUTE116/WEST_ABOVE_LEDGE": {
+ "parent_map": "MAP_ROUTE116",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ROUTE_116_ETHER",
+ "ITEM_ROUTE_116_POTION",
+ "HIDDEN_ITEM_ROUTE_116_SUPER_POTION",
+ "BERRY_TREE_25",
+ "BERRY_TREE_26",
+ "BERRY_TREE_66",
+ "BERRY_TREE_67",
+ "TRAINER_JANICE_REWARD",
+ "TRAINER_JERRY_1_REWARD",
+ "TRAINER_SARAH_REWARD",
+ "TRAINER_DAWSON_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE116/WEST"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE116/EAST": {
+ "parent_map": "MAP_ROUTE116",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ROUTE_116_HP_UP",
+ "HIDDEN_ITEM_ROUTE_116_BLACK_GLASSES"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_TERRA_CAVE_ENTRANCE/MAIN"
+ ],
+ "warps": [
+ "MAP_ROUTE116:2/MAP_RUSTURF_TUNNEL:2"
+ ]
+ },
+ "REGION_ROUTE116_TUNNELERS_REST_HOUSE/MAIN": {
+ "parent_map": "MAP_ROUTE116_TUNNELERS_REST_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE116_TUNNELERS_REST_HOUSE:0,1/MAP_ROUTE116:1"
+ ]
+ },
+ "REGION_ROUTE117/MAIN": {
+ "parent_map": "MAP_ROUTE117",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_117_GREAT_BALL",
+ "ITEM_ROUTE_117_REVIVE",
+ "HIDDEN_ITEM_ROUTE_117_REPEL",
+ "BERRY_TREE_27",
+ "BERRY_TREE_28",
+ "BERRY_TREE_29",
+ "TRAINER_LYDIA_1_REWARD",
+ "TRAINER_DEREK_REWARD",
+ "TRAINER_BRANDI_REWARD",
+ "TRAINER_MELINA_REWARD",
+ "TRAINER_AISHA_REWARD",
+ "TRAINER_MARIA_1_REWARD",
+ "TRAINER_ISAAC_1_REWARD",
+ "TRAINER_DYLAN_1_REWARD",
+ "TRAINER_ANNA_AND_MEG_1_REWARD"
+ ],
+ "events": [
+ "EVENT_ENCOUNTER_LATIOS"
+ ],
+ "exits": [
+ "REGION_VERDANTURF_TOWN/MAIN",
+ "REGION_MAUVILLE_CITY/MAIN",
+ "REGION_ROUTE117/PONDS"
+ ],
+ "warps": [
+ "MAP_ROUTE117:0/MAP_ROUTE117_POKEMON_DAY_CARE:0"
+ ]
+ },
+ "REGION_ROUTE117/PONDS": {
+ "parent_map": "MAP_ROUTE117",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE117_POKEMON_DAY_CARE/MAIN": {
+ "parent_map": "MAP_ROUTE117_POKEMON_DAY_CARE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE117_POKEMON_DAY_CARE:0,1/MAP_ROUTE117:0"
+ ]
+ },
+ "REGION_ROUTE118/WEST": {
+ "parent_map": "MAP_ROUTE118",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "HIDDEN_ITEM_ROUTE_118_HEART_SCALE",
+ "TRAINER_DEANDRE_REWARD",
+ "TRAINER_ROSE_1_REWARD",
+ "TRAINER_WADE_REWARD",
+ "TRAINER_DALTON_1_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_MAUVILLE_CITY/MAIN",
+ "REGION_ROUTE118/WEST_WATER",
+ "REGION_ROUTE118/EAST",
+ "REGION_TERRA_CAVE_ENTRANCE/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE118/WEST_WATER": {
+ "parent_map": "MAP_ROUTE118",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE118/WEST",
+ "REGION_ROUTE118/EAST_WATER"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE118/EAST_WATER": {
+ "parent_map": "MAP_ROUTE118",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE118/WEST_WATER",
+ "REGION_ROUTE118/EAST"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE118/EAST": {
+ "parent_map": "MAP_ROUTE118",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_118_HYPER_POTION",
+ "HIDDEN_ITEM_ROUTE_118_IRON",
+ "NPC_GIFT_RECEIVED_GOOD_ROD",
+ "BERRY_TREE_31",
+ "BERRY_TREE_32",
+ "BERRY_TREE_33",
+ "TRAINER_BARNY_REWARD",
+ "TRAINER_CHESTER_REWARD",
+ "TRAINER_PERRY_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE118/EAST_WATER",
+ "REGION_ROUTE118/WEST",
+ "REGION_ROUTE119/LOWER",
+ "REGION_ROUTE123/WEST",
+ "REGION_TERRA_CAVE_ENTRANCE/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE119/LOWER": {
+ "parent_map": "MAP_ROUTE119",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_119_SUPER_REPEL",
+ "ITEM_ROUTE_119_HYPER_POTION_1",
+ "HIDDEN_ITEM_ROUTE_119_FULL_HEAL",
+ "BERRY_TREE_85",
+ "BERRY_TREE_86",
+ "TRAINER_DONALD_REWARD",
+ "TRAINER_KENT_REWARD",
+ "TRAINER_TAYLOR_REWARD",
+ "TRAINER_GREG_REWARD",
+ "TRAINER_DOUG_REWARD",
+ "TRAINER_BRENT_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE119/MIDDLE",
+ "REGION_ROUTE119/LOWER_WATER",
+ "REGION_ROUTE119/LOWER_ACROSS_RAILS",
+ "REGION_ROUTE118/EAST"
+ ],
+ "warps": [
+ "MAP_ROUTE119:1/MAP_ROUTE119_HOUSE:0"
+ ]
+ },
+ "REGION_ROUTE119/LOWER_WATER": {
+ "parent_map": "MAP_ROUTE119",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE119/LOWER",
+ "REGION_ROUTE119/LOWER_ACROSS_WATER"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE119/LOWER_ACROSS_WATER": {
+ "parent_map": "MAP_ROUTE119",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_119_ZINC",
+ "TRAINER_CHRIS_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE119/LOWER_ACROSS_RAILS": {
+ "parent_map": "MAP_ROUTE119",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_ROUTE_119_CALCIUM"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE119/LOWER"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE119/MIDDLE": {
+ "parent_map": "MAP_ROUTE119",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ROUTE_119_ELIXIR_1",
+ "ITEM_ROUTE_119_HYPER_POTION_2",
+ "TRAINER_CATHERINE_1_REWARD",
+ "TRAINER_JACKSON_1_REWARD",
+ "TRAINER_RACHEL_REWARD",
+ "TRAINER_PHIL_REWARD",
+ "TRAINER_HUGH_REWARD",
+ "TRAINER_DAYTON_REWARD",
+ "TRAINER_TAKASHI_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE119/LOWER",
+ "REGION_ROUTE119/UPPER"
+ ],
+ "warps": [
+ "MAP_ROUTE119:0/MAP_ROUTE119_WEATHER_INSTITUTE_1F:0"
+ ]
+ },
+ "REGION_ROUTE119/UPPER": {
+ "parent_map": "MAP_ROUTE119",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_119_ELIXIR_2",
+ "NPC_GIFT_RECEIVED_HM_FLY",
+ "BERRY_TREE_34",
+ "BERRY_TREE_35",
+ "BERRY_TREE_36",
+ "TRAINER_FABIAN_REWARD",
+ "TRAINER_YASU_REWARD",
+ "TRAINER_HIDEO_REWARD",
+ "TRAINER_BRENDAN_ROUTE_119_MUDKIP_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE119/MIDDLE",
+ "REGION_ROUTE119/MIDDLE_RIVER",
+ "REGION_FORTREE_CITY/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE119/MIDDLE_RIVER": {
+ "parent_map": "MAP_ROUTE119",
+ "has_grass": true,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_119_LEAF_STONE",
+ "HIDDEN_ITEM_ROUTE_119_ULTRA_BALL",
+ "HIDDEN_ITEM_ROUTE_119_MAX_ETHER"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE119/UPPER",
+ "REGION_ROUTE119/ABOVE_WATERFALL"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE119/ABOVE_WATERFALL": {
+ "parent_map": "MAP_ROUTE119",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "BERRY_TREE_83",
+ "BERRY_TREE_84"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE119/MIDDLE_RIVER",
+ "REGION_ROUTE119/ABOVE_WATERFALL_ACROSS_RAILS"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE119/ABOVE_WATERFALL_ACROSS_RAILS": {
+ "parent_map": "MAP_ROUTE119",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ROUTE_119_RARE_CANDY",
+ "ITEM_ROUTE_119_NUGGET"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE119/ABOVE_WATERFALL"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE119_WEATHER_INSTITUTE_1F/MAIN": {
+ "parent_map": "MAP_ROUTE119_WEATHER_INSTITUTE_1F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GRUNT_WEATHER_INST_1_REWARD",
+ "TRAINER_GRUNT_WEATHER_INST_4_REWARD"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE119_WEATHER_INSTITUTE_1F:0,1/MAP_ROUTE119:0",
+ "MAP_ROUTE119_WEATHER_INSTITUTE_1F:2/MAP_ROUTE119_WEATHER_INSTITUTE_2F:0"
+ ]
+ },
+ "REGION_ROUTE119_WEATHER_INSTITUTE_2F/MAIN": {
+ "parent_map": "MAP_ROUTE119_WEATHER_INSTITUTE_2F",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "TRAINER_GRUNT_WEATHER_INST_2_REWARD",
+ "TRAINER_GRUNT_WEATHER_INST_3_REWARD",
+ "TRAINER_GRUNT_WEATHER_INST_5_REWARD",
+ "TRAINER_SHELLY_WEATHER_INSTITUTE_REWARD"
+ ],
+ "events": [
+ "EVENT_DEFEAT_SHELLY"
+ ],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE119_WEATHER_INSTITUTE_2F:0/MAP_ROUTE119_WEATHER_INSTITUTE_1F:2"
+ ]
+ },
+ "REGION_ROUTE119_HOUSE/MAIN": {
+ "parent_map": "MAP_ROUTE119_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE119_HOUSE:0,1/MAP_ROUTE119:1"
+ ]
+ },
+ "REGION_ROUTE120/NORTH": {
+ "parent_map": "MAP_ROUTE120",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_ROUTE_120_RARE_CANDY_1",
+ "HIDDEN_ITEM_ROUTE_120_REVIVE",
+ "NPC_GIFT_RECEIVED_DEVON_SCOPE",
+ "TRAINER_CLARISSA_REWARD",
+ "TRAINER_ROBERT_1_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_FORTREE_CITY/MAIN",
+ "REGION_ROUTE120/NORTH_POND_SHORE",
+ "REGION_ROUTE120/SOUTH"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE120/NORTH_POND_SHORE": {
+ "parent_map": "MAP_ROUTE120",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_120_NEST_BALL"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE120/NORTH",
+ "REGION_ROUTE120/NORTH_POND"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE120/NORTH_POND": {
+ "parent_map": "MAP_ROUTE120",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE120/NORTH_POND_SHORE"
+ ],
+ "warps": [
+ "MAP_ROUTE120:1/MAP_SCORCHED_SLAB:0"
+ ]
+ },
+ "REGION_ROUTE120/SOUTH": {
+ "parent_map": "MAP_ROUTE120",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_120_NUGGET",
+ "ITEM_ROUTE_120_REVIVE",
+ "ITEM_ROUTE_120_HYPER_POTION",
+ "HIDDEN_ITEM_ROUTE_120_ZINC",
+ "NPC_GIFT_ROUTE_120_RECEIVED_BERRY",
+ "BERRY_TREE_40",
+ "BERRY_TREE_41",
+ "BERRY_TREE_42",
+ "BERRY_TREE_43",
+ "BERRY_TREE_44",
+ "BERRY_TREE_45",
+ "BERRY_TREE_46",
+ "TRAINER_COLIN_REWARD",
+ "TRAINER_LEONEL_REWARD",
+ "TRAINER_ANGELICA_REWARD",
+ "TRAINER_CALLIE_REWARD",
+ "TRAINER_RILEY_REWARD",
+ "TRAINER_JENNIFER_REWARD",
+ "TRAINER_JENNA_REWARD",
+ "TRAINER_LORENZO_REWARD",
+ "TRAINER_JEFFREY_1_REWARD",
+ "TRAINER_KEIGO_REWARD",
+ "TRAINER_CHIP_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE120/NORTH",
+ "REGION_ROUTE120/SOUTH_ALCOVE",
+ "REGION_ROUTE120/SOUTH_PONDS",
+ "REGION_ROUTE121/WEST"
+ ],
+ "warps": [
+ "MAP_ROUTE120:0/MAP_ANCIENT_TOMB:0"
+ ]
+ },
+ "REGION_ROUTE120/SOUTH_ALCOVE": {
+ "parent_map": "MAP_ROUTE120",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "BERRY_TREE_37",
+ "BERRY_TREE_38",
+ "BERRY_TREE_39"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE120/SOUTH"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE120/SOUTH_PONDS": {
+ "parent_map": "MAP_ROUTE120",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2",
+ "ITEM_ROUTE_120_FULL_HEAL"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE121/WEST": {
+ "parent_map": "MAP_ROUTE121",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_ROUTE_121_HP_UP",
+ "BERRY_TREE_47",
+ "BERRY_TREE_48",
+ "BERRY_TREE_49",
+ "BERRY_TREE_50",
+ "TRAINER_CALE_REWARD",
+ "TRAINER_TAMMY_REWARD",
+ "TRAINER_JESSICA_1_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE121/EAST",
+ "REGION_ROUTE120/SOUTH"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE121/EAST": {
+ "parent_map": "MAP_ROUTE121",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_121_CARBOS",
+ "ITEM_ROUTE_121_REVIVE",
+ "ITEM_ROUTE_121_ZINC",
+ "HIDDEN_ITEM_ROUTE_121_NUGGET",
+ "HIDDEN_ITEM_ROUTE_121_FULL_HEAL",
+ "HIDDEN_ITEM_ROUTE_121_MAX_REVIVE",
+ "BERRY_TREE_51",
+ "BERRY_TREE_52",
+ "BERRY_TREE_53",
+ "BERRY_TREE_54",
+ "TRAINER_KATE_AND_JOY_REWARD",
+ "TRAINER_WALTER_1_REWARD",
+ "TRAINER_PAT_REWARD",
+ "TRAINER_MYLES_REWARD",
+ "TRAINER_VANESSA_REWARD",
+ "TRAINER_MARCEL_REWARD",
+ "TRAINER_CRISTIN_1_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE121/WEST",
+ "REGION_ROUTE121/WATER",
+ "REGION_LILYCOVE_CITY/MAIN"
+ ],
+ "warps": [
+ "MAP_ROUTE121:0/MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:2"
+ ]
+ },
+ "REGION_ROUTE121/WATER": {
+ "parent_map": "MAP_ROUTE121",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE121/EAST",
+ "REGION_ROUTE122/SEA"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE121_SAFARI_ZONE_ENTRANCE/MAIN": {
+ "parent_map": "MAP_ROUTE121_SAFARI_ZONE_ENTRANCE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:0,1/MAP_SAFARI_ZONE_SOUTH:0",
+ "MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:2,3/MAP_ROUTE121:0"
+ ]
+ },
+ "REGION_SAFARI_ZONE_NORTH/MAIN": {
+ "parent_map": "MAP_SAFARI_ZONE_NORTH",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_SAFARI_ZONE_NORTH_CALCIUM"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_SAFARI_ZONE_SOUTH/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_SAFARI_ZONE_NORTHWEST/MAIN": {
+ "parent_map": "MAP_SAFARI_ZONE_NORTHWEST",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SAFARI_ZONE_NORTHWEST/POND",
+ "REGION_SAFARI_ZONE_SOUTHWEST/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_SAFARI_ZONE_NORTHWEST/POND": {
+ "parent_map": "MAP_SAFARI_ZONE_NORTHWEST",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_SAFARI_ZONE_NORTH_WEST_TM_SOLAR_BEAM"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_SAFARI_ZONE_NORTHEAST/MAIN": {
+ "parent_map": "MAP_SAFARI_ZONE_NORTHEAST",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_SAFARI_ZONE_NORTH_EAST_NUGGET",
+ "HIDDEN_ITEM_SAFARI_ZONE_NORTH_EAST_RARE_CANDY",
+ "HIDDEN_ITEM_SAFARI_ZONE_NORTH_EAST_ZINC"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_SAFARI_ZONE_SOUTHEAST/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_SAFARI_ZONE_SOUTH/MAIN": {
+ "parent_map": "MAP_SAFARI_ZONE_SOUTH",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SAFARI_ZONE_NORTH/MAIN",
+ "REGION_SAFARI_ZONE_SOUTHEAST/MAIN",
+ "REGION_SAFARI_ZONE_SOUTHWEST/MAIN"
+ ],
+ "warps": [
+ "MAP_SAFARI_ZONE_SOUTH:0/MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:0"
+ ]
+ },
+ "REGION_SAFARI_ZONE_SOUTHWEST/MAIN": {
+ "parent_map": "MAP_SAFARI_ZONE_SOUTHWEST",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SAFARI_ZONE_SOUTHWEST/POND",
+ "REGION_SAFARI_ZONE_SOUTH/MAIN",
+ "REGION_SAFARI_ZONE_NORTHWEST/MAIN"
+ ],
+ "warps": [
+ "MAP_SAFARI_ZONE_SOUTHWEST:0/MAP_SAFARI_ZONE_REST_HOUSE:0"
+ ]
+ },
+ "REGION_SAFARI_ZONE_SOUTHWEST/POND": {
+ "parent_map": "MAP_SAFARI_ZONE_SOUTHWEST",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_SAFARI_ZONE_SOUTH_WEST_MAX_REVIVE"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_SAFARI_ZONE_SOUTHEAST/MAIN": {
+ "parent_map": "MAP_SAFARI_ZONE_SOUTHEAST",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "HIDDEN_ITEM_SAFARI_ZONE_SOUTH_EAST_PP_UP",
+ "HIDDEN_ITEM_SAFARI_ZONE_SOUTH_EAST_FULL_RESTORE"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_SAFARI_ZONE_SOUTHEAST/WATER",
+ "REGION_SAFARI_ZONE_SOUTH/MAIN",
+ "REGION_SAFARI_ZONE_NORTHEAST/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_SAFARI_ZONE_SOUTHEAST/WATER": {
+ "parent_map": "MAP_SAFARI_ZONE_SOUTHEAST",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SAFARI_ZONE_SOUTHEAST/MAIN",
+ "REGION_SAFARI_ZONE_SOUTHEAST/ISLAND"
+ ],
+ "warps": []
+ },
+ "REGION_SAFARI_ZONE_SOUTHEAST/ISLAND": {
+ "parent_map": "MAP_SAFARI_ZONE_SOUTHEAST",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_SAFARI_ZONE_SOUTH_EAST_BIG_PEARL"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_SAFARI_ZONE_REST_HOUSE/MAIN": {
+ "parent_map": "MAP_SAFARI_ZONE_REST_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_SAFARI_ZONE_REST_HOUSE:0,1/MAP_SAFARI_ZONE_SOUTHWEST:0"
+ ]
+ },
+ "REGION_ROUTE122/SEA": {
+ "parent_map": "MAP_ROUTE122",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE122/MT_PYRE_ENTRANCE",
+ "REGION_ROUTE121/WATER",
+ "REGION_ROUTE123/EAST"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE122/MT_PYRE_ENTRANCE": {
+ "parent_map": "MAP_ROUTE122",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE122/SEA"
+ ],
+ "warps": [
+ "MAP_ROUTE122:0/MAP_MT_PYRE_1F:0"
+ ]
+ },
+ "REGION_ROUTE123/WEST": {
+ "parent_map": "MAP_ROUTE123",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "ITEM_ROUTE_123_ULTRA_BALL",
+ "HIDDEN_ITEM_ROUTE_123_REVIVE",
+ "BERRY_TREE_14",
+ "BERRY_TREE_15",
+ "BERRY_TREE_30",
+ "BERRY_TREE_58",
+ "BERRY_TREE_59",
+ "BERRY_TREE_60",
+ "BERRY_TREE_61",
+ "BERRY_TREE_65",
+ "BERRY_TREE_72",
+ "BERRY_TREE_73",
+ "BERRY_TREE_74",
+ "BERRY_TREE_79",
+ "TRAINER_JAZMYN_REWARD",
+ "TRAINER_DAVIS_REWARD",
+ "TRAINER_VIOLET_REWARD",
+ "TRAINER_MIU_AND_YUKI_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE118/EAST"
+ ],
+ "warps": [
+ "MAP_ROUTE123:0/MAP_ROUTE123_BERRY_MASTERS_HOUSE:0"
+ ]
+ },
+ "REGION_ROUTE123/EAST": {
+ "parent_map": "MAP_ROUTE123",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_123_CALCIUM",
+ "ITEM_ROUTE_123_ELIXIR",
+ "ITEM_ROUTE_123_PP_UP",
+ "ITEM_ROUTE_123_REVIVAL_HERB",
+ "HIDDEN_ITEM_ROUTE_123_SUPER_REPEL",
+ "HIDDEN_ITEM_ROUTE_123_HYPER_POTION",
+ "NPC_GIFT_RECEIVED_TM_GIGA_DRAIN",
+ "BERRY_TREE_57",
+ "BERRY_TREE_62",
+ "BERRY_TREE_63",
+ "BERRY_TREE_64",
+ "BERRY_TREE_87",
+ "BERRY_TREE_88",
+ "TRAINER_JACKI_1_REWARD",
+ "TRAINER_FREDRICK_REWARD",
+ "TRAINER_BRAXTON_REWARD",
+ "TRAINER_FERNANDO_1_REWARD",
+ "TRAINER_ALBERTO_REWARD",
+ "TRAINER_WENDY_REWARD",
+ "TRAINER_KINDRA_REWARD",
+ "TRAINER_ED_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE123/WEST",
+ "REGION_ROUTE123/EAST_BEHIND_TREE",
+ "REGION_ROUTE123/POND",
+ "REGION_ROUTE122/SEA"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE123/POND": {
+ "parent_map": "MAP_ROUTE123",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE123/EAST_BEHIND_TREE": {
+ "parent_map": "MAP_ROUTE123",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_ROUTE_123_PP_UP",
+ "HIDDEN_ITEM_ROUTE_123_RARE_CANDY",
+ "TRAINER_CAMERON_1_REWARD",
+ "TRAINER_JONAS_REWARD",
+ "TRAINER_KAYLEY_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE123/EAST"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE123_BERRY_MASTERS_HOUSE/MAIN": {
+ "parent_map": "MAP_ROUTE123_BERRY_MASTERS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "NPC_GIFT_BERRY_MASTER_RECEIVED_BERRY_1",
+ "NPC_GIFT_BERRY_MASTER_RECEIVED_BERRY_2",
+ "NPC_GIFT_BERRY_MASTERS_WIFE"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE123_BERRY_MASTERS_HOUSE:0,1/MAP_ROUTE123:0"
+ ]
+ },
+ "REGION_ROUTE124/MAIN": {
+ "parent_map": "MAP_ROUTE124",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "TRAINER_DECLAN_REWARD",
+ "TRAINER_GRACE_REWARD",
+ "TRAINER_LILA_AND_ROY_1_REWARD",
+ "TRAINER_SPENCER_REWARD",
+ "TRAINER_JENNY_1_REWARD",
+ "TRAINER_CHAD_REWARD",
+ "TRAINER_ROLAND_REWARD",
+ "TRAINER_ISABELLA_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_LILYCOVE_CITY/SEA",
+ "REGION_MOSSDEEP_CITY/MAIN",
+ "REGION_UNDERWATER_ROUTE124/BIG_AREA",
+ "REGION_UNDERWATER_ROUTE124/SMALL_AREA_1",
+ "REGION_UNDERWATER_ROUTE124/SMALL_AREA_2",
+ "REGION_UNDERWATER_ROUTE124/SMALL_AREA_3",
+ "REGION_UNDERWATER_ROUTE124/TUNNEL_1",
+ "REGION_UNDERWATER_ROUTE124/TUNNEL_2",
+ "REGION_UNDERWATER_ROUTE124/TUNNEL_3",
+ "REGION_UNDERWATER_ROUTE124/TUNNEL_4"
+ ],
+ "warps": [
+ "MAP_ROUTE124:0/MAP_ROUTE124_DIVING_TREASURE_HUNTERS_HOUSE:0"
+ ]
+ },
+ "REGION_ROUTE124/NORTH_ENCLOSED_AREA_1": {
+ "parent_map": "MAP_ROUTE124",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_124_RED_SHARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_UNDERWATER_ROUTE124/TUNNEL_1"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE124/NORTH_ENCLOSED_AREA_2": {
+ "parent_map": "MAP_ROUTE124",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_UNDERWATER_ROUTE124/TUNNEL_1"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE124/NORTH_ENCLOSED_AREA_3": {
+ "parent_map": "MAP_ROUTE124",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_124_YELLOW_SHARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_UNDERWATER_ROUTE124/TUNNEL_2"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE124/SOUTH_ENCLOSED_AREA_1": {
+ "parent_map": "MAP_ROUTE124",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_124_BLUE_SHARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_UNDERWATER_ROUTE124/TUNNEL_3"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE124/SOUTH_ENCLOSED_AREA_2": {
+ "parent_map": "MAP_ROUTE124",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_UNDERWATER_ROUTE124/TUNNEL_3"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE124/SOUTH_ENCLOSED_AREA_3": {
+ "parent_map": "MAP_ROUTE124",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE126/NEAR_ROUTE_124",
+ "REGION_UNDERWATER_ROUTE124/TUNNEL_4"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE124/BIG_AREA": {
+ "parent_map": "MAP_UNDERWATER_ROUTE124",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_124_GREEN_SHARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE124/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE124/SMALL_AREA_1": {
+ "parent_map": "MAP_UNDERWATER_ROUTE124",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_124_PEARL"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE124/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE124/SMALL_AREA_2": {
+ "parent_map": "MAP_UNDERWATER_ROUTE124",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_124_BIG_PEARL"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE124/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE124/SMALL_AREA_3": {
+ "parent_map": "MAP_UNDERWATER_ROUTE124",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_124_HEART_SCALE_1"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE124/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE124/TUNNEL_1": {
+ "parent_map": "MAP_UNDERWATER_ROUTE124",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_124_CALCIUM",
+ "HIDDEN_ITEM_UNDERWATER_124_HEART_SCALE_2"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE124/NORTH_ENCLOSED_AREA_1",
+ "REGION_ROUTE124/NORTH_ENCLOSED_AREA_2",
+ "REGION_ROUTE124/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE124/TUNNEL_2": {
+ "parent_map": "MAP_UNDERWATER_ROUTE124",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE124/NORTH_ENCLOSED_AREA_3",
+ "REGION_ROUTE124/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE124/TUNNEL_3": {
+ "parent_map": "MAP_UNDERWATER_ROUTE124",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_124_CARBOS"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE124/SOUTH_ENCLOSED_AREA_1",
+ "REGION_ROUTE124/SOUTH_ENCLOSED_AREA_2",
+ "REGION_ROUTE124/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE124/TUNNEL_4": {
+ "parent_map": "MAP_UNDERWATER_ROUTE124",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE124/SOUTH_ENCLOSED_AREA_3",
+ "REGION_ROUTE124/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE124_DIVING_TREASURE_HUNTERS_HOUSE/MAIN": {
+ "parent_map": "MAP_ROUTE124_DIVING_TREASURE_HUNTERS_HOUSE",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ROUTE124_DIVING_TREASURE_HUNTERS_HOUSE:0,1/MAP_ROUTE124:0"
+ ]
+ },
+ "REGION_ROUTE125/SEA": {
+ "parent_map": "MAP_ROUTE125",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_125_BIG_PEARL",
+ "TRAINER_NOLEN_REWARD",
+ "TRAINER_ERNEST_1_REWARD",
+ "TRAINER_SHARON_REWARD",
+ "TRAINER_TANYA_REWARD",
+ "TRAINER_PRESLEY_REWARD",
+ "TRAINER_AURON_REWARD",
+ "TRAINER_STAN_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE125/SHOAL_CAVE_ENTRANCE",
+ "REGION_MOSSDEEP_CITY/MAIN",
+ "REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_1",
+ "REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_2"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE125/SHOAL_CAVE_ENTRANCE": {
+ "parent_map": "MAP_ROUTE125",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "TRAINER_KIM_AND_IRIS_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE125/SEA"
+ ],
+ "warps": [
+ "MAP_ROUTE125:0/MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM:0"
+ ]
+ },
+ "REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_1": {
+ "parent_map": "MAP_UNDERWATER_ROUTE125",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE125/SEA",
+ "REGION_UNDERWATER_MARINE_CAVE/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_2": {
+ "parent_map": "MAP_UNDERWATER_ROUTE125",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE125/SEA",
+ "REGION_UNDERWATER_MARINE_CAVE/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE126/MAIN": {
+ "parent_map": "MAP_ROUTE126",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "TRAINER_NIKKI_REWARD",
+ "TRAINER_BARRY_REWARD",
+ "TRAINER_SIENNA_REWARD",
+ "TRAINER_PABLO_1_REWARD",
+ "TRAINER_DEAN_REWARD",
+ "TRAINER_LEONARDO_REWARD",
+ "TRAINER_ISOBEL_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE127/MAIN",
+ "REGION_UNDERWATER_ROUTE126/MAIN",
+ "REGION_UNDERWATER_ROUTE126/SMALL_AREA_2"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE126/NEAR_ROUTE_124": {
+ "parent_map": "MAP_ROUTE126",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE124/SOUTH_ENCLOSED_AREA_3",
+ "REGION_UNDERWATER_ROUTE126/TUNNEL"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE126/NORTH_WEST_CORNER": {
+ "parent_map": "MAP_ROUTE126",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_126_GREEN_SHARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_UNDERWATER_ROUTE126/TUNNEL"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE126/WEST": {
+ "parent_map": "MAP_ROUTE126",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "TRAINER_BRENDA_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_UNDERWATER_ROUTE126/SMALL_AREA_1",
+ "REGION_UNDERWATER_ROUTE126/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE126/MAIN": {
+ "parent_map": "MAP_UNDERWATER_ROUTE126",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_126_HEART_SCALE",
+ "HIDDEN_ITEM_UNDERWATER_126_ULTRA_BALL",
+ "HIDDEN_ITEM_UNDERWATER_126_STARDUST",
+ "HIDDEN_ITEM_UNDERWATER_126_BIG_PEARL"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE126/MAIN",
+ "REGION_ROUTE126/WEST"
+ ],
+ "warps": [
+ "MAP_UNDERWATER_ROUTE126:0/MAP_UNDERWATER_SOOTOPOLIS_CITY:0"
+ ]
+ },
+ "REGION_UNDERWATER_ROUTE126/TUNNEL": {
+ "parent_map": "MAP_UNDERWATER_ROUTE126",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE126/NORTH_WEST_CORNER",
+ "REGION_ROUTE126/NEAR_ROUTE_124"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE126/SMALL_AREA_1": {
+ "parent_map": "MAP_UNDERWATER_ROUTE126",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_126_PEARL",
+ "HIDDEN_ITEM_UNDERWATER_126_IRON",
+ "HIDDEN_ITEM_UNDERWATER_126_YELLOW_SHARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE126/WEST"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE126/SMALL_AREA_2": {
+ "parent_map": "MAP_UNDERWATER_ROUTE126",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_126_BLUE_SHARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE126/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE127/MAIN": {
+ "parent_map": "MAP_ROUTE127",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_127_ZINC",
+ "ITEM_ROUTE_127_RARE_CANDY",
+ "TRAINER_CAMDEN_REWARD",
+ "TRAINER_ATHENA_REWARD",
+ "TRAINER_AIDAN_REWARD",
+ "TRAINER_JONAH_REWARD",
+ "TRAINER_HENRY_REWARD",
+ "TRAINER_ROGER_REWARD",
+ "TRAINER_DONNY_REWARD",
+ "TRAINER_KOJI_1_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE126/MAIN",
+ "REGION_MOSSDEEP_CITY/MAIN",
+ "REGION_ROUTE128/MAIN",
+ "REGION_UNDERWATER_ROUTE127/MAIN",
+ "REGION_UNDERWATER_ROUTE127/TUNNEL",
+ "REGION_UNDERWATER_ROUTE127/AREA_1",
+ "REGION_UNDERWATER_ROUTE127/AREA_2",
+ "REGION_UNDERWATER_ROUTE127/AREA_3",
+ "REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_1",
+ "REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_2"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE127/ENCLOSED_AREA": {
+ "parent_map": "MAP_ROUTE127",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_127_CARBOS"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_UNDERWATER_ROUTE127/TUNNEL"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE127/MAIN": {
+ "parent_map": "MAP_UNDERWATER_ROUTE127",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_127_HEART_SCALE"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE127/MAIN",
+ "REGION_UNDERWATER_ROUTE128/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE127/TUNNEL": {
+ "parent_map": "MAP_UNDERWATER_ROUTE127",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE127/MAIN",
+ "REGION_ROUTE127/ENCLOSED_AREA"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE127/AREA_1": {
+ "parent_map": "MAP_UNDERWATER_ROUTE127",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_127_STAR_PIECE"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE127/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE127/AREA_2": {
+ "parent_map": "MAP_UNDERWATER_ROUTE127",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_127_HP_UP"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE127/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE127/AREA_3": {
+ "parent_map": "MAP_UNDERWATER_ROUTE127",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_127_RED_SHARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE127/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_1": {
+ "parent_map": "MAP_UNDERWATER_ROUTE127",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE127/MAIN",
+ "REGION_UNDERWATER_MARINE_CAVE/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_2": {
+ "parent_map": "MAP_UNDERWATER_ROUTE127",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE127/MAIN",
+ "REGION_UNDERWATER_MARINE_CAVE/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE128/MAIN": {
+ "parent_map": "MAP_ROUTE128",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "HIDDEN_ITEM_ROUTE_128_HEART_SCALE_1",
+ "HIDDEN_ITEM_ROUTE_128_HEART_SCALE_2",
+ "HIDDEN_ITEM_ROUTE_128_HEART_SCALE_3",
+ "TRAINER_ALEXA_REWARD",
+ "TRAINER_RUBEN_REWARD",
+ "TRAINER_ISAIAH_1_REWARD",
+ "TRAINER_WAYNE_REWARD",
+ "TRAINER_KATELYN_1_REWARD",
+ "TRAINER_HARRISON_REWARD",
+ "TRAINER_CARLEE_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE127/MAIN",
+ "REGION_ROUTE129/MAIN",
+ "REGION_EVER_GRANDE_CITY/SEA",
+ "REGION_UNDERWATER_ROUTE128/MAIN",
+ "REGION_UNDERWATER_ROUTE128/AREA_1",
+ "REGION_UNDERWATER_ROUTE128/AREA_2"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE128/MAIN": {
+ "parent_map": "MAP_UNDERWATER_ROUTE128",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE128/MAIN",
+ "REGION_UNDERWATER_ROUTE127/MAIN"
+ ],
+ "warps": [
+ "MAP_UNDERWATER_ROUTE128:0/MAP_UNDERWATER_SEAFLOOR_CAVERN:0"
+ ]
+ },
+ "REGION_UNDERWATER_ROUTE128/AREA_1": {
+ "parent_map": "MAP_UNDERWATER_ROUTE128",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_128_PROTEIN"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE128/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE128/AREA_2": {
+ "parent_map": "MAP_UNDERWATER_ROUTE128",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [
+ "HIDDEN_ITEM_UNDERWATER_128_PEARL"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE128/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE129/MAIN": {
+ "parent_map": "MAP_ROUTE129",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "TRAINER_REED_REWARD",
+ "TRAINER_CHASE_REWARD",
+ "TRAINER_ALLISON_REWARD",
+ "TRAINER_TISHA_REWARD",
+ "TRAINER_CLARENCE_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE130/MAIN",
+ "REGION_ROUTE128/MAIN",
+ "REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_1",
+ "REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_2"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_1": {
+ "parent_map": "MAP_UNDERWATER_ROUTE129",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE129/MAIN",
+ "REGION_UNDERWATER_MARINE_CAVE/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_2": {
+ "parent_map": "MAP_UNDERWATER_ROUTE129",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE129/MAIN",
+ "REGION_UNDERWATER_MARINE_CAVE/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE130/MAIN": {
+ "parent_map": "MAP_ROUTE130",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "TRAINER_RODNEY_REWARD",
+ "TRAINER_KATIE_REWARD",
+ "TRAINER_SANTIAGO_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE130/MIRAGE_ISLAND",
+ "REGION_ROUTE129/MAIN",
+ "REGION_ROUTE131/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE130/MIRAGE_ISLAND": {
+ "parent_map": "MAP_ROUTE130",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": true,
+ "locations": [
+ "BERRY_TREE_82"
+ ],
+ "events": [],
+ "exits": [],
+ "warps": []
+ },
+ "REGION_ROUTE131/MAIN": {
+ "parent_map": "MAP_ROUTE131",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "TRAINER_KEVIN_REWARD",
+ "TRAINER_TALIA_REWARD",
+ "TRAINER_RICHARD_REWARD",
+ "TRAINER_KARA_REWARD",
+ "TRAINER_HERMAN_REWARD",
+ "TRAINER_SUSIE_REWARD",
+ "TRAINER_RELI_AND_IAN_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_PACIFIDLOG_TOWN/MAIN",
+ "REGION_ROUTE130/MAIN"
+ ],
+ "warps": [
+ "MAP_ROUTE131:0/MAP_SKY_PILLAR_ENTRANCE:0"
+ ]
+ },
+ "REGION_ROUTE132/EAST": {
+ "parent_map": "MAP_ROUTE132",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE132/WEST",
+ "REGION_PACIFIDLOG_TOWN/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE132/WEST": {
+ "parent_map": "MAP_ROUTE132",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_132_RARE_CANDY",
+ "ITEM_ROUTE_132_PROTEIN",
+ "TRAINER_GILBERT_REWARD",
+ "TRAINER_RONALD_REWARD",
+ "TRAINER_DARCY_REWARD",
+ "TRAINER_PAXTON_REWARD",
+ "TRAINER_JONATHAN_REWARD",
+ "TRAINER_MAKAYLA_REWARD",
+ "TRAINER_KIYO_REWARD",
+ "TRAINER_DANA_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE133/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE133/MAIN": {
+ "parent_map": "MAP_ROUTE133",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_133_BIG_PEARL",
+ "ITEM_ROUTE_133_STAR_PIECE",
+ "ITEM_ROUTE_133_MAX_REVIVE",
+ "TRAINER_LINDA_REWARD",
+ "TRAINER_FRANKLIN_REWARD",
+ "TRAINER_DEBRA_REWARD",
+ "TRAINER_MOLLIE_REWARD",
+ "TRAINER_CONOR_REWARD",
+ "TRAINER_WARREN_REWARD",
+ "TRAINER_BECK_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE134/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE134/MAIN": {
+ "parent_map": "MAP_ROUTE134",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [
+ "ITEM_ROUTE_134_CARBOS",
+ "ITEM_ROUTE_134_STAR_PIECE",
+ "TRAINER_ALEX_REWARD",
+ "TRAINER_KELVIN_REWARD",
+ "TRAINER_HUDSON_REWARD",
+ "TRAINER_REYNA_REWARD",
+ "TRAINER_HITOSHI_REWARD",
+ "TRAINER_MARLEY_REWARD",
+ "TRAINER_AARON_REWARD",
+ "TRAINER_LAUREL_REWARD",
+ "TRAINER_JACK_REWARD"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE134/WEST",
+ "REGION_UNDERWATER_ROUTE134/MAIN"
+ ],
+ "warps": []
+ },
+ "REGION_ROUTE134/WEST": {
+ "parent_map": "MAP_ROUTE134",
+ "has_grass": false,
+ "has_water": true,
+ "has_fishing": true,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_SLATEPORT_CITY/WATER"
+ ],
+ "warps": []
+ },
+ "REGION_UNDERWATER_ROUTE134/MAIN": {
+ "parent_map": "MAP_UNDERWATER_ROUTE134",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE134/MAIN"
+ ],
+ "warps": [
+ "MAP_UNDERWATER_ROUTE134:0/MAP_UNDERWATER_SEALED_CHAMBER:0"
+ ]
+ }
+}
diff --git a/worlds/pokemon_emerald/data/regions/unused/battle_frontier.json b/worlds/pokemon_emerald/data/regions/unused/battle_frontier.json
new file mode 100644
index 000000000000..cb3cf02a7f13
--- /dev/null
+++ b/worlds/pokemon_emerald/data/regions/unused/battle_frontier.json
@@ -0,0 +1,52 @@
+{
+ "REGION_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1!"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1!"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0",
+ "MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2",
+ "MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:3/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0!"
+ ]
+ },
+ "REGION_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM/MAIN": {
+ "parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM",
+ "has_grass": false,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2"
+ ]
+ }
+}
diff --git a/worlds/pokemon_emerald/data/regions/unused/dungeons.json b/worlds/pokemon_emerald/data/regions/unused/dungeons.json
new file mode 100644
index 000000000000..8d15d9bb2849
--- /dev/null
+++ b/worlds/pokemon_emerald/data/regions/unused/dungeons.json
@@ -0,0 +1,14 @@
+{
+ "REGION_ALTERING_CAVE/MAIN": {
+ "parent_map": "MAP_ALTERING_CAVE",
+ "has_grass": true,
+ "has_water": false,
+ "has_fishing": false,
+ "locations": [],
+ "events": [],
+ "exits": [],
+ "warps": [
+ "MAP_ALTERING_CAVE:0/MAP_ROUTE103:0"
+ ]
+ }
+}
diff --git a/worlds/pokemon_emerald/data/trade_pokemon_schema.json b/worlds/pokemon_emerald/data/trade_pokemon_schema.json
new file mode 100644
index 000000000000..c261c5b08fc9
--- /dev/null
+++ b/worlds/pokemon_emerald/data/trade_pokemon_schema.json
@@ -0,0 +1,162 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "title": "Pokemon",
+ "type": "object",
+ "required": ["version", "language", "nickname", "personality", "species", "experience", "ivs", "evs", "moves", "trainer"],
+ "properties": {
+ "version": {
+ "description": "The version of this schema that the data is formatted to match",
+ "type": "string",
+ "const": "1"
+ },
+ "language": {
+ "description": "The language of origin",
+ "enum": [
+ "Japanese",
+ "English",
+ "French",
+ "Italian",
+ "German",
+ "Spanish"
+ ]
+ },
+ "nickname": {
+ "description": "The pokemon's nickname",
+ "type": "string",
+ "minLength": 1
+ },
+ "personality": {
+ "description": "The pokemon's 32-bit personality value",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 4294967295
+ },
+ "species": {
+ "description": "The national dex number of the pokemon species",
+ "type": "integer",
+ "minimum": 0
+ },
+ "item": {
+ "description": "The id of the item the pokemon is holding according to modern tables",
+ "type": "integer"
+ },
+ "experience": {
+ "description": "The current total EXP",
+ "type": "integer",
+ "minimum": 0
+ },
+ "ability": {
+ "description": "The value of the ability bit (hidden abilities should be a separate bit)",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 1
+ },
+ "ivs": {
+ "description": "The 6 IVs of the pokemon",
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 31
+ },
+ "minItems": 6,
+ "maxItems": 6
+ },
+ "evs": {
+ "description": "The 6 EVs of the pokemon",
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 255
+ },
+ "minItems": 6,
+ "maxItems": 6
+ },
+ "conditions": {
+ "description": "The 6 condition (contest) stats of the pokemon",
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 255
+ },
+ "minItems": 6,
+ "maxItems": 6
+ },
+ "pokerus": {
+ "description": "The value of the pokerus status byte",
+ "type": "integer",
+ "minimum": 0
+ },
+ "game": {
+ "description": "The id of the game the pokemon originated in",
+ "type": "integer",
+ "minimum": 0
+ },
+ "location_met": {
+ "description": "The location id for where the pokemon was met",
+ "type": "integer",
+ "minimum": 0
+ },
+ "level_met": {
+ "description": "The level the pokemon was met at",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100
+ },
+ "ball": {
+ "description": "The type of poke ball the pokemon was caught in",
+ "type": "integer",
+ "minimum": 1
+ },
+ "moves": {
+ "description": "The move id, PP, and PP Ups used for each move slot",
+ "type": "array",
+ "items": {
+ "type": "array",
+ "prefixItems": [
+ {
+ "description": "The move's id according to modern tables (use 0 for an empty slot)",
+ "type": "integer"
+ },
+ {
+ "description": "The move's max PP",
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "description": "The number of times a PP Up has been used on this move",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 3
+ }
+ ],
+ "minLength": 4,
+ "maxLength": 4
+ }
+ },
+ "trainer": {
+ "description": "Original trainer info",
+ "type": "object",
+ "properties": {
+ "name": {
+ "description": "The original trainer's name",
+ "type": "string",
+ "minLength": 1
+ },
+ "id": {
+ "description": "The original trainer's 32-bit ID (includes secret id as higher order bytes)",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 4294967295
+ },
+ "female": {
+ "description": "Whether the original trainer is female",
+ "type": "boolean"
+ }
+ },
+ "required": ["name", "id"]
+ }
+ }
+}
\ No newline at end of file
diff --git a/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md b/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md
new file mode 100644
index 000000000000..9a3991e97f75
--- /dev/null
+++ b/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md
@@ -0,0 +1,88 @@
+# Pokémon Emerald
+
+## Where is the options page?
+
+You can read through all the options and generate a YAML [here](../player-options).
+
+## What does randomization do to this game?
+
+This randomizer handles both item randomization and pokémon randomization. Badges, HMs, gifts from NPCs, and items on
+the ground can all be randomized. There are also many options for randomizing wild pokémon, starters, opponent pokémon,
+abilities, types, etc… You can even change a percentage of single battles into double battles. Check the
+[options page](../player-options) for a more comprehensive list of what can be changed.
+
+## What items and locations get randomized?
+
+The most interesting items that can be added to the item pool are badges and HMs, which most affect what locations you
+can access. Key items like the Devon Scope or Mach Bike can also be randomized, as well as the many Potions, Revives,
+TMs, and other items that you can find on the ground or receive as gifts.
+
+## What other changes are made to the game?
+
+There are many quality of life improvements meant to speed up the game a little and improve the experience of playing a
+randomizer. Here are some of the more important ones:
+
+- Shoal Cave switches between high tide and low tide every time you re-enter
+- Bag space is greatly expanded (you're all but guaranteed to never need to store items in the PC)
+- Trade evolutions have been changed to level or item evolutions
+- You can have both bikes simultaneously
+- You can run or bike (almost) anywhere
+- The Wally catching tutorial is skipped
+- All text is instant and, with an option, can be automatically progressed by holding A
+- When a Repel runs out, you will be prompted to use another
+- Many more minor improvements…
+
+## Where is my starting inventory?
+
+Except for badges, your starting inventory will be in the PC.
+
+## What does another world's item look like in Pokémon Emerald?
+
+When you find an item that is not your own, you will see the item's name and its owner while the item received jingle
+plays.
+
+## When the player receives an item, what happens?
+
+You will only receive items while in the overworld and not during battles. Depending on your `Receive Item Messages`
+option, the received item will either be silently added to your bag or you will be shown a text box with the item's
+name and the item will be added to your bag while a fanfare plays.
+
+## Can I play offline?
+
+Yes, the client and connector are only necessary for sending and receiving items. If you're playing a solo game, you
+don't need to play online unless you want the rest of Archipelago's functionality (like hints and auto-tracking). If
+you're playing a multiworld game, the client will sync your game with the server the next time you connect.
+
+## Will battle mechanics be updated?
+
+Unfortunately, no. We don't want to force new mechanics on players who would prefer to play with the classic mechanics,
+but updating would require such drastic changes to the underlying code that it would be unreasonable to toggle between
+them.
+
+## Is this randomizer compatible with other mods?
+
+No, other mods cannot be applied. It would be impossible to generalize this implementation's changes in a way that is
+compatible with any other mod or romhack. Romhacks could be added as their own games, but they would have to be
+implemented separately. Check out [Archipelago's Discord server](https://discord.gg/8Z65BR2) if you want to make a
+suggestion or contribute.
+
+## Can I use tools like the Universal Pokémon Randomizer?
+
+No, tools like UPR expect data to be in certain locations and in a certain format, but this randomizer has to shift it
+around. Using tools to try to modify the game would only corrupt the ROM.
+
+We realize this means breaking from established habits when it comes to randomizing Pokémon games, but this randomizer
+would be many times more complex to develop if it were constrained by something like UPR.
+
+### There are two possible exceptions
+
+#### PKHex
+
+You may be able to extract pokémon from your save using PKHeX, but this isn't a guarantee, and we make no effort to keep
+our saves compatible with PKHeX. Box and party pokémon are the only aspects of your save file likely to work.
+
+#### PokéFinder/RNG Reporter
+
+In the spirit of randomization, Emerald's broken RNG is fixed in Archipelago. More specifically, it's reverted to work
+as it did in Ruby/Sapphire. So while you can't make the assumption that the RNG is seeded at 0, you can set the battery
+to dry, which will seed it in the same way that Ruby/Sapphire are seeded when the battery is dry.
diff --git a/worlds/pokemon_emerald/docs/region data.md b/worlds/pokemon_emerald/docs/region data.md
new file mode 100644
index 000000000000..767b5cac2c97
--- /dev/null
+++ b/worlds/pokemon_emerald/docs/region data.md
@@ -0,0 +1,79 @@
+## Region Data
+
+Regions, connections, and associated locations are defined in `data/regions`. If you know what you're doing, it should
+be pretty clear how the data works by taking a quick look through the files. But the quick tl;dr is:
+
+- Every map, even trivial ones, gets a region definition, and they cannot be coalesced (because of warp rando)
+- Stick to the naming convention for regions and events (look at Route 103 and Petalburg City for guidance)
+- Locations and warps can only be claimed by one region
+- Events are declared here
+
+A `Map`, which you will see referenced in `parent_map` attribute in the region JSON, is an id from the source code.
+`Map`s are sets of tiles, encounters, warps, events, and so on. Route 103, Littleroot Town, the Oldale Town Mart, the
+second floor of Devon Corp, and each level of Victory Road are all examples of `Map`s. You transition between `Map`s by
+stepping on a warp (warp pads, doorways, etc.) or walking over a border between `Map`s in the overworld. Some warps
+don't go to a different `Map`.
+
+Regions usually describe physical areas which are subsets of a `Map`. Every `Map` must have one or more defined regions.
+A region should not contain area from more than one `Map`. We'll need to draw those lines now even when there is no
+logical boundary (like between two the first and second floors of your rival's house), for warp rando.
+
+Most `Map`s have been split into multiple regions. In the example below, `MAP_ROUTE103` was split into
+`REGION_ROUTE_103/WEST`, `REGION_ROUTE_103/WATER`, and `REGION_ROUTE_103/EAST` (this document may be out of date; the
+example is demonstrative). Keeping the name consistent with the `Map` name and adding a label suffix for the subarea
+makes it clearer where we are in the world and where within a `Map` we're describing.
+
+Every region (except `Menu`) is configured here. All files in this directory are combined with each other at runtime,
+and are only split and ordered for organization. Regions defined in `data/regions/unused` are remnants from
+automatically generated regions and represent places that exist but aren't reachable or aren't currently relevant to the
+randomizer. Any locations or warps in there should be ignored. Data for a single region looks like this:
+
+```json
+"REGION_ROUTE103/EAST": {
+ "parent_map": "MAP_ROUTE103",
+ "locations": [
+ "ITEM_ROUTE_103_GUARD_SPEC",
+ "ITEM_ROUTE_103_PP_UP"
+ ],
+ "events": [],
+ "exits": [
+ "REGION_ROUTE103/WATER",
+ "REGION_ROUTE110/MAIN"
+ ],
+ "warps": [
+ "MAP_ROUTE103:0/MAP_ALTERING_CAVE:0"
+ ]
+}
+```
+
+- `[key]`: The name of the object, in this case `REGION_ROUTE103/EAST`, should be the value of `parent_map` where the
+`MAP` prefix is replaced with `REGION`. Then there should be a following `/` and a label describing this specific region
+within the `Map`. This is not enforced or required by the code, but it makes things much more clear.
+- `parent_map`: The name of the `Map` this region exists under. It can relate this region to information like encounter
+tables.
+- `locations`: Locations contained within this region. This can be anything from an item on the ground to a badge to a
+gift from an NPC. Locations themselves are defined in `data/extracted_data.json`, and the names used here should come
+directly from it.
+- `events`: Events that can be completed in this region. Defeating a gym leader or Aqua/Magma team leader, for example,
+can trigger story progression and unblock roads and buildings. Events are defined here and nowhere else, and access
+rules are set in `rules.py`.
+- `exits`: Names of regions that can be directly accessed from this one. Most often regions within the same `Map`,
+neighboring maps in the overworld, or transitions from using HM08 Dive. Most connections between maps/regions come from
+warps. Any region in this list should be defined somewhere in `data/regions/`.
+- `warps`: Warp events contained within this region. Warps are defined in `data/extracted_data.json`, and must exist
+there to be referenced here. More on warps in [../docs/warps.md](../docs/warps.md).
+
+Think of this data as defining which regions are "claiming" a given location, event, or warp. No more than one region
+may claim ownership of a location. Even if some "thing" may happen in two different regions and set the same flag, they
+should be defined as two different events and anything conditional on said "thing" happening can check whether either of
+the two events is accessible. (e.g. Interacting with the Poke Ball in your rival's room and going back downstairs will
+both trigger a conversation with them which enables you to rescue Professor Birch. It's the same "thing" on two
+different `Map`s.)
+
+Conceptually, you shouldn't have to "add" any new regions. You should only have to "split" existing regions. When you
+split a region, make sure to correctly reassign `locations`, `events`, `exits`, and `warps` according to which new
+region they now exist in. Make sure to define new `exits` to link the new regions to each other if applicable. And
+especially remember to rename incoming `exits` defined in other regions which are still pointing to the pre-split
+region. `sanity_check.py` should catch you if there are other regions that point to a region that no longer exists, but
+if one of your newly-split regions still has the same name as the original, it won't be detected and you may find that
+things aren't connected correctly.
diff --git a/worlds/pokemon_emerald/docs/rom changes.md b/worlds/pokemon_emerald/docs/rom changes.md
new file mode 100644
index 000000000000..6dec685f70d2
--- /dev/null
+++ b/worlds/pokemon_emerald/docs/rom changes.md
@@ -0,0 +1,107 @@
+## New Behaviors
+
+- The union room receptionist on the second floor of Pokemon Centers was reworked for wonder trading via Archipelago
+- Norman will give you all event ticket items when he gives you the S.S. Ticket
+- Use of event tickets is streamlined and the scripts are refactored to skip "first time use" stuff
+- The roaming pokemon is forced to Latios
+- The pokemon at Southern Island is forced to Latias
+- There is new code for changing your party's levels during trainer battles which also modifies exp gain
+
+## QoL
+
+- The menu has a GO HOME option instead of EXIT, which will immediately teleport you to Birch's Lab
+- It is possible to teach over HM moves
+- The catch tutorial and cutscenes during your first visit to Petalburg are skipped
+- The match call tutorial after you leave Devon Corp is skipped
+- Random match calls in general are skipped, and trainers no longer ask to register you after a battle
+- Searching by type in the pokedex includes species you have seen but not yet caught
+- Cycling and running is allowed in every map (some exceptions like Fortree and Pacifidlog)
+- When you run out of Repel steps, you'll be prompted to use another one if you have more in your bag
+- Text is always rendered in its entirety on the first frame (instant text)
+- With an option set, text will advance if A is held
+- The message explaining that the trainer is about to send out a new pokemon is shortened to fit on two lines so that
+you can still read the species when deciding whether to change pokemon
+- The Pokemon Center Nurse dialogue is entirely removed except for the final text box
+- When receiving TMs and HMs, the move that it teaches is consistently displayed in the "received item" message (by
+default, certain ways of receiving items would only display the TM/HM number)
+- The Pokedex starts in national mode
+- The fishing minigame is always successful at finding a catch, only requires one round, and will always show four dots
+- With an option in Archipelago, spinning trainers become predictable
+- Removed a ledge on Route 123 which allows you to collect every item without backtracking
+- The Oldale Pokemart sells Poke Balls at the start of the game
+- Pauses during battles (e.g. the ~1 second pause at the start of a turn before an opponent uses a potion) are shorter
+by 62.5%
+- The sliding animation for trainers and wild pokemon at the start of a battle runs at double speed.
+- Bag space was greatly expanded (there is room for one stack of every unique item in every pocket, plus a little bit
+extra for some pockets)
+ - Save data format was changed as a result of this. Shrank some unused space and removed some multiplayer phrases from
+ the save data.
+ - Pretty much any code that checks for bag space is ignored or bypassed (this sounds dangerous, but with expanded bag
+ space you should pretty much never have a full bag unless you're trying to fill it up, and skipping those checks
+ greatly simplifies detecting when items are picked up)
+- Pokemon are never disobedient
+- When moving in the overworld, set the input priority based on the most recently pressed direction rather than by some
+predetermined priority
+- Shoal cave changes state every time you reload the map and is no longer tied to the RTC
+- Increased safari zone steps from 500 to 50000
+- Trainers will not approach the player if the blind trainers option is set
+- Defeating the elite 4 respawns all legendary encounters where the encounter ended by fainting the pokemon
+- The cutscene revealing the existence of Latios also gives you dex info for having seen Latios
+- The braille wall hinting at the solution to the Wailord/Relicanth puzzle gives you dex info for having seen Wailord
+and Relicanth
+- Changed trade evolutions to be possible without trading:
+ - Politoed: Use King's Rock in bag menu
+ - Alakazam: Level 37
+ - Machamp: Level 37
+ - Golem: Level 37
+ - Slowking: Use King's Rock in bag menu
+ - Gengar: Level 37
+ - Steelix: Use Metal Coat in bag menu
+ - Kingdra: Use Dragon Scale in bag menu
+ - Scizor: Use Metal Coat in bag menu
+ - Porygon2: Use Up-Grade in bag menu
+ - Milotic: Level 30
+ - Huntail: Use Deep Sea Tooth in bag menu
+ - Gorebyss: Use Deep Sea Scale in bag menu
+
+## Game State Changes/Softlock Prevention
+
+- Mr. Briney never disappears or stops letting you use his ferry
+- Upon releasing Kyogre, Sootopolis and Sky Pillar will be advanced to after Rayquaza has been awakened, skipping the
+Wallace and Rayquaza fetch quest
+- Prevent the player from flying or surfing until they have received the Pokedex
+- The S.S. Tidal will be available at all times
+- All time-based berry gifts are locked to a one-time gift of a specific berry
+- Terra and Marine Cave are given fixed locations, and the weather events revealing them are permanent until the
+legendary encounter is resolved
+- Mirage Island is always present
+- During dexsanity, certain trainers don't disappear/deactivate
+- During berry randomization, it is impossible to plant berries or for berry trees to change state
+- Some NPCs or tiles are removed on the creation of a new save file based on player options
+- Ensured that every species has some damaging move by level 5
+- Route 115 has an alternate layout (must be enabled through Archipelago) which includes a bumpy slope that can cross
+the ledge normally blocking you from entering Meteor Falls from Rustboro City
+- Route 115 may have strength boulders (must be enabled through Archipelago) between the beach and cave entrance
+- Route 118 has an alternate layout (must be enabled through Archipelago) that blocks you from surfing between shores
+and adds a rail so that it can be crossed using the Acro Bike
+- The Petalburg Gym is set up based on your player options rather than after the first 4 gyms
+- The E4 guards will actually check all your badges (or gyms beaten based on your options) instead of just the Feather
+Badge
+- Steven cuts the conversation short in Granite Cave if you don't have the Letter
+- Dock checks that you have the Devon Goods before asking you to deliver them (and thus opening the museum)
+- Rydel gives you both bikes at the same time
+- The man in Pacifidlog who gives you Frustration and Return will give you both at the same time, does not check
+friendship first, and no longer has any behavior related to the RTC
+- The woman who gives you the Soothe Bell in Slateport does not check friendship
+- When trading the Scanner with Captain Stern, you will receive both the Deep Sea Tooth and Deep Sea Scale
+
+## Misc
+
+- You can no longer try to switch bikes in the bike shop
+- The Seashore House only rewards you with 1 Soda Pop instead of 6
+- Many small changes that make it possible to swap single battles to double battles
+ - Includes some safeguards against two trainers seeing you and initiating a battle while one or both of them are
+ "single trainer double battles"
+- Game now properly waits on vblank instead of spinning in a while loop
+- Misc small changes to text for consistency
+- Many bugfixes to the vanilla game code
diff --git a/worlds/pokemon_emerald/docs/setup_en.md b/worlds/pokemon_emerald/docs/setup_en.md
new file mode 100644
index 000000000000..2ae54d5e0c14
--- /dev/null
+++ b/worlds/pokemon_emerald/docs/setup_en.md
@@ -0,0 +1,72 @@
+# Pokémon Emerald Setup Guide
+
+## Required Software
+
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
+- An English Pokémon Emerald ROM. The Archipelago community cannot provide this.
+- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later
+
+### Configuring BizHawk
+
+Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings:
+
+- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from
+`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.)
+- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're
+tabbed out of EmuHawk.
+- Open a `.gba` file in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click
+`Controllers…`, load any `.gba` ROM first.
+- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to
+clear it.
+
+## Optional Software
+
+- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), for use with
+[PopTracker](https://github.com/black-sliver/PopTracker/releases)
+
+## Generating and Patching a Game
+
+1. Create your options file (YAML). You can make one on the
+[Pokémon Emerald options page](../../../games/Pokemon%20Emerald/player-options).
+2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
+This will generate an output file for you. Your patch file will have the `.apemerald` file extension.
+3. Open `ArchipelagoLauncher.exe`
+4. Select "Open Patch" on the left side and select your patch file.
+5. If this is your first time patching, you will be prompted to locate your vanilla ROM.
+6. A patched `.gba` file will be created in the same place as the patch file.
+7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your
+BizHawk install.
+
+If you're playing a single-player seed and you don't care about autotracking or hints, you can stop here, close the
+client, and load the patched ROM in any emulator. However, for multiworlds and other Archipelago features, continue
+below using BizHawk as your emulator.
+
+## Connecting to a Server
+
+By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just
+in case you have to close and reopen a window mid-game for some reason.
+
+1. Pokemon Emerald uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your game,
+you can re-open it from the launcher.
+2. Ensure EmuHawk is running the patched ROM.
+3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing.
+4. In the Lua Console window, go to `Script > Open Script…`.
+5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
+6. The emulator and client will eventually connect to each other. The BizHawk Client window should indicate that it
+connected and recognized Pokemon Emerald.
+7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the
+top text field of the client and click Connect.
+
+You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect. It is
+perfectly safe to make progress offline; everything will re-sync when you reconnect.
+
+## Auto-Tracking
+
+Pokémon Emerald has a fully functional map tracker that supports auto-tracking.
+
+1. Download [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest) and
+[PopTracker](https://github.com/black-sliver/PopTracker/releases).
+2. Put the tracker pack into packs/ in your PopTracker install.
+3. Open PopTracker, and load the Pokémon Emerald pack.
+4. For autotracking, click on the "AP" symbol at the top.
+5. Enter the Archipelago server address (the one you connected your client to), slot name, and password.
diff --git a/worlds/pokemon_emerald/docs/setup_es.md b/worlds/pokemon_emerald/docs/setup_es.md
new file mode 100644
index 000000000000..1d3721862a4f
--- /dev/null
+++ b/worlds/pokemon_emerald/docs/setup_es.md
@@ -0,0 +1,74 @@
+# GuÃa de Configuración para Pokémon Emerald
+
+## Software Requerido
+
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
+- Una ROM de Pokémon Emerald en Inglés. La comunidad de Archipelago no puede proveerla.
+- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 o posterior
+
+### Configuración de BizHawk
+
+Una vez que hayas instalado BizHawk, abre `EmuHawk.exe` y cambia las siguientes configuraciones:
+
+- Si estás usando BizHawk 2.7 o 2.8, ve a `Config > Customize`. En la pestaña Advanced, cambia el Lua Core de
+`NLua+KopiLua` a `Lua+LuaInterface`, luego reinicia EmuHawk. (Si estás usando BizHawk 2.9, puedes saltar este paso.)
+- En `Config > Customize`, activa la opción "Run in background" para prevenir desconexiones del cliente mientras
+la aplicación activa no sea EmuHawk.
+- Abre el archivo `.gba` en EmuHawk y luego ve a `Config > Controllers…` para configurar los controles. Si no puedes
+hacer clic en `Controllers…`, debes abrir cualquier ROM `.gba` primeramente.
+- Considera limpiar tus macros y atajos en `Config > Hotkeys…` si no quieres usarlas de manera intencional. Para
+limpiarlas, selecciona el atajo y presiona la tecla Esc.
+
+## Software Opcional
+
+- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), para usar
+con [PopTracker](https://github.com/black-sliver/PopTracker/releases)
+
+## Generando y Parcheando el Juego
+
+1. Crea tu archivo de configuración (YAML). Puedes hacerlo en
+[Página de Opciones de Pokémon Emerald](../../../games/Pokemon%20Emerald/player-options).
+2. Sigue las instrucciones generales de Archipelago para
+[Generar un juego](../../Archipelago/setup/en#generating-a-game). Esto generará un archivo de salida (output file) para
+ti. Tu archivo de parche tendrá la extensión de archivo `.apemerald`.
+3. Abre `ArchipelagoLauncher.exe`
+4. Selecciona "Open Patch" en el lado derecho y elige tu archivo de parcheo.
+5. Si esta es la primera vez que vas a parchear, se te pedirá que selecciones la ROM sin parchear.
+6. Un archivo parcheado con extensión `.gba` será creado en el mismo lugar que el archivo de parcheo.
+7. La primera vez que abras un archivo parcheado con el BizHawk Client, se te preguntará donde está localizado
+`EmuHawk.exe` en tu instalación de BizHawk.
+
+Si estás jugando una seed Single-Player y no te interesa el auto-tracking o las pistas, puedes parar aquÃ, cierra el
+cliente, y carga la ROM ya parcheada en cualquier emulador. Pero para partidas multi-worlds y para otras
+implementaciones de Archipelago, continúa usando BizHawk como tu emulador.
+
+## Conectando con el Servidor
+
+Por defecto, al abrir un archivo parcheado, se harán de manera automática 1-5 pasos. Aun asÃ, ten en cuenta lo
+siguiente en caso de que debas cerrar y volver a abrir la ventana en mitad de la partida por algún motivo.
+
+1. Pokémon Emerald usa el Archipelago BizHawk Client. Si el cliente no se encuentra abierto al abrir la rom
+parcheada, puedes volver a abrirlo desde el Archipelago Launcher.
+2. Asegúrate que EmuHawk está corriendo la ROM parcheada.
+3. En EmuHawk, ve a `Tools > Lua Console`. Debes tener esta ventana abierta mientras juegas.
+4. En la ventana de Lua Console, ve a `Script > Open Script…`.
+5. Ve a la carpeta donde está instalado Archipelago y abre `data/lua/connector_bizhawk_generic.lua`.
+6. El emulador y el cliente eventualmente se conectarán uno con el otro. La ventana de BizHawk Client indicará que te
+has conectado y reconocerá Pokémon Emerald.
+7. Para conectar el cliente con el servidor, ingresa la dirección y el puerto de la sala (ej. `archipelago.gg:38281`)
+en el campo de texto que se encuentra en la parte superior del cliente y haz click en Connect.
+
+Ahora deberÃas poder enviar y recibir Ãtems. Debes seguir estos pasos cada vez que quieras reconectarte. Es seguro
+jugar de manera offline; se sincronizará todo cuando te vuelvas a conectar.
+
+## Tracking Automático
+
+Pokémon Emerald tiene un Map Tracker completamente funcional que soporta auto-tracking.
+
+1. Descarga [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest) y
+[PopTracker](https://github.com/black-sliver/PopTracker/releases).
+2. Coloca la carpeta del Tracker en la carpeta packs/ dentro de la carpeta de instalación del PopTracker.
+3. Abre PopTracker, y carga el Pack de Pokémon Emerald Map Tracker.
+4. Para utilizar el auto-tracking, haz click en el sÃmbolo "AP" que se encuentra en la parte superior.
+5. Entra la dirección del Servidor de Archipelago (la misma a la que te conectaste para jugar), nombre del jugador, y
+contraseña (deja vacÃo este campo en caso de no utilizar contraseña).
diff --git a/worlds/pokemon_emerald/docs/setup_sv.md b/worlds/pokemon_emerald/docs/setup_sv.md
new file mode 100644
index 000000000000..88b1d384096b
--- /dev/null
+++ b/worlds/pokemon_emerald/docs/setup_sv.md
@@ -0,0 +1,78 @@
+# Pokémon Emerald Installationsguide
+
+## Programvara som behövs
+
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
+- Ett engelskt Pokémon Emerald ROM, Archipelago kan inte hjälpa dig med detta.
+- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 eller senare
+
+### Konfigurera BizHawk
+
+När du har installerat BizHawk, öppna `EmuHawk.exe` och ändra följande inställningar:
+
+- Om du använder BizHawk 2.7 eller 2.8, gå till `Config > Customize`. På "Advanced Tab", byt Lua core från
+`NLua+KopiLua` till `Lua+LuaInterface`, starta om EmuHawk efteråt. (Använder du BizHawk 2.9, kan du skippa detta steg.)
+- Gå till `Config > Customize`. Markera "Run in background" inställningen för att förhindra bortkoppling från
+klienten om du alt-tabbar bort från EmuHawk.
+- Öppna en `.gba` fil i EmuHawk och gå till `Config > Controllers…` för att konfigurera dina inputs.
+Om du inte hittar `Controllers…`, starta ett valfritt `.gba` ROM först.
+- Överväg att rensa keybinds i `Config > Hotkeys…` som du inte tänkt använda. Välj en keybind och tryck på ESC
+för att rensa bort den.
+
+## Extra programvara
+
+- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest),
+används tillsammans med
+[PopTracker](https://github.com/black-sliver/PopTracker/releases)
+
+## Generera och patcha ett spel
+
+1. Skapa din konfigurationsfil (YAML). Du kan göra en via att använda
+[Pokémon Emerald options hemsida](../../../games/Pokemon%20Emerald/player-options).
+2. Följ de allmänna Archipelago instruktionerna för att
+[Generera ett spel](../../Archipelago/setup/en#generating-a-game).
+Detta kommer generera en fil för dig. Din patchfil kommer ha `.apemerald` som sitt filnamnstillägg.
+3. Öppna `ArchipelagoLauncher.exe`
+4. Välj "Open Patch" på vänstra sidan, och välj din patchfil.
+5. Om detta är första gången du patchar, så kommer du behöva välja var ditt ursprungliga ROM är.
+6. En patchad `.gba` fil kommer skapas på samma plats som patchfilen.
+7. Första gången du öppnar en patch med BizHawk-klienten, kommer du också behöva bekräfta var `EmuHawk.exe` filen är
+installerad i din BizHawk-mapp.
+
+Om du bara tänkt spela själv och du inte bryr dig om automatisk spårning eller ledtrådar, så kan du stanna här, stänga
+av klienten, och starta ditt patchade ROM med valfri emulator. Dock, för multvärldsfunktionen eller andra
+Archipelago-funktioner, fortsätt nedanför med BizHawk.
+
+## Anslut till en server
+
+Om du vanligtsvis öppnar en patchad fil så görs steg 1-5 automatiskt åt dig. Även om det är så, kom ihåg dessa steg
+ifall du till exempel behöver stänga ner och starta om något medans du spelar.
+
+1. Pokemon Emerald använder Archipelagos BizHawk-klient. Om klienten inte startat efter att du patchat ditt spel,
+så kan du bara öppna den igen från launchern.
+2. Dubbelkolla att EmuHawk faktiskt startat med den patchade ROM-filen.
+3. I EmuHawk, gå till `Tools > Lua Console`. Luakonsolen måste vara igång medans du spelar.
+4. I Luakonsolen, Tryck på `Script > Open Script…`.
+5. Leta reda på din Archipelago-mapp och i den öppna `data/lua/connector_bizhawk_generic.lua`.
+6. Emulatorn och klienten kommer så småningom ansluta till varandra. I BizHawk-klienten kommer du kunna see om allt är
+anslutet och att Pokemon Emerald är igenkänt.
+7. För att ansluta klienten till en server, skriv in din lobbyadress och port i textfältet t.ex.
+`archipelago.gg:38281`
+längst upp i din klient och tryck sen på "Connect".
+
+Du borde nu kunna ta emot och skicka föremål. Du behöver göra dom här stegen varje gång du vill ansluta igen. Det är
+helt okej att göra saker offline utan att behöva oroa sig; allt kommer att synkronisera när du ansluter till servern
+igen.
+
+## Automatisk Spårning
+
+Pokémon Emerald har en fullt fungerande spårare med stöd för automatisk spårning.
+
+1. Ladda ner [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest)
+och
+[PopTracker](https://github.com/black-sliver/PopTracker/releases).
+2. Placera tracker pack zip-filen i packs/ där du har PopTracker installerat.
+3. Öppna PopTracker, och välj Pokemon Emerald.
+4. För att automatiskt spåra, tryck på "AP" symbolen längst upp.
+5. Skriv in Archipelago-serverns uppgifter (Samma som du använde för att ansluta med klienten), "Slot"-namn samt
+lösenord.
diff --git a/worlds/pokemon_emerald/docs/warps.md b/worlds/pokemon_emerald/docs/warps.md
new file mode 100644
index 000000000000..671c6aee2179
--- /dev/null
+++ b/worlds/pokemon_emerald/docs/warps.md
@@ -0,0 +1,50 @@
+## Warps
+
+Quick note to start, you should not be defining or modifying encoded warps from this repository. They're encoded in the
+source code repository for the mod, and then assigned to regions in `data/regions/`. All warps in the game already exist
+within `extracted_data.json`, and all relevant warps are already placed in `data/regions/` (unless they were deleted
+accidentally).
+
+Many warps are actually two or three events acting as one logical warp. Doorways, for example, are often 2 tiles wide
+indoors but only 1 tile wide outdoors. Both indoor warps point to the outdoor warp, and the outdoor warp points to only
+one of the indoor warps. We want to describe warps logically in a way that retains information about individual warp
+events. That way a 2-tile-wide doorway doesnt look like a one-way warp next to an unrelated two-way warp, but if we want
+to randomize the destinations of those warps, we can still get back each individual id of the multi-tile warp.
+
+This is how warps are encoded:
+
+`{source_map}:{source_warp_ids}/{dest_map}:{dest_warp_ids}[!]`
+
+- `source_map`: The map the warp events are located in
+- `source_warp_ids`: The ids of all adjacent warp events in source_map which lead to the same destination (these must be
+in ascending order)
+- `dest_map`: The map of the warp event to which this one is connected
+- `dest_warp_ids`: The ids of the warp events in dest_map
+- `[!]`: If the warp expects to lead to a destination which does not lead back to it, add a ! to the end
+
+Example: `MAP_LAVARIDGE_TOWN_HOUSE:0,1/MAP_LAVARIDGE_TOWN:4`
+
+Example 2: `MAP_AQUA_HIDEOUT_B1F:14/MAP_AQUA_HIDEOUT_B1F:12!`
+
+Note: A warp must have its destination set to another warp event. However, that does not guarantee that the destination
+warp event will warp back to the source.
+
+Note 2: Some warps _only_ act as destinations and cannot actually be interacted with by the player as sources. These are
+usually places you fall from a hole above. At the time of writing, these are actually not accounted for, but there are
+no instances where it changes logical access.
+
+Note 3: Some warp destinations go to the map `MAP_DYNAMIC` and have a special warp id. These edge cases are:
+
+- The Moving Truck
+- Terra Cave
+- Marine Cave
+- The Department Store Elevator
+- Secret Bases
+- The Trade Center
+- The Union Room
+- The Record Corner
+- 2P/4P Battle Colosseum
+
+Note 4: The trick house on Route 110 changes the warp destinations of its entrance and ending room as you progress
+through the puzzles, but the source code only sets the trick house up for the first puzzle, and I assume the destination
+gets overwritten at run time when certain flags are set.
diff --git a/worlds/pokemon_emerald/docs/wonder trades.md b/worlds/pokemon_emerald/docs/wonder trades.md
new file mode 100644
index 000000000000..1187e9edb627
--- /dev/null
+++ b/worlds/pokemon_emerald/docs/wonder trades.md
@@ -0,0 +1,103 @@
+# Wonder Trades
+
+Pokemon Emerald uses Archipelago's data storage to reproduce what the Pokemon series calls wonder trading. Wonder
+trading is meant as a sort of gacha game surprise trade where you give up one of your pokemon and at some point in the
+future you'll receive one in return from another player who decided to participate. In practice, small groups will be
+able to use it as a means of simple trading as well by coordinating when they participate.
+
+The goal of the implementation used by Pokemon Emerald is to allow players to interact with an NPC in-game to deposit
+and withdraw pokemon without having to touch their client. The client will automatically detect their state, look for
+available trades, and notify the player when they've received something.
+
+It's also intended to work for Pokemon games other than Emerald, should any other games decide to opt in and implement
+the feature into their clients.
+
+## Data Storage Format
+
+There is one wonder trade entry per team at `pokemon_wonder_trades_{team number}`.
+
+It should be a dict that looks something like this:
+
+```json
+{
+ "_lock": 0,
+ "0": [3, "{some json data}"],
+ "3": [2, "{some json data}"]
+}
+```
+
+### Lock
+
+`_lock` tells you whether you're allowed to try to modify the key. Its value should be either `0` to represent an
+unlocked state, or a timestamp represented by time since Epoch in ms (`int(time.time_ns() / 1000000)`).
+[See below](#preventing-race-conditions) for more info.
+
+### Non-lock Keys
+
+All other keys are just non-negative integers as strings. You can think of them as wonder trade slots. Pidgeon holes
+with a label. For consistency and ease of use, keep the keys between 0 and 255, and prefer the lowest number you can
+use. They ONLY act as names that can be easily written to and removed from.
+- You SHOULD NOT rely on those numbers being contiguous or starting at 0.
+- You SHOULD NOT rely on a "trade" residing at a single slot until it is removed.
+- You SHOULD NOT assume that the number has any significance to a player's slot, or trade order, or anything really.
+
+### Values
+
+The first entry in the tuple represents which slot put the pokemon up for trade. You could use this to display in your
+game or client who the trade came from, but its primary purpose is to discriminate entries you can take from those you
+can't. You don't want to send something to the server, see that the server has something to take, and then take your own
+pokemon right back.
+
+The JSON data should match the schema currently located at `data/trade_pokemon_schema.json`. It should be universally
+understandable by anything trying to interact with wonder trades. Of course, some Pokemon games include more data than
+others for a given pokemon, some games don't have species introduced in later generations, and some data is of a
+different format, has different values, or is even spelled differently. The hope is that translating to and from JSON is
+reasonable for any game (or at least any game likely to be integrated into AP), and you can easily tell from the JSON
+whether your game is capable of giving the pokemon to the player in-game.
+
+## Preventing Race Conditions
+
+This caused by far the most headache of implementing wonder trades. You should be very thorough in trying to prevent
+issues here.
+
+If you prefer more technical explanations, the Pokemon Emerald client has documented wonder trade functions. The rest of
+this section explains what problems are being solved and why the solutions work.
+
+The problem that needs solving is that your client needs to know what the value of the trade data is before it commits
+some sort of action. By design, multiple clients are writing to and removing from the same key in data storage, so if
+two clients try to interact and there's ambiguity in what the data looks like, it will cause issues of duplication and
+loss of data.
+
+For example, client 1 and client 2 both see a pokemon that they can take, so they copy the pokemon to their respective
+games, and both send a command to remove that pokemon from the data store. The first command works and removes the
+entry, which sends an update to both clients that there no longer exists a pokemon at that slot. And then the second
+command, which was already sent, tries to remove the same entry. At best, the data was duplicated, and at worst the
+server raises an exception or crashes.
+
+Thankfully, when you receive an update from the server that a storage value changed, it will tell you both the previous
+and current value. That's where the lock comes in. At a basic level, your client attempts to claim ownership of the key
+temporarily while it makes its modifications, and all other clients respect that claim by not interacting until the lock
+is released. You know you locked the key because the `SetReply` you receive for modifying the lock is the one that set
+it from an unlocked state to a locked state. When two clients try to lock at the same time, one will see an unlocked
+state move to a locked state, and the other will see an already locked state move to a locked state. You can identify
+whether a `SetReply` was triggered by your client's `Set` by attaching a uuid to the `Set` command, which will also be
+attached to the `SetReply`. See the Emerald client for an example.
+
+Which brings us to problem 2, which is the scenario where a client crashes or closes before unlocking the key. One rogue
+client might prevent all other clients from ever interacting with wonder trading again.
+
+So for this reason, the lock is a timestamp, and the key is considered "locked" if that timestamp is less than 5 seconds
+in the past. If a client dies after locking, its lock will expire, and other clients will be able to make modifications.
+Setting the lock to 0 is the canonical way of marking it as unlocked, but it's not a special case really. It's
+equivalent to marking the key as last locked in 1970.
+
+Which brings us to problem 3. Multiple clients which want to obtain the lock can only check whether the lock is
+obtainable by refreshing the current lock's timestamp. So two clients trying to secure a lock made by a dead client may
+trade back and forth, updating the lock to see if it is expired yet, seeing that it is not, and then waiting 5 seconds
+while the other client does the same thing, which causes the lock to again be less than 5 seconds old.
+
+Using a cooldown period longer than the time to expire only increases the minimum number of clients that can trigger
+this cycle. Instead, the solution is to double your cooldown every time you bounce off an expired lock (and reset it
+once you acquire it). Eventually the amount of time every client is waiting will be enough to create a gap large enough
+for one client to consider the lock expired, and it will acquire the lock, make its changes, and set the lock state to
+definitively unlocked, which will let the next client claim it, and so on.
diff --git a/worlds/pokemon_emerald/items.py b/worlds/pokemon_emerald/items.py
new file mode 100644
index 000000000000..436db771d396
--- /dev/null
+++ b/worlds/pokemon_emerald/items.py
@@ -0,0 +1,77 @@
+"""
+Classes and functions related to AP items for Pokemon Emerald
+"""
+from typing import Dict, FrozenSet, Optional
+
+from BaseClasses import Item, ItemClassification
+
+from .data import BASE_OFFSET, data
+
+
+class PokemonEmeraldItem(Item):
+ game: str = "Pokemon Emerald"
+ tags: FrozenSet[str]
+
+ def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int) -> None:
+ super().__init__(name, classification, code, player)
+
+ if code is None:
+ self.tags = frozenset(["Event"])
+ else:
+ self.tags = data.items[reverse_offset_item_value(code)].tags
+
+
+def offset_item_value(item_value: int) -> int:
+ """
+ Returns the AP item id (code) for a given item value
+ """
+ return item_value + BASE_OFFSET
+
+
+def reverse_offset_item_value(item_id: int) -> int:
+ """
+ Returns the item value for a given AP item id (code)
+ """
+ return item_id - BASE_OFFSET
+
+
+def create_item_label_to_code_map() -> Dict[str, int]:
+ """
+ Creates a map from item labels to their AP item id (code)
+ """
+ label_to_code_map: Dict[str, int] = {}
+ for item_value, attributes in data.items.items():
+ label_to_code_map[attributes.label] = offset_item_value(item_value)
+
+ return label_to_code_map
+
+
+ITEM_GROUPS = {
+ "Badges": {
+ "Stone Badge", "Knuckle Badge",
+ "Dynamo Badge", "Heat Badge",
+ "Balance Badge", "Feather Badge",
+ "Mind Badge", "Rain Badge",
+ },
+ "HMs": {
+ "HM01 Cut", "HM02 Fly",
+ "HM03 Surf", "HM04 Strength",
+ "HM05 Flash", "HM06 Rock Smash",
+ "HM07 Waterfall", "HM08 Dive",
+ },
+ "HM01": {"HM01 Cut"},
+ "HM02": {"HM02 Fly"},
+ "HM03": {"HM03 Surf"},
+ "HM04": {"HM04 Strength"},
+ "HM05": {"HM05 Flash"},
+ "HM06": {"HM06 Rock Smash"},
+ "HM07": {"HM07 Waterfall"},
+ "HM08": {"HM08 Dive"},
+}
+
+
+def get_item_classification(item_code: int) -> ItemClassification:
+ """
+ Returns the item classification for a given AP item id (code)
+ """
+ return data.items[reverse_offset_item_value(item_code)].classification
diff --git a/worlds/pokemon_emerald/locations.py b/worlds/pokemon_emerald/locations.py
new file mode 100644
index 000000000000..9123690bead7
--- /dev/null
+++ b/worlds/pokemon_emerald/locations.py
@@ -0,0 +1,226 @@
+"""
+Classes and functions related to AP locations for Pokemon Emerald
+"""
+from typing import TYPE_CHECKING, Dict, Optional, FrozenSet, Iterable
+
+from BaseClasses import Location, Region
+
+from .data import BASE_OFFSET, NATIONAL_ID_TO_SPECIES_ID, POKEDEX_OFFSET, data
+from .items import offset_item_value
+
+if TYPE_CHECKING:
+ from . import PokemonEmeraldWorld
+
+
+LOCATION_GROUPS = {
+ "Badges": {
+ "Rustboro Gym - Stone Badge",
+ "Dewford Gym - Knuckle Badge",
+ "Mauville Gym - Dynamo Badge",
+ "Lavaridge Gym - Heat Badge",
+ "Petalburg Gym - Balance Badge",
+ "Fortree Gym - Feather Badge",
+ "Mossdeep Gym - Mind Badge",
+ "Sootopolis Gym - Rain Badge",
+ },
+ "Gym TMs": {
+ "Rustboro Gym - TM39 from Roxanne",
+ "Dewford Gym - TM08 from Brawly",
+ "Mauville Gym - TM34 from Wattson",
+ "Lavaridge Gym - TM50 from Flannery",
+ "Petalburg Gym - TM42 from Norman",
+ "Fortree Gym - TM40 from Winona",
+ "Mossdeep Gym - TM04 from Tate and Liza",
+ "Sootopolis Gym - TM03 from Juan",
+ },
+ "Trick House": {
+ "Trick House Puzzle 1 - Item",
+ "Trick House Puzzle 2 - Item 1",
+ "Trick House Puzzle 2 - Item 2",
+ "Trick House Puzzle 3 - Item 1",
+ "Trick House Puzzle 3 - Item 2",
+ "Trick House Puzzle 4 - Item",
+ "Trick House Puzzle 6 - Item",
+ "Trick House Puzzle 7 - Item",
+ "Trick House Puzzle 8 - Item",
+ "Trick House Puzzle 1 - Reward",
+ "Trick House Puzzle 2 - Reward",
+ "Trick House Puzzle 3 - Reward",
+ "Trick House Puzzle 4 - Reward",
+ "Trick House Puzzle 5 - Reward",
+ "Trick House Puzzle 6 - Reward",
+ "Trick House Puzzle 7 - Reward",
+ }
+}
+
+
+VISITED_EVENT_NAME_TO_ID = {
+ "EVENT_VISITED_LITTLEROOT_TOWN": 0,
+ "EVENT_VISITED_OLDALE_TOWN": 1,
+ "EVENT_VISITED_PETALBURG_CITY": 2,
+ "EVENT_VISITED_RUSTBORO_CITY": 3,
+ "EVENT_VISITED_DEWFORD_TOWN": 4,
+ "EVENT_VISITED_SLATEPORT_CITY": 5,
+ "EVENT_VISITED_MAUVILLE_CITY": 6,
+ "EVENT_VISITED_VERDANTURF_TOWN": 7,
+ "EVENT_VISITED_FALLARBOR_TOWN": 8,
+ "EVENT_VISITED_LAVARIDGE_TOWN": 9,
+ "EVENT_VISITED_FORTREE_CITY": 10,
+ "EVENT_VISITED_LILYCOVE_CITY": 11,
+ "EVENT_VISITED_MOSSDEEP_CITY": 12,
+ "EVENT_VISITED_SOOTOPOLIS_CITY": 13,
+ "EVENT_VISITED_PACIFIDLOG_TOWN": 14,
+ "EVENT_VISITED_EVER_GRANDE_CITY": 15,
+ "EVENT_VISITED_BATTLE_FRONTIER": 16,
+ "EVENT_VISITED_SOUTHERN_ISLAND": 17,
+}
+
+
+class PokemonEmeraldLocation(Location):
+ game: str = "Pokemon Emerald"
+ item_address: Optional[int]
+ default_item_code: Optional[int]
+ tags: FrozenSet[str]
+
+ def __init__(
+ self,
+ player: int,
+ name: str,
+ address: Optional[int],
+ parent: Optional[Region] = None,
+ item_address: Optional[int] = None,
+ default_item_value: Optional[int] = None,
+ tags: FrozenSet[str] = frozenset()) -> None:
+ super().__init__(player, name, address, parent)
+ self.default_item_code = None if default_item_value is None else offset_item_value(default_item_value)
+ self.item_address = item_address
+ self.tags = tags
+
+
+def offset_flag(flag: int) -> int:
+ """
+ Returns the AP location id (address) for a given flag
+ """
+ if flag is None:
+ return None
+ return flag + BASE_OFFSET
+
+
+def reverse_offset_flag(location_id: int) -> int:
+ """
+ Returns the flag id for a given AP location id (address)
+ """
+ if location_id is None:
+ return None
+ return location_id - BASE_OFFSET
+
+
+def create_locations_with_tags(world: "PokemonEmeraldWorld", regions: Dict[str, Region], tags: Iterable[str]) -> None:
+ """
+ Iterates through region data and adds locations to the multiworld if
+ those locations include any of the provided tags.
+ """
+ tags = set(tags)
+
+ for region_name, region_data in data.regions.items():
+ region = regions[region_name]
+ filtered_locations = [loc for loc in region_data.locations if len(tags & data.locations[loc].tags) > 0]
+
+ for location_name in filtered_locations:
+ location_data = data.locations[location_name]
+
+ location_id = offset_flag(location_data.flag)
+ if location_data.flag == 0: # Dexsanity location
+ national_dex_id = int(location_name[-3:]) # Location names are formatted POKEDEX_REWARD_###
+
+ # Don't create this pokedex location if player can't find it in the wild
+ if NATIONAL_ID_TO_SPECIES_ID[national_dex_id] in world.blacklisted_wilds:
+ continue
+
+ location_id += POKEDEX_OFFSET + national_dex_id
+
+ location = PokemonEmeraldLocation(
+ world.player,
+ location_data.label,
+ location_id,
+ region,
+ location_data.address,
+ location_data.default_item,
+ location_data.tags
+ )
+ region.locations.append(location)
+
+
+def create_location_label_to_id_map() -> Dict[str, int]:
+ """
+ Creates a map from location labels to their AP location id (address)
+ """
+ label_to_id_map: Dict[str, int] = {}
+ for region_data in data.regions.values():
+ for location_name in region_data.locations:
+ location_data = data.locations[location_name]
+
+ if location_data.flag == 0:
+ label_to_id_map[location_data.label] = BASE_OFFSET + POKEDEX_OFFSET + int(location_data.name[15:])
+ else:
+ label_to_id_map[location_data.label] = offset_flag(location_data.flag)
+
+ return label_to_id_map
+
+
+def set_free_fly(world: "PokemonEmeraldWorld") -> None:
+ # Set our free fly location
+ # If not enabled, set it to Littleroot Town by default
+ fly_location_name = "EVENT_VISITED_LITTLEROOT_TOWN"
+ if world.options.free_fly_location:
+ fly_location_name = world.random.choice([
+ "EVENT_VISITED_SLATEPORT_CITY",
+ "EVENT_VISITED_MAUVILLE_CITY",
+ "EVENT_VISITED_VERDANTURF_TOWN",
+ "EVENT_VISITED_FALLARBOR_TOWN",
+ "EVENT_VISITED_LAVARIDGE_TOWN",
+ "EVENT_VISITED_FORTREE_CITY",
+ "EVENT_VISITED_LILYCOVE_CITY",
+ "EVENT_VISITED_MOSSDEEP_CITY",
+ "EVENT_VISITED_SOOTOPOLIS_CITY",
+ "EVENT_VISITED_EVER_GRANDE_CITY",
+ ])
+
+ world.free_fly_location_id = VISITED_EVENT_NAME_TO_ID[fly_location_name]
+
+ free_fly_location_location = world.multiworld.get_location("FREE_FLY_LOCATION", world.player)
+ free_fly_location_location.item = None
+ free_fly_location_location.place_locked_item(world.create_event(fly_location_name))
+
+
+def set_legendary_cave_entrances(world: "PokemonEmeraldWorld") -> None:
+ # Set Marine Cave and Terra Cave entrances
+ terra_cave_location_name = world.random.choice([
+ "TERRA_CAVE_ROUTE_114_1",
+ "TERRA_CAVE_ROUTE_114_2",
+ "TERRA_CAVE_ROUTE_115_1",
+ "TERRA_CAVE_ROUTE_115_2",
+ "TERRA_CAVE_ROUTE_116_1",
+ "TERRA_CAVE_ROUTE_116_2",
+ "TERRA_CAVE_ROUTE_118_1",
+ "TERRA_CAVE_ROUTE_118_2",
+ ])
+
+ terra_cave_location_location = world.multiworld.get_location("TERRA_CAVE_LOCATION", world.player)
+ terra_cave_location_location.item = None
+ terra_cave_location_location.place_locked_item(world.create_event(terra_cave_location_name))
+
+ marine_cave_location_name = world.random.choice([
+ "MARINE_CAVE_ROUTE_105_1",
+ "MARINE_CAVE_ROUTE_105_2",
+ "MARINE_CAVE_ROUTE_125_1",
+ "MARINE_CAVE_ROUTE_125_2",
+ "MARINE_CAVE_ROUTE_127_1",
+ "MARINE_CAVE_ROUTE_127_2",
+ "MARINE_CAVE_ROUTE_129_1",
+ # "MARINE_CAVE_ROUTE_129_2", # Cave ID too high for internal data type, needs patch update
+ ])
+
+ marine_cave_location_location = world.multiworld.get_location("MARINE_CAVE_LOCATION", world.player)
+ marine_cave_location_location.item = None
+ marine_cave_location_location.place_locked_item(world.create_event(marine_cave_location_name))
diff --git a/worlds/pokemon_emerald/opponents.py b/worlds/pokemon_emerald/opponents.py
new file mode 100644
index 000000000000..09e947546d7c
--- /dev/null
+++ b/worlds/pokemon_emerald/opponents.py
@@ -0,0 +1,116 @@
+from typing import TYPE_CHECKING, Dict, List, Set
+
+from .data import NUM_REAL_SPECIES, UNEVOLVED_POKEMON, TrainerPokemonData, data
+from .options import RandomizeTrainerParties
+from .pokemon import filter_species_by_nearby_bst
+from .util import int_to_bool_array
+
+if TYPE_CHECKING:
+ from . import PokemonEmeraldWorld
+
+
+def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None:
+ if world.options.trainer_parties == RandomizeTrainerParties.option_vanilla:
+ return
+
+ from collections import defaultdict
+
+ should_match_bst = world.options.trainer_parties in {
+ RandomizeTrainerParties.option_match_base_stats,
+ RandomizeTrainerParties.option_match_base_stats_and_type,
+ }
+ should_match_type = world.options.trainer_parties in {
+ RandomizeTrainerParties.option_match_type,
+ RandomizeTrainerParties.option_match_base_stats_and_type,
+ }
+
+ per_species_tmhm_moves: Dict[int, List[int]] = {}
+
+ for trainer in world.modified_trainers:
+ new_party = []
+ for pokemon in trainer.party.pokemon:
+ original_species = data.species[pokemon.species_id]
+
+ # Construct progressive tiers of blacklists that can be peeled back if they
+ # collectively cover too much of the pokedex. A lower index in `blacklists`
+ # indicates a more important set of species to avoid. Entries at `0` will
+ # always be blacklisted.
+ blacklists: Dict[int, List[Set[int]]] = defaultdict(list)
+
+ # Blacklist unevolved species
+ if pokemon.level >= world.options.force_fully_evolved:
+ blacklists[0].append(UNEVOLVED_POKEMON)
+
+ # Blacklist from player options
+ blacklists[2].append(world.blacklisted_opponent_pokemon)
+
+ # Type matching blacklist
+ if should_match_type:
+ blacklists[3].append({
+ species.species_id
+ for species in world.modified_species.values()
+ if not bool(set(species.types) & set(original_species.types))
+ })
+
+ merged_blacklist: Set[int] = set()
+ for max_priority in reversed(sorted(blacklists.keys())):
+ merged_blacklist = set()
+ for priority in blacklists.keys():
+ if priority <= max_priority:
+ for blacklist in blacklists[priority]:
+ merged_blacklist |= blacklist
+
+ if len(merged_blacklist) < NUM_REAL_SPECIES:
+ break
+ else:
+ raise RuntimeError("This should never happen")
+
+ candidates = [
+ species
+ for species in world.modified_species.values()
+ if species.species_id not in merged_blacklist
+ ]
+
+ if should_match_bst:
+ candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
+
+ new_species = world.random.choice(candidates)
+
+ if new_species.species_id not in per_species_tmhm_moves:
+ per_species_tmhm_moves[new_species.species_id] = sorted({
+ world.modified_tmhm_moves[i]
+ for i, is_compatible in enumerate(int_to_bool_array(new_species.tm_hm_compatibility))
+ if is_compatible and world.modified_tmhm_moves[i] not in world.blacklisted_moves
+ })
+
+ # TMs and HMs compatible with the species
+ tm_hm_movepool = per_species_tmhm_moves[new_species.species_id]
+
+ # Moves the pokemon could have learned by now
+ level_up_movepool = sorted({
+ move.move_id
+ for move in new_species.learnset
+ if move.move_id != 0 and move.level <= pokemon.level
+ })
+
+ if len(level_up_movepool) < 4:
+ level_up_moves = [level_up_movepool[i] if i < len(level_up_movepool) else 0 for i in range(4)]
+ else:
+ level_up_moves = world.random.sample(level_up_movepool, 4)
+
+ if len(tm_hm_movepool) < 4:
+ hm_moves = list(reversed(list(tm_hm_movepool[i] if i < len(tm_hm_movepool) else 0 for i in range(4))))
+ else:
+ hm_moves = world.random.sample(tm_hm_movepool, 4)
+
+ # 25% chance to pick a move from TMs or HMs
+ new_moves = (
+ hm_moves[0] if world.random.random() < 0.25 else level_up_moves[0],
+ hm_moves[1] if world.random.random() < 0.25 else level_up_moves[1],
+ hm_moves[2] if world.random.random() < 0.25 else level_up_moves[2],
+ hm_moves[3] if world.random.random() < 0.25 else level_up_moves[3]
+ )
+
+ new_party.append(TrainerPokemonData(new_species.species_id, pokemon.level, new_moves))
+
+ trainer.party.pokemon = new_party
diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py
new file mode 100644
index 000000000000..e05b5d96ac74
--- /dev/null
+++ b/worlds/pokemon_emerald/options.py
@@ -0,0 +1,879 @@
+"""
+Option definitions for Pokemon Emerald
+"""
+from dataclasses import dataclass
+
+from Options import (Choice, DeathLink, DefaultOnToggle, OptionSet, NamedRange, Range, Toggle, FreeText,
+ PerGameCommonOptions)
+
+from .data import data
+
+
+class Goal(Choice):
+ """
+ Determines what your goal is to consider the game beaten.
+
+ - Champion: Become the champion and enter the hall of fame
+ - Steven: Defeat Steven in Meteor Falls
+ - Norman: Defeat Norman in Petalburg Gym
+ - Legendary Hunt: Defeat or catch legendary pokemon (or whatever was randomized into their encounters)
+ """
+ display_name = "Goal"
+ default = 0
+ option_champion = 0
+ option_steven = 1
+ option_norman = 2
+ option_legendary_hunt = 3
+
+
+class RandomizeBadges(Choice):
+ """
+ Adds Badges to the pool.
+
+ - Vanilla: Gym leaders give their own badge
+ - Shuffle: Gym leaders give a random badge
+ - Completely Random: Badges can be found anywhere
+ """
+ display_name = "Randomize Badges"
+ default = 2
+ option_vanilla = 0
+ option_shuffle = 1
+ option_completely_random = 2
+
+
+class RandomizeHms(Choice):
+ """
+ Adds HMs to the pool.
+
+ - Vanilla: HMs are at their vanilla locations
+ - Shuffle: HMs are shuffled among vanilla HM locations
+ - Completely Random: HMs can be found anywhere
+ """
+ display_name = "Randomize HMs"
+ default = 2
+ option_vanilla = 0
+ option_shuffle = 1
+ option_completely_random = 2
+
+
+class RandomizeKeyItems(DefaultOnToggle):
+ """
+ Adds most key items to the pool.
+
+ These are usually required to unlock a location or region (e.g. Devon Scope, Letter, Basement Key).
+ """
+ display_name = "Randomize Key Items"
+
+
+class RandomizeBikes(Toggle):
+ """
+ Adds the Mach Bike and Acro Bike to the pool.
+ """
+ display_name = "Randomize Bikes"
+
+
+class RandomizeEventTickets(Toggle):
+ """
+ Adds the event tickets to the pool, which let you access legendaries by sailing from Lilycove.
+ """
+ display_name = "Randomize Event Tickets"
+
+
+class RandomizeRods(Toggle):
+ """
+ Adds fishing rods to the pool.
+ """
+ display_name = "Randomize Fishing Rods"
+
+
+class RandomizeOverworldItems(DefaultOnToggle):
+ """
+ Adds items on the ground with a Pokeball sprite to the pool.
+ """
+ display_name = "Randomize Overworld Items"
+
+
+class RandomizeHiddenItems(Toggle):
+ """
+ Adds hidden items to the pool.
+ """
+ display_name = "Randomize Hidden Items"
+
+
+class RandomizeNpcGifts(Toggle):
+ """
+ Adds most gifts received from NPCs to the pool (not including key items or HMs).
+ """
+ display_name = "Randomize NPC Gifts"
+
+
+class RandomizeBerryTrees(Toggle):
+ """
+ Adds berry trees to the pool. Empty soil patches are converted to locations and contribute Sitrus Berries to the pool.
+ """
+ display_name = "Randomize Berry Trees"
+
+
+class Dexsanity(Toggle):
+ """
+ Adding a "caught" pokedex entry gives you an item (catching, evolving, trading, etc.). Only wild encounters are considered logical access to a species.
+
+ Blacklisting wild encounters removes the dexsanity location.
+
+ Defeating gym leaders provides dex info, allowing you to see where on the map you can catch species you need.
+
+ Each pokedex entry adds a Poke Ball, Great Ball, or Ultra Ball to the pool.
+ """
+ display_name = "Dexsanity"
+
+
+class Trainersanity(Toggle):
+ """
+ Defeating a trainer gives you an item.
+
+ Trainers are no longer missable. Trainers no longer give you money for winning. Each trainer adds a valuable item (Nugget, Stardust, etc.) to the pool.
+ """
+ display_name = "Trainersanity"
+
+
+class ItemPoolType(Choice):
+ """
+ Determines which non-progression items get put into the item pool.
+
+ - Shuffled: Item pool consists of shuffled vanilla items
+ - Diverse Balanced: Item pool consists of random items approximately proportioned according to what they're replacing
+ - Diverse: Item pool consists of uniformly random (non-unique) items
+ """
+ display_name = "Item Pool Type"
+ default = 0
+ option_shuffled = 0
+ option_diverse_balanced = 1
+ option_diverse = 2
+
+
+class HiddenItemsRequireItemfinder(DefaultOnToggle):
+ """
+ The Itemfinder is logically required to pick up hidden items.
+ """
+ display_name = "Require Itemfinder"
+
+
+class DarkCavesRequireFlash(Choice):
+ """
+ Determines whether HM05 Flash is logically required to navigate a dark cave.
+ """
+ display_name = "Require Flash"
+ default = 3
+ option_neither = 0
+ option_only_granite_cave = 1
+ option_only_victory_road = 2
+ option_both = 3
+
+
+class EliteFourRequirement(Choice):
+ """
+ Sets the requirements to challenge the elite four.
+
+ - Badges: Obtain some number of badges
+ - Gyms: Defeat some number of gyms
+ """
+ display_name = "Elite Four Requirement"
+ default = 0
+ option_badges = 0
+ option_gyms = 1
+
+
+class EliteFourCount(Range):
+ """
+ Sets the number of badges/gyms required to challenge the elite four.
+ """
+ display_name = "Elite Four Count"
+ range_start = 0
+ range_end = 8
+ default = 8
+
+
+class NormanRequirement(Choice):
+ """
+ Sets the requirements to challenge the Petalburg Gym.
+
+ - Badges: Obtain some number of badges
+ - Gyms: Defeat some number of gym leaders
+ """
+ display_name = "Norman Requirement"
+ default = 0
+ option_badges = 0
+ option_gyms = 1
+
+
+class NormanCount(Range):
+ """
+ Sets the number of badges/gyms required to challenge the Petalburg Gym.
+ """
+ display_name = "Norman Count"
+ range_start = 0
+ range_end = 7
+ default = 4
+
+
+class LegendaryHuntCatch(Toggle):
+ """
+ Sets whether legendaries need to be caught to satisfy the Legendary Hunt win condition.
+
+ Defeated legendaries can be respawned by defeating the Elite 4.
+ """
+ display_name = "Legendary Hunt Requires Catching"
+
+
+class LegendaryHuntCount(Range):
+ """
+ Sets the number of legendaries that must be caught/defeated for the Legendary Hunt goal.
+ """
+ display_name = "Legendary Hunt Count"
+ range_start = 1
+ range_end = 12
+ default = 3
+
+
+class AllowedLegendaryHuntEncounters(OptionSet):
+ """
+ Sets which legendary encounters can contribute to the Legendary Hunt goal.
+
+ Latias will always be at Southern Island. Latios will always be the roamer. The TV broadcast describing the roamer gives you "seen" info for Latios.
+
+ The braille puzzle in Sealed Chamber gives you "seen" info for Wailord and Relicanth. The move tutor in Fortree City always teaches Dig.
+ """
+ display_name = "Allowed Legendary Hunt Encounters"
+ valid_keys = [
+ "Groudon",
+ "Kyogre",
+ "Rayquaza",
+ "Latios",
+ "Latias",
+ "Regirock",
+ "Registeel",
+ "Regice",
+ "Ho-Oh",
+ "Lugia",
+ "Deoxys",
+ "Mew",
+ ]
+ default = valid_keys.copy()
+
+
+class RandomizeWildPokemon(Choice):
+ """
+ Randomizes wild pokemon encounters (grass, caves, water, fishing).
+
+ - Vanilla: Wild encounters are unchanged
+ - Match Base Stats: Wild pokemon are replaced with species with approximately the same bst
+ - Match Type: Wild pokemon are replaced with species that share a type with the original
+ - Match Base Stats and Type: Apply both Match Base Stats and Match Type
+ - Completely Random: There are no restrictions
+ """
+ display_name = "Randomize Wild Pokemon"
+ default = 0
+ option_vanilla = 0
+ option_match_base_stats = 1
+ option_match_type = 2
+ option_match_base_stats_and_type = 3
+ option_completely_random = 4
+
+
+class WildEncounterBlacklist(OptionSet):
+ """
+ Prevents listed species from appearing in the wild when wild encounters are randomized.
+
+ May be overridden if enforcing other restrictions in combination with this blacklist is impossible.
+
+ Use "_Legendaries" as a shortcut for all legendary pokemon.
+ """
+ display_name = "Wild Encounter Blacklist"
+ valid_keys = ["_Legendaries"] + sorted([species.label for species in data.species.values()])
+
+
+class RandomizeStarters(Choice):
+ """
+ Randomizes the starter pokemon in Professor Birch's bag.
+
+ - Vanilla: Starters are unchanged
+ - Match Base Stats: Starters are replaced with species with approximately the same bst
+ - Match Type: Starters are replaced with species that share a type with the original
+ - Match Base Stats and Type: Apply both Match Base Stats and Match Type
+ - Completely Random: There are no restrictions
+ """
+ display_name = "Randomize Starters"
+ default = 0
+ option_vanilla = 0
+ option_match_base_stats = 1
+ option_match_type = 2
+ option_match_base_stats_and_type = 3
+ option_completely_random = 4
+
+
+class StarterBlacklist(OptionSet):
+ """
+ Prevents listed species from appearing as starters when starters are randomized.
+
+ May be overridden if enforcing other restrictions in combination with this blacklist is impossible.
+
+ Use "_Legendaries" as a shortcut for all legendary pokemon.
+ """
+ display_name = "Starter Blacklist"
+ valid_keys = ["_Legendaries"] + sorted([species.label for species in data.species.values()])
+
+
+class RandomizeTrainerParties(Choice):
+ """
+ Randomizes the parties of all trainers.
+
+ - Vanilla: Parties are unchanged
+ - Match Base Stats: Trainer pokemon are replaced with species with approximately the same bst
+ - Match Type: Trainer pokemon are replaced with species that share a type with the original
+ - Match Base Stats and Type: Apply both Match Base Stats and Match Type
+ - Completely Random: There are no restrictions
+ """
+ display_name = "Randomize Trainer Parties"
+ default = 0
+ option_vanilla = 0
+ option_match_base_stats = 1
+ option_match_type = 2
+ option_match_base_stats_and_type = 3
+ option_completely_random = 4
+
+
+class TrainerPartyBlacklist(OptionSet):
+ """
+ Prevents listed species from appearing in opponent trainers' parties if opponent parties are randomized.
+
+ May be overridden if enforcing other restrictions in combination with this blacklist is impossible.
+
+ Use "_Legendaries" as a shortcut for all legendary pokemon.
+ """
+ display_name = "Trainer Party Blacklist"
+ valid_keys = ["_Legendaries"] + sorted([species.label for species in data.species.values()])
+
+
+class ForceFullyEvolved(Range):
+ """
+ When an opponent uses a pokemon of the specified level or higher, restricts the species to only fully evolved pokemon.
+ """
+ display_name = "Force Fully Evolved"
+ range_start = 1
+ range_end = 100
+ default = 100
+
+
+class RandomizeLegendaryEncounters(Choice):
+ """
+ Randomizes legendary encounters (Rayquaza, Regice, Latias, etc.). The roamer will always be Latios during legendary hunts.
+
+ - Vanilla: Legendary encounters are unchanged
+ - Shuffle: Legendary encounters are shuffled between each other
+ - Match Base Stats: Legendary encounters are replaced with species with approximately the same bst
+ - Match Type: Legendary encounters are replaced with species that share a type with the original
+ - Match Base Stats and Type: Apply both Match Base Stats and Match Type
+ - Completely Random: There are no restrictions
+ """
+ display_name = "Randomize Legendary Encounters"
+ default = 0
+ option_vanilla = 0
+ option_shuffle = 1
+ option_match_base_stats = 2
+ option_match_type = 3
+ option_match_base_stats_and_type = 4
+ option_completely_random = 5
+
+
+class RandomizeMiscPokemon(Choice):
+ """
+ Randomizes non-legendary static encounters. May grow to include other pokemon like trades or gifts.
+
+ - Vanilla: Species are unchanged
+ - Shuffle: Species are shuffled between each other
+ - Match Base Stats: Species are replaced with species with approximately the same bst
+ - Match Type: Species are replaced with species that share a type with the original
+ - Match Base Stats and Type: Apply both Match Base Stats and Match Type
+ - Completely Random: There are no restrictions
+ """
+ display_name = "Randomize Misc Pokemon"
+ default = 0
+ option_vanilla = 0
+ option_shuffle = 1
+ option_match_base_stats = 2
+ option_match_type = 3
+ option_match_base_stats_and_type = 4
+ option_completely_random = 5
+
+
+class RandomizeTypes(Choice):
+ """
+ Randomizes the type(s) of every pokemon. Each species will have the same number of types.
+
+ - Vanilla: Types are unchanged
+ - Shuffle: Types are shuffled globally for all species (e.g. every Water-type pokemon becomes Fire-type)
+ - Completely Random: Each species has its type(s) randomized
+ - Follow Evolutions: Types are randomized per evolution line instead of per species
+ """
+ display_name = "Randomize Types"
+ default = 0
+ option_vanilla = 0
+ option_shuffle = 1
+ option_completely_random = 2
+ option_follow_evolutions = 3
+
+
+class RandomizeAbilities(Choice):
+ """
+ Randomizes abilities of every species. Each species will have the same number of abilities.
+
+ - Vanilla: Abilities are unchanged
+ - Completely Random: Each species has its abilities randomized
+ - Follow Evolutions: Abilities are randomized, but if a pokemon would normally retain its ability when evolving, the random ability will also be retained
+ """
+ display_name = "Randomize Abilities"
+ default = 0
+ option_vanilla = 0
+ option_completely_random = 1
+ option_follow_evolutions = 2
+
+
+class AbilityBlacklist(OptionSet):
+ """
+ Prevent species from being given these abilities.
+
+ Has no effect if abilities are not randomized.
+ """
+ display_name = "Ability Blacklist"
+ valid_keys = sorted([ability.label for ability in data.abilities])
+
+
+class LevelUpMoves(Choice):
+ """
+ Randomizes the moves a pokemon learns when they reach a level where they would learn a move. Your starter is guaranteed to have a usable damaging move.
+
+ - Vanilla: Learnset is unchanged
+ - Randomized: Moves are randomized
+ - Start with Four Moves: Moves are randomized and all Pokemon know 4 moves at level 1
+ """
+ display_name = "Level Up Moves"
+ default = 0
+ option_vanilla = 0
+ option_randomized = 1
+ option_start_with_four_moves = 2
+
+
+class MoveMatchTypeBias(Range):
+ """
+ Sets the probability that a learned move will be forced match one of the types of a pokemon.
+
+ If a move is not forced to match type, it will roll for Normal type bias.
+ """
+ display_name = "Move Match Type Bias"
+ range_start = 0
+ range_end = 100
+ default = 0
+
+
+class MoveNormalTypeBias(Range):
+ """
+ After it has been decided that a move will not be forced to match types, sets the probability that a learned move will be forced to be the Normal type.
+
+ If a move is not forced to be Normal, it will be completely random.
+ """
+ display_name = "Move Normal Type Bias"
+ range_start = 0
+ range_end = 100
+ default = 0
+
+
+class MoveBlacklist(OptionSet):
+ """
+ Prevents species from learning these moves via learnsets, TMs, and move tutors.
+
+ HM moves are already banned.
+ """
+ display_name = "Move Blacklist"
+ valid_keys = sorted(data.move_labels.keys())
+
+
+class HmCompatibility(NamedRange):
+ """
+ Sets the percent chance that a given HM is compatible with a species.
+
+ Some opponents like gym leaders are allowed to use HMs. This option can affect the moves they know.
+ """
+ display_name = "HM Compatibility"
+ default = -1
+ range_start = 50
+ range_end = 100
+ special_range_names = {
+ "vanilla": -1,
+ "full": 100,
+ }
+
+
+class TmTutorCompatibility(NamedRange):
+ """
+ Sets the percent chance that a given TM or move tutor is compatible with a species.
+
+ Some opponents like gym leaders are allowed to use TMs. This option can affect the moves they know.
+ """
+ display_name = "TM/Tutor Compatibility"
+ default = -1
+ range_start = 0
+ range_end = 100
+ special_range_names = {
+ "vanilla": -1,
+ "full": 100,
+ }
+
+
+class TmTutorMoves(Toggle):
+ """
+ Randomizes the moves taught by TMs and move tutors.
+
+ Some opponents like gym leaders are allowed to use TMs. This option can affect the moves they know.
+ """
+ display_name = "TM/Tutor Moves"
+
+
+class ReusableTmsTutors(Toggle):
+ """
+ Sets TMs to not break after use (they remain sellable). Sets move tutors to infinite use.
+ """
+ display_name = "Reusable TMs and Tutors"
+
+
+class MinCatchRate(Range):
+ """
+ Sets the minimum catch rate a pokemon can have. Any pokemon with a catch rate below this floor will have it raised to this value.
+
+ Legendaries are often in the single digits
+ Fully evolved pokemon are often double digits
+ Pidgey is 255
+ """
+ display_name = "Minimum Catch Rate"
+ range_start = 3
+ range_end = 255
+ default = 3
+
+
+class GuaranteedCatch(Toggle):
+ """
+ Every throw is guaranteed to catch a wild pokemon.
+ """
+ display_name = "Guaranteed Catch"
+
+
+class NormalizeEncounterRates(Toggle):
+ """
+ Make every slot on an encounter table approximately equally likely.
+
+ This does NOT mean each species is equally likely. In the vanilla game, each species may occupy more than one slot, and slots vary in probability.
+
+ Species will still occupy the same slots as vanilla, but the slots will be equally weighted. The minimum encounter rate will be 8% (higher in water).
+ """
+ display_name = "Normalize Encounter Rates"
+
+
+class ExpModifier(Range):
+ """
+ Multiplies gained experience by a percentage.
+
+ 100 is default
+ 50 is half
+ 200 is double
+ etc.
+ """
+ display_name = "Exp Modifier"
+ range_start = 0
+ range_end = 1000
+ default = 100
+
+
+class BlindTrainers(Toggle):
+ """
+ Trainers will not start a battle with you unless you talk to them.
+ """
+ display_name = "Blind Trainers"
+
+
+class PurgeSpinners(Toggle):
+ """
+ Trainers will rotate in predictable patterns on a set interval instead of randomly and don't turn toward you when you run.
+ """
+ display_name = "Purge Spinners"
+
+
+class MatchTrainerLevels(Choice):
+ """
+ When you start a battle with a trainer, your party's levels will be automatically set to match that trainer's highest level pokemon.
+
+ The experience you receive will match your party's average actual level, and will only be awarded when you win the battle.
+
+ This is a pseudo-replacement for a level cap and makes every trainer battle a fair fight while still allowing you to level up.
+
+ - Off: The vanilla experience
+ - Additive: The modifier you apply to your team is a flat bonus
+ - Multiplicative: The modifier you apply to your team is a percent bonus
+ """
+ display_name = "Match Trainer Levels"
+ default = 0
+ option_off = 0
+ option_additive = 1
+ option_multiplicative = 2
+
+
+class MatchTrainerLevelsBonus(Range):
+ """
+ A level bonus (or penalty) to apply to your team when matching an opponent's levels.
+
+ When the match trainer levels option is "additive", this value is added to your team's levels during a battle.
+ For example, if this value is 5 (+5 levels), you'll have a level 25 team against a level 20 team, and a level 45 team against a level 40 team.
+
+ When the match trainer levels option is "multiplicative", this is a percent bonus.
+ For example, if this value is 5 (+5%), you'll have a level 21 team against a level 20 team, and a level 42 team against a level 40 team.
+ """
+ display_name = "Match Trainer Levels Modifier"
+ range_start = -100
+ range_end = 100
+ default = 0
+
+
+class DoubleBattleChance(Range):
+ """
+ The percent chance that a trainer with more than 1 pokemon will be converted into a double battle.
+
+ If these trainers would normally approach you, they will only do so if you have 2 unfainted pokemon.
+
+ They can be battled by talking to them no matter what.
+ """
+ display_name = "Double Battle Chance"
+ range_start = 0
+ range_end = 100
+ default = 0
+
+
+class BetterShops(Toggle):
+ """
+ Pokemarts sell every item that can be obtained in a pokemart (except mail, which is still unique to the relevant city).
+ """
+ display_name = "Better Shops"
+
+
+class RemoveRoadblocks(OptionSet):
+ """
+ Removes specific NPCs that normally stand in your way until certain events are completed.
+
+ This can open up the world a bit and make your playthrough less linear, but be careful how many you remove; it may make too much of your world accessible upon receiving Surf.
+ """
+ display_name = "Remove Roadblocks"
+ valid_keys = [
+ "Route 110 Aqua Grunts",
+ "Route 112 Magma Grunts",
+ "Route 119 Aqua Grunts",
+ "Safari Zone Construction Workers",
+ "Lilycove City Wailmer",
+ "Aqua Hideout Grunts",
+ "Seafloor Cavern Aqua Grunt",
+ ]
+
+
+class ExtraBoulders(Toggle):
+ """
+ Places strength boulders on Route 115 which block access to Meteor Falls from the beach.
+
+ This aims to take some power away from Surf by restricting how much it allows you to access.
+ """
+ display_name = "Extra Boulders"
+
+
+class ExtraBumpySlope(Toggle):
+ """
+ Adds a bumpy slope to Route 115 which allows access to Meteor Falls if you have the Acro Bike.
+
+ This aims to take some power away from Surf by adding a new way to exit the Rustboro area.
+ """
+ display_name = "Extra Bumpy Slope"
+
+
+class ModifyRoute118(Toggle):
+ """
+ Changes the layout of Route 118 so that it must be crossed with the Acro Bike instead of Surf.
+
+ This aims to take some power away from Surf by restricting how much it allows you to access.
+ """
+ display_name = "Modify Route 118"
+
+
+class FreeFlyLocation(Toggle):
+ """
+ Enables flying to one random location (excluding cities reachable with no items).
+ """
+ display_name = "Free Fly Location"
+
+
+class HmRequirements(Choice):
+ """
+ Sets the requirements to use HMs outside of battle.
+ """
+ display_name = "HM Requirements"
+ default = 0
+ option_vanilla = 0
+ option_fly_without_badge = 1
+
+
+class TurboA(Toggle):
+ """
+ Holding A will advance most text automatically.
+ """
+ display_name = "Turbo A"
+
+
+class ReceiveItemMessages(Choice):
+ """
+ Determines whether you receive an in-game notification when receiving an item. Items can still only be received in the overworld.
+
+ - All: Every item shows a message
+ - Progression: Only progression items show a message
+ - None: All items are added to your bag silently (badges will still show).
+ """
+ display_name = "Receive Item Messages"
+ default = 0
+ option_all = 0
+ option_progression = 1
+ option_none = 2
+
+
+class RemoteItems(Toggle):
+ """
+ Instead of placing your own items directly into the ROM, all items are received from the server, including items you find for yourself.
+
+ This enables co-op of a single slot and recovering more items after a lost save file (if you're so unlucky).
+
+ But it changes pickup behavior slightly and requires connection to the server to receive any items.
+ """
+ display_name = "Remote Items"
+
+
+class RandomizeMusic(Toggle):
+ """
+ Shuffles music played in any situation where it loops. Includes many FRLG tracks.
+ """
+ display_name = "Randomize Music"
+
+
+class RandomizeFanfares(Toggle):
+ """
+ Shuffles fanfares for item pickups, healing at the pokecenter, etc.
+
+ When this option is enabled, pressing B will interrupt most fanfares.
+ """
+ display_name = "Randomize Fanfares"
+
+
+class WonderTrading(DefaultOnToggle):
+ """
+ Allows participation in wonder trading with other players in your current multiworld. Speak with the center receptionist on the second floor of any pokecenter.
+
+ Wonder trading NEVER affects logic.
+
+ Certain aspects of a pokemon species are per-game, not per-pokemon. As a result, some things are not retained during a trade, including type, ability, level up learnset, and so on.
+
+ Receiving a pokemon this way does not mark it as found in your pokedex.
+
+ Trade evolutions do not evolve this way; they retain their modified methods (level ups and item use).
+ """
+ display_name = "Wonder Trading"
+
+
+class EasterEgg(FreeText):
+ """
+ Enter certain phrases and something special might happen.
+
+ All secret phrases are something that could be a trendy phrase in Dewford Town. They are case insensitive.
+ """
+ display_name = "Easter Egg"
+ default = "EMERALD SECRET"
+
+
+@dataclass
+class PokemonEmeraldOptions(PerGameCommonOptions):
+ goal: Goal
+
+ badges: RandomizeBadges
+ hms: RandomizeHms
+ key_items: RandomizeKeyItems
+ bikes: RandomizeBikes
+ event_tickets: RandomizeEventTickets
+ rods: RandomizeRods
+ overworld_items: RandomizeOverworldItems
+ hidden_items: RandomizeHiddenItems
+ npc_gifts: RandomizeNpcGifts
+ berry_trees: RandomizeBerryTrees
+ dexsanity: Dexsanity
+ trainersanity: Trainersanity
+ item_pool_type: ItemPoolType
+
+ require_itemfinder: HiddenItemsRequireItemfinder
+ require_flash: DarkCavesRequireFlash
+ elite_four_requirement: EliteFourRequirement
+ elite_four_count: EliteFourCount
+ norman_requirement: NormanRequirement
+ norman_count: NormanCount
+ legendary_hunt_catch: LegendaryHuntCatch
+ legendary_hunt_count: LegendaryHuntCount
+ allowed_legendary_hunt_encounters: AllowedLegendaryHuntEncounters
+
+ wild_pokemon: RandomizeWildPokemon
+ wild_encounter_blacklist: WildEncounterBlacklist
+ starters: RandomizeStarters
+ starter_blacklist: StarterBlacklist
+ trainer_parties: RandomizeTrainerParties
+ trainer_party_blacklist: TrainerPartyBlacklist
+ force_fully_evolved: ForceFullyEvolved
+ legendary_encounters: RandomizeLegendaryEncounters
+ misc_pokemon: RandomizeMiscPokemon
+ types: RandomizeTypes
+ abilities: RandomizeAbilities
+ ability_blacklist: AbilityBlacklist
+
+ level_up_moves: LevelUpMoves
+ move_match_type_bias: MoveMatchTypeBias
+ move_normal_type_bias: MoveNormalTypeBias
+ tm_tutor_compatibility: TmTutorCompatibility
+ hm_compatibility: HmCompatibility
+ tm_tutor_moves: TmTutorMoves
+ reusable_tms_tutors: ReusableTmsTutors
+ move_blacklist: MoveBlacklist
+
+ min_catch_rate: MinCatchRate
+ guaranteed_catch: GuaranteedCatch
+ normalize_encounter_rates: NormalizeEncounterRates
+ exp_modifier: ExpModifier
+ blind_trainers: BlindTrainers
+ purge_spinners: PurgeSpinners
+ match_trainer_levels: MatchTrainerLevels
+ match_trainer_levels_bonus: MatchTrainerLevelsBonus
+ double_battle_chance: DoubleBattleChance
+ better_shops: BetterShops
+
+ remove_roadblocks: RemoveRoadblocks
+ extra_boulders: ExtraBoulders
+ extra_bumpy_slope: ExtraBumpySlope
+ modify_118: ModifyRoute118
+ free_fly_location: FreeFlyLocation
+ hm_requirements: HmRequirements
+
+ turbo_a: TurboA
+ receive_item_messages: ReceiveItemMessages
+ remote_items: RemoteItems
+
+ music: RandomizeMusic
+ fanfares: RandomizeFanfares
+
+ death_link: DeathLink
+
+ enable_wonder_trading: WonderTrading
+ easter_egg: EasterEgg
diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py
new file mode 100644
index 000000000000..c60e5e9d4f14
--- /dev/null
+++ b/worlds/pokemon_emerald/pokemon.py
@@ -0,0 +1,696 @@
+"""
+Functions related to pokemon species and moves
+"""
+import functools
+from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple
+
+from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, MiscPokemonData,
+ SpeciesData, data)
+from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters,
+ RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon,
+ TmTutorCompatibility)
+from .util import bool_array_to_int, get_easter_egg, int_to_bool_array
+
+if TYPE_CHECKING:
+ from random import Random
+ from . import PokemonEmeraldWorld
+
+
+_DAMAGING_MOVES = frozenset({
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13,
+ 16, 17, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30,
+ 31, 33, 34, 35, 36, 37, 38, 40, 41, 42, 44, 51,
+ 52, 53, 55, 56, 58, 59, 60, 61, 62, 63, 64, 65,
+ 66, 67, 69, 71, 72, 75, 76, 80, 82, 83, 84, 85,
+ 87, 88, 89, 91, 93, 94, 98, 99, 101, 121, 122, 123,
+ 124, 125, 126, 128, 129, 130, 131, 132, 136, 140, 141, 143,
+ 145, 146, 149, 152, 154, 155, 157, 158, 161, 162, 163, 167,
+ 168, 172, 175, 177, 179, 181, 183, 185, 188, 189, 190, 192,
+ 196, 198, 200, 202, 205, 209, 210, 211, 216, 217, 218, 221,
+ 222, 223, 224, 225, 228, 229, 231, 232, 233, 237, 238, 239,
+ 242, 245, 246, 247, 248, 250, 251, 253, 257, 263, 265, 267,
+ 276, 279, 280, 282, 284, 290, 292, 295, 296, 299, 301, 302,
+ 304, 305, 306, 307, 308, 309, 310, 311, 314, 315, 317, 318,
+ 323, 324, 325, 326, 327, 328, 330, 331, 332, 333, 337, 338,
+ 340, 341, 342, 343, 344, 345, 348, 350, 351, 352, 353, 354,
+})
+"""IDs for moves that safely deal direct damage, for avoiding putting the
+player in a situation where they can only use status moves, or are forced
+to faint themselves, or something of that nature."""
+
+_MOVE_TYPES = [
+ 0, 0, 1, 0, 0, 0, 0, 10, 15, 13, 0, 0, 0, 0, 0,
+ 0, 2, 2, 0, 2, 0, 0, 12, 0, 1, 0, 1, 1, 4, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 6, 6, 0, 17,
+ 0, 0, 0, 0, 0, 0, 3, 10, 10, 15, 11, 11, 11, 15, 15,
+ 14, 11, 15, 0, 2, 2, 1, 1, 1, 1, 0, 12, 12, 12, 0,
+ 12, 12, 3, 12, 12, 12, 6, 16, 10, 13, 13, 13, 13, 5, 4,
+ 4, 4, 3, 14, 14, 14, 14, 14, 0, 0, 14, 7, 0, 0, 0,
+ 0, 0, 0, 0, 7, 11, 0, 14, 14, 15, 14, 0, 0, 0, 2,
+ 0, 0, 7, 3, 3, 4, 10, 11, 11, 0, 0, 0, 0, 14, 14,
+ 0, 1, 0, 14, 3, 0, 6, 0, 2, 0, 11, 0, 12, 0, 14,
+ 0, 3, 11, 0, 0, 4, 14, 5, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 1, 17, 6, 0, 7, 10, 0, 9, 0, 0, 2, 12, 1,
+ 7, 15, 0, 1, 0, 17, 0, 0, 3, 4, 11, 4, 13, 0, 7,
+ 0, 15, 1, 4, 0, 16, 5, 12, 0, 0, 5, 0, 0, 0, 13,
+ 6, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 4, 1, 6,
+ 16, 0, 0, 17, 0, 0, 8, 8, 1, 0, 12, 0, 0, 1, 16,
+ 11, 10, 17, 14, 0, 0, 5, 7, 14, 1, 11, 17, 0, 0, 0,
+ 0, 0, 10, 15, 17, 17, 10, 17, 0, 1, 0, 0, 0, 13, 17,
+ 0, 14, 14, 0, 0, 12, 1, 14, 0, 1, 1, 0, 17, 0, 10,
+ 14, 14, 0, 7, 17, 0, 11, 1, 0, 6, 14, 14, 2, 0, 10,
+ 4, 15, 12, 0, 0, 3, 0, 10, 11, 8, 7, 0, 12, 17, 2,
+ 10, 0, 5, 6, 8, 12, 0, 14, 11, 6, 7, 14, 1, 4, 15,
+ 11, 12, 2, 15, 8, 0, 0, 16, 12, 1, 2, 4, 3, 0, 13,
+ 12, 11, 14, 12, 16, 5, 13, 11, 8, 14,
+]
+"""Maps move ids to the type of that move"""
+
+_MOVES_BY_TYPE: Dict[int, List[int]] = {}
+"""Categorizes move ids by their type"""
+for move, type in enumerate(_MOVE_TYPES):
+ _MOVES_BY_TYPE.setdefault(type, []).append(move)
+
+HM_MOVES = frozenset({
+ data.constants["MOVE_CUT"],
+ data.constants["MOVE_FLY"],
+ data.constants["MOVE_SURF"],
+ data.constants["MOVE_STRENGTH"],
+ data.constants["MOVE_FLASH"],
+ data.constants["MOVE_ROCK_SMASH"],
+ data.constants["MOVE_WATERFALL"],
+ data.constants["MOVE_DIVE"],
+})
+
+_MOVE_BLACKLIST = frozenset({
+ 0, # MOVE_NONE
+ 165, # Struggle
+} | HM_MOVES)
+
+
+@functools.lru_cache(maxsize=386)
+def get_species_id_by_label(label: str) -> int:
+ return next(species.species_id for species in data.species.values() if species.label == label)
+
+
+def get_random_type(random: "Random") -> int:
+ picked_type = random.randrange(0, 18)
+ while picked_type == 9: # Don't pick the ??? type
+ picked_type = random.randrange(0, 18)
+
+ return picked_type
+
+
+def get_random_move(
+ random: "Random",
+ blacklist: Optional[Set[int]] = None,
+ type_bias: int = 0,
+ normal_bias: int = 0,
+ type_target: Optional[Tuple[int, int]] = None) -> int:
+ expanded_blacklist = _MOVE_BLACKLIST | (blacklist if blacklist is not None else set())
+
+ bias = random.random() * 100
+ if bias < type_bias:
+ pass # Keep type_target unchanged
+ elif bias < type_bias + ((100 - type_bias) * (normal_bias / 100)):
+ type_target = (0, 0)
+ else:
+ type_target = None
+
+ chosen_move = None
+
+ # The blacklist is relatively small, so if we don't need to restrict
+ # ourselves to any particular types, it's usually much faster to pick
+ # a random number and hope it works. Limit this to 5 tries in case the
+ # blacklist is actually significant enough to make this unlikely to work.
+ if type_target is None:
+ remaining_attempts = 5
+ while remaining_attempts > 0:
+ remaining_attempts -= 1
+ chosen_move = random.randrange(0, data.constants["MOVES_COUNT"])
+ if chosen_move not in expanded_blacklist:
+ return chosen_move
+ else:
+ chosen_move = None
+
+ # We're either matching types or failed to pick a move above
+ if type_target is None:
+ possible_moves = [i for i in range(data.constants["MOVES_COUNT"]) if i not in expanded_blacklist]
+ else:
+ possible_moves = [move for move in _MOVES_BY_TYPE[type_target[0]] if move not in expanded_blacklist] + \
+ [move for move in _MOVES_BY_TYPE[type_target[1]] if move not in expanded_blacklist]
+
+ if len(possible_moves) == 0:
+ return get_random_move(random, None, type_bias, normal_bias, type_target)
+
+ return random.choice(possible_moves)
+
+
+def get_random_damaging_move(random: "Random", blacklist: Optional[Set[int]] = None) -> int:
+ expanded_blacklist = _MOVE_BLACKLIST | (blacklist if blacklist is not None else set())
+ move_options = list(_DAMAGING_MOVES)
+
+ move = random.choice(move_options)
+ while move in expanded_blacklist:
+ move = random.choice(move_options)
+
+ return move
+
+
+def filter_species_by_nearby_bst(species: List[SpeciesData], target_bst: int) -> List[SpeciesData]:
+ # Sort by difference in bst, then chop off the tail of the list that's more than
+ # 10% different. If that leaves the list empty, increase threshold to 20%, then 30%, etc.
+ species = sorted(species, key=lambda species: abs(sum(species.base_stats) - target_bst))
+ cutoff_index = 0
+ max_percent_different = 10
+ while cutoff_index == 0 and max_percent_different < 10000:
+ while cutoff_index < len(species) and abs(sum(species[cutoff_index].base_stats) - target_bst) < target_bst * (max_percent_different / 100):
+ cutoff_index += 1
+ max_percent_different += 10
+
+ return species[:cutoff_index + 1]
+
+
+def randomize_types(world: "PokemonEmeraldWorld") -> None:
+ if world.options.types == RandomizeTypes.option_shuffle:
+ type_map = list(range(18))
+ world.random.shuffle(type_map)
+
+ # We never want to map to the ??? type, so swap whatever index maps to ??? with ???
+ # which forces ??? to always map to itself. There are no pokemon which have the ??? type
+ mystery_type_index = type_map.index(9)
+ type_map[mystery_type_index], type_map[9] = type_map[9], type_map[mystery_type_index]
+
+ for species in world.modified_species.values():
+ species.types = (type_map[species.types[0]], type_map[species.types[1]])
+ elif world.options.types == RandomizeTypes.option_completely_random:
+ for species in world.modified_species.values():
+ new_type_1 = get_random_type(world.random)
+ new_type_2 = new_type_1
+ if species.types[0] != species.types[1]:
+ while new_type_1 == new_type_2:
+ new_type_2 = get_random_type(world.random)
+
+ species.types = (new_type_1, new_type_2)
+ elif world.options.types == RandomizeTypes.option_follow_evolutions:
+ already_modified: Set[int] = set()
+
+ # Similar to follow evolutions for abilities, but only needs to loop through once.
+ # For every pokemon without a pre-evolution, generates a random mapping from old types to new types
+ # and then walks through the evolution tree applying that map. This means that evolutions that share
+ # types will have those types mapped to the same new types, and evolutions with new or diverging types
+ # will still have new or diverging types.
+ # Consider:
+ # - Charmeleon (Fire/Fire) -> Charizard (Fire/Flying)
+ # - Onyx (Rock/Ground) -> Steelix (Steel/Ground)
+ # - Nincada (Bug/Ground) -> Ninjask (Bug/Flying) && Shedinja (Bug/Ghost)
+ # - Azurill (Normal/Normal) -> Marill (Water/Water)
+ for species in world.modified_species.values():
+ if species.species_id in already_modified:
+ continue
+ if species.pre_evolution is not None and species.pre_evolution not in already_modified:
+ continue
+
+ type_map = list(range(18))
+ world.random.shuffle(type_map)
+
+ # We never want to map to the ??? type, so swap whatever index maps to ??? with ???
+ # which forces ??? to always map to itself. There are no pokemon which have the ??? type
+ mystery_type_index = type_map.index(9)
+ type_map[mystery_type_index], type_map[9] = type_map[9], type_map[mystery_type_index]
+
+ evolutions = [species]
+ while len(evolutions) > 0:
+ evolution = evolutions.pop()
+ evolution.types = (type_map[evolution.types[0]], type_map[evolution.types[1]])
+ already_modified.add(evolution.species_id)
+ evolutions += [world.modified_species[evo.species_id] for evo in evolution.evolutions]
+
+
+def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
+ if world.options.wild_pokemon == RandomizeWildPokemon.option_vanilla:
+ return
+
+ from collections import defaultdict
+
+ should_match_bst = world.options.wild_pokemon in {
+ RandomizeWildPokemon.option_match_base_stats,
+ RandomizeWildPokemon.option_match_base_stats_and_type,
+ }
+ should_match_type = world.options.wild_pokemon in {
+ RandomizeWildPokemon.option_match_type,
+ RandomizeWildPokemon.option_match_base_stats_and_type,
+ }
+
+ already_placed = set()
+ num_placeable_species = NUM_REAL_SPECIES - len(world.blacklisted_wilds)
+
+ priority_species = [data.constants["SPECIES_WAILORD"], data.constants["SPECIES_RELICANTH"]]
+
+ # Loop over map data to modify their encounter slots
+ map_names = list(world.modified_maps.keys())
+ world.random.shuffle(map_names)
+ for map_name in map_names:
+ placed_priority_species = False
+ map_data = world.modified_maps[map_name]
+
+ new_encounters: List[Optional[EncounterTableData]] = [None, None, None]
+ old_encounters = [map_data.land_encounters, map_data.water_encounters, map_data.fishing_encounters]
+
+ for i, table in enumerate(old_encounters):
+ if table is not None:
+ # Create a map from the original species to new species
+ # instead of just randomizing every slot.
+ # Force area 1-to-1 mapping, in other words.
+ species_old_to_new_map: Dict[int, int] = {}
+ for species_id in table.slots:
+ if species_id not in species_old_to_new_map:
+ if not placed_priority_species and len(priority_species) > 0 \
+ and map_name not in OUT_OF_LOGIC_MAPS:
+ new_species_id = priority_species.pop()
+ placed_priority_species = True
+ else:
+ original_species = data.species[species_id]
+
+ # Construct progressive tiers of blacklists that can be peeled back if they
+ # collectively cover too much of the pokedex. A lower index in `blacklists`
+ # indicates a more important set of species to avoid. Entries at `0` will
+ # always be blacklisted.
+ blacklists: Dict[int, List[Set[int]]] = defaultdict(list)
+
+ # Blacklist pokemon already on this table
+ blacklists[0].append(set(species_old_to_new_map.values()))
+
+ # If doing legendary hunt, blacklist Latios from wild encounters so
+ # it can be tracked as the roamer. Otherwise it may be impossible
+ # to tell whether a highlighted route is the roamer or a wild
+ # encounter.
+ if world.options.goal == Goal.option_legendary_hunt:
+ blacklists[0].append({data.constants["SPECIES_LATIOS"]})
+
+ # If dexsanity/catch 'em all mode, blacklist already placed species
+ # until every species has been placed once
+ if world.options.dexsanity and len(already_placed) < num_placeable_species:
+ blacklists[1].append(already_placed)
+
+ # Blacklist from player options
+ blacklists[2].append(world.blacklisted_wilds)
+
+ # Type matching blacklist
+ if should_match_type:
+ blacklists[3].append({
+ species.species_id
+ for species in world.modified_species.values()
+ if not bool(set(species.types) & set(original_species.types))
+ })
+
+ merged_blacklist: Set[int] = set()
+ for max_priority in reversed(sorted(blacklists.keys())):
+ merged_blacklist = set()
+ for priority in blacklists.keys():
+ if priority <= max_priority:
+ for blacklist in blacklists[priority]:
+ merged_blacklist |= blacklist
+
+ if len(merged_blacklist) < NUM_REAL_SPECIES:
+ break
+ else:
+ raise RuntimeError("This should never happen")
+
+ candidates = [
+ species
+ for species in world.modified_species.values()
+ if species.species_id not in merged_blacklist
+ ]
+
+ if should_match_bst:
+ candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
+
+ new_species_id = world.random.choice(candidates).species_id
+ species_old_to_new_map[species_id] = new_species_id
+
+ if world.options.dexsanity and map_name not in OUT_OF_LOGIC_MAPS:
+ already_placed.add(new_species_id)
+
+ # Actually create the new list of slots and encounter table
+ new_slots: List[int] = []
+ for species_id in table.slots:
+ new_slots.append(species_old_to_new_map[species_id])
+
+ new_encounters[i] = EncounterTableData(new_slots, table.address)
+
+ # Rename event items for the new wild pokemon species
+ slot_category: Tuple[str, List[Tuple[Optional[str], range]]] = [
+ ("LAND", [(None, range(0, 12))]),
+ ("WATER", [(None, range(0, 5))]),
+ ("FISHING", [("OLD_ROD", range(0, 2)), ("GOOD_ROD", range(2, 5)), ("SUPER_ROD", range(5, 10))]),
+ ][i]
+ for j, new_species_id in enumerate(new_slots):
+ # Get the subcategory for rods
+ subcategory = next(sc for sc in slot_category[1] if j in sc[1])
+ subcategory_species = []
+ for k in subcategory[1]:
+ if new_slots[k] not in subcategory_species:
+ subcategory_species.append(new_slots[k])
+
+ # Create the name of the location that corresponds to this encounter slot
+ # Fishing locations include the rod name
+ subcategory_str = "" if subcategory[0] is None else "_" + subcategory[0]
+ encounter_location_index = subcategory_species.index(new_species_id) + 1
+ encounter_location_name = f"{map_data.name}_{slot_category[0]}_ENCOUNTERS{subcategory_str}_{encounter_location_index}"
+ try:
+ # Get the corresponding location and change the event name to reflect the new species
+ slot_location = world.multiworld.get_location(encounter_location_name, world.player)
+ slot_location.item.name = f"CATCH_{data.species[new_species_id].name}"
+ except KeyError:
+ pass # Map probably isn't included; should be careful here about bad encounter location names
+
+ map_data.land_encounters = new_encounters[0]
+ map_data.water_encounters = new_encounters[1]
+ map_data.fishing_encounters = new_encounters[2]
+
+
+def randomize_abilities(world: "PokemonEmeraldWorld") -> None:
+ if world.options.abilities == RandomizeAbilities.option_vanilla:
+ return
+
+ # Creating list of potential abilities
+ ability_label_to_value = {ability.label.lower(): ability.ability_id for ability in data.abilities}
+
+ ability_blacklist_labels = {"cacophony"} # Cacophony is defined and has a description, but no effect
+ option_ability_blacklist = world.options.ability_blacklist.value
+ if option_ability_blacklist is not None:
+ ability_blacklist_labels |= {ability_label.lower() for ability_label in option_ability_blacklist}
+
+ ability_blacklist = {ability_label_to_value[label] for label in ability_blacklist_labels}
+ ability_whitelist = [a.ability_id for a in data.abilities if a.ability_id not in ability_blacklist]
+
+ if world.options.abilities == RandomizeAbilities.option_follow_evolutions:
+ already_modified: Set[int] = set()
+
+ # Loops through species and only tries to modify abilities if the pokemon has no pre-evolution
+ # or if the pre-evolution has already been modified. Then tries to modify all species that evolve
+ # from this one which have the same abilities.
+ #
+ # The outer while loop only runs three times for vanilla ordering: Once for a first pass, once for
+ # Hitmonlee/Hitmonchan, and once to verify that there's nothing left to do.
+ while True:
+ had_clean_pass = True
+ for species in world.modified_species.values():
+ if species.species_id in already_modified:
+ continue
+ if species.pre_evolution is not None and species.pre_evolution not in already_modified:
+ continue
+
+ had_clean_pass = False
+
+ old_abilities = species.abilities
+ # 0 is the value for "no ability"; species with only 1 ability have the other set to 0
+ new_abilities = (
+ 0 if old_abilities[0] == 0 else world.random.choice(ability_whitelist),
+ 0 if old_abilities[1] == 0 else world.random.choice(ability_whitelist)
+ )
+
+ # Recursively modify the abilities of anything that evolves from this pokemon
+ # until the evolution doesn't have a matching set of abilities
+ evolutions = [species]
+ while len(evolutions) > 0:
+ evolution = evolutions.pop()
+ if evolution.abilities == old_abilities:
+ evolution.abilities = new_abilities
+ already_modified.add(evolution.species_id)
+ evolutions += [
+ world.modified_species[evolution.species_id]
+ for evolution in evolution.evolutions
+ if evolution.species_id not in already_modified
+ ]
+
+ if had_clean_pass:
+ break
+ else: # Not following evolutions
+ for species in world.modified_species.values():
+ old_abilities = species.abilities
+ # 0 is the value for "no ability"; species with only 1 ability have the other set to 0
+ new_abilities = (
+ 0 if old_abilities[0] == 0 else world.random.choice(ability_whitelist),
+ 0 if old_abilities[1] == 0 else world.random.choice(ability_whitelist)
+ )
+
+ species.abilities = new_abilities
+
+
+def randomize_learnsets(world: "PokemonEmeraldWorld") -> None:
+ if world.options.level_up_moves == LevelUpMoves.option_vanilla:
+ return
+
+ type_bias = world.options.move_match_type_bias.value
+ normal_bias = world.options.move_normal_type_bias.value
+
+ for species in world.modified_species.values():
+ old_learnset = species.learnset
+ new_learnset: List[LearnsetMove] = []
+
+ # All species have 4 moves at level 0. Up to 3 of them are blank spaces reserved for the
+ # start with four moves option. This either replaces those moves or leaves it blank
+ # and moves the cursor.
+ cursor = 0
+ while old_learnset[cursor].move_id == 0:
+ if world.options.level_up_moves == LevelUpMoves.option_start_with_four_moves:
+ new_move = get_random_move(world.random,
+ {move.move_id for move in new_learnset} | world.blacklisted_moves,
+ type_bias, normal_bias, species.types)
+ else:
+ new_move = 0
+ new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move))
+ cursor += 1
+
+ # All moves from here onward are actual moves.
+ while cursor < len(old_learnset):
+ # Guarantees the starter has a good damaging move; i will always be <=3 when entering this loop
+ if cursor == 3:
+ new_move = get_random_damaging_move(world.random, {move.move_id for move in new_learnset})
+ else:
+ new_move = get_random_move(world.random,
+ {move.move_id for move in new_learnset} | world.blacklisted_moves,
+ type_bias, normal_bias, species.types)
+ new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move))
+ cursor += 1
+
+ species.learnset = new_learnset
+
+
+def randomize_starters(world: "PokemonEmeraldWorld") -> None:
+ if world.options.starters == RandomizeStarters.option_vanilla:
+ return
+
+ should_match_bst = world.options.starters in {
+ RandomizeStarters.option_match_base_stats,
+ RandomizeStarters.option_match_base_stats_and_type,
+ }
+ should_match_type = world.options.starters in {
+ RandomizeStarters.option_match_type,
+ RandomizeStarters.option_match_base_stats_and_type,
+ }
+
+ new_starters: List[SpeciesData] = []
+
+ easter_egg_type, easter_egg_value = get_easter_egg(world.options.easter_egg.value)
+ if easter_egg_type == 1:
+ new_starters = [
+ world.modified_species[easter_egg_value],
+ world.modified_species[easter_egg_value],
+ world.modified_species[easter_egg_value]
+ ]
+ else:
+ for i, starter_id in enumerate(data.starters):
+ original_starter = data.species[starter_id]
+ type_blacklist = {
+ species.species_id
+ for species in world.modified_species.values()
+ if not bool(set(species.types) & set(original_starter.types))
+ } if should_match_type else set()
+
+ merged_blacklist = set(s.species_id for s in new_starters) | world.blacklisted_starters | type_blacklist
+ if len(merged_blacklist) == NUM_REAL_SPECIES:
+ merged_blacklist = set(s.species_id for s in new_starters) | world.blacklisted_starters
+ if len(merged_blacklist) == NUM_REAL_SPECIES:
+ merged_blacklist = set(s.species_id for s in new_starters)
+
+ candidates = [
+ species
+ for species in world.modified_species.values()
+ if species.species_id not in merged_blacklist
+ ]
+
+ if should_match_bst:
+ candidates = filter_species_by_nearby_bst(candidates, sum(original_starter.base_stats))
+
+ new_starters.append(world.random.choice(candidates))
+
+ world.modified_starters = (
+ new_starters[0].species_id,
+ new_starters[1].species_id,
+ new_starters[2].species_id
+ )
+
+ # Putting the unchosen starter onto the rival's team
+ # (trainer name, index of starter in team, whether the starter is evolved)
+ rival_teams: List[List[Tuple[str, int, bool]]] = [
+ [
+ ("TRAINER_BRENDAN_ROUTE_103_TREECKO", 0, False),
+ ("TRAINER_BRENDAN_RUSTBORO_TREECKO", 1, False),
+ ("TRAINER_BRENDAN_ROUTE_110_TREECKO", 2, True ),
+ ("TRAINER_BRENDAN_ROUTE_119_TREECKO", 2, True ),
+ ("TRAINER_BRENDAN_LILYCOVE_TREECKO", 3, True ),
+ ("TRAINER_MAY_ROUTE_103_TREECKO", 0, False),
+ ("TRAINER_MAY_RUSTBORO_TREECKO", 1, False),
+ ("TRAINER_MAY_ROUTE_110_TREECKO", 2, True ),
+ ("TRAINER_MAY_ROUTE_119_TREECKO", 2, True ),
+ ("TRAINER_MAY_LILYCOVE_TREECKO", 3, True ),
+ ],
+ [
+ ("TRAINER_BRENDAN_ROUTE_103_TORCHIC", 0, False),
+ ("TRAINER_BRENDAN_RUSTBORO_TORCHIC", 1, False),
+ ("TRAINER_BRENDAN_ROUTE_110_TORCHIC", 2, True ),
+ ("TRAINER_BRENDAN_ROUTE_119_TORCHIC", 2, True ),
+ ("TRAINER_BRENDAN_LILYCOVE_TORCHIC", 3, True ),
+ ("TRAINER_MAY_ROUTE_103_TORCHIC", 0, False),
+ ("TRAINER_MAY_RUSTBORO_TORCHIC", 1, False),
+ ("TRAINER_MAY_ROUTE_110_TORCHIC", 2, True ),
+ ("TRAINER_MAY_ROUTE_119_TORCHIC", 2, True ),
+ ("TRAINER_MAY_LILYCOVE_TORCHIC", 3, True ),
+ ],
+ [
+ ("TRAINER_BRENDAN_ROUTE_103_MUDKIP", 0, False),
+ ("TRAINER_BRENDAN_RUSTBORO_MUDKIP", 1, False),
+ ("TRAINER_BRENDAN_ROUTE_110_MUDKIP", 2, True ),
+ ("TRAINER_BRENDAN_ROUTE_119_MUDKIP", 2, True ),
+ ("TRAINER_BRENDAN_LILYCOVE_MUDKIP", 3, True ),
+ ("TRAINER_MAY_ROUTE_103_MUDKIP", 0, False),
+ ("TRAINER_MAY_RUSTBORO_MUDKIP", 1, False),
+ ("TRAINER_MAY_ROUTE_110_MUDKIP", 2, True ),
+ ("TRAINER_MAY_ROUTE_119_MUDKIP", 2, True ),
+ ("TRAINER_MAY_LILYCOVE_MUDKIP", 3, True ),
+ ],
+ ]
+
+ for i, starter in enumerate([new_starters[1], new_starters[2], new_starters[0]]):
+ potential_evolutions = [evolution.species_id for evolution in starter.evolutions]
+ picked_evolution = starter.species_id
+ if len(potential_evolutions) > 0:
+ picked_evolution = world.random.choice(potential_evolutions)
+
+ for trainer_name, starter_position, is_evolved in rival_teams[i]:
+ trainer_data = world.modified_trainers[data.constants[trainer_name]]
+ trainer_data.party.pokemon[starter_position].species_id = picked_evolution if is_evolved else starter.species_id
+
+
+def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None:
+ if world.options.legendary_encounters == RandomizeLegendaryEncounters.option_vanilla:
+ return
+ elif world.options.legendary_encounters == RandomizeLegendaryEncounters.option_shuffle:
+ # Just take the existing species and shuffle them
+ shuffled_species = [encounter.species_id for encounter in data.legendary_encounters]
+ world.random.shuffle(shuffled_species)
+
+ for i, encounter in enumerate(data.legendary_encounters):
+ world.modified_legendary_encounters.append(MiscPokemonData(
+ shuffled_species[i],
+ encounter.address
+ ))
+ else:
+ should_match_bst = world.options.legendary_encounters in {
+ RandomizeLegendaryEncounters.option_match_base_stats,
+ RandomizeLegendaryEncounters.option_match_base_stats_and_type
+ }
+ should_match_type = world.options.legendary_encounters in {
+ RandomizeLegendaryEncounters.option_match_type,
+ RandomizeLegendaryEncounters.option_match_base_stats_and_type
+ }
+
+ for encounter in data.legendary_encounters:
+ original_species = world.modified_species[encounter.species_id]
+
+ candidates = list(world.modified_species.values())
+ if should_match_type:
+ candidates = [
+ species
+ for species in candidates
+ if bool(set(species.types) & set(original_species.types))
+ ]
+ if should_match_bst:
+ candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
+
+ world.modified_legendary_encounters.append(MiscPokemonData(
+ world.random.choice(candidates).species_id,
+ encounter.address
+ ))
+
+
+def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None:
+ if world.options.misc_pokemon == RandomizeMiscPokemon.option_vanilla:
+ return
+ elif world.options.misc_pokemon == RandomizeMiscPokemon.option_shuffle:
+ # Just take the existing species and shuffle them
+ shuffled_species = [encounter.species_id for encounter in data.misc_pokemon]
+ world.random.shuffle(shuffled_species)
+
+ world.modified_misc_pokemon = []
+ for i, encounter in enumerate(data.misc_pokemon):
+ world.modified_misc_pokemon.append(MiscPokemonData(
+ shuffled_species[i],
+ encounter.address
+ ))
+ else:
+ should_match_bst = world.options.misc_pokemon in {
+ RandomizeMiscPokemon.option_match_base_stats,
+ RandomizeMiscPokemon.option_match_base_stats_and_type,
+ }
+ should_match_type = world.options.misc_pokemon in {
+ RandomizeMiscPokemon.option_match_type,
+ RandomizeMiscPokemon.option_match_base_stats_and_type,
+ }
+
+ for encounter in data.misc_pokemon:
+ original_species = world.modified_species[encounter.species_id]
+
+ candidates = list(world.modified_species.values())
+ if should_match_type:
+ candidates = [
+ species
+ for species in candidates
+ if bool(set(species.types) & set(original_species.types))
+ ]
+ if should_match_bst:
+ candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
+
+ player_filtered_candidates = [
+ species
+ for species in candidates
+ if species.species_id not in world.blacklisted_wilds
+ ]
+ if len(player_filtered_candidates) > 0:
+ candidates = player_filtered_candidates
+
+ world.modified_misc_pokemon.append(MiscPokemonData(
+ world.random.choice(candidates).species_id,
+ encounter.address
+ ))
+
+
+def randomize_tm_hm_compatibility(world: "PokemonEmeraldWorld") -> None:
+ for species in world.modified_species.values():
+ # TM and HM compatibility is stored as a 64-bit bitfield
+ combatibility_array = int_to_bool_array(species.tm_hm_compatibility)
+
+ # TMs
+ if world.options.tm_tutor_compatibility != TmTutorCompatibility.special_range_names["vanilla"]:
+ for i in range(0, 50):
+ combatibility_array[i] = world.random.random() < world.options.tm_tutor_compatibility / 100
+
+ # HMs
+ if world.options.hm_compatibility != HmCompatibility.special_range_names["vanilla"]:
+ for i in range(50, 58):
+ combatibility_array[i] = world.random.random() < world.options.hm_compatibility / 100
+
+ species.tm_hm_compatibility = bool_array_to_int(combatibility_array)
diff --git a/worlds/pokemon_emerald/regions.py b/worlds/pokemon_emerald/regions.py
new file mode 100644
index 000000000000..b74f5f5ebf76
--- /dev/null
+++ b/worlds/pokemon_emerald/regions.py
@@ -0,0 +1,123 @@
+"""
+Functions related to AP regions for Pokemon Emerald (see ./data/regions for region definitions)
+"""
+from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
+
+from BaseClasses import CollectionState, ItemClassification, Region
+
+from .data import data
+from .items import PokemonEmeraldItem
+from .locations import PokemonEmeraldLocation
+
+if TYPE_CHECKING:
+ from . import PokemonEmeraldWorld
+
+
+def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]:
+ """
+ Iterates through regions created from JSON to create regions and adds them to the multiworld.
+ Also creates and places events and connects regions via warps and the exits defined in the JSON.
+ """
+ # Used in connect_to_map_encounters. Splits encounter categories into "subcategories" and gives them names
+ # and rules so the rods can only access their specific slots.
+ encounter_categories: Dict[str, List[Tuple[Optional[str], range, Optional[Callable[[CollectionState], bool]]]]] = {
+ "LAND": [(None, range(0, 12), None)],
+ "WATER": [(None, range(0, 5), None)],
+ "FISHING": [
+ ("OLD_ROD", range(0, 2), lambda state: state.has("Old Rod", world.player)),
+ ("GOOD_ROD", range(2, 5), lambda state: state.has("Good Rod", world.player)),
+ ("SUPER_ROD", range(5, 10), lambda state: state.has("Super Rod", world.player)),
+ ],
+ }
+
+ def connect_to_map_encounters(region: Region, map_name: str, include_slots: Tuple[bool, bool, bool]):
+ """
+ Connects the provided region to the corresponding wild encounters for the given parent map.
+
+ Each in-game map may have a non-physical Region for encountering wild pokemon in each of the three categories
+ land, water, and fishing. Region data defines whether a given region includes places where those encounters can
+ be accessed (i.e. whether the region has tall grass, a river bank, is on water, etc.).
+
+ These regions are created lazily and dynamically so as not to bother with unused maps.
+ """
+ # For each of land, water, and fishing, connect the region if indicated by include_slots
+ for i, encounter_category in enumerate(encounter_categories.items()):
+ if include_slots[i]:
+ region_name = f"{map_name}_{encounter_category[0]}_ENCOUNTERS"
+
+ # If the region hasn't been created yet, create it now
+ try:
+ encounter_region = world.multiworld.get_region(region_name, world.player)
+ except KeyError:
+ encounter_region = Region(region_name, world.player, world.multiworld)
+ encounter_slots = getattr(data.maps[map_name], f"{encounter_category[0].lower()}_encounters").slots
+
+ # Subcategory is for splitting fishing rods; land and water only have one subcategory
+ for subcategory in encounter_category[1]:
+ # Want to create locations per species, not per slot
+ # encounter_categories includes info on which slots belong to which subcategory
+ unique_species = []
+ for j, species_id in enumerate(encounter_slots):
+ if j in subcategory[1] and not species_id in unique_species:
+ unique_species.append(species_id)
+
+ # Create a location for the species
+ for j, species_id in enumerate(unique_species):
+ encounter_location = PokemonEmeraldLocation(
+ world.player,
+ f"{region_name}{'_' + subcategory[0] if subcategory[0] is not None else ''}_{j + 1}",
+ None,
+ encounter_region
+ )
+ encounter_location.show_in_spoiler = False
+
+ # Add access rule
+ if subcategory[2] is not None:
+ encounter_location.access_rule = subcategory[2]
+
+ # Fill the location with an event for catching that species
+ encounter_location.place_locked_item(PokemonEmeraldItem(
+ f"CATCH_{data.species[species_id].name}",
+ ItemClassification.progression_skip_balancing,
+ None,
+ world.player
+ ))
+ encounter_region.locations.append(encounter_location)
+
+ # Add the new encounter region to the multiworld
+ world.multiworld.regions.append(encounter_region)
+
+ # Encounter region exists, just connect to it
+ region.connect(encounter_region, f"{region.name} -> {region_name}")
+
+ regions: Dict[str, Region] = {}
+ connections: List[Tuple[str, str, str]] = []
+ for region_name, region_data in data.regions.items():
+ new_region = Region(region_name, world.player, world.multiworld)
+
+ for event_data in region_data.events:
+ event = PokemonEmeraldLocation(world.player, event_data.name, None, new_region)
+ event.place_locked_item(PokemonEmeraldItem(event_data.name, ItemClassification.progression, None, world.player))
+ new_region.locations.append(event)
+
+ for region_exit in region_data.exits:
+ connections.append((f"{region_name} -> {region_exit}", region_name, region_exit))
+
+ for warp in region_data.warps:
+ dest_warp = data.warps[data.warp_map[warp]]
+ if dest_warp.parent_region is None:
+ continue
+ connections.append((warp, region_name, dest_warp.parent_region))
+
+ regions[region_name] = new_region
+
+ connect_to_map_encounters(new_region, region_data.parent_map.name,
+ (region_data.has_grass, region_data.has_water, region_data.has_fishing))
+
+ for name, source, dest in connections:
+ regions[source].connect(regions[dest], name)
+
+ regions["Menu"] = Region("Menu", world.player, world.multiworld)
+ regions["Menu"].connect(regions["REGION_LITTLEROOT_TOWN/MAIN"], "Start Game")
+
+ return regions
diff --git a/worlds/pokemon_emerald/rom.py b/worlds/pokemon_emerald/rom.py
new file mode 100644
index 000000000000..968a103ccd25
--- /dev/null
+++ b/worlds/pokemon_emerald/rom.py
@@ -0,0 +1,857 @@
+"""
+Classes and functions related to creating a ROM patch
+"""
+import copy
+import os
+import struct
+from typing import TYPE_CHECKING, Dict, List, Tuple
+
+from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
+from settings import get_settings
+
+from .data import TrainerPokemonDataTypeEnum, BASE_OFFSET, data
+from .items import reverse_offset_item_value
+from .options import (RandomizeWildPokemon, RandomizeTrainerParties, EliteFourRequirement, NormanRequirement,
+ MatchTrainerLevels)
+from .pokemon import HM_MOVES, get_random_move
+from .util import bool_array_to_int, encode_string, get_easter_egg
+
+if TYPE_CHECKING:
+ from . import PokemonEmeraldWorld
+
+
+_LOOPING_MUSIC = [
+ "MUS_GSC_ROUTE38", "MUS_GSC_PEWTER", "MUS_ROUTE101", "MUS_ROUTE110", "MUS_ROUTE120", "MUS_ROUTE122",
+ "MUS_PETALBURG", "MUS_OLDALE", "MUS_GYM", "MUS_SURF", "MUS_PETALBURG_WOODS", "MUS_LILYCOVE_MUSEUM",
+ "MUS_OCEANIC_MUSEUM", "MUS_ENCOUNTER_GIRL", "MUS_ENCOUNTER_MALE", "MUS_ABANDONED_SHIP", "MUS_FORTREE",
+ "MUS_BIRCH_LAB", "MUS_B_TOWER_RS", "MUS_ENCOUNTER_SWIMMER", "MUS_CAVE_OF_ORIGIN", "MUS_ENCOUNTER_RICH",
+ "MUS_VERDANTURF", "MUS_RUSTBORO", "MUS_POKE_CENTER", "MUS_CAUGHT", "MUS_VICTORY_GYM_LEADER", "MUS_VICTORY_LEAGUE",
+ "MUS_VICTORY_WILD", "MUS_C_VS_LEGEND_BEAST", "MUS_ROUTE104", "MUS_ROUTE119", "MUS_CYCLING", "MUS_POKE_MART",
+ "MUS_LITTLEROOT", "MUS_MT_CHIMNEY", "MUS_ENCOUNTER_FEMALE", "MUS_LILYCOVE", "MUS_DESERT", "MUS_HELP",
+ "MUS_UNDERWATER", "MUS_VICTORY_TRAINER", "MUS_ENCOUNTER_MAY", "MUS_ENCOUNTER_INTENSE", "MUS_ENCOUNTER_COOL",
+ "MUS_ROUTE113", "MUS_ENCOUNTER_AQUA", "MUS_FOLLOW_ME", "MUS_ENCOUNTER_BRENDAN", "MUS_EVER_GRANDE",
+ "MUS_ENCOUNTER_SUSPICIOUS", "MUS_VICTORY_AQUA_MAGMA", "MUS_GAME_CORNER", "MUS_DEWFORD", "MUS_SAFARI_ZONE",
+ "MUS_VICTORY_ROAD", "MUS_AQUA_MAGMA_HIDEOUT", "MUS_SAILING", "MUS_MT_PYRE", "MUS_SLATEPORT", "MUS_MT_PYRE_EXTERIOR",
+ "MUS_SCHOOL", "MUS_HALL_OF_FAME", "MUS_FALLARBOR", "MUS_SEALED_CHAMBER", "MUS_CONTEST_WINNER", "MUS_CONTEST",
+ "MUS_ENCOUNTER_MAGMA", "MUS_ABNORMAL_WEATHER", "MUS_WEATHER_GROUDON", "MUS_SOOTOPOLIS", "MUS_HALL_OF_FAME_ROOM",
+ "MUS_TRICK_HOUSE", "MUS_ENCOUNTER_TWINS", "MUS_ENCOUNTER_ELITE_FOUR", "MUS_ENCOUNTER_HIKER", "MUS_CONTEST_LOBBY",
+ "MUS_ENCOUNTER_INTERVIEWER", "MUS_ENCOUNTER_CHAMPION", "MUS_B_FRONTIER", "MUS_B_ARENA", "MUS_B_PYRAMID",
+ "MUS_B_PYRAMID_TOP", "MUS_B_PALACE", "MUS_B_TOWER", "MUS_B_DOME", "MUS_B_PIKE", "MUS_B_FACTORY", "MUS_VS_RAYQUAZA",
+ "MUS_VS_FRONTIER_BRAIN", "MUS_VS_MEW", "MUS_B_DOME_LOBBY", "MUS_VS_WILD", "MUS_VS_AQUA_MAGMA", "MUS_VS_TRAINER",
+ "MUS_VS_GYM_LEADER", "MUS_VS_CHAMPION", "MUS_VS_REGI", "MUS_VS_KYOGRE_GROUDON", "MUS_VS_RIVAL", "MUS_VS_ELITE_FOUR",
+ "MUS_VS_AQUA_MAGMA_LEADER", "MUS_RG_FOLLOW_ME", "MUS_RG_GAME_CORNER", "MUS_RG_ROCKET_HIDEOUT", "MUS_RG_GYM",
+ "MUS_RG_CINNABAR", "MUS_RG_LAVENDER", "MUS_RG_CYCLING", "MUS_RG_ENCOUNTER_ROCKET", "MUS_RG_ENCOUNTER_GIRL",
+ "MUS_RG_ENCOUNTER_BOY", "MUS_RG_HALL_OF_FAME", "MUS_RG_VIRIDIAN_FOREST", "MUS_RG_MT_MOON", "MUS_RG_POKE_MANSION",
+ "MUS_RG_ROUTE1", "MUS_RG_ROUTE24", "MUS_RG_ROUTE3", "MUS_RG_ROUTE11", "MUS_RG_VICTORY_ROAD", "MUS_RG_VS_GYM_LEADER",
+ "MUS_RG_VS_TRAINER", "MUS_RG_VS_WILD", "MUS_RG_VS_CHAMPION", "MUS_RG_PALLET", "MUS_RG_OAK_LAB", "MUS_RG_OAK",
+ "MUS_RG_POKE_CENTER", "MUS_RG_SS_ANNE", "MUS_RG_SURF", "MUS_RG_POKE_TOWER", "MUS_RG_SILPH", "MUS_RG_FUCHSIA",
+ "MUS_RG_CELADON", "MUS_RG_VICTORY_TRAINER", "MUS_RG_VICTORY_WILD", "MUS_RG_VICTORY_GYM_LEADER", "MUS_RG_VERMILLION",
+ "MUS_RG_PEWTER", "MUS_RG_ENCOUNTER_RIVAL", "MUS_RG_RIVAL_EXIT", "MUS_RG_CAUGHT", "MUS_RG_POKE_JUMP",
+ "MUS_RG_UNION_ROOM", "MUS_RG_NET_CENTER", "MUS_RG_MYSTERY_GIFT", "MUS_RG_BERRY_PICK", "MUS_RG_SEVII_CAVE",
+ "MUS_RG_TEACHY_TV_SHOW", "MUS_RG_SEVII_ROUTE", "MUS_RG_SEVII_DUNGEON", "MUS_RG_SEVII_123", "MUS_RG_SEVII_45",
+ "MUS_RG_SEVII_67", "MUS_RG_VS_DEOXYS", "MUS_RG_VS_MEWTWO", "MUS_RG_VS_LEGEND", "MUS_RG_ENCOUNTER_GYM_LEADER",
+ "MUS_RG_ENCOUNTER_DEOXYS", "MUS_RG_TRAINER_TOWER", "MUS_RG_SLOW_PALLET", "MUS_RG_TEACHY_TV_MENU",
+]
+
+_FANFARES: Dict[str, int] = {
+ "MUS_LEVEL_UP": 80,
+ "MUS_OBTAIN_ITEM": 160,
+ "MUS_EVOLVED": 220,
+ "MUS_OBTAIN_TMHM": 220,
+ "MUS_HEAL": 160,
+ "MUS_OBTAIN_BADGE": 340,
+ "MUS_MOVE_DELETED": 180,
+ "MUS_OBTAIN_BERRY": 120,
+ "MUS_AWAKEN_LEGEND": 710,
+ "MUS_SLOTS_JACKPOT": 250,
+ "MUS_SLOTS_WIN": 150,
+ "MUS_TOO_BAD": 160,
+ "MUS_RG_POKE_FLUTE": 450,
+ "MUS_RG_OBTAIN_KEY_ITEM": 170,
+ "MUS_RG_DEX_RATING": 196,
+ "MUS_OBTAIN_B_POINTS": 313,
+ "MUS_OBTAIN_SYMBOL": 318,
+ "MUS_REGISTER_MATCH_CALL": 135,
+}
+
+CAVE_EVENT_NAME_TO_ID = {
+ "TERRA_CAVE_ROUTE_114_1": 1,
+ "TERRA_CAVE_ROUTE_114_2": 2,
+ "TERRA_CAVE_ROUTE_115_1": 3,
+ "TERRA_CAVE_ROUTE_115_2": 4,
+ "TERRA_CAVE_ROUTE_116_1": 5,
+ "TERRA_CAVE_ROUTE_116_2": 6,
+ "TERRA_CAVE_ROUTE_118_1": 7,
+ "TERRA_CAVE_ROUTE_118_2": 8,
+ "MARINE_CAVE_ROUTE_105_1": 9,
+ "MARINE_CAVE_ROUTE_105_2": 10,
+ "MARINE_CAVE_ROUTE_125_1": 11,
+ "MARINE_CAVE_ROUTE_125_2": 12,
+ "MARINE_CAVE_ROUTE_127_1": 13,
+ "MARINE_CAVE_ROUTE_127_2": 14,
+ "MARINE_CAVE_ROUTE_129_1": 15,
+ "MARINE_CAVE_ROUTE_129_2": 16,
+}
+
+
+class PokemonEmeraldProcedurePatch(APProcedurePatch, APTokenMixin):
+ game = "Pokemon Emerald"
+ hash = "605b89b67018abcea91e693a4dd25be3"
+ patch_file_ending = ".apemerald"
+ result_file_ending = ".gba"
+
+ procedure = [
+ ("apply_bsdiff4", ["base_patch.bsdiff4"]),
+ ("apply_tokens", ["token_data.bin"])
+ ]
+
+ @classmethod
+ def get_source_data(cls) -> bytes:
+ with open(get_settings().pokemon_emerald_settings.rom_file, "rb") as infile:
+ base_rom_bytes = bytes(infile.read())
+
+ return base_rom_bytes
+
+
+def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch) -> None:
+ # Set free fly location
+ if world.options.free_fly_location:
+ patch.write_token(
+ APTokenTypes.WRITE,
+ data.rom_addresses["gArchipelagoOptions"] + 0x20,
+ struct.pack("= 50:
+ continue
+
+ player_name_ids[player_name] = len(player_name_ids)
+ for j, b in enumerate(encode_string(player_name, 17)):
+ patch.write_token(
+ APTokenTypes.WRITE,
+ data.rom_addresses["gArchipelagoPlayerNames"] + (player_name_ids[player_name] * 17) + j,
+ struct.pack(" 35:
+ item_name = item_name[:34] + "…"
+
+ # Only 36 * 250 bytes for item names
+ if next_item_name_offset + len(item_name) + 1 > 36 * 250:
+ continue
+
+ item_name_offsets[item_name] = next_item_name_offset
+ next_item_name_offset += len(item_name) + 1
+ patch.write_token(
+ APTokenTypes.WRITE,
+ data.rom_addresses["gArchipelagoItemNames"] + (item_name_offsets[item_name]),
+ encode_string(item_name) + b"\xFF"
+ )
+
+ # There should always be enough space for one entry per location
+ patch.write_token(
+ APTokenTypes.WRITE,
+ data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 0,
+ struct.pack(" 0:
+ starting_badges |= (1 << 0)
+ if start_inventory.pop("Knuckle Badge", 0) > 0:
+ starting_badges |= (1 << 1)
+ if start_inventory.pop("Dynamo Badge", 0) > 0:
+ starting_badges |= (1 << 2)
+ if start_inventory.pop("Heat Badge", 0) > 0:
+ starting_badges |= (1 << 3)
+ if start_inventory.pop("Balance Badge", 0) > 0:
+ starting_badges |= (1 << 4)
+ if start_inventory.pop("Feather Badge", 0) > 0:
+ starting_badges |= (1 << 5)
+ if start_inventory.pop("Mind Badge", 0) > 0:
+ starting_badges |= (1 << 6)
+ if start_inventory.pop("Rain Badge", 0) > 0:
+ starting_badges |= (1 << 7)
+
+ pc_slots: List[Tuple[str, int]] = []
+ while any(qty > 0 for qty in start_inventory.values()):
+ if len(pc_slots) >= 19:
+ break
+
+ for i, item_name in enumerate(start_inventory.keys()):
+ if len(pc_slots) >= 19:
+ break
+
+ quantity = min(start_inventory[item_name], 999)
+ if quantity == 0:
+ continue
+
+ start_inventory[item_name] -= quantity
+
+ pc_slots.append((item_name, quantity))
+
+ pc_slots.sort(reverse=True)
+
+ for i, slot in enumerate(pc_slots):
+ address = data.rom_addresses["sNewGamePCItems"] + (i * 4)
+ item = reverse_offset_item_value(world.item_name_to_id[slot[0]])
+ patch.write_token(APTokenTypes.WRITE, address + 0, struct.pack(" None:
+ """
+ Encounter tables are lists of
+ struct {
+ min_level: 0x01 bytes,
+ max_level: 0x01 bytes,
+ species_id: 0x02 bytes
+ }
+ """
+ for map_data in world.modified_maps.values():
+ tables = [map_data.land_encounters, map_data.water_encounters, map_data.fishing_encounters]
+ for table in tables:
+ if table is not None:
+ for i, species_id in enumerate(table.slots):
+ address = table.address + 2 + (4 * i)
+ patch.write_token(APTokenTypes.WRITE, address, struct.pack(" None:
+ for species in world.modified_species.values():
+ patch.write_token(APTokenTypes.WRITE, species.address + 6, struct.pack(" None:
+ for trainer in world.modified_trainers:
+ party_address = trainer.party.address
+
+ pokemon_data_size: int
+ if trainer.party.pokemon_data_type in {TrainerPokemonDataTypeEnum.NO_ITEM_DEFAULT_MOVES, TrainerPokemonDataTypeEnum.ITEM_DEFAULT_MOVES}:
+ pokemon_data_size = 8
+ else: # Custom Moves
+ pokemon_data_size = 16
+
+ for i, pokemon in enumerate(trainer.party.pokemon):
+ pokemon_address = party_address + (i * pokemon_data_size)
+
+ # Replace species
+ patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x04, struct.pack(" None:
+ for encounter in world.modified_legendary_encounters:
+ patch.write_token(APTokenTypes.WRITE, encounter.address, struct.pack(" None:
+ for encounter in world.modified_misc_pokemon:
+ patch.write_token(APTokenTypes.WRITE, encounter.address, struct.pack(" None:
+ patch.write_token(APTokenTypes.WRITE, data.rom_addresses["sStarterMon"] + 0, struct.pack(" None:
+ tmhm_list_address = data.rom_addresses["sTMHMMoves"]
+
+ for i, move in enumerate(world.modified_tmhm_moves):
+ # Don't modify HMs
+ if i >= 50:
+ break
+
+ if easter_egg[0] == 2:
+ patch.write_token(APTokenTypes.WRITE, tmhm_list_address + (i * 2), struct.pack(" None:
+ learnsets_address = data.rom_addresses["gTMHMLearnsets"]
+
+ for species in world.modified_species.values():
+ patch.write_token(
+ APTokenTypes.WRITE,
+ learnsets_address + (species.species_id * 8),
+ struct.pack(" None:
+ probability = world.options.double_battle_chance.value / 100
+
+ battle_type_map = {
+ 0: 4,
+ 1: 8,
+ 2: 6,
+ 3: 13,
+ }
+
+ for trainer_data in data.trainers:
+ if trainer_data.script_address != 0 and len(trainer_data.party.pokemon) > 1:
+ original_battle_type = trainer_data.battle_type
+ if original_battle_type in battle_type_map: # Don't touch anything other than regular single battles
+ if world.random.random() < probability:
+ # Set the trainer to be a double battle
+ patch.write_token(APTokenTypes.WRITE, trainer_data.address + 0x18, struct.pack(" None:
+ if easter_egg[0] == 2:
+ for i in range(30):
+ patch.write_token(
+ APTokenTypes.WRITE,
+ data.rom_addresses["gTutorMoves"] + (i * 2),
+ struct.pack(" None:
+ hm_rules: Dict[str, Callable[[CollectionState], bool]] = {}
+ for hm, badges in world.hm_requirements.items():
+ if isinstance(badges, list):
+ hm_rules[hm] = lambda state, hm=hm, badges=badges: state.has(hm, world.player) \
+ and state.has_all(badges, world.player)
+ else:
+ hm_rules[hm] = lambda state, hm=hm, badges=badges: state.has(hm, world.player) \
+ and state.has_group("Badges", world.player, badges)
+
+ def has_acro_bike(state: CollectionState):
+ return state.has("Acro Bike", world.player)
+
+ def has_mach_bike(state: CollectionState):
+ return state.has("Mach Bike", world.player)
+
+ def defeated_n_gym_leaders(state: CollectionState, n: int) -> bool:
+ return sum([state.has(event, world.player) for event in [
+ "EVENT_DEFEAT_ROXANNE",
+ "EVENT_DEFEAT_BRAWLY",
+ "EVENT_DEFEAT_WATTSON",
+ "EVENT_DEFEAT_FLANNERY",
+ "EVENT_DEFEAT_NORMAN",
+ "EVENT_DEFEAT_WINONA",
+ "EVENT_DEFEAT_TATE_AND_LIZA",
+ "EVENT_DEFEAT_JUAN",
+ ]]) >= n
+
+ huntable_legendary_events = [
+ f"EVENT_ENCOUNTER_{key}"
+ for name, key in {
+ "Groudon": "GROUDON",
+ "Kyogre": "KYOGRE",
+ "Rayquaza": "RAYQUAZA",
+ "Latias": "LATIAS",
+ "Latios": "LATIOS",
+ "Regirock": "REGIROCK",
+ "Regice": "REGICE",
+ "Registeel": "REGISTEEL",
+ "Mew": "MEW",
+ "Deoxys": "DEOXYS",
+ "Ho-Oh": "HO_OH",
+ "Lugia": "LUGIA",
+ }.items()
+ if name in world.options.allowed_legendary_hunt_encounters.value
+ ]
+ def encountered_n_legendaries(state: CollectionState, n: int) -> bool:
+ return sum(int(state.has(event, world.player)) for event in huntable_legendary_events) >= n
+
+ def get_entrance(entrance: str):
+ return world.multiworld.get_entrance(entrance, world.player)
+
+ def get_location(location: str):
+ if location in data.locations:
+ location = data.locations[location].label
+
+ return world.multiworld.get_location(location, world.player)
+
+ if world.options.goal == Goal.option_champion:
+ completion_condition = lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ elif world.options.goal == Goal.option_steven:
+ completion_condition = lambda state: state.has("EVENT_DEFEAT_STEVEN", world.player)
+ elif world.options.goal == Goal.option_norman:
+ completion_condition = lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player)
+ elif world.options.goal == Goal.option_legendary_hunt:
+ completion_condition = lambda state: encountered_n_legendaries(state, world.options.legendary_hunt_count.value)
+
+ world.multiworld.completion_condition[world.player] = completion_condition
+
+ if world.options.legendary_hunt_catch:
+ set_rule(get_location("EVENT_ENCOUNTER_GROUDON"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
+ set_rule(get_location("EVENT_ENCOUNTER_KYOGRE"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
+ set_rule(get_location("EVENT_ENCOUNTER_RAYQUAZA"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
+ set_rule(get_location("EVENT_ENCOUNTER_LATIAS"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
+ # Latios already only requires defeating the champion and access to Route 117
+ # set_rule(get_location("EVENT_ENCOUNTER_LATIOS"),
+ # lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
+ set_rule(get_location("EVENT_ENCOUNTER_REGIROCK"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
+ set_rule(get_location("EVENT_ENCOUNTER_REGICE"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
+ set_rule(get_location("EVENT_ENCOUNTER_REGISTEEL"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
+ set_rule(get_location("EVENT_ENCOUNTER_MEW"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
+ set_rule(get_location("EVENT_ENCOUNTER_DEOXYS"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
+ set_rule(get_location("EVENT_ENCOUNTER_HO_OH"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
+ set_rule(get_location("EVENT_ENCOUNTER_LUGIA"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player))
+
+ # Sky
+ set_rule(
+ get_entrance("REGION_LITTLEROOT_TOWN/MAIN -> REGION_SKY"),
+ hm_rules["HM02 Fly"]
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_LITTLEROOT_TOWN/MAIN"),
+ lambda state: state.has("EVENT_VISITED_LITTLEROOT_TOWN", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_OLDALE_TOWN/MAIN"),
+ lambda state: state.has("EVENT_VISITED_OLDALE_TOWN", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_PETALBURG_CITY/MAIN"),
+ lambda state: state.has("EVENT_VISITED_PETALBURG_CITY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_RUSTBORO_CITY/MAIN"),
+ lambda state: state.has("EVENT_VISITED_RUSTBORO_CITY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_DEWFORD_TOWN/MAIN"),
+ lambda state: state.has("EVENT_VISITED_DEWFORD_TOWN", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_SLATEPORT_CITY/MAIN"),
+ lambda state: state.has("EVENT_VISITED_SLATEPORT_CITY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_MAUVILLE_CITY/MAIN"),
+ lambda state: state.has("EVENT_VISITED_MAUVILLE_CITY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_VERDANTURF_TOWN/MAIN"),
+ lambda state: state.has("EVENT_VISITED_VERDANTURF_TOWN", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_FALLARBOR_TOWN/MAIN"),
+ lambda state: state.has("EVENT_VISITED_FALLARBOR_TOWN", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_LAVARIDGE_TOWN/MAIN"),
+ lambda state: state.has("EVENT_VISITED_LAVARIDGE_TOWN", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_FORTREE_CITY/MAIN"),
+ lambda state: state.has("EVENT_VISITED_FORTREE_CITY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_LILYCOVE_CITY/MAIN"),
+ lambda state: state.has("EVENT_VISITED_LILYCOVE_CITY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_MOSSDEEP_CITY/MAIN"),
+ lambda state: state.has("EVENT_VISITED_MOSSDEEP_CITY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_SOOTOPOLIS_CITY/EAST"),
+ lambda state: state.has("EVENT_VISITED_SOOTOPOLIS_CITY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY -> REGION_EVER_GRANDE_CITY/SOUTH"),
+ lambda state: state.has("EVENT_VISITED_EVER_GRANDE_CITY", world.player)
+ )
+
+ # Littleroot Town
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_SS_TICKET"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ )
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_AURORA_TICKET"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ )
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_EON_TICKET"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ )
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_MYSTIC_TICKET"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ )
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_OLD_SEA_MAP"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ )
+
+ # Route 102
+ set_rule(
+ get_entrance("REGION_ROUTE102/MAIN -> REGION_ROUTE102/POND"),
+ hm_rules["HM03 Surf"]
+ )
+
+ # Route 103
+ set_rule(
+ get_entrance("REGION_ROUTE103/EAST -> REGION_ROUTE103/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE103/WEST -> REGION_ROUTE103/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE103/EAST -> REGION_ROUTE103/EAST_TREE_MAZE"),
+ hm_rules["HM01 Cut"]
+ )
+
+ # Petalburg City
+ set_rule(
+ get_entrance("REGION_PETALBURG_CITY/MAIN -> REGION_PETALBURG_CITY/SOUTH_POND"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_PETALBURG_CITY/MAIN -> REGION_PETALBURG_CITY/NORTH_POND"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_HM_SURF"),
+ lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player)
+ )
+ if world.options.norman_requirement == NormanRequirement.option_badges:
+ set_rule(
+ get_entrance("MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3"),
+ lambda state: state.has_group("Badges", world.player, world.options.norman_count.value)
+ )
+ set_rule(
+ get_entrance("MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"),
+ lambda state: state.has_group("Badges", world.player, world.options.norman_count.value)
+ )
+ else:
+ set_rule(
+ get_entrance("MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3"),
+ lambda state: defeated_n_gym_leaders(state, world.options.norman_count.value)
+ )
+ set_rule(
+ get_entrance("MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"),
+ lambda state: defeated_n_gym_leaders(state, world.options.norman_count.value)
+ )
+
+ # Route 104
+ set_rule(
+ get_entrance("REGION_ROUTE104/SOUTH -> REGION_ROUTE104/SOUTH_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE104/NORTH -> REGION_ROUTE104/NORTH_POND"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE104/NORTH -> REGION_ROUTE104/TREE_ALCOVE_2"),
+ hm_rules["HM01 Cut"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN"),
+ lambda state: state.has("EVENT_TALK_TO_MR_STONE", world.player)
+ )
+
+ # Petalburg Woods
+ set_rule(
+ get_entrance("REGION_PETALBURG_WOODS/WEST_PATH -> REGION_PETALBURG_WOODS/EAST_PATH"),
+ hm_rules["HM01 Cut"]
+ )
+
+ # Rustboro City
+ set_rule(
+ get_location("EVENT_RETURN_DEVON_GOODS"),
+ lambda state: state.has("EVENT_RECOVER_DEVON_GOODS", world.player)
+ )
+ if world.options.trainersanity:
+ set_rule(
+ get_location("TRAINER_BRENDAN_RUSTBORO_MUDKIP_REWARD"),
+ lambda state: state.has("EVENT_RETURN_DEVON_GOODS", world.player)
+ )
+
+ # Devon Corp
+ set_rule(
+ get_entrance("MAP_RUSTBORO_CITY_DEVON_CORP_1F:2/MAP_RUSTBORO_CITY_DEVON_CORP_2F:0"),
+ lambda state: state.has("EVENT_RETURN_DEVON_GOODS", world.player)
+ )
+
+ # Route 116
+ set_rule(
+ get_entrance("REGION_ROUTE116/WEST -> REGION_ROUTE116/WEST_ABOVE_LEDGE"),
+ hm_rules["HM01 Cut"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE116/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("TERRA_CAVE_ROUTE_116_1", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE116/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("TERRA_CAVE_ROUTE_116_2", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+
+ # Rusturf Tunnel
+ set_rule(
+ get_entrance("REGION_RUSTURF_TUNNEL/WEST -> REGION_RUSTURF_TUNNEL/EAST"),
+ hm_rules["HM06 Rock Smash"]
+ )
+ set_rule(
+ get_entrance("REGION_RUSTURF_TUNNEL/EAST -> REGION_RUSTURF_TUNNEL/WEST"),
+ hm_rules["HM06 Rock Smash"]
+ )
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_HM_STRENGTH"),
+ hm_rules["HM06 Rock Smash"]
+ )
+ set_rule(
+ get_location("EVENT_RECOVER_DEVON_GOODS"),
+ lambda state: state.has("EVENT_DEFEAT_ROXANNE", world.player)
+ )
+
+ # Route 115
+ set_rule(
+ get_entrance("REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SEA"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE -> REGION_ROUTE115/SEA"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE115/SOUTH_ABOVE_LEDGE -> REGION_ROUTE115/SOUTH_BEHIND_ROCK"),
+ hm_rules["HM06 Rock Smash"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_ROUTE115/SEA"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_ROUTE115/NORTH_ABOVE_SLOPE"),
+ lambda state: has_mach_bike(state)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("TERRA_CAVE_ROUTE_115_1", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE115/NORTH_ABOVE_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("TERRA_CAVE_ROUTE_115_2", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+
+ if world.options.extra_boulders:
+ set_rule(
+ get_entrance("REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"),
+ hm_rules["HM04 Strength"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE115/SOUTH_ABOVE_LEDGE -> REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE"),
+ hm_rules["HM04 Strength"]
+ )
+
+ if world.options.extra_bumpy_slope:
+ set_rule(
+ get_entrance("REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"),
+ lambda state: has_acro_bike(state)
+ )
+ else:
+ set_rule(
+ get_entrance("REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"),
+ lambda state: False
+ )
+
+ # Route 105
+ set_rule(
+ get_entrance("REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
+ lambda state: hm_rules["HM08 Dive"](state) and \
+ state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("MARINE_CAVE_ROUTE_105_1", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
+ lambda state: hm_rules["HM08 Dive"](state) and \
+ state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("MARINE_CAVE_ROUTE_105_2", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+ set_rule(
+ get_entrance("MAP_ROUTE105:0/MAP_ISLAND_CAVE:0"),
+ lambda state: state.has("EVENT_UNDO_REGI_SEAL", world.player)
+ )
+
+ # Route 106
+ set_rule(
+ get_entrance("REGION_ROUTE106/EAST -> REGION_ROUTE106/SEA"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE106/WEST -> REGION_ROUTE106/SEA"),
+ hm_rules["HM03 Surf"]
+ )
+
+ # Dewford Town
+ set_rule(
+ get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH"),
+ lambda state:
+ state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player)
+ and state.has("EVENT_TALK_TO_MR_STONE", world.player)
+ and state.has("EVENT_DELIVER_LETTER", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN"),
+ lambda state:
+ state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player)
+ and state.has("EVENT_TALK_TO_MR_STONE", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_DEWFORD_TOWN/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+
+ # Granite Cave
+ set_rule(
+ get_entrance("REGION_GRANITE_CAVE_STEVENS_ROOM/MAIN -> REGION_GRANITE_CAVE_STEVENS_ROOM/LETTER_DELIVERED"),
+ lambda state: state.has("Letter", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_GRANITE_CAVE_B1F/LOWER -> REGION_GRANITE_CAVE_B1F/UPPER"),
+ lambda state: has_mach_bike(state)
+ )
+
+ # Route 107
+ set_rule(
+ get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE107/MAIN"),
+ hm_rules["HM03 Surf"]
+ )
+
+ # Route 109
+ set_rule(
+ get_entrance("REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN"),
+ lambda state:
+ state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player)
+ and state.can_reach("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH", "Entrance", world.player)
+ and state.has("EVENT_TALK_TO_MR_STONE", world.player)
+ and state.has("EVENT_DELIVER_LETTER", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE109/BEACH -> REGION_ROUTE109/SEA"),
+ hm_rules["HM03 Surf"]
+ )
+
+ # Slateport City
+ set_rule(
+ get_entrance("REGION_SLATEPORT_CITY/MAIN -> REGION_SLATEPORT_CITY/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_location("EVENT_TALK_TO_DOCK"),
+ lambda state: state.has("Devon Goods", world.player)
+ )
+ set_rule(
+ get_entrance("MAP_SLATEPORT_CITY:5,7/MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:0,1"),
+ lambda state: state.has("EVENT_TALK_TO_DOCK", world.player)
+ )
+ set_rule(
+ get_location("EVENT_AQUA_STEALS_SUBMARINE"),
+ lambda state: state.has("EVENT_RELEASE_GROUDON", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SLATEPORT_CITY_HARBOR/MAIN -> REGION_SS_TIDAL_CORRIDOR/MAIN"),
+ lambda state: state.has("S.S. Ticket", world.player)
+ )
+
+ # Route 110
+ set_rule(
+ get_entrance("REGION_ROUTE110/MAIN -> REGION_ROUTE110/SOUTH_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE110/MAIN -> REGION_ROUTE110/NORTH_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE/WEST -> REGION_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE/EAST"),
+ lambda state: has_acro_bike(state) or has_mach_bike(state)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE/WEST -> REGION_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE/EAST"),
+ lambda state: has_acro_bike(state) or has_mach_bike(state)
+ )
+ if "Route 110 Aqua Grunts" not in world.options.remove_roadblocks.value:
+ set_rule(
+ get_entrance("REGION_ROUTE110/SOUTH -> REGION_ROUTE110/MAIN"),
+ lambda state: state.has("EVENT_RESCUE_CAPT_STERN", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE110/MAIN -> REGION_ROUTE110/SOUTH"),
+ lambda state: state.has("EVENT_RESCUE_CAPT_STERN", world.player)
+ )
+
+ # Trick House
+ set_rule(
+ get_entrance("REGION_ROUTE110_TRICK_HOUSE_PUZZLE1/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE1/REWARDS"),
+ hm_rules["HM01 Cut"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE2/ENTRANCE"),
+ lambda state: state.has("Dynamo Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_1", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/ENTRANCE"),
+ lambda state: state.has("Heat Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_2", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/REWARDS"),
+ hm_rules["HM06 Rock Smash"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/ENTRANCE"),
+ lambda state: state.has("Balance Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_3", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/REWARDS"),
+ hm_rules["HM04 Strength"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE5/ENTRANCE"),
+ lambda state: state.has("Feather Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_4", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE6/ENTRANCE"),
+ lambda state: state.has("Mind Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_5", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE7/ENTRANCE"),
+ lambda state: state.has("Rain Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_6", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE8/ENTRANCE"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_7", world.player)
+ )
+
+ # Mauville City
+ set_rule(
+ get_location("NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON"),
+ lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player)
+ )
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_COIN_CASE"),
+ lambda state: state.has("EVENT_BUY_HARBOR_MAIL", world.player)
+ )
+
+ # Route 117
+ set_rule(
+ get_entrance("REGION_ROUTE117/MAIN -> REGION_ROUTE117/PONDS"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_location("EVENT_ENCOUNTER_LATIOS"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ )
+
+ # Route 111
+ set_rule(
+ get_entrance("REGION_ROUTE111/MIDDLE -> REGION_ROUTE111/DESERT"),
+ lambda state: state.has("Go Goggles", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE111/NORTH -> REGION_ROUTE111/DESERT"),
+ lambda state: state.has("Go Goggles", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE111/NORTH -> REGION_ROUTE111/ABOVE_SLOPE"),
+ has_mach_bike
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE111/MIDDLE -> REGION_ROUTE111/SOUTH"),
+ hm_rules["HM06 Rock Smash"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE111/SOUTH -> REGION_ROUTE111/SOUTH_POND"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE111/SOUTH -> REGION_ROUTE111/MIDDLE"),
+ hm_rules["HM06 Rock Smash"]
+ )
+ set_rule(
+ get_entrance("MAP_ROUTE111:4/MAP_TRAINER_HILL_ENTRANCE:0"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ )
+ set_rule(
+ get_entrance("MAP_ROUTE111:1/MAP_DESERT_RUINS:0"),
+ lambda state: state.has("EVENT_UNDO_REGI_SEAL", world.player)
+ )
+ set_rule(
+ get_entrance("MAP_DESERT_RUINS:0/MAP_ROUTE111:1"),
+ hm_rules["HM06 Rock Smash"]
+ )
+
+ # Route 112
+ if "Route 112 Magma Grunts" not in world.options.remove_roadblocks.value:
+ set_rule(
+ get_entrance("REGION_ROUTE112/SOUTH_EAST -> REGION_ROUTE112/CABLE_CAR_STATION_ENTRANCE"),
+ lambda state: state.has("EVENT_MAGMA_STEALS_METEORITE", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE112/CABLE_CAR_STATION_ENTRANCE -> REGION_ROUTE112/SOUTH_EAST"),
+ lambda state: state.has("EVENT_MAGMA_STEALS_METEORITE", world.player)
+ )
+
+ # Fiery Path
+ set_rule(
+ get_entrance("REGION_FIERY_PATH/MAIN -> REGION_FIERY_PATH/BEHIND_BOULDER"),
+ hm_rules["HM04 Strength"]
+ )
+
+ # Route 114
+ set_rule(
+ get_entrance("REGION_ROUTE114/MAIN -> REGION_ROUTE114/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE114/WATER -> REGION_ROUTE114/ABOVE_WATERFALL"),
+ hm_rules["HM07 Waterfall"]
+ )
+ set_rule(
+ get_entrance("MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:2/MAP_DESERT_UNDERPASS:0"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE114/ABOVE_WATERFALL -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("TERRA_CAVE_ROUTE_114_1", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE114/MAIN -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("TERRA_CAVE_ROUTE_114_2", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+
+ # Meteor Falls
+ set_rule(
+ get_entrance("REGION_METEOR_FALLS_1F_1R/MAIN -> REGION_METEOR_FALLS_1F_1R/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_METEOR_FALLS_1F_1R/WATER -> REGION_METEOR_FALLS_1F_1R/WATER_ABOVE_WATERFALL"),
+ hm_rules["HM07 Waterfall"]
+ )
+ set_rule(
+ get_entrance("REGION_METEOR_FALLS_1F_1R/ABOVE_WATERFALL -> REGION_METEOR_FALLS_1F_1R/WATER_ABOVE_WATERFALL"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("MAP_METEOR_FALLS_1F_1R:5/MAP_METEOR_FALLS_STEVENS_CAVE:0"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_METEOR_FALLS_1F_2R/LEFT_SPLIT -> REGION_METEOR_FALLS_1F_2R/LEFT_SPLIT_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_METEOR_FALLS_1F_2R/RIGHT_SPLIT -> REGION_METEOR_FALLS_1F_2R/RIGHT_SPLIT_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_METEOR_FALLS_B1F_1R/HIGHEST_LADDER -> REGION_METEOR_FALLS_B1F_1R/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_METEOR_FALLS_B1F_1R/NORTH_SHORE -> REGION_METEOR_FALLS_B1F_1R/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_METEOR_FALLS_B1F_1R/SOUTH_SHORE -> REGION_METEOR_FALLS_B1F_1R/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_METEOR_FALLS_B1F_2R/ENTRANCE -> REGION_METEOR_FALLS_B1F_2R/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+
+ # Jagged Pass
+ set_rule(
+ get_entrance("REGION_JAGGED_PASS/BOTTOM -> REGION_JAGGED_PASS/MIDDLE"),
+ lambda state: has_acro_bike(state)
+ )
+ set_rule(
+ get_entrance("REGION_JAGGED_PASS/MIDDLE -> REGION_JAGGED_PASS/TOP"),
+ lambda state: has_acro_bike(state)
+ )
+ set_rule(
+ get_entrance("MAP_JAGGED_PASS:4/MAP_MAGMA_HIDEOUT_1F:0"),
+ lambda state: state.has("Magma Emblem", world.player)
+ )
+
+ # Lavaridge Town
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_GO_GOGGLES"),
+ lambda state: state.has("EVENT_DEFEAT_FLANNERY", world.player)
+ )
+
+ # Mirage Tower
+ set_rule(
+ get_entrance("REGION_MIRAGE_TOWER_2F/TOP -> REGION_MIRAGE_TOWER_2F/BOTTOM"),
+ lambda state: has_mach_bike(state)
+ )
+ set_rule(
+ get_entrance("REGION_MIRAGE_TOWER_2F/BOTTOM -> REGION_MIRAGE_TOWER_2F/TOP"),
+ lambda state: has_mach_bike(state)
+ )
+ set_rule(
+ get_entrance("REGION_MIRAGE_TOWER_3F/TOP -> REGION_MIRAGE_TOWER_3F/BOTTOM"),
+ hm_rules["HM06 Rock Smash"]
+ )
+ set_rule(
+ get_entrance("REGION_MIRAGE_TOWER_3F/BOTTOM -> REGION_MIRAGE_TOWER_3F/TOP"),
+ hm_rules["HM06 Rock Smash"]
+ )
+ set_rule(
+ get_entrance("REGION_MIRAGE_TOWER_4F/MAIN -> REGION_MIRAGE_TOWER_4F/FOSSIL_PLATFORM"),
+ hm_rules["HM06 Rock Smash"]
+ )
+
+ # Abandoned Ship
+ set_rule(
+ get_entrance("REGION_ABANDONED_SHIP_ROOMS_B1F/CENTER -> REGION_ABANDONED_SHIP_UNDERWATER1/MAIN"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS/MAIN -> REGION_ABANDONED_SHIP_UNDERWATER2/MAIN"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:0/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:0"),
+ lambda state: state.has("Room 1 Key", world.player)
+ )
+ set_rule(
+ get_entrance("MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:1/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:2"),
+ lambda state: state.has("Room 2 Key", world.player)
+ )
+ set_rule(
+ get_entrance("MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:3/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:6"),
+ lambda state: state.has("Room 4 Key", world.player)
+ )
+ set_rule(
+ get_entrance("MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:5/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:8"),
+ lambda state: state.has("Room 6 Key", world.player)
+ )
+ set_rule(
+ get_entrance("MAP_ABANDONED_SHIP_CORRIDORS_B1F:5/MAP_ABANDONED_SHIP_ROOM_B1F:0"),
+ lambda state: state.has("Storage Key", world.player)
+ )
+
+ # New Mauville
+ set_rule(
+ get_entrance("MAP_NEW_MAUVILLE_ENTRANCE:1/MAP_NEW_MAUVILLE_INSIDE:0"),
+ lambda state: state.has("Basement Key", world.player)
+ )
+
+ # Route 118
+ if world.options.modify_118:
+ set_rule(
+ get_entrance("REGION_ROUTE118/WEST -> REGION_ROUTE118/EAST"),
+ has_acro_bike
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE118/EAST -> REGION_ROUTE118/WEST"),
+ has_acro_bike
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE118/WEST_WATER -> REGION_ROUTE118/EAST_WATER"),
+ lambda state: False
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE118/EAST_WATER -> REGION_ROUTE118/WEST_WATER"),
+ lambda state: False
+ )
+ else:
+ set_rule(
+ get_entrance("REGION_ROUTE118/WEST -> REGION_ROUTE118/EAST"),
+ lambda state: False
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE118/EAST -> REGION_ROUTE118/WEST"),
+ lambda state: False
+ )
+
+ set_rule(
+ get_entrance("REGION_ROUTE118/WEST -> REGION_ROUTE118/WEST_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE118/EAST -> REGION_ROUTE118/EAST_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE118/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("TERRA_CAVE_ROUTE_118_1", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE118/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("TERRA_CAVE_ROUTE_118_2", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+
+ # Route 119
+ set_rule(
+ get_entrance("REGION_ROUTE119/LOWER -> REGION_ROUTE119/LOWER_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE119/LOWER -> REGION_ROUTE119/LOWER_ACROSS_RAILS"),
+ lambda state: has_acro_bike(state)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE119/LOWER_ACROSS_RAILS -> REGION_ROUTE119/LOWER"),
+ lambda state: has_acro_bike(state)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE119/UPPER -> REGION_ROUTE119/MIDDLE_RIVER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE119/MIDDLE_RIVER -> REGION_ROUTE119/ABOVE_WATERFALL"),
+ hm_rules["HM07 Waterfall"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE119/ABOVE_WATERFALL -> REGION_ROUTE119/MIDDLE_RIVER"),
+ hm_rules["HM07 Waterfall"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE119/ABOVE_WATERFALL -> REGION_ROUTE119/ABOVE_WATERFALL_ACROSS_RAILS"),
+ lambda state: has_acro_bike(state)
+ )
+ if "Route 119 Aqua Grunts" not in world.options.remove_roadblocks.value:
+ set_rule(
+ get_entrance("REGION_ROUTE119/MIDDLE -> REGION_ROUTE119/UPPER"),
+ lambda state: state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE119/UPPER -> REGION_ROUTE119/MIDDLE"),
+ lambda state: state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+
+ # Fortree City
+ set_rule(
+ get_entrance("REGION_FORTREE_CITY/MAIN -> REGION_FORTREE_CITY/BEFORE_GYM"),
+ lambda state: state.has("Devon Scope", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_FORTREE_CITY/BEFORE_GYM -> REGION_FORTREE_CITY/MAIN"),
+ lambda state: state.has("Devon Scope", world.player)
+ )
+
+ # Route 120
+ set_rule(
+ get_entrance("REGION_ROUTE120/NORTH -> REGION_ROUTE120/NORTH_POND_SHORE"),
+ lambda state: state.has("Devon Scope", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE120/NORTH_POND_SHORE -> REGION_ROUTE120/NORTH"),
+ lambda state: state.has("Devon Scope", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE120/NORTH_POND_SHORE -> REGION_ROUTE120/NORTH_POND"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE120/SOUTH -> REGION_ROUTE120/SOUTH_ALCOVE"),
+ hm_rules["HM01 Cut"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE120/SOUTH -> REGION_ROUTE120/SOUTH_PONDS"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE120/SOUTH_ALCOVE -> REGION_ROUTE120/SOUTH"),
+ hm_rules["HM01 Cut"]
+ )
+ set_rule(
+ get_entrance("MAP_ROUTE120:0/MAP_ANCIENT_TOMB:0"),
+ lambda state: state.has("EVENT_UNDO_REGI_SEAL", world.player)
+ )
+ set_rule(
+ get_entrance("MAP_ANCIENT_TOMB:1/MAP_ANCIENT_TOMB:2"),
+ hm_rules["HM05 Flash"]
+ )
+
+ # Route 121
+ set_rule(
+ get_entrance("REGION_ROUTE121/EAST -> REGION_ROUTE121/WEST"),
+ hm_rules["HM01 Cut"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE121/EAST -> REGION_ROUTE121/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+
+ # Safari Zone
+ set_rule(
+ get_entrance("MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:0,1/MAP_SAFARI_ZONE_SOUTH:0"),
+ lambda state: state.has("Pokeblock Case", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SAFARI_ZONE_NORTHWEST/MAIN -> REGION_SAFARI_ZONE_NORTHWEST/POND"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SAFARI_ZONE_SOUTH/MAIN -> REGION_SAFARI_ZONE_NORTH/MAIN"),
+ lambda state: has_acro_bike(state)
+ )
+ set_rule(
+ get_entrance("REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_NORTHWEST/MAIN"),
+ lambda state: has_mach_bike(state)
+ )
+ set_rule(
+ get_entrance("REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_SOUTHWEST/POND"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SAFARI_ZONE_SOUTHEAST/MAIN -> REGION_SAFARI_ZONE_SOUTHEAST/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ if "Safari Zone Construction Workers" not in world.options.remove_roadblocks.value:
+ set_rule(
+ get_entrance("REGION_SAFARI_ZONE_SOUTH/MAIN -> REGION_SAFARI_ZONE_SOUTHEAST/MAIN"),
+ lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
+ )
+
+ # Route 122
+ set_rule(
+ get_entrance("REGION_ROUTE122/MT_PYRE_ENTRANCE -> REGION_ROUTE122/SEA"),
+ hm_rules["HM03 Surf"]
+ )
+
+ # Route 123
+ set_rule(
+ get_entrance("REGION_ROUTE123/EAST -> REGION_ROUTE122/SEA"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE123/EAST -> REGION_ROUTE123/POND"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE123/EAST -> REGION_ROUTE123/EAST_BEHIND_TREE"),
+ hm_rules["HM01 Cut"]
+ )
+
+ # Lilycove City
+ set_rule(
+ get_entrance("REGION_LILYCOVE_CITY/MAIN -> REGION_LILYCOVE_CITY/SEA"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_SS_TIDAL_CORRIDOR/MAIN"),
+ lambda state: state.has("S.S. Ticket", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_SOUTHERN_ISLAND_EXTERIOR/MAIN"),
+ lambda state: state.has("Eon Ticket", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_FARAWAY_ISLAND_ENTRANCE/MAIN"),
+ lambda state: state.has("Old Sea Map", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_BIRTH_ISLAND_HARBOR/MAIN"),
+ lambda state: state.has("Aurora Ticket", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_NAVEL_ROCK_HARBOR/MAIN"),
+ lambda state: state.has("Mystic Ticket", world.player)
+ )
+
+ if "Lilycove City Wailmer" not in world.options.remove_roadblocks.value:
+ set_rule(
+ get_entrance("REGION_LILYCOVE_CITY/SEA -> REGION_ROUTE124/MAIN"),
+ lambda state: state.has("EVENT_CLEAR_AQUA_HIDEOUT", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE124/MAIN -> REGION_LILYCOVE_CITY/SEA"),
+ lambda state: state.has("EVENT_CLEAR_AQUA_HIDEOUT", world.player)
+ )
+
+ # Magma Hideout
+ set_rule(
+ get_entrance("REGION_MAGMA_HIDEOUT_1F/ENTRANCE -> REGION_MAGMA_HIDEOUT_1F/MAIN"),
+ hm_rules["HM04 Strength"]
+ )
+ set_rule(
+ get_entrance("REGION_MAGMA_HIDEOUT_1F/MAIN -> REGION_MAGMA_HIDEOUT_1F/ENTRANCE"),
+ hm_rules["HM04 Strength"]
+ )
+
+ # Aqua Hideout
+ if "Aqua Hideout Grunts" not in world.options.remove_roadblocks.value:
+ set_rule(
+ get_entrance("REGION_AQUA_HIDEOUT_1F/WATER -> REGION_AQUA_HIDEOUT_1F/MAIN"),
+ lambda state: state.has("EVENT_AQUA_STEALS_SUBMARINE", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_AQUA_HIDEOUT_1F/MAIN -> REGION_AQUA_HIDEOUT_1F/WATER"),
+ lambda state: hm_rules["HM03 Surf"](state) and state.has("EVENT_AQUA_STEALS_SUBMARINE", world.player)
+ )
+
+ # Route 124
+ set_rule(
+ get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/BIG_AREA"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_1"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_2"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_3"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_2"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_4"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE124/NORTH_ENCLOSED_AREA_1 -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE124/NORTH_ENCLOSED_AREA_2 -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE124/NORTH_ENCLOSED_AREA_3 -> REGION_UNDERWATER_ROUTE124/TUNNEL_2"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE124/SOUTH_ENCLOSED_AREA_1 -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE124/SOUTH_ENCLOSED_AREA_2 -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE124/SOUTH_ENCLOSED_AREA_3 -> REGION_UNDERWATER_ROUTE124/TUNNEL_4"),
+ hm_rules["HM08 Dive"]
+ )
+
+ # Mossdeep City
+ set_rule(
+ get_entrance("REGION_MOSSDEEP_CITY/MAIN -> REGION_MOSSDEEP_CITY/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE124/MAIN"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE125/SEA"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE127/MAIN"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_location("EVENT_DEFEAT_MAXIE_AT_SPACE_STATION"),
+ lambda state: state.has("EVENT_DEFEAT_TATE_AND_LIZA", world.player)
+ )
+ set_rule(
+ get_location("EVENT_STEVEN_GIVES_DIVE"),
+ lambda state: state.has("EVENT_DEFEAT_MAXIE_AT_SPACE_STATION", world.player)
+ )
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_HM_DIVE"),
+ lambda state: state.has("EVENT_DEFEAT_MAXIE_AT_SPACE_STATION", world.player)
+ )
+
+ # Route 125
+ set_rule(
+ get_entrance("REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
+ lambda state: hm_rules["HM08 Dive"](state) and \
+ state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("MARINE_CAVE_ROUTE_125_1", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
+ lambda state: hm_rules["HM08 Dive"](state) and \
+ state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("MARINE_CAVE_ROUTE_125_2", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+
+ # Shoal Cave
+ set_rule(
+ get_entrance("REGION_SHOAL_CAVE_ENTRANCE_ROOM/SOUTH -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SHOAL_CAVE_ENTRANCE_ROOM/NORTH_WEST_CORNER -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SHOAL_CAVE_ENTRANCE_ROOM/NORTH_EAST_CORNER -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/EAST_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/NORTH_WEST_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_WEST_CORNER -> REGION_SHOAL_CAVE_INNER_ROOM/NORTH_WEST_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/RARE_CANDY_PLATFORM -> REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/NORTH_WEST -> REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/EAST"),
+ hm_rules["HM04 Strength"]
+ )
+ set_rule(
+ get_entrance("REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/EAST -> REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/NORTH_WEST"),
+ hm_rules["HM04 Strength"]
+ )
+
+ # Route 126
+ set_rule(
+ get_entrance("REGION_ROUTE126/MAIN -> REGION_UNDERWATER_ROUTE126/MAIN"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE126/MAIN -> REGION_UNDERWATER_ROUTE126/SMALL_AREA_2"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE126/NEAR_ROUTE_124 -> REGION_UNDERWATER_ROUTE126/TUNNEL"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE126/NORTH_WEST_CORNER -> REGION_UNDERWATER_ROUTE126/TUNNEL"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE126/WEST -> REGION_UNDERWATER_ROUTE126/MAIN"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE126/WEST -> REGION_UNDERWATER_ROUTE126/SMALL_AREA_1"),
+ hm_rules["HM08 Dive"]
+ )
+
+ # Sootopolis City
+ set_rule(
+ get_entrance("REGION_SOOTOPOLIS_CITY/WATER -> REGION_UNDERWATER_SOOTOPOLIS_CITY/MAIN"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_SOOTOPOLIS_CITY/EAST -> REGION_SOOTOPOLIS_CITY/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SOOTOPOLIS_CITY/WEST -> REGION_SOOTOPOLIS_CITY/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SOOTOPOLIS_CITY/ISLAND -> REGION_SOOTOPOLIS_CITY/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("MAP_SOOTOPOLIS_CITY:3/MAP_CAVE_OF_ORIGIN_ENTRANCE:0"),
+ lambda state: state.has("EVENT_RELEASE_KYOGRE", world.player)
+ )
+ set_rule(
+ get_entrance("MAP_SOOTOPOLIS_CITY:2/MAP_SOOTOPOLIS_CITY_GYM_1F:0"),
+ lambda state: state.has("EVENT_RAYQUAZA_STOPS_FIGHT", world.player)
+ )
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_HM_WATERFALL"),
+ lambda state: state.has("EVENT_RAYQUAZA_STOPS_FIGHT", world.player)
+ )
+ set_rule(
+ get_location("EVENT_RAYQUAZA_STOPS_FIGHT"),
+ lambda state: state.has("EVENT_RELEASE_KYOGRE", world.player)
+ )
+
+ # Route 127
+ set_rule(
+ get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/MAIN"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/TUNNEL"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_1"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_2"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_3"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE127/ENCLOSED_AREA -> REGION_UNDERWATER_ROUTE127/TUNNEL"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
+ lambda state: hm_rules["HM08 Dive"](state) and \
+ state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("MARINE_CAVE_ROUTE_127_1", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
+ lambda state: hm_rules["HM08 Dive"](state) and \
+ state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("MARINE_CAVE_ROUTE_127_2", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+
+ # Route 128
+ set_rule(
+ get_entrance("REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/MAIN"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/AREA_1"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/AREA_2"),
+ hm_rules["HM08 Dive"]
+ )
+
+ # Seafloor Cavern
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ENTRANCE/MAIN -> REGION_SEAFLOOR_CAVERN_ENTRANCE/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ENTRANCE/WATER -> REGION_UNDERWATER_SEAFLOOR_CAVERN/MAIN"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM1/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM1/NORTH"),
+ lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM1/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM1/SOUTH"),
+ hm_rules["HM04 Strength"]
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST"),
+ hm_rules["HM04 Strength"]
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST"),
+ hm_rules["HM04 Strength"]
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST"),
+ hm_rules["HM06 Rock Smash"]
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST"),
+ hm_rules["HM06 Rock Smash"]
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_EAST"),
+ lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST"),
+ lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/EAST"),
+ lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM5/EAST -> REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST"),
+ hm_rules["HM04 Strength"]
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/SOUTH_WEST"),
+ lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM5/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST"),
+ lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM6/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM6/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM6/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM6/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM7/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM7/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM7/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM7/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM8/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM8/SOUTH"),
+ hm_rules["HM04 Strength"]
+ )
+ set_rule(
+ get_entrance("REGION_SEAFLOOR_CAVERN_ROOM8/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM8/NORTH"),
+ hm_rules["HM04 Strength"]
+ )
+ if "Seafloor Cavern Aqua Grunt" not in world.options.remove_roadblocks.value:
+ set_rule(
+ get_entrance("MAP_SEAFLOOR_CAVERN_ENTRANCE:1/MAP_SEAFLOOR_CAVERN_ROOM1:0"),
+ lambda state: state.has("EVENT_STEVEN_GIVES_DIVE", world.player)
+ )
+
+ # Route 129
+ set_rule(
+ get_entrance("REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
+ lambda state: hm_rules["HM08 Dive"](state) and \
+ state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("MARINE_CAVE_ROUTE_129_1", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
+ lambda state: hm_rules["HM08 Dive"](state) and \
+ state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
+ state.has("MARINE_CAVE_ROUTE_129_2", world.player) and \
+ state.has("EVENT_DEFEAT_SHELLY", world.player)
+ )
+
+ # Pacifidlog Town
+ set_rule(
+ get_entrance("REGION_PACIFIDLOG_TOWN/MAIN -> REGION_PACIFIDLOG_TOWN/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_PACIFIDLOG_TOWN/MAIN -> REGION_ROUTE131/MAIN"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_PACIFIDLOG_TOWN/MAIN -> REGION_ROUTE132/EAST"),
+ hm_rules["HM03 Surf"]
+ )
+
+ # Sky Pillar
+ set_rule(
+ get_entrance("MAP_SKY_PILLAR_OUTSIDE:1/MAP_SKY_PILLAR_1F:0"),
+ lambda state: state.has("EVENT_RELEASE_KYOGRE", world.player)
+ )
+ add_rule(
+ get_location("EVENT_ENCOUNTER_RAYQUAZA"),
+ lambda state: state.has("EVENT_RAYQUAZA_STOPS_FIGHT", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SKY_PILLAR_2F/RIGHT -> REGION_SKY_PILLAR_2F/LEFT"),
+ has_mach_bike
+ )
+ set_rule(
+ get_entrance("REGION_SKY_PILLAR_2F/LEFT -> REGION_SKY_PILLAR_2F/RIGHT"),
+ has_mach_bike
+ )
+ set_rule(
+ get_entrance("REGION_SKY_PILLAR_4F/MAIN -> REGION_SKY_PILLAR_4F/ABOVE_3F_TOP_CENTER"),
+ has_mach_bike
+ )
+
+ # Route 134
+ set_rule(
+ get_entrance("REGION_ROUTE134/MAIN -> REGION_UNDERWATER_ROUTE134/MAIN"),
+ hm_rules["HM08 Dive"]
+ )
+ set_rule(
+ get_location("EVENT_UNDO_REGI_SEAL"),
+ lambda state: state.has("CATCH_SPECIES_WAILORD", world.player) and state.has("CATCH_SPECIES_RELICANTH", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_SEALED_CHAMBER_OUTER_ROOM/MAIN -> REGION_SEALED_CHAMBER_OUTER_ROOM/CRUMBLED_WALL"),
+ lambda state: state.has("EVENT_MOVE_TUTOR_DIG", world.player)
+ )
+
+ # Ever Grande City
+ set_rule(
+ get_entrance("REGION_EVER_GRANDE_CITY/SEA -> REGION_EVER_GRANDE_CITY/SOUTH"),
+ hm_rules["HM07 Waterfall"]
+ )
+ set_rule(
+ get_entrance("REGION_EVER_GRANDE_CITY/SOUTH -> REGION_EVER_GRANDE_CITY/SEA"),
+ hm_rules["HM03 Surf"]
+ )
+
+ # Victory Road
+ set_rule(
+ get_entrance("REGION_VICTORY_ROAD_B1F/SOUTH_WEST_MAIN -> REGION_VICTORY_ROAD_B1F/SOUTH_WEST_LADDER_UP"),
+ lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
+ )
+ set_rule(
+ get_entrance("REGION_VICTORY_ROAD_B1F/SOUTH_WEST_LADDER_UP -> REGION_VICTORY_ROAD_B1F/SOUTH_WEST_MAIN"),
+ lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
+ )
+ set_rule(
+ get_entrance("REGION_VICTORY_ROAD_B1F/MAIN_UPPER -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST"),
+ lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
+ )
+ set_rule(
+ get_entrance("REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST"),
+ hm_rules["HM06 Rock Smash"]
+ )
+ set_rule(
+ get_entrance("REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST"),
+ lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
+ )
+ set_rule(
+ get_entrance("REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST -> REGION_VICTORY_ROAD_B1F/MAIN_UPPER"),
+ lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
+ )
+ set_rule(
+ get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_WEST -> REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_WEST_ISLAND -> REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_EAST -> REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"),
+ hm_rules["HM07 Waterfall"]
+ )
+ set_rule(
+ get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"),
+ hm_rules["HM07 Waterfall"]
+ )
+ set_rule(
+ get_entrance("REGION_VICTORY_ROAD_B2F/UPPER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_VICTORY_ROAD_B2F/UPPER -> REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER"),
+ hm_rules["HM03 Surf"]
+ )
+
+ # Pokemon League
+ if world.options.elite_four_requirement == EliteFourRequirement.option_badges:
+ set_rule(
+ get_entrance("REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN -> REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"),
+ lambda state: state.has_group("Badges", world.player, world.options.elite_four_count.value)
+ )
+ else:
+ set_rule(
+ get_entrance("REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN -> REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"),
+ lambda state: defeated_n_gym_leaders(state, world.options.elite_four_count.value)
+ )
+
+ # Battle Frontier
+ set_rule(
+ get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_WEST/DOCK -> REGION_SS_TIDAL_CORRIDOR/MAIN"),
+ lambda state: state.has("S.S. Ticket", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_WEST/CAVE_ENTRANCE -> REGION_BATTLE_FRONTIER_OUTSIDE_WEST/WATER"),
+ hm_rules["HM03 Surf"]
+ )
+ set_rule(
+ get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL"),
+ lambda state: state.has("Wailmer Pail", world.player) and hm_rules["HM03 Surf"](state)
+ )
+ set_rule(
+ get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN"),
+ lambda state: state.has("Wailmer Pail", world.player)
+ )
+ set_rule(
+ get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_EAST/WATER -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL"),
+ hm_rules["HM07 Waterfall"]
+ )
+
+ # Pokedex Rewards
+ if world.options.dexsanity:
+ for i in range(NUM_REAL_SPECIES):
+ species = data.species[NATIONAL_ID_TO_SPECIES_ID[i + 1]]
+
+ if species.species_id in world.blacklisted_wilds:
+ continue
+
+ set_rule(
+ get_location(f"Pokedex - {species.label}"),
+ lambda state, species_name=species.name: state.has(f"CATCH_{species_name}", world.player)
+ )
+
+ # Legendary hunt prevents Latios from being a wild spawn so the roamer
+ # can be tracked, and also guarantees that the roamer is a Latios.
+ if world.options.goal == Goal.option_legendary_hunt and \
+ data.constants["SPECIES_LATIOS"] not in world.blacklisted_wilds:
+ set_rule(
+ get_location(f"Pokedex - Latios"),
+ lambda state: state.has("EVENT_ENCOUNTER_LATIOS", world.player)
+ )
+
+ # Overworld Items
+ if world.options.overworld_items:
+ # Route 117
+ set_rule(
+ get_location("ITEM_ROUTE_117_REVIVE"),
+ hm_rules["HM01 Cut"]
+ )
+
+ # Route 114
+ set_rule(
+ get_location("ITEM_ROUTE_114_PROTEIN"),
+ hm_rules["HM06 Rock Smash"]
+ )
+
+ # Victory Road
+ set_rule(
+ get_location("ITEM_VICTORY_ROAD_B1F_FULL_RESTORE"),
+ lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state)
+ )
+
+ # Hidden Items
+ if world.options.hidden_items:
+ # Route 120
+ set_rule(
+ get_location("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_1"),
+ hm_rules["HM01 Cut"]
+ )
+
+ # Route 121
+ set_rule(
+ get_location("HIDDEN_ITEM_ROUTE_121_NUGGET"),
+ hm_rules["HM01 Cut"]
+ )
+
+ # NPC Gifts
+ if world.options.npc_gifts:
+ # Littleroot Town
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_AMULET_COIN"),
+ lambda state: state.has("EVENT_TALK_TO_MR_STONE", world.player) and state.has("Balance Badge", world.player)
+ )
+
+ # Route 104
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_WHITE_HERB"),
+ lambda state: state.has("Dynamo Badge", world.player) and state.has("EVENT_MEET_FLOWER_SHOP_OWNER", world.player)
+ )
+
+ # Devon Corp
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_EXP_SHARE"),
+ lambda state: state.has("EVENT_DELIVER_LETTER", world.player)
+ )
+
+ # Route 116
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_REPEAT_BALL"),
+ lambda state: state.has("EVENT_RESCUE_CAPT_STERN", world.player)
+ )
+
+ # Dewford Town
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_TM_SLUDGE_BOMB"),
+ lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player)
+ )
+
+ # Slateport City
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_DEEP_SEA_TOOTH"),
+ lambda state: state.has("EVENT_AQUA_STEALS_SUBMARINE", world.player)
+ and state.has("Scanner", world.player)
+ and state.has("Mind Badge", world.player)
+ )
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_DEEP_SEA_SCALE"),
+ lambda state: state.has("EVENT_AQUA_STEALS_SUBMARINE", world.player)
+ and state.has("Scanner", world.player)
+ and state.has("Mind Badge", world.player)
+ )
+
+ # Mauville City
+ set_rule(
+ get_location("NPC_GIFT_GOT_TM_THUNDERBOLT_FROM_WATTSON"),
+ lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) and state.has("EVENT_TURN_OFF_GENERATOR", world.player)
+ )
+
+ # Fallarbor Town
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_TM_RETURN"),
+ lambda state: state.has("EVENT_RECOVER_METEORITE", world.player) and state.has("Meteorite", world.player)
+ )
+
+ # Fortree City
+ set_rule(
+ get_location("NPC_GIFT_RECEIVED_MENTAL_HERB"),
+ lambda state: state.has("EVENT_WINGULL_QUEST_2", world.player)
+ )
+
+ # Add Itemfinder requirement to hidden items
+ if world.options.require_itemfinder:
+ for location in world.multiworld.get_locations(world.player):
+ if location.tags is not None and "HiddenItem" in location.tags:
+ add_rule(
+ location,
+ lambda state: state.has("Itemfinder", world.player)
+ )
+
+ # Add Flash requirements to dark caves
+ # Granite Cave
+ if world.options.require_flash in [DarkCavesRequireFlash.option_only_granite_cave, DarkCavesRequireFlash.option_both]:
+ add_rule(
+ get_entrance("MAP_GRANITE_CAVE_1F:2/MAP_GRANITE_CAVE_B1F:1"),
+ hm_rules["HM05 Flash"]
+ )
+ add_rule(
+ get_entrance("MAP_GRANITE_CAVE_B1F:3/MAP_GRANITE_CAVE_B2F:1"),
+ hm_rules["HM05 Flash"]
+ )
+
+ # Victory Road
+ if world.options.require_flash in [DarkCavesRequireFlash.option_only_victory_road, DarkCavesRequireFlash.option_both]:
+ add_rule(
+ get_entrance("MAP_VICTORY_ROAD_1F:2/MAP_VICTORY_ROAD_B1F:5"),
+ hm_rules["HM05 Flash"]
+ )
+ add_rule(
+ get_entrance("MAP_VICTORY_ROAD_1F:4/MAP_VICTORY_ROAD_B1F:4"),
+ hm_rules["HM05 Flash"]
+ )
+ add_rule(
+ get_entrance("MAP_VICTORY_ROAD_1F:3/MAP_VICTORY_ROAD_B1F:2"),
+ hm_rules["HM05 Flash"]
+ )
+ add_rule(
+ get_entrance("MAP_VICTORY_ROAD_B1F:3/MAP_VICTORY_ROAD_B2F:1"),
+ hm_rules["HM05 Flash"]
+ )
+ add_rule(
+ get_entrance("MAP_VICTORY_ROAD_B1F:1/MAP_VICTORY_ROAD_B2F:2"),
+ hm_rules["HM05 Flash"]
+ )
+ add_rule(
+ get_entrance("MAP_VICTORY_ROAD_B1F:6/MAP_VICTORY_ROAD_B2F:3"),
+ hm_rules["HM05 Flash"]
+ )
+ add_rule(
+ get_entrance("MAP_VICTORY_ROAD_B1F:0/MAP_VICTORY_ROAD_B2F:0"),
+ hm_rules["HM05 Flash"]
+ )
+ add_rule(
+ get_entrance("MAP_VICTORY_ROAD_B2F:3/MAP_VICTORY_ROAD_B1F:6"),
+ hm_rules["HM05 Flash"]
+ )
+ add_rule(
+ get_entrance("MAP_VICTORY_ROAD_B2F:2/MAP_VICTORY_ROAD_B1F:1"),
+ hm_rules["HM05 Flash"]
+ )
+ add_rule(
+ get_entrance("MAP_VICTORY_ROAD_B2F:0/MAP_VICTORY_ROAD_B1F:0"),
+ hm_rules["HM05 Flash"]
+ )
+ add_rule(
+ get_entrance("MAP_VICTORY_ROAD_B2F:1/MAP_VICTORY_ROAD_B1F:3"),
+ hm_rules["HM05 Flash"]
+ )
diff --git a/worlds/pokemon_emerald/sanity_check.py b/worlds/pokemon_emerald/sanity_check.py
new file mode 100644
index 000000000000..24eb768bfbc5
--- /dev/null
+++ b/worlds/pokemon_emerald/sanity_check.py
@@ -0,0 +1,307 @@
+"""
+Looks through data object to double-check it makes sense. Will fail for missing or duplicate definitions or
+duplicate claims and give warnings for unused and unignored locations or warps.
+"""
+import logging
+from typing import List
+
+from .data import load_json_data, data
+
+
+_IGNORABLE_LOCATIONS = frozenset({
+ "HIDDEN_ITEM_TRICK_HOUSE_NUGGET", # Is permanently mssiable and has special behavior that sets the flag early
+
+ # Duplicate rival fights. All variations are represented by the Brandon + Mudkip version
+ "TRAINER_BRENDAN_ROUTE_103_TREECKO_REWARD",
+ "TRAINER_BRENDAN_ROUTE_103_TORCHIC_REWARD",
+ "TRAINER_MAY_ROUTE_103_MUDKIP_REWARD",
+ "TRAINER_MAY_ROUTE_103_TREECKO_REWARD",
+ "TRAINER_MAY_ROUTE_103_TORCHIC_REWARD",
+ "TRAINER_BRENDAN_ROUTE_110_TREECKO_REWARD",
+ "TRAINER_BRENDAN_ROUTE_110_TORCHIC_REWARD",
+ "TRAINER_MAY_ROUTE_110_MUDKIP_REWARD",
+ "TRAINER_MAY_ROUTE_110_TREECKO_REWARD",
+ "TRAINER_MAY_ROUTE_110_TORCHIC_REWARD",
+ "TRAINER_BRENDAN_ROUTE_119_TREECKO_REWARD",
+ "TRAINER_BRENDAN_ROUTE_119_TORCHIC_REWARD",
+ "TRAINER_MAY_ROUTE_119_MUDKIP_REWARD",
+ "TRAINER_MAY_ROUTE_119_TREECKO_REWARD",
+ "TRAINER_MAY_ROUTE_119_TORCHIC_REWARD",
+ "TRAINER_BRENDAN_RUSTBORO_TREECKO_REWARD",
+ "TRAINER_BRENDAN_RUSTBORO_TORCHIC_REWARD",
+ "TRAINER_MAY_RUSTBORO_MUDKIP_REWARD",
+ "TRAINER_MAY_RUSTBORO_TREECKO_REWARD",
+ "TRAINER_MAY_RUSTBORO_TORCHIC_REWARD",
+ "TRAINER_BRENDAN_LILYCOVE_TREECKO_REWARD",
+ "TRAINER_BRENDAN_LILYCOVE_TORCHIC_REWARD",
+ "TRAINER_MAY_LILYCOVE_MUDKIP_REWARD",
+ "TRAINER_MAY_LILYCOVE_TREECKO_REWARD",
+ "TRAINER_MAY_LILYCOVE_TORCHIC_REWARD",
+})
+
+_IGNORABLE_WARPS = frozenset({
+ # Trick House
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE2:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE2:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE3:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE3:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE4:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE4:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE5:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE5:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE6:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE6:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:10/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:9",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:11/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:12",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:12/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:11",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:3/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:4",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:4/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:3",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:5/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:6",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:6/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:5",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:7/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:8",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:8/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:7",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:9/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:10",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE8:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
+ "MAP_ROUTE110_TRICK_HOUSE_PUZZLE8:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
+
+ # Department store elevator
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0,1/MAP_DYNAMIC:-1!",
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:3/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
+ "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:1/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
+
+ # Intro truck
+ "MAP_INSIDE_OF_TRUCK:0,1,2/MAP_DYNAMIC:-1!",
+
+ # Battle Frontier
+ "MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1!",
+ "MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1!",
+ "MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2",
+ "MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2",
+ "MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0",
+ "MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:3/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0!",
+ "MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0",
+
+ # Terra Cave and Marine Cave
+ "MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!",
+ "MAP_ROUTE113:1/MAP_TERRA_CAVE_ENTRANCE:0!",
+ "MAP_ROUTE113:2/MAP_TERRA_CAVE_ENTRANCE:0!",
+ "MAP_ROUTE114:3/MAP_TERRA_CAVE_ENTRANCE:0!",
+ "MAP_ROUTE114:4/MAP_TERRA_CAVE_ENTRANCE:0!",
+ "MAP_ROUTE115:1/MAP_TERRA_CAVE_ENTRANCE:0!",
+ "MAP_ROUTE115:2/MAP_TERRA_CAVE_ENTRANCE:0!",
+ "MAP_ROUTE116:3/MAP_TERRA_CAVE_ENTRANCE:0!",
+ "MAP_ROUTE116:4/MAP_TERRA_CAVE_ENTRANCE:0!",
+ "MAP_ROUTE118:0/MAP_TERRA_CAVE_ENTRANCE:0!",
+ "MAP_ROUTE118:1/MAP_TERRA_CAVE_ENTRANCE:0!",
+
+ "MAP_UNDERWATER_MARINE_CAVE:0/MAP_DYNAMIC:-1!",
+ "MAP_UNDERWATER_ROUTE105:0/MAP_UNDERWATER_MARINE_CAVE:0!",
+ "MAP_UNDERWATER_ROUTE105:1/MAP_UNDERWATER_MARINE_CAVE:0!",
+ "MAP_UNDERWATER_ROUTE125:0/MAP_UNDERWATER_MARINE_CAVE:0!",
+ "MAP_UNDERWATER_ROUTE125:1/MAP_UNDERWATER_MARINE_CAVE:0!",
+ "MAP_UNDERWATER_ROUTE127:0/MAP_UNDERWATER_MARINE_CAVE:0!",
+ "MAP_UNDERWATER_ROUTE127:1/MAP_UNDERWATER_MARINE_CAVE:0!",
+ "MAP_UNDERWATER_ROUTE129:0/MAP_UNDERWATER_MARINE_CAVE:0!",
+ "MAP_UNDERWATER_ROUTE129:1/MAP_UNDERWATER_MARINE_CAVE:0!",
+
+ # Altering Cave
+ "MAP_ALTERING_CAVE:0/MAP_ROUTE103:0",
+ "MAP_ROUTE103:0/MAP_ALTERING_CAVE:0",
+
+ # Event islands
+ "MAP_BIRTH_ISLAND_EXTERIOR:0/MAP_BIRTH_ISLAND_HARBOR:0",
+ "MAP_BIRTH_ISLAND_HARBOR:0/MAP_BIRTH_ISLAND_EXTERIOR:0",
+
+ "MAP_FARAWAY_ISLAND_ENTRANCE:0,1/MAP_FARAWAY_ISLAND_INTERIOR:0,1",
+ "MAP_FARAWAY_ISLAND_INTERIOR:0,1/MAP_FARAWAY_ISLAND_ENTRANCE:0,1",
+
+ "MAP_SOUTHERN_ISLAND_EXTERIOR:0,1/MAP_SOUTHERN_ISLAND_INTERIOR:0,1",
+ "MAP_SOUTHERN_ISLAND_INTERIOR:0,1/MAP_SOUTHERN_ISLAND_EXTERIOR:0,1",
+
+ "MAP_NAVEL_ROCK_B1F:0/MAP_NAVEL_ROCK_ENTRANCE:0",
+ "MAP_NAVEL_ROCK_B1F:1/MAP_NAVEL_ROCK_FORK:1",
+ "MAP_NAVEL_ROCK_BOTTOM:0/MAP_NAVEL_ROCK_DOWN11:0",
+ "MAP_NAVEL_ROCK_DOWN01:0/MAP_NAVEL_ROCK_FORK:2",
+ "MAP_NAVEL_ROCK_DOWN01:1/MAP_NAVEL_ROCK_DOWN02:0",
+ "MAP_NAVEL_ROCK_DOWN02:0/MAP_NAVEL_ROCK_DOWN01:1",
+ "MAP_NAVEL_ROCK_DOWN02:1/MAP_NAVEL_ROCK_DOWN03:0",
+ "MAP_NAVEL_ROCK_DOWN03:0/MAP_NAVEL_ROCK_DOWN02:1",
+ "MAP_NAVEL_ROCK_DOWN03:1/MAP_NAVEL_ROCK_DOWN04:0",
+ "MAP_NAVEL_ROCK_DOWN04:0/MAP_NAVEL_ROCK_DOWN03:1",
+ "MAP_NAVEL_ROCK_DOWN04:1/MAP_NAVEL_ROCK_DOWN05:0",
+ "MAP_NAVEL_ROCK_DOWN05:0/MAP_NAVEL_ROCK_DOWN04:1",
+ "MAP_NAVEL_ROCK_DOWN05:1/MAP_NAVEL_ROCK_DOWN06:0",
+ "MAP_NAVEL_ROCK_DOWN06:0/MAP_NAVEL_ROCK_DOWN05:1",
+ "MAP_NAVEL_ROCK_DOWN06:1/MAP_NAVEL_ROCK_DOWN07:0",
+ "MAP_NAVEL_ROCK_DOWN07:0/MAP_NAVEL_ROCK_DOWN06:1",
+ "MAP_NAVEL_ROCK_DOWN07:1/MAP_NAVEL_ROCK_DOWN08:0",
+ "MAP_NAVEL_ROCK_DOWN08:0/MAP_NAVEL_ROCK_DOWN07:1",
+ "MAP_NAVEL_ROCK_DOWN08:1/MAP_NAVEL_ROCK_DOWN09:0",
+ "MAP_NAVEL_ROCK_DOWN09:0/MAP_NAVEL_ROCK_DOWN08:1",
+ "MAP_NAVEL_ROCK_DOWN09:1/MAP_NAVEL_ROCK_DOWN10:0",
+ "MAP_NAVEL_ROCK_DOWN10:0/MAP_NAVEL_ROCK_DOWN09:1",
+ "MAP_NAVEL_ROCK_DOWN10:1/MAP_NAVEL_ROCK_DOWN11:1",
+ "MAP_NAVEL_ROCK_DOWN11:0/MAP_NAVEL_ROCK_BOTTOM:0",
+ "MAP_NAVEL_ROCK_DOWN11:1/MAP_NAVEL_ROCK_DOWN10:1",
+ "MAP_NAVEL_ROCK_ENTRANCE:0/MAP_NAVEL_ROCK_B1F:0",
+ "MAP_NAVEL_ROCK_ENTRANCE:1/MAP_NAVEL_ROCK_EXTERIOR:1",
+ "MAP_NAVEL_ROCK_EXTERIOR:0/MAP_NAVEL_ROCK_HARBOR:0",
+ "MAP_NAVEL_ROCK_EXTERIOR:1/MAP_NAVEL_ROCK_ENTRANCE:1",
+ "MAP_NAVEL_ROCK_FORK:0/MAP_NAVEL_ROCK_UP1:0",
+ "MAP_NAVEL_ROCK_FORK:1/MAP_NAVEL_ROCK_B1F:1",
+ "MAP_NAVEL_ROCK_FORK:2/MAP_NAVEL_ROCK_DOWN01:0",
+ "MAP_NAVEL_ROCK_HARBOR:0/MAP_NAVEL_ROCK_EXTERIOR:0",
+ "MAP_NAVEL_ROCK_TOP:0/MAP_NAVEL_ROCK_UP4:1",
+ "MAP_NAVEL_ROCK_UP1:0/MAP_NAVEL_ROCK_FORK:0",
+ "MAP_NAVEL_ROCK_UP1:1/MAP_NAVEL_ROCK_UP2:0",
+ "MAP_NAVEL_ROCK_UP2:0/MAP_NAVEL_ROCK_UP1:1",
+ "MAP_NAVEL_ROCK_UP2:1/MAP_NAVEL_ROCK_UP3:0",
+ "MAP_NAVEL_ROCK_UP3:0/MAP_NAVEL_ROCK_UP2:1",
+ "MAP_NAVEL_ROCK_UP3:1/MAP_NAVEL_ROCK_UP4:0",
+ "MAP_NAVEL_ROCK_UP4:0/MAP_NAVEL_ROCK_UP3:1",
+ "MAP_NAVEL_ROCK_UP4:1/MAP_NAVEL_ROCK_TOP:0",
+
+ # Secret bases
+ "MAP_SECRET_BASE_BROWN_CAVE1:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_BROWN_CAVE2:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_BROWN_CAVE3:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_BROWN_CAVE4:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_BLUE_CAVE1:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_BLUE_CAVE2:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_BLUE_CAVE3:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_BLUE_CAVE4:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_YELLOW_CAVE1:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_YELLOW_CAVE2:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_YELLOW_CAVE3:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_YELLOW_CAVE4:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_RED_CAVE1:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_RED_CAVE2:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_RED_CAVE3:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_RED_CAVE4:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_SHRUB1:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_SHRUB2:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_SHRUB3:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_SHRUB4:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_TREE1:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_TREE2:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_TREE3:0/MAP_DYNAMIC:-2!",
+ "MAP_SECRET_BASE_TREE4:0/MAP_DYNAMIC:-2!",
+
+ # Multiplayer rooms
+ "MAP_RECORD_CORNER:0,1,2,3/MAP_DYNAMIC:-1!",
+
+ "MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!",
+ "MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_MAUVILLE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_PETALBURG_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_DEWFORD_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_OLDALE_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_SLATEPORT_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_RUSTBORO_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_LILYCOVE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_FORTREE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+ "MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
+
+ "MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!",
+ "MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_MAUVILLE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_PETALBURG_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_DEWFORD_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_OLDALE_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_SLATEPORT_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_RUSTBORO_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_LILYCOVE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_FORTREE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+ "MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
+
+ "MAP_BATTLE_COLOSSEUM_2P:0,1/MAP_DYNAMIC:-1!",
+ "MAP_BATTLE_COLOSSEUM_4P:0,1,2,3/MAP_DYNAMIC:-1!",
+
+ # Unused content
+ "MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1:0/MAP_CAVE_OF_ORIGIN_1F:1!",
+ "MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1:1/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:0",
+ "MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:0/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1:1",
+ "MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:1/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3:0",
+ "MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3:0/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:1",
+ "MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3:1/MAP_CAVE_OF_ORIGIN_B1F:0!",
+ "MAP_LILYCOVE_CITY_UNUSED_MART:0,1/MAP_LILYCOVE_CITY:0!",
+})
+
+
+def validate_regions() -> bool:
+ """
+ Verifies that Emerald's data doesn't have duplicate or missing
+ regions/warps/locations. Meant to catch problems during development like
+ forgetting to add a new location or incorrectly splitting a region.
+ """
+ extracted_data_json = load_json_data("extracted_data.json")
+ error_messages: List[str] = []
+ warn_messages: List[str] = []
+ failed = False
+
+ def error(message: str) -> None:
+ nonlocal failed
+ failed = True
+ error_messages.append(message)
+
+ def warn(message: str) -> None:
+ warn_messages.append(message)
+
+ # Check regions
+ for name, region in data.regions.items():
+ for region_exit in region.exits:
+ if region_exit not in data.regions:
+ error(f"Pokemon Emerald: Region [{region_exit}] referenced by [{name}] was not defined")
+
+ # Check warps
+ for warp_source, warp_dest in data.warp_map.items():
+ if warp_source in _IGNORABLE_WARPS:
+ continue
+
+ if warp_dest is None:
+ error(f"Pokemon Emerald: Warp [{warp_source}] has no destination")
+ elif not data.warps[warp_dest].connects_to(data.warps[warp_source]) and not data.warps[warp_source].is_one_way:
+ error(f"Pokemon Emerald: Warp [{warp_source}] appears to be a one-way warp but was not marked as one")
+
+ # Check locations
+ claimed_locations = [location for region in data.regions.values() for location in region.locations]
+ claimed_locations_set = set()
+ for location_name in claimed_locations:
+ if location_name in claimed_locations_set:
+ error(f"Pokemon Emerald: Location [{location_name}] was claimed by multiple regions")
+ claimed_locations_set.add(location_name)
+
+ for location_name in extracted_data_json["locations"]:
+ if location_name not in claimed_locations and location_name not in _IGNORABLE_LOCATIONS:
+ warn(f"Pokemon Emerald: Location [{location_name}] was not claimed by any region")
+
+ warn_messages.sort()
+ error_messages.sort()
+
+ for message in warn_messages:
+ logging.warning(message)
+ for message in error_messages:
+ logging.error(message)
+
+ logging.debug("Pokemon Emerald sanity check done. Found %s errors and %s warnings.", len(error_messages), len(warn_messages))
+
+ return not failed
diff --git a/worlds/pokemon_emerald/test/__init__.py b/worlds/pokemon_emerald/test/__init__.py
new file mode 100644
index 000000000000..84ce64003d57
--- /dev/null
+++ b/worlds/pokemon_emerald/test/__init__.py
@@ -0,0 +1,5 @@
+from test.TestBase import WorldTestBase
+
+
+class PokemonEmeraldTestBase(WorldTestBase):
+ game = "Pokemon Emerald"
diff --git a/worlds/pokemon_emerald/test/test_accessibility.py b/worlds/pokemon_emerald/test/test_accessibility.py
new file mode 100644
index 000000000000..4fb1884684b9
--- /dev/null
+++ b/worlds/pokemon_emerald/test/test_accessibility.py
@@ -0,0 +1,258 @@
+from Options import Toggle
+
+from . import PokemonEmeraldTestBase
+from ..util import location_name_to_label
+from ..options import NormanRequirement
+
+
+class TestBasic(PokemonEmeraldTestBase):
+ def test_always_accessible(self) -> None:
+ self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_102_POTION")))
+ self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_SUPER_POTION")))
+
+
+class TestScorchedSlabPond(PokemonEmeraldTestBase):
+ options = {
+ "enable_ferry": Toggle.option_true,
+ "require_flash": Toggle.option_false
+ }
+
+ def test_with_neither(self) -> None:
+ self.collect_by_name(["S.S. Ticket", "Letter", "Stone Badge", "HM01 Cut"])
+ self.assertTrue(self.can_reach_region("REGION_ROUTE120/NORTH"))
+ self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_NEST_BALL")))
+ self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_SCORCHED_SLAB_TM_SUNNY_DAY")))
+
+ def test_with_surf(self) -> None:
+ self.collect_by_name(["S.S. Ticket", "Letter", "Stone Badge", "HM01 Cut", "HM03 Surf", "Balance Badge"])
+ self.assertTrue(self.can_reach_region("REGION_ROUTE120/NORTH"))
+ self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_NEST_BALL")))
+ self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_SCORCHED_SLAB_TM_SUNNY_DAY")))
+
+ def test_with_scope(self) -> None:
+ self.collect_by_name(["S.S. Ticket", "Letter", "Stone Badge", "HM01 Cut", "Devon Scope"])
+ self.assertTrue(self.can_reach_region("REGION_ROUTE120/NORTH"))
+ self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_NEST_BALL")))
+ self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_SCORCHED_SLAB_TM_SUNNY_DAY")))
+
+ def test_with_both(self) -> None:
+ self.collect_by_name(["S.S. Ticket", "Letter", "Stone Badge", "HM01 Cut", "Devon Scope", "HM03 Surf", "Balance Badge"])
+ self.assertTrue(self.can_reach_region("REGION_ROUTE120/NORTH"))
+ self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_NEST_BALL")))
+ self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_SCORCHED_SLAB_TM_SUNNY_DAY")))
+
+
+class TestSurf(PokemonEmeraldTestBase):
+ options = {
+ "npc_gifts": Toggle.option_true,
+ "hidden_items": Toggle.option_true,
+ "require_itemfinder": Toggle.option_false
+ }
+
+ def test_inaccessible_with_no_surf(self) -> None:
+ self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER")))
+ self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
+ self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
+ self.assertFalse(self.can_reach_location(location_name_to_label("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2")))
+ self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_FULL_HEAL")))
+ self.assertFalse(self.can_reach_entrance("REGION_ROUTE118/EAST_WATER -> REGION_ROUTE118/EAST"))
+ self.assertFalse(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN"))
+ self.assertFalse(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0"))
+
+ # Slateport Access
+ self.collect_by_name(["HM06 Rock Smash", "Dynamo Badge", "Mach Bike"])
+ self.assertFalse(self.can_reach_region("MAP_SLATEPORT_CITY_WATER_ENCOUNTERS"))
+
+ def test_accessible_with_surf_only(self) -> None:
+ self.collect_by_name(["HM03 Surf", "Balance Badge"])
+ self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER")))
+ self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
+ self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
+ self.assertTrue(self.can_reach_location(location_name_to_label("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2")))
+ self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_FULL_HEAL")))
+ self.assertTrue(self.can_reach_entrance("REGION_ROUTE118/EAST_WATER -> REGION_ROUTE118/EAST"))
+ self.assertTrue(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN"))
+ self.assertTrue(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0"))
+ self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_4")))
+ self.assertTrue(self.can_reach_region("MAP_SLATEPORT_CITY_WATER_ENCOUNTERS"))
+
+
+class TestModify118(PokemonEmeraldTestBase):
+ options = {
+ "modify_118": Toggle.option_true,
+ "bikes": Toggle.option_true,
+ "rods": Toggle.option_true
+ }
+
+ def test_inaccessible_with_nothing(self) -> None:
+ self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_GOOD_ROD")))
+
+ def test_inaccessible_with_only_surf(self) -> None:
+ self.collect_by_name(["HM03 Surf", "Balance Badge"])
+ self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_GOOD_ROD")))
+
+ def test_accessible_with_surf_and_acro_bike(self) -> None:
+ self.collect_by_name(["HM03 Surf", "Balance Badge", "Acro Bike"])
+ self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_GOOD_ROD")))
+
+
+class TestFreeFly(PokemonEmeraldTestBase):
+ options = {
+ "npc_gifts": Toggle.option_true,
+ "free_fly_location": Toggle.option_true
+ }
+
+ def setUp(self) -> None:
+ super(PokemonEmeraldTestBase, self).setUp()
+
+ # Swap free fly to Sootopolis
+ free_fly_location = self.multiworld.get_location("FREE_FLY_LOCATION", 1)
+ free_fly_location.item = None
+ free_fly_location.place_locked_item(self.multiworld.worlds[1].create_event("EVENT_VISITED_SOOTOPOLIS_CITY"))
+
+ def test_sootopolis_gift_inaccessible_with_no_surf(self) -> None:
+ self.collect_by_name(["HM02 Fly", "Feather Badge"])
+ self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_TM_BRICK_BREAK")))
+
+ def test_sootopolis_gift_accessible_with_surf(self) -> None:
+ self.collect_by_name(["HM03 Surf", "Balance Badge", "HM02 Fly", "Feather Badge"])
+ self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_TM_BRICK_BREAK")))
+
+
+class TestLilycoveFromEast(PokemonEmeraldTestBase):
+ options = {
+ "modify_118": Toggle.option_true,
+ "bikes": Toggle.option_true,
+ "free_fly_location": Toggle.option_true
+ }
+
+ def setUp(self) -> None:
+ super(PokemonEmeraldTestBase, self).setUp()
+
+ # Swap free fly to Mossdeep
+ free_fly_location = self.multiworld.get_location("FREE_FLY_LOCATION", 1)
+ free_fly_location.item = None
+ free_fly_location.place_locked_item(self.multiworld.worlds[1].create_event("EVENT_VISITED_MOSSDEEP_CITY"))
+
+ def test_lilycove_inaccessible_from_east(self) -> None:
+ self.collect_by_name(["HM03 Surf", "Balance Badge", "HM02 Fly", "Feather Badge"])
+ self.assertFalse(self.can_reach_region("REGION_LILYCOVE_CITY/MAIN"))
+
+
+class TestFerry(PokemonEmeraldTestBase):
+ options = {
+ "npc_gifts": Toggle.option_true
+ }
+
+ def test_inaccessible_with_no_items(self) -> None:
+ self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
+ self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
+
+ def test_inaccessible_with_only_slateport_access(self) -> None:
+ self.collect_by_name(["HM06 Rock Smash", "Dynamo Badge", "Acro Bike"])
+ self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
+ self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
+
+ def test_accessible_with_slateport_access_and_ticket(self) -> None:
+ self.collect_by_name(["HM06 Rock Smash", "Dynamo Badge", "Acro Bike", "S.S. Ticket"])
+ self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
+ self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
+
+
+class TestExtraBouldersOn(PokemonEmeraldTestBase):
+ options = {
+ "extra_boulders": Toggle.option_true
+ }
+
+ def test_inaccessible_with_no_items(self) -> None:
+ self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
+
+ def test_inaccessible_with_surf_only(self) -> None:
+ self.collect_by_name(["HM03 Surf", "Balance Badge"])
+ self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
+
+ def test_accessible_with_surf_and_strength(self) -> None:
+ self.collect_by_name(["HM03 Surf", "Balance Badge", "HM04 Strength", "Heat Badge"])
+ self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
+
+
+class TestExtraBouldersOff(PokemonEmeraldTestBase):
+ options = {
+ "extra_boulders": Toggle.option_false
+ }
+
+ def test_inaccessible_with_no_items(self) -> None:
+ self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
+
+ def test_accessible_with_surf_only(self) -> None:
+ self.collect_by_name(["HM03 Surf", "Balance Badge"])
+ self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
+
+
+class TestNormanRequirement1(PokemonEmeraldTestBase):
+ options = {
+ "norman_requirement": NormanRequirement.option_badges,
+ "norman_count": 0
+ }
+
+ def test_accessible_with_no_items(self) -> None:
+ self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_5")))
+
+
+class TestNormanRequirement2(PokemonEmeraldTestBase):
+ options = {
+ "norman_requirement": NormanRequirement.option_badges,
+ "norman_count": 4
+ }
+
+ def test_inaccessible_with_no_items(self) -> None:
+ self.assertFalse(self.can_reach_location(location_name_to_label("BADGE_5")))
+
+ def test_accessible_with_enough_badges(self) -> None:
+ self.collect_by_name(["Stone Badge", "Knuckle Badge", "Feather Badge", "Balance Badge"])
+ self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_5")))
+
+
+class TestNormanRequirement3(PokemonEmeraldTestBase):
+ options = {
+ "norman_requirement": NormanRequirement.option_gyms,
+ "norman_count": 0
+ }
+
+ def test_accessible_with_no_items(self) -> None:
+ self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_5")))
+
+
+class TestNormanRequirement4(PokemonEmeraldTestBase):
+ options = {
+ "norman_requirement": NormanRequirement.option_gyms,
+ "norman_count": 4
+ }
+
+ def test_inaccessible_with_no_items(self) -> None:
+ self.assertFalse(self.can_reach_location(location_name_to_label("BADGE_5")))
+
+ def test_accessible_with_reachable_gyms(self) -> None:
+ self.collect_by_name(["HM03 Surf", "Balance Badge"]) # Reaches Roxanne, Brawley, Wattson, and Flannery
+ self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_5")))
+
+
+class TestVictoryRoad(PokemonEmeraldTestBase):
+ options = {
+ "elite_four_requirement": NormanRequirement.option_badges,
+ "elite_four_count": 0,
+ "remove_roadblocks": {"Lilycove City Wailmer"}
+ }
+
+ def test_accessible_with_specific_hms(self) -> None:
+ self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
+ self.collect_by_name(["HM03 Surf", "Balance Badge"])
+ self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
+ self.collect_by_name(["HM07 Waterfall", "Rain Badge"])
+ self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
+ self.collect_by_name(["HM04 Strength", "Heat Badge"])
+ self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
+ self.collect_by_name(["HM06 Rock Smash", "Dynamo Badge"])
+ self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
+ self.collect_by_name(["HM05 Flash", "Knuckle Badge"])
+ self.assertTrue(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
diff --git a/worlds/pokemon_emerald/test/test_warps.py b/worlds/pokemon_emerald/test/test_warps.py
new file mode 100644
index 000000000000..75a2417dfbe6
--- /dev/null
+++ b/worlds/pokemon_emerald/test/test_warps.py
@@ -0,0 +1,21 @@
+from test.TestBase import TestBase
+from ..data import Warp
+
+
+class TestWarps(TestBase):
+ def test_warps_connect_ltr(self) -> None:
+ # 2-way
+ self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:0").connects_to(Warp("FAKE_MAP_B:0/FAKE_MAP_A:0")))
+ self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:2/FAKE_MAP_A:0")))
+ self.assertTrue(Warp("FAKE_MAP_A:0,1/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:2/FAKE_MAP_A:0")))
+ self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:2,3/FAKE_MAP_A:0")))
+
+ # 1-way
+ self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2!").connects_to(Warp("FAKE_MAP_B:2/FAKE_MAP_A:3")))
+ self.assertTrue(Warp("FAKE_MAP_A:0,1/FAKE_MAP_B:2!").connects_to(Warp("FAKE_MAP_B:2/FAKE_MAP_A:3")))
+ self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2!").connects_to(Warp("FAKE_MAP_B:2,3/FAKE_MAP_A:3")))
+
+ # Invalid
+ self.assertFalse(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:4/FAKE_MAP_A:0")))
+ self.assertFalse(Warp("FAKE_MAP_A:0,4/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:4/FAKE_MAP_A:0")))
+ self.assertFalse(Warp("FAKE_MAP_A:0,4/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_C:2/FAKE_MAP_A:0")))
diff --git a/worlds/pokemon_emerald/util.py b/worlds/pokemon_emerald/util.py
new file mode 100644
index 000000000000..f7f02edd95d6
--- /dev/null
+++ b/worlds/pokemon_emerald/util.py
@@ -0,0 +1,349 @@
+import orjson
+from typing import Any, Dict, List, Optional, Tuple, Iterable
+
+from .data import NATIONAL_ID_TO_SPECIES_ID, data
+
+
+CHARACTER_DECODING_MAP = {
+ 0x00: " ", 0x01: "À", 0x02: "Ã", 0x03: "Â", 0x04: "Ç",
+ 0x05: "È", 0x06: "É", 0x07: "Ê", 0x08: "Ë", 0x09: "Ì",
+ 0x0B: "ÃŽ", 0x0C: "Ã", 0x0D: "Ã’", 0x0E: "Ó", 0x0F: "Ô",
+ 0x10: "Œ", 0x11: "Ù", 0x12: "Ú", 0x13: "Û", 0x14: "Ñ",
+ 0x15: "ß", 0x16: "à ", 0x17: "á", 0x19: "ç", 0x1A: "è",
+ 0x1B: "é", 0x1C: "ê", 0x1D: "ë", 0x1E: "ì", 0x20: "î",
+ 0x21: "ï", 0x22: "ò", 0x23: "ó", 0x24: "ô", 0x25: "œ",
+ 0x26: "ù", 0x27: "ú", 0x28: "û", 0x29: "ñ", 0x2A: "°",
+ 0x2B: "ª", 0x2D: "&", 0x2E: "+", 0x35: "=", 0x36: ";",
+ 0x50: "â–¯", 0x51: "¿", 0x52: "¡", 0x5A: "Ã", 0x5B: "%",
+ 0x5C: "(", 0x5D: ")", 0x68: "â", 0x6F: "Ã", 0x79: "⬆",
+ 0x7A: "⬇", 0x7B: "⬅", 0x7C: "➡", 0x7D: "*", 0x84: "ᵉ",
+ 0x85: "<", 0x86: ">", 0xA1: "0", 0xA2: "1", 0xA3: "2",
+ 0xA4: "3", 0xA5: "4", 0xA6: "5", 0xA7: "6", 0xA8: "7",
+ 0xA9: "8", 0xAA: "9", 0xAB: "!", 0xAC: "?", 0xAD: ".",
+ 0xAE: "-", 0xB0: "…", 0xB1: "“", 0xB2: "â€", 0xB3: "‘",
+ 0xB4: "’", 0xB5: "♂", 0xB6: "♀", 0xB8: ",", 0xB9: "×",
+ 0xBA: "/", 0xBB: "A", 0xBC: "B", 0xBD: "C", 0xBE: "D",
+ 0xBF: "E", 0xC0: "F", 0xC1: "G", 0xC2: "H", 0xC3: "I",
+ 0xC4: "J", 0xC5: "K", 0xC6: "L", 0xC7: "M", 0xC8: "N",
+ 0xC9: "O", 0xCA: "P", 0xCB: "Q", 0xCC: "R", 0xCD: "S",
+ 0xCE: "T", 0xCF: "U", 0xD0: "V", 0xD1: "W", 0xD2: "X",
+ 0xD3: "Y", 0xD4: "Z", 0xD5: "a", 0xD6: "b", 0xD7: "c",
+ 0xD8: "d", 0xD9: "e", 0xDA: "f", 0xDB: "g", 0xDC: "h",
+ 0xDD: "i", 0xDE: "j", 0xDF: "k", 0xE0: "l", 0xE1: "m",
+ 0xE2: "n", 0xE3: "o", 0xE4: "p", 0xE5: "q", 0xE6: "r",
+ 0xE7: "s", 0xE8: "t", 0xE9: "u", 0xEA: "v", 0xEB: "w",
+ 0xEC: "x", 0xED: "y", 0xEE: "z", 0xEF: "â–¶", 0xF0: ":",
+}
+
+CHARACTER_ENCODING_MAP = {value: key for key, value in CHARACTER_DECODING_MAP.items()}
+CHARACTER_ENCODING_MAP.update({
+ "'": CHARACTER_ENCODING_MAP["’"],
+ "\"": CHARACTER_ENCODING_MAP["â€"],
+ "_": CHARACTER_ENCODING_MAP[" "],
+})
+
+ALLOWED_TRAINER_NAME_CHARACTERS = frozenset({
+ " ", "0", "1", "2", "3", "4", "5", "6", "7", "8",
+ "9", "!", "?", ".", "-", "…", "“", "â€", "‘", "’",
+ "♂", "♀", ",", "/", "A", "B", "C", "D", "E", "F",
+ "G", "H", "I", "J", "K", "L", "M", "N", "O", "P",
+ "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
+ "k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
+ "u", "v", "w", "x", "y", "z",
+})
+
+
+def encode_string(string: str, length: Optional[int] = None) -> bytes:
+ arr = []
+ length = len(string) if length is None else length
+
+ for i in range(length):
+ if i >= len(string):
+ arr.append(0xFF)
+ continue
+
+ char = string[i]
+ if char in CHARACTER_ENCODING_MAP:
+ arr.append(CHARACTER_ENCODING_MAP[char])
+ else:
+ arr.append(CHARACTER_ENCODING_MAP["?"])
+
+ return bytes(arr)
+
+
+def decode_string(string_data: Iterable[int]) -> str:
+ string = ""
+ for code in string_data:
+ if code == 0xFF:
+ break
+
+ if code in CHARACTER_DECODING_MAP:
+ string += CHARACTER_DECODING_MAP[code]
+ else:
+ raise KeyError(f"The following value does not correspond to a character in Pokemon Emerald: {code}")
+
+ return string
+
+
+def get_easter_egg(easter_egg: str) -> Tuple[int, int]:
+ easter_egg = easter_egg.upper()
+ result1 = 0
+ result2 = 0
+ for c in easter_egg:
+ result1 = ((result1 << 5) - result1 + ord(c)) & 0xFFFFFFFF
+ result2 = ((result2 << 4) - result2 + ord(c)) & 0xFF
+
+ if result1 == 0x9137C17B:
+ value = (result2 + 23) & 0xFF
+ if value > 0 and (value < 252 or (value > 276 and value < 412)):
+ return (1, value)
+ elif result1 == 0x9AECC7C6:
+ value = (result2 + 64) & 0xFF
+ if value > 0 and value < 355:
+ return (2, value)
+ elif result1 == 0x506D2690:
+ value = (result2 + 169) & 0xFF
+ if value > 0 and value < 78:
+ return (3, value)
+ elif result1 == 0xA7850E45 and (result1 ^ result2) & 0xFF == 96:
+ return (4, 0)
+
+ return (0, 0)
+
+
+def location_name_to_label(name: str) -> str:
+ return data.locations[name].label
+
+
+def int_to_bool_array(num: int) -> List[bool]:
+ binary_string = format(num, "064b")
+ bool_array = [bit == "1" for bit in reversed(binary_string)]
+ return bool_array
+
+
+def bool_array_to_int(bool_array: List[bool]) -> int:
+ binary_string = "".join(["1" if bit else "0" for bit in reversed(bool_array)])
+ num = int(binary_string, 2)
+ return num
+
+
+_SUBSTRUCT_ORDERS = [
+ [0, 1, 2, 3], [0, 1, 3, 2], [0, 2, 1, 3], [0, 3, 1, 2],
+ [0, 2, 3, 1], [0, 3, 2, 1], [1, 0, 2, 3], [1, 0, 3, 2],
+ [2, 0, 1, 3], [3, 0, 1, 2], [2, 0, 3, 1], [3, 0, 2, 1],
+ [1, 2, 0, 3], [1, 3, 0, 2], [2, 1, 0, 3], [3, 1, 0, 2],
+ [2, 3, 0, 1], [3, 2, 0, 1], [1, 2, 3, 0], [1, 3, 2, 0],
+ [2, 1, 3, 0], [3, 1, 2, 0], [2, 3, 1, 0], [3, 2, 1, 0],
+]
+
+_LANGUAGE_IDS = {
+ "Japanese": 1,
+ "English": 2,
+ "French": 3,
+ "Italian": 4,
+ "German": 5,
+ "Spanish": 7,
+}
+
+_MODERN_ITEM_TO_EMERALD_ITEM = {
+ item.modern_id: item.item_id
+ for item in data.items.values()
+ if item.modern_id is not None
+}
+
+
+def _encrypt_or_decrypt_substruct(substruct_data: Iterable[int], key: int) -> bytearray:
+ modified_data = bytearray()
+ for i in range(int(len(substruct_data) / 4)):
+ modified_data.extend((int.from_bytes(substruct_data[i * 4 : (i + 1) * 4], "little") ^ key).to_bytes(4, "little"))
+
+ return modified_data
+
+
+def pokemon_data_to_json(pokemon_data: Iterable[int]) -> str:
+ personality = int.from_bytes(pokemon_data[0:4], "little")
+ tid = int.from_bytes(pokemon_data[4:8], "little")
+
+ substruct_order = _SUBSTRUCT_ORDERS[personality % 24]
+ substructs = []
+ for i in substruct_order:
+ substructs.append(pokemon_data[32 + (i * 12) : 32 + ((i + 1) * 12)])
+
+ decrypted_substructs = [_encrypt_or_decrypt_substruct(substruct, personality ^ tid) for substruct in substructs]
+
+ iv_ability_info = int.from_bytes(decrypted_substructs[3][4:8], "little")
+ met_info = int.from_bytes(decrypted_substructs[3][2:4], "little")
+
+ held_item = int.from_bytes(decrypted_substructs[0][2:4], "little")
+
+ json_object = {
+ "version": "1",
+ "personality": personality,
+ "nickname": decode_string(pokemon_data[8:18]),
+ "language": {v: k for k, v in _LANGUAGE_IDS.items()}[pokemon_data[18]],
+ "species": data.species[int.from_bytes(decrypted_substructs[0][0:2], "little")].national_dex_number,
+ "experience": int.from_bytes(decrypted_substructs[0][4:8], "little"),
+ "ability": iv_ability_info >> 31,
+ "ivs": [(iv_ability_info >> (i * 5)) & 0x1F for i in range(6)],
+ "evs": list(decrypted_substructs[2][0:6]),
+ "conditions": list(decrypted_substructs[2][6:12]),
+ "pokerus": decrypted_substructs[3][0],
+ "location_met": decrypted_substructs[3][1],
+ "level_met": met_info & 0b0000000001111111,
+ "game": (met_info & 0b0000011110000000) >> 7,
+ "ball": (met_info & 0b0111100000000000) >> 11,
+ "moves": [
+ [
+ int.from_bytes(decrypted_substructs[1][i * 2 : (i + 1) * 2], "little"),
+ decrypted_substructs[1][8 + i],
+ (decrypted_substructs[0][8] & (0b00000011 << (i * 2))) >> (i * 2)
+ ] for i in range(4)
+ ],
+ "trainer": {
+ "name": decode_string(pokemon_data[20:27]),
+ "id": tid,
+ "female": (met_info & 0b1000000000000000) != 0,
+ },
+ }
+
+ if held_item != 0:
+ json_object["item"] = data.items[held_item].modern_id
+
+ return orjson.dumps(json_object).decode("utf-8")
+
+
+def json_to_pokemon_data(json_str: str) -> bytearray:
+ pokemon_json: Dict[str, Any] = orjson.loads(json_str)
+
+ # Default values to cover for optional or accidentally missed fields
+ default_pokemon = {
+ "nickname": "A",
+ "personality": 0,
+ "species": 1,
+ "experience": 0,
+ "ability": 0,
+ "ivs": [0, 0, 0, 0, 0, 0],
+ "evs": [0, 0, 0, 0, 0, 0],
+ "conditions": [0, 0, 0, 0, 0, 0],
+ "pokerus": 0,
+ "game": 3,
+ "location_met": 0,
+ "level_met": 1,
+ "ball": 4,
+ "moves": [[33, 35, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
+ }
+
+ default_trainer = {
+ "name": "A",
+ "id": 0,
+ "female": False,
+ }
+
+ pokemon_json = {**default_pokemon, **{k: v for k, v in pokemon_json.items()}}
+ pokemon_json["trainer"] = {**default_trainer, **pokemon_json["trainer"]}
+
+ # Cutting string lengths to Emerald sizes
+ pokemon_json["nickname"] = pokemon_json["nickname"][0:10]
+ pokemon_json["trainer"]["name"] = pokemon_json["trainer"]["name"][0:7]
+
+ # Handle data from incompatible games
+ if pokemon_json["species"] > 387:
+ pokemon_json["species"] = 201 # Unown
+ if pokemon_json["ball"] > 12:
+ pokemon_json["ball"] = 4 # Pokeball
+ if "game" not in pokemon_json or (pokemon_json["game"] > 5 and pokemon_json["game"] != 15):
+ pokemon_json["game"] = 0 # Unknown
+ pokemon_json["location_met"] = 0 # Littleroot
+
+ substructs = [bytearray([0 for _ in range(12)]) for _ in range(4)]
+
+ # Substruct type 0
+ for i, byte in enumerate(NATIONAL_ID_TO_SPECIES_ID[pokemon_json["species"]].to_bytes(2, "little")):
+ substructs[0][0 + i] = byte
+
+ if "item" in pokemon_json:
+ if pokemon_json["item"] in _MODERN_ITEM_TO_EMERALD_ITEM:
+ for i, byte in enumerate(_MODERN_ITEM_TO_EMERALD_ITEM[pokemon_json["item"]].to_bytes(2, "little")):
+ substructs[0][2 + i] = byte
+
+ for i, byte in enumerate((pokemon_json["experience"]).to_bytes(4, "little")):
+ substructs[0][4 + i] = byte
+
+ for i, move_info in enumerate(pokemon_json["moves"]):
+ substructs[0][8] |= ((move_info[2] & 0b11) << (2 * i))
+
+ substructs[0][9] = data.species[NATIONAL_ID_TO_SPECIES_ID[pokemon_json["species"]]].friendship
+
+ # Substruct type 1
+ for i, move_info in enumerate(pokemon_json["moves"]):
+ for j, byte in enumerate(move_info[0].to_bytes(2, "little")):
+ substructs[1][(i * 2) + j] = byte
+
+ substructs[1][8 + i] = move_info[1]
+
+ # Substruct type 2
+ for i, ev in enumerate(pokemon_json["evs"]):
+ substructs[2][0 + i] = ev
+
+ for i, condition in enumerate(pokemon_json["conditions"]):
+ substructs[2][6 + i] = condition
+
+ # Substruct type 3
+ substructs[3][0] = pokemon_json["pokerus"]
+ substructs[3][1] = pokemon_json["location_met"]
+
+ origin = pokemon_json["level_met"] | (pokemon_json["game"] << 7) | (pokemon_json["ball"] << 11)
+ origin |= (1 << 15) if pokemon_json["trainer"]["female"] else 0
+ for i, byte in enumerate(origin.to_bytes(2, "little")):
+ substructs[3][2 + i] = byte
+
+ iv_ability_info = 0
+ for i, iv in enumerate(pokemon_json["ivs"]):
+ iv_ability_info |= iv << (i * 5)
+ iv_ability_info |= 1 << 31 if pokemon_json["ability"] == 1 else 0
+ for i, byte in enumerate(iv_ability_info.to_bytes(4, "little")):
+ substructs[3][4 + i] = byte
+
+ # Main data
+ pokemon_data = bytearray([0 for _ in range(80)])
+ for i, byte in enumerate(pokemon_json["personality"].to_bytes(4, "little")):
+ pokemon_data[0 + i] = byte
+
+ for i, byte in enumerate(pokemon_json["trainer"]["id"].to_bytes(4, "little")):
+ pokemon_data[4 + i] = byte
+
+ for i, byte in enumerate(encode_string(pokemon_json["nickname"], 10)):
+ pokemon_data[8 + i] = byte
+
+ pokemon_data[18] = _LANGUAGE_IDS[pokemon_json["language"]]
+ pokemon_data[19] = 0b00000010 # Flags for Bad Egg, Has Species, Is Egg, padding bits (low to high)
+
+ for i, byte in enumerate(encode_string(pokemon_json["trainer"]["name"], 7)):
+ pokemon_data[20 + i] = byte
+
+ # Markings, 1 byte
+
+ checksum = 0
+ for i in range(4):
+ for j in range(6):
+ checksum += int.from_bytes(substructs[i][j * 2 : (j + 1) * 2], "little")
+ checksum &= 0xFFFF
+ for i, byte in enumerate(checksum.to_bytes(2, "little")):
+ pokemon_data[28 + i] = byte
+
+ # Separator, 2 bytes
+
+ substruct_order = [_SUBSTRUCT_ORDERS[pokemon_json["personality"] % 24].index(n) for n in [0, 1, 2, 3]]
+ encrypted_substructs = [None for _ in range(4)]
+ encryption_key = pokemon_json["personality"] ^ pokemon_json["trainer"]["id"]
+ encrypted_substructs[0] = _encrypt_or_decrypt_substruct(substructs[substruct_order[0]], encryption_key)
+ encrypted_substructs[1] = _encrypt_or_decrypt_substruct(substructs[substruct_order[1]], encryption_key)
+ encrypted_substructs[2] = _encrypt_or_decrypt_substruct(substructs[substruct_order[2]], encryption_key)
+ encrypted_substructs[3] = _encrypt_or_decrypt_substruct(substructs[substruct_order[3]], encryption_key)
+
+ for i in range(4):
+ for j in range(12):
+ pokemon_data[32 + (i * 12) + j] = encrypted_substructs[i][j]
+
+ return pokemon_data
diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py
index 2c70f284160a..277d73b2258b 100644
--- a/worlds/pokemon_rb/__init__.py
+++ b/worlds/pokemon_rb/__init__.py
@@ -2,9 +2,11 @@
import settings
import typing
import threading
+import base64
from copy import deepcopy
from typing import TextIO
+from Utils import __version__
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification, LocationProgressType
from Fill import fill_restrictive, FillError, sweep_from_pool
from worlds.AutoWorld import World, WebWorld
@@ -16,12 +18,13 @@
from .rom_addresses import rom_addresses
from .text import encode_text
from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, RedDeltaPatch, BlueDeltaPatch
-from .pokemon import process_pokemon_data, process_move_data
+from .pokemon import process_pokemon_data, process_move_data, verify_hm_moves
from .encounters import process_pokemon_locations, process_trainer_data
from .rules import set_rules
from .level_scaling import level_scaling
from . import logic
from . import poke_data
+from . import client
class PokemonSettings(settings.Group):
@@ -36,27 +39,30 @@ class BlueRomFile(settings.UserFilePath):
copy_to = "Pokemon Blue (UE) [S][!].gb"
md5s = [BlueDeltaPatch.hash]
- class RomStart(str):
- """
- Set this to false to never autostart a rom (such as after patching)
- True for operating system default program
- Alternatively, a path to a program to open the .gb file with
- """
-
red_rom_file: RedRomFile = RedRomFile(RedRomFile.copy_to)
blue_rom_file: BlueRomFile = BlueRomFile(BlueRomFile.copy_to)
- rom_start: typing.Union[RomStart, bool] = True
class PokemonWebWorld(WebWorld):
- tutorials = [Tutorial(
+ setup_en = Tutorial(
"Multiworld Setup Guide",
- "A guide to playing Pokemon Red and Blue with Archipelago.",
+ "A guide to playing Pokémon Red and Blue with Archipelago.",
"English",
"setup_en.md",
"setup/en",
["Alchav"]
- )]
+ )
+
+ setup_es = Tutorial(
+ setup_en.tutorial_name,
+ setup_en.description,
+ "Español",
+ "setup_es.md",
+ "setup/es",
+ ["Shiny"]
+ )
+
+ tutorials = [setup_en, setup_es]
class PokemonRedBlueWorld(World):
@@ -68,7 +74,6 @@ class PokemonRedBlueWorld(World):
option_definitions = pokemon_rb_options
settings: typing.ClassVar[PokemonSettings]
- data_version = 9
required_client_version = (0, 4, 2)
topology_present = True
@@ -130,9 +135,6 @@ def encode_name(name, t):
else:
self.rival_name = encode_name(self.multiworld.rival_name[self.player].value, "Rival")
- if len(self.multiworld.player_name[self.player].encode()) > 16:
- raise Exception(f"Player name too long for {self.multiworld.get_player_name(self.player)}. Player name cannot exceed 16 bytes for Pokémon Red and Blue.")
-
if not self.multiworld.badgesanity[self.player]:
self.multiworld.non_local_items[self.player].value -= self.item_name_groups["Badges"]
@@ -192,11 +194,11 @@ def encode_name(name, t):
normals -= subtract_amounts[2]
while super_effectives + not_very_effectives + normals > 225 - immunities:
r = self.multiworld.random.randint(0, 2)
- if r == 0:
+ if r == 0 and super_effectives:
super_effectives -= 1
- elif r == 1:
+ elif r == 1 and not_very_effectives:
not_very_effectives -= 1
- else:
+ elif normals:
normals -= 1
chart = []
for matchup_list, matchup_value in zip([immunities, normals, super_effectives, not_very_effectives],
@@ -246,19 +248,22 @@ def stage_fill_hook(cls, multiworld, progitempool, usefulitempool, filleritempoo
itempool = progitempool + usefulitempool + filleritempool
multiworld.random.shuffle(itempool)
unplaced_items = []
- for item in itempool:
+ for i, item in enumerate(itempool):
if item.player == loc.player and loc.can_fill(multiworld.state, item, False):
- if item in progitempool:
- progitempool.remove(item)
- elif item in usefulitempool:
- usefulitempool.remove(item)
- elif item in filleritempool:
- filleritempool.remove(item)
+ if item.advancement:
+ pool = progitempool
+ elif item.useful:
+ pool = usefulitempool
+ else:
+ pool = filleritempool
+ for i, check_item in enumerate(pool):
+ if item is check_item:
+ pool.pop(i)
+ break
if item.advancement:
state = sweep_from_pool(multiworld.state, progitempool + unplaced_items)
if (not item.advancement) or state.can_reach(loc, "Location", loc.player):
multiworld.push_item(loc, item, False)
- loc.event = item.advancement
fill_locations.remove(loc)
break
else:
@@ -272,24 +277,32 @@ def stage_fill_hook(cls, multiworld, progitempool, usefulitempool, filleritempoo
def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations):
if not self.multiworld.badgesanity[self.player]:
# Door Shuffle options besides Simple place badges during door shuffling
- if not self.multiworld.door_shuffle[self.player] not in ("off", "simple"):
+ if self.multiworld.door_shuffle[self.player] in ("off", "simple"):
badges = [item for item in progitempool if "Badge" in item.name and item.player == self.player]
for badge in badges:
self.multiworld.itempool.remove(badge)
progitempool.remove(badge)
- for _ in range(5):
- badgelocs = [self.multiworld.get_location(loc, self.player) for loc in [
- "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize",
- "Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize",
- "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize",
- "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"]]
+ for attempt in range(6):
+ badgelocs = [
+ self.multiworld.get_location(loc, self.player) for loc in [
+ "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize",
+ "Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize",
+ "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize",
+ "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"
+ ] if self.multiworld.get_location(loc, self.player).item is None]
state = self.multiworld.get_all_state(False)
+ # Give it two tries to place badges with wild Pokemon and learnsets as-is.
+ # If it can't, then try with all Pokemon collected, and we'll try to fix HM move availability after.
+ if attempt > 1:
+ for mon in poke_data.pokemon_data.keys():
+ state.collect(self.create_item(mon), True)
+ state.sweep_for_events()
self.multiworld.random.shuffle(badges)
self.multiworld.random.shuffle(badgelocs)
badgelocs_copy = badgelocs.copy()
# allow_partial so that unplaced badges aren't lost, for debugging purposes
fill_restrictive(self.multiworld, state, badgelocs_copy, badges, True, True, allow_partial=True)
- if badges:
+ if len(badges) > 8 - len(badgelocs):
for location in badgelocs:
if location.item:
badges.append(location.item)
@@ -299,9 +312,11 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations
for location in badgelocs:
if location.item:
fill_locations.remove(location)
+ progitempool += badges
break
else:
raise FillError(f"Failed to place badges for player {self.player}")
+ verify_hm_moves(self.multiworld, self, self.player)
if self.multiworld.key_items_only[self.player]:
return
@@ -345,102 +360,15 @@ def pre_fill(self) -> None:
for location in self.multiworld.get_locations(self.player):
if location.name in locs:
location.show_in_spoiler = False
-
- def intervene(move, test_state):
- if self.multiworld.randomize_wild_pokemon[self.player]:
- accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if
- loc.type == "Wild Encounter"]
-
- def number_of_zones(mon):
- zones = set()
- for loc in [slot for slot in accessible_slots if slot.item.name == mon]:
- zones.add(loc.name.split(" - ")[0])
- return len(zones)
-
- move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
- viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit]
- placed_mons = [slot.item.name for slot in accessible_slots]
-
- if self.multiworld.area_1_to_1_mapping[self.player]:
- placed_mons.sort(key=lambda i: number_of_zones(i))
- else:
- # this sort method doesn't work if you reference the same list being sorted in the lambda
- placed_mons_copy = placed_mons.copy()
- placed_mons.sort(key=lambda i: placed_mons_copy.count(i))
-
- placed_mon = placed_mons.pop()
- replace_mon = self.multiworld.random.choice(viable_mons)
- replace_slot = self.multiworld.random.choice([slot for slot in accessible_slots if slot.item.name
- == placed_mon])
- if self.multiworld.area_1_to_1_mapping[self.player]:
- zone = " - ".join(replace_slot.name.split(" - ")[:-1])
- replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name
- == placed_mon]
- for replace_slot in replace_slots:
- replace_slot.item = self.create_item(replace_mon)
- else:
- replace_slot.item = self.create_item(replace_mon)
- else:
- tms_hms = self.local_tms + poke_data.hm_moves
- flag = tms_hms.index(move)
- mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, self.player)]
- self.multiworld.random.shuffle(mon_list)
- mon_list.sort(key=lambda mon: self.local_move_data[move]["type"] not in
- [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]])
- for mon in mon_list:
- if test_state.has(mon, self.player):
- self.local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8)
- break
-
- last_intervene = None
- while True:
- intervene_move = None
- test_state = self.multiworld.get_all_state(False)
- if not logic.can_learn_hm(test_state, "Surf", self.player):
- intervene_move = "Surf"
- elif not logic.can_learn_hm(test_state, "Strength", self.player):
- intervene_move = "Strength"
- # cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off,
- # as you will require cut to access celadon gyn
- elif ((not logic.can_learn_hm(test_state, "Cut", self.player)) and
- (self.multiworld.accessibility[self.player] != "minimal" or ((not
- self.multiworld.badgesanity[self.player]) and max(
- self.multiworld.elite_four_badges_condition[self.player],
- self.multiworld.route_22_gate_condition[self.player],
- self.multiworld.victory_road_condition[self.player])
- > 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")))):
- intervene_move = "Cut"
- elif ((not logic.can_learn_hm(test_state, "Flash", self.player)) and self.multiworld.dark_rock_tunnel_logic[self.player]
- and (((self.multiworld.accessibility[self.player] != "minimal" and
- (self.multiworld.trainersanity[self.player] or self.multiworld.extra_key_items[self.player])) or
- self.multiworld.door_shuffle[self.player]))):
- intervene_move = "Flash"
- # If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps
- # as reachable, and if on no door shuffle or simple, fly is simply never necessary.
- # We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been
- # considered in door shuffle.
- elif ((not logic.can_learn_hm(test_state, "Fly", self.player)) and logic.can_learn_hm(test_state, "Fly", self.player)
- and self.multiworld.door_shuffle[self.player] not in
- ("off", "simple") and [self.fly_map, self.town_map_fly_map] != ["Pallet Town", "Pallet Town"]):
- intervene_move = "Fly"
- if intervene_move:
- if intervene_move == last_intervene:
- raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {self.player}")
- intervene(intervene_move, test_state)
- last_intervene = intervene_move
- else:
- break
+ verify_hm_moves(self.multiworld, self, self.player)
# Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not
# fail. Re-use test_state from previous final loop.
+ all_state = self.multiworld.get_all_state(False)
evolutions_region = self.multiworld.get_region("Evolution", self.player)
- clear_cache = False
for location in evolutions_region.locations.copy():
- if not test_state.can_reach(location, player=self.player):
+ if not all_state.can_reach(location, player=self.player):
evolutions_region.locations.remove(location)
- clear_cache = True
- if clear_cache:
- self.multiworld.clear_location_cache()
if self.multiworld.old_man[self.player] == "early_parcel":
self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1
@@ -456,13 +384,17 @@ def number_of_zones(mon):
locs = {self.multiworld.get_location("Fossil - Choice A", self.player),
self.multiworld.get_location("Fossil - Choice B", self.player)}
- for loc in locs:
+ if not self.multiworld.key_items_only[self.player]:
+ rule = None
if self.multiworld.fossil_check_item_types[self.player] == "key_items":
- add_item_rule(loc, lambda i: i.advancement)
+ rule = lambda i: i.advancement
elif self.multiworld.fossil_check_item_types[self.player] == "unique_items":
- add_item_rule(loc, lambda i: i.name in item_groups["Unique"])
+ rule = lambda i: i.name in item_groups["Unique"]
elif self.multiworld.fossil_check_item_types[self.player] == "no_key_items":
- add_item_rule(loc, lambda i: not i.advancement)
+ rule = lambda i: not i.advancement
+ if rule:
+ for loc in locs:
+ add_item_rule(loc, rule)
for mon in ([" ".join(self.multiworld.get_location(
f"Oak's Lab - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)]
@@ -511,7 +443,7 @@ def number_of_zones(mon):
self.multiworld.elite_four_pokedex_condition[self.player].total = \
int((len(reachable_mons) / 100) * self.multiworld.elite_four_pokedex_condition[self.player].value)
- if self.multiworld.accessibility[self.player] == "locations":
+ if self.multiworld.accessibility[self.player] == "full":
balls = [self.create_item(ball) for ball in ["Poke Ball", "Great Ball", "Ultra Ball"]]
traps = [self.create_item(trap) for trap in item_groups["Traps"]]
locations = [location for location in self.multiworld.get_locations(self.player) if "Pokedex - " in
@@ -548,24 +480,21 @@ def number_of_zones(mon):
else:
raise Exception("Failed to remove corresponding item while deleting unreachable Dexsanity location")
- self.multiworld._recache()
-
- if self.multiworld.door_shuffle[self.player] == "decoupled":
- swept_state = self.multiworld.state.copy()
- swept_state.sweep_for_events(player=self.player)
- locations = [location for location in
- self.multiworld.get_reachable_locations(swept_state, self.player) if location.item is
- None]
- self.multiworld.random.shuffle(locations)
- while len(locations) > 10:
- location = locations.pop()
- location.progress_type = LocationProgressType.EXCLUDED
-
- if self.multiworld.key_items_only[self.player]:
- locations = [location for location in self.multiworld.get_unfilled_locations(self.player) if
- location.progress_type == LocationProgressType.DEFAULT]
- for location in locations:
- location.progress_type = LocationProgressType.PRIORITY
+ @classmethod
+ def stage_post_fill(cls, multiworld):
+ # Convert all but one of each instance of a wild Pokemon to useful classification.
+ # This cuts down on time spent calculating the spoiler playthrough.
+ found_mons = set()
+ for sphere in multiworld.get_spheres():
+ for location in sphere:
+ if (location.game == "Pokemon Red and Blue" and (location.item.name in poke_data.pokemon_data.keys()
+ or "Static " in location.item.name)
+ and location.item.advancement):
+ key = (location.player, location.item.name)
+ if key in found_mons:
+ location.item.classification = ItemClassification.useful
+ else:
+ found_mons.add(key)
def create_regions(self):
if (self.multiworld.old_man[self.player] == "vanilla" or
@@ -611,6 +540,13 @@ def stage_generate_output(cls, multiworld, output_directory):
def generate_output(self, output_directory: str):
generate_output(self, output_directory)
+ def modify_multidata(self, multidata: dict):
+ rom_name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0',
+ 'utf8')[:21]
+ rom_name.extend([0] * (21 - len(rom_name)))
+ new_name = base64.b64encode(bytes(rom_name)).decode()
+ multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]
+
def write_spoiler_header(self, spoiler_handle: TextIO):
spoiler_handle.write(f"Cerulean Cave Total Key Items: {self.multiworld.cerulean_cave_key_items_condition[self.player].total}\n")
spoiler_handle.write(f"Elite Four Total Key Items: {self.multiworld.elite_four_key_items_condition[self.player].total}\n")
@@ -725,6 +661,9 @@ def fill_slot_data(self) -> dict:
"dark_rock_tunnel_logic": self.multiworld.dark_rock_tunnel_logic[self.player].value,
"split_card_key": self.multiworld.split_card_key[self.player].value,
"all_elevators_locked": self.multiworld.all_elevators_locked[self.player].value,
+ "require_pokedex": self.multiworld.require_pokedex[self.player].value,
+ "area_1_to_1_mapping": self.multiworld.area_1_to_1_mapping[self.player].value,
+ "blind_trainers": self.multiworld.blind_trainers[self.player].value,
}
@@ -739,4 +678,4 @@ def __init__(self, name, player: int = None):
name,
item_data.classification,
item_data.id, player
- )
\ No newline at end of file
+ )
diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4
index b7bdda7fbbed..0f65564a737b 100644
Binary files a/worlds/pokemon_rb/basepatch_blue.bsdiff4 and b/worlds/pokemon_rb/basepatch_blue.bsdiff4 differ
diff --git a/worlds/pokemon_rb/basepatch_red.bsdiff4 b/worlds/pokemon_rb/basepatch_red.bsdiff4
index 51440789fd47..826b7bf8b4e5 100644
Binary files a/worlds/pokemon_rb/basepatch_red.bsdiff4 and b/worlds/pokemon_rb/basepatch_red.bsdiff4 differ
diff --git a/worlds/pokemon_rb/client.py b/worlds/pokemon_rb/client.py
new file mode 100644
index 000000000000..97ca126476fd
--- /dev/null
+++ b/worlds/pokemon_rb/client.py
@@ -0,0 +1,278 @@
+import base64
+import logging
+import time
+
+from NetUtils import ClientStatus
+from worlds._bizhawk.client import BizHawkClient
+from worlds._bizhawk import read, write, guarded_write
+
+from .locations import location_data
+
+logger = logging.getLogger("Client")
+
+BANK_EXCHANGE_RATE = 50000000
+
+DATA_LOCATIONS = {
+ "ItemIndex": (0x1A6E, 0x02),
+ "Deathlink": (0x00FD, 0x01),
+ "APItem": (0x00FF, 0x01),
+ "EventFlag": (0x1735, 0x140),
+ "Missable": (0x161A, 0x20),
+ "Hidden": (0x16DE, 0x0E),
+ "Rod": (0x1716, 0x01),
+ "DexSanityFlag": (0x1A71, 19),
+ "GameStatus": (0x1A84, 0x01),
+ "Money": (0x141F, 3),
+ "ResetCheck": (0x0100, 4),
+ # First and second Vermilion Gym trash can selection. Second is not used, so should always be 0.
+ # First should never be above 0x0F. This is just before Event Flags.
+ "CrashCheck1": (0x1731, 2),
+ # Unused, should always be 0. This is just before Missables flags.
+ "CrashCheck2": (0x1617, 1),
+ # Progressive keys, should never be above 10. Just before Dexsanity flags.
+ "CrashCheck3": (0x1A70, 1),
+ # Route 18 Gate script value. Should never be above 3. Just before Hidden items flags.
+ "CrashCheck4": (0x16DD, 1),
+}
+
+location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}}
+location_bytes_bits = {}
+for location in location_data:
+ if location.ram_address is not None:
+ if type(location.ram_address) == list:
+ location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address
+ location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit},
+ {'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}]
+ else:
+ location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address
+ location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit}
+
+location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"
+ and location.address is not None}
+
+
+class PokemonRBClient(BizHawkClient):
+ system = ("GB", "SGB")
+ patch_suffix = (".apred", ".apblue")
+ game = "Pokemon Red and Blue"
+
+ def __init__(self):
+ super().__init__()
+ self.auto_hints = set()
+ self.locations_array = None
+ self.disconnect_pending = False
+ self.set_deathlink = False
+ self.banking_command = None
+ self.game_state = False
+ self.last_death_link = 0
+
+ async def validate_rom(self, ctx):
+ game_name = await read(ctx.bizhawk_ctx, [(0x134, 12, "ROM")])
+ game_name = game_name[0].decode("ascii")
+ if game_name in ("POKEMON RED\00", "POKEMON BLUE"):
+ ctx.game = self.game
+ ctx.items_handling = 0b001
+ ctx.command_processor.commands["bank"] = cmd_bank
+ seed_name = await read(ctx.bizhawk_ctx, [(0xFFDB, 21, "ROM")])
+ ctx.seed_name = seed_name[0].split(b"\0")[0].decode("ascii")
+ self.set_deathlink = False
+ self.banking_command = None
+ self.locations_array = None
+ self.disconnect_pending = False
+ return True
+ return False
+
+ async def set_auth(self, ctx):
+ auth_name = await read(ctx.bizhawk_ctx, [(0xFFC6, 21, "ROM")])
+ if auth_name[0] == bytes([0] * 21):
+ # rom was patched before rom names implemented, use player name
+ auth_name = await read(ctx.bizhawk_ctx, [(0xFFF0, 16, "ROM")])
+ auth_name = auth_name[0].decode("ascii").split("\x00")[0]
+ else:
+ auth_name = base64.b64encode(auth_name[0]).decode()
+ ctx.auth = auth_name
+
+ async def game_watcher(self, ctx):
+ if not ctx.server or not ctx.server.socket.open or ctx.server.socket.closed:
+ return
+
+ data = await read(ctx.bizhawk_ctx, [(loc_data[0], loc_data[1], "WRAM")
+ for loc_data in DATA_LOCATIONS.values()])
+ data = {data_set_name: data_name for data_set_name, data_name in zip(DATA_LOCATIONS.keys(), data)}
+
+ if self.set_deathlink:
+ self.set_deathlink = False
+ await ctx.update_death_link(True)
+
+ if self.disconnect_pending:
+ self.disconnect_pending = False
+ await ctx.disconnect()
+
+ if data["GameStatus"][0] == 0 or data["ResetCheck"] == b'\xff\xff\xff\x7f':
+ # Do not handle anything before game save is loaded
+ self.game_state = False
+ return
+ elif (data["GameStatus"][0] not in (0x2A, 0xAC)
+ or data["CrashCheck1"][0] & 0xF0 or data["CrashCheck1"][1] & 0xFF
+ or data["CrashCheck2"][0]
+ or data["CrashCheck3"][0] > 10
+ or data["CrashCheck4"][0] > 3):
+ # Should mean game crashed
+ logger.warning("Pokémon Red/Blue game may have crashed. Disconnecting from server.")
+ self.game_state = False
+ await ctx.disconnect()
+ return
+ self.game_state = True
+
+ # SEND ITEMS TO CLIENT
+
+ if data["APItem"][0] == 0:
+ item_index = int.from_bytes(data["ItemIndex"], "little")
+ if len(ctx.items_received) > item_index:
+ item_code = ctx.items_received[item_index].item - 172000000
+ if item_code > 255:
+ item_code -= 256
+ await write(ctx.bizhawk_ctx, [(DATA_LOCATIONS["APItem"][0],
+ [item_code], "WRAM")])
+
+ # LOCATION CHECKS
+
+ locations = set()
+
+ for flag_type, loc_map in location_map.items():
+ for flag, loc_id in loc_map.items():
+ if flag_type == "list":
+ if (data["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 <<
+ location_bytes_bits[loc_id][0]['bit']
+ and data["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 <<
+ location_bytes_bits[loc_id][1]['bit']):
+ locations.add(loc_id)
+ elif data[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']:
+ locations.add(loc_id)
+
+ if locations != self.locations_array:
+ if locations:
+ self.locations_array = locations
+ await ctx.send_msgs([{"cmd": "LocationChecks", "locations": list(locations)}])
+
+ # AUTO HINTS
+
+ hints = []
+ if data["EventFlag"][280] & 16:
+ hints.append("Cerulean Bicycle Shop")
+ if data["EventFlag"][280] & 32:
+ hints.append("Route 2 Gate - Oak's Aide")
+ if data["EventFlag"][280] & 64:
+ hints.append("Route 11 Gate 2F - Oak's Aide")
+ if data["EventFlag"][280] & 128:
+ hints.append("Route 15 Gate 2F - Oak's Aide")
+ if data["EventFlag"][281] & 1:
+ hints += ["Celadon Prize Corner - Item Prize 1", "Celadon Prize Corner - Item Prize 2",
+ "Celadon Prize Corner - Item Prize 3"]
+ if (location_name_to_id["Fossil - Choice A"] in ctx.checked_locations and location_name_to_id[
+ "Fossil - Choice B"]
+ not in ctx.checked_locations):
+ hints.append("Fossil - Choice B")
+ elif (location_name_to_id["Fossil - Choice B"] in ctx.checked_locations and location_name_to_id[
+ "Fossil - Choice A"]
+ not in ctx.checked_locations):
+ hints.append("Fossil - Choice A")
+ hints = [
+ location_name_to_id[loc] for loc in hints if location_name_to_id[loc] not in self.auto_hints and
+ location_name_to_id[loc] in ctx.missing_locations and
+ location_name_to_id[loc] not in ctx.locations_checked
+ ]
+ if hints:
+ await ctx.send_msgs([{"cmd": "LocationScouts", "locations": hints, "create_as_hint": 2}])
+ self.auto_hints.update(hints)
+
+ # DEATHLINK
+
+ if "DeathLink" in ctx.tags:
+ if data["Deathlink"][0] == 3:
+ await ctx.send_death(ctx.player_names[ctx.slot] + " is out of usable Pokémon! "
+ + ctx.player_names[ctx.slot] + " blacked out!")
+ await write(ctx.bizhawk_ctx, [(DATA_LOCATIONS["Deathlink"][0], [0], "WRAM")])
+ self.last_death_link = ctx.last_death_link
+ elif ctx.last_death_link > self.last_death_link:
+ self.last_death_link = ctx.last_death_link
+ await write(ctx.bizhawk_ctx, [(DATA_LOCATIONS["Deathlink"][0], [1], "WRAM")])
+
+ # BANK
+
+ if self.banking_command:
+ original_money = data["Money"]
+ # Money is stored as binary-coded decimal.
+ money = int(original_money.hex())
+ if self.banking_command > money:
+ logger.warning(f"You do not have ${self.banking_command} to deposit!")
+ elif (-self.banking_command * BANK_EXCHANGE_RATE) > (ctx.stored_data[f"EnergyLink{ctx.team}"] or 0):
+ logger.warning("Not enough money in the EnergyLink storage!")
+ else:
+ if self.banking_command + money > 999999:
+ self.banking_command = 999999 - money
+ money = str(money - self.banking_command).zfill(6)
+ money = [int(money[:2], 16), int(money[2:4], 16), int(money[4:], 16)]
+ money_written = await guarded_write(ctx.bizhawk_ctx, [(0x141F, money, "WRAM")],
+ [(0x141F, original_money, "WRAM")])
+ if money_written:
+ if self.banking_command >= 0:
+ deposit = self.banking_command - int(self.banking_command / 4)
+ tax = self.banking_command - deposit
+ logger.info(f"Deposited ${deposit}, and charged a tax of ${tax}.")
+ self.banking_command = deposit
+ else:
+ logger.info(f"Withdrew ${-self.banking_command}.")
+ await ctx.send_msgs([{
+ "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations":
+ [{"operation": "add", "value": self.banking_command * BANK_EXCHANGE_RATE},
+ {"operation": "max", "value": 0}],
+ }])
+ self.banking_command = None
+
+ # VICTORY
+
+ if data["EventFlag"][280] & 1 and not ctx.finished_game:
+ await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
+ ctx.finished_game = True
+
+ def on_package(self, ctx, cmd, args):
+ if cmd == 'Connected':
+ if 'death_link' in args['slot_data'] and args['slot_data']['death_link']:
+ self.set_deathlink = True
+ self.last_death_link = time.time()
+ ctx.set_notify(f"EnergyLink{ctx.team}")
+ elif cmd == 'RoomInfo':
+ if ctx.seed_name and ctx.seed_name != args["seed_name"]:
+ # CommonClient's on_package displays an error to the user in this case, but connection is not cancelled.
+ self.game_state = False
+ self.disconnect_pending = True
+ super().on_package(ctx, cmd, args)
+
+
+def cmd_bank(self, cmd: str = "", amount: str = ""):
+ """Deposit or withdraw money with the server's EnergyLink storage.
+ /bank - check server balance.
+ /bank deposit # - deposit money. One quarter of the amount will be lost to taxation.
+ /bank withdraw # - withdraw money."""
+ if self.ctx.game != "Pokemon Red and Blue":
+ logger.warning("This command can only be used while playing Pokémon Red and Blue")
+ return
+ if (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state:
+ logger.info(f"Must be connected to server and in game.")
+ return
+ elif not cmd:
+ logger.info(f"Money available: {int((self.ctx.stored_data[f'EnergyLink{self.ctx.team}'] or 0) / BANK_EXCHANGE_RATE)}")
+ return
+ elif not amount:
+ logger.warning("You must specify an amount.")
+ elif cmd == "withdraw":
+ self.ctx.client_handler.banking_command = -int(amount)
+ elif cmd == "deposit":
+ if int(amount) < 4:
+ logger.warning("You must deposit at least $4, for tax purposes.")
+ return
+ self.ctx.client_handler.banking_command = int(amount)
+ else:
+ logger.warning(f"Invalid bank command {cmd}")
+ return
diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md
index daefd6b2f7eb..1e5c14eb99f5 100644
--- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md
+++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md
@@ -1,8 +1,8 @@
# Pokémon Red and Blue
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -12,7 +12,7 @@ always able to be completed, but because of the item shuffle the player may need
would in the vanilla game.
A great many things besides item placement can be randomized, such as the location of Pokémon, their stats, types, etc.,
-depending on your yaml settings.
+depending on your yaml options.
Many baseline changes are made to the game, including:
@@ -21,13 +21,13 @@ Many baseline changes are made to the game, including:
* You can hold B to run (or bike extra fast!).
* You can hold select while talking to a trainer to re-battle them.
* You can select "Pallet Warp" below the "Continue" option to warp to Pallet Town as you load your save.
-* Mew can be encountered at the S.S. Anne dock truck. This can be randomized depending on your settings.
+* Mew can be encountered at the S.S. Anne dock truck. This can be randomized depending on your options.
* The S.S. Anne will never depart.
* Seafoam Islands entrances are swapped. This means you need Strength to travel through from Cinnabar Island to Fuchsia
City. You also cannot Surf onto the water from the end of Seafoam Islands going backwards if you have not yet dropped
the boulders.
* After obtaining one of the fossil item checks in Mt Moon, the remaining item can be received from the Cinnabar Lab
-fossil scientist. This may require reviving a number of fossils, depending on your settings.
+fossil scientist. This may require reviving a number of fossils, depending on your options.
* Obedience depends on the total number of badges you have obtained instead of depending on specific badges.
* Pokémon that evolve by trading can also evolve by reaching level 35.
* Evolution stones are reusable key items.
@@ -41,6 +41,8 @@ and repeatable source of money.
* You can disable and re-enable experience gains by talking to an aide in Oak's Lab.
* You can reset static encounters (Poké Flute encounter, legendaries, and the trap Poké Ball battles in Power Plant)
for any Pokémon you have defeated but not caught, by talking to an aide in Oak's Lab.
+* Dungeons normally hidden on the Town Map are now present, and the "Sea Cottage" has been removed. This is to allow
+Simple Door Shuffle to update the locations of all of the dungeons on the Town Map.
## What items and locations get shuffled?
@@ -80,3 +82,12 @@ All items for other games will display simply as "AP ITEM," including those for
A "received item" sound effect will play. Currently, there is no in-game message informing you of what the item is.
If you are in battle, have menus or text boxes opened, or scripted events are occurring, the items will not be given to
you until these have ended.
+
+## Unique Local Commands
+
+You can use `/bank` commands to deposit and withdraw money from the server's EnergyLink storage. This can be accessed by
+any players playing games that use the EnergyLink feature.
+
+- `/bank` - check the amount of money available on the server.
+- `/bank withdraw #` - withdraw money from the server.
+- `/bank deposit #` - deposit money into the server. 25% of the amount will be lost to taxation.
\ No newline at end of file
diff --git a/worlds/pokemon_rb/docs/setup_en.md b/worlds/pokemon_rb/docs/setup_en.md
index 488f3fdc0791..773fb14da9e7 100644
--- a/worlds/pokemon_rb/docs/setup_en.md
+++ b/worlds/pokemon_rb/docs/setup_en.md
@@ -7,23 +7,22 @@ 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)
- (select `Pokemon Client` during installation).
- Pokémon Red and/or Blue ROM files. The Archipelago community cannot provide these.
## Optional Software
-- [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/j-imbo/pkmnrb_jim/releases/latest), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases)
+- [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/coveleski/rb_tracker/releases/latest), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases)
## Configuring BizHawk
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**
@@ -48,7 +47,7 @@ an experience customized for their taste, and different players in the same mult
### Where do I get a YAML file?
-You can generate a yaml or download a template by visiting the [Pokemon Red and Blue Player Settings Page](/games/Pokemon%20Red%20and%20Blue/player-settings)
+You can generate a yaml or download a template by visiting the [Pokemon Red and Blue Player Options Page](/games/Pokemon%20Red%20and%20Blue/player-options)
It is important to note that the `game_version` option determines the ROM file that will be patched.
Both the player and the person generating (if they are generating locally) will need the corresponding ROM file.
@@ -57,7 +56,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>`
@@ -71,35 +70,49 @@ And the following special characters (these each take up one character):
## Joining a MultiWorld Game
-### Obtain your Pokémon patch file
+### Generating and Patching a Game
-When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
-the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
-files. Your data file should have a `.apred` or `.apblue` extension.
+1. Create your options file (YAML).
+2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
+This will generate an output file for you. Your patch file will have a `.apred` or `.apblue` file extension.
+3. Open `ArchipelagoLauncher.exe`
+4. Select "Open Patch" on the left side and select your patch file.
+5. If this is your first time patching, you will be prompted to locate your vanilla ROM.
+6. A patched `.gb` file will be created in the same place as the patch file.
+7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your
+BizHawk install.
-Double-click on your patch file to start your client and start the ROM patch process. Once the process is finished
-(this can take a while), the client and the emulator will be started automatically (if you associated the extension
-to the emulator as recommended).
+If you're playing a single-player seed and you don't care about autotracking or hints, you can stop here, close the
+client, and load the patched ROM in any emulator. However, for multiworlds and other Archipelago features, continue
+below using BizHawk as your emulator.
### Connect to the Multiserver
-Once both the client and the emulator are started, you must connect them. Navigate to your Archipelago install folder,
-then to `data/lua`, and drag+drop the `connector_pkmn_rb.lua` script onto the main EmuHawk window. (You could instead
-open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `connector_pkmn_rb.lua` with the file
-picker.)
+By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just
+in case you have to close and reopen a window mid-game for some reason.
+
+1. Pokémon Red and Blue use Archipelago's BizHawk Client. If the client isn't still open from when you patched your
+game, you can re-open it from the launcher.
+2. Ensure EmuHawk is running the patched ROM.
+3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing.
+4. In the Lua Console window, go to `Script > Open Script…`.
+5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
+6. The emulator may freeze every few seconds until it manages to connect to the client. This is expected. The BizHawk
+Client window should indicate that it connected and recognized Pokémon Red/Blue.
+7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the
+top text field of the client and click Connect.
To connect the client to the multiserver simply put `:` on the textfield on top and press enter (if the
server uses password, type in the bottom textfield `/connect : [password]`)
-Now you are ready to start your adventure in Kanto.
-
## Auto-Tracking
Pokémon Red and Blue has a fully functional map tracker that supports auto-tracking.
-1. Download [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/j-imbo/pkmnrb_jim/releases/latest) and [PopTracker](https://github.com/black-sliver/PopTracker/releases).
+1. Download [Pokémon Red and Blue Archipelago Map Tracker](https://github.com/coveleski/rb_tracker/releases/latest) and [PopTracker](https://github.com/black-sliver/PopTracker/releases).
2. Open PopTracker, and load the Pokémon Red and Blue pack.
3. Click on the "AP" symbol at the top.
4. Enter the AP address, slot name and password.
-The rest should take care of itself! Items and checks will be marked automatically, and it even knows your settings - It will hide checks & adjust logic accordingly.
+The rest should take care of itself! Items and checks will be marked automatically, and it even knows your options - It
+will hide checks & adjust logic accordingly.
diff --git a/worlds/pokemon_rb/docs/setup_es.md b/worlds/pokemon_rb/docs/setup_es.md
new file mode 100644
index 000000000000..6499c9501263
--- /dev/null
+++ b/worlds/pokemon_rb/docs/setup_es.md
@@ -0,0 +1,110 @@
+# GuÃa de instalación para Pokémon Red and Blue: Archipelago
+
+## Importante
+
+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.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.
+- El cliente incorporado de Archipelago, que se puede encontrar [aquÃ](https://github.com/ArchipelagoMW/Archipelago/releases)
+ (selecciona `Pokemon Client` durante la instalación).
+- Los ROMs originales de Pokémon Red y/o Blue. La comunidad de Archipelago no puede proveerlos.
+
+## Software Opcional
+
+- [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/coveleski/rb_tracker/releases/latest), para usar con [PopTracker](https://github.com/black-sliver/PopTracker/releases)
+
+
+## Configurando BizHawk
+
+Una vez que Bizhawk se haya instalado, abre Emuhawk y cambia las siguientes configuraciones:
+
+- (≤ 2.8) Abrir EmuHawk e ir a Config > Customize. Abrir la pestaña Advanced, y en la opción de Lua Core cambiar desde
+ "NLua+KopiLua" a "Lua+LuaInterface". Luego reinicia EmuHawk. Esto es fundamental para que el script de Lua funcione
+ correctamente.
+ **NOTA: Incluso si "Lua+LuaInterface" ya estaba seleccionado, cambia entre las opciones y vuélvelo a seleccionar.
+ Algunas instalaciones de versiones nuevas de EmuHawk tienen una tendencia a mostrar "Lua+LuaInterface" por defecto
+ pero siguen cargando "NLua+KopiLua" hasta completar este paso.**
+- Aun en la pestaña Advanced, asegurate que la casilla de AutoSaveRAM esté marcada, y selecciona también la casilla 5s.
+ Esto reduce la posibilidad de que se pierdan datos guardados en el caso de que el emulador deje de funcionar (crash).
+- En Config > Customize, pestaña General, marcar la casilla "Run in background". Esto evitará que te desconectes del
+ cliente mientras EmuHawk se está ejecutando en segundo plano.
+
+Es muy recomendado asociar los archivos GB (\*.gb) al emulador EmuHawk que se acaba de instalar.
+Para hacerlo, simplemente busca uno de los ROMs de gameboy, presiona con el clic derecho sobre él y selecciona
+"Abrir con...", despliega la lista que aparece y selecciona la opción al final de la lista "Buscar otra aplicación en
+el equipo", luego navega a la carpeta de Bizhawk y selecciona EmuHawk.exe.
+
+## Configura tu archivo YAML
+
+### ¿Qué es un archivo YAML y por qué necesito uno?
+
+Tu archivo YAML contiene un número de opciones que proveen al generador con información sobre como debe generar tu
+juego. Cada jugador de un multiworld entregará su propio archivo YAML. Esto permite que cada jugador disfrute de una
+experiencia personalizada a su manera, y que diferentes jugadores dentro del mismo multiworld pueden tener diferentes
+opciones.
+
+### ¿Dónde puedo obtener un archivo YAML?
+
+Puedes generar un archivo YAML or descargar su plantilla en la [página de configuración de jugador de Pokémon Red and Blue](/games/Pokemon%20Red%20and%20Blue/player-options)
+
+Es importante tener en cuenta que la opción `game_version` determina el ROM que será parcheado.
+Tanto el jugador como la persona que genera (si está generando localmente) necesitarán el archivo del ROM
+correspondiente.
+
+Para las opciones `trainer_name` y `rival_name`, los siguientes caracteres normales son permitidos:
+
+* `‘’“â€Â·â€¦ ABCDEFGHIJKLMNOPQRSTUVWXYZ():;[]abcdefghijklmnopqrstuvwxyzé'-?!.♂$×/,♀0123456789`
+
+Y los siguientes caracteres especiales (cada uno ocupa un carácter):
+* `<'d>`
+* `<'l>`
+* `<'t>`
+* `<'v>`
+* `<'r>`
+* `<'m>`
+* ``
+* ``
+* `` alias para `♂`
+* `` alias para `♀`
+
+## Unirse a un juego MultiWorld
+
+### Obtener tu parche de Pokémon
+
+Cuando te unes a un juego multiworld, se te pedirá que entregues tu archivo YAML a quien lo esté organizando.
+Una vez que la generación acabe, el anfitrión te dará un enlace a tu archivo, o un .zip con los archivos de
+todos. Tu archivo tiene una extensión `.apred` o `.apblue`.
+
+Haz doble clic en tu archivo `.apred` o `.apblue` para que se ejecute el cliente y realice el parcheado del ROM.
+Una vez acabe ese proceso (esto puede tardar un poco), el cliente y el emulador se abrirán automáticamente (si es que se
+ha asociado la extensión al emulador tal como fue recomendado)
+
+### Conectarse al multiserver
+
+Una vez ejecutado tanto el cliente como el emulador, hay que conectarlos. Abre la carpeta de instalación de Archipelago,
+luego abre `data/lua`, y simplemente arrastra el archivo `connector_pkmn_rb.lua` a la ventana principal de Emuhawk.
+(Alternativamente, puedes abrir la consola de Lua manualmente. En Emuhawk ir a Tools > Lua Console, luego ir al menú
+`Script` 〉 `Open Script`, navegar a la ubicación de `connector_pkmn_rb.lua` y seleccionarlo.)
+
+Para conectar el cliente con el servidor, simplemente pon `:` en la caja de texto superior y presiona
+enter (si el servidor tiene contraseña, en la caja de texto inferior escribir `/connect : [contraseña]`)
+
+Ahora ya estás listo para tu aventura en Kanto.
+
+## Auto-Tracking
+
+Pokémon Red and Blue tiene un mapa completamente funcional que soporta seguimiento automático.
+
+1. Descarga el [Tracker de mapa para Pokémon Red and Blue Archipelago](https://github.com/coveleski/rb_tracker/releases/latest) y [PopTracker](https://github.com/black-sliver/PopTracker/releases).
+2. Abre PopTracker, y carga el pack de Pokémon Red and Blue.
+3. Haz clic en el sÃmbolo "AP" en la parte superior.
+4. Ingresa la dirección de AP, nombre del slot y contraseña (si es que hay).
+
+¡Y ya, el resto deberÃa hacerse solo! Los items y checks serán marcados automáticamente, e incluso reconocerá tus
+configuraciones - Ocultará checks y ajustará la lógica según corresponda.
diff --git a/worlds/pokemon_rb/encounters.py b/worlds/pokemon_rb/encounters.py
index a426374c2e6e..6d1762b0ca71 100644
--- a/worlds/pokemon_rb/encounters.py
+++ b/worlds/pokemon_rb/encounters.py
@@ -197,7 +197,6 @@ def process_pokemon_locations(self):
mon = randomize_pokemon(self, original_mon, mons_list, 2, self.multiworld.random)
placed_mons[mon] += 1
location.item = self.create_item(mon)
- location.event = True
location.locked = True
location.item.location = location
locations.append(location)
@@ -269,7 +268,6 @@ def process_pokemon_locations(self):
for slot in encounter_slots:
location = self.multiworld.get_location(slot.name, self.player)
location.item = self.create_item(slot.original_item)
- location.event = True
location.locked = True
location.item.location = location
placed_mons[location.item.name] += 1
\ No newline at end of file
diff --git a/worlds/pokemon_rb/items.py b/worlds/pokemon_rb/items.py
index b584869f41b9..de29f341c6df 100644
--- a/worlds/pokemon_rb/items.py
+++ b/worlds/pokemon_rb/items.py
@@ -42,7 +42,7 @@ def __init__(self, item_id, classification, groups):
"Repel": ItemData(30, ItemClassification.filler, ["Consumables"]),
"Old Amber": ItemData(31, ItemClassification.progression_skip_balancing, ["Unique", "Fossils", "Key Items"]),
"Fire Stone": ItemData(32, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones", "Key Items"]),
- "Thunder Stone": ItemData(33, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones" "Key Items"]),
+ "Thunder Stone": ItemData(33, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones", "Key Items"]),
"Water Stone": ItemData(34, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones", "Key Items"]),
"HP Up": ItemData(35, ItemClassification.filler, ["Consumables", "Vitamins"]),
"Protein": ItemData(36, ItemClassification.filler, ["Consumables", "Vitamins"]),
@@ -119,11 +119,11 @@ def __init__(self, item_id, classification, groups):
"Card Key 11F": ItemData(109, ItemClassification.progression, ["Unique", "Key Items", "Card Keys"]),
"Progressive Card Key": ItemData(110, ItemClassification.progression, ["Unique", "Key Items", "Card Keys"]),
"Sleep Trap": ItemData(111, ItemClassification.trap, ["Traps"]),
- "HM01 Cut": ItemData(196, ItemClassification.progression, ["Unique", "HMs", "Key Items"]),
- "HM02 Fly": ItemData(197, ItemClassification.progression, ["Unique", "HMs", "Key Items"]),
- "HM03 Surf": ItemData(198, ItemClassification.progression, ["Unique", "HMs", "Key Items"]),
- "HM04 Strength": ItemData(199, ItemClassification.progression, ["Unique", "HMs", "Key Items"]),
- "HM05 Flash": ItemData(200, ItemClassification.progression, ["Unique", "HMs", "Key Items"]),
+ "HM01 Cut": ItemData(196, ItemClassification.progression, ["Unique", "HMs", "HM01", "Key Items"]),
+ "HM02 Fly": ItemData(197, ItemClassification.progression, ["Unique", "HMs", "HM02", "Key Items"]),
+ "HM03 Surf": ItemData(198, ItemClassification.progression, ["Unique", "HMs", "HM03", "Key Items"]),
+ "HM04 Strength": ItemData(199, ItemClassification.progression, ["Unique", "HMs", "HM04", "Key Items"]),
+ "HM05 Flash": ItemData(200, ItemClassification.progression, ["Unique", "HMs", "HM05", "Key Items"]),
"TM01 Mega Punch": ItemData(201, ItemClassification.useful, ["Unique", "TMs"]),
"TM02 Razor Wind": ItemData(202, ItemClassification.filler, ["Unique", "TMs"]),
"TM03 Swords Dance": ItemData(203, ItemClassification.useful, ["Unique", "TMs"]),
diff --git a/worlds/pokemon_rb/level_scaling.py b/worlds/pokemon_rb/level_scaling.py
index 5f3dfc1acd7c..79cda394724a 100644
--- a/worlds/pokemon_rb/level_scaling.py
+++ b/worlds/pokemon_rb/level_scaling.py
@@ -10,7 +10,9 @@ def level_scaling(multiworld):
while locations:
sphere = set()
for world in multiworld.get_game_worlds("Pokemon Red and Blue"):
- if multiworld.level_scaling[world.player] != "by_spheres_and_distance":
+ if (multiworld.level_scaling[world.player] != "by_spheres_and_distance"
+ and (multiworld.level_scaling[world.player] != "auto" or multiworld.door_shuffle[world.player]
+ in ("off", "simple"))):
continue
regions = {multiworld.get_region("Menu", world.player)}
checked_regions = set()
@@ -45,18 +47,18 @@ def reachable():
return True
if (("Rock Tunnel 1F - Wild Pokemon" in location.name
and any([multiworld.get_entrance(e, location.player).connected_region.can_reach(state)
- for e in ['Rock Tunnel 1F-NE to Route 10-N',
- 'Rock Tunnel 1F-NE to Rock Tunnel B1F-E',
- 'Rock Tunnel 1F-NW to Rock Tunnel B1F-E',
- 'Rock Tunnel 1F-NW to Rock Tunnel B1F-W',
- 'Rock Tunnel 1F-S to Route 10-S',
- 'Rock Tunnel 1F-S to Rock Tunnel B1F-W']])) or
+ for e in ['Rock Tunnel 1F-NE 1 to Route 10-N',
+ 'Rock Tunnel 1F-NE 2 to Rock Tunnel B1F-E 1',
+ 'Rock Tunnel 1F-NW 1 to Rock Tunnel B1F-E 2',
+ 'Rock Tunnel 1F-NW 2 to Rock Tunnel B1F-W 1',
+ 'Rock Tunnel 1F-S 1 to Route 10-S',
+ 'Rock Tunnel 1F-S 2 to Rock Tunnel B1F-W 2']])) or
("Rock Tunnel B1F - Wild Pokemon" in location.name and
any([multiworld.get_entrance(e, location.player).connected_region.can_reach(state)
- for e in ['Rock Tunnel B1F-E to Rock Tunnel 1F-NE',
- 'Rock Tunnel B1F-E to Rock Tunnel 1F-NW',
- 'Rock Tunnel B1F-W to Rock Tunnel 1F-NW',
- 'Rock Tunnel B1F-W to Rock Tunnel 1F-S']]))):
+ for e in ['Rock Tunnel B1F-E 1 to Rock Tunnel 1F-NE 2',
+ 'Rock Tunnel B1F-E 2 to Rock Tunnel 1F-NW 1',
+ 'Rock Tunnel B1F-W 1 to Rock Tunnel 1F-NW 2',
+ 'Rock Tunnel B1F-W 2 to Rock Tunnel 1F-S 2']]))):
# Even if checks in Rock Tunnel are out of logic due to lack of Flash, it is very easy to
# wander in the dark and encounter wild Pokémon, even unintentionally while attempting to
# leave the way you entered. We'll count the wild Pokémon as reachable as soon as the Rock
@@ -135,4 +137,3 @@ def reachable():
sphere_objects[object].level = level_list_copy.pop(0)
for world in multiworld.get_game_worlds("Pokemon Red and Blue"):
world.finished_level_scaling.set()
-
diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py
index ec6375859bb9..6aee25df2637 100644
--- a/worlds/pokemon_rb/locations.py
+++ b/worlds/pokemon_rb/locations.py
@@ -175,7 +175,7 @@ def __init__(self, flag):
LocationData("Route 2-SE", "South Item", "Moon Stone", rom_addresses["Missable_Route_2_Item_1"],
Missable(25)),
LocationData("Route 2-SE", "North Item", "HP Up", rom_addresses["Missable_Route_2_Item_2"], Missable(26)),
- LocationData("Route 4-E", "Item", "TM04 Whirlwind", rom_addresses["Missable_Route_4_Item"], Missable(27)),
+ LocationData("Route 4-C", "Item", "TM04 Whirlwind", rom_addresses["Missable_Route_4_Item"], Missable(27)),
LocationData("Route 9", "Item", "TM30 Teleport", rom_addresses["Missable_Route_9_Item"], Missable(28)),
LocationData("Route 12-N", "Island Item", "TM16 Pay Day", rom_addresses["Missable_Route_12_Item_1"], Missable(30)),
LocationData("Route 12-Grass", "Item Behind Cuttable Tree", "Iron", rom_addresses["Missable_Route_12_Item_2"], Missable(31)),
@@ -427,7 +427,7 @@ def __init__(self, flag):
LocationData("Seafoam Islands B3F", "Hidden Item Rock", "Max Elixir", rom_addresses['Hidden_Item_Seafoam_Islands_B3F'], Hidden(50), inclusion=hidden_items),
LocationData("Vermilion City", "Hidden Item In Water Near Fan Club", "Max Ether", rom_addresses['Hidden_Item_Vermilion_City'], Hidden(51), inclusion=hidden_items),
LocationData("Cerulean City-Badge House Backyard", "Hidden Item Gym Badge Guy's Backyard", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_City'], Hidden(52), inclusion=hidden_items),
- LocationData("Route 4-E", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53), inclusion=hidden_items),
+ LocationData("Route 4-C", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53), inclusion=hidden_items),
LocationData("Oak's Lab", "Oak's Parcel Reward", "Pokedex", rom_addresses["Event_Pokedex"], EventFlag(0x38)),
@@ -502,8 +502,8 @@ def __init__(self, flag):
LocationData("Mt Moon 1F", "Lass 2", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_2_ITEM"], EventFlag(134), inclusion=trainersanity),
LocationData("Mt Moon 1F", "Youngster", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_1_ITEM"], EventFlag(135), inclusion=trainersanity),
LocationData("Mt Moon 1F", "Hiker", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_0_ITEM"], EventFlag(136), inclusion=trainersanity),
- LocationData("Mt Moon B2F-NE", "Rocket 1", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_1_ITEM"], EventFlag(127), inclusion=trainersanity),
- LocationData("Mt Moon B2F-C", "Rocket 2", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_2_ITEM"], EventFlag(126), inclusion=trainersanity),
+ LocationData("Mt Moon B2F-C", "Rocket 1", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_1_ITEM"], EventFlag(127), inclusion=trainersanity),
+ LocationData("Mt Moon B2F-NE", "Rocket 2", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_2_ITEM"], EventFlag(126), inclusion=trainersanity),
LocationData("Mt Moon B2F", "Rocket 3", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_3_ITEM"], EventFlag(125), inclusion=trainersanity),
LocationData("Mt Moon B2F", "Rocket 4", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_0_ITEM"], EventFlag(128), inclusion=trainersanity),
LocationData("Viridian Forest", "Bug Catcher 1", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM"], EventFlag(139), inclusion=trainersanity),
@@ -636,7 +636,7 @@ def __init__(self, flag):
LocationData("Rock Tunnel B1F-W", "PokeManiac 3", None, rom_addresses["Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_2_ITEM"], EventFlag(11), inclusion=trainersanity),
LocationData("Route 10-N", "Jr. Trainer F 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_3_ITEM"], EventFlag(308), inclusion=trainersanity),
LocationData("Route 10-C", "PokeManiac 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_0_ITEM"], EventFlag(311), inclusion=trainersanity),
- LocationData("Route 10-S", "J.r Trainer F 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_5_ITEM"], EventFlag(306), inclusion=trainersanity),
+ LocationData("Route 10-S", "Jr. Trainer F 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_5_ITEM"], EventFlag(306), inclusion=trainersanity),
LocationData("Route 10-S", "Hiker 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_1_ITEM"], EventFlag(310), inclusion=trainersanity),
LocationData("Route 10-S", "Hiker 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_4_ITEM"], EventFlag(307), inclusion=trainersanity),
LocationData("Route 10-S", "PokeManiac 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_2_ITEM"], EventFlag(309), inclusion=trainersanity),
@@ -795,7 +795,7 @@ def __init__(self, flag):
LocationData("Pewter Gym", "Defeat Brock", "Defeat Brock", event=True),
LocationData("Cerulean Gym", "Defeat Misty", "Defeat Misty", event=True),
LocationData("Vermilion Gym", "Defeat Lt. Surge", "Defeat Lt. Surge", event=True),
- LocationData("Celadon Gym", "Defeat Erika", "Defeat Erika", event=True),
+ LocationData("Celadon Gym-C", "Defeat Erika", "Defeat Erika", event=True),
LocationData("Fuchsia Gym", "Defeat Koga", "Defeat Koga", event=True),
LocationData("Cinnabar Gym", "Defeat Blaine", "Defeat Blaine", event=True),
LocationData("Saffron Gym-C", "Defeat Sabrina", "Defeat Sabrina", event=True),
@@ -1036,25 +1036,25 @@ def __init__(self, flag):
type="Wild Encounter", level=12),
LocationData("Mt Moon B2F-Wild", "Wild Pokemon - 10", "Clefairy", rom_addresses["Wild_MtMoonB2F"] + 19, None,
event=True, type="Wild Encounter", level=12),
- LocationData("Route 4-Grass", "Wild Pokemon - 1", "Rattata", rom_addresses["Wild_Route4"] + 1, None, event=True,
+ LocationData("Route 4-E", "Wild Pokemon - 1", "Rattata", rom_addresses["Wild_Route4"] + 1, None, event=True,
type="Wild Encounter", level=10),
- LocationData("Route 4-Grass", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route4"] + 3, None, event=True,
+ LocationData("Route 4-E", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route4"] + 3, None, event=True,
type="Wild Encounter", level=10),
- LocationData("Route 4-Grass", "Wild Pokemon - 3", "Rattata", rom_addresses["Wild_Route4"] + 5, None, event=True,
+ LocationData("Route 4-E", "Wild Pokemon - 3", "Rattata", rom_addresses["Wild_Route4"] + 5, None, event=True,
type="Wild Encounter", level=8),
- LocationData("Route 4-Grass", "Wild Pokemon - 4", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 7, None,
+ LocationData("Route 4-E", "Wild Pokemon - 4", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 7, None,
event=True, type="Wild Encounter", level=6),
- LocationData("Route 4-Grass", "Wild Pokemon - 5", "Spearow", rom_addresses["Wild_Route4"] + 9, None, event=True,
+ LocationData("Route 4-E", "Wild Pokemon - 5", "Spearow", rom_addresses["Wild_Route4"] + 9, None, event=True,
type="Wild Encounter", level=8),
- LocationData("Route 4-Grass", "Wild Pokemon - 6", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 11, None,
+ LocationData("Route 4-E", "Wild Pokemon - 6", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 11, None,
event=True, type="Wild Encounter", level=10),
- LocationData("Route 4-Grass", "Wild Pokemon - 7", "Rattata", rom_addresses["Wild_Route4"] + 13, None, event=True,
+ LocationData("Route 4-E", "Wild Pokemon - 7", "Rattata", rom_addresses["Wild_Route4"] + 13, None, event=True,
type="Wild Encounter", level=12),
- LocationData("Route 4-Grass", "Wild Pokemon - 8", "Spearow", rom_addresses["Wild_Route4"] + 15, None, event=True,
+ LocationData("Route 4-E", "Wild Pokemon - 8", "Spearow", rom_addresses["Wild_Route4"] + 15, None, event=True,
type="Wild Encounter", level=12),
- LocationData("Route 4-Grass", "Wild Pokemon - 9", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 17, None,
+ LocationData("Route 4-E", "Wild Pokemon - 9", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 17, None,
event=True, type="Wild Encounter", level=8),
- LocationData("Route 4-Grass", "Wild Pokemon - 10", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 19, None,
+ LocationData("Route 4-E", "Wild Pokemon - 10", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 19, None,
event=True, type="Wild Encounter", level=12),
LocationData("Route 24", "Wild Pokemon - 1", ["Weedle", "Caterpie"], rom_addresses["Wild_Route24"] + 1, None,
event=True, type="Wild Encounter", level=7),
@@ -2310,43 +2310,50 @@ def __init__(self, flag):
'Cerulean Gym': [{'level': 19, 'party': ['Goldeen'], 'party_address': 'Trainer_Party_Cerulean_Gym_JrTrainerF_A'},
{'level': 16, 'party': ['Horsea', 'Shellder'],
'party_address': 'Trainer_Party_Cerulean_Gym_Swimmer_A'},
- {'level': [18, 21], 'party': ['Staryu', 'Starmie'], 'party_address': 'Trainer_Party_Misty_A'},], 'Route 10-N': [ ###
- {'level': 20, 'party': ['Pikachu', 'Clefairy'], 'party_address': 'Trainer_Party_Route_10_JrTrainerF_A'},
+ {'level': [18, 21], 'party': ['Staryu', 'Starmie'], 'party_address': 'Trainer_Party_Misty_A'},],
+ 'Route 10-N': [{'level': 20, 'party': ['Pikachu', 'Clefairy'], 'party_address': 'Trainer_Party_Route_10_JrTrainerF_A'}],
+ 'Route 10-C': [
+ {'level': 30, 'party': ['Rhyhorn', 'Lickitung'], 'party_address': 'Trainer_Party_Route_10_Pokemaniac_A'}],
+ 'Route 10-S': [
{'level': 21, 'party': ['Pidgey', 'Pidgeotto'], 'party_address': 'Trainer_Party_Route_10_JrTrainerF_B'},
- {'level': 30, 'party': ['Rhyhorn', 'Lickitung'], 'party_address': 'Trainer_Party_Route_10_Pokemaniac_A'},
{'level': 20, 'party': ['Cubone', 'Slowpoke'], 'party_address': 'Trainer_Party_Route_10_Pokemaniac_B'},
{'level': 21, 'party': ['Geodude', 'Onix'], 'party_address': 'Trainer_Party_Route_10_Hiker_A'},
{'level': 19, 'party': ['Onix', 'Graveler'], 'party_address': 'Trainer_Party_Route_10_Hiker_B'}],
- 'Rock Tunnel B1F-E': [{'level': 21, 'party': ['Jigglypuff', 'Pidgey', 'Meowth'], ###
+ 'Rock Tunnel B1F-W': [{'level': 21, 'party': ['Jigglypuff', 'Pidgey', 'Meowth'],
'party_address': 'Trainer_Party_Rock_Tunnel_B1F_JrTrainerF_A'},
- {'level': 22, 'party': ['Oddish', 'Bulbasaur'],
- 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_JrTrainerF_B'},
{'level': 20, 'party': ['Slowpoke', 'Slowpoke', 'Slowpoke'],
'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_A'},
- {'level': 22, 'party': ['Charmander', 'Cubone'],
- 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_B'},
- {'level': 25, 'party': ['Slowpoke'],
- 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_C'},
{'level': 21, 'party': ['Geodude', 'Geodude', 'Graveler'],
- 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_A'},
- {'level': 25, 'party': ['Geodude'], 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_B'},
- {'level': 20, 'party': ['Machop', 'Onix'],
- 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_D'}], 'Route 13': [
+ 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_A'},],
+ 'Rock Tunnel B1F-E': [
+ {'level': 22, 'party': ['Oddish', 'Bulbasaur'],
+ 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_JrTrainerF_B'},
+ {'level': 22, 'party': ['Charmander', 'Cubone'],
+ 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_B'},
+ {'level': 25, 'party': ['Slowpoke'],
+ 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_C'},
+ {'level': 25, 'party': ['Geodude'], 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_B'},
+ {'level': 20, 'party': ['Machop', 'Onix'],
+ 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_D'}],
+ 'Route 13-E': [
+ {'level': 28, 'party': ['Goldeen', 'Poliwag', 'Horsea'],
+ 'party_address': 'Trainer_Party_Route_13_JrTrainerF_D'},
+ {'level': 29, 'party': ['Pidgey', 'Pidgeotto'], 'party_address': 'Trainer_Party_Route_13_BirdKeeper_A'},
{'level': 24, 'party': ['Pidgey', 'Meowth', 'Rattata', 'Pikachu', 'Meowth'],
'party_address': 'Trainer_Party_Route_13_JrTrainerF_A'},
+ ],
+ 'Route 13': [
{'level': 30, 'party': ['Poliwag', 'Poliwag'], 'party_address': 'Trainer_Party_Route_13_JrTrainerF_B'},
{'level': 27, 'party': ['Pidgey', 'Meowth', 'Pidgey', 'Pidgeotto'],
'party_address': 'Trainer_Party_Route_13_JrTrainerF_C'},
- {'level': 28, 'party': ['Goldeen', 'Poliwag', 'Horsea'],
- 'party_address': 'Trainer_Party_Route_13_JrTrainerF_D'},
{'level': 28, 'party': ['Koffing', 'Koffing', 'Koffing'], 'party_address': 'Trainer_Party_Route_13_Biker_A'},
{'level': 27, 'party': ['Rattata', 'Pikachu', 'Rattata'], 'party_address': 'Trainer_Party_Route_13_Beauty_A'},
{'level': 29, 'party': ['Clefairy', 'Meowth'], 'party_address': 'Trainer_Party_Route_13_Beauty_B'},
- {'level': 29, 'party': ['Pidgey', 'Pidgeotto'], 'party_address': 'Trainer_Party_Route_13_BirdKeeper_A'},
{'level': 25, 'party': ['Spearow', 'Pidgey', 'Pidgey', 'Spearow', 'Spearow'],
'party_address': 'Trainer_Party_Route_13_BirdKeeper_B'},
{'level': 26, 'party': ['Pidgey', 'Pidgeotto', 'Spearow', 'Fearow'],
'party_address': 'Trainer_Party_Route_13_BirdKeeper_C'}],
+
'Route 20-E': [
{'level': 31, 'party': ['Shellder', 'Cloyster'], 'party_address': 'Trainer_Party_Route_20_Swimmer_A'},
{'level': 28, 'party': ['Horsea', 'Horsea', 'Seadra', 'Horsea'],
@@ -2354,9 +2361,9 @@ def __init__(self, flag):
'party_address': 'Trainer_Party_Route_20_Swimmer_C'},
{'level': 30, 'party': ['Seadra', 'Horsea', 'Seadra'],
- 'party_address': 'Trainer_Party_Route_20_Beauty_E'}],
+ 'party_address': 'Trainer_Party_Route_20_Beauty_E'},
+ {'level': 35, 'party': ['Seaking'], 'party_address': 'Trainer_Party_Route_20_Beauty_A'},],
'Route 20-W': [
- {'level': 35, 'party': ['Seaking'], 'party_address': 'Trainer_Party_Route_20_Beauty_A'},
{'level': 31, 'party': ['Goldeen', 'Seaking'], 'party_address': 'Trainer_Party_Route_20_JrTrainerF_A'},
{'level': 30, 'party': ['Tentacool', 'Horsea', 'Seel'],
'party_address': 'Trainer_Party_Route_20_JrTrainerF_C'},
@@ -2374,16 +2381,19 @@ def __init__(self, flag):
{'level': 20, 'party': ['Meowth', 'Oddish', 'Pidgey'],
'party_address': 'Trainer_Party_Rock_Tunnel_1F_JrTrainerF_B'},
{'level': 19, 'party': ['Pidgey', 'Rattata', 'Rattata', 'Bellsprout'],
- 'party_address': 'Trainer_Party_Rock_Tunnel_1F_JrTrainerF_C'},
- {'level': 23, 'party': ['Cubone', 'Slowpoke'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_Pokemaniac_A'},
+ 'party_address': 'Trainer_Party_Rock_Tunnel_1F_JrTrainerF_C'}],
+ 'Rock Tunnel 1F-NE': [
+ {'level': 23, 'party': ['Cubone', 'Slowpoke'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_Pokemaniac_A'}],
+ 'Rock Tunnel 1F-NW': [
{'level': 19, 'party': ['Geodude', 'Machop', 'Geodude', 'Geodude'],
'party_address': 'Trainer_Party_Rock_Tunnel_1F_Hiker_A'},
{'level': 20, 'party': ['Onix', 'Onix', 'Geodude'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_Hiker_B'},
{'level': 21, 'party': ['Geodude', 'Graveler'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_Hiker_C'}],
+ 'Route 15-N': [
+ {'level': 33, 'party': ['Clefairy'], 'party_address': 'Trainer_Party_Route_15_JrTrainerF_C'}],
'Route 15': [
{'level': 28, 'party': ['Gloom', 'Oddish', 'Oddish'], 'party_address': 'Trainer_Party_Route_15_JrTrainerF_A'},
{'level': 29, 'party': ['Pikachu', 'Raichu'], 'party_address': 'Trainer_Party_Route_15_JrTrainerF_B'},
- {'level': 33, 'party': ['Clefairy'], 'party_address': 'Trainer_Party_Route_15_JrTrainerF_C'},
{'level': 29, 'party': ['Bellsprout', 'Oddish', 'Tangela'],
'party_address': 'Trainer_Party_Route_15_JrTrainerF_D'},
{'level': 25, 'party': ['Koffing', 'Koffing', 'Weezing', 'Koffing', 'Grimer'],
@@ -2394,15 +2404,16 @@ def __init__(self, flag):
{'level': 26, 'party': ['Pidgeotto', 'Farfetchd', 'Doduo', 'Pidgey'],
'party_address': 'Trainer_Party_Route_15_BirdKeeper_A'},
{'level': 28, 'party': ['Dodrio', 'Doduo', 'Doduo'], 'party_address': 'Trainer_Party_Route_15_BirdKeeper_B'}],
- 'Victory Road 2F-C': [{'level': 40, 'party': ['Charmeleon', 'Lapras', 'Lickitung'], ###
- 'party_address': 'Trainer_Party_Victory_Road_2F_Pokemaniac_A'},
- {'level': 41, 'party': ['Drowzee', 'Hypno', 'Kadabra', 'Kadabra'],
- 'party_address': 'Trainer_Party_Victory_Road_2F_Juggler_A'},
- {'level': 48, 'party': ['Mr Mime'], 'party_address': 'Trainer_Party_Victory_Road_2F_Juggler_C'},
- {'level': 44, 'party': ['Persian', 'Golduck'],
- 'party_address': 'Trainer_Party_Victory_Road_2F_Tamer_A'},
- {'level': 43, 'party': ['Machoke', 'Machop', 'Machoke'],
- 'party_address': 'Trainer_Party_Victory_Road_2F_Blackbelt_A'}], 'Mt Moon B2F': [
+ 'Victory Road 2F-NW': [{'level': 40, 'party': ['Charmeleon', 'Lapras', 'Lickitung'],
+ 'party_address': 'Trainer_Party_Victory_Road_2F_Pokemaniac_A'}],
+ 'Victory Road 2F-C': [
+ {'level': 41, 'party': ['Drowzee', 'Hypno', 'Kadabra', 'Kadabra'],
+ 'party_address': 'Trainer_Party_Victory_Road_2F_Juggler_A'},
+ {'level': 48, 'party': ['Mr Mime'], 'party_address': 'Trainer_Party_Victory_Road_2F_Juggler_C'},
+ {'level': 44, 'party': ['Persian', 'Golduck'],
+ 'party_address': 'Trainer_Party_Victory_Road_2F_Tamer_A'},
+ {'level': 43, 'party': ['Machoke', 'Machop', 'Machoke'],
+ 'party_address': 'Trainer_Party_Victory_Road_2F_Blackbelt_A'}], 'Mt Moon B2F': [
{'level': 12, 'party': ['Grimer', 'Voltorb', 'Koffing'],
'party_address': 'Trainer_Party_Mt_Moon_B2F_SuperNerd_A'},
{'level': 13, 'party': ['Rattata', 'Zubat'], 'party_address': 'Trainer_Party_Mt_Moon_B2F_Rocket_A'},
@@ -2585,7 +2596,7 @@ def __init__(self, flag):
['Pidgeotto', 'Abra', 'Rattata', 'Charmander']],
'party_address': ['Trainer_Party_Cerulean_City_Green1_A', 'Trainer_Party_Cerulean_City_Green1_B', 'Trainer_Party_Cerulean_City_Green1_C']},
{'level': 17, 'party': ['Machop', 'Drowzee'],
- 'party_address': 'Trainer_Party_Cerulean_City_Rocket_A'}], 'Pokemon Mansion 1F': [
+ 'party_address': 'Trainer_Party_Cerulean_City_Rocket_A'}], 'Pokemon Mansion 1F-SE': [
{'level': 29, 'party': ['Electrode', 'Weezing'], 'party_address': 'Trainer_Party_Mansion_1F_Scientist_A'}],
'Silph Co 2F-SW': [{'level': 26, 'party': ['Grimer', 'Weezing', 'Koffing', 'Weezing'],
'party_address': 'Trainer_Party_Silph_Co_2F_Scientist_A'}],
@@ -2595,7 +2606,7 @@ def __init__(self, flag):
{'level': 25, 'party': ['Golbat', 'Zubat', 'Zubat', 'Raticate', 'Zubat'],
'party_address': 'Trainer_Party_Silph_Co_2F_Rocket_B'}], 'Silph Co 3F-W': [
{'level': 29, 'party': ['Electrode', 'Weezing'], 'party_address': 'Trainer_Party_Silph_Co_3F_Scientist_A'}],
- 'Silph Co 3F': [ {'level': 28, 'party': ['Raticate', 'Hypno', 'Raticate'],
+ 'Silph Co 3F': [{'level': 28, 'party': ['Raticate', 'Hypno', 'Raticate'],
'party_address': 'Trainer_Party_Silph_Co_3F_Rocket_A'}],
'Silph Co 4F-N': [{'level': 33, 'party': ['Electrode'], 'party_address': 'Trainer_Party_Silph_Co_4F_Scientist_A'}],
'Silph Co 4F': [{'level': 29, 'party': ['Machop', 'Drowzee'],
@@ -2670,15 +2681,17 @@ def __init__(self, flag):
{'level': 26, 'party': ['Koffing', 'Drowzee'],
'party_address': 'Trainer_Party_Pokemon_Tower_7F_Rocket_B'},
{'level': 23, 'party': ['Zubat', 'Rattata', 'Raticate', 'Zubat'],
- 'party_address': 'Trainer_Party_Pokemon_Tower_7F_Rocket_C'}], 'Victory Road 3F': [
- {'level': 43, 'party': ['Exeggutor', 'Cloyster', 'Arcanine'],
+ 'party_address': 'Trainer_Party_Pokemon_Tower_7F_Rocket_C'}],
+ 'Victory Road 3F': [{'level': 43, 'party': ['Exeggutor', 'Cloyster', 'Arcanine'],
'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerM_A'},
+ {'level': 43, 'party': ['Parasect', 'Dewgong', 'Chansey'],
+ 'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerF_B'}],
+ 'Victory Road 3F-S': [
{'level': 43, 'party': ['Kingler', 'Tentacruel', 'Blastoise'],
'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerM_B'},
{'level': 43, 'party': ['Bellsprout', 'Weepinbell', 'Victreebel'],
'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerF_A'},
- {'level': 43, 'party': ['Parasect', 'Dewgong', 'Chansey'],
- 'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerF_B'}], 'Victory Road 1F': [
+], 'Victory Road 1F': [
{'level': 42, 'party': ['Ivysaur', 'Wartortle', 'Charmeleon', 'Charizard'],
'party_address': 'Trainer_Party_Victory_Road_1F_CooltrainerM_A'},
{'level': 44, 'party': ['Persian', 'Ninetales'],
diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py
index 794977d32d36..54d486a6cf9f 100644
--- a/worlds/pokemon_rb/options.py
+++ b/worlds/pokemon_rb/options.py
@@ -1,4 +1,4 @@
-from Options import Toggle, Choice, Range, SpecialRange, TextChoice, DeathLink
+from Options import Toggle, Choice, Range, NamedRange, TextChoice, DeathLink, ItemsAccessibility
class GameVersion(Choice):
@@ -228,7 +228,7 @@ class SplitCardKey(Choice):
class AllElevatorsLocked(Toggle):
"""Adds requirements to the Celadon Department Store elevator and Silph Co elevators to have the Lift Key.
- No logical implications normally, but may have a significant impact on Insanity Door Shuffle."""
+ No logical implications normally, but may have a significant impact on some Door Shuffle options."""
display_name = "All Elevators Locked"
default = 1
@@ -285,9 +285,9 @@ class AllPokemonSeen(Toggle):
display_name = "All Pokemon Seen"
-class DexSanity(SpecialRange):
+class DexSanity(NamedRange):
"""Adds location checks for Pokemon flagged "owned" on your Pokedex. You may specify a percentage of Pokemon to
- have checks added. If Accessibility is set to locations, this will be the percentage of all logically reachable
+ have checks added. If Accessibility is set to full, this will be the percentage of all logically reachable
Pokemon that will get a location check added to it. With items or minimal Accessibility, it will be the percentage
of all 151 Pokemon.
If Pokedex is required, the items for Pokemon acquired before acquiring the Pokedex can be found by talking to
@@ -317,42 +317,42 @@ class TownMapFlyLocation(Toggle):
class DoorShuffle(Choice):
"""Simple: entrances are randomized together in groups: Pokemarts, Gyms, single exit dungeons, dual exit dungeons,
single exit misc interiors, dual exit misc interiors are all shuffled separately. Safari Zone is not shuffled.
- Full: Any outdoor entrance may lead to any interior.
- Insanity: All rooms in the game are shuffled."""
+ On Simple only, the Town Map will be updated to show the new locations for each dungeon.
+ Interiors: Any outdoor entrance may lead to any interior, but intra-interior doors are not shuffled. Previously
+ named Full.
+ Full: Exterior to interior entrances are shuffled, and interior to interior doors are shuffled, separately.
+ Insanity: All doors in the game are shuffled.
+ Decoupled: Doors may be decoupled from each other, so that leaving through an exit may not return you to the
+ door you entered from."""
display_name = "Door Shuffle"
option_off = 0
option_simple = 1
- option_full = 2
- option_insanity = 3
- # Disabled for now, has issues with elevators that need to be resolved
- # option_decoupled = 4
- default = 0
-
- # remove assertions that blow up checks for decoupled
- def __eq__(self, other):
- if isinstance(other, self.__class__):
- return other.value == self.value
- elif isinstance(other, str):
- return other == self.current_key
- elif isinstance(other, int):
- return other == self.value
- elif isinstance(other, bool):
- return other == bool(self.value)
- else:
- raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
-
-
-class WarpTileShuffle(Toggle):
- """Shuffle the warp tiles in Silph Co and Sabrina's Gym among themselves, separately.
- On Insanity, turning this off means they are mixed into the general door shuffle instead of only being shuffled
- among themselves."""
+ option_interiors = 2
+ option_full = 3
+ option_insanity = 4
+ option_decoupled = 5
+ default = 0
+
+
+class WarpTileShuffle(Choice):
+ """Vanilla: The warp tiles in Silph Co and Sabrina's Gym are not changed.
+ Shuffle: The warp tile destinations are shuffled among themselves.
+ Mixed: The warp tiles are mixed into the pool of available doors for Full, Insanity, and Decoupled. Same as Shuffle
+ for any other door shuffle option."""
display_name = "Warp Tile Shuffle"
default = 0
+ option_vanilla = 0
+ option_shuffle = 1
+ option_mixed = 2
+ alias_true = 1
+ alias_on = 1
+ alias_off = 0
+ alias_false = 0
class RandomizeRockTunnel(Toggle):
- """Randomize the layout of Rock Tunnel.
- If Insanity Door Shuffle is on, this will cause only the main entrances to Rock Tunnel to be shuffled."""
+ """Randomize the layout of Rock Tunnel. If Full, Insanity, or Decoupled Door Shuffle is on, this will cause only the
+ main entrances to Rock Tunnel to be shuffled."""
display_name = "Randomize Rock Tunnel"
default = 0
@@ -401,18 +401,20 @@ class Stonesanity(Toggle):
class LevelScaling(Choice):
"""Off: Encounters use vanilla game levels.
By Spheres: Levels are scaled by access sphere. Areas reachable in later spheres will have higher levels.
- Spheres and Distance: Levels are scaled by access spheres as well as distance from Pallet Town, measured by number
- of internal region connections. This is a much more severe curving of levels and may lead to much less variation in
- levels found in a particular map. However, it may make the higher door shuffle settings significantly more bearable,
- as these options more often result in a smaller number of larger access spheres."""
+ By Spheres and Distance: Levels are scaled by access spheres as well as distance from Pallet Town, measured by
+ number of internal region connections. This is a much more severe curving of levels and may lead to much less
+ variation in levels found in a particular map. However, it may make the higher door shuffle settings significantly
+ more bearable, as these options more often result in a smaller number of larger access spheres.
+ Auto: Scales by Spheres if Door Shuffle is off or on Simple, otherwise scales by Spheres and Distance"""
display_name = "Level Scaling"
option_off = 0
option_by_spheres = 1
option_by_spheres_and_distance = 2
- default = 1
+ option_auto = 3
+ default = 3
-class ExpModifier(SpecialRange):
+class ExpModifier(NamedRange):
"""Modifier for EXP gained. When specifying a number, exp is multiplied by this amount and divided by 16."""
display_name = "Exp Modifier"
default = 16
@@ -607,8 +609,8 @@ class RandomizeTMMoves(Toggle):
display_name = "Randomize TM Moves"
-class TMHMCompatibility(SpecialRange):
- range_start = -1
+class TMHMCompatibility(NamedRange):
+ range_start = 0
range_end = 100
special_range_names = {
"vanilla": -1,
@@ -675,12 +677,12 @@ class RandomizeMoveTypes(Toggle):
default = 0
-class SecondaryTypeChance(SpecialRange):
+class SecondaryTypeChance(NamedRange):
"""If randomize_pokemon_types is on, this is the chance each Pokemon will have a secondary type. If follow_evolutions
is selected, it is the chance a second type will be added at each evolution stage. vanilla will give secondary types
to Pokemon that normally have a secondary type."""
display_name = "Secondary Type Chance"
- range_start = -1
+ range_start = 0
range_end = 100
default = -1
special_range_names = {
@@ -859,6 +861,7 @@ class RandomizePokemonPalettes(Choice):
pokemon_rb_options = {
+ "accessibility": ItemsAccessibility,
"game_version": GameVersion,
"trainer_name": TrainerName,
"rival_name": RivalName,
diff --git a/worlds/pokemon_rb/pokemon.py b/worlds/pokemon_rb/pokemon.py
index 267f424ca89a..28098a2c53fe 100644
--- a/worlds/pokemon_rb/pokemon.py
+++ b/worlds/pokemon_rb/pokemon.py
@@ -1,5 +1,5 @@
from copy import deepcopy
-from . import poke_data
+from . import poke_data, logic
from .rom_addresses import rom_addresses
@@ -135,7 +135,6 @@ def process_pokemon_data(self):
learnsets = deepcopy(poke_data.learnsets)
tms_hms = self.local_tms + poke_data.hm_moves
-
compat_hms = set()
for mon, mon_data in local_poke_data.items():
@@ -323,19 +322,20 @@ def roll_tm_compat(roll_move):
mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8))
hm_verify = ["Surf", "Strength"]
- if self.multiworld.accessibility[self.player] == "locations" or ((not
+ if self.multiworld.accessibility[self.player] != "minimal" or ((not
self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_badges_condition[self.player],
self.multiworld.route_22_gate_condition[self.player], self.multiworld.victory_road_condition[self.player])
> 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")):
hm_verify += ["Cut"]
- if self.multiworld.accessibility[self.player] == "locations" or (not
+ if self.multiworld.accessibility[self.player] != "minimal" or (not
self.multiworld.dark_rock_tunnel_logic[self.player]) and ((self.multiworld.trainersanity[self.player] or
self.multiworld.extra_key_items[self.player])
or self.multiworld.door_shuffle[self.player]):
hm_verify += ["Flash"]
- # Fly does not need to be verified. Full/Insanity door shuffle connects reachable regions to unreachable regions,
- # so if Fly is available and can be learned, the towns you can fly to would be reachable, but if no Pokémon can
- # learn it this simply would not occur
+ # Fly does not need to be verified. Full/Insanity/Decoupled door shuffle connects reachable regions to unreachable
+ # regions, so if Fly is available and can be learned, the towns you can fly to would be considered reachable for
+ # door shuffle purposes, but if no Pokémon can learn it, that connection would just be out of logic and it would
+ # ensure connections to those towns.
for hm_move in hm_verify:
if hm_move not in compat_hms:
@@ -346,3 +346,90 @@ def roll_tm_compat(roll_move):
self.local_poke_data = local_poke_data
self.learnsets = learnsets
+
+
+def verify_hm_moves(multiworld, world, player):
+ def intervene(move, test_state):
+ move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
+ viable_mons = [mon for mon in world.local_poke_data if world.local_poke_data[mon]["tms"][6] & move_bit]
+ if multiworld.randomize_wild_pokemon[player] and viable_mons:
+ accessible_slots = [loc for loc in multiworld.get_reachable_locations(test_state, player) if
+ loc.type == "Wild Encounter"]
+
+ def number_of_zones(mon):
+ zones = set()
+ for loc in [slot for slot in accessible_slots if slot.item.name == mon]:
+ zones.add(loc.name.split(" - ")[0])
+ return len(zones)
+
+ placed_mons = [slot.item.name for slot in accessible_slots]
+
+ if multiworld.area_1_to_1_mapping[player]:
+ placed_mons.sort(key=lambda i: number_of_zones(i))
+ else:
+ # this sort method doesn't work if you reference the same list being sorted in the lambda
+ placed_mons_copy = placed_mons.copy()
+ placed_mons.sort(key=lambda i: placed_mons_copy.count(i))
+
+ placed_mon = placed_mons.pop()
+ replace_mon = multiworld.random.choice(viable_mons)
+ replace_slot = multiworld.random.choice([slot for slot in accessible_slots if slot.item.name
+ == placed_mon])
+ if multiworld.area_1_to_1_mapping[player]:
+ zone = " - ".join(replace_slot.name.split(" - ")[:-1])
+ replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name
+ == placed_mon]
+ for replace_slot in replace_slots:
+ replace_slot.item = world.create_item(replace_mon)
+ else:
+ replace_slot.item = world.create_item(replace_mon)
+ else:
+ tms_hms = world.local_tms + poke_data.hm_moves
+ flag = tms_hms.index(move)
+ mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, player)]
+ multiworld.random.shuffle(mon_list)
+ mon_list.sort(key=lambda mon: world.local_move_data[move]["type"] not in
+ [world.local_poke_data[mon]["type1"], world.local_poke_data[mon]["type2"]])
+ for mon in mon_list:
+ if test_state.has(mon, player):
+ world.local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8)
+ break
+
+ last_intervene = None
+ while True:
+ intervene_move = None
+ test_state = multiworld.get_all_state(False)
+ if not logic.can_learn_hm(test_state, "Surf", player):
+ intervene_move = "Surf"
+ elif not logic.can_learn_hm(test_state, "Strength", player):
+ intervene_move = "Strength"
+ # cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off,
+ # as you will require cut to access celadon gyn
+ elif ((not logic.can_learn_hm(test_state, "Cut", player)) and
+ (multiworld.accessibility[player] != "minimal" or ((not
+ multiworld.badgesanity[player]) and max(
+ multiworld.elite_four_badges_condition[player],
+ multiworld.route_22_gate_condition[player],
+ multiworld.victory_road_condition[player])
+ > 7) or (multiworld.door_shuffle[player] not in ("off", "simple")))):
+ intervene_move = "Cut"
+ elif ((not logic.can_learn_hm(test_state, "Flash", player))
+ and multiworld.dark_rock_tunnel_logic[player]
+ and (multiworld.accessibility[player] != "minimal"
+ or multiworld.door_shuffle[player])):
+ intervene_move = "Flash"
+ # If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps
+ # as reachable, and if on no door shuffle or simple, fly is simply never necessary.
+ # We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been
+ # considered in door shuffle.
+ elif ((not logic.can_learn_hm(test_state, "Fly", player))
+ and multiworld.door_shuffle[player] not in
+ ("off", "simple") and [world.fly_map, world.town_map_fly_map] != ["Pallet Town", "Pallet Town"]):
+ intervene_move = "Fly"
+ if intervene_move:
+ if intervene_move == last_intervene:
+ raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {player}")
+ intervene(intervene_move, test_state)
+ last_intervene = intervene_move
+ else:
+ break
\ No newline at end of file
diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py
index cc788dd2ba5c..a9206fe66753 100644
--- a/worlds/pokemon_rb/regions.py
+++ b/worlds/pokemon_rb/regions.py
@@ -256,6 +256,22 @@
"Indigo Plateau Agatha's Room": 0xF7,
}
+town_map_coords = {
+ "Route 2-SW": ("Viridian Forest South Gate to Route 2-SW", 2, 4, (3,), "Viridian Forest", 4), #ViridianForestName
+ "Route 2-NE": ("Diglett's Cave Route 2 to Route 2-NE", 3, 4, (48,), "Diglett's Cave", 5), #DiglettsCaveName
+ "Route 4-W": ("Mt Moon 1F to Route 4-W", 6, 2, (5,), "Mt Moon 1F", 8), #MountMoonName
+ "Cerulean City-Cave": ("Cerulean Cave 1F-SE to Cerulean City-Cave", 9, 1, (54,), "Cerulean Cave 1F", 11), #CeruleanCaveName
+ "Vermilion City-Dock": ("Vermilion Dock to Vermilion City-Dock", 9, 10, (19,), "S.S. Anne 1F", 17), #SSAnneName
+ "Route 10-N": ("Rock Tunnel 1F-NE 1 to Route 10-N", 14, 3, (13, 57), "Rock Tunnel Pokemon Center", 19), #RockTunnelName
+ "Lavender Town": ("Pokemon Tower 1F to Lavender Town", 15, 5, (27,), "Pokemon Tower 2F", 22), #PokemonTowerName
+ "Celadon Game Corner-Hidden Stairs": ("Rocket Hideout B1F to Celadon Game Corner-Hidden Stairs", 7, 5, (50,), "Rocket Hideout B1F", 26), #RocketHQName
+ "Saffron City-Silph": ("Silph Co 1F to Saffron City-Silph", 10, 5, (51, 58), "Silph Co 2F", 28), #SilphCoName
+ "Route 20-IE": ("Seafoam Islands 1F to Route 20-IE", 5, 15, (32,), "Seafoam Islands B1F", 40), #SeafoamIslandsName
+ "Cinnabar Island-M": ("Pokemon Mansion 1F to Cinnabar Island-M", 2, 15, (35, 52), "Pokemon Mansion 1F", 43), #PokemonMansionName
+ "Route 23-C": ("Victory Road 1F-S to Route 23-C", 0, 4, (20, 45, 49), "Victory Road 1F", 47), #VictoryRoadName
+ "Route 10-P": ("Power Plant to Route 10-P", 15, 4, (14,), "Power Plant", 49), #PowerPlantName
+}
+
warp_data = {'Menu': [], 'Evolution': [], 'Old Rod Fishing': [], 'Good Rod Fishing': [], 'Fossil Level': [],
'Pokedex': [], 'Fossil': [], 'Celadon City': [
{'name': 'Celadon City to Celadon Department Store 1F W', 'address': 'Warps_CeladonCity', 'id': 0,
@@ -461,15 +477,21 @@
{'address': 'Warps_PokemonMansion3F', 'id': 0, 'to': {'map': 'Pokemon Mansion 2F', 'id': 1}}],
'Pokemon Mansion B1F': [
{'address': 'Warps_PokemonMansionB1F', 'id': 0, 'to': {'map': 'Pokemon Mansion 1F-SE', 'id': 5}}],
- 'Rock Tunnel 1F-NE': [{'address': 'Warps_RockTunnel1F', 'id': 0, 'to': {'map': 'Route 10-N', 'id': 1}},
- {'address': 'Warps_RockTunnel1F', 'id': 4,
- 'to': {'map': 'Rock Tunnel B1F-E', 'id': 0}}], 'Rock Tunnel 1F-NW': [
- {'address': 'Warps_RockTunnel1F', 'id': 5, 'to': {'map': 'Rock Tunnel B1F-E', 'id': 1}},
- {'address': 'Warps_RockTunnel1F', 'id': 6, 'to': {'map': 'Rock Tunnel B1F-W', 'id': 2}}],
- 'Rock Tunnel 1F-S': [{'address': 'Warps_RockTunnel1F', 'id': 2, 'to': {'map': 'Route 10-S', 'id': 2}},
+ 'Rock Tunnel 1F-NE 1': [{'address': 'Warps_RockTunnel1F', 'id': 0, 'to': {'map': 'Route 10-N', 'id': 1}}],
+ 'Rock Tunnel 1F-NE 2':
+ [{'address': 'Warps_RockTunnel1F', 'id': 4,
+ 'to': {'map': 'Rock Tunnel B1F-E 1', 'id': 0}}], 'Rock Tunnel 1F-NW 1': [
+ {'address': 'Warps_RockTunnel1F', 'id': 5, 'to': {'map': 'Rock Tunnel B1F-E 2', 'id': 1}}],
+ 'Rock Tunnel 1F-NW 2': [
+ {'address': 'Warps_RockTunnel1F', 'id': 6, 'to': {'map': 'Rock Tunnel B1F-W 1', 'id': 2}}],
+ 'Rock Tunnel 1F-S 1': [{'address': 'Warps_RockTunnel1F', 'id': 2, 'to': {'map': 'Route 10-S', 'id': 2}}],
+ 'Rock Tunnel 1F-S 2': [
{'address': 'Warps_RockTunnel1F', 'id': 7,
- 'to': {'map': 'Rock Tunnel B1F-W', 'id': 3}}], 'Rock Tunnel 1F-Wild': [],
- 'Rock Tunnel B1F-Wild': [], 'Seafoam Islands 1F': [
+ 'to': {'map': 'Rock Tunnel B1F-W 2', 'id': 3}}], 'Rock Tunnel 1F-Wild': [],
+ 'Rock Tunnel B1F-Wild': [],
+ 'Rock Tunnel 1F-NE': [], 'Rock Tunnel 1F-NW': [], 'Rock Tunnel 1F-S': [], 'Rock Tunnel B1F-E': [],
+ 'Rock Tunnel B1F-W': [],
+ 'Seafoam Islands 1F': [
{'address': 'Warps_SeafoamIslands1F', 'id': (2, 3), 'to': {'map': 'Route 20-IE', 'id': 1}},
{'address': 'Warps_SeafoamIslands1F', 'id': 4, 'to': {'map': 'Seafoam Islands B1F', 'id': 1}},
{'address': 'Warps_SeafoamIslands1F', 'id': 5, 'to': {'map': 'Seafoam Islands B1F-NE', 'id': 6}}],
@@ -569,12 +591,14 @@
{'address': 'Warps_CeruleanCave2F', 'id': 3, 'to': {'map': 'Cerulean Cave 1F-N', 'id': 5}}],
'Cerulean Cave B1F': [
{'address': 'Warps_CeruleanCaveB1F', 'id': 0, 'to': {'map': 'Cerulean Cave 1F-NW', 'id': 8}}],
- 'Cerulean Cave B1F-E': [], 'Rock Tunnel B1F-E': [
- {'address': 'Warps_RockTunnelB1F', 'id': 0, 'to': {'map': 'Rock Tunnel 1F-NE', 'id': 4}},
- {'address': 'Warps_RockTunnelB1F', 'id': 1, 'to': {'map': 'Rock Tunnel 1F-NW', 'id': 5}}],
- 'Rock Tunnel B1F-W': [
- {'address': 'Warps_RockTunnelB1F', 'id': 2, 'to': {'map': 'Rock Tunnel 1F-NW', 'id': 6}},
- {'address': 'Warps_RockTunnelB1F', 'id': 3, 'to': {'map': 'Rock Tunnel 1F-S', 'id': 7}}],
+ 'Cerulean Cave B1F-E': [], 'Rock Tunnel B1F-E 1': [
+ {'address': 'Warps_RockTunnelB1F', 'id': 0, 'to': {'map': 'Rock Tunnel 1F-NE 2', 'id': 4}}],
+ 'Rock Tunnel B1F-E 2': [
+ {'address': 'Warps_RockTunnelB1F', 'id': 1, 'to': {'map': 'Rock Tunnel 1F-NW 1', 'id': 5}}],
+ 'Rock Tunnel B1F-W 1': [
+ {'address': 'Warps_RockTunnelB1F', 'id': 2, 'to': {'map': 'Rock Tunnel 1F-NW 2', 'id': 6}}],
+ 'Rock Tunnel B1F-W 2': [
+ {'address': 'Warps_RockTunnelB1F', 'id': 3, 'to': {'map': 'Rock Tunnel 1F-S 2', 'id': 7}}],
'Seafoam Islands B1F': [
{'address': 'Warps_SeafoamIslandsB1F', 'id': 0, 'to': {'map': 'Seafoam Islands B2F-NW', 'id': 0}},
{'address': 'Warps_SeafoamIslandsB1F', 'id': 1, 'to': {'map': 'Seafoam Islands 1F', 'id': 4}},
@@ -802,7 +826,7 @@
'Route 4-W': [{'address': 'Warps_Route4', 'id': 0, 'to': {'map': 'Route 4 Pokemon Center', 'id': 0}},
{'address': 'Warps_Route4', 'id': 1, 'to': {'map': 'Mt Moon 1F', 'id': 0}}],
'Route 4-C': [{'address': 'Warps_Route4', 'id': 2, 'to': {'map': 'Mt Moon B1F-NE', 'id': 7}}],
- 'Route 4-E': [], 'Route 4-Lass': [], 'Route 4-Grass': [],
+ 'Route 4-Lass': [], 'Route 4-E': [],
'Route 5': [{'address': 'Warps_Route5', 'id': (1, 0), 'to': {'map': 'Route 5 Gate-N', 'id': (3, 2)}},
{'address': 'Warps_Route5', 'id': 3, 'to': {'map': 'Underground Path Route 5', 'id': 0}},
{'address': 'Warps_Route5', 'id': 4, 'to': {'map': 'Daycare', 'id': 0}}], 'Route 9': [],
@@ -838,8 +862,8 @@
{'address': 'Warps_Route8', 'id': 4, 'to': {'map': 'Underground Path Route 8', 'id': 0}}],
'Route 8-Grass': [],
'Route 10-N': [{'address': 'Warps_Route10', 'id': 0, 'to': {'map': 'Rock Tunnel Pokemon Center', 'id': 0}},
- {'address': 'Warps_Route10', 'id': 1, 'to': {'map': 'Rock Tunnel 1F-NE', 'id': 0}}],
- 'Route 10-S': [{'address': 'Warps_Route10', 'id': 2, 'to': {'map': 'Rock Tunnel 1F-S', 'id': 2}}],
+ {'address': 'Warps_Route10', 'id': 1, 'to': {'map': 'Rock Tunnel 1F-NE 1', 'id': 0}}],
+ 'Route 10-S': [{'address': 'Warps_Route10', 'id': 2, 'to': {'map': 'Rock Tunnel 1F-S 1', 'id': 2}}],
'Route 10-P': [{'address': 'Warps_Route10', 'id': 3, 'to': {'map': 'Power Plant', 'id': 0}}],
'Route 10-C': [],
'Route 11': [{'address': 'Warps_Route11', 'id': 4, 'to': {'map': "Diglett's Cave Route 11", 'id': 0}}],
@@ -1293,7 +1317,7 @@ def pair(a, b):
return (f"{a} to {b}", f"{b} to {a}")
-mandatory_connections = {
+safari_zone_connections = {
pair("Safari Zone Center-S", "Safari Zone Gate-N"),
pair("Safari Zone East", "Safari Zone North"),
pair("Safari Zone East", "Safari Zone Center-S"),
@@ -1302,14 +1326,8 @@ def pair(a, b):
pair("Safari Zone North", "Safari Zone West-NW"),
pair("Safari Zone West", "Safari Zone Center-NW"),
}
-insanity_mandatory_connections = {
- # pair("Seafoam Islands B1F-NE", "Seafoam Islands 1F"),
- # pair("Seafoam Islands 1F", "Seafoam Islands B1F"),
- # pair("Seafoam Islands B2F-NW", "Seafoam Islands B1F"),
- # pair("Seafoam Islands B3F-SE", "Seafoam Islands B2F-SE"),
- # pair("Seafoam Islands B3F-NE", "Seafoam Islands B2F-NE"),
- # pair("Seafoam Islands B4F", "Seafoam Islands B3F-NE"),
- # pair("Seafoam Islands B4F", "Seafoam Islands B3F"),
+
+full_mandatory_connections = {
pair("Player's House 1F", "Player's House 2F"),
pair("Indigo Plateau Lorelei's Room", "Indigo Plateau Lobby-N"),
pair("Indigo Plateau Bruno's Room", "Indigo Plateau Lorelei's Room"),
@@ -1338,7 +1356,7 @@ def pair(a, b):
unsafe_connecting_interior_dungeons = [
["Seafoam Islands 1F to Route 20-IE", "Seafoam Islands 1F-SE to Route 20-IW"],
- ["Rock Tunnel 1F-NE to Route 10-N", "Rock Tunnel 1F-S to Route 10-S"],
+ ["Rock Tunnel 1F-NE 1 to Route 10-N", "Rock Tunnel 1F-S 1 to Route 10-S"],
["Victory Road 1F-S to Route 23-C", "Victory Road 2F-E to Route 23-N"],
]
@@ -1357,7 +1375,7 @@ def pair(a, b):
["Route 2-NE to Diglett's Cave Route 2", "Route 11 to Diglett's Cave Route 11"],
['Route 20-IE to Seafoam Islands 1F', 'Route 20-IW to Seafoam Islands 1F-SE'],
['Route 4-W to Mt Moon 1F', 'Route 4-C to Mt Moon B1F-NE'],
- ['Route 10-N to Rock Tunnel 1F-NE', 'Route 10-S to Rock Tunnel 1F-S'],
+ ['Route 10-N to Rock Tunnel 1F-NE 1', 'Route 10-S to Rock Tunnel 1F-S 1'],
['Route 23-C to Victory Road 1F-S', 'Route 23-N to Victory Road 2F-E'],
]
@@ -1454,9 +1472,10 @@ def pair(a, b):
]
unreachable_outdoor_entrances = [
- "Route 4-C to Mt Moon B1F-NE",
"Fuchsia City-Good Rod House Backyard to Fuchsia Good Rod House",
- "Cerulean City-Badge House Backyard to Cerulean Badge House"
+ "Cerulean City-Badge House Backyard to Cerulean Badge House",
+ # TODO: This doesn't need to be forced if fly location is Pokemon League?
+ "Route 23-N to Victory Road 2F-E"
]
@@ -1494,7 +1513,6 @@ def create_regions(self):
start_inventory["Exp. All"] = 1
self.multiworld.push_precollected(self.create_item("Exp. All"))
- # locations = [location for location in location_data if location.type in ("Item", "Trainer Parties")]
self.item_pool = []
combined_traps = (self.multiworld.poison_trap_weight[self.player].value
+ self.multiworld.fire_trap_weight[self.player].value
@@ -1522,7 +1540,6 @@ def create_regions(self):
item = self.create_filler()
elif location.original_item == "Pokedex":
if self.multiworld.randomize_pokedex[self.player] == "vanilla":
- location_object.event = True
event = True
item = self.create_item("Pokedex")
elif location.original_item == "Moon Stone" and self.multiworld.stonesanity[self.player]:
@@ -1543,7 +1560,7 @@ def create_regions(self):
<= self.multiworld.trap_percentage[self.player].value and combined_traps != 0):
item = self.create_item(self.select_trap())
- if self.multiworld.key_items_only[self.player] and (not location.event) and (not item.advancement):
+ if self.multiworld.key_items_only[self.player] and (not location.event) and (not item.advancement) and location.original_item != "Exp. All":
continue
if item.name in start_inventory and start_inventory[item.name] > 0 and \
@@ -1554,7 +1571,6 @@ def create_regions(self):
if event:
location_object.place_locked_item(item)
if location.type == "Trainer Parties":
- # loc.item.classification = ItemClassification.filler
location_object.party_data = deepcopy(location.party_data)
else:
self.item_pool.append(item)
@@ -1564,7 +1580,7 @@ def create_regions(self):
+ [item.name for item in self.multiworld.precollected_items[self.player] if
item.advancement]
self.total_key_items = len(
- # The stonesanity items are not checekd for here and instead just always added as the `+ 4`
+ # The stonesanity items are not checked for here and instead just always added as the `+ 4`
# They will always exist, but if stonesanity is off, then only as events.
# We don't want to just add 4 if stonesanity is off while still putting them in this list in case
# the player puts stones in their start inventory, in which case they would be double-counted here.
@@ -1592,7 +1608,7 @@ def create_regions(self):
connect(multiworld, player, "Menu", "Pallet Town", one_way=True)
connect(multiworld, player, "Menu", "Pokedex", one_way=True)
connect(multiworld, player, "Menu", "Evolution", one_way=True)
- connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state,
+ connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state,
state.multiworld.second_fossil_check_condition[player].value, player), one_way=True)
connect(multiworld, player, "Pallet Town", "Route 1")
connect(multiworld, player, "Route 1", "Viridian City")
@@ -1617,19 +1633,18 @@ def create_regions(self):
connect(multiworld, player, "Pewter City-E", "Route 3", lambda state: logic.route_3(state, player), one_way=True)
connect(multiworld, player, "Route 3", "Pewter City-E", one_way=True)
connect(multiworld, player, "Route 4-W", "Route 3")
- connect(multiworld, player, "Route 24", "Cerulean City-Water", one_way=True)
+ connect(multiworld, player, "Route 24", "Cerulean City-Water", lambda state: logic.can_surf(state, player))
connect(multiworld, player, "Cerulean City-Water", "Route 4-Lass", lambda state: logic.can_surf(state, player), one_way=True)
connect(multiworld, player, "Mt Moon B2F", "Mt Moon B2F-Wild", one_way=True)
connect(multiworld, player, "Mt Moon B2F-NE", "Mt Moon B2F-Wild", one_way=True)
connect(multiworld, player, "Mt Moon B2F-C", "Mt Moon B2F-Wild", one_way=True)
- connect(multiworld, player, "Route 4-Lass", "Route 4-E", one_way=True)
+ connect(multiworld, player, "Route 4-Lass", "Route 4-C", one_way=True)
connect(multiworld, player, "Route 4-C", "Route 4-E", one_way=True)
- connect(multiworld, player, "Route 4-E", "Route 4-Grass", one_way=True)
- connect(multiworld, player, "Route 4-Grass", "Cerulean City", one_way=True)
- connect(multiworld, player, "Cerulean City", "Route 24", one_way=True)
+ connect(multiworld, player, "Route 4-E", "Cerulean City")
+ connect(multiworld, player, "Cerulean City", "Route 24")
connect(multiworld, player, "Cerulean City", "Cerulean City-T", lambda state: state.has("Help Bill", player))
connect(multiworld, player, "Cerulean City-Outskirts", "Cerulean City", one_way=True)
- connect(multiworld, player, "Cerulean City-Outskirts", "Cerulean City", lambda state: logic.can_cut(state, player))
+ connect(multiworld, player, "Cerulean City", "Cerulean City-Outskirts", lambda state: logic.can_cut(state, player), one_way=True)
connect(multiworld, player, "Cerulean City-Outskirts", "Route 9", lambda state: logic.can_cut(state, player))
connect(multiworld, player, "Cerulean City-Outskirts", "Route 5")
connect(multiworld, player, "Cerulean Cave B1F", "Cerulean Cave B1F-E", lambda state: logic.can_surf(state, player), one_way=True)
@@ -1705,7 +1720,6 @@ def create_regions(self):
connect(multiworld, player, "Route 12-S", "Route 12-Grass", lambda state: logic.can_cut(state, player), one_way=True)
connect(multiworld, player, "Route 12-L", "Lavender Town")
connect(multiworld, player, "Route 10-S", "Lavender Town")
- connect(multiworld, player, "Route 8-W", "Saffron City")
connect(multiworld, player, "Route 8", "Lavender Town")
connect(multiworld, player, "Pokemon Tower 6F", "Pokemon Tower 6F-S", lambda state: state.has("Silph Scope", player) or (state.has("Buy Poke Doll", player) and state.multiworld.poke_doll_skip[player]))
connect(multiworld, player, "Route 8", "Route 8-Grass", lambda state: logic.can_cut(state, player), one_way=True)
@@ -1784,7 +1798,6 @@ def create_regions(self):
connect(multiworld, player, "Seafoam Islands B3F-SE", "Seafoam Islands B3F-Wild", one_way=True)
connect(multiworld, player, "Seafoam Islands B4F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, player), one_way=True)
connect(multiworld, player, "Seafoam Islands B4F-W", "Seafoam Islands B4F", one_way=True)
- # This really shouldn't be necessary since if the boulders are reachable you can drop, but might as well be thorough
connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B3F-SE", lambda state: logic.can_surf(state, player) and logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6))
connect(multiworld, player, "Viridian City", "Viridian City-N", lambda state: state.has("Oak's Parcel", player) or state.multiworld.old_man[player].value == 2 or logic.can_cut(state, player))
connect(multiworld, player, "Route 11", "Route 11-C", lambda state: logic.can_strength(state, player) or not state.multiworld.extra_strength_boulders[player])
@@ -1803,6 +1816,16 @@ def create_regions(self):
connect(multiworld, player, "Pokemon Mansion 2F-E", "Pokemon Mansion 2F-Wild", one_way=True)
connect(multiworld, player, "Pokemon Mansion 1F-SE", "Pokemon Mansion 1F-Wild", one_way=True)
connect(multiworld, player, "Pokemon Mansion 1F", "Pokemon Mansion 1F-Wild", one_way=True)
+ connect(multiworld, player, "Rock Tunnel 1F-S 1", "Rock Tunnel 1F-S", lambda state: logic.rock_tunnel(state, player))
+ connect(multiworld, player, "Rock Tunnel 1F-S 2", "Rock Tunnel 1F-S", lambda state: logic.rock_tunnel(state, player))
+ connect(multiworld, player, "Rock Tunnel 1F-NW 1", "Rock Tunnel 1F-NW", lambda state: logic.rock_tunnel(state, player))
+ connect(multiworld, player, "Rock Tunnel 1F-NW 2", "Rock Tunnel 1F-NW", lambda state: logic.rock_tunnel(state, player))
+ connect(multiworld, player, "Rock Tunnel 1F-NE 1", "Rock Tunnel 1F-NE", lambda state: logic.rock_tunnel(state, player))
+ connect(multiworld, player, "Rock Tunnel 1F-NE 2", "Rock Tunnel 1F-NE", lambda state: logic.rock_tunnel(state, player))
+ connect(multiworld, player, "Rock Tunnel B1F-W 1", "Rock Tunnel B1F-W", lambda state: logic.rock_tunnel(state, player))
+ connect(multiworld, player, "Rock Tunnel B1F-W 2", "Rock Tunnel B1F-W", lambda state: logic.rock_tunnel(state, player))
+ connect(multiworld, player, "Rock Tunnel B1F-E 1", "Rock Tunnel B1F-E", lambda state: logic.rock_tunnel(state, player))
+ connect(multiworld, player, "Rock Tunnel B1F-E 2", "Rock Tunnel B1F-E", lambda state: logic.rock_tunnel(state, player))
connect(multiworld, player, "Rock Tunnel 1F-S", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True)
connect(multiworld, player, "Rock Tunnel 1F-NW", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True)
connect(multiworld, player, "Rock Tunnel 1F-NE", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True)
@@ -1829,7 +1852,8 @@ def create_regions(self):
connect(multiworld, player, "Silph Co 6F", "Silph Co 6F-SW", lambda state: logic.card_key(state, 6, player))
connect(multiworld, player, "Silph Co 7F", "Silph Co 7F-E", lambda state: logic.card_key(state, 7, player))
connect(multiworld, player, "Silph Co 7F-SE", "Silph Co 7F-E", lambda state: logic.card_key(state, 7, player))
- connect(multiworld, player, "Silph Co 8F", "Silph Co 8F-W", lambda state: logic.card_key(state, 8, player))
+ connect(multiworld, player, "Silph Co 8F", "Silph Co 8F-W", lambda state: logic.card_key(state, 8, player), one_way=True, name="Silph Co 8F to Silph Co 8F-W (Card Key)")
+ connect(multiworld, player, "Silph Co 8F-W", "Silph Co 8F", lambda state: logic.card_key(state, 8, player), one_way=True, name="Silph Co 8F-W to Silph Co 8F (Card Key)")
connect(multiworld, player, "Silph Co 9F", "Silph Co 9F-SW", lambda state: logic.card_key(state, 9, player))
connect(multiworld, player, "Silph Co 9F-NW", "Silph Co 9F-SW", lambda state: logic.card_key(state, 9, player))
connect(multiworld, player, "Silph Co 10F", "Silph Co 10F-SE", lambda state: logic.card_key(state, 10, player))
@@ -1858,26 +1882,25 @@ def create_regions(self):
logic.has_badges(state, self.multiworld.cerulean_cave_badges_condition[player].value, player) and
logic.has_key_items(state, self.multiworld.cerulean_cave_key_items_condition[player].total, player) and logic.can_surf(state, player))
-
# access to any part of a city will enable flying to the Pokemon Center
connect(multiworld, player, "Cerulean City-Cave", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True)
connect(multiworld, player, "Cerulean City-Badge House Backyard", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True)
+ connect(multiworld, player, "Cerulean City-T", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True, name="Cerulean City-T to Cerulean City (Fly)")
connect(multiworld, player, "Fuchsia City-Good Rod House Backyard", "Fuchsia City", lambda state: logic.can_fly(state, player), one_way=True)
- connect(multiworld, player, "Saffron City-G", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True)
- connect(multiworld, player, "Saffron City-Pidgey", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True)
- connect(multiworld, player, "Saffron City-Silph", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True)
- connect(multiworld, player, "Saffron City-Copycat", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True)
- connect(multiworld, player, "Celadon City-G", "Celadon City", lambda state: logic.can_fly(state, player), one_way=True)
- connect(multiworld, player, "Vermilion City-G", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True)
- connect(multiworld, player, "Vermilion City-Dock", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True)
- connect(multiworld, player, "Cinnabar Island-G", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True)
- connect(multiworld, player, "Cinnabar Island-M", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True)
-
+ connect(multiworld, player, "Saffron City-G", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-G to Saffron City (Fly)")
+ connect(multiworld, player, "Saffron City-Pidgey", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Pidgey to Saffron City (Fly)")
+ connect(multiworld, player, "Saffron City-Silph", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Silph to Saffron City (Fly)")
+ connect(multiworld, player, "Saffron City-Copycat", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Copycat to Saffron City (Fly)")
+ connect(multiworld, player, "Celadon City-G", "Celadon City", lambda state: logic.can_fly(state, player), one_way=True, name="Celadon City-G to Celadon City (Fly)")
+ connect(multiworld, player, "Vermilion City-G", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True, name="Vermilion City-G to Vermilion City (Fly)")
+ connect(multiworld, player, "Vermilion City-Dock", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True, name="Vermilion City-Dock to Vermilion City (Fly)")
+ connect(multiworld, player, "Cinnabar Island-G", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True, name="Cinnabar Island-G to Cinnabar Island (Fly)")
+ connect(multiworld, player, "Cinnabar Island-M", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True, name="Cinnabar Island-M to Cinnabar Island (Fly)")
# drops
- connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True)
- connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F-NE", one_way=True)
- connect(multiworld, player, "Seafoam Islands B1F", "Seafoam Islands B2F-NW", one_way=True)
+ connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B1F (Drop)")
+ connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F-NE", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B1F-NE (Drop)")
+ connect(multiworld, player, "Seafoam Islands B1F", "Seafoam Islands B2F-NW", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B2F-NW (Drop)")
connect(multiworld, player, "Seafoam Islands B1F-NE", "Seafoam Islands B2F-NE", one_way=True)
connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True)
connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True)
@@ -1886,7 +1909,7 @@ def create_regions(self):
# If you haven't dropped the boulders, you'll go straight to B4F
connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B4F-W", one_way=True)
connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B4F-W", one_way=True)
- connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True)
+ connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True, name="Seafoam Islands B1F to Seafoam Islands B4F (Drop)")
connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, player), one_way=True)
connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 2F", one_way=True)
connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 1F-SE", one_way=True)
@@ -1901,14 +1924,50 @@ def create_regions(self):
lambda state: logic.can_fly(state, player) and state.has("Town Map", player), one_way=True,
name="Town Map Fly Location")
+ cache = multiworld.regions.entrance_cache[self.player].copy()
+ if multiworld.badgesanity[player] or multiworld.door_shuffle[player] in ("off", "simple"):
+ badges = None
+ badge_locs = None
+ else:
+ badges = [item for item in self.item_pool if "Badge" in item.name]
+ for badge in badges:
+ self.item_pool.remove(badge)
+ badge_locs = [multiworld.get_location(loc, player) for loc in [
+ "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize", "Vermilion Gym - Lt. Surge Prize",
+ "Celadon Gym - Erika Prize", "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize",
+ "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"
+ ]]
+ for attempt in range(10):
+ try:
+ door_shuffle(self, multiworld, player, badges, badge_locs)
+ except DoorShuffleException as e:
+ if attempt == 9:
+ raise e
+ for region in self.multiworld.get_regions(player):
+ for entrance in reversed(region.exits):
+ if isinstance(entrance, PokemonRBWarp):
+ region.exits.remove(entrance)
+ multiworld.regions.entrance_cache[self.player] = cache.copy()
+ if badge_locs:
+ for loc in badge_locs:
+ loc.item = None
+ loc.locked = False
+ else:
+ break
+
+
+def door_shuffle(world, multiworld, player, badges, badge_locs):
entrances = []
+ full_interiors = []
for region_name, region_entrances in warp_data.items():
+ region = multiworld.get_region(region_name, player)
for entrance_data in region_entrances:
- region = multiworld.get_region(region_name, player)
shuffle = True
- if not outdoor_map(region.name) and not outdoor_map(entrance_data['to']['map']) and \
- multiworld.door_shuffle[player] not in ("insanity", "decoupled"):
- shuffle = False
+ interior = False
+ if not outdoor_map(region.name) and not outdoor_map(entrance_data['to']['map']):
+ if multiworld.door_shuffle[player] not in ("full", "insanity", "decoupled"):
+ shuffle = False
+ interior = True
if multiworld.door_shuffle[player] == "simple":
if sorted([entrance_data['to']['map'], region.name]) == ["Celadon Game Corner-Hidden Stairs",
"Rocket Hideout B1F"]:
@@ -1918,11 +1977,14 @@ def create_regions(self):
if (multiworld.randomize_rock_tunnel[player] and "Rock Tunnel" in region.name and "Rock Tunnel" in
entrance_data['to']['map']):
shuffle = False
- if (f"{region.name} to {entrance_data['to']['map']}" if "name" not in entrance_data else
+ elif (f"{region.name} to {entrance_data['to']['map']}" if "name" not in entrance_data else
entrance_data["name"]) in silph_co_warps + saffron_gym_warps:
- if multiworld.warp_tile_shuffle[player] or multiworld.door_shuffle[player] in ("insanity",
- "decoupled"):
+ if multiworld.warp_tile_shuffle[player]:
shuffle = True
+ if multiworld.warp_tile_shuffle[player] == "mixed" and multiworld.door_shuffle[player] == "full":
+ interior = True
+ else:
+ interior = False
else:
shuffle = False
elif not multiworld.door_shuffle[player]:
@@ -1932,32 +1994,49 @@ def create_regions(self):
entrance_data else entrance_data["name"], region, entrance_data["id"],
entrance_data["address"], entrance_data["flags"] if "flags" in
entrance_data else "")
- # if "Rock Tunnel" in region_name:
- # entrance.access_rule = lambda state: logic.rock_tunnel(state, player)
- entrances.append(entrance)
+ if interior and multiworld.door_shuffle[player] == "full":
+ full_interiors.append(entrance)
+ else:
+ entrances.append(entrance)
region.exits.append(entrance)
else:
- # connect(multiworld, player, region.name, entrance_data['to']['map'], one_way=True)
- if "Rock Tunnel" in region.name:
- connect(multiworld, player, region.name, entrance_data["to"]["map"],
- lambda state: logic.rock_tunnel(state, player), one_way=True)
- else:
- connect(multiworld, player, region.name, entrance_data["to"]["map"], one_way=True)
+ connect(multiworld, player, region.name, entrance_data["to"]["map"], one_way=True,
+ name=entrance_data["name"] if "name" in entrance_data else None)
forced_connections = set()
+ one_way_forced_connections = set()
if multiworld.door_shuffle[player]:
- forced_connections.update(mandatory_connections.copy())
+ if multiworld.door_shuffle[player] in ("full", "insanity", "decoupled"):
+ safari_zone_doors = [door for pair in safari_zone_connections for door in pair]
+ safari_zone_doors.sort()
+ order = ["Center", "East", "North", "West"]
+ multiworld.random.shuffle(order)
+ usable_doors = ["Safari Zone Gate-N to Safari Zone Center-S"]
+ for section in order:
+ section_doors = [door for door in safari_zone_doors if door.startswith(f"Safari Zone {section}")]
+ connect_door_a = multiworld.random.choice(usable_doors)
+ connect_door_b = multiworld.random.choice(section_doors)
+ usable_doors.remove(connect_door_a)
+ section_doors.remove(connect_door_b)
+ forced_connections.add((connect_door_a, connect_door_b))
+ usable_doors += section_doors
+ multiworld.random.shuffle(usable_doors)
+ while usable_doors:
+ forced_connections.add((usable_doors.pop(), usable_doors.pop()))
+ else:
+ forced_connections.update(safari_zone_connections)
+
usable_safe_rooms = safe_rooms.copy()
if multiworld.door_shuffle[player] == "simple":
forced_connections.update(simple_mandatory_connections)
else:
usable_safe_rooms += pokemarts
- if self.multiworld.key_items_only[self.player]:
+ if multiworld.key_items_only[player]:
usable_safe_rooms.remove("Viridian Pokemart to Viridian City")
- if multiworld.door_shuffle[player] in ("insanity", "decoupled"):
- forced_connections.update(insanity_mandatory_connections)
+ if multiworld.door_shuffle[player] in ("full", "insanity", "decoupled"):
+ forced_connections.update(full_mandatory_connections)
r = multiworld.random.randint(0, 3)
if r == 2:
forced_connections.add(("Pokemon Mansion 1F-SE to Pokemon Mansion B1F",
@@ -1965,6 +2044,9 @@ def create_regions(self):
forced_connections.add(("Pokemon Mansion 2F to Pokemon Mansion 3F",
multiworld.random.choice(mansion_stair_destinations + mansion_dead_ends
+ ["Pokemon Mansion B1F to Pokemon Mansion 1F-SE"])))
+ if multiworld.door_shuffle[player] == "full":
+ forced_connections.add(("Pokemon Mansion 1F to Pokemon Mansion 2F",
+ "Pokemon Mansion 3F to Pokemon Mansion 2F"))
elif r == 3:
dead_end = multiworld.random.randint(0, 1)
forced_connections.add(("Pokemon Mansion 3F-SE to Pokemon Mansion 2F-E",
@@ -1983,7 +2065,8 @@ def create_regions(self):
multiworld.random.choice(mansion_stair_destinations
+ ["Pokemon Mansion B1F to Pokemon Mansion 1F-SE"])))
- usable_safe_rooms += insanity_safe_rooms
+ if multiworld.door_shuffle[player] in ("insanity", "decoupled"):
+ usable_safe_rooms += insanity_safe_rooms
safe_rooms_sample = multiworld.random.sample(usable_safe_rooms, 6)
pallet_safe_room = safe_rooms_sample[-1]
@@ -1991,16 +2074,28 @@ def create_regions(self):
for a, b in zip(multiworld.random.sample(["Pallet Town to Player's House 1F", "Pallet Town to Oak's Lab",
"Pallet Town to Rival's House"], 3), ["Oak's Lab to Pallet Town",
"Player's House 1F to Pallet Town", pallet_safe_room]):
- forced_connections.add((a, b))
+ one_way_forced_connections.add((a, b))
+
+ if multiworld.door_shuffle[player] == "decoupled":
+ for a, b in zip(["Oak's Lab to Pallet Town", "Player's House 1F to Pallet Town", pallet_safe_room],
+ multiworld.random.sample(["Pallet Town to Player's House 1F", "Pallet Town to Oak's Lab",
+ "Pallet Town to Rival's House"], 3)):
+ one_way_forced_connections.add((a, b))
+
for a, b in zip(safari_zone_houses, safe_rooms_sample):
- forced_connections.add((a, b))
+ one_way_forced_connections.add((a, b))
+ if multiworld.door_shuffle[player] == "decoupled":
+ for a, b in zip(multiworld.random.sample(safe_rooms_sample[:-1], len(safe_rooms_sample) - 1),
+ safari_zone_houses):
+ one_way_forced_connections.add((a, b))
+
if multiworld.door_shuffle[player] == "simple":
# force Indigo Plateau Lobby to vanilla location on simple, otherwise shuffle with Pokemon Centers.
for a, b in zip(multiworld.random.sample(pokemon_center_entrances[0:-1], 11), pokemon_centers[0:-1]):
forced_connections.add((a, b))
forced_connections.add((pokemon_center_entrances[-1], pokemon_centers[-1]))
forced_pokemarts = multiworld.random.sample(pokemart_entrances, 8)
- if self.multiworld.key_items_only[self.player]:
+ if multiworld.key_items_only[player]:
forced_pokemarts.sort(key=lambda i: i[0] != "Viridian Pokemart to Viridian City")
for a, b in zip(forced_pokemarts, pokemarts):
forced_connections.add((a, b))
@@ -2010,15 +2105,19 @@ def create_regions(self):
# warping outside an entrance that isn't the Pokemon Center, just always put Pokemon Centers at Pokemon
# Center entrances
for a, b in zip(multiworld.random.sample(pokemon_center_entrances, 12), pokemon_centers):
- forced_connections.add((a, b))
+ one_way_forced_connections.add((a, b))
# Ensure a Pokemart is available at the beginning of the game
if multiworld.key_items_only[player]:
- forced_connections.add((multiworld.random.choice(initial_doors), "Viridian Pokemart to Viridian City"))
+ one_way_forced_connections.add((multiworld.random.choice(initial_doors),
+ "Viridian Pokemart to Viridian City"))
+
elif "Pokemart" not in pallet_safe_room:
- forced_connections.add((multiworld.random.choice(initial_doors), multiworld.random.choice(
- [mart for mart in pokemarts if mart not in safe_rooms_sample])))
+ one_way_forced_connections.add((multiworld.random.choice(initial_doors), multiworld.random.choice(
+ [mart for mart in pokemarts if mart not in safe_rooms_sample])))
- if multiworld.warp_tile_shuffle[player]:
+ if multiworld.warp_tile_shuffle[player] == "shuffle" or (multiworld.warp_tile_shuffle[player] == "mixed"
+ and multiworld.door_shuffle[player]
+ in ("off", "simple", "interiors")):
warps = multiworld.random.sample(silph_co_warps, len(silph_co_warps))
# The only warp tiles never reachable from the stairs/elevators are the two 7F-NW warps (where the rival is)
# and the final 11F-W warp. As long as the two 7F-NW warps aren't connected to each other, everything should
@@ -2051,13 +2150,38 @@ def create_regions(self):
while warps:
forced_connections.add((warps.pop(), warps.pop(),))
+ dc_destinations = None
+ if multiworld.door_shuffle[player] == "decoupled":
+ dc_destinations = entrances.copy()
+ for pair in one_way_forced_connections:
+ entrance_a = multiworld.get_entrance(pair[0], player)
+ entrance_b = multiworld.get_entrance(pair[1], player)
+ entrance_a.connect(entrance_b)
+ entrances.remove(entrance_a)
+ dc_destinations.remove(entrance_b)
+ else:
+ forced_connections.update(one_way_forced_connections)
+
for pair in forced_connections:
entrance_a = multiworld.get_entrance(pair[0], player)
entrance_b = multiworld.get_entrance(pair[1], player)
entrance_a.connect(entrance_b)
entrance_b.connect(entrance_a)
- entrances.remove(entrance_a)
- entrances.remove(entrance_b)
+ if entrance_a in entrances:
+ entrances.remove(entrance_a)
+ elif entrance_a in full_interiors:
+ full_interiors.remove(entrance_a)
+ else:
+ raise DoorShuffleException("Attempted to force connection with entrance not in any entrance pool, likely because it tried to force an entrance to connect twice.")
+ if entrance_b in entrances:
+ entrances.remove(entrance_b)
+ elif entrance_b in full_interiors:
+ full_interiors.remove(entrance_b)
+ else:
+ raise DoorShuffleException("Attempted to force connection with entrance not in any entrance pool, likely because it tried to force an entrance to connect twice.")
+ if multiworld.door_shuffle[player] == "decoupled":
+ dc_destinations.remove(entrance_a)
+ dc_destinations.remove(entrance_b)
if multiworld.door_shuffle[player] == "simple":
def connect_connecting_interiors(interior_exits, exterior_entrances):
@@ -2065,7 +2189,7 @@ def connect_connecting_interiors(interior_exits, exterior_entrances):
for a, b in zip(interior, exterior):
entrance_a = multiworld.get_entrance(a, player)
if b is None:
- #entrance_b = multiworld.get_entrance(entrances[0], player)
+ # entrance_b = multiworld.get_entrance(entrances[0], player)
# should just be able to use the entrance_b from the previous link?
pass
else:
@@ -2098,7 +2222,7 @@ def connect_interiors(interior_exits, exterior_entrances):
single_entrance_dungeon_entrances = dungeon_entrances.copy()
for i in range(2):
- if True or not multiworld.random.randint(0, 2):
+ if not multiworld.random.randint(0, 2):
placed_connecting_interior_dungeons.append(multi_purpose_dungeons[i])
interior_dungeon_entrances.append([multi_purpose_dungeon_entrances[i], None])
else:
@@ -2181,7 +2305,7 @@ def cerulean_city_problem():
and interiors[0] in connecting_interiors[13:17] # Saffron Gate at Underground Path North South
and interiors[13] in connecting_interiors[13:17] # Saffron Gate at Route 5 Saffron Gate
and multi_purpose_dungeons[0] == placed_connecting_interior_dungeons[4] # Pokémon Mansion at Rock Tunnel, which is
- and (not multiworld.tea[player]) # not traversable backwards
+ and (not multiworld.tea[player]) # not traversable backwards
and multiworld.route_3_condition[player] == "defeat_brock"
and multiworld.worlds[player].fly_map != "Cerulean City"
and multiworld.worlds[player].town_map_fly_map != "Cerulean City"):
@@ -2205,22 +2329,66 @@ def cerulean_city_problem():
entrance_b.connect(entrance_a)
elif multiworld.door_shuffle[player]:
if multiworld.door_shuffle[player] == "full":
+ multiworld.random.shuffle(full_interiors)
+
+ def search_for_exit(entrance, region, checked_regions):
+ checked_regions.add(region)
+ for exit_candidate in region.exits:
+ if ((not exit_candidate.connected_region)
+ and exit_candidate in entrances and exit_candidate is not entrance):
+ return exit_candidate
+ for entrance_candidate in region.entrances:
+ if entrance_candidate.parent_region not in checked_regions:
+ found_exit = search_for_exit(entrance, entrance_candidate.parent_region, checked_regions)
+ if found_exit is not None:
+ return found_exit
+ return None
+
+ while True:
+ for entrance_a in full_interiors:
+ if search_for_exit(entrance_a, entrance_a.parent_region, set()) is None:
+ for entrance_b in full_interiors:
+ if search_for_exit(entrance_b, entrance_b.parent_region, set()):
+ entrance_a.connect(entrance_b)
+ entrance_b.connect(entrance_a)
+ # Yes, it removes from full_interiors while iterating through it, but it immediately
+ # breaks out, from both loops.
+ full_interiors.remove(entrance_a)
+ full_interiors.remove(entrance_b)
+ break
+ else:
+ raise DoorShuffleException("No non-dead end interior sections found in Pokemon Red and Blue door shuffle.")
+ break
+ else:
+ break
+
+ loop_out_interiors = []
+ multiworld.random.shuffle(entrances)
+ for entrance in reversed(entrances):
+ if not outdoor_map(entrance.parent_region.name):
+ found_exit = search_for_exit(entrance, entrance.parent_region, set())
+ if found_exit is None:
+ continue
+ loop_out_interiors.append([found_exit, entrance])
+ entrances.remove(entrance)
+
+ if len(loop_out_interiors) == 2:
+ break
+
+ for entrance_a, entrance_b in zip(full_interiors[:len(full_interiors) // 2],
+ full_interiors[len(full_interiors) // 2:]):
+ entrance_a.connect(entrance_b)
+ entrance_b.connect(entrance_a)
+
+ elif multiworld.door_shuffle[player] == "interiors":
loop_out_interiors = [[multiworld.get_entrance(e[0], player), multiworld.get_entrance(e[1], player)] for e
in multiworld.random.sample(unsafe_connecting_interior_dungeons
+ safe_connecting_interior_dungeons, 2)]
entrances.remove(loop_out_interiors[0][1])
entrances.remove(loop_out_interiors[1][1])
if not multiworld.badgesanity[player]:
- badges = [item for item in self.item_pool if "Badge" in item.name]
- for badge in badges:
- self.item_pool.remove(badge)
- badge_locs = []
- for loc in ["Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize", "Vermilion Gym - Lt. Surge Prize",
- "Celadon Gym - Erika Prize", "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize",
- "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"]:
- badge_locs.append(multiworld.get_location(loc, player))
multiworld.random.shuffle(badges)
- while badges[3].name == "Cascade Badge" and multiworld.badges_needed_for_hm_moves[player] == "on":
+ while badges[3].name == "Cascade Badge" and multiworld.badges_needed_for_hm_moves[player]:
multiworld.random.shuffle(badges)
for badge, loc in zip(badges, badge_locs):
loc.place_locked_item(badge)
@@ -2229,7 +2397,7 @@ def cerulean_city_problem():
for item, data in item_table.items():
if (data.id or item in poke_data.pokemon_data) and data.classification == ItemClassification.progression \
and ("Badge" not in item or multiworld.badgesanity[player]):
- state.collect(self.create_item(item))
+ state.collect(world.create_item(item))
multiworld.random.shuffle(entrances)
reachable_entrances = []
@@ -2265,103 +2433,129 @@ def cerulean_city_problem():
"Defeat Viridian Gym Giovanni",
]
- def adds_reachable_entrances(entrances_copy, item):
- state.collect(item, False)
- ret = len([entrance for entrance in entrances_copy if entrance in reachable_entrances or
- entrance.parent_region.can_reach(state)]) > len(reachable_entrances)
- state.remove(item)
- return ret
+ event_locations = multiworld.get_filled_locations(player)
+
+ def adds_reachable_entrances(item):
- def dead_end(entrances_copy, e):
+ state_copy = state.copy()
+ state_copy.collect(item, True)
+ state.sweep_for_events(locations=event_locations)
+ new_reachable_entrances = len([entrance for entrance in entrances if entrance in reachable_entrances or
+ entrance.parent_region.can_reach(state_copy)])
+ return new_reachable_entrances > len(reachable_entrances)
+
+ def dead_end(e):
+ if e.can_reach(state):
+ return True
+ elif multiworld.door_shuffle[player] == "decoupled":
+ # Any unreachable exit in decoupled is not a dead end
+ return False
region = e.parent_region
check_warps = set()
checked_regions = {region}
check_warps.update(region.exits)
check_warps.remove(e)
for location in region.locations:
- if location.item and location.item.name in relevant_events and adds_reachable_entrances(entrances_copy,
- location.item):
+ if location.item and location.item.name in relevant_events and \
+ adds_reachable_entrances(location.item):
return False
while check_warps:
warp = check_warps.pop()
warp = warp
if warp not in reachable_entrances:
- if "Rock Tunnel" not in warp.name or logic.rock_tunnel(state, player):
- # confirm warp is in entrances list to ensure it's not a loop-out interior
- if warp.connected_region is None and warp in entrances_copy:
- return False
- elif (isinstance(warp, PokemonRBWarp) and ("Rock Tunnel" not in warp.name or
- logic.rock_tunnel(state, player))) or warp.access_rule(state):
- if warp.connected_region and warp.connected_region not in checked_regions:
- checked_regions.add(warp.connected_region)
- check_warps.update(warp.connected_region.exits)
- for location in warp.connected_region.locations:
- if (location.item and location.item.name in relevant_events and
- adds_reachable_entrances(entrances_copy, location.item)):
- return False
+ # confirm warp is in entrances list to ensure it's not a loop-out interior
+ if warp.connected_region is None and warp in entrances:
+ return False
+ elif isinstance(warp, PokemonRBWarp) or warp.access_rule(state):
+ if warp.connected_region and warp.connected_region not in checked_regions:
+ checked_regions.add(warp.connected_region)
+ check_warps.update(warp.connected_region.exits)
+ for location in warp.connected_region.locations:
+ if (location.item and location.item.name in relevant_events and
+ adds_reachable_entrances(location.item)):
+ return False
return True
starting_entrances = len(entrances)
- dc_connected = []
- event_locations = self.multiworld.get_filled_locations(player)
+
while entrances:
state.update_reachable_regions(player)
state.sweep_for_events(locations=event_locations)
- reachable_entrances = [entrance for entrance in entrances if entrance in reachable_entrances or
- entrance.parent_region.can_reach(state)]
- assert reachable_entrances, \
- "Ran out of reachable entrances in Pokemon Red and Blue door shuffle"
+
multiworld.random.shuffle(entrances)
- if multiworld.door_shuffle[player] == "decoupled" and len(entrances) == 1:
- entrances += dc_connected
- entrances[-1].connect(entrances[0])
- while len(entrances) > 1:
- entrances.pop(0).connect(entrances[0])
- break
- if multiworld.door_shuffle[player] == "full" or len(entrances) != len(reachable_entrances):
+
+ if multiworld.door_shuffle[player] == "decoupled":
+ multiworld.random.shuffle(dc_destinations)
+ else:
entrances.sort(key=lambda e: e.name not in entrance_only)
- if len(entrances) < 48 and multiworld.door_shuffle[player] == "full":
- # Prevent a situation where the only remaining outdoor entrances are ones that cannot be reached
- # except by connecting directly to it.
- entrances.sort(key=lambda e: e.name in unreachable_outdoor_entrances)
- # entrances list is empty while it's being sorted, must pass a copy to iterate through
- entrances_copy = entrances.copy()
+ reachable_entrances = [entrance for entrance in entrances if entrance in reachable_entrances or
+ entrance.parent_region.can_reach(state)]
+
+ entrances.sort(key=lambda e: e in reachable_entrances)
+
+ if not reachable_entrances:
+ raise DoorShuffleException("Ran out of reachable entrances in Pokemon Red and Blue door shuffle")
+
+ entrance_a = reachable_entrances.pop(0)
+ entrances.remove(entrance_a)
+
+ is_outdoor_map = outdoor_map(entrance_a.parent_region.name)
+
+ if multiworld.door_shuffle[player] in ("interiors", "full") or len(entrances) != len(reachable_entrances):
+
+ find_dead_end = False
+ if (len(reachable_entrances) >
+ (1 if multiworld.door_shuffle[player] in ("insanity", "decoupled") else 8) and len(entrances)
+ <= (starting_entrances - 3)):
+ find_dead_end = True
+
+ if (multiworld.door_shuffle[player] in ("interiors", "full") and len(entrances) < 48
+ and not is_outdoor_map):
+ # Try to prevent a situation where the only remaining outdoor entrances are ones that cannot be
+ # reached except by connecting directly to it.
+ entrances.sort(key=lambda e: e.name not in unreachable_outdoor_entrances)
+ if entrances[0].name in unreachable_outdoor_entrances and len([entrance for entrance
+ in reachable_entrances if not outdoor_map(entrance.parent_region.name)]) > 1:
+ find_dead_end = True
+
if multiworld.door_shuffle[player] == "decoupled":
- if len(reachable_entrances) <= 8 and not logic.rock_tunnel(state, player):
- entrances.sort(key=lambda e: 1 if "Rock Tunnel" in e.name else 2 if e.connected_region is not
- None else 3 if e not in reachable_entrances else 0)
- else:
- entrances.sort(key=lambda e: 1 if e.connected_region is not None else 2 if e not in
- reachable_entrances else 0)
- assert entrances[0].connected_region is None,\
- "Ran out of valid reachable entrances in Pokemon Red and Blue door shuffle"
- elif len(reachable_entrances) > (1 if multiworld.door_shuffle[player] == "insanity" else 8) and len(
- entrances) <= (starting_entrances - 3):
- entrances.sort(key=lambda e: 0 if e in reachable_entrances else 2 if
- dead_end(entrances_copy, e) else 1)
+ destinations = dc_destinations
+ elif multiworld.door_shuffle[player] in ("interiors", "full"):
+ destinations = [entrance for entrance in entrances if outdoor_map(entrance.parent_region.name) is
+ not is_outdoor_map]
+ if not destinations:
+ raise DoorShuffleException("Ran out of connectable destinations in Pokemon Red and Blue door shuffle")
else:
- entrances.sort(key=lambda e: 0 if e in reachable_entrances else 1 if
- dead_end(entrances_copy, e) else 2)
- if multiworld.door_shuffle[player] == "full":
- outdoor = outdoor_map(entrances[0].parent_region.name)
- entrances.sort(key=lambda e: outdoor_map(e.parent_region.name) != outdoor)
- assert entrances[0] in reachable_entrances, \
- "Ran out of valid reachable entrances in Pokemon Red and Blue door shuffle"
- if (multiworld.door_shuffle[player] == "decoupled" and len(reachable_entrances) > 8 and len(entrances)
- <= (starting_entrances - 3)):
- entrance_b = entrances.pop(1)
+ destinations = entrances
+
+ destinations.sort(key=lambda e: e == entrance_a)
+ for entrance in destinations:
+ if (dead_end(entrance) is find_dead_end and (multiworld.door_shuffle[player] != "decoupled"
+ or entrance.parent_region.name.split("-")[0] !=
+ entrance_a.parent_region.name.split("-")[0])):
+ entrance_b = entrance
+ destinations.remove(entrance)
+ break
+ else:
+ entrance_b = destinations.pop(0)
+
+ if multiworld.door_shuffle[player] in ("interiors", "full"):
+ # on Interiors/Full, the destinations variable does not point to the entrances list, so we need to
+ # remove from that list here.
+ entrances.remove(entrance_b)
else:
- entrance_b = entrances.pop()
- entrance_a = entrances.pop(0)
+ # Everything is reachable. Just start connecting the rest of the doors at random.
+ if multiworld.door_shuffle[player] == "decoupled":
+ entrance_b = dc_destinations.pop(0)
+ else:
+ entrance_b = entrances.pop(0)
+
entrance_a.connect(entrance_b)
- if multiworld.door_shuffle[player] == "decoupled":
- entrances.append(entrance_b)
- dc_connected.append(entrance_a)
- else:
+ if multiworld.door_shuffle[player] != "decoupled":
entrance_b.connect(entrance_a)
- if multiworld.door_shuffle[player] == "full":
+ if multiworld.door_shuffle[player] in ("interiors", "full"):
for pair in loop_out_interiors:
pair[1].connected_region = pair[0].connected_region
pair[1].parent_region.entrances.append(pair[0])
@@ -2426,11 +2620,18 @@ def connect(self, entrance):
def access_rule(self, state):
if self.connected_region is None:
return False
- if "Rock Tunnel" in self.parent_region.name or "Rock Tunnel" in self.connected_region.name:
- return logic.rock_tunnel(state, self.player)
+ if "Elevator" in self.parent_region.name and (
+ (state.multiworld.all_elevators_locked[self.player]
+ or "Rocket Hideout" in self.parent_region.name)
+ and not state.has("Lift Key", self.player)):
+ return False
return True
+class DoorShuffleException(Exception):
+ pass
+
+
class PokemonRBRegion(Region):
def __init__(self, name, player, multiworld):
super().__init__(name, player, multiworld)
diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py
index 4b191d91765b..b6c1221a29f4 100644
--- a/worlds/pokemon_rb/rom.py
+++ b/worlds/pokemon_rb/rom.py
@@ -9,9 +9,10 @@
from .pokemon import set_mon_palettes
from .rock_tunnel import randomize_rock_tunnel
from .rom_addresses import rom_addresses
-from .regions import PokemonRBWarp, map_ids
+from .regions import PokemonRBWarp, map_ids, town_map_coords
from . import poke_data
+
def write_quizzes(self, data, random):
def get_quiz(q, a):
@@ -204,19 +205,21 @@ def generate_output(self, output_directory: str):
basemd5 = hashlib.md5()
basemd5.update(data)
- lab_loc = self.multiworld.get_entrance("Oak's Lab to Pallet Town", self.player).target
+ pallet_connections = {entrance: self.multiworld.get_entrance(f"Pallet Town to {entrance}",
+ self.player).connected_region.name for
+ entrance in ["Player's House 1F", "Oak's Lab",
+ "Rival's House"]}
paths = None
- if lab_loc == 0: # Player's House
+ if pallet_connections["Player's House 1F"] == "Oak's Lab":
paths = ((0x00, 4, 0x80, 5, 0x40, 1, 0xE0, 1, 0xFF), (0x40, 2, 0x20, 5, 0x80, 5, 0xFF))
- elif lab_loc == 1: # Rival's House
+ elif pallet_connections["Rival's House"] == "Oak's Lab":
paths = ((0x00, 4, 0xC0, 3, 0x40, 1, 0xE0, 1, 0xFF), (0x40, 2, 0x10, 3, 0x80, 5, 0xFF))
if paths:
write_bytes(data, paths[0], rom_addresses["Path_Pallet_Oak"])
write_bytes(data, paths[1], rom_addresses["Path_Pallet_Player"])
- home_loc = self.multiworld.get_entrance("Player's House 1F to Pallet Town", self.player).target
- if home_loc == 1: # Rival's House
+ if pallet_connections["Rival's House"] == "Player's House 1F":
write_bytes(data, [0x2F, 0xC7, 0x06, 0x0D, 0x00, 0x01], rom_addresses["Pallet_Fly_Coords"])
- elif home_loc == 2: # Oak's Lab
+ elif pallet_connections["Oak's Lab"] == "Player's House 1F":
write_bytes(data, [0x5F, 0xC7, 0x0C, 0x0C, 0x00, 0x00], rom_addresses["Pallet_Fly_Coords"])
for region in self.multiworld.get_regions(self.player):
@@ -238,6 +241,14 @@ def generate_output(self, output_directory: str):
data[address] = 0 if "Elevator" in connected_map_name else warp_to_ids[i]
data[address + 1] = map_ids[connected_map_name]
+ if self.multiworld.door_shuffle[self.player] == "simple":
+ for (entrance, _, _, map_coords_entries, map_name, _) in town_map_coords.values():
+ destination = self.multiworld.get_entrance(entrance, self.player).connected_region.name
+ (_, x, y, _, _, map_order_entry) = town_map_coords[destination]
+ for map_coord_entry in map_coords_entries:
+ data[rom_addresses["Town_Map_Coords"] + (map_coord_entry * 4) + 1] = (y << 4) | x
+ data[rom_addresses["Town_Map_Order"] + map_order_entry] = map_ids[map_name]
+
if not self.multiworld.key_items_only[self.player]:
for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM",
"Vermilion Gym - Lt. Surge TM", "Celadon Gym - Erika TM",
@@ -539,6 +550,10 @@ def set_trade_mon(address, loc):
write_bytes(data, self.rival_name, rom_addresses['Rival_Name'])
data[0xFF00] = 2 # client compatibility version
+ rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0',
+ 'utf8')[:21]
+ rom_name.extend([0] * (21 - len(rom_name)))
+ write_bytes(data, rom_name, 0xFFC6)
write_bytes(data, self.multiworld.seed_name.encode(), 0xFFDB)
write_bytes(data, self.multiworld.player_name[self.player].encode(), 0xFFF0)
@@ -546,10 +561,8 @@ def set_trade_mon(address, loc):
write_quizzes(self, data, random)
- for location in self.multiworld.get_locations():
- if location.player != self.player:
- continue
- elif location.party_data:
+ for location in self.multiworld.get_locations(self.player):
+ if location.party_data:
for party in location.party_data:
if not isinstance(party["party_address"], list):
addresses = [rom_addresses[party["party_address"]]]
diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py
index 9c6621523cd1..e5c073971d5d 100644
--- a/worlds/pokemon_rb/rom_addresses.py
+++ b/worlds/pokemon_rb/rom_addresses.py
@@ -1,10 +1,10 @@
rom_addresses = {
"Option_Encounter_Minimum_Steps": 0x3c1,
- "Option_Pitch_Black_Rock_Tunnel": 0x758,
- "Option_Blind_Trainers": 0x30c3,
- "Option_Trainersanity1": 0x3153,
- "Option_Split_Card_Key": 0x3e0c,
- "Option_Fix_Combat_Bugs": 0x3e0d,
+ "Option_Pitch_Black_Rock_Tunnel": 0x76a,
+ "Option_Blind_Trainers": 0x30d5,
+ "Option_Trainersanity1": 0x3165,
+ "Option_Split_Card_Key": 0x3e1e,
+ "Option_Fix_Combat_Bugs": 0x3e1f,
"Option_Lose_Money": 0x40d4,
"Base_Stats_Mew": 0x4260,
"Title_Mon_First": 0x4373,
@@ -12,101 +12,101 @@
"Player_Name": 0x4568,
"Rival_Name": 0x4570,
"Price_Master_Ball": 0x45c8,
- "Title_Seed": 0x5f1b,
- "Title_Slot_Name": 0x5f3b,
- "PC_Item": 0x6309,
- "PC_Item_Quantity": 0x630e,
- "Fly_Location": 0x631c,
- "Skip_Player_Name": 0x6335,
- "Skip_Rival_Name": 0x6343,
- "Pallet_Fly_Coords": 0x666e,
- "Option_Old_Man": 0xcb0e,
- "Option_Old_Man_Lying": 0xcb11,
- "Option_Route3_Guard_A": 0xcb17,
- "Option_Trashed_House_Guard_A": 0xcb20,
- "Option_Trashed_House_Guard_B": 0xcb26,
- "Option_Boulders": 0xcdb7,
- "Option_Rock_Tunnel_Extra_Items": 0xcdc0,
- "Wild_Route1": 0xd13b,
- "Wild_Route2": 0xd151,
- "Wild_Route22": 0xd167,
- "Wild_ViridianForest": 0xd17d,
- "Wild_Route3": 0xd193,
- "Wild_MtMoon1F": 0xd1a9,
- "Wild_MtMoonB1F": 0xd1bf,
- "Wild_MtMoonB2F": 0xd1d5,
- "Wild_Route4": 0xd1eb,
- "Wild_Route24": 0xd201,
- "Wild_Route25": 0xd217,
- "Wild_Route9": 0xd22d,
- "Wild_Route5": 0xd243,
- "Wild_Route6": 0xd259,
- "Wild_Route11": 0xd26f,
- "Wild_RockTunnel1F": 0xd285,
- "Wild_RockTunnelB1F": 0xd29b,
- "Wild_Route10": 0xd2b1,
- "Wild_Route12": 0xd2c7,
- "Wild_Route8": 0xd2dd,
- "Wild_Route7": 0xd2f3,
- "Wild_PokemonTower3F": 0xd30d,
- "Wild_PokemonTower4F": 0xd323,
- "Wild_PokemonTower5F": 0xd339,
- "Wild_PokemonTower6F": 0xd34f,
- "Wild_PokemonTower7F": 0xd365,
- "Wild_Route13": 0xd37b,
- "Wild_Route14": 0xd391,
- "Wild_Route15": 0xd3a7,
- "Wild_Route16": 0xd3bd,
- "Wild_Route17": 0xd3d3,
- "Wild_Route18": 0xd3e9,
- "Wild_SafariZoneCenter": 0xd3ff,
- "Wild_SafariZoneEast": 0xd415,
- "Wild_SafariZoneNorth": 0xd42b,
- "Wild_SafariZoneWest": 0xd441,
- "Wild_SeaRoutes": 0xd458,
- "Wild_SeafoamIslands1F": 0xd46d,
- "Wild_SeafoamIslandsB1F": 0xd483,
- "Wild_SeafoamIslandsB2F": 0xd499,
- "Wild_SeafoamIslandsB3F": 0xd4af,
- "Wild_SeafoamIslandsB4F": 0xd4c5,
- "Wild_PokemonMansion1F": 0xd4db,
- "Wild_PokemonMansion2F": 0xd4f1,
- "Wild_PokemonMansion3F": 0xd507,
- "Wild_PokemonMansionB1F": 0xd51d,
- "Wild_Route21": 0xd533,
- "Wild_Surf_Route21": 0xd548,
- "Wild_CeruleanCave1F": 0xd55d,
- "Wild_CeruleanCave2F": 0xd573,
- "Wild_CeruleanCaveB1F": 0xd589,
- "Wild_PowerPlant": 0xd59f,
- "Wild_Route23": 0xd5b5,
- "Wild_VictoryRoad2F": 0xd5cb,
- "Wild_VictoryRoad3F": 0xd5e1,
- "Wild_VictoryRoad1F": 0xd5f7,
- "Wild_DiglettsCave": 0xd60d,
- "Ghost_Battle5": 0xd781,
- "HM_Surf_Badge_a": 0xda73,
- "HM_Surf_Badge_b": 0xda78,
- "Option_Fix_Combat_Bugs_Heal_Stat_Modifiers": 0xdcc2,
- "Option_Silph_Scope_Skip": 0xe207,
- "Wild_Old_Rod": 0xe382,
- "Wild_Good_Rod": 0xe3af,
- "Option_Fix_Combat_Bugs_PP_Restore": 0xe541,
- "Option_Reusable_TMs": 0xe675,
- "Wild_Super_Rod_A": 0xeaa9,
- "Wild_Super_Rod_B": 0xeaae,
- "Wild_Super_Rod_C": 0xeab3,
- "Wild_Super_Rod_D": 0xeaba,
- "Wild_Super_Rod_E": 0xeabf,
- "Wild_Super_Rod_F": 0xeac4,
- "Wild_Super_Rod_G": 0xeacd,
- "Wild_Super_Rod_H": 0xead6,
- "Wild_Super_Rod_I": 0xeadf,
- "Wild_Super_Rod_J": 0xeae8,
- "Starting_Money_High": 0xf9aa,
- "Starting_Money_Middle": 0xf9ad,
- "Starting_Money_Low": 0xf9b0,
- "Option_Pokedex_Seen": 0xf9cb,
+ "Title_Seed": 0x5f22,
+ "Title_Slot_Name": 0x5f42,
+ "PC_Item": 0x6310,
+ "PC_Item_Quantity": 0x6315,
+ "Fly_Location": 0x6323,
+ "Skip_Player_Name": 0x633c,
+ "Skip_Rival_Name": 0x634a,
+ "Pallet_Fly_Coords": 0x6675,
+ "Option_Old_Man": 0xcb0b,
+ "Option_Old_Man_Lying": 0xcb0e,
+ "Option_Route3_Guard_A": 0xcb14,
+ "Option_Trashed_House_Guard_A": 0xcb1d,
+ "Option_Trashed_House_Guard_B": 0xcb23,
+ "Option_Boulders": 0xcdb4,
+ "Option_Rock_Tunnel_Extra_Items": 0xcdbd,
+ "Wild_Route1": 0xd138,
+ "Wild_Route2": 0xd14e,
+ "Wild_Route22": 0xd164,
+ "Wild_ViridianForest": 0xd17a,
+ "Wild_Route3": 0xd190,
+ "Wild_MtMoon1F": 0xd1a6,
+ "Wild_MtMoonB1F": 0xd1bc,
+ "Wild_MtMoonB2F": 0xd1d2,
+ "Wild_Route4": 0xd1e8,
+ "Wild_Route24": 0xd1fe,
+ "Wild_Route25": 0xd214,
+ "Wild_Route9": 0xd22a,
+ "Wild_Route5": 0xd240,
+ "Wild_Route6": 0xd256,
+ "Wild_Route11": 0xd26c,
+ "Wild_RockTunnel1F": 0xd282,
+ "Wild_RockTunnelB1F": 0xd298,
+ "Wild_Route10": 0xd2ae,
+ "Wild_Route12": 0xd2c4,
+ "Wild_Route8": 0xd2da,
+ "Wild_Route7": 0xd2f0,
+ "Wild_PokemonTower3F": 0xd30a,
+ "Wild_PokemonTower4F": 0xd320,
+ "Wild_PokemonTower5F": 0xd336,
+ "Wild_PokemonTower6F": 0xd34c,
+ "Wild_PokemonTower7F": 0xd362,
+ "Wild_Route13": 0xd378,
+ "Wild_Route14": 0xd38e,
+ "Wild_Route15": 0xd3a4,
+ "Wild_Route16": 0xd3ba,
+ "Wild_Route17": 0xd3d0,
+ "Wild_Route18": 0xd3e6,
+ "Wild_SafariZoneCenter": 0xd3fc,
+ "Wild_SafariZoneEast": 0xd412,
+ "Wild_SafariZoneNorth": 0xd428,
+ "Wild_SafariZoneWest": 0xd43e,
+ "Wild_SeaRoutes": 0xd455,
+ "Wild_SeafoamIslands1F": 0xd46a,
+ "Wild_SeafoamIslandsB1F": 0xd480,
+ "Wild_SeafoamIslandsB2F": 0xd496,
+ "Wild_SeafoamIslandsB3F": 0xd4ac,
+ "Wild_SeafoamIslandsB4F": 0xd4c2,
+ "Wild_PokemonMansion1F": 0xd4d8,
+ "Wild_PokemonMansion2F": 0xd4ee,
+ "Wild_PokemonMansion3F": 0xd504,
+ "Wild_PokemonMansionB1F": 0xd51a,
+ "Wild_Route21": 0xd530,
+ "Wild_Surf_Route21": 0xd545,
+ "Wild_CeruleanCave1F": 0xd55a,
+ "Wild_CeruleanCave2F": 0xd570,
+ "Wild_CeruleanCaveB1F": 0xd586,
+ "Wild_PowerPlant": 0xd59c,
+ "Wild_Route23": 0xd5b2,
+ "Wild_VictoryRoad2F": 0xd5c8,
+ "Wild_VictoryRoad3F": 0xd5de,
+ "Wild_VictoryRoad1F": 0xd5f4,
+ "Wild_DiglettsCave": 0xd60a,
+ "Ghost_Battle5": 0xd77e,
+ "HM_Surf_Badge_a": 0xda70,
+ "HM_Surf_Badge_b": 0xda75,
+ "Option_Fix_Combat_Bugs_Heal_Stat_Modifiers": 0xdcbf,
+ "Option_Silph_Scope_Skip": 0xe204,
+ "Wild_Old_Rod": 0xe37f,
+ "Wild_Good_Rod": 0xe3ac,
+ "Option_Fix_Combat_Bugs_PP_Restore": 0xe53e,
+ "Option_Reusable_TMs": 0xe672,
+ "Wild_Super_Rod_A": 0xeaa6,
+ "Wild_Super_Rod_B": 0xeaab,
+ "Wild_Super_Rod_C": 0xeab0,
+ "Wild_Super_Rod_D": 0xeab7,
+ "Wild_Super_Rod_E": 0xeabc,
+ "Wild_Super_Rod_F": 0xeac1,
+ "Wild_Super_Rod_G": 0xeaca,
+ "Wild_Super_Rod_H": 0xead3,
+ "Wild_Super_Rod_I": 0xeadc,
+ "Wild_Super_Rod_J": 0xeae5,
+ "Starting_Money_High": 0xf9a7,
+ "Starting_Money_Middle": 0xf9aa,
+ "Starting_Money_Low": 0xf9ad,
+ "Option_Pokedex_Seen": 0xf9c8,
"HM_Fly_Badge_a": 0x13182,
"HM_Fly_Badge_b": 0x13187,
"HM_Cut_Badge_a": 0x131b8,
@@ -131,49 +131,49 @@
"Starter2_K": 0x19611,
"Starter3_K": 0x19619,
"Event_Rocket_Thief": 0x19733,
- "Option_Cerulean_Cave_Badges": 0x19857,
- "Option_Cerulean_Cave_Key_Items": 0x1985e,
- "Text_Cerulean_Cave_Badges": 0x198c3,
- "Text_Cerulean_Cave_Key_Items": 0x198d1,
- "Event_Stranded_Man": 0x19b28,
- "Event_Rivals_Sister": 0x19cfb,
- "Warps_BluesHouse": 0x19d51,
- "Warps_VermilionTradeHouse": 0x19da8,
- "Require_Pokedex_D": 0x19e3f,
- "Option_Elite_Four_Key_Items": 0x19e89,
- "Option_Elite_Four_Pokedex": 0x19e90,
- "Option_Elite_Four_Badges": 0x19e97,
- "Text_Elite_Four_Badges": 0x19f33,
- "Text_Elite_Four_Key_Items": 0x19f3d,
- "Text_Elite_Four_Pokedex": 0x19f50,
- "Shop10": 0x1a004,
- "Warps_IndigoPlateauLobby": 0x1a030,
- "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a158,
- "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a166,
- "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a174,
- "Event_SKC4F": 0x1a187,
- "Warps_SilphCo4F": 0x1a209,
- "Missable_Silph_Co_4F_Item_1": 0x1a249,
- "Missable_Silph_Co_4F_Item_2": 0x1a250,
- "Missable_Silph_Co_4F_Item_3": 0x1a257,
- "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a3af,
- "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a3bd,
- "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a3cb,
- "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a3d9,
- "Event_SKC5F": 0x1a3ec,
- "Warps_SilphCo5F": 0x1a496,
- "Missable_Silph_Co_5F_Item_1": 0x1a4de,
- "Missable_Silph_Co_5F_Item_2": 0x1a4e5,
- "Missable_Silph_Co_5F_Item_3": 0x1a4ec,
- "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a61c,
- "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a62a,
- "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a638,
- "Event_SKC6F": 0x1a666,
- "Warps_SilphCo6F": 0x1a741,
- "Missable_Silph_Co_6F_Item_1": 0x1a791,
- "Missable_Silph_Co_6F_Item_2": 0x1a798,
- "Path_Pallet_Oak": 0x1a91e,
- "Path_Pallet_Player": 0x1a92b,
+ "Option_Cerulean_Cave_Badges": 0x19861,
+ "Option_Cerulean_Cave_Key_Items": 0x19868,
+ "Text_Cerulean_Cave_Badges": 0x198d7,
+ "Text_Cerulean_Cave_Key_Items": 0x198e5,
+ "Event_Stranded_Man": 0x19b3c,
+ "Event_Rivals_Sister": 0x19d0f,
+ "Warps_BluesHouse": 0x19d65,
+ "Warps_VermilionTradeHouse": 0x19dbc,
+ "Require_Pokedex_D": 0x19e53,
+ "Option_Elite_Four_Key_Items": 0x19e9d,
+ "Option_Elite_Four_Pokedex": 0x19ea4,
+ "Option_Elite_Four_Badges": 0x19eab,
+ "Text_Elite_Four_Badges": 0x19f47,
+ "Text_Elite_Four_Key_Items": 0x19f51,
+ "Text_Elite_Four_Pokedex": 0x19f64,
+ "Shop10": 0x1a018,
+ "Warps_IndigoPlateauLobby": 0x1a044,
+ "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a16c,
+ "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a17a,
+ "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a188,
+ "Event_SKC4F": 0x1a19b,
+ "Warps_SilphCo4F": 0x1a21d,
+ "Missable_Silph_Co_4F_Item_1": 0x1a25d,
+ "Missable_Silph_Co_4F_Item_2": 0x1a264,
+ "Missable_Silph_Co_4F_Item_3": 0x1a26b,
+ "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a3c3,
+ "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a3d1,
+ "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a3df,
+ "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a3ed,
+ "Event_SKC5F": 0x1a400,
+ "Warps_SilphCo5F": 0x1a4aa,
+ "Missable_Silph_Co_5F_Item_1": 0x1a4f2,
+ "Missable_Silph_Co_5F_Item_2": 0x1a4f9,
+ "Missable_Silph_Co_5F_Item_3": 0x1a500,
+ "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a630,
+ "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a63e,
+ "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a64c,
+ "Event_SKC6F": 0x1a66d,
+ "Warps_SilphCo6F": 0x1a74b,
+ "Missable_Silph_Co_6F_Item_1": 0x1a79b,
+ "Missable_Silph_Co_6F_Item_2": 0x1a7a2,
+ "Path_Pallet_Oak": 0x1a928,
+ "Path_Pallet_Player": 0x1a935,
"Warps_CinnabarIsland": 0x1c026,
"Warps_Route1": 0x1c0e9,
"Option_Extra_Key_Items_B": 0x1ca46,
@@ -1074,112 +1074,112 @@
"Missable_Route_25_Item": 0x5080b,
"Warps_IndigoPlateau": 0x5093a,
"Warps_SaffronCity": 0x509e0,
- "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_0_ITEM": 0x50d63,
- "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_1_ITEM": 0x50d71,
- "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_2_ITEM": 0x50d7f,
- "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_3_ITEM": 0x50d8d,
- "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_4_ITEM": 0x50d9b,
- "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_5_ITEM": 0x50da9,
- "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_6_ITEM": 0x50db7,
- "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_7_ITEM": 0x50dc5,
- "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_8_ITEM": 0x50dd3,
- "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_9_ITEM": 0x50de1,
- "Starter2_B": 0x50ffe,
- "Starter3_B": 0x51000,
- "Starter1_B": 0x51002,
- "Starter2_A": 0x5111d,
- "Starter3_A": 0x5111f,
- "Starter1_A": 0x51121,
- "Option_Route23_Badges": 0x5126e,
- "Trainersanity_EVENT_BEAT_ROUTE_24_TRAINER_0_ITEM": 0x51384,
- "Trainersanity_EVENT_BEAT_ROUTE_24_TRAINER_1_ITEM": 0x51392,
- "Trainersanity_EVENT_BEAT_ROUTE_24_TRAINER_2_ITEM": 0x513a0,
- "Trainersanity_EVENT_BEAT_ROUTE_24_TRAINER_3_ITEM": 0x513ae,
- "Trainersanity_EVENT_BEAT_ROUTE_24_TRAINER_4_ITEM": 0x513bc,
- "Trainersanity_EVENT_BEAT_ROUTE_24_TRAINER_5_ITEM": 0x513ca,
- "Event_Nugget_Bridge": 0x513e1,
- "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_0_ITEM": 0x51569,
- "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_1_ITEM": 0x51577,
- "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_2_ITEM": 0x51585,
- "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_3_ITEM": 0x51593,
- "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_4_ITEM": 0x515a1,
- "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_5_ITEM": 0x515af,
- "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_6_ITEM": 0x515bd,
- "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_7_ITEM": 0x515cb,
- "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_8_ITEM": 0x515d9,
- "Trainersanity_EVENT_BEAT_VICTORY_ROAD_2_TRAINER_0_ITEM": 0x51772,
- "Trainersanity_EVENT_BEAT_VICTORY_ROAD_2_TRAINER_1_ITEM": 0x51780,
- "Trainersanity_EVENT_BEAT_VICTORY_ROAD_2_TRAINER_2_ITEM": 0x5178e,
- "Trainersanity_EVENT_BEAT_VICTORY_ROAD_2_TRAINER_3_ITEM": 0x5179c,
- "Trainersanity_EVENT_BEAT_VICTORY_ROAD_2_TRAINER_4_ITEM": 0x517aa,
- "Trainersanity_EVENT_BEAT_MOLTRES_ITEM": 0x517b8,
- "Warps_VictoryRoad2F": 0x51855,
- "Static_Encounter_Moltres": 0x5189f,
- "Missable_Victory_Road_2F_Item_1": 0x518a7,
- "Missable_Victory_Road_2F_Item_2": 0x518ae,
- "Missable_Victory_Road_2F_Item_3": 0x518b5,
- "Missable_Victory_Road_2F_Item_4": 0x518bc,
- "Warps_MtMoonB1F": 0x5198d,
- "Starter2_L": 0x51beb,
- "Starter3_L": 0x51bf3,
- "Trainersanity_EVENT_BEAT_SILPH_CO_7F_TRAINER_0_ITEM": 0x51ca4,
- "Trainersanity_EVENT_BEAT_SILPH_CO_7F_TRAINER_1_ITEM": 0x51cb2,
- "Trainersanity_EVENT_BEAT_SILPH_CO_7F_TRAINER_2_ITEM": 0x51cc0,
- "Trainersanity_EVENT_BEAT_SILPH_CO_7F_TRAINER_3_ITEM": 0x51cce,
- "Gift_Lapras": 0x51cef,
- "Event_SKC7F": 0x51d7a,
- "Warps_SilphCo7F": 0x51e49,
- "Missable_Silph_Co_7F_Item_1": 0x51ea5,
- "Missable_Silph_Co_7F_Item_2": 0x51eac,
- "Trainersanity_EVENT_BEAT_MANSION_2_TRAINER_0_ITEM": 0x51fd2,
- "Warps_PokemonMansion2F": 0x52045,
- "Missable_Pokemon_Mansion_2F_Item": 0x52063,
- "Trainersanity_EVENT_BEAT_MANSION_3_TRAINER_0_ITEM": 0x52213,
- "Trainersanity_EVENT_BEAT_MANSION_3_TRAINER_1_ITEM": 0x52221,
- "Warps_PokemonMansion3F": 0x5225e,
- "Missable_Pokemon_Mansion_3F_Item_1": 0x52280,
- "Missable_Pokemon_Mansion_3F_Item_2": 0x52287,
- "Trainersanity_EVENT_BEAT_MANSION_4_TRAINER_0_ITEM": 0x523c9,
- "Trainersanity_EVENT_BEAT_MANSION_4_TRAINER_1_ITEM": 0x523d7,
- "Warps_PokemonMansionB1F": 0x52414,
- "Missable_Pokemon_Mansion_B1F_Item_1": 0x5242e,
- "Missable_Pokemon_Mansion_B1F_Item_2": 0x52435,
- "Missable_Pokemon_Mansion_B1F_Item_3": 0x5243c,
- "Missable_Pokemon_Mansion_B1F_Item_4": 0x52443,
- "Missable_Pokemon_Mansion_B1F_Item_5": 0x52450,
- "Option_Safari_Zone_Battle_Type": 0x52565,
- "Prize_Mon_A2": 0x527ef,
- "Prize_Mon_B2": 0x527f0,
- "Prize_Mon_C2": 0x527f1,
- "Prize_Mon_D2": 0x527fa,
- "Prize_Mon_E2": 0x527fb,
- "Prize_Mon_F2": 0x527fc,
- "Prize_Item_A": 0x52805,
- "Prize_Item_B": 0x52806,
- "Prize_Item_C": 0x52807,
- "Prize_Mon_A": 0x5293c,
- "Prize_Mon_B": 0x5293e,
- "Prize_Mon_C": 0x52940,
- "Prize_Mon_D": 0x52942,
- "Prize_Mon_E": 0x52944,
- "Prize_Mon_F": 0x52946,
- "Start_Inventory": 0x52a7b,
- "Map_Fly_Location": 0x52c6f,
- "Reset_A": 0x52d1b,
- "Reset_B": 0x52d47,
- "Reset_C": 0x52d73,
- "Reset_D": 0x52d9f,
- "Reset_E": 0x52dcb,
- "Reset_F": 0x52df7,
- "Reset_G": 0x52e23,
- "Reset_H": 0x52e4f,
- "Reset_I": 0x52e7b,
- "Reset_J": 0x52ea7,
- "Reset_K": 0x52ed3,
- "Reset_L": 0x52eff,
- "Reset_M": 0x52f2b,
- "Reset_N": 0x52f57,
- "Reset_O": 0x52f83,
+ "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_0_ITEM": 0x50d8b,
+ "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_1_ITEM": 0x50d99,
+ "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_2_ITEM": 0x50da7,
+ "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_3_ITEM": 0x50db5,
+ "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_4_ITEM": 0x50dc3,
+ "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_5_ITEM": 0x50dd1,
+ "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_6_ITEM": 0x50ddf,
+ "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_7_ITEM": 0x50ded,
+ "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_8_ITEM": 0x50dfb,
+ "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_9_ITEM": 0x50e09,
+ "Starter2_B": 0x51026,
+ "Starter3_B": 0x51028,
+ "Starter1_B": 0x5102a,
+ "Starter2_A": 0x51145,
+ "Starter3_A": 0x51147,
+ "Starter1_A": 0x51149,
+ "Option_Route23_Badges": 0x51296,
+ "Trainersanity_EVENT_BEAT_ROUTE_24_TRAINER_0_ITEM": 0x513ac,
+ "Trainersanity_EVENT_BEAT_ROUTE_24_TRAINER_1_ITEM": 0x513ba,
+ "Trainersanity_EVENT_BEAT_ROUTE_24_TRAINER_2_ITEM": 0x513c8,
+ "Trainersanity_EVENT_BEAT_ROUTE_24_TRAINER_3_ITEM": 0x513d6,
+ "Trainersanity_EVENT_BEAT_ROUTE_24_TRAINER_4_ITEM": 0x513e4,
+ "Trainersanity_EVENT_BEAT_ROUTE_24_TRAINER_5_ITEM": 0x513f2,
+ "Event_Nugget_Bridge": 0x51409,
+ "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_0_ITEM": 0x51591,
+ "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_1_ITEM": 0x5159f,
+ "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_2_ITEM": 0x515ad,
+ "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_3_ITEM": 0x515bb,
+ "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_4_ITEM": 0x515c9,
+ "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_5_ITEM": 0x515d7,
+ "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_6_ITEM": 0x515e5,
+ "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_7_ITEM": 0x515f3,
+ "Trainersanity_EVENT_BEAT_ROUTE_25_TRAINER_8_ITEM": 0x51601,
+ "Trainersanity_EVENT_BEAT_VICTORY_ROAD_2_TRAINER_0_ITEM": 0x5179a,
+ "Trainersanity_EVENT_BEAT_VICTORY_ROAD_2_TRAINER_1_ITEM": 0x517a8,
+ "Trainersanity_EVENT_BEAT_VICTORY_ROAD_2_TRAINER_2_ITEM": 0x517b6,
+ "Trainersanity_EVENT_BEAT_VICTORY_ROAD_2_TRAINER_3_ITEM": 0x517c4,
+ "Trainersanity_EVENT_BEAT_VICTORY_ROAD_2_TRAINER_4_ITEM": 0x517d2,
+ "Trainersanity_EVENT_BEAT_MOLTRES_ITEM": 0x517e0,
+ "Warps_VictoryRoad2F": 0x5187d,
+ "Static_Encounter_Moltres": 0x518c7,
+ "Missable_Victory_Road_2F_Item_1": 0x518cf,
+ "Missable_Victory_Road_2F_Item_2": 0x518d6,
+ "Missable_Victory_Road_2F_Item_3": 0x518dd,
+ "Missable_Victory_Road_2F_Item_4": 0x518e4,
+ "Warps_MtMoonB1F": 0x519b5,
+ "Starter2_L": 0x51c13,
+ "Starter3_L": 0x51c1b,
+ "Trainersanity_EVENT_BEAT_SILPH_CO_7F_TRAINER_0_ITEM": 0x51ccc,
+ "Trainersanity_EVENT_BEAT_SILPH_CO_7F_TRAINER_1_ITEM": 0x51cda,
+ "Trainersanity_EVENT_BEAT_SILPH_CO_7F_TRAINER_2_ITEM": 0x51ce8,
+ "Trainersanity_EVENT_BEAT_SILPH_CO_7F_TRAINER_3_ITEM": 0x51cf6,
+ "Gift_Lapras": 0x51d17,
+ "Event_SKC7F": 0x51da2,
+ "Warps_SilphCo7F": 0x51e71,
+ "Missable_Silph_Co_7F_Item_1": 0x51ecd,
+ "Missable_Silph_Co_7F_Item_2": 0x51ed4,
+ "Trainersanity_EVENT_BEAT_MANSION_2_TRAINER_0_ITEM": 0x51ffa,
+ "Warps_PokemonMansion2F": 0x5206d,
+ "Missable_Pokemon_Mansion_2F_Item": 0x5208b,
+ "Trainersanity_EVENT_BEAT_MANSION_3_TRAINER_0_ITEM": 0x5223b,
+ "Trainersanity_EVENT_BEAT_MANSION_3_TRAINER_1_ITEM": 0x52249,
+ "Warps_PokemonMansion3F": 0x52286,
+ "Missable_Pokemon_Mansion_3F_Item_1": 0x522a8,
+ "Missable_Pokemon_Mansion_3F_Item_2": 0x522af,
+ "Trainersanity_EVENT_BEAT_MANSION_4_TRAINER_0_ITEM": 0x523f1,
+ "Trainersanity_EVENT_BEAT_MANSION_4_TRAINER_1_ITEM": 0x523ff,
+ "Warps_PokemonMansionB1F": 0x5243c,
+ "Missable_Pokemon_Mansion_B1F_Item_1": 0x52456,
+ "Missable_Pokemon_Mansion_B1F_Item_2": 0x5245d,
+ "Missable_Pokemon_Mansion_B1F_Item_3": 0x52464,
+ "Missable_Pokemon_Mansion_B1F_Item_4": 0x5246b,
+ "Missable_Pokemon_Mansion_B1F_Item_5": 0x52478,
+ "Option_Safari_Zone_Battle_Type": 0x5258d,
+ "Prize_Mon_A2": 0x52817,
+ "Prize_Mon_B2": 0x52818,
+ "Prize_Mon_C2": 0x52819,
+ "Prize_Mon_D2": 0x52822,
+ "Prize_Mon_E2": 0x52823,
+ "Prize_Mon_F2": 0x52824,
+ "Prize_Item_A": 0x5282d,
+ "Prize_Item_B": 0x5282e,
+ "Prize_Item_C": 0x5282f,
+ "Prize_Mon_A": 0x52964,
+ "Prize_Mon_B": 0x52966,
+ "Prize_Mon_C": 0x52968,
+ "Prize_Mon_D": 0x5296a,
+ "Prize_Mon_E": 0x5296c,
+ "Prize_Mon_F": 0x5296e,
+ "Start_Inventory": 0x52aa3,
+ "Map_Fly_Location": 0x52c9d,
+ "Reset_A": 0x52d49,
+ "Reset_B": 0x52d75,
+ "Reset_C": 0x52da1,
+ "Reset_D": 0x52dcd,
+ "Reset_E": 0x52df9,
+ "Reset_F": 0x52e25,
+ "Reset_G": 0x52e51,
+ "Reset_H": 0x52e7d,
+ "Reset_I": 0x52ea9,
+ "Reset_J": 0x52ed5,
+ "Reset_K": 0x52f01,
+ "Reset_L": 0x52f2d,
+ "Reset_M": 0x52f59,
+ "Reset_N": 0x52f85,
+ "Reset_O": 0x52fb1,
"Warps_Route2": 0x54026,
"Missable_Route_2_Item_1": 0x5404a,
"Missable_Route_2_Item_2": 0x54051,
@@ -1539,16 +1539,18 @@
"Event_SKC11F": 0x623bd,
"Warps_SilphCo11F": 0x62446,
"Ghost_Battle4": 0x708e1,
- "Trade_Terry": 0x71b77,
- "Trade_Marcel": 0x71b85,
- "Trade_Sailor": 0x71ba1,
- "Trade_Dux": 0x71baf,
- "Trade_Marc": 0x71bbd,
- "Trade_Lola": 0x71bcb,
- "Trade_Doris": 0x71bd9,
- "Trade_Crinkles": 0x71be7,
- "Trade_Spot": 0x71bf5,
- "Mon_Palettes": 0x725d3,
+ "Town_Map_Order": 0x70f0f,
+ "Town_Map_Coords": 0x71381,
+ "Trade_Terry": 0x71b7a,
+ "Trade_Marcel": 0x71b88,
+ "Trade_Sailor": 0x71ba4,
+ "Trade_Dux": 0x71bb2,
+ "Trade_Marc": 0x71bc0,
+ "Trade_Lola": 0x71bce,
+ "Trade_Doris": 0x71bdc,
+ "Trade_Crinkles": 0x71bea,
+ "Trade_Spot": 0x71bf8,
+ "Mon_Palettes": 0x725d6,
"Badge_Viridian_Gym": 0x749d9,
"Event_Viridian_Gym": 0x749ed,
"Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_0_ITEM": 0x74a48,
diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py
index 0855e7a108bd..1d68f3148963 100644
--- a/worlds/pokemon_rb/rules.py
+++ b/worlds/pokemon_rb/rules.py
@@ -22,7 +22,7 @@ def prize_rule(i):
item_rules["Celadon Prize Corner - Item Prize 2"] = prize_rule
item_rules["Celadon Prize Corner - Item Prize 3"] = prize_rule
- if multiworld.accessibility[player] != "locations":
+ if multiworld.accessibility[player] != "full":
multiworld.get_location("Cerulean Bicycle Shop", player).always_allow = (lambda state, item:
item.name == "Bike Voucher"
and item.player == player)
@@ -103,25 +103,25 @@ def prize_rule(i):
"Route 22 - Trainer Parties": lambda state: state.has("Oak's Parcel", player),
# # Rock Tunnel
- # "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, player),
- # "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, player),
+ "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, player),
# Pokédex check
"Oak's Lab - Oak's Parcel Reward": lambda state: state.has("Oak's Parcel", player),
diff --git a/worlds/raft/Options.py b/worlds/raft/Options.py
index b9d0a2298a07..efe460b50353 100644
--- a/worlds/raft/Options.py
+++ b/worlds/raft/Options.py
@@ -1,4 +1,5 @@
-from Options import Range, Toggle, DefaultOnToggle, Choice, DeathLink
+from dataclasses import dataclass
+from Options import Range, Toggle, DefaultOnToggle, Choice, DeathLink, PerGameCommonOptions
class MinimumResourcePackAmount(Range):
"""The minimum amount of resources available in a resource pack"""
@@ -32,7 +33,13 @@ class FillerItemTypes(Choice):
option_both = 2
class IslandFrequencyLocations(Choice):
- """Sets where frequencies for story islands are located."""
+ """Sets where frequencies for story islands are located.
+ Vanilla will keep frequencies in their vanilla, non-randomized locations.
+ Random On Island will randomize each frequency within its vanilla island, but will preserve island order.
+ Random Island Order will change the order you visit islands, but will preserve the vanilla location of each frequency unlock.
+ Random On Island Random Order will randomize the location containing the frequency on each island and randomize the order.
+ Progressive will randomize the frequencies to anywhere, but will always unlock the frequencies in vanilla order as the frequency items are received.
+ Anywhere will randomize the frequencies to anywhere, and frequencies will be received in any order."""
display_name = "Frequency locations"
option_vanilla = 0
option_random_on_island = 1
@@ -41,6 +48,8 @@ class IslandFrequencyLocations(Choice):
option_progressive = 4
option_anywhere = 5
default = 2
+ def is_filling_frequencies_in_world(self):
+ return self.value <= self.option_random_on_island_random_order
class IslandGenerationDistance(Choice):
"""Sets how far away islands spawn from you when you input their coordinates into the Receiver."""
@@ -53,7 +62,8 @@ class IslandGenerationDistance(Choice):
default = 8
class ExpensiveResearch(Toggle):
- """Makes unlocking items in the Crafting Table consume the researched items."""
+ """If No is selected, researching items and unlocking items in the Crafting Table works the same as vanilla Raft.
+ If Yes is selected, each unlock in the Crafting Table will require its own set of researched items in order to unlock it."""
display_name = "Expensive research"
class ProgressiveItems(DefaultOnToggle):
@@ -66,20 +76,19 @@ class BigIslandEarlyCrafting(Toggle):
display_name = "Early recipes behind big islands"
class PaddleboardMode(Toggle):
- """Sets later story islands to in logic without an Engine or Steering Wheel. May require lots of paddling. Not
- recommended."""
+ """Sets later story islands to be in logic without an Engine or Steering Wheel. May require lots of paddling."""
display_name = "Paddleboard Mode"
-raft_options = {
- "minimum_resource_pack_amount": MinimumResourcePackAmount,
- "maximum_resource_pack_amount": MaximumResourcePackAmount,
- "duplicate_items": DuplicateItems,
- "filler_item_types": FillerItemTypes,
- "island_frequency_locations": IslandFrequencyLocations,
- "island_generation_distance": IslandGenerationDistance,
- "expensive_research": ExpensiveResearch,
- "progressive_items": ProgressiveItems,
- "big_island_early_crafting": BigIslandEarlyCrafting,
- "paddleboard_mode": PaddleboardMode,
- "death_link": DeathLink
-}
+@dataclass
+class RaftOptions(PerGameCommonOptions):
+ minimum_resource_pack_amount: MinimumResourcePackAmount
+ maximum_resource_pack_amount: MaximumResourcePackAmount
+ duplicate_items: DuplicateItems
+ filler_item_types: FillerItemTypes
+ island_frequency_locations: IslandFrequencyLocations
+ island_generation_distance: IslandGenerationDistance
+ expensive_research: ExpensiveResearch
+ progressive_items: ProgressiveItems
+ big_island_early_crafting: BigIslandEarlyCrafting
+ paddleboard_mode: PaddleboardMode
+ death_link: DeathLink
diff --git a/worlds/raft/Rules.py b/worlds/raft/Rules.py
index e84068a6f584..b6bd49c187cd 100644
--- a/worlds/raft/Rules.py
+++ b/worlds/raft/Rules.py
@@ -5,10 +5,10 @@
class RaftLogic(LogicMixin):
def raft_paddleboard_mode_enabled(self, player):
- return self.multiworld.paddleboard_mode[player].value
+ return bool(self.multiworld.worlds[player].options.paddleboard_mode)
def raft_big_islands_available(self, player):
- return self.multiworld.big_island_early_crafting[player].value or self.raft_can_access_radio_tower(player)
+ return bool(self.multiworld.worlds[player].options.big_island_early_crafting) or self.raft_can_access_radio_tower(player)
def raft_can_smelt_items(self, player):
return self.has("Smelter", player)
diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py
index d00b5faa9e90..71d5d1c7e44b 100644
--- a/worlds/raft/__init__.py
+++ b/worlds/raft/__init__.py
@@ -1,5 +1,4 @@
import typing
-import random
from .Locations import location_table, lookup_name_to_id as locations_lookup_name_to_id
from .Items import (createResourcePackName, item_table, progressive_table, progressive_item_list,
@@ -7,7 +6,7 @@
from .Regions import create_regions, getConnectionName
from .Rules import set_rules
-from .Options import raft_options
+from .Options import RaftOptions
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, Tutorial
from ..AutoWorld import World, WebWorld
@@ -38,17 +37,17 @@ class RaftWorld(World):
lastItemId = max(filter(lambda val: val is not None, item_name_to_id.values()))
location_name_to_id = locations_lookup_name_to_id
- option_definitions = raft_options
+ options_dataclass = RaftOptions
+ options: RaftOptions
- data_version = 2
required_client_version = (0, 3, 4)
def create_items(self):
- minRPSpecified = self.multiworld.minimum_resource_pack_amount[self.player].value
- maxRPSpecified = self.multiworld.maximum_resource_pack_amount[self.player].value
+ minRPSpecified = self.options.minimum_resource_pack_amount.value
+ maxRPSpecified = self.options.maximum_resource_pack_amount.value
minimumResourcePackAmount = min(minRPSpecified, maxRPSpecified)
maximumResourcePackAmount = max(minRPSpecified, maxRPSpecified)
- isFillingFrequencies = self.multiworld.island_frequency_locations[self.player].value <= 3
+ isFillingFrequencies = self.options.island_frequency_locations.is_filling_frequencies_in_world()
# Generate item pool
pool = []
frequencyItems = []
@@ -66,20 +65,20 @@ def create_items(self):
extraItemNamePool = []
extras = len(location_table) - len(item_table) - 1 # Victory takes up 1 unaccounted-for slot
if extras > 0:
- if (self.multiworld.filler_item_types[self.player].value != 1): # Use resource packs
+ if (self.options.filler_item_types != self.options.filler_item_types.option_duplicates): # Use resource packs
for packItem in resourcePackItems:
for i in range(minimumResourcePackAmount, maximumResourcePackAmount + 1):
extraItemNamePool.append(createResourcePackName(i, packItem))
- if self.multiworld.filler_item_types[self.player].value != 0: # Use duplicate items
+ if self.options.filler_item_types != self.options.filler_item_types.option_resource_packs: # Use duplicate items
dupeItemPool = item_table.copy()
# Remove frequencies if necessary
- if self.multiworld.island_frequency_locations[self.player].value != 5: # Not completely random locations
+ if self.options.island_frequency_locations != self.options.island_frequency_locations.option_anywhere: # Not completely random locations
# If we let frequencies stay in with progressive-frequencies, the progressive-frequency item
# will be included 7 times. This is a massive flood of progressive-frequency items, so we
# instead add progressive-frequency as its own item a smaller amount of times to prevent
# flooding the duplicate item pool with them.
- if self.multiworld.island_frequency_locations[self.player].value == 4:
+ if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive:
for _ in range(2):
# Progressives are not in item_pool, need to create faux item for duplicate item pool
# This can still be filtered out later by duplicate_items setting
@@ -88,9 +87,9 @@ def create_items(self):
dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"])
# Remove progression or non-progression items if necessary
- if (self.multiworld.duplicate_items[self.player].value == 0): # Progression only
+ if (self.options.duplicate_items == self.options.duplicate_items.option_progression): # Progression only
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True)
- elif (self.multiworld.duplicate_items[self.player].value == 1): # Non-progression only
+ elif (self.options.duplicate_items == self.options.duplicate_items.option_non_progression): # Non-progression only
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False)
dupeItemPool = list(dupeItemPool)
@@ -100,7 +99,7 @@ def create_items(self):
extraItemNamePool.append(item["name"])
if (len(extraItemNamePool) > 0):
- for randomItem in random.choices(extraItemNamePool, k=extras):
+ for randomItem in self.random.choices(extraItemNamePool, k=extras):
raft_item = self.create_item_replaceAsNecessary(randomItem)
pool.append(raft_item)
@@ -117,14 +116,14 @@ def create_regions(self):
create_regions(self.multiworld, self.player)
def get_pre_fill_items(self):
- if self.multiworld.island_frequency_locations[self.player] in [0, 1, 2, 3]:
+ if self.options.island_frequency_locations.is_filling_frequencies_in_world():
return [loc.item for loc in self.multiworld.get_filled_locations()]
return []
def create_item_replaceAsNecessary(self, name: str) -> Item:
isFrequency = "Frequency" in name
- shouldUseProgressive = ((isFrequency and self.multiworld.island_frequency_locations[self.player].value == 4)
- or (not isFrequency and self.multiworld.progressive_items[self.player].value))
+ shouldUseProgressive = bool((isFrequency and self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive)
+ or (not isFrequency and self.options.progressive_items))
if shouldUseProgressive and name in progressive_table:
name = progressive_table[name]
return self.create_item(name)
@@ -138,6 +137,8 @@ def create_resourcePack(self, rpName: str) -> Item:
return RaftItem(rpName, ItemClassification.filler, self.item_name_to_id[rpName], player=self.player)
def collect_item(self, state, item, remove=False):
+ if item.advancement is False:
+ return None
if item.name in progressive_item_list:
prog_table = progressive_item_list[item.name]
if remove:
@@ -152,7 +153,7 @@ def collect_item(self, state, item, remove=False):
return super(RaftWorld, self).collect_item(state, item, remove)
def pre_fill(self):
- if self.multiworld.island_frequency_locations[self.player] == 0: # Vanilla
+ if self.options.island_frequency_locations == self.options.island_frequency_locations.option_vanilla:
self.setLocationItem("Radio Tower Frequency to Vasagatan", "Vasagatan Frequency")
self.setLocationItem("Vasagatan Frequency to Balboa", "Balboa Island Frequency")
self.setLocationItem("Relay Station quest", "Caravan Island Frequency")
@@ -160,7 +161,7 @@ def pre_fill(self):
self.setLocationItem("Tangaroa Frequency to Varuna Point", "Varuna Point Frequency")
self.setLocationItem("Varuna Point Frequency to Temperance", "Temperance Frequency")
self.setLocationItem("Temperance Frequency to Utopia", "Utopia Frequency")
- elif self.multiworld.island_frequency_locations[self.player] == 1: # Random on island
+ elif self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_on_island:
self.setLocationItemFromRegion("RadioTower", "Vasagatan Frequency")
self.setLocationItemFromRegion("Vasagatan", "Balboa Island Frequency")
self.setLocationItemFromRegion("BalboaIsland", "Caravan Island Frequency")
@@ -168,7 +169,10 @@ def pre_fill(self):
self.setLocationItemFromRegion("Tangaroa", "Varuna Point Frequency")
self.setLocationItemFromRegion("Varuna Point", "Temperance Frequency")
self.setLocationItemFromRegion("Temperance", "Utopia Frequency")
- elif self.multiworld.island_frequency_locations[self.player] in [2, 3]:
+ elif self.options.island_frequency_locations in [
+ self.options.island_frequency_locations.option_random_island_order,
+ self.options.island_frequency_locations.option_random_on_island_random_order
+ ]:
locationToFrequencyItemMap = {
"Vasagatan": "Vasagatan Frequency",
"BalboaIsland": "Balboa Island Frequency",
@@ -192,13 +196,13 @@ def pre_fill(self):
previousLocation = "RadioTower"
while (len(availableLocationList) > 0):
if (len(availableLocationList) > 1):
- currentLocation = availableLocationList[random.randint(0, len(availableLocationList) - 2)]
+ currentLocation = availableLocationList[self.random.randint(0, len(availableLocationList) - 2)]
else:
currentLocation = availableLocationList[0] # Utopia (only one left in list)
availableLocationList.remove(currentLocation)
- if self.multiworld.island_frequency_locations[self.player] == 2: # Random island order
+ if self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_island_order:
self.setLocationItem(locationToVanillaFrequencyLocationMap[previousLocation], locationToFrequencyItemMap[currentLocation])
- elif self.multiworld.island_frequency_locations[self.player] == 3: # Random on island random order
+ elif self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_on_island_random_order:
self.setLocationItemFromRegion(previousLocation, locationToFrequencyItemMap[currentLocation])
previousLocation = currentLocation
@@ -210,14 +214,14 @@ def setLocationItem(self, location: str, itemName: str):
def setLocationItemFromRegion(self, region: str, itemName: str):
itemToUse = next(filter(lambda itm: itm.name == itemName, self.multiworld.raft_frequencyItemsPerPlayer[self.player]))
self.multiworld.raft_frequencyItemsPerPlayer[self.player].remove(itemToUse)
- location = random.choice(list(loc for loc in location_table if loc["region"] == region))
+ location = self.random.choice(list(loc for loc in location_table if loc["region"] == region))
self.multiworld.get_location(location["name"], self.player).place_locked_item(itemToUse)
def fill_slot_data(self):
return {
- "IslandGenerationDistance": self.multiworld.island_generation_distance[self.player].value,
- "ExpensiveResearch": bool(self.multiworld.expensive_research[self.player].value),
- "DeathLink": bool(self.multiworld.death_link[self.player].value)
+ "IslandGenerationDistance": self.options.island_generation_distance.value,
+ "ExpensiveResearch": bool(self.options.expensive_research),
+ "DeathLink": bool(self.options.death_link)
}
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
diff --git a/worlds/raft/docs/en_Raft.md b/worlds/raft/docs/en_Raft.md
index 385377d45608..0c68e23d0019 100644
--- a/worlds/raft/docs/en_Raft.md
+++ b/worlds/raft/docs/en_Raft.md
@@ -1,7 +1,7 @@
# Raft
-## Where is the settings page?
-The player settings page for this game is located here . It contains all the options
+## Where is the options page?
+The player options page for this game is located here . It contains all the options
you need to configure and export a config file.
## What does randomization do to this game?
diff --git a/worlds/raft/docs/setup_en.md b/worlds/raft/docs/setup_en.md
index 236bb8d8acf2..16e7883776c3 100644
--- a/worlds/raft/docs/setup_en.md
+++ b/worlds/raft/docs/setup_en.md
@@ -17,7 +17,7 @@
4. Open RML and click Play. If you've already installed it, the executable that was used to install RML ("RMLLauncher.exe" unless renamed) should be used to run RML. Raft should start after clicking Play.
-5. Open the RML menu. This should open automatically when Raft first loads. If it does not, and you see RML information in the top center of the Raft main menu, press F9 to open it.
+5. Open the RML menu. This should open automatically when Raft first loads. If it does not, and you see RML information in the top center of the Raft main menu, press F9 to open it. If you do not see RML information at the top, close Raft+RML, go back to Step 4 and run RML as administrator.
6. Navigate to the "Mod manager" tab in the left-hand menu.
diff --git a/worlds/rogue_legacy/Presets.py b/worlds/rogue_legacy/Presets.py
new file mode 100644
index 000000000000..2dfeee64d8ca
--- /dev/null
+++ b/worlds/rogue_legacy/Presets.py
@@ -0,0 +1,61 @@
+from typing import Any, Dict
+
+from .Options import Architect, GoldGainMultiplier, Vendors
+
+rl_options_presets: Dict[str, Dict[str, Any]] = {
+ # Example preset using only literal values.
+ "Unknown Fate": {
+ "progression_balancing": "random",
+ "accessibility": "random",
+ "starting_gender": "random",
+ "starting_class": "random",
+ "new_game_plus": "random",
+ "fairy_chests_per_zone": "random",
+ "chests_per_zone": "random",
+ "universal_fairy_chests": "random",
+ "universal_chests": "random",
+ "vendors": "random",
+ "architect": "random",
+ "architect_fee": "random",
+ "disable_charon": "random",
+ "require_purchasing": "random",
+ "progressive_blueprints": "random",
+ "gold_gain_multiplier": "random",
+ "number_of_children": "random",
+ "free_diary_on_generation": "random",
+ "khidr": "random",
+ "alexander": "random",
+ "leon": "random",
+ "herodotus": "random",
+ "health_pool": "random",
+ "mana_pool": "random",
+ "attack_pool": "random",
+ "magic_damage_pool": "random",
+ "armor_pool": "random",
+ "equip_pool": "random",
+ "crit_chance_pool": "random",
+ "crit_damage_pool": "random",
+ "allow_default_names": True,
+ "death_link": "random",
+ },
+ # A preset I actually use, using some literal values and some from the option itself.
+ "Limited Potential": {
+ "progression_balancing": "disabled",
+ "fairy_chests_per_zone": 2,
+ "starting_class": "random",
+ "chests_per_zone": 30,
+ "vendors": Vendors.option_normal,
+ "architect": Architect.option_disabled,
+ "gold_gain_multiplier": GoldGainMultiplier.option_half,
+ "number_of_children": 2,
+ "free_diary_on_generation": False,
+ "health_pool": 10,
+ "mana_pool": 10,
+ "attack_pool": 10,
+ "magic_damage_pool": 10,
+ "armor_pool": 5,
+ "equip_pool": 10,
+ "crit_chance_pool": 5,
+ "crit_damage_pool": 5,
+ }
+}
diff --git a/worlds/rogue_legacy/Rules.py b/worlds/rogue_legacy/Rules.py
index 90f6cc08b1fb..2fac8d561399 100644
--- a/worlds/rogue_legacy/Rules.py
+++ b/worlds/rogue_legacy/Rules.py
@@ -7,8 +7,8 @@ def get_upgrade_total(multiworld: MultiWorld, player: int) -> int:
def get_upgrade_count(state: CollectionState, player: int) -> int:
- return state.item_count("Health Up", player) + state.item_count("Mana Up", player) + \
- state.item_count("Attack Up", player) + state.item_count("Magic Damage Up", player)
+ return state.count("Health Up", player) + state.count("Mana Up", player) + \
+ state.count("Attack Up", player) + state.count("Magic Damage Up", player)
def has_vendors(state: CollectionState, player: int) -> bool:
diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py
index 68a0c856c8ad..eb657699540f 100644
--- a/worlds/rogue_legacy/__init__.py
+++ b/worlds/rogue_legacy/__init__.py
@@ -5,6 +5,7 @@
from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table
from .Locations import RLLocation, location_table
from .Options import rl_options
+from .Presets import rl_options_presets
from .Regions import create_regions
from .Rules import set_rules
@@ -22,6 +23,7 @@ class RLWeb(WebWorld):
)]
bug_report_page = "https://github.com/ThePhar/RogueLegacyRandomizer/issues/new?assignees=&labels=bug&template=" \
"report-an-issue---.md&title=%5BIssue%5D"
+ options_presets = rl_options_presets
class RLWorld(World):
@@ -33,7 +35,6 @@ class RLWorld(World):
game = "Rogue Legacy"
option_definitions = rl_options
topology_present = True
- data_version = 4
required_client_version = (0, 3, 5)
web = RLWeb()
diff --git a/worlds/rogue_legacy/docs/en_Rogue Legacy.md b/worlds/rogue_legacy/docs/en_Rogue Legacy.md
index c91dc0de6f7a..dd203c73ac26 100644
--- a/worlds/rogue_legacy/docs/en_Rogue Legacy.md
+++ b/worlds/rogue_legacy/docs/en_Rogue Legacy.md
@@ -1,9 +1,9 @@
# Rogue Legacy (PC)
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains most of the options you need to
-configure and export a config file. Some settings can only be made in YAML, but an explanation can be found in the
+The [player options page for this game](../player-options) contains most of the options you need to
+configure and export a config file. Some options can only be made in YAML, but an explanation can be found in the
[template yaml here](../../../static/generated/configs/Rogue%20Legacy.yaml).
## What does randomization do to this game?
diff --git a/worlds/rogue_legacy/docs/rogue-legacy_en.md b/worlds/rogue_legacy/docs/rogue-legacy_en.md
index e513d0f0ca18..fc9f6920178d 100644
--- a/worlds/rogue_legacy/docs/rogue-legacy_en.md
+++ b/worlds/rogue_legacy/docs/rogue-legacy_en.md
@@ -21,7 +21,7 @@ an experience customized for their taste, and different players in the same mult
### Where do I get a YAML file?
-you can customize your settings by visiting the [Rogue Legacy Settings Page](/games/Rogue%20Legacy/player-settings).
+you can customize your options by visiting the [Rogue Legacy Options Page](/games/Rogue%20Legacy/player-options).
### Connect to the MultiServer
diff --git a/worlds/rogue_legacy/test/TestUnique.py b/worlds/rogue_legacy/test/TestUnique.py
index dbe35dd94fc0..1ae9968d5519 100644
--- a/worlds/rogue_legacy/test/TestUnique.py
+++ b/worlds/rogue_legacy/test/TestUnique.py
@@ -1,8 +1,8 @@
from typing import Dict
from . import RLTestBase
-from worlds.rogue_legacy.Items import RLItemData, item_table
-from worlds.rogue_legacy.Locations import RLLocationData, location_table
+from ..Items import item_table
+from ..Locations import location_table
class UniqueTest(RLTestBase):
diff --git a/worlds/ror2/Items.py b/worlds/ror2/Items.py
deleted file mode 100644
index 448e3272aef8..000000000000
--- a/worlds/ror2/Items.py
+++ /dev/null
@@ -1,194 +0,0 @@
-from BaseClasses import Item
-from .Options import ItemWeights
-from .RoR2Environments import *
-
-
-class RiskOfRainItem(Item):
- game: str = "Risk of Rain 2"
-
-
-# 37000 - 37699, 38000
-item_table: Dict[str, int] = {
- "Dio's Best Friend": 37001,
- "Common Item": 37002,
- "Uncommon Item": 37003,
- "Legendary Item": 37004,
- "Boss Item": 37005,
- "Lunar Item": 37006,
- "Equipment": 37007,
- "Item Scrap, White": 37008,
- "Item Scrap, Green": 37009,
- "Item Scrap, Red": 37010,
- "Item Scrap, Yellow": 37011,
- "Void Item": 37012,
- "Beads of Fealty": 37013
-}
-
-# 37700 - 37699
-##################################################
-# environments
-
-environment_offest = 37700
-
-# add ALL environments into the item table
-environment_offset_table = shift_by_offset(environment_ALL_table, environment_offest)
-item_table.update(shift_by_offset(environment_ALL_table, environment_offest))
-# use the sotv dlc in the item table so that all names can be looked up regardless of use
-
-# end of environments
-##################################################
-
-default_weights: Dict[str, int] = {
- "Item Scrap, Green": 16,
- "Item Scrap, Red": 4,
- "Item Scrap, Yellow": 1,
- "Item Scrap, White": 32,
- "Common Item": 64,
- "Uncommon Item": 32,
- "Legendary Item": 8,
- "Boss Item": 4,
- "Lunar Item": 16,
- "Void Item": 16,
- "Equipment": 32
-}
-
-new_weights: Dict[str, int] = {
- "Item Scrap, Green": 15,
- "Item Scrap, Red": 5,
- "Item Scrap, Yellow": 1,
- "Item Scrap, White": 30,
- "Common Item": 75,
- "Uncommon Item": 40,
- "Legendary Item": 10,
- "Boss Item": 5,
- "Lunar Item": 10,
- "Void Item": 16,
- "Equipment": 20
-}
-
-uncommon_weights: Dict[str, int] = {
- "Item Scrap, Green": 45,
- "Item Scrap, Red": 5,
- "Item Scrap, Yellow": 1,
- "Item Scrap, White": 30,
- "Common Item": 45,
- "Uncommon Item": 100,
- "Legendary Item": 10,
- "Boss Item": 5,
- "Lunar Item": 15,
- "Void Item": 16,
- "Equipment": 20
-}
-
-legendary_weights: Dict[str, int] = {
- "Item Scrap, Green": 15,
- "Item Scrap, Red": 5,
- "Item Scrap, Yellow": 1,
- "Item Scrap, White": 30,
- "Common Item": 50,
- "Uncommon Item": 25,
- "Legendary Item": 100,
- "Boss Item": 5,
- "Lunar Item": 15,
- "Void Item": 16,
- "Equipment": 20
-}
-
-lunartic_weights: Dict[str, int] = {
- "Item Scrap, Green": 0,
- "Item Scrap, Red": 0,
- "Item Scrap, Yellow": 0,
- "Item Scrap, White": 0,
- "Common Item": 0,
- "Uncommon Item": 0,
- "Legendary Item": 0,
- "Boss Item": 0,
- "Lunar Item": 100,
- "Void Item": 0,
- "Equipment": 0
-}
-
-chaos_weights: Dict[str, int] = {
- "Item Scrap, Green": 80,
- "Item Scrap, Red": 45,
- "Item Scrap, Yellow": 30,
- "Item Scrap, White": 100,
- "Common Item": 100,
- "Uncommon Item": 70,
- "Legendary Item": 30,
- "Boss Item": 20,
- "Lunar Item": 60,
- "Void Item": 60,
- "Equipment": 40
-}
-
-no_scraps_weights: Dict[str, int] = {
- "Item Scrap, Green": 0,
- "Item Scrap, Red": 0,
- "Item Scrap, Yellow": 0,
- "Item Scrap, White": 0,
- "Common Item": 100,
- "Uncommon Item": 40,
- "Legendary Item": 15,
- "Boss Item": 5,
- "Lunar Item": 10,
- "Void Item": 16,
- "Equipment": 25
-}
-
-even_weights: Dict[str, int] = {
- "Item Scrap, Green": 1,
- "Item Scrap, Red": 1,
- "Item Scrap, Yellow": 1,
- "Item Scrap, White": 1,
- "Common Item": 1,
- "Uncommon Item": 1,
- "Legendary Item": 1,
- "Boss Item": 1,
- "Lunar Item": 1,
- "Void Item": 1,
- "Equipment": 1
-}
-
-scraps_only: Dict[str, int] = {
- "Item Scrap, Green": 70,
- "Item Scrap, White": 100,
- "Item Scrap, Red": 30,
- "Item Scrap, Yellow": 5,
- "Common Item": 0,
- "Uncommon Item": 0,
- "Legendary Item": 0,
- "Boss Item": 0,
- "Lunar Item": 0,
- "Void Item": 0,
- "Equipment": 0
-}
-
-void_weights: Dict[str, int] = {
- "Item Scrap, Green": 0,
- "Item Scrap, Red": 0,
- "Item Scrap, Yellow": 0,
- "Item Scrap, White": 0,
- "Common Item": 0,
- "Uncommon Item": 0,
- "Legendary Item": 0,
- "Boss Item": 0,
- "Lunar Item": 0,
- "Void Item": 100,
- "Equipment": 0
-}
-
-item_pool_weights: Dict[int, Dict[str, int]] = {
- ItemWeights.option_default: default_weights,
- ItemWeights.option_new: new_weights,
- ItemWeights.option_uncommon: uncommon_weights,
- ItemWeights.option_legendary: legendary_weights,
- ItemWeights.option_lunartic: lunartic_weights,
- ItemWeights.option_chaos: chaos_weights,
- ItemWeights.option_no_scraps: no_scraps_weights,
- ItemWeights.option_even: even_weights,
- ItemWeights.option_scraps_only: scraps_only,
- ItemWeights.option_void: void_weights,
-}
-
-lookup_id_to_name: Dict[int, str] = {id: name for name, id in item_table.items()}
diff --git a/worlds/ror2/Locations.py b/worlds/ror2/Locations.py
deleted file mode 100644
index 7db3ceca73b3..000000000000
--- a/worlds/ror2/Locations.py
+++ /dev/null
@@ -1,119 +0,0 @@
-from typing import Tuple
-from BaseClasses import Location
-from .Options import TotalLocations
-from .Options import ChestsPerEnvironment
-from .Options import ShrinesPerEnvironment
-from .Options import ScavengersPerEnvironment
-from .Options import ScannersPerEnvironment
-from .Options import AltarsPerEnvironment
-from .RoR2Environments import *
-
-
-class RiskOfRainLocation(Location):
- game: str = "Risk of Rain 2"
-
-
-ror2_locations_start_id = 38000
-
-
-def get_classic_item_pickups(n: int) -> Dict[str, int]:
- """Get n ItemPickups, capped at the max value for TotalLocations"""
- n = max(n, 0)
- n = min(n, TotalLocations.range_end)
- return { f"ItemPickup{i+1}": ror2_locations_start_id+i for i in range(n) }
-
-
-item_pickups = get_classic_item_pickups(TotalLocations.range_end)
-location_table = item_pickups
-
-
-def environment_abreviation(long_name:str) -> str:
- """convert long environment names to initials"""
- abrev = ""
- # go through every word finding a letter (or number) for an initial
- for word in long_name.split():
- initial = word[0]
- for letter in word:
- if letter.isalnum():
- initial = letter
- break
- abrev+= initial
- return abrev
-
-# highest numbered orderedstages (this is so we can treat the easily caculate the check ids based on the environment and location "offset")
-highest_orderedstage: int= max(compress_dict_list_horizontal(environment_orderedstages_table).values())
-
-ror2_locations_start_orderedstage = ror2_locations_start_id + TotalLocations.range_end
-
-class orderedstage_location:
- """A class to behave like a struct for storing the offsets of location types in the allocated space per orderedstage environments."""
- # TODO is there a better, more generic way to do this?
- offset_ChestsPerEnvironment = 0
- offset_ShrinesPerEnvironment = offset_ChestsPerEnvironment + ChestsPerEnvironment.range_end
- offset_ScavengersPerEnvironment = offset_ShrinesPerEnvironment + ShrinesPerEnvironment.range_end
- offset_ScannersPerEnvironment = offset_ScavengersPerEnvironment + ScavengersPerEnvironment.range_end
- offset_AltarsPerEnvironment = offset_ScannersPerEnvironment + ScannersPerEnvironment.range_end
-
- # total space allocated to the locations in a single orderedstage environment
- allocation = offset_AltarsPerEnvironment + AltarsPerEnvironment.range_end
-
- def get_environment_locations(chests:int, shrines:int, scavengers:int, scanners:int, altars:int, environment: Tuple[str, int]) -> Dict[str, int]:
- """Get the locations within a specific environment"""
- environment_name = environment[0]
- environment_index = environment[1]
- locations = {}
-
- # due to this mapping, since environment ids are not consecutive, there are lots of "wasted" id numbers
- # TODO perhaps a hashing algorithm could be used to compress this range and save "wasted" ids
- environment_start_id = environment_index * orderedstage_location.allocation + ror2_locations_start_orderedstage
- for n in range(chests):
- locations.update({f"{environment_name}: Chest {n+1}": n + orderedstage_location.offset_ChestsPerEnvironment + environment_start_id})
- for n in range(shrines):
- locations.update({f"{environment_name}: Shrine {n+1}": n + orderedstage_location.offset_ShrinesPerEnvironment + environment_start_id})
- for n in range(scavengers):
- locations.update({f"{environment_name}: Scavenger {n+1}": n + orderedstage_location.offset_ScavengersPerEnvironment + environment_start_id})
- for n in range(scanners):
- locations.update({f"{environment_name}: Radio Scanner {n+1}": n + orderedstage_location.offset_ScannersPerEnvironment + environment_start_id})
- for n in range(altars):
- locations.update({f"{environment_name}: Newt Altar {n+1}": n + orderedstage_location.offset_AltarsPerEnvironment + environment_start_id})
- return locations
-
- def get_locations(chests:int, shrines:int, scavengers:int, scanners:int, altars:int, dlc_sotv:bool) -> Dict[str, int]:
- """Get a dictionary of locations for the ordedstage environments with the locations from the parameters."""
- locations = {}
- orderedstages = compress_dict_list_horizontal(environment_vanilla_orderedstages_table)
- if(dlc_sotv): orderedstages.update(compress_dict_list_horizontal(environment_sotv_orderedstages_table))
- # for every environment, generate the respective locations
- for environment_name, environment_index in orderedstages.items():
- # locations = locations | orderedstage_location.get_environment_locations(
- locations.update(orderedstage_location.get_environment_locations(
- chests=chests,
- shrines=shrines,
- scavengers=scavengers,
- scanners=scanners,
- altars=altars,
- environment=(environment_name, environment_index)
- ))
- return locations
-
- def getall_locations(dlc_sotv:bool=True) -> Dict[str, int]:
- """
- Get all locations in ordered stages.
- Set dlc_sotv to true for the SOTV DLC to be included.
- """
- # to get all locations, attempt using as many locations as possible
- return orderedstage_location.get_locations(
- chests=ChestsPerEnvironment.range_end,
- shrines=ShrinesPerEnvironment.range_end,
- scavengers=ScavengersPerEnvironment.range_end,
- scanners=ScannersPerEnvironment.range_end,
- altars=AltarsPerEnvironment.range_end,
- dlc_sotv=dlc_sotv
- )
-
-
-ror2_location_post_orderedstage = ror2_locations_start_orderedstage + highest_orderedstage*orderedstage_location.allocation
-location_table.update(orderedstage_location.getall_locations())
-# use the sotv dlc in the lookup table so that all ids can be looked up regardless of use
-
-lookup_id_to_name: Dict[int, str] = {id: name for name, id in location_table.items()}
diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py
deleted file mode 100644
index cdd548d33f2f..000000000000
--- a/worlds/ror2/Options.py
+++ /dev/null
@@ -1,312 +0,0 @@
-from typing import Dict
-from Options import Option, Toggle, DefaultOnToggle, DeathLink, Range, Choice
-
-
-# NOTE be aware that since the range of item ids that RoR2 uses is based off of the maximums of checks
-# Be careful when changing the range_end values not to go into another game's IDs
-# NOTE that these changes to range_end must also be reflected in the RoR2 client so it understands the same ids.
-
-class Goal(Choice):
- """
- Classic Mode: Every Item pickup increases fills a progress bar which gives location checks.
-
- Explore Mode: Each environment will have location checks within each environment.
- environments will be locked in the item pool until received.
- """
- display_name = "Game Mode"
- option_classic = 0
- option_explore = 1
- default = 0
-
-
-class TotalLocations(Range):
- """Classic Mode: Number of location checks which are added to the Risk of Rain playthrough."""
- display_name = "Total Locations"
- range_start = 40
- range_end = 250
- default = 40
-
-
-class ChestsPerEnvironment(Range):
- """Explore Mode: The number of chest locations per environment."""
- display_name = "Chests per Environment"
- range_start = 2
- range_end = 20
- default = 10
-
-
-class ShrinesPerEnvironment(Range):
- """Explore Mode: The number of shrine locations per environment."""
- display_name = "Shrines per Environment"
- range_start = 2
- range_end = 20
- default = 5
-
-
-class ScavengersPerEnvironment(Range):
- """Explore Mode: The number of scavenger locations per environment."""
- display_name = "Scavenger per Environment"
- range_start = 0
- range_end = 1
- default = 1
-
-class ScannersPerEnvironment(Range):
- """Explore Mode: The number of scanners locations per environment."""
- display_name = "Radio Scanners per Environment"
- range_start = 0
- range_end = 1
- default = 1
-
-class AltarsPerEnvironment(Range):
- """Explore Mode: The number of altars locations per environment."""
- display_name = "Newts Per Environment"
- range_start = 0
- range_end = 2
- default = 1
-
-class TotalRevivals(Range):
- """Total Percentage of `Dio's Best Friend` item put in the item pool."""
- display_name = "Total Revives as percentage"
- range_start = 0
- range_end = 10
- default = 4
-
-
-class ItemPickupStep(Range):
- """
- Number of items to pick up before an AP Check is completed.
- Setting to 1 means every other pickup.
- Setting to 2 means every third pickup. So on...
- """
- display_name = "Item Pickup Step"
- range_start = 0
- range_end = 5
- default = 1
-
-class ShrineUseStep(Range):
- """
- Explore Mode:
- Number of shrines to use up before an AP Check is completed.
- Setting to 1 means every other pickup.
- Setting to 2 means every third pickup. So on...
- """
- display_name = "Shrine use Step"
- range_start = 0
- range_end = 3
- default = 0
-
-
-class AllowLunarItems(DefaultOnToggle):
- """Allows Lunar items in the item pool."""
- display_name = "Enable Lunar Item Shuffling"
-
-
-class StartWithRevive(DefaultOnToggle):
- """Start the game with a `Dio's Best Friend` item."""
- display_name = "Start with a Revive"
-
-
-class FinalStageDeath(Toggle):
- """The following will count as a win if set to true:
- Dying in Commencement.
- Dying in The Planetarium.
- Obliterating yourself"""
- display_name = "Final Stage Death is Win"
-
-
-class BeginWithLoop(Toggle):
- """
- Enable to precollect a full loop of environments.
- Only has an effect with Explore Mode.
- """
- display_name = "Begin With Loop"
-
-
-class DLC_SOTV(Toggle):
- """
- Enable if you are using SOTV DLC.
- Affects environment availability for Explore Mode.
- Adds Void Items into the item pool
- """
- display_name = "Enable DLC - SOTV"
-
-
-
-class GreenScrap(Range):
- """Weight of Green Scraps in the item pool.
-
- (Ignored unless Item Weight Presets is 'No')"""
- display_name = "Green Scraps"
- range_start = 0
- range_end = 100
- default = 16
-
-
-class RedScrap(Range):
- """Weight of Red Scraps in the item pool.
-
- (Ignored unless Item Weight Presets is 'No')"""
- display_name = "Red Scraps"
- range_start = 0
- range_end = 100
- default = 4
-
-
-class YellowScrap(Range):
- """Weight of yellow scraps in the item pool.
-
- (Ignored unless Item Weight Presets is 'No')"""
- display_name = "Yellow Scraps"
- range_start = 0
- range_end = 100
- default = 1
-
-
-class WhiteScrap(Range):
- """Weight of white scraps in the item pool.
-
- (Ignored unless Item Weight Presets is 'No')"""
- display_name = "White Scraps"
- range_start = 0
- range_end = 100
- default = 32
-
-
-class CommonItem(Range):
- """Weight of common items in the item pool.
-
- (Ignored unless Item Weight Presets is 'No')"""
- display_name = "Common Items"
- range_start = 0
- range_end = 100
- default = 64
-
-
-class UncommonItem(Range):
- """Weight of uncommon items in the item pool.
-
- (Ignored unless Item Weight Presets is 'No')"""
- display_name = "Uncommon Items"
- range_start = 0
- range_end = 100
- default = 32
-
-
-class LegendaryItem(Range):
- """Weight of legendary items in the item pool.
-
- (Ignored unless Item Weight Presets is 'No')"""
- display_name = "Legendary Items"
- range_start = 0
- range_end = 100
- default = 8
-
-
-class BossItem(Range):
- """Weight of boss items in the item pool.
-
- (Ignored unless Item Weight Presets is 'No')"""
- display_name = "Boss Items"
- range_start = 0
- range_end = 100
- default = 4
-
-
-class LunarItem(Range):
- """Weight of lunar items in the item pool.
-
- (Ignored unless Item Weight Presets is 'No')"""
- display_name = "Lunar Items"
- range_start = 0
- range_end = 100
- default = 16
-
-
-class VoidItem(Range):
- """Weight of void items in the item pool.
-
- (Ignored unless Item Weight Presets is 'No')
-
- (Ignored if Enable DLC - SOTV is 'No') """
- display_name = "Void Items"
- range_start = 0
- range_end = 100
- default = 16
-
-
-class Equipment(Range):
- """Weight of equipment items in the item pool.
-
- (Ignored unless Item Weight Presets is 'No')"""
- display_name = "Equipment"
- range_start = 0
- range_end = 100
- default = 32
-
-
-class ItemPoolPresetToggle(Toggle):
- """Will use the item weight presets when set to true, otherwise will use the custom set item pool weights."""
- display_name = "Use Item Weight Presets"
-
-
-class ItemWeights(Choice):
- """Set item_pool_presets to true if you want to use one of these presets.
- Preset choices for determining the weights of the item pool.
- - New is a test for a potential adjustment to the default weights.
- - Uncommon puts a large number of uncommon items in the pool.
- - Legendary puts a large number of legendary items in the pool.
- - Lunartic makes everything a lunar item.
- - Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy.
- - No Scraps removes all scrap items from the item pool.
- - Even generates the item pool with every item having an even weight.
- - Scraps Only will be only scrap items in the item pool.
- - Void makes everything a void item."""
- display_name = "Item Weights"
- option_default = 0
- option_new = 1
- option_uncommon = 2
- option_legendary = 3
- option_lunartic = 4
- option_chaos = 5
- option_no_scraps = 6
- option_even = 7
- option_scraps_only = 8
- option_void = 9
-
-
-# define a dictionary for the weights of the generated item pool.
-ror2_weights: Dict[str, type(Option)] = {
- "green_scrap": GreenScrap,
- "red_scrap": RedScrap,
- "yellow_scrap": YellowScrap,
- "white_scrap": WhiteScrap,
- "common_item": CommonItem,
- "uncommon_item": UncommonItem,
- "legendary_item": LegendaryItem,
- "boss_item": BossItem,
- "lunar_item": LunarItem,
- "void_item": VoidItem,
- "equipment": Equipment
-}
-
-ror2_options: Dict[str, type(Option)] = {
- "goal": Goal,
- "total_locations": TotalLocations,
- "chests_per_stage": ChestsPerEnvironment,
- "shrines_per_stage": ShrinesPerEnvironment,
- "scavengers_per_stage": ScavengersPerEnvironment,
- "scanner_per_stage": ScannersPerEnvironment,
- "altars_per_stage": AltarsPerEnvironment,
- "total_revivals": TotalRevivals,
- "start_with_revive": StartWithRevive,
- "final_stage_death": FinalStageDeath,
- "begin_with_loop": BeginWithLoop,
- "dlc_sotv": DLC_SOTV,
- "death_link": DeathLink,
- "item_pickup_step": ItemPickupStep,
- "shrine_use_step": ShrineUseStep,
- "enable_lunar": AllowLunarItems,
- "item_weights": ItemWeights,
- "item_pool_presets": ItemPoolPresetToggle,
- **ror2_weights
-}
diff --git a/worlds/ror2/Regions.py b/worlds/ror2/Regions.py
deleted file mode 100644
index 94f5aaf71ee8..000000000000
--- a/worlds/ror2/Regions.py
+++ /dev/null
@@ -1,125 +0,0 @@
-from typing import Dict, List, NamedTuple, Optional
-
-from BaseClasses import MultiWorld, Region, Entrance
-from .Locations import location_table, RiskOfRainLocation
-
-
-class RoRRegionData(NamedTuple):
- locations: Optional[List[str]]
- region_exits: Optional[List[str]]
-
-
-def create_regions(multiworld: MultiWorld, player: int):
- # Default Locations
- non_dlc_regions: Dict[str, RoRRegionData] = {
- "Menu": RoRRegionData(None, ["Distant Roost", "Distant Roost (2)", "Titanic Plains", "Titanic Plains (2)"]),
- "Distant Roost": RoRRegionData([], ["OrderedStage_1"]),
- "Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]),
- "Titanic Plains": RoRRegionData([], ["OrderedStage_1"]),
- "Titanic Plains (2)": RoRRegionData([], ["OrderedStage_1"]),
- "Abandoned Aqueduct": RoRRegionData([], ["OrderedStage_2"]),
- "Wetland Aspect": RoRRegionData([], ["OrderedStage_2"]),
- "Rallypoint Delta": RoRRegionData([], ["OrderedStage_3"]),
- "Scorched Acres": RoRRegionData([], ["OrderedStage_3"]),
- "Abyssal Depths": RoRRegionData([], ["OrderedStage_4"]),
- "Siren's Call": RoRRegionData([], ["OrderedStage_4"]),
- "Sundered Grove": RoRRegionData([], ["OrderedStage_4"]),
- "Sky Meadow": RoRRegionData([], ["Hidden Realm: Bulwark's Ambry", "OrderedStage_5"]),
- }
- # SOTV Regions
- dlc_regions: Dict[str, RoRRegionData] = {
- "Siphoned Forest": RoRRegionData([], ["OrderedStage_1"]),
- "Aphelian Sanctuary": RoRRegionData([], ["OrderedStage_2"]),
- "Sulfur Pools": RoRRegionData([], ["OrderedStage_3"])
- }
- other_regions: Dict[str, RoRRegionData] = {
- "Commencement": RoRRegionData(None, ["Victory", "Petrichor V"]),
- "OrderedStage_5": RoRRegionData(None, ["Hidden Realm: A Moment, Fractured", "Commencement"]),
- "OrderedStage_1": RoRRegionData(None, ["Hidden Realm: Bazaar Between Time",
- "Hidden Realm: Gilded Coast", "Abandoned Aqueduct", "Wetland Aspect"]),
- "OrderedStage_2": RoRRegionData(None, ["Rallypoint Delta", "Scorched Acres"]),
- "OrderedStage_3": RoRRegionData(None, ["Abyssal Depths", "Siren's Call", "Sundered Grove"]),
- "OrderedStage_4": RoRRegionData(None, ["Sky Meadow"]),
- "Hidden Realm: A Moment, Fractured": RoRRegionData(None, ["Hidden Realm: A Moment, Whole"]),
- "Hidden Realm: A Moment, Whole": RoRRegionData(None, ["Victory"]),
- "Void Fields": RoRRegionData(None, []),
- "Victory": RoRRegionData(None, None),
- "Petrichor V": RoRRegionData(None, ["Victory"]),
- "Hidden Realm: Bulwark's Ambry": RoRRegionData(None, None),
- "Hidden Realm: Bazaar Between Time": RoRRegionData(None, ["Void Fields"]),
- "Hidden Realm: Gilded Coast": RoRRegionData(None, None)
- }
- dlc_other_regions: Dict[str, RoRRegionData] = {
- "The Planetarium": RoRRegionData(None, ["Victory"]),
- "Void Locus": RoRRegionData(None, ["The Planetarium"])
- }
- # Totals of each item
- chests = int(multiworld.chests_per_stage[player])
- shrines = int(multiworld.shrines_per_stage[player])
- scavengers = int(multiworld.scavengers_per_stage[player])
- scanners = int(multiworld.scanner_per_stage[player])
- newt = int(multiworld.altars_per_stage[player])
- all_location_regions = {**non_dlc_regions}
- if multiworld.dlc_sotv[player]:
- all_location_regions = {**non_dlc_regions, **dlc_regions}
-
- # Locations
- for key in all_location_regions:
- if key == "Menu":
- continue
- # Chests
- for i in range(0, chests):
- all_location_regions[key].locations.append(f"{key}: Chest {i + 1}")
- # Shrines
- for i in range(0, shrines):
- all_location_regions[key].locations.append(f"{key}: Shrine {i + 1}")
- # Scavengers
- if scavengers > 0:
- for i in range(0, scavengers):
- all_location_regions[key].locations.append(f"{key}: Scavenger {i + 1}")
- # Radio Scanners
- if scanners > 0:
- for i in range(0, scanners):
- all_location_regions[key].locations.append(f"{key}: Radio Scanner {i + 1}")
- # Newt Altars
- if newt > 0:
- for i in range(0, newt):
- all_location_regions[key].locations.append(f"{key}: Newt Altar {i + 1}")
- regions_pool: Dict = {**all_location_regions, **other_regions}
-
- # DLC Locations
- if multiworld.dlc_sotv[player]:
- non_dlc_regions["Menu"].region_exits.append("Siphoned Forest")
- other_regions["OrderedStage_1"].region_exits.append("Aphelian Sanctuary")
- other_regions["OrderedStage_2"].region_exits.append("Sulfur Pools")
- other_regions["Void Fields"].region_exits.append("Void Locus")
- regions_pool: Dict = {**all_location_regions, **other_regions, **dlc_other_regions}
-
- # Create all the regions
- for name, data in regions_pool.items():
- multiworld.regions.append(create_region(multiworld, player, name, data))
-
- # Connect all the regions to their exits
- for name, data in regions_pool.items():
- create_connections_in_regions(multiworld, player, name, data)
-
-
-def create_region(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData):
- region = Region(name, player, multiworld)
- if data.locations:
- for location_name in data.locations:
- location_data = location_table.get(location_name)
- location = RiskOfRainLocation(player, location_name, location_data, region)
- region.locations.append(location)
-
- return region
-
-
-def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData):
- region = multiworld.get_region(name, player)
- if data.region_exits:
- for region_exit in data.region_exits:
- r_exit_stage = Entrance(player, region_exit, region)
- exit_region = multiworld.get_region(region_exit, player)
- r_exit_stage.connect(exit_region)
- region.exits.append(r_exit_stage)
diff --git a/worlds/ror2/RoR2Environments.py b/worlds/ror2/RoR2Environments.py
deleted file mode 100644
index 2a9bf73e9805..000000000000
--- a/worlds/ror2/RoR2Environments.py
+++ /dev/null
@@ -1,118 +0,0 @@
-from typing import Dict, List, TypeVar
-
-# TODO probably move to Locations
-
-environment_vanilla_orderedstage_1_table: Dict[str, int] = {
- "Distant Roost": 7, # blackbeach
- "Distant Roost (2)": 8, # blackbeach2
- "Titanic Plains": 15, # golemplains
- "Titanic Plains (2)": 16, # golemplains2
-}
-environment_vanilla_orderedstage_2_table: Dict[str, int] = {
- "Abandoned Aqueduct": 17, # goolake
- "Wetland Aspect": 12, # foggyswamp
-}
-environment_vanilla_orderedstage_3_table: Dict[str, int] = {
- "Rallypoint Delta": 13, # frozenwall
- "Scorched Acres": 47, # wispgraveyard
-}
-environment_vanilla_orderedstage_4_table: Dict[str, int] = {
- "Abyssal Depths": 10, # dampcavesimple
- "Siren's Call": 37, # shipgraveyard
- "Sundered Grove": 35, # rootjungle
-}
-environment_vanilla_orderedstage_5_table: Dict[str, int] = {
- "Sky Meadow": 38, # skymeadow
-}
-
-environment_vanilla_hidden_realm_table: Dict[str, int] = {
- "Hidden Realm: Bulwark's Ambry": 5, # artifactworld
- "Hidden Realm: Bazaar Between Time": 6, # bazaar
- "Hidden Realm: Gilded Coast": 14, # goldshores
- "Hidden Realm: A Moment, Whole": 27, # limbo
- "Hidden Realm: A Moment, Fractured": 33, # mysteryspace
-}
-
-environment_vanilla_special_table: Dict[str, int] = {
- "Void Fields": 4, # arena
- "Commencement": 32, # moon2
-}
-
-environment_sotv_orderedstage_1_table: Dict[str, int] = {
- "Siphoned Forest": 39, # snowyforest
-}
-environment_sotv_orderedstage_2_table: Dict[str, int] = {
- "Aphelian Sanctuary": 3, # ancientloft
-}
-environment_sotv_orderedstage_3_table: Dict[str, int] = {
- "Sulfur Pools": 41, # sulfurpools
-}
-environment_sotv_orderedstage_4_table: Dict[str, int] = { }
-environment_sotv_orderedstage_5_table: Dict[str, int] = { }
-
-# TODO idk much and idc much about simulacrum, is there a forced order or something?
-environment_sotv_simulacrum_table: Dict[str, int] = {
- "The Simulacrum (Aphelian Sanctuary)": 20, # itancientloft
- "The Simulacrum (Abyssal Depths)": 21, # itdampcave
- "The Simulacrum (Rallypoint Delta)": 22, # itfrozenwall
- "The Simulacrum (Titanic Plains)": 23, # itgolemplains
- "The Simulacrum (Abandoned Aqueduct)": 24, # itgoolake
- "The Simulacrum (Commencement)": 25, # itmoon
- "The Simulacrum (Sky Meadow)": 26, # itskymeadow
-}
-
-environment_sotv_special_table: Dict[str, int] = {
- "Void Locus": 46, # voidstage
- "The Planetarium": 45, # voidraid
-}
-
-X = TypeVar("X")
-Y = TypeVar("Y")
-
-
-def compress_dict_list_horizontal(list_of_dict: List[Dict[X, Y]]) -> Dict[X, Y]:
- """Combine all dictionaries in a list together into one dictionary."""
- compressed: Dict[X,Y] = {}
- for individual in list_of_dict: compressed.update(individual)
- return compressed
-
-def collapse_dict_list_vertical(list_of_dict1: List[Dict[X, Y]], *args: List[Dict[X, Y]]) -> List[Dict[X, Y]]:
- """Combine all parallel dictionaries in lists together to make a new list of dictionaries of the same length."""
- # find the length of the longest list
- length = len(list_of_dict1)
- for list_of_dictN in args:
- length = max(length, len(list_of_dictN))
-
- # create a combined list with a length the same as the longest list
- collapsed = [{}] * (length)
- # The reason the list_of_dict1 is not directly used to make collapsed is
- # side effects can occur if all the dictionaries are not manually unioned.
-
- # merge contents from list_of_dict1
- for i in range(len(list_of_dict1)):
- collapsed[i] = {**collapsed[i], **list_of_dict1[i]}
-
- # merge contents of remaining lists_of_dicts
- for list_of_dictN in args:
- for i in range(len(list_of_dictN)):
- collapsed[i] = {**collapsed[i], **list_of_dictN[i]}
-
- return collapsed
-
-# TODO potentially these should only be created when they are directly referenced (unsure of the space/time cost of creating these initially)
-
-environment_vanilla_orderedstages_table = [ environment_vanilla_orderedstage_1_table, environment_vanilla_orderedstage_2_table, environment_vanilla_orderedstage_3_table, environment_vanilla_orderedstage_4_table, environment_vanilla_orderedstage_5_table ]
-environment_vanilla_table = {**compress_dict_list_horizontal(environment_vanilla_orderedstages_table), **environment_vanilla_hidden_realm_table, **environment_vanilla_special_table}
-
-environment_sotv_orderedstages_table = [ environment_sotv_orderedstage_1_table, environment_sotv_orderedstage_2_table, environment_sotv_orderedstage_3_table, environment_sotv_orderedstage_4_table, environment_sotv_orderedstage_5_table ]
-environment_sotv_non_simulacrum_table = {**compress_dict_list_horizontal(environment_sotv_orderedstages_table), **environment_sotv_special_table}
-environment_sotv_table = {**environment_sotv_non_simulacrum_table}
-
-environment_non_orderedstages_table = {**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table, **environment_sotv_simulacrum_table, **environment_sotv_special_table}
-environment_orderedstages_table = collapse_dict_list_vertical(environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table)
-environment_ALL_table = {**environment_vanilla_table, **environment_sotv_table}
-
-
-def shift_by_offset(dictionary: Dict[str, int], offset:int) -> Dict[str, int]:
- """Shift all indexes in a dictionary by an offset"""
- return {name:index+offset for name, index in dictionary.items()}
diff --git a/worlds/ror2/Rules.py b/worlds/ror2/Rules.py
deleted file mode 100644
index 7d94177417ea..000000000000
--- a/worlds/ror2/Rules.py
+++ /dev/null
@@ -1,155 +0,0 @@
-from BaseClasses import MultiWorld, CollectionState
-from worlds.generic.Rules import set_rule, add_rule
-from .Locations import orderedstage_location
-from .RoR2Environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table, \
- environment_orderedstages_table
-
-
-# Rule to see if it has access to the previous stage
-def has_entrance_access_rule(multiworld: MultiWorld, stage: str, entrance: str, player: int):
- multiworld.get_entrance(entrance, player).access_rule = \
- lambda state: state.has(entrance, player) and state.has(stage, player)
-
-
-# Checks to see if chest/shrine are accessible
-def has_location_access_rule(multiworld: MultiWorld, environment: str, player: int, item_number: int, item_type: str):
- if item_number == 1:
- multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \
- lambda state: state.has(environment, player)
- if item_type == "Scavenger":
- multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \
- lambda state: state.has(environment, player) and state.has("Stage_4", player)
- else:
- multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \
- lambda state: check_location(state, environment, player, item_number, item_type)
-
-
-def check_location(state, environment: str, player: int, item_number: int, item_name: str):
- return state.can_reach(f"{environment}: {item_name} {item_number - 1}", "Location", player)
-
-
-# unlock event to next set of stages
-def get_stage_event(multiworld: MultiWorld, player: int, stage_number: int):
- if not multiworld.dlc_sotv[player]:
- environment_name = multiworld.random.choices(list(environment_vanilla_orderedstages_table[stage_number].keys()),
- k=1)
- else:
- environment_name = multiworld.random.choices(list(environment_orderedstages_table[stage_number].keys()), k=1)
- multiworld.get_location(f"Stage_{stage_number + 1}", player).access_rule = \
- lambda state: get_one_of_the_stages(state, environment_name[0], player)
-
-
-def get_one_of_the_stages(state: CollectionState, stage: str, player: int):
- return state.has(stage, player)
-
-
-def set_rules(multiworld: MultiWorld, player: int) -> None:
- if multiworld.goal[player] == "classic":
- # classic mode
- total_locations = multiworld.total_locations[player].value # total locations for current player
- else:
- # explore mode
- total_locations = len(
- orderedstage_location.get_locations(
- chests=multiworld.chests_per_stage[player].value,
- shrines=multiworld.shrines_per_stage[player].value,
- scavengers=multiworld.scavengers_per_stage[player].value,
- scanners=multiworld.scanner_per_stage[player].value,
- altars=multiworld.altars_per_stage[player].value,
- dlc_sotv=multiworld.dlc_sotv[player].value
- )
- )
-
- event_location_step = 25 # set an event location at these locations for "spheres"
- divisions = total_locations // event_location_step
- total_revivals = multiworld.worlds[player].total_revivals # pulling this info we calculated in generate_basic
-
- if multiworld.goal[player] == "classic":
- # classic mode
- if divisions:
- for i in range(1, divisions + 1): # since divisions is the floor of total_locations / 25
- if i * event_location_step != total_locations:
- event_loc = multiworld.get_location(f"Pickup{i * event_location_step}", player)
- set_rule(event_loc,
- lambda state, i=i: state.can_reach(f"ItemPickup{i * event_location_step - 1}", "Location", player))
- # we want to create a rule for each of the 25 locations per division
- for n in range(i * event_location_step, (i + 1) * event_location_step + 1):
- if n > total_locations:
- break
- if n == i * event_location_step:
- set_rule(multiworld.get_location(f"ItemPickup{n}", player),
- lambda state, event_item=event_loc.item.name: state.has(event_item, player))
- else:
- set_rule(multiworld.get_location(f"ItemPickup{n}", player),
- lambda state, n=n: state.can_reach(f"ItemPickup{n - 1}", "Location", player))
- set_rule(multiworld.get_location("Victory", player),
- lambda state: state.can_reach(f"ItemPickup{total_locations}", "Location", player))
- if total_revivals or multiworld.start_with_revive[player].value:
- add_rule(multiworld.get_location("Victory", player),
- lambda state: state.has("Dio's Best Friend", player,
- total_revivals + multiworld.start_with_revive[player]))
-
- elif multiworld.goal[player] == "explore":
- # When explore_mode is used,
- # scavengers need to be locked till after a full loop since that is when they are capable of spawning.
- # (While technically the requirement is just beating 5 stages, this will ensure that the player will have
- # a long enough run to have enough director credits for scavengers and
- # help prevent being stuck in the same stages until that point.)
-
- for location in multiworld.get_locations():
- if location.player != player: continue # ignore all checks that don't belong to this player
- if "Scavenger" in location.name:
- add_rule(location, lambda state: state.has("Stage_5", player))
- # Regions
- chests = multiworld.chests_per_stage[player]
- shrines = multiworld.shrines_per_stage[player]
- newts = multiworld.altars_per_stage[player]
- scavengers = multiworld.scavengers_per_stage[player]
- scanners = multiworld.scanner_per_stage[player]
- for i in range(len(environment_vanilla_orderedstages_table)):
- for environment_name, _ in environment_vanilla_orderedstages_table[i].items():
- # Make sure to go through each location
- if scavengers == 1:
- has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger")
- if scanners == 1:
- has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner")
- for chest in range(1, chests + 1):
- has_location_access_rule(multiworld, environment_name, player, chest, "Chest")
- for shrine in range(1, shrines + 1):
- has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine")
- if newts > 0:
- for newt in range(1, newts + 1):
- has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar")
- if i > 0:
- has_entrance_access_rule(multiworld, f"Stage_{i}", environment_name, player)
- get_stage_event(multiworld, player, i)
-
- if multiworld.dlc_sotv[player]:
- for i in range(len(environment_sotv_orderedstages_table)):
- for environment_name, _ in environment_sotv_orderedstages_table[i].items():
- # Make sure to go through each location
- if scavengers == 1:
- has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger")
- if scanners == 1:
- has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner")
- for chest in range(1, chests + 1):
- has_location_access_rule(multiworld, environment_name, player, chest, "Chest")
- for shrine in range(1, shrines + 1):
- has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine")
- if newts > 0:
- for newt in range(1, newts + 1):
- has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar")
- if i > 0:
- has_entrance_access_rule(multiworld, f"Stage_{i}", environment_name, player)
- has_entrance_access_rule(multiworld, f"Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole",
- player)
- has_entrance_access_rule(multiworld, f"Stage_1", "Hidden Realm: Bazaar Between Time", player)
- has_entrance_access_rule(multiworld, f"Hidden Realm: Bazaar Between Time", "Void Fields", player)
- has_entrance_access_rule(multiworld, f"Stage_5", "Commencement", player)
- has_entrance_access_rule(multiworld, f"Stage_5", "Hidden Realm: A Moment, Fractured", player)
- has_entrance_access_rule(multiworld, "Beads of Fealty", "Hidden Realm: A Moment, Whole", player)
- if multiworld.dlc_sotv[player]:
- has_entrance_access_rule(multiworld, f"Stage_5", "Void Locus", player)
- has_entrance_access_rule(multiworld, f"Void Locus", "The Planetarium", player)
- # Win Condition
- multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py
index 7c638f50b34d..7873ae54bbba 100644
--- a/worlds/ror2/__init__.py
+++ b/worlds/ror2/__init__.py
@@ -1,14 +1,16 @@
import string
-from .Items import RiskOfRainItem, item_table, item_pool_weights, environment_offest
-from .Locations import RiskOfRainLocation, get_classic_item_pickups, item_pickups, orderedstage_location
-from .Rules import set_rules
-from .RoR2Environments import *
-
-from BaseClasses import Region, Entrance, Item, ItemClassification, MultiWorld, Tutorial
-from .Options import ror2_options, ItemWeights
+from .items import RiskOfRainItem, item_table, item_pool_weights, offset, filler_table, environment_offset
+from .locations import RiskOfRainLocation, item_pickups, get_locations
+from .rules import set_rules
+from .ror2environments import environment_vanilla_table, environment_vanilla_orderedstages_table, \
+ environment_sotv_orderedstages_table, environment_sotv_table, collapse_dict_list_vertical, shift_by_offset
+
+from BaseClasses import Item, ItemClassification, Tutorial
+from .options import ItemWeights, ROR2Options, ror2_option_groups
from worlds.AutoWorld import World, WebWorld
-from .Regions import create_regions
+from .regions import create_explore_regions, create_classic_regions
+from typing import List, Dict, Any
class RiskOfWeb(WebWorld):
@@ -18,9 +20,11 @@ class RiskOfWeb(WebWorld):
"English",
"setup_en.md",
"setup/en",
- ["Ijwu"]
+ ["Ijwu", "Kindasneaki"]
)]
+ option_groups = ror2_option_groups
+
class RiskOfRainWorld(World):
"""
@@ -28,249 +32,231 @@ class RiskOfRainWorld(World):
Combine loot in surprising ways and master each character until you become the havoc you feared upon your
first crash landing.
"""
- game: str = "Risk of Rain 2"
- option_definitions = ror2_options
+ game = "Risk of Rain 2"
+ options_dataclass = ROR2Options
+ options: ROR2Options
topology_present = False
-
- item_name_to_id = item_table
+ item_name_to_id = {name: data.code for name, data in item_table.items()}
+ item_name_groups = {
+ "Stages": {name for name, data in item_table.items() if data.category == "Stage"},
+ "Environments": {name for name, data in item_table.items() if data.category == "Environment"},
+ "Upgrades": {name for name, data in item_table.items() if data.category == "Upgrade"},
+ "Fillers": {name for name, data in item_table.items() if data.category == "Filler"},
+ "Traps": {name for name, data in item_table.items() if data.category == "Trap"},
+ }
location_name_to_id = item_pickups
- data_version = 7
- required_client_version = (0, 4, 2)
+ required_client_version = (0, 5, 0)
web = RiskOfWeb()
total_revivals: int
- def __init__(self, multiworld: "MultiWorld", player: int):
- super().__init__(multiworld, player)
- self.junk_pool: Dict[str, int] = {}
-
def generate_early(self) -> None:
# figure out how many revivals should exist in the pool
- if self.multiworld.goal[self.player] == "classic":
- total_locations = self.multiworld.total_locations[self.player].value
+ if self.options.goal == "classic":
+ total_locations = self.options.total_locations.value
else:
total_locations = len(
- orderedstage_location.get_locations(
- chests=self.multiworld.chests_per_stage[self.player].value,
- shrines=self.multiworld.shrines_per_stage[self.player].value,
- scavengers=self.multiworld.scavengers_per_stage[self.player].value,
- scanners=self.multiworld.scanner_per_stage[self.player].value,
- altars=self.multiworld.altars_per_stage[self.player].value,
- dlc_sotv=self.multiworld.dlc_sotv[self.player].value
+ get_locations(
+ chests=self.options.chests_per_stage.value,
+ shrines=self.options.shrines_per_stage.value,
+ scavengers=self.options.scavengers_per_stage.value,
+ scanners=self.options.scanner_per_stage.value,
+ altars=self.options.altars_per_stage.value,
+ dlc_sotv=bool(self.options.dlc_sotv.value)
)
)
- self.total_revivals = int(self.multiworld.total_revivals[self.player].value / 100 *
+ self.total_revivals = int(self.options.total_revivals.value / 100 *
total_locations)
- # self.total_revivals = self.multiworld.total_revivals[self.player].value
- if self.multiworld.start_with_revive[self.player].value:
+ if self.options.start_with_revive:
self.total_revivals -= 1
+ if self.options.victory == "voidling" and not self.options.dlc_sotv:
+ self.options.victory.value = self.options.victory.option_any
+
+ def create_regions(self) -> None:
+
+ if self.options.goal == "classic":
+ # classic mode
+ create_classic_regions(self)
+ else:
+ # explore mode
+ create_explore_regions(self)
+
+ self.create_events()
def create_items(self) -> None:
# shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend
- if self.multiworld.start_with_revive[self.player]:
+ if self.options.start_with_revive:
self.multiworld.push_precollected(self.multiworld.create_item("Dio's Best Friend", self.player))
environments_pool = {}
# only mess with the environments if they are set as items
- if self.multiworld.goal[self.player] == "explore":
+ if self.options.goal == "explore":
+
+ # check to see if the user doesn't want to use stages, and to figure out what type of stages are being used.
+ if not self.options.require_stages:
+ if not self.options.progressive_stages:
+ self.multiworld.push_precollected(self.multiworld.create_item("Stage 1", self.player))
+ self.multiworld.push_precollected(self.multiworld.create_item("Stage 2", self.player))
+ self.multiworld.push_precollected(self.multiworld.create_item("Stage 3", self.player))
+ self.multiworld.push_precollected(self.multiworld.create_item("Stage 4", self.player))
+ else:
+ for _ in range(4):
+ self.multiworld.push_precollected(self.multiworld.create_item("Progressive Stage", self.player))
# figure out all available ordered stages for each tier
environment_available_orderedstages_table = environment_vanilla_orderedstages_table
- if self.multiworld.dlc_sotv[self.player]:
- environment_available_orderedstages_table = collapse_dict_list_vertical(environment_available_orderedstages_table, environment_sotv_orderedstages_table)
+ if self.options.dlc_sotv:
+ environment_available_orderedstages_table = \
+ collapse_dict_list_vertical(environment_available_orderedstages_table,
+ environment_sotv_orderedstages_table)
- environments_pool = shift_by_offset(environment_vanilla_table, environment_offest)
+ environments_pool = shift_by_offset(environment_vanilla_table, environment_offset)
- if self.multiworld.dlc_sotv[self.player]:
- environment_offset_table = shift_by_offset(environment_sotv_table, environment_offest)
+ if self.options.dlc_sotv:
+ environment_offset_table = shift_by_offset(environment_sotv_table, environment_offset)
environments_pool = {**environments_pool, **environment_offset_table}
- environments_to_precollect = 5 if self.multiworld.begin_with_loop[self.player].value else 1
- # percollect environments for each stage (or just stage 1)
- for i in range(environments_to_precollect):
- unlock = self.multiworld.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1)
- self.multiworld.push_precollected(self.create_item(unlock[0]))
- environments_pool.pop(unlock[0])
+ # percollect starting environment for stage 1
+ unlock = self.random.choices(list(environment_available_orderedstages_table[0].keys()), k=1)
+ self.multiworld.push_precollected(self.create_item(unlock[0]))
+ environments_pool.pop(unlock[0])
# Generate item pool
- itempool: List = []
+ itempool: List[str] = ["Beads of Fealty", "Radar Scanner"]
# Add revive items for the player
itempool += ["Dio's Best Friend"] * self.total_revivals
- itempool += ["Beads of Fealty"]
for env_name, _ in environments_pool.items():
itempool += [env_name]
- if self.multiworld.goal[self.player] == "classic":
+ if self.options.goal == "classic":
# classic mode
- total_locations = self.multiworld.total_locations[self.player].value
+ total_locations = self.options.total_locations.value
else:
# explore mode
+
+ # Add Stage items to the pool
+ if self.options.require_stages:
+ itempool += ["Stage 1", "Stage 2", "Stage 3", "Stage 4"] if not self.options.progressive_stages else \
+ ["Progressive Stage"] * 4
+
total_locations = len(
- orderedstage_location.get_locations(
- chests=self.multiworld.chests_per_stage[self.player].value,
- shrines=self.multiworld.shrines_per_stage[self.player].value,
- scavengers=self.multiworld.scavengers_per_stage[self.player].value,
- scanners=self.multiworld.scanner_per_stage[self.player].value,
- altars=self.multiworld.altars_per_stage[self.player].value,
- dlc_sotv=self.multiworld.dlc_sotv[self.player].value
+ get_locations(
+ chests=self.options.chests_per_stage.value,
+ shrines=self.options.shrines_per_stage.value,
+ scavengers=self.options.scavengers_per_stage.value,
+ scanners=self.options.scanner_per_stage.value,
+ altars=self.options.altars_per_stage.value,
+ dlc_sotv=bool(self.options.dlc_sotv.value)
)
)
# Create junk items
- self.junk_pool = self.create_junk_pool()
+ junk_pool = self.create_junk_pool()
# Fill remaining items with randomly generated junk
- while len(itempool) < total_locations:
- itempool.append(self.get_filler_item_name())
+ filler = self.random.choices(*zip(*junk_pool.items()), k=total_locations - len(itempool))
+ itempool.extend(filler)
# Convert itempool into real items
- itempool = list(map(lambda name: self.create_item(name), itempool))
- self.multiworld.itempool += itempool
-
- def set_rules(self) -> None:
- set_rules(self.multiworld, self.player)
-
- def get_filler_item_name(self) -> str:
- if not self.junk_pool:
- self.junk_pool = self.create_junk_pool()
- weights = [data for data in self.junk_pool.values()]
- filler = self.multiworld.random.choices([filler for filler in self.junk_pool.keys()], weights,
- k=1)[0]
- return filler
+ self.multiworld.itempool += map(self.create_item, itempool)
- def create_junk_pool(self) -> Dict:
+ def create_junk_pool(self) -> Dict[str, int]:
# if presets are enabled generate junk_pool from the selected preset
- pool_option = self.multiworld.item_weights[self.player].value
+ pool_option = self.options.item_weights.value
junk_pool: Dict[str, int] = {}
- if self.multiworld.item_pool_presets[self.player]:
+ if self.options.item_pool_presets:
# generate chaos weights if the preset is chosen
if pool_option == ItemWeights.option_chaos:
for name, max_value in item_pool_weights[pool_option].items():
- junk_pool[name] = self.multiworld.random.randint(0, max_value)
+ junk_pool[name] = self.random.randint(0, max_value)
else:
junk_pool = item_pool_weights[pool_option].copy()
else: # generate junk pool from user created presets
junk_pool = {
- "Item Scrap, Green": self.multiworld.green_scrap[self.player].value,
- "Item Scrap, Red": self.multiworld.red_scrap[self.player].value,
- "Item Scrap, Yellow": self.multiworld.yellow_scrap[self.player].value,
- "Item Scrap, White": self.multiworld.white_scrap[self.player].value,
- "Common Item": self.multiworld.common_item[self.player].value,
- "Uncommon Item": self.multiworld.uncommon_item[self.player].value,
- "Legendary Item": self.multiworld.legendary_item[self.player].value,
- "Boss Item": self.multiworld.boss_item[self.player].value,
- "Lunar Item": self.multiworld.lunar_item[self.player].value,
- "Void Item": self.multiworld.void_item[self.player].value,
- "Equipment": self.multiworld.equipment[self.player].value
+ "Item Scrap, Green": self.options.green_scrap.value,
+ "Item Scrap, Red": self.options.red_scrap.value,
+ "Item Scrap, Yellow": self.options.yellow_scrap.value,
+ "Item Scrap, White": self.options.white_scrap.value,
+ "Common Item": self.options.common_item.value,
+ "Uncommon Item": self.options.uncommon_item.value,
+ "Legendary Item": self.options.legendary_item.value,
+ "Boss Item": self.options.boss_item.value,
+ "Lunar Item": self.options.lunar_item.value,
+ "Void Item": self.options.void_item.value,
+ "Equipment": self.options.equipment.value,
+ "Money": self.options.money.value,
+ "Lunar Coin": self.options.lunar_coin.value,
+ "1000 Exp": self.options.experience.value,
+ "Mountain Trap": self.options.mountain_trap.value,
+ "Time Warp Trap": self.options.time_warp_trap.value,
+ "Combat Trap": self.options.combat_trap.value,
+ "Teleport Trap": self.options.teleport_trap.value,
}
-
- # remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled
- if not (self.multiworld.enable_lunar[self.player] or pool_option == ItemWeights.option_lunartic):
+ # remove trap items from the pool (excluding lunar items)
+ if not self.options.enable_trap:
+ junk_pool.pop("Mountain Trap")
+ junk_pool.pop("Time Warp Trap")
+ junk_pool.pop("Combat Trap")
+ junk_pool.pop("Teleport Trap")
+ # remove lunar items from the pool
+ if not (self.options.enable_lunar or pool_option == ItemWeights.option_lunartic):
junk_pool.pop("Lunar Item")
# remove void items from the pool
- if not (self.multiworld.dlc_sotv[self.player] or pool_option == ItemWeights.option_void):
+ if not (self.options.dlc_sotv or pool_option == ItemWeights.option_void):
junk_pool.pop("Void Item")
return junk_pool
- def create_regions(self) -> None:
+ def create_item(self, name: str) -> Item:
+ data = item_table[name]
+ return RiskOfRainItem(name, data.item_type, data.code, self.player)
- if self.multiworld.goal[self.player] == "classic":
- # classic mode
- menu = create_region(self.multiworld, self.player, "Menu")
- self.multiworld.regions.append(menu)
- # By using a victory region, we can define it as being connected to by several regions
- # which can then determine the availability of the victory.
- victory_region = create_region(self.multiworld, self.player, "Victory")
- self.multiworld.regions.append(victory_region)
- petrichor = create_region(self.multiworld, self.player, "Petrichor V",
- get_classic_item_pickups(self.multiworld.total_locations[self.player].value))
- self.multiworld.regions.append(petrichor)
-
- # classic mode can get to victory from the beginning of the game
- to_victory = Entrance(self.player, "beating game", petrichor)
- petrichor.exits.append(to_victory)
- to_victory.connect(victory_region)
-
- connection = Entrance(self.player, "Lobby", menu)
- menu.exits.append(connection)
- connection.connect(petrichor)
- else:
- # explore mode
- create_regions(self.multiworld, self.player)
+ def set_rules(self) -> None:
+ set_rules(self)
- create_events(self.multiworld, self.player)
+ def get_filler_item_name(self) -> str:
+ weights = [data.weight for data in filler_table.values()]
+ filler = self.multiworld.random.choices([filler for filler in filler_table.keys()], weights,
+ k=1)[0]
+ return filler
- def fill_slot_data(self):
+ def fill_slot_data(self) -> Dict[str, Any]:
+ options_dict = self.options.as_dict("item_pickup_step", "shrine_use_step", "goal", "victory", "total_locations",
+ "chests_per_stage", "shrines_per_stage", "scavengers_per_stage",
+ "scanner_per_stage", "altars_per_stage", "total_revivals",
+ "start_with_revive", "final_stage_death", "death_link", "require_stages",
+ "progressive_stages", casing="camel")
return {
- "itemPickupStep": self.multiworld.item_pickup_step[self.player].value,
- "shrineUseStep": self.multiworld.shrine_use_step[self.player].value,
- "goal": self.multiworld.goal[self.player].value,
- "seed": "".join(self.multiworld.per_slot_randoms[self.player].choice(string.digits) for _ in range(16)),
- "totalLocations": self.multiworld.total_locations[self.player].value,
- "chestsPerStage": self.multiworld.chests_per_stage[self.player].value,
- "shrinesPerStage": self.multiworld.shrines_per_stage[self.player].value,
- "scavengersPerStage": self.multiworld.scavengers_per_stage[self.player].value,
- "scannerPerStage": self.multiworld.scanner_per_stage[self.player].value,
- "altarsPerStage": self.multiworld.altars_per_stage[self.player].value,
- "totalRevivals": self.multiworld.total_revivals[self.player].value,
- "startWithDio": self.multiworld.start_with_revive[self.player].value,
- "finalStageDeath": self.multiworld.final_stage_death[self.player].value,
- "deathLink": self.multiworld.death_link[self.player].value,
+ **options_dict,
+ "seed": "".join(self.random.choice(string.digits) for _ in range(16)),
+ "offset": offset
}
- def create_item(self, name: str) -> Item:
- item_id = item_table[name]
- classification = ItemClassification.filler
- if name in {"Dio's Best Friend", "Beads of Fealty"}:
- classification = ItemClassification.progression
- elif name in {"Legendary Item", "Boss Item"}:
- classification = ItemClassification.useful
- elif name == "Lunar Item":
- classification = ItemClassification.trap
-
- # Only check for an item to be a environment unlock if those are known to be in the pool.
- # This should shave down comparisons.
-
- elif name in environment_ALL_table.keys():
- if name in {"Hidden Realm: Bulwark's Ambry", "Hidden Realm: Gilded Coast,"}:
- classification = ItemClassification.useful
- else:
- classification = ItemClassification.progression
-
- item = RiskOfRainItem(name, classification, item_id, self.player)
- return item
-
-
-def create_events(world: MultiWorld, player: int) -> None:
- total_locations = world.total_locations[player].value
- num_of_events = total_locations // 25
- if total_locations / 25 == num_of_events:
- num_of_events -= 1
- world_region = world.get_region("Petrichor V", player)
- if world.goal[player] == "classic":
- # only setup Pickups when using classic_mode
- for i in range(num_of_events):
- event_loc = RiskOfRainLocation(player, f"Pickup{(i + 1) * 25}", None, world_region)
- event_loc.place_locked_item(RiskOfRainItem(f"Pickup{(i + 1) * 25}", ItemClassification.progression, None, player))
- event_loc.access_rule = \
- lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", player)
- world_region.locations.append(event_loc)
- elif world.goal[player] == "explore":
- for n in range(1, 6):
-
- event_region = world.get_region(f"OrderedStage_{n}", player)
- event_loc = RiskOfRainLocation(player, f"Stage_{n}", None, event_region)
- event_loc.place_locked_item(RiskOfRainItem(f"Stage_{n}", ItemClassification.progression, None, player))
+ def create_events(self) -> None:
+ total_locations = self.options.total_locations.value
+ num_of_events = total_locations // 25
+ if total_locations / 25 == num_of_events:
+ num_of_events -= 1
+ world_region = self.multiworld.get_region("Petrichor V", self.player)
+ if self.options.goal == "classic":
+ # classic mode
+ # only setup Pickups when using classic_mode
+ for i in range(num_of_events):
+ event_loc = RiskOfRainLocation(self.player, f"Pickup{(i + 1) * 25}", None, world_region)
+ event_loc.place_locked_item(
+ RiskOfRainItem(f"Pickup{(i + 1) * 25}", ItemClassification.progression, None,
+ self.player))
+ event_loc.access_rule = \
+ lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", self.player)
+ world_region.locations.append(event_loc)
+ else:
+ # explore mode
+ event_region = self.multiworld.get_region("OrderedStage_5", self.player)
+ event_loc = RiskOfRainLocation(self.player, "Stage 5", None, event_region)
+ event_loc.place_locked_item(RiskOfRainItem("Stage 5", ItemClassification.progression, None, self.player))
event_loc.show_in_spoiler = False
event_region.locations.append(event_loc)
+ event_loc.access_rule = lambda state: state.has("Sky Meadow", self.player)
- victory_region = world.get_region("Victory", player)
- victory_event = RiskOfRainLocation(player, "Victory", None, victory_region)
- victory_event.place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, None, player))
- world_region.locations.append(victory_event)
-
-
-def create_region(world: MultiWorld, player: int, name: str, locations: Dict[str, int] = {}) -> Region:
- ret = Region(name, player, world)
- for location_name, location_id in locations.items():
- ret.locations.append(RiskOfRainLocation(player, location_name, location_id, ret))
- return ret
+ victory_region = self.multiworld.get_region("Victory", self.player)
+ victory_event = RiskOfRainLocation(self.player, "Victory", None, victory_region)
+ victory_event.place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, None, self.player))
+ victory_region.locations.append(victory_event)
diff --git a/worlds/ror2/docs/en_Risk of Rain 2.md b/worlds/ror2/docs/en_Risk of Rain 2.md
index d30edf888944..651c89a33923 100644
--- a/worlds/ror2/docs/en_Risk of Rain 2.md
+++ b/worlds/ror2/docs/en_Risk of Rain 2.md
@@ -1,8 +1,8 @@
# Risk of Rain 2
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -23,7 +23,7 @@ Explore Mode:
- Chests will continue to work as they did in Classic Mode, the difference being that each environment
will have a set amount of items that can be sent out. In addition, shrines, radio scanners, newt altars,
- and scavenger bags will need to be checked, depending on your settings.
+ and scavenger bags will need to be checked, depending on your options.
This mode also makes each environment an item. In order to access a particular stage, you'll need it to be
sent in the multiworld.
@@ -32,7 +32,7 @@ Explore Mode:
Just like in the original game, any way to "beat the game" counts as a win. This means beating one of the bosses
on Commencement, The Planetarium, or A Moment, Whole. Alternatively, if you are new to the game and
aren't very confident in being able to "beat the game", you can set **Final Stage Death is Win** to true
-(You can turn this on in your player settings.) This will make it so dying on either Commencement or The Planetarium,
+(You can turn this on in your player options.) This will make it so dying on either Commencement or The Planetarium,
or **obliterating yourself in A Moment, Fractured** will count as your goal.
**You do not need to complete all the location checks** to win; any item you don't collect may be released if the
server options allow.
@@ -48,16 +48,15 @@ then finish a normal mode run while keeping the items you received via the multi
## Can you play multiplayer?
Yes! You can have a single multiplayer instance as one world in the multiworld. All the players involved need to have
-the Archipelago mod, but only the host needs to configure the Archipelago settings. When someone finds an item for your
+the Archipelago mod, but only the host needs to configure the Archipelago options. When someone finds an item for your
world, all the connected players will receive a copy of the item, and the location check bar will increase whenever any
player finds an item in Risk of Rain.
You cannot have players with different player slots in the same co-op game instance. Only the host's Archipelago
-settings apply, so each Risk of Rain 2 player slot in the multiworld needs to be a separate game instance. You could,
+options apply, so each Risk of Rain 2 player slot in the multiworld needs to be a separate game instance. You could,
for example, have two players trade off hosting and making progress on each other's player slot, but a single co-op
instance can't make progress towards multiple player slots in the multiworld.
-Explore mode is untested in multiplayer and will likely not work until a later release.
## What Risk of Rain items can appear in other players' worlds?
@@ -69,7 +68,7 @@ The Risk of Rain items are:
* `Legendary Item` (Red items)
* `Lunar Item` (Blue items)
* `Equipment` (Orange items)
-* `Dio's Best Friend` (Used if you set the YAML setting `total_revives_available` above `0`)
+* `Dio's Best Friend` (Used if you set the YAML option `total_revives_available` above `0`)
* `Void Item` (Purple items) (needs dlc_sotv: enabled)
Each item grants you a random in-game item from the category it belongs to.
@@ -127,7 +126,7 @@ what item you sent out. If the message does not appear, this likely means that a
## What is the item pickup step?
-The item pickup step is a setting in the YAML which allows you to set how many items you need to spawn before the _next_ item
+The item pickup step is an option in the YAML which allows you to set how many items you need to spawn before the _next_ item
that is spawned disappears (in a poof of smoke) and goes out to the multiworld. For instance, an item step of **1** means that
every other chest will send an item to the multiworld. An item step of **2** means that every third chest sends out an item
just as an item step of **0** would send an item on **each chest.**
diff --git a/worlds/ror2/docs/setup_en.md b/worlds/ror2/docs/setup_en.md
index 4e59d2bf4157..6acf2654a8b2 100644
--- a/worlds/ror2/docs/setup_en.md
+++ b/worlds/ror2/docs/setup_en.md
@@ -29,7 +29,7 @@ You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) h
about why Archipelago uses YAML files and what they're for.
### Where do I get a YAML?
-You can use the [game settings page](/games/Risk%20of%20Rain%202/player-settings) here on the Archipelago
+You can use the [game options page](/games/Risk%20of%20Rain%202/player-options) here on the Archipelago
website to generate a YAML using a graphical interface.
@@ -55,4 +55,15 @@ the player's YAML.
You can talk to other in the multiworld chat using the RoR2 chat. All other multiworld
remote commands list in the [commands guide](/tutorial/Archipelago/commands/en) work as well in the RoR2 chat. You can
also optionally connect to the multiworld using the text client, which can be found in the
-[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases).
\ No newline at end of file
+[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases).
+
+### In-Game Commands
+These commands are to be used in-game by using ``Ctrl + Alt + ` `` and then typing the following:
+ - `archipelago_connect [password]` example: "archipelago_connect archipelago.gg 38281 SlotName".
+ - `archipelago_deathlink true/false` Toggle deathlink.
+ - `archipelago_disconnect` Disconnect from AP.
+ - `archipelago_final_stage_death true/false` Toggle final stage death.
+
+Explore Mode only
+ - `archipelago_show_unlocked_stages` Show which stages have been received.
+ - `archipelago_highlight_satellite true/false` This will highlight the satellite to make it easier to see (Default false).
\ No newline at end of file
diff --git a/worlds/ror2/items.py b/worlds/ror2/items.py
new file mode 100644
index 000000000000..3586030816e9
--- /dev/null
+++ b/worlds/ror2/items.py
@@ -0,0 +1,309 @@
+from BaseClasses import Item, ItemClassification
+from .options import ItemWeights
+from .ror2environments import environment_all_table
+from typing import NamedTuple, Optional, Dict
+
+
+class RiskOfRainItem(Item):
+ game: str = "Risk of Rain 2"
+
+
+class RiskOfRainItemData(NamedTuple):
+ category: str
+ code: int
+ item_type: ItemClassification = ItemClassification.filler
+ weight: Optional[int] = None
+
+
+offset: int = 37000
+filler_offset: int = offset + 300
+trap_offset: int = offset + 400
+stage_offset: int = offset + 500
+environment_offset: int = offset + 700
+# Upgrade item ids 37002 - 37012
+upgrade_table: Dict[str, RiskOfRainItemData] = {
+ "Common Item": RiskOfRainItemData("Upgrade", 2 + offset, ItemClassification.filler, 64),
+ "Uncommon Item": RiskOfRainItemData("Upgrade", 3 + offset, ItemClassification.filler, 32),
+ "Legendary Item": RiskOfRainItemData("Upgrade", 4 + offset, ItemClassification.useful, 8),
+ "Boss Item": RiskOfRainItemData("Upgrade", 5 + offset, ItemClassification.useful, 4),
+ "Equipment": RiskOfRainItemData("Upgrade", 7 + offset, ItemClassification.filler, 32),
+ "Item Scrap, White": RiskOfRainItemData("Upgrade", 8 + offset, ItemClassification.filler, 32),
+ "Item Scrap, Green": RiskOfRainItemData("Upgrade", 9 + offset, ItemClassification.filler, 16),
+ "Item Scrap, Red": RiskOfRainItemData("Upgrade", 10 + offset, ItemClassification.filler, 4),
+ "Item Scrap, Yellow": RiskOfRainItemData("Upgrade", 11 + offset, ItemClassification.filler, 1),
+ "Void Item": RiskOfRainItemData("Upgrade", 12 + offset, ItemClassification.filler, 16),
+}
+# Other item ids 37001, 37013-37014
+other_table: Dict[str, RiskOfRainItemData] = {
+ "Dio's Best Friend": RiskOfRainItemData("ExtraLife", 1 + offset, ItemClassification.progression_skip_balancing),
+ "Beads of Fealty": RiskOfRainItemData("Beads", 13 + offset, ItemClassification.progression),
+ "Radar Scanner": RiskOfRainItemData("Radar", 14 + offset, ItemClassification.useful),
+}
+# Filler item ids 37301 - 37303
+filler_table: Dict[str, RiskOfRainItemData] = {
+ "Money": RiskOfRainItemData("Filler", 1 + filler_offset, ItemClassification.filler, 64),
+ "Lunar Coin": RiskOfRainItemData("Filler", 2 + filler_offset, ItemClassification.filler, 20),
+ "1000 Exp": RiskOfRainItemData("Filler", 3 + filler_offset, ItemClassification.filler, 40),
+}
+# Trap item ids 37401 - 37404 (Lunar items used to be part of the upgrade item list, so keeping the id the same)
+trap_table: Dict[str, RiskOfRainItemData] = {
+ "Lunar Item": RiskOfRainItemData("Trap", 6 + offset, ItemClassification.trap, 16),
+ "Mountain Trap": RiskOfRainItemData("Trap", 1 + trap_offset, ItemClassification.trap, 5),
+ "Time Warp Trap": RiskOfRainItemData("Trap", 2 + trap_offset, ItemClassification.trap, 20),
+ "Combat Trap": RiskOfRainItemData("Trap", 3 + trap_offset, ItemClassification.trap, 20),
+ "Teleport Trap": RiskOfRainItemData("Trap", 4 + trap_offset, ItemClassification.trap, 10),
+}
+# Stage item ids 37501 - 37504
+stage_table: Dict[str, RiskOfRainItemData] = {
+ "Stage 1": RiskOfRainItemData("Stage", 1 + stage_offset, ItemClassification.progression),
+ "Stage 2": RiskOfRainItemData("Stage", 2 + stage_offset, ItemClassification.progression),
+ "Stage 3": RiskOfRainItemData("Stage", 3 + stage_offset, ItemClassification.progression),
+ "Stage 4": RiskOfRainItemData("Stage", 4 + stage_offset, ItemClassification.progression),
+ "Progressive Stage": RiskOfRainItemData("Stage", 5 + stage_offset, ItemClassification.progression),
+}
+
+item_table = {**upgrade_table, **other_table, **filler_table, **trap_table, **stage_table}
+# Environment item ids 37700 - 37746
+##################################################
+# environments
+
+
+# add ALL environments into the item table
+def create_environment_table(name: str, environment_id: int, environment_classification: ItemClassification) \
+ -> Dict[str, RiskOfRainItemData]:
+ return {name: RiskOfRainItemData("Environment", environment_offset + environment_id, environment_classification)}
+
+
+environment_table: Dict[str, RiskOfRainItemData] = {}
+# use the sotv dlc in the item table so that all names can be looked up regardless of use
+for data, key in environment_all_table.items():
+ classification = ItemClassification.progression
+ if data in {"Hidden Realm: Bulwark's Ambry", "Hidden Realm: Gilded Coast"}:
+ classification = ItemClassification.useful
+ environment_table.update(create_environment_table(data, key, classification))
+
+item_table.update(environment_table)
+
+# end of environments
+##################################################
+
+default_weights: Dict[str, int] = {
+ "Item Scrap, Green": 16,
+ "Item Scrap, Red": 4,
+ "Item Scrap, Yellow": 1,
+ "Item Scrap, White": 32,
+ "Common Item": 64,
+ "Uncommon Item": 32,
+ "Legendary Item": 8,
+ "Boss Item": 4,
+ "Void Item": 16,
+ "Equipment": 32,
+ "Money": 64,
+ "Lunar Coin": 20,
+ "1000 Exp": 40,
+ "Lunar Item": 10,
+ "Mountain Trap": 4,
+ "Time Warp Trap": 20,
+ "Combat Trap": 20,
+ "Teleport Trap": 20
+}
+
+new_weights: Dict[str, int] = {
+ "Item Scrap, Green": 15,
+ "Item Scrap, Red": 5,
+ "Item Scrap, Yellow": 1,
+ "Item Scrap, White": 30,
+ "Common Item": 75,
+ "Uncommon Item": 40,
+ "Legendary Item": 10,
+ "Boss Item": 5,
+ "Void Item": 16,
+ "Equipment": 20,
+ "Money": 64,
+ "Lunar Coin": 20,
+ "1000 Exp": 40,
+ "Lunar Item": 10,
+ "Mountain Trap": 4,
+ "Time Warp Trap": 20,
+ "Combat Trap": 20,
+ "Teleport Trap": 20
+}
+
+uncommon_weights: Dict[str, int] = {
+ "Item Scrap, Green": 45,
+ "Item Scrap, Red": 5,
+ "Item Scrap, Yellow": 1,
+ "Item Scrap, White": 30,
+ "Common Item": 45,
+ "Uncommon Item": 100,
+ "Legendary Item": 10,
+ "Boss Item": 5,
+ "Void Item": 16,
+ "Equipment": 20,
+ "Money": 64,
+ "Lunar Coin": 20,
+ "1000 Exp": 40,
+ "Lunar Item": 10,
+ "Mountain Trap": 4,
+ "Time Warp Trap": 20,
+ "Combat Trap": 20,
+ "Teleport Trap": 20
+}
+
+legendary_weights: Dict[str, int] = {
+ "Item Scrap, Green": 15,
+ "Item Scrap, Red": 5,
+ "Item Scrap, Yellow": 1,
+ "Item Scrap, White": 30,
+ "Common Item": 50,
+ "Uncommon Item": 25,
+ "Legendary Item": 100,
+ "Boss Item": 5,
+ "Void Item": 16,
+ "Equipment": 20,
+ "Money": 64,
+ "Lunar Coin": 20,
+ "1000 Exp": 40,
+ "Lunar Item": 10,
+ "Mountain Trap": 4,
+ "Time Warp Trap": 20,
+ "Combat Trap": 20,
+ "Teleport Trap": 20
+}
+
+chaos_weights: Dict[str, int] = {
+ "Item Scrap, Green": 80,
+ "Item Scrap, Red": 45,
+ "Item Scrap, Yellow": 30,
+ "Item Scrap, White": 100,
+ "Common Item": 100,
+ "Uncommon Item": 70,
+ "Legendary Item": 30,
+ "Boss Item": 20,
+ "Void Item": 60,
+ "Equipment": 40,
+ "Money": 64,
+ "Lunar Coin": 20,
+ "1000 Exp": 40,
+ "Lunar Item": 10,
+ "Mountain Trap": 4,
+ "Time Warp Trap": 20,
+ "Combat Trap": 20,
+ "Teleport Trap": 20
+}
+
+no_scraps_weights: Dict[str, int] = {
+ "Item Scrap, Green": 0,
+ "Item Scrap, Red": 0,
+ "Item Scrap, Yellow": 0,
+ "Item Scrap, White": 0,
+ "Common Item": 100,
+ "Uncommon Item": 40,
+ "Legendary Item": 15,
+ "Boss Item": 5,
+ "Void Item": 16,
+ "Equipment": 25,
+ "Money": 64,
+ "Lunar Coin": 20,
+ "1000 Exp": 40,
+ "Lunar Item": 10,
+ "Mountain Trap": 4,
+ "Time Warp Trap": 20,
+ "Combat Trap": 20,
+ "Teleport Trap": 20
+}
+
+even_weights: Dict[str, int] = {
+ "Item Scrap, Green": 1,
+ "Item Scrap, Red": 1,
+ "Item Scrap, Yellow": 1,
+ "Item Scrap, White": 1,
+ "Common Item": 1,
+ "Uncommon Item": 1,
+ "Legendary Item": 1,
+ "Boss Item": 1,
+ "Void Item": 1,
+ "Equipment": 1,
+ "Money": 1,
+ "Lunar Coin": 1,
+ "1000 Exp": 1,
+ "Lunar Item": 1,
+ "Mountain Trap": 1,
+ "Time Warp Trap": 1,
+ "Combat Trap": 1,
+ "Teleport Trap": 1
+}
+
+scraps_only: Dict[str, int] = {
+ "Item Scrap, Green": 70,
+ "Item Scrap, White": 100,
+ "Item Scrap, Red": 30,
+ "Item Scrap, Yellow": 5,
+ "Common Item": 0,
+ "Uncommon Item": 0,
+ "Legendary Item": 0,
+ "Boss Item": 0,
+ "Void Item": 0,
+ "Equipment": 0,
+ "Money": 20,
+ "Lunar Coin": 10,
+ "1000 Exp": 10,
+ "Lunar Item": 0,
+ "Mountain Trap": 5,
+ "Time Warp Trap": 10,
+ "Combat Trap": 10,
+ "Teleport Trap": 10
+}
+lunartic_weights: Dict[str, int] = {
+ "Item Scrap, Green": 0,
+ "Item Scrap, Red": 0,
+ "Item Scrap, Yellow": 0,
+ "Item Scrap, White": 0,
+ "Common Item": 0,
+ "Uncommon Item": 0,
+ "Legendary Item": 0,
+ "Boss Item": 0,
+ "Void Item": 0,
+ "Equipment": 0,
+ "Money": 20,
+ "Lunar Coin": 10,
+ "1000 Exp": 10,
+ "Lunar Item": 100,
+ "Mountain Trap": 5,
+ "Time Warp Trap": 10,
+ "Combat Trap": 10,
+ "Teleport Trap": 10
+}
+void_weights: Dict[str, int] = {
+ "Item Scrap, Green": 0,
+ "Item Scrap, Red": 0,
+ "Item Scrap, Yellow": 0,
+ "Item Scrap, White": 0,
+ "Common Item": 0,
+ "Uncommon Item": 0,
+ "Legendary Item": 0,
+ "Boss Item": 0,
+ "Void Item": 100,
+ "Equipment": 0,
+ "Money": 20,
+ "Lunar Coin": 10,
+ "1000 Exp": 10,
+ "Lunar Item": 0,
+ "Mountain Trap": 5,
+ "Time Warp Trap": 10,
+ "Combat Trap": 10,
+ "Teleport Trap": 10
+}
+
+item_pool_weights: Dict[int, Dict[str, int]] = {
+ ItemWeights.option_default: default_weights,
+ ItemWeights.option_new: new_weights,
+ ItemWeights.option_uncommon: uncommon_weights,
+ ItemWeights.option_legendary: legendary_weights,
+ ItemWeights.option_chaos: chaos_weights,
+ ItemWeights.option_no_scraps: no_scraps_weights,
+ ItemWeights.option_even: even_weights,
+ ItemWeights.option_scraps_only: scraps_only,
+ ItemWeights.option_lunartic: lunartic_weights,
+ ItemWeights.option_void: void_weights,
+}
diff --git a/worlds/ror2/locations.py b/worlds/ror2/locations.py
new file mode 100644
index 000000000000..13077b3e149c
--- /dev/null
+++ b/worlds/ror2/locations.py
@@ -0,0 +1,89 @@
+from typing import Dict
+from BaseClasses import Location
+from .options import TotalLocations, ChestsPerEnvironment, ShrinesPerEnvironment, ScavengersPerEnvironment, \
+ ScannersPerEnvironment, AltarsPerEnvironment
+from .ror2environments import compress_dict_list_horizontal, environment_vanilla_orderedstages_table, \
+ environment_sotv_orderedstages_table
+
+
+class RiskOfRainLocation(Location):
+ game: str = "Risk of Rain 2"
+
+
+ror2_locations_start_id = 38000
+
+
+def get_classic_item_pickups(n: int) -> Dict[str, int]:
+ """Get n ItemPickups, capped at the max value for TotalLocations"""
+ n = max(n, 0)
+ n = min(n, TotalLocations.range_end)
+ return {f"ItemPickup{i + 1}": ror2_locations_start_id + i for i in range(n)}
+
+
+item_pickups = get_classic_item_pickups(TotalLocations.range_end)
+location_table = item_pickups
+
+# this is so we can easily calculate the environment and location "offset" ids
+ror2_locations_start_ordered_stage = ror2_locations_start_id + TotalLocations.range_end
+
+# TODO is there a better, more generic way to do this?
+offset_chests = 0
+offset_shrines = offset_chests + ChestsPerEnvironment.range_end
+offset_scavengers = offset_shrines + ShrinesPerEnvironment.range_end
+offset_scanners = offset_scavengers + ScavengersPerEnvironment.range_end
+offset_altars = offset_scanners + ScannersPerEnvironment.range_end
+
+# total space allocated to the locations in a single orderedstage environment
+allocation = offset_altars + AltarsPerEnvironment.range_end
+
+
+def get_environment_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int,
+ environment_name: str, environment_index: int) -> Dict[str, int]:
+ """Get the locations within a specific environment"""
+ locations = {}
+
+ # due to this mapping, since environment ids are not consecutive, there are lots of "wasted" id numbers
+ environment_start_id = environment_index * allocation + ror2_locations_start_ordered_stage
+ for n in range(chests):
+ locations.update({f"{environment_name}: Chest {n + 1}": n + offset_chests + environment_start_id})
+ for n in range(shrines):
+ locations.update({f"{environment_name}: Shrine {n + 1}": n + offset_shrines + environment_start_id})
+ for n in range(scavengers):
+ locations.update({f"{environment_name}: Scavenger {n + 1}": n + offset_scavengers + environment_start_id})
+ for n in range(scanners):
+ locations.update({f"{environment_name}: Radio Scanner {n + 1}": n + offset_scanners + environment_start_id})
+ for n in range(altars):
+ locations.update({f"{environment_name}: Newt Altar {n + 1}": n + offset_altars + environment_start_id})
+ return locations
+
+
+def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, dlc_sotv: bool) \
+ -> Dict[str, int]:
+ """Get a dictionary of locations for the orderedstage environments with the locations from the parameters."""
+ locations = {}
+ orderedstages = compress_dict_list_horizontal(environment_vanilla_orderedstages_table)
+ if dlc_sotv:
+ orderedstages.update(compress_dict_list_horizontal(environment_sotv_orderedstages_table))
+ # for every environment, generate the respective locations
+ for environment_name, environment_index in orderedstages.items():
+ locations.update(get_environment_locations(
+ chests=chests,
+ shrines=shrines,
+ scavengers=scavengers,
+ scanners=scanners,
+ altars=altars,
+ environment_name=environment_name,
+ environment_index=environment_index),
+ )
+ return locations
+
+
+# Get all locations in ordered stages.
+location_table.update(get_locations(
+ chests=ChestsPerEnvironment.range_end,
+ shrines=ShrinesPerEnvironment.range_end,
+ scavengers=ScavengersPerEnvironment.range_end,
+ scanners=ScannersPerEnvironment.range_end,
+ altars=AltarsPerEnvironment.range_end,
+ dlc_sotv=True,
+))
diff --git a/worlds/ror2/options.py b/worlds/ror2/options.py
new file mode 100644
index 000000000000..381c5942b07b
--- /dev/null
+++ b/worlds/ror2/options.py
@@ -0,0 +1,457 @@
+from dataclasses import dataclass
+from Options import Toggle, DefaultOnToggle, DeathLink, Range, Choice, PerGameCommonOptions, OptionGroup
+
+
+# NOTE be aware that since the range of item ids that RoR2 uses is based off of the maximums of checks
+# Be careful when changing the range_end values not to go into another game's IDs
+# NOTE that these changes to range_end must also be reflected in the RoR2 client, so it understands the same ids.
+
+class Goal(Choice):
+ """
+ Classic Mode: Every Item pickup increases fills a progress bar which gives location checks.
+
+ Explore Mode: Each environment will have location checks within each environment.
+ environments will be locked in the item pool until received.
+ """
+ display_name = "Game Mode"
+ option_classic = 0
+ option_explore = 1
+ default = 1
+
+
+class Victory(Choice):
+ """
+ Mithrix: Defeat Mithrix in Commencement
+ Voidling: Defeat the Voidling in The Planetarium (DLC required! Will select any if not enabled.)
+ Limbo: Defeat the Scavenger in Hidden Realm: A Moment, Whole
+ Any: Any victory in the game will count. See Final Stage Death for additional ways.
+ """
+ display_name = "Victory Condition"
+ option_any = 0
+ option_mithrix = 1
+ option_voidling = 2
+ option_limbo = 3
+ default = 0
+
+
+class TotalLocations(Range):
+ """Classic Mode: Number of location checks which are added to the Risk of Rain playthrough."""
+ display_name = "Total Locations"
+ range_start = 40
+ range_end = 250
+ default = 40
+
+
+class ChestsPerEnvironment(Range):
+ """Explore Mode: The number of chest locations per environment."""
+ display_name = "Chests per Environment"
+ range_start = 2
+ range_end = 20
+ default = 10
+
+
+class ShrinesPerEnvironment(Range):
+ """Explore Mode: The number of shrine locations per environment."""
+ display_name = "Shrines per Environment"
+ range_start = 2
+ range_end = 20
+ default = 5
+
+
+class ScavengersPerEnvironment(Range):
+ """Explore Mode: The number of scavenger locations per environment."""
+ display_name = "Scavenger per Environment"
+ range_start = 0
+ range_end = 1
+ default = 0
+
+
+class ScannersPerEnvironment(Range):
+ """Explore Mode: The number of scanners locations per environment."""
+ display_name = "Radio Scanners per Environment"
+ range_start = 0
+ range_end = 1
+ default = 1
+
+
+class AltarsPerEnvironment(Range):
+ """Explore Mode: The number of altars locations per environment."""
+ display_name = "Newts Per Environment"
+ range_start = 0
+ range_end = 2
+ default = 1
+
+
+class TotalRevivals(Range):
+ """Total Percentage of `Dio's Best Friend` item put in the item pool."""
+ display_name = "Total Revives as percentage"
+ range_start = 0
+ range_end = 10
+ default = 4
+
+
+class ItemPickupStep(Range):
+ """
+ Number of items to pick up before an AP Check is completed.
+ Setting to 1 means every other pickup.
+ Setting to 2 means every third pickup. So on...
+ """
+ display_name = "Item Pickup Step"
+ range_start = 0
+ range_end = 5
+ default = 1
+
+
+class ShrineUseStep(Range):
+ """
+ Explore Mode:
+ Number of shrines to use up before an AP Check is completed.
+ Setting to 1 means every other pickup.
+ Setting to 2 means every third pickup. So on...
+ """
+ display_name = "Shrine use Step"
+ range_start = 0
+ range_end = 3
+ default = 0
+
+
+class AllowTrapItems(Toggle):
+ """Allows Trap items in the item pool."""
+ display_name = "Enable Trap Items"
+
+
+class AllowLunarItems(DefaultOnToggle):
+ """Allows Lunar items in the item pool."""
+ display_name = "Enable Lunar Item Shuffling"
+
+
+class StartWithRevive(DefaultOnToggle):
+ """Start the game with a `Dio's Best Friend` item."""
+ display_name = "Start with a Revive"
+
+
+class FinalStageDeath(Toggle):
+ """The following will count as a win if set to "true", and victory is set to "any":
+ Dying in Commencement.
+ Dying in The Planetarium.
+ Obliterating yourself
+ If not use the following to tell if final stage death will count:
+ Victory: mithrix - only dying in Commencement will count.
+ Victory: voidling - only dying in The Planetarium will count.
+ Victory: limbo - Obliterating yourself will count."""
+ display_name = "Final Stage Death is Win"
+
+
+class DLC_SOTV(Toggle):
+ """
+ Enable if you are using SOTV DLC.
+ Affects environment availability for Explore Mode.
+ Adds Void Items into the item pool
+ """
+ display_name = "Enable DLC - SOTV"
+
+
+class RequireStages(DefaultOnToggle):
+ """Add Stage items to the pool to block access to the next set of environments."""
+ display_name = "Require Stages"
+
+
+class ProgressiveStages(DefaultOnToggle):
+ """This will convert Stage items to be a progressive item. For example instead of "Stage 2" it would be
+ "Progressive Stage" """
+ display_name = "Progressive Stages"
+
+
+class GreenScrap(Range):
+ """Weight of Green Scraps in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Green Scraps"
+ range_start = 0
+ range_end = 100
+ default = 16
+
+
+class RedScrap(Range):
+ """Weight of Red Scraps in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Red Scraps"
+ range_start = 0
+ range_end = 100
+ default = 4
+
+
+class YellowScrap(Range):
+ """Weight of yellow scraps in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Yellow Scraps"
+ range_start = 0
+ range_end = 100
+ default = 1
+
+
+class WhiteScrap(Range):
+ """Weight of white scraps in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "White Scraps"
+ range_start = 0
+ range_end = 100
+ default = 32
+
+
+class CommonItem(Range):
+ """Weight of common items in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Common Items"
+ range_start = 0
+ range_end = 100
+ default = 64
+
+
+class UncommonItem(Range):
+ """Weight of uncommon items in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Uncommon Items"
+ range_start = 0
+ range_end = 100
+ default = 32
+
+
+class LegendaryItem(Range):
+ """Weight of legendary items in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Legendary Items"
+ range_start = 0
+ range_end = 100
+ default = 8
+
+
+class BossItem(Range):
+ """Weight of boss items in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Boss Items"
+ range_start = 0
+ range_end = 100
+ default = 4
+
+
+class LunarItem(Range):
+ """Weight of lunar items in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Lunar Items"
+ range_start = 0
+ range_end = 100
+ default = 16
+
+
+class VoidItem(Range):
+ """Weight of void items in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')
+
+ (Ignored if Enable DLC - SOTV is 'No') """
+ display_name = "Void Items"
+ range_start = 0
+ range_end = 100
+ default = 16
+
+
+class Equipment(Range):
+ """Weight of equipment items in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Equipment"
+ range_start = 0
+ range_end = 100
+ default = 32
+
+
+class Money(Range):
+ """Weight of money items in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Money"
+ range_start = 0
+ range_end = 100
+ default = 64
+
+
+class LunarCoin(Range):
+ """Weight of lunar coin items in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Lunar Coins"
+ range_start = 0
+ range_end = 100
+ default = 20
+
+
+class Experience(Range):
+ """Weight of 1000 exp items in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "1000 Exp"
+ range_start = 0
+ range_end = 100
+ default = 40
+
+
+class MountainTrap(Range):
+ """Weight of mountain trap items in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Mountain Trap"
+ range_start = 0
+ range_end = 100
+ default = 5
+
+
+class TimeWarpTrap(Range):
+ """Weight of time warp trap items in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Time Warp Trap"
+ range_start = 0
+ range_end = 100
+ default = 20
+
+
+class CombatTrap(Range):
+ """Weight of combat trap items in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Combat Trap"
+ range_start = 0
+ range_end = 100
+ default = 20
+
+
+class TeleportTrap(Range):
+ """Weight of teleport trap items in the item pool.
+
+ (Ignored unless Item Weight Presets is 'No')"""
+ display_name = "Teleport Trap"
+ range_start = 0
+ range_end = 100
+ default = 20
+
+
+class ItemPoolPresetToggle(Toggle):
+ """Will use the item weight presets when set to true, otherwise will use the custom set item pool weights."""
+ display_name = "Use Item Weight Presets"
+
+
+class ItemWeights(Choice):
+ """Set Use Item Weight Presets to yes if you want to use one of these presets.
+ Preset choices for determining the weights of the item pool.
+ - New is a test for a potential adjustment to the default weights.
+ - Uncommon puts a large number of uncommon items in the pool.
+ - Legendary puts a large number of legendary items in the pool.
+ - Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being
+ too easy.
+ - No Scraps removes all scrap items from the item pool.
+ - Even generates the item pool with every item having an even weight.
+ - Scraps Only will be only scrap items in the item pool.
+ - Lunartic makes everything a lunar item.
+ - Void makes everything a void item."""
+ display_name = "Item Weights"
+ option_default = 0
+ option_new = 1
+ option_uncommon = 2
+ option_legendary = 3
+ option_chaos = 4
+ option_no_scraps = 5
+ option_even = 6
+ option_scraps_only = 7
+ option_lunartic = 8
+ option_void = 9
+
+
+ror2_option_groups = [
+ OptionGroup("Explore Mode Options", [
+ ChestsPerEnvironment,
+ ShrinesPerEnvironment,
+ ScavengersPerEnvironment,
+ ScannersPerEnvironment,
+ AltarsPerEnvironment,
+ RequireStages,
+ ProgressiveStages,
+ ]),
+ OptionGroup("Classic Mode Options", [
+ TotalLocations,
+ ], start_collapsed=True),
+ OptionGroup("Weighted Choices", [
+ ItemWeights,
+ ItemPoolPresetToggle,
+ WhiteScrap,
+ GreenScrap,
+ YellowScrap,
+ RedScrap,
+ CommonItem,
+ UncommonItem,
+ LegendaryItem,
+ BossItem,
+ LunarItem,
+ VoidItem,
+ Equipment,
+ Money,
+ LunarCoin,
+ Experience,
+ MountainTrap,
+ TimeWarpTrap,
+ CombatTrap,
+ TeleportTrap,
+ ]),
+]
+
+
+@dataclass
+class ROR2Options(PerGameCommonOptions):
+ goal: Goal
+ victory: Victory
+ total_locations: TotalLocations
+ chests_per_stage: ChestsPerEnvironment
+ shrines_per_stage: ShrinesPerEnvironment
+ scavengers_per_stage: ScavengersPerEnvironment
+ scanner_per_stage: ScannersPerEnvironment
+ altars_per_stage: AltarsPerEnvironment
+ total_revivals: TotalRevivals
+ start_with_revive: StartWithRevive
+ final_stage_death: FinalStageDeath
+ dlc_sotv: DLC_SOTV
+ require_stages: RequireStages
+ progressive_stages: ProgressiveStages
+ death_link: DeathLink
+ item_pickup_step: ItemPickupStep
+ shrine_use_step: ShrineUseStep
+ enable_trap: AllowTrapItems
+ enable_lunar: AllowLunarItems
+ item_weights: ItemWeights
+ item_pool_presets: ItemPoolPresetToggle
+ # define the weights of the generated item pool.
+ white_scrap: WhiteScrap
+ green_scrap: GreenScrap
+ yellow_scrap: YellowScrap
+ red_scrap: RedScrap
+ common_item: CommonItem
+ uncommon_item: UncommonItem
+ legendary_item: LegendaryItem
+ boss_item: BossItem
+ lunar_item: LunarItem
+ void_item: VoidItem
+ equipment: Equipment
+ money: Money
+ lunar_coin: LunarCoin
+ experience: Experience
+ mountain_trap: MountainTrap
+ time_warp_trap: TimeWarpTrap
+ combat_trap: CombatTrap
+ teleport_trap: TeleportTrap
diff --git a/worlds/ror2/regions.py b/worlds/ror2/regions.py
new file mode 100644
index 000000000000..def29b47286b
--- /dev/null
+++ b/worlds/ror2/regions.py
@@ -0,0 +1,176 @@
+from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING
+
+from BaseClasses import Region, Entrance, MultiWorld
+from .locations import location_table, RiskOfRainLocation, get_classic_item_pickups
+
+if TYPE_CHECKING:
+ from . import RiskOfRainWorld
+
+
+class RoRRegionData(NamedTuple):
+ locations: Optional[List[str]]
+ region_exits: Optional[List[str]]
+
+
+def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None:
+ player = ror2_world.player
+ ror2_options = ror2_world.options
+ multiworld = ror2_world.multiworld
+ # Default Locations
+ non_dlc_regions: Dict[str, RoRRegionData] = {
+ "Menu": RoRRegionData(None, ["Distant Roost", "Distant Roost (2)",
+ "Titanic Plains", "Titanic Plains (2)",
+ "Verdant Falls"]),
+ "Distant Roost": RoRRegionData([], ["OrderedStage_1"]),
+ "Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]),
+ "Titanic Plains": RoRRegionData([], ["OrderedStage_1"]),
+ "Titanic Plains (2)": RoRRegionData([], ["OrderedStage_1"]),
+ "Verdant Falls": RoRRegionData([], ["OrderedStage_1"]),
+ "Abandoned Aqueduct": RoRRegionData([], ["OrderedStage_2"]),
+ "Wetland Aspect": RoRRegionData([], ["OrderedStage_2"]),
+ "Rallypoint Delta": RoRRegionData([], ["OrderedStage_3"]),
+ "Scorched Acres": RoRRegionData([], ["OrderedStage_3"]),
+ "Abyssal Depths": RoRRegionData([], ["OrderedStage_4"]),
+ "Siren's Call": RoRRegionData([], ["OrderedStage_4"]),
+ "Sundered Grove": RoRRegionData([], ["OrderedStage_4"]),
+ "Sky Meadow": RoRRegionData([], ["Hidden Realm: Bulwark's Ambry", "OrderedStage_5"]),
+ }
+ # SOTV Regions
+ dlc_regions: Dict[str, RoRRegionData] = {
+ "Siphoned Forest": RoRRegionData([], ["OrderedStage_1"]),
+ "Aphelian Sanctuary": RoRRegionData([], ["OrderedStage_2"]),
+ "Sulfur Pools": RoRRegionData([], ["OrderedStage_3"])
+ }
+ other_regions: Dict[str, RoRRegionData] = {
+ "Commencement": RoRRegionData(None, ["Victory", "Petrichor V"]),
+ "OrderedStage_5": RoRRegionData(None, ["Hidden Realm: A Moment, Fractured",
+ "Commencement"]),
+ "OrderedStage_1": RoRRegionData(None, ["Hidden Realm: Bazaar Between Time",
+ "Hidden Realm: Gilded Coast", "Abandoned Aqueduct",
+ "Wetland Aspect"]),
+ "OrderedStage_2": RoRRegionData(None, ["Rallypoint Delta", "Scorched Acres"]),
+ "OrderedStage_3": RoRRegionData(None, ["Abyssal Depths", "Siren's Call",
+ "Sundered Grove"]),
+ "OrderedStage_4": RoRRegionData(None, ["Sky Meadow"]),
+ "Hidden Realm: A Moment, Fractured": RoRRegionData(None, ["Hidden Realm: A Moment, Whole"]),
+ "Hidden Realm: A Moment, Whole": RoRRegionData(None, ["Victory", "Petrichor V"]),
+ "Void Fields": RoRRegionData(None, []),
+ "Victory": RoRRegionData(None, None),
+ "Petrichor V": RoRRegionData(None, []),
+ "Hidden Realm: Bulwark's Ambry": RoRRegionData(None, None),
+ "Hidden Realm: Bazaar Between Time": RoRRegionData(None, ["Void Fields"]),
+ "Hidden Realm: Gilded Coast": RoRRegionData(None, None)
+ }
+ dlc_other_regions: Dict[str, RoRRegionData] = {
+ "The Planetarium": RoRRegionData(None, ["Victory", "Petrichor V"]),
+ "Void Locus": RoRRegionData(None, ["The Planetarium"])
+ }
+ # Totals of each item
+ chests = int(ror2_options.chests_per_stage)
+ shrines = int(ror2_options.shrines_per_stage)
+ scavengers = int(ror2_options.scavengers_per_stage)
+ scanners = int(ror2_options.scanner_per_stage)
+ newt = int(ror2_options.altars_per_stage)
+ all_location_regions = {**non_dlc_regions}
+ if ror2_options.dlc_sotv:
+ all_location_regions = {**non_dlc_regions, **dlc_regions}
+
+ # Locations
+ for key in all_location_regions:
+ if key == "Menu":
+ continue
+ # Chests
+ for i in range(0, chests):
+ all_location_regions[key].locations.append(f"{key}: Chest {i + 1}")
+ # Shrines
+ for i in range(0, shrines):
+ all_location_regions[key].locations.append(f"{key}: Shrine {i + 1}")
+ # Scavengers
+ if scavengers > 0:
+ for i in range(0, scavengers):
+ all_location_regions[key].locations.append(f"{key}: Scavenger {i + 1}")
+ # Radio Scanners
+ if scanners > 0:
+ for i in range(0, scanners):
+ all_location_regions[key].locations.append(f"{key}: Radio Scanner {i + 1}")
+ # Newt Altars
+ if newt > 0:
+ for i in range(0, newt):
+ all_location_regions[key].locations.append(f"{key}: Newt Altar {i + 1}")
+ regions_pool: Dict = {**all_location_regions, **other_regions}
+
+ # DLC Locations
+ if ror2_options.dlc_sotv:
+ non_dlc_regions["Menu"].region_exits.append("Siphoned Forest")
+ other_regions["OrderedStage_1"].region_exits.append("Aphelian Sanctuary")
+ other_regions["OrderedStage_2"].region_exits.append("Sulfur Pools")
+ other_regions["Void Fields"].region_exits.append("Void Locus")
+ other_regions["Commencement"].region_exits.append("The Planetarium")
+ regions_pool: Dict = {**all_location_regions, **other_regions, **dlc_other_regions}
+
+ # Check to see if Victory needs to be removed from regions
+ if ror2_options.victory == "mithrix":
+ other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0)
+ dlc_other_regions["The Planetarium"].region_exits.pop(0)
+ elif ror2_options.victory == "voidling":
+ other_regions["Commencement"].region_exits.pop(0)
+ other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0)
+ elif ror2_options.victory == "limbo":
+ other_regions["Commencement"].region_exits.pop(0)
+ dlc_other_regions["The Planetarium"].region_exits.pop(0)
+
+ # Create all the regions
+ for name, data in regions_pool.items():
+ multiworld.regions.append(create_explore_region(multiworld, player, name, data))
+
+ # Connect all the regions to their exits
+ for name, data in regions_pool.items():
+ create_connections_in_regions(multiworld, player, name, data)
+
+
+def create_explore_region(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData) -> Region:
+ region = Region(name, player, multiworld)
+ if data.locations:
+ for location_name in data.locations:
+ location_data = location_table.get(location_name)
+ location = RiskOfRainLocation(player, location_name, location_data, region)
+ region.locations.append(location)
+
+ return region
+
+
+def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData) -> None:
+ region = multiworld.get_region(name, player)
+ if data.region_exits:
+ region.add_exits(data.region_exits)
+
+
+def create_classic_regions(ror2_world: "RiskOfRainWorld") -> None:
+ player = ror2_world.player
+ ror2_options = ror2_world.options
+ multiworld = ror2_world.multiworld
+ menu = create_classic_region(multiworld, player, "Menu")
+ multiworld.regions.append(menu)
+ # By using a victory region, we can define it as being connected to by several regions
+ # which can then determine the availability of the victory.
+ victory_region = create_classic_region(multiworld, player, "Victory")
+ multiworld.regions.append(victory_region)
+ petrichor = create_classic_region(multiworld, player, "Petrichor V",
+ get_classic_item_pickups(ror2_options.total_locations.value))
+ multiworld.regions.append(petrichor)
+
+ # classic mode can get to victory from the beginning of the game
+ to_victory = Entrance(player, "beating game", petrichor)
+ petrichor.exits.append(to_victory)
+ to_victory.connect(victory_region)
+
+ connection = Entrance(player, "Lobby", menu)
+ menu.exits.append(connection)
+ connection.connect(petrichor)
+
+
+def create_classic_region(multiworld: MultiWorld, player: int, name: str, locations: Dict[str, int] = {}) -> Region:
+ ret = Region(name, player, multiworld)
+ for location_name, location_id in locations.items():
+ ret.locations.append(RiskOfRainLocation(player, location_name, location_id, ret))
+ return ret
diff --git a/worlds/ror2/ror2environments.py b/worlds/ror2/ror2environments.py
new file mode 100644
index 000000000000..61707b336241
--- /dev/null
+++ b/worlds/ror2/ror2environments.py
@@ -0,0 +1,119 @@
+from typing import Dict, List, TypeVar
+
+# TODO probably move to Locations
+
+environment_vanilla_orderedstage_1_table: Dict[str, int] = {
+ "Distant Roost": 7, # blackbeach
+ "Distant Roost (2)": 8, # blackbeach2
+ "Titanic Plains": 15, # golemplains
+ "Titanic Plains (2)": 16, # golemplains2
+ "Verdant Falls": 28, # lakes
+}
+environment_vanilla_orderedstage_2_table: Dict[str, int] = {
+ "Abandoned Aqueduct": 17, # goolake
+ "Wetland Aspect": 12, # foggyswamp
+}
+environment_vanilla_orderedstage_3_table: Dict[str, int] = {
+ "Rallypoint Delta": 13, # frozenwall
+ "Scorched Acres": 47, # wispgraveyard
+}
+environment_vanilla_orderedstage_4_table: Dict[str, int] = {
+ "Abyssal Depths": 10, # dampcavesimple
+ "Siren's Call": 37, # shipgraveyard
+ "Sundered Grove": 35, # rootjungle
+}
+environment_vanilla_orderedstage_5_table: Dict[str, int] = {
+ "Sky Meadow": 38, # skymeadow
+}
+
+environment_vanilla_hidden_realm_table: Dict[str, int] = {
+ "Hidden Realm: Bulwark's Ambry": 5, # artifactworld
+ "Hidden Realm: Bazaar Between Time": 6, # bazaar
+ "Hidden Realm: Gilded Coast": 14, # goldshores
+ "Hidden Realm: A Moment, Whole": 27, # limbo
+ "Hidden Realm: A Moment, Fractured": 33, # mysteryspace
+}
+
+environment_vanilla_special_table: Dict[str, int] = {
+ "Void Fields": 4, # arena
+ "Commencement": 32, # moon2
+}
+
+environment_sotv_orderedstage_1_table: Dict[str, int] = {
+ "Siphoned Forest": 39, # snowyforest
+}
+environment_sotv_orderedstage_2_table: Dict[str, int] = {
+ "Aphelian Sanctuary": 3, # ancientloft
+}
+environment_sotv_orderedstage_3_table: Dict[str, int] = {
+ "Sulfur Pools": 41, # sulfurpools
+}
+
+environment_sotv_special_table: Dict[str, int] = {
+ "Void Locus": 46, # voidstage
+ "The Planetarium": 45, # voidraid
+}
+
+X = TypeVar("X")
+Y = TypeVar("Y")
+
+
+def compress_dict_list_horizontal(list_of_dict: List[Dict[X, Y]]) -> Dict[X, Y]:
+ """Combine all dictionaries in a list together into one dictionary."""
+ compressed: Dict[X, Y] = {}
+ for individual in list_of_dict:
+ compressed.update(individual)
+ return compressed
+
+
+def collapse_dict_list_vertical(list_of_dict_1: List[Dict[X, Y]], *args: List[Dict[X, Y]]) -> List[Dict[X, Y]]:
+ """Combine all parallel dictionaries in lists together to make a new list of dictionaries of the same length."""
+ # find the length of the longest list
+ length = len(list_of_dict_1)
+ for list_of_dict_n in args:
+ length = max(length, len(list_of_dict_n))
+
+ # create a combined list with a length the same as the longest list
+ collapsed: List[Dict[X, Y]] = [{}] * length
+ # The reason the list_of_dict_1 is not directly used to make collapsed is
+ # side effects can occur if all the dictionaries are not manually unioned.
+
+ # merge contents from list_of_dict_1
+ for i in range(len(list_of_dict_1)):
+ collapsed[i] = {**collapsed[i], **list_of_dict_1[i]}
+
+ # merge contents of remaining lists_of_dicts
+ for list_of_dict_n in args:
+ for i in range(len(list_of_dict_n)):
+ collapsed[i] = {**collapsed[i], **list_of_dict_n[i]}
+
+ return collapsed
+
+
+# TODO potentially these should only be created when they are directly referenced
+# (unsure of the space/time cost of creating these initially)
+
+environment_vanilla_orderedstages_table = \
+ [environment_vanilla_orderedstage_1_table, environment_vanilla_orderedstage_2_table,
+ environment_vanilla_orderedstage_3_table, environment_vanilla_orderedstage_4_table,
+ environment_vanilla_orderedstage_5_table]
+environment_vanilla_table = \
+ {**compress_dict_list_horizontal(environment_vanilla_orderedstages_table),
+ **environment_vanilla_hidden_realm_table, **environment_vanilla_special_table}
+
+environment_sotv_orderedstages_table = \
+ [environment_sotv_orderedstage_1_table, environment_sotv_orderedstage_2_table,
+ environment_sotv_orderedstage_3_table]
+environment_sotv_table = \
+ {**compress_dict_list_horizontal(environment_sotv_orderedstages_table), **environment_sotv_special_table}
+
+environment_non_orderedstages_table = \
+ {**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table, **environment_sotv_special_table}
+environment_orderedstages_table = \
+ collapse_dict_list_vertical(environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table)
+environment_all_table = {**environment_vanilla_table, **environment_sotv_table}
+
+
+def shift_by_offset(dictionary: Dict[str, int], offset: int) -> Dict[str, int]:
+ """Shift all indexes in a dictionary by an offset"""
+ return {name: index+offset for name, index in dictionary.items()}
diff --git a/worlds/ror2/rules.py b/worlds/ror2/rules.py
new file mode 100644
index 000000000000..2e6b018f42fb
--- /dev/null
+++ b/worlds/ror2/rules.py
@@ -0,0 +1,158 @@
+from worlds.generic.Rules import set_rule, add_rule
+from BaseClasses import MultiWorld
+from .locations import get_locations
+from .ror2environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table
+from typing import Set, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from . import RiskOfRainWorld
+
+
+# Rule to see if it has access to the previous stage
+def has_entrance_access_rule(multiworld: MultiWorld, stage: str, region: str, player: int) -> None:
+ rule = lambda state: state.has(region, player) and state.has(stage, player)
+ for entrance in multiworld.get_region(region, player).entrances:
+ entrance.access_rule = rule
+
+
+def has_stage_access_rule(multiworld: MultiWorld, stage: str, amount: int, region: str, player: int) -> None:
+ rule = lambda state: state.has(region, player) and \
+ (state.has(stage, player) or state.count("Progressive Stage", player) >= amount)
+ for entrance in multiworld.get_region(region, player).entrances:
+ entrance.access_rule = rule
+
+
+def has_all_items(multiworld: MultiWorld, items: Set[str], region: str, player: int) -> None:
+ rule = lambda state: state.has_all(items, player) and state.has(region, player)
+ for entrance in multiworld.get_region(region, player).entrances:
+ entrance.access_rule = rule
+
+
+# Checks to see if chest/shrine are accessible
+def has_location_access_rule(multiworld: MultiWorld, environment: str, player: int, item_number: int, item_type: str)\
+ -> None:
+ if item_number == 1:
+ multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \
+ lambda state: state.has(environment, player)
+ # scavengers need to be locked till after a full loop since that is when they are capable of spawning.
+ # (While technically the requirement is just beating 5 stages, this will ensure that the player will have
+ # a long enough run to have enough director credits for scavengers and
+ # help prevent being stuck in the same stages until that point).
+ if item_type == "Scavenger":
+ multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \
+ lambda state: state.has(environment, player) and state.has("Stage 5", player)
+ else:
+ multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \
+ lambda state: check_location(state, environment, player, item_number, item_type)
+
+
+def check_location(state, environment: str, player: int, item_number: int, item_name: str) -> bool:
+ return state.can_reach(f"{environment}: {item_name} {item_number - 1}", "Location", player)
+
+
+def set_rules(ror2_world: "RiskOfRainWorld") -> None:
+ player = ror2_world.player
+ multiworld = ror2_world.multiworld
+ ror2_options = ror2_world.options
+ if ror2_options.goal == "classic":
+ # classic mode
+ total_locations = ror2_options.total_locations.value # total locations for current player
+ else:
+ # explore mode
+ total_locations = len(
+ get_locations(
+ chests=ror2_options.chests_per_stage.value,
+ shrines=ror2_options.shrines_per_stage.value,
+ scavengers=ror2_options.scavengers_per_stage.value,
+ scanners=ror2_options.scanner_per_stage.value,
+ altars=ror2_options.altars_per_stage.value,
+ dlc_sotv=bool(ror2_options.dlc_sotv.value)
+ )
+ )
+
+ event_location_step = 25 # set an event location at these locations for "spheres"
+ divisions = total_locations // event_location_step
+ total_revivals = multiworld.worlds[player].total_revivals # pulling this info we calculated in generate_basic
+
+ if ror2_options.goal == "classic":
+ # classic mode
+ if divisions:
+ for i in range(1, divisions + 1): # since divisions is the floor of total_locations / 25
+ if i * event_location_step != total_locations:
+ event_loc = multiworld.get_location(f"Pickup{i * event_location_step}", player)
+ set_rule(event_loc,
+ lambda state, i=i: state.can_reach(f"ItemPickup{i * event_location_step - 1}",
+ "Location", player))
+ # we want to create a rule for each of the 25 locations per division
+ for n in range(i * event_location_step, (i + 1) * event_location_step + 1):
+ if n > total_locations:
+ break
+ if n == i * event_location_step:
+ set_rule(multiworld.get_location(f"ItemPickup{n}", player),
+ lambda state, event_item=event_loc.item.name: state.has(event_item, player))
+ else:
+ set_rule(multiworld.get_location(f"ItemPickup{n}", player),
+ lambda state, n=n: state.can_reach(f"ItemPickup{n - 1}", "Location", player))
+ set_rule(multiworld.get_location("Victory", player),
+ lambda state: state.can_reach(f"ItemPickup{total_locations}", "Location", player))
+ if total_revivals or ror2_options.start_with_revive.value:
+ add_rule(multiworld.get_location("Victory", player),
+ lambda state: state.has("Dio's Best Friend", player,
+ total_revivals + ror2_options.start_with_revive))
+
+ else:
+ # explore mode
+ chests = ror2_options.chests_per_stage.value
+ shrines = ror2_options.shrines_per_stage.value
+ newts = ror2_options.altars_per_stage.value
+ scavengers = ror2_options.scavengers_per_stage.value
+ scanners = ror2_options.scanner_per_stage.value
+ for i in range(len(environment_vanilla_orderedstages_table)):
+ for environment_name, _ in environment_vanilla_orderedstages_table[i].items():
+ # Make sure to go through each location
+ if scavengers == 1:
+ has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger")
+ if scanners == 1:
+ has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner")
+ for chest in range(1, chests + 1):
+ has_location_access_rule(multiworld, environment_name, player, chest, "Chest")
+ for shrine in range(1, shrines + 1):
+ has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine")
+ if newts > 0:
+ for newt in range(1, newts + 1):
+ has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar")
+ if i > 0:
+ has_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player)
+
+ if ror2_options.dlc_sotv:
+ for i in range(len(environment_sotv_orderedstages_table)):
+ for environment_name, _ in environment_sotv_orderedstages_table[i].items():
+ # Make sure to go through each location
+ if scavengers == 1:
+ has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger")
+ if scanners == 1:
+ has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner")
+ for chest in range(1, chests + 1):
+ has_location_access_rule(multiworld, environment_name, player, chest, "Chest")
+ for shrine in range(1, shrines + 1):
+ has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine")
+ if newts > 0:
+ for newt in range(1, newts + 1):
+ has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar")
+ if i > 0:
+ has_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player)
+ has_entrance_access_rule(multiworld, "Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole",
+ player)
+ has_stage_access_rule(multiworld, "Stage 1", 1, "Hidden Realm: Bazaar Between Time", player)
+ has_entrance_access_rule(multiworld, "Hidden Realm: Bazaar Between Time", "Void Fields", player)
+ has_entrance_access_rule(multiworld, "Stage 5", "Commencement", player)
+ has_entrance_access_rule(multiworld, "Stage 5", "Hidden Realm: A Moment, Fractured", player)
+ has_entrance_access_rule(multiworld, "Beads of Fealty", "Hidden Realm: A Moment, Whole", player)
+ if ror2_options.dlc_sotv:
+ has_entrance_access_rule(multiworld, "Stage 5", "The Planetarium", player)
+ has_entrance_access_rule(multiworld, "Stage 5", "Void Locus", player)
+ if ror2_options.victory == "voidling":
+ has_all_items(multiworld, {"Stage 5", "The Planetarium"}, "Commencement", player)
+
+ # Win Condition
+ multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
diff --git a/worlds/ror2/test/__init__.py b/worlds/ror2/test/__init__.py
new file mode 100644
index 000000000000..87d8183ab847
--- /dev/null
+++ b/worlds/ror2/test/__init__.py
@@ -0,0 +1,5 @@
+from test.bases import WorldTestBase
+
+
+class RoR2TestBase(WorldTestBase):
+ game = "Risk of Rain 2"
diff --git a/worlds/ror2/test/test_any_goal.py b/worlds/ror2/test/test_any_goal.py
new file mode 100644
index 000000000000..18d49944195d
--- /dev/null
+++ b/worlds/ror2/test/test_any_goal.py
@@ -0,0 +1,26 @@
+from . import RoR2TestBase
+
+
+class DLCTest(RoR2TestBase):
+ options = {
+ "dlc_sotv": "true",
+ "victory": "any"
+ }
+
+ def test_commencement_victory(self) -> None:
+ self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"])
+ self.assertBeatable(False)
+ self.collect_by_name("Commencement")
+ self.assertBeatable(True)
+
+ def test_planetarium_victory(self) -> None:
+ self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"])
+ self.assertBeatable(False)
+ self.collect_by_name("The Planetarium")
+ self.assertBeatable(True)
+
+ def test_moment_whole_victory(self) -> None:
+ self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"])
+ self.assertBeatable(False)
+ self.collect_by_name("Hidden Realm: A Moment, Whole")
+ self.assertBeatable(True)
diff --git a/worlds/ror2/test/test_classic.py b/worlds/ror2/test/test_classic.py
new file mode 100644
index 000000000000..90ed2302b272
--- /dev/null
+++ b/worlds/ror2/test/test_classic.py
@@ -0,0 +1,7 @@
+from . import RoR2TestBase
+
+
+class ClassicTest(RoR2TestBase):
+ options = {
+ "goal": "classic",
+ }
diff --git a/worlds/ror2/test/test_limbo_goal.py b/worlds/ror2/test/test_limbo_goal.py
new file mode 100644
index 000000000000..9be9cca1206a
--- /dev/null
+++ b/worlds/ror2/test/test_limbo_goal.py
@@ -0,0 +1,15 @@
+from . import RoR2TestBase
+
+
+class LimboGoalTest(RoR2TestBase):
+ options = {
+ "victory": "limbo"
+ }
+
+ def test_limbo(self) -> None:
+ self.collect_all_but(["Hidden Realm: A Moment, Whole", "Victory"])
+ self.assertFalse(self.can_reach_region("Hidden Realm: A Moment, Whole"))
+ self.assertBeatable(False)
+ self.collect_by_name("Hidden Realm: A Moment, Whole")
+ self.assertTrue(self.can_reach_region("Hidden Realm: A Moment, Whole"))
+ self.assertBeatable(True)
diff --git a/worlds/ror2/test/test_mithrix_goal.py b/worlds/ror2/test/test_mithrix_goal.py
new file mode 100644
index 000000000000..a52301bef5eb
--- /dev/null
+++ b/worlds/ror2/test/test_mithrix_goal.py
@@ -0,0 +1,27 @@
+from . import RoR2TestBase
+
+
+class MithrixGoalTest(RoR2TestBase):
+ options = {
+ "victory": "mithrix",
+ "require_stages": "true",
+ "progressive_stages": "false"
+ }
+
+ def test_mithrix(self) -> None:
+ self.collect_all_but(["Commencement", "Victory"])
+ self.assertFalse(self.can_reach_region("Commencement"))
+ self.assertBeatable(False)
+ self.collect_by_name("Commencement")
+ self.assertTrue(self.can_reach_region("Commencement"))
+ self.assertBeatable(True)
+
+ def test_stage5(self) -> None:
+ self.collect_all_but(["Stage 4", "Sky Meadow", "Victory"])
+ self.assertFalse(self.can_reach_region("Sky Meadow"))
+ self.assertBeatable(False)
+ self.collect_by_name("Sky Meadow")
+ self.assertFalse(self.can_reach_region("Sky Meadow"))
+ self.collect_by_name("Stage 4")
+ self.assertTrue(self.can_reach_region("Sky Meadow"))
+ self.assertBeatable(True)
diff --git a/worlds/ror2/test/test_voidling_goal.py b/worlds/ror2/test/test_voidling_goal.py
new file mode 100644
index 000000000000..77d1349f10eb
--- /dev/null
+++ b/worlds/ror2/test/test_voidling_goal.py
@@ -0,0 +1,28 @@
+from . import RoR2TestBase
+
+
+class VoidlingGoalTest(RoR2TestBase):
+ options = {
+ "dlc_sotv": "true",
+ "victory": "voidling"
+ }
+
+ def test_planetarium(self) -> None:
+ self.collect_all_but(["The Planetarium", "Victory"])
+ self.assertFalse(self.can_reach_region("The Planetarium"))
+ self.assertBeatable(False)
+ self.collect_by_name("The Planetarium")
+ self.assertTrue(self.can_reach_region("The Planetarium"))
+ self.assertBeatable(True)
+
+ def test_void_locus_to_victory(self) -> None:
+ self.collect_all_but(["Void Locus", "Commencement"])
+ self.assertFalse(self.can_reach_location("Victory"))
+ self.collect_by_name("Void Locus")
+ self.assertTrue(self.can_reach_location("Victory"))
+
+ def test_commencement_to_victory(self) -> None:
+ self.collect_all_but(["Void Locus", "Commencement"])
+ self.assertFalse(self.can_reach_location("Victory"))
+ self.collect_by_name("Commencement")
+ self.assertTrue(self.can_reach_location("Victory"))
diff --git a/worlds/sa2b/AestheticData.py b/worlds/sa2b/AestheticData.py
new file mode 100644
index 000000000000..077f35fc01b0
--- /dev/null
+++ b/worlds/sa2b/AestheticData.py
@@ -0,0 +1,346 @@
+
+chao_name_conversion = {
+ "!": 0x01,
+ "!": 0x02,
+ "#": 0x03,
+ "$": 0x04,
+ "%": 0x05,
+ "&": 0x06,
+ "\\": 0x07,
+ "(": 0x08,
+ ")": 0x09,
+ "*": 0x0A,
+ "+": 0x0B,
+ ",": 0x0C,
+ "-": 0x0D,
+ ".": 0x0E,
+ "/": 0x0F,
+
+ "0": 0x10,
+ "1": 0x11,
+ "2": 0x12,
+ "3": 0x13,
+ "4": 0x14,
+ "5": 0x15,
+ "6": 0x16,
+ "7": 0x17,
+ "8": 0x18,
+ "9": 0x19,
+ ":": 0x1A,
+ ";": 0x1B,
+ "<": 0x1C,
+ "=": 0x1D,
+ ">": 0x1E,
+ "?": 0x1F,
+
+ "@": 0x20,
+ "A": 0x21,
+ "B": 0x22,
+ "C": 0x23,
+ "D": 0x24,
+ "E": 0x25,
+ "F": 0x26,
+ "G": 0x27,
+ "H": 0x28,
+ "I": 0x29,
+ "J": 0x2A,
+ "K": 0x2B,
+ "L": 0x2C,
+ "M": 0x2D,
+ "N": 0x2E,
+ "O": 0x2F,
+
+ "P": 0x30,
+ "Q": 0x31,
+ "R": 0x32,
+ "S": 0x33,
+ "T": 0x34,
+ "U": 0x35,
+ "V": 0x36,
+ "W": 0x37,
+ "X": 0x38,
+ "Y": 0x39,
+ "Z": 0x3A,
+ "[": 0x3B,
+ "Â¥": 0x3C,
+ "]": 0x3D,
+ "^": 0x3E,
+ "_": 0x3F,
+
+ "`": 0x40,
+ "a": 0x41,
+ "b": 0x42,
+ "c": 0x43,
+ "d": 0x44,
+ "e": 0x45,
+ "f": 0x46,
+ "g": 0x47,
+ "h": 0x48,
+ "i": 0x49,
+ "j": 0x4A,
+ "k": 0x4B,
+ "l": 0x4C,
+ "m": 0x4D,
+ "n": 0x4E,
+ "o": 0x4F,
+
+ "p": 0x50,
+ "q": 0x51,
+ "r": 0x52,
+ "s": 0x53,
+ "t": 0x54,
+ "u": 0x55,
+ "v": 0x56,
+ "w": 0x57,
+ "x": 0x58,
+ "y": 0x59,
+ "z": 0x5A,
+ "{": 0x5B,
+ "|": 0x5C,
+ "}": 0x5D,
+ "~": 0x5E,
+ " ": 0x5F,
+}
+
+sample_chao_names = [
+ "Aginah",
+ "Biter",
+ "Steve",
+ "Ryley",
+ "Watcher",
+ "Acrid",
+ "Sheik",
+ "Lunais",
+ "Samus",
+ "The Kid",
+ "Jack",
+ "Sir Lee",
+ "Viridian",
+ "Rouhi",
+ "Toad",
+ "Merit",
+ "Ridley",
+ "Hornet",
+ "Carl",
+ "Raynor",
+ "Dixie",
+ "Wolnir",
+ "Mario",
+ "Gary",
+ "Wayne",
+ "Kevin",
+ "J.J.",
+ "Maxim",
+ "Redento",
+ "Caesar",
+ "Abigail",
+ "Link",
+ "Ninja",
+ "Roxas",
+ "Marin",
+ "Yorgle",
+ "DLC",
+ "Mina",
+ "Sans",
+ "Lan",
+ "Rin",
+ "Doomguy",
+ "Guide",
+ "May",
+ "Hubert",
+ "Corvus",
+ "Nigel",
+]
+
+totally_real_item_names = [
+ "Mallet",
+ "Lava Rod",
+ "Master Knife",
+ "Slippers",
+ "Spade",
+
+ "Progressive Car Upgrade",
+ "Bonus Token",
+
+ "Shortnail",
+ "Runmaster",
+
+ "Courage Form",
+ "Auto Courage",
+ "Donald Defender",
+ "Goofy Blizzard",
+ "Ultimate Weapon",
+
+ "Song of the Sky Whale",
+ "Gryphon Shoes",
+ "Wing Key",
+ "Strength Anklet",
+
+ "Hairclip",
+
+ "Key of Wisdom",
+
+ "Baking",
+ "Progressive Block Mining",
+
+ "Jar",
+ "Whistle of Space",
+ "Rito Tunic",
+
+ "Kitchen Sink",
+
+ "Rock Badge",
+ "Key Card",
+ "Pikachu",
+ "Eevee",
+ "HM02 Strength",
+
+ "Progressive Astromancers",
+ "Progressive Chefs",
+ "The Living Safe",
+ "Lady Quinn",
+
+ "Dio's Worst Enemy",
+
+ "Pink Chaos Emerald",
+ "Black Chaos Emerald",
+ "Tails - Large Cannon",
+ "Eggman - Bazooka",
+ "Eggman - Booster",
+ "Knuckles - Shades",
+ "Sonic - Magic Shoes",
+ "Shadow - Bounce Bracelet",
+ "Rouge - Air Necklace",
+ "Big Key (Eggman's Pyramid)",
+
+ "Sensor Bunker",
+ "Phantom",
+ "Soldier",
+
+ "Plasma Suit",
+ "Gravity Beam",
+ "Hi-Jump Ball",
+
+ "Cannon Unlock LLL",
+ "Feather Cap",
+
+ "Progressive Yoshi",
+ "Purple Switch Palace",
+ "Cape Feather",
+
+ "Cane of Bryan",
+
+ "Van Repair",
+ "Autumn",
+ "Galaxy Knife",
+ "Green Cabbage Seeds",
+
+ "Timespinner Cog 1",
+
+ "Ladder",
+
+ "Visible Dots",
+]
+
+all_exits = [
+ 0x00, # Lobby to Neutral
+ 0x01, # Lobby to Hero
+ 0x02, # Lobby to Dark
+ 0x03, # Lobby to Kindergarten
+ 0x04, # Neutral to Lobby
+ 0x05, # Neutral to Cave
+ 0x06, # Neutral to Transporter
+ 0x07, # Hero to Lobby
+ 0x08, # Hero to Transporter
+ 0x09, # Dark to Lobby
+ 0x0A, # Dark to Transporter
+ 0x0B, # Cave to Neutral
+ 0x0C, # Cave to Race
+ 0x0D, # Cave to Karate
+ 0x0E, # Race to Cave
+ 0x0F, # Karate to Cave
+ 0x10, # Transporter to Neutral
+ #0x11, # Transporter to Hero
+ #0x12, # Transporter to Dark
+ 0x13, # Kindergarten to Lobby
+]
+
+all_destinations = [
+ 0x07, # Lobby
+ 0x07,
+ 0x07,
+ 0x07,
+ 0x01, # Neutral
+ 0x01,
+ 0x01,
+ 0x02, # Hero
+ 0x02,
+ 0x03, # Dark
+ 0x03,
+ 0x09, # Cave
+ 0x09,
+ 0x09,
+ 0x05, # Chao Race
+ 0x0A, # Chao Karate
+ 0x0C, # Transporter
+ #0x0C,
+ #0x0C,
+ 0x06, # Kindergarten
+]
+
+multi_rooms = [
+ 0x07,
+ 0x01,
+ 0x02,
+ 0x03,
+ 0x09,
+]
+
+single_rooms = [
+ 0x05,
+ 0x0A,
+ 0x0C,
+ 0x06,
+]
+
+room_to_exits_map = {
+ 0x07: [0x00, 0x01, 0x02, 0x03],
+ 0x01: [0x04, 0x05, 0x06],
+ 0x02: [0x07, 0x08],
+ 0x03: [0x09, 0x0A],
+ 0x09: [0x0B, 0x0C, 0x0D],
+ 0x05: [0x0E],
+ 0x0A: [0x0F],
+ 0x0C: [0x10],#, 0x11, 0x12],
+ 0x06: [0x13],
+}
+
+exit_to_room_map = {
+ 0x00: 0x07, # Lobby to Neutral
+ 0x01: 0x07, # Lobby to Hero
+ 0x02: 0x07, # Lobby to Dark
+ 0x03: 0x07, # Lobby to Kindergarten
+ 0x04: 0x01, # Neutral to Lobby
+ 0x05: 0x01, # Neutral to Cave
+ 0x06: 0x01, # Neutral to Transporter
+ 0x07: 0x02, # Hero to Lobby
+ 0x08: 0x02, # Hero to Transporter
+ 0x09: 0x03, # Dark to Lobby
+ 0x0A: 0x03, # Dark to Transporter
+ 0x0B: 0x09, # Cave to Neutral
+ 0x0C: 0x09, # Cave to Race
+ 0x0D: 0x09, # Cave to Karate
+ 0x0E: 0x05, # Race to Cave
+ 0x0F: 0x0A, # Karate to Cave
+ 0x10: 0x0C, # Transporter to Neutral
+ #0x11: 0x0C, # Transporter to Hero
+ #0x12: 0x0C, # Transporter to Dark
+ 0x13: 0x06, # Kindergarten to Lobby
+}
+
+valid_kindergarten_exits = [
+ 0x04, # Neutral to Lobby
+ 0x05, # Neutral to Cave
+ 0x07, # Hero to Lobby
+ 0x09, # Dark to Lobby
+]
diff --git a/worlds/sa2b/CHANGELOG.md b/worlds/sa2b/CHANGELOG.md
new file mode 100644
index 000000000000..af6c4afd229f
--- /dev/null
+++ b/worlds/sa2b/CHANGELOG.md
@@ -0,0 +1,247 @@
+# Sonic Adventure 2 Battle - Changelog
+
+
+## v2.3 - The Chao Update
+
+### Features:
+
+- New goal
+ - Chaos Chao
+ - Raise a Chaos Chao to win!
+- New optional Location Checks
+ - Chao Animal Parts
+ - Each body part from each type of animal is a location
+ - Chao Stats
+ - 0-99 levels of each of the 7 Chao stats can be locations
+ - The frequency of Chao Stat locations can be set (every level, every 2nd level, etc)
+ - Kindergartensanity
+ - Classroom lessons are locations
+ - Either all lessons or any one of each category can be set as locations
+ - Shopsanity
+ - A specified number of locations can be placed in the Chao Black Market
+ - These locations are unlocked by acquiring Chao Coins
+ - Ring costs for these items can be adjusted
+ - Chao Karate can now be set to one location per fight, instead of one per tournament
+- New Items
+ - If any Chao locations are active, the following will be in the item pool:
+ - Chao Eggs
+ - Garden Seeds
+ - Garden Fruit
+ - Chao Hats
+ - Chaos Drives
+ - New Trap
+ - Reverse Trap
+- The starting eggs in the garden can be a random color
+- Chao World entrances can be shuffled
+- Chao are given default names
+
+### Quality of Life:
+
+- Chao Save Data is now separate per-slot in addition to per-seed
+ - This allows a single player to have multiple slots in the same seed, each having separate Chao progress
+- Chao Race/Karate progress is now displayed on Stage Select (when hovering over Chao World)
+- All Chao can now enter the Hero and Dark races
+- Chao Karate difficulty can be set separately from Chao Race difficulty
+- Chao Aging can be sped up at will, up to 15×
+- New mod config option to fine-tune Chao Stat multiplication
+ - Note: This does not mix well with the Mod Manager "Chao Stat Multiplier" code
+- Pong Traps can now activate in Chao World
+- Maximum range for possible number of Emblems is now 1000
+- General APWorld cleanup and optimization
+- Option access has moved to the new options system
+- An item group now exists for trap items
+
+### Bug Fixes:
+
+- Dry Lagoon now has all 11 Animals
+- `Eternal Engine - 2` (Standard and Hard Logic) now requires only `Tails - Booster`
+- `Lost Colony - 2` (Hard Logic) now requires no upgrades
+- `Lost Colony - Animal 9` (Hard Logic) now requires either `Eggman - Jet Engine` or `Eggman - Large Cannon`
+
+
+## v2.2
+
+### Features:
+
+- New goals
+ - Boss Rush
+ - Complete the Boss Rush to win!
+ - Cannon's Core Boss Rush
+ - Beat Cannon's Core, then complete the Boss Rush
+ - Boss Rush Chaos Emerald Hunt
+ - Collect the seven Chaos Emeralds, then complete the Boss Rush
+- Boss Rush Shuffle option
+- New optional Location Checks
+ - Animalsanity
+ - Collect numbers of animals per stage
+- Ring Link option
+ - Any ring amounts gained and lost by a linked player will be instantly shared with all other active linked players
+- Voice line shuffle
+ - None
+ - Shuffled
+ - Rude
+ - Chao
+ - Singularity
+- New Traps
+ - Ice Trap
+ - Slow Trap
+ - Cutscene Trap
+
+### Quality of Life:
+
+- Maximum possible number of Emblems in item pool is now a player-facing option, in the range of 50-500
+- A cause is now included for sent DeathLinks
+- Death Cause messages are now displayed in-game
+- WSS connections are now supported
+
+### Bug Fixes:
+
+- Two rare softlock scenarios related to the Chaos Control Trap should no longer occur
+- Tracking of location checks while disconnected from the server should be more consistent
+- DeathLinks can now be sent and received between two players connected to the same slot
+- 2P mode should no longer be accessible
+- Boss Stages no longer display erroneous location tracking icons
+- Boss Stages are no longer subject to mission shuffle oddities
+- Fix Logic Errors
+ - Eternal Engine - Pipe 1 (Standard and Hard Logic) now requires no upgrades
+ - Egg Quarters - 5 (Standard Logic) now requires Rouge - Iron Boots
+
+
+## v2.1
+
+### Features:
+
+- New goal
+ - Grand Prix
+ - Complete all of the Kart Races to win!
+- New optional Location Checks
+ - Omosanity (Activating Omochao)
+ - Kart Race Mode
+- Ring Loss option
+ - Classic - lose all rings on hit
+ - Modern - lose 20 rings on hit
+ - OHKO - instantly die on hit, regardless of ring count (shields still protect you)
+- New Trap
+ - Pong Trap
+
+### Quality of Life:
+
+- SA2B is now distributed as an `.apworld`
+- Maximum possible number of Emblems in item pool is increased from 180 to 250
+- An indicator now shows on the Stage Select screen when Cannon's Core is available
+- Certain traps (Exposition and Pong) are now possible to receive on Route 101 and Route 280
+- Certain traps (Confusion, Chaos Control, Exposition and Pong) are now possible to receive on FinalHazard
+
+### Bug Fixes:
+
+- Actually swap Intermediate and Expert Chao Races correctly
+- Don't always grant double score for killing Gold Beetles anymore
+- Ensure upgrades are applied properly, even when received while dying
+- Fix the Message Queue getting disordered when receiving many messages in quick succession
+- Fix Logic errors
+ - `City Escape - 3` (Hard Logic) now requires no upgrades
+ - `Mission Street - Pipe 2` (Hard Logic) now requires no upgrades
+ - `Crazy Gadget - Pipe 3` (Hard Logic) now requires no upgrades
+ - `Egg Quarters - 3` (Hard Logic) now requires only `Rouge - Mystic Melody`
+ - `Mad Space - 5` (Hard Logic) now requires no upgrades
+
+
+## v2.0
+
+### Features:
+
+- Completely reworked mission progression system
+ - Control of which mission types can be active per-gameplay-style
+ - Control of how many missions are active per-gameplay-style
+ - Mission order shuffle
+- Two new Chaos Emerald Hunt goals
+ - Chaos Emerald Hunt involves finding the seven Chaos Emeralds and beating Green Hill
+ - FinalHazard Chaos Emerald Hunt is the same, but with the FinalHazard fight at the end of Green Hill
+- New optional Location Checks
+ - Keysanity (Chao Containers)
+ - Whistlesanity (Animal Pipes and hidden whistle spots)
+ - Beetlesanity (Destroying Gold Beetles)
+- Option to require clearing all active Cannon's Core Missions for access to the Biolizard fight in Biolizard goal
+- Hard Logic option
+- More Music Options
+ - Option to use SADX music
+ - New Singularity music shuffle option
+- Option to choose the Narrator theme
+- New Traps
+ - Tiny Trap is now permanent within a level
+ - Gravity Trap
+ - Exposition Trap
+
+### Quality of Life:
+
+- Significant revamp to Stage Select screen information conveyance
+ - Icons are displayed for:
+ - Relevant character's upgrades
+ - Which location checks are active/checked
+ - Chaos Emeralds found (if relevant)
+ - Gate and Cannon's Core emblem costs
+ - The above stage-specific info can also be viewed when paused in-level
+ - The current mission is also displayed when paused
+- Emblem Symbol on Mission Select subscreen now only displays if a high enough rank has been gotten on that mission to send the location check
+- Hints including SA2B locations will now specify which Gate that level is located in
+- Save file now stores slot name to help prevent false location checks in the case of one player having multiple SA2B slots in the same seed
+- Chao Intermediate and Expert race sets are now swapped, per player feedback
+ - Intermediate now includes Beginner + Challenge + Hero + Dark
+ - Expert now includes Beginner + Challenge + Hero + Dark + Jewel
+- New mod config option for the color of the Message Queue text
+
+### Bug Fixes:
+
+- Fixed bug where game stops properly tracking items after 127 have been received.
+- Several logic fixes
+- Game now refers to `Knuckles - Shovel Claws` correctly
+- Minor AP World code cleanup
+
+
+## v1.1
+
+### Features:
+
+- Unlocking each gate of levels requires beating a random boss
+- Chao Races and Karate are now available as an option for checks
+- Junk items can now be put into the itempool
+ - Five Rings
+ - Ten Rings
+ - Twenty Rings
+ - Extra Life
+ - Shield
+ - Magnetic Shield
+ - Invincibility
+- Traps can now be put into the itempool
+ - Omotrap
+ - Chaos Control Trap
+ - Confusion Trap
+ - Tiny Trap
+- The Credits now display a few stats about the run
+- An Option for the minimum required rank for mission checks is now available
+- An Option for influencing the costs of level gates is now available
+
+### Bug Fixes:
+
+- A message will display if the game loses connection to Archipelago
+- The game will gracefully reconnect to Archipelago
+- Kart Race mode is now properly hidden
+- Minor logic fixes
+
+
+## v1.0 - First Stable Release
+
+### Features:
+
+- Goal is to beat Cannon's Core and defeat the Biolizard
+- Locations included:
+ - Upgrade Pickups
+ - Mission Clears
+- Items included:
+ - Character Upgrades
+ - Emblems
+- Levels are unlocked by certain amounts of emblems
+ - An option exists to specify how many missions to include
+- Cannon's Core is unlocked by a certain percentage of existent emblems, depending on an option
+- Music Shuffle is supported
+- DeathLink is supported
diff --git a/worlds/sa2b/GateBosses.py b/worlds/sa2b/GateBosses.py
index e89d4c4557ef..76dd71fa3cd2 100644
--- a/worlds/sa2b/GateBosses.py
+++ b/worlds/sa2b/GateBosses.py
@@ -1,4 +1,6 @@
import typing
+from BaseClasses import MultiWorld
+from worlds.AutoWorld import World
speed_characters_1 = "Sonic vs Shadow 1"
speed_characters_2 = "Sonic vs Shadow 2"
@@ -59,17 +61,17 @@ def boss_has_requirement(boss: int):
return boss >= len(gate_bosses_no_requirements_table)
-def get_gate_bosses(world, player: int):
+def get_gate_bosses(multiworld: MultiWorld, world: World):
selected_bosses: typing.List[int] = []
boss_gates: typing.List[int] = []
available_bosses: typing.List[str] = list(gate_bosses_no_requirements_table.keys())
- world.random.shuffle(available_bosses)
+ multiworld.random.shuffle(available_bosses)
halfway = False
- for x in range(world.number_of_level_gates[player]):
- if (not halfway) and ((x + 1) / world.number_of_level_gates[player]) > 0.5:
+ for x in range(world.options.number_of_level_gates):
+ if (not halfway) and ((x + 1) / world.options.number_of_level_gates) > 0.5:
available_bosses.extend(gate_bosses_with_requirements_table)
- world.random.shuffle(available_bosses)
+ multiworld.random.shuffle(available_bosses)
halfway = True
selected_bosses.append(all_gate_bosses_table[available_bosses[0]])
boss_gates.append(x + 1)
@@ -80,27 +82,27 @@ def get_gate_bosses(world, player: int):
return bosses
-def get_boss_rush_bosses(multiworld, player: int):
+def get_boss_rush_bosses(multiworld: MultiWorld, world: World):
- if multiworld.boss_rush_shuffle[player] == 0:
+ if world.options.boss_rush_shuffle == 0:
boss_list_o = list(range(0, 16))
boss_list_s = [5, 2, 0, 10, 8, 4, 3, 1, 6, 13, 7, 11, 9, 15, 14, 12]
return dict(zip(boss_list_o, boss_list_s))
- elif multiworld.boss_rush_shuffle[player] == 1:
+ elif world.options.boss_rush_shuffle == 1:
boss_list_o = list(range(0, 16))
boss_list_s = boss_list_o.copy()
multiworld.random.shuffle(boss_list_s)
return dict(zip(boss_list_o, boss_list_s))
- elif multiworld.boss_rush_shuffle[player] == 2:
+ elif world.options.boss_rush_shuffle == 2:
boss_list_o = list(range(0, 16))
boss_list_s = [multiworld.random.choice(boss_list_o) for i in range(0, 16)]
if 10 not in boss_list_s:
boss_list_s[multiworld.random.randint(0, 15)] = 10
return dict(zip(boss_list_o, boss_list_s))
- elif multiworld.boss_rush_shuffle[player] == 3:
+ elif world.options.boss_rush_shuffle == 3:
boss_list_o = list(range(0, 16))
boss_list_s = [multiworld.random.choice(boss_list_o)] * len(boss_list_o)
if 10 not in boss_list_s:
diff --git a/worlds/sa2b/Items.py b/worlds/sa2b/Items.py
index 2b862c66afbf..318a57f75394 100644
--- a/worlds/sa2b/Items.py
+++ b/worlds/sa2b/Items.py
@@ -22,7 +22,8 @@ def __init__(self, name, classification: ItemClassification, code: int = None, p
# Separate tables for each type of item.
emblems_table = {
- ItemName.emblem: ItemData(0xFF0000, True),
+ ItemName.emblem: ItemData(0xFF0000, True),
+ ItemName.market_token: ItemData(0xFF001F, True),
}
upgrades_table = {
@@ -82,6 +83,7 @@ def __init__(self, name, classification: ItemClassification, code: int = None, p
ItemName.ice_trap: ItemData(0xFF0037, False, True),
ItemName.slow_trap: ItemData(0xFF0038, False, True),
ItemName.cutscene_trap: ItemData(0xFF0039, False, True),
+ ItemName.reverse_trap: ItemData(0xFF003A, False, True),
ItemName.pong_trap: ItemData(0xFF0050, False, True),
}
@@ -96,6 +98,142 @@ def __init__(self, name, classification: ItemClassification, code: int = None, p
ItemName.blue_emerald: ItemData(0xFF0046, True),
}
+eggs_table = {
+ ItemName.normal_egg: ItemData(0xFF0100, False),
+ ItemName.yellow_monotone_egg: ItemData(0xFF0101, False),
+ ItemName.white_monotone_egg: ItemData(0xFF0102, False),
+ ItemName.brown_monotone_egg: ItemData(0xFF0103, False),
+ ItemName.sky_blue_monotone_egg: ItemData(0xFF0104, False),
+ ItemName.pink_monotone_egg: ItemData(0xFF0105, False),
+ ItemName.blue_monotone_egg: ItemData(0xFF0106, False),
+ ItemName.grey_monotone_egg: ItemData(0xFF0107, False),
+ ItemName.green_monotone_egg: ItemData(0xFF0108, False),
+ ItemName.red_monotone_egg: ItemData(0xFF0109, False),
+ ItemName.lime_green_monotone_egg: ItemData(0xFF010A, False),
+ ItemName.purple_monotone_egg: ItemData(0xFF010B, False),
+ ItemName.orange_monotone_egg: ItemData(0xFF010C, False),
+ ItemName.black_monotone_egg: ItemData(0xFF010D, False),
+
+ ItemName.yellow_twotone_egg: ItemData(0xFF010E, False),
+ ItemName.white_twotone_egg: ItemData(0xFF010F, False),
+ ItemName.brown_twotone_egg: ItemData(0xFF0110, False),
+ ItemName.sky_blue_twotone_egg: ItemData(0xFF0111, False),
+ ItemName.pink_twotone_egg: ItemData(0xFF0112, False),
+ ItemName.blue_twotone_egg: ItemData(0xFF0113, False),
+ ItemName.grey_twotone_egg: ItemData(0xFF0114, False),
+ ItemName.green_twotone_egg: ItemData(0xFF0115, False),
+ ItemName.red_twotone_egg: ItemData(0xFF0116, False),
+ ItemName.lime_green_twotone_egg: ItemData(0xFF0117, False),
+ ItemName.purple_twotone_egg: ItemData(0xFF0118, False),
+ ItemName.orange_twotone_egg: ItemData(0xFF0119, False),
+ ItemName.black_twotone_egg: ItemData(0xFF011A, False),
+
+ ItemName.normal_shiny_egg: ItemData(0xFF011B, False),
+ ItemName.yellow_shiny_egg: ItemData(0xFF011C, False),
+ ItemName.white_shiny_egg: ItemData(0xFF011D, False),
+ ItemName.brown_shiny_egg: ItemData(0xFF011E, False),
+ ItemName.sky_blue_shiny_egg: ItemData(0xFF011F, False),
+ ItemName.pink_shiny_egg: ItemData(0xFF0120, False),
+ ItemName.blue_shiny_egg: ItemData(0xFF0121, False),
+ ItemName.grey_shiny_egg: ItemData(0xFF0122, False),
+ ItemName.green_shiny_egg: ItemData(0xFF0123, False),
+ ItemName.red_shiny_egg: ItemData(0xFF0124, False),
+ ItemName.lime_green_shiny_egg: ItemData(0xFF0125, False),
+ ItemName.purple_shiny_egg: ItemData(0xFF0126, False),
+ ItemName.orange_shiny_egg: ItemData(0xFF0127, False),
+ ItemName.black_shiny_egg: ItemData(0xFF0128, False),
+}
+
+fruits_table = {
+ ItemName.chao_garden_fruit: ItemData(0xFF0200, False),
+ ItemName.hero_garden_fruit: ItemData(0xFF0201, False),
+ ItemName.dark_garden_fruit: ItemData(0xFF0202, False),
+
+ ItemName.strong_fruit: ItemData(0xFF0203, False),
+ ItemName.tasty_fruit: ItemData(0xFF0204, False),
+ ItemName.hero_fruit: ItemData(0xFF0205, False),
+ ItemName.dark_fruit: ItemData(0xFF0206, False),
+ ItemName.round_fruit: ItemData(0xFF0207, False),
+ ItemName.triangle_fruit: ItemData(0xFF0208, False),
+ ItemName.square_fruit: ItemData(0xFF0209, False),
+ ItemName.heart_fruit: ItemData(0xFF020A, False),
+ ItemName.chao_fruit: ItemData(0xFF020B, False),
+ ItemName.smart_fruit: ItemData(0xFF020C, False),
+
+ ItemName.orange_fruit: ItemData(0xFF020D, False),
+ ItemName.blue_fruit: ItemData(0xFF020E, False),
+ ItemName.pink_fruit: ItemData(0xFF020F, False),
+ ItemName.green_fruit: ItemData(0xFF0210, False),
+ ItemName.purple_fruit: ItemData(0xFF0211, False),
+ ItemName.yellow_fruit: ItemData(0xFF0212, False),
+ ItemName.red_fruit: ItemData(0xFF0213, False),
+
+ ItemName.mushroom_fruit: ItemData(0xFF0214, False),
+ ItemName.super_mushroom_fruit: ItemData(0xFF0215, False),
+ ItemName.mint_candy_fruit: ItemData(0xFF0216, False),
+ ItemName.grapes_fruit: ItemData(0xFF0217, False),
+}
+
+seeds_table = {
+ ItemName.strong_seed: ItemData(0xFF0300, False),
+ ItemName.tasty_seed: ItemData(0xFF0301, False),
+ ItemName.hero_seed: ItemData(0xFF0302, False),
+ ItemName.dark_seed: ItemData(0xFF0303, False),
+ ItemName.round_seed: ItemData(0xFF0304, False),
+ ItemName.triangle_seed: ItemData(0xFF0305, False),
+ ItemName.square_seed: ItemData(0xFF0306, False),
+}
+
+hats_table = {
+ ItemName.pumpkin_hat: ItemData(0xFF0401, False),
+ ItemName.skull_hat: ItemData(0xFF0402, False),
+ ItemName.apple_hat: ItemData(0xFF0403, False),
+ ItemName.bucket_hat: ItemData(0xFF0404, False),
+ ItemName.empty_can_hat: ItemData(0xFF0405, False),
+ ItemName.cardboard_box_hat: ItemData(0xFF0406, False),
+ ItemName.flower_pot_hat: ItemData(0xFF0407, False),
+ ItemName.paper_bag_hat: ItemData(0xFF0408, False),
+ ItemName.pan_hat: ItemData(0xFF0409, False),
+ ItemName.stump_hat: ItemData(0xFF040A, False),
+ ItemName.watermelon_hat: ItemData(0xFF040B, False),
+
+ ItemName.red_wool_beanie_hat: ItemData(0xFF040C, False),
+ ItemName.blue_wool_beanie_hat: ItemData(0xFF040D, False),
+ ItemName.black_wool_beanie_hat: ItemData(0xFF040E, False),
+ ItemName.pacifier_hat: ItemData(0xFF040F, False),
+}
+
+animals_table = {
+ ItemName.animal_penguin: ItemData(0xFF0500, False),
+ ItemName.animal_seal: ItemData(0xFF0501, False),
+ ItemName.animal_otter: ItemData(0xFF0502, False),
+ ItemName.animal_rabbit: ItemData(0xFF0503, False),
+ ItemName.animal_cheetah: ItemData(0xFF0504, False),
+ ItemName.animal_warthog: ItemData(0xFF0505, False),
+ ItemName.animal_bear: ItemData(0xFF0506, False),
+ ItemName.animal_tiger: ItemData(0xFF0507, False),
+ ItemName.animal_gorilla: ItemData(0xFF0508, False),
+ ItemName.animal_peacock: ItemData(0xFF0509, False),
+ ItemName.animal_parrot: ItemData(0xFF050A, False),
+ ItemName.animal_condor: ItemData(0xFF050B, False),
+ ItemName.animal_skunk: ItemData(0xFF050C, False),
+ ItemName.animal_sheep: ItemData(0xFF050D, False),
+ ItemName.animal_raccoon: ItemData(0xFF050E, False),
+ ItemName.animal_halffish: ItemData(0xFF050F, False),
+ ItemName.animal_skeleton_dog: ItemData(0xFF0510, False),
+ ItemName.animal_bat: ItemData(0xFF0511, False),
+ ItemName.animal_dragon: ItemData(0xFF0512, False),
+ ItemName.animal_unicorn: ItemData(0xFF0513, False),
+ ItemName.animal_phoenix: ItemData(0xFF0514, False),
+}
+
+chaos_drives_table = {
+ ItemName.chaos_drive_yellow: ItemData(0xFF0515, False),
+ ItemName.chaos_drive_green: ItemData(0xFF0516, False),
+ ItemName.chaos_drive_red: ItemData(0xFF0517, False),
+ ItemName.chaos_drive_purple: ItemData(0xFF0518, False),
+}
+
event_table = {
ItemName.maria: ItemData(0xFF001D, True),
}
@@ -107,12 +245,25 @@ def __init__(self, name, classification: ItemClassification, code: int = None, p
**junk_table,
**trap_table,
**emeralds_table,
+ **eggs_table,
+ **fruits_table,
+ **seeds_table,
+ **hats_table,
+ **animals_table,
+ **chaos_drives_table,
**event_table,
}
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}
-item_groups: typing.Dict[str, str] = {"Chaos Emeralds": [item_name for item_name, data in emeralds_table.items()]}
+item_groups: typing.Dict[str, str] = {
+ "Chaos Emeralds": list(emeralds_table.keys()),
+ "Eggs": list(eggs_table.keys()),
+ "Fruits": list(fruits_table.keys()),
+ "Seeds": list(seeds_table.keys()),
+ "Hats": list(hats_table.keys()),
+ "Traps": list(trap_table.keys()),
+}
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.sonic_light_shoes].code] = "and the Soap Shoes"
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.shadow_air_shoes].code] = "and the Soap Shoes"
diff --git a/worlds/sa2b/Locations.py b/worlds/sa2b/Locations.py
index 461580bb6e39..c928e0c3890e 100644
--- a/worlds/sa2b/Locations.py
+++ b/worlds/sa2b/Locations.py
@@ -1,6 +1,7 @@
import typing
from BaseClasses import Location, MultiWorld
+from worlds.AutoWorld import World
from .Names import LocationName
from .Missions import stage_name_prefixes, mission_orders
@@ -1066,6 +1067,7 @@ class SA2BLocation(Location):
LocationName.final_rush_animal_11: 0xFF0C4F,
LocationName.iron_gate_animal_11: 0xFF0C50,
+ LocationName.dry_lagoon_animal_11: 0xFF0C51,
LocationName.sand_ocean_animal_11: 0xFF0C52,
LocationName.radical_highway_animal_11: 0xFF0C53,
LocationName.lost_colony_animal_11: 0xFF0C55,
@@ -1241,7 +1243,7 @@ class SA2BLocation(Location):
LocationName.boss_rush_16: 0xFF0114,
}
-chao_garden_beginner_location_table = {
+chao_race_beginner_location_table = {
LocationName.chao_race_crab_pool_1: 0xFF0200,
LocationName.chao_race_crab_pool_2: 0xFF0201,
LocationName.chao_race_crab_pool_3: 0xFF0202,
@@ -1254,11 +1256,17 @@ class SA2BLocation(Location):
LocationName.chao_race_block_canyon_1: 0xFF0209,
LocationName.chao_race_block_canyon_2: 0xFF020A,
LocationName.chao_race_block_canyon_3: 0xFF020B,
+}
- LocationName.chao_beginner_karate: 0xFF0300,
+chao_karate_beginner_location_table = {
+ LocationName.chao_beginner_karate_1: 0xFF0300,
+ LocationName.chao_beginner_karate_2: 0xFF0301,
+ LocationName.chao_beginner_karate_3: 0xFF0302,
+ LocationName.chao_beginner_karate_4: 0xFF0303,
+ LocationName.chao_beginner_karate_5: 0xFF0304,
}
-chao_garden_intermediate_location_table = {
+chao_race_intermediate_location_table = {
LocationName.chao_race_challenge_1: 0xFF022A,
LocationName.chao_race_challenge_2: 0xFF022B,
LocationName.chao_race_challenge_3: 0xFF022C,
@@ -1281,11 +1289,17 @@ class SA2BLocation(Location):
LocationName.chao_race_dark_2: 0xFF023B,
LocationName.chao_race_dark_3: 0xFF023C,
LocationName.chao_race_dark_4: 0xFF023D,
+}
- LocationName.chao_standard_karate: 0xFF0301,
+chao_karate_intermediate_location_table = {
+ LocationName.chao_standard_karate_1: 0xFF0305,
+ LocationName.chao_standard_karate_2: 0xFF0306,
+ LocationName.chao_standard_karate_3: 0xFF0307,
+ LocationName.chao_standard_karate_4: 0xFF0308,
+ LocationName.chao_standard_karate_5: 0xFF0309,
}
-chao_garden_expert_location_table = {
+chao_race_expert_location_table = {
LocationName.chao_race_aquamarine_1: 0xFF020C,
LocationName.chao_race_aquamarine_2: 0xFF020D,
LocationName.chao_race_aquamarine_3: 0xFF020E,
@@ -1316,11 +1330,187 @@ class SA2BLocation(Location):
LocationName.chao_race_diamond_3: 0xFF0227,
LocationName.chao_race_diamond_4: 0xFF0228,
LocationName.chao_race_diamond_5: 0xFF0229,
+}
+
+chao_karate_expert_location_table = {
+ LocationName.chao_expert_karate_1: 0xFF030A,
+ LocationName.chao_expert_karate_2: 0xFF030B,
+ LocationName.chao_expert_karate_3: 0xFF030C,
+ LocationName.chao_expert_karate_4: 0xFF030D,
+ LocationName.chao_expert_karate_5: 0xFF030E,
+}
+
+chao_karate_super_location_table = {
+ LocationName.chao_super_karate_1: 0xFF030F,
+ LocationName.chao_super_karate_2: 0xFF0310,
+ LocationName.chao_super_karate_3: 0xFF0311,
+ LocationName.chao_super_karate_4: 0xFF0312,
+ LocationName.chao_super_karate_5: 0xFF0313,
+}
+
+chao_stat_swim_table = { LocationName.chao_stat_swim_base + str(index): (0xFF0E00 + index) for index in range(1,100) }
+chao_stat_fly_table = { LocationName.chao_stat_fly_base + str(index): (0xFF0E80 + index) for index in range(1,100) }
+chao_stat_run_table = { LocationName.chao_stat_run_base + str(index): (0xFF0F00 + index) for index in range(1,100) }
+chao_stat_power_table = { LocationName.chao_stat_power_base + str(index): (0xFF0F80 + index) for index in range(1,100) }
+chao_stat_stamina_table = { LocationName.chao_stat_stamina_base + str(index): (0xFF1000 + index) for index in range(1,100) }
+chao_stat_luck_table = { LocationName.chao_stat_luck_base + str(index): (0xFF1080 + index) for index in range(1,100) }
+chao_stat_intelligence_table = { LocationName.chao_stat_intelligence_base + str(index): (0xFF1100 + index) for index in range(1,100) }
+
+chao_animal_event_location_table = {
+ LocationName.animal_penguin: None,
+ LocationName.animal_seal: None,
+ LocationName.animal_otter: None,
+ LocationName.animal_rabbit: None,
+ LocationName.animal_cheetah: None,
+ LocationName.animal_warthog: None,
+ LocationName.animal_bear: None,
+ LocationName.animal_tiger: None,
+ LocationName.animal_gorilla: None,
+ LocationName.animal_peacock: None,
+ LocationName.animal_parrot: None,
+ LocationName.animal_condor: None,
+ LocationName.animal_skunk: None,
+ LocationName.animal_sheep: None,
+ LocationName.animal_raccoon: None,
+ LocationName.animal_halffish: None,
+ LocationName.animal_skeleton_dog: None,
+ LocationName.animal_bat: None,
+ LocationName.animal_dragon: None,
+ LocationName.animal_unicorn: None,
+ LocationName.animal_phoenix: None,
+}
+
+chao_animal_part_location_table = {
+ LocationName.chao_penguin_arms: 0xFF1220,
+ LocationName.chao_penguin_forehead: 0xFF1222,
+ LocationName.chao_penguin_legs: 0xFF1224,
+
+ LocationName.chao_seal_arms: 0xFF1228,
+ LocationName.chao_seal_tail: 0xFF122E,
+
+ LocationName.chao_otter_arms: 0xFF1230,
+ LocationName.chao_otter_ears: 0xFF1231,
+ LocationName.chao_otter_face: 0xFF1233,
+ LocationName.chao_otter_legs: 0xFF1234,
+ LocationName.chao_otter_tail: 0xFF1236,
+
+ LocationName.chao_rabbit_arms: 0xFF1238,
+ LocationName.chao_rabbit_ears: 0xFF1239,
+ LocationName.chao_rabbit_legs: 0xFF123C,
+ LocationName.chao_rabbit_tail: 0xFF123E,
+
+ LocationName.chao_cheetah_arms: 0xFF1240,
+ LocationName.chao_cheetah_ears: 0xFF1241,
+ LocationName.chao_cheetah_legs: 0xFF1244,
+ LocationName.chao_cheetah_tail: 0xFF1246,
+
+ LocationName.chao_warthog_arms: 0xFF1248,
+ LocationName.chao_warthog_ears: 0xFF1249,
+ LocationName.chao_warthog_face: 0xFF124B,
+ LocationName.chao_warthog_legs: 0xFF124C,
+ LocationName.chao_warthog_tail: 0xFF124E,
+
+ LocationName.chao_bear_arms: 0xFF1250,
+ LocationName.chao_bear_ears: 0xFF1251,
+ LocationName.chao_bear_legs: 0xFF1254,
+
+ LocationName.chao_tiger_arms: 0xFF1258,
+ LocationName.chao_tiger_ears: 0xFF1259,
+ LocationName.chao_tiger_legs: 0xFF125C,
+ LocationName.chao_tiger_tail: 0xFF125E,
+
+ LocationName.chao_gorilla_arms: 0xFF1260,
+ LocationName.chao_gorilla_ears: 0xFF1261,
+ LocationName.chao_gorilla_forehead: 0xFF1262,
+ LocationName.chao_gorilla_legs: 0xFF1264,
+
+ LocationName.chao_peacock_forehead: 0xFF126A,
+ LocationName.chao_peacock_legs: 0xFF126C,
+ LocationName.chao_peacock_tail: 0xFF126E,
+ LocationName.chao_peacock_wings: 0xFF126F,
+
+ LocationName.chao_parrot_forehead: 0xFF1272,
+ LocationName.chao_parrot_legs: 0xFF1274,
+ LocationName.chao_parrot_tail: 0xFF1276,
+ LocationName.chao_parrot_wings: 0xFF1277,
+
+ LocationName.chao_condor_ears: 0xFF1279,
+ LocationName.chao_condor_legs: 0xFF127C,
+ LocationName.chao_condor_tail: 0xFF127E,
+ LocationName.chao_condor_wings: 0xFF127F,
+
+ LocationName.chao_skunk_arms: 0xFF1280,
+ LocationName.chao_skunk_forehead: 0xFF1282,
+ LocationName.chao_skunk_legs: 0xFF1284,
+ LocationName.chao_skunk_tail: 0xFF1286,
+
+ LocationName.chao_sheep_arms: 0xFF1288,
+ LocationName.chao_sheep_ears: 0xFF1289,
+ LocationName.chao_sheep_legs: 0xFF128C,
+ LocationName.chao_sheep_horn: 0xFF128D,
+ LocationName.chao_sheep_tail: 0xFF128E,
+
+ LocationName.chao_raccoon_arms: 0xFF1290,
+ LocationName.chao_raccoon_ears: 0xFF1291,
+ LocationName.chao_raccoon_legs: 0xFF1294,
+
+ LocationName.chao_dragon_arms: 0xFF12A0,
+ LocationName.chao_dragon_ears: 0xFF12A1,
+ LocationName.chao_dragon_legs: 0xFF12A4,
+ LocationName.chao_dragon_horn: 0xFF12A5,
+ LocationName.chao_dragon_tail: 0xFF12A6,
+ LocationName.chao_dragon_wings: 0xFF12A7,
+
+ LocationName.chao_unicorn_arms: 0xFF12A8,
+ LocationName.chao_unicorn_ears: 0xFF12A9,
+ LocationName.chao_unicorn_forehead: 0xFF12AA,
+ LocationName.chao_unicorn_legs: 0xFF12AC,
+ LocationName.chao_unicorn_tail: 0xFF12AE,
+
+ LocationName.chao_phoenix_forehead: 0xFF12B2,
+ LocationName.chao_phoenix_legs: 0xFF12B4,
+ LocationName.chao_phoenix_tail: 0xFF12B6,
+ LocationName.chao_phoenix_wings: 0xFF12B7,
+}
+
+chao_kindergarten_location_table = {
+ LocationName.chao_kindergarten_drawing_1: 0xFF12D0,
+ LocationName.chao_kindergarten_drawing_2: 0xFF12D1,
+ LocationName.chao_kindergarten_drawing_3: 0xFF12D2,
+ LocationName.chao_kindergarten_drawing_4: 0xFF12D3,
+ LocationName.chao_kindergarten_drawing_5: 0xFF12D4,
+
+ LocationName.chao_kindergarten_shake_dance: 0xFF12D8,
+ LocationName.chao_kindergarten_spin_dance: 0xFF12D9,
+ LocationName.chao_kindergarten_step_dance: 0xFF12DA,
+ LocationName.chao_kindergarten_gogo_dance: 0xFF12DB,
+ LocationName.chao_kindergarten_exercise: 0xFF12DC,
+
+ LocationName.chao_kindergarten_song_1: 0xFF12E0,
+ LocationName.chao_kindergarten_song_2: 0xFF12E1,
+ LocationName.chao_kindergarten_song_3: 0xFF12E2,
+ LocationName.chao_kindergarten_song_4: 0xFF12E3,
+ LocationName.chao_kindergarten_song_5: 0xFF12E4,
+
+ LocationName.chao_kindergarten_bell: 0xFF12E8,
+ LocationName.chao_kindergarten_castanets: 0xFF12E9,
+ LocationName.chao_kindergarten_cymbals: 0xFF12EA,
+ LocationName.chao_kindergarten_drum: 0xFF12EB,
+ LocationName.chao_kindergarten_flute: 0xFF12EC,
+ LocationName.chao_kindergarten_maracas: 0xFF12ED,
+ LocationName.chao_kindergarten_trumpet: 0xFF12EE,
+ LocationName.chao_kindergarten_tambourine: 0xFF12EF,
+}
- LocationName.chao_expert_karate: 0xFF0302,
- LocationName.chao_super_karate: 0xFF0303,
+chao_kindergarten_basics_location_table = {
+ LocationName.chao_kindergarten_any_drawing: 0xFF12F0,
+ LocationName.chao_kindergarten_any_dance: 0xFF12F1,
+ LocationName.chao_kindergarten_any_song: 0xFF12F2,
+ LocationName.chao_kindergarten_any_instrument: 0xFF12F3,
}
+black_market_location_table = { LocationName.chao_black_market_base + str(index): (0xFF1300 + index) for index in range(1,65) }
+
kart_race_beginner_location_table = {
LocationName.kart_race_beginner_sonic: 0xFF0A00,
LocationName.kart_race_beginner_tails: 0xFF0A01,
@@ -1375,6 +1565,10 @@ class SA2BLocation(Location):
LocationName.grand_prix: 0xFF007F,
}
+chaos_chao_location_table = {
+ LocationName.chaos_chao: 0xFF009F,
+}
+
all_locations = {
**mission_location_table,
**upgrade_location_table,
@@ -1386,9 +1580,13 @@ class SA2BLocation(Location):
**beetle_location_table,
**omochao_location_table,
**animal_location_table,
- **chao_garden_beginner_location_table,
- **chao_garden_intermediate_location_table,
- **chao_garden_expert_location_table,
+ **chao_race_beginner_location_table,
+ **chao_karate_beginner_location_table,
+ **chao_race_intermediate_location_table,
+ **chao_karate_intermediate_location_table,
+ **chao_race_expert_location_table,
+ **chao_karate_expert_location_table,
+ **chao_karate_super_location_table,
**kart_race_beginner_location_table,
**kart_race_standard_location_table,
**kart_race_expert_location_table,
@@ -1398,6 +1596,18 @@ class SA2BLocation(Location):
**green_hill_animal_location_table,
**final_boss_location_table,
**grand_prix_location_table,
+ **chaos_chao_location_table,
+ **chao_stat_swim_table,
+ **chao_stat_fly_table,
+ **chao_stat_run_table,
+ **chao_stat_power_table,
+ **chao_stat_stamina_table,
+ **chao_stat_luck_table,
+ **chao_stat_intelligence_table,
+ **chao_animal_part_location_table,
+ **chao_kindergarten_location_table,
+ **chao_kindergarten_basics_location_table,
+ **black_market_location_table,
}
boss_gate_set = [
@@ -1408,13 +1618,6 @@ class SA2BLocation(Location):
LocationName.gate_5_boss,
]
-chao_karate_set = [
- LocationName.chao_beginner_karate,
- LocationName.chao_standard_karate,
- LocationName.chao_expert_karate,
- LocationName.chao_super_karate,
-]
-
chao_race_prize_set = [
LocationName.chao_race_crab_pool_3,
LocationName.chao_race_stump_valley_3,
@@ -1437,19 +1640,24 @@ class SA2BLocation(Location):
LocationName.chao_race_dark_2,
LocationName.chao_race_dark_4,
+
+ LocationName.chao_beginner_karate_5,
+ LocationName.chao_standard_karate_5,
+ LocationName.chao_expert_karate_5,
+ LocationName.chao_super_karate_5,
]
-def setup_locations(world: MultiWorld, player: int, mission_map: typing.Dict[int, int], mission_count_map: typing.Dict[int, int]):
+def setup_locations(world: World, player: int, mission_map: typing.Dict[int, int], mission_count_map: typing.Dict[int, int]):
location_table = {}
chao_location_table = {}
- if world.goal[player] == 3:
- if world.kart_race_checks[player] == 2:
+ if world.options.goal == 3:
+ if world.options.kart_race_checks == 2:
location_table.update({**kart_race_beginner_location_table})
location_table.update({**kart_race_standard_location_table})
location_table.update({**kart_race_expert_location_table})
- elif world.kart_race_checks[player] == 1:
+ elif world.options.kart_race_checks == 1:
location_table.update({**kart_race_mini_location_table})
location_table.update({**grand_prix_location_table})
else:
@@ -1465,67 +1673,100 @@ def setup_locations(world: MultiWorld, player: int, mission_map: typing.Dict[int
location_table.update({**upgrade_location_table})
- if world.keysanity[player]:
+ if world.options.keysanity:
location_table.update({**chao_key_location_table})
- if world.whistlesanity[player].value == 1:
+ if world.options.whistlesanity.value == 1:
location_table.update({**pipe_location_table})
- elif world.whistlesanity[player].value == 2:
+ elif world.options.whistlesanity.value == 2:
location_table.update({**hidden_whistle_location_table})
- elif world.whistlesanity[player].value == 3:
+ elif world.options.whistlesanity.value == 3:
location_table.update({**pipe_location_table})
location_table.update({**hidden_whistle_location_table})
- if world.beetlesanity[player]:
+ if world.options.beetlesanity:
location_table.update({**beetle_location_table})
- if world.omosanity[player]:
+ if world.options.omosanity:
location_table.update({**omochao_location_table})
- if world.animalsanity[player]:
+ if world.options.animalsanity:
location_table.update({**animal_location_table})
- if world.kart_race_checks[player] == 2:
+ if world.options.kart_race_checks == 2:
location_table.update({**kart_race_beginner_location_table})
location_table.update({**kart_race_standard_location_table})
location_table.update({**kart_race_expert_location_table})
- elif world.kart_race_checks[player] == 1:
+ elif world.options.kart_race_checks == 1:
location_table.update({**kart_race_mini_location_table})
- if world.goal[player].value in [0, 2, 4, 5, 6]:
+ if world.options.goal.value in [0, 2, 4, 5, 6]:
location_table.update({**final_boss_location_table})
+ elif world.options.goal.value in [7]:
+ location_table.update({**chaos_chao_location_table})
- if world.goal[player].value in [1, 2]:
+ if world.options.goal.value in [1, 2]:
location_table.update({**green_hill_location_table})
- if world.keysanity[player]:
+ if world.options.keysanity:
location_table.update({**green_hill_chao_location_table})
- if world.animalsanity[player]:
+ if world.options.animalsanity:
location_table.update({**green_hill_animal_location_table})
- if world.goal[player].value in [4, 5, 6]:
+ if world.options.goal.value in [4, 5, 6]:
location_table.update({**boss_rush_location_table})
- if world.chao_garden_difficulty[player].value >= 1:
- chao_location_table.update({**chao_garden_beginner_location_table})
- if world.chao_garden_difficulty[player].value >= 2:
- chao_location_table.update({**chao_garden_intermediate_location_table})
- if world.chao_garden_difficulty[player].value >= 3:
- chao_location_table.update({**chao_garden_expert_location_table})
+ if world.options.chao_race_difficulty.value >= 1:
+ chao_location_table.update({**chao_race_beginner_location_table})
+ if world.options.chao_race_difficulty.value >= 2:
+ chao_location_table.update({**chao_race_intermediate_location_table})
+ if world.options.chao_race_difficulty.value >= 3:
+ chao_location_table.update({**chao_race_expert_location_table})
+
+ if world.options.chao_karate_difficulty.value >= 1:
+ chao_location_table.update({**chao_karate_beginner_location_table})
+ if world.options.chao_karate_difficulty.value >= 2:
+ chao_location_table.update({**chao_karate_intermediate_location_table})
+ if world.options.chao_karate_difficulty.value >= 3:
+ chao_location_table.update({**chao_karate_expert_location_table})
+ if world.options.chao_karate_difficulty.value >= 4:
+ chao_location_table.update({**chao_karate_super_location_table})
for key, value in chao_location_table.items():
- if key in chao_karate_set:
- if world.include_chao_karate[player]:
- location_table[key] = value
- elif key not in chao_race_prize_set:
- if world.chao_race_checks[player] == "all":
+ if key not in chao_race_prize_set:
+ if world.options.chao_stadium_checks == "all":
location_table[key] = value
else:
location_table[key] = value
+ for index in range(1, world.options.chao_stats.value + 1):
+ if (index % world.options.chao_stats_frequency.value) == (world.options.chao_stats.value % world.options.chao_stats_frequency.value):
+ location_table[LocationName.chao_stat_swim_base + str(index)] = chao_stat_swim_table[ LocationName.chao_stat_swim_base + str(index)]
+ location_table[LocationName.chao_stat_fly_base + str(index)] = chao_stat_fly_table[ LocationName.chao_stat_fly_base + str(index)]
+ location_table[LocationName.chao_stat_run_base + str(index)] = chao_stat_run_table[ LocationName.chao_stat_run_base + str(index)]
+ location_table[LocationName.chao_stat_power_base + str(index)] = chao_stat_power_table[ LocationName.chao_stat_power_base + str(index)]
+
+ if world.options.chao_stats_stamina:
+ location_table[LocationName.chao_stat_stamina_base + str(index)] = chao_stat_stamina_table[LocationName.chao_stat_stamina_base + str(index)]
+
+ if world.options.chao_stats_hidden:
+ location_table[LocationName.chao_stat_luck_base + str(index)] = chao_stat_luck_table[ LocationName.chao_stat_luck_base + str(index)]
+ location_table[LocationName.chao_stat_intelligence_base + str(index)] = chao_stat_intelligence_table[LocationName.chao_stat_intelligence_base + str(index)]
+
+ if world.options.chao_animal_parts:
+ location_table.update({**chao_animal_part_location_table})
+
+ if world.options.chao_kindergarten.value == 1:
+ location_table.update({**chao_kindergarten_basics_location_table})
+ elif world.options.chao_kindergarten.value == 2:
+ location_table.update({**chao_kindergarten_location_table})
+
+ for index in range(1, world.options.black_market_slots.value + 1):
+ location_table[LocationName.chao_black_market_base + str(index)] = black_market_location_table[LocationName.chao_black_market_base + str(index)]
+
for x in range(len(boss_gate_set)):
- if x < world.number_of_level_gates[player].value:
+ if x < world.options.number_of_level_gates.value:
location_table[boss_gate_set[x]] = boss_gate_location_table[boss_gate_set[x]]
return location_table
diff --git a/worlds/sa2b/Missions.py b/worlds/sa2b/Missions.py
index 1fcd2aed87c4..5ee48d564015 100644
--- a/worlds/sa2b/Missions.py
+++ b/worlds/sa2b/Missions.py
@@ -2,6 +2,7 @@
import copy
from BaseClasses import MultiWorld
+from worlds.AutoWorld import World
mission_orders: typing.List[typing.List[int]] = [
@@ -193,10 +194,10 @@
"Cannon's Core - ",
]
-def get_mission_count_table(multiworld: MultiWorld, player: int):
+def get_mission_count_table(multiworld: MultiWorld, world: World, player: int):
mission_count_table: typing.Dict[int, int] = {}
- if multiworld.goal[player] == 3:
+ if world.options.goal == 3:
for level in range(31):
mission_count_table[level] = 0
else:
@@ -207,26 +208,26 @@ def get_mission_count_table(multiworld: MultiWorld, player: int):
cannons_core_active_missions = 1
for i in range(2,6):
- if getattr(multiworld, "speed_mission_" + str(i), None)[player]:
+ if getattr(world.options, "speed_mission_" + str(i), None):
speed_active_missions += 1
- if getattr(multiworld, "mech_mission_" + str(i), None)[player]:
+ if getattr(world.options, "mech_mission_" + str(i), None):
mech_active_missions += 1
- if getattr(multiworld, "hunt_mission_" + str(i), None)[player]:
+ if getattr(world.options, "hunt_mission_" + str(i), None):
hunt_active_missions += 1
- if getattr(multiworld, "kart_mission_" + str(i), None)[player]:
+ if getattr(world.options, "kart_mission_" + str(i), None):
kart_active_missions += 1
- if getattr(multiworld, "cannons_core_mission_" + str(i), None)[player]:
+ if getattr(world.options, "cannons_core_mission_" + str(i), None):
cannons_core_active_missions += 1
- speed_active_missions = min(speed_active_missions, multiworld.speed_mission_count[player].value)
- mech_active_missions = min(mech_active_missions, multiworld.mech_mission_count[player].value)
- hunt_active_missions = min(hunt_active_missions, multiworld.hunt_mission_count[player].value)
- kart_active_missions = min(kart_active_missions, multiworld.kart_mission_count[player].value)
- cannons_core_active_missions = min(cannons_core_active_missions, multiworld.cannons_core_mission_count[player].value)
+ speed_active_missions = min(speed_active_missions, world.options.speed_mission_count.value)
+ mech_active_missions = min(mech_active_missions, world.options.mech_mission_count.value)
+ hunt_active_missions = min(hunt_active_missions, world.options.hunt_mission_count.value)
+ kart_active_missions = min(kart_active_missions, world.options.kart_mission_count.value)
+ cannons_core_active_missions = min(cannons_core_active_missions, world.options.cannons_core_mission_count.value)
active_missions: typing.List[typing.List[int]] = [
speed_active_missions,
@@ -244,10 +245,10 @@ def get_mission_count_table(multiworld: MultiWorld, player: int):
return mission_count_table
-def get_mission_table(multiworld: MultiWorld, player: int):
+def get_mission_table(multiworld: MultiWorld, world: World, player: int):
mission_table: typing.Dict[int, int] = {}
- if multiworld.goal[player] == 3:
+ if world.options.goal == 3:
for level in range(31):
mission_table[level] = 0
else:
@@ -259,19 +260,19 @@ def get_mission_table(multiworld: MultiWorld, player: int):
# Add included missions
for i in range(2,6):
- if getattr(multiworld, "speed_mission_" + str(i), None)[player]:
+ if getattr(world.options, "speed_mission_" + str(i), None):
speed_active_missions.append(i)
- if getattr(multiworld, "mech_mission_" + str(i), None)[player]:
+ if getattr(world.options, "mech_mission_" + str(i), None):
mech_active_missions.append(i)
- if getattr(multiworld, "hunt_mission_" + str(i), None)[player]:
+ if getattr(world.options, "hunt_mission_" + str(i), None):
hunt_active_missions.append(i)
- if getattr(multiworld, "kart_mission_" + str(i), None)[player]:
+ if getattr(world.options, "kart_mission_" + str(i), None):
kart_active_missions.append(i)
- if getattr(multiworld, "cannons_core_mission_" + str(i), None)[player]:
+ if getattr(world.options, "cannons_core_mission_" + str(i), None):
cannons_core_active_missions.append(i)
active_missions: typing.List[typing.List[int]] = [
@@ -292,10 +293,10 @@ def get_mission_table(multiworld: MultiWorld, player: int):
first_mission = 1
first_mission_options = [1, 2, 3]
- if not multiworld.animalsanity[player]:
+ if not world.options.animalsanity:
first_mission_options.append(4)
- if multiworld.mission_shuffle[player]:
+ if world.options.mission_shuffle:
first_mission = multiworld.random.choice([mission for mission in level_active_missions if mission in first_mission_options])
level_active_missions.remove(first_mission)
@@ -305,7 +306,7 @@ def get_mission_table(multiworld: MultiWorld, player: int):
if mission not in level_chosen_missions:
level_chosen_missions.append(mission)
- if multiworld.mission_shuffle[player]:
+ if world.options.mission_shuffle:
multiworld.random.shuffle(level_chosen_missions)
level_chosen_missions.insert(0, first_mission)
diff --git a/worlds/sa2b/Names/ItemName.py b/worlds/sa2b/Names/ItemName.py
index eb088ceb4057..c6de98183b9d 100644
--- a/worlds/sa2b/Names/ItemName.py
+++ b/worlds/sa2b/Names/ItemName.py
@@ -1,6 +1,9 @@
# Emblem Definition
emblem = "Emblem"
+# Market Token Definition
+market_token = "Chao Coin"
+
# Upgrade Definitions
sonic_gloves = "Sonic - Magic Glove"
sonic_light_shoes = "Sonic - Light Shoes"
@@ -36,6 +39,8 @@
rouge_treasure_scope = "Rouge - Treasure Scope"
rouge_iron_boots = "Rouge - Iron Boots"
+
+# Junk
five_rings = "Five Rings"
ten_rings = "Ten Rings"
twenty_rings = "Twenty Rings"
@@ -44,6 +49,8 @@
magnetic_shield = "Magnetic Shield"
invincibility = "Invincibility"
+
+# Traps
omochao_trap = "OmoTrap"
timestop_trap = "Chaos Control Trap"
confuse_trap = "Confusion Trap"
@@ -54,9 +61,12 @@
ice_trap = "Ice Trap"
slow_trap = "Slow Trap"
cutscene_trap = "Cutscene Trap"
+reverse_trap = "Reverse Trap"
pong_trap = "Pong Trap"
+
+# Chaos Emeralds
white_emerald = "White Chaos Emerald"
red_emerald = "Red Chaos Emerald"
cyan_emerald = "Cyan Chaos Emerald"
@@ -65,4 +75,140 @@
yellow_emerald = "Yellow Chaos Emerald"
blue_emerald = "Blue Chaos Emerald"
+
+# Chao Eggs
+normal_egg = "Normal Egg"
+yellow_monotone_egg = "Yellow Mono-Tone Egg"
+white_monotone_egg = "White Mono-Tone Egg"
+brown_monotone_egg = "Brown Mono-Tone Egg"
+sky_blue_monotone_egg = "Sky Blue Mono-Tone Egg"
+pink_monotone_egg = "Pink Mono-Tone Egg"
+blue_monotone_egg = "Blue Mono-Tone Egg"
+grey_monotone_egg = "Grey Mono-Tone Egg"
+green_monotone_egg = "Green Mono-Tone Egg"
+red_monotone_egg = "Red Mono-Tone Egg"
+lime_green_monotone_egg = "Lime Green Mono-Tone Egg"
+purple_monotone_egg = "Purple Mono-Tone Egg"
+orange_monotone_egg = "Orange Mono-Tone Egg"
+black_monotone_egg = "Black Mono-Tone Egg"
+
+yellow_twotone_egg = "Yellow Two-Tone Egg"
+white_twotone_egg = "White Two-Tone Egg"
+brown_twotone_egg = "Brown Two-Tone Egg"
+sky_blue_twotone_egg = "Sky Blue Two-Tone Egg"
+pink_twotone_egg = "Pink Two-Tone Egg"
+blue_twotone_egg = "Blue Two-Tone Egg"
+grey_twotone_egg = "Grey Two-Tone Egg"
+green_twotone_egg = "Green Two-Tone Egg"
+red_twotone_egg = "Red Two-Tone Egg"
+lime_green_twotone_egg = "Lime Green Two-Tone Egg"
+purple_twotone_egg = "Purple Two-Tone Egg"
+orange_twotone_egg = "Orange Two-Tone Egg"
+black_twotone_egg = "Black Two-Tone Egg"
+
+normal_shiny_egg = "Normal Shiny Egg"
+yellow_shiny_egg = "Yellow Shiny Egg"
+white_shiny_egg = "White Shiny Egg"
+brown_shiny_egg = "Brown Shiny Egg"
+sky_blue_shiny_egg = "Sky Blue Shiny Egg"
+pink_shiny_egg = "Pink Shiny Egg"
+blue_shiny_egg = "Blue Shiny Egg"
+grey_shiny_egg = "Grey Shiny Egg"
+green_shiny_egg = "Green Shiny Egg"
+red_shiny_egg = "Red Shiny Egg"
+lime_green_shiny_egg = "Lime Green Shiny Egg"
+purple_shiny_egg = "Purple Shiny Egg"
+orange_shiny_egg = "Orange Shiny Egg"
+black_shiny_egg = "Black Shiny Egg"
+
+
+# Chao Fruit
+chao_garden_fruit = "Chao Garden Fruit"
+hero_garden_fruit = "Hero Garden Fruit"
+dark_garden_fruit = "Dark Garden Fruit"
+
+strong_fruit = "Strong Fruit"
+tasty_fruit = "Tasty Fruit"
+hero_fruit = "Hero Fruit"
+dark_fruit = "Dark Fruit"
+round_fruit = "Round Fruit"
+triangle_fruit = "Triangle Fruit"
+square_fruit = "Square Fruit"
+heart_fruit = "Heart Fruit"
+chao_fruit = "Chao Fruit"
+smart_fruit = "Smart Fruit"
+
+orange_fruit = "Orange Fruit"
+blue_fruit = "Blue Fruit"
+pink_fruit = "Pink Fruit"
+green_fruit = "Green Fruit"
+purple_fruit = "Purple Fruit"
+yellow_fruit = "Yellow Fruit"
+red_fruit = "Red Fruit"
+
+mushroom_fruit = "Mushroom"
+super_mushroom_fruit = "Super Mushroom"
+mint_candy_fruit = "Mint Candy"
+grapes_fruit = "Grapes"
+
+
+# Chao Seeds
+strong_seed = "Strong Seed"
+tasty_seed = "Tasty Seed"
+hero_seed = "Hero Seed"
+dark_seed = "Dark Seed"
+round_seed = "Round Seed"
+triangle_seed = "Triangle Seed"
+square_seed = "Square Seed"
+
+
+# Chao Hats
+pumpkin_hat = "Pumpkin"
+skull_hat = "Skull"
+apple_hat = "Apple"
+bucket_hat = "Bucket"
+empty_can_hat = "Empty Can"
+cardboard_box_hat = "Cardboard Box"
+flower_pot_hat = "Flower Pot"
+paper_bag_hat = "Paper Bag"
+pan_hat = "Pan"
+stump_hat = "Stump"
+watermelon_hat = "Watermelon"
+
+red_wool_beanie_hat = "Red Wool Beanie"
+blue_wool_beanie_hat = "Blue Wool Beanie"
+black_wool_beanie_hat = "Black Wool Beanie"
+pacifier_hat = "Pacifier"
+
+
+# Animal Items
+animal_penguin = "Penguin"
+animal_seal = "Seal"
+animal_otter = "Otter"
+animal_rabbit = "Rabbit"
+animal_cheetah = "Cheetah"
+animal_warthog = "Warthog"
+animal_bear = "Bear"
+animal_tiger = "Tiger"
+animal_gorilla = "Gorilla"
+animal_peacock = "Peacock"
+animal_parrot = "Parrot"
+animal_condor = "Condor"
+animal_skunk = "Skunk"
+animal_sheep = "Sheep"
+animal_raccoon = "Raccoon"
+animal_halffish = "HalfFish"
+animal_skeleton_dog = "Skeleton Dog"
+animal_bat = "Bat"
+animal_dragon = "Dragon"
+animal_unicorn = "Unicorn"
+animal_phoenix = "Phoenix"
+
+chaos_drive_yellow = "Yellow Chaos Drive"
+chaos_drive_green = "Green Chaos Drive"
+chaos_drive_red = "Red Chaos Drive"
+chaos_drive_purple = "Purple Chaos Drive"
+
+
+# Goal Item
maria = "What Maria Wanted"
diff --git a/worlds/sa2b/Names/LocationName.py b/worlds/sa2b/Names/LocationName.py
index f0638430fc50..bde25a8a7597 100644
--- a/worlds/sa2b/Names/LocationName.py
+++ b/worlds/sa2b/Names/LocationName.py
@@ -909,6 +909,7 @@
dry_lagoon_animal_8 = "Dry Lagoon - 8 Animals"
dry_lagoon_animal_9 = "Dry Lagoon - 9 Animals"
dry_lagoon_animal_10 = "Dry Lagoon - 10 Animals"
+dry_lagoon_animal_11 = "Dry Lagoon - 11 Animals"
dry_lagoon_upgrade = "Dry Lagoon - Upgrade"
egg_quarters_1 = "Egg Quarters - 1"
egg_quarters_2 = "Egg Quarters - 2"
@@ -1150,10 +1151,190 @@
chao_race_dark_3 = "Chao Race - Dark 3"
chao_race_dark_4 = "Chao Race - Dark 4"
-chao_beginner_karate = "Chao Karate - Beginner"
-chao_standard_karate = "Chao Karate - Standard"
-chao_expert_karate = "Chao Karate - Expert"
-chao_super_karate = "Chao Karate - Super"
+chao_beginner_karate_1 = "Chao Karate - Beginner 1"
+chao_beginner_karate_2 = "Chao Karate - Beginner 2"
+chao_beginner_karate_3 = "Chao Karate - Beginner 3"
+chao_beginner_karate_4 = "Chao Karate - Beginner 4"
+chao_beginner_karate_5 = "Chao Karate - Beginner 5"
+chao_standard_karate_1 = "Chao Karate - Standard 1"
+chao_standard_karate_2 = "Chao Karate - Standard 2"
+chao_standard_karate_3 = "Chao Karate - Standard 3"
+chao_standard_karate_4 = "Chao Karate - Standard 4"
+chao_standard_karate_5 = "Chao Karate - Standard 5"
+chao_expert_karate_1 = "Chao Karate - Expert 1"
+chao_expert_karate_2 = "Chao Karate - Expert 2"
+chao_expert_karate_3 = "Chao Karate - Expert 3"
+chao_expert_karate_4 = "Chao Karate - Expert 4"
+chao_expert_karate_5 = "Chao Karate - Expert 5"
+chao_super_karate_1 = "Chao Karate - Super 1"
+chao_super_karate_2 = "Chao Karate - Super 2"
+chao_super_karate_3 = "Chao Karate - Super 3"
+chao_super_karate_4 = "Chao Karate - Super 4"
+chao_super_karate_5 = "Chao Karate - Super 5"
+
+chao_stat_swim_base = "Chao Stat - Swim - "
+chao_stat_fly_base = "Chao Stat - Fly - "
+chao_stat_run_base = "Chao Stat - Run - "
+chao_stat_power_base = "Chao Stat - Power - "
+chao_stat_stamina_base = "Chao Stat - Stamina - "
+chao_stat_luck_base = "Chao Stat - Luck - "
+chao_stat_intelligence_base = "Chao Stat - Intelligence - "
+
+chao_black_market_base = "Black Market - "
+
+# Animal Event Locations
+animal_penguin = "Penguin Behavior"
+animal_seal = "Seal Behavior"
+animal_otter = "Otter Behavior"
+animal_rabbit = "Rabbit Behavior"
+animal_cheetah = "Cheetah Behavior"
+animal_warthog = "Warthog Behavior"
+animal_bear = "Bear Behavior"
+animal_tiger = "Tiger Behavior"
+animal_gorilla = "Gorilla Behavior"
+animal_peacock = "Peacock Behavior"
+animal_parrot = "Parrot Behavior"
+animal_condor = "Condor Behavior"
+animal_skunk = "Skunk Behavior"
+animal_sheep = "Sheep Behavior"
+animal_raccoon = "Raccoon Behavior"
+animal_halffish = "HalfFish Behavior"
+animal_skeleton_dog = "Skeleton Dog Behavior"
+animal_bat = "Bat Behavior"
+animal_dragon = "Dragon Behavior"
+animal_unicorn = "Unicorn Behavior"
+animal_phoenix = "Phoenix Behavior"
+
+# Animal Body Part Locations
+chao_penguin_arms = "Chao - Penguin Arms"
+chao_penguin_forehead = "Chao - Penguin Forehead"
+chao_penguin_legs = "Chao - Penguin Legs"
+
+chao_seal_arms = "Chao - Seal Arms"
+chao_seal_tail = "Chao - Seal Tail"
+
+chao_otter_arms = "Chao - Otter Arms"
+chao_otter_ears = "Chao - Otter Ears"
+chao_otter_face = "Chao - Otter Face"
+chao_otter_legs = "Chao - Otter Legs"
+chao_otter_tail = "Chao - Otter Tail"
+
+chao_rabbit_arms = "Chao - Rabbit Arms"
+chao_rabbit_ears = "Chao - Rabbit Ears"
+chao_rabbit_legs = "Chao - Rabbit Legs"
+chao_rabbit_tail = "Chao - Rabbit Tail"
+
+chao_cheetah_arms = "Chao - Cheetah Arms"
+chao_cheetah_ears = "Chao - Cheetah Ears"
+chao_cheetah_legs = "Chao - Cheetah Legs"
+chao_cheetah_tail = "Chao - Cheetah Tail"
+
+chao_warthog_arms = "Chao - Warthog Arms"
+chao_warthog_ears = "Chao - Warthog Ears"
+chao_warthog_face = "Chao - Warthog Face"
+chao_warthog_legs = "Chao - Warthog Legs"
+chao_warthog_tail = "Chao - Warthog Tail"
+
+chao_bear_arms = "Chao - Bear Arms"
+chao_bear_ears = "Chao - Bear Ears"
+chao_bear_legs = "Chao - Bear Legs"
+
+chao_tiger_arms = "Chao - Tiger Arms"
+chao_tiger_ears = "Chao - Tiger Ears"
+chao_tiger_legs = "Chao - Tiger Legs"
+chao_tiger_tail = "Chao - Tiger Tail"
+
+chao_gorilla_arms = "Chao - Gorilla Arms"
+chao_gorilla_ears = "Chao - Gorilla Ears"
+chao_gorilla_forehead = "Chao - Gorilla Forehead"
+chao_gorilla_legs = "Chao - Gorilla Legs"
+
+chao_peacock_forehead = "Chao - Peacock Forehead"
+chao_peacock_legs = "Chao - Peacock Legs"
+chao_peacock_tail = "Chao - Peacock Tail"
+chao_peacock_wings = "Chao - Peacock Wings"
+
+chao_parrot_forehead = "Chao - Parrot Forehead"
+chao_parrot_legs = "Chao - Parrot Legs"
+chao_parrot_tail = "Chao - Parrot Tail"
+chao_parrot_wings = "Chao - Parrot Wings"
+
+chao_condor_ears = "Chao - Condor Ears"
+chao_condor_legs = "Chao - Condor Legs"
+chao_condor_tail = "Chao - Condor Tail"
+chao_condor_wings = "Chao - Condor Wings"
+
+chao_skunk_arms = "Chao - Skunk Arms"
+chao_skunk_forehead = "Chao - Skunk Forehead"
+chao_skunk_legs = "Chao - Skunk Legs"
+chao_skunk_tail = "Chao - Skunk Tail"
+
+chao_sheep_arms = "Chao - Sheep Arms"
+chao_sheep_ears = "Chao - Sheep Ears"
+chao_sheep_legs = "Chao - Sheep Legs"
+chao_sheep_horn = "Chao - Sheep Horn"
+chao_sheep_tail = "Chao - Sheep Tail"
+
+chao_raccoon_arms = "Chao - Raccoon Arms"
+chao_raccoon_ears = "Chao - Raccoon Ears"
+chao_raccoon_legs = "Chao - Raccoon Legs"
+
+chao_dragon_arms = "Chao - Dragon Arms"
+chao_dragon_ears = "Chao - Dragon Ears"
+chao_dragon_legs = "Chao - Dragon Legs"
+chao_dragon_horn = "Chao - Dragon Horn"
+chao_dragon_tail = "Chao - Dragon Tail"
+chao_dragon_wings = "Chao - Dragon Wings"
+
+chao_unicorn_arms = "Chao - Unicorn Arms"
+chao_unicorn_ears = "Chao - Unicorn Ears"
+chao_unicorn_forehead = "Chao - Unicorn Forehead"
+chao_unicorn_legs = "Chao - Unicorn Legs"
+chao_unicorn_tail = "Chao - Unicorn Tail"
+
+chao_phoenix_forehead = "Chao - Phoenix Forehead"
+chao_phoenix_legs = "Chao - Phoenix Legs"
+chao_phoenix_tail = "Chao - Phoenix Tail"
+chao_phoenix_wings = "Chao - Phoenix Wings"
+
+# Chao Kindergarten Locations
+chao_kindergarten_drawing_1 = "Chao Kindergarten - Drawing 1"
+chao_kindergarten_drawing_2 = "Chao Kindergarten - Drawing 2"
+chao_kindergarten_drawing_3 = "Chao Kindergarten - Drawing 3"
+chao_kindergarten_drawing_4 = "Chao Kindergarten - Drawing 4"
+chao_kindergarten_drawing_5 = "Chao Kindergarten - Drawing 5"
+
+chao_kindergarten_shake_dance = "Chao Kindergarten - Shake Dance"
+chao_kindergarten_spin_dance = "Chao Kindergarten - Spin Dance"
+chao_kindergarten_step_dance = "Chao Kindergarten - Step Dance"
+chao_kindergarten_gogo_dance = "Chao Kindergarten - Go-Go Dance"
+chao_kindergarten_exercise = "Chao Kindergarten - Exercise"
+
+chao_kindergarten_song_1 = "Chao Kindergarten - Song 1"
+chao_kindergarten_song_2 = "Chao Kindergarten - Song 2"
+chao_kindergarten_song_3 = "Chao Kindergarten - Song 3"
+chao_kindergarten_song_4 = "Chao Kindergarten - Song 4"
+chao_kindergarten_song_5 = "Chao Kindergarten - Song 5"
+
+chao_kindergarten_bell = "Chao Kindergarten - Bell"
+chao_kindergarten_castanets = "Chao Kindergarten - Castanets"
+chao_kindergarten_cymbals = "Chao Kindergarten - Cymbals"
+chao_kindergarten_drum = "Chao Kindergarten - Drum"
+chao_kindergarten_flute = "Chao Kindergarten - Flute"
+chao_kindergarten_maracas = "Chao Kindergarten - Maracas"
+chao_kindergarten_trumpet = "Chao Kindergarten - Trumpet"
+chao_kindergarten_tambourine = "Chao Kindergarten - Tambourine"
+
+chao_kindergarten_any_drawing = "Chao Kindergarten - Any Drawing"
+chao_kindergarten_any_dance = "Chao Kindergarten - Any Dance"
+chao_kindergarten_any_song = "Chao Kindergarten - Any Song"
+chao_kindergarten_any_instrument = "Chao Kindergarten - Any Instrument"
+
+
+# Chao Goal Locations
+chaos_chao = "Chaos Chao"
+chaos_chao_region = "Chaos Chao"
+
# Kart Race Definitions
kart_race_beginner_sonic = "Kart Race - Beginner - Sonic"
@@ -1261,9 +1442,18 @@
grand_prix = "Grand Prix"
grand_prix_region = "Grand Prix"
-chao_garden_beginner_region = "Chao Garden - Beginner"
-chao_garden_intermediate_region = "Chao Garden - Intermediate"
-chao_garden_expert_region = "Chao Garden - Expert"
+chao_race_beginner_region = "Chao Race - Beginner"
+chao_race_intermediate_region = "Chao Race - Intermediate"
+chao_race_expert_region = "Chao Race - Expert"
+
+chao_karate_beginner_region = "Chao Karate - Beginner"
+chao_karate_intermediate_region = "Chao Karate - Standard"
+chao_karate_expert_region = "Chao Karate - Expert"
+chao_karate_super_region = "Chao Karate - Super"
+
+chao_kindergarten_region = "Chao Kindergarten"
+
+black_market_region = "Black Market"
kart_race_beginner_region = "Kart Race - Beginner"
kart_race_standard_region = "Kart Race - Intermediate"
diff --git a/worlds/sa2b/Options.py b/worlds/sa2b/Options.py
index 6bef9def3dd8..438e59de5e16 100644
--- a/worlds/sa2b/Options.py
+++ b/worlds/sa2b/Options.py
@@ -1,18 +1,27 @@
-import typing
+from dataclasses import dataclass
-from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList
+from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions
class Goal(Choice):
"""
Determines the goal of the seed
+
Biolizard: Finish Cannon's Core and defeat the Biolizard and Finalhazard
+
Chaos Emerald Hunt: Find the Seven Chaos Emeralds and reach Green Hill Zone
+
Finalhazard Chaos Emerald Hunt: Find the Seven Chaos Emeralds and reach Green Hill Zone, then defeat Finalhazard
+
Grand Prix: Win every race in Kart Race Mode (all standard levels are disabled)
+
Boss Rush: Beat all of the bosses in the Boss Rush, ending with Finalhazard
+
Cannon's Core Boss Rush: Beat Cannon's Core, then beat all of the bosses in the Boss Rush, ending with Finalhazard
+
Boss Rush Chaos Emerald Hunt: Find the Seven Chaos Emeralds, then beat all of the bosses in the Boss Rush, ending with Finalhazard
+
+ Chaos Chao: Raise a Chaos Chao to win
"""
display_name = "Goal"
option_biolizard = 0
@@ -22,6 +31,7 @@ class Goal(Choice):
option_boss_rush = 4
option_cannons_core_boss_rush = 5
option_boss_rush_chaos_emerald_hunt = 6
+ option_chaos_chao = 7
default = 0
@classmethod
@@ -44,9 +54,13 @@ class MissionShuffle(Toggle):
class BossRushShuffle(Choice):
"""
Determines how bosses in Boss Rush Mode are shuffled
+
Vanilla: Bosses appear in the Vanilla ordering
+
Shuffled: The same bosses appear, but in a random order
+
Chaos: Each boss is randomly chosen separately (one will always be King Boom Boo)
+
Singularity: One boss is chosen and placed in every slot (one will always be replaced with King Boom Boo)
"""
display_name = "Boss Rush Shuffle"
@@ -70,74 +84,81 @@ class BaseTrapWeight(Choice):
class OmochaoTrapWeight(BaseTrapWeight):
"""
- Likelihood of a receiving a trap which spawns several Omochao around the player
+ Likelihood of receiving a trap which spawns several Omochao around the player
"""
display_name = "OmoTrap Weight"
class TimestopTrapWeight(BaseTrapWeight):
"""
- Likelihood of a receiving a trap which briefly stops time
+ Likelihood of receiving a trap which briefly stops time
"""
display_name = "Chaos Control Trap Weight"
class ConfusionTrapWeight(BaseTrapWeight):
"""
- Likelihood of a receiving a trap which causes the controls to be skewed for a period of time
+ Likelihood of receiving a trap which causes the controls to be skewed for a period of time
"""
display_name = "Confusion Trap Weight"
class TinyTrapWeight(BaseTrapWeight):
"""
- Likelihood of a receiving a trap which causes the player to become tiny
+ Likelihood of receiving a trap which causes the player to become tiny
"""
display_name = "Tiny Trap Weight"
class GravityTrapWeight(BaseTrapWeight):
"""
- Likelihood of a receiving a trap which increases gravity
+ Likelihood of receiving a trap which increases gravity
"""
display_name = "Gravity Trap Weight"
class ExpositionTrapWeight(BaseTrapWeight):
"""
- Likelihood of a receiving a trap which tells you the story
+ Likelihood of receiving a trap which tells you the story
"""
display_name = "Exposition Trap Weight"
class DarknessTrapWeight(BaseTrapWeight):
"""
- Likelihood of a receiving a trap which makes the world dark
+ Likelihood of receiving a trap which makes the world dark
"""
display_name = "Darkness Trap Weight"
class IceTrapWeight(BaseTrapWeight):
"""
- Likelihood of a receiving a trap which makes the world slippery
+ Likelihood of receiving a trap which makes the world slippery
"""
display_name = "Ice Trap Weight"
class SlowTrapWeight(BaseTrapWeight):
"""
- Likelihood of a receiving a trap which makes you gotta go slow
+ Likelihood of receiving a trap which makes you gotta go slow
"""
display_name = "Slow Trap Weight"
class CutsceneTrapWeight(BaseTrapWeight):
"""
- Likelihood of a receiving a trap which makes you watch an unskippable cutscene
+ Likelihood of receiving a trap which makes you watch an unskippable cutscene
"""
display_name = "Cutscene Trap Weight"
+class ReverseTrapWeight(BaseTrapWeight):
+ """
+ Likelihood of receiving a trap which reverses your controls
+ """
+ display_name = "Reverse Trap Weight"
+
+
class PongTrapWeight(BaseTrapWeight):
"""
Likelihood of receiving a trap which forces you to play a Pong minigame
@@ -187,9 +208,13 @@ class Keysanity(Toggle):
class Whistlesanity(Choice):
"""
Determines whether whistling at various spots grants checks
+
None: No Whistle Spots grant checks
+
Pipes: Whistling at Pipes grants checks (97 Locations)
+
Hidden: Whistling at Hidden Whistle Spots grants checks (32 Locations)
+
Both: Whistling at both Pipes and Hidden Whistle Spots grants checks (129 Locations)
"""
display_name = "Whistlesanity"
@@ -218,8 +243,10 @@ class Omosanity(Toggle):
class Animalsanity(Toggle):
"""
- Determines whether picking up counted small animals grants checks
- (420 Locations)
+ Determines whether unique counts of animals grant checks.
+ (421 Locations)
+
+ ALL animals must be collected in a single run of a mission to get all checks.
"""
display_name = "Animalsanity"
@@ -227,8 +254,11 @@ class Animalsanity(Toggle):
class KartRaceChecks(Choice):
"""
Determines whether Kart Race Mode grants checks
+
None: No Kart Races grant checks
+
Mini: Each Kart Race difficulty must be beaten only once
+
Full: Every Character must separately beat each Kart Race difficulty
"""
display_name = "Kart Race Checks"
@@ -261,8 +291,11 @@ class NumberOfLevelGates(Range):
class LevelGateDistribution(Choice):
"""
Determines how levels are distributed between level gate regions
+
Early: Earlier regions will have more levels than later regions
+
Even: Levels will be evenly distributed between all regions
+
Late: Later regions will have more levels than earlier regions
"""
display_name = "Level Gate Distribution"
@@ -286,12 +319,14 @@ class LevelGateCosts(Choice):
class MaximumEmblemCap(Range):
"""
Determines the maximum number of emblems that can be in the item pool.
+
If fewer available locations exist in the pool than this number, the number of available locations will be used instead.
+
Gate and Cannon's Core costs will be calculated based off of that number.
"""
display_name = "Max Emblem Cap"
range_start = 50
- range_end = 500
+ range_end = 1000
default = 180
@@ -308,15 +343,19 @@ class RequiredRank(Choice):
default = 0
-class ChaoGardenDifficulty(Choice):
+class ChaoRaceDifficulty(Choice):
"""
- Determines the number of chao garden difficulty levels included. Easier difficulty settings means fewer chao garden checks
- None: No Chao Garden Activities have checks
+ Determines the number of Chao Race difficulty levels included. Easier difficulty settings means fewer Chao Race checks
+
+ None: No Chao Races have checks
+
Beginner: Beginner Races
+
Intermediate: Beginner, Challenge, Hero, and Dark Races
+
Expert: Beginner, Challenge, Hero, Dark and Jewel Races
"""
- display_name = "Chao Garden Difficulty"
+ display_name = "Chao Race Difficulty"
option_none = 0
option_beginner = 1
option_intermediate = 2
@@ -324,30 +363,149 @@ class ChaoGardenDifficulty(Choice):
default = 0
-class IncludeChaoKarate(Toggle):
+class ChaoKarateDifficulty(Choice):
"""
- Determines whether the Chao Karate should be included as checks (Note: This setting requires purchase of the "Battle" DLC)
+ Determines the number of Chao Karate difficulty levels included. (Note: This setting requires purchase of the "Battle" DLC)
"""
- display_name = "Include Chao Karate"
+ display_name = "Chao Karate Difficulty"
+ option_none = 0
+ option_beginner = 1
+ option_standard = 2
+ option_expert = 3
+ option_super = 4
+ default = 0
-class ChaoRaceChecks(Choice):
+class ChaoStadiumChecks(Choice):
"""
- Determines which Chao Races grant checks
- All: Each individual race grants a check
- Prize: Only the races which grant Chao Toys grant checks (final race of each Beginner and Jewel cup, 4th, 8th, and
- 12th Challenge Races, 2nd and 4th Hero and Dark Races)
+ Determines which Chao Stadium activities grant checks
+
+ All: Each individual race and karate fight grants a check
+
+ Prize: Only the races which grant Chao Toys grant checks (final race of each Beginner and Jewel cup, 4th, 8th, and 12th Challenge Races, 2nd and 4th Hero and Dark Races, final fight of each Karate difficulty)
"""
- display_name = "Chao Race Checks"
+ display_name = "Chao Stadium Checks"
option_all = 0
option_prize = 1
default = 0
+class ChaoStats(Range):
+ """
+ Determines the highest level in each Chao Stat that grants checks
+ (Swim, Fly, Run, Power)
+ """
+ display_name = "Chao Stats"
+ range_start = 0
+ range_end = 99
+ default = 0
+
+
+class ChaoStatsFrequency(Range):
+ """
+ Determines how many levels in each Chao Stat grant checks (up to the maximum set in the `chao_stats` option)
+
+ `1` means every level is included, `2` means every other level is included, `3` means every third, and so on
+ """
+ display_name = "Chao Stats Frequency"
+ range_start = 1
+ range_end = 20
+ default = 5
+
+
+class ChaoStatsStamina(Toggle):
+ """
+ Determines whether Stamina is included in the `chao_stats` option
+ """
+ display_name = "Chao Stats - Stamina"
+
+
+class ChaoStatsHidden(Toggle):
+ """
+ Determines whether the hidden stats (Luck and Intelligence) are included in the `chao_stats` option
+ """
+ display_name = "Chao Stats - Luck and Intelligence"
+
+
+class ChaoAnimalParts(Toggle):
+ """
+ Determines whether giving Chao various animal parts grants checks
+ (73 Locations)
+ """
+ display_name = "Chao Animal Parts"
+
+
+class ChaoKindergarten(Choice):
+ """
+ Determines whether learning the lessons from the Kindergarten Classroom grants checks
+ (WARNING: VERY SLOW)
+
+ None: No Kindergarten classes have checks
+
+ Basics: One class from each category (Drawing, Dance, Song, and Instrument) is a check (4 Locations)
+
+ Full: Every class is a check (23 Locations)
+ """
+ display_name = "Chao Kindergarten Checks"
+ option_none = 0
+ option_basics = 1
+ option_full = 2
+ default = 0
+
+
+class BlackMarketSlots(Range):
+ """
+ Determines how many multiworld items are available to purchase from the Black Market
+ """
+ display_name = "Black Market Slots"
+ range_start = 0
+ range_end = 64
+ default = 0
+
+
+class BlackMarketUnlockCosts(Choice):
+ """
+ Determines how many Chao Coins are required to unlock sets of Black Market items
+ """
+ display_name = "Black Market Unlock Costs"
+ option_low = 0
+ option_medium = 1
+ option_high = 2
+ default = 1
+
+
+class BlackMarketPriceMultiplier(Range):
+ """
+ Determines how many rings the Black Market items cost
+
+ The base ring costs of items in the Black Market range from 50-100, and are then multiplied by this value
+ """
+ display_name = "Black Market Price Multiplier"
+ range_start = 0
+ range_end = 40
+ default = 1
+
+
+class ShuffleStartingChaoEggs(DefaultOnToggle):
+ """
+ Determines whether the starting Chao eggs in the gardens are random
+ """
+ display_name = "Shuffle Starting Chao Eggs"
+
+
+class ChaoEntranceRandomization(Toggle):
+ """
+ Determines whether entrances in Chao World are randomized
+ """
+ display_name = "Chao Entrance Randomization"
+
+
class RequiredCannonsCoreMissions(Choice):
"""
Determines how many Cannon's Core missions must be completed (for Biolizard or Cannon's Core goals)
+
First: Only the first mission must be completed
+
All Active: All active Cannon's Core missions must be completed
"""
display_name = "Required Cannon's Core Missions"
@@ -543,8 +701,11 @@ class CannonsCoreMission5(DefaultOnToggle):
class RingLoss(Choice):
"""
How taking damage is handled
+
Classic: You lose all of your rings when hit
+
Modern: You lose 20 rings when hit
+
OHKO: You die immediately when hit (NOTE: Some Hard Logic tricks may require damage boosts!)
"""
display_name = "Ring Loss"
@@ -571,9 +732,13 @@ class RingLink(Toggle):
class SADXMusic(Choice):
"""
Whether the randomizer will include Sonic Adventure DX Music in the music pool
+
SA2B: Only SA2B music will be played
+
SADX: Only SADX music will be played
+
Both: Both SA2B and SADX music will be played
+
NOTE: This option requires the player to own a PC copy of SADX and to follow the addition steps in the setup guide.
"""
display_name = "SADX Music"
@@ -593,9 +758,13 @@ def get_option_name(cls, value) -> str:
class MusicShuffle(Choice):
"""
What type of Music Shuffle is used
+
None: No music is shuffled.
+
Levels: Level music is shuffled.
+
Full: Level, Menu, and Additional music is shuffled.
+
Singularity: Level, Menu, and Additional music is all replaced with a single random song.
"""
display_name = "Music Shuffle Type"
@@ -609,10 +778,15 @@ class MusicShuffle(Choice):
class VoiceShuffle(Choice):
"""
What type of Voice Shuffle is used
+
None: No voices are shuffled.
+
Shuffled: Voices are shuffled.
+
Rude: Voices are shuffled, but some are replaced with rude words.
+
Chao: All voices are replaced with chao sounds.
+
Singularity: All voices are replaced with a single random voice.
"""
display_name = "Voice Shuffle Type"
@@ -646,7 +820,9 @@ class Narrator(Choice):
class LogicDifficulty(Choice):
"""
What set of Upgrade Requirement logic to use
+
Standard: The logic assumes the "intended" usage of Upgrades to progress through levels
+
Hard: Some simple skips or sequence breaks may be required
"""
display_name = "Logic Difficulty"
@@ -655,71 +831,195 @@ class LogicDifficulty(Choice):
default = 0
-sa2b_options: typing.Dict[str, type(Option)] = {
- "goal": Goal,
- "mission_shuffle": MissionShuffle,
- "boss_rush_shuffle": BossRushShuffle,
- "keysanity": Keysanity,
- "whistlesanity": Whistlesanity,
- "beetlesanity": Beetlesanity,
- "omosanity": Omosanity,
- "animalsanity": Animalsanity,
- "kart_race_checks": KartRaceChecks,
- "required_rank": RequiredRank,
- "emblem_percentage_for_cannons_core": EmblemPercentageForCannonsCore,
- "required_cannons_core_missions": RequiredCannonsCoreMissions,
- "number_of_level_gates": NumberOfLevelGates,
- "level_gate_distribution": LevelGateDistribution,
- "level_gate_costs": LevelGateCosts,
- "max_emblem_cap": MaximumEmblemCap,
- "chao_garden_difficulty": ChaoGardenDifficulty,
- "include_chao_karate": IncludeChaoKarate,
- "chao_race_checks": ChaoRaceChecks,
- "junk_fill_percentage": JunkFillPercentage,
- "trap_fill_percentage": TrapFillPercentage,
- "omochao_trap_weight": OmochaoTrapWeight,
- "timestop_trap_weight": TimestopTrapWeight,
- "confusion_trap_weight": ConfusionTrapWeight,
- "tiny_trap_weight": TinyTrapWeight,
- "gravity_trap_weight": GravityTrapWeight,
- "exposition_trap_weight": ExpositionTrapWeight,
- #"darkness_trap_weight": DarknessTrapWeight,
- "ice_trap_weight": IceTrapWeight,
- "slow_trap_weight": SlowTrapWeight,
- "cutscene_trap_weight": CutsceneTrapWeight,
- "pong_trap_weight": PongTrapWeight,
- "minigame_trap_difficulty": MinigameTrapDifficulty,
- "ring_loss": RingLoss,
- "ring_link": RingLink,
- "sadx_music": SADXMusic,
- "music_shuffle": MusicShuffle,
- "voice_shuffle": VoiceShuffle,
- "narrator": Narrator,
- "logic_difficulty": LogicDifficulty,
- "speed_mission_count": SpeedMissionCount,
- "speed_mission_2": SpeedMission2,
- "speed_mission_3": SpeedMission3,
- "speed_mission_4": SpeedMission4,
- "speed_mission_5": SpeedMission5,
- "mech_mission_count": MechMissionCount,
- "mech_mission_2": MechMission2,
- "mech_mission_3": MechMission3,
- "mech_mission_4": MechMission4,
- "mech_mission_5": MechMission5,
- "hunt_mission_count": HuntMissionCount,
- "hunt_mission_2": HuntMission2,
- "hunt_mission_3": HuntMission3,
- "hunt_mission_4": HuntMission4,
- "hunt_mission_5": HuntMission5,
- "kart_mission_count": KartMissionCount,
- "kart_mission_2": KartMission2,
- "kart_mission_3": KartMission3,
- "kart_mission_4": KartMission4,
- "kart_mission_5": KartMission5,
- "cannons_core_mission_count": CannonsCoreMissionCount,
- "cannons_core_mission_2": CannonsCoreMission2,
- "cannons_core_mission_3": CannonsCoreMission3,
- "cannons_core_mission_4": CannonsCoreMission4,
- "cannons_core_mission_5": CannonsCoreMission5,
- "death_link": DeathLink,
-}
+sa2b_option_groups = [
+ OptionGroup("General Options", [
+ Goal,
+ BossRushShuffle,
+ LogicDifficulty,
+ RequiredRank,
+ MaximumEmblemCap,
+ RingLoss,
+ ]),
+ OptionGroup("Stages", [
+ MissionShuffle,
+ EmblemPercentageForCannonsCore,
+ RequiredCannonsCoreMissions,
+ NumberOfLevelGates,
+ LevelGateCosts,
+ LevelGateDistribution,
+ ]),
+ OptionGroup("Sanity Options", [
+ Keysanity,
+ Whistlesanity,
+ Beetlesanity,
+ Omosanity,
+ Animalsanity,
+ KartRaceChecks,
+ ]),
+ OptionGroup("Chao", [
+ BlackMarketSlots,
+ BlackMarketUnlockCosts,
+ BlackMarketPriceMultiplier,
+ ChaoRaceDifficulty,
+ ChaoKarateDifficulty,
+ ChaoStadiumChecks,
+ ChaoAnimalParts,
+ ChaoStats,
+ ChaoStatsFrequency,
+ ChaoStatsStamina,
+ ChaoStatsHidden,
+ ChaoKindergarten,
+ ShuffleStartingChaoEggs,
+ ChaoEntranceRandomization,
+ ]),
+ OptionGroup("Junk and Traps", [
+ JunkFillPercentage,
+ TrapFillPercentage,
+ OmochaoTrapWeight,
+ TimestopTrapWeight,
+ ConfusionTrapWeight,
+ TinyTrapWeight,
+ GravityTrapWeight,
+ ExpositionTrapWeight,
+ IceTrapWeight,
+ SlowTrapWeight,
+ CutsceneTrapWeight,
+ ReverseTrapWeight,
+ PongTrapWeight,
+ MinigameTrapDifficulty,
+ ]),
+ OptionGroup("Speed Missions", [
+ SpeedMissionCount,
+ SpeedMission2,
+ SpeedMission3,
+ SpeedMission4,
+ SpeedMission5,
+ ]),
+ OptionGroup("Mech Missions", [
+ MechMissionCount,
+ MechMission2,
+ MechMission3,
+ MechMission4,
+ MechMission5,
+ ]),
+ OptionGroup("Hunt Missions", [
+ HuntMissionCount,
+ HuntMission2,
+ HuntMission3,
+ HuntMission4,
+ HuntMission5,
+ ]),
+ OptionGroup("Kart Missions", [
+ KartMissionCount,
+ KartMission2,
+ KartMission3,
+ KartMission4,
+ KartMission5,
+ ]),
+ OptionGroup("Cannon's Core Missions", [
+ CannonsCoreMissionCount,
+ CannonsCoreMission2,
+ CannonsCoreMission3,
+ CannonsCoreMission4,
+ CannonsCoreMission5,
+ ]),
+ OptionGroup("Aesthetics", [
+ SADXMusic,
+ MusicShuffle,
+ VoiceShuffle,
+ Narrator,
+ ]),
+]
+
+
+@dataclass
+class SA2BOptions(PerGameCommonOptions):
+ goal: Goal
+ boss_rush_shuffle: BossRushShuffle
+ logic_difficulty: LogicDifficulty
+ required_rank: RequiredRank
+ max_emblem_cap: MaximumEmblemCap
+ ring_loss: RingLoss
+
+ mission_shuffle: MissionShuffle
+ required_cannons_core_missions: RequiredCannonsCoreMissions
+ emblem_percentage_for_cannons_core: EmblemPercentageForCannonsCore
+ number_of_level_gates: NumberOfLevelGates
+ level_gate_distribution: LevelGateDistribution
+ level_gate_costs: LevelGateCosts
+
+ keysanity: Keysanity
+ whistlesanity: Whistlesanity
+ beetlesanity: Beetlesanity
+ omosanity: Omosanity
+ animalsanity: Animalsanity
+ kart_race_checks: KartRaceChecks
+
+ black_market_slots: BlackMarketSlots
+ black_market_unlock_costs: BlackMarketUnlockCosts
+ black_market_price_multiplier: BlackMarketPriceMultiplier
+ chao_race_difficulty: ChaoRaceDifficulty
+ chao_karate_difficulty: ChaoKarateDifficulty
+ chao_stadium_checks: ChaoStadiumChecks
+ chao_animal_parts: ChaoAnimalParts
+ chao_stats: ChaoStats
+ chao_stats_frequency: ChaoStatsFrequency
+ chao_stats_stamina: ChaoStatsStamina
+ chao_stats_hidden: ChaoStatsHidden
+ chao_kindergarten: ChaoKindergarten
+ shuffle_starting_chao_eggs: ShuffleStartingChaoEggs
+ chao_entrance_randomization: ChaoEntranceRandomization
+
+ junk_fill_percentage: JunkFillPercentage
+ trap_fill_percentage: TrapFillPercentage
+ omochao_trap_weight: OmochaoTrapWeight
+ timestop_trap_weight: TimestopTrapWeight
+ confusion_trap_weight: ConfusionTrapWeight
+ tiny_trap_weight: TinyTrapWeight
+ gravity_trap_weight: GravityTrapWeight
+ exposition_trap_weight: ExpositionTrapWeight
+ #darkness_trap_weight: DarknessTrapWeight
+ ice_trap_weight: IceTrapWeight
+ slow_trap_weight: SlowTrapWeight
+ cutscene_trap_weight: CutsceneTrapWeight
+ reverse_trap_weight: ReverseTrapWeight
+ pong_trap_weight: PongTrapWeight
+ minigame_trap_difficulty: MinigameTrapDifficulty
+
+ sadx_music: SADXMusic
+ music_shuffle: MusicShuffle
+ voice_shuffle: VoiceShuffle
+ narrator: Narrator
+
+ speed_mission_count: SpeedMissionCount
+ speed_mission_2: SpeedMission2
+ speed_mission_3: SpeedMission3
+ speed_mission_4: SpeedMission4
+ speed_mission_5: SpeedMission5
+
+ mech_mission_count: MechMissionCount
+ mech_mission_2: MechMission2
+ mech_mission_3: MechMission3
+ mech_mission_4: MechMission4
+ mech_mission_5: MechMission5
+
+ hunt_mission_count: HuntMissionCount
+ hunt_mission_2: HuntMission2
+ hunt_mission_3: HuntMission3
+ hunt_mission_4: HuntMission4
+ hunt_mission_5: HuntMission5
+
+ kart_mission_count: KartMissionCount
+ kart_mission_2: KartMission2
+ kart_mission_3: KartMission3
+ kart_mission_4: KartMission4
+ kart_mission_5: KartMission5
+
+ cannons_core_mission_count: CannonsCoreMissionCount
+ cannons_core_mission_2: CannonsCoreMission2
+ cannons_core_mission_3: CannonsCoreMission3
+ cannons_core_mission_4: CannonsCoreMission4
+ cannons_core_mission_5: CannonsCoreMission5
+
+ ring_link: RingLink
+ death_link: DeathLink
diff --git a/worlds/sa2b/Regions.py b/worlds/sa2b/Regions.py
index da519283300a..fb6472d65df7 100644
--- a/worlds/sa2b/Regions.py
+++ b/worlds/sa2b/Regions.py
@@ -1,8 +1,14 @@
import typing
+import math
-from BaseClasses import MultiWorld, Region, Entrance
+from BaseClasses import MultiWorld, Region, Entrance, ItemClassification
+from worlds.AutoWorld import World
from .Items import SA2BItem
-from .Locations import SA2BLocation, boss_gate_location_table, boss_gate_set
+from .Locations import SA2BLocation, boss_gate_location_table, boss_gate_set,\
+ chao_stat_swim_table, chao_stat_fly_table, chao_stat_run_table,\
+ chao_stat_power_table, chao_stat_stamina_table,\
+ chao_stat_luck_table, chao_stat_intelligence_table, chao_animal_event_location_table,\
+ chao_kindergarten_location_table, chao_kindergarten_basics_location_table, black_market_location_table
from .Names import LocationName, ItemName
from .GateBosses import get_boss_name, all_gate_bosses_table, king_boom_boo
@@ -86,35 +92,37 @@ def __init__(self, emblems):
]
-def create_regions(world, player: int, active_locations):
- menu_region = create_region(world, player, active_locations, 'Menu', None)
+def create_regions(multiworld: MultiWorld, world: World, player: int, active_locations):
+ menu_region = create_region(multiworld, player, active_locations, 'Menu', None)
- gate_0_region = create_region(world, player, active_locations, 'Gate 0', None)
+ conditional_regions = []
+ gate_0_region = create_region(multiworld, player, active_locations, 'Gate 0', None)
+ conditional_regions += [gate_0_region]
- if world.number_of_level_gates[player].value >= 1:
- gate_1_boss_region = create_region(world, player, active_locations, 'Gate 1 Boss', [LocationName.gate_1_boss])
- gate_1_region = create_region(world, player, active_locations, 'Gate 1', None)
- world.regions += [gate_1_region, gate_1_boss_region]
+ if world.options.number_of_level_gates.value >= 1:
+ gate_1_boss_region = create_region(multiworld, player, active_locations, 'Gate 1 Boss', [LocationName.gate_1_boss])
+ gate_1_region = create_region(multiworld, player, active_locations, 'Gate 1', None)
+ conditional_regions += [gate_1_region, gate_1_boss_region]
- if world.number_of_level_gates[player].value >= 2:
- gate_2_boss_region = create_region(world, player, active_locations, 'Gate 2 Boss', [LocationName.gate_2_boss])
- gate_2_region = create_region(world, player, active_locations, 'Gate 2', None)
- world.regions += [gate_2_region, gate_2_boss_region]
+ if world.options.number_of_level_gates.value >= 2:
+ gate_2_boss_region = create_region(multiworld, player, active_locations, 'Gate 2 Boss', [LocationName.gate_2_boss])
+ gate_2_region = create_region(multiworld, player, active_locations, 'Gate 2', None)
+ conditional_regions += [gate_2_region, gate_2_boss_region]
- if world.number_of_level_gates[player].value >= 3:
- gate_3_boss_region = create_region(world, player, active_locations, 'Gate 3 Boss', [LocationName.gate_3_boss])
- gate_3_region = create_region(world, player, active_locations, 'Gate 3', None)
- world.regions += [gate_3_region, gate_3_boss_region]
+ if world.options.number_of_level_gates.value >= 3:
+ gate_3_boss_region = create_region(multiworld, player, active_locations, 'Gate 3 Boss', [LocationName.gate_3_boss])
+ gate_3_region = create_region(multiworld, player, active_locations, 'Gate 3', None)
+ conditional_regions += [gate_3_region, gate_3_boss_region]
- if world.number_of_level_gates[player].value >= 4:
- gate_4_boss_region = create_region(world, player, active_locations, 'Gate 4 Boss', [LocationName.gate_4_boss])
- gate_4_region = create_region(world, player, active_locations, 'Gate 4', None)
- world.regions += [gate_4_region, gate_4_boss_region]
+ if world.options.number_of_level_gates.value >= 4:
+ gate_4_boss_region = create_region(multiworld, player, active_locations, 'Gate 4 Boss', [LocationName.gate_4_boss])
+ gate_4_region = create_region(multiworld, player, active_locations, 'Gate 4', None)
+ conditional_regions += [gate_4_region, gate_4_boss_region]
- if world.number_of_level_gates[player].value >= 5:
- gate_5_boss_region = create_region(world, player, active_locations, 'Gate 5 Boss', [LocationName.gate_5_boss])
- gate_5_region = create_region(world, player, active_locations, 'Gate 5', None)
- world.regions += [gate_5_region, gate_5_boss_region]
+ if world.options.number_of_level_gates.value >= 5:
+ gate_5_boss_region = create_region(multiworld, player, active_locations, 'Gate 5 Boss', [LocationName.gate_5_boss])
+ gate_5_region = create_region(multiworld, player, active_locations, 'Gate 5', None)
+ conditional_regions += [gate_5_region, gate_5_boss_region]
city_escape_region_locations = [
LocationName.city_escape_1,
@@ -171,7 +179,7 @@ def create_regions(world, player: int, active_locations):
LocationName.city_escape_animal_20,
LocationName.city_escape_upgrade,
]
- city_escape_region = create_region(world, player, active_locations, LocationName.city_escape_region,
+ city_escape_region = create_region(multiworld, player, active_locations, LocationName.city_escape_region,
city_escape_region_locations)
metal_harbor_region_locations = [
@@ -206,7 +214,7 @@ def create_regions(world, player: int, active_locations):
LocationName.metal_harbor_animal_14,
LocationName.metal_harbor_upgrade,
]
- metal_harbor_region = create_region(world, player, active_locations, LocationName.metal_harbor_region,
+ metal_harbor_region = create_region(multiworld, player, active_locations, LocationName.metal_harbor_region,
metal_harbor_region_locations)
green_forest_region_locations = [
@@ -245,7 +253,7 @@ def create_regions(world, player: int, active_locations):
LocationName.green_forest_animal_18,
LocationName.green_forest_upgrade,
]
- green_forest_region = create_region(world, player, active_locations, LocationName.green_forest_region,
+ green_forest_region = create_region(multiworld, player, active_locations, LocationName.green_forest_region,
green_forest_region_locations)
pyramid_cave_region_locations = [
@@ -287,7 +295,7 @@ def create_regions(world, player: int, active_locations):
LocationName.pyramid_cave_animal_19,
LocationName.pyramid_cave_upgrade,
]
- pyramid_cave_region = create_region(world, player, active_locations, LocationName.pyramid_cave_region,
+ pyramid_cave_region = create_region(multiworld, player, active_locations, LocationName.pyramid_cave_region,
pyramid_cave_region_locations)
crazy_gadget_region_locations = [
@@ -336,7 +344,7 @@ def create_regions(world, player: int, active_locations):
LocationName.crazy_gadget_animal_16,
LocationName.crazy_gadget_upgrade,
]
- crazy_gadget_region = create_region(world, player, active_locations, LocationName.crazy_gadget_region,
+ crazy_gadget_region = create_region(multiworld, player, active_locations, LocationName.crazy_gadget_region,
crazy_gadget_region_locations)
final_rush_region_locations = [
@@ -372,7 +380,7 @@ def create_regions(world, player: int, active_locations):
LocationName.final_rush_animal_16,
LocationName.final_rush_upgrade,
]
- final_rush_region = create_region(world, player, active_locations, LocationName.final_rush_region,
+ final_rush_region = create_region(multiworld, player, active_locations, LocationName.final_rush_region,
final_rush_region_locations)
prison_lane_region_locations = [
@@ -418,7 +426,7 @@ def create_regions(world, player: int, active_locations):
LocationName.prison_lane_animal_15,
LocationName.prison_lane_upgrade,
]
- prison_lane_region = create_region(world, player, active_locations, LocationName.prison_lane_region,
+ prison_lane_region = create_region(multiworld, player, active_locations, LocationName.prison_lane_region,
prison_lane_region_locations)
mission_street_region_locations = [
@@ -464,7 +472,7 @@ def create_regions(world, player: int, active_locations):
LocationName.mission_street_animal_16,
LocationName.mission_street_upgrade,
]
- mission_street_region = create_region(world, player, active_locations, LocationName.mission_street_region,
+ mission_street_region = create_region(multiworld, player, active_locations, LocationName.mission_street_region,
mission_street_region_locations)
route_101_region_locations = [
@@ -474,7 +482,7 @@ def create_regions(world, player: int, active_locations):
LocationName.route_101_4,
LocationName.route_101_5,
]
- route_101_region = create_region(world, player, active_locations, LocationName.route_101_region,
+ route_101_region = create_region(multiworld, player, active_locations, LocationName.route_101_region,
route_101_region_locations)
hidden_base_region_locations = [
@@ -512,7 +520,7 @@ def create_regions(world, player: int, active_locations):
LocationName.hidden_base_animal_15,
LocationName.hidden_base_upgrade,
]
- hidden_base_region = create_region(world, player, active_locations, LocationName.hidden_base_region,
+ hidden_base_region = create_region(multiworld, player, active_locations, LocationName.hidden_base_region,
hidden_base_region_locations)
eternal_engine_region_locations = [
@@ -559,7 +567,7 @@ def create_regions(world, player: int, active_locations):
LocationName.eternal_engine_animal_15,
LocationName.eternal_engine_upgrade,
]
- eternal_engine_region = create_region(world, player, active_locations, LocationName.eternal_engine_region,
+ eternal_engine_region = create_region(multiworld, player, active_locations, LocationName.eternal_engine_region,
eternal_engine_region_locations)
wild_canyon_region_locations = [
@@ -597,7 +605,7 @@ def create_regions(world, player: int, active_locations):
LocationName.wild_canyon_animal_10,
LocationName.wild_canyon_upgrade,
]
- wild_canyon_region = create_region(world, player, active_locations, LocationName.wild_canyon_region,
+ wild_canyon_region = create_region(multiworld, player, active_locations, LocationName.wild_canyon_region,
wild_canyon_region_locations)
pumpkin_hill_region_locations = [
@@ -635,7 +643,7 @@ def create_regions(world, player: int, active_locations):
LocationName.pumpkin_hill_animal_11,
LocationName.pumpkin_hill_upgrade,
]
- pumpkin_hill_region = create_region(world, player, active_locations, LocationName.pumpkin_hill_region,
+ pumpkin_hill_region = create_region(multiworld, player, active_locations, LocationName.pumpkin_hill_region,
pumpkin_hill_region_locations)
aquatic_mine_region_locations = [
@@ -670,7 +678,7 @@ def create_regions(world, player: int, active_locations):
LocationName.aquatic_mine_animal_10,
LocationName.aquatic_mine_upgrade,
]
- aquatic_mine_region = create_region(world, player, active_locations, LocationName.aquatic_mine_region,
+ aquatic_mine_region = create_region(multiworld, player, active_locations, LocationName.aquatic_mine_region,
aquatic_mine_region_locations)
death_chamber_region_locations = [
@@ -709,7 +717,7 @@ def create_regions(world, player: int, active_locations):
LocationName.death_chamber_animal_10,
LocationName.death_chamber_upgrade,
]
- death_chamber_region = create_region(world, player, active_locations, LocationName.death_chamber_region,
+ death_chamber_region = create_region(multiworld, player, active_locations, LocationName.death_chamber_region,
death_chamber_region_locations)
meteor_herd_region_locations = [
@@ -741,7 +749,7 @@ def create_regions(world, player: int, active_locations):
LocationName.meteor_herd_animal_11,
LocationName.meteor_herd_upgrade,
]
- meteor_herd_region = create_region(world, player, active_locations, LocationName.meteor_herd_region,
+ meteor_herd_region = create_region(multiworld, player, active_locations, LocationName.meteor_herd_region,
meteor_herd_region_locations)
radical_highway_region_locations = [
@@ -790,7 +798,7 @@ def create_regions(world, player: int, active_locations):
LocationName.radical_highway_animal_20,
LocationName.radical_highway_upgrade,
]
- radical_highway_region = create_region(world, player, active_locations, LocationName.radical_highway_region,
+ radical_highway_region = create_region(multiworld, player, active_locations, LocationName.radical_highway_region,
radical_highway_region_locations)
white_jungle_region_locations = [
@@ -833,7 +841,7 @@ def create_regions(world, player: int, active_locations):
LocationName.white_jungle_animal_16,
LocationName.white_jungle_upgrade,
]
- white_jungle_region = create_region(world, player, active_locations, LocationName.white_jungle_region,
+ white_jungle_region = create_region(multiworld, player, active_locations, LocationName.white_jungle_region,
white_jungle_region_locations)
sky_rail_region_locations = [
@@ -874,7 +882,7 @@ def create_regions(world, player: int, active_locations):
LocationName.sky_rail_animal_20,
LocationName.sky_rail_upgrade,
]
- sky_rail_region = create_region(world, player, active_locations, LocationName.sky_rail_region,
+ sky_rail_region = create_region(multiworld, player, active_locations, LocationName.sky_rail_region,
sky_rail_region_locations)
final_chase_region_locations = [
@@ -910,7 +918,7 @@ def create_regions(world, player: int, active_locations):
LocationName.final_chase_animal_17,
LocationName.final_chase_upgrade,
]
- final_chase_region = create_region(world, player, active_locations, LocationName.final_chase_region,
+ final_chase_region = create_region(multiworld, player, active_locations, LocationName.final_chase_region,
final_chase_region_locations)
iron_gate_region_locations = [
@@ -951,7 +959,7 @@ def create_regions(world, player: int, active_locations):
LocationName.iron_gate_animal_15,
LocationName.iron_gate_upgrade,
]
- iron_gate_region = create_region(world, player, active_locations, LocationName.iron_gate_region,
+ iron_gate_region = create_region(multiworld, player, active_locations, LocationName.iron_gate_region,
iron_gate_region_locations)
sand_ocean_region_locations = [
@@ -988,7 +996,7 @@ def create_regions(world, player: int, active_locations):
LocationName.sand_ocean_animal_15,
LocationName.sand_ocean_upgrade,
]
- sand_ocean_region = create_region(world, player, active_locations, LocationName.sand_ocean_region,
+ sand_ocean_region = create_region(multiworld, player, active_locations, LocationName.sand_ocean_region,
sand_ocean_region_locations)
lost_colony_region_locations = [
@@ -1028,7 +1036,7 @@ def create_regions(world, player: int, active_locations):
LocationName.lost_colony_animal_14,
LocationName.lost_colony_upgrade,
]
- lost_colony_region = create_region(world, player, active_locations, LocationName.lost_colony_region,
+ lost_colony_region = create_region(multiworld, player, active_locations, LocationName.lost_colony_region,
lost_colony_region_locations)
weapons_bed_region_locations = [
@@ -1065,7 +1073,7 @@ def create_regions(world, player: int, active_locations):
LocationName.weapons_bed_animal_15,
LocationName.weapons_bed_upgrade,
]
- weapons_bed_region = create_region(world, player, active_locations, LocationName.weapons_bed_region,
+ weapons_bed_region = create_region(multiworld, player, active_locations, LocationName.weapons_bed_region,
weapons_bed_region_locations)
cosmic_wall_region_locations = [
@@ -1101,7 +1109,7 @@ def create_regions(world, player: int, active_locations):
LocationName.cosmic_wall_animal_15,
LocationName.cosmic_wall_upgrade,
]
- cosmic_wall_region = create_region(world, player, active_locations, LocationName.cosmic_wall_region,
+ cosmic_wall_region = create_region(multiworld, player, active_locations, LocationName.cosmic_wall_region,
cosmic_wall_region_locations)
dry_lagoon_region_locations = [
@@ -1138,9 +1146,10 @@ def create_regions(world, player: int, active_locations):
LocationName.dry_lagoon_animal_8,
LocationName.dry_lagoon_animal_9,
LocationName.dry_lagoon_animal_10,
+ LocationName.dry_lagoon_animal_11,
LocationName.dry_lagoon_upgrade,
]
- dry_lagoon_region = create_region(world, player, active_locations, LocationName.dry_lagoon_region,
+ dry_lagoon_region = create_region(multiworld, player, active_locations, LocationName.dry_lagoon_region,
dry_lagoon_region_locations)
egg_quarters_region_locations = [
@@ -1176,7 +1185,7 @@ def create_regions(world, player: int, active_locations):
LocationName.egg_quarters_animal_10,
LocationName.egg_quarters_upgrade,
]
- egg_quarters_region = create_region(world, player, active_locations, LocationName.egg_quarters_region,
+ egg_quarters_region = create_region(multiworld, player, active_locations, LocationName.egg_quarters_region,
egg_quarters_region_locations)
security_hall_region_locations = [
@@ -1213,7 +1222,7 @@ def create_regions(world, player: int, active_locations):
LocationName.security_hall_animal_8,
LocationName.security_hall_upgrade,
]
- security_hall_region = create_region(world, player, active_locations, LocationName.security_hall_region,
+ security_hall_region = create_region(multiworld, player, active_locations, LocationName.security_hall_region,
security_hall_region_locations)
route_280_region_locations = [
@@ -1223,7 +1232,7 @@ def create_regions(world, player: int, active_locations):
LocationName.route_280_4,
LocationName.route_280_5,
]
- route_280_region = create_region(world, player, active_locations, LocationName.route_280_region,
+ route_280_region = create_region(multiworld, player, active_locations, LocationName.route_280_region,
route_280_region_locations)
mad_space_region_locations = [
@@ -1257,7 +1266,7 @@ def create_regions(world, player: int, active_locations):
LocationName.mad_space_animal_10,
LocationName.mad_space_upgrade,
]
- mad_space_region = create_region(world, player, active_locations, LocationName.mad_space_region,
+ mad_space_region = create_region(multiworld, player, active_locations, LocationName.mad_space_region,
mad_space_region_locations)
cannon_core_region_locations = [
@@ -1305,10 +1314,10 @@ def create_regions(world, player: int, active_locations):
LocationName.cannon_core_animal_19,
LocationName.cannon_core_beetle,
]
- cannon_core_region = create_region(world, player, active_locations, LocationName.cannon_core_region,
+ cannon_core_region = create_region(multiworld, player, active_locations, LocationName.cannon_core_region,
cannon_core_region_locations)
- chao_garden_beginner_region_locations = [
+ chao_race_beginner_region_locations = [
LocationName.chao_race_crab_pool_1,
LocationName.chao_race_crab_pool_2,
LocationName.chao_race_crab_pool_3,
@@ -1321,13 +1330,21 @@ def create_regions(world, player: int, active_locations):
LocationName.chao_race_block_canyon_1,
LocationName.chao_race_block_canyon_2,
LocationName.chao_race_block_canyon_3,
-
- LocationName.chao_beginner_karate,
]
- chao_garden_beginner_region = create_region(world, player, active_locations, LocationName.chao_garden_beginner_region,
- chao_garden_beginner_region_locations)
+ chao_race_beginner_region = create_region(multiworld, player, active_locations, LocationName.chao_race_beginner_region,
+ chao_race_beginner_region_locations)
+
+ chao_karate_beginner_region_locations = [
+ LocationName.chao_beginner_karate_1,
+ LocationName.chao_beginner_karate_2,
+ LocationName.chao_beginner_karate_3,
+ LocationName.chao_beginner_karate_4,
+ LocationName.chao_beginner_karate_5,
+ ]
+ chao_karate_beginner_region = create_region(multiworld, player, active_locations, LocationName.chao_karate_beginner_region,
+ chao_karate_beginner_region_locations)
- chao_garden_intermediate_region_locations = [
+ chao_race_intermediate_region_locations = [
LocationName.chao_race_challenge_1,
LocationName.chao_race_challenge_2,
LocationName.chao_race_challenge_3,
@@ -1350,13 +1367,21 @@ def create_regions(world, player: int, active_locations):
LocationName.chao_race_dark_2,
LocationName.chao_race_dark_3,
LocationName.chao_race_dark_4,
-
- LocationName.chao_standard_karate,
]
- chao_garden_intermediate_region = create_region(world, player, active_locations, LocationName.chao_garden_intermediate_region,
- chao_garden_intermediate_region_locations)
+ chao_race_intermediate_region = create_region(multiworld, player, active_locations, LocationName.chao_race_intermediate_region,
+ chao_race_intermediate_region_locations)
+
+ chao_karate_intermediate_region_locations = [
+ LocationName.chao_standard_karate_1,
+ LocationName.chao_standard_karate_2,
+ LocationName.chao_standard_karate_3,
+ LocationName.chao_standard_karate_4,
+ LocationName.chao_standard_karate_5,
+ ]
+ chao_karate_intermediate_region = create_region(multiworld, player, active_locations, LocationName.chao_karate_intermediate_region,
+ chao_karate_intermediate_region_locations)
- chao_garden_expert_region_locations = [
+ chao_race_expert_region_locations = [
LocationName.chao_race_aquamarine_1,
LocationName.chao_race_aquamarine_2,
LocationName.chao_race_aquamarine_3,
@@ -1387,15 +1412,266 @@ def create_regions(world, player: int, active_locations):
LocationName.chao_race_diamond_3,
LocationName.chao_race_diamond_4,
LocationName.chao_race_diamond_5,
-
- LocationName.chao_expert_karate,
- LocationName.chao_super_karate,
]
- chao_garden_expert_region = create_region(world, player, active_locations, LocationName.chao_garden_expert_region,
- chao_garden_expert_region_locations)
+ chao_race_expert_region = create_region(multiworld, player, active_locations, LocationName.chao_race_expert_region,
+ chao_race_expert_region_locations)
+
+ chao_karate_expert_region_locations = [
+ LocationName.chao_expert_karate_1,
+ LocationName.chao_expert_karate_2,
+ LocationName.chao_expert_karate_3,
+ LocationName.chao_expert_karate_4,
+ LocationName.chao_expert_karate_5,
+ ]
+ chao_karate_expert_region = create_region(multiworld, player, active_locations, LocationName.chao_karate_expert_region,
+ chao_karate_expert_region_locations)
+
+ chao_karate_super_region_locations = [
+ LocationName.chao_super_karate_1,
+ LocationName.chao_super_karate_2,
+ LocationName.chao_super_karate_3,
+ LocationName.chao_super_karate_4,
+ LocationName.chao_super_karate_5,
+ ]
+ chao_karate_super_region = create_region(multiworld, player, active_locations, LocationName.chao_karate_super_region,
+ chao_karate_super_region_locations)
+
+ if world.options.goal == 7 or world.options.chao_animal_parts:
+ animal_penguin_region_locations = [
+ LocationName.animal_penguin,
+ LocationName.chao_penguin_arms,
+ LocationName.chao_penguin_forehead,
+ LocationName.chao_penguin_legs,
+ ]
+ animal_penguin_region = create_region(multiworld, player, active_locations, LocationName.animal_penguin,
+ animal_penguin_region_locations)
+ conditional_regions += [animal_penguin_region]
+
+ animal_seal_region_locations = [
+ LocationName.animal_seal,
+ LocationName.chao_seal_arms,
+ LocationName.chao_seal_tail,
+ ]
+ animal_seal_region = create_region(multiworld, player, active_locations, LocationName.animal_seal,
+ animal_seal_region_locations)
+ conditional_regions += [animal_seal_region]
+
+ animal_otter_region_locations = [
+ LocationName.animal_otter,
+ LocationName.chao_otter_arms,
+ LocationName.chao_otter_ears,
+ LocationName.chao_otter_face,
+ LocationName.chao_otter_legs,
+ LocationName.chao_otter_tail,
+ ]
+ animal_otter_region = create_region(multiworld, player, active_locations, LocationName.animal_otter,
+ animal_otter_region_locations)
+ conditional_regions += [animal_otter_region]
+
+ animal_rabbit_region_locations = [
+ LocationName.animal_rabbit,
+ LocationName.chao_rabbit_arms,
+ LocationName.chao_rabbit_ears,
+ LocationName.chao_rabbit_legs,
+ LocationName.chao_rabbit_tail,
+ ]
+ animal_rabbit_region = create_region(multiworld, player, active_locations, LocationName.animal_rabbit,
+ animal_rabbit_region_locations)
+ conditional_regions += [animal_rabbit_region]
+
+ animal_cheetah_region_locations = [
+ LocationName.animal_cheetah,
+ LocationName.chao_cheetah_arms,
+ LocationName.chao_cheetah_ears,
+ LocationName.chao_cheetah_legs,
+ LocationName.chao_cheetah_tail,
+ ]
+ animal_cheetah_region = create_region(multiworld, player, active_locations, LocationName.animal_cheetah,
+ animal_cheetah_region_locations)
+ conditional_regions += [animal_cheetah_region]
+
+ animal_warthog_region_locations = [
+ LocationName.animal_warthog,
+ LocationName.chao_warthog_arms,
+ LocationName.chao_warthog_ears,
+ LocationName.chao_warthog_face,
+ LocationName.chao_warthog_legs,
+ LocationName.chao_warthog_tail,
+ ]
+ animal_warthog_region = create_region(multiworld, player, active_locations, LocationName.animal_warthog,
+ animal_warthog_region_locations)
+ conditional_regions += [animal_warthog_region]
+
+ animal_bear_region_locations = [
+ LocationName.animal_bear,
+ LocationName.chao_bear_arms,
+ LocationName.chao_bear_ears,
+ LocationName.chao_bear_legs,
+ ]
+ animal_bear_region = create_region(multiworld, player, active_locations, LocationName.animal_bear,
+ animal_bear_region_locations)
+ conditional_regions += [animal_bear_region]
+
+ animal_tiger_region_locations = [
+ LocationName.animal_tiger,
+ LocationName.chao_tiger_arms,
+ LocationName.chao_tiger_ears,
+ LocationName.chao_tiger_legs,
+ LocationName.chao_tiger_tail,
+ ]
+ animal_tiger_region = create_region(multiworld, player, active_locations, LocationName.animal_tiger,
+ animal_tiger_region_locations)
+ conditional_regions += [animal_tiger_region]
+
+ animal_gorilla_region_locations = [
+ LocationName.animal_gorilla,
+ LocationName.chao_gorilla_arms,
+ LocationName.chao_gorilla_ears,
+ LocationName.chao_gorilla_forehead,
+ LocationName.chao_gorilla_legs,
+ ]
+ animal_gorilla_region = create_region(multiworld, player, active_locations, LocationName.animal_gorilla,
+ animal_gorilla_region_locations)
+ conditional_regions += [animal_gorilla_region]
+
+ animal_peacock_region_locations = [
+ LocationName.animal_peacock,
+ LocationName.chao_peacock_forehead,
+ LocationName.chao_peacock_legs,
+ LocationName.chao_peacock_tail,
+ LocationName.chao_peacock_wings,
+ ]
+ animal_peacock_region = create_region(multiworld, player, active_locations, LocationName.animal_peacock,
+ animal_peacock_region_locations)
+ conditional_regions += [animal_peacock_region]
+
+ animal_parrot_region_locations = [
+ LocationName.animal_parrot,
+ LocationName.chao_parrot_forehead,
+ LocationName.chao_parrot_legs,
+ LocationName.chao_parrot_tail,
+ LocationName.chao_parrot_wings,
+ ]
+ animal_parrot_region = create_region(multiworld, player, active_locations, LocationName.animal_parrot,
+ animal_parrot_region_locations)
+ conditional_regions += [animal_parrot_region]
+
+ animal_condor_region_locations = [
+ LocationName.animal_condor,
+ LocationName.chao_condor_ears,
+ LocationName.chao_condor_legs,
+ LocationName.chao_condor_tail,
+ LocationName.chao_condor_wings,
+ ]
+ animal_condor_region = create_region(multiworld, player, active_locations, LocationName.animal_condor,
+ animal_condor_region_locations)
+ conditional_regions += [animal_condor_region]
+
+ animal_skunk_region_locations = [
+ LocationName.animal_skunk,
+ LocationName.chao_skunk_arms,
+ LocationName.chao_skunk_forehead,
+ LocationName.chao_skunk_legs,
+ LocationName.chao_skunk_tail,
+ ]
+ animal_skunk_region = create_region(multiworld, player, active_locations, LocationName.animal_skunk,
+ animal_skunk_region_locations)
+ conditional_regions += [animal_skunk_region]
+
+ animal_sheep_region_locations = [
+ LocationName.animal_sheep,
+ LocationName.chao_sheep_arms,
+ LocationName.chao_sheep_ears,
+ LocationName.chao_sheep_legs,
+ LocationName.chao_sheep_horn,
+ LocationName.chao_sheep_tail,
+ ]
+ animal_sheep_region = create_region(multiworld, player, active_locations, LocationName.animal_sheep,
+ animal_sheep_region_locations)
+ conditional_regions += [animal_sheep_region]
+
+ animal_raccoon_region_locations = [
+ LocationName.animal_raccoon,
+ LocationName.chao_raccoon_arms,
+ LocationName.chao_raccoon_ears,
+ LocationName.chao_raccoon_legs,
+ ]
+ animal_raccoon_region = create_region(multiworld, player, active_locations, LocationName.animal_raccoon,
+ animal_raccoon_region_locations)
+ conditional_regions += [animal_raccoon_region]
+
+ animal_halffish_region_locations = [
+ LocationName.animal_halffish,
+ ]
+ animal_halffish_region = create_region(multiworld, player, active_locations, LocationName.animal_halffish,
+ animal_halffish_region_locations)
+ conditional_regions += [animal_halffish_region]
+
+ animal_skeleton_dog_region_locations = [
+ LocationName.animal_skeleton_dog,
+ ]
+ animal_skeleton_dog_region = create_region(multiworld, player, active_locations, LocationName.animal_skeleton_dog,
+ animal_skeleton_dog_region_locations)
+ conditional_regions += [animal_skeleton_dog_region]
+
+ animal_bat_region_locations = [
+ LocationName.animal_bat,
+ ]
+ animal_bat_region = create_region(multiworld, player, active_locations, LocationName.animal_bat,
+ animal_bat_region_locations)
+ conditional_regions += [animal_bat_region]
+
+ animal_dragon_region_locations = [
+ LocationName.animal_dragon,
+ LocationName.chao_dragon_arms,
+ LocationName.chao_dragon_ears,
+ LocationName.chao_dragon_legs,
+ LocationName.chao_dragon_horn,
+ LocationName.chao_dragon_tail,
+ LocationName.chao_dragon_wings,
+ ]
+ animal_dragon_region = create_region(multiworld, player, active_locations, LocationName.animal_dragon,
+ animal_dragon_region_locations)
+ conditional_regions += [animal_dragon_region]
+
+ animal_unicorn_region_locations = [
+ LocationName.animal_unicorn,
+ LocationName.chao_unicorn_arms,
+ LocationName.chao_unicorn_ears,
+ LocationName.chao_unicorn_forehead,
+ LocationName.chao_unicorn_legs,
+ LocationName.chao_unicorn_tail,
+ ]
+ animal_unicorn_region = create_region(multiworld, player, active_locations, LocationName.animal_unicorn,
+ animal_unicorn_region_locations)
+ conditional_regions += [animal_unicorn_region]
+
+ animal_phoenix_region_locations = [
+ LocationName.animal_phoenix,
+ LocationName.chao_phoenix_forehead,
+ LocationName.chao_phoenix_legs,
+ LocationName.chao_phoenix_tail,
+ LocationName.chao_phoenix_wings,
+ ]
+ animal_phoenix_region = create_region(multiworld, player, active_locations, LocationName.animal_phoenix,
+ animal_phoenix_region_locations)
+ conditional_regions += [animal_phoenix_region]
+
+ if world.options.chao_kindergarten:
+ chao_kindergarten_region_locations = list(chao_kindergarten_location_table.keys()) + list(chao_kindergarten_basics_location_table.keys())
+ chao_kindergarten_region = create_region(multiworld, player, active_locations, LocationName.chao_kindergarten_region,
+ chao_kindergarten_region_locations)
+ conditional_regions += [chao_kindergarten_region]
+
+ if world.options.black_market_slots.value > 0:
+
+ black_market_region_locations = list(black_market_location_table.keys())
+ black_market_region = create_region(multiworld, player, active_locations, LocationName.black_market_region,
+ black_market_region_locations)
+ conditional_regions += [black_market_region]
kart_race_beginner_region_locations = []
- if world.kart_race_checks[player] == 2:
+ if world.options.kart_race_checks == 2:
kart_race_beginner_region_locations.extend([
LocationName.kart_race_beginner_sonic,
LocationName.kart_race_beginner_tails,
@@ -1404,13 +1680,13 @@ def create_regions(world, player: int, active_locations):
LocationName.kart_race_beginner_eggman,
LocationName.kart_race_beginner_rouge,
])
- if world.kart_race_checks[player] == 1:
+ if world.options.kart_race_checks == 1:
kart_race_beginner_region_locations.append(LocationName.kart_race_beginner)
- kart_race_beginner_region = create_region(world, player, active_locations, LocationName.kart_race_beginner_region,
+ kart_race_beginner_region = create_region(multiworld, player, active_locations, LocationName.kart_race_beginner_region,
kart_race_beginner_region_locations)
kart_race_standard_region_locations = []
- if world.kart_race_checks[player] == 2:
+ if world.options.kart_race_checks == 2:
kart_race_standard_region_locations.extend([
LocationName.kart_race_standard_sonic,
LocationName.kart_race_standard_tails,
@@ -1419,13 +1695,13 @@ def create_regions(world, player: int, active_locations):
LocationName.kart_race_standard_eggman,
LocationName.kart_race_standard_rouge,
])
- if world.kart_race_checks[player] == 1:
+ if world.options.kart_race_checks == 1:
kart_race_standard_region_locations.append(LocationName.kart_race_standard)
- kart_race_standard_region = create_region(world, player, active_locations, LocationName.kart_race_standard_region,
+ kart_race_standard_region = create_region(multiworld, player, active_locations, LocationName.kart_race_standard_region,
kart_race_standard_region_locations)
kart_race_expert_region_locations = []
- if world.kart_race_checks[player] == 2:
+ if world.options.kart_race_checks == 2:
kart_race_expert_region_locations.extend([
LocationName.kart_race_expert_sonic,
LocationName.kart_race_expert_tails,
@@ -1434,51 +1710,56 @@ def create_regions(world, player: int, active_locations):
LocationName.kart_race_expert_eggman,
LocationName.kart_race_expert_rouge,
])
- if world.kart_race_checks[player] == 1:
+ if world.options.kart_race_checks == 1:
kart_race_expert_region_locations.append(LocationName.kart_race_expert)
- kart_race_expert_region = create_region(world, player, active_locations, LocationName.kart_race_expert_region,
+ kart_race_expert_region = create_region(multiworld, player, active_locations, LocationName.kart_race_expert_region,
kart_race_expert_region_locations)
- if world.goal[player] == 3:
+ if world.options.goal == 3:
grand_prix_region_locations = [
LocationName.grand_prix,
]
- grand_prix_region = create_region(world, player, active_locations, LocationName.grand_prix_region,
+ grand_prix_region = create_region(multiworld, player, active_locations, LocationName.grand_prix_region,
grand_prix_region_locations)
- world.regions += [grand_prix_region]
-
- if world.goal[player] in [0, 2, 4, 5, 6]:
+ conditional_regions += [grand_prix_region]
+ elif world.options.goal in [0, 2, 4, 5, 6]:
biolizard_region_locations = [
LocationName.finalhazard,
]
- biolizard_region = create_region(world, player, active_locations, LocationName.biolizard_region,
+ biolizard_region = create_region(multiworld, player, active_locations, LocationName.biolizard_region,
biolizard_region_locations)
- world.regions += [biolizard_region]
+ conditional_regions += [biolizard_region]
+ elif world.options.goal == 7:
+ chaos_chao_region_locations = [
+ LocationName.chaos_chao,
+ ]
+ chaos_chao_region = create_region(multiworld, player, active_locations, LocationName.chaos_chao_region,
+ chaos_chao_region_locations)
+ conditional_regions += [chaos_chao_region]
- if world.goal[player] in [1, 2]:
+ if world.options.goal in [1, 2]:
green_hill_region_locations = [
LocationName.green_hill,
LocationName.green_hill_chao_1,
#LocationName.green_hill_animal_1,
]
- green_hill_region = create_region(world, player, active_locations, LocationName.green_hill_region,
+ green_hill_region = create_region(multiworld, player, active_locations, LocationName.green_hill_region,
green_hill_region_locations)
- world.regions += [green_hill_region]
+ conditional_regions += [green_hill_region]
- if world.goal[player] in [4, 5, 6]:
+ if world.options.goal in [4, 5, 6]:
for i in range(16):
boss_region_locations = [
"Boss Rush - " + str(i + 1),
]
- boss_region = create_region(world, player, active_locations, "Boss Rush " + str(i + 1),
+ boss_region = create_region(multiworld, player, active_locations, "Boss Rush " + str(i + 1),
boss_region_locations)
- world.regions += [boss_region]
+ conditional_regions += [boss_region]
# Set up the regions correctly.
- world.regions += [
+ multiworld.regions += [
menu_region,
- gate_0_region,
city_escape_region,
metal_harbor_region,
green_forest_region,
@@ -1510,32 +1791,38 @@ def create_regions(world, player: int, active_locations):
route_280_region,
mad_space_region,
cannon_core_region,
- chao_garden_beginner_region,
- chao_garden_intermediate_region,
- chao_garden_expert_region,
+ chao_race_beginner_region,
+ chao_karate_beginner_region,
+ chao_race_intermediate_region,
+ chao_karate_intermediate_region,
+ chao_race_expert_region,
+ chao_karate_expert_region,
+ chao_karate_super_region,
kart_race_beginner_region,
kart_race_standard_region,
kart_race_expert_region,
]
+ multiworld.regions += conditional_regions
-def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_emblems, gate_bosses, boss_rush_bosses, first_cannons_core_mission: str, final_cannons_core_mission: str):
+
+def connect_regions(multiworld: MultiWorld, world: World, player: int, gates: typing.List[LevelGate], cannon_core_emblems, gate_bosses, boss_rush_bosses, first_cannons_core_mission: str, final_cannons_core_mission: str):
names: typing.Dict[str, int] = {}
- connect(world, player, names, 'Menu', LocationName.gate_0_region)
- connect(world, player, names, LocationName.gate_0_region, LocationName.cannon_core_region,
+ connect(multiworld, player, names, 'Menu', LocationName.gate_0_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.cannon_core_region,
lambda state: (state.has(ItemName.emblem, player, cannon_core_emblems)))
- if world.goal[player] == 0:
+ if world.options.goal == 0:
required_mission_name = first_cannons_core_mission
- if world.required_cannons_core_missions[player].value == 1:
+ if world.options.required_cannons_core_missions.value == 1:
required_mission_name = final_cannons_core_mission
- connect(world, player, names, LocationName.cannon_core_region, LocationName.biolizard_region,
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.biolizard_region,
lambda state: (state.can_reach(required_mission_name, "Location", player)))
- elif world.goal[player] in [1, 2]:
- connect(world, player, names, 'Menu', LocationName.green_hill_region,
+ elif world.options.goal in [1, 2]:
+ connect(multiworld, player, names, 'Menu', LocationName.green_hill_region,
lambda state: (state.has(ItemName.white_emerald, player) and
state.has(ItemName.red_emerald, player) and
state.has(ItemName.cyan_emerald, player) and
@@ -1543,23 +1830,23 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em
state.has(ItemName.green_emerald, player) and
state.has(ItemName.yellow_emerald, player) and
state.has(ItemName.blue_emerald, player)))
- if world.goal[player] == 2:
- connect(world, player, names, LocationName.green_hill_region, LocationName.biolizard_region)
- elif world.goal[player] == 3:
- connect(world, player, names, LocationName.kart_race_expert_region, LocationName.grand_prix_region)
- elif world.goal[player] in [4, 5, 6]:
- if world.goal[player] == 4:
- connect(world, player, names, LocationName.gate_0_region, LocationName.boss_rush_1_region)
- elif world.goal[player] == 5:
+ if world.options.goal == 2:
+ connect(multiworld, player, names, LocationName.green_hill_region, LocationName.biolizard_region)
+ elif world.options.goal == 3:
+ connect(multiworld, player, names, LocationName.kart_race_expert_region, LocationName.grand_prix_region)
+ elif world.options.goal in [4, 5, 6]:
+ if world.options.goal == 4:
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.boss_rush_1_region)
+ elif world.options.goal == 5:
required_mission_name = first_cannons_core_mission
- if world.required_cannons_core_missions[player].value == 1:
+ if world.options.required_cannons_core_missions.value == 1:
required_mission_name = final_cannons_core_mission
- connect(world, player, names, LocationName.cannon_core_region, LocationName.boss_rush_1_region,
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.boss_rush_1_region,
lambda state: (state.can_reach(required_mission_name, "Location", player)))
- elif world.goal[player] == 6:
- connect(world, player, names, LocationName.gate_0_region, LocationName.boss_rush_1_region,
+ elif world.options.goal == 6:
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.boss_rush_1_region,
lambda state: (state.has(ItemName.white_emerald, player) and
state.has(ItemName.red_emerald, player) and
state.has(ItemName.cyan_emerald, player) and
@@ -1570,134 +1857,576 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em
for i in range(15):
if boss_rush_bosses[i] == all_gate_bosses_table[king_boom_boo]:
- connect(world, player, names, "Boss Rush " + str(i + 1), "Boss Rush " + str(i + 2),
+ connect(multiworld, player, names, "Boss Rush " + str(i + 1), "Boss Rush " + str(i + 2),
lambda state: (state.has(ItemName.knuckles_shovel_claws, player)))
else:
- connect(world, player, names, "Boss Rush " + str(i + 1), "Boss Rush " + str(i + 2))
+ connect(multiworld, player, names, "Boss Rush " + str(i + 1), "Boss Rush " + str(i + 2))
- connect(world, player, names, LocationName.boss_rush_16_region, LocationName.biolizard_region)
+ connect(multiworld, player, names, LocationName.boss_rush_16_region, LocationName.biolizard_region)
+ elif world.options.goal == 7:
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chaos_chao,
+ lambda state: (state.has_all(chao_animal_event_location_table.keys(), player)))
for i in range(len(gates[0].gate_levels)):
- connect(world, player, names, LocationName.gate_0_region, shuffleable_regions[gates[0].gate_levels[i]])
+ connect(multiworld, player, names, LocationName.gate_0_region, shuffleable_regions[gates[0].gate_levels[i]])
gates_len = len(gates)
if gates_len >= 2:
- connect(world, player, names, LocationName.gate_0_region, LocationName.gate_1_boss_region,
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.gate_1_boss_region,
lambda state: (state.has(ItemName.emblem, player, gates[1].gate_emblem_count)))
if gate_bosses[1] == all_gate_bosses_table[king_boom_boo]:
- connect(world, player, names, LocationName.gate_1_boss_region, LocationName.gate_1_region,
+ connect(multiworld, player, names, LocationName.gate_1_boss_region, LocationName.gate_1_region,
lambda state: (state.has(ItemName.knuckles_shovel_claws, player)))
else:
- connect(world, player, names, LocationName.gate_1_boss_region, LocationName.gate_1_region)
+ connect(multiworld, player, names, LocationName.gate_1_boss_region, LocationName.gate_1_region)
for i in range(len(gates[1].gate_levels)):
- connect(world, player, names, LocationName.gate_1_region, shuffleable_regions[gates[1].gate_levels[i]])
+ connect(multiworld, player, names, LocationName.gate_1_region, shuffleable_regions[gates[1].gate_levels[i]])
if gates_len >= 3:
- connect(world, player, names, LocationName.gate_1_region, LocationName.gate_2_boss_region,
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.gate_2_boss_region,
lambda state: (state.has(ItemName.emblem, player, gates[2].gate_emblem_count)))
if gate_bosses[2] == all_gate_bosses_table[king_boom_boo]:
- connect(world, player, names, LocationName.gate_2_boss_region, LocationName.gate_2_region,
+ connect(multiworld, player, names, LocationName.gate_2_boss_region, LocationName.gate_2_region,
lambda state: (state.has(ItemName.knuckles_shovel_claws, player)))
else:
- connect(world, player, names, LocationName.gate_2_boss_region, LocationName.gate_2_region)
+ connect(multiworld, player, names, LocationName.gate_2_boss_region, LocationName.gate_2_region)
for i in range(len(gates[2].gate_levels)):
- connect(world, player, names, LocationName.gate_2_region, shuffleable_regions[gates[2].gate_levels[i]])
+ connect(multiworld, player, names, LocationName.gate_2_region, shuffleable_regions[gates[2].gate_levels[i]])
if gates_len >= 4:
- connect(world, player, names, LocationName.gate_2_region, LocationName.gate_3_boss_region,
+ connect(multiworld, player, names, LocationName.gate_2_region, LocationName.gate_3_boss_region,
lambda state: (state.has(ItemName.emblem, player, gates[3].gate_emblem_count)))
if gate_bosses[3] == all_gate_bosses_table[king_boom_boo]:
- connect(world, player, names, LocationName.gate_3_boss_region, LocationName.gate_3_region,
+ connect(multiworld, player, names, LocationName.gate_3_boss_region, LocationName.gate_3_region,
lambda state: (state.has(ItemName.knuckles_shovel_claws, player)))
else:
- connect(world, player, names, LocationName.gate_3_boss_region, LocationName.gate_3_region)
+ connect(multiworld, player, names, LocationName.gate_3_boss_region, LocationName.gate_3_region)
for i in range(len(gates[3].gate_levels)):
- connect(world, player, names, LocationName.gate_3_region, shuffleable_regions[gates[3].gate_levels[i]])
+ connect(multiworld, player, names, LocationName.gate_3_region, shuffleable_regions[gates[3].gate_levels[i]])
if gates_len >= 5:
- connect(world, player, names, LocationName.gate_3_region, LocationName.gate_4_boss_region,
+ connect(multiworld, player, names, LocationName.gate_3_region, LocationName.gate_4_boss_region,
lambda state: (state.has(ItemName.emblem, player, gates[4].gate_emblem_count)))
if gate_bosses[4] == all_gate_bosses_table[king_boom_boo]:
- connect(world, player, names, LocationName.gate_4_boss_region, LocationName.gate_4_region,
+ connect(multiworld, player, names, LocationName.gate_4_boss_region, LocationName.gate_4_region,
lambda state: (state.has(ItemName.knuckles_shovel_claws, player)))
else:
- connect(world, player, names, LocationName.gate_4_boss_region, LocationName.gate_4_region)
+ connect(multiworld, player, names, LocationName.gate_4_boss_region, LocationName.gate_4_region)
for i in range(len(gates[4].gate_levels)):
- connect(world, player, names, LocationName.gate_4_region, shuffleable_regions[gates[4].gate_levels[i]])
+ connect(multiworld, player, names, LocationName.gate_4_region, shuffleable_regions[gates[4].gate_levels[i]])
if gates_len >= 6:
- connect(world, player, names, LocationName.gate_4_region, LocationName.gate_5_boss_region,
+ connect(multiworld, player, names, LocationName.gate_4_region, LocationName.gate_5_boss_region,
lambda state: (state.has(ItemName.emblem, player, gates[5].gate_emblem_count)))
if gate_bosses[5] == all_gate_bosses_table[king_boom_boo]:
- connect(world, player, names, LocationName.gate_5_boss_region, LocationName.gate_5_region,
+ connect(multiworld, player, names, LocationName.gate_5_boss_region, LocationName.gate_5_region,
lambda state: (state.has(ItemName.knuckles_shovel_claws, player)))
else:
- connect(world, player, names, LocationName.gate_5_boss_region, LocationName.gate_5_region)
+ connect(multiworld, player, names, LocationName.gate_5_boss_region, LocationName.gate_5_region)
for i in range(len(gates[5].gate_levels)):
- connect(world, player, names, LocationName.gate_5_region, shuffleable_regions[gates[5].gate_levels[i]])
+ connect(multiworld, player, names, LocationName.gate_5_region, shuffleable_regions[gates[5].gate_levels[i]])
if gates_len == 1:
- connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_beginner_region)
- connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_intermediate_region)
- connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_expert_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_race_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_race_intermediate_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_race_expert_region)
+
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_karate_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_karate_intermediate_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_karate_expert_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_karate_super_region)
+
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.kart_race_standard_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.kart_race_expert_region)
- connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region)
- connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_standard_region)
- connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_expert_region)
+ if world.options.chao_kindergarten:
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_kindergarten_region)
elif gates_len == 2:
- connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_beginner_region)
- connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_intermediate_region)
- connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_expert_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_race_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_race_intermediate_region)
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.chao_race_expert_region)
+
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_karate_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_karate_intermediate_region)
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.chao_karate_expert_region)
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.chao_karate_super_region)
+
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.kart_race_standard_region)
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.kart_race_expert_region)
- connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region)
- connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_standard_region)
- connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_expert_region)
+ if world.options.chao_kindergarten:
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.chao_kindergarten_region)
elif gates_len == 3:
- connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_beginner_region)
- connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_intermediate_region)
- connect(world, player, names, LocationName.gate_2_region, LocationName.chao_garden_expert_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_race_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.chao_race_intermediate_region)
+ connect(multiworld, player, names, LocationName.gate_2_region, LocationName.chao_race_expert_region)
- connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region)
- connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_standard_region)
- connect(world, player, names, LocationName.gate_2_region, LocationName.kart_race_expert_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_karate_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.chao_karate_intermediate_region)
+ connect(multiworld, player, names, LocationName.gate_2_region, LocationName.chao_karate_expert_region)
+ connect(multiworld, player, names, LocationName.gate_2_region, LocationName.chao_karate_super_region)
+
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.kart_race_standard_region)
+ connect(multiworld, player, names, LocationName.gate_2_region, LocationName.kart_race_expert_region)
+
+ if world.options.chao_kindergarten:
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.chao_kindergarten_region)
elif gates_len == 4:
- connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_beginner_region)
- connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_intermediate_region)
- connect(world, player, names, LocationName.gate_3_region, LocationName.chao_garden_expert_region)
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_race_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.chao_race_intermediate_region)
+ connect(multiworld, player, names, LocationName.gate_3_region, LocationName.chao_race_expert_region)
+
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.chao_karate_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.chao_karate_intermediate_region)
+ connect(multiworld, player, names, LocationName.gate_2_region, LocationName.chao_karate_expert_region)
+ connect(multiworld, player, names, LocationName.gate_3_region, LocationName.chao_karate_super_region)
+
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.kart_race_standard_region)
+ connect(multiworld, player, names, LocationName.gate_3_region, LocationName.kart_race_expert_region)
- connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region)
- connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_standard_region)
- connect(world, player, names, LocationName.gate_3_region, LocationName.kart_race_expert_region)
+ if world.options.chao_kindergarten:
+ connect(multiworld, player, names, LocationName.gate_2_region, LocationName.chao_kindergarten_region)
elif gates_len == 5:
- connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_beginner_region)
- connect(world, player, names, LocationName.gate_2_region, LocationName.chao_garden_intermediate_region)
- connect(world, player, names, LocationName.gate_3_region, LocationName.chao_garden_expert_region)
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.chao_race_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_2_region, LocationName.chao_race_intermediate_region)
+ connect(multiworld, player, names, LocationName.gate_3_region, LocationName.chao_race_expert_region)
- connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_beginner_region)
- connect(world, player, names, LocationName.gate_2_region, LocationName.kart_race_standard_region)
- connect(world, player, names, LocationName.gate_3_region, LocationName.kart_race_expert_region)
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.chao_karate_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_2_region, LocationName.chao_karate_intermediate_region)
+ connect(multiworld, player, names, LocationName.gate_3_region, LocationName.chao_karate_expert_region)
+ connect(multiworld, player, names, LocationName.gate_4_region, LocationName.chao_karate_super_region)
+
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.kart_race_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_2_region, LocationName.kart_race_standard_region)
+ connect(multiworld, player, names, LocationName.gate_3_region, LocationName.kart_race_expert_region)
+
+ if world.options.chao_kindergarten:
+ connect(multiworld, player, names, LocationName.gate_3_region, LocationName.chao_kindergarten_region)
elif gates_len >= 6:
- connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_beginner_region)
- connect(world, player, names, LocationName.gate_2_region, LocationName.chao_garden_intermediate_region)
- connect(world, player, names, LocationName.gate_4_region, LocationName.chao_garden_expert_region)
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.chao_race_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_2_region, LocationName.chao_race_intermediate_region)
+ connect(multiworld, player, names, LocationName.gate_4_region, LocationName.chao_race_expert_region)
+
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.chao_karate_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_2_region, LocationName.chao_karate_intermediate_region)
+ connect(multiworld, player, names, LocationName.gate_3_region, LocationName.chao_karate_expert_region)
+ connect(multiworld, player, names, LocationName.gate_4_region, LocationName.chao_karate_super_region)
+
+ connect(multiworld, player, names, LocationName.gate_1_region, LocationName.kart_race_beginner_region)
+ connect(multiworld, player, names, LocationName.gate_2_region, LocationName.kart_race_standard_region)
+ connect(multiworld, player, names, LocationName.gate_4_region, LocationName.kart_race_expert_region)
+
+ if world.options.chao_kindergarten:
+ connect(multiworld, player, names, LocationName.gate_3_region, LocationName.chao_kindergarten_region)
+
+ stat_checks_per_gate = world.options.chao_stats.value / (gates_len)
+ for index in range(1, world.options.chao_stats.value + 1):
+ if (index % world.options.chao_stats_frequency.value) == (world.options.chao_stats.value % world.options.chao_stats_frequency.value):
+ gate_val = math.ceil(index / stat_checks_per_gate) - 1
+ gate_region = multiworld.get_region("Gate " + str(gate_val), player)
+
+ loc_name_swim = LocationName.chao_stat_swim_base + str(index)
+ loc_id_swim = chao_stat_swim_table[loc_name_swim]
+ location_swim = SA2BLocation(player, loc_name_swim, loc_id_swim, gate_region)
+ gate_region.locations.append(location_swim)
+
+ loc_name_fly = LocationName.chao_stat_fly_base + str(index)
+ loc_id_fly = chao_stat_fly_table[loc_name_fly]
+ location_fly = SA2BLocation(player, loc_name_fly, loc_id_fly, gate_region)
+ gate_region.locations.append(location_fly)
+
+ loc_name_run = LocationName.chao_stat_run_base + str(index)
+ loc_id_run = chao_stat_run_table[loc_name_run]
+ location_run = SA2BLocation(player, loc_name_run, loc_id_run, gate_region)
+ gate_region.locations.append(location_run)
+
+ loc_name_power = LocationName.chao_stat_power_base + str(index)
+ loc_id_power = chao_stat_power_table[loc_name_power]
+ location_power = SA2BLocation(player, loc_name_power, loc_id_power, gate_region)
+ gate_region.locations.append(location_power)
+
+ if world.options.chao_stats_stamina:
+ loc_name_stamina = LocationName.chao_stat_stamina_base + str(index)
+ loc_id_stamina = chao_stat_stamina_table[loc_name_stamina]
+ location_stamina = SA2BLocation(player, loc_name_stamina, loc_id_stamina, gate_region)
+ gate_region.locations.append(location_stamina)
+
+ if world.options.chao_stats_hidden:
+ loc_name_luck = LocationName.chao_stat_luck_base + str(index)
+ loc_id_luck = chao_stat_luck_table[loc_name_luck]
+ location_luck = SA2BLocation(player, loc_name_luck, loc_id_luck, gate_region)
+ gate_region.locations.append(location_luck)
+
+ loc_name_intelligence = LocationName.chao_stat_intelligence_base + str(index)
+ loc_id_intelligence = chao_stat_intelligence_table[loc_name_intelligence]
+ location_intelligence = SA2BLocation(player, loc_name_intelligence, loc_id_intelligence, gate_region)
+ gate_region.locations.append(location_intelligence)
+
+ # Handle access to Animal Parts
+ if world.options.goal == 7 or world.options.chao_animal_parts:
+ connect(multiworld, player, names, LocationName.city_escape_region, LocationName.animal_rabbit)
+ connect(multiworld, player, names, LocationName.city_escape_region, LocationName.animal_skunk)
+ connect(multiworld, player, names, LocationName.city_escape_region, LocationName.animal_sheep)
+ connect(multiworld, player, names, LocationName.city_escape_region, LocationName.animal_raccoon)
+
+ connect(multiworld, player, names, LocationName.wild_canyon_region, LocationName.animal_cheetah)
+ connect(multiworld, player, names, LocationName.wild_canyon_region, LocationName.animal_peacock)
+ connect(multiworld, player, names, LocationName.wild_canyon_region, LocationName.animal_condor)
+ connect(multiworld, player, names, LocationName.wild_canyon_region, LocationName.animal_sheep)
+
+ connect(multiworld, player, names, LocationName.prison_lane_region, LocationName.animal_otter)
+ connect(multiworld, player, names, LocationName.prison_lane_region, LocationName.animal_tiger)
+ connect(multiworld, player, names, LocationName.prison_lane_region, LocationName.animal_gorilla)
+ connect(multiworld, player, names, LocationName.prison_lane_region, LocationName.animal_sheep)
+ connect(multiworld, player, names, LocationName.prison_lane_region, LocationName.animal_unicorn,
+ lambda state: (state.has(ItemName.tails_booster, player)))
+
+ connect(multiworld, player, names, LocationName.metal_harbor_region, LocationName.animal_penguin)
+ connect(multiworld, player, names, LocationName.metal_harbor_region, LocationName.animal_seal)
+ connect(multiworld, player, names, LocationName.metal_harbor_region, LocationName.animal_peacock)
+ connect(multiworld, player, names, LocationName.metal_harbor_region, LocationName.animal_raccoon)
+
+ connect(multiworld, player, names, LocationName.green_forest_region, LocationName.animal_rabbit)
+ connect(multiworld, player, names, LocationName.green_forest_region, LocationName.animal_cheetah)
+ connect(multiworld, player, names, LocationName.green_forest_region, LocationName.animal_parrot)
+ connect(multiworld, player, names, LocationName.green_forest_region, LocationName.animal_raccoon)
+ connect(multiworld, player, names, LocationName.green_forest_region, LocationName.animal_halffish)
+
+ connect(multiworld, player, names, LocationName.pumpkin_hill_region, LocationName.animal_cheetah)
+ connect(multiworld, player, names, LocationName.pumpkin_hill_region, LocationName.animal_warthog)
+ connect(multiworld, player, names, LocationName.pumpkin_hill_region, LocationName.animal_skeleton_dog)
+ connect(multiworld, player, names, LocationName.pumpkin_hill_region, LocationName.animal_bat)
+
+ connect(multiworld, player, names, LocationName.mission_street_region, LocationName.animal_rabbit)
+ connect(multiworld, player, names, LocationName.mission_street_region, LocationName.animal_warthog)
+ connect(multiworld, player, names, LocationName.mission_street_region, LocationName.animal_gorilla)
+ connect(multiworld, player, names, LocationName.mission_street_region, LocationName.animal_sheep)
+
+ connect(multiworld, player, names, LocationName.aquatic_mine_region, LocationName.animal_penguin)
+ connect(multiworld, player, names, LocationName.aquatic_mine_region, LocationName.animal_seal)
+ connect(multiworld, player, names, LocationName.aquatic_mine_region, LocationName.animal_condor)
+ connect(multiworld, player, names, LocationName.aquatic_mine_region, LocationName.animal_skunk)
+ connect(multiworld, player, names, LocationName.aquatic_mine_region, LocationName.animal_dragon)
+
+ connect(multiworld, player, names, LocationName.hidden_base_region, LocationName.animal_penguin,
+ lambda state: (state.has(ItemName.tails_booster, player)))
+ connect(multiworld, player, names, LocationName.hidden_base_region, LocationName.animal_otter,
+ lambda state: (state.has(ItemName.tails_booster, player)))
+ connect(multiworld, player, names, LocationName.hidden_base_region, LocationName.animal_tiger,
+ lambda state: (state.has(ItemName.tails_booster, player)))
+ connect(multiworld, player, names, LocationName.hidden_base_region, LocationName.animal_skunk)
+ connect(multiworld, player, names, LocationName.hidden_base_region, LocationName.animal_halffish,
+ lambda state: (state.has(ItemName.tails_booster, player)))
+
+ connect(multiworld, player, names, LocationName.pyramid_cave_region, LocationName.animal_peacock)
+ connect(multiworld, player, names, LocationName.pyramid_cave_region, LocationName.animal_condor)
+ connect(multiworld, player, names, LocationName.pyramid_cave_region, LocationName.animal_sheep)
+ connect(multiworld, player, names, LocationName.pyramid_cave_region, LocationName.animal_bat)
+
+ connect(multiworld, player, names, LocationName.death_chamber_region, LocationName.animal_rabbit)
+ connect(multiworld, player, names, LocationName.death_chamber_region, LocationName.animal_tiger)
+ connect(multiworld, player, names, LocationName.death_chamber_region, LocationName.animal_gorilla)
+ connect(multiworld, player, names, LocationName.death_chamber_region, LocationName.animal_skunk)
+
+ connect(multiworld, player, names, LocationName.eternal_engine_region, LocationName.animal_warthog)
+ connect(multiworld, player, names, LocationName.eternal_engine_region, LocationName.animal_parrot,
+ lambda state: (state.has(ItemName.tails_booster, player)))
+ connect(multiworld, player, names, LocationName.eternal_engine_region, LocationName.animal_condor)
+ connect(multiworld, player, names, LocationName.eternal_engine_region, LocationName.animal_raccoon)
+
+ connect(multiworld, player, names, LocationName.meteor_herd_region, LocationName.animal_penguin)
+ connect(multiworld, player, names, LocationName.meteor_herd_region, LocationName.animal_seal)
+ connect(multiworld, player, names, LocationName.meteor_herd_region, LocationName.animal_rabbit)
+ connect(multiworld, player, names, LocationName.meteor_herd_region, LocationName.animal_sheep)
+ connect(multiworld, player, names, LocationName.meteor_herd_region, LocationName.animal_phoenix)
+
+ connect(multiworld, player, names, LocationName.crazy_gadget_region, LocationName.animal_seal)
+ connect(multiworld, player, names, LocationName.crazy_gadget_region, LocationName.animal_bear)
+ connect(multiworld, player, names, LocationName.crazy_gadget_region, LocationName.animal_tiger)
+
+ connect(multiworld, player, names, LocationName.final_rush_region, LocationName.animal_penguin)
+ connect(multiworld, player, names, LocationName.final_rush_region, LocationName.animal_peacock)
+ connect(multiworld, player, names, LocationName.final_rush_region, LocationName.animal_condor)
+ connect(multiworld, player, names, LocationName.final_rush_region, LocationName.animal_sheep)
+ connect(multiworld, player, names, LocationName.final_rush_region, LocationName.animal_dragon,
+ lambda state: (state.has(ItemName.sonic_bounce_bracelet, player)))
+
+ connect(multiworld, player, names, LocationName.iron_gate_region, LocationName.animal_rabbit)
+ connect(multiworld, player, names, LocationName.iron_gate_region, LocationName.animal_tiger)
+ connect(multiworld, player, names, LocationName.iron_gate_region, LocationName.animal_gorilla)
+ connect(multiworld, player, names, LocationName.iron_gate_region, LocationName.animal_skunk)
+
+ connect(multiworld, player, names, LocationName.dry_lagoon_region, LocationName.animal_penguin)
+ connect(multiworld, player, names, LocationName.dry_lagoon_region, LocationName.animal_otter)
+ connect(multiworld, player, names, LocationName.dry_lagoon_region, LocationName.animal_peacock)
+ connect(multiworld, player, names, LocationName.dry_lagoon_region, LocationName.animal_sheep)
+ connect(multiworld, player, names, LocationName.dry_lagoon_region, LocationName.animal_unicorn)
+
+ connect(multiworld, player, names, LocationName.sand_ocean_region, LocationName.animal_peacock)
+ connect(multiworld, player, names, LocationName.sand_ocean_region, LocationName.animal_parrot)
+ connect(multiworld, player, names, LocationName.sand_ocean_region, LocationName.animal_raccoon)
+ connect(multiworld, player, names, LocationName.sand_ocean_region, LocationName.animal_bat)
+
+ connect(multiworld, player, names, LocationName.radical_highway_region, LocationName.animal_seal)
+ connect(multiworld, player, names, LocationName.radical_highway_region, LocationName.animal_cheetah)
+ connect(multiworld, player, names, LocationName.radical_highway_region, LocationName.animal_warthog)
+ connect(multiworld, player, names, LocationName.radical_highway_region, LocationName.animal_raccoon)
+
+ connect(multiworld, player, names, LocationName.egg_quarters_region, LocationName.animal_bear)
+ connect(multiworld, player, names, LocationName.egg_quarters_region, LocationName.animal_gorilla)
+ connect(multiworld, player, names, LocationName.egg_quarters_region, LocationName.animal_parrot)
+ connect(multiworld, player, names, LocationName.egg_quarters_region, LocationName.animal_skunk)
+ connect(multiworld, player, names, LocationName.egg_quarters_region, LocationName.animal_halffish)
+
+ connect(multiworld, player, names, LocationName.lost_colony_region, LocationName.animal_rabbit)
+ connect(multiworld, player, names, LocationName.lost_colony_region, LocationName.animal_warthog)
+ connect(multiworld, player, names, LocationName.lost_colony_region, LocationName.animal_bat)
+
+ connect(multiworld, player, names, LocationName.weapons_bed_region, LocationName.animal_seal)
+ connect(multiworld, player, names, LocationName.weapons_bed_region, LocationName.animal_otter)
+ connect(multiworld, player, names, LocationName.weapons_bed_region, LocationName.animal_cheetah)
+ connect(multiworld, player, names, LocationName.weapons_bed_region, LocationName.animal_sheep)
+
+ connect(multiworld, player, names, LocationName.security_hall_region, LocationName.animal_tiger)
+ connect(multiworld, player, names, LocationName.security_hall_region, LocationName.animal_parrot)
+ connect(multiworld, player, names, LocationName.security_hall_region, LocationName.animal_condor)
+ connect(multiworld, player, names, LocationName.security_hall_region, LocationName.animal_raccoon)
+
+ connect(multiworld, player, names, LocationName.white_jungle_region, LocationName.animal_bear)
+ connect(multiworld, player, names, LocationName.white_jungle_region, LocationName.animal_peacock)
+ connect(multiworld, player, names, LocationName.white_jungle_region, LocationName.animal_parrot)
+ connect(multiworld, player, names, LocationName.white_jungle_region, LocationName.animal_skunk)
+
+ connect(multiworld, player, names, LocationName.sky_rail_region, LocationName.animal_bear)
+ connect(multiworld, player, names, LocationName.sky_rail_region, LocationName.animal_tiger)
+ connect(multiworld, player, names, LocationName.sky_rail_region, LocationName.animal_condor)
+ connect(multiworld, player, names, LocationName.sky_rail_region, LocationName.animal_sheep)
+
+ connect(multiworld, player, names, LocationName.mad_space_region, LocationName.animal_peacock)
+ connect(multiworld, player, names, LocationName.mad_space_region, LocationName.animal_parrot)
+
+ connect(multiworld, player, names, LocationName.cosmic_wall_region, LocationName.animal_otter,
+ lambda state: (state.has(ItemName.eggman_jet_engine, player)))
+ connect(multiworld, player, names, LocationName.cosmic_wall_region, LocationName.animal_rabbit)
+ connect(multiworld, player, names, LocationName.cosmic_wall_region, LocationName.animal_cheetah,
+ lambda state: (state.has(ItemName.eggman_jet_engine, player)))
+ connect(multiworld, player, names, LocationName.cosmic_wall_region, LocationName.animal_sheep,
+ lambda state: (state.has(ItemName.eggman_jet_engine, player)))
+ connect(multiworld, player, names, LocationName.cosmic_wall_region, LocationName.animal_dragon,
+ lambda state: (state.has(ItemName.eggman_jet_engine, player)))
+
+ connect(multiworld, player, names, LocationName.final_chase_region, LocationName.animal_penguin)
+ connect(multiworld, player, names, LocationName.final_chase_region, LocationName.animal_otter)
+ connect(multiworld, player, names, LocationName.final_chase_region, LocationName.animal_tiger)
+ connect(multiworld, player, names, LocationName.final_chase_region, LocationName.animal_skunk)
+ connect(multiworld, player, names, LocationName.final_chase_region, LocationName.animal_phoenix)
+
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_seal)
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_bear,
+ lambda state: (state.has(ItemName.tails_booster, player)))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_gorilla)
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_skunk)
+
+ if world.options.goal in [1, 2]:
+ connect(multiworld, player, names, LocationName.green_hill_region, LocationName.animal_penguin)
+ connect(multiworld, player, names, LocationName.green_hill_region, LocationName.animal_otter)
+ connect(multiworld, player, names, LocationName.green_hill_region, LocationName.animal_gorilla)
+ connect(multiworld, player, names, LocationName.green_hill_region, LocationName.animal_raccoon)
+ connect(multiworld, player, names, LocationName.green_hill_region, LocationName.animal_unicorn)
+
+ if world.options.logic_difficulty.value == 0:
+ connect(multiworld, player, names, LocationName.metal_harbor_region, LocationName.animal_phoenix,
+ lambda state: (state.has(ItemName.sonic_light_shoes, player)))
+
+ connect(multiworld, player, names, LocationName.crazy_gadget_region, LocationName.animal_skunk,
+ lambda state: (state.has(ItemName.sonic_bounce_bracelet, player)))
+ connect(multiworld, player, names, LocationName.crazy_gadget_region, LocationName.animal_phoenix,
+ lambda state: (state.has(ItemName.sonic_light_shoes, player) and
+ state.has(ItemName.sonic_bounce_bracelet, player) and
+ state.has(ItemName.sonic_flame_ring, player)))
+
+ connect(multiworld, player, names, LocationName.weapons_bed_region, LocationName.animal_phoenix,
+ lambda state: (state.has(ItemName.eggman_jet_engine, player) and
+ state.has(ItemName.eggman_large_cannon, player)))
+
+ connect(multiworld, player, names, LocationName.mad_space_region, LocationName.animal_gorilla,
+ lambda state: (state.has(ItemName.rouge_iron_boots, player)))
+ connect(multiworld, player, names, LocationName.mad_space_region, LocationName.animal_raccoon,
+ lambda state: (state.has(ItemName.rouge_iron_boots, player)))
+ connect(multiworld, player, names, LocationName.mad_space_region, LocationName.animal_halffish,
+ lambda state: (state.has(ItemName.rouge_iron_boots, player)))
+
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_otter,
+ lambda state: (state.has(ItemName.tails_booster, player) and
+ state.has(ItemName.eggman_jet_engine, player)))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_rabbit,
+ lambda state: (state.has(ItemName.tails_booster, player) and
+ state.has(ItemName.eggman_jet_engine, player) and
+ state.has(ItemName.knuckles_air_necklace, player) and
+ state.has(ItemName.knuckles_hammer_gloves, player)))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_cheetah,
+ lambda state: (state.has(ItemName.tails_booster, player) and
+ state.has(ItemName.eggman_jet_engine, player)))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_warthog,
+ lambda state: (state.has(ItemName.tails_booster, player) and
+ state.has(ItemName.eggman_jet_engine, player)))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_parrot,
+ lambda state: (state.has(ItemName.tails_booster, player) and
+ state.has(ItemName.eggman_jet_engine, player) and
+ state.has(ItemName.knuckles_air_necklace, player) and
+ state.has(ItemName.knuckles_hammer_gloves, player) and
+ (state.has(ItemName.sonic_bounce_bracelet, player) or
+ state.has(ItemName.sonic_flame_ring, player))))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_condor,
+ lambda state: (state.has(ItemName.tails_booster, player) and
+ state.has(ItemName.eggman_jet_engine, player)))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_raccoon,
+ lambda state: (state.has(ItemName.tails_booster, player) and
+ (state.has(ItemName.eggman_jet_engine, player) or
+ state.has(ItemName.eggman_large_cannon, player))))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_phoenix,
+ lambda state: (state.has(ItemName.tails_booster, player) and
+ state.has(ItemName.eggman_jet_engine, player)))
+
+ elif world.options.logic_difficulty.value == 1:
+ connect(multiworld, player, names, LocationName.metal_harbor_region, LocationName.animal_phoenix)
+
+ connect(multiworld, player, names, LocationName.crazy_gadget_region, LocationName.animal_skunk)
+ connect(multiworld, player, names, LocationName.crazy_gadget_region, LocationName.animal_phoenix,
+ lambda state: (state.has(ItemName.sonic_light_shoes, player) and
+ state.has(ItemName.sonic_flame_ring, player)))
+
+ connect(multiworld, player, names, LocationName.weapons_bed_region, LocationName.animal_phoenix,
+ lambda state: (state.has(ItemName.eggman_jet_engine, player)))
+
+ connect(multiworld, player, names, LocationName.mad_space_region, LocationName.animal_gorilla)
+ connect(multiworld, player, names, LocationName.mad_space_region, LocationName.animal_raccoon)
+ connect(multiworld, player, names, LocationName.mad_space_region, LocationName.animal_halffish)
+
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_otter,
+ lambda state: (state.has(ItemName.tails_booster, player)))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_rabbit,
+ lambda state: (state.has(ItemName.tails_booster, player) and
+ state.has(ItemName.knuckles_hammer_gloves, player)))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_cheetah,
+ lambda state: (state.has(ItemName.tails_booster, player)))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_warthog,
+ lambda state: (state.has(ItemName.tails_booster, player)))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_parrot,
+ lambda state: (state.has(ItemName.tails_booster, player) and
+ state.has(ItemName.knuckles_hammer_gloves, player)))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_condor,
+ lambda state: (state.has(ItemName.tails_booster, player)))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_raccoon,
+ lambda state: (state.has(ItemName.tails_booster, player)))
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_phoenix,
+ lambda state: (state.has(ItemName.tails_booster, player)))
+
+ if world.options.keysanity:
+ connect(multiworld, player, names, LocationName.wild_canyon_region, LocationName.animal_dragon,
+ lambda state: (state.has(ItemName.knuckles_shovel_claws, player)))
+
+ connect(multiworld, player, names, LocationName.mission_street_region, LocationName.animal_phoenix,
+ lambda state: (state.has(ItemName.tails_booster, player) and
+ state.has(ItemName.tails_bazooka, player)))
+
+ connect(multiworld, player, names, LocationName.pyramid_cave_region, LocationName.animal_skeleton_dog,
+ lambda state: (state.has(ItemName.sonic_light_shoes, player) and
+ state.has(ItemName.sonic_flame_ring, player)))
+
+ connect(multiworld, player, names, LocationName.lost_colony_region, LocationName.animal_raccoon,
+ lambda state: (state.has(ItemName.eggman_jet_engine, player)))
+
+ if world.options.logic_difficulty.value == 0:
+ connect(multiworld, player, names, LocationName.iron_gate_region, LocationName.animal_dragon,
+ lambda state: (state.has(ItemName.eggman_jet_engine, player) and
+ state.has(ItemName.eggman_large_cannon, player)))
+
+ connect(multiworld, player, names, LocationName.sand_ocean_region, LocationName.animal_skeleton_dog,
+ lambda state: (state.has(ItemName.eggman_jet_engine, player) and
+ state.has(ItemName.eggman_large_cannon, player)))
+ if world.options.logic_difficulty.value == 1:
+ connect(multiworld, player, names, LocationName.iron_gate_region, LocationName.animal_dragon,
+ lambda state: (state.has(ItemName.eggman_jet_engine, player)))
+
+ connect(multiworld, player, names, LocationName.sand_ocean_region, LocationName.animal_skeleton_dog,
+ lambda state: (state.has(ItemName.eggman_jet_engine, player)))
+
+ else:
+ connect(multiworld, player, names, LocationName.city_escape_region, LocationName.animal_unicorn)
+
+ connect(multiworld, player, names, LocationName.wild_canyon_region, LocationName.animal_dragon)
+
+ connect(multiworld, player, names, LocationName.pumpkin_hill_region, LocationName.animal_halffish)
+
+ connect(multiworld, player, names, LocationName.mission_street_region, LocationName.animal_phoenix,
+ lambda state: (state.has(ItemName.tails_booster, player)))
+
+ connect(multiworld, player, names, LocationName.death_chamber_region, LocationName.animal_skeleton_dog,
+ lambda state: (state.has(ItemName.knuckles_shovel_claws, player) and
+ state.has(ItemName.knuckles_hammer_gloves, player)))
+
+ connect(multiworld, player, names, LocationName.eternal_engine_region, LocationName.animal_halffish,
+ lambda state: (state.has(ItemName.tails_booster, player) and
+ state.has(ItemName.tails_bazooka, player)))
+
+ connect(multiworld, player, names, LocationName.iron_gate_region, LocationName.animal_dragon)
+
+ connect(multiworld, player, names, LocationName.sand_ocean_region, LocationName.animal_skeleton_dog)
+
+ connect(multiworld, player, names, LocationName.radical_highway_region, LocationName.animal_unicorn)
+
+ connect(multiworld, player, names, LocationName.lost_colony_region, LocationName.animal_raccoon)
+ connect(multiworld, player, names, LocationName.lost_colony_region, LocationName.animal_skeleton_dog)
+
+ connect(multiworld, player, names, LocationName.security_hall_region, LocationName.animal_phoenix,
+ lambda state: (state.has(ItemName.rouge_pick_nails, player)))
+
+ connect(multiworld, player, names, LocationName.sky_rail_region, LocationName.animal_phoenix)
+
+ if world.options.logic_difficulty.value == 0:
+ connect(multiworld, player, names, LocationName.pyramid_cave_region, LocationName.animal_skeleton_dog,
+ lambda state: (state.has(ItemName.sonic_light_shoes, player) and
+ state.has(ItemName.sonic_bounce_bracelet, player) and
+ state.has(ItemName.sonic_mystic_melody, player)))
+
+ connect(multiworld, player, names, LocationName.white_jungle_region, LocationName.animal_dragon,
+ lambda state: (state.has(ItemName.shadow_air_shoes, player)))
+
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_dragon,
+ lambda state: (state.has(ItemName.tails_booster, player) and
+ state.has(ItemName.eggman_jet_engine, player) and
+ state.has(ItemName.knuckles_air_necklace, player) and
+ state.has(ItemName.knuckles_hammer_gloves, player)))
+ elif world.options.logic_difficulty.value == 1:
+ connect(multiworld, player, names, LocationName.pyramid_cave_region, LocationName.animal_skeleton_dog)
+
+ connect(multiworld, player, names, LocationName.white_jungle_region, LocationName.animal_dragon)
+
+ connect(multiworld, player, names, LocationName.cannon_core_region, LocationName.animal_dragon,
+ lambda state: (state.has(ItemName.tails_booster, player) and
+ state.has(ItemName.knuckles_hammer_gloves, player)))
- connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_beginner_region)
- connect(world, player, names, LocationName.gate_2_region, LocationName.kart_race_standard_region)
- connect(world, player, names, LocationName.gate_4_region, LocationName.kart_race_expert_region)
+ if world.options.black_market_slots.value > 0:
+ connect(multiworld, player, names, LocationName.gate_0_region, LocationName.black_market_region)
-def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None):
- ret = Region(name, player, world)
+def create_region(multiworld: MultiWorld, player: int, active_locations, name: str, locations=None):
+ ret = Region(name, player, multiworld)
if locations:
for location in locations:
loc_id = active_locations.get(location, 0)
@@ -1708,10 +2437,10 @@ def create_region(world: MultiWorld, player: int, active_locations, name: str, l
return ret
-def connect(world: MultiWorld, player: int, used_names: typing.Dict[str, int], source: str, target: str,
+def connect(multiworld: MultiWorld, player: int, used_names: typing.Dict[str, int], source: str, target: str,
rule: typing.Optional[typing.Callable] = None):
- source_region = world.get_region(source, player)
- target_region = world.get_region(target, player)
+ source_region = multiworld.get_region(source, player)
+ target_region = multiworld.get_region(target, player)
if target not in used_names:
used_names[target] = 1
diff --git a/worlds/sa2b/Rules.py b/worlds/sa2b/Rules.py
index 146938db7656..afd0dcb18226 100644
--- a/worlds/sa2b/Rules.py
+++ b/worlds/sa2b/Rules.py
@@ -1,6 +1,7 @@
import typing
from BaseClasses import MultiWorld
+from worlds.AutoWorld import World
from .Names import LocationName, ItemName
from .Locations import boss_gate_set
from worlds.AutoWorld import LogicMixin
@@ -19,7 +20,7 @@ def add_rule_safe(multiworld: MultiWorld, spot_name: str, player: int, rule: Col
add_rule(location, rule)
-def set_mission_progress_rules(world: MultiWorld, player: int, mission_map: typing.Dict[int, int], mission_count_map: typing.Dict[int, int]):
+def set_mission_progress_rules(multiworld: MultiWorld, player: int, mission_map: typing.Dict[int, int], mission_count_map: typing.Dict[int, int]):
for i in range(31):
mission_count = mission_count_map[i]
mission_order: typing.List[int] = mission_orders[mission_map[i]]
@@ -33,58 +34,58 @@ def set_mission_progress_rules(world: MultiWorld, player: int, mission_map: typi
prev_mission_number = mission_order[j - 1]
location_name: str = stage_prefix + str(mission_number)
prev_location_name: str = stage_prefix + str(prev_mission_number)
- set_rule(world.get_location(location_name, player),
+ set_rule(multiworld.get_location(location_name, player),
lambda state, prev_location_name=prev_location_name: state.can_reach(prev_location_name, "Location", player))
-def set_mission_upgrade_rules_standard(world: MultiWorld, player: int):
+def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, player: int):
# Mission 1 Upgrade Requirements
- add_rule_safe(world, LocationName.metal_harbor_1, player,
+ add_rule_safe(multiworld, LocationName.metal_harbor_1, player,
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule_safe(world, LocationName.pumpkin_hill_1, player,
+ add_rule_safe(multiworld, LocationName.pumpkin_hill_1, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player))
- add_rule_safe(world, LocationName.mission_street_1, player,
+ add_rule_safe(multiworld, LocationName.mission_street_1, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.aquatic_mine_1, player,
+ add_rule_safe(multiworld, LocationName.aquatic_mine_1, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player))
- add_rule_safe(world, LocationName.hidden_base_1, player,
+ add_rule_safe(multiworld, LocationName.hidden_base_1, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.pyramid_cave_1, player,
+ add_rule_safe(multiworld, LocationName.pyramid_cave_1, player,
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule_safe(world, LocationName.death_chamber_1, player,
+ add_rule_safe(multiworld, LocationName.death_chamber_1, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule_safe(world, LocationName.eternal_engine_1, player,
+ add_rule_safe(multiworld, LocationName.eternal_engine_1, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule_safe(world, LocationName.meteor_herd_1, player,
+ add_rule_safe(multiworld, LocationName.meteor_herd_1, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule_safe(world, LocationName.crazy_gadget_1, player,
+ add_rule_safe(multiworld, LocationName.crazy_gadget_1, player,
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule_safe(world, LocationName.final_rush_1, player,
+ add_rule_safe(multiworld, LocationName.final_rush_1, player,
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule_safe(world, LocationName.egg_quarters_1, player,
+ add_rule_safe(multiworld, LocationName.egg_quarters_1, player,
lambda state: state.has(ItemName.rouge_pick_nails, player))
- add_rule_safe(world, LocationName.lost_colony_1, player,
+ add_rule_safe(multiworld, LocationName.lost_colony_1, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.weapons_bed_1, player,
+ add_rule_safe(multiworld, LocationName.weapons_bed_1, player,
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule_safe(world, LocationName.security_hall_1, player,
+ add_rule_safe(multiworld, LocationName.security_hall_1, player,
lambda state: state.has(ItemName.rouge_pick_nails, player))
- add_rule_safe(world, LocationName.white_jungle_1, player,
+ add_rule_safe(multiworld, LocationName.white_jungle_1, player,
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule_safe(world, LocationName.mad_space_1, player,
+ add_rule_safe(multiworld, LocationName.mad_space_1, player,
lambda state: state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule_safe(world, LocationName.cosmic_wall_1, player,
+ add_rule_safe(multiworld, LocationName.cosmic_wall_1, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.cannon_core_1, player,
+ add_rule_safe(multiworld, LocationName.cannon_core_1, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.knuckles_hammer_gloves, player) and
@@ -92,129 +93,128 @@ def set_mission_upgrade_rules_standard(world: MultiWorld, player: int):
state.has(ItemName.sonic_bounce_bracelet, player))
# Mission 2 Upgrade Requirements
- add_rule_safe(world, LocationName.metal_harbor_2, player,
+ add_rule_safe(multiworld, LocationName.metal_harbor_2, player,
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule_safe(world, LocationName.mission_street_2, player,
+ add_rule_safe(multiworld, LocationName.mission_street_2, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.hidden_base_2, player,
+ add_rule_safe(multiworld, LocationName.hidden_base_2, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.death_chamber_2, player,
+ add_rule_safe(multiworld, LocationName.death_chamber_2, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule_safe(world, LocationName.eternal_engine_2, player,
- lambda state: state.has(ItemName.tails_booster, player) and
- state.has(ItemName.tails_bazooka, player))
- add_rule_safe(world, LocationName.crazy_gadget_2, player,
+ add_rule_safe(multiworld, LocationName.eternal_engine_2, player,
+ lambda state: state.has(ItemName.tails_booster, player))
+ add_rule_safe(multiworld, LocationName.crazy_gadget_2, player,
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule_safe(world, LocationName.lost_colony_2, player,
+ add_rule_safe(multiworld, LocationName.lost_colony_2, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.weapons_bed_2, player,
+ add_rule_safe(multiworld, LocationName.weapons_bed_2, player,
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule_safe(world, LocationName.security_hall_2, player,
+ add_rule_safe(multiworld, LocationName.security_hall_2, player,
lambda state: state.has(ItemName.rouge_pick_nails, player))
- add_rule_safe(world, LocationName.mad_space_2, player,
+ add_rule_safe(multiworld, LocationName.mad_space_2, player,
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule_safe(world, LocationName.cosmic_wall_2, player,
+ add_rule_safe(multiworld, LocationName.cosmic_wall_2, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.cannon_core_2, player,
+ add_rule_safe(multiworld, LocationName.cannon_core_2, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player))
# Mission 3 Upgrade Requirements
- add_rule_safe(world, LocationName.city_escape_3, player,
+ add_rule_safe(multiworld, LocationName.city_escape_3, player,
lambda state: state.has(ItemName.sonic_mystic_melody, player))
- add_rule_safe(world, LocationName.wild_canyon_3, player,
+ add_rule_safe(multiworld, LocationName.wild_canyon_3, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player) and
state.has(ItemName.knuckles_mystic_melody, player))
- add_rule_safe(world, LocationName.prison_lane_3, player,
+ add_rule_safe(multiworld, LocationName.prison_lane_3, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_mystic_melody, player))
- add_rule_safe(world, LocationName.metal_harbor_3, player,
+ add_rule_safe(multiworld, LocationName.metal_harbor_3, player,
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_mystic_melody, player))
- add_rule_safe(world, LocationName.green_forest_3, player,
+ add_rule_safe(multiworld, LocationName.green_forest_3, player,
lambda state: state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_mystic_melody, player))
- add_rule_safe(world, LocationName.pumpkin_hill_3, player,
+ add_rule_safe(multiworld, LocationName.pumpkin_hill_3, player,
lambda state: state.has(ItemName.knuckles_mystic_melody, player))
- add_rule_safe(world, LocationName.mission_street_3, player,
+ add_rule_safe(multiworld, LocationName.mission_street_3, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_mystic_melody, player))
- add_rule_safe(world, LocationName.aquatic_mine_3, player,
+ add_rule_safe(multiworld, LocationName.aquatic_mine_3, player,
lambda state: state.has(ItemName.knuckles_mystic_melody, player))
- add_rule_safe(world, LocationName.hidden_base_3, player,
+ add_rule_safe(multiworld, LocationName.hidden_base_3, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_mystic_melody, player))
- add_rule_safe(world, LocationName.pyramid_cave_3, player,
+ add_rule_safe(multiworld, LocationName.pyramid_cave_3, player,
lambda state: state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_mystic_melody, player))
- add_rule_safe(world, LocationName.death_chamber_3, player,
+ add_rule_safe(multiworld, LocationName.death_chamber_3, player,
lambda state: state.has(ItemName.knuckles_mystic_melody, player) and
state.has(ItemName.knuckles_air_necklace, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule_safe(world, LocationName.eternal_engine_3, player,
+ add_rule_safe(multiworld, LocationName.eternal_engine_3, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_mystic_melody, player))
- add_rule_safe(world, LocationName.meteor_herd_3, player,
+ add_rule_safe(multiworld, LocationName.meteor_herd_3, player,
lambda state: state.has(ItemName.knuckles_mystic_melody, player))
- add_rule_safe(world, LocationName.crazy_gadget_3, player,
+ add_rule_safe(multiworld, LocationName.crazy_gadget_3, player,
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player) and
state.has(ItemName.sonic_mystic_melody, player))
- add_rule_safe(world, LocationName.final_rush_3, player,
+ add_rule_safe(multiworld, LocationName.final_rush_3, player,
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_mystic_melody, player))
- add_rule_safe(world, LocationName.iron_gate_3, player,
+ add_rule_safe(multiworld, LocationName.iron_gate_3, player,
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule_safe(world, LocationName.dry_lagoon_3, player,
+ add_rule_safe(multiworld, LocationName.dry_lagoon_3, player,
lambda state: state.has(ItemName.rouge_mystic_melody, player) and
state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule_safe(world, LocationName.sand_ocean_3, player,
+ add_rule_safe(multiworld, LocationName.sand_ocean_3, player,
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule_safe(world, LocationName.radical_highway_3, player,
+ add_rule_safe(multiworld, LocationName.radical_highway_3, player,
lambda state: state.has(ItemName.shadow_mystic_melody, player))
- add_rule_safe(world, LocationName.egg_quarters_3, player,
+ add_rule_safe(multiworld, LocationName.egg_quarters_3, player,
lambda state: state.has(ItemName.rouge_mystic_melody, player) and
state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule_safe(world, LocationName.lost_colony_3, player,
+ add_rule_safe(multiworld, LocationName.lost_colony_3, player,
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.weapons_bed_3, player,
+ add_rule_safe(multiworld, LocationName.weapons_bed_3, player,
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule_safe(world, LocationName.security_hall_3, player,
+ add_rule_safe(multiworld, LocationName.security_hall_3, player,
lambda state: state.has(ItemName.rouge_treasure_scope, player))
- add_rule_safe(world, LocationName.white_jungle_3, player,
+ add_rule_safe(multiworld, LocationName.white_jungle_3, player,
lambda state: state.has(ItemName.shadow_air_shoes, player) and
state.has(ItemName.shadow_mystic_melody, player))
- add_rule_safe(world, LocationName.sky_rail_3, player,
+ add_rule_safe(multiworld, LocationName.sky_rail_3, player,
lambda state: state.has(ItemName.shadow_air_shoes, player) and
state.has(ItemName.shadow_mystic_melody, player))
- add_rule_safe(world, LocationName.mad_space_3, player,
+ add_rule_safe(multiworld, LocationName.mad_space_3, player,
lambda state: state.has(ItemName.rouge_mystic_melody, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule_safe(world, LocationName.cosmic_wall_3, player,
+ add_rule_safe(multiworld, LocationName.cosmic_wall_3, player,
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.final_chase_3, player,
+ add_rule_safe(multiworld, LocationName.final_chase_3, player,
lambda state: state.has(ItemName.shadow_air_shoes, player) and
state.has(ItemName.shadow_mystic_melody, player))
- add_rule_safe(world, LocationName.cannon_core_3, player,
+ add_rule_safe(multiworld, LocationName.cannon_core_3, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player) and
@@ -227,52 +227,52 @@ def set_mission_upgrade_rules_standard(world: MultiWorld, player: int):
state.has(ItemName.sonic_light_shoes, player))
# Mission 4 Upgrade Requirements
- add_rule_safe(world, LocationName.metal_harbor_4, player,
+ add_rule_safe(multiworld, LocationName.metal_harbor_4, player,
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule_safe(world, LocationName.pumpkin_hill_4, player,
+ add_rule_safe(multiworld, LocationName.pumpkin_hill_4, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player))
- add_rule_safe(world, LocationName.mission_street_4, player,
+ add_rule_safe(multiworld, LocationName.mission_street_4, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.aquatic_mine_4, player,
+ add_rule_safe(multiworld, LocationName.aquatic_mine_4, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player))
- add_rule_safe(world, LocationName.hidden_base_4, player,
+ add_rule_safe(multiworld, LocationName.hidden_base_4, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.pyramid_cave_4, player,
+ add_rule_safe(multiworld, LocationName.pyramid_cave_4, player,
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule_safe(world, LocationName.death_chamber_4, player,
+ add_rule_safe(multiworld, LocationName.death_chamber_4, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule_safe(world, LocationName.eternal_engine_4, player,
+ add_rule_safe(multiworld, LocationName.eternal_engine_4, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule_safe(world, LocationName.meteor_herd_4, player,
+ add_rule_safe(multiworld, LocationName.meteor_herd_4, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule_safe(world, LocationName.crazy_gadget_4, player,
+ add_rule_safe(multiworld, LocationName.crazy_gadget_4, player,
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule_safe(world, LocationName.final_rush_4, player,
+ add_rule_safe(multiworld, LocationName.final_rush_4, player,
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule_safe(world, LocationName.egg_quarters_4, player,
+ add_rule_safe(multiworld, LocationName.egg_quarters_4, player,
lambda state: state.has(ItemName.rouge_pick_nails, player))
- add_rule_safe(world, LocationName.lost_colony_4, player,
+ add_rule_safe(multiworld, LocationName.lost_colony_4, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.weapons_bed_4, player,
+ add_rule_safe(multiworld, LocationName.weapons_bed_4, player,
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule_safe(world, LocationName.security_hall_4, player,
+ add_rule_safe(multiworld, LocationName.security_hall_4, player,
lambda state: state.has(ItemName.rouge_pick_nails, player))
- add_rule_safe(world, LocationName.white_jungle_4, player,
+ add_rule_safe(multiworld, LocationName.white_jungle_4, player,
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule_safe(world, LocationName.mad_space_4, player,
+ add_rule_safe(multiworld, LocationName.mad_space_4, player,
lambda state: state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule_safe(world, LocationName.cosmic_wall_4, player,
+ add_rule_safe(multiworld, LocationName.cosmic_wall_4, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.cannon_core_4, player,
+ add_rule_safe(multiworld, LocationName.cannon_core_4, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.knuckles_hammer_gloves, player) and
@@ -280,76 +280,76 @@ def set_mission_upgrade_rules_standard(world: MultiWorld, player: int):
state.has(ItemName.sonic_bounce_bracelet, player))
# Mission 5 Upgrade Requirements
- add_rule_safe(world, LocationName.city_escape_5, player,
+ add_rule_safe(multiworld, LocationName.city_escape_5, player,
lambda state: state.has(ItemName.sonic_flame_ring, player) and
state.has(ItemName.sonic_light_shoes, player))
- add_rule_safe(world, LocationName.wild_canyon_5, player,
+ add_rule_safe(multiworld, LocationName.wild_canyon_5, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_sunglasses, player))
- add_rule_safe(world, LocationName.metal_harbor_5, player,
+ add_rule_safe(multiworld, LocationName.metal_harbor_5, player,
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule_safe(world, LocationName.green_forest_5, player,
+ add_rule_safe(multiworld, LocationName.green_forest_5, player,
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule_safe(world, LocationName.pumpkin_hill_5, player,
+ add_rule_safe(multiworld, LocationName.pumpkin_hill_5, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_sunglasses, player))
- add_rule_safe(world, LocationName.mission_street_5, player,
+ add_rule_safe(multiworld, LocationName.mission_street_5, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule_safe(world, LocationName.aquatic_mine_5, player,
+ add_rule_safe(multiworld, LocationName.aquatic_mine_5, player,
lambda state: state.has(ItemName.knuckles_mystic_melody, player) and
state.has(ItemName.knuckles_air_necklace, player) and
state.has(ItemName.knuckles_sunglasses, player))
- add_rule_safe(world, LocationName.hidden_base_5, player,
+ add_rule_safe(multiworld, LocationName.hidden_base_5, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.pyramid_cave_5, player,
+ add_rule_safe(multiworld, LocationName.pyramid_cave_5, player,
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule_safe(world, LocationName.death_chamber_5, player,
+ add_rule_safe(multiworld, LocationName.death_chamber_5, player,
lambda state: state.has(ItemName.knuckles_hammer_gloves, player) and
state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_mystic_melody, player) and
state.has(ItemName.knuckles_air_necklace, player))
- add_rule_safe(world, LocationName.eternal_engine_5, player,
+ add_rule_safe(multiworld, LocationName.eternal_engine_5, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule_safe(world, LocationName.meteor_herd_5, player,
+ add_rule_safe(multiworld, LocationName.meteor_herd_5, player,
lambda state: state.has(ItemName.knuckles_sunglasses, player))
- add_rule_safe(world, LocationName.crazy_gadget_5, player,
+ add_rule_safe(multiworld, LocationName.crazy_gadget_5, player,
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule_safe(world, LocationName.final_rush_5, player,
+ add_rule_safe(multiworld, LocationName.final_rush_5, player,
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule_safe(world, LocationName.iron_gate_5, player,
+ add_rule_safe(multiworld, LocationName.iron_gate_5, player,
lambda state: state.has(ItemName.eggman_large_cannon, player))
- add_rule_safe(world, LocationName.dry_lagoon_5, player,
+ add_rule_safe(multiworld, LocationName.dry_lagoon_5, player,
lambda state: state.has(ItemName.rouge_treasure_scope, player))
- add_rule_safe(world, LocationName.sand_ocean_5, player,
+ add_rule_safe(multiworld, LocationName.sand_ocean_5, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.egg_quarters_5, player,
+ add_rule_safe(multiworld, LocationName.egg_quarters_5, player,
lambda state: state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_treasure_scope, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule_safe(world, LocationName.lost_colony_5, player,
+ add_rule_safe(multiworld, LocationName.lost_colony_5, player,
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule_safe(world, LocationName.weapons_bed_5, player,
+ add_rule_safe(multiworld, LocationName.weapons_bed_5, player,
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule_safe(world, LocationName.security_hall_5, player,
+ add_rule_safe(multiworld, LocationName.security_hall_5, player,
lambda state: state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_treasure_scope, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule_safe(world, LocationName.white_jungle_5, player,
+ add_rule_safe(multiworld, LocationName.white_jungle_5, player,
lambda state: state.has(ItemName.shadow_air_shoes, player) and
state.has(ItemName.shadow_flame_ring, player))
- add_rule_safe(world, LocationName.mad_space_5, player,
+ add_rule_safe(multiworld, LocationName.mad_space_5, player,
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule_safe(world, LocationName.cosmic_wall_5, player,
+ add_rule_safe(multiworld, LocationName.cosmic_wall_5, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.cannon_core_5, player,
+ add_rule_safe(multiworld, LocationName.cannon_core_5, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.knuckles_mystic_melody, player) and
@@ -358,132 +358,132 @@ def set_mission_upgrade_rules_standard(world: MultiWorld, player: int):
state.has(ItemName.sonic_bounce_bracelet, player))
# Upgrade Spot Upgrade Requirements
- add_rule(world.get_location(LocationName.city_escape_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.city_escape_upgrade, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.wild_canyon_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.wild_canyon_upgrade, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player))
- add_rule(world.get_location(LocationName.prison_lane_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.prison_lane_upgrade, player),
lambda state: state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.hidden_base_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_upgrade, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.eternal_engine_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_upgrade, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.meteor_herd_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.meteor_herd_upgrade, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.crazy_gadget_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_upgrade, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.final_rush_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_upgrade, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.iron_gate_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.iron_gate_upgrade, player),
lambda state: state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.dry_lagoon_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.dry_lagoon_upgrade, player),
lambda state: state.has(ItemName.rouge_pick_nails, player))
- add_rule(world.get_location(LocationName.sand_ocean_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.sand_ocean_upgrade, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.radical_highway_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.radical_highway_upgrade, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.security_hall_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.security_hall_upgrade, player),
lambda state: state.has(ItemName.rouge_mystic_melody, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_upgrade, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
# Chao Key Upgrade Requirements
- if world.keysanity[player]:
- add_rule(world.get_location(LocationName.prison_lane_chao_1, player),
+ if world.options.keysanity:
+ add_rule(multiworld.get_location(LocationName.prison_lane_chao_1, player),
lambda state: state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.mission_street_chao_1, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_chao_1, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_chao_1, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_chao_1, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_chao_1, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_chao_1, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_chao_1, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_chao_1, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_chao_1, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_chao_1, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.cosmic_wall_chao_1, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_1, player),
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_chao_1, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_chao_1, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.prison_lane_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.prison_lane_chao_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.metal_harbor_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.metal_harbor_chao_2, player),
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule(world.get_location(LocationName.mission_street_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_chao_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_chao_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_chao_2, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.death_chamber_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_chao_2, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_chao_2, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_chao_2, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.weapons_bed_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_chao_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.white_jungle_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.white_jungle_chao_2, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.mad_space_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_chao_2, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_chao_2, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.metal_harbor_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.metal_harbor_chao_3, player),
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule(world.get_location(LocationName.mission_street_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_chao_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_chao_3, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_mystic_melody, player))
- add_rule(world.get_location(LocationName.death_chamber_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_chao_3, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_chao_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_chao_3, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.final_rush_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_chao_3, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.egg_quarters_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.egg_quarters_chao_3, player),
lambda state: state.has(ItemName.rouge_mystic_melody, player))
- add_rule(world.get_location(LocationName.lost_colony_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_chao_3, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.weapons_bed_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_chao_3, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.security_hall_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.security_hall_chao_3, player),
lambda state: state.has(ItemName.rouge_pick_nails, player))
- add_rule(world.get_location(LocationName.white_jungle_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.white_jungle_chao_3, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.mad_space_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_chao_3, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_3, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_chao_3, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.knuckles_hammer_gloves, player) and
@@ -491,804 +491,807 @@ def set_mission_upgrade_rules_standard(world: MultiWorld, player: int):
state.has(ItemName.sonic_flame_ring, player))
# Pipe Upgrade Requirements
- if world.whistlesanity[player].value == 1 or world.whistlesanity[player].value == 3:
- add_rule(world.get_location(LocationName.mission_street_pipe_1, player),
+ if world.options.whistlesanity.value == 1 or world.options.whistlesanity.value == 3:
+ add_rule(multiworld.get_location(LocationName.mission_street_pipe_1, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_pipe_1, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_pipe_1, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.sand_ocean_pipe_1, player),
+ add_rule(multiworld.get_location(LocationName.sand_ocean_pipe_1, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cosmic_wall_pipe_1, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_pipe_1, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.mission_street_pipe_2, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_pipe_1, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_pipe_2, player),
+
+ add_rule(multiworld.get_location(LocationName.mission_street_pipe_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_pipe_2, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_pipe_2, player),
+ lambda state: state.has(ItemName.tails_booster, player))
+ add_rule(multiworld.get_location(LocationName.death_chamber_pipe_2, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_pipe_2, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_pipe_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_pipe_2, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_pipe_2, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.sand_ocean_pipe_2, player),
+ add_rule(multiworld.get_location(LocationName.sand_ocean_pipe_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.lost_colony_pipe_2, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_pipe_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cosmic_wall_pipe_2, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_pipe_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_pipe_2, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_pipe_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.prison_lane_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.prison_lane_pipe_3, player),
lambda state: state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.mission_street_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_pipe_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_pipe_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_pipe_3, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.death_chamber_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_pipe_3, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_pipe_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_pipe_3, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_mystic_melody, player))
- add_rule(world.get_location(LocationName.weapons_bed_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_pipe_3, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.white_jungle_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.white_jungle_pipe_3, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.mad_space_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_pipe_3, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_pipe_3, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_pipe_3, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.hidden_base_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_pipe_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_pipe_4, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.eternal_engine_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_pipe_4, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_pipe_4, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.weapons_bed_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_pipe_4, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.white_jungle_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.white_jungle_pipe_4, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.mad_space_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_pipe_4, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_pipe_4, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_pipe_4, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.hidden_base_pipe_5, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_pipe_5, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.eternal_engine_pipe_5, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_pipe_5, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.weapons_bed_pipe_5, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_pipe_5, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.cosmic_wall_pipe_5, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_pipe_5, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_pipe_5, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_pipe_5, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.knuckles_hammer_gloves, player) and
state.has(ItemName.knuckles_air_necklace, player))
# Hidden Whistle Upgrade Requirements
- if world.whistlesanity[player].value == 2 or world.whistlesanity[player].value == 3:
- add_rule(world.get_location(LocationName.mission_street_hidden_3, player),
+ if world.options.whistlesanity.value == 2 or world.options.whistlesanity.value == 3:
+ add_rule(multiworld.get_location(LocationName.mission_street_hidden_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.mission_street_hidden_4, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_hidden_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_hidden_1, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_hidden_1, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.death_chamber_hidden_2, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_hidden_2, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.crazy_gadget_hidden_1, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_hidden_1, player),
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule(world.get_location(LocationName.white_jungle_hidden_3, player),
+ add_rule(multiworld.get_location(LocationName.white_jungle_hidden_3, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.cannon_core_hidden_1, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_hidden_1, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player))
# Omochao Upgrade Requirements
- if world.omosanity[player]:
- add_rule(world.get_location(LocationName.eternal_engine_omo_1, player),
+ if world.options.omosanity:
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_1, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_omo_2, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_omo_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_omo_2, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_omo_2, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.death_chamber_omo_2, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_omo_2, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_2, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.radical_highway_omo_2, player),
+ add_rule(multiworld.get_location(LocationName.radical_highway_omo_2, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.weapons_bed_omo_2, player),
- lambda state: state.has(ItemName.eggman_jet_engine, player))
+ add_rule(multiworld.get_location(LocationName.weapons_bed_omo_2, player),
+ lambda state: state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.mission_street_omo_3, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_omo_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_omo_3, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_omo_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_omo_3, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_omo_3, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_3, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.final_rush_omo_3, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_omo_3, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.weapons_bed_omo_3, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_omo_3, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.metal_harbor_omo_4, player),
+ add_rule(multiworld.get_location(LocationName.metal_harbor_omo_4, player),
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule(world.get_location(LocationName.mission_street_omo_4, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_omo_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_omo_4, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_omo_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_omo_4, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_omo_4, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.death_chamber_omo_4, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_omo_4, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_4, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.mad_space_omo_4, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_omo_4, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cannon_core_omo_4, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_omo_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.metal_harbor_omo_5, player),
+ add_rule(multiworld.get_location(LocationName.metal_harbor_omo_5, player),
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule(world.get_location(LocationName.mission_street_omo_5, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_omo_5, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_omo_5, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_omo_5, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_5, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_5, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_omo_5, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_omo_5, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.white_jungle_omo_5, player),
+ add_rule(multiworld.get_location(LocationName.white_jungle_omo_5, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.mad_space_omo_5, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_omo_5, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cannon_core_omo_5, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_omo_5, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.mission_street_omo_6, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_omo_6, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_omo_6, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_omo_6, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_6, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_6, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_omo_6, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_omo_6, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.lost_colony_omo_6, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_omo_6, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_omo_6, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_omo_6, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.mission_street_omo_7, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_omo_7, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_omo_7, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_omo_7, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_7, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_7, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_omo_7, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_omo_7, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.lost_colony_omo_7, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_omo_7, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_omo_7, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_omo_7, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.knuckles_hammer_gloves, player) and
state.has(ItemName.knuckles_air_necklace, player))
- add_rule(world.get_location(LocationName.mission_street_omo_8, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_omo_8, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_omo_8, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_omo_8, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_8, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_8, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_omo_8, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_omo_8, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.lost_colony_omo_8, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_omo_8, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.security_hall_omo_8, player),
+ add_rule(multiworld.get_location(LocationName.security_hall_omo_8, player),
lambda state: state.has(ItemName.rouge_mystic_melody, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cannon_core_omo_8, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_omo_8, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.knuckles_hammer_gloves, player) and
state.has(ItemName.knuckles_air_necklace, player))
- add_rule(world.get_location(LocationName.death_chamber_omo_9, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_omo_9, player),
lambda state: state.has(ItemName.knuckles_mystic_melody, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_9, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_9, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_omo_9, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_omo_9, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.cannon_core_omo_9, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_omo_9, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.knuckles_hammer_gloves, player) and
state.has(ItemName.knuckles_air_necklace, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_10, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_10, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_omo_10, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_omo_10, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_11, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_11, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_omo_11, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_omo_11, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_12, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_12, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_omo_12, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_omo_12, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.crazy_gadget_omo_13, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_omo_13, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
# Gold Beetle Upgrade Requirements
- if world.beetlesanity[player]:
- add_rule(world.get_location(LocationName.mission_street_beetle, player),
+ if world.options.beetlesanity:
+ add_rule(multiworld.get_location(LocationName.mission_street_beetle, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_beetle, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_beetle, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_beetle, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_beetle, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.death_chamber_beetle, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_beetle, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_beetle, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_beetle, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_beetle, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_beetle, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.dry_lagoon_beetle, player),
+ add_rule(multiworld.get_location(LocationName.dry_lagoon_beetle, player),
lambda state: state.has(ItemName.rouge_mystic_melody, player) and
state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.lost_colony_beetle, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_beetle, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.white_jungle_beetle, player),
+ add_rule(multiworld.get_location(LocationName.white_jungle_beetle, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.mad_space_beetle, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_beetle, player),
lambda state: state.has(ItemName.rouge_mystic_melody, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_beetle, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_beetle, player),
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_beetle, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_beetle, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.knuckles_hammer_gloves, player) and
state.has(ItemName.knuckles_air_necklace, player))
# Animal Upgrade Requirements
- if world.animalsanity[player]:
- add_rule(world.get_location(LocationName.hidden_base_animal_2, player),
+ if world.options.animalsanity:
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_2, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_3, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_3, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_3, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_3, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_3, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_3, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_3, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_3, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_3, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_4, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_4, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_4, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_4, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_4, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_4, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_4, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_4, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.mad_space_animal_4, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_animal_4, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_4, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_4, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_4, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.mission_street_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_5, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_5, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_5, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_5, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_5, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_5, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.mad_space_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_animal_5, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_5, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_5, player),
lambda state: state.has(ItemName.tails_booster, player) and
(state.has(ItemName.eggman_jet_engine, player) or
state.has(ItemName.eggman_large_cannon, player)))
- add_rule(world.get_location(LocationName.metal_harbor_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.metal_harbor_animal_6, player),
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule(world.get_location(LocationName.mission_street_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_6, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_6, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_6, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_6, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_6, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_6, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_6, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.mad_space_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_animal_6, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_6, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_6, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.metal_harbor_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.metal_harbor_animal_7, player),
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule(world.get_location(LocationName.mission_street_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_7, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_7, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_7, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_7, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_7, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_7, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.lost_colony_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_animal_7, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) or
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_7, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.security_hall_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.security_hall_animal_7, player),
lambda state: state.has(ItemName.rouge_pick_nails, player) or
state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.mad_space_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_animal_7, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_7, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_7, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.metal_harbor_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.metal_harbor_animal_8, player),
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule(world.get_location(LocationName.mission_street_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_8, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_8, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_8, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_8, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_8, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_8, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.lost_colony_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_animal_8, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_8, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.security_hall_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.security_hall_animal_8, player),
lambda state: state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.mad_space_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_animal_8, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_8, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_8, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.metal_harbor_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.metal_harbor_animal_9, player),
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule(world.get_location(LocationName.mission_street_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_9, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_9, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_9, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_9, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_9, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_9, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.final_rush_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_9, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.lost_colony_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_animal_9, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_9, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.mad_space_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_animal_9, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_9, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_9, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.wild_canyon_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.wild_canyon_animal_10, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player))
- add_rule(world.get_location(LocationName.metal_harbor_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.metal_harbor_animal_10, player),
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule(world.get_location(LocationName.mission_street_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_10, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.aquatic_mine_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.aquatic_mine_animal_10, player),
lambda state: state.has(ItemName.knuckles_mystic_melody, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_10, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_10, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_10, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_10, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_10, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.final_rush_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_10, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.egg_quarters_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.egg_quarters_animal_10, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.lost_colony_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_animal_10, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_10, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.mad_space_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_animal_10, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_10, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_10, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.metal_harbor_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.metal_harbor_animal_11, player),
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule(world.get_location(LocationName.mission_street_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_11, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_11, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_11, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_11, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_11, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player) and
(state.has(ItemName.sonic_flame_ring, player) or
state.has(ItemName.sonic_mystic_melody, player)))
- add_rule(world.get_location(LocationName.final_rush_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_11, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.lost_colony_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_animal_11, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_11, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.white_jungle_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.white_jungle_animal_11, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_11, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_11, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.metal_harbor_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.metal_harbor_animal_12, player),
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule(world.get_location(LocationName.mission_street_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_12, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_12, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_12, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_12, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_12, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player) and
(state.has(ItemName.sonic_light_shoes, player) or
state.has(ItemName.sonic_mystic_melody, player)))
- add_rule(world.get_location(LocationName.final_rush_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_12, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.sand_ocean_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.sand_ocean_animal_12, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.lost_colony_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_animal_12, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_12, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.white_jungle_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.white_jungle_animal_12, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_12, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_12, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.prison_lane_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.prison_lane_animal_13, player),
lambda state: state.has(ItemName.tails_booster, player) or
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.metal_harbor_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.metal_harbor_animal_13, player),
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule(world.get_location(LocationName.mission_street_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_13, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_13, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_13, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_13, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_13, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.final_rush_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_13, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.sand_ocean_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.sand_ocean_animal_13, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.lost_colony_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_animal_13, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_13, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.white_jungle_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.white_jungle_animal_13, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_13, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_13, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player) and
(state.has(ItemName.knuckles_air_necklace, player) or
state.has(ItemName.knuckles_hammer_gloves, player)))
- add_rule(world.get_location(LocationName.prison_lane_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.prison_lane_animal_14, player),
lambda state: state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.metal_harbor_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.metal_harbor_animal_14, player),
lambda state: state.has(ItemName.sonic_light_shoes, player))
- add_rule(world.get_location(LocationName.mission_street_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_14, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_14, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_14, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_14, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_14, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.final_rush_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_14, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.sand_ocean_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.sand_ocean_animal_14, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.lost_colony_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_animal_14, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_14, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.white_jungle_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.white_jungle_animal_14, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_14, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_14, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player) and
state.has(ItemName.knuckles_air_necklace, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.prison_lane_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.prison_lane_animal_15, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.mission_street_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_15, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_15, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_15, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_15, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_15, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.final_rush_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_15, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.iron_gate_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.iron_gate_animal_15, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.sand_ocean_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.sand_ocean_animal_15, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_15, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.white_jungle_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.white_jungle_animal_15, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_15, player),
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_15, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player) and
state.has(ItemName.knuckles_air_necklace, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.mission_street_animal_16, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_16, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_16, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_16, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player) and
(state.has(ItemName.sonic_flame_ring, player) or
(state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_mystic_melody, player))))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_16, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_16, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player) and
state.has(ItemName.sonic_mystic_melody, player))
- add_rule(world.get_location(LocationName.final_rush_animal_16, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_16, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.white_jungle_animal_16, player),
+ add_rule(multiworld.get_location(LocationName.white_jungle_animal_16, player),
lambda state: state.has(ItemName.shadow_flame_ring, player) and
state.has(ItemName.shadow_air_shoes, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_16, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_16, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player) and
state.has(ItemName.knuckles_air_necklace, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_17, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_17, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_mystic_melody, player))
- add_rule(world.get_location(LocationName.final_chase_animal_17, player),
+ add_rule(multiworld.get_location(LocationName.final_chase_animal_17, player),
lambda state: state.has(ItemName.shadow_flame_ring, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_17, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_17, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player) and
@@ -1297,12 +1300,12 @@ def set_mission_upgrade_rules_standard(world: MultiWorld, player: int):
(state.has(ItemName.sonic_bounce_bracelet, player) or
state.has(ItemName.sonic_flame_ring, player)))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_18, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_18, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_mystic_melody, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_18, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_18, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player) and
@@ -1310,12 +1313,12 @@ def set_mission_upgrade_rules_standard(world: MultiWorld, player: int):
state.has(ItemName.knuckles_hammer_gloves, player) and
state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_19, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_19, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_mystic_melody, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_19, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_19, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player) and
@@ -1324,897 +1327,901 @@ def set_mission_upgrade_rules_standard(world: MultiWorld, player: int):
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.radical_highway_animal_20, player),
+ add_rule(multiworld.get_location(LocationName.radical_highway_animal_20, player),
lambda state: state.has(ItemName.shadow_flame_ring, player))
-def set_mission_upgrade_rules_hard(world: MultiWorld, player: int):
+def set_mission_upgrade_rules_hard(multiworld: MultiWorld, world: World, player: int):
# Mission 1 Upgrade Requirements
- add_rule_safe(world, LocationName.pumpkin_hill_1, player,
+ add_rule_safe(multiworld, LocationName.pumpkin_hill_1, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player))
- add_rule_safe(world, LocationName.mission_street_1, player,
+ add_rule_safe(multiworld, LocationName.mission_street_1, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.hidden_base_1, player,
+ add_rule_safe(multiworld, LocationName.hidden_base_1, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.death_chamber_1, player,
+ add_rule_safe(multiworld, LocationName.death_chamber_1, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule_safe(world, LocationName.eternal_engine_1, player,
+ add_rule_safe(multiworld, LocationName.eternal_engine_1, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule_safe(world, LocationName.crazy_gadget_1, player,
+ add_rule_safe(multiworld, LocationName.crazy_gadget_1, player,
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule_safe(world, LocationName.final_rush_1, player,
+ add_rule_safe(multiworld, LocationName.final_rush_1, player,
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule_safe(world, LocationName.egg_quarters_1, player,
+ add_rule_safe(multiworld, LocationName.egg_quarters_1, player,
lambda state: state.has(ItemName.rouge_pick_nails, player))
- add_rule_safe(world, LocationName.lost_colony_1, player,
+ add_rule_safe(multiworld, LocationName.lost_colony_1, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.weapons_bed_1, player,
+ add_rule_safe(multiworld, LocationName.weapons_bed_1, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.cosmic_wall_1, player,
+ add_rule_safe(multiworld, LocationName.cosmic_wall_1, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.cannon_core_1, player,
+ add_rule_safe(multiworld, LocationName.cannon_core_1, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
# Mission 2 Upgrade Requirements
- add_rule_safe(world, LocationName.mission_street_2, player,
+ add_rule_safe(multiworld, LocationName.mission_street_2, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.hidden_base_2, player,
+ add_rule_safe(multiworld, LocationName.hidden_base_2, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.death_chamber_2, player,
+ add_rule_safe(multiworld, LocationName.death_chamber_2, player,
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule_safe(world, LocationName.eternal_engine_2, player,
- lambda state: state.has(ItemName.tails_booster, player) and
- state.has(ItemName.tails_bazooka, player))
+ add_rule_safe(multiworld, LocationName.eternal_engine_2, player,
+ lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.lost_colony_2, player,
- lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.weapons_bed_2, player,
+ add_rule_safe(multiworld, LocationName.weapons_bed_2, player,
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule_safe(world, LocationName.cosmic_wall_2, player,
+ add_rule_safe(multiworld, LocationName.cosmic_wall_2, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.cannon_core_2, player,
+ add_rule_safe(multiworld, LocationName.cannon_core_2, player,
lambda state: state.has(ItemName.tails_booster, player))
# Mission 3 Upgrade Requirements
- add_rule_safe(world, LocationName.wild_canyon_3, player,
+ add_rule_safe(multiworld, LocationName.wild_canyon_3, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player))
- add_rule_safe(world, LocationName.prison_lane_3, player,
+ add_rule_safe(multiworld, LocationName.prison_lane_3, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.mission_street_3, player,
+ add_rule_safe(multiworld, LocationName.mission_street_3, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.aquatic_mine_3, player,
+ add_rule_safe(multiworld, LocationName.aquatic_mine_3, player,
lambda state: state.has(ItemName.knuckles_mystic_melody, player))
- add_rule_safe(world, LocationName.hidden_base_3, player,
+ add_rule_safe(multiworld, LocationName.hidden_base_3, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_mystic_melody, player))
- add_rule_safe(world, LocationName.death_chamber_3, player,
+ add_rule_safe(multiworld, LocationName.death_chamber_3, player,
lambda state: state.has(ItemName.knuckles_mystic_melody, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule_safe(world, LocationName.eternal_engine_3, player,
+ add_rule_safe(multiworld, LocationName.eternal_engine_3, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.meteor_herd_3, player,
+ add_rule_safe(multiworld, LocationName.meteor_herd_3, player,
lambda state: state.has(ItemName.knuckles_mystic_melody, player))
- add_rule_safe(world, LocationName.crazy_gadget_3, player,
+ add_rule_safe(multiworld, LocationName.crazy_gadget_3, player,
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule_safe(world, LocationName.final_rush_3, player,
+ add_rule_safe(multiworld, LocationName.final_rush_3, player,
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule_safe(world, LocationName.iron_gate_3, player,
+ add_rule_safe(multiworld, LocationName.iron_gate_3, player,
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.dry_lagoon_3, player,
+ add_rule_safe(multiworld, LocationName.dry_lagoon_3, player,
lambda state: state.has(ItemName.rouge_mystic_melody, player) and
state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule_safe(world, LocationName.sand_ocean_3, player,
+ add_rule_safe(multiworld, LocationName.sand_ocean_3, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.egg_quarters_3, player,
+ add_rule_safe(multiworld, LocationName.egg_quarters_3, player,
lambda state: state.has(ItemName.rouge_mystic_melody, player))
- add_rule_safe(world, LocationName.lost_colony_3, player,
+ add_rule_safe(multiworld, LocationName.lost_colony_3, player,
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.weapons_bed_3, player,
+ add_rule_safe(multiworld, LocationName.weapons_bed_3, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.mad_space_3, player,
+ add_rule_safe(multiworld, LocationName.mad_space_3, player,
lambda state: state.has(ItemName.rouge_mystic_melody, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule_safe(world, LocationName.cosmic_wall_3, player,
+ add_rule_safe(multiworld, LocationName.cosmic_wall_3, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.cannon_core_3, player,
+ add_rule_safe(multiworld, LocationName.cannon_core_3, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
# Mission 4 Upgrade Requirements
- add_rule_safe(world, LocationName.pumpkin_hill_4, player,
+ add_rule_safe(multiworld, LocationName.pumpkin_hill_4, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player))
- add_rule_safe(world, LocationName.mission_street_4, player,
+ add_rule_safe(multiworld, LocationName.mission_street_4, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.hidden_base_4, player,
+ add_rule_safe(multiworld, LocationName.hidden_base_4, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.death_chamber_4, player,
+ add_rule_safe(multiworld, LocationName.death_chamber_4, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule_safe(world, LocationName.eternal_engine_4, player,
+ add_rule_safe(multiworld, LocationName.eternal_engine_4, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule_safe(world, LocationName.crazy_gadget_4, player,
+ add_rule_safe(multiworld, LocationName.crazy_gadget_4, player,
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule_safe(world, LocationName.final_rush_4, player,
+ add_rule_safe(multiworld, LocationName.final_rush_4, player,
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule_safe(world, LocationName.egg_quarters_4, player,
+ add_rule_safe(multiworld, LocationName.egg_quarters_4, player,
lambda state: state.has(ItemName.rouge_pick_nails, player))
- add_rule_safe(world, LocationName.lost_colony_4, player,
+ add_rule_safe(multiworld, LocationName.lost_colony_4, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.weapons_bed_4, player,
+ add_rule_safe(multiworld, LocationName.weapons_bed_4, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.cosmic_wall_4, player,
+ add_rule_safe(multiworld, LocationName.cosmic_wall_4, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.cannon_core_4, player,
+ add_rule_safe(multiworld, LocationName.cannon_core_4, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
# Mission 5 Upgrade Requirements
- add_rule_safe(world, LocationName.city_escape_5, player,
+ add_rule_safe(multiworld, LocationName.city_escape_5, player,
lambda state: state.has(ItemName.sonic_flame_ring, player))
- add_rule_safe(world, LocationName.wild_canyon_5, player,
+ add_rule_safe(multiworld, LocationName.wild_canyon_5, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player))
- add_rule_safe(world, LocationName.pumpkin_hill_5, player,
+ add_rule_safe(multiworld, LocationName.pumpkin_hill_5, player,
lambda state: state.has(ItemName.knuckles_shovel_claws, player))
- add_rule_safe(world, LocationName.mission_street_5, player,
+ add_rule_safe(multiworld, LocationName.mission_street_5, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.aquatic_mine_5, player,
+ add_rule_safe(multiworld, LocationName.aquatic_mine_5, player,
lambda state: state.has(ItemName.knuckles_mystic_melody, player))
- add_rule_safe(world, LocationName.hidden_base_5, player,
+ add_rule_safe(multiworld, LocationName.hidden_base_5, player,
lambda state: state.has(ItemName.tails_booster, player))
- add_rule_safe(world, LocationName.death_chamber_5, player,
+ add_rule_safe(multiworld, LocationName.death_chamber_5, player,
lambda state: state.has(ItemName.knuckles_hammer_gloves, player) and
state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_mystic_melody, player))
- add_rule_safe(world, LocationName.eternal_engine_5, player,
+ add_rule_safe(multiworld, LocationName.eternal_engine_5, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule_safe(world, LocationName.crazy_gadget_5, player,
+ add_rule_safe(multiworld, LocationName.crazy_gadget_5, player,
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule_safe(world, LocationName.final_rush_5, player,
+ add_rule_safe(multiworld, LocationName.final_rush_5, player,
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule_safe(world, LocationName.iron_gate_5, player,
+ add_rule_safe(multiworld, LocationName.iron_gate_5, player,
lambda state: state.has(ItemName.eggman_large_cannon, player))
- add_rule_safe(world, LocationName.dry_lagoon_5, player,
+ add_rule_safe(multiworld, LocationName.dry_lagoon_5, player,
lambda state: state.has(ItemName.rouge_treasure_scope, player))
- add_rule_safe(world, LocationName.sand_ocean_5, player,
+ add_rule_safe(multiworld, LocationName.sand_ocean_5, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.egg_quarters_5, player,
+ add_rule_safe(multiworld, LocationName.egg_quarters_5, player,
lambda state: state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_treasure_scope, player))
- add_rule_safe(world, LocationName.lost_colony_5, player,
+ add_rule_safe(multiworld, LocationName.lost_colony_5, player,
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule_safe(world, LocationName.weapons_bed_5, player,
+ add_rule_safe(multiworld, LocationName.weapons_bed_5, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.security_hall_5, player,
+ add_rule_safe(multiworld, LocationName.security_hall_5, player,
lambda state: state.has(ItemName.rouge_treasure_scope, player))
- add_rule_safe(world, LocationName.cosmic_wall_5, player,
+ add_rule_safe(multiworld, LocationName.cosmic_wall_5, player,
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule_safe(world, LocationName.cannon_core_5, player,
+ add_rule_safe(multiworld, LocationName.cannon_core_5, player,
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.knuckles_mystic_melody, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
# Upgrade Spot Upgrade Requirements
- add_rule(world.get_location(LocationName.city_escape_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.city_escape_upgrade, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.wild_canyon_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.wild_canyon_upgrade, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player))
- add_rule(world.get_location(LocationName.prison_lane_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.prison_lane_upgrade, player),
lambda state: state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.hidden_base_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_upgrade, player),
lambda state: state.has(ItemName.tails_booster, player) and
(state.has(ItemName.tails_bazooka, player) or state.has(ItemName.tails_mystic_melody, player)))
- add_rule(world.get_location(LocationName.eternal_engine_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_upgrade, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.meteor_herd_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.meteor_herd_upgrade, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.final_rush_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_upgrade, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.iron_gate_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.iron_gate_upgrade, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) or
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.dry_lagoon_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.dry_lagoon_upgrade, player),
lambda state: state.has(ItemName.rouge_pick_nails, player))
- add_rule(world.get_location(LocationName.sand_ocean_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.sand_ocean_upgrade, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.security_hall_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.security_hall_upgrade, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_upgrade, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_upgrade, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
# Chao Key Upgrade Requirements
- if world.keysanity[player]:
- add_rule(world.get_location(LocationName.prison_lane_chao_1, player),
+ if world.options.keysanity:
+ add_rule(multiworld.get_location(LocationName.prison_lane_chao_1, player),
lambda state: state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.mission_street_chao_1, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_chao_1, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_chao_1, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_chao_1, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_chao_1, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_chao_1, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_chao_1, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_chao_1, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.cosmic_wall_chao_1, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_1, player),
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_chao_1, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_chao_1, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.prison_lane_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.prison_lane_chao_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.mission_street_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_chao_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_chao_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_chao_2, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_chao_2, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_chao_2, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.weapons_bed_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_chao_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cosmic_wall_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_chao_2, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_chao_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.mission_street_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_chao_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_chao_3, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_chao_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.crazy_gadget_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_chao_3, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.final_rush_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_chao_3, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.egg_quarters_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.egg_quarters_chao_3, player),
lambda state: state.has(ItemName.rouge_mystic_melody, player))
- add_rule(world.get_location(LocationName.lost_colony_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_chao_3, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.weapons_bed_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_chao_3, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.security_hall_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.security_hall_chao_3, player),
lambda state: state.has(ItemName.rouge_pick_nails, player))
- add_rule(world.get_location(LocationName.cosmic_wall_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_3, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_chao_3, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_chao_3, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.knuckles_hammer_gloves, player) and
state.has(ItemName.sonic_flame_ring, player))
# Pipe Upgrade Requirements
- if world.whistlesanity[player].value == 1 or world.whistlesanity[player].value == 3:
- add_rule(world.get_location(LocationName.hidden_base_pipe_1, player),
+ if world.options.whistlesanity.value == 1 or world.options.whistlesanity.value == 3:
+ add_rule(multiworld.get_location(LocationName.hidden_base_pipe_1, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.cosmic_wall_pipe_1, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_pipe_1, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.hidden_base_pipe_2, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_pipe_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_pipe_2, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_pipe_2, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_pipe_2, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_pipe_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.lost_colony_pipe_2, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_pipe_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cosmic_wall_pipe_2, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_pipe_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_pipe_2, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_pipe_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.prison_lane_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.prison_lane_pipe_3, player),
lambda state: state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.mission_street_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_pipe_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_pipe_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_pipe_3, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_pipe_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.weapons_bed_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_pipe_3, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cosmic_wall_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_pipe_3, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_pipe_3, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_pipe_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_pipe_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.eternal_engine_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_pipe_4, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_pipe_4, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.weapons_bed_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_pipe_4, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.cosmic_wall_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_pipe_4, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_pipe_4, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_pipe_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_pipe_5, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_pipe_5, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.eternal_engine_pipe_5, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_pipe_5, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.weapons_bed_pipe_5, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_pipe_5, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cosmic_wall_pipe_5, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_pipe_5, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_pipe_5, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_pipe_5, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
# Hidden Whistle Upgrade Requirements
- if world.whistlesanity[player].value == 2 or world.whistlesanity[player].value == 3:
- add_rule(world.get_location(LocationName.mission_street_hidden_3, player),
+ if world.options.whistlesanity.value == 2 or world.options.whistlesanity.value == 3:
+ add_rule(multiworld.get_location(LocationName.mission_street_hidden_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.mission_street_hidden_4, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_hidden_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_hidden_1, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_hidden_1, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.death_chamber_hidden_2, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_hidden_2, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.cannon_core_hidden_1, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_hidden_1, player),
lambda state: state.has(ItemName.tails_booster, player))
# Omochao Upgrade Requirements
- if world.omosanity[player]:
- add_rule(world.get_location(LocationName.eternal_engine_omo_1, player),
+ if world.options.omosanity:
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_1, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_omo_2, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_omo_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_omo_2, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_omo_2, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_2, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.weapons_bed_omo_2, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_omo_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) or
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.hidden_base_omo_3, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_omo_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_3, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.final_rush_omo_3, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_omo_3, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.weapons_bed_omo_3, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_omo_3, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.hidden_base_omo_4, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_omo_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_omo_4, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_omo_4, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_4, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.cannon_core_omo_4, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_omo_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.mission_street_omo_5, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_omo_5, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_omo_5, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_omo_5, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_5, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_5, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.cannon_core_omo_5, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_omo_5, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.mission_street_omo_6, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_omo_6, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_omo_6, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_omo_6, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_6, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_6, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.lost_colony_omo_6, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_omo_6, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_omo_6, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_omo_6, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.mission_street_omo_7, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_omo_7, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_omo_7, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_omo_7, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_7, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_7, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.lost_colony_omo_7, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_omo_7, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_omo_7, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_omo_7, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.mission_street_omo_8, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_omo_8, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_omo_8, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_omo_8, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_8, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_8, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.lost_colony_omo_8, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_omo_8, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.security_hall_omo_8, player),
+ add_rule(multiworld.get_location(LocationName.security_hall_omo_8, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cannon_core_omo_8, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_omo_8, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.death_chamber_omo_9, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_omo_9, player),
lambda state: state.has(ItemName.knuckles_mystic_melody, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_9, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_9, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.cannon_core_omo_9, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_omo_9, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_10, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_10, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_11, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_11, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.eternal_engine_omo_12, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_omo_12, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_omo_12, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_omo_12, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.crazy_gadget_omo_13, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_omo_13, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_flame_ring, player))
# Gold Beetle Upgrade Requirements
- if world.beetlesanity[player]:
- add_rule(world.get_location(LocationName.hidden_base_beetle, player),
+ if world.options.beetlesanity:
+ add_rule(multiworld.get_location(LocationName.hidden_base_beetle, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_beetle, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_beetle, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_beetle, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_beetle, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_beetle, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_beetle, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.dry_lagoon_beetle, player),
+ add_rule(multiworld.get_location(LocationName.dry_lagoon_beetle, player),
lambda state: state.has(ItemName.rouge_mystic_melody, player) and
state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.lost_colony_beetle, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_beetle, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cosmic_wall_beetle, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_beetle, player),
lambda state: state.has(ItemName.eggman_mystic_melody, player) and
state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_beetle, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_beetle, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
# Animal Upgrade Requirements
- if world.animalsanity[player]:
- add_rule(world.get_location(LocationName.hidden_base_animal_2, player),
+ if world.options.animalsanity:
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_2, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_2, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_2, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_3, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_3, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_3, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_3, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_3, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_3, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_3, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_3, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_4, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_4, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_4, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_4, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_4, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_4, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_4, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_4, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_4, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_4, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_5, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_5, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_5, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_5, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_5, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_5, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_5, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_6, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_6, player),
lambda state: state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_6, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_6, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_6, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_6, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_6, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_7, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_7, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_7, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_7, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.security_hall_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.security_hall_animal_7, player),
lambda state: state.has(ItemName.rouge_pick_nails, player) or
state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_7, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_7, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_7, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_8, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_8, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_8, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_8, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.security_hall_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.security_hall_animal_8, player),
lambda state: state.has(ItemName.rouge_pick_nails, player) and
state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_8, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_8, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_8, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.mission_street_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_9, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_9, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_9, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_9, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.final_rush_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_9, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_animal_9, player),
+ lambda state: state.has(ItemName.eggman_jet_engine, player) or
+ state.has(ItemName.eggman_large_cannon, player))
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_9, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_9, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_9, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_9, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.wild_canyon_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.wild_canyon_animal_10, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player))
- add_rule(world.get_location(LocationName.mission_street_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_10, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.aquatic_mine_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.aquatic_mine_animal_10, player),
lambda state: state.has(ItemName.knuckles_mystic_melody, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_10, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.death_chamber_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.death_chamber_animal_10, player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_10, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.final_rush_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_10, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.egg_quarters_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.egg_quarters_animal_10, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.lost_colony_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_animal_10, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_10, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.mad_space_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.mad_space_animal_10, player),
lambda state: state.has(ItemName.rouge_iron_boots, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_10, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_10, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_10, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.mission_street_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_11, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_11, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_11, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.final_rush_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_11, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.lost_colony_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_animal_11, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_11, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_11, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_11, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_11, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.mission_street_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_12, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_12, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_12, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_12, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.final_rush_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_12, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.lost_colony_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_animal_12, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_12, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_12, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_12, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_12, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.prison_lane_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.prison_lane_animal_13, player),
lambda state: state.has(ItemName.tails_booster, player) or
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.mission_street_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_13, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_13, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_13, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_13, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.final_rush_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_13, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.lost_colony_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_animal_13, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_13, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_13, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_13, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_13, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.prison_lane_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.prison_lane_animal_14, player),
lambda state: state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.mission_street_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_14, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_14, player),
lambda state: state.has(ItemName.tails_booster, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_14, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_14, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.final_rush_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_14, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.lost_colony_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.lost_colony_animal_14, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_14, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_14, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_14, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_14, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_large_cannon, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.prison_lane_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.prison_lane_animal_15, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.mission_street_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_15, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.hidden_base_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.hidden_base_animal_15, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.eternal_engine_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.eternal_engine_animal_15, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_15, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.final_rush_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_15, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.iron_gate_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.iron_gate_animal_15, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.sand_ocean_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.sand_ocean_animal_15, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.weapons_bed_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.weapons_bed_animal_15, player),
lambda state: state.has(ItemName.eggman_jet_engine, player) and
state.has(ItemName.eggman_large_cannon, player))
- add_rule(world.get_location(LocationName.cosmic_wall_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_15, player),
lambda state: state.has(ItemName.eggman_jet_engine, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_15, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_15, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_large_cannon, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.mission_street_animal_16, player),
+ add_rule(multiworld.get_location(LocationName.mission_street_animal_16, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.tails_bazooka, player))
- add_rule(world.get_location(LocationName.crazy_gadget_animal_16, player),
+ add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_16, player),
lambda state: state.has(ItemName.sonic_light_shoes, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.final_rush_animal_16, player),
+ add_rule(multiworld.get_location(LocationName.final_rush_animal_16, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_16, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_16, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_large_cannon, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.final_chase_animal_17, player),
+ add_rule(multiworld.get_location(LocationName.final_chase_animal_17, player),
lambda state: state.has(ItemName.shadow_flame_ring, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_17, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_17, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_large_cannon, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_18, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_18, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_large_cannon, player) and
state.has(ItemName.knuckles_hammer_gloves, player))
- add_rule(world.get_location(LocationName.pyramid_cave_animal_19, player),
+ add_rule(multiworld.get_location(LocationName.pyramid_cave_animal_19, player),
lambda state: state.has(ItemName.sonic_bounce_bracelet, player) and
state.has(ItemName.sonic_mystic_melody, player))
- add_rule(world.get_location(LocationName.cannon_core_animal_19, player),
+ add_rule(multiworld.get_location(LocationName.cannon_core_animal_19, player),
lambda state: state.has(ItemName.tails_booster, player) and
state.has(ItemName.eggman_large_cannon, player) and
state.has(ItemName.knuckles_hammer_gloves, player) and
state.has(ItemName.sonic_flame_ring, player))
- add_rule(world.get_location(LocationName.radical_highway_animal_20, player),
+ add_rule(multiworld.get_location(LocationName.radical_highway_animal_20, player),
lambda state: state.has(ItemName.shadow_flame_ring, player))
-def set_boss_gate_rules(world: MultiWorld, player: int, gate_bosses: typing.Dict[int, int]):
+def set_boss_gate_rules(multiworld: MultiWorld, player: int, gate_bosses: typing.Dict[int, int]):
for x in range(len(gate_bosses)):
if boss_has_requirement(gate_bosses[x + 1]):
- add_rule(world.get_location(boss_gate_set[x], player),
+ add_rule(multiworld.get_location(boss_gate_set[x], player),
lambda state: state.has(ItemName.knuckles_shovel_claws, player))
-def set_rules(world: MultiWorld, player: int, gate_bosses: typing.Dict[int, int], boss_rush_map: typing.Dict[int, int], mission_map: typing.Dict[int, int], mission_count_map: typing.Dict[int, int]):
+def set_rules(multiworld: MultiWorld, world: World, player: int, gate_bosses: typing.Dict[int, int], boss_rush_map: typing.Dict[int, int], mission_map: typing.Dict[int, int], mission_count_map: typing.Dict[int, int], black_market_costs: typing.Dict[int, int]):
# Mission Progression Rules (Mission 1 begets Mission 2, etc.)
- set_mission_progress_rules(world, player, mission_map, mission_count_map)
+ set_mission_progress_rules(multiworld, player, mission_map, mission_count_map)
- if world.goal[player].value != 3:
+ if world.options.goal.value != 3:
# Upgrade Requirements for each mission location
- if world.logic_difficulty[player].value == 0:
- set_mission_upgrade_rules_standard(world, player)
- elif world.logic_difficulty[player].value == 1:
- set_mission_upgrade_rules_hard(world, player)
+ if world.options.logic_difficulty.value == 0:
+ set_mission_upgrade_rules_standard(multiworld, world, player)
+ elif world.options.logic_difficulty.value == 1:
+ set_mission_upgrade_rules_hard(multiworld, world, player)
+
+ for i in range(world.options.black_market_slots.value):
+ add_rule(multiworld.get_location(LocationName.chao_black_market_base + str(i + 1), player),
+ lambda state, i=i: (state.has(ItemName.market_token, player, black_market_costs[i])))
- if world.goal[player] in [4, 5, 6]:
+ if world.options.goal in [4, 5, 6]:
for i in range(16):
if boss_rush_map[i] == 10:
- add_rule(world.get_location("Boss Rush - " + str(i + 1), player),
+ add_rule(multiworld.get_location("Boss Rush - " + str(i + 1), player),
lambda state: (state.has(ItemName.knuckles_shovel_claws, player)))
# Upgrade Requirements for each boss gate
- set_boss_gate_rules(world, player, gate_bosses)
+ set_boss_gate_rules(multiworld, player, gate_bosses)
- world.completion_condition[player] = lambda state: state.has(ItemName.maria, player)
+ multiworld.completion_condition[player] = lambda state: state.has(ItemName.maria, player)
diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py
index 496d18fa379c..f7d1ca72b09f 100644
--- a/worlds/sa2b/__init__.py
+++ b/worlds/sa2b/__init__.py
@@ -1,18 +1,22 @@
import typing
import math
+import logging
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
-from .Items import SA2BItem, ItemData, item_table, upgrades_table, emeralds_table, junk_table, trap_table, item_groups
-from .Locations import SA2BLocation, all_locations, setup_locations
-from .Options import sa2b_options
-from .Regions import create_regions, shuffleable_regions, connect_regions, LevelGate, gate_0_whitelist_regions, \
- gate_0_blacklist_regions
-from .Rules import set_rules
-from .Names import ItemName, LocationName
from worlds.AutoWorld import WebWorld, World
+
+from .AestheticData import chao_name_conversion, sample_chao_names, totally_real_item_names, \
+ all_exits, all_destinations, multi_rooms, single_rooms, room_to_exits_map, exit_to_room_map, valid_kindergarten_exits
from .GateBosses import get_gate_bosses, get_boss_rush_bosses, get_boss_name
+from .Items import SA2BItem, ItemData, item_table, upgrades_table, emeralds_table, junk_table, trap_table, item_groups, \
+ eggs_table, fruits_table, seeds_table, hats_table, animals_table, chaos_drives_table
+from .Locations import SA2BLocation, all_locations, setup_locations, chao_animal_event_location_table, black_market_location_table
from .Missions import get_mission_table, get_mission_count_table, get_first_and_last_cannons_core_missions
-import Patch
+from .Names import ItemName, LocationName
+from .Options import SA2BOptions, sa2b_option_groups
+from .Regions import create_regions, shuffleable_regions, connect_regions, LevelGate, gate_0_whitelist_regions, \
+ gate_0_blacklist_regions
+from .Rules import set_rules
class SA2BWeb(WebWorld):
@@ -26,8 +30,9 @@ class SA2BWeb(WebWorld):
"setup/en",
["RaspberrySpaceJam", "PoryGone", "Entiss"]
)
-
+
tutorials = [setup_en]
+ option_groups = sa2b_option_groups
def check_for_impossible_shuffle(shuffled_levels: typing.List[int], gate_0_range: int, multiworld: MultiWorld):
@@ -50,9 +55,9 @@ class SA2BWorld(World):
Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rouge, and Eggman across 31 stages and prevent the destruction of the earth.
"""
game: str = "Sonic Adventure 2 Battle"
- option_definitions = sa2b_options
+ options_dataclass = SA2BOptions
+ options: SA2BOptions
topology_present = False
- data_version = 6
item_name_groups = item_groups
item_name_to_id = {name: data.code for name, data in item_table.items()}
@@ -60,8 +65,6 @@ class SA2BWorld(World):
location_table: typing.Dict[str, int]
- music_map: typing.Dict[int, int]
- voice_map: typing.Dict[int, int]
mission_map: typing.Dict[int, int]
mission_count_map: typing.Dict[int, int]
emblems_for_cannons_core: int
@@ -69,138 +72,126 @@ class SA2BWorld(World):
gate_costs: typing.Dict[int, int]
gate_bosses: typing.Dict[int, int]
boss_rush_map: typing.Dict[int, int]
+ black_market_costs: typing.Dict[int, int]
+
web = SA2BWeb()
- def _get_slot_data(self):
+ def fill_slot_data(self) -> dict:
return {
- "ModVersion": 202,
- "Goal": self.multiworld.goal[self.player].value,
- "MusicMap": self.music_map,
- "VoiceMap": self.voice_map,
+ "ModVersion": 203,
+ "Goal": self.options.goal.value,
+ "MusicMap": self.generate_music_data(),
+ "VoiceMap": self.generate_voice_data(),
+ "DefaultEggMap": self.generate_chao_egg_data(),
+ "DefaultChaoNameMap": self.generate_chao_name_data(),
"MissionMap": self.mission_map,
"MissionCountMap": self.mission_count_map,
- "MusicShuffle": self.multiworld.music_shuffle[self.player].value,
- "Narrator": self.multiworld.narrator[self.player].value,
- "MinigameTrapDifficulty": self.multiworld.minigame_trap_difficulty[self.player].value,
- "RingLoss": self.multiworld.ring_loss[self.player].value,
- "RingLink": self.multiworld.ring_link[self.player].value,
- "RequiredRank": self.multiworld.required_rank[self.player].value,
- "ChaoKeys": self.multiworld.keysanity[self.player].value,
- "Whistlesanity": self.multiworld.whistlesanity[self.player].value,
- "GoldBeetles": self.multiworld.beetlesanity[self.player].value,
- "OmochaoChecks": self.multiworld.omosanity[self.player].value,
- "AnimalChecks": self.multiworld.animalsanity[self.player].value,
- "KartRaceChecks": self.multiworld.kart_race_checks[self.player].value,
- "ChaoRaceChecks": self.multiworld.chao_race_checks[self.player].value,
- "ChaoGardenDifficulty": self.multiworld.chao_garden_difficulty[self.player].value,
- "DeathLink": self.multiworld.death_link[self.player].value,
- "EmblemPercentageForCannonsCore": self.multiworld.emblem_percentage_for_cannons_core[self.player].value,
- "RequiredCannonsCoreMissions": self.multiworld.required_cannons_core_missions[self.player].value,
- "NumberOfLevelGates": self.multiworld.number_of_level_gates[self.player].value,
- "LevelGateDistribution": self.multiworld.level_gate_distribution[self.player].value,
+ "MusicShuffle": self.options.music_shuffle.value,
+ "Narrator": self.options.narrator.value,
+ "MinigameTrapDifficulty": self.options.minigame_trap_difficulty.value,
+ "RingLoss": self.options.ring_loss.value,
+ "RingLink": self.options.ring_link.value,
+ "RequiredRank": self.options.required_rank.value,
+ "ChaoKeys": self.options.keysanity.value,
+ "Whistlesanity": self.options.whistlesanity.value,
+ "GoldBeetles": self.options.beetlesanity.value,
+ "OmochaoChecks": self.options.omosanity.value,
+ "AnimalChecks": self.options.animalsanity.value,
+ "KartRaceChecks": self.options.kart_race_checks.value,
+ "ChaoStadiumChecks": self.options.chao_stadium_checks.value,
+ "ChaoRaceDifficulty": self.options.chao_race_difficulty.value,
+ "ChaoKarateDifficulty": self.options.chao_karate_difficulty.value,
+ "ChaoStats": self.options.chao_stats.value,
+ "ChaoStatsFrequency": self.options.chao_stats_frequency.value,
+ "ChaoStatsStamina": self.options.chao_stats_stamina.value,
+ "ChaoStatsHidden": self.options.chao_stats_hidden.value,
+ "ChaoAnimalParts": self.options.chao_animal_parts.value,
+ "ChaoKindergarten": self.options.chao_kindergarten.value,
+ "BlackMarketSlots": self.options.black_market_slots.value,
+ "BlackMarketData": self.generate_black_market_data(),
+ "BlackMarketUnlockCosts": self.black_market_costs,
+ "BlackMarketUnlockSetting": self.options.black_market_unlock_costs.value,
+ "ChaoERLayout": self.generate_er_layout(),
+ "DeathLink": self.options.death_link.value,
+ "EmblemPercentageForCannonsCore": self.options.emblem_percentage_for_cannons_core.value,
+ "RequiredCannonsCoreMissions": self.options.required_cannons_core_missions.value,
+ "NumberOfLevelGates": self.options.number_of_level_gates.value,
+ "LevelGateDistribution": self.options.level_gate_distribution.value,
"EmblemsForCannonsCore": self.emblems_for_cannons_core,
"RegionEmblemMap": self.region_emblem_map,
"GateCosts": self.gate_costs,
"GateBosses": self.gate_bosses,
"BossRushMap": self.boss_rush_map,
+ "PlayerNum": self.player,
}
- def _create_items(self, name: str):
- data = item_table[name]
- return [self.create_item(name) for _ in range(data.quantity)]
-
- def fill_slot_data(self) -> dict:
- slot_data = self._get_slot_data()
- slot_data["MusicMap"] = self.music_map
- for option_name in sa2b_options:
- option = getattr(self.multiworld, option_name)[self.player]
- slot_data[option_name] = option.value
-
- return slot_data
-
- def get_levels_per_gate(self) -> list:
- levels_per_gate = list()
- max_gate_index = self.multiworld.number_of_level_gates[self.player]
- average_level_count = 30 / (max_gate_index + 1)
- levels_added = 0
-
- for i in range(max_gate_index + 1):
- levels_per_gate.append(average_level_count)
- levels_added += average_level_count
- additional_count_iterator = 0
- while levels_added < 30:
- levels_per_gate[additional_count_iterator] += 1
- levels_added += 1
- additional_count_iterator += 1 if additional_count_iterator < max_gate_index else -max_gate_index
-
- if self.multiworld.level_gate_distribution[self.player] == 0 or self.multiworld.level_gate_distribution[self.player] == 2:
- early_distribution = self.multiworld.level_gate_distribution[self.player] == 0
- levels_to_distribute = 5
- gate_index_offset = 0
- while levels_to_distribute > 0:
- if levels_per_gate[0 + gate_index_offset] == 1 or \
- levels_per_gate[max_gate_index - gate_index_offset] == 1:
- break
- if early_distribution:
- levels_per_gate[0 + gate_index_offset] += 1
- levels_per_gate[max_gate_index - gate_index_offset] -= 1
- else:
- levels_per_gate[0 + gate_index_offset] -= 1
- levels_per_gate[max_gate_index - gate_index_offset] += 1
- gate_index_offset += 1
- if gate_index_offset > math.floor(max_gate_index / 2):
- gate_index_offset = 0
- levels_to_distribute -= 1
-
- return levels_per_gate
-
def generate_early(self):
- if self.multiworld.goal[self.player].value == 3:
+ if self.options.goal.value == 3:
# Turn off everything else for Grand Prix goal
- self.multiworld.number_of_level_gates[self.player].value = 0
- self.multiworld.emblem_percentage_for_cannons_core[self.player].value = 0
- self.multiworld.junk_fill_percentage[self.player].value = 100
- self.multiworld.trap_fill_percentage[self.player].value = 100
- self.multiworld.omochao_trap_weight[self.player].value = 0
- self.multiworld.timestop_trap_weight[self.player].value = 0
- self.multiworld.confusion_trap_weight[self.player].value = 0
- self.multiworld.tiny_trap_weight[self.player].value = 0
- self.multiworld.gravity_trap_weight[self.player].value = 0
- self.multiworld.ice_trap_weight[self.player].value = 0
- self.multiworld.slow_trap_weight[self.player].value = 0
-
- valid_trap_weights = self.multiworld.exposition_trap_weight[self.player].value + \
- self.multiworld.cutscene_trap_weight[self.player].value + \
- self.multiworld.pong_trap_weight[self.player].value
+ self.options.number_of_level_gates.value = 0
+ self.options.emblem_percentage_for_cannons_core.value = 0
+
+ self.options.chao_race_difficulty.value = 0
+ self.options.chao_karate_difficulty.value = 0
+ self.options.chao_stats.value = 0
+ self.options.chao_animal_parts.value = 0
+ self.options.chao_kindergarten.value = 0
+ self.options.black_market_slots.value = 0
+
+ self.options.junk_fill_percentage.value = 100
+ self.options.trap_fill_percentage.value = 100
+ self.options.omochao_trap_weight.value = 0
+ self.options.timestop_trap_weight.value = 0
+ self.options.confusion_trap_weight.value = 0
+ self.options.tiny_trap_weight.value = 0
+ self.options.gravity_trap_weight.value = 0
+ self.options.ice_trap_weight.value = 0
+ self.options.slow_trap_weight.value = 0
+ self.options.cutscene_trap_weight.value = 0
+
+ valid_trap_weights = self.options.exposition_trap_weight.value + \
+ self.options.reverse_trap_weight.value + \
+ self.options.pong_trap_weight.value
if valid_trap_weights == 0:
- self.multiworld.exposition_trap_weight[self.player].value = 4
- self.multiworld.cutscene_trap_weight[self.player].value = 4
- self.multiworld.pong_trap_weight[self.player].value = 4
+ self.options.exposition_trap_weight.value = 4
+ self.options.reverse_trap_weight.value = 4
+ self.options.pong_trap_weight.value = 4
- if self.multiworld.kart_race_checks[self.player].value == 0:
- self.multiworld.kart_race_checks[self.player].value = 2
+ if self.options.kart_race_checks.value == 0:
+ self.options.kart_race_checks.value = 2
self.gate_bosses = {}
self.boss_rush_map = {}
else:
- self.gate_bosses = get_gate_bosses(self.multiworld, self.player)
- self.boss_rush_map = get_boss_rush_bosses(self.multiworld, self.player)
+ self.gate_bosses = get_gate_bosses(self.multiworld, self)
+ self.boss_rush_map = get_boss_rush_bosses(self.multiworld, self)
def create_regions(self):
- self.mission_map = get_mission_table(self.multiworld, self.player)
- self.mission_count_map = get_mission_count_table(self.multiworld, self.player)
+ self.mission_map = get_mission_table(self.multiworld, self, self.player)
+ self.mission_count_map = get_mission_count_table(self.multiworld, self, self.player)
- self.location_table = setup_locations(self.multiworld, self.player, self.mission_map, self.mission_count_map)
- create_regions(self.multiworld, self.player, self.location_table)
+ self.location_table = setup_locations(self, self.player, self.mission_map, self.mission_count_map)
+ create_regions(self.multiworld, self, self.player, self.location_table)
# Not Generate Basic
- if self.multiworld.goal[self.player].value in [0, 2, 4, 5, 6]:
+ self.black_market_costs = dict()
+
+ if self.options.goal.value in [0, 2, 4, 5, 6]:
self.multiworld.get_location(LocationName.finalhazard, self.player).place_locked_item(self.create_item(ItemName.maria))
- elif self.multiworld.goal[self.player].value == 1:
+ elif self.options.goal.value == 1:
self.multiworld.get_location(LocationName.green_hill, self.player).place_locked_item(self.create_item(ItemName.maria))
- elif self.multiworld.goal[self.player].value == 3:
+ elif self.options.goal.value == 3:
self.multiworld.get_location(LocationName.grand_prix, self.player).place_locked_item(self.create_item(ItemName.maria))
+ elif self.options.goal.value == 7:
+ self.multiworld.get_location(LocationName.chaos_chao, self.player).place_locked_item(self.create_item(ItemName.maria))
+
+ for animal_name in chao_animal_event_location_table.keys():
+ animal_region = self.multiworld.get_region(animal_name, self.player)
+ animal_event_location = SA2BLocation(self.player, animal_name, None, animal_region)
+ animal_region.locations.append(animal_event_location)
+ animal_event_item = SA2BItem(animal_name, ItemClassification.progression, None, self.player)
+ self.multiworld.get_location(animal_name, self.player).place_locked_item(animal_event_item)
itempool: typing.List[SA2BItem] = []
@@ -208,28 +199,40 @@ def create_regions(self):
total_required_locations = len(self.location_table)
total_required_locations -= 1; # Locked Victory Location
- if self.multiworld.goal[self.player].value != 3:
+ if self.options.goal.value != 3:
# Fill item pool with all required items
for item in {**upgrades_table}:
- itempool += [self.create_item(item, False, self.multiworld.goal[self.player].value)]
+ itempool += [self.create_item(item, False, self.options.goal.value)]
- if self.multiworld.goal[self.player].value in [1, 2, 6]:
+ if self.options.goal.value in [1, 2, 6]:
# Some flavor of Chaos Emerald Hunt
for item in {**emeralds_table}:
- itempool += self._create_items(item)
+ itempool.append(self.create_item(item))
+
+ # Black Market
+ itempool += [self.create_item(ItemName.market_token) for _ in range(self.options.black_market_slots.value)]
+
+ black_market_unlock_mult = 1.0
+ if self.options.black_market_unlock_costs.value == 0:
+ black_market_unlock_mult = 0.5
+ elif self.options.black_market_unlock_costs.value == 1:
+ black_market_unlock_mult = 0.75
+
+ for i in range(self.options.black_market_slots.value):
+ self.black_market_costs[i] = math.floor((i + 1) * black_market_unlock_mult)
# Cap at player-specified Emblem count
raw_emblem_count = total_required_locations - len(itempool)
- total_emblem_count = min(raw_emblem_count, self.multiworld.max_emblem_cap[self.player].value)
+ total_emblem_count = min(raw_emblem_count, self.options.max_emblem_cap.value)
extra_junk_count = raw_emblem_count - total_emblem_count
self.emblems_for_cannons_core = math.floor(
- total_emblem_count * (self.multiworld.emblem_percentage_for_cannons_core[self.player].value / 100.0))
+ total_emblem_count * (self.options.emblem_percentage_for_cannons_core.value / 100.0))
gate_cost_mult = 1.0
- if self.multiworld.level_gate_costs[self.player].value == 0:
+ if self.options.level_gate_costs.value == 0:
gate_cost_mult = 0.6
- elif self.multiworld.level_gate_costs[self.player].value == 1:
+ elif self.options.level_gate_costs.value == 1:
gate_cost_mult = 0.8
shuffled_region_list = list(range(30))
@@ -253,8 +256,8 @@ def create_regions(self):
total_levels_added += 1
if levels_added_to_gate >= levels_per_gate[current_gate]:
current_gate += 1
- if current_gate > self.multiworld.number_of_level_gates[self.player].value:
- current_gate = self.multiworld.number_of_level_gates[self.player].value
+ if current_gate > self.options.number_of_level_gates.value:
+ current_gate = self.options.number_of_level_gates.value
else:
current_gate_emblems = max(
math.floor(total_emblem_count * math.pow(total_levels_added / 30.0, 2.0) * gate_cost_mult), current_gate)
@@ -266,38 +269,70 @@ def create_regions(self):
first_cannons_core_mission, final_cannons_core_mission = get_first_and_last_cannons_core_missions(self.mission_map, self.mission_count_map)
- connect_regions(self.multiworld, self.player, gates, self.emblems_for_cannons_core, self.gate_bosses, self.boss_rush_map, first_cannons_core_mission, final_cannons_core_mission)
+ connect_regions(self.multiworld, self, self.player, gates, self.emblems_for_cannons_core, self.gate_bosses, self.boss_rush_map, first_cannons_core_mission, final_cannons_core_mission)
max_required_emblems = max(max(emblem_requirement_list), self.emblems_for_cannons_core)
itempool += [self.create_item(ItemName.emblem) for _ in range(max_required_emblems)]
non_required_emblems = (total_emblem_count - max_required_emblems)
- junk_count = math.floor(non_required_emblems * (self.multiworld.junk_fill_percentage[self.player].value / 100.0))
+ junk_count = math.floor(non_required_emblems * (self.options.junk_fill_percentage.value / 100.0))
itempool += [self.create_item(ItemName.emblem, True) for _ in range(non_required_emblems - junk_count)]
# Carve Traps out of junk_count
trap_weights = []
- trap_weights += ([ItemName.omochao_trap] * self.multiworld.omochao_trap_weight[self.player].value)
- trap_weights += ([ItemName.timestop_trap] * self.multiworld.timestop_trap_weight[self.player].value)
- trap_weights += ([ItemName.confuse_trap] * self.multiworld.confusion_trap_weight[self.player].value)
- trap_weights += ([ItemName.tiny_trap] * self.multiworld.tiny_trap_weight[self.player].value)
- trap_weights += ([ItemName.gravity_trap] * self.multiworld.gravity_trap_weight[self.player].value)
- trap_weights += ([ItemName.exposition_trap] * self.multiworld.exposition_trap_weight[self.player].value)
- #trap_weights += ([ItemName.darkness_trap] * self.multiworld.darkness_trap_weight[self.player].value)
- trap_weights += ([ItemName.ice_trap] * self.multiworld.ice_trap_weight[self.player].value)
- trap_weights += ([ItemName.slow_trap] * self.multiworld.slow_trap_weight[self.player].value)
- trap_weights += ([ItemName.cutscene_trap] * self.multiworld.cutscene_trap_weight[self.player].value)
- trap_weights += ([ItemName.pong_trap] * self.multiworld.pong_trap_weight[self.player].value)
+ trap_weights += ([ItemName.omochao_trap] * self.options.omochao_trap_weight.value)
+ trap_weights += ([ItemName.timestop_trap] * self.options.timestop_trap_weight.value)
+ trap_weights += ([ItemName.confuse_trap] * self.options.confusion_trap_weight.value)
+ trap_weights += ([ItemName.tiny_trap] * self.options.tiny_trap_weight.value)
+ trap_weights += ([ItemName.gravity_trap] * self.options.gravity_trap_weight.value)
+ trap_weights += ([ItemName.exposition_trap] * self.options.exposition_trap_weight.value)
+ #trap_weights += ([ItemName.darkness_trap] * self.options.darkness_trap_weight.value)
+ trap_weights += ([ItemName.ice_trap] * self.options.ice_trap_weight.value)
+ trap_weights += ([ItemName.slow_trap] * self.options.slow_trap_weight.value)
+ trap_weights += ([ItemName.cutscene_trap] * self.options.cutscene_trap_weight.value)
+ trap_weights += ([ItemName.reverse_trap] * self.options.reverse_trap_weight.value)
+ trap_weights += ([ItemName.pong_trap] * self.options.pong_trap_weight.value)
junk_count += extra_junk_count
- trap_count = 0 if (len(trap_weights) == 0) else math.ceil(junk_count * (self.multiworld.trap_fill_percentage[self.player].value / 100.0))
+ trap_count = 0 if (len(trap_weights) == 0) else math.ceil(junk_count * (self.options.trap_fill_percentage.value / 100.0))
junk_count -= trap_count
+ chao_active = self.any_chao_locations_active()
junk_pool = []
junk_keys = list(junk_table.keys())
+
+ # Chao Junk
+ if chao_active:
+ junk_keys += list(chaos_drives_table.keys())
+ eggs_keys = list(eggs_table.keys())
+ fruits_keys = list(fruits_table.keys())
+ seeds_keys = list(seeds_table.keys())
+ hats_keys = list(hats_table.keys())
+ eggs_count = 0
+ seeds_count = 0
+ hats_count = 0
+
for i in range(junk_count):
- junk_item = self.multiworld.random.choice(junk_keys)
- junk_pool.append(self.create_item(junk_item))
+ junk_type = self.random.randint(0, len(junk_keys) + 3)
+
+ if chao_active and junk_type == len(junk_keys) + 0 and eggs_count < 20:
+ junk_item = self.multiworld.random.choice(eggs_keys)
+ junk_pool.append(self.create_item(junk_item))
+ eggs_count += 1
+ elif chao_active and junk_type == len(junk_keys) + 1:
+ junk_item = self.multiworld.random.choice(fruits_keys)
+ junk_pool.append(self.create_item(junk_item))
+ elif chao_active and junk_type == len(junk_keys) + 2 and seeds_count < 12:
+ junk_item = self.multiworld.random.choice(seeds_keys)
+ junk_pool.append(self.create_item(junk_item))
+ seeds_count += 1
+ elif chao_active and junk_type == len(junk_keys) + 3 and hats_count < 20:
+ junk_item = self.multiworld.random.choice(hats_keys)
+ junk_pool.append(self.create_item(junk_item))
+ hats_count += 1
+ else:
+ junk_item = self.multiworld.random.choice(junk_keys)
+ junk_pool.append(self.create_item(junk_item))
itempool += junk_pool
@@ -310,95 +345,6 @@ def create_regions(self):
self.multiworld.itempool += itempool
- # Music Shuffle
- if self.multiworld.music_shuffle[self.player] == "levels":
- musiclist_o = list(range(0, 47))
- musiclist_s = musiclist_o.copy()
- self.multiworld.random.shuffle(musiclist_s)
- musiclist_o.extend(range(47, 78))
- musiclist_s.extend(range(47, 78))
-
- if self.multiworld.sadx_music[self.player].value == 1:
- musiclist_s = [x+100 for x in musiclist_s]
- elif self.multiworld.sadx_music[self.player].value == 2:
- for i in range(len(musiclist_s)):
- if self.multiworld.random.randint(0,1):
- musiclist_s[i] += 100
-
- self.music_map = dict(zip(musiclist_o, musiclist_s))
- elif self.multiworld.music_shuffle[self.player] == "full":
- musiclist_o = list(range(0, 78))
- musiclist_s = musiclist_o.copy()
- self.multiworld.random.shuffle(musiclist_s)
-
- if self.multiworld.sadx_music[self.player].value == 1:
- musiclist_s = [x+100 for x in musiclist_s]
- elif self.multiworld.sadx_music[self.player].value == 2:
- for i in range(len(musiclist_s)):
- if self.multiworld.random.randint(0,1):
- musiclist_s[i] += 100
-
- self.music_map = dict(zip(musiclist_o, musiclist_s))
- elif self.multiworld.music_shuffle[self.player] == "singularity":
- musiclist_o = list(range(0, 78))
- musiclist_s = [self.multiworld.random.choice(musiclist_o)] * len(musiclist_o)
-
- if self.multiworld.sadx_music[self.player].value == 1:
- musiclist_s = [x+100 for x in musiclist_s]
- elif self.multiworld.sadx_music[self.player].value == 2:
- if self.multiworld.random.randint(0,1):
- musiclist_s = [x+100 for x in musiclist_s]
-
- self.music_map = dict(zip(musiclist_o, musiclist_s))
- else:
- musiclist_o = list(range(0, 78))
- musiclist_s = musiclist_o.copy()
-
- if self.multiworld.sadx_music[self.player].value == 1:
- musiclist_s = [x+100 for x in musiclist_s]
- elif self.multiworld.sadx_music[self.player].value == 2:
- for i in range(len(musiclist_s)):
- if self.multiworld.random.randint(0,1):
- musiclist_s[i] += 100
-
- self.music_map = dict(zip(musiclist_o, musiclist_s))
-
- # Voice Shuffle
- if self.multiworld.voice_shuffle[self.player] == "shuffled":
- voicelist_o = list(range(0, 2623))
- voicelist_s = voicelist_o.copy()
- self.multiworld.random.shuffle(voicelist_s)
-
- self.voice_map = dict(zip(voicelist_o, voicelist_s))
- elif self.multiworld.voice_shuffle[self.player] == "rude":
- voicelist_o = list(range(0, 2623))
- voicelist_s = voicelist_o.copy()
- self.multiworld.random.shuffle(voicelist_s)
-
- for i in range(len(voicelist_s)):
- if self.multiworld.random.randint(1,100) > 80:
- voicelist_s[i] = 17
-
- self.voice_map = dict(zip(voicelist_o, voicelist_s))
- elif self.multiworld.voice_shuffle[self.player] == "chao":
- voicelist_o = list(range(0, 2623))
- voicelist_s = voicelist_o.copy()
- self.multiworld.random.shuffle(voicelist_s)
-
- for i in range(len(voicelist_s)):
- voicelist_s[i] = self.multiworld.random.choice(range(2586, 2608))
-
- self.voice_map = dict(zip(voicelist_o, voicelist_s))
- elif self.multiworld.voice_shuffle[self.player] == "singularity":
- voicelist_o = list(range(0, 2623))
- voicelist_s = [self.multiworld.random.choice(voicelist_o)] * len(voicelist_o)
-
- self.voice_map = dict(zip(voicelist_o, voicelist_s))
- else:
- voicelist_o = list(range(0, 2623))
- voicelist_s = voicelist_o.copy()
-
- self.voice_map = dict(zip(voicelist_o, voicelist_s))
def create_item(self, name: str, force_non_progression=False, goal=0) -> Item:
@@ -422,26 +368,32 @@ def create_item(self, name: str, force_non_progression=False, goal=0) -> Item:
return created_item
def get_filler_item_name(self) -> str:
- return self.multiworld.random.choice(list(junk_table.keys()))
+ junk_keys = list(junk_table.keys())
+
+ # Chao Junk
+ if self.any_chao_locations_active():
+ junk_keys += list(chaos_drives_table.keys())
+
+ return self.multiworld.random.choice(junk_keys)
def set_rules(self):
- set_rules(self.multiworld, self.player, self.gate_bosses, self.boss_rush_map, self.mission_map, self.mission_count_map)
+ set_rules(self.multiworld, self, self.player, self.gate_bosses, self.boss_rush_map, self.mission_map, self.mission_count_map, self.black_market_costs)
def write_spoiler(self, spoiler_handle: typing.TextIO):
- if self.multiworld.number_of_level_gates[self.player].value > 0 or self.multiworld.goal[self.player].value in [4, 5, 6]:
+ if self.options.number_of_level_gates.value > 0 or self.options.goal.value in [4, 5, 6]:
spoiler_handle.write("\n")
header_text = "Sonic Adventure 2 Bosses for {}:\n"
header_text = header_text.format(self.multiworld.player_name[self.player])
spoiler_handle.write(header_text)
- if self.multiworld.number_of_level_gates[self.player].value > 0:
+ if self.options.number_of_level_gates.value > 0:
for x in range(len(self.gate_bosses.values())):
text = "Gate {0} Boss: {1}\n"
text = text.format((x + 1), get_boss_name(self.gate_bosses[x + 1]))
spoiler_handle.writelines(text)
spoiler_handle.write("\n")
- if self.multiworld.goal[self.player].value in [4, 5, 6]:
+ if self.options.goal.value in [4, 5, 6]:
for x in range(len(self.boss_rush_map.values())):
text = "Boss Rush Boss {0}: {1}\n"
text = text.format((x + 1), get_boss_name(self.boss_rush_map[x]))
@@ -459,12 +411,21 @@ def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, s
]
no_hint_region_names = [
LocationName.cannon_core_region,
- LocationName.chao_garden_beginner_region,
- LocationName.chao_garden_intermediate_region,
- LocationName.chao_garden_expert_region,
+ LocationName.chao_race_beginner_region,
+ LocationName.chao_race_intermediate_region,
+ LocationName.chao_race_expert_region,
+ LocationName.chao_karate_beginner_region,
+ LocationName.chao_karate_intermediate_region,
+ LocationName.chao_karate_expert_region,
+ LocationName.chao_karate_super_region,
+ LocationName.kart_race_beginner_region,
+ LocationName.kart_race_standard_region,
+ LocationName.kart_race_expert_region,
+ LocationName.chao_kindergarten_region,
+ LocationName.black_market_region,
]
er_hint_data = {}
- for i in range(self.multiworld.number_of_level_gates[self.player].value + 1):
+ for i in range(self.options.number_of_level_gates.value + 1):
gate_name = gate_names[i]
gate_region = self.multiworld.get_region(gate_name, self.player)
if not gate_region:
@@ -476,10 +437,353 @@ def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, s
for location in level_region.locations:
er_hint_data[location.address] = gate_name
+ for i in range(self.options.black_market_slots.value):
+ location = self.multiworld.get_location(LocationName.chao_black_market_base + str(i + 1), self.player)
+ er_hint_data[location.address] = str(self.black_market_costs[i]) + " " + str(ItemName.market_token)
+
+
hint_data[self.player] = er_hint_data
@classmethod
- def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
- if world.get_game_players("Sonic Adventure 2 Battle"):
+ def stage_fill_hook(cls, multiworld: MultiWorld, progitempool, usefulitempool, filleritempool, fill_locations):
+ if multiworld.get_game_players("Sonic Adventure 2 Battle"):
progitempool.sort(
key=lambda item: 0 if (item.name != 'Emblem') else 1)
+
+ def get_levels_per_gate(self) -> list:
+ levels_per_gate = list()
+ max_gate_index = self.options.number_of_level_gates
+ average_level_count = 30 / (max_gate_index + 1)
+ levels_added = 0
+
+ for i in range(max_gate_index + 1):
+ levels_per_gate.append(average_level_count)
+ levels_added += average_level_count
+ additional_count_iterator = 0
+ while levels_added < 30:
+ levels_per_gate[additional_count_iterator] += 1
+ levels_added += 1
+ additional_count_iterator += 1 if additional_count_iterator < max_gate_index else -max_gate_index
+
+ if self.options.level_gate_distribution == 0 or self.options.level_gate_distribution == 2:
+ early_distribution = self.options.level_gate_distribution == 0
+ levels_to_distribute = 5
+ gate_index_offset = 0
+ while levels_to_distribute > 0:
+ if levels_per_gate[0 + gate_index_offset] == 1 or \
+ levels_per_gate[max_gate_index - gate_index_offset] == 1:
+ break
+ if early_distribution:
+ levels_per_gate[0 + gate_index_offset] += 1
+ levels_per_gate[max_gate_index - gate_index_offset] -= 1
+ else:
+ levels_per_gate[0 + gate_index_offset] -= 1
+ levels_per_gate[max_gate_index - gate_index_offset] += 1
+ gate_index_offset += 1
+ if gate_index_offset > math.floor(max_gate_index / 2):
+ gate_index_offset = 0
+ levels_to_distribute -= 1
+
+ return levels_per_gate
+
+ def any_chao_locations_active(self) -> bool:
+ if self.options.chao_race_difficulty.value > 0 or \
+ self.options.chao_karate_difficulty.value > 0 or \
+ self.options.chao_stats.value > 0 or \
+ self.options.chao_animal_parts or \
+ self.options.chao_kindergarten or \
+ self.options.black_market_slots.value > 0:
+ return True;
+
+ return False
+
+ def generate_music_data(self) -> typing.Dict[int, int]:
+ if self.options.music_shuffle == "levels":
+ musiclist_o = list(range(0, 47))
+ musiclist_s = musiclist_o.copy()
+ self.random.shuffle(musiclist_s)
+ musiclist_o.extend(range(47, 78))
+ musiclist_s.extend(range(47, 78))
+
+ if self.options.sadx_music.value == 1:
+ musiclist_s = [x+100 for x in musiclist_s]
+ elif self.options.sadx_music.value == 2:
+ for i in range(len(musiclist_s)):
+ if self.random.randint(0,1):
+ musiclist_s[i] += 100
+
+ return dict(zip(musiclist_o, musiclist_s))
+ elif self.options.music_shuffle == "full":
+ musiclist_o = list(range(0, 78))
+ musiclist_s = musiclist_o.copy()
+ self.random.shuffle(musiclist_s)
+
+ if self.options.sadx_music.value == 1:
+ musiclist_s = [x+100 for x in musiclist_s]
+ elif self.options.sadx_music.value == 2:
+ for i in range(len(musiclist_s)):
+ if self.random.randint(0,1):
+ musiclist_s[i] += 100
+
+ return dict(zip(musiclist_o, musiclist_s))
+ elif self.options.music_shuffle == "singularity":
+ musiclist_o = list(range(0, 78))
+ musiclist_s = [self.random.choice(musiclist_o)] * len(musiclist_o)
+
+ if self.options.sadx_music.value == 1:
+ musiclist_s = [x+100 for x in musiclist_s]
+ elif self.options.sadx_music.value == 2:
+ if self.random.randint(0,1):
+ musiclist_s = [x+100 for x in musiclist_s]
+
+ return dict(zip(musiclist_o, musiclist_s))
+ else:
+ musiclist_o = list(range(0, 78))
+ musiclist_s = musiclist_o.copy()
+
+ if self.options.sadx_music.value == 1:
+ musiclist_s = [x+100 for x in musiclist_s]
+ elif self.options.sadx_music.value == 2:
+ for i in range(len(musiclist_s)):
+ if self.random.randint(0,1):
+ musiclist_s[i] += 100
+
+ return dict(zip(musiclist_o, musiclist_s))
+
+ def generate_voice_data(self) -> typing.Dict[int, int]:
+ if self.options.voice_shuffle == "shuffled":
+ voicelist_o = list(range(0, 2623))
+ voicelist_s = voicelist_o.copy()
+ self.random.shuffle(voicelist_s)
+
+ return dict(zip(voicelist_o, voicelist_s))
+ elif self.options.voice_shuffle == "rude":
+ voicelist_o = list(range(0, 2623))
+ voicelist_s = voicelist_o.copy()
+ self.random.shuffle(voicelist_s)
+
+ for i in range(len(voicelist_s)):
+ if self.random.randint(1,100) > 80:
+ voicelist_s[i] = 17
+
+ return dict(zip(voicelist_o, voicelist_s))
+ elif self.options.voice_shuffle == "chao":
+ voicelist_o = list(range(0, 2623))
+ voicelist_s = voicelist_o.copy()
+ self.random.shuffle(voicelist_s)
+
+ for i in range(len(voicelist_s)):
+ voicelist_s[i] = self.random.choice(range(2586, 2608))
+
+ return dict(zip(voicelist_o, voicelist_s))
+ elif self.options.voice_shuffle == "singularity":
+ voicelist_o = list(range(0, 2623))
+ voicelist_s = [self.random.choice(voicelist_o)] * len(voicelist_o)
+
+ return dict(zip(voicelist_o, voicelist_s))
+ else:
+ voicelist_o = list(range(0, 2623))
+ voicelist_s = voicelist_o.copy()
+
+ return dict(zip(voicelist_o, voicelist_s))
+
+ def generate_chao_egg_data(self) -> typing.Dict[int, int]:
+ if self.options.shuffle_starting_chao_eggs:
+ egglist_o = list(range(0, 4))
+ egglist_s = self.random.sample(range(0,54), 4)
+
+ return dict(zip(egglist_o, egglist_s))
+ else:
+ # Indicate these are not shuffled
+ egglist_o = [0, 1, 2, 3]
+ egglist_s = [255, 255, 255, 255]
+
+ return dict(zip(egglist_o, egglist_s))
+
+ def generate_chao_name_data(self) -> typing.Dict[int, int]:
+ number_of_names = 30
+ name_list_o = list(range(number_of_names * 7))
+ name_list_s = []
+
+ name_list_base = []
+ name_list_copy = list(self.multiworld.player_name.values())
+ name_list_copy.remove(self.multiworld.player_name[self.player])
+
+ if len(name_list_copy) >= number_of_names:
+ name_list_base = self.random.sample(name_list_copy, number_of_names)
+ else:
+ name_list_base = name_list_copy
+ self.random.shuffle(name_list_base)
+
+ name_list_base += self.random.sample(sample_chao_names, number_of_names - len(name_list_base))
+
+ for name in name_list_base:
+ for char_idx in range(7):
+ if char_idx < len(name):
+ name_list_s.append(chao_name_conversion.get(name[char_idx], 0x5F))
+ else:
+ name_list_s.append(0x00)
+
+ return dict(zip(name_list_o, name_list_s))
+
+ def generate_black_market_data(self) -> typing.Dict[int, int]:
+ if self.options.black_market_slots.value == 0:
+ return {}
+
+ ring_costs = [50, 75, 100]
+
+ market_data = {}
+ item_names = []
+ player_names = []
+ progression_flags = []
+ totally_real_item_names_copy = totally_real_item_names.copy()
+ location_names = [(LocationName.chao_black_market_base + str(i)) for i in range(1, self.options.black_market_slots.value + 1)]
+ locations = [self.multiworld.get_location(location_name, self.player) for location_name in location_names]
+ for location in locations:
+ if location.item.classification & ItemClassification.trap:
+ item_name = self.random.choice(totally_real_item_names_copy)
+ totally_real_item_names_copy.remove(item_name)
+ item_names.append(item_name)
+ else:
+ item_names.append(location.item.name)
+ player_names.append(self.multiworld.player_name[location.item.player])
+
+ if location.item.classification & ItemClassification.progression or location.item.classification & ItemClassification.trap:
+ progression_flags.append(2)
+ elif location.item.classification & ItemClassification.useful:
+ progression_flags.append(1)
+ else:
+ progression_flags.append(0)
+
+ for item_idx in range(self.options.black_market_slots.value):
+ for chr_idx in range(len(item_names[item_idx][:26])):
+ market_data[(item_idx * 46) + chr_idx] = ord(item_names[item_idx][chr_idx])
+ for chr_idx in range(len(player_names[item_idx][:16])):
+ market_data[(item_idx * 46) + 26 + chr_idx] = ord(player_names[item_idx][chr_idx])
+
+ market_data[(item_idx * 46) + 42] = ring_costs[progression_flags[item_idx]] * self.options.black_market_price_multiplier.value
+
+ return market_data
+
+ def generate_er_layout(self) -> typing.Dict[int, int]:
+ if not self.options.chao_entrance_randomization:
+ return {}
+
+ er_layout = {}
+
+ start_exit = self.random.randint(0, 3)
+ accessible_rooms = []
+
+ multi_rooms_copy = multi_rooms.copy()
+ single_rooms_copy = single_rooms.copy()
+ all_exits_copy = all_exits.copy()
+ all_destinations_copy = all_destinations.copy()
+
+ multi_rooms_copy.remove(0x07)
+ accessible_rooms.append(0x07)
+
+ # Place Kindergarten somewhere sane
+ exit_choice = self.random.choice(valid_kindergarten_exits)
+ exit_room = exit_to_room_map[exit_choice]
+ all_exits_copy.remove(exit_choice)
+ multi_rooms_copy.remove(exit_room)
+
+ destination = 0x06
+ single_rooms_copy.remove(destination)
+ all_destinations_copy.remove(destination)
+
+ er_layout[exit_choice] = destination
+
+ reverse_exit = self.random.choice(room_to_exits_map[destination])
+
+ er_layout[reverse_exit] = exit_to_room_map[exit_choice]
+
+ all_exits_copy.remove(reverse_exit)
+ all_destinations_copy.remove(exit_room)
+
+ # Connect multi-exit rooms
+ loop_guard = 0
+ while len(multi_rooms_copy) > 0:
+ loop_guard += 1
+ if loop_guard > 2000:
+ logging.warning(f"Failed to generate Chao Entrance Randomization for player: {self.multiworld.player_name[self.player]}")
+ return {}
+
+ exit_room = self.random.choice(accessible_rooms)
+ possible_exits = [exit for exit in room_to_exits_map[exit_room] if exit in all_exits_copy]
+ if len(possible_exits) == 0:
+ continue
+ exit_choice = self.random.choice(possible_exits)
+ all_exits_copy.remove(exit_choice)
+
+ destination = self.random.choice(multi_rooms_copy)
+ multi_rooms_copy.remove(destination)
+ all_destinations_copy.remove(destination)
+ accessible_rooms.append(destination)
+
+ er_layout[exit_choice] = destination
+
+ reverse_exit = self.random.choice(room_to_exits_map[destination])
+
+ er_layout[reverse_exit] = exit_room
+
+ all_exits_copy.remove(reverse_exit)
+ all_destinations_copy.remove(exit_room)
+
+ # Connect dead-end rooms
+ loop_guard = 0
+ while len(single_rooms_copy) > 0:
+ loop_guard += 1
+ if loop_guard > 2000:
+ logging.warning(f"Failed to generate Chao Entrance Randomization for player: {self.multiworld.player_name[self.player]}")
+ return {}
+
+ exit_room = self.random.choice(accessible_rooms)
+ possible_exits = [exit for exit in room_to_exits_map[exit_room] if exit in all_exits_copy]
+ if len(possible_exits) == 0:
+ continue
+ exit_choice = self.random.choice(possible_exits)
+ all_exits_copy.remove(exit_choice)
+
+ destination = self.random.choice(single_rooms_copy)
+ single_rooms_copy.remove(destination)
+ all_destinations_copy.remove(destination)
+
+ er_layout[exit_choice] = destination
+
+ reverse_exit = self.random.choice(room_to_exits_map[destination])
+
+ er_layout[reverse_exit] = exit_room
+
+ all_exits_copy.remove(reverse_exit)
+ all_destinations_copy.remove(exit_room)
+
+ # Connect remaining exits
+ loop_guard = 0
+ while len(all_exits_copy) > 0:
+ loop_guard += 1
+ if loop_guard > 2000:
+ logging.warning(f"Failed to generate Chao Entrance Randomization for player: {self.multiworld.player_name[self.player]}")
+ return {}
+
+ exit_room = self.random.choice(all_destinations_copy)
+ possible_exits = [exit for exit in room_to_exits_map[exit_room] if exit in all_exits_copy]
+ if len(possible_exits) == 0:
+ continue
+ exit_choice = self.random.choice(possible_exits)
+ all_exits_copy.remove(exit_choice)
+ all_destinations_copy.remove(exit_room)
+
+ destination = self.random.choice(all_destinations_copy)
+ all_destinations_copy.remove(destination)
+
+ er_layout[exit_choice] = destination
+
+ possible_reverse_exits = [exit for exit in room_to_exits_map[destination] if exit in all_exits_copy]
+ reverse_exit = self.random.choice(possible_reverse_exits)
+
+ er_layout[reverse_exit] = exit_room
+
+ all_exits_copy.remove(reverse_exit)
+
+ return er_layout
diff --git a/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md b/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md
index 12ccc50ccd2c..e2f732ffe585 100644
--- a/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md
+++ b/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md
@@ -1,8 +1,8 @@
# Sonic Adventure 2: Battle
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
+The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
## What does randomization do to this game?
diff --git a/worlds/sa2b/docs/setup_en.md b/worlds/sa2b/docs/setup_en.md
index b30255ad73b2..f32001a67827 100644
--- a/worlds/sa2b/docs/setup_en.md
+++ b/worlds/sa2b/docs/setup_en.md
@@ -4,8 +4,8 @@
- Sonic Adventure 2: Battle from: [Sonic Adventure 2: Battle Steam Store Page](https://store.steampowered.com/app/213610/Sonic_Adventure_2/)
- The Battle DLC is required if you choose to add Chao Karate locations to the randomizer
-- Sonic Adventure 2 Mod Loader from: [Sonic Retro Mod Loader Page](http://info.sonicretro.org/SA2_Mod_Loader)
-- Microsoft Visual C++ 2013 from: [Microsoft Visual C++ 2013 Redistributable Page](https://www.microsoft.com/en-us/download/details.aspx?id=40784)
+- SA Mod Manager from: [SA Mod Manager GitHub Releases Page](https://github.com/X-Hax/SA-Mod-Manager/releases)
+- .NET Desktop Runtime 7.0 from: [.NET Desktop Runtime 7.0 Download Page](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-7.0.9-windows-x64-installer)
- Archipelago Mod for Sonic Adventure 2: Battle
from: [Sonic Adventure 2: Battle Archipelago Randomizer Mod Releases Page](https://github.com/PoryGone/SA2B_Archipelago/releases/)
@@ -15,6 +15,8 @@
- Sonic Adventure 2: Battle Archipelago PopTracker pack from: [SA2B AP Tracker Releases Page](https://github.com/PoryGone/SA2B_AP_Tracker/releases/)
- Quality of life mods
- SA2 Volume Controls from: [SA2 Volume Controls Release Page] (https://gamebanana.com/mods/381193)
+- Sonic Adventure DX from: [Sonic Adventure DX Steam Store Page](https://store.steampowered.com/app/71250/Sonic_Adventure_DX/)
+ - For setting up the `SADX Music` option (See Additional Options for instructions).
## Installation Procedures (Windows)
@@ -22,15 +24,13 @@
2. Launch the game at least once without mods.
-3. Install Sonic Adventure 2 Mod Loader as per its instructions.
+3. Install SA Mod Manager as per [its instructions](https://github.com/X-Hax/SA-Mod-Manager/tree/master?tab=readme-ov-file).
-4. The folder you installed the Sonic Adventure 2 Mod Loader into will now have a `/mods` directory.
+4. Unpack the Archipelago Mod into the `/mods` directory in the folder into which you installed Sonic Adventure 2: Battle, so that `/mods/SA2B_Archipelago` is a valid path.
-5. Unpack the Archipelago Mod into this folder, so that `/mods/SA2B_Archipelago` is a valid path.
+5. In the SA2B_Archipelago folder, run the `CopyAPCppDLL.bat` script (a window will very quickly pop up and go away).
-6. In the SA2B_Archipelago folder, run the `CopyAPCppDLL.bat` script (a window will very quickly pop up and go away).
-
-7. Launch the `SA2ModManager.exe` and make sure the SA2B_Archipelago mod is listed and enabled.
+6. Launch the `SAModManager.exe` and make sure the SA2B_Archipelago mod is listed and enabled.
## Installation Procedures (Linux and Steam Deck)
@@ -40,21 +40,29 @@
3. Launch the game at least once without mods.
-4. Install Sonic Adventure 2 Mod Loader as per its instructions. To launch it, add ``SA2ModManager.exe`` as a non-Steam game. In the properties on Steam for Sonic Adventure 2 Mod Loader, set it to use Proton as the compatibility tool.
+4. Create both a `/mods` directory and a `/SAManager` directory in the folder into which you installed Sonic Adventure 2: Battle.
+
+5. Install SA Mod Manager as per [its instructions](https://github.com/X-Hax/SA-Mod-Manager/tree/master?tab=readme-ov-file). Specifically, extract SAModManager.exe file to the folder that Sonic Adventure 2: Battle is installed to. To launch it, add ``SAModManager.exe`` as a non-Steam game. In the properties on Steam for SA Mod Manager, set it to use Proton as the compatibility tool.
+
+6. Run SAModManager.exe from Steam once. It should produce an error popup for a missing dependency, close the error.
-5. The folder you installed the Sonic Adventure 2 Mod Loader into will now have a `/mods` directory.
+7. Install protontricks, on the Steam Deck this can be done via the Discover store, on other distros instructions vary, [see its github page](https://github.com/Matoking/protontricks).
+
+8. Download the [.NET 7 Desktop Runtime for x64 Windows](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-7.0.17-windows-x64-installer). If this link does not work, the download can be found on [this page](https://dotnet.microsoft.com/en-us/download/dotnet/7.0).
+
+9. Right click the .NET 7 Desktop Runtime exe, and assuming protontricks was installed correctly, the option to "Open with Protontricks Launcher" should be available. Click that, and in the popup window that opens, select SAModManager.exe. Follow the prompts after this to install the .NET 7 Desktop Runtime for SAModManager. Once it is done, you should be able to successfully launch SAModManager to steam.
6. Unpack the Archipelago Mod into this folder, so that `/mods/SA2B_Archipelago` is a valid path.
-7. In the SA2B_Archipelago folder, copy the `APCpp.dll` file and paste it in the Sonic Adventure 2 install folder (where `SA2ModManager.exe` is).
+7. In the SA2B_Archipelago folder, copy the `APCpp.dll` file and paste it in the Sonic Adventure 2 install folder (where `sonic2app.exe` is).
-8. Launch the `SA2ModManager.exe` from Steam and make sure the SA2B_Archipelago mod is listed and enabled.
+8. Launch `SAModManager.exe` from Steam and make sure the SA2B_Archipelago mod is listed and enabled.
-Note: Ensure that you launch Sonic Adventure 2 from Steam directly on Linux, rather than launching using the `Save & Play` button in Sonic Adventure 2 Mod Loader.
+Note: Ensure that you launch Sonic Adventure 2 from Steam directly on Linux, rather than launching using the `Save & Play` button in SA Mod Manager.
## Joining a MultiWorld Game
-1. Before launching the game, run the `SA2ModManager.exe`, select the SA2B_Archipelago mod, and hit the `Configure...` button.
+1. Before launching the game, run the `SAModManager.exe`, select the SA2B_Archipelago mod, and hit the `Configure Mod` button.
2. For the `Server IP` field under `AP Settings`, enter the address of the server, such as archipelago.gg:38281, your server host should be able to tell you this.
@@ -68,7 +76,7 @@ Note: Ensure that you launch Sonic Adventure 2 from Steam directly on Linux, rat
## Additional Options
-Some additional settings related to the Archipelago messages in game can be adjusted in the SA2ModManager if you select `Configure...` on the SA2B_Archipelago mod. This settings will be under a `General Settings` tab.
+Some additional settings related to the Archipelago messages in game can be adjusted in the SAModManager if you select `Configure Mod` on the SA2B_Archipelago mod. This settings will be under a `General Settings` tab.
- Message Display Count: This is the maximum number of Archipelago messages that can be displayed on screen at any given time.
- Message Display Duration: This dictates how long Archipelago messages are displayed on screen (in seconds).
@@ -92,9 +100,9 @@ If you wish to use the `SADX Music` option of the Randomizer, you must own a cop
- Game is running too fast (Like Sonic).
- Limit framerate using the mod manager:
- 1. Launch `SA2ModManager.exe`.
- 2. Select the `Graphics` tab.
- 3. Check the `Lock framerate` box under the Visuals section.
+ 1. Launch `SAModManager.exe`.
+ 2. Select the `Game Config` tab, then select the `Patches` subtab.
+ 3. Check the `Lock framerate` box under the Patches section.
4. Press the `Save` button.
- If using an NVidia graphics card:
1. Open the NVIDIA Control Panel.
@@ -105,7 +113,7 @@ If you wish to use the `SADX Music` option of the Randomizer, you must own a cop
6. Choose the `On` radial option and in the input box next to the slide enter a value of 60 (or 59 if 60 causes the game to crash).
- Controller input is not working.
- 1. Run the Launcher.exe which should be in the same folder as the SA2ModManager.
+ 1. Run the Launcher.exe which should be in the same folder as the your Sonic Adventure 2: Battle install.
2. Select the `Player` tab and reselect the controller for the player 1 input method.
3. Click the `Save settings and launch SONIC ADVENTURE 2` button. (Any mod manager settings will apply even if the game is launched this way rather than through the mod manager)
@@ -125,10 +133,7 @@ If you wish to use the `SADX Music` option of the Randomizer, you must own a cop
- If you enabled an `SADX Music` option, then most likely the music data was not copied properly into the mod folder (See Additional Options for instructions).
- Mission 1 is missing a texture in the stage select UI.
- - Most likely another mod is conflicting and overwriting the texture pack. It is recommeded to have the SA2B Archipelago mod load last in the mod loader.
-
-- Received Cutscene Traps don't play after beating a level.
- - Make sure you don't have the "`Skip Intro`" option enabled in the mod manager.
+ - Most likely another mod is conflicting and overwriting the texture pack. It is recommeded to have the SA2B Archipelago mod load last in the mod manager.
## Save File Safeguard (Advanced Option)
diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py
new file mode 100644
index 000000000000..bb325ba1da45
--- /dev/null
+++ b/worlds/sc2/Client.py
@@ -0,0 +1,1630 @@
+from __future__ import annotations
+
+import asyncio
+import copy
+import ctypes
+import enum
+import inspect
+import logging
+import multiprocessing
+import os.path
+import re
+import sys
+import tempfile
+import typing
+import queue
+import zipfile
+import io
+import random
+import concurrent.futures
+from pathlib import Path
+
+# CommonClient import first to trigger ModuleUpdater
+from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
+from Utils import init_logging, is_windows, async_start
+from . import ItemNames, Options
+from .ItemGroups import item_name_groups
+from .Options import (
+ MissionOrder, KerriganPrimalStatus, kerrigan_unit_available, KerriganPresence,
+ GameSpeed, GenericUpgradeItems, GenericUpgradeResearch, ColorChoice, GenericUpgradeMissions,
+ LocationInclusion, ExtraLocations, MasteryLocations, ChallengeLocations, VanillaLocations,
+ DisableForcedCamera, SkipCutscenes, GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, RequiredTactics,
+ SpearOfAdunPresence, SpearOfAdunPresentInNoBuild, SpearOfAdunAutonomouslyCastAbilityPresence,
+ SpearOfAdunAutonomouslyCastPresentInNoBuild
+)
+
+
+if __name__ == "__main__":
+ init_logging("SC2Client", exception_logger="Client")
+
+logger = logging.getLogger("Client")
+sc2_logger = logging.getLogger("Starcraft2")
+
+import nest_asyncio
+from worlds._sc2common import bot
+from worlds._sc2common.bot.data import Race
+from worlds._sc2common.bot.main import run_game
+from worlds._sc2common.bot.player import Bot
+from .Items import (lookup_id_to_name, get_full_item_list, ItemData, type_flaggroups, upgrade_numbers,
+ upgrade_numbers_all)
+from .Locations import SC2WOL_LOC_ID_OFFSET, LocationType, SC2HOTS_LOC_ID_OFFSET
+from .MissionTables import (lookup_id_to_mission, SC2Campaign, lookup_name_to_mission,
+ lookup_id_to_campaign, MissionConnection, SC2Mission, campaign_mission_table, SC2Race)
+from .Regions import MissionInfo
+
+import colorama
+from Options import Option
+from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart, add_json_item, add_json_location, add_json_text, JSONTypes
+from MultiServer import mark_raw
+
+pool = concurrent.futures.ThreadPoolExecutor(1)
+loop = asyncio.get_event_loop_policy().new_event_loop()
+nest_asyncio.apply(loop)
+MAX_BONUS: int = 28
+VICTORY_MODULO: int = 100
+
+# GitHub repo where the Map/mod data is hosted for /download_data command
+DATA_REPO_OWNER = "Ziktofel"
+DATA_REPO_NAME = "Archipelago-SC2-data"
+DATA_API_VERSION = "API3"
+
+# Bot controller
+CONTROLLER_HEALTH: int = 38281
+CONTROLLER2_HEALTH: int = 38282
+
+# Games
+STARCRAFT2 = "Starcraft 2"
+STARCRAFT2_WOL = "Starcraft 2 Wings of Liberty"
+
+
+# Data version file path.
+# This file is used to tell if the downloaded data are outdated
+# Associated with /download_data command
+def get_metadata_file() -> str:
+ return os.environ["SC2PATH"] + os.sep + "ArchipelagoSC2Metadata.txt"
+
+
+class ConfigurableOptionType(enum.Enum):
+ INTEGER = enum.auto()
+ ENUM = enum.auto()
+
+class ConfigurableOptionInfo(typing.NamedTuple):
+ name: str
+ variable_name: str
+ option_class: typing.Type[Option]
+ option_type: ConfigurableOptionType = ConfigurableOptionType.ENUM
+ can_break_logic: bool = False
+
+
+class ColouredMessage:
+ def __init__(self, text: str = '') -> None:
+ self.parts: typing.List[dict] = []
+ if text:
+ self(text)
+ def __call__(self, text: str) -> 'ColouredMessage':
+ add_json_text(self.parts, text)
+ return self
+ def coloured(self, text: str, colour: str) -> 'ColouredMessage':
+ add_json_text(self.parts, text, type="color", color=colour)
+ return self
+ def location(self, location_id: int, player_id: int) -> 'ColouredMessage':
+ add_json_location(self.parts, location_id, player_id)
+ return self
+ def item(self, item_id: int, player_id: int, flags: int = 0) -> 'ColouredMessage':
+ add_json_item(self.parts, item_id, player_id, flags)
+ return self
+ def player(self, player_id: int) -> 'ColouredMessage':
+ add_json_text(self.parts, str(player_id), type=JSONTypes.player_id)
+ return self
+ def send(self, ctx: SC2Context) -> None:
+ ctx.on_print_json({"data": self.parts, "cmd": "PrintJSON"})
+
+
+class StarcraftClientProcessor(ClientCommandProcessor):
+ ctx: SC2Context
+
+ def formatted_print(self, text: str) -> None:
+ """Prints with kivy formatting to the GUI, and also prints to command-line and to all logs"""
+ # Note(mm): Bold/underline can help readability, but unfortunately the CommonClient does not filter bold tags from command-line output.
+ # Regardless, using `on_print_json` to get formatted text in the GUI and output in the command-line and in the logs,
+ # without having to branch code from CommonClient
+ self.ctx.on_print_json({"data": [{"text": text}]})
+
+ def _cmd_difficulty(self, difficulty: str = "") -> bool:
+ """Overrides the current difficulty set for the world. Takes the argument casual, normal, hard, or brutal"""
+ options = difficulty.split()
+ num_options = len(options)
+
+ if num_options > 0:
+ difficulty_choice = options[0].lower()
+ if difficulty_choice == "casual":
+ self.ctx.difficulty_override = 0
+ elif difficulty_choice == "normal":
+ self.ctx.difficulty_override = 1
+ elif difficulty_choice == "hard":
+ self.ctx.difficulty_override = 2
+ elif difficulty_choice == "brutal":
+ self.ctx.difficulty_override = 3
+ else:
+ self.output("Unable to parse difficulty '" + options[0] + "'")
+ return False
+
+ self.output("Difficulty set to " + options[0])
+ return True
+
+ else:
+ if self.ctx.difficulty == -1:
+ self.output("Please connect to a seed before checking difficulty.")
+ else:
+ current_difficulty = self.ctx.difficulty
+ if self.ctx.difficulty_override >= 0:
+ current_difficulty = self.ctx.difficulty_override
+ self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][current_difficulty])
+ self.output("To change the difficulty, add the name of the difficulty after the command.")
+ return False
+
+
+ def _cmd_game_speed(self, game_speed: str = "") -> bool:
+ """Overrides the current game speed for the world.
+ Takes the arguments default, slower, slow, normal, fast, faster"""
+ options = game_speed.split()
+ num_options = len(options)
+
+ if num_options > 0:
+ speed_choice = options[0].lower()
+ if speed_choice == "default":
+ self.ctx.game_speed_override = 0
+ elif speed_choice == "slower":
+ self.ctx.game_speed_override = 1
+ elif speed_choice == "slow":
+ self.ctx.game_speed_override = 2
+ elif speed_choice == "normal":
+ self.ctx.game_speed_override = 3
+ elif speed_choice == "fast":
+ self.ctx.game_speed_override = 4
+ elif speed_choice == "faster":
+ self.ctx.game_speed_override = 5
+ else:
+ self.output("Unable to parse game speed '" + options[0] + "'")
+ return False
+
+ self.output("Game speed set to " + options[0])
+ return True
+
+ else:
+ if self.ctx.game_speed == -1:
+ self.output("Please connect to a seed before checking game speed.")
+ else:
+ current_speed = self.ctx.game_speed
+ if self.ctx.game_speed_override >= 0:
+ current_speed = self.ctx.game_speed_override
+ self.output("Current game speed: "
+ + ["Default", "Slower", "Slow", "Normal", "Fast", "Faster"][current_speed])
+ self.output("To change the game speed, add the name of the speed after the command,"
+ " or Default to select based on difficulty.")
+ return False
+
+ @mark_raw
+ def _cmd_received(self, filter_search: str = "") -> bool:
+ """List received items.
+ Pass in a parameter to filter the search by partial item name or exact item group."""
+ # Groups must be matched case-sensitively, so we properly capitalize the search term
+ # eg. "Spear of Adun" over "Spear Of Adun" or "spear of adun"
+ # This fails a lot of item name matches, but those should be found by partial name match
+ formatted_filter_search = " ".join([(part.lower() if len(part) <= 3 else part.lower().capitalize()) for part in filter_search.split()])
+
+ def item_matches_filter(item_name: str) -> bool:
+ # The filter can be an exact group name or a partial item name
+ # Partial item name can be matched case-insensitively
+ if filter_search.lower() in item_name.lower():
+ return True
+ # The search term should already be formatted as a group name
+ if formatted_filter_search in item_name_groups and item_name in item_name_groups[formatted_filter_search]:
+ return True
+ return False
+
+ items = get_full_item_list()
+ categorized_items: typing.Dict[SC2Race, typing.List[int]] = {}
+ parent_to_child: typing.Dict[int, typing.List[int]] = {}
+ items_received: typing.Dict[int, typing.List[NetworkItem]] = {}
+ filter_match_count = 0
+ for item in self.ctx.items_received:
+ items_received.setdefault(item.item, []).append(item)
+ items_received_set = set(items_received)
+ for item_data in items.values():
+ if item_data.parent_item:
+ parent_to_child.setdefault(items[item_data.parent_item].code, []).append(item_data.code)
+ else:
+ categorized_items.setdefault(item_data.race, []).append(item_data.code)
+ for faction in SC2Race:
+ has_printed_faction_title = False
+ def print_faction_title():
+ if not has_printed_faction_title:
+ self.formatted_print(f" [u]{faction.name}[/u] ")
+
+ for item_id in categorized_items[faction]:
+ item_name = self.ctx.item_names.lookup_in_game(item_id)
+ received_child_items = items_received_set.intersection(parent_to_child.get(item_id, []))
+ matching_children = [child for child in received_child_items
+ if item_matches_filter(self.ctx.item_names.lookup_in_game(child))]
+ received_items_of_this_type = items_received.get(item_id, [])
+ item_is_match = item_matches_filter(item_name)
+ if item_is_match or len(matching_children) > 0:
+ # Print found item if it or its children match the filter
+ if item_is_match:
+ filter_match_count += len(received_items_of_this_type)
+ for item in received_items_of_this_type:
+ print_faction_title()
+ has_printed_faction_title = True
+ (ColouredMessage('* ').item(item.item, self.ctx.slot, flags=item.flags)
+ (" from ").location(item.location, self.ctx.slot)
+ (" by ").player(item.player)
+ ).send(self.ctx)
+
+ if received_child_items:
+ # We have this item's children
+ if len(matching_children) == 0:
+ # ...but none of them match the filter
+ continue
+
+ if not received_items_of_this_type:
+ # We didn't receive the item itself
+ print_faction_title()
+ has_printed_faction_title = True
+ ColouredMessage("- ").coloured(item_name, "black")(" - not obtained").send(self.ctx)
+
+ for child_item in matching_children:
+ received_items_of_this_type = items_received.get(child_item, [])
+ for item in received_items_of_this_type:
+ filter_match_count += len(received_items_of_this_type)
+ (ColouredMessage(' * ').item(item.item, self.ctx.slot, flags=item.flags)
+ (" from ").location(item.location, self.ctx.slot)
+ (" by ").player(item.player)
+ ).send(self.ctx)
+
+ non_matching_children = len(received_child_items) - len(matching_children)
+ if non_matching_children > 0:
+ self.formatted_print(f" + {non_matching_children} child items that don't match the filter")
+ if filter_search == "":
+ self.formatted_print(f"[b]Obtained: {len(self.ctx.items_received)} items[/b]")
+ else:
+ self.formatted_print(f"[b]Filter \"{filter_search}\" found {filter_match_count} out of {len(self.ctx.items_received)} obtained items[/b]")
+ return True
+
+ def _cmd_option(self, option_name: str = "", option_value: str = "") -> None:
+ """Sets a Starcraft game option that can be changed after generation. Use "/option list" to see all options."""
+
+ LOGIC_WARNING = f" *Note changing this may result in logically unbeatable games*\n"
+
+ options = (
+ ConfigurableOptionInfo('kerrigan_presence', 'kerrigan_presence', Options.KerriganPresence, can_break_logic=True),
+ ConfigurableOptionInfo('soa_presence', 'spear_of_adun_presence', Options.SpearOfAdunPresence, can_break_logic=True),
+ ConfigurableOptionInfo('soa_in_nobuilds', 'spear_of_adun_present_in_no_build', Options.SpearOfAdunPresentInNoBuild, can_break_logic=True),
+ ConfigurableOptionInfo('control_ally', 'take_over_ai_allies', Options.TakeOverAIAllies, can_break_logic=True),
+ ConfigurableOptionInfo('minerals_per_item', 'minerals_per_item', Options.MineralsPerItem, ConfigurableOptionType.INTEGER),
+ ConfigurableOptionInfo('gas_per_item', 'vespene_per_item', Options.VespenePerItem, ConfigurableOptionType.INTEGER),
+ ConfigurableOptionInfo('supply_per_item', 'starting_supply_per_item', Options.StartingSupplyPerItem, ConfigurableOptionType.INTEGER),
+ ConfigurableOptionInfo('no_forced_camera', 'disable_forced_camera', Options.DisableForcedCamera),
+ ConfigurableOptionInfo('skip_cutscenes', 'skip_cutscenes', Options.SkipCutscenes),
+ )
+
+ WARNING_COLOUR = "salmon"
+ CMD_COLOUR = "slateblue"
+ boolean_option_map = {
+ 'y': 'true', 'yes': 'true', 'n': 'false', 'no': 'false',
+ }
+
+ help_message = ColouredMessage(inspect.cleandoc("""
+ Options
+ --------------------
+ """))('\n')
+ for option in options:
+ option_help_text = inspect.cleandoc(option.option_class.__doc__ or "No description provided.").split('\n', 1)[0]
+ help_message.coloured(option.name, CMD_COLOUR)(": " + " | ".join(option.option_class.options)
+ + f" -- {option_help_text}\n")
+ if option.can_break_logic:
+ help_message.coloured(LOGIC_WARNING, WARNING_COLOUR)
+ help_message("--------------------\nEnter an option without arguments to see its current value.\n")
+
+ if not option_name or option_name == 'list' or option_name == 'help':
+ help_message.send(self.ctx)
+ return
+ for option in options:
+ if option_name == option.name:
+ option_value = boolean_option_map.get(option_value, option_value)
+ if not option_value:
+ pass
+ elif option.option_type == ConfigurableOptionType.ENUM and option_value in option.option_class.options:
+ self.ctx.__dict__[option.variable_name] = option.option_class.options[option_value]
+ elif option.option_type == ConfigurableOptionType.INTEGER:
+ try:
+ self.ctx.__dict__[option.variable_name] = int(option_value, base=0)
+ except:
+ self.output(f"{option_value} is not a valid integer")
+ else:
+ self.output(f"Unknown option value '{option_value}'")
+ ColouredMessage(f"{option.name} is '{option.option_class.get_option_name(self.ctx.__dict__[option.variable_name])}'").send(self.ctx)
+ break
+ else:
+ self.output(f"Unknown option '{option_name}'")
+ help_message.send(self.ctx)
+
+ def _cmd_color(self, faction: str = "", color: str = "") -> None:
+ """Changes the player color for a given faction."""
+ player_colors = [
+ "White", "Red", "Blue", "Teal",
+ "Purple", "Yellow", "Orange", "Green",
+ "LightPink", "Violet", "LightGrey", "DarkGreen",
+ "Brown", "LightGreen", "DarkGrey", "Pink",
+ "Rainbow", "Random", "Default"
+ ]
+ var_names = {
+ 'raynor': 'player_color_raynor',
+ 'kerrigan': 'player_color_zerg',
+ 'primal': 'player_color_zerg_primal',
+ 'protoss': 'player_color_protoss',
+ 'nova': 'player_color_nova',
+ }
+ faction = faction.lower()
+ if not faction:
+ for faction_name, key in var_names.items():
+ self.output(f"Current player color for {faction_name}: {player_colors[self.ctx.__dict__[key]]}")
+ self.output("To change your color, add the faction name and color after the command.")
+ self.output("Available factions: " + ', '.join(var_names))
+ self.output("Available colors: " + ', '.join(player_colors))
+ return
+ elif faction not in var_names:
+ self.output(f"Unknown faction '{faction}'.")
+ self.output("Available factions: " + ', '.join(var_names))
+ return
+ match_colors = [player_color.lower() for player_color in player_colors]
+ if not color:
+ self.output(f"Current player color for {faction}: {player_colors[self.ctx.__dict__[var_names[faction]]]}")
+ self.output("To change this faction's colors, add the name of the color after the command.")
+ self.output("Available colors: " + ', '.join(player_colors))
+ else:
+ if color.lower() not in match_colors:
+ self.output(color + " is not a valid color. Available colors: " + ', '.join(player_colors))
+ return
+ if color.lower() == "random":
+ color = random.choice(player_colors[:16])
+ self.ctx.__dict__[var_names[faction]] = match_colors.index(color.lower())
+ self.ctx.pending_color_update = True
+ self.output(f"Color for {faction} set to " + player_colors[self.ctx.__dict__[var_names[faction]]])
+
+ def _cmd_disable_mission_check(self) -> bool:
+ """Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play
+ the next mission in a chain the other player is doing."""
+ self.ctx.missions_unlocked = True
+ sc2_logger.info("Mission check has been disabled")
+ return True
+
+ def _cmd_play(self, mission_id: str = "") -> bool:
+ """Start a Starcraft 2 mission"""
+
+ options = mission_id.split()
+ num_options = len(options)
+
+ if num_options > 0:
+ mission_number = int(options[0])
+
+ self.ctx.play_mission(mission_number)
+
+ else:
+ sc2_logger.info(
+ "Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.")
+ return False
+
+ return True
+
+ def _cmd_available(self) -> bool:
+ """Get what missions are currently available to play"""
+
+ request_available_missions(self.ctx)
+ return True
+
+ def _cmd_unfinished(self) -> bool:
+ """Get what missions are currently available to play and have not had all locations checked"""
+
+ request_unfinished_missions(self.ctx)
+ return True
+
+ @mark_raw
+ def _cmd_set_path(self, path: str = '') -> bool:
+ """Manually set the SC2 install directory (if the automatic detection fails)."""
+ if path:
+ os.environ["SC2PATH"] = path
+ is_mod_installed_correctly()
+ return True
+ else:
+ sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.")
+ return False
+
+ def _cmd_download_data(self) -> bool:
+ """Download the most recent release of the necessary files for playing SC2 with
+ Archipelago. Will overwrite existing files."""
+ pool.submit(self._download_data)
+ return True
+
+ @staticmethod
+ def _download_data() -> bool:
+ if "SC2PATH" not in os.environ:
+ check_game_install_path()
+
+ if os.path.exists(get_metadata_file()):
+ with open(get_metadata_file(), "r") as f:
+ metadata = f.read()
+ else:
+ metadata = None
+
+ tempzip, metadata = download_latest_release_zip(
+ DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION, metadata=metadata, force_download=True)
+
+ if tempzip:
+ try:
+ zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"])
+ sc2_logger.info(f"Download complete. Package installed.")
+ if metadata is not None:
+ with open(get_metadata_file(), "w") as f:
+ f.write(metadata)
+ finally:
+ os.remove(tempzip)
+ else:
+ sc2_logger.warning("Download aborted/failed. Read the log for more information.")
+ return False
+ return True
+
+
+class SC2JSONtoTextParser(JSONtoTextParser):
+ def __init__(self, ctx) -> None:
+ self.handlers = {
+ "ItemSend": self._handle_color,
+ "ItemCheat": self._handle_color,
+ "Hint": self._handle_color,
+ }
+ super().__init__(ctx)
+
+ def _handle_color(self, node: JSONMessagePart) -> str:
+ codes = node["color"].split(";")
+ buffer = "".join(self.color_code(code) for code in codes if code in self.color_codes)
+ return buffer + self._handle_text(node) + ''
+
+ def color_code(self, code: str) -> str:
+ return ''
+
+
+class SC2Context(CommonContext):
+ command_processor = StarcraftClientProcessor
+ game = STARCRAFT2
+ items_handling = 0b111
+
+ def __init__(self, *args, **kwargs) -> None:
+ super(SC2Context, self).__init__(*args, **kwargs)
+ self.raw_text_parser = SC2JSONtoTextParser(self)
+
+ self.difficulty = -1
+ self.game_speed = -1
+ self.disable_forced_camera = 0
+ self.skip_cutscenes = 0
+ self.all_in_choice = 0
+ self.mission_order = 0
+ self.player_color_raynor = ColorChoice.option_blue
+ self.player_color_zerg = ColorChoice.option_orange
+ self.player_color_zerg_primal = ColorChoice.option_purple
+ self.player_color_protoss = ColorChoice.option_blue
+ self.player_color_nova = ColorChoice.option_dark_grey
+ self.pending_color_update = False
+ self.kerrigan_presence = 0
+ self.kerrigan_primal_status = 0
+ self.levels_per_check = 0
+ self.checks_per_level = 1
+ self.mission_req_table: typing.Dict[SC2Campaign, typing.Dict[str, MissionInfo]] = {}
+ self.final_mission: int = 29
+ self.announcements: queue.Queue = queue.Queue()
+ self.sc2_run_task: typing.Optional[asyncio.Task] = None
+ self.missions_unlocked: bool = False # allow launching missions ignoring requirements
+ self.generic_upgrade_missions = 0
+ self.generic_upgrade_research = 0
+ self.generic_upgrade_items = 0
+ self.location_inclusions: typing.Dict[LocationType, int] = {}
+ self.plando_locations: typing.List[str] = []
+ self.current_tooltip = None
+ self.last_loc_list = None
+ self.difficulty_override = -1
+ self.game_speed_override = -1
+ self.mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {}
+ self.last_bot: typing.Optional[ArchipelagoBot] = None
+ self.slot_data_version = 2
+ self.grant_story_tech = 0
+ self.required_tactics = RequiredTactics.option_standard
+ self.take_over_ai_allies = TakeOverAIAllies.option_false
+ self.spear_of_adun_presence = SpearOfAdunPresence.option_not_present
+ self.spear_of_adun_present_in_no_build = SpearOfAdunPresentInNoBuild.option_false
+ self.spear_of_adun_autonomously_cast_ability_presence = SpearOfAdunAutonomouslyCastAbilityPresence.option_not_present
+ self.spear_of_adun_autonomously_cast_present_in_no_build = SpearOfAdunAutonomouslyCastPresentInNoBuild.option_false
+ self.minerals_per_item = 15
+ self.vespene_per_item = 15
+ self.starting_supply_per_item = 2
+ self.nova_covert_ops_only = False
+ self.kerrigan_levels_per_mission_completed = 0
+
+ async def server_auth(self, password_requested: bool = False) -> None:
+ self.game = STARCRAFT2
+ if password_requested and not self.password:
+ await super(SC2Context, self).server_auth(password_requested)
+ await self.get_username()
+ await self.send_connect()
+ if self.ui:
+ self.ui.first_check = True
+
+ def is_legacy_game(self):
+ return self.game == STARCRAFT2_WOL
+
+ def event_invalid_game(self):
+ if self.is_legacy_game():
+ self.game = STARCRAFT2
+ super().event_invalid_game()
+ else:
+ self.game = STARCRAFT2_WOL
+ async_start(self.send_connect())
+
+ def on_package(self, cmd: str, args: dict) -> None:
+ if cmd == "Connected":
+ self.difficulty = args["slot_data"]["game_difficulty"]
+ self.game_speed = args["slot_data"].get("game_speed", GameSpeed.option_default)
+ self.disable_forced_camera = args["slot_data"].get("disable_forced_camera", DisableForcedCamera.default)
+ self.skip_cutscenes = args["slot_data"].get("skip_cutscenes", SkipCutscenes.default)
+ self.all_in_choice = args["slot_data"]["all_in_map"]
+ self.slot_data_version = args["slot_data"].get("version", 2)
+ slot_req_table: dict = args["slot_data"]["mission_req"]
+
+ first_item = list(slot_req_table.keys())[0]
+ # Maintaining backwards compatibility with older slot data
+ if first_item in [str(campaign.id) for campaign in SC2Campaign]:
+ # Multi-campaign
+ self.mission_req_table = {}
+ for campaign_id in slot_req_table:
+ campaign = lookup_id_to_campaign[int(campaign_id)]
+ self.mission_req_table[campaign] = {
+ mission: self.parse_mission_info(mission_info)
+ for mission, mission_info in slot_req_table[campaign_id].items()
+ }
+ else:
+ # Old format
+ self.mission_req_table = {SC2Campaign.GLOBAL: {
+ mission: self.parse_mission_info(mission_info)
+ for mission, mission_info in slot_req_table.items()
+ }
+ }
+
+ self.mission_order = args["slot_data"].get("mission_order", MissionOrder.option_vanilla)
+ self.final_mission = args["slot_data"].get("final_mission", SC2Mission.ALL_IN.id)
+ self.player_color_raynor = args["slot_data"].get("player_color_terran_raynor", ColorChoice.option_blue)
+ self.player_color_zerg = args["slot_data"].get("player_color_zerg", ColorChoice.option_orange)
+ self.player_color_zerg_primal = args["slot_data"].get("player_color_zerg_primal", ColorChoice.option_purple)
+ self.player_color_protoss = args["slot_data"].get("player_color_protoss", ColorChoice.option_blue)
+ self.player_color_nova = args["slot_data"].get("player_color_nova", ColorChoice.option_dark_grey)
+ self.generic_upgrade_missions = args["slot_data"].get("generic_upgrade_missions", GenericUpgradeMissions.default)
+ self.generic_upgrade_items = args["slot_data"].get("generic_upgrade_items", GenericUpgradeItems.option_individual_items)
+ self.generic_upgrade_research = args["slot_data"].get("generic_upgrade_research", GenericUpgradeResearch.option_vanilla)
+ self.kerrigan_presence = args["slot_data"].get("kerrigan_presence", KerriganPresence.option_vanilla)
+ self.kerrigan_primal_status = args["slot_data"].get("kerrigan_primal_status", KerriganPrimalStatus.option_vanilla)
+ self.kerrigan_levels_per_mission_completed = args["slot_data"].get("kerrigan_levels_per_mission_completed", 0)
+ self.kerrigan_levels_per_mission_completed_cap = args["slot_data"].get("kerrigan_levels_per_mission_completed_cap", -1)
+ self.kerrigan_total_level_cap = args["slot_data"].get("kerrigan_total_level_cap", -1)
+ self.grant_story_tech = args["slot_data"].get("grant_story_tech", GrantStoryTech.option_false)
+ self.grant_story_levels = args["slot_data"].get("grant_story_levels", GrantStoryLevels.option_additive)
+ self.required_tactics = args["slot_data"].get("required_tactics", RequiredTactics.option_standard)
+ self.take_over_ai_allies = args["slot_data"].get("take_over_ai_allies", TakeOverAIAllies.option_false)
+ self.spear_of_adun_presence = args["slot_data"].get("spear_of_adun_presence", SpearOfAdunPresence.option_not_present)
+ self.spear_of_adun_present_in_no_build = args["slot_data"].get("spear_of_adun_present_in_no_build", SpearOfAdunPresentInNoBuild.option_false)
+ self.spear_of_adun_autonomously_cast_ability_presence = args["slot_data"].get("spear_of_adun_autonomously_cast_ability_presence", SpearOfAdunAutonomouslyCastAbilityPresence.option_not_present)
+ self.spear_of_adun_autonomously_cast_present_in_no_build = args["slot_data"].get("spear_of_adun_autonomously_cast_present_in_no_build", SpearOfAdunAutonomouslyCastPresentInNoBuild.option_false)
+ self.minerals_per_item = args["slot_data"].get("minerals_per_item", 15)
+ self.vespene_per_item = args["slot_data"].get("vespene_per_item", 15)
+ self.starting_supply_per_item = args["slot_data"].get("starting_supply_per_item", 2)
+ self.nova_covert_ops_only = args["slot_data"].get("nova_covert_ops_only", False)
+
+ if self.required_tactics == RequiredTactics.option_no_logic:
+ # Locking Grant Story Tech/Levels if no logic
+ self.grant_story_tech = GrantStoryTech.option_true
+ self.grant_story_levels = GrantStoryLevels.option_minimum
+
+ self.location_inclusions = {
+ LocationType.VICTORY: LocationInclusion.option_enabled, # Victory checks are always enabled
+ LocationType.VANILLA: args["slot_data"].get("vanilla_locations", VanillaLocations.default),
+ LocationType.EXTRA: args["slot_data"].get("extra_locations", ExtraLocations.default),
+ LocationType.CHALLENGE: args["slot_data"].get("challenge_locations", ChallengeLocations.default),
+ LocationType.MASTERY: args["slot_data"].get("mastery_locations", MasteryLocations.default),
+ }
+ self.plando_locations = args["slot_data"].get("plando_locations", [])
+
+ self.build_location_to_mission_mapping()
+
+ # Looks for the required maps and mods for SC2. Runs check_game_install_path.
+ maps_present = is_mod_installed_correctly()
+ if os.path.exists(get_metadata_file()):
+ with open(get_metadata_file(), "r") as f:
+ current_ver = f.read()
+ sc2_logger.debug(f"Current version: {current_ver}")
+ if is_mod_update_available(DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION, current_ver):
+ sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.")
+ elif maps_present:
+ sc2_logger.warning("NOTICE: Your map files may be outdated (version number not found). "
+ "Run /download_data to update them.")
+
+ @staticmethod
+ def parse_mission_info(mission_info: dict[str, typing.Any]) -> MissionInfo:
+ if mission_info.get("id") is not None:
+ mission_info["mission"] = lookup_id_to_mission[mission_info["id"]]
+ elif isinstance(mission_info["mission"], int):
+ mission_info["mission"] = lookup_id_to_mission[mission_info["mission"]]
+
+ return MissionInfo(
+ **{field: value for field, value in mission_info.items() if field in MissionInfo._fields}
+ )
+
+ def find_campaign(self, mission_name: str) -> SC2Campaign:
+ data = self.mission_req_table
+ for campaign in data.keys():
+ if mission_name in data[campaign].keys():
+ return campaign
+ sc2_logger.info(f"Attempted to find campaign of unknown mission '{mission_name}'; defaulting to GLOBAL")
+ return SC2Campaign.GLOBAL
+
+
+
+ def on_print_json(self, args: dict) -> None:
+ # goes to this world
+ if "receiving" in args and self.slot_concerns_self(args["receiving"]):
+ relevant = True
+ # found in this world
+ elif "item" in args and self.slot_concerns_self(args["item"].player):
+ relevant = True
+ # not related
+ else:
+ relevant = False
+
+ if relevant:
+ self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"])))
+
+ super(SC2Context, self).on_print_json(args)
+
+ def run_gui(self) -> None:
+ from .ClientGui import start_gui
+ start_gui(self)
+
+
+ async def shutdown(self) -> None:
+ await super(SC2Context, self).shutdown()
+ if self.last_bot:
+ self.last_bot.want_close = True
+ if self.sc2_run_task:
+ self.sc2_run_task.cancel()
+
+ def play_mission(self, mission_id: int) -> bool:
+ if self.missions_unlocked or is_mission_available(self, mission_id):
+ if self.sc2_run_task:
+ if not self.sc2_run_task.done():
+ sc2_logger.warning("Starcraft 2 Client is still running!")
+ self.sc2_run_task.cancel() # doesn't actually close the game, just stops the python task
+ if self.slot is None:
+ sc2_logger.warning("Launching Mission without Archipelago authentication, "
+ "checks will not be registered to server.")
+ self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id),
+ name="Starcraft 2 Launch")
+ return True
+ else:
+ sc2_logger.info(
+ f"{lookup_id_to_mission[mission_id].mission_name} is not currently unlocked. "
+ f"Use /unfinished or /available to see what is available.")
+ return False
+
+ def build_location_to_mission_mapping(self) -> None:
+ mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = {
+ mission_info.mission.id: set() for campaign_mission in self.mission_req_table.values() for mission_info in campaign_mission.values()
+ }
+
+ for loc in self.server_locations:
+ offset = SC2WOL_LOC_ID_OFFSET if loc < SC2HOTS_LOC_ID_OFFSET \
+ else (SC2HOTS_LOC_ID_OFFSET - SC2Mission.ALL_IN.id * VICTORY_MODULO)
+ mission_id, objective = divmod(loc - offset, VICTORY_MODULO)
+ mission_id_to_location_ids[mission_id].add(objective)
+ self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in
+ mission_id_to_location_ids.items()}
+
+ def locations_for_mission(self, mission_name: str):
+ mission = lookup_name_to_mission[mission_name]
+ mission_id: int = mission.id
+ objectives = self.mission_id_to_location_ids[mission_id]
+ for objective in objectives:
+ yield get_location_offset(mission_id) + mission_id * VICTORY_MODULO + objective
+
+
+class CompatItemHolder(typing.NamedTuple):
+ name: str
+ quantity: int = 1
+
+
+async def main():
+ multiprocessing.freeze_support()
+ parser = get_base_parser()
+ parser.add_argument('--name', default=None, help="Slot Name to connect as.")
+ args = parser.parse_args()
+
+ ctx = SC2Context(args.connect, args.password)
+ ctx.auth = args.name
+ if ctx.server_task is None:
+ ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
+
+ if gui_enabled:
+ ctx.run_gui()
+ ctx.run_cli()
+
+ await ctx.exit_event.wait()
+
+ await ctx.shutdown()
+
+# These items must be given to the player if the game is generated on version 2
+API2_TO_API3_COMPAT_ITEMS: typing.Set[CompatItemHolder] = {
+ CompatItemHolder(ItemNames.PHOTON_CANNON),
+ CompatItemHolder(ItemNames.OBSERVER),
+ CompatItemHolder(ItemNames.WARP_HARMONIZATION),
+ CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_GROUND_WEAPON, 3),
+ CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_GROUND_ARMOR, 3),
+ CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_SHIELDS, 3),
+ CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_AIR_WEAPON, 3),
+ CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_AIR_ARMOR, 3),
+ CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE, 3)
+}
+
+
+def compat_item_to_network_items(compat_item: CompatItemHolder) -> typing.List[NetworkItem]:
+ item_id = get_full_item_list()[compat_item.name].code
+ network_item = NetworkItem(item_id, 0, 0, 0)
+ return compat_item.quantity * [network_item]
+
+
+def calculate_items(ctx: SC2Context) -> typing.Dict[SC2Race, typing.List[int]]:
+ items = ctx.items_received.copy()
+ # Items unlocked in API2 by default (Prophecy default items)
+ if ctx.slot_data_version < 3:
+ for compat_item in API2_TO_API3_COMPAT_ITEMS:
+ items.extend(compat_item_to_network_items(compat_item))
+
+ network_item: NetworkItem
+ accumulators: typing.Dict[SC2Race, typing.List[int]] = {race: [0 for _ in type_flaggroups[race]] for race in SC2Race}
+
+ # Protoss Shield grouped item specific logic
+ shields_from_ground_upgrade: int = 0
+ shields_from_air_upgrade: int = 0
+
+ item_list = get_full_item_list()
+ for network_item in items:
+ name: str = lookup_id_to_name[network_item.item]
+ item_data: ItemData = item_list[name]
+
+ # exists exactly once
+ if item_data.quantity == 1:
+ accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] |= 1 << item_data.number
+
+ # exists multiple times
+ elif item_data.type in ["Upgrade", "Progressive Upgrade","Progressive Upgrade 2"]:
+ flaggroup = type_flaggroups[item_data.race][item_data.type]
+
+ # Generic upgrades apply only to Weapon / Armor upgrades
+ if item_data.type != "Upgrade" or ctx.generic_upgrade_items == 0:
+ accumulators[item_data.race][flaggroup] += 1 << item_data.number
+ else:
+ if name == ItemNames.PROGRESSIVE_PROTOSS_GROUND_UPGRADE:
+ shields_from_ground_upgrade += 1
+ if name == ItemNames.PROGRESSIVE_PROTOSS_AIR_UPGRADE:
+ shields_from_air_upgrade += 1
+ for bundled_number in upgrade_numbers[item_data.number]:
+ accumulators[item_data.race][flaggroup] += 1 << bundled_number
+
+ # Regen bio-steel nerf with API3 - undo for older games
+ if ctx.slot_data_version < 3 and name == ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL:
+ current_level = (accumulators[item_data.race][flaggroup] >> item_data.number) % 4
+ if current_level == 2:
+ # Switch from level 2 to level 3 for compatibility
+ accumulators[item_data.race][flaggroup] += 1 << item_data.number
+ # sum
+ else:
+ if name == ItemNames.STARTING_MINERALS:
+ accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] += ctx.minerals_per_item
+ elif name == ItemNames.STARTING_VESPENE:
+ accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] += ctx.vespene_per_item
+ elif name == ItemNames.STARTING_SUPPLY:
+ accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] += ctx.starting_supply_per_item
+ else:
+ accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] += item_data.number
+
+ # Fix Shields from generic upgrades by unit class (Maximum of ground/air upgrades)
+ if shields_from_ground_upgrade > 0 or shields_from_air_upgrade > 0:
+ shield_upgrade_level = max(shields_from_ground_upgrade, shields_from_air_upgrade)
+ shield_upgrade_item = item_list[ItemNames.PROGRESSIVE_PROTOSS_SHIELDS]
+ for _ in range(0, shield_upgrade_level):
+ accumulators[shield_upgrade_item.race][type_flaggroups[shield_upgrade_item.race][shield_upgrade_item.type]] += 1 << shield_upgrade_item.number
+
+ # Kerrigan levels per check
+ accumulators[SC2Race.ZERG][type_flaggroups[SC2Race.ZERG]["Level"]] += (len(ctx.checked_locations) // ctx.checks_per_level) * ctx.levels_per_check
+
+ # Upgrades from completed missions
+ if ctx.generic_upgrade_missions > 0:
+ total_missions = sum(len(ctx.mission_req_table[campaign]) for campaign in ctx.mission_req_table)
+ for race in SC2Race:
+ if "Upgrade" not in type_flaggroups[race]:
+ continue
+ upgrade_flaggroup = type_flaggroups[race]["Upgrade"]
+ num_missions = ctx.generic_upgrade_missions * total_missions
+ amounts = [
+ num_missions // 100,
+ 2 * num_missions // 100,
+ 3 * num_missions // 100
+ ]
+ upgrade_count = 0
+ completed = len([id for id in ctx.mission_id_to_location_ids if get_location_offset(id) + VICTORY_MODULO * id in ctx.checked_locations])
+ for amount in amounts:
+ if completed >= amount:
+ upgrade_count += 1
+ # Equivalent to "Progressive Weapon/Armor Upgrade" item
+ for bundled_number in upgrade_numbers[upgrade_numbers_all[race]]:
+ accumulators[race][upgrade_flaggroup] += upgrade_count << bundled_number
+
+ return accumulators
+
+
+def calc_difficulty(difficulty: int):
+ if difficulty == 0:
+ return 'C'
+ elif difficulty == 1:
+ return 'N'
+ elif difficulty == 2:
+ return 'H'
+ elif difficulty == 3:
+ return 'B'
+
+ return 'X'
+
+
+def get_kerrigan_level(ctx: SC2Context, items: typing.Dict[SC2Race, typing.List[int]], missions_beaten: int) -> int:
+ item_value = items[SC2Race.ZERG][type_flaggroups[SC2Race.ZERG]["Level"]]
+ mission_value = missions_beaten * ctx.kerrigan_levels_per_mission_completed
+ if ctx.kerrigan_levels_per_mission_completed_cap != -1:
+ mission_value = min(mission_value, ctx.kerrigan_levels_per_mission_completed_cap)
+ total_value = item_value + mission_value
+ if ctx.kerrigan_total_level_cap != -1:
+ total_value = min(total_value, ctx.kerrigan_total_level_cap)
+ return total_value
+
+
+def calculate_kerrigan_options(ctx: SC2Context) -> int:
+ options = 0
+
+ # Bits 0, 1
+ # Kerrigan unit available
+ if ctx.kerrigan_presence in kerrigan_unit_available:
+ options |= 1 << 0
+
+ # Bit 2
+ # Kerrigan primal status by map
+ if ctx.kerrigan_primal_status == KerriganPrimalStatus.option_vanilla:
+ options |= 1 << 2
+
+ return options
+
+
+def caclulate_soa_options(ctx: SC2Context) -> int:
+ options = 0
+
+ # Bits 0, 1
+ # SoA Calldowns available
+ soa_presence_value = 0
+ if ctx.spear_of_adun_presence == SpearOfAdunPresence.option_not_present:
+ soa_presence_value = 0
+ elif ctx.spear_of_adun_presence == SpearOfAdunPresence.option_lotv_protoss:
+ soa_presence_value = 1
+ elif ctx.spear_of_adun_presence == SpearOfAdunPresence.option_protoss:
+ soa_presence_value = 2
+ elif ctx.spear_of_adun_presence == SpearOfAdunPresence.option_everywhere:
+ soa_presence_value = 3
+ options |= soa_presence_value << 0
+
+ # Bit 2
+ # SoA Calldowns for no-builds
+ if ctx.spear_of_adun_present_in_no_build == SpearOfAdunPresentInNoBuild.option_true:
+ options |= 1 << 2
+
+ # Bits 3,4
+ # Autocasts
+ soa_autocasts_presence_value = 0
+ if ctx.spear_of_adun_autonomously_cast_ability_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_not_present:
+ soa_autocasts_presence_value = 0
+ elif ctx.spear_of_adun_autonomously_cast_ability_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_lotv_protoss:
+ soa_autocasts_presence_value = 1
+ elif ctx.spear_of_adun_autonomously_cast_ability_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_protoss:
+ soa_autocasts_presence_value = 2
+ elif ctx.spear_of_adun_autonomously_cast_ability_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_everywhere:
+ soa_autocasts_presence_value = 3
+ options |= soa_autocasts_presence_value << 3
+
+ # Bit 5
+ # Autocasts in no-builds
+ if ctx.spear_of_adun_autonomously_cast_present_in_no_build == SpearOfAdunAutonomouslyCastPresentInNoBuild.option_true:
+ options |= 1 << 5
+
+ return options
+
+def kerrigan_primal(ctx: SC2Context, kerrigan_level: int) -> bool:
+ if ctx.kerrigan_primal_status == KerriganPrimalStatus.option_always_zerg:
+ return True
+ elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_always_human:
+ return False
+ elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_level_35:
+ return kerrigan_level >= 35
+ elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_half_completion:
+ total_missions = len(ctx.mission_id_to_location_ids)
+ completed = sum((mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations
+ for mission_id in ctx.mission_id_to_location_ids)
+ return completed >= (total_missions / 2)
+ elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_item:
+ codes = [item.item for item in ctx.items_received]
+ return get_full_item_list()[ItemNames.KERRIGAN_PRIMAL_FORM].code in codes
+ return False
+
+async def starcraft_launch(ctx: SC2Context, mission_id: int):
+ sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id].mission_name}. If game does not launch check log file for errors.")
+
+ with DllDirectory(None):
+ run_game(bot.maps.get(lookup_id_to_mission[mission_id].map_file), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
+ name="Archipelago", fullscreen=True)], realtime=True)
+
+
+class ArchipelagoBot(bot.bot_ai.BotAI):
+ __slots__ = [
+ 'game_running',
+ 'mission_completed',
+ 'boni',
+ 'setup_done',
+ 'ctx',
+ 'mission_id',
+ 'want_close',
+ 'can_read_game',
+ 'last_received_update',
+ ]
+
+ def __init__(self, ctx: SC2Context, mission_id: int):
+ self.game_running = False
+ self.mission_completed = False
+ self.want_close = False
+ self.can_read_game = False
+ self.last_received_update: int = 0
+ self.setup_done = False
+ self.ctx = ctx
+ self.ctx.last_bot = self
+ self.mission_id = mission_id
+ self.boni = [False for _ in range(MAX_BONUS)]
+
+ super(ArchipelagoBot, self).__init__()
+
+ async def on_step(self, iteration: int):
+ if self.want_close:
+ self.want_close = False
+ await self._client.leave()
+ return
+ game_state = 0
+ if not self.setup_done:
+ self.setup_done = True
+ start_items = calculate_items(self.ctx)
+ missions_beaten = self.missions_beaten_count()
+ kerrigan_level = get_kerrigan_level(self.ctx, start_items, missions_beaten)
+ kerrigan_options = calculate_kerrigan_options(self.ctx)
+ soa_options = caclulate_soa_options(self.ctx)
+ if self.ctx.difficulty_override >= 0:
+ difficulty = calc_difficulty(self.ctx.difficulty_override)
+ else:
+ difficulty = calc_difficulty(self.ctx.difficulty)
+ if self.ctx.game_speed_override >= 0:
+ game_speed = self.ctx.game_speed_override
+ else:
+ game_speed = self.ctx.game_speed
+ await self.chat_send("?SetOptions {} {} {} {} {} {} {} {} {} {} {} {} {}".format(
+ difficulty,
+ self.ctx.generic_upgrade_research,
+ self.ctx.all_in_choice,
+ game_speed,
+ self.ctx.disable_forced_camera,
+ self.ctx.skip_cutscenes,
+ kerrigan_options,
+ self.ctx.grant_story_tech,
+ self.ctx.take_over_ai_allies,
+ soa_options,
+ self.ctx.mission_order,
+ 1 if self.ctx.nova_covert_ops_only else 0,
+ self.ctx.grant_story_levels
+ ))
+ await self.chat_send("?GiveResources {} {} {}".format(
+ start_items[SC2Race.ANY][0],
+ start_items[SC2Race.ANY][1],
+ start_items[SC2Race.ANY][2]
+ ))
+ await self.updateTerranTech(start_items)
+ await self.updateZergTech(start_items, kerrigan_level)
+ await self.updateProtossTech(start_items)
+ await self.updateColors()
+ await self.chat_send("?LoadFinished")
+ self.last_received_update = len(self.ctx.items_received)
+
+ else:
+ if self.ctx.pending_color_update:
+ await self.updateColors()
+
+ if not self.ctx.announcements.empty():
+ message = self.ctx.announcements.get(timeout=1)
+ await self.chat_send("?SendMessage " + message)
+ self.ctx.announcements.task_done()
+
+ # Archipelago reads the health
+ controller1_state = 0
+ controller2_state = 0
+ for unit in self.all_own_units():
+ if unit.health_max == CONTROLLER_HEALTH:
+ controller1_state = int(CONTROLLER_HEALTH - unit.health)
+ self.can_read_game = True
+ elif unit.health_max == CONTROLLER2_HEALTH:
+ controller2_state = int(CONTROLLER2_HEALTH - unit.health)
+ self.can_read_game = True
+ game_state = controller1_state + (controller2_state << 15)
+
+ if iteration == 160 and not game_state & 1:
+ await self.chat_send("?SendMessage Warning: Archipelago unable to connect or has lost connection to " +
+ "Starcraft 2 (This is likely a map issue)")
+
+ if self.last_received_update < len(self.ctx.items_received):
+ current_items = calculate_items(self.ctx)
+ missions_beaten = self.missions_beaten_count()
+ kerrigan_level = get_kerrigan_level(self.ctx, current_items, missions_beaten)
+ await self.updateTerranTech(current_items)
+ await self.updateZergTech(current_items, kerrigan_level)
+ await self.updateProtossTech(current_items)
+ self.last_received_update = len(self.ctx.items_received)
+
+ if game_state & 1:
+ if not self.game_running:
+ print("Archipelago Connected")
+ self.game_running = True
+
+ if self.can_read_game:
+ if game_state & (1 << 1) and not self.mission_completed:
+ if self.mission_id != self.ctx.final_mission:
+ print("Mission Completed")
+ await self.ctx.send_msgs(
+ [{"cmd": 'LocationChecks',
+ "locations": [get_location_offset(self.mission_id) + VICTORY_MODULO * self.mission_id]}])
+ self.mission_completed = True
+ else:
+ print("Game Complete")
+ await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}])
+ self.mission_completed = True
+
+ for x, completed in enumerate(self.boni):
+ if not completed and game_state & (1 << (x + 2)):
+ await self.ctx.send_msgs(
+ [{"cmd": 'LocationChecks',
+ "locations": [get_location_offset(self.mission_id) + VICTORY_MODULO * self.mission_id + x + 1]}])
+ self.boni[x] = True
+ else:
+ await self.chat_send("?SendMessage LostConnection - Lost connection to game.")
+
+ def missions_beaten_count(self):
+ return len([location for location in self.ctx.checked_locations if location % VICTORY_MODULO == 0])
+
+ async def updateColors(self):
+ await self.chat_send("?SetColor rr " + str(self.ctx.player_color_raynor))
+ await self.chat_send("?SetColor ks " + str(self.ctx.player_color_zerg))
+ await self.chat_send("?SetColor pz " + str(self.ctx.player_color_zerg_primal))
+ await self.chat_send("?SetColor da " + str(self.ctx.player_color_protoss))
+ await self.chat_send("?SetColor nova " + str(self.ctx.player_color_nova))
+ self.ctx.pending_color_update = False
+
+ async def updateTerranTech(self, current_items):
+ terran_items = current_items[SC2Race.TERRAN]
+ await self.chat_send("?GiveTerranTech {} {} {} {} {} {} {} {} {} {} {} {} {} {}".format(
+ terran_items[0], terran_items[1], terran_items[2], terran_items[3], terran_items[4],
+ terran_items[5], terran_items[6], terran_items[7], terran_items[8], terran_items[9], terran_items[10],
+ terran_items[11], terran_items[12], terran_items[13]))
+
+ async def updateZergTech(self, current_items, kerrigan_level):
+ zerg_items = current_items[SC2Race.ZERG]
+ kerrigan_primal_by_items = kerrigan_primal(self.ctx, kerrigan_level)
+ kerrigan_primal_bot_value = 1 if kerrigan_primal_by_items else 0
+ await self.chat_send("?GiveZergTech {} {} {} {} {} {} {} {} {} {} {} {}".format(
+ kerrigan_level, kerrigan_primal_bot_value, zerg_items[0], zerg_items[1], zerg_items[2],
+ zerg_items[3], zerg_items[4], zerg_items[5], zerg_items[6], zerg_items[9], zerg_items[10], zerg_items[11]
+ ))
+
+ async def updateProtossTech(self, current_items):
+ protoss_items = current_items[SC2Race.PROTOSS]
+ await self.chat_send("?GiveProtossTech {} {} {} {} {} {} {} {} {} {}".format(
+ protoss_items[0], protoss_items[1], protoss_items[2], protoss_items[3], protoss_items[4],
+ protoss_items[5], protoss_items[6], protoss_items[7], protoss_items[8], protoss_items[9]
+ ))
+
+
+def request_unfinished_missions(ctx: SC2Context) -> None:
+ if ctx.mission_req_table:
+ message = "Unfinished Missions: "
+ unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
+ unfinished_locations: typing.Dict[SC2Mission, typing.List[str]] = {}
+
+ _, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks)
+
+ for mission in unfinished_missions:
+ objectives = set(ctx.locations_for_mission(mission))
+ if objectives:
+ remaining_objectives = objectives.difference(ctx.checked_locations)
+ unfinished_locations[mission] = [ctx.location_names.lookup_in_game(location_id) for location_id in remaining_objectives]
+ else:
+ unfinished_locations[mission] = []
+
+ # Removing All-In from location pool
+ final_mission = lookup_id_to_mission[ctx.final_mission]
+ if final_mission in unfinished_missions.keys():
+ message = f"Final Mission Available: {final_mission}[{ctx.final_mission}]\n" + message
+ if unfinished_missions[final_mission] == -1:
+ unfinished_missions.pop(final_mission)
+
+ message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[ctx.find_campaign(mission)][mission].mission.id}] " +
+ mark_up_objectives(
+ f"[{len(unfinished_missions[mission])}/"
+ f"{sum(1 for _ in ctx.locations_for_mission(mission))}]",
+ ctx, unfinished_locations, mission)
+ for mission in unfinished_missions)
+
+ if ctx.ui:
+ ctx.ui.log_panels['All'].on_message_markup(message)
+ ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
+ else:
+ sc2_logger.info(message)
+ else:
+ sc2_logger.warning("No mission table found, you are likely not connected to a server.")
+
+
+def calc_unfinished_missions(ctx: SC2Context, unlocks: typing.Optional[typing.Dict] = None):
+ unfinished_missions: typing.List[str] = []
+ locations_completed: typing.List[typing.Union[typing.Set[int], typing.Literal[-1]]] = []
+
+ if not unlocks:
+ unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
+
+ available_missions = calc_available_missions(ctx, unlocks)
+
+ for name in available_missions:
+ objectives = set(ctx.locations_for_mission(name))
+ if objectives:
+ objectives_completed = ctx.checked_locations & objectives
+ if len(objectives_completed) < len(objectives):
+ unfinished_missions.append(name)
+ locations_completed.append(objectives_completed)
+
+ else: # infer that this is the final mission as it has no objectives
+ unfinished_missions.append(name)
+ locations_completed.append(-1)
+
+ return available_missions, dict(zip(unfinished_missions, locations_completed))
+
+
+def is_mission_available(ctx: SC2Context, mission_id_to_check: int) -> bool:
+ unfinished_missions = calc_available_missions(ctx)
+
+ return any(mission_id_to_check == ctx.mission_req_table[ctx.find_campaign(mission)][mission].mission.id for mission in unfinished_missions)
+
+
+def mark_up_mission_name(ctx: SC2Context, mission_name: str, unlock_table: typing.Dict) -> str:
+ """Checks if the mission is required for game completion and adds '*' to the name to mark that."""
+
+ campaign = ctx.find_campaign(mission_name)
+ mission_info = ctx.mission_req_table[campaign][mission_name]
+ if mission_info.completion_critical:
+ if ctx.ui:
+ message = "[color=AF99EF]" + mission_name + "[/color]"
+ else:
+ message = "*" + mission_name + "*"
+ else:
+ message = mission_name
+
+ if ctx.ui:
+ campaign_missions = list(ctx.mission_req_table[campaign].keys())
+ unlocks: typing.List[str]
+ index = campaign_missions.index(mission_name)
+ if index in unlock_table[campaign]:
+ unlocks = unlock_table[campaign][index]
+ else:
+ unlocks = []
+
+ if len(unlocks) > 0:
+ pre_message = f"[ref={mission_info.mission.id}|Unlocks: "
+ pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[ctx.find_campaign(unlock)][unlock].mission.id})" for unlock in unlocks)
+ pre_message += f"]"
+ message = pre_message + message + "[/ref]"
+
+ return message
+
+
+def mark_up_objectives(message, ctx, unfinished_locations, mission):
+ formatted_message = message
+
+ if ctx.ui:
+ locations = unfinished_locations[mission]
+ campaign = ctx.find_campaign(mission)
+
+ pre_message = f"[ref={list(ctx.mission_req_table[campaign]).index(mission) + 30}|"
+ pre_message += " ".join(location for location in locations)
+ pre_message += f"]"
+ formatted_message = pre_message + message + "[/ref]"
+
+ return formatted_message
+
+
+def request_available_missions(ctx: SC2Context):
+ if ctx.mission_req_table:
+ message = "Available Missions: "
+
+ # Initialize mission unlock table
+ unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
+
+ missions = calc_available_missions(ctx, unlocks)
+ message += \
+ ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}"
+ f"[{ctx.mission_req_table[ctx.find_campaign(mission)][mission].mission.id}]"
+ for mission in missions)
+
+ if ctx.ui:
+ ctx.ui.log_panels['All'].on_message_markup(message)
+ ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
+ else:
+ sc2_logger.info(message)
+ else:
+ sc2_logger.warning("No mission table found, you are likely not connected to a server.")
+
+
+def calc_available_missions(ctx: SC2Context, unlocks: typing.Optional[dict] = None) -> typing.List[str]:
+ available_missions: typing.List[str] = []
+ missions_complete = 0
+
+ # Get number of missions completed
+ for loc in ctx.checked_locations:
+ if loc % VICTORY_MODULO == 0:
+ missions_complete += 1
+
+ for campaign in ctx.mission_req_table:
+ # Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips
+ for mission_name in ctx.mission_req_table[campaign]:
+ if unlocks:
+ for unlock in ctx.mission_req_table[campaign][mission_name].required_world:
+ parsed_unlock = parse_unlock(unlock)
+ # TODO prophecy-only wants to connect to WoL here
+ index = parsed_unlock.connect_to - 1
+ unlock_mission = list(ctx.mission_req_table[parsed_unlock.campaign])[index]
+ unlock_campaign = ctx.find_campaign(unlock_mission)
+ if unlock_campaign in unlocks:
+ if index not in unlocks[unlock_campaign]:
+ unlocks[unlock_campaign][index] = list()
+ unlocks[unlock_campaign][index].append(mission_name)
+
+ if mission_reqs_completed(ctx, mission_name, missions_complete):
+ available_missions.append(mission_name)
+
+ return available_missions
+
+
+def parse_unlock(unlock: typing.Union[typing.Dict[typing.Literal["connect_to", "campaign"], int], MissionConnection, int]) -> MissionConnection:
+ if isinstance(unlock, int):
+ # Legacy
+ return MissionConnection(unlock)
+ elif isinstance(unlock, MissionConnection):
+ return unlock
+ else:
+ # Multi-campaign
+ return MissionConnection(unlock["connect_to"], lookup_id_to_campaign[unlock["campaign"]])
+
+
+def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete: int) -> bool:
+ """Returns a bool signifying if the mission has all requirements complete and can be done
+
+ Arguments:
+ ctx -- instance of SC2Context
+ locations_to_check -- the mission string name to check
+ missions_complete -- an int of how many missions have been completed
+ mission_path -- a list of missions that have already been checked
+ """
+ campaign = ctx.find_campaign(mission_name)
+
+ if len(ctx.mission_req_table[campaign][mission_name].required_world) >= 1:
+ # A check for when the requirements are being or'd
+ or_success = False
+
+ # Loop through required missions
+ for req_mission in ctx.mission_req_table[campaign][mission_name].required_world:
+ req_success = True
+ parsed_req_mission = parse_unlock(req_mission)
+
+ # Check if required mission has been completed
+ mission_id = ctx.mission_req_table[parsed_req_mission.campaign][
+ list(ctx.mission_req_table[parsed_req_mission.campaign])[parsed_req_mission.connect_to - 1]].mission.id
+ if not (mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations:
+ if not ctx.mission_req_table[campaign][mission_name].or_requirements:
+ return False
+ else:
+ req_success = False
+
+ # Grid-specific logic (to avoid long path checks and infinite recursion)
+ if ctx.mission_order in (MissionOrder.option_grid, MissionOrder.option_mini_grid, MissionOrder.option_medium_grid):
+ if req_success:
+ return True
+ else:
+ if parsed_req_mission == ctx.mission_req_table[campaign][mission_name].required_world[-1]:
+ return False
+ else:
+ continue
+
+ # Recursively check required mission to see if it's requirements are met, in case !collect has been done
+ # Skipping recursive check on Grid settings to speed up checks and avoid infinite recursion
+ if not mission_reqs_completed(ctx, list(ctx.mission_req_table[parsed_req_mission.campaign])[parsed_req_mission.connect_to - 1], missions_complete):
+ if not ctx.mission_req_table[campaign][mission_name].or_requirements:
+ return False
+ else:
+ req_success = False
+
+ # If requirement check succeeded mark or as satisfied
+ if ctx.mission_req_table[campaign][mission_name].or_requirements and req_success:
+ or_success = True
+
+ if ctx.mission_req_table[campaign][mission_name].or_requirements:
+ # Return false if or requirements not met
+ if not or_success:
+ return False
+
+ # Check number of missions
+ if missions_complete >= ctx.mission_req_table[campaign][mission_name].number:
+ return True
+ else:
+ return False
+ else:
+ return True
+
+
+def initialize_blank_mission_dict(location_table: typing.Dict[SC2Campaign, typing.Dict[str, MissionInfo]]):
+ unlocks: typing.Dict[SC2Campaign, typing.Dict] = {}
+
+ for mission in list(location_table):
+ unlocks[mission] = {}
+
+ return unlocks
+
+
+def check_game_install_path() -> bool:
+ # First thing: go to the default location for ExecuteInfo.
+ # An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it.
+ if is_windows:
+ # The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow.
+ # https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555#
+ import ctypes.wintypes
+ CSIDL_PERSONAL = 5 # My Documents
+ SHGFP_TYPE_CURRENT = 0 # Get current, not default value
+
+ buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
+ ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf)
+ documentspath: str = buf.value
+ einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt"))
+ else:
+ einfo = str(bot.paths.get_home() / Path(bot.paths.USERPATH[bot.paths.PF]))
+
+ # Check if the file exists.
+ if os.path.isfile(einfo):
+
+ # Open the file and read it, picking out the latest executable's path.
+ with open(einfo) as f:
+ content = f.read()
+ if content:
+ search_result = re.search(r" = (.*)Versions", content)
+ if not search_result:
+ sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, "
+ "then try again.")
+ return False
+ base = search_result.group(1)
+
+ if os.path.exists(base):
+ executable = bot.paths.latest_executeble(Path(base).expanduser() / "Versions")
+
+ # Finally, check the path for an actual executable.
+ # If we find one, great. Set up the SC2PATH.
+ if os.path.isfile(executable):
+ sc2_logger.info(f"Found an SC2 install at {base}!")
+ sc2_logger.debug(f"Latest executable at {executable}.")
+ os.environ["SC2PATH"] = base
+ sc2_logger.debug(f"SC2PATH set to {base}.")
+ return True
+ else:
+ sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.")
+ else:
+ sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.")
+ else:
+ sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. "
+ f"If that fails, please run /set_path with your SC2 install directory.")
+ return False
+
+
+def is_mod_installed_correctly() -> bool:
+ """Searches for all required files."""
+ if "SC2PATH" not in os.environ:
+ check_game_install_path()
+ sc2_path: str = os.environ["SC2PATH"]
+ mapdir = sc2_path / Path('Maps/ArchipelagoCampaign')
+ mods = ["ArchipelagoCore", "ArchipelagoPlayer", "ArchipelagoPlayerSuper", "ArchipelagoPatches",
+ "ArchipelagoTriggers", "ArchipelagoPlayerWoL", "ArchipelagoPlayerHotS",
+ "ArchipelagoPlayerLotV", "ArchipelagoPlayerLotVPrologue", "ArchipelagoPlayerNCO"]
+ modfiles = [sc2_path / Path("Mods/" + mod + ".SC2Mod") for mod in mods]
+ wol_required_maps: typing.List[str] = ["WoL" + os.sep + mission.map_file + ".SC2Map" for mission in SC2Mission
+ if mission.campaign in (SC2Campaign.WOL, SC2Campaign.PROPHECY)]
+ hots_required_maps: typing.List[str] = ["HotS" + os.sep + mission.map_file + ".SC2Map" for mission in campaign_mission_table[SC2Campaign.HOTS]]
+ lotv_required_maps: typing.List[str] = ["LotV" + os.sep + mission.map_file + ".SC2Map" for mission in SC2Mission
+ if mission.campaign in (SC2Campaign.LOTV, SC2Campaign.PROLOGUE, SC2Campaign.EPILOGUE)]
+ nco_required_maps: typing.List[str] = ["NCO" + os.sep + mission.map_file + ".SC2Map" for mission in campaign_mission_table[SC2Campaign.NCO]]
+ required_maps = wol_required_maps + hots_required_maps + lotv_required_maps + nco_required_maps
+ needs_files = False
+
+ # Check for maps.
+ missing_maps: typing.List[str] = []
+ for mapfile in required_maps:
+ if not os.path.isfile(mapdir / mapfile):
+ missing_maps.append(mapfile)
+ if len(missing_maps) >= 19:
+ sc2_logger.warning(f"All map files missing from {mapdir}.")
+ needs_files = True
+ elif len(missing_maps) > 0:
+ for map in missing_maps:
+ sc2_logger.debug(f"Missing {map} from {mapdir}.")
+ sc2_logger.warning(f"Missing {len(missing_maps)} map files.")
+ needs_files = True
+ else: # Must be no maps missing
+ sc2_logger.info(f"All maps found in {mapdir}.")
+
+ # Check for mods.
+ for modfile in modfiles:
+ if os.path.isfile(modfile) or os.path.isdir(modfile):
+ sc2_logger.info(f"Archipelago mod found at {modfile}.")
+ else:
+ sc2_logger.warning(f"Archipelago mod could not be found at {modfile}.")
+ needs_files = True
+
+ # Final verdict.
+ if needs_files:
+ sc2_logger.warning(f"Required files are missing. Run /download_data to acquire them.")
+ return False
+ else:
+ sc2_logger.debug(f"All map/mod files are properly installed.")
+ return True
+
+
+class DllDirectory:
+ # Credit to Black Sliver for this code.
+ # More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw
+ _old: typing.Optional[str] = None
+ _new: typing.Optional[str] = None
+
+ def __init__(self, new: typing.Optional[str]):
+ self._new = new
+
+ def __enter__(self):
+ old = self.get()
+ if self.set(self._new):
+ self._old = old
+
+ def __exit__(self, *args):
+ if self._old is not None:
+ self.set(self._old)
+
+ @staticmethod
+ def get() -> typing.Optional[str]:
+ if sys.platform == "win32":
+ n = ctypes.windll.kernel32.GetDllDirectoryW(0, None)
+ buf = ctypes.create_unicode_buffer(n)
+ ctypes.windll.kernel32.GetDllDirectoryW(n, buf)
+ return buf.value
+ # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
+ return None
+
+ @staticmethod
+ def set(s: typing.Optional[str]) -> bool:
+ if sys.platform == "win32":
+ return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0
+ # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
+ return False
+
+
+def download_latest_release_zip(
+ owner: str,
+ repo: str,
+ api_version: str,
+ metadata: typing.Optional[str] = None,
+ force_download=False
+) -> typing.Tuple[str, typing.Optional[str]]:
+ """Downloads the latest release of a GitHub repo to the current directory as a .zip file."""
+ import requests
+
+ headers = {"Accept": 'application/vnd.github.v3+json'}
+ url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{api_version}"
+
+ r1 = requests.get(url, headers=headers)
+ if r1.status_code == 200:
+ latest_metadata = r1.json()
+ cleanup_downloaded_metadata(latest_metadata)
+ latest_metadata = str(latest_metadata)
+ # sc2_logger.info(f"Latest version: {latest_metadata}.")
+ else:
+ sc2_logger.warning(f"Status code: {r1.status_code}")
+ sc2_logger.warning(f"Failed to reach GitHub. Could not find download link.")
+ sc2_logger.warning(f"text: {r1.text}")
+ return "", metadata
+
+ if (force_download is False) and (metadata == latest_metadata):
+ sc2_logger.info("Latest version already installed.")
+ return "", metadata
+
+ sc2_logger.info(f"Attempting to download latest version of API version {api_version} of {repo}.")
+ download_url = r1.json()["assets"][0]["browser_download_url"]
+
+ r2 = requests.get(download_url, headers=headers)
+ if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)):
+ tempdir = tempfile.gettempdir()
+ file = tempdir + os.sep + f"{repo}.zip"
+ with open(file, "wb") as fh:
+ fh.write(r2.content)
+ sc2_logger.info(f"Successfully downloaded {repo}.zip.")
+ return file, latest_metadata
+ else:
+ sc2_logger.warning(f"Status code: {r2.status_code}")
+ sc2_logger.warning("Download failed.")
+ sc2_logger.warning(f"text: {r2.text}")
+ return "", metadata
+
+
+def cleanup_downloaded_metadata(medatada_json: dict) -> None:
+ for asset in medatada_json['assets']:
+ del asset['download_count']
+
+
+def is_mod_update_available(owner: str, repo: str, api_version: str, metadata: str) -> bool:
+ import requests
+
+ headers = {"Accept": 'application/vnd.github.v3+json'}
+ url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{api_version}"
+
+ r1 = requests.get(url, headers=headers)
+ if r1.status_code == 200:
+ latest_metadata = r1.json()
+ cleanup_downloaded_metadata(latest_metadata)
+ latest_metadata = str(latest_metadata)
+ if metadata != latest_metadata:
+ return True
+ else:
+ return False
+
+ else:
+ sc2_logger.warning(f"Failed to reach GitHub while checking for updates.")
+ sc2_logger.warning(f"Status code: {r1.status_code}")
+ sc2_logger.warning(f"text: {r1.text}")
+ return False
+
+
+def get_location_offset(mission_id):
+ return SC2WOL_LOC_ID_OFFSET if mission_id <= SC2Mission.ALL_IN.id \
+ else (SC2HOTS_LOC_ID_OFFSET - SC2Mission.ALL_IN.id * VICTORY_MODULO)
+
+
+def launch():
+ colorama.init()
+ asyncio.run(main())
+ colorama.deinit()
diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py
new file mode 100644
index 000000000000..22e444efe7c9
--- /dev/null
+++ b/worlds/sc2/ClientGui.py
@@ -0,0 +1,304 @@
+from typing import *
+import asyncio
+
+from kvui import GameManager, HoverBehavior, ServerToolTip
+from kivy.app import App
+from kivy.clock import Clock
+from kivy.uix.tabbedpanel import TabbedPanelItem
+from kivy.uix.gridlayout import GridLayout
+from kivy.lang import Builder
+from kivy.uix.label import Label
+from kivy.uix.button import Button
+from kivy.uix.floatlayout import FloatLayout
+from kivy.uix.scrollview import ScrollView
+from kivy.properties import StringProperty
+
+from .Client import SC2Context, calc_unfinished_missions, parse_unlock
+from .MissionTables import (lookup_id_to_mission, lookup_name_to_mission, campaign_race_exceptions, SC2Mission, SC2Race,
+ SC2Campaign)
+from .Locations import LocationType, lookup_location_id_to_type
+from .Options import LocationInclusion
+from . import SC2World, get_first_mission
+
+
+class HoverableButton(HoverBehavior, Button):
+ pass
+
+
+class MissionButton(HoverableButton):
+ tooltip_text = StringProperty("Test")
+
+ def __init__(self, *args, **kwargs):
+ super(HoverableButton, self).__init__(*args, **kwargs)
+ self.layout = FloatLayout()
+ self.popuplabel = ServerToolTip(text=self.text, markup=True)
+ self.popuplabel.padding = [5, 2, 5, 2]
+ self.layout.add_widget(self.popuplabel)
+
+ def on_enter(self):
+ self.popuplabel.text = self.tooltip_text
+
+ if self.ctx.current_tooltip:
+ App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
+
+ if self.tooltip_text == "":
+ self.ctx.current_tooltip = None
+ else:
+ App.get_running_app().root.add_widget(self.layout)
+ self.ctx.current_tooltip = self.layout
+
+ def on_leave(self):
+ self.ctx.ui.clear_tooltip()
+
+ @property
+ def ctx(self) -> SC2Context:
+ return App.get_running_app().ctx
+
+class CampaignScroll(ScrollView):
+ pass
+
+class MultiCampaignLayout(GridLayout):
+ pass
+
+class CampaignLayout(GridLayout):
+ pass
+
+class MissionLayout(GridLayout):
+ pass
+
+class MissionCategory(GridLayout):
+ pass
+
+class SC2Manager(GameManager):
+ logging_pairs = [
+ ("Client", "Archipelago"),
+ ("Starcraft2", "Starcraft2"),
+ ]
+ base_title = "Archipelago Starcraft 2 Client"
+
+ campaign_panel: Optional[CampaignLayout] = None
+ last_checked_locations: Set[int] = set()
+ mission_id_to_button: Dict[int, MissionButton] = {}
+ launching: Union[bool, int] = False # if int -> mission ID
+ refresh_from_launching = True
+ first_check = True
+ first_mission = ""
+ ctx: SC2Context
+
+ def __init__(self, ctx) -> None:
+ super().__init__(ctx)
+
+ def clear_tooltip(self) -> None:
+ if self.ctx.current_tooltip:
+ App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
+
+ self.ctx.current_tooltip = None
+
+ def build(self):
+ container = super().build()
+
+ panel = TabbedPanelItem(text="Starcraft 2 Launcher")
+ panel.content = CampaignScroll()
+ self.campaign_panel = MultiCampaignLayout()
+ panel.content.add_widget(self.campaign_panel)
+
+ self.tabs.add_widget(panel)
+
+ Clock.schedule_interval(self.build_mission_table, 0.5)
+
+ return container
+
+ def build_mission_table(self, dt) -> None:
+ if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or
+ not self.refresh_from_launching)) or self.first_check:
+ assert self.campaign_panel is not None
+ self.refresh_from_launching = True
+
+ self.campaign_panel.clear_widgets()
+ if self.ctx.mission_req_table:
+ self.last_checked_locations = self.ctx.checked_locations.copy()
+ self.first_check = False
+ self.first_mission = get_first_mission(self.ctx.mission_req_table)
+
+ self.mission_id_to_button = {}
+
+ available_missions, unfinished_missions = calc_unfinished_missions(self.ctx)
+
+ multi_campaign_layout_height = 0
+
+ for campaign, missions in sorted(self.ctx.mission_req_table.items(), key=lambda item: item[0].id):
+ categories: Dict[str, List[str]] = {}
+
+ # separate missions into categories
+ for mission_index in missions:
+ mission_info = self.ctx.mission_req_table[campaign][mission_index]
+ if mission_info.category not in categories:
+ categories[mission_info.category] = []
+
+ categories[mission_info.category].append(mission_index)
+
+ max_mission_count = max(len(categories[category]) for category in categories)
+ if max_mission_count == 1:
+ campaign_layout_height = 115
+ else:
+ campaign_layout_height = (max_mission_count + 2) * 50
+ multi_campaign_layout_height += campaign_layout_height
+ campaign_layout = CampaignLayout(size_hint_y=None, height=campaign_layout_height)
+ if campaign != SC2Campaign.GLOBAL:
+ campaign_layout.add_widget(
+ Label(text=campaign.campaign_name, size_hint_y=None, height=25, outline_width=1)
+ )
+ mission_layout = MissionLayout()
+
+ for category in categories:
+ category_name_height = 0
+ category_spacing = 3
+ if category.startswith('_'):
+ category_display_name = ''
+ else:
+ category_display_name = category
+ category_name_height += 25
+ category_spacing = 10
+ category_panel = MissionCategory(padding=[category_spacing,6,category_spacing,6])
+ category_panel.add_widget(
+ Label(text=category_display_name, size_hint_y=None, height=category_name_height, outline_width=1))
+
+ for mission in categories[category]:
+ text: str = mission
+ tooltip: str = ""
+ mission_obj: SC2Mission = lookup_name_to_mission[mission]
+ mission_id: int = mission_obj.id
+ mission_data = self.ctx.mission_req_table[campaign][mission]
+ remaining_locations, plando_locations, remaining_count = self.sort_unfinished_locations(mission)
+ # Map has uncollected locations
+ if mission in unfinished_missions:
+ if self.any_valuable_locations(remaining_locations):
+ text = f"[color=6495ED]{text}[/color]"
+ else:
+ text = f"[color=A0BEF4]{text}[/color]"
+ elif mission in available_missions:
+ text = f"[color=FFFFFF]{text}[/color]"
+ # Map requirements not met
+ else:
+ text = f"[color=a9a9a9]{text}[/color]"
+ tooltip = f"Requires: "
+ if mission_data.required_world:
+ tooltip += ", ".join(list(self.ctx.mission_req_table[parse_unlock(req_mission).campaign])[parse_unlock(req_mission).connect_to - 1] for
+ req_mission in
+ mission_data.required_world)
+
+ if mission_data.number:
+ tooltip += " and "
+ if mission_data.number:
+ tooltip += f"{self.ctx.mission_req_table[campaign][mission].number} missions completed"
+
+ if mission_id == self.ctx.final_mission:
+ if mission in available_missions:
+ text = f"[color=FFBC95]{mission}[/color]"
+ else:
+ text = f"[color=D0C0BE]{mission}[/color]"
+ if tooltip:
+ tooltip += "\n"
+ tooltip += "Final Mission"
+
+ if remaining_count > 0:
+ if tooltip:
+ tooltip += "\n\n"
+ tooltip += f"-- Uncollected locations --"
+ for loctype in LocationType:
+ if len(remaining_locations[loctype]) > 0:
+ if loctype == LocationType.VICTORY:
+ tooltip += f"\n- {remaining_locations[loctype][0]}"
+ else:
+ tooltip += f"\n{self.get_location_type_title(loctype)}:\n- "
+ tooltip += "\n- ".join(remaining_locations[loctype])
+ if len(plando_locations) > 0:
+ tooltip += f"\nPlando:\n- "
+ tooltip += "\n- ".join(plando_locations)
+
+ MISSION_BUTTON_HEIGHT = 50
+ for pad in range(mission_data.ui_vertical_padding):
+ column_spacer = Label(text='', size_hint_y=None, height=MISSION_BUTTON_HEIGHT)
+ category_panel.add_widget(column_spacer)
+ mission_button = MissionButton(text=text, size_hint_y=None, height=MISSION_BUTTON_HEIGHT)
+ mission_race = mission_obj.race
+ if mission_race == SC2Race.ANY:
+ mission_race = mission_obj.campaign.race
+ race = campaign_race_exceptions.get(mission_obj, mission_race)
+ racial_colors = {
+ SC2Race.TERRAN: (0.24, 0.84, 0.68),
+ SC2Race.ZERG: (1, 0.65, 0.37),
+ SC2Race.PROTOSS: (0.55, 0.7, 1)
+ }
+ if race in racial_colors:
+ mission_button.background_color = racial_colors[race]
+ mission_button.tooltip_text = tooltip
+ mission_button.bind(on_press=self.mission_callback)
+ self.mission_id_to_button[mission_id] = mission_button
+ category_panel.add_widget(mission_button)
+
+ category_panel.add_widget(Label(text=""))
+ mission_layout.add_widget(category_panel)
+ campaign_layout.add_widget(mission_layout)
+ self.campaign_panel.add_widget(campaign_layout)
+ self.campaign_panel.height = multi_campaign_layout_height
+
+ elif self.launching:
+ assert self.campaign_panel is not None
+ self.refresh_from_launching = False
+
+ self.campaign_panel.clear_widgets()
+ self.campaign_panel.add_widget(Label(text="Launching Mission: " +
+ lookup_id_to_mission[self.launching].mission_name))
+ if self.ctx.ui:
+ self.ctx.ui.clear_tooltip()
+
+ def mission_callback(self, button: MissionButton) -> None:
+ if not self.launching:
+ mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button)
+ if self.ctx.play_mission(mission_id):
+ self.launching = mission_id
+ Clock.schedule_once(self.finish_launching, 10)
+
+ def finish_launching(self, dt):
+ self.launching = False
+
+ def sort_unfinished_locations(self, mission_name: str) -> Tuple[Dict[LocationType, List[str]], List[str], int]:
+ locations: Dict[LocationType, List[str]] = {loctype: [] for loctype in LocationType}
+ count = 0
+ for loc in self.ctx.locations_for_mission(mission_name):
+ if loc in self.ctx.missing_locations:
+ count += 1
+ locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names.lookup_in_game(loc))
+
+ plando_locations = []
+ for plando_loc in self.ctx.plando_locations:
+ for loctype in LocationType:
+ if plando_loc in locations[loctype]:
+ locations[loctype].remove(plando_loc)
+ plando_locations.append(plando_loc)
+
+ return locations, plando_locations, count
+
+ def any_valuable_locations(self, locations: Dict[LocationType, List[str]]) -> bool:
+ for loctype in LocationType:
+ if len(locations[loctype]) > 0 and self.ctx.location_inclusions[loctype] == LocationInclusion.option_enabled:
+ return True
+ return False
+
+ def get_location_type_title(self, location_type: LocationType) -> str:
+ title = location_type.name.title().replace("_", " ")
+ if self.ctx.location_inclusions[location_type] == LocationInclusion.option_disabled:
+ title += " (Nothing)"
+ elif self.ctx.location_inclusions[location_type] == LocationInclusion.option_resources:
+ title += " (Resources)"
+ else:
+ title += ""
+ return title
+
+def start_gui(context: SC2Context):
+ context.ui = SC2Manager(context)
+ context.ui_task = asyncio.create_task(context.ui.async_run(), name="UI")
+ import pkgutil
+ data = pkgutil.get_data(SC2World.__module__, "Starcraft2.kv").decode()
+ Builder.load_string(data)
diff --git a/worlds/sc2/ItemGroups.py b/worlds/sc2/ItemGroups.py
new file mode 100644
index 000000000000..3a3733044579
--- /dev/null
+++ b/worlds/sc2/ItemGroups.py
@@ -0,0 +1,100 @@
+import typing
+from . import Items, ItemNames
+from .MissionTables import campaign_mission_table, SC2Campaign, SC2Mission
+
+"""
+Item name groups, given to Archipelago and used in YAMLs and /received filtering.
+For non-developers the following will be useful:
+* Items with a bracket get groups named after the unbracketed part
+ * eg. "Advanced Healing AI (Medivac)" is accessible as "Advanced Healing AI"
+ * The exception to this are item names that would be ambiguous (eg. "Resource Efficiency")
+* Item flaggroups get unique groups as well as combined groups for numbered flaggroups
+ * eg. "Unit" contains all units, "Armory" contains "Armory 1" through "Armory 6"
+ * The best place to look these up is at the bottom of Items.py
+* Items that have a parent are grouped together
+ * eg. "Zergling Items" contains all items that have "Zergling" as a parent
+ * These groups do NOT contain the parent item
+ * This currently does not include items with multiple potential parents, like some LotV unit upgrades
+* All items are grouped by their race ("Terran", "Protoss", "Zerg", "Any")
+* Hand-crafted item groups can be found at the bottom of this file
+"""
+
+item_name_groups: typing.Dict[str, typing.List[str]] = {}
+
+# Groups for use in world logic
+item_name_groups["Missions"] = ["Beat " + mission.mission_name for mission in SC2Mission]
+item_name_groups["WoL Missions"] = ["Beat " + mission.mission_name for mission in campaign_mission_table[SC2Campaign.WOL]] + \
+ ["Beat " + mission.mission_name for mission in campaign_mission_table[SC2Campaign.PROPHECY]]
+
+# These item name groups should not show up in documentation
+unlisted_item_name_groups = {
+ "Missions", "WoL Missions"
+}
+
+# Some item names only differ in bracketed parts
+# These items are ambiguous for short-hand name groups
+bracketless_duplicates: typing.Set[str]
+# This is a list of names in ItemNames with bracketed parts removed, for internal use
+_shortened_names = [(name[:name.find(' (')] if '(' in name else name)
+ for name in [ItemNames.__dict__[name] for name in ItemNames.__dir__() if not name.startswith('_')]]
+# Remove the first instance of every short-name from the full item list
+bracketless_duplicates = set(_shortened_names)
+for name in bracketless_duplicates:
+ _shortened_names.remove(name)
+# The remaining short-names are the duplicates
+bracketless_duplicates = set(_shortened_names)
+del _shortened_names
+
+# All items get sorted into their data type
+for item, data in Items.get_full_item_list().items():
+ # Items get assigned to their flaggroup's type
+ item_name_groups.setdefault(data.type, []).append(item)
+ # Numbered flaggroups get sorted into an unnumbered group
+ # Currently supports numbers of one or two digits
+ if data.type[-2:].strip().isnumeric():
+ type_group = data.type[:-2].strip()
+ item_name_groups.setdefault(type_group, []).append(item)
+ # Flaggroups with numbers are unlisted
+ unlisted_item_name_groups.add(data.type)
+ # Items with a bracket get a short-hand name group for ease of use in YAMLs
+ if '(' in item:
+ short_name = item[:item.find(' (')]
+ # Ambiguous short-names are dropped
+ if short_name not in bracketless_duplicates:
+ item_name_groups[short_name] = [item]
+ # Short-name groups are unlisted
+ unlisted_item_name_groups.add(short_name)
+ # Items with a parent get assigned to their parent's group
+ if data.parent_item:
+ # The parent groups need a special name, otherwise they are ambiguous with the parent
+ parent_group = f"{data.parent_item} Items"
+ item_name_groups.setdefault(parent_group, []).append(item)
+ # Parent groups are unlisted
+ unlisted_item_name_groups.add(parent_group)
+ # All items get assigned to their race's group
+ race_group = data.race.name.capitalize()
+ item_name_groups.setdefault(race_group, []).append(item)
+
+
+# Hand-made groups
+item_name_groups["Aiur"] = [
+ ItemNames.ZEALOT, ItemNames.DRAGOON, ItemNames.SENTRY, ItemNames.AVENGER, ItemNames.HIGH_TEMPLAR,
+ ItemNames.IMMORTAL, ItemNames.REAVER,
+ ItemNames.PHOENIX, ItemNames.SCOUT, ItemNames.ARBITER, ItemNames.CARRIER,
+]
+item_name_groups["Nerazim"] = [
+ ItemNames.CENTURION, ItemNames.STALKER, ItemNames.DARK_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.DARK_ARCHON,
+ ItemNames.ANNIHILATOR,
+ ItemNames.CORSAIR, ItemNames.ORACLE, ItemNames.VOID_RAY,
+]
+item_name_groups["Tal'Darim"] = [
+ ItemNames.SUPPLICANT, ItemNames.SLAYER, ItemNames.HAVOC, ItemNames.BLOOD_HUNTER, ItemNames.ASCENDANT,
+ ItemNames.VANGUARD, ItemNames.WRATHWALKER,
+ ItemNames.DESTROYER, ItemNames.MOTHERSHIP,
+ ItemNames.WARP_PRISM_PHASE_BLASTER,
+]
+item_name_groups["Purifier"] = [
+ ItemNames.SENTINEL, ItemNames.ADEPT, ItemNames.INSTIGATOR, ItemNames.ENERGIZER,
+ ItemNames.COLOSSUS, ItemNames.DISRUPTOR,
+ ItemNames.MIRAGE, ItemNames.TEMPEST,
+]
\ No newline at end of file
diff --git a/worlds/sc2/ItemNames.py b/worlds/sc2/ItemNames.py
new file mode 100644
index 000000000000..10c713910311
--- /dev/null
+++ b/worlds/sc2/ItemNames.py
@@ -0,0 +1,661 @@
+"""
+A complete collection of Starcraft 2 item names as strings.
+Users of this data may make some assumptions about the structure of a name:
+* The upgrade for a unit will end with the unit's name in parentheses
+* Weapon / armor upgrades may be grouped by a common prefix specified within this file
+"""
+
+# Terran Units
+MARINE = "Marine"
+MEDIC = "Medic"
+FIREBAT = "Firebat"
+MARAUDER = "Marauder"
+REAPER = "Reaper"
+HELLION = "Hellion"
+VULTURE = "Vulture"
+GOLIATH = "Goliath"
+DIAMONDBACK = "Diamondback"
+SIEGE_TANK = "Siege Tank"
+MEDIVAC = "Medivac"
+WRAITH = "Wraith"
+VIKING = "Viking"
+BANSHEE = "Banshee"
+BATTLECRUISER = "Battlecruiser"
+GHOST = "Ghost"
+SPECTRE = "Spectre"
+THOR = "Thor"
+RAVEN = "Raven"
+SCIENCE_VESSEL = "Science Vessel"
+PREDATOR = "Predator"
+HERCULES = "Hercules"
+# Extended units
+LIBERATOR = "Liberator"
+VALKYRIE = "Valkyrie"
+WIDOW_MINE = "Widow Mine"
+CYCLONE = "Cyclone"
+HERC = "HERC"
+WARHOUND = "Warhound"
+
+# Terran Buildings
+BUNKER = "Bunker"
+MISSILE_TURRET = "Missile Turret"
+SENSOR_TOWER = "Sensor Tower"
+PLANETARY_FORTRESS = "Planetary Fortress"
+PERDITION_TURRET = "Perdition Turret"
+HIVE_MIND_EMULATOR = "Hive Mind Emulator"
+PSI_DISRUPTER = "Psi Disrupter"
+
+# Terran Weapon / Armor Upgrades
+TERRAN_UPGRADE_PREFIX = "Progressive Terran"
+TERRAN_INFANTRY_UPGRADE_PREFIX = f"{TERRAN_UPGRADE_PREFIX} Infantry"
+TERRAN_VEHICLE_UPGRADE_PREFIX = f"{TERRAN_UPGRADE_PREFIX} Vehicle"
+TERRAN_SHIP_UPGRADE_PREFIX = f"{TERRAN_UPGRADE_PREFIX} Ship"
+
+PROGRESSIVE_TERRAN_INFANTRY_WEAPON = f"{TERRAN_INFANTRY_UPGRADE_PREFIX} Weapon"
+PROGRESSIVE_TERRAN_INFANTRY_ARMOR = f"{TERRAN_INFANTRY_UPGRADE_PREFIX} Armor"
+PROGRESSIVE_TERRAN_VEHICLE_WEAPON = f"{TERRAN_VEHICLE_UPGRADE_PREFIX} Weapon"
+PROGRESSIVE_TERRAN_VEHICLE_ARMOR = f"{TERRAN_VEHICLE_UPGRADE_PREFIX} Armor"
+PROGRESSIVE_TERRAN_SHIP_WEAPON = f"{TERRAN_SHIP_UPGRADE_PREFIX} Weapon"
+PROGRESSIVE_TERRAN_SHIP_ARMOR = f"{TERRAN_SHIP_UPGRADE_PREFIX} Armor"
+PROGRESSIVE_TERRAN_WEAPON_UPGRADE = f"{TERRAN_UPGRADE_PREFIX} Weapon Upgrade"
+PROGRESSIVE_TERRAN_ARMOR_UPGRADE = f"{TERRAN_UPGRADE_PREFIX} Armor Upgrade"
+PROGRESSIVE_TERRAN_INFANTRY_UPGRADE = f"{TERRAN_INFANTRY_UPGRADE_PREFIX} Upgrade"
+PROGRESSIVE_TERRAN_VEHICLE_UPGRADE = f"{TERRAN_VEHICLE_UPGRADE_PREFIX} Upgrade"
+PROGRESSIVE_TERRAN_SHIP_UPGRADE = f"{TERRAN_SHIP_UPGRADE_PREFIX} Upgrade"
+PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE = f"{TERRAN_UPGRADE_PREFIX} Weapon/Armor Upgrade"
+
+# Mercenaries
+WAR_PIGS = "War Pigs"
+DEVIL_DOGS = "Devil Dogs"
+HAMMER_SECURITIES = "Hammer Securities"
+SPARTAN_COMPANY = "Spartan Company"
+SIEGE_BREAKERS = "Siege Breakers"
+HELS_ANGELS = "Hel's Angels"
+DUSK_WINGS = "Dusk Wings"
+JACKSONS_REVENGE = "Jackson's Revenge"
+SKIBIS_ANGELS = "Skibi's Angels"
+DEATH_HEADS = "Death Heads"
+WINGED_NIGHTMARES = "Winged Nightmares"
+MIDNIGHT_RIDERS = "Midnight Riders"
+BRYNHILDS = "Brynhilds"
+JOTUN = "Jotun"
+
+# Lab / Global
+ULTRA_CAPACITORS = "Ultra-Capacitors"
+VANADIUM_PLATING = "Vanadium Plating"
+ORBITAL_DEPOTS = "Orbital Depots"
+MICRO_FILTERING = "Micro-Filtering"
+AUTOMATED_REFINERY = "Automated Refinery"
+COMMAND_CENTER_REACTOR = "Command Center Reactor"
+TECH_REACTOR = "Tech Reactor"
+ORBITAL_STRIKE = "Orbital Strike"
+CELLULAR_REACTOR = "Cellular Reactor"
+PROGRESSIVE_REGENERATIVE_BIO_STEEL = "Progressive Regenerative Bio-Steel"
+PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM = "Progressive Fire-Suppression System"
+PROGRESSIVE_ORBITAL_COMMAND = "Progressive Orbital Command"
+STRUCTURE_ARMOR = "Structure Armor"
+HI_SEC_AUTO_TRACKING = "Hi-Sec Auto Tracking"
+ADVANCED_OPTICS = "Advanced Optics"
+ROGUE_FORCES = "Rogue Forces"
+
+# Terran Unit Upgrades
+BANSHEE_HYPERFLIGHT_ROTORS = "Hyperflight Rotors (Banshee)"
+BANSHEE_INTERNAL_TECH_MODULE = "Internal Tech Module (Banshee)"
+BANSHEE_LASER_TARGETING_SYSTEM = "Laser Targeting System (Banshee)"
+BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS = "Progressive Cross-Spectrum Dampeners (Banshee)"
+BANSHEE_SHOCKWAVE_MISSILE_BATTERY = "Shockwave Missile Battery (Banshee)"
+BANSHEE_SHAPED_HULL = "Shaped Hull (Banshee)"
+BANSHEE_ADVANCED_TARGETING_OPTICS = "Advanced Targeting Optics (Banshee)"
+BANSHEE_DISTORTION_BLASTERS = "Distortion Blasters (Banshee)"
+BANSHEE_ROCKET_BARRAGE = "Rocket Barrage (Banshee)"
+BATTLECRUISER_ATX_LASER_BATTERY = "ATX Laser Battery (Battlecruiser)"
+BATTLECRUISER_CLOAK = "Cloak (Battlecruiser)"
+BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX = "Progressive Defensive Matrix (Battlecruiser)"
+BATTLECRUISER_INTERNAL_TECH_MODULE = "Internal Tech Module (Battlecruiser)"
+BATTLECRUISER_PROGRESSIVE_MISSILE_PODS = "Progressive Missile Pods (Battlecruiser)"
+BATTLECRUISER_OPTIMIZED_LOGISTICS = "Optimized Logistics (Battlecruiser)"
+BATTLECRUISER_TACTICAL_JUMP = "Tactical Jump (Battlecruiser)"
+BATTLECRUISER_BEHEMOTH_PLATING = "Behemoth Plating (Battlecruiser)"
+BATTLECRUISER_COVERT_OPS_ENGINES = "Covert Ops Engines (Battlecruiser)"
+BUNKER_NEOSTEEL_BUNKER = "Neosteel Bunker (Bunker)"
+BUNKER_PROJECTILE_ACCELERATOR = "Projectile Accelerator (Bunker)"
+BUNKER_SHRIKE_TURRET = "Shrike Turret (Bunker)"
+BUNKER_FORTIFIED_BUNKER = "Fortified Bunker (Bunker)"
+CYCLONE_MAG_FIELD_ACCELERATORS = "Mag-Field Accelerators (Cyclone)"
+CYCLONE_MAG_FIELD_LAUNCHERS = "Mag-Field Launchers (Cyclone)"
+CYCLONE_RAPID_FIRE_LAUNCHERS = "Rapid Fire Launchers (Cyclone)"
+CYCLONE_TARGETING_OPTICS = "Targeting Optics (Cyclone)"
+CYCLONE_RESOURCE_EFFICIENCY = "Resource Efficiency (Cyclone)"
+CYCLONE_INTERNAL_TECH_MODULE = "Internal Tech Module (Cyclone)"
+DIAMONDBACK_BURST_CAPACITORS = "Burst Capacitors (Diamondback)"
+DIAMONDBACK_HYPERFLUXOR = "Hyperfluxor (Diamondback)"
+DIAMONDBACK_RESOURCE_EFFICIENCY = "Resource Efficiency (Diamondback)"
+DIAMONDBACK_SHAPED_HULL = "Shaped Hull (Diamondback)"
+DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL = "Progressive Tri-Lithium Power Cell (Diamondback)"
+DIAMONDBACK_ION_THRUSTERS = "Ion Thrusters (Diamondback)"
+FIREBAT_INCINERATOR_GAUNTLETS = "Incinerator Gauntlets (Firebat)"
+FIREBAT_JUGGERNAUT_PLATING = "Juggernaut Plating (Firebat)"
+FIREBAT_RESOURCE_EFFICIENCY = "Resource Efficiency (Firebat)"
+FIREBAT_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Firebat)"
+FIREBAT_INFERNAL_PRE_IGNITER = "Infernal Pre-Igniter (Firebat)"
+FIREBAT_KINETIC_FOAM = "Kinetic Foam (Firebat)"
+FIREBAT_NANO_PROJECTORS = "Nano Projectors (Firebat)"
+GHOST_CRIUS_SUIT = "Crius Suit (Ghost)"
+GHOST_EMP_ROUNDS = "EMP Rounds (Ghost)"
+GHOST_LOCKDOWN = "Lockdown (Ghost)"
+GHOST_OCULAR_IMPLANTS = "Ocular Implants (Ghost)"
+GHOST_RESOURCE_EFFICIENCY = "Resource Efficiency (Ghost)"
+GOLIATH_ARES_CLASS_TARGETING_SYSTEM = "Ares-Class Targeting System (Goliath)"
+GOLIATH_JUMP_JETS = "Jump Jets (Goliath)"
+GOLIATH_MULTI_LOCK_WEAPONS_SYSTEM = "Multi-Lock Weapons System (Goliath)"
+GOLIATH_OPTIMIZED_LOGISTICS = "Optimized Logistics (Goliath)"
+GOLIATH_SHAPED_HULL = "Shaped Hull (Goliath)"
+GOLIATH_RESOURCE_EFFICIENCY = "Resource Efficiency (Goliath)"
+GOLIATH_INTERNAL_TECH_MODULE = "Internal Tech Module (Goliath)"
+HELLION_HELLBAT_ASPECT = "Hellbat Aspect (Hellion)"
+HELLION_JUMP_JETS = "Jump Jets (Hellion)"
+HELLION_OPTIMIZED_LOGISTICS = "Optimized Logistics (Hellion)"
+HELLION_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Hellion)"
+HELLION_SMART_SERVOS = "Smart Servos (Hellion)"
+HELLION_THERMITE_FILAMENTS = "Thermite Filaments (Hellion)"
+HELLION_TWIN_LINKED_FLAMETHROWER = "Twin-Linked Flamethrower (Hellion)"
+HELLION_INFERNAL_PLATING = "Infernal Plating (Hellion)"
+HERC_JUGGERNAUT_PLATING = "Juggernaut Plating (HERC)"
+HERC_KINETIC_FOAM = "Kinetic Foam (HERC)"
+HERC_RESOURCE_EFFICIENCY = "Resource Efficiency (HERC)"
+HERCULES_INTERNAL_FUSION_MODULE = "Internal Fusion Module (Hercules)"
+HERCULES_TACTICAL_JUMP = "Tactical Jump (Hercules)"
+LIBERATOR_ADVANCED_BALLISTICS = "Advanced Ballistics (Liberator)"
+LIBERATOR_CLOAK = "Cloak (Liberator)"
+LIBERATOR_LASER_TARGETING_SYSTEM = "Laser Targeting System (Liberator)"
+LIBERATOR_OPTIMIZED_LOGISTICS = "Optimized Logistics (Liberator)"
+LIBERATOR_RAID_ARTILLERY = "Raid Artillery (Liberator)"
+LIBERATOR_SMART_SERVOS = "Smart Servos (Liberator)"
+LIBERATOR_RESOURCE_EFFICIENCY = "Resource Efficiency (Liberator)"
+MARAUDER_CONCUSSIVE_SHELLS = "Concussive Shells (Marauder)"
+MARAUDER_INTERNAL_TECH_MODULE = "Internal Tech Module (Marauder)"
+MARAUDER_KINETIC_FOAM = "Kinetic Foam (Marauder)"
+MARAUDER_LASER_TARGETING_SYSTEM = "Laser Targeting System (Marauder)"
+MARAUDER_MAGRAIL_MUNITIONS = "Magrail Munitions (Marauder)"
+MARAUDER_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Marauder)"
+MARAUDER_JUGGERNAUT_PLATING = "Juggernaut Plating (Marauder)"
+MARINE_COMBAT_SHIELD = "Combat Shield (Marine)"
+MARINE_LASER_TARGETING_SYSTEM = "Laser Targeting System (Marine)"
+MARINE_MAGRAIL_MUNITIONS = "Magrail Munitions (Marine)"
+MARINE_OPTIMIZED_LOGISTICS = "Optimized Logistics (Marine)"
+MARINE_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Marine)"
+MEDIC_ADVANCED_MEDIC_FACILITIES = "Advanced Medic Facilities (Medic)"
+MEDIC_OPTICAL_FLARE = "Optical Flare (Medic)"
+MEDIC_RESOURCE_EFFICIENCY = "Resource Efficiency (Medic)"
+MEDIC_RESTORATION = "Restoration (Medic)"
+MEDIC_STABILIZER_MEDPACKS = "Stabilizer Medpacks (Medic)"
+MEDIC_ADAPTIVE_MEDPACKS = "Adaptive Medpacks (Medic)"
+MEDIC_NANO_PROJECTOR = "Nano Projector (Medic)"
+MEDIVAC_ADVANCED_HEALING_AI = "Advanced Healing AI (Medivac)"
+MEDIVAC_AFTERBURNERS = "Afterburners (Medivac)"
+MEDIVAC_EXPANDED_HULL = "Expanded Hull (Medivac)"
+MEDIVAC_RAPID_DEPLOYMENT_TUBE = "Rapid Deployment Tube (Medivac)"
+MEDIVAC_SCATTER_VEIL = "Scatter Veil (Medivac)"
+MEDIVAC_ADVANCED_CLOAKING_FIELD = "Advanced Cloaking Field (Medivac)"
+MISSILE_TURRET_HELLSTORM_BATTERIES = "Hellstorm Batteries (Missile Turret)"
+MISSILE_TURRET_TITANIUM_HOUSING = "Titanium Housing (Missile Turret)"
+PLANETARY_FORTRESS_PROGRESSIVE_AUGMENTED_THRUSTERS = "Progressive Augmented Thrusters (Planetary Fortress)"
+PLANETARY_FORTRESS_ADVANCED_TARGETING = "Advanced Targeting (Planetary Fortress)"
+PREDATOR_RESOURCE_EFFICIENCY = "Resource Efficiency (Predator)"
+PREDATOR_CLOAK = "Cloak (Predator)"
+PREDATOR_CHARGE = "Charge (Predator)"
+PREDATOR_PREDATOR_S_FURY = "Predator's Fury (Predator)"
+RAVEN_ANTI_ARMOR_MISSILE = "Anti-Armor Missile (Raven)"
+RAVEN_BIO_MECHANICAL_REPAIR_DRONE = "Bio Mechanical Repair Drone (Raven)"
+RAVEN_HUNTER_SEEKER_WEAPON = "Hunter-Seeker Weapon (Raven)"
+RAVEN_INTERFERENCE_MATRIX = "Interference Matrix (Raven)"
+RAVEN_INTERNAL_TECH_MODULE = "Internal Tech Module (Raven)"
+RAVEN_RAILGUN_TURRET = "Railgun Turret (Raven)"
+RAVEN_SPIDER_MINES = "Spider Mines (Raven)"
+RAVEN_RESOURCE_EFFICIENCY = "Resource Efficiency (Raven)"
+RAVEN_DURABLE_MATERIALS = "Durable Materials (Raven)"
+REAPER_ADVANCED_CLOAKING_FIELD = "Advanced Cloaking Field (Reaper)"
+REAPER_COMBAT_DRUGS = "Combat Drugs (Reaper)"
+REAPER_G4_CLUSTERBOMB = "G-4 Clusterbomb (Reaper)"
+REAPER_LASER_TARGETING_SYSTEM = "Laser Targeting System (Reaper)"
+REAPER_PROGRESSIVE_STIMPACK = "Progressive Stimpack (Reaper)"
+REAPER_SPIDER_MINES = "Spider Mines (Reaper)"
+REAPER_U238_ROUNDS = "U-238 Rounds (Reaper)"
+REAPER_JET_PACK_OVERDRIVE = "Jet Pack Overdrive (Reaper)"
+SCIENCE_VESSEL_DEFENSIVE_MATRIX = "Defensive Matrix (Science Vessel)"
+SCIENCE_VESSEL_EMP_SHOCKWAVE = "EMP Shockwave (Science Vessel)"
+SCIENCE_VESSEL_IMPROVED_NANO_REPAIR = "Improved Nano-Repair (Science Vessel)"
+SCIENCE_VESSEL_ADVANCED_AI_SYSTEMS = "Advanced AI Systems (Science Vessel)"
+SCV_ADVANCED_CONSTRUCTION = "Advanced Construction (SCV)"
+SCV_DUAL_FUSION_WELDERS = "Dual-Fusion Welders (SCV)"
+SCV_HOSTILE_ENVIRONMENT_ADAPTATION = "Hostile Environment Adaptation (SCV)"
+SIEGE_TANK_ADVANCED_SIEGE_TECH = "Advanced Siege Tech (Siege Tank)"
+SIEGE_TANK_GRADUATING_RANGE = "Graduating Range (Siege Tank)"
+SIEGE_TANK_INTERNAL_TECH_MODULE = "Internal Tech Module (Siege Tank)"
+SIEGE_TANK_JUMP_JETS = "Jump Jets (Siege Tank)"
+SIEGE_TANK_LASER_TARGETING_SYSTEM = "Laser Targeting System (Siege Tank)"
+SIEGE_TANK_MAELSTROM_ROUNDS = "Maelstrom Rounds (Siege Tank)"
+SIEGE_TANK_SHAPED_BLAST = "Shaped Blast (Siege Tank)"
+SIEGE_TANK_SMART_SERVOS = "Smart Servos (Siege Tank)"
+SIEGE_TANK_SPIDER_MINES = "Spider Mines (Siege Tank)"
+SIEGE_TANK_SHAPED_HULL = "Shaped Hull (Siege Tank)"
+SIEGE_TANK_RESOURCE_EFFICIENCY = "Resource Efficiency (Siege Tank)"
+SPECTRE_IMPALER_ROUNDS = "Impaler Rounds (Spectre)"
+SPECTRE_NYX_CLASS_CLOAKING_MODULE = "Nyx-Class Cloaking Module (Spectre)"
+SPECTRE_PSIONIC_LASH = "Psionic Lash (Spectre)"
+SPECTRE_RESOURCE_EFFICIENCY = "Resource Efficiency (Spectre)"
+SPIDER_MINE_CERBERUS_MINE = "Cerberus Mine (Spider Mine)"
+SPIDER_MINE_HIGH_EXPLOSIVE_MUNITION = "High Explosive Munition (Spider Mine)"
+THOR_330MM_BARRAGE_CANNON = "330mm Barrage Cannon (Thor)"
+THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL = "Progressive Immortality Protocol (Thor)"
+THOR_PROGRESSIVE_HIGH_IMPACT_PAYLOAD = "Progressive High Impact Payload (Thor)"
+THOR_BUTTON_WITH_A_SKULL_ON_IT = "Button With a Skull on It (Thor)"
+THOR_LASER_TARGETING_SYSTEM = "Laser Targeting System (Thor)"
+THOR_LARGE_SCALE_FIELD_CONSTRUCTION = "Large Scale Field Construction (Thor)"
+VALKYRIE_AFTERBURNERS = "Afterburners (Valkyrie)"
+VALKYRIE_FLECHETTE_MISSILES = "Flechette Missiles (Valkyrie)"
+VALKYRIE_ENHANCED_CLUSTER_LAUNCHERS = "Enhanced Cluster Launchers (Valkyrie)"
+VALKYRIE_SHAPED_HULL = "Shaped Hull (Valkyrie)"
+VALKYRIE_LAUNCHING_VECTOR_COMPENSATOR = "Launching Vector Compensator (Valkyrie)"
+VALKYRIE_RESOURCE_EFFICIENCY = "Resource Efficiency (Valkyrie)"
+VIKING_ANTI_MECHANICAL_MUNITION = "Anti-Mechanical Munition (Viking)"
+VIKING_PHOBOS_CLASS_WEAPONS_SYSTEM = "Phobos-Class Weapons System (Viking)"
+VIKING_RIPWAVE_MISSILES = "Ripwave Missiles (Viking)"
+VIKING_SMART_SERVOS = "Smart Servos (Viking)"
+VIKING_SHREDDER_ROUNDS = "Shredder Rounds (Viking)"
+VIKING_WILD_MISSILES = "W.I.L.D. Missiles (Viking)"
+VULTURE_AUTO_LAUNCHERS = "Auto Launchers (Vulture)"
+VULTURE_ION_THRUSTERS = "Ion Thrusters (Vulture)"
+VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE = "Progressive Replenishable Magazine (Vulture)"
+VULTURE_AUTO_REPAIR = "Auto-Repair (Vulture)"
+WARHOUND_RESOURCE_EFFICIENCY = "Resource Efficiency (Warhound)"
+WARHOUND_REINFORCED_PLATING = "Reinforced Plating (Warhound)"
+WIDOW_MINE_BLACK_MARKET_LAUNCHERS = "Black Market Launchers (Widow Mine)"
+WIDOW_MINE_CONCEALMENT = "Concealment (Widow Mine)"
+WIDOW_MINE_DRILLING_CLAWS = "Drilling Claws (Widow Mine)"
+WIDOW_MINE_EXECUTIONER_MISSILES = "Executioner Missiles (Widow Mine)"
+WRAITH_ADVANCED_LASER_TECHNOLOGY = "Advanced Laser Technology (Wraith)"
+WRAITH_DISPLACEMENT_FIELD = "Displacement Field (Wraith)"
+WRAITH_PROGRESSIVE_TOMAHAWK_POWER_CELLS = "Progressive Tomahawk Power Cells (Wraith)"
+WRAITH_TRIGGER_OVERRIDE = "Trigger Override (Wraith)"
+WRAITH_INTERNAL_TECH_MODULE = "Internal Tech Module (Wraith)"
+WRAITH_RESOURCE_EFFICIENCY = "Resource Efficiency (Wraith)"
+
+# Nova
+NOVA_GHOST_VISOR = "Ghost Visor (Nova Equipment)"
+NOVA_RANGEFINDER_OCULUS = "Rangefinder Oculus (Nova Equipment)"
+NOVA_DOMINATION = "Domination (Nova Ability)"
+NOVA_BLINK = "Blink (Nova Ability)"
+NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE = "Progressive Stealth Suit Module (Nova Suit Module)"
+NOVA_ENERGY_SUIT_MODULE = "Energy Suit Module (Nova Suit Module)"
+NOVA_ARMORED_SUIT_MODULE = "Armored Suit Module (Nova Suit Module)"
+NOVA_JUMP_SUIT_MODULE = "Jump Suit Module (Nova Suit Module)"
+NOVA_C20A_CANISTER_RIFLE = "C20A Canister Rifle (Nova Weapon)"
+NOVA_HELLFIRE_SHOTGUN = "Hellfire Shotgun (Nova Weapon)"
+NOVA_PLASMA_RIFLE = "Plasma Rifle (Nova Weapon)"
+NOVA_MONOMOLECULAR_BLADE = "Monomolecular Blade (Nova Weapon)"
+NOVA_BLAZEFIRE_GUNBLADE = "Blazefire Gunblade (Nova Weapon)"
+NOVA_STIM_INFUSION = "Stim Infusion (Nova Gadget)"
+NOVA_PULSE_GRENADES = "Pulse Grenades (Nova Gadget)"
+NOVA_FLASHBANG_GRENADES = "Flashbang Grenades (Nova Gadget)"
+NOVA_IONIC_FORCE_FIELD = "Ionic Force Field (Nova Gadget)"
+NOVA_HOLO_DECOY = "Holo Decoy (Nova Gadget)"
+NOVA_NUKE = "Tac Nuke Strike (Nova Ability)"
+
+# Zerg Units
+ZERGLING = "Zergling"
+SWARM_QUEEN = "Swarm Queen"
+ROACH = "Roach"
+HYDRALISK = "Hydralisk"
+ABERRATION = "Aberration"
+MUTALISK = "Mutalisk"
+SWARM_HOST = "Swarm Host"
+INFESTOR = "Infestor"
+ULTRALISK = "Ultralisk"
+CORRUPTOR = "Corruptor"
+SCOURGE = "Scourge"
+BROOD_QUEEN = "Brood Queen"
+DEFILER = "Defiler"
+
+# Zerg Buildings
+SPORE_CRAWLER = "Spore Crawler"
+SPINE_CRAWLER = "Spine Crawler"
+
+# Zerg Weapon / Armor Upgrades
+ZERG_UPGRADE_PREFIX = "Progressive Zerg"
+ZERG_FLYER_UPGRADE_PREFIX = f"{ZERG_UPGRADE_PREFIX} Flyer"
+
+PROGRESSIVE_ZERG_MELEE_ATTACK = f"{ZERG_UPGRADE_PREFIX} Melee Attack"
+PROGRESSIVE_ZERG_MISSILE_ATTACK = f"{ZERG_UPGRADE_PREFIX} Missile Attack"
+PROGRESSIVE_ZERG_GROUND_CARAPACE = f"{ZERG_UPGRADE_PREFIX} Ground Carapace"
+PROGRESSIVE_ZERG_FLYER_ATTACK = f"{ZERG_FLYER_UPGRADE_PREFIX} Attack"
+PROGRESSIVE_ZERG_FLYER_CARAPACE = f"{ZERG_FLYER_UPGRADE_PREFIX} Carapace"
+PROGRESSIVE_ZERG_WEAPON_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Weapon Upgrade"
+PROGRESSIVE_ZERG_ARMOR_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Armor Upgrade"
+PROGRESSIVE_ZERG_GROUND_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Ground Upgrade"
+PROGRESSIVE_ZERG_FLYER_UPGRADE = f"{ZERG_FLYER_UPGRADE_PREFIX} Upgrade"
+PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE = f"{ZERG_UPGRADE_PREFIX} Weapon/Armor Upgrade"
+
+# Zerg Unit Upgrades
+ZERGLING_HARDENED_CARAPACE = "Hardened Carapace (Zergling)"
+ZERGLING_ADRENAL_OVERLOAD = "Adrenal Overload (Zergling)"
+ZERGLING_METABOLIC_BOOST = "Metabolic Boost (Zergling)"
+ZERGLING_SHREDDING_CLAWS = "Shredding Claws (Zergling)"
+ROACH_HYDRIODIC_BILE = "Hydriodic Bile (Roach)"
+ROACH_ADAPTIVE_PLATING = "Adaptive Plating (Roach)"
+ROACH_TUNNELING_CLAWS = "Tunneling Claws (Roach)"
+ROACH_GLIAL_RECONSTITUTION = "Glial Reconstitution (Roach)"
+ROACH_ORGANIC_CARAPACE = "Organic Carapace (Roach)"
+HYDRALISK_FRENZY = "Frenzy (Hydralisk)"
+HYDRALISK_ANCILLARY_CARAPACE = "Ancillary Carapace (Hydralisk)"
+HYDRALISK_GROOVED_SPINES = "Grooved Spines (Hydralisk)"
+HYDRALISK_MUSCULAR_AUGMENTS = "Muscular Augments (Hydralisk)"
+HYDRALISK_RESOURCE_EFFICIENCY = "Resource Efficiency (Hydralisk)"
+BANELING_CORROSIVE_ACID = "Corrosive Acid (Baneling)"
+BANELING_RUPTURE = "Rupture (Baneling)"
+BANELING_REGENERATIVE_ACID = "Regenerative Acid (Baneling)"
+BANELING_CENTRIFUGAL_HOOKS = "Centrifugal Hooks (Baneling)"
+BANELING_TUNNELING_JAWS = "Tunneling Jaws (Baneling)"
+BANELING_RAPID_METAMORPH = "Rapid Metamorph (Baneling)"
+MUTALISK_VICIOUS_GLAIVE = "Vicious Glaive (Mutalisk)"
+MUTALISK_RAPID_REGENERATION = "Rapid Regeneration (Mutalisk)"
+MUTALISK_SUNDERING_GLAIVE = "Sundering Glaive (Mutalisk)"
+MUTALISK_SEVERING_GLAIVE = "Severing Glaive (Mutalisk)"
+MUTALISK_AERODYNAMIC_GLAIVE_SHAPE = "Aerodynamic Glaive Shape (Mutalisk)"
+SWARM_HOST_BURROW = "Burrow (Swarm Host)"
+SWARM_HOST_RAPID_INCUBATION = "Rapid Incubation (Swarm Host)"
+SWARM_HOST_PRESSURIZED_GLANDS = "Pressurized Glands (Swarm Host)"
+SWARM_HOST_LOCUST_METABOLIC_BOOST = "Locust Metabolic Boost (Swarm Host)"
+SWARM_HOST_ENDURING_LOCUSTS = "Enduring Locusts (Swarm Host)"
+SWARM_HOST_ORGANIC_CARAPACE = "Organic Carapace (Swarm Host)"
+SWARM_HOST_RESOURCE_EFFICIENCY = "Resource Efficiency (Swarm Host)"
+ULTRALISK_BURROW_CHARGE = "Burrow Charge (Ultralisk)"
+ULTRALISK_TISSUE_ASSIMILATION = "Tissue Assimilation (Ultralisk)"
+ULTRALISK_MONARCH_BLADES = "Monarch Blades (Ultralisk)"
+ULTRALISK_ANABOLIC_SYNTHESIS = "Anabolic Synthesis (Ultralisk)"
+ULTRALISK_CHITINOUS_PLATING = "Chitinous Plating (Ultralisk)"
+ULTRALISK_ORGANIC_CARAPACE = "Organic Carapace (Ultralisk)"
+ULTRALISK_RESOURCE_EFFICIENCY = "Resource Efficiency (Ultralisk)"
+CORRUPTOR_CORRUPTION = "Corruption (Corruptor)"
+CORRUPTOR_CAUSTIC_SPRAY = "Caustic Spray (Corruptor)"
+SCOURGE_VIRULENT_SPORES = "Virulent Spores (Scourge)"
+SCOURGE_RESOURCE_EFFICIENCY = "Resource Efficiency (Scourge)"
+SCOURGE_SWARM_SCOURGE = "Swarm Scourge (Scourge)"
+DEVOURER_CORROSIVE_SPRAY = "Corrosive Spray (Devourer)"
+DEVOURER_GAPING_MAW = "Gaping Maw (Devourer)"
+DEVOURER_IMPROVED_OSMOSIS = "Improved Osmosis (Devourer)"
+DEVOURER_PRESCIENT_SPORES = "Prescient Spores (Devourer)"
+GUARDIAN_PROLONGED_DISPERSION = "Prolonged Dispersion (Guardian)"
+GUARDIAN_PRIMAL_ADAPTATION = "Primal Adaptation (Guardian)"
+GUARDIAN_SORONAN_ACID = "Soronan Acid (Guardian)"
+IMPALER_ADAPTIVE_TALONS = "Adaptive Talons (Impaler)"
+IMPALER_SECRETION_GLANDS = "Secretion Glands (Impaler)"
+IMPALER_HARDENED_TENTACLE_SPINES = "Hardened Tentacle Spines (Impaler)"
+LURKER_SEISMIC_SPINES = "Seismic Spines (Lurker)"
+LURKER_ADAPTED_SPINES = "Adapted Spines (Lurker)"
+RAVAGER_POTENT_BILE = "Potent Bile (Ravager)"
+RAVAGER_BLOATED_BILE_DUCTS = "Bloated Bile Ducts (Ravager)"
+RAVAGER_DEEP_TUNNEL = "Deep Tunnel (Ravager)"
+VIPER_PARASITIC_BOMB = "Parasitic Bomb (Viper)"
+VIPER_PARALYTIC_BARBS = "Paralytic Barbs (Viper)"
+VIPER_VIRULENT_MICROBES = "Virulent Microbes (Viper)"
+BROOD_LORD_POROUS_CARTILAGE = "Porous Cartilage (Brood Lord)"
+BROOD_LORD_EVOLVED_CARAPACE = "Evolved Carapace (Brood Lord)"
+BROOD_LORD_SPLITTER_MITOSIS = "Splitter Mitosis (Brood Lord)"
+BROOD_LORD_RESOURCE_EFFICIENCY = "Resource Efficiency (Brood Lord)"
+INFESTOR_INFESTED_TERRAN = "Infested Terran (Infestor)"
+INFESTOR_MICROBIAL_SHROUD = "Microbial Shroud (Infestor)"
+SWARM_QUEEN_SPAWN_LARVAE = "Spawn Larvae (Swarm Queen)"
+SWARM_QUEEN_DEEP_TUNNEL = "Deep Tunnel (Swarm Queen)"
+SWARM_QUEEN_ORGANIC_CARAPACE = "Organic Carapace (Swarm Queen)"
+SWARM_QUEEN_BIO_MECHANICAL_TRANSFUSION = "Bio-Mechanical Transfusion (Swarm Queen)"
+SWARM_QUEEN_RESOURCE_EFFICIENCY = "Resource Efficiency (Swarm Queen)"
+SWARM_QUEEN_INCUBATOR_CHAMBER = "Incubator Chamber (Swarm Queen)"
+BROOD_QUEEN_FUNGAL_GROWTH = "Fungal Growth (Brood Queen)"
+BROOD_QUEEN_ENSNARE = "Ensnare (Brood Queen)"
+BROOD_QUEEN_ENHANCED_MITOCHONDRIA = "Enhanced Mitochondria (Brood Queen)"
+
+# Zerg Strains
+ZERGLING_RAPTOR_STRAIN = "Raptor Strain (Zergling)"
+ZERGLING_SWARMLING_STRAIN = "Swarmling Strain (Zergling)"
+ROACH_VILE_STRAIN = "Vile Strain (Roach)"
+ROACH_CORPSER_STRAIN = "Corpser Strain (Roach)"
+BANELING_SPLITTER_STRAIN = "Splitter Strain (Baneling)"
+BANELING_HUNTER_STRAIN = "Hunter Strain (Baneling)"
+SWARM_HOST_CARRION_STRAIN = "Carrion Strain (Swarm Host)"
+SWARM_HOST_CREEPER_STRAIN = "Creeper Strain (Swarm Host)"
+ULTRALISK_NOXIOUS_STRAIN = "Noxious Strain (Ultralisk)"
+ULTRALISK_TORRASQUE_STRAIN = "Torrasque Strain (Ultralisk)"
+
+# Morphs
+ZERGLING_BANELING_ASPECT = "Baneling Aspect (Zergling)"
+HYDRALISK_IMPALER_ASPECT = "Impaler Aspect (Hydralisk)"
+HYDRALISK_LURKER_ASPECT = "Lurker Aspect (Hydralisk)"
+MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT = "Brood Lord Aspect (Mutalisk/Corruptor)"
+MUTALISK_CORRUPTOR_VIPER_ASPECT = "Viper Aspect (Mutalisk/Corruptor)"
+MUTALISK_CORRUPTOR_GUARDIAN_ASPECT = "Guardian Aspect (Mutalisk/Corruptor)"
+MUTALISK_CORRUPTOR_DEVOURER_ASPECT = "Devourer Aspect (Mutalisk/Corruptor)"
+ROACH_RAVAGER_ASPECT = "Ravager Aspect (Roach)"
+
+# Zerg Mercs
+INFESTED_MEDICS = "Infested Medics"
+INFESTED_SIEGE_TANKS = "Infested Siege Tanks"
+INFESTED_BANSHEES = "Infested Banshees"
+
+# Kerrigan Upgrades
+KERRIGAN_KINETIC_BLAST = "Kinetic Blast (Kerrigan Tier 1)"
+KERRIGAN_HEROIC_FORTITUDE = "Heroic Fortitude (Kerrigan Tier 1)"
+KERRIGAN_LEAPING_STRIKE = "Leaping Strike (Kerrigan Tier 1)"
+KERRIGAN_CRUSHING_GRIP = "Crushing Grip (Kerrigan Tier 2)"
+KERRIGAN_CHAIN_REACTION = "Chain Reaction (Kerrigan Tier 2)"
+KERRIGAN_PSIONIC_SHIFT = "Psionic Shift (Kerrigan Tier 2)"
+KERRIGAN_WILD_MUTATION = "Wild Mutation (Kerrigan Tier 4)"
+KERRIGAN_SPAWN_BANELINGS = "Spawn Banelings (Kerrigan Tier 4)"
+KERRIGAN_MEND = "Mend (Kerrigan Tier 4)"
+KERRIGAN_INFEST_BROODLINGS = "Infest Broodlings (Kerrigan Tier 6)"
+KERRIGAN_FURY = "Fury (Kerrigan Tier 6)"
+KERRIGAN_ABILITY_EFFICIENCY = "Ability Efficiency (Kerrigan Tier 6)"
+KERRIGAN_APOCALYPSE = "Apocalypse (Kerrigan Tier 7)"
+KERRIGAN_SPAWN_LEVIATHAN = "Spawn Leviathan (Kerrigan Tier 7)"
+KERRIGAN_DROP_PODS = "Drop-Pods (Kerrigan Tier 7)"
+KERRIGAN_PRIMAL_FORM = "Primal Form (Kerrigan)"
+
+# Misc Upgrades
+KERRIGAN_ZERGLING_RECONSTITUTION = "Zergling Reconstitution (Kerrigan Tier 3)"
+KERRIGAN_IMPROVED_OVERLORDS = "Improved Overlords (Kerrigan Tier 3)"
+KERRIGAN_AUTOMATED_EXTRACTORS = "Automated Extractors (Kerrigan Tier 3)"
+KERRIGAN_TWIN_DRONES = "Twin Drones (Kerrigan Tier 5)"
+KERRIGAN_MALIGNANT_CREEP = "Malignant Creep (Kerrigan Tier 5)"
+KERRIGAN_VESPENE_EFFICIENCY = "Vespene Efficiency (Kerrigan Tier 5)"
+OVERLORD_VENTRAL_SACS = "Ventral Sacs (Overlord)"
+
+# Kerrigan Levels
+KERRIGAN_LEVELS_1 = "1 Kerrigan Level"
+KERRIGAN_LEVELS_2 = "2 Kerrigan Levels"
+KERRIGAN_LEVELS_3 = "3 Kerrigan Levels"
+KERRIGAN_LEVELS_4 = "4 Kerrigan Levels"
+KERRIGAN_LEVELS_5 = "5 Kerrigan Levels"
+KERRIGAN_LEVELS_6 = "6 Kerrigan Levels"
+KERRIGAN_LEVELS_7 = "7 Kerrigan Levels"
+KERRIGAN_LEVELS_8 = "8 Kerrigan Levels"
+KERRIGAN_LEVELS_9 = "9 Kerrigan Levels"
+KERRIGAN_LEVELS_10 = "10 Kerrigan Levels"
+KERRIGAN_LEVELS_14 = "14 Kerrigan Levels"
+KERRIGAN_LEVELS_35 = "35 Kerrigan Levels"
+KERRIGAN_LEVELS_70 = "70 Kerrigan Levels"
+
+# Protoss Units
+ZEALOT = "Zealot"
+STALKER = "Stalker"
+HIGH_TEMPLAR = "High Templar"
+DARK_TEMPLAR = "Dark Templar"
+IMMORTAL = "Immortal"
+COLOSSUS = "Colossus"
+PHOENIX = "Phoenix"
+VOID_RAY = "Void Ray"
+CARRIER = "Carrier"
+OBSERVER = "Observer"
+CENTURION = "Centurion"
+SENTINEL = "Sentinel"
+SUPPLICANT = "Supplicant"
+INSTIGATOR = "Instigator"
+SLAYER = "Slayer"
+SENTRY = "Sentry"
+ENERGIZER = "Energizer"
+HAVOC = "Havoc"
+SIGNIFIER = "Signifier"
+ASCENDANT = "Ascendant"
+AVENGER = "Avenger"
+BLOOD_HUNTER = "Blood Hunter"
+DRAGOON = "Dragoon"
+DARK_ARCHON = "Dark Archon"
+ADEPT = "Adept"
+WARP_PRISM = "Warp Prism"
+ANNIHILATOR = "Annihilator"
+VANGUARD = "Vanguard"
+WRATHWALKER = "Wrathwalker"
+REAVER = "Reaver"
+DISRUPTOR = "Disruptor"
+MIRAGE = "Mirage"
+CORSAIR = "Corsair"
+DESTROYER = "Destroyer"
+SCOUT = "Scout"
+TEMPEST = "Tempest"
+MOTHERSHIP = "Mothership"
+ARBITER = "Arbiter"
+ORACLE = "Oracle"
+
+# Upgrades
+PROTOSS_UPGRADE_PREFIX = "Progressive Protoss"
+PROTOSS_GROUND_UPGRADE_PREFIX = f"{PROTOSS_UPGRADE_PREFIX} Ground"
+PROTOSS_AIR_UPGRADE_PREFIX = f"{PROTOSS_UPGRADE_PREFIX} Air"
+PROGRESSIVE_PROTOSS_GROUND_WEAPON = f"{PROTOSS_GROUND_UPGRADE_PREFIX} Weapon"
+PROGRESSIVE_PROTOSS_GROUND_ARMOR = f"{PROTOSS_GROUND_UPGRADE_PREFIX} Armor"
+PROGRESSIVE_PROTOSS_SHIELDS = f"{PROTOSS_UPGRADE_PREFIX} Shields"
+PROGRESSIVE_PROTOSS_AIR_WEAPON = f"{PROTOSS_AIR_UPGRADE_PREFIX} Weapon"
+PROGRESSIVE_PROTOSS_AIR_ARMOR = f"{PROTOSS_AIR_UPGRADE_PREFIX} Armor"
+PROGRESSIVE_PROTOSS_WEAPON_UPGRADE = f"{PROTOSS_UPGRADE_PREFIX} Weapon Upgrade"
+PROGRESSIVE_PROTOSS_ARMOR_UPGRADE = f"{PROTOSS_UPGRADE_PREFIX} Armor Upgrade"
+PROGRESSIVE_PROTOSS_GROUND_UPGRADE = f"{PROTOSS_GROUND_UPGRADE_PREFIX} Upgrade"
+PROGRESSIVE_PROTOSS_AIR_UPGRADE = f"{PROTOSS_AIR_UPGRADE_PREFIX} Upgrade"
+PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE = f"{PROTOSS_UPGRADE_PREFIX} Weapon/Armor Upgrade"
+
+# Buildings
+PHOTON_CANNON = "Photon Cannon"
+KHAYDARIN_MONOLITH = "Khaydarin Monolith"
+SHIELD_BATTERY = "Shield Battery"
+
+# Unit Upgrades
+SUPPLICANT_BLOOD_SHIELD = "Blood Shield (Supplicant)"
+SUPPLICANT_SOUL_AUGMENTATION = "Soul Augmentation (Supplicant)"
+SUPPLICANT_SHIELD_REGENERATION = "Shield Regeneration (Supplicant)"
+ADEPT_SHOCKWAVE = "Shockwave (Adept)"
+ADEPT_RESONATING_GLAIVES = "Resonating Glaives (Adept)"
+ADEPT_PHASE_BULWARK = "Phase Bulwark (Adept)"
+STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES = "Disintegrating Particles (Stalker/Instigator/Slayer)"
+STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION = "Particle Reflection (Stalker/Instigator/Slayer)"
+DRAGOON_HIGH_IMPACT_PHASE_DISRUPTORS = "High Impact Phase Disruptor (Dragoon)"
+DRAGOON_TRILLIC_COMPRESSION_SYSTEM = "Trillic Compression System (Dragoon)"
+DRAGOON_SINGULARITY_CHARGE = "Singularity Charge (Dragoon)"
+DRAGOON_ENHANCED_STRIDER_SERVOS = "Enhanced Strider Servos (Dragoon)"
+SCOUT_COMBAT_SENSOR_ARRAY = "Combat Sensor Array (Scout)"
+SCOUT_APIAL_SENSORS = "Apial Sensors (Scout)"
+SCOUT_GRAVITIC_THRUSTERS = "Gravitic Thrusters (Scout)"
+SCOUT_ADVANCED_PHOTON_BLASTERS = "Advanced Photon Blasters (Scout)"
+TEMPEST_TECTONIC_DESTABILIZERS = "Tectonic Destabilizers (Tempest)"
+TEMPEST_QUANTIC_REACTOR = "Quantic Reactor (Tempest)"
+TEMPEST_GRAVITY_SLING = "Gravity Sling (Tempest)"
+PHOENIX_MIRAGE_IONIC_WAVELENGTH_FLUX = "Ionic Wavelength Flux (Phoenix/Mirage)"
+PHOENIX_MIRAGE_ANION_PULSE_CRYSTALS = "Anion Pulse-Crystals (Phoenix/Mirage)"
+CORSAIR_STEALTH_DRIVE = "Stealth Drive (Corsair)"
+CORSAIR_ARGUS_JEWEL = "Argus Jewel (Corsair)"
+CORSAIR_SUSTAINING_DISRUPTION = "Sustaining Disruption (Corsair)"
+CORSAIR_NEUTRON_SHIELDS = "Neutron Shields (Corsair)"
+ORACLE_STEALTH_DRIVE = "Stealth Drive (Oracle)"
+ORACLE_STASIS_CALIBRATION = "Stasis Calibration (Oracle)"
+ORACLE_TEMPORAL_ACCELERATION_BEAM = "Temporal Acceleration Beam (Oracle)"
+ARBITER_CHRONOSTATIC_REINFORCEMENT = "Chronostatic Reinforcement (Arbiter)"
+ARBITER_KHAYDARIN_CORE = "Khaydarin Core (Arbiter)"
+ARBITER_SPACETIME_ANCHOR = "Spacetime Anchor (Arbiter)"
+ARBITER_RESOURCE_EFFICIENCY = "Resource Efficiency (Arbiter)"
+ARBITER_ENHANCED_CLOAK_FIELD = "Enhanced Cloak Field (Arbiter)"
+CARRIER_GRAVITON_CATAPULT = "Graviton Catapult (Carrier)"
+CARRIER_HULL_OF_PAST_GLORIES = "Hull of Past Glories (Carrier)"
+VOID_RAY_DESTROYER_FLUX_VANES = "Flux Vanes (Void Ray/Destroyer)"
+DESTROYER_REFORGED_BLOODSHARD_CORE = "Reforged Bloodshard Core (Destroyer)"
+WARP_PRISM_GRAVITIC_DRIVE = "Gravitic Drive (Warp Prism)"
+WARP_PRISM_PHASE_BLASTER = "Phase Blaster (Warp Prism)"
+WARP_PRISM_WAR_CONFIGURATION = "War Configuration (Warp Prism)"
+OBSERVER_GRAVITIC_BOOSTERS = "Gravitic Boosters (Observer)"
+OBSERVER_SENSOR_ARRAY = "Sensor Array (Observer)"
+REAVER_SCARAB_DAMAGE = "Scarab Damage (Reaver)"
+REAVER_SOLARITE_PAYLOAD = "Solarite Payload (Reaver)"
+REAVER_REAVER_CAPACITY = "Reaver Capacity (Reaver)"
+REAVER_RESOURCE_EFFICIENCY = "Resource Efficiency (Reaver)"
+VANGUARD_AGONY_LAUNCHERS = "Agony Launchers (Vanguard)"
+VANGUARD_MATTER_DISPERSION = "Matter Dispersion (Vanguard)"
+IMMORTAL_ANNIHILATOR_SINGULARITY_CHARGE = "Singularity Charge (Immortal/Annihilator)"
+IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING_MECHANICS = "Advanced Targeting Mechanics (Immortal/Annihilator)"
+COLOSSUS_PACIFICATION_PROTOCOL = "Pacification Protocol (Colossus)"
+WRATHWALKER_RAPID_POWER_CYCLING = "Rapid Power Cycling (Wrathwalker)"
+WRATHWALKER_EYE_OF_WRATH = "Eye of Wrath (Wrathwalker)"
+DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHROUD_OF_ADUN = "Shroud of Adun (Dark Templar/Avenger/Blood Hunter)"
+DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHADOW_GUARD_TRAINING = "Shadow Guard Training (Dark Templar/Avenger/Blood Hunter)"
+DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_BLINK = "Blink (Dark Templar/Avenger/Blood Hunter)"
+DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_RESOURCE_EFFICIENCY = "Resource Efficiency (Dark Templar/Avenger/Blood Hunter)"
+DARK_TEMPLAR_DARK_ARCHON_MELD = "Dark Archon Meld (Dark Templar)"
+HIGH_TEMPLAR_SIGNIFIER_UNSHACKLED_PSIONIC_STORM = "Unshackled Psionic Storm (High Templar/Signifier)"
+HIGH_TEMPLAR_SIGNIFIER_HALLUCINATION = "Hallucination (High Templar/Signifier)"
+HIGH_TEMPLAR_SIGNIFIER_KHAYDARIN_AMULET = "Khaydarin Amulet (High Templar/Signifier)"
+ARCHON_HIGH_ARCHON = "High Archon (Archon)"
+DARK_ARCHON_FEEDBACK = "Feedback (Dark Archon)"
+DARK_ARCHON_MAELSTROM = "Maelstrom (Dark Archon)"
+DARK_ARCHON_ARGUS_TALISMAN = "Argus Talisman (Dark Archon)"
+ASCENDANT_POWER_OVERWHELMING = "Power Overwhelming (Ascendant)"
+ASCENDANT_CHAOTIC_ATTUNEMENT = "Chaotic Attunement (Ascendant)"
+ASCENDANT_BLOOD_AMULET = "Blood Amulet (Ascendant)"
+SENTRY_ENERGIZER_HAVOC_CLOAKING_MODULE = "Cloaking Module (Sentry/Energizer/Havoc)"
+SENTRY_ENERGIZER_HAVOC_SHIELD_BATTERY_RAPID_RECHARGING = "Rapid Recharging (Sentry/Energizer/Havoc/Shield Battery)"
+SENTRY_FORCE_FIELD = "Force Field (Sentry)"
+SENTRY_HALLUCINATION = "Hallucination (Sentry)"
+ENERGIZER_RECLAMATION = "Reclamation (Energizer)"
+ENERGIZER_FORGED_CHASSIS = "Forged Chassis (Energizer)"
+HAVOC_DETECT_WEAKNESS = "Detect Weakness (Havoc)"
+HAVOC_BLOODSHARD_RESONANCE = "Bloodshard Resonance (Havoc)"
+ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS = "Leg Enhancements (Zealot/Sentinel/Centurion)"
+ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY = "Shield Capacity (Zealot/Sentinel/Centurion)"
+
+# Spear Of Adun
+SOA_CHRONO_SURGE = "Chrono Surge (Spear of Adun Calldown)"
+SOA_PROGRESSIVE_PROXY_PYLON = "Progressive Proxy Pylon (Spear of Adun Calldown)"
+SOA_PYLON_OVERCHARGE = "Pylon Overcharge (Spear of Adun Calldown)"
+SOA_ORBITAL_STRIKE = "Orbital Strike (Spear of Adun Calldown)"
+SOA_TEMPORAL_FIELD = "Temporal Field (Spear of Adun Calldown)"
+SOA_SOLAR_LANCE = "Solar Lance (Spear of Adun Calldown)"
+SOA_MASS_RECALL = "Mass Recall (Spear of Adun Calldown)"
+SOA_SHIELD_OVERCHARGE = "Shield Overcharge (Spear of Adun Calldown)"
+SOA_DEPLOY_FENIX = "Deploy Fenix (Spear of Adun Calldown)"
+SOA_PURIFIER_BEAM = "Purifier Beam (Spear of Adun Calldown)"
+SOA_TIME_STOP = "Time Stop (Spear of Adun Calldown)"
+SOA_SOLAR_BOMBARDMENT = "Solar Bombardment (Spear of Adun Calldown)"
+
+# Generic upgrades
+MATRIX_OVERLOAD = "Matrix Overload"
+QUATRO = "Quatro"
+NEXUS_OVERCHARGE = "Nexus Overcharge"
+ORBITAL_ASSIMILATORS = "Orbital Assimilators"
+WARP_HARMONIZATION = "Warp Harmonization"
+GUARDIAN_SHELL = "Guardian Shell"
+RECONSTRUCTION_BEAM = "Reconstruction Beam (Spear of Adun Auto-Cast)"
+OVERWATCH = "Overwatch (Spear of Adun Auto-Cast)"
+SUPERIOR_WARP_GATES = "Superior Warp Gates"
+ENHANCED_TARGETING = "Enhanced Targeting"
+OPTIMIZED_ORDNANCE = "Optimized Ordnance"
+KHALAI_INGENUITY = "Khalai Ingenuity"
+AMPLIFIED_ASSIMILATORS = "Amplified Assimilators"
+
+# Filler items
+STARTING_MINERALS = "Additional Starting Minerals"
+STARTING_VESPENE = "Additional Starting Vespene"
+STARTING_SUPPLY = "Additional Starting Supply"
+NOTHING = "Nothing"
diff --git a/worlds/sc2/Items.py b/worlds/sc2/Items.py
new file mode 100644
index 000000000000..8277d0e7e13d
--- /dev/null
+++ b/worlds/sc2/Items.py
@@ -0,0 +1,2553 @@
+import inspect
+from pydoc import describe
+
+from BaseClasses import Item, ItemClassification, MultiWorld
+import typing
+
+from .Options import get_option_value, RequiredTactics
+from .MissionTables import SC2Mission, SC2Race, SC2Campaign, campaign_mission_table
+from . import ItemNames
+from worlds.AutoWorld import World
+
+
+class ItemData(typing.NamedTuple):
+ code: int
+ type: str
+ number: int # Important for bot commands to send the item into the game
+ race: SC2Race
+ classification: ItemClassification = ItemClassification.useful
+ quantity: int = 1
+ parent_item: typing.Optional[str] = None
+ origin: typing.Set[str] = {"wol"}
+ description: typing.Optional[str] = None
+ important_for_filtering: bool = False
+
+ def is_important_for_filtering(self):
+ return self.important_for_filtering \
+ or self.classification == ItemClassification.progression \
+ or self.classification == ItemClassification.progression_skip_balancing
+
+
+class StarcraftItem(Item):
+ game: str = "Starcraft 2"
+
+
+def get_full_item_list():
+ return item_table
+
+
+SC2WOL_ITEM_ID_OFFSET = 1000
+SC2HOTS_ITEM_ID_OFFSET = SC2WOL_ITEM_ID_OFFSET + 1000
+SC2LOTV_ITEM_ID_OFFSET = SC2HOTS_ITEM_ID_OFFSET + 1000
+
+# Descriptions
+WEAPON_ARMOR_UPGRADE_NOTE = inspect.cleandoc("""
+ Must be researched during the mission if the mission type isn't set to auto-unlock generic upgrades.
+""")
+LASER_TARGETING_SYSTEMS_DESCRIPTION = "Increases vision by 2 and weapon range by 1."
+STIMPACK_SMALL_COST = 10
+STIMPACK_SMALL_HEAL = 30
+STIMPACK_LARGE_COST = 20
+STIMPACK_LARGE_HEAL = 60
+STIMPACK_TEMPLATE = inspect.cleandoc("""
+ Level 1: Stimpack: Increases unit movement and attack speed for 15 seconds. Injures the unit for {} life.
+ Level 2: Super Stimpack: Instead of injuring the unit, heals the unit for {} life instead.
+""")
+STIMPACK_SMALL_DESCRIPTION = STIMPACK_TEMPLATE.format(STIMPACK_SMALL_COST, STIMPACK_SMALL_HEAL)
+STIMPACK_LARGE_DESCRIPTION = STIMPACK_TEMPLATE.format(STIMPACK_LARGE_COST, STIMPACK_LARGE_HEAL)
+SMART_SERVOS_DESCRIPTION = "Increases transformation speed between modes."
+INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE = "{} can be trained from a {} without an attached Tech Lab."
+RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE = "Reduces {} resource and supply cost."
+RESOURCE_EFFICIENCY_NO_SUPPLY_DESCRIPTION_TEMPLATE = "Reduces {} resource cost."
+CLOAK_DESCRIPTION_TEMPLATE = "Allows {} to use the Cloak ability."
+
+
+# The items are sorted by their IDs. The IDs shall be kept for compatibility with older games.
+item_table = {
+ # WoL
+ ItemNames.MARINE:
+ ItemData(0 + SC2WOL_ITEM_ID_OFFSET, "Unit", 0, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="General-purpose infantry."),
+ ItemNames.MEDIC:
+ ItemData(1 + SC2WOL_ITEM_ID_OFFSET, "Unit", 1, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Support trooper. Heals nearby biological units."),
+ ItemNames.FIREBAT:
+ ItemData(2 + SC2WOL_ITEM_ID_OFFSET, "Unit", 2, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Specialized anti-infantry attacker."),
+ ItemNames.MARAUDER:
+ ItemData(3 + SC2WOL_ITEM_ID_OFFSET, "Unit", 3, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Heavy assault infantry."),
+ ItemNames.REAPER:
+ ItemData(4 + SC2WOL_ITEM_ID_OFFSET, "Unit", 4, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Raider. Capable of jumping up and down cliffs. Throws explosive mines."),
+ ItemNames.HELLION:
+ ItemData(5 + SC2WOL_ITEM_ID_OFFSET, "Unit", 5, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Fast scout. Has a flame attack that damages all enemy units in its line of fire."),
+ ItemNames.VULTURE:
+ ItemData(6 + SC2WOL_ITEM_ID_OFFSET, "Unit", 6, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Fast skirmish unit. Can use the Spider Mine ability."),
+ ItemNames.GOLIATH:
+ ItemData(7 + SC2WOL_ITEM_ID_OFFSET, "Unit", 7, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Heavy-fire support unit."),
+ ItemNames.DIAMONDBACK:
+ ItemData(8 + SC2WOL_ITEM_ID_OFFSET, "Unit", 8, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Fast, high-damage hovertank. Rail Gun can fire while the Diamondback is moving."),
+ ItemNames.SIEGE_TANK:
+ ItemData(9 + SC2WOL_ITEM_ID_OFFSET, "Unit", 9, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Heavy tank. Long-range artillery in Siege Mode."),
+ ItemNames.MEDIVAC:
+ ItemData(10 + SC2WOL_ITEM_ID_OFFSET, "Unit", 10, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Air transport. Heals nearby biological units."),
+ ItemNames.WRAITH:
+ ItemData(11 + SC2WOL_ITEM_ID_OFFSET, "Unit", 11, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Highly mobile flying unit. Excellent at surgical strikes."),
+ ItemNames.VIKING:
+ ItemData(12 + SC2WOL_ITEM_ID_OFFSET, "Unit", 12, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description=inspect.cleandoc(
+ """
+ Durable support flyer. Loaded with strong anti-capital air missiles.
+ Can switch into Assault Mode to attack ground units.
+ """
+ )),
+ ItemNames.BANSHEE:
+ ItemData(13 + SC2WOL_ITEM_ID_OFFSET, "Unit", 13, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Tactical-strike aircraft."),
+ ItemNames.BATTLECRUISER:
+ ItemData(14 + SC2WOL_ITEM_ID_OFFSET, "Unit", 14, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Powerful warship."),
+ ItemNames.GHOST:
+ ItemData(15 + SC2WOL_ITEM_ID_OFFSET, "Unit", 15, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description=inspect.cleandoc(
+ """
+ Infiltration unit. Can use Snipe and Cloak abilities. Can also call down Tactical Nukes.
+ """
+ )),
+ ItemNames.SPECTRE:
+ ItemData(16 + SC2WOL_ITEM_ID_OFFSET, "Unit", 16, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description=inspect.cleandoc(
+ """
+ Infiltration unit. Can use Ultrasonic Pulse, Psionic Lash, and Cloak.
+ Can also call down Tactical Nukes.
+ """
+ )),
+ ItemNames.THOR:
+ ItemData(17 + SC2WOL_ITEM_ID_OFFSET, "Unit", 17, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Heavy assault mech."),
+ # EE units
+ ItemNames.LIBERATOR:
+ ItemData(18 + SC2WOL_ITEM_ID_OFFSET, "Unit", 18, SC2Race.TERRAN,
+ classification=ItemClassification.progression, origin={"nco", "ext"},
+ description=inspect.cleandoc(
+ """
+ Artillery fighter. Loaded with missiles that deal area damage to enemy air targets.
+ Can switch into Defender Mode to provide siege support.
+ """
+ )),
+ ItemNames.VALKYRIE:
+ ItemData(19 + SC2WOL_ITEM_ID_OFFSET, "Unit", 19, SC2Race.TERRAN,
+ classification=ItemClassification.progression, origin={"bw"},
+ description=inspect.cleandoc(
+ """
+ Advanced anti-aircraft fighter.
+ Able to use cluster missiles that deal area damage to air targets.
+ """
+ )),
+ ItemNames.WIDOW_MINE:
+ ItemData(20 + SC2WOL_ITEM_ID_OFFSET, "Unit", 20, SC2Race.TERRAN,
+ classification=ItemClassification.progression, origin={"ext"},
+ description=inspect.cleandoc(
+ """
+ Robotic mine. Launches missiles at nearby enemy units while burrowed.
+ Attacks deal splash damage in a small area around the target.
+ Widow Mine is revealed when Sentinel Missile is on cooldown.
+ """
+ )),
+ ItemNames.CYCLONE:
+ ItemData(21 + SC2WOL_ITEM_ID_OFFSET, "Unit", 21, SC2Race.TERRAN,
+ classification=ItemClassification.progression, origin={"ext"},
+ description=inspect.cleandoc(
+ """
+ Mobile assault vehicle. Can use Lock On to quickly fire while moving.
+ """
+ )),
+ ItemNames.HERC:
+ ItemData(22 + SC2WOL_ITEM_ID_OFFSET, "Unit", 26, SC2Race.TERRAN,
+ classification=ItemClassification.progression, origin={"ext"},
+ description=inspect.cleandoc(
+ """
+ Front-line infantry. Can use Grapple.
+ """
+ )),
+ ItemNames.WARHOUND:
+ ItemData(23 + SC2WOL_ITEM_ID_OFFSET, "Unit", 27, SC2Race.TERRAN,
+ classification=ItemClassification.progression, origin={"ext"},
+ description=inspect.cleandoc(
+ """
+ Anti-vehicle mech. Haywire missiles do bonus damage to mechanical units.
+ """
+ )),
+
+ # Some other items are moved to Upgrade group because of the way how the bot message is parsed
+ ItemNames.PROGRESSIVE_TERRAN_INFANTRY_WEAPON:
+ ItemData(100 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, SC2Race.TERRAN,
+ quantity=3,
+ description=inspect.cleandoc(
+ f"""
+ Increases damage of Terran infantry units.
+ {WEAPON_ARMOR_UPGRADE_NOTE}
+ """
+ )),
+ ItemNames.PROGRESSIVE_TERRAN_INFANTRY_ARMOR:
+ ItemData(102 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, SC2Race.TERRAN,
+ quantity=3,
+ description=inspect.cleandoc(
+ f"""
+ Increases armor of Terran infantry units.
+ {WEAPON_ARMOR_UPGRADE_NOTE}
+ """
+ )),
+ ItemNames.PROGRESSIVE_TERRAN_VEHICLE_WEAPON:
+ ItemData(103 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, SC2Race.TERRAN,
+ quantity=3,
+ description=inspect.cleandoc(
+ f"""
+ Increases damage of Terran vehicle units.
+ {WEAPON_ARMOR_UPGRADE_NOTE}
+ """
+ )),
+ ItemNames.PROGRESSIVE_TERRAN_VEHICLE_ARMOR:
+ ItemData(104 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 6, SC2Race.TERRAN,
+ quantity=3,
+ description=inspect.cleandoc(
+ f"""
+ Increases armor of Terran vehicle units.
+ {WEAPON_ARMOR_UPGRADE_NOTE}
+ """
+ )),
+ ItemNames.PROGRESSIVE_TERRAN_SHIP_WEAPON:
+ ItemData(105 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 8, SC2Race.TERRAN,
+ quantity=3,
+ description=inspect.cleandoc(
+ f"""
+ Increases damage of Terran starship units.
+ {WEAPON_ARMOR_UPGRADE_NOTE}
+ """
+ )),
+ ItemNames.PROGRESSIVE_TERRAN_SHIP_ARMOR:
+ ItemData(106 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 10, SC2Race.TERRAN,
+ quantity=3,
+ description=inspect.cleandoc(
+ f"""
+ Increases armor of Terran starship units.
+ {WEAPON_ARMOR_UPGRADE_NOTE}
+ """
+ )),
+ # Upgrade bundle 'number' values are used as indices to get affected 'number's
+ ItemNames.PROGRESSIVE_TERRAN_WEAPON_UPGRADE: ItemData(107 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, SC2Race.TERRAN, quantity=3),
+ ItemNames.PROGRESSIVE_TERRAN_ARMOR_UPGRADE: ItemData(108 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 1, SC2Race.TERRAN, quantity=3),
+ ItemNames.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE: ItemData(109 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, SC2Race.TERRAN, quantity=3),
+ ItemNames.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE: ItemData(110 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 3, SC2Race.TERRAN, quantity=3),
+ ItemNames.PROGRESSIVE_TERRAN_SHIP_UPGRADE: ItemData(111 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, SC2Race.TERRAN, quantity=3),
+ ItemNames.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE: ItemData(112 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 5, SC2Race.TERRAN, quantity=3),
+
+ # Unit and structure upgrades
+ ItemNames.BUNKER_PROJECTILE_ACCELERATOR:
+ ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0, SC2Race.TERRAN,
+ parent_item=ItemNames.BUNKER,
+ description="Increases range of all units in the Bunker by 1."),
+ ItemNames.BUNKER_NEOSTEEL_BUNKER:
+ ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1, SC2Race.TERRAN,
+ parent_item=ItemNames.BUNKER,
+ description="Increases the number of Bunker slots by 2."),
+ ItemNames.MISSILE_TURRET_TITANIUM_HOUSING:
+ ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MISSILE_TURRET,
+ description="Increases Missile Turret life by 75."),
+ ItemNames.MISSILE_TURRET_HELLSTORM_BATTERIES:
+ ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3, SC2Race.TERRAN,
+ parent_item=ItemNames.MISSILE_TURRET,
+ description="The Missile Turret unleashes an additional flurry of missiles with each attack."),
+ ItemNames.SCV_ADVANCED_CONSTRUCTION:
+ ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4, SC2Race.TERRAN,
+ description="Multiple SCVs can construct a structure, reducing its construction time."),
+ ItemNames.SCV_DUAL_FUSION_WELDERS:
+ ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5, SC2Race.TERRAN,
+ description="SCVs repair twice as fast."),
+ ItemNames.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM:
+ ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 24, SC2Race.TERRAN,
+ quantity=2,
+ description=inspect.cleandoc(
+ """
+ Level 1: While on low health, Terran structures are repaired to half health instead of burning down.
+ Level 2: Terran structures are repaired to full health instead of half health
+ """
+ )),
+ ItemNames.PROGRESSIVE_ORBITAL_COMMAND:
+ ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 26, SC2Race.TERRAN,
+ quantity=2, classification=ItemClassification.progression,
+ description=inspect.cleandoc(
+ """
+ Level 1: Allows Command Centers to use Scanner Sweep and Calldown: MULE abilities.
+ Level 2: Orbital Command abilities work even in Planetary Fortress mode.
+ """
+ )),
+ ItemNames.MARINE_PROGRESSIVE_STIMPACK:
+ ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 0, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.MARINE, quantity=2,
+ description=STIMPACK_SMALL_DESCRIPTION),
+ ItemNames.MARINE_COMBAT_SHIELD:
+ ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.MARINE,
+ description="Increases Marine life by 10."),
+ ItemNames.MEDIC_ADVANCED_MEDIC_FACILITIES:
+ ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MEDIC,
+ description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Medics", "Barracks")),
+ ItemNames.MEDIC_STABILIZER_MEDPACKS:
+ ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.MEDIC,
+ description="Increases Medic heal speed. Reduces the amount of energy required for each heal."),
+ ItemNames.FIREBAT_INCINERATOR_GAUNTLETS:
+ ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.FIREBAT,
+ description="Increases Firebat's damage radius by 40%"),
+ ItemNames.FIREBAT_JUGGERNAUT_PLATING:
+ ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13, SC2Race.TERRAN,
+ parent_item=ItemNames.FIREBAT,
+ description="Increases Firebat's armor by 2."),
+ ItemNames.MARAUDER_CONCUSSIVE_SHELLS:
+ ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14, SC2Race.TERRAN,
+ parent_item=ItemNames.MARAUDER,
+ description="Marauder attack temporarily slows all units in target area."),
+ ItemNames.MARAUDER_KINETIC_FOAM:
+ ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15, SC2Race.TERRAN,
+ parent_item=ItemNames.MARAUDER,
+ description="Increases Marauder life by 25."),
+ ItemNames.REAPER_U238_ROUNDS:
+ ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16, SC2Race.TERRAN,
+ parent_item=ItemNames.REAPER,
+ description=inspect.cleandoc(
+ """
+ Increases Reaper pistol attack range by 1.
+ Reaper pistols do additional 3 damage to Light Armor.
+ """
+ )),
+ ItemNames.REAPER_G4_CLUSTERBOMB:
+ ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.REAPER,
+ description="Timed explosive that does heavy area damage."),
+ ItemNames.CYCLONE_MAG_FIELD_ACCELERATORS:
+ ItemData(218 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 18, SC2Race.TERRAN,
+ parent_item=ItemNames.CYCLONE, origin={"ext"},
+ description="Increases Cyclone Lock On damage"),
+ ItemNames.CYCLONE_MAG_FIELD_LAUNCHERS:
+ ItemData(219 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 19, SC2Race.TERRAN,
+ parent_item=ItemNames.CYCLONE, origin={"ext"},
+ description="Increases Cyclone attack range by 2."),
+ ItemNames.MARINE_LASER_TARGETING_SYSTEM:
+ ItemData(220 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MARINE, origin={"nco"},
+ description=LASER_TARGETING_SYSTEMS_DESCRIPTION),
+ ItemNames.MARINE_MAGRAIL_MUNITIONS:
+ ItemData(221 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 20, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.MARINE, origin={"nco"},
+ description="Deals 20 damage to target unit. Autocast on attack with a cooldown."),
+ ItemNames.MARINE_OPTIMIZED_LOGISTICS:
+ ItemData(222 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 21, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MARINE, origin={"nco"},
+ description="Increases Marine training speed."),
+ ItemNames.MEDIC_RESTORATION:
+ ItemData(223 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 22, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MEDIC, origin={"bw"},
+ description="Removes negative status effects from target allied unit."),
+ ItemNames.MEDIC_OPTICAL_FLARE:
+ ItemData(224 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 23, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MEDIC, origin={"bw"},
+ description="Reduces vision range of target enemy unit. Disables detection."),
+ ItemNames.MEDIC_RESOURCE_EFFICIENCY:
+ ItemData(225 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 24, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MEDIC, origin={"bw"},
+ description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Medic")),
+ ItemNames.FIREBAT_PROGRESSIVE_STIMPACK:
+ ItemData(226 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 6, SC2Race.TERRAN,
+ parent_item=ItemNames.FIREBAT, quantity=2, origin={"bw"},
+ description=STIMPACK_LARGE_DESCRIPTION),
+ ItemNames.FIREBAT_RESOURCE_EFFICIENCY:
+ ItemData(227 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 25, SC2Race.TERRAN,
+ parent_item=ItemNames.FIREBAT, origin={"bw"},
+ description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Firebat")),
+ ItemNames.MARAUDER_PROGRESSIVE_STIMPACK:
+ ItemData(228 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 8, SC2Race.TERRAN,
+ parent_item=ItemNames.MARAUDER, quantity=2, origin={"nco"},
+ description=STIMPACK_LARGE_DESCRIPTION),
+ ItemNames.MARAUDER_LASER_TARGETING_SYSTEM:
+ ItemData(229 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 26, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MARAUDER, origin={"nco"},
+ description=LASER_TARGETING_SYSTEMS_DESCRIPTION),
+ ItemNames.MARAUDER_MAGRAIL_MUNITIONS:
+ ItemData(230 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 27, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MARAUDER, origin={"nco"},
+ description="Deals 20 damage to target unit. Autocast on attack with a cooldown."),
+ ItemNames.MARAUDER_INTERNAL_TECH_MODULE:
+ ItemData(231 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 28, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MARAUDER, origin={"nco"},
+ description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Marauders", "Barracks")),
+ ItemNames.SCV_HOSTILE_ENVIRONMENT_ADAPTATION:
+ ItemData(232 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 29, SC2Race.TERRAN,
+ classification=ItemClassification.filler, origin={"bw"},
+ description="Increases SCV life by 15 and attack speed slightly."),
+ ItemNames.MEDIC_ADAPTIVE_MEDPACKS:
+ ItemData(233 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.MEDIC, origin={"ext"},
+ description="Allows Medics to heal mechanical and air units."),
+ ItemNames.MEDIC_NANO_PROJECTOR:
+ ItemData(234 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MEDIC, origin={"ext"},
+ description="Increases Medic heal range by 2."),
+ ItemNames.FIREBAT_INFERNAL_PRE_IGNITER:
+ ItemData(235 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, SC2Race.TERRAN,
+ parent_item=ItemNames.FIREBAT, origin={"bw"},
+ description="Firebats do an additional 4 damage to Light Armor."),
+ ItemNames.FIREBAT_KINETIC_FOAM:
+ ItemData(236 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, SC2Race.TERRAN,
+ parent_item=ItemNames.FIREBAT, origin={"ext"},
+ description="Increases Firebat life by 100."),
+ ItemNames.FIREBAT_NANO_PROJECTORS:
+ ItemData(237 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4, SC2Race.TERRAN,
+ parent_item=ItemNames.FIREBAT, origin={"ext"},
+ description="Increases Firebat attack range by 2"),
+ ItemNames.MARAUDER_JUGGERNAUT_PLATING:
+ ItemData(238 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5, SC2Race.TERRAN,
+ parent_item=ItemNames.MARAUDER, origin={"ext"},
+ description="Increases Marauder's armor by 2."),
+ ItemNames.REAPER_JET_PACK_OVERDRIVE:
+ ItemData(239 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, SC2Race.TERRAN,
+ parent_item=ItemNames.REAPER, origin={"ext"},
+ description=inspect.cleandoc(
+ """
+ Allows the Reaper to fly for 10 seconds.
+ While flying, the Reaper can attack air units.
+ """
+ )),
+ ItemNames.HELLION_INFERNAL_PLATING:
+ ItemData(240 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, SC2Race.TERRAN,
+ parent_item=ItemNames.HELLION, origin={"ext"},
+ description="Increases Hellion and Hellbat armor by 2."),
+ ItemNames.VULTURE_AUTO_REPAIR:
+ ItemData(241 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8, SC2Race.TERRAN,
+ parent_item=ItemNames.VULTURE, origin={"ext"},
+ description="Vultures regenerate life."),
+ ItemNames.GOLIATH_SHAPED_HULL:
+ ItemData(242 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.GOLIATH, origin={"nco", "ext"},
+ description="Increases Goliath life by 25."),
+ ItemNames.GOLIATH_RESOURCE_EFFICIENCY:
+ ItemData(243 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, SC2Race.TERRAN,
+ parent_item=ItemNames.GOLIATH, origin={"nco", "bw"},
+ description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Goliath")),
+ ItemNames.GOLIATH_INTERNAL_TECH_MODULE:
+ ItemData(244 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.GOLIATH, origin={"nco", "bw"},
+ description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Goliaths", "Factory")),
+ ItemNames.SIEGE_TANK_SHAPED_HULL:
+ ItemData(245 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.SIEGE_TANK, origin={"nco", "ext"},
+ description="Increases Siege Tank life by 25."),
+ ItemNames.SIEGE_TANK_RESOURCE_EFFICIENCY:
+ ItemData(246 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, SC2Race.TERRAN,
+ parent_item=ItemNames.SIEGE_TANK, origin={"bw"},
+ description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Siege Tank")),
+ ItemNames.PREDATOR_CLOAK:
+ ItemData(247 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.PREDATOR, origin={"ext"},
+ description=CLOAK_DESCRIPTION_TEMPLATE.format("Predators")),
+ ItemNames.PREDATOR_CHARGE:
+ ItemData(248 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.PREDATOR, origin={"ext"},
+ description="Allows Predators to intercept enemy ground units."),
+ ItemNames.MEDIVAC_SCATTER_VEIL:
+ ItemData(249 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, SC2Race.TERRAN,
+ parent_item=ItemNames.MEDIVAC, origin={"ext"},
+ description="Medivacs get 100 shields."),
+ ItemNames.REAPER_PROGRESSIVE_STIMPACK:
+ ItemData(250 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 10, SC2Race.TERRAN,
+ parent_item=ItemNames.REAPER, quantity=2, origin={"nco"},
+ description=STIMPACK_SMALL_DESCRIPTION),
+ ItemNames.REAPER_LASER_TARGETING_SYSTEM:
+ ItemData(251 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.REAPER, origin={"nco"},
+ description=LASER_TARGETING_SYSTEMS_DESCRIPTION),
+ ItemNames.REAPER_ADVANCED_CLOAKING_FIELD:
+ ItemData(252 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, SC2Race.TERRAN,
+ parent_item=ItemNames.REAPER, origin={"nco"},
+ description="Reapers are permanently cloaked."),
+ ItemNames.REAPER_SPIDER_MINES:
+ ItemData(253 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.REAPER, origin={"nco"},
+ important_for_filtering=True,
+ description="Allows Reapers to lay Spider Mines. 3 charges per Reaper."),
+ ItemNames.REAPER_COMBAT_DRUGS:
+ ItemData(254 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.REAPER, origin={"ext"},
+ description="Reapers regenerate life while out of combat."),
+ ItemNames.HELLION_HELLBAT_ASPECT:
+ ItemData(255 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.HELLION, origin={"nco"},
+ description="Allows Hellions to transform into Hellbats."),
+ ItemNames.HELLION_SMART_SERVOS:
+ ItemData(256 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, SC2Race.TERRAN,
+ parent_item=ItemNames.HELLION, origin={"nco"},
+ description="Transforms faster between modes. Hellions can attack while moving."),
+ ItemNames.HELLION_OPTIMIZED_LOGISTICS:
+ ItemData(257 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.HELLION, origin={"nco"},
+ description="Increases Hellion training speed."),
+ ItemNames.HELLION_JUMP_JETS:
+ ItemData(258 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.HELLION, origin={"nco"},
+ description=inspect.cleandoc(
+ """
+ Increases movement speed in Hellion mode.
+ In Hellbat mode, launches the Hellbat toward enemy ground units and briefly stuns them.
+ """
+ )),
+ ItemNames.HELLION_PROGRESSIVE_STIMPACK:
+ ItemData(259 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 12, SC2Race.TERRAN,
+ parent_item=ItemNames.HELLION, quantity=2, origin={"nco"},
+ description=STIMPACK_LARGE_DESCRIPTION),
+ ItemNames.VULTURE_ION_THRUSTERS:
+ ItemData(260 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.VULTURE, origin={"bw"},
+ description="Increases Vulture movement speed."),
+ ItemNames.VULTURE_AUTO_LAUNCHERS:
+ ItemData(261 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 26, SC2Race.TERRAN,
+ parent_item=ItemNames.VULTURE, origin={"bw"},
+ description="Allows Vultures to attack while moving."),
+ ItemNames.SPIDER_MINE_HIGH_EXPLOSIVE_MUNITION:
+ ItemData(262 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 27, SC2Race.TERRAN,
+ origin={"bw"},
+ description="Increases Spider mine damage."),
+ ItemNames.GOLIATH_JUMP_JETS:
+ ItemData(263 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 28, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.GOLIATH, origin={"nco"},
+ description="Allows Goliaths to jump up and down cliffs."),
+ ItemNames.GOLIATH_OPTIMIZED_LOGISTICS:
+ ItemData(264 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 29, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.GOLIATH, origin={"nco"},
+ description="Increases Goliath training speed."),
+ ItemNames.DIAMONDBACK_HYPERFLUXOR:
+ ItemData(265 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 0, SC2Race.TERRAN,
+ parent_item=ItemNames.DIAMONDBACK, origin={"ext"},
+ description="Increases Diamondback attack speed."),
+ ItemNames.DIAMONDBACK_BURST_CAPACITORS:
+ ItemData(266 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 1, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.DIAMONDBACK, origin={"ext"},
+ description=inspect.cleandoc(
+ """
+ While not attacking, the Diamondback charges its weapon.
+ The next attack does 10 additional damage.
+ """
+ )),
+ ItemNames.DIAMONDBACK_RESOURCE_EFFICIENCY:
+ ItemData(267 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 2, SC2Race.TERRAN,
+ parent_item=ItemNames.DIAMONDBACK, origin={"ext"},
+ description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Diamondback")),
+ ItemNames.SIEGE_TANK_JUMP_JETS:
+ ItemData(268 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 3, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.SIEGE_TANK, origin={"nco"},
+ description=inspect.cleandoc(
+ """
+ Repositions Siege Tank to a target location.
+ Can be used in either mode and to jump up and down cliffs.
+ """
+ )),
+ ItemNames.SIEGE_TANK_SPIDER_MINES:
+ ItemData(269 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 4, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.SIEGE_TANK, origin={"nco"},
+ important_for_filtering=True,
+ description=inspect.cleandoc(
+ """
+ Allows Siege Tanks to lay Spider Mines.
+ Lays 3 Spider Mines at once. 3 charges
+ """
+ )),
+ ItemNames.SIEGE_TANK_SMART_SERVOS:
+ ItemData(270 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 5, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.SIEGE_TANK, origin={"nco"},
+ description=SMART_SERVOS_DESCRIPTION),
+ ItemNames.SIEGE_TANK_GRADUATING_RANGE:
+ ItemData(271 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 6, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.SIEGE_TANK, origin={"ext"},
+ description=inspect.cleandoc(
+ """
+ Increases the Siege Tank's attack range by 1 every 3 seconds while in Siege Mode,
+ up to a maximum of 5 additional range.
+ """
+ )),
+ ItemNames.SIEGE_TANK_LASER_TARGETING_SYSTEM:
+ ItemData(272 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 7, SC2Race.TERRAN,
+ parent_item=ItemNames.SIEGE_TANK, origin={"nco"},
+ description=LASER_TARGETING_SYSTEMS_DESCRIPTION),
+ ItemNames.SIEGE_TANK_ADVANCED_SIEGE_TECH:
+ ItemData(273 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 8, SC2Race.TERRAN,
+ parent_item=ItemNames.SIEGE_TANK, origin={"ext"},
+ description="Siege Tanks gain +3 armor in Siege Mode."),
+ ItemNames.SIEGE_TANK_INTERNAL_TECH_MODULE:
+ ItemData(274 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 9, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.SIEGE_TANK, origin={"nco"},
+ description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Siege Tanks", "Factory")),
+ ItemNames.PREDATOR_RESOURCE_EFFICIENCY:
+ ItemData(275 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 10, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.PREDATOR, origin={"ext"},
+ description="Decreases Predator resource and supply cost."),
+ ItemNames.MEDIVAC_EXPANDED_HULL:
+ ItemData(276 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 11, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MEDIVAC, origin={"ext"},
+ description="Increases Medivac cargo space by 4."),
+ ItemNames.MEDIVAC_AFTERBURNERS:
+ ItemData(277 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 12, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MEDIVAC, origin={"ext"},
+ description="Ability. Temporarily increases the Medivac's movement speed by 70%."),
+ ItemNames.WRAITH_ADVANCED_LASER_TECHNOLOGY:
+ ItemData(278 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 13, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.WRAITH, origin={"ext"},
+ description=inspect.cleandoc(
+ """
+ Burst Lasers do more damage and can hit both ground and air targets.
+ Replaces Gemini Missiles weapon.
+ """
+ )),
+ ItemNames.VIKING_SMART_SERVOS:
+ ItemData(279 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 14, SC2Race.TERRAN,
+ parent_item=ItemNames.VIKING, origin={"ext"},
+ description=SMART_SERVOS_DESCRIPTION),
+ ItemNames.VIKING_ANTI_MECHANICAL_MUNITION:
+ ItemData(280 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 15, SC2Race.TERRAN,
+ parent_item=ItemNames.VIKING, origin={"ext"},
+ description="Increases Viking damage to mechanical units while in Assault Mode."),
+ ItemNames.DIAMONDBACK_ION_THRUSTERS:
+ ItemData(281 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 21, SC2Race.TERRAN,
+ parent_item=ItemNames.DIAMONDBACK, origin={"ext"},
+ description="Increases Diamondback movement speed."),
+ ItemNames.WARHOUND_RESOURCE_EFFICIENCY:
+ ItemData(282 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 13, SC2Race.TERRAN,
+ parent_item=ItemNames.WARHOUND, origin={"ext"},
+ description=RESOURCE_EFFICIENCY_NO_SUPPLY_DESCRIPTION_TEMPLATE.format("Warhound")),
+ ItemNames.WARHOUND_REINFORCED_PLATING:
+ ItemData(283 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 14, SC2Race.TERRAN,
+ parent_item=ItemNames.WARHOUND, origin={"ext"},
+ description="Increases Warhound armor by 2."),
+ ItemNames.HERC_RESOURCE_EFFICIENCY:
+ ItemData(284 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 15, SC2Race.TERRAN,
+ parent_item=ItemNames.HERC, origin={"ext"},
+ description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("HERC")),
+ ItemNames.HERC_JUGGERNAUT_PLATING:
+ ItemData(285 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 16, SC2Race.TERRAN,
+ parent_item=ItemNames.HERC, origin={"ext"},
+ description="Increases HERC armor by 2."),
+ ItemNames.HERC_KINETIC_FOAM:
+ ItemData(286 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 17, SC2Race.TERRAN,
+ parent_item=ItemNames.HERC, origin={"ext"},
+ description="Increases HERC life by 50."),
+
+ ItemNames.HELLION_TWIN_LINKED_FLAMETHROWER:
+ ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 16, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.HELLION,
+ description="Doubles the width of the Hellion's flame attack."),
+ ItemNames.HELLION_THERMITE_FILAMENTS:
+ ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 17, SC2Race.TERRAN,
+ parent_item=ItemNames.HELLION,
+ description="Hellions do an additional 10 damage to Light Armor."),
+ ItemNames.SPIDER_MINE_CERBERUS_MINE:
+ ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 18, SC2Race.TERRAN,
+ classification=ItemClassification.filler,
+ description="Increases trigger and blast radius of Spider Mines."),
+ ItemNames.VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE:
+ ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 16, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.VULTURE, quantity=2,
+ description=inspect.cleandoc(
+ """
+ Level 1: Allows Vultures to replace used Spider Mines. Costs 15 minerals.
+ Level 2: Replacing used Spider Mines no longer costs minerals.
+ """
+ )),
+ ItemNames.GOLIATH_MULTI_LOCK_WEAPONS_SYSTEM:
+ ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 19, SC2Race.TERRAN,
+ parent_item=ItemNames.GOLIATH,
+ description="Goliaths can attack both ground and air targets simultaneously."),
+ ItemNames.GOLIATH_ARES_CLASS_TARGETING_SYSTEM:
+ ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 20, SC2Race.TERRAN,
+ parent_item=ItemNames.GOLIATH,
+ description="Increases Goliath ground attack range by 1 and air by 3."),
+ ItemNames.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL:
+ ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade 2", 4, SC2Race.TERRAN,
+ parent_item=ItemNames.DIAMONDBACK, quantity=2,
+ description=inspect.cleandoc(
+ """
+ Level 1: Tri-Lithium Power Cell: Increases Diamondback attack range by 1.
+ Level 2: Tungsten Spikes: Increases Diamondback attack range by 3.
+ """
+ )),
+ ItemNames.DIAMONDBACK_SHAPED_HULL:
+ ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 22, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.DIAMONDBACK,
+ description="Increases Diamondback life by 50."),
+ ItemNames.SIEGE_TANK_MAELSTROM_ROUNDS:
+ ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 23, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.SIEGE_TANK,
+ description="Siege Tanks do an additional 40 damage to the primary target in Siege Mode."),
+ ItemNames.SIEGE_TANK_SHAPED_BLAST:
+ ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 24, SC2Race.TERRAN,
+ parent_item=ItemNames.SIEGE_TANK,
+ description="Reduces splash damage to friendly targets while in Siege Mode by 75%."),
+ ItemNames.MEDIVAC_RAPID_DEPLOYMENT_TUBE:
+ ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 25, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MEDIVAC,
+ description="Medivacs deploy loaded troops almost instantly."),
+ ItemNames.MEDIVAC_ADVANCED_HEALING_AI:
+ ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 26, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.MEDIVAC,
+ description="Medivacs can heal two targets at once."),
+ ItemNames.WRAITH_PROGRESSIVE_TOMAHAWK_POWER_CELLS:
+ ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 18, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.WRAITH, quantity=2,
+ description=inspect.cleandoc(
+ """
+ Level 1: Tomahawk Power Cells: Increases Wraith starting energy by 100.
+ Level 2: Unregistered Cloaking Module: Wraiths do not require energy to cloak and remain cloaked.
+ """
+ )),
+ ItemNames.WRAITH_DISPLACEMENT_FIELD:
+ ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 27, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.WRAITH,
+ description="Wraiths evade 20% of incoming attacks while cloaked."),
+ ItemNames.VIKING_RIPWAVE_MISSILES:
+ ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 28, SC2Race.TERRAN,
+ parent_item=ItemNames.VIKING,
+ description="Vikings do area damage while in Fighter Mode"),
+ ItemNames.VIKING_PHOBOS_CLASS_WEAPONS_SYSTEM:
+ ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 29, SC2Race.TERRAN,
+ parent_item=ItemNames.VIKING,
+ description="Increases Viking attack range by 1 in Assault mode and 2 in Fighter mode."),
+ ItemNames.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS:
+ ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 2, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.BANSHEE, quantity=2,
+ description=inspect.cleandoc(
+ """
+ Level 1: Banshees can remain cloaked twice as long.
+ Level 2: Banshees do not require energy to cloak and remain cloaked.
+ """
+ )),
+ ItemNames.BANSHEE_SHOCKWAVE_MISSILE_BATTERY:
+ ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 0, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.BANSHEE,
+ description="Banshees do area damage in a straight line."),
+ ItemNames.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS:
+ ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade 2", 2, SC2Race.TERRAN,
+ parent_item=ItemNames.BATTLECRUISER, quantity=2,
+ description="Spell. Missile Pods do damage to air targets in a target area."),
+ ItemNames.BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX:
+ ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 20, SC2Race.TERRAN,
+ parent_item=ItemNames.BATTLECRUISER, quantity=2,
+ description=inspect.cleandoc(
+ """
+ Level 1: Spell. For 20 seconds the Battlecruiser gains a shield that can absorb up to 200 damage.
+ Level 2: Passive. Battlecruiser gets 200 shields.
+ """
+ )),
+ ItemNames.GHOST_OCULAR_IMPLANTS:
+ ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 2, SC2Race.TERRAN,
+ parent_item=ItemNames.GHOST,
+ description="Increases Ghost sight range by 3 and attack range by 2."),
+ ItemNames.GHOST_CRIUS_SUIT:
+ ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 3, SC2Race.TERRAN,
+ parent_item=ItemNames.GHOST,
+ description="Cloak no longer requires energy to activate or maintain."),
+ ItemNames.SPECTRE_PSIONIC_LASH:
+ ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 4, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.SPECTRE,
+ description="Spell. Deals 200 damage to a single target."),
+ ItemNames.SPECTRE_NYX_CLASS_CLOAKING_MODULE:
+ ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 5, SC2Race.TERRAN,
+ parent_item=ItemNames.SPECTRE,
+ description="Cloak no longer requires energy to activate or maintain."),
+ ItemNames.THOR_330MM_BARRAGE_CANNON:
+ ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 6, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.THOR,
+ description=inspect.cleandoc(
+ """
+ Improves 250mm Strike Cannons ability to deal area damage and stun units in a small area.
+ Can be also freely aimed on ground.
+ """
+ )),
+ ItemNames.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL:
+ ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 22, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.THOR, quantity=2,
+ description=inspect.cleandoc("""
+ Level 1: Allows destroyed Thors to be reconstructed on the field. Costs Vespene Gas.
+ Level 2: Thors are automatically reconstructed after falling for free.
+ """
+ )),
+ ItemNames.LIBERATOR_ADVANCED_BALLISTICS:
+ ItemData(326 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 7, SC2Race.TERRAN,
+ parent_item=ItemNames.LIBERATOR, origin={"ext"},
+ description="Increases Liberator range by 3 in Defender Mode."),
+ ItemNames.LIBERATOR_RAID_ARTILLERY:
+ ItemData(327 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 8, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.LIBERATOR, origin={"nco"},
+ description="Allows Liberators to attack structures while in Defender Mode."),
+ ItemNames.WIDOW_MINE_DRILLING_CLAWS:
+ ItemData(328 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 9, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.WIDOW_MINE, origin={"ext"},
+ description="Allows Widow Mines to burrow and unburrow faster."),
+ ItemNames.WIDOW_MINE_CONCEALMENT:
+ ItemData(329 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 10, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.WIDOW_MINE, origin={"ext"},
+ description="Burrowed Widow Mines are no longer revealed when the Sentinel Missile is on cooldown."),
+ ItemNames.MEDIVAC_ADVANCED_CLOAKING_FIELD:
+ ItemData(330 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 11, SC2Race.TERRAN,
+ parent_item=ItemNames.MEDIVAC, origin={"ext"},
+ description="Medivacs are permanently cloaked."),
+ ItemNames.WRAITH_TRIGGER_OVERRIDE:
+ ItemData(331 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 12, SC2Race.TERRAN,
+ parent_item=ItemNames.WRAITH, origin={"ext"},
+ description="Wraith attack speed increases by 10% with each attack, up to a maximum of 100%."),
+ ItemNames.WRAITH_INTERNAL_TECH_MODULE:
+ ItemData(332 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 13, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.WRAITH, origin={"bw"},
+ description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Wraiths", "Starport")),
+ ItemNames.WRAITH_RESOURCE_EFFICIENCY:
+ ItemData(333 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 14, SC2Race.TERRAN,
+ parent_item=ItemNames.WRAITH, origin={"bw"},
+ description=RESOURCE_EFFICIENCY_NO_SUPPLY_DESCRIPTION_TEMPLATE.format("Wraith")),
+ ItemNames.VIKING_SHREDDER_ROUNDS:
+ ItemData(334 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 15, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.VIKING, origin={"ext"},
+ description="Attacks in Assault mode do line splash damage."),
+ ItemNames.VIKING_WILD_MISSILES:
+ ItemData(335 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 16, SC2Race.TERRAN,
+ parent_item=ItemNames.VIKING, origin={"ext"},
+ description="Launches 5 rockets at the target unit. Each rocket does 25 (40 vs armored) damage."),
+ ItemNames.BANSHEE_SHAPED_HULL:
+ ItemData(336 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 17, SC2Race.TERRAN,
+ parent_item=ItemNames.BANSHEE, origin={"ext"},
+ description="Increases Banshee life by 100."),
+ ItemNames.BANSHEE_ADVANCED_TARGETING_OPTICS:
+ ItemData(337 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 18, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.BANSHEE, origin={"ext"},
+ description="Increases Banshee attack range by 2 while cloaked."),
+ ItemNames.BANSHEE_DISTORTION_BLASTERS:
+ ItemData(338 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 19, SC2Race.TERRAN,
+ parent_item=ItemNames.BANSHEE, origin={"ext"},
+ description="Increases Banshee attack damage by 25% while cloaked."),
+ ItemNames.BANSHEE_ROCKET_BARRAGE:
+ ItemData(339 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 20, SC2Race.TERRAN,
+ parent_item=ItemNames.BANSHEE, origin={"ext"},
+ description="Deals 75 damage to enemy ground units in the target area."),
+ ItemNames.GHOST_RESOURCE_EFFICIENCY:
+ ItemData(340 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 21, SC2Race.TERRAN,
+ parent_item=ItemNames.GHOST, origin={"bw"},
+ description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Ghost")),
+ ItemNames.SPECTRE_RESOURCE_EFFICIENCY:
+ ItemData(341 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 22, SC2Race.TERRAN,
+ parent_item=ItemNames.SPECTRE, origin={"ext"},
+ description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Spectre")),
+ ItemNames.THOR_BUTTON_WITH_A_SKULL_ON_IT:
+ ItemData(342 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 23, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.THOR, origin={"ext"},
+ description="Allows Thors to launch nukes."),
+ ItemNames.THOR_LASER_TARGETING_SYSTEM:
+ ItemData(343 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 24, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.THOR, origin={"ext"},
+ description=LASER_TARGETING_SYSTEMS_DESCRIPTION),
+ ItemNames.THOR_LARGE_SCALE_FIELD_CONSTRUCTION:
+ ItemData(344 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 25, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.THOR, origin={"ext"},
+ description="Allows Thors to be built by SCVs like a structure."),
+ ItemNames.RAVEN_RESOURCE_EFFICIENCY:
+ ItemData(345 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 26, SC2Race.TERRAN,
+ parent_item=ItemNames.RAVEN, origin={"ext"},
+ description=RESOURCE_EFFICIENCY_NO_SUPPLY_DESCRIPTION_TEMPLATE.format("Raven")),
+ ItemNames.RAVEN_DURABLE_MATERIALS:
+ ItemData(346 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 27, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.RAVEN, origin={"ext"},
+ description="Extends timed life duration of Raven's summoned objects."),
+ ItemNames.SCIENCE_VESSEL_IMPROVED_NANO_REPAIR:
+ ItemData(347 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 28, SC2Race.TERRAN,
+ parent_item=ItemNames.SCIENCE_VESSEL, origin={"ext"},
+ description="Nano-Repair no longer requires energy to use."),
+ ItemNames.SCIENCE_VESSEL_ADVANCED_AI_SYSTEMS:
+ ItemData(348 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 29, SC2Race.TERRAN,
+ parent_item=ItemNames.SCIENCE_VESSEL, origin={"ext"},
+ description="Science Vessel can use Nano-Repair at two targets at once."),
+ ItemNames.CYCLONE_RESOURCE_EFFICIENCY:
+ ItemData(349 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 0, SC2Race.TERRAN,
+ parent_item=ItemNames.CYCLONE, origin={"ext"},
+ description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Cyclone")),
+ ItemNames.BANSHEE_HYPERFLIGHT_ROTORS:
+ ItemData(350 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 1, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.BANSHEE, origin={"ext"},
+ description="Increases Banshee movement speed."),
+ ItemNames.BANSHEE_LASER_TARGETING_SYSTEM:
+ ItemData(351 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 2, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.BANSHEE, origin={"nco"},
+ description=LASER_TARGETING_SYSTEMS_DESCRIPTION),
+ ItemNames.BANSHEE_INTERNAL_TECH_MODULE:
+ ItemData(352 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 3, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.BANSHEE, origin={"nco"},
+ description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Banshees", "Starport")),
+ ItemNames.BATTLECRUISER_TACTICAL_JUMP:
+ ItemData(353 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 4, SC2Race.TERRAN,
+ parent_item=ItemNames.BATTLECRUISER, origin={"nco", "ext"},
+ description=inspect.cleandoc(
+ """
+ Allows Battlecruisers to warp to a target location anywhere on the map.
+ """
+ )),
+ ItemNames.BATTLECRUISER_CLOAK:
+ ItemData(354 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 5, SC2Race.TERRAN,
+ parent_item=ItemNames.BATTLECRUISER, origin={"nco"},
+ description=CLOAK_DESCRIPTION_TEMPLATE.format("Battlecruisers")),
+ ItemNames.BATTLECRUISER_ATX_LASER_BATTERY:
+ ItemData(355 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 6, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.BATTLECRUISER, origin={"nco"},
+ description=inspect.cleandoc(
+ """
+ Battlecruisers can attack while moving,
+ do the same damage to both ground and air targets, and fire faster.
+ """
+ )),
+ ItemNames.BATTLECRUISER_OPTIMIZED_LOGISTICS:
+ ItemData(356 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 7, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.BATTLECRUISER, origin={"ext"},
+ description="Increases Battlecruiser training speed."),
+ ItemNames.BATTLECRUISER_INTERNAL_TECH_MODULE:
+ ItemData(357 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 8, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.BATTLECRUISER, origin={"nco"},
+ description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Battlecruisers", "Starport")),
+ ItemNames.GHOST_EMP_ROUNDS:
+ ItemData(358 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 9, SC2Race.TERRAN,
+ parent_item=ItemNames.GHOST, origin={"ext"},
+ description=inspect.cleandoc(
+ """
+ Spell. Does 100 damage to shields and drains all energy from units in the targeted area.
+ Cloaked units hit by EMP are revealed for a short time.
+ """
+ )),
+ ItemNames.GHOST_LOCKDOWN:
+ ItemData(359 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 10, SC2Race.TERRAN,
+ parent_item=ItemNames.GHOST, origin={"bw"},
+ description="Spell. Stuns a target mechanical unit for a long time."),
+ ItemNames.SPECTRE_IMPALER_ROUNDS:
+ ItemData(360 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 11, SC2Race.TERRAN,
+ parent_item=ItemNames.SPECTRE, origin={"ext"},
+ description="Spectres do additional damage to armored targets."),
+ ItemNames.THOR_PROGRESSIVE_HIGH_IMPACT_PAYLOAD:
+ ItemData(361 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 14, SC2Race.TERRAN,
+ parent_item=ItemNames.THOR, quantity=2, origin={"ext"},
+ description=inspect.cleandoc(
+ f"""
+ Level 1: Allows Thors to transform in order to use an alternative air attack.
+ Level 2: {SMART_SERVOS_DESCRIPTION}
+ """
+ )),
+ ItemNames.RAVEN_BIO_MECHANICAL_REPAIR_DRONE:
+ ItemData(363 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 12, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.RAVEN, origin={"nco"},
+ description="Spell. Deploys a drone that can heal biological or mechanical units."),
+ ItemNames.RAVEN_SPIDER_MINES:
+ ItemData(364 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 13, SC2Race.TERRAN,
+ parent_item=ItemNames.RAVEN, origin={"nco"}, important_for_filtering=True,
+ description="Spell. Deploys 3 Spider Mines to a target location."),
+ ItemNames.RAVEN_RAILGUN_TURRET:
+ ItemData(365 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 14, SC2Race.TERRAN,
+ parent_item=ItemNames.RAVEN, origin={"nco"},
+ description=inspect.cleandoc(
+ """
+ Spell. Allows Ravens to deploy an advanced Auto-Turret,
+ that can attack enemy ground units in a straight line.
+ """
+ )),
+ ItemNames.RAVEN_HUNTER_SEEKER_WEAPON:
+ ItemData(366 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 15, SC2Race.TERRAN,
+ classification=ItemClassification.progression, parent_item=ItemNames.RAVEN, origin={"nco"},
+ description="Allows Ravens to attack with a Hunter-Seeker weapon."),
+ ItemNames.RAVEN_INTERFERENCE_MATRIX:
+ ItemData(367 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 16, SC2Race.TERRAN,
+ parent_item=ItemNames.RAVEN, origin={"ext"},
+ description=inspect.cleandoc(
+ """
+ Spell. Target enemy Mechanical or Psionic unit can't attack or use abilities for a short duration.
+ """
+ )),
+ ItemNames.RAVEN_ANTI_ARMOR_MISSILE:
+ ItemData(368 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 17, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.RAVEN, origin={"ext"},
+ description="Spell. Decreases target and nearby enemy units armor by 2."),
+ ItemNames.RAVEN_INTERNAL_TECH_MODULE:
+ ItemData(369 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 18, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.RAVEN, origin={"nco"},
+ description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Ravens", "Starport")),
+ ItemNames.SCIENCE_VESSEL_EMP_SHOCKWAVE:
+ ItemData(370 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 19, SC2Race.TERRAN,
+ parent_item=ItemNames.SCIENCE_VESSEL, origin={"bw"},
+ description="Spell. Depletes all energy and shields of all units in a target area."),
+ ItemNames.SCIENCE_VESSEL_DEFENSIVE_MATRIX:
+ ItemData(371 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 20, SC2Race.TERRAN,
+ parent_item=ItemNames.SCIENCE_VESSEL, origin={"bw"},
+ description=inspect.cleandoc(
+ """
+ Spell. Provides a target unit with a defensive barrier that can absorb up to 250 damage
+ """
+ )),
+ ItemNames.CYCLONE_TARGETING_OPTICS:
+ ItemData(372 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 21, SC2Race.TERRAN,
+ parent_item=ItemNames.CYCLONE, origin={"ext"},
+ description="Increases Cyclone Lock On casting range and the range while Locked On."),
+ ItemNames.CYCLONE_RAPID_FIRE_LAUNCHERS:
+ ItemData(373 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 22, SC2Race.TERRAN,
+ parent_item=ItemNames.CYCLONE, origin={"ext"},
+ description="The first 12 shots of Lock On are fired more quickly."),
+ ItemNames.LIBERATOR_CLOAK:
+ ItemData(374 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 23, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.LIBERATOR, origin={"nco"},
+ description=CLOAK_DESCRIPTION_TEMPLATE.format("Liberators")),
+ ItemNames.LIBERATOR_LASER_TARGETING_SYSTEM:
+ ItemData(375 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 24, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.LIBERATOR, origin={"ext"},
+ description=LASER_TARGETING_SYSTEMS_DESCRIPTION),
+ ItemNames.LIBERATOR_OPTIMIZED_LOGISTICS:
+ ItemData(376 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 25, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.LIBERATOR, origin={"nco"},
+ description="Increases Liberator training speed."),
+ ItemNames.WIDOW_MINE_BLACK_MARKET_LAUNCHERS:
+ ItemData(377 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 26, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.WIDOW_MINE, origin={"ext"},
+ description="Increases Widow Mine Sentinel Missile range."),
+ ItemNames.WIDOW_MINE_EXECUTIONER_MISSILES:
+ ItemData(378 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 27, SC2Race.TERRAN,
+ parent_item=ItemNames.WIDOW_MINE, origin={"ext"},
+ description=inspect.cleandoc(
+ """
+ Reduces Sentinel Missile cooldown.
+ When killed, Widow Mines will launch several missiles at random enemy targets.
+ """
+ )),
+ ItemNames.VALKYRIE_ENHANCED_CLUSTER_LAUNCHERS:
+ ItemData(379 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 28,
+ SC2Race.TERRAN, parent_item=ItemNames.VALKYRIE, origin={"ext"},
+ description="Valkyries fire 2 additional rockets each volley."),
+ ItemNames.VALKYRIE_SHAPED_HULL:
+ ItemData(380 + SC2WOL_ITEM_ID_OFFSET, "Armory 5", 29, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.VALKYRIE, origin={"ext"},
+ description="Increases Valkyrie life by 50."),
+ ItemNames.VALKYRIE_FLECHETTE_MISSILES:
+ ItemData(381 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 0, SC2Race.TERRAN,
+ parent_item=ItemNames.VALKYRIE, origin={"ext"},
+ description="Equips Valkyries with Air-to-Surface missiles to attack ground units."),
+ ItemNames.VALKYRIE_AFTERBURNERS:
+ ItemData(382 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 1, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.VALKYRIE, origin={"ext"},
+ description="Ability. Temporarily increases the Valkyries's movement speed by 70%."),
+ ItemNames.CYCLONE_INTERNAL_TECH_MODULE:
+ ItemData(383 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 2, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.CYCLONE, origin={"ext"},
+ description=INTERNAL_TECH_MODULE_DESCRIPTION_TEMPLATE.format("Cyclones", "Factory")),
+ ItemNames.LIBERATOR_SMART_SERVOS:
+ ItemData(384 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 3, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.LIBERATOR, origin={"nco"},
+ description=SMART_SERVOS_DESCRIPTION),
+ ItemNames.LIBERATOR_RESOURCE_EFFICIENCY:
+ ItemData(385 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 4, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.LIBERATOR, origin={"ext"},
+ description=RESOURCE_EFFICIENCY_NO_SUPPLY_DESCRIPTION_TEMPLATE.format("Liberator")),
+ ItemNames.HERCULES_INTERNAL_FUSION_MODULE:
+ ItemData(386 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 5, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.HERCULES, origin={"ext"},
+ description="Hercules can be trained from a Starport without having a Fusion Core."),
+ ItemNames.HERCULES_TACTICAL_JUMP:
+ ItemData(387 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 6, SC2Race.TERRAN,
+ parent_item=ItemNames.HERCULES, origin={"ext"},
+ description=inspect.cleandoc(
+ """
+ Allows Hercules to warp to a target location anywhere on the map.
+ """
+ )),
+ ItemNames.PLANETARY_FORTRESS_PROGRESSIVE_AUGMENTED_THRUSTERS:
+ ItemData(388 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 28, SC2Race.TERRAN,
+ parent_item=ItemNames.PLANETARY_FORTRESS, origin={"ext"}, quantity=2,
+ description=inspect.cleandoc(
+ """
+ Level 1: Lift Off - Planetary Fortress can lift off.
+ Level 2: Armament Stabilizers - Planetary Fortress can attack while lifted off.
+ """
+ )),
+ ItemNames.PLANETARY_FORTRESS_ADVANCED_TARGETING:
+ ItemData(389 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 7, SC2Race.TERRAN,
+ parent_item=ItemNames.PLANETARY_FORTRESS, origin={"ext"},
+ description="Planetary Fortress can attack air units."),
+ ItemNames.VALKYRIE_LAUNCHING_VECTOR_COMPENSATOR:
+ ItemData(390 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 8, SC2Race.TERRAN,
+ classification=ItemClassification.filler, parent_item=ItemNames.VALKYRIE, origin={"ext"},
+ description="Allows Valkyries to shoot air while moving."),
+ ItemNames.VALKYRIE_RESOURCE_EFFICIENCY:
+ ItemData(391 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 9, SC2Race.TERRAN,
+ parent_item=ItemNames.VALKYRIE, origin={"ext"},
+ description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("Valkyrie")),
+ ItemNames.PREDATOR_PREDATOR_S_FURY:
+ ItemData(392 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 10, SC2Race.TERRAN,
+ parent_item=ItemNames.PREDATOR, origin={"ext"},
+ description="Predators can use an attack that jumps between targets."),
+ ItemNames.BATTLECRUISER_BEHEMOTH_PLATING:
+ ItemData(393 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 11, SC2Race.TERRAN,
+ parent_item=ItemNames.BATTLECRUISER, origin={"ext"},
+ description="Increases Battlecruiser armor by 2."),
+ ItemNames.BATTLECRUISER_COVERT_OPS_ENGINES:
+ ItemData(394 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 12, SC2Race.TERRAN,
+ parent_item=ItemNames.BATTLECRUISER, origin={"nco"},
+ description="Increases Battlecruiser movement speed."),
+
+ #Buildings
+ ItemNames.BUNKER:
+ ItemData(400 + SC2WOL_ITEM_ID_OFFSET, "Building", 0, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Defensive structure. Able to load infantry units, giving them +1 range to their attacks."),
+ ItemNames.MISSILE_TURRET:
+ ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Anti-air defensive structure."),
+ ItemNames.SENSOR_TOWER:
+ ItemData(402 + SC2WOL_ITEM_ID_OFFSET, "Building", 2, SC2Race.TERRAN,
+ description="Reveals locations of enemy units at long range."),
+
+ ItemNames.WAR_PIGS:
+ ItemData(500 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 0, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Mercenary Marines"),
+ ItemNames.DEVIL_DOGS:
+ ItemData(501 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 1, SC2Race.TERRAN,
+ classification=ItemClassification.filler,
+ description="Mercenary Firebats"),
+ ItemNames.HAMMER_SECURITIES:
+ ItemData(502 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 2, SC2Race.TERRAN,
+ description="Mercenary Marauders"),
+ ItemNames.SPARTAN_COMPANY:
+ ItemData(503 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 3, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Mercenary Goliaths"),
+ ItemNames.SIEGE_BREAKERS:
+ ItemData(504 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 4, SC2Race.TERRAN,
+ description="Mercenary Siege Tanks"),
+ ItemNames.HELS_ANGELS:
+ ItemData(505 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 5, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Mercenary Vikings"),
+ ItemNames.DUSK_WINGS:
+ ItemData(506 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 6, SC2Race.TERRAN,
+ description="Mercenary Banshees"),
+ ItemNames.JACKSONS_REVENGE:
+ ItemData(507 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 7, SC2Race.TERRAN,
+ description="Mercenary Battlecruiser"),
+ ItemNames.SKIBIS_ANGELS:
+ ItemData(508 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 8, SC2Race.TERRAN,
+ origin={"ext"},
+ description="Mercenary Medics"),
+ ItemNames.DEATH_HEADS:
+ ItemData(509 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 9, SC2Race.TERRAN,
+ origin={"ext"},
+ description="Mercenary Reapers"),
+ ItemNames.WINGED_NIGHTMARES:
+ ItemData(510 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 10, SC2Race.TERRAN,
+ classification=ItemClassification.progression, origin={"ext"},
+ description="Mercenary Wraiths"),
+ ItemNames.MIDNIGHT_RIDERS:
+ ItemData(511 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 11, SC2Race.TERRAN,
+ origin={"ext"},
+ description="Mercenary Liberators"),
+ ItemNames.BRYNHILDS:
+ ItemData(512 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 12, SC2Race.TERRAN,
+ classification=ItemClassification.progression, origin={"ext"},
+ description="Mercenary Valkyries"),
+ ItemNames.JOTUN:
+ ItemData(513 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 13, SC2Race.TERRAN,
+ origin={"ext"},
+ description="Mercenary Thor"),
+
+ ItemNames.ULTRA_CAPACITORS:
+ ItemData(600 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 0, SC2Race.TERRAN,
+ description="Increases attack speed of units by 5% per weapon upgrade."),
+ ItemNames.VANADIUM_PLATING:
+ ItemData(601 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 1, SC2Race.TERRAN,
+ description="Increases the life of units by 5% per armor upgrade."),
+ ItemNames.ORBITAL_DEPOTS:
+ ItemData(602 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 2, SC2Race.TERRAN,
+ description="Supply depots are built instantly."),
+ ItemNames.MICRO_FILTERING:
+ ItemData(603 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 3, SC2Race.TERRAN,
+ description="Refineries produce Vespene gas 25% faster."),
+ ItemNames.AUTOMATED_REFINERY:
+ ItemData(604 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 4, SC2Race.TERRAN,
+ description="Eliminates the need for SCVs in vespene gas production."),
+ ItemNames.COMMAND_CENTER_REACTOR:
+ ItemData(605 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 5, SC2Race.TERRAN,
+ description="Command Centers can train two SCVs at once."),
+ ItemNames.RAVEN:
+ ItemData(606 + SC2WOL_ITEM_ID_OFFSET, "Unit", 22, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Aerial Caster unit."),
+ ItemNames.SCIENCE_VESSEL:
+ ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Unit", 23, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Aerial Caster unit. Can repair mechanical units."),
+ ItemNames.TECH_REACTOR:
+ ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 6, SC2Race.TERRAN,
+ description="Merges Tech Labs and Reactors into one add on structure to provide both functions."),
+ ItemNames.ORBITAL_STRIKE:
+ ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7, SC2Race.TERRAN,
+ description="Trained units from Barracks are instantly deployed on rally point."),
+ ItemNames.BUNKER_SHRIKE_TURRET:
+ ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6, SC2Race.TERRAN,
+ parent_item=ItemNames.BUNKER,
+ description="Adds an automated turret to Bunkers."),
+ ItemNames.BUNKER_FORTIFIED_BUNKER:
+ ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7, SC2Race.TERRAN,
+ parent_item=ItemNames.BUNKER,
+ description="Bunkers have more life."),
+ ItemNames.PLANETARY_FORTRESS:
+ ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Building", 3, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description=inspect.cleandoc(
+ """
+ Allows Command Centers to upgrade into a defensive structure with a turret and additional armor.
+ Planetary Fortresses cannot Lift Off, or cast Orbital Command spells.
+ """
+ )),
+ ItemNames.PERDITION_TURRET:
+ ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Building", 4, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Automated defensive turret. Burrows down while no enemies are nearby."),
+ ItemNames.PREDATOR:
+ ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Unit", 24, SC2Race.TERRAN,
+ classification=ItemClassification.filler,
+ description="Anti-infantry specialist that deals area damage with each attack."),
+ ItemNames.HERCULES:
+ ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Unit", 25, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Massive transport ship."),
+ ItemNames.CELLULAR_REACTOR:
+ ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8, SC2Race.TERRAN,
+ description="All Terran spellcasters get +100 starting and maximum energy."),
+ ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL:
+ ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 4, SC2Race.TERRAN, quantity=3,
+ classification= ItemClassification.progression,
+ description=inspect.cleandoc(
+ """
+ Allows Terran mechanical units to regenerate health while not in combat.
+ Each level increases life regeneration speed.
+ """
+ )),
+ ItemNames.HIVE_MIND_EMULATOR:
+ ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Building", 5, SC2Race.TERRAN,
+ ItemClassification.progression,
+ description="Defensive structure. Can permanently Mind Control Zerg units."),
+ ItemNames.PSI_DISRUPTER:
+ ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Building", 6, SC2Race.TERRAN,
+ classification=ItemClassification.progression,
+ description="Defensive structure. Slows the attack and movement speeds of all nearby Zerg units."),
+ ItemNames.STRUCTURE_ARMOR:
+ ItemData(620 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9, SC2Race.TERRAN,
+ description="Increases armor of all Terran structures by 2."),
+ ItemNames.HI_SEC_AUTO_TRACKING:
+ ItemData(621 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, SC2Race.TERRAN,
+ description="Increases attack range of all Terran structures by 1."),
+ ItemNames.ADVANCED_OPTICS:
+ ItemData(622 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, SC2Race.TERRAN,
+ description="Increases attack range of all Terran mechanical units by 1."),
+ ItemNames.ROGUE_FORCES:
+ ItemData(623 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, SC2Race.TERRAN,
+ description="Mercenary calldowns are no longer limited by charges."),
+
+ ItemNames.ZEALOT:
+ ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Unit", 0, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"wol", "lotv"},
+ description="Powerful melee warrior. Can use the charge ability."),
+ ItemNames.STALKER:
+ ItemData(701 + SC2WOL_ITEM_ID_OFFSET, "Unit", 1, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"wol", "lotv"},
+ description="Ranged attack strider. Can use the Blink ability."),
+ ItemNames.HIGH_TEMPLAR:
+ ItemData(702 + SC2WOL_ITEM_ID_OFFSET, "Unit", 2, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"wol", "lotv"},
+ description="Potent psionic master. Can use the Feedback and Psionic Storm abilities. Can merge into an Archon."),
+ ItemNames.DARK_TEMPLAR:
+ ItemData(703 + SC2WOL_ITEM_ID_OFFSET, "Unit", 3, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"wol", "lotv"},
+ description="Deadly warrior-assassin. Permanently cloaked. Can use the Shadow Fury ability."),
+ ItemNames.IMMORTAL:
+ ItemData(704 + SC2WOL_ITEM_ID_OFFSET, "Unit", 4, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"wol", "lotv"},
+ description="Assault strider. Can use Barrier to absorb damage."),
+ ItemNames.COLOSSUS:
+ ItemData(705 + SC2WOL_ITEM_ID_OFFSET, "Unit", 5, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"wol", "lotv"},
+ description="Battle strider with a powerful area attack. Can walk up and down cliffs. Attacks set fire to the ground, dealing extra damage to enemies over time."),
+ ItemNames.PHOENIX:
+ ItemData(706 + SC2WOL_ITEM_ID_OFFSET, "Unit", 6, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"wol", "lotv"},
+ description="Air superiority starfighter. Can use Graviton Beam and Phasing Armor abilities."),
+ ItemNames.VOID_RAY:
+ ItemData(707 + SC2WOL_ITEM_ID_OFFSET, "Unit", 7, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"wol", "lotv"},
+ description="Surgical strike craft. Has the Prismatic Alignment and Prismatic Range abilities."),
+ ItemNames.CARRIER:
+ ItemData(708 + SC2WOL_ITEM_ID_OFFSET, "Unit", 8, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"wol", "lotv"},
+ description="Capital ship. Builds and launches Interceptors that attack enemy targets. Repair Drones heal nearby mechanical units."),
+
+ # Filler items to fill remaining spots
+ ItemNames.STARTING_MINERALS:
+ ItemData(800 + SC2WOL_ITEM_ID_OFFSET, "Minerals", 15, SC2Race.ANY, quantity=0,
+ classification=ItemClassification.filler,
+ description="Increases the starting minerals for all missions."),
+ ItemNames.STARTING_VESPENE:
+ ItemData(801 + SC2WOL_ITEM_ID_OFFSET, "Vespene", 15, SC2Race.ANY, quantity=0,
+ classification=ItemClassification.filler,
+ description="Increases the starting vespene for all missions."),
+ ItemNames.STARTING_SUPPLY:
+ ItemData(802 + SC2WOL_ITEM_ID_OFFSET, "Supply", 2, SC2Race.ANY, quantity=0,
+ classification=ItemClassification.filler,
+ description="Increases the starting supply for all missions."),
+ # This item is used to "remove" location from the game. Never placed unless plando'd
+ ItemNames.NOTHING:
+ ItemData(803 + SC2WOL_ITEM_ID_OFFSET, "Nothing Group", 2, SC2Race.ANY, quantity=0,
+ classification=ItemClassification.trap,
+ description="Does nothing. Used to remove a location from the game."),
+
+ # Nova gear
+ ItemNames.NOVA_GHOST_VISOR:
+ ItemData(900 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 0, SC2Race.TERRAN, origin={"nco"},
+ description="Reveals the locations of enemy units in the fog of war around Nova. Can detect cloaked units."),
+ ItemNames.NOVA_RANGEFINDER_OCULUS:
+ ItemData(901 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 1, SC2Race.TERRAN, origin={"nco"},
+ description="Increaases Nova's vision range and non-melee weapon attack range by 2. Also increases range of melee weapons by 1."),
+ ItemNames.NOVA_DOMINATION:
+ ItemData(902 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 2, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Gives Nova the ability to mind-control a target enemy unit."),
+ ItemNames.NOVA_BLINK:
+ ItemData(903 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 3, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Gives Nova the ability to teleport a short distance and cloak for 10s."),
+ ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE:
+ ItemData(904 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade 2", 0, SC2Race.TERRAN, quantity=2, origin={"nco"},
+ classification=ItemClassification.progression,
+ description=inspect.cleandoc(
+ """
+ Level 1: Gives Nova the ability to cloak.
+ Level 2: Nova is permanently cloaked.
+ """
+ )),
+ ItemNames.NOVA_ENERGY_SUIT_MODULE:
+ ItemData(905 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 4, SC2Race.TERRAN, origin={"nco"},
+ description="Increases Nova's maximum energy and energy regeneration rate."),
+ ItemNames.NOVA_ARMORED_SUIT_MODULE:
+ ItemData(906 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 5, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Increases Nova's health by 100 and armour by 1. Nova also regenerates life quickly out of combat."),
+ ItemNames.NOVA_JUMP_SUIT_MODULE:
+ ItemData(907 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 6, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Increases Nova's movement speed and allows her to jump up and down cliffs."),
+ ItemNames.NOVA_C20A_CANISTER_RIFLE:
+ ItemData(908 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 7, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Allows Nova to equip the C20A Canister Rifle, which has a ranged attack and allows Nova to cast Snipe."),
+ ItemNames.NOVA_HELLFIRE_SHOTGUN:
+ ItemData(909 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 8, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Allows Nova to equip the Hellfire Shotgun, which has a short-range area attack in a cone and allows Nova to cast Penetrating Blast."),
+ ItemNames.NOVA_PLASMA_RIFLE:
+ ItemData(910 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 9, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Allows Nova to equip the Plasma Rifle, which has a rapidfire ranged attack and allows Nova to cast Plasma Shot."),
+ ItemNames.NOVA_MONOMOLECULAR_BLADE:
+ ItemData(911 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 10, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Allows Nova to equip the Monomolecular Blade, which has a melee attack and allows Nova to cast Dash Attack."),
+ ItemNames.NOVA_BLAZEFIRE_GUNBLADE:
+ ItemData(912 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 11, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Allows Nova to equip the Blazefire Gunblade, which has a melee attack and allows Nova to cast Fury of One."),
+ ItemNames.NOVA_STIM_INFUSION:
+ ItemData(913 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 12, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Gives Nova the ability to heal herself and temporarily increase her movement and attack speeds."),
+ ItemNames.NOVA_PULSE_GRENADES:
+ ItemData(914 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 13, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Gives Nova the ability to throw a grenade dealing large damage in an area."),
+ ItemNames.NOVA_FLASHBANG_GRENADES:
+ ItemData(915 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 14, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Gives Nova the ability to throw a grenade to stun enemies and disable detection in a large area."),
+ ItemNames.NOVA_IONIC_FORCE_FIELD:
+ ItemData(916 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 15, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Gives Nova the ability to shield herself temporarily."),
+ ItemNames.NOVA_HOLO_DECOY:
+ ItemData(917 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 16, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Gives Nova the ability to summon a decoy unit which enemies will prefer to target and takes reduced damage."),
+ ItemNames.NOVA_NUKE:
+ ItemData(918 + SC2WOL_ITEM_ID_OFFSET, "Nova Gear", 17, SC2Race.TERRAN, origin={"nco"},
+ classification=ItemClassification.progression,
+ description="Gives Nova the ability to launch tactical nukes built from the Shadow Ops."),
+
+ # HotS
+ ItemNames.ZERGLING:
+ ItemData(0 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 0, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"hots"},
+ description="Fast inexpensive melee attacker. Hatches in pairs from a single larva. Can morph into a Baneling."),
+ ItemNames.SWARM_QUEEN:
+ ItemData(1 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 1, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"hots"},
+ description="Ranged support caster. Can use the Spawn Creep Tumor and Rapid Transfusion abilities."),
+ ItemNames.ROACH:
+ ItemData(2 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 2, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"hots"},
+ description="Durable short ranged attacker. Regenerates life quickly when burrowed."),
+ ItemNames.HYDRALISK:
+ ItemData(3 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 3, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"hots"},
+ description="High-damage generalist ranged attacker."),
+ ItemNames.ZERGLING_BANELING_ASPECT:
+ ItemData(4 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 5, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"hots"},
+ description="Anti-ground suicide unit. Does damage over a small area on death."),
+ ItemNames.ABERRATION:
+ ItemData(5 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 5, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"hots"},
+ description="Durable melee attacker that deals heavy damage and can walk over other units."),
+ ItemNames.MUTALISK:
+ ItemData(6 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 6, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"hots"},
+ description="Fragile flying attacker. Attacks bounce between targets."),
+ ItemNames.SWARM_HOST:
+ ItemData(7 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 7, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"hots"},
+ description="Siege unit that attacks by rooting in place and continually spawning Locusts."),
+ ItemNames.INFESTOR:
+ ItemData(8 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 8, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"hots"},
+ description="Support caster that can move while burrowed. Can use the Fungal Growth, Parasitic Domination, and Consumption abilities."),
+ ItemNames.ULTRALISK:
+ ItemData(9 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 9, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"hots"},
+ description="Massive melee attacker. Has an area-damage cleave attack."),
+ ItemNames.SPORE_CRAWLER:
+ ItemData(10 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 10, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"hots"},
+ description="Anti-air defensive structure that can detect cloaked units."),
+ ItemNames.SPINE_CRAWLER:
+ ItemData(11 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 11, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"hots"},
+ description="Anti-ground defensive structure."),
+ ItemNames.CORRUPTOR:
+ ItemData(12 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 12, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"ext"},
+ description="Anti-air flying attacker specializing in taking down enemy capital ships."),
+ ItemNames.SCOURGE:
+ ItemData(13 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 13, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"bw", "ext"},
+ description="Flying anti-air suicide unit. Hatches in pairs from a single larva."),
+ ItemNames.BROOD_QUEEN:
+ ItemData(14 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 4, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"bw", "ext"},
+ description="Flying support caster. Can cast the Ocular Symbiote and Spawn Broodlings abilities."),
+ ItemNames.DEFILER:
+ ItemData(15 + SC2HOTS_ITEM_ID_OFFSET, "Unit", 14, SC2Race.ZERG,
+ classification=ItemClassification.progression, origin={"bw"},
+ description="Support caster. Can use the Dark Swarm, Consume, and Plague abilities."),
+
+ ItemNames.PROGRESSIVE_ZERG_MELEE_ATTACK: ItemData(100 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 0, SC2Race.ZERG, quantity=3, origin={"hots"}),
+ ItemNames.PROGRESSIVE_ZERG_MISSILE_ATTACK: ItemData(101 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 2, SC2Race.ZERG, quantity=3, origin={"hots"}),
+ ItemNames.PROGRESSIVE_ZERG_GROUND_CARAPACE: ItemData(102 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 4, SC2Race.ZERG, quantity=3, origin={"hots"}),
+ ItemNames.PROGRESSIVE_ZERG_FLYER_ATTACK: ItemData(103 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 6, SC2Race.ZERG, quantity=3, origin={"hots"}),
+ ItemNames.PROGRESSIVE_ZERG_FLYER_CARAPACE: ItemData(104 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 8, SC2Race.ZERG, quantity=3, origin={"hots"}),
+ # Upgrade bundle 'number' values are used as indices to get affected 'number's
+ ItemNames.PROGRESSIVE_ZERG_WEAPON_UPGRADE: ItemData(105 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 6, SC2Race.ZERG, quantity=3, origin={"hots"}),
+ ItemNames.PROGRESSIVE_ZERG_ARMOR_UPGRADE: ItemData(106 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 7, SC2Race.ZERG, quantity=3, origin={"hots"}),
+ ItemNames.PROGRESSIVE_ZERG_GROUND_UPGRADE: ItemData(107 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 8, SC2Race.ZERG, quantity=3, origin={"hots"}),
+ ItemNames.PROGRESSIVE_ZERG_FLYER_UPGRADE: ItemData(108 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 9, SC2Race.ZERG, quantity=3, origin={"hots"}),
+ ItemNames.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE: ItemData(109 + SC2HOTS_ITEM_ID_OFFSET, "Upgrade", 10, SC2Race.ZERG, quantity=3, origin={"hots"}),
+
+ ItemNames.ZERGLING_HARDENED_CARAPACE:
+ ItemData(200 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 0, SC2Race.ZERG, parent_item=ItemNames.ZERGLING,
+ origin={"hots"}, description="Increases Zergling health by +10."),
+ ItemNames.ZERGLING_ADRENAL_OVERLOAD:
+ ItemData(201 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 1, SC2Race.ZERG, parent_item=ItemNames.ZERGLING,
+ origin={"hots"}, description="Increases Zergling attack speed."),
+ ItemNames.ZERGLING_METABOLIC_BOOST:
+ ItemData(202 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 2, SC2Race.ZERG, parent_item=ItemNames.ZERGLING,
+ origin={"hots"}, classification=ItemClassification.filler,
+ description="Increases Zergling movement speed."),
+ ItemNames.ROACH_HYDRIODIC_BILE:
+ ItemData(203 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 3, SC2Race.ZERG, parent_item=ItemNames.ROACH,
+ origin={"hots"}, description="Roaches deal +8 damage to light targets."),
+ ItemNames.ROACH_ADAPTIVE_PLATING:
+ ItemData(204 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 4, SC2Race.ZERG, parent_item=ItemNames.ROACH,
+ origin={"hots"}, description="Roaches gain +3 armour when their life is below 50%."),
+ ItemNames.ROACH_TUNNELING_CLAWS:
+ ItemData(205 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 5, SC2Race.ZERG, parent_item=ItemNames.ROACH,
+ origin={"hots"}, classification=ItemClassification.filler,
+ description="Allows Roaches to move while burrowed."),
+ ItemNames.HYDRALISK_FRENZY:
+ ItemData(206 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 6, SC2Race.ZERG, parent_item=ItemNames.HYDRALISK,
+ origin={"hots"},
+ description="Allows Hydralisks to use the Frenzy ability, which increases their attack speed by 50%."),
+ ItemNames.HYDRALISK_ANCILLARY_CARAPACE:
+ ItemData(207 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 7, SC2Race.ZERG, parent_item=ItemNames.HYDRALISK,
+ origin={"hots"}, classification=ItemClassification.filler, description="Hydralisks gain +20 health."),
+ ItemNames.HYDRALISK_GROOVED_SPINES:
+ ItemData(208 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 8, SC2Race.ZERG, parent_item=ItemNames.HYDRALISK,
+ origin={"hots"}, description="Hydralisks gain +1 range."),
+ ItemNames.BANELING_CORROSIVE_ACID:
+ ItemData(209 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 9, SC2Race.ZERG,
+ parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"hots"},
+ description="Increases the damage banelings deal to their primary target. Splash damage remains the same."),
+ ItemNames.BANELING_RUPTURE:
+ ItemData(210 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 10, SC2Race.ZERG,
+ parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"hots"},
+ classification=ItemClassification.filler,
+ description="Increases the splash radius of baneling attacks."),
+ ItemNames.BANELING_REGENERATIVE_ACID:
+ ItemData(211 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 11, SC2Race.ZERG,
+ parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"hots"},
+ classification=ItemClassification.filler,
+ description="Banelings will heal nearby friendly units when they explode."),
+ ItemNames.MUTALISK_VICIOUS_GLAIVE:
+ ItemData(212 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 12, SC2Race.ZERG, parent_item=ItemNames.MUTALISK,
+ origin={"hots"}, description="Mutalisks attacks will bounce an additional 3 times."),
+ ItemNames.MUTALISK_RAPID_REGENERATION:
+ ItemData(213 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 13, SC2Race.ZERG, parent_item=ItemNames.MUTALISK,
+ origin={"hots"}, description="Mutalisks will regenerate quickly when out of combat."),
+ ItemNames.MUTALISK_SUNDERING_GLAIVE:
+ ItemData(214 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 14, SC2Race.ZERG, parent_item=ItemNames.MUTALISK,
+ origin={"hots"}, description="Mutalisks deal increased damage to their primary target."),
+ ItemNames.SWARM_HOST_BURROW:
+ ItemData(215 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 15, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST,
+ origin={"hots"}, classification=ItemClassification.filler,
+ description="Allows Swarm Hosts to burrow instead of root to spawn locusts."),
+ ItemNames.SWARM_HOST_RAPID_INCUBATION:
+ ItemData(216 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 16, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST,
+ origin={"hots"}, description="Swarm Hosts will spawn locusts 20% faster."),
+ ItemNames.SWARM_HOST_PRESSURIZED_GLANDS:
+ ItemData(217 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 17, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST,
+ origin={"hots"}, classification=ItemClassification.progression,
+ description="Allows Swarm Host Locusts to attack air targets."),
+ ItemNames.ULTRALISK_BURROW_CHARGE:
+ ItemData(218 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 18, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK,
+ origin={"hots"},
+ description="Allows Ultralisks to burrow and charge at enemy units, knocking back and stunning units when it emerges."),
+ ItemNames.ULTRALISK_TISSUE_ASSIMILATION:
+ ItemData(219 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 19, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK,
+ origin={"hots"}, description="Ultralisks recover health when they deal damage."),
+ ItemNames.ULTRALISK_MONARCH_BLADES:
+ ItemData(220 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 20, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK,
+ origin={"hots"}, description="Ultralisks gain increased splash damage."),
+ ItemNames.CORRUPTOR_CAUSTIC_SPRAY:
+ ItemData(221 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 21, SC2Race.ZERG, parent_item=ItemNames.CORRUPTOR,
+ origin={"ext"},
+ description="Allows Corruptors to use the Caustic Spray ability, which deals ramping damage to buildings over time."),
+ ItemNames.CORRUPTOR_CORRUPTION:
+ ItemData(222 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 22, SC2Race.ZERG, parent_item=ItemNames.CORRUPTOR,
+ origin={"ext"},
+ description="Allows Corruptors to use the Corruption ability, which causes a target enemy unit to take increased damage."),
+ ItemNames.SCOURGE_VIRULENT_SPORES:
+ ItemData(223 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 23, SC2Race.ZERG, parent_item=ItemNames.SCOURGE,
+ origin={"ext"}, description="Scourge will deal splash damage."),
+ ItemNames.SCOURGE_RESOURCE_EFFICIENCY:
+ ItemData(224 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 24, SC2Race.ZERG, parent_item=ItemNames.SCOURGE,
+ origin={"ext"}, classification=ItemClassification.progression,
+ description="Reduces the cost of Scourge by 50 gas per egg."),
+ ItemNames.SCOURGE_SWARM_SCOURGE:
+ ItemData(225 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 25, SC2Race.ZERG, parent_item=ItemNames.SCOURGE,
+ origin={"ext"}, description="An extra Scourge will be built from each egg at no additional cost."),
+ ItemNames.ZERGLING_SHREDDING_CLAWS:
+ ItemData(226 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 26, SC2Race.ZERG, parent_item=ItemNames.ZERGLING,
+ origin={"ext"}, description="Zergling attacks will temporarily reduce their target's armour to 0."),
+ ItemNames.ROACH_GLIAL_RECONSTITUTION:
+ ItemData(227 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 27, SC2Race.ZERG, parent_item=ItemNames.ROACH,
+ origin={"ext"}, description="Increases Roach movement speed."),
+ ItemNames.ROACH_ORGANIC_CARAPACE:
+ ItemData(228 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 28, SC2Race.ZERG, parent_item=ItemNames.ROACH,
+ origin={"ext"}, description="Increases Roach health by +25."),
+ ItemNames.HYDRALISK_MUSCULAR_AUGMENTS:
+ ItemData(229 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 1", 29, SC2Race.ZERG, parent_item=ItemNames.HYDRALISK,
+ origin={"bw"}, description="Increases Hydralisk movement speed."),
+ ItemNames.HYDRALISK_RESOURCE_EFFICIENCY:
+ ItemData(230 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 0, SC2Race.ZERG, parent_item=ItemNames.HYDRALISK,
+ origin={"bw"}, description="Reduces Hydralisk resource cost by 25/25 and supply cost by 1."),
+ ItemNames.BANELING_CENTRIFUGAL_HOOKS:
+ ItemData(231 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 1, SC2Race.ZERG,
+ parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"ext"},
+ description="Increases the movement speed of Banelings."),
+ ItemNames.BANELING_TUNNELING_JAWS:
+ ItemData(232 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 2, SC2Race.ZERG,
+ parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"ext"},
+ description="Allows Banelings to move while burrowed."),
+ ItemNames.BANELING_RAPID_METAMORPH:
+ ItemData(233 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 3, SC2Race.ZERG,
+ parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"ext"}, description="Banelings morph faster."),
+ ItemNames.MUTALISK_SEVERING_GLAIVE:
+ ItemData(234 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 4, SC2Race.ZERG, parent_item=ItemNames.MUTALISK,
+ origin={"ext"}, description="Mutalisk bounce attacks will deal full damage."),
+ ItemNames.MUTALISK_AERODYNAMIC_GLAIVE_SHAPE:
+ ItemData(235 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 5, SC2Race.ZERG, parent_item=ItemNames.MUTALISK,
+ origin={"ext"}, description="Increases the attack range of Mutalisks by 2."),
+ ItemNames.SWARM_HOST_LOCUST_METABOLIC_BOOST:
+ ItemData(236 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 6, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST,
+ origin={"ext"}, classification=ItemClassification.filler,
+ description="Increases Locust movement speed."),
+ ItemNames.SWARM_HOST_ENDURING_LOCUSTS:
+ ItemData(237 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 7, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST,
+ origin={"ext"}, description="Increases the duration of Swarm Hosts' Locusts by 10s."),
+ ItemNames.SWARM_HOST_ORGANIC_CARAPACE:
+ ItemData(238 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 8, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST,
+ origin={"ext"}, description="Increases Swarm Host health by +40."),
+ ItemNames.SWARM_HOST_RESOURCE_EFFICIENCY:
+ ItemData(239 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 9, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST,
+ origin={"ext"}, description="Reduces Swarm Host resource cost by 100/25."),
+ ItemNames.ULTRALISK_ANABOLIC_SYNTHESIS:
+ ItemData(240 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 10, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK,
+ origin={"bw"}, classification=ItemClassification.filler),
+ ItemNames.ULTRALISK_CHITINOUS_PLATING:
+ ItemData(241 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 11, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK,
+ origin={"bw"}),
+ ItemNames.ULTRALISK_ORGANIC_CARAPACE:
+ ItemData(242 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 12, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK,
+ origin={"ext"}),
+ ItemNames.ULTRALISK_RESOURCE_EFFICIENCY:
+ ItemData(243 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 13, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK,
+ origin={"bw"}),
+ ItemNames.DEVOURER_CORROSIVE_SPRAY:
+ ItemData(244 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 14, SC2Race.ZERG,
+ parent_item=ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT, origin={"ext"}),
+ ItemNames.DEVOURER_GAPING_MAW:
+ ItemData(245 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 15, SC2Race.ZERG,
+ parent_item=ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT, origin={"ext"}),
+ ItemNames.DEVOURER_IMPROVED_OSMOSIS:
+ ItemData(246 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 16, SC2Race.ZERG,
+ parent_item=ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT, origin={"ext"},
+ classification=ItemClassification.filler),
+ ItemNames.DEVOURER_PRESCIENT_SPORES:
+ ItemData(247 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 17, SC2Race.ZERG,
+ parent_item=ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT, origin={"ext"}),
+ ItemNames.GUARDIAN_PROLONGED_DISPERSION:
+ ItemData(248 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 18, SC2Race.ZERG,
+ parent_item=ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT, origin={"ext"}),
+ ItemNames.GUARDIAN_PRIMAL_ADAPTATION:
+ ItemData(249 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 19, SC2Race.ZERG,
+ parent_item=ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT, origin={"ext"}),
+ ItemNames.GUARDIAN_SORONAN_ACID:
+ ItemData(250 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 20, SC2Race.ZERG,
+ parent_item=ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT, origin={"ext"}),
+ ItemNames.IMPALER_ADAPTIVE_TALONS:
+ ItemData(251 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 21, SC2Race.ZERG,
+ parent_item=ItemNames.HYDRALISK_IMPALER_ASPECT, origin={"ext"},
+ classification=ItemClassification.filler),
+ ItemNames.IMPALER_SECRETION_GLANDS:
+ ItemData(252 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 22, SC2Race.ZERG,
+ parent_item=ItemNames.HYDRALISK_IMPALER_ASPECT, origin={"ext"}),
+ ItemNames.IMPALER_HARDENED_TENTACLE_SPINES:
+ ItemData(253 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 23, SC2Race.ZERG,
+ parent_item=ItemNames.HYDRALISK_IMPALER_ASPECT, origin={"ext"}),
+ ItemNames.LURKER_SEISMIC_SPINES:
+ ItemData(254 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 24, SC2Race.ZERG,
+ parent_item=ItemNames.HYDRALISK_LURKER_ASPECT, origin={"ext"}),
+ ItemNames.LURKER_ADAPTED_SPINES:
+ ItemData(255 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 25, SC2Race.ZERG,
+ parent_item=ItemNames.HYDRALISK_LURKER_ASPECT, origin={"ext"}),
+ ItemNames.RAVAGER_POTENT_BILE:
+ ItemData(256 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 26, SC2Race.ZERG,
+ parent_item=ItemNames.ROACH_RAVAGER_ASPECT, origin={"ext"}),
+ ItemNames.RAVAGER_BLOATED_BILE_DUCTS:
+ ItemData(257 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 27, SC2Race.ZERG,
+ parent_item=ItemNames.ROACH_RAVAGER_ASPECT, origin={"ext"}),
+ ItemNames.RAVAGER_DEEP_TUNNEL:
+ ItemData(258 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 28, SC2Race.ZERG,
+ parent_item=ItemNames.ROACH_RAVAGER_ASPECT, origin={"ext"}),
+ ItemNames.VIPER_PARASITIC_BOMB:
+ ItemData(259 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 2", 29, SC2Race.ZERG,
+ parent_item=ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT, origin={"ext"}),
+ ItemNames.VIPER_PARALYTIC_BARBS:
+ ItemData(260 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 0, SC2Race.ZERG,
+ parent_item=ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT, origin={"ext"}),
+ ItemNames.VIPER_VIRULENT_MICROBES:
+ ItemData(261 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 1, SC2Race.ZERG,
+ parent_item=ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT, origin={"ext"}),
+ ItemNames.BROOD_LORD_POROUS_CARTILAGE:
+ ItemData(262 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 2, SC2Race.ZERG,
+ parent_item=ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, origin={"ext"}),
+ ItemNames.BROOD_LORD_EVOLVED_CARAPACE:
+ ItemData(263 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 3, SC2Race.ZERG,
+ parent_item=ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, origin={"ext"}),
+ ItemNames.BROOD_LORD_SPLITTER_MITOSIS:
+ ItemData(264 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 4, SC2Race.ZERG,
+ parent_item=ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, origin={"ext"}),
+ ItemNames.BROOD_LORD_RESOURCE_EFFICIENCY:
+ ItemData(265 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 5, SC2Race.ZERG,
+ parent_item=ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, origin={"ext"}),
+ ItemNames.INFESTOR_INFESTED_TERRAN:
+ ItemData(266 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 6, SC2Race.ZERG, parent_item=ItemNames.INFESTOR,
+ origin={"ext"}),
+ ItemNames.INFESTOR_MICROBIAL_SHROUD:
+ ItemData(267 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 7, SC2Race.ZERG, parent_item=ItemNames.INFESTOR,
+ origin={"ext"}),
+ ItemNames.SWARM_QUEEN_SPAWN_LARVAE:
+ ItemData(268 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 8, SC2Race.ZERG, parent_item=ItemNames.SWARM_QUEEN,
+ origin={"ext"}),
+ ItemNames.SWARM_QUEEN_DEEP_TUNNEL:
+ ItemData(269 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 9, SC2Race.ZERG, parent_item=ItemNames.SWARM_QUEEN,
+ origin={"ext"}),
+ ItemNames.SWARM_QUEEN_ORGANIC_CARAPACE:
+ ItemData(270 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 10, SC2Race.ZERG, parent_item=ItemNames.SWARM_QUEEN,
+ origin={"ext"}, classification=ItemClassification.filler),
+ ItemNames.SWARM_QUEEN_BIO_MECHANICAL_TRANSFUSION:
+ ItemData(271 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 11, SC2Race.ZERG, parent_item=ItemNames.SWARM_QUEEN,
+ origin={"ext"}),
+ ItemNames.SWARM_QUEEN_RESOURCE_EFFICIENCY:
+ ItemData(272 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 12, SC2Race.ZERG, parent_item=ItemNames.SWARM_QUEEN,
+ origin={"ext"}),
+ ItemNames.SWARM_QUEEN_INCUBATOR_CHAMBER:
+ ItemData(273 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 13, SC2Race.ZERG, parent_item=ItemNames.SWARM_QUEEN,
+ origin={"ext"}),
+ ItemNames.BROOD_QUEEN_FUNGAL_GROWTH:
+ ItemData(274 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 14, SC2Race.ZERG, parent_item=ItemNames.BROOD_QUEEN,
+ origin={"ext"}),
+ ItemNames.BROOD_QUEEN_ENSNARE:
+ ItemData(275 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 15, SC2Race.ZERG, parent_item=ItemNames.BROOD_QUEEN,
+ origin={"ext"}),
+ ItemNames.BROOD_QUEEN_ENHANCED_MITOCHONDRIA:
+ ItemData(276 + SC2HOTS_ITEM_ID_OFFSET, "Mutation 3", 16, SC2Race.ZERG, parent_item=ItemNames.BROOD_QUEEN,
+ origin={"ext"}),
+
+ ItemNames.ZERGLING_RAPTOR_STRAIN:
+ ItemData(300 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 0, SC2Race.ZERG, parent_item=ItemNames.ZERGLING,
+ origin={"hots"},
+ description="Allows Zerglings to jump up and down cliffs and leap onto enemies. Also increases Zergling attack damage by 2."),
+ ItemNames.ZERGLING_SWARMLING_STRAIN:
+ ItemData(301 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 1, SC2Race.ZERG, parent_item=ItemNames.ZERGLING,
+ origin={"hots"},
+ description="Zerglings will spawn instantly and with an extra Zergling per egg at no additional cost."),
+ ItemNames.ROACH_VILE_STRAIN:
+ ItemData(302 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 2, SC2Race.ZERG, parent_item=ItemNames.ROACH, origin={"hots"},
+ description="Roach attacks will slow the movement and attack speed of enemies."),
+ ItemNames.ROACH_CORPSER_STRAIN:
+ ItemData(303 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 3, SC2Race.ZERG, parent_item=ItemNames.ROACH, origin={"hots"},
+ description="Units killed after being attacked by Roaches will spawn 2 Roachlings."),
+ ItemNames.HYDRALISK_IMPALER_ASPECT:
+ ItemData(304 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 0, SC2Race.ZERG, origin={"hots"},
+ classification=ItemClassification.progression,
+ description="Allows Hydralisks to morph into Impalers."),
+ ItemNames.HYDRALISK_LURKER_ASPECT:
+ ItemData(305 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 1, SC2Race.ZERG, origin={"hots"},
+ classification=ItemClassification.progression, description="Allows Hydralisks to morph into Lurkers."),
+ ItemNames.BANELING_SPLITTER_STRAIN:
+ ItemData(306 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 6, SC2Race.ZERG,
+ parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"hots"},
+ description="Banelings will split into two smaller Splitterlings on exploding."),
+ ItemNames.BANELING_HUNTER_STRAIN:
+ ItemData(307 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 7, SC2Race.ZERG,
+ parent_item=ItemNames.ZERGLING_BANELING_ASPECT, origin={"hots"},
+ description="Allows Banelings to jump up and down cliffs and leap onto enemies."),
+ ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT:
+ ItemData(308 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 2, SC2Race.ZERG, origin={"hots"},
+ classification=ItemClassification.progression,
+ description="Allows Mutalisks and Corruptors to morph into Brood Lords."),
+ ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT:
+ ItemData(309 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 3, SC2Race.ZERG, origin={"hots"},
+ classification=ItemClassification.progression,
+ description="Allows Mutalisks and Corruptors to morph into Vipers."),
+ ItemNames.SWARM_HOST_CARRION_STRAIN:
+ ItemData(310 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 10, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST,
+ origin={"hots"}, description="Swarm Hosts will spawn Flying Locusts."),
+ ItemNames.SWARM_HOST_CREEPER_STRAIN:
+ ItemData(311 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 11, SC2Race.ZERG, parent_item=ItemNames.SWARM_HOST,
+ origin={"hots"}, classification=ItemClassification.filler,
+ description="Allows Swarm Hosts to teleport to any creep on the map in vision. Swarm Hosts will spread creep around them when rooted or burrowed."),
+ ItemNames.ULTRALISK_NOXIOUS_STRAIN:
+ ItemData(312 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 12, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK,
+ origin={"hots"}, classification=ItemClassification.filler,
+ description="Ultralisks will periodically spread poison, damaging nearby biological enemies."),
+ ItemNames.ULTRALISK_TORRASQUE_STRAIN:
+ ItemData(313 + SC2HOTS_ITEM_ID_OFFSET, "Strain", 13, SC2Race.ZERG, parent_item=ItemNames.ULTRALISK,
+ origin={"hots"}, description="Ultralisks will revive after being killed."),
+
+ ItemNames.KERRIGAN_KINETIC_BLAST: ItemData(400 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 0, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_HEROIC_FORTITUDE: ItemData(401 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 1, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_LEAPING_STRIKE: ItemData(402 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 2, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_CRUSHING_GRIP: ItemData(403 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 3, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_CHAIN_REACTION: ItemData(404 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 4, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_PSIONIC_SHIFT: ItemData(405 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 5, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_ZERGLING_RECONSTITUTION: ItemData(406 + SC2HOTS_ITEM_ID_OFFSET, "Evolution Pit", 0, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.filler),
+ ItemNames.KERRIGAN_IMPROVED_OVERLORDS: ItemData(407 + SC2HOTS_ITEM_ID_OFFSET, "Evolution Pit", 1, SC2Race.ZERG, origin={"hots"}),
+ ItemNames.KERRIGAN_AUTOMATED_EXTRACTORS: ItemData(408 + SC2HOTS_ITEM_ID_OFFSET, "Evolution Pit", 2, SC2Race.ZERG, origin={"hots"}),
+ ItemNames.KERRIGAN_WILD_MUTATION: ItemData(409 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 6, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_SPAWN_BANELINGS: ItemData(410 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 7, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_MEND: ItemData(411 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 8, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_TWIN_DRONES: ItemData(412 + SC2HOTS_ITEM_ID_OFFSET, "Evolution Pit", 3, SC2Race.ZERG, origin={"hots"}),
+ ItemNames.KERRIGAN_MALIGNANT_CREEP: ItemData(413 + SC2HOTS_ITEM_ID_OFFSET, "Evolution Pit", 4, SC2Race.ZERG, origin={"hots"}),
+ ItemNames.KERRIGAN_VESPENE_EFFICIENCY: ItemData(414 + SC2HOTS_ITEM_ID_OFFSET, "Evolution Pit", 5, SC2Race.ZERG, origin={"hots"}),
+ ItemNames.KERRIGAN_INFEST_BROODLINGS: ItemData(415 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 9, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_FURY: ItemData(416 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 10, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_ABILITY_EFFICIENCY: ItemData(417 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 11, SC2Race.ZERG, origin={"hots"}),
+ ItemNames.KERRIGAN_APOCALYPSE: ItemData(418 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 12, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_SPAWN_LEVIATHAN: ItemData(419 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 13, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_DROP_PODS: ItemData(420 + SC2HOTS_ITEM_ID_OFFSET, "Ability", 14, SC2Race.ZERG, origin={"hots"}, classification=ItemClassification.progression),
+ # Handled separately from other abilities
+ ItemNames.KERRIGAN_PRIMAL_FORM: ItemData(421 + SC2HOTS_ITEM_ID_OFFSET, "Primal Form", 0, SC2Race.ZERG, origin={"hots"}),
+
+ ItemNames.KERRIGAN_LEVELS_10: ItemData(500 + SC2HOTS_ITEM_ID_OFFSET, "Level", 10, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_LEVELS_9: ItemData(501 + SC2HOTS_ITEM_ID_OFFSET, "Level", 9, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_LEVELS_8: ItemData(502 + SC2HOTS_ITEM_ID_OFFSET, "Level", 8, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_LEVELS_7: ItemData(503 + SC2HOTS_ITEM_ID_OFFSET, "Level", 7, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_LEVELS_6: ItemData(504 + SC2HOTS_ITEM_ID_OFFSET, "Level", 6, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_LEVELS_5: ItemData(505 + SC2HOTS_ITEM_ID_OFFSET, "Level", 5, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_LEVELS_4: ItemData(506 + SC2HOTS_ITEM_ID_OFFSET, "Level", 4, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression_skip_balancing),
+ ItemNames.KERRIGAN_LEVELS_3: ItemData(507 + SC2HOTS_ITEM_ID_OFFSET, "Level", 3, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression_skip_balancing),
+ ItemNames.KERRIGAN_LEVELS_2: ItemData(508 + SC2HOTS_ITEM_ID_OFFSET, "Level", 2, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression_skip_balancing),
+ ItemNames.KERRIGAN_LEVELS_1: ItemData(509 + SC2HOTS_ITEM_ID_OFFSET, "Level", 1, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression_skip_balancing),
+ ItemNames.KERRIGAN_LEVELS_14: ItemData(510 + SC2HOTS_ITEM_ID_OFFSET, "Level", 14, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_LEVELS_35: ItemData(511 + SC2HOTS_ITEM_ID_OFFSET, "Level", 35, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression),
+ ItemNames.KERRIGAN_LEVELS_70: ItemData(512 + SC2HOTS_ITEM_ID_OFFSET, "Level", 70, SC2Race.ZERG, origin={"hots"}, quantity=0, classification=ItemClassification.progression),
+
+ # Zerg Mercs
+ ItemNames.INFESTED_MEDICS: ItemData(600 + SC2HOTS_ITEM_ID_OFFSET, "Mercenary", 0, SC2Race.ZERG, origin={"ext"}),
+ ItemNames.INFESTED_SIEGE_TANKS: ItemData(601 + SC2HOTS_ITEM_ID_OFFSET, "Mercenary", 1, SC2Race.ZERG, origin={"ext"}),
+ ItemNames.INFESTED_BANSHEES: ItemData(602 + SC2HOTS_ITEM_ID_OFFSET, "Mercenary", 2, SC2Race.ZERG, origin={"ext"}),
+
+ # Misc Upgrades
+ ItemNames.OVERLORD_VENTRAL_SACS: ItemData(700 + SC2HOTS_ITEM_ID_OFFSET, "Evolution Pit", 6, SC2Race.ZERG, origin={"bw"}),
+
+ # Morphs
+ ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT: ItemData(800 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 6, SC2Race.ZERG, origin={"bw"}),
+ ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT: ItemData(801 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 7, SC2Race.ZERG, origin={"bw"}),
+ ItemNames.ROACH_RAVAGER_ASPECT: ItemData(802 + SC2HOTS_ITEM_ID_OFFSET, "Morph", 8, SC2Race.ZERG, origin={"ext"}),
+
+
+ # Protoss Units (those that aren't as items in WoL (Prophecy))
+ ItemNames.OBSERVER: ItemData(0 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 9, SC2Race.PROTOSS,
+ classification=ItemClassification.filler, origin={"wol"},
+ description="Flying spy. Cloak renders the unit invisible to enemies without detection."),
+ ItemNames.CENTURION: ItemData(1 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 10, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Powerful melee warrior. Has the Shadow Charge and Darkcoil abilities."),
+ ItemNames.SENTINEL: ItemData(2 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 11, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Powerful melee warrior. Has the Charge and Reconstruction abilities."),
+ ItemNames.SUPPLICANT: ItemData(3 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 12, SC2Race.PROTOSS,
+ classification=ItemClassification.filler, important_for_filtering=True, origin={"ext"},
+ description="Powerful melee warrior. Has powerful damage resistant shields."),
+ ItemNames.INSTIGATOR: ItemData(4 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 13, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"ext"},
+ description="Ranged support strider. Can store multiple Blink charges."),
+ ItemNames.SLAYER: ItemData(5 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 14, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"ext"},
+ description="Ranged attack strider. Can use the Phase Blink and Phasing Armor abilities."),
+ ItemNames.SENTRY: ItemData(6 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 15, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Robotic support unit can use the Guardian Shield ability and restore the shields of nearby Protoss units."),
+ ItemNames.ENERGIZER: ItemData(7 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 16, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Robotic support unit. Can use the Chrono Beam ability and become stationary to power nearby structures."),
+ ItemNames.HAVOC: ItemData(8 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 17, SC2Race.PROTOSS,
+ origin={"lotv"}, important_for_filtering=True,
+ description="Robotic support unit. Can use the Target Lock and Force Field abilities and increase the range of nearby Protoss units."),
+ ItemNames.SIGNIFIER: ItemData(9 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 18, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"ext"},
+ description="Potent permanently cloaked psionic master. Can use the Feedback and Crippling Psionic Storm abilities. Can merge into an Archon."),
+ ItemNames.ASCENDANT: ItemData(10 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 19, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Potent psionic master. Can use the Psionic Orb, Mind Blast, and Sacrifice abilities."),
+ ItemNames.AVENGER: ItemData(11 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 20, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Deadly warrior-assassin. Permanently cloaked. Recalls to the nearest Dark Shrine upon death."),
+ ItemNames.BLOOD_HUNTER: ItemData(12 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 21, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Deadly warrior-assassin. Permanently cloaked. Can use the Void Stasis ability."),
+ ItemNames.DRAGOON: ItemData(13 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 22, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Ranged assault strider. Has enhanced health and damage."),
+ ItemNames.DARK_ARCHON: ItemData(14 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 23, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Potent psionic master. Can use the Confuse and Mind Control abilities."),
+ ItemNames.ADEPT: ItemData(15 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 24, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Ranged specialist. Can use the Psionic Transfer ability."),
+ ItemNames.WARP_PRISM: ItemData(16 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 25, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"ext"},
+ description="Flying transport. Can carry units and become stationary to deploy a power field."),
+ ItemNames.ANNIHILATOR: ItemData(17 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 26, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Assault Strider. Can use the Shadow Cannon ability to damage air and ground units."),
+ ItemNames.VANGUARD: ItemData(18 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 27, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Assault Strider. Deals splash damage around the primary target."),
+ ItemNames.WRATHWALKER: ItemData(19 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 28, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Battle strider with a powerful single target attack. Can walk up and down cliffs."),
+ ItemNames.REAVER: ItemData(20 + SC2LOTV_ITEM_ID_OFFSET, "Unit", 29, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Area damage siege unit. Builds and launches explosive Scarabs for high burst damage."),
+ ItemNames.DISRUPTOR: ItemData(21 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 0, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"ext"},
+ description="Robotic disruption unit. Can use the Purification Nova ability to deal heavy area damage."),
+ ItemNames.MIRAGE: ItemData(22 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 1, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Air superiority starfighter. Can use Graviton Beam and Phasing Armor abilities."),
+ ItemNames.CORSAIR: ItemData(23 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 2, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Air superiority starfighter. Can use the Disruption Web ability."),
+ ItemNames.DESTROYER: ItemData(24 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 3, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Area assault craft. Can use the Destruction Beam ability to attack multiple units at once."),
+ ItemNames.SCOUT: ItemData(25 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 4, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"ext"},
+ description="Versatile high-speed fighter."),
+ ItemNames.TEMPEST: ItemData(26 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 5, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Siege artillery craft. Attacks from long range. Can use the Disintegration ability."),
+ ItemNames.MOTHERSHIP: ItemData(27 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 6, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Ultimate Protoss vessel, Can use the Vortex and Mass Recall abilities. Cloaks nearby units and structures."),
+ ItemNames.ARBITER: ItemData(28 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 7, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="Army support craft. Has the Stasis Field and Recall abilities. Cloaks nearby units."),
+ ItemNames.ORACLE: ItemData(29 + SC2LOTV_ITEM_ID_OFFSET, "Unit 2", 8, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"ext"},
+ description="Flying caster. Can use the Revelation and Stasis Ward abilities."),
+
+ # Protoss Upgrades
+ ItemNames.PROGRESSIVE_PROTOSS_GROUND_WEAPON: ItemData(100 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 0, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}),
+ ItemNames.PROGRESSIVE_PROTOSS_GROUND_ARMOR: ItemData(101 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 2, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}),
+ ItemNames.PROGRESSIVE_PROTOSS_SHIELDS: ItemData(102 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 4, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}),
+ ItemNames.PROGRESSIVE_PROTOSS_AIR_WEAPON: ItemData(103 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 6, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}),
+ ItemNames.PROGRESSIVE_PROTOSS_AIR_ARMOR: ItemData(104 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 8, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}),
+ # Upgrade bundle 'number' values are used as indices to get affected 'number's
+ ItemNames.PROGRESSIVE_PROTOSS_WEAPON_UPGRADE: ItemData(105 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 11, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}),
+ ItemNames.PROGRESSIVE_PROTOSS_ARMOR_UPGRADE: ItemData(106 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 12, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}),
+ ItemNames.PROGRESSIVE_PROTOSS_GROUND_UPGRADE: ItemData(107 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 13, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}),
+ ItemNames.PROGRESSIVE_PROTOSS_AIR_UPGRADE: ItemData(108 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 14, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}),
+ ItemNames.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE: ItemData(109 + SC2LOTV_ITEM_ID_OFFSET, "Upgrade", 15, SC2Race.PROTOSS, quantity=3, origin={"wol", "lotv"}),
+
+ # Protoss Buildings
+ ItemNames.PHOTON_CANNON: ItemData(200 + SC2LOTV_ITEM_ID_OFFSET, "Building", 0, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"wol", "lotv"}),
+ ItemNames.KHAYDARIN_MONOLITH: ItemData(201 + SC2LOTV_ITEM_ID_OFFSET, "Building", 1, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"lotv"}),
+ ItemNames.SHIELD_BATTERY: ItemData(202 + SC2LOTV_ITEM_ID_OFFSET, "Building", 2, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"lotv"}),
+
+ # Protoss Unit Upgrades
+ ItemNames.SUPPLICANT_BLOOD_SHIELD: ItemData(300 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 0, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.SUPPLICANT),
+ ItemNames.SUPPLICANT_SOUL_AUGMENTATION: ItemData(301 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 1, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.SUPPLICANT),
+ ItemNames.SUPPLICANT_SHIELD_REGENERATION: ItemData(302 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 2, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.SUPPLICANT),
+ ItemNames.ADEPT_SHOCKWAVE: ItemData(303 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 3, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ADEPT),
+ ItemNames.ADEPT_RESONATING_GLAIVES: ItemData(304 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 4, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ADEPT),
+ ItemNames.ADEPT_PHASE_BULWARK: ItemData(305 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 5, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ADEPT),
+ ItemNames.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES: ItemData(306 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 6, SC2Race.PROTOSS, origin={"ext"}, classification=ItemClassification.progression),
+ ItemNames.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION: ItemData(307 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 7, SC2Race.PROTOSS, origin={"ext"}, classification=ItemClassification.progression),
+ ItemNames.DRAGOON_HIGH_IMPACT_PHASE_DISRUPTORS: ItemData(308 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 8, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.DRAGOON),
+ ItemNames.DRAGOON_TRILLIC_COMPRESSION_SYSTEM: ItemData(309 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 9, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.DRAGOON),
+ ItemNames.DRAGOON_SINGULARITY_CHARGE: ItemData(310 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 10, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.DRAGOON),
+ ItemNames.DRAGOON_ENHANCED_STRIDER_SERVOS: ItemData(311 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 11, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.DRAGOON),
+ ItemNames.SCOUT_COMBAT_SENSOR_ARRAY: ItemData(312 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 12, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.SCOUT),
+ ItemNames.SCOUT_APIAL_SENSORS: ItemData(313 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 13, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.SCOUT),
+ ItemNames.SCOUT_GRAVITIC_THRUSTERS: ItemData(314 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 14, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.SCOUT),
+ ItemNames.SCOUT_ADVANCED_PHOTON_BLASTERS: ItemData(315 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 15, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.SCOUT),
+ ItemNames.TEMPEST_TECTONIC_DESTABILIZERS: ItemData(316 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 16, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.TEMPEST),
+ ItemNames.TEMPEST_QUANTIC_REACTOR: ItemData(317 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 17, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.TEMPEST),
+ ItemNames.TEMPEST_GRAVITY_SLING: ItemData(318 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 18, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.TEMPEST),
+ ItemNames.PHOENIX_MIRAGE_IONIC_WAVELENGTH_FLUX: ItemData(319 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 19, SC2Race.PROTOSS, origin={"ext"}),
+ ItemNames.PHOENIX_MIRAGE_ANION_PULSE_CRYSTALS: ItemData(320 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 20, SC2Race.PROTOSS, origin={"ext"}),
+ ItemNames.CORSAIR_STEALTH_DRIVE: ItemData(321 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 21, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.CORSAIR),
+ ItemNames.CORSAIR_ARGUS_JEWEL: ItemData(322 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 22, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.CORSAIR),
+ ItemNames.CORSAIR_SUSTAINING_DISRUPTION: ItemData(323 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 23, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.CORSAIR),
+ ItemNames.CORSAIR_NEUTRON_SHIELDS: ItemData(324 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 24, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.CORSAIR),
+ ItemNames.ORACLE_STEALTH_DRIVE: ItemData(325 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 25, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ORACLE),
+ ItemNames.ORACLE_STASIS_CALIBRATION: ItemData(326 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 26, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ORACLE),
+ ItemNames.ORACLE_TEMPORAL_ACCELERATION_BEAM: ItemData(327 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 27, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ORACLE),
+ ItemNames.ARBITER_CHRONOSTATIC_REINFORCEMENT: ItemData(328 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 28, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.ARBITER),
+ ItemNames.ARBITER_KHAYDARIN_CORE: ItemData(329 + SC2LOTV_ITEM_ID_OFFSET, "Forge 1", 29, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.ARBITER),
+ ItemNames.ARBITER_SPACETIME_ANCHOR: ItemData(330 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 0, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.ARBITER),
+ ItemNames.ARBITER_RESOURCE_EFFICIENCY: ItemData(331 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 1, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.ARBITER),
+ ItemNames.ARBITER_ENHANCED_CLOAK_FIELD: ItemData(332 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 2, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.ARBITER),
+ ItemNames.CARRIER_GRAVITON_CATAPULT:
+ ItemData(333 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 3, SC2Race.PROTOSS, origin={"wol"},
+ parent_item=ItemNames.CARRIER,
+ description="Carriers can launch Interceptors more quickly."),
+ ItemNames.CARRIER_HULL_OF_PAST_GLORIES:
+ ItemData(334 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 4, SC2Race.PROTOSS, origin={"bw"},
+ parent_item=ItemNames.CARRIER,
+ description="Carriers gain +2 armour."),
+ ItemNames.VOID_RAY_DESTROYER_FLUX_VANES:
+ ItemData(335 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 5, SC2Race.PROTOSS, classification=ItemClassification.filler,
+ origin={"ext"},
+ description="Increases Void Ray and Destroyer movement speed."),
+ ItemNames.DESTROYER_REFORGED_BLOODSHARD_CORE:
+ ItemData(336 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 6, SC2Race.PROTOSS, origin={"ext"},
+ parent_item=ItemNames.DESTROYER,
+ description="When fully charged, the Destroyer's Destruction Beam weapon does full damage to secondary targets."),
+ ItemNames.WARP_PRISM_GRAVITIC_DRIVE:
+ ItemData(337 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 7, SC2Race.PROTOSS, classification=ItemClassification.filler,
+ origin={"ext"}, parent_item=ItemNames.WARP_PRISM,
+ description="Increases the movement speed of Warp Prisms."),
+ ItemNames.WARP_PRISM_PHASE_BLASTER:
+ ItemData(338 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 8, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"ext"}, parent_item=ItemNames.WARP_PRISM,
+ description="Equips Warp Prisms with an auto-attack that can hit ground and air targets."),
+ ItemNames.WARP_PRISM_WAR_CONFIGURATION: ItemData(339 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 9, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.WARP_PRISM),
+ ItemNames.OBSERVER_GRAVITIC_BOOSTERS: ItemData(340 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 10, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.OBSERVER),
+ ItemNames.OBSERVER_SENSOR_ARRAY: ItemData(341 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 11, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.OBSERVER),
+ ItemNames.REAVER_SCARAB_DAMAGE: ItemData(342 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 12, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.REAVER),
+ ItemNames.REAVER_SOLARITE_PAYLOAD: ItemData(343 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 13, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.REAVER),
+ ItemNames.REAVER_REAVER_CAPACITY: ItemData(344 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 14, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}, parent_item=ItemNames.REAVER),
+ ItemNames.REAVER_RESOURCE_EFFICIENCY: ItemData(345 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 15, SC2Race.PROTOSS, origin={"bw"}, parent_item=ItemNames.REAVER),
+ ItemNames.VANGUARD_AGONY_LAUNCHERS: ItemData(346 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 16, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.VANGUARD),
+ ItemNames.VANGUARD_MATTER_DISPERSION: ItemData(347 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 17, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.VANGUARD),
+ ItemNames.IMMORTAL_ANNIHILATOR_SINGULARITY_CHARGE: ItemData(348 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 18, SC2Race.PROTOSS, origin={"ext"}),
+ ItemNames.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING_MECHANICS: ItemData(349 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 19, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"ext"}),
+ ItemNames.COLOSSUS_PACIFICATION_PROTOCOL: ItemData(350 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 20, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.COLOSSUS),
+ ItemNames.WRATHWALKER_RAPID_POWER_CYCLING: ItemData(351 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 21, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.WRATHWALKER),
+ ItemNames.WRATHWALKER_EYE_OF_WRATH: ItemData(352 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 22, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.WRATHWALKER),
+ ItemNames.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHROUD_OF_ADUN: ItemData(353 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 23, SC2Race.PROTOSS, origin={"ext"}),
+ ItemNames.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_SHADOW_GUARD_TRAINING: ItemData(354 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 24, SC2Race.PROTOSS, origin={"bw"}),
+ ItemNames.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_BLINK: ItemData(355 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 25, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"ext"}),
+ ItemNames.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_RESOURCE_EFFICIENCY: ItemData(356 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 26, SC2Race.PROTOSS, origin={"ext"}),
+ ItemNames.DARK_TEMPLAR_DARK_ARCHON_MELD: ItemData(357 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 27, SC2Race.PROTOSS, origin={"bw"}, important_for_filtering=True ,parent_item=ItemNames.DARK_TEMPLAR),
+ ItemNames.HIGH_TEMPLAR_SIGNIFIER_UNSHACKLED_PSIONIC_STORM: ItemData(358 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 28, SC2Race.PROTOSS, origin={"bw"}),
+ ItemNames.HIGH_TEMPLAR_SIGNIFIER_HALLUCINATION: ItemData(359 + SC2LOTV_ITEM_ID_OFFSET, "Forge 2", 29, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"bw"}),
+ ItemNames.HIGH_TEMPLAR_SIGNIFIER_KHAYDARIN_AMULET: ItemData(360 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 0, SC2Race.PROTOSS, origin={"bw"}),
+ ItemNames.ARCHON_HIGH_ARCHON: ItemData(361 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 1, SC2Race.PROTOSS, origin={"ext"}, important_for_filtering=True),
+ ItemNames.DARK_ARCHON_FEEDBACK: ItemData(362 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 2, SC2Race.PROTOSS, origin={"bw"}),
+ ItemNames.DARK_ARCHON_MAELSTROM: ItemData(363 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 3, SC2Race.PROTOSS, origin={"bw"}),
+ ItemNames.DARK_ARCHON_ARGUS_TALISMAN: ItemData(364 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 4, SC2Race.PROTOSS, origin={"bw"}),
+ ItemNames.ASCENDANT_POWER_OVERWHELMING: ItemData(365 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 5, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ASCENDANT),
+ ItemNames.ASCENDANT_CHAOTIC_ATTUNEMENT: ItemData(366 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 6, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ASCENDANT),
+ ItemNames.ASCENDANT_BLOOD_AMULET: ItemData(367 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 7, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ASCENDANT),
+ ItemNames.SENTRY_ENERGIZER_HAVOC_CLOAKING_MODULE: ItemData(368 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 8, SC2Race.PROTOSS, origin={"ext"}),
+ ItemNames.SENTRY_ENERGIZER_HAVOC_SHIELD_BATTERY_RAPID_RECHARGING: ItemData(369 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 9, SC2Race.PROTOSS, origin={"ext"}),
+ ItemNames.SENTRY_FORCE_FIELD: ItemData(370 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 10, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.SENTRY),
+ ItemNames.SENTRY_HALLUCINATION: ItemData(371 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 11, SC2Race.PROTOSS, classification=ItemClassification.filler, origin={"ext"}, parent_item=ItemNames.SENTRY),
+ ItemNames.ENERGIZER_RECLAMATION: ItemData(372 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 12, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ENERGIZER),
+ ItemNames.ENERGIZER_FORGED_CHASSIS: ItemData(373 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 13, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.ENERGIZER),
+ ItemNames.HAVOC_DETECT_WEAKNESS: ItemData(374 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 14, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.HAVOC),
+ ItemNames.HAVOC_BLOODSHARD_RESONANCE: ItemData(375 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 15, SC2Race.PROTOSS, origin={"ext"}, parent_item=ItemNames.HAVOC),
+ ItemNames.ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS: ItemData(376 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 16, SC2Race.PROTOSS, origin={"bw"}),
+ ItemNames.ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY: ItemData(377 + SC2LOTV_ITEM_ID_OFFSET, "Forge 3", 17, SC2Race.PROTOSS, origin={"bw"}),
+
+ # SoA Calldown powers
+ ItemNames.SOA_CHRONO_SURGE: ItemData(700 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 0, SC2Race.PROTOSS, origin={"lotv"}),
+ ItemNames.SOA_PROGRESSIVE_PROXY_PYLON: ItemData(701 + SC2LOTV_ITEM_ID_OFFSET, "Progressive Upgrade", 0, SC2Race.PROTOSS, origin={"lotv"}, quantity=2),
+ ItemNames.SOA_PYLON_OVERCHARGE: ItemData(702 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 1, SC2Race.PROTOSS, origin={"ext"}),
+ ItemNames.SOA_ORBITAL_STRIKE: ItemData(703 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 2, SC2Race.PROTOSS, origin={"lotv"}),
+ ItemNames.SOA_TEMPORAL_FIELD: ItemData(704 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 3, SC2Race.PROTOSS, origin={"lotv"}),
+ ItemNames.SOA_SOLAR_LANCE: ItemData(705 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 4, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"lotv"}),
+ ItemNames.SOA_MASS_RECALL: ItemData(706 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 5, SC2Race.PROTOSS, origin={"lotv"}),
+ ItemNames.SOA_SHIELD_OVERCHARGE: ItemData(707 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 6, SC2Race.PROTOSS, origin={"lotv"}),
+ ItemNames.SOA_DEPLOY_FENIX: ItemData(708 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 7, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"lotv"}),
+ ItemNames.SOA_PURIFIER_BEAM: ItemData(709 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 8, SC2Race.PROTOSS, origin={"lotv"}),
+ ItemNames.SOA_TIME_STOP: ItemData(710 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 9, SC2Race.PROTOSS, classification=ItemClassification.progression, origin={"lotv"}),
+ ItemNames.SOA_SOLAR_BOMBARDMENT: ItemData(711 + SC2LOTV_ITEM_ID_OFFSET, "Spear of Adun", 10, SC2Race.PROTOSS, origin={"lotv"}),
+
+ # Generic Protoss Upgrades
+ ItemNames.MATRIX_OVERLOAD:
+ ItemData(800 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 0, SC2Race.PROTOSS, origin={"lotv"},
+ description=r"All friendly units gain 25% movement speed and 15% attack speed within a Pylon's power field and for 15 seconds after leaving it."),
+ ItemNames.QUATRO:
+ ItemData(801 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 1, SC2Race.PROTOSS, origin={"ext"},
+ description="All friendly Protoss units gain the equivalent of their +1 armour, attack, and shield upgrades."),
+ ItemNames.NEXUS_OVERCHARGE:
+ ItemData(802 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 2, SC2Race.PROTOSS, origin={"lotv"},
+ important_for_filtering=True, description="The Protoss Nexus gains a long-range auto-attack."),
+ ItemNames.ORBITAL_ASSIMILATORS:
+ ItemData(803 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 3, SC2Race.PROTOSS, origin={"lotv"},
+ description="Assimilators automatically harvest Vespene Gas without the need for Probes."),
+ ItemNames.WARP_HARMONIZATION:
+ ItemData(804 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 4, SC2Race.PROTOSS, origin={"lotv"},
+ description=r"Stargates and Robotics Facilities can transform to utilize Warp In technology. Warp In cooldowns are 20% faster than original build times."),
+ ItemNames.GUARDIAN_SHELL:
+ ItemData(805 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 5, SC2Race.PROTOSS, origin={"lotv"},
+ description="The Spear of Adun passively shields friendly Protoss units before death, making them invulnerable for 5 seconds. Each unit can only be shielded once every 60 seconds."),
+ ItemNames.RECONSTRUCTION_BEAM:
+ ItemData(806 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 6, SC2Race.PROTOSS,
+ classification=ItemClassification.progression, origin={"lotv"},
+ description="The Spear of Adun will passively heal mechanical units for 5 and non-biological structures for 10 life per second. Up to 3 targets can be repaired at once."),
+ ItemNames.OVERWATCH:
+ ItemData(807 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 7, SC2Race.PROTOSS, origin={"ext"},
+ description="Once per second, the Spear of Adun will last-hit a damaged enemy unit that is below 50 health."),
+ ItemNames.SUPERIOR_WARP_GATES:
+ ItemData(808 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 8, SC2Race.PROTOSS, origin={"ext"},
+ description="Protoss Warp Gates can hold up to 3 charges of unit warp-ins."),
+ ItemNames.ENHANCED_TARGETING:
+ ItemData(809 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 9, SC2Race.PROTOSS, origin={"ext"},
+ description="Protoss defensive structures gain +2 range."),
+ ItemNames.OPTIMIZED_ORDNANCE:
+ ItemData(810 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 10, SC2Race.PROTOSS, origin={"ext"},
+ description="Increases the attack speed of Protoss defensive structures by 25%."),
+ ItemNames.KHALAI_INGENUITY:
+ ItemData(811 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 11, SC2Race.PROTOSS, origin={"ext"},
+ description="Pylons, Photon Cannons, Monoliths, and Shield Batteries warp in near-instantly."),
+ ItemNames.AMPLIFIED_ASSIMILATORS:
+ ItemData(812 + SC2LOTV_ITEM_ID_OFFSET, "Solarite Core", 12, SC2Race.PROTOSS, origin={"ext"},
+ description=r"Assimilators produce Vespene gas 25% faster."),
+}
+
+
+def get_item_table():
+ return item_table
+
+
+basic_units = {
+ SC2Race.TERRAN: {
+ ItemNames.MARINE,
+ ItemNames.MARAUDER,
+ ItemNames.GOLIATH,
+ ItemNames.HELLION,
+ ItemNames.VULTURE,
+ ItemNames.WARHOUND,
+ },
+ SC2Race.ZERG: {
+ ItemNames.ZERGLING,
+ ItemNames.SWARM_QUEEN,
+ ItemNames.ROACH,
+ ItemNames.HYDRALISK,
+ },
+ SC2Race.PROTOSS: {
+ ItemNames.ZEALOT,
+ ItemNames.CENTURION,
+ ItemNames.SENTINEL,
+ ItemNames.STALKER,
+ ItemNames.INSTIGATOR,
+ ItemNames.SLAYER,
+ ItemNames.DRAGOON,
+ ItemNames.ADEPT,
+ }
+}
+
+advanced_basic_units = {
+ SC2Race.TERRAN: basic_units[SC2Race.TERRAN].union({
+ ItemNames.REAPER,
+ ItemNames.DIAMONDBACK,
+ ItemNames.VIKING,
+ ItemNames.SIEGE_TANK,
+ ItemNames.BANSHEE,
+ ItemNames.THOR,
+ ItemNames.BATTLECRUISER,
+ ItemNames.CYCLONE
+ }),
+ SC2Race.ZERG: basic_units[SC2Race.ZERG].union({
+ ItemNames.INFESTOR,
+ ItemNames.ABERRATION,
+ }),
+ SC2Race.PROTOSS: basic_units[SC2Race.PROTOSS].union({
+ ItemNames.DARK_TEMPLAR,
+ ItemNames.BLOOD_HUNTER,
+ ItemNames.AVENGER,
+ ItemNames.IMMORTAL,
+ ItemNames.ANNIHILATOR,
+ ItemNames.VANGUARD,
+ })
+}
+
+no_logic_starting_units = {
+ SC2Race.TERRAN: advanced_basic_units[SC2Race.TERRAN].union({
+ ItemNames.FIREBAT,
+ ItemNames.GHOST,
+ ItemNames.SPECTRE,
+ ItemNames.WRAITH,
+ ItemNames.RAVEN,
+ ItemNames.PREDATOR,
+ ItemNames.LIBERATOR,
+ ItemNames.HERC,
+ }),
+ SC2Race.ZERG: advanced_basic_units[SC2Race.ZERG].union({
+ ItemNames.ULTRALISK,
+ ItemNames.SWARM_HOST
+ }),
+ SC2Race.PROTOSS: advanced_basic_units[SC2Race.PROTOSS].union({
+ ItemNames.CARRIER,
+ ItemNames.TEMPEST,
+ ItemNames.VOID_RAY,
+ ItemNames.DESTROYER,
+ ItemNames.COLOSSUS,
+ ItemNames.WRATHWALKER,
+ ItemNames.SCOUT,
+ ItemNames.HIGH_TEMPLAR,
+ ItemNames.SIGNIFIER,
+ ItemNames.ASCENDANT,
+ ItemNames.DARK_ARCHON,
+ ItemNames.SUPPLICANT,
+ })
+}
+
+not_balanced_starting_units = {
+ ItemNames.SIEGE_TANK,
+ ItemNames.THOR,
+ ItemNames.BANSHEE,
+ ItemNames.BATTLECRUISER,
+ ItemNames.ULTRALISK,
+ ItemNames.CARRIER,
+ ItemNames.TEMPEST,
+}
+
+
+def get_basic_units(world: World, race: SC2Race) -> typing.Set[str]:
+ logic_level = get_option_value(world, 'required_tactics')
+ if logic_level == RequiredTactics.option_no_logic:
+ return no_logic_starting_units[race]
+ elif logic_level == RequiredTactics.option_advanced:
+ return advanced_basic_units[race]
+ else:
+ return basic_units[race]
+
+
+# Items that can be placed before resources if not already in
+# General upgrades and Mercs
+second_pass_placeable_items: typing.Tuple[str, ...] = (
+ # Global weapon/armor upgrades
+ ItemNames.PROGRESSIVE_TERRAN_ARMOR_UPGRADE,
+ ItemNames.PROGRESSIVE_TERRAN_WEAPON_UPGRADE,
+ ItemNames.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE,
+ ItemNames.PROGRESSIVE_ZERG_ARMOR_UPGRADE,
+ ItemNames.PROGRESSIVE_ZERG_WEAPON_UPGRADE,
+ ItemNames.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE,
+ ItemNames.PROGRESSIVE_PROTOSS_ARMOR_UPGRADE,
+ ItemNames.PROGRESSIVE_PROTOSS_WEAPON_UPGRADE,
+ ItemNames.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE,
+ ItemNames.PROGRESSIVE_PROTOSS_SHIELDS,
+ # Terran Buildings without upgrades
+ ItemNames.SENSOR_TOWER,
+ ItemNames.HIVE_MIND_EMULATOR,
+ ItemNames.PSI_DISRUPTER,
+ ItemNames.PERDITION_TURRET,
+ # Terran units without upgrades
+ ItemNames.HERC,
+ ItemNames.WARHOUND,
+ # General Terran upgrades without any dependencies
+ ItemNames.SCV_ADVANCED_CONSTRUCTION,
+ ItemNames.SCV_DUAL_FUSION_WELDERS,
+ ItemNames.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM,
+ ItemNames.PROGRESSIVE_ORBITAL_COMMAND,
+ ItemNames.ULTRA_CAPACITORS,
+ ItemNames.VANADIUM_PLATING,
+ ItemNames.ORBITAL_DEPOTS,
+ ItemNames.MICRO_FILTERING,
+ ItemNames.AUTOMATED_REFINERY,
+ ItemNames.COMMAND_CENTER_REACTOR,
+ ItemNames.TECH_REACTOR,
+ ItemNames.CELLULAR_REACTOR,
+ ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL, # Place only L1
+ ItemNames.STRUCTURE_ARMOR,
+ ItemNames.HI_SEC_AUTO_TRACKING,
+ ItemNames.ADVANCED_OPTICS,
+ ItemNames.ROGUE_FORCES,
+ # Mercenaries (All races)
+ *[item_name for item_name, item_data in get_full_item_list().items()
+ if item_data.type == "Mercenary"],
+ # Kerrigan and Nova levels, abilities and generally useful stuff
+ *[item_name for item_name, item_data in get_full_item_list().items()
+ if item_data.type in ("Level", "Ability", "Evolution Pit", "Nova Gear")],
+ ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE,
+ # Zerg static defenses
+ ItemNames.SPORE_CRAWLER,
+ ItemNames.SPINE_CRAWLER,
+ # Defiler, Aberration (no upgrades)
+ ItemNames.DEFILER,
+ ItemNames.ABERRATION,
+ # Spear of Adun Abilities
+ ItemNames.SOA_CHRONO_SURGE,
+ ItemNames.SOA_PROGRESSIVE_PROXY_PYLON,
+ ItemNames.SOA_PYLON_OVERCHARGE,
+ ItemNames.SOA_ORBITAL_STRIKE,
+ ItemNames.SOA_TEMPORAL_FIELD,
+ ItemNames.SOA_SOLAR_LANCE,
+ ItemNames.SOA_MASS_RECALL,
+ ItemNames.SOA_SHIELD_OVERCHARGE,
+ ItemNames.SOA_DEPLOY_FENIX,
+ ItemNames.SOA_PURIFIER_BEAM,
+ ItemNames.SOA_TIME_STOP,
+ ItemNames.SOA_SOLAR_BOMBARDMENT,
+ # Protoss generic upgrades
+ ItemNames.MATRIX_OVERLOAD,
+ ItemNames.QUATRO,
+ ItemNames.NEXUS_OVERCHARGE,
+ ItemNames.ORBITAL_ASSIMILATORS,
+ ItemNames.WARP_HARMONIZATION,
+ ItemNames.GUARDIAN_SHELL,
+ ItemNames.RECONSTRUCTION_BEAM,
+ ItemNames.OVERWATCH,
+ ItemNames.SUPERIOR_WARP_GATES,
+ ItemNames.KHALAI_INGENUITY,
+ ItemNames.AMPLIFIED_ASSIMILATORS,
+ # Protoss static defenses
+ ItemNames.PHOTON_CANNON,
+ ItemNames.KHAYDARIN_MONOLITH,
+ ItemNames.SHIELD_BATTERY
+)
+
+
+filler_items: typing.Tuple[str, ...] = (
+ ItemNames.STARTING_MINERALS,
+ ItemNames.STARTING_VESPENE,
+ ItemNames.STARTING_SUPPLY,
+)
+
+# Defense rating table
+# Commented defense ratings are handled in LogicMixin
+defense_ratings = {
+ ItemNames.SIEGE_TANK: 5,
+ # "Maelstrom Rounds": 2,
+ ItemNames.PLANETARY_FORTRESS: 3,
+ # Bunker w/ Marine/Marauder: 3,
+ ItemNames.PERDITION_TURRET: 2,
+ ItemNames.VULTURE: 1,
+ ItemNames.BANSHEE: 1,
+ ItemNames.BATTLECRUISER: 1,
+ ItemNames.LIBERATOR: 4,
+ ItemNames.WIDOW_MINE: 1,
+ # "Concealment (Widow Mine)": 1
+}
+zerg_defense_ratings = {
+ ItemNames.PERDITION_TURRET: 2,
+ # Bunker w/ Firebat: 2,
+ ItemNames.LIBERATOR: -2,
+ ItemNames.HIVE_MIND_EMULATOR: 3,
+ ItemNames.PSI_DISRUPTER: 3,
+}
+air_defense_ratings = {
+ ItemNames.MISSILE_TURRET: 2,
+}
+
+kerrigan_levels = [item_name for item_name, item_data in get_full_item_list().items()
+ if item_data.type == "Level" and item_data.race == SC2Race.ZERG]
+
+spider_mine_sources = {
+ ItemNames.VULTURE,
+ ItemNames.REAPER_SPIDER_MINES,
+ ItemNames.SIEGE_TANK_SPIDER_MINES,
+ ItemNames.RAVEN_SPIDER_MINES,
+}
+
+progressive_if_nco = {
+ ItemNames.MARINE_PROGRESSIVE_STIMPACK,
+ ItemNames.FIREBAT_PROGRESSIVE_STIMPACK,
+ ItemNames.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS,
+ ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL,
+}
+
+progressive_if_ext = {
+ ItemNames.VULTURE_PROGRESSIVE_REPLENISHABLE_MAGAZINE,
+ ItemNames.WRAITH_PROGRESSIVE_TOMAHAWK_POWER_CELLS,
+ ItemNames.BATTLECRUISER_PROGRESSIVE_DEFENSIVE_MATRIX,
+ ItemNames.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS,
+ ItemNames.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL,
+ ItemNames.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM,
+ ItemNames.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL
+}
+
+kerrigan_actives: typing.List[typing.Set[str]] = [
+ {ItemNames.KERRIGAN_KINETIC_BLAST, ItemNames.KERRIGAN_LEAPING_STRIKE},
+ {ItemNames.KERRIGAN_CRUSHING_GRIP, ItemNames.KERRIGAN_PSIONIC_SHIFT},
+ set(),
+ {ItemNames.KERRIGAN_WILD_MUTATION, ItemNames.KERRIGAN_SPAWN_BANELINGS, ItemNames.KERRIGAN_MEND},
+ set(),
+ set(),
+ {ItemNames.KERRIGAN_APOCALYPSE, ItemNames.KERRIGAN_SPAWN_LEVIATHAN, ItemNames.KERRIGAN_DROP_PODS},
+]
+
+kerrigan_passives: typing.List[typing.Set[str]] = [
+ {ItemNames.KERRIGAN_HEROIC_FORTITUDE},
+ {ItemNames.KERRIGAN_CHAIN_REACTION},
+ {ItemNames.KERRIGAN_ZERGLING_RECONSTITUTION, ItemNames.KERRIGAN_IMPROVED_OVERLORDS, ItemNames.KERRIGAN_AUTOMATED_EXTRACTORS},
+ set(),
+ {ItemNames.KERRIGAN_TWIN_DRONES, ItemNames.KERRIGAN_MALIGNANT_CREEP, ItemNames.KERRIGAN_VESPENE_EFFICIENCY},
+ {ItemNames.KERRIGAN_INFEST_BROODLINGS, ItemNames.KERRIGAN_FURY, ItemNames.KERRIGAN_ABILITY_EFFICIENCY},
+ set(),
+]
+
+kerrigan_only_passives = {
+ ItemNames.KERRIGAN_HEROIC_FORTITUDE, ItemNames.KERRIGAN_CHAIN_REACTION,
+ ItemNames.KERRIGAN_INFEST_BROODLINGS, ItemNames.KERRIGAN_FURY, ItemNames.KERRIGAN_ABILITY_EFFICIENCY,
+}
+
+spear_of_adun_calldowns = {
+ ItemNames.SOA_CHRONO_SURGE,
+ ItemNames.SOA_PROGRESSIVE_PROXY_PYLON,
+ ItemNames.SOA_PYLON_OVERCHARGE,
+ ItemNames.SOA_ORBITAL_STRIKE,
+ ItemNames.SOA_TEMPORAL_FIELD,
+ ItemNames.SOA_SOLAR_LANCE,
+ ItemNames.SOA_MASS_RECALL,
+ ItemNames.SOA_SHIELD_OVERCHARGE,
+ ItemNames.SOA_DEPLOY_FENIX,
+ ItemNames.SOA_PURIFIER_BEAM,
+ ItemNames.SOA_TIME_STOP,
+ ItemNames.SOA_SOLAR_BOMBARDMENT
+}
+
+spear_of_adun_castable_passives = {
+ ItemNames.RECONSTRUCTION_BEAM,
+ ItemNames.OVERWATCH,
+}
+
+nova_equipment = {
+ *[item_name for item_name, item_data in get_full_item_list().items()
+ if item_data.type == "Nova Gear"],
+ ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE
+}
+
+# 'number' values of upgrades for upgrade bundle items
+upgrade_numbers = [
+ # Terran
+ {0, 4, 8}, # Weapon
+ {2, 6, 10}, # Armor
+ {0, 2}, # Infantry
+ {4, 6}, # Vehicle
+ {8, 10}, # Starship
+ {0, 2, 4, 6, 8, 10}, # All
+ # Zerg
+ {0, 2, 6}, # Weapon
+ {4, 8}, # Armor
+ {0, 2, 4}, # Ground
+ {6, 8}, # Flyer
+ {0, 2, 4, 6, 8}, # All
+ # Protoss
+ {0, 6}, # Weapon
+ {2, 4, 8}, # Armor
+ {0, 2}, # Ground, Shields are handled specially
+ {6, 8}, # Air, Shields are handled specially
+ {0, 2, 4, 6, 8}, # All
+]
+# 'upgrade_numbers' indices for all upgrades
+upgrade_numbers_all = {
+ SC2Race.TERRAN: 5,
+ SC2Race.ZERG: 10,
+ SC2Race.PROTOSS: 15,
+}
+
+# Names of upgrades to be included for different options
+upgrade_included_names = [
+ { # Individual Items
+ ItemNames.PROGRESSIVE_TERRAN_INFANTRY_WEAPON,
+ ItemNames.PROGRESSIVE_TERRAN_INFANTRY_ARMOR,
+ ItemNames.PROGRESSIVE_TERRAN_VEHICLE_WEAPON,
+ ItemNames.PROGRESSIVE_TERRAN_VEHICLE_ARMOR,
+ ItemNames.PROGRESSIVE_TERRAN_SHIP_WEAPON,
+ ItemNames.PROGRESSIVE_TERRAN_SHIP_ARMOR,
+ ItemNames.PROGRESSIVE_ZERG_MELEE_ATTACK,
+ ItemNames.PROGRESSIVE_ZERG_MISSILE_ATTACK,
+ ItemNames.PROGRESSIVE_ZERG_GROUND_CARAPACE,
+ ItemNames.PROGRESSIVE_ZERG_FLYER_ATTACK,
+ ItemNames.PROGRESSIVE_ZERG_FLYER_CARAPACE,
+ ItemNames.PROGRESSIVE_PROTOSS_GROUND_WEAPON,
+ ItemNames.PROGRESSIVE_PROTOSS_GROUND_ARMOR,
+ ItemNames.PROGRESSIVE_PROTOSS_SHIELDS,
+ ItemNames.PROGRESSIVE_PROTOSS_AIR_WEAPON,
+ ItemNames.PROGRESSIVE_PROTOSS_AIR_ARMOR,
+ },
+ { # Bundle Weapon And Armor
+ ItemNames.PROGRESSIVE_TERRAN_WEAPON_UPGRADE,
+ ItemNames.PROGRESSIVE_TERRAN_ARMOR_UPGRADE,
+ ItemNames.PROGRESSIVE_ZERG_WEAPON_UPGRADE,
+ ItemNames.PROGRESSIVE_ZERG_ARMOR_UPGRADE,
+ ItemNames.PROGRESSIVE_PROTOSS_WEAPON_UPGRADE,
+ ItemNames.PROGRESSIVE_PROTOSS_ARMOR_UPGRADE,
+ },
+ { # Bundle Unit Class
+ ItemNames.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE,
+ ItemNames.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE,
+ ItemNames.PROGRESSIVE_TERRAN_SHIP_UPGRADE,
+ ItemNames.PROGRESSIVE_ZERG_GROUND_UPGRADE,
+ ItemNames.PROGRESSIVE_ZERG_FLYER_UPGRADE,
+ ItemNames.PROGRESSIVE_PROTOSS_GROUND_UPGRADE,
+ ItemNames.PROGRESSIVE_PROTOSS_AIR_UPGRADE,
+ },
+ { # Bundle All
+ ItemNames.PROGRESSIVE_TERRAN_WEAPON_ARMOR_UPGRADE,
+ ItemNames.PROGRESSIVE_ZERG_WEAPON_ARMOR_UPGRADE,
+ ItemNames.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE,
+ }
+]
+
+lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if
+ data.code}
+
+# Map type to expected int
+type_flaggroups: typing.Dict[SC2Race, typing.Dict[str, int]] = {
+ SC2Race.ANY: {
+ "Minerals": 0,
+ "Vespene": 1,
+ "Supply": 2,
+ "Goal": 3,
+ "Nothing Group": 4,
+ },
+ SC2Race.TERRAN: {
+ "Armory 1": 0,
+ "Armory 2": 1,
+ "Armory 3": 2,
+ "Armory 4": 3,
+ "Armory 5": 4,
+ "Armory 6": 5,
+ "Progressive Upgrade": 6, # Unit upgrades that exist multiple times (Stimpack / Super Stimpack)
+ "Laboratory": 7,
+ "Upgrade": 8, # Weapon / Armor upgrades
+ "Unit": 9,
+ "Building": 10,
+ "Mercenary": 11,
+ "Nova Gear": 12,
+ "Progressive Upgrade 2": 13,
+ },
+ SC2Race.ZERG: {
+ "Ability": 0,
+ "Mutation 1": 1,
+ "Strain": 2,
+ "Morph": 3,
+ "Upgrade": 4,
+ "Mercenary": 5,
+ "Unit": 6,
+ "Level": 7,
+ "Primal Form": 8,
+ "Evolution Pit": 9,
+ "Mutation 2": 10,
+ "Mutation 3": 11
+ },
+ SC2Race.PROTOSS: {
+ "Unit": 0,
+ "Unit 2": 1,
+ "Upgrade": 2, # Weapon / Armor upgrades
+ "Building": 3,
+ "Progressive Upgrade": 4,
+ "Spear of Adun": 5,
+ "Solarite Core": 6,
+ "Forge 1": 7,
+ "Forge 2": 8,
+ "Forge 3": 9,
+ }
+}
diff --git a/worlds/sc2/Locations.py b/worlds/sc2/Locations.py
new file mode 100644
index 000000000000..bf9c06fa3f78
--- /dev/null
+++ b/worlds/sc2/Locations.py
@@ -0,0 +1,1638 @@
+from enum import IntEnum
+from typing import List, Tuple, Optional, Callable, NamedTuple, Set, Any
+from BaseClasses import MultiWorld
+from . import ItemNames
+from .Options import get_option_value, kerrigan_unit_available, RequiredTactics, GrantStoryTech, LocationInclusion, \
+ EnableHotsMissions
+from .Rules import SC2Logic
+
+from BaseClasses import Location
+from worlds.AutoWorld import World
+
+SC2WOL_LOC_ID_OFFSET = 1000
+SC2HOTS_LOC_ID_OFFSET = 20000000 # Avoid clashes with The Legend of Zelda
+SC2LOTV_LOC_ID_OFFSET = SC2HOTS_LOC_ID_OFFSET + 2000
+SC2NCO_LOC_ID_OFFSET = SC2LOTV_LOC_ID_OFFSET + 2500
+
+
+class SC2Location(Location):
+ game: str = "Starcraft2"
+
+
+class LocationType(IntEnum):
+ VICTORY = 0 # Winning a mission
+ VANILLA = 1 # Objectives that provided metaprogression in the original campaign, along with a few other locations for a balanced experience
+ EXTRA = 2 # Additional locations based on mission progression, collecting in-mission rewards, etc. that do not significantly increase the challenge.
+ CHALLENGE = 3 # Challenging objectives, often harder than just completing a mission, and often associated with Achievements
+ MASTERY = 4 # Extremely challenging objectives often associated with Masteries and Feats of Strength in the original campaign
+
+
+class LocationData(NamedTuple):
+ region: str
+ name: str
+ code: Optional[int]
+ type: LocationType
+ rule: Optional[Callable[[Any], bool]] = Location.access_rule
+
+
+def get_location_types(world: World, inclusion_type: LocationInclusion) -> Set[LocationType]:
+ """
+
+ :param multiworld:
+ :param player:
+ :param inclusion_type: Level of inclusion to check for
+ :return: A list of location types that match the inclusion type
+ """
+ exclusion_options = [
+ ("vanilla_locations", LocationType.VANILLA),
+ ("extra_locations", LocationType.EXTRA),
+ ("challenge_locations", LocationType.CHALLENGE),
+ ("mastery_locations", LocationType.MASTERY)
+ ]
+ excluded_location_types = set()
+ for option_name, location_type in exclusion_options:
+ if get_option_value(world, option_name) is inclusion_type:
+ excluded_location_types.add(location_type)
+ return excluded_location_types
+
+
+def get_plando_locations(world: World) -> List[str]:
+ """
+
+ :param multiworld:
+ :param player:
+ :return: A list of locations affected by a plando in a world
+ """
+ if world is None:
+ return []
+ plando_locations = []
+ for plando_setting in world.multiworld.plando_items[world.player]:
+ plando_locations += plando_setting.get("locations", [])
+ plando_setting_location = plando_setting.get("location", None)
+ if plando_setting_location is not None:
+ plando_locations.append(plando_setting_location)
+
+ return plando_locations
+
+
+def get_locations(world: Optional[World]) -> Tuple[LocationData, ...]:
+ # Note: rules which are ended with or True are rules identified as needed later when restricted units is an option
+ logic_level = get_option_value(world, 'required_tactics')
+ adv_tactics = logic_level != RequiredTactics.option_standard
+ kerriganless = get_option_value(world, 'kerrigan_presence') not in kerrigan_unit_available \
+ or get_option_value(world, "enable_hots_missions") == EnableHotsMissions.option_false
+ story_tech_granted = get_option_value(world, "grant_story_tech") == GrantStoryTech.option_true
+ logic = SC2Logic(world)
+ player = None if world is None else world.player
+ location_table: List[LocationData] = [
+ # WoL
+ LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100, LocationType.VICTORY),
+ LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101, LocationType.VANILLA),
+ LocationData("Liberation Day", "Liberation Day: Second Statue", SC2WOL_LOC_ID_OFFSET + 102, LocationType.VANILLA),
+ LocationData("Liberation Day", "Liberation Day: Third Statue", SC2WOL_LOC_ID_OFFSET + 103, LocationType.VANILLA),
+ LocationData("Liberation Day", "Liberation Day: Fourth Statue", SC2WOL_LOC_ID_OFFSET + 104, LocationType.VANILLA),
+ LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105, LocationType.VANILLA),
+ LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106, LocationType.VANILLA),
+ LocationData("Liberation Day", "Liberation Day: Special Delivery", SC2WOL_LOC_ID_OFFSET + 107, LocationType.EXTRA),
+ LocationData("Liberation Day", "Liberation Day: Transport", SC2WOL_LOC_ID_OFFSET + 108, LocationType.EXTRA),
+ LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200, LocationType.VICTORY,
+ lambda state: logic.terran_early_tech(state)),
+ LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201, LocationType.VANILLA,
+ lambda state: logic.terran_early_tech(state)),
+ LocationData("The Outlaws", "The Outlaws: North Resource Pickups", SC2WOL_LOC_ID_OFFSET + 202, LocationType.EXTRA,
+ lambda state: logic.terran_early_tech(state)),
+ LocationData("The Outlaws", "The Outlaws: Bunker", SC2WOL_LOC_ID_OFFSET + 203, LocationType.VANILLA,
+ lambda state: logic.terran_early_tech(state)),
+ LocationData("The Outlaws", "The Outlaws: Close Resource Pickups", SC2WOL_LOC_ID_OFFSET + 204, LocationType.EXTRA),
+ LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300, LocationType.VICTORY,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_defense_rating(state, True) >= 2 and
+ (adv_tactics or logic.terran_basic_anti_air(state))),
+ LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301, LocationType.VANILLA),
+ LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302, LocationType.VANILLA,
+ lambda state: logic.terran_common_unit(state)),
+ LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303, LocationType.VANILLA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_defense_rating(state, True) >= 2),
+ LocationData("Zero Hour", "Zero Hour: First Hatchery", SC2WOL_LOC_ID_OFFSET + 304, LocationType.CHALLENGE,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Zero Hour", "Zero Hour: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 305, LocationType.CHALLENGE,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Zero Hour", "Zero Hour: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 306, LocationType.CHALLENGE,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Zero Hour", "Zero Hour: Fourth Hatchery", SC2WOL_LOC_ID_OFFSET + 307, LocationType.CHALLENGE,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Zero Hour", "Zero Hour: Ride's on its Way", SC2WOL_LOC_ID_OFFSET + 308, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state)),
+ LocationData("Zero Hour", "Zero Hour: Hold Just a Little Longer", SC2WOL_LOC_ID_OFFSET + 309, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_defense_rating(state, True) >= 2),
+ LocationData("Zero Hour", "Zero Hour: Cavalry's on the Way", SC2WOL_LOC_ID_OFFSET + 310, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_defense_rating(state, True) >= 2),
+ LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400, LocationType.VICTORY,
+ lambda state: logic.terran_early_tech(state) and
+ (adv_tactics and logic.terran_basic_anti_air(state)
+ or logic.terran_competent_anti_air(state))),
+ LocationData("Evacuation", "Evacuation: North Chrysalis", SC2WOL_LOC_ID_OFFSET + 401, LocationType.VANILLA),
+ LocationData("Evacuation", "Evacuation: West Chrysalis", SC2WOL_LOC_ID_OFFSET + 402, LocationType.VANILLA,
+ lambda state: logic.terran_early_tech(state)),
+ LocationData("Evacuation", "Evacuation: East Chrysalis", SC2WOL_LOC_ID_OFFSET + 403, LocationType.VANILLA,
+ lambda state: logic.terran_early_tech(state)),
+ LocationData("Evacuation", "Evacuation: Reach Hanson", SC2WOL_LOC_ID_OFFSET + 404, LocationType.EXTRA),
+ LocationData("Evacuation", "Evacuation: Secret Resource Stash", SC2WOL_LOC_ID_OFFSET + 405, LocationType.EXTRA),
+ LocationData("Evacuation", "Evacuation: Flawless", SC2WOL_LOC_ID_OFFSET + 406, LocationType.CHALLENGE,
+ lambda state: logic.terran_early_tech(state) and
+ logic.terran_defense_rating(state, True, False) >= 2 and
+ (adv_tactics and logic.terran_basic_anti_air(state)
+ or logic.terran_competent_anti_air(state))),
+ LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500, LocationType.VICTORY,
+ lambda state: logic.terran_defense_rating(state, True, False) >= 4 and
+ (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))),
+ LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501, LocationType.VANILLA,
+ lambda state: logic.terran_defense_rating(state, True, False) >= 2 and
+ (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))),
+ LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502, LocationType.VANILLA,
+ lambda state: logic.terran_defense_rating(state, True, False) >= 2 and
+ (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))),
+ LocationData("Outbreak", "Outbreak: North Infested Command Center", SC2WOL_LOC_ID_OFFSET + 503, LocationType.EXTRA,
+ lambda state: logic.terran_defense_rating(state, True, False) >= 2 and
+ (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))),
+ LocationData("Outbreak", "Outbreak: South Infested Command Center", SC2WOL_LOC_ID_OFFSET + 504, LocationType.EXTRA,
+ lambda state: logic.terran_defense_rating(state, True, False) >= 2 and
+ (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))),
+ LocationData("Outbreak", "Outbreak: Northwest Bar", SC2WOL_LOC_ID_OFFSET + 505, LocationType.EXTRA,
+ lambda state: logic.terran_defense_rating(state, True, False) >= 2 and
+ (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))),
+ LocationData("Outbreak", "Outbreak: North Bar", SC2WOL_LOC_ID_OFFSET + 506, LocationType.EXTRA,
+ lambda state: logic.terran_defense_rating(state, True, False) >= 2 and
+ (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))),
+ LocationData("Outbreak", "Outbreak: South Bar", SC2WOL_LOC_ID_OFFSET + 507, LocationType.EXTRA,
+ lambda state: logic.terran_defense_rating(state, True, False) >= 2 and
+ (logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))),
+ LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600, LocationType.VICTORY,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_competent_anti_air(state)),
+ LocationData("Safe Haven", "Safe Haven: North Nexus", SC2WOL_LOC_ID_OFFSET + 601, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_competent_anti_air(state)),
+ LocationData("Safe Haven", "Safe Haven: East Nexus", SC2WOL_LOC_ID_OFFSET + 602, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_competent_anti_air(state)),
+ LocationData("Safe Haven", "Safe Haven: South Nexus", SC2WOL_LOC_ID_OFFSET + 603, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_competent_anti_air(state)),
+ LocationData("Safe Haven", "Safe Haven: First Terror Fleet", SC2WOL_LOC_ID_OFFSET + 604, LocationType.VANILLA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_competent_anti_air(state)),
+ LocationData("Safe Haven", "Safe Haven: Second Terror Fleet", SC2WOL_LOC_ID_OFFSET + 605, LocationType.VANILLA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_competent_anti_air(state)),
+ LocationData("Safe Haven", "Safe Haven: Third Terror Fleet", SC2WOL_LOC_ID_OFFSET + 606, LocationType.VANILLA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_competent_anti_air(state)),
+ LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700, LocationType.VICTORY,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_competent_anti_air(state) and
+ logic.terran_defense_rating(state, True) >= 3),
+ LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701, LocationType.VANILLA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_competent_anti_air(state) and
+ logic.terran_defense_rating(state, True) >= 3),
+ LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702, LocationType.VANILLA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_competent_anti_air(state) and
+ logic.terran_defense_rating(state, True) >= 3),
+ LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703, LocationType.VANILLA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_competent_anti_air(state) and
+ logic.terran_defense_rating(state, True) >= 3),
+ LocationData("Haven's Fall", "Haven's Fall: Northeast Colony Base", SC2WOL_LOC_ID_OFFSET + 704, LocationType.CHALLENGE,
+ lambda state: logic.terran_respond_to_colony_infestations(state)),
+ LocationData("Haven's Fall", "Haven's Fall: East Colony Base", SC2WOL_LOC_ID_OFFSET + 705, LocationType.CHALLENGE,
+ lambda state: logic.terran_respond_to_colony_infestations(state)),
+ LocationData("Haven's Fall", "Haven's Fall: Middle Colony Base", SC2WOL_LOC_ID_OFFSET + 706, LocationType.CHALLENGE,
+ lambda state: logic.terran_respond_to_colony_infestations(state)),
+ LocationData("Haven's Fall", "Haven's Fall: Southeast Colony Base", SC2WOL_LOC_ID_OFFSET + 707, LocationType.CHALLENGE,
+ lambda state: logic.terran_respond_to_colony_infestations(state)),
+ LocationData("Haven's Fall", "Haven's Fall: Southwest Colony Base", SC2WOL_LOC_ID_OFFSET + 708, LocationType.CHALLENGE,
+ lambda state: logic.terran_respond_to_colony_infestations(state)),
+ LocationData("Haven's Fall", "Haven's Fall: Southwest Gas Pickups", SC2WOL_LOC_ID_OFFSET + 709, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_competent_anti_air(state) and
+ logic.terran_defense_rating(state, True) >= 3),
+ LocationData("Haven's Fall", "Haven's Fall: East Gas Pickups", SC2WOL_LOC_ID_OFFSET + 710, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_competent_anti_air(state) and
+ logic.terran_defense_rating(state, True) >= 3),
+ LocationData("Haven's Fall", "Haven's Fall: Southeast Gas Pickups", SC2WOL_LOC_ID_OFFSET + 711, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state) and
+ logic.terran_competent_anti_air(state) and
+ logic.terran_defense_rating(state, True) >= 3),
+ LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800, LocationType.VICTORY,
+ lambda state: logic.terran_common_unit(state) and
+ (adv_tactics and logic.terran_basic_anti_air(state)
+ or logic.terran_competent_anti_air(state))),
+ LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801, LocationType.VANILLA),
+ LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802, LocationType.VANILLA),
+ LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803, LocationType.VANILLA,
+ lambda state: logic.terran_common_unit(state) and
+ (adv_tactics and logic.terran_basic_anti_air(state)
+ or logic.terran_competent_anti_air(state))),
+ LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804, LocationType.VANILLA,
+ lambda state: logic.terran_common_unit(state) and
+ (adv_tactics and logic.terran_basic_anti_air(state)
+ or logic.terran_competent_anti_air(state))),
+ LocationData("Smash and Grab", "Smash and Grab: First Forcefield Area Busted", SC2WOL_LOC_ID_OFFSET + 805, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state) and
+ (adv_tactics and logic.terran_basic_anti_air(state)
+ or logic.terran_competent_anti_air(state))),
+ LocationData("Smash and Grab", "Smash and Grab: Second Forcefield Area Busted", SC2WOL_LOC_ID_OFFSET + 806, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state) and
+ (adv_tactics and logic.terran_basic_anti_air(state)
+ or logic.terran_competent_anti_air(state))),
+ LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900, LocationType.VICTORY,
+ lambda state: logic.terran_basic_anti_air(state)
+ and logic.terran_defense_rating(state, False, True) >= 8
+ and logic.terran_defense_rating(state, False, False) >= 6
+ and logic.terran_common_unit(state)
+ and (logic.marine_medic_upgrade(state) or adv_tactics)),
+ LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901, LocationType.VANILLA,
+ lambda state: logic.terran_defense_rating(state, False, False) >= 6
+ and logic.terran_common_unit(state)
+ and (logic.marine_medic_upgrade(state) or adv_tactics)),
+ LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902, LocationType.VANILLA,
+ lambda state: logic.terran_defense_rating(state, False, False) >= 6
+ and logic.terran_common_unit(state)
+ and (logic.marine_medic_upgrade(state) or adv_tactics)),
+ LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903, LocationType.VANILLA,
+ lambda state: logic.terran_defense_rating(state, False, False) >= 6
+ and logic.terran_common_unit(state)
+ and (logic.marine_medic_upgrade(state) or adv_tactics)),
+ LocationData("The Dig", "The Dig: Moebius Base", SC2WOL_LOC_ID_OFFSET + 904, LocationType.EXTRA,
+ lambda state: logic.marine_medic_upgrade(state) or adv_tactics),
+ LocationData("The Dig", "The Dig: Door Outer Layer", SC2WOL_LOC_ID_OFFSET + 905, LocationType.EXTRA,
+ lambda state: logic.terran_defense_rating(state, False, False) >= 6
+ and logic.terran_common_unit(state)
+ and (logic.marine_medic_upgrade(state) or adv_tactics)),
+ LocationData("The Dig", "The Dig: Door Thermal Barrier", SC2WOL_LOC_ID_OFFSET + 906, LocationType.EXTRA,
+ lambda state: logic.terran_basic_anti_air(state)
+ and logic.terran_defense_rating(state, False, True) >= 8
+ and logic.terran_defense_rating(state, False, False) >= 6
+ and logic.terran_common_unit(state)
+ and (logic.marine_medic_upgrade(state) or adv_tactics)),
+ LocationData("The Dig", "The Dig: Cutting Through the Core", SC2WOL_LOC_ID_OFFSET + 907, LocationType.EXTRA,
+ lambda state: logic.terran_basic_anti_air(state)
+ and logic.terran_defense_rating(state, False, True) >= 8
+ and logic.terran_defense_rating(state, False, False) >= 6
+ and logic.terran_common_unit(state)
+ and (logic.marine_medic_upgrade(state) or adv_tactics)),
+ LocationData("The Dig", "The Dig: Structure Access Imminent", SC2WOL_LOC_ID_OFFSET + 908, LocationType.EXTRA,
+ lambda state: logic.terran_basic_anti_air(state)
+ and logic.terran_defense_rating(state, False, True) >= 8
+ and logic.terran_defense_rating(state, False, False) >= 6
+ and logic.terran_common_unit(state)
+ and (logic.marine_medic_upgrade(state) or adv_tactics)),
+ LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, LocationType.VICTORY,
+ lambda state: logic.terran_basic_anti_air(state) and
+ (logic.terran_air(state)
+ or state.has_any({ItemNames.MEDIVAC, ItemNames.HERCULES}, player)
+ and logic.terran_common_unit(state))),
+ LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core", SC2WOL_LOC_ID_OFFSET + 1001, LocationType.VANILLA),
+ LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002, LocationType.VANILLA,
+ lambda state: (logic.terran_air(state)
+ or state.has_any({ItemNames.MEDIVAC, ItemNames.HERCULES}, player)
+ and logic.terran_common_unit(state))),
+ LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, LocationType.EXTRA,
+ lambda state: logic.terran_can_rescue(state)),
+ LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, LocationType.EXTRA,
+ lambda state: logic.terran_can_rescue(state)),
+ LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005, LocationType.EXTRA,
+ lambda state: logic.terran_can_rescue(state)),
+ LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006, LocationType.EXTRA,
+ lambda state: logic.terran_can_rescue(state)),
+ LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, LocationType.EXTRA,
+ lambda state: logic.terran_can_rescue(state)),
+ LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, LocationType.VANILLA,
+ lambda state: logic.terran_basic_anti_air(state) and
+ (logic.terran_air(state)
+ or state.has_any({ItemNames.MEDIVAC, ItemNames.HERCULES}, player)
+ and logic.terran_common_unit(state))),
+ LocationData("The Moebius Factor", "The Moebius Factor: 3rd Data Core", SC2WOL_LOC_ID_OFFSET + 1009, LocationType.VANILLA,
+ lambda state: logic.terran_basic_anti_air(state) and
+ (logic.terran_air(state)
+ or state.has_any({ItemNames.MEDIVAC, ItemNames.HERCULES}, player)
+ and logic.terran_common_unit(state))),
+ LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100, LocationType.VICTORY,
+ lambda state: logic.terran_beats_protoss_deathball(state)),
+ LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101, LocationType.VANILLA),
+ LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102, LocationType.VANILLA),
+ LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103, LocationType.VANILLA,
+ lambda state: logic.terran_beats_protoss_deathball(state)),
+ LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, LocationType.VANILLA,
+ lambda state: logic.terran_beats_protoss_deathball(state)),
+ LocationData("Supernova", "Supernova: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1105, LocationType.EXTRA),
+ LocationData("Supernova", "Supernova: Middle Base", SC2WOL_LOC_ID_OFFSET + 1106, LocationType.EXTRA,
+ lambda state: logic.terran_beats_protoss_deathball(state)),
+ LocationData("Supernova", "Supernova: Southeast Base", SC2WOL_LOC_ID_OFFSET + 1107, LocationType.EXTRA,
+ lambda state: logic.terran_beats_protoss_deathball(state)),
+ LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200, LocationType.VICTORY,
+ lambda state: logic.terran_survives_rip_field(state)),
+ LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201, LocationType.EXTRA),
+ LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202, LocationType.VANILLA,
+ lambda state: adv_tactics or logic.terran_survives_rip_field(state)),
+ LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203, LocationType.VANILLA,
+ lambda state: adv_tactics or logic.terran_survives_rip_field(state)),
+ LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204, LocationType.VANILLA,
+ lambda state: logic.terran_survives_rip_field(state)),
+ LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205, LocationType.VANILLA,
+ lambda state: logic.terran_survives_rip_field(state)),
+ LocationData("Maw of the Void", "Maw of the Void: Mothership", SC2WOL_LOC_ID_OFFSET + 1206, LocationType.EXTRA,
+ lambda state: logic.terran_survives_rip_field(state)),
+ LocationData("Maw of the Void", "Maw of the Void: Expansion Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1207, LocationType.EXTRA,
+ lambda state: adv_tactics or logic.terran_survives_rip_field(state)),
+ LocationData("Maw of the Void", "Maw of the Void: Middle Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1208, LocationType.EXTRA,
+ lambda state: logic.terran_survives_rip_field(state)),
+ LocationData("Maw of the Void", "Maw of the Void: Southeast Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1209, LocationType.EXTRA,
+ lambda state: logic.terran_survives_rip_field(state)),
+ LocationData("Maw of the Void", "Maw of the Void: Stargate Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1210, LocationType.EXTRA,
+ lambda state: logic.terran_survives_rip_field(state)),
+ LocationData("Maw of the Void", "Maw of the Void: Northwest Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1211, LocationType.CHALLENGE,
+ lambda state: logic.terran_survives_rip_field(state)),
+ LocationData("Maw of the Void", "Maw of the Void: West Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1212, LocationType.CHALLENGE,
+ lambda state: logic.terran_survives_rip_field(state)),
+ LocationData("Maw of the Void", "Maw of the Void: Southwest Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1213, LocationType.CHALLENGE,
+ lambda state: logic.terran_survives_rip_field(state)),
+ LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, LocationType.VICTORY,
+ lambda state: adv_tactics or
+ logic.terran_basic_anti_air(state) and (
+ logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))),
+ LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301, LocationType.VANILLA),
+ LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302, LocationType.VANILLA,
+ lambda state: adv_tactics or logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player)),
+ LocationData("Devil's Playground", "Devil's Playground: North Reapers", SC2WOL_LOC_ID_OFFSET + 1303, LocationType.EXTRA),
+ LocationData("Devil's Playground", "Devil's Playground: Middle Reapers", SC2WOL_LOC_ID_OFFSET + 1304, LocationType.EXTRA,
+ lambda state: adv_tactics or logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player)),
+ LocationData("Devil's Playground", "Devil's Playground: Southwest Reapers", SC2WOL_LOC_ID_OFFSET + 1305, LocationType.EXTRA,
+ lambda state: adv_tactics or logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player)),
+ LocationData("Devil's Playground", "Devil's Playground: Southeast Reapers", SC2WOL_LOC_ID_OFFSET + 1306, LocationType.EXTRA,
+ lambda state: adv_tactics or
+ logic.terran_basic_anti_air(state) and (
+ logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))),
+ LocationData("Devil's Playground", "Devil's Playground: East Reapers", SC2WOL_LOC_ID_OFFSET + 1307, LocationType.CHALLENGE,
+ lambda state: logic.terran_basic_anti_air(state) and
+ (adv_tactics or
+ logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))),
+ LocationData("Devil's Playground", "Devil's Playground: Zerg Cleared", SC2WOL_LOC_ID_OFFSET + 1308, LocationType.CHALLENGE,
+ lambda state: logic.terran_competent_anti_air(state) and (
+ logic.terran_common_unit(state) or state.has(ItemNames.REAPER, player))),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400, LocationType.VICTORY,
+ lambda state: logic.welcome_to_the_jungle_requirement(state)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401, LocationType.VANILLA),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402, LocationType.VANILLA,
+ lambda state: logic.welcome_to_the_jungle_requirement(state)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403, LocationType.VANILLA,
+ lambda state: logic.welcome_to_the_jungle_requirement(state)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Middle Base", SC2WOL_LOC_ID_OFFSET + 1404, LocationType.EXTRA,
+ lambda state: logic.welcome_to_the_jungle_requirement(state)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Main Base", SC2WOL_LOC_ID_OFFSET + 1405,
+ LocationType.MASTERY,
+ lambda state: logic.welcome_to_the_jungle_requirement(state)
+ and logic.terran_beats_protoss_deathball(state)
+ and logic.terran_base_trasher(state)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: No Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1406, LocationType.CHALLENGE,
+ lambda state: logic.welcome_to_the_jungle_requirement(state)
+ and logic.terran_competent_ground_to_air(state)
+ and logic.terran_beats_protoss_deathball(state)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 1 Terrazine Node Sealed", SC2WOL_LOC_ID_OFFSET + 1407, LocationType.CHALLENGE,
+ lambda state: logic.welcome_to_the_jungle_requirement(state)
+ and logic.terran_competent_ground_to_air(state)
+ and logic.terran_beats_protoss_deathball(state)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 2 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1408, LocationType.CHALLENGE,
+ lambda state: logic.welcome_to_the_jungle_requirement(state)
+ and logic.terran_beats_protoss_deathball(state)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 3 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1409, LocationType.CHALLENGE,
+ lambda state: logic.welcome_to_the_jungle_requirement(state)
+ and logic.terran_competent_comp(state)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 4 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1410, LocationType.EXTRA,
+ lambda state: logic.welcome_to_the_jungle_requirement(state)),
+ LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 5 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1411, LocationType.EXTRA,
+ lambda state: logic.welcome_to_the_jungle_requirement(state)),
+ LocationData("Breakout", "Breakout: Victory", SC2WOL_LOC_ID_OFFSET + 1500, LocationType.VICTORY),
+ LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501, LocationType.VANILLA),
+ LocationData("Breakout", "Breakout: Siege Tank Prison", SC2WOL_LOC_ID_OFFSET + 1502, LocationType.VANILLA),
+ LocationData("Breakout", "Breakout: First Checkpoint", SC2WOL_LOC_ID_OFFSET + 1503, LocationType.EXTRA),
+ LocationData("Breakout", "Breakout: Second Checkpoint", SC2WOL_LOC_ID_OFFSET + 1504, LocationType.EXTRA),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Victory", SC2WOL_LOC_ID_OFFSET + 1600, LocationType.VICTORY),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Terrazine Tank", SC2WOL_LOC_ID_OFFSET + 1601, LocationType.EXTRA),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Jorium Stockpile", SC2WOL_LOC_ID_OFFSET + 1602, LocationType.EXTRA),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: First Island Spectres", SC2WOL_LOC_ID_OFFSET + 1603, LocationType.VANILLA),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604, LocationType.VANILLA),
+ LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605, LocationType.VANILLA),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700, LocationType.VICTORY,
+ lambda state: logic.great_train_robbery_train_stopper(state) and
+ logic.terran_basic_anti_air(state)),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701, LocationType.VANILLA),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702, LocationType.VANILLA),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703, LocationType.VANILLA),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Close Diamondback", SC2WOL_LOC_ID_OFFSET + 1704, LocationType.EXTRA),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Northwest Diamondback", SC2WOL_LOC_ID_OFFSET + 1705, LocationType.EXTRA),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: North Diamondback", SC2WOL_LOC_ID_OFFSET + 1706, LocationType.EXTRA),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Northeast Diamondback", SC2WOL_LOC_ID_OFFSET + 1707, LocationType.EXTRA),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Southwest Diamondback", SC2WOL_LOC_ID_OFFSET + 1708, LocationType.EXTRA),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Southeast Diamondback", SC2WOL_LOC_ID_OFFSET + 1709, LocationType.EXTRA),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Kill Team", SC2WOL_LOC_ID_OFFSET + 1710, LocationType.CHALLENGE,
+ lambda state: (adv_tactics or logic.terran_common_unit(state)) and
+ logic.great_train_robbery_train_stopper(state) and
+ logic.terran_basic_anti_air(state)),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: Flawless", SC2WOL_LOC_ID_OFFSET + 1711, LocationType.CHALLENGE,
+ lambda state: logic.great_train_robbery_train_stopper(state) and
+ logic.terran_basic_anti_air(state)),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: 2 Trains Destroyed", SC2WOL_LOC_ID_OFFSET + 1712, LocationType.EXTRA,
+ lambda state: logic.great_train_robbery_train_stopper(state)),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: 4 Trains Destroyed", SC2WOL_LOC_ID_OFFSET + 1713, LocationType.EXTRA,
+ lambda state: logic.great_train_robbery_train_stopper(state) and
+ logic.terran_basic_anti_air(state)),
+ LocationData("The Great Train Robbery", "The Great Train Robbery: 6 Trains Destroyed", SC2WOL_LOC_ID_OFFSET + 1714, LocationType.EXTRA,
+ lambda state: logic.great_train_robbery_train_stopper(state) and
+ logic.terran_basic_anti_air(state)),
+ LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800, LocationType.VICTORY,
+ lambda state: logic.terran_common_unit(state) and
+ (adv_tactics or logic.terran_basic_anti_air)),
+ LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state)),
+ LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802, LocationType.VANILLA,
+ lambda state: logic.terran_common_unit(state)),
+ LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803, LocationType.VANILLA),
+ LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804, LocationType.VANILLA,
+ lambda state: logic.terran_common_unit(state)),
+ LocationData("Cutthroat", "Cutthroat: North Command Center", SC2WOL_LOC_ID_OFFSET + 1805, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state)),
+ LocationData("Cutthroat", "Cutthroat: South Command Center", SC2WOL_LOC_ID_OFFSET + 1806, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state)),
+ LocationData("Cutthroat", "Cutthroat: West Command Center", SC2WOL_LOC_ID_OFFSET + 1807, LocationType.EXTRA,
+ lambda state: logic.terran_common_unit(state)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900, LocationType.VICTORY,
+ lambda state: logic.engine_of_destruction_requirement(state)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901, LocationType.EXTRA,
+ lambda state: logic.marine_medic_upgrade(state)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902,
+ LocationType.CHALLENGE,
+ lambda state: logic.engine_of_destruction_requirement(state)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903, LocationType.VANILLA,
+ lambda state: logic.marine_medic_upgrade(state)),
+ LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904, LocationType.VANILLA,
+ lambda state: logic.engine_of_destruction_requirement(state)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, LocationType.VANILLA,
+ lambda state: logic.engine_of_destruction_requirement(state)),
+ LocationData("Engine of Destruction", "Engine of Destruction: West Base", SC2WOL_LOC_ID_OFFSET + 1906, LocationType.EXTRA,
+ lambda state: logic.engine_of_destruction_requirement(state)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Northwest Base", SC2WOL_LOC_ID_OFFSET + 1907, LocationType.EXTRA,
+ lambda state: logic.engine_of_destruction_requirement(state)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Northeast Base", SC2WOL_LOC_ID_OFFSET + 1908, LocationType.EXTRA,
+ lambda state: logic.engine_of_destruction_requirement(state)),
+ LocationData("Engine of Destruction", "Engine of Destruction: Southeast Base", SC2WOL_LOC_ID_OFFSET + 1909, LocationType.EXTRA,
+ lambda state: logic.engine_of_destruction_requirement(state)),
+ LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000, LocationType.VICTORY,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001, LocationType.VANILLA,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Media Blitz", "Media Blitz: Tower 2", SC2WOL_LOC_ID_OFFSET + 2002, LocationType.VANILLA,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003, LocationType.VANILLA,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004, LocationType.VANILLA),
+ LocationData("Media Blitz", "Media Blitz: All Barracks", SC2WOL_LOC_ID_OFFSET + 2005, LocationType.EXTRA,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Media Blitz", "Media Blitz: All Factories", SC2WOL_LOC_ID_OFFSET + 2006, LocationType.EXTRA,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Media Blitz", "Media Blitz: All Starports", SC2WOL_LOC_ID_OFFSET + 2007, LocationType.EXTRA,
+ lambda state: adv_tactics or logic.terran_competent_comp(state)),
+ LocationData("Media Blitz", "Media Blitz: Odin Not Trashed", SC2WOL_LOC_ID_OFFSET + 2008, LocationType.CHALLENGE,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Media Blitz", "Media Blitz: Surprise Attack Ends", SC2WOL_LOC_ID_OFFSET + 2009, LocationType.EXTRA),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100, LocationType.VICTORY,
+ lambda state: logic.marine_medic_upgrade(state)),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101, LocationType.VANILLA),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102, LocationType.VANILLA,
+ lambda state: logic.marine_medic_upgrade(state)),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103, LocationType.VANILLA,
+ lambda state: logic.marine_medic_upgrade(state)),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104, LocationType.VANILLA,
+ lambda state: logic.marine_medic_upgrade(state)),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk", SC2WOL_LOC_ID_OFFSET + 2105, LocationType.VANILLA,
+ lambda state: logic.marine_medic_upgrade(state)),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Fusion Reactor", SC2WOL_LOC_ID_OFFSET + 2106, LocationType.EXTRA,
+ lambda state: logic.marine_medic_upgrade(state)),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Entrance Holding Pen", SC2WOL_LOC_ID_OFFSET + 2107, LocationType.EXTRA),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Cargo Bay Warbot", SC2WOL_LOC_ID_OFFSET + 2108, LocationType.EXTRA),
+ LocationData("Piercing the Shroud", "Piercing the Shroud: Escape Warbot", SC2WOL_LOC_ID_OFFSET + 2109, LocationType.EXTRA,
+ lambda state: logic.marine_medic_upgrade(state)),
+ LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200, LocationType.VICTORY),
+ LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201, LocationType.VANILLA),
+ LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202, LocationType.VANILLA),
+ LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203, LocationType.VANILLA),
+ LocationData("Whispers of Doom", "Whispers of Doom: First Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2204, LocationType.EXTRA),
+ LocationData("Whispers of Doom", "Whispers of Doom: Second Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2205, LocationType.EXTRA),
+ LocationData("Whispers of Doom", "Whispers of Doom: Third Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2206, LocationType.EXTRA),
+ LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300, LocationType.VICTORY,
+ lambda state: logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301, LocationType.VANILLA,
+ lambda state: adv_tactics or logic.protoss_common_unit(state)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302, LocationType.VANILLA,
+ lambda state: adv_tactics or logic.protoss_common_unit(state)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Northeast Base", SC2WOL_LOC_ID_OFFSET + 2304, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Southwest Base", SC2WOL_LOC_ID_OFFSET + 2305, LocationType.CHALLENGE,
+ lambda state: logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Maar", SC2WOL_LOC_ID_OFFSET + 2306, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Northwest Preserver", SC2WOL_LOC_ID_OFFSET + 2307, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)),
+ LocationData("A Sinister Turn", "A Sinister Turn: Southwest Preserver", SC2WOL_LOC_ID_OFFSET + 2308, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)),
+ LocationData("A Sinister Turn", "A Sinister Turn: East Preserver", SC2WOL_LOC_ID_OFFSET + 2309, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)),
+ LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400, LocationType.VICTORY,
+ lambda state: adv_tactics and logic.protoss_static_defense(state) or logic.protoss_common_unit(state) and logic.protoss_competent_anti_air(state)),
+ LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401, LocationType.VANILLA),
+ LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402, LocationType.VANILLA,
+ lambda state: adv_tactics and logic.protoss_static_defense(state) or logic.protoss_common_unit(state)),
+ LocationData("Echoes of the Future", "Echoes of the Future: Base", SC2WOL_LOC_ID_OFFSET + 2403, LocationType.EXTRA),
+ LocationData("Echoes of the Future", "Echoes of the Future: Southwest Tendril", SC2WOL_LOC_ID_OFFSET + 2404, LocationType.EXTRA),
+ LocationData("Echoes of the Future", "Echoes of the Future: Southeast Tendril", SC2WOL_LOC_ID_OFFSET + 2405, LocationType.EXTRA,
+ lambda state: adv_tactics and logic.protoss_static_defense(state) or logic.protoss_common_unit(state)),
+ LocationData("Echoes of the Future", "Echoes of the Future: Northeast Tendril", SC2WOL_LOC_ID_OFFSET + 2406, LocationType.EXTRA,
+ lambda state: adv_tactics and logic.protoss_static_defense(state) or logic.protoss_common_unit(state)),
+ LocationData("Echoes of the Future", "Echoes of the Future: Northwest Tendril", SC2WOL_LOC_ID_OFFSET + 2407, LocationType.EXTRA,
+ lambda state: adv_tactics and logic.protoss_static_defense(state) or logic.protoss_common_unit(state)),
+ LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500, LocationType.VICTORY),
+ LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501, LocationType.VANILLA,
+ lambda state: logic.last_stand_requirement(state)),
+ LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502, LocationType.VANILLA,
+ lambda state: logic.last_stand_requirement(state)),
+ LocationData("In Utter Darkness", "In Utter Darkness: Urun", SC2WOL_LOC_ID_OFFSET + 2503, LocationType.EXTRA),
+ LocationData("In Utter Darkness", "In Utter Darkness: Mohandar", SC2WOL_LOC_ID_OFFSET + 2504, LocationType.EXTRA,
+ lambda state: logic.last_stand_requirement(state)),
+ LocationData("In Utter Darkness", "In Utter Darkness: Selendis", SC2WOL_LOC_ID_OFFSET + 2505, LocationType.EXTRA,
+ lambda state: logic.last_stand_requirement(state)),
+ LocationData("In Utter Darkness", "In Utter Darkness: Artanis", SC2WOL_LOC_ID_OFFSET + 2506, LocationType.EXTRA,
+ lambda state: logic.last_stand_requirement(state)),
+ LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600, LocationType.VICTORY,
+ lambda state: logic.terran_competent_comp(state) and
+ logic.terran_defense_rating(state, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601, LocationType.VANILLA,
+ lambda state: logic.terran_competent_comp(state) and
+ logic.terran_defense_rating(state, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: 2 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2602, LocationType.VANILLA,
+ lambda state: logic.terran_competent_comp(state) and
+ logic.terran_defense_rating(state, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: 4 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2603, LocationType.VANILLA,
+ lambda state: logic.terran_competent_comp(state) and
+ logic.terran_defense_rating(state, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: 6 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2604, LocationType.EXTRA,
+ lambda state: logic.terran_competent_comp(state) and
+ logic.terran_defense_rating(state, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: 8 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2605, LocationType.CHALLENGE,
+ lambda state: logic.terran_competent_comp(state) and
+ logic.terran_defense_rating(state, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: Southwest Spore Cannon", SC2WOL_LOC_ID_OFFSET + 2606, LocationType.EXTRA,
+ lambda state: logic.terran_competent_comp(state) and
+ logic.terran_defense_rating(state, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: Northwest Spore Cannon", SC2WOL_LOC_ID_OFFSET + 2607, LocationType.EXTRA,
+ lambda state: logic.terran_competent_comp(state) and
+ logic.terran_defense_rating(state, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: Northeast Spore Cannon", SC2WOL_LOC_ID_OFFSET + 2608, LocationType.EXTRA,
+ lambda state: logic.terran_competent_comp(state) and
+ logic.terran_defense_rating(state, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: East Spore Cannon", SC2WOL_LOC_ID_OFFSET + 2609, LocationType.EXTRA,
+ lambda state: logic.terran_competent_comp(state) and
+ logic.terran_defense_rating(state, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: Southeast Spore Cannon", SC2WOL_LOC_ID_OFFSET + 2610, LocationType.EXTRA,
+ lambda state: logic.terran_competent_comp(state) and
+ logic.terran_defense_rating(state, True) > 6),
+ LocationData("Gates of Hell", "Gates of Hell: Expansion Spore Cannon", SC2WOL_LOC_ID_OFFSET + 2611, LocationType.EXTRA,
+ lambda state: logic.terran_competent_comp(state) and
+ logic.terran_defense_rating(state, True) > 6),
+ LocationData("Belly of the Beast", "Belly of the Beast: Victory", SC2WOL_LOC_ID_OFFSET + 2700, LocationType.VICTORY),
+ LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701, LocationType.EXTRA),
+ LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702, LocationType.EXTRA),
+ LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703, LocationType.EXTRA),
+ LocationData("Belly of the Beast", "Belly of the Beast: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 2704, LocationType.VANILLA),
+ LocationData("Belly of the Beast", "Belly of the Beast: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 2705, LocationType.VANILLA),
+ LocationData("Belly of the Beast", "Belly of the Beast: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 2706, LocationType.VANILLA),
+ LocationData("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800, LocationType.VICTORY,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801, LocationType.VANILLA,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Shatter the Sky", "Shatter the Sky: Northwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2802, LocationType.VANILLA,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Shatter the Sky", "Shatter the Sky: Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803, LocationType.VANILLA,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Shatter the Sky", "Shatter the Sky: Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804, LocationType.VANILLA,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, LocationType.VANILLA,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Shatter the Sky", "Shatter the Sky: East Hatchery", SC2WOL_LOC_ID_OFFSET + 2806, LocationType.EXTRA,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Shatter the Sky", "Shatter the Sky: North Hatchery", SC2WOL_LOC_ID_OFFSET + 2807, LocationType.EXTRA,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("Shatter the Sky", "Shatter the Sky: Mid Hatchery", SC2WOL_LOC_ID_OFFSET + 2808, LocationType.EXTRA,
+ lambda state: logic.terran_competent_comp(state)),
+ LocationData("All-In", "All-In: Victory", SC2WOL_LOC_ID_OFFSET + 2900, LocationType.VICTORY,
+ lambda state: logic.all_in_requirement(state)),
+ LocationData("All-In", "All-In: First Kerrigan Attack", SC2WOL_LOC_ID_OFFSET + 2901, LocationType.EXTRA,
+ lambda state: logic.all_in_requirement(state)),
+ LocationData("All-In", "All-In: Second Kerrigan Attack", SC2WOL_LOC_ID_OFFSET + 2902, LocationType.EXTRA,
+ lambda state: logic.all_in_requirement(state)),
+ LocationData("All-In", "All-In: Third Kerrigan Attack", SC2WOL_LOC_ID_OFFSET + 2903, LocationType.EXTRA,
+ lambda state: logic.all_in_requirement(state)),
+ LocationData("All-In", "All-In: Fourth Kerrigan Attack", SC2WOL_LOC_ID_OFFSET + 2904, LocationType.EXTRA,
+ lambda state: logic.all_in_requirement(state)),
+ LocationData("All-In", "All-In: Fifth Kerrigan Attack", SC2WOL_LOC_ID_OFFSET + 2905, LocationType.EXTRA,
+ lambda state: logic.all_in_requirement(state)),
+
+ # HotS
+ LocationData("Lab Rat", "Lab Rat: Victory", SC2HOTS_LOC_ID_OFFSET + 100, LocationType.VICTORY,
+ lambda state: logic.zerg_common_unit(state)),
+ LocationData("Lab Rat", "Lab Rat: Gather Minerals", SC2HOTS_LOC_ID_OFFSET + 101, LocationType.VANILLA),
+ LocationData("Lab Rat", "Lab Rat: South Zergling Group", SC2HOTS_LOC_ID_OFFSET + 102, LocationType.VANILLA,
+ lambda state: adv_tactics or logic.zerg_common_unit(state)),
+ LocationData("Lab Rat", "Lab Rat: East Zergling Group", SC2HOTS_LOC_ID_OFFSET + 103, LocationType.VANILLA,
+ lambda state: adv_tactics or logic.zerg_common_unit(state)),
+ LocationData("Lab Rat", "Lab Rat: West Zergling Group", SC2HOTS_LOC_ID_OFFSET + 104, LocationType.VANILLA,
+ lambda state: adv_tactics or logic.zerg_common_unit(state)),
+ LocationData("Lab Rat", "Lab Rat: Hatchery", SC2HOTS_LOC_ID_OFFSET + 105, LocationType.EXTRA),
+ LocationData("Lab Rat", "Lab Rat: Overlord", SC2HOTS_LOC_ID_OFFSET + 106, LocationType.EXTRA),
+ LocationData("Lab Rat", "Lab Rat: Gas Turrets", SC2HOTS_LOC_ID_OFFSET + 107, LocationType.EXTRA,
+ lambda state: adv_tactics or logic.zerg_common_unit(state)),
+ LocationData("Back in the Saddle", "Back in the Saddle: Victory", SC2HOTS_LOC_ID_OFFSET + 200, LocationType.VICTORY,
+ lambda state: logic.basic_kerrigan(state) or kerriganless or logic.story_tech_granted),
+ LocationData("Back in the Saddle", "Back in the Saddle: Defend the Tram", SC2HOTS_LOC_ID_OFFSET + 201, LocationType.EXTRA,
+ lambda state: logic.basic_kerrigan(state) or kerriganless or logic.story_tech_granted),
+ LocationData("Back in the Saddle", "Back in the Saddle: Kinetic Blast", SC2HOTS_LOC_ID_OFFSET + 202, LocationType.VANILLA),
+ LocationData("Back in the Saddle", "Back in the Saddle: Crushing Grip", SC2HOTS_LOC_ID_OFFSET + 203, LocationType.VANILLA),
+ LocationData("Back in the Saddle", "Back in the Saddle: Reach the Sublevel", SC2HOTS_LOC_ID_OFFSET + 204, LocationType.EXTRA),
+ LocationData("Back in the Saddle", "Back in the Saddle: Door Section Cleared", SC2HOTS_LOC_ID_OFFSET + 205, LocationType.EXTRA,
+ lambda state: logic.basic_kerrigan(state) or kerriganless or logic.story_tech_granted),
+ LocationData("Rendezvous", "Rendezvous: Victory", SC2HOTS_LOC_ID_OFFSET + 300, LocationType.VICTORY,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Rendezvous", "Rendezvous: Right Queen", SC2HOTS_LOC_ID_OFFSET + 301, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Rendezvous", "Rendezvous: Center Queen", SC2HOTS_LOC_ID_OFFSET + 302, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Rendezvous", "Rendezvous: Left Queen", SC2HOTS_LOC_ID_OFFSET + 303, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Rendezvous", "Rendezvous: Hold Out Finished", SC2HOTS_LOC_ID_OFFSET + 304, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Harvest of Screams", "Harvest of Screams: Victory", SC2HOTS_LOC_ID_OFFSET + 400, LocationType.VICTORY,
+ lambda state: logic.zerg_common_unit(state)
+ and logic.zerg_basic_anti_air(state)),
+ LocationData("Harvest of Screams", "Harvest of Screams: First Ursadon Matriarch", SC2HOTS_LOC_ID_OFFSET + 401, LocationType.VANILLA),
+ LocationData("Harvest of Screams", "Harvest of Screams: North Ursadon Matriarch", SC2HOTS_LOC_ID_OFFSET + 402, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state)),
+ LocationData("Harvest of Screams", "Harvest of Screams: West Ursadon Matriarch", SC2HOTS_LOC_ID_OFFSET + 403, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state)),
+ LocationData("Harvest of Screams", "Harvest of Screams: Lost Brood", SC2HOTS_LOC_ID_OFFSET + 404, LocationType.EXTRA),
+ LocationData("Harvest of Screams", "Harvest of Screams: Northeast Psi-link Spire", SC2HOTS_LOC_ID_OFFSET + 405, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state)),
+ LocationData("Harvest of Screams", "Harvest of Screams: Northwest Psi-link Spire", SC2HOTS_LOC_ID_OFFSET + 406, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state)
+ and logic.zerg_basic_anti_air(state)),
+ LocationData("Harvest of Screams", "Harvest of Screams: Southwest Psi-link Spire", SC2HOTS_LOC_ID_OFFSET + 407, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state)
+ and logic.zerg_basic_anti_air(state)),
+ LocationData("Harvest of Screams", "Harvest of Screams: Nafash", SC2HOTS_LOC_ID_OFFSET + 408, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state)
+ and logic.zerg_basic_anti_air(state)),
+ LocationData("Shoot the Messenger", "Shoot the Messenger: Victory", SC2HOTS_LOC_ID_OFFSET + 500, LocationType.VICTORY,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Shoot the Messenger", "Shoot the Messenger: East Stasis Chamber", SC2HOTS_LOC_ID_OFFSET + 501, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state) and logic.zerg_basic_anti_air(state)),
+ LocationData("Shoot the Messenger", "Shoot the Messenger: Center Stasis Chamber", SC2HOTS_LOC_ID_OFFSET + 502, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state) or adv_tactics),
+ LocationData("Shoot the Messenger", "Shoot the Messenger: West Stasis Chamber", SC2HOTS_LOC_ID_OFFSET + 503, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state) and logic.zerg_basic_anti_air(state)),
+ LocationData("Shoot the Messenger", "Shoot the Messenger: Destroy 4 Shuttles", SC2HOTS_LOC_ID_OFFSET + 504, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state) and logic.zerg_basic_anti_air(state)),
+ LocationData("Shoot the Messenger", "Shoot the Messenger: Frozen Expansion", SC2HOTS_LOC_ID_OFFSET + 505, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state)),
+ LocationData("Shoot the Messenger", "Shoot the Messenger: Southwest Frozen Zerg", SC2HOTS_LOC_ID_OFFSET + 506, LocationType.EXTRA),
+ LocationData("Shoot the Messenger", "Shoot the Messenger: Southeast Frozen Zerg", SC2HOTS_LOC_ID_OFFSET + 507, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state) or adv_tactics),
+ LocationData("Shoot the Messenger", "Shoot the Messenger: West Frozen Zerg", SC2HOTS_LOC_ID_OFFSET + 508, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state) and logic.zerg_basic_anti_air(state)),
+ LocationData("Shoot the Messenger", "Shoot the Messenger: East Frozen Zerg", SC2HOTS_LOC_ID_OFFSET + 509, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state) and logic.zerg_competent_anti_air(state)),
+ LocationData("Enemy Within", "Enemy Within: Victory", SC2HOTS_LOC_ID_OFFSET + 600, LocationType.VICTORY,
+ lambda state: logic.zerg_pass_vents(state)
+ and (logic.story_tech_granted
+ or state.has_any({ItemNames.ZERGLING_RAPTOR_STRAIN, ItemNames.ROACH,
+ ItemNames.HYDRALISK, ItemNames.INFESTOR}, player))
+ ),
+ LocationData("Enemy Within", "Enemy Within: Infest Giant Ursadon", SC2HOTS_LOC_ID_OFFSET + 601, LocationType.VANILLA,
+ lambda state: logic.zerg_pass_vents(state)),
+ LocationData("Enemy Within", "Enemy Within: First Niadra Evolution", SC2HOTS_LOC_ID_OFFSET + 602, LocationType.VANILLA,
+ lambda state: logic.zerg_pass_vents(state)),
+ LocationData("Enemy Within", "Enemy Within: Second Niadra Evolution", SC2HOTS_LOC_ID_OFFSET + 603, LocationType.VANILLA,
+ lambda state: logic.zerg_pass_vents(state)),
+ LocationData("Enemy Within", "Enemy Within: Third Niadra Evolution", SC2HOTS_LOC_ID_OFFSET + 604, LocationType.VANILLA,
+ lambda state: logic.zerg_pass_vents(state)),
+ LocationData("Enemy Within", "Enemy Within: Warp Drive", SC2HOTS_LOC_ID_OFFSET + 605, LocationType.EXTRA,
+ lambda state: logic.zerg_pass_vents(state)),
+ LocationData("Enemy Within", "Enemy Within: Stasis Quadrant", SC2HOTS_LOC_ID_OFFSET + 606, LocationType.EXTRA,
+ lambda state: logic.zerg_pass_vents(state)),
+ LocationData("Domination", "Domination: Victory", SC2HOTS_LOC_ID_OFFSET + 700, LocationType.VICTORY,
+ lambda state: logic.zerg_common_unit(state) and logic.zerg_basic_anti_air(state)),
+ LocationData("Domination", "Domination: Center Infested Command Center", SC2HOTS_LOC_ID_OFFSET + 701, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state)),
+ LocationData("Domination", "Domination: North Infested Command Center", SC2HOTS_LOC_ID_OFFSET + 702, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state)),
+ LocationData("Domination", "Domination: Repel Zagara", SC2HOTS_LOC_ID_OFFSET + 703, LocationType.EXTRA),
+ LocationData("Domination", "Domination: Close Baneling Nest", SC2HOTS_LOC_ID_OFFSET + 704, LocationType.EXTRA),
+ LocationData("Domination", "Domination: South Baneling Nest", SC2HOTS_LOC_ID_OFFSET + 705, LocationType.EXTRA,
+ lambda state: adv_tactics or logic.zerg_common_unit(state)),
+ LocationData("Domination", "Domination: Southwest Baneling Nest", SC2HOTS_LOC_ID_OFFSET + 706, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state)),
+ LocationData("Domination", "Domination: Southeast Baneling Nest", SC2HOTS_LOC_ID_OFFSET + 707, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state) and logic.zerg_basic_anti_air(state)),
+ LocationData("Domination", "Domination: North Baneling Nest", SC2HOTS_LOC_ID_OFFSET + 708, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state)),
+ LocationData("Domination", "Domination: Northeast Baneling Nest", SC2HOTS_LOC_ID_OFFSET + 709, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state)),
+ LocationData("Fire in the Sky", "Fire in the Sky: Victory", SC2HOTS_LOC_ID_OFFSET + 800, LocationType.VICTORY,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state) and
+ logic.spread_creep(state)),
+ LocationData("Fire in the Sky", "Fire in the Sky: West Biomass", SC2HOTS_LOC_ID_OFFSET + 801, LocationType.VANILLA),
+ LocationData("Fire in the Sky", "Fire in the Sky: North Biomass", SC2HOTS_LOC_ID_OFFSET + 802, LocationType.VANILLA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state) and
+ logic.spread_creep(state)),
+ LocationData("Fire in the Sky", "Fire in the Sky: South Biomass", SC2HOTS_LOC_ID_OFFSET + 803, LocationType.VANILLA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state) and
+ logic.spread_creep(state)),
+ LocationData("Fire in the Sky", "Fire in the Sky: Destroy 3 Gorgons", SC2HOTS_LOC_ID_OFFSET + 804, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state) and
+ logic.spread_creep(state)),
+ LocationData("Fire in the Sky", "Fire in the Sky: Close Zerg Rescue", SC2HOTS_LOC_ID_OFFSET + 805, LocationType.EXTRA),
+ LocationData("Fire in the Sky", "Fire in the Sky: South Zerg Rescue", SC2HOTS_LOC_ID_OFFSET + 806, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state)),
+ LocationData("Fire in the Sky", "Fire in the Sky: North Zerg Rescue", SC2HOTS_LOC_ID_OFFSET + 807, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state) and
+ logic.spread_creep(state)),
+ LocationData("Fire in the Sky", "Fire in the Sky: West Queen Rescue", SC2HOTS_LOC_ID_OFFSET + 808, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state) and
+ logic.spread_creep(state)),
+ LocationData("Fire in the Sky", "Fire in the Sky: East Queen Rescue", SC2HOTS_LOC_ID_OFFSET + 809, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state) and
+ logic.spread_creep(state)),
+ LocationData("Old Soldiers", "Old Soldiers: Victory", SC2HOTS_LOC_ID_OFFSET + 900, LocationType.VICTORY,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Old Soldiers", "Old Soldiers: East Science Lab", SC2HOTS_LOC_ID_OFFSET + 901, LocationType.VANILLA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Old Soldiers", "Old Soldiers: North Science Lab", SC2HOTS_LOC_ID_OFFSET + 902, LocationType.VANILLA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Old Soldiers", "Old Soldiers: Get Nuked", SC2HOTS_LOC_ID_OFFSET + 903, LocationType.EXTRA),
+ LocationData("Old Soldiers", "Old Soldiers: Entrance Gate", SC2HOTS_LOC_ID_OFFSET + 904, LocationType.EXTRA),
+ LocationData("Old Soldiers", "Old Soldiers: Citadel Gate", SC2HOTS_LOC_ID_OFFSET + 905, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Old Soldiers", "Old Soldiers: South Expansion", SC2HOTS_LOC_ID_OFFSET + 906, LocationType.EXTRA),
+ LocationData("Old Soldiers", "Old Soldiers: Rich Mineral Expansion", SC2HOTS_LOC_ID_OFFSET + 907, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Waking the Ancient", "Waking the Ancient: Victory", SC2HOTS_LOC_ID_OFFSET + 1000, LocationType.VICTORY,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Waking the Ancient", "Waking the Ancient: Center Essence Pool", SC2HOTS_LOC_ID_OFFSET + 1001, LocationType.VANILLA),
+ LocationData("Waking the Ancient", "Waking the Ancient: East Essence Pool", SC2HOTS_LOC_ID_OFFSET + 1002, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state) and
+ (adv_tactics and logic.zerg_basic_anti_air(state)
+ or logic.zerg_competent_anti_air(state))),
+ LocationData("Waking the Ancient", "Waking the Ancient: South Essence Pool", SC2HOTS_LOC_ID_OFFSET + 1003, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state) and
+ (adv_tactics and logic.zerg_basic_anti_air(state)
+ or logic.zerg_competent_anti_air(state))),
+ LocationData("Waking the Ancient", "Waking the Ancient: Finish Feeding", SC2HOTS_LOC_ID_OFFSET + 1004, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Waking the Ancient", "Waking the Ancient: South Proxy Primal Hive", SC2HOTS_LOC_ID_OFFSET + 1005, LocationType.CHALLENGE,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Waking the Ancient", "Waking the Ancient: East Proxy Primal Hive", SC2HOTS_LOC_ID_OFFSET + 1006, LocationType.CHALLENGE,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Waking the Ancient", "Waking the Ancient: South Main Primal Hive", SC2HOTS_LOC_ID_OFFSET + 1007, LocationType.CHALLENGE,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Waking the Ancient", "Waking the Ancient: East Main Primal Hive", SC2HOTS_LOC_ID_OFFSET + 1008, LocationType.CHALLENGE,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("The Crucible", "The Crucible: Victory", SC2HOTS_LOC_ID_OFFSET + 1100, LocationType.VICTORY,
+ lambda state: logic.zerg_competent_defense(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("The Crucible", "The Crucible: Tyrannozor", SC2HOTS_LOC_ID_OFFSET + 1101, LocationType.VANILLA,
+ lambda state: logic.zerg_competent_defense(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("The Crucible", "The Crucible: Reach the Pool", SC2HOTS_LOC_ID_OFFSET + 1102, LocationType.VANILLA),
+ LocationData("The Crucible", "The Crucible: 15 Minutes Remaining", SC2HOTS_LOC_ID_OFFSET + 1103, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_defense(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("The Crucible", "The Crucible: 5 Minutes Remaining", SC2HOTS_LOC_ID_OFFSET + 1104, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_defense(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("The Crucible", "The Crucible: Pincer Attack", SC2HOTS_LOC_ID_OFFSET + 1105, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_defense(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("The Crucible", "The Crucible: Yagdra Claims Brakk's Pack", SC2HOTS_LOC_ID_OFFSET + 1106, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_defense(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Supreme", "Supreme: Victory", SC2HOTS_LOC_ID_OFFSET + 1200, LocationType.VICTORY,
+ lambda state: logic.supreme_requirement(state)),
+ LocationData("Supreme", "Supreme: First Relic", SC2HOTS_LOC_ID_OFFSET + 1201, LocationType.VANILLA,
+ lambda state: logic.supreme_requirement(state)),
+ LocationData("Supreme", "Supreme: Second Relic", SC2HOTS_LOC_ID_OFFSET + 1202, LocationType.VANILLA,
+ lambda state: logic.supreme_requirement(state)),
+ LocationData("Supreme", "Supreme: Third Relic", SC2HOTS_LOC_ID_OFFSET + 1203, LocationType.VANILLA,
+ lambda state: logic.supreme_requirement(state)),
+ LocationData("Supreme", "Supreme: Fourth Relic", SC2HOTS_LOC_ID_OFFSET + 1204, LocationType.VANILLA,
+ lambda state: logic.supreme_requirement(state)),
+ LocationData("Supreme", "Supreme: Yagdra", SC2HOTS_LOC_ID_OFFSET + 1205, LocationType.EXTRA,
+ lambda state: logic.supreme_requirement(state)),
+ LocationData("Supreme", "Supreme: Kraith", SC2HOTS_LOC_ID_OFFSET + 1206, LocationType.EXTRA,
+ lambda state: logic.supreme_requirement(state)),
+ LocationData("Supreme", "Supreme: Slivan", SC2HOTS_LOC_ID_OFFSET + 1207, LocationType.EXTRA,
+ lambda state: logic.supreme_requirement(state)),
+ LocationData("Infested", "Infested: Victory", SC2HOTS_LOC_ID_OFFSET + 1300, LocationType.VICTORY,
+ lambda state: logic.zerg_common_unit(state) and
+ ((logic.zerg_competent_anti_air(state) and state.has(ItemNames.INFESTOR, player)) or
+ (adv_tactics and logic.zerg_basic_anti_air(state)))),
+ LocationData("Infested", "Infested: East Science Facility", SC2HOTS_LOC_ID_OFFSET + 1301, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_basic_anti_air(state) and
+ logic.spread_creep(state)),
+ LocationData("Infested", "Infested: Center Science Facility", SC2HOTS_LOC_ID_OFFSET + 1302, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_basic_anti_air(state) and
+ logic.spread_creep(state)),
+ LocationData("Infested", "Infested: West Science Facility", SC2HOTS_LOC_ID_OFFSET + 1303, LocationType.VANILLA,
+ lambda state: logic.zerg_common_unit(state) and
+ logic.zerg_basic_anti_air(state) and
+ logic.spread_creep(state)),
+ LocationData("Infested", "Infested: First Intro Garrison", SC2HOTS_LOC_ID_OFFSET + 1304, LocationType.EXTRA),
+ LocationData("Infested", "Infested: Second Intro Garrison", SC2HOTS_LOC_ID_OFFSET + 1305, LocationType.EXTRA),
+ LocationData("Infested", "Infested: Base Garrison", SC2HOTS_LOC_ID_OFFSET + 1306, LocationType.EXTRA),
+ LocationData("Infested", "Infested: East Garrison", SC2HOTS_LOC_ID_OFFSET + 1307, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state)
+ and logic.zerg_basic_anti_air(state)
+ and (adv_tactics or state.has(ItemNames.INFESTOR, player))),
+ LocationData("Infested", "Infested: Mid Garrison", SC2HOTS_LOC_ID_OFFSET + 1308, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state)
+ and logic.zerg_basic_anti_air(state)
+ and (adv_tactics or state.has(ItemNames.INFESTOR, player))),
+ LocationData("Infested", "Infested: North Garrison", SC2HOTS_LOC_ID_OFFSET + 1309, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state)
+ and logic.zerg_basic_anti_air(state)
+ and (adv_tactics or state.has(ItemNames.INFESTOR, player))),
+ LocationData("Infested", "Infested: Close Southwest Garrison", SC2HOTS_LOC_ID_OFFSET + 1310, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state)
+ and logic.zerg_basic_anti_air(state)
+ and (adv_tactics or state.has(ItemNames.INFESTOR, player))),
+ LocationData("Infested", "Infested: Far Southwest Garrison", SC2HOTS_LOC_ID_OFFSET + 1311, LocationType.EXTRA,
+ lambda state: logic.zerg_common_unit(state)
+ and logic.zerg_basic_anti_air(state)
+ and (adv_tactics or state.has(ItemNames.INFESTOR, player))),
+ LocationData("Hand of Darkness", "Hand of Darkness: Victory", SC2HOTS_LOC_ID_OFFSET + 1400, LocationType.VICTORY,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Hand of Darkness", "Hand of Darkness: North Brutalisk", SC2HOTS_LOC_ID_OFFSET + 1401, LocationType.VANILLA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Hand of Darkness", "Hand of Darkness: South Brutalisk", SC2HOTS_LOC_ID_OFFSET + 1402, LocationType.VANILLA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Hand of Darkness", "Hand of Darkness: Kill 1 Hybrid", SC2HOTS_LOC_ID_OFFSET + 1403, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Hand of Darkness", "Hand of Darkness: Kill 2 Hybrid", SC2HOTS_LOC_ID_OFFSET + 1404, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Hand of Darkness", "Hand of Darkness: Kill 3 Hybrid", SC2HOTS_LOC_ID_OFFSET + 1405, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Hand of Darkness", "Hand of Darkness: Kill 4 Hybrid", SC2HOTS_LOC_ID_OFFSET + 1406, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Hand of Darkness", "Hand of Darkness: Kill 5 Hybrid", SC2HOTS_LOC_ID_OFFSET + 1407, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Hand of Darkness", "Hand of Darkness: Kill 6 Hybrid", SC2HOTS_LOC_ID_OFFSET + 1408, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Hand of Darkness", "Hand of Darkness: Kill 7 Hybrid", SC2HOTS_LOC_ID_OFFSET + 1409, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_basic_anti_air(state)),
+ LocationData("Phantoms of the Void", "Phantoms of the Void: Victory", SC2HOTS_LOC_ID_OFFSET + 1500, LocationType.VICTORY,
+ lambda state: logic.zerg_competent_comp(state) and
+ (logic.zerg_competent_anti_air(state) or adv_tactics)),
+ LocationData("Phantoms of the Void", "Phantoms of the Void: Northwest Crystal", SC2HOTS_LOC_ID_OFFSET + 1501, LocationType.VANILLA,
+ lambda state: logic.zerg_competent_comp(state) and
+ (logic.zerg_competent_anti_air(state) or adv_tactics)),
+ LocationData("Phantoms of the Void", "Phantoms of the Void: Northeast Crystal", SC2HOTS_LOC_ID_OFFSET + 1502, LocationType.VANILLA,
+ lambda state: logic.zerg_competent_comp(state) and
+ (logic.zerg_competent_anti_air(state) or adv_tactics)),
+ LocationData("Phantoms of the Void", "Phantoms of the Void: South Crystal", SC2HOTS_LOC_ID_OFFSET + 1503, LocationType.VANILLA),
+ LocationData("Phantoms of the Void", "Phantoms of the Void: Base Established", SC2HOTS_LOC_ID_OFFSET + 1504, LocationType.EXTRA),
+ LocationData("Phantoms of the Void", "Phantoms of the Void: Close Temple", SC2HOTS_LOC_ID_OFFSET + 1505, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ (logic.zerg_competent_anti_air(state) or adv_tactics)),
+ LocationData("Phantoms of the Void", "Phantoms of the Void: Mid Temple", SC2HOTS_LOC_ID_OFFSET + 1506, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ (logic.zerg_competent_anti_air(state) or adv_tactics)),
+ LocationData("Phantoms of the Void", "Phantoms of the Void: Southeast Temple", SC2HOTS_LOC_ID_OFFSET + 1507, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ (logic.zerg_competent_anti_air(state) or adv_tactics)),
+ LocationData("Phantoms of the Void", "Phantoms of the Void: Northeast Temple", SC2HOTS_LOC_ID_OFFSET + 1508, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ (logic.zerg_competent_anti_air(state) or adv_tactics)),
+ LocationData("Phantoms of the Void", "Phantoms of the Void: Northwest Temple", SC2HOTS_LOC_ID_OFFSET + 1509, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ (logic.zerg_competent_anti_air(state) or adv_tactics)),
+ LocationData("With Friends Like These", "With Friends Like These: Victory", SC2HOTS_LOC_ID_OFFSET + 1600, LocationType.VICTORY),
+ LocationData("With Friends Like These", "With Friends Like These: Pirate Capital Ship", SC2HOTS_LOC_ID_OFFSET + 1601, LocationType.VANILLA),
+ LocationData("With Friends Like These", "With Friends Like These: First Mineral Patch", SC2HOTS_LOC_ID_OFFSET + 1602, LocationType.VANILLA),
+ LocationData("With Friends Like These", "With Friends Like These: Second Mineral Patch", SC2HOTS_LOC_ID_OFFSET + 1603, LocationType.VANILLA),
+ LocationData("With Friends Like These", "With Friends Like These: Third Mineral Patch", SC2HOTS_LOC_ID_OFFSET + 1604, LocationType.VANILLA),
+ LocationData("Conviction", "Conviction: Victory", SC2HOTS_LOC_ID_OFFSET + 1700, LocationType.VICTORY,
+ lambda state: logic.two_kerrigan_actives(state) and
+ (logic.basic_kerrigan(state) or logic.story_tech_granted) or kerriganless),
+ LocationData("Conviction", "Conviction: First Secret Documents", SC2HOTS_LOC_ID_OFFSET + 1701, LocationType.VANILLA,
+ lambda state: logic.two_kerrigan_actives(state) or kerriganless),
+ LocationData("Conviction", "Conviction: Second Secret Documents", SC2HOTS_LOC_ID_OFFSET + 1702, LocationType.VANILLA,
+ lambda state: logic.two_kerrigan_actives(state) and
+ (logic.basic_kerrigan(state) or logic.story_tech_granted) or kerriganless),
+ LocationData("Conviction", "Conviction: Power Coupling", SC2HOTS_LOC_ID_OFFSET + 1703, LocationType.EXTRA,
+ lambda state: logic.two_kerrigan_actives(state) or kerriganless),
+ LocationData("Conviction", "Conviction: Door Blasted", SC2HOTS_LOC_ID_OFFSET + 1704, LocationType.EXTRA,
+ lambda state: logic.two_kerrigan_actives(state) or kerriganless),
+ LocationData("Planetfall", "Planetfall: Victory", SC2HOTS_LOC_ID_OFFSET + 1800, LocationType.VICTORY,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Planetfall", "Planetfall: East Gate", SC2HOTS_LOC_ID_OFFSET + 1801, LocationType.VANILLA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Planetfall", "Planetfall: Northwest Gate", SC2HOTS_LOC_ID_OFFSET + 1802, LocationType.VANILLA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Planetfall", "Planetfall: North Gate", SC2HOTS_LOC_ID_OFFSET + 1803, LocationType.VANILLA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Planetfall", "Planetfall: 1 Bile Launcher Deployed", SC2HOTS_LOC_ID_OFFSET + 1804, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Planetfall", "Planetfall: 2 Bile Launchers Deployed", SC2HOTS_LOC_ID_OFFSET + 1805, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Planetfall", "Planetfall: 3 Bile Launchers Deployed", SC2HOTS_LOC_ID_OFFSET + 1806, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Planetfall", "Planetfall: 4 Bile Launchers Deployed", SC2HOTS_LOC_ID_OFFSET + 1807, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Planetfall", "Planetfall: 5 Bile Launchers Deployed", SC2HOTS_LOC_ID_OFFSET + 1808, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Planetfall", "Planetfall: Sons of Korhal", SC2HOTS_LOC_ID_OFFSET + 1809, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Planetfall", "Planetfall: Night Wolves", SC2HOTS_LOC_ID_OFFSET + 1810, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Planetfall", "Planetfall: West Expansion", SC2HOTS_LOC_ID_OFFSET + 1811, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Planetfall", "Planetfall: Mid Expansion", SC2HOTS_LOC_ID_OFFSET + 1812, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Death From Above", "Death From Above: Victory", SC2HOTS_LOC_ID_OFFSET + 1900, LocationType.VICTORY,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Death From Above", "Death From Above: First Power Link", SC2HOTS_LOC_ID_OFFSET + 1901, LocationType.VANILLA),
+ LocationData("Death From Above", "Death From Above: Second Power Link", SC2HOTS_LOC_ID_OFFSET + 1902, LocationType.VANILLA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Death From Above", "Death From Above: Third Power Link", SC2HOTS_LOC_ID_OFFSET + 1903, LocationType.VANILLA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Death From Above", "Death From Above: Expansion Command Center", SC2HOTS_LOC_ID_OFFSET + 1904, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("Death From Above", "Death From Above: Main Path Command Center", SC2HOTS_LOC_ID_OFFSET + 1905, LocationType.EXTRA,
+ lambda state: logic.zerg_competent_comp(state) and
+ logic.zerg_competent_anti_air(state)),
+ LocationData("The Reckoning", "The Reckoning: Victory", SC2HOTS_LOC_ID_OFFSET + 2000, LocationType.VICTORY,
+ lambda state: logic.the_reckoning_requirement(state)),
+ LocationData("The Reckoning", "The Reckoning: South Lane", SC2HOTS_LOC_ID_OFFSET + 2001, LocationType.VANILLA,
+ lambda state: logic.the_reckoning_requirement(state)),
+ LocationData("The Reckoning", "The Reckoning: North Lane", SC2HOTS_LOC_ID_OFFSET + 2002, LocationType.VANILLA,
+ lambda state: logic.the_reckoning_requirement(state)),
+ LocationData("The Reckoning", "The Reckoning: East Lane", SC2HOTS_LOC_ID_OFFSET + 2003, LocationType.VANILLA,
+ lambda state: logic.the_reckoning_requirement(state)),
+ LocationData("The Reckoning", "The Reckoning: Odin", SC2HOTS_LOC_ID_OFFSET + 2004, LocationType.EXTRA,
+ lambda state: logic.the_reckoning_requirement(state)),
+
+ # LotV Prologue
+ LocationData("Dark Whispers", "Dark Whispers: Victory", SC2LOTV_LOC_ID_OFFSET + 100, LocationType.VICTORY,
+ lambda state: logic.protoss_common_unit(state) \
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Dark Whispers", "Dark Whispers: First Prisoner Group", SC2LOTV_LOC_ID_OFFSET + 101, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state) \
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Dark Whispers", "Dark Whispers: Second Prisoner Group", SC2LOTV_LOC_ID_OFFSET + 102, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state) \
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Dark Whispers", "Dark Whispers: First Pylon", SC2LOTV_LOC_ID_OFFSET + 103, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state) \
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Dark Whispers", "Dark Whispers: Second Pylon", SC2LOTV_LOC_ID_OFFSET + 104, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state) \
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Ghosts in the Fog", "Ghosts in the Fog: Victory", SC2LOTV_LOC_ID_OFFSET + 200, LocationType.VICTORY,
+ lambda state: logic.protoss_common_unit(state) \
+ and logic.protoss_anti_armor_anti_air(state)),
+ LocationData("Ghosts in the Fog", "Ghosts in the Fog: South Rock Formation", SC2LOTV_LOC_ID_OFFSET + 201, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state) \
+ and logic.protoss_anti_armor_anti_air(state)),
+ LocationData("Ghosts in the Fog", "Ghosts in the Fog: West Rock Formation", SC2LOTV_LOC_ID_OFFSET + 202, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state) \
+ and logic.protoss_anti_armor_anti_air(state)),
+ LocationData("Ghosts in the Fog", "Ghosts in the Fog: East Rock Formation", SC2LOTV_LOC_ID_OFFSET + 203, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state) \
+ and logic.protoss_anti_armor_anti_air(state) \
+ and logic.protoss_can_attack_behind_chasm(state)),
+ LocationData("Evil Awoken", "Evil Awoken: Victory", SC2LOTV_LOC_ID_OFFSET + 300, LocationType.VICTORY,
+ lambda state: adv_tactics or logic.protoss_stalker_upgrade(state)),
+ LocationData("Evil Awoken", "Evil Awoken: Temple Investigated", SC2LOTV_LOC_ID_OFFSET + 301, LocationType.EXTRA),
+ LocationData("Evil Awoken", "Evil Awoken: Void Catalyst", SC2LOTV_LOC_ID_OFFSET + 302, LocationType.EXTRA),
+ LocationData("Evil Awoken", "Evil Awoken: First Particle Cannon", SC2LOTV_LOC_ID_OFFSET + 303, LocationType.VANILLA),
+ LocationData("Evil Awoken", "Evil Awoken: Second Particle Cannon", SC2LOTV_LOC_ID_OFFSET + 304, LocationType.VANILLA),
+ LocationData("Evil Awoken", "Evil Awoken: Third Particle Cannon", SC2LOTV_LOC_ID_OFFSET + 305, LocationType.VANILLA),
+
+
+ # LotV
+ LocationData("For Aiur!", "For Aiur!: Victory", SC2LOTV_LOC_ID_OFFSET + 400, LocationType.VICTORY),
+ LocationData("For Aiur!", "For Aiur!: Southwest Hive", SC2LOTV_LOC_ID_OFFSET + 401, LocationType.VANILLA),
+ LocationData("For Aiur!", "For Aiur!: Northwest Hive", SC2LOTV_LOC_ID_OFFSET + 402, LocationType.VANILLA),
+ LocationData("For Aiur!", "For Aiur!: Northeast Hive", SC2LOTV_LOC_ID_OFFSET + 403, LocationType.VANILLA),
+ LocationData("For Aiur!", "For Aiur!: East Hive", SC2LOTV_LOC_ID_OFFSET + 404, LocationType.VANILLA),
+ LocationData("For Aiur!", "For Aiur!: West Conduit", SC2LOTV_LOC_ID_OFFSET + 405, LocationType.EXTRA),
+ LocationData("For Aiur!", "For Aiur!: Middle Conduit", SC2LOTV_LOC_ID_OFFSET + 406, LocationType.EXTRA),
+ LocationData("For Aiur!", "For Aiur!: Northeast Conduit", SC2LOTV_LOC_ID_OFFSET + 407, LocationType.EXTRA),
+ LocationData("The Growing Shadow", "The Growing Shadow: Victory", SC2LOTV_LOC_ID_OFFSET + 500, LocationType.VICTORY,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("The Growing Shadow", "The Growing Shadow: Close Pylon", SC2LOTV_LOC_ID_OFFSET + 501, LocationType.VANILLA),
+ LocationData("The Growing Shadow", "The Growing Shadow: East Pylon", SC2LOTV_LOC_ID_OFFSET + 502, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("The Growing Shadow", "The Growing Shadow: West Pylon", SC2LOTV_LOC_ID_OFFSET + 503, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("The Growing Shadow", "The Growing Shadow: Nexus", SC2LOTV_LOC_ID_OFFSET + 504, LocationType.EXTRA),
+ LocationData("The Growing Shadow", "The Growing Shadow: Templar Base", SC2LOTV_LOC_ID_OFFSET + 505, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("The Spear of Adun", "The Spear of Adun: Victory", SC2LOTV_LOC_ID_OFFSET + 600, LocationType.VICTORY,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("The Spear of Adun", "The Spear of Adun: Close Warp Gate", SC2LOTV_LOC_ID_OFFSET + 601, LocationType.VANILLA),
+ LocationData("The Spear of Adun", "The Spear of Adun: West Warp Gate", SC2LOTV_LOC_ID_OFFSET + 602, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("The Spear of Adun", "The Spear of Adun: North Warp Gate", SC2LOTV_LOC_ID_OFFSET + 603, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("The Spear of Adun", "The Spear of Adun: North Power Cell", SC2LOTV_LOC_ID_OFFSET + 604, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("The Spear of Adun", "The Spear of Adun: East Power Cell", SC2LOTV_LOC_ID_OFFSET + 605, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("The Spear of Adun", "The Spear of Adun: South Power Cell", SC2LOTV_LOC_ID_OFFSET + 606, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("The Spear of Adun", "The Spear of Adun: Southeast Power Cell", SC2LOTV_LOC_ID_OFFSET + 607, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Sky Shield", "Sky Shield: Victory", SC2LOTV_LOC_ID_OFFSET + 700, LocationType.VICTORY,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Sky Shield", "Sky Shield: Mid EMP Scrambler", SC2LOTV_LOC_ID_OFFSET + 701, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Sky Shield", "Sky Shield: Southeast EMP Scrambler", SC2LOTV_LOC_ID_OFFSET + 702, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Sky Shield", "Sky Shield: North EMP Scrambler", SC2LOTV_LOC_ID_OFFSET + 703, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Sky Shield", "Sky Shield: Mid Stabilizer", SC2LOTV_LOC_ID_OFFSET + 704, LocationType.EXTRA),
+ LocationData("Sky Shield", "Sky Shield: Southwest Stabilizer", SC2LOTV_LOC_ID_OFFSET + 705, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Sky Shield", "Sky Shield: Northwest Stabilizer", SC2LOTV_LOC_ID_OFFSET + 706, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Sky Shield", "Sky Shield: Northeast Stabilizer", SC2LOTV_LOC_ID_OFFSET + 707, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Sky Shield", "Sky Shield: Southeast Stabilizer", SC2LOTV_LOC_ID_OFFSET + 708, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Sky Shield", "Sky Shield: West Raynor Base", SC2LOTV_LOC_ID_OFFSET + 709, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Sky Shield", "Sky Shield: East Raynor Base", SC2LOTV_LOC_ID_OFFSET + 710, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_basic_anti_air(state)),
+ LocationData("Brothers in Arms", "Brothers in Arms: Victory", SC2LOTV_LOC_ID_OFFSET + 800, LocationType.VICTORY,
+ lambda state: logic.brothers_in_arms_requirement(state)),
+ LocationData("Brothers in Arms", "Brothers in Arms: Mid Science Facility", SC2LOTV_LOC_ID_OFFSET + 801, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state) or logic.take_over_ai_allies),
+ LocationData("Brothers in Arms", "Brothers in Arms: North Science Facility", SC2LOTV_LOC_ID_OFFSET + 802, LocationType.VANILLA,
+ lambda state: logic.brothers_in_arms_requirement(state)
+ or logic.take_over_ai_allies
+ and logic.advanced_tactics
+ and (
+ logic.terran_common_unit(state)
+ or logic.protoss_common_unit(state)
+ )
+ ),
+ LocationData("Brothers in Arms", "Brothers in Arms: South Science Facility", SC2LOTV_LOC_ID_OFFSET + 803, LocationType.VANILLA,
+ lambda state: logic.brothers_in_arms_requirement(state)),
+ LocationData("Amon's Reach", "Amon's Reach: Victory", SC2LOTV_LOC_ID_OFFSET + 900, LocationType.VICTORY,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Amon's Reach", "Amon's Reach: Close Solarite Reserve", SC2LOTV_LOC_ID_OFFSET + 901, LocationType.VANILLA),
+ LocationData("Amon's Reach", "Amon's Reach: North Solarite Reserve", SC2LOTV_LOC_ID_OFFSET + 902, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Amon's Reach", "Amon's Reach: East Solarite Reserve", SC2LOTV_LOC_ID_OFFSET + 903, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Amon's Reach", "Amon's Reach: West Launch Bay", SC2LOTV_LOC_ID_OFFSET + 904, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Amon's Reach", "Amon's Reach: South Launch Bay", SC2LOTV_LOC_ID_OFFSET + 905, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Amon's Reach", "Amon's Reach: Northwest Launch Bay", SC2LOTV_LOC_ID_OFFSET + 906, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Amon's Reach", "Amon's Reach: East Launch Bay", SC2LOTV_LOC_ID_OFFSET + 907, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Last Stand", "Last Stand: Victory", SC2LOTV_LOC_ID_OFFSET + 1000, LocationType.VICTORY,
+ lambda state: logic.last_stand_requirement(state)),
+ LocationData("Last Stand", "Last Stand: West Zenith Stone", SC2LOTV_LOC_ID_OFFSET + 1001, LocationType.VANILLA,
+ lambda state: logic.last_stand_requirement(state)),
+ LocationData("Last Stand", "Last Stand: North Zenith Stone", SC2LOTV_LOC_ID_OFFSET + 1002, LocationType.VANILLA,
+ lambda state: logic.last_stand_requirement(state)),
+ LocationData("Last Stand", "Last Stand: East Zenith Stone", SC2LOTV_LOC_ID_OFFSET + 1003, LocationType.VANILLA,
+ lambda state: logic.last_stand_requirement(state)),
+ LocationData("Last Stand", "Last Stand: 1 Billion Zerg", SC2LOTV_LOC_ID_OFFSET + 1004, LocationType.EXTRA,
+ lambda state: logic.last_stand_requirement(state)),
+ LocationData("Last Stand", "Last Stand: 1.5 Billion Zerg", SC2LOTV_LOC_ID_OFFSET + 1005, LocationType.VANILLA,
+ lambda state: logic.last_stand_requirement(state) and (
+ state.has_all({ItemNames.KHAYDARIN_MONOLITH, ItemNames.PHOTON_CANNON, ItemNames.SHIELD_BATTERY}, player)
+ or state.has_any({ItemNames.SOA_SOLAR_LANCE, ItemNames.SOA_DEPLOY_FENIX}, player)
+ )),
+ LocationData("Forbidden Weapon", "Forbidden Weapon: Victory", SC2LOTV_LOC_ID_OFFSET + 1100, LocationType.VICTORY,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_armor_anti_air(state)),
+ LocationData("Forbidden Weapon", "Forbidden Weapon: South Solarite", SC2LOTV_LOC_ID_OFFSET + 1101, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_armor_anti_air(state)),
+ LocationData("Forbidden Weapon", "Forbidden Weapon: North Solarite", SC2LOTV_LOC_ID_OFFSET + 1102, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_armor_anti_air(state)),
+ LocationData("Forbidden Weapon", "Forbidden Weapon: Northwest Solarite", SC2LOTV_LOC_ID_OFFSET + 1103, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_armor_anti_air(state)),
+ LocationData("Temple of Unification", "Temple of Unification: Victory", SC2LOTV_LOC_ID_OFFSET + 1200, LocationType.VICTORY,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_armor_anti_air(state)),
+ LocationData("Temple of Unification", "Temple of Unification: Mid Celestial Lock", SC2LOTV_LOC_ID_OFFSET + 1201, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_armor_anti_air(state)),
+ LocationData("Temple of Unification", "Temple of Unification: West Celestial Lock", SC2LOTV_LOC_ID_OFFSET + 1202, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_armor_anti_air(state)),
+ LocationData("Temple of Unification", "Temple of Unification: South Celestial Lock", SC2LOTV_LOC_ID_OFFSET + 1203, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_armor_anti_air(state)),
+ LocationData("Temple of Unification", "Temple of Unification: East Celestial Lock", SC2LOTV_LOC_ID_OFFSET + 1204, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_armor_anti_air(state)),
+ LocationData("Temple of Unification", "Temple of Unification: North Celestial Lock", SC2LOTV_LOC_ID_OFFSET + 1205, LocationType.EXTRA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_armor_anti_air(state)),
+ LocationData("Temple of Unification", "Temple of Unification: Titanic Warp Prism", SC2LOTV_LOC_ID_OFFSET + 1206, LocationType.VANILLA,
+ lambda state: logic.protoss_common_unit(state)
+ and logic.protoss_anti_armor_anti_air(state)),
+ LocationData("The Infinite Cycle", "The Infinite Cycle: Victory", SC2LOTV_LOC_ID_OFFSET + 1300, LocationType.VICTORY,
+ lambda state: logic.the_infinite_cycle_requirement(state)),
+ LocationData("The Infinite Cycle", "The Infinite Cycle: First Hall of Revelation", SC2LOTV_LOC_ID_OFFSET + 1301, LocationType.EXTRA,
+ lambda state: logic.the_infinite_cycle_requirement(state)),
+ LocationData("The Infinite Cycle", "The Infinite Cycle: Second Hall of Revelation", SC2LOTV_LOC_ID_OFFSET + 1302, LocationType.EXTRA,
+ lambda state: logic.the_infinite_cycle_requirement(state)),
+ LocationData("The Infinite Cycle", "The Infinite Cycle: First Xel'Naga Device", SC2LOTV_LOC_ID_OFFSET + 1303, LocationType.VANILLA,
+ lambda state: logic.the_infinite_cycle_requirement(state)),
+ LocationData("The Infinite Cycle", "The Infinite Cycle: Second Xel'Naga Device", SC2LOTV_LOC_ID_OFFSET + 1304, LocationType.VANILLA,
+ lambda state: logic.the_infinite_cycle_requirement(state)),
+ LocationData("The Infinite Cycle", "The Infinite Cycle: Third Xel'Naga Device", SC2LOTV_LOC_ID_OFFSET + 1305, LocationType.VANILLA,
+ lambda state: logic.the_infinite_cycle_requirement(state)),
+ LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: Victory", SC2LOTV_LOC_ID_OFFSET + 1400, LocationType.VICTORY,
+ lambda state: logic.harbinger_of_oblivion_requirement(state)),
+ LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: Artanis", SC2LOTV_LOC_ID_OFFSET + 1401, LocationType.EXTRA),
+ LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: Northwest Void Crystal", SC2LOTV_LOC_ID_OFFSET + 1402, LocationType.EXTRA,
+ lambda state: logic.harbinger_of_oblivion_requirement(state)),
+ LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: Northeast Void Crystal", SC2LOTV_LOC_ID_OFFSET + 1403, LocationType.EXTRA,
+ lambda state: logic.harbinger_of_oblivion_requirement(state)),
+ LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: Southwest Void Crystal", SC2LOTV_LOC_ID_OFFSET + 1404, LocationType.EXTRA,
+ lambda state: logic.harbinger_of_oblivion_requirement(state)),
+ LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: Southeast Void Crystal", SC2LOTV_LOC_ID_OFFSET + 1405, LocationType.EXTRA,
+ lambda state: logic.harbinger_of_oblivion_requirement(state)),
+ LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: South Xel'Naga Vessel", SC2LOTV_LOC_ID_OFFSET + 1406, LocationType.VANILLA),
+ LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: Mid Xel'Naga Vessel", SC2LOTV_LOC_ID_OFFSET + 1407, LocationType.VANILLA,
+ lambda state: logic.harbinger_of_oblivion_requirement(state)),
+ LocationData("Harbinger of Oblivion", "Harbinger of Oblivion: North Xel'Naga Vessel", SC2LOTV_LOC_ID_OFFSET + 1408, LocationType.VANILLA,
+ lambda state: logic.harbinger_of_oblivion_requirement(state)),
+ LocationData("Unsealing the Past", "Unsealing the Past: Victory", SC2LOTV_LOC_ID_OFFSET + 1500, LocationType.VICTORY,
+ lambda state: logic.protoss_basic_splash(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Unsealing the Past", "Unsealing the Past: Zerg Cleared", SC2LOTV_LOC_ID_OFFSET + 1501, LocationType.EXTRA),
+ LocationData("Unsealing the Past", "Unsealing the Past: First Stasis Lock", SC2LOTV_LOC_ID_OFFSET + 1502, LocationType.EXTRA,
+ lambda state: logic.advanced_tactics \
+ or logic.protoss_basic_splash(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Unsealing the Past", "Unsealing the Past: Second Stasis Lock", SC2LOTV_LOC_ID_OFFSET + 1503, LocationType.EXTRA,
+ lambda state: logic.protoss_basic_splash(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Unsealing the Past", "Unsealing the Past: Third Stasis Lock", SC2LOTV_LOC_ID_OFFSET + 1504, LocationType.EXTRA,
+ lambda state: logic.protoss_basic_splash(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Unsealing the Past", "Unsealing the Past: Fourth Stasis Lock", SC2LOTV_LOC_ID_OFFSET + 1505, LocationType.EXTRA,
+ lambda state: logic.protoss_basic_splash(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Unsealing the Past", "Unsealing the Past: South Power Core", SC2LOTV_LOC_ID_OFFSET + 1506, LocationType.VANILLA,
+ lambda state: logic.protoss_basic_splash(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Unsealing the Past", "Unsealing the Past: East Power Core", SC2LOTV_LOC_ID_OFFSET + 1507, LocationType.VANILLA,
+ lambda state: logic.protoss_basic_splash(state)
+ and logic.protoss_anti_light_anti_air(state)),
+ LocationData("Purification", "Purification: Victory", SC2LOTV_LOC_ID_OFFSET + 1600, LocationType.VICTORY,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Purification", "Purification: North Sector: West Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1601, LocationType.VANILLA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Purification", "Purification: North Sector: Northeast Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1602, LocationType.EXTRA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Purification", "Purification: North Sector: Southeast Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1603, LocationType.EXTRA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Purification", "Purification: South Sector: West Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1604, LocationType.VANILLA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Purification", "Purification: South Sector: North Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1605, LocationType.EXTRA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Purification", "Purification: South Sector: East Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1606, LocationType.EXTRA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Purification", "Purification: West Sector: West Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1607, LocationType.VANILLA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Purification", "Purification: West Sector: Mid Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1608, LocationType.EXTRA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Purification", "Purification: West Sector: East Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1609, LocationType.EXTRA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Purification", "Purification: East Sector: North Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1610, LocationType.VANILLA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Purification", "Purification: East Sector: West Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1611, LocationType.EXTRA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Purification", "Purification: East Sector: South Null Circuit", SC2LOTV_LOC_ID_OFFSET + 1612, LocationType.EXTRA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Purification", "Purification: Purifier Warden", SC2LOTV_LOC_ID_OFFSET + 1613, LocationType.VANILLA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Steps of the Rite", "Steps of the Rite: Victory", SC2LOTV_LOC_ID_OFFSET + 1700, LocationType.VICTORY,
+ lambda state: logic.steps_of_the_rite_requirement(state)),
+ LocationData("Steps of the Rite", "Steps of the Rite: First Terrazine Fog", SC2LOTV_LOC_ID_OFFSET + 1701, LocationType.EXTRA,
+ lambda state: logic.steps_of_the_rite_requirement(state)),
+ LocationData("Steps of the Rite", "Steps of the Rite: Southwest Guardian", SC2LOTV_LOC_ID_OFFSET + 1702, LocationType.EXTRA,
+ lambda state: logic.steps_of_the_rite_requirement(state)),
+ LocationData("Steps of the Rite", "Steps of the Rite: West Guardian", SC2LOTV_LOC_ID_OFFSET + 1703, LocationType.EXTRA,
+ lambda state: logic.steps_of_the_rite_requirement(state)),
+ LocationData("Steps of the Rite", "Steps of the Rite: Northwest Guardian", SC2LOTV_LOC_ID_OFFSET + 1704, LocationType.EXTRA,
+ lambda state: logic.steps_of_the_rite_requirement(state)),
+ LocationData("Steps of the Rite", "Steps of the Rite: Northeast Guardian", SC2LOTV_LOC_ID_OFFSET + 1705, LocationType.EXTRA,
+ lambda state: logic.steps_of_the_rite_requirement(state)),
+ LocationData("Steps of the Rite", "Steps of the Rite: North Mothership", SC2LOTV_LOC_ID_OFFSET + 1706, LocationType.VANILLA,
+ lambda state: logic.steps_of_the_rite_requirement(state)),
+ LocationData("Steps of the Rite", "Steps of the Rite: South Mothership", SC2LOTV_LOC_ID_OFFSET + 1707, LocationType.VANILLA,
+ lambda state: logic.steps_of_the_rite_requirement(state)),
+ LocationData("Rak'Shir", "Rak'Shir: Victory", SC2LOTV_LOC_ID_OFFSET + 1800, LocationType.VICTORY,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Rak'Shir", "Rak'Shir: North Slayn Elemental", SC2LOTV_LOC_ID_OFFSET + 1801, LocationType.VANILLA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Rak'Shir", "Rak'Shir: Southwest Slayn Elemental", SC2LOTV_LOC_ID_OFFSET + 1802, LocationType.VANILLA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Rak'Shir", "Rak'Shir: East Slayn Elemental", SC2LOTV_LOC_ID_OFFSET + 1803, LocationType.VANILLA,
+ lambda state: logic.protoss_competent_comp(state)),
+ LocationData("Templar's Charge", "Templar's Charge: Victory", SC2LOTV_LOC_ID_OFFSET + 1900, LocationType.VICTORY,
+ lambda state: logic.templars_charge_requirement(state)),
+ LocationData("Templar's Charge", "Templar's Charge: Northwest Power Core", SC2LOTV_LOC_ID_OFFSET + 1901, LocationType.EXTRA,
+ lambda state: logic.templars_charge_requirement(state)),
+ LocationData("Templar's Charge", "Templar's Charge: Northeast Power Core", SC2LOTV_LOC_ID_OFFSET + 1902, LocationType.EXTRA,
+ lambda state: logic.templars_charge_requirement(state)),
+ LocationData("Templar's Charge", "Templar's Charge: Southeast Power Core", SC2LOTV_LOC_ID_OFFSET + 1903, LocationType.EXTRA,
+ lambda state: logic.templars_charge_requirement(state)),
+ LocationData("Templar's Charge", "Templar's Charge: West Hybrid Stasis Chamber", SC2LOTV_LOC_ID_OFFSET + 1904, LocationType.VANILLA,
+ lambda state: logic.templars_charge_requirement(state)),
+ LocationData("Templar's Charge", "Templar's Charge: Southeast Hybrid Stasis Chamber", SC2LOTV_LOC_ID_OFFSET + 1905, LocationType.VANILLA,
+ lambda state: logic.protoss_fleet(state)),
+ LocationData("Templar's Return", "Templar's Return: Victory", SC2LOTV_LOC_ID_OFFSET + 2000, LocationType.VICTORY,
+ lambda state: logic.templars_return_requirement(state)),
+ LocationData("Templar's Return", "Templar's Return: Citadel: First Gate", SC2LOTV_LOC_ID_OFFSET + 2001, LocationType.EXTRA),
+ LocationData("Templar's Return", "Templar's Return: Citadel: Second Gate", SC2LOTV_LOC_ID_OFFSET + 2002, LocationType.EXTRA),
+ LocationData("Templar's Return", "Templar's Return: Citadel: Power Structure", SC2LOTV_LOC_ID_OFFSET + 2003, LocationType.VANILLA),
+ LocationData("Templar's Return", "Templar's Return: Temple Grounds: Gather Army", SC2LOTV_LOC_ID_OFFSET + 2004, LocationType.VANILLA,
+ lambda state: logic.templars_return_requirement(state)),
+ LocationData("Templar's Return", "Templar's Return: Temple Grounds: Power Structure", SC2LOTV_LOC_ID_OFFSET + 2005, LocationType.VANILLA,
+ lambda state: logic.templars_return_requirement(state)),
+ LocationData("Templar's Return", "Templar's Return: Caverns: Purifier", SC2LOTV_LOC_ID_OFFSET + 2006, LocationType.EXTRA,
+ lambda state: logic.templars_return_requirement(state)),
+ LocationData("Templar's Return", "Templar's Return: Caverns: Dark Templar", SC2LOTV_LOC_ID_OFFSET + 2007, LocationType.EXTRA,
+ lambda state: logic.templars_return_requirement(state)),
+ LocationData("The Host", "The Host: Victory", SC2LOTV_LOC_ID_OFFSET + 2100, LocationType.VICTORY,
+ lambda state: logic.the_host_requirement(state)),
+ LocationData("The Host", "The Host: Southeast Void Shard", SC2LOTV_LOC_ID_OFFSET + 2101, LocationType.VICTORY,
+ lambda state: logic.the_host_requirement(state)),
+ LocationData("The Host", "The Host: South Void Shard", SC2LOTV_LOC_ID_OFFSET + 2102, LocationType.EXTRA,
+ lambda state: logic.the_host_requirement(state)),
+ LocationData("The Host", "The Host: Southwest Void Shard", SC2LOTV_LOC_ID_OFFSET + 2103, LocationType.EXTRA,
+ lambda state: logic.the_host_requirement(state)),
+ LocationData("The Host", "The Host: North Void Shard", SC2LOTV_LOC_ID_OFFSET + 2104, LocationType.EXTRA,
+ lambda state: logic.the_host_requirement(state)),
+ LocationData("The Host", "The Host: Northwest Void Shard", SC2LOTV_LOC_ID_OFFSET + 2105, LocationType.EXTRA,
+ lambda state: logic.the_host_requirement(state)),
+ LocationData("The Host", "The Host: Nerazim Warp in Zone", SC2LOTV_LOC_ID_OFFSET + 2106, LocationType.VANILLA,
+ lambda state: logic.the_host_requirement(state)),
+ LocationData("The Host", "The Host: Tal'darim Warp in Zone", SC2LOTV_LOC_ID_OFFSET + 2107, LocationType.VANILLA,
+ lambda state: logic.the_host_requirement(state)),
+ LocationData("The Host", "The Host: Purifier Warp in Zone", SC2LOTV_LOC_ID_OFFSET + 2108, LocationType.VANILLA,
+ lambda state: logic.the_host_requirement(state)),
+ LocationData("Salvation", "Salvation: Victory", SC2LOTV_LOC_ID_OFFSET + 2200, LocationType.VICTORY,
+ lambda state: logic.salvation_requirement(state)),
+ LocationData("Salvation", "Salvation: Fabrication Matrix", SC2LOTV_LOC_ID_OFFSET + 2201, LocationType.EXTRA,
+ lambda state: logic.salvation_requirement(state)),
+ LocationData("Salvation", "Salvation: Assault Cluster", SC2LOTV_LOC_ID_OFFSET + 2202, LocationType.EXTRA,
+ lambda state: logic.salvation_requirement(state)),
+ LocationData("Salvation", "Salvation: Hull Breach", SC2LOTV_LOC_ID_OFFSET + 2203, LocationType.EXTRA,
+ lambda state: logic.salvation_requirement(state)),
+ LocationData("Salvation", "Salvation: Core Critical", SC2LOTV_LOC_ID_OFFSET + 2204, LocationType.EXTRA,
+ lambda state: logic.salvation_requirement(state)),
+
+ # Epilogue
+ LocationData("Into the Void", "Into the Void: Victory", SC2LOTV_LOC_ID_OFFSET + 2300, LocationType.VICTORY,
+ lambda state: logic.into_the_void_requirement(state)),
+ LocationData("Into the Void", "Into the Void: Corruption Source", SC2LOTV_LOC_ID_OFFSET + 2301, LocationType.EXTRA),
+ LocationData("Into the Void", "Into the Void: Southwest Forward Position", SC2LOTV_LOC_ID_OFFSET + 2302, LocationType.VANILLA,
+ lambda state: logic.into_the_void_requirement(state)),
+ LocationData("Into the Void", "Into the Void: Northwest Forward Position", SC2LOTV_LOC_ID_OFFSET + 2303, LocationType.VANILLA,
+ lambda state: logic.into_the_void_requirement(state)),
+ LocationData("Into the Void", "Into the Void: Southeast Forward Position", SC2LOTV_LOC_ID_OFFSET + 2304, LocationType.VANILLA,
+ lambda state: logic.into_the_void_requirement(state)),
+ LocationData("Into the Void", "Into the Void: Northeast Forward Position", SC2LOTV_LOC_ID_OFFSET + 2305, LocationType.VANILLA),
+ LocationData("The Essence of Eternity", "The Essence of Eternity: Victory", SC2LOTV_LOC_ID_OFFSET + 2400, LocationType.VICTORY,
+ lambda state: logic.essence_of_eternity_requirement(state)),
+ LocationData("The Essence of Eternity", "The Essence of Eternity: Void Trashers", SC2LOTV_LOC_ID_OFFSET + 2401, LocationType.EXTRA),
+ LocationData("Amon's Fall", "Amon's Fall: Victory", SC2LOTV_LOC_ID_OFFSET + 2500, LocationType.VICTORY,
+ lambda state: logic.amons_fall_requirement(state)),
+
+ # Nova Covert Ops
+ LocationData("The Escape", "The Escape: Victory", SC2NCO_LOC_ID_OFFSET + 100, LocationType.VICTORY,
+ lambda state: logic.the_escape_requirement(state)),
+ LocationData("The Escape", "The Escape: Rifle", SC2NCO_LOC_ID_OFFSET + 101, LocationType.VANILLA,
+ lambda state: logic.the_escape_first_stage_requirement(state)),
+ LocationData("The Escape", "The Escape: Grenades", SC2NCO_LOC_ID_OFFSET + 102, LocationType.VANILLA,
+ lambda state: logic.the_escape_first_stage_requirement(state)),
+ LocationData("The Escape", "The Escape: Agent Delta", SC2NCO_LOC_ID_OFFSET + 103, LocationType.VANILLA,
+ lambda state: logic.the_escape_requirement(state)),
+ LocationData("The Escape", "The Escape: Agent Pierce", SC2NCO_LOC_ID_OFFSET + 104, LocationType.VANILLA,
+ lambda state: logic.the_escape_requirement(state)),
+ LocationData("The Escape", "The Escape: Agent Stone", SC2NCO_LOC_ID_OFFSET + 105, LocationType.VANILLA,
+ lambda state: logic.the_escape_requirement(state)),
+ LocationData("Sudden Strike", "Sudden Strike: Victory", SC2NCO_LOC_ID_OFFSET + 200, LocationType.VICTORY,
+ lambda state: logic.sudden_strike_can_reach_objectives(state)),
+ LocationData("Sudden Strike", "Sudden Strike: Research Center", SC2NCO_LOC_ID_OFFSET + 201, LocationType.VANILLA,
+ lambda state: logic.sudden_strike_can_reach_objectives(state)),
+ LocationData("Sudden Strike", "Sudden Strike: Weaponry Labs", SC2NCO_LOC_ID_OFFSET + 202, LocationType.VANILLA,
+ lambda state: logic.sudden_strike_requirement(state)),
+ LocationData("Sudden Strike", "Sudden Strike: Brutalisk", SC2NCO_LOC_ID_OFFSET + 203, LocationType.EXTRA,
+ lambda state: logic.sudden_strike_requirement(state)),
+ LocationData("Enemy Intelligence", "Enemy Intelligence: Victory", SC2NCO_LOC_ID_OFFSET + 300, LocationType.VICTORY,
+ lambda state: logic.enemy_intelligence_third_stage_requirement(state)),
+ LocationData("Enemy Intelligence", "Enemy Intelligence: West Garrison", SC2NCO_LOC_ID_OFFSET + 301, LocationType.EXTRA,
+ lambda state: logic.enemy_intelligence_first_stage_requirement(state)),
+ LocationData("Enemy Intelligence", "Enemy Intelligence: Close Garrison", SC2NCO_LOC_ID_OFFSET + 302, LocationType.EXTRA,
+ lambda state: logic.enemy_intelligence_first_stage_requirement(state)),
+ LocationData("Enemy Intelligence", "Enemy Intelligence: Northeast Garrison", SC2NCO_LOC_ID_OFFSET + 303, LocationType.EXTRA,
+ lambda state: logic.enemy_intelligence_first_stage_requirement(state)),
+ LocationData("Enemy Intelligence", "Enemy Intelligence: Southeast Garrison", SC2NCO_LOC_ID_OFFSET + 304, LocationType.EXTRA,
+ lambda state: logic.enemy_intelligence_first_stage_requirement(state)
+ and logic.enemy_intelligence_cliff_garrison(state)),
+ LocationData("Enemy Intelligence", "Enemy Intelligence: South Garrison", SC2NCO_LOC_ID_OFFSET + 305, LocationType.EXTRA,
+ lambda state: logic.enemy_intelligence_first_stage_requirement(state)),
+ LocationData("Enemy Intelligence", "Enemy Intelligence: All Garrisons", SC2NCO_LOC_ID_OFFSET + 306, LocationType.VANILLA,
+ lambda state: logic.enemy_intelligence_first_stage_requirement(state)
+ and logic.enemy_intelligence_cliff_garrison(state)),
+ LocationData("Enemy Intelligence", "Enemy Intelligence: Forces Rescued", SC2NCO_LOC_ID_OFFSET + 307, LocationType.VANILLA,
+ lambda state: logic.enemy_intelligence_first_stage_requirement(state)),
+ LocationData("Enemy Intelligence", "Enemy Intelligence: Communications Hub", SC2NCO_LOC_ID_OFFSET + 308, LocationType.VANILLA,
+ lambda state: logic.enemy_intelligence_second_stage_requirement(state)),
+ LocationData("Trouble In Paradise", "Trouble In Paradise: Victory", SC2NCO_LOC_ID_OFFSET + 400, LocationType.VICTORY,
+ lambda state: logic.trouble_in_paradise_requirement(state)),
+ LocationData("Trouble In Paradise", "Trouble In Paradise: North Base: West Hatchery", SC2NCO_LOC_ID_OFFSET + 401, LocationType.VANILLA,
+ lambda state: logic.trouble_in_paradise_requirement(state)),
+ LocationData("Trouble In Paradise", "Trouble In Paradise: North Base: North Hatchery", SC2NCO_LOC_ID_OFFSET + 402, LocationType.VANILLA,
+ lambda state: logic.trouble_in_paradise_requirement(state)),
+ LocationData("Trouble In Paradise", "Trouble In Paradise: North Base: East Hatchery", SC2NCO_LOC_ID_OFFSET + 403, LocationType.VANILLA),
+ LocationData("Trouble In Paradise", "Trouble In Paradise: South Base: Northwest Hatchery", SC2NCO_LOC_ID_OFFSET + 404, LocationType.VANILLA,
+ lambda state: logic.trouble_in_paradise_requirement(state)),
+ LocationData("Trouble In Paradise", "Trouble In Paradise: South Base: Southwest Hatchery", SC2NCO_LOC_ID_OFFSET + 405, LocationType.VANILLA,
+ lambda state: logic.trouble_in_paradise_requirement(state)),
+ LocationData("Trouble In Paradise", "Trouble In Paradise: South Base: East Hatchery", SC2NCO_LOC_ID_OFFSET + 406, LocationType.VANILLA),
+ LocationData("Trouble In Paradise", "Trouble In Paradise: North Shield Projector", SC2NCO_LOC_ID_OFFSET + 407, LocationType.EXTRA,
+ lambda state: logic.trouble_in_paradise_requirement(state)),
+ LocationData("Trouble In Paradise", "Trouble In Paradise: East Shield Projector", SC2NCO_LOC_ID_OFFSET + 408, LocationType.EXTRA,
+ lambda state: logic.trouble_in_paradise_requirement(state)),
+ LocationData("Trouble In Paradise", "Trouble In Paradise: South Shield Projector", SC2NCO_LOC_ID_OFFSET + 409, LocationType.EXTRA,
+ lambda state: logic.trouble_in_paradise_requirement(state)),
+ LocationData("Trouble In Paradise", "Trouble In Paradise: West Shield Projector", SC2NCO_LOC_ID_OFFSET + 410, LocationType.EXTRA,
+ lambda state: logic.trouble_in_paradise_requirement(state)),
+ LocationData("Trouble In Paradise", "Trouble In Paradise: Fleet Beacon", SC2NCO_LOC_ID_OFFSET + 411, LocationType.VANILLA,
+ lambda state: logic.trouble_in_paradise_requirement(state)),
+ LocationData("Night Terrors", "Night Terrors: Victory", SC2NCO_LOC_ID_OFFSET + 500, LocationType.VICTORY,
+ lambda state: logic.night_terrors_requirement(state)),
+ LocationData("Night Terrors", "Night Terrors: 1 Terrazine Node Collected", SC2NCO_LOC_ID_OFFSET + 501, LocationType.EXTRA,
+ lambda state: logic.night_terrors_requirement(state)),
+ LocationData("Night Terrors", "Night Terrors: 2 Terrazine Nodes Collected", SC2NCO_LOC_ID_OFFSET + 502, LocationType.EXTRA,
+ lambda state: logic.night_terrors_requirement(state)),
+ LocationData("Night Terrors", "Night Terrors: 3 Terrazine Nodes Collected", SC2NCO_LOC_ID_OFFSET + 503, LocationType.EXTRA,
+ lambda state: logic.night_terrors_requirement(state)),
+ LocationData("Night Terrors", "Night Terrors: 4 Terrazine Nodes Collected", SC2NCO_LOC_ID_OFFSET + 504, LocationType.EXTRA,
+ lambda state: logic.night_terrors_requirement(state)),
+ LocationData("Night Terrors", "Night Terrors: 5 Terrazine Nodes Collected", SC2NCO_LOC_ID_OFFSET + 505, LocationType.EXTRA,
+ lambda state: logic.night_terrors_requirement(state)),
+ LocationData("Night Terrors", "Night Terrors: HERC Outpost", SC2NCO_LOC_ID_OFFSET + 506, LocationType.VANILLA,
+ lambda state: logic.night_terrors_requirement(state)),
+ LocationData("Night Terrors", "Night Terrors: Umojan Mine", SC2NCO_LOC_ID_OFFSET + 507, LocationType.EXTRA,
+ lambda state: logic.night_terrors_requirement(state)),
+ LocationData("Night Terrors", "Night Terrors: Blightbringer", SC2NCO_LOC_ID_OFFSET + 508, LocationType.VANILLA,
+ lambda state: logic.night_terrors_requirement(state)
+ and logic.nova_ranged_weapon(state)
+ and state.has_any(
+ {ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_PULSE_GRENADES, ItemNames.NOVA_STIM_INFUSION,
+ ItemNames.NOVA_HOLO_DECOY}, player)),
+ LocationData("Night Terrors", "Night Terrors: Science Facility", SC2NCO_LOC_ID_OFFSET + 509, LocationType.EXTRA,
+ lambda state: logic.night_terrors_requirement(state)),
+ LocationData("Night Terrors", "Night Terrors: Eradicators", SC2NCO_LOC_ID_OFFSET + 510, LocationType.VANILLA,
+ lambda state: logic.night_terrors_requirement(state)
+ and logic.nova_any_weapon(state)),
+ LocationData("Flashpoint", "Flashpoint: Victory", SC2NCO_LOC_ID_OFFSET + 600, LocationType.VICTORY,
+ lambda state: logic.flashpoint_far_requirement(state)),
+ LocationData("Flashpoint", "Flashpoint: Close North Evidence Coordinates", SC2NCO_LOC_ID_OFFSET + 601, LocationType.EXTRA,
+ lambda state: state.has_any(
+ {ItemNames.LIBERATOR_RAID_ARTILLERY, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, player)
+ or logic.terran_common_unit(state)),
+ LocationData("Flashpoint", "Flashpoint: Close East Evidence Coordinates", SC2NCO_LOC_ID_OFFSET + 602, LocationType.EXTRA,
+ lambda state: state.has_any(
+ {ItemNames.LIBERATOR_RAID_ARTILLERY, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, player)
+ or logic.terran_common_unit(state)),
+ LocationData("Flashpoint", "Flashpoint: Far North Evidence Coordinates", SC2NCO_LOC_ID_OFFSET + 603, LocationType.EXTRA,
+ lambda state: logic.flashpoint_far_requirement(state)),
+ LocationData("Flashpoint", "Flashpoint: Far East Evidence Coordinates", SC2NCO_LOC_ID_OFFSET + 604, LocationType.EXTRA,
+ lambda state: logic.flashpoint_far_requirement(state)),
+ LocationData("Flashpoint", "Flashpoint: Experimental Weapon", SC2NCO_LOC_ID_OFFSET + 605, LocationType.VANILLA,
+ lambda state: logic.flashpoint_far_requirement(state)),
+ LocationData("Flashpoint", "Flashpoint: Northwest Subway Entrance", SC2NCO_LOC_ID_OFFSET + 606, LocationType.VANILLA,
+ lambda state: state.has_any(
+ {ItemNames.LIBERATOR_RAID_ARTILLERY, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, player)
+ and logic.terran_common_unit(state)
+ or logic.flashpoint_far_requirement(state)),
+ LocationData("Flashpoint", "Flashpoint: Southeast Subway Entrance", SC2NCO_LOC_ID_OFFSET + 607, LocationType.VANILLA,
+ lambda state: state.has_any(
+ {ItemNames.LIBERATOR_RAID_ARTILLERY, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, player)
+ and logic.terran_common_unit(state)
+ or logic.flashpoint_far_requirement(state)),
+ LocationData("Flashpoint", "Flashpoint: Northeast Subway Entrance", SC2NCO_LOC_ID_OFFSET + 608, LocationType.VANILLA,
+ lambda state: logic.flashpoint_far_requirement(state)),
+ LocationData("Flashpoint", "Flashpoint: Expansion Hatchery", SC2NCO_LOC_ID_OFFSET + 609, LocationType.EXTRA,
+ lambda state: state.has(ItemNames.LIBERATOR_RAID_ARTILLERY, player) and logic.terran_common_unit(state)
+ or logic.flashpoint_far_requirement(state)),
+ LocationData("Flashpoint", "Flashpoint: Baneling Spawns", SC2NCO_LOC_ID_OFFSET + 610, LocationType.EXTRA,
+ lambda state: logic.flashpoint_far_requirement(state)),
+ LocationData("Flashpoint", "Flashpoint: Mutalisk Spawns", SC2NCO_LOC_ID_OFFSET + 611, LocationType.EXTRA,
+ lambda state: logic.flashpoint_far_requirement(state)),
+ LocationData("Flashpoint", "Flashpoint: Nydus Worm Spawns", SC2NCO_LOC_ID_OFFSET + 612, LocationType.EXTRA,
+ lambda state: logic.flashpoint_far_requirement(state)),
+ LocationData("Flashpoint", "Flashpoint: Lurker Spawns", SC2NCO_LOC_ID_OFFSET + 613, LocationType.EXTRA,
+ lambda state: logic.flashpoint_far_requirement(state)),
+ LocationData("Flashpoint", "Flashpoint: Brood Lord Spawns", SC2NCO_LOC_ID_OFFSET + 614, LocationType.EXTRA,
+ lambda state: logic.flashpoint_far_requirement(state)),
+ LocationData("Flashpoint", "Flashpoint: Ultralisk Spawns", SC2NCO_LOC_ID_OFFSET + 615, LocationType.EXTRA,
+ lambda state: logic.flashpoint_far_requirement(state)),
+ LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Victory", SC2NCO_LOC_ID_OFFSET + 700, LocationType.VICTORY,
+ lambda state: logic.enemy_shadow_victory(state)),
+ LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Sewers: Domination Visor", SC2NCO_LOC_ID_OFFSET + 701, LocationType.VANILLA,
+ lambda state: logic.enemy_shadow_domination(state)),
+ LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Sewers: Resupply Crate", SC2NCO_LOC_ID_OFFSET + 702, LocationType.EXTRA,
+ lambda state: logic.enemy_shadow_first_stage(state)),
+ LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Sewers: Facility Access", SC2NCO_LOC_ID_OFFSET + 703, LocationType.VANILLA,
+ lambda state: logic.enemy_shadow_first_stage(state)),
+ LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Northwest Door Lock", SC2NCO_LOC_ID_OFFSET + 704, LocationType.VANILLA,
+ lambda state: logic.enemy_shadow_door_controls(state)),
+ LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Southeast Door Lock", SC2NCO_LOC_ID_OFFSET + 705, LocationType.VANILLA,
+ lambda state: logic.enemy_shadow_door_controls(state)),
+ LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Blazefire Gunblade", SC2NCO_LOC_ID_OFFSET + 706, LocationType.VANILLA,
+ lambda state: logic.enemy_shadow_second_stage(state)
+ and (story_tech_granted
+ or state.has(ItemNames.NOVA_BLINK, player)
+ or (adv_tactics and state.has_all({ItemNames.NOVA_DOMINATION, ItemNames.NOVA_HOLO_DECOY, ItemNames.NOVA_JUMP_SUIT_MODULE}, player))
+ )
+ ),
+ LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Blink Suit", SC2NCO_LOC_ID_OFFSET + 707, LocationType.VANILLA,
+ lambda state: logic.enemy_shadow_second_stage(state)),
+ LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Advanced Weaponry", SC2NCO_LOC_ID_OFFSET + 708, LocationType.VANILLA,
+ lambda state: logic.enemy_shadow_second_stage(state)),
+ LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: Entrance Resupply Crate", SC2NCO_LOC_ID_OFFSET + 709, LocationType.EXTRA,
+ lambda state: logic.enemy_shadow_first_stage(state)),
+ LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: West Resupply Crate", SC2NCO_LOC_ID_OFFSET + 710, LocationType.EXTRA,
+ lambda state: logic.enemy_shadow_second_stage(state)),
+ LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: North Resupply Crate", SC2NCO_LOC_ID_OFFSET + 711, LocationType.EXTRA,
+ lambda state: logic.enemy_shadow_second_stage(state)),
+ LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: East Resupply Crate", SC2NCO_LOC_ID_OFFSET + 712, LocationType.EXTRA,
+ lambda state: logic.enemy_shadow_second_stage(state)),
+ LocationData("In the Enemy's Shadow", "In the Enemy's Shadow: Facility: South Resupply Crate", SC2NCO_LOC_ID_OFFSET + 713, LocationType.EXTRA,
+ lambda state: logic.enemy_shadow_second_stage(state)),
+ LocationData("Dark Skies", "Dark Skies: Victory", SC2NCO_LOC_ID_OFFSET + 800, LocationType.VICTORY,
+ lambda state: logic.dark_skies_requirement(state)),
+ LocationData("Dark Skies", "Dark Skies: First Squadron of Dominion Fleet", SC2NCO_LOC_ID_OFFSET + 801, LocationType.EXTRA,
+ lambda state: logic.dark_skies_requirement(state)),
+ LocationData("Dark Skies", "Dark Skies: Remainder of Dominion Fleet", SC2NCO_LOC_ID_OFFSET + 802, LocationType.EXTRA,
+ lambda state: logic.dark_skies_requirement(state)),
+ LocationData("Dark Skies", "Dark Skies: Ji'nara", SC2NCO_LOC_ID_OFFSET + 803, LocationType.EXTRA,
+ lambda state: logic.dark_skies_requirement(state)),
+ LocationData("Dark Skies", "Dark Skies: Science Facility", SC2NCO_LOC_ID_OFFSET + 804, LocationType.VANILLA,
+ lambda state: logic.dark_skies_requirement(state)),
+ LocationData("End Game", "End Game: Victory", SC2NCO_LOC_ID_OFFSET + 900, LocationType.VICTORY,
+ lambda state: logic.end_game_requirement(state) and logic.nova_any_weapon(state)),
+ LocationData("End Game", "End Game: Xanthos", SC2NCO_LOC_ID_OFFSET + 901, LocationType.VANILLA,
+ lambda state: logic.end_game_requirement(state)),
+ ]
+
+ beat_events = []
+ # Filtering out excluded locations
+ if world is not None:
+ excluded_location_types = get_location_types(world, LocationInclusion.option_disabled)
+ plando_locations = get_plando_locations(world)
+ exclude_locations = get_option_value(world, "exclude_locations")
+ location_table = [location for location in location_table
+ if (location.type is LocationType.VICTORY or location.name not in exclude_locations)
+ and location.type not in excluded_location_types
+ or location.name in plando_locations]
+ for i, location_data in enumerate(location_table):
+ # Removing all item-based logic on No Logic
+ if logic_level == RequiredTactics.option_no_logic:
+ location_data = location_data._replace(rule=Location.access_rule)
+ location_table[i] = location_data
+ # Generating Beat event locations
+ if location_data.name.endswith((": Victory", ": Defeat")):
+ beat_events.append(
+ location_data._replace(name="Beat " + location_data.name.rsplit(": ", 1)[0], code=None)
+ )
+ return tuple(location_table + beat_events)
+
+lookup_location_id_to_type = {loc.code: loc.type for loc in get_locations(None) if loc.code is not None}
\ No newline at end of file
diff --git a/worlds/sc2/MissionTables.py b/worlds/sc2/MissionTables.py
new file mode 100644
index 000000000000..4dece46411bf
--- /dev/null
+++ b/worlds/sc2/MissionTables.py
@@ -0,0 +1,736 @@
+from typing import NamedTuple, Dict, List, Set, Union, Literal, Iterable, Callable
+from enum import IntEnum, Enum
+
+
+class SC2Race(IntEnum):
+ ANY = 0
+ TERRAN = 1
+ ZERG = 2
+ PROTOSS = 3
+
+
+class MissionPools(IntEnum):
+ STARTER = 0
+ EASY = 1
+ MEDIUM = 2
+ HARD = 3
+ VERY_HARD = 4
+ FINAL = 5
+
+
+class SC2CampaignGoalPriority(IntEnum):
+ """
+ Campaign's priority to goal election
+ """
+ NONE = 0
+ MINI_CAMPAIGN = 1 # A goal shouldn't be in a mini-campaign if there's at least one 'big' campaign
+ HARD = 2 # A campaign ending with a hard mission
+ VERY_HARD = 3 # A campaign ending with a very hard mission
+ EPILOGUE = 4 # Epilogue shall be always preferred as the goal if present
+
+
+class SC2Campaign(Enum):
+
+ def __new__(cls, *args, **kwargs):
+ value = len(cls.__members__) + 1
+ obj = object.__new__(cls)
+ obj._value_ = value
+ return obj
+
+ def __init__(self, campaign_id: int, name: str, goal_priority: SC2CampaignGoalPriority, race: SC2Race):
+ self.id = campaign_id
+ self.campaign_name = name
+ self.goal_priority = goal_priority
+ self.race = race
+
+ GLOBAL = 0, "Global", SC2CampaignGoalPriority.NONE, SC2Race.ANY
+ WOL = 1, "Wings of Liberty", SC2CampaignGoalPriority.VERY_HARD, SC2Race.TERRAN
+ PROPHECY = 2, "Prophecy", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS
+ HOTS = 3, "Heart of the Swarm", SC2CampaignGoalPriority.HARD, SC2Race.ZERG
+ PROLOGUE = 4, "Whispers of Oblivion (Legacy of the Void: Prologue)", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS
+ LOTV = 5, "Legacy of the Void", SC2CampaignGoalPriority.VERY_HARD, SC2Race.PROTOSS
+ EPILOGUE = 6, "Into the Void (Legacy of the Void: Epilogue)", SC2CampaignGoalPriority.EPILOGUE, SC2Race.ANY
+ NCO = 7, "Nova Covert Ops", SC2CampaignGoalPriority.HARD, SC2Race.TERRAN
+
+
+class SC2Mission(Enum):
+
+ def __new__(cls, *args, **kwargs):
+ value = len(cls.__members__) + 1
+ obj = object.__new__(cls)
+ obj._value_ = value
+ return obj
+
+ def __init__(self, mission_id: int, name: str, campaign: SC2Campaign, area: str, race: SC2Race, pool: MissionPools, map_file: str, build: bool = True):
+ self.id = mission_id
+ self.mission_name = name
+ self.campaign = campaign
+ self.area = area
+ self.race = race
+ self.pool = pool
+ self.map_file = map_file
+ self.build = build
+
+ # Wings of Liberty
+ LIBERATION_DAY = 1, "Liberation Day", SC2Campaign.WOL, "Mar Sara", SC2Race.ANY, MissionPools.STARTER, "ap_liberation_day", False
+ THE_OUTLAWS = 2, "The Outlaws", SC2Campaign.WOL, "Mar Sara", SC2Race.TERRAN, MissionPools.EASY, "ap_the_outlaws"
+ ZERO_HOUR = 3, "Zero Hour", SC2Campaign.WOL, "Mar Sara", SC2Race.TERRAN, MissionPools.EASY, "ap_zero_hour"
+ EVACUATION = 4, "Evacuation", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.EASY, "ap_evacuation"
+ OUTBREAK = 5, "Outbreak", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.EASY, "ap_outbreak"
+ SAFE_HAVEN = 6, "Safe Haven", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_safe_haven"
+ HAVENS_FALL = 7, "Haven's Fall", SC2Campaign.WOL, "Colonist", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_havens_fall"
+ SMASH_AND_GRAB = 8, "Smash and Grab", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.EASY, "ap_smash_and_grab"
+ THE_DIG = 9, "The Dig", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_dig"
+ THE_MOEBIUS_FACTOR = 10, "The Moebius Factor", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_moebius_factor"
+ SUPERNOVA = 11, "Supernova", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.HARD, "ap_supernova"
+ MAW_OF_THE_VOID = 12, "Maw of the Void", SC2Campaign.WOL, "Artifact", SC2Race.TERRAN, MissionPools.HARD, "ap_maw_of_the_void"
+ DEVILS_PLAYGROUND = 13, "Devil's Playground", SC2Campaign.WOL, "Covert", SC2Race.TERRAN, MissionPools.EASY, "ap_devils_playground"
+ WELCOME_TO_THE_JUNGLE = 14, "Welcome to the Jungle", SC2Campaign.WOL, "Covert", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_welcome_to_the_jungle"
+ BREAKOUT = 15, "Breakout", SC2Campaign.WOL, "Covert", SC2Race.ANY, MissionPools.STARTER, "ap_breakout", False
+ GHOST_OF_A_CHANCE = 16, "Ghost of a Chance", SC2Campaign.WOL, "Covert", SC2Race.ANY, MissionPools.STARTER, "ap_ghost_of_a_chance", False
+ THE_GREAT_TRAIN_ROBBERY = 17, "The Great Train Robbery", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_the_great_train_robbery"
+ CUTTHROAT = 18, "Cutthroat", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_cutthroat"
+ ENGINE_OF_DESTRUCTION = 19, "Engine of Destruction", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.HARD, "ap_engine_of_destruction"
+ MEDIA_BLITZ = 20, "Media Blitz", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_media_blitz"
+ PIERCING_OF_THE_SHROUD = 21, "Piercing the Shroud", SC2Campaign.WOL, "Rebellion", SC2Race.TERRAN, MissionPools.STARTER, "ap_piercing_the_shroud", False
+ GATES_OF_HELL = 26, "Gates of Hell", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.HARD, "ap_gates_of_hell"
+ BELLY_OF_THE_BEAST = 27, "Belly of the Beast", SC2Campaign.WOL, "Char", SC2Race.ANY, MissionPools.STARTER, "ap_belly_of_the_beast", False
+ SHATTER_THE_SKY = 28, "Shatter the Sky", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.HARD, "ap_shatter_the_sky"
+ ALL_IN = 29, "All-In", SC2Campaign.WOL, "Char", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_all_in"
+
+ # Prophecy
+ WHISPERS_OF_DOOM = 22, "Whispers of Doom", SC2Campaign.PROPHECY, "_1", SC2Race.ANY, MissionPools.STARTER, "ap_whispers_of_doom", False
+ A_SINISTER_TURN = 23, "A Sinister Turn", SC2Campaign.PROPHECY, "_2", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_a_sinister_turn"
+ ECHOES_OF_THE_FUTURE = 24, "Echoes of the Future", SC2Campaign.PROPHECY, "_3", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_echoes_of_the_future"
+ IN_UTTER_DARKNESS = 25, "In Utter Darkness", SC2Campaign.PROPHECY, "_4", SC2Race.PROTOSS, MissionPools.HARD, "ap_in_utter_darkness"
+
+ # Heart of the Swarm
+ LAB_RAT = 30, "Lab Rat", SC2Campaign.HOTS, "Umoja", SC2Race.ZERG, MissionPools.STARTER, "ap_lab_rat"
+ BACK_IN_THE_SADDLE = 31, "Back in the Saddle", SC2Campaign.HOTS, "Umoja", SC2Race.ANY, MissionPools.STARTER, "ap_back_in_the_saddle", False
+ RENDEZVOUS = 32, "Rendezvous", SC2Campaign.HOTS, "Umoja", SC2Race.ZERG, MissionPools.EASY, "ap_rendezvous"
+ HARVEST_OF_SCREAMS = 33, "Harvest of Screams", SC2Campaign.HOTS, "Kaldir", SC2Race.ZERG, MissionPools.EASY, "ap_harvest_of_screams"
+ SHOOT_THE_MESSENGER = 34, "Shoot the Messenger", SC2Campaign.HOTS, "Kaldir", SC2Race.ZERG, MissionPools.EASY, "ap_shoot_the_messenger"
+ ENEMY_WITHIN = 35, "Enemy Within", SC2Campaign.HOTS, "Kaldir", SC2Race.ANY, MissionPools.EASY, "ap_enemy_within", False
+ DOMINATION = 36, "Domination", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.EASY, "ap_domination"
+ FIRE_IN_THE_SKY = 37, "Fire in the Sky", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.MEDIUM, "ap_fire_in_the_sky"
+ OLD_SOLDIERS = 38, "Old Soldiers", SC2Campaign.HOTS, "Char", SC2Race.ZERG, MissionPools.MEDIUM, "ap_old_soldiers"
+ WAKING_THE_ANCIENT = 39, "Waking the Ancient", SC2Campaign.HOTS, "Zerus", SC2Race.ZERG, MissionPools.MEDIUM, "ap_waking_the_ancient"
+ THE_CRUCIBLE = 40, "The Crucible", SC2Campaign.HOTS, "Zerus", SC2Race.ZERG, MissionPools.MEDIUM, "ap_the_crucible"
+ SUPREME = 41, "Supreme", SC2Campaign.HOTS, "Zerus", SC2Race.ANY, MissionPools.MEDIUM, "ap_supreme", False
+ INFESTED = 42, "Infested", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.MEDIUM, "ap_infested"
+ HAND_OF_DARKNESS = 43, "Hand of Darkness", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.HARD, "ap_hand_of_darkness"
+ PHANTOMS_OF_THE_VOID = 44, "Phantoms of the Void", SC2Campaign.HOTS, "Skygeirr Station", SC2Race.ZERG, MissionPools.HARD, "ap_phantoms_of_the_void"
+ WITH_FRIENDS_LIKE_THESE = 45, "With Friends Like These", SC2Campaign.HOTS, "Dominion Space", SC2Race.ANY, MissionPools.STARTER, "ap_with_friends_like_these", False
+ CONVICTION = 46, "Conviction", SC2Campaign.HOTS, "Dominion Space", SC2Race.ANY, MissionPools.MEDIUM, "ap_conviction", False
+ PLANETFALL = 47, "Planetfall", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_planetfall"
+ DEATH_FROM_ABOVE = 48, "Death From Above", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_death_from_above"
+ THE_RECKONING = 49, "The Reckoning", SC2Campaign.HOTS, "Korhal", SC2Race.ZERG, MissionPools.HARD, "ap_the_reckoning"
+
+ # Prologue
+ DARK_WHISPERS = 50, "Dark Whispers", SC2Campaign.PROLOGUE, "_1", SC2Race.PROTOSS, MissionPools.EASY, "ap_dark_whispers"
+ GHOSTS_IN_THE_FOG = 51, "Ghosts in the Fog", SC2Campaign.PROLOGUE, "_2", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_ghosts_in_the_fog"
+ EVIL_AWOKEN = 52, "Evil Awoken", SC2Campaign.PROLOGUE, "_3", SC2Race.PROTOSS, MissionPools.STARTER, "ap_evil_awoken", False
+
+ # LotV
+ FOR_AIUR = 53, "For Aiur!", SC2Campaign.LOTV, "Aiur", SC2Race.ANY, MissionPools.STARTER, "ap_for_aiur", False
+ THE_GROWING_SHADOW = 54, "The Growing Shadow", SC2Campaign.LOTV, "Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_growing_shadow"
+ THE_SPEAR_OF_ADUN = 55, "The Spear of Adun", SC2Campaign.LOTV, "Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_the_spear_of_adun"
+ SKY_SHIELD = 56, "Sky Shield", SC2Campaign.LOTV, "Korhal", SC2Race.PROTOSS, MissionPools.EASY, "ap_sky_shield"
+ BROTHERS_IN_ARMS = 57, "Brothers in Arms", SC2Campaign.LOTV, "Korhal", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_brothers_in_arms"
+ AMON_S_REACH = 58, "Amon's Reach", SC2Campaign.LOTV, "Shakuras", SC2Race.PROTOSS, MissionPools.EASY, "ap_amon_s_reach"
+ LAST_STAND = 59, "Last Stand", SC2Campaign.LOTV, "Shakuras", SC2Race.PROTOSS, MissionPools.HARD, "ap_last_stand"
+ FORBIDDEN_WEAPON = 60, "Forbidden Weapon", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_forbidden_weapon"
+ TEMPLE_OF_UNIFICATION = 61, "Temple of Unification", SC2Campaign.LOTV, "Ulnar", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_temple_of_unification"
+ THE_INFINITE_CYCLE = 62, "The Infinite Cycle", SC2Campaign.LOTV, "Ulnar", SC2Race.ANY, MissionPools.HARD, "ap_the_infinite_cycle", False
+ HARBINGER_OF_OBLIVION = 63, "Harbinger of Oblivion", SC2Campaign.LOTV, "Ulnar", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_harbinger_of_oblivion"
+ UNSEALING_THE_PAST = 64, "Unsealing the Past", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.MEDIUM, "ap_unsealing_the_past"
+ PURIFICATION = 65, "Purification", SC2Campaign.LOTV, "Purifier", SC2Race.PROTOSS, MissionPools.HARD, "ap_purification"
+ STEPS_OF_THE_RITE = 66, "Steps of the Rite", SC2Campaign.LOTV, "Tal'darim", SC2Race.PROTOSS, MissionPools.HARD, "ap_steps_of_the_rite"
+ RAK_SHIR = 67, "Rak'Shir", SC2Campaign.LOTV, "Tal'darim", SC2Race.PROTOSS, MissionPools.HARD, "ap_rak_shir"
+ TEMPLAR_S_CHARGE = 68, "Templar's Charge", SC2Campaign.LOTV, "Moebius", SC2Race.PROTOSS, MissionPools.HARD, "ap_templar_s_charge"
+ TEMPLAR_S_RETURN = 69, "Templar's Return", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.EASY, "ap_templar_s_return", False
+ THE_HOST = 70, "The Host", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.HARD, "ap_the_host",
+ SALVATION = 71, "Salvation", SC2Campaign.LOTV, "Return to Aiur", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_salvation"
+
+ # Epilogue
+ INTO_THE_VOID = 72, "Into the Void", SC2Campaign.EPILOGUE, "_1", SC2Race.PROTOSS, MissionPools.VERY_HARD, "ap_into_the_void"
+ THE_ESSENCE_OF_ETERNITY = 73, "The Essence of Eternity", SC2Campaign.EPILOGUE, "_2", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_the_essence_of_eternity"
+ AMON_S_FALL = 74, "Amon's Fall", SC2Campaign.EPILOGUE, "_3", SC2Race.ZERG, MissionPools.VERY_HARD, "ap_amon_s_fall"
+
+ # Nova Covert Ops
+ THE_ESCAPE = 75, "The Escape", SC2Campaign.NCO, "_1", SC2Race.ANY, MissionPools.MEDIUM, "ap_the_escape", False
+ SUDDEN_STRIKE = 76, "Sudden Strike", SC2Campaign.NCO, "_1", SC2Race.TERRAN, MissionPools.EASY, "ap_sudden_strike"
+ ENEMY_INTELLIGENCE = 77, "Enemy Intelligence", SC2Campaign.NCO, "_1", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_enemy_intelligence"
+ TROUBLE_IN_PARADISE = 78, "Trouble In Paradise", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_trouble_in_paradise"
+ NIGHT_TERRORS = 79, "Night Terrors", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_night_terrors"
+ FLASHPOINT = 80, "Flashpoint", SC2Campaign.NCO, "_2", SC2Race.TERRAN, MissionPools.HARD, "ap_flashpoint"
+ IN_THE_ENEMY_S_SHADOW = 81, "In the Enemy's Shadow", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.MEDIUM, "ap_in_the_enemy_s_shadow", False
+ DARK_SKIES = 82, "Dark Skies", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.HARD, "ap_dark_skies"
+ END_GAME = 83, "End Game", SC2Campaign.NCO, "_3", SC2Race.TERRAN, MissionPools.VERY_HARD, "ap_end_game"
+
+
+class MissionConnection:
+ campaign: SC2Campaign
+ connect_to: int # -1 connects to Menu
+
+ def __init__(self, connect_to, campaign = SC2Campaign.GLOBAL):
+ self.campaign = campaign
+ self.connect_to = connect_to
+
+ def _asdict(self):
+ return {
+ "campaign": self.campaign.id,
+ "connect_to": self.connect_to
+ }
+
+
+class MissionInfo(NamedTuple):
+ mission: SC2Mission
+ required_world: List[Union[MissionConnection, Dict[Literal["campaign", "connect_to"], int]]]
+ category: str
+ number: int = 0 # number of worlds need beaten
+ completion_critical: bool = False # missions needed to beat game
+ or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
+ ui_vertical_padding: int = 0
+
+
+class FillMission(NamedTuple):
+ type: MissionPools
+ connect_to: List[MissionConnection]
+ category: str
+ number: int = 0 # number of worlds need beaten
+ completion_critical: bool = False # missions needed to beat game
+ or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
+ removal_priority: int = 0 # how many missions missing from the pool required to remove this mission
+
+
+
+def vanilla_shuffle_order() -> Dict[SC2Campaign, List[FillMission]]:
+ return {
+ SC2Campaign.WOL: [
+ FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.WOL)], "Mar Sara", completion_critical=True),
+ FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.WOL)], "Mar Sara", completion_critical=True),
+ FillMission(MissionPools.EASY, [MissionConnection(1, SC2Campaign.WOL)], "Mar Sara", completion_critical=True),
+ FillMission(MissionPools.EASY, [MissionConnection(2, SC2Campaign.WOL)], "Colonist"),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.WOL)], "Colonist"),
+ FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.WOL)], "Colonist", number=7),
+ FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.WOL)], "Colonist", number=7, removal_priority=1),
+ FillMission(MissionPools.EASY, [MissionConnection(2, SC2Campaign.WOL)], "Artifact", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(7, SC2Campaign.WOL)], "Artifact", number=8, completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(8, SC2Campaign.WOL)], "Artifact", number=11, completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(9, SC2Campaign.WOL)], "Artifact", number=14, completion_critical=True, removal_priority=7),
+ FillMission(MissionPools.HARD, [MissionConnection(10, SC2Campaign.WOL)], "Artifact", completion_critical=True, removal_priority=6),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.WOL)], "Covert", number=4),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(12, SC2Campaign.WOL)], "Covert"),
+ FillMission(MissionPools.HARD, [MissionConnection(13, SC2Campaign.WOL)], "Covert", number=8, removal_priority=3),
+ FillMission(MissionPools.HARD, [MissionConnection(13, SC2Campaign.WOL)], "Covert", number=8, removal_priority=2),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.WOL)], "Rebellion", number=6),
+ FillMission(MissionPools.HARD, [MissionConnection(16, SC2Campaign.WOL)], "Rebellion"),
+ FillMission(MissionPools.HARD, [MissionConnection(17, SC2Campaign.WOL)], "Rebellion"),
+ FillMission(MissionPools.HARD, [MissionConnection(18, SC2Campaign.WOL)], "Rebellion", removal_priority=8),
+ FillMission(MissionPools.HARD, [MissionConnection(19, SC2Campaign.WOL)], "Rebellion", removal_priority=5),
+ FillMission(MissionPools.HARD, [MissionConnection(11, SC2Campaign.WOL)], "Char", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(21, SC2Campaign.WOL)], "Char", completion_critical=True, removal_priority=4),
+ FillMission(MissionPools.HARD, [MissionConnection(21, SC2Campaign.WOL)], "Char", completion_critical=True),
+ FillMission(MissionPools.FINAL, [MissionConnection(22, SC2Campaign.WOL), MissionConnection(23, SC2Campaign.WOL)], "Char", completion_critical=True, or_requirements=True)
+ ],
+ SC2Campaign.PROPHECY: [
+ FillMission(MissionPools.MEDIUM, [MissionConnection(8, SC2Campaign.WOL)], "_1"),
+ FillMission(MissionPools.HARD, [MissionConnection(0, SC2Campaign.PROPHECY)], "_2", removal_priority=2),
+ FillMission(MissionPools.HARD, [MissionConnection(1, SC2Campaign.PROPHECY)], "_3", removal_priority=1),
+ FillMission(MissionPools.FINAL, [MissionConnection(2, SC2Campaign.PROPHECY)], "_4"),
+ ],
+ SC2Campaign.HOTS: [
+ FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.HOTS)], "Umoja", completion_critical=True),
+ FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.HOTS)], "Umoja", completion_critical=True),
+ FillMission(MissionPools.EASY, [MissionConnection(1, SC2Campaign.HOTS)], "Umoja", completion_critical=True, removal_priority=1),
+ FillMission(MissionPools.EASY, [MissionConnection(2, SC2Campaign.HOTS)], "Kaldir", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.HOTS)], "Kaldir", completion_critical=True, removal_priority=2),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(4, SC2Campaign.HOTS)], "Kaldir", completion_critical=True),
+ FillMission(MissionPools.EASY, [MissionConnection(2, SC2Campaign.HOTS)], "Char", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(6, SC2Campaign.HOTS)], "Char", completion_critical=True, removal_priority=3),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(7, SC2Campaign.HOTS)], "Char", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(5, SC2Campaign.HOTS), MissionConnection(8, SC2Campaign.HOTS)], "Zerus", completion_critical=True, or_requirements=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(9, SC2Campaign.HOTS)], "Zerus", completion_critical=True, removal_priority=4),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(10, SC2Campaign.HOTS)], "Zerus", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(5, SC2Campaign.HOTS), MissionConnection(8, SC2Campaign.HOTS), MissionConnection(11, SC2Campaign.HOTS)], "Skygeirr Station", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(12, SC2Campaign.HOTS)], "Skygeirr Station", completion_critical=True, removal_priority=5),
+ FillMission(MissionPools.HARD, [MissionConnection(13, SC2Campaign.HOTS)], "Skygeirr Station", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(5, SC2Campaign.HOTS), MissionConnection(8, SC2Campaign.HOTS), MissionConnection(11, SC2Campaign.HOTS)], "Dominion Space", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(15, SC2Campaign.HOTS)], "Dominion Space", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(14, SC2Campaign.HOTS), MissionConnection(16, SC2Campaign.HOTS)], "Korhal", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(17, SC2Campaign.HOTS)], "Korhal", completion_critical=True),
+ FillMission(MissionPools.FINAL, [MissionConnection(18, SC2Campaign.HOTS)], "Korhal", completion_critical=True),
+ ],
+ SC2Campaign.PROLOGUE: [
+ FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.PROLOGUE)], "_1"),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.PROLOGUE)], "_2", removal_priority=1),
+ FillMission(MissionPools.FINAL, [MissionConnection(1, SC2Campaign.PROLOGUE)], "_3")
+ ],
+ SC2Campaign.LOTV: [
+ FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.LOTV)], "Aiur", completion_critical=True),
+ FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.LOTV)], "Aiur", completion_critical=True, removal_priority=3),
+ FillMission(MissionPools.EASY, [MissionConnection(1, SC2Campaign.LOTV)], "Aiur", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.LOTV)], "Korhal", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.LOTV)], "Korhal", completion_critical=True, removal_priority=7),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.LOTV)], "Shakuras", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.LOTV)], "Shakuras", completion_critical=True, removal_priority=6),
+ FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.LOTV), MissionConnection(6, SC2Campaign.LOTV)], "Purifier", completion_critical=True, or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.LOTV), MissionConnection(6, SC2Campaign.LOTV), MissionConnection(7, SC2Campaign.LOTV)], "Ulnar", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(8, SC2Campaign.LOTV)], "Ulnar", completion_critical=True, removal_priority=1),
+ FillMission(MissionPools.HARD, [MissionConnection(9, SC2Campaign.LOTV)], "Ulnar", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(10, SC2Campaign.LOTV)], "Purifier", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(11, SC2Campaign.LOTV)], "Purifier", completion_critical=True, removal_priority=5),
+ FillMission(MissionPools.HARD, [MissionConnection(10, SC2Campaign.LOTV)], "Tal'darim", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(13, SC2Campaign.LOTV)], "Tal'darim", completion_critical=True, removal_priority=4),
+ FillMission(MissionPools.HARD, [MissionConnection(12, SC2Campaign.LOTV), MissionConnection(14, SC2Campaign.LOTV)], "Moebius", completion_critical=True, or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(12, SC2Campaign.LOTV), MissionConnection(14, SC2Campaign.LOTV), MissionConnection(15, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(16, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True, removal_priority=2),
+ FillMission(MissionPools.FINAL, [MissionConnection(17, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True),
+ ],
+ SC2Campaign.EPILOGUE: [
+ FillMission(MissionPools.VERY_HARD, [MissionConnection(24, SC2Campaign.WOL), MissionConnection(19, SC2Campaign.HOTS), MissionConnection(18, SC2Campaign.LOTV)], "_1", completion_critical=True),
+ FillMission(MissionPools.VERY_HARD, [MissionConnection(0, SC2Campaign.EPILOGUE)], "_2", completion_critical=True, removal_priority=1),
+ FillMission(MissionPools.FINAL, [MissionConnection(1, SC2Campaign.EPILOGUE)], "_3", completion_critical=True),
+ ],
+ SC2Campaign.NCO: [
+ FillMission(MissionPools.EASY, [MissionConnection(-1, SC2Campaign.NCO)], "_1", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.NCO)], "_1", completion_critical=True, removal_priority=6),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.NCO)], "_1", completion_critical=True, removal_priority=5),
+ FillMission(MissionPools.HARD, [MissionConnection(2, SC2Campaign.NCO)], "_2", completion_critical=True, removal_priority=7),
+ FillMission(MissionPools.HARD, [MissionConnection(3, SC2Campaign.NCO)], "_2", completion_critical=True, removal_priority=4),
+ FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.NCO)], "_2", completion_critical=True, removal_priority=3),
+ FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.NCO)], "_3", completion_critical=True, removal_priority=2),
+ FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.NCO)], "_3", completion_critical=True, removal_priority=1),
+ FillMission(MissionPools.FINAL, [MissionConnection(7, SC2Campaign.NCO)], "_3", completion_critical=True),
+ ]
+ }
+
+
+def mini_campaign_order() -> Dict[SC2Campaign, List[FillMission]]:
+ return {
+ SC2Campaign.WOL: [
+ FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.WOL)], "Mar Sara", completion_critical=True),
+ FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.WOL)], "Colonist"),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.WOL)], "Colonist"),
+ FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.WOL)], "Artifact", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.WOL)], "Artifact", number=4, completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.WOL)], "Artifact", number=8, completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.WOL)], "Covert", number=2),
+ FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.WOL)], "Covert"),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.WOL)], "Rebellion", number=3),
+ FillMission(MissionPools.HARD, [MissionConnection(8, SC2Campaign.WOL)], "Rebellion"),
+ FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.WOL)], "Char", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.WOL)], "Char", completion_critical=True),
+ FillMission(MissionPools.FINAL, [MissionConnection(10, SC2Campaign.WOL), MissionConnection(11, SC2Campaign.WOL)], "Char", completion_critical=True, or_requirements=True)
+ ],
+ SC2Campaign.PROPHECY: [
+ FillMission(MissionPools.MEDIUM, [MissionConnection(4, SC2Campaign.WOL)], "_1"),
+ FillMission(MissionPools.FINAL, [MissionConnection(0, SC2Campaign.PROPHECY)], "_2"),
+ ],
+ SC2Campaign.HOTS: [
+ FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.HOTS)], "Umoja", completion_critical=True),
+ FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.HOTS)], "Kaldir"),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.HOTS)], "Kaldir"),
+ FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.HOTS)], "Char"),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(3, SC2Campaign.HOTS)], "Char"),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.HOTS)], "Zerus", number=3),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(5, SC2Campaign.HOTS)], "Zerus"),
+ FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.HOTS)], "Skygeirr Station", number=5),
+ FillMission(MissionPools.HARD, [MissionConnection(7, SC2Campaign.HOTS)], "Skygeirr Station"),
+ FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.HOTS)], "Dominion Space", number=5),
+ FillMission(MissionPools.HARD, [MissionConnection(9, SC2Campaign.HOTS)], "Dominion Space"),
+ FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.HOTS)], "Korhal", completion_critical=True, number=8),
+ FillMission(MissionPools.FINAL, [MissionConnection(11, SC2Campaign.HOTS)], "Korhal", completion_critical=True),
+ ],
+ SC2Campaign.PROLOGUE: [
+ FillMission(MissionPools.EASY, [MissionConnection(-1, SC2Campaign.PROLOGUE)], "_1"),
+ FillMission(MissionPools.FINAL, [MissionConnection(0, SC2Campaign.PROLOGUE)], "_2")
+ ],
+ SC2Campaign.LOTV: [
+ FillMission(MissionPools.STARTER, [MissionConnection(-1, SC2Campaign.LOTV)], "Aiur",completion_critical=True),
+ FillMission(MissionPools.EASY, [MissionConnection(0, SC2Campaign.LOTV)], "Aiur", completion_critical=True),
+ FillMission(MissionPools.EASY, [MissionConnection(1, SC2Campaign.LOTV)], "Korhal", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.LOTV)], "Shakuras", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(2, SC2Campaign.LOTV), MissionConnection(3, SC2Campaign.LOTV)], "Purifier", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.LOTV)], "Purifier", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(4, SC2Campaign.LOTV)], "Ulnar", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(6, SC2Campaign.LOTV)], "Tal'darim", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(5, SC2Campaign.LOTV), MissionConnection(7, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True),
+ FillMission(MissionPools.FINAL, [MissionConnection(8, SC2Campaign.LOTV)], "Return to Aiur", completion_critical=True),
+ ],
+ SC2Campaign.EPILOGUE: [
+ FillMission(MissionPools.VERY_HARD, [MissionConnection(12, SC2Campaign.WOL), MissionConnection(12, SC2Campaign.HOTS), MissionConnection(9, SC2Campaign.LOTV)], "_1", completion_critical=True),
+ FillMission(MissionPools.FINAL, [MissionConnection(0, SC2Campaign.EPILOGUE)], "_2", completion_critical=True),
+ ],
+ SC2Campaign.NCO: [
+ FillMission(MissionPools.EASY, [MissionConnection(-1, SC2Campaign.NCO)], "_1", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(0, SC2Campaign.NCO)], "_1", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(1, SC2Campaign.NCO)], "_2", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(2, SC2Campaign.NCO)], "_3", completion_critical=True),
+ FillMission(MissionPools.FINAL, [MissionConnection(3, SC2Campaign.NCO)], "_3", completion_critical=True),
+ ]
+ }
+
+
+def gauntlet_order() -> Dict[SC2Campaign, List[FillMission]]:
+ return {
+ SC2Campaign.GLOBAL: [
+ FillMission(MissionPools.STARTER, [MissionConnection(-1)], "I", completion_critical=True),
+ FillMission(MissionPools.EASY, [MissionConnection(0)], "II", completion_critical=True),
+ FillMission(MissionPools.EASY, [MissionConnection(1)], "III", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(2)], "IV", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(3)], "V", completion_critical=True),
+ FillMission(MissionPools.HARD, [MissionConnection(4)], "VI", completion_critical=True),
+ FillMission(MissionPools.FINAL, [MissionConnection(5)], "Final", completion_critical=True)
+ ]
+ }
+
+
+def mini_gauntlet_order() -> Dict[SC2Campaign, List[FillMission]]:
+ return {
+ SC2Campaign.GLOBAL: [
+ FillMission(MissionPools.STARTER, [MissionConnection(-1)], "I", completion_critical=True),
+ FillMission(MissionPools.EASY, [MissionConnection(0)], "II", completion_critical=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(1)], "III", completion_critical=True),
+ FillMission(MissionPools.FINAL, [MissionConnection(2)], "Final", completion_critical=True)
+ ]
+ }
+
+
+def grid_order() -> Dict[SC2Campaign, List[FillMission]]:
+ return {
+ SC2Campaign.GLOBAL: [
+ FillMission(MissionPools.STARTER, [MissionConnection(-1)], "_1"),
+ FillMission(MissionPools.EASY, [MissionConnection(0)], "_1"),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(1), MissionConnection(6), MissionConnection( 3)], "_1", or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(2), MissionConnection(7)], "_1", or_requirements=True),
+ FillMission(MissionPools.EASY, [MissionConnection(0)], "_2"),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(1), MissionConnection(4)], "_2", or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(2), MissionConnection(5), MissionConnection(10), MissionConnection(7)], "_2", or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(3), MissionConnection(6), MissionConnection(11)], "_2", or_requirements=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(4), MissionConnection(9), MissionConnection(12)], "_3", or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(5), MissionConnection(8), MissionConnection(10), MissionConnection(13)], "_3", or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(6), MissionConnection(9), MissionConnection(11), MissionConnection(14)], "_3", or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(7), MissionConnection(10)], "_3", or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(8), MissionConnection(13)], "_4", or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(9), MissionConnection(12), MissionConnection(14)], "_4", or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(10), MissionConnection(13)], "_4", or_requirements=True),
+ FillMission(MissionPools.FINAL, [MissionConnection(11), MissionConnection(14)], "_4", or_requirements=True)
+ ]
+ }
+
+def mini_grid_order() -> Dict[SC2Campaign, List[FillMission]]:
+ return {
+ SC2Campaign.GLOBAL: [
+ FillMission(MissionPools.STARTER, [MissionConnection(-1)], "_1"),
+ FillMission(MissionPools.EASY, [MissionConnection(0)], "_1"),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(1), MissionConnection(5)], "_1", or_requirements=True),
+ FillMission(MissionPools.EASY, [MissionConnection(0)], "_2"),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(1), MissionConnection(3)], "_2", or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(2), MissionConnection(4)], "_2", or_requirements=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(3), MissionConnection(7)], "_3", or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(4), MissionConnection(6)], "_3", or_requirements=True),
+ FillMission(MissionPools.FINAL, [MissionConnection(5), MissionConnection(7)], "_3", or_requirements=True)
+ ]
+ }
+
+def tiny_grid_order() -> Dict[SC2Campaign, List[FillMission]]:
+ return {
+ SC2Campaign.GLOBAL: [
+ FillMission(MissionPools.STARTER, [MissionConnection(-1)], "_1"),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(0)], "_1"),
+ FillMission(MissionPools.EASY, [MissionConnection(0)], "_2"),
+ FillMission(MissionPools.FINAL, [MissionConnection(1), MissionConnection(2)], "_2", or_requirements=True),
+ ]
+ }
+
+def blitz_order() -> Dict[SC2Campaign, List[FillMission]]:
+ return {
+ SC2Campaign.GLOBAL: [
+ FillMission(MissionPools.STARTER, [MissionConnection(-1)], "I"),
+ FillMission(MissionPools.EASY, [MissionConnection(-1)], "I"),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(0), MissionConnection(1)], "II", number=1, or_requirements=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(0), MissionConnection(1)], "II", number=1, or_requirements=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(0), MissionConnection(1)], "III", number=2, or_requirements=True),
+ FillMission(MissionPools.MEDIUM, [MissionConnection(0), MissionConnection(1)], "III", number=2, or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "IV", number=3, or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "IV", number=3, or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "V", number=4, or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "V", number=4, or_requirements=True),
+ FillMission(MissionPools.HARD, [MissionConnection(0), MissionConnection(1)], "Final", number=5, or_requirements=True),
+ FillMission(MissionPools.FINAL, [MissionConnection(0), MissionConnection(1)], "Final", number=5, or_requirements=True)
+ ]
+ }
+
+
+mission_orders: List[Callable[[], Dict[SC2Campaign, List[FillMission]]]] = [
+ vanilla_shuffle_order,
+ vanilla_shuffle_order,
+ mini_campaign_order,
+ grid_order,
+ mini_grid_order,
+ blitz_order,
+ gauntlet_order,
+ mini_gauntlet_order,
+ tiny_grid_order
+]
+
+
+vanilla_mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]] = {
+ SC2Campaign.WOL: {
+ SC2Mission.LIBERATION_DAY.mission_name: MissionInfo(SC2Mission.LIBERATION_DAY, [], SC2Mission.LIBERATION_DAY.area, completion_critical=True),
+ SC2Mission.THE_OUTLAWS.mission_name: MissionInfo(SC2Mission.THE_OUTLAWS, [MissionConnection(1, SC2Campaign.WOL)], SC2Mission.THE_OUTLAWS.area, completion_critical=True),
+ SC2Mission.ZERO_HOUR.mission_name: MissionInfo(SC2Mission.ZERO_HOUR, [MissionConnection(2, SC2Campaign.WOL)], SC2Mission.ZERO_HOUR.area, completion_critical=True),
+ SC2Mission.EVACUATION.mission_name: MissionInfo(SC2Mission.EVACUATION, [MissionConnection(3, SC2Campaign.WOL)], SC2Mission.EVACUATION.area),
+ SC2Mission.OUTBREAK.mission_name: MissionInfo(SC2Mission.OUTBREAK, [MissionConnection(4, SC2Campaign.WOL)], SC2Mission.OUTBREAK.area),
+ SC2Mission.SAFE_HAVEN.mission_name: MissionInfo(SC2Mission.SAFE_HAVEN, [MissionConnection(5, SC2Campaign.WOL)], SC2Mission.SAFE_HAVEN.area, number=7),
+ SC2Mission.HAVENS_FALL.mission_name: MissionInfo(SC2Mission.HAVENS_FALL, [MissionConnection(5, SC2Campaign.WOL)], SC2Mission.HAVENS_FALL.area, number=7),
+ SC2Mission.SMASH_AND_GRAB.mission_name: MissionInfo(SC2Mission.SMASH_AND_GRAB, [MissionConnection(3, SC2Campaign.WOL)], SC2Mission.SMASH_AND_GRAB.area, completion_critical=True),
+ SC2Mission.THE_DIG.mission_name: MissionInfo(SC2Mission.THE_DIG, [MissionConnection(8, SC2Campaign.WOL)], SC2Mission.THE_DIG.area, number=8, completion_critical=True),
+ SC2Mission.THE_MOEBIUS_FACTOR.mission_name: MissionInfo(SC2Mission.THE_MOEBIUS_FACTOR, [MissionConnection(9, SC2Campaign.WOL)], SC2Mission.THE_MOEBIUS_FACTOR.area, number=11, completion_critical=True),
+ SC2Mission.SUPERNOVA.mission_name: MissionInfo(SC2Mission.SUPERNOVA, [MissionConnection(10, SC2Campaign.WOL)], SC2Mission.SUPERNOVA.area, number=14, completion_critical=True),
+ SC2Mission.MAW_OF_THE_VOID.mission_name: MissionInfo(SC2Mission.MAW_OF_THE_VOID, [MissionConnection(11, SC2Campaign.WOL)], SC2Mission.MAW_OF_THE_VOID.area, completion_critical=True),
+ SC2Mission.DEVILS_PLAYGROUND.mission_name: MissionInfo(SC2Mission.DEVILS_PLAYGROUND, [MissionConnection(3, SC2Campaign.WOL)], SC2Mission.DEVILS_PLAYGROUND.area, number=4),
+ SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name: MissionInfo(SC2Mission.WELCOME_TO_THE_JUNGLE, [MissionConnection(13, SC2Campaign.WOL)], SC2Mission.WELCOME_TO_THE_JUNGLE.area),
+ SC2Mission.BREAKOUT.mission_name: MissionInfo(SC2Mission.BREAKOUT, [MissionConnection(14, SC2Campaign.WOL)], SC2Mission.BREAKOUT.area, number=8),
+ SC2Mission.GHOST_OF_A_CHANCE.mission_name: MissionInfo(SC2Mission.GHOST_OF_A_CHANCE, [MissionConnection(14, SC2Campaign.WOL)], SC2Mission.GHOST_OF_A_CHANCE.area, number=8),
+ SC2Mission.THE_GREAT_TRAIN_ROBBERY.mission_name: MissionInfo(SC2Mission.THE_GREAT_TRAIN_ROBBERY, [MissionConnection(3, SC2Campaign.WOL)], SC2Mission.THE_GREAT_TRAIN_ROBBERY.area, number=6),
+ SC2Mission.CUTTHROAT.mission_name: MissionInfo(SC2Mission.CUTTHROAT, [MissionConnection(17, SC2Campaign.WOL)], SC2Mission.THE_GREAT_TRAIN_ROBBERY.area),
+ SC2Mission.ENGINE_OF_DESTRUCTION.mission_name: MissionInfo(SC2Mission.ENGINE_OF_DESTRUCTION, [MissionConnection(18, SC2Campaign.WOL)], SC2Mission.ENGINE_OF_DESTRUCTION.area),
+ SC2Mission.MEDIA_BLITZ.mission_name: MissionInfo(SC2Mission.MEDIA_BLITZ, [MissionConnection(19, SC2Campaign.WOL)], SC2Mission.MEDIA_BLITZ.area),
+ SC2Mission.PIERCING_OF_THE_SHROUD.mission_name: MissionInfo(SC2Mission.PIERCING_OF_THE_SHROUD, [MissionConnection(20, SC2Campaign.WOL)], SC2Mission.PIERCING_OF_THE_SHROUD.area),
+ SC2Mission.GATES_OF_HELL.mission_name: MissionInfo(SC2Mission.GATES_OF_HELL, [MissionConnection(12, SC2Campaign.WOL)], SC2Mission.GATES_OF_HELL.area, completion_critical=True),
+ SC2Mission.BELLY_OF_THE_BEAST.mission_name: MissionInfo(SC2Mission.BELLY_OF_THE_BEAST, [MissionConnection(22, SC2Campaign.WOL)], SC2Mission.BELLY_OF_THE_BEAST.area, completion_critical=True),
+ SC2Mission.SHATTER_THE_SKY.mission_name: MissionInfo(SC2Mission.SHATTER_THE_SKY, [MissionConnection(22, SC2Campaign.WOL)], SC2Mission.SHATTER_THE_SKY.area, completion_critical=True),
+ SC2Mission.ALL_IN.mission_name: MissionInfo(SC2Mission.ALL_IN, [MissionConnection(23, SC2Campaign.WOL), MissionConnection(24, SC2Campaign.WOL)], SC2Mission.ALL_IN.area, or_requirements=True, completion_critical=True)
+ },
+ SC2Campaign.PROPHECY: {
+ SC2Mission.WHISPERS_OF_DOOM.mission_name: MissionInfo(SC2Mission.WHISPERS_OF_DOOM, [MissionConnection(9, SC2Campaign.WOL)], SC2Mission.WHISPERS_OF_DOOM.area),
+ SC2Mission.A_SINISTER_TURN.mission_name: MissionInfo(SC2Mission.A_SINISTER_TURN, [MissionConnection(1, SC2Campaign.PROPHECY)], SC2Mission.A_SINISTER_TURN.area),
+ SC2Mission.ECHOES_OF_THE_FUTURE.mission_name: MissionInfo(SC2Mission.ECHOES_OF_THE_FUTURE, [MissionConnection(2, SC2Campaign.PROPHECY)], SC2Mission.ECHOES_OF_THE_FUTURE.area),
+ SC2Mission.IN_UTTER_DARKNESS.mission_name: MissionInfo(SC2Mission.IN_UTTER_DARKNESS, [MissionConnection(3, SC2Campaign.PROPHECY)], SC2Mission.IN_UTTER_DARKNESS.area)
+ },
+ SC2Campaign.HOTS: {
+ SC2Mission.LAB_RAT.mission_name: MissionInfo(SC2Mission.LAB_RAT, [], SC2Mission.LAB_RAT.area, completion_critical=True),
+ SC2Mission.BACK_IN_THE_SADDLE.mission_name: MissionInfo(SC2Mission.BACK_IN_THE_SADDLE, [MissionConnection(1, SC2Campaign.HOTS)], SC2Mission.BACK_IN_THE_SADDLE.area, completion_critical=True),
+ SC2Mission.RENDEZVOUS.mission_name: MissionInfo(SC2Mission.RENDEZVOUS, [MissionConnection(2, SC2Campaign.HOTS)], SC2Mission.RENDEZVOUS.area, completion_critical=True),
+ SC2Mission.HARVEST_OF_SCREAMS.mission_name: MissionInfo(SC2Mission.HARVEST_OF_SCREAMS, [MissionConnection(3, SC2Campaign.HOTS)], SC2Mission.HARVEST_OF_SCREAMS.area),
+ SC2Mission.SHOOT_THE_MESSENGER.mission_name: MissionInfo(SC2Mission.SHOOT_THE_MESSENGER, [MissionConnection(4, SC2Campaign.HOTS)], SC2Mission.SHOOT_THE_MESSENGER.area),
+ SC2Mission.ENEMY_WITHIN.mission_name: MissionInfo(SC2Mission.ENEMY_WITHIN, [MissionConnection(5, SC2Campaign.HOTS)], SC2Mission.ENEMY_WITHIN.area),
+ SC2Mission.DOMINATION.mission_name: MissionInfo(SC2Mission.DOMINATION, [MissionConnection(3, SC2Campaign.HOTS)], SC2Mission.DOMINATION.area),
+ SC2Mission.FIRE_IN_THE_SKY.mission_name: MissionInfo(SC2Mission.FIRE_IN_THE_SKY, [MissionConnection(7, SC2Campaign.HOTS)], SC2Mission.FIRE_IN_THE_SKY.area),
+ SC2Mission.OLD_SOLDIERS.mission_name: MissionInfo(SC2Mission.OLD_SOLDIERS, [MissionConnection(8, SC2Campaign.HOTS)], SC2Mission.OLD_SOLDIERS.area),
+ SC2Mission.WAKING_THE_ANCIENT.mission_name: MissionInfo(SC2Mission.WAKING_THE_ANCIENT, [MissionConnection(6, SC2Campaign.HOTS), MissionConnection(9, SC2Campaign.HOTS)], SC2Mission.WAKING_THE_ANCIENT.area, completion_critical=True, or_requirements=True),
+ SC2Mission.THE_CRUCIBLE.mission_name: MissionInfo(SC2Mission.THE_CRUCIBLE, [MissionConnection(10, SC2Campaign.HOTS)], SC2Mission.THE_CRUCIBLE.area, completion_critical=True),
+ SC2Mission.SUPREME.mission_name: MissionInfo(SC2Mission.SUPREME, [MissionConnection(11, SC2Campaign.HOTS)], SC2Mission.SUPREME.area, completion_critical=True),
+ SC2Mission.INFESTED.mission_name: MissionInfo(SC2Mission.INFESTED, [MissionConnection(6, SC2Campaign.HOTS), MissionConnection(9, SC2Campaign.HOTS), MissionConnection(12, SC2Campaign.HOTS)], SC2Mission.INFESTED.area),
+ SC2Mission.HAND_OF_DARKNESS.mission_name: MissionInfo(SC2Mission.HAND_OF_DARKNESS, [MissionConnection(13, SC2Campaign.HOTS)], SC2Mission.HAND_OF_DARKNESS.area),
+ SC2Mission.PHANTOMS_OF_THE_VOID.mission_name: MissionInfo(SC2Mission.PHANTOMS_OF_THE_VOID, [MissionConnection(14, SC2Campaign.HOTS)], SC2Mission.PHANTOMS_OF_THE_VOID.area),
+ SC2Mission.WITH_FRIENDS_LIKE_THESE.mission_name: MissionInfo(SC2Mission.WITH_FRIENDS_LIKE_THESE, [MissionConnection(6, SC2Campaign.HOTS), MissionConnection(9, SC2Campaign.HOTS), MissionConnection(12, SC2Campaign.HOTS)], SC2Mission.WITH_FRIENDS_LIKE_THESE.area),
+ SC2Mission.CONVICTION.mission_name: MissionInfo(SC2Mission.CONVICTION, [MissionConnection(16, SC2Campaign.HOTS)], SC2Mission.CONVICTION.area),
+ SC2Mission.PLANETFALL.mission_name: MissionInfo(SC2Mission.PLANETFALL, [MissionConnection(15, SC2Campaign.HOTS), MissionConnection(17, SC2Campaign.HOTS)], SC2Mission.PLANETFALL.area, completion_critical=True),
+ SC2Mission.DEATH_FROM_ABOVE.mission_name: MissionInfo(SC2Mission.DEATH_FROM_ABOVE, [MissionConnection(18, SC2Campaign.HOTS)], SC2Mission.DEATH_FROM_ABOVE.area, completion_critical=True),
+ SC2Mission.THE_RECKONING.mission_name: MissionInfo(SC2Mission.THE_RECKONING, [MissionConnection(19, SC2Campaign.HOTS)], SC2Mission.THE_RECKONING.area, completion_critical=True),
+ },
+ SC2Campaign.PROLOGUE: {
+ SC2Mission.DARK_WHISPERS.mission_name: MissionInfo(SC2Mission.DARK_WHISPERS, [], SC2Mission.DARK_WHISPERS.area),
+ SC2Mission.GHOSTS_IN_THE_FOG.mission_name: MissionInfo(SC2Mission.GHOSTS_IN_THE_FOG, [MissionConnection(1, SC2Campaign.PROLOGUE)], SC2Mission.GHOSTS_IN_THE_FOG.area),
+ SC2Mission.EVIL_AWOKEN.mission_name: MissionInfo(SC2Mission.EVIL_AWOKEN, [MissionConnection(2, SC2Campaign.PROLOGUE)], SC2Mission.EVIL_AWOKEN.area)
+ },
+ SC2Campaign.LOTV: {
+ SC2Mission.FOR_AIUR.mission_name: MissionInfo(SC2Mission.FOR_AIUR, [], SC2Mission.FOR_AIUR.area, completion_critical=True),
+ SC2Mission.THE_GROWING_SHADOW.mission_name: MissionInfo(SC2Mission.THE_GROWING_SHADOW, [MissionConnection(1, SC2Campaign.LOTV)], SC2Mission.THE_GROWING_SHADOW.area, completion_critical=True),
+ SC2Mission.THE_SPEAR_OF_ADUN.mission_name: MissionInfo(SC2Mission.THE_SPEAR_OF_ADUN, [MissionConnection(2, SC2Campaign.LOTV)], SC2Mission.THE_SPEAR_OF_ADUN.area, completion_critical=True),
+ SC2Mission.SKY_SHIELD.mission_name: MissionInfo(SC2Mission.SKY_SHIELD, [MissionConnection(3, SC2Campaign.LOTV)], SC2Mission.SKY_SHIELD.area, completion_critical=True),
+ SC2Mission.BROTHERS_IN_ARMS.mission_name: MissionInfo(SC2Mission.BROTHERS_IN_ARMS, [MissionConnection(4, SC2Campaign.LOTV)], SC2Mission.BROTHERS_IN_ARMS.area, completion_critical=True),
+ SC2Mission.AMON_S_REACH.mission_name: MissionInfo(SC2Mission.AMON_S_REACH, [MissionConnection(3, SC2Campaign.LOTV)], SC2Mission.AMON_S_REACH.area, completion_critical=True),
+ SC2Mission.LAST_STAND.mission_name: MissionInfo(SC2Mission.LAST_STAND, [MissionConnection(6, SC2Campaign.LOTV)], SC2Mission.LAST_STAND.area, completion_critical=True),
+ SC2Mission.FORBIDDEN_WEAPON.mission_name: MissionInfo(SC2Mission.FORBIDDEN_WEAPON, [MissionConnection(5, SC2Campaign.LOTV), MissionConnection(7, SC2Campaign.LOTV)], SC2Mission.FORBIDDEN_WEAPON.area, completion_critical=True, or_requirements=True),
+ SC2Mission.TEMPLE_OF_UNIFICATION.mission_name: MissionInfo(SC2Mission.TEMPLE_OF_UNIFICATION, [MissionConnection(5, SC2Campaign.LOTV), MissionConnection(7, SC2Campaign.LOTV), MissionConnection(8, SC2Campaign.LOTV)], SC2Mission.TEMPLE_OF_UNIFICATION.area, completion_critical=True),
+ SC2Mission.THE_INFINITE_CYCLE.mission_name: MissionInfo(SC2Mission.THE_INFINITE_CYCLE, [MissionConnection(9, SC2Campaign.LOTV)], SC2Mission.THE_INFINITE_CYCLE.area, completion_critical=True),
+ SC2Mission.HARBINGER_OF_OBLIVION.mission_name: MissionInfo(SC2Mission.HARBINGER_OF_OBLIVION, [MissionConnection(10, SC2Campaign.LOTV)], SC2Mission.HARBINGER_OF_OBLIVION.area, completion_critical=True),
+ SC2Mission.UNSEALING_THE_PAST.mission_name: MissionInfo(SC2Mission.UNSEALING_THE_PAST, [MissionConnection(11, SC2Campaign.LOTV)], SC2Mission.UNSEALING_THE_PAST.area, completion_critical=True),
+ SC2Mission.PURIFICATION.mission_name: MissionInfo(SC2Mission.PURIFICATION, [MissionConnection(12, SC2Campaign.LOTV)], SC2Mission.PURIFICATION.area, completion_critical=True),
+ SC2Mission.STEPS_OF_THE_RITE.mission_name: MissionInfo(SC2Mission.STEPS_OF_THE_RITE, [MissionConnection(11, SC2Campaign.LOTV)], SC2Mission.STEPS_OF_THE_RITE.area, completion_critical=True),
+ SC2Mission.RAK_SHIR.mission_name: MissionInfo(SC2Mission.RAK_SHIR, [MissionConnection(14, SC2Campaign.LOTV)], SC2Mission.RAK_SHIR.area, completion_critical=True),
+ SC2Mission.TEMPLAR_S_CHARGE.mission_name: MissionInfo(SC2Mission.TEMPLAR_S_CHARGE, [MissionConnection(13, SC2Campaign.LOTV), MissionConnection(15, SC2Campaign.LOTV)], SC2Mission.TEMPLAR_S_CHARGE.area, completion_critical=True, or_requirements=True),
+ SC2Mission.TEMPLAR_S_RETURN.mission_name: MissionInfo(SC2Mission.TEMPLAR_S_RETURN, [MissionConnection(13, SC2Campaign.LOTV), MissionConnection(15, SC2Campaign.LOTV), MissionConnection(16, SC2Campaign.LOTV)], SC2Mission.TEMPLAR_S_RETURN.area, completion_critical=True),
+ SC2Mission.THE_HOST.mission_name: MissionInfo(SC2Mission.THE_HOST, [MissionConnection(17, SC2Campaign.LOTV)], SC2Mission.THE_HOST.area, completion_critical=True),
+ SC2Mission.SALVATION.mission_name: MissionInfo(SC2Mission.SALVATION, [MissionConnection(18, SC2Campaign.LOTV)], SC2Mission.SALVATION.area, completion_critical=True),
+ },
+ SC2Campaign.EPILOGUE: {
+ SC2Mission.INTO_THE_VOID.mission_name: MissionInfo(SC2Mission.INTO_THE_VOID, [MissionConnection(25, SC2Campaign.WOL), MissionConnection(20, SC2Campaign.HOTS), MissionConnection(19, SC2Campaign.LOTV)], SC2Mission.INTO_THE_VOID.area, completion_critical=True),
+ SC2Mission.THE_ESSENCE_OF_ETERNITY.mission_name: MissionInfo(SC2Mission.THE_ESSENCE_OF_ETERNITY, [MissionConnection(1, SC2Campaign.EPILOGUE)], SC2Mission.THE_ESSENCE_OF_ETERNITY.area, completion_critical=True),
+ SC2Mission.AMON_S_FALL.mission_name: MissionInfo(SC2Mission.AMON_S_FALL, [MissionConnection(2, SC2Campaign.EPILOGUE)], SC2Mission.AMON_S_FALL.area, completion_critical=True),
+ },
+ SC2Campaign.NCO: {
+ SC2Mission.THE_ESCAPE.mission_name: MissionInfo(SC2Mission.THE_ESCAPE, [], SC2Mission.THE_ESCAPE.area, completion_critical=True),
+ SC2Mission.SUDDEN_STRIKE.mission_name: MissionInfo(SC2Mission.SUDDEN_STRIKE, [MissionConnection(1, SC2Campaign.NCO)], SC2Mission.SUDDEN_STRIKE.area, completion_critical=True),
+ SC2Mission.ENEMY_INTELLIGENCE.mission_name: MissionInfo(SC2Mission.ENEMY_INTELLIGENCE, [MissionConnection(2, SC2Campaign.NCO)], SC2Mission.ENEMY_INTELLIGENCE.area, completion_critical=True),
+ SC2Mission.TROUBLE_IN_PARADISE.mission_name: MissionInfo(SC2Mission.TROUBLE_IN_PARADISE, [MissionConnection(3, SC2Campaign.NCO)], SC2Mission.TROUBLE_IN_PARADISE.area, completion_critical=True),
+ SC2Mission.NIGHT_TERRORS.mission_name: MissionInfo(SC2Mission.NIGHT_TERRORS, [MissionConnection(4, SC2Campaign.NCO)], SC2Mission.NIGHT_TERRORS.area, completion_critical=True),
+ SC2Mission.FLASHPOINT.mission_name: MissionInfo(SC2Mission.FLASHPOINT, [MissionConnection(5, SC2Campaign.NCO)], SC2Mission.FLASHPOINT.area, completion_critical=True),
+ SC2Mission.IN_THE_ENEMY_S_SHADOW.mission_name: MissionInfo(SC2Mission.IN_THE_ENEMY_S_SHADOW, [MissionConnection(6, SC2Campaign.NCO)], SC2Mission.IN_THE_ENEMY_S_SHADOW.area, completion_critical=True),
+ SC2Mission.DARK_SKIES.mission_name: MissionInfo(SC2Mission.DARK_SKIES, [MissionConnection(7, SC2Campaign.NCO)], SC2Mission.DARK_SKIES.area, completion_critical=True),
+ SC2Mission.END_GAME.mission_name: MissionInfo(SC2Mission.END_GAME, [MissionConnection(8, SC2Campaign.NCO)], SC2Mission.END_GAME.area, completion_critical=True),
+ }
+}
+
+lookup_id_to_mission: Dict[int, SC2Mission] = {
+ mission.id: mission for mission in SC2Mission
+}
+
+lookup_name_to_mission: Dict[str, SC2Mission] = {
+ mission.mission_name: mission for mission in SC2Mission
+}
+
+lookup_id_to_campaign: Dict[int, SC2Campaign] = {
+ campaign.id: campaign for campaign in SC2Campaign
+}
+
+
+campaign_mission_table: Dict[SC2Campaign, Set[SC2Mission]] = {
+ campaign: set() for campaign in SC2Campaign
+}
+for mission in SC2Mission:
+ campaign_mission_table[mission.campaign].add(mission)
+
+
+def get_campaign_difficulty(campaign: SC2Campaign, excluded_missions: Iterable[SC2Mission] = ()) -> MissionPools:
+ """
+
+ :param campaign:
+ :param excluded_missions:
+ :return: Campaign's the most difficult non-excluded mission
+ """
+ excluded_mission_set = set(excluded_missions)
+ included_missions = campaign_mission_table[campaign].difference(excluded_mission_set)
+ return max([mission.pool for mission in included_missions])
+
+
+def get_campaign_goal_priority(campaign: SC2Campaign, excluded_missions: Iterable[SC2Mission] = ()) -> SC2CampaignGoalPriority:
+ """
+ Gets a modified campaign goal priority.
+ If all the campaign's goal missions are excluded, it's ineligible to have the goal
+ If the campaign's very hard missions are excluded, the priority is lowered to hard
+ :param campaign:
+ :param excluded_missions:
+ :return:
+ """
+ if excluded_missions is None:
+ return campaign.goal_priority
+ else:
+ goal_missions = set(get_campaign_potential_goal_missions(campaign))
+ excluded_mission_set = set(excluded_missions)
+ remaining_goals = goal_missions.difference(excluded_mission_set)
+ if remaining_goals == set():
+ # All potential goals are excluded, the campaign can't be a goal
+ return SC2CampaignGoalPriority.NONE
+ elif campaign.goal_priority == SC2CampaignGoalPriority.VERY_HARD:
+ # Check if a very hard campaign doesn't get rid of it's last very hard mission
+ difficulty = get_campaign_difficulty(campaign, excluded_missions)
+ if difficulty == MissionPools.VERY_HARD:
+ return SC2CampaignGoalPriority.VERY_HARD
+ else:
+ return SC2CampaignGoalPriority.HARD
+ else:
+ return campaign.goal_priority
+
+
+class SC2CampaignGoal(NamedTuple):
+ mission: SC2Mission
+ location: str
+
+
+campaign_final_mission_locations: Dict[SC2Campaign, SC2CampaignGoal] = {
+ SC2Campaign.WOL: SC2CampaignGoal(SC2Mission.ALL_IN, "All-In: Victory"),
+ SC2Campaign.PROPHECY: SC2CampaignGoal(SC2Mission.IN_UTTER_DARKNESS, "In Utter Darkness: Kills"),
+ SC2Campaign.HOTS: None,
+ SC2Campaign.PROLOGUE: SC2CampaignGoal(SC2Mission.EVIL_AWOKEN, "Evil Awoken: Victory"),
+ SC2Campaign.LOTV: SC2CampaignGoal(SC2Mission.SALVATION, "Salvation: Victory"),
+ SC2Campaign.EPILOGUE: None,
+ SC2Campaign.NCO: SC2CampaignGoal(SC2Mission.END_GAME, "End Game: Victory"),
+}
+
+campaign_alt_final_mission_locations: Dict[SC2Campaign, Dict[SC2Mission, str]] = {
+ SC2Campaign.WOL: {
+ SC2Mission.MAW_OF_THE_VOID: "Maw of the Void: Victory",
+ SC2Mission.ENGINE_OF_DESTRUCTION: "Engine of Destruction: Victory",
+ SC2Mission.SUPERNOVA: "Supernova: Victory",
+ SC2Mission.GATES_OF_HELL: "Gates of Hell: Victory",
+ SC2Mission.SHATTER_THE_SKY: "Shatter the Sky: Victory"
+ },
+ SC2Campaign.PROPHECY: None,
+ SC2Campaign.HOTS: {
+ SC2Mission.THE_RECKONING: "The Reckoning: Victory",
+ SC2Mission.THE_CRUCIBLE: "The Crucible: Victory",
+ SC2Mission.HAND_OF_DARKNESS: "Hand of Darkness: Victory",
+ SC2Mission.PHANTOMS_OF_THE_VOID: "Phantoms of the Void: Victory",
+ SC2Mission.PLANETFALL: "Planetfall: Victory",
+ SC2Mission.DEATH_FROM_ABOVE: "Death From Above: Victory"
+ },
+ SC2Campaign.PROLOGUE: {
+ SC2Mission.GHOSTS_IN_THE_FOG: "Ghosts in the Fog: Victory"
+ },
+ SC2Campaign.LOTV: {
+ SC2Mission.THE_HOST: "The Host: Victory",
+ SC2Mission.TEMPLAR_S_CHARGE: "Templar's Charge: Victory"
+ },
+ SC2Campaign.EPILOGUE: {
+ SC2Mission.AMON_S_FALL: "Amon's Fall: Victory",
+ SC2Mission.INTO_THE_VOID: "Into the Void: Victory",
+ SC2Mission.THE_ESSENCE_OF_ETERNITY: "The Essence of Eternity: Victory",
+ },
+ SC2Campaign.NCO: {
+ SC2Mission.FLASHPOINT: "Flashpoint: Victory",
+ SC2Mission.DARK_SKIES: "Dark Skies: Victory",
+ SC2Mission.NIGHT_TERRORS: "Night Terrors: Victory",
+ SC2Mission.TROUBLE_IN_PARADISE: "Trouble In Paradise: Victory"
+ }
+}
+
+campaign_race_exceptions: Dict[SC2Mission, SC2Race] = {
+ SC2Mission.WITH_FRIENDS_LIKE_THESE: SC2Race.TERRAN
+}
+
+
+def get_goal_location(mission: SC2Mission) -> Union[str, None]:
+ """
+
+ :param mission:
+ :return: Goal location assigned to the goal mission
+ """
+ campaign = mission.campaign
+ primary_campaign_goal = campaign_final_mission_locations[campaign]
+ if primary_campaign_goal is not None:
+ if primary_campaign_goal.mission == mission:
+ return primary_campaign_goal.location
+
+ campaign_alt_goals = campaign_alt_final_mission_locations[campaign]
+ if campaign_alt_goals is not None and mission in campaign_alt_goals:
+ return campaign_alt_goals.get(mission)
+
+ return mission.mission_name + ": Victory"
+
+
+def get_campaign_potential_goal_missions(campaign: SC2Campaign) -> List[SC2Mission]:
+ """
+
+ :param campaign:
+ :return: All missions that can be the campaign's goal
+ """
+ missions: List[SC2Mission] = list()
+ primary_goal_mission = campaign_final_mission_locations[campaign]
+ if primary_goal_mission is not None:
+ missions.append(primary_goal_mission.mission)
+ alt_goal_locations = campaign_alt_final_mission_locations[campaign]
+ if alt_goal_locations is not None:
+ for mission in alt_goal_locations.keys():
+ missions.append(mission)
+
+ return missions
+
+
+def get_no_build_missions() -> List[SC2Mission]:
+ return [mission for mission in SC2Mission if not mission.build]
diff --git a/worlds/sc2/Options.py b/worlds/sc2/Options.py
new file mode 100644
index 000000000000..88febb7096ef
--- /dev/null
+++ b/worlds/sc2/Options.py
@@ -0,0 +1,908 @@
+from dataclasses import dataclass, fields, Field
+from typing import FrozenSet, Union, Set
+
+from Options import Choice, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range, PerGameCommonOptions
+from .MissionTables import SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_no_build_missions, \
+ campaign_mission_table
+from worlds.AutoWorld import World
+
+
+class GameDifficulty(Choice):
+ """
+ The difficulty of the campaign, affects enemy AI, starting units, and game speed.
+
+ For those unfamiliar with the Archipelago randomizer, the recommended settings are one difficulty level
+ lower than the vanilla game
+ """
+ display_name = "Game Difficulty"
+ option_casual = 0
+ option_normal = 1
+ option_hard = 2
+ option_brutal = 3
+ default = 1
+
+
+class GameSpeed(Choice):
+ """Optional setting to override difficulty-based game speed."""
+ display_name = "Game Speed"
+ option_default = 0
+ option_slower = 1
+ option_slow = 2
+ option_normal = 3
+ option_fast = 4
+ option_faster = 5
+ default = option_default
+
+
+class DisableForcedCamera(Toggle):
+ """
+ Prevents the game from moving or locking the camera without the player's consent.
+ """
+ display_name = "Disable Forced Camera Movement"
+
+
+class SkipCutscenes(Toggle):
+ """
+ Skips all cutscenes and prevents dialog from blocking progress.
+ """
+ display_name = "Skip Cutscenes"
+
+
+class AllInMap(Choice):
+ """Determines what version of All-In (WoL final map) that will be generated for the campaign."""
+ display_name = "All In Map"
+ option_ground = 0
+ option_air = 1
+
+
+class MissionOrder(Choice):
+ """
+ Determines the order the missions are played in. The last three mission orders end in a random mission.
+ Vanilla (83 total if all campaigns enabled): Keeps the standard mission order and branching from the vanilla Campaigns.
+ Vanilla Shuffled (83 total if all campaigns enabled): Keeps same branching paths from the vanilla Campaigns but randomizes the order of missions within.
+ Mini Campaign (47 total if all campaigns enabled): Shorter version of the campaign with randomized missions and optional branches.
+ Medium Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards bottom-right mission to win.
+ Mini Grid (9): A 3x3 version of Grid. Complete the bottom-right mission to win.
+ Blitz (12): 12 random missions that open up very quickly. Complete the bottom-right mission to win.
+ Gauntlet (7): Linear series of 7 random missions to complete the campaign.
+ Mini Gauntlet (4): Linear series of 4 random missions to complete the campaign.
+ Tiny Grid (4): A 2x2 version of Grid. Complete the bottom-right mission to win.
+ Grid (variable): A grid that will resize to use all non-excluded missions. Corners may be omitted to make the grid more square. Complete the bottom-right mission to win.
+ """
+ display_name = "Mission Order"
+ option_vanilla = 0
+ option_vanilla_shuffled = 1
+ option_mini_campaign = 2
+ option_medium_grid = 3
+ option_mini_grid = 4
+ option_blitz = 5
+ option_gauntlet = 6
+ option_mini_gauntlet = 7
+ option_tiny_grid = 8
+ option_grid = 9
+
+
+class MaximumCampaignSize(Range):
+ """
+ Sets an upper bound on how many missions to include when a variable-size mission order is selected.
+ If a set-size mission order is selected, does nothing.
+ """
+ display_name = "Maximum Campaign Size"
+ range_start = 1
+ range_end = 83
+ default = 83
+
+
+class GridTwoStartPositions(Toggle):
+ """
+ If turned on and 'grid' mission order is selected, removes a mission from the starting
+ corner sets the adjacent two missions as the starter missions.
+ """
+ display_name = "Start with two unlocked missions on grid"
+ default = Toggle.option_false
+
+
+class ColorChoice(Choice):
+ option_white = 0
+ option_red = 1
+ option_blue = 2
+ option_teal = 3
+ option_purple = 4
+ option_yellow = 5
+ option_orange = 6
+ option_green = 7
+ option_light_pink = 8
+ option_violet = 9
+ option_light_grey = 10
+ option_dark_green = 11
+ option_brown = 12
+ option_light_green = 13
+ option_dark_grey = 14
+ option_pink = 15
+ option_rainbow = 16
+ option_default = 17
+ default = option_default
+
+
+class PlayerColorTerranRaynor(ColorChoice):
+ """Determines in-game team color for playable Raynor's Raiders (Terran) factions."""
+ display_name = "Terran Player Color (Raynor)"
+
+
+class PlayerColorProtoss(ColorChoice):
+ """Determines in-game team color for playable Protoss factions."""
+ display_name = "Protoss Player Color"
+
+
+class PlayerColorZerg(ColorChoice):
+ """Determines in-game team color for playable Zerg factions before Kerrigan becomes Primal Kerrigan."""
+ display_name = "Zerg Player Color"
+
+
+class PlayerColorZergPrimal(ColorChoice):
+ """Determines in-game team color for playable Zerg factions after Kerrigan becomes Primal Kerrigan."""
+ display_name = "Zerg Player Color (Primal)"
+
+
+class EnableWolMissions(DefaultOnToggle):
+ """
+ Enables missions from main Wings of Liberty campaign.
+ """
+ display_name = "Enable Wings of Liberty missions"
+
+
+class EnableProphecyMissions(DefaultOnToggle):
+ """
+ Enables missions from Prophecy mini-campaign.
+ """
+ display_name = "Enable Prophecy missions"
+
+
+class EnableHotsMissions(DefaultOnToggle):
+ """
+ Enables missions from Heart of the Swarm campaign.
+ """
+ display_name = "Enable Heart of the Swarm missions"
+
+
+class EnableLotVPrologueMissions(DefaultOnToggle):
+ """
+ Enables missions from Prologue campaign.
+ """
+ display_name = "Enable Prologue (Legacy of the Void) missions"
+
+
+class EnableLotVMissions(DefaultOnToggle):
+ """
+ Enables missions from Legacy of the Void campaign.
+ """
+ display_name = "Enable Legacy of the Void (main campaign) missions"
+
+
+class EnableEpilogueMissions(DefaultOnToggle):
+ """
+ Enables missions from Epilogue campaign.
+ These missions are considered very hard.
+
+ Enabling Wings of Liberty, Heart of the Swarm and Legacy of the Void is strongly recommended in order to play Epilogue.
+ Not recommended for short mission orders.
+ See also: Exclude Very Hard Missions
+ """
+ display_name = "Enable Epilogue missions"
+
+
+class EnableNCOMissions(DefaultOnToggle):
+ """
+ Enables missions from Nova Covert Ops campaign.
+
+ Note: For best gameplay experience it's recommended to also enable Wings of Liberty campaign.
+ """
+ display_name = "Enable Nova Covert Ops missions"
+
+
+class ShuffleCampaigns(DefaultOnToggle):
+ """
+ Shuffles the missions between campaigns if enabled.
+ Only available for Vanilla Shuffled and Mini Campaign mission order
+ """
+ display_name = "Shuffle Campaigns"
+
+
+class ShuffleNoBuild(DefaultOnToggle):
+ """
+ Determines if the no-build missions are included in the shuffle.
+ If turned off, the no-build missions will not appear. Has no effect for Vanilla mission order.
+ """
+ display_name = "Shuffle No-Build Missions"
+
+
+class StarterUnit(Choice):
+ """
+ Unlocks a random unit at the start of the game.
+
+ Off: No units are provided, the first unit must be obtained from the randomizer
+ Balanced: A unit that doesn't give the player too much power early on is given
+ Any Starter Unit: Any starter unit can be given
+ """
+ display_name = "Starter Unit"
+ option_off = 0
+ option_balanced = 1
+ option_any_starter_unit = 2
+
+
+class RequiredTactics(Choice):
+ """
+ Determines the maximum tactical difficulty of the world (separate from mission difficulty). Higher settings
+ increase randomness.
+
+ Standard: All missions can be completed with good micro and macro.
+ Advanced: Completing missions may require relying on starting units and micro-heavy units.
+ No Logic: Units and upgrades may be placed anywhere. LIKELY TO RENDER THE RUN IMPOSSIBLE ON HARDER DIFFICULTIES!
+ Locks Grant Story Tech option to true.
+ """
+ display_name = "Required Tactics"
+ option_standard = 0
+ option_advanced = 1
+ option_no_logic = 2
+
+
+class GenericUpgradeMissions(Range):
+ """Determines the percentage of missions in the mission order that must be completed before
+ level 1 of all weapon and armor upgrades is unlocked. Level 2 upgrades require double the amount of missions,
+ and level 3 requires triple the amount. The required amounts are always rounded down.
+ If set to 0, upgrades are instead added to the item pool and must be found to be used."""
+ display_name = "Generic Upgrade Missions"
+ range_start = 0
+ range_end = 100
+ default = 0
+
+
+class GenericUpgradeResearch(Choice):
+ """Determines how weapon and armor upgrades affect missions once unlocked.
+
+ Vanilla: Upgrades must be researched as normal.
+ Auto In No-Build: In No-Build missions, upgrades are automatically researched.
+ In all other missions, upgrades must be researched as normal.
+ Auto In Build: In No-Build missions, upgrades are unavailable as normal.
+ In all other missions, upgrades are automatically researched.
+ Always Auto: Upgrades are automatically researched in all missions."""
+ display_name = "Generic Upgrade Research"
+ option_vanilla = 0
+ option_auto_in_no_build = 1
+ option_auto_in_build = 2
+ option_always_auto = 3
+
+
+class GenericUpgradeItems(Choice):
+ """Determines how weapon and armor upgrades are split into items. All options produce 3 levels of each item.
+ Does nothing if upgrades are unlocked by completed mission counts.
+
+ Individual Items: All weapon and armor upgrades are each an item,
+ resulting in 18 total upgrade items for Terran and 15 total items for Zerg and Protoss each.
+ Bundle Weapon And Armor: All types of weapon upgrades are one item per race,
+ and all types of armor upgrades are one item per race,
+ resulting in 18 total items.
+ Bundle Unit Class: Weapon and armor upgrades are merged,
+ but upgrades are bundled separately for each race:
+ Infantry, Vehicle, and Starship upgrades for Terran (9 items),
+ Ground and Flyer upgrades for Zerg (6 items),
+ Ground and Air upgrades for Protoss (6 items),
+ resulting in 21 total items.
+ Bundle All: All weapon and armor upgrades are one item per race,
+ resulting in 9 total items."""
+ display_name = "Generic Upgrade Items"
+ option_individual_items = 0
+ option_bundle_weapon_and_armor = 1
+ option_bundle_unit_class = 2
+ option_bundle_all = 3
+
+
+class NovaCovertOpsItems(Toggle):
+ """
+ If turned on, the equipment upgrades from Nova Covert Ops may be present in the world.
+
+ If Nova Covert Ops campaign is enabled, this option is locked to be turned on.
+ """
+ display_name = "Nova Covert Ops Items"
+ default = Toggle.option_true
+
+
+class BroodWarItems(Toggle):
+ """If turned on, returning items from StarCraft: Brood War may appear in the world."""
+ display_name = "Brood War Items"
+ default = Toggle.option_true
+
+
+class ExtendedItems(Toggle):
+ """If turned on, original items that did not appear in Campaign mode may appear in the world."""
+ display_name = "Extended Items"
+ default = Toggle.option_true
+
+
+# Current maximum number of upgrades for a unit
+MAX_UPGRADES_OPTION = 12
+
+
+class EnsureGenericItems(Range):
+ """
+ Specifies a minimum percentage of the generic item pool that will be present for the slot.
+ The generic item pool is the pool of all generically useful items after all exclusions.
+ Generically-useful items include: Worker upgrades, Building upgrades, economy upgrades,
+ Mercenaries, Kerrigan levels and abilities, and Spear of Adun abilities
+ Increasing this percentage will make units less common.
+ """
+ display_name = "Ensure Generic Items"
+ range_start = 0
+ range_end = 100
+ default = 25
+
+
+class MinNumberOfUpgrades(Range):
+ """
+ Set a minimum to the number of upgrades a unit/structure can have.
+ Note that most units have 4 or 6 upgrades.
+ If a unit has fewer upgrades than the minimum, it will have all of its upgrades.
+
+ Doesn't affect shared unit upgrades.
+ """
+ display_name = "Minimum number of upgrades per unit/structure"
+ range_start = 0
+ range_end = MAX_UPGRADES_OPTION
+ default = 2
+
+
+class MaxNumberOfUpgrades(Range):
+ """
+ Set a maximum to the number of upgrades a unit/structure can have. -1 is used to define unlimited.
+ Note that most unit have 4 to 6 upgrades.
+
+ Doesn't affect shared unit upgrades.
+ """
+ display_name = "Maximum number of upgrades per unit/structure"
+ range_start = -1
+ range_end = MAX_UPGRADES_OPTION
+ default = -1
+
+
+class KerriganPresence(Choice):
+ """
+ Determines whether Kerrigan is playable outside of missions that require her.
+
+ Vanilla: Kerrigan is playable as normal, appears in the same missions as in vanilla game.
+ Not Present: Kerrigan is not playable, unless the mission requires her to be present. Other hero units stay playable,
+ and locations normally requiring Kerrigan can be checked by any unit.
+ Kerrigan level items, active abilities and passive abilities affecting her will not appear.
+ In missions where the Kerrigan unit is required, story abilities are given in same way as Grant Story Tech is set to true
+ Not Present And No Passives: In addition to the above, Kerrigan's passive abilities affecting other units (such as Twin Drones) will not appear.
+
+ Note: Always set to "Not Present" if Heart of the Swarm campaign is disabled.
+ """
+ display_name = "Kerrigan Presence"
+ option_vanilla = 0
+ option_not_present = 1
+ option_not_present_and_no_passives = 2
+
+
+class KerriganLevelsPerMissionCompleted(Range):
+ """
+ Determines how many levels Kerrigan gains when a mission is beaten.
+
+ NOTE: Setting this too low can result in generation failures if The Infinite Cycle or Supreme are in the mission pool.
+ """
+ display_name = "Levels Per Mission Beaten"
+ range_start = 0
+ range_end = 20
+ default = 0
+
+
+class KerriganLevelsPerMissionCompletedCap(Range):
+ """
+ Limits how many total levels Kerrigan can gain from beating missions. This does not affect levels gained from items.
+ Set to -1 to disable this limit.
+
+ NOTE: The following missions have these level requirements:
+ Supreme: 35
+ The Infinite Cycle: 70
+ See Grant Story Levels for more details.
+ """
+ display_name = "Levels Per Mission Beaten Cap"
+ range_start = -1
+ range_end = 140
+ default = -1
+
+
+class KerriganLevelItemSum(Range):
+ """
+ Determines the sum of the level items in the world. This does not affect levels gained from beating missions.
+
+ NOTE: The following missions have these level requirements:
+ Supreme: 35
+ The Infinite Cycle: 70
+ See Grant Story Levels for more details.
+ """
+ display_name = "Kerrigan Level Item Sum"
+ range_start = 0
+ range_end = 140
+ default = 70
+
+
+class KerriganLevelItemDistribution(Choice):
+ """Determines the amount and size of Kerrigan level items.
+
+ Vanilla: Uses the distribution in the vanilla campaign.
+ This entails 32 individual levels and 6 packs of varying sizes.
+ This distribution always adds up to 70, ignoring the Level Item Sum setting.
+ Smooth: Uses a custom, condensed distribution of 10 items between sizes 4 and 10,
+ intended to fit more levels into settings with little room for filler while keeping some variance in level gains.
+ This distribution always adds up to 70, ignoring the Level Item Sum setting.
+ Size 70: Uses items worth 70 levels each.
+ Size 35: Uses items worth 35 levels each.
+ Size 14: Uses items worth 14 levels each.
+ Size 10: Uses items worth 10 levels each.
+ Size 7: Uses items worth 7 levels each.
+ Size 5: Uses items worth 5 levels each.
+ Size 2: Uses items worth 2 level eachs.
+ Size 1: Uses individual levels. As there are not enough locations in the game for this distribution,
+ this will result in a greatly reduced total level, and is likely to remove many other items."""
+ display_name = "Kerrigan Level Item Distribution"
+ option_vanilla = 0
+ option_smooth = 1
+ option_size_70 = 2
+ option_size_35 = 3
+ option_size_14 = 4
+ option_size_10 = 5
+ option_size_7 = 6
+ option_size_5 = 7
+ option_size_2 = 8
+ option_size_1 = 9
+ default = option_smooth
+
+
+class KerriganTotalLevelCap(Range):
+ """
+ Limits how many total levels Kerrigan can gain from any source. Depending on your other settings,
+ there may be more levels available in the world, but they will not affect Kerrigan.
+ Set to -1 to disable this limit.
+
+ NOTE: The following missions have these level requirements:
+ Supreme: 35
+ The Infinite Cycle: 70
+ See Grant Story Levels for more details.
+ """
+ display_name = "Total Level Cap"
+ range_start = -1
+ range_end = 140
+ default = -1
+
+
+class StartPrimaryAbilities(Range):
+ """Number of Primary Abilities (Kerrigan Tier 1, 2, and 4) to start the game with.
+ If set to 4, a Tier 7 ability is also included."""
+ display_name = "Starting Primary Abilities"
+ range_start = 0
+ range_end = 4
+ default = 0
+
+
+class KerriganPrimalStatus(Choice):
+ """Determines when Kerrigan appears in her Primal Zerg form.
+ This greatly increases her energy regeneration.
+
+ Vanilla: Kerrigan is human in missions that canonically appear before The Crucible,
+ and zerg thereafter.
+ Always Zerg: Kerrigan is always zerg.
+ Always Human: Kerrigan is always human.
+ Level 35: Kerrigan is human until reaching level 35, and zerg thereafter.
+ Half Completion: Kerrigan is human until half of the missions in the world are completed,
+ and zerg thereafter.
+ Item: Kerrigan's Primal Form is an item. She is human until it is found, and zerg thereafter."""
+ display_name = "Kerrigan Primal Status"
+ option_vanilla = 0
+ option_always_zerg = 1
+ option_always_human = 2
+ option_level_35 = 3
+ option_half_completion = 4
+ option_item = 5
+
+
+class SpearOfAdunPresence(Choice):
+ """
+ Determines in which missions Spear of Adun calldowns will be available.
+ Affects only abilities used from Spear of Adun top menu.
+
+ Not Present: Spear of Adun calldowns are unavailable.
+ LotV Protoss: Spear of Adun calldowns are only available in LotV main campaign
+ Protoss: Spear od Adun calldowns are available in any Protoss mission
+ Everywhere: Spear od Adun calldowns are available in any mission of any race
+ """
+ display_name = "Spear of Adun Presence"
+ option_not_present = 0
+ option_lotv_protoss = 1
+ option_protoss = 2
+ option_everywhere = 3
+ default = option_lotv_protoss
+
+ # Fix case
+ @classmethod
+ def get_option_name(cls, value: int) -> str:
+ if value == SpearOfAdunPresence.option_lotv_protoss:
+ return "LotV Protoss"
+ else:
+ return super().get_option_name(value)
+
+
+class SpearOfAdunPresentInNoBuild(Toggle):
+ """
+ Determines if Spear of Adun calldowns are available in no-build missions.
+
+ If turned on, Spear of Adun calldown powers are available in missions specified under "Spear of Adun Presence".
+ If turned off, Spear of Adun calldown powers are unavailable in all no-build missions
+ """
+ display_name = "Spear of Adun Present in No-Build"
+
+
+class SpearOfAdunAutonomouslyCastAbilityPresence(Choice):
+ """
+ Determines availability of Spear of Adun powers, that are autonomously cast.
+ Affects abilities like Reconstruction Beam or Overwatch
+
+ Not Presents: Autocasts are not available.
+ LotV Protoss: Spear of Adun autocasts are only available in LotV main campaign
+ Protoss: Spear od Adun autocasts are available in any Protoss mission
+ Everywhere: Spear od Adun autocasts are available in any mission of any race
+ """
+ display_name = "Spear of Adun Autonomously Cast Powers Presence"
+ option_not_present = 0
+ option_lotv_protoss = 1
+ option_protoss = 2
+ option_everywhere = 3
+ default = option_lotv_protoss
+
+ # Fix case
+ @classmethod
+ def get_option_name(cls, value: int) -> str:
+ if value == SpearOfAdunPresence.option_lotv_protoss:
+ return "LotV Protoss"
+ else:
+ return super().get_option_name(value)
+
+
+class SpearOfAdunAutonomouslyCastPresentInNoBuild(Toggle):
+ """
+ Determines if Spear of Adun autocasts are available in no-build missions.
+
+ If turned on, Spear of Adun autocasts are available in missions specified under "Spear of Adun Autonomously Cast Powers Presence".
+ If turned off, Spear of Adun autocasts are unavailable in all no-build missions
+ """
+ display_name = "Spear of Adun Autonomously Cast Powers Present in No-Build"
+
+
+class GrantStoryTech(Toggle):
+ """
+ If set true, grants special tech required for story mission completion for duration of the mission.
+ Otherwise, you need to find these tech by a normal means as items.
+ Affects story missions like Back in the Saddle and Supreme
+
+ Locked to true if Required Tactics is set to no logic.
+ """
+ display_name = "Grant Story Tech"
+
+
+class GrantStoryLevels(Choice):
+ """
+ If enabled, grants Kerrigan the required minimum levels for the following missions:
+ Supreme: 35
+ The Infinite Cycle: 70
+ The bonus levels only apply during the listed missions, and can exceed the Total Level Cap.
+
+ If disabled, either of these missions is included, and there are not enough levels in the world, generation may fail.
+ To prevent this, either increase the amount of levels in the world, or enable this option.
+
+ If disabled and Required Tactics is set to no logic, this option is forced to Minimum.
+
+ Disabled: Kerrigan does not get bonus levels for these missions,
+ instead the levels must be gained from items or beating missions.
+ Additive: Kerrigan gains bonus levels equal to the mission's required level.
+ Minimum: Kerrigan is either at her real level, or at the mission's required level,
+ depending on which is higher.
+ """
+ display_name = "Grant Story Levels"
+ option_disabled = 0
+ option_additive = 1
+ option_minimum = 2
+ default = option_minimum
+
+
+class TakeOverAIAllies(Toggle):
+ """
+ On maps supporting this feature allows you to take control over an AI Ally.
+ """
+ display_name = "Take Over AI Allies"
+
+
+class LockedItems(ItemSet):
+ """Guarantees that these items will be unlockable"""
+ display_name = "Locked Items"
+
+
+class ExcludedItems(ItemSet):
+ """Guarantees that these items will not be unlockable"""
+ display_name = "Excluded Items"
+
+
+class ExcludedMissions(OptionSet):
+ """Guarantees that these missions will not appear in the campaign
+ Doesn't apply to vanilla mission order.
+ It may be impossible to build a valid campaign if too many missions are excluded."""
+ display_name = "Excluded Missions"
+ valid_keys = {mission.mission_name for mission in SC2Mission}
+
+
+class ExcludeVeryHardMissions(Choice):
+ """
+ Excludes Very Hard missions outside of Epilogue campaign (All-In, Salvation, and all Epilogue missions are considered Very Hard).
+ Doesn't apply to "Vanilla" mission order.
+
+ Default: Not excluded for mission orders "Vanilla Shuffled" or "Grid" with Maximum Campaign Size >= 20,
+ excluded for any other order
+ Yes: Non-Epilogue Very Hard missions are excluded and won't be generated
+ No: Non-Epilogue Very Hard missions can appear normally. Not recommended for too short mission orders.
+
+ See also: Excluded Missions, Enable Epilogue Missions, Maximum Campaign Size
+ """
+ display_name = "Exclude Very Hard Missions"
+ option_default = 0
+ option_true = 1
+ option_false = 2
+
+ @classmethod
+ def get_option_name(cls, value):
+ return ["Default", "Yes", "No"][int(value)]
+
+
+class LocationInclusion(Choice):
+ option_enabled = 0
+ option_resources = 1
+ option_disabled = 2
+
+
+class VanillaLocations(LocationInclusion):
+ """
+ Enables or disables item rewards for completing vanilla objectives.
+ Vanilla objectives are bonus objectives from the vanilla game,
+ along with some additional objectives to balance the missions.
+ Enable these locations for a balanced experience.
+
+ Enabled: All locations fitting into this do their normal rewards
+ Resources: Forces these locations to contain Starting Resources
+ Disabled: Removes item rewards from these locations.
+
+ Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
+ See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
+ """
+ display_name = "Vanilla Locations"
+
+
+class ExtraLocations(LocationInclusion):
+ """
+ Enables or disables item rewards for mission progress and minor objectives.
+ This includes mandatory mission objectives,
+ collecting reinforcements and resource pickups,
+ destroying structures, and overcoming minor challenges.
+ Enables these locations to add more checks and items to your world.
+
+ Enabled: All locations fitting into this do their normal rewards
+ Resources: Forces these locations to contain Starting Resources
+ Disabled: Removes item rewards from these locations.
+
+ Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
+ See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
+ """
+ display_name = "Extra Locations"
+
+
+class ChallengeLocations(LocationInclusion):
+ """
+ Enables or disables item rewards for completing challenge tasks.
+ Challenges are tasks that are more difficult than completing the mission, and are often based on achievements.
+ You might be required to visit the same mission later after getting stronger in order to finish these tasks.
+ Enable these locations to increase the difficulty of completing the multiworld.
+
+ Enabled: All locations fitting into this do their normal rewards
+ Resources: Forces these locations to contain Starting Resources
+ Disabled: Removes item rewards from these locations.
+
+ Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
+ See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
+ """
+ display_name = "Challenge Locations"
+
+
+class MasteryLocations(LocationInclusion):
+ """
+ Enables or disables item rewards for overcoming especially difficult challenges.
+ These challenges are often based on Mastery achievements and Feats of Strength.
+ Enable these locations to add the most difficult checks to the world.
+
+ Enabled: All locations fitting into this do their normal rewards
+ Resources: Forces these locations to contain Starting Resources
+ Disabled: Removes item rewards from these locations.
+
+ Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
+ See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
+ """
+ display_name = "Mastery Locations"
+
+
+class MineralsPerItem(Range):
+ """
+ Configures how many minerals are given per resource item.
+ """
+ display_name = "Minerals Per Item"
+ range_start = 0
+ range_end = 500
+ default = 25
+
+
+class VespenePerItem(Range):
+ """
+ Configures how much vespene gas is given per resource item.
+ """
+ display_name = "Vespene Per Item"
+ range_start = 0
+ range_end = 500
+ default = 25
+
+
+class StartingSupplyPerItem(Range):
+ """
+ Configures how much starting supply per is given per item.
+ """
+ display_name = "Starting Supply Per Item"
+ range_start = 0
+ range_end = 200
+ default = 5
+
+
+@dataclass
+class Starcraft2Options(PerGameCommonOptions):
+ game_difficulty: GameDifficulty
+ game_speed: GameSpeed
+ disable_forced_camera: DisableForcedCamera
+ skip_cutscenes: SkipCutscenes
+ all_in_map: AllInMap
+ mission_order: MissionOrder
+ maximum_campaign_size: MaximumCampaignSize
+ grid_two_start_positions: GridTwoStartPositions
+ player_color_terran_raynor: PlayerColorTerranRaynor
+ player_color_protoss: PlayerColorProtoss
+ player_color_zerg: PlayerColorZerg
+ player_color_zerg_primal: PlayerColorZergPrimal
+ enable_wol_missions: EnableWolMissions
+ enable_prophecy_missions: EnableProphecyMissions
+ enable_hots_missions: EnableHotsMissions
+ enable_lotv_prologue_missions: EnableLotVPrologueMissions
+ enable_lotv_missions: EnableLotVMissions
+ enable_epilogue_missions: EnableEpilogueMissions
+ enable_nco_missions: EnableNCOMissions
+ shuffle_campaigns: ShuffleCampaigns
+ shuffle_no_build: ShuffleNoBuild
+ starter_unit: StarterUnit
+ required_tactics: RequiredTactics
+ ensure_generic_items: EnsureGenericItems
+ min_number_of_upgrades: MinNumberOfUpgrades
+ max_number_of_upgrades: MaxNumberOfUpgrades
+ generic_upgrade_missions: GenericUpgradeMissions
+ generic_upgrade_research: GenericUpgradeResearch
+ generic_upgrade_items: GenericUpgradeItems
+ kerrigan_presence: KerriganPresence
+ kerrigan_levels_per_mission_completed: KerriganLevelsPerMissionCompleted
+ kerrigan_levels_per_mission_completed_cap: KerriganLevelsPerMissionCompletedCap
+ kerrigan_level_item_sum: KerriganLevelItemSum
+ kerrigan_level_item_distribution: KerriganLevelItemDistribution
+ kerrigan_total_level_cap: KerriganTotalLevelCap
+ start_primary_abilities: StartPrimaryAbilities
+ kerrigan_primal_status: KerriganPrimalStatus
+ spear_of_adun_presence: SpearOfAdunPresence
+ spear_of_adun_present_in_no_build: SpearOfAdunPresentInNoBuild
+ spear_of_adun_autonomously_cast_ability_presence: SpearOfAdunAutonomouslyCastAbilityPresence
+ spear_of_adun_autonomously_cast_present_in_no_build: SpearOfAdunAutonomouslyCastPresentInNoBuild
+ grant_story_tech: GrantStoryTech
+ grant_story_levels: GrantStoryLevels
+ take_over_ai_allies: TakeOverAIAllies
+ locked_items: LockedItems
+ excluded_items: ExcludedItems
+ excluded_missions: ExcludedMissions
+ exclude_very_hard_missions: ExcludeVeryHardMissions
+ nco_items: NovaCovertOpsItems
+ bw_items: BroodWarItems
+ ext_items: ExtendedItems
+ vanilla_locations: VanillaLocations
+ extra_locations: ExtraLocations
+ challenge_locations: ChallengeLocations
+ mastery_locations: MasteryLocations
+ minerals_per_item: MineralsPerItem
+ vespene_per_item: VespenePerItem
+ starting_supply_per_item: StartingSupplyPerItem
+
+
+def get_option_value(world: World, name: str) -> Union[int, FrozenSet]:
+ if world is None:
+ field: Field = [class_field for class_field in fields(Starcraft2Options) if class_field.name == name][0]
+ return field.type.default
+
+ player_option = getattr(world.options, name)
+
+ return player_option.value
+
+
+def get_enabled_campaigns(world: World) -> Set[SC2Campaign]:
+ enabled_campaigns = set()
+ if get_option_value(world, "enable_wol_missions"):
+ enabled_campaigns.add(SC2Campaign.WOL)
+ if get_option_value(world, "enable_prophecy_missions"):
+ enabled_campaigns.add(SC2Campaign.PROPHECY)
+ if get_option_value(world, "enable_hots_missions"):
+ enabled_campaigns.add(SC2Campaign.HOTS)
+ if get_option_value(world, "enable_lotv_prologue_missions"):
+ enabled_campaigns.add(SC2Campaign.PROLOGUE)
+ if get_option_value(world, "enable_lotv_missions"):
+ enabled_campaigns.add(SC2Campaign.LOTV)
+ if get_option_value(world, "enable_epilogue_missions"):
+ enabled_campaigns.add(SC2Campaign.EPILOGUE)
+ if get_option_value(world, "enable_nco_missions"):
+ enabled_campaigns.add(SC2Campaign.NCO)
+ return enabled_campaigns
+
+
+def get_disabled_campaigns(world: World) -> Set[SC2Campaign]:
+ all_campaigns = set(SC2Campaign)
+ enabled_campaigns = get_enabled_campaigns(world)
+ disabled_campaigns = all_campaigns.difference(enabled_campaigns)
+ disabled_campaigns.remove(SC2Campaign.GLOBAL)
+ return disabled_campaigns
+
+
+def get_excluded_missions(world: World) -> Set[SC2Mission]:
+ mission_order_type = get_option_value(world, "mission_order")
+ excluded_mission_names = get_option_value(world, "excluded_missions")
+ shuffle_no_build = get_option_value(world, "shuffle_no_build")
+ disabled_campaigns = get_disabled_campaigns(world)
+
+ excluded_missions: Set[SC2Mission] = set([lookup_name_to_mission[name] for name in excluded_mission_names])
+
+ # Excluding Very Hard missions depending on options
+ if (get_option_value(world, "exclude_very_hard_missions") == ExcludeVeryHardMissions.option_true
+ ) or (
+ get_option_value(world, "exclude_very_hard_missions") == ExcludeVeryHardMissions.option_default
+ and (
+ mission_order_type not in [MissionOrder.option_vanilla_shuffled, MissionOrder.option_grid]
+ or (
+ mission_order_type == MissionOrder.option_grid
+ and get_option_value(world, "maximum_campaign_size") < 20
+ )
+ )
+ ):
+ excluded_missions = excluded_missions.union(
+ [mission for mission in SC2Mission if
+ mission.pool == MissionPools.VERY_HARD and mission.campaign != SC2Campaign.EPILOGUE]
+ )
+ # Omitting No-Build missions if not shuffling no-build
+ if not shuffle_no_build:
+ excluded_missions = excluded_missions.union(get_no_build_missions())
+ # Omitting missions not in enabled campaigns
+ for campaign in disabled_campaigns:
+ excluded_missions = excluded_missions.union(campaign_mission_table[campaign])
+
+ return excluded_missions
+
+
+campaign_depending_orders = [
+ MissionOrder.option_vanilla,
+ MissionOrder.option_vanilla_shuffled,
+ MissionOrder.option_mini_campaign
+]
+
+kerrigan_unit_available = [
+ KerriganPresence.option_vanilla,
+]
\ No newline at end of file
diff --git a/worlds/sc2/PoolFilter.py b/worlds/sc2/PoolFilter.py
new file mode 100644
index 000000000000..f5f6faa96d62
--- /dev/null
+++ b/worlds/sc2/PoolFilter.py
@@ -0,0 +1,661 @@
+from typing import Callable, Dict, List, Set, Union, Tuple, Optional
+from BaseClasses import Item, Location
+from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, progressive_if_nco, \
+ progressive_if_ext, spear_of_adun_calldowns, spear_of_adun_castable_passives, nova_equipment
+from .MissionTables import mission_orders, MissionInfo, MissionPools, \
+ get_campaign_goal_priority, campaign_final_mission_locations, campaign_alt_final_mission_locations, \
+ SC2Campaign, SC2Race, SC2CampaignGoalPriority, SC2Mission
+from .Options import get_option_value, MissionOrder, \
+ get_enabled_campaigns, get_disabled_campaigns, RequiredTactics, kerrigan_unit_available, GrantStoryTech, \
+ TakeOverAIAllies, SpearOfAdunPresence, SpearOfAdunAutonomouslyCastAbilityPresence, campaign_depending_orders, \
+ ShuffleCampaigns, get_excluded_missions, ShuffleNoBuild, ExtraLocations, GrantStoryLevels
+from . import ItemNames
+from worlds.AutoWorld import World
+
+# Items with associated upgrades
+UPGRADABLE_ITEMS = {item.parent_item for item in get_full_item_list().values() if item.parent_item}
+
+BARRACKS_UNITS = {
+ ItemNames.MARINE, ItemNames.MEDIC, ItemNames.FIREBAT, ItemNames.MARAUDER,
+ ItemNames.REAPER, ItemNames.GHOST, ItemNames.SPECTRE, ItemNames.HERC,
+}
+FACTORY_UNITS = {
+ ItemNames.HELLION, ItemNames.VULTURE, ItemNames.GOLIATH, ItemNames.DIAMONDBACK,
+ ItemNames.SIEGE_TANK, ItemNames.THOR, ItemNames.PREDATOR, ItemNames.WIDOW_MINE,
+ ItemNames.CYCLONE, ItemNames.WARHOUND,
+}
+STARPORT_UNITS = {
+ ItemNames.MEDIVAC, ItemNames.WRAITH, ItemNames.VIKING, ItemNames.BANSHEE,
+ ItemNames.BATTLECRUISER, ItemNames.HERCULES, ItemNames.SCIENCE_VESSEL, ItemNames.RAVEN,
+ ItemNames.LIBERATOR, ItemNames.VALKYRIE,
+}
+
+
+def filter_missions(world: World) -> Dict[MissionPools, List[SC2Mission]]:
+
+ """
+ Returns a semi-randomly pruned tuple of no-build, easy, medium, and hard mission sets
+ """
+ world: World = world
+ mission_order_type = get_option_value(world, "mission_order")
+ shuffle_no_build = get_option_value(world, "shuffle_no_build")
+ enabled_campaigns = get_enabled_campaigns(world)
+ grant_story_tech = get_option_value(world, "grant_story_tech") == GrantStoryTech.option_true
+ grant_story_levels = get_option_value(world, "grant_story_levels") != GrantStoryLevels.option_disabled
+ extra_locations = get_option_value(world, "extra_locations")
+ excluded_missions: Set[SC2Mission] = get_excluded_missions(world)
+ mission_pools: Dict[MissionPools, List[SC2Mission]] = {}
+ for mission in SC2Mission:
+ if not mission_pools.get(mission.pool):
+ mission_pools[mission.pool] = list()
+ mission_pools[mission.pool].append(mission)
+ # A bit of safeguard:
+ for mission_pool in MissionPools:
+ if not mission_pools.get(mission_pool):
+ mission_pools[mission_pool] = []
+
+ if mission_order_type == MissionOrder.option_vanilla:
+ # Vanilla uses the entire mission pool
+ goal_priorities: Dict[SC2Campaign, SC2CampaignGoalPriority] = {campaign: get_campaign_goal_priority(campaign) for campaign in enabled_campaigns}
+ goal_level = max(goal_priorities.values())
+ candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level]
+ candidate_campaigns.sort(key=lambda it: it.id)
+ goal_campaign = world.random.choice(candidate_campaigns)
+ if campaign_final_mission_locations[goal_campaign] is not None:
+ mission_pools[MissionPools.FINAL] = [campaign_final_mission_locations[goal_campaign].mission]
+ else:
+ mission_pools[MissionPools.FINAL] = [list(campaign_alt_final_mission_locations[goal_campaign].keys())[0]]
+ remove_final_mission_from_other_pools(mission_pools)
+ return mission_pools
+
+ # Finding the goal map
+ goal_mission: Optional[SC2Mission] = None
+ if mission_order_type in campaign_depending_orders:
+ # Prefer long campaigns over shorter ones and harder missions over easier ones
+ goal_priorities = {campaign: get_campaign_goal_priority(campaign, excluded_missions) for campaign in enabled_campaigns}
+ goal_level = max(goal_priorities.values())
+ candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level]
+ candidate_campaigns.sort(key=lambda it: it.id)
+
+ goal_campaign = world.random.choice(candidate_campaigns)
+ primary_goal = campaign_final_mission_locations[goal_campaign]
+ if primary_goal is None or primary_goal.mission in excluded_missions:
+ # No primary goal or its mission is excluded
+ candidate_missions = list(campaign_alt_final_mission_locations[goal_campaign].keys())
+ candidate_missions = [mission for mission in candidate_missions if mission not in excluded_missions]
+ if len(candidate_missions) == 0:
+ raise Exception("There are no valid goal missions. Please exclude fewer missions.")
+ goal_mission = world.random.choice(candidate_missions)
+ else:
+ goal_mission = primary_goal.mission
+ else:
+ # Find one of the missions with the hardest difficulty
+ available_missions: List[SC2Mission] = \
+ [mission for mission in SC2Mission
+ if (mission not in excluded_missions and mission.campaign in enabled_campaigns)]
+ available_missions.sort(key=lambda it: it.id)
+ # Loop over pools, from hardest to easiest
+ for mission_pool in range(MissionPools.VERY_HARD, MissionPools.STARTER - 1, -1):
+ pool_missions: List[SC2Mission] = [mission for mission in available_missions if mission.pool == mission_pool]
+ if pool_missions:
+ goal_mission = world.random.choice(pool_missions)
+ break
+ if goal_mission is None:
+ raise Exception("There are no valid goal missions. Please exclude fewer missions.")
+
+ # Excluding missions
+ for difficulty, mission_pool in mission_pools.items():
+ mission_pools[difficulty] = [mission for mission in mission_pool if mission not in excluded_missions]
+ mission_pools[MissionPools.FINAL] = [goal_mission]
+
+ # Mission pool changes
+ adv_tactics = get_option_value(world, "required_tactics") != RequiredTactics.option_standard
+
+ def move_mission(mission: SC2Mission, current_pool, new_pool):
+ if mission in mission_pools[current_pool]:
+ mission_pools[current_pool].remove(mission)
+ mission_pools[new_pool].append(mission)
+ # WoL
+ if shuffle_no_build == ShuffleNoBuild.option_false or adv_tactics:
+ # Replacing No Build missions with Easy missions
+ # WoL
+ move_mission(SC2Mission.ZERO_HOUR, MissionPools.EASY, MissionPools.STARTER)
+ move_mission(SC2Mission.EVACUATION, MissionPools.EASY, MissionPools.STARTER)
+ move_mission(SC2Mission.DEVILS_PLAYGROUND, MissionPools.EASY, MissionPools.STARTER)
+ # LotV
+ move_mission(SC2Mission.THE_GROWING_SHADOW, MissionPools.EASY, MissionPools.STARTER)
+ move_mission(SC2Mission.THE_SPEAR_OF_ADUN, MissionPools.EASY, MissionPools.STARTER)
+ if extra_locations == ExtraLocations.option_enabled:
+ move_mission(SC2Mission.SKY_SHIELD, MissionPools.EASY, MissionPools.STARTER)
+ # Pushing this to Easy
+ move_mission(SC2Mission.THE_GREAT_TRAIN_ROBBERY, MissionPools.MEDIUM, MissionPools.EASY)
+ if shuffle_no_build == ShuffleNoBuild.option_false:
+ # Pushing Outbreak to Normal, as it cannot be placed as the second mission on Build-Only
+ move_mission(SC2Mission.OUTBREAK, MissionPools.EASY, MissionPools.MEDIUM)
+ # Pushing extra Normal missions to Easy
+ move_mission(SC2Mission.ECHOES_OF_THE_FUTURE, MissionPools.MEDIUM, MissionPools.EASY)
+ move_mission(SC2Mission.CUTTHROAT, MissionPools.MEDIUM, MissionPools.EASY)
+ # Additional changes on Advanced Tactics
+ if adv_tactics:
+ # WoL
+ move_mission(SC2Mission.THE_GREAT_TRAIN_ROBBERY, MissionPools.EASY, MissionPools.STARTER)
+ move_mission(SC2Mission.SMASH_AND_GRAB, MissionPools.EASY, MissionPools.STARTER)
+ move_mission(SC2Mission.THE_MOEBIUS_FACTOR, MissionPools.MEDIUM, MissionPools.EASY)
+ move_mission(SC2Mission.WELCOME_TO_THE_JUNGLE, MissionPools.MEDIUM, MissionPools.EASY)
+ move_mission(SC2Mission.ENGINE_OF_DESTRUCTION, MissionPools.HARD, MissionPools.MEDIUM)
+ # LotV
+ move_mission(SC2Mission.AMON_S_REACH, MissionPools.EASY, MissionPools.STARTER)
+ # Prophecy needs to be adjusted on tiny grid
+ if enabled_campaigns == {SC2Campaign.PROPHECY} and mission_order_type == MissionOrder.option_tiny_grid:
+ move_mission(SC2Mission.A_SINISTER_TURN, MissionPools.MEDIUM, MissionPools.EASY)
+ # Prologue's only valid starter is the goal mission
+ if enabled_campaigns == {SC2Campaign.PROLOGUE} \
+ or mission_order_type in campaign_depending_orders \
+ and get_option_value(world, "shuffle_campaigns") == ShuffleCampaigns.option_false:
+ move_mission(SC2Mission.DARK_WHISPERS, MissionPools.EASY, MissionPools.STARTER)
+ # HotS
+ kerriganless = get_option_value(world, "kerrigan_presence") not in kerrigan_unit_available \
+ or SC2Campaign.HOTS not in enabled_campaigns
+ if adv_tactics:
+ # Medium -> Easy
+ for mission in (SC2Mission.FIRE_IN_THE_SKY, SC2Mission.WAKING_THE_ANCIENT, SC2Mission.CONVICTION):
+ move_mission(mission, MissionPools.MEDIUM, MissionPools.EASY)
+ # Hard -> Medium
+ move_mission(SC2Mission.PHANTOMS_OF_THE_VOID, MissionPools.HARD, MissionPools.MEDIUM)
+ if not kerriganless:
+ # Additional starter mission assuming player starts with minimal anti-air
+ move_mission(SC2Mission.WAKING_THE_ANCIENT, MissionPools.EASY, MissionPools.STARTER)
+ if grant_story_tech:
+ # Additional starter mission if player is granted story tech
+ move_mission(SC2Mission.ENEMY_WITHIN, MissionPools.EASY, MissionPools.STARTER)
+ move_mission(SC2Mission.TEMPLAR_S_RETURN, MissionPools.EASY, MissionPools.STARTER)
+ move_mission(SC2Mission.THE_ESCAPE, MissionPools.MEDIUM, MissionPools.STARTER)
+ move_mission(SC2Mission.IN_THE_ENEMY_S_SHADOW, MissionPools.MEDIUM, MissionPools.STARTER)
+ if (grant_story_tech and grant_story_levels) or kerriganless:
+ # The player has, all the stuff he needs, provided under these settings
+ move_mission(SC2Mission.SUPREME, MissionPools.MEDIUM, MissionPools.STARTER)
+ move_mission(SC2Mission.THE_INFINITE_CYCLE, MissionPools.HARD, MissionPools.STARTER)
+ if get_option_value(world, "take_over_ai_allies") == TakeOverAIAllies.option_true:
+ move_mission(SC2Mission.HARBINGER_OF_OBLIVION, MissionPools.MEDIUM, MissionPools.STARTER)
+ if len(mission_pools[MissionPools.STARTER]) < 2 and not kerriganless or adv_tactics:
+ # Conditionally moving Easy missions to Starter
+ move_mission(SC2Mission.HARVEST_OF_SCREAMS, MissionPools.EASY, MissionPools.STARTER)
+ move_mission(SC2Mission.DOMINATION, MissionPools.EASY, MissionPools.STARTER)
+ if len(mission_pools[MissionPools.STARTER]) < 2:
+ move_mission(SC2Mission.TEMPLAR_S_RETURN, MissionPools.EASY, MissionPools.STARTER)
+ if len(mission_pools[MissionPools.STARTER]) + len(mission_pools[MissionPools.EASY]) < 2:
+ # Flashpoint needs just a few items at start but competent comp at the end
+ move_mission(SC2Mission.FLASHPOINT, MissionPools.HARD, MissionPools.EASY)
+
+ remove_final_mission_from_other_pools(mission_pools)
+ return mission_pools
+
+
+def remove_final_mission_from_other_pools(mission_pools: Dict[MissionPools, List[SC2Mission]]):
+ final_missions = mission_pools[MissionPools.FINAL]
+ for pool, missions in mission_pools.items():
+ if pool == MissionPools.FINAL:
+ continue
+ for final_mission in final_missions:
+ while final_mission in missions:
+ missions.remove(final_mission)
+
+
+def get_item_upgrades(inventory: List[Item], parent_item: Union[Item, str]) -> List[Item]:
+ item_name = parent_item.name if isinstance(parent_item, Item) else parent_item
+ return [
+ inv_item for inv_item in inventory
+ if get_full_item_list()[inv_item.name].parent_item == item_name
+ ]
+
+
+def get_item_quantity(item: Item, world: World):
+ if (not get_option_value(world, "nco_items")) \
+ and SC2Campaign.NCO in get_disabled_campaigns(world) \
+ and item.name in progressive_if_nco:
+ return 1
+ if (not get_option_value(world, "ext_items")) \
+ and item.name in progressive_if_ext:
+ return 1
+ return get_full_item_list()[item.name].quantity
+
+
+def copy_item(item: Item):
+ return Item(item.name, item.classification, item.code, item.player)
+
+
+def num_missions(world: World) -> int:
+ mission_order_type = get_option_value(world, "mission_order")
+ if mission_order_type != MissionOrder.option_grid:
+ mission_order = mission_orders[mission_order_type]()
+ misssions = [mission for campaign in mission_order for mission in mission_order[campaign]]
+ return len(misssions) - 1 # Menu
+ else:
+ mission_pools = filter_missions(world)
+ return sum(len(pool) for _, pool in mission_pools.items())
+
+
+class ValidInventory:
+
+ def has(self, item: str, player: int):
+ return item in self.logical_inventory
+
+ def has_any(self, items: Set[str], player: int):
+ return any(item in self.logical_inventory for item in items)
+
+ def has_all(self, items: Set[str], player: int):
+ return all(item in self.logical_inventory for item in items)
+
+ def has_group(self, item_group: str, player: int, count: int = 1):
+ return False # Deliberately fails here, as item pooling is not aware about mission layout
+
+ def count_group(self, item_name_group: str, player: int) -> int:
+ return 0 # For item filtering assume no missions are beaten
+
+ def count(self, item: str, player: int) -> int:
+ return len([inventory_item for inventory_item in self.logical_inventory if inventory_item == item])
+
+ def has_units_per_structure(self) -> bool:
+ return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
+ len(FACTORY_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
+ len(STARPORT_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure
+
+ def generate_reduced_inventory(self, inventory_size: int, mission_requirements: List[Tuple[str, Callable]]) -> List[Item]:
+ """Attempts to generate a reduced inventory that can fulfill the mission requirements."""
+ inventory: List[Item] = list(self.item_pool)
+ locked_items: List[Item] = list(self.locked_items)
+ item_list = get_full_item_list()
+ self.logical_inventory = [
+ item.name for item in inventory + locked_items + self.existing_items
+ if item_list[item.name].is_important_for_filtering() # Track all Progression items and those with complex rules for filtering
+ ]
+ requirements = mission_requirements
+ parent_items = self.item_children.keys()
+ parent_lookup = {child: parent for parent, children in self.item_children.items() for child in children}
+ minimum_upgrades = get_option_value(self.world, "min_number_of_upgrades")
+
+ def attempt_removal(item: Item) -> bool:
+ inventory.remove(item)
+ # Only run logic checks when removing logic items
+ if item.name in self.logical_inventory:
+ self.logical_inventory.remove(item.name)
+ if not all(requirement(self) for (_, requirement) in mission_requirements):
+ # If item cannot be removed, lock or revert
+ self.logical_inventory.append(item.name)
+ for _ in range(get_item_quantity(item, self.world)):
+ locked_items.append(copy_item(item))
+ return False
+ return True
+
+ # Limit the maximum number of upgrades
+ maxNbUpgrade = get_option_value(self.world, "max_number_of_upgrades")
+ if maxNbUpgrade != -1:
+ unit_avail_upgrades = {}
+ # Needed to take into account locked/existing items
+ unit_nb_upgrades = {}
+ for item in inventory:
+ cItem = item_list[item.name]
+ if item.name in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades:
+ unit_avail_upgrades[item.name] = []
+ unit_nb_upgrades[item.name] = 0
+ elif cItem.parent_item is not None:
+ if cItem.parent_item not in unit_avail_upgrades:
+ unit_avail_upgrades[cItem.parent_item] = [item]
+ unit_nb_upgrades[cItem.parent_item] = 1
+ else:
+ unit_avail_upgrades[cItem.parent_item].append(item)
+ unit_nb_upgrades[cItem.parent_item] += 1
+ # For those two categories, we count them but dont include them in removal
+ for item in locked_items + self.existing_items:
+ cItem = item_list[item.name]
+ if item.name in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades:
+ unit_avail_upgrades[item.name] = []
+ unit_nb_upgrades[item.name] = 0
+ elif cItem.parent_item is not None:
+ if cItem.parent_item not in unit_avail_upgrades:
+ unit_nb_upgrades[cItem.parent_item] = 1
+ else:
+ unit_nb_upgrades[cItem.parent_item] += 1
+ # Making sure that the upgrades being removed is random
+ shuffled_unit_upgrade_list = list(unit_avail_upgrades.keys())
+ self.world.random.shuffle(shuffled_unit_upgrade_list)
+ for unit in shuffled_unit_upgrade_list:
+ while (unit_nb_upgrades[unit] > maxNbUpgrade) \
+ and (len(unit_avail_upgrades[unit]) > 0):
+ itemCandidate = self.world.random.choice(unit_avail_upgrades[unit])
+ success = attempt_removal(itemCandidate)
+ # Whatever it succeed to remove the iventory or it fails and thus
+ # lock it, the upgrade is no longer available for removal
+ unit_avail_upgrades[unit].remove(itemCandidate)
+ if success:
+ unit_nb_upgrades[unit] -= 1
+
+ # Locking minimum upgrades for items that have already been locked/placed when minimum required
+ if minimum_upgrades > 0:
+ known_items = self.existing_items + locked_items
+ known_parents = [item for item in known_items if item in parent_items]
+ for parent in known_parents:
+ child_items = self.item_children[parent]
+ removable_upgrades = [item for item in inventory if item in child_items]
+ locked_upgrade_count = sum(1 if item in child_items else 0 for item in known_items)
+ self.world.random.shuffle(removable_upgrades)
+ while len(removable_upgrades) > 0 and locked_upgrade_count < minimum_upgrades:
+ item_to_lock = removable_upgrades.pop()
+ inventory.remove(item_to_lock)
+ locked_items.append(copy_item(item_to_lock))
+ locked_upgrade_count += 1
+
+ if self.min_units_per_structure > 0 and self.has_units_per_structure():
+ requirements.append(("Minimum units per structure", lambda state: state.has_units_per_structure()))
+
+ # Determining if the full-size inventory can complete campaign
+ failed_locations: List[str] = [location for (location, requirement) in requirements if not requirement(self)]
+ if len(failed_locations) > 0:
+ raise Exception(f"Too many items excluded - couldn't satisfy access rules for the following locations:\n{failed_locations}")
+
+ # Optionally locking generic items
+ generic_items = [item for item in inventory if item.name in second_pass_placeable_items]
+ reserved_generic_percent = get_option_value(self.world, "ensure_generic_items") / 100
+ reserved_generic_amount = int(len(generic_items) * reserved_generic_percent)
+ removable_generic_items = []
+ self.world.random.shuffle(generic_items)
+ for item in generic_items[:reserved_generic_amount]:
+ locked_items.append(copy_item(item))
+ inventory.remove(item)
+ if item.name not in self.logical_inventory and item.name not in self.locked_items:
+ removable_generic_items.append(item)
+
+ # Main cull process
+ unused_items: List[str] = [] # Reusable items for the second pass
+ while len(inventory) + len(locked_items) > inventory_size:
+ if len(inventory) == 0:
+ # There are more items than locations and all of them are already locked due to YAML or logic.
+ # First, drop non-logic generic items to free up space
+ while len(removable_generic_items) > 0 and len(locked_items) > inventory_size:
+ removed_item = removable_generic_items.pop()
+ locked_items.remove(removed_item)
+ # If there still isn't enough space, push locked items into start inventory
+ self.world.random.shuffle(locked_items)
+ while len(locked_items) > inventory_size:
+ item: Item = locked_items.pop()
+ self.multiworld.push_precollected(item)
+ break
+ # Select random item from removable items
+ item = self.world.random.choice(inventory)
+ # Do not remove item if it would drop upgrades below minimum
+ if minimum_upgrades > 0:
+ parent_item = parent_lookup.get(item, None)
+ if parent_item:
+ count = sum(1 if item in self.item_children[parent_item] else 0 for item in inventory + locked_items)
+ if count <= minimum_upgrades:
+ if parent_item in inventory:
+ # Attempt to remove parent instead, if possible
+ item = parent_item
+ else:
+ # Lock remaining upgrades
+ for item in self.item_children[parent_item]:
+ if item in inventory:
+ inventory.remove(item)
+ locked_items.append(copy_item(item))
+ continue
+
+ # Drop child items when removing a parent
+ if item in parent_items:
+ items_to_remove = [item for item in self.item_children[item] if item in inventory]
+ success = attempt_removal(item)
+ if success:
+ while len(items_to_remove) > 0:
+ item_to_remove = items_to_remove.pop()
+ if item_to_remove not in inventory:
+ continue
+ attempt_removal(item_to_remove)
+ else:
+ # Unimportant upgrades may be added again in the second pass
+ if attempt_removal(item):
+ unused_items.append(item.name)
+
+ pool_items: List[str] = [item.name for item in (inventory + locked_items + self.existing_items)]
+ unused_items = [
+ unused_item for unused_item in unused_items
+ if item_list[unused_item].parent_item is None
+ or item_list[unused_item].parent_item in pool_items
+ ]
+
+ # Removing extra dependencies
+ # WoL
+ logical_inventory_set = set(self.logical_inventory)
+ if not spider_mine_sources & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.endswith("(Spider Mine)")]
+ unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Spider Mine)")]
+ if not BARRACKS_UNITS & logical_inventory_set:
+ inventory = [
+ item for item in inventory
+ if not (item.name.startswith(ItemNames.TERRAN_INFANTRY_UPGRADE_PREFIX)
+ or item.name == ItemNames.ORBITAL_STRIKE)]
+ unused_items = [
+ item_name for item_name in unused_items
+ if not (item_name.startswith(
+ ItemNames.TERRAN_INFANTRY_UPGRADE_PREFIX)
+ or item_name == ItemNames.ORBITAL_STRIKE)]
+ if not FACTORY_UNITS & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.startswith(ItemNames.TERRAN_VEHICLE_UPGRADE_PREFIX)]
+ unused_items = [item_name for item_name in unused_items if not item_name.startswith(ItemNames.TERRAN_VEHICLE_UPGRADE_PREFIX)]
+ if not STARPORT_UNITS & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.startswith(ItemNames.TERRAN_SHIP_UPGRADE_PREFIX)]
+ unused_items = [item_name for item_name in unused_items if not item_name.startswith(ItemNames.TERRAN_SHIP_UPGRADE_PREFIX)]
+ # HotS
+ # Baneling without sources => remove Baneling and upgrades
+ if (ItemNames.ZERGLING_BANELING_ASPECT in self.logical_inventory
+ and ItemNames.ZERGLING not in self.logical_inventory
+ and ItemNames.KERRIGAN_SPAWN_BANELINGS not in self.logical_inventory
+ ):
+ inventory = [item for item in inventory if item.name != ItemNames.ZERGLING_BANELING_ASPECT]
+ inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.ZERGLING_BANELING_ASPECT]
+ unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ZERGLING_BANELING_ASPECT]
+ unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.ZERGLING_BANELING_ASPECT]
+ # Spawn Banelings without Zergling => remove Baneling unit, keep upgrades except macro ones
+ if (ItemNames.ZERGLING_BANELING_ASPECT in self.logical_inventory
+ and ItemNames.ZERGLING not in self.logical_inventory
+ and ItemNames.KERRIGAN_SPAWN_BANELINGS in self.logical_inventory
+ ):
+ inventory = [item for item in inventory if item.name != ItemNames.ZERGLING_BANELING_ASPECT]
+ inventory = [item for item in inventory if item.name != ItemNames.BANELING_RAPID_METAMORPH]
+ unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ZERGLING_BANELING_ASPECT]
+ unused_items = [item_name for item_name in unused_items if item_name != ItemNames.BANELING_RAPID_METAMORPH]
+ if not {ItemNames.MUTALISK, ItemNames.CORRUPTOR, ItemNames.SCOURGE} & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)]
+ locked_items = [item for item in locked_items if not item.name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)]
+ unused_items = [item_name for item_name in unused_items if not item_name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)]
+ # T3 items removal rules - remove morph and its upgrades if the basic unit isn't in
+ if not {ItemNames.MUTALISK, ItemNames.CORRUPTOR} & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.endswith("(Mutalisk/Corruptor)")]
+ inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT]
+ inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT]
+ inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT]
+ inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT]
+ unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Mutalisk/Corruptor)")]
+ unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT]
+ unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT]
+ unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT]
+ unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT]
+ if ItemNames.ROACH not in logical_inventory_set:
+ inventory = [item for item in inventory if item.name != ItemNames.ROACH_RAVAGER_ASPECT]
+ inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.ROACH_RAVAGER_ASPECT]
+ unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ROACH_RAVAGER_ASPECT]
+ unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.ROACH_RAVAGER_ASPECT]
+ if ItemNames.HYDRALISK not in logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.endswith("(Hydralisk)")]
+ inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.HYDRALISK_LURKER_ASPECT]
+ inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.HYDRALISK_IMPALER_ASPECT]
+ unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Hydralisk)")]
+ unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.HYDRALISK_LURKER_ASPECT]
+ unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.HYDRALISK_IMPALER_ASPECT]
+ # LotV
+ # Shared unit upgrades between several units
+ if not {ItemNames.STALKER, ItemNames.INSTIGATOR, ItemNames.SLAYER} & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.endswith("(Stalker/Instigator/Slayer)")]
+ unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Stalker/Instigator/Slayer)")]
+ if not {ItemNames.PHOENIX, ItemNames.MIRAGE} & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.endswith("(Phoenix/Mirage)")]
+ unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Phoenix/Mirage)")]
+ if not {ItemNames.VOID_RAY, ItemNames.DESTROYER} & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.endswith("(Void Ray/Destroyer)")]
+ unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Void Ray/Destroyer)")]
+ if not {ItemNames.IMMORTAL, ItemNames.ANNIHILATOR} & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.endswith("(Immortal/Annihilator)")]
+ unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Immortal/Annihilator)")]
+ if not {ItemNames.DARK_TEMPLAR, ItemNames.AVENGER, ItemNames.BLOOD_HUNTER} & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.endswith("(Dark Templar/Avenger/Blood Hunter)")]
+ unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Dark Templar/Avenger/Blood Hunter)")]
+ if not {ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.ASCENDANT, ItemNames.DARK_TEMPLAR} & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.endswith("(Archon)")]
+ unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Archon)")]
+ logical_inventory_set.difference_update([item_name for item_name in logical_inventory_set if item_name.endswith("(Archon)")])
+ if not {ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.ARCHON_HIGH_ARCHON} & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.endswith("(High Templar/Signifier)")]
+ unused_items = [item_name for item_name in unused_items if not item_name.endswith("(High Templar/Signifier)")]
+ if ItemNames.SUPPLICANT not in logical_inventory_set:
+ inventory = [item for item in inventory if item.name != ItemNames.ASCENDANT_POWER_OVERWHELMING]
+ unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ASCENDANT_POWER_OVERWHELMING]
+ if not {ItemNames.DARK_ARCHON, ItemNames.DARK_TEMPLAR_DARK_ARCHON_MELD} & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.endswith("(Dark Archon)")]
+ unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Dark Archon)")]
+ if not {ItemNames.SENTRY, ItemNames.ENERGIZER, ItemNames.HAVOC} & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.endswith("(Sentry/Energizer/Havoc)")]
+ unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Sentry/Energizer/Havoc)")]
+ if not {ItemNames.SENTRY, ItemNames.ENERGIZER, ItemNames.HAVOC, ItemNames.SHIELD_BATTERY} & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.endswith("(Sentry/Energizer/Havoc/Shield Battery)")]
+ unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Sentry/Energizer/Havoc/Shield Battery)")]
+ if not {ItemNames.ZEALOT, ItemNames.CENTURION, ItemNames.SENTINEL} & logical_inventory_set:
+ inventory = [item for item in inventory if not item.name.endswith("(Zealot/Sentinel/Centurion)")]
+ unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Zealot/Sentinel/Centurion)")]
+ # Static defense upgrades only if static defense present
+ if not {ItemNames.PHOTON_CANNON, ItemNames.KHAYDARIN_MONOLITH, ItemNames.NEXUS_OVERCHARGE, ItemNames.SHIELD_BATTERY} & logical_inventory_set:
+ inventory = [item for item in inventory if item.name != ItemNames.ENHANCED_TARGETING]
+ unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ENHANCED_TARGETING]
+ if not {ItemNames.PHOTON_CANNON, ItemNames.KHAYDARIN_MONOLITH, ItemNames.NEXUS_OVERCHARGE} & logical_inventory_set:
+ inventory = [item for item in inventory if item.name != ItemNames.OPTIMIZED_ORDNANCE]
+ unused_items = [item_name for item_name in unused_items if item_name != ItemNames.OPTIMIZED_ORDNANCE]
+
+ # Cull finished, adding locked items back into inventory
+ inventory += locked_items
+
+ # Replacing empty space with generically useful items
+ replacement_items = [item for item in self.item_pool
+ if (item not in inventory
+ and item not in self.locked_items
+ and (
+ item.name in second_pass_placeable_items
+ or item.name in unused_items))]
+ self.world.random.shuffle(replacement_items)
+ while len(inventory) < inventory_size and len(replacement_items) > 0:
+ item = replacement_items.pop()
+ inventory.append(item)
+
+ return inventory
+
+ def __init__(self, world: World ,
+ item_pool: List[Item], existing_items: List[Item], locked_items: List[Item],
+ used_races: Set[SC2Race], nova_equipment_used: bool):
+ self.multiworld = world.multiworld
+ self.player = world.player
+ self.world: World = world
+ self.logical_inventory = list()
+ self.locked_items = locked_items[:]
+ self.existing_items = existing_items
+ soa_presence = get_option_value(world, "spear_of_adun_presence")
+ soa_autocast_presence = get_option_value(world, "spear_of_adun_autonomously_cast_ability_presence")
+ # Initial filter of item pool
+ self.item_pool = []
+ item_quantities: dict[str, int] = dict()
+ # Inventory restrictiveness based on number of missions with checks
+ mission_count = num_missions(world)
+ self.min_units_per_structure = int(mission_count / 7)
+ min_upgrades = 1 if mission_count < 10 else 2
+ for item in item_pool:
+ item_info = get_full_item_list()[item.name]
+ if item_info.race != SC2Race.ANY and item_info.race not in used_races:
+ if soa_presence == SpearOfAdunPresence.option_everywhere \
+ and item.name in spear_of_adun_calldowns:
+ # Add SoA powers regardless of used races as it's present everywhere
+ self.item_pool.append(item)
+ if soa_autocast_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_everywhere \
+ and item.name in spear_of_adun_castable_passives:
+ self.item_pool.append(item)
+ # Drop any item belonging to a race not used in the campaign
+ continue
+ if item.name in nova_equipment and not nova_equipment_used:
+ # Drop Nova equipment if there's no NCO mission generated
+ continue
+ if item_info.type == "Upgrade":
+ # Locking upgrades based on mission duration
+ if item.name not in item_quantities:
+ item_quantities[item.name] = 0
+ item_quantities[item.name] += 1
+ if item_quantities[item.name] <= min_upgrades:
+ self.locked_items.append(item)
+ else:
+ self.item_pool.append(item)
+ elif item_info.type == "Goal":
+ self.locked_items.append(item)
+ else:
+ self.item_pool.append(item)
+ self.item_children: Dict[Item, List[Item]] = dict()
+ for item in self.item_pool + locked_items + existing_items:
+ if item.name in UPGRADABLE_ITEMS:
+ self.item_children[item] = get_item_upgrades(self.item_pool, item)
+
+
+def filter_items(world: World, mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]], location_cache: List[Location],
+ item_pool: List[Item], existing_items: List[Item], locked_items: List[Item]) -> List[Item]:
+ """
+ Returns a semi-randomly pruned set of items based on number of available locations.
+ The returned inventory must be capable of logically accessing every location in the world.
+ """
+ open_locations = [location for location in location_cache if location.item is None]
+ inventory_size = len(open_locations)
+ used_races = get_used_races(mission_req_table, world)
+ nova_equipment_used = is_nova_equipment_used(mission_req_table)
+ mission_requirements = [(location.name, location.access_rule) for location in location_cache]
+ valid_inventory = ValidInventory(world, item_pool, existing_items, locked_items, used_races, nova_equipment_used)
+
+ valid_items = valid_inventory.generate_reduced_inventory(inventory_size, mission_requirements)
+ return valid_items
+
+
+def get_used_races(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]], world: World) -> Set[SC2Race]:
+ grant_story_tech = get_option_value(world, "grant_story_tech")
+ take_over_ai_allies = get_option_value(world, "take_over_ai_allies")
+ kerrigan_presence = get_option_value(world, "kerrigan_presence") in kerrigan_unit_available \
+ and SC2Campaign.HOTS in get_enabled_campaigns(world)
+ missions = missions_in_mission_table(mission_req_table)
+
+ # By missions
+ races = set([mission.race for mission in missions])
+
+ # Conditionally logic-less no-builds (They're set to SC2Race.ANY):
+ if grant_story_tech == GrantStoryTech.option_false:
+ if SC2Mission.ENEMY_WITHIN in missions:
+ # Zerg units need to be unlocked
+ races.add(SC2Race.ZERG)
+ if kerrigan_presence \
+ and not missions.isdisjoint({SC2Mission.BACK_IN_THE_SADDLE, SC2Mission.SUPREME, SC2Mission.CONVICTION, SC2Mission.THE_INFINITE_CYCLE}):
+ # You need some Kerrigan abilities (they're granted if Kerriganless or story tech granted)
+ races.add(SC2Race.ZERG)
+
+ # If you take over the AI Ally, you need to have its race stuff
+ if take_over_ai_allies == TakeOverAIAllies.option_true \
+ and not missions.isdisjoint({SC2Mission.THE_RECKONING}):
+ # Jimmy in The Reckoning
+ races.add(SC2Race.TERRAN)
+
+ return races
+
+def is_nova_equipment_used(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]]) -> bool:
+ missions = missions_in_mission_table(mission_req_table)
+ return any([mission.campaign == SC2Campaign.NCO for mission in missions])
+
+
+def missions_in_mission_table(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]]) -> Set[SC2Mission]:
+ return set([mission.mission for campaign_missions in mission_req_table.values() for mission in
+ campaign_missions.values()])
diff --git a/worlds/sc2/Regions.py b/worlds/sc2/Regions.py
new file mode 100644
index 000000000000..84830a9a32bd
--- /dev/null
+++ b/worlds/sc2/Regions.py
@@ -0,0 +1,691 @@
+from typing import List, Dict, Tuple, Optional, Callable, NamedTuple, Union
+import math
+
+from BaseClasses import MultiWorld, Region, Entrance, Location, CollectionState
+from .Locations import LocationData
+from .Options import get_option_value, MissionOrder, get_enabled_campaigns, campaign_depending_orders, \
+ GridTwoStartPositions
+from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, \
+ MissionPools, SC2Campaign, get_goal_location, SC2Mission, MissionConnection
+from .PoolFilter import filter_missions
+from worlds.AutoWorld import World
+
+
+class SC2MissionSlot(NamedTuple):
+ campaign: SC2Campaign
+ slot: Union[MissionPools, SC2Mission, None]
+
+
+def create_regions(
+ world: World, locations: Tuple[LocationData, ...], location_cache: List[Location]
+) -> Tuple[Dict[SC2Campaign, Dict[str, MissionInfo]], int, str]:
+ """
+ Creates region connections by calling the multiworld's `connect()` methods
+ Returns a 3-tuple containing:
+ * dict[SC2Campaign, Dict[str, MissionInfo]] mapping a campaign and mission name to its data
+ * int The number of missions in the world
+ * str The name of the goal location
+ """
+ mission_order_type: int = get_option_value(world, "mission_order")
+
+ if mission_order_type == MissionOrder.option_vanilla:
+ return create_vanilla_regions(world, locations, location_cache)
+ elif mission_order_type == MissionOrder.option_grid:
+ return create_grid_regions(world, locations, location_cache)
+ else:
+ return create_structured_regions(world, locations, location_cache, mission_order_type)
+
+def create_vanilla_regions(
+ world: World,
+ locations: Tuple[LocationData, ...],
+ location_cache: List[Location],
+) -> Tuple[Dict[SC2Campaign, Dict[str, MissionInfo]], int, str]:
+ locations_per_region = get_locations_per_region(locations)
+ regions = [create_region(world, locations_per_region, location_cache, "Menu")]
+
+ mission_pools: Dict[MissionPools, List[SC2Mission]] = filter_missions(world)
+ final_mission = mission_pools[MissionPools.FINAL][0]
+
+ enabled_campaigns = get_enabled_campaigns(world)
+ names: Dict[str, int] = {}
+
+ # Generating all regions and locations for each enabled campaign
+ for campaign in enabled_campaigns:
+ for region_name in vanilla_mission_req_table[campaign].keys():
+ regions.append(create_region(world, locations_per_region, location_cache, region_name))
+ world.multiworld.regions += regions
+ vanilla_mission_reqs = {campaign: missions for campaign, missions in vanilla_mission_req_table.items() if campaign in enabled_campaigns}
+
+ def wol_cleared_missions(state: CollectionState, mission_count: int) -> bool:
+ return state.has_group("WoL Missions", world.player, mission_count)
+
+ player: int = world.player
+ if SC2Campaign.WOL in enabled_campaigns:
+ connect(world, names, 'Menu', 'Liberation Day')
+ connect(world, names, 'Liberation Day', 'The Outlaws',
+ lambda state: state.has("Beat Liberation Day", player))
+ connect(world, names, 'The Outlaws', 'Zero Hour',
+ lambda state: state.has("Beat The Outlaws", player))
+ connect(world, names, 'Zero Hour', 'Evacuation',
+ lambda state: state.has("Beat Zero Hour", player))
+ connect(world, names, 'Evacuation', 'Outbreak',
+ lambda state: state.has("Beat Evacuation", player))
+ connect(world, names, "Outbreak", "Safe Haven",
+ lambda state: wol_cleared_missions(state, 7) and state.has("Beat Outbreak", player))
+ connect(world, names, "Outbreak", "Haven's Fall",
+ lambda state: wol_cleared_missions(state, 7) and state.has("Beat Outbreak", player))
+ connect(world, names, 'Zero Hour', 'Smash and Grab',
+ lambda state: state.has("Beat Zero Hour", player))
+ connect(world, names, 'Smash and Grab', 'The Dig',
+ lambda state: wol_cleared_missions(state, 8) and state.has("Beat Smash and Grab", player))
+ connect(world, names, 'The Dig', 'The Moebius Factor',
+ lambda state: wol_cleared_missions(state, 11) and state.has("Beat The Dig", player))
+ connect(world, names, 'The Moebius Factor', 'Supernova',
+ lambda state: wol_cleared_missions(state, 14) and state.has("Beat The Moebius Factor", player))
+ connect(world, names, 'Supernova', 'Maw of the Void',
+ lambda state: state.has("Beat Supernova", player))
+ connect(world, names, 'Zero Hour', "Devil's Playground",
+ lambda state: wol_cleared_missions(state, 4) and state.has("Beat Zero Hour", player))
+ connect(world, names, "Devil's Playground", 'Welcome to the Jungle',
+ lambda state: state.has("Beat Devil's Playground", player))
+ connect(world, names, "Welcome to the Jungle", 'Breakout',
+ lambda state: wol_cleared_missions(state, 8) and state.has("Beat Welcome to the Jungle", player))
+ connect(world, names, "Welcome to the Jungle", 'Ghost of a Chance',
+ lambda state: wol_cleared_missions(state, 8) and state.has("Beat Welcome to the Jungle", player))
+ connect(world, names, "Zero Hour", 'The Great Train Robbery',
+ lambda state: wol_cleared_missions(state, 6) and state.has("Beat Zero Hour", player))
+ connect(world, names, 'The Great Train Robbery', 'Cutthroat',
+ lambda state: state.has("Beat The Great Train Robbery", player))
+ connect(world, names, 'Cutthroat', 'Engine of Destruction',
+ lambda state: state.has("Beat Cutthroat", player))
+ connect(world, names, 'Engine of Destruction', 'Media Blitz',
+ lambda state: state.has("Beat Engine of Destruction", player))
+ connect(world, names, 'Media Blitz', 'Piercing the Shroud',
+ lambda state: state.has("Beat Media Blitz", player))
+ connect(world, names, 'Maw of the Void', 'Gates of Hell',
+ lambda state: state.has("Beat Maw of the Void", player))
+ connect(world, names, 'Gates of Hell', 'Belly of the Beast',
+ lambda state: state.has("Beat Gates of Hell", player))
+ connect(world, names, 'Gates of Hell', 'Shatter the Sky',
+ lambda state: state.has("Beat Gates of Hell", player))
+ connect(world, names, 'Gates of Hell', 'All-In',
+ lambda state: state.has('Beat Gates of Hell', player) and (
+ state.has('Beat Shatter the Sky', player) or state.has('Beat Belly of the Beast', player)))
+
+ if SC2Campaign.PROPHECY in enabled_campaigns:
+ if SC2Campaign.WOL in enabled_campaigns:
+ connect(world, names, 'The Dig', 'Whispers of Doom',
+ lambda state: state.has("Beat The Dig", player)),
+ else:
+ vanilla_mission_reqs[SC2Campaign.PROPHECY] = vanilla_mission_reqs[SC2Campaign.PROPHECY].copy()
+ vanilla_mission_reqs[SC2Campaign.PROPHECY][SC2Mission.WHISPERS_OF_DOOM.mission_name] = MissionInfo(
+ SC2Mission.WHISPERS_OF_DOOM, [], SC2Mission.WHISPERS_OF_DOOM.area)
+ connect(world, names, 'Menu', 'Whispers of Doom'),
+ connect(world, names, 'Whispers of Doom', 'A Sinister Turn',
+ lambda state: state.has("Beat Whispers of Doom", player))
+ connect(world, names, 'A Sinister Turn', 'Echoes of the Future',
+ lambda state: state.has("Beat A Sinister Turn", player))
+ connect(world, names, 'Echoes of the Future', 'In Utter Darkness',
+ lambda state: state.has("Beat Echoes of the Future", player))
+
+ if SC2Campaign.HOTS in enabled_campaigns:
+ connect(world, names, 'Menu', 'Lab Rat'),
+ connect(world, names, 'Lab Rat', 'Back in the Saddle',
+ lambda state: state.has("Beat Lab Rat", player)),
+ connect(world, names, 'Back in the Saddle', 'Rendezvous',
+ lambda state: state.has("Beat Back in the Saddle", player)),
+ connect(world, names, 'Rendezvous', 'Harvest of Screams',
+ lambda state: state.has("Beat Rendezvous", player)),
+ connect(world, names, 'Harvest of Screams', 'Shoot the Messenger',
+ lambda state: state.has("Beat Harvest of Screams", player)),
+ connect(world, names, 'Shoot the Messenger', 'Enemy Within',
+ lambda state: state.has("Beat Shoot the Messenger", player)),
+ connect(world, names, 'Rendezvous', 'Domination',
+ lambda state: state.has("Beat Rendezvous", player)),
+ connect(world, names, 'Domination', 'Fire in the Sky',
+ lambda state: state.has("Beat Domination", player)),
+ connect(world, names, 'Fire in the Sky', 'Old Soldiers',
+ lambda state: state.has("Beat Fire in the Sky", player)),
+ connect(world, names, 'Old Soldiers', 'Waking the Ancient',
+ lambda state: state.has("Beat Old Soldiers", player)),
+ connect(world, names, 'Enemy Within', 'Waking the Ancient',
+ lambda state: state.has("Beat Enemy Within", player)),
+ connect(world, names, 'Waking the Ancient', 'The Crucible',
+ lambda state: state.has("Beat Waking the Ancient", player)),
+ connect(world, names, 'The Crucible', 'Supreme',
+ lambda state: state.has("Beat The Crucible", player)),
+ connect(world, names, 'Supreme', 'Infested',
+ lambda state: state.has("Beat Supreme", player) and
+ state.has("Beat Old Soldiers", player) and
+ state.has("Beat Enemy Within", player)),
+ connect(world, names, 'Infested', 'Hand of Darkness',
+ lambda state: state.has("Beat Infested", player)),
+ connect(world, names, 'Hand of Darkness', 'Phantoms of the Void',
+ lambda state: state.has("Beat Hand of Darkness", player)),
+ connect(world, names, 'Supreme', 'With Friends Like These',
+ lambda state: state.has("Beat Supreme", player) and
+ state.has("Beat Old Soldiers", player) and
+ state.has("Beat Enemy Within", player)),
+ connect(world, names, 'With Friends Like These', 'Conviction',
+ lambda state: state.has("Beat With Friends Like These", player)),
+ connect(world, names, 'Conviction', 'Planetfall',
+ lambda state: state.has("Beat Conviction", player) and
+ state.has("Beat Phantoms of the Void", player)),
+ connect(world, names, 'Planetfall', 'Death From Above',
+ lambda state: state.has("Beat Planetfall", player)),
+ connect(world, names, 'Death From Above', 'The Reckoning',
+ lambda state: state.has("Beat Death From Above", player)),
+
+ if SC2Campaign.PROLOGUE in enabled_campaigns:
+ connect(world, names, "Menu", "Dark Whispers")
+ connect(world, names, "Dark Whispers", "Ghosts in the Fog",
+ lambda state: state.has("Beat Dark Whispers", player))
+ connect(world, names, "Ghosts in the Fog", "Evil Awoken",
+ lambda state: state.has("Beat Ghosts in the Fog", player))
+
+ if SC2Campaign.LOTV in enabled_campaigns:
+ connect(world, names, "Menu", "For Aiur!")
+ connect(world, names, "For Aiur!", "The Growing Shadow",
+ lambda state: state.has("Beat For Aiur!", player)),
+ connect(world, names, "The Growing Shadow", "The Spear of Adun",
+ lambda state: state.has("Beat The Growing Shadow", player)),
+ connect(world, names, "The Spear of Adun", "Sky Shield",
+ lambda state: state.has("Beat The Spear of Adun", player)),
+ connect(world, names, "Sky Shield", "Brothers in Arms",
+ lambda state: state.has("Beat Sky Shield", player)),
+ connect(world, names, "Brothers in Arms", "Forbidden Weapon",
+ lambda state: state.has("Beat Brothers in Arms", player)),
+ connect(world, names, "The Spear of Adun", "Amon's Reach",
+ lambda state: state.has("Beat The Spear of Adun", player)),
+ connect(world, names, "Amon's Reach", "Last Stand",
+ lambda state: state.has("Beat Amon's Reach", player)),
+ connect(world, names, "Last Stand", "Forbidden Weapon",
+ lambda state: state.has("Beat Last Stand", player)),
+ connect(world, names, "Forbidden Weapon", "Temple of Unification",
+ lambda state: state.has("Beat Brothers in Arms", player)
+ and state.has("Beat Last Stand", player)
+ and state.has("Beat Forbidden Weapon", player)),
+ connect(world, names, "Temple of Unification", "The Infinite Cycle",
+ lambda state: state.has("Beat Temple of Unification", player)),
+ connect(world, names, "The Infinite Cycle", "Harbinger of Oblivion",
+ lambda state: state.has("Beat The Infinite Cycle", player)),
+ connect(world, names, "Harbinger of Oblivion", "Unsealing the Past",
+ lambda state: state.has("Beat Harbinger of Oblivion", player)),
+ connect(world, names, "Unsealing the Past", "Purification",
+ lambda state: state.has("Beat Unsealing the Past", player)),
+ connect(world, names, "Purification", "Templar's Charge",
+ lambda state: state.has("Beat Purification", player)),
+ connect(world, names, "Harbinger of Oblivion", "Steps of the Rite",
+ lambda state: state.has("Beat Harbinger of Oblivion", player)),
+ connect(world, names, "Steps of the Rite", "Rak'Shir",
+ lambda state: state.has("Beat Steps of the Rite", player)),
+ connect(world, names, "Rak'Shir", "Templar's Charge",
+ lambda state: state.has("Beat Rak'Shir", player)),
+ connect(world, names, "Templar's Charge", "Templar's Return",
+ lambda state: state.has("Beat Purification", player)
+ and state.has("Beat Rak'Shir", player)
+ and state.has("Beat Templar's Charge", player)),
+ connect(world, names, "Templar's Return", "The Host",
+ lambda state: state.has("Beat Templar's Return", player)),
+ connect(world, names, "The Host", "Salvation",
+ lambda state: state.has("Beat The Host", player)),
+
+ if SC2Campaign.EPILOGUE in enabled_campaigns:
+ # TODO: Make this aware about excluded campaigns
+ connect(world, names, "Salvation", "Into the Void",
+ lambda state: state.has("Beat Salvation", player)
+ and state.has("Beat The Reckoning", player)
+ and state.has("Beat All-In", player)),
+ connect(world, names, "Into the Void", "The Essence of Eternity",
+ lambda state: state.has("Beat Into the Void", player)),
+ connect(world, names, "The Essence of Eternity", "Amon's Fall",
+ lambda state: state.has("Beat The Essence of Eternity", player)),
+
+ if SC2Campaign.NCO in enabled_campaigns:
+ connect(world, names, "Menu", "The Escape")
+ connect(world, names, "The Escape", "Sudden Strike",
+ lambda state: state.has("Beat The Escape", player))
+ connect(world, names, "Sudden Strike", "Enemy Intelligence",
+ lambda state: state.has("Beat Sudden Strike", player))
+ connect(world, names, "Enemy Intelligence", "Trouble In Paradise",
+ lambda state: state.has("Beat Enemy Intelligence", player))
+ connect(world, names, "Trouble In Paradise", "Night Terrors",
+ lambda state: state.has("Beat Trouble In Paradise", player))
+ connect(world, names, "Night Terrors", "Flashpoint",
+ lambda state: state.has("Beat Night Terrors", player))
+ connect(world, names, "Flashpoint", "In the Enemy's Shadow",
+ lambda state: state.has("Beat Flashpoint", player))
+ connect(world, names, "In the Enemy's Shadow", "Dark Skies",
+ lambda state: state.has("Beat In the Enemy's Shadow", player))
+ connect(world, names, "Dark Skies", "End Game",
+ lambda state: state.has("Beat Dark Skies", player))
+
+ goal_location = get_goal_location(final_mission)
+ assert goal_location, f"Unable to find a goal location for mission {final_mission}"
+ setup_final_location(goal_location, location_cache)
+
+ return (vanilla_mission_reqs, final_mission.id, goal_location)
+
+
+def create_grid_regions(
+ world: World,
+ locations: Tuple[LocationData, ...],
+ location_cache: List[Location],
+) -> Tuple[Dict[SC2Campaign, Dict[str, MissionInfo]], int, str]:
+ locations_per_region = get_locations_per_region(locations)
+
+ mission_pools = filter_missions(world)
+ final_mission = mission_pools[MissionPools.FINAL][0]
+
+ mission_pool = [mission for mission_pool in mission_pools.values() for mission in mission_pool]
+
+ num_missions = min(len(mission_pool), get_option_value(world, "maximum_campaign_size"))
+ remove_top_left: bool = get_option_value(world, "grid_two_start_positions") == GridTwoStartPositions.option_true
+
+ regions = [create_region(world, locations_per_region, location_cache, "Menu")]
+ names: Dict[str, int] = {}
+ missions: Dict[Tuple[int, int], SC2Mission] = {}
+
+ grid_size_x, grid_size_y, num_corners_to_remove = get_grid_dimensions(num_missions + remove_top_left)
+ # pick missions in order along concentric diagonals
+ # each diagonal will have the same difficulty
+ # this keeps long sides from possibly stealing lower-difficulty missions from future columns
+ num_diagonals = grid_size_x + grid_size_y - 1
+ diagonal_difficulty = MissionPools.STARTER
+ missions_to_add = mission_pools[MissionPools.STARTER]
+ for diagonal in range(num_diagonals):
+ if diagonal == num_diagonals - 1:
+ diagonal_difficulty = MissionPools.FINAL
+ grid_coords = (grid_size_x-1, grid_size_y-1)
+ missions[grid_coords] = final_mission
+ break
+ if diagonal == 0 and remove_top_left:
+ continue
+ diagonal_length = min(diagonal + 1, num_diagonals - diagonal, grid_size_x, grid_size_y)
+ if len(missions_to_add) < diagonal_length:
+ raise Exception(f"There are not enough {diagonal_difficulty.name} missions to fill the campaign. Please exclude fewer missions.")
+ for i in range(diagonal_length):
+ # (0,0) + (0,1)*diagonal + (1,-1)*i + (1,-1)*max(diagonal - grid_size_y + 1, 0)
+ grid_coords = (i + max(diagonal - grid_size_y + 1, 0), diagonal - i - max(diagonal - grid_size_y + 1, 0))
+ if grid_coords == (grid_size_x - 1, 0) and num_corners_to_remove >= 2:
+ pass
+ elif grid_coords == (0, grid_size_y - 1) and num_corners_to_remove >= 1:
+ pass
+ else:
+ mission_index = world.random.randint(0, len(missions_to_add) - 1)
+ missions[grid_coords] = missions_to_add.pop(mission_index)
+
+ if diagonal_difficulty < MissionPools.VERY_HARD:
+ diagonal_difficulty = MissionPools(diagonal_difficulty.value + 1)
+ missions_to_add.extend(mission_pools[diagonal_difficulty])
+
+ # Generating regions and locations from selected missions
+ for x in range(grid_size_x):
+ for y in range(grid_size_y):
+ if missions.get((x, y)):
+ regions.append(create_region(world, locations_per_region, location_cache, missions[(x, y)].mission_name))
+ world.multiworld.regions += regions
+
+ # This pattern is horrifying, why are we using the dict as an ordered dict???
+ slot_map: Dict[Tuple[int, int], int] = {}
+ for index, coords in enumerate(missions):
+ slot_map[coords] = index + 1
+
+ mission_req_table: Dict[str, MissionInfo] = {}
+ for coords, mission in missions.items():
+ prepend_vertical = 0
+ if not mission:
+ continue
+ connections: List[MissionConnection] = []
+ if coords == (0, 0) or (remove_top_left and sum(coords) == 1):
+ # Connect to the "Menu" starting region
+ connect(world, names, "Menu", mission.mission_name)
+ else:
+ for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)):
+ connected_coords = (coords[0] + dx, coords[1] + dy)
+ if connected_coords in missions:
+ # connections.append(missions[connected_coords])
+ connections.append(MissionConnection(slot_map[connected_coords]))
+ connect(world, names, missions[connected_coords].mission_name, mission.mission_name,
+ make_grid_connect_rule(missions, connected_coords, world.player),
+ )
+ if coords[1] == 1 and not missions.get((coords[0], 0)):
+ prepend_vertical = 1
+ mission_req_table[mission.mission_name] = MissionInfo(
+ mission,
+ connections,
+ category=f'_{coords[0] + 1}',
+ or_requirements=True,
+ ui_vertical_padding=prepend_vertical,
+ )
+
+ final_mission_id = final_mission.id
+ # Changing the completion condition for alternate final missions into an event
+ final_location = get_goal_location(final_mission)
+ setup_final_location(final_location, location_cache)
+
+ return {SC2Campaign.GLOBAL: mission_req_table}, final_mission_id, final_location
+
+
+def make_grid_connect_rule(
+ missions: Dict[Tuple[int, int], SC2Mission],
+ connected_coords: Tuple[int, int],
+ player: int
+) -> Callable[[CollectionState], bool]:
+ return lambda state: state.has(f"Beat {missions[connected_coords].mission_name}", player)
+
+
+def create_structured_regions(
+ world: World,
+ locations: Tuple[LocationData, ...],
+ location_cache: List[Location],
+ mission_order_type: int,
+) -> Tuple[Dict[SC2Campaign, Dict[str, MissionInfo]], int, str]:
+ locations_per_region = get_locations_per_region(locations)
+
+ mission_order = mission_orders[mission_order_type]()
+ enabled_campaigns = get_enabled_campaigns(world)
+ shuffle_campaigns = get_option_value(world, "shuffle_campaigns")
+
+ mission_pools: Dict[MissionPools, List[SC2Mission]] = filter_missions(world)
+ final_mission = mission_pools[MissionPools.FINAL][0]
+
+ regions = [create_region(world, locations_per_region, location_cache, "Menu")]
+
+ names: Dict[str, int] = {}
+
+ mission_slots: List[SC2MissionSlot] = []
+ mission_pool = [mission for mission_pool in mission_pools.values() for mission in mission_pool]
+
+ if mission_order_type in campaign_depending_orders:
+ # Do slot removal per campaign
+ for campaign in enabled_campaigns:
+ campaign_mission_pool = [mission for mission in mission_pool if mission.campaign == campaign]
+ campaign_mission_pool_size = len(campaign_mission_pool)
+
+ removals = len(mission_order[campaign]) - campaign_mission_pool_size
+
+ for mission in mission_order[campaign]:
+ # Removing extra missions if mission pool is too small
+ if 0 < mission.removal_priority <= removals:
+ mission_slots.append(SC2MissionSlot(campaign, None))
+ elif mission.type == MissionPools.FINAL:
+ if campaign == final_mission.campaign:
+ # Campaign is elected to be goal
+ mission_slots.append(SC2MissionSlot(campaign, final_mission))
+ else:
+ # Not the goal, find the most difficult mission in the pool and set the difficulty
+ campaign_difficulty = max(mission.pool for mission in campaign_mission_pool)
+ mission_slots.append(SC2MissionSlot(campaign, campaign_difficulty))
+ else:
+ mission_slots.append(SC2MissionSlot(campaign, mission.type))
+ else:
+ order = mission_order[SC2Campaign.GLOBAL]
+ # Determining if missions must be removed
+ mission_pool_size = sum(len(mission_pool) for mission_pool in mission_pools.values())
+ removals = len(order) - mission_pool_size
+
+ # Initial fill out of mission list and marking All-In mission
+ for mission in order:
+ # Removing extra missions if mission pool is too small
+ if 0 < mission.removal_priority <= removals:
+ mission_slots.append(SC2MissionSlot(SC2Campaign.GLOBAL, None))
+ elif mission.type == MissionPools.FINAL:
+ mission_slots.append(SC2MissionSlot(SC2Campaign.GLOBAL, final_mission))
+ else:
+ mission_slots.append(SC2MissionSlot(SC2Campaign.GLOBAL, mission.type))
+
+ no_build_slots = []
+ easy_slots = []
+ medium_slots = []
+ hard_slots = []
+ very_hard_slots = []
+
+ # Search through missions to find slots needed to fill
+ for i in range(len(mission_slots)):
+ mission_slot = mission_slots[i]
+ if mission_slot is None:
+ continue
+ if isinstance(mission_slot, SC2MissionSlot):
+ if mission_slot.slot is None:
+ continue
+ if mission_slot.slot == MissionPools.STARTER:
+ no_build_slots.append(i)
+ elif mission_slot.slot == MissionPools.EASY:
+ easy_slots.append(i)
+ elif mission_slot.slot == MissionPools.MEDIUM:
+ medium_slots.append(i)
+ elif mission_slot.slot == MissionPools.HARD:
+ hard_slots.append(i)
+ elif mission_slot.slot == MissionPools.VERY_HARD:
+ very_hard_slots.append(i)
+
+ def pick_mission(slot):
+ if shuffle_campaigns or mission_order_type not in campaign_depending_orders:
+ # Pick a mission from any campaign
+ filler = world.random.randint(0, len(missions_to_add) - 1)
+ mission = missions_to_add.pop(filler)
+ slot_campaign = mission_slots[slot].campaign
+ mission_slots[slot] = SC2MissionSlot(slot_campaign, mission)
+ else:
+ # Pick a mission from required campaign
+ slot_campaign = mission_slots[slot].campaign
+ campaign_mission_candidates = [mission for mission in missions_to_add if mission.campaign == slot_campaign]
+ mission = world.random.choice(campaign_mission_candidates)
+ missions_to_add.remove(mission)
+ mission_slots[slot] = SC2MissionSlot(slot_campaign, mission)
+
+ # Add no_build missions to the pool and fill in no_build slots
+ missions_to_add: List[SC2Mission] = mission_pools[MissionPools.STARTER]
+ if len(no_build_slots) > len(missions_to_add):
+ raise Exception("There are no valid No-Build missions. Please exclude fewer missions.")
+ for slot in no_build_slots:
+ pick_mission(slot)
+
+ # Add easy missions into pool and fill in easy slots
+ missions_to_add = missions_to_add + mission_pools[MissionPools.EASY]
+ if len(easy_slots) > len(missions_to_add):
+ raise Exception("There are not enough Easy missions to fill the campaign. Please exclude fewer missions.")
+ for slot in easy_slots:
+ pick_mission(slot)
+
+ # Add medium missions into pool and fill in medium slots
+ missions_to_add = missions_to_add + mission_pools[MissionPools.MEDIUM]
+ if len(medium_slots) > len(missions_to_add):
+ raise Exception("There are not enough Easy and Medium missions to fill the campaign. Please exclude fewer missions.")
+ for slot in medium_slots:
+ pick_mission(slot)
+
+ # Add hard missions into pool and fill in hard slots
+ missions_to_add = missions_to_add + mission_pools[MissionPools.HARD]
+ if len(hard_slots) > len(missions_to_add):
+ raise Exception("There are not enough missions to fill the campaign. Please exclude fewer missions.")
+ for slot in hard_slots:
+ pick_mission(slot)
+
+ # Add very hard missions into pool and fill in very hard slots
+ missions_to_add = missions_to_add + mission_pools[MissionPools.VERY_HARD]
+ if len(very_hard_slots) > len(missions_to_add):
+ raise Exception("There are not enough missions to fill the campaign. Please exclude fewer missions.")
+ for slot in very_hard_slots:
+ pick_mission(slot)
+
+ # Generating regions and locations from selected missions
+ for mission_slot in mission_slots:
+ if isinstance(mission_slot.slot, SC2Mission):
+ regions.append(create_region(world, locations_per_region, location_cache, mission_slot.slot.mission_name))
+ world.multiworld.regions += regions
+
+ campaigns: List[SC2Campaign]
+ if mission_order_type in campaign_depending_orders:
+ campaigns = list(enabled_campaigns)
+ else:
+ campaigns = [SC2Campaign.GLOBAL]
+
+ mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]] = {}
+ campaign_mission_slots: Dict[SC2Campaign, List[SC2MissionSlot]] = \
+ {
+ campaign: [mission_slot for mission_slot in mission_slots if campaign == mission_slot.campaign]
+ for campaign in campaigns
+ }
+
+ slot_map: Dict[SC2Campaign, List[int]] = dict()
+
+ for campaign in campaigns:
+ mission_req_table.update({campaign: dict()})
+
+ # Mapping original mission slots to shifted mission slots when missions are removed
+ slot_map[campaign] = []
+ slot_offset = 0
+ for position, mission in enumerate(campaign_mission_slots[campaign]):
+ slot_map[campaign].append(position - slot_offset + 1)
+ if mission is None or mission.slot is None:
+ slot_offset += 1
+
+ def build_connection_rule(mission_names: List[str], missions_req: int) -> Callable:
+ player = world.player
+ if len(mission_names) > 1:
+ return lambda state: state.has_all({f"Beat {name}" for name in mission_names}, player) \
+ and state.has_group("Missions", player, missions_req)
+ else:
+ return lambda state: state.has(f"Beat {mission_names[0]}", player) \
+ and state.has_group("Missions", player, missions_req)
+
+ for campaign in campaigns:
+ # Loop through missions to create requirements table and connect regions
+ for i, mission in enumerate(campaign_mission_slots[campaign]):
+ if mission is None or mission.slot is None:
+ continue
+ connections: List[MissionConnection] = []
+ all_connections: List[SC2MissionSlot] = []
+ connection: MissionConnection
+ for connection in mission_order[campaign][i].connect_to:
+ if connection.connect_to == -1:
+ continue
+ # If mission normally connects to an excluded campaign, connect to menu instead
+ if connection.campaign not in campaign_mission_slots:
+ connection.connect_to = -1
+ continue
+ while campaign_mission_slots[connection.campaign][connection.connect_to].slot is None:
+ connection.connect_to -= 1
+ all_connections.append(campaign_mission_slots[connection.campaign][connection.connect_to])
+ for connection in mission_order[campaign][i].connect_to:
+ if connection.connect_to == -1:
+ connect(world, names, "Menu", mission.slot.mission_name)
+ else:
+ required_mission = campaign_mission_slots[connection.campaign][connection.connect_to]
+ if ((required_mission is None or required_mission.slot is None)
+ and not mission_order[campaign][i].completion_critical): # Drop non-critical null slots
+ continue
+ while required_mission is None or required_mission.slot is None: # Substituting null slot with prior slot
+ connection.connect_to -= 1
+ required_mission = campaign_mission_slots[connection.campaign][connection.connect_to]
+ required_missions = [required_mission] if mission_order[campaign][i].or_requirements else all_connections
+ if isinstance(required_mission.slot, SC2Mission):
+ required_mission_name = required_mission.slot.mission_name
+ required_missions_names = [mission.slot.mission_name for mission in required_missions]
+ connect(world, names, required_mission_name, mission.slot.mission_name,
+ build_connection_rule(required_missions_names, mission_order[campaign][i].number))
+ connections.append(MissionConnection(slot_map[connection.campaign][connection.connect_to], connection.campaign))
+
+ mission_req_table[campaign].update({mission.slot.mission_name: MissionInfo(
+ mission.slot, connections, mission_order[campaign][i].category,
+ number=mission_order[campaign][i].number,
+ completion_critical=mission_order[campaign][i].completion_critical,
+ or_requirements=mission_order[campaign][i].or_requirements)})
+
+ final_mission_id = final_mission.id
+ # Changing the completion condition for alternate final missions into an event
+ final_location = get_goal_location(final_mission)
+ setup_final_location(final_location, location_cache)
+
+ return mission_req_table, final_mission_id, final_location
+
+
+def setup_final_location(final_location, location_cache):
+ # Final location should be near the end of the cache
+ for i in range(len(location_cache) - 1, -1, -1):
+ if location_cache[i].name == final_location:
+ location_cache[i].address = None
+ break
+
+
+def create_location(player: int, location_data: LocationData, region: Region,
+ location_cache: List[Location]) -> Location:
+ location = Location(player, location_data.name, location_data.code, region)
+ location.access_rule = location_data.rule
+
+ location_cache.append(location)
+
+ return location
+
+
+def create_region(world: World, locations_per_region: Dict[str, List[LocationData]],
+ location_cache: List[Location], name: str) -> Region:
+ region = Region(name, world.player, world.multiworld)
+
+ if name in locations_per_region:
+ for location_data in locations_per_region[name]:
+ location = create_location(world.player, location_data, region, location_cache)
+ region.locations.append(location)
+
+ return region
+
+
+def connect(world: World, used_names: Dict[str, int], source: str, target: str,
+ rule: Optional[Callable] = None):
+ source_region = world.get_region(source)
+ target_region = world.get_region(target)
+
+ if target not in used_names:
+ used_names[target] = 1
+ name = target
+ else:
+ used_names[target] += 1
+ name = target + (' ' * used_names[target])
+
+ connection = Entrance(world.player, name, source_region)
+
+ if rule:
+ connection.access_rule = rule
+
+ source_region.exits.append(connection)
+ connection.connect(target_region)
+
+
+def get_locations_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]:
+ per_region: Dict[str, List[LocationData]] = {}
+
+ for location in locations:
+ per_region.setdefault(location.region, []).append(location)
+
+ return per_region
+
+
+def get_factors(number: int) -> Tuple[int, int]:
+ """
+ Simple factorization into pairs of numbers (x, y) using a sieve method.
+ Returns the factorization that is most square, i.e. where x + y is minimized.
+ Factor order is such that x <= y.
+ """
+ assert number > 0
+ for divisor in range(math.floor(math.sqrt(number)), 1, -1):
+ quotient = number // divisor
+ if quotient * divisor == number:
+ return divisor, quotient
+ return 1, number
+
+
+def get_grid_dimensions(size: int) -> Tuple[int, int, int]:
+ """
+ Get the dimensions of a grid mission order from the number of missions, int the format (x, y, error).
+ * Error will always be 0, 1, or 2, so the missions can be removed from the corners that aren't the start or end.
+ * Dimensions are chosen such that x <= y, as buttons in the UI are wider than they are tall.
+ * Dimensions are chosen to be maximally square. That is, x + y + error is minimized.
+ * If multiple options of the same rating are possible, the one with the larger error is chosen,
+ as it will appear more square. Compare 3x11 to 5x7-2 for an example of this.
+ """
+ dimension_candidates: List[Tuple[int, int, int]] = [(*get_factors(size + x), x) for x in (2, 1, 0)]
+ best_dimension = min(dimension_candidates, key=sum)
+ return best_dimension
+
diff --git a/worlds/sc2/Rules.py b/worlds/sc2/Rules.py
new file mode 100644
index 000000000000..8b9097ea1d78
--- /dev/null
+++ b/worlds/sc2/Rules.py
@@ -0,0 +1,952 @@
+from typing import Set
+
+from BaseClasses import CollectionState
+from .Options import get_option_value, RequiredTactics, kerrigan_unit_available, AllInMap, \
+ GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, SpearOfAdunAutonomouslyCastAbilityPresence, \
+ get_enabled_campaigns, MissionOrder
+from .Items import get_basic_units, defense_ratings, zerg_defense_ratings, kerrigan_actives, air_defense_ratings, \
+ kerrigan_levels, get_full_item_list
+from .MissionTables import SC2Race, SC2Campaign
+from . import ItemNames
+from worlds.AutoWorld import World
+
+
+class SC2Logic:
+
+ def lock_any_item(self, state: CollectionState, items: Set[str]) -> bool:
+ """
+ Guarantees that at least one of these items will remain in the world. Doesn't affect placement.
+ Needed for cases when the dynamic pool filtering could remove all the item prerequisites
+ :param state:
+ :param items:
+ :return:
+ """
+ return self.is_item_placement(state) \
+ or state.has_any(items, self.player)
+
+ def is_item_placement(self, state):
+ """
+ Tells if it's item placement or item pool filter
+ :param state:
+ :return: True for item placement, False for pool filter
+ """
+ # has_group with count = 0 is always true for item placement and always false for SC2 item filtering
+ return state.has_group("Missions", self.player, 0)
+
+ # WoL
+ def terran_common_unit(self, state: CollectionState) -> bool:
+ return state.has_any(self.basic_terran_units, self.player)
+
+ def terran_early_tech(self, state: CollectionState):
+ """
+ Basic combat unit that can be deployed quickly from mission start
+ :param state
+ :return:
+ """
+ return (
+ state.has_any({ItemNames.MARINE, ItemNames.FIREBAT, ItemNames.MARAUDER, ItemNames.REAPER, ItemNames.HELLION}, self.player)
+ or (self.advanced_tactics and state.has_any({ItemNames.GOLIATH, ItemNames.DIAMONDBACK, ItemNames.VIKING, ItemNames.BANSHEE}, self.player))
+ )
+
+ def terran_air(self, state: CollectionState) -> bool:
+ """
+ Air units or drops on advanced tactics
+ :param state:
+ :return:
+ """
+ return (state.has_any({ItemNames.VIKING, ItemNames.WRAITH, ItemNames.BANSHEE, ItemNames.BATTLECRUISER}, self.player) or self.advanced_tactics
+ and state.has_any({ItemNames.HERCULES, ItemNames.MEDIVAC}, self.player) and self.terran_common_unit(state)
+ )
+
+ def terran_air_anti_air(self, state: CollectionState) -> bool:
+ """
+ Air-to-air
+ :param state:
+ :return:
+ """
+ return (
+ state.has(ItemNames.VIKING, self.player)
+ or state.has_all({ItemNames.WRAITH, ItemNames.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player)
+ or state.has_all({ItemNames.BATTLECRUISER, ItemNames.BATTLECRUISER_ATX_LASER_BATTERY}, self.player)
+ or self.advanced_tactics and state.has_any({ItemNames.WRAITH, ItemNames.VALKYRIE, ItemNames.BATTLECRUISER}, self.player)
+ )
+
+ def terran_competent_ground_to_air(self, state: CollectionState) -> bool:
+ """
+ Ground-to-air
+ :param state:
+ :return:
+ """
+ return (
+ state.has(ItemNames.GOLIATH, self.player)
+ or state.has(ItemNames.MARINE, self.player) and self.terran_bio_heal(state)
+ or self.advanced_tactics and state.has(ItemNames.CYCLONE, self.player)
+ )
+
+ def terran_competent_anti_air(self, state: CollectionState) -> bool:
+ """
+ Good AA unit
+ :param state:
+ :return:
+ """
+ return (
+ self.terran_competent_ground_to_air(state)
+ or self.terran_air_anti_air(state)
+ )
+
+ def welcome_to_the_jungle_requirement(self, state: CollectionState) -> bool:
+ """
+ Welcome to the Jungle requirements - able to deal with Scouts, Void Rays, Zealots and Stalkers
+ :param state:
+ :return:
+ """
+ return (
+ self.terran_common_unit(state)
+ and self.terran_competent_ground_to_air(state)
+ ) or (
+ self.advanced_tactics
+ and state.has_any({ItemNames.MARINE, ItemNames.VULTURE}, self.player)
+ and self.terran_air_anti_air(state)
+ )
+
+ def terran_basic_anti_air(self, state: CollectionState) -> bool:
+ """
+ Basic AA to deal with few air units
+ :param state:
+ :return:
+ """
+ return (
+ state.has_any({
+ ItemNames.MISSILE_TURRET, ItemNames.THOR, ItemNames.WAR_PIGS, ItemNames.SPARTAN_COMPANY,
+ ItemNames.HELS_ANGELS, ItemNames.BATTLECRUISER, ItemNames.MARINE, ItemNames.WRAITH,
+ ItemNames.VALKYRIE, ItemNames.CYCLONE, ItemNames.WINGED_NIGHTMARES, ItemNames.BRYNHILDS
+ }, self.player)
+ or self.terran_competent_anti_air(state)
+ or self.advanced_tactics and state.has_any({ItemNames.GHOST, ItemNames.SPECTRE, ItemNames.WIDOW_MINE, ItemNames.LIBERATOR}, self.player)
+ )
+
+ def terran_defense_rating(self, state: CollectionState, zerg_enemy: bool, air_enemy: bool = True) -> int:
+ """
+ Ability to handle defensive missions
+ :param state:
+ :param zerg_enemy:
+ :param air_enemy:
+ :return:
+ """
+ defense_score = sum((defense_ratings[item] for item in defense_ratings if state.has(item, self.player)))
+ # Manned Bunker
+ if state.has_any({ItemNames.MARINE, ItemNames.MARAUDER}, self.player) and state.has(ItemNames.BUNKER, self.player):
+ defense_score += 3
+ elif zerg_enemy and state.has(ItemNames.FIREBAT, self.player) and state.has(ItemNames.BUNKER, self.player):
+ defense_score += 2
+ # Siege Tank upgrades
+ if state.has_all({ItemNames.SIEGE_TANK, ItemNames.SIEGE_TANK_MAELSTROM_ROUNDS}, self.player):
+ defense_score += 2
+ if state.has_all({ItemNames.SIEGE_TANK, ItemNames.SIEGE_TANK_GRADUATING_RANGE}, self.player):
+ defense_score += 1
+ # Widow Mine upgrade
+ if state.has_all({ItemNames.WIDOW_MINE, ItemNames.WIDOW_MINE_CONCEALMENT}, self.player):
+ defense_score += 1
+ # Viking with splash
+ if state.has_all({ItemNames.VIKING, ItemNames.VIKING_SHREDDER_ROUNDS}, self.player):
+ defense_score += 2
+
+ # General enemy-based rules
+ if zerg_enemy:
+ defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if state.has(item, self.player)))
+ if air_enemy:
+ defense_score += sum((air_defense_ratings[item] for item in air_defense_ratings if state.has(item, self.player)))
+ if air_enemy and zerg_enemy and state.has(ItemNames.VALKYRIE, self.player):
+ # Valkyries shred mass Mutas, most common air enemy that's massed in these cases
+ defense_score += 2
+ # Advanced Tactics bumps defense rating requirements down by 2
+ if self.advanced_tactics:
+ defense_score += 2
+ return defense_score
+
+ def terran_competent_comp(self, state: CollectionState) -> bool:
+ """
+ Ability to deal with most of hard missions
+ :param state:
+ :return:
+ """
+ return (
+ (
+ (state.has_any({ItemNames.MARINE, ItemNames.MARAUDER}, self.player) and self.terran_bio_heal(state))
+ or state.has_any({ItemNames.THOR, ItemNames.BANSHEE, ItemNames.SIEGE_TANK}, self.player)
+ or state.has_all({ItemNames.LIBERATOR, ItemNames.LIBERATOR_RAID_ARTILLERY}, self.player)
+ )
+ and self.terran_competent_anti_air(state)
+ ) or (
+ state.has(ItemNames.BATTLECRUISER, self.player) and self.terran_common_unit(state)
+ )
+
+ def great_train_robbery_train_stopper(self, state: CollectionState) -> bool:
+ """
+ Ability to deal with trains (moving target with a lot of HP)
+ :param state:
+ :return:
+ """
+ return (
+ state.has_any({ItemNames.SIEGE_TANK, ItemNames.DIAMONDBACK, ItemNames.MARAUDER, ItemNames.CYCLONE, ItemNames.BANSHEE}, self.player)
+ or self.advanced_tactics
+ and (
+ state.has_all({ItemNames.REAPER, ItemNames.REAPER_G4_CLUSTERBOMB}, self.player)
+ or state.has_all({ItemNames.SPECTRE, ItemNames.SPECTRE_PSIONIC_LASH}, self.player)
+ or state.has_any({ItemNames.VULTURE, ItemNames.LIBERATOR}, self.player)
+ )
+ )
+
+ def terran_can_rescue(self, state) -> bool:
+ """
+ Rescuing in The Moebius Factor
+ :param state:
+ :return:
+ """
+ return state.has_any({ItemNames.MEDIVAC, ItemNames.HERCULES, ItemNames.RAVEN, ItemNames.VIKING}, self.player) or self.advanced_tactics
+
+ def terran_beats_protoss_deathball(self, state: CollectionState) -> bool:
+ """
+ Ability to deal with Immortals, Colossi with some air support
+ :param state:
+ :return:
+ """
+ return (
+ (
+ state.has_any({ItemNames.BANSHEE, ItemNames.BATTLECRUISER}, self.player)
+ or state.has_all({ItemNames.LIBERATOR, ItemNames.LIBERATOR_RAID_ARTILLERY}, self.player)
+ ) and self.terran_competent_anti_air(state)
+ or self.terran_competent_comp(state) and self.terran_air_anti_air(state)
+ )
+
+ def marine_medic_upgrade(self, state: CollectionState) -> bool:
+ """
+ Infantry upgrade to infantry-only no-build segments
+ :param state:
+ :return:
+ """
+ return state.has_any({
+ ItemNames.MARINE_COMBAT_SHIELD, ItemNames.MARINE_MAGRAIL_MUNITIONS, ItemNames.MEDIC_STABILIZER_MEDPACKS
+ }, self.player) \
+ or (state.count(ItemNames.MARINE_PROGRESSIVE_STIMPACK, self.player) >= 2
+ and state.has_group("Missions", self.player, 1))
+
+ def terran_survives_rip_field(self, state: CollectionState) -> bool:
+ """
+ Ability to deal with large areas with environment damage
+ :param state:
+ :return:
+ """
+ return (state.has(ItemNames.BATTLECRUISER, self.player)
+ or self.terran_air(state) and self.terran_competent_anti_air(state) and self.terran_sustainable_mech_heal(state))
+
+ def terran_sustainable_mech_heal(self, state: CollectionState) -> bool:
+ """
+ Can heal mech units without spending resources
+ :param state:
+ :return:
+ """
+ return state.has(ItemNames.SCIENCE_VESSEL, self.player) \
+ or state.has_all({ItemNames.MEDIC, ItemNames.MEDIC_ADAPTIVE_MEDPACKS}, self.player) \
+ or state.count(ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL, self.player) >= 3 \
+ or (self.advanced_tactics
+ and (
+ state.has_all({ItemNames.RAVEN, ItemNames.RAVEN_BIO_MECHANICAL_REPAIR_DRONE}, self.player)
+ or state.count(ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL, self.player) >= 2)
+ )
+
+ def terran_bio_heal(self, state: CollectionState) -> bool:
+ """
+ Ability to heal bio units
+ :param state:
+ :return:
+ """
+ return state.has_any({ItemNames.MEDIC, ItemNames.MEDIVAC}, self.player) \
+ or self.advanced_tactics and state.has_all({ItemNames.RAVEN, ItemNames.RAVEN_BIO_MECHANICAL_REPAIR_DRONE}, self.player)
+
+ def terran_base_trasher(self, state: CollectionState) -> bool:
+ """
+ Can attack heavily defended bases
+ :param state:
+ :return:
+ """
+ return state.has(ItemNames.SIEGE_TANK, self.player) \
+ or state.has_all({ItemNames.BATTLECRUISER, ItemNames.BATTLECRUISER_ATX_LASER_BATTERY}, self.player) \
+ or state.has_all({ItemNames.LIBERATOR, ItemNames.LIBERATOR_RAID_ARTILLERY}, self.player) \
+ or (self.advanced_tactics
+ and ((state.has_all({ItemNames.RAVEN, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, self.player)
+ or self.can_nuke(state))
+ and (
+ state.has_all({ItemNames.VIKING, ItemNames.VIKING_SHREDDER_ROUNDS}, self.player)
+ or state.has_all({ItemNames.BANSHEE, ItemNames.BANSHEE_SHOCKWAVE_MISSILE_BATTERY}, self.player))
+ )
+ )
+
+ def terran_mobile_detector(self, state: CollectionState) -> bool:
+ return state.has_any({ItemNames.RAVEN, ItemNames.SCIENCE_VESSEL, ItemNames.PROGRESSIVE_ORBITAL_COMMAND}, self.player)
+
+ def can_nuke(self, state: CollectionState) -> bool:
+ """
+ Ability to launch nukes
+ :param state:
+ :return:
+ """
+ return (self.advanced_tactics
+ and (state.has_any({ItemNames.GHOST, ItemNames.SPECTRE}, self.player)
+ or state.has_all({ItemNames.THOR, ItemNames.THOR_BUTTON_WITH_A_SKULL_ON_IT}, self.player)))
+
+ def terran_respond_to_colony_infestations(self, state: CollectionState) -> bool:
+ """
+ Can deal quickly with Brood Lords and Mutas in Haven's Fall and being able to progress the mission
+ :param state:
+ :return:
+ """
+ return (
+ self.terran_common_unit(state)
+ and self.terran_competent_anti_air(state)
+ and (
+ self.terran_air_anti_air(state)
+ or state.has_any({ItemNames.BATTLECRUISER, ItemNames.VALKYRIE}, self.player)
+ )
+ and self.terran_defense_rating(state, True) >= 3
+ )
+
+ def engine_of_destruction_requirement(self, state: CollectionState):
+ return self.marine_medic_upgrade(state) \
+ and (
+ self.terran_competent_anti_air(state)
+ and self.terran_common_unit(state) or state.has(ItemNames.WRAITH, self.player)
+ )
+
+ def all_in_requirement(self, state: CollectionState):
+ """
+ All-in
+ :param state:
+ :return:
+ """
+ beats_kerrigan = state.has_any({ItemNames.MARINE, ItemNames.BANSHEE, ItemNames.GHOST}, self.player) or self.advanced_tactics
+ if get_option_value(self.world, 'all_in_map') == AllInMap.option_ground:
+ # Ground
+ defense_rating = self.terran_defense_rating(state, True, False)
+ if state.has_any({ItemNames.BATTLECRUISER, ItemNames.BANSHEE}, self.player):
+ defense_rating += 2
+ return defense_rating >= 13 and beats_kerrigan
+ else:
+ # Air
+ defense_rating = self.terran_defense_rating(state, True, True)
+ return defense_rating >= 9 and beats_kerrigan \
+ and state.has_any({ItemNames.VIKING, ItemNames.BATTLECRUISER, ItemNames.VALKYRIE}, self.player) \
+ and state.has_any({ItemNames.HIVE_MIND_EMULATOR, ItemNames.PSI_DISRUPTER, ItemNames.MISSILE_TURRET}, self.player)
+
+ # HotS
+ def zerg_common_unit(self, state: CollectionState) -> bool:
+ return state.has_any(self.basic_zerg_units, self.player)
+
+ def zerg_competent_anti_air(self, state: CollectionState) -> bool:
+ return state.has_any({ItemNames.HYDRALISK, ItemNames.MUTALISK, ItemNames.CORRUPTOR, ItemNames.BROOD_QUEEN}, self.player) \
+ or state.has_all({ItemNames.SWARM_HOST, ItemNames.SWARM_HOST_PRESSURIZED_GLANDS}, self.player) \
+ or state.has_all({ItemNames.SCOURGE, ItemNames.SCOURGE_RESOURCE_EFFICIENCY}, self.player) \
+ or (self.advanced_tactics and state.has(ItemNames.INFESTOR, self.player))
+
+ def zerg_basic_anti_air(self, state: CollectionState) -> bool:
+ return self.zerg_competent_anti_air(state) or self.kerrigan_unit_available in kerrigan_unit_available or \
+ state.has_any({ItemNames.SWARM_QUEEN, ItemNames.SCOURGE}, self.player) or (self.advanced_tactics and state.has(ItemNames.SPORE_CRAWLER, self.player))
+
+ def morph_brood_lord(self, state: CollectionState) -> bool:
+ return state.has_any({ItemNames.MUTALISK, ItemNames.CORRUPTOR}, self.player) \
+ and state.has(ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT, self.player)
+
+ def morph_viper(self, state: CollectionState) -> bool:
+ return state.has_any({ItemNames.MUTALISK, ItemNames.CORRUPTOR}, self.player) \
+ and state.has(ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT, self.player)
+
+ def morph_impaler_or_lurker(self, state: CollectionState) -> bool:
+ return state.has(ItemNames.HYDRALISK, self.player) and state.has_any({ItemNames.HYDRALISK_IMPALER_ASPECT, ItemNames.HYDRALISK_LURKER_ASPECT}, self.player)
+
+ def zerg_competent_comp(self, state: CollectionState) -> bool:
+ advanced = self.advanced_tactics
+ core_unit = state.has_any({ItemNames.ROACH, ItemNames.ABERRATION, ItemNames.ZERGLING}, self.player)
+ support_unit = state.has_any({ItemNames.SWARM_QUEEN, ItemNames.HYDRALISK}, self.player) \
+ or self.morph_brood_lord(state) \
+ or advanced and (state.has_any({ItemNames.INFESTOR, ItemNames.DEFILER}, self.player) or self.morph_viper(state))
+ if core_unit and support_unit:
+ return True
+ vespene_unit = state.has_any({ItemNames.ULTRALISK, ItemNames.ABERRATION}, self.player) \
+ or advanced and self.morph_viper(state)
+ return vespene_unit and state.has_any({ItemNames.ZERGLING, ItemNames.SWARM_QUEEN}, self.player)
+
+ def spread_creep(self, state: CollectionState) -> bool:
+ return self.advanced_tactics or state.has(ItemNames.SWARM_QUEEN, self.player)
+
+ def zerg_competent_defense(self, state: CollectionState) -> bool:
+ return (
+ self.zerg_common_unit(state)
+ and (
+ (
+ state.has(ItemNames.SWARM_HOST, self.player)
+ or self.morph_brood_lord(state)
+ or self.morph_impaler_or_lurker(state)
+ ) or (
+ self.advanced_tactics
+ and (self.morph_viper(state)
+ or state.has(ItemNames.SPINE_CRAWLER, self.player))
+ )
+ )
+ )
+
+ def basic_kerrigan(self, state: CollectionState) -> bool:
+ # One active ability that can be used to defeat enemies directly on Standard
+ if not self.advanced_tactics and \
+ not state.has_any({ItemNames.KERRIGAN_KINETIC_BLAST, ItemNames.KERRIGAN_LEAPING_STRIKE,
+ ItemNames.KERRIGAN_CRUSHING_GRIP, ItemNames.KERRIGAN_PSIONIC_SHIFT,
+ ItemNames.KERRIGAN_SPAWN_BANELINGS}, self.player):
+ return False
+ # Two non-ultimate abilities
+ count = 0
+ for item in (ItemNames.KERRIGAN_KINETIC_BLAST, ItemNames.KERRIGAN_LEAPING_STRIKE, ItemNames.KERRIGAN_HEROIC_FORTITUDE,
+ ItemNames.KERRIGAN_CHAIN_REACTION, ItemNames.KERRIGAN_CRUSHING_GRIP, ItemNames.KERRIGAN_PSIONIC_SHIFT,
+ ItemNames.KERRIGAN_SPAWN_BANELINGS, ItemNames.KERRIGAN_INFEST_BROODLINGS, ItemNames.KERRIGAN_FURY):
+ if state.has(item, self.player):
+ count += 1
+ if count >= 2:
+ return True
+ return False
+
+ def two_kerrigan_actives(self, state: CollectionState) -> bool:
+ count = 0
+ for i in range(7):
+ if state.has_any(kerrigan_actives[i], self.player):
+ count += 1
+ return count >= 2
+
+ def zerg_pass_vents(self, state: CollectionState) -> bool:
+ return self.story_tech_granted \
+ or state.has_any({ItemNames.ZERGLING, ItemNames.HYDRALISK, ItemNames.ROACH}, self.player) \
+ or (self.advanced_tactics and state.has(ItemNames.INFESTOR, self.player))
+
+ def supreme_requirement(self, state: CollectionState) -> bool:
+ return self.story_tech_granted \
+ or not self.kerrigan_unit_available \
+ or (
+ state.has_all({ItemNames.KERRIGAN_LEAPING_STRIKE, ItemNames.KERRIGAN_MEND}, self.player)
+ and self.kerrigan_levels(state, 35)
+ )
+
+ def kerrigan_levels(self, state: CollectionState, target: int) -> bool:
+ if self.story_levels_granted or not self.kerrigan_unit_available:
+ return True # Levels are granted
+ if self.kerrigan_levels_per_mission_completed > 0 \
+ and self.kerrigan_levels_per_mission_completed_cap > 0 \
+ and not self.is_item_placement(state):
+ # Levels can be granted from mission completion.
+ # Item pool filtering isn't aware of missions beaten. Assume that missions beaten will fulfill this rule.
+ return True
+ # Levels from missions beaten
+ levels = self.kerrigan_levels_per_mission_completed * state.count_group("Missions", self.player)
+ if self.kerrigan_levels_per_mission_completed_cap != -1:
+ levels = min(levels, self.kerrigan_levels_per_mission_completed_cap)
+ # Levels from items
+ for kerrigan_level_item in kerrigan_levels:
+ level_amount = get_full_item_list()[kerrigan_level_item].number
+ item_count = state.count(kerrigan_level_item, self.player)
+ levels += item_count * level_amount
+ # Total level cap
+ if self.kerrigan_total_level_cap != -1:
+ levels = min(levels, self.kerrigan_total_level_cap)
+
+ return levels >= target
+
+
+ def the_reckoning_requirement(self, state: CollectionState) -> bool:
+ if self.take_over_ai_allies:
+ return self.terran_competent_comp(state) \
+ and self.zerg_competent_comp(state) \
+ and (self.zerg_competent_anti_air(state)
+ or self.terran_competent_anti_air(state))
+ else:
+ return self.zerg_competent_comp(state) \
+ and self.zerg_competent_anti_air(state)
+
+ # LotV
+
+ def protoss_common_unit(self, state: CollectionState) -> bool:
+ return state.has_any(self.basic_protoss_units, self.player)
+
+ def protoss_basic_anti_air(self, state: CollectionState) -> bool:
+ return self.protoss_competent_anti_air(state) \
+ or state.has_any({ItemNames.PHOENIX, ItemNames.MIRAGE, ItemNames.CORSAIR, ItemNames.CARRIER, ItemNames.SCOUT,
+ ItemNames.DARK_ARCHON, ItemNames.WRATHWALKER, ItemNames.MOTHERSHIP}, self.player) \
+ or state.has_all({ItemNames.WARP_PRISM, ItemNames.WARP_PRISM_PHASE_BLASTER}, self.player) \
+ or self.advanced_tactics and state.has_any(
+ {ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.ASCENDANT, ItemNames.DARK_TEMPLAR,
+ ItemNames.SENTRY, ItemNames.ENERGIZER}, self.player)
+
+ def protoss_anti_armor_anti_air(self, state: CollectionState) -> bool:
+ return self.protoss_competent_anti_air(state) \
+ or state.has_any({ItemNames.SCOUT, ItemNames.WRATHWALKER}, self.player) \
+ or (state.has_any({ItemNames.IMMORTAL, ItemNames.ANNIHILATOR}, self.player)
+ and state.has(ItemNames.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING_MECHANICS, self.player))
+
+ def protoss_anti_light_anti_air(self, state: CollectionState) -> bool:
+ return self.protoss_competent_anti_air(state) \
+ or state.has_any({ItemNames.PHOENIX, ItemNames.MIRAGE, ItemNames.CORSAIR, ItemNames.CARRIER}, self.player)
+
+ def protoss_competent_anti_air(self, state: CollectionState) -> bool:
+ return state.has_any(
+ {ItemNames.STALKER, ItemNames.SLAYER, ItemNames.INSTIGATOR, ItemNames.DRAGOON, ItemNames.ADEPT,
+ ItemNames.VOID_RAY, ItemNames.DESTROYER, ItemNames.TEMPEST}, self.player) \
+ or (state.has_any({ItemNames.PHOENIX, ItemNames.MIRAGE, ItemNames.CORSAIR, ItemNames.CARRIER}, self.player)
+ and state.has_any({ItemNames.SCOUT, ItemNames.WRATHWALKER}, self.player)) \
+ or (self.advanced_tactics
+ and state.has_any({ItemNames.IMMORTAL, ItemNames.ANNIHILATOR}, self.player)
+ and state.has(ItemNames.IMMORTAL_ANNIHILATOR_ADVANCED_TARGETING_MECHANICS, self.player))
+
+ def protoss_has_blink(self, state: CollectionState) -> bool:
+ return state.has_any({ItemNames.STALKER, ItemNames.INSTIGATOR, ItemNames.SLAYER}, self.player) \
+ or (
+ state.has(ItemNames.DARK_TEMPLAR_AVENGER_BLOOD_HUNTER_BLINK, self.player)
+ and state.has_any({ItemNames.DARK_TEMPLAR, ItemNames.BLOOD_HUNTER, ItemNames.AVENGER}, self.player)
+ )
+
+ def protoss_can_attack_behind_chasm(self, state: CollectionState) -> bool:
+ return state.has_any(
+ {ItemNames.SCOUT, ItemNames.TEMPEST,
+ ItemNames.CARRIER, ItemNames.VOID_RAY, ItemNames.DESTROYER, ItemNames.MOTHERSHIP}, self.player) \
+ or self.protoss_has_blink(state) \
+ or (state.has(ItemNames.WARP_PRISM, self.player)
+ and (self.protoss_common_unit(state) or state.has(ItemNames.WARP_PRISM_PHASE_BLASTER, self.player))) \
+ or (self.advanced_tactics
+ and state.has_any({ItemNames.ORACLE, ItemNames.ARBITER}, self.player))
+
+ def protoss_fleet(self, state: CollectionState) -> bool:
+ return state.has_any({ItemNames.CARRIER, ItemNames.TEMPEST, ItemNames.VOID_RAY, ItemNames.DESTROYER}, self.player)
+
+ def templars_return_requirement(self, state: CollectionState) -> bool:
+ return self.story_tech_granted \
+ or (
+ state.has_any({ItemNames.IMMORTAL, ItemNames.ANNIHILATOR}, self.player)
+ and state.has_any({ItemNames.COLOSSUS, ItemNames.VANGUARD, ItemNames.REAVER, ItemNames.DARK_TEMPLAR}, self.player)
+ and state.has_any({ItemNames.SENTRY, ItemNames.HIGH_TEMPLAR}, self.player)
+ )
+
+ def brothers_in_arms_requirement(self, state: CollectionState) -> bool:
+ return (
+ self.protoss_common_unit(state)
+ and self.protoss_anti_armor_anti_air(state)
+ and self.protoss_hybrid_counter(state)
+ ) or (
+ self.take_over_ai_allies
+ and (
+ self.terran_common_unit(state)
+ or self.protoss_common_unit(state)
+ )
+ and (
+ self.terran_competent_anti_air(state)
+ or self.protoss_anti_armor_anti_air(state)
+ )
+ and (
+ self.protoss_hybrid_counter(state)
+ or state.has_any({ItemNames.BATTLECRUISER, ItemNames.LIBERATOR, ItemNames.SIEGE_TANK}, self.player)
+ or state.has_all({ItemNames.SPECTRE, ItemNames.SPECTRE_PSIONIC_LASH}, self.player)
+ or (state.has(ItemNames.IMMORTAL, self.player)
+ and state.has_any({ItemNames.MARINE, ItemNames.MARAUDER}, self.player)
+ and self.terran_bio_heal(state))
+ )
+ )
+
+ def protoss_hybrid_counter(self, state: CollectionState) -> bool:
+ """
+ Ground Hybrids
+ """
+ return state.has_any(
+ {ItemNames.ANNIHILATOR, ItemNames.ASCENDANT, ItemNames.TEMPEST, ItemNames.CARRIER, ItemNames.VOID_RAY,
+ ItemNames.WRATHWALKER, ItemNames.VANGUARD}, self.player) \
+ or (state.has(ItemNames.IMMORTAL, self.player) or self.advanced_tactics) and state.has_any(
+ {ItemNames.STALKER, ItemNames.DRAGOON, ItemNames.ADEPT, ItemNames.INSTIGATOR, ItemNames.SLAYER}, self.player)
+
+ def the_infinite_cycle_requirement(self, state: CollectionState) -> bool:
+ return self.story_tech_granted \
+ or not self.kerrigan_unit_available \
+ or (
+ self.two_kerrigan_actives(state)
+ and self.basic_kerrigan(state)
+ and self.kerrigan_levels(state, 70)
+ )
+
+ def protoss_basic_splash(self, state: CollectionState) -> bool:
+ return state.has_any(
+ {ItemNames.ZEALOT, ItemNames.COLOSSUS, ItemNames.VANGUARD, ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER,
+ ItemNames.DARK_TEMPLAR, ItemNames.REAVER, ItemNames.ASCENDANT}, self.player)
+
+ def protoss_static_defense(self, state: CollectionState) -> bool:
+ return state.has_any({ItemNames.PHOTON_CANNON, ItemNames.KHAYDARIN_MONOLITH}, self.player)
+
+ def last_stand_requirement(self, state: CollectionState) -> bool:
+ return self.protoss_common_unit(state) \
+ and self.protoss_competent_anti_air(state) \
+ and self.protoss_static_defense(state) \
+ and (
+ self.advanced_tactics
+ or self.protoss_basic_splash(state)
+ )
+
+ def harbinger_of_oblivion_requirement(self, state: CollectionState) -> bool:
+ return self.protoss_anti_armor_anti_air(state) and (
+ self.take_over_ai_allies
+ or (
+ self.protoss_common_unit(state)
+ and self.protoss_hybrid_counter(state)
+ )
+ )
+
+ def protoss_competent_comp(self, state: CollectionState) -> bool:
+ return self.protoss_common_unit(state) \
+ and self.protoss_competent_anti_air(state) \
+ and self.protoss_hybrid_counter(state) \
+ and self.protoss_basic_splash(state)
+
+ def protoss_stalker_upgrade(self, state: CollectionState) -> bool:
+ return (
+ state.has_any(
+ {
+ ItemNames.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES,
+ ItemNames.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION
+ }, self.player)
+ and self.lock_any_item(state, {ItemNames.STALKER, ItemNames.INSTIGATOR, ItemNames.SLAYER})
+ )
+
+ def steps_of_the_rite_requirement(self, state: CollectionState) -> bool:
+ return self.protoss_competent_comp(state) \
+ or (
+ self.protoss_common_unit(state)
+ and self.protoss_competent_anti_air(state)
+ and self.protoss_static_defense(state)
+ )
+
+ def protoss_heal(self, state: CollectionState) -> bool:
+ return state.has_any({ItemNames.CARRIER, ItemNames.SENTRY, ItemNames.SHIELD_BATTERY, ItemNames.RECONSTRUCTION_BEAM}, self.player)
+
+ def templars_charge_requirement(self, state: CollectionState) -> bool:
+ return self.protoss_heal(state) \
+ and self.protoss_anti_armor_anti_air(state) \
+ and (
+ self.protoss_fleet(state)
+ or (self.advanced_tactics
+ and self.protoss_competent_comp(state)
+ )
+ )
+
+ def the_host_requirement(self, state: CollectionState) -> bool:
+ return (self.protoss_fleet(state)
+ and self.protoss_static_defense(state)
+ ) or (
+ self.protoss_competent_comp(state)
+ and state.has(ItemNames.SOA_TIME_STOP, self.player)
+ )
+
+ def salvation_requirement(self, state: CollectionState) -> bool:
+ return [
+ self.protoss_competent_comp(state),
+ self.protoss_fleet(state),
+ self.protoss_static_defense(state)
+ ].count(True) >= 2
+
+ def into_the_void_requirement(self, state: CollectionState) -> bool:
+ return self.protoss_competent_comp(state) \
+ or (
+ self.take_over_ai_allies
+ and (
+ state.has(ItemNames.BATTLECRUISER, self.player)
+ or (
+ state.has(ItemNames.ULTRALISK, self.player)
+ and self.protoss_competent_anti_air(state)
+ )
+ )
+ )
+
+ def essence_of_eternity_requirement(self, state: CollectionState) -> bool:
+ defense_score = self.terran_defense_rating(state, False, True)
+ if self.take_over_ai_allies and self.protoss_static_defense(state):
+ defense_score += 2
+ return defense_score >= 10 \
+ and (
+ self.terran_competent_anti_air(state)
+ or self.take_over_ai_allies
+ and self.protoss_competent_anti_air(state)
+ ) \
+ and (
+ state.has(ItemNames.BATTLECRUISER, self.player)
+ or (state.has(ItemNames.BANSHEE, self.player) and state.has_any({ItemNames.VIKING, ItemNames.VALKYRIE},
+ self.player))
+ or self.take_over_ai_allies and self.protoss_fleet(state)
+ ) \
+ and state.has_any({ItemNames.SIEGE_TANK, ItemNames.LIBERATOR}, self.player)
+
+ def amons_fall_requirement(self, state: CollectionState) -> bool:
+ if self.take_over_ai_allies:
+ return (
+ (
+ state.has_any({ItemNames.BATTLECRUISER, ItemNames.CARRIER}, self.player)
+ )
+ or (state.has(ItemNames.ULTRALISK, self.player)
+ and self.protoss_competent_anti_air(state)
+ and (
+ state.has_any({ItemNames.LIBERATOR, ItemNames.BANSHEE, ItemNames.VALKYRIE, ItemNames.VIKING}, self.player)
+ or state.has_all({ItemNames.WRAITH, ItemNames.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player)
+ or self.protoss_fleet(state)
+ )
+ and (self.terran_sustainable_mech_heal(state)
+ or (self.spear_of_adun_autonomously_cast_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_everywhere
+ and state.has(ItemNames.RECONSTRUCTION_BEAM, self.player))
+ )
+ )
+ ) \
+ and self.terran_competent_anti_air(state) \
+ and self.protoss_competent_comp(state) \
+ and self.zerg_competent_comp(state)
+ else:
+ return state.has(ItemNames.MUTALISK, self.player) and self.zerg_competent_comp(state)
+
+ def nova_any_weapon(self, state: CollectionState) -> bool:
+ return state.has_any(
+ {ItemNames.NOVA_C20A_CANISTER_RIFLE, ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_PLASMA_RIFLE,
+ ItemNames.NOVA_MONOMOLECULAR_BLADE, ItemNames.NOVA_BLAZEFIRE_GUNBLADE}, self.player)
+
+ def nova_ranged_weapon(self, state: CollectionState) -> bool:
+ return state.has_any(
+ {ItemNames.NOVA_C20A_CANISTER_RIFLE, ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_PLASMA_RIFLE},
+ self.player)
+
+ def nova_splash(self, state: CollectionState) -> bool:
+ return state.has_any({
+ ItemNames.NOVA_HELLFIRE_SHOTGUN, ItemNames.NOVA_BLAZEFIRE_GUNBLADE, ItemNames.NOVA_PULSE_GRENADES
+ }, self.player) \
+ or self.advanced_tactics and state.has_any(
+ {ItemNames.NOVA_PLASMA_RIFLE, ItemNames.NOVA_MONOMOLECULAR_BLADE}, self.player)
+
+ def nova_dash(self, state: CollectionState) -> bool:
+ return state.has_any({ItemNames.NOVA_MONOMOLECULAR_BLADE, ItemNames.NOVA_BLINK}, self.player)
+
+ def nova_full_stealth(self, state: CollectionState) -> bool:
+ return state.count(ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, self.player) >= 2
+
+ def nova_heal(self, state: CollectionState) -> bool:
+ return state.has_any({ItemNames.NOVA_ARMORED_SUIT_MODULE, ItemNames.NOVA_STIM_INFUSION}, self.player)
+
+ def nova_escape_assist(self, state: CollectionState) -> bool:
+ return state.has_any({ItemNames.NOVA_BLINK, ItemNames.NOVA_HOLO_DECOY, ItemNames.NOVA_IONIC_FORCE_FIELD}, self.player)
+
+ def the_escape_stuff_granted(self) -> bool:
+ """
+ The NCO first mission requires having too much stuff first before actually able to do anything
+ :return:
+ """
+ return self.story_tech_granted \
+ or (self.mission_order == MissionOrder.option_vanilla and self.enabled_campaigns == {SC2Campaign.NCO})
+
+ def the_escape_first_stage_requirement(self, state: CollectionState) -> bool:
+ return self.the_escape_stuff_granted() \
+ or (self.nova_ranged_weapon(state) and (self.nova_full_stealth(state) or self.nova_heal(state)))
+
+ def the_escape_requirement(self, state: CollectionState) -> bool:
+ return self.the_escape_first_stage_requirement(state) \
+ and (self.the_escape_stuff_granted() or self.nova_splash(state))
+
+ def terran_cliffjumper(self, state: CollectionState) -> bool:
+ return state.has(ItemNames.REAPER, self.player) \
+ or state.has_all({ItemNames.GOLIATH, ItemNames.GOLIATH_JUMP_JETS}, self.player) \
+ or state.has_all({ItemNames.SIEGE_TANK, ItemNames.SIEGE_TANK_JUMP_JETS}, self.player)
+
+ def terran_able_to_snipe_defiler(self, state: CollectionState) -> bool:
+ return state.has_all({ItemNames.NOVA_JUMP_SUIT_MODULE, ItemNames.NOVA_C20A_CANISTER_RIFLE}, self.player) \
+ or state.has_all({ItemNames.SIEGE_TANK, ItemNames.SIEGE_TANK_MAELSTROM_ROUNDS, ItemNames.SIEGE_TANK_JUMP_JETS}, self.player)
+
+ def sudden_strike_requirement(self, state: CollectionState) -> bool:
+ return self.sudden_strike_can_reach_objectives(state) \
+ and self.terran_able_to_snipe_defiler(state) \
+ and state.has_any({ItemNames.SIEGE_TANK, ItemNames.VULTURE}, self.player) \
+ and self.nova_splash(state) \
+ and (self.terran_defense_rating(state, True, False) >= 2
+ or state.has(ItemNames.NOVA_JUMP_SUIT_MODULE, self.player))
+
+ def sudden_strike_can_reach_objectives(self, state: CollectionState) -> bool:
+ return self.terran_cliffjumper(state) \
+ or state.has_any({ItemNames.BANSHEE, ItemNames.VIKING}, self.player) \
+ or (
+ self.advanced_tactics
+ and state.has(ItemNames.MEDIVAC, self.player)
+ and state.has_any({ItemNames.MARINE, ItemNames.MARAUDER, ItemNames.VULTURE, ItemNames.HELLION,
+ ItemNames.GOLIATH}, self.player)
+ )
+
+ def enemy_intelligence_garrisonable_unit(self, state: CollectionState) -> bool:
+ """
+ Has unit usable as a Garrison in Enemy Intelligence
+ :param state:
+ :return:
+ """
+ return state.has_any(
+ {ItemNames.MARINE, ItemNames.REAPER, ItemNames.MARAUDER, ItemNames.GHOST, ItemNames.SPECTRE,
+ ItemNames.HELLION, ItemNames.GOLIATH, ItemNames.WARHOUND, ItemNames.DIAMONDBACK, ItemNames.VIKING},
+ self.player)
+
+ def enemy_intelligence_cliff_garrison(self, state: CollectionState) -> bool:
+ return state.has_any({ItemNames.REAPER, ItemNames.VIKING, ItemNames.MEDIVAC, ItemNames.HERCULES}, self.player) \
+ or state.has_all({ItemNames.GOLIATH, ItemNames.GOLIATH_JUMP_JETS}, self.player) \
+ or self.advanced_tactics and state.has_any({ItemNames.HELS_ANGELS, ItemNames.BRYNHILDS}, self.player)
+
+ def enemy_intelligence_first_stage_requirement(self, state: CollectionState) -> bool:
+ return self.enemy_intelligence_garrisonable_unit(state) \
+ and (self.terran_competent_comp(state)
+ or (
+ self.terran_common_unit(state)
+ and self.terran_competent_anti_air(state)
+ and state.has(ItemNames.NOVA_NUKE, self.player)
+ )
+ ) \
+ and self.terran_defense_rating(state, True, True) >= 5
+
+ def enemy_intelligence_second_stage_requirement(self, state: CollectionState) -> bool:
+ return self.enemy_intelligence_first_stage_requirement(state) \
+ and self.enemy_intelligence_cliff_garrison(state) \
+ and (
+ self.story_tech_granted
+ or (
+ self.nova_any_weapon(state)
+ and (
+ self.nova_full_stealth(state)
+ or (self.nova_heal(state)
+ and self.nova_splash(state)
+ and self.nova_ranged_weapon(state))
+ )
+ )
+ )
+
+ def enemy_intelligence_third_stage_requirement(self, state: CollectionState) -> bool:
+ return self.enemy_intelligence_second_stage_requirement(state) \
+ and (
+ self.story_tech_granted
+ or (
+ state.has(ItemNames.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE, self.player)
+ and self.nova_dash(state)
+ )
+ )
+
+ def trouble_in_paradise_requirement(self, state: CollectionState) -> bool:
+ return self.nova_any_weapon(state) \
+ and self.nova_splash(state) \
+ and self.terran_beats_protoss_deathball(state) \
+ and self.terran_defense_rating(state, True, True) >= 7
+
+ def night_terrors_requirement(self, state: CollectionState) -> bool:
+ return self.terran_common_unit(state) \
+ and self.terran_competent_anti_air(state) \
+ and (
+ # These can handle the waves of infested, even volatile ones
+ state.has(ItemNames.SIEGE_TANK, self.player)
+ or state.has_all({ItemNames.VIKING, ItemNames.VIKING_SHREDDER_ROUNDS}, self.player)
+ or (
+ (
+ # Regular infesteds
+ state.has(ItemNames.FIREBAT, self.player)
+ or state.has_all({ItemNames.HELLION, ItemNames.HELLION_HELLBAT_ASPECT}, self.player)
+ or (
+ self.advanced_tactics
+ and state.has_any({ItemNames.PERDITION_TURRET, ItemNames.PLANETARY_FORTRESS}, self.player)
+ )
+ )
+ and self.terran_bio_heal(state)
+ and (
+ # Volatile infesteds
+ state.has(ItemNames.LIBERATOR, self.player)
+ or (
+ self.advanced_tactics
+ and state.has_any({ItemNames.HERC, ItemNames.VULTURE}, self.player)
+ )
+ )
+ )
+ )
+
+ def flashpoint_far_requirement(self, state: CollectionState) -> bool:
+ return self.terran_competent_comp(state) \
+ and self.terran_mobile_detector(state) \
+ and self.terran_defense_rating(state, True, False) >= 6
+
+ def enemy_shadow_tripwires_tool(self, state: CollectionState) -> bool:
+ return state.has_any({ItemNames.NOVA_FLASHBANG_GRENADES, ItemNames.NOVA_BLINK, ItemNames.NOVA_DOMINATION},
+ self.player)
+
+ def enemy_shadow_door_unlocks_tool(self, state: CollectionState) -> bool:
+ return state.has_any({ItemNames.NOVA_DOMINATION, ItemNames.NOVA_BLINK, ItemNames.NOVA_JUMP_SUIT_MODULE},
+ self.player)
+
+ def enemy_shadow_domination(self, state: CollectionState) -> bool:
+ return self.story_tech_granted \
+ or (self.nova_ranged_weapon(state)
+ and (self.nova_full_stealth(state)
+ or state.has(ItemNames.NOVA_JUMP_SUIT_MODULE, self.player)
+ or (self.nova_heal(state) and self.nova_splash(state))
+ )
+ )
+
+ def enemy_shadow_first_stage(self, state: CollectionState) -> bool:
+ return self.enemy_shadow_domination(state) \
+ and (self.story_tech_granted
+ or ((self.nova_full_stealth(state) and self.enemy_shadow_tripwires_tool(state))
+ or (self.nova_heal(state) and self.nova_splash(state))
+ )
+ )
+
+ def enemy_shadow_second_stage(self, state: CollectionState) -> bool:
+ return self.enemy_shadow_first_stage(state) \
+ and (self.story_tech_granted
+ or self.nova_splash(state)
+ or self.nova_heal(state)
+ or self.nova_escape_assist(state)
+ )
+
+ def enemy_shadow_door_controls(self, state: CollectionState) -> bool:
+ return self.enemy_shadow_second_stage(state) \
+ and (self.story_tech_granted or self.enemy_shadow_door_unlocks_tool(state))
+
+ def enemy_shadow_victory(self, state: CollectionState) -> bool:
+ return self.enemy_shadow_door_controls(state) \
+ and (self.story_tech_granted or self.nova_heal(state))
+
+ def dark_skies_requirement(self, state: CollectionState) -> bool:
+ return self.terran_common_unit(state) \
+ and self.terran_beats_protoss_deathball(state) \
+ and self.terran_defense_rating(state, False, True) >= 8
+
+ def end_game_requirement(self, state: CollectionState) -> bool:
+ return self.terran_competent_comp(state) \
+ and self.terran_mobile_detector(state) \
+ and (
+ state.has_any({ItemNames.BATTLECRUISER, ItemNames.LIBERATOR, ItemNames.BANSHEE}, self.player)
+ or state.has_all({ItemNames.WRAITH, ItemNames.WRAITH_ADVANCED_LASER_TECHNOLOGY}, self.player)
+ ) \
+ and (state.has_any({ItemNames.BATTLECRUISER, ItemNames.VIKING, ItemNames.LIBERATOR}, self.player)
+ or (self.advanced_tactics
+ and state.has_all({ItemNames.RAVEN, ItemNames.RAVEN_HUNTER_SEEKER_WEAPON}, self.player)
+ )
+ )
+
+ def __init__(self, world: World):
+ self.world: World = world
+ self.player = None if world is None else world.player
+ self.logic_level = get_option_value(world, 'required_tactics')
+ self.advanced_tactics = self.logic_level != RequiredTactics.option_standard
+ self.take_over_ai_allies = get_option_value(world, "take_over_ai_allies") == TakeOverAIAllies.option_true
+ self.kerrigan_unit_available = get_option_value(world, 'kerrigan_presence') in kerrigan_unit_available \
+ and SC2Campaign.HOTS in get_enabled_campaigns(world)
+ self.kerrigan_levels_per_mission_completed = get_option_value(world, "kerrigan_levels_per_mission_completed")
+ self.kerrigan_levels_per_mission_completed_cap = get_option_value(world, "kerrigan_levels_per_mission_completed_cap")
+ self.kerrigan_total_level_cap = get_option_value(world, "kerrigan_total_level_cap")
+ self.story_tech_granted = get_option_value(world, "grant_story_tech") == GrantStoryTech.option_true
+ self.story_levels_granted = get_option_value(world, "grant_story_levels") != GrantStoryLevels.option_disabled
+ self.basic_terran_units = get_basic_units(world, SC2Race.TERRAN)
+ self.basic_zerg_units = get_basic_units(world, SC2Race.ZERG)
+ self.basic_protoss_units = get_basic_units(world, SC2Race.PROTOSS)
+ self.spear_of_adun_autonomously_cast_presence = get_option_value(world, "spear_of_adun_autonomously_cast_ability_presence")
+ self.enabled_campaigns = get_enabled_campaigns(world)
+ self.mission_order = get_option_value(world, "mission_order")
diff --git a/worlds/sc2/Starcraft2.kv b/worlds/sc2/Starcraft2.kv
new file mode 100644
index 000000000000..6b112c2f00a6
--- /dev/null
+++ b/worlds/sc2/Starcraft2.kv
@@ -0,0 +1,28 @@
+
+ scroll_type: ["content", "bars"]
+ bar_width: dp(12)
+ effect_cls: "ScrollEffect"
+
+
+ cols: 1
+ size_hint_y: None
+ height: self.minimum_height + 15
+ padding: [5,0,dp(12),0]
+
+:
+ cols: 1
+
+:
+ rows: 1
+
+:
+ cols: 1
+ spacing: [0,5]
+
+:
+ text_size: self.size
+ markup: True
+ halign: 'center'
+ valign: 'middle'
+ padding: [5,0,5,0]
+ outline_width: 1
diff --git a/worlds/sc2/__init__.py b/worlds/sc2/__init__.py
new file mode 100644
index 000000000000..ec8a447d931e
--- /dev/null
+++ b/worlds/sc2/__init__.py
@@ -0,0 +1,490 @@
+import typing
+from dataclasses import fields
+
+from typing import List, Set, Iterable, Sequence, Dict, Callable, Union
+from math import floor, ceil
+from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification
+from worlds.AutoWorld import WebWorld, World
+from . import ItemNames
+from .Items import StarcraftItem, filler_items, get_item_table, get_full_item_list, \
+ get_basic_units, ItemData, upgrade_included_names, progressive_if_nco, kerrigan_actives, kerrigan_passives, \
+ kerrigan_only_passives, progressive_if_ext, not_balanced_starting_units, spear_of_adun_calldowns, \
+ spear_of_adun_castable_passives, nova_equipment
+from .ItemGroups import item_name_groups
+from .Locations import get_locations, LocationType, get_location_types, get_plando_locations
+from .Regions import create_regions
+from .Options import get_option_value, LocationInclusion, KerriganLevelItemDistribution, \
+ KerriganPresence, KerriganPrimalStatus, RequiredTactics, kerrigan_unit_available, StarterUnit, SpearOfAdunPresence, \
+ get_enabled_campaigns, SpearOfAdunAutonomouslyCastAbilityPresence, Starcraft2Options
+from .PoolFilter import filter_items, get_item_upgrades, UPGRADABLE_ITEMS, missions_in_mission_table, get_used_races
+from .MissionTables import MissionInfo, SC2Campaign, lookup_name_to_mission, SC2Mission, \
+ SC2Race
+
+
+class Starcraft2WebWorld(WebWorld):
+ setup_en = Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to setting up the Starcraft 2 randomizer connected to an Archipelago Multiworld",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["TheCondor", "Phaneros"]
+ )
+
+ setup_fr = Tutorial(
+ setup_en.tutorial_name,
+ setup_en.description,
+ "Français",
+ "setup_fr.md",
+ "setup/fr",
+ ["Neocerber"]
+ )
+
+ tutorials = [setup_en, setup_fr]
+
+
+class SC2World(World):
+ """
+ StarCraft II is a science fiction real-time strategy video game developed and published by Blizzard Entertainment.
+ Play as one of three factions across four campaigns in a battle for supremacy of the Koprulu Sector.
+ """
+
+ game = "Starcraft 2"
+ web = Starcraft2WebWorld()
+
+ item_name_to_id = {name: data.code for name, data in get_full_item_list().items()}
+ location_name_to_id = {location.name: location.code for location in get_locations(None)}
+ options_dataclass = Starcraft2Options
+ options: Starcraft2Options
+
+ item_name_groups = item_name_groups
+ locked_locations: typing.List[str]
+ location_cache: typing.List[Location]
+ mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]] = {}
+ final_mission_id: int
+ victory_item: str
+ required_client_version = 0, 4, 5
+
+ def __init__(self, multiworld: MultiWorld, player: int):
+ super(SC2World, self).__init__(multiworld, player)
+ self.location_cache = []
+ self.locked_locations = []
+
+ def create_item(self, name: str) -> Item:
+ data = get_full_item_list()[name]
+ return StarcraftItem(name, data.classification, data.code, self.player)
+
+ def create_regions(self):
+ self.mission_req_table, self.final_mission_id, self.victory_item = create_regions(
+ self, get_locations(self), self.location_cache
+ )
+
+ def create_items(self):
+ setup_events(self.player, self.locked_locations, self.location_cache)
+
+ excluded_items = get_excluded_items(self)
+
+ starter_items = assign_starter_items(self, excluded_items, self.locked_locations, self.location_cache)
+
+ fill_resource_locations(self, self.locked_locations, self.location_cache)
+
+ pool = get_item_pool(self, self.mission_req_table, starter_items, excluded_items, self.location_cache)
+
+ fill_item_pool_with_dummy_items(self, self.locked_locations, self.location_cache, pool)
+
+ self.multiworld.itempool += pool
+
+ def set_rules(self):
+ self.multiworld.completion_condition[self.player] = lambda state: state.has(self.victory_item, self.player)
+
+ def get_filler_item_name(self) -> str:
+ return self.random.choice(filler_items)
+
+ def fill_slot_data(self):
+ slot_data = {}
+ for option_name in [field.name for field in fields(Starcraft2Options)]:
+ option = get_option_value(self, option_name)
+ if type(option) in {str, int}:
+ slot_data[option_name] = int(option)
+ slot_req_table = {}
+
+ # Serialize data
+ for campaign in self.mission_req_table:
+ slot_req_table[campaign.id] = {}
+ for mission in self.mission_req_table[campaign]:
+ slot_req_table[campaign.id][mission] = self.mission_req_table[campaign][mission]._asdict()
+ # Replace mission objects with mission IDs
+ slot_req_table[campaign.id][mission]["mission"] = slot_req_table[campaign.id][mission]["mission"].id
+
+ for index in range(len(slot_req_table[campaign.id][mission]["required_world"])):
+ # TODO this is a band-aid, sometimes the mission_req_table already contains dicts
+ # as far as I can tell it's related to having multiple vanilla mission orders
+ if not isinstance(slot_req_table[campaign.id][mission]["required_world"][index], dict):
+ slot_req_table[campaign.id][mission]["required_world"][index] = slot_req_table[campaign.id][mission]["required_world"][index]._asdict()
+
+ enabled_campaigns = get_enabled_campaigns(self)
+ slot_data["plando_locations"] = get_plando_locations(self)
+ slot_data["nova_covert_ops_only"] = (enabled_campaigns == {SC2Campaign.NCO})
+ slot_data["mission_req"] = slot_req_table
+ slot_data["final_mission"] = self.final_mission_id
+ slot_data["version"] = 3
+
+ if SC2Campaign.HOTS not in enabled_campaigns:
+ slot_data["kerrigan_presence"] = KerriganPresence.option_not_present
+ return slot_data
+
+
+def setup_events(player: int, locked_locations: typing.List[str], location_cache: typing.List[Location]):
+ for location in location_cache:
+ if location.address is None:
+ item = Item(location.name, ItemClassification.progression, None, player)
+
+ locked_locations.append(location.name)
+
+ location.place_locked_item(item)
+
+
+def get_excluded_items(world: World) -> Set[str]:
+ excluded_items: Set[str] = set(get_option_value(world, 'excluded_items'))
+ for item in world.multiworld.precollected_items[world.player]:
+ excluded_items.add(item.name)
+ locked_items: Set[str] = set(get_option_value(world, 'locked_items'))
+ # Starter items are also excluded items
+ starter_items: Set[str] = set(get_option_value(world, 'start_inventory'))
+ item_table = get_full_item_list()
+ soa_presence = get_option_value(world, "spear_of_adun_presence")
+ soa_autocast_presence = get_option_value(world, "spear_of_adun_autonomously_cast_ability_presence")
+ enabled_campaigns = get_enabled_campaigns(world)
+
+ # Ensure no item is both guaranteed and excluded
+ invalid_items = excluded_items.intersection(locked_items)
+ invalid_count = len(invalid_items)
+ # Don't count starter items that can appear multiple times
+ invalid_count -= len([item for item in starter_items.intersection(locked_items) if item_table[item].quantity != 1])
+ if invalid_count > 0:
+ raise Exception(f"{invalid_count} item{'s are' if invalid_count > 1 else ' is'} both locked and excluded from generation. Please adjust your excluded items and locked items.")
+
+ def smart_exclude(item_choices: Set[str], choices_to_keep: int):
+ expected_choices = len(item_choices)
+ if expected_choices == 0:
+ return
+ item_choices = set(item_choices)
+ starter_choices = item_choices.intersection(starter_items)
+ excluded_choices = item_choices.intersection(excluded_items)
+ item_choices.difference_update(excluded_choices)
+ item_choices.difference_update(locked_items)
+ candidates = sorted(item_choices)
+ exclude_amount = min(expected_choices - choices_to_keep - len(excluded_choices) + len(starter_choices), len(candidates))
+ if exclude_amount > 0:
+ excluded_items.update(world.random.sample(candidates, exclude_amount))
+
+ # Nova gear exclusion if NCO not in campaigns
+ if SC2Campaign.NCO not in enabled_campaigns:
+ excluded_items = excluded_items.union(nova_equipment)
+
+ kerrigan_presence = get_option_value(world, "kerrigan_presence")
+ # Exclude Primal Form item if option is not set or Kerrigan is unavailable
+ if get_option_value(world, "kerrigan_primal_status") != KerriganPrimalStatus.option_item or \
+ (kerrigan_presence in {KerriganPresence.option_not_present, KerriganPresence.option_not_present_and_no_passives}):
+ excluded_items.add(ItemNames.KERRIGAN_PRIMAL_FORM)
+
+ # no Kerrigan & remove all passives => remove all abilities
+ if kerrigan_presence == KerriganPresence.option_not_present_and_no_passives:
+ for tier in range(7):
+ smart_exclude(kerrigan_actives[tier].union(kerrigan_passives[tier]), 0)
+ else:
+ # no Kerrigan, but keep non-Kerrigan passives
+ if kerrigan_presence == KerriganPresence.option_not_present:
+ smart_exclude(kerrigan_only_passives, 0)
+ for tier in range(7):
+ smart_exclude(kerrigan_actives[tier], 0)
+
+ # SOA exclusion, other cases are handled by generic race logic
+ if (soa_presence == SpearOfAdunPresence.option_lotv_protoss and SC2Campaign.LOTV not in enabled_campaigns) \
+ or soa_presence == SpearOfAdunPresence.option_not_present:
+ excluded_items.update(spear_of_adun_calldowns)
+ if (soa_autocast_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_lotv_protoss \
+ and SC2Campaign.LOTV not in enabled_campaigns) \
+ or soa_autocast_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_not_present:
+ excluded_items.update(spear_of_adun_castable_passives)
+
+ return excluded_items
+
+
+def assign_starter_items(world: World, excluded_items: Set[str], locked_locations: List[str], location_cache: typing.List[Location]) -> List[Item]:
+ starter_items: List[Item] = []
+ non_local_items = get_option_value(world, "non_local_items")
+ starter_unit = get_option_value(world, "starter_unit")
+ enabled_campaigns = get_enabled_campaigns(world)
+ first_mission = get_first_mission(world.mission_req_table)
+ # Ensuring that first mission is completable
+ if starter_unit == StarterUnit.option_off:
+ starter_mission_locations = [location.name for location in location_cache
+ if location.parent_region.name == first_mission
+ and location.access_rule == Location.access_rule]
+ if not starter_mission_locations:
+ # Force early unit if first mission is impossible without one
+ starter_unit = StarterUnit.option_any_starter_unit
+
+ if starter_unit != StarterUnit.option_off:
+ first_race = lookup_name_to_mission[first_mission].race
+
+ if first_race == SC2Race.ANY:
+ # If the first mission is a logic-less no-build
+ mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]] = world.mission_req_table
+ races = get_used_races(mission_req_table, world)
+ races.remove(SC2Race.ANY)
+ if lookup_name_to_mission[first_mission].race in races:
+ # The campaign's race is in (At least one mission that's not logic-less no-build exists)
+ first_race = lookup_name_to_mission[first_mission].campaign.race
+ elif len(races) > 0:
+ # The campaign only has logic-less no-build missions. Find any other valid race
+ first_race = world.random.choice(list(races))
+
+ if first_race != SC2Race.ANY:
+ # The race of the early unit has been chosen
+ basic_units = get_basic_units(world, first_race)
+ if starter_unit == StarterUnit.option_balanced:
+ basic_units = basic_units.difference(not_balanced_starting_units)
+ if first_mission == SC2Mission.DARK_WHISPERS.mission_name:
+ # Special case - you don't have a logicless location but need an AA
+ basic_units = basic_units.difference(
+ {ItemNames.ZEALOT, ItemNames.CENTURION, ItemNames.SENTINEL, ItemNames.BLOOD_HUNTER,
+ ItemNames.AVENGER, ItemNames.IMMORTAL, ItemNames.ANNIHILATOR, ItemNames.VANGUARD})
+ if first_mission == SC2Mission.SUDDEN_STRIKE.mission_name:
+ # Special case - cliffjumpers
+ basic_units = {ItemNames.REAPER, ItemNames.GOLIATH, ItemNames.SIEGE_TANK, ItemNames.VIKING, ItemNames.BANSHEE}
+ local_basic_unit = sorted(item for item in basic_units if item not in non_local_items and item not in excluded_items)
+ if not local_basic_unit:
+ # Drop non_local_items constraint
+ local_basic_unit = sorted(item for item in basic_units if item not in excluded_items)
+ if not local_basic_unit:
+ raise Exception("Early Unit: At least one basic unit must be included")
+
+ unit: Item = add_starter_item(world, excluded_items, local_basic_unit)
+ starter_items.append(unit)
+
+ # NCO-only specific rules
+ if first_mission == SC2Mission.SUDDEN_STRIKE.mission_name:
+ support_item: Union[str, None] = None
+ if unit.name == ItemNames.REAPER:
+ support_item = ItemNames.REAPER_SPIDER_MINES
+ elif unit.name == ItemNames.GOLIATH:
+ support_item = ItemNames.GOLIATH_JUMP_JETS
+ elif unit.name == ItemNames.SIEGE_TANK:
+ support_item = ItemNames.SIEGE_TANK_JUMP_JETS
+ elif unit.name == ItemNames.VIKING:
+ support_item = ItemNames.VIKING_SMART_SERVOS
+ if support_item is not None:
+ starter_items.append(add_starter_item(world, excluded_items, [support_item]))
+ starter_items.append(add_starter_item(world, excluded_items, [ItemNames.NOVA_JUMP_SUIT_MODULE]))
+ starter_items.append(
+ add_starter_item(world, excluded_items,
+ [
+ ItemNames.NOVA_HELLFIRE_SHOTGUN,
+ ItemNames.NOVA_PLASMA_RIFLE,
+ ItemNames.NOVA_PULSE_GRENADES
+ ]))
+ if enabled_campaigns == {SC2Campaign.NCO}:
+ starter_items.append(add_starter_item(world, excluded_items, [ItemNames.LIBERATOR_RAID_ARTILLERY]))
+
+ starter_abilities = get_option_value(world, 'start_primary_abilities')
+ assert isinstance(starter_abilities, int)
+ if starter_abilities:
+ ability_count = starter_abilities
+ ability_tiers = [0, 1, 3]
+ world.random.shuffle(ability_tiers)
+ if ability_count > 3:
+ ability_tiers.append(6)
+ for tier in ability_tiers:
+ abilities = kerrigan_actives[tier].union(kerrigan_passives[tier]).difference(excluded_items, non_local_items)
+ if not abilities:
+ abilities = kerrigan_actives[tier].union(kerrigan_passives[tier]).difference(excluded_items)
+ if abilities:
+ ability_count -= 1
+ starter_items.append(add_starter_item(world, excluded_items, list(abilities)))
+ if ability_count == 0:
+ break
+
+ return starter_items
+
+
+def get_first_mission(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]]) -> str:
+ # The first world should also be the starting world
+ campaigns = mission_req_table.keys()
+ lowest_id = min([campaign.id for campaign in campaigns])
+ first_campaign = [campaign for campaign in campaigns if campaign.id == lowest_id][0]
+ first_mission = list(mission_req_table[first_campaign])[0]
+ return first_mission
+
+
+def add_starter_item(world: World, excluded_items: Set[str], item_list: Sequence[str]) -> Item:
+
+ item_name = world.random.choice(sorted(item_list))
+
+ excluded_items.add(item_name)
+
+ item = create_item_with_correct_settings(world.player, item_name)
+
+ world.multiworld.push_precollected(item)
+
+ return item
+
+
+def get_item_pool(world: World, mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]],
+ starter_items: List[Item], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]:
+ pool: List[Item] = []
+
+ # For the future: goal items like Artifact Shards go here
+ locked_items = []
+
+ # YAML items
+ yaml_locked_items = get_option_value(world, 'locked_items')
+ assert not isinstance(yaml_locked_items, int)
+
+ # Adjust generic upgrade availability based on options
+ include_upgrades = get_option_value(world, 'generic_upgrade_missions') == 0
+ upgrade_items = get_option_value(world, 'generic_upgrade_items')
+ assert isinstance(upgrade_items, int)
+
+ # Include items from outside main campaigns
+ item_sets = {'wol', 'hots', 'lotv'}
+ if get_option_value(world, 'nco_items') \
+ or SC2Campaign.NCO in get_enabled_campaigns(world):
+ item_sets.add('nco')
+ if get_option_value(world, 'bw_items'):
+ item_sets.add('bw')
+ if get_option_value(world, 'ext_items'):
+ item_sets.add('ext')
+
+ def allowed_quantity(name: str, data: ItemData) -> int:
+ if name in excluded_items \
+ or data.type == "Upgrade" and (not include_upgrades or name not in upgrade_included_names[upgrade_items]) \
+ or not data.origin.intersection(item_sets):
+ return 0
+ elif name in progressive_if_nco and 'nco' not in item_sets:
+ return 1
+ elif name in progressive_if_ext and 'ext' not in item_sets:
+ return 1
+ else:
+ return data.quantity
+
+ for name, data in get_item_table().items():
+ for _ in range(allowed_quantity(name, data)):
+ item = create_item_with_correct_settings(world.player, name)
+ if name in yaml_locked_items:
+ locked_items.append(item)
+ else:
+ pool.append(item)
+
+ existing_items = starter_items + [item for item in world.multiworld.precollected_items[world.player] if item not in starter_items]
+ existing_names = [item.name for item in existing_items]
+
+ # Check the parent item integrity, exclude items
+ pool[:] = [item for item in pool if pool_contains_parent(item, pool + locked_items + existing_items)]
+
+ # Removing upgrades for excluded items
+ for item_name in excluded_items:
+ if item_name in existing_names:
+ continue
+ invalid_upgrades = get_item_upgrades(pool, item_name)
+ for invalid_upgrade in invalid_upgrades:
+ pool.remove(invalid_upgrade)
+
+ fill_pool_with_kerrigan_levels(world, pool)
+ filtered_pool = filter_items(world, mission_req_table, location_cache, pool, existing_items, locked_items)
+ return filtered_pool
+
+
+def fill_item_pool_with_dummy_items(self: SC2World, locked_locations: List[str],
+ location_cache: List[Location], pool: List[Item]):
+ for _ in range(len(location_cache) - len(locked_locations) - len(pool)):
+ item = create_item_with_correct_settings(self.player, self.get_filler_item_name())
+ pool.append(item)
+
+
+def create_item_with_correct_settings(player: int, name: str) -> Item:
+ data = get_full_item_list()[name]
+
+ item = Item(name, data.classification, data.code, player)
+
+ return item
+
+
+def pool_contains_parent(item: Item, pool: Iterable[Item]):
+ item_data = get_full_item_list().get(item.name)
+ if item_data.parent_item is None:
+ # The item has not associated parent, the item is valid
+ return True
+ parent_item = item_data.parent_item
+ # Check if the pool contains the parent item
+ return parent_item in [pool_item.name for pool_item in pool]
+
+
+def fill_resource_locations(world: World, locked_locations: List[str], location_cache: List[Location]):
+ """
+ Filters the locations in the world using a trash or Nothing item
+ :param multiworld:
+ :param player:
+ :param locked_locations:
+ :param location_cache:
+ :return:
+ """
+ open_locations = [location for location in location_cache if location.item is None]
+ plando_locations = get_plando_locations(world)
+ resource_location_types = get_location_types(world, LocationInclusion.option_resources)
+ location_data = {sc2_location.name: sc2_location for sc2_location in get_locations(world)}
+ for location in open_locations:
+ # Go through the locations that aren't locked yet (early unit, etc)
+ if location.name not in plando_locations:
+ # The location is not plando'd
+ sc2_location = location_data[location.name]
+ if sc2_location.type in resource_location_types:
+ item_name = world.random.choice(filler_items)
+ item = create_item_with_correct_settings(world.player, item_name)
+ location.place_locked_item(item)
+ locked_locations.append(location.name)
+
+
+def place_exclusion_item(item_name, location, locked_locations, player):
+ item = create_item_with_correct_settings(player, item_name)
+ location.place_locked_item(item)
+ locked_locations.append(location.name)
+
+
+def fill_pool_with_kerrigan_levels(world: World, item_pool: List[Item]):
+ total_levels = get_option_value(world, "kerrigan_level_item_sum")
+ if get_option_value(world, "kerrigan_presence") not in kerrigan_unit_available \
+ or total_levels == 0 \
+ or SC2Campaign.HOTS not in get_enabled_campaigns(world):
+ return
+
+ def add_kerrigan_level_items(level_amount: int, item_amount: int):
+ name = f"{level_amount} Kerrigan Level"
+ if level_amount > 1:
+ name += "s"
+ for _ in range(item_amount):
+ item_pool.append(create_item_with_correct_settings(world.player, name))
+
+ sizes = [70, 35, 14, 10, 7, 5, 2, 1]
+ option = get_option_value(world, "kerrigan_level_item_distribution")
+
+ assert isinstance(option, int)
+ assert isinstance(total_levels, int)
+
+ if option in (KerriganLevelItemDistribution.option_vanilla, KerriganLevelItemDistribution.option_smooth):
+ distribution = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ if option == KerriganLevelItemDistribution.option_vanilla:
+ distribution = [32, 0, 0, 1, 3, 0, 0, 0, 1, 1]
+ else: # Smooth
+ distribution = [0, 0, 0, 1, 1, 2, 2, 2, 1, 1]
+ for tier in range(len(distribution)):
+ add_kerrigan_level_items(tier + 1, distribution[tier])
+ else:
+ size = sizes[option - 2]
+ round_func: Callable[[float], int] = round
+ if total_levels > 70:
+ round_func = floor
+ else:
+ round_func = ceil
+ add_kerrigan_level_items(size, round_func(float(total_levels) / size))
diff --git a/worlds/sc2/docs/contributors.md b/worlds/sc2/docs/contributors.md
new file mode 100644
index 000000000000..5b62466d7e45
--- /dev/null
+++ b/worlds/sc2/docs/contributors.md
@@ -0,0 +1,42 @@
+# Contributors
+Contibutors are listed with preferred or Discord names first, with github usernames prepended with an `@`
+
+## Update 2024.0
+### Code Changes
+* Ziktofel (@Ziktofel)
+* Salzkorn (@Salzkorn)
+* EnvyDragon (@EnvyDragon)
+* Phanerus (@MatthewMarinets)
+* Madi Sylveon (@MadiMadsen)
+* Magnemania (@Magnemania)
+* Subsourian (@Subsourian)
+* Hopop (@hopop201)
+* Alice Voltaire (@AliceVoltaire)
+* Genderdruid (@ArchonofFail)
+* CrazedCollie (@FoxOfWar)
+
+### Additional Beta testing and bug reports
+* Varcklen (@Varcklen)
+* BicolourSnake (@Bicoloursnake)
+* @NobleXenon
+* Severencir (@Severencir)
+* neocerber (@neocerber)
+* Mati (@Matiya-star)
+* Ixzine
+* sweetox
+* 8thDaughterOfFrost
+* The M8
+* Berserker (@Berserker66)
+* KaitoKid
+* Sheen
+* ProfBytes
+* IncoherentOrange
+* eudaimonistic
+* Figment
+
+## Older versions
+Not all contributors to older versions of Archipelago Starcraft 2 are known.
+
+TheCondor (@TheCondor07) is the original maintainer of the project. Other known contributors include:
+* soldieroforder
+* Berserker (@Berserker66)
diff --git a/worlds/sc2/docs/en_Starcraft 2.md b/worlds/sc2/docs/en_Starcraft 2.md
new file mode 100644
index 000000000000..06464e3cd2fd
--- /dev/null
+++ b/worlds/sc2/docs/en_Starcraft 2.md
@@ -0,0 +1,67 @@
+# Starcraft 2
+
+## Game page in other languages:
+* [Français](/games/Starcraft%202/info/fr)
+
+## What does randomization do to this game?
+
+The following unlocks are randomized as items:
+1. Your ability to build any non-worker unit.
+2. Unit specific upgrades including some combinations not available in the vanilla campaigns, such as both strain choices simultaneously for Zerg and every Spear of Adun upgrade simultaneously for Protoss!
+3. Your ability to get the generic unit upgrades, such as attack and armour upgrades.
+4. Other miscellaneous upgrades such as laboratory upgrades and mercenaries for Terran, Kerrigan levels and upgrades for Zerg, and Spear of Adun upgrades for Protoss.
+5. Small boosts to your starting mineral, vespene gas, and supply totals on each mission.
+
+You find items by making progress in these categories:
+* Completing missions
+* Completing bonus objectives (like by gathering lab research material in Wings of Liberty)
+* Reaching milestones in the mission, such as completing part of a main objective
+* Completing challenges based on achievements in the base game, such as clearing all Zerg on Devil's Playground
+
+Except for mission completion, these categories can be disabled in the game's settings. For instance, you can disable getting items for reaching required milestones.
+
+When you receive items, they will immediately become available, even during a mission, and you will be
+notified via a text box in the top-right corner of the game screen. Item unlocks are also logged in the Archipelago client.
+
+Missions are launched through the Starcraft 2 Archipelago client, through the Starcraft 2 Launcher tab. The between mission segments on the Hyperion, the Leviathan, and the Spear of Adun are not included. Additionally, metaprogression currencies such as credits and Solarite are not used.
+
+## What is the goal of this game when randomized?
+
+The goal is to beat the final mission in the mission order. The yaml configuration file controls the mission order and how missions are shuffled.
+
+## What non-randomized changes are there from vanilla Starcraft 2?
+
+1. Some missions have more vespene geysers available to allow a wider variety of units.
+2. Many new units and upgrades have been added as items, coming from co-op, melee, later campaigns, later expansions, brood war, and original ideas.
+3. Higher-tech production structures, including Factories, Starports, Robotics Facilities, and Stargates, no longer have tech requirements.
+4. Zerg missions have been adjusted to give the player a starting Lair where they would only have Hatcheries.
+5. Upgrades with a downside have had the downside removed, such as automated refineries costing more or tech reactors taking longer to build.
+6. Unit collision within the vents in Enemy Within has been adjusted to allow larger units to travel through them without getting stuck in odd places.
+7. Several vanilla bugs have been fixed.
+
+## Which of my items can be in another player's world?
+
+By default, any of StarCraft 2's items (specified above) can be in another player's world. See the
+[Advanced YAML Guide](/tutorial/Archipelago/advanced_settings/en)
+for more information on how to change this.
+
+## Unique Local Commands
+
+The following commands are only available when using the Starcraft 2 Client to play with Archipelago. You can list them any time in the client with `/help`.
+
+* `/download_data` Download the most recent release of the necessary files for playing SC2 with Archipelago. Will overwrite existing files
+* `/difficulty [difficulty]` Overrides the difficulty set for the world.
+ * Options: casual, normal, hard, brutal
+* `/game_speed [game_speed]` Overrides the game speed for the world
+ * Options: default, slower, slow, normal, fast, faster
+* `/color [faction] [color]` Changes your color for one of your playable factions.
+ * Faction options: raynor, kerrigan, primal, protoss, nova
+ * Color options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen, brown, lightgreen, darkgrey, pink, rainbow, random, default
+* `/option [option_name] [option_value]` Sets an option normally controlled by your yaml after generation.
+ * Run without arguments to list all options.
+ * Options pertain to automatic cutscene skipping, Kerrigan presence, Spear of Adun presence, starting resource amounts, controlling AI allies, etc.
+* `/disable_mission_check` Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play the next mission in a chain the other player is doing.
+* `/play [mission_id]` Starts a Starcraft 2 mission based off of the mission_id provided
+* `/available` Get what missions are currently available to play
+* `/unfinished` Get what missions are currently available to play and have not had all locations checked
+* `/set_path [path]` Manually set the SC2 install directory (if the automatic detection fails)
diff --git a/worlds/sc2/docs/fr_Starcraft 2.md b/worlds/sc2/docs/fr_Starcraft 2.md
new file mode 100644
index 000000000000..4fcc8e689baa
--- /dev/null
+++ b/worlds/sc2/docs/fr_Starcraft 2.md
@@ -0,0 +1,95 @@
+# *StarCraft 2*
+
+## Quel est l'effet de la *randomization* sur ce jeu ?
+
+Les éléments qui suivent sont les *items* qui sont *randomized* et qui doivent être débloqués pour être utilisés dans
+le jeu:
+1. La capacité de produire des unités, excepté les drones/probes/scv.
+2. Des améliorations spécifiques à certaines unités incluant quelques combinaisons qui ne sont pas disponibles dans les
+campagnes génériques, comme le fait d'avoir les deux types d'évolution en même temps pour une unité *Zerg* et toutes
+les améliorations de la *Spear of Adun* simultanément pour les *Protoss*.
+3. L'accès aux améliorations génériques des unités, e.g. les améliorations d'attaque et d'armure.
+4. D'autres améliorations diverses telles que les améliorations de laboratoire et les mercenaires pour les *Terran*,
+les niveaux et les améliorations de Kerrigan pour les *Zerg*, et les améliorations de la *Spear of Adun* pour les
+*Protoss*.
+5. Avoir des *minerals*, du *vespene gas*, et du *supply* au début de chaque mission.
+
+Les *items* sont trouvés en accomplissant du progrès dans les catégories suivantes:
+* Terminer des missions
+* Réussir des objectifs supplémentaires (e.g., récolter le matériel pour les recherches dans *Wings of Liberty*)
+* Atteindre des étapes importantes dans la mission, e.g. réussir des sous-objectifs
+* Réussir des défis basés sur les succès du jeu de base, e.g. éliminer tous les *Zerg* dans la mission
+*Devil's Playground*
+
+Ces catégories, outre la première, peuvent être désactivées dans les options du jeu.
+Par exemple, vous pouvez désactiver le fait d'obtenir des *items* lorsque des étapes importantes d'une mission sont
+accomplies.
+
+Quand vous recevez un *item*, il devient immédiatement disponible, même pendant une mission, et vous serez avertis via
+la boîte de texte situé dans le coin en haut à droite de *StarCraft 2*.
+L'acquisition d'un *item* est aussi indiquée dans le client d'Archipelago.
+
+Les missions peuvent être lancées par le client *StarCraft 2 Archipelago*, via l'interface graphique de l'onglet
+*StarCraft 2 Launcher*.
+Les segments qui se passent sur l'*Hyperion*, un Léviathan et la *Spear of Adun* ne sont pas inclus.
+De plus, les points de progression tels que les crédits ou la Solarite ne sont pas utilisés dans *StarCraft 2
+Archipelago*.
+
+## Quel est le but de ce jeu quand il est *randomized*?
+
+Le but est de réussir la mission finale dans la disposition des missions (e.g. *blitz*, *grid*, etc.).
+Les choix faits dans le fichier *yaml* définissent la disposition des missions et comment elles sont mélangées.
+
+## Quelles sont les modifications non aléatoires comparativement à la version de base de *StarCraft 2*
+
+1. Certaines des missions ont plus de *vespene geysers* pour permettre l'utilisation d'une plus grande variété d'unités.
+2. Plusieurs unités et améliorations ont été ajoutées sous la forme d*items*.
+Ils proviennent de la version *co-op*, *melee*, des autres campagnes, d'expansions ultérieures, de *Brood War*, ou de
+l'imagination des développeurs de *StarCraft 2 Archipelago*.
+3. Les structures de production, e.g. *Factory*, *Starport*, *Robotics Facility*, and *Stargate*, n'ont plus
+d'exigences technologiques.
+4. Les missions avec la race *Zerg* ont été modifiées pour que les joueurs débuttent avec un *Lair* lorsqu'elles
+commençaient avec une *Hatchery*.
+5. Les désavantages des améliorations ont été enlevés, e.g. *automated refinery* qui coûte plus cher ou les *tech
+reactors* qui prennent plus de temps à construire.
+6. La collision des unités dans les couloirs de la mission *Enemy Within* a été ajustée pour permettre des unités
+plus larges de les traverser sans être coincés dans des endroits étranges.
+7. Plusieurs *bugs* du jeu original ont été corrigés.
+
+## Quels sont les *items* qui peuvent être dans le monde d'un autre joueur?
+
+Par défaut, tous les *items* de *StarCraft 2 Archipelago* (voir la section précédente) peuvent être dans le monde d'un
+autre joueur.
+Consulter [*Advanced YAML Guide*](/tutorial/Archipelago/advanced_settings/en) pour savoir comment
+changer ça.
+
+## Commandes du client qui sont uniques à ce jeu
+
+Les commandes qui suivent sont seulement disponibles uniquement pour le client de *StarCraft 2 Archipelago*.
+Vous pouvez les afficher en utilisant la commande `/help` dans le client de *StarCraft 2 Archipelago*.
+Toutes ces commandes affectent seulement le client où elles sont utilisées.
+
+* `/download_data` Télécharge les versions les plus récentes des fichiers pour jouer à *StarCraft 2 Archipelago*.
+Les fichiers existants vont être écrasés.
+* `/difficulty [difficulty]` Remplace la difficulté choisie pour le monde.
+ * Les options sont *casual*, *normal*, *hard*, et *brutal*.
+* `/game_speed [game_speed]` Remplace la vitesse du jeu pour le monde.
+ * Les options sont *default*, *slower*, *slow*, *normal*, *fast*, and *faster*.
+* `/color [faction] [color]` Remplace la couleur d'une des *factions* qui est jouable.
+ * Les options de *faction*: raynor, kerrigan, primal, protoss, nova.
+ * Les options de couleur: *white*, *red*, *blue*, *teal*, *purple*, *yellow*, *orange*, *green*, *lightpink*,
+*violet*, *lightgrey*, *darkgreen*, *brown*, *lightgreen*, *darkgrey*, *pink*, *rainbow*, *random*, *default*.
+* `/option [option_name] [option_value]` Permet de changer un option normalement définit dans le *yaml*.
+ * Si la commande est lancée sans option, la liste des options qui sont modifiables va être affichée.
+ * Les options qui peuvent être changées avec cette commande incluent sauter les cinématiques automatiquement, la
+présence de Kerrigan dans les missions, la disponibilité de la *Spear of Adun*, la quantité de ressources
+supplémentaires données au début des missions, la capacité de contrôler les alliées IA, etc.
+* `/disable_mission_check` Désactive les requit pour lancer les missions.
+Cette option a pour but de permettre de jouer en mode coopératif en permettant à un joueur de jouer à la prochaine
+mission de la chaîne qu'un autre joueur est en train d'entamer.
+* `/play [mission_id]` Lance la mission correspondant à l'identifiant donné.
+* `/available` Affiche les missions qui sont présentement accessibles.
+* `/unfinished` Affiche les missions qui sont présentement accessibles et dont certains des objectifs permettant
+l'accès à un *item* n'ont pas été accomplis.
+* `/set_path [path]` Permet de définir manuellement où *StarCraft 2* est installé ce qui est pertinent seulement si la
+détection automatique de cette dernière échoue.
diff --git a/worlds/sc2/docs/setup_en.md b/worlds/sc2/docs/setup_en.md
new file mode 100644
index 000000000000..991ed57e8741
--- /dev/null
+++ b/worlds/sc2/docs/setup_en.md
@@ -0,0 +1,143 @@
+# StarCraft 2 Randomizer Setup Guide
+
+This guide contains instructions on how to install and troubleshoot the StarCraft 2 Archipelago client, as well as where
+to obtain a config file for StarCraft 2.
+
+## Required Software
+
+- [StarCraft 2](https://starcraft2.com/en-us/)
+- [The most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
+
+## How do I install this randomizer?
+
+1. Install StarCraft 2 and Archipelago using the links above. The StarCraft 2 Archipelago client is downloaded by the Archipelago installer.
+ - Linux users should also follow the instructions found at the bottom of this page
+ (["Running in Linux"](#running-in-linux)).
+2. Run ArchipelagoStarcraft2Client.exe.
+ - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only.
+3. Type the command `/download_data`. This will automatically install the Maps and Data files from the third link above.
+
+## Where do I get a config file (aka "YAML") for this game?
+
+Yaml files are configuration files that tell Archipelago how you'd like your game to be randomized, even if you're only using default options.
+When you're setting up a multiworld, every world needs its own yaml file.
+
+There are three basic ways to get a yaml:
+* You can go to the [Player Options](/games/Starcraft%202/player-options) page, set your options in the GUI, and export the yaml.
+* You can generate a template, either by downloading it from the [Player Options](/games/Starcraft%202/player-options) page or by generating it from the Launcher (ArchipelagoLauncher.exe). The template includes descriptions of each option, you just have to edit it in your text editor of choice.
+* You can ask someone else to share their yaml to use it for yourself or adjust it as you wish.
+
+Remember the name you enter in the options page or in the yaml file, you'll need it to connect later!
+
+Check out [Creating a YAML](/tutorial/Archipelago/setup/en#creating-a-yaml) for more game-agnostic information.
+
+### Common yaml questions
+#### How do I know I set my yaml up correctly?
+
+The simplest way to check is to use the website [validator](/check).
+
+You can also test it by attempting to generate a multiworld with your yaml. Save your yaml to the Players/ folder within your Archipelago installation and run ArchipelagoGenerate.exe. You should see a new .zip file within the output/ folder of your Archipelago installation if things worked correctly. It's advisable to run ArchipelagoGenerate through a terminal so that you can see the printout, which will include any errors and the precise output file name if it's successful. If you don't like terminals, you can also check the log file in the logs/ folder.
+
+#### What does Progression Balancing do?
+
+For Starcraft 2, not much. It's an Archipelago-wide option meant to shift required items earlier in the playthrough, but Starcraft 2 tends to be much more open in what items you can use. As such, this adjustment isn't very noticeable. It can also increase generation times, so we generally recommend turning it off.
+
+#### How do I specify items in a list, like in excluded items?
+
+You can look up the syntax for yaml collections in the [YAML specification](https://yaml.org/spec/1.2.2/#21-collections). For lists, every item goes on its own line, started with a hyphen:
+
+```yaml
+excluded_items:
+ - Battlecruiser
+ - Drop-Pods (Kerrigan Tier 7)
+```
+
+An empty list is just a matching pair of square brackets: `[]`. That's the default value in the template, which should let you know to use this syntax.
+
+#### How do I specify items for the starting inventory?
+
+The starting inventory is a YAML mapping rather than a list, which associates an item with the amount you start with. The syntax looks like the item name, followed by a colon, then a whitespace character, and then the value:
+
+```yaml
+start_inventory:
+ Micro-Filtering: 1
+ Additional Starting Vespene: 5
+```
+
+An empty mapping is just a matching pair of curly braces: `{}`. That's the default value in the template, which should let you know to use this syntax.
+
+#### How do I know the exact names of items and locations?
+
+The [*datapackage*](/datapackage) page of the Archipelago website provides a complete list of the items and locations for each game that it currently supports, including StarCraft 2.
+
+You can also look up a complete list of the item names in the [Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/) page.
+This page also contains supplementary information of each item.
+However, the items shown in that page might differ from those shown in the datapackage page of Archipelago since the former is generated, most of the time, from beta versions of StarCraft 2 Archipelago undergoing development.
+
+As for the locations, you can see all the locations associated to a mission in your world by placing your cursor over the mission in the 'StarCraft 2 Launcher' tab in the client.
+
+## How do I join a MultiWorld game?
+
+1. Run ArchipelagoStarcraft2Client.exe.
+ - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only.
+2. Type `/connect [server ip]`.
+ - If you're running through the website, the server IP should be displayed near the top of the room page.
+3. Type your slot name from your YAML when prompted.
+4. If the server has a password, enter that when prompted.
+5. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your world. Unreachable missions will have greyed-out text. Just click on an available mission to start it!
+
+## The game isn't launching when I try to start a mission.
+
+First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If you can't figure out
+the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel for help. Please include a
+specific description of what's going wrong and attach your log file to your message.
+
+## Running in macOS
+
+To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](/tutorial/Archipelago/mac/en). Note: to launch the client, you will need to run the command `python3 Starcraft2Client.py`.
+
+## Running in Linux
+
+To run StarCraft 2 through Archipelago in Linux, you will need to install the game using Wine, then run the Linux build
+of the Archipelago client.
+
+Make sure you have StarCraft 2 installed using Wine, and that you have followed the
+[installation procedures](#how-do-i-install-this-randomizer?) to add the Archipelago maps to the correct location. You will not
+need to copy the .dll files. If you're having trouble installing or running StarCraft 2 on Linux, I recommend using the
+Lutris installer.
+
+Copy the following into a .sh file, replacing the values of **WINE** and **SC2PATH** variables with the relevant
+locations, as well as setting **PATH_TO_ARCHIPELAGO** to the directory containing the AppImage if it is not in the same
+folder as the script.
+
+```sh
+# Let the client know we're running SC2 in Wine
+export SC2PF=WineLinux
+export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
+
+# FIXME Replace with path to the version of Wine used to run SC2
+export WINE="/usr/bin/wine"
+
+# FIXME Replace with path to StarCraft II install folder
+export SC2PATH="/home/user/Games/starcraft-ii/drive_c/Program Files (x86)/StarCraft II/"
+
+# FIXME Set to directory which contains Archipelago AppImage file
+PATH_TO_ARCHIPELAGO=
+
+# Gets the latest version of Archipelago AppImage in PATH_TO_ARCHIPELAGO.
+# If PATH_TO_ARCHIPELAGO is not set, this defaults to the directory containing
+# this script file.
+ARCHIPELAGO="$(ls ${PATH_TO_ARCHIPELAGO:-$(dirname $0)}/Archipelago_*.AppImage | sort -r | head -1)"
+
+# Start the Archipelago client
+$ARCHIPELAGO Starcraft2Client
+```
+
+For Lutris installs, you can run `lutris -l` to get the numerical ID of your StarCraft II install, then run the command
+below, replacing **${ID}** with the numerical ID.
+
+ lutris lutris:rungameid/${ID} --output-script sc2.sh
+
+This will get all of the relevant environment variables Lutris sets to run StarCraft 2 in a script, including the path
+to the Wine binary that Lutris uses. You can then remove the line that runs the Battle.Net launcher and copy the code
+above into the existing script.
diff --git a/worlds/sc2/docs/setup_fr.md b/worlds/sc2/docs/setup_fr.md
new file mode 100644
index 000000000000..bb6c35bce1c7
--- /dev/null
+++ b/worlds/sc2/docs/setup_fr.md
@@ -0,0 +1,214 @@
+# Guide d'installation du *StarCraft 2 Randomizer*
+
+Ce guide contient les instructions pour installer et dépanner le client de *StarCraft 2 Archipelago*, ainsi que des
+indications pour obtenir un fichier de configuration de *StarCraft 2 Archipelago* et comment modifier ce dernier.
+
+## Logiciels requis
+
+- [*StarCraft 2*](https://starcraft2.com/en-us/)
+- [La version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
+
+## Comment est-ce que j'installe ce *randomizer*?
+
+1. Installer *StarCraft 2* et Archipelago en suivant les instructions indiquées dans les liens précédents. Le client de
+*StarCraft 2 Archipelago* est téléchargé par le programme d'installation d'Archipelago.
+ - Les utilisateurs de Linux devraient aussi suivre les instructions qui se retrouvent à la fin de cette page
+(["Exécuter sous Linux"](#exécuter-sous-linux)).
+ - Notez que votre jeu *StarCraft 2* doit être en anglais pour fonctionner avec Archipelago.
+2. Exécuter `ArchipelagoStarcraft2Client.exe`.
+ - Uniquement pour cette étape, les utilisateurs de macOS devraient plutôt suivre les instructions qui se trouvent Ã
+["Exécuter sous macOS"](#exécuter-sous-macos).
+3. Dans le client de *StarCraft 2 Archipelago*, écrire la commande `/download_data`. Cette commande va lancer
+l'installation des fichiers qui sont nécessaires pour jouer à *StarCraft 2 Archipelago*.
+
+## Où est-ce que j'obtiens le fichier de configuration (i.e., le *yaml*) pour ce jeu?
+
+Un fichier dans le format *yaml* est utilisé pour communiquer à Archipelago comment vous voulez que votre jeu soit
+*randomized*.
+Ce dernier est nécessaire même si vous voulez utiliser les options par défaut.
+L'approche usuelle pour générer un *multiworld* consiste à avoir un fichier *yaml* par monde.
+
+Il y a trois approches pour obtenir un fichier *yaml* pour *StarCraft 2 Randomizer*:
+* Vous pouvez aller à la page [*Player options*](/games/Starcraft%202/player-options) qui vous permet de définir vos
+choix via une interface graphique et ensuite télécharger le *yaml* correspondant à ces choix.
+* Vous pouvez obtenir le modèle de base en le téléchargeant à la page
+[*Player options*](/games/Starcraft%202/player-options) ou en cliquant sur *Generate template* après avoir exécuté le
+*Launcher* d'Archipelago (i.e., `ArchipelagoLauncher.exe`). Ce modèle de base inclut une description pour chacune des
+options et vous n'avez qu'à modifier les options dans un éditeur de texte de votre choix.
+* Vous pouvez demander à quelqu'un d'autre de partager un de ces fichiers *yaml* pour l'utiliser ou l'ajuster à vos
+préférences.
+
+Prenez soin de vous rappeler du nom de joueur que vous avez inscrit dans la page à options ou dans le fichier *yaml*
+puisque vous en aurez besoin pour vous connecter à votre monde!
+
+Notez que la page *Player options* ne permet pas de définir certaines des options avancées, e.g., l'exclusion de
+certaines unités ou de leurs améliorations.
+Utilisez la page [*Weighted Options*](/weighted-options) pour avoir accès à ces dernières.
+
+Si vous désirez des informations et/ou instructions générales sur l'utilisation d'un fichier *yaml* pour Archipelago,
+veuillez consulter [*Creating a YAML*](/tutorial/Archipelago/setup/en#creating-a-yaml).
+
+### Questions récurrentes à propos du fichier *yaml*
+#### Comment est-ce que je sais que mon *yaml* est bien défini?
+
+La manière la plus simple de valider votre *yaml* est d'utiliser le
+[système de validation](/check) du site web.
+
+Vous pouvez aussi le tester en tentant de générer un *multiworld* avec votre *yaml*.
+Pour faire ça, sauvegardez votre *yaml* dans le dossier `Players/` de votre installation d'Archipelago et exécutez
+`ArchipelagoGenerate.exe`.
+Si votre *yaml* est bien défini, vous devriez voir un nouveau fichier, avec l'extension `.zip`, apparaître dans le
+dossier `output/` de votre installation d'Archipelago.
+Il est recommandé de lancer `ArchipelagoGenerate.exe` via un terminal afin que vous puissiez voir les messages générés
+par le logiciel, ce qui va inclure toutes erreurs qui ont eu lieu et le nom de fichier généré.
+Si vous n'appréciez pas le fait d'utiliser un terminal, vous pouvez aussi regarder le fichier *log* qui va être produit
+dans le dossier `logs/`.
+
+#### À quoi sert l'option *Progression Balancing*?
+
+Pour *Starcraft 2*, cette option ne fait pas grand-chose.
+Il s'agit d'une option d'Archipelago permettant d'équilibrer la progression des mondes en interchangeant les *items*
+dans les *spheres*.
+Si le *Progression Balancing* d'un monde est plus grand que ceux des autres, les *items* de progression de ce monde ont
+plus de chance d'être obtenus tôt et vice-versa si sa valeur est plus petite que celle des autres mondes.
+Cependant, *Starcraft 2* est beaucoup plus permissif en termes d'*items* qui permettent de progresser, ce réglage Ã
+donc peu d'influence sur la progression dans *StarCraft 2*.
+Vu qu'il augmente le temps de génération d'un *MultiWorld*, nous recommandons de le désactiver, c-à -d le définir Ã
+zéro, pour *Starcraft 2*.
+
+
+#### Comment est-ce que je définis une liste d'*items*, e.g. pour l'option *excluded items*?
+
+Vous pouvez lire sur la syntaxe des conteneurs dans le format *yaml* Ã la page
+[*YAML specification*](https://yaml.org/spec/1.2.2/#21-collections).
+Pour les listes, chaque *item* doit être sur sa propre ligne et doit être précédé par un trait d'union.
+
+```yaml
+excluded_items:
+ - Battlecruiser
+ - Drop-Pods (Kerrigan Tier 7)
+```
+
+Une liste vide est représentée par une paire de crochets: `[]`.
+Il s'agit de la valeur par défaut dans le modèle de base, ce qui devrait vous aider à apprendre à utiliser cette
+syntaxe.
+
+#### Comment est-ce que je fais pour avoir des *items* dès le départ?
+
+L'option *starting inventory* est un *map* et non une liste.
+Ainsi, elle permet de spécifier le nombre de chaque *item* avec lequel vous allez commencer.
+Sa syntaxe consiste à indiquer le nom de l'*item*, suivi par un deux-points, puis par un espace et enfin par le nombre
+désiré de cet *item*.
+
+```yaml
+start_inventory:
+ Micro-Filtering: 1
+ Additional Starting Vespene: 5
+```
+
+Un *map* vide est représenté par une paire d'accolades: `{}`.
+Il s'agit de la valeur par défaut dans le modèle de base, ce qui devrait vous aider à apprendre à utiliser cette
+syntaxe.
+
+#### Comment est-ce que je fais pour connaître le nom des *items* et des *locations* dans *StarCraft 2 Archipelago*?
+
+La page [*datapackage*](/datapackage) d'Archipelago liste l'ensemble des *items* et des *locations* de tous les jeux
+que le site web prend en charge actuellement, dont ceux de *StarCraft 2*.
+
+Vous trouverez aussi la liste complète des *items* de *StarCraft 2 Archipelago* à la page
+[*Icon Repository*](https://matthewmarinets.github.io/ap_sc2_icons/).
+Notez que cette page contient diverses informations supplémentaires sur chacun des *items*.
+Cependant, l'information présente dans cette dernière peut différer de celle du *datapackage* d'Archipelago
+puisqu'elle est générée, habituellement, à partir de la version en développement de *StarCraft 2 Archipelago* qui
+n'ont peut-être pas encore été inclus dans le site web d'Archipelago.
+
+## Comment est-ce que je peux joindre un *MultiWorld*?
+
+1. Exécuter `ArchipelagoStarcraft2Client.exe`.
+ - Uniquement pour cette étape, les utilisateurs de macOS devraient plutôt suivre les instructions à la page
+["Exécuter sous macOS"](#exécuter-sous-macos).
+2. Entrer la commande `/connect [server ip]`.
+ - Si le *MultiWorld* est hébergé via un siteweb, l'IP du server devrait être indiqué dans le haut de la page de
+votre *room*.
+3. Inscrivez le nom de joueur spécifié dans votre *yaml* lorsque vous y êtes invité.
+4. Si le serveur a un mot de passe, l'inscrire lorsque vous y êtes invité.
+5. Une fois connecté, aller sur l'onglet *StarCraft 2 Launcher* dans le client. Dans cet onglet, vous devriez trouver
+toutes les missions de votre monde. Les missions qui ne sont pas disponibles présentement auront leur texte dans une
+nuance de gris. Vous n'avez qu'Ã cliquer une des missions qui est disponible pour la commencer!
+
+## *StarCraft 2* ne démarre pas quand je tente de commencer une mission
+
+Pour commencer, regarder le fichier *log* pour trouver le problème (ce dernier devrait être dans
+`[Archipelago Directory]/logs/SC2Client.txt`).
+Si vous ne comprenez pas le problème avec le fichier *log*, visitez notre
+[*Discord*](https://discord.com/invite/8Z65BR2) pour demander de l'aide dans le forum *tech-support*.
+Dans votre message, veuillez inclure une description détaillée de ce qui ne marche pas et ajouter en pièce jointe le
+fichier *log*.
+
+## Mon profil de raccourcis clavier n'est pas disponibles quand je joue à *StarCraft 2 Archipelago*
+
+Pour que votre profil de raccourcis clavier fonctionne dans Archipelago, vous devez copier votre fichier de raccourcis
+qui se trouve dans `Documents/StarCraft II/Accounts/######/Hotkeys` vers `Documents/StarCraft II/Hotkeys`.
+Si le dossier n'existe pas, créez-le.
+
+Pour que *StarCraft 2 Archipelago* utilise votre profil, suivez les étapes suivantes.
+Lancez *Starcraft 2* via l'application *Battle.net*.
+Changez votre profil de raccourcis clavier pour le mode standard et acceptez, puis sélectionnez votre profil
+personnalisé et acceptez.
+Vous n'aurez besoin de faire ça qu'une seule fois.
+
+## Exécuter sous macOS
+
+Pour exécuter *StarCraft 2* via Archipelago sous macOS, vous devez exécuter le client à partir de la source
+comme indiqué ici: [*macOS Guide*](/tutorial/Archipelago/mac/en).
+Notez que pour lancer le client, vous devez exécuter la commande `python3 Starcraft2Client.py`.
+
+## Exécuter sous Linux
+
+Pour exécuter *StarCraft 2* via Archipelago sous Linux, vous allez devoir installer le jeu avec *Wine* et ensuite
+exécuter le client d'Archipelago pour Linux.
+
+Confirmez que vous avez installé *StarCraft 2* via *Wine* et que vous avez suivi les
+[instructions d'installation](#comment-est-ce-que-j'installe-ce-randomizer?) pour ajouter les *Maps* et les *Data
+files* nécessairent pour *StarCraft 2 Archipelago* au bon endroit.
+Vous n'avez pas besoin de copier les fichiers `.dll`.
+Si vous avez des difficultés pour installer ou exécuter *StarCraft 2* sous Linux, il est recommandé d'utiliser le
+logiciel *Lutris*.
+
+Copier ce qui suit dans un fichier avec l'extension `.sh`, en prenant soin de définir les variables **WINE** et
+**SC2PATH** avec les bons chemins et de définir **PATH_TO_ARCHIPELAGO** avec le chemin vers le dossier qui contient le
+*AppImage* si ce dernier n'est pas dans le même dossier que ce script.
+
+```sh
+# Permet au client de savoir que SC2 est exécuté via Wine
+export SC2PF=WineLinux
+export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
+
+# À_CHANGER Remplacer le chemin avec celui qui correspond à la version de Wine utilisé pour exécuter SC2
+export WINE="/usr/bin/wine"
+
+# À_CHANGER Remplacer le chemin par celui qui indique où StarCraft II est installé
+export SC2PATH="/home/user/Games/starcraft-ii/drive_c/Program Files (x86)/StarCraft II/"
+
+# À_CHANGER Indiquer le dossier qui contient l'AppImage d'Archipelago
+PATH_TO_ARCHIPELAGO=
+
+# Obtiens la dernière version de l'AppImage de Archipelago dans le dossier PATH_TO_ARCHIPELAGO.
+# Si PATH_TO_ARCHIPELAGO n'est pas défini, la valeur par défaut est le dossier qui contient ce script.
+ARCHIPELAGO="$(ls ${PATH_TO_ARCHIPELAGO:-$(dirname $0)}/Archipelago_*.AppImage | sort -r | head -1)"
+
+# Lance le client de Archipelago
+$ARCHIPELAGO Starcraft2Client
+```
+
+Pour une installation via Lutris, vous pouvez exécuter `lutris -l` pour obtenir l'identifiant numérique de votre
+installation *StarCraft II* et ensuite exécuter la commande suivante, en remplacant **${ID}** pour cet identifiant
+numérique.
+
+ lutris lutris:rungameid/${ID} --output-script sc2.sh
+
+Cette commande va définir toutes les variables d'environnement nécessaires pour exécuter *StarCraft 2* dans un script,
+incluant le chemin vers l'exécutable *Wine* que Lutris utilise.
+Après ça, vous pouvez enlever la ligne qui permet de démarrer *Battle.Net* et copier le code décrit plus haut dans le
+script produit.
+
diff --git a/worlds/sc2/requirements.txt b/worlds/sc2/requirements.txt
new file mode 100644
index 000000000000..5bc808b639db
--- /dev/null
+++ b/worlds/sc2/requirements.txt
@@ -0,0 +1 @@
+nest-asyncio >= 1.5.5
diff --git a/worlds/sc2/test/__init__.py b/worlds/sc2/test/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/sc2/test/test_Regions.py b/worlds/sc2/test/test_Regions.py
new file mode 100644
index 000000000000..c268b65da9a8
--- /dev/null
+++ b/worlds/sc2/test/test_Regions.py
@@ -0,0 +1,41 @@
+import unittest
+from .test_base import Sc2TestBase
+from .. import Regions
+from .. import Options, MissionTables
+
+class TestGridsizes(unittest.TestCase):
+ def test_grid_sizes_meet_specs(self):
+ self.assertTupleEqual((1, 2, 0), Regions.get_grid_dimensions(2))
+ self.assertTupleEqual((1, 3, 0), Regions.get_grid_dimensions(3))
+ self.assertTupleEqual((2, 2, 0), Regions.get_grid_dimensions(4))
+ self.assertTupleEqual((2, 3, 1), Regions.get_grid_dimensions(5))
+ self.assertTupleEqual((2, 4, 1), Regions.get_grid_dimensions(7))
+ self.assertTupleEqual((2, 4, 0), Regions.get_grid_dimensions(8))
+ self.assertTupleEqual((3, 3, 0), Regions.get_grid_dimensions(9))
+ self.assertTupleEqual((2, 5, 0), Regions.get_grid_dimensions(10))
+ self.assertTupleEqual((3, 4, 1), Regions.get_grid_dimensions(11))
+ self.assertTupleEqual((3, 4, 0), Regions.get_grid_dimensions(12))
+ self.assertTupleEqual((3, 5, 0), Regions.get_grid_dimensions(15))
+ self.assertTupleEqual((4, 4, 0), Regions.get_grid_dimensions(16))
+ self.assertTupleEqual((4, 6, 0), Regions.get_grid_dimensions(24))
+ self.assertTupleEqual((5, 5, 0), Regions.get_grid_dimensions(25))
+ self.assertTupleEqual((5, 6, 1), Regions.get_grid_dimensions(29))
+ self.assertTupleEqual((5, 7, 2), Regions.get_grid_dimensions(33))
+
+
+class TestGridGeneration(Sc2TestBase):
+ options = {
+ "mission_order": Options.MissionOrder.option_grid,
+ "excluded_missions": [MissionTables.SC2Mission.ZERO_HOUR.mission_name,],
+ "enable_hots_missions": False,
+ "enable_prophecy_missions": True,
+ "enable_lotv_prologue_missions": False,
+ "enable_lotv_missions": False,
+ "enable_epilogue_missions": False,
+ "enable_nco_missions": False
+ }
+
+ def test_size_matches_exclusions(self):
+ self.assertNotIn(MissionTables.SC2Mission.ZERO_HOUR.mission_name, self.multiworld.regions)
+ # WoL has 29 missions. -1 for Zero Hour being excluded, +1 for the automatically-added menu location
+ self.assertEqual(len(self.multiworld.regions), 29)
diff --git a/worlds/sc2/test/test_base.py b/worlds/sc2/test/test_base.py
new file mode 100644
index 000000000000..28529e37edd5
--- /dev/null
+++ b/worlds/sc2/test/test_base.py
@@ -0,0 +1,11 @@
+from typing import *
+
+from test.TestBase import WorldTestBase
+from .. import SC2World
+from .. import Client
+
+class Sc2TestBase(WorldTestBase):
+ game = Client.SC2Context.game
+ world: SC2World
+ player: ClassVar[int] = 1
+ skip_long_tests: bool = True
diff --git a/worlds/sc2/test/test_options.py b/worlds/sc2/test/test_options.py
new file mode 100644
index 000000000000..30d21f39697e
--- /dev/null
+++ b/worlds/sc2/test/test_options.py
@@ -0,0 +1,7 @@
+import unittest
+from .test_base import Sc2TestBase
+from .. import Options, MissionTables
+
+class TestOptions(unittest.TestCase):
+ def test_campaign_size_option_max_matches_number_of_missions(self):
+ self.assertEqual(Options.MaximumCampaignSize.range_end, len(MissionTables.SC2Mission))
diff --git a/worlds/sc2wol/Client.py b/worlds/sc2wol/Client.py
deleted file mode 100644
index c544cf0c5519..000000000000
--- a/worlds/sc2wol/Client.py
+++ /dev/null
@@ -1,1201 +0,0 @@
-from __future__ import annotations
-
-import asyncio
-import copy
-import ctypes
-import logging
-import multiprocessing
-import os.path
-import re
-import sys
-import typing
-import queue
-import zipfile
-import io
-import random
-from pathlib import Path
-
-# CommonClient import first to trigger ModuleUpdater
-from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
-from Utils import init_logging, is_windows
-
-if __name__ == "__main__":
- init_logging("SC2Client", exception_logger="Client")
-
-logger = logging.getLogger("Client")
-sc2_logger = logging.getLogger("Starcraft2")
-
-import nest_asyncio
-from worlds._sc2common import bot
-from worlds._sc2common.bot.data import Race
-from worlds._sc2common.bot.main import run_game
-from worlds._sc2common.bot.player import Bot
-from worlds.sc2wol import SC2WoLWorld
-from worlds.sc2wol.Items import lookup_id_to_name, get_full_item_list, ItemData, type_flaggroups, upgrade_numbers
-from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
-from worlds.sc2wol.MissionTables import lookup_id_to_mission
-from worlds.sc2wol.Regions import MissionInfo
-
-import colorama
-from NetUtils import ClientStatus, NetworkItem, RawJSONtoTextParser, JSONtoTextParser, JSONMessagePart
-from MultiServer import mark_raw
-
-loop = asyncio.get_event_loop_policy().new_event_loop()
-nest_asyncio.apply(loop)
-max_bonus: int = 13
-victory_modulo: int = 100
-
-# GitHub repo where the Map/mod data is hosted for /download_data command
-DATA_REPO_OWNER = "Ziktofel"
-DATA_REPO_NAME = "Archipelago-SC2-data"
-DATA_API_VERSION = "API2"
-
-
-# Data version file path.
-# This file is used to tell if the downloaded data are outdated
-# Associated with /download_data command
-def get_metadata_file():
- return os.environ["SC2PATH"] + os.sep + "ArchipelagoSC2Metadata.txt"
-
-
-class StarcraftClientProcessor(ClientCommandProcessor):
- ctx: SC2Context
-
- def _cmd_difficulty(self, difficulty: str = "") -> bool:
- """Overrides the current difficulty set for the world. Takes the argument casual, normal, hard, or brutal"""
- options = difficulty.split()
- num_options = len(options)
-
- if num_options > 0:
- difficulty_choice = options[0].lower()
- if difficulty_choice == "casual":
- self.ctx.difficulty_override = 0
- elif difficulty_choice == "normal":
- self.ctx.difficulty_override = 1
- elif difficulty_choice == "hard":
- self.ctx.difficulty_override = 2
- elif difficulty_choice == "brutal":
- self.ctx.difficulty_override = 3
- else:
- self.output("Unable to parse difficulty '" + options[0] + "'")
- return False
-
- self.output("Difficulty set to " + options[0])
- return True
-
- else:
- if self.ctx.difficulty == -1:
- self.output("Please connect to a seed before checking difficulty.")
- else:
- current_difficulty = self.ctx.difficulty
- if self.ctx.difficulty_override >= 0:
- current_difficulty = self.ctx.difficulty_override
- self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][current_difficulty])
- self.output("To change the difficulty, add the name of the difficulty after the command.")
- return False
-
-
- def _cmd_game_speed(self, game_speed: str = "") -> bool:
- """Overrides the current game speed for the world.
- Takes the arguments default, slower, slow, normal, fast, faster"""
- options = game_speed.split()
- num_options = len(options)
-
- if num_options > 0:
- speed_choice = options[0].lower()
- if speed_choice == "default":
- self.ctx.game_speed_override = 0
- elif speed_choice == "slower":
- self.ctx.game_speed_override = 1
- elif speed_choice == "slow":
- self.ctx.game_speed_override = 2
- elif speed_choice == "normal":
- self.ctx.game_speed_override = 3
- elif speed_choice == "fast":
- self.ctx.game_speed_override = 4
- elif speed_choice == "faster":
- self.ctx.game_speed_override = 5
- else:
- self.output("Unable to parse game speed '" + options[0] + "'")
- return False
-
- self.output("Game speed set to " + options[0])
- return True
-
- else:
- if self.ctx.game_speed == -1:
- self.output("Please connect to a seed before checking game speed.")
- else:
- current_speed = self.ctx.game_speed
- if self.ctx.game_speed_override >= 0:
- current_speed = self.ctx.game_speed_override
- self.output("Current game speed: "
- + ["Default", "Slower", "Slow", "Normal", "Fast", "Faster"][current_speed])
- self.output("To change the game speed, add the name of the speed after the command,"
- " or Default to select based on difficulty.")
- return False
-
- def _cmd_color(self, color: str = "") -> bool:
- player_colors = [
- "White", "Red", "Blue", "Teal",
- "Purple", "Yellow", "Orange", "Green",
- "LightPink", "Violet", "LightGrey", "DarkGreen",
- "Brown", "LightGreen", "DarkGrey", "Pink",
- "Rainbow", "Random", "Default"
- ]
- match_colors = [player_color.lower() for player_color in player_colors]
- if color:
- if color.lower() not in match_colors:
- self.output(color + " is not a valid color. Available colors: " + ', '.join(player_colors))
- return False
- if color.lower() == "random":
- color = random.choice(player_colors[:16])
- self.ctx.player_color = match_colors.index(color.lower())
- self.output("Color set to " + player_colors[self.ctx.player_color])
- else:
- self.output("Current player color: " + player_colors[self.ctx.player_color])
- self.output("To change your colors, add the name of the color after the command.")
- self.output("Available colors: " + ', '.join(player_colors))
-
- def _cmd_disable_mission_check(self) -> bool:
- """Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play
- the next mission in a chain the other player is doing."""
- self.ctx.missions_unlocked = True
- sc2_logger.info("Mission check has been disabled")
- return True
-
- def _cmd_play(self, mission_id: str = "") -> bool:
- """Start a Starcraft 2 mission"""
-
- options = mission_id.split()
- num_options = len(options)
-
- if num_options > 0:
- mission_number = int(options[0])
-
- self.ctx.play_mission(mission_number)
-
- else:
- sc2_logger.info(
- "Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.")
- return False
-
- return True
-
- def _cmd_available(self) -> bool:
- """Get what missions are currently available to play"""
-
- request_available_missions(self.ctx)
- return True
-
- def _cmd_unfinished(self) -> bool:
- """Get what missions are currently available to play and have not had all locations checked"""
-
- request_unfinished_missions(self.ctx)
- return True
-
- @mark_raw
- def _cmd_set_path(self, path: str = '') -> bool:
- """Manually set the SC2 install directory (if the automatic detection fails)."""
- if path:
- os.environ["SC2PATH"] = path
- is_mod_installed_correctly()
- return True
- else:
- sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.")
- return False
-
- def _cmd_download_data(self) -> bool:
- """Download the most recent release of the necessary files for playing SC2 with
- Archipelago. Will overwrite existing files."""
- if "SC2PATH" not in os.environ:
- check_game_install_path()
-
- if os.path.exists(get_metadata_file()):
- with open(get_metadata_file(), "r") as f:
- metadata = f.read()
- else:
- metadata = None
-
- tempzip, metadata = download_latest_release_zip(DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION,
- metadata=metadata, force_download=True)
-
- if tempzip != '':
- try:
- zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"])
- sc2_logger.info(f"Download complete. Package installed.")
- with open(get_metadata_file(), "w") as f:
- f.write(metadata)
- finally:
- os.remove(tempzip)
- else:
- sc2_logger.warning("Download aborted/failed. Read the log for more information.")
- return False
- return True
-
-
-class SC2JSONtoTextParser(JSONtoTextParser):
- def __init__(self, ctx):
- self.handlers = {
- "ItemSend": self._handle_color,
- "ItemCheat": self._handle_color,
- "Hint": self._handle_color,
- }
- super().__init__(ctx)
-
- def _handle_color(self, node: JSONMessagePart):
- codes = node["color"].split(";")
- buffer = "".join(self.color_code(code) for code in codes if code in self.color_codes)
- return buffer + self._handle_text(node) + ' '
-
- def color_code(self, code: str):
- return ''
-
-
-class SC2Context(CommonContext):
- command_processor = StarcraftClientProcessor
- game = "Starcraft 2 Wings of Liberty"
- items_handling = 0b111
- difficulty = -1
- game_speed = -1
- all_in_choice = 0
- mission_order = 0
- player_color = 2
- mission_req_table: typing.Dict[str, MissionInfo] = {}
- final_mission: int = 29
- announcements = queue.Queue()
- sc2_run_task: typing.Optional[asyncio.Task] = None
- missions_unlocked: bool = False # allow launching missions ignoring requirements
- generic_upgrade_missions = 0
- generic_upgrade_research = 0
- generic_upgrade_items = 0
- current_tooltip = None
- last_loc_list = None
- difficulty_override = -1
- game_speed_override = -1
- mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {}
- last_bot: typing.Optional[ArchipelagoBot] = None
-
- def __init__(self, *args, **kwargs):
- super(SC2Context, self).__init__(*args, **kwargs)
- self.raw_text_parser = SC2JSONtoTextParser(self)
-
- async def server_auth(self, password_requested: bool = False):
- if password_requested and not self.password:
- await super(SC2Context, self).server_auth(password_requested)
- await self.get_username()
- await self.send_connect()
-
- def on_package(self, cmd: str, args: dict):
- if cmd in {"Connected"}:
- self.difficulty = args["slot_data"]["game_difficulty"]
- if "game_speed" in args["slot_data"]:
- self.game_speed = args["slot_data"]["game_speed"]
- else:
- self.game_speed = 0
- self.all_in_choice = args["slot_data"]["all_in_map"]
- slot_req_table = args["slot_data"]["mission_req"]
- # Maintaining backwards compatibility with older slot data
- self.mission_req_table = {
- mission: MissionInfo(
- **{field: value for field, value in mission_info.items() if field in MissionInfo._fields}
- )
- for mission, mission_info in slot_req_table.items()
- }
- self.mission_order = args["slot_data"].get("mission_order", 0)
- self.final_mission = args["slot_data"].get("final_mission", 29)
- self.player_color = args["slot_data"].get("player_color", 2)
- self.generic_upgrade_missions = args["slot_data"].get("generic_upgrade_missions", 0)
- self.generic_upgrade_items = args["slot_data"].get("generic_upgrade_items", 0)
- self.generic_upgrade_research = args["slot_data"].get("generic_upgrade_research", 0)
-
- self.build_location_to_mission_mapping()
-
- # Looks for the required maps and mods for SC2. Runs check_game_install_path.
- maps_present = is_mod_installed_correctly()
- if os.path.exists(get_metadata_file()):
- with open(get_metadata_file(), "r") as f:
- current_ver = f.read()
- sc2_logger.debug(f"Current version: {current_ver}")
- if is_mod_update_available(DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION, current_ver):
- sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.")
- elif maps_present:
- sc2_logger.warning("NOTICE: Your map files may be outdated (version number not found). "
- "Run /download_data to update them.")
-
-
- def on_print_json(self, args: dict):
- # goes to this world
- if "receiving" in args and self.slot_concerns_self(args["receiving"]):
- relevant = True
- # found in this world
- elif "item" in args and self.slot_concerns_self(args["item"].player):
- relevant = True
- # not related
- else:
- relevant = False
-
- if relevant:
- self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"])))
-
- super(SC2Context, self).on_print_json(args)
-
- def run_gui(self):
- from kvui import GameManager, HoverBehavior, ServerToolTip
- from kivy.app import App
- from kivy.clock import Clock
- from kivy.uix.tabbedpanel import TabbedPanelItem
- from kivy.uix.gridlayout import GridLayout
- from kivy.lang import Builder
- from kivy.uix.label import Label
- from kivy.uix.button import Button
- from kivy.uix.floatlayout import FloatLayout
- from kivy.properties import StringProperty
-
- class HoverableButton(HoverBehavior, Button):
- pass
-
- class MissionButton(HoverableButton):
- tooltip_text = StringProperty("Test")
- ctx: SC2Context
-
- def __init__(self, *args, **kwargs):
- super(HoverableButton, self).__init__(*args, **kwargs)
- self.layout = FloatLayout()
- self.popuplabel = ServerToolTip(text=self.text)
- self.layout.add_widget(self.popuplabel)
-
- def on_enter(self):
- self.popuplabel.text = self.tooltip_text
-
- if self.ctx.current_tooltip:
- App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
-
- if self.tooltip_text == "":
- self.ctx.current_tooltip = None
- else:
- App.get_running_app().root.add_widget(self.layout)
- self.ctx.current_tooltip = self.layout
-
- def on_leave(self):
- self.ctx.ui.clear_tooltip()
-
- @property
- def ctx(self) -> CommonContext:
- return App.get_running_app().ctx
-
- class MissionLayout(GridLayout):
- pass
-
- class MissionCategory(GridLayout):
- pass
-
- class SC2Manager(GameManager):
- logging_pairs = [
- ("Client", "Archipelago"),
- ("Starcraft2", "Starcraft2"),
- ]
- base_title = "Archipelago Starcraft 2 Client"
-
- mission_panel = None
- last_checked_locations = {}
- mission_id_to_button = {}
- launching: typing.Union[bool, int] = False # if int -> mission ID
- refresh_from_launching = True
- first_check = True
- ctx: SC2Context
-
- def __init__(self, ctx):
- super().__init__(ctx)
-
- def clear_tooltip(self):
- if self.ctx.current_tooltip:
- App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
-
- self.ctx.current_tooltip = None
-
- def build(self):
- container = super().build()
-
- panel = TabbedPanelItem(text="Starcraft 2 Launcher")
- self.mission_panel = panel.content = MissionLayout()
-
- self.tabs.add_widget(panel)
-
- Clock.schedule_interval(self.build_mission_table, 0.5)
-
- return container
-
- def build_mission_table(self, dt):
- if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or
- not self.refresh_from_launching)) or self.first_check:
- self.refresh_from_launching = True
-
- self.mission_panel.clear_widgets()
- if self.ctx.mission_req_table:
- self.last_checked_locations = self.ctx.checked_locations.copy()
- self.first_check = False
-
- self.mission_id_to_button = {}
- categories = {}
- available_missions, unfinished_missions = calc_unfinished_missions(self.ctx)
-
- # separate missions into categories
- for mission in self.ctx.mission_req_table:
- if not self.ctx.mission_req_table[mission].category in categories:
- categories[self.ctx.mission_req_table[mission].category] = []
-
- categories[self.ctx.mission_req_table[mission].category].append(mission)
-
- for category in categories:
- category_panel = MissionCategory()
- if category.startswith('_'):
- category_display_name = ''
- else:
- category_display_name = category
- category_panel.add_widget(
- Label(text=category_display_name, size_hint_y=None, height=50, outline_width=1))
-
- for mission in categories[category]:
- text: str = mission
- tooltip: str = ""
- mission_id: int = self.ctx.mission_req_table[mission].id
- # Map has uncollected locations
- if mission in unfinished_missions:
- text = f"[color=6495ED]{text}[/color]"
- elif mission in available_missions:
- text = f"[color=FFFFFF]{text}[/color]"
- # Map requirements not met
- else:
- text = f"[color=a9a9a9]{text}[/color]"
- tooltip = f"Requires: "
- if self.ctx.mission_req_table[mission].required_world:
- tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for
- req_mission in
- self.ctx.mission_req_table[mission].required_world)
-
- if self.ctx.mission_req_table[mission].number:
- tooltip += " and "
- if self.ctx.mission_req_table[mission].number:
- tooltip += f"{self.ctx.mission_req_table[mission].number} missions completed"
- remaining_location_names: typing.List[str] = [
- self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission)
- if loc in self.ctx.missing_locations]
-
- if mission_id == self.ctx.final_mission:
- if mission in available_missions:
- text = f"[color=FFBC95]{mission}[/color]"
- else:
- text = f"[color=D0C0BE]{mission}[/color]"
- if tooltip:
- tooltip += "\n"
- tooltip += "Final Mission"
-
- if remaining_location_names:
- if tooltip:
- tooltip += "\n"
- tooltip += f"Uncollected locations:\n"
- tooltip += "\n".join(remaining_location_names)
-
- mission_button = MissionButton(text=text, size_hint_y=None, height=50)
- mission_button.tooltip_text = tooltip
- mission_button.bind(on_press=self.mission_callback)
- self.mission_id_to_button[mission_id] = mission_button
- category_panel.add_widget(mission_button)
-
- category_panel.add_widget(Label(text=""))
- self.mission_panel.add_widget(category_panel)
-
- elif self.launching:
- self.refresh_from_launching = False
-
- self.mission_panel.clear_widgets()
- self.mission_panel.add_widget(Label(text="Launching Mission: " +
- lookup_id_to_mission[self.launching]))
- if self.ctx.ui:
- self.ctx.ui.clear_tooltip()
-
- def mission_callback(self, button):
- if not self.launching:
- mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button)
- if self.ctx.play_mission(mission_id):
- self.launching = mission_id
- Clock.schedule_once(self.finish_launching, 10)
-
- def finish_launching(self, dt):
- self.launching = False
-
- self.ui = SC2Manager(self)
- self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
- import pkgutil
- data = pkgutil.get_data(SC2WoLWorld.__module__, "Starcraft2.kv").decode()
- Builder.load_string(data)
-
- async def shutdown(self):
- await super(SC2Context, self).shutdown()
- if self.last_bot:
- self.last_bot.want_close = True
- if self.sc2_run_task:
- self.sc2_run_task.cancel()
-
- def play_mission(self, mission_id: int) -> bool:
- if self.missions_unlocked or \
- is_mission_available(self, mission_id):
- if self.sc2_run_task:
- if not self.sc2_run_task.done():
- sc2_logger.warning("Starcraft 2 Client is still running!")
- self.sc2_run_task.cancel() # doesn't actually close the game, just stops the python task
- if self.slot is None:
- sc2_logger.warning("Launching Mission without Archipelago authentication, "
- "checks will not be registered to server.")
- self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id),
- name="Starcraft 2 Launch")
- return True
- else:
- sc2_logger.info(
- f"{lookup_id_to_mission[mission_id]} is not currently unlocked. "
- f"Use /unfinished or /available to see what is available.")
- return False
-
- def build_location_to_mission_mapping(self):
- mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = {
- mission_info.id: set() for mission_info in self.mission_req_table.values()
- }
-
- for loc in self.server_locations:
- mission_id, objective = divmod(loc - SC2WOL_LOC_ID_OFFSET, victory_modulo)
- mission_id_to_location_ids[mission_id].add(objective)
- self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in
- mission_id_to_location_ids.items()}
-
- def locations_for_mission(self, mission: str):
- mission_id: int = self.mission_req_table[mission].id
- objectives = self.mission_id_to_location_ids[self.mission_req_table[mission].id]
- for objective in objectives:
- yield SC2WOL_LOC_ID_OFFSET + mission_id * 100 + objective
-
-
-async def main():
- multiprocessing.freeze_support()
- parser = get_base_parser()
- parser.add_argument('--name', default=None, help="Slot Name to connect as.")
- args = parser.parse_args()
-
- ctx = SC2Context(args.connect, args.password)
- ctx.auth = args.name
- if ctx.server_task is None:
- ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
-
- if gui_enabled:
- ctx.run_gui()
- ctx.run_cli()
-
- await ctx.exit_event.wait()
-
- await ctx.shutdown()
-
-
-maps_table = [
- "ap_liberation_day", "ap_the_outlaws", "ap_zero_hour",
- "ap_evacuation", "ap_outbreak", "ap_safe_haven", "ap_havens_fall",
- "ap_smash_and_grab", "ap_the_dig", "ap_the_moebius_factor", "ap_supernova", "ap_maw_of_the_void",
- "ap_devils_playground", "ap_welcome_to_the_jungle", "ap_breakout", "ap_ghost_of_a_chance",
- "ap_the_great_train_robbery", "ap_cutthroat", "ap_engine_of_destruction", "ap_media_blitz", "ap_piercing_the_shroud",
- "ap_whispers_of_doom", "ap_a_sinister_turn", "ap_echoes_of_the_future", "ap_in_utter_darkness",
- "ap_gates_of_hell", "ap_belly_of_the_beast", "ap_shatter_the_sky", "ap_all_in"
-]
-
-wol_default_categories = [
- "Mar Sara", "Mar Sara", "Mar Sara", "Colonist", "Colonist", "Colonist", "Colonist",
- "Artifact", "Artifact", "Artifact", "Artifact", "Artifact", "Covert", "Covert", "Covert", "Covert",
- "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Prophecy", "Prophecy", "Prophecy", "Prophecy",
- "Char", "Char", "Char", "Char"
-]
-wol_default_category_names = [
- "Mar Sara", "Colonist", "Artifact", "Covert", "Rebellion", "Prophecy", "Char"
-]
-
-
-def calculate_items(ctx: SC2Context) -> typing.List[int]:
- items = ctx.items_received
- network_item: NetworkItem
- accumulators: typing.List[int] = [0 for _ in type_flaggroups]
-
- for network_item in items:
- name: str = lookup_id_to_name[network_item.item]
- item_data: ItemData = get_full_item_list()[name]
-
- # exists exactly once
- if item_data.quantity == 1:
- accumulators[type_flaggroups[item_data.type]] |= 1 << item_data.number
-
- # exists multiple times
- elif item_data.type == "Upgrade" or item_data.type == "Progressive Upgrade":
- flaggroup = type_flaggroups[item_data.type]
-
- # Generic upgrades apply only to Weapon / Armor upgrades
- if item_data.type != "Upgrade" or ctx.generic_upgrade_items == 0:
- accumulators[flaggroup] += 1 << item_data.number
- else:
- for bundled_number in upgrade_numbers[item_data.number]:
- accumulators[flaggroup] += 1 << bundled_number
-
- # sum
- else:
- accumulators[type_flaggroups[item_data.type]] += item_data.number
-
- # Upgrades from completed missions
- if ctx.generic_upgrade_missions > 0:
- upgrade_flaggroup = type_flaggroups["Upgrade"]
- num_missions = ctx.generic_upgrade_missions * len(ctx.mission_req_table)
- amounts = [
- num_missions // 100,
- 2 * num_missions // 100,
- 3 * num_missions // 100
- ]
- upgrade_count = 0
- completed = len([id for id in ctx.mission_id_to_location_ids if SC2WOL_LOC_ID_OFFSET + victory_modulo * id in ctx.checked_locations])
- for amount in amounts:
- if completed >= amount:
- upgrade_count += 1
- # Equivalent to "Progressive Weapon/Armor Upgrade" item
- for bundled_number in upgrade_numbers[5]:
- accumulators[upgrade_flaggroup] += upgrade_count << bundled_number
-
- return accumulators
-
-
-def calc_difficulty(difficulty):
- if difficulty == 0:
- return 'C'
- elif difficulty == 1:
- return 'N'
- elif difficulty == 2:
- return 'H'
- elif difficulty == 3:
- return 'B'
-
- return 'X'
-
-
-async def starcraft_launch(ctx: SC2Context, mission_id: int):
- sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
-
- with DllDirectory(None):
- run_game(bot.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
- name="Archipelago", fullscreen=True)], realtime=True)
-
-
-class ArchipelagoBot(bot.bot_ai.BotAI):
- game_running: bool = False
- mission_completed: bool = False
- boni: typing.List[bool]
- setup_done: bool
- ctx: SC2Context
- mission_id: int
- want_close: bool = False
- can_read_game = False
- last_received_update: int = 0
-
- def __init__(self, ctx: SC2Context, mission_id):
- self.setup_done = False
- self.ctx = ctx
- self.ctx.last_bot = self
- self.mission_id = mission_id
- self.boni = [False for _ in range(max_bonus)]
-
- super(ArchipelagoBot, self).__init__()
-
- async def on_step(self, iteration: int):
- if self.want_close:
- self.want_close = False
- await self._client.leave()
- return
- game_state = 0
- if not self.setup_done:
- self.setup_done = True
- start_items = calculate_items(self.ctx)
- if self.ctx.difficulty_override >= 0:
- difficulty = calc_difficulty(self.ctx.difficulty_override)
- else:
- difficulty = calc_difficulty(self.ctx.difficulty)
- if self.ctx.game_speed_override >= 0:
- game_speed = self.ctx.game_speed_override
- else:
- game_speed = self.ctx.game_speed
- await self.chat_send("?SetOptions {} {} {} {}".format(
- difficulty,
- self.ctx.generic_upgrade_research,
- self.ctx.all_in_choice,
- game_speed
- ))
- await self.chat_send("?GiveResources {} {} {}".format(
- start_items[8],
- start_items[9],
- start_items[10]
- ))
- await self.chat_send("?GiveTerranTech {} {} {} {} {} {} {} {} {} {}".format(
- start_items[0], start_items[1], start_items[2], start_items[3], start_items[4],
- start_items[5], start_items[6], start_items[12], start_items[13], start_items[14]))
- await self.chat_send("?GiveProtossTech {}".format(start_items[7]))
- await self.chat_send("?SetColor rr " + str(self.ctx.player_color)) # TODO: Add faction color options
- await self.chat_send("?LoadFinished")
- self.last_received_update = len(self.ctx.items_received)
-
- else:
- if not self.ctx.announcements.empty():
- message = self.ctx.announcements.get(timeout=1)
- await self.chat_send("?SendMessage " + message)
- self.ctx.announcements.task_done()
-
- # Archipelago reads the health
- for unit in self.all_own_units():
- if unit.health_max == 38281:
- game_state = int(38281 - unit.health)
- self.can_read_game = True
-
- if iteration == 160 and not game_state & 1:
- await self.chat_send("?SendMessage Warning: Archipelago unable to connect or has lost connection to " +
- "Starcraft 2 (This is likely a map issue)")
-
- if self.last_received_update < len(self.ctx.items_received):
- current_items = calculate_items(self.ctx)
- await self.chat_send("?GiveTerranTech {} {} {} {} {} {} {} {} {} {}".format(
- current_items[0], current_items[1], current_items[2], current_items[3], current_items[4],
- current_items[5], current_items[6], current_items[12], current_items[13], current_items[14]))
- await self.chat_send("?GiveProtossTech {}".format(current_items[7]))
- self.last_received_update = len(self.ctx.items_received)
-
- if game_state & 1:
- if not self.game_running:
- print("Archipelago Connected")
- self.game_running = True
-
- if self.can_read_game:
- if game_state & (1 << 1) and not self.mission_completed:
- if self.mission_id != self.ctx.final_mission:
- print("Mission Completed")
- await self.ctx.send_msgs(
- [{"cmd": 'LocationChecks',
- "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id]}])
- self.mission_completed = True
- else:
- print("Game Complete")
- await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}])
- self.mission_completed = True
-
- for x, completed in enumerate(self.boni):
- if not completed and game_state & (1 << (x + 2)):
- await self.ctx.send_msgs(
- [{"cmd": 'LocationChecks',
- "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id + x + 1]}])
- self.boni[x] = True
-
- else:
- await self.chat_send("?SendMessage LostConnection - Lost connection to game.")
-
-
-def request_unfinished_missions(ctx: SC2Context):
- if ctx.mission_req_table:
- message = "Unfinished Missions: "
- unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
- unfinished_locations = initialize_blank_mission_dict(ctx.mission_req_table)
-
- _, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks)
-
- # Removing All-In from location pool
- final_mission = lookup_id_to_mission[ctx.final_mission]
- if final_mission in unfinished_missions.keys():
- message = f"Final Mission Available: {final_mission}[{ctx.final_mission}]\n" + message
- if unfinished_missions[final_mission] == -1:
- unfinished_missions.pop(final_mission)
-
- message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " +
- mark_up_objectives(
- f"[{len(unfinished_missions[mission])}/"
- f"{sum(1 for _ in ctx.locations_for_mission(mission))}]",
- ctx, unfinished_locations, mission)
- for mission in unfinished_missions)
-
- if ctx.ui:
- ctx.ui.log_panels['All'].on_message_markup(message)
- ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
- else:
- sc2_logger.info(message)
- else:
- sc2_logger.warning("No mission table found, you are likely not connected to a server.")
-
-
-def calc_unfinished_missions(ctx: SC2Context, unlocks=None):
- unfinished_missions = []
- locations_completed = []
-
- if not unlocks:
- unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
-
- available_missions = calc_available_missions(ctx, unlocks)
-
- for name in available_missions:
- objectives = set(ctx.locations_for_mission(name))
- if objectives:
- objectives_completed = ctx.checked_locations & objectives
- if len(objectives_completed) < len(objectives):
- unfinished_missions.append(name)
- locations_completed.append(objectives_completed)
-
- else: # infer that this is the final mission as it has no objectives
- unfinished_missions.append(name)
- locations_completed.append(-1)
-
- return available_missions, dict(zip(unfinished_missions, locations_completed))
-
-
-def is_mission_available(ctx: SC2Context, mission_id_to_check):
- unfinished_missions = calc_available_missions(ctx)
-
- return any(mission_id_to_check == ctx.mission_req_table[mission].id for mission in unfinished_missions)
-
-
-def mark_up_mission_name(ctx: SC2Context, mission, unlock_table):
- """Checks if the mission is required for game completion and adds '*' to the name to mark that."""
-
- if ctx.mission_req_table[mission].completion_critical:
- if ctx.ui:
- message = "[color=AF99EF]" + mission + "[/color]"
- else:
- message = "*" + mission + "*"
- else:
- message = mission
-
- if ctx.ui:
- unlocks = unlock_table[mission]
-
- if len(unlocks) > 0:
- pre_message = f"[ref={list(ctx.mission_req_table).index(mission)}|Unlocks: "
- pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[unlock].id})" for unlock in unlocks)
- pre_message += f"]"
- message = pre_message + message + "[/ref]"
-
- return message
-
-
-def mark_up_objectives(message, ctx, unfinished_locations, mission):
- formatted_message = message
-
- if ctx.ui:
- locations = unfinished_locations[mission]
-
- pre_message = f"[ref={list(ctx.mission_req_table).index(mission) + 30}|"
- pre_message += " ".join(location for location in locations)
- pre_message += f"]"
- formatted_message = pre_message + message + "[/ref]"
-
- return formatted_message
-
-
-def request_available_missions(ctx: SC2Context):
- if ctx.mission_req_table:
- message = "Available Missions: "
-
- # Initialize mission unlock table
- unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
-
- missions = calc_available_missions(ctx, unlocks)
- message += \
- ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}"
- f"[{ctx.mission_req_table[mission].id}]"
- for mission in missions)
-
- if ctx.ui:
- ctx.ui.log_panels['All'].on_message_markup(message)
- ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
- else:
- sc2_logger.info(message)
- else:
- sc2_logger.warning("No mission table found, you are likely not connected to a server.")
-
-
-def calc_available_missions(ctx: SC2Context, unlocks=None):
- available_missions = []
- missions_complete = 0
-
- # Get number of missions completed
- for loc in ctx.checked_locations:
- if loc % victory_modulo == 0:
- missions_complete += 1
-
- for name in ctx.mission_req_table:
- # Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips
- if unlocks:
- for unlock in ctx.mission_req_table[name].required_world:
- unlocks[list(ctx.mission_req_table)[unlock - 1]].append(name)
-
- if mission_reqs_completed(ctx, name, missions_complete):
- available_missions.append(name)
-
- return available_missions
-
-
-def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete: int):
- """Returns a bool signifying if the mission has all requirements complete and can be done
-
- Arguments:
- ctx -- instance of SC2Context
- locations_to_check -- the mission string name to check
- missions_complete -- an int of how many missions have been completed
- mission_path -- a list of missions that have already been checked
-"""
- if len(ctx.mission_req_table[mission_name].required_world) >= 1:
- # A check for when the requirements are being or'd
- or_success = False
-
- # Loop through required missions
- for req_mission in ctx.mission_req_table[mission_name].required_world:
- req_success = True
-
- # Check if required mission has been completed
- if not (ctx.mission_req_table[list(ctx.mission_req_table)[req_mission - 1]].id *
- victory_modulo + SC2WOL_LOC_ID_OFFSET) in ctx.checked_locations:
- if not ctx.mission_req_table[mission_name].or_requirements:
- return False
- else:
- req_success = False
-
- # Grid-specific logic (to avoid long path checks and infinite recursion)
- if ctx.mission_order in (3, 4):
- if req_success:
- return True
- else:
- if req_mission is ctx.mission_req_table[mission_name].required_world[-1]:
- return False
- else:
- continue
-
- # Recursively check required mission to see if it's requirements are met, in case !collect has been done
- # Skipping recursive check on Grid settings to speed up checks and avoid infinite recursion
- if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete):
- if not ctx.mission_req_table[mission_name].or_requirements:
- return False
- else:
- req_success = False
-
- # If requirement check succeeded mark or as satisfied
- if ctx.mission_req_table[mission_name].or_requirements and req_success:
- or_success = True
-
- if ctx.mission_req_table[mission_name].or_requirements:
- # Return false if or requirements not met
- if not or_success:
- return False
-
- # Check number of missions
- if missions_complete >= ctx.mission_req_table[mission_name].number:
- return True
- else:
- return False
- else:
- return True
-
-
-def initialize_blank_mission_dict(location_table):
- unlocks = {}
-
- for mission in list(location_table):
- unlocks[mission] = []
-
- return unlocks
-
-
-def check_game_install_path() -> bool:
- # First thing: go to the default location for ExecuteInfo.
- # An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it.
- if is_windows:
- # The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow.
- # https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555#
- import ctypes.wintypes
- CSIDL_PERSONAL = 5 # My Documents
- SHGFP_TYPE_CURRENT = 0 # Get current, not default value
-
- buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
- ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf)
- documentspath = buf.value
- einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt"))
- else:
- einfo = str(bot.paths.get_home() / Path(bot.paths.USERPATH[bot.paths.PF]))
-
- # Check if the file exists.
- if os.path.isfile(einfo):
-
- # Open the file and read it, picking out the latest executable's path.
- with open(einfo) as f:
- content = f.read()
- if content:
- try:
- base = re.search(r" = (.*)Versions", content).group(1)
- except AttributeError:
- sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, then "
- f"try again.")
- return False
- if os.path.exists(base):
- executable = bot.paths.latest_executeble(Path(base).expanduser() / "Versions")
-
- # Finally, check the path for an actual executable.
- # If we find one, great. Set up the SC2PATH.
- if os.path.isfile(executable):
- sc2_logger.info(f"Found an SC2 install at {base}!")
- sc2_logger.debug(f"Latest executable at {executable}.")
- os.environ["SC2PATH"] = base
- sc2_logger.debug(f"SC2PATH set to {base}.")
- return True
- else:
- sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.")
- else:
- sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.")
- else:
- sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. "
- f"If that fails, please run /set_path with your SC2 install directory.")
- return False
-
-
-def is_mod_installed_correctly() -> bool:
- """Searches for all required files."""
- if "SC2PATH" not in os.environ:
- check_game_install_path()
-
- mapdir = os.environ['SC2PATH'] / Path('Maps/ArchipelagoCampaign')
- mods = ["ArchipelagoCore", "ArchipelagoPlayer", "ArchipelagoPlayerWoL", "ArchipelagoTriggers"]
- modfiles = [os.environ["SC2PATH"] / Path("Mods/" + mod + ".SC2Mod") for mod in mods]
- wol_required_maps = ["WoL" + os.sep + map_name + ".SC2Map" for map_name in maps_table]
- needs_files = False
-
- # Check for maps.
- missing_maps = []
- for mapfile in wol_required_maps:
- if not os.path.isfile(mapdir / mapfile):
- missing_maps.append(mapfile)
- if len(missing_maps) >= 19:
- sc2_logger.warning(f"All map files missing from {mapdir}.")
- needs_files = True
- elif len(missing_maps) > 0:
- for map in missing_maps:
- sc2_logger.debug(f"Missing {map} from {mapdir}.")
- sc2_logger.warning(f"Missing {len(missing_maps)} map files.")
- needs_files = True
- else: # Must be no maps missing
- sc2_logger.info(f"All maps found in {mapdir}.")
-
- # Check for mods.
- for modfile in modfiles:
- if os.path.isfile(modfile) or os.path.isdir(modfile):
- sc2_logger.info(f"Archipelago mod found at {modfile}.")
- else:
- sc2_logger.warning(f"Archipelago mod could not be found at {modfile}.")
- needs_files = True
-
- # Final verdict.
- if needs_files:
- sc2_logger.warning(f"Required files are missing. Run /download_data to acquire them.")
- return False
- else:
- sc2_logger.debug(f"All map/mod files are properly installed.")
- return True
-
-
-class DllDirectory:
- # Credit to Black Sliver for this code.
- # More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw
- _old: typing.Optional[str] = None
- _new: typing.Optional[str] = None
-
- def __init__(self, new: typing.Optional[str]):
- self._new = new
-
- def __enter__(self):
- old = self.get()
- if self.set(self._new):
- self._old = old
-
- def __exit__(self, *args):
- if self._old is not None:
- self.set(self._old)
-
- @staticmethod
- def get() -> typing.Optional[str]:
- if sys.platform == "win32":
- n = ctypes.windll.kernel32.GetDllDirectoryW(0, None)
- buf = ctypes.create_unicode_buffer(n)
- ctypes.windll.kernel32.GetDllDirectoryW(n, buf)
- return buf.value
- # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
- return None
-
- @staticmethod
- def set(s: typing.Optional[str]) -> bool:
- if sys.platform == "win32":
- return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0
- # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
- return False
-
-
-def download_latest_release_zip(owner: str, repo: str, api_version: str, metadata: str = None, force_download=False) -> (str, str):
- """Downloads the latest release of a GitHub repo to the current directory as a .zip file."""
- import requests
-
- headers = {"Accept": 'application/vnd.github.v3+json'}
- url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{api_version}"
-
- r1 = requests.get(url, headers=headers)
- if r1.status_code == 200:
- latest_metadata = str(r1.json())
- # sc2_logger.info(f"Latest version: {latest_metadata}.")
- else:
- sc2_logger.warning(f"Status code: {r1.status_code}")
- sc2_logger.warning(f"Failed to reach GitHub. Could not find download link.")
- sc2_logger.warning(f"text: {r1.text}")
- return "", metadata
-
- if (force_download is False) and (metadata == latest_metadata):
- sc2_logger.info("Latest version already installed.")
- return "", metadata
-
- sc2_logger.info(f"Attempting to download latest version of API version {api_version} of {repo}.")
- download_url = r1.json()["assets"][0]["browser_download_url"]
-
- r2 = requests.get(download_url, headers=headers)
- if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)):
- with open(f"{repo}.zip", "wb") as fh:
- fh.write(r2.content)
- sc2_logger.info(f"Successfully downloaded {repo}.zip.")
- return f"{repo}.zip", latest_metadata
- else:
- sc2_logger.warning(f"Status code: {r2.status_code}")
- sc2_logger.warning("Download failed.")
- sc2_logger.warning(f"text: {r2.text}")
- return "", metadata
-
-
-def is_mod_update_available(owner: str, repo: str, api_version: str, metadata: str) -> bool:
- import requests
-
- headers = {"Accept": 'application/vnd.github.v3+json'}
- url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{api_version}"
-
- r1 = requests.get(url, headers=headers)
- if r1.status_code == 200:
- latest_metadata = str(r1.json())
- if metadata != latest_metadata:
- return True
- else:
- return False
-
- else:
- sc2_logger.warning(f"Failed to reach GitHub while checking for updates.")
- sc2_logger.warning(f"Status code: {r1.status_code}")
- sc2_logger.warning(f"text: {r1.text}")
- return False
-
-
-def launch():
- colorama.init()
- asyncio.run(main())
- colorama.deinit()
diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py
deleted file mode 100644
index 971a75375fe4..000000000000
--- a/worlds/sc2wol/Items.py
+++ /dev/null
@@ -1,421 +0,0 @@
-from BaseClasses import Item, ItemClassification, MultiWorld
-import typing
-
-from .Options import get_option_value
-from .MissionTables import vanilla_mission_req_table
-
-
-class ItemData(typing.NamedTuple):
- code: typing.Optional[int]
- type: typing.Optional[str]
- number: typing.Optional[int]
- classification: ItemClassification = ItemClassification.useful
- quantity: int = 1
- parent_item: str = None
- origin: typing.Set[str] = {"wol"}
-
-
-class StarcraftWoLItem(Item):
- game: str = "Starcraft 2 Wings of Liberty"
-
-
-def get_full_item_list():
- return item_table
-
-
-SC2WOL_ITEM_ID_OFFSET = 1000
-
-item_table = {
- "Marine": ItemData(0 + SC2WOL_ITEM_ID_OFFSET, "Unit", 0, classification=ItemClassification.progression),
- "Medic": ItemData(1 + SC2WOL_ITEM_ID_OFFSET, "Unit", 1, classification=ItemClassification.progression),
- "Firebat": ItemData(2 + SC2WOL_ITEM_ID_OFFSET, "Unit", 2, classification=ItemClassification.progression),
- "Marauder": ItemData(3 + SC2WOL_ITEM_ID_OFFSET, "Unit", 3, classification=ItemClassification.progression),
- "Reaper": ItemData(4 + SC2WOL_ITEM_ID_OFFSET, "Unit", 4, classification=ItemClassification.progression),
- "Hellion": ItemData(5 + SC2WOL_ITEM_ID_OFFSET, "Unit", 5, classification=ItemClassification.progression),
- "Vulture": ItemData(6 + SC2WOL_ITEM_ID_OFFSET, "Unit", 6, classification=ItemClassification.progression),
- "Goliath": ItemData(7 + SC2WOL_ITEM_ID_OFFSET, "Unit", 7, classification=ItemClassification.progression),
- "Diamondback": ItemData(8 + SC2WOL_ITEM_ID_OFFSET, "Unit", 8, classification=ItemClassification.progression),
- "Siege Tank": ItemData(9 + SC2WOL_ITEM_ID_OFFSET, "Unit", 9, classification=ItemClassification.progression),
- "Medivac": ItemData(10 + SC2WOL_ITEM_ID_OFFSET, "Unit", 10, classification=ItemClassification.progression),
- "Wraith": ItemData(11 + SC2WOL_ITEM_ID_OFFSET, "Unit", 11, classification=ItemClassification.progression),
- "Viking": ItemData(12 + SC2WOL_ITEM_ID_OFFSET, "Unit", 12, classification=ItemClassification.progression),
- "Banshee": ItemData(13 + SC2WOL_ITEM_ID_OFFSET, "Unit", 13, classification=ItemClassification.progression),
- "Battlecruiser": ItemData(14 + SC2WOL_ITEM_ID_OFFSET, "Unit", 14, classification=ItemClassification.progression),
- "Ghost": ItemData(15 + SC2WOL_ITEM_ID_OFFSET, "Unit", 15, classification=ItemClassification.progression),
- "Spectre": ItemData(16 + SC2WOL_ITEM_ID_OFFSET, "Unit", 16, classification=ItemClassification.progression),
- "Thor": ItemData(17 + SC2WOL_ITEM_ID_OFFSET, "Unit", 17, classification=ItemClassification.progression),
- # EE units
- "Liberator": ItemData(18 + SC2WOL_ITEM_ID_OFFSET, "Unit", 18, classification=ItemClassification.progression, origin={"nco", "ext"}),
- "Valkyrie": ItemData(19 + SC2WOL_ITEM_ID_OFFSET, "Unit", 19, classification=ItemClassification.progression, origin={"bw"}),
- "Widow Mine": ItemData(20 + SC2WOL_ITEM_ID_OFFSET, "Unit", 20, classification=ItemClassification.progression, origin={"ext"}),
- "Cyclone": ItemData(21 + SC2WOL_ITEM_ID_OFFSET, "Unit", 21, classification=ItemClassification.progression, origin={"ext"}),
-
- # Some other items are moved to Upgrade group because of the way how the bot message is parsed
- "Progressive Infantry Weapon": ItemData(100 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, quantity=3),
- "Progressive Infantry Armor": ItemData(102 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, quantity=3),
- "Progressive Vehicle Weapon": ItemData(103 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, quantity=3),
- "Progressive Vehicle Armor": ItemData(104 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 6, quantity=3),
- "Progressive Ship Weapon": ItemData(105 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 8, quantity=3),
- "Progressive Ship Armor": ItemData(106 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 10, quantity=3),
- # Upgrade bundle 'number' values are used as indices to get affected 'number's
- "Progressive Weapon Upgrade": ItemData(107 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 0, quantity=3),
- "Progressive Armor Upgrade": ItemData(108 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 1, quantity=3),
- "Progressive Infantry Upgrade": ItemData(109 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 2, quantity=3),
- "Progressive Vehicle Upgrade": ItemData(110 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 3, quantity=3),
- "Progressive Ship Upgrade": ItemData(111 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 4, quantity=3),
- "Progressive Weapon/Armor Upgrade": ItemData(112 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 5, quantity=3),
-
- "Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0, parent_item="Bunker"),
- "Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1, parent_item="Bunker"),
- "Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler, parent_item="Missile Turret"),
- "Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3, parent_item="Missile Turret"),
- "Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4),
- "Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5),
- "Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6),
- "Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7),
- "Progressive Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 0, parent_item="Marine", quantity=2),
- "Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression, parent_item="Marine"),
- "Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.filler, parent_item="Medic"),
- "Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression, parent_item="Medic"),
- "Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler, parent_item="Firebat"),
- "Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13, parent_item="Firebat"),
- "Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14, parent_item="Marauder"),
- "Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15, parent_item="Marauder"),
- "U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16, parent_item="Reaper"),
- "G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression, parent_item="Reaper"),
- # Items from EE
- "Mag-Field Accelerators (Cyclone)": ItemData(218 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 18, parent_item="Cyclone", origin={"ext"}),
- "Mag-Field Launchers (Cyclone)": ItemData(219 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 19, parent_item="Cyclone", origin={"ext"}),
- # Items from new mod
- "Laser Targeting System (Marine)": ItemData(220 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, classification=ItemClassification.filler, parent_item="Marine", origin={"nco"}), # Freed slot from Stimpack
- "Magrail Munitions (Marine)": ItemData(221 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 20, parent_item="Marine", origin={"nco"}),
- "Optimized Logistics (Marine)": ItemData(222 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 21, classification=ItemClassification.filler, parent_item="Marine", origin={"nco"}),
- "Restoration (Medic)": ItemData(223 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 22, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}),
- "Optical Flare (Medic)": ItemData(224 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 23, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}),
- "Optimized Logistics (Medic)": ItemData(225 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 24, classification=ItemClassification.filler, parent_item="Medic", origin={"bw"}),
- "Progressive Stimpack (Firebat)": ItemData(226 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 6, parent_item="Firebat", quantity=2, origin={"bw"}),
- "Optimized Logistics (Firebat)": ItemData(227 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 25, parent_item="Firebat", origin={"bw"}),
- "Progressive Stimpack (Marauder)": ItemData(228 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 8, parent_item="Marauder", quantity=2, origin={"nco"}),
- "Laser Targeting System (Marauder)": ItemData(229 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 26, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}),
- "Magrail Munitions (Marauder)": ItemData(230 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 27, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}),
- "Internal Tech Module (Marauder)": ItemData(231 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 28, classification=ItemClassification.filler, parent_item="Marauder", origin={"nco"}),
-
- # Items from new mod
- "Progressive Stimpack (Reaper)": ItemData(250 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 10, parent_item="Reaper", quantity=2, origin={"nco"}),
- "Laser Targeting System (Reaper)": ItemData(251 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 0, classification=ItemClassification.filler, parent_item="Reaper", origin={"nco"}),
- "Advanced Cloaking Field (Reaper)": ItemData(252 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 1, parent_item="Reaper", origin={"nco"}),
- "Spider Mines (Reaper)": ItemData(253 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 2, classification=ItemClassification.filler, parent_item="Reaper", origin={"nco"}),
- "Combat Drugs (Reaper)": ItemData(254 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 3, classification=ItemClassification.filler, parent_item="Reaper", origin={"ext"}),
- "Hellbat Aspect (Hellion)": ItemData(255 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 4, parent_item="Hellion", origin={"nco"}),
- "Smart Servos (Hellion)": ItemData(256 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 5, parent_item="Hellion", origin={"nco"}),
- "Optimized Logistics (Hellion)": ItemData(257 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 6, classification=ItemClassification.filler, parent_item="Hellion", origin={"nco"}),
- "Jump Jets (Hellion)": ItemData(258 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 7, classification=ItemClassification.filler, parent_item="Hellion", origin={"nco"}),
- "Progressive Stimpack (Hellion)": ItemData(259 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 12, parent_item="Hellion", quantity=2, origin={"nco"}),
- "Ion Thrusters (Vulture)": ItemData(260 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 8, classification=ItemClassification.filler, parent_item="Vulture", origin={"bw"}),
- "Auto Launchers (Vulture)": ItemData(261 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 9, parent_item="Vulture", origin={"bw"}),
- "High Explosive Munition (Spider Mine)": ItemData(262 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 10, origin={"bw"}),
- "Jump Jets (Goliath)": ItemData(263 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 11, classification=ItemClassification.filler, parent_item="Goliath", origin={"nco"}),
- "Optimized Logistics (Goliath)": ItemData(264 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 12, classification=ItemClassification.filler, parent_item="Goliath", origin={"nco"}),
- "Hyperfluxor (Diamondback)": ItemData(265 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 13, parent_item="Diamondback", origin={"ext"}),
- "Burst Capacitors (Diamondback)": ItemData(266 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 14, classification=ItemClassification.filler, parent_item="Diamondback", origin={"ext"}),
- "Optimized Logistics (Diamondback)": ItemData(267 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 15, parent_item="Diamondback", origin={"ext"}),
- "Jump Jets (Siege Tank)": ItemData(268 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 16, parent_item="Siege Tank", origin={"nco"}),
- "Spider Mines (Siege Tank)": ItemData(269 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 17, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}),
- "Smart Servos (Siege Tank)": ItemData(270 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 18, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}),
- "Graduating Range (Siege Tank)": ItemData(271 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 19, classification=ItemClassification.progression, parent_item="Siege Tank", origin={"ext"}),
- "Laser Targeting System (Siege Tank)": ItemData(272 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 20, parent_item="Siege Tank", origin={"nco"}),
- "Advanced Siege Tech (Siege Tank)": ItemData(273 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 21, parent_item="Siege Tank", origin={"ext"}),
- "Internal Tech Module (Siege Tank)": ItemData(274 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 22, classification=ItemClassification.filler, parent_item="Siege Tank", origin={"nco"}),
- "Optimized Logistics (Predator)": ItemData(275 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 23, classification=ItemClassification.filler, parent_item="Predator", origin={"ext"}),
- "Expanded Hull (Medivac)": ItemData(276 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 24, classification=ItemClassification.filler, parent_item="Medivac", origin={"ext"}),
- "Afterburners (Medivac)": ItemData(277 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 25, classification=ItemClassification.filler, parent_item="Medivac", origin={"ext"}),
- "Advanced Laser Technology (Wraith)": ItemData(278 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 26, classification=ItemClassification.progression, parent_item="Wraith", origin={"ext"}),
- "Smart Servos (Viking)": ItemData(279 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 27, parent_item="Viking", origin={"ext"}),
- "Magrail Munitions (Viking)": ItemData(280 + SC2WOL_ITEM_ID_OFFSET, "Armory 3", 28, parent_item="Viking", origin={"ext"}),
-
- "Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler, parent_item="Hellion"),
- "Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1, parent_item="Hellion"),
- "Cerberus Mine (Spider Mine)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler),
- "Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler, parent_item="Vulture"),
- "Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4, parent_item="Goliath"),
- "Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5, parent_item="Goliath"),
- "Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler, parent_item="Diamondback"),
- "Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, classification=ItemClassification.filler, parent_item="Diamondback"),
- "Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8, classification=ItemClassification.progression, parent_item="Siege Tank"),
- "Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9, parent_item="Siege Tank"),
- "Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler, parent_item="Medivac"),
- "Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, classification=ItemClassification.filler, parent_item="Medivac"),
- "Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler, parent_item="Wraith"),
- "Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler, parent_item="Wraith"),
- "Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14, parent_item="Viking"),
- "Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15, parent_item="Viking"),
- "Progressive Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 2, classification=ItemClassification.filler, parent_item="Banshee", quantity=2),
- "Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17, parent_item="Banshee"),
- "Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler, parent_item="Battlecruiser"),
- "Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler, parent_item="Battlecruiser"),
- "Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20, parent_item="Ghost"),
- "Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21, parent_item="Ghost"),
- "Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, classification=ItemClassification.progression, parent_item="Spectre"),
- "Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23, parent_item="Spectre"),
- "330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler, parent_item="Thor"),
- "Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler, parent_item="Thor"),
- # Items from EE
- "Advanced Ballistics (Liberator)": ItemData(326 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 26, parent_item="Liberator", origin={"ext"}),
- "Raid Artillery (Liberator)": ItemData(327 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 27, classification=ItemClassification.progression, parent_item="Liberator", origin={"nco"}),
- "Drilling Claws (Widow Mine)": ItemData(328 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 28, classification=ItemClassification.filler, parent_item="Widow Mine", origin={"ext"}),
- "Concealment (Widow Mine)": ItemData(329 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 29, classification=ItemClassification.progression, parent_item="Widow Mine", origin={"ext"}),
-
- #Items from new mod
- "Hyperflight Rotors (Banshee)": ItemData(350 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 0, classification=ItemClassification.filler, parent_item="Banshee", origin={"ext"}),
- "Laser Targeting System (Banshee)": ItemData(351 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 1, classification=ItemClassification.filler, parent_item="Banshee", origin={"nco"}),
- "Internal Tech Module (Banshee)": ItemData(352 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 2, classification=ItemClassification.filler, parent_item="Banshee", origin={"nco"}),
- "Tactical Jump (Battlecruiser)": ItemData(353 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 3, parent_item="Battlecruiser", origin={"nco", "ext"}),
- "Cloak (Battlecruiser)": ItemData(354 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 4, parent_item="Battlecruiser", origin={"nco"}),
- "ATX Laser Battery (Battlecruiser)": ItemData(355 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 5, classification=ItemClassification.progression, parent_item="Battlecruiser", origin={"nco"}),
- "Optimized Logistics (Battlecruiser)": ItemData(356 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 6, classification=ItemClassification.filler, parent_item="Battlecruiser", origin={"ext"}),
- "Internal Tech Module (Battlecruiser)": ItemData(357 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 7, classification=ItemClassification.filler, parent_item="Battlecruiser", origin={"nco"}),
- "EMP Rounds (Ghost)": ItemData(358 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 8, parent_item="Ghost", origin={"ext"}),
- "Lockdown (Ghost)": ItemData(359 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 9, parent_item="Ghost", origin={"bw"}),
- "Impaler Rounds (Spectre)": ItemData(360 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 10, parent_item="Spectre", origin={"ext"}),
- "Progressive High Impact Payload (Thor)": ItemData(361 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 14, parent_item="Thor", quantity=2, origin={"ext"}), # L2 is Smart Servos
- "Bio Mechanical Repair Drone (Raven)": ItemData(363 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 13, parent_item="Raven", origin={"nco"}),
- "Spider Mines (Raven)": ItemData(364 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 14, parent_item="Raven", origin={"nco"}),
- "Railgun Turret (Raven)": ItemData(365 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 15, parent_item="Raven", origin={"nco"}),
- "Hunter-Seeker Weapon (Raven)": ItemData(366 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 16, parent_item="Raven", origin={"nco"}),
- "Interference Matrix (Raven)": ItemData(367 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 17, parent_item="Raven", origin={"ext"}),
- "Anti-Armor Missile (Raven)": ItemData(368 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 18, classification=ItemClassification.filler, parent_item="Raven", origin={"ext"}),
- "Internal Tech Module (Raven)": ItemData(369 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 19, classification=ItemClassification.filler, parent_item="Raven", origin={"nco"}),
- "EMP Shockwave (Science Vessel)": ItemData(370 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 20, parent_item="Science Vessel", origin={"bw"}),
- "Defensive Matrix (Science Vessel)": ItemData(371 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 21, parent_item="Science Vessel", origin={"bw"}),
- "Targeting Optics (Cyclone)": ItemData(372 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 22, parent_item="Cyclone", origin={"ext"}),
- "Rapid Fire Launchers (Cyclone)": ItemData(373 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 23, parent_item="Cyclone", origin={"ext"}),
- "Cloak (Liberator)": ItemData(374 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 24, classification=ItemClassification.filler, parent_item="Liberator", origin={"nco"}),
- "Laser Targeting System (Liberator)": ItemData(375 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 25, classification=ItemClassification.filler, parent_item="Liberator", origin={"ext"}),
- "Optimized Logistics (Liberator)": ItemData(376 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 26, classification=ItemClassification.filler, parent_item="Liberator", origin={"nco"}),
- "Black Market Launchers (Widow Mine)": ItemData(377 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 27, classification=ItemClassification.filler, parent_item="Widow Mine", origin={"ext"}),
- "Executioner Missiles (Widow Mine)": ItemData(378 + SC2WOL_ITEM_ID_OFFSET, "Armory 4", 28, parent_item="Widow Mine", origin={"ext"}),
-
- # Just lazy to create a new group for one unit
- "Enhanced Cluster Launchers (Valkyrie)": ItemData(379 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17, parent_item="Valkyrie", origin={"ext"}),
- "Shaped Hull (Valkyrie)": ItemData(380 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 20, classification=ItemClassification.filler, parent_item="Valkyrie", origin={"ext"}),
- "Burst Lasers (Valkyrie)": ItemData(381 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 21, parent_item="Valkyrie", origin={"ext"}),
- "Afterburners (Valkyrie)": ItemData(382 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 22, classification=ItemClassification.filler, parent_item="Valkyrie", origin={"ext"}),
-
- "Bunker": ItemData(400 + SC2WOL_ITEM_ID_OFFSET, "Building", 0, classification=ItemClassification.progression),
- "Missile Turret": ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, classification=ItemClassification.progression),
- "Sensor Tower": ItemData(402 + SC2WOL_ITEM_ID_OFFSET, "Building", 2),
-
- "War Pigs": ItemData(500 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 0, classification=ItemClassification.progression),
- "Devil Dogs": ItemData(501 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 1, classification=ItemClassification.filler),
- "Hammer Securities": ItemData(502 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 2),
- "Spartan Company": ItemData(503 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 3, classification=ItemClassification.progression),
- "Siege Breakers": ItemData(504 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 4),
- "Hel's Angel": ItemData(505 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 5, classification=ItemClassification.progression),
- "Dusk Wings": ItemData(506 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 6),
- "Jackson's Revenge": ItemData(507 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 7),
-
- "Ultra-Capacitors": ItemData(600 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 0),
- "Vanadium Plating": ItemData(601 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 1),
- "Orbital Depots": ItemData(602 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 2),
- "Micro-Filtering": ItemData(603 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 3),
- "Automated Refinery": ItemData(604 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 4),
- "Command Center Reactor": ItemData(605 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 5),
- "Raven": ItemData(606 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 6),
- "Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7, classification=ItemClassification.progression),
- "Tech Reactor": ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8),
- "Orbital Strike": ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9),
- "Shrike Turret (Bunker)": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, parent_item="Bunker"),
- "Fortified Bunker (Bunker)": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, parent_item="Bunker"),
- "Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, classification=ItemClassification.progression),
- "Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13, classification=ItemClassification.progression),
- "Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.filler),
- "Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, classification=ItemClassification.progression),
- "Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16),
- "Progressive Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Progressive Upgrade", 4, quantity=2),
- "Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18, ItemClassification.progression),
- "Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.progression),
-
- "Zealot": ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 0, classification=ItemClassification.progression),
- "Stalker": ItemData(701 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 1, classification=ItemClassification.progression),
- "High Templar": ItemData(702 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 2, classification=ItemClassification.progression),
- "Dark Templar": ItemData(703 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 3, classification=ItemClassification.progression),
- "Immortal": ItemData(704 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 4, classification=ItemClassification.progression),
- "Colossus": ItemData(705 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 5),
- "Phoenix": ItemData(706 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 6, classification=ItemClassification.filler),
- "Void Ray": ItemData(707 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 7, classification=ItemClassification.progression),
- "Carrier": ItemData(708 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 8, classification=ItemClassification.progression),
-
- # Filler items to fill remaining spots
- "+15 Starting Minerals": ItemData(800 + SC2WOL_ITEM_ID_OFFSET, "Minerals", 15, quantity=0, classification=ItemClassification.filler),
- "+15 Starting Vespene": ItemData(801 + SC2WOL_ITEM_ID_OFFSET, "Vespene", 15, quantity=0, classification=ItemClassification.filler),
- # This Filler item isn't placed by the generator yet unless plando'd
- "+2 Starting Supply": ItemData(802 + SC2WOL_ITEM_ID_OFFSET, "Supply", 2, quantity=0, classification=ItemClassification.filler),
- # This item is used to "remove" location from the game. Never placed unless plando'd
- "Nothing": ItemData(803 + SC2WOL_ITEM_ID_OFFSET, "Nothing Group", 2, quantity=0, classification=ItemClassification.trap),
-
- # "Keystone Piece": ItemData(850 + SC2WOL_ITEM_ID_OFFSET, "Goal", 0, quantity=0, classification=ItemClassification.progression_skip_balancing)
-}
-
-def get_item_table(multiworld: MultiWorld, player: int):
- return item_table
-
-basic_units = {
- 'Marine',
- 'Marauder',
- 'Goliath',
- 'Hellion',
- 'Vulture'
-}
-
-advanced_basic_units = basic_units.union({
- 'Reaper',
- 'Diamondback',
- 'Viking'
-})
-
-
-def get_basic_units(multiworld: MultiWorld, player: int) -> typing.Set[str]:
- if get_option_value(multiworld, player, 'required_tactics') > 0:
- return advanced_basic_units
- else:
- return basic_units
-
-
-item_name_groups = {}
-for item, data in get_full_item_list().items():
- item_name_groups.setdefault(data.type, []).append(item)
- if data.type in ("Armory 1", "Armory 2") and '(' in item:
- short_name = item[:item.find(' (')]
- item_name_groups[short_name] = [item]
-item_name_groups["Missions"] = ["Beat " + mission_name for mission_name in vanilla_mission_req_table]
-
-
-# Items that can be placed before resources if not already in
-# General upgrades and Mercs
-second_pass_placeable_items: typing.Tuple[str, ...] = (
- # Buildings without upgrades
- "Sensor Tower",
- "Hive Mind Emulator",
- "Psi Disrupter",
- "Perdition Turret",
- # General upgrades without any dependencies
- "Advanced Construction (SCV)",
- "Dual-Fusion Welders (SCV)",
- "Fire-Suppression System (Building)",
- "Orbital Command (Building)",
- "Ultra-Capacitors",
- "Vanadium Plating",
- "Orbital Depots",
- "Micro-Filtering",
- "Automated Refinery",
- "Command Center Reactor",
- "Tech Reactor",
- "Planetary Fortress",
- "Cellular Reactor",
- "Progressive Regenerative Bio-Steel", # Place only L1
- # Mercenaries
- "War Pigs",
- "Devil Dogs",
- "Hammer Securities",
- "Spartan Company",
- "Siege Breakers",
- "Hel's Angel",
- "Dusk Wings",
- "Jackson's Revenge"
-)
-
-
-filler_items: typing.Tuple[str, ...] = (
- '+15 Starting Minerals',
- '+15 Starting Vespene'
-)
-
-# Defense rating table
-# Commented defense ratings are handled in LogicMixin
-defense_ratings = {
- "Siege Tank": 5,
- # "Maelstrom Rounds": 2,
- "Planetary Fortress": 3,
- # Bunker w/ Marine/Marauder: 3,
- "Perdition Turret": 2,
- "Missile Turret": 2,
- "Vulture": 2,
- "Liberator": 2,
- "Widow Mine": 2
- # "Concealment (Widow Mine)": 1
-}
-zerg_defense_ratings = {
- "Perdition Turret": 2,
- # Bunker w/ Firebat: 2,
- "Hive Mind Emulator": 3,
- "Psi Disruptor": 3
-}
-
-spider_mine_sources = {
- "Vulture",
- "Spider Mines (Reaper)",
- "Spider Mines (Siege Tank)",
- "Spider Mines (Raven)"
-}
-
-progressive_if_nco = {
- "Progressive Stimpack (Marine)",
- "Progressive Stimpack (Firebat)",
- "Progressive Cross-Spectrum Dampeners (Banshee)",
- "Progressive Regenerative Bio-Steel"
-}
-
-# 'number' values of upgrades for upgrade bundle items
-upgrade_numbers = [
- {0, 4, 8}, # Weapon
- {2, 6, 10}, # Armor
- {0, 2}, # Infantry
- {4, 6}, # Vehicle
- {8, 10}, # Starship
- {0, 2, 4, 6, 8, 10} # All
-]
-# Names of upgrades to be included for different options
-upgrade_included_names = [
- { # Individual Items
- "Progressive Infantry Weapon",
- "Progressive Infantry Armor",
- "Progressive Vehicle Weapon",
- "Progressive Vehicle Armor",
- "Progressive Ship Weapon",
- "Progressive Ship Armor"
- },
- { # Bundle Weapon And Armor
- "Progressive Weapon Upgrade",
- "Progressive Armor Upgrade"
- },
- { # Bundle Unit Class
- "Progressive Infantry Upgrade",
- "Progressive Vehicle Upgrade",
- "Progressive Starship Upgrade"
- },
- { # Bundle All
- "Progressive Weapon/Armor Upgrade"
- }
-]
-
-lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if
- data.code}
-# Map type to expected int
-type_flaggroups: typing.Dict[str, int] = {
- "Unit": 0,
- "Upgrade": 1, # Weapon / Armor upgrades
- "Armory 1": 2, # Unit upgrades
- "Armory 2": 3, # Unit upgrades
- "Building": 4,
- "Mercenary": 5,
- "Laboratory": 6,
- "Protoss": 7,
- "Minerals": 8,
- "Vespene": 9,
- "Supply": 10,
- "Goal": 11,
- "Armory 3": 12, # Unit upgrades
- "Armory 4": 13, # Unit upgrades
- "Progressive Upgrade": 14, # Unit upgrades that exist multiple times (Stimpack / Super Stimpack)
- "Nothing Group": 15
-}
diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py
deleted file mode 100644
index ae31fa8eaadd..000000000000
--- a/worlds/sc2wol/Locations.py
+++ /dev/null
@@ -1,516 +0,0 @@
-from enum import IntEnum
-from typing import List, Tuple, Optional, Callable, NamedTuple
-from BaseClasses import MultiWorld
-from .Options import get_option_value
-
-from BaseClasses import Location
-
-SC2WOL_LOC_ID_OFFSET = 1000
-
-
-class SC2WoLLocation(Location):
- game: str = "Starcraft2WoL"
-
-
-class LocationType(IntEnum):
- VICTORY = 0 # Winning a mission
- MISSION_PROGRESS = 1 # All tasks done for progressing the mission normally towards victory. All cleaning of expansion bases falls here
- BONUS = 2 # Bonus objective, getting a campaign or mission bonus in vanilla (credits, research, bonus units or resources)
- CHALLENGE = 3 # Challenging objectives, often harder than just completing a mission
- OPTIONAL_BOSS = 4 # Any boss that's not required to win the mission. All Brutalisks, Loki, etc.
-
-class LocationData(NamedTuple):
- region: str
- name: str
- code: Optional[int]
- type: LocationType
- rule: Callable = lambda state: True
-
-
-def get_locations(multiworld: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]:
- # Note: rules which are ended with or True are rules identified as needed later when restricted units is an option
- logic_level = get_option_value(multiworld, player, 'required_tactics')
- location_table: List[LocationData] = [
- LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100, LocationType.VICTORY),
- LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101, LocationType.BONUS),
- LocationData("Liberation Day", "Liberation Day: Second Statue", SC2WOL_LOC_ID_OFFSET + 102, LocationType.BONUS),
- LocationData("Liberation Day", "Liberation Day: Third Statue", SC2WOL_LOC_ID_OFFSET + 103, LocationType.BONUS),
- LocationData("Liberation Day", "Liberation Day: Fourth Statue", SC2WOL_LOC_ID_OFFSET + 104, LocationType.BONUS),
- LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105, LocationType.BONUS),
- LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106, LocationType.BONUS),
- LocationData("Liberation Day", "Liberation Day: Special Delivery", SC2WOL_LOC_ID_OFFSET + 107, LocationType.MISSION_PROGRESS),
- LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200, LocationType.VICTORY,
- lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201, LocationType.BONUS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("The Outlaws", "The Outlaws: North Resource Pickups", SC2WOL_LOC_ID_OFFSET + 202, LocationType.BONUS),
- LocationData("The Outlaws", "The Outlaws: Bunker", SC2WOL_LOC_ID_OFFSET + 203, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300, LocationType.VICTORY,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_defense_rating(multiworld, player, True) >= 2 and
- (logic_level > 0 or state._sc2wol_has_anti_air(multiworld, player))),
- LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301, LocationType.BONUS),
- LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302, LocationType.BONUS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303, LocationType.BONUS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_defense_rating(multiworld, player, True) >= 2),
- LocationData("Zero Hour", "Zero Hour: First Hatchery", SC2WOL_LOC_ID_OFFSET + 304, LocationType.CHALLENGE,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Zero Hour", "Zero Hour: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 305, LocationType.CHALLENGE,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Zero Hour", "Zero Hour: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 306, LocationType.CHALLENGE,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Zero Hour", "Zero Hour: Fourth Hatchery", SC2WOL_LOC_ID_OFFSET + 307, LocationType.CHALLENGE,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400, LocationType.VICTORY,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
- or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("Evacuation", "Evacuation: First Chrysalis", SC2WOL_LOC_ID_OFFSET + 401, LocationType.BONUS),
- LocationData("Evacuation", "Evacuation: Second Chrysalis", SC2WOL_LOC_ID_OFFSET + 402, LocationType.BONUS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Evacuation", "Evacuation: Third Chrysalis", SC2WOL_LOC_ID_OFFSET + 403, LocationType.BONUS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Evacuation", "Evacuation: Reach Hanson", SC2WOL_LOC_ID_OFFSET + 404, LocationType.MISSION_PROGRESS),
- LocationData("Evacuation", "Evacuation: Secret Resource Stash", SC2WOL_LOC_ID_OFFSET + 405, LocationType.BONUS),
- LocationData("Evacuation", "Evacuation: Flawless", SC2WOL_LOC_ID_OFFSET + 406, LocationType.CHALLENGE,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
- (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
- or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500, LocationType.VICTORY,
- lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 4 and
- (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501, LocationType.BONUS,
- lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
- (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502, LocationType.BONUS,
- lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
- (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Outbreak", "Outbreak: North Infested Command Center", SC2WOL_LOC_ID_OFFSET + 503, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
- (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Outbreak", "Outbreak: South Infested Command Center", SC2WOL_LOC_ID_OFFSET + 504, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
- (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Outbreak", "Outbreak: Northwest Bar", SC2WOL_LOC_ID_OFFSET + 505, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
- (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Outbreak", "Outbreak: North Bar", SC2WOL_LOC_ID_OFFSET + 506, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
- (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Outbreak", "Outbreak: South Bar", SC2WOL_LOC_ID_OFFSET + 507, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_defense_rating(multiworld, player, True, False) >= 2 and
- (state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600, LocationType.VICTORY,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Safe Haven", "Safe Haven: North Nexus", SC2WOL_LOC_ID_OFFSET + 601, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Safe Haven", "Safe Haven: East Nexus", SC2WOL_LOC_ID_OFFSET + 602, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Safe Haven", "Safe Haven: South Nexus", SC2WOL_LOC_ID_OFFSET + 603, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Safe Haven", "Safe Haven: First Terror Fleet", SC2WOL_LOC_ID_OFFSET + 604, LocationType.BONUS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Safe Haven", "Safe Haven: Second Terror Fleet", SC2WOL_LOC_ID_OFFSET + 605, LocationType.BONUS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Safe Haven", "Safe Haven: Third Terror Fleet", SC2WOL_LOC_ID_OFFSET + 606, LocationType.BONUS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player)),
- LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700, LocationType.VICTORY,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player) and
- state._sc2wol_defense_rating(multiworld, player, True) >= 3),
- LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player) and
- state._sc2wol_defense_rating(multiworld, player, True) >= 3),
- LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player) and
- state._sc2wol_defense_rating(multiworld, player, True) >= 3),
- LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- state._sc2wol_has_competent_anti_air(multiworld, player) and
- state._sc2wol_defense_rating(multiworld, player, True) >= 3),
- LocationData("Haven's Fall", "Haven's Fall: Northeast Colony Base", SC2WOL_LOC_ID_OFFSET + 704, LocationType.CHALLENGE,
- lambda state: state._sc2wol_can_respond_to_colony_infestations),
- LocationData("Haven's Fall", "Haven's Fall: East Colony Base", SC2WOL_LOC_ID_OFFSET + 705, LocationType.CHALLENGE,
- lambda state: state._sc2wol_can_respond_to_colony_infestations),
- LocationData("Haven's Fall", "Haven's Fall: Middle Colony Base", SC2WOL_LOC_ID_OFFSET + 706, LocationType.CHALLENGE,
- lambda state: state._sc2wol_can_respond_to_colony_infestations),
- LocationData("Haven's Fall", "Haven's Fall: Southeast Colony Base", SC2WOL_LOC_ID_OFFSET + 707, LocationType.CHALLENGE,
- lambda state: state._sc2wol_can_respond_to_colony_infestations),
- LocationData("Haven's Fall", "Haven's Fall: Southwest Colony Base", SC2WOL_LOC_ID_OFFSET + 708, LocationType.CHALLENGE,
- lambda state: state._sc2wol_can_respond_to_colony_infestations),
- LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800, LocationType.VICTORY,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
- or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801, LocationType.BONUS),
- LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802, LocationType.BONUS),
- LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803, LocationType.BONUS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
- or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804, LocationType.BONUS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
- or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("Smash and Grab", "Smash and Grab: First Forcefield Area Busted", SC2WOL_LOC_ID_OFFSET + 805, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
- or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("Smash and Grab", "Smash and Grab: Second Forcefield Area Busted", SC2WOL_LOC_ID_OFFSET + 806, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player)
- or state._sc2wol_has_competent_anti_air(multiworld, player))),
- LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900, LocationType.VICTORY,
- lambda state: state._sc2wol_has_anti_air(multiworld, player) and
- state._sc2wol_defense_rating(multiworld, player, False) >= 7),
- LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901, LocationType.BONUS,
- lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5),
- LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902, LocationType.BONUS,
- lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5),
- LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903, LocationType.BONUS,
- lambda state: state._sc2wol_defense_rating(multiworld, player, False) >= 5),
- LocationData("The Dig", "The Dig: Moebius Base", SC2WOL_LOC_ID_OFFSET + 904, LocationType.MISSION_PROGRESS),
- LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, LocationType.VICTORY,
- lambda state: state._sc2wol_has_anti_air(multiworld, player) and
- (state._sc2wol_has_air(multiworld, player)
- or state.has_any({'Medivac', 'Hercules'}, player)
- and state._sc2wol_has_common_unit(multiworld, player))),
- LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core", SC2WOL_LOC_ID_OFFSET + 1001, LocationType.MISSION_PROGRESS),
- LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002, LocationType.MISSION_PROGRESS,
- lambda state: (state._sc2wol_has_air(multiworld, player)
- or state.has_any({'Medivac', 'Hercules'}, player)
- and state._sc2wol_has_common_unit(multiworld, player))),
- LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, LocationType.BONUS,
- lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, LocationType.BONUS,
- lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005, LocationType.BONUS,
- lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006, LocationType.BONUS,
- lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, LocationType.BONUS,
- lambda state: state._sc2wol_able_to_rescue(multiworld, player)),
- LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, LocationType.OPTIONAL_BOSS,
- lambda state: state._sc2wol_has_anti_air(multiworld, player) and
- (state._sc2wol_has_air(multiworld, player)
- or state.has_any({'Medivac', 'Hercules'}, player)
- and state._sc2wol_has_common_unit(multiworld, player))),
- LocationData("The Moebius Factor", "The Moebius Factor: 3rd Data Core", SC2WOL_LOC_ID_OFFSET + 1009, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_anti_air(multiworld, player) and
- (state._sc2wol_has_air(multiworld, player)
- or state.has_any({'Medivac', 'Hercules'}, player)
- and state._sc2wol_has_common_unit(multiworld, player))),
- LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100, LocationType.VICTORY,
- lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
- LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101, LocationType.BONUS),
- LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102, LocationType.BONUS),
- LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103, LocationType.BONUS,
- lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
- LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, LocationType.BONUS,
- lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
- LocationData("Supernova", "Supernova: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1105, LocationType.MISSION_PROGRESS),
- LocationData("Supernova", "Supernova: Middle Base", SC2WOL_LOC_ID_OFFSET + 1106, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
- LocationData("Supernova", "Supernova: Southeast Base", SC2WOL_LOC_ID_OFFSET + 1107, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_beats_protoss_deathball(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200, LocationType.VICTORY,
- lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201, LocationType.MISSION_PROGRESS),
- LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202, LocationType.BONUS,
- lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203, LocationType.BONUS,
- lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204, LocationType.BONUS,
- lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205, LocationType.BONUS,
- lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: Mothership", SC2WOL_LOC_ID_OFFSET + 1206, LocationType.OPTIONAL_BOSS,
- lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: Expansion Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1207, LocationType.MISSION_PROGRESS,
- lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: Middle Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1208, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: Southeast Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1209, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: Stargate Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1210, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: Northwest Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1211, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: West Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1212, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Maw of the Void", "Maw of the Void: Southwest Rip Field Generator", SC2WOL_LOC_ID_OFFSET + 1213, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_survives_rip_field(multiworld, player)),
- LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, LocationType.VICTORY,
- lambda state: logic_level > 0 or
- state._sc2wol_has_anti_air(multiworld, player) and (
- state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301, LocationType.BONUS),
- LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302, LocationType.OPTIONAL_BOSS,
- lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)),
- LocationData("Devil's Playground", "Devil's Playground: North Reapers", SC2WOL_LOC_ID_OFFSET + 1303, LocationType.BONUS),
- LocationData("Devil's Playground", "Devil's Playground: Middle Reapers", SC2WOL_LOC_ID_OFFSET + 1304, LocationType.BONUS,
- lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)),
- LocationData("Devil's Playground", "Devil's Playground: Southwest Reapers", SC2WOL_LOC_ID_OFFSET + 1305, LocationType.BONUS,
- lambda state: logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player)),
- LocationData("Devil's Playground", "Devil's Playground: Southeast Reapers", SC2WOL_LOC_ID_OFFSET + 1306, LocationType.BONUS,
- lambda state: logic_level > 0 or
- state._sc2wol_has_anti_air(multiworld, player) and (
- state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Devil's Playground", "Devil's Playground: East Reapers", SC2WOL_LOC_ID_OFFSET + 1307, LocationType.BONUS,
- lambda state: state._sc2wol_has_anti_air(multiworld, player) and
- (logic_level > 0 or
- state._sc2wol_has_common_unit(multiworld, player) or state.has("Reaper", player))),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400, LocationType.VICTORY,
- lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401, LocationType.BONUS),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402, LocationType.BONUS,
- lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403, LocationType.BONUS,
- lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: Middle Base", SC2WOL_LOC_ID_OFFSET + 1404, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: Main Base", SC2WOL_LOC_ID_OFFSET + 1405, LocationType.CHALLENGE,
- lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
- and state._sc2wol_beats_protoss_deathball(multiworld, player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: No Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1406, LocationType.CHALLENGE,
- lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
- and state._sc2wol_has_competent_ground_to_air(multiworld, player)
- and state._sc2wol_beats_protoss_deathball(multiworld, player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 1 Terrazine Node Sealed", SC2WOL_LOC_ID_OFFSET + 1407, LocationType.CHALLENGE,
- lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
- and state._sc2wol_has_competent_ground_to_air(multiworld, player)
- and state._sc2wol_beats_protoss_deathball(multiworld, player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 2 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1408, LocationType.CHALLENGE,
- lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
- and state._sc2wol_beats_protoss_deathball(multiworld, player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 3 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1409, LocationType.CHALLENGE,
- lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)
- and state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 4 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1410, LocationType.CHALLENGE,
- lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
- LocationData("Welcome to the Jungle", "Welcome to the Jungle: Up to 5 Terrazine Nodes Sealed", SC2WOL_LOC_ID_OFFSET + 1411, LocationType.CHALLENGE,
- lambda state: state._sc2wol_welcome_to_the_jungle_requirement(multiworld, player)),
- LocationData("Breakout", "Breakout: Victory", SC2WOL_LOC_ID_OFFSET + 1500, LocationType.VICTORY),
- LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501, LocationType.BONUS),
- LocationData("Breakout", "Breakout: Siege Tank Prison", SC2WOL_LOC_ID_OFFSET + 1502, LocationType.BONUS),
- LocationData("Breakout", "Breakout: First Checkpoint", SC2WOL_LOC_ID_OFFSET + 1503, LocationType.MISSION_PROGRESS),
- LocationData("Breakout", "Breakout: Second Checkpoint", SC2WOL_LOC_ID_OFFSET + 1504, LocationType.MISSION_PROGRESS),
- LocationData("Ghost of a Chance", "Ghost of a Chance: Victory", SC2WOL_LOC_ID_OFFSET + 1600, LocationType.VICTORY),
- LocationData("Ghost of a Chance", "Ghost of a Chance: Terrazine Tank", SC2WOL_LOC_ID_OFFSET + 1601, LocationType.MISSION_PROGRESS),
- LocationData("Ghost of a Chance", "Ghost of a Chance: Jorium Stockpile", SC2WOL_LOC_ID_OFFSET + 1602, LocationType.MISSION_PROGRESS),
- LocationData("Ghost of a Chance", "Ghost of a Chance: First Island Spectres", SC2WOL_LOC_ID_OFFSET + 1603, LocationType.BONUS),
- LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604, LocationType.BONUS),
- LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605, LocationType.BONUS),
- LocationData("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700, LocationType.VICTORY,
- lambda state: state._sc2wol_has_train_killers(multiworld, player) and
- state._sc2wol_has_anti_air(multiworld, player)),
- LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701, LocationType.BONUS),
- LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702, LocationType.BONUS),
- LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703, LocationType.BONUS),
- LocationData("The Great Train Robbery", "The Great Train Robbery: Close Diamondback", SC2WOL_LOC_ID_OFFSET + 1704, LocationType.BONUS),
- LocationData("The Great Train Robbery", "The Great Train Robbery: Northwest Diamondback", SC2WOL_LOC_ID_OFFSET + 1705, LocationType.BONUS),
- LocationData("The Great Train Robbery", "The Great Train Robbery: North Diamondback", SC2WOL_LOC_ID_OFFSET + 1706, LocationType.BONUS),
- LocationData("The Great Train Robbery", "The Great Train Robbery: Northeast Diamondback", SC2WOL_LOC_ID_OFFSET + 1707, LocationType.BONUS),
- LocationData("The Great Train Robbery", "The Great Train Robbery: Southwest Diamondback", SC2WOL_LOC_ID_OFFSET + 1708, LocationType.BONUS),
- LocationData("The Great Train Robbery", "The Great Train Robbery: Southeast Diamondback", SC2WOL_LOC_ID_OFFSET + 1709, LocationType.BONUS),
- LocationData("The Great Train Robbery", "The Great Train Robbery: Kill Team", SC2WOL_LOC_ID_OFFSET + 1710, LocationType.CHALLENGE,
- lambda state: (logic_level > 0 or state._sc2wol_has_common_unit(multiworld, player)) and
- state._sc2wol_has_train_killers(multiworld, player) and
- state._sc2wol_has_anti_air(multiworld, player)),
- LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800, LocationType.VICTORY,
- lambda state: state._sc2wol_has_common_unit(multiworld, player) and
- (logic_level > 0 or state._sc2wol_has_anti_air)),
- LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802, LocationType.BONUS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803, LocationType.BONUS),
- LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804, LocationType.BONUS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Cutthroat", "Cutthroat: North Command Center", SC2WOL_LOC_ID_OFFSET + 1805, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Cutthroat", "Cutthroat: South Command Center", SC2WOL_LOC_ID_OFFSET + 1806, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Cutthroat", "Cutthroat: West Command Center", SC2WOL_LOC_ID_OFFSET + 1807, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_common_unit(multiworld, player)),
- LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900, LocationType.VICTORY,
- lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
- state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
- LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901, LocationType.MISSION_PROGRESS),
- LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902, LocationType.OPTIONAL_BOSS,
- lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
- state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
- LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903, LocationType.BONUS),
- LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904, LocationType.BONUS,
- lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
- state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
- LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, LocationType.BONUS,
- lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
- state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
- LocationData("Engine of Destruction", "Engine of Destruction: West Base", SC2WOL_LOC_ID_OFFSET + 1906, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
- state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
- LocationData("Engine of Destruction", "Engine of Destruction: Northwest Base", SC2WOL_LOC_ID_OFFSET + 1907, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
- state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
- LocationData("Engine of Destruction", "Engine of Destruction: Northeast Base", SC2WOL_LOC_ID_OFFSET + 1908, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
- state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
- LocationData("Engine of Destruction", "Engine of Destruction: Southeast Base", SC2WOL_LOC_ID_OFFSET + 1909, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_anti_air(multiworld, player) and
- state._sc2wol_has_common_unit(multiworld, player) or state.has('Wraith', player)),
- LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000, LocationType.VICTORY,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: Tower 2", SC2WOL_LOC_ID_OFFSET + 2002, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004, LocationType.BONUS),
- LocationData("Media Blitz", "Media Blitz: All Barracks", SC2WOL_LOC_ID_OFFSET + 2005, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: All Factories", SC2WOL_LOC_ID_OFFSET + 2006, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: All Starports", SC2WOL_LOC_ID_OFFSET + 2007, LocationType.MISSION_PROGRESS,
- lambda state: logic_level > 0 or state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Media Blitz", "Media Blitz: Odin Not Trashed", SC2WOL_LOC_ID_OFFSET + 2008, LocationType.CHALLENGE,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100, LocationType.VICTORY,
- lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101, LocationType.BONUS),
- LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102, LocationType.BONUS,
- lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103,LocationType.BONUS,
- lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104, LocationType.BONUS,
- lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk", SC2WOL_LOC_ID_OFFSET + 2105, LocationType.OPTIONAL_BOSS,
- lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Piercing the Shroud", "Piercing the Shroud: Fusion Reactor", SC2WOL_LOC_ID_OFFSET + 2106, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)),
- LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200, LocationType.VICTORY),
- LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201, LocationType.BONUS),
- LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202, LocationType.BONUS),
- LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203, LocationType.BONUS),
- LocationData("Whispers of Doom", "Whispers of Doom: First Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2204, LocationType.MISSION_PROGRESS),
- LocationData("Whispers of Doom", "Whispers of Doom: Second Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2205, LocationType.MISSION_PROGRESS),
- LocationData("Whispers of Doom", "Whispers of Doom: Third Prophecy Fragment", SC2WOL_LOC_ID_OFFSET + 2206, LocationType.MISSION_PROGRESS),
- LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300, LocationType.VICTORY,
- lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301, LocationType.BONUS,
- lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302, LocationType.BONUS,
- lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303, LocationType.BONUS,
- lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Northeast Base", SC2WOL_LOC_ID_OFFSET + 2304, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Southeast Base", SC2WOL_LOC_ID_OFFSET + 2305, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Maar", SC2WOL_LOC_ID_OFFSET + 2306, LocationType.MISSION_PROGRESS,
- lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Northwest Preserver", SC2WOL_LOC_ID_OFFSET + 2307, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: Southwest Preserver", SC2WOL_LOC_ID_OFFSET + 2308, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("A Sinister Turn", "A Sinister Turn: East Preserver", SC2WOL_LOC_ID_OFFSET + 2309, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400, LocationType.VICTORY,
- lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401, LocationType.BONUS),
- LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402, LocationType.BONUS,
- lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("Echoes of the Future", "Echoes of the Future: Base", SC2WOL_LOC_ID_OFFSET + 2403, LocationType.MISSION_PROGRESS),
- LocationData("Echoes of the Future", "Echoes of the Future: Southwest Tendril", SC2WOL_LOC_ID_OFFSET + 2404, LocationType.MISSION_PROGRESS),
- LocationData("Echoes of the Future", "Echoes of the Future: Southeast Tendril", SC2WOL_LOC_ID_OFFSET + 2405, LocationType.MISSION_PROGRESS,
- lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("Echoes of the Future", "Echoes of the Future: Northeast Tendril", SC2WOL_LOC_ID_OFFSET + 2406, LocationType.MISSION_PROGRESS,
- lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("Echoes of the Future", "Echoes of the Future: Northwest Tendril", SC2WOL_LOC_ID_OFFSET + 2407, LocationType.MISSION_PROGRESS,
- lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500, LocationType.VICTORY),
- LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501, LocationType.BONUS,
- lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)),
- LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502, LocationType.CHALLENGE,
- lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("In Utter Darkness", "In Utter Darkness: Urun", SC2WOL_LOC_ID_OFFSET + 2503, LocationType.MISSION_PROGRESS),
- LocationData("In Utter Darkness", "In Utter Darkness: Mohandar", SC2WOL_LOC_ID_OFFSET + 2504, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("In Utter Darkness", "In Utter Darkness: Selendis", SC2WOL_LOC_ID_OFFSET + 2505, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("In Utter Darkness", "In Utter Darkness: Artanis", SC2WOL_LOC_ID_OFFSET + 2506, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_protoss_common_units(multiworld, player)),
- LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600, LocationType.VICTORY,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
- state._sc2wol_defense_rating(multiworld, player, True) > 6),
- LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
- state._sc2wol_defense_rating(multiworld, player, True) > 6),
- LocationData("Gates of Hell", "Gates of Hell: 2 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2602, LocationType.BONUS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
- state._sc2wol_defense_rating(multiworld, player, True) > 6),
- LocationData("Gates of Hell", "Gates of Hell: 4 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2603, LocationType.BONUS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
- state._sc2wol_defense_rating(multiworld, player, True) > 6),
- LocationData("Gates of Hell", "Gates of Hell: 6 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2604, LocationType.BONUS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
- state._sc2wol_defense_rating(multiworld, player, True) > 6),
- LocationData("Gates of Hell", "Gates of Hell: 8 Drop Pods", SC2WOL_LOC_ID_OFFSET + 2605, LocationType.BONUS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player) and
- state._sc2wol_defense_rating(multiworld, player, True) > 6),
- LocationData("Belly of the Beast", "Belly of the Beast: Victory", SC2WOL_LOC_ID_OFFSET + 2700, LocationType.VICTORY),
- LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701, LocationType.MISSION_PROGRESS),
- LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702, LocationType.MISSION_PROGRESS),
- LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703, LocationType.MISSION_PROGRESS),
- LocationData("Belly of the Beast", "Belly of the Beast: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 2704, LocationType.BONUS),
- LocationData("Belly of the Beast", "Belly of the Beast: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 2705, LocationType.BONUS),
- LocationData("Belly of the Beast", "Belly of the Beast: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 2706, LocationType.BONUS),
- LocationData("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800, LocationType.VICTORY,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Northwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2802, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Southeast Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2803, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Southwest Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2804, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, LocationType.OPTIONAL_BOSS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: East Hatchery", SC2WOL_LOC_ID_OFFSET + 2806, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: North Hatchery", SC2WOL_LOC_ID_OFFSET + 2807, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("Shatter the Sky", "Shatter the Sky: Mid Hatchery", SC2WOL_LOC_ID_OFFSET + 2808, LocationType.MISSION_PROGRESS,
- lambda state: state._sc2wol_has_competent_comp(multiworld, player)),
- LocationData("All-In", "All-In: Victory", None, LocationType.VICTORY,
- lambda state: state._sc2wol_final_mission_requirements(multiworld, player))
- ]
-
- beat_events = []
-
- for i, location_data in enumerate(location_table):
- # Removing all item-based logic on No Logic
- if logic_level == 2:
- location_data = location_data._replace(rule=Location.access_rule)
- location_table[i] = location_data
- # Generating Beat event locations
- if location_data.name.endswith((": Victory", ": Defeat")):
- beat_events.append(
- location_data._replace(name="Beat " + location_data.name.rsplit(": ", 1)[0], code=None)
- )
- return tuple(location_table + beat_events)
diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py
deleted file mode 100644
index 112302beb207..000000000000
--- a/worlds/sc2wol/LogicMixin.py
+++ /dev/null
@@ -1,148 +0,0 @@
-from BaseClasses import MultiWorld
-from worlds.AutoWorld import LogicMixin
-from .Options import get_option_value
-from .Items import get_basic_units, defense_ratings, zerg_defense_ratings
-
-
-class SC2WoLLogic(LogicMixin):
- def _sc2wol_has_common_unit(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has_any(get_basic_units(multiworld, player), player)
-
- def _sc2wol_has_air(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has_any({'Viking', 'Wraith', 'Banshee', 'Battlecruiser'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0 \
- and self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(multiworld, player)
-
- def _sc2wol_has_air_anti_air(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has('Viking', player) \
- or self.has_all({'Wraith', 'Advanced Laser Technology (Wraith)'}, player) \
- or self.has_all({'Battlecruiser', 'ATX Laser Battery (Battlecruiser)'}, player) \
- or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Wraith', 'Valkyrie', 'Battlecruiser'}, player)
-
- def _sc2wol_has_competent_ground_to_air(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has('Goliath', player) \
- or self.has('Marine', player) and self.has_any({'Medic', 'Medivac'}, player) \
- or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Cyclone', player)
-
- def _sc2wol_has_competent_anti_air(self, multiworld: MultiWorld, player: int) -> bool:
- return self._sc2wol_has_competent_ground_to_air(multiworld, player) \
- or self._sc2wol_has_air_anti_air(multiworld, player)
-
- def _sc2wol_welcome_to_the_jungle_requirement(self, multiworld: MultiWorld, player: int) -> bool:
- return (
- self._sc2wol_has_common_unit(multiworld, player)
- and self._sc2wol_has_competent_ground_to_air(multiworld, player)
- ) or (
- get_option_value(multiworld, player, 'required_tactics') > 0
- and self.has_any({'Marine', 'Vulture'}, player)
- and self._sc2wol_has_air_anti_air(multiworld, player)
- )
-
- def _sc2wol_has_anti_air(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Marine', 'Wraith', 'Valkyrie', 'Cyclone'}, player) \
- or self._sc2wol_has_competent_anti_air(multiworld, player) \
- or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre', 'Widow Mine', 'Liberator'}, player)
-
- def _sc2wol_defense_rating(self, multiworld: MultiWorld, player: int, zerg_enemy: bool, air_enemy: bool = True) -> bool:
- defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player)))
- if self.has_any({'Marine', 'Marauder'}, player) and self.has('Bunker', player):
- defense_score += 3
- if self.has_all({'Siege Tank', 'Maelstrom Rounds (Siege Tank)'}, player):
- defense_score += 2
- if self.has_all({'Siege Tank', 'Graduating Range (Siege Tank)'}, player):
- defense_score += 1
- if self.has_all({'Widow Mine', 'Concealment (Widow Mine)'}, player):
- defense_score += 1
- if zerg_enemy:
- defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if self.has(item, player)))
- if self.has('Firebat', player) and self.has('Bunker', player):
- defense_score += 2
- if not air_enemy and self.has('Missile Turret', player):
- defense_score -= defense_ratings['Missile Turret']
- # Advanced Tactics bumps defense rating requirements down by 2
- if get_option_value(multiworld, player, 'required_tactics') > 0:
- defense_score += 2
- return defense_score
-
- def _sc2wol_has_competent_comp(self, multiworld: MultiWorld, player: int) -> bool:
- return \
- (
- (
- self.has_any({'Marine', 'Marauder'}, player) and self.has_any({'Medivac', 'Medic'}, player)
- or self.has_any({'Thor', 'Banshee', 'Siege Tank'}, player)
- or self.has_all({'Liberator', 'Raid Artillery (Liberator)'}, player)
- ) and self._sc2wol_has_competent_anti_air(multiworld, player)
- ) \
- or \
- (
- self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(multiworld, player)
- )
-
- def _sc2wol_has_train_killers(self, multiworld: MultiWorld, player: int) -> bool:
- return (
- self.has_any({'Siege Tank', 'Diamondback', 'Marauder', 'Cyclone'}, player)
- or get_option_value(multiworld, player, 'required_tactics') > 0
- and (
- self.has_all({'Reaper', "G-4 Clusterbomb"}, player)
- or self.has_all({'Spectre', 'Psionic Lash'}, player)
- or self.has_any({'Vulture', 'Liberator'}, player)
- )
- )
-
- def _sc2wol_able_to_rescue(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0
-
- def _sc2wol_has_protoss_common_units(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) \
- or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('High Templar', player)
-
- def _sc2wol_has_protoss_medium_units(self, multiworld: MultiWorld, player: int) -> bool:
- return self._sc2wol_has_protoss_common_units(multiworld, player) and \
- self.has_any({'Stalker', 'Void Ray', 'Carrier'}, player) \
- or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Dark Templar', player)
-
- def _sc2wol_beats_protoss_deathball(self, multiworld: MultiWorld, player: int) -> bool:
- return (self.has_any({'Banshee', 'Battlecruiser'}, player) or
- self.has_all({'Liberator', 'Raid Artillery (Liberator)'}, player)) \
- and self._sc2wol_has_competent_anti_air(multiworld, player) or \
- self._sc2wol_has_competent_comp(multiworld, player) and self._sc2wol_has_air_anti_air(multiworld, player)
-
- def _sc2wol_has_mm_upgrade(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has_any({"Combat Shield (Marine)", "Stabilizer Medpacks (Medic)"}, player)
-
- def _sc2wol_survives_rip_field(self, multiworld: MultiWorld, player: int) -> bool:
- return self.has("Battlecruiser", player) or \
- self._sc2wol_has_air(multiworld, player) and \
- self._sc2wol_has_competent_anti_air(multiworld, player) and \
- self.has("Science Vessel", player)
-
- def _sc2wol_has_nukes(self, multiworld: MultiWorld, player: int) -> bool:
- return get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player)
-
- def _sc2wol_can_respond_to_colony_infestations(self, multiworld: MultiWorld, player: int) -> bool:
- return self._sc2wol_has_common_unit(multiworld, player) \
- and self._sc2wol_has_competent_anti_air(multiworld, player) \
- and \
- (
- self._sc2wol_has_air_anti_air(multiworld, player) or
- self.has_any({'Battlecruiser', 'Valkyrie'}), player
- ) \
- and \
- self._sc2wol_defense_rating(multiworld, player, True) >= 3
-
- def _sc2wol_final_mission_requirements(self, multiworld: MultiWorld, player: int):
- beats_kerrigan = self.has_any({'Marine', 'Banshee', 'Ghost'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0
- if get_option_value(multiworld, player, 'all_in_map') == 0:
- # Ground
- defense_rating = self._sc2wol_defense_rating(multiworld, player, True, False)
- if self.has_any({'Battlecruiser', 'Banshee'}, player):
- defense_rating += 3
- return defense_rating >= 12 and beats_kerrigan
- else:
- # Air
- defense_rating = self._sc2wol_defense_rating(multiworld, player, True, True)
- return defense_rating >= 8 and beats_kerrigan \
- and self.has_any({'Viking', 'Battlecruiser', 'Valkyrie'}, player) \
- and self.has_any({'Hive Mind Emulator', 'Psi Disruptor', 'Missile Turret'}, player)
-
- def _sc2wol_cleared_missions(self, multiworld: MultiWorld, player: int, mission_count: int) -> bool:
- return self.has_group("Missions", player, mission_count)
diff --git a/worlds/sc2wol/MissionTables.py b/worlds/sc2wol/MissionTables.py
deleted file mode 100644
index 298cd7a978a6..000000000000
--- a/worlds/sc2wol/MissionTables.py
+++ /dev/null
@@ -1,230 +0,0 @@
-from typing import NamedTuple, Dict, List
-from enum import IntEnum
-
-no_build_regions_list = ["Liberation Day", "Breakout", "Ghost of a Chance", "Piercing the Shroud", "Whispers of Doom",
- "Belly of the Beast"]
-easy_regions_list = ["The Outlaws", "Zero Hour", "Evacuation", "Outbreak", "Smash and Grab", "Devil's Playground"]
-medium_regions_list = ["Safe Haven", "Haven's Fall", "The Dig", "The Moebius Factor", "Supernova",
- "Welcome to the Jungle", "The Great Train Robbery", "Cutthroat", "Media Blitz",
- "A Sinister Turn", "Echoes of the Future"]
-hard_regions_list = ["Maw of the Void", "Engine of Destruction", "In Utter Darkness", "Gates of Hell",
- "Shatter the Sky"]
-
-
-class MissionPools(IntEnum):
- STARTER = 0
- EASY = 1
- MEDIUM = 2
- HARD = 3
- FINAL = 4
-
-
-class MissionInfo(NamedTuple):
- id: int
- required_world: List[int]
- category: str
- number: int = 0 # number of worlds need beaten
- completion_critical: bool = False # missions needed to beat game
- or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
-
-
-class FillMission(NamedTuple):
- type: int
- connect_to: List[int] # -1 connects to Menu
- category: str
- number: int = 0 # number of worlds need beaten
- completion_critical: bool = False # missions needed to beat game
- or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed
- removal_priority: int = 0 # how many missions missing from the pool required to remove this mission
-
-
-vanilla_shuffle_order = [
- FillMission(MissionPools.STARTER, [-1], "Mar Sara", completion_critical=True),
- FillMission(MissionPools.EASY, [0], "Mar Sara", completion_critical=True),
- FillMission(MissionPools.EASY, [1], "Mar Sara", completion_critical=True),
- FillMission(MissionPools.EASY, [2], "Colonist"),
- FillMission(MissionPools.MEDIUM, [3], "Colonist"),
- FillMission(MissionPools.HARD, [4], "Colonist", number=7),
- FillMission(MissionPools.HARD, [4], "Colonist", number=7, removal_priority=1),
- FillMission(MissionPools.EASY, [2], "Artifact", completion_critical=True),
- FillMission(MissionPools.MEDIUM, [7], "Artifact", number=8, completion_critical=True),
- FillMission(MissionPools.HARD, [8], "Artifact", number=11, completion_critical=True),
- FillMission(MissionPools.HARD, [9], "Artifact", number=14, completion_critical=True, removal_priority=11),
- FillMission(MissionPools.HARD, [10], "Artifact", completion_critical=True, removal_priority=10),
- FillMission(MissionPools.MEDIUM, [2], "Covert", number=4),
- FillMission(MissionPools.MEDIUM, [12], "Covert"),
- FillMission(MissionPools.HARD, [13], "Covert", number=8, removal_priority=3),
- FillMission(MissionPools.HARD, [13], "Covert", number=8, removal_priority=2),
- FillMission(MissionPools.MEDIUM, [2], "Rebellion", number=6),
- FillMission(MissionPools.HARD, [16], "Rebellion"),
- FillMission(MissionPools.HARD, [17], "Rebellion"),
- FillMission(MissionPools.HARD, [18], "Rebellion", removal_priority=12),
- FillMission(MissionPools.HARD, [19], "Rebellion", removal_priority=5),
- FillMission(MissionPools.MEDIUM, [8], "Prophecy", removal_priority=9),
- FillMission(MissionPools.HARD, [21], "Prophecy", removal_priority=8),
- FillMission(MissionPools.HARD, [22], "Prophecy", removal_priority=7),
- FillMission(MissionPools.HARD, [23], "Prophecy", removal_priority=6),
- FillMission(MissionPools.HARD, [11], "Char", completion_critical=True),
- FillMission(MissionPools.HARD, [25], "Char", completion_critical=True, removal_priority=4),
- FillMission(MissionPools.HARD, [25], "Char", completion_critical=True),
- FillMission(MissionPools.FINAL, [26, 27], "Char", completion_critical=True, or_requirements=True)
-]
-
-mini_campaign_order = [
- FillMission(MissionPools.STARTER, [-1], "Mar Sara", completion_critical=True),
- FillMission(MissionPools.EASY, [0], "Colonist"),
- FillMission(MissionPools.MEDIUM, [1], "Colonist"),
- FillMission(MissionPools.EASY, [0], "Artifact", completion_critical=True),
- FillMission(MissionPools.MEDIUM, [3], "Artifact", number=4, completion_critical=True),
- FillMission(MissionPools.HARD, [4], "Artifact", number=8, completion_critical=True),
- FillMission(MissionPools.MEDIUM, [0], "Covert", number=2),
- FillMission(MissionPools.HARD, [6], "Covert"),
- FillMission(MissionPools.MEDIUM, [0], "Rebellion", number=3),
- FillMission(MissionPools.HARD, [8], "Rebellion"),
- FillMission(MissionPools.MEDIUM, [4], "Prophecy"),
- FillMission(MissionPools.HARD, [10], "Prophecy"),
- FillMission(MissionPools.HARD, [5], "Char", completion_critical=True),
- FillMission(MissionPools.HARD, [5], "Char", completion_critical=True),
- FillMission(MissionPools.FINAL, [12, 13], "Char", completion_critical=True, or_requirements=True)
-]
-
-gauntlet_order = [
- FillMission(MissionPools.STARTER, [-1], "I", completion_critical=True),
- FillMission(MissionPools.EASY, [0], "II", completion_critical=True),
- FillMission(MissionPools.EASY, [1], "III", completion_critical=True),
- FillMission(MissionPools.MEDIUM, [2], "IV", completion_critical=True),
- FillMission(MissionPools.MEDIUM, [3], "V", completion_critical=True),
- FillMission(MissionPools.HARD, [4], "VI", completion_critical=True),
- FillMission(MissionPools.FINAL, [5], "Final", completion_critical=True)
-]
-
-mini_gauntlet_order = [
- FillMission(MissionPools.STARTER, [-1], "I", completion_critical=True),
- FillMission(MissionPools.EASY, [0], "II", completion_critical=True),
- FillMission(MissionPools.MEDIUM, [1], "III", completion_critical=True),
- FillMission(MissionPools.FINAL, [2], "Final", completion_critical=True)
-]
-
-grid_order = [
- FillMission(MissionPools.STARTER, [-1], "_1"),
- FillMission(MissionPools.EASY, [0], "_1"),
- FillMission(MissionPools.MEDIUM, [1, 6, 3], "_1", or_requirements=True),
- FillMission(MissionPools.HARD, [2, 7], "_1", or_requirements=True),
- FillMission(MissionPools.EASY, [0], "_2"),
- FillMission(MissionPools.MEDIUM, [1, 4], "_2", or_requirements=True),
- FillMission(MissionPools.HARD, [2, 5, 10, 7], "_2", or_requirements=True),
- FillMission(MissionPools.HARD, [3, 6, 11], "_2", or_requirements=True),
- FillMission(MissionPools.MEDIUM, [4, 9, 12], "_3", or_requirements=True),
- FillMission(MissionPools.HARD, [5, 8, 10, 13], "_3", or_requirements=True),
- FillMission(MissionPools.HARD, [6, 9, 11, 14], "_3", or_requirements=True),
- FillMission(MissionPools.HARD, [7, 10], "_3", or_requirements=True),
- FillMission(MissionPools.HARD, [8, 13], "_4", or_requirements=True),
- FillMission(MissionPools.HARD, [9, 12, 14], "_4", or_requirements=True),
- FillMission(MissionPools.HARD, [10, 13], "_4", or_requirements=True),
- FillMission(MissionPools.FINAL, [11, 14], "_4", or_requirements=True)
-]
-
-mini_grid_order = [
- FillMission(MissionPools.STARTER, [-1], "_1"),
- FillMission(MissionPools.EASY, [0], "_1"),
- FillMission(MissionPools.MEDIUM, [1, 5], "_1", or_requirements=True),
- FillMission(MissionPools.EASY, [0], "_2"),
- FillMission(MissionPools.MEDIUM, [1, 3], "_2", or_requirements=True),
- FillMission(MissionPools.HARD, [2, 4], "_2", or_requirements=True),
- FillMission(MissionPools.MEDIUM, [3, 7], "_3", or_requirements=True),
- FillMission(MissionPools.HARD, [4, 6], "_3", or_requirements=True),
- FillMission(MissionPools.FINAL, [5, 7], "_3", or_requirements=True)
-]
-
-tiny_grid_order = [
- FillMission(MissionPools.STARTER, [-1], "_1"),
- FillMission(MissionPools.MEDIUM, [0], "_1"),
- FillMission(MissionPools.EASY, [0], "_2"),
- FillMission(MissionPools.FINAL, [1, 2], "_2", or_requirements=True),
-]
-
-blitz_order = [
- FillMission(MissionPools.STARTER, [-1], "I"),
- FillMission(MissionPools.EASY, [-1], "I"),
- FillMission(MissionPools.MEDIUM, [0, 1], "II", number=1, or_requirements=True),
- FillMission(MissionPools.MEDIUM, [0, 1], "II", number=1, or_requirements=True),
- FillMission(MissionPools.MEDIUM, [0, 1], "III", number=2, or_requirements=True),
- FillMission(MissionPools.MEDIUM, [0, 1], "III", number=2, or_requirements=True),
- FillMission(MissionPools.HARD, [0, 1], "IV", number=3, or_requirements=True),
- FillMission(MissionPools.HARD, [0, 1], "IV", number=3, or_requirements=True),
- FillMission(MissionPools.HARD, [0, 1], "V", number=4, or_requirements=True),
- FillMission(MissionPools.HARD, [0, 1], "V", number=4, or_requirements=True),
- FillMission(MissionPools.HARD, [0, 1], "Final", number=5, or_requirements=True),
- FillMission(MissionPools.FINAL, [0, 1], "Final", number=5, or_requirements=True)
-]
-
-mission_orders = [
- vanilla_shuffle_order,
- vanilla_shuffle_order,
- mini_campaign_order,
- grid_order,
- mini_grid_order,
- blitz_order,
- gauntlet_order,
- mini_gauntlet_order,
- tiny_grid_order
-]
-
-
-vanilla_mission_req_table = {
- "Liberation Day": MissionInfo(1, [], "Mar Sara", completion_critical=True),
- "The Outlaws": MissionInfo(2, [1], "Mar Sara", completion_critical=True),
- "Zero Hour": MissionInfo(3, [2], "Mar Sara", completion_critical=True),
- "Evacuation": MissionInfo(4, [3], "Colonist"),
- "Outbreak": MissionInfo(5, [4], "Colonist"),
- "Safe Haven": MissionInfo(6, [5], "Colonist", number=7),
- "Haven's Fall": MissionInfo(7, [5], "Colonist", number=7),
- "Smash and Grab": MissionInfo(8, [3], "Artifact", completion_critical=True),
- "The Dig": MissionInfo(9, [8], "Artifact", number=8, completion_critical=True),
- "The Moebius Factor": MissionInfo(10, [9], "Artifact", number=11, completion_critical=True),
- "Supernova": MissionInfo(11, [10], "Artifact", number=14, completion_critical=True),
- "Maw of the Void": MissionInfo(12, [11], "Artifact", completion_critical=True),
- "Devil's Playground": MissionInfo(13, [3], "Covert", number=4),
- "Welcome to the Jungle": MissionInfo(14, [13], "Covert"),
- "Breakout": MissionInfo(15, [14], "Covert", number=8),
- "Ghost of a Chance": MissionInfo(16, [14], "Covert", number=8),
- "The Great Train Robbery": MissionInfo(17, [3], "Rebellion", number=6),
- "Cutthroat": MissionInfo(18, [17], "Rebellion"),
- "Engine of Destruction": MissionInfo(19, [18], "Rebellion"),
- "Media Blitz": MissionInfo(20, [19], "Rebellion"),
- "Piercing the Shroud": MissionInfo(21, [20], "Rebellion"),
- "Whispers of Doom": MissionInfo(22, [9], "Prophecy"),
- "A Sinister Turn": MissionInfo(23, [22], "Prophecy"),
- "Echoes of the Future": MissionInfo(24, [23], "Prophecy"),
- "In Utter Darkness": MissionInfo(25, [24], "Prophecy"),
- "Gates of Hell": MissionInfo(26, [12], "Char", completion_critical=True),
- "Belly of the Beast": MissionInfo(27, [26], "Char", completion_critical=True),
- "Shatter the Sky": MissionInfo(28, [26], "Char", completion_critical=True),
- "All-In": MissionInfo(29, [27, 28], "Char", completion_critical=True, or_requirements=True)
-}
-
-lookup_id_to_mission: Dict[int, str] = {
- data.id: mission_name for mission_name, data in vanilla_mission_req_table.items() if data.id}
-
-starting_mission_locations = {
- "Liberation Day": "Liberation Day: Victory",
- "Breakout": "Breakout: Victory",
- "Ghost of a Chance": "Ghost of a Chance: Victory",
- "Piercing the Shroud": "Piercing the Shroud: Victory",
- "Whispers of Doom": "Whispers of Doom: Victory",
- "Belly of the Beast": "Belly of the Beast: Victory",
- "Zero Hour": "Zero Hour: First Group Rescued",
- "Evacuation": "Evacuation: Reach Hanson",
- "Devil's Playground": "Devil's Playground: Tosh's Miners",
- "Smash and Grab": "Smash and Grab: First Relic",
- "The Great Train Robbery": "The Great Train Robbery: North Defiler"
-}
-
-
-alt_final_mission_locations = {
- "Maw of the Void": "Maw of the Void: Victory",
- "Engine of Destruction": "Engine of Destruction: Victory",
- "Supernova": "Supernova: Victory",
- "Gates of Hell": "Gates of Hell: Victory",
- "Shatter the Sky": "Shatter the Sky: Victory"
-}
\ No newline at end of file
diff --git a/worlds/sc2wol/Options.py b/worlds/sc2wol/Options.py
deleted file mode 100644
index 0702e431a4de..000000000000
--- a/worlds/sc2wol/Options.py
+++ /dev/null
@@ -1,362 +0,0 @@
-from typing import Dict, FrozenSet, Union
-from BaseClasses import MultiWorld
-from Options import Choice, Option, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range
-from .MissionTables import vanilla_mission_req_table
-
-ORDER_VANILLA = 0
-ORDER_VANILLA_SHUFFLED = 1
-
-class GameDifficulty(Choice):
- """
- The difficulty of the campaign, affects enemy AI, starting units, and game speed.
-
- For those unfamiliar with the Archipelago randomizer, the recommended settings are one difficulty level
- lower than the vanilla game
- """
- display_name = "Game Difficulty"
- option_casual = 0
- option_normal = 1
- option_hard = 2
- option_brutal = 3
- default = 1
-
-class GameSpeed(Choice):
- """Optional setting to override difficulty-based game speed."""
- display_name = "Game Speed"
- option_default = 0
- option_slower = 1
- option_slow = 2
- option_normal = 3
- option_fast = 4
- option_faster = 5
- default = option_default
-
-class FinalMap(Choice):
- """
- Determines if the final map and goal of the campaign.
- All in: You need to beat All-in map
- Random Hard: A random hard mission is selected as a goal.
- Beat this mission in order to complete the game.
- All-in map won't be in the campaign
-
- Vanilla mission order always ends with All in mission!
-
- This option is short-lived. It may be changed in the future
- """
- display_name = "Final Map"
- option_all_in = 0
- option_random_hard = 1
-
-class AllInMap(Choice):
- """Determines what version of All-In (final map) that will be generated for the campaign."""
- display_name = "All In Map"
- option_ground = 0
- option_air = 1
-
-
-class MissionOrder(Choice):
- """
- Determines the order the missions are played in. The last three mission orders end in a random mission.
- Vanilla (29): Keeps the standard mission order and branching from the WoL Campaign.
- Vanilla Shuffled (29): Keeps same branching paths from the WoL Campaign but randomizes the order of missions within.
- Mini Campaign (15): Shorter version of the campaign with randomized missions and optional branches.
- Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards bottom-right mission to win.
- Mini Grid (9): A 3x3 version of Grid. Complete the bottom-right mission to win.
- Blitz (12): 12 random missions that open up very quickly. Complete the bottom-right mission to win.
- Gauntlet (7): Linear series of 7 random missions to complete the campaign.
- Mini Gauntlet (4): Linear series of 4 random missions to complete the campaign.
- Tiny Grid (4): A 2x2 version of Grid. Complete the bottom-right mission to win.
- """
- display_name = "Mission Order"
- option_vanilla = 0
- option_vanilla_shuffled = 1
- option_mini_campaign = 2
- option_grid = 3
- option_mini_grid = 4
- option_blitz = 5
- option_gauntlet = 6
- option_mini_gauntlet = 7
- option_tiny_grid = 8
-
-
-class PlayerColor(Choice):
- """Determines in-game team color."""
- display_name = "Player Color"
- option_white = 0
- option_red = 1
- option_blue = 2
- option_teal = 3
- option_purple = 4
- option_yellow = 5
- option_orange = 6
- option_green = 7
- option_light_pink = 8
- option_violet = 9
- option_light_grey = 10
- option_dark_green = 11
- option_brown = 12
- option_light_green = 13
- option_dark_grey = 14
- option_pink = 15
- option_rainbow = 16
- option_default = 17
- default = option_default
-
-
-class ShuffleProtoss(DefaultOnToggle):
- """Determines if the 3 protoss missions are included in the shuffle if Vanilla mission order is not enabled.
- If turned off, the 3 protoss missions will not appear and Protoss units are removed from the pool."""
- display_name = "Shuffle Protoss Missions"
-
-
-class ShuffleNoBuild(DefaultOnToggle):
- """Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled.
- If turned off, the 5 no-build missions will not appear."""
- display_name = "Shuffle No-Build Missions"
-
-
-class EarlyUnit(DefaultOnToggle):
- """
- Guarantees that the first mission will contain a unit.
-
- Each mission available to be the first mission has a pre-defined location where the unit should spawn.
- This location gets overriden over any exclusion. It's guaranteed to be reachable with an empty inventory.
- """
- display_name = "Early Unit"
-
-
-class RequiredTactics(Choice):
- """Determines the maximum tactical difficulty of the seed (separate from mission difficulty). Higher settings
- increase randomness.
-
- Standard: All missions can be completed with good micro and macro.
- Advanced: Completing missions may require relying on starting units and micro-heavy units.
- No Logic: Units and upgrades may be placed anywhere. LIKELY TO RENDER THE RUN IMPOSSIBLE ON HARDER DIFFICULTIES!"""
- display_name = "Required Tactics"
- option_standard = 0
- option_advanced = 1
- option_no_logic = 2
-
-
-class UnitsAlwaysHaveUpgrades(DefaultOnToggle):
- """
- If turned on, all upgrades will be present for each unit and structure in the seed.
- This usually results in fewer units.
-
- See also: Max Number of Upgrades
- """
- display_name = "Units Always Have Upgrades"
-
-
-class GenericUpgradeMissions(Range):
- """Determines the percentage of missions in the mission order that must be completed before
- level 1 of all weapon and armor upgrades is unlocked. Level 2 upgrades require double the amount of missions,
- and level 3 requires triple the amount. The required amounts are always rounded down.
- If set to 0, upgrades are instead added to the item pool and must be found to be used."""
- display_name = "Generic Upgrade Missions"
- range_start = 0
- range_end = 100
- default = 0
-
-
-class GenericUpgradeResearch(Choice):
- """Determines how weapon and armor upgrades affect missions once unlocked.
-
- Vanilla: Upgrades must be researched as normal.
- Auto In No-Build: In No-Build missions, upgrades are automatically researched.
- In all other missions, upgrades must be researched as normal.
- Auto In Build: In No-Build missions, upgrades are unavailable as normal.
- In all other missions, upgrades are automatically researched.
- Always Auto: Upgrades are automatically researched in all missions."""
- display_name = "Generic Upgrade Research"
- option_vanilla = 0
- option_auto_in_no_build = 1
- option_auto_in_build = 2
- option_always_auto = 3
-
-
-class GenericUpgradeItems(Choice):
- """Determines how weapon and armor upgrades are split into items. All options produce 3 levels of each item.
- Does nothing if upgrades are unlocked by completed mission counts.
-
- Individual Items: All weapon and armor upgrades are each an item,
- resulting in 18 total upgrade items.
- Bundle Weapon And Armor: All types of weapon upgrades are one item,
- and all types of armor upgrades are one item,
- resulting in 6 total items.
- Bundle Unit Class: Weapon and armor upgrades are merged,
- but Infantry, Vehicle, and Starship upgrades are bundled separately,
- resulting in 9 total items.
- Bundle All: All weapon and armor upgrades are one item,
- resulting in 3 total items."""
- display_name = "Generic Upgrade Items"
- option_individual_items = 0
- option_bundle_weapon_and_armor = 1
- option_bundle_unit_class = 2
- option_bundle_all = 3
-
-
-class NovaCovertOpsItems(Toggle):
- """If turned on, the equipment upgrades from Nova Covert Ops may be present in the world."""
- display_name = "Nova Covert Ops Items"
- default = Toggle.option_true
-
-
-class BroodWarItems(Toggle):
- """If turned on, returning items from StarCraft: Brood War may appear in the world."""
- display_name = "Brood War Items"
- default = Toggle.option_true
-
-
-class ExtendedItems(Toggle):
- """If turned on, original items that did not appear in Campaign mode may appear in the world."""
- display_name = "Extended Items"
- default = Toggle.option_true
-
-
-class MaxNumberOfUpgrades(Range):
- """
- Set a maximum to the number of upgrades a unit/structure can have. -1 is used to define unlimited.
- Note that most unit have 4 or 6 upgrades.
-
- If used with Units Always Have Upgrades, each unit has this given amount of upgrades (if there enough upgrades exist)
-
- See also: Units Always Have Upgrades
- """
- display_name = "Maximum number of upgrades per unit/structure"
- range_start = -1
- # Do not know the maximum, but it is less than 123!
- range_end = 123
- default = -1
-
-
-class LockedItems(ItemSet):
- """Guarantees that these items will be unlockable"""
- display_name = "Locked Items"
-
-
-class ExcludedItems(ItemSet):
- """Guarantees that these items will not be unlockable"""
- display_name = "Excluded Items"
-
-
-class ExcludedMissions(OptionSet):
- """Guarantees that these missions will not appear in the campaign
- Doesn't apply to vanilla mission order.
- It may be impossible to build a valid campaign if too many missions are excluded."""
- display_name = "Excluded Missions"
- valid_keys = {mission_name for mission_name in vanilla_mission_req_table.keys() if mission_name != 'All-In'}
-
-
-class LocationInclusion(Choice):
- option_enabled = 0
- option_trash = 1
- option_nothing = 2
-
-
-class MissionProgressLocations(LocationInclusion):
- """
- Enables or disables item rewards for progressing (not finishing) a mission.
- Progressing a mission is usually a task of completing or progressing into a main objective.
- Clearing an expansion base also counts here.
-
- Enabled: All locations fitting into this do their normal rewards
- Trash: Forces a trash item in
- Nothing: No rewards for this type of tasks, effectively disabling such locations
-
- Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
- Warning: The generation may fail if too many locations are excluded by this way.
- See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
- """
- display_name = "Mission Progress Locations"
-
-
-class BonusLocations(LocationInclusion):
- """
- Enables or disables item rewards for completing bonus tasks.
- Bonus tasks are those giving you a campaign-wide or mission-wide bonus in vanilla game:
- Research, credits, bonus units or resources, etc.
-
- Enabled: All locations fitting into this do their normal rewards
- Trash: Forces a trash item in
- Nothing: No rewards for this type of tasks, effectively disabling such locations
-
- Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
- Warning: The generation may fail if too many locations are excluded by this way.
- See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
- """
- display_name = "Bonus Locations"
-
-
-class ChallengeLocations(LocationInclusion):
- """
- Enables or disables item rewards for completing challenge tasks.
- Challenges are tasks that have usually higher requirements to be completed
- than to complete the mission they're in successfully.
- You might be required to visit the same mission later when getting stronger in order to finish these tasks.
-
- Enabled: All locations fitting into this do their normal rewards
- Trash: Forces a trash item in
- Nothing: No rewards for this type of tasks, effectively disabling such locations
-
- Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
- Warning: The generation may fail if too many locations are excluded by this way.
- See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
- """
- display_name = "Challenge Locations"
-
-
-class OptionalBossLocations(LocationInclusion):
- """
- Enables or disables item rewards for defeating optional bosses.
- An optional boss is any boss that's not required to kill in order to finish the mission successfully.
- All Brutalisks, Loki, etc. belongs here.
-
- Enabled: All locations fitting into this do their normal rewards
- Trash: Forces a trash item in
- Nothing: No rewards for this type of tasks, effectively disabling such locations
-
- Note: Individual locations subject to plando are always enabled, so the plando can be placed properly.
- Warning: The generation may fail if too many locations are excluded by this way.
- See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando)
- """
- display_name = "Challenge Locations"
-
-
-# noinspection PyTypeChecker
-sc2wol_options: Dict[str, Option] = {
- "game_difficulty": GameDifficulty,
- "game_speed": GameSpeed,
- "all_in_map": AllInMap,
- "final_map": FinalMap,
- "mission_order": MissionOrder,
- "player_color": PlayerColor,
- "shuffle_protoss": ShuffleProtoss,
- "shuffle_no_build": ShuffleNoBuild,
- "early_unit": EarlyUnit,
- "required_tactics": RequiredTactics,
- "units_always_have_upgrades": UnitsAlwaysHaveUpgrades,
- "max_number_of_upgrades": MaxNumberOfUpgrades,
- "generic_upgrade_missions": GenericUpgradeMissions,
- "generic_upgrade_research": GenericUpgradeResearch,
- "generic_upgrade_items": GenericUpgradeItems,
- "locked_items": LockedItems,
- "excluded_items": ExcludedItems,
- "excluded_missions": ExcludedMissions,
- "nco_items": NovaCovertOpsItems,
- "bw_items": BroodWarItems,
- "ext_items": ExtendedItems,
- "mission_progress_locations": MissionProgressLocations,
- "bonus_locations": BonusLocations,
- "challenge_locations": ChallengeLocations,
- "optional_boss_locations": OptionalBossLocations
-}
-
-
-def get_option_value(multiworld: MultiWorld, player: int, name: str) -> Union[int, FrozenSet]:
- if multiworld is None:
- return sc2wol_options[name].default
-
- player_option = getattr(multiworld, name)[player]
-
- return player_option.value
diff --git a/worlds/sc2wol/PoolFilter.py b/worlds/sc2wol/PoolFilter.py
deleted file mode 100644
index 4a19e2dbb305..000000000000
--- a/worlds/sc2wol/PoolFilter.py
+++ /dev/null
@@ -1,356 +0,0 @@
-from typing import Callable, Dict, List, Set
-from BaseClasses import MultiWorld, ItemClassification, Item, Location
-from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, filler_items
-from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\
- mission_orders, MissionInfo, alt_final_mission_locations, MissionPools
-from .Options import get_option_value, MissionOrder, FinalMap, MissionProgressLocations, LocationInclusion
-from .LogicMixin import SC2WoLLogic
-
-# Items with associated upgrades
-UPGRADABLE_ITEMS = [
- "Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre",
- "Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine", "Cyclone",
- "Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Raven", "Science Vessel", "Liberator", "Valkyrie",
- "Bunker", "Missile Turret"
-]
-
-BARRACKS_UNITS = {"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre"}
-FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine"}
-STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven", "Liberator", "Valkyrie"}
-
-PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"}
-
-
-def filter_missions(multiworld: MultiWorld, player: int) -> Dict[int, List[str]]:
- """
- Returns a semi-randomly pruned tuple of no-build, easy, medium, and hard mission sets
- """
-
- mission_order_type = get_option_value(multiworld, player, "mission_order")
- shuffle_no_build = get_option_value(multiworld, player, "shuffle_no_build")
- shuffle_protoss = get_option_value(multiworld, player, "shuffle_protoss")
- excluded_missions = get_option_value(multiworld, player, "excluded_missions")
- final_map = get_option_value(multiworld, player, "final_map")
- mission_pools = {
- MissionPools.STARTER: no_build_regions_list[:],
- MissionPools.EASY: easy_regions_list[:],
- MissionPools.MEDIUM: medium_regions_list[:],
- MissionPools.HARD: hard_regions_list[:],
- MissionPools.FINAL: []
- }
- if mission_order_type == MissionOrder.option_vanilla:
- # Vanilla uses the entire mission pool
- mission_pools[MissionPools.FINAL] = ['All-In']
- return mission_pools
- # Omitting No-Build missions if not shuffling no-build
- if not shuffle_no_build:
- excluded_missions = excluded_missions.union(no_build_regions_list)
- # Omitting Protoss missions if not shuffling protoss
- if not shuffle_protoss:
- excluded_missions = excluded_missions.union(PROTOSS_REGIONS)
- # Replacing All-In with alternate ending depending on option
- if final_map == FinalMap.option_random_hard:
- final_mission = multiworld.random.choice([mission for mission in alt_final_mission_locations.keys() if mission not in excluded_missions])
- excluded_missions.add(final_mission)
- else:
- final_mission = 'All-In'
- # Excluding missions
- for difficulty, mission_pool in mission_pools.items():
- mission_pools[difficulty] = [mission for mission in mission_pool if mission not in excluded_missions]
- mission_pools[MissionPools.FINAL].append(final_mission)
- # Mission pool changes on Build-Only
- if not get_option_value(multiworld, player, 'shuffle_no_build'):
- def move_mission(mission_name, current_pool, new_pool):
- if mission_name in mission_pools[current_pool]:
- mission_pools[current_pool].remove(mission_name)
- mission_pools[new_pool].append(mission_name)
- # Replacing No Build missions with Easy missions
- move_mission("Zero Hour", MissionPools.EASY, MissionPools.STARTER)
- move_mission("Evacuation", MissionPools.EASY, MissionPools.STARTER)
- move_mission("Devil's Playground", MissionPools.EASY, MissionPools.STARTER)
- # Pushing Outbreak to Normal, as it cannot be placed as the second mission on Build-Only
- move_mission("Outbreak", MissionPools.EASY, MissionPools.MEDIUM)
- # Pushing extra Normal missions to Easy
- move_mission("The Great Train Robbery", MissionPools.MEDIUM, MissionPools.EASY)
- move_mission("Echoes of the Future", MissionPools.MEDIUM, MissionPools.EASY)
- move_mission("Cutthroat", MissionPools.MEDIUM, MissionPools.EASY)
- # Additional changes on Advanced Tactics
- if get_option_value(multiworld, player, "required_tactics") > 0:
- move_mission("The Great Train Robbery", MissionPools.EASY, MissionPools.STARTER)
- move_mission("Smash and Grab", MissionPools.EASY, MissionPools.STARTER)
- move_mission("Moebius Factor", MissionPools.MEDIUM, MissionPools.EASY)
- move_mission("Welcome to the Jungle", MissionPools.MEDIUM, MissionPools.EASY)
- move_mission("Engine of Destruction", MissionPools.HARD, MissionPools.MEDIUM)
-
- return mission_pools
-
-
-def get_item_upgrades(inventory: List[Item], parent_item: Item or str):
- item_name = parent_item.name if isinstance(parent_item, Item) else parent_item
- return [
- inv_item for inv_item in inventory
- if get_full_item_list()[inv_item.name].parent_item == item_name
- ]
-
-
-def get_item_quantity(item):
- return get_full_item_list()[item.name].quantity
-
-
-def copy_item(item: Item):
- return Item(item.name, item.classification, item.code, item.player)
-
-
-class ValidInventory:
-
- def has(self, item: str, player: int):
- return item in self.logical_inventory
-
- def has_any(self, items: Set[str], player: int):
- return any(item in self.logical_inventory for item in items)
-
- def has_all(self, items: Set[str], player: int):
- return all(item in self.logical_inventory for item in items)
-
- def has_units_per_structure(self) -> bool:
- return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
- len(FACTORY_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
- len(STARPORT_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure
-
- def generate_reduced_inventory(self, inventory_size: int, mission_requirements: List[Callable]) -> List[Item]:
- """Attempts to generate a reduced inventory that can fulfill the mission requirements."""
- inventory = list(self.item_pool)
- locked_items = list(self.locked_items)
- self.logical_inventory = {
- item.name for item in inventory + locked_items + self.existing_items
- if item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing)
- }
- requirements = mission_requirements
- cascade_keys = self.cascade_removal_map.keys()
- units_always_have_upgrades = get_option_value(self.multiworld, self.player, "units_always_have_upgrades")
-
- def attempt_removal(item: Item) -> bool:
- # If item can be removed and has associated items, remove them as well
- inventory.remove(item)
- # Only run logic checks when removing logic items
- if item.name in self.logical_inventory:
- self.logical_inventory.remove(item.name)
- if not all(requirement(self) for requirement in requirements):
- # If item cannot be removed, lock or revert
- self.logical_inventory.add(item.name)
- for _ in range(get_item_quantity(item)):
- locked_items.append(copy_item(item))
- return False
- return True
-
- # Limit the maximum number of upgrades
- maxUpgrad = get_option_value(self.multiworld, self.player,
- "max_number_of_upgrades")
- if maxUpgrad != -1:
- unit_avail_upgrades = {}
- # Needed to take into account locked/existing items
- unit_nb_upgrades = {}
- for item in inventory:
- cItem = get_full_item_list()[item.name]
- if cItem.type in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades:
- unit_avail_upgrades[item.name] = []
- unit_nb_upgrades[item.name] = 0
- elif cItem.parent_item is not None:
- if cItem.parent_item not in unit_avail_upgrades:
- unit_avail_upgrades[cItem.parent_item] = [item]
- unit_nb_upgrades[cItem.parent_item] = 1
- else:
- unit_avail_upgrades[cItem.parent_item].append(item)
- unit_nb_upgrades[cItem.parent_item] += 1
- # For those two categories, we count them but dont include them in removal
- for item in locked_items + self.existing_items:
- cItem = get_full_item_list()[item.name]
- if cItem.type in UPGRADABLE_ITEMS and item.name not in unit_avail_upgrades:
- unit_avail_upgrades[item.name] = []
- unit_nb_upgrades[item.name] = 0
- elif cItem.parent_item is not None:
- if cItem.parent_item not in unit_avail_upgrades:
- unit_nb_upgrades[cItem.parent_item] = 1
- else:
- unit_nb_upgrades[cItem.parent_item] += 1
- # Making sure that the upgrades being removed is random
- # Currently, only for combat shield vs Stabilizer Medpacks...
- shuffled_unit_upgrade_list = list(unit_avail_upgrades.keys())
- self.multiworld.random.shuffle(shuffled_unit_upgrade_list)
- for unit in shuffled_unit_upgrade_list:
- while (unit_nb_upgrades[unit] > maxUpgrad) \
- and (len(unit_avail_upgrades[unit]) > 0):
- itemCandidate = self.multiworld.random.choice(unit_avail_upgrades[unit])
- _ = attempt_removal(itemCandidate)
- # Whatever it succeed to remove the iventory or it fails and thus
- # lock it, the upgrade is no longer available for removal
- unit_avail_upgrades[unit].remove(itemCandidate)
- unit_nb_upgrades[unit] -= 1
-
- # Locking associated items for items that have already been placed when units_always_have_upgrades is on
- if units_always_have_upgrades:
- existing_items = set(self.existing_items[:] + locked_items)
- while existing_items:
- existing_item = existing_items.pop()
- items_to_lock = self.cascade_removal_map.get(existing_item, [existing_item])
- if get_full_item_list()[existing_item.name].type != "Upgrade":
- # Don't process general upgrades, they may have been pre-locked per-level
- for item in items_to_lock:
- if item in inventory:
- # Unit upgrades, lock all levels
- for _ in range(inventory.count(item)):
- inventory.remove(item)
- if item not in locked_items:
- # Lock all the associated items if not already locked
- for _ in range(get_item_quantity(item)):
- locked_items.append(copy_item(item))
- if item in existing_items:
- existing_items.remove(item)
-
- if self.min_units_per_structure > 0 and self.has_units_per_structure():
- requirements.append(lambda state: state.has_units_per_structure())
-
- # Determining if the full-size inventory can complete campaign
- if not all(requirement(self) for requirement in requirements):
- raise Exception("Too many items excluded - campaign is impossible to complete.")
-
- while len(inventory) + len(locked_items) > inventory_size:
- if len(inventory) == 0:
- raise Exception("Reduced item pool generation failed - not enough locations available to place items.")
- # Select random item from removable items
- item = self.multiworld.random.choice(inventory)
- # Cascade removals to associated items
- if item in cascade_keys:
- items_to_remove = self.cascade_removal_map[item]
- transient_items = []
- cascade_failure = False
- while len(items_to_remove) > 0:
- item_to_remove = items_to_remove.pop()
- transient_items.append(item_to_remove)
- if item_to_remove not in inventory:
- if units_always_have_upgrades and item_to_remove in locked_items:
- cascade_failure = True
- break
- else:
- continue
- success = attempt_removal(item_to_remove)
- if not success and units_always_have_upgrades:
- cascade_failure = True
- transient_items += items_to_remove
- break
- # Lock all associated items if any of them cannot be removed on Units Always Have Upgrades
- if cascade_failure:
- for transient_item in transient_items:
- if transient_item in inventory:
- for _ in range(inventory.count(transient_item)):
- inventory.remove(transient_item)
- if transient_item not in locked_items:
- for _ in range(get_item_quantity(transient_item)):
- locked_items.append(copy_item(transient_item))
- if transient_item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing):
- self.logical_inventory.add(transient_item.name)
- else:
- attempt_removal(item)
-
- if not spider_mine_sources & self.logical_inventory:
- inventory = [item for item in inventory if not item.name.endswith("(Spider Mine)")]
- if not BARRACKS_UNITS & self.logical_inventory:
- inventory = [item for item in inventory if
- not (item.name.startswith("Progressive Infantry") or item.name == "Orbital Strike")]
- if not FACTORY_UNITS & self.logical_inventory:
- inventory = [item for item in inventory if not item.name.startswith("Progressive Vehicle")]
- if not STARPORT_UNITS & self.logical_inventory:
- inventory = [item for item in inventory if not item.name.startswith("Progressive Ship")]
-
- # Cull finished, adding locked items back into inventory
- inventory += locked_items
-
- # Replacing empty space with generically useful items
- replacement_items = [item for item in self.item_pool
- if (item not in inventory
- and item not in self.locked_items
- and item.name in second_pass_placeable_items)]
- self.multiworld.random.shuffle(replacement_items)
- while len(inventory) < inventory_size and len(replacement_items) > 0:
- item = replacement_items.pop()
- inventory.append(item)
-
- return inventory
-
- def _read_logic(self):
- self._sc2wol_has_common_unit = lambda world, player: SC2WoLLogic._sc2wol_has_common_unit(self, world, player)
- self._sc2wol_has_air = lambda world, player: SC2WoLLogic._sc2wol_has_air(self, world, player)
- self._sc2wol_has_air_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_air_anti_air(self, world, player)
- self._sc2wol_has_competent_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_anti_air(self, world, player)
- self._sc2wol_has_competent_ground_to_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_ground_to_air(self, world, player)
- self._sc2wol_has_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_anti_air(self, world, player)
- self._sc2wol_defense_rating = lambda world, player, zerg_enemy, air_enemy=False: SC2WoLLogic._sc2wol_defense_rating(self, world, player, zerg_enemy, air_enemy)
- self._sc2wol_has_competent_comp = lambda world, player: SC2WoLLogic._sc2wol_has_competent_comp(self, world, player)
- self._sc2wol_has_train_killers = lambda world, player: SC2WoLLogic._sc2wol_has_train_killers(self, world, player)
- self._sc2wol_able_to_rescue = lambda world, player: SC2WoLLogic._sc2wol_able_to_rescue(self, world, player)
- self._sc2wol_beats_protoss_deathball = lambda world, player: SC2WoLLogic._sc2wol_beats_protoss_deathball(self, world, player)
- self._sc2wol_survives_rip_field = lambda world, player: SC2WoLLogic._sc2wol_survives_rip_field(self, world, player)
- self._sc2wol_has_protoss_common_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_common_units(self, world, player)
- self._sc2wol_has_protoss_medium_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_medium_units(self, world, player)
- self._sc2wol_has_mm_upgrade = lambda world, player: SC2WoLLogic._sc2wol_has_mm_upgrade(self, world, player)
- self._sc2wol_welcome_to_the_jungle_requirement = lambda world, player: SC2WoLLogic._sc2wol_welcome_to_the_jungle_requirement(self, world, player)
- self._sc2wol_can_respond_to_colony_infestations = lambda world, player: SC2WoLLogic._sc2wol_can_respond_to_colony_infestations(self, world, player)
- self._sc2wol_final_mission_requirements = lambda world, player: SC2WoLLogic._sc2wol_final_mission_requirements(self, world, player)
-
- def __init__(self, multiworld: MultiWorld, player: int,
- item_pool: List[Item], existing_items: List[Item], locked_items: List[Item],
- has_protoss: bool):
- self.multiworld = multiworld
- self.player = player
- self.logical_inventory = set()
- self.locked_items = locked_items[:]
- self.existing_items = existing_items
- self._read_logic()
- # Initial filter of item pool
- self.item_pool = []
- item_quantities: dict[str, int] = dict()
- # Inventory restrictiveness based on number of missions with checks
- mission_order_type = get_option_value(self.multiworld, self.player, "mission_order")
- mission_count = len(mission_orders[mission_order_type]) - 1
- self.min_units_per_structure = int(mission_count / 7)
- min_upgrades = 1 if mission_count < 10 else 2
- for item in item_pool:
- item_info = get_full_item_list()[item.name]
- if item_info.type == "Upgrade":
- # Locking upgrades based on mission duration
- if item.name not in item_quantities:
- item_quantities[item.name] = 0
- item_quantities[item.name] += 1
- if item_quantities[item.name] < min_upgrades:
- self.locked_items.append(item)
- else:
- self.item_pool.append(item)
- elif item_info.type == "Goal":
- locked_items.append(item)
- elif item_info.type != "Protoss" or has_protoss:
- self.item_pool.append(item)
- self.cascade_removal_map: Dict[Item, List[Item]] = dict()
- for item in self.item_pool + locked_items + existing_items:
- if item.name in UPGRADABLE_ITEMS:
- upgrades = get_item_upgrades(self.item_pool, item)
- associated_items = [*upgrades, item]
- self.cascade_removal_map[item] = associated_items
- if get_option_value(multiworld, player, "units_always_have_upgrades"):
- for upgrade in upgrades:
- self.cascade_removal_map[upgrade] = associated_items
-
-
-def filter_items(multiworld: MultiWorld, player: int, mission_req_table: Dict[str, MissionInfo], location_cache: List[Location],
- item_pool: List[Item], existing_items: List[Item], locked_items: List[Item]) -> List[Item]:
- """
- Returns a semi-randomly pruned set of items based on number of available locations.
- The returned inventory must be capable of logically accessing every location in the world.
- """
- open_locations = [location for location in location_cache if location.item is None]
- inventory_size = len(open_locations)
- has_protoss = bool(PROTOSS_REGIONS.intersection(mission_req_table.keys()))
- mission_requirements = [location.access_rule for location in location_cache]
- valid_inventory = ValidInventory(multiworld, player, item_pool, existing_items, locked_items, has_protoss)
-
- valid_items = valid_inventory.generate_reduced_inventory(inventory_size, mission_requirements)
- return valid_items
diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py
deleted file mode 100644
index f588ce7e982e..000000000000
--- a/worlds/sc2wol/Regions.py
+++ /dev/null
@@ -1,313 +0,0 @@
-from typing import List, Set, Dict, Tuple, Optional, Callable
-from BaseClasses import MultiWorld, Region, Entrance, Location
-from .Locations import LocationData
-from .Options import get_option_value, MissionOrder
-from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations, \
- MissionPools, vanilla_shuffle_order
-from .PoolFilter import filter_missions
-
-PROPHECY_CHAIN_MISSION_COUNT = 4
-
-VANILLA_SHUFFLED_FIRST_PROPHECY_MISSION = 21
-
-def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location])\
- -> Tuple[Dict[str, MissionInfo], int, str]:
- locations_per_region = get_locations_per_region(locations)
-
- mission_order_type = get_option_value(multiworld, player, "mission_order")
- mission_order = mission_orders[mission_order_type]
-
- mission_pools = filter_missions(multiworld, player)
-
- regions = [create_region(multiworld, player, locations_per_region, location_cache, "Menu")]
-
- names: Dict[str, int] = {}
-
- if mission_order_type == MissionOrder.option_vanilla:
-
- # Generating all regions and locations
- for region_name in vanilla_mission_req_table.keys():
- regions.append(create_region(multiworld, player, locations_per_region, location_cache, region_name))
- multiworld.regions += regions
-
- connect(multiworld, player, names, 'Menu', 'Liberation Day'),
- connect(multiworld, player, names, 'Liberation Day', 'The Outlaws',
- lambda state: state.has("Beat Liberation Day", player)),
- connect(multiworld, player, names, 'The Outlaws', 'Zero Hour',
- lambda state: state.has("Beat The Outlaws", player)),
- connect(multiworld, player, names, 'Zero Hour', 'Evacuation',
- lambda state: state.has("Beat Zero Hour", player)),
- connect(multiworld, player, names, 'Evacuation', 'Outbreak',
- lambda state: state.has("Beat Evacuation", player)),
- connect(multiworld, player, names, "Outbreak", "Safe Haven",
- lambda state: state._sc2wol_cleared_missions(multiworld, player, 7) and
- state.has("Beat Outbreak", player)),
- connect(multiworld, player, names, "Outbreak", "Haven's Fall",
- lambda state: state._sc2wol_cleared_missions(multiworld, player, 7) and
- state.has("Beat Outbreak", player)),
- connect(multiworld, player, names, 'Zero Hour', 'Smash and Grab',
- lambda state: state.has("Beat Zero Hour", player)),
- connect(multiworld, player, names, 'Smash and Grab', 'The Dig',
- lambda state: state._sc2wol_cleared_missions(multiworld, player, 8) and
- state.has("Beat Smash and Grab", player)),
- connect(multiworld, player, names, 'The Dig', 'The Moebius Factor',
- lambda state: state._sc2wol_cleared_missions(multiworld, player, 11) and
- state.has("Beat The Dig", player)),
- connect(multiworld, player, names, 'The Moebius Factor', 'Supernova',
- lambda state: state._sc2wol_cleared_missions(multiworld, player, 14) and
- state.has("Beat The Moebius Factor", player)),
- connect(multiworld, player, names, 'Supernova', 'Maw of the Void',
- lambda state: state.has("Beat Supernova", player)),
- connect(multiworld, player, names, 'Zero Hour', "Devil's Playground",
- lambda state: state._sc2wol_cleared_missions(multiworld, player, 4) and
- state.has("Beat Zero Hour", player)),
- connect(multiworld, player, names, "Devil's Playground", 'Welcome to the Jungle',
- lambda state: state.has("Beat Devil's Playground", player)),
- connect(multiworld, player, names, "Welcome to the Jungle", 'Breakout',
- lambda state: state._sc2wol_cleared_missions(multiworld, player, 8) and
- state.has("Beat Welcome to the Jungle", player)),
- connect(multiworld, player, names, "Welcome to the Jungle", 'Ghost of a Chance',
- lambda state: state._sc2wol_cleared_missions(multiworld, player, 8) and
- state.has("Beat Welcome to the Jungle", player)),
- connect(multiworld, player, names, "Zero Hour", 'The Great Train Robbery',
- lambda state: state._sc2wol_cleared_missions(multiworld, player, 6) and
- state.has("Beat Zero Hour", player)),
- connect(multiworld, player, names, 'The Great Train Robbery', 'Cutthroat',
- lambda state: state.has("Beat The Great Train Robbery", player)),
- connect(multiworld, player, names, 'Cutthroat', 'Engine of Destruction',
- lambda state: state.has("Beat Cutthroat", player)),
- connect(multiworld, player, names, 'Engine of Destruction', 'Media Blitz',
- lambda state: state.has("Beat Engine of Destruction", player)),
- connect(multiworld, player, names, 'Media Blitz', 'Piercing the Shroud',
- lambda state: state.has("Beat Media Blitz", player)),
- connect(multiworld, player, names, 'The Dig', 'Whispers of Doom',
- lambda state: state.has("Beat The Dig", player)),
- connect(multiworld, player, names, 'Whispers of Doom', 'A Sinister Turn',
- lambda state: state.has("Beat Whispers of Doom", player)),
- connect(multiworld, player, names, 'A Sinister Turn', 'Echoes of the Future',
- lambda state: state.has("Beat A Sinister Turn", player)),
- connect(multiworld, player, names, 'Echoes of the Future', 'In Utter Darkness',
- lambda state: state.has("Beat Echoes of the Future", player)),
- connect(multiworld, player, names, 'Maw of the Void', 'Gates of Hell',
- lambda state: state.has("Beat Maw of the Void", player)),
- connect(multiworld, player, names, 'Gates of Hell', 'Belly of the Beast',
- lambda state: state.has("Beat Gates of Hell", player)),
- connect(multiworld, player, names, 'Gates of Hell', 'Shatter the Sky',
- lambda state: state.has("Beat Gates of Hell", player)),
- connect(multiworld, player, names, 'Gates of Hell', 'All-In',
- lambda state: state.has('Beat Gates of Hell', player) and (
- state.has('Beat Shatter the Sky', player) or state.has('Beat Belly of the Beast', player)))
-
- return vanilla_mission_req_table, 29, 'All-In: Victory'
-
- else:
- missions = []
-
- remove_prophecy = mission_order_type == 1 and not get_option_value(multiworld, player, "shuffle_protoss")
-
- final_mission = mission_pools[MissionPools.FINAL][0]
-
- # Determining if missions must be removed
- mission_pool_size = sum(len(mission_pool) for mission_pool in mission_pools.values())
- removals = len(mission_order) - mission_pool_size
- # Removing entire Prophecy chain on vanilla shuffled when not shuffling protoss
- if remove_prophecy:
- removals -= PROPHECY_CHAIN_MISSION_COUNT
-
- # Initial fill out of mission list and marking all-in mission
- for mission in mission_order:
- # Removing extra missions if mission pool is too small
- # Also handle lower removal priority than Prophecy
- if 0 < mission.removal_priority <= removals or mission.category == 'Prophecy' and remove_prophecy \
- or (remove_prophecy and mission_order_type == MissionOrder.option_vanilla_shuffled
- and mission.removal_priority > vanilla_shuffle_order[
- VANILLA_SHUFFLED_FIRST_PROPHECY_MISSION].removal_priority
- and 0 < mission.removal_priority <= removals + PROPHECY_CHAIN_MISSION_COUNT):
- missions.append(None)
- elif mission.type == MissionPools.FINAL:
- missions.append(final_mission)
- else:
- missions.append(mission.type)
-
- no_build_slots = []
- easy_slots = []
- medium_slots = []
- hard_slots = []
-
- # Search through missions to find slots needed to fill
- for i in range(len(missions)):
- if missions[i] is None:
- continue
- if missions[i] == MissionPools.STARTER:
- no_build_slots.append(i)
- elif missions[i] == MissionPools.EASY:
- easy_slots.append(i)
- elif missions[i] == MissionPools.MEDIUM:
- medium_slots.append(i)
- elif missions[i] == MissionPools.HARD:
- hard_slots.append(i)
-
- # Add no_build missions to the pool and fill in no_build slots
- missions_to_add = mission_pools[MissionPools.STARTER]
- if len(no_build_slots) > len(missions_to_add):
- raise Exception("There are no valid No-Build missions. Please exclude fewer missions.")
- for slot in no_build_slots:
- filler = multiworld.random.randint(0, len(missions_to_add) - 1)
-
- missions[slot] = missions_to_add.pop(filler)
-
- # Add easy missions into pool and fill in easy slots
- missions_to_add = missions_to_add + mission_pools[MissionPools.EASY]
- if len(easy_slots) > len(missions_to_add):
- raise Exception("There are not enough Easy missions to fill the campaign. Please exclude fewer missions.")
- for slot in easy_slots:
- filler = multiworld.random.randint(0, len(missions_to_add) - 1)
-
- missions[slot] = missions_to_add.pop(filler)
-
- # Add medium missions into pool and fill in medium slots
- missions_to_add = missions_to_add + mission_pools[MissionPools.MEDIUM]
- if len(medium_slots) > len(missions_to_add):
- raise Exception("There are not enough Easy and Medium missions to fill the campaign. Please exclude fewer missions.")
- for slot in medium_slots:
- filler = multiworld.random.randint(0, len(missions_to_add) - 1)
-
- missions[slot] = missions_to_add.pop(filler)
-
- # Add hard missions into pool and fill in hard slots
- missions_to_add = missions_to_add + mission_pools[MissionPools.HARD]
- if len(hard_slots) > len(missions_to_add):
- raise Exception("There are not enough missions to fill the campaign. Please exclude fewer missions.")
- for slot in hard_slots:
- filler = multiworld.random.randint(0, len(missions_to_add) - 1)
-
- missions[slot] = missions_to_add.pop(filler)
-
- # Generating regions and locations from selected missions
- for region_name in missions:
- regions.append(create_region(multiworld, player, locations_per_region, location_cache, region_name))
- multiworld.regions += regions
-
- # Mapping original mission slots to shifted mission slots when missions are removed
- slot_map = []
- slot_offset = 0
- for position, mission in enumerate(missions):
- slot_map.append(position - slot_offset + 1)
- if mission is None:
- slot_offset += 1
-
- # Loop through missions to create requirements table and connect regions
- # TODO: Handle 'and' connections
- mission_req_table = {}
-
- def build_connection_rule(mission_names: List[str], missions_req: int) -> Callable:
- if len(mission_names) > 1:
- return lambda state: state.has_all({f"Beat {name}" for name in mission_names}, player) and \
- state._sc2wol_cleared_missions(multiworld, player, missions_req)
- else:
- return lambda state: state.has(f"Beat {mission_names[0]}", player) and \
- state._sc2wol_cleared_missions(multiworld, player, missions_req)
-
- for i, mission in enumerate(missions):
- if mission is None:
- continue
- connections = []
- all_connections = []
- for connection in mission_order[i].connect_to:
- if connection == -1:
- continue
- while missions[connection] is None:
- connection -= 1
- all_connections.append(missions[connection])
- for connection in mission_order[i].connect_to:
- required_mission = missions[connection]
- if connection == -1:
- connect(multiworld, player, names, "Menu", mission)
- else:
- if required_mission is None and not mission_order[i].completion_critical: # Drop non-critical null slots
- continue
- while required_mission is None: # Substituting null slot with prior slot
- connection -= 1
- required_mission = missions[connection]
- required_missions = [required_mission] if mission_order[i].or_requirements else all_connections
- connect(multiworld, player, names, required_mission, mission,
- build_connection_rule(required_missions, mission_order[i].number))
- connections.append(slot_map[connection])
-
- mission_req_table.update({mission: MissionInfo(
- vanilla_mission_req_table[mission].id, connections, mission_order[i].category,
- number=mission_order[i].number,
- completion_critical=mission_order[i].completion_critical,
- or_requirements=mission_order[i].or_requirements)})
-
- final_mission_id = vanilla_mission_req_table[final_mission].id
-
- # Changing the completion condition for alternate final missions into an event
- if final_mission != 'All-In':
- final_location = alt_final_mission_locations[final_mission]
- # Final location should be near the end of the cache
- for i in range(len(location_cache) - 1, -1, -1):
- if location_cache[i].name == final_location:
- location_cache[i].locked = True
- location_cache[i].event = True
- location_cache[i].address = None
- break
- else:
- final_location = 'All-In: Victory'
-
- return mission_req_table, final_mission_id, final_location
-
-def create_location(player: int, location_data: LocationData, region: Region,
- location_cache: List[Location]) -> Location:
- location = Location(player, location_data.name, location_data.code, region)
- location.access_rule = location_data.rule
-
- if id is None:
- location.event = True
- location.locked = True
-
- location_cache.append(location)
-
- return location
-
-
-def create_region(multiworld: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]],
- location_cache: List[Location], name: str) -> Region:
- region = Region(name, player, multiworld)
-
- if name in locations_per_region:
- for location_data in locations_per_region[name]:
- location = create_location(player, location_data, region, location_cache)
- region.locations.append(location)
-
- return region
-
-
-def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: str, target: str,
- rule: Optional[Callable] = None):
- sourceRegion = world.get_region(source, player)
- targetRegion = world.get_region(target, player)
-
- if target not in used_names:
- used_names[target] = 1
- name = target
- else:
- used_names[target] += 1
- name = target + (' ' * used_names[target])
-
- connection = Entrance(player, name, sourceRegion)
-
- if rule:
- connection.access_rule = rule
-
- sourceRegion.exits.append(connection)
- connection.connect(targetRegion)
-
-
-def get_locations_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]:
- per_region: Dict[str, List[LocationData]] = {}
-
- for location in locations:
- per_region.setdefault(location.region, []).append(location)
-
- return per_region
diff --git a/worlds/sc2wol/Starcraft2.kv b/worlds/sc2wol/Starcraft2.kv
deleted file mode 100644
index 9c52d64c4702..000000000000
--- a/worlds/sc2wol/Starcraft2.kv
+++ /dev/null
@@ -1,16 +0,0 @@
-:
- rows: 1
-
-:
- cols: 1
- padding: [10,5,10,5]
- spacing: [0,5]
-
-:
- text_size: self.size
- markup: True
- halign: 'center'
- valign: 'middle'
- padding_x: 5
- markup: True
- outline_width: 1
diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py
deleted file mode 100644
index 93aebb7ad15a..000000000000
--- a/worlds/sc2wol/__init__.py
+++ /dev/null
@@ -1,324 +0,0 @@
-import typing
-
-from typing import List, Set, Tuple, Dict
-from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification
-from worlds.AutoWorld import WebWorld, World
-from .Items import StarcraftWoLItem, filler_items, item_name_groups, get_item_table, get_full_item_list, \
- get_basic_units, ItemData, upgrade_included_names, progressive_if_nco
-from .Locations import get_locations, LocationType
-from .Regions import create_regions
-from .Options import sc2wol_options, get_option_value, LocationInclusion
-from .LogicMixin import SC2WoLLogic
-from .PoolFilter import filter_missions, filter_items, get_item_upgrades
-from .MissionTables import starting_mission_locations, MissionInfo
-
-
-class Starcraft2WoLWebWorld(WebWorld):
- setup = Tutorial(
- "Multiworld Setup Guide",
- "A guide to setting up the Starcraft 2 randomizer connected to an Archipelago Multiworld",
- "English",
- "setup_en.md",
- "setup/en",
- ["TheCondor"]
- )
-
- tutorials = [setup]
-
-
-class SC2WoLWorld(World):
- """
- StarCraft II: Wings of Liberty is a science fiction real-time strategy video game developed and published by Blizzard Entertainment.
- Command Raynor's Raiders in collecting pieces of the Keystone in order to stop the zerg threat posed by the Queen of Blades.
- """
-
- game = "Starcraft 2 Wings of Liberty"
- web = Starcraft2WoLWebWorld()
- data_version = 4
-
- item_name_to_id = {name: data.code for name, data in get_full_item_list().items()}
- location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
- option_definitions = sc2wol_options
-
- item_name_groups = item_name_groups
- locked_locations: typing.List[str]
- location_cache: typing.List[Location]
- mission_req_table = {}
- final_mission_id: int
- victory_item: str
- required_client_version = 0, 3, 6
-
- def __init__(self, multiworld: MultiWorld, player: int):
- super(SC2WoLWorld, self).__init__(multiworld, player)
- self.location_cache = []
- self.locked_locations = []
-
- def create_item(self, name: str) -> Item:
- data = get_full_item_list()[name]
- return StarcraftWoLItem(name, data.classification, data.code, self.player)
-
- def create_regions(self):
- self.mission_req_table, self.final_mission_id, self.victory_item = create_regions(
- self.multiworld, self.player, get_locations(self.multiworld, self.player), self.location_cache
- )
-
- def create_items(self):
- setup_events(self.player, self.locked_locations, self.location_cache)
-
- excluded_items = get_excluded_items(self.multiworld, self.player)
-
- starter_items = assign_starter_items(self.multiworld, self.player, excluded_items, self.locked_locations)
-
- filter_locations(self.multiworld, self.player, self.locked_locations, self.location_cache)
-
- pool = get_item_pool(self.multiworld, self.player, self.mission_req_table, starter_items, excluded_items, self.location_cache)
-
- fill_item_pool_with_dummy_items(self, self.multiworld, self.player, self.locked_locations, self.location_cache, pool)
-
- self.multiworld.itempool += pool
-
- def set_rules(self):
- self.multiworld.completion_condition[self.player] = lambda state: state.has(self.victory_item, self.player)
-
- def get_filler_item_name(self) -> str:
- return self.multiworld.random.choice(filler_items)
-
- def fill_slot_data(self):
- slot_data = {}
- for option_name in sc2wol_options:
- option = getattr(self.multiworld, option_name)[self.player]
- if type(option.value) in {str, int}:
- slot_data[option_name] = int(option.value)
- slot_req_table = {}
- for mission in self.mission_req_table:
- slot_req_table[mission] = self.mission_req_table[mission]._asdict()
-
- slot_data["mission_req"] = slot_req_table
- slot_data["final_mission"] = self.final_mission_id
- return slot_data
-
-
-def setup_events(player: int, locked_locations: typing.List[str], location_cache: typing.List[Location]):
- for location in location_cache:
- if location.address is None:
- item = Item(location.name, ItemClassification.progression, None, player)
-
- locked_locations.append(location.name)
-
- location.place_locked_item(item)
-
-
-def get_excluded_items(multiworld: MultiWorld, player: int) -> Set[str]:
- excluded_items: Set[str] = set()
-
- for item in multiworld.precollected_items[player]:
- excluded_items.add(item.name)
-
- excluded_items_option = getattr(multiworld, 'excluded_items', [])
-
- excluded_items.update(excluded_items_option[player].value)
-
- return excluded_items
-
-
-def assign_starter_items(multiworld: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]) -> List[Item]:
- non_local_items = multiworld.non_local_items[player].value
- if get_option_value(multiworld, player, "early_unit"):
- local_basic_unit = sorted(item for item in get_basic_units(multiworld, player) if item not in non_local_items and item not in excluded_items)
- if not local_basic_unit:
- raise Exception("At least one basic unit must be local")
-
- # The first world should also be the starting world
- first_mission = list(multiworld.worlds[player].mission_req_table)[0]
- if first_mission in starting_mission_locations:
- first_location = starting_mission_locations[first_mission]
- elif first_mission == "In Utter Darkness":
- first_location = first_mission + ": Defeat"
- else:
- first_location = first_mission + ": Victory"
-
- return [assign_starter_item(multiworld, player, excluded_items, locked_locations, first_location, local_basic_unit)]
- else:
- return []
-
-
-def assign_starter_item(multiworld: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str],
- location: str, item_list: Tuple[str, ...]) -> Item:
-
- item_name = multiworld.random.choice(item_list)
-
- excluded_items.add(item_name)
-
- item = create_item_with_correct_settings(player, item_name)
-
- multiworld.get_location(location, player).place_locked_item(item)
-
- locked_locations.append(location)
-
- return item
-
-
-def get_item_pool(multiworld: MultiWorld, player: int, mission_req_table: Dict[str, MissionInfo],
- starter_items: List[Item], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]:
- pool: List[Item] = []
-
- # For the future: goal items like Artifact Shards go here
- locked_items = []
-
- # YAML items
- yaml_locked_items = get_option_value(multiworld, player, 'locked_items')
-
- # Adjust generic upgrade availability based on options
- include_upgrades = get_option_value(multiworld, player, 'generic_upgrade_missions') == 0
- upgrade_items = get_option_value(multiworld, player, 'generic_upgrade_items')
-
- # Include items from outside Wings of Liberty
- item_sets = {'wol'}
- if get_option_value(multiworld, player, 'nco_items'):
- item_sets.add('nco')
- if get_option_value(multiworld, player, 'bw_items'):
- item_sets.add('bw')
- if get_option_value(multiworld, player, 'ext_items'):
- item_sets.add('ext')
-
- def allowed_quantity(name: str, data: ItemData) -> int:
- if name in excluded_items \
- or data.type == "Upgrade" and (not include_upgrades or name not in upgrade_included_names[upgrade_items]) \
- or not data.origin.intersection(item_sets):
- return 0
- elif name in progressive_if_nco and 'nco' not in item_sets:
- return 1
- else:
- return data.quantity
-
- for name, data in get_item_table(multiworld, player).items():
- for i in range(allowed_quantity(name, data)):
- item = create_item_with_correct_settings(player, name)
- if name in yaml_locked_items:
- locked_items.append(item)
- else:
- pool.append(item)
-
- existing_items = starter_items + [item for item in multiworld.precollected_items[player]]
- existing_names = [item.name for item in existing_items]
-
- # Check the parent item integrity, exclude items
- pool[:] = [item for item in pool if pool_contains_parent(item, pool + locked_items + existing_items)]
-
- # Removing upgrades for excluded items
- for item_name in excluded_items:
- if item_name in existing_names:
- continue
- invalid_upgrades = get_item_upgrades(pool, item_name)
- for invalid_upgrade in invalid_upgrades:
- pool.remove(invalid_upgrade)
-
- filtered_pool = filter_items(multiworld, player, mission_req_table, location_cache, pool, existing_items, locked_items)
- return filtered_pool
-
-
-def fill_item_pool_with_dummy_items(self: SC2WoLWorld, multiworld: MultiWorld, player: int, locked_locations: List[str],
- location_cache: List[Location], pool: List[Item]):
- for _ in range(len(location_cache) - len(locked_locations) - len(pool)):
- item = create_item_with_correct_settings(player, self.get_filler_item_name())
- pool.append(item)
-
-
-def create_item_with_correct_settings(player: int, name: str) -> Item:
- data = get_full_item_list()[name]
-
- item = Item(name, data.classification, data.code, player)
-
- return item
-
-
-def pool_contains_parent(item: Item, pool: [Item]):
- item_data = get_full_item_list().get(item.name)
- if item_data.parent_item is None:
- # The item has not associated parent, the item is valid
- return True
- parent_item = item_data.parent_item
- # Check if the pool contains the parent item
- return parent_item in [pool_item.name for pool_item in pool]
-
-
-def filter_locations(multiworld: MultiWorld, player, locked_locations: List[str], location_cache: List[Location]):
- """
- Filters the locations in the world using a trash or Nothing item
- :param multiworld:
- :param player:
- :param locked_locations:
- :param location_cache:
- :return:
- """
- open_locations = [location for location in location_cache if location.item is None]
- plando_locations = get_plando_locations(multiworld, player)
- mission_progress_locations = get_option_value(multiworld, player, "mission_progress_locations")
- bonus_locations = get_option_value(multiworld, player, "bonus_locations")
- challenge_locations = get_option_value(multiworld, player, "challenge_locations")
- optional_boss_locations = get_option_value(multiworld, player, "optional_boss_locations")
- location_data = get_locations(multiworld, player)
- for location in open_locations:
- # Go through the locations that aren't locked yet (early unit, etc)
- if location.name not in plando_locations:
- # The location is not plando'd
- sc2_location = [sc2_location for sc2_location in location_data if sc2_location.name == location.name][0]
- location_type = sc2_location.type
-
- if location_type == LocationType.MISSION_PROGRESS \
- and mission_progress_locations != LocationInclusion.option_enabled:
- item_name = get_exclusion_item(multiworld, mission_progress_locations)
- place_exclusion_item(item_name, location, locked_locations, player)
-
- if location_type == LocationType.BONUS \
- and bonus_locations != LocationInclusion.option_enabled:
- item_name = get_exclusion_item(multiworld, bonus_locations)
- place_exclusion_item(item_name, location, locked_locations, player)
-
- if location_type == LocationType.CHALLENGE \
- and challenge_locations != LocationInclusion.option_enabled:
- item_name = get_exclusion_item(multiworld, challenge_locations)
- place_exclusion_item(item_name, location, locked_locations, player)
-
- if location_type == LocationType.OPTIONAL_BOSS \
- and optional_boss_locations != LocationInclusion.option_enabled:
- item_name = get_exclusion_item(multiworld, optional_boss_locations)
- place_exclusion_item(item_name, location, locked_locations, player)
-
-
-def place_exclusion_item(item_name, location, locked_locations, player):
- item = create_item_with_correct_settings(player, item_name)
- location.place_locked_item(item)
- locked_locations.append(location.name)
-
-
-def get_exclusion_item(multiworld: MultiWorld, option) -> str:
- """
- Gets the exclusion item according to settings (trash/nothing)
- :param multiworld:
- :param option:
- :return: Item used for location exclusion
- """
- if option == LocationInclusion.option_nothing:
- return "Nothing"
- elif option == LocationInclusion.option_trash:
- index = multiworld.random.randint(0, len(filler_items) - 1)
- return filler_items[index]
- raise Exception(f"Unsupported option type: {option}")
-
-
-def get_plando_locations(multiworld: MultiWorld, player) -> List[str]:
- """
-
- :param multiworld:
- :param player:
- :return: A list of locations affected by a plando in a world
- """
- plando_locations = []
- for plando_setting in multiworld.plando_items[player]:
- plando_locations += plando_setting.get("locations", [])
- plando_setting_location = plando_setting.get("location", None)
- if plando_setting_location is not None:
- plando_locations.append(plando_setting_location)
-
- return plando_locations
diff --git a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md
deleted file mode 100644
index f7c8519a2a7c..000000000000
--- a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Starcraft 2 Wings of Liberty
-
-## What does randomization do to this game?
-
-The following unlocks are randomized as items:
-1. Your ability to build any non-worker unit (including Marines!).
-2. Your ability to upgrade infantry weapons, infantry armor, vehicle weapons, etc.
-3. All armory upgrades
-4. All laboratory upgrades
-5. All mercenaries
-6. Small boosts to your starting mineral and vespene gas totals on each mission
-
-You find items by making progress in bonus objectives (like by rescuing allies in 'Zero Hour') and by completing
-missions. When you receive items, they will immediately become available, even during a mission, and you will be
-notified via a text box in the top-right corner of the game screen. (The text client for StarCraft 2 also records all
-items in all worlds.)
-
-Missions are launched only through the text client. The Hyperion is never visited. Additionally, credits are not used.
-
-## What is the goal of this game when randomized?
-
-The goal is to beat the final mission: 'All In'. The config file determines which variant you must complete.
-
-## What non-randomized changes are there from vanilla Starcraft 2?
-
-1. Some missions have more vespene geysers available to allow a wider variety of units.
-2. Starports no longer require Factories in order to be built.
-3. In 'A Sinister Turn' and 'Echoes of the Future', you can research Protoss air weapon/armor upgrades.
-
-## Which of my items can be in another player's world?
-
-By default, any of StarCraft 2's items (specified above) can be in another player's world. See the
-[Advanced YAML Guide](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en)
-for more information on how to change this.
\ No newline at end of file
diff --git a/worlds/sc2wol/docs/setup_en.md b/worlds/sc2wol/docs/setup_en.md
deleted file mode 100644
index 419f98a7330d..000000000000
--- a/worlds/sc2wol/docs/setup_en.md
+++ /dev/null
@@ -1,98 +0,0 @@
-# StarCraft 2 Wings of Liberty Randomizer Setup Guide
-
-This guide contains instructions on how to install and troubleshoot the StarCraft 2 Archipelago client, as well as where
-to obtain a config file for StarCraft 2.
-
-## Required Software
-
-- [StarCraft 2](https://starcraft2.com/en-us/)
-- [The most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
-- [StarCraft 2 AP Maps and Data](https://github.com/TheCondor07/Starcraft2ArchipelagoData)
-
-## How do I install this randomizer?
-
-1. Install StarCraft 2 and Archipelago using the first two links above. (The StarCraft 2 client for Archipelago is
- included by default.)
- - Linux users should also follow the instructions found at the bottom of this page
- (["Running in Linux"](#running-in-linux)).
-2. Run ArchipelagoStarcraft2Client.exe.
- - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only.
-3. Type the command `/download_data`. This will automatically install the Maps and Data files from the third link above.
-
-## Where do I get a config file (aka "YAML") for this game?
-
-The [Player Settings](https://archipelago.gg/games/Starcraft%202%20Wings%20of%20Liberty/player-settings) page on this
-website allows you to choose your personal settings for the randomizer and download them into a config file. Remember
-the name you type in the `Player Name` box; that's the "slot name" the client will ask you for when you attempt to
-connect!
-
-### And why do I need a config file?
-
-Config files tell Archipelago how you'd like your game to be randomized, even if you're only using default settings.
-When you're setting up a multiworld, every world needs its own config file.
-Check out [Creating a YAML](https://archipelago.gg/tutorial/Archipelago/setup/en#creating-a-yaml) for more information.
-
-## How do I join a MultiWorld game?
-
-1. Run ArchipelagoStarcraft2Client.exe.
- - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only.
-2. Type `/connect [server ip]`.
-3. Type your slot name and the server's password when prompted.
-4. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see every mission. By default,
- only 'Liberation Day' will be available at the beginning. Just click on a mission to start it!
-
-## The game isn't launching when I try to start a mission.
-
-First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If you can't figure out
-the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel for help. Please include a
-specific description of what's going wrong and attach your log file to your message.
-
-## Running in macOS
-
-To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](https://archipelago.gg/tutorial/Archipelago/mac/en). Note: when running the client, you will need to run the command `python3 Starcraft2Client.py`.
-
-## Running in Linux
-
-To run StarCraft 2 through Archipelago in Linux, you will need to install the game using Wine, then run the Linux build
-of the Archipelago client.
-
-Make sure you have StarCraft 2 installed using Wine, and that you have followed the
-[installation procedures](#how-do-i-install-this-randomizer?) to add the Archipelago maps to the correct location. You will not
-need to copy the .dll files. If you're having trouble installing or running StarCraft 2 on Linux, I recommend using the
-Lutris installer.
-
-Copy the following into a .sh file, replacing the values of **WINE** and **SC2PATH** variables with the relevant
-locations, as well as setting **PATH_TO_ARCHIPELAGO** to the directory containing the AppImage if it is not in the same
-folder as the script.
-
-```sh
-# Let the client know we're running SC2 in Wine
-export SC2PF=WineLinux
-export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
-
-# FIXME Replace with path to the version of Wine used to run SC2
-export WINE="/usr/bin/wine"
-
-# FIXME Replace with path to StarCraft II install folder
-export SC2PATH="/home/user/Games/starcraft-ii/drive_c/Program Files (x86)/StarCraft II/"
-
-# FIXME Set to directory which contains Archipelago AppImage file
-PATH_TO_ARCHIPELAGO=
-
-# Gets the latest version of Archipelago AppImage in PATH_TO_ARCHIPELAGO.
-# If PATH_TO_ARCHIPELAGO is not set, this defaults to the directory containing
-# this script file.
-ARCHIPELAGO="$(ls ${PATH_TO_ARCHIPELAGO:-$(dirname $0)}/Archipelago_*.AppImage | sort -r | head -1)"
-
-# Start the Archipelago client
-$ARCHIPELAGO Starcraft2Client
-```
-
-For Lutris installs, you can run `lutris -l` to get the numerical ID of your StarCraft II install, then run the command
-below, replacing **${ID}** with the numerical ID.
-
- lutris lutris:rungameid/${ID} --output-script sc2.sh
-
-This will get all of the relevant environment variables Lutris sets to run StarCraft 2 in a script, including the path
-to the Wine binary that Lutris uses. You can then remove the line that runs the Battle.Net launcher and copy the code
-above into the existing script.
diff --git a/worlds/sc2wol/requirements.txt b/worlds/sc2wol/requirements.txt
deleted file mode 100644
index 9b84863c4590..000000000000
--- a/worlds/sc2wol/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-nest-asyncio >= 1.5.5
-six >= 1.16.0
\ No newline at end of file
diff --git a/worlds/shivers/Constants.py b/worlds/shivers/Constants.py
new file mode 100644
index 000000000000..0b00cecec3ec
--- /dev/null
+++ b/worlds/shivers/Constants.py
@@ -0,0 +1,17 @@
+import os
+import json
+import pkgutil
+
+def load_data_file(*args) -> dict:
+ fname = os.path.join("data", *args)
+ return json.loads(pkgutil.get_data(__name__, fname).decode())
+
+location_id_offset: int = 27000
+
+location_info = load_data_file("locations.json")
+location_name_to_id = {name: location_id_offset + index \
+ for index, name in enumerate(location_info["all_locations"])}
+
+exclusion_info = load_data_file("excluded_locations.json")
+
+region_info = load_data_file("regions.json")
diff --git a/worlds/shivers/Items.py b/worlds/shivers/Items.py
new file mode 100644
index 000000000000..10d234d450bb
--- /dev/null
+++ b/worlds/shivers/Items.py
@@ -0,0 +1,132 @@
+from BaseClasses import Item, ItemClassification
+import typing
+
+class ShiversItem(Item):
+ game: str = "Shivers"
+
+class ItemData(typing.NamedTuple):
+ code: int
+ type: str
+ classification: ItemClassification = ItemClassification.progression
+
+SHIVERS_ITEM_ID_OFFSET = 27000
+
+item_table = {
+ #Pot Pieces
+ "Water Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 0, "pot"),
+ "Wax Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 1, "pot"),
+ "Ash Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 2, "pot"),
+ "Oil Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 3, "pot"),
+ "Cloth Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 4, "pot"),
+ "Wood Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 5, "pot"),
+ "Crystal Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 6, "pot"),
+ "Lightning Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 7, "pot"),
+ "Sand Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 8, "pot"),
+ "Metal Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 9, "pot"),
+ "Water Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 10, "pot"),
+ "Wax Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 11, "pot"),
+ "Ash Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 12, "pot"),
+ "Oil Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 13, "pot"),
+ "Cloth Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 14, "pot"),
+ "Wood Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 15, "pot"),
+ "Crystal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 16, "pot"),
+ "Lightning Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 17, "pot"),
+ "Sand Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 18, "pot"),
+ "Metal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 19, "pot"),
+ "Water Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 20, "pot_type2"),
+ "Wax Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 21, "pot_type2"),
+ "Ash Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 22, "pot_type2"),
+ "Oil Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 23, "pot_type2"),
+ "Cloth Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 24, "pot_type2"),
+ "Wood Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 25, "pot_type2"),
+ "Crystal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 26, "pot_type2"),
+ "Lightning Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 27, "pot_type2"),
+ "Sand Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 28, "pot_type2"),
+ "Metal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 29, "pot_type2"),
+
+ #Keys
+ "Key for Office Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 30, "key"),
+ "Key for Bedroom Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 31, "key"),
+ "Key for Three Floor Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 32, "key"),
+ "Key for Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 33, "key"),
+ "Key for Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 34, "key"),
+ "Key for Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 35, "key"),
+ "Key for Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 36, "key"),
+ "Key for Ocean Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 37, "key"),
+ "Key for Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 38, "key"),
+ "Key for Generator Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 39, "key"),
+ "Key for Egypt Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 40, "key"),
+ "Key for Library Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 41, "key"),
+ "Key for Shaman Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 42, "key"),
+ "Key for UFO Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 43, "key"),
+ "Key for Torture Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 44, "key"),
+ "Key for Puzzle Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 45, "key"),
+ "Key for Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 46, "key"),
+ "Key for Underground Lake Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 47, "key"),
+ "Key for Janitor Closet": ItemData(SHIVERS_ITEM_ID_OFFSET + 48, "key"),
+ "Key for Front Door": ItemData(SHIVERS_ITEM_ID_OFFSET + 49, "key-optional"),
+
+ #Abilities
+ "Crawling": ItemData(SHIVERS_ITEM_ID_OFFSET + 50, "ability"),
+
+ #Event Items
+ "Victory": ItemData(SHIVERS_ITEM_ID_OFFSET + 60, "victory"),
+
+ #Duplicate pot pieces for fill_Restrictive
+ "Water Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 70, "potduplicate"),
+ "Wax Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 71, "potduplicate"),
+ "Ash Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 72, "potduplicate"),
+ "Oil Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 73, "potduplicate"),
+ "Cloth Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 74, "potduplicate"),
+ "Wood Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 75, "potduplicate"),
+ "Crystal Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 76, "potduplicate"),
+ "Lightning Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 77, "potduplicate"),
+ "Sand Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 78, "potduplicate"),
+ "Metal Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 79, "potduplicate"),
+ "Water Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 80, "potduplicate"),
+ "Wax Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 81, "potduplicate"),
+ "Ash Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 82, "potduplicate"),
+ "Oil Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 83, "potduplicate"),
+ "Cloth Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 84, "potduplicate"),
+ "Wood Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 85, "potduplicate"),
+ "Crystal Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 86, "potduplicate"),
+ "Lightning Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 87, "potduplicate"),
+ "Sand Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 88, "potduplicate"),
+ "Metal Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 89, "potduplicate"),
+ "Water Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 140, "potduplicate_type2"),
+ "Wax Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 141, "potduplicate_type2"),
+ "Ash Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 142, "potduplicate_type2"),
+ "Oil Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 143, "potduplicate_type2"),
+ "Cloth Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 144, "potduplicate_type2"),
+ "Wood Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 145, "potduplicate_type2"),
+ "Crystal Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 146, "potduplicate_type2"),
+ "Lightning Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 147, "potduplicate_type2"),
+ "Sand Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 148, "potduplicate_type2"),
+ "Metal Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 149, "potduplicate_type2"),
+
+ #Filler
+ "Empty": ItemData(SHIVERS_ITEM_ID_OFFSET + 90, "filler"),
+ "Easier Lyre": ItemData(SHIVERS_ITEM_ID_OFFSET + 91, "filler", ItemClassification.filler),
+ "Water Always Available in Lobby": ItemData(SHIVERS_ITEM_ID_OFFSET + 92, "filler2", ItemClassification.filler),
+ "Wax Always Available in Library": ItemData(SHIVERS_ITEM_ID_OFFSET + 93, "filler2", ItemClassification.filler),
+ "Wax Always Available in Anansi Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 94, "filler2", ItemClassification.filler),
+ "Wax Always Available in Shaman Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 95, "filler2", ItemClassification.filler),
+ "Ash Always Available in Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 96, "filler2", ItemClassification.filler),
+ "Ash Always Available in Burial Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 97, "filler2", ItemClassification.filler),
+ "Oil Always Available in Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 98, "filler2", ItemClassification.filler),
+ "Cloth Always Available in Egypt": ItemData(SHIVERS_ITEM_ID_OFFSET + 99, "filler2", ItemClassification.filler),
+ "Cloth Always Available in Burial Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 100, "filler2", ItemClassification.filler),
+ "Wood Always Available in Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 101, "filler2", ItemClassification.filler),
+ "Wood Always Available in Blue Maze": ItemData(SHIVERS_ITEM_ID_OFFSET + 102, "filler2", ItemClassification.filler),
+ "Wood Always Available in Pegasus Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 103, "filler2", ItemClassification.filler),
+ "Wood Always Available in Gods Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 104, "filler2", ItemClassification.filler),
+ "Crystal Always Available in Lobby": ItemData(SHIVERS_ITEM_ID_OFFSET + 105, "filler2", ItemClassification.filler),
+ "Crystal Always Available in Ocean": ItemData(SHIVERS_ITEM_ID_OFFSET + 106, "filler2", ItemClassification.filler),
+ "Sand Always Available in Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 107, "filler2", ItemClassification.filler),
+ "Sand Always Available in Ocean": ItemData(SHIVERS_ITEM_ID_OFFSET + 108, "filler2", ItemClassification.filler),
+ "Metal Always Available in Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 109, "filler2", ItemClassification.filler),
+ "Metal Always Available in Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 110, "filler2", ItemClassification.filler),
+ "Metal Always Available in Prehistoric": ItemData(SHIVERS_ITEM_ID_OFFSET + 111, "filler2", ItemClassification.filler),
+ "Heal": ItemData(SHIVERS_ITEM_ID_OFFSET + 112, "filler3", ItemClassification.filler)
+
+}
diff --git a/worlds/shivers/Options.py b/worlds/shivers/Options.py
new file mode 100644
index 000000000000..2f33eb18e5d1
--- /dev/null
+++ b/worlds/shivers/Options.py
@@ -0,0 +1,106 @@
+from Options import Choice, DefaultOnToggle, Toggle, PerGameCommonOptions, Range
+from dataclasses import dataclass
+
+
+class IxupiCapturesNeeded(Range):
+ """
+ Number of Ixupi Captures needed for goal condition.
+ """
+ display_name = "Number of Ixupi Captures Needed"
+ range_start = 1
+ range_end = 10
+ default = 10
+
+class LobbyAccess(Choice):
+ """
+ Chooses how keys needed to reach the lobby are placed.
+ - Normal: Keys are placed anywhere
+ - Early: Keys are placed early
+ - Local: Keys are placed locally
+ """
+ display_name = "Lobby Access"
+ option_normal = 0
+ option_early = 1
+ option_local = 2
+ default = 1
+
+class PuzzleHintsRequired(DefaultOnToggle):
+ """
+ If turned on puzzle hints/solutions will be available before the corresponding puzzle is required.
+
+ For example: The Red Door puzzle will be logically required only after access to the Beth's Address Book which gives you the solution.
+
+ Turning this off allows for greater randomization.
+ """
+ display_name = "Puzzle Hints Required"
+
+class InformationPlaques(Toggle):
+ """
+ Adds Information Plaques as checks.
+ (40 Locations)
+ """
+ display_name = "Include Information Plaques"
+
+class FrontDoorUsable(Toggle):
+ """
+ Adds a key to unlock the front door of the museum.
+ """
+ display_name = "Front Door Usable"
+
+class ElevatorsStaySolved(DefaultOnToggle):
+ """
+ Adds elevators as checks and will remain open upon solving them.
+ (3 Locations)
+ """
+ display_name = "Elevators Stay Solved"
+
+class EarlyBeth(DefaultOnToggle):
+ """
+ Beth's body is open at the start of the game. This allows any pot piece to be placed in the slide and early checks on the second half of the final riddle.
+ """
+ display_name = "Early Beth"
+
+class EarlyLightning(Toggle):
+ """
+ Allows lightning to be captured at any point in the game. You will still need to capture all ten Ixupi for victory.
+ (1 Location)
+ """
+ display_name = "Early Lightning"
+
+class LocationPotPieces(Choice):
+ """
+ Chooses where pot pieces will be located within the multiworld.
+ - Own World: Pot pieces will be located within your own world
+ - Different World: Pot pieces will be located in another world
+ - Any World: Pot pieces will be located in any world
+ """
+ display_name = "Location of Pot Pieces"
+ option_own_world = 0
+ option_different_world = 1
+ option_any_world = 2
+
+class FullPots(Choice):
+ """
+ Chooses if pots will be in pieces or already completed
+ - Pieces: Only pot pieces will be added to the item pool
+ - Complete: Only completed pots will be added to the item pool
+ - Mixed: Each pot will be randomly chosen to be pieces or already completed.
+ """
+ display_name = "Full Pots"
+ option_pieces = 0
+ option_complete = 1
+ option_mixed = 2
+
+
+@dataclass
+class ShiversOptions(PerGameCommonOptions):
+ ixupi_captures_needed: IxupiCapturesNeeded
+ lobby_access: LobbyAccess
+ puzzle_hints_required: PuzzleHintsRequired
+ include_information_plaques: InformationPlaques
+ front_door_usable: FrontDoorUsable
+ elevators_stay_solved: ElevatorsStaySolved
+ early_beth: EarlyBeth
+ early_lightning: EarlyLightning
+ location_pot_pieces: LocationPotPieces
+ full_pots: FullPots
diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py
new file mode 100644
index 000000000000..5288fa2c9c3f
--- /dev/null
+++ b/worlds/shivers/Rules.py
@@ -0,0 +1,244 @@
+from typing import Dict, TYPE_CHECKING
+from collections.abc import Callable
+from BaseClasses import CollectionState
+from worlds.generic.Rules import forbid_item
+
+if TYPE_CHECKING:
+ from . import ShiversWorld
+
+
+def water_capturable(state: CollectionState, player: int) -> bool:
+ return state.has_all({"Water Pot Bottom", "Water Pot Top", "Water Pot Bottom DUPE", "Water Pot Top DUPE"}, player) or \
+ state.has_all({"Water Pot Complete", "Water Pot Complete DUPE"}, player)
+
+
+def wax_capturable(state: CollectionState, player: int) -> bool:
+ return state.has_all({"Wax Pot Bottom", "Wax Pot Top", "Wax Pot Bottom DUPE", "Wax Pot Top DUPE"}, player) or \
+ state.has_all({"Wax Pot Complete", "Wax Pot Complete DUPE"}, player)
+
+
+def ash_capturable(state: CollectionState, player: int) -> bool:
+ return state.has_all({"Ash Pot Bottom", "Ash Pot Top", "Ash Pot Bottom DUPE", "Ash Pot Top DUPE"}, player) or \
+ state.has_all({"Ash Pot Complete", "Ash Pot Complete DUPE"}, player)
+
+
+def oil_capturable(state: CollectionState, player: int) -> bool:
+ return state.has_all({"Oil Pot Bottom", "Oil Pot Top", "Oil Pot Bottom DUPE", "Oil Pot Top DUPE"}, player) or \
+ state.has_all({"Oil Pot Complete", "Oil Pot Complete DUPE"}, player)
+
+
+def cloth_capturable(state: CollectionState, player: int) -> bool:
+ return state.has_all({"Cloth Pot Bottom", "Cloth Pot Top", "Cloth Pot Bottom DUPE", "Cloth Pot Top DUPE"}, player) or \
+ state.has_all({"Cloth Pot Complete", "Cloth Pot Complete DUPE"}, player)
+
+
+def wood_capturable(state: CollectionState, player: int) -> bool:
+ return state.has_all({"Wood Pot Bottom", "Wood Pot Top", "Wood Pot Bottom DUPE", "Wood Pot Top DUPE"}, player) or \
+ state.has_all({"Wood Pot Complete", "Wood Pot Complete DUPE"}, player)
+
+
+def crystal_capturable(state: CollectionState, player: int) -> bool:
+ return state.has_all({"Crystal Pot Bottom", "Crystal Pot Top", "Crystal Pot Bottom DUPE", "Crystal Pot Top DUPE"}, player) or \
+ state.has_all({"Crystal Pot Complete", "Crystal Pot Complete DUPE"}, player)
+
+
+def sand_capturable(state: CollectionState, player: int) -> bool:
+ return state.has_all({"Sand Pot Bottom", "Sand Pot Top", "Sand Pot Bottom DUPE", "Sand Pot Top DUPE"}, player) or \
+ state.has_all({"Sand Pot Complete", "Sand Pot Complete DUPE"}, player)
+
+
+def metal_capturable(state: CollectionState, player: int) -> bool:
+ return state.has_all({"Metal Pot Bottom", "Metal Pot Top", "Metal Pot Bottom DUPE", "Metal Pot Top DUPE"}, player) or \
+ state.has_all({"Metal Pot Complete", "Metal Pot Complete DUPE"}, player)
+
+
+def lightning_capturable(state: CollectionState, player: int) -> bool:
+ return (first_nine_ixupi_capturable(state, player) or state.multiworld.worlds[player].options.early_lightning.value) \
+ and (state.has_all({"Lightning Pot Bottom", "Lightning Pot Top", "Lightning Pot Bottom DUPE", "Lightning Pot Top DUPE"}, player) or \
+ state.has_all({"Lightning Pot Complete", "Lightning Pot Complete DUPE"}, player))
+
+
+def beths_body_available(state: CollectionState, player: int) -> bool:
+ return (first_nine_ixupi_capturable(state, player) or state.multiworld.worlds[player].options.early_beth.value) \
+ and state.can_reach("Generator", "Region", player)
+
+
+def first_nine_ixupi_capturable(state: CollectionState, player: int) -> bool:
+ return water_capturable(state, player) and wax_capturable(state, player) \
+ and ash_capturable(state, player) and oil_capturable(state, player) \
+ and cloth_capturable(state, player) and wood_capturable(state, player) \
+ and crystal_capturable(state, player) and sand_capturable(state, player) \
+ and metal_capturable(state, player)
+
+
+def all_skull_dials_available(state: CollectionState, player: int) -> bool:
+ return state.can_reach("Prehistoric", "Region", player) and state.can_reach("Tar River", "Region", player) \
+ and state.can_reach("Egypt", "Region", player) and state.can_reach("Burial", "Region", player) \
+ and state.can_reach("Gods Room", "Region", player) and state.can_reach("Werewolf", "Region", player)
+
+
+def get_rules_lookup(player: int):
+ rules_lookup: Dict[str, Dict[str, Callable[[CollectionState], bool]]] = {
+ "entrances": {
+ "To Office Elevator From Underground Blue Tunnels": lambda state: state.has("Key for Office Elevator", player),
+ "To Office Elevator From Office": lambda state: state.has("Key for Office Elevator", player),
+ "To Bedroom Elevator From Office": lambda state: state.has_all({"Key for Bedroom Elevator", "Crawling"}, player),
+ "To Office From Bedroom Elevator": lambda state: state.has_all({"Key for Bedroom Elevator", "Crawling"}, player),
+ "To Three Floor Elevator From Maintenance Tunnels": lambda state: state.has("Key for Three Floor Elevator", player),
+ "To Three Floor Elevator From Blue Maze Bottom": lambda state: state.has("Key for Three Floor Elevator", player),
+ "To Three Floor Elevator From Blue Maze Top": lambda state: state.has("Key for Three Floor Elevator", player),
+ "To Workshop": lambda state: state.has("Key for Workshop", player),
+ "To Lobby From Office": lambda state: state.has("Key for Office", player),
+ "To Office From Lobby": lambda state: state.has("Key for Office", player),
+ "To Library From Lobby": lambda state: state.has("Key for Library Room", player),
+ "To Lobby From Library": lambda state: state.has("Key for Library Room", player),
+ "To Prehistoric From Lobby": lambda state: state.has("Key for Prehistoric Room", player),
+ "To Lobby From Prehistoric": lambda state: state.has("Key for Prehistoric Room", player),
+ "To Greenhouse": lambda state: state.has("Key for Greenhouse Room", player),
+ "To Ocean From Prehistoric": lambda state: state.has("Key for Ocean Room", player),
+ "To Prehistoric From Ocean": lambda state: state.has("Key for Ocean Room", player),
+ "To Projector Room": lambda state: state.has("Key for Projector Room", player),
+ "To Generator": lambda state: state.has("Key for Generator Room", player),
+ "To Lobby From Egypt": lambda state: state.has("Key for Egypt Room", player),
+ "To Egypt From Lobby": lambda state: state.has("Key for Egypt Room", player),
+ "To Janitor Closet": lambda state: state.has("Key for Janitor Closet", player),
+ "To Shaman From Burial": lambda state: state.has("Key for Shaman Room", player),
+ "To Burial From Shaman": lambda state: state.has("Key for Shaman Room", player),
+ "To Inventions From UFO": lambda state: state.has("Key for UFO Room", player),
+ "To UFO From Inventions": lambda state: state.has("Key for UFO Room", player),
+ "To Torture From Inventions": lambda state: state.has("Key for Torture Room", player),
+ "To Inventions From Torture": lambda state: state.has("Key for Torture Room", player),
+ "To Torture": lambda state: state.has("Key for Puzzle Room", player),
+ "To Puzzle Room Mastermind From Torture": lambda state: state.has("Key for Puzzle Room", player),
+ "To Bedroom": lambda state: state.has("Key for Bedroom", player),
+ "To Underground Lake From Underground Tunnels": lambda state: state.has("Key for Underground Lake Room", player),
+ "To Underground Tunnels From Underground Lake": lambda state: state.has("Key for Underground Lake Room", player),
+ "To Outside From Lobby": lambda state: state.has("Key for Front Door", player),
+ "To Lobby From Outside": lambda state: state.has("Key for Front Door", player),
+ "To Maintenance Tunnels From Theater Back Hallways": lambda state: state.has("Crawling", player),
+ "To Blue Maze From Egypt": lambda state: state.has("Crawling", player),
+ "To Egypt From Blue Maze": lambda state: state.has("Crawling", player),
+ "To Lobby From Tar River": lambda state: (state.has("Crawling", player) and oil_capturable(state, player)),
+ "To Tar River From Lobby": lambda state: (state.has("Crawling", player) and oil_capturable(state, player) and state.can_reach("Tar River", "Region", player)),
+ "To Burial From Egypt": lambda state: state.can_reach("Egypt", "Region", player),
+ "To Gods Room From Anansi": lambda state: state.can_reach("Gods Room", "Region", player),
+ "To Slide Room": lambda state: all_skull_dials_available(state, player),
+ "To Lobby From Slide Room": lambda state: beths_body_available(state, player),
+ "To Water Capture From Janitor Closet": lambda state: cloth_capturable(state, player)
+ },
+ "locations_required": {
+ "Puzzle Solved Anansi Musicbox": lambda state: state.can_reach("Clock Tower", "Region", player),
+ "Accessible: Storage: Janitor Closet": lambda state: cloth_capturable(state, player),
+ "Accessible: Storage: Tar River": lambda state: oil_capturable(state, player),
+ "Accessible: Storage: Theater": lambda state: state.can_reach("Projector Room", "Region", player),
+ "Accessible: Storage: Slide": lambda state: beths_body_available(state, player) and state.can_reach("Slide Room", "Region", player),
+ "Ixupi Captured Water": lambda state: water_capturable(state, player),
+ "Ixupi Captured Wax": lambda state: wax_capturable(state, player),
+ "Ixupi Captured Ash": lambda state: ash_capturable(state, player),
+ "Ixupi Captured Oil": lambda state: oil_capturable(state, player),
+ "Ixupi Captured Cloth": lambda state: cloth_capturable(state, player),
+ "Ixupi Captured Wood": lambda state: wood_capturable(state, player),
+ "Ixupi Captured Crystal": lambda state: crystal_capturable(state, player),
+ "Ixupi Captured Sand": lambda state: sand_capturable(state, player),
+ "Ixupi Captured Metal": lambda state: metal_capturable(state, player),
+ "Final Riddle: Planets Aligned": lambda state: state.can_reach("Fortune Teller", "Region", player),
+ "Final Riddle: Norse God Stone Message": lambda state: (state.can_reach("Fortune Teller", "Region", player) and state.can_reach("UFO", "Region", player)),
+ "Final Riddle: Beth's Body Page 17": lambda state: beths_body_available(state, player),
+ "Final Riddle: Guillotine Dropped": lambda state: beths_body_available(state, player),
+ "Puzzle Solved Skull Dial Door": lambda state: all_skull_dials_available(state, player),
+ },
+ "locations_puzzle_hints": {
+ "Puzzle Solved Clock Tower Door": lambda state: state.can_reach("Three Floor Elevator", "Region", player),
+ "Puzzle Solved Clock Chains": lambda state: state.can_reach("Bedroom", "Region", player),
+ "Puzzle Solved Shaman Drums": lambda state: state.can_reach("Clock Tower", "Region", player),
+ "Puzzle Solved Red Door": lambda state: state.can_reach("Maintenance Tunnels", "Region", player),
+ "Puzzle Solved UFO Symbols": lambda state: state.can_reach("Library", "Region", player),
+ "Puzzle Solved Maze Door": lambda state: state.can_reach("Projector Room", "Region", player),
+ "Puzzle Solved Theater Door": lambda state: state.can_reach("Underground Lake", "Region", player),
+ "Puzzle Solved Columns of RA": lambda state: state.can_reach("Underground Lake", "Region", player),
+ "Final Riddle: Guillotine Dropped": lambda state: (beths_body_available(state, player) and state.can_reach("Underground Lake", "Region", player))
+ },
+ "elevators": {
+ "Puzzle Solved Office Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player))
+ and state.has("Key for Office Elevator", player)),
+ "Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)),
+ "Puzzle Solved Three Floor Elevator": lambda state: ((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player))
+ and state.has("Key for Three Floor Elevator", player))
+ },
+ "lightning": {
+ "Ixupi Captured Lightning": lambda state: lightning_capturable(state, player)
+ }
+ }
+ return rules_lookup
+
+
+def set_rules(world: "ShiversWorld") -> None:
+ multiworld = world.multiworld
+ player = world.player
+
+ rules_lookup = get_rules_lookup(player)
+ # Set required entrance rules
+ for entrance_name, rule in rules_lookup["entrances"].items():
+ multiworld.get_entrance(entrance_name, player).access_rule = rule
+
+ # Set required location rules
+ for location_name, rule in rules_lookup["locations_required"].items():
+ multiworld.get_location(location_name, player).access_rule = rule
+
+ # Set option location rules
+ if world.options.puzzle_hints_required.value:
+ for location_name, rule in rules_lookup["locations_puzzle_hints"].items():
+ multiworld.get_location(location_name, player).access_rule = rule
+ if world.options.elevators_stay_solved.value:
+ for location_name, rule in rules_lookup["elevators"].items():
+ multiworld.get_location(location_name, player).access_rule = rule
+ if world.options.early_lightning.value:
+ for location_name, rule in rules_lookup["lightning"].items():
+ multiworld.get_location(location_name, player).access_rule = rule
+
+ # Register indirect conditions
+ multiworld.register_indirect_condition(world.get_region("Burial"), world.get_entrance("To Slide Room"))
+ multiworld.register_indirect_condition(world.get_region("Egypt"), world.get_entrance("To Slide Room"))
+ multiworld.register_indirect_condition(world.get_region("Gods Room"), world.get_entrance("To Slide Room"))
+ multiworld.register_indirect_condition(world.get_region("Prehistoric"), world.get_entrance("To Slide Room"))
+ multiworld.register_indirect_condition(world.get_region("Tar River"), world.get_entrance("To Slide Room"))
+ multiworld.register_indirect_condition(world.get_region("Werewolf"), world.get_entrance("To Slide Room"))
+ multiworld.register_indirect_condition(world.get_region("Prehistoric"), world.get_entrance("To Tar River From Lobby"))
+
+ # forbid cloth in janitor closet and oil in tar river
+ forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Bottom DUPE", player)
+ forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Top DUPE", player)
+ forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Complete DUPE", player)
+ forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Bottom DUPE", player)
+ forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Top DUPE", player)
+ forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Complete DUPE", player)
+
+ # Filler Item Forbids
+ forbid_item(multiworld.get_location("Puzzle Solved Lyre", player), "Easier Lyre", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Water", player), "Water Always Available in Lobby", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Library", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Anansi Room", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Shaman Room", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Ash", player), "Ash Always Available in Office", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Ash", player), "Ash Always Available in Burial Room", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Oil", player), "Oil Always Available in Prehistoric Room", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Cloth", player), "Cloth Always Available in Egypt", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Cloth", player), "Cloth Always Available in Burial Room", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Workshop", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Blue Maze", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Pegasus Room", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Gods Room", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Crystal", player), "Crystal Always Available in Lobby", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Crystal", player), "Crystal Always Available in Ocean", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Sand", player), "Sand Always Available in Plants Room", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Sand", player), "Sand Always Available in Ocean", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Projector Room", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Bedroom", player)
+ forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Prehistoric", player)
+
+ # Set completion condition
+ multiworld.completion_condition[player] = lambda state: ((
+ water_capturable(state, player) + wax_capturable(state, player) + ash_capturable(state, player) \
+ + oil_capturable(state, player) + cloth_capturable(state, player) + wood_capturable(state, player) \
+ + crystal_capturable(state, player) + sand_capturable(state, player) + metal_capturable(state, player) \
+ + lightning_capturable(state, player)) >= world.options.ixupi_captures_needed.value)
diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py
new file mode 100644
index 000000000000..a2d7bc14644e
--- /dev/null
+++ b/worlds/shivers/__init__.py
@@ -0,0 +1,227 @@
+from typing import List
+from .Items import item_table, ShiversItem
+from .Rules import set_rules
+from BaseClasses import Item, Tutorial, Region, Location
+from Fill import fill_restrictive
+from worlds.AutoWorld import WebWorld, World
+from . import Constants, Rules
+from .Options import ShiversOptions
+
+
+class ShiversWeb(WebWorld):
+ tutorials = [Tutorial(
+ "Shivers Setup Guide",
+ "A guide to setting up Shivers for Multiworld.",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["GodlFire", "Mathx2"]
+ )]
+
+class ShiversWorld(World):
+ """
+ Shivers is a horror themed point and click adventure. Explore the mysteries of Windlenot's Museum of the Strange and Unusual.
+ """
+
+ game = "Shivers"
+ topology_present = False
+ web = ShiversWeb()
+ options_dataclass = ShiversOptions
+ options: ShiversOptions
+
+ item_name_to_id = {name: data.code for name, data in item_table.items()}
+ location_name_to_id = Constants.location_name_to_id
+ shivers_item_id_offset = 27000
+ pot_completed_list: List[int]
+
+
+ def generate_early(self):
+ self.pot_completed_list = []
+
+ def create_item(self, name: str) -> Item:
+ data = item_table[name]
+ return ShiversItem(name, data.classification, data.code, self.player)
+
+ def create_event(self, region_name: str, event_name: str) -> None:
+ region = self.multiworld.get_region(region_name, self.player)
+ loc = ShiversLocation(self.player, event_name, None, region)
+ loc.place_locked_item(self.create_event_item(event_name))
+ region.locations.append(loc)
+
+ def create_regions(self) -> None:
+ # Create regions
+ for region_name, exits in Constants.region_info["regions"]:
+ r = Region(region_name, self.player, self.multiworld)
+ self.multiworld.regions.append(r)
+ for exit_name in exits:
+ r.create_exit(exit_name)
+
+
+ # Bind mandatory connections
+ for entr_name, region_name in Constants.region_info["mandatory_connections"]:
+ e = self.multiworld.get_entrance(entr_name, self.player)
+ r = self.multiworld.get_region(region_name, self.player)
+ e.connect(r)
+
+ # Locations
+ # Build exclusion list
+ self.removed_locations = set()
+ if not self.options.include_information_plaques:
+ self.removed_locations.update(Constants.exclusion_info["plaques"])
+ if not self.options.elevators_stay_solved:
+ self.removed_locations.update(Constants.exclusion_info["elevators"])
+ if not self.options.early_lightning:
+ self.removed_locations.update(Constants.exclusion_info["lightning"])
+
+ # Add locations
+ for region_name, locations in Constants.location_info["locations_by_region"].items():
+ region = self.multiworld.get_region(region_name, self.player)
+ for loc_name in locations:
+ if loc_name not in self.removed_locations:
+ loc = ShiversLocation(self.player, loc_name, self.location_name_to_id.get(loc_name, None), region)
+ region.locations.append(loc)
+
+ def create_items(self) -> None:
+ #Add items to item pool
+ itempool = []
+ for name, data in item_table.items():
+ if data.type in {"key", "ability", "filler2"}:
+ itempool.append(self.create_item(name))
+
+ # Pot pieces/Completed/Mixed:
+ for i in range(10):
+ if self.options.full_pots == "pieces":
+ itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + i]))
+ itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 10 + i]))
+ elif self.options.full_pots == "complete":
+ itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 20 + i]))
+ else:
+ # Roll for if pieces or a complete pot will be used.
+ # Pot Pieces
+ if self.random.randint(0, 1) == 0:
+ self.pot_completed_list.append(0)
+ itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + i]))
+ itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 10 + i]))
+ # Completed Pot
+ else:
+ self.pot_completed_list.append(1)
+ itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 20 + i]))
+
+ #Add Filler
+ itempool += [self.create_item("Easier Lyre") for i in range(9)]
+
+ #Extra filler is random between Heals and Easier Lyre. Heals weighted 95%.
+ filler_needed = len(self.multiworld.get_unfilled_locations(self.player)) - 24 - len(itempool)
+ itempool += [self.random.choices([self.create_item("Heal"), self.create_item("Easier Lyre")], weights=[95, 5])[0] for i in range(filler_needed)]
+
+ #Place library escape items. Choose a location to place the escape item
+ library_region = self.multiworld.get_region("Library", self.player)
+ librarylocation = self.random.choice([loc for loc in library_region.locations if not loc.name.startswith("Accessible:")])
+
+ #Roll for which escape items will be placed in the Library
+ library_random = self.random.randint(1, 3)
+ if library_random == 1:
+ librarylocation.place_locked_item(self.create_item("Crawling"))
+
+ itempool = [item for item in itempool if item.name != "Crawling"]
+
+ elif library_random == 2:
+ librarylocation.place_locked_item(self.create_item("Key for Library Room"))
+
+ itempool = [item for item in itempool if item.name != "Key for Library Room"]
+ elif library_random == 3:
+ librarylocation.place_locked_item(self.create_item("Key for Three Floor Elevator"))
+
+ librarylocationkeytwo = self.random.choice([loc for loc in library_region.locations if not loc.name.startswith("Accessible:") and loc != librarylocation])
+ librarylocationkeytwo.place_locked_item(self.create_item("Key for Egypt Room"))
+
+ itempool = [item for item in itempool if item.name not in ["Key for Three Floor Elevator", "Key for Egypt Room"]]
+
+ #If front door option is on, determine which set of keys will be used for lobby access and add front door key to item pool
+ lobby_access_keys = 1
+ if self.options.front_door_usable:
+ lobby_access_keys = self.random.randint(1, 2)
+ itempool += [self.create_item("Key for Front Door")]
+ else:
+ itempool += [self.create_item("Heal")]
+
+ self.multiworld.itempool += itempool
+
+ #Lobby acess:
+ if self.options.lobby_access == "early":
+ if lobby_access_keys == 1:
+ self.multiworld.early_items[self.player]["Key for Underground Lake Room"] = 1
+ self.multiworld.early_items[self.player]["Key for Office Elevator"] = 1
+ self.multiworld.early_items[self.player]["Key for Office"] = 1
+ elif lobby_access_keys == 2:
+ self.multiworld.early_items[self.player]["Key for Front Door"] = 1
+ if self.options.lobby_access == "local":
+ if lobby_access_keys == 1:
+ self.multiworld.local_early_items[self.player]["Key for Underground Lake Room"] = 1
+ self.multiworld.local_early_items[self.player]["Key for Office Elevator"] = 1
+ self.multiworld.local_early_items[self.player]["Key for Office"] = 1
+ elif lobby_access_keys == 2:
+ self.multiworld.local_early_items[self.player]["Key for Front Door"] = 1
+
+ #Pot piece shuffle location:
+ if self.options.location_pot_pieces == "own_world":
+ self.options.local_items.value |= {name for name, data in item_table.items() if data.type == "pot" or data.type == "pot_type2"}
+ if self.options.location_pot_pieces == "different_world":
+ self.options.non_local_items.value |= {name for name, data in item_table.items() if data.type == "pot" or data.type == "pot_type2"}
+
+ def pre_fill(self) -> None:
+ # Prefills event storage locations with duplicate pots
+ storagelocs = []
+ storageitems = []
+ self.storage_placements = []
+
+ for locations in Constants.location_info["locations_by_region"].values():
+ for loc_name in locations:
+ if loc_name.startswith("Accessible: "):
+ storagelocs.append(self.multiworld.get_location(loc_name, self.player))
+
+ #Pot pieces/Completed/Mixed:
+ if self.options.full_pots == "pieces":
+ storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate']
+ elif self.options.full_pots == "complete":
+ storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate_type2']
+ storageitems += [self.create_item("Empty") for i in range(10)]
+ else:
+ for i in range(10):
+ #Pieces
+ if self.pot_completed_list[i] == 0:
+ storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 70 + i])]
+ storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 80 + i])]
+ #Complete
+ else:
+ storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 140 + i])]
+ storageitems += [self.create_item("Empty")]
+
+ storageitems += [self.create_item("Empty") for i in range(3)]
+
+ state = self.multiworld.get_all_state(True)
+
+ self.random.shuffle(storagelocs)
+ self.random.shuffle(storageitems)
+
+ fill_restrictive(self.multiworld, state, storagelocs.copy(), storageitems, True, True)
+
+ self.storage_placements = {location.name: location.item.name for location in storagelocs}
+
+ set_rules = set_rules
+
+ def fill_slot_data(self) -> dict:
+
+ return {
+ "StoragePlacements": self.storage_placements,
+ "ExcludedLocations": list(self.options.exclude_locations.value),
+ "IxupiCapturesNeeded": self.options.ixupi_captures_needed.value,
+ "ElevatorsStaySolved": self.options.elevators_stay_solved.value,
+ "EarlyBeth": self.options.early_beth.value,
+ "EarlyLightning": self.options.early_lightning.value,
+ "FrontDoorUsable": self.options.front_door_usable.value
+ }
+
+
+class ShiversLocation(Location):
+ game = "Shivers"
diff --git a/worlds/shivers/data/excluded_locations.json b/worlds/shivers/data/excluded_locations.json
new file mode 100644
index 000000000000..29655d4a5024
--- /dev/null
+++ b/worlds/shivers/data/excluded_locations.json
@@ -0,0 +1,52 @@
+{
+ "plaques": [
+ "Information Plaque: (Lobby) Transforming Masks",
+ "Information Plaque: (Lobby) Jade Skull",
+ "Information Plaque: (Prehistoric) Bronze Unicorn",
+ "Information Plaque: (Prehistoric) Griffin",
+ "Information Plaque: (Prehistoric) Eagles Nest",
+ "Information Plaque: (Prehistoric) Large Spider",
+ "Information Plaque: (Prehistoric) Starfish",
+ "Information Plaque: (Ocean) Quartz Crystal",
+ "Information Plaque: (Ocean) Poseidon",
+ "Information Plaque: (Ocean) Colossus of Rhodes",
+ "Information Plaque: (Ocean) Poseidon's Temple",
+ "Information Plaque: (Underground Maze) Subterranean World",
+ "Information Plaque: (Underground Maze) Dero",
+ "Information Plaque: (Egypt) Tomb of the Ixupi",
+ "Information Plaque: (Egypt) The Sphinx",
+ "Information Plaque: (Egypt) Curse of Anubis",
+ "Information Plaque: (Burial) Norse Burial Ship",
+ "Information Plaque: (Burial) Paracas Burial Bundles",
+ "Information Plaque: (Burial) Spectacular Coffins of Ghana",
+ "Information Plaque: (Burial) Cremation",
+ "Information Plaque: (Burial) Animal Crematorium",
+ "Information Plaque: (Shaman) Witch Doctors of the Congo",
+ "Information Plaque: (Shaman) Sarombe doctor of Mozambique",
+ "Information Plaque: (Gods) Fisherman's Canoe God",
+ "Information Plaque: (Gods) Mayan Gods",
+ "Information Plaque: (Gods) Thor",
+ "Information Plaque: (Gods) Celtic Janus Sculpture",
+ "Information Plaque: (Gods) Sumerian Bull God - An",
+ "Information Plaque: (Gods) Sumerian Lyre",
+ "Information Plaque: (Gods) Chuen",
+ "Information Plaque: (Anansi) African Creation Myth",
+ "Information Plaque: (Anansi) Apophis the Serpent",
+ "Information Plaque: (Anansi) Death",
+ "Information Plaque: (Pegasus) Cyclops",
+ "Information Plaque: (Werewolf) Lycanthropy",
+ "Information Plaque: (UFO) Coincidence or Extraterrestrial Visits?",
+ "Information Plaque: (UFO) Planets",
+ "Information Plaque: (UFO) Astronomical Construction",
+ "Information Plaque: (Torture) Guillotine",
+ "Information Plaque: (UFO) Aliens"
+ ],
+ "elevators": [
+ "Puzzle Solved Office Elevator",
+ "Puzzle Solved Bedroom Elevator",
+ "Puzzle Solved Three Floor Elevator"
+ ],
+ "lightning": [
+ "Ixupi Captured Lightning"
+ ]
+}
\ No newline at end of file
diff --git a/worlds/shivers/data/locations.json b/worlds/shivers/data/locations.json
new file mode 100644
index 000000000000..64fe3647348d
--- /dev/null
+++ b/worlds/shivers/data/locations.json
@@ -0,0 +1,343 @@
+{
+ "all_locations": [
+ "Puzzle Solved Gears",
+ "Puzzle Solved Stone Henge",
+ "Puzzle Solved Workshop Drawers",
+ "Puzzle Solved Library Statue",
+ "Puzzle Solved Theater Door",
+ "Puzzle Solved Clock Tower Door",
+ "Puzzle Solved Clock Chains",
+ "Puzzle Solved Atlantis",
+ "Puzzle Solved Organ",
+ "Puzzle Solved Maze Door",
+ "Puzzle Solved Columns of RA",
+ "Puzzle Solved Burial Door",
+ "Puzzle Solved Chinese Solitaire",
+ "Puzzle Solved Shaman Drums",
+ "Puzzle Solved Lyre",
+ "Puzzle Solved Red Door",
+ "Puzzle Solved Fortune Teller Door",
+ "Puzzle Solved Alchemy",
+ "Puzzle Solved UFO Symbols",
+ "Puzzle Solved Anansi Musicbox",
+ "Puzzle Solved Gallows",
+ "Puzzle Solved Mastermind",
+ "Puzzle Solved Marble Flipper",
+ "Puzzle Solved Skull Dial Door",
+ "Flashback Memory Obtained Beth's Ghost",
+ "Flashback Memory Obtained Merrick's Ghost",
+ "Flashback Memory Obtained Windlenot's Ghost",
+ "Flashback Memory Obtained Ancient Astrology",
+ "Flashback Memory Obtained Scrapbook",
+ "Flashback Memory Obtained Museum Brochure",
+ "Flashback Memory Obtained In Search of the Unexplained",
+ "Flashback Memory Obtained Egyptian Hieroglyphics Explained",
+ "Flashback Memory Obtained South American Pictographs",
+ "Flashback Memory Obtained Mythology of the Stars",
+ "Flashback Memory Obtained Black Book",
+ "Flashback Memory Obtained Theater Movie",
+ "Flashback Memory Obtained Museum Blueprints",
+ "Flashback Memory Obtained Beth's Address Book",
+ "Flashback Memory Obtained Merrick's Notebook",
+ "Flashback Memory Obtained Professor Windlenot's Diary",
+ "Ixupi Captured Water",
+ "Ixupi Captured Wax",
+ "Ixupi Captured Ash",
+ "Ixupi Captured Oil",
+ "Ixupi Captured Cloth",
+ "Ixupi Captured Wood",
+ "Ixupi Captured Crystal",
+ "Ixupi Captured Sand",
+ "Ixupi Captured Metal",
+ "Final Riddle: Fortune Teller",
+ "Final Riddle: Planets Aligned",
+ "Final Riddle: Norse God Stone Message",
+ "Final Riddle: Beth's Body Page 17",
+ "Final Riddle: Guillotine Dropped",
+ "Puzzle Hint Found: Combo Lock in Mailbox",
+ "Puzzle Hint Found: Orange Symbol",
+ "Puzzle Hint Found: Silver Symbol",
+ "Puzzle Hint Found: Green Symbol",
+ "Puzzle Hint Found: White Symbol",
+ "Puzzle Hint Found: Brown Symbol",
+ "Puzzle Hint Found: Tan Symbol",
+ "Puzzle Hint Found: Basilisk Bone Fragments",
+ "Puzzle Hint Found: Atlantis Map",
+ "Puzzle Hint Found: Sirens Song Heard",
+ "Puzzle Hint Found: Egyptian Sphinx Heard",
+ "Puzzle Hint Found: Gallows Information Plaque",
+ "Puzzle Hint Found: Mastermind Information Plaque",
+ "Puzzle Hint Found: Elevator Writing",
+ "Puzzle Hint Found: Shaman Security Camera",
+ "Puzzle Hint Found: Tape Recorder Heard",
+ "Information Plaque: (Lobby) Transforming Masks",
+ "Information Plaque: (Lobby) Jade Skull",
+ "Information Plaque: (Prehistoric) Bronze Unicorn",
+ "Information Plaque: (Prehistoric) Griffin",
+ "Information Plaque: (Prehistoric) Eagles Nest",
+ "Information Plaque: (Prehistoric) Large Spider",
+ "Information Plaque: (Prehistoric) Starfish",
+ "Information Plaque: (Ocean) Quartz Crystal",
+ "Information Plaque: (Ocean) Poseidon",
+ "Information Plaque: (Ocean) Colossus of Rhodes",
+ "Information Plaque: (Ocean) Poseidon's Temple",
+ "Information Plaque: (Underground Maze Staircase) Subterranean World",
+ "Information Plaque: (Underground Maze) Dero",
+ "Information Plaque: (Egypt) Tomb of the Ixupi",
+ "Information Plaque: (Egypt) The Sphinx",
+ "Information Plaque: (Egypt) Curse of Anubis",
+ "Information Plaque: (Burial) Norse Burial Ship",
+ "Information Plaque: (Burial) Paracas Burial Bundles",
+ "Information Plaque: (Burial) Spectacular Coffins of Ghana",
+ "Information Plaque: (Burial) Cremation",
+ "Information Plaque: (Burial) Animal Crematorium",
+ "Information Plaque: (Shaman) Witch Doctors of the Congo",
+ "Information Plaque: (Shaman) Sarombe doctor of Mozambique",
+ "Information Plaque: (Gods) Fisherman's Canoe God",
+ "Information Plaque: (Gods) Mayan Gods",
+ "Information Plaque: (Gods) Thor",
+ "Information Plaque: (Gods) Celtic Janus Sculpture",
+ "Information Plaque: (Gods) Sumerian Bull God - An",
+ "Information Plaque: (Gods) Sumerian Lyre",
+ "Information Plaque: (Gods) Chuen",
+ "Information Plaque: (Anansi) African Creation Myth",
+ "Information Plaque: (Anansi) Apophis the Serpent",
+ "Information Plaque: (Anansi) Death",
+ "Information Plaque: (Pegasus) Cyclops",
+ "Information Plaque: (Werewolf) Lycanthropy",
+ "Information Plaque: (UFO) Coincidence or Extraterrestrial Visits?",
+ "Information Plaque: (UFO) Planets",
+ "Information Plaque: (UFO) Astronomical Construction",
+ "Information Plaque: (Torture) Guillotine",
+ "Information Plaque: (UFO) Aliens",
+ "Puzzle Solved Office Elevator",
+ "Puzzle Solved Bedroom Elevator",
+ "Puzzle Solved Three Floor Elevator",
+ "Ixupi Captured Lightning"
+ ],
+ "locations_by_region": {
+ "Outside": [
+ "Puzzle Solved Gears",
+ "Puzzle Solved Stone Henge",
+ "Puzzle Solved Office Elevator",
+ "Puzzle Solved Three Floor Elevator",
+ "Puzzle Hint Found: Combo Lock in Mailbox",
+ "Puzzle Hint Found: Orange Symbol",
+ "Puzzle Hint Found: Silver Symbol",
+ "Puzzle Hint Found: Green Symbol",
+ "Puzzle Hint Found: White Symbol",
+ "Puzzle Hint Found: Brown Symbol",
+ "Puzzle Hint Found: Tan Symbol"
+ ],
+ "Underground Lake": [
+ "Flashback Memory Obtained Windlenot's Ghost",
+ "Flashback Memory Obtained Egyptian Hieroglyphics Explained"
+ ],
+ "Office": [
+ "Flashback Memory Obtained Scrapbook",
+ "Accessible: Storage: Desk Drawer",
+ "Puzzle Hint Found: Atlantis Map",
+ "Puzzle Hint Found: Tape Recorder Heard",
+ "Puzzle Solved Bedroom Elevator"
+ ],
+ "Workshop": [
+ "Puzzle Solved Workshop Drawers",
+ "Accessible: Storage: Workshop Drawers",
+ "Puzzle Hint Found: Basilisk Bone Fragments"
+ ],
+ "Bedroom": [
+ "Flashback Memory Obtained Professor Windlenot's Diary"
+ ],
+ "Library": [
+ "Puzzle Solved Library Statue",
+ "Flashback Memory Obtained In Search of the Unexplained",
+ "Flashback Memory Obtained South American Pictographs",
+ "Flashback Memory Obtained Mythology of the Stars",
+ "Flashback Memory Obtained Black Book",
+ "Accessible: Storage: Library Cabinet",
+ "Accessible: Storage: Library Statue"
+ ],
+ "Maintenance Tunnels": [
+ "Flashback Memory Obtained Beth's Address Book"
+ ],
+ "Three Floor Elevator": [
+ "Puzzle Hint Found: Elevator Writing"
+ ],
+ "Lobby": [
+ "Puzzle Solved Theater Door",
+ "Flashback Memory Obtained Museum Brochure",
+ "Information Plaque: (Lobby) Jade Skull",
+ "Information Plaque: (Lobby) Transforming Masks",
+ "Accessible: Storage: Slide",
+ "Accessible: Storage: Transforming Mask"
+ ],
+ "Generator": [
+ "Final Riddle: Beth's Body Page 17",
+ "Ixupi Captured Lightning"
+ ],
+ "Theater Back Hallways": [
+ "Puzzle Solved Clock Tower Door"
+ ],
+ "Clock Tower Staircase": [
+ "Puzzle Solved Clock Chains"
+ ],
+ "Clock Tower": [
+ "Flashback Memory Obtained Beth's Ghost",
+ "Accessible: Storage: Clock Tower",
+ "Puzzle Hint Found: Shaman Security Camera"
+ ],
+ "Projector Room": [
+ "Flashback Memory Obtained Theater Movie"
+ ],
+ "Ocean": [
+ "Puzzle Solved Atlantis",
+ "Puzzle Solved Organ",
+ "Flashback Memory Obtained Museum Blueprints",
+ "Accessible: Storage: Ocean",
+ "Puzzle Hint Found: Sirens Song Heard",
+ "Information Plaque: (Ocean) Quartz Crystal",
+ "Information Plaque: (Ocean) Poseidon",
+ "Information Plaque: (Ocean) Colossus of Rhodes",
+ "Information Plaque: (Ocean) Poseidon's Temple"
+ ],
+ "Maze Staircase": [
+ "Information Plaque: (Underground Maze Staircase) Subterranean World",
+ "Puzzle Solved Maze Door"
+ ],
+ "Egypt": [
+ "Puzzle Solved Columns of RA",
+ "Puzzle Solved Burial Door",
+ "Accessible: Storage: Egypt",
+ "Puzzle Hint Found: Egyptian Sphinx Heard",
+ "Information Plaque: (Egypt) Tomb of the Ixupi",
+ "Information Plaque: (Egypt) The Sphinx",
+ "Information Plaque: (Egypt) Curse of Anubis"
+ ],
+ "Burial": [
+ "Puzzle Solved Chinese Solitaire",
+ "Flashback Memory Obtained Merrick's Notebook",
+ "Accessible: Storage: Chinese Solitaire",
+ "Information Plaque: (Burial) Norse Burial Ship",
+ "Information Plaque: (Burial) Paracas Burial Bundles",
+ "Information Plaque: (Burial) Spectacular Coffins of Ghana",
+ "Information Plaque: (Burial) Animal Crematorium",
+ "Information Plaque: (Burial) Cremation"
+ ],
+ "Shaman": [
+ "Puzzle Solved Shaman Drums",
+ "Accessible: Storage: Shaman Hut",
+ "Information Plaque: (Shaman) Witch Doctors of the Congo",
+ "Information Plaque: (Shaman) Sarombe doctor of Mozambique"
+ ],
+ "Gods Room": [
+ "Puzzle Solved Lyre",
+ "Puzzle Solved Red Door",
+ "Accessible: Storage: Lyre",
+ "Final Riddle: Norse God Stone Message",
+ "Information Plaque: (Gods) Fisherman's Canoe God",
+ "Information Plaque: (Gods) Mayan Gods",
+ "Information Plaque: (Gods) Thor",
+ "Information Plaque: (Gods) Celtic Janus Sculpture",
+ "Information Plaque: (Gods) Sumerian Bull God - An",
+ "Information Plaque: (Gods) Sumerian Lyre",
+ "Information Plaque: (Gods) Chuen"
+ ],
+ "Blue Maze": [
+ "Puzzle Solved Fortune Teller Door"
+ ],
+ "Fortune Teller": [
+ "Flashback Memory Obtained Merrick's Ghost",
+ "Final Riddle: Fortune Teller"
+ ],
+ "Inventions": [
+ "Puzzle Solved Alchemy",
+ "Accessible: Storage: Alchemy"
+ ],
+ "UFO": [
+ "Puzzle Solved UFO Symbols",
+ "Accessible: Storage: UFO",
+ "Final Riddle: Planets Aligned",
+ "Information Plaque: (UFO) Coincidence or Extraterrestrial Visits?",
+ "Information Plaque: (UFO) Planets",
+ "Information Plaque: (UFO) Astronomical Construction",
+ "Information Plaque: (UFO) Aliens"
+ ],
+ "Anansi": [
+ "Puzzle Solved Anansi Musicbox",
+ "Flashback Memory Obtained Ancient Astrology",
+ "Accessible: Storage: Skeleton",
+ "Accessible: Storage: Anansi",
+ "Information Plaque: (Anansi) African Creation Myth",
+ "Information Plaque: (Anansi) Apophis the Serpent",
+ "Information Plaque: (Anansi) Death",
+ "Information Plaque: (Pegasus) Cyclops",
+ "Information Plaque: (Werewolf) Lycanthropy"
+ ],
+ "Torture": [
+ "Puzzle Solved Gallows",
+ "Accessible: Storage: Gallows",
+ "Final Riddle: Guillotine Dropped",
+ "Puzzle Hint Found: Gallows Information Plaque",
+ "Information Plaque: (Torture) Guillotine"
+ ],
+ "Puzzle Room Mastermind": [
+ "Puzzle Solved Mastermind",
+ "Puzzle Hint Found: Mastermind Information Plaque"
+ ],
+ "Puzzle Room Marbles": [
+ "Puzzle Solved Marble Flipper"
+ ],
+ "Prehistoric": [
+ "Information Plaque: (Prehistoric) Bronze Unicorn",
+ "Information Plaque: (Prehistoric) Griffin",
+ "Information Plaque: (Prehistoric) Eagles Nest",
+ "Information Plaque: (Prehistoric) Large Spider",
+ "Information Plaque: (Prehistoric) Starfish",
+ "Accessible: Storage: Eagles Nest"
+ ],
+ "Tar River": [
+ "Accessible: Storage: Tar River",
+ "Information Plaque: (Underground Maze) Dero"
+ ],
+ "Theater": [
+ "Accessible: Storage: Theater"
+ ],
+ "Greenhouse": [
+ "Accessible: Storage: Greenhouse"
+ ],
+ "Janitor Closet": [
+ "Accessible: Storage: Janitor Closet"
+ ],
+ "Skull Dial Bridge": [
+ "Accessible: Storage: Skull Bridge",
+ "Puzzle Solved Skull Dial Door"
+ ],
+ "Water Capture": [
+ "Ixupi Captured Water"
+ ],
+ "Wax Capture": [
+ "Ixupi Captured Wax"
+ ],
+ "Ash Capture": [
+ "Ixupi Captured Ash"
+ ],
+ "Oil Capture": [
+ "Ixupi Captured Oil"
+ ],
+ "Cloth Capture": [
+ "Ixupi Captured Cloth"
+ ],
+ "Wood Capture": [
+ "Ixupi Captured Wood"
+ ],
+ "Crystal Capture": [
+ "Ixupi Captured Crystal"
+ ],
+ "Sand Capture": [
+ "Ixupi Captured Sand"
+ ],
+ "Metal Capture": [
+ "Ixupi Captured Metal"
+ ]
+ }
+}
diff --git a/worlds/shivers/data/regions.json b/worlds/shivers/data/regions.json
new file mode 100644
index 000000000000..aeb5aa737366
--- /dev/null
+++ b/worlds/shivers/data/regions.json
@@ -0,0 +1,177 @@
+{
+ "regions": [
+ ["Menu", ["To Registry"]],
+ ["Registry", ["To Outside From Registry"]],
+ ["Outside", ["To Underground Tunnels From Outside", "To Lobby From Outside"]],
+ ["Underground Tunnels", ["To Underground Lake From Underground Tunnels", "To Outside From Underground"]],
+ ["Underground Lake", ["To Underground Tunnels From Underground Lake", "To Underground Blue Tunnels From Underground Lake"]],
+ ["Underground Blue Tunnels", ["To Underground Lake From Underground Blue Tunnels", "To Office Elevator From Underground Blue Tunnels"]],
+ ["Office Elevator", ["To Underground Blue Tunnels From Office Elevator","To Office From Office Elevator"]],
+ ["Office", ["To Office Elevator From Office", "To Workshop", "To Lobby From Office", "To Bedroom Elevator From Office", "To Ash Capture From Office"]],
+ ["Workshop", ["To Office From Workshop", "To Wood Capture From Workshop"]],
+ ["Bedroom Elevator", ["To Office From Bedroom Elevator", "To Bedroom"]],
+ ["Bedroom", ["To Bedroom Elevator From Bedroom", "To Metal Capture From Bedroom"]],
+ ["Lobby", ["To Office From Lobby", "To Library From Lobby", "To Theater From Lobby", "To Prehistoric From Lobby", "To Egypt From Lobby", "To Tar River From Lobby", "To Outside From Lobby", "To Water Capture From Lobby", "To Crystal Capture From Lobby"]],
+ ["Library", ["To Lobby From Library", "To Maintenance Tunnels From Library", "To Wax Capture From Library"]],
+ ["Maintenance Tunnels", ["To Library From Maintenance Tunnels", "To Three Floor Elevator From Maintenance Tunnels", "To Generator"]],
+ ["Generator", ["To Maintenance Tunnels From Generator"]],
+ ["Theater", ["To Lobby From Theater", "To Theater Back Hallways From Theater"]],
+ ["Theater Back Hallways", ["To Theater From Theater Back Hallways", "To Clock Tower Staircase From Theater Back Hallways", "To Maintenance Tunnels From Theater Back Hallways", "To Projector Room"]],
+ ["Clock Tower Staircase", ["To Theater Back Hallways From Clock Tower Staircase", "To Clock Tower"]],
+ ["Clock Tower", ["To Clock Tower Staircase From Clock Tower"]],
+ ["Projector Room", ["To Theater Back Hallways From Projector Room", "To Metal Capture From Projector Room"]],
+ ["Prehistoric", ["To Lobby From Prehistoric", "To Greenhouse", "To Ocean From Prehistoric", "To Oil Capture From Prehistoric", "To Metal Capture From Prehistoric"]],
+ ["Greenhouse", ["To Prehistoric From Greenhouse", "To Sand Capture From Greenhouse"]],
+ ["Ocean", ["To Prehistoric From Ocean", "To Maze Staircase From Ocean", "To Crystal Capture From Ocean", "To Sand Capture From Ocean"]],
+ ["Maze Staircase", ["To Ocean From Maze Staircase", "To Maze From Maze Staircase"]],
+ ["Maze", ["To Maze Staircase From Maze", "To Tar River"]],
+ ["Tar River", ["To Maze From Tar River", "To Lobby From Tar River", "To Oil Capture From Tar River"]],
+ ["Egypt", ["To Lobby From Egypt", "To Burial From Egypt", "To Blue Maze From Egypt", "To Cloth Capture From Egypt"]],
+ ["Burial", ["To Egypt From Burial", "To Shaman From Burial", "To Ash Capture From Burial", "To Cloth Capture From Burial"]],
+ ["Shaman", ["To Burial From Shaman", "To Gods Room", "To Wax Capture From Shaman"]],
+ ["Gods Room", ["To Shaman From Gods Room", "To Anansi From Gods Room", "To Wood Capture From Gods Room"]],
+ ["Anansi", ["To Gods Room From Anansi", "To Werewolf From Anansi", "To Wax Capture From Anansi", "To Wood Capture From Anansi"]],
+ ["Werewolf", ["To Anansi From Werewolf", "To Night Staircase From Werewolf"]],
+ ["Night Staircase", ["To Werewolf From Night Staircase", "To Janitor Closet", "To UFO"]],
+ ["Janitor Closet", ["To Night Staircase From Janitor Closet", "To Water Capture From Janitor Closet", "To Cloth Capture From Janitor Closet"]],
+ ["UFO", ["To Night Staircase From UFO", "To Inventions From UFO"]],
+ ["Blue Maze", ["To Egypt From Blue Maze", "To Three Floor Elevator From Blue Maze Bottom", "To Three Floor Elevator From Blue Maze Top", "To Fortune Teller", "To Inventions From Blue Maze", "To Wood Capture From Blue Maze"]],
+ ["Three Floor Elevator", ["To Maintenance Tunnels From Three Floor Elevator", "To Blue Maze From Three Floor Elevator"]],
+ ["Fortune Teller", ["To Blue Maze From Fortune Teller"]],
+ ["Inventions", ["To Blue Maze From Inventions", "To UFO From Inventions", "To Torture From Inventions"]],
+ ["Torture", ["To Inventions From Torture", "To Puzzle Room Mastermind From Torture"]],
+ ["Puzzle Room Mastermind", ["To Torture", "To Puzzle Room Marbles From Puzzle Room Mastermind"]],
+ ["Puzzle Room Marbles", ["To Puzzle Room Mastermind From Puzzle Room Marbles", "To Skull Dial Bridge From Puzzle Room Marbles"]],
+ ["Skull Dial Bridge", ["To Puzzle Room Marbles From Skull Dial Bridge", "To Slide Room"]],
+ ["Slide Room", ["To Skull Dial Bridge From Slide Room", "To Lobby From Slide Room"]],
+ ["Water Capture", []],
+ ["Wax Capture", []],
+ ["Ash Capture", []],
+ ["Oil Capture", []],
+ ["Cloth Capture", []],
+ ["Wood Capture", []],
+ ["Crystal Capture", []],
+ ["Sand Capture", []],
+ ["Metal Capture", []]
+ ],
+ "mandatory_connections": [
+ ["To Registry", "Registry"],
+ ["To Outside From Registry", "Outside"],
+ ["To Outside From Underground", "Outside"],
+ ["To Outside From Lobby", "Outside"],
+ ["To Underground Tunnels From Outside", "Underground Tunnels"],
+ ["To Underground Tunnels From Underground Lake", "Underground Tunnels"],
+ ["To Underground Lake From Underground Tunnels", "Underground Lake"],
+ ["To Underground Lake From Underground Blue Tunnels", "Underground Lake"],
+ ["To Underground Blue Tunnels From Underground Lake", "Underground Blue Tunnels"],
+ ["To Underground Blue Tunnels From Office Elevator", "Underground Blue Tunnels"],
+ ["To Office Elevator From Underground Blue Tunnels", "Office Elevator"],
+ ["To Office Elevator From Office", "Office Elevator"],
+ ["To Office From Office Elevator", "Office"],
+ ["To Office From Workshop", "Office"],
+ ["To Office From Bedroom Elevator", "Office"],
+ ["To Office From Lobby", "Office"],
+ ["To Workshop", "Workshop"],
+ ["To Lobby From Office", "Lobby"],
+ ["To Lobby From Library", "Lobby"],
+ ["To Lobby From Tar River", "Lobby"],
+ ["To Lobby From Slide Room", "Lobby"],
+ ["To Lobby From Egypt", "Lobby"],
+ ["To Lobby From Theater", "Lobby"],
+ ["To Lobby From Prehistoric", "Lobby"],
+ ["To Lobby From Outside", "Lobby"],
+ ["To Bedroom Elevator From Office", "Bedroom Elevator"],
+ ["To Bedroom Elevator From Bedroom", "Bedroom Elevator"],
+ ["To Bedroom", "Bedroom"],
+ ["To Library From Lobby", "Library"],
+ ["To Library From Maintenance Tunnels", "Library"],
+ ["To Theater From Lobby", "Theater" ],
+ ["To Theater From Theater Back Hallways", "Theater"],
+ ["To Prehistoric From Lobby", "Prehistoric"],
+ ["To Prehistoric From Greenhouse", "Prehistoric"],
+ ["To Prehistoric From Ocean", "Prehistoric"],
+ ["To Egypt From Lobby", "Egypt"],
+ ["To Egypt From Burial", "Egypt"],
+ ["To Egypt From Blue Maze", "Egypt"],
+ ["To Maintenance Tunnels From Generator", "Maintenance Tunnels"],
+ ["To Maintenance Tunnels From Three Floor Elevator", "Maintenance Tunnels"],
+ ["To Maintenance Tunnels From Library", "Maintenance Tunnels"],
+ ["To Maintenance Tunnels From Theater Back Hallways", "Maintenance Tunnels"],
+ ["To Three Floor Elevator From Maintenance Tunnels", "Three Floor Elevator"],
+ ["To Three Floor Elevator From Blue Maze Bottom", "Three Floor Elevator"],
+ ["To Three Floor Elevator From Blue Maze Top", "Three Floor Elevator"],
+ ["To Generator", "Generator"],
+ ["To Theater Back Hallways From Theater", "Theater Back Hallways"],
+ ["To Theater Back Hallways From Clock Tower Staircase", "Theater Back Hallways"],
+ ["To Theater Back Hallways From Projector Room", "Theater Back Hallways"],
+ ["To Clock Tower Staircase From Theater Back Hallways", "Clock Tower Staircase"],
+ ["To Clock Tower Staircase From Clock Tower", "Clock Tower Staircase"],
+ ["To Projector Room", "Projector Room"],
+ ["To Clock Tower", "Clock Tower"],
+ ["To Greenhouse", "Greenhouse"],
+ ["To Ocean From Prehistoric", "Ocean"],
+ ["To Ocean From Maze Staircase", "Ocean"],
+ ["To Maze Staircase From Ocean", "Maze Staircase"],
+ ["To Maze Staircase From Maze", "Maze Staircase"],
+ ["To Maze From Maze Staircase", "Maze"],
+ ["To Maze From Tar River", "Maze"],
+ ["To Tar River", "Tar River"],
+ ["To Tar River From Lobby", "Tar River"],
+ ["To Burial From Egypt", "Burial"],
+ ["To Burial From Shaman", "Burial"],
+ ["To Blue Maze From Three Floor Elevator", "Blue Maze"],
+ ["To Blue Maze From Fortune Teller", "Blue Maze"],
+ ["To Blue Maze From Inventions", "Blue Maze"],
+ ["To Blue Maze From Egypt", "Blue Maze"],
+ ["To Shaman From Burial", "Shaman"],
+ ["To Shaman From Gods Room", "Shaman"],
+ ["To Gods Room", "Gods Room" ],
+ ["To Gods Room From Anansi", "Gods Room"],
+ ["To Anansi From Gods Room", "Anansi"],
+ ["To Anansi From Werewolf", "Anansi"],
+ ["To Werewolf From Anansi", "Werewolf"],
+ ["To Werewolf From Night Staircase", "Werewolf"],
+ ["To Night Staircase From Werewolf", "Night Staircase"],
+ ["To Night Staircase From Janitor Closet", "Night Staircase"],
+ ["To Night Staircase From UFO", "Night Staircase"],
+ ["To Janitor Closet", "Janitor Closet"],
+ ["To UFO", "UFO"],
+ ["To UFO From Inventions", "UFO"],
+ ["To Inventions From UFO", "Inventions"],
+ ["To Inventions From Blue Maze", "Inventions"],
+ ["To Inventions From Torture", "Inventions"],
+ ["To Fortune Teller", "Fortune Teller"],
+ ["To Torture", "Torture"],
+ ["To Torture From Inventions", "Torture"],
+ ["To Puzzle Room Mastermind From Torture", "Puzzle Room Mastermind"],
+ ["To Puzzle Room Mastermind From Puzzle Room Marbles", "Puzzle Room Mastermind"],
+ ["To Puzzle Room Marbles From Puzzle Room Mastermind", "Puzzle Room Marbles"],
+ ["To Puzzle Room Marbles From Skull Dial Bridge", "Puzzle Room Marbles"],
+ ["To Skull Dial Bridge From Puzzle Room Marbles", "Skull Dial Bridge"],
+ ["To Skull Dial Bridge From Slide Room", "Skull Dial Bridge"],
+ ["To Slide Room", "Slide Room"],
+ ["To Wax Capture From Library", "Wax Capture"],
+ ["To Wax Capture From Shaman", "Wax Capture"],
+ ["To Wax Capture From Anansi", "Wax Capture"],
+ ["To Water Capture From Lobby", "Water Capture"],
+ ["To Water Capture From Janitor Closet", "Water Capture"],
+ ["To Ash Capture From Office", "Ash Capture"],
+ ["To Ash Capture From Burial", "Ash Capture"],
+ ["To Oil Capture From Prehistoric", "Oil Capture"],
+ ["To Oil Capture From Tar River", "Oil Capture"],
+ ["To Cloth Capture From Egypt", "Cloth Capture"],
+ ["To Cloth Capture From Burial", "Cloth Capture"],
+ ["To Cloth Capture From Janitor Closet", "Cloth Capture"],
+ ["To Wood Capture From Workshop", "Wood Capture"],
+ ["To Wood Capture From Gods Room", "Wood Capture"],
+ ["To Wood Capture From Anansi", "Wood Capture"],
+ ["To Wood Capture From Blue Maze", "Wood Capture"],
+ ["To Crystal Capture From Lobby", "Crystal Capture"],
+ ["To Crystal Capture From Ocean", "Crystal Capture"],
+ ["To Sand Capture From Greenhouse", "Sand Capture"],
+ ["To Sand Capture From Ocean", "Sand Capture"],
+ ["To Metal Capture From Bedroom", "Metal Capture"],
+ ["To Metal Capture From Projector Room", "Metal Capture"],
+ ["To Metal Capture From Prehistoric", "Metal Capture"]
+ ]
+}
diff --git a/worlds/shivers/docs/en_Shivers.md b/worlds/shivers/docs/en_Shivers.md
new file mode 100644
index 000000000000..2c56152a7a0c
--- /dev/null
+++ b/worlds/shivers/docs/en_Shivers.md
@@ -0,0 +1,31 @@
+# Shivers
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
+configuration file.
+
+## What does randomization do to this game?
+
+All Ixupi pot pieces are randomized. Keys have been added to the game to lock off different rooms in the museum,
+these are randomized. Crawling has been added and is required to use any crawl space.
+
+## What is considered a location check in Shivers?
+
+1. All puzzle solves are location checks.
+2. All Ixupi captures are location checks.
+3. Puzzle hints/solutions are location checks. For example, looking at the Atlantis map.
+4. Optionally information plaques are location checks.
+
+## When the player receives an item, what happens?
+
+If the player receives a key then the corresponding door will be unlocked. If the player receives a pot piece, it is placed into a pot piece storage location.
+
+## What is the victory condition?
+
+Victory is achieved when the player has captured the required number Ixupi set in their options.
+
+## Encountered a bug?
+
+Please contact GodlFire on Discord for bugs related to Shivers world generation.
+Please contact GodlFire or mouse on Discord for bugs related to the Shivers Randomizer.
diff --git a/worlds/shivers/docs/setup_en.md b/worlds/shivers/docs/setup_en.md
new file mode 100644
index 000000000000..c53edcdf2b57
--- /dev/null
+++ b/worlds/shivers/docs/setup_en.md
@@ -0,0 +1,60 @@
+# Shivers Randomizer Setup Guide
+
+
+## Required Software
+
+- [Shivers (GOG version)](https://www.gog.com/en/game/shivers) or original disc
+- [ScummVM](https://www.scummvm.org/downloads/) version 2.7.0 or later
+- [Shivers Randomizer](https://github.com/GodlFire/Shivers-Randomizer-CSharp/releases/latest) Latest release version
+
+## Setup ScummVM for Shivers
+
+### GOG version of Shivers
+
+1. Launch ScummVM
+2. Click Add Game...
+3. Locate the folder for Shivers (typically in GOG Galaxy\Games\Shivers)
+4. Click OK
+
+### Disc copy of Shivers
+
+1. Copy contents of Shivers disc to a desired location on your computer
+2. Launch ScummVM
+3. Click Add Game...
+4. Locate the folder for Shivers and click Choose
+5. Click OK
+
+## Create a Config (.yaml) File
+
+### What is a config file and why do I need one?
+
+See the guide on setting up a basic YAML at the Archipelago setup
+guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
+
+### Where do I get a config file?
+
+The Player Options page on the website allows you to configure your personal options and export a config file from
+them. Player options page: [Shivers Player Options Page](/games/Shivers/player-options)
+
+### 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)
+
+## Joining a MultiWorld Game
+
+1. Launch ScummVM
+2. Highlight Shivers and click "Start"
+3. Launch the Shivers Randomizer
+4. Click "Attach"
+5. Click "Archipelago"
+6. Enter the Archipelago server address, slot name, and password
+7. Click "Connect"
+8. In Shivers click "New Game"
+
+## What is a check
+
+- Every puzzle
+- Every puzzle hint/solution
+- Every document that is considered a Flashback
+- Optionally information plaques.
diff --git a/worlds/shorthike/Items.py b/worlds/shorthike/Items.py
new file mode 100644
index 000000000000..7a5a81db9be6
--- /dev/null
+++ b/worlds/shorthike/Items.py
@@ -0,0 +1,62 @@
+from BaseClasses import ItemClassification
+from typing import TypedDict, Dict, List, Set
+
+class ItemDict(TypedDict):
+ name: str
+ id: int
+ count: int
+ classification: ItemClassification
+
+base_id = 82000
+
+item_table: List[ItemDict] = [
+ {"name": "Stick", "id": base_id + 1, "count": 0, "classification": ItemClassification.progression_skip_balancing},
+ {"name": "Seashell", "id": base_id + 2, "count": 23, "classification": ItemClassification.progression_skip_balancing},
+ {"name": "Golden Feather", "id": base_id + 3, "count": 0, "classification": ItemClassification.progression},
+ {"name": "Silver Feather", "id": base_id + 4, "count": 0, "classification": ItemClassification.useful},
+ {"name": "Bucket", "id": base_id + 5, "count": 0, "classification": ItemClassification.progression},
+ {"name": "Bait", "id": base_id + 6, "count": 2, "classification": ItemClassification.filler},
+ {"name": "Progressive Fishing Rod", "id": base_id + 7, "count": 2, "classification": ItemClassification.progression},
+ {"name": "Shovel", "id": base_id + 8, "count": 1, "classification": ItemClassification.progression},
+ {"name": "Toy Shovel", "id": base_id + 9, "count": 0, "classification": ItemClassification.progression_skip_balancing},
+ {"name": "Compass", "id": base_id + 10, "count": 1, "classification": ItemClassification.useful},
+ {"name": "Medal", "id": base_id + 11, "count": 3, "classification": ItemClassification.filler},
+ {"name": "Shell Necklace", "id": base_id + 12, "count": 1, "classification": ItemClassification.progression},
+ {"name": "Wristwatch", "id": base_id + 13, "count": 1, "classification": ItemClassification.progression},
+ {"name": "Motorboat Key", "id": base_id + 14, "count": 1, "classification": ItemClassification.progression},
+ {"name": "Pickaxe", "id": base_id + 15, "count": 3, "classification": ItemClassification.useful},
+ {"name": "Fishing Journal", "id": base_id + 16, "count": 1, "classification": ItemClassification.useful},
+ {"name": "A Stormy View Map", "id": base_id + 17, "count": 1, "classification": ItemClassification.filler},
+ {"name": "The King Map", "id": base_id + 18, "count": 1, "classification": ItemClassification.filler},
+ {"name": "The Treasure of Sid Beach Map", "id": base_id + 19, "count": 1, "classification": ItemClassification.filler},
+ {"name": "In Her Shadow Map", "id": base_id + 20, "count": 1, "classification": ItemClassification.filler},
+ {"name": "Sunhat", "id": base_id + 21, "count": 1, "classification": ItemClassification.filler},
+ {"name": "Baseball Cap", "id": base_id + 22, "count": 1, "classification": ItemClassification.filler},
+ {"name": "Provincial Park Hat", "id": base_id + 23, "count": 1, "classification": ItemClassification.filler},
+ {"name": "Headband", "id": base_id + 24, "count": 1, "classification": ItemClassification.progression},
+ {"name": "Running Shoes", "id": base_id + 25, "count": 1, "classification": ItemClassification.useful},
+ {"name": "Camping Permit", "id": base_id + 26, "count": 1, "classification": ItemClassification.progression},
+ {"name": "Walkie Talkie", "id": base_id + 27, "count": 0, "classification": ItemClassification.useful},
+
+ # Not in the item pool for now
+ #{"name": "Boating Manual", "id": base_id + ~, "count": 1, "classification": ItemClassification.filler},
+
+ # Different Coin Amounts (Fillers)
+ {"name": "7 Coins", "id": base_id + 28, "count": 3, "classification": ItemClassification.filler},
+ {"name": "15 Coins", "id": base_id + 29, "count": 1, "classification": ItemClassification.filler},
+ {"name": "18 Coins", "id": base_id + 30, "count": 1, "classification": ItemClassification.filler},
+ {"name": "21 Coins", "id": base_id + 31, "count": 2, "classification": ItemClassification.filler},
+ {"name": "25 Coins", "id": base_id + 32, "count": 7, "classification": ItemClassification.filler},
+ {"name": "27 Coins", "id": base_id + 33, "count": 1, "classification": ItemClassification.filler},
+ {"name": "32 Coins", "id": base_id + 34, "count": 1, "classification": ItemClassification.useful},
+ {"name": "33 Coins", "id": base_id + 35, "count": 6, "classification": ItemClassification.useful},
+ {"name": "50 Coins", "id": base_id + 36, "count": 1, "classification": ItemClassification.useful},
+
+ # Filler item determined by settings
+ {"name": "13 Coins", "id": base_id + 37, "count": 0, "classification": ItemClassification.filler},
+]
+
+group_table: Dict[str, Set[str]] = {
+ "Coins": {"7 Coins", "13 Coins", "15 Coins", "18 Coins", "21 Coins", "25 Coins", "27 Coins", "32 Coins", "33 Coins", "50 Coins"},
+ "Maps": {"A Stormy View Map", "The King Map", "The Treasure of Sid Beach Map", "In Her Shadow Map"},
+}
diff --git a/worlds/shorthike/Locations.py b/worlds/shorthike/Locations.py
new file mode 100644
index 000000000000..657035a03011
--- /dev/null
+++ b/worlds/shorthike/Locations.py
@@ -0,0 +1,709 @@
+from typing import List, TypedDict
+
+class LocationInfo(TypedDict):
+ name: str
+ id: int
+ inGameId: str
+ needsShovel: bool
+ purchase: int
+ minGoldenFeathers: int
+ minGoldenFeathersEasy: int
+ minGoldenFeathersBucket: int
+
+base_id = 83000
+
+location_table: List[LocationInfo] = [
+ # Original Seashell Locations
+ {"name": "Start Beach Seashell",
+ "id": base_id + 1,
+ "inGameId": "PickUps.3",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Beach Hut Seashell",
+ "id": base_id + 2,
+ "inGameId": "PickUps.2",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Beach Umbrella Seashell",
+ "id": base_id + 3,
+ "inGameId": "PickUps.8",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Sid Beach Mound Seashell",
+ "id": base_id + 4,
+ "inGameId": "PickUps.12",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Sid Beach Seashell",
+ "id": base_id + 5,
+ "inGameId": "PickUps.11",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Shirley's Point Beach Seashell",
+ "id": base_id + 6,
+ "inGameId": "PickUps.18",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Shirley's Point Rock Seashell",
+ "id": base_id + 7,
+ "inGameId": "PickUps.17",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Visitor's Center Beach Seashell",
+ "id": base_id + 8,
+ "inGameId": "PickUps.19",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "West River Seashell",
+ "id": base_id + 9,
+ "inGameId": "PickUps.10",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "West Riverbank Seashell",
+ "id": base_id + 10,
+ "inGameId": "PickUps.4",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Stone Tower Riverbank Seashell",
+ "id": base_id + 11,
+ "inGameId": "PickUps.23",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "North Beach Seashell",
+ "id": base_id + 12,
+ "inGameId": "PickUps.6",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "North Coast Seashell",
+ "id": base_id + 13,
+ "inGameId": "PickUps.7",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Boat Cliff Seashell",
+ "id": base_id + 14,
+ "inGameId": "PickUps.14",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Boat Isle Mound Seashell",
+ "id": base_id + 15,
+ "inGameId": "PickUps.22",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "East Coast Seashell",
+ "id": base_id + 16,
+ "inGameId": "PickUps.21",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "House North Beach Seashell",
+ "id": base_id + 17,
+ "inGameId": "PickUps.16",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Airstream Island North Seashell",
+ "id": base_id + 18,
+ "inGameId": "PickUps.13",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Airstream Island South Seashell",
+ "id": base_id + 19,
+ "inGameId": "PickUps.15",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Secret Island Beach Seashell",
+ "id": base_id + 20,
+ "inGameId": "PickUps.1",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Meteor Lake Seashell",
+ "id": base_id + 126,
+ "inGameId": "PickUps.20",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Good Creek Path Seashell",
+ "id": base_id + 127,
+ "inGameId": "PickUps.9",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+
+ # Visitor's Center Shop
+ {"name": "Visitor's Center Shop Golden Feather 1",
+ "id": base_id + 21,
+ "inGameId": "CampRangerNPC[0]",
+ "needsShovel": False, "purchase": 40,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Visitor's Center Shop Golden Feather 2",
+ "id": base_id + 22,
+ "inGameId": "CampRangerNPC[1]",
+ "needsShovel": False, "purchase": 40,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Visitor's Center Shop Hat",
+ "id": base_id + 23,
+ "inGameId": "CampRangerNPC[9]",
+ "needsShovel": False, "purchase": 100,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+
+ # Tough Bird Salesman
+ {"name": "Tough Bird Salesman Golden Feather 1",
+ "id": base_id + 24,
+ "inGameId": "ToughBirdNPC (1)[0]",
+ "needsShovel": False, "purchase": 100,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Tough Bird Salesman Golden Feather 2",
+ "id": base_id + 25,
+ "inGameId": "ToughBirdNPC (1)[1]",
+ "needsShovel": False, "purchase": 100,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Tough Bird Salesman Golden Feather 3",
+ "id": base_id + 26,
+ "inGameId": "ToughBirdNPC (1)[2]",
+ "needsShovel": False, "purchase": 100,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Tough Bird Salesman Golden Feather 4",
+ "id": base_id + 27,
+ "inGameId": "ToughBirdNPC (1)[3]",
+ "needsShovel": False, "purchase": 100,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Tough Bird Salesman (400 Coins)",
+ "id": base_id + 28,
+ "inGameId": "ToughBirdNPC (1)[9]",
+ "needsShovel": False, "purchase": 400,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+
+ # Beachstickball
+ {"name": "Beachstickball (10 Hits)",
+ "id": base_id + 29,
+ "inGameId": "VolleyballOpponent[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Beachstickball (20 Hits)",
+ "id": base_id + 30,
+ "inGameId": "VolleyballOpponent[1]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Beachstickball (30 Hits)",
+ "id": base_id + 31,
+ "inGameId": "VolleyballOpponent[2]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+
+ # Misc Item Locations
+ {"name": "Shovel Kid Trade",
+ "id": base_id + 32,
+ "inGameId": "Frog_StandingNPC[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Compass Guy",
+ "id": base_id + 33,
+ "inGameId": "Fox_WalkingNPC[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Hawk Peak Bucket Rock",
+ "id": base_id + 34,
+ "inGameId": "Tools.23",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Orange Islands Bucket Rock",
+ "id": base_id + 35,
+ "inGameId": "Tools.42",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Bill the Walrus Fisherman",
+ "id": base_id + 36,
+ "inGameId": "SittingNPC (1)[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Catch 3 Fish Reward",
+ "id": base_id + 37,
+ "inGameId": "FishBuyer[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Catch All Fish Reward",
+ "id": base_id + 38,
+ "inGameId": "FishBuyer[1]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 7, "minGoldenFeathersEasy": 9, "minGoldenFeathersBucket": 7},
+ {"name": "Permit Guy Bribe",
+ "id": base_id + 39,
+ "inGameId": "CamperNPC[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Catch Fish with Permit",
+ "id": base_id + 129,
+ "inGameId": "Player[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Return Camping Permit",
+ "id": base_id + 130,
+ "inGameId": "CamperNPC[1]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+
+ # Original Pickaxe Locations
+ {"name": "Blocked Mine Pickaxe 1",
+ "id": base_id + 40,
+ "inGameId": "Tools.31",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Blocked Mine Pickaxe 2",
+ "id": base_id + 41,
+ "inGameId": "Tools.32",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Blocked Mine Pickaxe 3",
+ "id": base_id + 42,
+ "inGameId": "Tools.33",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+
+ # Original Toy Shovel Locations
+ {"name": "Blackwood Trail Lookout Toy Shovel",
+ "id": base_id + 43,
+ "inGameId": "PickUps.27",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Shirley's Point Beach Toy Shovel",
+ "id": base_id + 44,
+ "inGameId": "PickUps.30",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Visitor's Center Beach Toy Shovel",
+ "id": base_id + 45,
+ "inGameId": "PickUps.29",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Blackwood Trail Rock Toy Shovel",
+ "id": base_id + 46,
+ "inGameId": "PickUps.26",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Beach Hut Cliff Toy Shovel",
+ "id": base_id + 128,
+ "inGameId": "PickUps.28",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+
+ # Original Stick Locations
+ {"name": "Secret Island Beach Trail Stick",
+ "id": base_id + 47,
+ "inGameId": "PickUps.25",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Below Lighthouse Walkway Stick",
+ "id": base_id + 48,
+ "inGameId": "Tools.3",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Beach Hut Rocky Pool Sand Stick",
+ "id": base_id + 49,
+ "inGameId": "Tools.0",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Cliff Overlooking West River Waterfall Stick",
+ "id": base_id + 50,
+ "inGameId": "Tools.2",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 2, "minGoldenFeathersBucket": 0},
+ {"name": "Trail to Tough Bird Salesman Stick",
+ "id": base_id + 51,
+ "inGameId": "Tools.8",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "North Beach Stick",
+ "id": base_id + 52,
+ "inGameId": "Tools.4",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Beachstickball Court Stick",
+ "id": base_id + 53,
+ "inGameId": "VolleyballMinigame.4",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Stick Under Sid Beach Umbrella",
+ "id": base_id + 54,
+ "inGameId": "Tools.1",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+
+ # Boating
+ {"name": "Boat Rental",
+ "id": base_id + 55,
+ "inGameId": "DadDeer[0]",
+ "needsShovel": False, "purchase": 100,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Boat Challenge Reward",
+ "id": base_id + 56,
+ "inGameId": "DeerKidBoat[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+
+ # Not a location for now, corresponding with the Boating Manual
+ # {"name": "Receive Boating Manual",
+ # "id": base_id + 133,
+ # "inGameId": "DadDeer[1]",
+ # "needsShovel": False, "purchase": 0,
+ # "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+
+ # Original Map Locations
+ {"name": "Outlook Point Dog Gift",
+ "id": base_id + 57,
+ "inGameId": "Dog_WalkingNPC_BlueEyed[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+
+ # Original Clothes Locations
+ {"name": "Collect 15 Seashells",
+ "id": base_id + 58,
+ "inGameId": "LittleKidNPCVariant (1)[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Return to Shell Kid",
+ "id": base_id + 132,
+ "inGameId": "LittleKidNPCVariant (1)[1]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Taylor the Turtle Headband Gift",
+ "id": base_id + 59,
+ "inGameId": "Turtle_WalkingNPC[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Sue the Rabbit Shoes Reward",
+ "id": base_id + 60,
+ "inGameId": "Bunny_WalkingNPC (1)[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Purchase Sunhat",
+ "id": base_id + 61,
+ "inGameId": "SittingNPC[0]",
+ "needsShovel": False, "purchase": 100,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+
+ # Original Golden Feather Locations
+ {"name": "Blackwood Forest Golden Feather",
+ "id": base_id + 62,
+ "inGameId": "Feathers.3",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Ranger May Shell Necklace Golden Feather",
+ "id": base_id + 63,
+ "inGameId": "AuntMayNPC[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Sand Castle Golden Feather",
+ "id": base_id + 64,
+ "inGameId": "SandProvince.3",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Artist Golden Feather",
+ "id": base_id + 65,
+ "inGameId": "StandingNPC[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Visitor Camp Rock Golden Feather",
+ "id": base_id + 66,
+ "inGameId": "Feathers.8",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Outlook Cliff Golden Feather",
+ "id": base_id + 67,
+ "inGameId": "Feathers.2",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Meteor Lake Cliff Golden Feather",
+ "id": base_id + 68,
+ "inGameId": "Feathers.7",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 0},
+
+ # Original Silver Feather Locations
+ {"name": "Secret Island Peak",
+ "id": base_id + 69,
+ "inGameId": "PickUps.24",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 5, "minGoldenFeathersEasy": 7, "minGoldenFeathersBucket": 7},
+ {"name": "Wristwatch Trade",
+ "id": base_id + 70,
+ "inGameId": "Goat_StandingNPC[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+
+ # Golden Chests
+ {"name": "Lighthouse Golden Chest",
+ "id": base_id + 71,
+ "inGameId": "Feathers.0",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 2, "minGoldenFeathersEasy": 3, "minGoldenFeathersBucket": 0},
+ {"name": "Outlook Golden Chest",
+ "id": base_id + 72,
+ "inGameId": "Feathers.6",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Stone Tower Golden Chest",
+ "id": base_id + 73,
+ "inGameId": "Feathers.5",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "North Cliff Golden Chest",
+ "id": base_id + 74,
+ "inGameId": "Feathers.4",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 3, "minGoldenFeathersEasy": 10, "minGoldenFeathersBucket": 10},
+
+ # Chests
+ {"name": "Blackwood Cliff Chest",
+ "id": base_id + 75,
+ "inGameId": "Coins.22",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "White Coast Trail Chest",
+ "id": base_id + 76,
+ "inGameId": "Coins.6",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Sid Beach Chest",
+ "id": base_id + 77,
+ "inGameId": "Coins.7",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Sid Beach Buried Treasure Chest",
+ "id": base_id + 78,
+ "inGameId": "Coins.46",
+ "needsShovel": True, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Sid Beach Cliff Chest",
+ "id": base_id + 79,
+ "inGameId": "Coins.9",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Visitor's Center Buried Chest",
+ "id": base_id + 80,
+ "inGameId": "Coins.94",
+ "needsShovel": True, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Visitor's Center Hidden Chest",
+ "id": base_id + 81,
+ "inGameId": "Coins.42",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Shirley's Point Chest",
+ "id": base_id + 82,
+ "inGameId": "Coins.10",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 1, "minGoldenFeathersEasy": 2, "minGoldenFeathersBucket": 2},
+ {"name": "Caravan Cliff Chest",
+ "id": base_id + 83,
+ "inGameId": "Coins.12",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Caravan Arch Chest",
+ "id": base_id + 84,
+ "inGameId": "Coins.11",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "King Buried Treasure Chest",
+ "id": base_id + 85,
+ "inGameId": "Coins.41",
+ "needsShovel": True, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Good Creek Path Buried Chest",
+ "id": base_id + 86,
+ "inGameId": "Coins.48",
+ "needsShovel": True, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Good Creek Path West Chest",
+ "id": base_id + 87,
+ "inGameId": "Coins.33",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Good Creek Path East Chest",
+ "id": base_id + 88,
+ "inGameId": "Coins.62",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "West Waterfall Chest",
+ "id": base_id + 89,
+ "inGameId": "Coins.20",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Stone Tower West Cliff Chest",
+ "id": base_id + 90,
+ "inGameId": "PickUps.0",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Bucket Path Chest",
+ "id": base_id + 91,
+ "inGameId": "Coins.50",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Bucket Cliff Chest",
+ "id": base_id + 92,
+ "inGameId": "Coins.49",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 3, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 5},
+ {"name": "In Her Shadow Buried Treasure Chest",
+ "id": base_id + 93,
+ "inGameId": "Feathers.9",
+ "needsShovel": True, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Meteor Lake Buried Chest",
+ "id": base_id + 94,
+ "inGameId": "Coins.86",
+ "needsShovel": True, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Meteor Lake Chest",
+ "id": base_id + 95,
+ "inGameId": "Coins.64",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "House North Beach Chest",
+ "id": base_id + 96,
+ "inGameId": "Coins.65",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "East Coast Chest",
+ "id": base_id + 97,
+ "inGameId": "Coins.98",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Fisherman's Boat Chest 1",
+ "id": base_id + 99,
+ "inGameId": "Boat.0",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Fisherman's Boat Chest 2",
+ "id": base_id + 100,
+ "inGameId": "Boat.7",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Airstream Island Chest",
+ "id": base_id + 101,
+ "inGameId": "Coins.31",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "West River Waterfall Head Chest",
+ "id": base_id + 102,
+ "inGameId": "Coins.34",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Old Building Chest",
+ "id": base_id + 103,
+ "inGameId": "Coins.104",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Old Building West Chest",
+ "id": base_id + 104,
+ "inGameId": "Coins.109",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Old Building East Chest",
+ "id": base_id + 105,
+ "inGameId": "Coins.8",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Hawk Peak West Chest",
+ "id": base_id + 106,
+ "inGameId": "Coins.21",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 3, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 5},
+ {"name": "Hawk Peak East Buried Chest",
+ "id": base_id + 107,
+ "inGameId": "Coins.76",
+ "needsShovel": True, "purchase": 0,
+ "minGoldenFeathers": 3, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 5},
+ {"name": "Hawk Peak Northeast Chest",
+ "id": base_id + 108,
+ "inGameId": "Coins.79",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 3, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 5},
+ {"name": "Northern East Coast Chest",
+ "id": base_id + 109,
+ "inGameId": "Coins.45",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 2, "minGoldenFeathersBucket": 0},
+ {"name": "North Coast Chest",
+ "id": base_id + 110,
+ "inGameId": "Coins.28",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "North Coast Buried Chest",
+ "id": base_id + 111,
+ "inGameId": "Coins.47",
+ "needsShovel": True, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Small South Island Buried Chest",
+ "id": base_id + 112,
+ "inGameId": "Coins.87",
+ "needsShovel": True, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Secret Island Bottom Chest",
+ "id": base_id + 113,
+ "inGameId": "Coins.88",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Secret Island Treehouse Chest",
+ "id": base_id + 114,
+ "inGameId": "Coins.89",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 1, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 1},
+ {"name": "Sunhat Island Buried Chest",
+ "id": base_id + 115,
+ "inGameId": "Coins.112",
+ "needsShovel": True, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Orange Islands South Buried Chest",
+ "id": base_id + 116,
+ "inGameId": "Coins.119",
+ "needsShovel": True, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Orange Islands West Chest",
+ "id": base_id + 117,
+ "inGameId": "Coins.121",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Orange Islands North Buried Chest",
+ "id": base_id + 118,
+ "inGameId": "Coins.117",
+ "needsShovel": True, "purchase": 0,
+ "minGoldenFeathers": 1, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
+ {"name": "Orange Islands East Chest",
+ "id": base_id + 119,
+ "inGameId": "Coins.120",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Orange Islands South Hidden Chest",
+ "id": base_id + 120,
+ "inGameId": "Coins.124",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "A Stormy View Buried Treasure Chest",
+ "id": base_id + 121,
+ "inGameId": "Coins.113",
+ "needsShovel": True, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+ {"name": "Orange Islands Ruins Buried Chest",
+ "id": base_id + 122,
+ "inGameId": "Coins.118",
+ "needsShovel": True, "purchase": 0,
+ "minGoldenFeathers": 2, "minGoldenFeathersEasy": 4, "minGoldenFeathersBucket": 0},
+
+ # Race Rewards
+ {"name": "Lighthouse Race Reward",
+ "id": base_id + 123,
+ "inGameId": "RaceOpponent[0]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 2, "minGoldenFeathersEasy": 3, "minGoldenFeathersBucket": 1},
+ {"name": "Old Building Race Reward",
+ "id": base_id + 124,
+ "inGameId": "RaceOpponent[1]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 1, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 0},
+ {"name": "Hawk Peak Race Reward",
+ "id": base_id + 125,
+ "inGameId": "RaceOpponent[2]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 7, "minGoldenFeathersEasy": 9, "minGoldenFeathersBucket": 7},
+ {"name": "Lose Race Gift",
+ "id": base_id + 131,
+ "inGameId": "RaceOpponent[9]",
+ "needsShovel": False, "purchase": 0,
+ "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
+]
diff --git a/worlds/shorthike/Options.py b/worlds/shorthike/Options.py
new file mode 100644
index 000000000000..3d9bf81a3cf8
--- /dev/null
+++ b/worlds/shorthike/Options.py
@@ -0,0 +1,170 @@
+from dataclasses import dataclass
+from Options import Choice, OptionGroup, PerGameCommonOptions, Range, StartInventoryPool, Toggle, DefaultOnToggle
+
+class Goal(Choice):
+ """Choose the end goal.
+ Nap: Complete the climb to the top of Hawk Peak and take a nap
+ Photo: Get your picture taken at the top of Hawk Peak
+ Races: Complete all three races with Avery
+ Help Everyone: Travel around Hawk Peak and help every character with their troubles
+ Fishmonger: Catch one of every fish from around Hawk Peak"""
+ display_name = "Goal"
+ option_nap = 0
+ option_photo = 1
+ option_races = 2
+ option_help_everyone = 3
+ option_fishmonger = 4
+ default = 3
+
+class CoinsInShops(Toggle):
+ """When enabled, the randomizer can place coins into locations that are purchased, such as shops."""
+ display_name = "Coins in Purchaseable Locations"
+ default = False
+
+class GoldenFeathers(Range):
+ """
+ Number of Golden Feathers in the item pool.
+ (Note that for the Photo and Help Everyone goals, a minimum of 12 Golden Feathers is enforced)
+ """
+ display_name = "Golden Feathers"
+ range_start = 0
+ range_end = 20
+ default = 20
+
+class SilverFeathers(Range):
+ """Number of Silver Feathers in the item pool."""
+ display_name = "Silver Feathers"
+ range_start = 0
+ range_end = 20
+ default = 2
+
+class Buckets(Range):
+ """Number of Buckets in the item pool."""
+ display_name = "Buckets"
+ range_start = 0
+ range_end = 2
+ default = 2
+
+class Sticks(Range):
+ """Number of Sticks in the item pool."""
+ display_name = "Sticks"
+ range_start = 1
+ range_end = 8
+ default = 8
+
+class ToyShovels(Range):
+ """Number of Toy Shovels in the item pool."""
+ display_name = "Toy Shovels"
+ range_start = 1
+ range_end = 5
+ default = 5
+
+class GoldenFeatherProgression(Choice):
+ """Determines which locations are considered in logic based on the required amount of golden feathers to reach them.
+ Easy: Locations will be considered inaccessible until the player has enough golden feathers to easily reach them. A minimum of 10 golden feathers is recommended for this setting.
+ Normal: Locations will be considered inaccessible until the player has the minimum possible number of golden feathers to reach them. A minimum of 7 golden feathers is recommended for this setting.
+ Hard: Removes the requirement of golden feathers for progression entirely and glitches may need to be used to progress"""
+ display_name = "Golden Feather Progression"
+ option_easy = 0
+ option_normal = 1
+ option_hard = 2
+ default = 1
+
+class CostMultiplier(Range):
+ """The percentage that all item shop costs will be of the vanilla values."""
+ display_name = "Shop Cost Multiplier"
+ range_start = 25
+ range_end = 200
+ default = 100
+
+class FillerCoinAmount(Choice):
+ """The number of coins that will be in each filler coin item."""
+ display_name = "Coins per Filler Item"
+ option_7_coins = 0
+ option_13_coins = 1
+ option_15_coins = 2
+ option_18_coins = 3
+ option_21_coins = 4
+ option_25_coins = 5
+ option_27_coins = 6
+ option_32_coins = 7
+ option_33_coins = 8
+ option_50_coins = 9
+ default = 1
+
+class RandomWalkieTalkie(DefaultOnToggle):
+ """
+ When enabled, the Walkie Talkie item will be placed into the item pool. Otherwise, it will be placed in its vanilla location.
+ This item usually allows the player to locate Avery around the map or restart a race.
+ """
+ display_name = "Randomize Walkie Talkie"
+
+class EasierRaces(Toggle):
+ """When enabled, the Running Shoes will be added as a logical requirement for beating any of the races."""
+ display_name = "Easier Races"
+
+class ShopCheckLogic(Choice):
+ """Determines which items will be added as logical requirements to making certain purchases in shops."""
+ display_name = "Shop Check Logic"
+ option_nothing = 0
+ option_fishing_rod = 1
+ option_shovel = 2
+ option_fishing_rod_and_shovel = 3
+ option_golden_fishing_rod = 4
+ option_golden_fishing_rod_and_shovel = 5
+ default = 1
+
+class MinShopCheckLogic(Choice):
+ """
+ Determines the minimum cost of a shop item that will have the shop check logic applied to it.
+ If the cost of a shop item is less than this value, no items will be required to access it.
+ This is based on the vanilla prices of the shop item. The set cost multiplier will not affect this value.
+ """
+ display_name = "Minimum Shop Check Logic Application"
+ option_40_coins = 0
+ option_100_coins = 1
+ option_400_coins = 2
+ default = 1
+
+@dataclass
+class ShortHikeOptions(PerGameCommonOptions):
+ start_inventory_from_pool: StartInventoryPool
+ goal: Goal
+ coins_in_shops: CoinsInShops
+ golden_feathers: GoldenFeathers
+ silver_feathers: SilverFeathers
+ buckets: Buckets
+ sticks: Sticks
+ toy_shovels: ToyShovels
+ golden_feather_progression: GoldenFeatherProgression
+ cost_multiplier: CostMultiplier
+ filler_coin_amount: FillerCoinAmount
+ random_walkie_talkie: RandomWalkieTalkie
+ easier_races: EasierRaces
+ shop_check_logic: ShopCheckLogic
+ min_shop_check_logic: MinShopCheckLogic
+
+shorthike_option_groups = [
+ OptionGroup("General Options", [
+ Goal,
+ FillerCoinAmount,
+ RandomWalkieTalkie
+ ]),
+ OptionGroup("Logic Options", [
+ GoldenFeatherProgression,
+ EasierRaces
+ ]),
+ OptionGroup("Item Pool Options", [
+ GoldenFeathers,
+ SilverFeathers,
+ Buckets,
+ Sticks,
+ ToyShovels
+ ]),
+ OptionGroup("Shop Options", [
+ CoinsInShops,
+ CostMultiplier,
+ ShopCheckLogic,
+ MinShopCheckLogic
+ ])
+]
diff --git a/worlds/shorthike/Rules.py b/worlds/shorthike/Rules.py
new file mode 100644
index 000000000000..33741c6d80c6
--- /dev/null
+++ b/worlds/shorthike/Rules.py
@@ -0,0 +1,106 @@
+from worlds.generic.Rules import forbid_items_for_player, add_rule
+from .Options import Goal, GoldenFeatherProgression, MinShopCheckLogic, ShopCheckLogic
+
+
+def create_rules(self, location_table):
+ multiworld = self.multiworld
+ player = self.player
+ options = self.options
+
+ # Shovel Rules
+ for loc in location_table:
+ if loc["needsShovel"]:
+ forbid_items_for_player(multiworld.get_location(loc["name"], player), self.item_name_groups['Maps'], player)
+ add_rule(multiworld.get_location(loc["name"], player),
+ lambda state: state.has("Shovel", player))
+
+ # Shop Rules
+ if loc["purchase"] and not options.coins_in_shops:
+ forbid_items_for_player(multiworld.get_location(loc["name"], player), self.item_name_groups['Coins'], player)
+ if loc["purchase"] >= get_min_shop_logic_cost(self) and options.shop_check_logic != ShopCheckLogic.option_nothing:
+ if options.shop_check_logic in {ShopCheckLogic.option_fishing_rod, ShopCheckLogic.option_fishing_rod_and_shovel}:
+ add_rule(multiworld.get_location(loc["name"], player),
+ lambda state: state.has("Progressive Fishing Rod", player))
+ if options.shop_check_logic in {ShopCheckLogic.option_golden_fishing_rod, ShopCheckLogic.option_golden_fishing_rod_and_shovel}:
+ add_rule(multiworld.get_location(loc["name"], player),
+ lambda state: state.has("Progressive Fishing Rod", player, 2))
+ if options.shop_check_logic in {ShopCheckLogic.option_shovel, ShopCheckLogic.option_fishing_rod_and_shovel, ShopCheckLogic.option_golden_fishing_rod_and_shovel}:
+ add_rule(multiworld.get_location(loc["name"], player),
+ lambda state: state.has("Shovel", player))
+
+ # Minimum Feather Rules
+ if options.golden_feather_progression != GoldenFeatherProgression.option_hard:
+ min_feathers = get_min_feathers(self, loc["minGoldenFeathers"], loc["minGoldenFeathersEasy"])
+
+ if options.buckets > 0 and loc["minGoldenFeathersBucket"] < min_feathers:
+ add_rule(multiworld.get_location(loc["name"], player),
+ lambda state, loc=loc, min_feathers=min_feathers: state.has("Golden Feather", player, min_feathers)
+ or (state.has("Bucket", player) and state.has("Golden Feather", player, loc["minGoldenFeathersBucket"])))
+ elif min_feathers > 0:
+ add_rule(multiworld.get_location(loc["name"], player),
+ lambda state, min_feathers=min_feathers: state.has("Golden Feather", player, min_feathers))
+ add_rule(multiworld.get_location("Shovel Kid Trade", player),
+ lambda state: state.has("Toy Shovel", player))
+ add_rule(multiworld.get_location("Sand Castle Golden Feather", player),
+ lambda state: state.has("Toy Shovel", player))
+
+ # Fishing Rules
+ add_rule(multiworld.get_location("Catch 3 Fish Reward", player),
+ lambda state: state.has("Progressive Fishing Rod", player))
+ add_rule(multiworld.get_location("Catch Fish with Permit", player),
+ lambda state: state.has("Progressive Fishing Rod", player))
+ add_rule(multiworld.get_location("Catch All Fish Reward", player),
+ lambda state: state.has("Progressive Fishing Rod", player, 2))
+
+ # Misc Rules
+ add_rule(multiworld.get_location("Return Camping Permit", player),
+ lambda state: state.has("Camping Permit", player))
+ add_rule(multiworld.get_location("Boat Challenge Reward", player),
+ lambda state: state.has("Motorboat Key", player))
+ add_rule(multiworld.get_location("Collect 15 Seashells", player),
+ lambda state: state.has("Seashell", player, 15))
+ add_rule(multiworld.get_location("Wristwatch Trade", player),
+ lambda state: state.has("Wristwatch", player))
+ add_rule(multiworld.get_location("Sue the Rabbit Shoes Reward", player),
+ lambda state: state.has("Headband", player))
+ add_rule(multiworld.get_location("Return to Shell Kid", player),
+ lambda state: state.has("Shell Necklace", player) and state.has("Seashell", player, 15))
+ add_rule(multiworld.get_location("Ranger May Shell Necklace Golden Feather", player),
+ lambda state: state.has("Shell Necklace", player))
+ add_rule(multiworld.get_location("Beachstickball (10 Hits)", player),
+ lambda state: state.has("Stick", player))
+ add_rule(multiworld.get_location("Beachstickball (20 Hits)", player),
+ lambda state: state.has("Stick", player))
+ add_rule(multiworld.get_location("Beachstickball (30 Hits)", player),
+ lambda state: state.has("Stick", player))
+
+ # Race Rules
+ if options.easier_races:
+ add_rule(multiworld.get_location("Lighthouse Race Reward", player),
+ lambda state: state.has("Running Shoes", player))
+ add_rule(multiworld.get_location("Old Building Race Reward", player),
+ lambda state: state.has("Running Shoes", player))
+ add_rule(multiworld.get_location("Hawk Peak Race Reward", player),
+ lambda state: state.has("Running Shoes", player))
+
+def get_min_feathers(self, min_golden_feathers, min_golden_feathers_easy):
+ options = self.options
+
+ min_feathers = min_golden_feathers
+ if options.golden_feather_progression == GoldenFeatherProgression.option_easy:
+ min_feathers = min_golden_feathers_easy
+ if min_feathers > options.golden_feathers:
+ if options.goal not in {Goal.option_help_everyone, Goal.option_photo}:
+ min_feathers = options.golden_feathers
+
+ return min_feathers
+
+def get_min_shop_logic_cost(self):
+ options = self.options
+
+ if options.min_shop_check_logic == MinShopCheckLogic.option_40_coins:
+ return 40
+ elif options.min_shop_check_logic == MinShopCheckLogic.option_100_coins:
+ return 100
+ elif options.min_shop_check_logic == MinShopCheckLogic.option_400_coins:
+ return 400
diff --git a/worlds/shorthike/__init__.py b/worlds/shorthike/__init__.py
new file mode 100644
index 000000000000..299169a40c6b
--- /dev/null
+++ b/worlds/shorthike/__init__.py
@@ -0,0 +1,153 @@
+from typing import ClassVar, Dict, Any, Type
+from BaseClasses import ItemClassification, Region, Location, Item, Tutorial
+from Options import PerGameCommonOptions
+from worlds.AutoWorld import World, WebWorld
+from .Items import item_table, group_table, base_id
+from .Locations import location_table
+from .Rules import create_rules, get_min_feathers
+from .Options import ShortHikeOptions, shorthike_option_groups
+
+class ShortHikeWeb(WebWorld):
+ theme = "ocean"
+ tutorials = [Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to setting up the A Short Hike randomizer connected to an Archipelago Multiworld",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["Chandler"]
+ )]
+ option_groups = shorthike_option_groups
+
+class ShortHikeWorld(World):
+ """
+ A Short Hike is a relaxing adventure set on the islands of Hawk Peak. Fly and climb using Claire's wings and Golden Feathers
+ to make your way up to the summit. Along the way you'll meet other hikers, discover hidden treasures,
+ and take in the beautiful world around you.
+ """
+
+ game = "A Short Hike"
+ web = ShortHikeWeb()
+
+ item_name_to_id = {item["name"]: item["id"] for item in item_table}
+ location_name_to_id = {loc["name"]: loc["id"] for loc in location_table}
+ location_name_to_game_id = {loc["name"]: loc["inGameId"] for loc in location_table}
+
+ item_name_groups = group_table
+
+ options_dataclass: ClassVar[Type[PerGameCommonOptions]] = ShortHikeOptions
+ options: ShortHikeOptions
+
+ required_client_version = (0, 4, 4)
+
+ def get_filler_item_name(self) -> str:
+ return self.options.filler_coin_amount.current_option_name
+
+ def create_item(self, name: str) -> "ShortHikeItem":
+ item_id: int = self.item_name_to_id[name]
+ id = item_id - base_id - 1
+
+ classification = item_table[id]["classification"]
+ if self.options.easier_races and name == "Running Shoes":
+ classification = ItemClassification.progression
+
+ return ShortHikeItem(name, classification, item_id, player=self.player)
+
+ def create_items(self) -> None:
+ itempool = []
+ for item in item_table:
+ count = item["count"]
+
+ if count <= 0:
+ continue
+ else:
+ for i in range(count):
+ itempool.append(self.create_item(item["name"]))
+
+ feather_count = self.options.golden_feathers
+ if self.options.goal == 1 or self.options.goal == 3:
+ if feather_count < 12:
+ feather_count = 12
+
+ itempool += [self.create_item("Golden Feather") for _ in range(feather_count)]
+ itempool += [self.create_item("Silver Feather") for _ in range(self.options.silver_feathers)]
+ itempool += [self.create_item("Bucket") for _ in range(self.options.buckets)]
+ itempool += [self.create_item("Stick") for _ in range(self.options.sticks)]
+ itempool += [self.create_item("Toy Shovel") for _ in range(self.options.toy_shovels)]
+
+ if self.options.random_walkie_talkie:
+ itempool.append(self.create_item("Walkie Talkie"))
+ else:
+ self.multiworld.get_location("Lose Race Gift", self.player).place_locked_item(self.create_item("Walkie Talkie"))
+
+ junk = len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool)
+ itempool += [self.create_item(self.get_filler_item_name()) for _ in range(junk)]
+
+ self.multiworld.itempool += itempool
+
+ def create_regions(self) -> None:
+ menu_region = Region("Menu", self.player, self.multiworld)
+ self.multiworld.regions.append(menu_region)
+
+ main_region = Region("Hawk Peak", self.player, self.multiworld)
+
+ for loc in self.location_name_to_id.keys():
+ main_region.locations.append(ShortHikeLocation(self.player, loc, self.location_name_to_id[loc], main_region))
+
+ self.multiworld.regions.append(main_region)
+
+ menu_region.connect(main_region)
+
+ if self.options.goal == "nap":
+ # Nap
+ self.multiworld.completion_condition[self.player] = lambda state: (state.has("Golden Feather", self.player, get_min_feathers(self, 7, 9))
+ or (state.has("Bucket", self.player) and state.has("Golden Feather", self.player, 7)))
+ elif self.options.goal == "photo":
+ # Photo
+ self.multiworld.completion_condition[self.player] = lambda state: state.has("Golden Feather", self.player, 12)
+ elif self.options.goal == "races":
+ # Races
+ self.multiworld.completion_condition[self.player] = lambda state: state.can_reach_location("Hawk Peak Race Reward", self.player)
+ elif self.options.goal == "help_everyone":
+ # Help Everyone
+ self.multiworld.completion_condition[self.player] = lambda state: (state.can_reach_location("Collect 15 Seashells", self.player)
+ and state.has("Golden Feather", self.player, 12)
+ and state.can_reach_location("Tough Bird Salesman (400 Coins)", self.player)
+ and state.can_reach_location("Ranger May Shell Necklace Golden Feather", self.player)
+ and state.can_reach_location("Sue the Rabbit Shoes Reward", self.player)
+ and state.can_reach_location("Wristwatch Trade", self.player)
+ and state.can_reach_location("Return Camping Permit", self.player)
+ and state.can_reach_location("Boat Challenge Reward", self.player)
+ and state.can_reach_location("Shovel Kid Trade", self.player)
+ and state.can_reach_location("Purchase Sunhat", self.player)
+ and state.can_reach_location("Artist Golden Feather", self.player))
+ elif self.options.goal == "fishmonger":
+ # Fishmonger
+ self.multiworld.completion_condition[self.player] = lambda state: state.can_reach_location("Catch All Fish Reward", self.player)
+
+ def set_rules(self):
+ create_rules(self, location_table)
+
+ def fill_slot_data(self) -> Dict[str, Any]:
+ options = self.options
+
+ settings = {
+ "goal": int(options.goal),
+ "logicLevel": int(options.golden_feather_progression),
+ "costMultiplier": int(options.cost_multiplier),
+ "shopCheckLogic": int(options.shop_check_logic),
+ "minShopCheckLogic": int(options.min_shop_check_logic),
+ "easierRaces": bool(options.easier_races),
+ }
+
+ slot_data = {
+ "settings": settings,
+ }
+
+ return slot_data
+
+class ShortHikeItem(Item):
+ game: str = "A Short Hike"
+
+class ShortHikeLocation(Location):
+ game: str = "A Short Hike"
diff --git a/worlds/shorthike/docs/en_A Short Hike.md b/worlds/shorthike/docs/en_A Short Hike.md
new file mode 100644
index 000000000000..11b5b4b8ca85
--- /dev/null
+++ b/worlds/shorthike/docs/en_A Short Hike.md
@@ -0,0 +1,29 @@
+# A Short Hike
+
+## What does randomization do to this game?
+
+All items that can be obtained from chests, the ground, and NPCs are randomized.
+
+## What does another world's item look like in A Short Hike?
+
+All items are replaced with chests that can contain items from other worlds.
+Items will appear with the Archipelago logo next to them when obtained.
+
+## Which characters need to be helped for the Help Everyone goal?
+
+To achieve the Help Everyone goal, the following characters will need to be helped:
+- Pay Tough Bird Salesman's Tuition Fee
+- Give Frog a Toy Shovel
+- Return the Camper's Camping Permit
+- Complete the Deer Kid's Boating Challenge
+- Find Sue's Headband
+- Clean Up and Purchase the Sunhat from the Deer
+- Return the Camper's Wristwatch
+- Cheer Up the Artist
+- Collect 15 Shells for the Kid
+- Give the Shell Necklace to Aunt May
+- Help the Fox Climb the Mountain
+
+## Can I have more than one save at a time?
+
+You can have up to 3 saves at a time. To switch between them, use the Save Data button in the options menu.
diff --git a/worlds/shorthike/docs/setup_en.md b/worlds/shorthike/docs/setup_en.md
new file mode 100644
index 000000000000..96e4d8dbbd1d
--- /dev/null
+++ b/worlds/shorthike/docs/setup_en.md
@@ -0,0 +1,27 @@
+# A Short Hike Multiworld Setup Guide
+
+## Required Software
+
+- A Short Hike: [Steam](https://store.steampowered.com/app/1055540/A_Short_Hike/)
+ - The Epic Games Store or itch.io version of A Short Hike will also work.
+- A Short Hike Randomizer: [GitHub](https://github.com/BrandenEK/AShortHike.Randomizer)
+
+## Optional Software
+
+- [PopTracker](https://github.com/black-sliver/PopTracker/)
+ - [Chandler's A Short Hike PopTracker Pack](https://github.com/chandler05/shorthike-archipelago-poptracker/releases)
+
+## Installation
+
+Open the [Randomizer Repository](https://github.com/BrandenEK/AShortHike.Randomizer) and follow
+the installation instructions listed there.
+
+## Connecting
+
+A Short Hike will prompt you with the server details when a new game is started or a previous one is continued.
+Enter in the Server Address and Port, Name, and Password (optional) in the popup menu that appears and hit connect.
+
+## Tracking
+
+Install PopTracker from the link above and place the PopTracker pack into the packs folder.
+Connect to Archipelago via the AP button in the top left.
diff --git a/worlds/sm/Client.py b/worlds/sm/Client.py
index df73ae47a0b6..b9c13433aeb7 100644
--- a/worlds/sm/Client.py
+++ b/worlds/sm/Client.py
@@ -37,6 +37,7 @@
class SMSNIClient(SNIClient):
game = "Super Metroid"
+ patch_suffix = [".apsm", ".apm3"]
async def deathlink_kill_player(self, ctx):
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
@@ -61,7 +62,7 @@ async def validate_rom(self, ctx):
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
rom_name = await snes_read(ctx, SM_ROMNAME_START, ROMNAME_SIZE)
- if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"SM" or rom_name[:3] == b"SMW":
+ if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"SM" or rom_name[2] not in b"1234567890":
return False
ctx.game = self.game
@@ -122,7 +123,7 @@ async def game_watcher(self, ctx):
location_id = locations_start_id + item_index
ctx.locations_checked.add(location_id)
- location = ctx.location_names[location_id]
+ location = ctx.location_names.lookup_in_game(location_id)
snes_logger.info(
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
@@ -138,7 +139,7 @@ async def game_watcher(self, ctx):
if item_out_ptr < len(ctx.items_received):
item = ctx.items_received[item_out_ptr]
item_id = item.item - items_start_id
- if bool(ctx.items_handling & 0b010):
+ if bool(ctx.items_handling & 0b010) or item.location < 0: # item.location < 0 for !getitem to work
location_id = (item.location - locations_start_id) if (item.location >= 0 and item.player == ctx.slot) else 0xFF
else:
location_id = 0x00 #backward compat
@@ -150,9 +151,8 @@ async def game_watcher(self, ctx):
snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT,
bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF]))
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
- color(ctx.item_names[item.item], 'red', 'bold'),
+ color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'),
color(ctx.player_names[item.player], 'yellow'),
- ctx.location_names[item.location], item_out_ptr, len(ctx.items_received)))
+ ctx.location_names.lookup_in_slot(item.location, item.player), item_out_ptr, len(ctx.items_received)))
await snes_flush_writes(ctx)
-
diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py
index 21e29868eda5..826b1447793d 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")
@@ -100,7 +99,6 @@ class SMWorld(World):
game: str = "Super Metroid"
topology_present = True
- data_version = 3
option_definitions = sm_options
settings: typing.ClassVar[SMSettings]
@@ -113,15 +111,12 @@ class SMWorld(World):
required_client_version = (0, 2, 6)
itemManager: ItemManager
- spheres = None
Logic.factory('vanilla')
def __init__(self, world: MultiWorld, player: int):
self.rom_name_available_event = threading.Event()
self.locations = {}
- if SMWorld.spheres != None:
- SMWorld.spheres = None
super().__init__(world, player)
@classmethod
@@ -148,7 +143,7 @@ def generate_early(self):
self.remote_items = self.multiworld.remote_items[self.player]
if (len(self.variaRando.randoExec.setup.restrictedLocs) > 0):
- self.multiworld.accessibility[self.player] = self.multiworld.accessibility[self.player].from_text("minimal")
+ self.multiworld.accessibility[self.player].value = Accessibility.option_minimal
logger.warning(f"accessibility forced to 'minimal' for player {self.multiworld.get_player_name(self.player)} because of 'fun' settings")
def create_items(self):
@@ -295,7 +290,7 @@ def create_regions(self):
for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions:
src_region = self.multiworld.get_region(src.Name, self.player)
dest_region = self.multiworld.get_region(dest.Name, self.player)
- if ((src.Name + "->" + dest.Name, self.player) not in self.multiworld._entrance_cache):
+ if src.Name + "->" + dest.Name not in self.multiworld.regions.entrance_cache[self.player]:
src_region.exits.append(Entrance(self.player, src.Name + "->" + dest.Name, src_region))
srcDestEntrance = self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player)
srcDestEntrance.connect(dest_region)
@@ -362,23 +357,35 @@ def pre_fill(self):
def post_fill(self):
def get_player_ItemLocation(progression_only: bool):
return [
- ItemLocation(copy.copy(ItemManager.Items[
- itemLoc.item.type if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else
- 'ArchipelagoItem']),
- copy.copy(locationsDict[itemLoc.name] if itemLoc.game == self.game else
- locationsDict[first_local_collected_loc.name]),
- itemLoc.item.player,
- True)
- for itemLoc in SMWorld.spheres if itemLoc.item.player == self.player and (not progression_only or itemLoc.item.advancement)
- ]
-
+ ItemLocation(
+ copy.copy(
+ ItemManager.Items[
+ itemLoc.item.type
+ if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items
+ else 'ArchipelagoItem'
+ ]
+ ),
+ copy.copy(
+ locationsDict[itemLoc.name]
+ if itemLoc.game == self.game
+ else locationsDict[first_local_collected_loc.name]
+ ),
+ itemLoc.item.player,
+ True
+ )
+ for itemLoc in spheres
+ if itemLoc.item.player == self.player and (not progression_only or itemLoc.item.advancement)
+ ]
+
# Having a sorted itemLocs from collection order is required for escapeTrigger when Tourian is Disabled.
# We cant use stage_post_fill for this as its called after worlds' post_fill.
# get_spheres could be cached in multiworld?
# Another possible solution would be to have a globally accessible list of items in the order in which the get placed in push_item
# and use the inversed starting from the first progression item.
- if (SMWorld.spheres == None):
- SMWorld.spheres = [itemLoc for sphere in self.multiworld.get_spheres() for itemLoc in sorted(sphere, key=lambda location: location.name)]
+ spheres: List[Location] = getattr(self.multiworld, "_sm_spheres", None)
+ if spheres is None:
+ spheres = [itemLoc for sphere in self.multiworld.get_spheres() for itemLoc in sorted(sphere, key=lambda location: location.name)]
+ setattr(self.multiworld, "_sm_spheres", spheres)
self.itemLocs = [
ItemLocation(copy.copy(ItemManager.Items[itemLoc.item.type
@@ -391,7 +398,7 @@ def get_player_ItemLocation(progression_only: bool):
escapeTrigger = None
if self.variaRando.randoExec.randoSettings.restrictions["EscapeTrigger"]:
#used to simulate received items
- first_local_collected_loc = next(itemLoc for itemLoc in SMWorld.spheres if itemLoc.player == self.player)
+ first_local_collected_loc = next(itemLoc for itemLoc in spheres if itemLoc.player == self.player)
playerItemsItemLocs = get_player_ItemLocation(False)
playerProgItemsItemLocs = get_player_ItemLocation(True)
@@ -564,8 +571,8 @@ def APPostPatchRom(self, romPatcher):
multiWorldItems: List[ByteEdit] = []
idx = 0
vanillaItemTypesCount = 21
- for itemLoc in self.multiworld.get_locations():
- if itemLoc.player == self.player and "Boss" not in locationsDict[itemLoc.name].Class:
+ for itemLoc in self.multiworld.get_locations(self.player):
+ if "Boss" not in locationsDict[itemLoc.name].Class:
SMZ3NameToSMType = {
"ETank": "ETank", "Missile": "Missile", "Super": "Super", "PowerBomb": "PowerBomb", "Bombs": "Bomb",
"Charge": "Charge", "Ice": "Ice", "HiJump": "HiJump", "SpeedBooster": "SpeedBooster",
diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips
index 67863bb9f002..7854ca333287 100644
Binary files a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips and b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips differ
diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym
index 470b7a8af023..0a76c2071484 100644
--- a/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym
+++ b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym
@@ -2,7 +2,7 @@
; generated by asar
[labels]
-82:FA35 :neg_1_1
+82:FA40 :neg_1_1
B8:83C1 :neg_1_2
B8:85DA :neg_1_3
B8:85F9 :neg_1_4
@@ -177,7 +177,7 @@ A0:FE00 setup_music
A0:FE0B setup_music_quick
A0:FE94 setup_samus
A0:FEA5 setup_samus_normal
-82:FA27 sm_fix_checksum
+82:FA2A sm_fix_checksum
B8:8800 sm_item_graphics
B8:882E sm_item_plm_pickup_sequence_pointers
B8:847B sm_save_done_hook
@@ -192,7 +192,7 @@ B8:83F4 write_repeated_memory_loop
[source files]
0000 fe019f55 main.asm
-0001 be17692f ../common/fast_reload.asm
+0001 62a29254 ../common/fast_reload.asm
0002 06780555 ../common/nofanfare.asm
0003 7a8904b6 ../common/multiworld.asm
0004 f7e9db95 ../common/itemextras.asm
@@ -200,7 +200,7 @@ B8:83F4 write_repeated_memory_loop
0006 dbfcb38d ../common/startitem.asm
[rom checksum]
-979ac031
+e50233eb
[addr-to-line mapping]
ff:ffff 0000:00000001
@@ -245,125 +245,132 @@ ce:ff0a 0001:00000013
82:f9e6 0001:00000040
82:f9ea 0001:00000042
82:f9ee 0001:00000043
-82:f9f0 0001:00000044
-82:f9f1 0001:00000045
-82:f9f2 0001:00000046
-82:f9f6 0001:00000047
+82:f9f1 0001:00000044
+82:f9f2 0001:00000045
+82:f9f4 0001:00000046
+82:f9f5 0001:00000047
82:f9f9 0001:00000048
-82:f9fa 0001:00000049
-82:f9fb 0001:0000004a
-82:f9fc 0001:0000004b
-82:f9ff 0001:0000004b
-82:fa00 0001:0000004b
-82:fa04 0001:0000004c
-82:fa05 0001:0000004d
-82:fa08 0001:0000004d
-82:fa0c 0001:0000004e
-82:fa10 0001:00000050
-82:fa14 0001:00000051
+82:f9fc 0001:00000049
+82:f9fd 0001:0000004a
+82:f9fe 0001:0000004b
+82:f9ff 0001:0000004c
+82:fa02 0001:0000004c
+82:fa03 0001:0000004c
+82:fa07 0001:0000004d
+82:fa08 0001:0000004e
+82:fa0b 0001:0000004e
+82:fa0f 0001:0000004f
+82:fa13 0001:00000051
82:fa17 0001:00000052
-82:fa1b 0001:00000053
-82:fa1f 0001:00000054
+82:fa1a 0001:00000053
+82:fa1e 0001:00000054
82:fa22 0001:00000055
82:fa25 0001:00000056
-82:fa26 0001:00000057
-82:fa27 0001:0000005a
-82:fa28 0001:0000005b
-82:fa29 0001:0000005c
-82:fa2a 0001:0000005d
-82:fa2b 0000:00000013
-82:fa2d 0001:00000061
-82:fa2f 0001:00000062
-82:fa30 0001:00000063
-82:fa32 0001:00000064
-82:fa35 0001:00000066
-82:fa39 0001:00000067
-82:fa3a 0001:00000068
-82:fa3c 0001:00000069
-82:fa3e 0001:0000006a
-82:fa3f 0001:0000006b
-82:fa40 0001:0000006c
-82:fa43 0001:0000006d
-82:fa45 0001:0000006f
-82:fa48 0001:00000070
-82:fa4a 0001:00000071
-82:fa4e 0001:00000072
-82:fa52 0001:00000073
-82:fa55 0001:00000074
-82:fa59 0001:00000075
-82:fa5d 0001:00000076
-82:fa5e 0001:00000077
-82:fa60 0001:00000079
-82:fa61 0001:0000007a
-82:fa62 0001:0000007b
-82:fa63 0001:0000007c
-82:fa64 0001:0000007d
-80:a088 0001:00000083
-80:a08c 0001:00000083
-80:a08d 0001:00000083
-80:a095 0001:00000086
-80:a0ce 0001:00000089
-80:a113 0001:0000008c
-91:e164 0001:0000008f
-91:e168 0001:0000008f
-91:e169 0001:0000008f
-a0:fe00 0001:00000094
-a0:fe03 0001:00000095
-a0:fe05 0001:00000096
-a0:fe08 0001:00000097
-a0:fe0b 0001:00000099
-a0:fe0c 0001:0000009c
-a0:fe10 0001:0000009d
-a0:fe13 0001:0000009e
-a0:fe15 0001:0000009f
-a0:fe18 0001:000000a0
-a0:fe1b 0001:000000a1
-a0:fe1f 0001:000000a3
-a0:fe23 0001:000000a4
-a0:fe27 0001:000000a5
-a0:fe2b 0001:000000a6
-a0:fe2f 0001:000000a9
-a0:fe33 0001:000000aa
-a0:fe36 0001:000000ab
-a0:fe38 0001:000000ac
-a0:fe3c 0001:000000ad
-a0:fe40 0001:000000af
-a0:fe44 0001:000000b2
-a0:fe48 0001:000000b3
-a0:fe49 0001:000000b4
-a0:fe4c 0001:000000b5
-a0:fe4e 0001:000000b6
-a0:fe4f 0001:000000b7
-a0:fe53 0001:000000b8
-a0:fe57 0001:000000ba
-a0:fe58 0001:000000bb
-a0:fe5c 0001:000000bc
-a0:fe5f 0001:000000bd
-a0:fe62 0001:000000be
-a0:fe65 0001:000000bf
-a0:fe67 0001:000000c0
-a0:fe6a 0001:000000c1
-a0:fe6d 0001:000000c2
-a0:fe6f 0001:000000c3
-a0:fe73 0001:000000c6
-a0:fe76 0001:000000c7
-a0:fe79 0001:000000c8
-a0:fe7c 0001:000000c9
-a0:fe7f 0001:000000cb
-a0:fe82 0001:000000cc
-a0:fe85 0001:000000cd
-a0:fe89 0001:000000ce
-a0:fe8c 0001:000000cf
-a0:fe90 0001:000000d1
-a0:fe94 0001:000000d4
-a0:fe97 0001:000000d5
-a0:fe99 0001:000000d6
-a0:fe9c 0001:000000d7
-a0:fe9f 0001:000000d8
-a0:fea2 0001:000000d9
-a0:fea5 0001:000000db
-a0:fea8 0001:000000dc
-a0:feab 0001:000000dd
+82:fa28 0001:00000057
+82:fa29 0001:00000058
+82:fa2a 0001:0000005b
+82:fa2b 0001:0000005c
+82:fa2c 0001:0000005d
+82:fa2d 0001:0000005e
+82:fa2e 0000:00000013
+82:fa30 0001:00000062
+82:fa32 0001:00000063
+82:fa33 0001:00000064
+82:fa35 0001:00000065
+82:fa37 0001:00000066
+82:fa38 0001:00000067
+82:fa3c 0001:00000068
+82:fa3d 0001:00000069
+82:fa40 0001:0000006b
+82:fa44 0001:0000006c
+82:fa45 0001:0000006d
+82:fa47 0001:0000006e
+82:fa49 0001:0000006f
+82:fa4a 0001:00000070
+82:fa4b 0001:00000071
+82:fa4c 0001:00000072
+82:fa4d 0001:00000073
+82:fa50 0001:00000074
+82:fa52 0001:00000076
+82:fa54 0001:00000077
+82:fa56 0001:00000078
+82:fa5a 0001:00000079
+82:fa5e 0001:0000007a
+82:fa61 0001:0000007b
+82:fa65 0001:0000007c
+82:fa69 0001:0000007d
+82:fa6a 0001:0000007e
+82:fa6c 0001:00000080
+82:fa6d 0001:00000081
+82:fa6e 0001:00000082
+82:fa6f 0001:00000083
+82:fa70 0001:00000084
+80:a088 0001:0000008a
+80:a08c 0001:0000008a
+80:a08d 0001:0000008a
+80:a095 0001:0000008d
+80:a0ce 0001:00000090
+80:a113 0001:00000093
+91:e164 0001:00000096
+91:e168 0001:00000096
+91:e169 0001:00000096
+a0:fe00 0001:0000009b
+a0:fe03 0001:0000009c
+a0:fe05 0001:0000009d
+a0:fe08 0001:0000009e
+a0:fe0b 0001:000000a0
+a0:fe0c 0001:000000a3
+a0:fe10 0001:000000a4
+a0:fe13 0001:000000a5
+a0:fe15 0001:000000a6
+a0:fe18 0001:000000a7
+a0:fe1b 0001:000000a8
+a0:fe1f 0001:000000aa
+a0:fe23 0001:000000ab
+a0:fe27 0001:000000ac
+a0:fe2b 0001:000000ad
+a0:fe2f 0001:000000b0
+a0:fe33 0001:000000b1
+a0:fe36 0001:000000b2
+a0:fe38 0001:000000b3
+a0:fe3c 0001:000000b4
+a0:fe40 0001:000000b6
+a0:fe44 0001:000000b9
+a0:fe48 0001:000000ba
+a0:fe49 0001:000000bb
+a0:fe4c 0001:000000bc
+a0:fe4e 0001:000000bd
+a0:fe4f 0001:000000be
+a0:fe53 0001:000000bf
+a0:fe57 0001:000000c1
+a0:fe58 0001:000000c2
+a0:fe5c 0001:000000c3
+a0:fe5f 0001:000000c4
+a0:fe62 0001:000000c5
+a0:fe65 0001:000000c6
+a0:fe67 0001:000000c7
+a0:fe6a 0001:000000c8
+a0:fe6d 0001:000000c9
+a0:fe6f 0001:000000ca
+a0:fe73 0001:000000cd
+a0:fe76 0001:000000ce
+a0:fe79 0001:000000cf
+a0:fe7c 0001:000000d0
+a0:fe7f 0001:000000d2
+a0:fe82 0001:000000d3
+a0:fe85 0001:000000d4
+a0:fe89 0001:000000d5
+a0:fe8c 0001:000000d6
+a0:fe90 0001:000000d8
+a0:fe94 0001:000000db
+a0:fe97 0001:000000dc
+a0:fe99 0001:000000dd
+a0:fe9c 0001:000000de
+a0:fe9f 0001:000000df
+a0:fea2 0001:000000e0
+a0:fea5 0001:000000e2
+a0:fea8 0001:000000e3
+a0:feab 0001:000000e4
85:ff00 0002:0000010b
85:ff03 0002:0000010c
85:ff06 0002:0000010d
diff --git a/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json
index 813937d3f43f..0be5a05715b1 100644
--- a/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json
+++ b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json
@@ -158,7 +158,7 @@
"setup_music_quick": "A0:FE0B",
"setup_samus": "A0:FE94",
"setup_samus_normal": "A0:FEA5",
- "sm_fix_checksum": "82:FA27",
+ "sm_fix_checksum": "82:FA2A",
"sm_item_graphics": "B8:8800",
"sm_item_plm_pickup_sequence_pointers": "B8:882E",
"sm_save_done_hook": "B8:847B",
@@ -172,7 +172,7 @@
"write_repeated_memory_loop": "B8:83F4",
"deathhook82": "82:DDC7",
"freespace82_start": "82:F990",
- "freespace82_end": "82:FA65",
+ "freespace82_end": "82:FA71",
"freespacea0": "a0:fe00",
"SRAM_SAVING": "70:2604",
"current_save_slot": "7e:0952",
diff --git a/worlds/sm/docs/en_Super Metroid.md b/worlds/sm/docs/en_Super Metroid.md
index c177582e221e..c8c1d0faabee 100644
--- a/worlds/sm/docs/en_Super Metroid.md
+++ b/worlds/sm/docs/en_Super Metroid.md
@@ -1,8 +1,8 @@
# Super Metroid
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -34,5 +34,6 @@ When the player receives an item, a text box will appear to show which item was
It can happen that a required item is in a place where you cant get back from. While in normal gameplay state, by holding
Start+Select+L+R at the same time, the game will save your progress and put you back at your original starting position.
-This can be required by the logic.
+This can be required by the logic. Since the addition of that feature, VARIA's automatic backup saves are disabled since
+you can't softlock anymore.
diff --git a/worlds/sm/docs/multiworld_en.md b/worlds/sm/docs/multiworld_en.md
index 20c055bc91bf..8f30630bc96d 100644
--- a/worlds/sm/docs/multiworld_en.md
+++ b/worlds/sm/docs/multiworld_en.md
@@ -2,25 +2,28 @@
## Required Software
-- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Super Metroid Patch Setup`
-
-
-- Hardware or software capable of loading and playing SNES ROM files
- - An emulator capable of connecting to SNI such as:
- - snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases),
- - BizHawk from: [TASVideos](https://tasvideos.org/BizHawk)
- - RetroArch 1.10.1 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or,
- - An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other
- compatible hardware
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
+- [SNI](https://github.com/alttpo/sni/releases). This is automatically included with your Archipelago installation above.
+- SNI is not compatible with (Q)Usb2Snes.
+- Hardware or software capable of loading and playing SNES ROM files, including:
+ - An emulator capable of connecting to SNI
+ ([snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases), [snes9x-rr](https://github.com/gocha/snes9x-rr/releases),
+ [BSNES-plus](https://github.com/black-sliver/bsnes-plus),
+ [BizHawk](http://tasvideos.org/BizHawk.html), or
+ [RetroArch](https://retroarch.com?page=platforms) 1.10.1 or newer)
+ - An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware. **note:
+ modded SNES minis are currently not supported by SNI. Some users have claimed success with QUsb2Snes for this system,
+ but it is not supported.**
- Your legally obtained Super Metroid ROM file, probably named `Super Metroid (Japan, USA).sfc`
## Installation Procedures
### Windows Setup
-1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
- or you are on an older version, you may run the installer again to install the SNI Client.
-2. During setup, you will be asked to locate your base ROM file. This is your Super Metroid ROM file.
+1. Download and install [Archipelago](). **The installer
+ file is located in the assets section at the bottom of the version information.**
+2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
+ This is your Super Metroid ROM file. This only needs to be done once.
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
@@ -43,18 +46,18 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a config file?
-The Player Settings page on the website allows you to configure your personal settings and export a config file from
-them. Player settings page: [Super Metroid Player Settings Page](/games/Super%20Metroid/player-settings)
+The Player Options page on the website allows you to configure your personal options and export a config file from
+them. Player options page: [Super Metroid Player Options Page](/games/Super%20Metroid/player-options)
### 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
-1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button.
- - Player Settings page: [Super Metroid Player Settings Page](/games/Super%20Metroid/player-settings)
+1. Navigate to the Player Options page, configure your options, and click the "Generate Game" button.
+ - Player Options page: [Super Metroid Player Options Page](/games/Super%20Metroid/player-options)
2. You will be presented with a "Seed Info" page.
3. Click the "Create New Room" link.
4. You will be presented with a server page, from which you can download your patch file.
@@ -80,6 +83,11 @@ client, and will also create your ROM in the same place as your patch file.
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
+#### snes9x-nwa
+
+1. Click on the Network Menu and check **Enable Emu Network Control**
+2. Load your ROM file if it hasn't already been loaded.
+
##### snes9x-rr
1. Load your ROM file if it hasn't already been loaded.
@@ -87,11 +95,16 @@ first time launching, you may be prompted to allow it to communicate through the
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Select the connector lua file included with your client
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
+#### BSNES-Plus
+
+1. Load your ROM file if it hasn't already been loaded.
+2. The emulator should automatically connect while SNI is running.
+
+
##### BizHawk
1. Ensure you have the BSNES core loaded. This is done with the main menubar, under:
@@ -100,8 +113,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas
2. Load your ROM file if it hasn't already been loaded.
If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R).
3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window.
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
- You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua`
with the file picker.
diff --git a/worlds/sm/variaRandomizer/rando/GraphBuilder.py b/worlds/sm/variaRandomizer/rando/GraphBuilder.py
index 3577baff0880..88b539e7f04a 100644
--- a/worlds/sm/variaRandomizer/rando/GraphBuilder.py
+++ b/worlds/sm/variaRandomizer/rando/GraphBuilder.py
@@ -150,7 +150,6 @@ def escapeTrigger(self, emptyContainer, graph, maxDiff, escapeTrigger):
# update item% objectives
accessibleItems = [il.Item for il in allItemLocs if ilCheck(il)]
majorUpgrades = [item.Type for item in accessibleItems if item.BeamBits != 0 or item.ItemBits != 0]
- sm.objectives.setItemPercentFuncs(len(accessibleItems), majorUpgrades)
if split == "Scavenger":
# update escape access for scav with last scav loc
lastScavItemLoc = progItemLocs[-1]
@@ -163,6 +162,7 @@ def escapeTrigger(self, emptyContainer, graph, maxDiff, escapeTrigger):
if ilCheck(itemLoc) and (split.startswith("Full") or itemLoc.Location.isClass(split)):
availLocsByArea[itemLoc.Location.GraphArea].append(itemLoc.Location.Name)
self.log.debug("escapeTrigger. availLocsByArea="+str(availLocsByArea))
+ sm.objectives.setItemPercentFuncs(len(accessibleItems), majorUpgrades, container)
sm.objectives.setAreaFuncs({area:lambda sm,ap:SMBool(len(container.getLocs(lambda loc: loc.Name in availLocsByArea[area]))==0) for area in availLocsByArea})
self.log.debug("escapeTrigger. collect locs until G4 access")
# collect all item/locations up until we can pass G4 (the escape triggers)
@@ -170,7 +170,8 @@ def escapeTrigger(self, emptyContainer, graph, maxDiff, escapeTrigger):
ap = "Landing Site" # dummy value it'll be overwritten at first collection
while len(itemLocs) > 0 and not (sm.canPassG4() and graph.canAccess(sm, ap, "Landing Site", maxDiff)):
il = itemLocs.pop(0)
- if il.Location.restricted or il.Item.Type == "ArchipelagoItem":
+ # can happen with item links replacement items that its not in the container's itemPool
+ if il.Location.restricted or il.Item.Type == "ArchipelagoItem" or il.Item not in container.itemPool:
continue
self.log.debug("collecting " + getItemLocStr(il))
container.collect(il)
diff --git a/worlds/sm/variaRandomizer/rom/rompatcher.py b/worlds/sm/variaRandomizer/rom/rompatcher.py
index 1120eacf4b33..2dcf554a0065 100644
--- a/worlds/sm/variaRandomizer/rom/rompatcher.py
+++ b/worlds/sm/variaRandomizer/rom/rompatcher.py
@@ -238,10 +238,12 @@ def applyIPSPatches(self):
plms.append('WS_Save_Blinking_Door')
if self.settings["boss"] == True:
stdPatches.append("Phantoon_Eye_Door")
- if (self.settings["area"] == True
- or self.settings["doorsColorsRando"] == True
- or not GraphUtils.isStandardStart(self.settings["startLocation"])):
- stdPatches.append("Enable_Backup_Saves")
+ # rolling saves is not required anymore since the addition of fast_save_reload
+ # also, both arent completely compatible as-is
+ #if (self.settings["area"] == True
+ # or self.settings["doorsColorsRando"] == True
+ # or not GraphUtils.isStandardStart(self.settings["startLocation"])):
+ # stdPatches.append("Enable_Backup_Saves")
if 'varia_hud.ips' in self.settings["optionalPatches"]:
# varia hud can make demos glitch out
self.applyIPSPatch("no_demo.ips")
diff --git a/worlds/sm/variaRandomizer/utils/objectives.py b/worlds/sm/variaRandomizer/utils/objectives.py
index 8c886674fd2c..67cdb9a1c132 100644
--- a/worlds/sm/variaRandomizer/utils/objectives.py
+++ b/worlds/sm/variaRandomizer/utils/objectives.py
@@ -511,16 +511,18 @@ def updateItemPercentEscapeAccess(self, collectedLocsAccessPoints):
def setScavengerHuntFunc(self, scavClearFunc):
self.goals["finish scavenger hunt"].clearFunc = scavClearFunc
- def setItemPercentFuncs(self, totalItemsCount=None, allUpgradeTypes=None):
- def getPctFunc(pct, totalItemsCount):
+ def setItemPercentFuncs(self, totalItemsCount=None, allUpgradeTypes=None, container=None):
+ def getPctFunc(total_needed, container):
def f(sm, ap):
- nonlocal pct, totalItemsCount
- return sm.hasItemsPercent(pct, totalItemsCount)
+ nonlocal total_needed, container
+ locs_checked = len(container.getUsedLocs(lambda loc: True))
+ return SMBool(locs_checked >= total_needed)
return f
+ # AP: now based on location checks instead of local item
for pct in [25,50,75,100]:
goal = 'collect %d%% items' % pct
- self.goals[goal].clearFunc = getPctFunc(pct, totalItemsCount)
+ self.goals[goal].clearFunc = getPctFunc(totalItemsCount * pct / 100, container)
if allUpgradeTypes is not None:
self.goals["collect all upgrades"].clearFunc = lambda sm, ap: sm.haveItems(allUpgradeTypes)
diff --git a/worlds/sm64ex/Items.py b/worlds/sm64ex/Items.py
index 5b429a23cdc3..546f1abd316b 100644
--- a/worlds/sm64ex/Items.py
+++ b/worlds/sm64ex/Items.py
@@ -16,6 +16,21 @@ class SM64Item(Item):
"1Up Mushroom": 3626184
}
+action_item_table = {
+ "Double Jump": 3626185,
+ "Triple Jump": 3626186,
+ "Long Jump": 3626187,
+ "Backflip": 3626188,
+ "Side Flip": 3626189,
+ "Wall Kick": 3626190,
+ "Dive": 3626191,
+ "Ground Pound": 3626192,
+ "Kick": 3626193,
+ "Climb": 3626194,
+ "Ledge Grab": 3626195
+}
+
+
cannon_item_table = {
"Cannon Unlock BoB": 3626200,
"Cannon Unlock WF": 3626201,
@@ -29,4 +44,4 @@ class SM64Item(Item):
"Cannon Unlock RR": 3626214
}
-item_table = {**generic_item_table, **cannon_item_table}
\ No newline at end of file
+item_table = {**generic_item_table, **action_item_table, **cannon_item_table}
\ No newline at end of file
diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py
index a603b61c5809..60ec4bbe13c2 100644
--- a/worlds/sm64ex/Options.py
+++ b/worlds/sm64ex/Options.py
@@ -1,9 +1,11 @@
import typing
-from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink, Choice
-
+from dataclasses import dataclass
+from Options import DefaultOnToggle, Range, Toggle, DeathLink, Choice, PerGameCommonOptions, OptionSet
+from .Items import action_item_table
class EnableCoinStars(DefaultOnToggle):
- """Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything"""
+ """Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything.
+ Removes 15 locations from the pool."""
display_name = "Enable 100 Coin Stars"
@@ -13,56 +15,63 @@ class StrictCapRequirements(DefaultOnToggle):
class StrictCannonRequirements(DefaultOnToggle):
- """If disabled, Stars that expect cannons may have to be acquired without them. Only makes a difference if Buddy
- Checks are enabled"""
+ """If disabled, Stars that expect cannons may have to be acquired without them.
+ Has no effect if Buddy Checks and Move Randomizer are disabled"""
display_name = "Strict Cannon Requirements"
class FirstBowserStarDoorCost(Range):
- """How many stars are required at the Star Door to Bowser in the Dark World"""
+ """What percent of the total stars are required at the Star Door to Bowser in the Dark World"""
+ display_name = "First Star Door Cost %"
range_start = 0
- range_end = 50
- default = 8
+ range_end = 40
+ default = 7
class BasementStarDoorCost(Range):
- """How many stars are required at the Star Door in the Basement"""
+ """What percent of the total stars are required at the Star Door in the Basement"""
+ display_name = "Basement Star Door %"
range_start = 0
- range_end = 70
- default = 30
+ range_end = 50
+ default = 25
class SecondFloorStarDoorCost(Range):
- """How many stars are required to access the third floor"""
+ """What percent of the total stars are required to access the third floor"""
+ display_name = 'Second Floor Star Door %'
range_start = 0
- range_end = 90
- default = 50
+ range_end = 70
+ default = 42
class MIPS1Cost(Range):
- """How many stars are required to spawn MIPS the first time"""
+ """What percent of the total stars are required to spawn MIPS the first time"""
+ display_name = "MIPS 1 Star %"
range_start = 0
- range_end = 40
- default = 15
+ range_end = 35
+ default = 12
class MIPS2Cost(Range):
- """How many stars are required to spawn MIPS the second time."""
+ """What percent of the total stars are required to spawn MIPS the second time."""
+ display_name = "MIPS 2 Star %"
range_start = 0
- range_end = 80
- default = 50
+ range_end = 70
+ default = 42
class StarsToFinish(Range):
- """How many stars are required at the infinite stairs"""
- display_name = "Endless Stairs Stars"
+ """What percent of the total stars are required at the infinite stairs"""
+ display_name = "Endless Stairs Star %"
range_start = 0
- range_end = 100
- default = 70
+ range_end = 90
+ default = 58
class AmountOfStars(Range):
- """How many stars exist. Disabling 100 Coin Stars removes 15 from the Pool. At least max of any Cost set"""
+ """How many stars exist.
+ If there aren't enough locations to hold the given total, the total will be reduced."""
+ display_name = "Total Power Stars"
range_start = 35
range_end = 120
default = 120
@@ -83,31 +92,60 @@ class BuddyChecks(Toggle):
class ExclamationBoxes(Choice):
- """Include 1Up Exclamation Boxes during randomization"""
+ """Include 1Up Exclamation Boxes during randomization.
+ Adds 29 locations to the pool."""
display_name = "Randomize 1Up !-Blocks"
option_Off = 0
option_1Ups_Only = 1
+class CompletionType(Choice):
+ """Set goal for game completion"""
+ display_name = "Completion Goal"
+ option_Last_Bowser_Stage = 0
+ option_All_Bowser_Stages = 1
+
+
class ProgressiveKeys(DefaultOnToggle):
- """Keys will first grant you access to the Basement, then to the Secound Floor"""
+ """Keys will first grant you access to the Basement, then to the Second Floor"""
display_name = "Progressive Keys"
-
-sm64_options: typing.Dict[str, type(Option)] = {
- "AreaRandomizer": AreaRandomizer,
- "ProgressiveKeys": ProgressiveKeys,
- "EnableCoinStars": EnableCoinStars,
- "AmountOfStars": AmountOfStars,
- "StrictCapRequirements": StrictCapRequirements,
- "StrictCannonRequirements": StrictCannonRequirements,
- "FirstBowserStarDoorCost": FirstBowserStarDoorCost,
- "BasementStarDoorCost": BasementStarDoorCost,
- "SecondFloorStarDoorCost": SecondFloorStarDoorCost,
- "MIPS1Cost": MIPS1Cost,
- "MIPS2Cost": MIPS2Cost,
- "StarsToFinish": StarsToFinish,
- "death_link": DeathLink,
- "BuddyChecks": BuddyChecks,
- "ExclamationBoxes": ExclamationBoxes,
-}
+class StrictMoveRequirements(DefaultOnToggle):
+ """If disabled, Stars that expect certain moves may have to be acquired without them. Only makes a difference
+ if Move Randomization is enabled"""
+ display_name = "Strict Move Requirements"
+
+class EnableMoveRandomizer(Toggle):
+ """Mario is unable to perform some actions until a corresponding item is picked up.
+ This option is incompatible with builds using a 'nomoverando' branch.
+ Specific actions to randomize can be specified in the YAML."""
+ display_name = "Enable Move Randomizer"
+
+class MoveRandomizerActions(OptionSet):
+ """Which actions to randomize when Move Randomizer is enabled"""
+ display_name = "Randomized Moves"
+ # HACK: Disable randomization for double jump
+ valid_keys = [action for action in action_item_table if action != 'Double Jump']
+ default = valid_keys
+
+@dataclass
+class SM64Options(PerGameCommonOptions):
+ area_rando: AreaRandomizer
+ buddy_checks: BuddyChecks
+ exclamation_boxes: ExclamationBoxes
+ progressive_keys: ProgressiveKeys
+ enable_coin_stars: EnableCoinStars
+ enable_move_rando: EnableMoveRandomizer
+ move_rando_actions: MoveRandomizerActions
+ strict_cap_requirements: StrictCapRequirements
+ strict_cannon_requirements: StrictCannonRequirements
+ strict_move_requirements: StrictMoveRequirements
+ amount_of_stars: AmountOfStars
+ first_bowser_star_door_cost: FirstBowserStarDoorCost
+ basement_star_door_cost: BasementStarDoorCost
+ second_floor_star_door_cost: SecondFloorStarDoorCost
+ mips1_cost: MIPS1Cost
+ mips2_cost: MIPS2Cost
+ stars_to_finish: StarsToFinish
+ death_link: DeathLink
+ completion_type: CompletionType
diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py
index c2e9e2d98115..6fc2d74b96dc 100644
--- a/worlds/sm64ex/Regions.py
+++ b/worlds/sm64ex/Regions.py
@@ -1,5 +1,8 @@
import typing
+from enum import Enum
+
from BaseClasses import MultiWorld, Region, Entrance, Location
+from .Options import SM64Options
from .Locations import SM64Location, location_table, locBoB_table, locWhomp_table, locJRB_table, locCCM_table, \
locBBH_table, \
locHMC_table, locLLL_table, locSSL_table, locDDD_table, locSL_table, \
@@ -7,180 +10,271 @@
locPSS_table, locSA_table, locBitDW_table, locTotWC_table, locCotMC_table, \
locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table
-# List of all courses, including secrets, without BitS as that one is static
-sm64courses = ["Bob-omb Battlefield", "Whomp's Fortress", "Jolly Roger Bay", "Cool, Cool Mountain", "Big Boo's Haunt",
- "Hazy Maze Cave", "Lethal Lava Land", "Shifting Sand Land", "Dire, Dire Docks", "Snowman's Land",
- "Wet-Dry World", "Tall, Tall Mountain", "Tiny-Huge Island", "Tick Tock Clock", "Rainbow Ride",
- "The Princess's Secret Slide", "The Secret Aquarium", "Bowser in the Dark World", "Tower of the Wing Cap",
- "Cavern of the Metal Cap", "Vanish Cap under the Moat", "Bowser in the Fire Sea", "Wing Mario over the Rainbow"]
-
-# sm64paintings is list of entrances, format LEVEL | AREA. String Reference below
-sm64paintings = [91,241,121,51,41,71,221,81,231,101,111,361,132,131,141,151]
-sm64paintings_s = ["BOB", "WF", "JRB", "CCM", "BBH", "HMC", "LLL", "SSL", "DDD", "SL", "WDW", "TTM", "THI Tiny", "THI Huge", "TTC", "RR"]
-# sm64secrets is list of secret areas
-sm64secrets = [271, 201, 171, 291, 281, 181, 191, 311]
-sm64secrets_s = ["PSS", "SA", "BitDW", "TOTWC", "COTMC", "VCUTM", "BitFS", "WMOTR"]
-
-sm64entrances = sm64paintings + sm64secrets
-sm64entrances_s = sm64paintings_s + sm64secrets_s
-sm64_internalloc_to_string = dict(zip(sm64paintings+sm64secrets, sm64entrances_s))
-sm64_internalloc_to_regionid = dict(zip(sm64paintings+sm64secrets, list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets)))))
-
-def create_regions(world: MultiWorld, player: int):
+
+class SM64Levels(int, Enum):
+ BOB_OMB_BATTLEFIELD = 91
+ WHOMPS_FORTRESS = 241
+ JOLLY_ROGER_BAY = 121
+ COOL_COOL_MOUNTAIN = 51
+ BIG_BOOS_HAUNT = 41
+ HAZY_MAZE_CAVE = 71
+ LETHAL_LAVA_LAND = 221
+ SHIFTING_SAND_LAND = 81
+ DIRE_DIRE_DOCKS = 231
+ SNOWMANS_LAND = 101
+ WET_DRY_WORLD = 111
+ TALL_TALL_MOUNTAIN = 361
+ TINY_HUGE_ISLAND_TINY = 132
+ TINY_HUGE_ISLAND_HUGE = 131
+ TICK_TOCK_CLOCK = 141
+ RAINBOW_RIDE = 151
+ THE_PRINCESS_SECRET_SLIDE = 271
+ THE_SECRET_AQUARIUM = 201
+ BOWSER_IN_THE_DARK_WORLD = 171
+ TOWER_OF_THE_WING_CAP = 291
+ CAVERN_OF_THE_METAL_CAP = 281
+ VANISH_CAP_UNDER_THE_MOAT = 181
+ BOWSER_IN_THE_FIRE_SEA = 191
+ WING_MARIO_OVER_THE_RAINBOW = 311
+
+
+class SM64Region(Region):
+ subregions: typing.List[Region] = []
+
+
+# sm64paintings is a dict of entrances, format LEVEL | AREA
+sm64_level_to_paintings: typing.Dict[SM64Levels, str] = {
+ SM64Levels.BOB_OMB_BATTLEFIELD: "Bob-omb Battlefield",
+ SM64Levels.WHOMPS_FORTRESS: "Whomp's Fortress",
+ SM64Levels.JOLLY_ROGER_BAY: "Jolly Roger Bay",
+ SM64Levels.COOL_COOL_MOUNTAIN: "Cool, Cool Mountain",
+ SM64Levels.BIG_BOOS_HAUNT: "Big Boo's Haunt",
+ SM64Levels.HAZY_MAZE_CAVE: "Hazy Maze Cave",
+ SM64Levels.LETHAL_LAVA_LAND: "Lethal Lava Land",
+ SM64Levels.SHIFTING_SAND_LAND: "Shifting Sand Land",
+ SM64Levels.DIRE_DIRE_DOCKS: "Dire, Dire Docks",
+ SM64Levels.SNOWMANS_LAND: "Snowman's Land",
+ SM64Levels.WET_DRY_WORLD: "Wet-Dry World",
+ SM64Levels.TALL_TALL_MOUNTAIN: "Tall, Tall Mountain",
+ SM64Levels.TINY_HUGE_ISLAND_TINY: "Tiny-Huge Island (Tiny)",
+ SM64Levels.TINY_HUGE_ISLAND_HUGE: "Tiny-Huge Island (Huge)",
+ SM64Levels.TICK_TOCK_CLOCK: "Tick Tock Clock",
+ SM64Levels.RAINBOW_RIDE: "Rainbow Ride"
+}
+sm64_paintings_to_level = {painting: level for (level, painting) in sm64_level_to_paintings.items() }
+
+# sm64secrets is a dict of secret areas, same format as sm64paintings
+sm64_level_to_secrets: typing.Dict[SM64Levels, str] = {
+ SM64Levels.THE_PRINCESS_SECRET_SLIDE: "The Princess's Secret Slide",
+ SM64Levels.THE_SECRET_AQUARIUM: "The Secret Aquarium",
+ SM64Levels.BOWSER_IN_THE_DARK_WORLD: "Bowser in the Dark World",
+ SM64Levels.TOWER_OF_THE_WING_CAP: "Tower of the Wing Cap",
+ SM64Levels.CAVERN_OF_THE_METAL_CAP: "Cavern of the Metal Cap",
+ SM64Levels.VANISH_CAP_UNDER_THE_MOAT: "Vanish Cap under the Moat",
+ SM64Levels.BOWSER_IN_THE_FIRE_SEA: "Bowser in the Fire Sea",
+ SM64Levels.WING_MARIO_OVER_THE_RAINBOW: "Wing Mario over the Rainbow"
+}
+sm64_secrets_to_level = {secret: level for (level,secret) in sm64_level_to_secrets.items() }
+
+sm64_entrances_to_level = {**sm64_paintings_to_level, **sm64_secrets_to_level }
+sm64_level_to_entrances = {**sm64_level_to_paintings, **sm64_level_to_secrets }
+
+def create_regions(world: MultiWorld, options: SM64Options, player: int):
regSS = Region("Menu", player, world, "Castle Area")
- create_default_locs(regSS, locSS_table, player)
+ create_default_locs(regSS, locSS_table)
world.regions.append(regSS)
regBoB = create_region("Bob-omb Battlefield", player, world)
- create_default_locs(regBoB, locBoB_table, player)
- if (world.EnableCoinStars[player].value):
- regBoB.locations.append(SM64Location(player, "BoB: 100 Coins", location_table["BoB: 100 Coins"], regBoB))
- world.regions.append(regBoB)
+ create_locs(regBoB, "BoB: Big Bob-Omb on the Summit", "BoB: Footrace with Koopa The Quick",
+ "BoB: Mario Wings to the Sky", "BoB: Behind Chain Chomp's Gate", "BoB: Bob-omb Buddy")
+ bob_island = create_subregion(regBoB, "BoB: Island", "BoB: Shoot to the Island in the Sky", "BoB: Find the 8 Red Coins")
+ regBoB.subregions = [bob_island]
+ if options.enable_coin_stars:
+ create_locs(regBoB, "BoB: 100 Coins")
regWhomp = create_region("Whomp's Fortress", player, world)
- create_default_locs(regWhomp, locWhomp_table, player)
- if (world.EnableCoinStars[player].value):
- regWhomp.locations.append(SM64Location(player, "WF: 100 Coins", location_table["WF: 100 Coins"], regWhomp))
- world.regions.append(regWhomp)
+ create_locs(regWhomp, "WF: Chip Off Whomp's Block", "WF: Shoot into the Wild Blue", "WF: Red Coins on the Floating Isle",
+ "WF: Fall onto the Caged Island", "WF: Blast Away the Wall")
+ wf_tower = create_subregion(regWhomp, "WF: Tower", "WF: To the Top of the Fortress", "WF: Bob-omb Buddy")
+ regWhomp.subregions = [wf_tower]
+ if options.enable_coin_stars:
+ create_locs(regWhomp, "WF: 100 Coins")
regJRB = create_region("Jolly Roger Bay", player, world)
- create_default_locs(regJRB, locJRB_table, player)
- if (world.EnableCoinStars[player].value):
- regJRB.locations.append(SM64Location(player, "JRB: 100 Coins", location_table["JRB: 100 Coins"], regJRB))
- world.regions.append(regJRB)
+ create_locs(regJRB, "JRB: Plunder in the Sunken Ship", "JRB: Can the Eel Come Out to Play?", "JRB: Treasure of the Ocean Cave",
+ "JRB: Blast to the Stone Pillar", "JRB: Through the Jet Stream", "JRB: Bob-omb Buddy")
+ jrb_upper = create_subregion(regJRB, 'JRB: Upper', "JRB: Red Coins on the Ship Afloat")
+ regJRB.subregions = [jrb_upper]
+ if options.enable_coin_stars:
+ create_locs(jrb_upper, "JRB: 100 Coins")
regCCM = create_region("Cool, Cool Mountain", player, world)
- create_default_locs(regCCM, locCCM_table, player)
- if (world.EnableCoinStars[player].value):
- regCCM.locations.append(SM64Location(player, "CCM: 100 Coins", location_table["CCM: 100 Coins"], regCCM))
- world.regions.append(regCCM)
+ create_default_locs(regCCM, locCCM_table)
+ if options.enable_coin_stars:
+ create_locs(regCCM, "CCM: 100 Coins")
regBBH = create_region("Big Boo's Haunt", player, world)
- create_default_locs(regBBH, locBBH_table, player)
- if (world.EnableCoinStars[player].value):
- regBBH.locations.append(SM64Location(player, "BBH: 100 Coins", location_table["BBH: 100 Coins"], regBBH))
- world.regions.append(regBBH)
+ create_locs(regBBH, "BBH: Go on a Ghost Hunt", "BBH: Ride Big Boo's Merry-Go-Round",
+ "BBH: Secret of the Haunted Books", "BBH: Seek the 8 Red Coins")
+ bbh_third_floor = create_subregion(regBBH, "BBH: Third Floor", "BBH: Eye to Eye in the Secret Room")
+ bbh_roof = create_subregion(bbh_third_floor, "BBH: Roof", "BBH: Big Boo's Balcony", "BBH: 1Up Block Top of Mansion")
+ regBBH.subregions = [bbh_third_floor, bbh_roof]
+ if options.enable_coin_stars:
+ create_locs(regBBH, "BBH: 100 Coins")
regPSS = create_region("The Princess's Secret Slide", player, world)
- create_default_locs(regPSS, locPSS_table, player)
- world.regions.append(regPSS)
+ create_default_locs(regPSS, locPSS_table)
regSA = create_region("The Secret Aquarium", player, world)
- create_default_locs(regSA, locSA_table, player)
- world.regions.append(regSA)
+ create_default_locs(regSA, locSA_table)
regTotWC = create_region("Tower of the Wing Cap", player, world)
- create_default_locs(regTotWC, locTotWC_table, player)
- world.regions.append(regTotWC)
+ create_default_locs(regTotWC, locTotWC_table)
regBitDW = create_region("Bowser in the Dark World", player, world)
- create_default_locs(regBitDW, locBitDW_table, player)
- world.regions.append(regBitDW)
+ create_default_locs(regBitDW, locBitDW_table)
- regBasement = create_region("Basement", player, world)
- world.regions.append(regBasement)
+ create_region("Basement", player, world)
regHMC = create_region("Hazy Maze Cave", player, world)
- create_default_locs(regHMC, locHMC_table, player)
- if (world.EnableCoinStars[player].value):
- regHMC.locations.append(SM64Location(player, "HMC: 100 Coins", location_table["HMC: 100 Coins"], regHMC))
- world.regions.append(regHMC)
+ create_locs(regHMC, "HMC: Swimming Beast in the Cavern", "HMC: Metal-Head Mario Can Move!",
+ "HMC: Watch for Rolling Rocks", "HMC: Navigating the Toxic Maze","HMC: 1Up Block Past Rolling Rocks")
+ hmc_red_coin_area = create_subregion(regHMC, "HMC: Red Coin Area", "HMC: Elevate for 8 Red Coins")
+ hmc_pit_islands = create_subregion(regHMC, "HMC: Pit Islands", "HMC: A-Maze-Ing Emergency Exit", "HMC: 1Up Block above Pit")
+ regHMC.subregions = [hmc_red_coin_area, hmc_pit_islands]
+ if options.enable_coin_stars:
+ create_locs(hmc_red_coin_area, "HMC: 100 Coins")
regLLL = create_region("Lethal Lava Land", player, world)
- create_default_locs(regLLL, locLLL_table, player)
- if (world.EnableCoinStars[player].value):
- regLLL.locations.append(SM64Location(player, "LLL: 100 Coins", location_table["LLL: 100 Coins"], regLLL))
- world.regions.append(regLLL)
+ create_locs(regLLL, "LLL: Boil the Big Bully", "LLL: Bully the Bullies",
+ "LLL: 8-Coin Puzzle with 15 Pieces", "LLL: Red-Hot Log Rolling")
+ lll_upper_volcano = create_subregion(regLLL, "LLL: Upper Volcano", "LLL: Hot-Foot-It into the Volcano", "LLL: Elevator Tour in the Volcano")
+ regLLL.subregions = [lll_upper_volcano]
+ if options.enable_coin_stars:
+ create_locs(regLLL, "LLL: 100 Coins")
regSSL = create_region("Shifting Sand Land", player, world)
- create_default_locs(regSSL, locSSL_table, player)
- if (world.EnableCoinStars[player].value):
- regSSL.locations.append(SM64Location(player, "SSL: 100 Coins", location_table["SSL: 100 Coins"], regSSL))
- world.regions.append(regSSL)
+ create_locs(regSSL, "SSL: In the Talons of the Big Bird", "SSL: Shining Atop the Pyramid",
+ "SSL: Free Flying for 8 Red Coins", "SSL: Bob-omb Buddy",
+ "SSL: 1Up Block Outside Pyramid", "SSL: 1Up Block Pyramid Left Path", "SSL: 1Up Block Pyramid Back")
+ ssl_upper_pyramid = create_subregion(regSSL, "SSL: Upper Pyramid", "SSL: Inside the Ancient Pyramid",
+ "SSL: Stand Tall on the Four Pillars", "SSL: Pyramid Puzzle")
+ regSSL.subregions = [ssl_upper_pyramid]
+ if options.enable_coin_stars:
+ create_locs(regSSL, "SSL: 100 Coins")
regDDD = create_region("Dire, Dire Docks", player, world)
- create_default_locs(regDDD, locDDD_table, player)
- if (world.EnableCoinStars[player].value):
- regDDD.locations.append(SM64Location(player, "DDD: 100 Coins", location_table["DDD: 100 Coins"], regDDD))
- world.regions.append(regDDD)
+ create_locs(regDDD, "DDD: Board Bowser's Sub", "DDD: Chests in the Current", "DDD: Through the Jet Stream",
+ "DDD: The Manta Ray's Reward", "DDD: Collect the Caps...", "DDD: Pole-Jumping for Red Coins")
+ if options.enable_coin_stars:
+ create_locs(regDDD, "DDD: 100 Coins")
regCotMC = create_region("Cavern of the Metal Cap", player, world)
- create_default_locs(regCotMC, locCotMC_table, player)
- world.regions.append(regCotMC)
+ create_default_locs(regCotMC, locCotMC_table)
regVCutM = create_region("Vanish Cap under the Moat", player, world)
- create_default_locs(regVCutM, locVCutM_table, player)
- world.regions.append(regVCutM)
+ create_default_locs(regVCutM, locVCutM_table)
regBitFS = create_region("Bowser in the Fire Sea", player, world)
- create_default_locs(regBitFS, locBitFS_table, player)
- world.regions.append(regBitFS)
+ bitfs_upper = create_subregion(regBitFS, "BitFS: Upper", *locBitFS_table.keys())
+ regBitFS.subregions = [bitfs_upper]
- regFloor2 = create_region("Second Floor", player, world)
- world.regions.append(regFloor2)
+ create_region("Second Floor", player, world)
regSL = create_region("Snowman's Land", player, world)
- create_default_locs(regSL, locSL_table, player)
- if (world.EnableCoinStars[player].value):
- regSL.locations.append(SM64Location(player, "SL: 100 Coins", location_table["SL: 100 Coins"], regSL))
- world.regions.append(regSL)
+ create_default_locs(regSL, locSL_table)
+ if options.enable_coin_stars:
+ create_locs(regSL, "SL: 100 Coins")
regWDW = create_region("Wet-Dry World", player, world)
- create_default_locs(regWDW, locWDW_table, player)
- if (world.EnableCoinStars[player].value):
- regWDW.locations.append(SM64Location(player, "WDW: 100 Coins", location_table["WDW: 100 Coins"], regWDW))
- world.regions.append(regWDW)
+ create_locs(regWDW, "WDW: Express Elevator--Hurry Up!")
+ wdw_top = create_subregion(regWDW, "WDW: Top", "WDW: Shocking Arrow Lifts!", "WDW: Top o' the Town",
+ "WDW: Secrets in the Shallows & Sky", "WDW: Bob-omb Buddy")
+ wdw_downtown = create_subregion(regWDW, "WDW: Downtown", "WDW: Go to Town for Red Coins", "WDW: Quick Race Through Downtown!", "WDW: 1Up Block in Downtown")
+ regWDW.subregions = [wdw_top, wdw_downtown]
+ if options.enable_coin_stars:
+ create_locs(wdw_top, "WDW: 100 Coins")
regTTM = create_region("Tall, Tall Mountain", player, world)
- create_default_locs(regTTM, locTTM_table, player)
- if (world.EnableCoinStars[player].value):
- regTTM.locations.append(SM64Location(player, "TTM: 100 Coins", location_table["TTM: 100 Coins"], regTTM))
- world.regions.append(regTTM)
-
+ ttm_middle = create_subregion(regTTM, "TTM: Middle", "TTM: Scary 'Shrooms, Red Coins", "TTM: Blast to the Lonely Mushroom",
+ "TTM: Bob-omb Buddy", "TTM: 1Up Block on Red Mushroom")
+ ttm_top = create_subregion(ttm_middle, "TTM: Top", "TTM: Scale the Mountain", "TTM: Mystery of the Monkey Cage",
+ "TTM: Mysterious Mountainside", "TTM: Breathtaking View from Bridge")
+ regTTM.subregions = [ttm_middle, ttm_top]
+ if options.enable_coin_stars:
+ create_locs(ttm_top, "TTM: 100 Coins")
+
+ create_region("Tiny-Huge Island (Huge)", player, world)
+ create_region("Tiny-Huge Island (Tiny)", player, world)
regTHI = create_region("Tiny-Huge Island", player, world)
- create_default_locs(regTHI, locTHI_table, player)
- if (world.EnableCoinStars[player].value):
- regTHI.locations.append(SM64Location(player, "THI: 100 Coins", location_table["THI: 100 Coins"], regTHI))
- world.regions.append(regTHI)
+ create_locs(regTHI, "THI: 1Up Block THI Small near Start")
+ thi_pipes = create_subregion(regTHI, "THI: Pipes", "THI: The Tip Top of the Huge Island", "THI: Pluck the Piranha Flower", "THI: Rematch with Koopa the Quick",
+ "THI: Five Itty Bitty Secrets", "THI: Wiggler's Red Coins", "THI: Bob-omb Buddy",
+ "THI: 1Up Block THI Large near Start", "THI: 1Up Block Windy Area")
+ thi_large_top = create_subregion(thi_pipes, "THI: Large Top", "THI: Make Wiggler Squirm")
+ regTHI.subregions = [thi_pipes, thi_large_top]
+ if options.enable_coin_stars:
+ create_locs(thi_large_top, "THI: 100 Coins")
regFloor3 = create_region("Third Floor", player, world)
- world.regions.append(regFloor3)
regTTC = create_region("Tick Tock Clock", player, world)
- create_default_locs(regTTC, locTTC_table, player)
- if (world.EnableCoinStars[player].value):
- regTTC.locations.append(SM64Location(player, "TTC: 100 Coins", location_table["TTC: 100 Coins"], regTTC))
- world.regions.append(regTTC)
+ create_locs(regTTC, "TTC: Stop Time for Red Coins")
+ ttc_lower = create_subregion(regTTC, "TTC: Lower", "TTC: Roll into the Cage", "TTC: Get a Hand")
+ ttc_upper = create_subregion(ttc_lower, "TTC: Upper", "TTC: Timed Jumps on Moving Bars", "TTC: The Pit and the Pendulums")
+ ttc_top = create_subregion(ttc_upper, "TTC: Top", "TTC: 1Up Block Midway Up", "TTC: Stomp on the Thwomp", "TTC: 1Up Block at the Top")
+ regTTC.subregions = [ttc_lower, ttc_upper, ttc_top]
+ if options.enable_coin_stars:
+ create_locs(ttc_top, "TTC: 100 Coins")
regRR = create_region("Rainbow Ride", player, world)
- create_default_locs(regRR, locRR_table, player)
- if (world.EnableCoinStars[player].value):
- regRR.locations.append(SM64Location(player, "RR: 100 Coins", location_table["RR: 100 Coins"], regRR))
- world.regions.append(regRR)
+ create_locs(regRR, "RR: Swingin' in the Breeze", "RR: Tricky Triangles!",
+ "RR: 1Up Block Top of Red Coin Maze", "RR: 1Up Block Under Fly Guy", "RR: Bob-omb Buddy")
+ rr_maze = create_subregion(regRR, "RR: Maze", "RR: Coins Amassed in a Maze")
+ rr_cruiser = create_subregion(regRR, "RR: Cruiser", "RR: Cruiser Crossing the Rainbow", "RR: Somewhere Over the Rainbow")
+ rr_house = create_subregion(regRR, "RR: House", "RR: The Big House in the Sky", "RR: 1Up Block On House in the Sky")
+ regRR.subregions = [rr_maze, rr_cruiser, rr_house]
+ if options.enable_coin_stars:
+ create_locs(rr_maze, "RR: 100 Coins")
regWMotR = create_region("Wing Mario over the Rainbow", player, world)
- create_default_locs(regWMotR, locWMotR_table, player)
- world.regions.append(regWMotR)
+ create_default_locs(regWMotR, locWMotR_table)
regBitS = create_region("Bowser in the Sky", player, world)
- create_default_locs(regBitS, locBitS_table, player)
- world.regions.append(regBitS)
+ create_locs(regBitS, "Bowser in the Sky 1Up Block")
+ bits_top = create_subregion(regBitS, "BitS: Top", "Bowser in the Sky Red Coins")
+ regBitS.subregions = [bits_top]
def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None):
sourceRegion = world.get_region(source, player)
targetRegion = world.get_region(target, player)
+ sourceRegion.connect(targetRegion, rule=rule)
+
+
+def create_region(name: str, player: int, world: MultiWorld) -> SM64Region:
+ region = SM64Region(name, player, world)
+ world.regions.append(region)
+ return region
+
+
+def create_subregion(source_region: Region, name: str, *locs: str) -> SM64Region:
+ region = SM64Region(name, source_region.player, source_region.multiworld)
+ connection = Entrance(source_region.player, name, source_region)
+ source_region.exits.append(connection)
+ connection.connect(region)
+ source_region.multiworld.regions.append(region)
+ create_locs(region, *locs)
+ return region
+
+
+def set_subregion_access_rule(world, player, region_name: str, rule):
+ world.get_entrance(world, player, region_name).access_rule = rule
- connection = Entrance(player, '', sourceRegion)
- if rule:
- connection.access_rule = rule
- sourceRegion.exits.append(connection)
- connection.connect(targetRegion)
+def create_default_locs(reg: Region, default_locs: dict):
+ create_locs(reg, *default_locs.keys())
-def create_region(name: str, player: int, world: MultiWorld) -> Region:
- return Region(name, player, world)
-def create_default_locs(reg: Region, locs, player):
- reg_names = [name for name, id in locs.items()]
- reg.locations += [SM64Location(player, loc_name, location_table[loc_name], reg) for loc_name in locs]
+def create_locs(reg: Region, *locs: str):
+ reg.locations += [SM64Location(reg.player, loc_name, location_table[loc_name], reg) for loc_name in locs]
diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py
index 7c50ba4708af..9add8d9b2932 100644
--- a/worlds/sm64ex/Rules.py
+++ b/worlds/sm64ex/Rules.py
@@ -1,127 +1,377 @@
-from ..generic.Rules import add_rule
-from .Regions import connect_regions, sm64courses, sm64paintings, sm64secrets, sm64entrances
-
-def fix_reg(entrance_ids, reg, invalidspot, swaplist, world):
- if entrance_ids.index(reg) == invalidspot: # Unlucky :C
- swaplist.remove(invalidspot)
- rand = world.random.choice(swaplist)
- entrance_ids[invalidspot], entrance_ids[rand] = entrance_ids[rand], entrance_ids[invalidspot]
- swaplist.append(invalidspot)
- swaplist.remove(rand)
-
-def set_rules(world, player: int, area_connections):
- destination_regions = list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets))) # Two instances of Destination Course THI. Past normal course idx are secret regions
- secret_entrance_ids = list(range(len(sm64paintings), len(sm64paintings) + len(sm64secrets)))
- course_entrance_ids = list(range(len(sm64paintings)))
- if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses
- world.random.shuffle(course_entrance_ids)
- if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well
- world.random.shuffle(secret_entrance_ids)
- entrance_ids = course_entrance_ids + secret_entrance_ids
- if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool
- world.random.shuffle(entrance_ids)
+from typing import Callable, Union, Dict, Set
+
+from BaseClasses import MultiWorld
+from ..generic.Rules import add_rule, set_rule
+from .Locations import location_table
+from .Options import SM64Options
+from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level,\
+sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances
+from .Items import action_item_table
+
+def shuffle_dict_keys(world, dictionary: dict) -> dict:
+ keys = list(dictionary.keys())
+ values = list(dictionary.values())
+ world.random.shuffle(keys)
+ return dict(zip(keys, values))
+
+def fix_reg(entrance_map: Dict[SM64Levels, str], entrance: SM64Levels, invalid_regions: Set[str],
+ swapdict: Dict[SM64Levels, str], world):
+ if entrance_map[entrance] in invalid_regions: # Unlucky :C
+ replacement_regions = [(rand_entrance, rand_region) for rand_entrance, rand_region in swapdict.items()
+ if rand_region not in invalid_regions]
+ rand_entrance, rand_region = world.random.choice(replacement_regions)
+ old_dest = entrance_map[entrance]
+ entrance_map[entrance], entrance_map[rand_entrance] = rand_region, old_dest
+ swapdict[entrance], swapdict[rand_entrance] = rand_region, old_dest
+ swapdict.pop(entrance)
+
+def set_rules(world, options: SM64Options, player: int, area_connections: dict, star_costs: dict, move_rando_bitvec: int):
+ randomized_level_to_paintings = sm64_level_to_paintings.copy()
+ randomized_level_to_secrets = sm64_level_to_secrets.copy()
+ valid_move_randomizer_start_courses = [
+ "Bob-omb Battlefield", "Jolly Roger Bay", "Cool, Cool Mountain",
+ "Big Boo's Haunt", "Lethal Lava Land", "Shifting Sand Land",
+ "Dire, Dire Docks", "Snowman's Land"
+ ] # Excluding WF, HMC, WDW, TTM, THI, TTC, and RR
+ if options.area_rando >= 1: # Some randomization is happening, randomize Courses
+ randomized_level_to_paintings = shuffle_dict_keys(world,sm64_level_to_paintings)
+ # If not shuffling later, ensure a valid start course on move randomizer
+ if options.area_rando < 3 and move_rando_bitvec > 0:
+ swapdict = randomized_level_to_paintings.copy()
+ invalid_start_courses = {course for course in randomized_level_to_paintings.values() if course not in valid_move_randomizer_start_courses}
+ fix_reg(randomized_level_to_paintings, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, world)
+ fix_reg(randomized_level_to_paintings, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, world)
+
+ if options.area_rando == 2: # Randomize Secrets as well
+ randomized_level_to_secrets = shuffle_dict_keys(world,sm64_level_to_secrets)
+ randomized_entrances = {**randomized_level_to_paintings, **randomized_level_to_secrets}
+ if options.area_rando == 3: # Randomize Courses and Secrets in one pool
+ randomized_entrances = shuffle_dict_keys(world, randomized_entrances)
# Guarantee first entrance is a course
- swaplist = list(range(len(entrance_ids)))
- if entrance_ids.index(0) > 15: # Unlucky :C
- rand = world.random.randint(0,15)
- entrance_ids[entrance_ids.index(0)], entrance_ids[rand] = entrance_ids[rand], entrance_ids[entrance_ids.index(0)]
- swaplist.remove(entrance_ids.index(0))
- # Guarantee COTMC is not mapped to HMC, cuz thats impossible
- fix_reg(entrance_ids, 20, 5, swaplist, world)
+ swapdict = randomized_entrances.copy()
+ if move_rando_bitvec == 0:
+ fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, world)
+ else:
+ invalid_start_courses = {course for course in randomized_entrances.values() if course not in valid_move_randomizer_start_courses}
+ fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, world)
+ fix_reg(randomized_entrances, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, world)
# Guarantee BITFS is not mapped to DDD
- fix_reg(entrance_ids, 22, 8, swaplist, world)
- if entrance_ids.index(22) == 5: # If BITFS is mapped to HMC...
- fix_reg(entrance_ids, 20, 8, swaplist, world) # ... then dont allow COTMC to be mapped to DDD
- temp_assign = dict(zip(entrance_ids,destination_regions)) # Used for Rules only
+ fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, world)
+ # Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD.
+ if randomized_entrances[SM64Levels.BOWSER_IN_THE_FIRE_SEA] == "Hazy Maze Cave":
+ fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave", "Dire, Dire Docks"}, swapdict, world)
+ else:
+ fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave"}, swapdict, world)
# Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code
- area_connections.update({sm64entrances[entrance]: destination for entrance, destination in zip(entrance_ids,sm64entrances)})
-
- connect_regions(world, player, "Menu", sm64courses[temp_assign[0]]) # BOB
- connect_regions(world, player, "Menu", sm64courses[temp_assign[1]], lambda state: state.has("Power Star", player, 1)) # WF
- connect_regions(world, player, "Menu", sm64courses[temp_assign[2]], lambda state: state.has("Power Star", player, 3)) # JRB
- connect_regions(world, player, "Menu", sm64courses[temp_assign[3]], lambda state: state.has("Power Star", player, 3)) # CCM
- connect_regions(world, player, "Menu", sm64courses[temp_assign[4]], lambda state: state.has("Power Star", player, 12)) # BBH
- connect_regions(world, player, "Menu", sm64courses[temp_assign[16]], lambda state: state.has("Power Star", player, 1)) # PSS
- connect_regions(world, player, "Menu", sm64courses[temp_assign[17]], lambda state: state.has("Power Star", player, 3)) # SA
- connect_regions(world, player, "Menu", sm64courses[temp_assign[19]], lambda state: state.has("Power Star", player, 10)) # TOTWC
- connect_regions(world, player, "Menu", sm64courses[temp_assign[18]], lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value)) # BITDW
+ # Cast to int to not rely on availability of SM64Levels enum. Will cause crash in MultiServer otherwise
+ area_connections.update({int(entrance_lvl): int(sm64_entrances_to_level[destination]) for (entrance_lvl,destination) in randomized_entrances.items()})
+ randomized_entrances_s = {sm64_level_to_entrances[entrance_lvl]: destination for (entrance_lvl,destination) in randomized_entrances.items()}
+
+ rf = RuleFactory(world, options, player, move_rando_bitvec)
+
+ connect_regions(world, player, "Menu", randomized_entrances_s["Bob-omb Battlefield"])
+ connect_regions(world, player, "Menu", randomized_entrances_s["Whomp's Fortress"], lambda state: state.has("Power Star", player, 1))
+ connect_regions(world, player, "Menu", randomized_entrances_s["Jolly Roger Bay"], lambda state: state.has("Power Star", player, 3))
+ connect_regions(world, player, "Menu", randomized_entrances_s["Cool, Cool Mountain"], lambda state: state.has("Power Star", player, 3))
+ connect_regions(world, player, "Menu", randomized_entrances_s["Big Boo's Haunt"], lambda state: state.has("Power Star", player, 12))
+ connect_regions(world, player, "Menu", randomized_entrances_s["The Princess's Secret Slide"], lambda state: state.has("Power Star", player, 1))
+ connect_regions(world, player, randomized_entrances_s["Jolly Roger Bay"], randomized_entrances_s["The Secret Aquarium"],
+ rf.build_rule("SF/BF | TJ & LG | MOVELESS & TJ"))
+ connect_regions(world, player, "Menu", randomized_entrances_s["Tower of the Wing Cap"], lambda state: state.has("Power Star", player, 10))
+ connect_regions(world, player, "Menu", randomized_entrances_s["Bowser in the Dark World"],
+ lambda state: state.has("Power Star", player, star_costs["FirstBowserDoorCost"]))
connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player) or state.has("Progressive Key", player, 1))
- connect_regions(world, player, "Basement", sm64courses[temp_assign[5]]) # HMC
- connect_regions(world, player, "Basement", sm64courses[temp_assign[6]]) # LLL
- connect_regions(world, player, "Basement", sm64courses[temp_assign[7]]) # SSL
- connect_regions(world, player, "Basement", sm64courses[temp_assign[8]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value)) # DDD
- connect_regions(world, player, "Hazy Maze Cave", sm64courses[temp_assign[20]]) # COTMC
- connect_regions(world, player, "Basement", sm64courses[temp_assign[21]]) # VCUTM
- connect_regions(world, player, "Basement", sm64courses[temp_assign[22]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and
- state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) # BITFS
+ connect_regions(world, player, "Basement", randomized_entrances_s["Hazy Maze Cave"])
+ connect_regions(world, player, "Basement", randomized_entrances_s["Lethal Lava Land"])
+ connect_regions(world, player, "Basement", randomized_entrances_s["Shifting Sand Land"])
+ connect_regions(world, player, "Basement", randomized_entrances_s["Dire, Dire Docks"],
+ lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]))
+ connect_regions(world, player, "Hazy Maze Cave", randomized_entrances_s["Cavern of the Metal Cap"])
+ connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"],
+ rf.build_rule("GP"))
+ connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"],
+ lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and
+ state.can_reach("DDD: Board Bowser's Sub", 'Location', player))
connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2))
- connect_regions(world, player, "Second Floor", sm64courses[temp_assign[9]]) # SL
- connect_regions(world, player, "Second Floor", sm64courses[temp_assign[10]]) # WDW
- connect_regions(world, player, "Second Floor", sm64courses[temp_assign[11]]) # TTM
- connect_regions(world, player, "Second Floor", sm64courses[temp_assign[12]]) # THI Tiny
- connect_regions(world, player, "Second Floor", sm64courses[temp_assign[13]]) # THI Huge
-
- connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, world.SecondFloorStarDoorCost[player].value))
-
- connect_regions(world, player, "Third Floor", sm64courses[temp_assign[14]]) # TTC
- connect_regions(world, player, "Third Floor", sm64courses[temp_assign[15]]) # RR
- connect_regions(world, player, "Third Floor", sm64courses[temp_assign[23]]) # WMOTR
- connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, world.StarsToFinish[player].value)) # BITS
-
- #Special Rules for some Locations
- add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Cannon Unlock BoB", player))
- add_rule(world.get_location("BBH: Eye to Eye in the Secret Room", player), lambda state: state.has("Vanish Cap", player))
- add_rule(world.get_location("DDD: Collect the Caps...", player), lambda state: state.has("Vanish Cap", player))
- add_rule(world.get_location("DDD: Pole-Jumping for Red Coins", player), lambda state: state.can_reach("Bowser in the Fire Sea", 'Region', player))
- if world.EnableCoinStars[player]:
- add_rule(world.get_location("DDD: 100 Coins", player), lambda state: state.can_reach("Bowser in the Fire Sea", 'Region', player))
- add_rule(world.get_location("SL: Into the Igloo", player), lambda state: state.has("Vanish Cap", player))
- add_rule(world.get_location("WDW: Quick Race Through Downtown!", player), lambda state: state.has("Vanish Cap", player))
- add_rule(world.get_location("RR: Somewhere Over the Rainbow", player), lambda state: state.has("Cannon Unlock RR", player))
-
- if world.AreaRandomizer[player] or world.StrictCannonRequirements[player]:
- # If area rando is on, it may not be possible to modify WDW's starting water level,
- # which would make it impossible to reach downtown area without the cannon.
- add_rule(world.get_location("WDW: Quick Race Through Downtown!", player), lambda state: state.has("Cannon Unlock WDW", player))
- add_rule(world.get_location("WDW: Go to Town for Red Coins", player), lambda state: state.has("Cannon Unlock WDW", player))
- add_rule(world.get_location("WDW: 1Up Block in Downtown", player), lambda state: state.has("Cannon Unlock WDW", player))
-
- if world.StrictCapRequirements[player]:
- add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Wing Cap", player))
- add_rule(world.get_location("HMC: Metal-Head Mario Can Move!", player), lambda state: state.has("Metal Cap", player))
- add_rule(world.get_location("JRB: Through the Jet Stream", player), lambda state: state.has("Metal Cap", player))
- add_rule(world.get_location("SSL: Free Flying for 8 Red Coins", player), lambda state: state.has("Wing Cap", player))
- add_rule(world.get_location("DDD: Through the Jet Stream", player), lambda state: state.has("Metal Cap", player))
- add_rule(world.get_location("DDD: Collect the Caps...", player), lambda state: state.has("Metal Cap", player))
- add_rule(world.get_location("Vanish Cap Under the Moat Red Coins", player), lambda state: state.has("Vanish Cap", player))
- add_rule(world.get_location("Cavern of the Metal Cap Red Coins", player), lambda state: state.has("Metal Cap", player))
- if world.StrictCannonRequirements[player]:
- add_rule(world.get_location("WF: Blast Away the Wall", player), lambda state: state.has("Cannon Unlock WF", player))
- add_rule(world.get_location("JRB: Blast to the Stone Pillar", player), lambda state: state.has("Cannon Unlock JRB", player))
- add_rule(world.get_location("CCM: Wall Kicks Will Work", player), lambda state: state.has("Cannon Unlock CCM", player))
- add_rule(world.get_location("TTM: Blast to the Lonely Mushroom", player), lambda state: state.has("Cannon Unlock TTM", player))
- if world.StrictCapRequirements[player] and world.StrictCannonRequirements[player]:
- # Ability to reach the floating island. Need some of those coins to get 100 coin star as well.
- add_rule(world.get_location("BoB: Find the 8 Red Coins", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player))
- add_rule(world.get_location("BoB: Shoot to the Island in the Sky", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player))
- if world.EnableCoinStars[player]:
- add_rule(world.get_location("BoB: 100 Coins", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player))
-
- #Rules for Secret Stars
- add_rule(world.get_location("Wing Mario Over the Rainbow Red Coins", player), lambda state: state.has("Wing Cap", player))
- add_rule(world.get_location("Wing Mario Over the Rainbow 1Up Block", player), lambda state: state.has("Wing Cap", player))
+ connect_regions(world, player, "Second Floor", randomized_entrances_s["Snowman's Land"])
+ connect_regions(world, player, "Second Floor", randomized_entrances_s["Wet-Dry World"])
+ connect_regions(world, player, "Second Floor", randomized_entrances_s["Tall, Tall Mountain"])
+ connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Tiny)"])
+ connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Huge)"])
+ connect_regions(world, player, "Tiny-Huge Island (Tiny)", "Tiny-Huge Island")
+ connect_regions(world, player, "Tiny-Huge Island (Huge)", "Tiny-Huge Island")
+
+ connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, star_costs["SecondFloorDoorCost"]))
+
+ connect_regions(world, player, "Third Floor", randomized_entrances_s["Tick Tock Clock"], rf.build_rule("LG/TJ/SF/BF/WK"))
+ connect_regions(world, player, "Third Floor", randomized_entrances_s["Rainbow Ride"], rf.build_rule("TJ/SF/BF"))
+ connect_regions(world, player, "Third Floor", randomized_entrances_s["Wing Mario over the Rainbow"], rf.build_rule("TJ/SF/BF"))
+ connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, star_costs["StarsToFinish"]))
+
+ # Course Rules
+ # Bob-omb Battlefield
+ rf.assign_rule("BoB: Island", "CANN | CANNLESS & WC & TJ | CAPLESS & CANNLESS & LJ")
+ rf.assign_rule("BoB: Mario Wings to the Sky", "CANN & WC | CAPLESS & CANN")
+ rf.assign_rule("BoB: Behind Chain Chomp's Gate", "GP | MOVELESS")
+ # Whomp's Fortress
+ rf.assign_rule("WF: Tower", "GP")
+ rf.assign_rule("WF: Chip Off Whomp's Block", "GP")
+ rf.assign_rule("WF: Shoot into the Wild Blue", "WK & TJ/SF | CANN")
+ rf.assign_rule("WF: Fall onto the Caged Island", "CL & {WF: Tower} | MOVELESS & TJ | MOVELESS & LJ | MOVELESS & CANN")
+ rf.assign_rule("WF: Blast Away the Wall", "CANN | CANNLESS & LG")
+ # Jolly Roger Bay
+ rf.assign_rule("JRB: Upper", "TJ/BF/SF/WK | MOVELESS & LG")
+ rf.assign_rule("JRB: Red Coins on the Ship Afloat", "CL/CANN/TJ | MOVELESS & BF/WK")
+ rf.assign_rule("JRB: Blast to the Stone Pillar", "CANN+CL | CANNLESS & MOVELESS | CANN & MOVELESS")
+ rf.assign_rule("JRB: Through the Jet Stream", "MC | CAPLESS")
+ # Cool, Cool Mountain
+ rf.assign_rule("CCM: Wall Kicks Will Work", "TJ/WK & CANN | CANNLESS & TJ/WK | MOVELESS")
+ # Big Boo's Haunt
+ rf.assign_rule("BBH: Third Floor", "WK+LG | MOVELESS & WK")
+ rf.assign_rule("BBH: Roof", "LJ | MOVELESS")
+ rf.assign_rule("BBH: Secret of the Haunted Books", "KK | MOVELESS")
+ rf.assign_rule("BBH: Seek the 8 Red Coins", "BF/WK/TJ/SF")
+ rf.assign_rule("BBH: Eye to Eye in the Secret Room", "VC")
+ # Haze Maze Cave
+ rf.assign_rule("HMC: Red Coin Area", "CL & WK/LG/BF/SF/TJ | MOVELESS & WK")
+ rf.assign_rule("HMC: Pit Islands", "TJ+CL | MOVELESS & WK & TJ/LJ | MOVELESS & WK+SF+LG")
+ rf.assign_rule("HMC: Metal-Head Mario Can Move!", "LJ+MC | CAPLESS & LJ+TJ | CAPLESS & MOVELESS & LJ/TJ/WK")
+ rf.assign_rule("HMC: Navigating the Toxic Maze", "WK/SF/BF/TJ")
+ rf.assign_rule("HMC: Watch for Rolling Rocks", "WK")
+ # Lethal Lava Land
+ rf.assign_rule("LLL: Upper Volcano", "CL")
+ # Shifting Sand Land
+ rf.assign_rule("SSL: Upper Pyramid", "CL & TJ/BF/SF/LG | MOVELESS")
+ rf.assign_rule("SSL: Stand Tall on the Four Pillars", "TJ+WC+GP | CANN+WC+GP | TJ/SF/BF & CAPLESS | MOVELESS")
+ rf.assign_rule("SSL: Free Flying for 8 Red Coins", "TJ+WC | CANN+WC | TJ/SF/BF & CAPLESS | MOVELESS & CAPLESS")
+ # Dire, Dire Docks
+ rf.assign_rule("DDD: Pole-Jumping for Red Coins", "CL & {{Bowser in the Fire Sea Key}} | TJ+DV+LG+WK & MOVELESS")
+ rf.assign_rule("DDD: Through the Jet Stream", "MC | CAPLESS")
+ rf.assign_rule("DDD: Collect the Caps...", "VC+MC | CAPLESS & VC")
+ # Snowman's Land
+ rf.assign_rule("SL: Snowman's Big Head", "BF/SF/CANN/TJ")
+ rf.assign_rule("SL: In the Deep Freeze", "WK/SF/LG/BF/CANN/TJ")
+ rf.assign_rule("SL: Into the Igloo", "VC & TJ/SF/BF/WK/LG | MOVELESS & VC")
+ # Wet-Dry World
+ rf.assign_rule("WDW: Top", "WK/TJ/SF/BF | MOVELESS")
+ rf.assign_rule("WDW: Downtown", "NAR & LG & TJ/SF/BF | CANN | MOVELESS & TJ+DV")
+ rf.assign_rule("WDW: Go to Town for Red Coins", "WK | MOVELESS & TJ")
+ rf.assign_rule("WDW: Quick Race Through Downtown!", "VC & WK/BF | VC & TJ+LG | MOVELESS & VC & TJ")
+ rf.assign_rule("WDW: Bob-omb Buddy", "TJ | SF+LG | NAR & BF/SF")
+ # Tall, Tall Mountain
+ rf.assign_rule("TTM: Top", "MOVELESS & TJ | LJ/DV & LG/KK | MOVELESS & WK & SF/LG | MOVELESS & KK/DV")
+ rf.assign_rule("TTM: Blast to the Lonely Mushroom", "CANN | CANNLESS & LJ | MOVELESS & CANNLESS")
+ # Tiny-Huge Island
+ rf.assign_rule("THI: 1Up Block THI Small near Start", "NAR | {THI: Pipes}")
+ rf.assign_rule("THI: Pipes", "NAR | LJ/TJ/DV/LG | MOVELESS & BF/SF/KK")
+ rf.assign_rule("THI: Large Top", "NAR | LJ/TJ/DV | MOVELESS")
+ rf.assign_rule("THI: Wiggler's Red Coins", "WK")
+ rf.assign_rule("THI: Make Wiggler Squirm", "GP | MOVELESS & DV")
+ # Tick Tock Clock
+ rf.assign_rule("TTC: Lower", "LG/TJ/SF/BF/WK")
+ rf.assign_rule("TTC: Upper", "CL | MOVELESS & WK")
+ rf.assign_rule("TTC: Top", "TJ+LG | MOVELESS & WK/TJ")
+ rf.assign_rule("TTC: Stop Time for Red Coins", "NAR | {TTC: Lower}")
+ # Rainbow Ride
+ rf.assign_rule("RR: Maze", "WK | LJ & SF/BF/TJ | MOVELESS & LG/TJ")
+ rf.assign_rule("RR: Bob-omb Buddy", "WK | MOVELESS & LG")
+ rf.assign_rule("RR: Swingin' in the Breeze", "LG/TJ/BF/SF | MOVELESS")
+ rf.assign_rule("RR: Tricky Triangles!", "LG/TJ/BF/SF | MOVELESS")
+ rf.assign_rule("RR: Cruiser", "WK/SF/BF/LG/TJ")
+ rf.assign_rule("RR: House", "TJ/SF/BF/LG")
+ rf.assign_rule("RR: Somewhere Over the Rainbow", "CANN")
+ # Cavern of the Metal Cap
+ rf.assign_rule("Cavern of the Metal Cap Red Coins", "MC | CAPLESS")
+ # Vanish Cap Under the Moat
+ rf.assign_rule("Vanish Cap Under the Moat Switch", "WK/TJ/BF/SF/LG | MOVELESS")
+ rf.assign_rule("Vanish Cap Under the Moat Red Coins", "TJ/BF/SF/LG/WK & VC | CAPLESS & WK")
+ # Bowser in the Fire Sea
+ rf.assign_rule("BitFS: Upper", "CL")
+ rf.assign_rule("Bowser in the Fire Sea Red Coins", "LG/WK")
+ rf.assign_rule("Bowser in the Fire Sea 1Up Block Near Poles", "LG/WK")
+ # Wing Mario Over the Rainbow
+ rf.assign_rule("Wing Mario Over the Rainbow Red Coins", "TJ+WC")
+ rf.assign_rule("Wing Mario Over the Rainbow 1Up Block", "TJ+WC")
+ # Bowser in the Sky
+ rf.assign_rule("BitS: Top", "CL+TJ | CL+SF+LG | MOVELESS & TJ+WK+LG")
+ # 100 Coin Stars
+ if options.enable_coin_stars:
+ rf.assign_rule("BoB: 100 Coins", "CANN & WC | CANNLESS & WC & TJ")
+ rf.assign_rule("WF: 100 Coins", "GP | MOVELESS")
+ rf.assign_rule("JRB: 100 Coins", "GP & {JRB: Upper}")
+ rf.assign_rule("HMC: 100 Coins", "GP")
+ rf.assign_rule("SSL: 100 Coins", "{SSL: Upper Pyramid} | GP")
+ rf.assign_rule("DDD: 100 Coins", "GP & {{DDD: Pole-Jumping for Red Coins}}")
+ rf.assign_rule("SL: 100 Coins", "VC | CAPLESS")
+ rf.assign_rule("WDW: 100 Coins", "GP | {WDW: Downtown}")
+ rf.assign_rule("TTC: 100 Coins", "GP")
+ rf.assign_rule("THI: 100 Coins", "GP")
+ rf.assign_rule("RR: 100 Coins", "GP & WK")
+ # Castle Stars
add_rule(world.get_location("Toad (Basement)", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, 12))
add_rule(world.get_location("Toad (Second Floor)", player), lambda state: state.can_reach("Second Floor", 'Region', player) and state.has("Power Star", player, 25))
add_rule(world.get_location("Toad (Third Floor)", player), lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Power Star", player, 35))
- if world.MIPS1Cost[player].value > world.MIPS2Cost[player].value:
- (world.MIPS2Cost[player].value, world.MIPS1Cost[player].value) = (world.MIPS1Cost[player].value, world.MIPS2Cost[player].value)
- add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS1Cost[player].value))
- add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS2Cost[player].value))
+ if star_costs["MIPS1Cost"] > star_costs["MIPS2Cost"]:
+ (star_costs["MIPS2Cost"], star_costs["MIPS1Cost"]) = (star_costs["MIPS1Cost"], star_costs["MIPS2Cost"])
+ rf.assign_rule("MIPS 1", "DV | MOVELESS")
+ rf.assign_rule("MIPS 2", "DV | MOVELESS")
+ add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, star_costs["MIPS1Cost"]))
+ add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, star_costs["MIPS2Cost"]))
+
+ world.completion_condition[player] = lambda state: state.can_reach("BitS: Top", 'Region', player)
+
+ if options.completion_type == "last_bowser_stage":
+ world.completion_condition[player] = lambda state: state.can_reach("BitS: Top", 'Region', player)
+ elif options.completion_type == "all_bowser_stages":
+ world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Dark World", 'Region', player) and \
+ state.can_reach("BitFS: Upper", 'Region', player) and \
+ state.can_reach("BitS: Top", 'Region', player)
+
+
+class RuleFactory:
+
+ world: MultiWorld
+ player: int
+ move_rando_bitvec: bool
+ area_randomizer: bool
+ capless: bool
+ cannonless: bool
+ moveless: bool
+
+ token_table = {
+ "TJ": "Triple Jump",
+ "DJ": "Triple Jump",
+ "LJ": "Long Jump",
+ "BF": "Backflip",
+ "SF": "Side Flip",
+ "WK": "Wall Kick",
+ "DV": "Dive",
+ "GP": "Ground Pound",
+ "KK": "Kick",
+ "CL": "Climb",
+ "LG": "Ledge Grab",
+ "WC": "Wing Cap",
+ "MC": "Metal Cap",
+ "VC": "Vanish Cap"
+ }
+
+ class SM64LogicException(Exception):
+ pass
+
+ def __init__(self, world, options: SM64Options, player: int, move_rando_bitvec: int):
+ self.world = world
+ self.player = player
+ self.move_rando_bitvec = move_rando_bitvec
+ self.area_randomizer = options.area_rando > 0
+ self.capless = not options.strict_cap_requirements
+ self.cannonless = not options.strict_cannon_requirements
+ self.moveless = not options.strict_move_requirements
+
+ def assign_rule(self, target_name: str, rule_expr: str):
+ target = self.world.get_location(target_name, self.player) if target_name in location_table else self.world.get_entrance(target_name, self.player)
+ cannon_name = "Cannon Unlock " + target_name.split(':')[0]
+ try:
+ rule = self.build_rule(rule_expr, cannon_name)
+ except RuleFactory.SM64LogicException as exception:
+ raise RuleFactory.SM64LogicException(
+ f"Error generating rule for {target_name} using rule expression {rule_expr}: {exception}")
+ if rule:
+ set_rule(target, rule)
+
+ def build_rule(self, rule_expr: str, cannon_name: str = '') -> Callable:
+ expressions = rule_expr.split(" | ")
+ rules = []
+ for expression in expressions:
+ or_clause = self.combine_and_clauses(expression, cannon_name)
+ if or_clause is True:
+ return None
+ if or_clause is not False:
+ rules.append(or_clause)
+ if rules:
+ if len(rules) == 1:
+ return rules[0]
+ else:
+ return lambda state: any(rule(state) for rule in rules)
+ else:
+ return None
+
+ def combine_and_clauses(self, rule_expr: str, cannon_name: str) -> Union[Callable, bool]:
+ expressions = rule_expr.split(" & ")
+ rules = []
+ for expression in expressions:
+ and_clause = self.make_lambda(expression, cannon_name)
+ if and_clause is False:
+ return False
+ if and_clause is not True:
+ rules.append(and_clause)
+ if rules:
+ if len(rules) == 1:
+ return rules[0]
+ return lambda state: all(rule(state) for rule in rules)
+ else:
+ return True
+
+ def make_lambda(self, expression: str, cannon_name: str) -> Union[Callable, bool]:
+ if '+' in expression:
+ tokens = expression.split('+')
+ items = set()
+ for token in tokens:
+ item = self.parse_token(token, cannon_name)
+ if item is True:
+ continue
+ if item is False:
+ return False
+ items.add(item)
+ if items:
+ return lambda state: state.has_all(items, self.player)
+ else:
+ return True
+ if '/' in expression:
+ tokens = expression.split('/')
+ items = set()
+ for token in tokens:
+ item = self.parse_token(token, cannon_name)
+ if item is True:
+ return True
+ if item is False:
+ continue
+ items.add(item)
+ if items:
+ return lambda state: state.has_any(items, self.player)
+ else:
+ return False
+ if '{{' in expression:
+ return lambda state: state.can_reach(expression[2:-2], "Location", self.player)
+ if '{' in expression:
+ return lambda state: state.can_reach(expression[1:-1], "Region", self.player)
+ item = self.parse_token(expression, cannon_name)
+ if item in (True, False):
+ return item
+ return lambda state: state.has(item, self.player)
+
+ def parse_token(self, token: str, cannon_name: str) -> Union[str, bool]:
+ if token == "CANN":
+ return cannon_name
+ if token == "CAPLESS":
+ return self.capless
+ if token == "CANNLESS":
+ return self.cannonless
+ if token == "MOVELESS":
+ return self.moveless
+ if token == "NAR":
+ return not self.area_randomizer
+ item = self.token_table.get(token, None)
+ if not item:
+ raise Exception(f"Invalid token: '{item}'")
+ if item in action_item_table:
+ if self.move_rando_bitvec & (1 << (action_item_table[item] - action_item_table['Double Jump'])) == 0:
+ # This action item is not randomized.
+ return True
+ return item
- world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player)
diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py
index 6a7a3bd27214..833ae56ca302 100644
--- a/worlds/sm64ex/__init__.py
+++ b/worlds/sm64ex/__init__.py
@@ -1,12 +1,12 @@
import typing
import os
import json
-from .Items import item_table, cannon_item_table, SM64Item
+from .Items import item_table, action_item_table, cannon_item_table, SM64Item
from .Locations import location_table, SM64Location
-from .Options import sm64_options
+from .Options import SM64Options
from .Rules import set_rules
-from .Regions import create_regions, sm64courses, sm64entrances_s, sm64_internalloc_to_string, sm64_internalloc_to_regionid
-from BaseClasses import Item, Tutorial, ItemClassification
+from .Regions import create_regions, sm64_level_to_entrances, SM64Levels
+from BaseClasses import Item, Tutorial, ItemClassification, Region
from ..AutoWorld import World, WebWorld
@@ -35,28 +35,55 @@ class SM64World(World):
item_name_to_id = item_table
location_name_to_id = location_table
- data_version = 8
required_client_version = (0, 3, 5)
area_connections: typing.Dict[int, int]
- option_definitions = sm64_options
+ options_dataclass = SM64Options
+
+ number_of_stars: int
+ move_rando_bitvec: int
+ filler_count: int
+ star_costs: typing.Dict[str, int]
def generate_early(self):
- self.topology_present = self.multiworld.AreaRandomizer[self.player].value
+ max_stars = 120
+ if (not self.options.enable_coin_stars):
+ max_stars -= 15
+ self.move_rando_bitvec = 0
+ if self.options.enable_move_rando:
+ for action in self.options.move_rando_actions.value:
+ max_stars -= 1
+ self.move_rando_bitvec |= (1 << (action_item_table[action] - action_item_table['Double Jump']))
+ if (self.options.exclamation_boxes > 0):
+ max_stars += 29
+ self.number_of_stars = min(self.options.amount_of_stars, max_stars)
+ self.filler_count = max_stars - self.number_of_stars
+ self.star_costs = {
+ 'FirstBowserDoorCost': round(self.options.first_bowser_star_door_cost * self.number_of_stars / 100),
+ 'BasementDoorCost': round(self.options.basement_star_door_cost * self.number_of_stars / 100),
+ 'SecondFloorDoorCost': round(self.options.second_floor_star_door_cost * self.number_of_stars / 100),
+ 'MIPS1Cost': round(self.options.mips1_cost * self.number_of_stars / 100),
+ 'MIPS2Cost': round(self.options.mips2_cost * self.number_of_stars / 100),
+ 'StarsToFinish': round(self.options.stars_to_finish * self.number_of_stars / 100)
+ }
+ # Nudge MIPS 1 to match vanilla on default percentage
+ if self.number_of_stars == 120 and self.options.mips1_cost == 12:
+ self.star_costs['MIPS1Cost'] = 15
+ self.topology_present = self.options.area_rando
def create_regions(self):
- create_regions(self.multiworld, self.player)
+ create_regions(self.multiworld, self.options, self.player)
def set_rules(self):
self.area_connections = {}
- set_rules(self.multiworld, self.player, self.area_connections)
+ set_rules(self.multiworld, self.options, self.player, self.area_connections, self.star_costs, self.move_rando_bitvec)
if self.topology_present:
# Write area_connections to spoiler log
for entrance, destination in self.area_connections.items():
self.multiworld.spoiler.set_entrance(
- sm64_internalloc_to_string[entrance] + " Entrance",
- sm64_internalloc_to_string[destination],
+ sm64_level_to_entrances[entrance] + " Entrance",
+ sm64_level_to_entrances[destination],
'entrance', self.player)
def create_item(self, name: str) -> Item:
@@ -72,31 +99,29 @@ def create_item(self, name: str) -> Item:
return item
def create_items(self):
- starcount = self.multiworld.AmountOfStars[self.player].value
- if (not self.multiworld.EnableCoinStars[self.player].value):
- starcount = max(35,self.multiworld.AmountOfStars[self.player].value-15)
- starcount = max(starcount, self.multiworld.FirstBowserStarDoorCost[self.player].value,
- self.multiworld.BasementStarDoorCost[self.player].value, self.multiworld.SecondFloorStarDoorCost[self.player].value,
- self.multiworld.MIPS1Cost[self.player].value, self.multiworld.MIPS2Cost[self.player].value,
- self.multiworld.StarsToFinish[self.player].value)
- self.multiworld.itempool += [self.create_item("Power Star") for i in range(0,starcount)]
- self.multiworld.itempool += [self.create_item("1Up Mushroom") for i in range(starcount,120 - (15 if not self.multiworld.EnableCoinStars[self.player].value else 0))]
-
- if (not self.multiworld.ProgressiveKeys[self.player].value):
+ # 1Up Mushrooms
+ self.multiworld.itempool += [self.create_item("1Up Mushroom") for i in range(0,self.filler_count)]
+ # Power Stars
+ self.multiworld.itempool += [self.create_item("Power Star") for i in range(0,self.number_of_stars)]
+ # Keys
+ if (not self.options.progressive_keys):
key1 = self.create_item("Basement Key")
key2 = self.create_item("Second Floor Key")
self.multiworld.itempool += [key1, key2]
else:
self.multiworld.itempool += [self.create_item("Progressive Key") for i in range(0,2)]
-
- wingcap = self.create_item("Wing Cap")
- metalcap = self.create_item("Metal Cap")
- vanishcap = self.create_item("Vanish Cap")
- self.multiworld.itempool += [wingcap, metalcap, vanishcap]
-
- if (self.multiworld.BuddyChecks[self.player].value):
+ # Caps
+ self.multiworld.itempool += [self.create_item(cap_name) for cap_name in ["Wing Cap", "Metal Cap", "Vanish Cap"]]
+ # Cannons
+ if (self.options.buddy_checks):
self.multiworld.itempool += [self.create_item(name) for name, id in cannon_item_table.items()]
- else:
+ # Moves
+ self.multiworld.itempool += [self.create_item(action)
+ for action, itemid in action_item_table.items()
+ if self.move_rando_bitvec & (1 << itemid - action_item_table['Double Jump'])]
+
+ def generate_basic(self):
+ if not (self.options.buddy_checks):
self.multiworld.get_location("BoB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock BoB"))
self.multiworld.get_location("WF: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock WF"))
self.multiworld.get_location("JRB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock JRB"))
@@ -108,9 +133,7 @@ def create_items(self):
self.multiworld.get_location("THI: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock THI"))
self.multiworld.get_location("RR: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock RR"))
- if (self.multiworld.ExclamationBoxes[self.player].value > 0):
- self.multiworld.itempool += [self.create_item("1Up Mushroom") for i in range(0,29)]
- else:
+ if (self.options.exclamation_boxes == 0):
self.multiworld.get_location("CCM: 1Up Block Near Snowman", self.player).place_locked_item(self.create_item("1Up Mushroom"))
self.multiworld.get_location("CCM: 1Up Block Ice Pillar", self.player).place_locked_item(self.create_item("1Up Mushroom"))
self.multiworld.get_location("CCM: 1Up Block Secret Slide", self.player).place_locked_item(self.create_item("1Up Mushroom"))
@@ -147,13 +170,10 @@ def get_filler_item_name(self) -> str:
def fill_slot_data(self):
return {
"AreaRando": self.area_connections,
- "FirstBowserDoorCost": self.multiworld.FirstBowserStarDoorCost[self.player].value,
- "BasementDoorCost": self.multiworld.BasementStarDoorCost[self.player].value,
- "SecondFloorDoorCost": self.multiworld.SecondFloorStarDoorCost[self.player].value,
- "MIPS1Cost": self.multiworld.MIPS1Cost[self.player].value,
- "MIPS2Cost": self.multiworld.MIPS2Cost[self.player].value,
- "StarsToFinish": self.multiworld.StarsToFinish[self.player].value,
- "DeathLink": self.multiworld.death_link[self.player].value,
+ "MoveRandoVec": self.move_rando_bitvec,
+ "DeathLink": self.options.death_link.value,
+ "CompletionType": self.options.completion_type.value,
+ **self.star_costs
}
def generate_output(self, output_directory: str):
@@ -177,12 +197,21 @@ def generate_output(self, output_directory: str):
with open(os.path.join(output_directory, filename), 'w') as f:
json.dump(data, f)
- def modify_multidata(self, multidata):
+ def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]):
if self.topology_present:
er_hint_data = {}
for entrance, destination in self.area_connections.items():
- regionid = sm64_internalloc_to_regionid[destination]
- region = self.multiworld.get_region(sm64courses[regionid], self.player)
- for location in region.locations:
- er_hint_data[location.address] = sm64_internalloc_to_string[entrance]
- multidata['er_hint_data'][self.player] = er_hint_data
+ regions = [self.multiworld.get_region(sm64_level_to_entrances[destination], self.player)]
+ if regions[0].name == "Tiny-Huge Island (Huge)":
+ # Special rules for Tiny-Huge Island's dual entrances
+ reverse_area_connections = {destination: entrance for entrance, destination in self.area_connections.items()}
+ entrance_name = sm64_level_to_entrances[reverse_area_connections[SM64Levels.TINY_HUGE_ISLAND_HUGE]] \
+ + ' or ' + sm64_level_to_entrances[reverse_area_connections[SM64Levels.TINY_HUGE_ISLAND_TINY]]
+ regions[0] = self.multiworld.get_region("Tiny-Huge Island", self.player)
+ else:
+ entrance_name = sm64_level_to_entrances[entrance]
+ regions += regions[0].subregions
+ for region in regions:
+ for location in region.locations:
+ er_hint_data[location.address] = entrance_name
+ hint_data[self.player] = er_hint_data
diff --git a/worlds/sm64ex/docs/en_Super Mario 64.md b/worlds/sm64ex/docs/en_Super Mario 64.md
index def6e2a37536..4c85881a8525 100644
--- a/worlds/sm64ex/docs/en_Super Mario 64.md
+++ b/worlds/sm64ex/docs/en_Super Mario 64.md
@@ -1,28 +1,28 @@
# Super Mario 64 EX
-## Where is the settings page?
+## Where is the options page?
-The player settings page for this game contains all the options you need to configure and export a config file. Player
-settings page link: [SM64EX Player Settings Page](../player-settings).
+The player options page for this game contains all the options you need to configure and export a config file. Player
+options page link: [SM64EX Player Options Page](../player-options).
## What does randomization do to this game?
-All 120 Stars, the 3 Cap Switches, the Basement and Secound Floor Key are now Location Checks and may contain Items for different games as well
-as different Items from within SM64.
+All 120 Stars, the 3 Cap Switches, the Basement and Second Floor Key are now location checks and may contain items for different games as well
+as different items from within SM64.
## What is the goal of SM64EX when randomized?
-As in most Mario Games, save the Princess!
+As in most Mario games, save the Princess!
## Which items can be in another player's world?
Any of the 120 Stars, and the two Castle Keys. Additionally, Cap Switches are also considered "Items" and the "!"-Boxes will only be active
-when someone collects the corresponding Cap Switch Item.
+when someone collects the corresponding Cap Switch item.
## What does another world's item look like in SM64EX?
-The Items are visually unchanged, though after collecting a Message will pop up to inform you what you collected,
+The items are visually unchanged, though after collecting a message will pop up to inform you what you collected,
and who will receive it.
## When the player receives an item, what happens?
-When you receive an Item, a Message will pop up to inform you where you received the Item from,
+When you receive an item, a message will pop up to inform you where you received the item from,
and which one it is.
-NOTE: The Secret Star count in the Menu is broken.
+NOTE: The Secret Star count in the menu is broken.
diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md
index 38edeb2c4ab6..5983057f7d7a 100644
--- a/worlds/sm64ex/docs/setup_en.md
+++ b/worlds/sm64ex/docs/setup_en.md
@@ -2,71 +2,77 @@
## Required Software
-- Super Mario 64 US Rom (Japanese may work also. Europe and Shindou not supported)
+- Super Mario 64 US or JP Rom (Europe and Shindou not supported)
- Either of
- - [sm64pclauncher](https://github.com/N00byKing/sm64pclauncher/releases) or
+ - [SM64AP-Launcher](https://github.com/N00byKing/SM64AP-Launcher/releases) or
- Cloning and building [sm64ex](https://github.com/N00byKing/sm64ex) manually
- Optional, for sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
-NOTE: The above linked sm64pclauncher is a special version designed to work with the Archipelago build of sm64ex.
+NOTE: The above linked launcher is a special version designed to work with the Archipelago build of sm64ex.
You can use other sm64-port based builds with it, but you can't use a different launcher with the Archipelago build of sm64ex.
## Installation and Game Start Procedures
-### Installation via sm64pclauncher (For Windows)
+### Installation via SM64AP-Launcher
+
+*Windows Preparations*
First, install [MSYS](https://www.msys2.org/) as described on the page. DO NOT INSTALL INTO A FOLDER PATH WITH SPACES.
-Do all steps up to including step 6.
-Best use default install directory.
-Then follow the steps below
-
-1. Go to the page linked for sm64pclauncher, and press on the topmost entry
-3. Scroll down, and download the zip file
-4. Unpack the zip file in an empty folder
-5. Run the Launcher and press build.
-6. Set the location where you installed MSYS when prompted. Check the "Install Dependencies" Checkbox
-7. Set the Repo link to `https://github.com/N00byKing/sm64ex` and the Branch to `archipelago` (Top two boxes). You can choose the folder (Secound Box) at will, as long as it does not exist yet
-8. Point the Launcher to your Super Mario 64 US/JP Rom, and set the Region correspondingly
-9. Set Build Options and press build.
- - Recommended: To build faster, use `-jn` where `n` is the number of CPU cores to use (e.g., `-j4` to use 4 cores).
- - Optional: Add options from [this list](https://github.com/sm64pc/sm64ex/wiki/Build-options), separated by spaces (e.g., `-j4 BETTERCAMERA=1`).
-10. SM64EX will now be compiled. The Launcher will appear to have crashed, but this is not likely the case. Best wait a bit, but there may be a problem if it takes longer than 10 Minutes
-
-After it's done, the Build list should have another entry titled with what you named the folder in step 7.
-
-NOTE: For some reason first start of the game always crashes the launcher. Just restart it.
-If it still crashes, recheck if you typed the launch options correctly (Described in "Joining a MultiWorld Game")
+It is extremely encouraged to use the default install directory!
+Then continue to `Using the Launcher`
+
+*Linux Preparations*
+
+You will need to install some dependencies before using the launcher.
+The launcher itself needs `qt6`, `patch` and `git`, and building the game requires `sdl2 glew cmake python make` (If you install `jsoncpp` as well, it will be linked dynamically).
+Then continue to `Using the Launcher`
+
+*Using the Launcher*
+
+1. Go to the page linked for SM64AP-Launcher, and press on the topmost entry
+2. Scroll down, and download the zip file for your OS.
+3. Unpack the zip file in an empty folder
+4. Run the Launcher. On first start, press `Check Requirements`, which will guide you through the rest of the needed steps.
+ - Windows: If you did not use the default install directory for MSYS, close this window, check `Show advanced options` and reopen using `Re-check Requirements`. You can then set the path manually.
+5. When finished, use `Compile default SM64AP build` to continue
+ - Advanced user can use `Show advanced options` to build with custom makeflags (`BETTERCAMERA`, `NODRAWINGDISTANCE`, ...), different repos and branches, and game patches such as 60FPS, Enhanced Moveset and others.
+6. Press `Download Files` to prepare the build, afterwards `Create Build`.
+7. SM64EX will now be compiled. This can take a while.
+
+After it's done, the build list should have another entry with the name you gave it.
+
+NOTE: If it does not start when pressing `Play selected build`, recheck if you typed the launch options correctly (Described in "Joining a MultiWorld Game")
### Manual Compilation (Linux/Windows)
-*Windows Instructions*
+*Windows Preparations*
First, install [MSYS](https://www.msys2.org/) as described on the page. DO NOT INSTALL INTO A FOLDER PATH WITH SPACES.
-After launching msys2, and update by entering `pacman -Syuu` in the command prompt. Next, install the relevant dependencies by entering `pacman -S unzip mingw-w64-x86_64-gcc mingw-w64-x86_64-glew mingw-w64-x86_64-SDL2 git make python3 mingw-w64-x86_64-cmake`. SM64EX will link `jsoncpp` dynamic if installed. If not, it will compile and link statically.
+After launching msys2 using a MinGW x64 shell (there should be a start menu entry), update by entering `pacman -Syuu` in the command prompt. Next, install the relevant dependencies by entering `pacman -S unzip mingw-w64-x86_64-gcc mingw-w64-x86_64-glew mingw-w64-x86_64-SDL2 git make python3 mingw-w64-x86_64-cmake`.
-After this, obtain the code base by cloning the relevant repository manually via `git clone --recursive https://github.com/N00byKing/sm64ex`. Ready your ROM by copying your legally dumped rom into your sm64ex folder (if you are not sure where your folder is located, do a quick Windows search for sm64ex). The name of the ROM needs to be `baserom.REGION.z64` where `REGION` is either `us` or `jp` respectively.
+Continue to `Compiling`.
-After all these preparatory steps have succeeded, type `make` in your command prompt and get ready to wait for a bit. If you want to speed up compilation, tell the compiler how many CPU cores to use by using `make -jn` where n is the number of cores you want.
+*Linux Preparations*
-After the compliation was successful, there will be a binary in your `sm64ex/build/REGION_pc/` folder.
+Install the relevant dependencies `sdl2 glew cmake python make patch git`. SM64EX will link `jsoncpp` dynamic if installed. If not, it will compile and link statically.
-*Linux Instructions*
+Continue to `Compiling`.
-Install the relevant dependencies `sdl2 glew cmake python make`. SM64EX will link `jsoncpp` dynamic if installed. If not, it will compile and link statically.
+*Compiling*
-After this, obtain the code base by cloning the relevant repository manually via `git clone --recursive https://github.com/N00byKing/sm64ex`. Ready your ROM by copying your legally dumped rom into your sm64ex folder. The name of the ROM needs to be `baserom.REGION.z64` where `REGION` is either `us` or `jp` respectively.
+Obtain the code base by cloning the relevant repository via `git clone --recursive https://github.com/N00byKing/sm64ex`. Copy your legally dumped rom into your sm64ex folder (if you are not sure where your folder is located, do a quick Windows search for sm64ex). The name of the ROM needs to be `baserom.REGION.z64` where `REGION` is either `us` or `jp` respectively.
-After all these preparatory steps have succeeded, type `make` in your command prompt and get ready to wait for a bit. If you want to speed up compilation, tell the compiler how many CPU cores to use by using `make -jn` where n is the number of cores you want.
+After all these preparatory steps have succeeded, type `cd sm64ex && make` in your command prompt and get ready to wait for a bit. If you want to speed up compilation, tell the compiler how many CPU cores to use by using `make -jn` instead, where n is the number of cores you want.
After the compliation was successful, there will be a binary in your `sm64ex/build/REGION_pc/` folder.
### Joining a MultiWorld Game
To join, set the following launch options: `--sm64ap_name YourName --sm64ap_ip ServerIP:Port`.
+For example, if you are hosting a game using the website, `YourName` will be the name from the Options Page, `ServerIP` is `archipelago.gg` and `Port` the port given on the Archipelago room page.
Optionally, add `--sm64ap_passwd "YourPassword"` if the room you are using requires a password.
-The Name in this case is the one specified in your generated .yaml file.
-In case you are using the Archipelago Website, the IP should be `archipelago.gg`.
+Should your name or password have spaces, enclose it in quotes: `"YourPassword"` and `"YourName"`.
Should the connection fail (for example when using the wrong name or IP/Port combination) the game will inform you of that.
Additionally, any time the game is not connected (for example when the connection is unstable) it will attempt to reconnect and display a status text.
@@ -76,12 +82,12 @@ Failing to use a new file may make some locations unavailable. However, this can
### Playing offline
-To play offline, first generate a seed on the game's settings page.
+To play offline, first generate a seed on the game's options page.
Create a room and download the `.apsm64ex` file, and start the game with the `--sm64ap_file "path/to/FileName"` launch argument.
### Optional: Using Batch Files to play offline and MultiWorld games
-As an alternative to launching the game with sm64pclauncher, it is also possible to launch the completed build with the use of Windows batch files. This has the added benefit of streamlining the join process so that manual editing of connection info is not needed for each new game. However, you'll need to be somewhat comfortable with creating and using batch files.
+As an alternative to launching the game with SM64AP-Launcher, it is also possible to launch the completed build with the use of Windows batch files. This has the added benefit of streamlining the join process so that manual editing of connection info is not needed for each new game. However, you'll need to be somewhat comfortable with creating and using batch files.
IMPORTANT NOTE: The remainder of this section uses copy-and-paste code that assumes you're using the US version. If you instead use the Japanese version, you'll need to edit the EXE name accordingly by changing "sm64.us.f3dex2e.exe" to "sm64.jp.f3dex2e.exe".
@@ -91,7 +97,7 @@ Open Notepad. Paste in the following text: `start sm64.us.f3dex2e.exe --sm64ap_f
Go to File > Save As...
-Navigate to the folder you selected for your SM64 build when you followed the Build guide for SM64PCLauncher earlier. Once there, navigate further into `build` and then `us_pc`. This folder should be the same folder that `sm64.us.f3dex2e.exe` resides in.
+Navigate to the folder you selected for your SM64 build when you followed the Build guide for SM64AP-Launcher earlier. Once there, navigate further into `build` and then `us_pc`. This folder should be the same folder that `sm64.us.f3dex2e.exe` resides in.
Make the file name `"offline.bat"` . THE QUOTE MARKS ARE IMPORTANT! Otherwise, it will create a text file instead ("offline.bat.txt"), which won't work as a batch file.
@@ -120,8 +126,8 @@ To use this batch file, double-click it. A window will open. Type the five-digi
- The port number is provided on the room page. The game host should share this page with all players.
- The slot name is whatever you typed in the "Name" field when creating a config file. All slot names are visible on the room page.
-Once you provide those two bits of information, the game will open. If the info is correct, when the game starts, you will see "Connected to Archipelago" on the bottom of your screen, and you will be able to enter the castle.
-- If you don't see this text and crash upon entering the castle, try again. Double-check the port number and slot name; even a single typo will cause your connection to fail.
+Once you provide those two bits of information, the game will open.
+- If the game only says `Connecting`, try again. Double-check the port number and slot name; even a single typo will cause your connection to fail.
### Addendum - Deleting old saves
@@ -170,6 +176,5 @@ Should the problem still be there after about a minute or two, just save and res
### How do I update the Game to a new Build?
+When using the Launcher follow the normal build steps, but when choosing a folder name use the same as before. The launcher will recognize this, and offer to replace it.
When manually compiling just pull in changes and run `make` again. Sometimes it helps to run `make clean` before.
-
-When using the Launcher follow the normal build steps, but when choosing a folder name use the same as before. Then continue as normal.
diff --git a/worlds/smw/Aesthetics.py b/worlds/smw/Aesthetics.py
index 73ca61650886..16b2b138f3e2 100644
--- a/worlds/smw/Aesthetics.py
+++ b/worlds/smw/Aesthetics.py
@@ -1,3 +1,82 @@
+import json
+import pkgutil
+
+from worlds.AutoWorld import World
+
+tileset_names = [
+ "grass_hills",
+ "grass_forest",
+ "grass_rocks",
+ "grass_clouds",
+ "grass_mountains",
+ "cave",
+ "cave_rocks",
+ "water",
+ "mushroom_rocks",
+ "mushroom_clouds",
+ "mushroom_forest",
+ "mushroom_hills",
+ "mushroom_stars",
+ "mushroom_cave",
+ "forest",
+ "logs",
+ "clouds",
+ "castle_pillars",
+ "castle_windows",
+ "castle_wall",
+ "castle_small_windows",
+ "ghost_house",
+ "ghost_house_exit",
+ "ship_exterior",
+ "ship_interior",
+ "switch_palace",
+ "yoshi_house"
+]
+
+map_names = [
+ "main",
+ "yoshi",
+ "vanilla",
+ "forest",
+ "valley",
+ "special",
+ "star"
+]
+
+level_palette_index = [
+ 0xFF,0x03,0x09,0x01,0x15,0x0A,0x04,0x12,0x19,0x06,0x07,0x12,0x09,0x0F,0x13,0x09, # Levels 000-00F
+ 0x03,0x07,0xFF,0x15,0x19,0x04,0x04,0xFF,0x17,0xFF,0x14,0x12,0x02,0x05,0xFF,0x11, # Levels 010-01F
+ 0x12,0x15,0x04,0x02,0x02,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 020-02F
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 030-03F
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 040-04F
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 050-05F
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 060-06F
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 070-07F
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 080-08F
+ 0xFF,0xFF,0xFF,0x12,0x12,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 090-09F
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 0A0-0AF
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x19,0x08,0x09, # Levels 0B0-0BF
+ 0x02,0x08,0x05,0x04,0x16,0x1A,0x04,0x02,0x0C,0x19,0x19,0x09,0xFF,0x02,0x02,0x02, # Levels 0C0-0CF
+ 0x04,0x04,0x05,0x12,0x14,0xFF,0x12,0x10,0x05,0xFF,0x19,0x12,0x14,0x0F,0x15,0xFF, # Levels 0D0-0DF
+ 0x12,0x12,0xFF,0x04,0x15,0xFF,0x19,0x14,0x12,0x05,0x05,0x16,0x15,0x15,0x15,0x12, # Levels 0E0-0EF
+ 0x16,0x15,0x15,0x09,0x19,0x04,0x04,0x13,0x18,0x15,0x15,0x16,0x15,0x19,0x15,0x04, # Levels 0F0-0FF
+ 0xFF,0x11,0x08,0x02,0x1A,0x00,0x01,0x15,0xFF,0x05,0x05,0x05,0xFF,0x11,0x12,0x05, # Levels 100-10F
+ 0x12,0x14,0xFF,0x0D,0x15,0x06,0x05,0x05,0x05,0x0C,0x05,0x19,0x12,0x15,0x0E,0x01, # Levels 110-11F
+ 0x07,0x19,0x0E,0x0E,0xFF,0x04,0x0E,0x02,0x02,0xFF,0x09,0x04,0x0B,0x02,0xFF,0xFF, # Levels 120-12F
+ 0x07,0xFF,0x0C,0xFF,0x05,0x0C,0x0C,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 130-13F
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 140-14F
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 150-15F
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 160-16F
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 170-17F
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 180-18F
+ 0xFF,0xFF,0xFF,0x12,0x12,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 190-19F
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, # Levels 1A0-1AF
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x19,0x19,0x12,0x02,0x05, # Levels 1B0-1BF
+ 0x02,0x07,0x05,0x05,0x03,0x03,0x00,0xFF,0x0F,0x10,0x05,0x05,0x12,0x11,0x14,0x14, # Levels 1C0-1CF
+ 0x11,0x12,0x12,0x12,0x11,0x03,0x03,0x19,0x19,0x15,0x16,0x15,0x15,0x15,0xFF,0x05, # Levels 1D0-1DF
+ 0x10,0x02,0x06,0x06,0x19,0x05,0x16,0x16,0x15,0x15,0x15,0xFF,0x06,0x05,0x05,0x06, # Levels 1E0-1EF
+ 0x05,0x05,0x12,0x14,0x12,0x05,0xFF,0x19,0x05,0x16,0x15,0x15,0x11,0x05,0x12,0x09 # Levels 1F0-1FF
+]
mario_palettes = [
[0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x1F, 0x39, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0xB6, 0x30, 0xDF, 0x35, 0xFF, 0x03], # Mario
@@ -145,36 +224,413 @@
0x2D24: [0x00, 0x02, 0x03], # Star Road
}
-def generate_shuffled_level_music(world, player):
+valid_sfxs = [
+ [0x01, 1], # Jump
+ [0x01, 0], # Hit head
+ [0x02, 0], # Contact/Spinjump on an enemy
+ [0x03, 0], # Kick item
+ [0x04, 0], # Go in pipe, get hurt
+ [0x05, 0], # Midway point
+ [0x06, 0], # Yoshi gulp
+ [0x07, 0], # Dry bones collapse
+ [0x08, 0], # Kill enemy with a spin jump
+ [0x09, 0], # Fly with cape
+ [0x0A, 0], # Get powerup
+ [0x0B, 0], # ON/OFF switch
+ [0x0C, 0], # Carry item past the goal
+ [0x0D, 0], # Get cape
+ [0x0E, 0], # Swim
+ [0x0F, 0], # Hurt while flying
+ [0x10, 0], # Magikoopa shoot magic
+ [0x13, 0], # Enemy stomp #1
+ [0x14, 0], # Enemy stomp #2
+ [0x15, 0], # Enemy stomp #3
+ [0x16, 0], # Enemy stomp #4
+ [0x17, 0], # Enemy stomp #5
+ [0x18, 0], # Enemy stomp #6
+ [0x19, 0], # Enemy stomp #7
+ [0x1C, 0], # Yoshi Coin
+ [0x1E, 0], # P-Balloon
+ [0x1F, 0], # Koopaling defeated
+ [0x20, 0], # Yoshi spit
+ [0x23, 0], # Lemmy/Wendy falling
+ [0x25, 0], # Blargg roar
+ [0x26, 0], # Firework whistle
+ [0x27, 0], # Firework bang
+ [0x2A, 0], # Peach pops up from the Clown Car
+ [0x04, 1], # Grinder
+ [0x01, 3], # Coin
+ [0x02, 3], # Hit a ? block
+ [0x03, 3], # Hit a block with a vine inside
+ [0x04, 3], # Spin jump
+ [0x05, 3], # 1up
+ [0x06, 3], # Shatter block
+ [0x07, 3], # Shoot fireball
+ [0x08, 3], # Springboard
+ [0x09, 3], # Bullet bill
+ [0x0A, 3], # Egg hatch
+ [0x0B, 3], # Item going into item box
+ [0x0C, 3], # Item falls from item box
+ [0x0E, 3], # L/R scroll
+ [0x0F, 3], # Door
+ [0x13, 3], # Lose Yoshi
+ [0x14, 3], # SMW2: New level available
+ [0x15, 3], # OW tile reveal
+ [0x16, 3], # OW castle collapse
+ [0x17, 3], # Fire spit
+ [0x18, 3], # Thunder
+ [0x19, 3], # Clap
+ [0x1A, 3], # Castle bomb
+ [0x1C, 3], # OW switch palace block ejection
+ [0x1E, 3], # Whistle
+ [0x1F, 3], # Yoshi mount
+ [0x20, 3], # Lemmy/Wendy going in lava
+ [0x21, 3], # Yoshi's tongue
+ [0x22, 3], # Message box hit
+ [0x23, 3], # Landing in a level tile
+ [0x24, 3], # P-Switch running out
+ [0x25, 3], # Yoshi defeats an enemy
+ [0x26, 3], # Swooper
+ [0x27, 3], # Podoboo
+ [0x28, 3], # Enemy hurt
+ [0x29, 3], # Correct
+ [0x2A, 3], # Wrong
+ [0x2B, 3], # Firework whistle
+ [0x2C, 3] # Firework bang
+]
+
+game_sfx_calls = [
+ 0x0565E, # Jump
+ 0x1BABD, # Spin jump
+ 0x06D41, # Hit head on ceiling
+ 0x0B4F2, # Hit head on sprite
+ 0x07EB5, # Shooting a fireball
+ 0x0507B, # Cape spin
+ 0x058A8, # Cape smash
+ 0x075F3, # Taking damage
+ 0x075E2, # Taking damage while flying
+ 0x07919, # Something during a boss fight
+ 0x05AA9, # Swim
+ 0x1BC04, # Spin jump off water
+ 0x05BA5, # Jump off a net
+ 0x05BB2, # Punching a net
+ 0x06C10, # Entering a door
+ 0x05254, # Entering a pipe #1
+ 0x07439, # Entering a pipe #2
+ 0x052A5, # Shot from a diagonal pipe
+ 0x072E8, # Hit a midway point
+ 0x07236, # Hit a wrong block
+ 0x07B7D, # Spawn a powerup during the goal tape
+ 0x1C342, # Invisible mushroom spawn
+ 0x04E3F, # Scrolling the screen with L/R
+ 0x0AAFD, # Pressing a P-Switch
+ 0x04557, # P-Switch running out
+ 0x0BAD7, # Climbing door turning
+ 0x0C109, # Break goal tape
+ 0x0C548, # Putting item in item box
+ 0x10012, # Trigger item box
+ 0x2B34D, # Collecting a coin
+ 0x07358, # Collecting a Yoshi Coin
+ 0x0C57A, # Collecting a powerup (generic)
+ 0x0C59C, # Collecting a feather
+ 0x0C309, # Collecting a P-Balloon
+ 0x0E6A9, # Bouncing off a springboard
+ 0x1117D, # Bouncing off a note block
+ 0x14DEC, # Bouncing off a wall springboard
+ 0x1067F, # Block shattering
+ 0x1081E, # Activate ON/OFF switch #1
+ 0x1118C, # Activate ON/OFF switch #2
+ 0x12045, # Fireballs hitting a block/sprite
+ 0x12124, # Fireballs converting an enemy into a coin
+ 0x12106, # Fireballs defeating a Chuck
+ 0x18D7D, # Activating a message box
+ 0x1C209, # Activating a red question block
+ 0x0A290, # Baby Yoshi swallowing an item #1
+ 0x1C037, # Baby Yoshi swallowing an item #2
+ 0x0F756, # Yoshi egg hatching
+ 0x0A2C5, # Yoshi growing #1
+ 0x1C06C, # Yoshi growing #2
+ 0x0ED5F, # Mounting Yoshi
+ 0x0F71D, # Yoshi hurt
+ 0x12481, # Yoshi hurt (projectiles)
+ 0x0EF0E, # Yoshi flying
+ 0x06F90, # Yoshi stomping an enemy
+ 0x06FB6, # Yoshi ground pound (yellow shell)
+ 0x07024, # Yoshi bounces off a triangle
+ 0x11BE9, # Yoshi stomping the ground
+ 0x0F0D3, # Yoshi swallowing a sprite
+ 0x0F0FD, # Yoshi eating a green berry
+ 0x1BA7D, # Yoshi sticking out tongue
+ 0x0F5A1, # Yoshi unable to eat
+ 0x0F2DF, # Yoshi spitting out an item
+ 0x0F28F, # Yoshi spitting out flames
+ 0x0F3EC, # Collecting Yoshi's wings (eaten)
+ 0x0F6C8, # Collecting Yoshi's wings (touched)
+ 0x7FE04, # Defeated sprite combo #1 (using Y index)
+ 0x7FE0E, # Defeated sprite combo #2 (using Y index)
+ 0x7FE18, # Defeated sprite combo #3 (using Y index)
+ 0x7FE22, # Defeated sprite combo #4 (using Y index)
+ 0x7FE2C, # Defeated sprite combo #5 (using Y index)
+ 0x7FE36, # Defeated sprite combo #6 (using Y index)
+ 0x7FE40, # Defeated sprite combo #7 (using Y index)
+ 0x7FE4B, # Defeated sprite combo #1 (using X index)
+ 0x7FE55, # Defeated sprite combo #2 (using X index)
+ 0x7FE5F, # Defeated sprite combo #3 (using X index)
+ 0x7FE69, # Defeated sprite combo #4 (using X index)
+ 0x7FE73, # Defeated sprite combo #5 (using X index)
+ 0x7FE7D, # Defeated sprite combo #6 (using X index)
+ 0x7FE87, # Defeated sprite combo #7 (using X index)
+ 0x0A728, # Kicking a carryable item
+ 0x0B12F, # Kicking a stunned and vulnerable enemy
+ 0x0A8D8, # Performing a spinjump on a immune enemy
+ 0x0A93F, # Defeating an enemy via spinjump
+ 0x0999E, # Thrown sprite hitting the ground from the side
+ 0x192B8, # Creating/Eating block moving
+ 0x195EC, # Rex stomped
+ 0x134A7, # Bullet bill blaster shooting
+ 0x13088, # Bullet bill generator #1
+ 0x130DF, # Bullet bill generator #2
+ 0x09631, # Bob-omb explosion
+ 0x15918, # Popping a bubble
+ 0x15D64, # Sumo bro stomping the ground
+ 0x15ECC, # Sumo bro lightning spawning flames
+ 0x1726B, # Bouncing off wiggler
+ 0x08390, # Banzai bill spawn
+ 0x0AF17, # Thwomp hitting the ground
+ 0x0AFFC, # Thwimp hitting the ground
+ 0x14707, # Chuck running
+ 0x14381, # Chuck whistling
+ 0x144F8, # Chuck clapping
+ 0x14536, # Chuck jumping
+ 0x145AE, # Chuck splitting
+ 0x147D2, # Chuck bounce
+ 0x147F6, # Chuck hurt
+ 0x147B8, # Chuck defeated
+ 0x19D55, # Dino torch shooting fire
+ 0x19FFA, # Blargg attacking
+ 0x188FF, # Swooper bat swooping
+ 0x08584, # Bowser statue flame spawn
+ 0x18ADA, # Bowser statue shooting a flame
+ 0x13043, # Bowser statue flame from generator
+ 0x0BF28, # Magikoopa shooting a magic spell
+ 0x0BC5F, # Magikoopa's magic spell hitting the ground
+ 0x0D745, # Line guided sprites' motor
+ 0x0DB70, # Grinder sound
+ 0x0E0A1, # Podoboo jumping
+ 0x0E5F2, # Dry bones/Bony beetle collapsing
+ 0x15474, # Giant wooden pillar hitting the ground
+ 0x2C9C1, # Spiked columns hitting the ground
+ 0x19B03, # Reznor shooting a fireball
+ 0x19A66, # Reznor: Hitting a platform
+ 0x1D752, # Reznor: Bridge collapsing
+ 0x19ABB, # Reznor: Defeated
+ 0x180E9, # Big Boo: Reappearing
+ 0x18233, # Big Boo: Hurt
+ 0x181DE, # Big Boo: Defeated
+ 0x1CEC1, # Wendy/Lemmy: Hitting a dummy
+ 0x1CECB, # Wendy/Lemmy: Hurt
+ 0x1CE33, # Wendy/Lemmy: Hurt (correct)
+ 0x1CE46, # Wendy/Lemmy: Hurt (incorrect)
+ 0x1CE24, # Wendy/Lemmy: Defeated
+ 0x1CE7E, # Wendy/Lemmy: Falling into lava
+ 0x0CF0A, # Ludwig: Jumping
+ 0x0D059, # Ludwig: Shooting a fireball
+ 0x10414, # Morton/Roy: Pillar drop
+ 0x0D299, # Morton/Roy: Ground smash
+ 0x0D3AB, # Morton/Roy/Ludwig: Hit by a fireball
+ 0x0D2FD, # Morton/Roy/Ludwig: Bouncing off
+ 0x0D31E, # Morton/Roy/Ludwig: Bouncing off (immune)
+ 0x0D334, # Morton/Roy/Ludwig: Bouncing off (immune, going up a wall)
+ 0x0CFD0, # Morton/Roy/Ludwig: Defeated
+ 0x0FCCE, # Iggy/Larry: Being hit
+ 0x0FD40, # Iggy/Larry: Being hit by a fireball
+ 0x0FB60, # Iggy/Larry: Falling in lava
+ 0x1A8B2, # Peach emerging from Clown Car
+ 0x1A8E3, # Peach throwing an item
+ 0x1B0B8, # Bumping into Clown Car
+ 0x1B129, # Bowser: Hurt
+ 0x1AB8C, # Bowser: Slamming the ground (third phase)
+ 0x1A5D0, # Bowser: Throwing a Mechakoopa
+ 0x1A603, # Bowser: Dropping a ball
+ 0x1A7F6, # Bowser: Spawning a flame
+ 0x1B1A3, # Bowser's ball slam #1
+ 0x1B1B1, # Bowser's ball slam #2
+ 0x1E016, # Bowser's arena lightning effect
+ 0x26CAA, # Map: Level tile reveal
+ 0x26763, # Map: Terrain reveal
+ 0x21170, # Map: Using a star
+ 0x2666F, # Map: Castle destruction
+ 0x272A4, # Map: Switch palace blocks spawning
+ 0x203CC, # Map: Earthquake
+ 0x27A78, # Map: Fish jumping
+ 0x27736, # Map: Valley of bowser thunder
+ 0x013C0, # Menu: Nintendo presents
+ 0x01AE3, # Menu: File menu option select
+ 0x01AF9, # Menu: File menu option change
+ 0x01BBB, # Menu: Saving game
+ 0x273FF, # Menu: Map misc menu appearing
+ 0x27567, # Menu: Something during the map
+ 0x1767A, # Cutscene: Castle door opening
+ 0x17683, # Cutscene: Castle door closing
+ 0x17765, # Cutscene: Ghost house door opening
+ 0x1776E, # Cutscene: Ghost house door closing
+ 0x04720, # Cutscene: Detonator fuse
+ 0x04732, # Cutscene: Bouncing off something
+ 0x0475F, # Cutscene: Tossing the castle
+ 0x04798, # Cutscene: Picking up the castle
+ 0x047AC, # Cutscene: Huff
+ 0x047D1, # Cutscene: Hitting a castle
+ 0x1C830, # Cutscene: Shooting a firework
+ 0x625AF, # Cutscene: Egg shattering
+ 0x64F2C, # Cutscene: Hitting a hill
+ 0x6512A, # Cutscene: Castle crashing
+ 0x65295, # Cutscene: Explosion
+ 0x652B2, # Cutscene: Castle sinking
+ 0x652BD, # Cutscene: Castle flying
+ 0x652D8, # Cutscene: Fake explosion
+ 0x653E7, # Cutscene: Castle being hit by a hammer
+ 0x657D8 # Cutscene: Castle being mopped away
+]
+
+def generate_shuffled_sfx(rom, world: World):
+ # Adjust "hitting sprites in succession" codes
+ rom.write_bytes(0x0A60B, bytearray([0x22, 0x00, 0xFE, 0x0F, 0xEA, 0xEA])) # jsl $0FFE00 : nop #2 # Thrown sprites combo #1
+ rom.write_bytes(0x0A659, bytearray([0x22, 0x47, 0xFE, 0x0F, 0xEA, 0xEA])) # jsl $0FFE47 : nop #2 # Thrown sprites combo #2
+ rom.write_bytes(0x0A865, bytearray([0x22, 0x47, 0xFE, 0x0F, 0xEA, 0xEA])) # jsl $0FFE47 : nop #2 # Star combo
+ rom.write_bytes(0x0AB57, bytearray([0x22, 0x00, 0xFE, 0x0F, 0xEA, 0xEA])) # jsl $0FFE00 : nop #2 # Bouncing off enemies
+ rom.write_bytes(0x172C0, bytearray([0x22, 0x00, 0xFE, 0x0F, 0xEA, 0xEA])) # jsl $0FFE00 : nop #2 # Star combo (wigglers)
+ rom.write_bytes(0x1961D, bytearray([0x22, 0x00, 0xFE, 0x0F, 0xEA, 0xEA])) # jsl $0FFE00 : nop #2 # Star combo (rexes)
+ rom.write_bytes(0x19639, bytearray([0x22, 0x00, 0xFE, 0x0F, 0xEA, 0xEA])) # jsl $0FFE00 : nop #2 # Bouncing off rexes
+
+ COMBO_SFX_ADDR = 0x7FE00
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0000, bytearray([0xC0, 0x01])) # COMBO_Y: CPY #$01
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0002, bytearray([0xD0, 0x06])) # BNE label_0FFE0A
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0004, bytearray([0xA9, 0x13])) # LDA #$13
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0006, bytearray([0x8D, 0xF9, 0x1D])) # STA $1DF9
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0009, bytearray([0x6B])) # RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x000A, bytearray([0xC0, 0x02])) # label_0FFE0A: CPY #$02
+ rom.write_bytes(COMBO_SFX_ADDR + 0x000C, bytearray([0xD0, 0x06])) # BNE label_0FFE14
+ rom.write_bytes(COMBO_SFX_ADDR + 0x000E, bytearray([0xA9, 0x14])) # LDA #$14
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0010, bytearray([0x8D, 0xF9, 0x1D])) # STA $1DF9
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0013, bytearray([0x6B])) # RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0014, bytearray([0xC0, 0x03])) # label_0FFE14: CPY #$03
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0016, bytearray([0xD0, 0x06])) # BNE label_0FFE1E
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0018, bytearray([0xA9, 0x15])) # LDA #$15
+ rom.write_bytes(COMBO_SFX_ADDR + 0x001A, bytearray([0x8D, 0xF9, 0x1D])) # STA $1DF9
+ rom.write_bytes(COMBO_SFX_ADDR + 0x001D, bytearray([0x6B])) # RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x001E, bytearray([0xC0, 0x04])) # label_0FFE1E: CPY #$04
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0020, bytearray([0xD0, 0x06])) # BNE label_0FFE28
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0022, bytearray([0xA9, 0x16])) # LDA #$16
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0024, bytearray([0x8D, 0xF9, 0x1D])) # STA $1DF9
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0027, bytearray([0x6B])) # RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0028, bytearray([0xC0, 0x05])) # label_0FFE28: CPY #$05
+ rom.write_bytes(COMBO_SFX_ADDR + 0x002A, bytearray([0xD0, 0x06])) # BNE label_0FFE32
+ rom.write_bytes(COMBO_SFX_ADDR + 0x002C, bytearray([0xA9, 0x17])) # LDA #$17
+ rom.write_bytes(COMBO_SFX_ADDR + 0x002E, bytearray([0x8D, 0xF9, 0x1D])) # STA $1DF9
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0031, bytearray([0x6B])) # RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0032, bytearray([0xC0, 0x06])) # label_0FFE32: CPY #$06
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0034, bytearray([0xD0, 0x06])) # BNE label_0FFE3C
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0036, bytearray([0xA9, 0x18])) # LDA #$18
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0038, bytearray([0x8D, 0xF9, 0x1D])) # STA $1DF9
+ rom.write_bytes(COMBO_SFX_ADDR + 0x003B, bytearray([0x6B])) # RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x003C, bytearray([0xC0, 0x07])) # label_0FFE3C: CPY #$07
+ rom.write_bytes(COMBO_SFX_ADDR + 0x003E, bytearray([0xD0, 0x06])) # BNE label_0FFE46
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0040, bytearray([0xA9, 0x19])) # LDA #$19
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0042, bytearray([0x8D, 0xF9, 0x1D])) # STA $1DF9
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0045, bytearray([0x6B])) # RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0046, bytearray([0x6B])) # label_0FFE46: RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0047, bytearray([0xE0, 0x01])) # COMBO_X: CPX #$01
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0049, bytearray([0xD0, 0x06])) # BNE label_0FFE51
+ rom.write_bytes(COMBO_SFX_ADDR + 0x004B, bytearray([0xA9, 0x13])) # LDA #$13
+ rom.write_bytes(COMBO_SFX_ADDR + 0x004D, bytearray([0x8D, 0xF9, 0x1D])) # STA $1DF9
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0050, bytearray([0x6B])) # RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0051, bytearray([0xE0, 0x02])) # label_0FFE51: CPX #$02
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0053, bytearray([0xD0, 0x06])) # BNE label_0FFE5B
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0055, bytearray([0xA9, 0x14])) # LDA #$14
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0057, bytearray([0x8D, 0xF9, 0x1D])) # STA $1DF9
+ rom.write_bytes(COMBO_SFX_ADDR + 0x005A, bytearray([0x6B])) # RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x005B, bytearray([0xE0, 0x03])) # label_0FFE5B: CPX #$03
+ rom.write_bytes(COMBO_SFX_ADDR + 0x005D, bytearray([0xD0, 0x06])) # BNE label_0FFE65
+ rom.write_bytes(COMBO_SFX_ADDR + 0x005F, bytearray([0xA9, 0x15])) # LDA #$15
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0061, bytearray([0x8D, 0xF9, 0x1D])) # STA $1DF9
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0064, bytearray([0x6B])) # RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0065, bytearray([0xE0, 0x04])) # label_0FFE65: CPX #$04
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0067, bytearray([0xD0, 0x06])) # BNE label_0FFE6F
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0069, bytearray([0xA9, 0x16])) # LDA #$16
+ rom.write_bytes(COMBO_SFX_ADDR + 0x006B, bytearray([0x8D, 0xF9, 0x1D])) # STA $1DF9
+ rom.write_bytes(COMBO_SFX_ADDR + 0x006E, bytearray([0x6B])) # RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x006F, bytearray([0xE0, 0x05])) # label_0FFE6F: CPX #$05
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0071, bytearray([0xD0, 0x06])) # BNE label_0FFE79
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0073, bytearray([0xA9, 0x17])) # LDA #$17
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0075, bytearray([0x8D, 0xF9, 0x1D])) # STA $1DF9
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0078, bytearray([0x6B])) # RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0079, bytearray([0xE0, 0x06])) # label_0FFE79: CPX #$06
+ rom.write_bytes(COMBO_SFX_ADDR + 0x007B, bytearray([0xD0, 0x06])) # BNE label_0FFE83
+ rom.write_bytes(COMBO_SFX_ADDR + 0x007D, bytearray([0xA9, 0x18])) # LDA #$18
+ rom.write_bytes(COMBO_SFX_ADDR + 0x007F, bytearray([0x8D, 0xF9, 0x1D])) # STA $1DF9
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0082, bytearray([0x6B])) # RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0083, bytearray([0xE0, 0x07])) # label_0FFE83: CPX #$07
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0085, bytearray([0xD0, 0x06])) # BNE label_0FFE8D
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0087, bytearray([0xA9, 0x19])) # LDA #$19
+ rom.write_bytes(COMBO_SFX_ADDR + 0x0089, bytearray([0x8D, 0xF9, 0x1D])) # STA $1DF9
+ rom.write_bytes(COMBO_SFX_ADDR + 0x008C, bytearray([0x6B])) # RTL
+ rom.write_bytes(COMBO_SFX_ADDR + 0x008D, bytearray([0x6B])) # label_0FFE8D: RTL
+
+ # Adjust "Hit head on ceiling" code
+ rom.write_bytes(0x06D41 + 0x00, bytearray([0xA9, 0x01])) # lda #$01
+ rom.write_bytes(0x06D41 + 0x02, bytearray([0x8D, 0xF9, 0x1D])) # sta $1DF9
+ rom.write_bytes(0x06D41 + 0x05, bytearray([0xEA, 0xEA, 0xEA, 0xEA])) # nop #4
+
+ # Manually add "Map: Stepping onto a level tile" random SFX
+ selected_sfx = world.random.choice(valid_sfxs)
+ rom.write_byte(0x2169F + 0x01, selected_sfx[0])
+ rom.write_byte(0x2169F + 0x04, selected_sfx[1] + 0xF9)
+
+ # Disable panning on Bowser's flames
+ rom.write_bytes(0x1A83D, bytearray([0xEA, 0xEA, 0xEA])) # nop #3
+
+ # Randomize SFX calls
+ for address in game_sfx_calls:
+ # Get random SFX
+ if world.options.sfx_shuffle != "singularity":
+ selected_sfx = world.random.choice(valid_sfxs)
+ # Write randomized SFX num
+ rom.write_byte(address + 0x01, selected_sfx[0])
+ # Write randomized SFX port
+ rom.write_byte(address + 0x03, selected_sfx[1] + 0xF9)
+
+def generate_shuffled_level_music(world: World):
shuffled_level_music = level_music_value_data.copy()
- if world.music_shuffle[player] == "consistent":
- world.per_slot_randoms[player].shuffle(shuffled_level_music)
- elif world.music_shuffle[player] == "singularity":
- single_song = world.per_slot_randoms[player].choice(shuffled_level_music)
+ if world.options.music_shuffle == "consistent":
+ world.random.shuffle(shuffled_level_music)
+ elif world.options.music_shuffle == "singularity":
+ single_song = world.random.choice(shuffled_level_music)
shuffled_level_music = [single_song for i in range(len(shuffled_level_music))]
return shuffled_level_music
-def generate_shuffled_ow_music(world, player):
+def generate_shuffled_ow_music(world: World):
shuffled_ow_music = ow_music_value_data.copy()
- if world.music_shuffle[player] == "consistent" or world.music_shuffle[player] == "full":
- world.per_slot_randoms[player].shuffle(shuffled_ow_music)
- elif world.music_shuffle[player] == "singularity":
- single_song = world.per_slot_randoms[player].choice(shuffled_ow_music)
+ if world.options.music_shuffle == "consistent" or world.options.music_shuffle == "full":
+ world.random.shuffle(shuffled_ow_music)
+ elif world.options.music_shuffle == "singularity":
+ single_song = world.random.choice(shuffled_ow_music)
shuffled_ow_music = [single_song for i in range(len(shuffled_ow_music))]
return shuffled_ow_music
-def generate_shuffled_ow_palettes(rom, world, player):
- if world.overworld_palette_shuffle[player]:
- for address, valid_palettes in valid_ow_palettes.items():
- chosen_palette = world.per_slot_randoms[player].choice(valid_palettes)
- rom.write_byte(address, chosen_palette)
+def generate_shuffled_ow_palettes(rom, world: World):
+ if world.options.overworld_palette_shuffle != "on_legacy":
+ return
+
+ for address, valid_palettes in valid_ow_palettes.items():
+ chosen_palette = world.random.choice(valid_palettes)
+ rom.write_byte(address, chosen_palette)
-def generate_shuffled_header_data(rom, world, player):
- if world.music_shuffle[player] != "full" and not world.foreground_palette_shuffle[player] and not world.background_palette_shuffle[player]:
+def generate_shuffled_header_data(rom, world: World):
+ if world.options.music_shuffle != "full" and world.options.level_palette_shuffle != "on_legacy":
return
for level_id in range(0, 0x200):
@@ -194,24 +650,425 @@ def generate_shuffled_header_data(rom, world, player):
tileset = level_header[4] & 0x0F
- if world.music_shuffle[player] == "full":
+ if world.options.music_shuffle == "full":
level_header[2] &= 0x8F
- level_header[2] |= (world.per_slot_randoms[player].randint(0, 7) << 5)
+ level_header[2] |= (world.random.randint(0, 7) << 5)
- if (world.foreground_palette_shuffle[player] and tileset in valid_foreground_palettes):
- level_header[3] &= 0xF8
- level_header[3] |= world.per_slot_randoms[player].choice(valid_foreground_palettes[tileset])
+ if world.options.level_palette_shuffle == "on_legacy":
+ if tileset in valid_foreground_palettes:
+ level_header[3] &= 0xF8
+ level_header[3] |= world.random.choice(valid_foreground_palettes[tileset])
- if world.background_palette_shuffle[player]:
layer2_ptr_list = list(rom.read_bytes(0x2E600 + level_id * 3, 3))
layer2_ptr = (layer2_ptr_list[2] << 16 | layer2_ptr_list[1] << 8 | layer2_ptr_list[0])
if layer2_ptr in valid_background_palettes:
level_header[0] &= 0x1F
- level_header[0] |= (world.per_slot_randoms[player].choice(valid_background_palettes[layer2_ptr]) << 5)
+ level_header[0] |= (world.random.choice(valid_background_palettes[layer2_ptr]) << 5)
if layer2_ptr in valid_background_colors:
level_header[1] &= 0x1F
- level_header[1] |= (world.per_slot_randoms[player].choice(valid_background_colors[layer2_ptr]) << 5)
+ level_header[1] |= (world.random.choice(valid_background_colors[layer2_ptr]) << 5)
rom.write_bytes(layer1_ptr, bytes(level_header))
+
+def generate_curated_level_palette_data(rom, world: World):
+ PALETTE_LEVEL_CODE_ADDR = 0x88000
+ PALETTE_INDEX_ADDR = 0x8F000
+ PALETTE_LEVEL_TILESET_ADDR = 0x8F200
+ PALETTE_LEVEL_PTR_ADDR = 0x92000
+ PALETTE_LEVEL_DATA_ADDR = 0xA8000
+
+ addr = pc_to_snes(PALETTE_LEVEL_PTR_ADDR)
+ snes_level_palette_pointers_1 = bytearray([0xBF, (addr)&0xFF, (addr>>8)&0xFF, (addr>>16)&0xFF])
+ snes_level_palette_pointers_2 = bytearray([0xBF, (addr+2)&0xFF, (addr>>8)&0xFF, (addr>>16)&0xFF])
+
+ # Enable curated palette loader
+ rom.write_bytes(0x02BED, bytearray([0x5C, 0x00, 0x80, 0x11])) # org $00ABED : jml custom_palettes
+ rom.write_bytes(0x02330, bytearray([0x5C, 0x02, 0x80, 0x11])) # org $00A318 : jml custom_palettes_original
+ rom.write_bytes(0x013D7, bytearray([0x20, 0x30, 0xA3])) # org $0093D7 : jmp $A330
+ rom.write_bytes(0x014DA, bytearray([0x20, 0x30, 0xA3])) # org $0094DA : jmp $A330
+ rom.write_bytes(0x015EC, bytearray([0x20, 0x30, 0xA3])) # org $0095EC : jmp $A330
+ rom.write_bytes(0x0165B, bytearray([0x20, 0x30, 0xA3])) # org $00965B : jmp $A330
+ rom.write_bytes(0x02DD9, bytearray([0x20, 0x30, 0xA3])) # org $00ADD9 : jmp $A330
+ rom.write_bytes(0x02E1F, bytearray([0x20, 0x30, 0xA3])) # org $00AE1F : jmp $A330
+
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0000, bytearray([0x80, 0x09])) # bra custom_palettes
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0002, bytearray([0xC2, 0x30])) # .original rep #$30
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0004, bytearray([0xA9, 0xDD, 0x7F])) # lda #$7FDD
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0007, bytearray([0x5C, 0xF2, 0xAB, 0x00])) # jml $00ABF2
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x000B, bytearray([0xC2, 0x30])) # custom_palettes: rep #$30
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x000D, bytearray([0xA9, 0x70, 0xB1])) # lda #$B170
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0010, bytearray([0x85, 0x0A])) # sta !_ptr
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0012, bytearray([0x64, 0x0C])) # stz !_ptr+$02
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0014, bytearray([0xA9, 0x10, 0x00])) # lda.w #$0010
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0017, bytearray([0x85, 0x04])) # sta !_index
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0019, bytearray([0xA9, 0x07, 0x00])) # lda #$0007
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x001C, bytearray([0x85, 0x06])) # sta !_x_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x001E, bytearray([0xA9, 0x01, 0x00])) # lda #$0001
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0021, bytearray([0x85, 0x08])) # sta !_y_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0023, bytearray([0x20, 0xE4, 0x80])) # jsr load_colors
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0026, bytearray([0xAE, 0x0B, 0x01])) # .get_index ldx $010B
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0029, bytearray([0xBF, 0x00, 0xF2, 0x11])) # lda.l level_tilesets,x
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x002D, bytearray([0x29, 0xFF, 0x00])) # and #$00FF
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0030, bytearray([0xEB])) # xba
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0031, bytearray([0x85, 0x00])) # sta !_tileset
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0033, bytearray([0xBF, 0x00, 0xF0, 0x11])) # lda.l level_index,x
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0037, bytearray([0x29, 0xFF, 0x00])) # and #$00FF
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x003A, bytearray([0x05, 0x00])) # ora !_tileset
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x003C, bytearray([0x85, 0x0A])) # sta !_ptr
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x003E, bytearray([0x0A])) # asl
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x003F, bytearray([0x18])) # clc
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0040, bytearray([0x65, 0x0A])) # adc !_ptr
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0042, bytearray([0x85, 0x0E])) # sta !_num
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0044, bytearray([0xAA])) # tax
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0045, snes_level_palette_pointers_1) # .back_color lda.l palette_pointers,x
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0049, bytearray([0x85, 0x0A])) # sta !_ptr
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x004B, snes_level_palette_pointers_2) # lda.l palette_pointers+$02,x
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x004F, bytearray([0x85, 0x0C])) # sta !_ptr+$02
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0051, bytearray([0xA7, 0x0A])) # lda [!_ptr]
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0053, bytearray([0x8D, 0x01, 0x07])) # sta $0701
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0056, bytearray([0xE6, 0x0A])) # inc !_ptr
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0058, bytearray([0xE6, 0x0A])) # inc !_ptr
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x005A, bytearray([0xA9, 0x02, 0x00])) # .background lda.w #$0001*$02
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x005D, bytearray([0x85, 0x04])) # sta !_index
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x005F, bytearray([0xA9, 0x06, 0x00])) # lda #$0006
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0062, bytearray([0x85, 0x06])) # sta !_x_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0064, bytearray([0xA9, 0x01, 0x00])) # lda #$0001
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0067, bytearray([0x85, 0x08])) # sta !_y_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0069, bytearray([0x20, 0xE4, 0x80])) # jsr load_colors
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x006C, bytearray([0xA9, 0x42, 0x00])) # .foreground lda.w #$0021*$02
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x006F, bytearray([0x85, 0x04])) # sta !_index
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0071, bytearray([0xA9, 0x06, 0x00])) # lda #$0006
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0074, bytearray([0x85, 0x06])) # sta !_x_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0076, bytearray([0xA9, 0x01, 0x00])) # lda #$0001
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0079, bytearray([0x85, 0x08])) # sta !_y_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x007B, bytearray([0x20, 0xE4, 0x80])) # jsr load_colors
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x007E, bytearray([0xA9, 0x52, 0x00])) # .berries lda.w #$0029*$02
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0081, bytearray([0x85, 0x04])) # sta !_index
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0083, bytearray([0xA9, 0x06, 0x00])) # lda #$0006
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0086, bytearray([0x85, 0x06])) # sta !_x_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0088, bytearray([0xA9, 0x02, 0x00])) # lda #$0002
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x008B, bytearray([0x85, 0x08])) # sta !_y_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x008D, bytearray([0xA5, 0x0A])) # lda !_ptr
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x008F, bytearray([0x48])) # pha
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0090, bytearray([0x20, 0xE4, 0x80])) # jsr load_colors
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0093, bytearray([0x68])) # pla
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0094, bytearray([0x85, 0x0A])) # sta !_ptr
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0096, bytearray([0xA9, 0x32, 0x01])) # lda.w #$0099*$02
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0099, bytearray([0x85, 0x04])) # sta !_index
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x009B, bytearray([0xA9, 0x06, 0x00])) # lda #$0006
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x009E, bytearray([0x85, 0x06])) # sta !_x_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00A0, bytearray([0xA9, 0x02, 0x00])) # lda #$0002
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00A3, bytearray([0x85, 0x08])) # sta !_y_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00A5, bytearray([0x20, 0xE4, 0x80])) # jsr load_colors
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00A8, bytearray([0xA9, 0x82, 0x00])) # .global lda.w #$0041*$02
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00AB, bytearray([0x85, 0x04])) # sta !_index
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00AD, bytearray([0xA9, 0x06, 0x00])) # lda #$0006
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00B0, bytearray([0x85, 0x06])) # sta !_x_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00B2, bytearray([0xA9, 0x0B, 0x00])) # lda #$000B
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00B5, bytearray([0x85, 0x08])) # sta !_y_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00B7, bytearray([0x20, 0xE4, 0x80])) # jsr load_colors
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00BA, bytearray([0xA5, 0x00])) # .sprite_specific lda !_tileset
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00BC, bytearray([0xC9, 0x00, 0x05])) # cmp #$0500
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00BF, bytearray([0xD0, 0x1D])) # bne .end
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00C1, bytearray([0xAD, 0x2E, 0x19])) # lda $192E
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00C4, bytearray([0x29, 0x0F, 0x00])) # and #$000F
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00C7, bytearray([0xC9, 0x02, 0x00])) # cmp #$0002
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00CA, bytearray([0xD0, 0x12])) # bne .end
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00CC, bytearray([0xA9, 0xC2, 0x01])) # lda.w #$00E1*$02
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00CF, bytearray([0x85, 0x04])) # sta !_index
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00D1, bytearray([0xA9, 0x06, 0x00])) # lda #$0006
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00D4, bytearray([0x85, 0x06])) # sta !_x_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00D6, bytearray([0xA9, 0x01, 0x00])) # lda #$0001
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00D9, bytearray([0x85, 0x08])) # sta !_y_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00DB, bytearray([0x20, 0xE4, 0x80])) # jsr load_colors
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00DE, bytearray([0xE2, 0x30])) # .end sep #$30
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00E0, bytearray([0x5C, 0xEC, 0xAC, 0x00])) # jml $00ACEC
+
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00E4, bytearray([0xA6, 0x04])) # load_colors: ldx !_index
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00E6, bytearray([0xA4, 0x06])) # ldy !_x_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00E8, bytearray([0xA7, 0x0A])) # .x_loop lda [!_ptr]
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00EA, bytearray([0x9D, 0x03, 0x07])) # sta $0703,x
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00ED, bytearray([0xE6, 0x0A])) # inc !_ptr
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00EF, bytearray([0xE6, 0x0A])) # inc !_ptr
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00F1, bytearray([0xE8])) # inx
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00F2, bytearray([0xE8])) # inx
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00F3, bytearray([0x88])) # dey
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00F4, bytearray([0x10, 0xF2])) # bpl .x_loop
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00F6, bytearray([0xA5, 0x04])) # lda !_index
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00F8, bytearray([0x18])) # clc
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00F9, bytearray([0x69, 0x20, 0x00])) # adc #$0020
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00FC, bytearray([0x85, 0x04])) # sta !_index
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00FE, bytearray([0xC6, 0x08])) # dec !_y_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0100, bytearray([0x10, 0xE2])) # bpl load_colors
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0102, bytearray([0x60])) # rts
+
+ # Load palette paths
+ data = pkgutil.get_data(__name__, f"data/palettes/level/palettes.json").decode("utf-8")
+ tilesets = json.loads(data)
+
+ # Writes the level tileset index to ROM
+ rom.write_bytes(PALETTE_LEVEL_TILESET_ADDR, bytearray(level_palette_index))
+
+ # Builds the table in ROM that holds the palette index for each level, including sublevels
+ for level_id in range(0x200):
+ tileset_num = level_palette_index[level_id]
+ if tileset_num != 0xFF:
+ tileset = tileset_names[tileset_num]
+ else:
+ tileset = tileset_names[0x19]
+ palette = world.random.randint(0, len(tilesets[tileset])-1)
+ rom.write_bytes(PALETTE_INDEX_ADDR + level_id, bytearray([palette]))
+
+ # Writes the actual level palette data and pointer to said data to the ROM
+ pal_offset = 0x0000
+ tileset_num = 0
+ bank_palette_count = 0
+ for tileset in tilesets.keys():
+ for palette in range(len(tilesets[tileset])):
+ # Handle bank crossing
+ if bank_palette_count == 110:
+ pal_offset = (pal_offset & 0xF8000) + 0x8000
+ bank_palette_count = 0
+ # Write pointer
+ data_ptr = pc_to_snes(PALETTE_LEVEL_DATA_ADDR + pal_offset)
+ rom.write_bytes(PALETTE_LEVEL_PTR_ADDR + ((tileset_num*3)<<8) + (palette*3), bytearray([data_ptr & 0xFF, (data_ptr>>8)&0xFF, (data_ptr>>16)&0xFF]))
+ # Write data
+ rom.write_bytes(PALETTE_LEVEL_DATA_ADDR + pal_offset, read_palette_file(tileset, tilesets[tileset][palette], "level"))
+ pal_offset += 0x128
+ bank_palette_count += 1
+ tileset_num += 1
+
+ # Fix eaten berry tiles
+ EATEN_BERRY_ADDR = 0x68248
+ rom.write_byte(EATEN_BERRY_ADDR + 0x01, 0x04)
+ rom.write_byte(EATEN_BERRY_ADDR + 0x03, 0x04)
+ rom.write_byte(EATEN_BERRY_ADDR + 0x05, 0x04)
+ rom.write_byte(EATEN_BERRY_ADDR + 0x07, 0x04)
+
+ # Fix title screen changing background colors
+ rom.write_bytes(0x1D30, bytearray([0xEA, 0xEA, 0xEA]))
+
+ # Skips level intros automatically
+ rom.write_byte(0x4896, 0x80)
+
+def generate_curated_map_palette_data(rom, world: World):
+ PALETTE_MAP_CODE_ADDR = 0x88200
+ PALETTE_UPLOADER_EDIT = 0x88400
+ PALETTE_MAP_INDEX_ADDR = 0x8F400
+ PALETTE_MAP_PTR_ADDR = 0x90000
+ PALETTE_MAP_DATA_ADDR = 0x98000
+
+ addr = pc_to_snes(PALETTE_MAP_PTR_ADDR)
+ snes_map_palette_pointers_1 = bytearray([0xBF, (addr)&0xFF, (addr>>8)&0xFF, (addr>>16)&0xFF])
+ snes_map_palette_pointers_2 = bytearray([0xBF, (addr+2)&0xFF, (addr>>8)&0xFF, (addr>>16)&0xFF])
+
+ rom.write_bytes(0x02D25, bytearray([0x5C, 0x09, 0x82, 0x11])) # org $00AD25 : jml map_palettes
+
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0000, bytearray([0xC2, 0x30])) # map_og_palettes: rep #$30
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0002, bytearray([0xA0, 0xD8, 0xB3])) # ldy #$B3D8
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0005, bytearray([0x5C, 0x2A, 0xAD, 0x00])) # jml $00AD2A
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0009, bytearray([0xC2, 0x30])) # map_palettes: rep #$30
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x000B, bytearray([0xAD, 0x31, 0x19])) # .prepare_index lda $1931
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x000E, bytearray([0x29, 0x0F, 0x00])) # and #$000F
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0011, bytearray([0x3A])) # dec
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0012, bytearray([0xAA])) # tax
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0013, bytearray([0xEB])) # xba
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0014, bytearray([0x85, 0x0E])) # sta !_num
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0016, bytearray([0xBF, 0x00, 0xF4, 0x11])) # lda.l map_index,x
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x001A, bytearray([0x29, 0xFF, 0x00])) # and #$00FF
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x001D, bytearray([0x05, 0x0E])) # ora !_num
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x001F, bytearray([0x85, 0x0A])) # sta !_ptr
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0021, bytearray([0x0A])) # asl
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0022, bytearray([0x18])) # clc
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0023, bytearray([0x65, 0x0A])) # adc !_ptr
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0025, bytearray([0xAA])) # tax
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0026, snes_map_palette_pointers_1) # lda.l map_palette_pointers,x
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x002A, bytearray([0x85, 0x0A])) # sta !_ptr
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x002C, snes_map_palette_pointers_2) # lda.l map_palette_pointers+$02,x
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0030, bytearray([0x85, 0x0C])) # sta !_ptr+$02
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0032, bytearray([0xA7, 0x0A])) # .load_back_color lda [!_ptr]
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0034, bytearray([0x8D, 0x01, 0x07])) # sta $0701
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0037, bytearray([0xE6, 0x0A])) # inc !_ptr
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0039, bytearray([0xE6, 0x0A])) # inc !_ptr
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x003B, bytearray([0xA9, 0x82, 0x00])) # .load_layer_2 lda.w #$0041*$02
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x003E, bytearray([0x85, 0x04])) # sta !_index
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0040, bytearray([0xA9, 0x06, 0x00])) # lda #$0006
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0043, bytearray([0x85, 0x06])) # sta !_x_span
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0045, bytearray([0xA9, 0x03, 0x00])) # lda #$0003
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0048, bytearray([0x85, 0x08])) # sta !_y_span
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x004A, bytearray([0x20, 0xE4, 0x80])) # jsr load_colors
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x004D, bytearray([0xA9, 0x52, 0x00])) # .load_layer_1 lda.w #$0029*$02
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0050, bytearray([0x85, 0x04])) # sta !_index
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0052, bytearray([0xA9, 0x06, 0x00])) # lda #$0006
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0055, bytearray([0x85, 0x06])) # sta !_x_span
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0057, bytearray([0xA9, 0x05, 0x00])) # lda #$0005
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x005A, bytearray([0x85, 0x08])) # sta !_y_span
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x005C, bytearray([0x20, 0xE4, 0x80])) # jsr load_colors
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x005F, bytearray([0xA9, 0x10, 0x00])) # .load_layer_3 lda.w #$0008*$02
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0062, bytearray([0x85, 0x04])) # sta !_index
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0064, bytearray([0xA9, 0x07, 0x00])) # lda #$0007
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0067, bytearray([0x85, 0x06])) # sta !_x_span
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0069, bytearray([0xA9, 0x01, 0x00])) # lda #$0001
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x006C, bytearray([0x85, 0x08])) # sta !_y_span
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x006E, bytearray([0x20, 0xE4, 0x80])) # jsr load_colors
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0071, bytearray([0xA9, 0x02, 0x01])) # .load_sprites lda.w #$0081*$02
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0074, bytearray([0x85, 0x04])) # sta !_index
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0076, bytearray([0xA9, 0x06, 0x00])) # lda #$0006
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0079, bytearray([0x85, 0x06])) # sta !_x_span
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x007B, bytearray([0xA9, 0x07, 0x00])) # lda #$0007
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x007E, bytearray([0x85, 0x08])) # sta !_y_span
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0080, bytearray([0x20, 0xE4, 0x80])) # jsr load_colors
+ rom.write_bytes(PALETTE_MAP_CODE_ADDR + 0x0083, bytearray([0x5C, 0xA3, 0xAD, 0x00])) # .return jml $00ADA3
+
+ rom.write_bytes(0x2488, bytearray([0x5C, 0x00, 0x84, 0x11])) # org $00A488 : jml palette_upload
+
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0000, bytearray([0xAD, 0x00, 0x01])) # palette_upload: lda $0100
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0003, bytearray([0xC9, 0x0E])) # cmp #$0E
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0005, bytearray([0xF0, 0x0A])) # beq .map
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0007, bytearray([0xAC, 0x80, 0x06])) # .regular ldy $0680
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x000A, bytearray([0xBE, 0x81, 0xA4])) # ldx.w $A47F+2,y
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x000D, bytearray([0x5C, 0x8E, 0xA4, 0x00])) # jml $00A48E
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0011, bytearray([0xAD, 0xD9, 0x13])) # .map lda $13D9
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0014, bytearray([0xC9, 0x0A])) # cmp #$0A
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0016, bytearray([0xD0, 0xEF])) # bne .regular
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0018, bytearray([0xAD, 0xE8, 0x1D])) # lda $1DE8
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x001B, bytearray([0xC9, 0x06])) # cmp #$06
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x001D, bytearray([0xD0, 0xE8])) # bne .regular
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x001F, bytearray([0x9C, 0x03, 0x07])) # stz $0703
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0022, bytearray([0x9C, 0x04, 0x07])) # stz $0704
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0025, bytearray([0x9C, 0x21, 0x21])) # stz $2121
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0028, bytearray([0xA2, 0x06])) # ldx #$06
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x002A, bytearray([0xBD, 0x49, 0x92])) # .loop lda.w $9249,x
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x002D, bytearray([0x9D, 0x20, 0x43])) # sta $4320,x
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0030, bytearray([0xCA])) # dex
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0031, bytearray([0x10, 0xF7])) # bpl .loop
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0033, bytearray([0xA9, 0x04])) # lda #$04
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0035, bytearray([0x8D, 0x0B, 0x42])) # sta $420B
+ rom.write_bytes(PALETTE_UPLOADER_EDIT + 0x0038, bytearray([0x5C, 0xCF, 0xA4, 0x00])) # jml $00A4CF
+
+ # Insert this piece of ASM again in case levels are disabled
+ PALETTE_LEVEL_CODE_ADDR = 0x88000
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00E4, bytearray([0xA6, 0x04])) # load_colors: ldx !_index
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00E6, bytearray([0xA4, 0x06])) # ldy !_x_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00E8, bytearray([0xA7, 0x0A])) # .x_loop lda [!_ptr]
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00EA, bytearray([0x9D, 0x03, 0x07])) # sta $0703,x
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00ED, bytearray([0xE6, 0x0A])) # inc !_ptr
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00EF, bytearray([0xE6, 0x0A])) # inc !_ptr
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00F1, bytearray([0xE8])) # inx
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00F2, bytearray([0xE8])) # inx
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00F3, bytearray([0x88])) # dey
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00F4, bytearray([0x10, 0xF2])) # bpl .x_loop
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00F6, bytearray([0xA5, 0x04])) # lda !_index
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00F8, bytearray([0x18])) # clc
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00F9, bytearray([0x69, 0x20, 0x00])) # adc #$0020
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00FC, bytearray([0x85, 0x04])) # sta !_index
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x00FE, bytearray([0xC6, 0x08])) # dec !_y_span
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0100, bytearray([0x10, 0xE2])) # bpl load_colors
+ rom.write_bytes(PALETTE_LEVEL_CODE_ADDR + 0x0102, bytearray([0x60])) # rts
+
+ # Load palette paths
+ data = pkgutil.get_data(__name__, f"data/palettes/map/palettes.json").decode("utf-8")
+ maps = json.loads(data)
+
+ for map_id in range(0x07):
+ current_map_name = map_names[map_id]
+ palette = world.random.randint(0, len(maps[current_map_name])-1)
+ rom.write_bytes(PALETTE_MAP_INDEX_ADDR + map_id, bytearray([palette]))
+
+ # Writes the actual map palette data and pointer to said data to the ROM
+ pal_offset = 0x0000
+ map_num = 0
+ bank_palette_count = 0
+ for current_map in maps.keys():
+ for palette in range(len(maps[current_map])):
+ # Handle bank crossing
+ if bank_palette_count == 113:
+ pal_offset = (pal_offset & 0xF8000) + 0x8000
+ bank_palette_count = 0
+ # Write pointer
+ data_ptr = pc_to_snes(PALETTE_MAP_DATA_ADDR + pal_offset)
+ rom.write_bytes(PALETTE_MAP_PTR_ADDR + ((map_num*3)<<8) + (palette*3), bytearray([data_ptr & 0xFF, (data_ptr>>8)&0xFF, (data_ptr>>16)&0xFF]))
+ # Write data
+ rom.write_bytes(PALETTE_MAP_DATA_ADDR + pal_offset, read_palette_file(current_map, maps[current_map][palette], "map"))
+ # Update map mario palette
+ chosen_palette = world.options.mario_palette.value
+ rom.write_bytes(PALETTE_MAP_DATA_ADDR + pal_offset + 206, bytes(ow_mario_palettes[chosen_palette]))
+ pal_offset += 0x11C
+ bank_palette_count += 1
+ map_num += 1
+
+
+def pc_to_snes(address):
+ return ((address << 1) & 0x7F0000) | (address & 0x7FFF) | 0x8000
+
+def read_palette_file(tileset, filename, type_):
+ palette_file = pkgutil.get_data(__name__, f"data/palettes/{type_}/{tileset}/{filename}")
+ colors = bytearray([])
+
+ # Copy back colors
+ colors += bytearray([palette_file[0x200], palette_file[0x201]])
+
+ if type_ == "level":
+ # Copy background colors
+ colors += bytearray([palette_file[(0x01*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x11*2)+(i)] for i in range(14)])
+
+ # Copy foreground colors
+ colors += bytearray([palette_file[(0x21*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x31*2)+(i)] for i in range(14)])
+
+ # Copy berry colors
+ colors += bytearray([palette_file[(0x29*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x39*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x49*2)+(i)] for i in range(14)])
+
+ # Copy global colors
+ colors += bytearray([palette_file[(0x41*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x51*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x61*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x71*2)+(i)] for i in range(14)])
+
+ # Copy sprite colors
+ colors += bytearray([palette_file[(0x81*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x91*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0xA1*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0xB1*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0xC1*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0xD1*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0xE1*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0xF1*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0xE9*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0xF9*2)+(i)] for i in range(14)])
+
+ elif type_ == "map":
+ # Copy layer 2 colors
+ colors += bytearray([palette_file[(0x41*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x51*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x61*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x71*2)+(i)] for i in range(14)])
+
+ # Copy layer 1 colors
+ colors += bytearray([palette_file[(0x29*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x39*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x49*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x59*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x69*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x79*2)+(i)] for i in range(14)])
+
+ # Copy layer 3 colors
+ colors += bytearray([palette_file[(0x08*2)+(i)] for i in range(16)])
+ colors += bytearray([palette_file[(0x18*2)+(i)] for i in range(16)])
+
+ # Copy sprite colors
+ colors += bytearray([palette_file[(0x81*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0x91*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0xA1*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0xB1*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0xC1*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0xD1*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0xE1*2)+(i)] for i in range(14)])
+ colors += bytearray([palette_file[(0xF1*2)+(i)] for i in range(14)])
+
+ return colors
diff --git a/worlds/smw/CHANGELOG.md b/worlds/smw/CHANGELOG.md
new file mode 100644
index 000000000000..7f62997adcef
--- /dev/null
+++ b/worlds/smw/CHANGELOG.md
@@ -0,0 +1,118 @@
+# Super Mario World - Changelog
+
+
+## v2.0
+
+### Features:
+
+- New optional Location Checks
+ - 3-Up Moons
+ - Hidden 1-Ups
+ - Bonus Blocks
+ - Blocksanity
+ - All blocks that contain coins or items are included, with the exception of:
+ - Blocks in Top Secret Area & Front Door/Bowser Castle
+ - Blocks that are unreachable without glitches/unreasonable movement
+- New Items
+ - Special Zone Clear
+ - New Filler Items
+ - 1 Coin
+ - 5 Coins
+ - 10 Coins
+ - 50 Coins
+ - New Trap Items
+ - Reverse Trap
+ - Thwimp Trap
+- SFX Shuffle
+- Palette Shuffle Overhaul
+ - New Curated Palette can now be used for the Overworld and Level Palette Shuffle options
+ - Foreground and Background Shuffle options have been merged into a single option
+- Max possible Yoshi Egg value is 255
+ - UI in-game is updated to handle 3-digits
+ - New `Display Received Item Popups` option: `progression_minus_yoshi_eggs`
+
+### Quality of Life:
+
+- In-Game Indicators are now displayed on the map screen for location checks and received items
+- In-level sprites are displayed upon receiving certain items
+- The Camera Scroll unlocking is now only enabled on levels where it needs to be
+- SMW can now handle receiving more than 255 items
+- Significant World Code cleanup
+ - New Options API
+ - Removal of `world: MultiWorld` across the world
+- The PopTracker pack now has tabs for every level/sublevel, and can automatically swap tabs while playing if connected to the server
+
+### Bug Fixes:
+
+- Several logic tweaks/fixes
+
+
+## v1.1
+
+### Features:
+
+- New Item
+ - Timer Trap
+- `Bowser Castle Rooms` option which changes the behavior of the Front Door
+- `Boss Shuffle` option
+- `Overworld Palette Shuffle` option
+
+### Quality of Life:
+
+- `Overworld Speed` option which allows the player to move faster or slower on the map
+- `Early Climb` option which guarantees that `Climb` will be found locally in an early location
+- `Exclude Special Zone` option which fully excludes Special Zone levels from the seed
+ - Note: this option is ignored if the player chooses to fill their entire item pool with Yoshi Eggs, which are Progression Items
+- Ice and Stun Traps are now queued if received while another of its type is already active, and are then activated at the next opportunity
+
+### Bug Fixes:
+
+- Fixed `Chocolate Island 4 - Dragon Coins` requiring `Run` instead of `P-Switch`
+- Fixed a Literature Trap typo
+
+
+## v1.0 - First Stable Release
+
+### Features:
+
+- Goal
+ - Bowser
+ - Defeat bosses, reach Bowser's Castle, and defeat Bowser
+ - Yoshi Egg Hunt
+ - Find a certain number of Yoshi Eggs spread across the MultiWorld`
+- Locations included:
+ - Level exits (Normal and Secret)
+ - Dragon Coins
+ - Collect at least five Dragon Coins in a level to send a location check
+- Items included:
+ - Run
+ - Carry
+ - Swim
+ - Spin Jump
+ - Climb
+ - Yoshi
+ - P-Switch
+ - P-Balloon
+ - Progressive Powerup
+ - Unlocks the ability to use Mushrooms, Fire Flowers, and Capes, progressively
+ - Super Star Activate
+ - Yellow Switch Palace
+ - Green Switch Palace
+ - Red Switch Palace
+ - Blue Switch Palace
+ - 1-Up Mushroom
+ - Yoshi Egg
+ - Only on `Yoshi Egg Hunt` goal
+ - Traps
+ - Ice Trap
+ - Stun Trap
+ - Literature Trap
+- `Bowser Castle Doors` option
+ - Whether the Front Door and Back Door map tiles lead to the Front Door or Back Door levels
+- DeathLink is supported
+- Level Shuffle is supported
+- Autosave is supported
+- Music Shuffle is supported
+- Mario's palette can be selected
+- Level Palettes can be shuffled
+- Starting life count can be set
diff --git a/worlds/smw/Client.py b/worlds/smw/Client.py
index 92aeac4d4a2b..600e1bff8304 100644
--- a/worlds/smw/Client.py
+++ b/worlds/smw/Client.py
@@ -1,5 +1,4 @@
import logging
-import asyncio
import time
from NetUtils import ClientStatus, color
@@ -17,11 +16,19 @@
SMW_ROMHASH_START = 0x7FC0
ROMHASH_SIZE = 0x15
-SMW_PROGRESS_DATA = WRAM_START + 0x1F02
-SMW_DRAGON_COINS_DATA = WRAM_START + 0x1F2F
-SMW_PATH_DATA = WRAM_START + 0x1EA2
-SMW_EVENT_ROM_DATA = ROM_START + 0x2D608
-SMW_ACTIVE_LEVEL_DATA = ROM_START + 0x37F70
+SMW_PROGRESS_DATA = WRAM_START + 0x1F02
+SMW_DRAGON_COINS_DATA = WRAM_START + 0x1F2F
+SMW_PATH_DATA = WRAM_START + 0x1EA2
+SMW_EVENT_ROM_DATA = ROM_START + 0x2D608
+SMW_ACTIVE_LEVEL_DATA = ROM_START + 0x37F70
+SMW_MOON_DATA = WRAM_START + 0x1FEE
+SMW_HIDDEN_1UP_DATA = WRAM_START + 0x1F3C
+SMW_BONUS_BLOCK_DATA = WRAM_START + 0x1A000
+SMW_BLOCKSANITY_DATA = WRAM_START + 0x1A400
+SMW_BLOCKSANITY_FLAGS = WRAM_START + 0x1A010
+SMW_LEVEL_CLEAR_FLAGS = WRAM_START + 0x1A200
+SMW_SPECIAL_WORLD_CLEAR = WRAM_START + 0x1F1E
+
SMW_GOAL_DATA = ROM_START + 0x01BFA0
SMW_REQUIRED_BOSSES_DATA = ROM_START + 0x01BFA1
@@ -31,32 +38,44 @@
SMW_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x01BFA5
SMW_DRAGON_COINS_ACTIVE_ADDR = ROM_START + 0x01BFA6
SMW_SWAMP_DONUT_GH_ADDR = ROM_START + 0x01BFA7
-
-SMW_GAME_STATE_ADDR = WRAM_START + 0x100
-SMW_MARIO_STATE_ADDR = WRAM_START + 0x71
-SMW_BOSS_STATE_ADDR = WRAM_START + 0xD9B
-SMW_ACTIVE_BOSS_ADDR = WRAM_START + 0x13FC
-SMW_CURRENT_LEVEL_ADDR = WRAM_START + 0x13BF
-SMW_MESSAGE_BOX_ADDR = WRAM_START + 0x1426
-SMW_BONUS_STAR_ADDR = WRAM_START + 0xF48
-SMW_EGG_COUNT_ADDR = WRAM_START + 0x1F24
-SMW_BOSS_COUNT_ADDR = WRAM_START + 0x1F26
-SMW_NUM_EVENTS_ADDR = WRAM_START + 0x1F2E
-SMW_SFX_ADDR = WRAM_START + 0x1DFC
-SMW_PAUSE_ADDR = WRAM_START + 0x13D4
-SMW_MESSAGE_QUEUE_ADDR = WRAM_START + 0xC391
-
-SMW_RECV_PROGRESS_ADDR = WRAM_START + 0x1F2B
-
-SMW_GOAL_LEVELS = [0x28, 0x31, 0x32]
-SMW_INVALID_MARIO_STATES = [0x05, 0x06, 0x0A, 0x0C, 0x0D]
-SMW_BAD_TEXT_BOX_LEVELS = [0x00, 0x26, 0x02, 0x4B]
-SMW_BOSS_STATES = [0x80, 0xC0, 0xC1]
-SMW_UNCOLLECTABLE_LEVELS = [0x25, 0x07, 0x0B, 0x40, 0x0E, 0x1F, 0x20, 0x1B, 0x1A, 0x35, 0x34, 0x31, 0x32]
+SMW_MOON_ACTIVE_ADDR = ROM_START + 0x01BFA8
+SMW_HIDDEN_1UP_ACTIVE_ADDR = ROM_START + 0x01BFA9
+SMW_BONUS_BLOCK_ACTIVE_ADDR = ROM_START + 0x01BFAA
+SMW_BLOCKSANITY_ACTIVE_ADDR = ROM_START + 0x01BFAB
+
+
+SMW_GAME_STATE_ADDR = WRAM_START + 0x100
+SMW_MARIO_STATE_ADDR = WRAM_START + 0x71
+SMW_BOSS_STATE_ADDR = WRAM_START + 0xD9B
+SMW_ACTIVE_BOSS_ADDR = WRAM_START + 0x13FC
+SMW_CURRENT_LEVEL_ADDR = WRAM_START + 0x13BF
+SMW_CURRENT_SUBLEVEL_ADDR = WRAM_START + 0x10B
+SMW_MESSAGE_BOX_ADDR = WRAM_START + 0x1426
+SMW_BONUS_STAR_ADDR = WRAM_START + 0xF48
+SMW_EGG_COUNT_ADDR = WRAM_START + 0x1F24
+SMW_BOSS_COUNT_ADDR = WRAM_START + 0x1F26
+SMW_NUM_EVENTS_ADDR = WRAM_START + 0x1F2E
+SMW_SFX_ADDR = WRAM_START + 0x1DFC
+SMW_PAUSE_ADDR = WRAM_START + 0x13D4
+SMW_MESSAGE_QUEUE_ADDR = WRAM_START + 0xC391
+SMW_ACTIVE_THWIMP_ADDR = WRAM_START + 0x0F3C
+SMW_GOAL_ITEM_COUNT = WRAM_START + 0x1A01E
+
+SMW_RECV_PROGRESS_ADDR = WRAM_START + 0x01F2B
+
+SMW_BLOCKSANITY_BLOCK_COUNT = 582
+
+SMW_GOAL_LEVELS = [0x28, 0x31, 0x32]
+SMW_INVALID_MARIO_STATES = [0x05, 0x06, 0x0A, 0x0C, 0x0D]
+SMW_BAD_TEXT_BOX_LEVELS = [0x00, 0x26, 0x02, 0x4B]
+SMW_BOSS_STATES = [0x80, 0xC0, 0xC1]
+SMW_UNCOLLECTABLE_LEVELS = [0x25, 0x07, 0x0B, 0x40, 0x0E, 0x1F, 0x20, 0x1B, 0x1A, 0x35, 0x34, 0x31, 0x32]
+SMW_UNCOLLECTABLE_DRAGON_COINS = [0x24]
class SMWSNIClient(SNIClient):
game = "Super Mario World"
+ patch_suffix = ".apsmw"
async def deathlink_kill_player(self, ctx):
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
@@ -114,6 +133,9 @@ async def validate_rom(self, ctx):
if death_link:
await ctx.update_death_link(bool(death_link[0] & 0b1))
+ if ctx.rom != rom_name:
+ ctx.current_sublevel_value = 0
+
ctx.rom = rom_name
return True
@@ -175,6 +197,11 @@ def add_trap_to_queue(self, trap_item, trap_msg):
self.trap_queue.append((trap_item, trap_msg))
+ def should_show_message(self, ctx, next_item):
+ return ctx.receive_option == 1 or \
+ (ctx.receive_option == 2 and ((next_item.flags & 1) != 0)) or \
+ (ctx.receive_option == 3 and ((next_item.flags & 1) != 0 and next_item.item != 0xBC0002))
+
async def handle_trap_queue(self, ctx):
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
@@ -196,7 +223,7 @@ async def handle_trap_queue(self, ctx):
next_trap, message = self.trap_queue.pop(0)
- from worlds.smw.Rom import trap_rom_data
+ from .Rom import trap_rom_data
if next_trap.item in trap_rom_data:
trap_active = await snes_read(ctx, WRAM_START + trap_rom_data[next_trap.item][0], 0x3)
@@ -216,6 +243,13 @@ async def handle_trap_queue(self, ctx):
self.add_trap_to_queue(next_trap, message)
return
else:
+ if next_trap.item == 0xBC001D:
+ # Special case thwimp trap
+ # Do not fire if the previous thwimp hasn't reached the player's Y pos
+ active_thwimp = await snes_read(ctx, SMW_ACTIVE_THWIMP_ADDR, 0x1)
+ if active_thwimp[0] != 0xFF:
+ self.add_trap_to_queue(next_trap, message)
+ return
verify_game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
if verify_game_state[0] == 0x14 and len(trap_rom_data[next_trap.item]) > 2:
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([trap_rom_data[next_trap.item][2]]))
@@ -235,13 +269,14 @@ async def handle_trap_queue(self, ctx):
if active_boss[0] != 0x00:
return
- if ctx.receive_option == 1 or (ctx.receive_option == 2 and ((next_trap.flags & 1) != 0)):
+ if self.should_show_message(ctx, next_trap):
self.add_message_to_queue(message)
async def game_watcher(self, ctx):
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
-
+
+ boss_state = await snes_read(ctx, SMW_BOSS_STATE_ADDR, 0x1)
game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1)
if game_state is None:
@@ -258,6 +293,7 @@ async def game_watcher(self, ctx):
elif game_state[0] < 0x0B:
# We haven't loaded a save file
ctx.message_queue = []
+ ctx.current_sublevel_value = 0
return
elif mario_state[0] in SMW_INVALID_MARIO_STATES:
# Mario can't come to the phone right now
@@ -303,8 +339,18 @@ async def game_watcher(self, ctx):
progress_data = bytearray(await snes_read(ctx, SMW_PROGRESS_DATA, 0x0F))
dragon_coins_data = bytearray(await snes_read(ctx, SMW_DRAGON_COINS_DATA, 0x0C))
dragon_coins_active = await snes_read(ctx, SMW_DRAGON_COINS_ACTIVE_ADDR, 0x1)
- from worlds.smw.Rom import item_rom_data, ability_rom_data, trap_rom_data
- from worlds.smw.Levels import location_id_to_level_id, level_info_dict
+ moon_data = bytearray(await snes_read(ctx, SMW_MOON_DATA, 0x0C))
+ moon_active = await snes_read(ctx, SMW_MOON_ACTIVE_ADDR, 0x1)
+ hidden_1up_data = bytearray(await snes_read(ctx, SMW_HIDDEN_1UP_DATA, 0x0C))
+ hidden_1up_active = await snes_read(ctx, SMW_HIDDEN_1UP_ACTIVE_ADDR, 0x1)
+ bonus_block_data = bytearray(await snes_read(ctx, SMW_BONUS_BLOCK_DATA, 0x0C))
+ bonus_block_active = await snes_read(ctx, SMW_BONUS_BLOCK_ACTIVE_ADDR, 0x1)
+ blocksanity_data = bytearray(await snes_read(ctx, SMW_BLOCKSANITY_DATA, SMW_BLOCKSANITY_BLOCK_COUNT))
+ blocksanity_flags = bytearray(await snes_read(ctx, SMW_BLOCKSANITY_FLAGS, 0xC))
+ blocksanity_active = await snes_read(ctx, SMW_BLOCKSANITY_ACTIVE_ADDR, 0x1)
+ level_clear_flags = bytearray(await snes_read(ctx, SMW_LEVEL_CLEAR_FLAGS, 0x60))
+ from .Rom import item_rom_data, ability_rom_data, trap_rom_data, icon_rom_data
+ from .Levels import location_id_to_level_id, level_info_dict, level_blocks_data
from worlds import AutoWorldRegister
for loc_name, level_data in location_id_to_level_id.items():
loc_id = AutoWorldRegister.world_types[ctx.game].location_name_to_id[loc_name]
@@ -326,6 +372,54 @@ async def game_watcher(self, ctx):
if bit_set:
new_checks.append(loc_id)
+ elif level_data[1] == 3:
+ # Moon Check
+ if not moon_active or moon_active[0] == 0:
+ continue
+
+ progress_byte = (level_data[0] // 8)
+ progress_bit = 7 - (level_data[0] % 8)
+
+ data = moon_data[progress_byte]
+ masked_data = data & (1 << progress_bit)
+ bit_set = (masked_data != 0)
+
+ if bit_set:
+ new_checks.append(loc_id)
+ elif level_data[1] == 4:
+ # Hidden 1-Up Check
+ if not hidden_1up_active or hidden_1up_active[0] == 0:
+ continue
+
+ progress_byte = (level_data[0] // 8)
+ progress_bit = 7 - (level_data[0] % 8)
+
+ data = hidden_1up_data[progress_byte]
+ masked_data = data & (1 << progress_bit)
+ bit_set = (masked_data != 0)
+
+ if bit_set:
+ new_checks.append(loc_id)
+ elif level_data[1] == 5:
+ # Bonus Block Check
+ if not bonus_block_active or bonus_block_active[0] == 0:
+ continue
+
+ progress_byte = (level_data[0] // 8)
+ progress_bit = 7 - (level_data[0] % 8)
+
+ data = bonus_block_data[progress_byte]
+ masked_data = data & (1 << progress_bit)
+ bit_set = (masked_data != 0)
+
+ if bit_set:
+ new_checks.append(loc_id)
+ elif level_data[1] >= 100:
+ if not blocksanity_active or blocksanity_active[0] == 0:
+ continue
+ block_index = level_data[1] - 100
+ if blocksanity_data[block_index] != 0:
+ new_checks.append(loc_id)
else:
event_id_value = event_id + level_data[1]
@@ -354,38 +448,75 @@ async def game_watcher(self, ctx):
for new_check_id in new_checks:
ctx.locations_checked.add(new_check_id)
- location = ctx.location_names[new_check_id]
+ location = ctx.location_names.lookup_in_game(new_check_id)
snes_logger.info(
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
+ # Send Current Room for Tracker
+ current_sublevel_data = await snes_read(ctx, SMW_CURRENT_SUBLEVEL_ADDR, 2)
+ current_sublevel_value = current_sublevel_data[0] + (current_sublevel_data[1] << 8)
+
+ if game_state[0] != 0x14:
+ current_sublevel_value = 0
+
+ if ctx.current_sublevel_value != current_sublevel_value:
+ ctx.current_sublevel_value = current_sublevel_value
+
+ # Send level id data to tracker
+ await ctx.send_msgs(
+ [
+ {
+ "cmd": "Set",
+ "key": f"smw_curlevelid_{ctx.team}_{ctx.slot}",
+ "default": 0,
+ "want_reply": False,
+ "operations": [
+ {
+ "operation": "replace",
+ "value": ctx.current_sublevel_value,
+ }
+ ],
+ }
+ ]
+ )
+
if game_state[0] != 0x14:
# Don't receive items or collect locations outside of in-level mode
+ ctx.current_sublevel_value = 0
+ return
+
+ if boss_state[0] in SMW_BOSS_STATES:
+ # Don't receive items or collect locations inside boss battles
return
- recv_count = await snes_read(ctx, SMW_RECV_PROGRESS_ADDR, 1)
- recv_index = recv_count[0]
+ recv_count = await snes_read(ctx, SMW_RECV_PROGRESS_ADDR, 2)
+ if recv_count is None:
+ # Add a small failsafe in case we get a None. Other SNI games do this...
+ return
+ recv_index = recv_count[0] | (recv_count[1] << 8)
if recv_index < len(ctx.items_received):
item = ctx.items_received[recv_index]
recv_index += 1
+ sending_game = ctx.slot_info[item.player].game
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
- color(ctx.item_names[item.item], 'red', 'bold'),
+ color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'),
color(ctx.player_names[item.player], 'yellow'),
- ctx.location_names[item.location], recv_index, len(ctx.items_received)))
+ ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received)))
- if ctx.receive_option == 1 or (ctx.receive_option == 2 and ((item.flags & 1) != 0)):
+ if self.should_show_message(ctx, item):
if item.item != 0xBC0012 and item.item not in trap_rom_data:
# Don't send messages for Boss Tokens
- item_name = ctx.item_names[item.item]
+ item_name = ctx.item_names.lookup_in_game(item.item)
player_name = ctx.player_names[item.player]
receive_message = generate_received_text(item_name, player_name)
self.add_message_to_queue(receive_message)
- snes_buffered_write(ctx, SMW_RECV_PROGRESS_ADDR, bytes([recv_index]))
+ snes_buffered_write(ctx, SMW_RECV_PROGRESS_ADDR, bytes([recv_index&0xFF, (recv_index>>8)&0xFF]))
if item.item in trap_rom_data:
- item_name = ctx.item_names[item.item]
+ item_name = ctx.item_names.lookup_in_game(item.item)
player_name = ctx.player_names[item.player]
receive_message = generate_received_text(item_name, player_name)
@@ -404,6 +535,15 @@ async def game_watcher(self, ctx):
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([item_rom_data[item.item][2]]))
snes_buffered_write(ctx, WRAM_START + item_rom_data[item.item][0], bytes([new_item_count]))
+ elif item.item in icon_rom_data:
+ queue_addr = await snes_read(ctx, WRAM_START + icon_rom_data[item.item][0], 2)
+ queue_addr = queue_addr[0] + (queue_addr[1] << 8)
+ queue_addr += 1
+ snes_buffered_write(ctx, WRAM_START + icon_rom_data[item.item][0], bytes([queue_addr&0xFF, (queue_addr>>8)&0xFF]))
+ if (goal[0] == 0 and item.item == 0xBC0012) or (goal[0] == 1 and item.item == 0xBC0002):
+ goal_item_count = await snes_read(ctx, SMW_GOAL_ITEM_COUNT, 1)
+ snes_buffered_write(ctx, SMW_GOAL_ITEM_COUNT, bytes([goal_item_count[0] + 1]))
+
elif item.item in ability_rom_data:
# Handle Upgrades
for rom_data in ability_rom_data[item.item]:
@@ -448,10 +588,16 @@ async def game_watcher(self, ctx):
path_data = bytearray(await snes_read(ctx, SMW_PATH_DATA, 0x60))
donut_gh_swapped = await snes_read(ctx, SMW_SWAMP_DONUT_GH_ADDR, 0x1)
new_dragon_coin = False
+ new_moon = False
+ new_hidden_1up = False
+ new_bonus_block = False
+ new_blocksanity = False
+ new_blocksanity_flags = False
+
for loc_id in ctx.checked_locations:
if loc_id not in ctx.locations_checked:
ctx.locations_checked.add(loc_id)
- loc_name = ctx.location_names[loc_id]
+ loc_name = ctx.location_names.lookup_in_game(loc_id)
if loc_name not in location_id_to_level_id:
continue
@@ -460,6 +606,8 @@ async def game_watcher(self, ctx):
if level_data[1] == 2:
# Dragon Coins Check
+ if level_data[0] in SMW_UNCOLLECTABLE_DRAGON_COINS:
+ continue
progress_byte = (level_data[0] // 8)
progress_bit = 7 - (level_data[0] % 8)
@@ -469,10 +617,64 @@ async def game_watcher(self, ctx):
dragon_coins_data[progress_byte] = new_data
new_dragon_coin = True
+ elif level_data[1] == 3:
+ # Moon Check
+
+ progress_byte = (level_data[0] // 8)
+ progress_bit = 7 - (level_data[0] % 8)
+
+ data = moon_data[progress_byte]
+ new_data = data | (1 << progress_bit)
+ moon_data[progress_byte] = new_data
+
+ new_moon = True
+ elif level_data[1] == 4:
+ # Hidden 1-Up Check
+ progress_byte = (level_data[0] // 8)
+ progress_bit = 7 - (level_data[0] % 8)
+
+ data = hidden_1up_data[progress_byte]
+ new_data = data | (1 << progress_bit)
+ hidden_1up_data[progress_byte] = new_data
+
+ new_hidden_1up = True
+ elif level_data[1] == 5:
+ # Bonus block prize Check
+
+ progress_byte = (level_data[0] // 8)
+ progress_bit = 7 - (level_data[0] % 8)
+
+ data = bonus_block_data[progress_byte]
+ new_data = data | (1 << progress_bit)
+ bonus_block_data[progress_byte] = new_data
+
+ new_bonus_block = True
+ elif level_data[1] >= 100:
+ # Blocksanity flag Check
+ block_index = level_data[1] - 100
+ blocksanity_data[block_index] = 1
+ new_blocksanity = True
+
+ # All blocksanity blocks flag
+ new_blocksanity_flags = True
+ for block_id in level_blocks_data[level_data[0]]:
+ if blocksanity_data[block_id] != 1:
+ new_blocksanity_flags = False
+ continue
+ if new_blocksanity_flags is True:
+ progress_byte = (level_data[0] // 8)
+ progress_bit = 7 - (level_data[0] % 8)
+ data = blocksanity_flags[progress_byte]
+ new_data = data | (1 << progress_bit)
+ blocksanity_flags[progress_byte] = new_data
else:
if level_data[0] in SMW_UNCOLLECTABLE_LEVELS:
continue
+ # Handle map indicators
+ flag = 1 if level_data[1] == 0 else 2
+ level_clear_flags[level_data[0]] |= flag
+
event_id = event_data[level_data[0]]
event_id_value = event_id + level_data[1]
@@ -513,7 +715,18 @@ async def game_watcher(self, ctx):
if new_dragon_coin:
snes_buffered_write(ctx, SMW_DRAGON_COINS_DATA, bytes(dragon_coins_data))
+ if new_moon:
+ snes_buffered_write(ctx, SMW_MOON_DATA, bytes(moon_data))
+ if new_hidden_1up:
+ snes_buffered_write(ctx, SMW_HIDDEN_1UP_DATA, bytes(hidden_1up_data))
+ if new_bonus_block:
+ snes_buffered_write(ctx, SMW_BONUS_BLOCK_DATA, bytes(bonus_block_data))
+ if new_blocksanity:
+ snes_buffered_write(ctx, SMW_BLOCKSANITY_DATA, bytes(blocksanity_data))
+ if new_blocksanity_flags:
+ snes_buffered_write(ctx, SMW_BLOCKSANITY_FLAGS, bytes(blocksanity_flags))
if new_events > 0:
+ snes_buffered_write(ctx, SMW_LEVEL_CLEAR_FLAGS, bytes(level_clear_flags))
snes_buffered_write(ctx, SMW_PROGRESS_DATA, bytes(progress_data))
snes_buffered_write(ctx, SMW_PATH_DATA, bytes(path_data))
old_events = await snes_read(ctx, SMW_NUM_EVENTS_ADDR, 0x1)
diff --git a/worlds/smw/Items.py b/worlds/smw/Items.py
index 5b6cce5a7f6e..eaf58b9b8e4e 100644
--- a/worlds/smw/Items.py
+++ b/worlds/smw/Items.py
@@ -18,6 +18,10 @@ class SMWItem(Item):
# Separate tables for each type of item.
junk_table = {
+ ItemName.one_coin: ItemData(0xBC0017, False),
+ ItemName.five_coins: ItemData(0xBC0018, False),
+ ItemName.ten_coins: ItemData(0xBC0019, False),
+ ItemName.fifty_coins: ItemData(0xBC001A, False),
ItemName.one_up_mushroom: ItemData(0xBC0001, False),
}
@@ -36,6 +40,7 @@ class SMWItem(Item):
ItemName.progressive_powerup: ItemData(0xBC000A, True),
ItemName.p_balloon: ItemData(0xBC000B, True),
ItemName.super_star_active: ItemData(0xBC000D, True),
+ ItemName.special_world_clear: ItemData(0xBC001B, True),
}
switch_palace_table = {
@@ -46,10 +51,12 @@ class SMWItem(Item):
}
trap_table = {
- ItemName.ice_trap: ItemData(0xBC0013, False, True),
- ItemName.stun_trap: ItemData(0xBC0014, False, True),
- ItemName.literature_trap: ItemData(0xBC0015, False, True),
- ItemName.timer_trap: ItemData(0xBC0016, False, True),
+ ItemName.ice_trap: ItemData(0xBC0013, False, True),
+ ItemName.stun_trap: ItemData(0xBC0014, False, True),
+ ItemName.literature_trap: ItemData(0xBC0015, False, True),
+ ItemName.timer_trap: ItemData(0xBC0016, False, True),
+ ItemName.reverse_controls_trap: ItemData(0xBC001C, False, True),
+ ItemName.thwimp_trap: ItemData(0xBC001D, False, True),
}
event_table = {
diff --git a/worlds/smw/Levels.py b/worlds/smw/Levels.py
index 3940a08c7c5b..7aa9428b9110 100644
--- a/worlds/smw/Levels.py
+++ b/worlds/smw/Levels.py
@@ -1,4 +1,5 @@
+from worlds.AutoWorld import World
from .Names import LocationName
@@ -75,6 +76,103 @@ def __init__(self, name: str, exitAddress: int, roomID: int, exitAddressAlt=None
]
+level_blocks_data = {
+ 0x01: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ 0x02: [12, 13],
+ 0x04: [14, 15, 16, 17, 18, 19],
+ 0x05: [20, 21, 22, 23, 24, 25],
+ 0x06: [26, 27, 28, 29],
+ 0x07: [30, 31, 32, 33, 34, 35, 36, 37, 38, 39],
+ 0x09: [40, 41, 42, 43, 44, 45, 46, 47, 48, 49],
+ 0x0A: [50, 51, 52, 53, 54, 55, 56, 57, 58, 59],
+ 0x0B: [60, 61, 62],
+ 0x0C: [63, 64, 65, 66, 67, 68],
+ 0x0D: [69, 70, 71],
+ 0x0E: [72],
+ 0x0F: [73, 74, 75, 76],
+ 0x10: [77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93,
+ 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108,
+ 109, 110, 111
+ ],
+ 0x11: [112],
+ 0x13: [113, 114, 115, 116, 117],
+ 0x15: [118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131,
+ 132, 133, 134, 135, 136, 137, 138, 139, 140
+ ],
+ 0x18: [141, 142],
+ 0x1A: [143, 144, 145],
+ 0x1B: [146, 147, 148, 149, 150],
+ 0x1C: [151, 152, 153, 154],
+ 0x1D: [155, 156, 157],
+ 0x1F: [158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168],
+ 0x20: [169],
+ 0x21: [170, 171, 172],
+ 0x22: [173, 174, 175, 176, 177],
+ 0x23: [178, 179, 180, 181, 182, 183, 184, 185, 186],
+ 0x24: [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200,
+ 201, 202
+ ],
+ 0x25: [203, 204, 205, 206, 207, 208],
+ 0x26: [209, 210, 211, 212],
+ 0x27: [213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226,
+ 227, 228, 229
+ ],
+ 0x29: [230, 231, 232, 233],
+ 0x2A: [234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247,
+ 248, 249
+ ],
+ 0x2B: [250, 251, 252, 253, 254],
+ 0x2D: [255, 256, 257, 258, 259, 260, 261, 262],
+ 0x2E: [263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276,
+ 277, 278, 279
+ ],
+ 0x2F: [280, 281, 282, 283, 284],
+ 0x33: [285, 286, 287, 288, 289, 290],
+ 0x34: [291, 292, 293],
+ 0x35: [294, 295],
+ 0x37: [296, 297],
+ 0x38: [298, 299, 300, 301],
+ 0x39: [302, 303, 304, 305],
+ 0x3A: [306, 307, 308, 309, 310, 311, 312, 313, 314],
+ 0x3B: [315, 316],
+ 0x3C: [317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330],
+ 0x3D: [331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341],
+ 0x3E: [342, 343, 344, 345, 346, 347, 348, 349, 350, 351],
+ 0x40: [352, 353, 354, 355, 356],
+ 0x41: [357, 358, 359, 360, 361],
+ 0x42: [362, 363, 364, 365, 366],
+ 0x43: [367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379],
+ 0x44: [380, 381, 382, 383, 384, 385, 386],
+ 0x46: [387, 388, 389],
+ 0x47: [390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403,
+ 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416
+ ],
+ 0x49: [417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430,
+ 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443,
+ 444, 445, 446
+ ],
+ 0x4A: [447, 448, 449, 450, 451],
+ 0x4B: [452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465,
+ 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478,
+ 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489
+ ],
+ 0x4C: [490],
+ 0x4E: [491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504,
+ 505, 506, 507, 508, 509, 510, 511, 512
+ ],
+ 0x4F: [513, 514, 515, 516, 517, 518, 519, 520, 521, 522],
+ 0x50: [523, 524, 525],
+ 0x51: [526, 527],
+ 0x54: [528],
+ 0x56: [529],
+ 0x59: [530, 531, 532, 533, 534, 535, 536, 537, 538],
+ 0x5A: [539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552,
+ 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565,
+ 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578,
+ 579, 580, 581
+ ]
+}
+
class SMWPath():
thisEndDirection: int
otherLevelID: int
@@ -330,12 +428,15 @@ def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1
location_id_to_level_id = {
LocationName.yoshis_island_1_exit_1: [0x29, 0],
LocationName.yoshis_island_1_dragon: [0x29, 2],
+ LocationName.yoshis_island_1_moon: [0x29, 3],
LocationName.yoshis_island_2_exit_1: [0x2A, 0],
LocationName.yoshis_island_2_dragon: [0x2A, 2],
LocationName.yoshis_island_3_exit_1: [0x27, 0],
LocationName.yoshis_island_3_dragon: [0x27, 2],
+ LocationName.yoshis_island_3_bonus_block: [0x27, 5],
LocationName.yoshis_island_4_exit_1: [0x26, 0],
LocationName.yoshis_island_4_dragon: [0x26, 2],
+ LocationName.yoshis_island_4_hidden_1up: [0x26, 4],
LocationName.yoshis_island_castle: [0x25, 0],
LocationName.yoshis_island_koopaling: [0x25, 0],
LocationName.yellow_switch_palace: [0x14, 0],
@@ -343,13 +444,17 @@ def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1
LocationName.donut_plains_1_exit_1: [0x15, 0],
LocationName.donut_plains_1_exit_2: [0x15, 1],
LocationName.donut_plains_1_dragon: [0x15, 2],
+ LocationName.donut_plains_1_hidden_1up: [0x15, 4],
LocationName.donut_plains_2_exit_1: [0x09, 0],
LocationName.donut_plains_2_exit_2: [0x09, 1],
LocationName.donut_plains_2_dragon: [0x09, 2],
LocationName.donut_plains_3_exit_1: [0x05, 0],
LocationName.donut_plains_3_dragon: [0x05, 2],
+ LocationName.donut_plains_3_bonus_block: [0x05, 5],
LocationName.donut_plains_4_exit_1: [0x06, 0],
LocationName.donut_plains_4_dragon: [0x06, 2],
+ LocationName.donut_plains_4_moon: [0x06, 3],
+ LocationName.donut_plains_4_hidden_1up: [0x06, 4],
LocationName.donut_secret_1_exit_1: [0x0A, 0],
LocationName.donut_secret_1_exit_2: [0x0A, 1],
LocationName.donut_secret_1_dragon: [0x0A, 2],
@@ -360,6 +465,7 @@ def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1
LocationName.donut_secret_house_exit_1: [0x13, 0],
LocationName.donut_secret_house_exit_2: [0x13, 1],
LocationName.donut_plains_castle: [0x07, 0],
+ LocationName.donut_plains_castle_hidden_1up: [0x07, 4],
LocationName.donut_plains_koopaling: [0x07, 0],
LocationName.green_switch_palace: [0x08, 0],
@@ -371,8 +477,10 @@ def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1
LocationName.vanilla_dome_2_dragon: [0x3C, 2],
LocationName.vanilla_dome_3_exit_1: [0x2E, 0],
LocationName.vanilla_dome_3_dragon: [0x2E, 2],
+ LocationName.vanilla_dome_3_moon: [0x2E, 3],
LocationName.vanilla_dome_4_exit_1: [0x3D, 0],
LocationName.vanilla_dome_4_dragon: [0x3D, 2],
+ LocationName.vanilla_dome_4_hidden_1up: [0x3D, 4],
LocationName.vanilla_secret_1_exit_1: [0x2D, 0],
LocationName.vanilla_secret_1_exit_2: [0x2D, 1],
LocationName.vanilla_secret_1_dragon: [0x2D, 2],
@@ -382,7 +490,9 @@ def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1
LocationName.vanilla_secret_3_dragon: [0x02, 2],
LocationName.vanilla_ghost_house_exit_1: [0x2B, 0],
LocationName.vanilla_ghost_house_dragon: [0x2B, 2],
+ LocationName.vanilla_ghost_house_hidden_1up: [0x2B, 4],
LocationName.vanilla_fortress: [0x0B, 0],
+ LocationName.vanilla_fortress_hidden_1up: [0x0B, 4],
LocationName.vanilla_reznor: [0x0B, 0],
LocationName.vanilla_dome_castle: [0x40, 0],
LocationName.vanilla_dome_koopaling: [0x40, 0],
@@ -390,13 +500,16 @@ def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1
LocationName.butter_bridge_1_exit_1: [0x0C, 0],
LocationName.butter_bridge_1_dragon: [0x0C, 2],
+ LocationName.butter_bridge_1_bonus_block: [0x0C, 5],
LocationName.butter_bridge_2_exit_1: [0x0D, 0],
LocationName.butter_bridge_2_dragon: [0x0D, 2],
LocationName.cheese_bridge_exit_1: [0x0F, 0],
LocationName.cheese_bridge_exit_2: [0x0F, 1],
LocationName.cheese_bridge_dragon: [0x0F, 2],
+ LocationName.cheese_bridge_moon: [0x0F, 3],
LocationName.cookie_mountain_exit_1: [0x10, 0],
LocationName.cookie_mountain_dragon: [0x10, 2],
+ LocationName.cookie_mountain_hidden_1up: [0x10, 4],
LocationName.soda_lake_exit_1: [0x11, 0],
LocationName.soda_lake_dragon: [0x11, 2],
LocationName.twin_bridges_castle: [0x0E, 0],
@@ -410,12 +523,14 @@ def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1
LocationName.forest_of_illusion_3_exit_1: [0x47, 0],
LocationName.forest_of_illusion_3_exit_2: [0x47, 1],
LocationName.forest_of_illusion_3_dragon: [0x47, 2],
+ LocationName.forest_of_illusion_3_hidden_1up: [0x47, 4],
LocationName.forest_of_illusion_4_exit_1: [0x43, 0],
LocationName.forest_of_illusion_4_exit_2: [0x43, 1],
LocationName.forest_of_illusion_4_dragon: [0x43, 2],
LocationName.forest_ghost_house_exit_1: [0x41, 0],
LocationName.forest_ghost_house_exit_2: [0x41, 1],
LocationName.forest_ghost_house_dragon: [0x41, 2],
+ LocationName.forest_ghost_house_moon: [0x41, 3],
LocationName.forest_secret_exit_1: [0x46, 0],
LocationName.forest_secret_dragon: [0x46, 2],
LocationName.forest_fortress: [0x1F, 0],
@@ -427,12 +542,15 @@ def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1
LocationName.chocolate_island_1_exit_1: [0x22, 0],
LocationName.chocolate_island_1_dragon: [0x22, 2],
+ LocationName.chocolate_island_1_moon: [0x22, 3],
LocationName.chocolate_island_2_exit_1: [0x24, 0],
LocationName.chocolate_island_2_exit_2: [0x24, 1],
LocationName.chocolate_island_2_dragon: [0x24, 2],
+ LocationName.chocolate_island_2_hidden_1up: [0x24, 4],
LocationName.chocolate_island_3_exit_1: [0x23, 0],
LocationName.chocolate_island_3_exit_2: [0x23, 1],
LocationName.chocolate_island_3_dragon: [0x23, 2],
+ LocationName.chocolate_island_3_bonus_block: [0x23, 5],
LocationName.chocolate_island_4_exit_1: [0x1D, 0],
LocationName.chocolate_island_4_dragon: [0x1D, 2],
LocationName.chocolate_island_5_exit_1: [0x1C, 0],
@@ -442,6 +560,7 @@ def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1
LocationName.chocolate_fortress: [0x1B, 0],
LocationName.chocolate_reznor: [0x1B, 0],
LocationName.chocolate_castle: [0x1A, 0],
+ LocationName.chocolate_castle_hidden_1up: [0x1A, 4],
LocationName.chocolate_koopaling: [0x1A, 0],
LocationName.sunken_ghost_ship: [0x18, 0],
@@ -449,9 +568,11 @@ def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1
LocationName.valley_of_bowser_1_exit_1: [0x3A, 0],
LocationName.valley_of_bowser_1_dragon: [0x3A, 2],
+ LocationName.valley_of_bowser_1_moon: [0x3A, 3],
LocationName.valley_of_bowser_2_exit_1: [0x39, 0],
LocationName.valley_of_bowser_2_exit_2: [0x39, 1],
LocationName.valley_of_bowser_2_dragon: [0x39, 2],
+ LocationName.valley_of_bowser_2_hidden_1up: [0x39, 4],
LocationName.valley_of_bowser_3_exit_1: [0x37, 0],
LocationName.valley_of_bowser_3_dragon: [0x37, 2],
LocationName.valley_of_bowser_4_exit_1: [0x33, 0],
@@ -464,6 +585,7 @@ def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1
LocationName.valley_castle: [0x34, 0],
LocationName.valley_koopaling: [0x34, 0],
LocationName.valley_castle_dragon: [0x34, 2],
+ LocationName.valley_castle_hidden_1up: [0x34, 4],
LocationName.star_road_1_exit_1: [0x58, 0],
LocationName.star_road_1_exit_2: [0x58, 1],
@@ -479,6 +601,7 @@ def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1
LocationName.special_zone_1_exit_1: [0x4E, 0],
LocationName.special_zone_1_dragon: [0x4E, 2],
+ LocationName.special_zone_1_hidden_1up: [0x4E, 4],
LocationName.special_zone_2_exit_1: [0x4F, 0],
LocationName.special_zone_2_dragon: [0x4F, 2],
LocationName.special_zone_3_exit_1: [0x50, 0],
@@ -493,19 +616,602 @@ def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1
LocationName.special_zone_7_dragon: [0x4A, 2],
LocationName.special_zone_8_exit_1: [0x49, 0],
LocationName.special_zone_8_dragon: [0x49, 2],
+
+ LocationName.vanilla_secret_2_yoshi_block_1: [0x01, 100],
+ LocationName.vanilla_secret_2_green_block_1: [0x01, 101],
+ LocationName.vanilla_secret_2_powerup_block_1: [0x01, 102],
+ LocationName.vanilla_secret_2_powerup_block_2: [0x01, 103],
+ LocationName.vanilla_secret_2_multi_coin_block_1: [0x01, 104],
+ LocationName.vanilla_secret_2_gray_pow_block_1: [0x01, 105],
+ LocationName.vanilla_secret_2_coin_block_1: [0x01, 106],
+ LocationName.vanilla_secret_2_coin_block_2: [0x01, 107],
+ LocationName.vanilla_secret_2_coin_block_3: [0x01, 108],
+ LocationName.vanilla_secret_2_coin_block_4: [0x01, 109],
+ LocationName.vanilla_secret_2_coin_block_5: [0x01, 110],
+ LocationName.vanilla_secret_2_coin_block_6: [0x01, 111],
+ LocationName.vanilla_secret_3_powerup_block_1: [0x02, 112],
+ LocationName.vanilla_secret_3_powerup_block_2: [0x02, 113],
+ LocationName.donut_ghost_house_vine_block_1: [0x04, 114],
+ LocationName.donut_ghost_house_directional_coin_block_1: [0x04, 115],
+ LocationName.donut_ghost_house_life_block_1: [0x04, 116],
+ LocationName.donut_ghost_house_life_block_2: [0x04, 117],
+ LocationName.donut_ghost_house_life_block_3: [0x04, 118],
+ LocationName.donut_ghost_house_life_block_4: [0x04, 119],
+ LocationName.donut_plains_3_green_block_1: [0x05, 120],
+ LocationName.donut_plains_3_coin_block_1: [0x05, 121],
+ LocationName.donut_plains_3_coin_block_2: [0x05, 122],
+ LocationName.donut_plains_3_vine_block_1: [0x05, 123],
+ LocationName.donut_plains_3_powerup_block_1: [0x05, 124],
+ LocationName.donut_plains_3_bonus_block_1: [0x05, 125],
+ LocationName.donut_plains_4_coin_block_1: [0x06, 126],
+ LocationName.donut_plains_4_powerup_block_1: [0x06, 127],
+ LocationName.donut_plains_4_coin_block_2: [0x06, 128],
+ LocationName.donut_plains_4_yoshi_block_1: [0x06, 129],
+ LocationName.donut_plains_castle_yellow_block_1: [0x07, 130],
+ LocationName.donut_plains_castle_coin_block_1: [0x07, 131],
+ LocationName.donut_plains_castle_powerup_block_1: [0x07, 132],
+ LocationName.donut_plains_castle_coin_block_2: [0x07, 133],
+ LocationName.donut_plains_castle_vine_block_1: [0x07, 134],
+ LocationName.donut_plains_castle_invis_life_block_1: [0x07, 135],
+ LocationName.donut_plains_castle_coin_block_3: [0x07, 136],
+ LocationName.donut_plains_castle_coin_block_4: [0x07, 137],
+ LocationName.donut_plains_castle_coin_block_5: [0x07, 138],
+ LocationName.donut_plains_castle_green_block_1: [0x07, 139],
+ LocationName.donut_plains_2_coin_block_1: [0x09, 140],
+ LocationName.donut_plains_2_coin_block_2: [0x09, 141],
+ LocationName.donut_plains_2_coin_block_3: [0x09, 142],
+ LocationName.donut_plains_2_yellow_block_1: [0x09, 143],
+ LocationName.donut_plains_2_powerup_block_1: [0x09, 144],
+ LocationName.donut_plains_2_multi_coin_block_1: [0x09, 145],
+ LocationName.donut_plains_2_flying_block_1: [0x09, 146],
+ LocationName.donut_plains_2_green_block_1: [0x09, 147],
+ LocationName.donut_plains_2_yellow_block_2: [0x09, 148],
+ LocationName.donut_plains_2_vine_block_1: [0x09, 149],
+ LocationName.donut_secret_1_coin_block_1: [0x0A, 150],
+ LocationName.donut_secret_1_coin_block_2: [0x0A, 151],
+ LocationName.donut_secret_1_powerup_block_1: [0x0A, 152],
+ LocationName.donut_secret_1_coin_block_3: [0x0A, 153],
+ LocationName.donut_secret_1_powerup_block_2: [0x0A, 154],
+ LocationName.donut_secret_1_powerup_block_3: [0x0A, 155],
+ LocationName.donut_secret_1_life_block_1: [0x0A, 156],
+ LocationName.donut_secret_1_powerup_block_4: [0x0A, 157],
+ LocationName.donut_secret_1_powerup_block_5: [0x0A, 158],
+ LocationName.donut_secret_1_key_block_1: [0x0A, 159],
+ LocationName.vanilla_fortress_powerup_block_1: [0x0B, 160],
+ LocationName.vanilla_fortress_powerup_block_2: [0x0B, 161],
+ LocationName.vanilla_fortress_yellow_block_1: [0x0B, 162],
+ LocationName.butter_bridge_1_powerup_block_1: [0x0C, 163],
+ LocationName.butter_bridge_1_multi_coin_block_1: [0x0C, 164],
+ LocationName.butter_bridge_1_multi_coin_block_2: [0x0C, 165],
+ LocationName.butter_bridge_1_multi_coin_block_3: [0x0C, 166],
+ LocationName.butter_bridge_1_life_block_1: [0x0C, 167],
+ LocationName.butter_bridge_1_bonus_block_1: [0x0C, 168],
+ LocationName.butter_bridge_2_powerup_block_1: [0x0D, 169],
+ LocationName.butter_bridge_2_green_block_1: [0x0D, 170],
+ LocationName.butter_bridge_2_yoshi_block_1: [0x0D, 171],
+ LocationName.twin_bridges_castle_powerup_block_1: [0x0E, 172],
+ LocationName.cheese_bridge_powerup_block_1: [0x0F, 173],
+ LocationName.cheese_bridge_powerup_block_2: [0x0F, 174],
+ LocationName.cheese_bridge_wings_block_1: [0x0F, 175],
+ LocationName.cheese_bridge_powerup_block_3: [0x0F, 176],
+ LocationName.cookie_mountain_coin_block_1: [0x10, 177],
+ LocationName.cookie_mountain_coin_block_2: [0x10, 178],
+ LocationName.cookie_mountain_coin_block_3: [0x10, 179],
+ LocationName.cookie_mountain_coin_block_4: [0x10, 180],
+ LocationName.cookie_mountain_coin_block_5: [0x10, 181],
+ LocationName.cookie_mountain_coin_block_6: [0x10, 182],
+ LocationName.cookie_mountain_coin_block_7: [0x10, 183],
+ LocationName.cookie_mountain_coin_block_8: [0x10, 184],
+ LocationName.cookie_mountain_coin_block_9: [0x10, 185],
+ LocationName.cookie_mountain_powerup_block_1: [0x10, 186],
+ LocationName.cookie_mountain_life_block_1: [0x10, 187],
+ LocationName.cookie_mountain_vine_block_1: [0x10, 188],
+ LocationName.cookie_mountain_yoshi_block_1: [0x10, 189],
+ LocationName.cookie_mountain_coin_block_10: [0x10, 190],
+ LocationName.cookie_mountain_coin_block_11: [0x10, 191],
+ LocationName.cookie_mountain_powerup_block_2: [0x10, 192],
+ LocationName.cookie_mountain_coin_block_12: [0x10, 193],
+ LocationName.cookie_mountain_coin_block_13: [0x10, 194],
+ LocationName.cookie_mountain_coin_block_14: [0x10, 195],
+ LocationName.cookie_mountain_coin_block_15: [0x10, 196],
+ LocationName.cookie_mountain_coin_block_16: [0x10, 197],
+ LocationName.cookie_mountain_coin_block_17: [0x10, 198],
+ LocationName.cookie_mountain_coin_block_18: [0x10, 199],
+ LocationName.cookie_mountain_coin_block_19: [0x10, 200],
+ LocationName.cookie_mountain_coin_block_20: [0x10, 201],
+ LocationName.cookie_mountain_coin_block_21: [0x10, 202],
+ LocationName.cookie_mountain_coin_block_22: [0x10, 203],
+ LocationName.cookie_mountain_coin_block_23: [0x10, 204],
+ LocationName.cookie_mountain_coin_block_24: [0x10, 205],
+ LocationName.cookie_mountain_coin_block_25: [0x10, 206],
+ LocationName.cookie_mountain_coin_block_26: [0x10, 207],
+ LocationName.cookie_mountain_coin_block_27: [0x10, 208],
+ LocationName.cookie_mountain_coin_block_28: [0x10, 209],
+ LocationName.cookie_mountain_coin_block_29: [0x10, 210],
+ LocationName.cookie_mountain_coin_block_30: [0x10, 211],
+ LocationName.soda_lake_powerup_block_1: [0x11, 212],
+ LocationName.donut_secret_house_powerup_block_1: [0x13, 213],
+ LocationName.donut_secret_house_multi_coin_block_1: [0x13, 214],
+ LocationName.donut_secret_house_life_block_1: [0x13, 215],
+ LocationName.donut_secret_house_vine_block_1: [0x13, 216],
+ LocationName.donut_secret_house_directional_coin_block_1: [0x13, 217],
+ LocationName.donut_plains_1_coin_block_1: [0x15, 218],
+ LocationName.donut_plains_1_coin_block_2: [0x15, 219],
+ LocationName.donut_plains_1_yoshi_block_1: [0x15, 220],
+ LocationName.donut_plains_1_vine_block_1: [0x15, 221],
+ LocationName.donut_plains_1_green_block_1: [0x15, 222],
+ LocationName.donut_plains_1_green_block_2: [0x15, 223],
+ LocationName.donut_plains_1_green_block_3: [0x15, 224],
+ LocationName.donut_plains_1_green_block_4: [0x15, 225],
+ LocationName.donut_plains_1_green_block_5: [0x15, 226],
+ LocationName.donut_plains_1_green_block_6: [0x15, 227],
+ LocationName.donut_plains_1_green_block_7: [0x15, 228],
+ LocationName.donut_plains_1_green_block_8: [0x15, 229],
+ LocationName.donut_plains_1_green_block_9: [0x15, 230],
+ LocationName.donut_plains_1_green_block_10: [0x15, 231],
+ LocationName.donut_plains_1_green_block_11: [0x15, 232],
+ LocationName.donut_plains_1_green_block_12: [0x15, 233],
+ LocationName.donut_plains_1_green_block_13: [0x15, 234],
+ LocationName.donut_plains_1_green_block_14: [0x15, 235],
+ LocationName.donut_plains_1_green_block_15: [0x15, 236],
+ LocationName.donut_plains_1_green_block_16: [0x15, 237],
+ LocationName.donut_plains_1_yellow_block_1: [0x15, 238],
+ LocationName.donut_plains_1_yellow_block_2: [0x15, 239],
+ LocationName.donut_plains_1_yellow_block_3: [0x15, 240],
+ LocationName.sunken_ghost_ship_powerup_block_1: [0x18, 241],
+ LocationName.sunken_ghost_ship_star_block_1: [0x18, 242],
+ LocationName.chocolate_castle_yellow_block_1: [0x1A, 243],
+ LocationName.chocolate_castle_yellow_block_2: [0x1A, 244],
+ LocationName.chocolate_castle_green_block_1: [0x1A, 245],
+ LocationName.chocolate_fortress_powerup_block_1: [0x1B, 246],
+ LocationName.chocolate_fortress_powerup_block_2: [0x1B, 247],
+ LocationName.chocolate_fortress_coin_block_1: [0x1B, 248],
+ LocationName.chocolate_fortress_coin_block_2: [0x1B, 249],
+ LocationName.chocolate_fortress_green_block_1: [0x1B, 250],
+ LocationName.chocolate_island_5_yoshi_block_1: [0x1C, 251],
+ LocationName.chocolate_island_5_powerup_block_1: [0x1C, 252],
+ LocationName.chocolate_island_5_life_block_1: [0x1C, 253],
+ LocationName.chocolate_island_5_yellow_block_1: [0x1C, 254],
+ LocationName.chocolate_island_4_yellow_block_1: [0x1D, 255],
+ LocationName.chocolate_island_4_blue_pow_block_1: [0x1D, 256],
+ LocationName.chocolate_island_4_powerup_block_1: [0x1D, 257],
+ LocationName.forest_fortress_yellow_block_1: [0x1F, 258],
+ LocationName.forest_fortress_powerup_block_1: [0x1F, 259],
+ LocationName.forest_fortress_life_block_1: [0x1F, 260],
+ LocationName.forest_fortress_life_block_2: [0x1F, 261],
+ LocationName.forest_fortress_life_block_3: [0x1F, 262],
+ LocationName.forest_fortress_life_block_4: [0x1F, 263],
+ LocationName.forest_fortress_life_block_5: [0x1F, 264],
+ LocationName.forest_fortress_life_block_6: [0x1F, 265],
+ LocationName.forest_fortress_life_block_7: [0x1F, 266],
+ LocationName.forest_fortress_life_block_8: [0x1F, 267],
+ LocationName.forest_fortress_life_block_9: [0x1F, 268],
+ LocationName.forest_castle_green_block_1: [0x20, 269],
+ LocationName.chocolate_ghost_house_powerup_block_1: [0x21, 270],
+ LocationName.chocolate_ghost_house_powerup_block_2: [0x21, 271],
+ LocationName.chocolate_ghost_house_life_block_1: [0x21, 272],
+ LocationName.chocolate_island_1_flying_block_1: [0x22, 273],
+ LocationName.chocolate_island_1_flying_block_2: [0x22, 274],
+ LocationName.chocolate_island_1_yoshi_block_1: [0x22, 275],
+ LocationName.chocolate_island_1_green_block_1: [0x22, 276],
+ LocationName.chocolate_island_1_life_block_1: [0x22, 277],
+ LocationName.chocolate_island_3_powerup_block_1: [0x23, 278],
+ LocationName.chocolate_island_3_powerup_block_2: [0x23, 279],
+ LocationName.chocolate_island_3_powerup_block_3: [0x23, 280],
+ LocationName.chocolate_island_3_green_block_1: [0x23, 281],
+ LocationName.chocolate_island_3_bonus_block_1: [0x23, 282],
+ LocationName.chocolate_island_3_vine_block_1: [0x23, 283],
+ LocationName.chocolate_island_3_life_block_1: [0x23, 284],
+ LocationName.chocolate_island_3_life_block_2: [0x23, 285],
+ LocationName.chocolate_island_3_life_block_3: [0x23, 286],
+ LocationName.chocolate_island_2_multi_coin_block_1: [0x24, 287],
+ LocationName.chocolate_island_2_invis_coin_block_1: [0x24, 288],
+ LocationName.chocolate_island_2_yoshi_block_1: [0x24, 289],
+ LocationName.chocolate_island_2_coin_block_1: [0x24, 290],
+ LocationName.chocolate_island_2_coin_block_2: [0x24, 291],
+ LocationName.chocolate_island_2_multi_coin_block_2: [0x24, 292],
+ LocationName.chocolate_island_2_powerup_block_1: [0x24, 293],
+ LocationName.chocolate_island_2_blue_pow_block_1: [0x24, 294],
+ LocationName.chocolate_island_2_yellow_block_1: [0x24, 295],
+ LocationName.chocolate_island_2_yellow_block_2: [0x24, 296],
+ LocationName.chocolate_island_2_green_block_1: [0x24, 297],
+ LocationName.chocolate_island_2_green_block_2: [0x24, 298],
+ LocationName.chocolate_island_2_green_block_3: [0x24, 299],
+ LocationName.chocolate_island_2_green_block_4: [0x24, 300],
+ LocationName.chocolate_island_2_green_block_5: [0x24, 301],
+ LocationName.chocolate_island_2_green_block_6: [0x24, 302],
+ LocationName.yoshis_island_castle_coin_block_1: [0x25, 303],
+ LocationName.yoshis_island_castle_coin_block_2: [0x25, 304],
+ LocationName.yoshis_island_castle_powerup_block_1: [0x25, 305],
+ LocationName.yoshis_island_castle_coin_block_3: [0x25, 306],
+ LocationName.yoshis_island_castle_coin_block_4: [0x25, 307],
+ LocationName.yoshis_island_castle_flying_block_1: [0x25, 308],
+ LocationName.yoshis_island_4_yellow_block_1: [0x26, 309],
+ LocationName.yoshis_island_4_powerup_block_1: [0x26, 310],
+ LocationName.yoshis_island_4_multi_coin_block_1: [0x26, 311],
+ LocationName.yoshis_island_4_star_block_1: [0x26, 312],
+ LocationName.yoshis_island_3_yellow_block_1: [0x27, 313],
+ LocationName.yoshis_island_3_yellow_block_2: [0x27, 314],
+ LocationName.yoshis_island_3_yellow_block_3: [0x27, 315],
+ LocationName.yoshis_island_3_yellow_block_4: [0x27, 316],
+ LocationName.yoshis_island_3_yellow_block_5: [0x27, 317],
+ LocationName.yoshis_island_3_yellow_block_6: [0x27, 318],
+ LocationName.yoshis_island_3_yellow_block_7: [0x27, 319],
+ LocationName.yoshis_island_3_yellow_block_8: [0x27, 320],
+ LocationName.yoshis_island_3_yellow_block_9: [0x27, 321],
+ LocationName.yoshis_island_3_coin_block_1: [0x27, 322],
+ LocationName.yoshis_island_3_yoshi_block_1: [0x27, 323],
+ LocationName.yoshis_island_3_coin_block_2: [0x27, 324],
+ LocationName.yoshis_island_3_powerup_block_1: [0x27, 325],
+ LocationName.yoshis_island_3_yellow_block_10: [0x27, 326],
+ LocationName.yoshis_island_3_yellow_block_11: [0x27, 327],
+ LocationName.yoshis_island_3_yellow_block_12: [0x27, 328],
+ LocationName.yoshis_island_3_bonus_block_1: [0x27, 329],
+ LocationName.yoshis_island_1_flying_block_1: [0x29, 330],
+ LocationName.yoshis_island_1_yellow_block_1: [0x29, 331],
+ LocationName.yoshis_island_1_life_block_1: [0x29, 332],
+ LocationName.yoshis_island_1_powerup_block_1: [0x29, 333],
+ LocationName.yoshis_island_2_flying_block_1: [0x2A, 334],
+ LocationName.yoshis_island_2_flying_block_2: [0x2A, 335],
+ LocationName.yoshis_island_2_flying_block_3: [0x2A, 336],
+ LocationName.yoshis_island_2_flying_block_4: [0x2A, 337],
+ LocationName.yoshis_island_2_flying_block_5: [0x2A, 338],
+ LocationName.yoshis_island_2_flying_block_6: [0x2A, 339],
+ LocationName.yoshis_island_2_coin_block_1: [0x2A, 340],
+ LocationName.yoshis_island_2_yellow_block_1: [0x2A, 341],
+ LocationName.yoshis_island_2_coin_block_2: [0x2A, 342],
+ LocationName.yoshis_island_2_coin_block_3: [0x2A, 343],
+ LocationName.yoshis_island_2_yoshi_block_1: [0x2A, 344],
+ LocationName.yoshis_island_2_coin_block_4: [0x2A, 345],
+ LocationName.yoshis_island_2_yoshi_block_2: [0x2A, 346],
+ LocationName.yoshis_island_2_coin_block_5: [0x2A, 347],
+ LocationName.yoshis_island_2_vine_block_1: [0x2A, 348],
+ LocationName.yoshis_island_2_yellow_block_2: [0x2A, 349],
+ LocationName.vanilla_ghost_house_powerup_block_1: [0x2B, 350],
+ LocationName.vanilla_ghost_house_vine_block_1: [0x2B, 351],
+ LocationName.vanilla_ghost_house_powerup_block_2: [0x2B, 352],
+ LocationName.vanilla_ghost_house_multi_coin_block_1: [0x2B, 353],
+ LocationName.vanilla_ghost_house_blue_pow_block_1: [0x2B, 354],
+ LocationName.vanilla_secret_1_coin_block_1: [0x2D, 355],
+ LocationName.vanilla_secret_1_powerup_block_1: [0x2D, 356],
+ LocationName.vanilla_secret_1_multi_coin_block_1: [0x2D, 357],
+ LocationName.vanilla_secret_1_vine_block_1: [0x2D, 358],
+ LocationName.vanilla_secret_1_vine_block_2: [0x2D, 359],
+ LocationName.vanilla_secret_1_coin_block_2: [0x2D, 360],
+ LocationName.vanilla_secret_1_coin_block_3: [0x2D, 361],
+ LocationName.vanilla_secret_1_powerup_block_2: [0x2D, 362],
+ LocationName.vanilla_dome_3_coin_block_1: [0x2E, 363],
+ LocationName.vanilla_dome_3_flying_block_1: [0x2E, 364],
+ LocationName.vanilla_dome_3_flying_block_2: [0x2E, 365],
+ LocationName.vanilla_dome_3_powerup_block_1: [0x2E, 366],
+ LocationName.vanilla_dome_3_flying_block_3: [0x2E, 367],
+ LocationName.vanilla_dome_3_invis_coin_block_1: [0x2E, 368],
+ LocationName.vanilla_dome_3_powerup_block_2: [0x2E, 369],
+ LocationName.vanilla_dome_3_multi_coin_block_1: [0x2E, 370],
+ LocationName.vanilla_dome_3_powerup_block_3: [0x2E, 371],
+ LocationName.vanilla_dome_3_yoshi_block_1: [0x2E, 372],
+ LocationName.vanilla_dome_3_powerup_block_4: [0x2E, 373],
+ LocationName.vanilla_dome_3_pswitch_coin_block_1: [0x2E, 374],
+ LocationName.vanilla_dome_3_pswitch_coin_block_2: [0x2E, 375],
+ LocationName.vanilla_dome_3_pswitch_coin_block_3: [0x2E, 376],
+ LocationName.vanilla_dome_3_pswitch_coin_block_4: [0x2E, 377],
+ LocationName.vanilla_dome_3_pswitch_coin_block_5: [0x2E, 378],
+ LocationName.vanilla_dome_3_pswitch_coin_block_6: [0x2E, 379],
+ LocationName.donut_secret_2_directional_coin_block_1: [0x2F, 380],
+ LocationName.donut_secret_2_vine_block_1: [0x2F, 381],
+ LocationName.donut_secret_2_star_block_1: [0x2F, 382],
+ LocationName.donut_secret_2_powerup_block_1: [0x2F, 383],
+ LocationName.donut_secret_2_star_block_2: [0x2F, 384],
+ LocationName.valley_of_bowser_4_yellow_block_1: [0x33, 385],
+ LocationName.valley_of_bowser_4_powerup_block_1: [0x33, 386],
+ LocationName.valley_of_bowser_4_vine_block_1: [0x33, 387],
+ LocationName.valley_of_bowser_4_yoshi_block_1: [0x33, 388],
+ LocationName.valley_of_bowser_4_life_block_1: [0x33, 389],
+ LocationName.valley_of_bowser_4_powerup_block_2: [0x33, 390],
+ LocationName.valley_castle_yellow_block_1: [0x34, 391],
+ LocationName.valley_castle_yellow_block_2: [0x34, 392],
+ LocationName.valley_castle_green_block_1: [0x34, 393],
+ LocationName.valley_fortress_green_block_1: [0x35, 394],
+ LocationName.valley_fortress_yellow_block_1: [0x35, 395],
+ LocationName.valley_of_bowser_3_powerup_block_1: [0x37, 396],
+ LocationName.valley_of_bowser_3_powerup_block_2: [0x37, 397],
+ LocationName.valley_ghost_house_pswitch_coin_block_1: [0x38, 398],
+ LocationName.valley_ghost_house_multi_coin_block_1: [0x38, 399],
+ LocationName.valley_ghost_house_powerup_block_1: [0x38, 400],
+ LocationName.valley_ghost_house_directional_coin_block_1: [0x38, 401],
+ LocationName.valley_of_bowser_2_powerup_block_1: [0x39, 402],
+ LocationName.valley_of_bowser_2_yellow_block_1: [0x39, 403],
+ LocationName.valley_of_bowser_2_powerup_block_2: [0x39, 404],
+ LocationName.valley_of_bowser_2_wings_block_1: [0x39, 405],
+ LocationName.valley_of_bowser_1_green_block_1: [0x3A, 406],
+ LocationName.valley_of_bowser_1_invis_coin_block_1: [0x3A, 407],
+ LocationName.valley_of_bowser_1_invis_coin_block_2: [0x3A, 408],
+ LocationName.valley_of_bowser_1_invis_coin_block_3: [0x3A, 409],
+ LocationName.valley_of_bowser_1_yellow_block_1: [0x3A, 410],
+ LocationName.valley_of_bowser_1_yellow_block_2: [0x3A, 411],
+ LocationName.valley_of_bowser_1_yellow_block_3: [0x3A, 412],
+ LocationName.valley_of_bowser_1_yellow_block_4: [0x3A, 413],
+ LocationName.valley_of_bowser_1_vine_block_1: [0x3A, 414],
+ LocationName.chocolate_secret_powerup_block_1: [0x3B, 415],
+ LocationName.chocolate_secret_powerup_block_2: [0x3B, 416],
+ LocationName.vanilla_dome_2_coin_block_1: [0x3C, 417],
+ LocationName.vanilla_dome_2_powerup_block_1: [0x3C, 418],
+ LocationName.vanilla_dome_2_coin_block_2: [0x3C, 419],
+ LocationName.vanilla_dome_2_coin_block_3: [0x3C, 420],
+ LocationName.vanilla_dome_2_vine_block_1: [0x3C, 421],
+ LocationName.vanilla_dome_2_invis_life_block_1: [0x3C, 422],
+ LocationName.vanilla_dome_2_coin_block_4: [0x3C, 423],
+ LocationName.vanilla_dome_2_coin_block_5: [0x3C, 424],
+ LocationName.vanilla_dome_2_powerup_block_2: [0x3C, 425],
+ LocationName.vanilla_dome_2_powerup_block_3: [0x3C, 426],
+ LocationName.vanilla_dome_2_powerup_block_4: [0x3C, 427],
+ LocationName.vanilla_dome_2_powerup_block_5: [0x3C, 428],
+ LocationName.vanilla_dome_2_multi_coin_block_1: [0x3C, 429],
+ LocationName.vanilla_dome_2_multi_coin_block_2: [0x3C, 430],
+ LocationName.vanilla_dome_4_powerup_block_1: [0x3D, 431],
+ LocationName.vanilla_dome_4_powerup_block_2: [0x3D, 432],
+ LocationName.vanilla_dome_4_coin_block_1: [0x3D, 433],
+ LocationName.vanilla_dome_4_coin_block_2: [0x3D, 434],
+ LocationName.vanilla_dome_4_coin_block_3: [0x3D, 435],
+ LocationName.vanilla_dome_4_life_block_1: [0x3D, 436],
+ LocationName.vanilla_dome_4_coin_block_4: [0x3D, 437],
+ LocationName.vanilla_dome_4_coin_block_5: [0x3D, 438],
+ LocationName.vanilla_dome_4_coin_block_6: [0x3D, 439],
+ LocationName.vanilla_dome_4_coin_block_7: [0x3D, 440],
+ LocationName.vanilla_dome_4_coin_block_8: [0x3D, 441],
+ LocationName.vanilla_dome_1_flying_block_1: [0x3E, 442],
+ LocationName.vanilla_dome_1_powerup_block_1: [0x3E, 443],
+ LocationName.vanilla_dome_1_powerup_block_2: [0x3E, 444],
+ LocationName.vanilla_dome_1_coin_block_1: [0x3E, 445],
+ LocationName.vanilla_dome_1_life_block_1: [0x3E, 446],
+ LocationName.vanilla_dome_1_powerup_block_3: [0x3E, 447],
+ LocationName.vanilla_dome_1_vine_block_1: [0x3E, 448],
+ LocationName.vanilla_dome_1_star_block_1: [0x3E, 449],
+ LocationName.vanilla_dome_1_powerup_block_4: [0x3E, 450],
+ LocationName.vanilla_dome_1_coin_block_2: [0x3E, 451],
+ LocationName.vanilla_dome_castle_life_block_1: [0x40, 452],
+ LocationName.vanilla_dome_castle_life_block_2: [0x40, 453],
+ LocationName.vanilla_dome_castle_powerup_block_1: [0x40, 454],
+ LocationName.vanilla_dome_castle_life_block_3: [0x40, 455],
+ LocationName.vanilla_dome_castle_green_block_1: [0x40, 456],
+ LocationName.forest_ghost_house_coin_block_1: [0x41, 457],
+ LocationName.forest_ghost_house_powerup_block_1: [0x41, 458],
+ LocationName.forest_ghost_house_flying_block_1: [0x41, 459],
+ LocationName.forest_ghost_house_powerup_block_2: [0x41, 460],
+ LocationName.forest_ghost_house_life_block_1: [0x41, 461],
+ LocationName.forest_of_illusion_1_powerup_block_1: [0x42, 462],
+ LocationName.forest_of_illusion_1_yoshi_block_1: [0x42, 463],
+ LocationName.forest_of_illusion_1_powerup_block_2: [0x42, 464],
+ LocationName.forest_of_illusion_1_key_block_1: [0x42, 465],
+ LocationName.forest_of_illusion_1_life_block_1: [0x42, 466],
+ LocationName.forest_of_illusion_4_multi_coin_block_1: [0x43, 467],
+ LocationName.forest_of_illusion_4_coin_block_1: [0x43, 468],
+ LocationName.forest_of_illusion_4_coin_block_2: [0x43, 469],
+ LocationName.forest_of_illusion_4_coin_block_3: [0x43, 470],
+ LocationName.forest_of_illusion_4_coin_block_4: [0x43, 471],
+ LocationName.forest_of_illusion_4_powerup_block_1: [0x43, 472],
+ LocationName.forest_of_illusion_4_coin_block_5: [0x43, 473],
+ LocationName.forest_of_illusion_4_coin_block_6: [0x43, 474],
+ LocationName.forest_of_illusion_4_coin_block_7: [0x43, 475],
+ LocationName.forest_of_illusion_4_powerup_block_2: [0x43, 476],
+ LocationName.forest_of_illusion_4_coin_block_8: [0x43, 477],
+ LocationName.forest_of_illusion_4_coin_block_9: [0x43, 478],
+ LocationName.forest_of_illusion_4_coin_block_10: [0x43, 479],
+ LocationName.forest_of_illusion_2_green_block_1: [0x44, 480],
+ LocationName.forest_of_illusion_2_powerup_block_1: [0x44, 481],
+ LocationName.forest_of_illusion_2_invis_coin_block_1: [0x44, 482],
+ LocationName.forest_of_illusion_2_invis_coin_block_2: [0x44, 483],
+ LocationName.forest_of_illusion_2_invis_life_block_1: [0x44, 484],
+ LocationName.forest_of_illusion_2_invis_coin_block_3: [0x44, 485],
+ LocationName.forest_of_illusion_2_yellow_block_1: [0x44, 486],
+ LocationName.forest_secret_powerup_block_1: [0x46, 487],
+ LocationName.forest_secret_powerup_block_2: [0x46, 488],
+ LocationName.forest_secret_life_block_1: [0x46, 489],
+ LocationName.forest_of_illusion_3_yoshi_block_1: [0x47, 490],
+ LocationName.forest_of_illusion_3_coin_block_1: [0x47, 491],
+ LocationName.forest_of_illusion_3_multi_coin_block_1: [0x47, 492],
+ LocationName.forest_of_illusion_3_coin_block_2: [0x47, 493],
+ LocationName.forest_of_illusion_3_multi_coin_block_2: [0x47, 494],
+ LocationName.forest_of_illusion_3_coin_block_3: [0x47, 495],
+ LocationName.forest_of_illusion_3_coin_block_4: [0x47, 496],
+ LocationName.forest_of_illusion_3_coin_block_5: [0x47, 497],
+ LocationName.forest_of_illusion_3_coin_block_6: [0x47, 498],
+ LocationName.forest_of_illusion_3_coin_block_7: [0x47, 499],
+ LocationName.forest_of_illusion_3_coin_block_8: [0x47, 500],
+ LocationName.forest_of_illusion_3_coin_block_9: [0x47, 501],
+ LocationName.forest_of_illusion_3_coin_block_10: [0x47, 502],
+ LocationName.forest_of_illusion_3_coin_block_11: [0x47, 503],
+ LocationName.forest_of_illusion_3_coin_block_12: [0x47, 504],
+ LocationName.forest_of_illusion_3_coin_block_13: [0x47, 505],
+ LocationName.forest_of_illusion_3_coin_block_14: [0x47, 506],
+ LocationName.forest_of_illusion_3_coin_block_15: [0x47, 507],
+ LocationName.forest_of_illusion_3_coin_block_16: [0x47, 508],
+ LocationName.forest_of_illusion_3_coin_block_17: [0x47, 509],
+ LocationName.forest_of_illusion_3_coin_block_18: [0x47, 510],
+ LocationName.forest_of_illusion_3_coin_block_19: [0x47, 511],
+ LocationName.forest_of_illusion_3_coin_block_20: [0x47, 512],
+ LocationName.forest_of_illusion_3_coin_block_21: [0x47, 513],
+ LocationName.forest_of_illusion_3_coin_block_22: [0x47, 514],
+ LocationName.forest_of_illusion_3_coin_block_23: [0x47, 515],
+ LocationName.forest_of_illusion_3_coin_block_24: [0x47, 516],
+ LocationName.special_zone_8_yoshi_block_1: [0x49, 517],
+ LocationName.special_zone_8_coin_block_1: [0x49, 518],
+ LocationName.special_zone_8_coin_block_2: [0x49, 519],
+ LocationName.special_zone_8_coin_block_3: [0x49, 520],
+ LocationName.special_zone_8_coin_block_4: [0x49, 521],
+ LocationName.special_zone_8_coin_block_5: [0x49, 522],
+ LocationName.special_zone_8_blue_pow_block_1: [0x49, 523],
+ LocationName.special_zone_8_powerup_block_1: [0x49, 524],
+ LocationName.special_zone_8_star_block_1: [0x49, 525],
+ LocationName.special_zone_8_coin_block_6: [0x49, 526],
+ LocationName.special_zone_8_coin_block_7: [0x49, 527],
+ LocationName.special_zone_8_coin_block_8: [0x49, 528],
+ LocationName.special_zone_8_coin_block_9: [0x49, 529],
+ LocationName.special_zone_8_coin_block_10: [0x49, 530],
+ LocationName.special_zone_8_coin_block_11: [0x49, 531],
+ LocationName.special_zone_8_coin_block_12: [0x49, 532],
+ LocationName.special_zone_8_coin_block_13: [0x49, 533],
+ LocationName.special_zone_8_coin_block_14: [0x49, 534],
+ LocationName.special_zone_8_coin_block_15: [0x49, 535],
+ LocationName.special_zone_8_coin_block_16: [0x49, 536],
+ LocationName.special_zone_8_coin_block_17: [0x49, 537],
+ LocationName.special_zone_8_coin_block_18: [0x49, 538],
+ LocationName.special_zone_8_multi_coin_block_1: [0x49, 539],
+ LocationName.special_zone_8_coin_block_19: [0x49, 540],
+ LocationName.special_zone_8_coin_block_20: [0x49, 541],
+ LocationName.special_zone_8_coin_block_21: [0x49, 542],
+ LocationName.special_zone_8_coin_block_22: [0x49, 543],
+ LocationName.special_zone_8_coin_block_23: [0x49, 544],
+ LocationName.special_zone_8_powerup_block_2: [0x49, 545],
+ LocationName.special_zone_8_flying_block_1: [0x49, 546],
+ LocationName.special_zone_7_powerup_block_1: [0x4A, 547],
+ LocationName.special_zone_7_yoshi_block_1: [0x4A, 548],
+ LocationName.special_zone_7_coin_block_1: [0x4A, 549],
+ LocationName.special_zone_7_powerup_block_2: [0x4A, 550],
+ LocationName.special_zone_7_coin_block_2: [0x4A, 551],
+ LocationName.special_zone_6_powerup_block_1: [0x4B, 552],
+ LocationName.special_zone_6_coin_block_1: [0x4B, 553],
+ LocationName.special_zone_6_coin_block_2: [0x4B, 554],
+ LocationName.special_zone_6_yoshi_block_1: [0x4B, 555],
+ LocationName.special_zone_6_life_block_1: [0x4B, 556],
+ LocationName.special_zone_6_multi_coin_block_1: [0x4B, 557],
+ LocationName.special_zone_6_coin_block_3: [0x4B, 558],
+ LocationName.special_zone_6_coin_block_4: [0x4B, 559],
+ LocationName.special_zone_6_coin_block_5: [0x4B, 560],
+ LocationName.special_zone_6_coin_block_6: [0x4B, 561],
+ LocationName.special_zone_6_coin_block_7: [0x4B, 562],
+ LocationName.special_zone_6_coin_block_8: [0x4B, 563],
+ LocationName.special_zone_6_coin_block_9: [0x4B, 564],
+ LocationName.special_zone_6_coin_block_10: [0x4B, 565],
+ LocationName.special_zone_6_coin_block_11: [0x4B, 566],
+ LocationName.special_zone_6_coin_block_12: [0x4B, 567],
+ LocationName.special_zone_6_coin_block_13: [0x4B, 568],
+ LocationName.special_zone_6_coin_block_14: [0x4B, 569],
+ LocationName.special_zone_6_coin_block_15: [0x4B, 570],
+ LocationName.special_zone_6_coin_block_16: [0x4B, 571],
+ LocationName.special_zone_6_coin_block_17: [0x4B, 572],
+ LocationName.special_zone_6_coin_block_18: [0x4B, 573],
+ LocationName.special_zone_6_coin_block_19: [0x4B, 574],
+ LocationName.special_zone_6_coin_block_20: [0x4B, 575],
+ LocationName.special_zone_6_coin_block_21: [0x4B, 576],
+ LocationName.special_zone_6_coin_block_22: [0x4B, 577],
+ LocationName.special_zone_6_coin_block_23: [0x4B, 578],
+ LocationName.special_zone_6_coin_block_24: [0x4B, 579],
+ LocationName.special_zone_6_coin_block_25: [0x4B, 580],
+ LocationName.special_zone_6_coin_block_26: [0x4B, 581],
+ LocationName.special_zone_6_coin_block_27: [0x4B, 582],
+ LocationName.special_zone_6_coin_block_28: [0x4B, 583],
+ LocationName.special_zone_6_powerup_block_2: [0x4B, 584],
+ LocationName.special_zone_6_coin_block_29: [0x4B, 585],
+ LocationName.special_zone_6_coin_block_30: [0x4B, 586],
+ LocationName.special_zone_6_coin_block_31: [0x4B, 587],
+ LocationName.special_zone_6_coin_block_32: [0x4B, 588],
+ LocationName.special_zone_6_coin_block_33: [0x4B, 589],
+ LocationName.special_zone_5_yoshi_block_1: [0x4C, 590],
+ LocationName.special_zone_1_vine_block_1: [0x4E, 591],
+ LocationName.special_zone_1_vine_block_2: [0x4E, 592],
+ LocationName.special_zone_1_vine_block_3: [0x4E, 593],
+ LocationName.special_zone_1_vine_block_4: [0x4E, 594],
+ LocationName.special_zone_1_life_block_1: [0x4E, 595],
+ LocationName.special_zone_1_vine_block_5: [0x4E, 596],
+ LocationName.special_zone_1_blue_pow_block_1: [0x4E, 597],
+ LocationName.special_zone_1_vine_block_6: [0x4E, 598],
+ LocationName.special_zone_1_powerup_block_1: [0x4E, 599],
+ LocationName.special_zone_1_pswitch_coin_block_1: [0x4E, 600],
+ LocationName.special_zone_1_pswitch_coin_block_2: [0x4E, 601],
+ LocationName.special_zone_1_pswitch_coin_block_3: [0x4E, 602],
+ LocationName.special_zone_1_pswitch_coin_block_4: [0x4E, 603],
+ LocationName.special_zone_1_pswitch_coin_block_5: [0x4E, 604],
+ LocationName.special_zone_1_pswitch_coin_block_6: [0x4E, 605],
+ LocationName.special_zone_1_pswitch_coin_block_7: [0x4E, 606],
+ LocationName.special_zone_1_pswitch_coin_block_8: [0x4E, 607],
+ LocationName.special_zone_1_pswitch_coin_block_9: [0x4E, 608],
+ LocationName.special_zone_1_pswitch_coin_block_10: [0x4E, 609],
+ LocationName.special_zone_1_pswitch_coin_block_11: [0x4E, 610],
+ LocationName.special_zone_1_pswitch_coin_block_12: [0x4E, 611],
+ LocationName.special_zone_1_pswitch_coin_block_13: [0x4E, 612],
+ LocationName.special_zone_2_powerup_block_1: [0x4F, 613],
+ LocationName.special_zone_2_coin_block_1: [0x4F, 614],
+ LocationName.special_zone_2_coin_block_2: [0x4F, 615],
+ LocationName.special_zone_2_powerup_block_2: [0x4F, 616],
+ LocationName.special_zone_2_coin_block_3: [0x4F, 617],
+ LocationName.special_zone_2_coin_block_4: [0x4F, 618],
+ LocationName.special_zone_2_powerup_block_3: [0x4F, 619],
+ LocationName.special_zone_2_multi_coin_block_1: [0x4F, 620],
+ LocationName.special_zone_2_coin_block_5: [0x4F, 621],
+ LocationName.special_zone_2_coin_block_6: [0x4F, 622],
+ LocationName.special_zone_3_powerup_block_1: [0x50, 623],
+ LocationName.special_zone_3_yoshi_block_1: [0x50, 624],
+ LocationName.special_zone_3_wings_block_1: [0x50, 625],
+ LocationName.special_zone_4_powerup_block_1: [0x51, 626],
+ LocationName.special_zone_4_star_block_1: [0x51, 627],
+ LocationName.star_road_2_star_block_1: [0x54, 628],
+ LocationName.star_road_3_key_block_1: [0x56, 629],
+ LocationName.star_road_4_powerup_block_1: [0x59, 630],
+ LocationName.star_road_4_green_block_1: [0x59, 631],
+ LocationName.star_road_4_green_block_2: [0x59, 632],
+ LocationName.star_road_4_green_block_3: [0x59, 633],
+ LocationName.star_road_4_green_block_4: [0x59, 634],
+ LocationName.star_road_4_green_block_5: [0x59, 635],
+ LocationName.star_road_4_green_block_6: [0x59, 636],
+ LocationName.star_road_4_green_block_7: [0x59, 637],
+ LocationName.star_road_4_key_block_1: [0x59, 638],
+ LocationName.star_road_5_directional_coin_block_1: [0x5A, 639],
+ LocationName.star_road_5_life_block_1: [0x5A, 640],
+ LocationName.star_road_5_vine_block_1: [0x5A, 641],
+ LocationName.star_road_5_yellow_block_1: [0x5A, 642],
+ LocationName.star_road_5_yellow_block_2: [0x5A, 643],
+ LocationName.star_road_5_yellow_block_3: [0x5A, 644],
+ LocationName.star_road_5_yellow_block_4: [0x5A, 645],
+ LocationName.star_road_5_yellow_block_5: [0x5A, 646],
+ LocationName.star_road_5_yellow_block_6: [0x5A, 647],
+ LocationName.star_road_5_yellow_block_7: [0x5A, 648],
+ LocationName.star_road_5_yellow_block_8: [0x5A, 649],
+ LocationName.star_road_5_yellow_block_9: [0x5A, 650],
+ LocationName.star_road_5_yellow_block_10: [0x5A, 651],
+ LocationName.star_road_5_yellow_block_11: [0x5A, 652],
+ LocationName.star_road_5_yellow_block_12: [0x5A, 653],
+ LocationName.star_road_5_yellow_block_13: [0x5A, 654],
+ LocationName.star_road_5_yellow_block_14: [0x5A, 655],
+ LocationName.star_road_5_yellow_block_15: [0x5A, 656],
+ LocationName.star_road_5_yellow_block_16: [0x5A, 657],
+ LocationName.star_road_5_yellow_block_17: [0x5A, 658],
+ LocationName.star_road_5_yellow_block_18: [0x5A, 659],
+ LocationName.star_road_5_yellow_block_19: [0x5A, 660],
+ LocationName.star_road_5_yellow_block_20: [0x5A, 661],
+ LocationName.star_road_5_green_block_1: [0x5A, 662],
+ LocationName.star_road_5_green_block_2: [0x5A, 663],
+ LocationName.star_road_5_green_block_3: [0x5A, 664],
+ LocationName.star_road_5_green_block_4: [0x5A, 665],
+ LocationName.star_road_5_green_block_5: [0x5A, 666],
+ LocationName.star_road_5_green_block_6: [0x5A, 667],
+ LocationName.star_road_5_green_block_7: [0x5A, 668],
+ LocationName.star_road_5_green_block_8: [0x5A, 669],
+ LocationName.star_road_5_green_block_9: [0x5A, 670],
+ LocationName.star_road_5_green_block_10: [0x5A, 671],
+ LocationName.star_road_5_green_block_11: [0x5A, 672],
+ LocationName.star_road_5_green_block_12: [0x5A, 673],
+ LocationName.star_road_5_green_block_13: [0x5A, 674],
+ LocationName.star_road_5_green_block_14: [0x5A, 675],
+ LocationName.star_road_5_green_block_15: [0x5A, 676],
+ LocationName.star_road_5_green_block_16: [0x5A, 677],
+ LocationName.star_road_5_green_block_17: [0x5A, 678],
+ LocationName.star_road_5_green_block_18: [0x5A, 679],
+ LocationName.star_road_5_green_block_19: [0x5A, 680],
+ LocationName.star_road_5_green_block_20: [0x5A, 681]
}
-def generate_level_list(world, player):
+def generate_level_list(world: World):
- if not world.level_shuffle[player]:
+ if not world.options.level_shuffle:
out_level_list = full_level_list.copy()
out_level_list[0x00] = 0x03
out_level_list[0x11] = 0x28
- if world.bowser_castle_doors[player] == "fast":
+ if world.options.bowser_castle_doors == "fast":
out_level_list[0x41] = 0x82
out_level_list[0x42] = 0x32
- elif world.bowser_castle_doors[player] == "slow":
+ elif world.options.bowser_castle_doors == "slow":
out_level_list[0x41] = 0x31
out_level_list[0x42] = 0x81
@@ -552,7 +1258,7 @@ def generate_level_list(world, player):
shuffled_level_list.append(0x16)
single_levels_copy = (easy_single_levels_copy.copy() + hard_single_levels_copy.copy())
- if not world.exclude_special_zone[player]:
+ if not world.options.exclude_special_zone:
single_levels_copy.extend(special_zone_levels_copy)
world.random.shuffle(single_levels_copy)
@@ -619,10 +1325,10 @@ def generate_level_list(world, player):
shuffled_level_list.append(castle_fortress_levels_copy.pop(0))
# Front/Back Door
- if world.bowser_castle_doors[player] == "fast":
+ if world.options.bowser_castle_doors == "fast":
shuffled_level_list.append(0x82)
shuffled_level_list.append(0x32)
- elif world.bowser_castle_doors[player] == "slow":
+ elif world.options.bowser_castle_doors == "slow":
shuffled_level_list.append(0x31)
shuffled_level_list.append(0x81)
else:
@@ -646,7 +1352,7 @@ def generate_level_list(world, player):
# Special Zone
shuffled_level_list.append(0x4D)
- if not world.exclude_special_zone[player]:
+ if not world.options.exclude_special_zone:
shuffled_level_list.append(single_levels_copy.pop(0))
shuffled_level_list.append(single_levels_copy.pop(0))
shuffled_level_list.append(single_levels_copy.pop(0))
diff --git a/worlds/smw/Locations.py b/worlds/smw/Locations.py
index a8b7f7a4ec2c..47e821fc61ff 100644
--- a/worlds/smw/Locations.py
+++ b/worlds/smw/Locations.py
@@ -1,9 +1,9 @@
import typing
from BaseClasses import Location
+from worlds.AutoWorld import World
from .Names import LocationName
-
class SMWLocation(Location):
game: str = "Super Mario World"
@@ -197,6 +197,624 @@ def __init__(self, player: int, name: str = '', address: int = None, parent=None
LocationName.special_zone_8_dragon: 0xBC0162,
}
+moon_location_table = {
+ LocationName.yoshis_island_1_moon: 0xBC0300,
+ LocationName.donut_plains_4_moon: 0xBC030B,
+ LocationName.vanilla_dome_3_moon: 0xBC0318,
+ LocationName.cheese_bridge_moon: 0xBC0325,
+ LocationName.forest_ghost_house_moon: 0xBC0332,
+ LocationName.chocolate_island_1_moon: 0xBC0338,
+ LocationName.valley_of_bowser_1_moon: 0xBC0345
+}
+
+hidden_1ups_location_table = {
+ LocationName.yoshis_island_4_hidden_1up: 0xBC0403,
+ LocationName.donut_plains_1_hidden_1up: 0xBC0406,
+ LocationName.donut_plains_4_hidden_1up: 0xBC040B,
+ LocationName.donut_plains_castle_hidden_1up: 0xBC0412,
+ LocationName.vanilla_dome_4_hidden_1up: 0xBC0419,
+ LocationName.vanilla_ghost_house_hidden_1up: 0xBC041E,
+ LocationName.vanilla_fortress_hidden_1up: 0xBC0420,
+ LocationName.cookie_mountain_hidden_1up: 0xBC0427,
+ LocationName.forest_of_illusion_3_hidden_1up: 0xBC042E,
+ LocationName.chocolate_island_2_hidden_1up: 0xBC0439,
+ LocationName.chocolate_castle_hidden_1up: 0xBC0443,
+ LocationName.valley_of_bowser_2_hidden_1up: 0xBC0446,
+ LocationName.valley_castle_hidden_1up: 0xBC044F,
+ LocationName.special_zone_1_hidden_1up: 0xBC045B
+}
+bonus_block_location_table = {
+ LocationName.yoshis_island_3_bonus_block: 0xBC0502,
+ LocationName.donut_plains_3_bonus_block: 0xBC050A,
+ LocationName.butter_bridge_1_bonus_block: 0xBC0523,
+ LocationName.chocolate_island_3_bonus_block: 0xBC053B
+}
+
+blocksanity_location_table = {
+ LocationName.vanilla_secret_2_yoshi_block_1: 0xBC0600,
+ LocationName.vanilla_secret_2_green_block_1: 0xBC0601,
+ LocationName.vanilla_secret_2_powerup_block_1: 0xBC0602,
+ LocationName.vanilla_secret_2_powerup_block_2: 0xBC0603,
+ LocationName.vanilla_secret_2_multi_coin_block_1: 0xBC0604,
+ LocationName.vanilla_secret_2_gray_pow_block_1: 0xBC0605,
+ LocationName.vanilla_secret_2_coin_block_1: 0xBC0606,
+ LocationName.vanilla_secret_2_coin_block_2: 0xBC0607,
+ LocationName.vanilla_secret_2_coin_block_3: 0xBC0608,
+ LocationName.vanilla_secret_2_coin_block_4: 0xBC0609,
+ LocationName.vanilla_secret_2_coin_block_5: 0xBC060A,
+ LocationName.vanilla_secret_2_coin_block_6: 0xBC060B,
+ LocationName.vanilla_secret_3_powerup_block_1: 0xBC060C,
+ LocationName.vanilla_secret_3_powerup_block_2: 0xBC060D,
+ LocationName.donut_ghost_house_vine_block_1: 0xBC060E,
+ LocationName.donut_ghost_house_directional_coin_block_1: 0xBC060F,
+ LocationName.donut_ghost_house_life_block_1: 0xBC0610,
+ LocationName.donut_ghost_house_life_block_2: 0xBC0611,
+ LocationName.donut_ghost_house_life_block_3: 0xBC0612,
+ LocationName.donut_ghost_house_life_block_4: 0xBC0613,
+ LocationName.donut_plains_3_green_block_1: 0xBC0614,
+ LocationName.donut_plains_3_coin_block_1: 0xBC0615,
+ LocationName.donut_plains_3_coin_block_2: 0xBC0616,
+ LocationName.donut_plains_3_vine_block_1: 0xBC0617,
+ LocationName.donut_plains_3_powerup_block_1: 0xBC0618,
+ LocationName.donut_plains_3_bonus_block_1: 0xBC0619,
+ LocationName.donut_plains_4_coin_block_1: 0xBC061A,
+ LocationName.donut_plains_4_powerup_block_1: 0xBC061B,
+ LocationName.donut_plains_4_coin_block_2: 0xBC061C,
+ LocationName.donut_plains_4_yoshi_block_1: 0xBC061D,
+ LocationName.donut_plains_castle_yellow_block_1: 0xBC061E,
+ LocationName.donut_plains_castle_coin_block_1: 0xBC061F,
+ LocationName.donut_plains_castle_powerup_block_1: 0xBC0620,
+ LocationName.donut_plains_castle_coin_block_2: 0xBC0621,
+ LocationName.donut_plains_castle_vine_block_1: 0xBC0622,
+ LocationName.donut_plains_castle_invis_life_block_1: 0xBC0623,
+ LocationName.donut_plains_castle_coin_block_3: 0xBC0624,
+ LocationName.donut_plains_castle_coin_block_4: 0xBC0625,
+ LocationName.donut_plains_castle_coin_block_5: 0xBC0626,
+ LocationName.donut_plains_castle_green_block_1: 0xBC0627,
+ LocationName.donut_plains_2_coin_block_1: 0xBC0628,
+ LocationName.donut_plains_2_coin_block_2: 0xBC0629,
+ LocationName.donut_plains_2_coin_block_3: 0xBC062A,
+ LocationName.donut_plains_2_yellow_block_1: 0xBC062B,
+ LocationName.donut_plains_2_powerup_block_1: 0xBC062C,
+ LocationName.donut_plains_2_multi_coin_block_1: 0xBC062D,
+ LocationName.donut_plains_2_flying_block_1: 0xBC062E,
+ LocationName.donut_plains_2_green_block_1: 0xBC062F,
+ LocationName.donut_plains_2_yellow_block_2: 0xBC0630,
+ LocationName.donut_plains_2_vine_block_1: 0xBC0631,
+ LocationName.donut_secret_1_coin_block_1: 0xBC0632,
+ LocationName.donut_secret_1_coin_block_2: 0xBC0633,
+ LocationName.donut_secret_1_powerup_block_1: 0xBC0634,
+ LocationName.donut_secret_1_coin_block_3: 0xBC0635,
+ LocationName.donut_secret_1_powerup_block_2: 0xBC0636,
+ LocationName.donut_secret_1_powerup_block_3: 0xBC0637,
+ LocationName.donut_secret_1_life_block_1: 0xBC0638,
+ LocationName.donut_secret_1_powerup_block_4: 0xBC0639,
+ LocationName.donut_secret_1_powerup_block_5: 0xBC063A,
+ LocationName.donut_secret_1_key_block_1: 0xBC063B,
+ LocationName.vanilla_fortress_powerup_block_1: 0xBC063C,
+ LocationName.vanilla_fortress_powerup_block_2: 0xBC063D,
+ LocationName.vanilla_fortress_yellow_block_1: 0xBC063E,
+ LocationName.butter_bridge_1_powerup_block_1: 0xBC063F,
+ LocationName.butter_bridge_1_multi_coin_block_1: 0xBC0640,
+ LocationName.butter_bridge_1_multi_coin_block_2: 0xBC0641,
+ LocationName.butter_bridge_1_multi_coin_block_3: 0xBC0642,
+ LocationName.butter_bridge_1_life_block_1: 0xBC0643,
+ LocationName.butter_bridge_1_bonus_block_1: 0xBC0644,
+ LocationName.butter_bridge_2_powerup_block_1: 0xBC0645,
+ LocationName.butter_bridge_2_green_block_1: 0xBC0646,
+ LocationName.butter_bridge_2_yoshi_block_1: 0xBC0647,
+ LocationName.twin_bridges_castle_powerup_block_1: 0xBC0648,
+ LocationName.cheese_bridge_powerup_block_1: 0xBC0649,
+ LocationName.cheese_bridge_powerup_block_2: 0xBC064A,
+ LocationName.cheese_bridge_wings_block_1: 0xBC064B,
+ LocationName.cheese_bridge_powerup_block_3: 0xBC064C,
+ LocationName.cookie_mountain_coin_block_1: 0xBC064D,
+ LocationName.cookie_mountain_coin_block_2: 0xBC064E,
+ LocationName.cookie_mountain_coin_block_3: 0xBC064F,
+ LocationName.cookie_mountain_coin_block_4: 0xBC0650,
+ LocationName.cookie_mountain_coin_block_5: 0xBC0651,
+ LocationName.cookie_mountain_coin_block_6: 0xBC0652,
+ LocationName.cookie_mountain_coin_block_7: 0xBC0653,
+ LocationName.cookie_mountain_coin_block_8: 0xBC0654,
+ LocationName.cookie_mountain_coin_block_9: 0xBC0655,
+ LocationName.cookie_mountain_powerup_block_1: 0xBC0656,
+ LocationName.cookie_mountain_life_block_1: 0xBC0657,
+ LocationName.cookie_mountain_vine_block_1: 0xBC0658,
+ LocationName.cookie_mountain_yoshi_block_1: 0xBC0659,
+ LocationName.cookie_mountain_coin_block_10: 0xBC065A,
+ LocationName.cookie_mountain_coin_block_11: 0xBC065B,
+ LocationName.cookie_mountain_powerup_block_2: 0xBC065C,
+ LocationName.cookie_mountain_coin_block_12: 0xBC065D,
+ LocationName.cookie_mountain_coin_block_13: 0xBC065E,
+ LocationName.cookie_mountain_coin_block_14: 0xBC065F,
+ LocationName.cookie_mountain_coin_block_15: 0xBC0660,
+ LocationName.cookie_mountain_coin_block_16: 0xBC0661,
+ LocationName.cookie_mountain_coin_block_17: 0xBC0662,
+ LocationName.cookie_mountain_coin_block_18: 0xBC0663,
+ LocationName.cookie_mountain_coin_block_19: 0xBC0664,
+ LocationName.cookie_mountain_coin_block_20: 0xBC0665,
+ LocationName.cookie_mountain_coin_block_21: 0xBC0666,
+ LocationName.cookie_mountain_coin_block_22: 0xBC0667,
+ LocationName.cookie_mountain_coin_block_23: 0xBC0668,
+ LocationName.cookie_mountain_coin_block_24: 0xBC0669,
+ LocationName.cookie_mountain_coin_block_25: 0xBC066A,
+ LocationName.cookie_mountain_coin_block_26: 0xBC066B,
+ LocationName.cookie_mountain_coin_block_27: 0xBC066C,
+ LocationName.cookie_mountain_coin_block_28: 0xBC066D,
+ LocationName.cookie_mountain_coin_block_29: 0xBC066E,
+ LocationName.cookie_mountain_coin_block_30: 0xBC066F,
+ LocationName.soda_lake_powerup_block_1: 0xBC0670,
+ LocationName.donut_secret_house_powerup_block_1: 0xBC0671,
+ LocationName.donut_secret_house_multi_coin_block_1: 0xBC0672,
+ LocationName.donut_secret_house_life_block_1: 0xBC0673,
+ LocationName.donut_secret_house_vine_block_1: 0xBC0674,
+ LocationName.donut_secret_house_directional_coin_block_1: 0xBC0675,
+ LocationName.donut_plains_1_coin_block_1: 0xBC0676,
+ LocationName.donut_plains_1_coin_block_2: 0xBC0677,
+ LocationName.donut_plains_1_yoshi_block_1: 0xBC0678,
+ LocationName.donut_plains_1_vine_block_1: 0xBC0679,
+ LocationName.donut_plains_1_green_block_1: 0xBC067A,
+ LocationName.donut_plains_1_green_block_2: 0xBC067B,
+ LocationName.donut_plains_1_green_block_3: 0xBC067C,
+ LocationName.donut_plains_1_green_block_4: 0xBC067D,
+ LocationName.donut_plains_1_green_block_5: 0xBC067E,
+ LocationName.donut_plains_1_green_block_6: 0xBC067F,
+ LocationName.donut_plains_1_green_block_7: 0xBC0680,
+ LocationName.donut_plains_1_green_block_8: 0xBC0681,
+ LocationName.donut_plains_1_green_block_9: 0xBC0682,
+ LocationName.donut_plains_1_green_block_10: 0xBC0683,
+ LocationName.donut_plains_1_green_block_11: 0xBC0684,
+ LocationName.donut_plains_1_green_block_12: 0xBC0685,
+ LocationName.donut_plains_1_green_block_13: 0xBC0686,
+ LocationName.donut_plains_1_green_block_14: 0xBC0687,
+ LocationName.donut_plains_1_green_block_15: 0xBC0688,
+ LocationName.donut_plains_1_green_block_16: 0xBC0689,
+ LocationName.donut_plains_1_yellow_block_1: 0xBC068A,
+ LocationName.donut_plains_1_yellow_block_2: 0xBC068B,
+ LocationName.donut_plains_1_yellow_block_3: 0xBC068C,
+ LocationName.sunken_ghost_ship_powerup_block_1: 0xBC068D,
+ LocationName.sunken_ghost_ship_star_block_1: 0xBC068E,
+ LocationName.chocolate_castle_yellow_block_1: 0xBC068F,
+ LocationName.chocolate_castle_yellow_block_2: 0xBC0690,
+ LocationName.chocolate_castle_green_block_1: 0xBC0691,
+ LocationName.chocolate_fortress_powerup_block_1: 0xBC0692,
+ LocationName.chocolate_fortress_powerup_block_2: 0xBC0693,
+ LocationName.chocolate_fortress_coin_block_1: 0xBC0694,
+ LocationName.chocolate_fortress_coin_block_2: 0xBC0695,
+ LocationName.chocolate_fortress_green_block_1: 0xBC0696,
+ LocationName.chocolate_island_5_yoshi_block_1: 0xBC0697,
+ LocationName.chocolate_island_5_powerup_block_1: 0xBC0698,
+ LocationName.chocolate_island_5_life_block_1: 0xBC0699,
+ LocationName.chocolate_island_5_yellow_block_1: 0xBC069A,
+ LocationName.chocolate_island_4_yellow_block_1: 0xBC069B,
+ LocationName.chocolate_island_4_blue_pow_block_1: 0xBC069C,
+ LocationName.chocolate_island_4_powerup_block_1: 0xBC069D,
+ LocationName.forest_fortress_yellow_block_1: 0xBC069E,
+ LocationName.forest_fortress_powerup_block_1: 0xBC069F,
+ LocationName.forest_fortress_life_block_1: 0xBC06A0,
+ LocationName.forest_fortress_life_block_2: 0xBC06A1,
+ LocationName.forest_fortress_life_block_3: 0xBC06A2,
+ LocationName.forest_fortress_life_block_4: 0xBC06A3,
+ LocationName.forest_fortress_life_block_5: 0xBC06A4,
+ LocationName.forest_fortress_life_block_6: 0xBC06A5,
+ LocationName.forest_fortress_life_block_7: 0xBC06A6,
+ LocationName.forest_fortress_life_block_8: 0xBC06A7,
+ LocationName.forest_fortress_life_block_9: 0xBC06A8,
+ LocationName.forest_castle_green_block_1: 0xBC06A9,
+ LocationName.chocolate_ghost_house_powerup_block_1: 0xBC06AA,
+ LocationName.chocolate_ghost_house_powerup_block_2: 0xBC06AB,
+ LocationName.chocolate_ghost_house_life_block_1: 0xBC06AC,
+ LocationName.chocolate_island_1_flying_block_1: 0xBC06AD,
+ LocationName.chocolate_island_1_flying_block_2: 0xBC06AE,
+ LocationName.chocolate_island_1_yoshi_block_1: 0xBC06AF,
+ LocationName.chocolate_island_1_green_block_1: 0xBC06B0,
+ LocationName.chocolate_island_1_life_block_1: 0xBC06B1,
+ LocationName.chocolate_island_3_powerup_block_1: 0xBC06B2,
+ LocationName.chocolate_island_3_powerup_block_2: 0xBC06B3,
+ LocationName.chocolate_island_3_powerup_block_3: 0xBC06B4,
+ LocationName.chocolate_island_3_green_block_1: 0xBC06B5,
+ LocationName.chocolate_island_3_bonus_block_1: 0xBC06B6,
+ LocationName.chocolate_island_3_vine_block_1: 0xBC06B7,
+ LocationName.chocolate_island_3_life_block_1: 0xBC06B8,
+ LocationName.chocolate_island_3_life_block_2: 0xBC06B9,
+ LocationName.chocolate_island_3_life_block_3: 0xBC06BA,
+ LocationName.chocolate_island_2_multi_coin_block_1: 0xBC06BB,
+ LocationName.chocolate_island_2_invis_coin_block_1: 0xBC06BC,
+ LocationName.chocolate_island_2_yoshi_block_1: 0xBC06BD,
+ LocationName.chocolate_island_2_coin_block_1: 0xBC06BE,
+ LocationName.chocolate_island_2_coin_block_2: 0xBC06BF,
+ LocationName.chocolate_island_2_multi_coin_block_2: 0xBC06C0,
+ LocationName.chocolate_island_2_powerup_block_1: 0xBC06C1,
+ LocationName.chocolate_island_2_blue_pow_block_1: 0xBC06C2,
+ LocationName.chocolate_island_2_yellow_block_1: 0xBC06C3,
+ LocationName.chocolate_island_2_yellow_block_2: 0xBC06C4,
+ LocationName.chocolate_island_2_green_block_1: 0xBC06C5,
+ LocationName.chocolate_island_2_green_block_2: 0xBC06C6,
+ LocationName.chocolate_island_2_green_block_3: 0xBC06C7,
+ LocationName.chocolate_island_2_green_block_4: 0xBC06C8,
+ LocationName.chocolate_island_2_green_block_5: 0xBC06C9,
+ LocationName.chocolate_island_2_green_block_6: 0xBC06CA,
+ LocationName.yoshis_island_castle_coin_block_1: 0xBC06CB,
+ LocationName.yoshis_island_castle_coin_block_2: 0xBC06CC,
+ LocationName.yoshis_island_castle_powerup_block_1: 0xBC06CD,
+ LocationName.yoshis_island_castle_coin_block_3: 0xBC06CE,
+ LocationName.yoshis_island_castle_coin_block_4: 0xBC06CF,
+ LocationName.yoshis_island_castle_flying_block_1: 0xBC06D0,
+ LocationName.yoshis_island_4_yellow_block_1: 0xBC06D1,
+ LocationName.yoshis_island_4_powerup_block_1: 0xBC06D2,
+ LocationName.yoshis_island_4_multi_coin_block_1: 0xBC06D3,
+ LocationName.yoshis_island_4_star_block_1: 0xBC06D4,
+ LocationName.yoshis_island_3_yellow_block_1: 0xBC06D5,
+ LocationName.yoshis_island_3_yellow_block_2: 0xBC06D6,
+ LocationName.yoshis_island_3_yellow_block_3: 0xBC06D7,
+ LocationName.yoshis_island_3_yellow_block_4: 0xBC06D8,
+ LocationName.yoshis_island_3_yellow_block_5: 0xBC06D9,
+ LocationName.yoshis_island_3_yellow_block_6: 0xBC06DA,
+ LocationName.yoshis_island_3_yellow_block_7: 0xBC06DB,
+ LocationName.yoshis_island_3_yellow_block_8: 0xBC06DC,
+ LocationName.yoshis_island_3_yellow_block_9: 0xBC06DD,
+ LocationName.yoshis_island_3_coin_block_1: 0xBC06DE,
+ LocationName.yoshis_island_3_yoshi_block_1: 0xBC06DF,
+ LocationName.yoshis_island_3_coin_block_2: 0xBC06E0,
+ LocationName.yoshis_island_3_powerup_block_1: 0xBC06E1,
+ LocationName.yoshis_island_3_yellow_block_10: 0xBC06E2,
+ LocationName.yoshis_island_3_yellow_block_11: 0xBC06E3,
+ LocationName.yoshis_island_3_yellow_block_12: 0xBC06E4,
+ LocationName.yoshis_island_3_bonus_block_1: 0xBC06E5,
+ LocationName.yoshis_island_1_flying_block_1: 0xBC06E6,
+ LocationName.yoshis_island_1_yellow_block_1: 0xBC06E7,
+ LocationName.yoshis_island_1_life_block_1: 0xBC06E8,
+ LocationName.yoshis_island_1_powerup_block_1: 0xBC06E9,
+ LocationName.yoshis_island_2_flying_block_1: 0xBC06EA,
+ LocationName.yoshis_island_2_flying_block_2: 0xBC06EB,
+ LocationName.yoshis_island_2_flying_block_3: 0xBC06EC,
+ LocationName.yoshis_island_2_flying_block_4: 0xBC06ED,
+ LocationName.yoshis_island_2_flying_block_5: 0xBC06EE,
+ LocationName.yoshis_island_2_flying_block_6: 0xBC06EF,
+ LocationName.yoshis_island_2_coin_block_1: 0xBC06F0,
+ LocationName.yoshis_island_2_yellow_block_1: 0xBC06F1,
+ LocationName.yoshis_island_2_coin_block_2: 0xBC06F2,
+ LocationName.yoshis_island_2_coin_block_3: 0xBC06F3,
+ LocationName.yoshis_island_2_yoshi_block_1: 0xBC06F4,
+ LocationName.yoshis_island_2_coin_block_4: 0xBC06F5,
+ LocationName.yoshis_island_2_yoshi_block_2: 0xBC06F6,
+ LocationName.yoshis_island_2_coin_block_5: 0xBC06F7,
+ LocationName.yoshis_island_2_vine_block_1: 0xBC06F8,
+ LocationName.yoshis_island_2_yellow_block_2: 0xBC06F9,
+ LocationName.vanilla_ghost_house_powerup_block_1: 0xBC06FA,
+ LocationName.vanilla_ghost_house_vine_block_1: 0xBC06FB,
+ LocationName.vanilla_ghost_house_powerup_block_2: 0xBC06FC,
+ LocationName.vanilla_ghost_house_multi_coin_block_1: 0xBC06FD,
+ LocationName.vanilla_ghost_house_blue_pow_block_1: 0xBC06FE,
+ LocationName.vanilla_secret_1_coin_block_1: 0xBC06FF,
+ LocationName.vanilla_secret_1_powerup_block_1: 0xBC0700,
+ LocationName.vanilla_secret_1_multi_coin_block_1: 0xBC0701,
+ LocationName.vanilla_secret_1_vine_block_1: 0xBC0702,
+ LocationName.vanilla_secret_1_vine_block_2: 0xBC0703,
+ LocationName.vanilla_secret_1_coin_block_2: 0xBC0704,
+ LocationName.vanilla_secret_1_coin_block_3: 0xBC0705,
+ LocationName.vanilla_secret_1_powerup_block_2: 0xBC0706,
+ LocationName.vanilla_dome_3_coin_block_1: 0xBC0707,
+ LocationName.vanilla_dome_3_flying_block_1: 0xBC0708,
+ LocationName.vanilla_dome_3_flying_block_2: 0xBC0709,
+ LocationName.vanilla_dome_3_powerup_block_1: 0xBC070A,
+ LocationName.vanilla_dome_3_flying_block_3: 0xBC070B,
+ LocationName.vanilla_dome_3_invis_coin_block_1: 0xBC070C,
+ LocationName.vanilla_dome_3_powerup_block_2: 0xBC070D,
+ LocationName.vanilla_dome_3_multi_coin_block_1: 0xBC070E,
+ LocationName.vanilla_dome_3_powerup_block_3: 0xBC070F,
+ LocationName.vanilla_dome_3_yoshi_block_1: 0xBC0710,
+ LocationName.vanilla_dome_3_powerup_block_4: 0xBC0711,
+ LocationName.vanilla_dome_3_pswitch_coin_block_1: 0xBC0712,
+ LocationName.vanilla_dome_3_pswitch_coin_block_2: 0xBC0713,
+ LocationName.vanilla_dome_3_pswitch_coin_block_3: 0xBC0714,
+ LocationName.vanilla_dome_3_pswitch_coin_block_4: 0xBC0715,
+ LocationName.vanilla_dome_3_pswitch_coin_block_5: 0xBC0716,
+ LocationName.vanilla_dome_3_pswitch_coin_block_6: 0xBC0717,
+ LocationName.donut_secret_2_directional_coin_block_1: 0xBC0718,
+ LocationName.donut_secret_2_vine_block_1: 0xBC0719,
+ LocationName.donut_secret_2_star_block_1: 0xBC071A,
+ LocationName.donut_secret_2_powerup_block_1: 0xBC071B,
+ LocationName.donut_secret_2_star_block_2: 0xBC071C,
+ LocationName.valley_of_bowser_4_yellow_block_1: 0xBC071D,
+ LocationName.valley_of_bowser_4_powerup_block_1: 0xBC071E,
+ LocationName.valley_of_bowser_4_vine_block_1: 0xBC071F,
+ LocationName.valley_of_bowser_4_yoshi_block_1: 0xBC0720,
+ LocationName.valley_of_bowser_4_life_block_1: 0xBC0721,
+ LocationName.valley_of_bowser_4_powerup_block_2: 0xBC0722,
+ LocationName.valley_castle_yellow_block_1: 0xBC0723,
+ LocationName.valley_castle_yellow_block_2: 0xBC0724,
+ LocationName.valley_castle_green_block_1: 0xBC0725,
+ LocationName.valley_fortress_green_block_1: 0xBC0726,
+ LocationName.valley_fortress_yellow_block_1: 0xBC0727,
+ LocationName.valley_of_bowser_3_powerup_block_1: 0xBC0728,
+ LocationName.valley_of_bowser_3_powerup_block_2: 0xBC0729,
+ LocationName.valley_ghost_house_pswitch_coin_block_1: 0xBC072A,
+ LocationName.valley_ghost_house_multi_coin_block_1: 0xBC072B,
+ LocationName.valley_ghost_house_powerup_block_1: 0xBC072C,
+ LocationName.valley_ghost_house_directional_coin_block_1: 0xBC072D,
+ LocationName.valley_of_bowser_2_powerup_block_1: 0xBC072E,
+ LocationName.valley_of_bowser_2_yellow_block_1: 0xBC072F,
+ LocationName.valley_of_bowser_2_powerup_block_2: 0xBC0730,
+ LocationName.valley_of_bowser_2_wings_block_1: 0xBC0731,
+ LocationName.valley_of_bowser_1_green_block_1: 0xBC0732,
+ LocationName.valley_of_bowser_1_invis_coin_block_1: 0xBC0733,
+ LocationName.valley_of_bowser_1_invis_coin_block_2: 0xBC0734,
+ LocationName.valley_of_bowser_1_invis_coin_block_3: 0xBC0735,
+ LocationName.valley_of_bowser_1_yellow_block_1: 0xBC0736,
+ LocationName.valley_of_bowser_1_yellow_block_2: 0xBC0737,
+ LocationName.valley_of_bowser_1_yellow_block_3: 0xBC0738,
+ LocationName.valley_of_bowser_1_yellow_block_4: 0xBC0739,
+ LocationName.valley_of_bowser_1_vine_block_1: 0xBC073A,
+ LocationName.chocolate_secret_powerup_block_1: 0xBC073B,
+ LocationName.chocolate_secret_powerup_block_2: 0xBC073C,
+ LocationName.vanilla_dome_2_coin_block_1: 0xBC073D,
+ LocationName.vanilla_dome_2_powerup_block_1: 0xBC073E,
+ LocationName.vanilla_dome_2_coin_block_2: 0xBC073F,
+ LocationName.vanilla_dome_2_coin_block_3: 0xBC0740,
+ LocationName.vanilla_dome_2_vine_block_1: 0xBC0741,
+ LocationName.vanilla_dome_2_invis_life_block_1: 0xBC0742,
+ LocationName.vanilla_dome_2_coin_block_4: 0xBC0743,
+ LocationName.vanilla_dome_2_coin_block_5: 0xBC0744,
+ LocationName.vanilla_dome_2_powerup_block_2: 0xBC0745,
+ LocationName.vanilla_dome_2_powerup_block_3: 0xBC0746,
+ LocationName.vanilla_dome_2_powerup_block_4: 0xBC0747,
+ LocationName.vanilla_dome_2_powerup_block_5: 0xBC0748,
+ LocationName.vanilla_dome_2_multi_coin_block_1: 0xBC0749,
+ LocationName.vanilla_dome_2_multi_coin_block_2: 0xBC074A,
+ LocationName.vanilla_dome_4_powerup_block_1: 0xBC074B,
+ LocationName.vanilla_dome_4_powerup_block_2: 0xBC074C,
+ LocationName.vanilla_dome_4_coin_block_1: 0xBC074D,
+ LocationName.vanilla_dome_4_coin_block_2: 0xBC074E,
+ LocationName.vanilla_dome_4_coin_block_3: 0xBC074F,
+ LocationName.vanilla_dome_4_life_block_1: 0xBC0750,
+ LocationName.vanilla_dome_4_coin_block_4: 0xBC0751,
+ LocationName.vanilla_dome_4_coin_block_5: 0xBC0752,
+ LocationName.vanilla_dome_4_coin_block_6: 0xBC0753,
+ LocationName.vanilla_dome_4_coin_block_7: 0xBC0754,
+ LocationName.vanilla_dome_4_coin_block_8: 0xBC0755,
+ LocationName.vanilla_dome_1_flying_block_1: 0xBC0756,
+ LocationName.vanilla_dome_1_powerup_block_1: 0xBC0757,
+ LocationName.vanilla_dome_1_powerup_block_2: 0xBC0758,
+ LocationName.vanilla_dome_1_coin_block_1: 0xBC0759,
+ LocationName.vanilla_dome_1_life_block_1: 0xBC075A,
+ LocationName.vanilla_dome_1_powerup_block_3: 0xBC075B,
+ LocationName.vanilla_dome_1_vine_block_1: 0xBC075C,
+ LocationName.vanilla_dome_1_star_block_1: 0xBC075D,
+ LocationName.vanilla_dome_1_powerup_block_4: 0xBC075E,
+ LocationName.vanilla_dome_1_coin_block_2: 0xBC075F,
+ LocationName.vanilla_dome_castle_life_block_1: 0xBC0760,
+ LocationName.vanilla_dome_castle_life_block_2: 0xBC0761,
+ LocationName.vanilla_dome_castle_powerup_block_1: 0xBC0762,
+ LocationName.vanilla_dome_castle_life_block_3: 0xBC0763,
+ LocationName.vanilla_dome_castle_green_block_1: 0xBC0764,
+ LocationName.forest_ghost_house_coin_block_1: 0xBC0765,
+ LocationName.forest_ghost_house_powerup_block_1: 0xBC0766,
+ LocationName.forest_ghost_house_flying_block_1: 0xBC0767,
+ LocationName.forest_ghost_house_powerup_block_2: 0xBC0768,
+ LocationName.forest_ghost_house_life_block_1: 0xBC0769,
+ LocationName.forest_of_illusion_1_powerup_block_1: 0xBC076A,
+ LocationName.forest_of_illusion_1_yoshi_block_1: 0xBC076B,
+ LocationName.forest_of_illusion_1_powerup_block_2: 0xBC076C,
+ LocationName.forest_of_illusion_1_key_block_1: 0xBC076D,
+ LocationName.forest_of_illusion_1_life_block_1: 0xBC076E,
+ LocationName.forest_of_illusion_4_multi_coin_block_1: 0xBC076F,
+ LocationName.forest_of_illusion_4_coin_block_1: 0xBC0770,
+ LocationName.forest_of_illusion_4_coin_block_2: 0xBC0771,
+ LocationName.forest_of_illusion_4_coin_block_3: 0xBC0772,
+ LocationName.forest_of_illusion_4_coin_block_4: 0xBC0773,
+ LocationName.forest_of_illusion_4_powerup_block_1: 0xBC0774,
+ LocationName.forest_of_illusion_4_coin_block_5: 0xBC0775,
+ LocationName.forest_of_illusion_4_coin_block_6: 0xBC0776,
+ LocationName.forest_of_illusion_4_coin_block_7: 0xBC0777,
+ LocationName.forest_of_illusion_4_powerup_block_2: 0xBC0778,
+ LocationName.forest_of_illusion_4_coin_block_8: 0xBC0779,
+ LocationName.forest_of_illusion_4_coin_block_9: 0xBC077A,
+ LocationName.forest_of_illusion_4_coin_block_10: 0xBC077B,
+ LocationName.forest_of_illusion_2_green_block_1: 0xBC077C,
+ LocationName.forest_of_illusion_2_powerup_block_1: 0xBC077D,
+ LocationName.forest_of_illusion_2_invis_coin_block_1: 0xBC077E,
+ LocationName.forest_of_illusion_2_invis_coin_block_2: 0xBC077F,
+ LocationName.forest_of_illusion_2_invis_life_block_1: 0xBC0780,
+ LocationName.forest_of_illusion_2_invis_coin_block_3: 0xBC0781,
+ LocationName.forest_of_illusion_2_yellow_block_1: 0xBC0782,
+ LocationName.forest_secret_powerup_block_1: 0xBC0783,
+ LocationName.forest_secret_powerup_block_2: 0xBC0784,
+ LocationName.forest_secret_life_block_1: 0xBC0785,
+ LocationName.forest_of_illusion_3_yoshi_block_1: 0xBC0786,
+ LocationName.forest_of_illusion_3_coin_block_1: 0xBC0787,
+ LocationName.forest_of_illusion_3_multi_coin_block_1: 0xBC0788,
+ LocationName.forest_of_illusion_3_coin_block_2: 0xBC0789,
+ LocationName.forest_of_illusion_3_multi_coin_block_2: 0xBC078A,
+ LocationName.forest_of_illusion_3_coin_block_3: 0xBC078B,
+ LocationName.forest_of_illusion_3_coin_block_4: 0xBC078C,
+ LocationName.forest_of_illusion_3_coin_block_5: 0xBC078D,
+ LocationName.forest_of_illusion_3_coin_block_6: 0xBC078E,
+ LocationName.forest_of_illusion_3_coin_block_7: 0xBC078F,
+ LocationName.forest_of_illusion_3_coin_block_8: 0xBC0790,
+ LocationName.forest_of_illusion_3_coin_block_9: 0xBC0791,
+ LocationName.forest_of_illusion_3_coin_block_10: 0xBC0792,
+ LocationName.forest_of_illusion_3_coin_block_11: 0xBC0793,
+ LocationName.forest_of_illusion_3_coin_block_12: 0xBC0794,
+ LocationName.forest_of_illusion_3_coin_block_13: 0xBC0795,
+ LocationName.forest_of_illusion_3_coin_block_14: 0xBC0796,
+ LocationName.forest_of_illusion_3_coin_block_15: 0xBC0797,
+ LocationName.forest_of_illusion_3_coin_block_16: 0xBC0798,
+ LocationName.forest_of_illusion_3_coin_block_17: 0xBC0799,
+ LocationName.forest_of_illusion_3_coin_block_18: 0xBC079A,
+ LocationName.forest_of_illusion_3_coin_block_19: 0xBC079B,
+ LocationName.forest_of_illusion_3_coin_block_20: 0xBC079C,
+ LocationName.forest_of_illusion_3_coin_block_21: 0xBC079D,
+ LocationName.forest_of_illusion_3_coin_block_22: 0xBC079E,
+ LocationName.forest_of_illusion_3_coin_block_23: 0xBC079F,
+ LocationName.forest_of_illusion_3_coin_block_24: 0xBC07A0,
+ LocationName.special_zone_8_yoshi_block_1: 0xBC07A1,
+ LocationName.special_zone_8_coin_block_1: 0xBC07A2,
+ LocationName.special_zone_8_coin_block_2: 0xBC07A3,
+ LocationName.special_zone_8_coin_block_3: 0xBC07A4,
+ LocationName.special_zone_8_coin_block_4: 0xBC07A5,
+ LocationName.special_zone_8_coin_block_5: 0xBC07A6,
+ LocationName.special_zone_8_blue_pow_block_1: 0xBC07A7,
+ LocationName.special_zone_8_powerup_block_1: 0xBC07A8,
+ LocationName.special_zone_8_star_block_1: 0xBC07A9,
+ LocationName.special_zone_8_coin_block_6: 0xBC07AA,
+ LocationName.special_zone_8_coin_block_7: 0xBC07AB,
+ LocationName.special_zone_8_coin_block_8: 0xBC07AC,
+ LocationName.special_zone_8_coin_block_9: 0xBC07AD,
+ LocationName.special_zone_8_coin_block_10: 0xBC07AE,
+ LocationName.special_zone_8_coin_block_11: 0xBC07AF,
+ LocationName.special_zone_8_coin_block_12: 0xBC07B0,
+ LocationName.special_zone_8_coin_block_13: 0xBC07B1,
+ LocationName.special_zone_8_coin_block_14: 0xBC07B2,
+ LocationName.special_zone_8_coin_block_15: 0xBC07B3,
+ LocationName.special_zone_8_coin_block_16: 0xBC07B4,
+ LocationName.special_zone_8_coin_block_17: 0xBC07B5,
+ LocationName.special_zone_8_coin_block_18: 0xBC07B6,
+ LocationName.special_zone_8_multi_coin_block_1: 0xBC07B7,
+ LocationName.special_zone_8_coin_block_19: 0xBC07B8,
+ LocationName.special_zone_8_coin_block_20: 0xBC07B9,
+ LocationName.special_zone_8_coin_block_21: 0xBC07BA,
+ LocationName.special_zone_8_coin_block_22: 0xBC07BB,
+ LocationName.special_zone_8_coin_block_23: 0xBC07BC,
+ LocationName.special_zone_8_powerup_block_2: 0xBC07BD,
+ LocationName.special_zone_8_flying_block_1: 0xBC07BE,
+ LocationName.special_zone_7_powerup_block_1: 0xBC07BF,
+ LocationName.special_zone_7_yoshi_block_1: 0xBC07C0,
+ LocationName.special_zone_7_coin_block_1: 0xBC07C1,
+ LocationName.special_zone_7_powerup_block_2: 0xBC07C2,
+ LocationName.special_zone_7_coin_block_2: 0xBC07C3,
+ LocationName.special_zone_6_powerup_block_1: 0xBC07C4,
+ LocationName.special_zone_6_coin_block_1: 0xBC07C5,
+ LocationName.special_zone_6_coin_block_2: 0xBC07C6,
+ LocationName.special_zone_6_yoshi_block_1: 0xBC07C7,
+ LocationName.special_zone_6_life_block_1: 0xBC07C8,
+ LocationName.special_zone_6_multi_coin_block_1: 0xBC07C9,
+ LocationName.special_zone_6_coin_block_3: 0xBC07CA,
+ LocationName.special_zone_6_coin_block_4: 0xBC07CB,
+ LocationName.special_zone_6_coin_block_5: 0xBC07CC,
+ LocationName.special_zone_6_coin_block_6: 0xBC07CD,
+ LocationName.special_zone_6_coin_block_7: 0xBC07CE,
+ LocationName.special_zone_6_coin_block_8: 0xBC07CF,
+ LocationName.special_zone_6_coin_block_9: 0xBC07D0,
+ LocationName.special_zone_6_coin_block_10: 0xBC07D1,
+ LocationName.special_zone_6_coin_block_11: 0xBC07D2,
+ LocationName.special_zone_6_coin_block_12: 0xBC07D3,
+ LocationName.special_zone_6_coin_block_13: 0xBC07D4,
+ LocationName.special_zone_6_coin_block_14: 0xBC07D5,
+ LocationName.special_zone_6_coin_block_15: 0xBC07D6,
+ LocationName.special_zone_6_coin_block_16: 0xBC07D7,
+ LocationName.special_zone_6_coin_block_17: 0xBC07D8,
+ LocationName.special_zone_6_coin_block_18: 0xBC07D9,
+ LocationName.special_zone_6_coin_block_19: 0xBC07DA,
+ LocationName.special_zone_6_coin_block_20: 0xBC07DB,
+ LocationName.special_zone_6_coin_block_21: 0xBC07DC,
+ LocationName.special_zone_6_coin_block_22: 0xBC07DD,
+ LocationName.special_zone_6_coin_block_23: 0xBC07DE,
+ LocationName.special_zone_6_coin_block_24: 0xBC07DF,
+ LocationName.special_zone_6_coin_block_25: 0xBC07E0,
+ LocationName.special_zone_6_coin_block_26: 0xBC07E1,
+ LocationName.special_zone_6_coin_block_27: 0xBC07E2,
+ LocationName.special_zone_6_coin_block_28: 0xBC07E3,
+ LocationName.special_zone_6_powerup_block_2: 0xBC07E4,
+ LocationName.special_zone_6_coin_block_29: 0xBC07E5,
+ LocationName.special_zone_6_coin_block_30: 0xBC07E6,
+ LocationName.special_zone_6_coin_block_31: 0xBC07E7,
+ LocationName.special_zone_6_coin_block_32: 0xBC07E8,
+ LocationName.special_zone_6_coin_block_33: 0xBC07E9,
+ LocationName.special_zone_5_yoshi_block_1: 0xBC07EA,
+ LocationName.special_zone_1_vine_block_1: 0xBC07EB,
+ LocationName.special_zone_1_vine_block_2: 0xBC07EC,
+ LocationName.special_zone_1_vine_block_3: 0xBC07ED,
+ LocationName.special_zone_1_vine_block_4: 0xBC07EE,
+ LocationName.special_zone_1_life_block_1: 0xBC07EF,
+ LocationName.special_zone_1_vine_block_5: 0xBC07F0,
+ LocationName.special_zone_1_blue_pow_block_1: 0xBC07F1,
+ LocationName.special_zone_1_vine_block_6: 0xBC07F2,
+ LocationName.special_zone_1_powerup_block_1: 0xBC07F3,
+ LocationName.special_zone_1_pswitch_coin_block_1: 0xBC07F4,
+ LocationName.special_zone_1_pswitch_coin_block_2: 0xBC07F5,
+ LocationName.special_zone_1_pswitch_coin_block_3: 0xBC07F6,
+ LocationName.special_zone_1_pswitch_coin_block_4: 0xBC07F7,
+ LocationName.special_zone_1_pswitch_coin_block_5: 0xBC07F8,
+ LocationName.special_zone_1_pswitch_coin_block_6: 0xBC07F9,
+ LocationName.special_zone_1_pswitch_coin_block_7: 0xBC07FA,
+ LocationName.special_zone_1_pswitch_coin_block_8: 0xBC07FB,
+ LocationName.special_zone_1_pswitch_coin_block_9: 0xBC07FC,
+ LocationName.special_zone_1_pswitch_coin_block_10: 0xBC07FD,
+ LocationName.special_zone_1_pswitch_coin_block_11: 0xBC07FE,
+ LocationName.special_zone_1_pswitch_coin_block_12: 0xBC07FF,
+ LocationName.special_zone_1_pswitch_coin_block_13: 0xBC0800,
+ LocationName.special_zone_2_powerup_block_1: 0xBC0801,
+ LocationName.special_zone_2_coin_block_1: 0xBC0802,
+ LocationName.special_zone_2_coin_block_2: 0xBC0803,
+ LocationName.special_zone_2_powerup_block_2: 0xBC0804,
+ LocationName.special_zone_2_coin_block_3: 0xBC0805,
+ LocationName.special_zone_2_coin_block_4: 0xBC0806,
+ LocationName.special_zone_2_powerup_block_3: 0xBC0807,
+ LocationName.special_zone_2_multi_coin_block_1: 0xBC0808,
+ LocationName.special_zone_2_coin_block_5: 0xBC0809,
+ LocationName.special_zone_2_coin_block_6: 0xBC080A,
+ LocationName.special_zone_3_powerup_block_1: 0xBC080B,
+ LocationName.special_zone_3_yoshi_block_1: 0xBC080C,
+ LocationName.special_zone_3_wings_block_1: 0xBC080D,
+ LocationName.special_zone_4_powerup_block_1: 0xBC080E,
+ LocationName.special_zone_4_star_block_1: 0xBC080F,
+ LocationName.star_road_2_star_block_1: 0xBC0810,
+ LocationName.star_road_3_key_block_1: 0xBC0811,
+ LocationName.star_road_4_powerup_block_1: 0xBC0812,
+ LocationName.star_road_4_green_block_1: 0xBC0813,
+ LocationName.star_road_4_green_block_2: 0xBC0814,
+ LocationName.star_road_4_green_block_3: 0xBC0815,
+ LocationName.star_road_4_green_block_4: 0xBC0816,
+ LocationName.star_road_4_green_block_5: 0xBC0817,
+ LocationName.star_road_4_green_block_6: 0xBC0818,
+ LocationName.star_road_4_green_block_7: 0xBC0819,
+ LocationName.star_road_4_key_block_1: 0xBC081A,
+ LocationName.star_road_5_directional_coin_block_1: 0xBC081B,
+ LocationName.star_road_5_life_block_1: 0xBC081C,
+ LocationName.star_road_5_vine_block_1: 0xBC081D,
+ LocationName.star_road_5_yellow_block_1: 0xBC081E,
+ LocationName.star_road_5_yellow_block_2: 0xBC081F,
+ LocationName.star_road_5_yellow_block_3: 0xBC0820,
+ LocationName.star_road_5_yellow_block_4: 0xBC0821,
+ LocationName.star_road_5_yellow_block_5: 0xBC0822,
+ LocationName.star_road_5_yellow_block_6: 0xBC0823,
+ LocationName.star_road_5_yellow_block_7: 0xBC0824,
+ LocationName.star_road_5_yellow_block_8: 0xBC0825,
+ LocationName.star_road_5_yellow_block_9: 0xBC0826,
+ LocationName.star_road_5_yellow_block_10: 0xBC0827,
+ LocationName.star_road_5_yellow_block_11: 0xBC0828,
+ LocationName.star_road_5_yellow_block_12: 0xBC0829,
+ LocationName.star_road_5_yellow_block_13: 0xBC082A,
+ LocationName.star_road_5_yellow_block_14: 0xBC082B,
+ LocationName.star_road_5_yellow_block_15: 0xBC082C,
+ LocationName.star_road_5_yellow_block_16: 0xBC082D,
+ LocationName.star_road_5_yellow_block_17: 0xBC082E,
+ LocationName.star_road_5_yellow_block_18: 0xBC082F,
+ LocationName.star_road_5_yellow_block_19: 0xBC0830,
+ LocationName.star_road_5_yellow_block_20: 0xBC0831,
+ LocationName.star_road_5_green_block_1: 0xBC0832,
+ LocationName.star_road_5_green_block_2: 0xBC0833,
+ LocationName.star_road_5_green_block_3: 0xBC0834,
+ LocationName.star_road_5_green_block_4: 0xBC0835,
+ LocationName.star_road_5_green_block_5: 0xBC0836,
+ LocationName.star_road_5_green_block_6: 0xBC0837,
+ LocationName.star_road_5_green_block_7: 0xBC0838,
+ LocationName.star_road_5_green_block_8: 0xBC0839,
+ LocationName.star_road_5_green_block_9: 0xBC083A,
+ LocationName.star_road_5_green_block_10: 0xBC083B,
+ LocationName.star_road_5_green_block_11: 0xBC083C,
+ LocationName.star_road_5_green_block_12: 0xBC083D,
+ LocationName.star_road_5_green_block_13: 0xBC083E,
+ LocationName.star_road_5_green_block_14: 0xBC083F,
+ LocationName.star_road_5_green_block_15: 0xBC0840,
+ LocationName.star_road_5_green_block_16: 0xBC0841,
+ LocationName.star_road_5_green_block_17: 0xBC0842,
+ LocationName.star_road_5_green_block_18: 0xBC0843,
+ LocationName.star_road_5_green_block_19: 0xBC0844,
+ LocationName.star_road_5_green_block_20: 0xBC0845
+}
+
bowser_location_table = {
LocationName.bowser: 0xBC0200,
}
@@ -208,6 +826,10 @@ def __init__(self, player: int, name: str = '', address: int = None, parent=None
all_locations = {
**level_location_table,
**dragon_coin_location_table,
+ **moon_location_table,
+ **hidden_1ups_location_table,
+ **bonus_block_location_table,
+ **blocksanity_location_table,
**bowser_location_table,
**yoshi_house_location_table,
}
@@ -234,20 +856,149 @@ def __init__(self, player: int, name: str = '', address: int = None, parent=None
LocationName.special_zone_8_dragon,
]
+special_zone_hidden_1up_names = [
+ LocationName.special_zone_1_hidden_1up
+]
+
+special_zone_blocksanity_names = [
+ LocationName.special_zone_8_yoshi_block_1,
+ LocationName.special_zone_8_coin_block_1,
+ LocationName.special_zone_8_coin_block_2,
+ LocationName.special_zone_8_coin_block_3,
+ LocationName.special_zone_8_coin_block_4,
+ LocationName.special_zone_8_coin_block_5,
+ LocationName.special_zone_8_blue_pow_block_1,
+ LocationName.special_zone_8_powerup_block_1,
+ LocationName.special_zone_8_star_block_1,
+ LocationName.special_zone_8_coin_block_6,
+ LocationName.special_zone_8_coin_block_7,
+ LocationName.special_zone_8_coin_block_8,
+ LocationName.special_zone_8_coin_block_9,
+ LocationName.special_zone_8_coin_block_10,
+ LocationName.special_zone_8_coin_block_11,
+ LocationName.special_zone_8_coin_block_12,
+ LocationName.special_zone_8_coin_block_13,
+ LocationName.special_zone_8_coin_block_14,
+ LocationName.special_zone_8_coin_block_15,
+ LocationName.special_zone_8_coin_block_16,
+ LocationName.special_zone_8_coin_block_17,
+ LocationName.special_zone_8_coin_block_18,
+ LocationName.special_zone_8_multi_coin_block_1,
+ LocationName.special_zone_8_coin_block_19,
+ LocationName.special_zone_8_coin_block_20,
+ LocationName.special_zone_8_coin_block_21,
+ LocationName.special_zone_8_coin_block_22,
+ LocationName.special_zone_8_coin_block_23,
+ LocationName.special_zone_8_powerup_block_2,
+ LocationName.special_zone_8_flying_block_1,
+ LocationName.special_zone_7_powerup_block_1,
+ LocationName.special_zone_7_yoshi_block_1,
+ LocationName.special_zone_7_coin_block_1,
+ LocationName.special_zone_7_powerup_block_2,
+ LocationName.special_zone_7_coin_block_2,
+ LocationName.special_zone_6_powerup_block_1,
+ LocationName.special_zone_6_coin_block_1,
+ LocationName.special_zone_6_coin_block_2,
+ LocationName.special_zone_6_yoshi_block_1,
+ LocationName.special_zone_6_life_block_1,
+ LocationName.special_zone_6_multi_coin_block_1,
+ LocationName.special_zone_6_coin_block_3,
+ LocationName.special_zone_6_coin_block_4,
+ LocationName.special_zone_6_coin_block_5,
+ LocationName.special_zone_6_coin_block_6,
+ LocationName.special_zone_6_coin_block_7,
+ LocationName.special_zone_6_coin_block_8,
+ LocationName.special_zone_6_coin_block_9,
+ LocationName.special_zone_6_coin_block_10,
+ LocationName.special_zone_6_coin_block_11,
+ LocationName.special_zone_6_coin_block_12,
+ LocationName.special_zone_6_coin_block_13,
+ LocationName.special_zone_6_coin_block_14,
+ LocationName.special_zone_6_coin_block_15,
+ LocationName.special_zone_6_coin_block_16,
+ LocationName.special_zone_6_coin_block_17,
+ LocationName.special_zone_6_coin_block_18,
+ LocationName.special_zone_6_coin_block_19,
+ LocationName.special_zone_6_coin_block_20,
+ LocationName.special_zone_6_coin_block_21,
+ LocationName.special_zone_6_coin_block_22,
+ LocationName.special_zone_6_coin_block_23,
+ LocationName.special_zone_6_coin_block_24,
+ LocationName.special_zone_6_coin_block_25,
+ LocationName.special_zone_6_coin_block_26,
+ LocationName.special_zone_6_coin_block_27,
+ LocationName.special_zone_6_coin_block_28,
+ LocationName.special_zone_6_powerup_block_2,
+ LocationName.special_zone_6_coin_block_29,
+ LocationName.special_zone_6_coin_block_30,
+ LocationName.special_zone_6_coin_block_31,
+ LocationName.special_zone_6_coin_block_32,
+ LocationName.special_zone_6_coin_block_33,
+ LocationName.special_zone_5_yoshi_block_1,
+ LocationName.special_zone_1_vine_block_1,
+ LocationName.special_zone_1_vine_block_2,
+ LocationName.special_zone_1_vine_block_3,
+ LocationName.special_zone_1_vine_block_4,
+ LocationName.special_zone_1_life_block_1,
+ LocationName.special_zone_1_vine_block_5,
+ LocationName.special_zone_1_blue_pow_block_1,
+ LocationName.special_zone_1_vine_block_6,
+ LocationName.special_zone_1_powerup_block_1,
+ LocationName.special_zone_1_pswitch_coin_block_1,
+ LocationName.special_zone_1_pswitch_coin_block_2,
+ LocationName.special_zone_1_pswitch_coin_block_3,
+ LocationName.special_zone_1_pswitch_coin_block_4,
+ LocationName.special_zone_1_pswitch_coin_block_5,
+ LocationName.special_zone_1_pswitch_coin_block_6,
+ LocationName.special_zone_1_pswitch_coin_block_7,
+ LocationName.special_zone_1_pswitch_coin_block_8,
+ LocationName.special_zone_1_pswitch_coin_block_9,
+ LocationName.special_zone_1_pswitch_coin_block_10,
+ LocationName.special_zone_1_pswitch_coin_block_11,
+ LocationName.special_zone_1_pswitch_coin_block_12,
+ LocationName.special_zone_1_pswitch_coin_block_13,
+ LocationName.special_zone_2_powerup_block_1,
+ LocationName.special_zone_2_coin_block_1,
+ LocationName.special_zone_2_coin_block_2,
+ LocationName.special_zone_2_powerup_block_2,
+ LocationName.special_zone_2_coin_block_3,
+ LocationName.special_zone_2_coin_block_4,
+ LocationName.special_zone_2_powerup_block_3,
+ LocationName.special_zone_2_multi_coin_block_1,
+ LocationName.special_zone_2_coin_block_5,
+ LocationName.special_zone_2_coin_block_6,
+ LocationName.special_zone_3_powerup_block_1,
+ LocationName.special_zone_3_yoshi_block_1,
+ LocationName.special_zone_3_wings_block_1,
+ LocationName.special_zone_4_powerup_block_1,
+ LocationName.special_zone_4_star_block_1
+]
+
location_table = {}
-def setup_locations(world, player: int):
+def setup_locations(world: World):
location_table = {**level_location_table}
- # Dragon Coins here
- if world.dragon_coin_checks[player].value:
- location_table.update({**dragon_coin_location_table})
+ if world.options.dragon_coin_checks:
+ location_table.update(dragon_coin_location_table)
+
+ if world.options.moon_checks:
+ location_table.update(moon_location_table)
+
+ if world.options.hidden_1up_checks:
+ location_table.update(hidden_1ups_location_table)
+
+ if world.options.bonus_block_checks:
+ location_table.update(bonus_block_location_table)
+
+ if world.options.blocksanity:
+ location_table.update(blocksanity_location_table)
- if world.goal[player] == "yoshi_egg_hunt":
- location_table.update({**yoshi_house_location_table})
+ if world.options.goal == "yoshi_egg_hunt":
+ location_table.update(yoshi_house_location_table)
else:
- location_table.update({**bowser_location_table})
+ location_table.update(bowser_location_table)
return location_table
diff --git a/worlds/smw/Names/ItemName.py b/worlds/smw/Names/ItemName.py
index fecb18685ef4..e6eced410059 100644
--- a/worlds/smw/Names/ItemName.py
+++ b/worlds/smw/Names/ItemName.py
@@ -1,5 +1,9 @@
# Junk Definitions
one_up_mushroom = "1-Up Mushroom"
+one_coin = "1 coin"
+five_coins = "5 coins"
+ten_coins = "10 coins"
+fifty_coins = "50 coins"
# Collectable Definitions
yoshi_egg = "Yoshi Egg"
@@ -22,11 +26,16 @@
red_switch_palace = "Red Switch Palace"
blue_switch_palace = "Blue Switch Palace"
+# Special Zone clear flag definition
+special_world_clear = "Special Zone Clear"
+
# Trap Definitions
-ice_trap = "Ice Trap"
-stun_trap = "Stun Trap"
-literature_trap = "Literature Trap"
-timer_trap = "Timer Trap"
+ice_trap = "Ice Trap"
+stun_trap = "Stun Trap"
+literature_trap = "Literature Trap"
+timer_trap = "Timer Trap"
+reverse_controls_trap = "Reverse Trap"
+thwimp_trap = "Thwimp Trap"
# Other Definitions
victory = "The Princess"
diff --git a/worlds/smw/Names/LocationName.py b/worlds/smw/Names/LocationName.py
index cc01b05ece59..847b724f7837 100644
--- a/worlds/smw/Names/LocationName.py
+++ b/worlds/smw/Names/LocationName.py
@@ -4,12 +4,15 @@
yoshis_island_1_exit_1 = "Yoshi's Island 1 - Normal Exit"
yoshis_island_1_dragon = "Yoshi's Island 1 - Dragon Coins"
+yoshis_island_1_moon = "Yoshi's Island 1 - 3-Up Moon"
yoshis_island_2_exit_1 = "Yoshi's Island 2 - Normal Exit"
yoshis_island_2_dragon = "Yoshi's Island 2 - Dragon Coins"
yoshis_island_3_exit_1 = "Yoshi's Island 3 - Normal Exit"
yoshis_island_3_dragon = "Yoshi's Island 3 - Dragon Coins"
+yoshis_island_3_bonus_block = "Yoshi's Island 3 - 1-Up from Bonus Block"
yoshis_island_4_exit_1 = "Yoshi's Island 4 - Normal Exit"
yoshis_island_4_dragon = "Yoshi's Island 4 - Dragon Coins"
+yoshis_island_4_hidden_1up = "Yoshi's Island 4 - Hidden 1-Up"
yoshis_island_castle = "#1 Iggy's Castle - Normal Exit"
yoshis_island_koopaling = "#1 Iggy's Castle - Boss"
@@ -18,13 +21,17 @@
donut_plains_1_exit_1 = "Donut Plains 1 - Normal Exit"
donut_plains_1_exit_2 = "Donut Plains 1 - Secret Exit"
donut_plains_1_dragon = "Donut Plains 1 - Dragon Coins"
+donut_plains_1_hidden_1up = "Donut Plains 1 - Hidden 1-Up"
donut_plains_2_exit_1 = "Donut Plains 2 - Normal Exit"
donut_plains_2_exit_2 = "Donut Plains 2 - Secret Exit"
donut_plains_2_dragon = "Donut Plains 2 - Dragon Coins"
donut_plains_3_exit_1 = "Donut Plains 3 - Normal Exit"
donut_plains_3_dragon = "Donut Plains 3 - Dragon Coins"
+donut_plains_3_bonus_block = "Donut Plains 3 - 1-Up from Bonus Block"
donut_plains_4_exit_1 = "Donut Plains 4 - Normal Exit"
donut_plains_4_dragon = "Donut Plains 4 - Dragon Coins"
+donut_plains_4_moon = "Donut Plains 4 - 3-Up Moon"
+donut_plains_4_hidden_1up = "Donut Plains 4 - Hidden 1-Up"
donut_secret_1_exit_1 = "Donut Secret 1 - Normal Exit"
donut_secret_1_exit_2 = "Donut Secret 1 - Secret Exit"
donut_secret_1_dragon = "Donut Secret 1 - Dragon Coins"
@@ -35,6 +42,7 @@
donut_secret_house_exit_1 = "Donut Secret House - Normal Exit"
donut_secret_house_exit_2 = "Donut Secret House - Secret Exit"
donut_plains_castle = "#2 Morton's Castle - Normal Exit"
+donut_plains_castle_hidden_1up = "#2 Morton's Castle - Hidden 1-Up"
donut_plains_koopaling = "#2 Morton's Castle - Boss"
green_switch_palace = "Green Switch Palace"
@@ -47,8 +55,10 @@
vanilla_dome_2_dragon = "Vanilla Dome 2 - Dragon Coins"
vanilla_dome_3_exit_1 = "Vanilla Dome 3 - Normal Exit"
vanilla_dome_3_dragon = "Vanilla Dome 3 - Dragon Coins"
+vanilla_dome_3_moon = "Vanilla Dome 3 - 3-Up Moon"
vanilla_dome_4_exit_1 = "Vanilla Dome 4 - Normal Exit"
vanilla_dome_4_dragon = "Vanilla Dome 4 - Dragon Coins"
+vanilla_dome_4_hidden_1up = "Vanilla Dome 4 - Hidden 1-Up"
vanilla_secret_1_exit_1 = "Vanilla Secret 1 - Normal Exit"
vanilla_secret_1_exit_2 = "Vanilla Secret 1 - Secret Exit"
vanilla_secret_1_dragon = "Vanilla Secret 1 - Dragon Coins"
@@ -58,7 +68,9 @@
vanilla_secret_3_dragon = "Vanilla Secret 3 - Dragon Coins"
vanilla_ghost_house_exit_1 = "Vanilla Ghost House - Normal Exit"
vanilla_ghost_house_dragon = "Vanilla Ghost House - Dragon Coins"
+vanilla_ghost_house_hidden_1up = "Vanilla Ghost House - Hidden 1-Up"
vanilla_fortress = "Vanilla Fortress - Normal Exit"
+vanilla_fortress_hidden_1up = "Vanilla Fortress - Hidden 1-Up"
vanilla_reznor = "Vanilla Fortress - Boss"
vanilla_dome_castle = "#3 Lemmy's Castle - Normal Exit"
vanilla_dome_koopaling = "#3 Lemmy's Castle - Boss"
@@ -67,13 +79,16 @@
butter_bridge_1_exit_1 = "Butter Bridge 1 - Normal Exit"
butter_bridge_1_dragon = "Butter Bridge 1 - Dragon Coins"
+butter_bridge_1_bonus_block = "Butter Bridge 1 - 1-Up from Bonus Block"
butter_bridge_2_exit_1 = "Butter Bridge 2 - Normal Exit"
butter_bridge_2_dragon = "Butter Bridge 2 - Dragon Coins"
cheese_bridge_exit_1 = "Cheese Bridge - Normal Exit"
cheese_bridge_exit_2 = "Cheese Bridge - Secret Exit"
cheese_bridge_dragon = "Cheese Bridge - Dragon Coins"
+cheese_bridge_moon = "Cheese Bridge - 3-Up Moon"
cookie_mountain_exit_1 = "Cookie Mountain - Normal Exit"
cookie_mountain_dragon = "Cookie Mountain - Dragon Coins"
+cookie_mountain_hidden_1up = "Cookie Mountain - Hidden 1-Up"
soda_lake_exit_1 = "Soda Lake - Normal Exit"
soda_lake_dragon = "Soda Lake - Dragon Coins"
twin_bridges_castle = "#4 Ludwig's Castle - Normal Exit"
@@ -87,12 +102,14 @@
forest_of_illusion_3_exit_1 = "Forest of Illusion 3 - Normal Exit"
forest_of_illusion_3_exit_2 = "Forest of Illusion 3 - Secret Exit"
forest_of_illusion_3_dragon = "Forest of Illusion 3 - Dragon Coins"
+forest_of_illusion_3_hidden_1up = "Forest of Illusion 3 - Hidden 1-Up"
forest_of_illusion_4_exit_1 = "Forest of Illusion 4 - Normal Exit"
forest_of_illusion_4_exit_2 = "Forest of Illusion 4 - Secret Exit"
forest_of_illusion_4_dragon = "Forest of Illusion 4 - Dragon Coins"
forest_ghost_house_exit_1 = "Forest Ghost House - Normal Exit"
forest_ghost_house_exit_2 = "Forest Ghost House - Secret Exit"
forest_ghost_house_dragon = "Forest Ghost House - Dragon Coins"
+forest_ghost_house_moon = "Forest Ghost House - 3-Up Moon"
forest_secret_exit_1 = "Forest Secret - Normal Exit"
forest_secret_dragon = "Forest Secret - Dragon Coins"
forest_fortress = "Forest Fortress - Normal Exit"
@@ -105,12 +122,15 @@
chocolate_island_1_exit_1 = "Chocolate Island 1 - Normal Exit"
chocolate_island_1_dragon = "Chocolate Island 1 - Dragon Coins"
+chocolate_island_1_moon = "Chocolate Island 1 - 3-Up Moon"
chocolate_island_2_exit_1 = "Chocolate Island 2 - Normal Exit"
chocolate_island_2_exit_2 = "Chocolate Island 2 - Secret Exit"
chocolate_island_2_dragon = "Chocolate Island 2 - Dragon Coins"
+chocolate_island_2_hidden_1up = "Chocolate Island 2 - Hidden 1-Up"
chocolate_island_3_exit_1 = "Chocolate Island 3 - Normal Exit"
chocolate_island_3_exit_2 = "Chocolate Island 3 - Secret Exit"
chocolate_island_3_dragon = "Chocolate Island 3 - Dragon Coins"
+chocolate_island_3_bonus_block = "Chocolate Island 3 - 1-Up from Bonus Block"
chocolate_island_4_exit_1 = "Chocolate Island 4 - Normal Exit"
chocolate_island_4_dragon = "Chocolate Island 4 - Dragon Coins"
chocolate_island_5_exit_1 = "Chocolate Island 5 - Normal Exit"
@@ -120,6 +140,7 @@
chocolate_fortress = "Chocolate Fortress - Normal Exit"
chocolate_reznor = "Chocolate Fortress - Boss"
chocolate_castle = "#6 Wendy's Castle - Normal Exit"
+chocolate_castle_hidden_1up = "#6 Wendy's Castle - Hidden 1-Up"
chocolate_koopaling = "#6 Wendy's Castle - Boss"
sunken_ghost_ship = "Sunken Ghost Ship - Normal Exit"
@@ -127,9 +148,11 @@
valley_of_bowser_1_exit_1 = "Valley of Bowser 1 - Normal Exit"
valley_of_bowser_1_dragon = "Valley of Bowser 1 - Dragon Coins"
+valley_of_bowser_1_moon = "Valley of Bowser 1 - 3-Up Moon"
valley_of_bowser_2_exit_1 = "Valley of Bowser 2 - Normal Exit"
valley_of_bowser_2_exit_2 = "Valley of Bowser 2 - Secret Exit"
valley_of_bowser_2_dragon = "Valley of Bowser 2 - Dragon Coins"
+valley_of_bowser_2_hidden_1up = "Valley of Bowser 2 - Hidden 1-Up"
valley_of_bowser_3_exit_1 = "Valley of Bowser 3 - Normal Exit"
valley_of_bowser_3_dragon = "Valley of Bowser 3 - Dragon Coins"
valley_of_bowser_4_exit_1 = "Valley of Bowser 4 - Normal Exit"
@@ -141,6 +164,7 @@
valley_reznor = "Valley Fortress - Boss"
valley_castle = "#7 Larry's Castle - Normal Exit"
valley_castle_dragon = "#7 Larry's Castle - Dragon Coins"
+valley_castle_hidden_1up = "#7 Larry's Castle - Hidden 1-Up"
valley_koopaling = "#7 Larry's Castle - Boss"
front_door = "Front Door"
@@ -161,6 +185,7 @@
special_zone_1_exit_1 = "Gnarly - Normal Exit"
special_zone_1_dragon = "Gnarly - Dragon Coins"
+special_zone_1_hidden_1up = "Gnarly - Hidden 1-Up"
special_zone_2_exit_1 = "Tubular - Normal Exit"
special_zone_2_dragon = "Tubular - Dragon Coins"
special_zone_3_exit_1 = "Way Cool - Normal Exit"
@@ -362,3 +387,586 @@
special_zone_8_tile = "Funky - Tile"
special_zone_8_region = "Funky"
special_complete = "Special Zone - Star Road - Complete"
+
+vanilla_secret_2_yoshi_block_1 = "Vanilla Secret 2 - Yoshi Block #1"
+vanilla_secret_2_green_block_1 = "Vanilla Secret 2 - Green Switch Palace Block #1"
+vanilla_secret_2_powerup_block_1 = "Vanilla Secret 2 - Powerup Block #1"
+vanilla_secret_2_powerup_block_2 = "Vanilla Secret 2 - Powerup Block #2"
+vanilla_secret_2_multi_coin_block_1 = "Vanilla Secret 2 - Multi Coin Block #1"
+vanilla_secret_2_gray_pow_block_1 = "Vanilla Secret 2 - Gray P-Switch Block #1"
+vanilla_secret_2_coin_block_1 = "Vanilla Secret 2 - Coin Block #1"
+vanilla_secret_2_coin_block_2 = "Vanilla Secret 2 - Coin Block #2"
+vanilla_secret_2_coin_block_3 = "Vanilla Secret 2 - Coin Block #3"
+vanilla_secret_2_coin_block_4 = "Vanilla Secret 2 - Coin Block #4"
+vanilla_secret_2_coin_block_5 = "Vanilla Secret 2 - Coin Block #5"
+vanilla_secret_2_coin_block_6 = "Vanilla Secret 2 - Coin Block #6"
+vanilla_secret_3_powerup_block_1 = "Vanilla Secret 3 - Powerup Block #1"
+vanilla_secret_3_powerup_block_2 = "Vanilla Secret 3 - Powerup Block #2"
+donut_ghost_house_vine_block_1 = "Donut Ghost House - Vine|P-Switch Block #1"
+donut_ghost_house_directional_coin_block_1 = "Donut Ghost House - Directional Coin Block #1"
+donut_ghost_house_life_block_1 = "Donut Ghost House - 1-Up Mushroom Block #1"
+donut_ghost_house_life_block_2 = "Donut Ghost House - 1-Up Mushroom Block #2"
+donut_ghost_house_life_block_3 = "Donut Ghost House - 1-Up Mushroom Block #3"
+donut_ghost_house_life_block_4 = "Donut Ghost House - 1-Up Mushroom Block #4"
+donut_plains_3_green_block_1 = "Donut Plains 3 - Green Switch Palace Block #1"
+donut_plains_3_coin_block_1 = "Donut Plains 3 - Coin Block #1"
+donut_plains_3_coin_block_2 = "Donut Plains 3 - Coin Block #2"
+donut_plains_3_vine_block_1 = "Donut Plains 3 - Vine Block #1"
+donut_plains_3_powerup_block_1 = "Donut Plains 3 - Powerup Block #1"
+donut_plains_3_bonus_block_1 = "Donut Plains 3 - Bonus Block #1"
+donut_plains_4_coin_block_1 = "Donut Plains 4 - Coin Block #1"
+donut_plains_4_powerup_block_1 = "Donut Plains 4 - Powerup Block #1"
+donut_plains_4_coin_block_2 = "Donut Plains 4 - Coin Block #2"
+donut_plains_4_yoshi_block_1 = "Donut Plains 4 - Yoshi Block #1"
+donut_plains_castle_yellow_block_1 = "#2 Morton's Castle - Yellow Switch Palace Block #1"
+donut_plains_castle_coin_block_1 = "#2 Morton's Castle - Coin Block #1"
+donut_plains_castle_powerup_block_1 = "#2 Morton's Castle - Powerup Block #1"
+donut_plains_castle_coin_block_2 = "#2 Morton's Castle - Coin Block #2"
+donut_plains_castle_vine_block_1 = "#2 Morton's Castle - Vine Block #1"
+donut_plains_castle_invis_life_block_1 = "#2 Morton's Castle - Invisible 1-Up Mushroom Block #1"
+donut_plains_castle_coin_block_3 = "#2 Morton's Castle - Coin Block #3"
+donut_plains_castle_coin_block_4 = "#2 Morton's Castle - Coin Block #4"
+donut_plains_castle_coin_block_5 = "#2 Morton's Castle - Coin Block #5"
+donut_plains_castle_green_block_1 = "#2 Morton's Castle - Green Switch Palace Block #1"
+donut_plains_2_coin_block_1 = "Donut Plains 2 - Coin Block #1"
+donut_plains_2_coin_block_2 = "Donut Plains 2 - Coin Block #2"
+donut_plains_2_coin_block_3 = "Donut Plains 2 - Coin Block #3"
+donut_plains_2_yellow_block_1 = "Donut Plains 2 - Yellow Switch Palace Block #1"
+donut_plains_2_powerup_block_1 = "Donut Plains 2 - Powerup Block #1"
+donut_plains_2_multi_coin_block_1 = "Donut Plains 2 - Multi Coin Block #1"
+donut_plains_2_flying_block_1 = "Donut Plains 2 - Flying Question Block #1"
+donut_plains_2_green_block_1 = "Donut Plains 2 - Green Switch Palace Block #1"
+donut_plains_2_yellow_block_2 = "Donut Plains 2 - Yellow Switch Palace Block #2"
+donut_plains_2_vine_block_1 = "Donut Plains 2 - Vine Block #1"
+donut_secret_1_coin_block_1 = "Donut Secret 1 - Coin Block #1"
+donut_secret_1_coin_block_2 = "Donut Secret 1 - Coin Block #2"
+donut_secret_1_powerup_block_1 = "Donut Secret 1 - Powerup Block #1"
+donut_secret_1_coin_block_3 = "Donut Secret 1 - Coin Block #3"
+donut_secret_1_powerup_block_2 = "Donut Secret 1 - Powerup Block #2"
+donut_secret_1_powerup_block_3 = "Donut Secret 1 - Powerup Block #3"
+donut_secret_1_life_block_1 = "Donut Secret 1 - 1-Up Mushroom Block #1"
+donut_secret_1_powerup_block_4 = "Donut Secret 1 - Powerup Block #4"
+donut_secret_1_powerup_block_5 = "Donut Secret 1 - Powerup Block #5"
+donut_secret_1_key_block_1 = "Donut Secret 1 - Key Block #1"
+vanilla_fortress_powerup_block_1 = "Vanilla Fortress - Powerup Block #1"
+vanilla_fortress_powerup_block_2 = "Vanilla Fortress - Powerup Block #2"
+vanilla_fortress_yellow_block_1 = "Vanilla Fortress - Yellow Switch Palace Block #1"
+butter_bridge_1_powerup_block_1 = "Butter Bridge 1 - Powerup Block #1"
+butter_bridge_1_multi_coin_block_1 = "Butter Bridge 1 - Multi Coin Block #1"
+butter_bridge_1_multi_coin_block_2 = "Butter Bridge 1 - Multi Coin Block #2"
+butter_bridge_1_multi_coin_block_3 = "Butter Bridge 1 - Multi Coin Block #3"
+butter_bridge_1_life_block_1 = "Butter Bridge 1 - 1-Up Mushroom Block #1"
+butter_bridge_1_bonus_block_1 = "Butter Bridge 1 - Bonus Block #1"
+butter_bridge_2_powerup_block_1 = "Butter Bridge 2 - Powerup Block #1"
+butter_bridge_2_green_block_1 = "Butter Bridge 2 - Green Switch Palace Block #1"
+butter_bridge_2_yoshi_block_1 = "Butter Bridge 2 - Yoshi Block #1"
+twin_bridges_castle_powerup_block_1 = "#4 Ludwig Castle - Powerup Block #1"
+cheese_bridge_powerup_block_1 = "Cheese Bridge Area - Powerup Block #1"
+cheese_bridge_powerup_block_2 = "Cheese Bridge Area - Powerup Block #2"
+cheese_bridge_wings_block_1 = "Cheese Bridge Area - Wings Block #1"
+cheese_bridge_powerup_block_3 = "Cheese Bridge Area - Powerup Block #3"
+cookie_mountain_coin_block_1 = "Cookie Mountain - Coin Block #1"
+cookie_mountain_coin_block_2 = "Cookie Mountain - Coin Block #2"
+cookie_mountain_coin_block_3 = "Cookie Mountain - Coin Block #3"
+cookie_mountain_coin_block_4 = "Cookie Mountain - Coin Block #4"
+cookie_mountain_coin_block_5 = "Cookie Mountain - Coin Block #5"
+cookie_mountain_coin_block_6 = "Cookie Mountain - Coin Block #6"
+cookie_mountain_coin_block_7 = "Cookie Mountain - Coin Block #7"
+cookie_mountain_coin_block_8 = "Cookie Mountain - Coin Block #8"
+cookie_mountain_coin_block_9 = "Cookie Mountain - Coin Block #9"
+cookie_mountain_powerup_block_1 = "Cookie Mountain - Powerup Block #1"
+cookie_mountain_life_block_1 = "Cookie Mountain - 1-Up Mushroom Block #1"
+cookie_mountain_vine_block_1 = "Cookie Mountain - Vine Block #1"
+cookie_mountain_yoshi_block_1 = "Cookie Mountain - Yoshi Block #1"
+cookie_mountain_coin_block_10 = "Cookie Mountain - Coin Block #10"
+cookie_mountain_coin_block_11 = "Cookie Mountain - Coin Block #11"
+cookie_mountain_powerup_block_2 = "Cookie Mountain - Powerup Block #2"
+cookie_mountain_coin_block_12 = "Cookie Mountain - Coin Block #12"
+cookie_mountain_coin_block_13 = "Cookie Mountain - Coin Block #13"
+cookie_mountain_coin_block_14 = "Cookie Mountain - Coin Block #14"
+cookie_mountain_coin_block_15 = "Cookie Mountain - Coin Block #15"
+cookie_mountain_coin_block_16 = "Cookie Mountain - Coin Block #16"
+cookie_mountain_coin_block_17 = "Cookie Mountain - Coin Block #17"
+cookie_mountain_coin_block_18 = "Cookie Mountain - Coin Block #18"
+cookie_mountain_coin_block_19 = "Cookie Mountain - Coin Block #19"
+cookie_mountain_coin_block_20 = "Cookie Mountain - Coin Block #20"
+cookie_mountain_coin_block_21 = "Cookie Mountain - Coin Block #21"
+cookie_mountain_coin_block_22 = "Cookie Mountain - Coin Block #22"
+cookie_mountain_coin_block_23 = "Cookie Mountain - Coin Block #23"
+cookie_mountain_coin_block_24 = "Cookie Mountain - Coin Block #24"
+cookie_mountain_coin_block_25 = "Cookie Mountain - Coin Block #25"
+cookie_mountain_coin_block_26 = "Cookie Mountain - Coin Block #26"
+cookie_mountain_coin_block_27 = "Cookie Mountain - Coin Block #27"
+cookie_mountain_coin_block_28 = "Cookie Mountain - Coin Block #28"
+cookie_mountain_coin_block_29 = "Cookie Mountain - Coin Block #29"
+cookie_mountain_coin_block_30 = "Cookie Mountain - Coin Block #30"
+soda_lake_powerup_block_1 = "Soda Lake - Powerup Block #1"
+donut_secret_house_powerup_block_1 = "Donut Secret House - Powerup Block #1"
+donut_secret_house_multi_coin_block_1 = "Donut Secret House - Multi Coin Block #1"
+donut_secret_house_life_block_1 = "Donut Secret House - 1-Up Mushroom Block #1"
+donut_secret_house_vine_block_1 = "Donut Secret House - Vine Block #1"
+donut_secret_house_directional_coin_block_1 = "Donut Secret House - Directional Coin Block #1"
+donut_plains_1_coin_block_1 = "Donut Plains 1 - Coin Block #1"
+donut_plains_1_coin_block_2 = "Donut Plains 1 - Coin Block #2"
+donut_plains_1_yoshi_block_1 = "Donut Plains 1 - Yoshi Block #1"
+donut_plains_1_vine_block_1 = "Donut Plains 1 - Vine Block #1"
+donut_plains_1_green_block_1 = "Donut Plains 1 - Green Switch Palace Block #1"
+donut_plains_1_green_block_2 = "Donut Plains 1 - Green Switch Palace Block #2"
+donut_plains_1_green_block_3 = "Donut Plains 1 - Green Switch Palace Block #3"
+donut_plains_1_green_block_4 = "Donut Plains 1 - Green Switch Palace Block #4"
+donut_plains_1_green_block_5 = "Donut Plains 1 - Green Switch Palace Block #5"
+donut_plains_1_green_block_6 = "Donut Plains 1 - Green Switch Palace Block #6"
+donut_plains_1_green_block_7 = "Donut Plains 1 - Green Switch Palace Block #7"
+donut_plains_1_green_block_8 = "Donut Plains 1 - Green Switch Palace Block #8"
+donut_plains_1_green_block_9 = "Donut Plains 1 - Green Switch Palace Block #9"
+donut_plains_1_green_block_10 = "Donut Plains 1 - Green Switch Palace Block #10"
+donut_plains_1_green_block_11 = "Donut Plains 1 - Green Switch Palace Block #11"
+donut_plains_1_green_block_12 = "Donut Plains 1 - Green Switch Palace Block #12"
+donut_plains_1_green_block_13 = "Donut Plains 1 - Green Switch Palace Block #13"
+donut_plains_1_green_block_14 = "Donut Plains 1 - Green Switch Palace Block #14"
+donut_plains_1_green_block_15 = "Donut Plains 1 - Green Switch Palace Block #15"
+donut_plains_1_green_block_16 = "Donut Plains 1 - Green Switch Palace Block #16"
+donut_plains_1_yellow_block_1 = "Donut Plains 1 - Yellow Switch Palace Block #1"
+donut_plains_1_yellow_block_2 = "Donut Plains 1 - Yellow Switch Palace Block #2"
+donut_plains_1_yellow_block_3 = "Donut Plains 1 - Yellow Switch Palace Block #3"
+sunken_ghost_ship_powerup_block_1 = "Sunken Ghost Ship - Powerup Block #1"
+sunken_ghost_ship_star_block_1 = "Sunken Ghost Ship - Star Block #1"
+chocolate_castle_yellow_block_1 = "#6 Wendy's Castle - Yellow Switch Palace Block #1"
+chocolate_castle_yellow_block_2 = "#6 Wendy's Castle - Yellow Switch Palace Block #2"
+chocolate_castle_green_block_1 = "#6 Wendy's Castle - Green Switch Palace Block #1"
+chocolate_fortress_powerup_block_1 = "Chocolate Fortress - Powerup Block #1"
+chocolate_fortress_powerup_block_2 = "Chocolate Fortress - Powerup Block #2"
+chocolate_fortress_coin_block_1 = "Chocolate Fortress - Coin Block #1"
+chocolate_fortress_coin_block_2 = "Chocolate Fortress - Coin Block #2"
+chocolate_fortress_green_block_1 = "Chocolate Fortress - Green Switch Palace Block #1"
+chocolate_island_5_yoshi_block_1 = "Chocolate Island 5 - Yoshi Block #1"
+chocolate_island_5_powerup_block_1 = "Chocolate Island 5 - Powerup Block #1"
+chocolate_island_5_life_block_1 = "Chocolate Island 5 - 1-Up Mushroom Block #1"
+chocolate_island_5_yellow_block_1 = "Chocolate Island 5 - Yellow Switch Palace Block #1"
+chocolate_island_4_yellow_block_1 = "Chocolate Island 4 - Yellow Switch Palace Block #1"
+chocolate_island_4_blue_pow_block_1 = "Chocolate Island 4 - Blue P-Switch Block #1"
+chocolate_island_4_powerup_block_1 = "Chocolate Island 4 - Powerup Block #1"
+forest_fortress_yellow_block_1 = "Forest Fortress - Yellow Switch Palace Block #1"
+forest_fortress_powerup_block_1 = "Forest Fortress - Powerup Block #1"
+forest_fortress_life_block_1 = "Forest Fortress - 1-Up Mushroom Block #1"
+forest_fortress_life_block_2 = "Forest Fortress - 1-Up Mushroom Block #2"
+forest_fortress_life_block_3 = "Forest Fortress - 1-Up Mushroom Block #3"
+forest_fortress_life_block_4 = "Forest Fortress - 1-Up Mushroom Block #4"
+forest_fortress_life_block_5 = "Forest Fortress - 1-Up Mushroom Block #5"
+forest_fortress_life_block_6 = "Forest Fortress - 1-Up Mushroom Block #6"
+forest_fortress_life_block_7 = "Forest Fortress - 1-Up Mushroom Block #7"
+forest_fortress_life_block_8 = "Forest Fortress - 1-Up Mushroom Block #8"
+forest_fortress_life_block_9 = "Forest Fortress - 1-Up Mushroom Block #9"
+forest_castle_green_block_1 = "#5 Roy's Castle - Green Switch Palace Block #1"
+chocolate_ghost_house_powerup_block_1 = "Choco Ghost House - Powerup Block #1"
+chocolate_ghost_house_powerup_block_2 = "Choco Ghost House - Powerup Block #2"
+chocolate_ghost_house_life_block_1 = "Choco Ghost House - 1-Up Mushroom Block #1"
+chocolate_island_1_flying_block_1 = "Chocolate Island 1 - Flying Question Block #1"
+chocolate_island_1_flying_block_2 = "Chocolate Island 1 - Flying Question Block #2"
+chocolate_island_1_yoshi_block_1 = "Chocolate Island 1 - Yoshi Block #1"
+chocolate_island_1_green_block_1 = "Chocolate Island 1 - Green|Yellow Switch Palace Block #1"
+chocolate_island_1_life_block_1 = "Chocolate Island 1 - 1-Up Mushroom Block #1"
+chocolate_island_3_powerup_block_1 = "Chocolate Island 3 - Powerup Block #1"
+chocolate_island_3_powerup_block_2 = "Chocolate Island 3 - Powerup Block #2"
+chocolate_island_3_powerup_block_3 = "Chocolate Island 3 - Powerup Block #3"
+chocolate_island_3_green_block_1 = "Chocolate Island 3 - Green Switch Palace Block #1"
+chocolate_island_3_bonus_block_1 = "Chocolate Island 3 - Bonus Block #1"
+chocolate_island_3_vine_block_1 = "Chocolate Island 3 - Vine Block #1"
+chocolate_island_3_life_block_1 = "Chocolate Island 3 - 1-Up Mushroom Block #1"
+chocolate_island_3_life_block_2 = "Chocolate Island 3 - 1-Up Mushroom Block #2"
+chocolate_island_3_life_block_3 = "Chocolate Island 3 - 1-Up Mushroom Block #3"
+chocolate_island_2_multi_coin_block_1 = "Chocolate Island 2 - Multi Coin Block #1"
+chocolate_island_2_invis_coin_block_1 = "Chocolate Island 2 - Invisible Coin Block #1"
+chocolate_island_2_yoshi_block_1 = "Chocolate Island 2 - Yoshi Block #1"
+chocolate_island_2_coin_block_1 = "Chocolate Island 2 - Coin Block #1"
+chocolate_island_2_coin_block_2 = "Chocolate Island 2 - Coin Block #2"
+chocolate_island_2_multi_coin_block_2 = "Chocolate Island 2 - Multi Coin Block #2"
+chocolate_island_2_powerup_block_1 = "Chocolate Island 2 - Powerup Block #1"
+chocolate_island_2_blue_pow_block_1 = "Chocolate Island 2 - Blue P-Switch Block #1"
+chocolate_island_2_yellow_block_1 = "Chocolate Island 2 - Yellow Switch Palace Block #1"
+chocolate_island_2_yellow_block_2 = "Chocolate Island 2 - Yellow Switch Palace Block #2"
+chocolate_island_2_green_block_1 = "Chocolate Island 2 - Green Switch Palace Block #1"
+chocolate_island_2_green_block_2 = "Chocolate Island 2 - Green Switch Palace Block #2"
+chocolate_island_2_green_block_3 = "Chocolate Island 2 - Green Switch Palace Block #3"
+chocolate_island_2_green_block_4 = "Chocolate Island 2 - Green Switch Palace Block #4"
+chocolate_island_2_green_block_5 = "Chocolate Island 2 - Green Switch Palace Block #5"
+chocolate_island_2_green_block_6 = "Chocolate Island 2 - Green Switch Palace Block #6"
+yoshis_island_castle_coin_block_1 = "#1 Iggy's Castle - Coin Block #1"
+yoshis_island_castle_coin_block_2 = "#1 Iggy's Castle - Coin Block #2"
+yoshis_island_castle_powerup_block_1 = "#1 Iggy's Castle - Powerup Block #1"
+yoshis_island_castle_coin_block_3 = "#1 Iggy's Castle - Coin Block #3"
+yoshis_island_castle_coin_block_4 = "#1 Iggy's Castle - Coin Block #4"
+yoshis_island_castle_flying_block_1 = "#1 Iggy's Castle - Flying Question Block #1"
+yoshis_island_4_yellow_block_1 = "Yoshi's Island 4 - Yellow Switch Palace Block #1"
+yoshis_island_4_powerup_block_1 = "Yoshi's Island 4 - Powerup Block #1"
+yoshis_island_4_multi_coin_block_1 = "Yoshi's Island 4 - Multi Coin Block #1"
+yoshis_island_4_star_block_1 = "Yoshi's Island 4 - Star Block #1"
+yoshis_island_3_yellow_block_1 = "Yoshi's Island 3 - Yellow Switch Palace Block #1"
+yoshis_island_3_yellow_block_2 = "Yoshi's Island 3 - Yellow Switch Palace Block #2"
+yoshis_island_3_yellow_block_3 = "Yoshi's Island 3 - Yellow Switch Palace Block #3"
+yoshis_island_3_yellow_block_4 = "Yoshi's Island 3 - Yellow Switch Palace Block #4"
+yoshis_island_3_yellow_block_5 = "Yoshi's Island 3 - Yellow Switch Palace Block #5"
+yoshis_island_3_yellow_block_6 = "Yoshi's Island 3 - Yellow Switch Palace Block #6"
+yoshis_island_3_yellow_block_7 = "Yoshi's Island 3 - Yellow Switch Palace Block #7"
+yoshis_island_3_yellow_block_8 = "Yoshi's Island 3 - Yellow Switch Palace Block #8"
+yoshis_island_3_yellow_block_9 = "Yoshi's Island 3 - Yellow Switch Palace Block #9"
+yoshis_island_3_coin_block_1 = "Yoshi's Island 3 - Coin Block #1"
+yoshis_island_3_yoshi_block_1 = "Yoshi's Island 3 - Yoshi Block #1"
+yoshis_island_3_coin_block_2 = "Yoshi's Island 3 - Coin Block #2"
+yoshis_island_3_powerup_block_1 = "Yoshi's Island 3 - Powerup Block #1"
+yoshis_island_3_yellow_block_10 = "Yoshi's Island 3 - Yellow Switch Palace Block #10"
+yoshis_island_3_yellow_block_11 = "Yoshi's Island 3 - Yellow Switch Palace Block #11"
+yoshis_island_3_yellow_block_12 = "Yoshi's Island 3 - Yellow Switch Palace Block #12"
+yoshis_island_3_bonus_block_1 = "Yoshi's Island 3 - Bonus Block #1"
+yoshis_island_1_flying_block_1 = "Yoshi's Island 1 - Flying Question Block #1"
+yoshis_island_1_yellow_block_1 = "Yoshi's Island 1 - Yellow Switch Palace Block #1"
+yoshis_island_1_life_block_1 = "Yoshi's Island 1 - 1-Up Mushroom Block #1"
+yoshis_island_1_powerup_block_1 = "Yoshi's Island 1 - Powerup Block #1"
+yoshis_island_2_flying_block_1 = "Yoshi's Island 2 - Flying Question Block #1"
+yoshis_island_2_flying_block_2 = "Yoshi's Island 2 - Flying Question Block #2"
+yoshis_island_2_flying_block_3 = "Yoshi's Island 2 - Flying Question Block #3"
+yoshis_island_2_flying_block_4 = "Yoshi's Island 2 - Flying Question Block #4"
+yoshis_island_2_flying_block_5 = "Yoshi's Island 2 - Flying Question Block #5"
+yoshis_island_2_flying_block_6 = "Yoshi's Island 2 - Flying Question Block #6"
+yoshis_island_2_coin_block_1 = "Yoshi's Island 2 - Coin Block #1"
+yoshis_island_2_yellow_block_1 = "Yoshi's Island 2 - Yellow Switch Palace Block #1"
+yoshis_island_2_coin_block_2 = "Yoshi's Island 2 - Coin Block #2"
+yoshis_island_2_coin_block_3 = "Yoshi's Island 2 - Coin Block #3"
+yoshis_island_2_yoshi_block_1 = "Yoshi's Island 2 - Yoshi Block #1"
+yoshis_island_2_coin_block_4 = "Yoshi's Island 2 - Coin Block #4"
+yoshis_island_2_yoshi_block_2 = "Yoshi's Island 2 - Yoshi Block #2"
+yoshis_island_2_coin_block_5 = "Yoshi's Island 2 - Coin Block #5"
+yoshis_island_2_vine_block_1 = "Yoshi's Island 2 - Vine Block #1"
+yoshis_island_2_yellow_block_2 = "Yoshi's Island 2 - Yellow Switch Palace Block #2"
+vanilla_ghost_house_powerup_block_1 = "Vanilla Ghost House - Powerup Block #1"
+vanilla_ghost_house_vine_block_1 = "Vanilla Ghost House - Vine Block #1"
+vanilla_ghost_house_powerup_block_2 = "Vanilla Ghost House - Powerup Block #2"
+vanilla_ghost_house_multi_coin_block_1 = "Vanilla Ghost House - Multi Coin Block #1"
+vanilla_ghost_house_blue_pow_block_1 = "Vanilla Ghost House - Blue P-Switch Block #1"
+vanilla_secret_1_coin_block_1 = "Vanilla Secret 1 - Coin Block #1"
+vanilla_secret_1_powerup_block_1 = "Vanilla Secret 1 - Powerup Block #1"
+vanilla_secret_1_multi_coin_block_1 = "Vanilla Secret 1 - Multi Coin Block #1"
+vanilla_secret_1_vine_block_1 = "Vanilla Secret 1 - Vine Block #1"
+vanilla_secret_1_vine_block_2 = "Vanilla Secret 1 - Vine Block #2"
+vanilla_secret_1_coin_block_2 = "Vanilla Secret 1 - Coin Block #2"
+vanilla_secret_1_coin_block_3 = "Vanilla Secret 1 - Coin Block #3"
+vanilla_secret_1_powerup_block_2 = "Vanilla Secret 1 - Powerup Block #2"
+vanilla_dome_3_coin_block_1 = "Vanilla Dome 3 - Coin Block #1"
+vanilla_dome_3_flying_block_1 = "Vanilla Dome 3 - Flying Question Block #1"
+vanilla_dome_3_flying_block_2 = "Vanilla Dome 3 - Flying Question Block #2"
+vanilla_dome_3_powerup_block_1 = "Vanilla Dome 3 - Powerup Block #1"
+vanilla_dome_3_flying_block_3 = "Vanilla Dome 3 - Flying Question Block #3"
+vanilla_dome_3_invis_coin_block_1 = "Vanilla Dome 3 - Invisible Coin Block #1"
+vanilla_dome_3_powerup_block_2 = "Vanilla Dome 3 - Powerup Block #2"
+vanilla_dome_3_multi_coin_block_1 = "Vanilla Dome 3 - Multi Coin Block #1"
+vanilla_dome_3_powerup_block_3 = "Vanilla Dome 3 - Powerup Block #3"
+vanilla_dome_3_yoshi_block_1 = "Vanilla Dome 3 - Yoshi Block #1"
+vanilla_dome_3_powerup_block_4 = "Vanilla Dome 3 - Powerup Block #4"
+vanilla_dome_3_pswitch_coin_block_1 = "Vanilla Dome 3 - P-Switch Coin Block #1"
+vanilla_dome_3_pswitch_coin_block_2 = "Vanilla Dome 3 - P-Switch Coin Block #2"
+vanilla_dome_3_pswitch_coin_block_3 = "Vanilla Dome 3 - P-Switch Coin Block #3"
+vanilla_dome_3_pswitch_coin_block_4 = "Vanilla Dome 3 - P-Switch Coin Block #4"
+vanilla_dome_3_pswitch_coin_block_5 = "Vanilla Dome 3 - P-Switch Coin Block #5"
+vanilla_dome_3_pswitch_coin_block_6 = "Vanilla Dome 3 - P-Switch Coin Block #6"
+donut_secret_2_directional_coin_block_1 = "Donut Secret 2 - Directional Coin Block #1"
+donut_secret_2_vine_block_1 = "Donut Secret 2 - Vine Block #1"
+donut_secret_2_star_block_1 = "Donut Secret 2 - Star Block #1"
+donut_secret_2_powerup_block_1 = "Donut Secret 2 - Powerup Block #1"
+donut_secret_2_star_block_2 = "Donut Secret 2 - Star Block #2"
+valley_of_bowser_4_yellow_block_1 = "Valley of Bowser 4 - Yellow Switch Palace Block #1"
+valley_of_bowser_4_powerup_block_1 = "Valley of Bowser 4 - Powerup Block #1"
+valley_of_bowser_4_vine_block_1 = "Valley of Bowser 4 - Vine Block #1"
+valley_of_bowser_4_yoshi_block_1 = "Valley of Bowser 4 - Yoshi Block #1"
+valley_of_bowser_4_life_block_1 = "Valley of Bowser 4 - 1-Up Mushroom Block #1"
+valley_of_bowser_4_powerup_block_2 = "Valley of Bowser 4 - Powerup Block #2"
+valley_castle_yellow_block_1 = "#7 Larry's Castle - Yellow Switch Palace Block #1"
+valley_castle_yellow_block_2 = "#7 Larry's Castle - Yellow Switch Palace Block #2"
+valley_castle_green_block_1 = "#7 Larry's Castle - Green Switch Palace Block #1"
+valley_fortress_green_block_1 = "Valley Fortress - Green Switch Palace Block #1"
+valley_fortress_yellow_block_1 = "Valley Fortress - Yellow Switch Palace Block #1"
+valley_of_bowser_3_powerup_block_1 = "Valley of Bowser 3 - Powerup Block #1"
+valley_of_bowser_3_powerup_block_2 = "Valley of Bowser 3 - Powerup Block #2"
+valley_ghost_house_pswitch_coin_block_1 = "Valley Ghost House - P-Switch Coin Block #1"
+valley_ghost_house_multi_coin_block_1 = "Valley Ghost House - Multi Coin Block #1"
+valley_ghost_house_powerup_block_1 = "Valley Ghost House - Powerup Block #1"
+valley_ghost_house_directional_coin_block_1 = "Valley Ghost House - Directional Coin Block #1"
+valley_of_bowser_2_powerup_block_1 = "Valley of Bowser 2 - Powerup Block #1"
+valley_of_bowser_2_yellow_block_1 = "Valley of Bowser 2 - Yellow Switch Palace Block #1"
+valley_of_bowser_2_powerup_block_2 = "Valley of Bowser 2 - Powerup Block #2"
+valley_of_bowser_2_wings_block_1 = "Valley of Bowser 2 - Wings Block #1"
+valley_of_bowser_1_green_block_1 = "Valley of Bowser 1 - Green Switch Palace Block #1"
+valley_of_bowser_1_invis_coin_block_1 = "Valley of Bowser 1 - Invisible Coin Block #1"
+valley_of_bowser_1_invis_coin_block_2 = "Valley of Bowser 1 - Invisible Coin Block #2"
+valley_of_bowser_1_invis_coin_block_3 = "Valley of Bowser 1 - Invisible Coin Block #3"
+valley_of_bowser_1_yellow_block_1 = "Valley of Bowser 1 - Yellow Switch Palace Block #1"
+valley_of_bowser_1_yellow_block_2 = "Valley of Bowser 1 - Yellow Switch Palace Block #2"
+valley_of_bowser_1_yellow_block_3 = "Valley of Bowser 1 - Yellow Switch Palace Block #3"
+valley_of_bowser_1_yellow_block_4 = "Valley of Bowser 1 - Yellow Switch Palace Block #4"
+valley_of_bowser_1_vine_block_1 = "Valley of Bowser 1 - Vine Block #1"
+chocolate_secret_powerup_block_1 = "Chocolate Secret - Powerup Block #1"
+chocolate_secret_powerup_block_2 = "Chocolate Secret - Powerup Block #2"
+vanilla_dome_2_coin_block_1 = "Vanilla Dome 2 - Coin Block #1"
+vanilla_dome_2_powerup_block_1 = "Vanilla Dome 2 - Powerup Block #1"
+vanilla_dome_2_coin_block_2 = "Vanilla Dome 2 - Coin Block #2"
+vanilla_dome_2_coin_block_3 = "Vanilla Dome 2 - Coin Block #3"
+vanilla_dome_2_vine_block_1 = "Vanilla Dome 2 - Vine Block #1"
+vanilla_dome_2_invis_life_block_1 = "Vanilla Dome 2 - Invisible 1-Up Mushroom Block #1"
+vanilla_dome_2_coin_block_4 = "Vanilla Dome 2 - Coin Block #4"
+vanilla_dome_2_coin_block_5 = "Vanilla Dome 2 - Coin Block #5"
+vanilla_dome_2_powerup_block_2 = "Vanilla Dome 2 - Powerup Block #2"
+vanilla_dome_2_powerup_block_3 = "Vanilla Dome 2 - Powerup Block #3"
+vanilla_dome_2_powerup_block_4 = "Vanilla Dome 2 - Powerup Block #4"
+vanilla_dome_2_powerup_block_5 = "Vanilla Dome 2 - Powerup Block #5"
+vanilla_dome_2_multi_coin_block_1 = "Vanilla Dome 2 - Multi Coin Block #1"
+vanilla_dome_2_multi_coin_block_2 = "Vanilla Dome 2 - Multi Coin Block #2"
+vanilla_dome_4_powerup_block_1 = "Vanilla Dome 4 - Powerup Block #1"
+vanilla_dome_4_powerup_block_2 = "Vanilla Dome 4 - Powerup Block #2"
+vanilla_dome_4_coin_block_1 = "Vanilla Dome 4 - Coin Block #1"
+vanilla_dome_4_coin_block_2 = "Vanilla Dome 4 - Coin Block #2"
+vanilla_dome_4_coin_block_3 = "Vanilla Dome 4 - Coin Block #3"
+vanilla_dome_4_life_block_1 = "Vanilla Dome 4 - 1-Up Mushroom Block #1"
+vanilla_dome_4_coin_block_4 = "Vanilla Dome 4 - Coin Block #4"
+vanilla_dome_4_coin_block_5 = "Vanilla Dome 4 - Coin Block #5"
+vanilla_dome_4_coin_block_6 = "Vanilla Dome 4 - Coin Block #6"
+vanilla_dome_4_coin_block_7 = "Vanilla Dome 4 - Coin Block #7"
+vanilla_dome_4_coin_block_8 = "Vanilla Dome 4 - Coin Block #8"
+vanilla_dome_1_flying_block_1 = "Vanilla Dome 1 - Flying Question Block #1"
+vanilla_dome_1_powerup_block_1 = "Vanilla Dome 1 - Powerup Block #1"
+vanilla_dome_1_powerup_block_2 = "Vanilla Dome 1 - Powerup Block #2"
+vanilla_dome_1_coin_block_1 = "Vanilla Dome 1 - Coin Block #1"
+vanilla_dome_1_life_block_1 = "Vanilla Dome 1 - 1-Up Mushroom Block #1"
+vanilla_dome_1_powerup_block_3 = "Vanilla Dome 1 - Powerup Block #3"
+vanilla_dome_1_vine_block_1 = "Vanilla Dome 1 - Vine Block #1"
+vanilla_dome_1_star_block_1 = "Vanilla Dome 1 - Star Block #1"
+vanilla_dome_1_powerup_block_4 = "Vanilla Dome 1 - Powerup Block #4"
+vanilla_dome_1_coin_block_2 = "Vanilla Dome 1 - Coin Block #2"
+vanilla_dome_castle_life_block_1 = "#3 Lemmy's Castle - 1-Up Mushroom Block #1"
+vanilla_dome_castle_life_block_2 = "#3 Lemmy's Castle - 1-Up Mushroom Block #2"
+vanilla_dome_castle_powerup_block_1 = "#3 Lemmy's Castle - Powerup Block #1"
+vanilla_dome_castle_life_block_3 = "#3 Lemmy's Castle - 1-Up Mushroom Block #3"
+vanilla_dome_castle_green_block_1 = "#3 Lemmy's Castle - Green Switch Palace Block #1"
+forest_ghost_house_coin_block_1 = "Forest Ghost House - Coin Block #1"
+forest_ghost_house_powerup_block_1 = "Forest Ghost House - Powerup Block #1"
+forest_ghost_house_flying_block_1 = "Forest Ghost House - Flying Question Block #1"
+forest_ghost_house_powerup_block_2 = "Forest Ghost House - Powerup Block #2"
+forest_ghost_house_life_block_1 = "Forest Ghost House - 1-Up Mushroom Block #1"
+forest_of_illusion_1_powerup_block_1 = "Forest of Illusion 1 - Powerup Block #1"
+forest_of_illusion_1_yoshi_block_1 = "Forest of Illusion 1 - Yoshi Block #1"
+forest_of_illusion_1_powerup_block_2 = "Forest of Illusion 1 - Powerup Block #2"
+forest_of_illusion_1_key_block_1 = "Forest of Illusion 1 - Key Block #1"
+forest_of_illusion_1_life_block_1 = "Forest of Illusion 1 - 1-Up Mushroom Block #1"
+forest_of_illusion_4_multi_coin_block_1 = "Forest of Illusion 4 - Multi Coin Block #1"
+forest_of_illusion_4_coin_block_1 = "Forest of Illusion 4 - Coin Block #1"
+forest_of_illusion_4_coin_block_2 = "Forest of Illusion 4 - Coin Block #2"
+forest_of_illusion_4_coin_block_3 = "Forest of Illusion 4 - Coin Block #3"
+forest_of_illusion_4_coin_block_4 = "Forest of Illusion 4 - Coin Block #4"
+forest_of_illusion_4_powerup_block_1 = "Forest of Illusion 4 - Powerup Block #1"
+forest_of_illusion_4_coin_block_5 = "Forest of Illusion 4 - Coin Block #5"
+forest_of_illusion_4_coin_block_6 = "Forest of Illusion 4 - Coin Block #6"
+forest_of_illusion_4_coin_block_7 = "Forest of Illusion 4 - Coin Block #7"
+forest_of_illusion_4_powerup_block_2 = "Forest of Illusion 4 - Powerup Block #2"
+forest_of_illusion_4_coin_block_8 = "Forest of Illusion 4 - Coin Block #8"
+forest_of_illusion_4_coin_block_9 = "Forest of Illusion 4 - Coin Block #9"
+forest_of_illusion_4_coin_block_10 = "Forest of Illusion 4 - Coin Block #10"
+forest_of_illusion_2_green_block_1 = "Forest of Illusion 2 - Green Switch Palace Block #1"
+forest_of_illusion_2_powerup_block_1 = "Forest of Illusion 2 - Powerup Block #1"
+forest_of_illusion_2_invis_coin_block_1 = "Forest of Illusion 2 - Invisible Coin Block #1"
+forest_of_illusion_2_invis_coin_block_2 = "Forest of Illusion 2 - Invisible Coin Block #2"
+forest_of_illusion_2_invis_life_block_1 = "Forest of Illusion 2 - Invisible 1-Up Mushroom Block #1"
+forest_of_illusion_2_invis_coin_block_3 = "Forest of Illusion 2 - Invisible Coin Block #3"
+forest_of_illusion_2_yellow_block_1 = "Forest of Illusion 2 - Yellow Switch Palace Block #1"
+forest_secret_powerup_block_1 = "Forest Secret Area - Powerup Block #1"
+forest_secret_powerup_block_2 = "Forest Secret Area - Powerup Block #2"
+forest_secret_life_block_1 = "Forest Secret Area - 1-Up Mushroom Block #1"
+forest_of_illusion_3_yoshi_block_1 = "Forest of Illusion 3 - Yoshi Block #1"
+forest_of_illusion_3_coin_block_1 = "Forest of Illusion 3 - Coin Block #1"
+forest_of_illusion_3_multi_coin_block_1 = "Forest of Illusion 3 - Multi Coin Block #1"
+forest_of_illusion_3_coin_block_2 = "Forest of Illusion 3 - Coin Block #2"
+forest_of_illusion_3_multi_coin_block_2 = "Forest of Illusion 3 - Multi Coin Block #2"
+forest_of_illusion_3_coin_block_3 = "Forest of Illusion 3 - Coin Block #3"
+forest_of_illusion_3_coin_block_4 = "Forest of Illusion 3 - Coin Block #4"
+forest_of_illusion_3_coin_block_5 = "Forest of Illusion 3 - Coin Block #5"
+forest_of_illusion_3_coin_block_6 = "Forest of Illusion 3 - Coin Block #6"
+forest_of_illusion_3_coin_block_7 = "Forest of Illusion 3 - Coin Block #7"
+forest_of_illusion_3_coin_block_8 = "Forest of Illusion 3 - Coin Block #8"
+forest_of_illusion_3_coin_block_9 = "Forest of Illusion 3 - Coin Block #9"
+forest_of_illusion_3_coin_block_10 = "Forest of Illusion 3 - Coin Block #10"
+forest_of_illusion_3_coin_block_11 = "Forest of Illusion 3 - Coin Block #11"
+forest_of_illusion_3_coin_block_12 = "Forest of Illusion 3 - Coin Block #12"
+forest_of_illusion_3_coin_block_13 = "Forest of Illusion 3 - Coin Block #13"
+forest_of_illusion_3_coin_block_14 = "Forest of Illusion 3 - Coin Block #14"
+forest_of_illusion_3_coin_block_15 = "Forest of Illusion 3 - Coin Block #15"
+forest_of_illusion_3_coin_block_16 = "Forest of Illusion 3 - Coin Block #16"
+forest_of_illusion_3_coin_block_17 = "Forest of Illusion 3 - Coin Block #17"
+forest_of_illusion_3_coin_block_18 = "Forest of Illusion 3 - Coin Block #18"
+forest_of_illusion_3_coin_block_19 = "Forest of Illusion 3 - Coin Block #19"
+forest_of_illusion_3_coin_block_20 = "Forest of Illusion 3 - Coin Block #20"
+forest_of_illusion_3_coin_block_21 = "Forest of Illusion 3 - Coin Block #21"
+forest_of_illusion_3_coin_block_22 = "Forest of Illusion 3 - Coin Block #22"
+forest_of_illusion_3_coin_block_23 = "Forest of Illusion 3 - Coin Block #23"
+forest_of_illusion_3_coin_block_24 = "Forest of Illusion 3 - Coin Block #24"
+special_zone_8_yoshi_block_1 = "Funky - Yoshi Block #1"
+special_zone_8_coin_block_1 = "Funky - Coin Block #1"
+special_zone_8_coin_block_2 = "Funky - Coin Block #2"
+special_zone_8_coin_block_3 = "Funky - Coin Block #3"
+special_zone_8_coin_block_4 = "Funky - Coin Block #4"
+special_zone_8_coin_block_5 = "Funky - Coin Block #5"
+special_zone_8_blue_pow_block_1 = "Funky - Blue P-Switch Block #1"
+special_zone_8_powerup_block_1 = "Funky - Powerup Block #1"
+special_zone_8_star_block_1 = "Funky - Star Block #1"
+special_zone_8_coin_block_6 = "Funky - Coin Block #6"
+special_zone_8_coin_block_7 = "Funky - Coin Block #7"
+special_zone_8_coin_block_8 = "Funky - Coin Block #8"
+special_zone_8_coin_block_9 = "Funky - Coin Block #9"
+special_zone_8_coin_block_10 = "Funky - Coin Block #10"
+special_zone_8_coin_block_11 = "Funky - Coin Block #11"
+special_zone_8_coin_block_12 = "Funky - Coin Block #12"
+special_zone_8_coin_block_13 = "Funky - Coin Block #13"
+special_zone_8_coin_block_14 = "Funky - Coin Block #14"
+special_zone_8_coin_block_15 = "Funky - Coin Block #15"
+special_zone_8_coin_block_16 = "Funky - Coin Block #16"
+special_zone_8_coin_block_17 = "Funky - Coin Block #17"
+special_zone_8_coin_block_18 = "Funky - Coin Block #18"
+special_zone_8_multi_coin_block_1 = "Funky - Multi Coin Block #1"
+special_zone_8_coin_block_19 = "Funky - Coin Block #19"
+special_zone_8_coin_block_20 = "Funky - Coin Block #20"
+special_zone_8_coin_block_21 = "Funky - Coin Block #21"
+special_zone_8_coin_block_22 = "Funky - Coin Block #22"
+special_zone_8_coin_block_23 = "Funky - Coin Block #23"
+special_zone_8_powerup_block_2 = "Funky - Powerup Block #2"
+special_zone_8_flying_block_1 = "Funky - Flying Question Block #1"
+special_zone_7_powerup_block_1 = "Outrageous - Powerup Block #1"
+special_zone_7_yoshi_block_1 = "Outrageous - Yoshi Block #1"
+special_zone_7_coin_block_1 = "Outrageous - Coin Block #1"
+special_zone_7_powerup_block_2 = "Outrageous - Powerup Block #2"
+special_zone_7_coin_block_2 = "Outrageous - Coin Block #2"
+special_zone_6_powerup_block_1 = "Mondo - Powerup Block #1"
+special_zone_6_coin_block_1 = "Mondo - Coin Block #1"
+special_zone_6_coin_block_2 = "Mondo - Coin Block #2"
+special_zone_6_yoshi_block_1 = "Mondo - Yoshi Block #1"
+special_zone_6_life_block_1 = "Mondo - 1-Up Mushroom Block #1"
+special_zone_6_multi_coin_block_1 = "Mondo - Multi Coin Block #1"
+special_zone_6_coin_block_3 = "Mondo - Coin Block #3"
+special_zone_6_coin_block_4 = "Mondo - Coin Block #4"
+special_zone_6_coin_block_5 = "Mondo - Coin Block #5"
+special_zone_6_coin_block_6 = "Mondo - Coin Block #6"
+special_zone_6_coin_block_7 = "Mondo - Coin Block #7"
+special_zone_6_coin_block_8 = "Mondo - Coin Block #8"
+special_zone_6_coin_block_9 = "Mondo - Coin Block #9"
+special_zone_6_coin_block_10 = "Mondo - Coin Block #10"
+special_zone_6_coin_block_11 = "Mondo - Coin Block #11"
+special_zone_6_coin_block_12 = "Mondo - Coin Block #12"
+special_zone_6_coin_block_13 = "Mondo - Coin Block #13"
+special_zone_6_coin_block_14 = "Mondo - Coin Block #14"
+special_zone_6_coin_block_15 = "Mondo - Coin Block #15"
+special_zone_6_coin_block_16 = "Mondo - Coin Block #16"
+special_zone_6_coin_block_17 = "Mondo - Coin Block #17"
+special_zone_6_coin_block_18 = "Mondo - Coin Block #18"
+special_zone_6_coin_block_19 = "Mondo - Coin Block #19"
+special_zone_6_coin_block_20 = "Mondo - Coin Block #20"
+special_zone_6_coin_block_21 = "Mondo - Coin Block #21"
+special_zone_6_coin_block_22 = "Mondo - Coin Block #22"
+special_zone_6_coin_block_23 = "Mondo - Coin Block #23"
+special_zone_6_coin_block_24 = "Mondo - Coin Block #24"
+special_zone_6_coin_block_25 = "Mondo - Coin Block #25"
+special_zone_6_coin_block_26 = "Mondo - Coin Block #26"
+special_zone_6_coin_block_27 = "Mondo - Coin Block #27"
+special_zone_6_coin_block_28 = "Mondo - Coin Block #28"
+special_zone_6_powerup_block_2 = "Mondo - Powerup Block #2"
+special_zone_6_coin_block_29 = "Mondo - Coin Block #29"
+special_zone_6_coin_block_30 = "Mondo - Coin Block #30"
+special_zone_6_coin_block_31 = "Mondo - Coin Block #31"
+special_zone_6_coin_block_32 = "Mondo - Coin Block #32"
+special_zone_6_coin_block_33 = "Mondo - Coin Block #33"
+special_zone_5_yoshi_block_1 = "Groovy - Yoshi Block #1"
+special_zone_1_vine_block_1 = "Gnarly - Vine Block #1"
+special_zone_1_vine_block_2 = "Gnarly - Vine Block #2"
+special_zone_1_vine_block_3 = "Gnarly - Vine Block #3"
+special_zone_1_vine_block_4 = "Gnarly - Vine Block #4"
+special_zone_1_life_block_1 = "Gnarly - 1-Up Mushroom Block #1"
+special_zone_1_vine_block_5 = "Gnarly - Vine Block #5"
+special_zone_1_blue_pow_block_1 = "Gnarly - Blue P-Switch Block #1"
+special_zone_1_vine_block_6 = "Gnarly - Vine Block #6"
+special_zone_1_powerup_block_1 = "Gnarly - Powerup Block #1"
+special_zone_1_pswitch_coin_block_1 = "Gnarly - P-Switch Coin Block #1"
+special_zone_1_pswitch_coin_block_2 = "Gnarly - P-Switch Coin Block #2"
+special_zone_1_pswitch_coin_block_3 = "Gnarly - P-Switch Coin Block #3"
+special_zone_1_pswitch_coin_block_4 = "Gnarly - P-Switch Coin Block #4"
+special_zone_1_pswitch_coin_block_5 = "Gnarly - P-Switch Coin Block #5"
+special_zone_1_pswitch_coin_block_6 = "Gnarly - P-Switch Coin Block #6"
+special_zone_1_pswitch_coin_block_7 = "Gnarly - P-Switch Coin Block #7"
+special_zone_1_pswitch_coin_block_8 = "Gnarly - P-Switch Coin Block #8"
+special_zone_1_pswitch_coin_block_9 = "Gnarly - P-Switch Coin Block #9"
+special_zone_1_pswitch_coin_block_10 = "Gnarly - P-Switch Coin Block #10"
+special_zone_1_pswitch_coin_block_11 = "Gnarly - P-Switch Coin Block #11"
+special_zone_1_pswitch_coin_block_12 = "Gnarly - P-Switch Coin Block #12"
+special_zone_1_pswitch_coin_block_13 = "Gnarly - P-Switch Coin Block #13"
+special_zone_2_powerup_block_1 = "Tubular - Powerup Block #1"
+special_zone_2_coin_block_1 = "Tubular - Coin Block #1"
+special_zone_2_coin_block_2 = "Tubular - Coin Block #2"
+special_zone_2_powerup_block_2 = "Tubular - Powerup Block #2"
+special_zone_2_coin_block_3 = "Tubular - Coin Block #3"
+special_zone_2_coin_block_4 = "Tubular - Coin Block #4"
+special_zone_2_powerup_block_3 = "Tubular - Powerup Block #3"
+special_zone_2_multi_coin_block_1 = "Tubular - Multi Coin Block #1"
+special_zone_2_coin_block_5 = "Tubular - Coin Block #5"
+special_zone_2_coin_block_6 = "Tubular - Coin Block #6"
+special_zone_3_powerup_block_1 = "Way Cool - Powerup Block #1"
+special_zone_3_yoshi_block_1 = "Way Cool - Yoshi Block #1"
+special_zone_3_wings_block_1 = "Way Cool - Wings Block #1"
+special_zone_4_powerup_block_1 = "Awesome - Powerup Block #1"
+special_zone_4_star_block_1 = "Awesome - Star Block #1"
+star_road_2_star_block_1 = "Star Road 2 - Star Block #1"
+star_road_3_key_block_1 = "Star Road 3 - Key Block #1"
+star_road_4_powerup_block_1 = "Star Road 4 - Powerup Block #1"
+star_road_4_green_block_1 = "Star Road 4 - Green Switch Palace Block #1"
+star_road_4_green_block_2 = "Star Road 4 - Green Switch Palace Block #2"
+star_road_4_green_block_3 = "Star Road 4 - Green Switch Palace Block #3"
+star_road_4_green_block_4 = "Star Road 4 - Green Switch Palace Block #4"
+star_road_4_green_block_5 = "Star Road 4 - Green Switch Palace Block #5"
+star_road_4_green_block_6 = "Star Road 4 - Green Switch Palace Block #6"
+star_road_4_green_block_7 = "Star Road 4 - Green Switch Palace Block #7"
+star_road_4_key_block_1 = "Star Road 4 - Key Block #1"
+star_road_5_directional_coin_block_1 = "Star Road 5 - Directional Coin Block #1"
+star_road_5_life_block_1 = "Star Road 5 - 1-Up Mushroom Block #1"
+star_road_5_vine_block_1 = "Star Road 5 - Vine Block #1"
+star_road_5_yellow_block_1 = "Star Road 5 - Yellow Switch Palace Block #1"
+star_road_5_yellow_block_2 = "Star Road 5 - Yellow Switch Palace Block #2"
+star_road_5_yellow_block_3 = "Star Road 5 - Yellow Switch Palace Block #3"
+star_road_5_yellow_block_4 = "Star Road 5 - Yellow Switch Palace Block #4"
+star_road_5_yellow_block_5 = "Star Road 5 - Yellow Switch Palace Block #5"
+star_road_5_yellow_block_6 = "Star Road 5 - Yellow Switch Palace Block #6"
+star_road_5_yellow_block_7 = "Star Road 5 - Yellow Switch Palace Block #7"
+star_road_5_yellow_block_8 = "Star Road 5 - Yellow Switch Palace Block #8"
+star_road_5_yellow_block_9 = "Star Road 5 - Yellow Switch Palace Block #9"
+star_road_5_yellow_block_10 = "Star Road 5 - Yellow Switch Palace Block #10"
+star_road_5_yellow_block_11 = "Star Road 5 - Yellow Switch Palace Block #11"
+star_road_5_yellow_block_12 = "Star Road 5 - Yellow Switch Palace Block #12"
+star_road_5_yellow_block_13 = "Star Road 5 - Yellow Switch Palace Block #13"
+star_road_5_yellow_block_14 = "Star Road 5 - Yellow Switch Palace Block #14"
+star_road_5_yellow_block_15 = "Star Road 5 - Yellow Switch Palace Block #15"
+star_road_5_yellow_block_16 = "Star Road 5 - Yellow Switch Palace Block #16"
+star_road_5_yellow_block_17 = "Star Road 5 - Yellow Switch Palace Block #17"
+star_road_5_yellow_block_18 = "Star Road 5 - Yellow Switch Palace Block #18"
+star_road_5_yellow_block_19 = "Star Road 5 - Yellow Switch Palace Block #19"
+star_road_5_yellow_block_20 = "Star Road 5 - Yellow Switch Palace Block #20"
+star_road_5_green_block_1 = "Star Road 5 - Green Switch Palace Block #1"
+star_road_5_green_block_2 = "Star Road 5 - Green Switch Palace Block #2"
+star_road_5_green_block_3 = "Star Road 5 - Green Switch Palace Block #3"
+star_road_5_green_block_4 = "Star Road 5 - Green Switch Palace Block #4"
+star_road_5_green_block_5 = "Star Road 5 - Green Switch Palace Block #5"
+star_road_5_green_block_6 = "Star Road 5 - Green Switch Palace Block #6"
+star_road_5_green_block_7 = "Star Road 5 - Green Switch Palace Block #7"
+star_road_5_green_block_8 = "Star Road 5 - Green Switch Palace Block #8"
+star_road_5_green_block_9 = "Star Road 5 - Green Switch Palace Block #9"
+star_road_5_green_block_10 = "Star Road 5 - Green Switch Palace Block #10"
+star_road_5_green_block_11 = "Star Road 5 - Green Switch Palace Block #11"
+star_road_5_green_block_12 = "Star Road 5 - Green Switch Palace Block #12"
+star_road_5_green_block_13 = "Star Road 5 - Green Switch Palace Block #13"
+star_road_5_green_block_14 = "Star Road 5 - Green Switch Palace Block #14"
+star_road_5_green_block_15 = "Star Road 5 - Green Switch Palace Block #15"
+star_road_5_green_block_16 = "Star Road 5 - Green Switch Palace Block #16"
+star_road_5_green_block_17 = "Star Road 5 - Green Switch Palace Block #17"
+star_road_5_green_block_18 = "Star Road 5 - Green Switch Palace Block #18"
+star_road_5_green_block_19 = "Star Road 5 - Green Switch Palace Block #19"
+star_road_5_green_block_20 = "Star Road 5 - Green Switch Palace Block #20"
diff --git a/worlds/smw/Names/TextBox.py b/worlds/smw/Names/TextBox.py
index cecf0886617c..2302a5f85fc9 100644
--- a/worlds/smw/Names/TextBox.py
+++ b/worlds/smw/Names/TextBox.py
@@ -1,5 +1,5 @@
-from BaseClasses import MultiWorld
+from worlds.AutoWorld import World
import math
@@ -63,21 +63,23 @@ def generate_text_box(input_string):
return out_bytes
-def generate_goal_text(world: MultiWorld, player: int):
+def generate_goal_text(world: World):
out_array = bytearray()
- if world.goal[player] == "yoshi_egg_hunt":
- required_yoshi_eggs = max(math.floor(
- world.number_of_yoshi_eggs[player].value * (world.percentage_of_yoshi_eggs[player].value / 100.0)), 1)
+ if world.options.goal == "yoshi_egg_hunt":
+ required_yoshi_eggs = world.required_egg_count
+ actual_yoshi_eggs = world.actual_egg_count
out_array += bytearray([0x9F, 0x9F])
out_array += string_to_bytes(" You must acquire")
out_array[-1] += 0x80
- out_array += string_to_bytes(f' {required_yoshi_eggs:02} Yoshi Eggs,')
+ out_array += string_to_bytes(f' {required_yoshi_eggs:03} of {actual_yoshi_eggs:03}')
+ out_array[-1] += 0x80
+ out_array += string_to_bytes(f' Yoshi Eggs,')
out_array[-1] += 0x80
out_array += string_to_bytes("then return here.")
out_array[-1] += 0x80
- out_array += bytearray([0x9F, 0x9F, 0x9F])
+ out_array += bytearray([0x9F, 0x9F])
else:
- bosses_required = world.bosses_required[player].value
+ bosses_required = world.options.bosses_required.value
out_array += bytearray([0x9F, 0x9F])
out_array += string_to_bytes(" You must defeat")
out_array[-1] += 0x80
diff --git a/worlds/smw/Options.py b/worlds/smw/Options.py
index 60135896c86c..545b3c931b42 100644
--- a/worlds/smw/Options.py
+++ b/worlds/smw/Options.py
@@ -1,12 +1,14 @@
-import typing
+from dataclasses import dataclass
-from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList
+from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions
class Goal(Choice):
"""
Determines the goal of the seed
+
Bowser: Defeat Koopalings, reach Bowser's Castle and defeat Bowser
+
Yoshi Egg Hunt: Find a certain number of Yoshi Eggs
"""
display_name = "Goal"
@@ -27,11 +29,15 @@ class BossesRequired(Range):
class NumberOfYoshiEggs(Range):
"""
- How many Yoshi Eggs are in the pool for Yoshi Egg Hunt
+ Maximum possible number of Yoshi Eggs that will be in the item pool
+
+ If fewer available locations exist in the pool than this number, the number of available locations will be used instead.
+
+ Required Percentage of Yoshi Eggs will be calculated based off of that number.
"""
- display_name = "Total Number of Yoshi Eggs"
+ display_name = "Max Number of Yoshi Eggs"
range_start = 1
- range_end = 80
+ range_end = 255
default = 50
@@ -52,13 +58,56 @@ class DragonCoinChecks(Toggle):
display_name = "Dragon Coin Checks"
+class MoonChecks(Toggle):
+ """
+ Whether collecting a 3-Up Moon in a level will grant a check
+ """
+ display_name = "3up Moon Checks"
+
+
+class Hidden1UpChecks(Toggle):
+ """
+ Whether collecting a hidden 1-Up mushroom in a level will grant a check
+
+ These checks are considered cryptic as there's no actual indicator that they're in their respective places
+
+ Enable this option at your own risk
+ """
+ display_name = "Hidden 1-Up Checks"
+
+
+class BonusBlockChecks(Toggle):
+ """
+ Whether collecting a 1-Up mushroom from a Bonus Block in a level will grant a check
+ """
+ display_name = "Bonus Block Checks"
+
+
+class Blocksanity(Toggle):
+ """
+ Whether hitting a block with an item or coin inside will grant a check
+
+ Note that some blocks are excluded due to how the option and the game works!
+
+ Exclusion list:
+ * Blocks in Top Secret Area & Front Door/Bowser Castle
+ * Blocks that are unreachable unless you glitch your way in
+ """
+ display_name = "Blocksanity"
+
+
class BowserCastleDoors(Choice):
"""
How the doors of Bowser's Castle behave
+
Vanilla: Front and Back Doors behave as vanilla
+
Fast: Both doors behave as the Back Door
+
Slow: Both doors behave as the Front Door
+
"Front Door" rooms depend on the `bowser_castle_rooms` option
+
"Back Door" only requires going through the dark hallway to Bowser
"""
display_name = "Bowser Castle Doors"
@@ -71,10 +120,15 @@ class BowserCastleDoors(Choice):
class BowserCastleRooms(Choice):
"""
How the rooms of Bowser's Castle Front Door behave
+
Vanilla: You can choose which rooms to enter, as in vanilla
+
Random Two Room: Two random rooms are chosen
+
Random Five Room: Five random rooms are chosen
+
Gauntlet: All eight rooms must be cleared
+
Labyrinth: Which room leads to Bowser?
"""
display_name = "Bowser Castle Rooms"
@@ -89,9 +143,13 @@ class BowserCastleRooms(Choice):
class BossShuffle(Choice):
"""
How bosses are shuffled
+
None: Bosses are not shuffled
+
Simple: Four Reznors and the seven Koopalings are shuffled around
+
Full: Each boss location gets a fully random boss
+
Singularity: One or two bosses are chosen and placed at every boss location
"""
display_name = "Boss Shuffle"
@@ -112,6 +170,7 @@ class LevelShuffle(Toggle):
class ExcludeSpecialZone(Toggle):
"""
If active, this option will prevent any progression items from being placed in Special Zone levels.
+
Additionally, if Level Shuffle is active, Special Zone levels will not be shuffled away from their vanilla tiles.
"""
display_name = "Exclude Special Zone"
@@ -119,24 +178,15 @@ class ExcludeSpecialZone(Toggle):
class SwapDonutGhostHouseExits(Toggle):
"""
- If enabled, this option will swap which overworld direction the two exits of the level at the Donut Ghost House
- overworld tile go:
+ If enabled, this option will swap which overworld direction the two exits of the level at the Donut Ghost House overworld tile go:
+
False: Normal Exit goes up, Secret Exit goes right.
+
True: Normal Exit goes right, Secret Exit goes up.
"""
display_name = "Swap Donut GH Exits"
-class DisplaySentItemPopups(Choice):
- """
- What messages to display in-game for items sent
- """
- display_name = "Display Sent Item Popups"
- option_none = 0
- option_all = 1
- default = 1
-
-
class DisplayReceivedItemPopups(Choice):
"""
What messages to display in-game for items received
@@ -145,7 +195,18 @@ class DisplayReceivedItemPopups(Choice):
option_none = 0
option_all = 1
option_progression = 2
- default = 2
+ option_progression_minus_yoshi_eggs = 3
+ default = 3
+
+
+class JunkFillPercentage(Range):
+ """
+ Replace a percentage of non-required Yoshi Eggs in the item pool with random junk items (only applicable on Yoshi Egg Hunt goal)
+ """
+ display_name = "Junk Fill Percentage"
+ range_start = 0
+ range_end = 100
+ default = 0
class TrapFillPercentage(Range):
@@ -197,6 +258,20 @@ class TimerTrapWeight(BaseTrapWeight):
display_name = "Timer Trap Weight"
+class ReverseTrapWeight(BaseTrapWeight):
+ """
+ Likelihood of a receiving a trap which causes the controls to be reversed in the current level
+ """
+ display_name = "Reverse Trap Weight"
+
+
+class ThwimpTrapWeight(BaseTrapWeight):
+ """
+ Likelihood of a receiving a trap which causes a Thwimp to spawn above the player
+ """
+ display_name = "Thwimp Trap Weight"
+
+
class Autosave(DefaultOnToggle):
"""
Whether a save prompt will appear after every level
@@ -207,6 +282,7 @@ class Autosave(DefaultOnToggle):
class EarlyClimb(Toggle):
"""
Force Climb to appear early in the seed as a local item.
+
This is particularly useful to prevent BK when Level Shuffle is disabled
"""
display_name = "Early Climb"
@@ -226,9 +302,13 @@ class OverworldSpeed(Choice):
class MusicShuffle(Choice):
"""
Music shuffle type
+
None: No Music is shuffled
+
Consistent: Each music track is consistently shuffled throughout the game
+
Full: Each individual level has a random music track
+
Singularity: The entire game uses one song for overworld and one song for levels
"""
display_name = "Music Shuffle"
@@ -239,6 +319,25 @@ class MusicShuffle(Choice):
default = 0
+class SFXShuffle(Choice):
+ """
+ Shuffles almost every instance of sound effect playback
+
+ Archipelago elements that play sound effects aren't randomized
+
+ None: No SFX are shuffled
+
+ Full: Each individual SFX call has a random SFX
+
+ Singularity: The entire game uses one SFX for every SFX call
+ """
+ display_name = "Sound Effect Shuffle"
+ option_none = 0
+ option_full = 1
+ option_singularity = 2
+ default = 0
+
+
class MarioPalette(Choice):
"""
Mario palette color
@@ -255,25 +354,38 @@ class MarioPalette(Choice):
default = 0
-class ForegroundPaletteShuffle(Toggle):
- """
- Whether to shuffle level foreground palettes
+class LevelPaletteShuffle(Choice):
"""
- display_name = "Foreground Palette Shuffle"
+ Whether to shuffle level palettes
+ Off: Do not shuffle palettes
-class BackgroundPaletteShuffle(Toggle):
- """
- Whether to shuffle level background palettes
+ On Legacy: Uses only the palette sets from the original game
+
+ On Curated: Uses custom, hand-crafted palette sets
"""
- display_name = "Background Palette Shuffle"
+ display_name = "Level Palette Shuffle"
+ option_off = 0
+ option_on_legacy = 1
+ option_on_curated = 2
+ default = 0
-class OverworldPaletteShuffle(Toggle):
+class OverworldPaletteShuffle(Choice):
"""
Whether to shuffle overworld palettes
+
+ Off: Do not shuffle palettes
+
+ On Legacy: Uses only the palette sets from the original game
+
+ On Curated: Uses custom, hand-crafted palette sets
"""
display_name = "Overworld Palette Shuffle"
+ option_off = 0
+ option_on_legacy = 1
+ option_on_curated = 2
+ default = 0
class StartingLifeCount(Range):
@@ -286,34 +398,85 @@ class StartingLifeCount(Range):
default = 5
-
-smw_options: typing.Dict[str, type(Option)] = {
- "death_link": DeathLink,
- "goal": Goal,
- "bosses_required": BossesRequired,
- "number_of_yoshi_eggs": NumberOfYoshiEggs,
- "percentage_of_yoshi_eggs": PercentageOfYoshiEggs,
- "dragon_coin_checks": DragonCoinChecks,
- "bowser_castle_doors": BowserCastleDoors,
- "bowser_castle_rooms": BowserCastleRooms,
- "level_shuffle": LevelShuffle,
- "exclude_special_zone": ExcludeSpecialZone,
- "boss_shuffle": BossShuffle,
- "swap_donut_gh_exits": SwapDonutGhostHouseExits,
- #"display_sent_item_popups": DisplaySentItemPopups,
- "display_received_item_popups": DisplayReceivedItemPopups,
- "trap_fill_percentage": TrapFillPercentage,
- "ice_trap_weight": IceTrapWeight,
- "stun_trap_weight": StunTrapWeight,
- "literature_trap_weight": LiteratureTrapWeight,
- "timer_trap_weight": TimerTrapWeight,
- "autosave": Autosave,
- "early_climb": EarlyClimb,
- "overworld_speed": OverworldSpeed,
- "music_shuffle": MusicShuffle,
- "mario_palette": MarioPalette,
- "foreground_palette_shuffle": ForegroundPaletteShuffle,
- "background_palette_shuffle": BackgroundPaletteShuffle,
- "overworld_palette_shuffle": OverworldPaletteShuffle,
- "starting_life_count": StartingLifeCount,
-}
+smw_option_groups = [
+ OptionGroup("Goal Options", [
+ Goal,
+ BossesRequired,
+ NumberOfYoshiEggs,
+ PercentageOfYoshiEggs,
+ ]),
+ OptionGroup("Sanity Options", [
+ DragonCoinChecks,
+ MoonChecks,
+ Hidden1UpChecks,
+ BonusBlockChecks,
+ Blocksanity,
+ ]),
+ OptionGroup("Level Shuffling", [
+ LevelShuffle,
+ ExcludeSpecialZone,
+ BowserCastleDoors,
+ BowserCastleRooms,
+ BossShuffle,
+ SwapDonutGhostHouseExits,
+ ]),
+ OptionGroup("Junk and Traps", [
+ JunkFillPercentage,
+ TrapFillPercentage,
+ IceTrapWeight,
+ StunTrapWeight,
+ LiteratureTrapWeight,
+ TimerTrapWeight,
+ ReverseTrapWeight,
+ ThwimpTrapWeight,
+ ]),
+ OptionGroup("Aesthetics", [
+ DisplayReceivedItemPopups,
+ Autosave,
+ OverworldSpeed,
+ MusicShuffle,
+ SFXShuffle,
+ MarioPalette,
+ LevelPaletteShuffle,
+ OverworldPaletteShuffle,
+ StartingLifeCount,
+ ]),
+]
+
+
+@dataclass
+class SMWOptions(PerGameCommonOptions):
+ death_link: DeathLink
+ goal: Goal
+ bosses_required: BossesRequired
+ max_yoshi_egg_cap: NumberOfYoshiEggs
+ percentage_of_yoshi_eggs: PercentageOfYoshiEggs
+ dragon_coin_checks: DragonCoinChecks
+ moon_checks: MoonChecks
+ hidden_1up_checks: Hidden1UpChecks
+ bonus_block_checks: BonusBlockChecks
+ blocksanity: Blocksanity
+ bowser_castle_doors: BowserCastleDoors
+ bowser_castle_rooms: BowserCastleRooms
+ level_shuffle: LevelShuffle
+ exclude_special_zone: ExcludeSpecialZone
+ boss_shuffle: BossShuffle
+ swap_donut_gh_exits: SwapDonutGhostHouseExits
+ display_received_item_popups: DisplayReceivedItemPopups
+ junk_fill_percentage: JunkFillPercentage
+ trap_fill_percentage: TrapFillPercentage
+ ice_trap_weight: IceTrapWeight
+ stun_trap_weight: StunTrapWeight
+ literature_trap_weight: LiteratureTrapWeight
+ timer_trap_weight: TimerTrapWeight
+ reverse_trap_weight: ReverseTrapWeight
+ thwimp_trap_weight: ThwimpTrapWeight
+ autosave: Autosave
+ early_climb: EarlyClimb
+ overworld_speed: OverworldSpeed
+ music_shuffle: MusicShuffle
+ sfx_shuffle: SFXShuffle
+ mario_palette: MarioPalette
+ level_palette_shuffle: LevelPaletteShuffle
+ overworld_palette_shuffle: OverworldPaletteShuffle
+ starting_life_count: StartingLifeCount
diff --git a/worlds/smw/Presets.py b/worlds/smw/Presets.py
new file mode 100644
index 000000000000..17a80e3efccd
--- /dev/null
+++ b/worlds/smw/Presets.py
@@ -0,0 +1,57 @@
+from typing import Dict, Any
+
+all_random = {
+ "goal": "random",
+ "bosses_required": "random",
+ "max_yoshi_egg_cap": "random",
+ "percentage_of_yoshi_eggs": "random",
+ "dragon_coin_checks": "random",
+ "moon_checks": "random",
+ "hidden_1up_checks": "random",
+ "bonus_block_checks": "random",
+ "blocksanity": "random",
+ "bowser_castle_doors": "random",
+ "bowser_castle_rooms": "random",
+ "level_shuffle": "random",
+ "exclude_special_zone": "random",
+ "boss_shuffle": "random",
+ "swap_donut_gh_exits": "random",
+ "display_received_item_popups": "random",
+ "junk_fill_percentage": "random",
+ "trap_fill_percentage": "random",
+ "ice_trap_weight": "random",
+ "stun_trap_weight": "random",
+ "literature_trap_weight": "random",
+ "timer_trap_weight": "random",
+ "reverse_trap_weight": "random",
+ "thwimp_trap_weight": "random",
+ "autosave": "random",
+ "early_climb": "random",
+ "overworld_speed": "random",
+ "music_shuffle": "random",
+ "sfx_shuffle": "random",
+ "mario_palette": "random",
+ "level_palette_shuffle": "random",
+ "overworld_palette_shuffle": "random",
+ "starting_life_count": "random",
+}
+
+allsanity = {
+ "dragon_coin_checks": True,
+ "moon_checks": True,
+ "hidden_1up_checks": True,
+ "bonus_block_checks": True,
+ "blocksanity": True,
+ "level_shuffle": True,
+ "boss_shuffle": "full",
+ "music_shuffle": "full",
+ "sfx_shuffle": "full",
+ "mario_palette": "random",
+ "level_palette_shuffle": "on_curated",
+ "overworld_palette_shuffle": "on_curated",
+}
+
+smw_options_presets: Dict[str, Dict[str, Any]] = {
+ "All Random": all_random,
+ "Allsanity": allsanity,
+}
diff --git a/worlds/smw/Regions.py b/worlds/smw/Regions.py
index 885f209aa74c..249604987401 100644
--- a/worlds/smw/Regions.py
+++ b/worlds/smw/Regions.py
@@ -1,467 +1,470 @@
import typing
-from BaseClasses import MultiWorld, Region, Entrance
+from BaseClasses import CollectionState, MultiWorld, Region, Entrance
from .Locations import SMWLocation
from .Levels import level_info_dict
from .Names import LocationName, ItemName
from worlds.generic.Rules import add_rule, set_rule
+from worlds.AutoWorld import World
-def create_regions(world, player: int, active_locations):
- menu_region = create_region(world, player, active_locations, 'Menu', None)
+def create_regions(world: World, active_locations):
+ multiworld: MultiWorld = world.multiworld
+ player: int = world.player
- yoshis_island_region = create_region(world, player, active_locations, LocationName.yoshis_island_region, None)
+ menu_region = create_region(multiworld, player, active_locations, 'Menu', None)
+ yoshis_island_region = create_region(multiworld, player, active_locations, LocationName.yoshis_island_region, None)
- yoshis_house_tile = create_region(world, player, active_locations, LocationName.yoshis_house_tile, None)
+ yoshis_house_tile = create_region(multiworld, player, active_locations, LocationName.yoshis_house_tile, None)
yoshis_house_region_locations = []
- if world.goal[player] == "yoshi_egg_hunt":
+ if world.options.goal == "yoshi_egg_hunt":
yoshis_house_region_locations.append(LocationName.yoshis_house)
- yoshis_house_region = create_region(world, player, active_locations, LocationName.yoshis_house,
+ yoshis_house_region = create_region(multiworld, player, active_locations, LocationName.yoshis_house,
yoshis_house_region_locations)
- yoshis_island_1_tile = create_region(world, player, active_locations, LocationName.yoshis_island_1_tile, None)
- yoshis_island_1_region = create_region(world, player, active_locations, LocationName.yoshis_island_1_region, None)
- yoshis_island_1_exit_1 = create_region(world, player, active_locations, LocationName.yoshis_island_1_exit_1,
+ yoshis_island_1_tile = create_region(multiworld, player, active_locations, LocationName.yoshis_island_1_tile, None)
+ yoshis_island_1_region = create_region(multiworld, player, active_locations, LocationName.yoshis_island_1_region, None)
+ yoshis_island_1_exit_1 = create_region(multiworld, player, active_locations, LocationName.yoshis_island_1_exit_1,
[LocationName.yoshis_island_1_exit_1])
- yoshis_island_2_tile = create_region(world, player, active_locations, LocationName.yoshis_island_2_tile, None)
- yoshis_island_2_region = create_region(world, player, active_locations, LocationName.yoshis_island_2_region, None)
- yoshis_island_2_exit_1 = create_region(world, player, active_locations, LocationName.yoshis_island_2_exit_1,
+ yoshis_island_2_tile = create_region(multiworld, player, active_locations, LocationName.yoshis_island_2_tile, None)
+ yoshis_island_2_region = create_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, None)
+ yoshis_island_2_exit_1 = create_region(multiworld, player, active_locations, LocationName.yoshis_island_2_exit_1,
[LocationName.yoshis_island_2_exit_1])
- yoshis_island_3_tile = create_region(world, player, active_locations, LocationName.yoshis_island_3_tile, None)
- yoshis_island_3_region = create_region(world, player, active_locations, LocationName.yoshis_island_3_region, None)
- yoshis_island_3_exit_1 = create_region(world, player, active_locations, LocationName.yoshis_island_3_exit_1,
+ yoshis_island_3_tile = create_region(multiworld, player, active_locations, LocationName.yoshis_island_3_tile, None)
+ yoshis_island_3_region = create_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, None)
+ yoshis_island_3_exit_1 = create_region(multiworld, player, active_locations, LocationName.yoshis_island_3_exit_1,
[LocationName.yoshis_island_3_exit_1])
- yoshis_island_4_tile = create_region(world, player, active_locations, LocationName.yoshis_island_4_tile, None)
- yoshis_island_4_region = create_region(world, player, active_locations, LocationName.yoshis_island_4_region, None)
- yoshis_island_4_exit_1 = create_region(world, player, active_locations, LocationName.yoshis_island_4_exit_1,
+ yoshis_island_4_tile = create_region(multiworld, player, active_locations, LocationName.yoshis_island_4_tile, None)
+ yoshis_island_4_region = create_region(multiworld, player, active_locations, LocationName.yoshis_island_4_region, None)
+ yoshis_island_4_exit_1 = create_region(multiworld, player, active_locations, LocationName.yoshis_island_4_exit_1,
[LocationName.yoshis_island_4_exit_1])
- yoshis_island_castle_tile = create_region(world, player, active_locations, LocationName.yoshis_island_castle_tile, None)
- yoshis_island_castle_region = create_region(world, player, active_locations, LocationName.yoshis_island_castle_region, None)
- yoshis_island_castle = create_region(world, player, active_locations, LocationName.yoshis_island_castle,
+ yoshis_island_castle_tile = create_region(multiworld, player, active_locations, LocationName.yoshis_island_castle_tile, None)
+ yoshis_island_castle_region = create_region(multiworld, player, active_locations, LocationName.yoshis_island_castle_region, None)
+ yoshis_island_castle = create_region(multiworld, player, active_locations, LocationName.yoshis_island_castle,
[LocationName.yoshis_island_castle, LocationName.yoshis_island_koopaling])
- yellow_switch_palace_tile = create_region(world, player, active_locations, LocationName.yellow_switch_palace_tile, None)
- yellow_switch_palace = create_region(world, player, active_locations, LocationName.yellow_switch_palace,
+ yellow_switch_palace_tile = create_region(multiworld, player, active_locations, LocationName.yellow_switch_palace_tile, None)
+ yellow_switch_palace = create_region(multiworld, player, active_locations, LocationName.yellow_switch_palace,
[LocationName.yellow_switch_palace])
- donut_plains_1_tile = create_region(world, player, active_locations, LocationName.donut_plains_1_tile, None)
- donut_plains_1_region = create_region(world, player, active_locations, LocationName.donut_plains_1_region, None)
- donut_plains_1_exit_1 = create_region(world, player, active_locations, LocationName.donut_plains_1_exit_1,
+ donut_plains_1_tile = create_region(multiworld, player, active_locations, LocationName.donut_plains_1_tile, None)
+ donut_plains_1_region = create_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, None)
+ donut_plains_1_exit_1 = create_region(multiworld, player, active_locations, LocationName.donut_plains_1_exit_1,
[LocationName.donut_plains_1_exit_1])
- donut_plains_1_exit_2 = create_region(world, player, active_locations, LocationName.donut_plains_1_exit_2,
+ donut_plains_1_exit_2 = create_region(multiworld, player, active_locations, LocationName.donut_plains_1_exit_2,
[LocationName.donut_plains_1_exit_2])
- donut_plains_2_tile = create_region(world, player, active_locations, LocationName.donut_plains_2_tile, None)
- donut_plains_2_region = create_region(world, player, active_locations, LocationName.donut_plains_2_region, None)
- donut_plains_2_exit_1 = create_region(world, player, active_locations, LocationName.donut_plains_2_exit_1,
+ donut_plains_2_tile = create_region(multiworld, player, active_locations, LocationName.donut_plains_2_tile, None)
+ donut_plains_2_region = create_region(multiworld, player, active_locations, LocationName.donut_plains_2_region, None)
+ donut_plains_2_exit_1 = create_region(multiworld, player, active_locations, LocationName.donut_plains_2_exit_1,
[LocationName.donut_plains_2_exit_1])
- donut_plains_2_exit_2 = create_region(world, player, active_locations, LocationName.donut_plains_2_exit_2,
+ donut_plains_2_exit_2 = create_region(multiworld, player, active_locations, LocationName.donut_plains_2_exit_2,
[LocationName.donut_plains_2_exit_2])
- donut_plains_3_tile = create_region(world, player, active_locations, LocationName.donut_plains_3_tile, None)
- donut_plains_3_region = create_region(world, player, active_locations, LocationName.donut_plains_3_region, None)
- donut_plains_3_exit_1 = create_region(world, player, active_locations, LocationName.donut_plains_3_exit_1,
+ donut_plains_3_tile = create_region(multiworld, player, active_locations, LocationName.donut_plains_3_tile, None)
+ donut_plains_3_region = create_region(multiworld, player, active_locations, LocationName.donut_plains_3_region, None)
+ donut_plains_3_exit_1 = create_region(multiworld, player, active_locations, LocationName.donut_plains_3_exit_1,
[LocationName.donut_plains_3_exit_1])
- donut_plains_4_tile = create_region(world, player, active_locations, LocationName.donut_plains_4_tile, None)
- donut_plains_4_region = create_region(world, player, active_locations, LocationName.donut_plains_4_region, None)
- donut_plains_4_exit_1 = create_region(world, player, active_locations, LocationName.donut_plains_4_exit_1,
+ donut_plains_4_tile = create_region(multiworld, player, active_locations, LocationName.donut_plains_4_tile, None)
+ donut_plains_4_region = create_region(multiworld, player, active_locations, LocationName.donut_plains_4_region, None)
+ donut_plains_4_exit_1 = create_region(multiworld, player, active_locations, LocationName.donut_plains_4_exit_1,
[LocationName.donut_plains_4_exit_1])
- donut_secret_1_tile = create_region(world, player, active_locations, LocationName.donut_secret_1_tile, None)
- donut_secret_1_region = create_region(world, player, active_locations, LocationName.donut_secret_1_region, None)
- donut_secret_1_exit_1 = create_region(world, player, active_locations, LocationName.donut_secret_1_exit_1,
+ donut_secret_1_tile = create_region(multiworld, player, active_locations, LocationName.donut_secret_1_tile, None)
+ donut_secret_1_region = create_region(multiworld, player, active_locations, LocationName.donut_secret_1_region, None)
+ donut_secret_1_exit_1 = create_region(multiworld, player, active_locations, LocationName.donut_secret_1_exit_1,
[LocationName.donut_secret_1_exit_1])
- donut_secret_1_exit_2 = create_region(world, player, active_locations, LocationName.donut_secret_1_exit_2,
+ donut_secret_1_exit_2 = create_region(multiworld, player, active_locations, LocationName.donut_secret_1_exit_2,
[LocationName.donut_secret_1_exit_2])
- donut_secret_2_tile = create_region(world, player, active_locations, LocationName.donut_secret_2_tile, None)
- donut_secret_2_region = create_region(world, player, active_locations, LocationName.donut_secret_2_region, None)
- donut_secret_2_exit_1 = create_region(world, player, active_locations, LocationName.donut_secret_2_exit_1,
+ donut_secret_2_tile = create_region(multiworld, player, active_locations, LocationName.donut_secret_2_tile, None)
+ donut_secret_2_region = create_region(multiworld, player, active_locations, LocationName.donut_secret_2_region, None)
+ donut_secret_2_exit_1 = create_region(multiworld, player, active_locations, LocationName.donut_secret_2_exit_1,
[LocationName.donut_secret_2_exit_1])
- donut_ghost_house_tile = create_region(world, player, active_locations, LocationName.donut_ghost_house_tile, None)
- donut_ghost_house_region = create_region(world, player, active_locations, LocationName.donut_ghost_house_region, None)
- donut_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.donut_ghost_house_exit_1,
+ donut_ghost_house_tile = create_region(multiworld, player, active_locations, LocationName.donut_ghost_house_tile, None)
+ donut_ghost_house_region = create_region(multiworld, player, active_locations, LocationName.donut_ghost_house_region, None)
+ donut_ghost_house_exit_1 = create_region(multiworld, player, active_locations, LocationName.donut_ghost_house_exit_1,
[LocationName.donut_ghost_house_exit_1])
- donut_ghost_house_exit_2 = create_region(world, player, active_locations, LocationName.donut_ghost_house_exit_2,
+ donut_ghost_house_exit_2 = create_region(multiworld, player, active_locations, LocationName.donut_ghost_house_exit_2,
[LocationName.donut_ghost_house_exit_2])
- donut_secret_house_tile = create_region(world, player, active_locations, LocationName.donut_secret_house_tile, None)
- donut_secret_house_region = create_region(world, player, active_locations, LocationName.donut_secret_house_region, None)
- donut_secret_house_exit_1 = create_region(world, player, active_locations, LocationName.donut_secret_house_exit_1,
+ donut_secret_house_tile = create_region(multiworld, player, active_locations, LocationName.donut_secret_house_tile, None)
+ donut_secret_house_region = create_region(multiworld, player, active_locations, LocationName.donut_secret_house_region, None)
+ donut_secret_house_exit_1 = create_region(multiworld, player, active_locations, LocationName.donut_secret_house_exit_1,
[LocationName.donut_secret_house_exit_1])
- donut_secret_house_exit_2 = create_region(world, player, active_locations, LocationName.donut_secret_house_exit_2,
+ donut_secret_house_exit_2 = create_region(multiworld, player, active_locations, LocationName.donut_secret_house_exit_2,
[LocationName.donut_secret_house_exit_2])
- donut_plains_castle_tile = create_region(world, player, active_locations, LocationName.donut_plains_castle_tile, None)
- donut_plains_castle_region = create_region(world, player, active_locations, LocationName.donut_plains_castle_region, None)
- donut_plains_castle = create_region(world, player, active_locations, LocationName.donut_plains_castle,
+ donut_plains_castle_tile = create_region(multiworld, player, active_locations, LocationName.donut_plains_castle_tile, None)
+ donut_plains_castle_region = create_region(multiworld, player, active_locations, LocationName.donut_plains_castle_region, None)
+ donut_plains_castle = create_region(multiworld, player, active_locations, LocationName.donut_plains_castle,
[LocationName.donut_plains_castle, LocationName.donut_plains_koopaling])
- green_switch_palace_tile = create_region(world, player, active_locations, LocationName.green_switch_palace_tile, None)
- green_switch_palace = create_region(world, player, active_locations, LocationName.green_switch_palace,
+ green_switch_palace_tile = create_region(multiworld, player, active_locations, LocationName.green_switch_palace_tile, None)
+ green_switch_palace = create_region(multiworld, player, active_locations, LocationName.green_switch_palace,
[LocationName.green_switch_palace])
- donut_plains_top_secret_tile = create_region(world, player, active_locations, LocationName.donut_plains_top_secret_tile, None)
- donut_plains_top_secret = create_region(world, player, active_locations, LocationName.donut_plains_top_secret, None)
+ donut_plains_top_secret_tile = create_region(multiworld, player, active_locations, LocationName.donut_plains_top_secret_tile, None)
+ donut_plains_top_secret = create_region(multiworld, player, active_locations, LocationName.donut_plains_top_secret, None)
- vanilla_dome_1_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_1_tile, None)
- vanilla_dome_1_region = create_region(world, player, active_locations, LocationName.vanilla_dome_1_region, None)
- vanilla_dome_1_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_dome_1_exit_1,
+ vanilla_dome_1_tile = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_tile, None)
+ vanilla_dome_1_region = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_region, None)
+ vanilla_dome_1_exit_1 = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_exit_1,
[LocationName.vanilla_dome_1_exit_1])
- vanilla_dome_1_exit_2 = create_region(world, player, active_locations, LocationName.vanilla_dome_1_exit_2,
+ vanilla_dome_1_exit_2 = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_exit_2,
[LocationName.vanilla_dome_1_exit_2])
- vanilla_dome_2_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_2_tile, None)
- vanilla_dome_2_region = create_region(world, player, active_locations, LocationName.vanilla_dome_2_region, None)
- vanilla_dome_2_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_dome_2_exit_1,
+ vanilla_dome_2_tile = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_tile, None)
+ vanilla_dome_2_region = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, None)
+ vanilla_dome_2_exit_1 = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_exit_1,
[LocationName.vanilla_dome_2_exit_1])
- vanilla_dome_2_exit_2 = create_region(world, player, active_locations, LocationName.vanilla_dome_2_exit_2,
+ vanilla_dome_2_exit_2 = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_exit_2,
[LocationName.vanilla_dome_2_exit_2])
- vanilla_dome_3_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_3_tile, None)
- vanilla_dome_3_region = create_region(world, player, active_locations, LocationName.vanilla_dome_3_region, None)
- vanilla_dome_3_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_dome_3_exit_1,
+ vanilla_dome_3_tile = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_tile, None)
+ vanilla_dome_3_region = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, None)
+ vanilla_dome_3_exit_1 = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_exit_1,
[LocationName.vanilla_dome_3_exit_1])
- vanilla_dome_4_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_4_tile, None)
- vanilla_dome_4_region = create_region(world, player, active_locations, LocationName.vanilla_dome_4_region, None)
- vanilla_dome_4_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_dome_4_exit_1,
+ vanilla_dome_4_tile = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_tile, None)
+ vanilla_dome_4_region = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_region, None)
+ vanilla_dome_4_exit_1 = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_exit_1,
[LocationName.vanilla_dome_4_exit_1])
- vanilla_secret_1_tile = create_region(world, player, active_locations, LocationName.vanilla_secret_1_tile, None)
- vanilla_secret_1_region = create_region(world, player, active_locations, LocationName.vanilla_secret_1_region, None)
- vanilla_secret_1_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_secret_1_exit_1,
+ vanilla_secret_1_tile = create_region(multiworld, player, active_locations, LocationName.vanilla_secret_1_tile, None)
+ vanilla_secret_1_region = create_region(multiworld, player, active_locations, LocationName.vanilla_secret_1_region, None)
+ vanilla_secret_1_exit_1 = create_region(multiworld, player, active_locations, LocationName.vanilla_secret_1_exit_1,
[LocationName.vanilla_secret_1_exit_1])
- vanilla_secret_1_exit_2 = create_region(world, player, active_locations, LocationName.vanilla_secret_1_exit_2,
+ vanilla_secret_1_exit_2 = create_region(multiworld, player, active_locations, LocationName.vanilla_secret_1_exit_2,
[LocationName.vanilla_secret_1_exit_2])
- vanilla_secret_2_tile = create_region(world, player, active_locations, LocationName.vanilla_secret_2_tile, None)
- vanilla_secret_2_region = create_region(world, player, active_locations, LocationName.vanilla_secret_2_region, None)
- vanilla_secret_2_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_secret_2_exit_1,
+ vanilla_secret_2_tile = create_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_tile, None)
+ vanilla_secret_2_region = create_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_region, None)
+ vanilla_secret_2_exit_1 = create_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_exit_1,
[LocationName.vanilla_secret_2_exit_1])
- vanilla_secret_3_tile = create_region(world, player, active_locations, LocationName.vanilla_secret_3_tile, None)
- vanilla_secret_3_region = create_region(world, player, active_locations, LocationName.vanilla_secret_3_region, None)
- vanilla_secret_3_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_secret_3_exit_1,
+ vanilla_secret_3_tile = create_region(multiworld, player, active_locations, LocationName.vanilla_secret_3_tile, None)
+ vanilla_secret_3_region = create_region(multiworld, player, active_locations, LocationName.vanilla_secret_3_region, None)
+ vanilla_secret_3_exit_1 = create_region(multiworld, player, active_locations, LocationName.vanilla_secret_3_exit_1,
[LocationName.vanilla_secret_3_exit_1])
- vanilla_ghost_house_tile = create_region(world, player, active_locations, LocationName.vanilla_ghost_house_tile, None)
- vanilla_ghost_house_region = create_region(world, player, active_locations, LocationName.vanilla_ghost_house_region, None)
- vanilla_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_ghost_house_exit_1,
+ vanilla_ghost_house_tile = create_region(multiworld, player, active_locations, LocationName.vanilla_ghost_house_tile, None)
+ vanilla_ghost_house_region = create_region(multiworld, player, active_locations, LocationName.vanilla_ghost_house_region, None)
+ vanilla_ghost_house_exit_1 = create_region(multiworld, player, active_locations, LocationName.vanilla_ghost_house_exit_1,
[LocationName.vanilla_ghost_house_exit_1])
- vanilla_fortress_tile = create_region(world, player, active_locations, LocationName.vanilla_fortress_tile, None)
- vanilla_fortress_region = create_region(world, player, active_locations, LocationName.vanilla_fortress_region, None)
- vanilla_fortress = create_region(world, player, active_locations, LocationName.vanilla_fortress,
+ vanilla_fortress_tile = create_region(multiworld, player, active_locations, LocationName.vanilla_fortress_tile, None)
+ vanilla_fortress_region = create_region(multiworld, player, active_locations, LocationName.vanilla_fortress_region, None)
+ vanilla_fortress = create_region(multiworld, player, active_locations, LocationName.vanilla_fortress,
[LocationName.vanilla_fortress, LocationName.vanilla_reznor])
- vanilla_dome_castle_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_castle_tile, None)
- vanilla_dome_castle_region = create_region(world, player, active_locations, LocationName.vanilla_dome_castle_region, None)
- vanilla_dome_castle = create_region(world, player, active_locations, LocationName.vanilla_dome_castle,
+ vanilla_dome_castle_tile = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_castle_tile, None)
+ vanilla_dome_castle_region = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_castle_region, None)
+ vanilla_dome_castle = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_castle,
[LocationName.vanilla_dome_castle, LocationName.vanilla_dome_koopaling])
- red_switch_palace_tile = create_region(world, player, active_locations, LocationName.red_switch_palace_tile, None)
- red_switch_palace = create_region(world, player, active_locations, LocationName.red_switch_palace,
+ red_switch_palace_tile = create_region(multiworld, player, active_locations, LocationName.red_switch_palace_tile, None)
+ red_switch_palace = create_region(multiworld, player, active_locations, LocationName.red_switch_palace,
[LocationName.red_switch_palace])
- butter_bridge_1_tile = create_region(world, player, active_locations, LocationName.butter_bridge_1_tile, None)
- butter_bridge_1_region = create_region(world, player, active_locations, LocationName.butter_bridge_1_region, None)
- butter_bridge_1_exit_1 = create_region(world, player, active_locations, LocationName.butter_bridge_1_exit_1,
+ butter_bridge_1_tile = create_region(multiworld, player, active_locations, LocationName.butter_bridge_1_tile, None)
+ butter_bridge_1_region = create_region(multiworld, player, active_locations, LocationName.butter_bridge_1_region, None)
+ butter_bridge_1_exit_1 = create_region(multiworld, player, active_locations, LocationName.butter_bridge_1_exit_1,
[LocationName.butter_bridge_1_exit_1])
- butter_bridge_2_tile = create_region(world, player, active_locations, LocationName.butter_bridge_2_tile, None)
- butter_bridge_2_region = create_region(world, player, active_locations, LocationName.butter_bridge_2_region, None)
- butter_bridge_2_exit_1 = create_region(world, player, active_locations, LocationName.butter_bridge_2_exit_1,
+ butter_bridge_2_tile = create_region(multiworld, player, active_locations, LocationName.butter_bridge_2_tile, None)
+ butter_bridge_2_region = create_region(multiworld, player, active_locations, LocationName.butter_bridge_2_region, None)
+ butter_bridge_2_exit_1 = create_region(multiworld, player, active_locations, LocationName.butter_bridge_2_exit_1,
[LocationName.butter_bridge_2_exit_1])
- cheese_bridge_tile = create_region(world, player, active_locations, LocationName.cheese_bridge_tile, None)
- cheese_bridge_region = create_region(world, player, active_locations, LocationName.cheese_bridge_region, None)
- cheese_bridge_exit_1 = create_region(world, player, active_locations, LocationName.cheese_bridge_exit_1,
+ cheese_bridge_tile = create_region(multiworld, player, active_locations, LocationName.cheese_bridge_tile, None)
+ cheese_bridge_region = create_region(multiworld, player, active_locations, LocationName.cheese_bridge_region, None)
+ cheese_bridge_exit_1 = create_region(multiworld, player, active_locations, LocationName.cheese_bridge_exit_1,
[LocationName.cheese_bridge_exit_1])
- cheese_bridge_exit_2 = create_region(world, player, active_locations, LocationName.cheese_bridge_exit_2,
+ cheese_bridge_exit_2 = create_region(multiworld, player, active_locations, LocationName.cheese_bridge_exit_2,
[LocationName.cheese_bridge_exit_2])
- cookie_mountain_tile = create_region(world, player, active_locations, LocationName.cookie_mountain_tile, None)
- cookie_mountain_region = create_region(world, player, active_locations, LocationName.cookie_mountain_region, None)
- cookie_mountain_exit_1 = create_region(world, player, active_locations, LocationName.cookie_mountain_exit_1,
+ cookie_mountain_tile = create_region(multiworld, player, active_locations, LocationName.cookie_mountain_tile, None)
+ cookie_mountain_region = create_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, None)
+ cookie_mountain_exit_1 = create_region(multiworld, player, active_locations, LocationName.cookie_mountain_exit_1,
[LocationName.cookie_mountain_exit_1])
- soda_lake_tile = create_region(world, player, active_locations, LocationName.soda_lake_tile, None)
- soda_lake_region = create_region(world, player, active_locations, LocationName.soda_lake_region, None)
- soda_lake_exit_1 = create_region(world, player, active_locations, LocationName.soda_lake_exit_1,
+ soda_lake_tile = create_region(multiworld, player, active_locations, LocationName.soda_lake_tile, None)
+ soda_lake_region = create_region(multiworld, player, active_locations, LocationName.soda_lake_region, None)
+ soda_lake_exit_1 = create_region(multiworld, player, active_locations, LocationName.soda_lake_exit_1,
[LocationName.soda_lake_exit_1])
- twin_bridges_castle_tile = create_region(world, player, active_locations, LocationName.twin_bridges_castle_tile, None)
- twin_bridges_castle_region = create_region(world, player, active_locations, LocationName.twin_bridges_castle_region, None)
- twin_bridges_castle = create_region(world, player, active_locations, LocationName.twin_bridges_castle,
+ twin_bridges_castle_tile = create_region(multiworld, player, active_locations, LocationName.twin_bridges_castle_tile, None)
+ twin_bridges_castle_region = create_region(multiworld, player, active_locations, LocationName.twin_bridges_castle_region, None)
+ twin_bridges_castle = create_region(multiworld, player, active_locations, LocationName.twin_bridges_castle,
[LocationName.twin_bridges_castle, LocationName.twin_bridges_koopaling])
- forest_of_illusion_1_tile = create_region(world, player, active_locations, LocationName.forest_of_illusion_1_tile, None)
- forest_of_illusion_1_region = create_region(world, player, active_locations, LocationName.forest_of_illusion_1_region, None)
- forest_of_illusion_1_exit_1 = create_region(world, player, active_locations, LocationName.forest_of_illusion_1_exit_1,
+ forest_of_illusion_1_tile = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_1_tile, None)
+ forest_of_illusion_1_region = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_1_region, None)
+ forest_of_illusion_1_exit_1 = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_1_exit_1,
[LocationName.forest_of_illusion_1_exit_1])
- forest_of_illusion_1_exit_2 = create_region(world, player, active_locations, LocationName.forest_of_illusion_1_exit_2,
+ forest_of_illusion_1_exit_2 = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_1_exit_2,
[LocationName.forest_of_illusion_1_exit_2])
- forest_of_illusion_2_tile = create_region(world, player, active_locations, LocationName.forest_of_illusion_2_tile, None)
- forest_of_illusion_2_region = create_region(world, player, active_locations, LocationName.forest_of_illusion_2_region, None)
- forest_of_illusion_2_exit_1 = create_region(world, player, active_locations, LocationName.forest_of_illusion_2_exit_1,
+ forest_of_illusion_2_tile = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_tile, None)
+ forest_of_illusion_2_region = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_region, None)
+ forest_of_illusion_2_exit_1 = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_exit_1,
[LocationName.forest_of_illusion_2_exit_1])
- forest_of_illusion_2_exit_2 = create_region(world, player, active_locations, LocationName.forest_of_illusion_2_exit_2,
+ forest_of_illusion_2_exit_2 = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_exit_2,
[LocationName.forest_of_illusion_2_exit_2])
- forest_of_illusion_3_tile = create_region(world, player, active_locations, LocationName.forest_of_illusion_3_tile, None)
- forest_of_illusion_3_region = create_region(world, player, active_locations, LocationName.forest_of_illusion_3_region, None)
- forest_of_illusion_3_exit_1 = create_region(world, player, active_locations, LocationName.forest_of_illusion_3_exit_1,
+ forest_of_illusion_3_tile = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_tile, None)
+ forest_of_illusion_3_region = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, None)
+ forest_of_illusion_3_exit_1 = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_exit_1,
[LocationName.forest_of_illusion_3_exit_1])
- forest_of_illusion_3_exit_2 = create_region(world, player, active_locations, LocationName.forest_of_illusion_3_exit_2,
+ forest_of_illusion_3_exit_2 = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_exit_2,
[LocationName.forest_of_illusion_3_exit_2])
- forest_of_illusion_4_tile = create_region(world, player, active_locations, LocationName.forest_of_illusion_4_tile, None)
- forest_of_illusion_4_region = create_region(world, player, active_locations, LocationName.forest_of_illusion_4_region, None)
- forest_of_illusion_4_exit_1 = create_region(world, player, active_locations, LocationName.forest_of_illusion_4_exit_1,
+ forest_of_illusion_4_tile = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_tile, None)
+ forest_of_illusion_4_region = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, None)
+ forest_of_illusion_4_exit_1 = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_exit_1,
[LocationName.forest_of_illusion_4_exit_1])
- forest_of_illusion_4_exit_2 = create_region(world, player, active_locations, LocationName.forest_of_illusion_4_exit_2,
+ forest_of_illusion_4_exit_2 = create_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_exit_2,
[LocationName.forest_of_illusion_4_exit_2])
- forest_ghost_house_tile = create_region(world, player, active_locations, LocationName.forest_ghost_house_tile, None)
- forest_ghost_house_region = create_region(world, player, active_locations, LocationName.forest_ghost_house_region, None)
- forest_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.forest_ghost_house_exit_1,
+ forest_ghost_house_tile = create_region(multiworld, player, active_locations, LocationName.forest_ghost_house_tile, None)
+ forest_ghost_house_region = create_region(multiworld, player, active_locations, LocationName.forest_ghost_house_region, None)
+ forest_ghost_house_exit_1 = create_region(multiworld, player, active_locations, LocationName.forest_ghost_house_exit_1,
[LocationName.forest_ghost_house_exit_1])
- forest_ghost_house_exit_2 = create_region(world, player, active_locations, LocationName.forest_ghost_house_exit_2,
+ forest_ghost_house_exit_2 = create_region(multiworld, player, active_locations, LocationName.forest_ghost_house_exit_2,
[LocationName.forest_ghost_house_exit_2])
- forest_secret_tile = create_region(world, player, active_locations, LocationName.forest_secret_tile, None)
- forest_secret_region = create_region(world, player, active_locations, LocationName.forest_secret_region, None)
- forest_secret_exit_1 = create_region(world, player, active_locations, LocationName.forest_secret_exit_1,
+ forest_secret_tile = create_region(multiworld, player, active_locations, LocationName.forest_secret_tile, None)
+ forest_secret_region = create_region(multiworld, player, active_locations, LocationName.forest_secret_region, None)
+ forest_secret_exit_1 = create_region(multiworld, player, active_locations, LocationName.forest_secret_exit_1,
[LocationName.forest_secret_exit_1])
- forest_fortress_tile = create_region(world, player, active_locations, LocationName.forest_fortress_tile, None)
- forest_fortress_region = create_region(world, player, active_locations, LocationName.forest_fortress_region, None)
- forest_fortress = create_region(world, player, active_locations, LocationName.forest_fortress,
+ forest_fortress_tile = create_region(multiworld, player, active_locations, LocationName.forest_fortress_tile, None)
+ forest_fortress_region = create_region(multiworld, player, active_locations, LocationName.forest_fortress_region, None)
+ forest_fortress = create_region(multiworld, player, active_locations, LocationName.forest_fortress,
[LocationName.forest_fortress, LocationName.forest_reznor])
- forest_castle_tile = create_region(world, player, active_locations, LocationName.forest_castle_tile, None)
- forest_castle_region = create_region(world, player, active_locations, LocationName.forest_castle_region, None)
- forest_castle = create_region(world, player, active_locations, LocationName.forest_castle,
+ forest_castle_tile = create_region(multiworld, player, active_locations, LocationName.forest_castle_tile, None)
+ forest_castle_region = create_region(multiworld, player, active_locations, LocationName.forest_castle_region, None)
+ forest_castle = create_region(multiworld, player, active_locations, LocationName.forest_castle,
[LocationName.forest_castle, LocationName.forest_koopaling])
- blue_switch_palace_tile = create_region(world, player, active_locations, LocationName.blue_switch_palace_tile, None)
- blue_switch_palace = create_region(world, player, active_locations, LocationName.blue_switch_palace,
+ blue_switch_palace_tile = create_region(multiworld, player, active_locations, LocationName.blue_switch_palace_tile, None)
+ blue_switch_palace = create_region(multiworld, player, active_locations, LocationName.blue_switch_palace,
[LocationName.blue_switch_palace])
- chocolate_island_1_tile = create_region(world, player, active_locations, LocationName.chocolate_island_1_tile, None)
- chocolate_island_1_region = create_region(world, player, active_locations, LocationName.chocolate_island_1_region, None)
- chocolate_island_1_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_1_exit_1,
+ chocolate_island_1_tile = create_region(multiworld, player, active_locations, LocationName.chocolate_island_1_tile, None)
+ chocolate_island_1_region = create_region(multiworld, player, active_locations, LocationName.chocolate_island_1_region, None)
+ chocolate_island_1_exit_1 = create_region(multiworld, player, active_locations, LocationName.chocolate_island_1_exit_1,
[LocationName.chocolate_island_1_exit_1])
- chocolate_island_2_tile = create_region(world, player, active_locations, LocationName.chocolate_island_2_tile, None)
- chocolate_island_2_region = create_region(world, player, active_locations, LocationName.chocolate_island_2_region, None)
- chocolate_island_2_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_2_exit_1,
+ chocolate_island_2_tile = create_region(multiworld, player, active_locations, LocationName.chocolate_island_2_tile, None)
+ chocolate_island_2_region = create_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, None)
+ chocolate_island_2_exit_1 = create_region(multiworld, player, active_locations, LocationName.chocolate_island_2_exit_1,
[LocationName.chocolate_island_2_exit_1])
- chocolate_island_2_exit_2 = create_region(world, player, active_locations, LocationName.chocolate_island_2_exit_2,
+ chocolate_island_2_exit_2 = create_region(multiworld, player, active_locations, LocationName.chocolate_island_2_exit_2,
[LocationName.chocolate_island_2_exit_2])
- chocolate_island_3_tile = create_region(world, player, active_locations, LocationName.chocolate_island_3_tile, None)
- chocolate_island_3_region = create_region(world, player, active_locations, LocationName.chocolate_island_3_region, None)
- chocolate_island_3_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_3_exit_1,
+ chocolate_island_3_tile = create_region(multiworld, player, active_locations, LocationName.chocolate_island_3_tile, None)
+ chocolate_island_3_region = create_region(multiworld, player, active_locations, LocationName.chocolate_island_3_region, None)
+ chocolate_island_3_exit_1 = create_region(multiworld, player, active_locations, LocationName.chocolate_island_3_exit_1,
[LocationName.chocolate_island_3_exit_1])
- chocolate_island_3_exit_2 = create_region(world, player, active_locations, LocationName.chocolate_island_3_exit_2,
+ chocolate_island_3_exit_2 = create_region(multiworld, player, active_locations, LocationName.chocolate_island_3_exit_2,
[LocationName.chocolate_island_3_exit_2])
- chocolate_island_4_tile = create_region(world, player, active_locations, LocationName.chocolate_island_4_tile, None)
- chocolate_island_4_region = create_region(world, player, active_locations, LocationName.chocolate_island_4_region, None)
- chocolate_island_4_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_4_exit_1,
+ chocolate_island_4_tile = create_region(multiworld, player, active_locations, LocationName.chocolate_island_4_tile, None)
+ chocolate_island_4_region = create_region(multiworld, player, active_locations, LocationName.chocolate_island_4_region, None)
+ chocolate_island_4_exit_1 = create_region(multiworld, player, active_locations, LocationName.chocolate_island_4_exit_1,
[LocationName.chocolate_island_4_exit_1])
- chocolate_island_5_tile = create_region(world, player, active_locations, LocationName.chocolate_island_5_tile, None)
- chocolate_island_5_region = create_region(world, player, active_locations, LocationName.chocolate_island_5_region, None)
- chocolate_island_5_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_5_exit_1,
+ chocolate_island_5_tile = create_region(multiworld, player, active_locations, LocationName.chocolate_island_5_tile, None)
+ chocolate_island_5_region = create_region(multiworld, player, active_locations, LocationName.chocolate_island_5_region, None)
+ chocolate_island_5_exit_1 = create_region(multiworld, player, active_locations, LocationName.chocolate_island_5_exit_1,
[LocationName.chocolate_island_5_exit_1])
- chocolate_ghost_house_tile = create_region(world, player, active_locations, LocationName.chocolate_ghost_house_tile, None)
- chocolate_ghost_house_region = create_region(world, player, active_locations, LocationName.chocolate_ghost_house_region, None)
- chocolate_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_ghost_house_exit_1,
+ chocolate_ghost_house_tile = create_region(multiworld, player, active_locations, LocationName.chocolate_ghost_house_tile, None)
+ chocolate_ghost_house_region = create_region(multiworld, player, active_locations, LocationName.chocolate_ghost_house_region, None)
+ chocolate_ghost_house_exit_1 = create_region(multiworld, player, active_locations, LocationName.chocolate_ghost_house_exit_1,
[LocationName.chocolate_ghost_house_exit_1])
- chocolate_secret_tile = create_region(world, player, active_locations, LocationName.chocolate_secret_tile, None)
- chocolate_secret_region = create_region(world, player, active_locations, LocationName.chocolate_secret_region, None)
- chocolate_secret_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_secret_exit_1,
+ chocolate_secret_tile = create_region(multiworld, player, active_locations, LocationName.chocolate_secret_tile, None)
+ chocolate_secret_region = create_region(multiworld, player, active_locations, LocationName.chocolate_secret_region, None)
+ chocolate_secret_exit_1 = create_region(multiworld, player, active_locations, LocationName.chocolate_secret_exit_1,
[LocationName.chocolate_secret_exit_1])
- chocolate_fortress_tile = create_region(world, player, active_locations, LocationName.chocolate_fortress_tile, None)
- chocolate_fortress_region = create_region(world, player, active_locations, LocationName.chocolate_fortress_region, None)
- chocolate_fortress = create_region(world, player, active_locations, LocationName.chocolate_fortress,
+ chocolate_fortress_tile = create_region(multiworld, player, active_locations, LocationName.chocolate_fortress_tile, None)
+ chocolate_fortress_region = create_region(multiworld, player, active_locations, LocationName.chocolate_fortress_region, None)
+ chocolate_fortress = create_region(multiworld, player, active_locations, LocationName.chocolate_fortress,
[LocationName.chocolate_fortress, LocationName.chocolate_reznor])
- chocolate_castle_tile = create_region(world, player, active_locations, LocationName.chocolate_castle_tile, None)
- chocolate_castle_region = create_region(world, player, active_locations, LocationName.chocolate_castle_region, None)
- chocolate_castle = create_region(world, player, active_locations, LocationName.chocolate_castle,
+ chocolate_castle_tile = create_region(multiworld, player, active_locations, LocationName.chocolate_castle_tile, None)
+ chocolate_castle_region = create_region(multiworld, player, active_locations, LocationName.chocolate_castle_region, None)
+ chocolate_castle = create_region(multiworld, player, active_locations, LocationName.chocolate_castle,
[LocationName.chocolate_castle, LocationName.chocolate_koopaling])
- sunken_ghost_ship_tile = create_region(world, player, active_locations, LocationName.sunken_ghost_ship_tile, None)
- sunken_ghost_ship_region = create_region(world, player, active_locations, LocationName.sunken_ghost_ship_region, None)
- sunken_ghost_ship = create_region(world, player, active_locations, LocationName.sunken_ghost_ship,
+ sunken_ghost_ship_tile = create_region(multiworld, player, active_locations, LocationName.sunken_ghost_ship_tile, None)
+ sunken_ghost_ship_region = create_region(multiworld, player, active_locations, LocationName.sunken_ghost_ship_region, None)
+ sunken_ghost_ship = create_region(multiworld, player, active_locations, LocationName.sunken_ghost_ship,
[LocationName.sunken_ghost_ship])
- valley_of_bowser_1_tile = create_region(world, player, active_locations, LocationName.valley_of_bowser_1_tile, None)
- valley_of_bowser_1_region = create_region(world, player, active_locations, LocationName.valley_of_bowser_1_region, None)
- valley_of_bowser_1_exit_1 = create_region(world, player, active_locations, LocationName.valley_of_bowser_1_exit_1,
+ valley_of_bowser_1_tile = create_region(multiworld, player, active_locations, LocationName.valley_of_bowser_1_tile, None)
+ valley_of_bowser_1_region = create_region(multiworld, player, active_locations, LocationName.valley_of_bowser_1_region, None)
+ valley_of_bowser_1_exit_1 = create_region(multiworld, player, active_locations, LocationName.valley_of_bowser_1_exit_1,
[LocationName.valley_of_bowser_1_exit_1])
- valley_of_bowser_2_tile = create_region(world, player, active_locations, LocationName.valley_of_bowser_2_tile, None)
- valley_of_bowser_2_region = create_region(world, player, active_locations, LocationName.valley_of_bowser_2_region, None)
- valley_of_bowser_2_exit_1 = create_region(world, player, active_locations, LocationName.valley_of_bowser_2_exit_1,
+ valley_of_bowser_2_tile = create_region(multiworld, player, active_locations, LocationName.valley_of_bowser_2_tile, None)
+ valley_of_bowser_2_region = create_region(multiworld, player, active_locations, LocationName.valley_of_bowser_2_region, None)
+ valley_of_bowser_2_exit_1 = create_region(multiworld, player, active_locations, LocationName.valley_of_bowser_2_exit_1,
[LocationName.valley_of_bowser_2_exit_1])
- valley_of_bowser_2_exit_2 = create_region(world, player, active_locations, LocationName.valley_of_bowser_2_exit_2,
+ valley_of_bowser_2_exit_2 = create_region(multiworld, player, active_locations, LocationName.valley_of_bowser_2_exit_2,
[LocationName.valley_of_bowser_2_exit_2])
- valley_of_bowser_3_tile = create_region(world, player, active_locations, LocationName.valley_of_bowser_3_tile, None)
- valley_of_bowser_3_region = create_region(world, player, active_locations, LocationName.valley_of_bowser_3_region, None)
- valley_of_bowser_3_exit_1 = create_region(world, player, active_locations, LocationName.valley_of_bowser_3_exit_1,
+ valley_of_bowser_3_tile = create_region(multiworld, player, active_locations, LocationName.valley_of_bowser_3_tile, None)
+ valley_of_bowser_3_region = create_region(multiworld, player, active_locations, LocationName.valley_of_bowser_3_region, None)
+ valley_of_bowser_3_exit_1 = create_region(multiworld, player, active_locations, LocationName.valley_of_bowser_3_exit_1,
[LocationName.valley_of_bowser_3_exit_1])
- valley_of_bowser_4_tile = create_region(world, player, active_locations, LocationName.valley_of_bowser_4_tile, None)
- valley_of_bowser_4_region = create_region(world, player, active_locations, LocationName.valley_of_bowser_4_region, None)
- valley_of_bowser_4_exit_1 = create_region(world, player, active_locations, LocationName.valley_of_bowser_4_exit_1,
+ valley_of_bowser_4_tile = create_region(multiworld, player, active_locations, LocationName.valley_of_bowser_4_tile, None)
+ valley_of_bowser_4_region = create_region(multiworld, player, active_locations, LocationName.valley_of_bowser_4_region, None)
+ valley_of_bowser_4_exit_1 = create_region(multiworld, player, active_locations, LocationName.valley_of_bowser_4_exit_1,
[LocationName.valley_of_bowser_4_exit_1])
- valley_of_bowser_4_exit_2 = create_region(world, player, active_locations, LocationName.valley_of_bowser_4_exit_2,
+ valley_of_bowser_4_exit_2 = create_region(multiworld, player, active_locations, LocationName.valley_of_bowser_4_exit_2,
[LocationName.valley_of_bowser_4_exit_2])
- valley_ghost_house_tile = create_region(world, player, active_locations, LocationName.valley_ghost_house_tile, None)
- valley_ghost_house_region = create_region(world, player, active_locations, LocationName.valley_ghost_house_region, None)
- valley_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.valley_ghost_house_exit_1,
+ valley_ghost_house_tile = create_region(multiworld, player, active_locations, LocationName.valley_ghost_house_tile, None)
+ valley_ghost_house_region = create_region(multiworld, player, active_locations, LocationName.valley_ghost_house_region, None)
+ valley_ghost_house_exit_1 = create_region(multiworld, player, active_locations, LocationName.valley_ghost_house_exit_1,
[LocationName.valley_ghost_house_exit_1])
- valley_ghost_house_exit_2 = create_region(world, player, active_locations, LocationName.valley_ghost_house_exit_2,
+ valley_ghost_house_exit_2 = create_region(multiworld, player, active_locations, LocationName.valley_ghost_house_exit_2,
[LocationName.valley_ghost_house_exit_2])
- valley_fortress_tile = create_region(world, player, active_locations, LocationName.valley_fortress_tile, None)
- valley_fortress_region = create_region(world, player, active_locations, LocationName.valley_fortress_region, None)
- valley_fortress = create_region(world, player, active_locations, LocationName.valley_fortress,
+ valley_fortress_tile = create_region(multiworld, player, active_locations, LocationName.valley_fortress_tile, None)
+ valley_fortress_region = create_region(multiworld, player, active_locations, LocationName.valley_fortress_region, None)
+ valley_fortress = create_region(multiworld, player, active_locations, LocationName.valley_fortress,
[LocationName.valley_fortress, LocationName.valley_reznor])
- valley_castle_tile = create_region(world, player, active_locations, LocationName.valley_castle_tile, None)
- valley_castle_region = create_region(world, player, active_locations, LocationName.valley_castle_region, None)
- valley_castle = create_region(world, player, active_locations, LocationName.valley_castle,
+ valley_castle_tile = create_region(multiworld, player, active_locations, LocationName.valley_castle_tile, None)
+ valley_castle_region = create_region(multiworld, player, active_locations, LocationName.valley_castle_region, None)
+ valley_castle = create_region(multiworld, player, active_locations, LocationName.valley_castle,
[LocationName.valley_castle, LocationName.valley_koopaling])
- front_door_tile = create_region(world, player, active_locations, LocationName.front_door_tile, None)
- front_door_region = create_region(world, player, active_locations, LocationName.front_door, None)
- back_door_tile = create_region(world, player, active_locations, LocationName.back_door_tile, None)
- back_door_region = create_region(world, player, active_locations, LocationName.back_door, None)
+ front_door_tile = create_region(multiworld, player, active_locations, LocationName.front_door_tile, None)
+ front_door_region = create_region(multiworld, player, active_locations, LocationName.front_door, None)
+ back_door_tile = create_region(multiworld, player, active_locations, LocationName.back_door_tile, None)
+ back_door_region = create_region(multiworld, player, active_locations, LocationName.back_door, None)
bowser_region_locations = []
- if world.goal[player] == "bowser":
+ if world.options.goal == "bowser":
bowser_region_locations += [LocationName.bowser]
- bowser_region = create_region(world, player, active_locations, LocationName.bowser_region, bowser_region_locations)
-
-
- donut_plains_star_road = create_region(world, player, active_locations, LocationName.donut_plains_star_road, None)
- vanilla_dome_star_road = create_region(world, player, active_locations, LocationName.vanilla_dome_star_road, None)
- twin_bridges_star_road = create_region(world, player, active_locations, LocationName.twin_bridges_star_road, None)
- forest_star_road = create_region(world, player, active_locations, LocationName.forest_star_road, None)
- valley_star_road = create_region(world, player, active_locations, LocationName.valley_star_road, None)
- star_road_donut = create_region(world, player, active_locations, LocationName.star_road_donut, None)
- star_road_vanilla = create_region(world, player, active_locations, LocationName.star_road_vanilla, None)
- star_road_twin_bridges = create_region(world, player, active_locations, LocationName.star_road_twin_bridges, None)
- star_road_forest = create_region(world, player, active_locations, LocationName.star_road_forest, None)
- star_road_valley = create_region(world, player, active_locations, LocationName.star_road_valley, None)
- star_road_special = create_region(world, player, active_locations, LocationName.star_road_special, None)
- special_star_road = create_region(world, player, active_locations, LocationName.special_star_road, None)
-
- star_road_1_tile = create_region(world, player, active_locations, LocationName.star_road_1_tile, None)
- star_road_1_region = create_region(world, player, active_locations, LocationName.star_road_1_region, None)
- star_road_1_exit_1 = create_region(world, player, active_locations, LocationName.star_road_1_exit_1,
+ bowser_region = create_region(multiworld, player, active_locations, LocationName.bowser_region, bowser_region_locations)
+
+
+ donut_plains_star_road = create_region(multiworld, player, active_locations, LocationName.donut_plains_star_road, None)
+ vanilla_dome_star_road = create_region(multiworld, player, active_locations, LocationName.vanilla_dome_star_road, None)
+ twin_bridges_star_road = create_region(multiworld, player, active_locations, LocationName.twin_bridges_star_road, None)
+ forest_star_road = create_region(multiworld, player, active_locations, LocationName.forest_star_road, None)
+ valley_star_road = create_region(multiworld, player, active_locations, LocationName.valley_star_road, None)
+ star_road_donut = create_region(multiworld, player, active_locations, LocationName.star_road_donut, None)
+ star_road_vanilla = create_region(multiworld, player, active_locations, LocationName.star_road_vanilla, None)
+ star_road_twin_bridges = create_region(multiworld, player, active_locations, LocationName.star_road_twin_bridges, None)
+ star_road_forest = create_region(multiworld, player, active_locations, LocationName.star_road_forest, None)
+ star_road_valley = create_region(multiworld, player, active_locations, LocationName.star_road_valley, None)
+ star_road_special = create_region(multiworld, player, active_locations, LocationName.star_road_special, None)
+ special_star_road = create_region(multiworld, player, active_locations, LocationName.special_star_road, None)
+
+ star_road_1_tile = create_region(multiworld, player, active_locations, LocationName.star_road_1_tile, None)
+ star_road_1_region = create_region(multiworld, player, active_locations, LocationName.star_road_1_region, None)
+ star_road_1_exit_1 = create_region(multiworld, player, active_locations, LocationName.star_road_1_exit_1,
[LocationName.star_road_1_exit_1])
- star_road_1_exit_2 = create_region(world, player, active_locations, LocationName.star_road_1_exit_2,
+ star_road_1_exit_2 = create_region(multiworld, player, active_locations, LocationName.star_road_1_exit_2,
[LocationName.star_road_1_exit_2])
- star_road_2_tile = create_region(world, player, active_locations, LocationName.star_road_2_tile, None)
- star_road_2_region = create_region(world, player, active_locations, LocationName.star_road_2_region, None)
- star_road_2_exit_1 = create_region(world, player, active_locations, LocationName.star_road_2_exit_1,
+ star_road_2_tile = create_region(multiworld, player, active_locations, LocationName.star_road_2_tile, None)
+ star_road_2_region = create_region(multiworld, player, active_locations, LocationName.star_road_2_region, None)
+ star_road_2_exit_1 = create_region(multiworld, player, active_locations, LocationName.star_road_2_exit_1,
[LocationName.star_road_2_exit_1])
- star_road_2_exit_2 = create_region(world, player, active_locations, LocationName.star_road_2_exit_2,
+ star_road_2_exit_2 = create_region(multiworld, player, active_locations, LocationName.star_road_2_exit_2,
[LocationName.star_road_2_exit_2])
- star_road_3_tile = create_region(world, player, active_locations, LocationName.star_road_3_tile, None)
- star_road_3_region = create_region(world, player, active_locations, LocationName.star_road_3_region, None)
- star_road_3_exit_1 = create_region(world, player, active_locations, LocationName.star_road_3_exit_1,
+ star_road_3_tile = create_region(multiworld, player, active_locations, LocationName.star_road_3_tile, None)
+ star_road_3_region = create_region(multiworld, player, active_locations, LocationName.star_road_3_region, None)
+ star_road_3_exit_1 = create_region(multiworld, player, active_locations, LocationName.star_road_3_exit_1,
[LocationName.star_road_3_exit_1])
- star_road_3_exit_2 = create_region(world, player, active_locations, LocationName.star_road_3_exit_2,
+ star_road_3_exit_2 = create_region(multiworld, player, active_locations, LocationName.star_road_3_exit_2,
[LocationName.star_road_3_exit_2])
- star_road_4_tile = create_region(world, player, active_locations, LocationName.star_road_4_tile, None)
- star_road_4_region = create_region(world, player, active_locations, LocationName.star_road_4_region, None)
- star_road_4_exit_1 = create_region(world, player, active_locations, LocationName.star_road_4_exit_1,
+ star_road_4_tile = create_region(multiworld, player, active_locations, LocationName.star_road_4_tile, None)
+ star_road_4_region = create_region(multiworld, player, active_locations, LocationName.star_road_4_region, None)
+ star_road_4_exit_1 = create_region(multiworld, player, active_locations, LocationName.star_road_4_exit_1,
[LocationName.star_road_4_exit_1])
- star_road_4_exit_2 = create_region(world, player, active_locations, LocationName.star_road_4_exit_2,
+ star_road_4_exit_2 = create_region(multiworld, player, active_locations, LocationName.star_road_4_exit_2,
[LocationName.star_road_4_exit_2])
- star_road_5_tile = create_region(world, player, active_locations, LocationName.star_road_5_tile, None)
- star_road_5_region = create_region(world, player, active_locations, LocationName.star_road_5_region, None)
- star_road_5_exit_1 = create_region(world, player, active_locations, LocationName.star_road_5_exit_1,
+ star_road_5_tile = create_region(multiworld, player, active_locations, LocationName.star_road_5_tile, None)
+ star_road_5_region = create_region(multiworld, player, active_locations, LocationName.star_road_5_region, None)
+ star_road_5_exit_1 = create_region(multiworld, player, active_locations, LocationName.star_road_5_exit_1,
[LocationName.star_road_5_exit_1])
- star_road_5_exit_2 = create_region(world, player, active_locations, LocationName.star_road_5_exit_2,
+ star_road_5_exit_2 = create_region(multiworld, player, active_locations, LocationName.star_road_5_exit_2,
[LocationName.star_road_5_exit_2])
- special_zone_1_tile = create_region(world, player, active_locations, LocationName.special_zone_1_tile, None)
- special_zone_1_region = create_region(world, player, active_locations, LocationName.special_zone_1_region, None)
- special_zone_1_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_1_exit_1,
+ special_zone_1_tile = create_region(multiworld, player, active_locations, LocationName.special_zone_1_tile, None)
+ special_zone_1_region = create_region(multiworld, player, active_locations, LocationName.special_zone_1_region, None)
+ special_zone_1_exit_1 = create_region(multiworld, player, active_locations, LocationName.special_zone_1_exit_1,
[LocationName.special_zone_1_exit_1])
- special_zone_2_tile = create_region(world, player, active_locations, LocationName.special_zone_2_tile, None)
- special_zone_2_region = create_region(world, player, active_locations, LocationName.special_zone_2_region, None)
- special_zone_2_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_2_exit_1,
+ special_zone_2_tile = create_region(multiworld, player, active_locations, LocationName.special_zone_2_tile, None)
+ special_zone_2_region = create_region(multiworld, player, active_locations, LocationName.special_zone_2_region, None)
+ special_zone_2_exit_1 = create_region(multiworld, player, active_locations, LocationName.special_zone_2_exit_1,
[LocationName.special_zone_2_exit_1])
- special_zone_3_tile = create_region(world, player, active_locations, LocationName.special_zone_3_tile, None)
- special_zone_3_region = create_region(world, player, active_locations, LocationName.special_zone_3_region, None)
- special_zone_3_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_3_exit_1,
+ special_zone_3_tile = create_region(multiworld, player, active_locations, LocationName.special_zone_3_tile, None)
+ special_zone_3_region = create_region(multiworld, player, active_locations, LocationName.special_zone_3_region, None)
+ special_zone_3_exit_1 = create_region(multiworld, player, active_locations, LocationName.special_zone_3_exit_1,
[LocationName.special_zone_3_exit_1])
- special_zone_4_tile = create_region(world, player, active_locations, LocationName.special_zone_4_tile, None)
- special_zone_4_region = create_region(world, player, active_locations, LocationName.special_zone_4_region, None)
- special_zone_4_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_4_exit_1,
+ special_zone_4_tile = create_region(multiworld, player, active_locations, LocationName.special_zone_4_tile, None)
+ special_zone_4_region = create_region(multiworld, player, active_locations, LocationName.special_zone_4_region, None)
+ special_zone_4_exit_1 = create_region(multiworld, player, active_locations, LocationName.special_zone_4_exit_1,
[LocationName.special_zone_4_exit_1])
- special_zone_5_tile = create_region(world, player, active_locations, LocationName.special_zone_5_tile, None)
- special_zone_5_region = create_region(world, player, active_locations, LocationName.special_zone_5_region, None)
- special_zone_5_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_5_exit_1,
+ special_zone_5_tile = create_region(multiworld, player, active_locations, LocationName.special_zone_5_tile, None)
+ special_zone_5_region = create_region(multiworld, player, active_locations, LocationName.special_zone_5_region, None)
+ special_zone_5_exit_1 = create_region(multiworld, player, active_locations, LocationName.special_zone_5_exit_1,
[LocationName.special_zone_5_exit_1])
- special_zone_6_tile = create_region(world, player, active_locations, LocationName.special_zone_6_tile, None)
- special_zone_6_region = create_region(world, player, active_locations, LocationName.special_zone_6_region, None)
- special_zone_6_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_6_exit_1,
+ special_zone_6_tile = create_region(multiworld, player, active_locations, LocationName.special_zone_6_tile, None)
+ special_zone_6_region = create_region(multiworld, player, active_locations, LocationName.special_zone_6_region, None)
+ special_zone_6_exit_1 = create_region(multiworld, player, active_locations, LocationName.special_zone_6_exit_1,
[LocationName.special_zone_6_exit_1])
- special_zone_7_tile = create_region(world, player, active_locations, LocationName.special_zone_7_tile, None)
- special_zone_7_region = create_region(world, player, active_locations, LocationName.special_zone_7_region, None)
- special_zone_7_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_7_exit_1,
+ special_zone_7_tile = create_region(multiworld, player, active_locations, LocationName.special_zone_7_tile, None)
+ special_zone_7_region = create_region(multiworld, player, active_locations, LocationName.special_zone_7_region, None)
+ special_zone_7_exit_1 = create_region(multiworld, player, active_locations, LocationName.special_zone_7_exit_1,
[LocationName.special_zone_7_exit_1])
- special_zone_8_tile = create_region(world, player, active_locations, LocationName.special_zone_8_tile, None)
- special_zone_8_region = create_region(world, player, active_locations, LocationName.special_zone_8_region, None)
- special_zone_8_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_8_exit_1,
+ special_zone_8_tile = create_region(multiworld, player, active_locations, LocationName.special_zone_8_tile, None)
+ special_zone_8_region = create_region(multiworld, player, active_locations, LocationName.special_zone_8_region, None)
+ special_zone_8_exit_1 = create_region(multiworld, player, active_locations, LocationName.special_zone_8_exit_1,
[LocationName.special_zone_8_exit_1])
- special_complete = create_region(world, player, active_locations, LocationName.special_complete, None)
+ special_complete = create_region(multiworld, player, active_locations, LocationName.special_complete, None)
# Set up the regions correctly.
- world.regions += [
+ multiworld.regions += [
menu_region,
yoshis_island_region,
yoshis_house_tile,
@@ -725,323 +728,1327 @@ def create_regions(world, player: int, active_locations):
]
- if world.dragon_coin_checks[player]:
- add_location_to_region(world, player, active_locations, LocationName.yoshis_island_1_region, LocationName.yoshis_island_1_dragon,
+ if world.options.dragon_coin_checks:
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_1_region, LocationName.yoshis_island_1_dragon,
lambda state: (state.has(ItemName.mario_spin_jump, player) and
state.has(ItemName.progressive_powerup, player, 1)))
- add_location_to_region(world, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_dragon,
lambda state: (state.has(ItemName.yoshi_activate, player) or
state.has(ItemName.mario_climb, player)))
- add_location_to_region(world, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_dragon,
lambda state: state.has(ItemName.p_switch, player))
- add_location_to_region(world, player, active_locations, LocationName.yoshis_island_4_region, LocationName.yoshis_island_4_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_4_region, LocationName.yoshis_island_4_dragon,
lambda state: (state.has(ItemName.yoshi_activate, player) or
state.has(ItemName.mario_swim, player) or
(state.has(ItemName.mario_carry, player) and state.has(ItemName.p_switch, player))))
- add_location_to_region(world, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_dragon,
lambda state: (state.has(ItemName.mario_climb, player) or
state.has(ItemName.yoshi_activate, player) or
(state.has(ItemName.progressive_powerup, player, 3) and state.has(ItemName.mario_run, player))))
- add_location_to_region(world, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_dragon)
- add_location_to_region(world, player, active_locations, LocationName.donut_plains_3_region, LocationName.donut_plains_3_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_dragon)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_3_region, LocationName.donut_plains_3_dragon,
lambda state: ((state.has(ItemName.mario_spin_jump, player) and state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_climb, player) or
state.has(ItemName.yoshi_activate, player) or
(state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))))
- add_location_to_region(world, player, active_locations, LocationName.donut_plains_4_region, LocationName.donut_plains_4_dragon)
- add_location_to_region(world, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_4_region, LocationName.donut_plains_4_dragon)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_dragon,
lambda state: state.has(ItemName.mario_swim, player))
- add_location_to_region(world, player, active_locations, LocationName.donut_secret_2_region, LocationName.donut_secret_2_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_2_region, LocationName.donut_secret_2_dragon,
lambda state: (state.has(ItemName.mario_climb, player) or state.has(ItemName.yoshi_activate, player)))
- add_location_to_region(world, player, active_locations, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_dragon,
lambda state: (state.has(ItemName.mario_carry, player) and
state.has(ItemName.mario_run, player) and
(state.has(ItemName.super_star_active, player) or
state.has(ItemName.progressive_powerup, player, 1))))
- add_location_to_region(world, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_dragon,
lambda state: (state.has(ItemName.mario_swim, player) and
state.has(ItemName.p_switch, player) and
(state.has(ItemName.mario_climb, player) or state.has(ItemName.yoshi_activate, player))))
- add_location_to_region(world, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_dragon)
- add_location_to_region(world, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_dragon)
- add_location_to_region(world, player, active_locations, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_dragon)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_dragon)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_dragon,
lambda state: (state.has(ItemName.mario_climb, player) and
state.has(ItemName.mario_carry, player)))
- add_location_to_region(world, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_dragon,
lambda state: (state.has(ItemName.mario_run, player) and
state.has(ItemName.progressive_powerup, player, 3)))
- add_location_to_region(world, player, active_locations, LocationName.vanilla_secret_3_region, LocationName.vanilla_secret_3_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_3_region, LocationName.vanilla_secret_3_dragon,
lambda state: state.has(ItemName.mario_swim, player))
- add_location_to_region(world, player, active_locations, LocationName.vanilla_ghost_house_region, LocationName.vanilla_ghost_house_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_ghost_house_region, LocationName.vanilla_ghost_house_dragon,
lambda state: state.has(ItemName.mario_climb, player))
- add_location_to_region(world, player, active_locations, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_dragon)
- add_location_to_region(world, player, active_locations, LocationName.butter_bridge_2_region, LocationName.butter_bridge_2_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_dragon)
+ add_location_to_region(multiworld, player, active_locations, LocationName.butter_bridge_2_region, LocationName.butter_bridge_2_dragon,
lambda state: (state.has(ItemName.mario_run, player) and
state.has(ItemName.progressive_powerup, player, 3)))
- add_location_to_region(world, player, active_locations, LocationName.cheese_bridge_region, LocationName.cheese_bridge_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.cheese_bridge_region, LocationName.cheese_bridge_dragon,
lambda state: (state.has(ItemName.yoshi_activate, player) or
state.has(ItemName.mario_climb, player)))
- add_location_to_region(world, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_dragon,
lambda state: (state.has(ItemName.yoshi_activate, player) or
state.has(ItemName.mario_climb, player)))
- add_location_to_region(world, player, active_locations, LocationName.soda_lake_region, LocationName.soda_lake_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.soda_lake_region, LocationName.soda_lake_dragon,
lambda state: state.has(ItemName.mario_swim, player))
- add_location_to_region(world, player, active_locations, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_dragon,
lambda state: state.has(ItemName.mario_swim, player))
- add_location_to_region(world, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_dragon,
lambda state: (state.has(ItemName.yoshi_activate, player) or
state.has(ItemName.mario_carry, player)))
- add_location_to_region(world, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_dragon,
lambda state: (state.has(ItemName.yoshi_activate, player) or
state.has(ItemName.mario_carry, player) or
state.has(ItemName.p_switch, player) or
state.has(ItemName.progressive_powerup, player, 2)))
- add_location_to_region(world, player, active_locations, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_dragon,
lambda state: state.has(ItemName.p_switch, player))
- add_location_to_region(world, player, active_locations, LocationName.forest_secret_region, LocationName.forest_secret_dragon)
- add_location_to_region(world, player, active_locations, LocationName.forest_castle_region, LocationName.forest_castle_dragon)
- add_location_to_region(world, player, active_locations, LocationName.chocolate_island_1_region, LocationName.chocolate_island_1_dragon,
- lambda state: state.has(ItemName.mario_swim, player))
- add_location_to_region(world, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_secret_region, LocationName.forest_secret_dragon)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_castle_region, LocationName.forest_castle_dragon)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_1_region, LocationName.chocolate_island_1_dragon,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_dragon,
lambda state: (state.has(ItemName.blue_switch_palace, player) and
(state.has(ItemName.p_switch, player) or
state.has(ItemName.green_switch_palace, player) or
(state.has(ItemName.yellow_switch_palace, player) or state.has(ItemName.red_switch_palace, player)))))
- add_location_to_region(world, player, active_locations, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_dragon)
- add_location_to_region(world, player, active_locations, LocationName.chocolate_island_4_region, LocationName.chocolate_island_4_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_dragon)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_4_region, LocationName.chocolate_island_4_dragon,
lambda state: (state.has(ItemName.p_switch, player) and
state.has(ItemName.progressive_powerup, player, 3)))
- add_location_to_region(world, player, active_locations, LocationName.chocolate_island_5_region, LocationName.chocolate_island_5_dragon,
- lambda state: (state.has(ItemName.mario_swim, player) or
- (state.has(ItemName.mario_carry, player) and state.has(ItemName.p_switch, player))))
- add_location_to_region(world, player, active_locations, LocationName.sunken_ghost_ship_region, LocationName.sunken_ghost_ship_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_5_region, LocationName.chocolate_island_5_dragon,
+ lambda state: (state.has(ItemName.mario_carry, player) and state.has(ItemName.p_switch, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.sunken_ghost_ship_region, LocationName.sunken_ghost_ship_dragon,
lambda state: (state.has(ItemName.mario_swim, player) and
state.has(ItemName.super_star_active, player) and
state.has(ItemName.progressive_powerup, player, 3)))
- add_location_to_region(world, player, active_locations, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_dragon)
- add_location_to_region(world, player, active_locations, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_dragon)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_dragon,
lambda state: state.has(ItemName.yoshi_activate, player))
- add_location_to_region(world, player, active_locations, LocationName.valley_of_bowser_3_region, LocationName.valley_of_bowser_3_dragon)
- add_location_to_region(world, player, active_locations, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_3_region, LocationName.valley_of_bowser_3_dragon)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_dragon,
lambda state: state.has(ItemName.p_switch, player))
- add_location_to_region(world, player, active_locations, LocationName.valley_castle_region, LocationName.valley_castle_dragon)
- add_location_to_region(world, player, active_locations, LocationName.star_road_1_region, LocationName.star_road_1_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_castle_region, LocationName.valley_castle_dragon)
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_1_region, LocationName.star_road_1_dragon,
lambda state: (state.has(ItemName.mario_spin_jump, player) and
state.has(ItemName.progressive_powerup, player, 1)))
- add_location_to_region(world, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_dragon,
lambda state: state.has(ItemName.mario_climb, player))
- add_location_to_region(world, player, active_locations, LocationName.special_zone_2_region, LocationName.special_zone_2_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_2_region, LocationName.special_zone_2_dragon,
lambda state: state.has(ItemName.p_balloon, player))
- add_location_to_region(world, player, active_locations, LocationName.special_zone_3_region, LocationName.special_zone_3_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_3_region, LocationName.special_zone_3_dragon,
lambda state: state.has(ItemName.yoshi_activate, player))
- add_location_to_region(world, player, active_locations, LocationName.special_zone_4_region, LocationName.special_zone_4_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_4_region, LocationName.special_zone_4_dragon,
lambda state: state.has(ItemName.progressive_powerup, player, 1))
- add_location_to_region(world, player, active_locations, LocationName.special_zone_5_region, LocationName.special_zone_5_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_5_region, LocationName.special_zone_5_dragon,
lambda state: state.has(ItemName.progressive_powerup, player, 1))
- add_location_to_region(world, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_dragon,
lambda state: state.has(ItemName.mario_swim, player))
- add_location_to_region(world, player, active_locations, LocationName.special_zone_7_region, LocationName.special_zone_7_dragon,
- lambda state: state.has(ItemName.progressive_powerup, player, 1))
- add_location_to_region(world, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_dragon,
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_7_region, LocationName.special_zone_7_dragon,
lambda state: state.has(ItemName.progressive_powerup, player, 1))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_dragon,
+ lambda state: ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player)) or
+ state.has(ItemName.progressive_powerup, player, 3) or
+ state.has(ItemName.yoshi_activate, player) or
+ state.has(ItemName.mario_carry, player)))
+
+ if world.options.moon_checks:
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_1_region, LocationName.yoshis_island_1_moon,
+ lambda state: ((state.has(ItemName.mario_run, player) and
+ state.has(ItemName.progressive_powerup, player, 3)) or
+ state.has(ItemName.yoshi_activate, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_4_region, LocationName.donut_plains_4_moon,
+ lambda state: (state.has(ItemName.mario_run, player) and
+ state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_moon,
+ lambda state: (state.has(ItemName.mario_run, player) and
+ state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.cheese_bridge_region, LocationName.cheese_bridge_moon,
+ lambda state: (state.has(ItemName.mario_run, player) and
+ (state.has(ItemName.progressive_powerup, player, 3) or
+ state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_moon,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_1_region, LocationName.chocolate_island_1_moon,
+ lambda state: ((state.has(ItemName.mario_run, player) and
+ state.has(ItemName.progressive_powerup, player, 3)) or
+ state.has(ItemName.yoshi_activate, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_moon)
+
+ if world.options.hidden_1up_checks:
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_4_region, LocationName.yoshis_island_4_hidden_1up,
+ lambda state: (state.has(ItemName.yoshi_activate, player) or
+ (state.has(ItemName.mario_run, player, player) and
+ state.has(ItemName.progressive_powerup, player, 3))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_hidden_1up)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_4_region, LocationName.donut_plains_4_hidden_1up,
+ lambda state: (state.has(ItemName.mario_run, player) and
+ state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle_hidden_1up)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_ghost_house_region, LocationName.vanilla_ghost_house_hidden_1up)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_hidden_1up)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_fortress_region, LocationName.vanilla_fortress_hidden_1up,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_hidden_1up,
+ lambda state: (state.has(ItemName.mario_swim, player) or
+ state.has(ItemName.yoshi_activate, player) or
+ (state.has(ItemName.mario_run, player, player) and
+ state.has(ItemName.progressive_powerup, player, 3))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_hidden_1up,
+ lambda state: (state.has(ItemName.mario_carry, player) or
+ state.has(ItemName.yoshi_activate, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_hidden_1up)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_castle_region, LocationName.chocolate_castle_hidden_1up,
+ lambda state: (state.has(ItemName.progressive_powerup, player, 1)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_hidden_1up)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_castle_region, LocationName.valley_castle_hidden_1up)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_hidden_1up,
+ lambda state: state.has(ItemName.mario_climb, player))
+
+ if world.options.bonus_block_checks:
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_bonus_block)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_3_region, LocationName.donut_plains_3_bonus_block)
+ add_location_to_region(multiworld, player, active_locations, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_bonus_block)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_bonus_block)
+
+ if world.options.blocksanity:
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_yoshi_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_green_block_1,
+ lambda state:( ((state.has(ItemName.green_switch_palace, player) and state.has(ItemName.mario_carry, player))) or ((state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_multi_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_gray_pow_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_coin_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_coin_block_4)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_coin_block_5)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_coin_block_6)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_3_region, LocationName.vanilla_secret_3_powerup_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_3_region, LocationName.vanilla_secret_3_powerup_block_2,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_ghost_house_region, LocationName.donut_ghost_house_vine_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_ghost_house_region, LocationName.donut_ghost_house_directional_coin_block_1,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_ghost_house_region, LocationName.donut_ghost_house_life_block_1,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_ghost_house_region, LocationName.donut_ghost_house_life_block_2,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_ghost_house_region, LocationName.donut_ghost_house_life_block_3,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_ghost_house_region, LocationName.donut_ghost_house_life_block_4,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_3_region, LocationName.donut_plains_3_green_block_1,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_3_region, LocationName.donut_plains_3_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_3_region, LocationName.donut_plains_3_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_3_region, LocationName.donut_plains_3_vine_block_1,
+ lambda state: (state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_3_region, LocationName.donut_plains_3_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_3_region, LocationName.donut_plains_3_bonus_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_4_region, LocationName.donut_plains_4_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_4_region, LocationName.donut_plains_4_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_4_region, LocationName.donut_plains_4_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_4_region, LocationName.donut_plains_4_yoshi_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle_yellow_block_1,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle_vine_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle_invis_life_block_1,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle_coin_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle_coin_block_4)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle_coin_block_5)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle_green_block_1,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_coin_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_yellow_block_1,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_multi_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_flying_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_green_block_1,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_yellow_block_2,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_vine_block_1,
+ lambda state:( ((state.has(ItemName.mario_carry, player) and state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_coin_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_coin_block_2,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_powerup_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_coin_block_3,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_powerup_block_2,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_powerup_block_3,
+ lambda state: (state.has(ItemName.mario_swim, player) and state.has(ItemName.p_balloon, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_life_block_1,
+ lambda state: (state.has(ItemName.mario_swim, player) and state.has(ItemName.p_balloon, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_powerup_block_4,
+ lambda state: (state.has(ItemName.mario_swim, player) and state.has(ItemName.p_balloon, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_powerup_block_5,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_key_block_1,
+ lambda state: (state.has(ItemName.mario_swim, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.p_switch, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_fortress_region, LocationName.vanilla_fortress_powerup_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_fortress_region, LocationName.vanilla_fortress_powerup_block_2,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_fortress_region, LocationName.vanilla_fortress_yellow_block_1,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_swim, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_multi_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_multi_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_multi_coin_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_life_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_bonus_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.butter_bridge_2_region, LocationName.butter_bridge_2_powerup_block_1,
+ lambda state: state.has(ItemName.mario_carry, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.butter_bridge_2_region, LocationName.butter_bridge_2_green_block_1,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.butter_bridge_2_region, LocationName.butter_bridge_2_yoshi_block_1,
+ lambda state: state.has(ItemName.mario_carry, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.twin_bridges_castle_region, LocationName.twin_bridges_castle_powerup_block_1,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.cheese_bridge_region, LocationName.cheese_bridge_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cheese_bridge_region, LocationName.cheese_bridge_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cheese_bridge_region, LocationName.cheese_bridge_wings_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cheese_bridge_region, LocationName.cheese_bridge_powerup_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_4)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_5)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_6)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_7)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_8)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_9)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_life_block_1,
+ lambda state:( (state.has(ItemName.mario_climb, player)) or (state.has(ItemName.mario_swim, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_vine_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_yoshi_block_1,
+ lambda state: state.has(ItemName.red_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_10)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_11)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_12)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_13)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_14)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_15)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_16)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_17)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_18)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_19)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_20)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_21)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_22)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_23)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_24)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_25)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_26)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_27)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_28)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_29)
+ add_location_to_region(multiworld, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_coin_block_30)
+ add_location_to_region(multiworld, player, active_locations, LocationName.soda_lake_region, LocationName.soda_lake_powerup_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_house_region, LocationName.donut_secret_house_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_house_region, LocationName.donut_secret_house_multi_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_house_region, LocationName.donut_secret_house_life_block_1,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_house_region, LocationName.donut_secret_house_vine_block_1,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_house_region, LocationName.donut_secret_house_directional_coin_block_1,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_yoshi_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_vine_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_1,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_2,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_3,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_4,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_5,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_6,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_7,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_8,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_9,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_10,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_11,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_12,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_13,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_14,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_15,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_green_block_16,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_yellow_block_1,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_yellow_block_2,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_yellow_block_3,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.sunken_ghost_ship_region, LocationName.sunken_ghost_ship_powerup_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.sunken_ghost_ship_region, LocationName.sunken_ghost_ship_star_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_castle_region, LocationName.chocolate_castle_yellow_block_1,
+ lambda state: (state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.yellow_switch_palace, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_castle_region, LocationName.chocolate_castle_yellow_block_2,
+ lambda state: (state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.yellow_switch_palace, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_castle_region, LocationName.chocolate_castle_green_block_1,
+ lambda state: (state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.green_switch_palace, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_fortress_region, LocationName.chocolate_fortress_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_fortress_region, LocationName.chocolate_fortress_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_fortress_region, LocationName.chocolate_fortress_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_fortress_region, LocationName.chocolate_fortress_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_fortress_region, LocationName.chocolate_fortress_green_block_1,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_5_region, LocationName.chocolate_island_5_yoshi_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_5_region, LocationName.chocolate_island_5_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_5_region, LocationName.chocolate_island_5_life_block_1,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.progressive_powerup, player, 3))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_5_region, LocationName.chocolate_island_5_yellow_block_1,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.mario_carry, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_4_region, LocationName.chocolate_island_4_yellow_block_1,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.blue_switch_palace, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_4_region, LocationName.chocolate_island_4_blue_pow_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_4_region, LocationName.chocolate_island_4_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_fortress_region, LocationName.forest_fortress_yellow_block_1,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_fortress_region, LocationName.forest_fortress_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_fortress_region, LocationName.forest_fortress_life_block_1,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_fortress_region, LocationName.forest_fortress_life_block_2,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_fortress_region, LocationName.forest_fortress_life_block_3,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_fortress_region, LocationName.forest_fortress_life_block_4,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_fortress_region, LocationName.forest_fortress_life_block_5,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_fortress_region, LocationName.forest_fortress_life_block_6,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_fortress_region, LocationName.forest_fortress_life_block_7,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_fortress_region, LocationName.forest_fortress_life_block_8,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_fortress_region, LocationName.forest_fortress_life_block_9,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_castle_region, LocationName.forest_castle_green_block_1,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_ghost_house_region, LocationName.chocolate_ghost_house_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_ghost_house_region, LocationName.chocolate_ghost_house_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_ghost_house_region, LocationName.chocolate_ghost_house_life_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_1_region, LocationName.chocolate_island_1_flying_block_1,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_1_region, LocationName.chocolate_island_1_flying_block_2,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_1_region, LocationName.chocolate_island_1_yoshi_block_1,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_1_region, LocationName.chocolate_island_1_green_block_1,
+ lambda state:( ((state.has(ItemName.green_switch_palace, player) and state.has(ItemName.blue_switch_palace, player) and state.has(ItemName.p_switch, player))) or ((state.has(ItemName.green_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3) and state.has(ItemName.p_switch, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.blue_switch_palace, player) and state.has(ItemName.p_switch, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3) and state.has(ItemName.p_switch, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_1_region, LocationName.chocolate_island_1_life_block_1,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_powerup_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_green_block_1,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_bonus_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_vine_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_life_block_1,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_life_block_2,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_life_block_3,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_multi_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_invis_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_yoshi_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_multi_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_blue_pow_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_yellow_block_1,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_yellow_block_2,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_green_block_1,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_green_block_2,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_green_block_3,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_green_block_4,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_green_block_5,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_green_block_6,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_castle_region, LocationName.yoshis_island_castle_coin_block_1,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_castle_region, LocationName.yoshis_island_castle_coin_block_2,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_castle_region, LocationName.yoshis_island_castle_powerup_block_1,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_castle_region, LocationName.yoshis_island_castle_coin_block_3,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_castle_region, LocationName.yoshis_island_castle_coin_block_4,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_castle_region, LocationName.yoshis_island_castle_flying_block_1,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_4_region, LocationName.yoshis_island_4_yellow_block_1,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_4_region, LocationName.yoshis_island_4_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_4_region, LocationName.yoshis_island_4_multi_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_4_region, LocationName.yoshis_island_4_star_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_yellow_block_1,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_yellow_block_2,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_yellow_block_3,
+ lambda state:( ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_carry, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_yellow_block_4,
+ lambda state:( ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_carry, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_yellow_block_5,
+ lambda state:( ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_carry, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_yellow_block_6,
+ lambda state:( ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_carry, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_yellow_block_7,
+ lambda state:( ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_carry, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_yellow_block_8,
+ lambda state:( ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_carry, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_yellow_block_9,
+ lambda state:( ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_carry, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_yoshi_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_yellow_block_10,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_yellow_block_11,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_yellow_block_12,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_bonus_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_1_region, LocationName.yoshis_island_1_flying_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_1_region, LocationName.yoshis_island_1_yellow_block_1,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_1_region, LocationName.yoshis_island_1_life_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_1_region, LocationName.yoshis_island_1_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_flying_block_1,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_flying_block_2,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_flying_block_3,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_flying_block_4,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_flying_block_5,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_flying_block_6,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_yellow_block_1,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_coin_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_yoshi_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_coin_block_4)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_yoshi_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_coin_block_5)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_vine_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_yellow_block_2,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_ghost_house_region, LocationName.vanilla_ghost_house_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_ghost_house_region, LocationName.vanilla_ghost_house_vine_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_ghost_house_region, LocationName.vanilla_ghost_house_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_ghost_house_region, LocationName.vanilla_ghost_house_multi_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_ghost_house_region, LocationName.vanilla_ghost_house_blue_pow_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_multi_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_vine_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_vine_block_2,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_coin_block_2,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_coin_block_3,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_powerup_block_2,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_flying_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_flying_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_flying_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_invis_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_multi_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_powerup_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_yoshi_block_1,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_powerup_block_4)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_pswitch_coin_block_1,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3) and state.has(ItemName.p_switch, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_pswitch_coin_block_2,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3) and state.has(ItemName.p_switch, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_pswitch_coin_block_3,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3) and state.has(ItemName.p_switch, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_pswitch_coin_block_4,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3) and state.has(ItemName.p_switch, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_pswitch_coin_block_5,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3) and state.has(ItemName.p_switch, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_pswitch_coin_block_6,
+ lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3) and state.has(ItemName.p_switch, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_2_region, LocationName.donut_secret_2_directional_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_2_region, LocationName.donut_secret_2_vine_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_2_region, LocationName.donut_secret_2_star_block_1,
+ lambda state:( (state.has(ItemName.mario_climb, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_2_region, LocationName.donut_secret_2_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_2_region, LocationName.donut_secret_2_star_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_4_region, LocationName.valley_of_bowser_4_yellow_block_1,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_4_region, LocationName.valley_of_bowser_4_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_4_region, LocationName.valley_of_bowser_4_vine_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_4_region, LocationName.valley_of_bowser_4_yoshi_block_1,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_4_region, LocationName.valley_of_bowser_4_life_block_1,
+ lambda state: (state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player) and state.has(ItemName.mario_climb, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_4_region, LocationName.valley_of_bowser_4_powerup_block_2,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_climb, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_castle_region, LocationName.valley_castle_yellow_block_1,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_castle_region, LocationName.valley_castle_yellow_block_2,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_castle_region, LocationName.valley_castle_green_block_1,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_fortress_region, LocationName.valley_fortress_green_block_1,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_fortress_region, LocationName.valley_fortress_yellow_block_1,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_3_region, LocationName.valley_of_bowser_3_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_3_region, LocationName.valley_of_bowser_3_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_pswitch_coin_block_1,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_multi_coin_block_1,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_directional_coin_block_1,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_yellow_block_1,
+ lambda state: state.has(ItemName.yellow_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_wings_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_green_block_1,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_invis_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_invis_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_invis_coin_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_yellow_block_1,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_yellow_block_2,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_yellow_block_3,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_yellow_block_4,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_vine_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_secret_region, LocationName.chocolate_secret_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_secret_region, LocationName.chocolate_secret_powerup_block_2,
+ lambda state: state.has(ItemName.mario_run, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_coin_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_powerup_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_coin_block_2,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_coin_block_3,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_vine_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_invis_life_block_1,
+ lambda state:( ((state.has(ItemName.mario_swim, player) and state.has(ItemName.mario_climb, player))) or ((state.has(ItemName.mario_swim, player) and state.has(ItemName.yoshi_activate, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_coin_block_4,
+ lambda state:( ((state.has(ItemName.mario_swim, player) and state.has(ItemName.mario_climb, player))) or ((state.has(ItemName.mario_swim, player) and state.has(ItemName.yoshi_activate, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_coin_block_5,
+ lambda state:( ((state.has(ItemName.mario_swim, player) and state.has(ItemName.mario_climb, player))) or ((state.has(ItemName.mario_swim, player) and state.has(ItemName.yoshi_activate, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_powerup_block_2,
+ lambda state:( ((state.has(ItemName.mario_swim, player) and state.has(ItemName.mario_climb, player))) or ((state.has(ItemName.mario_swim, player) and state.has(ItemName.yoshi_activate, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_powerup_block_3,
+ lambda state:( ((state.has(ItemName.mario_swim, player) and state.has(ItemName.mario_climb, player))) or ((state.has(ItemName.mario_swim, player) and state.has(ItemName.yoshi_activate, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_powerup_block_4,
+ lambda state:( ((state.has(ItemName.mario_swim, player) and state.has(ItemName.mario_climb, player))) or ((state.has(ItemName.mario_swim, player) and state.has(ItemName.yoshi_activate, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_powerup_block_5,
+ lambda state:( ((state.has(ItemName.mario_swim, player) and state.has(ItemName.mario_climb, player))) or ((state.has(ItemName.mario_swim, player) and state.has(ItemName.yoshi_activate, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_multi_coin_block_1,
+ lambda state:( ((state.has(ItemName.mario_swim, player) and state.has(ItemName.mario_climb, player))) or ((state.has(ItemName.mario_swim, player) and state.has(ItemName.yoshi_activate, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_multi_coin_block_2,
+ lambda state:( ((state.has(ItemName.mario_swim, player) and state.has(ItemName.mario_climb, player))) or ((state.has(ItemName.mario_swim, player) and state.has(ItemName.yoshi_activate, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_coin_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_life_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_coin_block_4)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_coin_block_5)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_coin_block_6)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_coin_block_7)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_coin_block_8,
+ lambda state: state.has(ItemName.mario_carry, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_flying_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_life_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_powerup_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_vine_block_1,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.red_switch_palace, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_star_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_powerup_block_4,
+ lambda state:( ((state.has(ItemName.mario_run, player) and state.has(ItemName.super_star_active, player))) or ((state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 1)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_coin_block_2,
+ lambda state:( ((state.has(ItemName.mario_run, player) and state.has(ItemName.super_star_active, player))) or ((state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 1)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_castle_region, LocationName.vanilla_dome_castle_life_block_1,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.progressive_powerup, player, 1))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_castle_region, LocationName.vanilla_dome_castle_life_block_2,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.progressive_powerup, player, 1))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_castle_region, LocationName.vanilla_dome_castle_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_castle_region, LocationName.vanilla_dome_castle_life_block_3,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.vanilla_dome_castle_region, LocationName.vanilla_dome_castle_green_block_1,
+ lambda state: state.has(ItemName.green_switch_palace, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_flying_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_life_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_1_region, LocationName.forest_of_illusion_1_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_1_region, LocationName.forest_of_illusion_1_yoshi_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_1_region, LocationName.forest_of_illusion_1_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_1_region, LocationName.forest_of_illusion_1_key_block_1,
+ lambda state: state.has(ItemName.p_balloon, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_1_region, LocationName.forest_of_illusion_1_life_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_multi_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_coin_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_coin_block_4)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_coin_block_5)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_coin_block_6)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_coin_block_7)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_coin_block_8)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_coin_block_9)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_coin_block_10)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_green_block_1,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.mario_swim, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_powerup_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_invis_coin_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_invis_coin_block_2,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_invis_life_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_invis_coin_block_3,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_yellow_block_1,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.mario_swim, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_secret_region, LocationName.forest_secret_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_secret_region, LocationName.forest_secret_powerup_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_secret_region, LocationName.forest_secret_life_block_1,
+ lambda state:( (state.has(ItemName.blue_switch_palace, player)) or (state.has(ItemName.mario_carry, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_yoshi_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_multi_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_2,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_multi_coin_block_2,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_3,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_4,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_5,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_6,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_7,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_8,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_9,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_10,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_11,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_12,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_13,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_14,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_15,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_16,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_17,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_18,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_19,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_20,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_21,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_22,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_23,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_coin_block_24,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_yoshi_block_1,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_4)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_5)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_blue_pow_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_star_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_6,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_7,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_8,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_9,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_10,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_11,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_12,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_13,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_14,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_15,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_16,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_17,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_18,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_multi_coin_block_1,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_19,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_20,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_21,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_22,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_coin_block_23,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_powerup_block_2,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_flying_block_1,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.progressive_powerup, player, 3)) or (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.yoshi_activate, player))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_7_region, LocationName.special_zone_7_powerup_block_1,
+ lambda state: state.has(ItemName.progressive_powerup, player, 1))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_7_region, LocationName.special_zone_7_yoshi_block_1,
+ lambda state: state.has(ItemName.progressive_powerup, player, 1))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_7_region, LocationName.special_zone_7_coin_block_1,
+ lambda state: state.has(ItemName.progressive_powerup, player, 1))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_7_region, LocationName.special_zone_7_powerup_block_2,
+ lambda state: state.has(ItemName.progressive_powerup, player, 1))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_7_region, LocationName.special_zone_7_coin_block_2,
+ lambda state: state.has(ItemName.progressive_powerup, player, 1))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_powerup_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_2,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_yoshi_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_life_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_multi_coin_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_3,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_4,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_5,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_6,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_7,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_8,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_9,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_10,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_11,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_12,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_13,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_14,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_15,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_16,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_17,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_18,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_19,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_20,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_21,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_22,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_23,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_24,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_25,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_26,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_27,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_28,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_powerup_block_2,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_29,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_30,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_31,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_32,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_coin_block_33,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_5_region, LocationName.special_zone_5_yoshi_block_1,
+ lambda state: state.has(ItemName.progressive_powerup, player, 1))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_vine_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_vine_block_2)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_vine_block_3)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_vine_block_4)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_life_block_1,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_vine_block_5,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_blue_pow_block_1,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_vine_block_6,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_powerup_block_1,
+ lambda state: state.has(ItemName.mario_climb, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_pswitch_coin_block_1,
+ lambda state: (state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_pswitch_coin_block_2,
+ lambda state: (state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_pswitch_coin_block_3,
+ lambda state: (state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_pswitch_coin_block_4,
+ lambda state: (state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_pswitch_coin_block_5,
+ lambda state: (state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_pswitch_coin_block_6,
+ lambda state: (state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_pswitch_coin_block_7,
+ lambda state: (state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_pswitch_coin_block_8,
+ lambda state: (state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_pswitch_coin_block_9,
+ lambda state: (state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.progressive_powerup, player, 3)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_pswitch_coin_block_10,
+ lambda state:( ((state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.progressive_powerup, player, 3))) or ((state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.mario_carry, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_pswitch_coin_block_11,
+ lambda state:( ((state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.progressive_powerup, player, 3))) or ((state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.mario_carry, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_pswitch_coin_block_12,
+ lambda state:( ((state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.progressive_powerup, player, 3))) or ((state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.mario_carry, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_pswitch_coin_block_13,
+ lambda state:( ((state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.progressive_powerup, player, 3))) or ((state.has(ItemName.mario_climb, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.mario_carry, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_2_region, LocationName.special_zone_2_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_2_region, LocationName.special_zone_2_coin_block_1,
+ lambda state: state.has(ItemName.p_balloon, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_2_region, LocationName.special_zone_2_coin_block_2,
+ lambda state: state.has(ItemName.p_balloon, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_2_region, LocationName.special_zone_2_powerup_block_2,
+ lambda state: state.has(ItemName.p_balloon, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_2_region, LocationName.special_zone_2_coin_block_3,
+ lambda state: state.has(ItemName.p_balloon, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_2_region, LocationName.special_zone_2_coin_block_4,
+ lambda state: state.has(ItemName.p_balloon, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_2_region, LocationName.special_zone_2_powerup_block_3,
+ lambda state: state.has(ItemName.p_balloon, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_2_region, LocationName.special_zone_2_multi_coin_block_1,
+ lambda state: state.has(ItemName.p_balloon, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_2_region, LocationName.special_zone_2_coin_block_5,
+ lambda state: state.has(ItemName.p_balloon, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_2_region, LocationName.special_zone_2_coin_block_6,
+ lambda state: state.has(ItemName.p_balloon, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_3_region, LocationName.special_zone_3_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_3_region, LocationName.special_zone_3_yoshi_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_3_region, LocationName.special_zone_3_wings_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_4_region, LocationName.special_zone_4_powerup_block_1,
+ lambda state: state.has(ItemName.progressive_powerup, player, 2))
+ add_location_to_region(multiworld, player, active_locations, LocationName.special_zone_4_region, LocationName.special_zone_4_star_block_1,
+ lambda state:( ((state.has(ItemName.progressive_powerup, player, 2) and state.has(ItemName.mario_carry, player))) or ((state.has(ItemName.progressive_powerup, player, 2) and state.has(ItemName.p_switch, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_2_region, LocationName.star_road_2_star_block_1,
+ lambda state: state.has(ItemName.mario_swim, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_3_region, LocationName.star_road_3_key_block_1,
+ lambda state:( (state.has(ItemName.mario_carry, player)) or (state.has(ItemName.progressive_powerup, player, 2))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_4_region, LocationName.star_road_4_powerup_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_4_region, LocationName.star_road_4_green_block_1,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_4_region, LocationName.star_road_4_green_block_2,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_4_region, LocationName.star_road_4_green_block_3,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_4_region, LocationName.star_road_4_green_block_4,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_4_region, LocationName.star_road_4_green_block_5,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_4_region, LocationName.star_road_4_green_block_6,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_4_region, LocationName.star_road_4_green_block_7,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_4_region, LocationName.star_road_4_key_block_1,
+ lambda state:( ((state.has(ItemName.mario_carry, player) and state.has(ItemName.yoshi_activate, player))) or ((state.has(ItemName.green_switch_palace, player) and state.has(ItemName.red_switch_palace, player) and state.has(ItemName.mario_carry, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_directional_coin_block_1)
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_life_block_1,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_vine_block_1,
+ lambda state: state.has(ItemName.p_switch, player))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_1,
+ lambda state:( ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.mario_climb, player) and state.has(ItemName.mario_carry, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_2,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_3,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_4,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_5,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_6,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_7,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_8,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_9,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_10,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_11,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_12,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_13,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_14,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_15,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_16,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_17,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_18,
+ lambda state: (state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_19,
+ lambda state:( ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.green_switch_palace, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.mario_climb, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.progressive_powerup, player, 3)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_yellow_block_20,
+ lambda state:( ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player))) or ((state.has(ItemName.yellow_switch_palace, player) and state.has(ItemName.green_switch_palace, player) and state.has(ItemName.p_switch, player) and state.has(ItemName.mario_climb, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.progressive_powerup, player, 3)))))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_1,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_2,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_3,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_4,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_5,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_6,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_7,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_8,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_9,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_10,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_11,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_12,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_13,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_14,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_15,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_16,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_17,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_18,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_19,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+ add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_green_block_20,
+ lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player) and state.has(ItemName.special_world_clear, player)))
+
+def connect_regions(world: World, level_to_tile_dict):
+ multiworld: MultiWorld = world.multiworld
+ player: int = world.player
-
-
-def connect_regions(world, player, level_to_tile_dict):
names: typing.Dict[str, int] = {}
- connect(world, player, names, "Menu", LocationName.yoshis_island_region)
- connect(world, player, names, LocationName.yoshis_island_region, LocationName.yoshis_house_tile)
- connect(world, player, names, LocationName.yoshis_house_tile, LocationName.donut_plains_top_secret)
- connect(world, player, names, LocationName.yoshis_island_region, LocationName.yoshis_island_1_tile)
- connect(world, player, names, LocationName.yoshis_island_region, LocationName.yoshis_island_2_tile)
+ connect(world, "Menu", LocationName.yoshis_island_region)
+ connect(world, LocationName.yoshis_island_region, LocationName.yoshis_house_tile)
+ connect(world, LocationName.yoshis_island_region, LocationName.yoshis_island_1_tile)
+ connect(world, LocationName.yoshis_island_region, LocationName.yoshis_island_2_tile)
# Connect regions within levels using rules
- connect(world, player, names, LocationName.yoshis_island_1_region, LocationName.yoshis_island_1_exit_1)
- connect(world, player, names, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_exit_1)
- connect(world, player, names, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_exit_1)
- connect(world, player, names, LocationName.yoshis_island_4_region, LocationName.yoshis_island_4_exit_1)
- connect(world, player, names, LocationName.yoshis_island_castle_region, LocationName.yoshis_island_castle,
+ connect(world, LocationName.yoshis_island_1_region, LocationName.yoshis_island_1_exit_1)
+ connect(world, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_exit_1)
+ connect(world, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_exit_1)
+ connect(world, LocationName.yoshis_island_4_region, LocationName.yoshis_island_4_exit_1)
+ connect(world, LocationName.yoshis_island_castle_region, LocationName.yoshis_island_castle,
lambda state: (state.has(ItemName.mario_climb, player)))
- connect(world, player, names, LocationName.donut_plains_1_region, LocationName.donut_plains_1_exit_1)
- connect(world, player, names, LocationName.donut_plains_1_region, LocationName.donut_plains_1_exit_2,
+ connect(world, LocationName.donut_plains_1_region, LocationName.donut_plains_1_exit_1)
+ connect(world, LocationName.donut_plains_1_region, LocationName.donut_plains_1_exit_2,
lambda state: (state.has(ItemName.mario_carry, player) and
(state.has(ItemName.yoshi_activate, player) or
state.has(ItemName.green_switch_palace, player) or
(state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))))
- connect(world, player, names, LocationName.donut_plains_2_region, LocationName.donut_plains_2_exit_1)
- connect(world, player, names, LocationName.donut_plains_2_region, LocationName.donut_plains_2_exit_2,
+ connect(world, LocationName.donut_plains_2_region, LocationName.donut_plains_2_exit_1)
+ connect(world, LocationName.donut_plains_2_region, LocationName.donut_plains_2_exit_2,
lambda state: (state.has(ItemName.mario_carry, player) and
(state.has(ItemName.yoshi_activate, player) or
(state.has(ItemName.mario_spin_jump, player) and state.has(ItemName.mario_climb, player) and state.has(ItemName.progressive_powerup, player, 1)))))
- connect(world, player, names, LocationName.donut_secret_1_region, LocationName.donut_secret_1_exit_1,
+ connect(world, LocationName.donut_secret_1_region, LocationName.donut_secret_1_exit_1,
lambda state: state.has(ItemName.mario_swim, player))
- connect(world, player, names, LocationName.donut_secret_1_region, LocationName.donut_secret_1_exit_2,
+ connect(world, LocationName.donut_secret_1_region, LocationName.donut_secret_1_exit_2,
lambda state: (state.has(ItemName.mario_carry, player) and
state.has(ItemName.mario_swim, player) and
state.has(ItemName.p_switch, player)))
- connect(world, player, names, LocationName.donut_ghost_house_region, LocationName.donut_ghost_house_exit_1,
+ connect(world, LocationName.donut_ghost_house_region, LocationName.donut_ghost_house_exit_1,
lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
- connect(world, player, names, LocationName.donut_ghost_house_region, LocationName.donut_ghost_house_exit_2,
+ connect(world, LocationName.donut_ghost_house_region, LocationName.donut_ghost_house_exit_2,
lambda state: (state.has(ItemName.mario_climb, player) or
(state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))))
- connect(world, player, names, LocationName.donut_secret_house_region, LocationName.donut_secret_house_exit_1,
+ connect(world, LocationName.donut_secret_house_region, LocationName.donut_secret_house_exit_1,
lambda state: state.has(ItemName.p_switch, player))
- connect(world, player, names, LocationName.donut_secret_house_region, LocationName.donut_secret_house_exit_2,
+ connect(world, LocationName.donut_secret_house_region, LocationName.donut_secret_house_exit_2,
lambda state: (state.has(ItemName.p_switch, player) and state.has(ItemName.mario_carry, player) and
(state.has(ItemName.mario_climb, player) or
(state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))))
- connect(world, player, names, LocationName.donut_plains_3_region, LocationName.donut_plains_3_exit_1)
- connect(world, player, names, LocationName.donut_plains_4_region, LocationName.donut_plains_4_exit_1)
- connect(world, player, names, LocationName.donut_secret_2_region, LocationName.donut_secret_2_exit_1)
- connect(world, player, names, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle)
+ connect(world, LocationName.donut_plains_3_region, LocationName.donut_plains_3_exit_1)
+ connect(world, LocationName.donut_plains_4_region, LocationName.donut_plains_4_exit_1)
+ connect(world, LocationName.donut_secret_2_region, LocationName.donut_secret_2_exit_1)
+ connect(world, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle)
- connect(world, player, names, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_exit_1,
+ connect(world, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_exit_1,
lambda state: (state.has(ItemName.mario_run, player) and
(state.has(ItemName.super_star_active, player) or
state.has(ItemName.progressive_powerup, player, 1))))
- connect(world, player, names, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_exit_2,
+ connect(world, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_exit_2,
lambda state: (state.has(ItemName.mario_carry, player) and
((state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_climb, player)) or
(state.has(ItemName.yoshi_activate, player) and state.has(ItemName.red_switch_palace, player)) or
(state.has(ItemName.red_switch_palace, player) and state.has(ItemName.mario_climb, player)))))
- connect(world, player, names, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_exit_1,
+ connect(world, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_exit_1,
lambda state: (state.has(ItemName.mario_swim, player) and
(state.has(ItemName.mario_climb, player) or state.has(ItemName.yoshi_activate, player))))
- connect(world, player, names, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_exit_2,
+ connect(world, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_exit_2,
lambda state: (state.has(ItemName.mario_swim, player) and
state.has(ItemName.p_switch, player) and
state.has(ItemName.mario_carry, player) and
(state.has(ItemName.mario_climb, player) or state.has(ItemName.yoshi_activate, player))))
- connect(world, player, names, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_exit_1,
+ connect(world, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_exit_1,
lambda state: state.has(ItemName.mario_climb, player))
- connect(world, player, names, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_exit_2,
+ connect(world, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_exit_2,
lambda state: (state.has(ItemName.mario_climb, player) and
(state.has(ItemName.mario_carry, player) and state.has(ItemName.blue_switch_palace, player))))
- connect(world, player, names, LocationName.vanilla_ghost_house_region, LocationName.vanilla_ghost_house_exit_1,
+ connect(world, LocationName.vanilla_ghost_house_region, LocationName.vanilla_ghost_house_exit_1,
lambda state: state.has(ItemName.p_switch, player))
- connect(world, player, names, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_exit_1)
- connect(world, player, names, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_exit_1)
- connect(world, player, names, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_exit_1)
- connect(world, player, names, LocationName.vanilla_secret_3_region, LocationName.vanilla_secret_3_exit_1,
+ connect(world, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_exit_1)
+ connect(world, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_exit_1)
+ connect(world, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_exit_1)
+ connect(world, LocationName.vanilla_secret_3_region, LocationName.vanilla_secret_3_exit_1,
lambda state: state.has(ItemName.mario_swim, player))
- connect(world, player, names, LocationName.vanilla_fortress_region, LocationName.vanilla_fortress,
+ connect(world, LocationName.vanilla_fortress_region, LocationName.vanilla_fortress,
lambda state: state.has(ItemName.mario_swim, player))
- connect(world, player, names, LocationName.vanilla_dome_castle_region, LocationName.vanilla_dome_castle)
+ connect(world, LocationName.vanilla_dome_castle_region, LocationName.vanilla_dome_castle)
- connect(world, player, names, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_exit_1)
- connect(world, player, names, LocationName.butter_bridge_2_region, LocationName.butter_bridge_2_exit_1)
- connect(world, player, names, LocationName.cheese_bridge_region, LocationName.cheese_bridge_exit_1,
+ connect(world, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_exit_1)
+ connect(world, LocationName.butter_bridge_2_region, LocationName.butter_bridge_2_exit_1)
+ connect(world, LocationName.cheese_bridge_region, LocationName.cheese_bridge_exit_1,
lambda state: state.has(ItemName.mario_climb, player))
- connect(world, player, names, LocationName.cheese_bridge_region, LocationName.cheese_bridge_exit_2,
- lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
- connect(world, player, names, LocationName.soda_lake_region, LocationName.soda_lake_exit_1,
+ connect(world, LocationName.cheese_bridge_region, LocationName.cheese_bridge_exit_2,
+ lambda state: (state.has(ItemName.mario_run, player) and
+ (state.has(ItemName.progressive_powerup, player, 3) or
+ state.has(ItemName.yoshi_activate, player))))
+ connect(world, LocationName.soda_lake_region, LocationName.soda_lake_exit_1,
lambda state: state.has(ItemName.mario_swim, player))
- connect(world, player, names, LocationName.cookie_mountain_region, LocationName.cookie_mountain_exit_1)
- connect(world, player, names, LocationName.twin_bridges_castle_region, LocationName.twin_bridges_castle,
+ connect(world, LocationName.cookie_mountain_region, LocationName.cookie_mountain_exit_1)
+ connect(world, LocationName.twin_bridges_castle_region, LocationName.twin_bridges_castle,
lambda state: (state.has(ItemName.mario_run, player) and
state.has(ItemName.mario_climb, player)))
- connect(world, player, names, LocationName.forest_of_illusion_1_region, LocationName.forest_of_illusion_1_exit_1)
- connect(world, player, names, LocationName.forest_of_illusion_1_region, LocationName.forest_of_illusion_1_exit_2,
+ connect(world, LocationName.forest_of_illusion_1_region, LocationName.forest_of_illusion_1_exit_1)
+ connect(world, LocationName.forest_of_illusion_1_region, LocationName.forest_of_illusion_1_exit_2,
lambda state: (state.has(ItemName.mario_carry, player) and
state.has(ItemName.p_balloon, player)))
- connect(world, player, names, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_exit_1,
+ connect(world, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_exit_1,
lambda state: state.has(ItemName.mario_swim, player))
- connect(world, player, names, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_exit_2,
+ connect(world, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_exit_2,
lambda state: (state.has(ItemName.mario_swim, player) and
state.has(ItemName.mario_carry, player)))
- connect(world, player, names, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_exit_1,
+ connect(world, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_exit_1,
lambda state: (state.has(ItemName.mario_carry, player) or
state.has(ItemName.yoshi_activate, player)))
- connect(world, player, names, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_exit_2,
+ connect(world, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_exit_2,
lambda state: (state.has(ItemName.mario_spin_jump, player) and
state.has(ItemName.mario_carry, player) and
state.has(ItemName.progressive_powerup, player, 1)))
- connect(world, player, names, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_exit_1)
- connect(world, player, names, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_exit_2,
+ connect(world, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_exit_1)
+ connect(world, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_exit_2,
lambda state: state.has(ItemName.mario_carry, player))
- connect(world, player, names, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_exit_1,
+ connect(world, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_exit_1,
lambda state: state.has(ItemName.p_switch, player))
- connect(world, player, names, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_exit_2,
+ connect(world, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_exit_2,
lambda state: state.has(ItemName.p_switch, player))
- connect(world, player, names, LocationName.forest_secret_region, LocationName.forest_secret_exit_1)
- connect(world, player, names, LocationName.forest_fortress_region, LocationName.forest_fortress)
- connect(world, player, names, LocationName.forest_castle_region, LocationName.forest_castle)
+ connect(world, LocationName.forest_secret_region, LocationName.forest_secret_exit_1)
+ connect(world, LocationName.forest_fortress_region, LocationName.forest_fortress)
+ connect(world, LocationName.forest_castle_region, LocationName.forest_castle)
- connect(world, player, names, LocationName.chocolate_island_1_region, LocationName.chocolate_island_1_exit_1,
+ connect(world, LocationName.chocolate_island_1_region, LocationName.chocolate_island_1_exit_1,
lambda state: state.has(ItemName.p_switch, player))
- connect(world, player, names, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_exit_1)
- connect(world, player, names, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_exit_2,
+ connect(world, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_exit_1)
+ connect(world, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_exit_2,
lambda state: state.has(ItemName.mario_carry, player))
- connect(world, player, names, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_exit_1,
+ connect(world, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_exit_1,
lambda state: (state.has(ItemName.mario_climb, player) or
(state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))))
- connect(world, player, names, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_exit_2,
+ connect(world, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_exit_2,
lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))
- connect(world, player, names, LocationName.chocolate_island_4_region, LocationName.chocolate_island_4_exit_1)
- connect(world, player, names, LocationName.chocolate_island_5_region, LocationName.chocolate_island_5_exit_1)
- connect(world, player, names, LocationName.chocolate_ghost_house_region, LocationName.chocolate_ghost_house_exit_1)
- connect(world, player, names, LocationName.chocolate_fortress_region, LocationName.chocolate_fortress)
- connect(world, player, names, LocationName.chocolate_secret_region, LocationName.chocolate_secret_exit_1,
+ connect(world, LocationName.chocolate_island_4_region, LocationName.chocolate_island_4_exit_1)
+ connect(world, LocationName.chocolate_island_5_region, LocationName.chocolate_island_5_exit_1)
+ connect(world, LocationName.chocolate_ghost_house_region, LocationName.chocolate_ghost_house_exit_1)
+ connect(world, LocationName.chocolate_fortress_region, LocationName.chocolate_fortress)
+ connect(world, LocationName.chocolate_secret_region, LocationName.chocolate_secret_exit_1,
lambda state: state.has(ItemName.mario_run, player))
- connect(world, player, names, LocationName.chocolate_castle_region, LocationName.chocolate_castle,
+ connect(world, LocationName.chocolate_castle_region, LocationName.chocolate_castle,
lambda state: (state.has(ItemName.progressive_powerup, player, 1)))
- connect(world, player, names, LocationName.sunken_ghost_ship_region, LocationName.sunken_ghost_ship,
+ connect(world, LocationName.sunken_ghost_ship_region, LocationName.sunken_ghost_ship,
lambda state: state.has(ItemName.mario_swim, player))
- connect(world, player, names, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_exit_1)
- connect(world, player, names, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_exit_1)
- connect(world, player, names, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_exit_2,
+ connect(world, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_exit_1)
+ connect(world, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_exit_1)
+ connect(world, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_exit_2,
lambda state: state.has(ItemName.mario_carry, player))
- connect(world, player, names, LocationName.valley_of_bowser_3_region, LocationName.valley_of_bowser_3_exit_1)
- connect(world, player, names, LocationName.valley_of_bowser_4_region, LocationName.valley_of_bowser_4_exit_1,
+ connect(world, LocationName.valley_of_bowser_3_region, LocationName.valley_of_bowser_3_exit_1)
+ connect(world, LocationName.valley_of_bowser_4_region, LocationName.valley_of_bowser_4_exit_1,
lambda state: state.has(ItemName.mario_climb, player))
- connect(world, player, names, LocationName.valley_of_bowser_4_region, LocationName.valley_of_bowser_4_exit_2,
+ connect(world, LocationName.valley_of_bowser_4_region, LocationName.valley_of_bowser_4_exit_2,
lambda state: (state.has(ItemName.mario_climb, player) and
state.has(ItemName.mario_carry, player) and
state.has(ItemName.yoshi_activate, player)))
- connect(world, player, names, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_exit_1,
+ connect(world, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_exit_1,
lambda state: state.has(ItemName.p_switch, player))
- connect(world, player, names, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_exit_2,
+ connect(world, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_exit_2,
lambda state: (state.has(ItemName.p_switch, player) and
state.has(ItemName.mario_carry, player) and
state.has(ItemName.mario_run, player)))
- connect(world, player, names, LocationName.valley_fortress_region, LocationName.valley_fortress,
+ connect(world, LocationName.valley_fortress_region, LocationName.valley_fortress,
lambda state: state.has(ItemName.progressive_powerup, player, 1))
- connect(world, player, names, LocationName.valley_castle_region, LocationName.valley_castle)
- connect(world, player, names, LocationName.front_door, LocationName.bowser_region,
+ connect(world, LocationName.valley_castle_region, LocationName.valley_castle)
+ connect(world, LocationName.front_door, LocationName.bowser_region,
lambda state: (state.has(ItemName.mario_climb, player) and
state.has(ItemName.mario_run, player) and
state.has(ItemName.mario_swim, player) and
state.has(ItemName.progressive_powerup, player, 1) and
- state.has(ItemName.koopaling, player, world.bosses_required[player].value)))
- connect(world, player, names, LocationName.back_door, LocationName.bowser_region,
- lambda state: state.has(ItemName.koopaling, player, world.bosses_required[player].value))
+ state.has(ItemName.koopaling, player, world.options.bosses_required.value)))
+ connect(world, LocationName.back_door, LocationName.bowser_region,
+ lambda state: state.has(ItemName.koopaling, player, world.options.bosses_required.value))
- connect(world, player, names, LocationName.star_road_1_region, LocationName.star_road_1_exit_1,
+ connect(world, LocationName.star_road_1_region, LocationName.star_road_1_exit_1,
lambda state: (state.has(ItemName.mario_spin_jump, player) and
state.has(ItemName.progressive_powerup, player, 1)))
- connect(world, player, names, LocationName.star_road_1_region, LocationName.star_road_1_exit_2,
+ connect(world, LocationName.star_road_1_region, LocationName.star_road_1_exit_2,
lambda state: (state.has(ItemName.mario_spin_jump, player) and
state.has(ItemName.mario_carry, player) and
state.has(ItemName.progressive_powerup, player, 1)))
- connect(world, player, names, LocationName.star_road_2_region, LocationName.star_road_2_exit_1,
+ connect(world, LocationName.star_road_2_region, LocationName.star_road_2_exit_1,
lambda state: state.has(ItemName.mario_swim, player))
- connect(world, player, names, LocationName.star_road_2_region, LocationName.star_road_2_exit_2,
+ connect(world, LocationName.star_road_2_region, LocationName.star_road_2_exit_2,
lambda state: (state.has(ItemName.mario_swim, player) and
state.has(ItemName.mario_carry, player)))
- connect(world, player, names, LocationName.star_road_3_region, LocationName.star_road_3_exit_1)
- connect(world, player, names, LocationName.star_road_3_region, LocationName.star_road_3_exit_2,
+ connect(world, LocationName.star_road_3_region, LocationName.star_road_3_exit_1)
+ connect(world, LocationName.star_road_3_region, LocationName.star_road_3_exit_2,
lambda state: state.has(ItemName.mario_carry, player))
- connect(world, player, names, LocationName.star_road_4_region, LocationName.star_road_4_exit_1)
- connect(world, player, names, LocationName.star_road_4_region, LocationName.star_road_4_exit_2,
+ connect(world, LocationName.star_road_4_region, LocationName.star_road_4_exit_1)
+ connect(world, LocationName.star_road_4_region, LocationName.star_road_4_exit_2,
lambda state: (state.has(ItemName.mario_carry, player) and
(state.has(ItemName.yoshi_activate, player) or
(state.has(ItemName.green_switch_palace, player) and state.has(ItemName.red_switch_palace, player)))))
- connect(world, player, names, LocationName.star_road_5_region, LocationName.star_road_5_exit_1,
+ connect(world, LocationName.star_road_5_region, LocationName.star_road_5_exit_1,
lambda state: state.has(ItemName.p_switch, player))
- connect(world, player, names, LocationName.star_road_5_region, LocationName.star_road_5_exit_2,
+ connect(world, LocationName.star_road_5_region, LocationName.star_road_5_exit_2,
lambda state: (state.has(ItemName.mario_carry, player) and
state.has(ItemName.mario_climb, player) and
state.has(ItemName.p_switch, player) and
@@ -1050,26 +2057,29 @@ def connect_regions(world, player, level_to_tile_dict):
state.has(ItemName.red_switch_palace, player) and
state.has(ItemName.blue_switch_palace, player)))
- connect(world, player, names, LocationName.special_zone_1_region, LocationName.special_zone_1_exit_1,
+ connect(world, LocationName.special_zone_1_region, LocationName.special_zone_1_exit_1,
lambda state: (state.has(ItemName.mario_climb, player) and
(state.has(ItemName.p_switch, player) or
(state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))))
- connect(world, player, names, LocationName.special_zone_2_region, LocationName.special_zone_2_exit_1,
+ connect(world, LocationName.special_zone_2_region, LocationName.special_zone_2_exit_1,
lambda state: state.has(ItemName.p_balloon, player))
- connect(world, player, names, LocationName.special_zone_3_region, LocationName.special_zone_3_exit_1,
+ connect(world, LocationName.special_zone_3_region, LocationName.special_zone_3_exit_1,
lambda state: (state.has(ItemName.mario_climb, player) or
- state.has(ItemName.p_switch, player) or
- (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))))
- connect(world, player, names, LocationName.special_zone_4_region, LocationName.special_zone_4_exit_1,
- lambda state: state.has(ItemName.progressive_powerup, player, 1))
- connect(world, player, names, LocationName.special_zone_5_region, LocationName.special_zone_5_exit_1,
+ state.has(ItemName.yoshi_activate, player)))
+ connect(world, LocationName.special_zone_4_region, LocationName.special_zone_4_exit_1,
+ lambda state: (state.has(ItemName.progressive_powerup, player, 2) or
+ state.has(ItemName.super_star_active, player)))
+ connect(world, LocationName.special_zone_5_region, LocationName.special_zone_5_exit_1,
lambda state: state.has(ItemName.progressive_powerup, player, 1))
- connect(world, player, names, LocationName.special_zone_6_region, LocationName.special_zone_6_exit_1,
+ connect(world, LocationName.special_zone_6_region, LocationName.special_zone_6_exit_1,
lambda state: state.has(ItemName.mario_swim, player))
- connect(world, player, names, LocationName.special_zone_7_region, LocationName.special_zone_7_exit_1,
- lambda state: state.has(ItemName.progressive_powerup, player, 1))
- connect(world, player, names, LocationName.special_zone_8_region, LocationName.special_zone_8_exit_1,
+ connect(world, LocationName.special_zone_7_region, LocationName.special_zone_7_exit_1,
lambda state: state.has(ItemName.progressive_powerup, player, 1))
+ connect(world, LocationName.special_zone_8_region, LocationName.special_zone_8_exit_1,
+ lambda state: ((state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player)) or
+ state.has(ItemName.progressive_powerup, player, 3) or
+ state.has(ItemName.yoshi_activate, player) or
+ state.has(ItemName.mario_carry, player)))
@@ -1085,52 +2095,52 @@ def connect_regions(world, player, level_to_tile_dict):
current_tile_name = current_tile_data.levelName
if ("Star Road - " not in current_tile_name) and (" - Star Road" not in current_tile_name):
current_tile_name += " - Tile"
- connect(world, player, names, current_tile_name, current_level_data.levelName)
+ connect(world, current_tile_name, current_level_data.levelName)
# Connect Exit regions to next tile regions
if current_tile_data.exit1Path:
next_tile_id = current_tile_data.exit1Path.otherLevelID
- if world.swap_donut_gh_exits[player] and current_tile_id == 0x04:
+ if world.options.swap_donut_gh_exits and current_tile_id == 0x04:
next_tile_id = current_tile_data.exit2Path.otherLevelID
next_tile_name = level_info_dict[next_tile_id].levelName
if ("Star Road - " not in next_tile_name) and (" - Star Road" not in next_tile_name):
next_tile_name += " - Tile"
current_exit_name = (current_level_data.levelName + " - Normal Exit")
- connect(world, player, names, current_exit_name, next_tile_name)
+ connect(world, current_exit_name, next_tile_name)
if current_tile_data.exit2Path:
next_tile_id = current_tile_data.exit2Path.otherLevelID
- if world.swap_donut_gh_exits[player] and current_tile_id == 0x04:
+ if world.options.swap_donut_gh_exits and current_tile_id == 0x04:
next_tile_id = current_tile_data.exit1Path.otherLevelID
next_tile_name = level_info_dict[next_tile_id].levelName
if ("Star Road - " not in next_tile_name) and (" - Star Road" not in next_tile_name):
next_tile_name += " - Tile"
current_exit_name = (current_level_data.levelName + " - Secret Exit")
- connect(world, player, names, current_exit_name, next_tile_name)
-
- connect(world, player, names, LocationName.donut_plains_star_road, LocationName.star_road_donut)
- connect(world, player, names, LocationName.star_road_donut, LocationName.donut_plains_star_road)
- connect(world, player, names, LocationName.star_road_donut, LocationName.star_road_1_tile)
- connect(world, player, names, LocationName.vanilla_dome_star_road, LocationName.star_road_vanilla)
- connect(world, player, names, LocationName.star_road_vanilla, LocationName.vanilla_dome_star_road)
- connect(world, player, names, LocationName.star_road_vanilla, LocationName.star_road_2_tile)
- connect(world, player, names, LocationName.twin_bridges_star_road, LocationName.star_road_twin_bridges)
- connect(world, player, names, LocationName.star_road_twin_bridges, LocationName.twin_bridges_star_road)
- connect(world, player, names, LocationName.star_road_twin_bridges, LocationName.star_road_3_tile)
- connect(world, player, names, LocationName.forest_star_road, LocationName.star_road_forest)
- connect(world, player, names, LocationName.star_road_forest, LocationName.forest_star_road)
- connect(world, player, names, LocationName.star_road_forest, LocationName.star_road_4_tile)
- connect(world, player, names, LocationName.valley_star_road, LocationName.star_road_valley)
- connect(world, player, names, LocationName.star_road_valley, LocationName.valley_star_road)
- connect(world, player, names, LocationName.star_road_valley, LocationName.star_road_5_tile)
- connect(world, player, names, LocationName.star_road_special, LocationName.special_star_road)
- connect(world, player, names, LocationName.special_star_road, LocationName.star_road_special)
- connect(world, player, names, LocationName.special_star_road, LocationName.special_zone_1_tile)
+ connect(world, current_exit_name, next_tile_name)
+
+ connect(world, LocationName.donut_plains_star_road, LocationName.star_road_donut)
+ connect(world, LocationName.star_road_donut, LocationName.donut_plains_star_road)
+ connect(world, LocationName.star_road_donut, LocationName.star_road_1_tile)
+ connect(world, LocationName.vanilla_dome_star_road, LocationName.star_road_vanilla)
+ connect(world, LocationName.star_road_vanilla, LocationName.vanilla_dome_star_road)
+ connect(world, LocationName.star_road_vanilla, LocationName.star_road_2_tile)
+ connect(world, LocationName.twin_bridges_star_road, LocationName.star_road_twin_bridges)
+ connect(world, LocationName.star_road_twin_bridges, LocationName.twin_bridges_star_road)
+ connect(world, LocationName.star_road_twin_bridges, LocationName.star_road_3_tile)
+ connect(world, LocationName.forest_star_road, LocationName.star_road_forest)
+ connect(world, LocationName.star_road_forest, LocationName.forest_star_road)
+ connect(world, LocationName.star_road_forest, LocationName.star_road_4_tile)
+ connect(world, LocationName.valley_star_road, LocationName.star_road_valley)
+ connect(world, LocationName.star_road_valley, LocationName.valley_star_road)
+ connect(world, LocationName.star_road_valley, LocationName.star_road_5_tile)
+ connect(world, LocationName.star_road_special, LocationName.special_star_road)
+ connect(world, LocationName.special_star_road, LocationName.star_road_special)
+ connect(world, LocationName.special_star_road, LocationName.special_zone_1_tile)
- connect(world, player, names, LocationName.star_road_valley, LocationName.front_door_tile)
+ connect(world, LocationName.star_road_valley, LocationName.front_door_tile)
-def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None):
- ret = Region(name, player, world)
+def create_region(multiworld: MultiWorld, player: int, active_locations, name: str, locations=None):
+ ret = Region(name, player, multiworld)
if locations:
for locationName in locations:
loc_id = active_locations.get(locationName, 0)
@@ -1140,9 +2150,9 @@ def create_region(world: MultiWorld, player: int, active_locations, name: str, l
return ret
-def add_location_to_region(world: MultiWorld, player: int, active_locations, region_name: str, location_name: str,
+def add_location_to_region(multiworld: MultiWorld, player: int, active_locations, region_name: str, location_name: str,
rule: typing.Optional[typing.Callable] = None):
- region = world.get_region(region_name, player)
+ region = multiworld.get_region(region_name, player)
loc_id = active_locations.get(location_name, 0)
if loc_id:
location = SMWLocation(player, location_name, loc_id, region)
@@ -1151,23 +2161,8 @@ def add_location_to_region(world: MultiWorld, player: int, active_locations, reg
add_rule(location, rule)
-
-def connect(world: MultiWorld, player: int, used_names: typing.Dict[str, int], source: str, target: str,
+def connect(world: World, source: str, target: str,
rule: typing.Optional[typing.Callable] = None):
- source_region = world.get_region(source, player)
- target_region = world.get_region(target, player)
-
- if target not in used_names:
- used_names[target] = 1
- name = target
- else:
- used_names[target] += 1
- name = target + (' ' * used_names[target])
-
- connection = Entrance(player, name, source_region)
-
- if rule:
- connection.access_rule = rule
-
- source_region.exits.append(connection)
- connection.connect(target_region)
+ source_region: Region = world.get_region(source)
+ target_region: Region = world.get_region(target)
+ source_region.connect(target_region, rule=rule)
diff --git a/worlds/smw/Rom.py b/worlds/smw/Rom.py
index 0f5ec7e4f066..ff3b5c31634d 100644
--- a/worlds/smw/Rom.py
+++ b/worlds/smw/Rom.py
@@ -1,6 +1,7 @@
import Utils
+from worlds.AutoWorld import World
from worlds.Files import APDeltaPatch
-from .Aesthetics import generate_shuffled_header_data, generate_shuffled_ow_palettes
+from .Aesthetics import generate_shuffled_header_data, generate_shuffled_ow_palettes, generate_curated_level_palette_data, generate_curated_map_palette_data, generate_shuffled_sfx
from .Levels import level_info_dict, full_bowser_rooms, standard_bowser_rooms, submap_boss_rooms, ow_boss_rooms
from .Names.TextBox import generate_goal_text, title_text_mapping, generate_text_box
@@ -10,38 +11,48 @@
import hashlib
import os
import math
+import pkgutil
ability_rom_data = {
- 0xBC0003: [[0x1F2C, 0x7]], # Run 0x80
- 0xBC0004: [[0x1F2C, 0x6]], # Carry 0x40
- 0xBC0005: [[0x1F2C, 0x2]], # Swim 0x04
- 0xBC0006: [[0x1F2C, 0x3]], # Spin Jump 0x08
- 0xBC0007: [[0x1F2C, 0x5]], # Climb 0x20
- 0xBC0008: [[0x1F2C, 0x1]], # Yoshi 0x02
- 0xBC0009: [[0x1F2C, 0x4]], # P-Switch 0x10
+ 0xBC0003: [[0x1F1C, 0x7]], # Run 0x80
+ 0xBC0004: [[0x1F1C, 0x6]], # Carry 0x40
+ 0xBC0005: [[0x1F1C, 0x2]], # Swim 0x04
+ 0xBC0006: [[0x1F1C, 0x3]], # Spin Jump 0x08
+ 0xBC0007: [[0x1F1C, 0x5]], # Climb 0x20
+ 0xBC0008: [[0x1F1C, 0x1]], # Yoshi 0x02
+ 0xBC0009: [[0x1F1C, 0x4]], # P-Switch 0x10
#0xBC000A: [[]]
0xBC000B: [[0x1F2D, 0x3]], # P-Balloon 0x08
- 0xBC000D: [[0x1F2D, 0x4]], # Super Star 0x10
+ 0xBC000D: [[0x1F2D, 0x4]] # Super Star 0x10
}
+icon_rom_data = {
+ 0xBC0002: [0x1B00C], # Yoshi Egg
+ 0xBC0012: [0x1B00E], # Boss Token
-item_rom_data = {
- 0xBC0001: [0x18E4, 0x1], # 1-Up Mushroom
-
- 0xBC0002: [0x1F24, 0x1, 0x1F], # Yoshi Egg
- 0xBC0012: [0x1F26, 0x1, 0x09], # Boss Token
+ 0xBC0017: [0x1B004], # 1 coin
+ 0xBC0018: [0x1B006], # 5 coins
+ 0xBC0019: [0x1B008], # 10 coins
+ 0xBC001A: [0x1B00A], # 50 coins
- 0xBC000E: [0x1F28, 0x1, 0x1C], # Yellow Switch Palace
- 0xBC000F: [0x1F27, 0x1, 0x1C], # Green Switch Palace
- 0xBC0010: [0x1F2A, 0x1, 0x1C], # Red Switch Palace
- 0xBC0011: [0x1F29, 0x1, 0x1C], # Blue Switch Palace
+ 0xBC0001: [0x1B010] # 1-Up Mushroom
+}
+
+item_rom_data = {
+ 0xBC000E: [0x1F28, 0x1, 0x1C], # Yellow Switch Palace
+ 0xBC000F: [0x1F27, 0x1, 0x1C], # Green Switch Palace
+ 0xBC0010: [0x1F2A, 0x1, 0x1C], # Red Switch Palace
+ 0xBC0011: [0x1F29, 0x1, 0x1C], # Blue Switch Palace
+ 0xBC001B: [0x1F1E, 0x80, 0x39] # Special Zone Clear
}
trap_rom_data = {
- 0xBC0013: [0x0086, 0x1, 0x0E], # Ice Trap
+ 0xBC0013: [0x0086, 0x1, 0x0E], # Ice Trap
0xBC0014: [0x18BD, 0x7F, 0x18], # Stun Trap
- 0xBC0016: [0x0F31, 0x1], # Timer Trap
+ 0xBC0016: [0x0F31, 0x1], # Timer Trap
+ 0xBC001C: [0x18B4, 0x1, 0x44], # Reverse controls trap
+ 0xBC001D: [0x18B7, 0x1], # Thwimp Trap
}
@@ -72,7 +83,7 @@ def read_bit(self, address: int, bit_number: int) -> bool:
def read_byte(self, address: int) -> int:
return self.buffer[address]
- def read_bytes(self, startaddress: int, length: int) -> bytes:
+ def read_bytes(self, startaddress: int, length: int) -> bytearray:
return self.buffer[startaddress:startaddress + length]
def write_byte(self, address: int, value: int):
@@ -109,7 +120,7 @@ def handle_ability_code(rom):
rom.write_bytes(RUN_SUB_ADDR + 0x04, bytearray([0xC8])) # INY
rom.write_bytes(RUN_SUB_ADDR + 0x05, bytearray([0xA9, 0x70])) # LDA #70
rom.write_bytes(RUN_SUB_ADDR + 0x07, bytearray([0xAA])) # TAX
- rom.write_bytes(RUN_SUB_ADDR + 0x08, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(RUN_SUB_ADDR + 0x08, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(RUN_SUB_ADDR + 0x0B, bytearray([0x89, 0x80])) # BIT #80
rom.write_bytes(RUN_SUB_ADDR + 0x0D, bytearray([0xF0, 0x04])) # BEQ +0x04
rom.write_bytes(RUN_SUB_ADDR + 0x0F, bytearray([0x8A])) # TXA
@@ -126,7 +137,7 @@ def handle_ability_code(rom):
PURPLE_BLOCK_CARRY_SUB_ADDR = 0x01BA28
rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x00, bytearray([0x08])) # PHP
- rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x01, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x04, bytearray([0x89, 0x40])) # BIT #40
rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x06, bytearray([0xF0, 0x09])) # BEQ +0x09
rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x08, bytearray([0x28])) # PLP
@@ -145,7 +156,7 @@ def handle_ability_code(rom):
SPRINGBOARD_CARRY_SUB_ADDR = 0x01BA40
rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x00, bytearray([0x48])) # PHA
rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x01, bytearray([0x08])) # PHP
- rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x02, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x02, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x05, bytearray([0x89, 0x40])) # BIT #40
rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x07, bytearray([0xF0, 0x08])) # BEQ +0x08
rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x09, bytearray([0xA9, 0x0B])) # LDA #0B
@@ -157,7 +168,7 @@ def handle_ability_code(rom):
# End Springboard Carry
# Shell Carry
- rom.write_bytes(0xAA66, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(0xAA66, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(0xAA69, bytearray([0x89, 0x40])) # BIT #40
rom.write_bytes(0xAA6B, bytearray([0xF0, 0x07])) # BEQ +0x07
rom.write_bytes(0xAA6D, bytearray([0x22, 0x60, 0xBA, 0x03])) # JSL $03BA60
@@ -180,7 +191,7 @@ def handle_ability_code(rom):
YOSHI_CARRY_SUB_ADDR = 0x01BA70
rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x00, bytearray([0x08])) # PHP
- rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x01, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x04, bytearray([0x89, 0x40])) # BIT #40
rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x06, bytearray([0xF0, 0x0A])) # BEQ +0x0A
rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x08, bytearray([0xA9, 0x12])) # LDA #12
@@ -197,7 +208,7 @@ def handle_ability_code(rom):
CLIMB_SUB_ADDR = 0x01BA88
rom.write_bytes(CLIMB_SUB_ADDR + 0x00, bytearray([0x08])) # PHP
- rom.write_bytes(CLIMB_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(CLIMB_SUB_ADDR + 0x01, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(CLIMB_SUB_ADDR + 0x04, bytearray([0x89, 0x20])) # BIT #20
rom.write_bytes(CLIMB_SUB_ADDR + 0x06, bytearray([0xF0, 0x09])) # BEQ +0x09
rom.write_bytes(CLIMB_SUB_ADDR + 0x08, bytearray([0xA5, 0x8B])) # LDA $8B
@@ -213,7 +224,7 @@ def handle_ability_code(rom):
CLIMB_ROPE_SUB_ADDR = 0x01BC70
rom.write_bytes(CLIMB_ROPE_SUB_ADDR + 0x00, bytearray([0x08])) # PHP
- rom.write_bytes(CLIMB_ROPE_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(CLIMB_ROPE_SUB_ADDR + 0x01, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(CLIMB_ROPE_SUB_ADDR + 0x04, bytearray([0x89, 0x20])) # BIT #20
rom.write_bytes(CLIMB_ROPE_SUB_ADDR + 0x06, bytearray([0xF0, 0x07])) # BEQ +0x07
rom.write_bytes(CLIMB_ROPE_SUB_ADDR + 0x08, bytearray([0x28])) # PLP
@@ -230,7 +241,7 @@ def handle_ability_code(rom):
P_SWITCH_SUB_ADDR = 0x01BAA0
rom.write_bytes(P_SWITCH_SUB_ADDR + 0x00, bytearray([0x08])) # PHP
- rom.write_bytes(P_SWITCH_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(P_SWITCH_SUB_ADDR + 0x01, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(P_SWITCH_SUB_ADDR + 0x04, bytearray([0x89, 0x10])) # BIT #10
rom.write_bytes(P_SWITCH_SUB_ADDR + 0x06, bytearray([0xF0, 0x04])) # BEQ +0x04
rom.write_bytes(P_SWITCH_SUB_ADDR + 0x08, bytearray([0xA9, 0xB0])) # LDA #B0
@@ -242,7 +253,7 @@ def handle_ability_code(rom):
# End P-Switch
# Spin Jump
- rom.write_bytes(0x5645, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(0x5645, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(0x5648, bytearray([0x89, 0x08])) # BIT #08
rom.write_bytes(0x564A, bytearray([0xF0, 0x12])) # BEQ +0x12
rom.write_bytes(0x564C, bytearray([0x22, 0xB8, 0xBA, 0x03])) # JSL $03BAB8
@@ -264,7 +275,7 @@ def handle_ability_code(rom):
SPIN_JUMP_WATER_SUB_ADDR = 0x01BBF8
rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x00, bytearray([0x08])) # PHP
- rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x01, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x04, bytearray([0x89, 0x08])) # BIT #08
rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x06, bytearray([0xF0, 0x09])) # BEQ +0x09
rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x08, bytearray([0x1A])) # INC
@@ -281,7 +292,7 @@ def handle_ability_code(rom):
SPIN_JUMP_SPRING_SUB_ADDR = 0x01BC0C
rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x00, bytearray([0x08])) # PHP
- rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x01, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x04, bytearray([0x89, 0x08])) # BIT #08
rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x06, bytearray([0xF0, 0x05])) # BEQ +0x05
rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x08, bytearray([0xA9, 0x01])) # LDA #01
@@ -297,7 +308,7 @@ def handle_ability_code(rom):
SWIM_SUB_ADDR = 0x01BAC8
rom.write_bytes(SWIM_SUB_ADDR + 0x00, bytearray([0x48])) # PHA
rom.write_bytes(SWIM_SUB_ADDR + 0x01, bytearray([0x08])) # PHP
- rom.write_bytes(SWIM_SUB_ADDR + 0x02, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(SWIM_SUB_ADDR + 0x02, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(SWIM_SUB_ADDR + 0x05, bytearray([0x89, 0x04])) # BIT #04
rom.write_bytes(SWIM_SUB_ADDR + 0x07, bytearray([0xF0, 0x0C])) # BEQ +0x0C
rom.write_bytes(SWIM_SUB_ADDR + 0x09, bytearray([0x28])) # PLP
@@ -321,7 +332,7 @@ def handle_ability_code(rom):
SWIM_SUB_ADDR = 0x01BAE8
rom.write_bytes(SWIM_SUB_ADDR + 0x00, bytearray([0x48])) # PHA
rom.write_bytes(SWIM_SUB_ADDR + 0x01, bytearray([0x08])) # PHP
- rom.write_bytes(SWIM_SUB_ADDR + 0x02, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(SWIM_SUB_ADDR + 0x02, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(SWIM_SUB_ADDR + 0x05, bytearray([0x89, 0x04])) # BIT #04
rom.write_bytes(SWIM_SUB_ADDR + 0x07, bytearray([0xF0, 0x0A])) # BEQ +0x0A
rom.write_bytes(SWIM_SUB_ADDR + 0x09, bytearray([0x28])) # PLP
@@ -344,7 +355,7 @@ def handle_ability_code(rom):
YOSHI_SUB_ADDR = 0x01BB08
rom.write_bytes(YOSHI_SUB_ADDR + 0x00, bytearray([0x08])) # PHP
- rom.write_bytes(YOSHI_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(YOSHI_SUB_ADDR + 0x01, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(YOSHI_SUB_ADDR + 0x04, bytearray([0x89, 0x02])) # BIT #02
rom.write_bytes(YOSHI_SUB_ADDR + 0x06, bytearray([0xF0, 0x06])) # BEQ +0x06
rom.write_bytes(YOSHI_SUB_ADDR + 0x08, bytearray([0x28])) # PLP
@@ -366,7 +377,7 @@ def handle_ability_code(rom):
YOSHI_SUB_ADDR = 0x01BB20
rom.write_bytes(YOSHI_SUB_ADDR + 0x00, bytearray([0x08])) # PHP
rom.write_bytes(YOSHI_SUB_ADDR + 0x01, bytearray([0x9C, 0x1E, 0x14])) # STZ $141E
- rom.write_bytes(YOSHI_SUB_ADDR + 0x04, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C
+ rom.write_bytes(YOSHI_SUB_ADDR + 0x04, bytearray([0xAD, 0x1C, 0x1F])) # LDA $1F1C
rom.write_bytes(YOSHI_SUB_ADDR + 0x07, bytearray([0x89, 0x02])) # BIT #02
rom.write_bytes(YOSHI_SUB_ADDR + 0x09, bytearray([0xF0, 0x05])) # BEQ +0x05
rom.write_bytes(YOSHI_SUB_ADDR + 0x0B, bytearray([0x28])) # PLP
@@ -576,18 +587,17 @@ def handle_yoshi_box(rom):
def handle_bowser_damage(rom):
- rom.write_bytes(0x1A509, bytearray([0x20, 0x50, 0xBC])) # JSR $03BC50
+ rom.write_bytes(0x1A509, bytearray([0x5C, 0x50, 0xBC, 0x03])) # JML $03BC50
BOWSER_BALLS_SUB_ADDR = 0x01BC50
- rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x00, bytearray([0x08])) # PHP
- rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x01, bytearray([0xAD, 0x48, 0x0F])) # LDA $F48
- rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x04, bytearray([0xCF, 0xA1, 0xBF, 0x03])) # CMP $03BFA1
- rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x08, bytearray([0x90, 0x06])) # BCC +0x06
- rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0A, bytearray([0x28])) # PLP
- rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0B, bytearray([0xEE, 0xB8, 0x14])) # INC $14B8
- rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0E, bytearray([0x80, 0x01])) # BRA +0x01
- rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x10, bytearray([0x28])) # PLP
- rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x11, bytearray([0x60])) # RTS
+ rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0000, bytearray([0xAF, 0xA0, 0xBF, 0x03])) # bowser_infinite_balls: lda.l goal_setting
+ rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0004, bytearray([0xD0, 0x0C])) # bne .nope
+ rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0006, bytearray([0xAD, 0x48, 0x0F])) # lda $0F48
+ rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0009, bytearray([0xCF, 0xA1, 0xBF, 0x03])) # cmp.l required_bosses_setting
+ rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x000D, bytearray([0x90, 0x03])) # bcc .nope
+ rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x000F, bytearray([0xEE, 0xB8, 0x14])) # inc $14B8
+ rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0012, bytearray([0xAD, 0xB8, 0x14])) # .nope lda $14B8
+ rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0015, bytearray([0x5C, 0x0F, 0xA5, 0x03])) # jml $03A50F
return
@@ -654,6 +664,7 @@ def handle_level_shuffle(rom, active_level_dict):
for level_id, tile_id in active_level_dict.items():
rom.write_byte(0x37F70 + level_id, tile_id)
+ rom.write_byte(0x37F00 + tile_id, level_id)
def handle_collected_paths(rom):
@@ -673,38 +684,2155 @@ def handle_collected_paths(rom):
def handle_vertical_scroll(rom):
- rom.write_bytes(0x285BA, bytearray([0x22, 0x90, 0xBC, 0x03])) # JSL $03BC90
-
- VERTICAL_SCROLL_SUB_ADDR = 0x01BC90
- rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x00, bytearray([0x4A])) # LSR
- rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x01, bytearray([0x4A])) # LSR
- rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x02, bytearray([0x4A])) # LSR
- rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x03, bytearray([0x4A])) # LSR
- rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x04, bytearray([0x08])) # PHP
- rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x05, bytearray([0xC9, 0x02])) # CMP #02
- rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x07, bytearray([0xD0, 0x02])) # BNE +0x02
- rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x09, bytearray([0xA9, 0x01])) # LDA #01
- rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x0B, bytearray([0x28])) # PLP
- rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x0C, bytearray([0x6B])) # RTL
-
-
-def handle_music_shuffle(rom, world, player):
+ rom.write_bytes(0x285BA, bytearray([0x22, 0x80, 0xF4, 0x0F])) # JSL $0FF480
+
+ VERTICAL_SCROLL_SUB_ADDR = 0x7F480
+ rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x0000, bytearray([0x4A])) # vertical_scroll: lsr
+ rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x0001, bytearray([0x4A])) # lsr
+ rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x0002, bytearray([0x4A])) # lsr
+ rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x0003, bytearray([0x4A])) # lsr
+ rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x0004, bytearray([0x08])) # php
+ rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x0005, bytearray([0xC9, 0x02])) # cmp #$02
+ rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x0007, bytearray([0xD0, 0x0B])) # bne +
+ rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x0009, bytearray([0xC2, 0x10])) # rep #$10
+ rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x000B, bytearray([0xDA])) # phx
+ rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x000C, bytearray([0xAE, 0x0B, 0x01])) # ldx $010B
+ rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x000F, bytearray([0xBF, 0x00, 0xF5, 0x0F])) # lda.l vertical_scroll_levels,x
+ rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x0013, bytearray([0xFA])) # plx
+ rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x0014, bytearray([0x28])) # + plp
+ rom.write_bytes(VERTICAL_SCROLL_SUB_ADDR + 0x0015, bytearray([0x6B])) # rtl
+
+ vertical_scroll_table = [
+ 0x02, 0x01, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x02, # Levels 000-00F
+ 0x01, 0x02, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, # Levels 010-01F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 020-02F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 030-03F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 040-04F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 050-05F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 060-06F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 070-07F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 080-08F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 090-09F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 0A0-0AF
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 0B0-0BF
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 0C0-0CF
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x02, # Levels 0D0-0DF
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, # Levels 0E0-0EF
+ 0x02, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0x02, 0x01, 0x02, # Levels 0F0-0FF
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x01, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0x01, # Levels 100-10F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 110-11F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 120-12F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 130-13F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 140-14F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 150-15F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 160-16F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 170-17F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 180-18F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 190-19F
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 1A0-1AF
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 1B0-1BF
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 1C0-1CF
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 1D0-1DF
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, # Levels 1E0-1EF
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0x02] # Levels 1F0-1FF
+
+ rom.write_bytes(0x7F500, bytes(vertical_scroll_table))
+
+
+def handle_bonus_block(rom):
+ rom.write_bytes(0x71A5, bytearray([0x5C, 0x19, 0x8E, 0x05])) # JML $058E19
+
+ BONUS_BLOCK_ADDR = 0x28E19
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x00, bytearray([0xA9, 0x06])) # LDA #$06
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x02, bytearray([0xAC, 0xC0, 0x0D])) # LDY $0DC0
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x05, bytearray([0xD0, 0x1E])) # BNE IGNORE
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x07, bytearray([0xDA])) # PHX
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x08, bytearray([0xAD, 0xBF, 0x13])) # LDA $13BF
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x0B, bytearray([0x4A])) # LSR
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x0C, bytearray([0x4A])) # LSR
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x0D, bytearray([0x4A])) # LSR
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x0E, bytearray([0x48])) # PHA
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x0F, bytearray([0xAD, 0xBF, 0x13])) # LDA $13BF
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x12, bytearray([0x29, 0x07])) # AND #$07
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x14, bytearray([0xAA])) # TAX
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x15, bytearray([0xBF, 0x5B, 0xB3, 0x05])) # LDA $05B35B,x
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x19, bytearray([0xFA])) # PLX
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x1A, bytearray([0x1F, 0x00, 0xA0, 0x7F])) # ORA $7FA000,x
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x1E, bytearray([0x9F, 0x00, 0xA0, 0x7F])) # STA $7FA000,x
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x22, bytearray([0xFA])) # PLX
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x23, bytearray([0xA9, 0x05])) # LDA #$05
+ rom.write_bytes(BONUS_BLOCK_ADDR + 0x25, bytearray([0x5C, 0xD0, 0xF1, 0x00])) # IGNORE: JML $00F1D0
+
+
+def handle_blocksanity(rom):
+ import json
+ blocksanity_data = pkgutil.get_data(__name__, f"data/blocksanity.json").decode("utf-8")
+ blocksanity_data = json.loads(blocksanity_data)
+ blocksanity_coords = bytearray([])
+ blocksanity_bytes = bytearray([])
+
+ block_count = 0
+ entries = 0
+ for level_name, level_data in blocksanity_data.items():
+ # Calculate blocksanity pointer
+ if level_data == []:
+ # Skip if the level doesn't have any data
+ blocksanity_bytes += bytearray([0xFF, 0xFF])
+ continue
+ level_ptr = 0x80C0 + entries
+ blocksanity_bytes += bytearray([level_ptr & 0xFF, (level_ptr >> 8) & 0xFF])
+
+ # Get block data
+ block_coords = bytearray([])
+ for x in range(len(level_data)):
+ block_coords += bytearray([
+ int(level_data[x][1], 16) & 0xFF, (int(level_data[x][1], 16) >> 8) & 0xFF,
+ int(level_data[x][2], 16) & 0xFF, (int(level_data[x][2], 16) >> 8) & 0xFF,
+ block_count & 0xFF, (block_count >> 8) & 0xFF])
+ entries += 6
+ block_count += 1
+ block_coords += bytearray([0xFF, 0xFF])
+ entries += 2
+
+ blocksanity_coords += block_coords
+
+ blocksanity_bytes += blocksanity_coords
+
+ rom.write_bytes(0x80000, blocksanity_bytes)
+ rom.write_bytes(0x071D0, bytearray([0x5C, 0x00, 0xF7, 0x0F])) # org $00F1D0 : jml blocksanity_main
+ rom.write_bytes(0x0AD59, bytearray([0x5C, 0x15, 0xF7, 0x0F])) # org $01AD5C : jml blocksanity_flying_init
+ rom.write_bytes(0x0AE16, bytearray([0x22, 0x39, 0xF7, 0x0F])) # org $01AE16 : jsl blocksanity_flying_main
+
+ BLOCKSANITY_ADDR = 0x7F700
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0000, bytearray([0x85, 0x05])) # blocksanity_main: sta $05
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0002, bytearray([0x8B])) # phb
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0003, bytearray([0xA9, 0x10])) # lda.b #blocksanity_pointers>>16
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0005, bytearray([0x48])) # pha
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0006, bytearray([0xAB])) # plb
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0007, bytearray([0x5A])) # phy
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0008, bytearray([0x20, 0x63, 0xF7])) # jsr process_block
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x000B, bytearray([0x7A])) # ply
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x000C, bytearray([0xAB])) # plb
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x000D, bytearray([0xA5, 0x05])) # lda $05
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x000F, bytearray([0xC9, 0x05])) # cmp #$05
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0011, bytearray([0x5C, 0xD4, 0xF1, 0x00])) # jml $00F1D4
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0015, bytearray([0xB5, 0xD8])) # blocksanity_flying_init: lda $D8,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0017, bytearray([0x29, 0xF0])) # and #$F0
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0019, bytearray([0x9F, 0x20, 0xB8, 0x7F])) # sta !sprite_blocksanity_y_lo,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x001D, bytearray([0xBD, 0xD4, 0x14])) # lda $14D4,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0020, bytearray([0x9F, 0x30, 0xB8, 0x7F])) # sta !sprite_blocksanity_y_hi,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0024, bytearray([0xBD, 0xE0, 0x14])) # lda $14E0,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0027, bytearray([0x9F, 0x10, 0xB8, 0x7F])) # sta !sprite_blocksanity_x_hi,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x002B, bytearray([0xB5, 0xE4])) # lda $E4,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x002D, bytearray([0x29, 0xF0])) # and #$F0
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x002F, bytearray([0x9F, 0x00, 0xB8, 0x7F])) # sta !sprite_blocksanity_x_lo,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0033, bytearray([0x4A])) # lsr
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0034, bytearray([0x4A])) # lsr
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0035, bytearray([0x5C, 0x5D, 0xAD, 0x01])) # jml $01AD5D
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0039, bytearray([0xBF, 0x20, 0xB8, 0x7F])) # blocksanity_flying_main: lda !sprite_blocksanity_y_lo,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x003D, bytearray([0x85, 0x98])) # sta $98
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x003F, bytearray([0xBF, 0x30, 0xB8, 0x7F])) # lda !sprite_blocksanity_y_hi,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0043, bytearray([0x85, 0x99])) # sta $99
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0045, bytearray([0xBF, 0x00, 0xB8, 0x7F])) # lda !sprite_blocksanity_x_lo,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0049, bytearray([0x85, 0x9A])) # sta $9A
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x004B, bytearray([0xBF, 0x10, 0xB8, 0x7F])) # lda !sprite_blocksanity_x_hi,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x004F, bytearray([0x85, 0x9B])) # sta $9B
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0051, bytearray([0x8B])) # phb
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0052, bytearray([0xA9, 0x10])) # lda.b #blocksanity_pointers>>16
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0054, bytearray([0x48])) # pha
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0055, bytearray([0xAB])) # plb
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0056, bytearray([0x5A])) # phy
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0057, bytearray([0xDA])) # phx
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0058, bytearray([0x20, 0x63, 0xF7])) # jsr process_block
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x005B, bytearray([0xFA])) # plx
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x005C, bytearray([0x7A])) # ply
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x005D, bytearray([0xAB])) # plb
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x005E, bytearray([0xB5, 0xE4])) # lda $E4,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0060, bytearray([0x85, 0x9A])) # sta $9A
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0062, bytearray([0x6B])) # rtl
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0063, bytearray([0xA9, 0x0F])) # process_block: lda #$0F
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0065, bytearray([0x14, 0x98])) # trb $98
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0067, bytearray([0x14, 0x9A])) # trb $9A
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0069, bytearray([0xC2, 0x30])) # rep #$30
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x006B, bytearray([0xA5, 0x60])) # lda $60
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x006D, bytearray([0x29, 0xFF, 0x00])) # and #$00FF
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0070, bytearray([0x0A])) # asl
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0071, bytearray([0x18])) # clc
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0072, bytearray([0x69, 0x00, 0x80])) # adc.w #blocksanity_pointers
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0075, bytearray([0x48])) # pha
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0076, bytearray([0xA0, 0x00, 0x00])) # ldy #$0000
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0079, bytearray([0xB3, 0x01])) # lda ($01,s),y
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x007B, bytearray([0x48])) # pha
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x007C, bytearray([0xB3, 0x01])) # .loop lda ($01,s),y
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x007E, bytearray([0xC9, 0xFF, 0xFF])) # cmp #$FFFF
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0081, bytearray([0xF0, 0x16])) # beq .return
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0083, bytearray([0xC5, 0x9A])) # cmp $9A
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0085, bytearray([0xD0, 0x0A])) # bne .next_block_x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0087, bytearray([0xC8])) # iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0088, bytearray([0xC8])) # iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0089, bytearray([0xB3, 0x01])) # lda ($01,s),y
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x008B, bytearray([0xC5, 0x98])) # cmp $98
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x008D, bytearray([0xF0, 0x0F])) # beq .valid_block
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x008F, bytearray([0x80, 0x02])) # bra .next_block_y
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0091, bytearray([0xC8])) # .next_block_x iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0092, bytearray([0xC8])) # iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0093, bytearray([0xC8])) # .next_block_y iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0094, bytearray([0xC8])) # iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0095, bytearray([0xC8])) # iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0096, bytearray([0xC8])) # iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0097, bytearray([0x80, 0xE3])) # bra .loop
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x0099, bytearray([0x68])) # .return pla
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x009A, bytearray([0x68])) # pla
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x009B, bytearray([0xE2, 0x30])) # sep #$30
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x009D, bytearray([0x60])) # rts
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x009E, bytearray([0xC8])) # .valid_block iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x009F, bytearray([0xC8])) # iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00A0, bytearray([0xB3, 0x01])) # lda ($01,s),y
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00A2, bytearray([0xAA])) # tax
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00A3, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00A5, bytearray([0xDA])) # phx
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00A6, bytearray([0xBF, 0x00, 0xA4, 0x7F])) # lda !blocksanity_data_flags,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00AA, bytearray([0xD0, 0x08])) # bne .processed
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00AC, bytearray([0x1A])) # inc
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00AD, bytearray([0x9F, 0x00, 0xA4, 0x7F])) # sta !blocksanity_data_flags,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00B1, bytearray([0x20, 0xBA, 0xF7])) # jsr blocksanity_check_flags
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00B4, bytearray([0xFA])) # .processed plx
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00B5, bytearray([0xFA])) # plx
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00B6, bytearray([0xFA])) # plx
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00B7, bytearray([0xE2, 0x10])) # sep #$10
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00B9, bytearray([0x60])) # rts
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00BA, bytearray([0xC2, 0x20])) # blocksanity_check_flags: rep #$20
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00BC, bytearray([0xA0, 0x00, 0x00])) # ldy #$0000
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00BF, bytearray([0xB3, 0x05])) # .loop lda ($05,s),y
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00C1, bytearray([0xC9, 0xFF, 0xFF])) # cmp #$FFFF
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00C4, bytearray([0xF0, 0x14])) # beq .check
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00C6, bytearray([0xC8])) # iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00C7, bytearray([0xC8])) # iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00C8, bytearray([0xC8])) # iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00C9, bytearray([0xC8])) # iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00CA, bytearray([0xB3, 0x05])) # lda ($05,s),y
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00CC, bytearray([0xAA])) # tax
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00CD, bytearray([0xBF, 0x00, 0xA4, 0x7F])) # lda !blocksanity_data_flags,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00D1, bytearray([0x29, 0xFF, 0x00])) # and #$00FF
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00D4, bytearray([0xF0, 0x22])) # beq .invalid
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00D6, bytearray([0xC8])) # iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00D7, bytearray([0xC8])) # iny
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00D8, bytearray([0x80, 0xE5])) # bra .loop
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00DA, bytearray([0xE2, 0x20])) # .check sep #$20
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00DC, bytearray([0xA9, 0x00])) # lda #$00
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00DE, bytearray([0xEB])) # xba
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00DF, bytearray([0xA5, 0x60])) # lda $60
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00E1, bytearray([0x4A])) # lsr
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00E2, bytearray([0x4A])) # lsr
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00E3, bytearray([0x4A])) # lsr
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00E4, bytearray([0xA8])) # tay
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00E5, bytearray([0xA5, 0x60])) # lda $60
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00E7, bytearray([0x29, 0x07])) # and #$07
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00E9, bytearray([0xAA])) # tax
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00EA, bytearray([0xBF, 0x5B, 0xB3, 0x05])) # lda.l $05B35B,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00EE, bytearray([0xBB])) # tyx
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00EF, bytearray([0x1F, 0x10, 0xA0, 0x7F])) # ora !blocksanity_flags,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00F3, bytearray([0x9F, 0x10, 0xA0, 0x7F])) # sta !blocksanity_flags,x
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00F7, bytearray([0x60])) # rts
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00F8, bytearray([0xE2, 0x20])) # .invalid sep #$20
+ rom.write_bytes(BLOCKSANITY_ADDR + 0x00FA, bytearray([0x60])) # rts
+
+def handle_ram(rom):
+ rom.write_byte(0x07FD8, 0x02) # Expand SRAM
+ rom.write_bytes(0x01CF5, bytearray([0x5C, 0x00, 0xF2, 0x0F])) # org $009CF5 : jml init_sram
+ rom.write_bytes(0x01C0F, bytearray([0x5C, 0x00, 0xF3, 0x0F])) # org $009C0F : jml save_sram
+ rom.write_bytes(0x013BB, bytearray([0x5C, 0xA0, 0xF0, 0x0F])) # org $0093BB : jml init_ram
+
+ INIT_SRAM_ADDR = 0x7F200
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0000, bytearray([0xD0, 0x74])) # init_sram: bne .clear
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0002, bytearray([0x9C, 0x09, 0x01])) # stz $0109
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0005, bytearray([0xDA])) # phx
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0006, bytearray([0x08])) # php
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0007, bytearray([0xE2, 0x10])) # sep #$10
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0009, bytearray([0xA2, 0x5F])) # ldx.b #$5F
+ rom.write_bytes(INIT_SRAM_ADDR + 0x000B, bytearray([0xBF, 0x00, 0x08, 0x70])) # - lda !level_clears_sram,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x000F, bytearray([0x9F, 0x00, 0xA2, 0x7F])) # sta !level_clears,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0013, bytearray([0xCA])) # dex
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0014, bytearray([0x10, 0xF5])) # bpl -
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0016, bytearray([0xA2, 0x0B])) # ldx #$0B
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0018, bytearray([0xBF, 0x40, 0x09, 0x70])) # - lda !blocksanity_sram,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x001C, bytearray([0x9F, 0x10, 0xA0, 0x7F])) # sta !blocksanity_flags,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0020, bytearray([0xBF, 0x10, 0x09, 0x70])) # lda !moons_sram,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0024, bytearray([0x9D, 0xEE, 0x1F])) # sta !moons_flags,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0027, bytearray([0xBF, 0x00, 0x09, 0x70])) # lda !yoshi_coins_sram,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x002B, bytearray([0x9D, 0x2F, 0x1F])) # sta !yoshi_coins_flags,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x002E, bytearray([0xBF, 0x30, 0x09, 0x70])) # lda !bonus_block_sram,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0032, bytearray([0x9F, 0x00, 0xA0, 0x7F])) # sta !bonus_block_flags,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0036, bytearray([0xBF, 0x20, 0x09, 0x70])) # lda !checkpoints_sram,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x003A, bytearray([0x9D, 0x3C, 0x1F])) # sta !checkpoints_flags,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x003D, bytearray([0xCA])) # dex
+ rom.write_bytes(INIT_SRAM_ADDR + 0x003E, bytearray([0x10, 0xD8])) # bpl -
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0040, bytearray([0xC2, 0x10])) # rep #$10
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0042, bytearray([0xA2, 0x45, 0x02])) # ldx.w #!blocksanity_locs-1
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0045, bytearray([0xBF, 0x00, 0x0A, 0x70])) # - lda !blocksanity_data_sram,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0049, bytearray([0x9F, 0x00, 0xA4, 0x7F])) # sta !blocksanity_data_flags,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x004D, bytearray([0xCA])) # dex
+ rom.write_bytes(INIT_SRAM_ADDR + 0x004E, bytearray([0x10, 0xF5])) # bpl -
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0050, bytearray([0xE2, 0x10])) # sep #$10
+ #rom.write_bytes(INIT_SRAM_ADDR + 0x0052, bytearray([0xAF, 0x50, 0x09, 0x70])) # lda !received_items_count_sram+$00
+ #rom.write_bytes(INIT_SRAM_ADDR + 0x0056, bytearray([0x8F, 0x0E, 0xA0, 0x7F])) # sta !received_items_count+$00
+ #rom.write_bytes(INIT_SRAM_ADDR + 0x005A, bytearray([0xAF, 0x51, 0x09, 0x70])) # lda !received_items_count_sram+$01
+ #rom.write_bytes(INIT_SRAM_ADDR + 0x005E, bytearray([0x8F, 0x0F, 0xA0, 0x7F])) # sta !received_items_count+$01
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0052, bytearray([0xEA] * 0x17)) # Ugly, will apply be better when we port everything to a Base Patch
+ #rom.write_bytes(INIT_SRAM_ADDR + 0x0062, bytearray([0xAF, 0x52, 0x09, 0x70])) # lda !special_world_clear_sram
+ #rom.write_bytes(INIT_SRAM_ADDR + 0x0066, bytearray([0x8D, 0xFF, 0x1F])) # sta !special_world_clear_flag
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0069, bytearray([0xAF, 0x54, 0x09, 0x70])) # lda !goal_item_count_sram
+ rom.write_bytes(INIT_SRAM_ADDR + 0x006D, bytearray([0x8F, 0x1E, 0xA0, 0x7F])) # sta !goal_item_count
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0071, bytearray([0x28])) # plp
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0072, bytearray([0x5C, 0xFB, 0x9C, 0x00])) # jml $009CFB
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0076, bytearray([0xDA])) # .clear phx
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0077, bytearray([0xA2, 0x5F, 0x00])) # ldx.w #$005F
+ rom.write_bytes(INIT_SRAM_ADDR + 0x007A, bytearray([0xA9, 0x00])) # lda #$00
+ rom.write_bytes(INIT_SRAM_ADDR + 0x007C, bytearray([0x9F, 0x00, 0x08, 0x70])) # - sta !level_clears_sram,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0080, bytearray([0xCA])) # dex
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0081, bytearray([0x10, 0xF9])) # bpl -
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0083, bytearray([0xA2, 0x0B, 0x00])) # ldx.w #$000B
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0086, bytearray([0x9F, 0x40, 0x09, 0x70])) # - sta !blocksanity_sram,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x008A, bytearray([0x9F, 0x00, 0x09, 0x70])) # sta !yoshi_coins_sram,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x008E, bytearray([0x9F, 0x30, 0x09, 0x70])) # sta !bonus_block_sram,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0092, bytearray([0x9F, 0x10, 0x09, 0x70])) # sta !moons_sram,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x0096, bytearray([0x9F, 0x20, 0x09, 0x70])) # sta !checkpoints_sram,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x009A, bytearray([0xCA])) # dex
+ rom.write_bytes(INIT_SRAM_ADDR + 0x009B, bytearray([0x10, 0xE9])) # bpl -
+ rom.write_bytes(INIT_SRAM_ADDR + 0x009D, bytearray([0xA2, 0x45, 0x02])) # ldx.w #!blocksanity_locs-1
+ rom.write_bytes(INIT_SRAM_ADDR + 0x00A0, bytearray([0x9F, 0x00, 0x0A, 0x70])) # - sta !blocksanity_data_sram,x
+ rom.write_bytes(INIT_SRAM_ADDR + 0x00A4, bytearray([0xCA])) # dex
+ rom.write_bytes(INIT_SRAM_ADDR + 0x00A5, bytearray([0x10, 0xF9])) # bpl -
+ rom.write_bytes(INIT_SRAM_ADDR + 0x00A7, bytearray([0x8F, 0x52, 0x09, 0x70])) # sta !special_world_clear_sram
+ rom.write_bytes(INIT_SRAM_ADDR + 0x00AB, bytearray([0x8F, 0x50, 0x09, 0x70])) # sta !received_items_count_sram+$00
+ rom.write_bytes(INIT_SRAM_ADDR + 0x00AF, bytearray([0x8F, 0x51, 0x09, 0x70])) # sta !received_items_count_sram+$01
+ rom.write_bytes(INIT_SRAM_ADDR + 0x00B3, bytearray([0x8F, 0x54, 0x09, 0x70])) # sta !goal_item_count_sram
+ rom.write_bytes(INIT_SRAM_ADDR + 0x00B7, bytearray([0xFA])) # plx
+ rom.write_bytes(INIT_SRAM_ADDR + 0x00B8, bytearray([0x5C, 0x22, 0x9D, 0x00])) # jml $009D22
+
+ SAVE_SRAM_ADDR = 0x7F300
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0000, bytearray([0xE2, 0x30])) # save_sram: sep #$30
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0002, bytearray([0xAB])) # plb
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0003, bytearray([0xA2, 0x5F])) # ldx.b #$5F
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0005, bytearray([0xBF, 0x00, 0xA2, 0x7F])) # - lda !level_clears,x
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0009, bytearray([0x9F, 0x00, 0x08, 0x70])) # sta !level_clears_sram,x
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x000D, bytearray([0xCA])) # dex
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x000E, bytearray([0x10, 0xF5])) # bpl -
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0010, bytearray([0xA2, 0x0B])) # ldx #$0B
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0012, bytearray([0xBF, 0x10, 0xA0, 0x7F])) # - lda !blocksanity_flags,x
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0016, bytearray([0x9F, 0x40, 0x09, 0x70])) # sta !blocksanity_sram,x
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x001A, bytearray([0xBD, 0x2F, 0x1F])) # lda !yoshi_coins_flags,x
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x001D, bytearray([0x9F, 0x00, 0x09, 0x70])) # sta !yoshi_coins_sram,x
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0021, bytearray([0xBD, 0xEE, 0x1F])) # lda !moons_flags,x
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0024, bytearray([0x9F, 0x10, 0x09, 0x70])) # sta !moons_sram,x
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0028, bytearray([0xBF, 0x00, 0xA0, 0x7F])) # lda !bonus_block_flags,x
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x002C, bytearray([0x9F, 0x30, 0x09, 0x70])) # sta !bonus_block_sram,x
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0030, bytearray([0xBD, 0x3C, 0x1F])) # lda !checkpoints_flags,x
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0033, bytearray([0x9F, 0x20, 0x09, 0x70])) # sta !checkpoints_sram,x
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0037, bytearray([0xCA])) # dex
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0038, bytearray([0x10, 0xD8])) # bpl -
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x003A, bytearray([0xC2, 0x10])) # rep #$10
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x003C, bytearray([0xA2, 0x45, 0x02])) # ldx.w #!blocksanity_locs-1
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x003F, bytearray([0xBF, 0x00, 0xA4, 0x7F])) # - lda !blocksanity_data_flags,x
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0043, bytearray([0x9F, 0x00, 0x0A, 0x70])) # sta !blocksanity_data_sram,x
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0047, bytearray([0xCA])) # dex
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0048, bytearray([0x10, 0xF5])) # bpl -
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x004A, bytearray([0xE2, 0x10])) # sep #$10
+ #rom.write_bytes(SAVE_SRAM_ADDR + 0x004C, bytearray([0xAD, 0xFF, 0x1F])) # lda !special_world_clear_flag
+ #rom.write_bytes(SAVE_SRAM_ADDR + 0x004F, bytearray([0x8F, 0x52, 0x09, 0x70])) # sta !special_world_clear_sram
+ #rom.write_bytes(SAVE_SRAM_ADDR + 0x0053, bytearray([0xAF, 0x0E, 0xA0, 0x7F])) # lda !received_items_count+$00
+ #rom.write_bytes(SAVE_SRAM_ADDR + 0x0057, bytearray([0x8F, 0x50, 0x09, 0x70])) # sta !received_items_count_sram+$00
+ #rom.write_bytes(SAVE_SRAM_ADDR + 0x005B, bytearray([0xAF, 0x0F, 0xA0, 0x7F])) # lda !received_items_count+$01
+ #rom.write_bytes(SAVE_SRAM_ADDR + 0x005F, bytearray([0x8F, 0x51, 0x09, 0x70])) # sta !received_items_count_sram+$01
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x004C, bytearray([0xEA] * 0x17)) # Ugly, will apply be better when we port everything to a Base Patch
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0063, bytearray([0xAF, 0x0F, 0xA0, 0x7F])) # lda !goal_item_count
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x0067, bytearray([0x8F, 0x51, 0x09, 0x70])) # sta !goal_item_count_sram
+ rom.write_bytes(SAVE_SRAM_ADDR + 0x006B, bytearray([0x6B])) # rtl
+
+ INIT_RAM_ADDR = 0x7F0A0
+ rom.write_bytes(INIT_RAM_ADDR + 0x0000, bytearray([0xA9, 0xAA])) # init_ram: lda #$AA
+ rom.write_bytes(INIT_RAM_ADDR + 0x0002, bytearray([0x8D, 0x00, 0x04])) # sta $0400
+ rom.write_bytes(INIT_RAM_ADDR + 0x0005, bytearray([0xA9, 0x00])) # clear_level_data: lda #$00
+ rom.write_bytes(INIT_RAM_ADDR + 0x0007, bytearray([0xA2, 0x5F])) # ldx #$5F
+ rom.write_bytes(INIT_RAM_ADDR + 0x0009, bytearray([0x9F, 0x00, 0xA2, 0x7F])) # .loop sta !level_clears,x
+ rom.write_bytes(INIT_RAM_ADDR + 0x000D, bytearray([0xCA])) # dex
+ rom.write_bytes(INIT_RAM_ADDR + 0x000E, bytearray([0x10, 0xF9])) # bpl .loop
+ rom.write_bytes(INIT_RAM_ADDR + 0x0010, bytearray([0xC2, 0x10])) # rep #$10
+ rom.write_bytes(INIT_RAM_ADDR + 0x0012, bytearray([0xA2, 0x0B, 0x00])) # ldx.w #$000B
+ rom.write_bytes(INIT_RAM_ADDR + 0x0015, bytearray([0x9F, 0x10, 0xA0, 0x7F])) # - sta !blocksanity_flags,x
+ rom.write_bytes(INIT_RAM_ADDR + 0x0019, bytearray([0x9D, 0x2F, 0x1F])) # sta !yoshi_coins_flags,x
+ rom.write_bytes(INIT_RAM_ADDR + 0x001C, bytearray([0x9D, 0xEE, 0x1F])) # sta !moons_flags,x
+ rom.write_bytes(INIT_RAM_ADDR + 0x001F, bytearray([0x9F, 0x00, 0xA0, 0x7F])) # sta !bonus_block_flags,x
+ rom.write_bytes(INIT_RAM_ADDR + 0x0023, bytearray([0x9D, 0x3C, 0x1F])) # sta !checkpoints_flags,x
+ rom.write_bytes(INIT_RAM_ADDR + 0x0026, bytearray([0xCA])) # dex
+ rom.write_bytes(INIT_RAM_ADDR + 0x0027, bytearray([0x10, 0xEC])) # bpl -
+ rom.write_bytes(INIT_RAM_ADDR + 0x0029, bytearray([0xA2, 0x45, 0x02])) # ldx.w #!blocksanity_locs-1
+ rom.write_bytes(INIT_RAM_ADDR + 0x002C, bytearray([0x9F, 0x00, 0xA4, 0x7F])) # - sta !blocksanity_data_flags,x
+ rom.write_bytes(INIT_RAM_ADDR + 0x0030, bytearray([0xCA])) # dex
+ rom.write_bytes(INIT_RAM_ADDR + 0x0031, bytearray([0x10, 0xF9])) # bpl -
+ rom.write_bytes(INIT_RAM_ADDR + 0x0033, bytearray([0xA2, 0x22, 0x04])) # ldx #$0422
+ rom.write_bytes(INIT_RAM_ADDR + 0x0036, bytearray([0x9F, 0x00, 0xB0, 0x7F])) # - sta !score_sprite_count,x
+ rom.write_bytes(INIT_RAM_ADDR + 0x003A, bytearray([0xCA])) # dex
+ rom.write_bytes(INIT_RAM_ADDR + 0x003B, bytearray([0x10, 0xF9])) # bpl -
+ #rom.write_bytes(INIT_RAM_ADDR + 0x003D, bytearray([0x8D, 0xFF, 0x1F])) # sta !special_world_clear_flag
+ rom.write_bytes(INIT_RAM_ADDR + 0x003D, bytearray([0xEA, 0xEA, 0xEA])) # sta !special_world_clear_flag
+ rom.write_bytes(INIT_RAM_ADDR + 0x0040, bytearray([0x8F, 0x0E, 0xA0, 0x7F])) # sta !received_items_count+$00
+ rom.write_bytes(INIT_RAM_ADDR + 0x0044, bytearray([0x8F, 0x0F, 0xA0, 0x7F])) # sta !received_items_count+$01
+ rom.write_bytes(INIT_RAM_ADDR + 0x0048, bytearray([0x8F, 0x1E, 0xA0, 0x7F])) # sta !goal_item_count
+ rom.write_bytes(INIT_RAM_ADDR + 0x004C, bytearray([0xA9, 0xFF])) # lda #$FF
+ rom.write_bytes(INIT_RAM_ADDR + 0x004E, bytearray([0x8D, 0x3C, 0x0F])) # sta !thwimp_index
+ rom.write_bytes(INIT_RAM_ADDR + 0x0051, bytearray([0xE2, 0x10])) # sep #$10
+ rom.write_bytes(INIT_RAM_ADDR + 0x0053, bytearray([0x22, 0x20, 0xF1, 0x0F])) # jsl clear_tilemap
+ rom.write_bytes(INIT_RAM_ADDR + 0x0057, bytearray([0x5C, 0xC0, 0x93, 0x00])) # jml $0093C0
+
+def handle_map_indicators(rom):
+ rom.write_bytes(0x265EE, bytearray([0x4C, 0x00, 0xA3])) # org $04E5EE : jmp check_events
+
+ GET_MAP_LEVEL_NUM_ADDR = 0x22340
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0000, bytearray([0xC2, 0x30])) # get_translevel_num: rep #$30
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0002, bytearray([0xAE, 0xD6, 0x0D])) # ldx $0DD6
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0005, bytearray([0xBD, 0x1F, 0x1F])) # lda $1F1F,x
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0008, bytearray([0x85, 0x00])) # sta $00
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x000A, bytearray([0xBD, 0x21, 0x1F])) # lda $1F21,x
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x000D, bytearray([0x85, 0x02])) # sta $02
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x000F, bytearray([0x8A])) # txa
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0010, bytearray([0x4A])) # lsr
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0011, bytearray([0x4A])) # lsr
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0012, bytearray([0xAA])) # tax
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0013, bytearray([0x20, 0x85, 0x98])) # jsr $9885
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0016, bytearray([0xA6, 0x04])) # ldx $04
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0018, bytearray([0xBF, 0x00, 0xD0, 0x7E])) # lda $7ED000,x
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x001C, bytearray([0xE2, 0x30])) # sep #$30
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x001E, bytearray([0x85, 0x60])) # sta $60
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0020, bytearray([0xAA])) # tax
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0021, bytearray([0xBF, 0x00, 0xFF, 0x06])) # lda $06FF00,x
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0025, bytearray([0xC9, 0xFF])) # cmp #$FF
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0027, bytearray([0xF0, 0x02])) # beq +
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x0029, bytearray([0x85, 0x60])) # sta $60
+ rom.write_bytes(GET_MAP_LEVEL_NUM_ADDR + 0x002B, bytearray([0x60])) # + rts
+
+ GET_MAP_LEVEL_BIT_ADDR = 0x22380
+ rom.write_bytes(GET_MAP_LEVEL_BIT_ADDR + 0x0000, bytearray([0xA5, 0x60])) # get_translevel_bit: lda $60
+ rom.write_bytes(GET_MAP_LEVEL_BIT_ADDR + 0x0002, bytearray([0x4A])) # lsr
+ rom.write_bytes(GET_MAP_LEVEL_BIT_ADDR + 0x0003, bytearray([0x4A])) # lsr
+ rom.write_bytes(GET_MAP_LEVEL_BIT_ADDR + 0x0004, bytearray([0x4A])) # lsr
+ rom.write_bytes(GET_MAP_LEVEL_BIT_ADDR + 0x0005, bytearray([0xA8])) # tay
+ rom.write_bytes(GET_MAP_LEVEL_BIT_ADDR + 0x0006, bytearray([0xA5, 0x60])) # lda $60
+ rom.write_bytes(GET_MAP_LEVEL_BIT_ADDR + 0x0008, bytearray([0x29, 0x07])) # and #$07
+ rom.write_bytes(GET_MAP_LEVEL_BIT_ADDR + 0x000A, bytearray([0xAA])) # tax
+ rom.write_bytes(GET_MAP_LEVEL_BIT_ADDR + 0x000B, bytearray([0x60])) # rts
+
+ UPDATE_MAP_PTRS_ADDR = 0x223C0
+ rom.write_bytes(UPDATE_MAP_PTRS_ADDR + 0x0000, bytearray([0xE6, 0x00])) # update_flag_pointers: inc $00
+ rom.write_bytes(UPDATE_MAP_PTRS_ADDR + 0x0002, bytearray([0xE6, 0x00])) # inc $00
+ rom.write_bytes(UPDATE_MAP_PTRS_ADDR + 0x0004, bytearray([0xE6, 0x03])) # inc $03
+ rom.write_bytes(UPDATE_MAP_PTRS_ADDR + 0x0006, bytearray([0xE6, 0x03])) # inc $03
+ rom.write_bytes(UPDATE_MAP_PTRS_ADDR + 0x0008, bytearray([0xE6, 0x06])) # inc $06
+ rom.write_bytes(UPDATE_MAP_PTRS_ADDR + 0x000A, bytearray([0xE6, 0x06])) # inc $06
+ rom.write_bytes(UPDATE_MAP_PTRS_ADDR + 0x000C, bytearray([0xE6, 0x62])) # inc $62
+ rom.write_bytes(UPDATE_MAP_PTRS_ADDR + 0x000E, bytearray([0xE6, 0x62])) # inc $62
+ rom.write_bytes(UPDATE_MAP_PTRS_ADDR + 0x0010, bytearray([0xE6, 0x63])) # inc $63
+ rom.write_bytes(UPDATE_MAP_PTRS_ADDR + 0x0012, bytearray([0x60])) # rts
+
+ CLEAR_TILEMAP_ADDR = 0x7F120
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x0000, bytearray([0xC2, 0x20])) # clear_tilemap: rep #$20
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x0002, bytearray([0xA9, 0x1F, 0x39])) # lda.w #$3900+!icon_disabled
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x0005, bytearray([0xA2, 0x1E])) # ldx #$1E
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x0007, bytearray([0x9F, 0x20, 0xA1, 0x7F])) # .loop sta !ow_tilemap_switches,x
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x000B, bytearray([0x9F, 0x00, 0xA1, 0x7F])) # sta !ow_tilemap_abilities,x
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x000F, bytearray([0x9F, 0x40, 0xA1, 0x7F])) # sta !ow_tilemap_flags_top,x
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x0013, bytearray([0x9F, 0x60, 0xA1, 0x7F])) # sta !ow_tilemap_flags_mid,x
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x0017, bytearray([0x9F, 0x80, 0xA1, 0x7F])) # sta !ow_tilemap_flags_bot,x
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x001B, bytearray([0xCA])) # dex
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x001C, bytearray([0xCA])) # dex
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x001D, bytearray([0x10, 0xE8])) # bpl .loop
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x001F, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x0021, bytearray([0xA9, 0x07])) # lda #$07
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x0023, bytearray([0x85, 0x63])) # sta $63
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x0025, bytearray([0x0A])) # asl
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x0026, bytearray([0x85, 0x62])) # sta $62
+ rom.write_bytes(CLEAR_TILEMAP_ADDR + 0x0028, bytearray([0x6B])) # rtl
+
+ CLEAR_TILEMAP_FLAGS_ADDR = 0x7F180
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x0000, bytearray([0xC2, 0x20])) # clear_tilemap_flags: rep #$20
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x0002, bytearray([0xA9, 0x1F, 0x39])) # lda.w #$3900+!icon_disabled
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x0005, bytearray([0xA2, 0x0C])) # ldx.b #($07*2)-2
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x0007, bytearray([0x9F, 0x40, 0xA1, 0x7F])) # .loop sta !ow_tilemap_flags_top,x
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x000B, bytearray([0x9F, 0x60, 0xA1, 0x7F])) # sta !ow_tilemap_flags_mid,x
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x000F, bytearray([0x9F, 0x80, 0xA1, 0x7F])) # sta !ow_tilemap_flags_bot,x
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x0013, bytearray([0xCA])) # dex
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x0014, bytearray([0xCA])) # dex
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x0015, bytearray([0x10, 0xF0])) # bpl .loop
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x0017, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x0019, bytearray([0xA9, 0x06])) # lda #$06
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x001B, bytearray([0x85, 0x63])) # sta $63
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x001D, bytearray([0x0A])) # asl
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x001E, bytearray([0x85, 0x62])) # sta $62
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x0020, bytearray([0xA9, 0xFF])) # lda #$FF
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x0022, bytearray([0x8D, 0x3C, 0x0F])) # sta !thwimp_index
+ rom.write_bytes(CLEAR_TILEMAP_FLAGS_ADDR + 0x0025, bytearray([0x6B])) # rtl
+
+ CHECK_EVENTS_ADDR = 0x22300
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x0000, bytearray([0xDA])) # check_events: phx
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x0001, bytearray([0x20, 0x40, 0xA3])) # jsr get_translevel_num
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x0004, bytearray([0xAD, 0xD5, 0x0D])) # lda $0DD5
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x0007, bytearray([0xF0, 0x17])) # beq .dont_sync
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x0009, bytearray([0x30, 0x15])) # bmi .dont_sync
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x000B, bytearray([0xC9, 0x05])) # cmp #$05
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x000D, bytearray([0xB0, 0x11])) # bcs .dont_sync
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x000F, bytearray([0x29, 0x07])) # and #$07
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x0011, bytearray([0xAA])) # tax
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x0012, bytearray([0xBF, 0x7D, 0x9E, 0x00])) # lda.l $009E7D,x
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x0016, bytearray([0xA6, 0x60])) # ldx $60
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x0018, bytearray([0x1F, 0x00, 0xA2, 0x7F])) # ora !level_clears,x
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x001C, bytearray([0x9F, 0x00, 0xA2, 0x7F])) # sta !level_clears,x
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x0020, bytearray([0xFA])) # .dont_sync plx
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x0021, bytearray([0xAD, 0xD5, 0x0D])) # lda $0DD5
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x0024, bytearray([0xC9, 0x02])) # cmp #$02
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x0026, bytearray([0xD0, 0x03])) # bne .no_secret
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x0028, bytearray([0xEE, 0xEA, 0x1D])) # inc $1DEA
+ rom.write_bytes(CHECK_EVENTS_ADDR + 0x002B, bytearray([0x4C, 0xF8, 0xE5])) # .no_secret jmp $E5F8
+
+ DRAW_MAP_TILEMAP_ADDR = 0x221B6
+ rom.write_bytes(0x00222, bytearray([0x5C, 0xB6, 0xA1, 0x04])) # org $008222 : jml draw_ow_tilemap
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0000, bytearray([0xAD, 0xD9, 0x13])) # draw_ow_tilemap: lda $13D9
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0003, bytearray([0xC9, 0x0A])) # cmp #$0A
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0005, bytearray([0xD0, 0x04])) # bne write_tilemap
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0007, bytearray([0x5C, 0x29, 0x82, 0x00])) # jml $008229
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x000B, bytearray([0xC2, 0x20])) # write_tilemap: rep #$20
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x000D, bytearray([0xA0, 0x80])) # ldy #$80
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x000F, bytearray([0x8C, 0x15, 0x21])) # sty $2115
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0012, bytearray([0xA9, 0x27, 0x50])) # write_abilities: lda #!vram_abilities_top
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0015, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0018, bytearray([0xA2, 0x00])) # ldx.b #$00
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x001A, bytearray([0xBF, 0xA2, 0xA2, 0x04])) # ..loop lda.l abilities_top,x
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x001E, bytearray([0x8D, 0x18, 0x21])) # sta $2118
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0021, bytearray([0xE8])) # inx
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0022, bytearray([0xE8])) # inx
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0023, bytearray([0xE0, 0x14])) # cpx.b #$0A*2
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0025, bytearray([0x90, 0xF3])) # bcc ..loop
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0027, bytearray([0xA9, 0x47, 0x50])) # .mid lda #!vram_abilities_mid
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x002A, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x002D, bytearray([0xA2, 0x00])) # ldx.b #$00
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x002F, bytearray([0xBF, 0xB6, 0xA2, 0x04])) # ..loop lda.l abilities_bottom,x
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0033, bytearray([0x8D, 0x18, 0x21])) # sta $2118
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0036, bytearray([0xE8])) # inx
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0037, bytearray([0xE8])) # inx
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0038, bytearray([0xE0, 0x14])) # cpx.b #$0A*2
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x003A, bytearray([0x90, 0xF3])) # bcc ..loop
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x003C, bytearray([0xA9, 0x67, 0x50])) # .bot lda #!vram_abilities_bot
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x003F, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0042, bytearray([0xA2, 0x00])) # ldx.b #$00
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0044, bytearray([0xBF, 0x00, 0xA1, 0x7F])) # ..loop lda !ow_tilemap_abilities,x
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0048, bytearray([0x8D, 0x18, 0x21])) # sta $2118
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x004B, bytearray([0xE8])) # inx
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x004C, bytearray([0xE8])) # inx
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x004D, bytearray([0xE0, 0x14])) # cpx.b #$0A*2
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x004F, bytearray([0x90, 0xF3])) # bcc ..loop
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0051, bytearray([0xA9, 0x32, 0x50])) # write_switches: lda #!vram_switches_top
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0054, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0057, bytearray([0xA2, 0x00])) # ldx.b #$00
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0059, bytearray([0xBF, 0xCA, 0xA2, 0x04])) # ..loop lda.l switches_top,x
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x005D, bytearray([0x8D, 0x18, 0x21])) # sta $2118
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0060, bytearray([0xE8])) # inx
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0061, bytearray([0xE8])) # inx
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0062, bytearray([0xE0, 0x0A])) # cpx.b #$05*2
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0064, bytearray([0x90, 0xF3])) # bcc ..loop
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0066, bytearray([0xA9, 0x52, 0x50])) # .mid lda #!vram_switches_mid
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0069, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x006C, bytearray([0xA2, 0x00])) # ldx.b #$00
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x006E, bytearray([0xBF, 0xD4, 0xA2, 0x04])) # ..loop lda.l switches_bottom,x
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0072, bytearray([0x8D, 0x18, 0x21])) # sta $2118
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0075, bytearray([0xE8])) # inx
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0076, bytearray([0xE8])) # inx
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0077, bytearray([0xE0, 0x0A])) # cpx.b #$05*2
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0079, bytearray([0x90, 0xF3])) # bcc ..loop
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x007B, bytearray([0xA9, 0x72, 0x50])) # .bot lda #!vram_switches_bot
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x007E, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0081, bytearray([0xA2, 0x00])) # ldx.b #$00
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0083, bytearray([0xBF, 0x20, 0xA1, 0x7F])) # ..loop lda !ow_tilemap_switches,x
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0087, bytearray([0x8D, 0x18, 0x21])) # sta $2118
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x008A, bytearray([0xE8])) # inx
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x008B, bytearray([0xE8])) # inx
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x008C, bytearray([0xE0, 0x0A])) # cpx.b #$05*2
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x008E, bytearray([0x90, 0xF3])) # bcc ..loop
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0090, bytearray([0xD4, 0x00])) # write_level_data: pei ($00)
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0092, bytearray([0xA5, 0x63])) # lda $63
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0094, bytearray([0x29, 0xFF, 0x00])) # and #$00FF
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0097, bytearray([0x85, 0x00])) # sta $00
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0099, bytearray([0xF0, 0x48])) # beq .skip_flags
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x009B, bytearray([0xA9, 0x3E, 0x50])) # .top lda.w #!vram_level_data_top+$01
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x009E, bytearray([0x38])) # sec
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x009F, bytearray([0xE5, 0x00])) # sbc $00
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00A1, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00A4, bytearray([0xA6, 0x62])) # ldx.b $62
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00A6, bytearray([0xCA])) # dex
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00A7, bytearray([0xCA])) # dex
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00A8, bytearray([0xBF, 0x40, 0xA1, 0x7F])) # ..loop lda.l !ow_tilemap_flags_top,x
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00AC, bytearray([0x8D, 0x18, 0x21])) # sta $2118
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00AF, bytearray([0xCA])) # dex
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00B0, bytearray([0xCA])) # dex
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00B1, bytearray([0x10, 0xF5])) # bpl ..loop
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00B3, bytearray([0xA9, 0x5E, 0x50])) # .mid lda.w #!vram_level_data_mid+$01
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00B6, bytearray([0x38])) # sec
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00B7, bytearray([0xE5, 0x00])) # sbc $00
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00B9, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00BC, bytearray([0xA6, 0x62])) # ldx.b $62
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00BE, bytearray([0xCA])) # dex
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00BF, bytearray([0xCA])) # dex
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00C0, bytearray([0xBF, 0x60, 0xA1, 0x7F])) # ..loop lda.l !ow_tilemap_flags_mid,x
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00C4, bytearray([0x8D, 0x18, 0x21])) # sta $2118
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00C7, bytearray([0xCA])) # dex
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00C8, bytearray([0xCA])) # dex
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00C9, bytearray([0x10, 0xF5])) # bpl ..loop
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00CB, bytearray([0xA9, 0x7E, 0x50])) # .bot lda.w #!vram_level_data_bot+$01
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00CE, bytearray([0x38])) # sec
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00CF, bytearray([0xE5, 0x00])) # sbc $00
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00D1, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00D4, bytearray([0xA6, 0x62])) # ldx.b $62
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00D6, bytearray([0xCA])) # dex
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00D7, bytearray([0xCA])) # dex
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00D8, bytearray([0xBF, 0x80, 0xA1, 0x7F])) # ..loop lda.l !ow_tilemap_flags_bot,x
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00DC, bytearray([0x8D, 0x18, 0x21])) # sta $2118
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00DF, bytearray([0xCA])) # dex
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00E0, bytearray([0xCA])) # dex
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00E1, bytearray([0x10, 0xF5])) # bpl ..loop
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00E3, bytearray([0x68])) # .skip_flags pla
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00E4, bytearray([0x85, 0x00])) # sta $00
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00E6, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00E8, bytearray([0x5C, 0x37, 0x82, 0x00])) # jml $008237
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00EC, bytearray([0x0F, 0x39, 0x12, 0x39])) # abilities_top: dw $390F,$3912
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00F0, bytearray([0x11, 0x39, 0x02, 0x39])) # dw $3911,$3902
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00F4, bytearray([0x12, 0x39, 0x02, 0x39])) # dw $3912,$3902
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00F8, bytearray([0x18, 0x39, 0x0F, 0x39])) # dw $3918,$390F
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x00FC, bytearray([0x0F, 0x39, 0x12, 0x39])) # dw $390F,$3912
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0100, bytearray([0x4E, 0x39, 0x4F, 0x39])) # abilities_bottom: dw $394E,$394F
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0104, bytearray([0x54, 0x39, 0x40, 0x39])) # dw $3954,$3940
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0108, bytearray([0x56, 0x39, 0x4B, 0x39])) # dw $3956,$394B
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x010C, bytearray([0x4E, 0x39, 0x52, 0x39])) # dw $394E,$3952
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0110, bytearray([0x41, 0x39, 0x53, 0x39])) # dw $3941,$3953
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0114, bytearray([0x18, 0x39, 0x06, 0x39])) # switches_top: dw $3918,$3906
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0118, bytearray([0x11, 0x39, 0x01, 0x39])) # dw $3911,$3901
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x011C, bytearray([0x12, 0x39])) # dw $3912
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x011E, bytearray([0x12, 0x39, 0x12, 0x39])) # switches_bottom: dw $3912,$3912
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0122, bytearray([0x12, 0x39, 0x12, 0x39])) # dw $3912,$3912
+ rom.write_bytes(DRAW_MAP_TILEMAP_ADDR + 0x0126, bytearray([0x4F, 0x39])) # dw $394F
+
+ BUILD_TILEMAP_ADDR = 0x26F3E
+ rom.write_bytes(0x021C7, bytearray([0x22, 0x3E, 0xEF, 0x04])) # org $00A1C7 : jsl prepare_dynamic_tilemap
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0000, bytearray([0x22, 0x41, 0x82, 0x04])) # prepare_dynamic_tilemap: jsl $048241
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0004, bytearray([0xA0, 0x22])) # .handle_powerup: ldy #$22
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0006, bytearray([0xAD, 0x2D, 0x1F])) # lda $1F2D
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0009, bytearray([0x4A])) # lsr
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x000A, bytearray([0x90, 0x01])) # bcc $01
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x000C, bytearray([0xC8])) # iny
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x000D, bytearray([0x4A])) # lsr
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x000E, bytearray([0x90, 0x01])) # bcc $01
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0010, bytearray([0xC8])) # iny
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0011, bytearray([0x4A])) # lsr
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0012, bytearray([0x90, 0x01])) # bcc $01
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0014, bytearray([0xC8])) # iny
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0015, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0016, bytearray([0x8F, 0x00, 0xA1, 0x7F])) # sta !ow_tilemap_abilities ; Progressive powerup
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x001A, bytearray([0xA0, 0x5E])) # .handle_spinjump: ldy #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x001C, bytearray([0xAD, 0x1C, 0x1F])) # lda $1F1C
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x001F, bytearray([0x29, 0x08])) # and #$08
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0021, bytearray([0xF0, 0x02])) # beq $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0023, bytearray([0xA0, 0x3F])) # ldy #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0025, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0026, bytearray([0x8F, 0x02, 0xA1, 0x7F])) # sta !ow_tilemap_abilities+$02 ; Spin jump
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x002A, bytearray([0xA0, 0x5E])) # .handle_run: ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x002C, bytearray([0xAD, 0x1C, 0x1F])) # lda $1F1C
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x002F, bytearray([0x29, 0x80])) # and #$80
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0031, bytearray([0xF0, 0x02])) # beq $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0033, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0035, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0036, bytearray([0x8F, 0x04, 0xA1, 0x7F])) # sta !ow_tilemap_abilities+$04 ; Run
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x003A, bytearray([0xA0, 0x5E])) # .handle_carry: ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x003C, bytearray([0xAD, 0x1C, 0x1F])) # lda $1F1C
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x003F, bytearray([0x29, 0x40])) # and #$40
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0041, bytearray([0xF0, 0x02])) # beq $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0043, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0045, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0046, bytearray([0x8F, 0x06, 0xA1, 0x7F])) # sta !ow_tilemap_abilities+$06 ; Carry
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x004A, bytearray([0xA0, 0x5E])) # .handle_swim: ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x004C, bytearray([0xAD, 0x1C, 0x1F])) # lda $1F1C
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x004F, bytearray([0x29, 0x04])) # and #$04
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0051, bytearray([0xF0, 0x02])) # beq $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0053, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0055, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0056, bytearray([0x8F, 0x08, 0xA1, 0x7F])) # sta !ow_tilemap_abilities+$08 ; Swim
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x005A, bytearray([0xA0, 0x5E])) # .handle_climb: ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x005C, bytearray([0xAD, 0x1C, 0x1F])) # lda $1F1C
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x005F, bytearray([0x29, 0x20])) # and #$20
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0061, bytearray([0xF0, 0x02])) # beq $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0063, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0065, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0066, bytearray([0x8F, 0x0A, 0xA1, 0x7F])) # sta !ow_tilemap_abilities+$0A ; Climb
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x006A, bytearray([0xA0, 0x5E])) # .handle_yoshi: ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x006C, bytearray([0xAD, 0x1C, 0x1F])) # lda $1F1C
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x006F, bytearray([0x29, 0x02])) # and #$02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0071, bytearray([0xF0, 0x02])) # beq $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0073, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0075, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0076, bytearray([0x8F, 0x0C, 0xA1, 0x7F])) # sta !ow_tilemap_abilities+$0C ; Yoshi
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x007A, bytearray([0xA0, 0x5E])) # .handle_pswitch: ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x007C, bytearray([0xAD, 0x1C, 0x1F])) # lda $1F1C
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x007F, bytearray([0x29, 0x10])) # and #$10
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0081, bytearray([0xF0, 0x02])) # beq $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0083, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0085, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0086, bytearray([0x8F, 0x0E, 0xA1, 0x7F])) # sta !ow_tilemap_abilities+$0E ; P-Switch
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x008A, bytearray([0xA0, 0x5E])) # .handle_pballoon: ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x008C, bytearray([0xAD, 0x2D, 0x1F])) # lda $1F2D
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x008F, bytearray([0x29, 0x08])) # and #$08
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0091, bytearray([0xF0, 0x02])) # beq $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0093, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0095, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0096, bytearray([0x8F, 0x10, 0xA1, 0x7F])) # sta !ow_tilemap_abilities+$10 ; P-Balloon
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x009A, bytearray([0xA0, 0x5E])) # .handle_star: ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x009C, bytearray([0xAD, 0x2D, 0x1F])) # lda $1F2D
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x009F, bytearray([0x29, 0x10])) # and #$10
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00A1, bytearray([0xF0, 0x02])) # beq $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00A3, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00A5, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00A6, bytearray([0x8F, 0x12, 0xA1, 0x7F])) # sta !ow_tilemap_abilities+$12 ; Star
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00AA, bytearray([0xA0, 0x5E])) # .handle_yellow_switch: ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00AC, bytearray([0xAD, 0x28, 0x1F])) # lda $1F28
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00AF, bytearray([0xF0, 0x02])) # beq $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00B1, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00B3, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00B4, bytearray([0x8F, 0x20, 0xA1, 0x7F])) # sta !ow_tilemap_switches+$00
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00B8, bytearray([0xA0, 0x5E])) # .handle_green_switch: ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00BA, bytearray([0xAD, 0x27, 0x1F])) # lda $1F27
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00BD, bytearray([0xF0, 0x02])) # beq $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00BF, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00C1, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00C2, bytearray([0x8F, 0x22, 0xA1, 0x7F])) # sta !ow_tilemap_switches+$02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00C6, bytearray([0xA0, 0x5E])) # .handle_red_switch: ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00C8, bytearray([0xAD, 0x2A, 0x1F])) # lda $1F2A
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00CB, bytearray([0xF0, 0x02])) # beq $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00CD, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00CF, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00D0, bytearray([0x8F, 0x24, 0xA1, 0x7F])) # sta !ow_tilemap_switches+$04
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00D4, bytearray([0xA0, 0x5E])) # .handle_blue_switch: ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00D6, bytearray([0xAD, 0x29, 0x1F])) # lda $1F29
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00D9, bytearray([0xF0, 0x02])) # beq $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00DB, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00DD, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00DE, bytearray([0x8F, 0x26, 0xA1, 0x7F])) # sta !ow_tilemap_switches+$06
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00E2, bytearray([0xA0, 0x5E])) # .handle_special_world_clear: ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00E4, bytearray([0xAD, 0x1E, 0x1F])) # lda !special_world_clear_flag
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00E7, bytearray([0xF0, 0x02])) # beq $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00E9, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00EB, bytearray([0x98])) # tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00EC, bytearray([0x8F, 0x28, 0xA1, 0x7F])) # sta !ow_tilemap_switches+$08
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00F0, bytearray([0x22, 0x80, 0xF1, 0x0F])) # jsl clear_tilemap_flags
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00F4, bytearray([0xAD, 0xD9, 0x13])) # lda $13D9
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00F7, bytearray([0xC9, 0x01])) # cmp #$01
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00F9, bytearray([0xF0, 0x05])) # beq process_level
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00FB, bytearray([0xC9, 0x03])) # cmp #$03
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00FD, bytearray([0xF0, 0x01])) # beq process_level
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x00FF, bytearray([0x6B])) # rtl
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0100, bytearray([0x20, 0x40, 0xA3])) # process_level: jsr get_translevel_num
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0103, bytearray([0xA6, 0x60])) # ldx $60
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0105, bytearray([0xBF, 0x00, 0xF4, 0x0F])) # lda.l level_data,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0109, bytearray([0x10, 0x01])) # bpl .handle_data
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x010B, bytearray([0x6B])) # rtl
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x010C, bytearray([0x64, 0x62])) # .handle_data stz $62
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x010E, bytearray([0x64, 0x63])) # stz $63
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0110, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0112, bytearray([0xA9, 0x40, 0xA1])) # lda.w #!ow_tilemap_flags_top
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0115, bytearray([0x85, 0x00])) # sta $00
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0117, bytearray([0xA9, 0x60, 0xA1])) # lda.w #!ow_tilemap_flags_mid
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x011A, bytearray([0x85, 0x03])) # sta $03
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x011C, bytearray([0xA9, 0x80, 0xA1])) # lda.w #!ow_tilemap_flags_bot
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x011F, bytearray([0x85, 0x06])) # sta $06
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0121, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0123, bytearray([0xA9, 0x7F])) # lda.b #!ow_tilemap_flags_top>>16
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0125, bytearray([0x85, 0x02])) # sta $02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0127, bytearray([0x85, 0x05])) # sta $05
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0129, bytearray([0x85, 0x08])) # sta $08
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x012B, bytearray([0xAF, 0xAB, 0xBF, 0x03])) # handle_blocksanity: lda.l blocksanity_enabled_flag
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x012F, bytearray([0xF0, 0x30])) # beq handle_bonus_blocks
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0131, bytearray([0xA6, 0x60])) # ldx $60
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0133, bytearray([0xA0, 0x1F])) # ldy.b #!icon_disabled
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0135, bytearray([0xBF, 0x00, 0xF4, 0x0F])) # lda.l level_data,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0139, bytearray([0x29, 0x40])) # and #$40
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x013B, bytearray([0xF0, 0x24])) # beq handle_bonus_blocks
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x013D, bytearray([0xA0, 0x5E])) # ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x013F, bytearray([0x5A])) # phy
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0140, bytearray([0x20, 0x80, 0xA3])) # jsr get_translevel_bit
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0143, bytearray([0xDA])) # phx
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0144, bytearray([0xBB])) # tyx
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0145, bytearray([0xBF, 0x10, 0xA0, 0x7F])) # lda.l !blocksanity_flags,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0149, bytearray([0xFA])) # plx
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x014A, bytearray([0x7A])) # ply
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x014B, bytearray([0x3F, 0xA6, 0xA8, 0x0D])) # and.l $0DA8A6,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x014F, bytearray([0xF0, 0x02])) # beq .write
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0151, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0153, bytearray([0x98])) # .write tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0154, bytearray([0x87, 0x06])) # sta [$06]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0156, bytearray([0xA9, 0x01])) # lda #$01
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0158, bytearray([0x87, 0x00])) # sta [$00]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x015A, bytearray([0xA9, 0x12])) # lda #$12
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x015C, bytearray([0x87, 0x03])) # sta [$03]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x015E, bytearray([0x20, 0xC0, 0xA3])) # jsr update_flag_pointers
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0161, bytearray([0xAF, 0xAA, 0xBF, 0x03])) # handle_bonus_blocks: lda.l bonus_block_enabled_flag
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0165, bytearray([0xF0, 0x30])) # beq handle_checkpoints
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0167, bytearray([0xA6, 0x60])) # ldx $60
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0169, bytearray([0xA0, 0x1F])) # ldy.b #!icon_disabled
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x016B, bytearray([0xBF, 0x00, 0xF4, 0x0F])) # lda.l level_data,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x016F, bytearray([0x29, 0x20])) # and #$20
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0171, bytearray([0xF0, 0x24])) # beq handle_checkpoints
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0173, bytearray([0xA0, 0x5E])) # ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0175, bytearray([0x5A])) # phy
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0176, bytearray([0x20, 0x80, 0xA3])) # jsr get_translevel_bit
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0179, bytearray([0xDA])) # phx
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x017A, bytearray([0xBB])) # tyx
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x017B, bytearray([0xBF, 0x00, 0xA0, 0x7F])) # lda !bonus_block_flags,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x017F, bytearray([0xFA])) # plx
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0180, bytearray([0x7A])) # ply
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0181, bytearray([0x3F, 0xA6, 0xA8, 0x0D])) # and.l $0DA8A6,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0185, bytearray([0xF0, 0x02])) # beq .write
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0187, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0189, bytearray([0x98])) # .write tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x018A, bytearray([0x87, 0x06])) # sta [$06]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x018C, bytearray([0xA9, 0x01])) # lda #$01
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x018E, bytearray([0x87, 0x00])) # sta [$00]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0190, bytearray([0xA9, 0x4E])) # lda #$4E
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0192, bytearray([0x87, 0x03])) # sta [$03]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0194, bytearray([0x20, 0xC0, 0xA3])) # jsr update_flag_pointers
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0197, bytearray([0xAF, 0xA9, 0xBF, 0x03])) # handle_checkpoints: lda.l checkpoints_enabled_flag
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x019B, bytearray([0xF0, 0x2A])) # beq handle_moons
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x019D, bytearray([0xA6, 0x60])) # ldx $60
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x019F, bytearray([0xBF, 0x00, 0xF4, 0x0F])) # lda.l level_data,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01A3, bytearray([0x29, 0x10])) # and #$10
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01A5, bytearray([0xF0, 0x20])) # beq handle_moons
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01A7, bytearray([0xA0, 0x5E])) # ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01A9, bytearray([0x5A])) # phy
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01AA, bytearray([0x20, 0x80, 0xA3])) # jsr get_translevel_bit
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01AD, bytearray([0xB9, 0x3C, 0x1F])) # lda !checkpoints_flags,y
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01B0, bytearray([0x7A])) # ply
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01B1, bytearray([0x3F, 0xA6, 0xA8, 0x0D])) # and.l $0DA8A6,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01B5, bytearray([0xF0, 0x02])) # beq .write
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01B7, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01B9, bytearray([0x98])) # .write tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01BA, bytearray([0x87, 0x06])) # sta [$06]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01BC, bytearray([0xA9, 0x07])) # lda #$07
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01BE, bytearray([0x87, 0x00])) # sta [$00]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01C0, bytearray([0xA9, 0x48])) # lda #$48
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01C2, bytearray([0x87, 0x03])) # sta [$03]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01C4, bytearray([0x20, 0xC0, 0xA3])) # jsr update_flag_pointers
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01C7, bytearray([0xAF, 0xA8, 0xBF, 0x03])) # handle_moons: lda.l moon_enabled_flag
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01CB, bytearray([0xF0, 0x2A])) # beq handle_dragon_coins
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01CD, bytearray([0xA6, 0x60])) # ldx $60
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01CF, bytearray([0xBF, 0x00, 0xF4, 0x0F])) # lda.l level_data,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01D3, bytearray([0x29, 0x08])) # and #$08
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01D5, bytearray([0xF0, 0x20])) # beq handle_dragon_coins
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01D7, bytearray([0xA0, 0x5E])) # ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01D9, bytearray([0x5A])) # phy
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01DA, bytearray([0x20, 0x80, 0xA3])) # jsr get_translevel_bit
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01DD, bytearray([0xB9, 0xEE, 0x1F])) # lda !moons_flags,y
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01E0, bytearray([0x7A])) # ply
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01E1, bytearray([0x3F, 0xA6, 0xA8, 0x0D])) # and.l $0DA8A6,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01E5, bytearray([0xF0, 0x02])) # beq .write
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01E7, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01E9, bytearray([0x98])) # .write tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01EA, bytearray([0x87, 0x06])) # sta [$06]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01EC, bytearray([0xA9, 0x0C])) # lda #$0C
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01EE, bytearray([0x87, 0x00])) # sta [$00]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01F0, bytearray([0xA9, 0x4E])) # lda #$4E
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01F2, bytearray([0x87, 0x03])) # sta [$03]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01F4, bytearray([0x20, 0xC0, 0xA3])) # jsr update_flag_pointers
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01F7, bytearray([0xAF, 0xA6, 0xBF, 0x03])) # handle_dragon_coins: lda.l dragon_coin_enabled_flag
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01FB, bytearray([0xF0, 0x2A])) # beq handle_exit_2
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01FD, bytearray([0xA6, 0x60])) # ldx $60
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x01FF, bytearray([0xBF, 0x00, 0xF4, 0x0F])) # lda.l level_data,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0203, bytearray([0x29, 0x04])) # and #$04
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0205, bytearray([0xF0, 0x20])) # beq handle_exit_2
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0207, bytearray([0xA0, 0x5E])) # ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0209, bytearray([0x5A])) # phy
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x020A, bytearray([0x20, 0x80, 0xA3])) # jsr get_translevel_bit
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x020D, bytearray([0xB9, 0x2F, 0x1F])) # lda !yoshi_coins_flags,y
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0210, bytearray([0x7A])) # ply
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0211, bytearray([0x3F, 0xA6, 0xA8, 0x0D])) # and.l $0DA8A6,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0215, bytearray([0xF0, 0x02])) # beq .write
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0217, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0219, bytearray([0x98])) # .write tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x021A, bytearray([0x87, 0x06])) # sta [$06]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x021C, bytearray([0xA9, 0x03])) # lda #$03
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x021E, bytearray([0x87, 0x00])) # sta [$00]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0220, bytearray([0xA9, 0x02])) # lda #$02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0222, bytearray([0x87, 0x03])) # sta [$03]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0224, bytearray([0x20, 0xC0, 0xA3])) # jsr update_flag_pointers
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0227, bytearray([0xA6, 0x60])) # handle_exit_2: ldx $60
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0229, bytearray([0xBF, 0x00, 0xF4, 0x0F])) # lda.l level_data,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x022D, bytearray([0x29, 0x02])) # and #$02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x022F, bytearray([0xF0, 0x1A])) # beq handle_exit_1
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0231, bytearray([0xA0, 0x5E])) # ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0233, bytearray([0xBF, 0x00, 0xA2, 0x7F])) # lda !level_clears,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0237, bytearray([0x29, 0x02])) # and #$02
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0239, bytearray([0xF0, 0x02])) # beq .write
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x023B, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x023D, bytearray([0x98])) # .write tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x023E, bytearray([0x87, 0x06])) # sta [$06]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0240, bytearray([0xA9, 0x04])) # lda #$04
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0242, bytearray([0x87, 0x00])) # sta [$00]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0244, bytearray([0xA9, 0x24])) # lda #$24
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0246, bytearray([0x87, 0x03])) # sta [$03]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0248, bytearray([0x20, 0xC0, 0xA3])) # jsr update_flag_pointers
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x024B, bytearray([0xA6, 0x60])) # handle_exit_1: ldx $60
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x024D, bytearray([0xBF, 0x00, 0xF4, 0x0F])) # lda.l level_data,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0251, bytearray([0x29, 0x01])) # and #$01
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0253, bytearray([0xF0, 0x1A])) # beq .dont_draw
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0255, bytearray([0xA0, 0x5E])) # ldy.b #!icon_not_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0257, bytearray([0xBF, 0x00, 0xA2, 0x7F])) # lda !level_clears,x
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x025B, bytearray([0x29, 0x01])) # and #$01
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x025D, bytearray([0xF0, 0x02])) # beq .write
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x025F, bytearray([0xA0, 0x3F])) # ldy.b #!icon_obtained
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0261, bytearray([0x98])) # .write tya
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0262, bytearray([0x87, 0x06])) # sta [$06]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0264, bytearray([0xA9, 0x04])) # lda #$04
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0266, bytearray([0x87, 0x00])) # sta [$00]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x0268, bytearray([0xA9, 0x23])) # lda #$23
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x026A, bytearray([0x87, 0x03])) # sta [$03]
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x026C, bytearray([0x20, 0xC0, 0xA3])) # jsr update_flag_pointers
+ rom.write_bytes(BUILD_TILEMAP_ADDR + 0x026F, bytearray([0x6B])) # .dont_draw rtl
+
+ LEVEL_INDICATOR_DATA_ADDR = 0x7F400
+ rom.write_bytes(LEVEL_INDICATOR_DATA_ADDR + 0x0000, bytearray([0x80,0x45,0x45,0x80,0x43,0x65,0x5D,0x51]))
+ rom.write_bytes(LEVEL_INDICATOR_DATA_ADDR + 0x0008, bytearray([0x01,0x47,0x47,0x51,0x65,0x45,0x41,0x4F]))
+ rom.write_bytes(LEVEL_INDICATOR_DATA_ADDR + 0x0010, bytearray([0x55,0x45,0x80,0x43,0x01,0x57,0x80,0x80]))
+ rom.write_bytes(LEVEL_INDICATOR_DATA_ADDR + 0x0018, bytearray([0x45,0x80,0x51,0x41,0x45,0x45,0x80,0x41]))
+ rom.write_bytes(LEVEL_INDICATOR_DATA_ADDR + 0x0020, bytearray([0x45,0x41,0x4D,0x67,0x57,0x41,0x55,0x65]))
+ rom.write_bytes(LEVEL_INDICATOR_DATA_ADDR + 0x0028, bytearray([0x80,0x4D,0x45,0x55,0x80,0x47,0x4D,0x45]))
+ rom.write_bytes(LEVEL_INDICATOR_DATA_ADDR + 0x0030, bytearray([0x80,0x80,0x80,0x43,0x55,0x41,0x80,0x45]))
+ rom.write_bytes(LEVEL_INDICATOR_DATA_ADDR + 0x0038, bytearray([0x47,0x57,0x4D,0x41,0x47,0x55,0x47,0x01]))
+ rom.write_bytes(LEVEL_INDICATOR_DATA_ADDR + 0x0040, bytearray([0x41,0x4F,0x43,0x47,0x47,0x01,0x45,0x57]))
+ rom.write_bytes(LEVEL_INDICATOR_DATA_ADDR + 0x0048, bytearray([0x80,0x45,0x45,0x45,0x45,0x80,0x55,0x45]))
+ rom.write_bytes(LEVEL_INDICATOR_DATA_ADDR + 0x0050, bytearray([0x45,0x45,0x80,0x80,0x43,0x80,0x43,0x80]))
+ rom.write_bytes(LEVEL_INDICATOR_DATA_ADDR + 0x0058, bytearray([0x07,0x43,0x43,0x80,0x80,0x80,0x80,0x80]))
+
+
+def handle_indicators(rom):
+ INDICATOR_QUEUE_CODE = 0x86000
+ rom.write_bytes(0x022E6, bytearray([0x22, 0x00, 0xE0, 0x10])) # org $00A2E6 : jsl gm14_hijack
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0000, bytearray([0xAD, 0x00, 0x01])) # gm14_hijack: lda $0100
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0003, bytearray([0xC9, 0x14])) # cmp #$14
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0005, bytearray([0xD0, 0x04])) # bne .invalid
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0007, bytearray([0xA5, 0x71])) # lda $71
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0009, bytearray([0xF0, 0x04])) # beq .valid
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x000B, bytearray([0x5C, 0xB1, 0x8A, 0x02])) # .invalid jml $028AB1
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x000F, bytearray([0xC2, 0x30])) # .valid rep #$30
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0011, bytearray([0xAF, 0x04, 0xB0, 0x7F])) # lda !score_sprite_add_1_coin
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0015, bytearray([0xF0, 0x03])) # beq .no_1_coin
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0017, bytearray([0x20, 0xC1, 0xE0])) # jsr add_1_coin
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x001A, bytearray([0xAF, 0x06, 0xB0, 0x7F])) # .no_1_coin lda !score_sprite_add_5_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x001E, bytearray([0xF0, 0x03])) # beq .no_5_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0020, bytearray([0x20, 0xDF, 0xE0])) # jsr add_5_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0023, bytearray([0xAF, 0x08, 0xB0, 0x7F])) # .no_5_coins lda !score_sprite_add_10_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0027, bytearray([0xF0, 0x03])) # beq .no_10_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0029, bytearray([0x20, 0xFD, 0xE0])) # jsr add_10_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x002C, bytearray([0xAF, 0x0A, 0xB0, 0x7F])) # .no_10_coins lda !score_sprite_add_15_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0030, bytearray([0xF0, 0x03])) # beq .no_15_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0032, bytearray([0x20, 0x1B, 0xE1])) # jsr add_15_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0035, bytearray([0xAF, 0x10, 0xB0, 0x7F])) # .no_15_coins lda !score_sprite_add_1up
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0039, bytearray([0xF0, 0x03])) # beq .no_1up
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x003B, bytearray([0x20, 0x39, 0xE1])) # jsr add_1up
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x003E, bytearray([0xAF, 0x0C, 0xB0, 0x7F])) # .no_1up lda !score_sprite_add_yoshi_egg
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0042, bytearray([0xF0, 0x03])) # beq .no_yoshi_egg
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0044, bytearray([0x20, 0x57, 0xE1])) # jsr add_yoshi_egg
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0047, bytearray([0xAF, 0x0E, 0xB0, 0x7F])) # .no_yoshi_egg lda !score_sprite_add_boss_token
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x004B, bytearray([0xF0, 0x03])) # beq .no_boss_token
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x004D, bytearray([0x20, 0xCF, 0xE1])) # jsr add_boss_token
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0050, bytearray([0xE2, 0x30])) # .no_boss_token sep #$30
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0052, bytearray([0x20, 0xED, 0xE1])) # jsr goal_sanity_check
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0055, bytearray([0x20, 0x5C, 0xE0])) # jsr score_sprite_queue
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0058, bytearray([0x5C, 0xB1, 0x8A, 0x02])) # jml $028AB1
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x005C, bytearray([0xAF, 0x20, 0xB0, 0x7F])) # score_sprite_queue: lda !score_sprite_queue_delay
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0060, bytearray([0xF0, 0x06])) # beq .spawn
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0062, bytearray([0x3A])) # dec
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0063, bytearray([0x8F, 0x20, 0xB0, 0x7F])) # sta !score_sprite_queue_delay
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0067, bytearray([0x60])) # rts
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0068, bytearray([0xA9, 0x08])) # .spawn lda #$08
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x006A, bytearray([0x8F, 0x20, 0xB0, 0x7F])) # sta !score_sprite_queue_delay
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x006E, bytearray([0xC2, 0x30])) # rep #$30
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0070, bytearray([0xAF, 0x02, 0xB0, 0x7F])) # lda !score_sprite_index
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0074, bytearray([0xCF, 0x00, 0xB0, 0x7F])) # cmp !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0078, bytearray([0xD0, 0x03])) # bne .check_slots
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x007A, bytearray([0xE2, 0x30])) # sep #$30
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x007C, bytearray([0x60])) # rts
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x007D, bytearray([0xA0, 0x05, 0x00])) # .check_slots ldy #$0005
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0080, bytearray([0xB9, 0xE1, 0x16])) # ..loop lda !score_sprite_num,y
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0083, bytearray([0x29, 0xFF, 0x00])) # and #$00FF
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0086, bytearray([0xF0, 0x06])) # beq .found_free
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0088, bytearray([0x88])) # dey
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0089, bytearray([0x10, 0xF5])) # bpl ..loop
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x008B, bytearray([0xE2, 0x30])) # sep #$30
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x008D, bytearray([0x60])) # rts
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x008E, bytearray([0xAF, 0x02, 0xB0, 0x7F])) # .found_free lda !score_sprite_index
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0092, bytearray([0x1A])) # inc
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0093, bytearray([0xAA])) # tax
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0094, bytearray([0x8F, 0x02, 0xB0, 0x7F])) # sta !score_sprite_index
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0098, bytearray([0xBF, 0x22, 0xB0, 0x7F])) # lda !score_sprite_queue,x
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x009C, bytearray([0xE2, 0x30])) # sep #$30
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x009E, bytearray([0x99, 0xE1, 0x16])) # sta !score_sprite_num,y
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00A1, bytearray([0xA5, 0x94])) # lda $94
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00A3, bytearray([0x99, 0xED, 0x16])) # sta !score_sprite_x_lo,y
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00A6, bytearray([0xA5, 0x95])) # lda $95
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00A8, bytearray([0x99, 0xF3, 0x16])) # sta !score_sprite_x_hi,y
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00AB, bytearray([0xA5, 0x96])) # lda $96
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00AD, bytearray([0x99, 0xE7, 0x16])) # sta !score_sprite_y_lo,y
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00B0, bytearray([0xA5, 0x97])) # lda $97
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00B2, bytearray([0x99, 0xF9, 0x16])) # sta !score_sprite_y_hi,y
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00B5, bytearray([0xA9, 0x30])) # lda #$30
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00B7, bytearray([0x99, 0xFF, 0x16])) # sta !score_sprite_timer,y
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00BA, bytearray([0xAD, 0xF9, 0x13])) # lda $13F9
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00BD, bytearray([0x99, 0x05, 0x17])) # sta !score_sprite_layer,y
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00C0, bytearray([0x60])) # rts
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00C1, bytearray([0xAF, 0x04, 0xB0, 0x7F])) # add_1_coin: lda !score_sprite_add_1_coin
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00C5, bytearray([0x3A])) # dec
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00C6, bytearray([0x8F, 0x04, 0xB0, 0x7F])) # sta !score_sprite_add_1_coin
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00CA, bytearray([0xAF, 0x00, 0xB0, 0x7F])) # lda !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00CE, bytearray([0x1A])) # inc
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00CF, bytearray([0x8F, 0x00, 0xB0, 0x7F])) # sta !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00D3, bytearray([0xAA])) # tax
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00D4, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00D6, bytearray([0xA9, 0x11])) # lda #$11
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00D8, bytearray([0x9F, 0x22, 0xB0, 0x7F])) # sta !score_sprite_queue,x
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00DC, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00DE, bytearray([0x60])) # rts
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00DF, bytearray([0xAF, 0x06, 0xB0, 0x7F])) # add_5_coins: lda !score_sprite_add_5_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00E3, bytearray([0x3A])) # dec
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00E4, bytearray([0x8F, 0x06, 0xB0, 0x7F])) # sta !score_sprite_add_5_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00E8, bytearray([0xAF, 0x00, 0xB0, 0x7F])) # lda !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00EC, bytearray([0x1A])) # inc
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00ED, bytearray([0x8F, 0x00, 0xB0, 0x7F])) # sta !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00F1, bytearray([0xAA])) # tax
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00F2, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00F4, bytearray([0xA9, 0x12])) # lda #$12
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00F6, bytearray([0x9F, 0x22, 0xB0, 0x7F])) # sta !score_sprite_queue,x
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00FA, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00FC, bytearray([0x60])) # rts
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x00FD, bytearray([0xAF, 0x08, 0xB0, 0x7F])) # add_10_coins: lda !score_sprite_add_10_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0101, bytearray([0x3A])) # dec
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0102, bytearray([0x8F, 0x08, 0xB0, 0x7F])) # sta !score_sprite_add_10_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0106, bytearray([0xAF, 0x00, 0xB0, 0x7F])) # lda !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x010A, bytearray([0x1A])) # inc
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x010B, bytearray([0x8F, 0x00, 0xB0, 0x7F])) # sta !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x010F, bytearray([0xAA])) # tax
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0110, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0112, bytearray([0xA9, 0x13])) # lda #$13
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0114, bytearray([0x9F, 0x22, 0xB0, 0x7F])) # sta !score_sprite_queue,x
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0118, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x011A, bytearray([0x60])) # rts
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x011B, bytearray([0xAF, 0x0A, 0xB0, 0x7F])) # add_15_coins: lda !score_sprite_add_15_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x011F, bytearray([0x3A])) # dec
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0120, bytearray([0x8F, 0x0A, 0xB0, 0x7F])) # sta !score_sprite_add_15_coins
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0124, bytearray([0xAF, 0x00, 0xB0, 0x7F])) # lda !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0128, bytearray([0x1A])) # inc
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0129, bytearray([0x8F, 0x00, 0xB0, 0x7F])) # sta !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x012D, bytearray([0xAA])) # tax
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x012E, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0130, bytearray([0xA9, 0x14])) # lda #$14
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0132, bytearray([0x9F, 0x22, 0xB0, 0x7F])) # sta !score_sprite_queue,x
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0136, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0138, bytearray([0x60])) # rts
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0139, bytearray([0xAF, 0x10, 0xB0, 0x7F])) # add_1up: lda !score_sprite_add_1up
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x013D, bytearray([0x3A])) # dec
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x013E, bytearray([0x8F, 0x10, 0xB0, 0x7F])) # sta !score_sprite_add_1up
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0142, bytearray([0xAF, 0x00, 0xB0, 0x7F])) # lda !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0146, bytearray([0x1A])) # inc
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0147, bytearray([0x8F, 0x00, 0xB0, 0x7F])) # sta !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x014B, bytearray([0xAA])) # tax
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x014C, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x014E, bytearray([0xA9, 0x16])) # lda #$16
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0150, bytearray([0x9F, 0x22, 0xB0, 0x7F])) # sta !score_sprite_queue,x
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0154, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0156, bytearray([0x60])) # rts
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0157, bytearray([0xAF, 0x0C, 0xB0, 0x7F])) # add_yoshi_egg: lda !score_sprite_add_yoshi_egg
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x015B, bytearray([0x3A])) # dec
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x015C, bytearray([0x8F, 0x0C, 0xB0, 0x7F])) # sta !score_sprite_add_yoshi_egg
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0160, bytearray([0xAF, 0x00, 0xB0, 0x7F])) # lda !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0164, bytearray([0x1A])) # inc
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0165, bytearray([0x8F, 0x00, 0xB0, 0x7F])) # sta !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0169, bytearray([0xAA])) # tax
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x016A, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x016C, bytearray([0xA9, 0x15])) # lda #$15
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x016E, bytearray([0x9F, 0x22, 0xB0, 0x7F])) # sta !score_sprite_queue,x
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0172, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0174, bytearray([0x60])) # rts
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0175, bytearray([0xAF, 0x12, 0xB0, 0x7F])) # add_mushroom: lda !score_sprite_add_mushroom
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0179, bytearray([0x3A])) # dec
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x017A, bytearray([0x8F, 0x12, 0xB0, 0x7F])) # sta !score_sprite_add_mushroom
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x017E, bytearray([0xAF, 0x00, 0xB0, 0x7F])) # lda !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0182, bytearray([0x1A])) # inc
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0183, bytearray([0x8F, 0x00, 0xB0, 0x7F])) # sta !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0187, bytearray([0xAA])) # tax
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0188, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x018A, bytearray([0xA9, 0x17])) # lda #$17
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x018C, bytearray([0x9F, 0x22, 0xB0, 0x7F])) # sta !score_sprite_queue,x
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0190, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0192, bytearray([0x60])) # rts
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0193, bytearray([0xAF, 0x14, 0xB0, 0x7F])) # add_flower: lda !score_sprite_add_flower
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0197, bytearray([0x3A])) # dec
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0198, bytearray([0x8F, 0x14, 0xB0, 0x7F])) # sta !score_sprite_add_flower
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x019C, bytearray([0xAF, 0x00, 0xB0, 0x7F])) # lda !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01A0, bytearray([0x1A])) # inc
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01A1, bytearray([0x8F, 0x00, 0xB0, 0x7F])) # sta !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01A5, bytearray([0xAA])) # tax
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01A6, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01A8, bytearray([0xA9, 0x18])) # lda #$18
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01AA, bytearray([0x9F, 0x22, 0xB0, 0x7F])) # sta !score_sprite_queue,x
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01AE, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01B0, bytearray([0x60])) # rts
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01B1, bytearray([0xAF, 0x16, 0xB0, 0x7F])) # add_feather: lda !score_sprite_add_feather
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01B5, bytearray([0x3A])) # dec
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01B6, bytearray([0x8F, 0x16, 0xB0, 0x7F])) # sta !score_sprite_add_feather
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01BA, bytearray([0xAF, 0x00, 0xB0, 0x7F])) # lda !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01BE, bytearray([0x1A])) # inc
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01BF, bytearray([0x8F, 0x00, 0xB0, 0x7F])) # sta !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01C3, bytearray([0xAA])) # tax
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01C4, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01C6, bytearray([0xA9, 0x19])) # lda #$19
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01C8, bytearray([0x9F, 0x22, 0xB0, 0x7F])) # sta !score_sprite_queue,x
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01CC, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01CE, bytearray([0x60])) # rts
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01CF, bytearray([0xAF, 0x0E, 0xB0, 0x7F])) # add_boss_token: lda !score_sprite_add_boss_token
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01D3, bytearray([0x3A])) # dec
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01D4, bytearray([0x8F, 0x0E, 0xB0, 0x7F])) # sta !score_sprite_add_boss_token
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01D8, bytearray([0xAF, 0x00, 0xB0, 0x7F])) # lda !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01DC, bytearray([0x1A])) # inc
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01DD, bytearray([0x8F, 0x00, 0xB0, 0x7F])) # sta !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01E1, bytearray([0xAA])) # tax
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01E2, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01E4, bytearray([0xA9, 0x1A])) # lda #$1A
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01E6, bytearray([0x9F, 0x22, 0xB0, 0x7F])) # sta !score_sprite_queue,x
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01EA, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01EC, bytearray([0x60])) # rts
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01ED, bytearray([0xAF, 0xA0, 0xBF, 0x03])) # goal_sanity_check: lda $03BFA0
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01F1, bytearray([0x29, 0x01])) # and #$01
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01F3, bytearray([0x49, 0x01])) # eor #$01
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01F5, bytearray([0x0A])) # asl
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01F6, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01F8, bytearray([0xBF, 0x0C, 0xB0, 0x7F])) # lda !score_sprite_add_yoshi_egg,x
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01FC, bytearray([0xD0, 0x18])) # bne .return
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x01FE, bytearray([0xAF, 0x02, 0xB0, 0x7F])) # .check_queue lda !score_sprite_index
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0202, bytearray([0xCF, 0x00, 0xB0, 0x7F])) # cmp !score_sprite_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0206, bytearray([0xD0, 0x0E])) # bne .return
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0208, bytearray([0xE2, 0x20])) # .check_count sep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x020A, bytearray([0xAF, 0x1E, 0xA0, 0x7F])) # lda !goal_item_count
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x020E, bytearray([0xDD, 0x24, 0x1F])) # cmp $1F24,x
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0211, bytearray([0xF0, 0x03])) # beq .return
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0213, bytearray([0x9D, 0x24, 0x1F])) # sta $1F24,x
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0216, bytearray([0xE2, 0x20])) # .return sep #$20
+ rom.write_bytes(INDICATOR_QUEUE_CODE + 0x0218, bytearray([0x60])) # rts
+
+ # Add code for indicators when receiving items during levels
+ INDICATOR_CODE = 0x84000
+ rom.write_bytes(0x12DBA, bytearray([0x5C, 0x00, 0xC0, 0x10])) # org $02ADBA : jsl score_sprites
+ rom.write_bytes(INDICATOR_CODE + 0x0000, bytearray([0xBD, 0xE1, 0x16])) # score_sprites: lda !score_sprite_num,x
+ rom.write_bytes(INDICATOR_CODE + 0x0003, bytearray([0xF0, 0x2D])) # beq .return
+ rom.write_bytes(INDICATOR_CODE + 0x0005, bytearray([0x8E, 0xE9, 0x15])) # stx $15E9
+ rom.write_bytes(INDICATOR_CODE + 0x0008, bytearray([0xC2, 0x30])) # rep #$30
+ rom.write_bytes(INDICATOR_CODE + 0x000A, bytearray([0x29, 0x1F, 0x00])) # and #$001F
+ rom.write_bytes(INDICATOR_CODE + 0x000D, bytearray([0x85, 0x00])) # sta $00
+ rom.write_bytes(INDICATOR_CODE + 0x000F, bytearray([0x0A])) # asl
+ rom.write_bytes(INDICATOR_CODE + 0x0010, bytearray([0x18])) # clc
+ rom.write_bytes(INDICATOR_CODE + 0x0011, bytearray([0x65, 0x00])) # adc $00
+ rom.write_bytes(INDICATOR_CODE + 0x0013, bytearray([0xAA])) # tax
+ rom.write_bytes(INDICATOR_CODE + 0x0014, bytearray([0xBF, 0x37, 0xC0, 0x10])) # lda.l .pointers-3,x
+ rom.write_bytes(INDICATOR_CODE + 0x0018, bytearray([0x85, 0x00])) # sta $00
+ rom.write_bytes(INDICATOR_CODE + 0x001A, bytearray([0xE2, 0x30])) # sep #$30
+ rom.write_bytes(INDICATOR_CODE + 0x001C, bytearray([0xBF, 0x39, 0xC0, 0x10])) # lda.l .pointers-3+2,x
+ rom.write_bytes(INDICATOR_CODE + 0x0020, bytearray([0x85, 0x02])) # sta $02
+ rom.write_bytes(INDICATOR_CODE + 0x0022, bytearray([0xE2, 0x10])) # sep #$10
+ rom.write_bytes(INDICATOR_CODE + 0x0024, bytearray([0xAE, 0xE9, 0x15])) # ldx $15E9
+ rom.write_bytes(INDICATOR_CODE + 0x0027, bytearray([0x8B])) # phb
+ rom.write_bytes(INDICATOR_CODE + 0x0028, bytearray([0x48])) # pha
+ rom.write_bytes(INDICATOR_CODE + 0x0029, bytearray([0xAB])) # plb
+ rom.write_bytes(INDICATOR_CODE + 0x002A, bytearray([0x4B])) # phk
+ rom.write_bytes(INDICATOR_CODE + 0x002B, bytearray([0xF4, 0x30, 0xC0])) # pea.w .return_code-1
+ rom.write_bytes(INDICATOR_CODE + 0x002E, bytearray([0xDC, 0x00, 0x00])) # jml [$0000]
+ rom.write_bytes(INDICATOR_CODE + 0x0031, bytearray([0xAB])) # .return_code plb
+ rom.write_bytes(INDICATOR_CODE + 0x0032, bytearray([0x5C, 0xC5, 0xAD, 0x02])) # .return jml $02ADC5
+ rom.write_bytes(INDICATOR_CODE + 0x0036, bytearray([0x9E, 0xE1, 0x16])) # .kill stz !score_sprite_num,x
+ rom.write_bytes(INDICATOR_CODE + 0x0039, bytearray([0x6B])) # rtl
+ rom.write_bytes(INDICATOR_CODE + 0x003A, bytearray([0x97, 0xC0, 0x10])) # .pointers dl original_score_sprites ; 01 - 10 points
+ rom.write_bytes(INDICATOR_CODE + 0x003D, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 02 - 20 points
+ rom.write_bytes(INDICATOR_CODE + 0x0040, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 03 - 40 points
+ rom.write_bytes(INDICATOR_CODE + 0x0043, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 04 - 80 points
+ rom.write_bytes(INDICATOR_CODE + 0x0046, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 05 - 100 points
+ rom.write_bytes(INDICATOR_CODE + 0x0049, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 06 - 200 points
+ rom.write_bytes(INDICATOR_CODE + 0x004C, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 07 - 400 points
+ rom.write_bytes(INDICATOR_CODE + 0x004F, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 08 - 800 points
+ rom.write_bytes(INDICATOR_CODE + 0x0052, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 09 - 1000 points
+ rom.write_bytes(INDICATOR_CODE + 0x0055, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 0A - 2000 points
+ rom.write_bytes(INDICATOR_CODE + 0x0058, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 0B - 4000 points
+ rom.write_bytes(INDICATOR_CODE + 0x005B, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 0C - 8000 points
+ rom.write_bytes(INDICATOR_CODE + 0x005E, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 0D - 1-up
+ rom.write_bytes(INDICATOR_CODE + 0x0061, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 0E - 2-up
+ rom.write_bytes(INDICATOR_CODE + 0x0064, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 0F - 3-up
+ rom.write_bytes(INDICATOR_CODE + 0x0067, bytearray([0x97, 0xC0, 0x10])) # dl original_score_sprites ; 10 - 5-up
+ rom.write_bytes(INDICATOR_CODE + 0x006A, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 11 - 1 coin
+ rom.write_bytes(INDICATOR_CODE + 0x006D, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 12 - 5 coins
+ rom.write_bytes(INDICATOR_CODE + 0x0070, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 13 - 10 coins
+ rom.write_bytes(INDICATOR_CODE + 0x0073, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 14 - 15 coins
+ rom.write_bytes(INDICATOR_CODE + 0x0076, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 15 - Yoshi Egg
+ rom.write_bytes(INDICATOR_CODE + 0x0079, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 16 - 1up Mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x007C, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 17 - Mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x007F, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 18 - Flower
+ rom.write_bytes(INDICATOR_CODE + 0x0082, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 19 - Feather
+ rom.write_bytes(INDICATOR_CODE + 0x0085, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 1A - Boss token
+ rom.write_bytes(INDICATOR_CODE + 0x0088, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 1B -
+ rom.write_bytes(INDICATOR_CODE + 0x008B, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 1C -
+ rom.write_bytes(INDICATOR_CODE + 0x008E, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 1D -
+ rom.write_bytes(INDICATOR_CODE + 0x0091, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 1E -
+ rom.write_bytes(INDICATOR_CODE + 0x0094, bytearray([0xA7, 0xC0, 0x10])) # dl icon_score ; 1F -
+ rom.write_bytes(INDICATOR_CODE + 0x0097, bytearray([0xA9, 0x02])) # original_score_sprites: lda #$02
+ rom.write_bytes(INDICATOR_CODE + 0x0099, bytearray([0x48])) # pha
+ rom.write_bytes(INDICATOR_CODE + 0x009A, bytearray([0xAB])) # plb
+ rom.write_bytes(INDICATOR_CODE + 0x009B, bytearray([0x4B])) # phk
+ rom.write_bytes(INDICATOR_CODE + 0x009C, bytearray([0xF4, 0xA5, 0xC0])) # pea.w .jslrtsreturn-1
+ rom.write_bytes(INDICATOR_CODE + 0x009F, bytearray([0xF4, 0x88, 0xB8])) # pea.w $B889-1
+ rom.write_bytes(INDICATOR_CODE + 0x00A2, bytearray([0x5C, 0xC9, 0xAD, 0x02])) # jml $02ADC9
+ rom.write_bytes(INDICATOR_CODE + 0x00A6, bytearray([0x6B])) # .jslrtsreturn rtl
+ rom.write_bytes(INDICATOR_CODE + 0x00A7, bytearray([0xBD, 0xFF, 0x16])) # icon_score: lda !score_sprite_timer,x
+ rom.write_bytes(INDICATOR_CODE + 0x00AA, bytearray([0xD0, 0x04])) # bne .active
+ rom.write_bytes(INDICATOR_CODE + 0x00AC, bytearray([0x9E, 0xE1, 0x16])) # stz !score_sprite_num,x
+ rom.write_bytes(INDICATOR_CODE + 0x00AF, bytearray([0x6B])) # rtl
+ rom.write_bytes(INDICATOR_CODE + 0x00B0, bytearray([0xDE, 0xFF, 0x16])) # .active dec !score_sprite_timer,x
+ rom.write_bytes(INDICATOR_CODE + 0x00B3, bytearray([0xC9, 0x30])) # cmp #$30
+ rom.write_bytes(INDICATOR_CODE + 0x00B5, bytearray([0xD0, 0x14])) # bne .handle_movement
+ rom.write_bytes(INDICATOR_CODE + 0x00B7, bytearray([0xBD, 0xE1, 0x16])) # lda !score_sprite_num,x
+ rom.write_bytes(INDICATOR_CODE + 0x00BA, bytearray([0x38])) # sec
+ rom.write_bytes(INDICATOR_CODE + 0x00BB, bytearray([0xE9, 0x11])) # sbc #$11
+ rom.write_bytes(INDICATOR_CODE + 0x00BD, bytearray([0x0A])) # asl
+ rom.write_bytes(INDICATOR_CODE + 0x00BE, bytearray([0xA8])) # tay
+ rom.write_bytes(INDICATOR_CODE + 0x00BF, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(INDICATOR_CODE + 0x00C1, bytearray([0xB9, 0x4B, 0xC2])) # lda .reward_ptrs,y
+ rom.write_bytes(INDICATOR_CODE + 0x00C4, bytearray([0x85, 0x00])) # sta $00
+ rom.write_bytes(INDICATOR_CODE + 0x00C6, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(INDICATOR_CODE + 0x00C8, bytearray([0x6C, 0x00, 0x00])) # jmp ($0000)
+ rom.write_bytes(INDICATOR_CODE + 0x00CB, bytearray([0xBD, 0xFF, 0x16])) # .handle_movement lda !score_sprite_timer,x
+ rom.write_bytes(INDICATOR_CODE + 0x00CE, bytearray([0x4A])) # lsr
+ rom.write_bytes(INDICATOR_CODE + 0x00CF, bytearray([0x4A])) # lsr
+ rom.write_bytes(INDICATOR_CODE + 0x00D0, bytearray([0x4A])) # lsr
+ rom.write_bytes(INDICATOR_CODE + 0x00D1, bytearray([0x4A])) # lsr
+ rom.write_bytes(INDICATOR_CODE + 0x00D2, bytearray([0xA8])) # tay
+ rom.write_bytes(INDICATOR_CODE + 0x00D3, bytearray([0xA5, 0x13])) # lda $13
+ rom.write_bytes(INDICATOR_CODE + 0x00D5, bytearray([0x39, 0xF0, 0xC0])) # and .speed,y
+ rom.write_bytes(INDICATOR_CODE + 0x00D8, bytearray([0xD0, 0x14])) # bne ..skip_update
+ rom.write_bytes(INDICATOR_CODE + 0x00DA, bytearray([0xBD, 0xE7, 0x16])) # lda !score_sprite_y_lo,x
+ rom.write_bytes(INDICATOR_CODE + 0x00DD, bytearray([0xA8])) # tay
+ rom.write_bytes(INDICATOR_CODE + 0x00DE, bytearray([0x38])) # sec
+ rom.write_bytes(INDICATOR_CODE + 0x00DF, bytearray([0xE5, 0x1C])) # sbc $1C
+ rom.write_bytes(INDICATOR_CODE + 0x00E1, bytearray([0xC9, 0x04])) # cmp #$04
+ rom.write_bytes(INDICATOR_CODE + 0x00E3, bytearray([0x90, 0x09])) # bcc ..skip_update
+ rom.write_bytes(INDICATOR_CODE + 0x00E5, bytearray([0xDE, 0xE7, 0x16])) # dec !score_sprite_y_lo,x
+ rom.write_bytes(INDICATOR_CODE + 0x00E8, bytearray([0x98])) # tya
+ rom.write_bytes(INDICATOR_CODE + 0x00E9, bytearray([0xD0, 0x03])) # bne ..skip_update
+ rom.write_bytes(INDICATOR_CODE + 0x00EB, bytearray([0xDE, 0xF9, 0x16])) # dec !score_sprite_y_hi,x
+ rom.write_bytes(INDICATOR_CODE + 0x00EE, bytearray([0x80, 0x05])) # ..skip_update bra .gfx
+ rom.write_bytes(INDICATOR_CODE + 0x00F0, bytearray([0x03, 0x01, 0x00, 0x00])) # .speed db $03,$01,$00,$00
+ rom.write_bytes(INDICATOR_CODE + 0x00F4, bytearray([0x6B])) # .return rtl
+ rom.write_bytes(INDICATOR_CODE + 0x00F5, bytearray([0xBD, 0x05, 0x17])) # .gfx lda !score_sprite_layer,x
+ rom.write_bytes(INDICATOR_CODE + 0x00F8, bytearray([0x0A])) # asl
+ rom.write_bytes(INDICATOR_CODE + 0x00F9, bytearray([0x0A])) # asl
+ rom.write_bytes(INDICATOR_CODE + 0x00FA, bytearray([0xA8])) # tay
+ rom.write_bytes(INDICATOR_CODE + 0x00FB, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(INDICATOR_CODE + 0x00FD, bytearray([0xB9, 0x1C, 0x00])) # lda $001C,y
+ rom.write_bytes(INDICATOR_CODE + 0x0100, bytearray([0x85, 0x02])) # sta $02
+ rom.write_bytes(INDICATOR_CODE + 0x0102, bytearray([0xB9, 0x1A, 0x00])) # lda $001A,y
+ rom.write_bytes(INDICATOR_CODE + 0x0105, bytearray([0x85, 0x04])) # sta $04
+ rom.write_bytes(INDICATOR_CODE + 0x0107, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(INDICATOR_CODE + 0x0109, bytearray([0xBD, 0xF3, 0x16])) # lda !score_sprite_x_hi,x
+ rom.write_bytes(INDICATOR_CODE + 0x010C, bytearray([0xEB])) # xba
+ rom.write_bytes(INDICATOR_CODE + 0x010D, bytearray([0xBD, 0xED, 0x16])) # lda !score_sprite_x_lo,x
+ rom.write_bytes(INDICATOR_CODE + 0x0110, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(INDICATOR_CODE + 0x0112, bytearray([0x38])) # sec
+ rom.write_bytes(INDICATOR_CODE + 0x0113, bytearray([0xE5, 0x04])) # sbc $04
+ rom.write_bytes(INDICATOR_CODE + 0x0115, bytearray([0x38])) # sec
+ rom.write_bytes(INDICATOR_CODE + 0x0116, bytearray([0xE9, 0x06, 0x00])) # sbc #$0006
+ rom.write_bytes(INDICATOR_CODE + 0x0119, bytearray([0xC9, 0xEA, 0x00])) # cmp #$00EA
+ rom.write_bytes(INDICATOR_CODE + 0x011C, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(INDICATOR_CODE + 0x011E, bytearray([0xB0, 0xD4])) # bcs .return
+ rom.write_bytes(INDICATOR_CODE + 0x0120, bytearray([0xBD, 0xE7, 0x16])) # lda !score_sprite_y_lo,x
+ rom.write_bytes(INDICATOR_CODE + 0x0123, bytearray([0xC5, 0x02])) # cmp $02
+ rom.write_bytes(INDICATOR_CODE + 0x0125, bytearray([0xBD, 0xF9, 0x16])) # lda !score_sprite_y_hi,x
+ rom.write_bytes(INDICATOR_CODE + 0x0128, bytearray([0xE5, 0x03])) # sbc $03
+ rom.write_bytes(INDICATOR_CODE + 0x012A, bytearray([0xD0, 0xC8])) # bne .return
+ rom.write_bytes(INDICATOR_CODE + 0x012C, bytearray([0xBF, 0x9E, 0xAD, 0x02])) # lda $02AD9E,x
+ rom.write_bytes(INDICATOR_CODE + 0x0130, bytearray([0xA8])) # tay
+ rom.write_bytes(INDICATOR_CODE + 0x0131, bytearray([0xBD, 0xE7, 0x16])) # lda !score_sprite_y_lo,x
+ rom.write_bytes(INDICATOR_CODE + 0x0134, bytearray([0x38])) # sec
+ rom.write_bytes(INDICATOR_CODE + 0x0135, bytearray([0xE5, 0x02])) # sbc $02
+ rom.write_bytes(INDICATOR_CODE + 0x0137, bytearray([0x99, 0x01, 0x02])) # sta $0201,y
+ rom.write_bytes(INDICATOR_CODE + 0x013A, bytearray([0x99, 0x05, 0x02])) # sta $0205,y
+ rom.write_bytes(INDICATOR_CODE + 0x013D, bytearray([0xBD, 0xED, 0x16])) # lda !score_sprite_x_lo,x
+ rom.write_bytes(INDICATOR_CODE + 0x0140, bytearray([0x38])) # sec
+ rom.write_bytes(INDICATOR_CODE + 0x0141, bytearray([0xE5, 0x04])) # sbc $04
+ rom.write_bytes(INDICATOR_CODE + 0x0143, bytearray([0x18])) # clc
+ rom.write_bytes(INDICATOR_CODE + 0x0144, bytearray([0x69, 0x09])) # adc #$09
+ rom.write_bytes(INDICATOR_CODE + 0x0146, bytearray([0x99, 0x00, 0x02])) # sta $0200,y
+ rom.write_bytes(INDICATOR_CODE + 0x0149, bytearray([0x18])) # clc
+ rom.write_bytes(INDICATOR_CODE + 0x014A, bytearray([0x69, 0x05])) # adc #$05
+ rom.write_bytes(INDICATOR_CODE + 0x014C, bytearray([0x99, 0x04, 0x02])) # sta $0204,y
+ rom.write_bytes(INDICATOR_CODE + 0x014F, bytearray([0xDA])) # phx
+ rom.write_bytes(INDICATOR_CODE + 0x0150, bytearray([0xBD, 0xE1, 0x16])) # lda !score_sprite_num,x
+ rom.write_bytes(INDICATOR_CODE + 0x0153, bytearray([0x38])) # sec
+ rom.write_bytes(INDICATOR_CODE + 0x0154, bytearray([0xE9, 0x11])) # sbc #$11
+ rom.write_bytes(INDICATOR_CODE + 0x0156, bytearray([0x0A])) # asl
+ rom.write_bytes(INDICATOR_CODE + 0x0157, bytearray([0xAA])) # tax
+ rom.write_bytes(INDICATOR_CODE + 0x0158, bytearray([0xBD, 0x09, 0xC2])) # lda ..num_tile+$00,x
+ rom.write_bytes(INDICATOR_CODE + 0x015B, bytearray([0x99, 0x02, 0x02])) # sta $0202,y
+ rom.write_bytes(INDICATOR_CODE + 0x015E, bytearray([0xBD, 0x0A, 0xC2])) # lda ..num_tile+$01,x
+ rom.write_bytes(INDICATOR_CODE + 0x0161, bytearray([0x99, 0x06, 0x02])) # sta $0206,y
+ rom.write_bytes(INDICATOR_CODE + 0x0164, bytearray([0xBD, 0x27, 0xC2])) # lda ..num_props+$00,x
+ rom.write_bytes(INDICATOR_CODE + 0x0167, bytearray([0x99, 0x03, 0x02])) # sta $0203,y
+ rom.write_bytes(INDICATOR_CODE + 0x016A, bytearray([0xBD, 0x28, 0xC2])) # lda ..num_props+$01,x
+ rom.write_bytes(INDICATOR_CODE + 0x016D, bytearray([0x99, 0x07, 0x02])) # sta $0207,y
+ rom.write_bytes(INDICATOR_CODE + 0x0170, bytearray([0xFA])) # plx
+ rom.write_bytes(INDICATOR_CODE + 0x0171, bytearray([0x98])) # tya
+ rom.write_bytes(INDICATOR_CODE + 0x0172, bytearray([0x4A])) # lsr
+ rom.write_bytes(INDICATOR_CODE + 0x0173, bytearray([0x4A])) # lsr
+ rom.write_bytes(INDICATOR_CODE + 0x0174, bytearray([0xA8])) # tay
+ rom.write_bytes(INDICATOR_CODE + 0x0175, bytearray([0xA9, 0x00])) # lda #$00
+ rom.write_bytes(INDICATOR_CODE + 0x0177, bytearray([0x99, 0x20, 0x04])) # sta $0420,y
+ rom.write_bytes(INDICATOR_CODE + 0x017A, bytearray([0x99, 0x21, 0x04])) # sta $0421,y
+ rom.write_bytes(INDICATOR_CODE + 0x017D, bytearray([0xBF, 0x45, 0xC2, 0x10])) # lda.l ..oam_2,x
+ rom.write_bytes(INDICATOR_CODE + 0x0181, bytearray([0xA8])) # tay
+ rom.write_bytes(INDICATOR_CODE + 0x0182, bytearray([0xBD, 0xE7, 0x16])) # lda !score_sprite_y_lo,x
+ rom.write_bytes(INDICATOR_CODE + 0x0185, bytearray([0x38])) # sec
+ rom.write_bytes(INDICATOR_CODE + 0x0186, bytearray([0xE5, 0x02])) # sbc $02
+ rom.write_bytes(INDICATOR_CODE + 0x0188, bytearray([0x99, 0x01, 0x02])) # sta $0201,y
+ rom.write_bytes(INDICATOR_CODE + 0x018B, bytearray([0x99, 0x05, 0x02])) # sta $0205,y
+ rom.write_bytes(INDICATOR_CODE + 0x018E, bytearray([0xBD, 0xED, 0x16])) # lda !score_sprite_x_lo,x
+ rom.write_bytes(INDICATOR_CODE + 0x0191, bytearray([0x38])) # sec
+ rom.write_bytes(INDICATOR_CODE + 0x0192, bytearray([0xE5, 0x04])) # sbc $04
+ rom.write_bytes(INDICATOR_CODE + 0x0194, bytearray([0xE9, 0x07])) # sbc #$07
+ rom.write_bytes(INDICATOR_CODE + 0x0196, bytearray([0x99, 0x00, 0x02])) # sta $0200,y
+ rom.write_bytes(INDICATOR_CODE + 0x0199, bytearray([0x18])) # clc
+ rom.write_bytes(INDICATOR_CODE + 0x019A, bytearray([0x69, 0x08])) # adc #$08
+ rom.write_bytes(INDICATOR_CODE + 0x019C, bytearray([0x99, 0x04, 0x02])) # sta $0204,y
+ rom.write_bytes(INDICATOR_CODE + 0x019F, bytearray([0xDA])) # phx
+ rom.write_bytes(INDICATOR_CODE + 0x01A0, bytearray([0xBD, 0xE1, 0x16])) # lda !score_sprite_num,x
+ rom.write_bytes(INDICATOR_CODE + 0x01A3, bytearray([0x38])) # sec
+ rom.write_bytes(INDICATOR_CODE + 0x01A4, bytearray([0xE9, 0x11])) # sbc #$11
+ rom.write_bytes(INDICATOR_CODE + 0x01A6, bytearray([0xAA])) # tax
+ rom.write_bytes(INDICATOR_CODE + 0x01A7, bytearray([0xBD, 0xCD, 0xC1])) # lda ..icon_tile,x
+ rom.write_bytes(INDICATOR_CODE + 0x01AA, bytearray([0x99, 0x02, 0x02])) # sta $0202,y
+ rom.write_bytes(INDICATOR_CODE + 0x01AD, bytearray([0xBD, 0xDC, 0xC1])) # lda ..icon_props,x
+ rom.write_bytes(INDICATOR_CODE + 0x01B0, bytearray([0x99, 0x03, 0x02])) # sta $0203,y
+ rom.write_bytes(INDICATOR_CODE + 0x01B3, bytearray([0xBD, 0xFA, 0xC1])) # lda ..plus_props,x
+ rom.write_bytes(INDICATOR_CODE + 0x01B6, bytearray([0x99, 0x07, 0x02])) # sta $0207,y
+ rom.write_bytes(INDICATOR_CODE + 0x01B9, bytearray([0xBD, 0xEB, 0xC1])) # lda ..plus_tile,x
+ rom.write_bytes(INDICATOR_CODE + 0x01BC, bytearray([0x99, 0x06, 0x02])) # sta $0206,y
+ rom.write_bytes(INDICATOR_CODE + 0x01BF, bytearray([0xFA])) # plx
+ rom.write_bytes(INDICATOR_CODE + 0x01C0, bytearray([0x98])) # tya
+ rom.write_bytes(INDICATOR_CODE + 0x01C1, bytearray([0x4A])) # lsr
+ rom.write_bytes(INDICATOR_CODE + 0x01C2, bytearray([0x4A])) # lsr
+ rom.write_bytes(INDICATOR_CODE + 0x01C3, bytearray([0xA8])) # tay
+ rom.write_bytes(INDICATOR_CODE + 0x01C4, bytearray([0xA9, 0x00])) # lda #$00
+ rom.write_bytes(INDICATOR_CODE + 0x01C6, bytearray([0x99, 0x20, 0x04])) # sta $0420,y
+ rom.write_bytes(INDICATOR_CODE + 0x01C9, bytearray([0x99, 0x21, 0x04])) # sta $0421,y
+ rom.write_bytes(INDICATOR_CODE + 0x01CC, bytearray([0x6B])) # rtl
+ rom.write_bytes(INDICATOR_CODE + 0x01CD, bytearray([0x1B])) # ..icon_tile db $1B ; 1 coin
+ rom.write_bytes(INDICATOR_CODE + 0x01CE, bytearray([0x1B])) # db $1B ; 5 coins
+ rom.write_bytes(INDICATOR_CODE + 0x01CF, bytearray([0x1B])) # db $1B ; 10 coins
+ rom.write_bytes(INDICATOR_CODE + 0x01D0, bytearray([0x1B])) # db $1B ; 15 coins
+ rom.write_bytes(INDICATOR_CODE + 0x01D1, bytearray([0x0A])) # db $0A ; yoshi egg
+ rom.write_bytes(INDICATOR_CODE + 0x01D2, bytearray([0x0B])) # db $0B ; 1up mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x01D3, bytearray([0x0B])) # db $0B ; mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x01D4, bytearray([0x7E])) # db $7E ; flower
+ rom.write_bytes(INDICATOR_CODE + 0x01D5, bytearray([0x7F])) # db $7F ; feather
+ rom.write_bytes(INDICATOR_CODE + 0x01D6, bytearray([0x38])) # db $38 ; boss token
+ rom.write_bytes(INDICATOR_CODE + 0x01D7, bytearray([0x5A])) # db $5A ;
+ rom.write_bytes(INDICATOR_CODE + 0x01D8, bytearray([0x5A])) # db $5A ;
+ rom.write_bytes(INDICATOR_CODE + 0x01D9, bytearray([0x5A])) # db $5A ;
+ rom.write_bytes(INDICATOR_CODE + 0x01DA, bytearray([0x5A])) # db $5A ;
+ rom.write_bytes(INDICATOR_CODE + 0x01DB, bytearray([0x0B])) # db $0B ;
+ rom.write_bytes(INDICATOR_CODE + 0x01DC, bytearray([0x34])) # ..icon_props db $34 ; coin
+ rom.write_bytes(INDICATOR_CODE + 0x01DD, bytearray([0x34])) # db $34 ; coin
+ rom.write_bytes(INDICATOR_CODE + 0x01DE, bytearray([0x34])) # db $34 ; coin
+ rom.write_bytes(INDICATOR_CODE + 0x01DF, bytearray([0x34])) # db $34 ; coin
+ rom.write_bytes(INDICATOR_CODE + 0x01E0, bytearray([0x3A])) # db $3A ; yoshi egg
+ rom.write_bytes(INDICATOR_CODE + 0x01E1, bytearray([0x3A])) # db $3A ; 1up mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x01E2, bytearray([0x38])) # db $38 ; mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x01E3, bytearray([0x3A])) # db $3A ; flower
+ rom.write_bytes(INDICATOR_CODE + 0x01E4, bytearray([0x34])) # db $34 ; feather
+ rom.write_bytes(INDICATOR_CODE + 0x01E5, bytearray([0x34])) # db $34 ; boss token
+ rom.write_bytes(INDICATOR_CODE + 0x01E6, bytearray([0x34])) # db $34 ;
+ rom.write_bytes(INDICATOR_CODE + 0x01E7, bytearray([0x3A])) # db $3A ;
+ rom.write_bytes(INDICATOR_CODE + 0x01E8, bytearray([0x38])) # db $38 ;
+ rom.write_bytes(INDICATOR_CODE + 0x01E9, bytearray([0x36])) # db $36 ;
+ rom.write_bytes(INDICATOR_CODE + 0x01EA, bytearray([0x36])) # db $36 ;
+ rom.write_bytes(INDICATOR_CODE + 0x01EB, bytearray([0x1A])) # ..plus_tile db $1A ; 1 coin
+ rom.write_bytes(INDICATOR_CODE + 0x01EC, bytearray([0x1A])) # db $1A ; 3 coins
+ rom.write_bytes(INDICATOR_CODE + 0x01ED, bytearray([0x1A])) # db $1A ; 5 coins
+ rom.write_bytes(INDICATOR_CODE + 0x01EE, bytearray([0x1A])) # db $1A ; 10 coins
+ rom.write_bytes(INDICATOR_CODE + 0x01EF, bytearray([0x1A])) # db $1A ; yoshi egg
+ rom.write_bytes(INDICATOR_CODE + 0x01F0, bytearray([0x1A])) # db $1A ; 1up mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x01F1, bytearray([0x1A])) # db $1A ; mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x01F2, bytearray([0x1A])) # db $1A ; flower
+ rom.write_bytes(INDICATOR_CODE + 0x01F3, bytearray([0x1A])) # db $1A ; feather
+ rom.write_bytes(INDICATOR_CODE + 0x01F4, bytearray([0x1A])) # db $1A ; boss token
+ rom.write_bytes(INDICATOR_CODE + 0x01F5, bytearray([0x1A])) # db $1A ;
+ rom.write_bytes(INDICATOR_CODE + 0x01F6, bytearray([0x1A])) # db $1A ;
+ rom.write_bytes(INDICATOR_CODE + 0x01F7, bytearray([0x1A])) # db $1A ;
+ rom.write_bytes(INDICATOR_CODE + 0x01F8, bytearray([0x1A])) # db $1A ;
+ rom.write_bytes(INDICATOR_CODE + 0x01F9, bytearray([0x1A])) # db $1A ;
+ rom.write_bytes(INDICATOR_CODE + 0x01FA, bytearray([0x32])) # ..plus_props db $32 ; 1 coin
+ rom.write_bytes(INDICATOR_CODE + 0x01FB, bytearray([0x32])) # db $32 ; 5 coins
+ rom.write_bytes(INDICATOR_CODE + 0x01FC, bytearray([0x32])) # db $32 ; 10 coins
+ rom.write_bytes(INDICATOR_CODE + 0x01FD, bytearray([0x32])) # db $32 ; 50 coins
+ rom.write_bytes(INDICATOR_CODE + 0x01FE, bytearray([0x32])) # db $32 ; yoshi egg
+ rom.write_bytes(INDICATOR_CODE + 0x01FF, bytearray([0x32])) # db $32 ; 1up mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x0200, bytearray([0x32])) # db $32 ; mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x0201, bytearray([0x32])) # db $32 ; flower
+ rom.write_bytes(INDICATOR_CODE + 0x0202, bytearray([0x32])) # db $32 ; feather
+ rom.write_bytes(INDICATOR_CODE + 0x0203, bytearray([0x32])) # db $32 ; boss token
+ rom.write_bytes(INDICATOR_CODE + 0x0204, bytearray([0x32])) # db $32 ;
+ rom.write_bytes(INDICATOR_CODE + 0x0205, bytearray([0x32])) # db $32 ;
+ rom.write_bytes(INDICATOR_CODE + 0x0206, bytearray([0x32])) # db $32 ;
+ rom.write_bytes(INDICATOR_CODE + 0x0207, bytearray([0x32])) # db $32 ;
+ rom.write_bytes(INDICATOR_CODE + 0x0208, bytearray([0x32])) # db $32 ;
+ rom.write_bytes(INDICATOR_CODE + 0x0209, bytearray([0x4B, 0x69])) # ..num_tile db $4B,$69 ; 1 coin
+ rom.write_bytes(INDICATOR_CODE + 0x020B, bytearray([0x5B, 0x69])) # db $5B,$69 ; 5 coins
+ rom.write_bytes(INDICATOR_CODE + 0x020D, bytearray([0x4B, 0x4A])) # db $4B,$4A ; 10 coins
+ rom.write_bytes(INDICATOR_CODE + 0x020F, bytearray([0x5B, 0x4A])) # db $4B,$5B ; 50 coins
+ rom.write_bytes(INDICATOR_CODE + 0x0211, bytearray([0x4B, 0x69])) # db $4B,$69 ; yoshi egg
+ rom.write_bytes(INDICATOR_CODE + 0x0213, bytearray([0x4B, 0x69])) # db $4B,$69 ; 1up mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x0215, bytearray([0x4B, 0x69])) # db $4B,$69 ; mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x0217, bytearray([0x4B, 0x69])) # db $4B,$69 ; flower
+ rom.write_bytes(INDICATOR_CODE + 0x0219, bytearray([0x4B, 0x69])) # db $4B,$69 ; feather
+ rom.write_bytes(INDICATOR_CODE + 0x021B, bytearray([0x4B, 0x69])) # db $4B,$69 ; boss token
+ rom.write_bytes(INDICATOR_CODE + 0x021D, bytearray([0x69, 0x69])) # db $69,$69 ;
+ rom.write_bytes(INDICATOR_CODE + 0x021F, bytearray([0x69, 0x69])) # db $69,$69 ;
+ rom.write_bytes(INDICATOR_CODE + 0x0221, bytearray([0x69, 0x69])) # db $69,$69 ;
+ rom.write_bytes(INDICATOR_CODE + 0x0223, bytearray([0x69, 0x69])) # db $69,$69 ;
+ rom.write_bytes(INDICATOR_CODE + 0x0225, bytearray([0x69, 0x69])) # db $69,$69 ;
+ rom.write_bytes(INDICATOR_CODE + 0x0227, bytearray([0x34, 0x34])) # ..num_props db $34,$34 ; 1 coin
+ rom.write_bytes(INDICATOR_CODE + 0x0229, bytearray([0x34, 0x34])) # db $34,$34 ; 5 coins
+ rom.write_bytes(INDICATOR_CODE + 0x022B, bytearray([0x34, 0x34])) # db $34,$34 ; 10 coins
+ rom.write_bytes(INDICATOR_CODE + 0x022D, bytearray([0x34, 0x34])) # db $34,$34 ; 50 coins
+ rom.write_bytes(INDICATOR_CODE + 0x022F, bytearray([0x34, 0x34])) # db $34,$34 ; yoshi egg
+ rom.write_bytes(INDICATOR_CODE + 0x0231, bytearray([0x34, 0x34])) # db $34,$34 ; 1up mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x0233, bytearray([0x34, 0x34])) # db $34,$34 ; mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x0235, bytearray([0x34, 0x34])) # db $34,$34 ; flower
+ rom.write_bytes(INDICATOR_CODE + 0x0237, bytearray([0x34, 0x34])) # db $34,$34 ; feather
+ rom.write_bytes(INDICATOR_CODE + 0x0239, bytearray([0x34, 0x34])) # db $34,$34 ; boss token
+ rom.write_bytes(INDICATOR_CODE + 0x023B, bytearray([0x34, 0x34])) # db $34,$34 ;
+ rom.write_bytes(INDICATOR_CODE + 0x023D, bytearray([0x34, 0x34])) # db $34,$34 ;
+ rom.write_bytes(INDICATOR_CODE + 0x023F, bytearray([0x34, 0x34])) # db $34,$34 ;
+ rom.write_bytes(INDICATOR_CODE + 0x0241, bytearray([0x34, 0x34])) # db $34,$34 ;
+ rom.write_bytes(INDICATOR_CODE + 0x0243, bytearray([0x34, 0x34])) # db $34,$34 ;
+ rom.write_bytes(INDICATOR_CODE + 0x0245, bytearray([0x50, 0x58, 0x60, 0x68, 0x70, 0x78]))# ..oam_2 db $50,$58,$60,$68,$70,$78
+ rom.write_bytes(INDICATOR_CODE + 0x024B, bytearray([0x69, 0xC2])) # .reward_ptrs dw .one_coin
+ rom.write_bytes(INDICATOR_CODE + 0x024D, bytearray([0x6D, 0xC2])) # dw .five_coins
+ rom.write_bytes(INDICATOR_CODE + 0x024F, bytearray([0x71, 0xC2])) # dw .ten_coins
+ rom.write_bytes(INDICATOR_CODE + 0x0251, bytearray([0x75, 0xC2])) # dw .fifty_coins
+ rom.write_bytes(INDICATOR_CODE + 0x0253, bytearray([0x8A, 0xC2])) # dw .yoshi_egg
+ rom.write_bytes(INDICATOR_CODE + 0x0255, bytearray([0xA7, 0xC2])) # dw .green_mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x0257, bytearray([0xAD, 0xC2])) # dw .mushroom
+ rom.write_bytes(INDICATOR_CODE + 0x0259, bytearray([0xAF, 0xC2])) # dw .flower
+ rom.write_bytes(INDICATOR_CODE + 0x025B, bytearray([0xB1, 0xC2])) # dw .shared_item
+ rom.write_bytes(INDICATOR_CODE + 0x025D, bytearray([0x9C, 0xC2])) # dw .boss_token
+ rom.write_bytes(INDICATOR_CODE + 0x025F, bytearray([0xCB, 0xC0])) # dw .handle_movement
+ rom.write_bytes(INDICATOR_CODE + 0x0261, bytearray([0xCB, 0xC0])) # dw .handle_movement
+ rom.write_bytes(INDICATOR_CODE + 0x0263, bytearray([0xCB, 0xC0])) # dw .handle_movement
+ rom.write_bytes(INDICATOR_CODE + 0x0265, bytearray([0xCB, 0xC0])) # dw .handle_movement
+ rom.write_bytes(INDICATOR_CODE + 0x0267, bytearray([0xCB, 0xC0])) # dw .handle_movement
+ rom.write_bytes(INDICATOR_CODE + 0x0269, bytearray([0xA9, 0x01])) # .one_coin lda #$01
+ rom.write_bytes(INDICATOR_CODE + 0x026B, bytearray([0x80, 0x0A])) # bra .shared_coins
+ rom.write_bytes(INDICATOR_CODE + 0x026D, bytearray([0xA9, 0x05])) # .five_coins lda #$05
+ rom.write_bytes(INDICATOR_CODE + 0x026F, bytearray([0x80, 0x06])) # bra .shared_coins
+ rom.write_bytes(INDICATOR_CODE + 0x0271, bytearray([0xA9, 0x0A])) # .ten_coins lda #$0A
+ rom.write_bytes(INDICATOR_CODE + 0x0273, bytearray([0x80, 0x02])) # bra .shared_coins
+ rom.write_bytes(INDICATOR_CODE + 0x0275, bytearray([0xA9, 0x32])) # .fifty_coins lda #$32
+ rom.write_bytes(INDICATOR_CODE + 0x0277, bytearray([0x18])) # .shared_coins clc
+ rom.write_bytes(INDICATOR_CODE + 0x0278, bytearray([0x6D, 0xCC, 0x13])) # adc $13CC
+ rom.write_bytes(INDICATOR_CODE + 0x027B, bytearray([0x90, 0x02])) # bcc +
+ rom.write_bytes(INDICATOR_CODE + 0x027D, bytearray([0xA9, 0xFF])) # lda #$FF
+ rom.write_bytes(INDICATOR_CODE + 0x027F, bytearray([0x8D, 0xCC, 0x13])) # + sta $13CC
+ rom.write_bytes(INDICATOR_CODE + 0x0282, bytearray([0xA9, 0x01])) # lda #$01
+ rom.write_bytes(INDICATOR_CODE + 0x0284, bytearray([0x8D, 0xFC, 0x1D])) # sta $1DFC
+ rom.write_bytes(INDICATOR_CODE + 0x0287, bytearray([0x4C, 0xCB, 0xC0])) # jmp .handle_movement
+ rom.write_bytes(INDICATOR_CODE + 0x028A, bytearray([0xAD, 0x24, 0x1F])) # .yoshi_egg lda $1F24
+ rom.write_bytes(INDICATOR_CODE + 0x028D, bytearray([0xC9, 0xFF])) # cmp #$FF
+ rom.write_bytes(INDICATOR_CODE + 0x028F, bytearray([0xF0, 0x03])) # beq ..nope
+ rom.write_bytes(INDICATOR_CODE + 0x0291, bytearray([0xEE, 0x24, 0x1F])) # inc $1F24
+ rom.write_bytes(INDICATOR_CODE + 0x0294, bytearray([0xA9, 0x1F])) # ..nope lda #$1F
+ rom.write_bytes(INDICATOR_CODE + 0x0296, bytearray([0x8D, 0xFC, 0x1D])) # sta $1DFC
+ rom.write_bytes(INDICATOR_CODE + 0x0299, bytearray([0x4C, 0xCB, 0xC0])) # jmp .handle_movement
+ rom.write_bytes(INDICATOR_CODE + 0x029C, bytearray([0xEE, 0x26, 0x1F])) # .boss_token inc $1F26
+ rom.write_bytes(INDICATOR_CODE + 0x029F, bytearray([0xA9, 0x09])) # lda #$09
+ rom.write_bytes(INDICATOR_CODE + 0x02A1, bytearray([0x8D, 0xFC, 0x1D])) # sta $1DFC
+ rom.write_bytes(INDICATOR_CODE + 0x02A4, bytearray([0x4C, 0xCB, 0xC0])) # jmp .handle_movement
+ rom.write_bytes(INDICATOR_CODE + 0x02A7, bytearray([0xEE, 0xE4, 0x18])) # .green_mushroom inc $18E4
+ rom.write_bytes(INDICATOR_CODE + 0x02AA, bytearray([0x4C, 0xCB, 0xC0])) # jmp .handle_movement
+ rom.write_bytes(INDICATOR_CODE + 0x02AD, bytearray([0x80, 0x02])) # .mushroom bra .shared_item
+ rom.write_bytes(INDICATOR_CODE + 0x02AF, bytearray([0x80, 0x00])) # .flower bra .shared_item
+ rom.write_bytes(INDICATOR_CODE + 0x02B1, bytearray([0xA9, 0x0B])) # .shared_item lda #$0B
+ rom.write_bytes(INDICATOR_CODE + 0x02B3, bytearray([0x8D, 0xFC, 0x1D])) # sta $1DFC
+ rom.write_bytes(INDICATOR_CODE + 0x02B6, bytearray([0x4C, 0xCB, 0xC0])) # jmp .handle_movement
+
+def handle_traps(rom):
+ TRAPS_CODE = 0x86C00
+ rom.write_bytes(0x022D8, bytearray([0x22, 0x00, 0xEC, 0x10])) # org $00A2D8 : jsl score_sprites
+ rom.write_bytes(TRAPS_CODE + 0x0000, bytearray([0xAD, 0x00, 0x01])) # handle_traps: lda $0100
+ rom.write_bytes(TRAPS_CODE + 0x0003, bytearray([0xC9, 0x14])) # cmp #$14
+ rom.write_bytes(TRAPS_CODE + 0x0005, bytearray([0xD0, 0x04])) # bne .invalid
+ rom.write_bytes(TRAPS_CODE + 0x0007, bytearray([0xA5, 0x71])) # lda $71
+ rom.write_bytes(TRAPS_CODE + 0x0009, bytearray([0xF0, 0x09])) # beq .valid
+ rom.write_bytes(TRAPS_CODE + 0x000B, bytearray([0xA9, 0xFF])) # .invalid lda #$FF
+ rom.write_bytes(TRAPS_CODE + 0x000D, bytearray([0x8D, 0x3C, 0x0F])) # sta !thwimp_index
+ rom.write_bytes(TRAPS_CODE + 0x0010, bytearray([0x5C, 0xBD, 0xE2, 0x00])) # jml $00E2BD
+ rom.write_bytes(TRAPS_CODE + 0x0014, bytearray([0xAD, 0xB4, 0x18])) # .valid lda !reverse_controls_trap
+ rom.write_bytes(TRAPS_CODE + 0x0017, bytearray([0xF0, 0x03])) # beq .no_reverse_controls
+ rom.write_bytes(TRAPS_CODE + 0x0019, bytearray([0x20, 0x2B, 0xEC])) # jsr reverse_controls_trap
+ rom.write_bytes(TRAPS_CODE + 0x001C, bytearray([0xAD, 0xB7, 0x18])) # .no_reverse_controls lda !thwimp_trap
+ rom.write_bytes(TRAPS_CODE + 0x001F, bytearray([0xF0, 0x03])) # beq .no_thwimp
+ rom.write_bytes(TRAPS_CODE + 0x0021, bytearray([0x20, 0x86, 0xEC])) # jsr spawn_thwimp
+ rom.write_bytes(TRAPS_CODE + 0x0024, bytearray([0x20, 0xCB, 0xEC])) # .no_thwimp jsr handle_thwimp
+ rom.write_bytes(TRAPS_CODE + 0x0027, bytearray([0x5C, 0xBD, 0xE2, 0x00])) # jml $00E2BD
+ rom.write_bytes(TRAPS_CODE + 0x002B, bytearray([0xA5, 0x15])) # reverse_controls_trap: lda $15
+ rom.write_bytes(TRAPS_CODE + 0x002D, bytearray([0x89, 0x03])) # bit #$03
+ rom.write_bytes(TRAPS_CODE + 0x002F, bytearray([0xF0, 0x04])) # beq ..no_swap_hold
+ rom.write_bytes(TRAPS_CODE + 0x0031, bytearray([0x49, 0x03])) # eor #$03
+ rom.write_bytes(TRAPS_CODE + 0x0033, bytearray([0x85, 0x15])) # sta $15
+ rom.write_bytes(TRAPS_CODE + 0x0035, bytearray([0xA5, 0x16])) # ..no_swap_hold lda $16
+ rom.write_bytes(TRAPS_CODE + 0x0037, bytearray([0x89, 0x03])) # bit #$03
+ rom.write_bytes(TRAPS_CODE + 0x0039, bytearray([0xF0, 0x04])) # beq ..no_swap_press
+ rom.write_bytes(TRAPS_CODE + 0x003B, bytearray([0x49, 0x03])) # eor #$03
+ rom.write_bytes(TRAPS_CODE + 0x003D, bytearray([0x85, 0x16])) # sta $16
+ rom.write_bytes(TRAPS_CODE + 0x003F, bytearray([0xA5, 0x15])) # .swap_up_and_down lda $15
+ rom.write_bytes(TRAPS_CODE + 0x0041, bytearray([0x89, 0x0C])) # bit #$0C
+ rom.write_bytes(TRAPS_CODE + 0x0043, bytearray([0xF0, 0x04])) # beq .no_swap_hold
+ rom.write_bytes(TRAPS_CODE + 0x0045, bytearray([0x49, 0x0C])) # eor #$0C
+ rom.write_bytes(TRAPS_CODE + 0x0047, bytearray([0x85, 0x15])) # sta $15
+ rom.write_bytes(TRAPS_CODE + 0x0049, bytearray([0xA5, 0x16])) # .no_swap_hold lda $16
+ rom.write_bytes(TRAPS_CODE + 0x004B, bytearray([0x89, 0x0C])) # bit #$0C
+ rom.write_bytes(TRAPS_CODE + 0x004D, bytearray([0xF0, 0x04])) # beq ..no_swap_press
+ rom.write_bytes(TRAPS_CODE + 0x004F, bytearray([0x49, 0x0C])) # eor #$0C
+ rom.write_bytes(TRAPS_CODE + 0x0051, bytearray([0x85, 0x16])) # sta $16
+ rom.write_bytes(TRAPS_CODE + 0x0053, bytearray([0xA5, 0x16])) # .swap_a_and_b lda $16
+ rom.write_bytes(TRAPS_CODE + 0x0055, bytearray([0x10, 0x0C])) # bpl ..no_swap_b
+ rom.write_bytes(TRAPS_CODE + 0x0057, bytearray([0x49, 0x80])) # eor #$80
+ rom.write_bytes(TRAPS_CODE + 0x0059, bytearray([0x85, 0x16])) # sta $16
+ rom.write_bytes(TRAPS_CODE + 0x005B, bytearray([0xA5, 0x18])) # lda $18
+ rom.write_bytes(TRAPS_CODE + 0x005D, bytearray([0x49, 0x80])) # eor #$80
+ rom.write_bytes(TRAPS_CODE + 0x005F, bytearray([0x85, 0x18])) # sta $18
+ rom.write_bytes(TRAPS_CODE + 0x0061, bytearray([0x80, 0x0E])) # bra .swap_l_and_r
+ rom.write_bytes(TRAPS_CODE + 0x0063, bytearray([0xA5, 0x18])) # ..no_swap_b lda $18
+ rom.write_bytes(TRAPS_CODE + 0x0065, bytearray([0x10, 0x0A])) # bpl .swap_l_and_r
+ rom.write_bytes(TRAPS_CODE + 0x0067, bytearray([0x49, 0x80])) # eor #$80
+ rom.write_bytes(TRAPS_CODE + 0x0069, bytearray([0x85, 0x18])) # sta $18
+ rom.write_bytes(TRAPS_CODE + 0x006B, bytearray([0xA5, 0x16])) # lda $16
+ rom.write_bytes(TRAPS_CODE + 0x006D, bytearray([0x49, 0x80])) # eor #$80
+ rom.write_bytes(TRAPS_CODE + 0x006F, bytearray([0x85, 0x16])) # sta $16
+ rom.write_bytes(TRAPS_CODE + 0x0071, bytearray([0xA5, 0x17])) # .swap_l_and_r lda $17
+ rom.write_bytes(TRAPS_CODE + 0x0073, bytearray([0x89, 0x30])) # bit #$30
+ rom.write_bytes(TRAPS_CODE + 0x0075, bytearray([0xF0, 0x04])) # beq ..no_swap_hold
+ rom.write_bytes(TRAPS_CODE + 0x0077, bytearray([0x49, 0x30])) # eor #$30
+ rom.write_bytes(TRAPS_CODE + 0x0079, bytearray([0x85, 0x17])) # sta $17
+ rom.write_bytes(TRAPS_CODE + 0x007B, bytearray([0xA5, 0x18])) # ..no_swap_hold lda $18
+ rom.write_bytes(TRAPS_CODE + 0x007D, bytearray([0x89, 0x30])) # bit #$30
+ rom.write_bytes(TRAPS_CODE + 0x007F, bytearray([0xF0, 0x04])) # beq ..no_swap_press
+ rom.write_bytes(TRAPS_CODE + 0x0081, bytearray([0x49, 0x30])) # eor #$30
+ rom.write_bytes(TRAPS_CODE + 0x0083, bytearray([0x85, 0x18])) # sta $18
+ rom.write_bytes(TRAPS_CODE + 0x0085, bytearray([0x60])) # ..no_swap_press rts
+ rom.write_bytes(TRAPS_CODE + 0x0086, bytearray([0xAE, 0x3C, 0x0F])) # spawn_thwimp: ldx !thwimp_index
+ rom.write_bytes(TRAPS_CODE + 0x0089, bytearray([0x10, 0x06])) # bpl .return
+ rom.write_bytes(TRAPS_CODE + 0x008B, bytearray([0x22, 0xE4, 0xA9, 0x02])) # jsl $02A9E4
+ rom.write_bytes(TRAPS_CODE + 0x008F, bytearray([0x10, 0x01])) # bpl .found
+ rom.write_bytes(TRAPS_CODE + 0x0091, bytearray([0x60])) # .return rts
+ rom.write_bytes(TRAPS_CODE + 0x0092, bytearray([0xBB])) # .found tyx
+ rom.write_bytes(TRAPS_CODE + 0x0093, bytearray([0x9C, 0xB7, 0x18])) # stz !thwimp_trap
+ rom.write_bytes(TRAPS_CODE + 0x0096, bytearray([0xA9, 0x10])) # lda #$10
+ rom.write_bytes(TRAPS_CODE + 0x0098, bytearray([0x8D, 0xF9, 0x1D])) # sta $1DF9
+ rom.write_bytes(TRAPS_CODE + 0x009B, bytearray([0xA9, 0x27])) # lda #$27
+ rom.write_bytes(TRAPS_CODE + 0x009D, bytearray([0x95, 0x9E])) # sta $9E,x
+ rom.write_bytes(TRAPS_CODE + 0x009F, bytearray([0xA9, 0x08])) # lda #$08
+ rom.write_bytes(TRAPS_CODE + 0x00A1, bytearray([0x9D, 0xC8, 0x14])) # sta $14C8,x
+ rom.write_bytes(TRAPS_CODE + 0x00A4, bytearray([0x22, 0xD2, 0xF7, 0x07])) # jsl $07F7D2
+ rom.write_bytes(TRAPS_CODE + 0x00A8, bytearray([0xA5, 0x94])) # lda $94
+ rom.write_bytes(TRAPS_CODE + 0x00AA, bytearray([0x95, 0xE4])) # sta $E4,x
+ rom.write_bytes(TRAPS_CODE + 0x00AC, bytearray([0xA5, 0x95])) # lda $95
+ rom.write_bytes(TRAPS_CODE + 0x00AE, bytearray([0x9D, 0xE0, 0x14])) # sta $14E0,x
+ rom.write_bytes(TRAPS_CODE + 0x00B1, bytearray([0xA5, 0x1C])) # lda $1C
+ rom.write_bytes(TRAPS_CODE + 0x00B3, bytearray([0x38])) # sec
+ rom.write_bytes(TRAPS_CODE + 0x00B4, bytearray([0xE9, 0x0F])) # sbc #$0F
+ rom.write_bytes(TRAPS_CODE + 0x00B6, bytearray([0x95, 0xD8])) # sta $D8,x
+ rom.write_bytes(TRAPS_CODE + 0x00B8, bytearray([0xA5, 0x1D])) # lda $1D
+ rom.write_bytes(TRAPS_CODE + 0x00BA, bytearray([0xE9, 0x00])) # sbc #$00
+ rom.write_bytes(TRAPS_CODE + 0x00BC, bytearray([0x9D, 0xD4, 0x14])) # sta $14D4,x
+ rom.write_bytes(TRAPS_CODE + 0x00BF, bytearray([0xBD, 0x86, 0x16])) # lda $1686,x
+ rom.write_bytes(TRAPS_CODE + 0x00C2, bytearray([0x09, 0x80])) # ora #$80
+ rom.write_bytes(TRAPS_CODE + 0x00C4, bytearray([0x9D, 0x86, 0x16])) # sta $1686,x
+ rom.write_bytes(TRAPS_CODE + 0x00C7, bytearray([0x8E, 0x3C, 0x0F])) # stx !thwimp_index
+ rom.write_bytes(TRAPS_CODE + 0x00CA, bytearray([0x60])) # rts
+ rom.write_bytes(TRAPS_CODE + 0x00CB, bytearray([0xAE, 0x3C, 0x0F])) # handle_thwimp: ldx !thwimp_index
+ rom.write_bytes(TRAPS_CODE + 0x00CE, bytearray([0x30, 0x1C])) # bmi .return
+ rom.write_bytes(TRAPS_CODE + 0x00D0, bytearray([0xBD, 0xD4, 0x14])) # lda $14D4,x
+ rom.write_bytes(TRAPS_CODE + 0x00D3, bytearray([0xEB])) # xba
+ rom.write_bytes(TRAPS_CODE + 0x00D4, bytearray([0xB5, 0xD8])) # lda $D8,x
+ rom.write_bytes(TRAPS_CODE + 0x00D6, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(TRAPS_CODE + 0x00D8, bytearray([0x38])) # sec
+ rom.write_bytes(TRAPS_CODE + 0x00D9, bytearray([0xE5, 0x96])) # sbc $96
+ rom.write_bytes(TRAPS_CODE + 0x00DB, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(TRAPS_CODE + 0x00DD, bytearray([0x30, 0x0D])) # bmi .return
+ rom.write_bytes(TRAPS_CODE + 0x00DF, bytearray([0xA9, 0xFF])) # lda #$FF
+ rom.write_bytes(TRAPS_CODE + 0x00E1, bytearray([0x8D, 0x3C, 0x0F])) # sta !thwimp_index
+ rom.write_bytes(TRAPS_CODE + 0x00E4, bytearray([0xBD, 0x86, 0x16])) # lda $1686,x
+ rom.write_bytes(TRAPS_CODE + 0x00E7, bytearray([0x29, 0x7F])) # and #$7F
+ rom.write_bytes(TRAPS_CODE + 0x00E9, bytearray([0x9D, 0x86, 0x16])) # sta $1686,x
+ rom.write_bytes(TRAPS_CODE + 0x00EC, bytearray([0x60])) # .return rts
+
+
+
+def read_graphics_file(filename):
+ return pkgutil.get_data(__name__, f"data/graphics/{filename}")
+
+def handle_uncompressed_player_gfx(rom):
+ # Decompresses and moves into a expanded region the player, yoshi and animated graphics
+ # This should make swapping the graphics a lot easier.
+ # Maybe I should look into making a 32x32 version at some point...
+ # It also moves some 8x8 tiles in GFX00, thus making some free space for indicators and other stuff
+ # in VRAM during gameplay, will come super handy later.
+ #
+ # FOR FUTURE REFERENCE
+ # Player graphics are now located at 0xE0000
+ # Player auxiliary tiles are now located at 0xE6000
+ # Yoshi graphics are now located at 0xE8800
+ SMW_COMPRESSED_PLAYER_GFX = 0x40000
+ SMW_COMPRESSED_ANIMATED_GFX = 0x43FC0
+ SMW_COMPRESSED_GFX_00 = 0x459F9
+ SMW_COMPRESSED_GFX_10 = 0x4EF1E
+ SMW_COMPRESSED_GFX_28 = 0x5C06C
+ compressed_player_gfx = rom.read_bytes(SMW_COMPRESSED_PLAYER_GFX, 0x3FC0)
+ compressed_animated_gfx = rom.read_bytes(SMW_COMPRESSED_ANIMATED_GFX, 0x1A39)
+ compressed_gfx_00 = rom.read_bytes(SMW_COMPRESSED_GFX_00, 0x0838)
+ compressed_gfx_10 = rom.read_bytes(SMW_COMPRESSED_GFX_10, 0x0891)
+ compressed_gfx_28 = rom.read_bytes(SMW_COMPRESSED_GFX_28, 0x0637)
+ decompressed_player_gfx = decompress_gfx(compressed_player_gfx)
+ decompressed_animated_gfx = convert_3bpp(decompress_gfx(compressed_animated_gfx))
+ decompressed_gfx_00 = convert_3bpp(decompress_gfx(compressed_gfx_00))
+ decompressed_gfx_10 = convert_3bpp(decompress_gfx(compressed_gfx_10))
+ decompressed_gfx_28 = decompress_gfx(compressed_gfx_28)
+
+ # Copy berry tiles
+ order = [0x26C, 0x26D, 0x26E, 0x26F,
+ 0x27C, 0x27D, 0x27E, 0x27F,
+ 0x2E0, 0x2E1, 0x2E2, 0x2E3,
+ 0x2E4, 0x2E5, 0x2E6, 0x2E7]
+ decompressed_animated_gfx += copy_gfx_tiles(decompressed_player_gfx, order, [5, 32])
+
+ # Copy Mario's auxiliary tiles
+ order = [0x80, 0x91, 0x81, 0x90, 0x82, 0x83]
+ decompressed_gfx_00 += copy_gfx_tiles(decompressed_player_gfx, order, [5, 32])
+ order = [0x69, 0x69, 0x0C, 0x69, 0x1A, 0x1B, 0x0D, 0x69, 0x22, 0x23, 0x32, 0x33, 0x0A, 0x0B, 0x20, 0x21,
+ 0x30, 0x31, 0x7E, 0x69, 0x80, 0x4A, 0x81, 0x5B, 0x82, 0x4B, 0x83, 0x5A, 0x84, 0x69, 0x85, 0x85]
+ player_small_tiles = copy_gfx_tiles(decompressed_gfx_00, order, [5, 32])
+
+ # Copy OW mario tiles
+ order = [0x06, 0x07, 0x16, 0x17,
+ 0x08, 0x09, 0x18, 0x19,
+ 0x0A, 0x0B, 0x1A, 0x1B,
+ 0x0C, 0x0D, 0x1C, 0x1D,
+ 0x0E, 0x0F, 0x1E, 0x1F,
+ 0x20, 0x21, 0x30, 0x31,
+ 0x24, 0x25, 0x34, 0x35,
+ 0x46, 0x47, 0x56, 0x57,
+ 0x64, 0x65, 0x74, 0x75,
+ 0x66, 0x67, 0x76, 0x77,
+ 0x2E, 0x2F, 0x3E, 0x3F,
+ 0x40, 0x41, 0x50, 0x51,
+ 0x42, 0x43, 0x52, 0x53]
+ player_map_tiles = copy_gfx_tiles(decompressed_gfx_10, order, [5, 32])
+
+ # Copy HUD mario tiles
+ order = [0x30, 0x31, 0x32, 0x33, 0x34]
+ player_name_tiles = copy_gfx_tiles(decompressed_gfx_28, order, [4, 16])
+
+ rom.write_bytes(0xE0000, decompressed_player_gfx)
+ rom.write_bytes(0xE8000, decompressed_animated_gfx)
+ rom.write_bytes(0xE6000, player_small_tiles)
+ rom.write_bytes(0xE6400, player_map_tiles)
+ rom.write_bytes(0xE6C00, player_name_tiles)
+
+ # Skip Player & Animated tile decompression
+ rom.write_bytes(0x03888, bytearray([0x60])) # RTS
+
+ # Edit Mario DMA routine
+ MARIO_GFX_DMA_ADDR = 0x02300
+ rom.write_bytes(MARIO_GFX_DMA_ADDR + 0x0000, bytearray([0xA2, 0x04])) # LDX #$04
+ rom.write_bytes(MARIO_GFX_DMA_ADDR + 0x0002, bytearray([0x22, 0x00, 0xF0, 0x10])) # JSL $10F000 ; upload_score_sprite_gfx
+ rom.write_bytes(MARIO_GFX_DMA_ADDR + 0x0006, bytearray([0x22, 0x00, 0xF8, 0x0F])) # JSL $0FF800 ; player_code
+ rom.write_bytes(MARIO_GFX_DMA_ADDR + 0x000A, bytearray([0x60])) # RTS
+
+ PLAYER_UPLOAD_ADDR = 0x7F800
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0000, bytearray([0xC2, 0x20])) # player_code: rep #$20
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0002, bytearray([0xAC, 0x84, 0x0D])) # ldy $0D84
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0005, bytearray([0xD0, 0x03])) # bne .upload_player_palette
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0007, bytearray([0x4C, 0xD2, 0xF8])) # jmp .skip_everything
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x000A, bytearray([0xA0, 0x86])) # .upload_player_palette ldy #$86
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x000C, bytearray([0x8C, 0x21, 0x21])) # sty $2121
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x000F, bytearray([0xA9, 0x00, 0x22])) # lda #$2200
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0012, bytearray([0x8D, 0x20, 0x43])) # sta $4320
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0015, bytearray([0xA8])) # tay
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0016, bytearray([0xAD, 0x82, 0x0D])) # lda $0D82
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0019, bytearray([0x8D, 0x22, 0x43])) # sta $4322
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x001C, bytearray([0x8C, 0x24, 0x43])) # sty $4324
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x001F, bytearray([0xA9, 0x14, 0x00])) # lda #$0014
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0022, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0025, bytearray([0x8E, 0x0B, 0x42])) # stx $420B
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0028, bytearray([0xA0, 0x80])) # ldy #$80
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x002A, bytearray([0x8C, 0x15, 0x21])) # sty $2115
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x002D, bytearray([0xA9, 0x01, 0x18])) # lda #$1801
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0030, bytearray([0x8D, 0x20, 0x43])) # sta $4320
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0033, bytearray([0xA0, 0x1C])) # ldy.b #player_gfx>>16
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0035, bytearray([0x8C, 0x24, 0x43])) # sty $4324
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0038, bytearray([0xA9, 0x00, 0x60])) # .upload_player_top lda #$6000
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x003B, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x003E, bytearray([0xA8])) # tay
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x003F, bytearray([0xB9, 0x85, 0x0D])) # - lda $0D85,y
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0042, bytearray([0x8D, 0x22, 0x43])) # sta $4322
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0045, bytearray([0xA9, 0x40, 0x00])) # lda #$0040
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0048, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x004B, bytearray([0x8E, 0x0B, 0x42])) # stx $420B
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x004E, bytearray([0xC8])) # iny
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x004F, bytearray([0xC8])) # iny
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0050, bytearray([0xC0, 0x06])) # cpy #$06
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0052, bytearray([0xD0, 0xEB])) # bne -
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0054, bytearray([0xA9, 0x00, 0x61])) # .upload_player_bottom lda #$6100
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0057, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x005A, bytearray([0xA8])) # tay
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x005B, bytearray([0xB9, 0x8F, 0x0D])) # - lda $0D8F,y
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x005E, bytearray([0x8D, 0x22, 0x43])) # sta $4322
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0061, bytearray([0xA9, 0x40, 0x00])) # lda #$0040
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0064, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0067, bytearray([0x8E, 0x0B, 0x42])) # stx $420B
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x006A, bytearray([0xC8])) # iny
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x006B, bytearray([0xC8])) # iny
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x006C, bytearray([0xC0, 0x06])) # cpy #$06
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x006E, bytearray([0xD0, 0xEB])) # bne -
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0070, bytearray([0xAC, 0x9B, 0x0D])) # .upload_player_extended ldy $0D9B
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0073, bytearray([0xC0, 0x02])) # cpy #$02
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0075, bytearray([0xF0, 0x5B])) # beq .skip_everything
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0077, bytearray([0xA9, 0xC0, 0x60])) # lda #$60C0
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x007A, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x007D, bytearray([0xAD, 0x99, 0x0D])) # lda $0D99
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0080, bytearray([0x8D, 0x22, 0x43])) # sta $4322
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0083, bytearray([0xA9, 0x40, 0x00])) # lda #$0040
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0086, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0089, bytearray([0x8E, 0x0B, 0x42])) # stx $420B
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x008C, bytearray([0xA0, 0x1D])) # .upload_misc_tiles ldy.b #animated_tiles>>16
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x008E, bytearray([0x8C, 0x24, 0x43])) # sty $4324
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0091, bytearray([0xA9, 0x60, 0x60])) # lda #$6060
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0094, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0097, bytearray([0xA0, 0x06])) # ldy #$06
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x0099, bytearray([0xCC, 0x84, 0x0D])) # cpy $0D84
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x009C, bytearray([0xB0, 0x34])) # bcs .skip_everything
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x009E, bytearray([0xB9, 0x85, 0x0D])) # - lda $0D85,y
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00A1, bytearray([0x8D, 0x22, 0x43])) # sta $4322
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00A4, bytearray([0xA9, 0x40, 0x00])) # lda #$0040
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00A7, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00AA, bytearray([0x8E, 0x0B, 0x42])) # stx $420B
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00AD, bytearray([0xC8])) # iny
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00AE, bytearray([0xC8])) # iny
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00AF, bytearray([0xCC, 0x84, 0x0D])) # cpy $0D84
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00B2, bytearray([0x90, 0xEA])) # bcc -
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00B4, bytearray([0xA9, 0x60, 0x61])) # lda #$6160
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00B7, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00BA, bytearray([0xA0, 0x06])) # ldy #$06
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00BC, bytearray([0xB9, 0x8F, 0x0D])) # - lda $0D8F,y
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00BF, bytearray([0x8D, 0x22, 0x43])) # sta $4322
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00C2, bytearray([0xA9, 0x40, 0x00])) # lda #$0040
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00C5, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00C8, bytearray([0x8E, 0x0B, 0x42])) # stx $420B
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00CB, bytearray([0xC8])) # iny
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00CC, bytearray([0xC8])) # iny
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00CD, bytearray([0xCC, 0x84, 0x0D])) # cpy $0D84
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00D0, bytearray([0x90, 0xEA])) # bcc -
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00D2, bytearray([0xE2, 0x20])) # .skip_everything sep #$20
+ rom.write_bytes(PLAYER_UPLOAD_ADDR + 0x00D4, bytearray([0x6B])) # rtl
+
+ # Obtain data for new 8x8 tile
+ CHAR_TILE_CODE_ADDR = 0x05FE2
+ rom.write_bytes(0x063B1, bytearray([0x20, 0xE2, 0xDF])) # jsr $DFE2
+ rom.write_bytes(CHAR_TILE_CODE_ADDR + 0x0000, bytearray([0xB9, 0x1A, 0xDF])) # lda $DF1A,y
+ rom.write_bytes(CHAR_TILE_CODE_ADDR + 0x0003, bytearray([0x10, 0x06])) # bpl $06
+ rom.write_bytes(CHAR_TILE_CODE_ADDR + 0x0005, bytearray([0x29, 0x7F])) # and #$7F
+ rom.write_bytes(CHAR_TILE_CODE_ADDR + 0x0007, bytearray([0x85, 0x0D])) # sta $0D
+ rom.write_bytes(CHAR_TILE_CODE_ADDR + 0x0009, bytearray([0xA9, 0x04])) # lda #$04
+ rom.write_bytes(CHAR_TILE_CODE_ADDR + 0x000B, bytearray([0x60])) # rts
+
+ rom.write_bytes(0x0640D, bytearray([0x20, 0xEE, 0xDF])) # jsr $DFEE
+ CAPE_TILE_CODE_ADDR = 0x05FEE
+ rom.write_bytes(CAPE_TILE_CODE_ADDR + 0x0000, bytearray([0xA5, 0x0D])) # lda $0D
+ rom.write_bytes(CAPE_TILE_CODE_ADDR + 0x0002, bytearray([0xE0, 0x2B])) # cpx #$2B
+ rom.write_bytes(CAPE_TILE_CODE_ADDR + 0x0004, bytearray([0x90, 0x07])) # bcc $07
+ rom.write_bytes(CAPE_TILE_CODE_ADDR + 0x0006, bytearray([0xE0, 0x40])) # cpx #$40
+ rom.write_bytes(CAPE_TILE_CODE_ADDR + 0x0008, bytearray([0xB0, 0x03])) # bcs $03
+ rom.write_bytes(CAPE_TILE_CODE_ADDR + 0x000A, bytearray([0xBD, 0xD7, 0xE1])) # lda $E1D7,x
+ rom.write_bytes(CAPE_TILE_CODE_ADDR + 0x000D, bytearray([0x60])) # rts
+
+ # Edit Mario's 8x8 tile data
+ MARIO_AUX_TILE_DATA_ADDR = 0x05F1A
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0000, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0008, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0010, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0018, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0020, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0028, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0030, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0038, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0040, bytearray([0x00,0x00,0x00,0x28,0x00,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0048, bytearray([0x00,0x00,0x82,0x82,0x82,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0050, bytearray([0x00,0x00,0x84,0x00,0x00,0x00,0x00,0x86]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0058, bytearray([0x86,0x86,0x00,0x00,0x88,0x88,0x8A,0x8A]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0060, bytearray([0x8C,0x8C,0x00,0x00,0x90,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0068, bytearray([0x00,0x8E,0x00,0x00,0x00,0x00,0x92,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0070, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0078, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0080, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x82]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0088, bytearray([0x82,0x82,0x00,0x00,0x00,0x00,0x00,0x84]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0090, bytearray([0x00,0x00,0x00,0x00,0x86,0x86,0x86,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x0098, bytearray([0x00,0x88,0x88,0x8A,0x8A,0x8C,0x8C,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x00A0, bytearray([0x00,0x90,0x00,0x00,0x00,0x00,0x8E,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x00A8, bytearray([0x00,0x00,0x00,0x92,0x00,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x00B0, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]))
+ rom.write_bytes(MARIO_AUX_TILE_DATA_ADDR + 0x00B8, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]))
+
+ MARIO_AUX_TILE_OFFSETS_ADDR = 0x05FDA # ends at $00E00C
+ rom.write_bytes(MARIO_AUX_TILE_OFFSETS_ADDR + 0x0000, bytearray([0x00,0x02,0x80,0x80,0x00,0x02,0x0C,0x0D]))
+ rom.write_bytes(MARIO_AUX_TILE_OFFSETS_ADDR + 0x0022, bytearray([0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0x02]))
+ rom.write_bytes(MARIO_AUX_TILE_OFFSETS_ADDR + 0x002A, bytearray([0x02,0x80,0x04,0x0C,0x0D,0xFF,0xFF,0xFF]))
+
+ MARIO_AUX_CAPE_TILE_DATA_ADDR = 0x061FF
+ rom.write_bytes(MARIO_AUX_CAPE_TILE_DATA_ADDR + 0x0000, bytearray([0x00,0x8C,0x14,0x14,0x2E]))
+ rom.write_bytes(MARIO_AUX_CAPE_TILE_DATA_ADDR + 0x0005, bytearray([0x00,0xCA,0x16,0x16,0x2E]))
+ rom.write_bytes(MARIO_AUX_CAPE_TILE_DATA_ADDR + 0x000A, bytearray([0x00,0x8E,0x18,0x18,0x2E]))
+ rom.write_bytes(MARIO_AUX_CAPE_TILE_DATA_ADDR + 0x000F, bytearray([0x00,0xEB,0x1A,0x1A,0x2E]))
+ rom.write_bytes(MARIO_AUX_CAPE_TILE_DATA_ADDR + 0x0014, bytearray([0x04,0xED,0x1C,0x1C]))
+
+ # Edit player data offsets
+ rom.write_bytes(0x07649, bytearray([0x69, 0x00, 0x80])) # adc #$8000
+ rom.write_bytes(0x07667, bytearray([0x69, 0x00, 0x80])) # adc #$8000
+ rom.write_bytes(0x0767C, bytearray([0x69, 0x00, 0x80])) # adc #$8000
+ rom.write_bytes(0x07691, bytearray([0x69, 0x00, 0xE0])) # adc #$E000
+
+ # Fix berries
+ FIX_BERRIES_ADDR = 0x7FFE0
+ rom.write_bytes(FIX_BERRIES_ADDR + 0x0000, bytearray([0xA0, 0x1D])) # fix_berries: ldy.b #animated_tiles>>16
+ rom.write_bytes(FIX_BERRIES_ADDR + 0x0002, bytearray([0x8C, 0x24, 0x43])) # sty $4324
+ rom.write_bytes(FIX_BERRIES_ADDR + 0x0005, bytearray([0xAD, 0x76, 0x0D])) # lda $0D76
+ rom.write_bytes(FIX_BERRIES_ADDR + 0x0008, bytearray([0x8D, 0x22, 0x43])) # sta $4322
+ rom.write_bytes(FIX_BERRIES_ADDR + 0x000B, bytearray([0x6B])) # rtl
+
+ # Fix animated graphics
+ rom.write_bytes(0x018D1, bytearray([0x1D])) # db $1D
+ rom.write_bytes(0x0239E, bytearray([0x1D])) # db $1D
+
+ rom.write_bytes(0x023F0, bytearray([0x22, 0xE0, 0xFF, 0x0F])) # jsl $0FFFE0
+ rom.write_bytes(0x023F4, bytearray([0xEA])) # nop
+ rom.write_bytes(0x023F5, bytearray([0xEA])) # nop
+
+ rom.write_bytes(0x0E1A8, bytearray([0x69, 0x00, 0x88])) # adc #$8800
+ rom.write_bytes(0x0EEB4, bytearray([0x69, 0x00, 0x88])) # adc #$8800
+ rom.write_bytes(0x0EEC9, bytearray([0x69, 0x00, 0x88])) # adc #$8800
+ rom.write_bytes(0x16A3E, bytearray([0x69, 0x00, 0x88])) # adc #$8800
+
+ ANIMATED_TILE_DATA_ADDR = 0x2B999
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0000, bytearray([0x00,0x98,0x00,0x9A,0x00,0x9C,0x00,0x9E]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0008, bytearray([0x80,0x98,0x80,0x9A,0x80,0x9C,0x80,0x9E]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0010, bytearray([0x00,0x99,0x00,0x99,0x00,0x99,0x00,0x99]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0018, bytearray([0x80,0xA0,0x80,0xA2,0x80,0xA4,0x80,0xA6]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0020, bytearray([0x00,0x99,0x00,0x9B,0x00,0x9D,0x00,0x9F]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0028, bytearray([0x00,0xB0,0x80,0xB0,0x00,0xB1,0x80,0xB1]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0030, bytearray([0x20,0xAF,0x20,0xAF,0x20,0xAF,0x20,0xAF]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0038, bytearray([0x20,0xAF,0x20,0xAF,0x20,0xAF,0x20,0xAF]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0040, bytearray([0x80,0x96,0x80,0x96,0x80,0x96,0x80,0x96]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0048, bytearray([0x00,0xA7,0x80,0xA7,0x00,0xA7,0x80,0xA7]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0050, bytearray([0x20,0xAF,0x20,0xAF,0x20,0xAF,0x20,0xAF]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0058, bytearray([0x00,0xAF,0x00,0xAF,0x00,0xAF,0x00,0xAF]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0060, bytearray([0x00,0x94,0x00,0x94,0x00,0x94,0x00,0x94]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0068, bytearray([0x80,0x99,0x80,0x9B,0x80,0x9D,0x80,0x9F]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0070, bytearray([0x00,0xA0,0x00,0xA2,0x00,0xA4,0x00,0xA6]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0078, bytearray([0x80,0x91,0x80,0x93,0x80,0x95,0x80,0x97]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0080, bytearray([0x00,0x98,0x00,0x98,0x00,0x98,0x00,0x98]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0088, bytearray([0x00,0x98,0x00,0x98,0x00,0x98,0x00,0x98]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0090, bytearray([0x00,0x98,0x00,0x98,0x00,0x98,0x00,0x98]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0098, bytearray([0x00,0xA0,0x00,0xA2,0x00,0xA4,0x00,0xA6]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x00A0, bytearray([0x80,0x91,0x80,0x93,0x80,0x95,0x80,0x97]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x00A8, bytearray([0x00,0x80,0x00,0x82,0x00,0x84,0x00,0x86]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x00B0, bytearray([0x00,0x86,0x00,0x84,0x00,0x82,0x00,0x80]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x00B8, bytearray([0x00,0xA1,0x00,0xA3,0x00,0xA5,0x00,0xA3]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x00C0, bytearray([0x00,0xA0,0x00,0xA2,0x00,0xA4,0x00,0xA6]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x00C8, bytearray([0x00,0xA8,0x00,0xAA,0x00,0xAC,0x00,0xAE]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x00D0, bytearray([0x80,0xA8,0x80,0xAA,0x80,0xAC,0x80,0xAE]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x00D8, bytearray([0x80,0xAE,0x80,0xAC,0x80,0xAA,0x80,0xA8]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x00E0, bytearray([0x00,0x98,0x00,0x98,0x00,0x98,0x00,0x98]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x00E8, bytearray([0x80,0xA1,0x80,0xA3,0x80,0xA5,0x80,0xA3]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x00F0, bytearray([0x80,0x80,0x80,0x82,0x80,0x84,0x80,0x86]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x00F8, bytearray([0x00,0x81,0x00,0x83,0x00,0x85,0x00,0x87]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0100, bytearray([0x80,0x81,0x80,0x83,0x80,0x85,0x80,0x87]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0108, bytearray([0x80,0x86,0x80,0x84,0x80,0x82,0x80,0x80]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0110, bytearray([0x00,0x98,0x00,0x98,0x00,0x98,0x00,0x98]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0118, bytearray([0x80,0xA9,0x80,0xAB,0x80,0xAD,0x80,0xAB]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0120, bytearray([0x00,0x91,0x00,0x93,0x00,0x95,0x00,0x97]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0128, bytearray([0x00,0x98,0x00,0x98,0x00,0x98,0x00,0x98]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0130, bytearray([0x00,0x98,0x00,0x98,0x00,0x98,0x00,0x98]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0138, bytearray([0x80,0xA1,0x80,0xA3,0x80,0xA5,0x80,0xA3]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0140, bytearray([0x00,0xA9,0x00,0xAB,0x00,0xAD,0x00,0xAB]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0148, bytearray([0x00,0x98,0x00,0x98,0x00,0x98,0x00,0x98]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0150, bytearray([0x00,0x98,0x00,0x98,0x00,0x98,0x00,0x98]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0158, bytearray([0x00,0x98,0x00,0x98,0x00,0x98,0x00,0x98]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0160, bytearray([0x80,0x94,0x80,0x94,0x80,0x94,0x80,0x94]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0168, bytearray([0x80,0x99,0x80,0x9B,0x80,0x9D,0x80,0x9F]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0170, bytearray([0x80,0x99,0x80,0x9B,0x80,0x9D,0x80,0x9F]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0178, bytearray([0x80,0x99,0x80,0x9B,0x80,0x9D,0x80,0x9F]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0180, bytearray([0x00,0x98,0x00,0x9A,0x00,0x9C,0x00,0x9E]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0188, bytearray([0x80,0xAF,0x80,0xAF,0x80,0xAF,0x80,0xAF]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0190, bytearray([0x00,0x96,0x00,0x96,0x00,0x96,0x00,0x96]))
+ rom.write_bytes(ANIMATED_TILE_DATA_ADDR + 0x0198, bytearray([0x80,0x96,0x80,0x96,0x80,0x96,0x80,0x96]))
+
+ # Insert hand drawn graphics for in level indicators
+ rom.write_bytes(0xE7000, read_graphics_file("indicators.bin"))
+ # Upload indicator GFX
+ UPLOAD_INDICATOR_GFX = 0x87000
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0000, bytearray([0xAD, 0x00, 0x01])) # upload_score_sprite_gfx: lda $0100
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0003, bytearray([0xC9, 0x13])) # cmp #$13
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0005, bytearray([0xF0, 0x03])) # beq .check_level
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0007, bytearray([0x4C, 0x9D, 0xF0])) # jmp .check_map
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x000A, bytearray([0xA5, 0x7C])) # .check_level lda $7C
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x000C, bytearray([0xF0, 0x03])) # beq ..perform
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x000E, bytearray([0x4C, 0x9C, 0xF0])) # jmp .skip
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0011, bytearray([0xE6, 0x7C])) # ..perform inc $7C
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0013, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0015, bytearray([0xA0, 0x80])) # ldy #$80
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0017, bytearray([0x8C, 0x15, 0x21])) # sty $2115
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x001A, bytearray([0xA9, 0x01, 0x18])) # lda #$1801
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x001D, bytearray([0x8D, 0x20, 0x43])) # sta $4320
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0020, bytearray([0xA0, 0x1C])) # ldy.b #$1C
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0022, bytearray([0x8C, 0x24, 0x43])) # sty $4324
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0025, bytearray([0xA9, 0x00, 0xF0])) # lda.w #$F000
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0028, bytearray([0x8D, 0x22, 0x43])) # sta $4322
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x002B, bytearray([0xA9, 0xA0, 0x64])) # .nums_01 lda #$64A0
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x002E, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0031, bytearray([0xA9, 0x40, 0x00])) # lda #$0040
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0034, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0037, bytearray([0x8E, 0x0B, 0x42])) # stx $420B
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x003A, bytearray([0xA9, 0xA0, 0x65])) # .nums_35 lda #$65A0
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x003D, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0040, bytearray([0xA9, 0x40, 0x00])) # lda #$0040
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0043, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0046, bytearray([0x8E, 0x0B, 0x42])) # stx $420B
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0049, bytearray([0xA9, 0xA0, 0x61])) # .plus_coin lda #$61A0
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x004C, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x004F, bytearray([0xA9, 0x40, 0x00])) # lda #$0040
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0052, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0055, bytearray([0x8E, 0x0B, 0x42])) # stx $420B
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0058, bytearray([0xA9, 0xA0, 0x60])) # .egg_mushroom lda #$60A0
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x005B, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x005E, bytearray([0xA9, 0x40, 0x00])) # lda #$0040
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0061, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0064, bytearray([0x8E, 0x0B, 0x42])) # stx $420B
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0067, bytearray([0xA9, 0xE0, 0x67])) # .thwimp lda #$67E0
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x006A, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x006D, bytearray([0xA9, 0x40, 0x00])) # lda #$0040
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0070, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0073, bytearray([0x8E, 0x0B, 0x42])) # stx $420B
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0076, bytearray([0xA9, 0x80, 0x63])) # .token lda #$6380
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0079, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x007C, bytearray([0xA9, 0x20, 0x00])) # lda #$0020
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x007F, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0082, bytearray([0x8E, 0x0B, 0x42])) # stx $420B
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0085, bytearray([0xA9, 0x00, 0xEC])) # .layer_3 lda #$EC00
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0088, bytearray([0x8D, 0x22, 0x43])) # sta $4322
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x008B, bytearray([0xA9, 0x80, 0x41])) # lda #$4180
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x008E, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0091, bytearray([0xA9, 0x50, 0x00])) # lda #$0050
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0094, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0097, bytearray([0x8E, 0x0B, 0x42])) # stx $420B
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x009A, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x009C, bytearray([0x6B])) # .skip rtl
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x009D, bytearray([0xC9, 0x0E])) # .check_map cmp #$0E
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x009F, bytearray([0xF0, 0x51])) # beq .map_pal
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00A1, bytearray([0xC9, 0x0D])) # cmp #$0D
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00A3, bytearray([0xD0, 0xF7])) # bne .skip
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00A5, bytearray([0xA5, 0x7C])) # lda $7C
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00A7, bytearray([0xD0, 0xF3])) # bne .skip
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00A9, bytearray([0xE6, 0x7C])) # inc $7C
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00AB, bytearray([0xC2, 0x20])) # rep #$20
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00AD, bytearray([0xA0, 0x80])) # ldy #$80
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00AF, bytearray([0x8C, 0x15, 0x21])) # sty $2115
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00B2, bytearray([0xA9, 0x01, 0x18])) # lda #$1801
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00B5, bytearray([0x8D, 0x20, 0x43])) # sta $4320
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00B8, bytearray([0xA0, 0x1C])) # ldy.b #$1C
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00BA, bytearray([0x8C, 0x24, 0x43])) # sty $4324
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00BD, bytearray([0xA9, 0x00, 0xE4])) # lda.w #$E400
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00C0, bytearray([0x8D, 0x22, 0x43])) # sta $4322
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00C3, bytearray([0xDA])) # phx
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00C4, bytearray([0x9B])) # txy
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00C5, bytearray([0xA2, 0x18])) # ldx.b #(.map_targets_end-.map_targets-1)*2
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00C7, bytearray([0xA9, 0x40, 0x00])) # ..loop lda #$0040
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00CA, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00CD, bytearray([0xBF, 0x80, 0xFF, 0x10])) # lda.l .map_targets,x
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00D1, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00D4, bytearray([0x8C, 0x0B, 0x42])) # sty $420B
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00D7, bytearray([0xBF, 0x80, 0xFF, 0x10])) # lda.l .map_targets,x
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00DB, bytearray([0x18])) # clc
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00DC, bytearray([0x69, 0x00, 0x01])) # adc #$0100
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00DF, bytearray([0x8D, 0x16, 0x21])) # sta $2116
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00E2, bytearray([0xA9, 0x40, 0x00])) # lda #$0040
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00E5, bytearray([0x8D, 0x25, 0x43])) # sta $4325
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00E8, bytearray([0x8C, 0x0B, 0x42])) # sty $420B
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00EB, bytearray([0xCA])) # dex
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00EC, bytearray([0xCA])) # dex
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00ED, bytearray([0x10, 0xD8])) # bpl .loop
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00EF, bytearray([0xFA])) # plx
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00F0, bytearray([0xE2, 0x20])) # sep #$20
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00F2, bytearray([0xA9, 0xA3])) # .map_pal lda #$A3
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00F4, bytearray([0x8D, 0x21, 0x21])) # sta $2121
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00F7, bytearray([0xAF, 0x9C, 0xB5, 0x00])) # lda $00B59C
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00FB, bytearray([0x8D, 0x22, 0x21])) # sta $2122
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x00FE, bytearray([0xAF, 0x9D, 0xB5, 0x00])) # lda $00B59D
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0102, bytearray([0x8D, 0x22, 0x21])) # sta $2122
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0105, bytearray([0xAF, 0x9E, 0xB5, 0x00])) # lda $00B59E
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0109, bytearray([0x8D, 0x22, 0x21])) # sta $2122
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x010C, bytearray([0xAF, 0x9F, 0xB5, 0x00])) # lda $00B59F
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0110, bytearray([0x8D, 0x22, 0x21])) # sta $2122
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0113, bytearray([0xAF, 0xA0, 0xB5, 0x00])) # lda $00B5A0
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0117, bytearray([0x8D, 0x22, 0x21])) # sta $2122
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x011A, bytearray([0xAF, 0xA1, 0xB5, 0x00])) # lda $00B5A1
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x011E, bytearray([0x8D, 0x22, 0x21])) # sta $2122
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0121, bytearray([0xAF, 0xA2, 0xB5, 0x00])) # lda $00B5A2
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0125, bytearray([0x8D, 0x22, 0x21])) # sta $2122
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0128, bytearray([0xAF, 0xA3, 0xB5, 0x00])) # lda $00B5A3
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x012C, bytearray([0x8D, 0x22, 0x21])) # sta $2122
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x012F, bytearray([0xAF, 0xA4, 0xB5, 0x00])) # lda $00B5A4
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0133, bytearray([0x8D, 0x22, 0x21])) # sta $2122
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x0136, bytearray([0xAF, 0xA5, 0xB5, 0x00])) # lda $00B5A5
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x013A, bytearray([0x8D, 0x22, 0x21])) # sta $2122
+ rom.write_bytes(UPLOAD_INDICATOR_GFX + 0x013D, bytearray([0x6B])) # rtl
+
+ vram_targets = bytearray([
+ 0x20,0x64, 0x00,0x64, 0xE0,0x62,
+ 0x60,0x66, 0x40,0x66,
+ 0x60,0x64,
+ 0x40,0x62, 0x00,0x62,
+ 0xE0,0x60, 0xC0,0x60, 0xA0,0x60, 0x80,0x60, 0x60,0x60
+ ])
+ rom.write_bytes(0x87F80, vram_targets)
+
+
+def handle_chocolate_island_2(rom):
+ FIX_CHOCOISLAND2_ADDR = 0x87200
+ rom.write_bytes(0x2DB3E, bytearray([0x5C, 0x00, 0xF2, 0x10])) # jml fix_choco_island_2
+ rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x0000, bytearray([0xAD, 0x33, 0x1F])) # fix_choco_island_2 lda $1F2F+$04
+ rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x0003, bytearray([0x29, 0x08])) # and #$08
+ rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x0005, bytearray([0xD0, 0x0D])) # bne .dc_room
+ rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x0007, bytearray([0xAD, 0x22, 0x14])) # lda $1422
+ rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x000A, bytearray([0xC9, 0x04])) # cmp #$04
+ rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x000C, bytearray([0xF0, 0x06])) # beq .dc_room
+ rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x000E, bytearray([0xA2, 0x02])) # .rex_room ldx #$02
+ rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x0010, bytearray([0x5C, 0x49, 0xDB, 0x05])) # jml $05DB49
+ rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x0014, bytearray([0xA2, 0x00])) # .dc_room ldx #$00
+ rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x0016, bytearray([0x5C, 0x49, 0xDB, 0x05])) # jml $05DB49
+
+
+def decompress_gfx(compressed_graphics):
+ # This code decompresses graphics in LC_LZ2 format in order to be able to swap player and yoshi's graphics with ease.
+ decompressed_gfx = bytearray([])
+ i = 0
+ while True:
+ cmd = compressed_graphics[i]
+ i += 1
+ if cmd == 0xFF:
+ break
+ else:
+ if (cmd >> 5) == 0x07:
+ size = ((cmd & 0x03) << 8) + compressed_graphics[i] + 1
+ cmd = (cmd & 0x1C) >> 2
+ i += 1
+ else:
+ size = (cmd & 0x1F) + 1
+ cmd = cmd >> 5
+ if cmd == 0x00:
+ decompressed_gfx += bytearray([compressed_graphics[i+j] for j in range(size)])
+ i += size
+ elif cmd == 0x01:
+ byte_fill = compressed_graphics[i]
+ i += 1
+ decompressed_gfx += bytearray([byte_fill for j in range(size)])
+ elif cmd == 0x02:
+ byte_fill_1 = compressed_graphics[i]
+ i += 1
+ byte_fill_2 = compressed_graphics[i]
+ i += 1
+ for j in range(size):
+ if (j & 0x1) == 0x00:
+ decompressed_gfx += bytearray([byte_fill_1])
+ else:
+ decompressed_gfx += bytearray([byte_fill_2])
+ elif cmd == 0x03:
+ byte_read = compressed_graphics[i]
+ i += 1
+ decompressed_gfx += bytearray([(byte_read + j) for j in range(size)])
+ elif cmd == 0x04:
+ position = (compressed_graphics[i] << 8) + compressed_graphics[i+1]
+ i += 2
+ for j in range(size):
+ copy_byte = decompressed_gfx[position+j]
+ decompressed_gfx += bytearray([copy_byte])
+ return decompressed_gfx
+
+
+def convert_3bpp(decompressed_gfx):
+ i = 0
+ converted_gfx = bytearray([])
+ while i < len(decompressed_gfx):
+ converted_gfx += bytearray([decompressed_gfx[i+j] for j in range(16)])
+ i += 16
+ for j in range(8):
+ converted_gfx += bytearray([decompressed_gfx[i]])
+ converted_gfx += bytearray([0x00])
+ i += 1
+ return converted_gfx
+
+
+def copy_gfx_tiles(original, order, size):
+ result = bytearray([])
+ for x in range(len(order)):
+ z = order[x] << size[0]
+ result += bytearray([original[z+y] for y in range(size[1])])
+ return result
+
+
+def file_to_bytes(filename):
+ return open(os.path.dirname(__file__)+filename, "rb").read()
+
+
+def handle_music_shuffle(rom, world: World):
from .Aesthetics import generate_shuffled_level_music, generate_shuffled_ow_music, level_music_address_data, ow_music_address_data
- shuffled_level_music = generate_shuffled_level_music(world, player)
+ shuffled_level_music = generate_shuffled_level_music(world)
for i in range(len(shuffled_level_music)):
rom.write_byte(level_music_address_data[i], shuffled_level_music[i])
- shuffled_ow_music = generate_shuffled_ow_music(world, player)
+ shuffled_ow_music = generate_shuffled_ow_music(world)
for i in range(len(shuffled_ow_music)):
for addr in ow_music_address_data[i]:
rom.write_byte(addr, shuffled_ow_music[i])
-def handle_mario_palette(rom, world, player):
+def handle_mario_palette(rom, world: World):
from .Aesthetics import mario_palettes, fire_mario_palettes, ow_mario_palettes
- chosen_palette = world.mario_palette[player].value
+ chosen_palette = world.options.mario_palette.value
rom.write_bytes(0x32C8, bytes(mario_palettes[chosen_palette]))
rom.write_bytes(0x32F0, bytes(fire_mario_palettes[chosen_palette]))
@@ -723,9 +2851,9 @@ def handle_swap_donut_gh_exits(rom):
rom.write_bytes(0x26371, bytes([0x32]))
-def handle_bowser_rooms(rom, world, player: int):
- if world.bowser_castle_rooms[player] == "random_two_room":
- chosen_rooms = world.per_slot_randoms[player].sample(standard_bowser_rooms, 2)
+def handle_bowser_rooms(rom, world: World):
+ if world.options.bowser_castle_rooms == "random_two_room":
+ chosen_rooms = world.random.sample(standard_bowser_rooms, 2)
rom.write_byte(0x3A680, chosen_rooms[0].roomID)
rom.write_byte(0x3A684, chosen_rooms[0].roomID)
@@ -737,8 +2865,8 @@ def handle_bowser_rooms(rom, world, player: int):
rom.write_byte(chosen_rooms[len(chosen_rooms)-1].exitAddress, 0xBD)
- elif world.bowser_castle_rooms[player] == "random_five_room":
- chosen_rooms = world.per_slot_randoms[player].sample(standard_bowser_rooms, 5)
+ elif world.options.bowser_castle_rooms == "random_five_room":
+ chosen_rooms = world.random.sample(standard_bowser_rooms, 5)
rom.write_byte(0x3A680, chosen_rooms[0].roomID)
rom.write_byte(0x3A684, chosen_rooms[0].roomID)
@@ -750,9 +2878,9 @@ def handle_bowser_rooms(rom, world, player: int):
rom.write_byte(chosen_rooms[len(chosen_rooms)-1].exitAddress, 0xBD)
- elif world.bowser_castle_rooms[player] == "gauntlet":
+ elif world.options.bowser_castle_rooms == "gauntlet":
chosen_rooms = standard_bowser_rooms.copy()
- world.per_slot_randoms[player].shuffle(chosen_rooms)
+ world.random.shuffle(chosen_rooms)
rom.write_byte(0x3A680, chosen_rooms[0].roomID)
rom.write_byte(0x3A684, chosen_rooms[0].roomID)
@@ -763,12 +2891,12 @@ def handle_bowser_rooms(rom, world, player: int):
rom.write_byte(chosen_rooms[i-1].exitAddress, chosen_rooms[i].roomID)
rom.write_byte(chosen_rooms[len(chosen_rooms)-1].exitAddress, 0xBD)
- elif world.bowser_castle_rooms[player] == "labyrinth":
+ elif world.options.bowser_castle_rooms == "labyrinth":
bowser_rooms_copy = full_bowser_rooms.copy()
entrance_point = bowser_rooms_copy.pop(0)
- world.per_slot_randoms[player].shuffle(bowser_rooms_copy)
+ world.random.shuffle(bowser_rooms_copy)
rom.write_byte(entrance_point.exitAddress, bowser_rooms_copy[0].roomID)
for i in range(0, len(bowser_rooms_copy) - 1):
@@ -777,13 +2905,13 @@ def handle_bowser_rooms(rom, world, player: int):
rom.write_byte(bowser_rooms_copy[len(bowser_rooms_copy)-1].exitAddress, 0xBD)
-def handle_boss_shuffle(rom, world, player):
- if world.boss_shuffle[player] == "simple":
+def handle_boss_shuffle(rom, world: World):
+ if world.options.boss_shuffle == "simple":
submap_boss_rooms_copy = submap_boss_rooms.copy()
ow_boss_rooms_copy = ow_boss_rooms.copy()
- world.per_slot_randoms[player].shuffle(submap_boss_rooms_copy)
- world.per_slot_randoms[player].shuffle(ow_boss_rooms_copy)
+ world.random.shuffle(submap_boss_rooms_copy)
+ world.random.shuffle(ow_boss_rooms_copy)
for i in range(len(submap_boss_rooms_copy)):
rom.write_byte(submap_boss_rooms[i].exitAddress, submap_boss_rooms_copy[i].roomID)
@@ -794,21 +2922,21 @@ def handle_boss_shuffle(rom, world, player):
if ow_boss_rooms[i].exitAddressAlt is not None:
rom.write_byte(ow_boss_rooms[i].exitAddressAlt, ow_boss_rooms_copy[i].roomID)
- elif world.boss_shuffle[player] == "full":
+ elif world.options.boss_shuffle == "full":
for i in range(len(submap_boss_rooms)):
- chosen_boss = world.per_slot_randoms[player].choice(submap_boss_rooms)
+ chosen_boss = world.random.choice(submap_boss_rooms)
rom.write_byte(submap_boss_rooms[i].exitAddress, chosen_boss.roomID)
for i in range(len(ow_boss_rooms)):
- chosen_boss = world.per_slot_randoms[player].choice(ow_boss_rooms)
+ chosen_boss = world.random.choice(ow_boss_rooms)
rom.write_byte(ow_boss_rooms[i].exitAddress, chosen_boss.roomID)
if ow_boss_rooms[i].exitAddressAlt is not None:
rom.write_byte(ow_boss_rooms[i].exitAddressAlt, chosen_boss.roomID)
- elif world.boss_shuffle[player] == "singularity":
- chosen_submap_boss = world.per_slot_randoms[player].choice(submap_boss_rooms)
- chosen_ow_boss = world.per_slot_randoms[player].choice(ow_boss_rooms)
+ elif world.options.boss_shuffle == "singularity":
+ chosen_submap_boss = world.random.choice(submap_boss_rooms)
+ chosen_ow_boss = world.random.choice(ow_boss_rooms)
for i in range(len(submap_boss_rooms)):
rom.write_byte(submap_boss_rooms[i].exitAddress, chosen_submap_boss.roomID)
@@ -820,8 +2948,8 @@ def handle_boss_shuffle(rom, world, player):
rom.write_byte(ow_boss_rooms[i].exitAddressAlt, chosen_ow_boss.roomID)
-def patch_rom(world, rom, player, active_level_dict):
- goal_text = generate_goal_text(world, player)
+def patch_rom(world: World, rom, player, active_level_dict):
+ goal_text = generate_goal_text(world)
rom.write_bytes(0x2A6E2, goal_text)
rom.write_byte(0x2B1D8, 0x80)
@@ -829,19 +2957,23 @@ def patch_rom(world, rom, player, active_level_dict):
intro_text = generate_text_box("Bowser has stolen all of Mario's abilities. Can you help Mario travel across Dinosaur land to get them back and save the Princess from him?")
rom.write_bytes(0x2A5D9, intro_text)
- handle_bowser_rooms(rom, world, player)
- handle_boss_shuffle(rom, world, player)
+ handle_bowser_rooms(rom, world)
+ handle_boss_shuffle(rom, world)
+
+ # Handle ROM expansion
+ rom.write_bytes(0x07FD7, bytearray([0x0A]))
+ rom.write_bytes(0x80000, bytearray([0x00 for _ in range(0x80000)]))
# Prevent Title Screen Deaths
rom.write_byte(0x1C6A, 0x80)
# Title Screen Text
player_name_bytes = bytearray()
- player_name = world.get_player_name(player)
+ player_name = world.multiworld.get_player_name(player)
for i in range(16):
char = " "
if i < len(player_name):
- char = world.get_player_name(player)[i]
+ char = player_name[i]
upper_char = char.upper()
if upper_char not in title_text_mapping:
for byte in title_text_mapping["."]:
@@ -869,33 +3001,58 @@ def patch_rom(world, rom, player, active_level_dict):
rom.write_bytes(0x2B88E, bytearray([0x2C, 0x31, 0x73, 0x31, 0x75, 0x31, 0x82, 0x30, 0x30, 0x31, 0xFC, 0x38, 0x31, 0x31, 0x73, 0x31,
0x73, 0x31, 0x7C, 0x30, 0xFC, 0x38, 0xFC, 0x38, 0xFC, 0x38])) # 1 Player Game
- rom.write_bytes(0x2B6D7, bytearray([0xFC, 0x38, 0xFC, 0x38, 0x16, 0x38, 0x18, 0x38, 0x0D, 0x38, 0xFC, 0x38, 0x0B, 0x38, 0x22, 0x38,
+ rom.write_bytes(0x2B6D7, bytearray([0x16, 0x38, 0x18, 0x38, 0x0D, 0x38, 0xFC, 0x38, 0x0B, 0x38, 0x22, 0x38,
0xFC, 0x38, 0x19, 0x38, 0x18, 0x38, 0x1B, 0x38, 0x22, 0x38, 0x10, 0x38, 0x18, 0x38, 0x17, 0x38,
- 0x0E, 0x38, 0xFC, 0x38, 0xFC, 0x38])) # Mod by PoryGone
+ 0x0E, 0x38, 0xFC, 0x38, 0x15, 0x38, 0x21, 0x38, 0x05, 0x38])) # Mod by PoryGone + lx5
# Title Options
rom.write_bytes(0x1E6A, bytearray([0x01]))
rom.write_bytes(0x1E6C, bytearray([0x01]))
rom.write_bytes(0x1E6E, bytearray([0x01]))
+ # Save current level number to RAM (not translevel)
+ rom.write_bytes(0x2D8B9, bytearray([0x20, 0x46, 0xDC])) # org $05D8B9 : jsr level_num
+ rom.write_bytes(0x2DC46 + 0x0000, bytearray([0xA5, 0x0E])) # level_num: lda $0E
+ rom.write_bytes(0x2DC46 + 0x0002, bytearray([0x8D, 0x0B, 0x01])) # sta $010B
+ rom.write_bytes(0x2DC46 + 0x0005, bytearray([0x0A])) # asl
+ rom.write_bytes(0x2DC46 + 0x0006, bytearray([0x60])) # rts
+
# Always allow Start+Select
rom.write_bytes(0x2267, bytearray([0xEA, 0xEA]))
# Always bring up save prompt on beating a level
- if world.autosave[player]:
+ if world.options.autosave:
rom.write_bytes(0x20F93, bytearray([0x00]))
- if world.overworld_speed[player] == "fast":
+ if world.options.overworld_speed == "fast":
rom.write_bytes(0x21414, bytearray([0x20, 0x10]))
- elif world.overworld_speed[player] == "slow":
+ elif world.options.overworld_speed == "slow":
rom.write_bytes(0x21414, bytearray([0x05, 0x05]))
# Starting Life Count
- rom.write_bytes(0x1E25, bytearray([world.starting_life_count[player].value - 1]))
+ rom.write_bytes(0x1E25, bytearray([world.options.starting_life_count.value - 1]))
# Repurpose Bonus Stars counter for Boss Token or Yoshi Eggs
rom.write_bytes(0x3F1AA, bytearray([0x00] * 0x20))
+ # Make bonus star counter go up to 255 (999 in theory, but can't load a 16-bit addr there)
+ rom.write_bytes(0x00F5B, bytearray([0x4C, 0x73, 0x8F]))
+ rom.write_byte(0x00F95, 0x08)
+ rom.write_byte(0x00F97, 0x0C)
+ rom.write_byte(0x00FAC, 0x02)
+ rom.write_byte(0x00F9E, 0x1D)
+ rom.write_byte(0x00FA5, 0x1D)
+ rom.write_byte(0x00FA8, 0x02)
+ rom.write_byte(0x00FB0, 0x1D)
+ rom.write_byte(0x00FB8, 0x02)
+ rom.write_byte(0x00FBE, 0x1D)
+ rom.write_byte(0x00FC2, 0x03)
+
+ # Move Dragon coins one spot to the left & fix tilemap
+ rom.write_byte(0x00FF0, 0xFE)
+ rom.write_byte(0x00C94, 0x3C)
+ rom.write_byte(0x00C9C, 0x38)
+
# Delete Routine that would copy Mario position data over repurposed Luigi save data
rom.write_bytes(0x20F9F, bytearray([0xEA] * 0x3D))
@@ -904,6 +3061,12 @@ def patch_rom(world, rom, player, active_level_dict):
rom.write_bytes(0x6EB1, bytearray([0xEA, 0xEA]))
rom.write_bytes(0x6EB4, bytearray([0xEA, 0xEA, 0xEA]))
+ # Move Thwimps tilemap to another spot in VRAM in order to make them global
+ rom.write_bytes(0x09C13, bytearray([0x7E, 0x7E, 0x7F, 0x7F]))
+ rom.write_byte(0x3F425, 0x32)
+
+ handle_chocolate_island_2(rom)
+
handle_ability_code(rom)
handle_yoshi_box(rom)
@@ -913,44 +3076,97 @@ def patch_rom(world, rom, player, active_level_dict):
handle_vertical_scroll(rom)
+ handle_ram(rom)
+ handle_bonus_block(rom)
+ handle_blocksanity(rom)
+
+ handle_uncompressed_player_gfx(rom)
+
+ # Handle Special Zone Clear flag
+ rom.write_bytes(0x02A74, bytearray([0x1E, 0x1F]))
+ rom.write_bytes(0x09826, bytearray([0x1E, 0x1F]))
+ rom.write_bytes(0x0B9CD, bytearray([0x1E, 0x1F]))
+ rom.write_bytes(0x12986, bytearray([0x1E, 0x1F]))
+ rom.write_bytes(0x62E0F, bytearray([0x1E, 0x1F]))
+
+ handle_indicators(rom)
+ handle_map_indicators(rom)
+
+ # Handle extra traps
+ handle_traps(rom)
+
+ # Mario Start! -> Player Start!
+ text_data_top_tiles = bytearray([
+ 0x00,0xFF,0x4D,0x4C,0x03,0x4D,0x5D,0xFF,0x4C,0x4B,
+ 0x4A,0x03,0x4E,0x01,0x00,0x02,0x00,0x4a,0x4E,0xFF
+ ])
+ text_data_top_props = bytearray([
+ 0x34,0x30,0x34,0x34,0x34,0x34,0x34,0x30,0x34,0x34,
+ 0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x30
+ ])
+ text_data_bottom_tiles = bytearray([
+ 0x10,0xFF,0x00,0x5C,0x13,0x00,0x5D,0xFF,0x5C,0x5B,
+ 0x00,0x13,0x5E,0x11,0x00,0x12,0x00,0x03,0x5E,0xFF
+ ])
+ text_data_bottom_props = bytearray([
+ 0x34,0x30,0xb4,0x34,0x34,0xb4,0xf4,0x30,0x34,0x34,
+ 0xB4,0x34,0x34,0x34,0xb4,0x34,0xb4,0xb4,0x34,0x30
+ ])
+
+ rom.write_bytes(0x010D1, text_data_top_tiles)
+ rom.write_bytes(0x01139, text_data_top_props)
+ rom.write_bytes(0x01105, text_data_bottom_tiles)
+ rom.write_bytes(0x0116A, text_data_bottom_props)
+
# Handle Level Shuffle
handle_level_shuffle(rom, active_level_dict)
# Handle Music Shuffle
- if world.music_shuffle[player] != "none":
- handle_music_shuffle(rom, world, player)
+ if world.options.music_shuffle != "none":
+ handle_music_shuffle(rom, world)
+
+ generate_shuffled_ow_palettes(rom, world)
- generate_shuffled_ow_palettes(rom, world, player)
+ generate_shuffled_header_data(rom, world)
- generate_shuffled_header_data(rom, world, player)
+ if world.options.level_palette_shuffle == "on_curated":
+ generate_curated_level_palette_data(rom, world)
- if world.swap_donut_gh_exits[player]:
+ if world.options.overworld_palette_shuffle == "on_curated":
+ generate_curated_map_palette_data(rom, world)
+
+ if world.options.sfx_shuffle != "none":
+ generate_shuffled_sfx(rom, world)
+
+ if world.options.swap_donut_gh_exits:
handle_swap_donut_gh_exits(rom)
- handle_mario_palette(rom, world, player)
+ handle_mario_palette(rom, world)
# Store all relevant option results in ROM
- rom.write_byte(0x01BFA0, world.goal[player].value)
- if world.goal[player].value == 0:
- rom.write_byte(0x01BFA1, world.bosses_required[player].value)
+ rom.write_byte(0x01BFA0, world.options.goal.value)
+ if world.options.goal.value == 0:
+ rom.write_byte(0x01BFA1, world.options.bosses_required.value)
else:
rom.write_byte(0x01BFA1, 0x7F)
- required_yoshi_eggs = max(math.floor(
- world.number_of_yoshi_eggs[player].value * (world.percentage_of_yoshi_eggs[player].value / 100.0)), 1)
+ required_yoshi_eggs = world.required_egg_count
rom.write_byte(0x01BFA2, required_yoshi_eggs)
- #rom.write_byte(0x01BFA3, world.display_sent_item_popups[player].value)
- rom.write_byte(0x01BFA4, world.display_received_item_popups[player].value)
- rom.write_byte(0x01BFA5, world.death_link[player].value)
- rom.write_byte(0x01BFA6, world.dragon_coin_checks[player].value)
- rom.write_byte(0x01BFA7, world.swap_donut_gh_exits[player].value)
+ #rom.write_byte(0x01BFA3, world.options.display_sent_item_popups.value)
+ rom.write_byte(0x01BFA4, world.options.display_received_item_popups.value)
+ rom.write_byte(0x01BFA5, world.options.death_link.value)
+ rom.write_byte(0x01BFA6, world.options.dragon_coin_checks.value)
+ rom.write_byte(0x01BFA7, world.options.swap_donut_gh_exits.value)
+ rom.write_byte(0x01BFA8, world.options.moon_checks.value)
+ rom.write_byte(0x01BFA9, world.options.hidden_1up_checks.value)
+ rom.write_byte(0x01BFAA, world.options.bonus_block_checks.value)
+ rom.write_byte(0x01BFAB, world.options.blocksanity.value)
from Utils import __version__
- rom.name = bytearray(f'SMW{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
+ rom.name = bytearray(f'SMW{__version__.replace(".", "")[0:3]}_{player}_{world.multiworld.seed:11}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, rom.name)
-
def get_base_rom_bytes(file_name: str = "") -> bytes:
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:
diff --git a/worlds/smw/Rules.py b/worlds/smw/Rules.py
index 82f22c3a34c1..a900b4fd20ec 100644
--- a/worlds/smw/Rules.py
+++ b/worlds/smw/Rules.py
@@ -2,19 +2,18 @@
from BaseClasses import MultiWorld
from .Names import LocationName, ItemName
-from worlds.AutoWorld import LogicMixin
+from worlds.AutoWorld import World
from worlds.generic.Rules import add_rule, set_rule
-def set_rules(world: MultiWorld, player: int):
+def set_rules(world: World):
- if world.goal[player] == "yoshi_egg_hunt":
- required_yoshi_eggs = max(math.floor(
- world.number_of_yoshi_eggs[player].value * (world.percentage_of_yoshi_eggs[player].value / 100.0)), 1)
+ if world.options.goal == "yoshi_egg_hunt":
+ required_yoshi_eggs = world.required_egg_count
- add_rule(world.get_location(LocationName.yoshis_house, player),
- lambda state: state.has(ItemName.yoshi_egg, player, required_yoshi_eggs))
+ add_rule(world.multiworld.get_location(LocationName.yoshis_house, world.player),
+ lambda state: state.has(ItemName.yoshi_egg, world.player, required_yoshi_eggs))
else:
- add_rule(world.get_location(LocationName.bowser, player), lambda state: state.has(ItemName.mario_carry, player))
+ add_rule(world.multiworld.get_location(LocationName.bowser, world.player), lambda state: state.has(ItemName.mario_carry, world.player))
- world.completion_condition[player] = lambda state: state.has(ItemName.victory, player)
+ world.multiworld.completion_condition[world.player] = lambda state: state.has(ItemName.victory, world.player)
diff --git a/worlds/smw/__init__.py b/worlds/smw/__init__.py
index 431287c32bef..97fc84f003a0 100644
--- a/worlds/smw/__init__.py
+++ b/worlds/smw/__init__.py
@@ -1,3 +1,4 @@
+import dataclasses
import os
import typing
import math
@@ -5,17 +6,19 @@
import threading
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
-from .Items import SMWItem, ItemData, item_table
-from .Locations import SMWLocation, all_locations, setup_locations, special_zone_level_names, special_zone_dragon_coin_names
-from .Options import smw_options
-from .Regions import create_regions, connect_regions
-from .Levels import full_level_list, generate_level_list, location_id_to_level_id
-from .Rules import set_rules
+from worlds.AutoWorld import WebWorld, World
from worlds.generic.Rules import add_rule, exclusion_rules
-from .Names import ItemName, LocationName
+
from .Client import SMWSNIClient
-from worlds.AutoWorld import WebWorld, World
+from .Items import SMWItem, ItemData, item_table, junk_table
+from .Levels import full_level_list, generate_level_list, location_id_to_level_id
+from .Locations import SMWLocation, all_locations, setup_locations, special_zone_level_names, special_zone_dragon_coin_names, special_zone_hidden_1up_names, special_zone_blocksanity_names
+from .Names import ItemName, LocationName
+from .Options import SMWOptions, smw_option_groups
+from .Presets import smw_options_presets
+from .Regions import create_regions, connect_regions
from .Rom import LocalRom, patch_rom, get_base_rom_path, SMWDeltaPatch
+from .Rules import set_rules
class SMWSettings(settings.Group):
@@ -39,9 +42,12 @@ class SMWWeb(WebWorld):
"setup/en",
["PoryGone"]
)
-
+
tutorials = [setup_en]
+ option_groups = smw_option_groups
+ options_presets = smw_options_presets
+
class SMWWorld(World):
"""
@@ -50,11 +56,14 @@ class SMWWorld(World):
lost all of his abilities. Can he get them back in time to save the Princess?
"""
game: str = "Super Mario World"
- option_definitions = smw_options
+
settings: typing.ClassVar[SMWSettings]
+
+ options_dataclass = SMWOptions
+ options: SMWOptions
+
topology_present = False
- data_version = 3
- required_client_version = (0, 3, 5)
+ required_client_version = (0, 4, 5)
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = all_locations
@@ -62,9 +71,9 @@ class SMWWorld(World):
active_level_dict: typing.Dict[int,int]
web = SMWWeb()
- def __init__(self, world: MultiWorld, player: int):
+ def __init__(self, multiworld: MultiWorld, player: int):
self.rom_name_available_event = threading.Event()
- super().__init__(world, player)
+ super().__init__(multiworld, player)
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld):
@@ -72,37 +81,34 @@ def stage_assert_generate(cls, multiworld: MultiWorld):
if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file)
- def _get_slot_data(self):
- return {
- #"death_link": self.multiworld.death_link[self.player].value,
- "active_levels": self.active_level_dict,
- }
-
def fill_slot_data(self) -> dict:
- slot_data = self._get_slot_data()
- for option_name in smw_options:
- option = getattr(self.multiworld, option_name)[self.player]
- slot_data[option_name] = option.value
+ slot_data = self.options.as_dict(
+ "dragon_coin_checks",
+ "moon_checks",
+ "hidden_1up_checks",
+ "bonus_block_checks",
+ "blocksanity",
+ )
+ slot_data["active_levels"] = self.active_level_dict
return slot_data
def generate_early(self):
- if self.multiworld.early_climb[self.player]:
+ if self.options.early_climb:
self.multiworld.local_early_items[self.player][ItemName.mario_climb] = 1
-
def create_regions(self):
- location_table = setup_locations(self.multiworld, self.player)
- create_regions(self.multiworld, self.player, location_table)
+ location_table = setup_locations(self)
+ create_regions(self, location_table)
# Not generate basic
itempool: typing.List[SMWItem] = []
- self.active_level_dict = dict(zip(generate_level_list(self.multiworld, self.player), full_level_list))
- self.topology_present = self.multiworld.level_shuffle[self.player]
+ self.active_level_dict = dict(zip(generate_level_list(self), full_level_list))
+ self.topology_present = self.options.level_shuffle
+
+ connect_regions(self, self.active_level_dict)
- connect_regions(self.multiworld, self.player, self.active_level_dict)
-
# Add Boss Token amount requirements for Worlds
add_rule(self.multiworld.get_region(LocationName.donut_plains_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 1))
add_rule(self.multiworld.get_region(LocationName.vanilla_dome_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 2))
@@ -110,18 +116,29 @@ def create_regions(self):
add_rule(self.multiworld.get_region(LocationName.chocolate_island_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 5))
add_rule(self.multiworld.get_region(LocationName.valley_of_bowser_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 6))
- if self.multiworld.exclude_special_zone[self.player]:
- exclusion_pool = set()
- if self.multiworld.dragon_coin_checks[self.player]:
- exclusion_pool.update(special_zone_level_names)
+ exclusion_pool = set()
+ if self.options.exclude_special_zone:
+ exclusion_pool.update(special_zone_level_names)
+ if self.options.dragon_coin_checks:
exclusion_pool.update(special_zone_dragon_coin_names)
- elif self.multiworld.number_of_yoshi_eggs[self.player].value <= 72:
- exclusion_pool.update(special_zone_level_names)
+ if self.options.hidden_1up_checks:
+ exclusion_pool.update(special_zone_hidden_1up_names)
+ if self.options.blocksanity:
+ exclusion_pool.update(special_zone_blocksanity_names)
+
exclusion_rules(self.multiworld, self.player, exclusion_pool)
total_required_locations = 96
- if self.multiworld.dragon_coin_checks[self.player]:
+ if self.options.dragon_coin_checks:
total_required_locations += 49
+ if self.options.moon_checks:
+ total_required_locations += 7
+ if self.options.hidden_1up_checks:
+ total_required_locations += 14
+ if self.options.bonus_block_checks:
+ total_required_locations += 4
+ if self.options.blocksanity:
+ total_required_locations += 582
itempool += [self.create_item(ItemName.mario_run)]
itempool += [self.create_item(ItemName.mario_carry)]
@@ -137,31 +154,53 @@ def create_regions(self):
itempool += [self.create_item(ItemName.green_switch_palace)]
itempool += [self.create_item(ItemName.red_switch_palace)]
itempool += [self.create_item(ItemName.blue_switch_palace)]
+ itempool += [self.create_item(ItemName.special_world_clear)]
- if self.multiworld.goal[self.player] == "yoshi_egg_hunt":
- itempool += [self.create_item(ItemName.yoshi_egg)
- for _ in range(self.multiworld.number_of_yoshi_eggs[self.player])]
+ if self.options.goal == "yoshi_egg_hunt":
+ raw_egg_count = total_required_locations - len(itempool) - len(exclusion_pool)
+ total_egg_count = min(raw_egg_count, self.options.max_yoshi_egg_cap.value)
+ self.required_egg_count = max(math.floor(total_egg_count * (self.options.percentage_of_yoshi_eggs.value / 100.0)), 1)
+ extra_egg_count = total_egg_count - self.required_egg_count
+ removed_egg_count = math.floor(extra_egg_count * (self.options.junk_fill_percentage.value / 100.0))
+ self.actual_egg_count = total_egg_count - removed_egg_count
+
+ itempool += [self.create_item(ItemName.yoshi_egg) for _ in range(self.actual_egg_count)]
+
self.multiworld.get_location(LocationName.yoshis_house, self.player).place_locked_item(self.create_item(ItemName.victory))
else:
+ self.actual_egg_count = 0
+ self.required_egg_count = 0
+
self.multiworld.get_location(LocationName.bowser, self.player).place_locked_item(self.create_item(ItemName.victory))
junk_count = total_required_locations - len(itempool)
trap_weights = []
- trap_weights += ([ItemName.ice_trap] * self.multiworld.ice_trap_weight[self.player].value)
- trap_weights += ([ItemName.stun_trap] * self.multiworld.stun_trap_weight[self.player].value)
- trap_weights += ([ItemName.literature_trap] * self.multiworld.literature_trap_weight[self.player].value)
- trap_weights += ([ItemName.timer_trap] * self.multiworld.timer_trap_weight[self.player].value)
- trap_count = 0 if (len(trap_weights) == 0) else math.ceil(junk_count * (self.multiworld.trap_fill_percentage[self.player].value / 100.0))
+ trap_weights += ([ItemName.ice_trap] * self.options.ice_trap_weight.value)
+ trap_weights += ([ItemName.stun_trap] * self.options.stun_trap_weight.value)
+ trap_weights += ([ItemName.literature_trap] * self.options.literature_trap_weight.value)
+ trap_weights += ([ItemName.timer_trap] * self.options.timer_trap_weight.value)
+ trap_weights += ([ItemName.reverse_controls_trap] * self.options.reverse_trap_weight.value)
+ trap_weights += ([ItemName.thwimp_trap] * self.options.thwimp_trap_weight.value)
+ trap_count = 0 if (len(trap_weights) == 0) else math.ceil(junk_count * (self.options.trap_fill_percentage.value / 100.0))
junk_count -= trap_count
trap_pool = []
for i in range(trap_count):
- trap_item = self.multiworld.random.choice(trap_weights)
+ trap_item = self.random.choice(trap_weights)
trap_pool.append(self.create_item(trap_item))
itempool += trap_pool
- itempool += [self.create_item(ItemName.one_up_mushroom) for _ in range(junk_count)]
+ junk_weights = []
+ junk_weights += ([ItemName.one_coin] * 15)
+ junk_weights += ([ItemName.five_coins] * 15)
+ junk_weights += ([ItemName.ten_coins] * 25)
+ junk_weights += ([ItemName.fifty_coins] * 25)
+ junk_weights += ([ItemName.one_up_mushroom] * 20)
+
+ junk_pool = [self.create_item(self.random.choice(junk_weights)) for _ in range(junk_count)]
+
+ itempool += junk_pool
boss_location_names = [LocationName.yoshis_island_koopaling, LocationName.donut_plains_koopaling, LocationName.vanilla_dome_koopaling,
LocationName.twin_bridges_koopaling, LocationName.forest_koopaling, LocationName.chocolate_koopaling,
@@ -176,18 +215,18 @@ def create_regions(self):
def generate_output(self, output_directory: str):
rompath = "" # if variable is not declared finally clause may fail
try:
- world = self.multiworld
+ multiworld = self.multiworld
player = self.player
rom = LocalRom(get_base_rom_path())
- patch_rom(self.multiworld, rom, self.player, self.active_level_dict)
+ patch_rom(self, rom, self.player, self.active_level_dict)
rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")
rom.write_to_file(rompath)
self.rom_name = rom.name
patch = SMWDeltaPatch(os.path.splitext(rompath)[0]+SMWDeltaPatch.patch_file_ending, player=player,
- player_name=world.player_name[player], patched_path=rompath)
+ player_name=multiworld.player_name[player], patched_path=rompath)
patch.write()
except:
raise
@@ -243,7 +282,15 @@ def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, s
if level_index >= world_cutoffs[i]:
continue
- if self.multiworld.dragon_coin_checks[self.player].value == 0 and "Dragon Coins" in loc_name:
+ if not self.options.dragon_coin_checks and "Dragon Coins" in loc_name:
+ continue
+ if not self.options.moon_checks and "3-Up Moon" in loc_name:
+ continue
+ if not self.options.hidden_1up_checks and "Hidden 1-Up" in loc_name:
+ continue
+ if not self.options.bonus_block_checks and "1-Up from Bonus Block" in loc_name:
+ continue
+ if not self.options.blocksanity and "Block #" in loc_name:
continue
location = self.multiworld.get_location(loc_name, self.player)
@@ -271,7 +318,7 @@ def create_item(self, name: str, force_non_progression=False) -> Item:
return created_item
def get_filler_item_name(self) -> str:
- return ItemName.one_up_mushroom
+ return self.random.choice(list(junk_table.keys()))
def set_rules(self):
- set_rules(self.multiworld, self.player)
+ set_rules(self)
diff --git a/worlds/smw/data/blocksanity.json b/worlds/smw/data/blocksanity.json
new file mode 100644
index 000000000000..e3737d25978d
--- /dev/null
+++ b/worlds/smw/data/blocksanity.json
@@ -0,0 +1,747 @@
+{
+ "000_bonus": [],
+ "001_vanilla_secret_2": [
+ ["yoshi", "0170", "0130", []],
+ ["green", "02F0", "0170", ["greenswitch carry", "greenswitch cape"]],
+ ["power", "0660", "0110", []],
+ ["power", "0B70", "0100", []],
+ ["multi", "0DC0", "0120", []],
+ ["gray", "0E70", "0120", []],
+ ["single", "1180", "0130", []],
+ ["single", "1190", "0130", []],
+ ["single", "11A0", "0130", []],
+ ["single", "11B0", "0130", []],
+ ["single", "11C0", "0130", []],
+ ["single", "11D0", "0130", []]
+ ],
+ "002_vanilla_secret_3": [
+ ["power", "0270", "00D0", ["swim"]],
+ ["power", "06E0", "00E0", ["swim"]]
+ ],
+ "003_top_secret_area": [],
+ "004_donut_ghost_house": [
+ ["vine", "0120", "0120", []],
+ ["dir", "0070", "0140", ["pswitch"]],
+ ["life", "0610", "0140", ["run cape"]],
+ ["life", "0640", "0140", ["run cape"]],
+ ["life", "0670", "0140", ["run cape"]],
+ ["life", "06A0", "0140", ["run cape"]]
+ ],
+ "005_donut_plains_3": [
+ ["green", "01B0", "00E0", ["greenswitch"]],
+ ["single", "0450", "00F0", []],
+ ["single", "0480", "00F0", []],
+ ["vine", "04E0", "0130", ["mushroom spin"]],
+ ["power", "0BD0", "0140", []],
+ ["bonus", "1250", "00F0", []]
+ ],
+ "006_donut_plains_4": [
+ ["single", "0660", "0130", []],
+ ["power", "0670", "0130", []],
+ ["single", "0680", "0130", []],
+ ["yoshi", "0AF0", "0150", []]
+ ],
+ "007_donut_plains_castle": [
+ ["yellow", "01E0", "00C0", ["yellowswitch"]],
+ ["single", "00A0", "0680", []],
+ ["power", "00B0", "0680", []],
+ ["single", "00C0", "0680", []],
+ ["vine", "0050", "0450", []],
+ ["inlife", "0030", "0320", ["climb"]],
+ ["single", "0050", "0250", []],
+ ["single", "0080", "0250", []],
+ ["single", "00B0", "0250", []],
+ ["green", "0090", "0060", ["greenswitch"]]
+ ],
+ "008_green_switch_palace": [],
+ "009_donut_plains_2": [
+ ["single", "00D0", "0120", []],
+ ["single", "00E0", "0120", []],
+ ["single", "00F0", "0120", []],
+ ["yellow", "0100", "0120", ["yellowswitch"]],
+ ["power", "0330", "00D0", []],
+ ["multi", "03C0", "00C0", []],
+ ["fly", "0820", "00E0", []],
+ ["green", "0560", "00E0", ["greenswitch"]],
+ ["yellow", "0050", "0140", ["yellowswitch"]],
+ ["vine", "02B0", "00E0", ["carry spin mushroom", "yoshi"]]
+ ],
+ "00A_donut_secret_1": [
+ ["single", "02C0", "0130", ["swim"]],
+ ["single", "02D0", "0130", ["swim"]],
+ ["power", "02E0", "0130", ["swim"]],
+ ["single", "02F0", "0130", ["swim"]],
+ ["power", "00E0", "0480", ["swim"]],
+ ["power", "0060", "0250", ["swim balloon"]],
+ ["life", "0110", "0070", ["swim balloon"]],
+ ["power", "01A0", "0250", ["swim balloon"]],
+ ["power", "0570", "0150", ["swim"]],
+ ["key", "0940", "0150", ["swim carry pswitch"]]
+ ],
+ "00B_vanilla_fortress": [
+ ["power", "04E0", "0130", ["swim"]],
+ ["power", "0220", "0130", ["swim"]],
+ ["yellow", "0780", "0110", ["yellowswitch swim"]]
+ ],
+ "00C_butter_bridge_1": [
+ ["power", "08A0", "0110", []],
+ ["multi", "08B0", "00D0", []],
+ ["multi", "0860", "0090", []],
+ ["multi", "08E0", "0050", []],
+ ["life", "0840", "0050", []],
+ ["bonus", "0BD0", "0130", []]
+ ],
+ "00D_butter_bridge_2": [
+ ["power", "0310", "0100", ["carry"]],
+ ["green", "0AC0", "0120", ["greenswitch"]],
+ ["yoshi", "0C70", "0110", ["carry"]]
+ ],
+ "00E_twin_bridges_castle": [
+ ["power", "01B0", "0370", ["climb"]]
+ ],
+ "00F_cheese_bridge": [
+ ["power", "00C0", "0140", []],
+ ["power", "0560", "00E0", []],
+ ["wings", "0A10", "0140", []],
+ ["power", "0B60", "0150", []]
+ ],
+ "010_cookie_mountain": [
+ ["single", "01C0", "0130", []],
+ ["single", "01D0", "0130", []],
+ ["single", "01E0", "0130", []],
+ ["single", "01F0", "0130", []],
+ ["single", "0200", "0130", []],
+ ["single", "0210", "0130", []],
+ ["single", "0220", "0130", []],
+ ["single", "0230", "0130", []],
+ ["single", "0240", "0130", []],
+ ["power", "0200", "00F0", []],
+ ["life", "0A40", "0070", ["climb", "swim"]],
+ ["vine", "0B20", "0140", []],
+ ["yoshi", "0C40", "0140", ["redswitch"]],
+ ["single", "11C0", "0140", []],
+ ["single", "11D0", "0140", []],
+ ["power", "11E0", "0140", []],
+ ["single", "11F0", "0140", []],
+ ["single", "1200", "0140", []],
+ ["single", "1210", "0140", []],
+ ["single", "1220", "0140", []],
+ ["single", "1230", "0140", []],
+ ["single", "1240", "0140", []],
+ ["single", "1250", "0140", []],
+ ["single", "11B0", "0100", []],
+ ["single", "11C0", "0100", []],
+ ["single", "11D0", "0100", []],
+ ["single", "11E0", "0100", []],
+ ["single", "11F0", "0100", []],
+ ["single", "1200", "0100", []],
+ ["single", "1210", "0100", []],
+ ["single", "1220", "0100", []],
+ ["single", "1230", "0100", []],
+ ["single", "1240", "0100", []],
+ ["single", "1250", "0100", []],
+ ["single", "1360", "0140", []]
+ ],
+ "011_soda_lake": [
+ ["power", "0200", "0110", ["swim"]]
+ ],
+ "012_test": [],
+ "013_donut_secret_house": [
+ ["power", "0480", "0140", []],
+ ["multi", "0310", "0140", []],
+ ["life", "04A0", "0140", ["pswitch"]],
+ ["vine", "01E0", "0110", ["pswitch"]],
+ ["dir", "0180", "0130", ["pswitch"]]
+ ],
+ "014_yellow_switch_palace": [],
+ "015_donut_plains_1": [
+ ["single", "0710", "0140", []],
+ ["single", "0720", "0140", []],
+ ["yoshi", "0D20", "00F0", []],
+ ["vine", "0DB0", "0130", []],
+ ["green", "11A0", "0070", ["greenswitch cape"]],
+ ["green", "11A0", "0080", ["greenswitch cape"]],
+ ["green", "11A0", "0090", ["greenswitch cape"]],
+ ["green", "11A0", "00A0", ["greenswitch cape"]],
+ ["green", "11A0", "00B0", ["greenswitch cape"]],
+ ["green", "11A0", "00C0", ["greenswitch cape"]],
+ ["green", "11A0", "00D0", ["greenswitch cape"]],
+ ["green", "11A0", "00E0", ["greenswitch cape"]],
+ ["green", "11A0", "00F0", ["greenswitch cape"]],
+ ["green", "11A0", "0100", ["greenswitch cape"]],
+ ["green", "11A0", "0110", ["greenswitch cape"]],
+ ["green", "11A0", "0120", ["greenswitch cape"]],
+ ["green", "11A0", "0130", ["greenswitch cape"]],
+ ["green", "11A0", "0140", ["greenswitch cape"]],
+ ["green", "11A0", "0150", ["greenswitch cape"]],
+ ["green", "11A0", "0160", ["greenswitch cape"]],
+ ["yellow", "1240", "0120", ["yellowswitch"]],
+ ["yellow", "1280", "0140", ["yellowswitch"]],
+ ["yellow", "1290", "0140", ["yellowswitch"]]
+ ],
+ "016_test": [],
+ "017_test": [],
+ "018_sunken_ghost_ship": [
+ ["power", "0110", "0070", ["swim"]],
+ ["star", "0100", "0C80", ["swim"]]
+ ],
+ "019_test": [],
+ "01A_chocolate_castle": [
+ ["yellow", "09C0", "0110", ["yellowswitch"]],
+ ["yellow", "0150", "0110", ["yellowswitch"]],
+ ["green", "0750", "0140", ["greenswitch"]]
+ ],
+ "01B_chocolate_fortress": [
+ ["power", "0380", "0140", []],
+ ["power", "0360", "0150", []],
+ ["single", "0370", "0150", []],
+ ["single", "0380", "0150", []],
+ ["green", "0AC0", "0130", ["greenswitch"]]
+ ],
+ "01C_chocolate_island_5": [
+ ["yoshi", "0170", "0130", []],
+ ["power", "0180", "0150", []],
+ ["life", "0340", "0170", ["carry", "cape"]],
+ ["yellow", "0560", "0140", ["yellowswitch pswitch"]]
+ ],
+ "01D_chocolate_island_4": [
+ ["yellow", "0700", "0140", ["yellowswitch blueswitch"]],
+ ["pow", "0920", "00A0", []],
+ ["power", "0B50", "0040", []]
+ ],
+ "01E_test": [],
+ "01F_forest_fortress": [
+ ["yellow", "02B0", "00E0", ["yellowswitch"]],
+ ["power", "0750", "00D0", []],
+ ["life", "0ED0", "0140", ["run cape"]],
+ ["life", "0F10", "0140", ["run cape"]],
+ ["life", "0F10", "0100", ["run cape"]],
+ ["life", "0F40", "0130", ["run cape"]],
+ ["life", "0F70", "0140", ["run cape"]],
+ ["life", "0F70", "00F0", ["run cape"]],
+ ["life", "0FA0", "0130", ["run cape"]],
+ ["life", "0FD0", "0140", ["run cape"]],
+ ["life", "0FD0", "0100", ["run cape"]]
+ ],
+ "020_forest_castle": [
+ ["green", "0CC0", "0120", ["greenswitch"]]
+ ],
+ "021_chocolate_ghost_house": [
+ ["power", "0910", "0140", []],
+ ["power", "0110", "0140", []],
+ ["life", "05D0", "0140", []]
+ ],
+ "022_chocolate_island_1": [
+ ["fly", "0490", "0120", ["pswitch"]],
+ ["fly", "0CD0", "0100", ["pswitch"]],
+ ["yoshi", "0E10", "0110", ["pswitch"]],
+ ["green", "0F00", "0140", ["greenswitch blueswitch", "greenswitch cape", "yellowswitch blueswitch", "yellowswitch cape"]],
+ ["life", "0070", "0120", ["pswitch"]]
+ ],
+ "023_chocolate_island_3": [
+ ["power", "0530", "0140", []],
+ ["power", "0A20", "0140", []],
+ ["power", "0F50", "00F0", []],
+ ["green", "1080", "00F0", ["greenswitch"]],
+ ["bonus", "11D0", "0140", []],
+ ["vine", "1220", "0140", []],
+ ["life", "1640", "0140", ["run cape"]],
+ ["life", "1670", "0140", ["run cape"]],
+ ["life", "16A0", "0140", ["run cape"]]
+ ],
+ "024_chocolate_island_2": [
+ ["multi", "00E0", "00A0", []],
+ ["invis", "00F0", "00D0", []],
+ ["yoshi", "0280", "0040", []],
+ ["single", "0080", "0140", []],
+ ["single", "05C0", "0140", []],
+ ["multi" , "05F0", "0120", []],
+ ["power", "0620", "0100", []],
+ ["pow", "0040", "0140", []],
+ ["yellow", "0190", "0110", ["yellowswitch"]],
+ ["yellow", "01A0", "0110", ["yellowswitch"]],
+ ["green", "0240", "0110", ["greenswitch"]],
+ ["green", "0250", "0110", ["greenswitch"]],
+ ["green", "0260", "0110", ["greenswitch"]],
+ ["green", "0270", "0110", ["greenswitch"]],
+ ["green", "0280", "0110", ["greenswitch"]],
+ ["green", "0290", "0110", ["greenswitch"]]
+ ],
+ "101_yoshis_island_castle": [
+ ["single", "0280", "00F0", ["climb"]],
+ ["single", "0290", "00F0", ["climb"]],
+ ["power", "02A0", "00F0", ["climb"]],
+ ["single", "02B0", "00F0", ["climb"]],
+ ["single", "02C0", "00F0", ["climb"]],
+ ["fly", "0250", "0150", ["climb"]]
+ ],
+ "102_yoshis_island_4": [
+ ["yellow", "00D0", "00F0", ["yellowswitch"]],
+ ["power", "0160", "0140", []],
+ ["multi", "0290", "0140", []],
+ ["star", "04E0", "0120", []]
+ ],
+ "103_yoshis_island_3": [
+ ["yellow", "0250", "00C0", ["yellowswitch"]],
+ ["yellow", "0290", "0140", ["yellowswitch"]],
+ ["yellow", "02F0", "00E0", ["yellowswitch carry", "yellowswitch yoshi", "yellowswitch run cape"]],
+ ["yellow", "0300", "00E0", ["yellowswitch carry", "yellowswitch yoshi", "yellowswitch run cape"]],
+ ["yellow", "0310", "00E0", ["yellowswitch carry", "yellowswitch yoshi", "yellowswitch run cape"]],
+ ["yellow", "0320", "00E0", ["yellowswitch carry", "yellowswitch yoshi", "yellowswitch run cape"]],
+ ["yellow", "0330", "00E0", ["yellowswitch carry", "yellowswitch yoshi", "yellowswitch run cape"]],
+ ["yellow", "0340", "00E0", ["yellowswitch carry", "yellowswitch yoshi", "yellowswitch run cape"]],
+ ["yellow", "0350", "00E0", ["yellowswitch carry", "yellowswitch yoshi", "yellowswitch run cape"]],
+ ["single", "04B0", "00A0", []],
+ ["yoshi", "04C0", "00A0", []],
+ ["single", "0AC0", "0140", []],
+ ["power", "0B00", "00C0", []],
+ ["yellow", "0CD0", "0120", ["yellowswitch"]],
+ ["yellow", "0CE0", "0120", ["yellowswitch"]],
+ ["yellow", "0DA0", "00F0", ["yellowswitch"]],
+ ["bonus", "10A0", "0080", []]
+ ],
+ "104_yoshis_house": [],
+ "105_yoshis_island_1": [
+ ["fly", "0250", "0140", []],
+ ["yellow", "09C0", "0140", ["yellowswitch"]],
+ ["life", "0D10", "00F0", []],
+ ["power", "0F30", "0110", []]
+ ],
+ "106_yoshis_island_2": [
+ ["fly", "0080", "00F0", ["carry", "yoshi"]],
+ ["fly", "00C0", "00E0", ["carry", "yoshi"]],
+ ["fly", "0130", "00F0", ["carry", "yoshi"]],
+ ["fly", "0140", "00D0", ["carry", "yoshi"]],
+ ["fly", "0180", "0100", ["carry", "yoshi"]],
+ ["fly", "01C0", "00E0", ["carry", "yoshi"]],
+ ["single", "0260", "0140", []],
+ ["yellow", "0270", "0140", ["yellowswitch"]],
+ ["single", "0280", "0140", []],
+ ["single", "0350", "0150", []],
+ ["yoshi", "0360", "0150", []],
+ ["single", "0B20", "0150", []],
+ ["yoshi", "0B30", "0150", []],
+ ["single", "0B40", "0150", []],
+ ["vine", "0C80", "0100", []],
+ ["yellow", "0DF0", "0120", ["yellowswitch"]]
+ ],
+ "107_vanilla_ghost_house": [
+ ["power", "0200", "0100", []],
+ ["vine", "0750", "0150", []],
+ ["power", "0860", "0140", []],
+ ["multi", "0A00", "0140", []],
+ ["pow", "05E0", "0120", []]
+ ],
+ "108_test": [],
+ "109_vanilla_secret_1": [
+ ["single", "0030", "0590", []],
+ ["power", "0040", "0590", []],
+ ["multi", "0110", "0490", []],
+ ["vine", "01B0", "0520", []],
+ ["vine", "0180", "0470", ["climb"]],
+ ["single", "0190", "0350", ["climb"]],
+ ["single", "01A0", "0350", ["climb"]],
+ ["power", "01B0", "0350", ["climb"]]
+ ],
+ "10A_vanilla_dome_3": [
+ ["single", "01C0", "0140", []],
+ ["fly", "0200", "0160", []],
+ ["fly", "0230", "0120", []],
+ ["power", "02D0", "0110", []],
+ ["fly", "0480", "0150", []],
+ ["invis", "0800", "0130", []],
+ ["power", "0830", "0130", []],
+ ["multi", "0D90", "0120", []],
+ ["power", "03D0", "0130", []],
+ ["yoshi", "10A0", "0100", ["carry", "yoshi"]],
+ ["power", "1A60", "0140", []],
+ ["pswitch", "1E40", "0090", ["run cape pswitch"]],
+ ["pswitch", "1E50", "00A0", ["run cape pswitch"]],
+ ["pswitch", "1E60", "00B0", ["run cape pswitch"]],
+ ["pswitch", "1E70", "00C0", ["run cape pswitch"]],
+ ["pswitch", "1E80", "00D0", ["run cape pswitch"]],
+ ["pswitch", "1E90", "00E0", ["run cape pswitch"]]
+ ],
+ "10B_donut_secret_2": [
+ ["dir", "00A0", "0170", []],
+ ["vine", "0220", "0100", []],
+ ["star", "0250", "0040", ["climb", "yoshi"]],
+ ["power", "0060", "0140", []],
+ ["star", "0B00", "0140", []]
+ ],
+ "10C_test": [],
+ "10D_front_door": [],
+ "10E_back_door": [],
+ "10F_valley_of_bowser_4": [
+ ["yellow", "0370", "0130", ["yellowswitch"]],
+ ["power", "0210", "0130", []],
+ ["vine", "05E0", "0110", []],
+ ["yoshi", "0610", "0040", ["climb"]],
+ ["life", "07A0", "00D0", ["mushroom spin climb"]],
+ ["power", "0B60", "0110", ["yellowswitch climb"]]
+ ],
+ "110_valley_castle": [
+ ["yellow", "0290", "0030", ["yellowswitch"]],
+ ["yellow", "0330", "0110", ["yellowswitch"]],
+ ["green", "0980", "0140", ["greenswitch"]]
+ ],
+ "111_valley_fortress": [
+ ["green", "0220", "0140", ["greenswitch"]],
+ ["yellow", "0940", "0100", ["yellowswitch"]]
+ ],
+ "112_test": [],
+ "113_valley_of_bowser_3": [
+ ["power", "0130", "0110", []],
+ ["power", "08A0", "00E0", []]
+ ],
+ "114_valley_ghost_house": [
+ ["pswitch", "0160", "0100", ["pswitch"]],
+ ["multi", "0570", "0110", ["pswitch"]],
+ ["power", "00E0", "0100", []],
+ ["dir", "0270", "0140", ["pswitch"]]
+ ],
+ "115_valley_of_bowser_2": [
+ ["power", "0330", "0130", []],
+ ["yellow", "0720", "0140", ["yellowswitch"]],
+ ["power", "0010", "00A0", []],
+ ["wings", "00D0", "0140", []]
+ ],
+ "116_valley_of_bowser_1": [
+ ["green", "0810", "0140", ["greenswitch"]],
+ ["invis", "0D40", "0100", []],
+ ["invis", "0D50", "0100", []],
+ ["invis", "0D60", "0100", []],
+ ["yellow", "0D60", "0080", ["yellowswitch cape"]],
+ ["yellow", "0D60", "0090", ["yellowswitch cape"]],
+ ["yellow", "0D60", "00A0", ["yellowswitch cape"]],
+ ["yellow", "0D60", "00B0", ["yellowswitch cape"]],
+ ["vine", "0F20", "0120", []]
+ ],
+ "117_chocolate_secret": [
+ ["power", "04A0", "0120", []],
+ ["power", "0960", "0140", []]
+ ],
+ "118_vanilla_dome_2": [
+ ["single", "0240", "0100", ["swim"]],
+ ["power", "0250", "0100", ["swim"]],
+ ["single", "0260", "0100", ["swim"]],
+ ["single", "0270", "0100", ["swim"]],
+ ["vine", "03B0", "0100", ["swim"]],
+ ["inlife", "0720", "00F0", ["swim climb", "swim yoshi"]],
+ ["single", "0760", "00F0", ["swim climb", "swim yoshi"]],
+ ["single", "0770", "00F0", ["swim climb", "swim yoshi"]],
+ ["power", "0780", "00F0", ["swim climb", "swim yoshi"]],
+ ["power", "0880", "0100", ["swim climb", "swim yoshi"]],
+ ["power", "0730", "0040", ["swim climb", "swim yoshi"]],
+ ["power", "0D10", "0100", ["swim climb", "swim yoshi"]],
+ ["multi", "0290", "0130", ["swim climb", "swim yoshi"]],
+ ["multi", "1150", "0140", ["swim climb", "swim yoshi"]]
+ ],
+ "119_vanilla_dome_4": [
+ ["power", "0690", "0100", []],
+ ["power", "0CB0", "0140", []],
+ ["single", "0E10", "0120", []],
+ ["single", "0E20", "0120", []],
+ ["single", "0E30", "0120", []],
+ ["life", "0E40", "0120", []],
+ ["single", "0E50", "0120", []],
+ ["single", "0E60", "0120", []],
+ ["single", "0E70", "0120", []],
+ ["single", "0E80", "0120", []],
+ ["single", "0E90", "0120", ["carry"]]
+ ],
+ "11A_vanilla_dome_1": [
+ ["fly", "0250", "0110", []],
+ ["power", "0400", "0120", []],
+ ["power", "0450", "00E0", []],
+ ["single", "0460", "0120", []],
+ ["life", "04D0", "0120", []],
+ ["power", "0640", "0180", []],
+ ["vine", "0680", "00E0", ["carry", "redswitch"]],
+ ["star", "00F0", "00E0", []],
+ ["power", "13A0", "0140", ["run star", "run mushroom"]],
+ ["single", "17D0", "0150", ["run star", "run mushroom"]]
+ ],
+ "11B_red_switch_palace": [],
+ "11C_vanilla_dome_castle": [
+ ["life", "0110", "0100", ["carry", "mushroom"]],
+ ["life", "0210", "0100", ["carry", "mushroom"]],
+ ["power", "03A0", "0130", []],
+ ["life", "0170", "0140", ["pswitch"]],
+ ["green", "0B90", "0140", ["greenswitch"]]
+ ],
+ "11D_forest_ghost_house": [
+ ["single", "0950", "0110", []],
+ ["power", "0990", "0110", []],
+ ["fly", "0190", "0150", []],
+ ["power", "0370", "0140", []],
+ ["life", "0640", "0160", []]
+ ],
+ "11E_forest_of_illusion_1": [
+ ["power", "01A0", "0110", []],
+ ["yoshi", "0360", "0130", []],
+ ["power", "0FA0", "0150", []],
+ ["key", "0E00", "0160", ["pballoon"]],
+ ["life", "0610", "0130", []]
+ ],
+ "11F_forest_of_illusion_4": [
+ ["multi", "0540", "0140", []],
+ ["single", "05E0", "0140", []],
+ ["single", "05F0", "0140", []],
+ ["single", "0600", "0140", []],
+ ["single", "0620", "0140", []],
+ ["power", "0630", "0140", []],
+ ["single", "0640", "0140", []],
+ ["single", "0E30", "0140", []],
+ ["single", "0E40", "0140", []],
+ ["power", "0E40", "0100", []],
+ ["single", "0EF0", "0140", []],
+ ["single", "0F00", "0140", []],
+ ["single", "0EF0", "0100", []]
+ ],
+ "120_forest_of_illusion_2": [
+ ["green", "0070", "0130", ["greenswitch swim"]],
+ ["power", "0480", "0040", ["swim"]],
+ ["invis", "0600", "0120", ["swim"]],
+ ["invis", "0610", "0120", ["swim"]],
+ ["inlife", "0620", "0120", ["swim"]],
+ ["invis", "0630", "0120", ["swim"]],
+ ["yellow", "0950", "0160", ["yellowswitch swim"]]
+ ],
+ "121_blue_switch_palace": [],
+ "122_forest_secret": [
+ ["power", "0330", "00A0", []],
+ ["power", "0450", "0110", []],
+ ["life", "06A0", "00B0", ["blueswitch", "carry"]]
+ ],
+ "123_forest_of_illusion_3": [
+ ["yoshi", "0350", "0150", []],
+ ["single", "04C0", "0150", []],
+ ["multi", "04D0", "0140", []],
+ ["single", "04F0", "0120", ["carry", "yoshi"]],
+ ["multi", "0D90", "0110", ["carry", "yoshi"]],
+ ["single", "0D80", "0150", ["carry", "yoshi"]],
+ ["single", "0D90", "0150", ["carry", "yoshi"]],
+ ["single", "0DA0", "0150", ["carry", "yoshi"]],
+ ["single", "0E40", "0140", ["carry", "yoshi"]],
+ ["single", "0E50", "0120", ["carry", "yoshi"]],
+ ["single", "0E70", "0150", ["carry", "yoshi"]],
+ ["single", "0E90", "0110", ["carry", "yoshi"]],
+ ["single", "0EB0", "0130", ["carry", "yoshi"]],
+ ["single", "0EE0", "0140", ["carry", "yoshi"]],
+ ["single", "0EF0", "0100", ["carry", "yoshi"]],
+ ["single", "0F10", "0120", ["carry", "yoshi"]],
+ ["single", "0F50", "0130", ["carry", "yoshi"]],
+ ["single", "0F70", "0150", ["carry", "yoshi"]],
+ ["single", "0F80", "0110", ["carry", "yoshi"]],
+ ["single", "0F90", "0130", ["carry", "yoshi"]],
+ ["single", "0FC0", "0150", ["carry", "yoshi"]],
+ ["single", "0FD0", "0120", ["carry", "yoshi"]],
+ ["single", "11A0", "0150", ["carry", "yoshi"]],
+ ["single", "11B0", "0120", ["carry", "yoshi"]],
+ ["single", "1230", "0150", ["carry", "yoshi"]],
+ ["single", "1240", "0140", ["carry", "yoshi"]],
+ ["single", "1250", "0130", ["carry", "yoshi"]]
+ ],
+ "124_test": [],
+ "125_special_zone_8": [
+ ["yoshi", "0390", "0100", ["carry", "yoshi"]],
+ ["single", "04A0", "0130", []],
+ ["single", "04B0", "0130", []],
+ ["single", "04C0", "0130", []],
+ ["single", "04D0", "0130", []],
+ ["single", "04E0", "0130", []],
+ ["pow", "0560", "0140", []],
+ ["power", "05D0", "0140", []],
+ ["star", "0750", "00F0", []],
+ ["single", "0670", "0140", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "0680", "0140", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "0690", "0140", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "06A0", "0140", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "06B0", "0140", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "06C0", "0140", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "06F0", "0140", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "0700", "0140", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "0710", "0140", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "0720", "0140", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "0730", "0140", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "0740", "0140", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "0750", "0140", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["multi", "0CA0", "0100", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "1100", "0120", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "1110", "0120", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "1120", "0120", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "1130", "0120", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["single", "1140", "0120", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["power", "13F0", "0140", ["mushroom spin", "cape", "carry", "yoshi"]],
+ ["fly", "1570", "00F0", ["mushroom spin", "cape", "carry", "yoshi"]]
+ ],
+ "126_special_zone_7": [
+ ["power", "0350", "0150", ["mushroom"]],
+ ["yoshi", "0C80", "0140", ["mushroom"]],
+ ["single", "0F90", "0140", ["mushroom"]],
+ ["power", "0FA0", "0140", ["mushroom"]],
+ ["single", "0FB0", "0140", ["mushroom"]]
+ ],
+ "127_special_zone_6": [
+ ["power", "0370", "00F0", ["swim"]],
+ ["single", "0610", "0140", ["swim"]],
+ ["single", "0630", "0120", ["swim"]],
+ ["yoshi", "0650", "0100", ["swim"]],
+ ["life", "07D0", "0140", ["swim"]],
+ ["multi", "0950", "0140", ["swim"]],
+ ["single", "0D80", "0140", ["swim"]],
+ ["single", "0D90", "0140", ["swim"]],
+ ["single", "0DA0", "0140", ["swim"]],
+ ["single", "0DB0", "0140", ["swim"]],
+ ["single", "0DC0", "0140", ["swim"]],
+ ["single", "0DD0", "0140", ["swim"]],
+ ["single", "0DE0", "0140", ["swim"]],
+ ["single", "0DF0", "0140", ["swim"]],
+ ["single", "0E00", "0140", ["swim"]],
+ ["single", "0E10", "0140", ["swim"]],
+ ["single", "0E20", "0140", ["swim"]],
+ ["single", "0E30", "0140", ["swim"]],
+ ["single", "0E40", "0140", ["swim"]],
+ ["single", "0E50", "0140", ["swim"]],
+ ["single", "0E60", "0140", ["swim"]],
+ ["single", "0E70", "0140", ["swim"]],
+ ["single", "0D80", "0100", ["swim"]],
+ ["single", "0D90", "0100", ["swim"]],
+ ["single", "0DA0", "0100", ["swim"]],
+ ["single", "0DB0", "0100", ["swim"]],
+ ["single", "0DC0", "0100", ["swim"]],
+ ["single", "0DD0", "0100", ["swim"]],
+ ["single", "0DE0", "0100", ["swim"]],
+ ["single", "0DF0", "0100", ["swim"]],
+ ["single", "0E00", "0100", ["swim"]],
+ ["single", "0E10", "0100", ["swim"]],
+ ["power", "0E20", "0100", ["swim"]],
+ ["single", "0E30", "0100", ["swim"]],
+ ["single", "0E40", "0100", ["swim"]],
+ ["single", "0E50", "0100", ["swim"]],
+ ["single", "0E60", "0100", ["swim"]],
+ ["single", "0E70", "0100", ["swim"]]
+ ],
+ "128_special_zone_5": [
+ ["yoshi", "01D0", "0160", ["mushroom"]]
+ ],
+ "129_test": [],
+ "12A_special_zone_1": [
+ ["vine", "0020", "03C0", []],
+ ["vine", "0050", "03C0", []],
+ ["vine", "0080", "03C0", []],
+ ["vine", "00B0", "03C0", []],
+ ["life", "0110", "0340", ["climb"]],
+ ["vine", "0120", "0280", ["climb"]],
+ ["pow", "0080", "01F0", ["climb"]],
+ ["vine", "00B0", "01F0", ["climb"]],
+ ["power", "00F0", "00D0", ["climb"]],
+ ["pswitch", "0190", "00C0", ["climb pswitch cape"]],
+ ["pswitch", "01C0", "0130", ["climb pswitch cape"]],
+ ["pswitch", "0180", "01A0", ["climb pswitch cape"]],
+ ["pswitch", "01D0", "01A0", ["climb pswitch cape"]],
+ ["pswitch", "01C0", "0270", ["climb pswitch cape"]],
+ ["pswitch", "01A0", "02C0", ["climb pswitch cape"]],
+ ["pswitch", "0190", "0310", ["climb pswitch cape"]],
+ ["pswitch", "01B0", "0370", ["climb pswitch cape"]],
+ ["pswitch", "0180", "03D0", ["climb pswitch cape"]],
+ ["pswitch", "0200", "0120", ["climb pswitch cape", "climb pswitch carry"]],
+ ["pswitch", "0210", "0130", ["climb pswitch cape", "climb pswitch carry"]],
+ ["pswitch", "0220", "0140", ["climb pswitch cape", "climb pswitch carry"]],
+ ["pswitch", "0230", "0150", ["climb pswitch cape", "climb pswitch carry"]]
+ ],
+ "12B_special_zone_2": [
+ ["power", "02E0", "0120", []],
+ ["single", "0380", "0110", ["pballoon"]],
+ ["single", "0450", "0140", ["pballoon"]],
+ ["power", "04A0", "00F0", ["pballoon"]],
+ ["single", "05C0", "0150", ["pballoon"]],
+ ["single", "05C0", "00F0", ["pballoon"]],
+ ["power", "0760", "0140", ["pballoon"]],
+ ["multi", "07E0", "00E0", ["pballoon"]],
+ ["single", "0850", "0100", ["pballoon"]],
+ ["single", "0920", "0140", ["pballoon"]]
+ ],
+ "12C_special_zone_3": [
+ ["power", "03F0", "0110", []],
+ ["yoshi", "0080", "0140", []],
+ ["wings", "0A50", "0140", []]
+ ],
+ "12D_special_zone_4": [
+ ["power", "0490", "0140", ["flower"]],
+ ["star", "0AF0", "00F0", ["flower carry", "flower pswitch"]]
+ ],
+ "12E_test": [],
+ "12F_test": [],
+ "130_star_road_2": [
+ ["star", "0460", "0130", ["swim"]]
+ ],
+ "131_test": [],
+ "132_star_road_3": [
+ ["key", "0080", "0030", ["carry"]]
+ ],
+ "133_test": [],
+ "134_star_road_1": [],
+ "135_star_road_4": [
+ ["power", "0540", "0090", []],
+ ["green", "0C00", "0140", ["greenswitch yoshi carry"]],
+ ["green", "0C10", "0140", ["greenswitch yoshi carry"]],
+ ["green", "0C20", "0140", ["greenswitch yoshi carry"]],
+ ["green", "0C30", "0140", ["greenswitch yoshi carry"]],
+ ["green", "0C40", "0140", ["greenswitch yoshi carry"]],
+ ["green", "0C50", "0140", ["greenswitch yoshi carry"]],
+ ["green", "0C60", "0140", ["greenswitch yoshi carry"]],
+ ["key", "0D40", "0160", ["carry yoshi", "greenswitch redswitch carry"]]
+ ],
+ "136_star_road_5": [
+ ["dir", "0510", "0140", []],
+ ["life", "07D0", "0150", ["pswitch"]],
+ ["vine", "08E0", "0100", ["pswitch"]],
+ ["yellow", "08F0", "0050", ["yellowswitch pswitch climb carry", "yellowswitch specialworld yoshi carry"]],
+ ["yellow", "0900", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "0910", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "0920", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "0930", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "0940", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "0950", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "0960", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "0970", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "0980", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "0990", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "09A0", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "09B0", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "09C0", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "09D0", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "09E0", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "09F0", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "0A00", "0050", ["yellowswitch specialworld yoshi carry"]],
+ ["yellow", "0A10", "0050", ["yellowswitch greenswitch yoshi carry", "yellowswitch greenswitch pswitch climb carry cape"]],
+ ["yellow", "0A10", "0060", ["yellowswitch greenswitch yoshi carry", "yellowswitch greenswitch pswitch climb carry cape"]],
+ ["green", "0A20", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0A30", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0A40", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0A50", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0A60", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0A70", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0A80", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0A90", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0AA0", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0AB0", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0AC0", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0AD0", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0AE0", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0AF0", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0B00", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0B10", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0B20", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0B30", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0B40", "0080", ["greenswitch specialworld yoshi carry"]],
+ ["green", "0B50", "0080", ["greenswitch specialworld yoshi carry"]]
+ ],
+ "137_test": [],
+ "138_test": [],
+ "139_test": [],
+ "13A_test": [],
+ "13B_test": []
+}
\ No newline at end of file
diff --git a/worlds/smw/data/graphics/indicators.bin b/worlds/smw/data/graphics/indicators.bin
new file mode 100644
index 000000000000..70e7036533c9
Binary files /dev/null and b/worlds/smw/data/graphics/indicators.bin differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/agnus_castle.mw3 b/worlds/smw/data/palettes/level/castle_pillars/agnus_castle.mw3
new file mode 100644
index 000000000000..99924e1facf8
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/agnus_castle.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/cheese.mw3 b/worlds/smw/data/palettes/level/castle_pillars/cheese.mw3
new file mode 100644
index 000000000000..e9b83f1bc976
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/cheese.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/chocolate_blue.mw3 b/worlds/smw/data/palettes/level/castle_pillars/chocolate_blue.mw3
new file mode 100644
index 000000000000..51073a167485
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/chocolate_blue.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/dark_aqua_marine.mw3 b/worlds/smw/data/palettes/level/castle_pillars/dark_aqua_marine.mw3
new file mode 100644
index 000000000000..a482231a048b
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/dark_aqua_marine.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/dollhouse.mw3 b/worlds/smw/data/palettes/level/castle_pillars/dollhouse.mw3
new file mode 100644
index 000000000000..06849ceaf1d5
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/dollhouse.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/gold_caslte.mw3 b/worlds/smw/data/palettes/level/castle_pillars/gold_caslte.mw3
new file mode 100644
index 000000000000..f4c03d2cb17a
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/gold_caslte.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/keves_castle.mw3 b/worlds/smw/data/palettes/level/castle_pillars/keves_castle.mw3
new file mode 100644
index 000000000000..a1ab883c34ef
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/keves_castle.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/original_gray.mw3 b/worlds/smw/data/palettes/level/castle_pillars/original_gray.mw3
new file mode 100644
index 000000000000..37143fb2b889
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/original_gray.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/original_green.mw3 b/worlds/smw/data/palettes/level/castle_pillars/original_green.mw3
new file mode 100644
index 000000000000..4124e1900166
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/original_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/original_mustard.mw3 b/worlds/smw/data/palettes/level/castle_pillars/original_mustard.mw3
new file mode 100644
index 000000000000..1ce7a0f3eac2
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/original_mustard.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/original_white.mw3 b/worlds/smw/data/palettes/level/castle_pillars/original_white.mw3
new file mode 100644
index 000000000000..f235216f1da2
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/original_white.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/pink_purple.mw3 b/worlds/smw/data/palettes/level/castle_pillars/pink_purple.mw3
new file mode 100644
index 000000000000..591ce013c9aa
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/pink_purple.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/purple_pink.mw3 b/worlds/smw/data/palettes/level/castle_pillars/purple_pink.mw3
new file mode 100644
index 000000000000..5bea72483006
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/purple_pink.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/sand_gray.mw3 b/worlds/smw/data/palettes/level/castle_pillars/sand_gray.mw3
new file mode 100644
index 000000000000..eff02a49e13f
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/sand_gray.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/sand_green.mw3 b/worlds/smw/data/palettes/level/castle_pillars/sand_green.mw3
new file mode 100644
index 000000000000..5757d8fbfaa1
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/sand_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/shenhe.mw3 b/worlds/smw/data/palettes/level/castle_pillars/shenhe.mw3
new file mode 100644
index 000000000000..93dc170a525e
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/shenhe.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_pillars/whatsapp.mw3 b/worlds/smw/data/palettes/level/castle_pillars/whatsapp.mw3
new file mode 100644
index 000000000000..198f46eca8a9
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_pillars/whatsapp.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_small_windows/dark_lava.mw3 b/worlds/smw/data/palettes/level/castle_small_windows/dark_lava.mw3
new file mode 100644
index 000000000000..477701e86a9b
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_small_windows/dark_lava.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_small_windows/dark_purple.mw3 b/worlds/smw/data/palettes/level/castle_small_windows/dark_purple.mw3
new file mode 100644
index 000000000000..29eff5aeffa3
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_small_windows/dark_purple.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_small_windows/dollhouse.mw3 b/worlds/smw/data/palettes/level/castle_small_windows/dollhouse.mw3
new file mode 100644
index 000000000000..73f69240205b
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_small_windows/dollhouse.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_small_windows/forgotten_temple.mw3 b/worlds/smw/data/palettes/level/castle_small_windows/forgotten_temple.mw3
new file mode 100644
index 000000000000..35d97033f847
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_small_windows/forgotten_temple.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_small_windows/original_gray.mw3 b/worlds/smw/data/palettes/level/castle_small_windows/original_gray.mw3
new file mode 100644
index 000000000000..37143fb2b889
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_small_windows/original_gray.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_small_windows/original_volcanic.mw3 b/worlds/smw/data/palettes/level/castle_small_windows/original_volcanic.mw3
new file mode 100644
index 000000000000..21d82d7c84a1
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_small_windows/original_volcanic.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_small_windows/original_water.mw3 b/worlds/smw/data/palettes/level/castle_small_windows/original_water.mw3
new file mode 100644
index 000000000000..20ee47e8bcba
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_small_windows/original_water.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_small_windows/sand_gray.mw3 b/worlds/smw/data/palettes/level/castle_small_windows/sand_gray.mw3
new file mode 100644
index 000000000000..5b11808ae6b9
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_small_windows/sand_gray.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_small_windows/sand_green.mw3 b/worlds/smw/data/palettes/level/castle_small_windows/sand_green.mw3
new file mode 100644
index 000000000000..2a5ff0ed8551
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_small_windows/sand_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_small_windows/shenhe.mw3 b/worlds/smw/data/palettes/level/castle_small_windows/shenhe.mw3
new file mode 100644
index 000000000000..93dc170a525e
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_small_windows/shenhe.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_small_windows/water.mw3 b/worlds/smw/data/palettes/level/castle_small_windows/water.mw3
new file mode 100644
index 000000000000..9822a3c2eaa2
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_small_windows/water.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_small_windows/whatsapp.mw3 b/worlds/smw/data/palettes/level/castle_small_windows/whatsapp.mw3
new file mode 100644
index 000000000000..198f46eca8a9
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_small_windows/whatsapp.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_wall/cheese.mw3 b/worlds/smw/data/palettes/level/castle_wall/cheese.mw3
new file mode 100644
index 000000000000..913ad3977875
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_wall/cheese.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_wall/dollhouse.mw3 b/worlds/smw/data/palettes/level/castle_wall/dollhouse.mw3
new file mode 100644
index 000000000000..73f69240205b
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_wall/dollhouse.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_wall/grand_marshall.mw3 b/worlds/smw/data/palettes/level/castle_wall/grand_marshall.mw3
new file mode 100644
index 000000000000..574d557f1ead
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_wall/grand_marshall.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_wall/hot_wall.mw3 b/worlds/smw/data/palettes/level/castle_wall/hot_wall.mw3
new file mode 100644
index 000000000000..44703fe4f681
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_wall/hot_wall.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_wall/original.mw3 b/worlds/smw/data/palettes/level/castle_wall/original.mw3
new file mode 100644
index 000000000000..395fb66a0178
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_wall/original.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_wall/sand_green.mw3 b/worlds/smw/data/palettes/level/castle_wall/sand_green.mw3
new file mode 100644
index 000000000000..5757d8fbfaa1
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_wall/sand_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_wall/shenhe.mw3 b/worlds/smw/data/palettes/level/castle_wall/shenhe.mw3
new file mode 100644
index 000000000000..93dc170a525e
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_wall/shenhe.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_wall/water.mw3 b/worlds/smw/data/palettes/level/castle_wall/water.mw3
new file mode 100644
index 000000000000..a0955e820349
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_wall/water.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_windows/brawler_pink.mw3 b/worlds/smw/data/palettes/level/castle_windows/brawler_pink.mw3
new file mode 100644
index 000000000000..69c496fc5d07
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_windows/brawler_pink.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_windows/cheese.mw3 b/worlds/smw/data/palettes/level/castle_windows/cheese.mw3
new file mode 100644
index 000000000000..d91826e8647a
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_windows/cheese.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_windows/dark_aqua_marine.mw3 b/worlds/smw/data/palettes/level/castle_windows/dark_aqua_marine.mw3
new file mode 100644
index 000000000000..501f11d1a954
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_windows/dark_aqua_marine.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_windows/dollhouse.mw3 b/worlds/smw/data/palettes/level/castle_windows/dollhouse.mw3
new file mode 100644
index 000000000000..d3e5fe4b407f
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_windows/dollhouse.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_windows/original_brown.mw3 b/worlds/smw/data/palettes/level/castle_windows/original_brown.mw3
new file mode 100644
index 000000000000..0bcc7305b7aa
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_windows/original_brown.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_windows/original_gray.mw3 b/worlds/smw/data/palettes/level/castle_windows/original_gray.mw3
new file mode 100644
index 000000000000..37143fb2b889
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_windows/original_gray.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_windows/original_water.mw3 b/worlds/smw/data/palettes/level/castle_windows/original_water.mw3
new file mode 100644
index 000000000000..d61f6dba36f6
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_windows/original_water.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_windows/red_castle.mw3 b/worlds/smw/data/palettes/level/castle_windows/red_castle.mw3
new file mode 100644
index 000000000000..666c17f66f4d
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_windows/red_castle.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_windows/shenhe.mw3 b/worlds/smw/data/palettes/level/castle_windows/shenhe.mw3
new file mode 100644
index 000000000000..93dc170a525e
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_windows/shenhe.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_windows/underwater.mw3 b/worlds/smw/data/palettes/level/castle_windows/underwater.mw3
new file mode 100644
index 000000000000..db5c1a996ccb
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_windows/underwater.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_windows/water.mw3 b/worlds/smw/data/palettes/level/castle_windows/water.mw3
new file mode 100644
index 000000000000..a0955e820349
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_windows/water.mw3 differ
diff --git a/worlds/smw/data/palettes/level/castle_windows/whatsapp.mw3 b/worlds/smw/data/palettes/level/castle_windows/whatsapp.mw3
new file mode 100644
index 000000000000..198f46eca8a9
Binary files /dev/null and b/worlds/smw/data/palettes/level/castle_windows/whatsapp.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/brawler_dark.mw3 b/worlds/smw/data/palettes/level/cave/brawler_dark.mw3
new file mode 100644
index 000000000000..9197e99e15d5
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/brawler_dark.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/brawler_purple.mw3 b/worlds/smw/data/palettes/level/cave/brawler_purple.mw3
new file mode 100644
index 000000000000..0bd5a1bf1056
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/brawler_purple.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/brawler_red.mw3 b/worlds/smw/data/palettes/level/cave/brawler_red.mw3
new file mode 100644
index 000000000000..e3f5cdfaaf02
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/brawler_red.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/brawler_teal.mw3 b/worlds/smw/data/palettes/level/cave/brawler_teal.mw3
new file mode 100644
index 000000000000..1927ac99ed87
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/brawler_teal.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/bright_magma.mw3 b/worlds/smw/data/palettes/level/cave/bright_magma.mw3
new file mode 100644
index 000000000000..92e059d863c9
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/bright_magma.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/dark_red.mw3 b/worlds/smw/data/palettes/level/cave/dark_red.mw3
new file mode 100644
index 000000000000..b2cc60680590
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/dark_red.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/glowing_mushroom.mw3 b/worlds/smw/data/palettes/level/cave/glowing_mushroom.mw3
new file mode 100644
index 000000000000..5b5531c7ef2d
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/glowing_mushroom.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/green_depths.mw3 b/worlds/smw/data/palettes/level/cave/green_depths.mw3
new file mode 100644
index 000000000000..7f7593d2e34d
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/green_depths.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/ice.mw3 b/worlds/smw/data/palettes/level/cave/ice.mw3
new file mode 100644
index 000000000000..23b0b2ef08fd
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/ice.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/magma_cave.mw3 b/worlds/smw/data/palettes/level/cave/magma_cave.mw3
new file mode 100644
index 000000000000..ca297deb2578
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/magma_cave.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/original_chocolate.mw3 b/worlds/smw/data/palettes/level/cave/original_chocolate.mw3
new file mode 100644
index 000000000000..db2693d6be98
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/original_chocolate.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/original_gray.mw3 b/worlds/smw/data/palettes/level/cave/original_gray.mw3
new file mode 100644
index 000000000000..2e01f09820c8
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/original_gray.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/original_ice.mw3 b/worlds/smw/data/palettes/level/cave/original_ice.mw3
new file mode 100644
index 000000000000..6d17d16efefb
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/original_ice.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/original_mustard.mw3 b/worlds/smw/data/palettes/level/cave/original_mustard.mw3
new file mode 100644
index 000000000000..001ed133195b
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/original_mustard.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/original_volcanic.mw3 b/worlds/smw/data/palettes/level/cave/original_volcanic.mw3
new file mode 100644
index 000000000000..96befdfa3d55
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/original_volcanic.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/snow.mw3 b/worlds/smw/data/palettes/level/cave/snow.mw3
new file mode 100644
index 000000000000..2328e1a21428
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/snow.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/toxic.mw3 b/worlds/smw/data/palettes/level/cave/toxic.mw3
new file mode 100644
index 000000000000..a78538ddba77
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/toxic.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave/toxic_moss.mw3 b/worlds/smw/data/palettes/level/cave/toxic_moss.mw3
new file mode 100644
index 000000000000..9afe61103098
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave/toxic_moss.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave_rocks/bocchi_rock_hair_cube_things.mw3 b/worlds/smw/data/palettes/level/cave_rocks/bocchi_rock_hair_cube_things.mw3
new file mode 100644
index 000000000000..0fb33b2d6a38
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave_rocks/bocchi_rock_hair_cube_things.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave_rocks/brawler_volcanic.mw3 b/worlds/smw/data/palettes/level/cave_rocks/brawler_volcanic.mw3
new file mode 100644
index 000000000000..5a3cf230f04d
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave_rocks/brawler_volcanic.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave_rocks/ice.mw3 b/worlds/smw/data/palettes/level/cave_rocks/ice.mw3
new file mode 100644
index 000000000000..baa52fbf0b8b
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave_rocks/ice.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave_rocks/layer_2.mw3 b/worlds/smw/data/palettes/level/cave_rocks/layer_2.mw3
new file mode 100644
index 000000000000..ff354e34fefa
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave_rocks/layer_2.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave_rocks/original_gray.mw3 b/worlds/smw/data/palettes/level/cave_rocks/original_gray.mw3
new file mode 100644
index 000000000000..bf50bed4088a
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave_rocks/original_gray.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave_rocks/original_mustard.mw3 b/worlds/smw/data/palettes/level/cave_rocks/original_mustard.mw3
new file mode 100644
index 000000000000..8150d4687553
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave_rocks/original_mustard.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave_rocks/pyra_mythra_ft_pneuma.mw3 b/worlds/smw/data/palettes/level/cave_rocks/pyra_mythra_ft_pneuma.mw3
new file mode 100644
index 000000000000..8f2b2817d805
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave_rocks/pyra_mythra_ft_pneuma.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave_rocks/snow.mw3 b/worlds/smw/data/palettes/level/cave_rocks/snow.mw3
new file mode 100644
index 000000000000..b36bff4fef30
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave_rocks/snow.mw3 differ
diff --git a/worlds/smw/data/palettes/level/cave_rocks/toxic.mw3 b/worlds/smw/data/palettes/level/cave_rocks/toxic.mw3
new file mode 100644
index 000000000000..4f98b11bc175
Binary files /dev/null and b/worlds/smw/data/palettes/level/cave_rocks/toxic.mw3 differ
diff --git a/worlds/smw/data/palettes/level/clouds/atardecer.mw3 b/worlds/smw/data/palettes/level/clouds/atardecer.mw3
new file mode 100644
index 000000000000..bb1754963ea6
Binary files /dev/null and b/worlds/smw/data/palettes/level/clouds/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/level/clouds/charcoal.mw3 b/worlds/smw/data/palettes/level/clouds/charcoal.mw3
new file mode 100644
index 000000000000..755f9af87a3e
Binary files /dev/null and b/worlds/smw/data/palettes/level/clouds/charcoal.mw3 differ
diff --git a/worlds/smw/data/palettes/level/clouds/cloudy.mw3 b/worlds/smw/data/palettes/level/clouds/cloudy.mw3
new file mode 100644
index 000000000000..b7d07d348c42
Binary files /dev/null and b/worlds/smw/data/palettes/level/clouds/cloudy.mw3 differ
diff --git a/worlds/smw/data/palettes/level/clouds/cotton_candy.mw3 b/worlds/smw/data/palettes/level/clouds/cotton_candy.mw3
new file mode 100644
index 000000000000..f9ddeb89c87d
Binary files /dev/null and b/worlds/smw/data/palettes/level/clouds/cotton_candy.mw3 differ
diff --git a/worlds/smw/data/palettes/level/clouds/original_green.mw3 b/worlds/smw/data/palettes/level/clouds/original_green.mw3
new file mode 100644
index 000000000000..79af508740ad
Binary files /dev/null and b/worlds/smw/data/palettes/level/clouds/original_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/clouds/original_orange.mw3 b/worlds/smw/data/palettes/level/clouds/original_orange.mw3
new file mode 100644
index 000000000000..453b717b9038
Binary files /dev/null and b/worlds/smw/data/palettes/level/clouds/original_orange.mw3 differ
diff --git a/worlds/smw/data/palettes/level/forest/agnian_queen.mw3 b/worlds/smw/data/palettes/level/forest/agnian_queen.mw3
new file mode 100644
index 000000000000..f187d8a1abb4
Binary files /dev/null and b/worlds/smw/data/palettes/level/forest/agnian_queen.mw3 differ
diff --git a/worlds/smw/data/palettes/level/forest/atardecer.mw3 b/worlds/smw/data/palettes/level/forest/atardecer.mw3
new file mode 100644
index 000000000000..95e07701c24e
Binary files /dev/null and b/worlds/smw/data/palettes/level/forest/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/level/forest/frozen.mw3 b/worlds/smw/data/palettes/level/forest/frozen.mw3
new file mode 100644
index 000000000000..f6dc97c10629
Binary files /dev/null and b/worlds/smw/data/palettes/level/forest/frozen.mw3 differ
diff --git a/worlds/smw/data/palettes/level/forest/halloween.mw3 b/worlds/smw/data/palettes/level/forest/halloween.mw3
new file mode 100644
index 000000000000..2e23b6dd6f95
Binary files /dev/null and b/worlds/smw/data/palettes/level/forest/halloween.mw3 differ
diff --git a/worlds/smw/data/palettes/level/forest/kevesi_queen.mw3 b/worlds/smw/data/palettes/level/forest/kevesi_queen.mw3
new file mode 100644
index 000000000000..1a4cd413d3b2
Binary files /dev/null and b/worlds/smw/data/palettes/level/forest/kevesi_queen.mw3 differ
diff --git a/worlds/smw/data/palettes/level/forest/original_dark.mw3 b/worlds/smw/data/palettes/level/forest/original_dark.mw3
new file mode 100644
index 000000000000..7c28daa0d3a6
Binary files /dev/null and b/worlds/smw/data/palettes/level/forest/original_dark.mw3 differ
diff --git a/worlds/smw/data/palettes/level/forest/original_fall.mw3 b/worlds/smw/data/palettes/level/forest/original_fall.mw3
new file mode 100644
index 000000000000..4b67c03d934a
Binary files /dev/null and b/worlds/smw/data/palettes/level/forest/original_fall.mw3 differ
diff --git a/worlds/smw/data/palettes/level/forest/original_green.mw3 b/worlds/smw/data/palettes/level/forest/original_green.mw3
new file mode 100644
index 000000000000..9139338c3158
Binary files /dev/null and b/worlds/smw/data/palettes/level/forest/original_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/forest/sakura.mw3 b/worlds/smw/data/palettes/level/forest/sakura.mw3
new file mode 100644
index 000000000000..df694e2bd34f
Binary files /dev/null and b/worlds/smw/data/palettes/level/forest/sakura.mw3 differ
diff --git a/worlds/smw/data/palettes/level/forest/snow_dark_leaves.mw3 b/worlds/smw/data/palettes/level/forest/snow_dark_leaves.mw3
new file mode 100644
index 000000000000..320256986039
Binary files /dev/null and b/worlds/smw/data/palettes/level/forest/snow_dark_leaves.mw3 differ
diff --git a/worlds/smw/data/palettes/level/forest/snow_green_leaves.mw3 b/worlds/smw/data/palettes/level/forest/snow_green_leaves.mw3
new file mode 100644
index 000000000000..a2346ca3c307
Binary files /dev/null and b/worlds/smw/data/palettes/level/forest/snow_green_leaves.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house/brawler_cyan.mw3 b/worlds/smw/data/palettes/level/ghost_house/brawler_cyan.mw3
new file mode 100644
index 000000000000..cb6590850f3f
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house/brawler_cyan.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house/brawler_orange.mw3 b/worlds/smw/data/palettes/level/ghost_house/brawler_orange.mw3
new file mode 100644
index 000000000000..750def41f99b
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house/brawler_orange.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house/brawler_purple.mw3 b/worlds/smw/data/palettes/level/ghost_house/brawler_purple.mw3
new file mode 100644
index 000000000000..60ac884bf7af
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house/brawler_purple.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house/creepypasta.mw3 b/worlds/smw/data/palettes/level/ghost_house/creepypasta.mw3
new file mode 100644
index 000000000000..92876c3c0eee
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house/creepypasta.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house/crimson_house.mw3 b/worlds/smw/data/palettes/level/ghost_house/crimson_house.mw3
new file mode 100644
index 000000000000..fc7369f7efb1
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house/crimson_house.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house/golden_house.mw3 b/worlds/smw/data/palettes/level/ghost_house/golden_house.mw3
new file mode 100644
index 000000000000..5d4e153bd1cc
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house/golden_house.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house/halloween_pallet.mw3 b/worlds/smw/data/palettes/level/ghost_house/halloween_pallet.mw3
new file mode 100644
index 000000000000..f73c16e46101
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house/halloween_pallet.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house/orange_lights.mw3 b/worlds/smw/data/palettes/level/ghost_house/orange_lights.mw3
new file mode 100644
index 000000000000..8eddf82fd34e
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house/orange_lights.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house/original_aqua.mw3 b/worlds/smw/data/palettes/level/ghost_house/original_aqua.mw3
new file mode 100644
index 000000000000..fec63947beaa
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house/original_aqua.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house/original_blue.mw3 b/worlds/smw/data/palettes/level/ghost_house/original_blue.mw3
new file mode 100644
index 000000000000..b46979edd84e
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house/original_blue.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house/original_dark.mw3 b/worlds/smw/data/palettes/level/ghost_house/original_dark.mw3
new file mode 100644
index 000000000000..eb6152098a04
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house/original_dark.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house/original_white.mw3 b/worlds/smw/data/palettes/level/ghost_house/original_white.mw3
new file mode 100644
index 000000000000..0d5c43f3b913
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house/original_white.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house_exit/evening_exit.mw3 b/worlds/smw/data/palettes/level/ghost_house_exit/evening_exit.mw3
new file mode 100644
index 000000000000..98a5f92a603a
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house_exit/evening_exit.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house_exit/golden_house.mw3 b/worlds/smw/data/palettes/level/ghost_house_exit/golden_house.mw3
new file mode 100644
index 000000000000..45f5c6701ab3
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house_exit/golden_house.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house_exit/original.mw3 b/worlds/smw/data/palettes/level/ghost_house_exit/original.mw3
new file mode 100644
index 000000000000..3856df591aad
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house_exit/original.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house_exit/original_blue_door.mw3 b/worlds/smw/data/palettes/level/ghost_house_exit/original_blue_door.mw3
new file mode 100644
index 000000000000..b43818c57827
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house_exit/original_blue_door.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ghost_house_exit/underwater.mw3 b/worlds/smw/data/palettes/level/ghost_house_exit/underwater.mw3
new file mode 100644
index 000000000000..a92bc7a1ba74
Binary files /dev/null and b/worlds/smw/data/palettes/level/ghost_house_exit/underwater.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_clouds/atardecer.mw3 b/worlds/smw/data/palettes/level/grass_clouds/atardecer.mw3
new file mode 100644
index 000000000000..2b055f2fe658
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_clouds/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_clouds/crimson.mw3 b/worlds/smw/data/palettes/level/grass_clouds/crimson.mw3
new file mode 100644
index 000000000000..aefeb5d35680
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_clouds/crimson.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_clouds/electro.mw3 b/worlds/smw/data/palettes/level/grass_clouds/electro.mw3
new file mode 100644
index 000000000000..8a3971982011
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_clouds/electro.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_clouds/geo.mw3 b/worlds/smw/data/palettes/level/grass_clouds/geo.mw3
new file mode 100644
index 000000000000..f085c6f368eb
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_clouds/geo.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_clouds/miku.mw3 b/worlds/smw/data/palettes/level/grass_clouds/miku.mw3
new file mode 100644
index 000000000000..3b009fbc0f67
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_clouds/miku.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_clouds/original_blue.mw3 b/worlds/smw/data/palettes/level/grass_clouds/original_blue.mw3
new file mode 100644
index 000000000000..bcf57d822c71
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_clouds/original_blue.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_clouds/original_green.mw3 b/worlds/smw/data/palettes/level/grass_clouds/original_green.mw3
new file mode 100644
index 000000000000..e896e0dce38b
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_clouds/original_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_clouds/pizza.mw3 b/worlds/smw/data/palettes/level/grass_clouds/pizza.mw3
new file mode 100644
index 000000000000..2be2f2db72ee
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_clouds/pizza.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_clouds/sakura.mw3 b/worlds/smw/data/palettes/level/grass_clouds/sakura.mw3
new file mode 100644
index 000000000000..9a0d8d868733
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_clouds/sakura.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_clouds/shukfr.mw3 b/worlds/smw/data/palettes/level/grass_clouds/shukfr.mw3
new file mode 100644
index 000000000000..635d22c98abb
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_clouds/shukfr.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_clouds/snow_day.mw3 b/worlds/smw/data/palettes/level/grass_clouds/snow_day.mw3
new file mode 100644
index 000000000000..8a7c632110cf
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_clouds/snow_day.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_clouds/volcanic_rock.mw3 b/worlds/smw/data/palettes/level/grass_clouds/volcanic_rock.mw3
new file mode 100644
index 000000000000..224bfed2cb41
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_clouds/volcanic_rock.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/atardecer.mw3 b/worlds/smw/data/palettes/level/grass_forest/atardecer.mw3
new file mode 100644
index 000000000000..cf97cf05e99b
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/autumn.mw3 b/worlds/smw/data/palettes/level/grass_forest/autumn.mw3
new file mode 100644
index 000000000000..89d75ed27fcf
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/autumn.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/brawler.mw3 b/worlds/smw/data/palettes/level/grass_forest/brawler.mw3
new file mode 100644
index 000000000000..85db596ea5c1
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/brawler.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/brawler_atardecer.mw3 b/worlds/smw/data/palettes/level/grass_forest/brawler_atardecer.mw3
new file mode 100644
index 000000000000..39b73646578d
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/brawler_atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/brawler_green.mw3 b/worlds/smw/data/palettes/level/grass_forest/brawler_green.mw3
new file mode 100644
index 000000000000..8ada9a25c474
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/brawler_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/crimson.mw3 b/worlds/smw/data/palettes/level/grass_forest/crimson.mw3
new file mode 100644
index 000000000000..48465a548a19
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/crimson.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/deep_forest.mw3 b/worlds/smw/data/palettes/level/grass_forest/deep_forest.mw3
new file mode 100644
index 000000000000..91517aa15e27
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/deep_forest.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/electro.mw3 b/worlds/smw/data/palettes/level/grass_forest/electro.mw3
new file mode 100644
index 000000000000..d462646a48d8
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/electro.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/geo.mw3 b/worlds/smw/data/palettes/level/grass_forest/geo.mw3
new file mode 100644
index 000000000000..f085c6f368eb
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/geo.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/miku.mw3 b/worlds/smw/data/palettes/level/grass_forest/miku.mw3
new file mode 100644
index 000000000000..5eeaf6c571d8
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/miku.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/myon.mw3 b/worlds/smw/data/palettes/level/grass_forest/myon.mw3
new file mode 100644
index 000000000000..8420ad56ec5c
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/myon.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/original_aqua.mw3 b/worlds/smw/data/palettes/level/grass_forest/original_aqua.mw3
new file mode 100644
index 000000000000..ba8ffc90ae0b
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/original_aqua.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/original_green.mw3 b/worlds/smw/data/palettes/level/grass_forest/original_green.mw3
new file mode 100644
index 000000000000..607d80cc9b92
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/original_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/pizza.mw3 b/worlds/smw/data/palettes/level/grass_forest/pizza.mw3
new file mode 100644
index 000000000000..2be2f2db72ee
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/pizza.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/sakura.mw3 b/worlds/smw/data/palettes/level/grass_forest/sakura.mw3
new file mode 100644
index 000000000000..5b7627fe74f2
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/sakura.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/snow_dark_leaves.mw3 b/worlds/smw/data/palettes/level/grass_forest/snow_dark_leaves.mw3
new file mode 100644
index 000000000000..47bbd42b97a7
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/snow_dark_leaves.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/snow_green.mw3 b/worlds/smw/data/palettes/level/grass_forest/snow_green.mw3
new file mode 100644
index 000000000000..50364a4a79c4
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/snow_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_forest/winter.mw3 b/worlds/smw/data/palettes/level/grass_forest/winter.mw3
new file mode 100644
index 000000000000..63109c24b8fa
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_forest/winter.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_hills/atardecer.mw3 b/worlds/smw/data/palettes/level/grass_hills/atardecer.mw3
new file mode 100644
index 000000000000..0c32989c5eac
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_hills/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_hills/brawler_green.mw3 b/worlds/smw/data/palettes/level/grass_hills/brawler_green.mw3
new file mode 100644
index 000000000000..612dcce98c7e
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_hills/brawler_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_hills/crimson.mw3 b/worlds/smw/data/palettes/level/grass_hills/crimson.mw3
new file mode 100644
index 000000000000..9f56757809fe
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_hills/crimson.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_hills/electro.mw3 b/worlds/smw/data/palettes/level/grass_hills/electro.mw3
new file mode 100644
index 000000000000..c0796b530a99
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_hills/electro.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_hills/geo.mw3 b/worlds/smw/data/palettes/level/grass_hills/geo.mw3
new file mode 100644
index 000000000000..ac56278a9fc4
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_hills/geo.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_hills/miku.mw3 b/worlds/smw/data/palettes/level/grass_hills/miku.mw3
new file mode 100644
index 000000000000..49c78fadba25
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_hills/miku.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_hills/mogumogu.mw3 b/worlds/smw/data/palettes/level/grass_hills/mogumogu.mw3
new file mode 100644
index 000000000000..88af74ceafcb
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_hills/mogumogu.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_hills/nocturno.mw3 b/worlds/smw/data/palettes/level/grass_hills/nocturno.mw3
new file mode 100644
index 000000000000..f0eeaee1e62c
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_hills/nocturno.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_hills/original.mw3 b/worlds/smw/data/palettes/level/grass_hills/original.mw3
new file mode 100644
index 000000000000..705c6602f7bb
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_hills/original.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_hills/sakura.mw3 b/worlds/smw/data/palettes/level/grass_hills/sakura.mw3
new file mode 100644
index 000000000000..6c65dc4ff010
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_hills/sakura.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_hills/snow.mw3 b/worlds/smw/data/palettes/level/grass_hills/snow.mw3
new file mode 100644
index 000000000000..38d114681250
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_hills/snow.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_hills/sunsetish_grass_hills.mw3 b/worlds/smw/data/palettes/level/grass_hills/sunsetish_grass_hills.mw3
new file mode 100644
index 000000000000..c51bdfc6a7d3
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_hills/sunsetish_grass_hills.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_hills/toothpaste.mw3 b/worlds/smw/data/palettes/level/grass_hills/toothpaste.mw3
new file mode 100644
index 000000000000..7c108ae348ae
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_hills/toothpaste.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/brawler_lifeless.mw3 b/worlds/smw/data/palettes/level/grass_mountains/brawler_lifeless.mw3
new file mode 100644
index 000000000000..b9979692f09e
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/brawler_lifeless.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/classic_sm.mw3 b/worlds/smw/data/palettes/level/grass_mountains/classic_sm.mw3
new file mode 100644
index 000000000000..0e77cc017034
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/classic_sm.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/crimson.mw3 b/worlds/smw/data/palettes/level/grass_mountains/crimson.mw3
new file mode 100644
index 000000000000..9527a0a24af2
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/crimson.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/dry_hills.mw3 b/worlds/smw/data/palettes/level/grass_mountains/dry_hills.mw3
new file mode 100644
index 000000000000..61bdc7b82e90
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/dry_hills.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/electro.mw3 b/worlds/smw/data/palettes/level/grass_mountains/electro.mw3
new file mode 100644
index 000000000000..684b84a6af3f
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/electro.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/geo.mw3 b/worlds/smw/data/palettes/level/grass_mountains/geo.mw3
new file mode 100644
index 000000000000..f085c6f368eb
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/geo.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/late_sandish.mw3 b/worlds/smw/data/palettes/level/grass_mountains/late_sandish.mw3
new file mode 100644
index 000000000000..d94156adda5c
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/late_sandish.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/miku.mw3 b/worlds/smw/data/palettes/level/grass_mountains/miku.mw3
new file mode 100644
index 000000000000..5eeaf6c571d8
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/miku.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/original_aqua.mw3 b/worlds/smw/data/palettes/level/grass_mountains/original_aqua.mw3
new file mode 100644
index 000000000000..fda1d358f7f2
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/original_aqua.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/original_blue.mw3 b/worlds/smw/data/palettes/level/grass_mountains/original_blue.mw3
new file mode 100644
index 000000000000..29404f64bc8f
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/original_blue.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/original_green.mw3 b/worlds/smw/data/palettes/level/grass_mountains/original_green.mw3
new file mode 100644
index 000000000000..607d80cc9b92
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/original_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/original_white.mw3 b/worlds/smw/data/palettes/level/grass_mountains/original_white.mw3
new file mode 100644
index 000000000000..7ecd4e3ccb0b
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/original_white.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/recksfr.mw3 b/worlds/smw/data/palettes/level/grass_mountains/recksfr.mw3
new file mode 100644
index 000000000000..b5da161c2cb5
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/recksfr.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/sakura_hills.mw3 b/worlds/smw/data/palettes/level/grass_mountains/sakura_hills.mw3
new file mode 100644
index 000000000000..0f8144cd23cd
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/sakura_hills.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_mountains/snow_day.mw3 b/worlds/smw/data/palettes/level/grass_mountains/snow_day.mw3
new file mode 100644
index 000000000000..3b182c3b7b21
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_mountains/snow_day.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/atardecer.mw3 b/worlds/smw/data/palettes/level/grass_rocks/atardecer.mw3
new file mode 100644
index 000000000000..2212fc60ce5b
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/crimson.mw3 b/worlds/smw/data/palettes/level/grass_rocks/crimson.mw3
new file mode 100644
index 000000000000..9527a0a24af2
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/crimson.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/dark.mw3 b/worlds/smw/data/palettes/level/grass_rocks/dark.mw3
new file mode 100644
index 000000000000..1f6aa06a19e9
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/dark.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/electro.mw3 b/worlds/smw/data/palettes/level/grass_rocks/electro.mw3
new file mode 100644
index 000000000000..684b84a6af3f
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/electro.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/geo.mw3 b/worlds/smw/data/palettes/level/grass_rocks/geo.mw3
new file mode 100644
index 000000000000..f085c6f368eb
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/geo.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/ice.mw3 b/worlds/smw/data/palettes/level/grass_rocks/ice.mw3
new file mode 100644
index 000000000000..349ce8e099d2
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/ice.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/miku.mw3 b/worlds/smw/data/palettes/level/grass_rocks/miku.mw3
new file mode 100644
index 000000000000..5eeaf6c571d8
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/miku.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/napolitano.mw3 b/worlds/smw/data/palettes/level/grass_rocks/napolitano.mw3
new file mode 100644
index 000000000000..bc6ebc6621d9
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/napolitano.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/original_aqua.mw3 b/worlds/smw/data/palettes/level/grass_rocks/original_aqua.mw3
new file mode 100644
index 000000000000..fda1d358f7f2
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/original_aqua.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/original_choco_volcanic.mw3 b/worlds/smw/data/palettes/level/grass_rocks/original_choco_volcanic.mw3
new file mode 100644
index 000000000000..4b4631e841fc
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/original_choco_volcanic.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/original_ice.mw3 b/worlds/smw/data/palettes/level/grass_rocks/original_ice.mw3
new file mode 100644
index 000000000000..041aa9edb1c3
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/original_ice.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/original_volcanic.mw3 b/worlds/smw/data/palettes/level/grass_rocks/original_volcanic.mw3
new file mode 100644
index 000000000000..d7a19dc3bcb2
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/original_volcanic.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/original_volcanic_green.mw3 b/worlds/smw/data/palettes/level/grass_rocks/original_volcanic_green.mw3
new file mode 100644
index 000000000000..86b038d6e4fd
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/original_volcanic_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/original_white.mw3 b/worlds/smw/data/palettes/level/grass_rocks/original_white.mw3
new file mode 100644
index 000000000000..53d76fe5308b
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/original_white.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/original_white_2.mw3 b/worlds/smw/data/palettes/level/grass_rocks/original_white_2.mw3
new file mode 100644
index 000000000000..596e5b93b404
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/original_white_2.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/recks.mw3 b/worlds/smw/data/palettes/level/grass_rocks/recks.mw3
new file mode 100644
index 000000000000..57ed9fcb375f
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/recks.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/sakura.mw3 b/worlds/smw/data/palettes/level/grass_rocks/sakura.mw3
new file mode 100644
index 000000000000..a339b4a62d15
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/sakura.mw3 differ
diff --git a/worlds/smw/data/palettes/level/grass_rocks/thanks_doc.mw3 b/worlds/smw/data/palettes/level/grass_rocks/thanks_doc.mw3
new file mode 100644
index 000000000000..2359c277f376
Binary files /dev/null and b/worlds/smw/data/palettes/level/grass_rocks/thanks_doc.mw3 differ
diff --git a/worlds/smw/data/palettes/level/logs/brawler.mw3 b/worlds/smw/data/palettes/level/logs/brawler.mw3
new file mode 100644
index 000000000000..ed25ef979498
Binary files /dev/null and b/worlds/smw/data/palettes/level/logs/brawler.mw3 differ
diff --git a/worlds/smw/data/palettes/level/logs/evening.mw3 b/worlds/smw/data/palettes/level/logs/evening.mw3
new file mode 100644
index 000000000000..a5cb1bf58a9c
Binary files /dev/null and b/worlds/smw/data/palettes/level/logs/evening.mw3 differ
diff --git a/worlds/smw/data/palettes/level/logs/mahogany.mw3 b/worlds/smw/data/palettes/level/logs/mahogany.mw3
new file mode 100644
index 000000000000..80d5c2c1adfc
Binary files /dev/null and b/worlds/smw/data/palettes/level/logs/mahogany.mw3 differ
diff --git a/worlds/smw/data/palettes/level/logs/not_quite_dawnbreak.mw3 b/worlds/smw/data/palettes/level/logs/not_quite_dawnbreak.mw3
new file mode 100644
index 000000000000..ecce214b80fb
Binary files /dev/null and b/worlds/smw/data/palettes/level/logs/not_quite_dawnbreak.mw3 differ
diff --git a/worlds/smw/data/palettes/level/logs/original.mw3 b/worlds/smw/data/palettes/level/logs/original.mw3
new file mode 100644
index 000000000000..a10489387001
Binary files /dev/null and b/worlds/smw/data/palettes/level/logs/original.mw3 differ
diff --git a/worlds/smw/data/palettes/level/logs/riesgo_de_chubascos.mw3 b/worlds/smw/data/palettes/level/logs/riesgo_de_chubascos.mw3
new file mode 100644
index 000000000000..0a7e877996b9
Binary files /dev/null and b/worlds/smw/data/palettes/level/logs/riesgo_de_chubascos.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_cave/argent_cave.mw3 b/worlds/smw/data/palettes/level/mushroom_cave/argent_cave.mw3
new file mode 100644
index 000000000000..872685ebc8e8
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_cave/argent_cave.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_cave/glowing_mushroom.mw3 b/worlds/smw/data/palettes/level/mushroom_cave/glowing_mushroom.mw3
new file mode 100644
index 000000000000..b3ed246dc190
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_cave/glowing_mushroom.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_cave/green_aqua.mw3 b/worlds/smw/data/palettes/level/mushroom_cave/green_aqua.mw3
new file mode 100644
index 000000000000..dcd6d0f0940e
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_cave/green_aqua.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_cave/ice.mw3 b/worlds/smw/data/palettes/level/mushroom_cave/ice.mw3
new file mode 100644
index 000000000000..ec308ed93b4a
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_cave/ice.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_cave/original.mw3 b/worlds/smw/data/palettes/level/mushroom_cave/original.mw3
new file mode 100644
index 000000000000..7d97ca364024
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_cave/original.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_cave/really_dark.mw3 b/worlds/smw/data/palettes/level/mushroom_cave/really_dark.mw3
new file mode 100644
index 000000000000..696f6df3bb76
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_cave/really_dark.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_cave/toxic.mw3 b/worlds/smw/data/palettes/level/mushroom_cave/toxic.mw3
new file mode 100644
index 000000000000..75f87d3c389a
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_cave/toxic.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_clouds/atardecer.mw3 b/worlds/smw/data/palettes/level/mushroom_clouds/atardecer.mw3
new file mode 100644
index 000000000000..8e3b464f8b53
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_clouds/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_clouds/greenshroom.mw3 b/worlds/smw/data/palettes/level/mushroom_clouds/greenshroom.mw3
new file mode 100644
index 000000000000..4ad23210af62
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_clouds/greenshroom.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_clouds/oilshroom.mw3 b/worlds/smw/data/palettes/level/mushroom_clouds/oilshroom.mw3
new file mode 100644
index 000000000000..5013182a3e99
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_clouds/oilshroom.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_clouds/original_aqua.mw3 b/worlds/smw/data/palettes/level/mushroom_clouds/original_aqua.mw3
new file mode 100644
index 000000000000..2448aa853472
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_clouds/original_aqua.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_clouds/original_blue.mw3 b/worlds/smw/data/palettes/level/mushroom_clouds/original_blue.mw3
new file mode 100644
index 000000000000..268518d3a692
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_clouds/original_blue.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_clouds/original_yellow.mw3 b/worlds/smw/data/palettes/level/mushroom_clouds/original_yellow.mw3
new file mode 100644
index 000000000000..7241f8e9545c
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_clouds/original_yellow.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_clouds/riesgo_de_chubascos.mw3 b/worlds/smw/data/palettes/level/mushroom_clouds/riesgo_de_chubascos.mw3
new file mode 100644
index 000000000000..d515e876d315
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_clouds/riesgo_de_chubascos.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_forest/atardecer.mw3 b/worlds/smw/data/palettes/level/mushroom_forest/atardecer.mw3
new file mode 100644
index 000000000000..a78e54d23f26
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_forest/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_forest/autumn.mw3 b/worlds/smw/data/palettes/level/mushroom_forest/autumn.mw3
new file mode 100644
index 000000000000..499e038e9e9b
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_forest/autumn.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_forest/count_shroomcula.mw3 b/worlds/smw/data/palettes/level/mushroom_forest/count_shroomcula.mw3
new file mode 100644
index 000000000000..74e336cf6f76
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_forest/count_shroomcula.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_forest/cursed_gold.mw3 b/worlds/smw/data/palettes/level/mushroom_forest/cursed_gold.mw3
new file mode 100644
index 000000000000..f0c7d6124fc6
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_forest/cursed_gold.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_forest/dark_green.mw3 b/worlds/smw/data/palettes/level/mushroom_forest/dark_green.mw3
new file mode 100644
index 000000000000..2b1f15cbb1e0
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_forest/dark_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_forest/lifeless_gray.mw3 b/worlds/smw/data/palettes/level/mushroom_forest/lifeless_gray.mw3
new file mode 100644
index 000000000000..4690266bc650
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_forest/lifeless_gray.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_forest/original.mw3 b/worlds/smw/data/palettes/level/mushroom_forest/original.mw3
new file mode 100644
index 000000000000..cab6dd134869
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_forest/original.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_forest/snow_dark.mw3 b/worlds/smw/data/palettes/level/mushroom_forest/snow_dark.mw3
new file mode 100644
index 000000000000..cf3390d5f186
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_forest/snow_dark.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_forest/snow_green.mw3 b/worlds/smw/data/palettes/level/mushroom_forest/snow_green.mw3
new file mode 100644
index 000000000000..850630f921c6
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_forest/snow_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_hills/atardecer.mw3 b/worlds/smw/data/palettes/level/mushroom_hills/atardecer.mw3
new file mode 100644
index 000000000000..19d9d3245171
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_hills/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_hills/atardecer_naranjo.mw3 b/worlds/smw/data/palettes/level/mushroom_hills/atardecer_naranjo.mw3
new file mode 100644
index 000000000000..dfb9d3b4def1
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_hills/atardecer_naranjo.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_hills/atardecer_verde.mw3 b/worlds/smw/data/palettes/level/mushroom_hills/atardecer_verde.mw3
new file mode 100644
index 000000000000..2c59282acb07
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_hills/atardecer_verde.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_hills/future.mw3 b/worlds/smw/data/palettes/level/mushroom_hills/future.mw3
new file mode 100644
index 000000000000..4b0e3cc59f0b
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_hills/future.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_hills/original.mw3 b/worlds/smw/data/palettes/level/mushroom_hills/original.mw3
new file mode 100644
index 000000000000..3e4854584838
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_hills/original.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_hills/riesgo_de_chubascos_azul.mw3 b/worlds/smw/data/palettes/level/mushroom_hills/riesgo_de_chubascos_azul.mw3
new file mode 100644
index 000000000000..02bfc3fbfe33
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_hills/riesgo_de_chubascos_azul.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_hills/riesgo_de_chubascos_cafe.mw3 b/worlds/smw/data/palettes/level/mushroom_hills/riesgo_de_chubascos_cafe.mw3
new file mode 100644
index 000000000000..e3f6885c27e2
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_hills/riesgo_de_chubascos_cafe.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_hills/riesgo_de_chubascos_negro.mw3 b/worlds/smw/data/palettes/level/mushroom_hills/riesgo_de_chubascos_negro.mw3
new file mode 100644
index 000000000000..ce79e6ac6693
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_hills/riesgo_de_chubascos_negro.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_hills/watermelon_skies.mw3 b/worlds/smw/data/palettes/level/mushroom_hills/watermelon_skies.mw3
new file mode 100644
index 000000000000..c4b24625edca
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_hills/watermelon_skies.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_rocks/atardecer.mw3 b/worlds/smw/data/palettes/level/mushroom_rocks/atardecer.mw3
new file mode 100644
index 000000000000..4540f32bf586
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_rocks/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_rocks/brightshroom.mw3 b/worlds/smw/data/palettes/level/mushroom_rocks/brightshroom.mw3
new file mode 100644
index 000000000000..2b0b66ac4bb0
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_rocks/brightshroom.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_rocks/original_green.mw3 b/worlds/smw/data/palettes/level/mushroom_rocks/original_green.mw3
new file mode 100644
index 000000000000..3c4ba93067ae
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_rocks/original_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_rocks/original_ice.mw3 b/worlds/smw/data/palettes/level/mushroom_rocks/original_ice.mw3
new file mode 100644
index 000000000000..82b72af52030
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_rocks/original_ice.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_rocks/original_volcanic.mw3 b/worlds/smw/data/palettes/level/mushroom_rocks/original_volcanic.mw3
new file mode 100644
index 000000000000..b366b224fadc
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_rocks/original_volcanic.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_rocks/original_white.mw3 b/worlds/smw/data/palettes/level/mushroom_rocks/original_white.mw3
new file mode 100644
index 000000000000..9ab8fe7eaa36
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_rocks/original_white.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_rocks/riesgo_de_chubascos_cafe.mw3 b/worlds/smw/data/palettes/level/mushroom_rocks/riesgo_de_chubascos_cafe.mw3
new file mode 100644
index 000000000000..c8da0fa17881
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_rocks/riesgo_de_chubascos_cafe.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_rocks/riesgo_de_chubascos_negro.mw3 b/worlds/smw/data/palettes/level/mushroom_rocks/riesgo_de_chubascos_negro.mw3
new file mode 100644
index 000000000000..868945e9f427
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_rocks/riesgo_de_chubascos_negro.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_rocks/shuk_ft_reyn.mw3 b/worlds/smw/data/palettes/level/mushroom_rocks/shuk_ft_reyn.mw3
new file mode 100644
index 000000000000..4fa3e7600a15
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_rocks/shuk_ft_reyn.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_stars/atardecer.mw3 b/worlds/smw/data/palettes/level/mushroom_stars/atardecer.mw3
new file mode 100644
index 000000000000..d64a4bb22982
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_stars/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_stars/cool.mw3 b/worlds/smw/data/palettes/level/mushroom_stars/cool.mw3
new file mode 100644
index 000000000000..326b693e141b
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_stars/cool.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_stars/dark_night.mw3 b/worlds/smw/data/palettes/level/mushroom_stars/dark_night.mw3
new file mode 100644
index 000000000000..a4c0442e27e5
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_stars/dark_night.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_stars/halloween.mw3 b/worlds/smw/data/palettes/level/mushroom_stars/halloween.mw3
new file mode 100644
index 000000000000..1fcbd5610253
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_stars/halloween.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_stars/light_pollution.mw3 b/worlds/smw/data/palettes/level/mushroom_stars/light_pollution.mw3
new file mode 100644
index 000000000000..17c47d689170
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_stars/light_pollution.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_stars/midas.mw3 b/worlds/smw/data/palettes/level/mushroom_stars/midas.mw3
new file mode 100644
index 000000000000..9adffb69bd5f
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_stars/midas.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_stars/original_green.mw3 b/worlds/smw/data/palettes/level/mushroom_stars/original_green.mw3
new file mode 100644
index 000000000000..9abb79150620
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_stars/original_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_stars/original_night.mw3 b/worlds/smw/data/palettes/level/mushroom_stars/original_night.mw3
new file mode 100644
index 000000000000..c60a87abc87d
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_stars/original_night.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_stars/purpleish_night.mw3 b/worlds/smw/data/palettes/level/mushroom_stars/purpleish_night.mw3
new file mode 100644
index 000000000000..5757d04f552d
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_stars/purpleish_night.mw3 differ
diff --git a/worlds/smw/data/palettes/level/mushroom_stars/riesgo_de_chubascos.mw3 b/worlds/smw/data/palettes/level/mushroom_stars/riesgo_de_chubascos.mw3
new file mode 100644
index 000000000000..b17323af6555
Binary files /dev/null and b/worlds/smw/data/palettes/level/mushroom_stars/riesgo_de_chubascos.mw3 differ
diff --git a/worlds/smw/data/palettes/level/palettes.json b/worlds/smw/data/palettes/level/palettes.json
new file mode 100644
index 000000000000..9690c2069db1
--- /dev/null
+++ b/worlds/smw/data/palettes/level/palettes.json
@@ -0,0 +1,358 @@
+{
+ "grass_hills": [
+ "atardecer.mw3",
+ "brawler_green.mw3",
+ "crimson.mw3",
+ "electro.mw3",
+ "geo.mw3",
+ "miku.mw3",
+ "mogumogu.mw3",
+ "nocturno.mw3",
+ "original.mw3",
+ "sakura.mw3",
+ "snow.mw3",
+ "sunsetish_grass_hills.mw3",
+ "toothpaste.mw3"
+ ],
+ "grass_forest": [
+ "atardecer.mw3",
+ "autumn.mw3",
+ "brawler.mw3",
+ "brawler_atardecer.mw3",
+ "brawler_green.mw3",
+ "crimson.mw3",
+ "deep_forest.mw3",
+ "electro.mw3",
+ "geo.mw3",
+ "miku.mw3",
+ "myon.mw3",
+ "original_aqua.mw3",
+ "original_green.mw3",
+ "pizza.mw3",
+ "sakura.mw3",
+ "snow_dark_leaves.mw3",
+ "snow_green.mw3",
+ "winter.mw3"
+ ],
+ "grass_rocks": [
+ "atardecer.mw3",
+ "crimson.mw3",
+ "dark.mw3",
+ "electro.mw3",
+ "geo.mw3",
+ "ice.mw3",
+ "miku.mw3",
+ "napolitano.mw3",
+ "original_aqua.mw3",
+ "original_choco_volcanic.mw3",
+ "original_ice.mw3",
+ "original_volcanic.mw3",
+ "original_volcanic_green.mw3",
+ "original_white.mw3",
+ "original_white_2.mw3",
+ "recks.mw3",
+ "sakura.mw3",
+ "thanks_doc.mw3"
+ ],
+ "grass_clouds": [
+ "atardecer.mw3",
+ "crimson.mw3",
+ "electro.mw3",
+ "geo.mw3",
+ "miku.mw3",
+ "original_blue.mw3",
+ "original_green.mw3",
+ "pizza.mw3",
+ "sakura.mw3",
+ "shukfr.mw3",
+ "snow_day.mw3",
+ "volcanic_rock.mw3"
+ ],
+ "grass_mountains": [
+ "brawler_lifeless.mw3",
+ "classic_sm.mw3",
+ "crimson.mw3",
+ "dry_hills.mw3",
+ "electro.mw3",
+ "geo.mw3",
+ "late_sandish.mw3",
+ "miku.mw3",
+ "original_aqua.mw3",
+ "original_blue.mw3",
+ "original_green.mw3",
+ "original_white.mw3",
+ "recksfr.mw3",
+ "sakura_hills.mw3",
+ "snow_day.mw3"
+ ],
+ "cave": [
+ "brawler_dark.mw3",
+ "brawler_purple.mw3",
+ "brawler_red.mw3",
+ "brawler_teal.mw3",
+ "bright_magma.mw3",
+ "dark_red.mw3",
+ "glowing_mushroom.mw3",
+ "green_depths.mw3",
+ "ice.mw3",
+ "magma_cave.mw3",
+ "original_chocolate.mw3",
+ "original_gray.mw3",
+ "original_ice.mw3",
+ "original_mustard.mw3",
+ "original_volcanic.mw3",
+ "snow.mw3",
+ "toxic.mw3",
+ "toxic_moss.mw3"
+ ],
+ "cave_rocks": [
+ "bocchi_rock_hair_cube_things.mw3",
+ "brawler_volcanic.mw3",
+ "ice.mw3",
+ "layer_2.mw3",
+ "original_gray.mw3",
+ "original_mustard.mw3",
+ "pyra_mythra_ft_pneuma.mw3",
+ "snow.mw3",
+ "toxic.mw3"
+ ],
+ "water": [
+ "dark_water.mw3",
+ "deep_aqua.mw3",
+ "deep_chocolate.mw3",
+ "harmless_magma.mw3",
+ "murky.mw3",
+ "oil_spill.mw3",
+ "original_brown.mw3",
+ "original_gray.mw3",
+ "original_green.mw3",
+ "original_mustard.mw3",
+ "original_volcanic.mw3",
+ "pickle_juice.mw3"
+ ],
+ "mushroom_rocks": [
+ "atardecer.mw3",
+ "brightshroom.mw3",
+ "original_green.mw3",
+ "original_ice.mw3",
+ "original_volcanic.mw3",
+ "original_white.mw3",
+ "riesgo_de_chubascos_cafe.mw3",
+ "riesgo_de_chubascos_negro.mw3",
+ "shuk_ft_reyn.mw3"
+ ],
+ "mushroom_clouds": [
+ "atardecer.mw3",
+ "greenshroom.mw3",
+ "oilshroom.mw3",
+ "original_aqua.mw3",
+ "original_blue.mw3",
+ "original_yellow.mw3",
+ "riesgo_de_chubascos.mw3"
+ ],
+ "mushroom_forest": [
+ "atardecer.mw3",
+ "autumn.mw3",
+ "count_shroomcula.mw3",
+ "cursed_gold.mw3",
+ "dark_green.mw3",
+ "lifeless_gray.mw3",
+ "original.mw3",
+ "snow_dark.mw3",
+ "snow_green.mw3"
+ ],
+ "mushroom_hills": [
+ "atardecer.mw3",
+ "atardecer_naranjo.mw3",
+ "atardecer_verde.mw3",
+ "future.mw3",
+ "original.mw3",
+ "riesgo_de_chubascos_azul.mw3",
+ "riesgo_de_chubascos_cafe.mw3",
+ "riesgo_de_chubascos_negro.mw3",
+ "watermelon_skies.mw3"
+ ],
+ "mushroom_stars": [
+ "atardecer.mw3",
+ "cool.mw3",
+ "dark_night.mw3",
+ "halloween.mw3",
+ "light_pollution.mw3",
+ "midas.mw3",
+ "original_green.mw3",
+ "original_night.mw3",
+ "purpleish_night.mw3",
+ "riesgo_de_chubascos.mw3"
+ ],
+ "mushroom_cave": [
+ "argent_cave.mw3",
+ "glowing_mushroom.mw3",
+ "green_aqua.mw3",
+ "ice.mw3",
+ "original.mw3",
+ "really_dark.mw3",
+ "toxic.mw3"
+ ],
+ "forest": [
+ "agnian_queen.mw3",
+ "atardecer.mw3",
+ "frozen.mw3",
+ "halloween.mw3",
+ "kevesi_queen.mw3",
+ "original_dark.mw3",
+ "original_fall.mw3",
+ "original_green.mw3",
+ "sakura.mw3",
+ "snow_dark_leaves.mw3",
+ "snow_green_leaves.mw3"
+ ],
+ "logs": [
+ "brawler.mw3",
+ "evening.mw3",
+ "mahogany.mw3",
+ "not_quite_dawnbreak.mw3",
+ "original.mw3",
+ "riesgo_de_chubascos.mw3"
+ ],
+ "clouds": [
+ "atardecer.mw3",
+ "charcoal.mw3",
+ "cloudy.mw3",
+ "cotton_candy.mw3",
+ "original_green.mw3",
+ "original_orange.mw3"
+ ],
+ "castle_pillars": [
+ "agnus_castle.mw3",
+ "cheese.mw3",
+ "chocolate_blue.mw3",
+ "dark_aqua_marine.mw3",
+ "dollhouse.mw3",
+ "gold_caslte.mw3",
+ "keves_castle.mw3",
+ "original_gray.mw3",
+ "original_green.mw3",
+ "original_mustard.mw3",
+ "original_white.mw3",
+ "pink_purple.mw3",
+ "purple_pink.mw3",
+ "sand_gray.mw3",
+ "sand_green.mw3",
+ "shenhe.mw3",
+ "whatsapp.mw3"
+ ],
+ "castle_windows": [
+ "brawler_pink.mw3",
+ "cheese.mw3",
+ "dark_aqua_marine.mw3",
+ "dollhouse.mw3",
+ "original_brown.mw3",
+ "original_gray.mw3",
+ "original_water.mw3",
+ "red_castle.mw3",
+ "shenhe.mw3",
+ "underwater.mw3",
+ "water.mw3",
+ "whatsapp.mw3"
+ ],
+ "castle_wall": [
+ "cheese.mw3",
+ "dollhouse.mw3",
+ "grand_marshall.mw3",
+ "hot_wall.mw3",
+ "original.mw3",
+ "sand_green.mw3",
+ "shenhe.mw3",
+ "water.mw3"
+ ],
+ "castle_small_windows": [
+ "dark_lava.mw3",
+ "dark_purple.mw3",
+ "dollhouse.mw3",
+ "forgotten_temple.mw3",
+ "original_gray.mw3",
+ "original_volcanic.mw3",
+ "original_water.mw3",
+ "sand_gray.mw3",
+ "sand_green.mw3",
+ "shenhe.mw3",
+ "water.mw3",
+ "whatsapp.mw3"
+ ],
+ "ghost_house": [
+ "brawler_cyan.mw3",
+ "brawler_orange.mw3",
+ "brawler_purple.mw3",
+ "creepypasta.mw3",
+ "crimson_house.mw3",
+ "golden_house.mw3",
+ "halloween_pallet.mw3",
+ "orange_lights.mw3",
+ "original_aqua.mw3",
+ "original_blue.mw3",
+ "original_dark.mw3",
+ "original_white.mw3"
+ ],
+ "ghost_house_exit": [
+ "evening_exit.mw3",
+ "golden_house.mw3",
+ "original.mw3",
+ "original_blue_door.mw3",
+ "underwater.mw3"
+ ],
+ "ship_exterior": [
+ "blue_purple.mw3",
+ "doc_ship.mw3",
+ "grey_ship.mw3",
+ "original.mw3",
+ "reddish.mw3"
+ ],
+ "ship_interior": [
+ "blue_purple.mw3",
+ "bocchi_hitori.mw3",
+ "bocchi_rock.mw3",
+ "brawler.mw3",
+ "grey_ship.mw3",
+ "original.mw3"
+ ],
+ "switch_palace": [
+ "blue_grid.mw3",
+ "brawler_brown.mw3",
+ "cafe_claro.mw3",
+ "color_del_gato_2.mw3",
+ "color_de_gato.mw3",
+ "green_grid.mw3",
+ "gris.mw3",
+ "mario_pants.mw3",
+ "monado.mw3",
+ "morado.mw3",
+ "negro.mw3",
+ "onigiria.mw3",
+ "original.mw3",
+ "original_bonus.mw3",
+ "pink.mw3",
+ "red_grid.mw3",
+ "verde.mw3",
+ "verde_agua.mw3",
+ "yellow_grid.mw3",
+ "youbonus.mw3"
+ ],
+ "yoshi_house": [
+ "atardecer.mw3",
+ "brawler_green.mw3",
+ "choco.mw3",
+ "crimson.mw3",
+ "miku.mw3",
+ "mogumogu.mw3",
+ "monocromo.mw3",
+ "neon.mw3",
+ "nieve.mw3",
+ "night.mw3",
+ "nocturno.mw3",
+ "original.mw3",
+ "sakura.mw3",
+ "snow.mw3",
+ "strong_sun.mw3",
+ "sunsetish_grass_hills.mw3"
+ ]
+}
\ No newline at end of file
diff --git a/worlds/smw/data/palettes/level/ship_exterior/blue_purple.mw3 b/worlds/smw/data/palettes/level/ship_exterior/blue_purple.mw3
new file mode 100644
index 000000000000..2e098de76404
Binary files /dev/null and b/worlds/smw/data/palettes/level/ship_exterior/blue_purple.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ship_exterior/doc_ship.mw3 b/worlds/smw/data/palettes/level/ship_exterior/doc_ship.mw3
new file mode 100644
index 000000000000..8946830bb78d
Binary files /dev/null and b/worlds/smw/data/palettes/level/ship_exterior/doc_ship.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ship_exterior/grey_ship.mw3 b/worlds/smw/data/palettes/level/ship_exterior/grey_ship.mw3
new file mode 100644
index 000000000000..406656ff64e3
Binary files /dev/null and b/worlds/smw/data/palettes/level/ship_exterior/grey_ship.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ship_exterior/original.mw3 b/worlds/smw/data/palettes/level/ship_exterior/original.mw3
new file mode 100644
index 000000000000..20eec6ee80be
Binary files /dev/null and b/worlds/smw/data/palettes/level/ship_exterior/original.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ship_exterior/reddish.mw3 b/worlds/smw/data/palettes/level/ship_exterior/reddish.mw3
new file mode 100644
index 000000000000..674aafd0c0a6
Binary files /dev/null and b/worlds/smw/data/palettes/level/ship_exterior/reddish.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ship_interior/blue_purple.mw3 b/worlds/smw/data/palettes/level/ship_interior/blue_purple.mw3
new file mode 100644
index 000000000000..9ac0f022af3d
Binary files /dev/null and b/worlds/smw/data/palettes/level/ship_interior/blue_purple.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ship_interior/bocchi_hitori.mw3 b/worlds/smw/data/palettes/level/ship_interior/bocchi_hitori.mw3
new file mode 100644
index 000000000000..646c240f68e2
Binary files /dev/null and b/worlds/smw/data/palettes/level/ship_interior/bocchi_hitori.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ship_interior/bocchi_rock.mw3 b/worlds/smw/data/palettes/level/ship_interior/bocchi_rock.mw3
new file mode 100644
index 000000000000..4d45708585c8
Binary files /dev/null and b/worlds/smw/data/palettes/level/ship_interior/bocchi_rock.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ship_interior/brawler.mw3 b/worlds/smw/data/palettes/level/ship_interior/brawler.mw3
new file mode 100644
index 000000000000..d1f3f03d7e90
Binary files /dev/null and b/worlds/smw/data/palettes/level/ship_interior/brawler.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ship_interior/grey_ship.mw3 b/worlds/smw/data/palettes/level/ship_interior/grey_ship.mw3
new file mode 100644
index 000000000000..786802304f33
Binary files /dev/null and b/worlds/smw/data/palettes/level/ship_interior/grey_ship.mw3 differ
diff --git a/worlds/smw/data/palettes/level/ship_interior/original.mw3 b/worlds/smw/data/palettes/level/ship_interior/original.mw3
new file mode 100644
index 000000000000..a208db211f51
Binary files /dev/null and b/worlds/smw/data/palettes/level/ship_interior/original.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/blue_grid.mw3 b/worlds/smw/data/palettes/level/switch_palace/blue_grid.mw3
new file mode 100644
index 000000000000..d613b8061deb
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/blue_grid.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/brawler_brown.mw3 b/worlds/smw/data/palettes/level/switch_palace/brawler_brown.mw3
new file mode 100644
index 000000000000..a3073c31adca
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/brawler_brown.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/cafe_claro.mw3 b/worlds/smw/data/palettes/level/switch_palace/cafe_claro.mw3
new file mode 100644
index 000000000000..90ffc5cf73f8
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/cafe_claro.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/color_de_gato.mw3 b/worlds/smw/data/palettes/level/switch_palace/color_de_gato.mw3
new file mode 100644
index 000000000000..b5ba743bfae9
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/color_de_gato.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/color_del_gato_2.mw3 b/worlds/smw/data/palettes/level/switch_palace/color_del_gato_2.mw3
new file mode 100644
index 000000000000..9c52a56a872a
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/color_del_gato_2.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/green_grid.mw3 b/worlds/smw/data/palettes/level/switch_palace/green_grid.mw3
new file mode 100644
index 000000000000..3a8ded956b43
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/green_grid.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/gris.mw3 b/worlds/smw/data/palettes/level/switch_palace/gris.mw3
new file mode 100644
index 000000000000..8e8df8008bb4
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/gris.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/mario_pants.mw3 b/worlds/smw/data/palettes/level/switch_palace/mario_pants.mw3
new file mode 100644
index 000000000000..b18aee2e1841
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/mario_pants.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/monado.mw3 b/worlds/smw/data/palettes/level/switch_palace/monado.mw3
new file mode 100644
index 000000000000..c37acf989847
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/monado.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/morado.mw3 b/worlds/smw/data/palettes/level/switch_palace/morado.mw3
new file mode 100644
index 000000000000..ab495c8b33b6
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/morado.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/negro.mw3 b/worlds/smw/data/palettes/level/switch_palace/negro.mw3
new file mode 100644
index 000000000000..dd9db46ffdea
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/negro.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/onigiria.mw3 b/worlds/smw/data/palettes/level/switch_palace/onigiria.mw3
new file mode 100644
index 000000000000..7632dc08b226
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/onigiria.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/original.mw3 b/worlds/smw/data/palettes/level/switch_palace/original.mw3
new file mode 100644
index 000000000000..9cc5a26c042b
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/original.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/original_bonus.mw3 b/worlds/smw/data/palettes/level/switch_palace/original_bonus.mw3
new file mode 100644
index 000000000000..b793117f4a6b
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/original_bonus.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/pink.mw3 b/worlds/smw/data/palettes/level/switch_palace/pink.mw3
new file mode 100644
index 000000000000..aaa49aa32fc9
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/pink.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/red_grid.mw3 b/worlds/smw/data/palettes/level/switch_palace/red_grid.mw3
new file mode 100644
index 000000000000..1336dc834680
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/red_grid.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/verde.mw3 b/worlds/smw/data/palettes/level/switch_palace/verde.mw3
new file mode 100644
index 000000000000..92515d0c32f5
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/verde.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/verde_agua.mw3 b/worlds/smw/data/palettes/level/switch_palace/verde_agua.mw3
new file mode 100644
index 000000000000..f845850fbaf0
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/verde_agua.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/yellow_grid.mw3 b/worlds/smw/data/palettes/level/switch_palace/yellow_grid.mw3
new file mode 100644
index 000000000000..c41276492eb0
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/yellow_grid.mw3 differ
diff --git a/worlds/smw/data/palettes/level/switch_palace/youbonus.mw3 b/worlds/smw/data/palettes/level/switch_palace/youbonus.mw3
new file mode 100644
index 000000000000..b213f5660795
Binary files /dev/null and b/worlds/smw/data/palettes/level/switch_palace/youbonus.mw3 differ
diff --git a/worlds/smw/data/palettes/level/water/dark_water.mw3 b/worlds/smw/data/palettes/level/water/dark_water.mw3
new file mode 100644
index 000000000000..e0af55f5d2f8
Binary files /dev/null and b/worlds/smw/data/palettes/level/water/dark_water.mw3 differ
diff --git a/worlds/smw/data/palettes/level/water/deep_aqua.mw3 b/worlds/smw/data/palettes/level/water/deep_aqua.mw3
new file mode 100644
index 000000000000..7407102bf716
Binary files /dev/null and b/worlds/smw/data/palettes/level/water/deep_aqua.mw3 differ
diff --git a/worlds/smw/data/palettes/level/water/deep_chocolate.mw3 b/worlds/smw/data/palettes/level/water/deep_chocolate.mw3
new file mode 100644
index 000000000000..19d645e39f4c
Binary files /dev/null and b/worlds/smw/data/palettes/level/water/deep_chocolate.mw3 differ
diff --git a/worlds/smw/data/palettes/level/water/harmless_magma.mw3 b/worlds/smw/data/palettes/level/water/harmless_magma.mw3
new file mode 100644
index 000000000000..043870cf82c4
Binary files /dev/null and b/worlds/smw/data/palettes/level/water/harmless_magma.mw3 differ
diff --git a/worlds/smw/data/palettes/level/water/murky.mw3 b/worlds/smw/data/palettes/level/water/murky.mw3
new file mode 100644
index 000000000000..57ea1ce44f5e
Binary files /dev/null and b/worlds/smw/data/palettes/level/water/murky.mw3 differ
diff --git a/worlds/smw/data/palettes/level/water/oil_spill.mw3 b/worlds/smw/data/palettes/level/water/oil_spill.mw3
new file mode 100644
index 000000000000..ac1ffed27f62
Binary files /dev/null and b/worlds/smw/data/palettes/level/water/oil_spill.mw3 differ
diff --git a/worlds/smw/data/palettes/level/water/original_brown.mw3 b/worlds/smw/data/palettes/level/water/original_brown.mw3
new file mode 100644
index 000000000000..5f5366cebd11
Binary files /dev/null and b/worlds/smw/data/palettes/level/water/original_brown.mw3 differ
diff --git a/worlds/smw/data/palettes/level/water/original_gray.mw3 b/worlds/smw/data/palettes/level/water/original_gray.mw3
new file mode 100644
index 000000000000..b5087eccbed5
Binary files /dev/null and b/worlds/smw/data/palettes/level/water/original_gray.mw3 differ
diff --git a/worlds/smw/data/palettes/level/water/original_green.mw3 b/worlds/smw/data/palettes/level/water/original_green.mw3
new file mode 100644
index 000000000000..6697f2839edc
Binary files /dev/null and b/worlds/smw/data/palettes/level/water/original_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/water/original_mustard.mw3 b/worlds/smw/data/palettes/level/water/original_mustard.mw3
new file mode 100644
index 000000000000..bf14a8cb157e
Binary files /dev/null and b/worlds/smw/data/palettes/level/water/original_mustard.mw3 differ
diff --git a/worlds/smw/data/palettes/level/water/original_volcanic.mw3 b/worlds/smw/data/palettes/level/water/original_volcanic.mw3
new file mode 100644
index 000000000000..ef22bf7bf6a0
Binary files /dev/null and b/worlds/smw/data/palettes/level/water/original_volcanic.mw3 differ
diff --git a/worlds/smw/data/palettes/level/water/pickle_juice.mw3 b/worlds/smw/data/palettes/level/water/pickle_juice.mw3
new file mode 100644
index 000000000000..a27ebcfd8975
Binary files /dev/null and b/worlds/smw/data/palettes/level/water/pickle_juice.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/atardecer.mw3 b/worlds/smw/data/palettes/level/yoshi_house/atardecer.mw3
new file mode 100644
index 000000000000..561b3f490217
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/brawler_green.mw3 b/worlds/smw/data/palettes/level/yoshi_house/brawler_green.mw3
new file mode 100644
index 000000000000..96c93099f306
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/brawler_green.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/choco.mw3 b/worlds/smw/data/palettes/level/yoshi_house/choco.mw3
new file mode 100644
index 000000000000..cd4ebe585dd5
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/choco.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/crimson.mw3 b/worlds/smw/data/palettes/level/yoshi_house/crimson.mw3
new file mode 100644
index 000000000000..e334904a372b
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/crimson.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/miku.mw3 b/worlds/smw/data/palettes/level/yoshi_house/miku.mw3
new file mode 100644
index 000000000000..5d3b9e81144a
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/miku.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/mogumogu.mw3 b/worlds/smw/data/palettes/level/yoshi_house/mogumogu.mw3
new file mode 100644
index 000000000000..cf76396c8fb0
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/mogumogu.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/monocromo.mw3 b/worlds/smw/data/palettes/level/yoshi_house/monocromo.mw3
new file mode 100644
index 000000000000..8e1529f51498
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/monocromo.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/neon.mw3 b/worlds/smw/data/palettes/level/yoshi_house/neon.mw3
new file mode 100644
index 000000000000..d3e183a770fd
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/neon.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/nieve.mw3 b/worlds/smw/data/palettes/level/yoshi_house/nieve.mw3
new file mode 100644
index 000000000000..07db0b1339dd
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/nieve.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/night.mw3 b/worlds/smw/data/palettes/level/yoshi_house/night.mw3
new file mode 100644
index 000000000000..7cc0122339b0
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/night.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/nocturno.mw3 b/worlds/smw/data/palettes/level/yoshi_house/nocturno.mw3
new file mode 100644
index 000000000000..de764ef9e823
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/nocturno.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/original.mw3 b/worlds/smw/data/palettes/level/yoshi_house/original.mw3
new file mode 100644
index 000000000000..051d266fbd86
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/original.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/sakura.mw3 b/worlds/smw/data/palettes/level/yoshi_house/sakura.mw3
new file mode 100644
index 000000000000..d099ba935312
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/sakura.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/snow.mw3 b/worlds/smw/data/palettes/level/yoshi_house/snow.mw3
new file mode 100644
index 000000000000..4fea26a690b2
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/snow.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/strong_sun.mw3 b/worlds/smw/data/palettes/level/yoshi_house/strong_sun.mw3
new file mode 100644
index 000000000000..7c671aa368ed
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/strong_sun.mw3 differ
diff --git a/worlds/smw/data/palettes/level/yoshi_house/sunsetish_grass_hills.mw3 b/worlds/smw/data/palettes/level/yoshi_house/sunsetish_grass_hills.mw3
new file mode 100644
index 000000000000..c51bdfc6a7d3
Binary files /dev/null and b/worlds/smw/data/palettes/level/yoshi_house/sunsetish_grass_hills.mw3 differ
diff --git a/worlds/smw/data/palettes/map/forest/atardecer.mw3 b/worlds/smw/data/palettes/map/forest/atardecer.mw3
new file mode 100644
index 000000000000..5ce855399b98
Binary files /dev/null and b/worlds/smw/data/palettes/map/forest/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/map/forest/burnt_forest.mw3 b/worlds/smw/data/palettes/map/forest/burnt_forest.mw3
new file mode 100644
index 000000000000..58b0648f18e3
Binary files /dev/null and b/worlds/smw/data/palettes/map/forest/burnt_forest.mw3 differ
diff --git a/worlds/smw/data/palettes/map/forest/dark_forest.mw3 b/worlds/smw/data/palettes/map/forest/dark_forest.mw3
new file mode 100644
index 000000000000..90b9b18809bc
Binary files /dev/null and b/worlds/smw/data/palettes/map/forest/dark_forest.mw3 differ
diff --git a/worlds/smw/data/palettes/map/forest/halloween.mw3 b/worlds/smw/data/palettes/map/forest/halloween.mw3
new file mode 100644
index 000000000000..c3899751bf31
Binary files /dev/null and b/worlds/smw/data/palettes/map/forest/halloween.mw3 differ
diff --git a/worlds/smw/data/palettes/map/forest/ice_forest.mw3 b/worlds/smw/data/palettes/map/forest/ice_forest.mw3
new file mode 100644
index 000000000000..32d62829bd5c
Binary files /dev/null and b/worlds/smw/data/palettes/map/forest/ice_forest.mw3 differ
diff --git a/worlds/smw/data/palettes/map/forest/lost_woods.mw3 b/worlds/smw/data/palettes/map/forest/lost_woods.mw3
new file mode 100644
index 000000000000..ba9486b627ba
Binary files /dev/null and b/worlds/smw/data/palettes/map/forest/lost_woods.mw3 differ
diff --git a/worlds/smw/data/palettes/map/forest/mono.mw3 b/worlds/smw/data/palettes/map/forest/mono.mw3
new file mode 100644
index 000000000000..32bce2c463ac
Binary files /dev/null and b/worlds/smw/data/palettes/map/forest/mono.mw3 differ
diff --git a/worlds/smw/data/palettes/map/forest/original.mw3 b/worlds/smw/data/palettes/map/forest/original.mw3
new file mode 100644
index 000000000000..766522e0651d
Binary files /dev/null and b/worlds/smw/data/palettes/map/forest/original.mw3 differ
diff --git a/worlds/smw/data/palettes/map/forest/original_special.mw3 b/worlds/smw/data/palettes/map/forest/original_special.mw3
new file mode 100644
index 000000000000..57fcf7dea59b
Binary files /dev/null and b/worlds/smw/data/palettes/map/forest/original_special.mw3 differ
diff --git a/worlds/smw/data/palettes/map/forest/sepia.mw3 b/worlds/smw/data/palettes/map/forest/sepia.mw3
new file mode 100644
index 000000000000..b265f978060d
Binary files /dev/null and b/worlds/smw/data/palettes/map/forest/sepia.mw3 differ
diff --git a/worlds/smw/data/palettes/map/forest/snow_day.mw3 b/worlds/smw/data/palettes/map/forest/snow_day.mw3
new file mode 100644
index 000000000000..c76540325bae
Binary files /dev/null and b/worlds/smw/data/palettes/map/forest/snow_day.mw3 differ
diff --git a/worlds/smw/data/palettes/map/main/atardecer.mw3 b/worlds/smw/data/palettes/map/main/atardecer.mw3
new file mode 100644
index 000000000000..5252db002dfe
Binary files /dev/null and b/worlds/smw/data/palettes/map/main/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/map/main/brawler.mw3 b/worlds/smw/data/palettes/map/main/brawler.mw3
new file mode 100644
index 000000000000..731696fcfc82
Binary files /dev/null and b/worlds/smw/data/palettes/map/main/brawler.mw3 differ
diff --git a/worlds/smw/data/palettes/map/main/cake_frosting.mw3 b/worlds/smw/data/palettes/map/main/cake_frosting.mw3
new file mode 100644
index 000000000000..aec0fd7e40c9
Binary files /dev/null and b/worlds/smw/data/palettes/map/main/cake_frosting.mw3 differ
diff --git a/worlds/smw/data/palettes/map/main/invertido.mw3 b/worlds/smw/data/palettes/map/main/invertido.mw3
new file mode 100644
index 000000000000..79a7147acd17
Binary files /dev/null and b/worlds/smw/data/palettes/map/main/invertido.mw3 differ
diff --git a/worlds/smw/data/palettes/map/main/mono.mw3 b/worlds/smw/data/palettes/map/main/mono.mw3
new file mode 100644
index 000000000000..d4d0f72d2955
Binary files /dev/null and b/worlds/smw/data/palettes/map/main/mono.mw3 differ
diff --git a/worlds/smw/data/palettes/map/main/morning.mw3 b/worlds/smw/data/palettes/map/main/morning.mw3
new file mode 100644
index 000000000000..b2fe88e2cf89
Binary files /dev/null and b/worlds/smw/data/palettes/map/main/morning.mw3 differ
diff --git a/worlds/smw/data/palettes/map/main/night.mw3 b/worlds/smw/data/palettes/map/main/night.mw3
new file mode 100644
index 000000000000..a5e84ec80c00
Binary files /dev/null and b/worlds/smw/data/palettes/map/main/night.mw3 differ
diff --git a/worlds/smw/data/palettes/map/main/night_time.mw3 b/worlds/smw/data/palettes/map/main/night_time.mw3
new file mode 100644
index 000000000000..13a5af30c2e5
Binary files /dev/null and b/worlds/smw/data/palettes/map/main/night_time.mw3 differ
diff --git a/worlds/smw/data/palettes/map/main/original.mw3 b/worlds/smw/data/palettes/map/main/original.mw3
new file mode 100644
index 000000000000..cb655d882e71
Binary files /dev/null and b/worlds/smw/data/palettes/map/main/original.mw3 differ
diff --git a/worlds/smw/data/palettes/map/main/original_special.mw3 b/worlds/smw/data/palettes/map/main/original_special.mw3
new file mode 100644
index 000000000000..5c1e63e8fcd3
Binary files /dev/null and b/worlds/smw/data/palettes/map/main/original_special.mw3 differ
diff --git a/worlds/smw/data/palettes/map/main/sepia.mw3 b/worlds/smw/data/palettes/map/main/sepia.mw3
new file mode 100644
index 000000000000..3a6ece7743e9
Binary files /dev/null and b/worlds/smw/data/palettes/map/main/sepia.mw3 differ
diff --git a/worlds/smw/data/palettes/map/main/snow_day.mw3 b/worlds/smw/data/palettes/map/main/snow_day.mw3
new file mode 100644
index 000000000000..1ad307f078dc
Binary files /dev/null and b/worlds/smw/data/palettes/map/main/snow_day.mw3 differ
diff --git a/worlds/smw/data/palettes/map/palettes.json b/worlds/smw/data/palettes/map/palettes.json
new file mode 100644
index 000000000000..a926418cf934
--- /dev/null
+++ b/worlds/smw/data/palettes/map/palettes.json
@@ -0,0 +1,92 @@
+{
+ "main": [
+ "atardecer.mw3",
+ "brawler.mw3",
+ "cake_frosting.mw3",
+ "invertido.mw3",
+ "mono.mw3",
+ "morning.mw3",
+ "night.mw3",
+ "night_time.mw3",
+ "original.mw3",
+ "original_special.mw3",
+ "sepia.mw3",
+ "snow_day.mw3"
+ ],
+ "yoshi": [
+ "atardecer.mw3",
+ "gum.mw3",
+ "lava_island.mw3",
+ "mono.mw3",
+ "original.mw3",
+ "original_special.mw3",
+ "sepia.mw3",
+ "snow_day.mw3",
+ "sunset.mw3",
+ "tritanopia.mw3",
+ "yochis_ailand.mw3"
+ ],
+ "vanilla": [
+ "aqua_marine.mw3",
+ "dark cave.mw3",
+ "DOMO.mw3",
+ "fire cave.mw3",
+ "gold_mine.mw3",
+ "invertido.mw3",
+ "mono.mw3",
+ "original.mw3",
+ "original_special.mw3",
+ "purple.mw3",
+ "sepia.mw3",
+ "witches_cauldron.mw3"
+ ],
+ "forest": [
+ "atardecer.mw3",
+ "burnt_forest.mw3",
+ "dark_forest.mw3",
+ "halloween.mw3",
+ "ice_forest.mw3",
+ "lost_woods.mw3",
+ "mono.mw3",
+ "original.mw3",
+ "original_special.mw3",
+ "sepia.mw3",
+ "snow_day.mw3"
+ ],
+ "valley": [
+ "bowser.mw3",
+ "castle_colors.mw3",
+ "dark cave.mw3",
+ "dream_world.mw3",
+ "fire cave.mw3",
+ "invertido.mw3",
+ "mono.mw3",
+ "orange.mw3",
+ "original.mw3",
+ "original_special.mw3",
+ "purple_blue.mw3",
+ "sepia.mw3",
+ "snow.mw3",
+ "Tamaulipas.mw3"
+ ],
+ "special": [
+ "black_out.mw3",
+ "blood_star.mw3",
+ "brawler.mw3",
+ "green.mw3",
+ "light_pollution_map.mw3",
+ "original.mw3",
+ "purple.mw3",
+ "white_special.mw3"
+ ],
+ "star": [
+ "blood_moon.mw3",
+ "mono.mw3",
+ "mountain_top.mw3",
+ "original.mw3",
+ "original_special.mw3",
+ "pink_star.mw3",
+ "sepia.mw3",
+ "yellow_star.mw3"
+ ]
+}
\ No newline at end of file
diff --git a/worlds/smw/data/palettes/map/special/black_out.mw3 b/worlds/smw/data/palettes/map/special/black_out.mw3
new file mode 100644
index 000000000000..52d4e6d4a40f
Binary files /dev/null and b/worlds/smw/data/palettes/map/special/black_out.mw3 differ
diff --git a/worlds/smw/data/palettes/map/special/blood_star.mw3 b/worlds/smw/data/palettes/map/special/blood_star.mw3
new file mode 100644
index 000000000000..bc778b202b99
Binary files /dev/null and b/worlds/smw/data/palettes/map/special/blood_star.mw3 differ
diff --git a/worlds/smw/data/palettes/map/special/brawler.mw3 b/worlds/smw/data/palettes/map/special/brawler.mw3
new file mode 100644
index 000000000000..595edb04935b
Binary files /dev/null and b/worlds/smw/data/palettes/map/special/brawler.mw3 differ
diff --git a/worlds/smw/data/palettes/map/special/green.mw3 b/worlds/smw/data/palettes/map/special/green.mw3
new file mode 100644
index 000000000000..2bdb5fd4e075
Binary files /dev/null and b/worlds/smw/data/palettes/map/special/green.mw3 differ
diff --git a/worlds/smw/data/palettes/map/special/light_pollution_map.mw3 b/worlds/smw/data/palettes/map/special/light_pollution_map.mw3
new file mode 100644
index 000000000000..a1bc6bbcf9a1
Binary files /dev/null and b/worlds/smw/data/palettes/map/special/light_pollution_map.mw3 differ
diff --git a/worlds/smw/data/palettes/map/special/original.mw3 b/worlds/smw/data/palettes/map/special/original.mw3
new file mode 100644
index 000000000000..0a75a99a0400
Binary files /dev/null and b/worlds/smw/data/palettes/map/special/original.mw3 differ
diff --git a/worlds/smw/data/palettes/map/special/purple.mw3 b/worlds/smw/data/palettes/map/special/purple.mw3
new file mode 100644
index 000000000000..122b5c785af4
Binary files /dev/null and b/worlds/smw/data/palettes/map/special/purple.mw3 differ
diff --git a/worlds/smw/data/palettes/map/special/white_special.mw3 b/worlds/smw/data/palettes/map/special/white_special.mw3
new file mode 100644
index 000000000000..e4d2613aa964
Binary files /dev/null and b/worlds/smw/data/palettes/map/special/white_special.mw3 differ
diff --git a/worlds/smw/data/palettes/map/star/blood_moon.mw3 b/worlds/smw/data/palettes/map/star/blood_moon.mw3
new file mode 100644
index 000000000000..42f155ef33e2
Binary files /dev/null and b/worlds/smw/data/palettes/map/star/blood_moon.mw3 differ
diff --git a/worlds/smw/data/palettes/map/star/mono.mw3 b/worlds/smw/data/palettes/map/star/mono.mw3
new file mode 100644
index 000000000000..15d33bdf3a23
Binary files /dev/null and b/worlds/smw/data/palettes/map/star/mono.mw3 differ
diff --git a/worlds/smw/data/palettes/map/star/mountain_top.mw3 b/worlds/smw/data/palettes/map/star/mountain_top.mw3
new file mode 100644
index 000000000000..d2b96b0e3dd2
Binary files /dev/null and b/worlds/smw/data/palettes/map/star/mountain_top.mw3 differ
diff --git a/worlds/smw/data/palettes/map/star/original.mw3 b/worlds/smw/data/palettes/map/star/original.mw3
new file mode 100644
index 000000000000..2107a5555eba
Binary files /dev/null and b/worlds/smw/data/palettes/map/star/original.mw3 differ
diff --git a/worlds/smw/data/palettes/map/star/original_special.mw3 b/worlds/smw/data/palettes/map/star/original_special.mw3
new file mode 100644
index 000000000000..d2bd439c97b5
Binary files /dev/null and b/worlds/smw/data/palettes/map/star/original_special.mw3 differ
diff --git a/worlds/smw/data/palettes/map/star/pink_star.mw3 b/worlds/smw/data/palettes/map/star/pink_star.mw3
new file mode 100644
index 000000000000..55f68ecf6ce2
Binary files /dev/null and b/worlds/smw/data/palettes/map/star/pink_star.mw3 differ
diff --git a/worlds/smw/data/palettes/map/star/sepia.mw3 b/worlds/smw/data/palettes/map/star/sepia.mw3
new file mode 100644
index 000000000000..4c6a5f3c1df2
Binary files /dev/null and b/worlds/smw/data/palettes/map/star/sepia.mw3 differ
diff --git a/worlds/smw/data/palettes/map/star/yellow_star.mw3 b/worlds/smw/data/palettes/map/star/yellow_star.mw3
new file mode 100644
index 000000000000..96028903959a
Binary files /dev/null and b/worlds/smw/data/palettes/map/star/yellow_star.mw3 differ
diff --git a/worlds/smw/data/palettes/map/valley/Tamaulipas.mw3 b/worlds/smw/data/palettes/map/valley/Tamaulipas.mw3
new file mode 100644
index 000000000000..0cb18cc2fd3e
Binary files /dev/null and b/worlds/smw/data/palettes/map/valley/Tamaulipas.mw3 differ
diff --git a/worlds/smw/data/palettes/map/valley/bowser.mw3 b/worlds/smw/data/palettes/map/valley/bowser.mw3
new file mode 100644
index 000000000000..f3d6c23aa060
Binary files /dev/null and b/worlds/smw/data/palettes/map/valley/bowser.mw3 differ
diff --git a/worlds/smw/data/palettes/map/valley/castle_colors.mw3 b/worlds/smw/data/palettes/map/valley/castle_colors.mw3
new file mode 100644
index 000000000000..7e245cc64ecb
Binary files /dev/null and b/worlds/smw/data/palettes/map/valley/castle_colors.mw3 differ
diff --git a/worlds/smw/data/palettes/map/valley/dark cave.mw3 b/worlds/smw/data/palettes/map/valley/dark cave.mw3
new file mode 100644
index 000000000000..2b6c0f57cc61
Binary files /dev/null and b/worlds/smw/data/palettes/map/valley/dark cave.mw3 differ
diff --git a/worlds/smw/data/palettes/map/valley/dream_world.mw3 b/worlds/smw/data/palettes/map/valley/dream_world.mw3
new file mode 100644
index 000000000000..bcf8d9514213
Binary files /dev/null and b/worlds/smw/data/palettes/map/valley/dream_world.mw3 differ
diff --git a/worlds/smw/data/palettes/map/valley/fire cave.mw3 b/worlds/smw/data/palettes/map/valley/fire cave.mw3
new file mode 100644
index 000000000000..2980210dd233
Binary files /dev/null and b/worlds/smw/data/palettes/map/valley/fire cave.mw3 differ
diff --git a/worlds/smw/data/palettes/map/valley/invertido.mw3 b/worlds/smw/data/palettes/map/valley/invertido.mw3
new file mode 100644
index 000000000000..37cf2e9f50d2
Binary files /dev/null and b/worlds/smw/data/palettes/map/valley/invertido.mw3 differ
diff --git a/worlds/smw/data/palettes/map/valley/mono.mw3 b/worlds/smw/data/palettes/map/valley/mono.mw3
new file mode 100644
index 000000000000..f96409bebb0f
Binary files /dev/null and b/worlds/smw/data/palettes/map/valley/mono.mw3 differ
diff --git a/worlds/smw/data/palettes/map/valley/orange.mw3 b/worlds/smw/data/palettes/map/valley/orange.mw3
new file mode 100644
index 000000000000..c9f6ac2ff2e9
Binary files /dev/null and b/worlds/smw/data/palettes/map/valley/orange.mw3 differ
diff --git a/worlds/smw/data/palettes/map/valley/original.mw3 b/worlds/smw/data/palettes/map/valley/original.mw3
new file mode 100644
index 000000000000..c165e81b818b
Binary files /dev/null and b/worlds/smw/data/palettes/map/valley/original.mw3 differ
diff --git a/worlds/smw/data/palettes/map/valley/original_special.mw3 b/worlds/smw/data/palettes/map/valley/original_special.mw3
new file mode 100644
index 000000000000..a4a0acda9ed6
Binary files /dev/null and b/worlds/smw/data/palettes/map/valley/original_special.mw3 differ
diff --git a/worlds/smw/data/palettes/map/valley/purple_blue.mw3 b/worlds/smw/data/palettes/map/valley/purple_blue.mw3
new file mode 100644
index 000000000000..2342f0acd205
Binary files /dev/null and b/worlds/smw/data/palettes/map/valley/purple_blue.mw3 differ
diff --git a/worlds/smw/data/palettes/map/valley/sepia.mw3 b/worlds/smw/data/palettes/map/valley/sepia.mw3
new file mode 100644
index 000000000000..aa5aeb51d38b
Binary files /dev/null and b/worlds/smw/data/palettes/map/valley/sepia.mw3 differ
diff --git a/worlds/smw/data/palettes/map/valley/snow.mw3 b/worlds/smw/data/palettes/map/valley/snow.mw3
new file mode 100644
index 000000000000..185d0d42c77a
Binary files /dev/null and b/worlds/smw/data/palettes/map/valley/snow.mw3 differ
diff --git a/worlds/smw/data/palettes/map/vanilla/DOMO.mw3 b/worlds/smw/data/palettes/map/vanilla/DOMO.mw3
new file mode 100644
index 000000000000..595638077050
Binary files /dev/null and b/worlds/smw/data/palettes/map/vanilla/DOMO.mw3 differ
diff --git a/worlds/smw/data/palettes/map/vanilla/aqua_marine.mw3 b/worlds/smw/data/palettes/map/vanilla/aqua_marine.mw3
new file mode 100644
index 000000000000..b382964de02c
Binary files /dev/null and b/worlds/smw/data/palettes/map/vanilla/aqua_marine.mw3 differ
diff --git a/worlds/smw/data/palettes/map/vanilla/dark cave.mw3 b/worlds/smw/data/palettes/map/vanilla/dark cave.mw3
new file mode 100644
index 000000000000..2b6c0f57cc61
Binary files /dev/null and b/worlds/smw/data/palettes/map/vanilla/dark cave.mw3 differ
diff --git a/worlds/smw/data/palettes/map/vanilla/fire cave.mw3 b/worlds/smw/data/palettes/map/vanilla/fire cave.mw3
new file mode 100644
index 000000000000..2980210dd233
Binary files /dev/null and b/worlds/smw/data/palettes/map/vanilla/fire cave.mw3 differ
diff --git a/worlds/smw/data/palettes/map/vanilla/gold_mine.mw3 b/worlds/smw/data/palettes/map/vanilla/gold_mine.mw3
new file mode 100644
index 000000000000..ad5460a75e50
Binary files /dev/null and b/worlds/smw/data/palettes/map/vanilla/gold_mine.mw3 differ
diff --git a/worlds/smw/data/palettes/map/vanilla/invertido.mw3 b/worlds/smw/data/palettes/map/vanilla/invertido.mw3
new file mode 100644
index 000000000000..37cf2e9f50d2
Binary files /dev/null and b/worlds/smw/data/palettes/map/vanilla/invertido.mw3 differ
diff --git a/worlds/smw/data/palettes/map/vanilla/mono.mw3 b/worlds/smw/data/palettes/map/vanilla/mono.mw3
new file mode 100644
index 000000000000..f96409bebb0f
Binary files /dev/null and b/worlds/smw/data/palettes/map/vanilla/mono.mw3 differ
diff --git a/worlds/smw/data/palettes/map/vanilla/original.mw3 b/worlds/smw/data/palettes/map/vanilla/original.mw3
new file mode 100644
index 000000000000..c165e81b818b
Binary files /dev/null and b/worlds/smw/data/palettes/map/vanilla/original.mw3 differ
diff --git a/worlds/smw/data/palettes/map/vanilla/original_special.mw3 b/worlds/smw/data/palettes/map/vanilla/original_special.mw3
new file mode 100644
index 000000000000..a4a0acda9ed6
Binary files /dev/null and b/worlds/smw/data/palettes/map/vanilla/original_special.mw3 differ
diff --git a/worlds/smw/data/palettes/map/vanilla/purple.mw3 b/worlds/smw/data/palettes/map/vanilla/purple.mw3
new file mode 100644
index 000000000000..db0008bca7cd
Binary files /dev/null and b/worlds/smw/data/palettes/map/vanilla/purple.mw3 differ
diff --git a/worlds/smw/data/palettes/map/vanilla/sepia.mw3 b/worlds/smw/data/palettes/map/vanilla/sepia.mw3
new file mode 100644
index 000000000000..aa5aeb51d38b
Binary files /dev/null and b/worlds/smw/data/palettes/map/vanilla/sepia.mw3 differ
diff --git a/worlds/smw/data/palettes/map/vanilla/witches_cauldron.mw3 b/worlds/smw/data/palettes/map/vanilla/witches_cauldron.mw3
new file mode 100644
index 000000000000..ef6a81e5d49d
Binary files /dev/null and b/worlds/smw/data/palettes/map/vanilla/witches_cauldron.mw3 differ
diff --git a/worlds/smw/data/palettes/map/yoshi/atardecer.mw3 b/worlds/smw/data/palettes/map/yoshi/atardecer.mw3
new file mode 100644
index 000000000000..a75c898cee98
Binary files /dev/null and b/worlds/smw/data/palettes/map/yoshi/atardecer.mw3 differ
diff --git a/worlds/smw/data/palettes/map/yoshi/gum.mw3 b/worlds/smw/data/palettes/map/yoshi/gum.mw3
new file mode 100644
index 000000000000..cfde2f53bba4
Binary files /dev/null and b/worlds/smw/data/palettes/map/yoshi/gum.mw3 differ
diff --git a/worlds/smw/data/palettes/map/yoshi/lava_island.mw3 b/worlds/smw/data/palettes/map/yoshi/lava_island.mw3
new file mode 100644
index 000000000000..570bdee3aa9c
Binary files /dev/null and b/worlds/smw/data/palettes/map/yoshi/lava_island.mw3 differ
diff --git a/worlds/smw/data/palettes/map/yoshi/mono.mw3 b/worlds/smw/data/palettes/map/yoshi/mono.mw3
new file mode 100644
index 000000000000..62c9761b4673
Binary files /dev/null and b/worlds/smw/data/palettes/map/yoshi/mono.mw3 differ
diff --git a/worlds/smw/data/palettes/map/yoshi/original.mw3 b/worlds/smw/data/palettes/map/yoshi/original.mw3
new file mode 100644
index 000000000000..eb9451b1fe5c
Binary files /dev/null and b/worlds/smw/data/palettes/map/yoshi/original.mw3 differ
diff --git a/worlds/smw/data/palettes/map/yoshi/original_special.mw3 b/worlds/smw/data/palettes/map/yoshi/original_special.mw3
new file mode 100644
index 000000000000..269b45db6171
Binary files /dev/null and b/worlds/smw/data/palettes/map/yoshi/original_special.mw3 differ
diff --git a/worlds/smw/data/palettes/map/yoshi/sepia.mw3 b/worlds/smw/data/palettes/map/yoshi/sepia.mw3
new file mode 100644
index 000000000000..3cbf6b0390bf
Binary files /dev/null and b/worlds/smw/data/palettes/map/yoshi/sepia.mw3 differ
diff --git a/worlds/smw/data/palettes/map/yoshi/snow_day.mw3 b/worlds/smw/data/palettes/map/yoshi/snow_day.mw3
new file mode 100644
index 000000000000..464b32bad17c
Binary files /dev/null and b/worlds/smw/data/palettes/map/yoshi/snow_day.mw3 differ
diff --git a/worlds/smw/data/palettes/map/yoshi/sunset.mw3 b/worlds/smw/data/palettes/map/yoshi/sunset.mw3
new file mode 100644
index 000000000000..9477a08cb8e6
Binary files /dev/null and b/worlds/smw/data/palettes/map/yoshi/sunset.mw3 differ
diff --git a/worlds/smw/data/palettes/map/yoshi/tritanopia.mw3 b/worlds/smw/data/palettes/map/yoshi/tritanopia.mw3
new file mode 100644
index 000000000000..c90b7f9af0d2
Binary files /dev/null and b/worlds/smw/data/palettes/map/yoshi/tritanopia.mw3 differ
diff --git a/worlds/smw/data/palettes/map/yoshi/yochis_ailand.mw3 b/worlds/smw/data/palettes/map/yoshi/yochis_ailand.mw3
new file mode 100644
index 000000000000..3e1de6680d02
Binary files /dev/null and b/worlds/smw/data/palettes/map/yoshi/yochis_ailand.mw3 differ
diff --git a/worlds/smw/docs/en_Super Mario World.md b/worlds/smw/docs/en_Super Mario World.md
index 87a96e558b65..f7a12839df4d 100644
--- a/worlds/smw/docs/en_Super Mario World.md
+++ b/worlds/smw/docs/en_Super Mario World.md
@@ -1,8 +1,8 @@
# Super Mario World
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
+The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
## What does randomization do to this game?
@@ -25,10 +25,16 @@ There are two goals which can be chosen:
## What items and locations get shuffled?
-Each unique level exit awards a location check. Optionally, collecting five Dragon Coins in each level can also award a location check.
+Each unique level exit awards a location check. Additionally, the following in-level actions can be set to award a location check:
+- Collecting Five Dragon Coins
+- Collecting 3-Up Moons
+- Activating Bonus Blocks
+- Receiving Hidden 1-Ups
+- Hitting Blocks containing coins or items
+
Mario's various abilities and powerups as described above are placed into the item pool.
If the player is playing Yoshi Egg Hunt, a certain number of Yoshi Eggs will be placed into the item pool.
-Any additional items that are needed to fill out the item pool with be 1-Up Mushrooms.
+Any additional items that are needed to fill out the item pool will be 1-Up Mushrooms, bundles of coins, or, if enabled, various trap items.
## Which items can be in another player's world?
diff --git a/worlds/smw/docs/setup_en.md b/worlds/smw/docs/setup_en.md
index 2a9435c95d42..825f0954c8f1 100644
--- a/worlds/smw/docs/setup_en.md
+++ b/worlds/smw/docs/setup_en.md
@@ -2,8 +2,7 @@
## Required Software
-- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Super Mario World Patch Setup`
-
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of connecting to SNI such as:
@@ -23,9 +22,10 @@
### Windows Setup
-1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
- or you are on an older version, you may run the installer again to install the SNI Client.
-2. During setup, you will be asked to locate your base ROM file. This is your Super Mario World ROM file.
+1. Download and install [Archipelago](). **The installer
+ file is located in the assets section at the bottom of the version information.**
+2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
+ This is your Super Mario World ROM file. This only needs to be done once.
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
@@ -44,13 +44,13 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a config file?
-The Player Settings page on the website allows you to configure your personal settings and export a config file from
-them. Player settings page: [Super Mario World Player Settings Page](/games/Super%20Mario%20World/player-settings)
+The Player Options page on the website allows you to configure your personal options and export a config file from
+them. Player options page: [Super Mario World Player Options Page](/games/Super%20Mario%20World/player-options)
### 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
@@ -77,8 +77,7 @@ first time launching, you may be prompted to allow it to communicate through the
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Select the connector lua file included with your client
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
@@ -90,8 +89,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas
2. Load your ROM file if it hasn't already been loaded.
If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R).
3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window.
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
- You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua`
with the file picker.
diff --git a/worlds/smz3/Client.py b/worlds/smz3/Client.py
index 687a43b00f5c..028c44869a68 100644
--- a/worlds/smz3/Client.py
+++ b/worlds/smz3/Client.py
@@ -32,6 +32,7 @@
class SMZ3SNIClient(SNIClient):
game = "SMZ3"
+ patch_suffix = ".apsmz3"
async def validate_rom(self, ctx):
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
@@ -40,6 +41,7 @@ async def validate_rom(self, ctx):
if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:3] != b"ZSM":
return False
+ ctx.smz3_new_message_queue = rom_name[7] in b"1234567890"
ctx.game = self.game
ctx.items_handling = 0b101 # local items and remote start inventory
@@ -53,6 +55,22 @@ async def game_watcher(self, ctx):
if ctx.server is None or ctx.slot is None:
# not successfully connected to a multiworld server, cannot process the game sending items
return
+
+ send_progress_addr_ptr_offset = 0x680
+ send_progress_size = 8
+ send_progress_message_byte_offset = 4
+ send_progress_addr_table_offset = 0x700
+ recv_progress_addr_ptr_offset = 0x600
+ recv_progress_size = 4
+ recv_progress_addr_table_offset = 0x602
+ if ctx.smz3_new_message_queue:
+ send_progress_addr_ptr_offset = 0xD3C
+ send_progress_size = 2
+ send_progress_message_byte_offset = 0
+ send_progress_addr_table_offset = 0xDA0
+ recv_progress_addr_ptr_offset = 0xD36
+ recv_progress_size = 2
+ recv_progress_addr_table_offset = 0xD38
currentGame = await snes_read(ctx, SRAM_START + 0x33FE, 2)
if (currentGame is not None):
@@ -69,7 +87,7 @@ async def game_watcher(self, ctx):
ctx.finished_game = True
return
- data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, 4)
+ data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + send_progress_addr_ptr_offset, 4)
if data is None:
return
@@ -77,25 +95,25 @@ async def game_watcher(self, ctx):
recv_item = data[2] | (data[3] << 8)
while (recv_index < recv_item):
- item_address = recv_index * 8
- message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x700 + item_address, 8)
- is_z3_item = ((message[5] & 0x80) != 0)
- masked_part = (message[5] & 0x7F) if is_z3_item else message[5]
- item_index = ((message[4] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0)
+ item_address = recv_index * send_progress_size
+ message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + send_progress_addr_table_offset + item_address, send_progress_size)
+ is_z3_item = ((message[send_progress_message_byte_offset+1] & 0x80) != 0)
+ masked_part = (message[send_progress_message_byte_offset+1] & 0x7F) if is_z3_item else message[send_progress_message_byte_offset+1]
+ item_index = ((message[send_progress_message_byte_offset] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0)
recv_index += 1
- snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
+ snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + send_progress_addr_ptr_offset, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
from .TotalSMZ3.Location import locations_start_id
from . import convertLocSMZ3IDToAPID
location_id = locations_start_id + convertLocSMZ3IDToAPID(item_index)
ctx.locations_checked.add(location_id)
- location = ctx.location_names[location_id]
+ location = ctx.location_names.lookup_in_game(location_id)
snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
- data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x600, 4)
+ data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + recv_progress_addr_ptr_offset, 4)
if data is None:
return
@@ -106,13 +124,15 @@ async def game_watcher(self, ctx):
item = ctx.items_received[item_out_ptr]
item_id = item.item - items_start_id
- player_id = item.player if item.player <= SMZ3_ROM_PLAYER_LIMIT else 0
- snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 4, bytes([player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, (item_id >> 8) & 0xFF]))
+ player_id = item.player if item.player < SMZ3_ROM_PLAYER_LIMIT else 0
+ snes_buffered_write(ctx,
+ SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * recv_progress_size,
+ bytes([player_id, item_id]) if ctx.smz3_new_message_queue else
+ bytes([player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, (item_id >> 8) & 0xFF]))
item_out_ptr += 1
- snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x602, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF]))
+ snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + recv_progress_addr_table_offset, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF]))
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
- color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
- ctx.location_names[item.location], item_out_ptr, len(ctx.items_received)))
+ color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
+ ctx.location_names.lookup_in_slot(item.location, item.player), item_out_ptr, len(ctx.items_received)))
await snes_flush_writes(ctx)
-
diff --git a/worlds/smz3/Options.py b/worlds/smz3/Options.py
index e3d8b8dd10e2..8c5efc431f5c 100644
--- a/worlds/smz3/Options.py
+++ b/worlds/smz3/Options.py
@@ -1,5 +1,5 @@
import typing
-from Options import Choice, Option, Toggle, DefaultOnToggle, Range
+from Options import Choice, Option, Toggle, DefaultOnToggle, Range, ItemsAccessibility
class SMLogic(Choice):
"""This option selects what kind of logic to use for item placement inside
@@ -25,7 +25,7 @@ class SwordLocation(Choice):
Randomized - The sword can be placed anywhere.
Early - The sword will be placed in a location accessible from the start of
the game.
- Unce assured - The sword will always be placed on Link's Uncle."""
+ Uncle - The sword will always be placed on Link's Uncle."""
display_name = "Sword Location"
option_Randomized = 0
option_Early = 1
@@ -48,7 +48,7 @@ class MorphLocation(Choice):
class Goal(Choice):
"""This option decides what goal is required to finish the randomizer.
- Defeat Ganon and Mother Brain - Find the required crystals and boss tokens kill both bosses.
+ Defeat Ganon and Mother Brain - Find the required crystals and boss tokens to kill both bosses.
Fast Ganon and Defeat Mother Brain - The hole to ganon is open without having to defeat Agahnim in
Ganon's Tower and Ganon can be defeat as soon you have the required
crystals to make Ganon vulnerable. For keysanity, this mode also removes
@@ -128,6 +128,7 @@ class EnergyBeep(DefaultOnToggle):
smz3_options: typing.Dict[str, type(Option)] = {
+ "accessibility": ItemsAccessibility,
"sm_logic": SMLogic,
"sword_location": SwordLocation,
"morph_location": MorphLocation,
diff --git a/worlds/smz3/TotalSMZ3/Item.py b/worlds/smz3/TotalSMZ3/Item.py
index b4fc9d592550..28e9658ce1d0 100644
--- a/worlds/smz3/TotalSMZ3/Item.py
+++ b/worlds/smz3/TotalSMZ3/Item.py
@@ -181,6 +181,7 @@ class Item:
keycard = re.compile("^Card")
smMap = re.compile("^SmMap")
+ def IsNameDungeonItem(item_name): return Item.dungeon.match(item_name)
def IsDungeonItem(self): return self.dungeon.match(self.Type.name)
def IsBigKey(self): return self.bigKey.match(self.Type.name)
def IsKey(self): return self.key.match(self.Type.name)
diff --git a/worlds/smz3/TotalSMZ3/Patch.py b/worlds/smz3/TotalSMZ3/Patch.py
index 049b200c46b0..27fd8dcc3535 100644
--- a/worlds/smz3/TotalSMZ3/Patch.py
+++ b/worlds/smz3/TotalSMZ3/Patch.py
@@ -319,7 +319,7 @@ def GetSMItemPLM(location:Location):
def WriteZ3Locations(self, locations: List[Location]):
for location in locations:
if (location.Type == LocationType.HeraStandingKey):
- self.patches.append((Snes(0x9E3BB), [0xE4] if location.APLocation.item.game == "SMZ3" and location.APLocation.item.item.Type == ItemType.KeyTH else [0xEB]))
+ self.patches.append((Snes(0x9E3BB), [0xEB]))
elif (location.Type in [LocationType.Pedestal, LocationType.Ether, LocationType.Bombos]):
text = Texts.ItemTextbox(location.APLocation.item.item if location.APLocation.item.game == "SMZ3" else Item(ItemType.Something))
if (location.Type == LocationType.Pedestal):
@@ -616,7 +616,8 @@ def WriteGameTitle(self):
"H" if self.myWorld.Config.SMLogic == Config.SMLogic.Hard else \
"X"
- self.title = f"ZSM{Patch.Major}{Patch.Minor}{Patch.Patch}{z3Glitch}{smGlitch}{self.myWorld.Id}{self.seed:08x}".ljust(21)[:21]
+ from Utils import __version__
+ self.title = f"ZSM{Patch.Major}{Patch.Minor}{Patch.Patch}{__version__.replace('.', '')[0:3]}{z3Glitch}{smGlitch}{self.myWorld.Id}{self.seed:08x}".ljust(21)[:21]
self.patches.append((Snes(0x00FFC0), bytearray(self.title, 'utf8')))
self.patches.append((Snes(0x80FFC0), bytearray(self.title, 'utf8')))
diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py
index 79ba17db82b1..690e5172a25c 100644
--- a/worlds/smz3/__init__.py
+++ b/worlds/smz3/__init__.py
@@ -68,7 +68,6 @@ class SMZ3World(World):
"""
game: str = "SMZ3"
topology_present = False
- data_version = 3
option_definitions = smz3_options
item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id)
location_names: Set[str]
@@ -80,7 +79,8 @@ class SMZ3World(World):
locationNamesGT: Set[str] = {loc.Name for loc in GanonsTower(None, None).Locations}
# first added for 0.2.6
- required_client_version = (0, 2, 6)
+ # optimized message queues for 0.4.4
+ required_client_version = (0, 4, 4)
def __init__(self, world: MultiWorld, player: int):
self.rom_name_available_event = threading.Event()
@@ -215,18 +215,20 @@ def create_items(self):
niceItems = TotalSMZ3Item.Item.CreateNicePool(self.smz3World)
junkItems = TotalSMZ3Item.Item.CreateJunkPool(self.smz3World)
- allJunkItems = niceItems + junkItems
self.junkItemsNames = [item.Type.name for item in junkItems]
if (self.smz3World.Config.Keysanity):
progressionItems = self.progression + self.dungeon + self.keyCardsItems + self.SmMapsItems
else:
- progressionItems = self.progression
+ progressionItems = self.progression
+ # Dungeons items here are not in the itempool and will be prefilled locally so they must stay local
+ self.multiworld.non_local_items[self.player].value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name))
for item in self.keyCardsItems:
self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item))
itemPool = [SMZ3Item(item.Type.name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in progressionItems] + \
- [SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in allJunkItems]
+ [SMZ3Item(item.Type.name, ItemClassification.useful, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in niceItems] + \
+ [SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in junkItems]
self.smz3DungeonItems = [SMZ3Item(item.Type.name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in self.dungeon]
self.multiworld.itempool += itemPool
@@ -242,7 +244,7 @@ def set_rules(self):
set_rule(entrance, lambda state, region=region: region.CanEnter(state.smz3state[self.player]))
for loc in region.Locations:
l = self.locations[loc.Name]
- if self.multiworld.accessibility[self.player] != 'locations':
+ if self.multiworld.accessibility[self.player] != 'full':
l.always_allow = lambda state, item, loc=loc: \
item.game == "SMZ3" and \
loc.alwaysAllow(item.item, state.smz3state[self.player])
@@ -281,55 +283,89 @@ def apply_sm_custom_sprite(self):
return offworldSprites
def convert_to_sm_item_name(self, itemName):
- charMap = { "A" : 0x3CE0,
- "B" : 0x3CE1,
- "C" : 0x3CE2,
- "D" : 0x3CE3,
- "E" : 0x3CE4,
- "F" : 0x3CE5,
- "G" : 0x3CE6,
- "H" : 0x3CE7,
- "I" : 0x3CE8,
- "J" : 0x3CE9,
- "K" : 0x3CEA,
- "L" : 0x3CEB,
- "M" : 0x3CEC,
- "N" : 0x3CED,
- "O" : 0x3CEE,
- "P" : 0x3CEF,
- "Q" : 0x3CF0,
- "R" : 0x3CF1,
- "S" : 0x3CF2,
- "T" : 0x3CF3,
- "U" : 0x3CF4,
- "V" : 0x3CF5,
- "W" : 0x3CF6,
- "X" : 0x3CF7,
- "Y" : 0x3CF8,
- "Z" : 0x3CF9,
- " " : 0x3C4E,
- "!" : 0x3CFF,
- "?" : 0x3CFE,
- "'" : 0x3CFD,
- "," : 0x3CFB,
- "." : 0x3CFA,
- "-" : 0x3CCF,
- "_" : 0x000E,
- "1" : 0x3C00,
- "2" : 0x3C01,
- "3" : 0x3C02,
- "4" : 0x3C03,
- "5" : 0x3C04,
- "6" : 0x3C05,
- "7" : 0x3C06,
- "8" : 0x3C07,
- "9" : 0x3C08,
- "0" : 0x3C09,
- "%" : 0x3C0A}
+ # SMZ3 uses a different font; this map is not compatible with just SM alone.
+ charMap = {
+ "A": 0x3CE0,
+ "B": 0x3CE1,
+ "C": 0x3CE2,
+ "D": 0x3CE3,
+ "E": 0x3CE4,
+ "F": 0x3CE5,
+ "G": 0x3CE6,
+ "H": 0x3CE7,
+ "I": 0x3CE8,
+ "J": 0x3CE9,
+ "K": 0x3CEA,
+ "L": 0x3CEB,
+ "M": 0x3CEC,
+ "N": 0x3CED,
+ "O": 0x3CEE,
+ "P": 0x3CEF,
+ "Q": 0x3CF0,
+ "R": 0x3CF1,
+ "S": 0x3CF2,
+ "T": 0x3CF3,
+ "U": 0x3CF4,
+ "V": 0x3CF5,
+ "W": 0x3CF6,
+ "X": 0x3CF7,
+ "Y": 0x3CF8,
+ "Z": 0x3CF9,
+ " ": 0x3C4E,
+ "!": 0x3CFF,
+ "?": 0x3CFE,
+ "'": 0x3CFD,
+ ",": 0x3CFB,
+ ".": 0x3CFA,
+ "-": 0x3CCF,
+ "1": 0x3C80,
+ "2": 0x3C81,
+ "3": 0x3C82,
+ "4": 0x3C83,
+ "5": 0x3C84,
+ "6": 0x3C85,
+ "7": 0x3C86,
+ "8": 0x3C87,
+ "9": 0x3C88,
+ "0": 0x3C89,
+ "%": 0x3C0A,
+ "a": 0x3C90,
+ "b": 0x3C91,
+ "c": 0x3C92,
+ "d": 0x3C93,
+ "e": 0x3C94,
+ "f": 0x3C95,
+ "g": 0x3C96,
+ "h": 0x3C97,
+ "i": 0x3C98,
+ "j": 0x3C99,
+ "k": 0x3C9A,
+ "l": 0x3C9B,
+ "m": 0x3C9C,
+ "n": 0x3C9D,
+ "o": 0x3C9E,
+ "p": 0x3C9F,
+ "q": 0x3CA0,
+ "r": 0x3CA1,
+ "s": 0x3CA2,
+ "t": 0x3CA3,
+ "u": 0x3CA4,
+ "v": 0x3CA5,
+ "w": 0x3CA6,
+ "x": 0x3CA7,
+ "y": 0x3CA8,
+ "z": 0x3CA9,
+ '"': 0x3CAA,
+ ":": 0x3CAB,
+ "~": 0x3CAC,
+ "@": 0x3CAD,
+ "#": 0x3CAE,
+ "+": 0x3CAF,
+ "_": 0x000E
+ }
data = []
- itemName = itemName.upper()[:26]
- itemName = itemName.strip()
+ itemName = itemName.replace("_", "-").strip()[:26]
itemName = itemName.center(26, " ")
itemName = "___" + itemName + "___"
@@ -468,7 +504,7 @@ def fill_slot_data(self):
def collect(self, state: CollectionState, item: Item) -> bool:
state.smz3state[self.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)])
if item.advancement:
- state.prog_items[item.name, item.player] += 1
+ state.prog_items[item.player][item.name] += 1
return True # indicate that a logical state change has occured
return False
@@ -476,9 +512,9 @@ def remove(self, state: CollectionState, item: Item) -> bool:
name = self.collect_item(state, item, True)
if name:
state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)])
- state.prog_items[name, item.player] -= 1
- if state.prog_items[name, item.player] < 1:
- del (state.prog_items[name, item.player])
+ state.prog_items[item.player][item.name] -= 1
+ if state.prog_items[item.player][item.name] < 1:
+ del (state.prog_items[item.player][item.name])
return True
return False
@@ -524,7 +560,6 @@ def post_fill(self):
if (loc.item.player == self.player and loc.always_allow(state, loc.item)):
loc.item.classification = ItemClassification.filler
loc.item.item.Progression = False
- loc.item.location.event = False
self.unreachable.append(loc)
def get_filler_item_name(self) -> str:
@@ -548,11 +583,8 @@ def write_spoiler(self, spoiler_handle: TextIO):
def JunkFillGT(self, factor):
poolLength = len(self.multiworld.itempool)
- playerGroups = self.multiworld.get_player_groups(self.player)
- playerGroups.add(self.player)
junkPoolIdx = [i for i in range(0, poolLength)
- if self.multiworld.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap) and
- self.multiworld.itempool[i].player in playerGroups]
+ if self.multiworld.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap)]
toRemove = []
for loc in self.locations.values():
# commenting this for now since doing a partial GT pre fill would allow for non SMZ3 progression in GT
@@ -563,6 +595,7 @@ def JunkFillGT(self, factor):
poolLength = len(junkPoolIdx)
# start looking at a random starting index and loop at start if no match found
start = self.multiworld.random.randint(0, poolLength)
+ itemFromPool = None
for off in range(0, poolLength):
i = (start + off) % poolLength
candidate = self.multiworld.itempool[junkPoolIdx[i]]
@@ -570,8 +603,8 @@ def JunkFillGT(self, factor):
itemFromPool = candidate
toRemove.append(junkPoolIdx[i])
break
+ assert itemFromPool is not None, "Can't find anymore item(s) to pre fill GT"
self.multiworld.push_item(loc, itemFromPool, False)
- loc.event = False
toRemove.sort(reverse = True)
for i in toRemove:
self.multiworld.itempool.pop(i)
diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips
index fff36d95d15c..87a4f924f193 100644
Binary files a/worlds/smz3/data/zsm.ips and b/worlds/smz3/data/zsm.ips differ
diff --git a/worlds/smz3/docs/en_SMZ3.md b/worlds/smz3/docs/en_SMZ3.md
index f0302d12f3a1..2116432ea7a1 100644
--- a/worlds/smz3/docs/en_SMZ3.md
+++ b/worlds/smz3/docs/en_SMZ3.md
@@ -1,8 +1,8 @@
# SMZ3
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
diff --git a/worlds/smz3/docs/multiworld_en.md b/worlds/smz3/docs/multiworld_en.md
index 27c8a507e3df..38c410faee02 100644
--- a/worlds/smz3/docs/multiworld_en.md
+++ b/worlds/smz3/docs/multiworld_en.md
@@ -2,17 +2,18 @@
## Required Software
-- One of the client programs:
- - [SNIClient](https://github.com/ArchipelagoMW/Archipelago/releases), included with the main
- Archipelago install. Make sure to check the box for `SNI Client - Super Metroid Patch Setup` and
- `SNI Client - A Link to the Past Patch Setup`
-- Hardware or software capable of loading and playing SNES ROM files
- - An emulator capable of connecting to SNI such as:
- - snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases),
- - BizHawk from: [TASVideos](https://tasvideos.org/BizHawk), or
- - RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or,
- - An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other
- compatible hardware
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
+- [SNI](https://github.com/alttpo/sni/releases). This is automatically included with your Archipelago installation above.
+- SNI is not compatible with (Q)Usb2Snes.
+- Hardware or software capable of loading and playing SNES ROM files, including:
+ - An emulator capable of connecting to SNI
+ ([snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases), [snes9x-rr](https://github.com/gocha/snes9x-rr/releases),
+ [BSNES-plus](https://github.com/black-sliver/bsnes-plus),
+ [BizHawk](http://tasvideos.org/BizHawk.html), or
+ [RetroArch](https://retroarch.com?page=platforms) 1.10.1 or newer)
+ - An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware. **note:
+ modded SNES minis are currently not supported by SNI. Some users have claimed success with QUsb2Snes for this system,
+ but it is not supported.**
- Your legally obtained Super Metroid ROM file, probably named `Super Metroid (Japan, USA).sfc` and
Your Japanese Zelda3 v1.0 ROM file, probably named `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc`
@@ -20,9 +21,10 @@
### Windows Setup
-1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
- or you are on an older version, you may run the installer again to install the SNI Client.
-2. During setup, you will be asked to locate your base ROM files. This is your Super Metroid and Zelda3 ROM files.
+1. Download and install [Archipelago](). **The installer
+ file is located in the assets section at the bottom of the version information.**
+2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
+ This is your Super Metroid and Zelda3 ROM files. This only needs to be done once.
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
@@ -41,18 +43,18 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a config file?
-The Player Settings page on the website allows you to configure your personal settings and export a config file from
-them. Player settings page: [SMZ3 Player Settings Page](/games/SMZ3/player-settings)
+The Player Options page on the website allows you to configure your personal options and export a config file from
+them. Player options page: [SMZ3 Player Options Page](/games/SMZ3/player-options)
### 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
-1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button.
- - Player Settings page: [SMZ3 Player Settings Page](/games/SMZ3/player-settings)
+1. Navigate to the Player Options page, configure your options, and click the "Generate Game" button.
+ - Player Options page: [SMZ3 Player Options Page](/games/SMZ3/player-options)
2. You will be presented with a "Seed Info" page.
3. Click the "Create New Room" link.
4. You will be presented with a server page, from which you can download your patch file.
@@ -78,6 +80,11 @@ client, and will also create your ROM in the same place as your patch file.
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
+#### snes9x-nwa
+
+1. Click on the Network Menu and check **Enable Emu Network Control**
+2. Load your ROM file if it hasn't already been loaded.
+
##### snes9x-rr
1. Load your ROM file if it hasn't already been loaded.
@@ -85,11 +92,15 @@ first time launching, you may be prompted to allow it to communicate through the
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Select the connector lua file included with your client
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
+#### BSNES-Plus
+
+1. Load your ROM file if it hasn't already been loaded.
+2. The emulator should automatically connect while SNI is running.
+
##### BizHawk
1. Ensure you have the BSNES core loaded. This is done with the main menubar, under:
@@ -98,8 +109,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas
2. Load your ROM file if it hasn't already been loaded.
If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R).
3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window.
- - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
- emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
- You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua`
with the file picker.
diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py
deleted file mode 100644
index 3c173dec2f31..000000000000
--- a/worlds/soe/Logic.py
+++ /dev/null
@@ -1,64 +0,0 @@
-from typing import Protocol, Set
-
-from BaseClasses import MultiWorld
-from worlds.AutoWorld import LogicMixin
-from . import pyevermizer
-from .Options import EnergyCore
-
-# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early?
-
-# TODO: resolve/flatten/expand rules to get rid of recursion below where possible
-# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items)
-rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0]
-# Logic.items are all items and extra items excluding non-progression items and duplicates
-item_names: Set[str] = set()
-items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items())
- if item.name not in item_names and not item_names.add(item.name)]
-
-
-class LogicProtocol(Protocol):
- def has(self, name: str, player: int) -> bool: ...
- def item_count(self, name: str, player: int) -> int: ...
- def soe_has(self, progress: int, world: MultiWorld, player: int, count: int) -> bool: ...
- def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int) -> int: ...
-
-
-# when this module is loaded, this mixin will extend BaseClasses.CollectionState
-class SecretOfEvermoreLogic(LogicMixin):
- def _soe_count(self: LogicProtocol, progress: int, world: MultiWorld, player: int, max_count: int = 0) -> int:
- """
- Returns reached count of one of evermizer's progress steps based on collected items.
- i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP
- """
- n = 0
- for item in items:
- for pvd in item.provides:
- if pvd[1] == progress:
- if self.has(item.name, player):
- n += self.item_count(item.name, player) * pvd[0]
- if n >= max_count > 0:
- return n
- for rule in rules:
- for pvd in rule.provides:
- if pvd[1] == progress and pvd[0] > 0:
- has = True
- for req in rule.requires:
- if not self.soe_has(req[1], world, player, req[0]):
- has = False
- break
- if has:
- n += pvd[0]
- if n >= max_count > 0:
- return n
- return n
-
- def soe_has(self: LogicProtocol, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool:
- """
- Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE
- """
- if progress == pyevermizer.P_ENERGY_CORE: # logic is shared between worlds, so we override in the call
- w = world.worlds[player]
- if w.energy_core == EnergyCore.option_fragments:
- progress = pyevermizer.P_CORE_FRAGMENT
- count = w.required_fragments
- return self._soe_count(progress, world, player, count) >= count
diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py
deleted file mode 100644
index f1a30745f8f0..000000000000
--- a/worlds/soe/Options.py
+++ /dev/null
@@ -1,264 +0,0 @@
-import typing
-
-from Options import Range, Choice, Toggle, DefaultOnToggle, AssembleOptions, DeathLink, ProgressionBalancing
-
-
-# typing boilerplate
-class FlagsProtocol(typing.Protocol):
- value: int
- default: int
- flags: typing.List[str]
-
-
-class FlagProtocol(typing.Protocol):
- value: int
- default: int
- flag: str
-
-
-# meta options
-class EvermizerFlags:
- flags: typing.List[str]
-
- def to_flag(self: FlagsProtocol) -> str:
- return self.flags[self.value]
-
-
-class EvermizerFlag:
- flag: str
-
- def to_flag(self: FlagProtocol) -> str:
- return self.flag if self.value != self.default else ''
-
-
-class OffOnFullChoice(Choice):
- option_off = 0
- option_on = 1
- option_full = 2
- alias_chaos = 2
-
-
-# actual options
-class Difficulty(EvermizerFlags, Choice):
- """Changes relative spell cost and stuff"""
- display_name = "Difficulty"
- option_easy = 0
- option_normal = 1
- option_hard = 2
- option_mystery = 3 # 'random' is reserved
- alias_chaos = 3
- default = 1
- flags = ['e', 'n', 'h', 'x']
-
-
-class EnergyCore(EvermizerFlags, Choice):
- """How to obtain the Energy Core"""
- display_name = "Energy Core"
- option_vanilla = 0
- option_shuffle = 1
- option_fragments = 2
- default = 1
- flags = ['z', '', 'Z']
-
-
-class RequiredFragments(Range):
- """Required fragment count for Energy Core = Fragments"""
- display_name = "Required Fragments"
- range_start = 1
- range_end = 99
- default = 10
-
-
-class AvailableFragments(Range):
- """Placed fragment count for Energy Core = Fragments"""
- display_name = "Available Fragments"
- range_start = 1
- range_end = 99
- default = 11
-
-
-class MoneyModifier(Range):
- """Money multiplier in %"""
- display_name = "Money Modifier"
- range_start = 1
- range_end = 2500
- default = 200
-
-
-class ExpModifier(Range):
- """EXP multiplier for Weapons, Characters and Spells in %"""
- display_name = "Exp Modifier"
- range_start = 1
- range_end = 2500
- default = 200
-
-
-class FixSequence(EvermizerFlag, DefaultOnToggle):
- """Fix some sequence breaks"""
- display_name = "Fix Sequence"
- flag = '1'
-
-
-class FixCheats(EvermizerFlag, DefaultOnToggle):
- """Fix cheats left in by the devs (not desert skip)"""
- display_name = "Fix Cheats"
- flag = '2'
-
-
-class FixInfiniteAmmo(EvermizerFlag, Toggle):
- """Fix infinite ammo glitch"""
- display_name = "Fix Infinite Ammo"
- flag = '5'
-
-
-class FixAtlasGlitch(EvermizerFlag, Toggle):
- """Fix atlas underflowing stats"""
- display_name = "Fix Atlas Glitch"
- flag = '6'
-
-
-class FixWingsGlitch(EvermizerFlag, Toggle):
- """Fix wings making you invincible in some areas"""
- display_name = "Fix Wings Glitch"
- flag = '7'
-
-
-class ShorterDialogs(EvermizerFlag, DefaultOnToggle):
- """Cuts some dialogs"""
- display_name = "Shorter Dialogs"
- flag = '9'
-
-
-class ShortBossRush(EvermizerFlag, DefaultOnToggle):
- """Start boss rush at Metal Magmar, cut enemy HP in half"""
- display_name = "Short Boss Rush"
- flag = 'f'
-
-
-class Ingredienizer(EvermizerFlags, OffOnFullChoice):
- """On Shuffles, Full randomizes spell ingredients"""
- display_name = "Ingredienizer"
- default = 1
- flags = ['i', '', 'I']
-
-
-class Sniffamizer(EvermizerFlags, OffOnFullChoice):
- """On Shuffles, Full randomizes drops in sniff locations"""
- display_name = "Sniffamizer"
- default = 1
- flags = ['s', '', 'S']
-
-
-class Callbeadamizer(EvermizerFlags, OffOnFullChoice):
- """On Shuffles call bead characters, Full shuffles individual spells"""
- display_name = "Callbeadamizer"
- default = 1
- flags = ['c', '', 'C']
-
-
-class Musicmizer(EvermizerFlag, Toggle):
- """Randomize music for some rooms"""
- display_name = "Musicmizer"
- flag = 'm'
-
-
-class Doggomizer(EvermizerFlags, OffOnFullChoice):
- """On shuffles dog per act, Full randomizes dog per screen, Pupdunk gives you Everpupper everywhere"""
- display_name = "Doggomizer"
- option_pupdunk = 3
- default = 0
- flags = ['', 'd', 'D', 'p']
-
-
-class TurdoMode(EvermizerFlag, Toggle):
- """Replace offensive spells by Turd Balls with varying strength and make weapons weak"""
- display_name = "Turdo Mode"
- flag = 't'
-
-
-class TrapCount(Range):
- """Replace some filler items with traps"""
- display_name = "Trap Count"
- range_start = 0
- range_end = 100
- default = 0
-
-
-# more meta options
-class ItemChanceMeta(AssembleOptions):
- def __new__(mcs, name, bases, attrs):
- if 'item_name' in attrs:
- attrs["display_name"] = f"{attrs['item_name']} Chance"
- attrs["range_start"] = 0
- attrs["range_end"] = 100
-
- return super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs)
-
-
-class TrapChance(Range, metaclass=ItemChanceMeta):
- item_name: str
- default = 20
-
-
-# more actual options
-class TrapChanceQuake(TrapChance):
- """Sets the chance/ratio of quake traps"""
- item_name = "Quake Trap"
-
-
-class TrapChancePoison(TrapChance):
- """Sets the chance/ratio of poison effect traps"""
- item_name = "Poison Trap"
-
-
-class TrapChanceConfound(TrapChance):
- """Sets the chance/ratio of confound effect traps"""
- item_name = "Confound Trap"
-
-
-class TrapChanceHUD(TrapChance):
- """Sets the chance/ratio of HUD visibility traps"""
- item_name = "HUD Trap"
-
-
-class TrapChanceOHKO(TrapChance):
- """Sets the chance/ratio of OHKO (1HP left) traps"""
- item_name = "OHKO Trap"
-
-
-class SoEProgressionBalancing(ProgressionBalancing):
- default = 30
- __doc__ = ProgressionBalancing.__doc__.replace(f"default {ProgressionBalancing.default}", f"default {default}") \
- if ProgressionBalancing.__doc__ else None
- special_range_names = {**ProgressionBalancing.special_range_names, "normal": default}
-
-
-soe_options: typing.Dict[str, AssembleOptions] = {
- "difficulty": Difficulty,
- "energy_core": EnergyCore,
- "required_fragments": RequiredFragments,
- "available_fragments": AvailableFragments,
- "money_modifier": MoneyModifier,
- "exp_modifier": ExpModifier,
- "fix_sequence": FixSequence,
- "fix_cheats": FixCheats,
- "fix_infinite_ammo": FixInfiniteAmmo,
- "fix_atlas_glitch": FixAtlasGlitch,
- "fix_wings_glitch": FixWingsGlitch,
- "shorter_dialogs": ShorterDialogs,
- "short_boss_rush": ShortBossRush,
- "ingredienizer": Ingredienizer,
- "sniffamizer": Sniffamizer,
- "callbeadamizer": Callbeadamizer,
- "musicmizer": Musicmizer,
- "doggomizer": Doggomizer,
- "turdo_mode": TurdoMode,
- "death_link": DeathLink,
- "trap_count": TrapCount,
- "trap_chance_quake": TrapChanceQuake,
- "trap_chance_poison": TrapChancePoison,
- "trap_chance_confound": TrapChanceConfound,
- "trap_chance_hud": TrapChanceHUD,
- "trap_chance_ohko": TrapChanceOHKO,
- "progression_balancing": SoEProgressionBalancing,
-}
diff --git a/worlds/soe/Patch.py b/worlds/soe/Patch.py
deleted file mode 100644
index f4de5d06ead1..000000000000
--- a/worlds/soe/Patch.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import os
-from typing import Optional
-
-import Utils
-from worlds.Files import APDeltaPatch
-
-
-USHASH = '6e9c94511d04fac6e0a1e582c170be3a'
-
-
-class SoEDeltaPatch(APDeltaPatch):
- hash = USHASH
- game = "Secret of Evermore"
- patch_file_ending = ".apsoe"
-
- @classmethod
- def get_source_data(cls) -> bytes:
- with open(get_base_rom_path(), "rb") as stream:
- return read_rom(stream)
-
-
-def get_base_rom_path(file_name: Optional[str] = None) -> str:
- options = Utils.get_options()
- if not file_name:
- file_name = options["soe_options"]["rom_file"]
- if not file_name:
- raise ValueError("Missing soe_options -> rom_file from host.yaml")
- if not os.path.exists(file_name):
- file_name = Utils.user_path(file_name)
- return file_name
-
-
-def read_rom(stream, strip_header=True) -> bytes:
- """Reads rom into bytearray and optionally strips off any smc header"""
- data = stream.read()
- if strip_header and len(data) % 0x400 == 0x200:
- return data[0x200:]
- return data
-
-
-if __name__ == '__main__':
- import sys
- print('Please use ../../Patch.py', file=sys.stderr)
- sys.exit(1)
diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py
index f887325c60ea..3baed165d821 100644
--- a/worlds/soe/__init__.py
+++ b/worlds/soe/__init__.py
@@ -4,22 +4,23 @@
import threading
import typing
+# from . import pyevermizer # as part of the source tree
+import pyevermizer # from package
+
import settings
+from BaseClasses import Item, ItemClassification, Location, LocationProgressType, Region, Tutorial
+from Utils import output_path
from worlds.AutoWorld import WebWorld, World
from worlds.generic.Rules import add_item_rule, set_rule
-from BaseClasses import Entrance, Item, ItemClassification, Location, LocationProgressType, Region, Tutorial
-from Utils import output_path
+from .logic import SoEPlayerLogic
+from .options import Difficulty, EnergyCore, Sniffamizer, SniffIngredients, SoEOptions
+from .patch import SoEDeltaPatch, get_base_rom_path
+
+if typing.TYPE_CHECKING:
+ from BaseClasses import MultiWorld, CollectionState
-try:
- import pyevermizer # from package
-except ImportError:
- import traceback
- traceback.print_exc()
- from . import pyevermizer # as part of the source tree
+__all__ = ["pyevermizer", "SoEWorld"]
-from . import Logic # load logic mixin
-from .Options import soe_options, Difficulty, EnergyCore, RequiredFragments, AvailableFragments
-from .Patch import SoEDeltaPatch, get_base_rom_path
"""
In evermizer:
@@ -28,17 +29,17 @@
For most items this is their vanilla location (i.e. CHECK_GOURD, number).
Items have `provides`, which give the actual progression
-instead of providing multiple events per item, we iterate through them in Logic.py
+instead of providing multiple events per item, we iterate through them in logic.py
e.g. Found any weapon
Locations have `requires` and `provides`.
Requirements have to be converted to (access) rules for AP
e.g. Chest locked behind having a weapon
-Provides could be events, but instead we iterate through the entire logic in Logic.py
+Provides could be events, but instead we iterate through the entire logic in logic.py
e.g. NPC available after fighting a Boss
Rules are special locations that don't have a physical location
-instead of implementing virtual locations and virtual items, we simply use them in Logic.py
+instead of implementing virtual locations and virtual items, we simply use them in logic.py
e.g. 2DEs+Wheel+Gauge = Rocket
Rules and Locations live on the same logic tree returned by pyevermizer.get_logic()
@@ -63,24 +64,32 @@
pyevermizer.CHECK_BOSS: _id_base + 50, # bosses 64050..6499
pyevermizer.CHECK_GOURD: _id_base + 100, # gourds 64100..64399
pyevermizer.CHECK_NPC: _id_base + 400, # npc 64400..64499
- # TODO: sniff 64500..64799
+ # blank 64500..64799
pyevermizer.CHECK_EXTRA: _id_base + 800, # extra items 64800..64899
pyevermizer.CHECK_TRAP: _id_base + 900, # trap 64900..64999
+ pyevermizer.CHECK_SNIFF: _id_base + 1000 # sniff 65000..65592
}
# cache native evermizer items and locations
_items = pyevermizer.get_items()
+_sniff_items = pyevermizer.get_sniff_items() # optional, not part of the default location pool
_traps = pyevermizer.get_traps()
_extras = pyevermizer.get_extra_items() # items that are not placed by default
_locations = pyevermizer.get_locations()
+_sniff_locations = pyevermizer.get_sniff_locations() # optional, not part of the default location pool
# fix up texts for AP
for _loc in _locations:
if _loc.type == pyevermizer.CHECK_GOURD:
- _loc.name = f'{_loc.name} #{_loc.index}'
+ _loc.name = f"{_loc.name} #{_loc.index}"
+for _loc in _sniff_locations:
+ if _loc.type == pyevermizer.CHECK_SNIFF:
+ _loc.name = f"{_loc.name} Sniff #{_loc.index}"
+del _loc
+
# item helpers
_ingredients = (
'Wax', 'Water', 'Vinegar', 'Root', 'Oil', 'Mushroom', 'Mud Pepper', 'Meteorite', 'Limestone', 'Iron',
- 'Gunpowder', 'Grease', 'Feather', 'Ethanol', 'Dry Ice', 'Crystal', 'Clay', 'Brimstone', 'Bone', 'Atlas Amulet',
+ 'Gunpowder', 'Grease', 'Feather', 'Ethanol', 'Dry Ice', 'Crystal', 'Clay', 'Brimstone', 'Bone', 'Atlas Medallion',
'Ash', 'Acorn'
)
_other_items = (
@@ -88,15 +97,15 @@
)
-def _match_item_name(item, substr: str) -> bool:
- sub = item.name.split(' ', 1)[1] if item.name[0].isdigit() else item.name
+def _match_item_name(item: pyevermizer.Item, substr: str) -> bool:
+ sub: str = item.name.split(' ', 1)[1] if item.name[0].isdigit() else item.name
return sub == substr or sub == substr+'s'
def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Location]]:
name_to_id = {}
id_to_raw = {}
- for loc in _locations:
+ for loc in itertools.chain(_locations, _sniff_locations):
ap_id = _id_offset[loc.type] + loc.index
id_to_raw[ap_id] = loc
name_to_id[loc.name] = ap_id
@@ -107,7 +116,7 @@ def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[i
def _get_item_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Item]]:
name_to_id = {}
id_to_raw = {}
- for item in itertools.chain(_items, _extras, _traps):
+ for item in itertools.chain(_items, _sniff_items, _extras, _traps):
if item.name in name_to_id:
continue
ap_id = _id_offset[item.type] + item.index
@@ -160,41 +169,35 @@ class RomFile(settings.SNESRomPath):
class SoEWorld(World):
"""
Secret of Evermore is a SNES action RPG. You learn alchemy spells, fight bosses and gather rocket parts to visit a
- space station where the final boss must be defeated.
+ space station where the final boss must be defeated.
"""
- game: str = "Secret of Evermore"
- option_definitions = soe_options
+ game: typing.ClassVar[str] = "Secret of Evermore"
+ options_dataclass = SoEOptions
+ options: SoEOptions
settings: typing.ClassVar[SoESettings]
topology_present = False
- data_version = 4
web = SoEWebWorld()
- required_client_version = (0, 3, 5)
+ required_client_version = (0, 4, 4)
item_name_to_id, item_id_to_raw = _get_item_mapping()
location_name_to_id, location_id_to_raw = _get_location_mapping()
item_name_groups = _get_item_grouping()
- trap_types = [name[12:] for name in option_definitions if name.startswith('trap_chance_')]
-
+ logic: SoEPlayerLogic
evermizer_seed: int
connect_name: str
- energy_core: int
- available_fragments: int
- required_fragments: int
_halls_ne_chest_names: typing.List[str] = [loc.name for loc in _locations if 'Halls NE' in loc.name]
- def __init__(self, *args, **kwargs):
+ def __init__(self, multiworld: "MultiWorld", player: int):
self.connect_name_available_event = threading.Event()
- super(SoEWorld, self).__init__(*args, **kwargs)
+ super(SoEWorld, self).__init__(multiworld, player)
def generate_early(self) -> None:
- # store option values that change logic
- self.energy_core = self.multiworld.energy_core[self.player].value
- self.required_fragments = self.multiworld.required_fragments[self.player].value
- if self.required_fragments > self.multiworld.available_fragments[self.player].value:
- self.multiworld.available_fragments[self.player].value = self.required_fragments
- self.available_fragments = self.multiworld.available_fragments[self.player].value
+ # create logic from options
+ if self.options.required_fragments.value > self.options.available_fragments.value:
+ self.options.available_fragments.value = self.options.required_fragments.value
+ self.logic = SoEPlayerLogic(self.player, self.options)
def create_event(self, event: str) -> Item:
return SoEItem(event, ItemClassification.progression, None, self.player)
@@ -214,54 +217,66 @@ def create_item(self, item: typing.Union[pyevermizer.Item, str]) -> Item:
return SoEItem(item.name, classification, self.item_name_to_id[item.name], self.player)
@classmethod
- def stage_assert_generate(cls, multiworld):
+ def stage_assert_generate(cls, _: "MultiWorld") -> None:
rom_file = get_base_rom_path()
if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file)
- def create_regions(self):
+ def create_regions(self) -> None:
# exclude 'hidden' on easy
- max_difficulty = 1 if self.multiworld.difficulty[self.player] == Difficulty.option_easy else 256
+ max_difficulty = 1 if self.options.difficulty == Difficulty.option_easy else 256
# TODO: generate *some* regions from locations' requirements?
- r = Region('Menu', self.player, self.multiworld)
- r.exits = [Entrance(self.player, 'New Game', r)]
- self.multiworld.regions += [r]
+ menu = Region('Menu', self.player, self.multiworld)
+ self.multiworld.regions += [menu]
- def get_sphere_index(evermizer_loc):
+ def get_sphere_index(evermizer_loc: pyevermizer.Location) -> int:
"""Returns 0, 1 or 2 for locations in spheres 1, 2, 3+"""
if len(evermizer_loc.requires) == 1 and evermizer_loc.requires[0][1] != pyevermizer.P_WEAPON:
return 2
return min(2, len(evermizer_loc.requires))
+ # create ingame region
+ ingame = Region('Ingame', self.player, self.multiworld)
+
# group locations into spheres (1, 2, 3+ at index 0, 1, 2)
spheres: typing.Dict[int, typing.Dict[int, typing.List[SoELocation]]] = {}
for loc in _locations:
spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append(
- SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r,
+ SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame,
loc.difficulty > max_difficulty))
+ # extend pool if feature and setting enabled
+ if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere:
+ for loc in _sniff_locations:
+ spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append(
+ SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame,
+ loc.difficulty > max_difficulty))
# location balancing data
trash_fills: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int, int]]] = {
- 0: {pyevermizer.CHECK_GOURD: (20, 40, 40, 40)}, # remove up to 40 gourds from sphere 1
- 1: {pyevermizer.CHECK_GOURD: (70, 90, 90, 90)}, # remove up to 90 gourds from sphere 2
+ 0: {pyevermizer.CHECK_GOURD: (20, 40, 40, 40), # remove up to 40 gourds from sphere 1
+ pyevermizer.CHECK_SNIFF: (100, 130, 130, 130)}, # remove up to 130 sniff spots from sphere 1
+ 1: {pyevermizer.CHECK_GOURD: (70, 90, 90, 90), # remove up to 90 gourds from sphere 2
+ pyevermizer.CHECK_SNIFF: (160, 200, 200, 200)}, # remove up to 200 sniff spots from sphere 2
}
# mark some as excluded based on numbers above
for trash_sphere, fills in trash_fills.items():
for typ, counts in fills.items():
- count = counts[self.multiworld.difficulty[self.player].value]
- for location in self.multiworld.random.sample(spheres[trash_sphere][typ], count):
+ if typ not in spheres[trash_sphere]:
+ continue # e.g. player does not have sniff locations
+ count = counts[self.options.difficulty.value]
+ for location in self.random.sample(spheres[trash_sphere][typ], count):
assert location.name != "Energy Core #285", "Error in sphere generation"
location.progress_type = LocationProgressType.EXCLUDED
- def sphere1_blocked_items_rule(item):
+ def sphere1_blocked_items_rule(item: pyevermizer.Item) -> bool:
if isinstance(item, SoEItem):
# disable certain items in sphere 1
if item.name in {"Gauge", "Wheel"}:
return False
# and some more for non-easy, non-mystery
- if self.multiworld.difficulty[item.player] not in (Difficulty.option_easy, Difficulty.option_mystery):
+ if self.options.difficulty not in (Difficulty.option_easy, Difficulty.option_mystery):
if item.name in {"Laser Lance", "Atom Smasher", "Diamond Eye"}:
return False
return True
@@ -271,39 +286,46 @@ def sphere1_blocked_items_rule(item):
add_item_rule(location, sphere1_blocked_items_rule)
# make some logically late(r) bosses priority locations to increase complexity
- if self.multiworld.difficulty[self.player] == Difficulty.option_mystery:
- late_count = self.multiworld.random.randint(0, 2)
+ if self.options.difficulty == Difficulty.option_mystery:
+ late_count = self.random.randint(0, 2)
else:
- late_count = self.multiworld.difficulty[self.player].value
+ late_count = self.options.difficulty.value
late_bosses = ("Tiny", "Aquagoth", "Megataur", "Rimsala",
"Mungola", "Lightning Storm", "Magmar", "Volcano Viper")
- late_locations = self.multiworld.random.sample(late_bosses, late_count)
+ late_locations = self.random.sample(late_bosses, late_count)
# add locations to the world
- r = Region('Ingame', self.player, self.multiworld)
for sphere in spheres.values():
for locations in sphere.values():
for location in locations:
- r.locations.append(location)
+ ingame.locations.append(location)
if location.name in late_locations:
location.progress_type = LocationProgressType.PRIORITY
- r.locations.append(SoELocation(self.player, 'Done', None, r))
- self.multiworld.regions += [r]
-
- self.multiworld.get_entrance('New Game', self.player).connect(self.multiworld.get_region('Ingame', self.player))
+ ingame.locations.append(SoELocation(self.player, 'Done', None, ingame))
+ menu.connect(ingame, "New Game")
+ self.multiworld.regions += [ingame]
- def create_items(self):
+ def create_items(self) -> None:
# add regular items to the pool
exclusions: typing.List[str] = []
- if self.energy_core != EnergyCore.option_shuffle:
+ if self.options.energy_core != EnergyCore.option_shuffle:
exclusions.append("Energy Core") # will be placed in generate_basic or replaced by a fragment below
items = list(map(lambda item: self.create_item(item), (item for item in _items if item.name not in exclusions)))
# remove one pair of wings that will be placed in generate_basic
items.remove(self.create_item("Wings"))
- def is_ingredient(item):
+ # extend pool if feature and setting enabled
+ if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere:
+ if self.options.sniff_ingredients == SniffIngredients.option_vanilla_ingredients:
+ # vanilla ingredients
+ items += list(map(lambda item: self.create_item(item), _sniff_items))
+ else:
+ # random ingredients
+ items += [self.create_item(self.get_filler_item_name()) for _ in _sniff_items]
+
+ def is_ingredient(item: pyevermizer.Item) -> bool:
for ingredient in _ingredients:
if _match_item_name(item, ingredient):
return True
@@ -311,84 +333,77 @@ def is_ingredient(item):
# add energy core fragments to the pool
ingredients = [n for n, item in enumerate(items) if is_ingredient(item)]
- if self.energy_core == EnergyCore.option_fragments:
+ if self.options.energy_core == EnergyCore.option_fragments:
items.append(self.create_item("Energy Core Fragment")) # replaces the vanilla energy core
- for _ in range(self.available_fragments - 1):
+ for _ in range(self.options.available_fragments - 1):
if len(ingredients) < 1:
break # out of ingredients to replace
- r = self.multiworld.random.choice(ingredients)
+ r = self.random.choice(ingredients)
ingredients.remove(r)
items[r] = self.create_item("Energy Core Fragment")
# add traps to the pool
- trap_count = self.multiworld.trap_count[self.player].value
- trap_chances = {}
- trap_names = {}
+ trap_count = self.options.trap_count.value
+ trap_names: typing.List[str] = []
+ trap_weights: typing.List[int] = []
if trap_count > 0:
- for trap_type in self.trap_types:
- trap_option = getattr(self.multiworld, f'trap_chance_{trap_type}')[self.player]
- trap_chances[trap_type] = trap_option.value
- trap_names[trap_type] = trap_option.item_name
- trap_chances_total = sum(trap_chances.values())
- if trap_chances_total == 0:
- for trap_type in trap_chances:
- trap_chances[trap_type] = 1
- trap_chances_total = len(trap_chances)
+ for trap_option in self.options.trap_chances:
+ trap_names.append(trap_option.item_name)
+ trap_weights.append(trap_option.value)
+ if sum(trap_weights) == 0:
+ trap_weights = [1 for _ in trap_weights]
def create_trap() -> Item:
- v = self.multiworld.random.randrange(trap_chances_total)
- for t, c in trap_chances.items():
- if v < c:
- return self.create_item(trap_names[t])
- v -= c
- assert False, "Bug in create_trap"
+ return self.create_item(self.random.choices(trap_names, trap_weights)[0])
for _ in range(trap_count):
if len(ingredients) < 1:
break # out of ingredients to replace
- r = self.multiworld.random.choice(ingredients)
+ r = self.random.choice(ingredients)
ingredients.remove(r)
items[r] = create_trap()
self.multiworld.itempool += items
- def set_rules(self):
+ def set_rules(self) -> None:
self.multiworld.completion_condition[self.player] = lambda state: state.has('Victory', self.player)
# set Done from goal option once we have multiple goals
set_rule(self.multiworld.get_location('Done', self.player),
- lambda state: state.soe_has(pyevermizer.P_FINAL_BOSS, self.multiworld, self.player))
+ lambda state: self.logic.has(state, pyevermizer.P_FINAL_BOSS))
set_rule(self.multiworld.get_entrance('New Game', self.player), lambda state: True)
- for loc in _locations:
+ locations: typing.Iterable[pyevermizer.Location]
+ if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere:
+ locations = itertools.chain(_locations, _sniff_locations)
+ else:
+ locations = _locations
+ for loc in locations:
location = self.multiworld.get_location(loc.name, self.player)
set_rule(location, self.make_rule(loc.requires))
def make_rule(self, requires: typing.List[typing.Tuple[int, int]]) -> typing.Callable[[typing.Any], bool]:
- def rule(state) -> bool:
+ def rule(state: "CollectionState") -> bool:
for count, progress in requires:
- if not state.soe_has(progress, self.multiworld, self.player, count):
+ if not self.logic.has(state, progress, count):
return False
return True
return rule
- def make_item_type_limit_rule(self, item_type: int):
- return lambda item: item.player != self.player or self.item_id_to_raw[item.code].type == item_type
-
- def generate_basic(self):
+ def generate_basic(self) -> None:
# place Victory event
self.multiworld.get_location('Done', self.player).place_locked_item(self.create_event('Victory'))
# place wings in halls NE to avoid softlock
- wings_location = self.multiworld.random.choice(self._halls_ne_chest_names)
+ wings_location = self.random.choice(self._halls_ne_chest_names)
wings_item = self.create_item('Wings')
self.multiworld.get_location(wings_location, self.player).place_locked_item(wings_item)
# place energy core at vanilla location for vanilla mode
- if self.energy_core == EnergyCore.option_vanilla:
+ if self.options.energy_core == EnergyCore.option_vanilla:
energy_core = self.create_item('Energy Core')
self.multiworld.get_location('Energy Core #285', self.player).place_locked_item(energy_core)
# generate stuff for later
- self.evermizer_seed = self.multiworld.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando?
+ self.evermizer_seed = self.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando?
- def generate_output(self, output_directory: str):
+ def generate_output(self, output_directory: str) -> None:
player_name = self.multiworld.get_player_name(self.player)
self.connect_name = player_name[:32]
while len(self.connect_name.encode('utf-8')) > 32:
@@ -397,27 +412,24 @@ def generate_output(self, output_directory: str):
placement_file = ""
out_file = ""
try:
- money = self.multiworld.money_modifier[self.player].value
- exp = self.multiworld.exp_modifier[self.player].value
+ money = self.options.money_modifier.value
+ exp = self.options.exp_modifier.value
switches: typing.List[str] = []
- if self.multiworld.death_link[self.player].value:
+ if self.options.death_link.value:
switches.append("--death-link")
- if self.energy_core == EnergyCore.option_fragments:
- switches.extend(('--available-fragments', str(self.available_fragments),
- '--required-fragments', str(self.required_fragments)))
+ if self.options.energy_core == EnergyCore.option_fragments:
+ switches.extend(('--available-fragments', str(self.options.available_fragments.value),
+ '--required-fragments', str(self.options.required_fragments.value)))
rom_file = get_base_rom_path()
out_base = output_path(output_directory, self.multiworld.get_out_file_name_base(self.player))
out_file = out_base + '.sfc'
placement_file = out_base + '.txt'
patch_file = out_base + '.apsoe'
flags = 'l' # spoiler log
- for option_name in self.option_definitions:
- option = getattr(self.multiworld, option_name)[self.player]
- if hasattr(option, 'to_flag'):
- flags += option.to_flag()
+ flags += self.options.flags
with open(placement_file, "wb") as f: # generate placement file
- for location in filter(lambda l: l.player == self.player, self.multiworld.get_locations()):
+ for location in self.multiworld.get_locations(self.player):
item = location.item
assert item is not None, "Can't handle unfilled location"
if item.code is None or location.address is None:
@@ -448,7 +460,7 @@ def generate_output(self, output_directory: str):
except FileNotFoundError:
pass
- def modify_multidata(self, multidata: dict):
+ def modify_multidata(self, multidata: typing.Dict[str, typing.Any]) -> None:
# wait for self.connect_name to be available.
self.connect_name_available_event.wait()
# we skip in case of error, so that the original error in the output thread is the one that gets raised
@@ -457,7 +469,7 @@ def modify_multidata(self, multidata: dict):
multidata["connect_names"][self.connect_name] = payload
def get_filler_item_name(self) -> str:
- return self.multiworld.random.choice(list(self.item_name_groups["Ingredients"]))
+ return self.random.choice(list(self.item_name_groups["Ingredients"]))
class SoEItem(Item):
@@ -473,4 +485,3 @@ def __init__(self, player: int, name: str, address: typing.Optional[int], parent
super().__init__(player, name, address, parent)
# unconditional assignments favor a split dict, saving memory
self.progress_type = LocationProgressType.EXCLUDED if exclude else LocationProgressType.DEFAULT
- self.event = not address
diff --git a/worlds/soe/docs/en_Secret of Evermore.md b/worlds/soe/docs/en_Secret of Evermore.md
index 215a5387bb9f..98882b6ff7e3 100644
--- a/worlds/soe/docs/en_Secret of Evermore.md
+++ b/worlds/soe/docs/en_Secret of Evermore.md
@@ -1,8 +1,8 @@
# Secret of Evermore
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
diff --git a/worlds/soe/docs/multiworld_en.md b/worlds/soe/docs/multiworld_en.md
index d995cea56ae9..a2944d4c012b 100644
--- a/worlds/soe/docs/multiworld_en.md
+++ b/worlds/soe/docs/multiworld_en.md
@@ -2,16 +2,19 @@
## Required Software
-- SNI from: [SNI Releases Page](https://github.com/alttpo/sni/releases)
- - v0.0.59 or newer (included in Archipelago 0.2.1 setup)
-- Hardware or software capable of loading and playing SNES ROM files
- - An emulator capable of connecting to SNI with ROM access. Any one of the following will work:
- - snes9x-rr from: [snes9x-rr Releases Page](https://github.com/gocha/snes9x-rr/releases)
- - BizHawk from: [TASVideos](https://tasvideos.org/BizHawk)
- - bsnes-plus-nwa from: [bsnes-plus GitHub](https://github.com/black-sliver/bsnes-plus)
- - RetroArch from: [RetroArch Website](https://retroarch.com?page=platforms). Or,
- - Or SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other
- compatible hardware.
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) (optional, but recommended).
+- [SNI](https://github.com/alttpo/sni/releases). This is automatically included with your Archipelago installation above.
+- SNI is not compatible with (Q)Usb2Snes.
+- Hardware or software capable of loading and playing SNES ROM files, including:
+ - An emulator capable of connecting to SNI
+ ([snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases), [snes9x-rr](https://github.com/gocha/snes9x-rr/releases),
+ [BSNES-plus](https://github.com/black-sliver/bsnes-plus),
+ [BizHawk](http://tasvideos.org/BizHawk.html), or
+ [RetroArch](https://retroarch.com?page=platforms) 1.10.1 or newer)
+ - An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware. **note:
+ modded SNES minis are currently not supported by SNI. Some users have claimed success with QUsb2Snes for this system,
+ but it is not supported.**
+- A modern web browser to run the client.
- Your legally obtained Secret of Evermore US ROM file, probably named `Secret of Evermore (USA).sfc`
## Create a Config (.yaml) File
@@ -23,21 +26,21 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a config file?
-The Player Settings page on the website allows you to configure your personal settings and export a config file from
-them. Player settings page: [Secret of Evermore Player Settings PAge](/games/Secret%20of%20Evermore/player-settings)
+The Player Options page on the website allows you to configure your personal options and export a config file from
+them. Player options page: [Secret of Evermore Player Options Page](/games/Secret%20of%20Evermore/player-options)
### 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
Stand-alone "Evermizer" has a way of balancing single-player games, but may not always be on par feature-wise. Head over
to the [Evermizer Website](https://evermizer.com) if you want to try the official stand-alone, otherwise read below.
-1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button.
- - Player Settings page: [Secret of Evermore Player Settings Page](/games/Secret%20of%20Evermore/player-settings)
+1. Navigate to the Player Options page, configure your options, and click the "Generate Game" button.
+ - Player Options page: [Secret of Evermore Player Options Page](/games/Secret%20of%20Evermore/player-options)
2. You will be presented with a "Seed Info" page.
3. Click the "Create New Room" link.
4. You will be presented with a server page, from which you can download your patch file.
@@ -58,11 +61,16 @@ page: [Evermizer apbpatch Page](https://evermizer.com/apbpatch)
### Connect to SNI
-#### With an emulator
-
Start SNI either from the Archipelago install folder or the stand-alone version. If this is its first time launching,
you may be prompted to allow it to communicate through the Windows Firewall.
+#### With an emulator
+
+##### snes9x-nwa
+
+1. Click on the Network Menu and check **Enable Emu Network Control**
+2. Load your ROM file if it hasn't already been loaded.
+
##### snes9x-rr
1. Load your ROM file if it hasn't already been loaded.
diff --git a/worlds/soe/logic.py b/worlds/soe/logic.py
new file mode 100644
index 000000000000..92ffb14b3f95
--- /dev/null
+++ b/worlds/soe/logic.py
@@ -0,0 +1,88 @@
+import typing
+from itertools import chain
+from typing import Callable, Set
+
+from . import pyevermizer
+from .options import EnergyCore, OutOfBounds, SequenceBreaks, SoEOptions
+
+if typing.TYPE_CHECKING:
+ from BaseClasses import CollectionState
+
+# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early?
+
+# TODO: resolve/flatten/expand rules to get rid of recursion below where possible
+# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items)
+rules = pyevermizer.get_logic()
+# Logic.items are all items and extra items excluding non-progression items and duplicates
+# NOTE: we are skipping sniff items here because none of them is supposed to provide progression
+item_names: Set[str] = set()
+items = [item for item in filter(lambda item: item.progression, # type: ignore[arg-type]
+ chain(pyevermizer.get_items(), pyevermizer.get_extra_items()))
+ if item.name not in item_names and not item_names.add(item.name)] # type: ignore[func-returns-value]
+
+
+class SoEPlayerLogic:
+ __slots__ = "player", "out_of_bounds", "sequence_breaks", "has"
+ player: int
+ out_of_bounds: bool
+ sequence_breaks: bool
+
+ has: Callable[..., bool]
+ """
+ Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE
+ """
+
+ def __init__(self, player: int, options: "SoEOptions"):
+ self.player = player
+ self.out_of_bounds = options.out_of_bounds == OutOfBounds.option_logic
+ self.sequence_breaks = options.sequence_breaks == SequenceBreaks.option_logic
+
+ if options.energy_core == EnergyCore.option_fragments:
+ # override logic for energy core fragments
+ required_fragments = options.required_fragments.value
+
+ def fragmented_has(state: "CollectionState", progress: int, count: int = 1) -> bool:
+ if progress == pyevermizer.P_ENERGY_CORE:
+ progress = pyevermizer.P_CORE_FRAGMENT
+ count = required_fragments
+ return self._has(state, progress, count)
+
+ self.has = fragmented_has
+ else:
+ # default (energy core) logic
+ self.has = self._has
+
+ def _count(self, state: "CollectionState", progress: int, max_count: int = 0) -> int:
+ """
+ Returns reached count of one of evermizer's progress steps based on collected items.
+ i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP
+ """
+ n = 0
+ for item in items:
+ for pvd in item.provides:
+ if pvd[1] == progress:
+ if state.has(item.name, self.player):
+ n += state.count(item.name, self.player) * pvd[0]
+ if n >= max_count > 0:
+ return n
+ for rule in rules:
+ for pvd in rule.provides:
+ if pvd[1] == progress and pvd[0] > 0:
+ has = True
+ for req in rule.requires:
+ if not self.has(state, req[1], req[0]):
+ has = False
+ break
+ if has:
+ n += pvd[0]
+ if n >= max_count > 0:
+ return n
+ return n
+
+ def _has(self, state: "CollectionState", progress: int, count: int = 1) -> bool:
+ """Default implementation of has"""
+ if self.out_of_bounds is True and progress == pyevermizer.P_ALLOW_OOB:
+ return True
+ if self.sequence_breaks is True and progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS:
+ return True
+ return self._count(state, progress, count) >= count
diff --git a/worlds/soe/options.py b/worlds/soe/options.py
new file mode 100644
index 000000000000..5ecd0f9e6666
--- /dev/null
+++ b/worlds/soe/options.py
@@ -0,0 +1,319 @@
+from dataclasses import dataclass, fields
+from datetime import datetime
+from typing import Any, ClassVar, cast, Dict, Iterator, List, Tuple, Protocol
+
+from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option, PerGameCommonOptions, \
+ ProgressionBalancing, Range, Toggle
+
+
+# typing boilerplate
+class FlagsProtocol(Protocol):
+ value: int
+ default: ClassVar[int]
+ flags: List[str]
+
+
+class FlagProtocol(Protocol):
+ value: int
+ default: ClassVar[int]
+ flag: str
+
+
+# meta options
+class EvermizerFlags:
+ flags: List[str]
+
+ def to_flag(self: FlagsProtocol) -> str:
+ return self.flags[self.value]
+
+
+class EvermizerFlag:
+ flag: str
+
+ def to_flag(self: FlagProtocol) -> str:
+ return self.flag if self.value != self.default else ''
+
+
+class OffOnFullChoice(Choice):
+ option_off = 0
+ option_on = 1
+ option_full = 2
+ alias_chaos = 2
+
+
+class OffOnLogicChoice(Choice):
+ option_off = 0
+ option_on = 1
+ option_logic = 2
+
+
+# actual options
+class Difficulty(EvermizerFlags, Choice):
+ """Changes relative spell cost and stuff"""
+ display_name = "Difficulty"
+ option_easy = 0
+ option_normal = 1
+ option_hard = 2
+ option_mystery = 3 # 'random' is reserved
+ alias_chaos = 3
+ default = 1
+ flags = ['e', 'n', 'h', 'x']
+
+
+class EnergyCore(EvermizerFlags, Choice):
+ """How to obtain the Energy Core"""
+ display_name = "Energy Core"
+ option_vanilla = 0
+ option_shuffle = 1
+ option_fragments = 2
+ default = 1
+ flags = ['z', '', 'Z']
+
+
+class RequiredFragments(Range):
+ """Required fragment count for Energy Core = Fragments"""
+ display_name = "Required Fragments"
+ range_start = 1
+ range_end = 99
+ default = 10
+
+
+class AvailableFragments(Range):
+ """Placed fragment count for Energy Core = Fragments"""
+ display_name = "Available Fragments"
+ range_start = 1
+ range_end = 99
+ default = 11
+
+
+class MoneyModifier(Range):
+ """Money multiplier in %"""
+ display_name = "Money Modifier"
+ range_start = 1
+ range_end = 2500
+ default = 200
+
+
+class ExpModifier(Range):
+ """EXP multiplier for Weapons, Characters and Spells in %"""
+ display_name = "Exp Modifier"
+ range_start = 1
+ range_end = 2500
+ default = 200
+
+
+class SequenceBreaks(EvermizerFlags, OffOnLogicChoice):
+ """Disable, enable some sequence breaks or put them in logic"""
+ display_name = "Sequence Breaks"
+ default = 0
+ flags = ['', 'j', 'J']
+
+
+class OutOfBounds(EvermizerFlags, OffOnLogicChoice):
+ """Disable, enable the out-of-bounds glitch or put it in logic"""
+ display_name = "Out Of Bounds"
+ default = 0
+ flags = ['', 'u', 'U']
+
+
+class FixCheats(EvermizerFlag, DefaultOnToggle):
+ """Fix cheats left in by the devs (not desert skip)"""
+ display_name = "Fix Cheats"
+ flag = '2'
+
+
+class FixInfiniteAmmo(EvermizerFlag, Toggle):
+ """Fix infinite ammo glitch"""
+ display_name = "Fix Infinite Ammo"
+ flag = '5'
+
+
+class FixAtlasGlitch(EvermizerFlag, Toggle):
+ """Fix atlas underflowing stats"""
+ display_name = "Fix Atlas Glitch"
+ flag = '6'
+
+
+class FixWingsGlitch(EvermizerFlag, Toggle):
+ """Fix wings making you invincible in some areas"""
+ display_name = "Fix Wings Glitch"
+ flag = '7'
+
+
+class ShorterDialogs(EvermizerFlag, DefaultOnToggle):
+ """Cuts some dialogs"""
+ display_name = "Shorter Dialogs"
+ flag = '9'
+
+
+class ShortBossRush(EvermizerFlag, DefaultOnToggle):
+ """Start boss rush at Metal Magmar, cut enemy HP in half"""
+ display_name = "Short Boss Rush"
+ flag = 'f'
+
+
+class Ingredienizer(EvermizerFlags, OffOnFullChoice):
+ """On Shuffles, Full randomizes spell ingredients"""
+ display_name = "Ingredienizer"
+ default = 1
+ flags = ['i', '', 'I']
+
+
+class Sniffamizer(EvermizerFlags, Choice):
+ """
+ Off: all vanilla items in sniff spots
+ Shuffle: sniff items shuffled into random sniff spots
+ """
+ display_name = "Sniffamizer"
+ option_off = 0
+ option_shuffle = 1
+ if datetime.today().year > 2024 or datetime.today().month > 3:
+ option_everywhere = 2
+ __doc__ = __doc__ + " Everywhere: add sniff spots to multiworld pool"
+ alias_true = 1
+ default = 1
+ flags = ['s', '', 'S']
+
+
+class SniffIngredients(EvermizerFlag, Choice):
+ """Select which items should be used as sniff items"""
+ display_name = "Sniff Ingredients"
+ option_vanilla_ingredients = 0
+ option_random_ingredients = 1
+ flag = 'v'
+
+
+class Callbeadamizer(EvermizerFlags, OffOnFullChoice):
+ """On Shuffles call bead characters, Full shuffles individual spells"""
+ display_name = "Callbeadamizer"
+ default = 1
+ flags = ['c', '', 'C']
+
+
+class Musicmizer(EvermizerFlag, Toggle):
+ """Randomize music for some rooms"""
+ display_name = "Musicmizer"
+ flag = 'm'
+
+
+class Doggomizer(EvermizerFlags, OffOnFullChoice):
+ """On shuffles dog per act, Full randomizes dog per screen, Pupdunk gives you Everpupper everywhere"""
+ display_name = "Doggomizer"
+ option_pupdunk = 3
+ default = 0
+ flags = ['', 'd', 'D', 'p']
+
+
+class TurdoMode(EvermizerFlag, Toggle):
+ """Replace offensive spells by Turd Balls with varying strength and make weapons weak"""
+ display_name = "Turdo Mode"
+ flag = 't'
+
+
+class TrapCount(Range):
+ """Replace some filler items with traps"""
+ display_name = "Trap Count"
+ range_start = 0
+ range_end = 100
+ default = 0
+
+
+# more meta options
+class ItemChanceMeta(AssembleOptions):
+ def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[Any, Any]) -> "ItemChanceMeta":
+ if 'item_name' in attrs:
+ attrs["display_name"] = f"{attrs['item_name']} Chance"
+ attrs["range_start"] = 0
+ attrs["range_end"] = 100
+ cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs) # type: ignore[no-untyped-call]
+ return cast(ItemChanceMeta, cls)
+
+
+class TrapChance(Range, metaclass=ItemChanceMeta):
+ item_name: str
+ default = 20
+
+
+# more actual options
+class TrapChanceQuake(TrapChance):
+ """Sets the chance/ratio of quake traps"""
+ item_name = "Quake Trap"
+
+
+class TrapChancePoison(TrapChance):
+ """Sets the chance/ratio of poison effect traps"""
+ item_name = "Poison Trap"
+
+
+class TrapChanceConfound(TrapChance):
+ """Sets the chance/ratio of confound effect traps"""
+ item_name = "Confound Trap"
+
+
+class TrapChanceHUD(TrapChance):
+ """Sets the chance/ratio of HUD visibility traps"""
+ item_name = "HUD Trap"
+
+
+class TrapChanceOHKO(TrapChance):
+ """Sets the chance/ratio of OHKO (1HP left) traps"""
+ item_name = "OHKO Trap"
+
+
+class SoEProgressionBalancing(ProgressionBalancing):
+ default = 30
+ __doc__ = ProgressionBalancing.__doc__.replace(f"default {ProgressionBalancing.default}", f"default {default}") \
+ if ProgressionBalancing.__doc__ else None
+ special_range_names = {**ProgressionBalancing.special_range_names, "normal": default}
+
+
+# noinspection SpellCheckingInspection
+@dataclass
+class SoEOptions(PerGameCommonOptions):
+ difficulty: Difficulty
+ energy_core: EnergyCore
+ required_fragments: RequiredFragments
+ available_fragments: AvailableFragments
+ money_modifier: MoneyModifier
+ exp_modifier: ExpModifier
+ sequence_breaks: SequenceBreaks
+ out_of_bounds: OutOfBounds
+ fix_cheats: FixCheats
+ fix_infinite_ammo: FixInfiniteAmmo
+ fix_atlas_glitch: FixAtlasGlitch
+ fix_wings_glitch: FixWingsGlitch
+ shorter_dialogs: ShorterDialogs
+ short_boss_rush: ShortBossRush
+ ingredienizer: Ingredienizer
+ sniffamizer: Sniffamizer
+ sniff_ingredients: SniffIngredients
+ callbeadamizer: Callbeadamizer
+ musicmizer: Musicmizer
+ doggomizer: Doggomizer
+ turdo_mode: TurdoMode
+ death_link: DeathLink
+ trap_count: TrapCount
+ trap_chance_quake: TrapChanceQuake
+ trap_chance_poison: TrapChancePoison
+ trap_chance_confound: TrapChanceConfound
+ trap_chance_hud: TrapChanceHUD
+ trap_chance_ohko: TrapChanceOHKO
+ progression_balancing: SoEProgressionBalancing
+
+ @property
+ def trap_chances(self) -> Iterator[TrapChance]:
+ for field in fields(self):
+ option = getattr(self, field.name)
+ if isinstance(option, TrapChance):
+ yield option
+
+ @property
+ def flags(self) -> str:
+ flags = ''
+ for field in fields(self):
+ option = getattr(self, field.name)
+ if isinstance(option, (EvermizerFlag, EvermizerFlags)):
+ assert isinstance(option, Option)
+ # noinspection PyUnresolvedReferences
+ flags += option.to_flag()
+ return flags
diff --git a/worlds/soe/patch.py b/worlds/soe/patch.py
new file mode 100644
index 000000000000..a322de2af65f
--- /dev/null
+++ b/worlds/soe/patch.py
@@ -0,0 +1,44 @@
+import os
+from typing import BinaryIO, Optional
+
+import Utils
+from worlds.Files import APDeltaPatch
+
+
+USHASH = '6e9c94511d04fac6e0a1e582c170be3a'
+
+
+class SoEDeltaPatch(APDeltaPatch):
+ hash = USHASH
+ game = "Secret of Evermore"
+ patch_file_ending = ".apsoe"
+
+ @classmethod
+ def get_source_data(cls) -> bytes:
+ with open(get_base_rom_path(), "rb") as stream:
+ return read_rom(stream)
+
+
+def get_base_rom_path(file_name: Optional[str] = None) -> str:
+ options = Utils.get_options()
+ if not file_name:
+ file_name = options["soe_options"]["rom_file"]
+ if not file_name:
+ raise ValueError("Missing soe_options -> rom_file from host.yaml")
+ if not os.path.exists(file_name):
+ file_name = Utils.user_path(file_name)
+ return file_name
+
+
+def read_rom(stream: BinaryIO, strip_header: bool = True) -> bytes:
+ """Reads rom into bytearray and optionally strips off any smc header"""
+ data = stream.read()
+ if strip_header and len(data) % 0x400 == 0x200:
+ return data[0x200:]
+ return data
+
+
+if __name__ == '__main__':
+ import sys
+ print('Please use ../../patch.py', file=sys.stderr)
+ sys.exit(1)
diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt
index 878a2a80cc32..4bcacb33c33c 100644
--- a/worlds/soe/requirements.txt
+++ b/worlds/soe/requirements.txt
@@ -1,18 +1,36 @@
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.11'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.11'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.11'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-macosx_10_9_x86_64.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.8'
-#pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-macosx_10_9_x86_64.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.9'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.9'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.10'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.11'
-pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-1.tar.gz#0.44.0 ; python_version < '3.8' or python_version > '3.11' or (sys_platform != 'win32' and sys_platform != 'linux' and sys_platform != 'darwin') or (platform_machine != 'AMD64' and platform_machine != 'x86_64' and platform_machine != 'aarch64' and platform_machine != 'universal2' and platform_machine != 'arm64')
+pyevermizer==0.48.0 \
+ --hash=sha256:069ce348e480e04fd6208cfd0f789c600b18d7c34b5272375b95823be191ed57 \
+ --hash=sha256:58164dddaba2f340b0a8b4f39605e9dac46d8b0ffb16120e2e57bef2bfc1d683 \
+ --hash=sha256:115dd09d38a10f11d4629b340dfd75e2ba4089a1ff9e9748a11619829e02c876 \
+ --hash=sha256:b5e79cfe721e75cd7dec306b5eecd6385ce059e31ef7523ba7f677e22161ec6f \
+ --hash=sha256:382882fa9d641b9969a6c3ed89449a814bdabcb6b17b558872d95008a6cc908b \
+ --hash=sha256:92f67700e9132064a90858d391dd0b8fb111aff6dfd472befed57772d89ae567 \
+ --hash=sha256:fe4c453b7dbd5aa834b81f9a7aedb949a605455650b938b8b304d8e5a7edcbf7 \
+ --hash=sha256:c6bdbc45daf73818f763ed59ad079f16494593395d806f772dd62605c722b3e9 \
+ --hash=sha256:bb09f45448fdfd28566ae6fcc38c35a6632f4c31a9de2483848f6ce17b2359b5 \
+ --hash=sha256:00a8b9014744bd1528d0d39c33ede7c0d1713ad797a331cebb33d377a5bc1064 \
+ --hash=sha256:64ee69edc0a7d3b3caded78f2e46975f9beaff1ff8feaf29b87da44c45f38d7d \
+ --hash=sha256:9211bdb1313e9f4869ed5bdc61f3831d39679bd08bb4087f1c1e5475d9e3018b \
+ --hash=sha256:4a57821e422a1d75fe3307931a78db7a65e76955f8e401c4b347db6570390d09 \
+ --hash=sha256:04670cee0a0b913f24d2b9a1e771781560e2485bda31e6cd372a08421cf85cfa \
+ --hash=sha256:971fe77d0a20a1db984020ad253b613d0983f5e23ff22cba60ee5ac00d8128de \
+ --hash=sha256:127265fdb49f718f54706bf15604af1cec23590afd00d423089dea4331dcfc61 \
+ --hash=sha256:d47576360337c1a23f424cd49944a8d68fc4f3338e00719c9f89972c84604bef \
+ --hash=sha256:879659603e51130a0de8d9885d815a2fa1df8bd6cebe6d520d1c6002302adfdb \
+ --hash=sha256:6a91bfc53dd130db6424adf8ac97a1133e97b4157ed00f889d8cbd26a2a4b340 \
+ --hash=sha256:f3bf35fc5eef4cda49d2de77339fc201dd3206660a3dc15db005625b15bb806c \
+ --hash=sha256:e7c8d5bf59a3c16db20411bc5d8e9c9087a30b6b4edf1b5ed9f4c013291427e4 \
+ --hash=sha256:054a4d84ffe75448d41e88e1e0642ef719eb6111be5fe608e71e27a558c59069 \
+ --hash=sha256:e6f141ca367469c69ba7fbf65836c479ec6672c598cfcb6b39e8098c60d346bc \
+ --hash=sha256:6e65eb88f0c1ff4acde1c13b24ce649b0fe3d1d3916d02d96836c781a5022571 \
+ --hash=sha256:e61e8f476b6da809cf38912755ed8bb009665f589e913eb8df877e9fa763024b \
+ --hash=sha256:7e7c5484c0a2e3da6064de3f73d8d988d6703db58ab0be4730cbbf1a82319237 \
+ --hash=sha256:9033b954e5f4878fd94af6d2056c78e3316115521fb1c24a4416d5cbf2ad66ad \
+ --hash=sha256:824c623fff8ae4da176306c458ad63ad16a06a495a16db700665eca3c115924f \
+ --hash=sha256:8e31031409a8386c6a63b79d480393481badb3ba29f32ff7a0db2b4abed20ac8 \
+ --hash=sha256:7dbb7bb13e1e94f69f7ccdbcf4d35776424555fce5af1ca29d0256f91fdf087a \
+ --hash=sha256:3a24e331b259407b6912d6e0738aa8a675831db3b7493fcf54dc17cb0cb80d37 \
+ --hash=sha256:fdda06662a994271e96633cba100dd92b2fcd524acef8b2f664d1aaa14503cbd \
+ --hash=sha256:0f0fc81bef3dbb78ba6a7622dd4296f23c59825968a0bb0448beb16eb3397cc2 \
+ --hash=sha256:e07cbef776a7468669211546887357cc88e9afcf1578b23a4a4f2480517b15d9 \
+ --hash=sha256:e442212695bdf60e455673b7b9dd83a5d4b830d714376477093d2c9054d92832
diff --git a/worlds/soe/test/TestAccess.py b/worlds/soe/test/TestAccess.py
deleted file mode 100644
index c7da7b889627..000000000000
--- a/worlds/soe/test/TestAccess.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import typing
-from . import SoETestBase
-
-
-class AccessTest(SoETestBase):
- @staticmethod
- def _resolveGourds(gourds: typing.Dict[str, typing.Iterable[int]]):
- return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers]
-
- def testBronzeAxe(self):
- gourds = {
- "Pyramid bottom": (118, 121, 122, 123, 124, 125),
- "Pyramid top": (140,)
- }
- locations = ["Rimsala"] + self._resolveGourds(gourds)
- items = [["Bronze Axe"]]
- self.assertAccessDependency(locations, items)
-
- def testBronzeSpearPlus(self):
- locations = ["Megataur"]
- items = [["Bronze Spear"], ["Lance (Weapon)"], ["Laser Lance"]]
- self.assertAccessDependency(locations, items)
diff --git a/worlds/soe/test/TestGoal.py b/worlds/soe/test/TestGoal.py
deleted file mode 100644
index d127d3899869..000000000000
--- a/worlds/soe/test/TestGoal.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from . import SoETestBase
-
-
-class TestFragmentGoal(SoETestBase):
- options = {
- "energy_core": "fragments",
- "available_fragments": 21,
- "required_fragments": 20,
- }
-
- def testFragments(self):
- self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"])
- self.assertBeatable(False) # 0 fragments
- fragments = self.get_items_by_name("Energy Core Fragment")
- victory = self.get_item_by_name("Victory")
- self.collect(fragments[:-2]) # 1 too few
- self.assertEqual(self.count("Energy Core Fragment"), 19)
- self.assertBeatable(False)
- self.collect(fragments[-2:-1]) # exact
- self.assertEqual(self.count("Energy Core Fragment"), 20)
- self.assertBeatable(True)
- self.remove([victory]) # reset
- self.collect(fragments[-1:]) # 1 extra
- self.assertEqual(self.count("Energy Core Fragment"), 21)
- self.assertBeatable(True)
-
- def testNoWeapon(self):
- self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core Fragment"])
- self.assertBeatable(False)
-
- def testNoRocket(self):
- self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core Fragment"])
- self.assertBeatable(False)
-
-
-class TestShuffleGoal(SoETestBase):
- options = {
- "energy_core": "shuffle",
- }
-
- def testCore(self):
- self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"])
- self.assertBeatable(False)
- self.collect_by_name(["Energy Core"])
- self.assertBeatable(True)
-
- def testNoWeapon(self):
- self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core"])
- self.assertBeatable(False)
-
- def testNoRocket(self):
- self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core"])
- self.assertBeatable(False)
diff --git a/worlds/soe/test/__init__.py b/worlds/soe/test/__init__.py
index 3c2a0dc1b625..e84d7e669d2c 100644
--- a/worlds/soe/test/__init__.py
+++ b/worlds/soe/test/__init__.py
@@ -1,5 +1,33 @@
-from test.TestBase import WorldTestBase
+from test.bases import WorldTestBase
+from typing import Iterable
+from .. import SoEWorld
class SoETestBase(WorldTestBase):
game = "Secret of Evermore"
+ world: SoEWorld
+
+ def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (),
+ satisfied: bool = True) -> None:
+ """
+ Tests that unreachable can't be reached. Tests that reachable can be reached if satisfied=True.
+ Usage: test with satisfied=False, collect requirements into state, test again with satisfied=True
+ """
+ for location in reachable:
+ self.assertEqual(self.can_reach_location(location), satisfied,
+ f"{location} is unreachable but should be" if satisfied else
+ f"{location} is reachable but shouldn't be")
+ for location in unreachable:
+ self.assertFalse(self.can_reach_location(location),
+ f"{location} is reachable but shouldn't be")
+
+ def testRocketPartsExist(self) -> None:
+ """Tests that rocket parts exist and are unique"""
+ self.assertEqual(len(self.get_items_by_name("Gauge")), 1)
+ self.assertEqual(len(self.get_items_by_name("Wheel")), 1)
+ diamond_eyes = self.get_items_by_name("Diamond Eye")
+ self.assertEqual(len(diamond_eyes), 3)
+ # verify diamond eyes are individual items
+ self.assertFalse(diamond_eyes[0] is diamond_eyes[1])
+ self.assertFalse(diamond_eyes[0] is diamond_eyes[2])
+ self.assertFalse(diamond_eyes[1] is diamond_eyes[2])
diff --git a/worlds/soe/test/test_access.py b/worlds/soe/test/test_access.py
new file mode 100644
index 000000000000..f1d6ee993b34
--- /dev/null
+++ b/worlds/soe/test/test_access.py
@@ -0,0 +1,22 @@
+import typing
+from . import SoETestBase
+
+
+class AccessTest(SoETestBase):
+ @staticmethod
+ def _resolveGourds(gourds: typing.Mapping[str, typing.Iterable[int]]) -> typing.List[str]:
+ return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers]
+
+ def test_bronze_axe(self) -> None:
+ gourds = {
+ "Pyramid bottom": (118, 121, 122, 123, 124, 125),
+ "Pyramid top": (140,)
+ }
+ locations = ["Rimsala"] + self._resolveGourds(gourds)
+ items = [["Bronze Axe"]]
+ self.assertAccessDependency(locations, items)
+
+ def test_bronze_spear_plus(self) -> None:
+ locations = ["Megataur"]
+ items = [["Bronze Spear"], ["Lance (Weapon)"], ["Laser Lance"]]
+ self.assertAccessDependency(locations, items)
diff --git a/worlds/soe/test/test_goal.py b/worlds/soe/test/test_goal.py
new file mode 100644
index 000000000000..bb64b8eca759
--- /dev/null
+++ b/worlds/soe/test/test_goal.py
@@ -0,0 +1,53 @@
+from . import SoETestBase
+
+
+class TestFragmentGoal(SoETestBase):
+ options = {
+ "energy_core": "fragments",
+ "available_fragments": 21,
+ "required_fragments": 20,
+ }
+
+ def test_fragments(self) -> None:
+ self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"])
+ self.assertBeatable(False) # 0 fragments
+ fragments = self.get_items_by_name("Energy Core Fragment")
+ victory = self.get_item_by_name("Victory")
+ self.collect(fragments[:-2]) # 1 too few
+ self.assertEqual(self.count("Energy Core Fragment"), 19)
+ self.assertBeatable(False)
+ self.collect(fragments[-2:-1]) # exact
+ self.assertEqual(self.count("Energy Core Fragment"), 20)
+ self.assertBeatable(True)
+ self.remove([victory]) # reset
+ self.collect(fragments[-1:]) # 1 extra
+ self.assertEqual(self.count("Energy Core Fragment"), 21)
+ self.assertBeatable(True)
+
+ def test_no_weapon(self) -> None:
+ self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core Fragment"])
+ self.assertBeatable(False)
+
+ def test_no_rocket(self) -> None:
+ self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core Fragment"])
+ self.assertBeatable(False)
+
+
+class TestShuffleGoal(SoETestBase):
+ options = {
+ "energy_core": "shuffle",
+ }
+
+ def test_core(self) -> None:
+ self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"])
+ self.assertBeatable(False)
+ self.collect_by_name(["Energy Core"])
+ self.assertBeatable(True)
+
+ def test_no_weapon(self) -> None:
+ self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core"])
+ self.assertBeatable(False)
+
+ def test_no_rocket(self) -> None:
+ self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core"])
+ self.assertBeatable(False)
diff --git a/worlds/soe/test/test_item_mapping.py b/worlds/soe/test/test_item_mapping.py
new file mode 100644
index 000000000000..7df05837c78b
--- /dev/null
+++ b/worlds/soe/test/test_item_mapping.py
@@ -0,0 +1,21 @@
+from unittest import TestCase
+from .. import SoEWorld
+
+
+class TestMapping(TestCase):
+ def test_atlas_medallion_name_group(self) -> None:
+ """
+ Test that we used the pyevermizer name for Atlas Medallion (not Amulet) in item groups.
+ """
+ self.assertIn("Any Atlas Medallion", SoEWorld.item_name_groups)
+
+ def test_atlas_medallion_name_items(self) -> None:
+ """
+ Test that we used the pyevermizer name for Atlas Medallion (not Amulet) in items.
+ """
+ found_medallion = False
+ for name in SoEWorld.item_name_to_id:
+ self.assertNotIn("Atlas Amulet", name, "Expected Atlas Medallion, not Amulet")
+ if "Atlas Medallion" in name:
+ found_medallion = True
+ self.assertTrue(found_medallion, "Did not find Atlas Medallion in items")
diff --git a/worlds/soe/test/test_oob.py b/worlds/soe/test/test_oob.py
new file mode 100644
index 000000000000..3c1a2829de8e
--- /dev/null
+++ b/worlds/soe/test/test_oob.py
@@ -0,0 +1,51 @@
+import typing
+from . import SoETestBase
+
+
+class OoBTest(SoETestBase):
+ """Tests that 'on' doesn't put out-of-bounds in logic. This is also the test base for OoB in logic."""
+ options: typing.Dict[str, typing.Any] = {"out_of_bounds": "on"}
+
+ def test_oob_access(self) -> None:
+ in_logic = self.options["out_of_bounds"] == "logic"
+
+ # some locations that just need a weapon + OoB
+ oob_reachable = [
+ "Aquagoth", "Sons of Sth.", "Mad Monk", "Magmar", # OoB can use volcano shop to skip rock skip
+ "Levitate", "Fireball", "Drain", "Speed",
+ "E. Crustacia #107", "Energy Core #285", "Vanilla Gauge #57",
+ ]
+ # some locations that should still be unreachable
+ oob_unreachable = [
+ "Tiny", "Rimsala",
+ "Barrier", "Call Up", "Reflect", "Force Field", "Stop", # Stop guy doesn't spawn for the other entrances
+ "Pyramid bottom #118", "Tiny's hideout #160", "Tiny's hideout #161", "Greenhouse #275",
+ ]
+ # OoB + Diamond Eyes
+ de_reachable = [
+ "Tiny's hideout #160",
+ ]
+ # still unreachable
+ de_unreachable = [
+ "Tiny",
+ "Tiny's hideout #161",
+ ]
+
+ self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=False)
+ self.collect_by_name("Gladiator Sword")
+ self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=in_logic)
+ self.collect_by_name("Diamond Eye")
+ self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic)
+
+ def test_oob_goal(self) -> None:
+ # still need Energy Core with OoB if sequence breaks are not in logic
+ for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]:
+ self.collect_by_name(item)
+ self.assertBeatable(False)
+ self.collect_by_name("Energy Core")
+ self.assertBeatable(True)
+
+
+class OoBInLogicTest(OoBTest):
+ """Tests that stuff that should be reachable/unreachable with out-of-bounds actually is."""
+ options: typing.Dict[str, typing.Any] = {"out_of_bounds": "logic"}
diff --git a/worlds/soe/test/test_sequence_breaks.py b/worlds/soe/test/test_sequence_breaks.py
new file mode 100644
index 000000000000..2da8c9242cb9
--- /dev/null
+++ b/worlds/soe/test/test_sequence_breaks.py
@@ -0,0 +1,45 @@
+import typing
+from . import SoETestBase
+
+
+class SequenceBreaksTest(SoETestBase):
+ """Tests that 'on' doesn't put sequence breaks in logic. This is also the test base for in-logic."""
+ options: typing.Dict[str, typing.Any] = {"sequence_breaks": "on"}
+
+ def test_sequence_breaks_access(self) -> None:
+ in_logic = self.options["sequence_breaks"] == "logic"
+
+ # some locations that just need any weapon + sequence break
+ break_reachable = [
+ "Sons of Sth.", "Mad Monk", "Magmar",
+ "Fireball",
+ "Volcano Room1 #73", "Pyramid top #135",
+ ]
+ # some locations that should still be unreachable
+ break_unreachable = [
+ "Aquagoth", "Megataur", "Tiny", "Rimsala",
+ "Barrier", "Call Up", "Levitate", "Stop", "Drain", "Escape",
+ "Greenhouse #275", "E. Crustacia #107", "Energy Core #285", "Vanilla Gauge #57",
+ ]
+
+ self.assertLocationReachability(reachable=break_reachable, unreachable=break_unreachable, satisfied=False)
+ self.collect_by_name("Gladiator Sword")
+ self.assertLocationReachability(reachable=break_reachable, unreachable=break_unreachable, satisfied=in_logic)
+ self.collect_by_name("Spider Claw") # Gauge now just needs non-sword
+ self.assertEqual(self.can_reach_location("Vanilla Gauge #57"), in_logic)
+ self.collect_by_name("Bronze Spear") # Escape now just needs either Megataur or Rimsala dead
+ self.assertEqual(self.can_reach_location("Escape"), in_logic)
+
+ def test_sequence_breaks_goal(self) -> None:
+ in_logic = self.options["sequence_breaks"] == "logic"
+
+ # don't need Energy Core with sequence breaks in logic
+ for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]:
+ self.assertBeatable(False)
+ self.collect_by_name(item)
+ self.assertBeatable(in_logic)
+
+
+class SequenceBreaksInLogicTest(SequenceBreaksTest):
+ """Tests that stuff that should be reachable/unreachable with sequence breaks actually is."""
+ options: typing.Dict[str, typing.Any] = {"sequence_breaks": "logic"}
diff --git a/worlds/soe/test/test_sniffamizer.py b/worlds/soe/test/test_sniffamizer.py
new file mode 100644
index 000000000000..45aff1fa4d6d
--- /dev/null
+++ b/worlds/soe/test/test_sniffamizer.py
@@ -0,0 +1,130 @@
+import typing
+from unittest import TestCase, skipUnless
+
+from . import SoETestBase
+from .. import pyevermizer
+from ..options import Sniffamizer
+
+
+class TestCount(TestCase):
+ """
+ Test that counts line up for sniff spots
+ """
+
+ def test_compare_counts(self) -> None:
+ self.assertEqual(len(pyevermizer.get_sniff_locations()), len(pyevermizer.get_sniff_items()),
+ "Sniff locations and sniff items don't line up")
+
+
+class Bases:
+ # class in class to avoid running tests for helper class
+ class TestSniffamizerLocal(SoETestBase):
+ """
+ Test that provided options do not add sniff items or locations
+ """
+ def test_no_sniff_items(self) -> None:
+ self.assertLess(len(self.multiworld.itempool), 500,
+ "Unexpected number of items")
+ for item in self.multiworld.itempool:
+ if item.code is not None:
+ self.assertLess(item.code, 65000,
+ "Unexpected item type")
+
+ def test_no_sniff_locations(self) -> None:
+ location_count = sum(1 for location in self.multiworld.get_locations(self.player) if location.item is None)
+ self.assertLess(location_count, 500,
+ "Unexpected number of locations")
+ for location in self.multiworld.get_locations(self.player):
+ if location.address is not None:
+ self.assertLess(location.address, 65000,
+ "Unexpected location type")
+ self.assertEqual(location_count, len(self.multiworld.itempool),
+ "Locations and item counts do not line up")
+
+ class TestSniffamizerPool(SoETestBase):
+ """
+ Test that provided options add sniff items and locations
+ """
+ def test_sniff_items(self) -> None:
+ self.assertGreater(len(self.multiworld.itempool), 500,
+ "Unexpected number of items")
+
+ def test_sniff_locations(self) -> None:
+ location_count = sum(1 for location in self.multiworld.get_locations(self.player) if location.item is None)
+ self.assertGreater(location_count, 500,
+ "Unexpected number of locations")
+ self.assertTrue(any(location.address is not None and location.address >= 65000
+ for location in self.multiworld.get_locations(self.player)),
+ "No sniff locations")
+ self.assertEqual(location_count, len(self.multiworld.itempool),
+ "Locations and item counts do not line up")
+
+
+class TestSniffamizerShuffle(Bases.TestSniffamizerLocal):
+ """
+ Test that shuffle does not add extra items or locations
+ """
+ options: typing.Dict[str, typing.Any] = {
+ "sniffamizer": "shuffle"
+ }
+
+ def test_flags(self) -> None:
+ # default -> no flags
+ flags = self.world.options.flags
+ self.assertNotIn("s", flags)
+ self.assertNotIn("S", flags)
+ self.assertNotIn("v", flags)
+
+
+@skipUnless(hasattr(Sniffamizer, "option_everywhere"), "Feature disabled")
+class TestSniffamizerEverywhereVanilla(Bases.TestSniffamizerPool):
+ """
+ Test that everywhere + vanilla ingredients does add extra items and locations
+ """
+ options: typing.Dict[str, typing.Any] = {
+ "sniffamizer": "everywhere",
+ "sniff_ingredients": "vanilla_ingredients",
+ }
+
+ def test_flags(self) -> None:
+ flags = self.world.options.flags
+ self.assertIn("S", flags)
+ self.assertNotIn("v", flags)
+
+
+@skipUnless(hasattr(Sniffamizer, "option_everywhere"), "Feature disabled")
+class TestSniffamizerEverywhereRandom(Bases.TestSniffamizerPool):
+ """
+ Test that everywhere + random ingredients also adds extra items and locations
+ """
+ options: typing.Dict[str, typing.Any] = {
+ "sniffamizer": "everywhere",
+ "sniff_ingredients": "random_ingredients",
+ }
+
+ def test_flags(self) -> None:
+ flags = self.world.options.flags
+ self.assertIn("S", flags)
+ self.assertIn("v", flags)
+
+
+@skipUnless(hasattr(Sniffamizer, "option_everywhere"), "Feature disabled")
+class EverywhereAccessTest(SoETestBase):
+ """
+ Test that everywhere has certain rules
+ """
+ options: typing.Dict[str, typing.Any] = {
+ "sniffamizer": "everywhere",
+ }
+
+ @staticmethod
+ def _resolve_numbers(spots: typing.Mapping[str, typing.Iterable[int]]) -> typing.List[str]:
+ return [f"{name} #{number}" for name, numbers in spots.items() for number in numbers]
+
+ def test_knight_basher(self) -> None:
+ locations = ["Mungola", "Lightning Storm"] + self._resolve_numbers({
+ "Gomi's Tower Sniff": range(473, 491),
+ "Gomi's Tower": range(195, 199),
+ })
+ items = [["Knight Basher"]]
+ self.assertAccessDependency(locations, items)
diff --git a/worlds/soe/test/test_traps.py b/worlds/soe/test/test_traps.py
new file mode 100644
index 000000000000..7babd4522b30
--- /dev/null
+++ b/worlds/soe/test/test_traps.py
@@ -0,0 +1,56 @@
+import typing
+from dataclasses import fields
+
+from . import SoETestBase
+from ..options import SoEOptions
+
+if typing.TYPE_CHECKING:
+ from .. import SoEWorld
+
+
+class Bases:
+ # class in class to avoid running tests for TrapTest class
+ class TrapTestBase(SoETestBase):
+ """Test base for trap tests"""
+ option_name_to_item_name = {
+ # filtering by name here validates that there is no confusion between name and type
+ field.name: field.type.item_name for field in fields(SoEOptions) if field.name.startswith("trap_chance_")
+ }
+
+ def test_dataclass(self) -> None:
+ """Test that the dataclass helper property returns the expected sequence"""
+ self.assertGreater(len(self.option_name_to_item_name), 0, "Expected more than 0 trap types")
+ world: "SoEWorld" = typing.cast("SoEWorld", self.multiworld.worlds[1])
+ item_name_to_rolled_option = {option.item_name: option for option in world.options.trap_chances}
+ # compare that all fields are present - that is property in dataclass and selector code in test line up
+ self.assertEqual(sorted(self.option_name_to_item_name.values()), sorted(item_name_to_rolled_option),
+ "field names probably do not match field types")
+ # sanity check that chances are correctly set and returned by property
+ for option_name, item_name in self.option_name_to_item_name.items():
+ self.assertEqual(item_name_to_rolled_option[item_name].value,
+ self.options.get(option_name, item_name_to_rolled_option[item_name].default))
+
+ def test_trap_count(self) -> None:
+ """Test that total trap count is correct"""
+ self.assertEqual(self.options["trap_count"],
+ len(self.get_items_by_name(self.option_name_to_item_name.values())))
+
+
+class TestTrapAllZeroChance(Bases.TrapTestBase):
+ """Tests all zero chances still gives traps if trap_count is set."""
+ options: typing.Dict[str, typing.Any] = {
+ "trap_count": 1,
+ **{name: 0 for name in Bases.TrapTestBase.option_name_to_item_name}
+ }
+
+
+class TestTrapNoConfound(Bases.TrapTestBase):
+ """Tests that one zero chance does not give that trap."""
+ options: typing.Dict[str, typing.Any] = {
+ "trap_count": 99,
+ "trap_chance_confound": 0,
+ }
+
+ def test_no_confound_trap(self) -> None:
+ self.assertEqual(self.option_name_to_item_name["trap_chance_confound"], "Confound Trap")
+ self.assertEqual(len(self.get_items_by_name("Confound Trap")), 0)
diff --git a/worlds/spire/Rules.py b/worlds/spire/Rules.py
index 7c8c1c0f3d86..3c6f09b34dce 100644
--- a/worlds/spire/Rules.py
+++ b/worlds/spire/Rules.py
@@ -5,11 +5,11 @@
class SpireLogic(LogicMixin):
def _spire_has_relics(self, player: int, amount: int) -> bool:
- count: int = self.item_count("Relic", player) + self.item_count("Boss Relic", player)
+ count: int = self.count("Relic", player) + self.count("Boss Relic", player)
return count >= amount
def _spire_has_cards(self, player: int, amount: int) -> bool:
- count = self.item_count("Card Draw", player) + self.item_count("Rare Card Draw", player)
+ count = self.count("Card Draw", player) + self.count("Rare Card Draw", player)
return count >= amount
diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py
index 35ef94090656..5b0e1e17f23d 100644
--- a/worlds/spire/__init__.py
+++ b/worlds/spire/__init__.py
@@ -30,7 +30,6 @@ class SpireWorld(World):
option_definitions = spire_options
game = "Slay the Spire"
topology_present = False
- data_version = 2
web = SpireWeb()
required_client_version = (0, 3, 7)
@@ -92,12 +91,6 @@ def create_region(world: MultiWorld, player: int, name: str, locations=None, exi
class SpireLocation(Location):
game: str = "Slay the Spire"
- def __init__(self, player: int, name: str, address=None, parent=None):
- super(SpireLocation, self).__init__(player, name, address, parent)
- if address is None:
- self.event = True
- self.locked = True
-
class SpireItem(Item):
game = "Slay the Spire"
diff --git a/worlds/spire/docs/en_Slay the Spire.md b/worlds/spire/docs/en_Slay the Spire.md
index f4519455fa83..4591db58dc51 100644
--- a/worlds/spire/docs/en_Slay the Spire.md
+++ b/worlds/spire/docs/en_Slay the Spire.md
@@ -1,8 +1,8 @@
# Slay the Spire (PC)
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
diff --git a/worlds/spire/docs/slay-the-spire_en.md b/worlds/spire/docs/slay-the-spire_en.md
index d85a17d987b2..daeb65415196 100644
--- a/worlds/spire/docs/slay-the-spire_en.md
+++ b/worlds/spire/docs/slay-the-spire_en.md
@@ -43,8 +43,8 @@ an experience customized for their taste, and different players in the same mult
### Where do I get a YAML file?
-you can customize your settings by visiting
-the [Slay the Spire Settings Page](/games/Slay%20the%20Spire/player-settings).
+you can customize your options by visiting
+the [Slay the Spire Options Page](/games/Slay%20the%20Spire/player-options).
### Connect to the MultiServer
diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py
index 150e3589b073..f9df8c292e37 100644
--- a/worlds/stardew_valley/__init__.py
+++ b/worlds/stardew_valley/__init__.py
@@ -1,37 +1,54 @@
import logging
-from typing import Dict, Any, Iterable, Optional, Union, Set
+from random import Random
+from typing import Dict, Any, Iterable, Optional, Union, List, TextIO
-from BaseClasses import Region, Entrance, Location, Item, Tutorial, CollectionState, ItemClassification, MultiWorld
+from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
+from Options import PerGameCommonOptions
from worlds.AutoWorld import World, WebWorld
-from . import rules, logic, options
-from .bundles import get_all_bundles, Bundle
-from .items import item_table, create_items, ItemData, Group, items_by_group
-from .locations import location_table, create_locations, LocationData
-from .logic import StardewLogic, StardewRule, True_, MAX_MONTHS
-from .options import stardew_valley_options, StardewOptions, fetch_options
+from . import rules
+from .bundles.bundle_room import BundleRoom
+from .bundles.bundles import get_all_bundles
+from .content import content_packs, StardewContent, unpack_content, create_content
+from .early_items import setup_early_items
+from .items import item_table, create_items, ItemData, Group, items_by_group, get_all_filler_items, remove_limited_amount_packs
+from .locations import location_table, create_locations, LocationData, locations_by_tag
+from .logic.bundle_logic import BundleLogic
+from .logic.logic import StardewLogic
+from .logic.time_logic import MAX_MONTHS
+from .option_groups import sv_option_groups
+from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, EnabledFillerBuffs, NumberOfMovementBuffs, \
+ BackpackProgression, BuildingProgression, ExcludeGingerIsland, TrapItems, EntranceRandomization, FarmType, Walnutsanity
+from .presets import sv_options_presets
from .regions import create_regions
from .rules import set_rules
-from worlds.generic.Rules import set_rule
-from .strings.goal_names import Goal
+from .stardew_rule import True_, StardewRule, HasProgressionPercent, true_
+from .strings.ap_names.event_names import Event
+from .strings.entrance_names import Entrance as EntranceName
+from .strings.goal_names import Goal as GoalName
+from .strings.metal_names import Ore
+from .strings.region_names import Region as RegionName, LogicRegion
+
+logger = logging.getLogger(__name__)
+
+STARDEW_VALLEY = "Stardew Valley"
+UNIVERSAL_TRACKER_SEED_PROPERTY = "ut_seed"
client_version = 0
class StardewLocation(Location):
- game: str = "Stardew Valley"
-
- def __init__(self, player: int, name: str, address: Optional[int], parent=None):
- super().__init__(player, name, address, parent)
- self.event = not address
+ game: str = STARDEW_VALLEY
class StardewItem(Item):
- game: str = "Stardew Valley"
+ game: str = STARDEW_VALLEY
class StardewWebWorld(WebWorld):
theme = "dirt"
bug_report_page = "https://github.com/agilbert1412/StardewArchipelago/issues/new?labels=bug&title=%5BBug%5D%3A+Brief+Description+of+bug+here"
+ options_presets = sv_options_presets
+ option_groups = sv_option_groups
tutorials = [
Tutorial(
@@ -49,48 +66,72 @@ class StardewValleyWorld(World):
Stardew Valley is an open-ended country-life RPG. You can farm, fish, mine, fight, complete quests,
befriend villagers, and uncover dark secrets.
"""
- game = "Stardew Valley"
- option_definitions = stardew_valley_options
+ game = STARDEW_VALLEY
topology_present = False
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {name: data.code for name, data in location_table.items()}
- data_version = 3
+ item_name_groups = {
+ group.name.replace("_", " ").title() + (" Group" if group.name.replace("_", " ").title() in item_table else ""):
+ [item.name for item in items] for group, items in items_by_group.items()
+ }
+ location_name_groups = {
+ group.name.replace("_", " ").title() + (" Group" if group.name.replace("_", " ").title() in locations_by_tag else ""):
+ [location.name for location in locations] for group, locations in locations_by_tag.items()
+ }
+
required_client_version = (0, 4, 0)
- options: StardewOptions
+ options_dataclass = StardewValleyOptions
+ options: StardewValleyOptions
+ content: StardewContent
logic: StardewLogic
web = StardewWebWorld()
- modified_bundles: Dict[str, Bundle]
+ modified_bundles: List[BundleRoom]
randomized_entrances: Dict[str, str]
- all_progression_items: Set[str]
+ total_progression_items: int
- def __init__(self, world: MultiWorld, player: int):
- super().__init__(world, player)
- self.all_progression_items = set()
+ # all_progression_items: Dict[str, int] # If you need to debug total_progression_items, uncommenting this will help tremendously
+
+ def __init__(self, multiworld: MultiWorld, player: int):
+ super().__init__(multiworld, player)
+ self.filler_item_pool_names = []
+ self.total_progression_items = 0
+ # self.all_progression_items = dict()
+
+ # Taking the seed specified in slot data for UT, otherwise just generating the seed.
+ self.seed = getattr(multiworld, "re_gen_passthrough", {}).get(STARDEW_VALLEY, self.random.getrandbits(64))
+ self.random = Random(self.seed)
+
+ def interpret_slot_data(self, slot_data: Dict[str, Any]) -> Optional[int]:
+ # If the seed is not specified in the slot data, this mean the world was generated before Universal Tracker support.
+ seed = slot_data.get(UNIVERSAL_TRACKER_SEED_PROPERTY)
+ if seed is None:
+ logger.warning(f"World was generated before Universal Tracker support. Tracker might not be accurate.")
+ return seed
def generate_early(self):
- self.options = fetch_options(self.multiworld, self.player)
self.force_change_options_if_incompatible()
-
- self.logic = StardewLogic(self.player, self.options)
- self.modified_bundles = get_all_bundles(self.multiworld.random,
- self.logic,
- self.options[options.BundleRandomization],
- self.options[options.BundlePrice])
+ self.content = create_content(self.options)
def force_change_options_if_incompatible(self):
- goal_is_walnut_hunter = self.options[options.Goal] == options.Goal.option_greatest_walnut_hunter
- goal_is_perfection = self.options[options.Goal] == options.Goal.option_perfection
+ goal_is_walnut_hunter = self.options.goal == Goal.option_greatest_walnut_hunter
+ goal_is_perfection = self.options.goal == Goal.option_perfection
goal_is_island_related = goal_is_walnut_hunter or goal_is_perfection
- exclude_ginger_island = self.options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true
+ exclude_ginger_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
if goal_is_island_related and exclude_ginger_island:
- self.options[options.ExcludeGingerIsland] = options.ExcludeGingerIsland.option_false
- goal = options.Goal.name_lookup[self.options[options.Goal]]
+ self.options.exclude_ginger_island.value = ExcludeGingerIsland.option_false
+ goal_name = self.options.goal.current_key
+ player_name = self.multiworld.player_name[self.player]
+ logger.warning(
+ f"Goal '{goal_name}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({player_name})")
+ if exclude_ginger_island and self.options.walnutsanity != Walnutsanity.preset_none:
+ self.options.walnutsanity.value = Walnutsanity.preset_none
player_name = self.multiworld.player_name[self.player]
- logging.warning(f"Goal '{goal}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({player_name})")
+ logger.warning(
+ f"Walnutsanity requires Ginger Island. Ginger Island was excluded from {self.player} ({player_name})'s world, so walnutsanity was force disabled")
def create_regions(self):
def create_region(name: str, exits: Iterable[str]) -> Region:
@@ -98,128 +139,203 @@ def create_region(name: str, exits: Iterable[str]) -> Region:
region.exits = [Entrance(self.player, exit_name, region) for exit_name in exits]
return region
- world_regions, self.randomized_entrances = create_regions(create_region, self.multiworld.random, self.options)
- self.multiworld.regions.extend(world_regions)
+ world_regions, world_entrances, self.randomized_entrances = create_regions(create_region, self.random, self.options)
+
+ self.logic = StardewLogic(self.player, self.options, self.content, world_regions.keys())
+ self.modified_bundles = get_all_bundles(self.random,
+ self.logic,
+ self.content,
+ self.options)
def add_location(name: str, code: Optional[int], region: str):
- region = self.multiworld.get_region(region, self.player)
+ region = world_regions[region]
location = StardewLocation(self.player, name, code, region)
- location.access_rule = lambda _: True
region.locations.append(location)
- create_locations(add_location, self.options, self.multiworld.random)
+ create_locations(add_location, self.modified_bundles, self.options, self.content, self.random)
+ self.multiworld.regions.extend(world_regions.values())
def create_items(self):
self.precollect_starting_season()
+ self.precollect_farm_type_items()
items_to_exclude = [excluded_items
for excluded_items in self.multiworld.precollected_items[self.player]
if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK,
Group.FRIENDSHIP_PACK)]
- if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled:
+ if self.options.season_randomization == SeasonRandomization.option_disabled:
items_to_exclude = [item for item in items_to_exclude
if item_table[item.name] not in items_by_group[Group.SEASON]]
locations_count = len([location
for location in self.multiworld.get_locations(self.player)
- if not location.event])
+ if location.address is not None])
- created_items = create_items(self.create_item, locations_count, items_to_exclude, self.options,
- self.multiworld.random)
+ created_items = create_items(self.create_item, self.delete_item, locations_count, items_to_exclude, self.options, self.content,
+ self.random)
self.multiworld.itempool += created_items
- self.setup_early_items()
- self.setup_month_events()
+ setup_early_items(self.multiworld, self.options, self.player, self.random)
+ self.setup_player_events()
self.setup_victory()
- def precollect_starting_season(self) -> Optional[StardewItem]:
- if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive:
+ def precollect_starting_season(self):
+ if self.options.season_randomization == SeasonRandomization.option_progressive:
return
season_pool = items_by_group[Group.SEASON]
- if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled:
+ if self.options.season_randomization == SeasonRandomization.option_disabled:
for season in season_pool:
- self.multiworld.push_precollected(self.create_item(season))
+ self.multiworld.push_precollected(self.create_starting_item(season))
return
if [item for item in self.multiworld.precollected_items[self.player]
if item.name in {season.name for season in items_by_group[Group.SEASON]}]:
return
- if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_randomized_not_winter:
+ if self.options.season_randomization == SeasonRandomization.option_randomized_not_winter:
season_pool = [season for season in season_pool if season.name != "Winter"]
- starting_season = self.create_item(self.multiworld.random.choice(season_pool))
+ starting_season = self.create_starting_item(self.random.choice(season_pool))
self.multiworld.push_precollected(starting_season)
- def setup_early_items(self):
- if (self.options[options.BuildingProgression] ==
- options.BuildingProgression.option_progressive_early_shipping_bin):
- self.multiworld.early_items[self.player]["Shipping Bin"] = 1
+ def precollect_farm_type_items(self):
+ if self.options.farm_type == FarmType.option_meadowlands and self.options.building_progression & BuildingProgression.option_progressive:
+ self.multiworld.push_precollected(self.create_starting_item("Progressive Coop"))
+
+ def setup_player_events(self):
+ self.setup_construction_events()
+ self.setup_quest_events()
+ self.setup_action_events()
+ self.setup_logic_events()
+
+ def setup_construction_events(self):
+ can_construct_buildings = LocationData(None, RegionName.carpenter, Event.can_construct_buildings)
+ self.create_event_location(can_construct_buildings, True_(), Event.can_construct_buildings)
+
+ def setup_quest_events(self):
+ start_dark_talisman_quest = LocationData(None, RegionName.railroad, Event.start_dark_talisman_quest)
+ self.create_event_location(start_dark_talisman_quest, self.logic.wallet.has_rusty_key(), Event.start_dark_talisman_quest)
+
+ def setup_action_events(self):
+ can_ship_event = LocationData(None, LogicRegion.shipping, Event.can_ship_items)
+ self.create_event_location(can_ship_event, true_, Event.can_ship_items)
+ can_shop_pierre_event = LocationData(None, RegionName.pierre_store, Event.can_shop_at_pierre)
+ self.create_event_location(can_shop_pierre_event, true_, Event.can_shop_at_pierre)
+
+ spring_farming = LocationData(None, LogicRegion.spring_farming, Event.spring_farming)
+ self.create_event_location(spring_farming, true_, Event.spring_farming)
+ summer_farming = LocationData(None, LogicRegion.summer_farming, Event.summer_farming)
+ self.create_event_location(summer_farming, true_, Event.summer_farming)
+ fall_farming = LocationData(None, LogicRegion.fall_farming, Event.fall_farming)
+ self.create_event_location(fall_farming, true_, Event.fall_farming)
+ winter_farming = LocationData(None, LogicRegion.winter_farming, Event.winter_farming)
+ self.create_event_location(winter_farming, true_, Event.winter_farming)
+
+ def setup_logic_events(self):
+ def register_event(name: str, region: str, rule: StardewRule):
+ event_location = LocationData(None, region, name)
+ self.create_event_location(event_location, rule, name)
+
+ self.logic.setup_events(register_event)
+
+ def setup_victory(self):
+ if self.options.goal == Goal.option_community_center:
+ self.create_event_location(location_table[GoalName.community_center],
+ self.logic.bundle.can_complete_community_center,
+ Event.victory)
+ elif self.options.goal == Goal.option_grandpa_evaluation:
+ self.create_event_location(location_table[GoalName.grandpa_evaluation],
+ self.logic.can_finish_grandpa_evaluation(),
+ Event.victory)
+ elif self.options.goal == Goal.option_bottom_of_the_mines:
+ self.create_event_location(location_table[GoalName.bottom_of_the_mines],
+ True_(),
+ Event.victory)
+ elif self.options.goal == Goal.option_cryptic_note:
+ self.create_event_location(location_table[GoalName.cryptic_note],
+ self.logic.quest.can_complete_quest("Cryptic Note"),
+ Event.victory)
+ elif self.options.goal == Goal.option_master_angler:
+ self.create_event_location(location_table[GoalName.master_angler],
+ self.logic.fishing.can_catch_every_fish_for_fishsanity(),
+ Event.victory)
+ elif self.options.goal == Goal.option_complete_collection:
+ self.create_event_location(location_table[GoalName.complete_museum],
+ self.logic.museum.can_complete_museum(),
+ Event.victory)
+ elif self.options.goal == Goal.option_full_house:
+ self.create_event_location(location_table[GoalName.full_house],
+ (self.logic.relationship.has_children(2) & self.logic.relationship.can_reproduce()),
+ Event.victory)
+ elif self.options.goal == Goal.option_greatest_walnut_hunter:
+ self.create_event_location(location_table[GoalName.greatest_walnut_hunter],
+ self.logic.walnut.has_walnut(130),
+ Event.victory)
+ elif self.options.goal == Goal.option_protector_of_the_valley:
+ self.create_event_location(location_table[GoalName.protector_of_the_valley],
+ self.logic.monster.can_complete_all_monster_slaying_goals(),
+ Event.victory)
+ elif self.options.goal == Goal.option_full_shipment:
+ self.create_event_location(location_table[GoalName.full_shipment],
+ self.logic.shipping.can_ship_everything_in_slot(self.get_all_location_names()),
+ Event.victory)
+ elif self.options.goal == Goal.option_gourmet_chef:
+ self.create_event_location(location_table[GoalName.gourmet_chef],
+ self.logic.cooking.can_cook_everything,
+ Event.victory)
+ elif self.options.goal == Goal.option_craft_master:
+ self.create_event_location(location_table[GoalName.craft_master],
+ self.logic.crafting.can_craft_everything,
+ Event.victory)
+ elif self.options.goal == Goal.option_legend:
+ self.create_event_location(location_table[GoalName.legend],
+ self.logic.money.can_have_earned_total(10_000_000),
+ Event.victory)
+ elif self.options.goal == Goal.option_mystery_of_the_stardrops:
+ self.create_event_location(location_table[GoalName.mystery_of_the_stardrops],
+ self.logic.has_all_stardrops(),
+ Event.victory)
+ elif self.options.goal == Goal.option_allsanity:
+ self.create_event_location(location_table[GoalName.allsanity],
+ HasProgressionPercent(self.player, 100),
+ Event.victory)
+ elif self.options.goal == Goal.option_perfection:
+ self.create_event_location(location_table[GoalName.perfection],
+ HasProgressionPercent(self.player, 100),
+ Event.victory)
+
+ self.multiworld.completion_condition[self.player] = lambda state: state.has(Event.victory, self.player)
+
+ def get_all_location_names(self) -> List[str]:
+ return list(location.name for location in self.multiworld.get_locations(self.player))
+
+ def create_item(self, item: Union[str, ItemData], override_classification: ItemClassification = None) -> StardewItem:
+ if isinstance(item, str):
+ item = item_table[item]
- if self.options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive:
- self.multiworld.early_items[self.player]["Progressive Backpack"] = 1
+ if override_classification is None:
+ override_classification = item.classification
- def setup_month_events(self):
- for i in range(0, MAX_MONTHS):
- month_end = LocationData(None, "Stardew Valley", f"Month End {i + 1}")
- if i == 0:
- self.create_event_location(month_end, True_(), "Month End")
- continue
+ if override_classification == ItemClassification.progression:
+ self.total_progression_items += 1
+ return StardewItem(item.name, override_classification, item.code, self.player)
- self.create_event_location(month_end, self.logic.received("Month End", i).simplify(), "Month End")
+ def delete_item(self, item: Item):
+ if item.classification & ItemClassification.progression:
+ self.total_progression_items -= 1
- def setup_victory(self):
- if self.options[options.Goal] == options.Goal.option_community_center:
- self.create_event_location(location_table[Goal.community_center],
- self.logic.can_complete_community_center().simplify(),
- "Victory")
- elif self.options[options.Goal] == options.Goal.option_grandpa_evaluation:
- self.create_event_location(location_table[Goal.grandpa_evaluation],
- self.logic.can_finish_grandpa_evaluation().simplify(),
- "Victory")
- elif self.options[options.Goal] == options.Goal.option_bottom_of_the_mines:
- self.create_event_location(location_table[Goal.bottom_of_the_mines],
- self.logic.can_mine_to_floor(120).simplify(),
- "Victory")
- elif self.options[options.Goal] == options.Goal.option_cryptic_note:
- self.create_event_location(location_table[Goal.cryptic_note],
- self.logic.can_complete_quest("Cryptic Note").simplify(),
- "Victory")
- elif self.options[options.Goal] == options.Goal.option_master_angler:
- self.create_event_location(location_table[Goal.master_angler],
- self.logic.can_catch_every_fish().simplify(),
- "Victory")
- elif self.options[options.Goal] == options.Goal.option_complete_collection:
- self.create_event_location(location_table[Goal.complete_museum],
- self.logic.can_complete_museum().simplify(),
- "Victory")
- elif self.options[options.Goal] == options.Goal.option_full_house:
- self.create_event_location(location_table[Goal.full_house],
- (self.logic.has_children(2) & self.logic.can_reproduce()).simplify(),
- "Victory")
- elif self.options[options.Goal] == options.Goal.option_greatest_walnut_hunter:
- self.create_event_location(location_table[Goal.greatest_walnut_hunter],
- self.logic.has_walnut(130).simplify(),
- "Victory")
- elif self.options[options.Goal] == options.Goal.option_perfection:
- self.create_event_location(location_table[Goal.perfection],
- self.logic.has_everything(self.all_progression_items).simplify(),
- "Victory")
-
- self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
-
- def create_item(self, item: Union[str, ItemData]) -> StardewItem:
+ def create_starting_item(self, item: Union[str, ItemData]) -> StardewItem:
if isinstance(item, str):
item = item_table[item]
- if item.classification == ItemClassification.progression:
- self.all_progression_items.add(item.name)
return StardewItem(item.name, item.classification, item.code, self.player)
- def create_event_location(self, location_data: LocationData, rule: StardewRule, item: Optional[str] = None):
+ def create_event_location(self, location_data: LocationData, rule: StardewRule = None, item: Optional[str] = None):
+ if rule is None:
+ rule = True_()
if item is None:
item = location_data.name
@@ -227,65 +343,120 @@ def create_event_location(self, location_data: LocationData, rule: StardewRule,
location = StardewLocation(self.player, location_data.name, None, region)
location.access_rule = rule
region.locations.append(location)
- location.place_locked_item(self.create_item(item))
-
- def set_rules(self):
- set_rules(self.multiworld, self.player, self.options, self.logic, self.modified_bundles)
- self.force_first_month_once_all_early_items_are_found()
-
- def force_first_month_once_all_early_items_are_found(self):
- """
- The Fill algorithm sweeps all event when calculating the early location. This causes an issue where
- location only locked behind event are considered early, which they are not really...
-
- This patches the issue, by adding a dependency to the first month end on all early items, so all the locations
- that depends on it will not be considered early. This requires at least one early item to be progression, or
- it just won't work...
- """
-
- early_items = []
- for player, item_count in self.multiworld.early_items.items():
- for item, count in item_count.items():
- if self.multiworld.worlds[player].create_item(item).advancement:
- early_items.append((player, item, count))
-
- for item, count in self.multiworld.local_early_items[self.player].items():
- if self.create_item(item).advancement:
- early_items.append((self.player, item, count))
+ location.place_locked_item(StardewItem(item, ItemClassification.progression, None, self.player))
- def first_month_require_all_early_items(state: CollectionState) -> bool:
- for player, item, count in early_items:
- if not state.has(item, player, count):
- return False
+ # This is not ideal, but the rule count them so...
+ if item != Event.victory:
+ self.total_progression_items += 1
- return True
-
- first_month_end = self.multiworld.get_location("Month End 1", self.player)
- set_rule(first_month_end, first_month_require_all_early_items)
+ def set_rules(self):
+ set_rules(self)
def generate_basic(self):
pass
def get_filler_item_name(self) -> str:
- return "Joja Cola"
+ if not self.filler_item_pool_names:
+ self.generate_filler_item_pool_names()
+ return self.random.choice(self.filler_item_pool_names)
+
+ def generate_filler_item_pool_names(self):
+ include_traps, exclude_island = self.get_filler_item_rules()
+ available_filler = get_all_filler_items(include_traps, exclude_island)
+ available_filler = remove_limited_amount_packs(available_filler)
+ self.filler_item_pool_names = [item.name for item in available_filler]
+
+ def get_filler_item_rules(self):
+ if self.player in self.multiworld.groups:
+ link_group = self.multiworld.groups[self.player]
+ include_traps = True
+ exclude_island = False
+ for player in link_group["players"]:
+ player_options = self.multiworld.worlds[player].options
+ if self.multiworld.game[player] != self.game:
+ continue
+ if player_options.trap_items == TrapItems.option_no_traps:
+ include_traps = False
+ if player_options.exclude_ginger_island == ExcludeGingerIsland.option_true:
+ exclude_island = True
+ return include_traps, exclude_island
+ else:
+ return self.options.trap_items != TrapItems.option_no_traps, self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
+
+ def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
+ """Write to the spoiler header. If individual it's right at the end of that player's options,
+ if as stage it's right under the common header before per-player options."""
+ self.add_entrances_to_spoiler_log()
+
+ def write_spoiler(self, spoiler_handle: TextIO) -> None:
+ """Write to the spoiler "middle", this is after the per-player options and before locations,
+ meant for useful or interesting info."""
+ self.add_bundles_to_spoiler_log(spoiler_handle)
+
+ def add_bundles_to_spoiler_log(self, spoiler_handle: TextIO):
+ if self.options.bundle_randomization == BundleRandomization.option_vanilla:
+ return
+ player_name = self.multiworld.get_player_name(self.player)
+ spoiler_handle.write(f"\n\nCommunity Center ({player_name}):\n")
+ for room in self.modified_bundles:
+ for bundle in room.bundles:
+ spoiler_handle.write(f"\t[{room.name}] {bundle.name} ({bundle.number_required} required):\n")
+ for i, item in enumerate(bundle.items):
+ if "Basic" in item.quality:
+ quality = ""
+ else:
+ quality = f" ({item.quality.split(' ')[0]})"
+ spoiler_handle.write(f"\t\t{item.amount}x {item.get_item()}{quality}\n")
+
+ def add_entrances_to_spoiler_log(self):
+ if self.options.entrance_randomization == EntranceRandomization.option_disabled:
+ return
+ for original_entrance, replaced_entrance in self.randomized_entrances.items():
+ self.multiworld.spoiler.set_entrance(original_entrance, replaced_entrance, "entrance", self.player)
def fill_slot_data(self) -> Dict[str, Any]:
-
- modified_bundles = {}
- for bundle_key in self.modified_bundles:
- key, value = self.modified_bundles[bundle_key].to_pair()
- modified_bundles[key] = value
-
- excluded_options = [options.BundleRandomization, options.BundlePrice,
- options.NumberOfMovementBuffs, options.NumberOfLuckBuffs]
- slot_data = dict(self.options.options)
- for option in excluded_options:
- slot_data.pop(option.internal_name)
+ bundles = dict()
+ for room in self.modified_bundles:
+ bundles[room.name] = dict()
+ for bundle in room.bundles:
+ bundles[room.name][bundle.name] = {"number_required": bundle.number_required}
+ for i, item in enumerate(bundle.items):
+ bundles[room.name][bundle.name][i] = f"{item.get_item()}|{item.amount}|{item.quality}"
+
+ excluded_options = [BundleRandomization, NumberOfMovementBuffs, EnabledFillerBuffs]
+ excluded_option_names = [option.internal_name for option in excluded_options]
+ generic_option_names = [option_name for option_name in PerGameCommonOptions.type_hints]
+ excluded_option_names.extend(generic_option_names)
+ included_option_names: List[str] = [option_name for option_name in self.options_dataclass.type_hints if option_name not in excluded_option_names]
+ slot_data = self.options.as_dict(*included_option_names)
slot_data.update({
- "seed": self.multiworld.per_slot_randoms[self.player].randrange(1000000000), # Seed should be max 9 digits
+ UNIVERSAL_TRACKER_SEED_PROPERTY: self.seed,
+ "seed": self.random.randrange(1000000000), # Seed should be max 9 digits
"randomized_entrances": self.randomized_entrances,
- "modified_bundles": modified_bundles,
- "client_version": "4.0.0",
+ "modified_bundles": bundles,
+ "client_version": "6.0.0",
})
return slot_data
+
+ def collect(self, state: CollectionState, item: StardewItem) -> bool:
+ change = super().collect(state, item)
+ if change:
+ state.prog_items[self.player][Event.received_walnuts] += self.get_walnut_amount(item.name)
+ return change
+
+ def remove(self, state: CollectionState, item: StardewItem) -> bool:
+ change = super().remove(state, item)
+ if change:
+ state.prog_items[self.player][Event.received_walnuts] -= self.get_walnut_amount(item.name)
+ return change
+
+ @staticmethod
+ def get_walnut_amount(item_name: str) -> int:
+ if item_name == "Golden Walnut":
+ return 1
+ if item_name == "3 Golden Walnuts":
+ return 3
+ if item_name == "5 Golden Walnuts":
+ return 5
+ return 0
diff --git a/worlds/stardew_valley/bundles.py b/worlds/stardew_valley/bundles.py
deleted file mode 100644
index 7cbb13923705..000000000000
--- a/worlds/stardew_valley/bundles.py
+++ /dev/null
@@ -1,254 +0,0 @@
-from random import Random
-from typing import List, Dict, Union
-
-from .data.bundle_data import *
-from .logic import StardewLogic
-from .options import BundleRandomization, BundlePrice
-
-vanilla_bundles = {
- "Pantry/0": "Spring Crops/O 465 20/24 1 0 188 1 0 190 1 0 192 1 0/0",
- "Pantry/1": "Summer Crops/O 621 1/256 1 0 260 1 0 258 1 0 254 1 0/3",
- "Pantry/2": "Fall Crops/BO 10 1/270 1 0 272 1 0 276 1 0 280 1 0/2",
- "Pantry/3": "Quality Crops/BO 15 1/24 5 2 254 5 2 276 5 2 270 5 2/6/3",
- "Pantry/4": "Animal/BO 16 1/186 1 0 182 1 0 174 1 0 438 1 0 440 1 0 442 1 0/4/5",
- # 639 1 0 640 1 0 641 1 0 642 1 0 643 1 0
- "Pantry/5": "Artisan/BO 12 1/432 1 0 428 1 0 426 1 0 424 1 0 340 1 0 344 1 0 613 1 0 634 1 0 635 1 0 636 1 0 637 1 0 638 1 0/1/6",
- "Crafts Room/13": "Spring Foraging/O 495 30/16 1 0 18 1 0 20 1 0 22 1 0/0",
- "Crafts Room/14": "Summer Foraging/O 496 30/396 1 0 398 1 0 402 1 0/3",
- "Crafts Room/15": "Fall Foraging/O 497 30/404 1 0 406 1 0 408 1 0 410 1 0/2",
- "Crafts Room/16": "Winter Foraging/O 498 30/412 1 0 414 1 0 416 1 0 418 1 0/6",
- "Crafts Room/17": "Construction/BO 114 1/388 99 0 388 99 0 390 99 0 709 10 0/4",
- "Crafts Room/19": "Exotic Foraging/O 235 5/88 1 0 90 1 0 78 1 0 420 1 0 422 1 0 724 1 0 725 1 0 726 1 0 257 1 0/1/5",
- "Fish Tank/6": "River Fish/O 685 30/145 1 0 143 1 0 706 1 0 699 1 0/6",
- "Fish Tank/7": "Lake Fish/O 687 1/136 1 0 142 1 0 700 1 0 698 1 0/0",
- "Fish Tank/8": "Ocean Fish/O 690 5/131 1 0 130 1 0 150 1 0 701 1 0/5",
- "Fish Tank/9": "Night Fishing/R 516 1/140 1 0 132 1 0 148 1 0/1",
- "Fish Tank/10": "Specialty Fish/O 242 5/128 1 0 156 1 0 164 1 0 734 1 0/4",
- "Fish Tank/11": "Crab Pot/O 710 3/715 1 0 716 1 0 717 1 0 718 1 0 719 1 0 720 1 0 721 1 0 722 1 0 723 1 0 372 1 0/1/5",
- "Boiler Room/20": "Blacksmith's/BO 13 1/334 1 0 335 1 0 336 1 0/2",
- "Boiler Room/21": "Geologist's/O 749 5/80 1 0 86 1 0 84 1 0 82 1 0/1",
- "Boiler Room/22": "Adventurer's/R 518 1/766 99 0 767 10 0 768 1 0 769 1 0/1/2",
- "Vault/23": "2,500g/O 220 3/-1 2500 2500/4",
- "Vault/24": "5,000g/O 369 30/-1 5000 5000/2",
- "Vault/25": "10,000g/BO 9 1/-1 10000 10000/3",
- "Vault/26": "25,000g/BO 21 1/-1 25000 25000/1",
- "Bulletin Board/31": "Chef's/O 221 3/724 1 0 259 1 0 430 1 0 376 1 0 228 1 0 194 1 0/4",
- "Bulletin Board/32": "Field Research/BO 20 1/422 1 0 392 1 0 702 1 0 536 1 0/5",
- "Bulletin Board/33": "Enchanter's/O 336 5/725 1 0 348 1 0 446 1 0 637 1 0/1",
- "Bulletin Board/34": "Dye/BO 25 1/420 1 0 397 1 0 421 1 0 444 1 0 62 1 0 266 1 0/6",
- "Bulletin Board/35": "Fodder/BO 104 1/262 10 0 178 10 0 613 3 0/3",
- # "Abandoned Joja Mart/36": "The Missing//348 1 1 807 1 0 74 1 0 454 5 2 795 1 2 445 1 0/1/5"
-}
-
-
-class Bundle:
- room: str
- sprite: str
- original_name: str
- name: str
- rewards: List[str]
- requirements: List[BundleItem]
- color: str
- number_required: int
-
- def __init__(self, key: str, value: str):
- key_parts = key.split("/")
- self.room = key_parts[0]
- self.sprite = key_parts[1]
-
- value_parts = value.split("/")
- self.original_name = value_parts[0]
- self.name = value_parts[0]
- self.rewards = self.parse_stardew_objects(value_parts[1])
- self.requirements = self.parse_stardew_bundle_items(value_parts[2])
- self.color = value_parts[3]
- if len(value_parts) > 4:
- self.number_required = int(value_parts[4])
- else:
- self.number_required = len(self.requirements)
-
- def __repr__(self):
- return f"{self.original_name} -> {repr(self.requirements)}"
-
- def get_name_with_bundle(self) -> str:
- return f"{self.original_name} Bundle"
-
- def to_pair(self) -> (str, str):
- key = f"{self.room}/{self.sprite}"
- str_rewards = ""
- for reward in self.rewards:
- str_rewards += f" {reward}"
- str_rewards = str_rewards.strip()
- str_requirements = ""
- for requirement in self.requirements:
- str_requirements += f" {requirement.item.item_id} {requirement.amount} {requirement.quality}"
- str_requirements = str_requirements.strip()
- value = f"{self.name}/{str_rewards}/{str_requirements}/{self.color}/{self.number_required}"
- return key, value
-
- def remove_rewards(self):
- self.rewards = []
-
- def change_number_required(self, difference: int):
- self.number_required = min(len(self.requirements), max(1, self.number_required + difference))
- if len(self.requirements) == 1 and self.requirements[0].item.item_id == -1:
- one_fifth = self.requirements[0].amount / 5
- new_amount = int(self.requirements[0].amount + (difference * one_fifth))
- self.requirements[0] = BundleItem.money_bundle(new_amount)
- thousand_amount = int(new_amount / 1000)
- dollar_amount = str(new_amount % 1000)
- while len(dollar_amount) < 3:
- dollar_amount = f"0{dollar_amount}"
- self.name = f"{thousand_amount},{dollar_amount}g"
-
- def randomize_requirements(self, random: Random,
- potential_requirements: Union[List[BundleItem], List[List[BundleItem]]]):
- if not potential_requirements:
- return
-
- number_to_generate = len(self.requirements)
- self.requirements.clear()
- if number_to_generate > len(potential_requirements):
- choices: Union[BundleItem, List[BundleItem]] = random.choices(potential_requirements, k=number_to_generate)
- else:
- choices: Union[BundleItem, List[BundleItem]] = random.sample(potential_requirements, number_to_generate)
- for choice in choices:
- if isinstance(choice, BundleItem):
- self.requirements.append(choice)
- else:
- self.requirements.append(random.choice(choice))
-
- def assign_requirements(self, new_requirements: List[BundleItem]) -> List[BundleItem]:
- number_to_generate = len(self.requirements)
- self.requirements.clear()
- for requirement in new_requirements:
- self.requirements.append(requirement)
- if len(self.requirements) >= number_to_generate:
- return new_requirements[number_to_generate:]
-
- @staticmethod
- def parse_stardew_objects(string_objects: str) -> List[str]:
- objects = []
- if len(string_objects) < 5:
- return objects
- rewards_parts = string_objects.split(" ")
- for index in range(0, len(rewards_parts), 3):
- objects.append(f"{rewards_parts[index]} {rewards_parts[index + 1]} {rewards_parts[index + 2]}")
- return objects
-
- @staticmethod
- def parse_stardew_bundle_items(string_objects: str) -> List[BundleItem]:
- bundle_items = []
- parts = string_objects.split(" ")
- for index in range(0, len(parts), 3):
- item_id = int(parts[index])
- bundle_item = BundleItem(all_bundle_items_by_id[item_id].item,
- int(parts[index + 1]),
- int(parts[index + 2]))
- bundle_items.append(bundle_item)
- return bundle_items
-
- # Shuffling the Vault doesn't really work with the stardew system in place
- # shuffle_vault_amongst_themselves(random, bundles)
-
-
-def get_all_bundles(random: Random, logic: StardewLogic, randomization: int, price: int) -> Dict[str, Bundle]:
- bundles = {}
- for bundle_key in vanilla_bundles:
- bundle_value = vanilla_bundles[bundle_key]
- bundle = Bundle(bundle_key, bundle_value)
- bundles[bundle.get_name_with_bundle()] = bundle
-
- if randomization == BundleRandomization.option_thematic:
- shuffle_bundles_thematically(random, bundles)
- elif randomization == BundleRandomization.option_shuffled:
- shuffle_bundles_completely(random, logic, bundles)
-
- price_difference = 0
- if price == BundlePrice.option_very_cheap:
- price_difference = -2
- elif price == BundlePrice.option_cheap:
- price_difference = -1
- elif price == BundlePrice.option_expensive:
- price_difference = 1
-
- for bundle_key in bundles:
- bundles[bundle_key].remove_rewards()
- bundles[bundle_key].change_number_required(price_difference)
-
- return bundles
-
-
-def shuffle_bundles_completely(random: Random, logic: StardewLogic, bundles: Dict[str, Bundle]):
- total_required_item_number = sum(len(bundle.requirements) for bundle in bundles.values())
- quality_crops_items_set = set(quality_crops_items)
- all_bundle_items_without_quality_and_money = [item
- for item in all_bundle_items_except_money
- if item not in quality_crops_items_set] + \
- random.sample(quality_crops_items, 10)
- choices = random.sample(all_bundle_items_without_quality_and_money, total_required_item_number - 4)
-
- items_sorted = sorted(choices, key=lambda x: logic.item_rules[x.item.name].get_difficulty())
-
- keys = sorted(bundles.keys())
- random.shuffle(keys)
-
- for key in keys:
- if not bundles[key].original_name.endswith("00g"):
- items_sorted = bundles[key].assign_requirements(items_sorted)
-
-
-def shuffle_bundles_thematically(random: Random, bundles: Dict[str, Bundle]):
- shuffle_crafts_room_bundle_thematically(random, bundles)
- shuffle_pantry_bundle_thematically(random, bundles)
- shuffle_fish_tank_thematically(random, bundles)
- shuffle_boiler_room_thematically(random, bundles)
- shuffle_bulletin_board_thematically(random, bundles)
-
-
-def shuffle_crafts_room_bundle_thematically(random: Random, bundles: Dict[str, Bundle]):
- bundles["Spring Foraging Bundle"].randomize_requirements(random, spring_foraging_items)
- bundles["Summer Foraging Bundle"].randomize_requirements(random, summer_foraging_items)
- bundles["Fall Foraging Bundle"].randomize_requirements(random, fall_foraging_items)
- bundles["Winter Foraging Bundle"].randomize_requirements(random, winter_foraging_items)
- bundles["Exotic Foraging Bundle"].randomize_requirements(random, exotic_foraging_items)
- bundles["Construction Bundle"].randomize_requirements(random, construction_items)
-
-
-def shuffle_pantry_bundle_thematically(random: Random, bundles: Dict[str, Bundle]):
- bundles["Spring Crops Bundle"].randomize_requirements(random, spring_crop_items)
- bundles["Summer Crops Bundle"].randomize_requirements(random, summer_crops_items)
- bundles["Fall Crops Bundle"].randomize_requirements(random, fall_crops_items)
- bundles["Quality Crops Bundle"].randomize_requirements(random, quality_crops_items)
- bundles["Animal Bundle"].randomize_requirements(random, animal_product_items)
- bundles["Artisan Bundle"].randomize_requirements(random, artisan_goods_items)
-
-
-def shuffle_fish_tank_thematically(random: Random, bundles: Dict[str, Bundle]):
- bundles["River Fish Bundle"].randomize_requirements(random, river_fish_items)
- bundles["Lake Fish Bundle"].randomize_requirements(random, lake_fish_items)
- bundles["Ocean Fish Bundle"].randomize_requirements(random, ocean_fish_items)
- bundles["Night Fishing Bundle"].randomize_requirements(random, night_fish_items)
- bundles["Crab Pot Bundle"].randomize_requirements(random, crab_pot_items)
- bundles["Specialty Fish Bundle"].randomize_requirements(random, specialty_fish_items)
-
-
-def shuffle_boiler_room_thematically(random: Random, bundles: Dict[str, Bundle]):
- bundles["Blacksmith's Bundle"].randomize_requirements(random, blacksmith_items)
- bundles["Geologist's Bundle"].randomize_requirements(random, geologist_items)
- bundles["Adventurer's Bundle"].randomize_requirements(random, adventurer_items)
-
-
-def shuffle_bulletin_board_thematically(random: Random, bundles: Dict[str, Bundle]):
- bundles["Chef's Bundle"].randomize_requirements(random, chef_items)
- bundles["Dye Bundle"].randomize_requirements(random, dye_items)
- bundles["Field Research Bundle"].randomize_requirements(random, field_research_items)
- bundles["Fodder Bundle"].randomize_requirements(random, fodder_items)
- bundles["Enchanter's Bundle"].randomize_requirements(random, enchanter_items)
-
-
-def shuffle_vault_amongst_themselves(random: Random, bundles: Dict[str, Bundle]):
- bundles["2,500g Bundle"].randomize_requirements(random, vault_bundle_items)
- bundles["5,000g Bundle"].randomize_requirements(random, vault_bundle_items)
- bundles["10,000g Bundle"].randomize_requirements(random, vault_bundle_items)
- bundles["25,000g Bundle"].randomize_requirements(random, vault_bundle_items)
diff --git a/worlds/stardew_valley/bundles/__init__.py b/worlds/stardew_valley/bundles/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/stardew_valley/bundles/bundle.py b/worlds/stardew_valley/bundles/bundle.py
new file mode 100644
index 000000000000..43afc750b87a
--- /dev/null
+++ b/worlds/stardew_valley/bundles/bundle.py
@@ -0,0 +1,175 @@
+import math
+from dataclasses import dataclass
+from random import Random
+from typing import List, Tuple
+
+from .bundle_item import BundleItem
+from ..content import StardewContent
+from ..options import BundlePrice, StardewValleyOptions, ExcludeGingerIsland, FestivalLocations
+from ..strings.currency_names import Currency
+
+
+@dataclass
+class Bundle:
+ room: str
+ name: str
+ items: List[BundleItem]
+ number_required: int
+
+ def __repr__(self):
+ return f"{self.name} -> {self.number_required} from {repr(self.items)}"
+
+
+@dataclass
+class BundleTemplate:
+ room: str
+ name: str
+ items: List[BundleItem]
+ number_possible_items: int
+ number_required_items: int
+
+ def __init__(self, room: str, name: str, items: List[BundleItem], number_possible_items: int,
+ number_required_items: int):
+ self.room = room
+ self.name = name
+ self.items = items
+ self.number_possible_items = number_possible_items
+ self.number_required_items = number_required_items
+
+ @staticmethod
+ def extend_from(template, items: List[BundleItem]):
+ return BundleTemplate(template.room, template.name, items, template.number_possible_items,
+ template.number_required_items)
+
+ def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle:
+ number_required, price_multiplier = get_bundle_final_prices(options.bundle_price, self.number_required_items, False)
+ filtered_items = [item for item in self.items if item.can_appear(content, options)]
+ number_items = len(filtered_items)
+ number_chosen_items = self.number_possible_items
+ if number_chosen_items < number_required:
+ number_chosen_items = number_required
+
+ if number_chosen_items > number_items:
+ chosen_items = filtered_items + random.choices(filtered_items, k=number_chosen_items - number_items)
+ else:
+ chosen_items = random.sample(filtered_items, number_chosen_items)
+ chosen_items = [item.as_amount(max(1, math.floor(item.amount * price_multiplier))) for item in chosen_items]
+ return Bundle(self.room, self.name, chosen_items, number_required)
+
+ def can_appear(self, options: StardewValleyOptions) -> bool:
+ return True
+
+
+class CurrencyBundleTemplate(BundleTemplate):
+ item: BundleItem
+
+ def __init__(self, room: str, name: str, item: BundleItem):
+ super().__init__(room, name, [item], 1, 1)
+ self.item = item
+
+ def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle:
+ currency_amount = self.get_currency_amount(options.bundle_price)
+ return Bundle(self.room, self.name, [BundleItem(self.item.item_name, currency_amount)], 1)
+
+ def get_currency_amount(self, bundle_price_option: BundlePrice):
+ _, price_multiplier = get_bundle_final_prices(bundle_price_option, self.number_required_items, True)
+ currency_amount = max(1, int(self.item.amount * price_multiplier))
+ return currency_amount
+
+ def can_appear(self, options: StardewValleyOptions) -> bool:
+ if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
+ if self.item.item_name == Currency.qi_gem or self.item.item_name == Currency.golden_walnut or self.item.item_name == Currency.cinder_shard:
+ return False
+ if options.festival_locations == FestivalLocations.option_disabled:
+ if self.item.item_name == Currency.star_token:
+ return False
+ return True
+
+
+class MoneyBundleTemplate(CurrencyBundleTemplate):
+
+ def __init__(self, room: str, default_name: str, item: BundleItem):
+ super().__init__(room, default_name, item)
+
+ def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle:
+ currency_amount = self.get_currency_amount(options.bundle_price)
+ currency_name = "g"
+ if currency_amount >= 1000:
+ unit_amount = currency_amount % 1000
+ unit_amount = "000" if unit_amount == 0 else unit_amount
+ currency_display = f"{currency_amount // 1000},{unit_amount}"
+ else:
+ currency_display = f"{currency_amount}"
+ name = f"{currency_display}{currency_name} Bundle"
+ return Bundle(self.room, name, [BundleItem(self.item.item_name, currency_amount)], 1)
+
+ def get_currency_amount(self, bundle_price_option: BundlePrice):
+ _, price_multiplier = get_bundle_final_prices(bundle_price_option, self.number_required_items, True)
+ currency_amount = max(1, int(self.item.amount * price_multiplier))
+ return currency_amount
+
+
+class IslandBundleTemplate(BundleTemplate):
+ def can_appear(self, options: StardewValleyOptions) -> bool:
+ return options.exclude_ginger_island == ExcludeGingerIsland.option_false
+
+
+class FestivalBundleTemplate(BundleTemplate):
+ def can_appear(self, options: StardewValleyOptions) -> bool:
+ return options.festival_locations != FestivalLocations.option_disabled
+
+
+class DeepBundleTemplate(BundleTemplate):
+ categories: List[List[BundleItem]]
+
+ def __init__(self, room: str, name: str, categories: List[List[BundleItem]], number_possible_items: int,
+ number_required_items: int):
+ super().__init__(room, name, [], number_possible_items, number_required_items)
+ self.categories = categories
+
+ def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle:
+ number_required, price_multiplier = get_bundle_final_prices(options.bundle_price, self.number_required_items, False)
+ number_categories = len(self.categories)
+ number_chosen_categories = self.number_possible_items
+ if number_chosen_categories < number_required:
+ number_chosen_categories = number_required
+
+ if number_chosen_categories > number_categories:
+ chosen_categories = self.categories + random.choices(self.categories,
+ k=number_chosen_categories - number_categories)
+ else:
+ chosen_categories = random.sample(self.categories, number_chosen_categories)
+
+ chosen_items = []
+ for category in chosen_categories:
+ filtered_items = [item for item in category if item.can_appear(content, options)]
+ chosen_items.append(random.choice(filtered_items))
+
+ chosen_items = [item.as_amount(max(1, math.floor(item.amount * price_multiplier))) for item in chosen_items]
+ return Bundle(self.room, self.name, chosen_items, number_required)
+
+
+def get_bundle_final_prices(bundle_price_option: BundlePrice, default_required_items: int, is_currency: bool) -> Tuple[int, float]:
+ number_required_items = get_number_required_items(bundle_price_option, default_required_items)
+ price_multiplier = get_price_multiplier(bundle_price_option, is_currency)
+ return number_required_items, price_multiplier
+
+
+def get_number_required_items(bundle_price_option: BundlePrice, default_required_items: int) -> int:
+ if bundle_price_option == BundlePrice.option_minimum:
+ return 1
+ if bundle_price_option == BundlePrice.option_maximum:
+ return 8
+ number_required = default_required_items + bundle_price_option.value
+ return min(8, max(1, number_required))
+
+
+def get_price_multiplier(bundle_price_option: BundlePrice, is_currency: bool) -> float:
+ if bundle_price_option == BundlePrice.option_minimum:
+ return 0.1 if is_currency else 0.2
+ if bundle_price_option == BundlePrice.option_maximum:
+ return 4 if is_currency else 1.4
+ price_factor = 0.4 if is_currency else (0.2 if bundle_price_option.value <= 0 else 0.1)
+ price_multiplier_difference = bundle_price_option.value * price_factor
+ price_multiplier = 1 + price_multiplier_difference
+ return round(price_multiplier, 2)
diff --git a/worlds/stardew_valley/bundles/bundle_item.py b/worlds/stardew_valley/bundles/bundle_item.py
new file mode 100644
index 000000000000..7dc9c0e1a3b5
--- /dev/null
+++ b/worlds/stardew_valley/bundles/bundle_item.py
@@ -0,0 +1,101 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+
+from ..content import StardewContent
+from ..options import StardewValleyOptions, ExcludeGingerIsland, FestivalLocations, SkillProgression
+from ..strings.crop_names import Fruit
+from ..strings.currency_names import Currency
+from ..strings.quality_names import CropQuality, FishQuality, ForageQuality
+
+
+class BundleItemSource(ABC):
+ @abstractmethod
+ def can_appear(self, options: StardewValleyOptions) -> bool:
+ ...
+
+
+class VanillaItemSource(BundleItemSource):
+ def can_appear(self, options: StardewValleyOptions) -> bool:
+ return True
+
+
+class IslandItemSource(BundleItemSource):
+ def can_appear(self, options: StardewValleyOptions) -> bool:
+ return options.exclude_ginger_island == ExcludeGingerIsland.option_false
+
+
+class FestivalItemSource(BundleItemSource):
+ def can_appear(self, options: StardewValleyOptions) -> bool:
+ return options.festival_locations != FestivalLocations.option_disabled
+
+
+class MasteryItemSource(BundleItemSource):
+ def can_appear(self, options: StardewValleyOptions) -> bool:
+ return options.skill_progression == SkillProgression.option_progressive_with_masteries
+
+
+class ContentItemSource(BundleItemSource):
+ """This is meant to be used for items that are managed by the content packs."""
+
+ def can_appear(self, options: StardewValleyOptions) -> bool:
+ raise ValueError("This should not be called, check if the item is in the content instead.")
+
+
+@dataclass(frozen=True, order=True)
+class BundleItem:
+ class Sources:
+ vanilla = VanillaItemSource()
+ island = IslandItemSource()
+ festival = FestivalItemSource()
+ masteries = MasteryItemSource()
+ content = ContentItemSource()
+
+ item_name: str
+ amount: int = 1
+ quality: str = CropQuality.basic
+ source: BundleItemSource = Sources.vanilla
+ flavor: str = None
+ can_have_quality: bool = True
+
+ @staticmethod
+ def money_bundle(amount: int) -> BundleItem:
+ return BundleItem(Currency.money, amount)
+
+ def get_item(self) -> str:
+ if self.flavor is None:
+ return self.item_name
+ return f"{self.item_name} [{self.flavor}]"
+
+ def as_amount(self, amount: int) -> BundleItem:
+ return BundleItem(self.item_name, amount, self.quality, self.source, self.flavor)
+
+ def as_quality(self, quality: str) -> BundleItem:
+ if self.can_have_quality:
+ return BundleItem(self.item_name, self.amount, quality, self.source, self.flavor)
+ return BundleItem(self.item_name, self.amount, self.quality, self.source, self.flavor)
+
+ def as_quality_crop(self) -> BundleItem:
+ amount = 5
+ difficult_crops = [Fruit.sweet_gem_berry, Fruit.ancient_fruit]
+ if self.item_name in difficult_crops:
+ amount = 1
+ return self.as_quality(CropQuality.gold).as_amount(amount)
+
+ def as_quality_fish(self) -> BundleItem:
+ return self.as_quality(FishQuality.gold)
+
+ def as_quality_forage(self) -> BundleItem:
+ return self.as_quality(ForageQuality.gold)
+
+ def __repr__(self):
+ quality = "" if self.quality == CropQuality.basic else self.quality
+ return f"{self.amount} {quality} {self.get_item()}"
+
+ def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool:
+ if isinstance(self.source, ContentItemSource):
+ return self.get_item() in content.game_items
+
+ return self.source.can_appear(options)
+
diff --git a/worlds/stardew_valley/bundles/bundle_room.py b/worlds/stardew_valley/bundles/bundle_room.py
new file mode 100644
index 000000000000..8068ff17ac83
--- /dev/null
+++ b/worlds/stardew_valley/bundles/bundle_room.py
@@ -0,0 +1,43 @@
+from dataclasses import dataclass
+from random import Random
+from typing import List
+
+from .bundle import Bundle, BundleTemplate
+from ..content import StardewContent
+from ..options import BundlePrice, StardewValleyOptions
+
+
+@dataclass
+class BundleRoom:
+ name: str
+ bundles: List[Bundle]
+
+
+@dataclass
+class BundleRoomTemplate:
+ name: str
+ bundles: List[BundleTemplate]
+ number_bundles: int
+
+ def create_bundle_room(self, random: Random, content: StardewContent, options: StardewValleyOptions):
+ filtered_bundles = [bundle for bundle in self.bundles if bundle.can_appear(options)]
+
+ priority_bundles = []
+ unpriority_bundles = []
+ for bundle in filtered_bundles:
+ if bundle.name in options.bundle_plando:
+ priority_bundles.append(bundle)
+ else:
+ unpriority_bundles.append(bundle)
+
+ if self.number_bundles <= len(priority_bundles):
+ chosen_bundles = random.sample(priority_bundles, self.number_bundles)
+ else:
+ chosen_bundles = priority_bundles
+ num_remaining_bundles = self.number_bundles - len(priority_bundles)
+ if num_remaining_bundles > len(unpriority_bundles):
+ chosen_bundles.extend(random.choices(unpriority_bundles, k=num_remaining_bundles))
+ else:
+ chosen_bundles.extend(random.sample(unpriority_bundles, num_remaining_bundles))
+
+ return BundleRoom(self.name, [bundle.create_bundle(random, content, options) for bundle in chosen_bundles])
diff --git a/worlds/stardew_valley/bundles/bundles.py b/worlds/stardew_valley/bundles/bundles.py
new file mode 100644
index 000000000000..99619e09aadf
--- /dev/null
+++ b/worlds/stardew_valley/bundles/bundles.py
@@ -0,0 +1,124 @@
+from random import Random
+from typing import List, Tuple
+
+from .bundle import Bundle
+from .bundle_room import BundleRoom, BundleRoomTemplate
+from ..content import StardewContent
+from ..data.bundle_data import pantry_vanilla, crafts_room_vanilla, fish_tank_vanilla, boiler_room_vanilla, bulletin_board_vanilla, vault_vanilla, \
+ pantry_thematic, crafts_room_thematic, fish_tank_thematic, boiler_room_thematic, bulletin_board_thematic, vault_thematic, pantry_remixed, \
+ crafts_room_remixed, fish_tank_remixed, boiler_room_remixed, bulletin_board_remixed, vault_remixed, all_bundle_items_except_money, \
+ abandoned_joja_mart_thematic, abandoned_joja_mart_vanilla, abandoned_joja_mart_remixed, raccoon_vanilla, raccoon_thematic, raccoon_remixed, \
+ community_center_remixed_anywhere
+from ..logic.logic import StardewLogic
+from ..options import BundleRandomization, StardewValleyOptions
+
+
+def get_all_bundles(random: Random, logic: StardewLogic, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]:
+ if options.bundle_randomization == BundleRandomization.option_vanilla:
+ return get_vanilla_bundles(random, content, options)
+ elif options.bundle_randomization == BundleRandomization.option_thematic:
+ return get_thematic_bundles(random, content, options)
+ elif options.bundle_randomization == BundleRandomization.option_remixed:
+ return get_remixed_bundles(random, content, options)
+ elif options.bundle_randomization == BundleRandomization.option_remixed_anywhere:
+ return get_remixed_bundles_anywhere(random, content, options)
+ elif options.bundle_randomization == BundleRandomization.option_shuffled:
+ return get_shuffled_bundles(random, logic, content, options)
+
+ raise NotImplementedError
+
+
+def get_vanilla_bundles(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]:
+ pantry = pantry_vanilla.create_bundle_room(random, content, options)
+ crafts_room = crafts_room_vanilla.create_bundle_room(random, content, options)
+ fish_tank = fish_tank_vanilla.create_bundle_room(random, content, options)
+ boiler_room = boiler_room_vanilla.create_bundle_room(random, content, options)
+ bulletin_board = bulletin_board_vanilla.create_bundle_room(random, content, options)
+ vault = vault_vanilla.create_bundle_room(random, content, options)
+ abandoned_joja_mart = abandoned_joja_mart_vanilla.create_bundle_room(random, content, options)
+ raccoon = raccoon_vanilla.create_bundle_room(random, content, options)
+ fix_raccoon_bundle_names(raccoon)
+ return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon]
+
+
+def get_thematic_bundles(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]:
+ pantry = pantry_thematic.create_bundle_room(random, content, options)
+ crafts_room = crafts_room_thematic.create_bundle_room(random, content, options)
+ fish_tank = fish_tank_thematic.create_bundle_room(random, content, options)
+ boiler_room = boiler_room_thematic.create_bundle_room(random, content, options)
+ bulletin_board = bulletin_board_thematic.create_bundle_room(random, content, options)
+ vault = vault_thematic.create_bundle_room(random, content, options)
+ abandoned_joja_mart = abandoned_joja_mart_thematic.create_bundle_room(random, content, options)
+ raccoon = raccoon_thematic.create_bundle_room(random, content, options)
+ fix_raccoon_bundle_names(raccoon)
+ return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon]
+
+
+def get_remixed_bundles(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]:
+ pantry = pantry_remixed.create_bundle_room(random, content, options)
+ crafts_room = crafts_room_remixed.create_bundle_room(random, content, options)
+ fish_tank = fish_tank_remixed.create_bundle_room(random, content, options)
+ boiler_room = boiler_room_remixed.create_bundle_room(random, content, options)
+ bulletin_board = bulletin_board_remixed.create_bundle_room(random, content, options)
+ vault = vault_remixed.create_bundle_room(random, content, options)
+ abandoned_joja_mart = abandoned_joja_mart_remixed.create_bundle_room(random, content, options)
+ raccoon = raccoon_remixed.create_bundle_room(random, content, options)
+ fix_raccoon_bundle_names(raccoon)
+ return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon]
+
+
+def get_remixed_bundles_anywhere(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]:
+ big_room = community_center_remixed_anywhere.create_bundle_room(random, content, options)
+ all_chosen_bundles = big_room.bundles
+ random.shuffle(all_chosen_bundles)
+
+ end_index = 0
+
+ pantry, end_index = create_room_from_bundles(pantry_remixed, all_chosen_bundles, end_index)
+ crafts_room, end_index = create_room_from_bundles(crafts_room_remixed, all_chosen_bundles, end_index)
+ fish_tank, end_index = create_room_from_bundles(fish_tank_remixed, all_chosen_bundles, end_index)
+ boiler_room, end_index = create_room_from_bundles(boiler_room_remixed, all_chosen_bundles, end_index)
+ bulletin_board, end_index = create_room_from_bundles(bulletin_board_remixed, all_chosen_bundles, end_index)
+
+ vault = vault_remixed.create_bundle_room(random, content, options)
+ abandoned_joja_mart = abandoned_joja_mart_remixed.create_bundle_room(random, content, options)
+ raccoon = raccoon_remixed.create_bundle_room(random, content, options)
+ fix_raccoon_bundle_names(raccoon)
+ return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon]
+
+
+def create_room_from_bundles(template: BundleRoomTemplate, all_bundles: List[Bundle], end_index: int) -> Tuple[BundleRoom, int]:
+ start_index = end_index
+ end_index += template.number_bundles
+ return BundleRoom(template.name, all_bundles[start_index:end_index]), end_index
+
+
+def get_shuffled_bundles(random: Random, logic: StardewLogic, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]:
+ valid_bundle_items = [bundle_item for bundle_item in all_bundle_items_except_money if bundle_item.can_appear(content, options)]
+
+ rooms = [room for room in get_remixed_bundles(random, content, options) if room.name != "Vault"]
+ required_items = 0
+ for room in rooms:
+ for bundle in room.bundles:
+ required_items += len(bundle.items)
+ random.shuffle(room.bundles)
+ random.shuffle(rooms)
+
+ # Remove duplicates of the same item
+ valid_bundle_items = [item1 for i, item1 in enumerate(valid_bundle_items)
+ if not any(item1.item_name == item2.item_name and item1.quality == item2.quality for item2 in valid_bundle_items[:i])]
+ chosen_bundle_items = random.sample(valid_bundle_items, required_items)
+ for room in rooms:
+ for bundle in room.bundles:
+ num_items = len(bundle.items)
+ bundle.items = chosen_bundle_items[:num_items]
+ chosen_bundle_items = chosen_bundle_items[num_items:]
+
+ vault = vault_remixed.create_bundle_room(random, content, options)
+ return [*rooms, vault]
+
+
+def fix_raccoon_bundle_names(raccoon):
+ for i in range(len(raccoon.bundles)):
+ raccoon_bundle = raccoon.bundles[i]
+ raccoon_bundle.name = f"Raccoon Request {i + 1}"
diff --git a/worlds/stardew_valley/content/__init__.py b/worlds/stardew_valley/content/__init__.py
new file mode 100644
index 000000000000..9130873fa405
--- /dev/null
+++ b/worlds/stardew_valley/content/__init__.py
@@ -0,0 +1,107 @@
+from . import content_packs
+from .feature import cropsanity, friendsanity, fishsanity, booksanity
+from .game_content import ContentPack, StardewContent, StardewFeatures
+from .unpacking import unpack_content
+from .. import options
+
+
+def create_content(player_options: options.StardewValleyOptions) -> StardewContent:
+ active_packs = choose_content_packs(player_options)
+ features = choose_features(player_options)
+ return unpack_content(features, active_packs)
+
+
+def choose_content_packs(player_options: options.StardewValleyOptions):
+ active_packs = [content_packs.pelican_town, content_packs.the_desert, content_packs.the_farm, content_packs.the_mines]
+
+ if player_options.exclude_ginger_island == options.ExcludeGingerIsland.option_false:
+ active_packs.append(content_packs.ginger_island_content_pack)
+
+ if player_options.special_order_locations & options.SpecialOrderLocations.value_qi:
+ active_packs.append(content_packs.qi_board_content_pack)
+
+ for mod in player_options.mods.value:
+ active_packs.append(content_packs.by_mod[mod])
+
+ return active_packs
+
+
+def choose_features(player_options: options.StardewValleyOptions) -> StardewFeatures:
+ return StardewFeatures(
+ choose_booksanity(player_options.booksanity),
+ choose_cropsanity(player_options.cropsanity),
+ choose_fishsanity(player_options.fishsanity),
+ choose_friendsanity(player_options.friendsanity, player_options.friendsanity_heart_size)
+ )
+
+
+booksanity_by_option = {
+ options.Booksanity.option_none: booksanity.BooksanityDisabled(),
+ options.Booksanity.option_power: booksanity.BooksanityPower(),
+ options.Booksanity.option_power_skill: booksanity.BooksanityPowerSkill(),
+ options.Booksanity.option_all: booksanity.BooksanityAll(),
+}
+
+
+def choose_booksanity(booksanity_option: options.Booksanity) -> booksanity.BooksanityFeature:
+ booksanity_feature = booksanity_by_option.get(booksanity_option)
+
+ if booksanity_feature is None:
+ raise ValueError(f"No booksanity feature mapped to {str(booksanity_option.value)}")
+
+ return booksanity_feature
+
+
+cropsanity_by_option = {
+ options.Cropsanity.option_disabled: cropsanity.CropsanityDisabled(),
+ options.Cropsanity.option_enabled: cropsanity.CropsanityEnabled(),
+}
+
+
+def choose_cropsanity(cropsanity_option: options.Cropsanity) -> cropsanity.CropsanityFeature:
+ cropsanity_feature = cropsanity_by_option.get(cropsanity_option)
+
+ if cropsanity_feature is None:
+ raise ValueError(f"No cropsanity feature mapped to {str(cropsanity_option.value)}")
+
+ return cropsanity_feature
+
+
+fishsanity_by_option = {
+ options.Fishsanity.option_none: fishsanity.FishsanityNone(),
+ options.Fishsanity.option_legendaries: fishsanity.FishsanityLegendaries(),
+ options.Fishsanity.option_special: fishsanity.FishsanitySpecial(),
+ options.Fishsanity.option_randomized: fishsanity.FishsanityAll(randomization_ratio=0.4),
+ options.Fishsanity.option_all: fishsanity.FishsanityAll(),
+ options.Fishsanity.option_exclude_legendaries: fishsanity.FishsanityExcludeLegendaries(),
+ options.Fishsanity.option_exclude_hard_fish: fishsanity.FishsanityExcludeHardFish(),
+ options.Fishsanity.option_only_easy_fish: fishsanity.FishsanityOnlyEasyFish(),
+}
+
+
+def choose_fishsanity(fishsanity_option: options.Fishsanity) -> fishsanity.FishsanityFeature:
+ fishsanity_feature = fishsanity_by_option.get(fishsanity_option)
+
+ if fishsanity_feature is None:
+ raise ValueError(f"No fishsanity feature mapped to {str(fishsanity_option.value)}")
+
+ return fishsanity_feature
+
+
+def choose_friendsanity(friendsanity_option: options.Friendsanity, heart_size: options.FriendsanityHeartSize) -> friendsanity.FriendsanityFeature:
+ if friendsanity_option == options.Friendsanity.option_none:
+ return friendsanity.FriendsanityNone()
+
+ if friendsanity_option == options.Friendsanity.option_bachelors:
+ return friendsanity.FriendsanityBachelors(heart_size.value)
+
+ if friendsanity_option == options.Friendsanity.option_starting_npcs:
+ return friendsanity.FriendsanityStartingNpc(heart_size.value)
+
+ if friendsanity_option == options.Friendsanity.option_all:
+ return friendsanity.FriendsanityAll(heart_size.value)
+
+ if friendsanity_option == options.Friendsanity.option_all_with_marriage:
+ return friendsanity.FriendsanityAllWithMarriage(heart_size.value)
+
+ raise ValueError(f"No friendsanity feature mapped to {str(friendsanity_option.value)}")
diff --git a/worlds/stardew_valley/content/content_packs.py b/worlds/stardew_valley/content/content_packs.py
new file mode 100644
index 000000000000..fb8df8c70cba
--- /dev/null
+++ b/worlds/stardew_valley/content/content_packs.py
@@ -0,0 +1,31 @@
+import importlib
+import pkgutil
+
+from . import mods
+from .mod_registry import by_mod
+from .vanilla.base import base_game
+from .vanilla.ginger_island import ginger_island_content_pack
+from .vanilla.pelican_town import pelican_town
+from .vanilla.qi_board import qi_board_content_pack
+from .vanilla.the_desert import the_desert
+from .vanilla.the_farm import the_farm
+from .vanilla.the_mines import the_mines
+
+assert base_game
+assert ginger_island_content_pack
+assert pelican_town
+assert qi_board_content_pack
+assert the_desert
+assert the_farm
+assert the_mines
+
+# Dynamically register everything currently in the mods folder. This would ideally be done through a metaclass, but I have not looked into that yet.
+mod_modules = pkgutil.iter_modules(mods.__path__)
+
+loaded_modules = {}
+for mod_module in mod_modules:
+ module_name = mod_module.name
+ module = importlib.import_module("." + module_name, mods.__name__)
+ loaded_modules[module_name] = module
+
+assert by_mod
diff --git a/worlds/stardew_valley/content/feature/__init__.py b/worlds/stardew_valley/content/feature/__init__.py
new file mode 100644
index 000000000000..74249c808257
--- /dev/null
+++ b/worlds/stardew_valley/content/feature/__init__.py
@@ -0,0 +1,4 @@
+from . import booksanity
+from . import cropsanity
+from . import fishsanity
+from . import friendsanity
diff --git a/worlds/stardew_valley/content/feature/booksanity.py b/worlds/stardew_valley/content/feature/booksanity.py
new file mode 100644
index 000000000000..5eade5932535
--- /dev/null
+++ b/worlds/stardew_valley/content/feature/booksanity.py
@@ -0,0 +1,72 @@
+from abc import ABC, abstractmethod
+from typing import ClassVar, Optional, Iterable
+
+from ...data.game_item import GameItem, ItemTag
+from ...strings.book_names import ordered_lost_books
+
+item_prefix = "Power: "
+location_prefix = "Read "
+
+
+def to_item_name(book: str) -> str:
+ return item_prefix + book
+
+
+def to_location_name(book: str) -> str:
+ return location_prefix + book
+
+
+def extract_book_from_location_name(location_name: str) -> Optional[str]:
+ if not location_name.startswith(location_prefix):
+ return None
+
+ return location_name[len(location_prefix):]
+
+
+class BooksanityFeature(ABC):
+ is_enabled: ClassVar[bool]
+
+ to_item_name = staticmethod(to_item_name)
+ progressive_lost_book = "Progressive Lost Book"
+ to_location_name = staticmethod(to_location_name)
+ extract_book_from_location_name = staticmethod(extract_book_from_location_name)
+
+ @abstractmethod
+ def is_included(self, book: GameItem) -> bool:
+ ...
+
+ @staticmethod
+ def get_randomized_lost_books() -> Iterable[str]:
+ return []
+
+
+class BooksanityDisabled(BooksanityFeature):
+ is_enabled = False
+
+ def is_included(self, book: GameItem) -> bool:
+ return False
+
+
+class BooksanityPower(BooksanityFeature):
+ is_enabled = True
+
+ def is_included(self, book: GameItem) -> bool:
+ return ItemTag.BOOK_POWER in book.tags
+
+
+class BooksanityPowerSkill(BooksanityFeature):
+ is_enabled = True
+
+ def is_included(self, book: GameItem) -> bool:
+ return ItemTag.BOOK_POWER in book.tags or ItemTag.BOOK_SKILL in book.tags
+
+
+class BooksanityAll(BooksanityFeature):
+ is_enabled = True
+
+ def is_included(self, book: GameItem) -> bool:
+ return ItemTag.BOOK_POWER in book.tags or ItemTag.BOOK_SKILL in book.tags
+
+ @staticmethod
+ def get_randomized_lost_books() -> Iterable[str]:
+ return ordered_lost_books
diff --git a/worlds/stardew_valley/content/feature/cropsanity.py b/worlds/stardew_valley/content/feature/cropsanity.py
new file mode 100644
index 000000000000..18ef370815ee
--- /dev/null
+++ b/worlds/stardew_valley/content/feature/cropsanity.py
@@ -0,0 +1,42 @@
+from abc import ABC, abstractmethod
+from typing import ClassVar, Optional
+
+from ...data.game_item import GameItem, ItemTag
+
+location_prefix = "Harvest "
+
+
+def to_location_name(crop: str) -> str:
+ return location_prefix + crop
+
+
+def extract_crop_from_location_name(location_name: str) -> Optional[str]:
+ if not location_name.startswith(location_prefix):
+ return None
+
+ return location_name[len(location_prefix):]
+
+
+class CropsanityFeature(ABC):
+ is_enabled: ClassVar[bool]
+
+ to_location_name = staticmethod(to_location_name)
+ extract_crop_from_location_name = staticmethod(extract_crop_from_location_name)
+
+ @abstractmethod
+ def is_included(self, crop: GameItem) -> bool:
+ ...
+
+
+class CropsanityDisabled(CropsanityFeature):
+ is_enabled = False
+
+ def is_included(self, crop: GameItem) -> bool:
+ return False
+
+
+class CropsanityEnabled(CropsanityFeature):
+ is_enabled = True
+
+ def is_included(self, crop: GameItem) -> bool:
+ return ItemTag.CROPSANITY_SEED in crop.tags
diff --git a/worlds/stardew_valley/content/feature/fishsanity.py b/worlds/stardew_valley/content/feature/fishsanity.py
new file mode 100644
index 000000000000..02f9a632a873
--- /dev/null
+++ b/worlds/stardew_valley/content/feature/fishsanity.py
@@ -0,0 +1,101 @@
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+from typing import ClassVar, Optional
+
+from ...data.fish_data import FishItem
+from ...strings.fish_names import Fish
+
+location_prefix = "Fishsanity: "
+
+
+def to_location_name(fish: str) -> str:
+ return location_prefix + fish
+
+
+def extract_fish_from_location_name(location_name: str) -> Optional[str]:
+ if not location_name.startswith(location_prefix):
+ return None
+
+ return location_name[len(location_prefix):]
+
+
+@dataclass(frozen=True)
+class FishsanityFeature(ABC):
+ is_enabled: ClassVar[bool]
+
+ randomization_ratio: float = 1
+
+ to_location_name = staticmethod(to_location_name)
+ extract_fish_from_location_name = staticmethod(extract_fish_from_location_name)
+
+ @property
+ def is_randomized(self) -> bool:
+ return self.randomization_ratio != 1
+
+ @abstractmethod
+ def is_included(self, fish: FishItem) -> bool:
+ ...
+
+
+class FishsanityNone(FishsanityFeature):
+ is_enabled = False
+
+ def is_included(self, fish: FishItem) -> bool:
+ return False
+
+
+class FishsanityLegendaries(FishsanityFeature):
+ is_enabled = True
+
+ def is_included(self, fish: FishItem) -> bool:
+ return fish.legendary
+
+
+class FishsanitySpecial(FishsanityFeature):
+ is_enabled = True
+
+ included_fishes = {
+ Fish.angler,
+ Fish.crimsonfish,
+ Fish.glacierfish,
+ Fish.legend,
+ Fish.mutant_carp,
+ Fish.blobfish,
+ Fish.lava_eel,
+ Fish.octopus,
+ Fish.scorpion_carp,
+ Fish.ice_pip,
+ Fish.super_cucumber,
+ Fish.dorado
+ }
+
+ def is_included(self, fish: FishItem) -> bool:
+ return fish.name in self.included_fishes
+
+
+class FishsanityAll(FishsanityFeature):
+ is_enabled = True
+
+ def is_included(self, fish: FishItem) -> bool:
+ return True
+
+
+class FishsanityExcludeLegendaries(FishsanityFeature):
+ is_enabled = True
+
+ def is_included(self, fish: FishItem) -> bool:
+ return not fish.legendary
+
+
+class FishsanityExcludeHardFish(FishsanityFeature):
+ is_enabled = True
+
+ def is_included(self, fish: FishItem) -> bool:
+ return fish.difficulty < 80
+
+
+class FishsanityOnlyEasyFish(FishsanityFeature):
+ is_enabled = True
+
+ def is_included(self, fish: FishItem) -> bool:
+ return fish.difficulty < 50
diff --git a/worlds/stardew_valley/content/feature/friendsanity.py b/worlds/stardew_valley/content/feature/friendsanity.py
new file mode 100644
index 000000000000..3e1581b4e2f1
--- /dev/null
+++ b/worlds/stardew_valley/content/feature/friendsanity.py
@@ -0,0 +1,139 @@
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+from functools import lru_cache
+from typing import Optional, Tuple, ClassVar
+
+from ...data.villagers_data import Villager
+from ...strings.villager_names import NPC
+
+suffix = " <3"
+location_prefix = "Friendsanity: "
+
+
+def to_item_name(npc_name: str) -> str:
+ return npc_name + suffix
+
+
+def to_location_name(npc_name: str, heart: int) -> str:
+ return location_prefix + npc_name + " " + str(heart) + suffix
+
+
+pet_heart_item_name = to_item_name(NPC.pet)
+
+
+def extract_npc_from_item_name(item_name: str) -> Optional[str]:
+ if not item_name.endswith(suffix):
+ return None
+
+ return item_name[:-len(suffix)]
+
+
+def extract_npc_from_location_name(location_name: str) -> Tuple[Optional[str], int]:
+ if not location_name.endswith(suffix):
+ return None, 0
+
+ trimmed = location_name[len(location_prefix):-len(suffix)]
+ last_space = trimmed.rindex(" ")
+ return trimmed[:last_space], int(trimmed[last_space + 1:])
+
+
+@lru_cache(maxsize=32) # Should not go pass 32 values if every friendsanity options are in the multi world
+def get_heart_steps(max_heart: int, heart_size: int) -> Tuple[int, ...]:
+ return tuple(range(heart_size, max_heart + 1, heart_size)) + ((max_heart,) if max_heart % heart_size else ())
+
+
+@dataclass(frozen=True)
+class FriendsanityFeature(ABC):
+ is_enabled: ClassVar[bool]
+
+ heart_size: int
+
+ to_item_name = staticmethod(to_item_name)
+ to_location_name = staticmethod(to_location_name)
+ pet_heart_item_name = pet_heart_item_name
+ extract_npc_from_item_name = staticmethod(extract_npc_from_item_name)
+ extract_npc_from_location_name = staticmethod(extract_npc_from_location_name)
+
+ @abstractmethod
+ def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]:
+ ...
+
+ @property
+ def is_pet_randomized(self):
+ return bool(self.get_pet_randomized_hearts())
+
+ @abstractmethod
+ def get_pet_randomized_hearts(self) -> Tuple[int, ...]:
+ ...
+
+
+class FriendsanityNone(FriendsanityFeature):
+ is_enabled = False
+
+ def __init__(self):
+ super().__init__(1)
+
+ def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]:
+ return ()
+
+ def get_pet_randomized_hearts(self) -> Tuple[int, ...]:
+ return ()
+
+
+@dataclass(frozen=True)
+class FriendsanityBachelors(FriendsanityFeature):
+ is_enabled = True
+
+ def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]:
+ if not villager.bachelor:
+ return ()
+
+ return get_heart_steps(8, self.heart_size)
+
+ def get_pet_randomized_hearts(self) -> Tuple[int, ...]:
+ return ()
+
+
+@dataclass(frozen=True)
+class FriendsanityStartingNpc(FriendsanityFeature):
+ is_enabled = True
+
+ def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]:
+ if not villager.available:
+ return ()
+
+ if villager.bachelor:
+ return get_heart_steps(8, self.heart_size)
+
+ return get_heart_steps(10, self.heart_size)
+
+ def get_pet_randomized_hearts(self) -> Tuple[int, ...]:
+ return get_heart_steps(5, self.heart_size)
+
+
+@dataclass(frozen=True)
+class FriendsanityAll(FriendsanityFeature):
+ is_enabled = True
+
+ def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]:
+ if villager.bachelor:
+ return get_heart_steps(8, self.heart_size)
+
+ return get_heart_steps(10, self.heart_size)
+
+ def get_pet_randomized_hearts(self) -> Tuple[int, ...]:
+ return get_heart_steps(5, self.heart_size)
+
+
+@dataclass(frozen=True)
+class FriendsanityAllWithMarriage(FriendsanityFeature):
+ is_enabled = True
+
+ def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]:
+ if villager.bachelor:
+ return get_heart_steps(14, self.heart_size)
+
+ return get_heart_steps(10, self.heart_size)
+
+ def get_pet_randomized_hearts(self) -> Tuple[int, ...]:
+ return get_heart_steps(5, self.heart_size)
diff --git a/worlds/stardew_valley/content/game_content.py b/worlds/stardew_valley/content/game_content.py
new file mode 100644
index 000000000000..8dcf933145e3
--- /dev/null
+++ b/worlds/stardew_valley/content/game_content.py
@@ -0,0 +1,117 @@
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import Dict, Iterable, Set, Any, Mapping, Type, Tuple, Union
+
+from .feature import booksanity, cropsanity, fishsanity, friendsanity
+from ..data.fish_data import FishItem
+from ..data.game_item import GameItem, ItemSource, ItemTag
+from ..data.skill import Skill
+from ..data.villagers_data import Villager
+
+
+@dataclass(frozen=True)
+class StardewContent:
+ features: StardewFeatures
+ registered_packs: Set[str] = field(default_factory=set)
+
+ # regions -> To be used with can reach rule
+
+ game_items: Dict[str, GameItem] = field(default_factory=dict)
+ fishes: Dict[str, FishItem] = field(default_factory=dict)
+ villagers: Dict[str, Villager] = field(default_factory=dict)
+ skills: Dict[str, Skill] = field(default_factory=dict)
+ quests: Dict[str, Any] = field(default_factory=dict)
+
+ def find_sources_of_type(self, types: Union[Type[ItemSource], Tuple[Type[ItemSource]]]) -> Iterable[ItemSource]:
+ for item in self.game_items.values():
+ for source in item.sources:
+ if isinstance(source, types):
+ yield source
+
+ def source_item(self, item_name: str, *sources: ItemSource):
+ item = self.game_items.setdefault(item_name, GameItem(item_name))
+ item.add_sources(sources)
+
+ def tag_item(self, item_name: str, *tags: ItemTag):
+ item = self.game_items.setdefault(item_name, GameItem(item_name))
+ item.add_tags(tags)
+
+ def untag_item(self, item_name: str, tag: ItemTag):
+ self.game_items[item_name].tags.remove(tag)
+
+ def find_tagged_items(self, tag: ItemTag) -> Iterable[GameItem]:
+ # TODO might be worth caching this, but it need to only be cached once the content is finalized...
+ for item in self.game_items.values():
+ if tag in item.tags:
+ yield item
+
+
+@dataclass(frozen=True)
+class StardewFeatures:
+ booksanity: booksanity.BooksanityFeature
+ cropsanity: cropsanity.CropsanityFeature
+ fishsanity: fishsanity.FishsanityFeature
+ friendsanity: friendsanity.FriendsanityFeature
+
+
+@dataclass(frozen=True)
+class ContentPack:
+ name: str
+
+ dependencies: Iterable[str] = ()
+ """ Hard requirement, generation will fail if it's missing. """
+ weak_dependencies: Iterable[str] = ()
+ """ Not a strict dependency, only used only for ordering the packs to make sure hooks are applied correctly. """
+
+ # items
+ # def item_hook
+ # ...
+
+ harvest_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict)
+ """Harvest sources contains both crops and forageables, but also fruits from trees, the cave farm and stuff harvested from tapping like maple syrup."""
+
+ def harvest_source_hook(self, content: StardewContent):
+ ...
+
+ shop_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict)
+
+ def shop_source_hook(self, content: StardewContent):
+ ...
+
+ fishes: Iterable[FishItem] = ()
+
+ def fish_hook(self, content: StardewContent):
+ ...
+
+ crafting_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict)
+
+ def crafting_hook(self, content: StardewContent):
+ ...
+
+ artisan_good_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict)
+
+ def artisan_good_hook(self, content: StardewContent):
+ ...
+
+ villagers: Iterable[Villager] = ()
+
+ def villager_hook(self, content: StardewContent):
+ ...
+
+ skills: Iterable[Skill] = ()
+
+ def skill_hook(self, content: StardewContent):
+ ...
+
+ quests: Iterable[Any] = ()
+
+ def quest_hook(self, content: StardewContent):
+ ...
+
+ def finalize_hook(self, content: StardewContent):
+ """Last hook called on the pack, once all other content packs have been registered.
+
+ This is the place to do any final adjustments to the content, like adding rules based on tags applied by other packs.
+ """
+ ...
diff --git a/worlds/stardew_valley/content/mod_registry.py b/worlds/stardew_valley/content/mod_registry.py
new file mode 100644
index 000000000000..c598fcbad295
--- /dev/null
+++ b/worlds/stardew_valley/content/mod_registry.py
@@ -0,0 +1,7 @@
+from .game_content import ContentPack
+
+by_mod = {}
+
+
+def register_mod_content_pack(content_pack: ContentPack):
+ by_mod[content_pack.name] = content_pack
diff --git a/worlds/stardew_valley/content/mods/__init__.py b/worlds/stardew_valley/content/mods/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/stardew_valley/content/mods/alecto.py b/worlds/stardew_valley/content/mods/alecto.py
new file mode 100644
index 000000000000..c05c936de3c0
--- /dev/null
+++ b/worlds/stardew_valley/content/mods/alecto.py
@@ -0,0 +1,33 @@
+from ..game_content import ContentPack, StardewContent
+from ..mod_registry import register_mod_content_pack
+from ...data import villagers_data
+from ...data.harvest import ForagingSource
+from ...data.requirement import QuestRequirement
+from ...mods.mod_data import ModNames
+from ...strings.quest_names import ModQuest
+from ...strings.region_names import Region
+from ...strings.seed_names import DistantLandsSeed
+
+
+class AlectoContentPack(ContentPack):
+
+ def harvest_source_hook(self, content: StardewContent):
+ if ModNames.distant_lands in content.registered_packs:
+ content.game_items.pop(DistantLandsSeed.void_mint)
+ content.game_items.pop(DistantLandsSeed.vile_ancient_fruit)
+ content.source_item(DistantLandsSeed.void_mint,
+ ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.WitchOrder),)),),
+ content.source_item(DistantLandsSeed.vile_ancient_fruit,
+ ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.WitchOrder),)), ),
+
+
+register_mod_content_pack(ContentPack(
+ ModNames.alecto,
+ weak_dependencies=(
+ ModNames.distant_lands, # For Witch's order
+ ),
+ villagers=(
+ villagers_data.alecto,
+ )
+
+))
diff --git a/worlds/stardew_valley/content/mods/archeology.py b/worlds/stardew_valley/content/mods/archeology.py
new file mode 100644
index 000000000000..5eb8af4cfc38
--- /dev/null
+++ b/worlds/stardew_valley/content/mods/archeology.py
@@ -0,0 +1,34 @@
+from ..game_content import ContentPack, StardewContent
+from ..mod_registry import register_mod_content_pack
+from ...data.artisan import MachineSource
+from ...data.skill import Skill
+from ...mods.mod_data import ModNames
+from ...strings.craftable_names import ModMachine
+from ...strings.fish_names import ModTrash
+from ...strings.metal_names import all_artifacts, all_fossils
+from ...strings.skill_names import ModSkill
+
+
+class ArchaeologyContentPack(ContentPack):
+ def artisan_good_hook(self, content: StardewContent):
+ # Done as honestly there are too many display items to put into the initial registration traditionally.
+ display_items = all_artifacts + all_fossils
+ for item in display_items:
+ self.source_display_items(item, content)
+ content.source_item(ModTrash.rusty_scrap, *(MachineSource(item=artifact, machine=ModMachine.grinder) for artifact in all_artifacts))
+
+ def source_display_items(self, item: str, content: StardewContent):
+ wood_display = f"Wooden Display: {item}"
+ hardwood_display = f"Hardwood Display: {item}"
+ if item == "Trilobite":
+ wood_display = f"Wooden Display: Trilobite Fossil"
+ hardwood_display = f"Hardwood Display: Trilobite Fossil"
+ content.source_item(wood_display, MachineSource(item=str(item), machine=ModMachine.preservation_chamber))
+ content.source_item(hardwood_display, MachineSource(item=str(item), machine=ModMachine.hardwood_preservation_chamber))
+
+
+register_mod_content_pack(ArchaeologyContentPack(
+ ModNames.archaeology,
+ skills=(Skill(name=ModSkill.archaeology, has_mastery=False),),
+
+))
diff --git a/worlds/stardew_valley/content/mods/big_backpack.py b/worlds/stardew_valley/content/mods/big_backpack.py
new file mode 100644
index 000000000000..27b4ea1f816c
--- /dev/null
+++ b/worlds/stardew_valley/content/mods/big_backpack.py
@@ -0,0 +1,7 @@
+from ..game_content import ContentPack
+from ..mod_registry import register_mod_content_pack
+from ...mods.mod_data import ModNames
+
+register_mod_content_pack(ContentPack(
+ ModNames.big_backpack,
+))
diff --git a/worlds/stardew_valley/content/mods/boarding_house.py b/worlds/stardew_valley/content/mods/boarding_house.py
new file mode 100644
index 000000000000..f3ad138fa7c2
--- /dev/null
+++ b/worlds/stardew_valley/content/mods/boarding_house.py
@@ -0,0 +1,13 @@
+from ..game_content import ContentPack
+from ..mod_registry import register_mod_content_pack
+from ...data import villagers_data
+from ...mods.mod_data import ModNames
+
+register_mod_content_pack(ContentPack(
+ ModNames.boarding_house,
+ villagers=(
+ villagers_data.gregory,
+ villagers_data.sheila,
+ villagers_data.joel,
+ )
+))
diff --git a/worlds/stardew_valley/content/mods/deepwoods.py b/worlds/stardew_valley/content/mods/deepwoods.py
new file mode 100644
index 000000000000..a78629da57c0
--- /dev/null
+++ b/worlds/stardew_valley/content/mods/deepwoods.py
@@ -0,0 +1,28 @@
+from ..game_content import ContentPack
+from ..mod_registry import register_mod_content_pack
+from ...data.harvest import ForagingSource
+from ...mods.mod_data import ModNames
+from ...strings.crop_names import Fruit
+from ...strings.flower_names import Flower
+from ...strings.region_names import DeepWoodsRegion
+from ...strings.season_names import Season
+
+register_mod_content_pack(ContentPack(
+ ModNames.deepwoods,
+ harvest_sources={
+ # Deep enough to have seen such a tree at least once
+ Fruit.apple: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),),
+ Fruit.apricot: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),),
+ Fruit.cherry: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),),
+ Fruit.orange: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),),
+ Fruit.peach: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),),
+ Fruit.pomegranate: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),),
+ Fruit.mango: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),),
+
+ Flower.tulip: (ForagingSource(seasons=Season.not_winter, regions=(DeepWoodsRegion.floor_10,)),),
+ Flower.blue_jazz: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),),
+ Flower.summer_spangle: (ForagingSource(seasons=Season.not_winter, regions=(DeepWoodsRegion.floor_10,)),),
+ Flower.poppy: (ForagingSource(seasons=Season.not_winter, regions=(DeepWoodsRegion.floor_10,)),),
+ Flower.fairy_rose: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),),
+ }
+))
diff --git a/worlds/stardew_valley/content/mods/distant_lands.py b/worlds/stardew_valley/content/mods/distant_lands.py
new file mode 100644
index 000000000000..c5614d130250
--- /dev/null
+++ b/worlds/stardew_valley/content/mods/distant_lands.py
@@ -0,0 +1,42 @@
+from ..game_content import ContentPack, StardewContent
+from ..mod_registry import register_mod_content_pack
+from ...data import villagers_data, fish_data
+from ...data.game_item import ItemTag, Tag
+from ...data.harvest import ForagingSource, HarvestCropSource
+from ...data.requirement import QuestRequirement
+from ...mods.mod_data import ModNames
+from ...strings.crop_names import DistantLandsCrop
+from ...strings.forageable_names import DistantLandsForageable
+from ...strings.quest_names import ModQuest
+from ...strings.region_names import Region
+from ...strings.season_names import Season
+from ...strings.seed_names import DistantLandsSeed
+
+
+class DistantLandsContentPack(ContentPack):
+
+ def harvest_source_hook(self, content: StardewContent):
+ content.untag_item(DistantLandsSeed.void_mint, tag=ItemTag.CROPSANITY_SEED)
+ content.untag_item(DistantLandsSeed.vile_ancient_fruit, tag=ItemTag.CROPSANITY_SEED)
+
+
+register_mod_content_pack(DistantLandsContentPack(
+ ModNames.distant_lands,
+ fishes=(
+ fish_data.void_minnow,
+ fish_data.purple_algae,
+ fish_data.swamp_leech,
+ fish_data.giant_horsehoe_crab,
+ ),
+ villagers=(
+ villagers_data.zic,
+ ),
+ harvest_sources={
+ DistantLandsForageable.swamp_herb: (ForagingSource(regions=(Region.witch_swamp,)),),
+ DistantLandsForageable.brown_amanita: (ForagingSource(regions=(Region.witch_swamp,)),),
+ DistantLandsSeed.void_mint: (ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.CorruptedCropsTask),)),),
+ DistantLandsCrop.void_mint: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=DistantLandsSeed.void_mint, seasons=(Season.spring, Season.summer, Season.fall)),),
+ DistantLandsSeed.vile_ancient_fruit: (ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.CorruptedCropsTask),)),),
+ DistantLandsCrop.vile_ancient_fruit: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=DistantLandsSeed.vile_ancient_fruit, seasons=(Season.spring, Season.summer, Season.fall)),)
+ }
+))
diff --git a/worlds/stardew_valley/content/mods/jasper.py b/worlds/stardew_valley/content/mods/jasper.py
new file mode 100644
index 000000000000..146b291d800a
--- /dev/null
+++ b/worlds/stardew_valley/content/mods/jasper.py
@@ -0,0 +1,14 @@
+from ..game_content import ContentPack
+from ..mod_registry import register_mod_content_pack
+from ..override import override
+from ...data import villagers_data
+from ...mods.mod_data import ModNames
+
+register_mod_content_pack(ContentPack(
+ ModNames.jasper,
+ villagers=(
+ villagers_data.jasper,
+ override(villagers_data.gunther, mod_name=ModNames.jasper),
+ override(villagers_data.marlon, mod_name=ModNames.jasper),
+ )
+))
diff --git a/worlds/stardew_valley/content/mods/magic.py b/worlds/stardew_valley/content/mods/magic.py
new file mode 100644
index 000000000000..aae3617cb00c
--- /dev/null
+++ b/worlds/stardew_valley/content/mods/magic.py
@@ -0,0 +1,10 @@
+from ..game_content import ContentPack
+from ..mod_registry import register_mod_content_pack
+from ...data.skill import Skill
+from ...mods.mod_data import ModNames
+from ...strings.skill_names import ModSkill
+
+register_mod_content_pack(ContentPack(
+ ModNames.magic,
+ skills=(Skill(name=ModSkill.magic, has_mastery=False),)
+))
diff --git a/worlds/stardew_valley/content/mods/npc_mods.py b/worlds/stardew_valley/content/mods/npc_mods.py
new file mode 100644
index 000000000000..52d97d5c52b7
--- /dev/null
+++ b/worlds/stardew_valley/content/mods/npc_mods.py
@@ -0,0 +1,81 @@
+from ..game_content import ContentPack
+from ..mod_registry import register_mod_content_pack
+from ...data import villagers_data
+from ...mods.mod_data import ModNames
+
+register_mod_content_pack(ContentPack(
+ ModNames.alec,
+ villagers=(
+ villagers_data.alec,
+ )
+))
+
+register_mod_content_pack(ContentPack(
+ ModNames.ayeisha,
+ villagers=(
+ villagers_data.ayeisha,
+ )
+))
+
+register_mod_content_pack(ContentPack(
+ ModNames.delores,
+ villagers=(
+ villagers_data.delores,
+ )
+))
+
+register_mod_content_pack(ContentPack(
+ ModNames.eugene,
+ villagers=(
+ villagers_data.eugene,
+ )
+))
+
+register_mod_content_pack(ContentPack(
+ ModNames.juna,
+ villagers=(
+ villagers_data.juna,
+ )
+))
+
+register_mod_content_pack(ContentPack(
+ ModNames.ginger,
+ villagers=(
+ villagers_data.kitty,
+ )
+))
+
+register_mod_content_pack(ContentPack(
+ ModNames.shiko,
+ villagers=(
+ villagers_data.shiko,
+ )
+))
+
+register_mod_content_pack(ContentPack(
+ ModNames.wellwick,
+ villagers=(
+ villagers_data.wellwick,
+ )
+))
+
+register_mod_content_pack(ContentPack(
+ ModNames.yoba,
+ villagers=(
+ villagers_data.yoba,
+ )
+))
+
+register_mod_content_pack(ContentPack(
+ ModNames.riley,
+ villagers=(
+ villagers_data.riley,
+ )
+))
+
+register_mod_content_pack(ContentPack(
+ ModNames.lacey,
+ villagers=(
+ villagers_data.lacey,
+ )
+))
diff --git a/worlds/stardew_valley/content/mods/skill_mods.py b/worlds/stardew_valley/content/mods/skill_mods.py
new file mode 100644
index 000000000000..7f88b2ebf2dc
--- /dev/null
+++ b/worlds/stardew_valley/content/mods/skill_mods.py
@@ -0,0 +1,25 @@
+from ..game_content import ContentPack
+from ..mod_registry import register_mod_content_pack
+from ...data.skill import Skill
+from ...mods.mod_data import ModNames
+from ...strings.skill_names import ModSkill
+
+register_mod_content_pack(ContentPack(
+ ModNames.luck_skill,
+ skills=(Skill(name=ModSkill.luck, has_mastery=False),)
+))
+
+register_mod_content_pack(ContentPack(
+ ModNames.socializing_skill,
+ skills=(Skill(name=ModSkill.socializing, has_mastery=False),)
+))
+
+register_mod_content_pack(ContentPack(
+ ModNames.cooking_skill,
+ skills=(Skill(name=ModSkill.cooking, has_mastery=False),)
+))
+
+register_mod_content_pack(ContentPack(
+ ModNames.binning_skill,
+ skills=(Skill(name=ModSkill.binning, has_mastery=False),)
+))
diff --git a/worlds/stardew_valley/content/mods/skull_cavern_elevator.py b/worlds/stardew_valley/content/mods/skull_cavern_elevator.py
new file mode 100644
index 000000000000..ff8c089608e5
--- /dev/null
+++ b/worlds/stardew_valley/content/mods/skull_cavern_elevator.py
@@ -0,0 +1,7 @@
+from ..game_content import ContentPack
+from ..mod_registry import register_mod_content_pack
+from ...mods.mod_data import ModNames
+
+register_mod_content_pack(ContentPack(
+ ModNames.skull_cavern_elevator,
+))
diff --git a/worlds/stardew_valley/content/mods/sve.py b/worlds/stardew_valley/content/mods/sve.py
new file mode 100644
index 000000000000..a68d4ae9c097
--- /dev/null
+++ b/worlds/stardew_valley/content/mods/sve.py
@@ -0,0 +1,210 @@
+from ..game_content import ContentPack, StardewContent
+from ..mod_registry import register_mod_content_pack
+from ..override import override
+from ..vanilla.ginger_island import ginger_island_content_pack as ginger_island_content_pack
+from ...data import villagers_data, fish_data
+from ...data.game_item import ItemTag, Tag
+from ...data.harvest import ForagingSource, HarvestCropSource
+from ...data.requirement import YearRequirement, CombatRequirement, RelationshipRequirement, ToolRequirement, SkillRequirement, FishingRequirement
+from ...data.shop import ShopSource
+from ...mods.mod_data import ModNames
+from ...strings.craftable_names import ModEdible
+from ...strings.crop_names import Fruit, SVEVegetable, SVEFruit
+from ...strings.fish_names import WaterItem, SVEFish, SVEWaterItem
+from ...strings.flower_names import Flower
+from ...strings.food_names import SVEMeal, SVEBeverage
+from ...strings.forageable_names import Mushroom, Forageable, SVEForage
+from ...strings.gift_names import SVEGift
+from ...strings.metal_names import Ore
+from ...strings.monster_drop_names import ModLoot, Loot
+from ...strings.performance_names import Performance
+from ...strings.region_names import Region, SVERegion, LogicRegion
+from ...strings.season_names import Season
+from ...strings.seed_names import SVESeed
+from ...strings.skill_names import Skill
+from ...strings.tool_names import Tool, ToolMaterial
+from ...strings.villager_names import ModNPC
+
+
+class SVEContentPack(ContentPack):
+
+ def fish_hook(self, content: StardewContent):
+ if ginger_island_content_pack.name not in content.registered_packs:
+ content.fishes.pop(fish_data.baby_lunaloo.name)
+ content.fishes.pop(fish_data.clownfish.name)
+ content.fishes.pop(fish_data.lunaloo.name)
+ content.fishes.pop(fish_data.seahorse.name)
+ content.fishes.pop(fish_data.shiny_lunaloo.name)
+ content.fishes.pop(fish_data.starfish.name)
+ content.fishes.pop(fish_data.sea_sponge.name)
+
+ # Remove Highlands fishes at it requires 2 Lance hearts for the quest to access it
+ content.fishes.pop(fish_data.daggerfish.name)
+ content.fishes.pop(fish_data.gemfish.name)
+
+ # Remove Fable Reef fishes at it requires 8 Lance hearts for the event to access it
+ content.fishes.pop(fish_data.torpedo_trout.name)
+
+ def villager_hook(self, content: StardewContent):
+ if ginger_island_content_pack.name not in content.registered_packs:
+ # Remove Lance if Ginger Island is not in content since he is first encountered in Volcano Forge
+ content.villagers.pop(villagers_data.lance.name)
+
+ def harvest_source_hook(self, content: StardewContent):
+ content.untag_item(SVESeed.shrub, tag=ItemTag.CROPSANITY_SEED)
+ content.untag_item(SVESeed.fungus, tag=ItemTag.CROPSANITY_SEED)
+ content.untag_item(SVESeed.slime, tag=ItemTag.CROPSANITY_SEED)
+ content.untag_item(SVESeed.stalk, tag=ItemTag.CROPSANITY_SEED)
+ content.untag_item(SVESeed.void, tag=ItemTag.CROPSANITY_SEED)
+ content.untag_item(SVESeed.ancient_fern, tag=ItemTag.CROPSANITY_SEED)
+ if ginger_island_content_pack.name not in content.registered_packs:
+ # Remove Highlands seeds as these are behind Lance existing.
+ content.game_items.pop(SVESeed.void)
+ content.game_items.pop(SVEVegetable.void_root)
+ content.game_items.pop(SVESeed.stalk)
+ content.game_items.pop(SVEFruit.monster_fruit)
+ content.game_items.pop(SVESeed.fungus)
+ content.game_items.pop(SVEVegetable.monster_mushroom)
+ content.game_items.pop(SVESeed.slime)
+ content.game_items.pop(SVEFruit.slime_berry)
+
+
+register_mod_content_pack(SVEContentPack(
+ ModNames.sve,
+ weak_dependencies=(
+ ginger_island_content_pack.name,
+ ModNames.jasper, # To override Marlon and Gunther
+ ),
+ shop_sources={
+ SVEGift.aged_blue_moon_wine: (ShopSource(money_price=28000, shop_region=SVERegion.blue_moon_vineyard),),
+ SVEGift.blue_moon_wine: (ShopSource(money_price=3000, shop_region=SVERegion.blue_moon_vineyard),),
+ ModEdible.lightning_elixir: (ShopSource(money_price=12000, shop_region=SVERegion.galmoran_outpost),),
+ ModEdible.barbarian_elixir: (ShopSource(money_price=22000, shop_region=SVERegion.galmoran_outpost),),
+ ModEdible.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),),
+ SVEMeal.grampleton_orange_chicken: (ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),),
+ ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),),
+ ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),),
+ SVEBeverage.sports_drink: (ShopSource(money_price=750, shop_region=Region.hospital),),
+ SVEMeal.stamina_capsule: (ShopSource(money_price=4000, shop_region=Region.hospital),),
+ },
+ harvest_sources={
+ Mushroom.red: (
+ ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.summer, Season.fall)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), )
+ ),
+ Mushroom.purple: (
+ ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave, SVERegion.junimo_woods), )
+ ),
+ Mushroom.morel: (
+ ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), )
+ ),
+ Mushroom.chanterelle: (
+ ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), )
+ ),
+ Flower.tulip: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring,)),),
+ Flower.blue_jazz: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring,)),),
+ Flower.summer_spangle: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.summer,)),),
+ Flower.sunflower: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.summer,)),),
+ Flower.fairy_rose: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.fall,)),),
+ Fruit.ancient_fruit: (
+ ForagingSource(regions=(SVERegion.sprite_spring,), seasons=Season.not_winter, other_requirements=(YearRequirement(3),)),
+ ForagingSource(regions=(SVERegion.sprite_spring_cave,)),
+ ),
+ Fruit.sweet_gem_berry: (
+ ForagingSource(regions=(SVERegion.sprite_spring,), seasons=Season.not_winter, other_requirements=(YearRequirement(3),)),
+ ),
+
+ # New items
+
+ ModLoot.green_mushroom: (ForagingSource(regions=(SVERegion.highlands_pond,), seasons=Season.not_winter),),
+ ModLoot.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,),
+ other_requirements=(CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),),
+ ModLoot.swirl_stone: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.galaxy),)),),
+ ModLoot.void_soul: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.good),)),),
+ SVEForage.winter_star_rose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.winter,)),),
+ SVEForage.bearberry: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.winter,)),),
+ SVEForage.poison_mushroom: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.summer, Season.fall)),),
+ SVEForage.red_baneberry: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.summer, Season.summer)),),
+ SVEForage.ferngill_primrose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.spring,)),),
+ SVEForage.goldenrod: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.summer, Season.fall)),),
+ SVEForage.conch: (ForagingSource(regions=(Region.beach, SVERegion.fable_reef,)),),
+ SVEForage.dewdrop_berry: (ForagingSource(regions=(SVERegion.enchanted_grove,)),),
+ SVEForage.sand_dollar: (ForagingSource(regions=(Region.beach, SVERegion.fable_reef,), seasons=(Season.spring, Season.summer)),),
+ SVEForage.golden_ocean_flower: (ForagingSource(regions=(SVERegion.fable_reef,)),),
+ SVEForage.four_leaf_clover: (ForagingSource(regions=(Region.secret_woods, SVERegion.forest_west,), seasons=(Season.summer, Season.fall)),),
+ SVEForage.mushroom_colony: (ForagingSource(regions=(Region.secret_woods, SVERegion.junimo_woods, SVERegion.forest_west,), seasons=(Season.fall,)),),
+ SVEForage.rusty_blade: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),),
+ SVEForage.rafflesia: (ForagingSource(regions=(Region.secret_woods,), seasons=Season.not_winter),),
+ SVEForage.thistle: (ForagingSource(regions=(SVERegion.summit,)),),
+ ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),),
+ ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,),
+ other_requirements=(CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),),
+ SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),),
+
+ # Fable Reef
+ WaterItem.coral: (ForagingSource(regions=(SVERegion.fable_reef,)),),
+ Forageable.rainbow_shell: (ForagingSource(regions=(SVERegion.fable_reef,)),),
+ WaterItem.sea_urchin: (ForagingSource(regions=(SVERegion.fable_reef,)),),
+
+ # Crops
+ SVESeed.shrub: (ForagingSource(regions=(Region.secret_woods,), other_requirements=(CombatRequirement(Performance.good),)),),
+ SVEFruit.salal_berry: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.shrub, seasons=(Season.spring,)),),
+ SVESeed.slime: (ForagingSource(regions=(SVERegion.highlands_outside,), other_requirements=(CombatRequirement(Performance.good),)),),
+ SVEFruit.slime_berry: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.slime, seasons=(Season.spring,)),),
+ SVESeed.ancient_fern: (ForagingSource(regions=(Region.secret_woods,)),),
+ SVEVegetable.ancient_fiber: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.ancient_fern, seasons=(Season.summer,)),),
+ SVESeed.stalk: (ForagingSource(regions=(SVERegion.highlands_outside,), other_requirements=(CombatRequirement(Performance.good),)),),
+ SVEFruit.monster_fruit: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.stalk, seasons=(Season.summer,)),),
+ SVESeed.fungus: (ForagingSource(regions=(SVERegion.highlands_pond,), other_requirements=(CombatRequirement(Performance.good),)),),
+ SVEVegetable.monster_mushroom: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.fungus, seasons=(Season.fall,)),),
+ SVESeed.void: (ForagingSource(regions=(SVERegion.highlands_cavern,), other_requirements=(CombatRequirement(Performance.good),)),),
+ SVEVegetable.void_root: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.void, seasons=(Season.winter,)),),
+
+ },
+ fishes=(
+ fish_data.baby_lunaloo, # Removed when no ginger island
+ fish_data.bonefish,
+ fish_data.bull_trout,
+ fish_data.butterfish,
+ fish_data.clownfish, # Removed when no ginger island
+ fish_data.daggerfish,
+ fish_data.frog,
+ fish_data.gemfish,
+ fish_data.goldenfish,
+ fish_data.grass_carp,
+ fish_data.king_salmon,
+ fish_data.kittyfish,
+ fish_data.lunaloo, # Removed when no ginger island
+ fish_data.meteor_carp,
+ fish_data.minnow,
+ fish_data.puppyfish,
+ fish_data.radioactive_bass,
+ fish_data.seahorse, # Removed when no ginger island
+ fish_data.shiny_lunaloo, # Removed when no ginger island
+ fish_data.snatcher_worm,
+ fish_data.starfish, # Removed when no ginger island
+ fish_data.torpedo_trout,
+ fish_data.undeadfish,
+ fish_data.void_eel,
+ fish_data.water_grub,
+ fish_data.sea_sponge, # Removed when no ginger island
+
+ ),
+ villagers=(
+ villagers_data.claire,
+ villagers_data.lance, # Removed when no ginger island
+ villagers_data.mommy,
+ villagers_data.sophia,
+ villagers_data.victor,
+ villagers_data.andy,
+ villagers_data.apples,
+ villagers_data.gunther,
+ villagers_data.martin,
+ villagers_data.marlon,
+ villagers_data.morgan,
+ villagers_data.scarlett,
+ villagers_data.susan,
+ villagers_data.morris,
+ # The wizard leaves his tower on sunday, for like 1 hour... Good enough for entrance rando!
+ override(villagers_data.wizard, locations=(Region.wizard_tower, Region.forest), bachelor=True, mod_name=ModNames.sve),
+ )
+))
diff --git a/worlds/stardew_valley/content/mods/tractor.py b/worlds/stardew_valley/content/mods/tractor.py
new file mode 100644
index 000000000000..8f143001791c
--- /dev/null
+++ b/worlds/stardew_valley/content/mods/tractor.py
@@ -0,0 +1,7 @@
+from ..game_content import ContentPack
+from ..mod_registry import register_mod_content_pack
+from ...mods.mod_data import ModNames
+
+register_mod_content_pack(ContentPack(
+ ModNames.tractor,
+))
diff --git a/worlds/stardew_valley/content/override.py b/worlds/stardew_valley/content/override.py
new file mode 100644
index 000000000000..adfc64c95b49
--- /dev/null
+++ b/worlds/stardew_valley/content/override.py
@@ -0,0 +1,7 @@
+from typing import Any
+
+
+def override(content: Any, **kwargs) -> Any:
+ attributes = dict(content.__dict__)
+ attributes.update(kwargs)
+ return type(content)(**attributes)
diff --git a/worlds/stardew_valley/content/unpacking.py b/worlds/stardew_valley/content/unpacking.py
new file mode 100644
index 000000000000..f069866d56cd
--- /dev/null
+++ b/worlds/stardew_valley/content/unpacking.py
@@ -0,0 +1,97 @@
+from __future__ import annotations
+
+from typing import Iterable, Mapping, Callable
+
+from .game_content import StardewContent, ContentPack, StardewFeatures
+from .vanilla.base import base_game as base_game_content_pack
+from ..data.game_item import GameItem, ItemSource
+
+try:
+ from graphlib import TopologicalSorter
+except ImportError:
+ from graphlib_backport import TopologicalSorter # noqa
+
+
+def unpack_content(features: StardewFeatures, packs: Iterable[ContentPack]) -> StardewContent:
+ # Base game is always registered first.
+ content = StardewContent(features)
+ packs_to_finalize = [base_game_content_pack]
+ register_pack(content, base_game_content_pack)
+
+ # Content packs are added in order based on their dependencies
+ sorter = TopologicalSorter()
+ packs_by_name = {p.name: p for p in packs}
+
+ # Build the dependency graph
+ for name, pack in packs_by_name.items():
+ sorter.add(name,
+ *pack.dependencies,
+ *(wd for wd in pack.weak_dependencies if wd in packs_by_name))
+
+ # Graph is traversed in BFS
+ sorter.prepare()
+ while sorter.is_active():
+ # Packs get shuffled in TopologicalSorter, most likely due to hash seeding.
+ for pack_name in sorted(sorter.get_ready()):
+ pack = packs_by_name[pack_name]
+ register_pack(content, pack)
+ sorter.done(pack_name)
+ packs_to_finalize.append(pack)
+
+ prune_inaccessible_items(content)
+
+ for pack in packs_to_finalize:
+ pack.finalize_hook(content)
+
+ # Maybe items without source should be removed at some point
+ return content
+
+
+def register_pack(content: StardewContent, pack: ContentPack):
+ # register regions
+
+ # register entrances
+
+ register_sources_and_call_hook(content, pack.harvest_sources, pack.harvest_source_hook)
+ register_sources_and_call_hook(content, pack.shop_sources, pack.shop_source_hook)
+ register_sources_and_call_hook(content, pack.crafting_sources, pack.crafting_hook)
+ register_sources_and_call_hook(content, pack.artisan_good_sources, pack.artisan_good_hook)
+
+ for fish in pack.fishes:
+ content.fishes[fish.name] = fish
+ pack.fish_hook(content)
+
+ for villager in pack.villagers:
+ content.villagers[villager.name] = villager
+ pack.villager_hook(content)
+
+ for skill in pack.skills:
+ content.skills[skill.name] = skill
+ pack.skill_hook(content)
+
+ # register_quests
+
+ # ...
+
+ content.registered_packs.add(pack.name)
+
+
+def register_sources_and_call_hook(content: StardewContent,
+ sources_by_item_name: Mapping[str, Iterable[ItemSource]],
+ hook: Callable[[StardewContent], None]):
+ for item_name, sources in sources_by_item_name.items():
+ item = content.game_items.setdefault(item_name, GameItem(item_name))
+ item.add_sources(sources)
+
+ for source in sources:
+ for requirement_name, tags in source.requirement_tags.items():
+ requirement_item = content.game_items.setdefault(requirement_name, GameItem(requirement_name))
+ requirement_item.add_tags(tags)
+
+ hook(content)
+
+
+def prune_inaccessible_items(content: StardewContent):
+ for item in list(content.game_items.values()):
+ if not item.sources:
+ content.game_items.pop(item.name)
diff --git a/worlds/stardew_valley/content/vanilla/__init__.py b/worlds/stardew_valley/content/vanilla/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/stardew_valley/content/vanilla/base.py b/worlds/stardew_valley/content/vanilla/base.py
new file mode 100644
index 000000000000..2c910df5d00f
--- /dev/null
+++ b/worlds/stardew_valley/content/vanilla/base.py
@@ -0,0 +1,172 @@
+from ..game_content import ContentPack, StardewContent
+from ...data.artisan import MachineSource
+from ...data.game_item import ItemTag, CustomRuleSource, GameItem
+from ...data.harvest import HarvestFruitTreeSource, HarvestCropSource
+from ...data.skill import Skill
+from ...strings.artisan_good_names import ArtisanGood
+from ...strings.craftable_names import WildSeeds
+from ...strings.crop_names import Fruit, Vegetable
+from ...strings.flower_names import Flower
+from ...strings.food_names import Beverage
+from ...strings.forageable_names import all_edible_mushrooms, Mushroom, Forageable
+from ...strings.fruit_tree_names import Sapling
+from ...strings.machine_names import Machine
+from ...strings.monster_names import Monster
+from ...strings.season_names import Season
+from ...strings.seed_names import Seed
+from ...strings.skill_names import Skill as SkillName
+
+all_fruits = (
+ Fruit.ancient_fruit, Fruit.apple, Fruit.apricot, Fruit.banana, Forageable.blackberry, Fruit.blueberry, Forageable.cactus_fruit, Fruit.cherry,
+ Forageable.coconut, Fruit.cranberries, Forageable.crystal_fruit, Fruit.grape, Fruit.hot_pepper, Fruit.mango, Fruit.melon, Fruit.orange, Fruit.peach,
+ Fruit.pineapple, Fruit.pomegranate, Fruit.powdermelon, Fruit.qi_fruit, Fruit.rhubarb, Forageable.salmonberry, Forageable.spice_berry, Fruit.starfruit,
+ Fruit.strawberry
+)
+
+all_vegetables = (
+ Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.bok_choy, Vegetable.broccoli, Vegetable.carrot, Vegetable.cauliflower,
+ Vegetable.corn, Vegetable.eggplant, Forageable.fiddlehead_fern, Vegetable.garlic, Vegetable.green_bean, Vegetable.hops, Vegetable.kale,
+ Vegetable.parsnip, Vegetable.potato, Vegetable.pumpkin, Vegetable.radish, Vegetable.red_cabbage, Vegetable.summer_squash, Vegetable.taro_root,
+ Vegetable.tea_leaves, Vegetable.tomato, Vegetable.unmilled_rice, Vegetable.wheat, Vegetable.yam
+)
+
+non_juiceable_vegetables = (Vegetable.hops, Vegetable.tea_leaves, Vegetable.wheat, Vegetable.tea_leaves)
+
+
+# This will hold items, skills and stuff that is available everywhere across the game, but not directly needing pelican town (crops, ore, foraging, etc.)
+class BaseGameContentPack(ContentPack):
+
+ def harvest_source_hook(self, content: StardewContent):
+ coffee_starter = content.game_items[Seed.coffee_starter]
+ content.game_items[Seed.coffee_starter] = GameItem(Seed.coffee, sources=coffee_starter.sources, tags=coffee_starter.tags)
+
+ content.untag_item(WildSeeds.ancient, ItemTag.CROPSANITY_SEED)
+
+ for fruit in all_fruits:
+ content.tag_item(fruit, ItemTag.FRUIT)
+
+ for vegetable in all_vegetables:
+ content.tag_item(vegetable, ItemTag.VEGETABLE)
+
+ for edible_mushroom in all_edible_mushrooms:
+ if edible_mushroom == Mushroom.magma_cap:
+ continue
+
+ content.tag_item(edible_mushroom, ItemTag.EDIBLE_MUSHROOM)
+
+ def finalize_hook(self, content: StardewContent):
+ # FIXME I hate this design. A listener design pattern would be more appropriate so artisan good are register at the exact moment a FRUIT tag is added.
+ for fruit in tuple(content.find_tagged_items(ItemTag.FRUIT)):
+ wine = ArtisanGood.specific_wine(fruit.name)
+ content.source_item(wine, MachineSource(item=fruit.name, machine=Machine.keg))
+ content.source_item(ArtisanGood.wine, MachineSource(item=fruit.name, machine=Machine.keg))
+
+ if fruit.name == Fruit.grape:
+ content.source_item(ArtisanGood.raisins, MachineSource(item=fruit.name, machine=Machine.dehydrator))
+ else:
+ dried_fruit = ArtisanGood.specific_dried_fruit(fruit.name)
+ content.source_item(dried_fruit, MachineSource(item=fruit.name, machine=Machine.dehydrator))
+ content.source_item(ArtisanGood.dried_fruit, MachineSource(item=fruit.name, machine=Machine.dehydrator))
+
+ jelly = ArtisanGood.specific_jelly(fruit.name)
+ content.source_item(jelly, MachineSource(item=fruit.name, machine=Machine.preserves_jar))
+ content.source_item(ArtisanGood.jelly, MachineSource(item=fruit.name, machine=Machine.preserves_jar))
+
+ for vegetable in tuple(content.find_tagged_items(ItemTag.VEGETABLE)):
+ if vegetable.name not in non_juiceable_vegetables:
+ juice = ArtisanGood.specific_juice(vegetable.name)
+ content.source_item(juice, MachineSource(item=vegetable.name, machine=Machine.keg))
+ content.source_item(ArtisanGood.juice, MachineSource(item=vegetable.name, machine=Machine.keg))
+
+ pickles = ArtisanGood.specific_pickles(vegetable.name)
+ content.source_item(pickles, MachineSource(item=vegetable.name, machine=Machine.preserves_jar))
+ content.source_item(ArtisanGood.pickles, MachineSource(item=vegetable.name, machine=Machine.preserves_jar))
+
+ for mushroom in tuple(content.find_tagged_items(ItemTag.EDIBLE_MUSHROOM)):
+ dried_mushroom = ArtisanGood.specific_dried_mushroom(mushroom.name)
+ content.source_item(dried_mushroom, MachineSource(item=mushroom.name, machine=Machine.dehydrator))
+ content.source_item(ArtisanGood.dried_mushroom, MachineSource(item=mushroom.name, machine=Machine.dehydrator))
+
+ # for fish in tuple(content.find_tagged_items(ItemTag.FISH)):
+ # smoked_fish = ArtisanGood.specific_smoked_fish(fish.name)
+ # content.source_item(smoked_fish, MachineSource(item=fish.name, machine=Machine.fish_smoker))
+ # content.source_item(ArtisanGood.smoked_fish, MachineSource(item=fish.name, machine=Machine.fish_smoker))
+
+
+base_game = BaseGameContentPack(
+ "Base game (Vanilla)",
+ harvest_sources={
+ # Fruit tree
+ Fruit.apple: (HarvestFruitTreeSource(sapling=Sapling.apple, seasons=(Season.fall,)),),
+ Fruit.apricot: (HarvestFruitTreeSource(sapling=Sapling.apricot, seasons=(Season.spring,)),),
+ Fruit.cherry: (HarvestFruitTreeSource(sapling=Sapling.cherry, seasons=(Season.spring,)),),
+ Fruit.orange: (HarvestFruitTreeSource(sapling=Sapling.orange, seasons=(Season.summer,)),),
+ Fruit.peach: (HarvestFruitTreeSource(sapling=Sapling.peach, seasons=(Season.summer,)),),
+ Fruit.pomegranate: (HarvestFruitTreeSource(sapling=Sapling.pomegranate, seasons=(Season.fall,)),),
+
+ # Crops
+ Vegetable.parsnip: (HarvestCropSource(seed=Seed.parsnip, seasons=(Season.spring,)),),
+ Vegetable.green_bean: (HarvestCropSource(seed=Seed.bean, seasons=(Season.spring,)),),
+ Vegetable.cauliflower: (HarvestCropSource(seed=Seed.cauliflower, seasons=(Season.spring,)),),
+ Vegetable.potato: (HarvestCropSource(seed=Seed.potato, seasons=(Season.spring,)),),
+ Flower.tulip: (HarvestCropSource(seed=Seed.tulip, seasons=(Season.spring,)),),
+ Vegetable.kale: (HarvestCropSource(seed=Seed.kale, seasons=(Season.spring,)),),
+ Flower.blue_jazz: (HarvestCropSource(seed=Seed.jazz, seasons=(Season.spring,)),),
+ Vegetable.garlic: (HarvestCropSource(seed=Seed.garlic, seasons=(Season.spring,)),),
+ Vegetable.unmilled_rice: (HarvestCropSource(seed=Seed.rice, seasons=(Season.spring,)),),
+
+ Fruit.melon: (HarvestCropSource(seed=Seed.melon, seasons=(Season.summer,)),),
+ Vegetable.tomato: (HarvestCropSource(seed=Seed.tomato, seasons=(Season.summer,)),),
+ Fruit.blueberry: (HarvestCropSource(seed=Seed.blueberry, seasons=(Season.summer,)),),
+ Fruit.hot_pepper: (HarvestCropSource(seed=Seed.pepper, seasons=(Season.summer,)),),
+ Vegetable.wheat: (HarvestCropSource(seed=Seed.wheat, seasons=(Season.summer, Season.fall)),),
+ Vegetable.radish: (HarvestCropSource(seed=Seed.radish, seasons=(Season.summer,)),),
+ Flower.poppy: (HarvestCropSource(seed=Seed.poppy, seasons=(Season.summer,)),),
+ Flower.summer_spangle: (HarvestCropSource(seed=Seed.spangle, seasons=(Season.summer,)),),
+ Vegetable.hops: (HarvestCropSource(seed=Seed.hops, seasons=(Season.summer,)),),
+ Vegetable.corn: (HarvestCropSource(seed=Seed.corn, seasons=(Season.summer, Season.fall)),),
+ Flower.sunflower: (HarvestCropSource(seed=Seed.sunflower, seasons=(Season.summer, Season.fall)),),
+ Vegetable.red_cabbage: (HarvestCropSource(seed=Seed.red_cabbage, seasons=(Season.summer,)),),
+
+ Vegetable.eggplant: (HarvestCropSource(seed=Seed.eggplant, seasons=(Season.fall,)),),
+ Vegetable.pumpkin: (HarvestCropSource(seed=Seed.pumpkin, seasons=(Season.fall,)),),
+ Vegetable.bok_choy: (HarvestCropSource(seed=Seed.bok_choy, seasons=(Season.fall,)),),
+ Vegetable.yam: (HarvestCropSource(seed=Seed.yam, seasons=(Season.fall,)),),
+ Fruit.cranberries: (HarvestCropSource(seed=Seed.cranberry, seasons=(Season.fall,)),),
+ Flower.fairy_rose: (HarvestCropSource(seed=Seed.fairy, seasons=(Season.fall,)),),
+ Vegetable.amaranth: (HarvestCropSource(seed=Seed.amaranth, seasons=(Season.fall,)),),
+ Fruit.grape: (HarvestCropSource(seed=Seed.grape, seasons=(Season.fall,)),),
+ Vegetable.artichoke: (HarvestCropSource(seed=Seed.artichoke, seasons=(Season.fall,)),),
+
+ Vegetable.broccoli: (HarvestCropSource(seed=Seed.broccoli, seasons=(Season.fall,)),),
+ Vegetable.carrot: (HarvestCropSource(seed=Seed.carrot, seasons=(Season.spring,)),),
+ Fruit.powdermelon: (HarvestCropSource(seed=Seed.powdermelon, seasons=(Season.summer,)),),
+ Vegetable.summer_squash: (HarvestCropSource(seed=Seed.summer_squash, seasons=(Season.summer,)),),
+
+ Fruit.strawberry: (HarvestCropSource(seed=Seed.strawberry, seasons=(Season.spring,)),),
+ Fruit.sweet_gem_berry: (HarvestCropSource(seed=Seed.rare_seed, seasons=(Season.fall,)),),
+ Fruit.ancient_fruit: (HarvestCropSource(seed=WildSeeds.ancient, seasons=(Season.spring, Season.summer, Season.fall,)),),
+
+ Seed.coffee_starter: (CustomRuleSource(lambda logic: logic.traveling_merchant.has_days(3) & logic.monster.can_kill_many(Monster.dust_sprite)),),
+ Seed.coffee: (HarvestCropSource(seed=Seed.coffee_starter, seasons=(Season.spring, Season.summer,)),),
+
+ Vegetable.tea_leaves: (CustomRuleSource(lambda logic: logic.has(Sapling.tea) & logic.time.has_lived_months(2) & logic.season.has_any_not_winter()),),
+ },
+ artisan_good_sources={
+ Beverage.beer: (MachineSource(item=Vegetable.wheat, machine=Machine.keg),),
+ # Ingredient.vinegar: (MachineSource(item=Ingredient.rice, machine=Machine.keg),),
+ Beverage.coffee: (MachineSource(item=Seed.coffee, machine=Machine.keg),
+ CustomRuleSource(lambda logic: logic.has(Machine.coffee_maker)),
+ CustomRuleSource(lambda logic: logic.has("Hot Java Ring"))),
+ ArtisanGood.green_tea: (MachineSource(item=Vegetable.tea_leaves, machine=Machine.keg),),
+ ArtisanGood.mead: (MachineSource(item=ArtisanGood.honey, machine=Machine.keg),),
+ ArtisanGood.pale_ale: (MachineSource(item=Vegetable.hops, machine=Machine.keg),),
+ },
+ skills=(
+ Skill(SkillName.farming, has_mastery=True),
+ Skill(SkillName.foraging, has_mastery=True),
+ Skill(SkillName.fishing, has_mastery=True),
+ Skill(SkillName.mining, has_mastery=True),
+ Skill(SkillName.combat, has_mastery=True),
+ )
+)
diff --git a/worlds/stardew_valley/content/vanilla/ginger_island.py b/worlds/stardew_valley/content/vanilla/ginger_island.py
new file mode 100644
index 000000000000..2fbcb032799e
--- /dev/null
+++ b/worlds/stardew_valley/content/vanilla/ginger_island.py
@@ -0,0 +1,85 @@
+from .pelican_town import pelican_town as pelican_town_content_pack
+from ..game_content import ContentPack, StardewContent
+from ...data import villagers_data, fish_data
+from ...data.game_item import ItemTag, Tag
+from ...data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource
+from ...data.requirement import WalnutRequirement
+from ...data.shop import ShopSource
+from ...strings.book_names import Book
+from ...strings.crop_names import Fruit, Vegetable
+from ...strings.fish_names import Fish
+from ...strings.forageable_names import Forageable, Mushroom
+from ...strings.fruit_tree_names import Sapling
+from ...strings.metal_names import Fossil, Mineral
+from ...strings.region_names import Region, LogicRegion
+from ...strings.season_names import Season
+from ...strings.seed_names import Seed
+
+
+class GingerIslandContentPack(ContentPack):
+
+ def harvest_source_hook(self, content: StardewContent):
+ content.tag_item(Fruit.banana, ItemTag.FRUIT)
+ content.tag_item(Fruit.pineapple, ItemTag.FRUIT)
+ content.tag_item(Fruit.mango, ItemTag.FRUIT)
+ content.tag_item(Vegetable.taro_root, ItemTag.VEGETABLE)
+ content.tag_item(Mushroom.magma_cap, ItemTag.EDIBLE_MUSHROOM)
+
+
+ginger_island_content_pack = GingerIslandContentPack(
+ "Ginger Island (Vanilla)",
+ weak_dependencies=(
+ pelican_town_content_pack.name,
+ ),
+ harvest_sources={
+ # Foraging
+ Forageable.dragon_tooth: (
+ ForagingSource(regions=(Region.volcano_floor_10,)),
+ ),
+ Forageable.ginger: (
+ ForagingSource(regions=(Region.island_west,)),
+ ),
+ Mushroom.magma_cap: (
+ ForagingSource(regions=(Region.volcano_floor_5,)),
+ ),
+
+ # Fruit tree
+ Fruit.banana: (HarvestFruitTreeSource(sapling=Sapling.banana, seasons=(Season.summer,)),),
+ Fruit.mango: (HarvestFruitTreeSource(sapling=Sapling.mango, seasons=(Season.summer,)),),
+
+ # Crop
+ Vegetable.taro_root: (HarvestCropSource(seed=Seed.taro, seasons=(Season.summer,)),),
+ Fruit.pineapple: (HarvestCropSource(seed=Seed.pineapple, seasons=(Season.summer,)),),
+
+ },
+ shop_sources={
+ Seed.taro: (ShopSource(items_price=((2, Fossil.bone_fragment),), shop_region=Region.island_trader),),
+ Seed.pineapple: (ShopSource(items_price=((1, Mushroom.magma_cap),), shop_region=Region.island_trader),),
+ Sapling.banana: (ShopSource(items_price=((5, Forageable.dragon_tooth),), shop_region=Region.island_trader),),
+ Sapling.mango: (ShopSource(items_price=((75, Fish.mussel_node),), shop_region=Region.island_trader),),
+
+ # This one is 10 diamonds, should maybe add time?
+ Book.the_diamond_hunter: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ ShopSource(items_price=((10, Mineral.diamond),), shop_region=Region.volcano_dwarf_shop),
+ ),
+ Book.queen_of_sauce_cookbook: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL),
+ ShopSource(money_price=50000, shop_region=LogicRegion.bookseller_2, other_requirements=(WalnutRequirement(100),)),), # Worst book ever
+
+ },
+ fishes=(
+ # TODO override region so no need to add inaccessible regions in logic
+ fish_data.blue_discus,
+ fish_data.lionfish,
+ fish_data.midnight_carp,
+ fish_data.pufferfish,
+ fish_data.stingray,
+ fish_data.super_cucumber,
+ fish_data.tilapia,
+ fish_data.tuna
+ ),
+ villagers=(
+ villagers_data.leo,
+ )
+)
diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py
new file mode 100644
index 000000000000..220b46eae2a4
--- /dev/null
+++ b/worlds/stardew_valley/content/vanilla/pelican_town.py
@@ -0,0 +1,390 @@
+from ..game_content import ContentPack
+from ...data import villagers_data, fish_data
+from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource
+from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource
+from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement
+from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
+from ...strings.book_names import Book
+from ...strings.crop_names import Fruit
+from ...strings.fish_names import WaterItem
+from ...strings.food_names import Beverage, Meal
+from ...strings.forageable_names import Forageable, Mushroom
+from ...strings.fruit_tree_names import Sapling
+from ...strings.generic_names import Generic
+from ...strings.material_names import Material
+from ...strings.region_names import Region, LogicRegion
+from ...strings.season_names import Season
+from ...strings.seed_names import Seed, TreeSeed
+from ...strings.skill_names import Skill
+from ...strings.tool_names import Tool, ToolMaterial
+
+pelican_town = ContentPack(
+ "Pelican Town (Vanilla)",
+ harvest_sources={
+ # Spring
+ Forageable.daffodil: (
+ ForagingSource(seasons=(Season.spring,), regions=(Region.bus_stop, Region.town, Region.railroad)),
+ ),
+ Forageable.dandelion: (
+ ForagingSource(seasons=(Season.spring,), regions=(Region.bus_stop, Region.forest, Region.railroad)),
+ ),
+ Forageable.leek: (
+ ForagingSource(seasons=(Season.spring,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.railroad)),
+ ),
+ Forageable.wild_horseradish: (
+ ForagingSource(seasons=(Season.spring,), regions=(Region.backwoods, Region.mountain, Region.forest, Region.secret_woods)),
+ ),
+ Forageable.salmonberry: (
+ SeasonalForagingSource(season=Season.spring, days=(15, 16, 17, 18),
+ regions=(Region.backwoods, Region.mountain, Region.town, Region.forest, Region.tunnel_entrance, Region.railroad)),
+ ),
+ Forageable.spring_onion: (
+ ForagingSource(seasons=(Season.spring,), regions=(Region.forest,)),
+ ),
+
+ # Summer
+ Fruit.grape: (
+ ForagingSource(seasons=(Season.summer,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.railroad)),
+ ),
+ Forageable.spice_berry: (
+ ForagingSource(seasons=(Season.summer,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.forest, Region.railroad)),
+ ),
+ Forageable.sweet_pea: (
+ ForagingSource(seasons=(Season.summer,), regions=(Region.bus_stop, Region.town, Region.forest, Region.railroad)),
+ ),
+ Forageable.fiddlehead_fern: (
+ ForagingSource(seasons=(Season.summer,), regions=(Region.secret_woods,)),
+ ),
+
+ # Fall
+ Forageable.blackberry: (
+ ForagingSource(seasons=(Season.fall,), regions=(Region.backwoods, Region.town, Region.forest, Region.railroad)),
+ SeasonalForagingSource(season=Season.fall, days=(8, 9, 10, 11),
+ regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.tunnel_entrance,
+ Region.railroad)),
+ ),
+ Forageable.hazelnut: (
+ ForagingSource(seasons=(Season.fall,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.railroad)),
+ ),
+ Forageable.wild_plum: (
+ ForagingSource(seasons=(Season.fall,), regions=(Region.mountain, Region.bus_stop, Region.railroad)),
+ ),
+
+ # Winter
+ Forageable.crocus: (
+ ForagingSource(seasons=(Season.winter,),
+ regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.secret_woods)),
+ ),
+ Forageable.crystal_fruit: (
+ ForagingSource(seasons=(Season.winter,),
+ regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad)),
+ ),
+ Forageable.holly: (
+ ForagingSource(seasons=(Season.winter,),
+ regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad)),
+ ),
+ Forageable.snow_yam: (
+ ForagingSource(seasons=(Season.winter,),
+ regions=(Region.farm, Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad,
+ Region.secret_woods, Region.beach),
+ other_requirements=(ToolRequirement(Tool.hoe),)),
+ ),
+ Forageable.winter_root: (
+ ForagingSource(seasons=(Season.winter,),
+ regions=(Region.farm, Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad,
+ Region.secret_woods, Region.beach),
+ other_requirements=(ToolRequirement(Tool.hoe),)),
+ ),
+
+ # Mushrooms
+ Mushroom.common: (
+ ForagingSource(seasons=(Season.spring,), regions=(Region.secret_woods,)),
+ ForagingSource(seasons=(Season.fall,), regions=(Region.backwoods, Region.mountain, Region.forest)),
+ ),
+ Mushroom.chanterelle: (
+ ForagingSource(seasons=(Season.fall,), regions=(Region.secret_woods,)),
+ ),
+ Mushroom.morel: (
+ ForagingSource(seasons=(Season.spring, Season.fall), regions=(Region.secret_woods,)),
+ ),
+ Mushroom.red: (
+ ForagingSource(seasons=(Season.summer, Season.fall), regions=(Region.secret_woods,)),
+ ),
+
+ # Beach
+ WaterItem.coral: (
+ ForagingSource(regions=(Region.tide_pools,)),
+ SeasonalForagingSource(season=Season.summer, days=(12, 13, 14), regions=(Region.beach,)),
+ ),
+ WaterItem.nautilus_shell: (
+ ForagingSource(seasons=(Season.winter,), regions=(Region.beach,)),
+ ),
+ Forageable.rainbow_shell: (
+ ForagingSource(seasons=(Season.summer,), regions=(Region.beach,)),
+ ),
+ WaterItem.sea_urchin: (
+ ForagingSource(regions=(Region.tide_pools,)),
+ ),
+
+ Seed.mixed: (
+ ForagingSource(seasons=(Season.spring, Season.summer, Season.fall,), regions=(Region.town, Region.farm, Region.forest)),
+ ),
+
+ Seed.mixed_flower: (
+ ForagingSource(seasons=(Season.summer,), regions=(Region.town, Region.farm, Region.forest)),
+ ),
+
+ # Books
+ Book.jack_be_nimble_jack_be_thick: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ ArtifactSpotSource(amount=22),), # After 22 spots, there are 50.48% chances player received the book.
+ Book.woodys_secret: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ GenericSource(regions=(Region.forest, Region.mountain),
+ other_requirements=(ToolRequirement(Tool.axe, ToolMaterial.iron), SkillRequirement(Skill.foraging, 5))),),
+ },
+ shop_sources={
+ # Saplings
+ Sapling.apple: (ShopSource(money_price=4000, shop_region=Region.pierre_store),),
+ Sapling.apricot: (ShopSource(money_price=2000, shop_region=Region.pierre_store),),
+ Sapling.cherry: (ShopSource(money_price=3400, shop_region=Region.pierre_store),),
+ Sapling.orange: (ShopSource(money_price=4000, shop_region=Region.pierre_store),),
+ Sapling.peach: (ShopSource(money_price=6000, shop_region=Region.pierre_store),),
+ Sapling.pomegranate: (ShopSource(money_price=6000, shop_region=Region.pierre_store),),
+
+ # Crop seeds, assuming they are bought in season, otherwise price is different with missing stock list.
+ Seed.parsnip: (ShopSource(money_price=20, shop_region=Region.pierre_store, seasons=(Season.spring,)),),
+ Seed.bean: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.spring,)),),
+ Seed.cauliflower: (ShopSource(money_price=80, shop_region=Region.pierre_store, seasons=(Season.spring,)),),
+ Seed.potato: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.spring,)),),
+ Seed.tulip: (ShopSource(money_price=20, shop_region=Region.pierre_store, seasons=(Season.spring,)),),
+ Seed.kale: (ShopSource(money_price=70, shop_region=Region.pierre_store, seasons=(Season.spring,)),),
+ Seed.jazz: (ShopSource(money_price=30, shop_region=Region.pierre_store, seasons=(Season.spring,)),),
+ Seed.garlic: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.spring,)),),
+ Seed.rice: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.spring,)),),
+
+ Seed.melon: (ShopSource(money_price=80, shop_region=Region.pierre_store, seasons=(Season.summer,)),),
+ Seed.tomato: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.summer,)),),
+ Seed.blueberry: (ShopSource(money_price=80, shop_region=Region.pierre_store, seasons=(Season.summer,)),),
+ Seed.pepper: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.summer,)),),
+ Seed.wheat: (ShopSource(money_price=10, shop_region=Region.pierre_store, seasons=(Season.summer, Season.fall)),),
+ Seed.radish: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.summer,)),),
+ Seed.poppy: (ShopSource(money_price=100, shop_region=Region.pierre_store, seasons=(Season.summer,)),),
+ Seed.spangle: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.summer,)),),
+ Seed.hops: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.summer,)),),
+ Seed.corn: (ShopSource(money_price=150, shop_region=Region.pierre_store, seasons=(Season.summer, Season.fall)),),
+ Seed.sunflower: (ShopSource(money_price=200, shop_region=Region.pierre_store, seasons=(Season.summer, Season.fall)),),
+ Seed.red_cabbage: (ShopSource(money_price=100, shop_region=Region.pierre_store, seasons=(Season.summer,)),),
+
+ Seed.eggplant: (ShopSource(money_price=20, shop_region=Region.pierre_store, seasons=(Season.fall,)),),
+ Seed.pumpkin: (ShopSource(money_price=100, shop_region=Region.pierre_store, seasons=(Season.fall,)),),
+ Seed.bok_choy: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.fall,)),),
+ Seed.yam: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.fall,)),),
+ Seed.cranberry: (ShopSource(money_price=240, shop_region=Region.pierre_store, seasons=(Season.fall,)),),
+ Seed.fairy: (ShopSource(money_price=200, shop_region=Region.pierre_store, seasons=(Season.fall,)),),
+ Seed.amaranth: (ShopSource(money_price=70, shop_region=Region.pierre_store, seasons=(Season.fall,)),),
+ Seed.grape: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.fall,)),),
+ Seed.artichoke: (ShopSource(money_price=30, shop_region=Region.pierre_store, seasons=(Season.fall,)),),
+
+ Seed.broccoli: (ShopSource(items_price=((5, Material.moss),), shop_region=LogicRegion.raccoon_shop),),
+ Seed.carrot: (ShopSource(items_price=((1, TreeSeed.maple),), shop_region=LogicRegion.raccoon_shop),),
+ Seed.powdermelon: (ShopSource(items_price=((2, TreeSeed.acorn),), shop_region=LogicRegion.raccoon_shop),),
+ Seed.summer_squash: (ShopSource(items_price=((15, Material.sap),), shop_region=LogicRegion.raccoon_shop),),
+
+ Seed.strawberry: (ShopSource(money_price=100, shop_region=LogicRegion.egg_festival, seasons=(Season.spring,)),),
+ Seed.rare_seed: (ShopSource(money_price=1000, shop_region=LogicRegion.traveling_cart, seasons=(Season.spring, Season.summer)),),
+
+ # Saloon
+ Beverage.beer: (ShopSource(money_price=400, shop_region=Region.saloon),),
+ Meal.salad: (ShopSource(money_price=220, shop_region=Region.saloon),),
+ Meal.bread: (ShopSource(money_price=100, shop_region=Region.saloon),),
+ Meal.spaghetti: (ShopSource(money_price=240, shop_region=Region.saloon),),
+ Meal.pizza: (ShopSource(money_price=600, shop_region=Region.saloon),),
+ Beverage.coffee: (ShopSource(money_price=300, shop_region=Region.saloon),),
+
+ # Books
+ Book.animal_catalogue: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ ShopSource(money_price=5000, shop_region=Region.ranch),),
+ Book.book_of_mysteries: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ MysteryBoxSource(amount=38),), # After 38 boxes, there are 49.99% chances player received the book.
+ Book.dwarvish_safety_manual: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ ShopSource(money_price=4000, shop_region=LogicRegion.mines_dwarf_shop),
+ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
+ Book.friendship_101: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ PrizeMachineSource(amount=9),
+ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
+ Book.horse_the_book: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ ShopSource(money_price=25000, shop_region=LogicRegion.bookseller_2),),
+ Book.jack_be_nimble_jack_be_thick: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
+ Book.jewels_of_the_sea: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ FishingTreasureChestSource(amount=21), # After 21 chests, there are 49.44% chances player received the book.
+ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
+ Book.mapping_cave_systems: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ GenericSource(regions=(Region.adventurer_guild_bedroom,)),
+ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
+ Book.monster_compendium: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ CustomRuleSource(create_rule=lambda logic: logic.monster.can_kill_many(Generic.any)),
+ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
+ Book.ol_slitherlegs: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ ShopSource(money_price=25000, shop_region=LogicRegion.bookseller_2),),
+ Book.price_catalogue: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ ShopSource(money_price=3000, shop_region=LogicRegion.bookseller_2),),
+ Book.the_alleyway_buffet: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ GenericSource(regions=(Region.town,),
+ other_requirements=(ToolRequirement(Tool.axe, ToolMaterial.iron), ToolRequirement(Tool.pickaxe, ToolMaterial.iron))),
+ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
+ Book.the_art_o_crabbing: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ GenericSource(regions=(Region.beach,),
+ other_requirements=(ToolRequirement(Tool.fishing_rod, ToolMaterial.iridium),
+ SkillRequirement(Skill.fishing, 6),
+ SeasonRequirement(Season.winter))),
+ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
+ Book.treasure_appraisal_guide: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ ArtifactTroveSource(amount=18), # After 18 troves, there is 49,88% chances player received the book.
+ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
+ Book.raccoon_journal: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),
+ ShopSource(items_price=((999, Material.fiber),), shop_region=LogicRegion.raccoon_shop),),
+ Book.way_of_the_wind_pt_1: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ ShopSource(money_price=15000, shop_region=LogicRegion.bookseller_2),),
+ Book.way_of_the_wind_pt_2: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ ShopSource(money_price=35000, shop_region=LogicRegion.bookseller_2, other_requirements=(BookRequirement(Book.way_of_the_wind_pt_1),)),),
+ Book.woodys_secret: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
+ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
+
+ # Experience Books
+ Book.book_of_stars: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL),
+ ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),),
+ Book.bait_and_bobber: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL),
+ ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),),
+ Book.combat_quarterly: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL),
+ ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),),
+ Book.mining_monthly: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL),
+ ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),),
+ Book.stardew_valley_almanac: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL),
+ ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),),
+ Book.woodcutters_weekly: (
+ Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL),
+ ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),),
+ },
+ fishes=(
+ fish_data.albacore,
+ fish_data.anchovy,
+ fish_data.bream,
+ fish_data.bullhead,
+ fish_data.carp,
+ fish_data.catfish,
+ fish_data.chub,
+ fish_data.dorado,
+ fish_data.eel,
+ fish_data.flounder,
+ fish_data.goby,
+ fish_data.halibut,
+ fish_data.herring,
+ fish_data.largemouth_bass,
+ fish_data.lingcod,
+ fish_data.midnight_carp, # Ginger island override
+ fish_data.octopus,
+ fish_data.perch,
+ fish_data.pike,
+ fish_data.pufferfish, # Ginger island override
+ fish_data.rainbow_trout,
+ fish_data.red_mullet,
+ fish_data.red_snapper,
+ fish_data.salmon,
+ fish_data.sardine,
+ fish_data.sea_cucumber,
+ fish_data.shad,
+ fish_data.slimejack,
+ fish_data.smallmouth_bass,
+ fish_data.squid,
+ fish_data.sturgeon,
+ fish_data.sunfish,
+ fish_data.super_cucumber, # Ginger island override
+ fish_data.tiger_trout,
+ fish_data.tilapia, # Ginger island override
+ fish_data.tuna, # Ginger island override
+ fish_data.void_salmon,
+ fish_data.walleye,
+ fish_data.woodskip,
+ fish_data.blobfish,
+ fish_data.midnight_squid,
+ fish_data.spook_fish,
+
+ # Legendaries
+ fish_data.angler,
+ fish_data.crimsonfish,
+ fish_data.glacierfish,
+ fish_data.legend,
+ fish_data.mutant_carp,
+
+ # Crab pot
+ fish_data.clam,
+ fish_data.cockle,
+ fish_data.crab,
+ fish_data.crayfish,
+ fish_data.lobster,
+ fish_data.mussel,
+ fish_data.oyster,
+ fish_data.periwinkle,
+ fish_data.shrimp,
+ fish_data.snail,
+ ),
+ villagers=(
+ villagers_data.josh,
+ villagers_data.elliott,
+ villagers_data.harvey,
+ villagers_data.sam,
+ villagers_data.sebastian,
+ villagers_data.shane,
+ villagers_data.abigail,
+ villagers_data.emily,
+ villagers_data.haley,
+ villagers_data.leah,
+ villagers_data.maru,
+ villagers_data.penny,
+ villagers_data.caroline,
+ villagers_data.clint,
+ villagers_data.demetrius,
+ villagers_data.evelyn,
+ villagers_data.george,
+ villagers_data.gus,
+ villagers_data.jas,
+ villagers_data.jodi,
+ villagers_data.kent,
+ villagers_data.krobus,
+ villagers_data.lewis,
+ villagers_data.linus,
+ villagers_data.marnie,
+ villagers_data.pam,
+ villagers_data.pierre,
+ villagers_data.robin,
+ villagers_data.vincent,
+ villagers_data.willy,
+ villagers_data.wizard,
+ )
+)
diff --git a/worlds/stardew_valley/content/vanilla/qi_board.py b/worlds/stardew_valley/content/vanilla/qi_board.py
new file mode 100644
index 000000000000..d859d3b16ff7
--- /dev/null
+++ b/worlds/stardew_valley/content/vanilla/qi_board.py
@@ -0,0 +1,36 @@
+from .ginger_island import ginger_island_content_pack as ginger_island_content_pack
+from .pelican_town import pelican_town as pelican_town_content_pack
+from ..game_content import ContentPack, StardewContent
+from ...data import fish_data
+from ...data.game_item import GenericSource, ItemTag
+from ...data.harvest import HarvestCropSource
+from ...strings.crop_names import Fruit
+from ...strings.region_names import Region
+from ...strings.season_names import Season
+from ...strings.seed_names import Seed
+
+
+class QiBoardContentPack(ContentPack):
+ def harvest_source_hook(self, content: StardewContent):
+ content.untag_item(Seed.qi_bean, ItemTag.CROPSANITY_SEED)
+
+
+qi_board_content_pack = QiBoardContentPack(
+ "Qi Board (Vanilla)",
+ dependencies=(
+ pelican_town_content_pack.name,
+ ginger_island_content_pack.name,
+ ),
+ harvest_sources={
+ # This one is a bit special, because it's only available during the special order, but it can be found from like, everywhere.
+ Seed.qi_bean: (GenericSource(regions=(Region.qi_walnut_room,)),),
+ Fruit.qi_fruit: (HarvestCropSource(seed=Seed.qi_bean),),
+ },
+ fishes=(
+ fish_data.ms_angler,
+ fish_data.son_of_crimsonfish,
+ fish_data.glacierfish_jr,
+ fish_data.legend_ii,
+ fish_data.radioactive_carp,
+ )
+)
diff --git a/worlds/stardew_valley/content/vanilla/the_desert.py b/worlds/stardew_valley/content/vanilla/the_desert.py
new file mode 100644
index 000000000000..a207e169ca46
--- /dev/null
+++ b/worlds/stardew_valley/content/vanilla/the_desert.py
@@ -0,0 +1,46 @@
+from .pelican_town import pelican_town as pelican_town_content_pack
+from ..game_content import ContentPack
+from ...data import fish_data, villagers_data
+from ...data.harvest import ForagingSource, HarvestCropSource
+from ...data.shop import ShopSource
+from ...strings.crop_names import Fruit, Vegetable
+from ...strings.forageable_names import Forageable, Mushroom
+from ...strings.region_names import Region
+from ...strings.season_names import Season
+from ...strings.seed_names import Seed
+
+the_desert = ContentPack(
+ "The Desert (Vanilla)",
+ dependencies=(
+ pelican_town_content_pack.name,
+ ),
+ harvest_sources={
+ Forageable.cactus_fruit: (
+ ForagingSource(regions=(Region.desert,)),
+ HarvestCropSource(seed=Seed.cactus, seasons=())
+ ),
+ Forageable.coconut: (
+ ForagingSource(regions=(Region.desert,)),
+ ),
+ Mushroom.purple: (
+ ForagingSource(regions=(Region.skull_cavern_25,)),
+ ),
+
+ Fruit.rhubarb: (HarvestCropSource(seed=Seed.rhubarb, seasons=(Season.spring,)),),
+ Fruit.starfruit: (HarvestCropSource(seed=Seed.starfruit, seasons=(Season.summer,)),),
+ Vegetable.beet: (HarvestCropSource(seed=Seed.beet, seasons=(Season.fall,)),),
+ },
+ shop_sources={
+ Seed.cactus: (ShopSource(money_price=150, shop_region=Region.oasis),),
+ Seed.rhubarb: (ShopSource(money_price=100, shop_region=Region.oasis, seasons=(Season.spring,)),),
+ Seed.starfruit: (ShopSource(money_price=400, shop_region=Region.oasis, seasons=(Season.summer,)),),
+ Seed.beet: (ShopSource(money_price=20, shop_region=Region.oasis, seasons=(Season.fall,)),),
+ },
+ fishes=(
+ fish_data.sandfish,
+ fish_data.scorpion_carp,
+ ),
+ villagers=(
+ villagers_data.sandy,
+ ),
+)
diff --git a/worlds/stardew_valley/content/vanilla/the_farm.py b/worlds/stardew_valley/content/vanilla/the_farm.py
new file mode 100644
index 000000000000..68d0bf10f6b8
--- /dev/null
+++ b/worlds/stardew_valley/content/vanilla/the_farm.py
@@ -0,0 +1,43 @@
+from .pelican_town import pelican_town as pelican_town_content_pack
+from ..game_content import ContentPack
+from ...data.harvest import FruitBatsSource, MushroomCaveSource
+from ...strings.forageable_names import Forageable, Mushroom
+
+the_farm = ContentPack(
+ "The Farm (Vanilla)",
+ dependencies=(
+ pelican_town_content_pack.name,
+ ),
+ harvest_sources={
+ # Fruit cave
+ Forageable.blackberry: (
+ FruitBatsSource(),
+ ),
+ Forageable.salmonberry: (
+ FruitBatsSource(),
+ ),
+ Forageable.spice_berry: (
+ FruitBatsSource(),
+ ),
+ Forageable.wild_plum: (
+ FruitBatsSource(),
+ ),
+
+ # Mushrooms
+ Mushroom.common: (
+ MushroomCaveSource(),
+ ),
+ Mushroom.chanterelle: (
+ MushroomCaveSource(),
+ ),
+ Mushroom.morel: (
+ MushroomCaveSource(),
+ ),
+ Mushroom.purple: (
+ MushroomCaveSource(),
+ ),
+ Mushroom.red: (
+ MushroomCaveSource(),
+ ),
+ }
+)
diff --git a/worlds/stardew_valley/content/vanilla/the_mines.py b/worlds/stardew_valley/content/vanilla/the_mines.py
new file mode 100644
index 000000000000..729b195f7b06
--- /dev/null
+++ b/worlds/stardew_valley/content/vanilla/the_mines.py
@@ -0,0 +1,35 @@
+from .pelican_town import pelican_town as pelican_town_content_pack
+from ..game_content import ContentPack
+from ...data import fish_data, villagers_data
+from ...data.harvest import ForagingSource
+from ...data.requirement import ToolRequirement
+from ...strings.forageable_names import Forageable, Mushroom
+from ...strings.region_names import Region
+from ...strings.tool_names import Tool
+
+the_mines = ContentPack(
+ "The Mines (Vanilla)",
+ dependencies=(
+ pelican_town_content_pack.name,
+ ),
+ harvest_sources={
+ Forageable.cave_carrot: (
+ ForagingSource(regions=(Region.mines_floor_10,), other_requirements=(ToolRequirement(Tool.hoe),)),
+ ),
+ Mushroom.red: (
+ ForagingSource(regions=(Region.mines_floor_95,)),
+ ),
+ Mushroom.purple: (
+ ForagingSource(regions=(Region.mines_floor_95,)),
+ )
+ },
+ fishes=(
+ fish_data.ghostfish,
+ fish_data.ice_pip,
+ fish_data.lava_eel,
+ fish_data.stonefish,
+ ),
+ villagers=(
+ villagers_data.dwarf,
+ ),
+)
diff --git a/worlds/stardew_valley/data/__init__.py b/worlds/stardew_valley/data/__init__.py
index d14d9cfb8e97..e69de29bb2d1 100644
--- a/worlds/stardew_valley/data/__init__.py
+++ b/worlds/stardew_valley/data/__init__.py
@@ -1,2 +0,0 @@
-from .crops_data import CropItem, SeedItem, all_crops, all_purchasable_seeds
-from .fish_data import FishItem, all_fish
diff --git a/worlds/stardew_valley/data/artisan.py b/worlds/stardew_valley/data/artisan.py
new file mode 100644
index 000000000000..593ab6a3ddf0
--- /dev/null
+++ b/worlds/stardew_valley/data/artisan.py
@@ -0,0 +1,10 @@
+from dataclasses import dataclass
+
+from .game_item import kw_only, ItemSource
+
+
+@dataclass(frozen=True, **kw_only)
+class MachineSource(ItemSource):
+ item: str # this should be optional (worm bin)
+ machine: str
+ # seasons
diff --git a/worlds/stardew_valley/data/bundle_data.py b/worlds/stardew_valley/data/bundle_data.py
index 8a1a6a5bcf53..8b2e189c796e 100644
--- a/worlds/stardew_valley/data/bundle_data.py
+++ b/worlds/stardew_valley/data/bundle_data.py
@@ -1,420 +1,906 @@
-from dataclasses import dataclass
-
-from . import fish_data
-from .common_data import quality_dict
-from .game_item import GameItem
-from .museum_data import Mineral
-
-@dataclass(frozen=True)
-class BundleItem:
- item: GameItem
- amount: int
- quality: int
-
- @staticmethod
- def item_bundle(name: str, item_id: int, amount: int, quality: int):
- return BundleItem(GameItem(name, item_id), amount, quality)
-
- @staticmethod
- def money_bundle(amount: int):
- return BundleItem.item_bundle("Money", -1, amount, amount)
-
- def as_amount(self, amount: int):
- return BundleItem.item_bundle(self.item.name, self.item.item_id, amount, self.quality)
-
- def as_quality(self, quality: int):
- return BundleItem.item_bundle(self.item.name, self.item.item_id, self.amount, quality)
-
- def as_gold_quality(self):
- return self.as_quality(2)
-
- def as_quality_crop(self):
- amount = 5
- difficult_crops = ["Sweet Gem Berry", "Ancient Fruit"]
- if self.item.name in difficult_crops:
- amount = 1
- return self.as_gold_quality().as_amount(amount)
-
- def is_gold_quality(self) -> bool:
- return self.quality >= 2
-
- def __repr__(self):
- return f"{self.amount} {quality_dict[self.quality]} {self.item.name}"
-
- def __lt__(self, other):
- return self.item < other.item
-
-
-wild_horseradish = BundleItem.item_bundle("Wild Horseradish", 16, 1, 0)
-daffodil = BundleItem.item_bundle("Daffodil", 18, 1, 0)
-leek = BundleItem.item_bundle("Leek", 20, 1, 0)
-dandelion = BundleItem.item_bundle("Dandelion", 22, 1, 0)
-morel = BundleItem.item_bundle("Morel", 257, 1, 0)
-common_mushroom = BundleItem.item_bundle("Common Mushroom", 404, 1, 0)
-salmonberry = BundleItem.item_bundle("Salmonberry", 296, 1, 0)
-spring_onion = BundleItem.item_bundle("Spring Onion", 399, 1, 0)
-
-grape = BundleItem.item_bundle("Grape", 398, 1, 0)
-spice_berry = BundleItem.item_bundle("Spice Berry", 396, 1, 0)
-sweet_pea = BundleItem.item_bundle("Sweet Pea", 402, 1, 0)
-red_mushroom = BundleItem.item_bundle("Red Mushroom", 420, 1, 0)
-fiddlehead_fern = BundleItem.item_bundle("Fiddlehead Fern", 259, 1, 0)
-
-wild_plum = BundleItem.item_bundle("Wild Plum", 406, 1, 0)
-hazelnut = BundleItem.item_bundle("Hazelnut", 408, 1, 0)
-blackberry = BundleItem.item_bundle("Blackberry", 410, 1, 0)
-chanterelle = BundleItem.item_bundle("Chanterelle", 281, 1, 0)
-
-winter_root = BundleItem.item_bundle("Winter Root", 412, 1, 0)
-crystal_fruit = BundleItem.item_bundle("Crystal Fruit", 414, 1, 0)
-snow_yam = BundleItem.item_bundle("Snow Yam", 416, 1, 0)
-crocus = BundleItem.item_bundle("Crocus", 418, 1, 0)
-holly = BundleItem.item_bundle("Holly", 283, 1, 0)
-
-coconut = BundleItem.item_bundle("Coconut", 88, 1, 0)
-cactus_fruit = BundleItem.item_bundle("Cactus Fruit", 90, 1, 0)
-cave_carrot = BundleItem.item_bundle("Cave Carrot", 78, 1, 0)
-purple_mushroom = BundleItem.item_bundle("Purple Mushroom", 422, 1, 0)
-maple_syrup = BundleItem.item_bundle("Maple Syrup", 724, 1, 0)
-oak_resin = BundleItem.item_bundle("Oak Resin", 725, 1, 0)
-pine_tar = BundleItem.item_bundle("Pine Tar", 726, 1, 0)
-nautilus_shell = BundleItem.item_bundle("Nautilus Shell", 392, 1, 0)
-coral = BundleItem.item_bundle("Coral", 393, 1, 0)
-sea_urchin = BundleItem.item_bundle("Sea Urchin", 397, 1, 0)
-rainbow_shell = BundleItem.item_bundle("Rainbow Shell", 394, 1, 0)
-clam = BundleItem(fish_data.clam, 1, 0)
-cockle = BundleItem(fish_data.cockle, 1, 0)
-mussel = BundleItem(fish_data.mussel, 1, 0)
-oyster = BundleItem(fish_data.oyster, 1, 0)
-seaweed = BundleItem.item_bundle("Seaweed", 152, 1, 0)
-
-wood = BundleItem.item_bundle("Wood", 388, 99, 0)
-stone = BundleItem.item_bundle("Stone", 390, 99, 0)
-hardwood = BundleItem.item_bundle("Hardwood", 709, 10, 0)
-clay = BundleItem.item_bundle("Clay", 330, 10, 0)
-fiber = BundleItem.item_bundle("Fiber", 771, 99, 0)
-
-blue_jazz = BundleItem.item_bundle("Blue Jazz", 597, 1, 0)
-cauliflower = BundleItem.item_bundle("Cauliflower", 190, 1, 0)
-green_bean = BundleItem.item_bundle("Green Bean", 188, 1, 0)
-kale = BundleItem.item_bundle("Kale", 250, 1, 0)
-parsnip = BundleItem.item_bundle("Parsnip", 24, 1, 0)
-potato = BundleItem.item_bundle("Potato", 192, 1, 0)
-strawberry = BundleItem.item_bundle("Strawberry", 400, 1, 0)
-tulip = BundleItem.item_bundle("Tulip", 591, 1, 0)
-unmilled_rice = BundleItem.item_bundle("Unmilled Rice", 271, 1, 0)
-blueberry = BundleItem.item_bundle("Blueberry", 258, 1, 0)
-corn = BundleItem.item_bundle("Corn", 270, 1, 0)
-hops = BundleItem.item_bundle("Hops", 304, 1, 0)
-hot_pepper = BundleItem.item_bundle("Hot Pepper", 260, 1, 0)
-melon = BundleItem.item_bundle("Melon", 254, 1, 0)
-poppy = BundleItem.item_bundle("Poppy", 376, 1, 0)
-radish = BundleItem.item_bundle("Radish", 264, 1, 0)
-summer_spangle = BundleItem.item_bundle("Summer Spangle", 593, 1, 0)
-sunflower = BundleItem.item_bundle("Sunflower", 421, 1, 0)
-tomato = BundleItem.item_bundle("Tomato", 256, 1, 0)
-wheat = BundleItem.item_bundle("Wheat", 262, 1, 0)
-hay = BundleItem.item_bundle("Hay", 178, 1, 0)
-amaranth = BundleItem.item_bundle("Amaranth", 300, 1, 0)
-bok_choy = BundleItem.item_bundle("Bok Choy", 278, 1, 0)
-cranberries = BundleItem.item_bundle("Cranberries", 282, 1, 0)
-eggplant = BundleItem.item_bundle("Eggplant", 272, 1, 0)
-fairy_rose = BundleItem.item_bundle("Fairy Rose", 595, 1, 0)
-pumpkin = BundleItem.item_bundle("Pumpkin", 276, 1, 0)
-yam = BundleItem.item_bundle("Yam", 280, 1, 0)
-sweet_gem_berry = BundleItem.item_bundle("Sweet Gem Berry", 417, 1, 0)
-rhubarb = BundleItem.item_bundle("Rhubarb", 252, 1, 0)
-beet = BundleItem.item_bundle("Beet", 284, 1, 0)
-red_cabbage = BundleItem.item_bundle("Red Cabbage", 266, 1, 0)
-artichoke = BundleItem.item_bundle("Artichoke", 274, 1, 0)
-
-egg = BundleItem.item_bundle("Egg", 176, 1, 0)
-large_egg = BundleItem.item_bundle("Large Egg", 174, 1, 0)
-brown_egg = BundleItem.item_bundle("Egg (Brown)", 180, 1, 0)
-large_brown_egg = BundleItem.item_bundle("Large Egg (Brown)", 182, 1, 0)
-wool = BundleItem.item_bundle("Wool", 440, 1, 0)
-milk = BundleItem.item_bundle("Milk", 184, 1, 0)
-large_milk = BundleItem.item_bundle("Large Milk", 186, 1, 0)
-goat_milk = BundleItem.item_bundle("Goat Milk", 436, 1, 0)
-large_goat_milk = BundleItem.item_bundle("Large Goat Milk", 438, 1, 0)
-truffle = BundleItem.item_bundle("Truffle", 430, 1, 0)
-duck_feather = BundleItem.item_bundle("Duck Feather", 444, 1, 0)
-duck_egg = BundleItem.item_bundle("Duck Egg", 442, 1, 0)
-rabbit_foot = BundleItem.item_bundle("Rabbit's Foot", 446, 1, 0)
-
-truffle_oil = BundleItem.item_bundle("Truffle Oil", 432, 1, 0)
-cloth = BundleItem.item_bundle("Cloth", 428, 1, 0)
-goat_cheese = BundleItem.item_bundle("Goat Cheese", 426, 1, 0)
-cheese = BundleItem.item_bundle("Cheese", 424, 1, 0)
-honey = BundleItem.item_bundle("Honey", 340, 1, 0)
-beer = BundleItem.item_bundle("Beer", 346, 1, 0)
-juice = BundleItem.item_bundle("Juice", 350, 1, 0)
-mead = BundleItem.item_bundle("Mead", 459, 1, 0)
-pale_ale = BundleItem.item_bundle("Pale Ale", 303, 1, 0)
-wine = BundleItem.item_bundle("Wine", 348, 1, 0)
-jelly = BundleItem.item_bundle("Jelly", 344, 1, 0)
-pickles = BundleItem.item_bundle("Pickles", 342, 1, 0)
-caviar = BundleItem.item_bundle("Caviar", 445, 1, 0)
-aged_roe = BundleItem.item_bundle("Aged Roe", 447, 1, 0)
-apple = BundleItem.item_bundle("Apple", 613, 1, 0)
-apricot = BundleItem.item_bundle("Apricot", 634, 1, 0)
-orange = BundleItem.item_bundle("Orange", 635, 1, 0)
-peach = BundleItem.item_bundle("Peach", 636, 1, 0)
-pomegranate = BundleItem.item_bundle("Pomegranate", 637, 1, 0)
-cherry = BundleItem.item_bundle("Cherry", 638, 1, 0)
-lobster = BundleItem(fish_data.lobster, 1, 0)
-crab = BundleItem(fish_data.crab, 1, 0)
-shrimp = BundleItem(fish_data.shrimp, 1, 0)
-crayfish = BundleItem(fish_data.crayfish, 1, 0)
-snail = BundleItem(fish_data.snail, 1, 0)
-periwinkle = BundleItem(fish_data.periwinkle, 1, 0)
-trash = BundleItem.item_bundle("Trash", 168, 1, 0)
-driftwood = BundleItem.item_bundle("Driftwood", 169, 1, 0)
-soggy_newspaper = BundleItem.item_bundle("Soggy Newspaper", 172, 1, 0)
-broken_cd = BundleItem.item_bundle("Broken CD", 171, 1, 0)
-broken_glasses = BundleItem.item_bundle("Broken Glasses", 170, 1, 0)
-
-chub = BundleItem(fish_data.chub, 1, 0)
-catfish = BundleItem(fish_data.catfish, 1, 0)
-rainbow_trout = BundleItem(fish_data.rainbow_trout, 1, 0)
-lingcod = BundleItem(fish_data.lingcod, 1, 0)
-walleye = BundleItem(fish_data.walleye, 1, 0)
-perch = BundleItem(fish_data.perch, 1, 0)
-pike = BundleItem(fish_data.pike, 1, 0)
-bream = BundleItem(fish_data.bream, 1, 0)
-salmon = BundleItem(fish_data.salmon, 1, 0)
-sunfish = BundleItem(fish_data.sunfish, 1, 0)
-tiger_trout = BundleItem(fish_data.tiger_trout, 1, 0)
-shad = BundleItem(fish_data.shad, 1, 0)
-smallmouth_bass = BundleItem(fish_data.smallmouth_bass, 1, 0)
-dorado = BundleItem(fish_data.dorado, 1, 0)
-carp = BundleItem(fish_data.carp, 1, 0)
-midnight_carp = BundleItem(fish_data.midnight_carp, 1, 0)
-largemouth_bass = BundleItem(fish_data.largemouth_bass, 1, 0)
-sturgeon = BundleItem(fish_data.sturgeon, 1, 0)
-bullhead = BundleItem(fish_data.bullhead, 1, 0)
-tilapia = BundleItem(fish_data.tilapia, 1, 0)
-pufferfish = BundleItem(fish_data.pufferfish, 1, 0)
-tuna = BundleItem(fish_data.tuna, 1, 0)
-super_cucumber = BundleItem(fish_data.super_cucumber, 1, 0)
-flounder = BundleItem(fish_data.flounder, 1, 0)
-anchovy = BundleItem(fish_data.anchovy, 1, 0)
-sardine = BundleItem(fish_data.sardine, 1, 0)
-red_mullet = BundleItem(fish_data.red_mullet, 1, 0)
-herring = BundleItem(fish_data.herring, 1, 0)
-eel = BundleItem(fish_data.eel, 1, 0)
-octopus = BundleItem(fish_data.octopus, 1, 0)
-red_snapper = BundleItem(fish_data.red_snapper, 1, 0)
-squid = BundleItem(fish_data.squid, 1, 0)
-sea_cucumber = BundleItem(fish_data.sea_cucumber, 1, 0)
-albacore = BundleItem(fish_data.albacore, 1, 0)
-halibut = BundleItem(fish_data.halibut, 1, 0)
-scorpion_carp = BundleItem(fish_data.scorpion_carp, 1, 0)
-sandfish = BundleItem(fish_data.sandfish, 1, 0)
-woodskip = BundleItem(fish_data.woodskip, 1, 0)
-lava_eel = BundleItem(fish_data.lava_eel, 1, 0)
-ice_pip = BundleItem(fish_data.ice_pip, 1, 0)
-stonefish = BundleItem(fish_data.stonefish, 1, 0)
-ghostfish = BundleItem(fish_data.ghostfish, 1, 0)
-
-wilted_bouquet = BundleItem.item_bundle("Wilted Bouquet", 277, 1, 0)
-copper_bar = BundleItem.item_bundle("Copper Bar", 334, 2, 0)
-iron_Bar = BundleItem.item_bundle("Iron Bar", 335, 2, 0)
-gold_bar = BundleItem.item_bundle("Gold Bar", 336, 1, 0)
-iridium_bar = BundleItem.item_bundle("Iridium Bar", 337, 1, 0)
-refined_quartz = BundleItem.item_bundle("Refined Quartz", 338, 2, 0)
-coal = BundleItem.item_bundle("Coal", 382, 5, 0)
-
-quartz = BundleItem(Mineral.quartz, 1, 0)
-fire_quartz = BundleItem(Mineral.fire_quartz, 1, 0)
-frozen_tear = BundleItem(Mineral.frozen_tear, 1, 0)
-earth_crystal = BundleItem(Mineral.earth_crystal, 1, 0)
-emerald = BundleItem(Mineral.emerald, 1, 0)
-aquamarine = BundleItem(Mineral.aquamarine, 1, 0)
-ruby = BundleItem(Mineral.ruby, 1, 0)
-amethyst = BundleItem(Mineral.amethyst, 1, 0)
-topaz = BundleItem(Mineral.topaz, 1, 0)
-jade = BundleItem(Mineral.jade, 1, 0)
-
-slime = BundleItem.item_bundle("Slime", 766, 99, 0)
-bug_meat = BundleItem.item_bundle("Bug Meat", 684, 10, 0)
-bat_wing = BundleItem.item_bundle("Bat Wing", 767, 10, 0)
-solar_essence = BundleItem.item_bundle("Solar Essence", 768, 1, 0)
-void_essence = BundleItem.item_bundle("Void Essence", 769, 1, 0)
-
-maki_roll = BundleItem.item_bundle("Maki Roll", 228, 1, 0)
-fried_egg = BundleItem.item_bundle("Fried Egg", 194, 1, 0)
-omelet = BundleItem.item_bundle("Omelet", 195, 1, 0)
-pizza = BundleItem.item_bundle("Pizza", 206, 1, 0)
-hashbrowns = BundleItem.item_bundle("Hashbrowns", 210, 1, 0)
-pancakes = BundleItem.item_bundle("Pancakes", 211, 1, 0)
-bread = BundleItem.item_bundle("Bread", 216, 1, 0)
-tortilla = BundleItem.item_bundle("Tortilla", 229, 1, 0)
-triple_shot_espresso = BundleItem.item_bundle("Triple Shot Espresso", 253, 1, 0)
-farmer_s_lunch = BundleItem.item_bundle("Farmer's Lunch", 240, 1, 0)
-survival_burger = BundleItem.item_bundle("Survival Burger", 241, 1, 0)
-dish_o_the_sea = BundleItem.item_bundle("Dish O' The Sea", 242, 1, 0)
-miner_s_treat = BundleItem.item_bundle("Miner's Treat", 243, 1, 0)
-roots_platter = BundleItem.item_bundle("Roots Platter", 244, 1, 0)
-salad = BundleItem.item_bundle("Salad", 196, 1, 0)
-cheese_cauliflower = BundleItem.item_bundle("Cheese Cauliflower", 197, 1, 0)
-parsnip_soup = BundleItem.item_bundle("Parsnip Soup", 199, 1, 0)
-fried_mushroom = BundleItem.item_bundle("Fried Mushroom", 205, 1, 0)
-salmon_dinner = BundleItem.item_bundle("Salmon Dinner", 212, 1, 0)
-pepper_poppers = BundleItem.item_bundle("Pepper Poppers", 215, 1, 0)
-spaghetti = BundleItem.item_bundle("Spaghetti", 224, 1, 0)
-sashimi = BundleItem.item_bundle("Sashimi", 227, 1, 0)
-blueberry_tart = BundleItem.item_bundle("Blueberry Tart", 234, 1, 0)
-algae_soup = BundleItem.item_bundle("Algae Soup", 456, 1, 0)
-pale_broth = BundleItem.item_bundle("Pale Broth", 457, 1, 0)
-chowder = BundleItem.item_bundle("Chowder", 727, 1, 0)
-green_algae = BundleItem.item_bundle("Green Algae", 153, 1, 0)
-white_algae = BundleItem.item_bundle("White Algae", 157, 1, 0)
-geode = BundleItem.item_bundle("Geode", 535, 1, 0)
-frozen_geode = BundleItem.item_bundle("Frozen Geode", 536, 1, 0)
-magma_geode = BundleItem.item_bundle("Magma Geode", 537, 1, 0)
-omni_geode = BundleItem.item_bundle("Omni Geode", 749, 1, 0)
-
-spring_foraging_items = [wild_horseradish, daffodil, leek, dandelion, salmonberry, spring_onion]
-summer_foraging_items = [grape, spice_berry, sweet_pea, fiddlehead_fern, rainbow_shell]
-fall_foraging_items = [common_mushroom, wild_plum, hazelnut, blackberry]
-winter_foraging_items = [winter_root, crystal_fruit, snow_yam, crocus, holly, nautilus_shell]
-exotic_foraging_items = [coconut, cactus_fruit, cave_carrot, red_mushroom, purple_mushroom,
- maple_syrup, oak_resin, pine_tar, morel, coral,
- sea_urchin, clam, cockle, mussel, oyster, seaweed]
-construction_items = [wood, stone, hardwood, clay, fiber]
-
-# TODO coffee_bean, garlic, rhubarb, tea_leaves
-spring_crop_items = [blue_jazz, cauliflower, green_bean, kale, parsnip, potato, strawberry, tulip, unmilled_rice]
-# TODO red_cabbage, starfruit, ancient_fruit, pineapple, taro_root
-summer_crops_items = [blueberry, corn, hops, hot_pepper, melon, poppy,
- radish, summer_spangle, sunflower, tomato, wheat]
-# TODO artichoke, beet
-fall_crops_items = [corn, sunflower, wheat, amaranth, bok_choy, cranberries,
- eggplant, fairy_rose, grape, pumpkin, yam, sweet_gem_berry]
-all_crops_items = sorted({*spring_crop_items, *summer_crops_items, *fall_crops_items})
-quality_crops_items = [item.as_quality_crop() for item in all_crops_items]
-# TODO void_egg, dinosaur_egg, ostrich_egg, golden_egg
-animal_product_items = [egg, large_egg, brown_egg, large_brown_egg, wool, milk, large_milk,
- goat_milk, large_goat_milk, truffle, duck_feather, duck_egg, rabbit_foot]
-# TODO coffee, green_tea
-artisan_goods_items = [truffle_oil, cloth, goat_cheese, cheese, honey, beer, juice, mead, pale_ale, wine, jelly,
- pickles, caviar, aged_roe, apple, apricot, orange, peach, pomegranate, cherry]
-
-river_fish_items = [chub, catfish, rainbow_trout, lingcod, walleye, perch, pike, bream,
- salmon, sunfish, tiger_trout, shad, smallmouth_bass, dorado]
-lake_fish_items = [chub, rainbow_trout, lingcod, walleye, perch, carp, midnight_carp,
- largemouth_bass, sturgeon, bullhead, midnight_carp]
-ocean_fish_items = [tilapia, pufferfish, tuna, super_cucumber, flounder, anchovy, sardine, red_mullet,
- herring, eel, octopus, red_snapper, squid, sea_cucumber, albacore, halibut]
-night_fish_items = [walleye, bream, super_cucumber, eel, squid, midnight_carp]
-# TODO void_salmon
-specialty_fish_items = [scorpion_carp, sandfish, woodskip, pufferfish, eel, octopus,
- squid, lava_eel, ice_pip, stonefish, ghostfish, dorado]
-crab_pot_items = [lobster, clam, crab, cockle, mussel, shrimp, oyster, crayfish, snail,
- periwinkle, trash, driftwood, soggy_newspaper, broken_cd, broken_glasses]
-
-# TODO radioactive_bar
-blacksmith_items = [wilted_bouquet, copper_bar, iron_Bar, gold_bar, iridium_bar, refined_quartz, coal]
-geologist_items = [quartz, earth_crystal, frozen_tear, fire_quartz, emerald, aquamarine, ruby, amethyst, topaz, jade]
-adventurer_items = [slime, bug_meat, bat_wing, solar_essence, void_essence, coal]
-
-chef_items = [maki_roll, fried_egg, omelet, pizza, hashbrowns, pancakes, bread, tortilla, triple_shot_espresso,
- farmer_s_lunch, survival_burger, dish_o_the_sea, miner_s_treat, roots_platter, salad,
- cheese_cauliflower, parsnip_soup, fried_mushroom, salmon_dinner, pepper_poppers, spaghetti,
- sashimi, blueberry_tart, algae_soup, pale_broth, chowder]
-
-dwarf_scroll_1 = BundleItem.item_bundle("Dwarf Scroll I", 96, 1, 0)
-dwarf_scroll_2 = BundleItem.item_bundle("Dwarf Scroll II", 97, 1, 0)
-dwarf_scroll_3 = BundleItem.item_bundle("Dwarf Scroll III", 98, 1, 0)
-dwarf_scroll_4 = BundleItem.item_bundle("Dwarf Scroll IV", 99, 1, 0)
-elvish_jewelry = BundleItem.item_bundle("Elvish Jewelry", 104, 1, 0)
-ancient_drum = BundleItem.item_bundle("Ancient Drum", 123, 1, 0)
-dried_starfish = BundleItem.item_bundle("Dried Starfish", 116, 1, 0)
-
-dye_red_items = [cranberries, hot_pepper, radish, rhubarb, spaghetti, strawberry, tomato, tulip]
-dye_orange_items = [poppy, pumpkin, apricot, orange, spice_berry, winter_root]
-dye_yellow_items = [corn, parsnip, summer_spangle, sunflower]
-dye_green_items = [fiddlehead_fern, kale, artichoke, bok_choy, green_bean]
-dye_blue_items = [blueberry, blue_jazz, blackberry, crystal_fruit]
-dye_purple_items = [beet, crocus, eggplant, red_cabbage, sweet_pea]
-dye_items = [dye_red_items, dye_orange_items, dye_yellow_items, dye_green_items, dye_blue_items, dye_purple_items]
-field_research_items = [purple_mushroom, nautilus_shell, chub, geode, frozen_geode, magma_geode, omni_geode,
- rainbow_shell, amethyst, bream, carp]
-fodder_items = [wheat.as_amount(10), hay.as_amount(10), apple.as_amount(3), kale.as_amount(3), corn.as_amount(3),
- green_bean.as_amount(3), potato.as_amount(3), green_algae.as_amount(5), white_algae.as_amount(3)]
-enchanter_items = [oak_resin, wine, rabbit_foot, pomegranate, purple_mushroom, solar_essence,
- super_cucumber, void_essence, fire_quartz, frozen_tear, jade]
-
-vault_2500_items = [BundleItem.money_bundle(2500)]
-vault_5000_items = [BundleItem.money_bundle(5000)]
-vault_10000_items = [BundleItem.money_bundle(10000)]
-vault_25000_items = [BundleItem.money_bundle(25000)]
-
-crafts_room_bundle_items = [
- *spring_foraging_items,
- *summer_foraging_items,
- *fall_foraging_items,
- *winter_foraging_items,
- *exotic_foraging_items,
- *construction_items,
-]
-
-pantry_bundle_items = sorted({
- *spring_crop_items,
- *summer_crops_items,
- *fall_crops_items,
- *quality_crops_items,
- *animal_product_items,
- *artisan_goods_items,
-})
-
-fish_tank_bundle_items = sorted({
- *river_fish_items,
- *lake_fish_items,
- *ocean_fish_items,
- *night_fish_items,
- *crab_pot_items,
- *specialty_fish_items,
-})
-
-boiler_room_bundle_items = sorted({
- *blacksmith_items,
- *geologist_items,
- *adventurer_items,
+from ..bundles.bundle import BundleTemplate, IslandBundleTemplate, DeepBundleTemplate, CurrencyBundleTemplate, MoneyBundleTemplate, FestivalBundleTemplate
+from ..bundles.bundle_item import BundleItem
+from ..bundles.bundle_room import BundleRoomTemplate
+from ..content import content_packs
+from ..content.vanilla.base import all_fruits, all_vegetables, all_edible_mushrooms
+from ..strings.animal_product_names import AnimalProduct
+from ..strings.artisan_good_names import ArtisanGood
+from ..strings.bundle_names import CCRoom, BundleName
+from ..strings.craftable_names import Fishing, Craftable, Bomb, Consumable, Lighting
+from ..strings.crop_names import Fruit, Vegetable
+from ..strings.currency_names import Currency
+from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro
+from ..strings.fish_names import Fish, WaterItem, Trash, all_fish
+from ..strings.flower_names import Flower
+from ..strings.food_names import Beverage, Meal
+from ..strings.forageable_names import Forageable, Mushroom
+from ..strings.geode_names import Geode
+from ..strings.gift_names import Gift
+from ..strings.ingredient_names import Ingredient
+from ..strings.material_names import Material
+from ..strings.metal_names import MetalBar, Artifact, Fossil, Ore, Mineral
+from ..strings.monster_drop_names import Loot
+from ..strings.quality_names import ForageQuality, ArtisanQuality, FishQuality
+from ..strings.seed_names import Seed, TreeSeed
+
+wild_horseradish = BundleItem(Forageable.wild_horseradish)
+daffodil = BundleItem(Forageable.daffodil)
+leek = BundleItem(Forageable.leek)
+dandelion = BundleItem(Forageable.dandelion)
+morel = BundleItem(Mushroom.morel)
+common_mushroom = BundleItem(Mushroom.common)
+salmonberry = BundleItem(Forageable.salmonberry)
+spring_onion = BundleItem(Forageable.spring_onion)
+
+grape = BundleItem(Fruit.grape)
+spice_berry = BundleItem(Forageable.spice_berry)
+sweet_pea = BundleItem(Forageable.sweet_pea)
+red_mushroom = BundleItem(Mushroom.red)
+fiddlehead_fern = BundleItem(Forageable.fiddlehead_fern)
+
+wild_plum = BundleItem(Forageable.wild_plum)
+hazelnut = BundleItem(Forageable.hazelnut)
+blackberry = BundleItem(Forageable.blackberry)
+chanterelle = BundleItem(Mushroom.chanterelle)
+
+winter_root = BundleItem(Forageable.winter_root)
+crystal_fruit = BundleItem(Forageable.crystal_fruit)
+snow_yam = BundleItem(Forageable.snow_yam)
+crocus = BundleItem(Forageable.crocus)
+holly = BundleItem(Forageable.holly)
+
+coconut = BundleItem(Forageable.coconut)
+cactus_fruit = BundleItem(Forageable.cactus_fruit)
+cave_carrot = BundleItem(Forageable.cave_carrot)
+purple_mushroom = BundleItem(Mushroom.purple)
+maple_syrup = BundleItem(ArtisanGood.maple_syrup)
+oak_resin = BundleItem(ArtisanGood.oak_resin)
+pine_tar = BundleItem(ArtisanGood.pine_tar)
+nautilus_shell = BundleItem(WaterItem.nautilus_shell)
+coral = BundleItem(WaterItem.coral)
+sea_urchin = BundleItem(WaterItem.sea_urchin)
+rainbow_shell = BundleItem(Forageable.rainbow_shell)
+clam = BundleItem(Fish.clam)
+cockle = BundleItem(Fish.cockle)
+mussel = BundleItem(Fish.mussel)
+oyster = BundleItem(Fish.oyster)
+seaweed = BundleItem(WaterItem.seaweed, can_have_quality=False)
+
+wood = BundleItem(Material.wood, 99)
+stone = BundleItem(Material.stone, 99)
+hardwood = BundleItem(Material.hardwood, 10)
+clay = BundleItem(Material.clay, 10)
+fiber = BundleItem(Material.fiber, 99)
+moss = BundleItem(Material.moss, 10)
+
+mixed_seeds = BundleItem(Seed.mixed)
+acorn = BundleItem(TreeSeed.acorn)
+maple_seed = BundleItem(TreeSeed.maple)
+pine_cone = BundleItem(TreeSeed.pine)
+mahogany_seed = BundleItem(TreeSeed.mahogany)
+mushroom_tree_seed = BundleItem(TreeSeed.mushroom, source=BundleItem.Sources.island)
+mystic_tree_seed = BundleItem(TreeSeed.mystic, source=BundleItem.Sources.masteries)
+mossy_seed = BundleItem(TreeSeed.mossy)
+
+strawberry_seeds = BundleItem(Seed.strawberry)
+
+blue_jazz = BundleItem(Flower.blue_jazz)
+cauliflower = BundleItem(Vegetable.cauliflower)
+green_bean = BundleItem(Vegetable.green_bean)
+kale = BundleItem(Vegetable.kale)
+parsnip = BundleItem(Vegetable.parsnip)
+potato = BundleItem(Vegetable.potato)
+strawberry = BundleItem(Fruit.strawberry, source=BundleItem.Sources.festival)
+tulip = BundleItem(Flower.tulip)
+unmilled_rice = BundleItem(Vegetable.unmilled_rice)
+coffee_bean = BundleItem(Seed.coffee)
+garlic = BundleItem(Vegetable.garlic)
+blueberry = BundleItem(Fruit.blueberry)
+corn = BundleItem(Vegetable.corn)
+hops = BundleItem(Vegetable.hops)
+hot_pepper = BundleItem(Fruit.hot_pepper)
+melon = BundleItem(Fruit.melon)
+poppy = BundleItem(Flower.poppy)
+radish = BundleItem(Vegetable.radish)
+summer_spangle = BundleItem(Flower.summer_spangle)
+sunflower = BundleItem(Flower.sunflower)
+tomato = BundleItem(Vegetable.tomato)
+wheat = BundleItem(Vegetable.wheat)
+hay = BundleItem(Forageable.hay)
+amaranth = BundleItem(Vegetable.amaranth)
+bok_choy = BundleItem(Vegetable.bok_choy)
+cranberries = BundleItem(Fruit.cranberries)
+eggplant = BundleItem(Vegetable.eggplant)
+fairy_rose = BundleItem(Flower.fairy_rose)
+pumpkin = BundleItem(Vegetable.pumpkin)
+yam = BundleItem(Vegetable.yam)
+sweet_gem_berry = BundleItem(Fruit.sweet_gem_berry)
+rhubarb = BundleItem(Fruit.rhubarb)
+beet = BundleItem(Vegetable.beet)
+red_cabbage = BundleItem(Vegetable.red_cabbage)
+starfruit = BundleItem(Fruit.starfruit)
+artichoke = BundleItem(Vegetable.artichoke)
+pineapple = BundleItem(Fruit.pineapple, source=BundleItem.Sources.content)
+taro_root = BundleItem(Vegetable.taro_root, source=BundleItem.Sources.content)
+
+carrot = BundleItem(Vegetable.carrot)
+summer_squash = BundleItem(Vegetable.summer_squash)
+broccoli = BundleItem(Vegetable.broccoli)
+powdermelon = BundleItem(Fruit.powdermelon)
+
+egg = BundleItem(AnimalProduct.egg)
+large_egg = BundleItem(AnimalProduct.large_egg)
+brown_egg = BundleItem(AnimalProduct.brown_egg)
+large_brown_egg = BundleItem(AnimalProduct.large_brown_egg)
+wool = BundleItem(AnimalProduct.wool)
+milk = BundleItem(AnimalProduct.milk)
+large_milk = BundleItem(AnimalProduct.large_milk)
+goat_milk = BundleItem(AnimalProduct.goat_milk)
+large_goat_milk = BundleItem(AnimalProduct.large_goat_milk)
+truffle = BundleItem(AnimalProduct.truffle)
+duck_feather = BundleItem(AnimalProduct.duck_feather)
+duck_egg = BundleItem(AnimalProduct.duck_egg)
+rabbit_foot = BundleItem(AnimalProduct.rabbit_foot)
+dinosaur_egg = BundleItem(AnimalProduct.dinosaur_egg)
+void_egg = BundleItem(AnimalProduct.void_egg)
+ostrich_egg = BundleItem(AnimalProduct.ostrich_egg, source=BundleItem.Sources.island, )
+golden_egg = BundleItem(AnimalProduct.golden_egg)
+
+truffle_oil = BundleItem(ArtisanGood.truffle_oil)
+cloth = BundleItem(ArtisanGood.cloth)
+goat_cheese = BundleItem(ArtisanGood.goat_cheese)
+cheese = BundleItem(ArtisanGood.cheese)
+honey = BundleItem(ArtisanGood.honey)
+beer = BundleItem(Beverage.beer)
+juice = BundleItem(ArtisanGood.juice)
+mead = BundleItem(ArtisanGood.mead)
+pale_ale = BundleItem(ArtisanGood.pale_ale)
+wine = BundleItem(ArtisanGood.wine)
+jelly = BundleItem(ArtisanGood.jelly)
+pickles = BundleItem(ArtisanGood.pickles)
+caviar = BundleItem(ArtisanGood.caviar)
+aged_roe = BundleItem(ArtisanGood.aged_roe)
+roe = BundleItem(AnimalProduct.roe)
+squid_ink = BundleItem(AnimalProduct.squid_ink)
+coffee = BundleItem(Beverage.coffee)
+green_tea = BundleItem(ArtisanGood.green_tea)
+apple = BundleItem(Fruit.apple)
+apricot = BundleItem(Fruit.apricot)
+orange = BundleItem(Fruit.orange)
+peach = BundleItem(Fruit.peach)
+pomegranate = BundleItem(Fruit.pomegranate)
+cherry = BundleItem(Fruit.cherry)
+banana = BundleItem(Fruit.banana, source=BundleItem.Sources.content)
+mango = BundleItem(Fruit.mango, source=BundleItem.Sources.content)
+
+basic_fertilizer = BundleItem(Fertilizer.basic, 100)
+quality_fertilizer = BundleItem(Fertilizer.quality, 20)
+deluxe_fertilizer = BundleItem(Fertilizer.deluxe, 5, source=BundleItem.Sources.island)
+basic_retaining_soil = BundleItem(RetainingSoil.basic, 80)
+quality_retaining_soil = BundleItem(RetainingSoil.quality, 50)
+deluxe_retaining_soil = BundleItem(RetainingSoil.deluxe, 20, source=BundleItem.Sources.island)
+speed_gro = BundleItem(SpeedGro.basic, 40)
+deluxe_speed_gro = BundleItem(SpeedGro.deluxe, 20)
+hyper_speed_gro = BundleItem(SpeedGro.hyper, 5, source=BundleItem.Sources.island)
+tree_fertilizer = BundleItem(Fertilizer.tree, 20)
+
+lobster = BundleItem(Fish.lobster)
+crab = BundleItem(Fish.crab)
+shrimp = BundleItem(Fish.shrimp)
+crayfish = BundleItem(Fish.crayfish)
+snail = BundleItem(Fish.snail)
+periwinkle = BundleItem(Fish.periwinkle)
+trash = BundleItem(Trash.trash)
+driftwood = BundleItem(Trash.driftwood)
+soggy_newspaper = BundleItem(Trash.soggy_newspaper)
+broken_cd = BundleItem(Trash.broken_cd)
+broken_glasses = BundleItem(Trash.broken_glasses)
+
+chub = BundleItem(Fish.chub)
+catfish = BundleItem(Fish.catfish)
+rainbow_trout = BundleItem(Fish.rainbow_trout)
+lingcod = BundleItem(Fish.lingcod)
+walleye = BundleItem(Fish.walleye)
+perch = BundleItem(Fish.perch)
+pike = BundleItem(Fish.pike)
+bream = BundleItem(Fish.bream)
+salmon = BundleItem(Fish.salmon)
+sunfish = BundleItem(Fish.sunfish)
+tiger_trout = BundleItem(Fish.tiger_trout)
+shad = BundleItem(Fish.shad)
+smallmouth_bass = BundleItem(Fish.smallmouth_bass)
+dorado = BundleItem(Fish.dorado)
+carp = BundleItem(Fish.carp)
+midnight_carp = BundleItem(Fish.midnight_carp)
+largemouth_bass = BundleItem(Fish.largemouth_bass)
+sturgeon = BundleItem(Fish.sturgeon)
+bullhead = BundleItem(Fish.bullhead)
+tilapia = BundleItem(Fish.tilapia)
+pufferfish = BundleItem(Fish.pufferfish)
+tuna = BundleItem(Fish.tuna)
+super_cucumber = BundleItem(Fish.super_cucumber)
+flounder = BundleItem(Fish.flounder)
+anchovy = BundleItem(Fish.anchovy)
+sardine = BundleItem(Fish.sardine)
+red_mullet = BundleItem(Fish.red_mullet)
+herring = BundleItem(Fish.herring)
+eel = BundleItem(Fish.eel)
+octopus = BundleItem(Fish.octopus)
+red_snapper = BundleItem(Fish.red_snapper)
+squid = BundleItem(Fish.squid)
+sea_cucumber = BundleItem(Fish.sea_cucumber)
+albacore = BundleItem(Fish.albacore)
+halibut = BundleItem(Fish.halibut)
+scorpion_carp = BundleItem(Fish.scorpion_carp)
+sandfish = BundleItem(Fish.sandfish)
+woodskip = BundleItem(Fish.woodskip)
+lava_eel = BundleItem(Fish.lava_eel)
+ice_pip = BundleItem(Fish.ice_pip)
+stonefish = BundleItem(Fish.stonefish)
+ghostfish = BundleItem(Fish.ghostfish)
+
+bouquet = BundleItem(Gift.bouquet)
+wilted_bouquet = BundleItem(Gift.wilted_bouquet)
+copper_bar = BundleItem(MetalBar.copper)
+iron_Bar = BundleItem(MetalBar.iron)
+gold_bar = BundleItem(MetalBar.gold)
+iridium_bar = BundleItem(MetalBar.iridium)
+refined_quartz = BundleItem(MetalBar.quartz)
+coal = BundleItem(Material.coal, 5)
+iridium_ore = BundleItem(Ore.iridium)
+gold_ore = BundleItem(Ore.gold)
+iron_ore = BundleItem(Ore.iron)
+copper_ore = BundleItem(Ore.copper)
+battery_pack = BundleItem(ArtisanGood.battery_pack)
+
+quartz = BundleItem(Mineral.quartz)
+fire_quartz = BundleItem(Mineral.fire_quartz)
+frozen_tear = BundleItem(Mineral.frozen_tear)
+earth_crystal = BundleItem(Mineral.earth_crystal)
+emerald = BundleItem(Mineral.emerald)
+aquamarine = BundleItem(Mineral.aquamarine)
+ruby = BundleItem(Mineral.ruby)
+amethyst = BundleItem(Mineral.amethyst)
+topaz = BundleItem(Mineral.topaz)
+jade = BundleItem(Mineral.jade)
+
+slime = BundleItem(Loot.slime, 99)
+bug_meat = BundleItem(Loot.bug_meat, 10)
+bat_wing = BundleItem(Loot.bat_wing, 10)
+solar_essence = BundleItem(Loot.solar_essence)
+void_essence = BundleItem(Loot.void_essence)
+
+petrified_slime = BundleItem(Mineral.petrified_slime)
+blue_slime_egg = BundleItem(Loot.blue_slime_egg)
+red_slime_egg = BundleItem(Loot.red_slime_egg)
+purple_slime_egg = BundleItem(Loot.purple_slime_egg)
+green_slime_egg = BundleItem(Loot.green_slime_egg)
+tiger_slime_egg = BundleItem(Loot.tiger_slime_egg, source=BundleItem.Sources.island)
+
+cherry_bomb = BundleItem(Bomb.cherry_bomb, 5)
+bomb = BundleItem(Bomb.bomb, 2)
+mega_bomb = BundleItem(Bomb.mega_bomb)
+explosive_ammo = BundleItem(Craftable.explosive_ammo, 5)
+
+maki_roll = BundleItem(Meal.maki_roll)
+fried_egg = BundleItem(Meal.fried_egg)
+omelet = BundleItem(Meal.omelet)
+pizza = BundleItem(Meal.pizza)
+hashbrowns = BundleItem(Meal.hashbrowns)
+pancakes = BundleItem(Meal.pancakes)
+bread = BundleItem(Meal.bread)
+tortilla = BundleItem(Meal.tortilla)
+triple_shot_espresso = BundleItem(Beverage.triple_shot_espresso)
+farmer_s_lunch = BundleItem(Meal.farmer_lunch)
+survival_burger = BundleItem(Meal.survival_burger)
+dish_o_the_sea = BundleItem(Meal.dish_o_the_sea)
+miner_s_treat = BundleItem(Meal.miners_treat)
+roots_platter = BundleItem(Meal.roots_platter)
+salad = BundleItem(Meal.salad)
+cheese_cauliflower = BundleItem(Meal.cheese_cauliflower)
+parsnip_soup = BundleItem(Meal.parsnip_soup)
+fried_mushroom = BundleItem(Meal.fried_mushroom)
+salmon_dinner = BundleItem(Meal.salmon_dinner)
+pepper_poppers = BundleItem(Meal.pepper_poppers)
+spaghetti = BundleItem(Meal.spaghetti)
+sashimi = BundleItem(Meal.sashimi)
+blueberry_tart = BundleItem(Meal.blueberry_tart)
+algae_soup = BundleItem(Meal.algae_soup)
+pale_broth = BundleItem(Meal.pale_broth)
+chowder = BundleItem(Meal.chowder)
+cookie = BundleItem(Meal.cookie)
+ancient_doll = BundleItem(Artifact.ancient_doll)
+ice_cream = BundleItem(Meal.ice_cream)
+cranberry_candy = BundleItem(Meal.cranberry_candy)
+ginger_ale = BundleItem(Beverage.ginger_ale, source=BundleItem.Sources.island)
+pink_cake = BundleItem(Meal.pink_cake)
+plum_pudding = BundleItem(Meal.plum_pudding)
+chocolate_cake = BundleItem(Meal.chocolate_cake)
+rhubarb_pie = BundleItem(Meal.rhubarb_pie)
+shrimp_cocktail = BundleItem(Meal.shrimp_cocktail)
+pina_colada = BundleItem(Beverage.pina_colada, source=BundleItem.Sources.island)
+stuffing = BundleItem(Meal.stuffing)
+magic_rock_candy = BundleItem(Meal.magic_rock_candy)
+spicy_eel = BundleItem(Meal.spicy_eel)
+crab_cakes = BundleItem(Meal.crab_cakes)
+eggplant_parmesan = BundleItem(Meal.eggplant_parmesan)
+pumpkin_soup = BundleItem(Meal.pumpkin_soup)
+lucky_lunch = BundleItem(Meal.lucky_lunch)
+
+green_algae = BundleItem(WaterItem.green_algae)
+white_algae = BundleItem(WaterItem.white_algae)
+geode = BundleItem(Geode.geode)
+frozen_geode = BundleItem(Geode.frozen)
+magma_geode = BundleItem(Geode.magma)
+omni_geode = BundleItem(Geode.omni)
+sap = BundleItem(Material.sap)
+
+dwarf_scroll_1 = BundleItem(Artifact.dwarf_scroll_i)
+dwarf_scroll_2 = BundleItem(Artifact.dwarf_scroll_ii)
+dwarf_scroll_3 = BundleItem(Artifact.dwarf_scroll_iii)
+dwarf_scroll_4 = BundleItem(Artifact.dwarf_scroll_iv)
+elvish_jewelry = BundleItem(Artifact.elvish_jewelry)
+ancient_drum = BundleItem(Artifact.ancient_drum)
+dried_starfish = BundleItem(Fossil.dried_starfish)
+bone_fragment = BundleItem(Fossil.bone_fragment)
+
+golden_mask = BundleItem(Artifact.golden_mask)
+golden_relic = BundleItem(Artifact.golden_relic)
+dwarf_gadget = BundleItem(Artifact.dwarf_gadget)
+dwarvish_helm = BundleItem(Artifact.dwarvish_helm)
+prehistoric_handaxe = BundleItem(Artifact.prehistoric_handaxe)
+bone_flute = BundleItem(Artifact.bone_flute)
+anchor = BundleItem(Artifact.anchor)
+prehistoric_tool = BundleItem(Artifact.prehistoric_tool)
+chicken_statue = BundleItem(Artifact.chicken_statue)
+rusty_cog = BundleItem(Artifact.rusty_cog)
+rusty_spur = BundleItem(Artifact.rusty_spur)
+rusty_spoon = BundleItem(Artifact.rusty_spoon)
+ancient_sword = BundleItem(Artifact.ancient_sword)
+ornamental_fan = BundleItem(Artifact.ornamental_fan)
+chipped_amphora = BundleItem(Artifact.chipped_amphora)
+
+prehistoric_scapula = BundleItem(Fossil.prehistoric_scapula)
+prehistoric_tibia = BundleItem(Fossil.prehistoric_tibia)
+prehistoric_skull = BundleItem(Fossil.prehistoric_skull)
+skeletal_hand = BundleItem(Fossil.skeletal_hand)
+prehistoric_rib = BundleItem(Fossil.prehistoric_rib)
+prehistoric_vertebra = BundleItem(Fossil.prehistoric_vertebra)
+skeletal_tail = BundleItem(Fossil.skeletal_tail)
+nautilus_fossil = BundleItem(Fossil.nautilus_fossil)
+amphibian_fossil = BundleItem(Fossil.amphibian_fossil)
+palm_fossil = BundleItem(Fossil.palm_fossil)
+trilobite = BundleItem(Fossil.trilobite)
+
+dinosaur_mayo = BundleItem(ArtisanGood.dinosaur_mayonnaise)
+void_mayo = BundleItem(ArtisanGood.void_mayonnaise)
+prismatic_shard = BundleItem(Mineral.prismatic_shard)
+diamond = BundleItem(Mineral.diamond)
+ancient_fruit = BundleItem(Fruit.ancient_fruit)
+void_salmon = BundleItem(Fish.void_salmon)
+tea_leaves = BundleItem(Vegetable.tea_leaves)
+blobfish = BundleItem(Fish.blobfish)
+spook_fish = BundleItem(Fish.spook_fish)
+lionfish = BundleItem(Fish.lionfish, source=BundleItem.Sources.island)
+blue_discus = BundleItem(Fish.blue_discus, source=BundleItem.Sources.island)
+stingray = BundleItem(Fish.stingray, source=BundleItem.Sources.island)
+spookfish = BundleItem(Fish.spookfish)
+midnight_squid = BundleItem(Fish.midnight_squid)
+
+angler = BundleItem(Fish.angler)
+crimsonfish = BundleItem(Fish.crimsonfish)
+mutant_carp = BundleItem(Fish.mutant_carp)
+glacierfish = BundleItem(Fish.glacierfish)
+legend = BundleItem(Fish.legend)
+
+spinner = BundleItem(Fishing.spinner)
+dressed_spinner = BundleItem(Fishing.dressed_spinner)
+trap_bobber = BundleItem(Fishing.trap_bobber)
+sonar_bobber = BundleItem(Fishing.sonar_bobber)
+cork_bobber = BundleItem(Fishing.cork_bobber)
+lead_bobber = BundleItem(Fishing.lead_bobber)
+treasure_hunter = BundleItem(Fishing.treasure_hunter)
+barbed_hook = BundleItem(Fishing.barbed_hook)
+curiosity_lure = BundleItem(Fishing.curiosity_lure)
+quality_bobber = BundleItem(Fishing.quality_bobber)
+bait = BundleItem(Fishing.bait, 100)
+deluxe_bait = BundleItem(Fishing.deluxe_bait, 50)
+magnet = BundleItem(Fishing.magnet)
+wild_bait = BundleItem(Fishing.wild_bait, 20)
+magic_bait = BundleItem(Fishing.magic_bait, 10, source=BundleItem.Sources.island)
+pearl = BundleItem(Gift.pearl)
+challenge_bait = BundleItem(Fishing.challenge_bait, 25, source=BundleItem.Sources.masteries)
+targeted_bait = BundleItem(ArtisanGood.targeted_bait, 25, source=BundleItem.Sources.content)
+
+ginger = BundleItem(Forageable.ginger, source=BundleItem.Sources.content)
+magma_cap = BundleItem(Mushroom.magma_cap, source=BundleItem.Sources.content)
+
+wheat_flour = BundleItem(Ingredient.wheat_flour)
+sugar = BundleItem(Ingredient.sugar)
+vinegar = BundleItem(Ingredient.vinegar)
+
+jack_o_lantern = BundleItem(Lighting.jack_o_lantern)
+prize_ticket = BundleItem(Currency.prize_ticket)
+mystery_box = BundleItem(Consumable.mystery_box)
+gold_mystery_box = BundleItem(Consumable.gold_mystery_box, source=BundleItem.Sources.masteries)
+calico_egg = BundleItem(Currency.calico_egg)
+
+raccoon_crab_pot_fish_items = [periwinkle.as_amount(5), snail.as_amount(5), crayfish.as_amount(5), mussel.as_amount(5),
+ oyster.as_amount(5), cockle.as_amount(5), clam.as_amount(5)]
+raccoon_smoked_fish_items = [BundleItem(ArtisanGood.smoked_fish, flavor=fish) for fish in
+ [Fish.largemouth_bass, Fish.bream, Fish.bullhead, Fish.chub, Fish.ghostfish, Fish.flounder, Fish.shad,
+ Fish.rainbow_trout, Fish.tilapia, Fish.red_mullet, Fish.tuna, Fish.midnight_carp, Fish.salmon, Fish.perch]]
+raccoon_fish_items_flat = [*raccoon_crab_pot_fish_items, *raccoon_smoked_fish_items]
+raccoon_fish_items_deep = [raccoon_crab_pot_fish_items, raccoon_smoked_fish_items]
+raccoon_fish_bundle_vanilla = DeepBundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_fish, raccoon_fish_items_deep, 2, 2)
+raccoon_fish_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_fish, raccoon_fish_items_flat, 3, 2)
+
+all_specific_jellies = [BundleItem(ArtisanGood.jelly, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits]
+all_specific_pickles = [BundleItem(ArtisanGood.pickles, flavor=vegetable, source=BundleItem.Sources.content) for vegetable in all_vegetables]
+all_specific_dried_fruits = [*[BundleItem(ArtisanGood.dried_fruit, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits],
+ BundleItem(ArtisanGood.raisins, source=BundleItem.Sources.content)]
+all_specific_juices = [BundleItem(ArtisanGood.juice, flavor=vegetable, source=BundleItem.Sources.content) for vegetable in all_vegetables]
+raccoon_artisan_items = [*all_specific_jellies, *all_specific_pickles, *all_specific_dried_fruits, *all_specific_juices]
+raccoon_artisan_bundle_vanilla = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_artisan, raccoon_artisan_items, 2, 2)
+raccoon_artisan_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_artisan, raccoon_artisan_items, 3, 2)
+
+all_specific_dried_mushrooms = [BundleItem(ArtisanGood.dried_mushroom, flavor=mushroom, source=BundleItem.Sources.content) for mushroom in all_edible_mushrooms]
+raccoon_food_items = [egg.as_amount(5), cave_carrot.as_amount(5), white_algae.as_amount(5)]
+raccoon_food_items_vanilla = [all_specific_dried_mushrooms, raccoon_food_items]
+raccoon_food_items_thematic = [*all_specific_dried_mushrooms, *raccoon_food_items, brown_egg.as_amount(5), large_egg.as_amount(2), large_brown_egg.as_amount(2),
+ green_algae.as_amount(10)]
+raccoon_food_bundle_vanilla = DeepBundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_food, raccoon_food_items_vanilla, 2, 2)
+raccoon_food_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_food, raccoon_food_items_thematic, 3, 2)
+
+raccoon_foraging_items = [moss, rusty_spoon, trash.as_amount(5), slime.as_amount(99), bat_wing.as_amount(10), geode.as_amount(8),
+ frozen_geode.as_amount(5), magma_geode.as_amount(3), coral.as_amount(4), sea_urchin.as_amount(2), bug_meat.as_amount(10),
+ diamond, topaz.as_amount(3), ghostfish.as_amount(3)]
+raccoon_foraging_bundle_vanilla = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_foraging, raccoon_foraging_items, 2, 2)
+raccoon_foraging_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_foraging, raccoon_foraging_items, 3, 2)
+
+raccoon_bundles_vanilla = [raccoon_fish_bundle_vanilla, raccoon_artisan_bundle_vanilla, raccoon_food_bundle_vanilla, raccoon_foraging_bundle_vanilla]
+raccoon_bundles_thematic = [raccoon_fish_bundle_thematic, raccoon_artisan_bundle_thematic, raccoon_food_bundle_thematic, raccoon_foraging_bundle_thematic]
+raccoon_bundles_remixed = raccoon_bundles_thematic
+raccoon_vanilla = BundleRoomTemplate(CCRoom.raccoon_requests, raccoon_bundles_vanilla, 8)
+raccoon_thematic = BundleRoomTemplate(CCRoom.raccoon_requests, raccoon_bundles_thematic, 8)
+raccoon_remixed = BundleRoomTemplate(CCRoom.raccoon_requests, raccoon_bundles_remixed, 8)
+
+# Crafts Room
+spring_foraging_items_vanilla = [wild_horseradish, daffodil, leek, dandelion]
+spring_foraging_items_thematic = [*spring_foraging_items_vanilla, spring_onion, salmonberry, morel]
+spring_foraging_bundle_vanilla = BundleTemplate(CCRoom.crafts_room, BundleName.spring_foraging, spring_foraging_items_vanilla, 4, 4)
+spring_foraging_bundle_thematic = BundleTemplate.extend_from(spring_foraging_bundle_vanilla, spring_foraging_items_thematic)
+
+summer_foraging_items_vanilla = [grape, spice_berry, sweet_pea]
+summer_foraging_items_thematic = [*summer_foraging_items_vanilla, fiddlehead_fern, red_mushroom, rainbow_shell]
+summer_foraging_bundle_vanilla = BundleTemplate(CCRoom.crafts_room, BundleName.summer_foraging, summer_foraging_items_vanilla, 3, 3)
+summer_foraging_bundle_thematic = BundleTemplate.extend_from(summer_foraging_bundle_vanilla, summer_foraging_items_thematic)
+
+fall_foraging_items_vanilla = [common_mushroom, wild_plum, hazelnut, blackberry]
+fall_foraging_items_thematic = [*fall_foraging_items_vanilla, chanterelle]
+fall_foraging_bundle_vanilla = BundleTemplate(CCRoom.crafts_room, BundleName.fall_foraging, fall_foraging_items_vanilla, 4, 4)
+fall_foraging_bundle_thematic = BundleTemplate.extend_from(fall_foraging_bundle_vanilla, fall_foraging_items_thematic)
+
+winter_foraging_items_vanilla = [winter_root, crystal_fruit, snow_yam, crocus]
+winter_foraging_items_thematic = [*winter_foraging_items_vanilla, holly, nautilus_shell]
+winter_foraging_bundle_vanilla = BundleTemplate(CCRoom.crafts_room, BundleName.winter_foraging, winter_foraging_items_vanilla, 4, 4)
+winter_foraging_bundle_thematic = BundleTemplate.extend_from(winter_foraging_bundle_vanilla, winter_foraging_items_thematic)
+
+construction_items_vanilla = [wood, stone, hardwood]
+construction_items_thematic = [*construction_items_vanilla, clay, fiber, sap.as_amount(50)]
+construction_bundle_vanilla = BundleTemplate(CCRoom.crafts_room, BundleName.construction, construction_items_vanilla, 4, 4)
+construction_bundle_thematic = BundleTemplate.extend_from(construction_bundle_vanilla, construction_items_thematic)
+
+exotic_foraging_items_vanilla = [coconut, cactus_fruit, cave_carrot, red_mushroom, purple_mushroom, maple_syrup, oak_resin, pine_tar, morel]
+exotic_foraging_items_thematic = [*exotic_foraging_items_vanilla, coral, sea_urchin, clam, cockle, mussel, oyster, seaweed]
+exotic_foraging_bundle_vanilla = BundleTemplate(CCRoom.crafts_room, BundleName.exotic_foraging, exotic_foraging_items_vanilla, 9, 5)
+exotic_foraging_bundle_thematic = BundleTemplate.extend_from(exotic_foraging_bundle_vanilla, exotic_foraging_items_thematic)
+
+beach_foraging_items = [nautilus_shell, coral, sea_urchin, rainbow_shell, clam, cockle, mussel, oyster, seaweed]
+beach_foraging_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.beach_foraging, beach_foraging_items, 4, 4)
+
+mines_foraging_items = [quartz, earth_crystal, frozen_tear, fire_quartz, red_mushroom, purple_mushroom, cave_carrot]
+mines_foraging_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.mines_foraging, mines_foraging_items, 4, 4)
+
+desert_foraging_items = [cactus_fruit.as_quality(ForageQuality.gold), cactus_fruit.as_amount(5), coconut.as_quality(ForageQuality.gold), coconut.as_amount(5)]
+desert_foraging_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.desert_foraging, desert_foraging_items, 2, 2)
+
+island_foraging_items = [ginger.as_amount(5), magma_cap.as_quality(ForageQuality.gold), magma_cap.as_amount(5),
+ fiddlehead_fern.as_quality(ForageQuality.gold), fiddlehead_fern.as_amount(5)]
+island_foraging_bundle = IslandBundleTemplate(CCRoom.crafts_room, BundleName.island_foraging, island_foraging_items, 2, 2)
+
+sticky_items = [sap.as_amount(500), sap.as_amount(500)]
+sticky_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.sticky, sticky_items, 1, 1)
+
+forest_items = [moss, fiber.as_amount(200), acorn.as_amount(10), maple_seed.as_amount(10), pine_cone.as_amount(10), mahogany_seed,
+ mushroom_tree_seed, mossy_seed.as_amount(5), mystic_tree_seed]
+forest_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.forest, forest_items, 4, 2)
+
+wild_medicine_items = [item.as_amount(5) for item in [purple_mushroom, fiddlehead_fern, white_algae, hops, blackberry, dandelion]]
+wild_medicine_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.wild_medicine, wild_medicine_items, 4, 3)
+
+quality_foraging_items = sorted({item.as_quality(ForageQuality.gold).as_amount(3)
+ for item in
+ [*spring_foraging_items_thematic, *summer_foraging_items_thematic, *fall_foraging_items_thematic,
+ *winter_foraging_items_thematic, *beach_foraging_items, *desert_foraging_items, magma_cap] if item.can_have_quality})
+quality_foraging_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.quality_foraging, quality_foraging_items, 4, 3)
+
+green_rain_items = [moss.as_amount(200), fiber.as_amount(200), mossy_seed.as_amount(20), fiddlehead_fern.as_amount(10)]
+green_rain_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.green_rain, green_rain_items, 4, 3)
+
+crafts_room_bundles_vanilla = [spring_foraging_bundle_vanilla, summer_foraging_bundle_vanilla, fall_foraging_bundle_vanilla,
+ winter_foraging_bundle_vanilla, construction_bundle_vanilla, exotic_foraging_bundle_vanilla]
+crafts_room_bundles_thematic = [spring_foraging_bundle_thematic, summer_foraging_bundle_thematic, fall_foraging_bundle_thematic,
+ winter_foraging_bundle_thematic, construction_bundle_thematic, exotic_foraging_bundle_thematic]
+crafts_room_bundles_remixed = [*crafts_room_bundles_thematic, beach_foraging_bundle, mines_foraging_bundle, desert_foraging_bundle,
+ island_foraging_bundle, sticky_bundle, forest_bundle, wild_medicine_bundle, quality_foraging_bundle, green_rain_bundle]
+crafts_room_vanilla = BundleRoomTemplate(CCRoom.crafts_room, crafts_room_bundles_vanilla, 6)
+crafts_room_thematic = BundleRoomTemplate(CCRoom.crafts_room, crafts_room_bundles_thematic, 6)
+crafts_room_remixed = BundleRoomTemplate(CCRoom.crafts_room, crafts_room_bundles_remixed, 6)
+
+# Pantry
+spring_crops_items_vanilla = [parsnip, green_bean, cauliflower, potato]
+spring_crops_items_thematic = [*spring_crops_items_vanilla, blue_jazz, coffee_bean, garlic, kale, rhubarb, strawberry, tulip, unmilled_rice, carrot]
+spring_crops_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.spring_crops, spring_crops_items_vanilla, 4, 4)
+spring_crops_bundle_thematic = BundleTemplate.extend_from(spring_crops_bundle_vanilla, spring_crops_items_thematic)
+
+summer_crops_items_vanilla = [tomato, hot_pepper, blueberry, melon]
+summer_crops_items_thematic = [*summer_crops_items_vanilla, corn, hops, poppy, radish, red_cabbage, starfruit, summer_spangle, sunflower, wheat, summer_squash]
+summer_crops_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.summer_crops, summer_crops_items_vanilla, 4, 4)
+summer_crops_bundle_thematic = BundleTemplate.extend_from(summer_crops_bundle_vanilla, summer_crops_items_thematic)
+
+fall_crops_items_vanilla = [corn, eggplant, pumpkin, yam]
+fall_crops_items_thematic = [*fall_crops_items_vanilla, amaranth, artichoke, beet, bok_choy, cranberries, fairy_rose, grape,
+ sunflower, wheat, sweet_gem_berry, broccoli]
+fall_crops_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.fall_crops, fall_crops_items_vanilla, 4, 4)
+fall_crops_bundle_thematic = BundleTemplate.extend_from(fall_crops_bundle_vanilla, fall_crops_items_thematic)
+
+all_crops_items = sorted({*spring_crops_items_thematic, *summer_crops_items_thematic, *fall_crops_items_thematic, powdermelon})
+
+quality_crops_items_vanilla = [item.as_quality_crop() for item in [parsnip, melon, pumpkin, corn]]
+quality_crops_items_thematic = [item.as_quality_crop() for item in all_crops_items]
+quality_crops_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.quality_crops, quality_crops_items_vanilla, 4, 3)
+quality_crops_bundle_thematic = BundleTemplate.extend_from(quality_crops_bundle_vanilla, quality_crops_items_thematic)
+
+animal_items_vanilla = [large_milk, large_brown_egg, large_egg, large_goat_milk, wool, duck_egg]
+animal_items_thematic = [*animal_items_vanilla, egg, brown_egg, milk, goat_milk, truffle,
+ duck_feather, rabbit_foot, dinosaur_egg, void_egg, golden_egg, ostrich_egg]
+animal_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.animal, animal_items_vanilla, 6, 5)
+animal_bundle_thematic = BundleTemplate.extend_from(animal_bundle_vanilla, animal_items_thematic)
+
+artisan_items_vanilla = [truffle_oil, cloth, goat_cheese, cheese, honey, jelly, apple, apricot, orange, peach, pomegranate, cherry]
+artisan_items_thematic = [*artisan_items_vanilla, beer, juice, mead, pale_ale, wine, pickles, caviar, aged_roe, coffee, green_tea, banana, mango]
+artisan_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.artisan, artisan_items_vanilla, 12, 6)
+artisan_bundle_thematic = BundleTemplate.extend_from(artisan_bundle_vanilla, artisan_items_thematic)
+
+rare_crops_items = [ancient_fruit, sweet_gem_berry]
+rare_crops_bundle = BundleTemplate(CCRoom.pantry, BundleName.rare_crops, rare_crops_items, 2, 2)
+
+# all_specific_roes = [BundleItem(AnimalProduct.roe, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fish]
+fish_farmer_items = [roe.as_amount(15), aged_roe.as_amount(5), squid_ink, caviar.as_amount(5)]
+fish_farmer_bundle = BundleTemplate(CCRoom.pantry, BundleName.fish_farmer, fish_farmer_items, 3, 2)
+
+garden_items = [tulip, blue_jazz, summer_spangle, sunflower, fairy_rose, poppy, bouquet]
+garden_bundle = BundleTemplate(CCRoom.pantry, BundleName.garden, garden_items, 5, 4)
+
+brewer_items = [mead, pale_ale, wine, juice, green_tea, beer]
+brewer_bundle = BundleTemplate(CCRoom.pantry, BundleName.brewer, brewer_items, 5, 4)
+
+orchard_items = [apple, apricot, orange, peach, pomegranate, cherry, banana, mango]
+orchard_bundle = BundleTemplate(CCRoom.pantry, BundleName.orchard, orchard_items, 6, 4)
+
+island_crops_items = [pineapple, taro_root, banana, mango]
+island_crops_bundle = IslandBundleTemplate(CCRoom.pantry, BundleName.island_crops, island_crops_items, 3, 3)
+
+agronomist_items = [basic_fertilizer, quality_fertilizer, deluxe_fertilizer,
+ basic_retaining_soil, quality_retaining_soil, deluxe_retaining_soil,
+ speed_gro, deluxe_speed_gro, hyper_speed_gro, tree_fertilizer]
+agronomist_bundle = BundleTemplate(CCRoom.pantry, BundleName.agronomist, agronomist_items, 4, 3)
+
+slime_farmer_items = [slime.as_amount(99), petrified_slime.as_amount(10), blue_slime_egg, red_slime_egg,
+ purple_slime_egg, green_slime_egg, tiger_slime_egg]
+slime_farmer_bundle = BundleTemplate(CCRoom.pantry, BundleName.slime_farmer, slime_farmer_items, 4, 3)
+
+sommelier_items = [BundleItem(ArtisanGood.wine, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits]
+sommelier_bundle = BundleTemplate(CCRoom.pantry, BundleName.sommelier, sommelier_items, 6, 3)
+
+dry_items = [*[BundleItem(ArtisanGood.dried_fruit, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits],
+ *[BundleItem(ArtisanGood.dried_mushroom, flavor=mushroom, source=BundleItem.Sources.content) for mushroom in all_edible_mushrooms],
+ BundleItem(ArtisanGood.raisins, source=BundleItem.Sources.content)]
+dry_bundle = BundleTemplate(CCRoom.pantry, BundleName.dry, dry_items, 6, 3)
+
+pantry_bundles_vanilla = [spring_crops_bundle_vanilla, summer_crops_bundle_vanilla, fall_crops_bundle_vanilla,
+ quality_crops_bundle_vanilla, animal_bundle_vanilla, artisan_bundle_vanilla]
+pantry_bundles_thematic = [spring_crops_bundle_thematic, summer_crops_bundle_thematic, fall_crops_bundle_thematic,
+ quality_crops_bundle_thematic, animal_bundle_thematic, artisan_bundle_thematic]
+pantry_bundles_remixed = [*pantry_bundles_thematic, rare_crops_bundle, fish_farmer_bundle, garden_bundle,
+ brewer_bundle, orchard_bundle, island_crops_bundle, agronomist_bundle, slime_farmer_bundle, sommelier_bundle, dry_bundle]
+pantry_vanilla = BundleRoomTemplate(CCRoom.pantry, pantry_bundles_vanilla, 6)
+pantry_thematic = BundleRoomTemplate(CCRoom.pantry, pantry_bundles_thematic, 6)
+pantry_remixed = BundleRoomTemplate(CCRoom.pantry, pantry_bundles_remixed, 6)
+
+# Fish Tank
+river_fish_items_vanilla = [sunfish, catfish, shad, tiger_trout]
+river_fish_items_thematic = [*river_fish_items_vanilla, chub, rainbow_trout, lingcod, walleye, perch, pike, bream, salmon, smallmouth_bass, dorado]
+river_fish_bundle_vanilla = BundleTemplate(CCRoom.fish_tank, BundleName.river_fish, river_fish_items_vanilla, 4, 4)
+river_fish_bundle_thematic = BundleTemplate.extend_from(river_fish_bundle_vanilla, river_fish_items_thematic)
+
+lake_fish_items_vanilla = [largemouth_bass, carp, bullhead, sturgeon]
+lake_fish_items_thematic = [*lake_fish_items_vanilla, chub, rainbow_trout, lingcod, walleye, perch, midnight_carp]
+lake_fish_bundle_vanilla = BundleTemplate(CCRoom.fish_tank, BundleName.lake_fish, lake_fish_items_vanilla, 4, 4)
+lake_fish_bundle_thematic = BundleTemplate.extend_from(lake_fish_bundle_vanilla, lake_fish_items_thematic)
+
+ocean_fish_items_vanilla = [sardine, tuna, red_snapper, tilapia]
+ocean_fish_items_thematic = [*ocean_fish_items_vanilla, pufferfish, super_cucumber, flounder, anchovy, red_mullet,
+ herring, eel, octopus, squid, sea_cucumber, albacore, halibut]
+ocean_fish_bundle_vanilla = BundleTemplate(CCRoom.fish_tank, BundleName.ocean_fish, ocean_fish_items_vanilla, 4, 4)
+ocean_fish_bundle_thematic = BundleTemplate.extend_from(ocean_fish_bundle_vanilla, ocean_fish_items_thematic)
+
+night_fish_items_vanilla = [walleye, bream, eel]
+night_fish_items_thematic = [*night_fish_items_vanilla, super_cucumber, squid, midnight_carp, midnight_squid]
+night_fish_bundle_vanilla = BundleTemplate(CCRoom.fish_tank, BundleName.night_fish, night_fish_items_vanilla, 3, 3)
+night_fish_bundle_thematic = BundleTemplate.extend_from(night_fish_bundle_vanilla, night_fish_items_thematic)
+
+crab_pot_items_vanilla = [lobster, crayfish, crab, cockle, mussel, shrimp, snail, periwinkle, oyster, clam]
+crab_pot_trash_items = [trash, driftwood, soggy_newspaper, broken_cd, broken_glasses]
+crab_pot_items_thematic = [*crab_pot_items_vanilla, *crab_pot_trash_items]
+crab_pot_bundle_vanilla = BundleTemplate(CCRoom.fish_tank, BundleName.crab_pot, crab_pot_items_vanilla, 10, 5)
+crab_pot_bundle_thematic = BundleTemplate.extend_from(crab_pot_bundle_vanilla, crab_pot_items_thematic)
+trash_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.trash, crab_pot_trash_items, 4, 4)
+
+specialty_fish_items_vanilla = [pufferfish, ghostfish, sandfish, woodskip]
+specialty_fish_items_thematic = [*specialty_fish_items_vanilla, scorpion_carp, eel, octopus, lava_eel, ice_pip,
+ stonefish, void_salmon, stingray, spookfish, midnight_squid]
+specialty_fish_bundle_vanilla = BundleTemplate(CCRoom.fish_tank, BundleName.specialty_fish, specialty_fish_items_vanilla, 4, 4)
+specialty_fish_bundle_thematic = BundleTemplate.extend_from(specialty_fish_bundle_vanilla, specialty_fish_items_thematic)
+
+spring_fish_items = [herring, halibut, shad, flounder, sunfish, sardine, catfish, anchovy, smallmouth_bass, eel, legend]
+spring_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.spring_fish, spring_fish_items, 4, 4)
+
+summer_fish_items = [tuna, pike, red_mullet, sturgeon, red_snapper, super_cucumber, tilapia, pufferfish, rainbow_trout,
+ octopus, dorado, halibut, shad, flounder, sunfish, crimsonfish]
+summer_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.summer_fish, summer_fish_items, 4, 4)
+
+fall_fish_items = [red_snapper, super_cucumber, tilapia, shad, sardine, catfish, anchovy, smallmouth_bass, eel, midnight_carp,
+ walleye, sea_cucumber, tiger_trout, albacore, salmon, angler]
+fall_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.fall_fish, fall_fish_items, 4, 4)
+
+winter_fish_items = [perch, squid, lingcod, tuna, pike, red_mullet, sturgeon, red_snapper, herring, halibut, sardine,
+ midnight_carp, sea_cucumber, tiger_trout, albacore, glacierfish]
+winter_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.winter_fish, winter_fish_items, 4, 4)
+
+rain_fish_items = [red_snapper, shad, catfish, eel, walleye]
+rain_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.rain_fish, rain_fish_items, 3, 3)
+
+quality_fish_items = sorted({
+ item.as_quality(FishQuality.gold).as_amount(2)
+ for item in [*river_fish_items_thematic, *lake_fish_items_thematic, *ocean_fish_items_thematic]
})
-
-bulletin_board_bundle_items = sorted({
- *chef_items,
- *[item for dye_color_items in dye_items for item in dye_color_items],
- *field_research_items,
- *fodder_items,
- *enchanter_items
-})
-
-vault_bundle_items = [
- *vault_2500_items,
- *vault_5000_items,
- *vault_10000_items,
- *vault_25000_items,
-]
-
-all_bundle_items_except_money = sorted({
- *crafts_room_bundle_items,
- *pantry_bundle_items,
- *fish_tank_bundle_items,
- *boiler_room_bundle_items,
- *bulletin_board_bundle_items,
-}, key=lambda x: x.item.name)
-
-all_bundle_items = sorted({
- *crafts_room_bundle_items,
- *pantry_bundle_items,
- *fish_tank_bundle_items,
- *boiler_room_bundle_items,
- *bulletin_board_bundle_items,
- *vault_bundle_items,
-}, key=lambda x: x.item.name)
-
-all_bundle_items_by_name = {item.item.name: item for item in all_bundle_items}
-all_bundle_items_by_id = {item.item.item_id: item for item in all_bundle_items}
+quality_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.quality_fish, quality_fish_items, 4, 3)
+
+master_fisher_items = [lava_eel, scorpion_carp, octopus, blobfish, lingcod, ice_pip, super_cucumber, stingray, void_salmon, pufferfish]
+master_fisher_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.master_fisher, master_fisher_items, 4, 2)
+
+legendary_fish_items = [angler, legend, mutant_carp, crimsonfish, glacierfish]
+legendary_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.legendary_fish, legendary_fish_items, 4, 2)
+
+island_fish_items = [lionfish, blue_discus, stingray]
+island_fish_bundle = IslandBundleTemplate(CCRoom.fish_tank, BundleName.island_fish, island_fish_items, 3, 3)
+
+tackle_items = [spinner, dressed_spinner, trap_bobber, sonar_bobber, cork_bobber, lead_bobber, treasure_hunter, barbed_hook, curiosity_lure, quality_bobber]
+tackle_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.tackle, tackle_items, 3, 2)
+
+bait_items = [bait, magnet, wild_bait, magic_bait, challenge_bait, deluxe_bait, targeted_bait]
+bait_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.bait, bait_items, 3, 2)
+
+# This bundle could change based on content packs, once the fish are properly in it. Until then, I'm not sure how, so pelican town only
+specific_bait_items = [BundleItem(ArtisanGood.targeted_bait, flavor=fish.name).as_amount(10) for fish in content_packs.pelican_town.fishes]
+specific_bait_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.specific_bait, specific_bait_items, 6, 3)
+
+deep_fishing_items = [blobfish, spook_fish, midnight_squid, sea_cucumber, super_cucumber, octopus, pearl, seaweed]
+deep_fishing_bundle = FestivalBundleTemplate(CCRoom.fish_tank, BundleName.deep_fishing, deep_fishing_items, 4, 3)
+
+smokeable_fish = [Fish.largemouth_bass, Fish.bream, Fish.bullhead, Fish.chub, Fish.ghostfish, Fish.flounder, Fish.shad, Fish.rainbow_trout, Fish.tilapia,
+ Fish.red_mullet, Fish.tuna, Fish.midnight_carp, Fish.salmon, Fish.perch]
+fish_smoker_items = [BundleItem(ArtisanGood.smoked_fish, flavor=fish) for fish in smokeable_fish]
+fish_smoker_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.fish_smoker, fish_smoker_items, 6, 3)
+
+fish_tank_bundles_vanilla = [river_fish_bundle_vanilla, lake_fish_bundle_vanilla, ocean_fish_bundle_vanilla,
+ night_fish_bundle_vanilla, crab_pot_bundle_vanilla, specialty_fish_bundle_vanilla]
+fish_tank_bundles_thematic = [river_fish_bundle_thematic, lake_fish_bundle_thematic, ocean_fish_bundle_thematic,
+ night_fish_bundle_thematic, crab_pot_bundle_thematic, specialty_fish_bundle_thematic]
+fish_tank_bundles_remixed = [*fish_tank_bundles_thematic, spring_fish_bundle, summer_fish_bundle, fall_fish_bundle, winter_fish_bundle, trash_bundle,
+ rain_fish_bundle, quality_fish_bundle, master_fisher_bundle, legendary_fish_bundle, tackle_bundle, bait_bundle,
+ specific_bait_bundle, deep_fishing_bundle, fish_smoker_bundle]
+
+# In Remixed, the trash items are in the recycling bundle, so we don't use the thematic version of the crab pot bundle that added trash items to it
+fish_tank_bundles_remixed.remove(crab_pot_bundle_thematic)
+fish_tank_bundles_remixed.append(crab_pot_bundle_vanilla)
+fish_tank_vanilla = BundleRoomTemplate(CCRoom.fish_tank, fish_tank_bundles_vanilla, 6)
+fish_tank_thematic = BundleRoomTemplate(CCRoom.fish_tank, fish_tank_bundles_thematic, 6)
+fish_tank_remixed = BundleRoomTemplate(CCRoom.fish_tank, fish_tank_bundles_remixed, 6)
+
+# Boiler Room
+blacksmith_items_vanilla = [copper_bar, iron_Bar, gold_bar]
+blacksmith_items_thematic = [*blacksmith_items_vanilla, iridium_bar, refined_quartz.as_amount(3), wilted_bouquet]
+blacksmith_bundle_vanilla = BundleTemplate(CCRoom.boiler_room, BundleName.blacksmith, blacksmith_items_vanilla, 3, 3)
+blacksmith_bundle_thematic = BundleTemplate.extend_from(blacksmith_bundle_vanilla, blacksmith_items_thematic)
+
+geologist_items_vanilla = [quartz, earth_crystal, frozen_tear, fire_quartz]
+geologist_items_thematic = [*geologist_items_vanilla, emerald, aquamarine, ruby, amethyst, topaz, jade, diamond]
+geologist_bundle_vanilla = BundleTemplate(CCRoom.boiler_room, BundleName.geologist, geologist_items_vanilla, 4, 4)
+geologist_bundle_thematic = BundleTemplate.extend_from(geologist_bundle_vanilla, geologist_items_thematic)
+
+adventurer_items_vanilla = [slime, bat_wing, solar_essence, void_essence]
+adventurer_items_thematic = [*adventurer_items_vanilla, bug_meat, coal, bone_fragment.as_amount(10)]
+adventurer_bundle_vanilla = BundleTemplate(CCRoom.boiler_room, BundleName.adventurer, adventurer_items_vanilla, 4, 2)
+adventurer_bundle_thematic = BundleTemplate.extend_from(adventurer_bundle_vanilla, adventurer_items_thematic)
+
+# Where to put radioactive bar?
+treasure_hunter_items = [emerald, aquamarine, ruby, amethyst, topaz, jade, diamond]
+treasure_hunter_bundle = BundleTemplate(CCRoom.boiler_room, BundleName.treasure_hunter, treasure_hunter_items, 6, 5)
+
+engineer_items = [iridium_ore.as_amount(5), battery_pack, refined_quartz.as_amount(5), diamond]
+engineer_bundle = BundleTemplate(CCRoom.boiler_room, BundleName.engineer, engineer_items, 3, 3)
+
+demolition_items = [cherry_bomb, bomb, mega_bomb, explosive_ammo]
+demolition_bundle = BundleTemplate(CCRoom.boiler_room, BundleName.demolition, demolition_items, 3, 3)
+
+recycling_items = [stone, coal, iron_ore, wood, cloth, refined_quartz]
+recycling_bundle = BundleTemplate(CCRoom.boiler_room, BundleName.recycling, recycling_items, 4, 4)
+
+archaeologist_items = [golden_mask, golden_relic, ancient_drum, dwarf_gadget, dwarvish_helm, prehistoric_handaxe, bone_flute, anchor, prehistoric_tool,
+ chicken_statue, rusty_cog, rusty_spur, rusty_spoon, ancient_sword, ornamental_fan, elvish_jewelry, ancient_doll, chipped_amphora]
+archaeologist_bundle = BundleTemplate(CCRoom.boiler_room, BundleName.archaeologist, archaeologist_items, 6, 3)
+
+paleontologist_items = [prehistoric_scapula, prehistoric_tibia, prehistoric_skull, skeletal_hand, prehistoric_rib, prehistoric_vertebra, skeletal_tail,
+ nautilus_fossil, amphibian_fossil, palm_fossil, trilobite]
+paleontologist_bundle = BundleTemplate(CCRoom.boiler_room, BundleName.paleontologist, paleontologist_items, 6, 3)
+
+boiler_room_bundles_vanilla = [blacksmith_bundle_vanilla, geologist_bundle_vanilla, adventurer_bundle_vanilla]
+boiler_room_bundles_thematic = [blacksmith_bundle_thematic, geologist_bundle_thematic, adventurer_bundle_thematic]
+boiler_room_bundles_remixed = [*boiler_room_bundles_thematic, treasure_hunter_bundle, engineer_bundle,
+ demolition_bundle, recycling_bundle, archaeologist_bundle, paleontologist_bundle]
+boiler_room_vanilla = BundleRoomTemplate(CCRoom.boiler_room, boiler_room_bundles_vanilla, 3)
+boiler_room_thematic = BundleRoomTemplate(CCRoom.boiler_room, boiler_room_bundles_thematic, 3)
+boiler_room_remixed = BundleRoomTemplate(CCRoom.boiler_room, boiler_room_bundles_remixed, 3)
+
+# Bulletin Board
+chef_items_vanilla = [maple_syrup, fiddlehead_fern, truffle, poppy, maki_roll, fried_egg]
+# More recipes?
+chef_items_thematic = [maki_roll, fried_egg, omelet, pizza, hashbrowns, pancakes, bread, tortilla,
+ farmer_s_lunch, survival_burger, dish_o_the_sea, miner_s_treat, roots_platter, salad,
+ cheese_cauliflower, parsnip_soup, fried_mushroom, salmon_dinner, pepper_poppers, spaghetti,
+ sashimi, blueberry_tart, algae_soup, pale_broth, chowder]
+chef_bundle_vanilla = BundleTemplate(CCRoom.bulletin_board, BundleName.chef, chef_items_vanilla, 6, 6)
+chef_bundle_thematic = BundleTemplate.extend_from(chef_bundle_vanilla, chef_items_thematic)
+
+dye_items_vanilla = [red_mushroom, sea_urchin, sunflower, duck_feather, aquamarine, red_cabbage]
+dye_red_items = [cranberries, hot_pepper, radish, rhubarb, spaghetti, strawberry, tomato, tulip, red_mushroom]
+dye_orange_items = [poppy, pumpkin, apricot, orange, spice_berry, winter_root]
+dye_yellow_items = [corn, parsnip, summer_spangle, sunflower, starfruit]
+dye_green_items = [fiddlehead_fern, kale, artichoke, bok_choy, green_bean, cactus_fruit, duck_feather, dinosaur_egg]
+dye_blue_items = [blueberry, blue_jazz, blackberry, crystal_fruit, aquamarine]
+dye_purple_items = [beet, crocus, eggplant, red_cabbage, sweet_pea, iridium_bar, sea_urchin, amaranth]
+dye_items_thematic = [dye_red_items, dye_orange_items, dye_yellow_items, dye_green_items, dye_blue_items, dye_purple_items]
+dye_bundle_vanilla = BundleTemplate(CCRoom.bulletin_board, BundleName.dye, dye_items_vanilla, 6, 6)
+dye_bundle_thematic = DeepBundleTemplate(CCRoom.bulletin_board, BundleName.dye, dye_items_thematic, 6, 6)
+
+field_research_items_vanilla = [purple_mushroom, nautilus_shell, chub, frozen_geode]
+field_research_items_thematic = [*field_research_items_vanilla, geode, magma_geode, omni_geode,
+ rainbow_shell, amethyst, bream, carp]
+field_research_bundle_vanilla = BundleTemplate(CCRoom.bulletin_board, BundleName.field_research, field_research_items_vanilla, 4, 4)
+field_research_bundle_thematic = BundleTemplate.extend_from(field_research_bundle_vanilla, field_research_items_thematic)
+
+fodder_items_vanilla = [wheat.as_amount(10), hay.as_amount(10), apple.as_amount(3)]
+fodder_items_thematic = [*fodder_items_vanilla, kale.as_amount(3), corn.as_amount(3), green_bean.as_amount(3),
+ potato.as_amount(3), green_algae.as_amount(5), white_algae.as_amount(3)]
+fodder_bundle_vanilla = BundleTemplate(CCRoom.bulletin_board, BundleName.fodder, fodder_items_vanilla, 3, 3)
+fodder_bundle_thematic = BundleTemplate.extend_from(fodder_bundle_vanilla, fodder_items_thematic)
+
+enchanter_items_vanilla = [oak_resin, wine, rabbit_foot, pomegranate]
+enchanter_items_thematic = [*enchanter_items_vanilla, purple_mushroom, solar_essence,
+ super_cucumber, void_essence, fire_quartz, frozen_tear, jade]
+enchanter_bundle_vanilla = BundleTemplate(CCRoom.bulletin_board, BundleName.enchanter, enchanter_items_vanilla, 4, 4)
+enchanter_bundle_thematic = BundleTemplate.extend_from(enchanter_bundle_vanilla, enchanter_items_thematic)
+
+children_items = [salmonberry.as_amount(10), cookie, ancient_doll, ice_cream, cranberry_candy, ginger_ale,
+ grape.as_amount(10), pink_cake, snail, fairy_rose, plum_pudding]
+children_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.children, children_items, 4, 3)
+
+forager_items = [salmonberry.as_amount(50), blackberry.as_amount(50), wild_plum.as_amount(20), snow_yam.as_amount(20),
+ common_mushroom.as_amount(20), grape.as_amount(20), spring_onion.as_amount(20)]
+forager_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.forager, forager_items, 3, 2)
+
+home_cook_items = [egg.as_amount(10), milk.as_amount(10), wheat_flour.as_amount(100), sugar.as_amount(100), vinegar.as_amount(100),
+ chocolate_cake, pancakes, rhubarb_pie]
+home_cook_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.home_cook, home_cook_items, 3, 3)
+
+helper_items = [prize_ticket, mystery_box.as_amount(5), gold_mystery_box]
+helper_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.helper, helper_items, 2, 2)
+
+spirit_eve_items = [jack_o_lantern, corn.as_amount(10), bat_wing.as_amount(10)]
+spirit_eve_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.spirit_eve, spirit_eve_items, 3, 3)
+
+winter_star_items = [holly.as_amount(5), plum_pudding, stuffing, powdermelon.as_amount(5)]
+winter_star_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.winter_star, winter_star_items, 2, 2)
+
+bartender_items = [shrimp_cocktail, triple_shot_espresso, ginger_ale, cranberry_candy, beer, pale_ale, pina_colada]
+bartender_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.bartender, bartender_items, 3, 3)
+
+calico_items = [calico_egg.as_amount(200), calico_egg.as_amount(200), calico_egg.as_amount(200), calico_egg.as_amount(200),
+ magic_rock_candy, mega_bomb.as_amount(10), mystery_box.as_amount(10), mixed_seeds.as_amount(50),
+ strawberry_seeds.as_amount(20),
+ spicy_eel.as_amount(5), crab_cakes.as_amount(5), eggplant_parmesan.as_amount(5),
+ pumpkin_soup.as_amount(5), lucky_lunch.as_amount(5),]
+calico_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.calico, calico_items, 2, 2)
+
+raccoon_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.raccoon, raccoon_foraging_items, 4, 4)
+
+bulletin_board_bundles_vanilla = [chef_bundle_vanilla, dye_bundle_vanilla, field_research_bundle_vanilla, fodder_bundle_vanilla, enchanter_bundle_vanilla]
+bulletin_board_bundles_thematic = [chef_bundle_thematic, dye_bundle_thematic, field_research_bundle_thematic, fodder_bundle_thematic, enchanter_bundle_thematic]
+bulletin_board_bundles_remixed = [*bulletin_board_bundles_thematic, children_bundle, forager_bundle, home_cook_bundle,
+ helper_bundle, spirit_eve_bundle, winter_star_bundle, bartender_bundle, calico_bundle, raccoon_bundle]
+bulletin_board_vanilla = BundleRoomTemplate(CCRoom.bulletin_board, bulletin_board_bundles_vanilla, 5)
+bulletin_board_thematic = BundleRoomTemplate(CCRoom.bulletin_board, bulletin_board_bundles_thematic, 5)
+bulletin_board_remixed = BundleRoomTemplate(CCRoom.bulletin_board, bulletin_board_bundles_remixed, 5)
+
+missing_bundle_items_vanilla = [wine.as_quality(ArtisanQuality.silver), dinosaur_mayo, prismatic_shard, caviar,
+ ancient_fruit.as_quality_crop(), void_salmon.as_quality(FishQuality.gold)]
+missing_bundle_items_thematic = [*missing_bundle_items_vanilla, pale_ale.as_quality(ArtisanQuality.silver), beer.as_quality(ArtisanQuality.silver),
+ mead.as_quality(ArtisanQuality.silver),
+ cheese.as_quality(ArtisanQuality.silver), goat_cheese.as_quality(ArtisanQuality.silver), void_mayo, cloth, green_tea,
+ truffle_oil, diamond,
+ sweet_gem_berry.as_quality_crop(), starfruit.as_quality_crop(),
+ tea_leaves.as_amount(5), lava_eel.as_quality(FishQuality.gold), scorpion_carp.as_quality(FishQuality.gold),
+ blobfish.as_quality(FishQuality.gold)]
+missing_bundle_vanilla = BundleTemplate(CCRoom.abandoned_joja_mart, BundleName.missing_bundle, missing_bundle_items_vanilla, 6, 5)
+missing_bundle_thematic = BundleTemplate.extend_from(missing_bundle_vanilla, missing_bundle_items_thematic)
+
+abandoned_joja_mart_bundles_vanilla = [missing_bundle_vanilla]
+abandoned_joja_mart_bundles_thematic = [missing_bundle_thematic]
+abandoned_joja_mart_vanilla = BundleRoomTemplate(CCRoom.abandoned_joja_mart, abandoned_joja_mart_bundles_vanilla, 1)
+abandoned_joja_mart_thematic = BundleRoomTemplate(CCRoom.abandoned_joja_mart, abandoned_joja_mart_bundles_thematic, 1)
+abandoned_joja_mart_remixed = abandoned_joja_mart_thematic
+
+vault_2500_gold = BundleItem.money_bundle(2500)
+vault_5000_gold = BundleItem.money_bundle(5000)
+vault_10000_gold = BundleItem.money_bundle(10000)
+vault_25000_gold = BundleItem.money_bundle(25000)
+
+vault_2500_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_2500, vault_2500_gold)
+vault_5000_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_5000, vault_5000_gold)
+vault_10000_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_10000, vault_10000_gold)
+vault_25000_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_25000, vault_25000_gold)
+
+vault_gambler_items = BundleItem(Currency.qi_coin, 10000)
+vault_gambler_bundle = CurrencyBundleTemplate(CCRoom.vault, BundleName.gambler, vault_gambler_items)
+
+vault_carnival_items = BundleItem(Currency.star_token, 2500, source=BundleItem.Sources.festival)
+vault_carnival_bundle = CurrencyBundleTemplate(CCRoom.vault, BundleName.carnival, vault_carnival_items)
+
+vault_walnut_hunter_items = BundleItem(Currency.golden_walnut, 25)
+vault_walnut_hunter_bundle = CurrencyBundleTemplate(CCRoom.vault, BundleName.walnut_hunter, vault_walnut_hunter_items)
+
+vault_qi_helper_items = BundleItem(Currency.qi_gem, 25, source=BundleItem.Sources.island)
+vault_qi_helper_bundle = CurrencyBundleTemplate(CCRoom.vault, BundleName.qi_helper, vault_qi_helper_items)
+
+vault_bundles_vanilla = [vault_2500_bundle, vault_5000_bundle, vault_10000_bundle, vault_25000_bundle]
+vault_bundles_thematic = vault_bundles_vanilla
+vault_bundles_remixed = [*vault_bundles_vanilla, vault_gambler_bundle, vault_qi_helper_bundle, vault_carnival_bundle] # , vault_walnut_hunter_bundle
+vault_vanilla = BundleRoomTemplate(CCRoom.vault, vault_bundles_vanilla, 4)
+vault_thematic = BundleRoomTemplate(CCRoom.vault, vault_bundles_thematic, 4)
+vault_remixed = BundleRoomTemplate(CCRoom.vault, vault_bundles_remixed, 4)
+
+all_cc_remixed_bundles = [*crafts_room_bundles_remixed, *pantry_bundles_remixed, *fish_tank_bundles_remixed,
+ *boiler_room_bundles_remixed, *bulletin_board_bundles_remixed]
+community_center_remixed_anywhere = BundleRoomTemplate("Community Center", all_cc_remixed_bundles, 26)
+
+all_bundle_items_except_money = []
+all_remixed_bundles = [*crafts_room_bundles_remixed, *pantry_bundles_remixed, *fish_tank_bundles_remixed,
+ *boiler_room_bundles_remixed, *bulletin_board_bundles_remixed, missing_bundle_thematic,
+ *raccoon_bundles_remixed]
+for bundle in all_remixed_bundles:
+ all_bundle_items_except_money.extend(bundle.items)
+
+all_bundle_items_by_name = {item.item_name: item for item in all_bundle_items_except_money}
diff --git a/worlds/stardew_valley/data/common_data.py b/worlds/stardew_valley/data/common_data.py
deleted file mode 100644
index 8a2d0f5eecfc..000000000000
--- a/worlds/stardew_valley/data/common_data.py
+++ /dev/null
@@ -1,9 +0,0 @@
-fishing_chest = "Fishing Chest"
-secret_note = "Secret Note"
-
-quality_dict = {
- 0: "",
- 1: "Silver",
- 2: "Gold",
- 3: "Iridium"
-}
diff --git a/worlds/stardew_valley/data/craftable_data.py b/worlds/stardew_valley/data/craftable_data.py
new file mode 100644
index 000000000000..d83478a62051
--- /dev/null
+++ b/worlds/stardew_valley/data/craftable_data.py
@@ -0,0 +1,374 @@
+from typing import Dict, List, Optional
+
+from .recipe_source import RecipeSource, StarterSource, QueenOfSauceSource, ShopSource, SkillSource, FriendshipSource, ShopTradeSource, CutsceneSource, \
+ ArchipelagoSource, LogicSource, SpecialOrderSource, FestivalShopSource, QuestSource, MasterySource
+from ..mods.mod_data import ModNames
+from ..strings.animal_product_names import AnimalProduct
+from ..strings.artisan_good_names import ArtisanGood
+from ..strings.craftable_names import Bomb, Fence, Sprinkler, WildSeeds, Floor, Fishing, Ring, Consumable, Edible, Lighting, Storage, Furniture, Sign, \
+ Craftable, \
+ ModEdible, ModCraftable, ModMachine, ModFloor, ModConsumable, Statue
+from ..strings.crop_names import Fruit, Vegetable
+from ..strings.currency_names import Currency
+from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro
+from ..strings.fish_names import Fish, WaterItem, ModTrash
+from ..strings.flower_names import Flower
+from ..strings.food_names import Meal
+from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom
+from ..strings.gift_names import Gift
+from ..strings.ingredient_names import Ingredient
+from ..strings.machine_names import Machine
+from ..strings.material_names import Material
+from ..strings.metal_names import Ore, MetalBar, Fossil, Artifact, Mineral, ModFossil
+from ..strings.monster_drop_names import Loot, ModLoot
+from ..strings.quest_names import Quest
+from ..strings.region_names import Region, SVERegion, LogicRegion
+from ..strings.seed_names import Seed, TreeSeed
+from ..strings.skill_names import Skill, ModSkill
+from ..strings.special_order_names import SpecialOrder
+from ..strings.villager_names import NPC, ModNPC
+
+
+class CraftingRecipe:
+ item: str
+ ingredients: Dict[str, int]
+ source: RecipeSource
+ mod_name: Optional[str]
+
+ def __init__(self, item: str, ingredients: Dict[str, int], source: RecipeSource, mod_name: Optional[str] = None):
+ self.item = item
+ self.ingredients = ingredients
+ self.source = source
+ self.mod_name = mod_name
+
+ def __repr__(self):
+ return f"{self.item} (Source: {self.source} |" \
+ f" Ingredients: {self.ingredients})"
+
+
+all_crafting_recipes: List[CraftingRecipe] = []
+
+
+def friendship_recipe(name: str, friend: str, hearts: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe:
+ source = FriendshipSource(friend, hearts)
+ return create_recipe(name, ingredients, source, mod_name)
+
+
+def cutscene_recipe(name: str, region: str, friend: str, hearts: int, ingredients: Dict[str, int]) -> CraftingRecipe:
+ source = CutsceneSource(region, friend, hearts)
+ return create_recipe(name, ingredients, source)
+
+
+def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe:
+ source = SkillSource(skill, level)
+ return create_recipe(name, ingredients, source, mod_name)
+
+
+def mastery_recipe(name: str, skill: str, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe:
+ source = MasterySource(skill)
+ return create_recipe(name, ingredients, source, mod_name)
+
+
+def shop_recipe(name: str, region: str, price: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe:
+ source = ShopSource(region, price)
+ return create_recipe(name, ingredients, source, mod_name)
+
+
+def festival_shop_recipe(name: str, region: str, price: int, ingredients: Dict[str, int]) -> CraftingRecipe:
+ source = FestivalShopSource(region, price)
+ return create_recipe(name, ingredients, source)
+
+
+def shop_trade_recipe(name: str, region: str, currency: str, price: int, ingredients: Dict[str, int]) -> CraftingRecipe:
+ source = ShopTradeSource(region, currency, price)
+ return create_recipe(name, ingredients, source)
+
+
+def queen_of_sauce_recipe(name: str, year: int, season: str, day: int, ingredients: Dict[str, int]) -> CraftingRecipe:
+ source = QueenOfSauceSource(year, season, day)
+ return create_recipe(name, ingredients, source)
+
+
+def quest_recipe(name: str, quest: str, ingredients: Dict[str, int]) -> CraftingRecipe:
+ source = QuestSource(quest)
+ return create_recipe(name, ingredients, source)
+
+
+def special_order_recipe(name: str, special_order: str, ingredients: Dict[str, int]) -> CraftingRecipe:
+ source = SpecialOrderSource(special_order)
+ return create_recipe(name, ingredients, source)
+
+
+def starter_recipe(name: str, ingredients: Dict[str, int]) -> CraftingRecipe:
+ source = StarterSource()
+ return create_recipe(name, ingredients, source)
+
+
+def ap_recipe(name: str, ingredients: Dict[str, int], ap_item: str = None) -> CraftingRecipe:
+ if ap_item is None:
+ ap_item = f"{name} Recipe"
+ source = ArchipelagoSource(ap_item)
+ return create_recipe(name, ingredients, source)
+
+
+def cellar_recipe(name: str, ingredients: Dict[str, int]) -> CraftingRecipe:
+ source = LogicSource("Cellar")
+ return create_recipe(name, ingredients, source)
+
+
+def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, mod_name: Optional[str] = None) -> CraftingRecipe:
+ recipe = CraftingRecipe(name, ingredients, source, mod_name)
+ all_crafting_recipes.append(recipe)
+ return recipe
+
+
+cherry_bomb = skill_recipe(Bomb.cherry_bomb, Skill.mining, 1, {Ore.copper: 4, Material.coal: 1})
+bomb = skill_recipe(Bomb.bomb, Skill.mining, 6, {Ore.iron: 4, Material.coal: 1})
+mega_bomb = skill_recipe(Bomb.mega_bomb, Skill.mining, 8, {Ore.gold: 4, Loot.solar_essence: 1, Loot.void_essence: 1})
+
+gate = starter_recipe(Fence.gate, {Material.wood: 10})
+wood_fence = starter_recipe(Fence.wood, {Material.wood: 2})
+stone_fence = skill_recipe(Fence.stone, Skill.farming, 2, {Material.stone: 2})
+iron_fence = skill_recipe(Fence.iron, Skill.farming, 4, {MetalBar.iron: 2})
+hardwood_fence = skill_recipe(Fence.hardwood, Skill.farming, 6, {Material.hardwood: 2})
+
+sprinkler = skill_recipe(Sprinkler.basic, Skill.farming, 2, {MetalBar.copper: 1, MetalBar.iron: 1})
+quality_sprinkler = skill_recipe(Sprinkler.quality, Skill.farming, 6, {MetalBar.iron: 1, MetalBar.gold: 1, MetalBar.quartz: 1})
+iridium_sprinkler = skill_recipe(Sprinkler.iridium, Skill.farming, 9, {MetalBar.gold: 1, MetalBar.iridium: 1, ArtisanGood.battery_pack: 1})
+
+bee_house = skill_recipe(Machine.bee_house, Skill.farming, 3, {Material.wood: 40, Material.coal: 8, MetalBar.iron: 1, ArtisanGood.maple_syrup: 1})
+cask = cellar_recipe(Machine.cask, {Material.wood: 40, Material.hardwood: 1})
+cheese_press = skill_recipe(Machine.cheese_press, Skill.farming, 6, {Material.wood: 45, Material.stone: 45, Material.hardwood: 10, MetalBar.copper: 1})
+keg = skill_recipe(Machine.keg, Skill.farming, 8, {Material.wood: 30, MetalBar.copper: 1, MetalBar.iron: 1, ArtisanGood.oak_resin: 1})
+loom = skill_recipe(Machine.loom, Skill.farming, 7, {Material.wood: 60, Material.fiber: 30, ArtisanGood.pine_tar: 1})
+mayonnaise_machine = skill_recipe(Machine.mayonnaise_machine, Skill.farming, 2,
+ {Material.wood: 15, Material.stone: 15, Mineral.earth_crystal: 10, MetalBar.copper: 1})
+oil_maker = skill_recipe(Machine.oil_maker, Skill.farming, 8, {Loot.slime: 50, Material.hardwood: 20, MetalBar.gold: 1})
+preserves_jar = skill_recipe(Machine.preserves_jar, Skill.farming, 4, {Material.wood: 50, Material.stone: 40, Material.coal: 8})
+fish_smoker = shop_recipe(Machine.fish_smoker, Region.fish_shop, 10000,
+ {Material.hardwood: 10, WaterItem.sea_jelly: 1, WaterItem.river_jelly: 1, WaterItem.cave_jelly: 1})
+dehydrator = shop_recipe(Machine.dehydrator, Region.pierre_store, 10000, {Material.wood: 30, Material.clay: 2, Mineral.fire_quartz: 1})
+
+basic_fertilizer = skill_recipe(Fertilizer.basic, Skill.farming, 1, {Material.sap: 2})
+
+quality_fertilizer = skill_recipe(Fertilizer.quality, Skill.farming, 9, {Material.sap: 4, Fish.any: 1})
+deluxe_fertilizer = ap_recipe(Fertilizer.deluxe, {MetalBar.iridium: 1, Material.sap: 40})
+
+basic_speed_gro = skill_recipe(SpeedGro.basic, Skill.farming, 3, {ArtisanGood.pine_tar: 1, Material.moss: 5})
+deluxe_speed_gro = skill_recipe(SpeedGro.deluxe, Skill.farming, 8, {ArtisanGood.oak_resin: 1, Fossil.bone_fragment: 5})
+hyper_speed_gro = ap_recipe(SpeedGro.hyper, {Ore.radioactive: 1, Fossil.bone_fragment: 3, Loot.solar_essence: 1})
+basic_retaining_soil = skill_recipe(RetainingSoil.basic, Skill.farming, 4, {Material.stone: 2})
+quality_retaining_soil = skill_recipe(RetainingSoil.quality, Skill.farming, 7, {Material.stone: 3, Material.clay: 1})
+deluxe_retaining_soil = shop_trade_recipe(RetainingSoil.deluxe, Region.island_trader, Currency.cinder_shard, 50,
+ {Material.stone: 5, Material.fiber: 3, Material.clay: 1})
+tree_fertilizer = skill_recipe(Fertilizer.tree, Skill.foraging, 7, {Material.fiber: 5, Material.stone: 5})
+
+spring_seeds = skill_recipe(WildSeeds.spring, Skill.foraging, 1,
+ {Forageable.wild_horseradish: 1, Forageable.daffodil: 1, Forageable.leek: 1, Forageable.dandelion: 1})
+summer_seeds = skill_recipe(WildSeeds.summer, Skill.foraging, 4, {Forageable.spice_berry: 1, Fruit.grape: 1, Forageable.sweet_pea: 1})
+fall_seeds = skill_recipe(WildSeeds.fall, Skill.foraging, 6, {Mushroom.common: 1, Forageable.wild_plum: 1, Forageable.hazelnut: 1, Forageable.blackberry: 1})
+winter_seeds = skill_recipe(WildSeeds.winter, Skill.foraging, 7,
+ {Forageable.winter_root: 1, Forageable.crystal_fruit: 1, Forageable.snow_yam: 1, Forageable.crocus: 1})
+ancient_seeds = ap_recipe(WildSeeds.ancient, {Artifact.ancient_seed: 1})
+grass_starter = shop_recipe(WildSeeds.grass_starter, Region.pierre_store, 1000, {Material.fiber: 10})
+blue_grass_starter = ap_recipe(WildSeeds.blue_grass_starter, {Material.fiber: 25, Material.moss: 10, ArtisanGood.mystic_syrup: 1})
+for wild_seeds in [WildSeeds.spring, WildSeeds.summer, WildSeeds.fall, WildSeeds.winter]:
+ tea_sapling = cutscene_recipe(WildSeeds.tea_sapling, Region.sunroom, NPC.caroline, 2, {wild_seeds: 2, Material.fiber: 5, Material.wood: 5})
+fiber_seeds = special_order_recipe(WildSeeds.fiber, SpecialOrder.community_cleanup, {Seed.mixed: 1, Material.sap: 5, Material.clay: 1})
+
+wood_floor = shop_recipe(Floor.wood, Region.carpenter, 100, {Material.wood: 1})
+rustic_floor = shop_recipe(Floor.rustic, Region.carpenter, 200, {Material.wood: 1})
+straw_floor = shop_recipe(Floor.straw, Region.carpenter, 200, {Material.wood: 1, Material.fiber: 1})
+weathered_floor = shop_recipe(Floor.weathered, LogicRegion.mines_dwarf_shop, 500, {Material.wood: 1})
+crystal_floor = shop_recipe(Floor.crystal, Region.sewer, 500, {MetalBar.quartz: 1})
+stone_floor = shop_recipe(Floor.stone, Region.carpenter, 100, {Material.stone: 1})
+stone_walkway_floor = shop_recipe(Floor.stone_walkway, Region.carpenter, 200, {Material.stone: 1})
+brick_floor = shop_recipe(Floor.brick, Region.carpenter, 500, {Material.clay: 2, Material.stone: 5})
+wood_path = starter_recipe(Floor.wood_path, {Material.wood: 1})
+gravel_path = starter_recipe(Floor.gravel_path, {Material.stone: 1})
+cobblestone_path = starter_recipe(Floor.cobblestone_path, {Material.stone: 1})
+stepping_stone_path = shop_recipe(Floor.stepping_stone_path, Region.carpenter, 100, {Material.stone: 1})
+crystal_path = shop_recipe(Floor.crystal_path, Region.carpenter, 200, {MetalBar.quartz: 1})
+
+spinner = skill_recipe(Fishing.spinner, Skill.fishing, 6, {MetalBar.iron: 2})
+trap_bobber = skill_recipe(Fishing.trap_bobber, Skill.fishing, 6, {MetalBar.copper: 1, Material.sap: 10})
+sonar_bobber = skill_recipe(Fishing.sonar_bobber, Skill.fishing, 6, {MetalBar.iron: 1, MetalBar.quartz: 2})
+cork_bobber = skill_recipe(Fishing.cork_bobber, Skill.fishing, 7, {Material.wood: 10, Material.hardwood: 5, Loot.slime: 10})
+quality_bobber = special_order_recipe(Fishing.quality_bobber, SpecialOrder.juicy_bugs_wanted, {MetalBar.copper: 1, Material.sap: 20, Loot.solar_essence: 5})
+treasure_hunter = skill_recipe(Fishing.treasure_hunter, Skill.fishing, 7, {MetalBar.gold: 2})
+dressed_spinner = skill_recipe(Fishing.dressed_spinner, Skill.fishing, 8, {MetalBar.iron: 2, ArtisanGood.cloth: 1})
+barbed_hook = skill_recipe(Fishing.barbed_hook, Skill.fishing, 8, {MetalBar.copper: 1, MetalBar.iron: 1, MetalBar.gold: 1})
+magnet = skill_recipe(Fishing.magnet, Skill.fishing, 9, {MetalBar.iron: 1})
+bait = skill_recipe(Fishing.bait, Skill.fishing, 2, {Loot.bug_meat: 1})
+deluxe_bait = skill_recipe(Fishing.deluxe_bait, Skill.fishing, 4, {Fishing.bait: 5, Material.moss: 2})
+wild_bait = cutscene_recipe(Fishing.wild_bait, Region.tent, NPC.linus, 4, {Material.fiber: 10, Loot.bug_meat: 5, Loot.slime: 5})
+magic_bait = ap_recipe(Fishing.magic_bait, {Ore.radioactive: 1, Loot.bug_meat: 3})
+crab_pot = skill_recipe(Machine.crab_pot, Skill.fishing, 3, {Material.wood: 40, MetalBar.iron: 3})
+
+sturdy_ring = skill_recipe(Ring.sturdy_ring, Skill.combat, 1, {MetalBar.copper: 2, Loot.bug_meat: 25, Loot.slime: 25})
+warrior_ring = skill_recipe(Ring.warrior_ring, Skill.combat, 4, {MetalBar.iron: 10, Material.coal: 25, Mineral.frozen_tear: 10})
+ring_of_yoba = skill_recipe(Ring.ring_of_yoba, Skill.combat, 7, {MetalBar.gold: 5, MetalBar.iron: 5, Mineral.diamond: 1})
+thorns_ring = skill_recipe(Ring.thorns_ring, Skill.combat, 7, {Fossil.bone_fragment: 50, Material.stone: 50, MetalBar.gold: 1})
+glowstone_ring = skill_recipe(Ring.glowstone_ring, Skill.mining, 4, {Loot.solar_essence: 5, MetalBar.iron: 5})
+iridium_band = skill_recipe(Ring.iridium_band, Skill.combat, 9, {MetalBar.iridium: 5, Loot.solar_essence: 50, Loot.void_essence: 50})
+wedding_ring = shop_recipe(Ring.wedding_ring, LogicRegion.traveling_cart, 500, {MetalBar.iridium: 5, Mineral.prismatic_shard: 1})
+
+field_snack = skill_recipe(Edible.field_snack, Skill.foraging, 1, {TreeSeed.acorn: 1, TreeSeed.maple: 1, TreeSeed.pine: 1})
+bug_steak = skill_recipe(Edible.bug_steak, Skill.combat, 1, {Loot.bug_meat: 10})
+life_elixir = skill_recipe(Edible.life_elixir, Skill.combat, 2, {Mushroom.red: 1, Mushroom.purple: 1, Mushroom.morel: 1, Mushroom.chanterelle: 1})
+oil_of_garlic = skill_recipe(Edible.oil_of_garlic, Skill.combat, 6, {Vegetable.garlic: 10, Ingredient.oil: 1})
+
+monster_musk = special_order_recipe(Consumable.monster_musk, SpecialOrder.prismatic_jelly, {Loot.bat_wing: 30, Loot.slime: 30})
+fairy_dust = quest_recipe(Consumable.fairy_dust, Quest.the_pirates_wife, {Mineral.diamond: 1, Flower.fairy_rose: 1})
+warp_totem_beach = skill_recipe(Consumable.warp_totem_beach, Skill.foraging, 6, {Material.hardwood: 1, WaterItem.coral: 2, Material.fiber: 10})
+warp_totem_mountains = skill_recipe(Consumable.warp_totem_mountains, Skill.foraging, 7, {Material.hardwood: 1, MetalBar.iron: 1, Material.stone: 25})
+warp_totem_farm = skill_recipe(Consumable.warp_totem_farm, Skill.foraging, 8, {Material.hardwood: 1, ArtisanGood.honey: 1, Material.fiber: 20})
+warp_totem_desert = shop_trade_recipe(Consumable.warp_totem_desert, Region.desert, MetalBar.iridium, 10,
+ {Material.hardwood: 2, Forageable.coconut: 1, Ore.iridium: 4})
+warp_totem_island = shop_recipe(Consumable.warp_totem_island, Region.volcano_dwarf_shop, 10000,
+ {Material.hardwood: 5, Forageable.dragon_tooth: 1, Forageable.ginger: 1})
+rain_totem = skill_recipe(Consumable.rain_totem, Skill.foraging, 9, {Material.hardwood: 1, ArtisanGood.truffle_oil: 1, ArtisanGood.pine_tar: 5})
+
+torch = starter_recipe(Lighting.torch, {Material.wood: 1, Material.sap: 2})
+campfire = starter_recipe(Lighting.campfire, {Material.stone: 10, Material.wood: 10, Material.fiber: 10})
+wooden_brazier = shop_recipe(Lighting.wooden_brazier, Region.carpenter, 250, {Material.wood: 10, Material.coal: 1, Material.fiber: 5})
+stone_brazier = shop_recipe(Lighting.stone_brazier, Region.carpenter, 400, {Material.stone: 10, Material.coal: 1, Material.fiber: 5})
+gold_brazier = shop_recipe(Lighting.gold_brazier, Region.carpenter, 1000, {MetalBar.gold: 1, Material.coal: 1, Material.fiber: 5})
+carved_brazier = shop_recipe(Lighting.carved_brazier, Region.carpenter, 2000, {Material.hardwood: 10, Material.coal: 1})
+stump_brazier = shop_recipe(Lighting.stump_brazier, Region.carpenter, 800, {Material.hardwood: 5, Material.coal: 1})
+barrel_brazier = shop_recipe(Lighting.barrel_brazier, Region.carpenter, 800, {Material.wood: 50, Loot.solar_essence: 1, Material.coal: 1})
+skull_brazier = shop_recipe(Lighting.skull_brazier, Region.carpenter, 3000, {Fossil.bone_fragment: 10})
+marble_brazier = shop_recipe(Lighting.marble_brazier, Region.carpenter, 5000, {Mineral.marble: 1, Mineral.aquamarine: 1, Material.stone: 100})
+wood_lamp_post = shop_recipe(Lighting.wood_lamp_post, Region.carpenter, 500, {Material.wood: 50, ArtisanGood.battery_pack: 1})
+iron_lamp_post = shop_recipe(Lighting.iron_lamp_post, Region.carpenter, 1000, {MetalBar.iron: 1, ArtisanGood.battery_pack: 1})
+jack_o_lantern = festival_shop_recipe(Lighting.jack_o_lantern, LogicRegion.spirit_eve, 2000, {Vegetable.pumpkin: 1, Lighting.torch: 1})
+
+bone_mill = special_order_recipe(Machine.bone_mill, SpecialOrder.fragments_of_the_past, {Fossil.bone_fragment: 10, Material.clay: 3, Material.stone: 20})
+bait_maker = skill_recipe(Machine.bait_maker, Skill.fishing, 6, {MetalBar.iron: 3, WaterItem.coral: 3, WaterItem.sea_urchin: 1})
+
+charcoal_kiln = skill_recipe(Machine.charcoal_kiln, Skill.foraging, 2, {Material.wood: 20, MetalBar.copper: 2})
+
+crystalarium = skill_recipe(Machine.crystalarium, Skill.mining, 9, {Material.stone: 99, MetalBar.gold: 5, MetalBar.iridium: 2, ArtisanGood.battery_pack: 1})
+furnace = skill_recipe(Machine.furnace, Skill.mining, 1, {Ore.copper: 20, Material.stone: 25})
+geode_crusher = special_order_recipe(Machine.geode_crusher, SpecialOrder.cave_patrol, {MetalBar.gold: 2, Material.stone: 50, Mineral.diamond: 1})
+mushroom_log = skill_recipe(Machine.mushroom_log, Skill.foraging, 4, {Material.hardwood: 10, Material.moss: 10})
+heavy_tapper = ap_recipe(Machine.heavy_tapper, {Material.hardwood: 30, MetalBar.radioactive: 1})
+lightning_rod = skill_recipe(Machine.lightning_rod, Skill.foraging, 6, {MetalBar.iron: 1, MetalBar.quartz: 1, Loot.bat_wing: 5})
+ostrich_incubator = ap_recipe(Machine.ostrich_incubator, {Fossil.bone_fragment: 50, Material.hardwood: 50, Currency.cinder_shard: 20})
+recycling_machine = skill_recipe(Machine.recycling_machine, Skill.fishing, 4, {Material.wood: 25, Material.stone: 25, MetalBar.iron: 1})
+seed_maker = skill_recipe(Machine.seed_maker, Skill.farming, 9, {Material.wood: 25, Material.coal: 10, MetalBar.gold: 1})
+slime_egg_press = skill_recipe(Machine.slime_egg_press, Skill.combat, 6, {Material.coal: 25, Mineral.fire_quartz: 1, ArtisanGood.battery_pack: 1})
+slime_incubator = skill_recipe(Machine.slime_incubator, Skill.combat, 8, {MetalBar.iridium: 2, Loot.slime: 100})
+solar_panel = special_order_recipe(Machine.solar_panel, SpecialOrder.island_ingredients, {MetalBar.quartz: 10, MetalBar.iron: 5, MetalBar.gold: 5})
+
+tapper = skill_recipe(Machine.tapper, Skill.foraging, 4, {Material.wood: 40, MetalBar.copper: 2})
+
+worm_bin = skill_recipe(Machine.worm_bin, Skill.fishing, 4, {Material.hardwood: 25, MetalBar.gold: 1, MetalBar.iron: 1, Material.fiber: 50})
+deluxe_worm_bin = skill_recipe(Machine.deluxe_worm_bin, Skill.fishing, 8, {Machine.worm_bin: 1, Material.moss: 30})
+
+tub_o_flowers = festival_shop_recipe(Furniture.tub_o_flowers, LogicRegion.flower_dance, 2000,
+ {Material.wood: 15, Seed.tulip: 1, Seed.jazz: 1, Seed.poppy: 1, Seed.spangle: 1})
+wicked_statue = shop_recipe(Furniture.wicked_statue, Region.sewer, 1000, {Material.stone: 25, Material.coal: 5})
+flute_block = cutscene_recipe(Furniture.flute_block, Region.carpenter, NPC.robin, 6, {Material.wood: 10, Ore.copper: 2, Material.fiber: 20})
+drum_block = cutscene_recipe(Furniture.drum_block, Region.carpenter, NPC.robin, 6, {Material.stone: 10, Ore.copper: 2, Material.fiber: 20})
+
+chest = starter_recipe(Storage.chest, {Material.wood: 50})
+stone_chest = special_order_recipe(Storage.stone_chest, SpecialOrder.robins_resource_rush, {Material.stone: 50})
+big_chest = shop_recipe(Storage.big_chest, Region.carpenter, 5000, {Material.wood: 120, MetalBar.copper: 2})
+big_stone_chest = shop_recipe(Storage.big_stone_chest, LogicRegion.mines_dwarf_shop, 5000, {Material.stone: 250})
+
+wood_sign = starter_recipe(Sign.wood, {Material.wood: 25})
+stone_sign = starter_recipe(Sign.stone, {Material.stone: 25})
+dark_sign = friendship_recipe(Sign.dark, NPC.krobus, 3, {Loot.bat_wing: 5, Fossil.bone_fragment: 5})
+text_sign = starter_recipe(Sign.text, {Material.wood: 25})
+
+garden_pot = ap_recipe(Craftable.garden_pot, {Material.clay: 1, Material.stone: 10, MetalBar.quartz: 1}, "Greenhouse")
+scarecrow = skill_recipe(Craftable.scarecrow, Skill.farming, 1, {Material.wood: 50, Material.coal: 1, Material.fiber: 20})
+deluxe_scarecrow = ap_recipe(Craftable.deluxe_scarecrow, {Material.wood: 50, Material.fiber: 40, Ore.iridium: 1})
+staircase = skill_recipe(Craftable.staircase, Skill.mining, 2, {Material.stone: 99})
+explosive_ammo = skill_recipe(Craftable.explosive_ammo, Skill.combat, 8, {MetalBar.iron: 1, Material.coal: 2})
+transmute_fe = skill_recipe(Craftable.transmute_fe, Skill.mining, 4, {MetalBar.copper: 3})
+transmute_au = skill_recipe(Craftable.transmute_au, Skill.mining, 7, {MetalBar.iron: 2})
+mini_jukebox = cutscene_recipe(Craftable.mini_jukebox, Region.saloon, NPC.gus, 5, {MetalBar.iron: 2, ArtisanGood.battery_pack: 1})
+mini_obelisk = special_order_recipe(Craftable.mini_obelisk, SpecialOrder.a_curious_substance, {Material.hardwood: 30, Loot.solar_essence: 20, MetalBar.gold: 3})
+farm_computer = special_order_recipe(Craftable.farm_computer, SpecialOrder.aquatic_overpopulation,
+ {Artifact.dwarf_gadget: 1, ArtisanGood.battery_pack: 1, MetalBar.quartz: 10})
+hopper = ap_recipe(Craftable.hopper, {Material.hardwood: 10, MetalBar.iridium: 1, MetalBar.radioactive: 1})
+
+cookout_kit = skill_recipe(Craftable.cookout_kit, Skill.foraging, 3, {Material.wood: 15, Material.fiber: 10, Material.coal: 3})
+tent_kit = skill_recipe(Craftable.tent_kit, Skill.foraging, 8, {Material.hardwood: 10, Material.fiber: 25, ArtisanGood.cloth: 1})
+
+statue_of_blessings = mastery_recipe(Statue.blessings, Skill.farming, {Material.sap: 999, Material.fiber: 999, Material.stone: 999})
+statue_of_dwarf_king = mastery_recipe(Statue.dwarf_king, Skill.mining, {MetalBar.iridium: 20})
+heavy_furnace = mastery_recipe(Machine.heavy_furnace, Skill.mining, {Machine.furnace: 2, MetalBar.iron: 3, Material.stone: 50})
+mystic_tree_seed = mastery_recipe(TreeSeed.mystic, Skill.foraging, {TreeSeed.acorn: 5, TreeSeed.maple: 5, TreeSeed.pine: 5, TreeSeed.mahogany: 5})
+treasure_totem = mastery_recipe(Consumable.treasure_totem, Skill.foraging, {Material.hardwood: 5, ArtisanGood.mystic_syrup: 1, Material.moss: 10})
+challenge_bait = mastery_recipe(Fishing.challenge_bait, Skill.fishing, {Fossil.bone_fragment: 5, Material.moss: 2})
+anvil = mastery_recipe(Machine.anvil, Skill.combat, {MetalBar.iron: 50})
+mini_forge = mastery_recipe(Machine.mini_forge, Skill.combat, {Forageable.dragon_tooth: 5, MetalBar.iron: 10, MetalBar.gold: 10, MetalBar.iridium: 5})
+
+travel_charm = shop_recipe(ModCraftable.travel_core, Region.adventurer_guild, 250, {Loot.solar_essence: 1, Loot.void_essence: 1}, ModNames.magic)
+preservation_chamber = skill_recipe(ModMachine.preservation_chamber, ModSkill.archaeology, 1,
+ {MetalBar.copper: 1, Material.wood: 15, ArtisanGood.oak_resin: 30},
+ ModNames.archaeology)
+restoration_table = skill_recipe(ModMachine.restoration_table, ModSkill.archaeology, 1, {Material.wood: 15, MetalBar.copper: 1, MetalBar.iron: 1}, ModNames.archaeology)
+preservation_chamber_h = skill_recipe(ModMachine.hardwood_preservation_chamber, ModSkill.archaeology, 6, {MetalBar.copper: 1, Material.hardwood: 15,
+ ArtisanGood.oak_resin: 30}, ModNames.archaeology)
+grinder = skill_recipe(ModMachine.grinder, ModSkill.archaeology, 2, {Artifact.rusty_cog: 10, MetalBar.iron: 5, ArtisanGood.battery_pack: 1},
+ ModNames.archaeology)
+ancient_battery = skill_recipe(ModMachine.ancient_battery, ModSkill.archaeology, 7, {Material.stone: 40, MetalBar.copper: 10, MetalBar.iron: 5},
+ ModNames.archaeology)
+glass_bazier = skill_recipe(ModCraftable.glass_brazier, ModSkill.archaeology, 4, {Artifact.glass_shards: 10}, ModNames.archaeology)
+glass_path = skill_recipe(ModFloor.glass_path, ModSkill.archaeology, 3, {Artifact.glass_shards: 1}, ModNames.archaeology)
+glass_fence = skill_recipe(ModCraftable.glass_fence, ModSkill.archaeology, 7, {Artifact.glass_shards: 5}, ModNames.archaeology)
+bone_path = skill_recipe(ModFloor.bone_path, ModSkill.archaeology, 4, {Fossil.bone_fragment: 1}, ModNames.archaeology)
+rust_path = skill_recipe(ModFloor.rusty_path, ModSkill.archaeology, 2, {ModTrash.rusty_scrap: 2}, ModNames.archaeology)
+rusty_brazier = skill_recipe(ModCraftable.rusty_brazier, ModSkill.archaeology, 3, {ModTrash.rusty_scrap: 10, Material.coal: 1, Material.fiber: 1}, ModNames.archaeology)
+bone_fence = skill_recipe(ModCraftable.bone_fence, ModSkill.archaeology, 8, {Fossil.bone_fragment: 2}, ModNames.archaeology)
+water_shifter = skill_recipe(ModCraftable.water_shifter, ModSkill.archaeology, 4, {Material.wood: 40, MetalBar.copper: 4}, ModNames.archaeology)
+wooden_display = skill_recipe(ModCraftable.wooden_display, ModSkill.archaeology, 1, {Material.wood: 25}, ModNames.archaeology)
+hardwood_display = skill_recipe(ModCraftable.hardwood_display, ModSkill.archaeology, 7, {Material.hardwood: 10}, ModNames.archaeology)
+lucky_ring = skill_recipe(Ring.lucky_ring, ModSkill.archaeology, 8, {Artifact.elvish_jewelry: 1, AnimalProduct.rabbit_foot: 5, Mineral.tigerseye: 1}, ModNames.archaeology)
+volcano_totem = skill_recipe(ModConsumable.volcano_totem, ModSkill.archaeology, 9, {Material.cinder_shard: 5, Artifact.rare_disc: 1, Artifact.dwarf_gadget: 1},
+ ModNames.archaeology)
+haste_elixir = shop_recipe(ModEdible.haste_elixir, SVERegion.alesia_shop, 35000, {Loot.void_essence: 35, ModLoot.void_soul: 5, Ingredient.sugar: 1,
+ Meal.spicy_eel: 1}, ModNames.sve)
+hero_elixir = shop_recipe(ModEdible.hero_elixir, SVERegion.isaac_shop, 65000, {ModLoot.void_pebble: 3, ModLoot.void_soul: 5, Ingredient.oil: 1,
+ Loot.slime: 10}, ModNames.sve)
+armor_elixir = shop_recipe(ModEdible.armor_elixir, SVERegion.alesia_shop, 50000, {Loot.solar_essence: 30, ModLoot.void_soul: 5, Ingredient.vinegar: 5,
+ Fossil.bone_fragment: 5}, ModNames.sve)
+ginger_tincture = friendship_recipe(ModConsumable.ginger_tincture, ModNPC.goblin, 4, {DistantLandsForageable.brown_amanita: 1, Forageable.ginger: 5,
+ Material.cinder_shard: 1, DistantLandsForageable.swamp_herb: 1},
+ ModNames.distant_lands)
+
+neanderthal_skeleton = shop_recipe(ModCraftable.neanderthal_skeleton, LogicRegion.mines_dwarf_shop, 5000,
+ {ModFossil.neanderthal_skull: 1, ModFossil.neanderthal_ribs: 1, ModFossil.neanderthal_pelvis: 1,
+ ModFossil.neanderthal_limb_bones: 1,
+ MetalBar.iron: 5, Material.hardwood: 10}, ModNames.boarding_house)
+pterodactyl_skeleton_l = shop_recipe(ModCraftable.pterodactyl_skeleton_l, LogicRegion.mines_dwarf_shop, 5000,
+ {ModFossil.pterodactyl_phalange: 1, ModFossil.pterodactyl_skull: 1, ModFossil.pterodactyl_l_wing_bone: 1,
+ MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house)
+pterodactyl_skeleton_m = shop_recipe(ModCraftable.pterodactyl_skeleton_m, LogicRegion.mines_dwarf_shop, 5000,
+ {ModFossil.pterodactyl_phalange: 1, ModFossil.pterodactyl_vertebra: 1, ModFossil.pterodactyl_ribs: 1,
+ MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house)
+pterodactyl_skeleton_r = shop_recipe(ModCraftable.pterodactyl_skeleton_r, LogicRegion.mines_dwarf_shop, 5000,
+ {ModFossil.pterodactyl_phalange: 1, ModFossil.pterodactyl_claw: 1, ModFossil.pterodactyl_r_wing_bone: 1,
+ MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house)
+trex_skeleton_l = shop_recipe(ModCraftable.trex_skeleton_l, LogicRegion.mines_dwarf_shop, 5000,
+ {ModFossil.dinosaur_vertebra: 1, ModFossil.dinosaur_tooth: 1, ModFossil.dinosaur_skull: 1,
+ MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house)
+trex_skeleton_m = shop_recipe(ModCraftable.trex_skeleton_m, LogicRegion.mines_dwarf_shop, 5000,
+ {ModFossil.dinosaur_vertebra: 1, ModFossil.dinosaur_ribs: 1, ModFossil.dinosaur_claw: 1,
+ MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house)
+trex_skeleton_r = shop_recipe(ModCraftable.trex_skeleton_r, LogicRegion.mines_dwarf_shop, 5000,
+ {ModFossil.dinosaur_vertebra: 1, ModFossil.dinosaur_femur: 1, ModFossil.dinosaur_pelvis: 1,
+ MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house)
+
+bouquet = skill_recipe(Gift.bouquet, ModSkill.socializing, 3, {Flower.tulip: 3}, ModNames.socializing_skill)
+trash_bin = skill_recipe(ModMachine.trash_bin, ModSkill.binning, 2, {Material.stone: 30, MetalBar.iron: 2}, ModNames.binning_skill)
+composter = skill_recipe(ModMachine.composter, ModSkill.binning, 4, {Material.wood: 70, Material.sap: 20, Material.fiber: 30}, ModNames.binning_skill)
+recycling_bin = skill_recipe(ModMachine.recycling_bin, ModSkill.binning, 7, {MetalBar.iron: 3, Material.fiber: 10, MetalBar.gold: 2}, ModNames.binning_skill)
+advanced_recycling_machine = skill_recipe(ModMachine.advanced_recycling_machine, ModSkill.binning, 9,
+ {MetalBar.iridium: 5, ArtisanGood.battery_pack: 2, MetalBar.quartz: 10}, ModNames.binning_skill)
+
+all_crafting_recipes_by_name = {recipe.item: recipe for recipe in all_crafting_recipes}
diff --git a/worlds/stardew_valley/data/crops.csv b/worlds/stardew_valley/data/crops.csv
deleted file mode 100644
index e3d2dc8256db..000000000000
--- a/worlds/stardew_valley/data/crops.csv
+++ /dev/null
@@ -1,40 +0,0 @@
-crop,farm_growth_seasons,seed,seed_seasons,seed_regions
-Amaranth,Fall,Amaranth Seeds,Fall,"Pierre's General Store,JojaMart"
-Artichoke,Fall,Artichoke Seeds,Fall,"Pierre's General Store,JojaMart"
-Beet,Fall,Beet Seeds,Fall,Oasis
-Blue Jazz,Spring,Jazz Seeds,Spring,"Pierre's General Store,JojaMart"
-Blueberry,Summer,Blueberry Seeds,Summer,"Pierre's General Store,JojaMart"
-Bok Choy,Fall,Bok Choy Seeds,Fall,"Pierre's General Store,JojaMart"
-Cactus Fruit,,Cactus Seeds,,Oasis
-Cauliflower,Spring,Cauliflower Seeds,Spring,"Pierre's General Store,JojaMart"
-Coffee Bean,"Spring,Summer",Coffee Bean,"Summer,Fall","Traveling Cart"
-Corn,"Summer,Fall",Corn Seeds,"Summer,Fall","Pierre's General Store,JojaMart"
-Cranberries,Fall,Cranberry Seeds,Fall,"Pierre's General Store,JojaMart"
-Eggplant,Fall,Eggplant Seeds,Fall,"Pierre's General Store,JojaMart"
-Fairy Rose,Fall,Fairy Seeds,Fall,"Pierre's General Store,JojaMart"
-Garlic,Spring,Garlic Seeds,Spring,"Pierre's General Store,JojaMart"
-Grape,Fall,Grape Starter,Fall,"Pierre's General Store,JojaMart"
-Green Bean,Spring,Bean Starter,Spring,"Pierre's General Store,JojaMart"
-Hops,Summer,Hops Starter,Summer,"Pierre's General Store,JojaMart"
-Hot Pepper,Summer,Pepper Seeds,Summer,"Pierre's General Store,JojaMart"
-Kale,Spring,Kale Seeds,Spring,"Pierre's General Store,JojaMart"
-Melon,Summer,Melon Seeds,Summer,"Pierre's General Store,JojaMart"
-Parsnip,Spring,Parsnip Seeds,Spring,"Pierre's General Store,JojaMart"
-Pineapple,Summer,Pineapple Seeds,Summer,"Island Trader"
-Poppy,Summer,Poppy Seeds,Summer,"Pierre's General Store,JojaMart"
-Potato,Spring,Potato Seeds,Spring,"Pierre's General Store,JojaMart"
-Pumpkin,Fall,Pumpkin Seeds,Fall,"Pierre's General Store,JojaMart"
-Radish,Summer,Radish Seeds,Summer,"Pierre's General Store,JojaMart"
-Red Cabbage,Summer,Red Cabbage Seeds,Summer,"Pierre's General Store,JojaMart"
-Rhubarb,Spring,Rhubarb Seeds,Spring,Oasis
-Starfruit,Summer,Starfruit Seeds,Summer,Oasis
-Strawberry,Spring,Strawberry Seeds,Spring,"Pierre's General Store,JojaMart"
-Summer Spangle,Summer,Spangle Seeds,Summer,"Pierre's General Store,JojaMart"
-Sunflower,"Summer,Fall",Sunflower Seeds,"Summer,Fall","Pierre's General Store,JojaMart"
-Sweet Gem Berry,Fall,Rare Seed,"Spring,Summer",Traveling Cart
-Taro Root,Summer,Taro Tuber,Summer,"Island Trader"
-Tomato,Summer,Tomato Seeds,Summer,"Pierre's General Store,JojaMart"
-Tulip,Spring,Tulip Bulb,Spring,"Pierre's General Store,JojaMart"
-Unmilled Rice,Spring,Rice Shoot,Spring,"Pierre's General Store,JojaMart"
-Wheat,"Summer,Fall",Wheat Seeds,"Summer,Fall","Pierre's General Store,JojaMart"
-Yam,Fall,Yam Seeds,Fall,"Pierre's General Store,JojaMart"
diff --git a/worlds/stardew_valley/data/crops_data.py b/worlds/stardew_valley/data/crops_data.py
deleted file mode 100644
index e798235060f2..000000000000
--- a/worlds/stardew_valley/data/crops_data.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from dataclasses import dataclass
-from typing import List
-
-from .. import data
-
-
-@dataclass(frozen=True)
-class SeedItem:
- name: str
- seasons: List[str]
- regions: List[str]
-
-
-@dataclass(frozen=True)
-class CropItem:
- name: str
- farm_growth_seasons: List[str]
- seed: SeedItem
-
-
-def load_crop_csv():
- import csv
- try:
- from importlib.resources import files
- except ImportError:
- from importlib_resources import files # noqa
-
- with files(data).joinpath("crops.csv").open() as file:
- reader = csv.DictReader(file)
- crops = []
- seeds = []
-
- for item in reader:
- seeds.append(SeedItem(item["seed"],
- [season for season in item["seed_seasons"].split(",")]
- if item["seed_seasons"] else [],
- [region for region in item["seed_regions"].split(",")]
- if item["seed_regions"] else []))
- crops.append(CropItem(item["crop"],
- [season for season in item["farm_growth_seasons"].split(",")]
- if item["farm_growth_seasons"] else [],
- seeds[-1]))
- return crops, seeds
-
-
-# TODO Those two should probably be split to we can include rest of seeds
-all_crops, all_purchasable_seeds = load_crop_csv()
-crops_by_name = {crop.name: crop for crop in all_crops}
diff --git a/worlds/stardew_valley/data/fish_data.py b/worlds/stardew_valley/data/fish_data.py
index 91a4431c6552..26b1a0d58a81 100644
--- a/worlds/stardew_valley/data/fish_data.py
+++ b/worlds/stardew_valley/data/fish_data.py
@@ -1,20 +1,24 @@
from dataclasses import dataclass
-from typing import List, Tuple, Union, Optional
+from typing import Tuple, Union, Optional
from . import season_data as season
-from .game_item import GameItem
-from ..strings.region_names import Region
+from ..mods.mod_data import ModNames
+from ..strings.fish_names import Fish, SVEFish, DistantLandsFish
+from ..strings.region_names import Region, SVERegion, LogicRegion
@dataclass(frozen=True)
-class FishItem(GameItem):
+class FishItem:
+ name: str
locations: Tuple[str]
seasons: Tuple[str]
difficulty: int
- mod_name: Optional[str]
+ legendary: bool
+ extended_family: bool
+ mod_name: Optional[str] = None
def __repr__(self):
- return f"{self.name} [{self.item_id}] (Locations: {self.locations} |" \
+ return f"{self.name} (Locations: {self.locations} |" \
f" Seasons: {self.seasons} |" \
f" Difficulty: {self.difficulty}) |" \
f"Mod: {self.mod_name}"
@@ -26,6 +30,7 @@ def __repr__(self):
mountain_lake = (Region.mountain,)
forest_pond = (Region.forest,)
forest_river = (Region.forest,)
+forest_waterfall = (LogicRegion.forest_waterfall,)
secret_woods = (Region.secret_woods,)
mines_floor_20 = (Region.mines_floor_20,)
mines_floor_60 = (Region.mines_floor_60,)
@@ -39,92 +44,132 @@ def __repr__(self):
ginger_island_river = (Region.island_west,)
pirate_cove = (Region.pirate_cove,)
-all_fish: List[FishItem] = []
+crimson_badlands = (SVERegion.crimson_badlands,)
+shearwater = (SVERegion.shearwater,)
+highlands_pond = (SVERegion.highlands_pond,)
+highlands_cave = (SVERegion.highlands_cavern,)
+sprite_spring = (SVERegion.sprite_spring,)
+fable_reef = (SVERegion.fable_reef,)
+vineyard = (SVERegion.blue_moon_vineyard,)
-def create_fish(name: str, item_id: int, locations: Tuple[str, ...], seasons: Union[str, Tuple[str, ...]],
- difficulty: int, mod_name: Optional[str] = None) -> FishItem:
+def create_fish(name: str, locations: Tuple[str, ...], seasons: Union[str, Tuple[str, ...]],
+ difficulty: int, legendary: bool = False, extended_family: bool = False, mod_name: Optional[str] = None) -> FishItem:
if isinstance(seasons, str):
seasons = (seasons,)
- fish_item = FishItem(name, item_id, locations, seasons, difficulty, mod_name)
- all_fish.append(fish_item)
+ fish_item = FishItem(name, locations, seasons, difficulty, legendary, extended_family, mod_name)
return fish_item
-albacore = create_fish("Albacore", 705, ocean, (season.fall, season.winter), 60)
-anchovy = create_fish("Anchovy", 129, ocean, (season.spring, season.fall), 30)
-blue_discus = create_fish("Blue Discus", 838, ginger_island_river, season.all_seasons, 60)
-bream = create_fish("Bream", 132, town_river + forest_river, season.all_seasons, 35)
-bullhead = create_fish("Bullhead", 700, mountain_lake, season.all_seasons, 46)
-carp = create_fish("Carp", 142, mountain_lake + secret_woods + sewers + mutant_bug_lair, season.not_winter, 15)
-catfish = create_fish("Catfish", 143, town_river + forest_river + secret_woods, (season.spring, season.fall), 75)
-chub = create_fish("Chub", 702, forest_river + mountain_lake, season.all_seasons, 35)
-dorado = create_fish("Dorado", 704, forest_river, season.summer, 78)
-eel = create_fish("Eel", 148, ocean, (season.spring, season.fall), 70)
-flounder = create_fish("Flounder", 267, ocean, (season.spring, season.summer), 50)
-ghostfish = create_fish("Ghostfish", 156, mines_floor_20 + mines_floor_60, season.all_seasons, 50)
-halibut = create_fish("Halibut", 708, ocean, season.not_fall, 50)
-herring = create_fish("Herring", 147, ocean, (season.spring, season.winter), 25)
-ice_pip = create_fish("Ice Pip", 161, mines_floor_60, season.all_seasons, 85)
-largemouth_bass = create_fish("Largemouth Bass", 136, mountain_lake, season.all_seasons, 50)
-lava_eel = create_fish("Lava Eel", 162, mines_floor_100, season.all_seasons, 90)
-lingcod = create_fish("Lingcod", 707, town_river + forest_river + mountain_lake, season.winter, 85)
-lionfish = create_fish("Lionfish", 837, ginger_island_ocean, season.all_seasons, 50)
-midnight_carp = create_fish("Midnight Carp", 269, mountain_lake + forest_pond + ginger_island_river,
+albacore = create_fish(Fish.albacore, ocean, (season.fall, season.winter), 60)
+anchovy = create_fish(Fish.anchovy, ocean, (season.spring, season.fall), 30)
+blue_discus = create_fish(Fish.blue_discus, ginger_island_river, season.all_seasons, 60)
+bream = create_fish(Fish.bream, town_river + forest_river, season.all_seasons, 35)
+bullhead = create_fish(Fish.bullhead, mountain_lake, season.all_seasons, 46)
+carp = create_fish(Fish.carp, mountain_lake + secret_woods + sewers + mutant_bug_lair, season.not_winter, 15)
+catfish = create_fish(Fish.catfish, town_river + forest_river + secret_woods, (season.spring, season.fall), 75)
+chub = create_fish(Fish.chub, forest_river + mountain_lake, season.all_seasons, 35)
+dorado = create_fish(Fish.dorado, forest_river, season.summer, 78)
+eel = create_fish(Fish.eel, ocean, (season.spring, season.fall), 70)
+flounder = create_fish(Fish.flounder, ocean, (season.spring, season.summer), 50)
+ghostfish = create_fish(Fish.ghostfish, mines_floor_20 + mines_floor_60, season.all_seasons, 50)
+goby = create_fish(Fish.goby, forest_waterfall, season.all_seasons, 55)
+halibut = create_fish(Fish.halibut, ocean, season.not_fall, 50)
+herring = create_fish(Fish.herring, ocean, (season.spring, season.winter), 25)
+ice_pip = create_fish(Fish.ice_pip, mines_floor_60, season.all_seasons, 85)
+largemouth_bass = create_fish(Fish.largemouth_bass, mountain_lake, season.all_seasons, 50)
+lava_eel = create_fish(Fish.lava_eel, mines_floor_100, season.all_seasons, 90)
+lingcod = create_fish(Fish.lingcod, town_river + forest_river + mountain_lake, season.winter, 85)
+lionfish = create_fish(Fish.lionfish, ginger_island_ocean, season.all_seasons, 50)
+midnight_carp = create_fish(Fish.midnight_carp, mountain_lake + forest_pond + ginger_island_river,
(season.fall, season.winter), 55)
-octopus = create_fish("Octopus", 149, ocean, season.summer, 95)
-perch = create_fish("Perch", 141, town_river + forest_river + forest_pond + mountain_lake, season.winter, 35)
-pike = create_fish("Pike", 144, town_river + forest_river + forest_pond, (season.summer, season.winter), 60)
-pufferfish = create_fish("Pufferfish", 128, ocean + ginger_island_ocean, season.summer, 80)
-rainbow_trout = create_fish("Rainbow Trout", 138, town_river + forest_river + mountain_lake, season.summer, 45)
-red_mullet = create_fish("Red Mullet", 146, ocean, (season.summer, season.winter), 55)
-red_snapper = create_fish("Red Snapper", 150, ocean, (season.summer, season.fall), 40)
-salmon = create_fish("Salmon", 139, town_river + forest_river, season.fall, 50)
-sandfish = create_fish("Sandfish", 164, desert, season.all_seasons, 65)
-sardine = create_fish("Sardine", 131, ocean, (season.spring, season.fall, season.winter), 30)
-scorpion_carp = create_fish("Scorpion Carp", 165, desert, season.all_seasons, 90)
-sea_cucumber = create_fish("Sea Cucumber", 154, ocean, (season.fall, season.winter), 40)
-shad = create_fish("Shad", 706, town_river + forest_river, season.not_winter, 45)
-slimejack = create_fish("Slimejack", 796, mutant_bug_lair, season.all_seasons, 55)
-smallmouth_bass = create_fish("Smallmouth Bass", 137, town_river + forest_river, (season.spring, season.fall), 28)
-squid = create_fish("Squid", 151, ocean, season.winter, 75)
-stingray = create_fish("Stingray", 836, pirate_cove, season.all_seasons, 80)
-stonefish = create_fish("Stonefish", 158, mines_floor_20, season.all_seasons, 65)
-sturgeon = create_fish("Sturgeon", 698, mountain_lake, (season.summer, season.winter), 78)
-sunfish = create_fish("Sunfish", 145, town_river + forest_river, (season.spring, season.summer), 30)
-super_cucumber = create_fish("Super Cucumber", 155, ocean + ginger_island_ocean, (season.summer, season.fall), 80)
-tiger_trout = create_fish("Tiger Trout", 699, town_river + forest_river, (season.fall, season.winter), 60)
-tilapia = create_fish("Tilapia", 701, ocean + ginger_island_ocean, (season.summer, season.fall), 50)
+octopus = create_fish(Fish.octopus, ocean, season.summer, 95)
+perch = create_fish(Fish.perch, town_river + forest_river + forest_pond + mountain_lake, season.winter, 35)
+pike = create_fish(Fish.pike, town_river + forest_river + forest_pond, (season.summer, season.winter), 60)
+pufferfish = create_fish(Fish.pufferfish, ocean + ginger_island_ocean, season.summer, 80)
+rainbow_trout = create_fish(Fish.rainbow_trout, town_river + forest_river + mountain_lake, season.summer, 45)
+red_mullet = create_fish(Fish.red_mullet, ocean, (season.summer, season.winter), 55)
+red_snapper = create_fish(Fish.red_snapper, ocean, (season.summer, season.fall), 40)
+salmon = create_fish(Fish.salmon, town_river + forest_river, season.fall, 50)
+sandfish = create_fish(Fish.sandfish, desert, season.all_seasons, 65)
+sardine = create_fish(Fish.sardine, ocean, (season.spring, season.fall, season.winter), 30)
+scorpion_carp = create_fish(Fish.scorpion_carp, desert, season.all_seasons, 90)
+sea_cucumber = create_fish(Fish.sea_cucumber, ocean, (season.fall, season.winter), 40)
+shad = create_fish(Fish.shad, town_river + forest_river, season.not_winter, 45)
+slimejack = create_fish(Fish.slimejack, mutant_bug_lair, season.all_seasons, 55)
+smallmouth_bass = create_fish(Fish.smallmouth_bass, town_river + forest_river, (season.spring, season.fall), 28)
+squid = create_fish(Fish.squid, ocean, season.winter, 75)
+stingray = create_fish(Fish.stingray, pirate_cove, season.all_seasons, 80)
+stonefish = create_fish(Fish.stonefish, mines_floor_20, season.all_seasons, 65)
+sturgeon = create_fish(Fish.sturgeon, mountain_lake, (season.summer, season.winter), 78)
+sunfish = create_fish(Fish.sunfish, town_river + forest_river, (season.spring, season.summer), 30)
+super_cucumber = create_fish(Fish.super_cucumber, ocean + ginger_island_ocean, (season.summer, season.fall), 80)
+tiger_trout = create_fish(Fish.tiger_trout, town_river + forest_river, (season.fall, season.winter), 60)
+tilapia = create_fish(Fish.tilapia, ocean + ginger_island_ocean, (season.summer, season.fall), 50)
# Tuna has different seasons on ginger island. Should be changed when the whole fish thing is refactored
-tuna = create_fish("Tuna", 130, ocean + ginger_island_ocean, (season.summer, season.winter), 70)
-void_salmon = create_fish("Void Salmon", 795, witch_swamp, season.all_seasons, 80)
-walleye = create_fish("Walleye", 140, town_river + forest_river + forest_pond + mountain_lake, season.fall, 45)
-woodskip = create_fish("Woodskip", 734, secret_woods, season.all_seasons, 50)
-
-blob_fish = create_fish("Blobfish", 800, night_market, season.winter, 75)
-midnight_squid = create_fish("Midnight Squid", 798, night_market, season.winter, 55)
-spook_fish = create_fish("Spook Fish", 799, night_market, season.winter, 60)
-
-angler = create_fish("Angler", 160, town_river, season.fall, 85)
-crimsonfish = create_fish("Crimsonfish", 159, ocean, season.summer, 95)
-glacierfish = create_fish("Glacierfish", 775, forest_river, season.winter, 100)
-legend = create_fish("Legend", 163, mountain_lake, season.spring, 110)
-mutant_carp = create_fish("Mutant Carp", 682, sewers, season.all_seasons, 80)
-
-clam = create_fish("Clam", 372, ocean, season.all_seasons, -1)
-cockle = create_fish("Cockle", 718, ocean, season.all_seasons, -1)
-crab = create_fish("Crab", 717, ocean, season.all_seasons, -1)
-crayfish = create_fish("Crayfish", 716, fresh_water, season.all_seasons, -1)
-lobster = create_fish("Lobster", 715, ocean, season.all_seasons, -1)
-mussel = create_fish("Mussel", 719, ocean, season.all_seasons, -1)
-oyster = create_fish("Oyster", 723, ocean, season.all_seasons, -1)
-periwinkle = create_fish("Periwinkle", 722, fresh_water, season.all_seasons, -1)
-shrimp = create_fish("Shrimp", 720, ocean, season.all_seasons, -1)
-snail = create_fish("Snail", 721, fresh_water, season.all_seasons, -1)
-
-legendary_fish = [crimsonfish, angler, legend, glacierfish, mutant_carp]
-special_fish = [*legendary_fish, blob_fish, lava_eel, octopus, scorpion_carp, ice_pip, super_cucumber, dorado]
-island_fish = [lionfish, blue_discus, stingray]
-
-all_fish_by_name = {fish.name: fish for fish in all_fish}
+tuna = create_fish(Fish.tuna, ocean + ginger_island_ocean, (season.summer, season.winter), 70)
+void_salmon = create_fish(Fish.void_salmon, witch_swamp, season.all_seasons, 80)
+walleye = create_fish(Fish.walleye, town_river + forest_river + forest_pond + mountain_lake, season.fall, 45)
+woodskip = create_fish(Fish.woodskip, secret_woods, season.all_seasons, 50)
+
+blobfish = create_fish(Fish.blobfish, night_market, season.winter, 75)
+midnight_squid = create_fish(Fish.midnight_squid, night_market, season.winter, 55)
+spook_fish = create_fish(Fish.spook_fish, night_market, season.winter, 60)
+
+angler = create_fish(Fish.angler, town_river, season.fall, 85, True, False)
+crimsonfish = create_fish(Fish.crimsonfish, ocean, season.summer, 95, True, False)
+glacierfish = create_fish(Fish.glacierfish, forest_river, season.winter, 100, True, False)
+legend = create_fish(Fish.legend, mountain_lake, season.spring, 110, True, False)
+mutant_carp = create_fish(Fish.mutant_carp, sewers, season.all_seasons, 80, True, False)
+
+ms_angler = create_fish(Fish.ms_angler, town_river, season.fall, 85, True, True)
+son_of_crimsonfish = create_fish(Fish.son_of_crimsonfish, ocean, season.summer, 95, True, True)
+glacierfish_jr = create_fish(Fish.glacierfish_jr, forest_river, season.winter, 100, True, True)
+legend_ii = create_fish(Fish.legend_ii, mountain_lake, season.spring, 110, True, True)
+radioactive_carp = create_fish(Fish.radioactive_carp, sewers, season.all_seasons, 80, True, True)
+
+baby_lunaloo = create_fish(SVEFish.baby_lunaloo, ginger_island_ocean, season.all_seasons, 15, mod_name=ModNames.sve)
+bonefish = create_fish(SVEFish.bonefish, crimson_badlands, season.all_seasons, 70, mod_name=ModNames.sve)
+bull_trout = create_fish(SVEFish.bull_trout, forest_river, season.not_spring, 45, mod_name=ModNames.sve)
+butterfish = create_fish(SVEFish.butterfish, shearwater, season.not_winter, 75, mod_name=ModNames.sve)
+clownfish = create_fish(SVEFish.clownfish, ginger_island_ocean, season.all_seasons, 45, mod_name=ModNames.sve)
+daggerfish = create_fish(SVEFish.daggerfish, highlands_pond, season.all_seasons, 50, mod_name=ModNames.sve)
+frog = create_fish(SVEFish.frog, mountain_lake, (season.spring, season.summer), 70, mod_name=ModNames.sve)
+gemfish = create_fish(SVEFish.gemfish, highlands_cave, season.all_seasons, 100, mod_name=ModNames.sve)
+goldenfish = create_fish(SVEFish.goldenfish, sprite_spring, season.all_seasons, 60, mod_name=ModNames.sve)
+grass_carp = create_fish(SVEFish.grass_carp, secret_woods, (season.spring, season.summer), 85, mod_name=ModNames.sve)
+king_salmon = create_fish(SVEFish.king_salmon, forest_river, (season.spring, season.summer), 80, mod_name=ModNames.sve)
+kittyfish = create_fish(SVEFish.kittyfish, shearwater, (season.fall, season.winter), 85, mod_name=ModNames.sve)
+lunaloo = create_fish(SVEFish.lunaloo, ginger_island_ocean, season.all_seasons, 70, mod_name=ModNames.sve)
+meteor_carp = create_fish(SVEFish.meteor_carp, sprite_spring, season.all_seasons, 80, mod_name=ModNames.sve)
+minnow = create_fish(SVEFish.minnow, town_river, season.all_seasons, 1, mod_name=ModNames.sve)
+puppyfish = create_fish(SVEFish.puppyfish, shearwater, season.not_winter, 85, mod_name=ModNames.sve)
+radioactive_bass = create_fish(SVEFish.radioactive_bass, sewers, season.all_seasons, 90, mod_name=ModNames.sve)
+seahorse = create_fish(SVEFish.seahorse, ginger_island_ocean, season.all_seasons, 25, mod_name=ModNames.sve)
+shiny_lunaloo = create_fish(SVEFish.shiny_lunaloo, ginger_island_ocean, season.all_seasons, 110, mod_name=ModNames.sve)
+snatcher_worm = create_fish(SVEFish.snatcher_worm, mutant_bug_lair, season.all_seasons, 75, mod_name=ModNames.sve)
+starfish = create_fish(SVEFish.starfish, ginger_island_ocean, season.all_seasons, 75, mod_name=ModNames.sve)
+torpedo_trout = create_fish(SVEFish.torpedo_trout, fable_reef, season.all_seasons, 70, mod_name=ModNames.sve)
+undeadfish = create_fish(SVEFish.undeadfish, crimson_badlands, season.all_seasons, 80, mod_name=ModNames.sve)
+void_eel = create_fish(SVEFish.void_eel, witch_swamp, season.all_seasons, 100, mod_name=ModNames.sve)
+water_grub = create_fish(SVEFish.water_grub, mutant_bug_lair, season.all_seasons, 60, mod_name=ModNames.sve)
+sea_sponge = create_fish(SVEFish.sea_sponge, ginger_island_ocean, season.all_seasons, 40, mod_name=ModNames.sve)
+
+void_minnow = create_fish(DistantLandsFish.void_minnow, witch_swamp, season.all_seasons, 15, mod_name=ModNames.distant_lands)
+purple_algae = create_fish(DistantLandsFish.purple_algae, witch_swamp, season.all_seasons, 15, mod_name=ModNames.distant_lands)
+swamp_leech = create_fish(DistantLandsFish.swamp_leech, witch_swamp, season.all_seasons, 15, mod_name=ModNames.distant_lands)
+giant_horsehoe_crab = create_fish(DistantLandsFish.giant_horsehoe_crab, witch_swamp, season.all_seasons, 90, mod_name=ModNames.distant_lands)
+
+clam = create_fish(Fish.clam, ocean, season.all_seasons, -1)
+cockle = create_fish(Fish.cockle, ocean, season.all_seasons, -1)
+crab = create_fish(Fish.crab, ocean, season.all_seasons, -1)
+crayfish = create_fish(Fish.crayfish, fresh_water, season.all_seasons, -1)
+lobster = create_fish(Fish.lobster, ocean, season.all_seasons, -1)
+mussel = create_fish(Fish.mussel, ocean, season.all_seasons, -1)
+oyster = create_fish(Fish.oyster, ocean, season.all_seasons, -1)
+periwinkle = create_fish(Fish.periwinkle, fresh_water, season.all_seasons, -1)
+shrimp = create_fish(Fish.shrimp, ocean, season.all_seasons, -1)
+snail = create_fish(Fish.snail, fresh_water, season.all_seasons, -1)
+
+vanilla_legendary_fish = [angler, crimsonfish, glacierfish, legend, mutant_carp]
diff --git a/worlds/stardew_valley/data/game_item.py b/worlds/stardew_valley/data/game_item.py
index cac86d527d86..2107ca30d33a 100644
--- a/worlds/stardew_valley/data/game_item.py
+++ b/worlds/stardew_valley/data/game_item.py
@@ -1,13 +1,86 @@
-from dataclasses import dataclass
+import enum
+import sys
+from abc import ABC
+from dataclasses import dataclass, field
+from types import MappingProxyType
+from typing import List, Iterable, Set, ClassVar, Tuple, Mapping, Callable, Any
+
+from ..stardew_rule.protocol import StardewRule
+
+if sys.version_info >= (3, 10):
+ kw_only = {"kw_only": True}
+else:
+ kw_only = {}
+
+DEFAULT_REQUIREMENT_TAGS = MappingProxyType({})
+
+
+@dataclass(frozen=True)
+class Requirement(ABC):
+ ...
+
+
+class ItemTag(enum.Enum):
+ CROPSANITY_SEED = enum.auto()
+ CROPSANITY = enum.auto()
+ FISH = enum.auto()
+ FRUIT = enum.auto()
+ VEGETABLE = enum.auto()
+ EDIBLE_MUSHROOM = enum.auto()
+ BOOK = enum.auto()
+ BOOK_POWER = enum.auto()
+ BOOK_SKILL = enum.auto()
+
+
+@dataclass(frozen=True)
+class ItemSource(ABC):
+ add_tags: ClassVar[Tuple[ItemTag]] = ()
+
+ @property
+ def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]:
+ return DEFAULT_REQUIREMENT_TAGS
+
+ # FIXME this should just be an optional field, but kw_only requires python 3.10...
+ @property
+ def other_requirements(self) -> Iterable[Requirement]:
+ return ()
+
+
+@dataclass(frozen=True, **kw_only)
+class GenericSource(ItemSource):
+ regions: Tuple[str, ...] = ()
+ """No region means it's available everywhere."""
+ other_requirements: Tuple[Requirement, ...] = ()
+
+
+@dataclass(frozen=True)
+class CustomRuleSource(ItemSource):
+ """Hopefully once everything is migrated to sources, we won't need these custom logic anymore."""
+ create_rule: Callable[[Any], StardewRule]
+
+
+class Tag(ItemSource):
+ """Not a real source, just a way to add tags to an item. Will be removed from the item sources during unpacking."""
+ tag: Tuple[ItemTag, ...]
+
+ def __init__(self, *tag: ItemTag):
+ self.tag = tag # noqa
+
+ @property
+ def add_tags(self):
+ return self.tag
@dataclass(frozen=True)
class GameItem:
name: str
- item_id: int
+ sources: List[ItemSource] = field(default_factory=list)
+ tags: Set[ItemTag] = field(default_factory=set)
- def __repr__(self):
- return f"{self.name} [{self.item_id}]"
+ def add_sources(self, sources: Iterable[ItemSource]):
+ self.sources.extend(source for source in sources if type(source) is not Tag)
+ for source in sources:
+ self.add_tags(source.add_tags)
- def __lt__(self, other):
- return self.name < other.name
+ def add_tags(self, tags: Iterable[ItemTag]):
+ self.tags.update(tags)
diff --git a/worlds/stardew_valley/data/harvest.py b/worlds/stardew_valley/data/harvest.py
new file mode 100644
index 000000000000..087d7c3fa86b
--- /dev/null
+++ b/worlds/stardew_valley/data/harvest.py
@@ -0,0 +1,66 @@
+from dataclasses import dataclass
+from typing import Tuple, Sequence, Mapping
+
+from .game_item import ItemSource, kw_only, ItemTag, Requirement
+from ..strings.season_names import Season
+
+
+@dataclass(frozen=True, **kw_only)
+class ForagingSource(ItemSource):
+ regions: Tuple[str, ...]
+ seasons: Tuple[str, ...] = Season.all
+ other_requirements: Tuple[Requirement, ...] = ()
+
+
+@dataclass(frozen=True, **kw_only)
+class SeasonalForagingSource(ItemSource):
+ season: str
+ days: Sequence[int]
+ regions: Tuple[str, ...]
+
+ def as_foraging_source(self) -> ForagingSource:
+ return ForagingSource(seasons=(self.season,), regions=self.regions)
+
+
+@dataclass(frozen=True, **kw_only)
+class FruitBatsSource(ItemSource):
+ ...
+
+
+@dataclass(frozen=True, **kw_only)
+class MushroomCaveSource(ItemSource):
+ ...
+
+
+@dataclass(frozen=True, **kw_only)
+class HarvestFruitTreeSource(ItemSource):
+ add_tags = (ItemTag.CROPSANITY,)
+
+ sapling: str
+ seasons: Tuple[str, ...] = Season.all
+
+ @property
+ def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]:
+ return {
+ self.sapling: (ItemTag.CROPSANITY_SEED,)
+ }
+
+
+@dataclass(frozen=True, **kw_only)
+class HarvestCropSource(ItemSource):
+ add_tags = (ItemTag.CROPSANITY,)
+
+ seed: str
+ seasons: Tuple[str, ...] = Season.all
+ """Empty means it can't be grown on the farm."""
+
+ @property
+ def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]:
+ return {
+ self.seed: (ItemTag.CROPSANITY_SEED,)
+ }
+
+
+@dataclass(frozen=True, **kw_only)
+class ArtifactSpotSource(ItemSource):
+ amount: int
diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv
index a3d61e8b58e0..e026090f8659 100644
--- a/worlds/stardew_valley/data/items.csv
+++ b/worlds/stardew_valley/data/items.csv
@@ -1,54 +1,52 @@
id,name,classification,groups,mod_name
0,Joja Cola,filler,TRASH,
-15,Rusty Key,progression,MUSEUM,
-16,Dwarvish Translation Guide,progression,MUSEUM,
+15,Rusty Key,progression,,
+16,Dwarvish Translation Guide,progression,,
17,Bridge Repair,progression,COMMUNITY_REWARD,
18,Greenhouse,progression,COMMUNITY_REWARD,
19,Glittering Boulder Removed,progression,COMMUNITY_REWARD,
20,Minecarts Repair,useful,COMMUNITY_REWARD,
21,Bus Repair,progression,COMMUNITY_REWARD,
-22,Movie Theater,useful,,
+22,Progressive Movie Theater,progression,COMMUNITY_REWARD,
23,Stardrop,progression,,
24,Progressive Backpack,progression,,
-25,Rusty Sword,progression,WEAPON,
-26,Leather Boots,progression,"FOOTWEAR,MINES_FLOOR_10",
-27,Work Boots,useful,"FOOTWEAR,MINES_FLOOR_10",
-28,Wooden Blade,progression,"MINES_FLOOR_10,WEAPON",
-29,Iron Dirk,progression,"MINES_FLOOR_10,WEAPON",
-30,Wind Spire,progression,"MINES_FLOOR_10,WEAPON",
-31,Femur,progression,"MINES_FLOOR_10,WEAPON",
-32,Steel Smallsword,progression,"MINES_FLOOR_20,WEAPON",
-33,Wood Club,progression,"MINES_FLOOR_20,WEAPON",
-34,Elf Blade,progression,"MINES_FLOOR_20,WEAPON",
-35,Glow Ring,useful,"MINES_FLOOR_20,RING",
-36,Magnet Ring,useful,"MINES_FLOOR_20,RING",
-37,Slingshot,progression,WEAPON,
-38,Tundra Boots,useful,"FOOTWEAR,MINES_FLOOR_50",
-39,Thermal Boots,useful,"FOOTWEAR,MINES_FLOOR_50",
-40,Combat Boots,useful,"FOOTWEAR,MINES_FLOOR_50",
-41,Silver Saber,progression,"MINES_FLOOR_50,WEAPON",
-42,Pirate's Sword,progression,"MINES_FLOOR_50,WEAPON",
-43,Crystal Dagger,progression,"MINES_FLOOR_60,WEAPON",
-44,Cutlass,progression,"MINES_FLOOR_60,WEAPON",
-45,Iron Edge,progression,"MINES_FLOOR_60,WEAPON",
-46,Burglar's Shank,progression,"MINES_FLOOR_60,WEAPON",
-47,Wood Mallet,progression,"MINES_FLOOR_60,WEAPON",
-48,Master Slingshot,progression,WEAPON,
-49,Firewalker Boots,useful,"FOOTWEAR,MINES_FLOOR_80",
-50,Dark Boots,useful,"FOOTWEAR,MINES_FLOOR_80",
-51,Claymore,progression,"MINES_FLOOR_80,WEAPON",
-52,Templar's Blade,progression,"MINES_FLOOR_80,WEAPON",
-53,Kudgel,progression,"MINES_FLOOR_80,WEAPON",
-54,Shadow Dagger,progression,"MINES_FLOOR_80,WEAPON",
-55,Obsidian Edge,progression,"MINES_FLOOR_90,WEAPON",
-56,Tempered Broadsword,progression,"MINES_FLOOR_90,WEAPON",
-57,Wicked Kris,progression,"MINES_FLOOR_90,WEAPON",
-58,Bone Sword,progression,"MINES_FLOOR_90,WEAPON",
-59,Ossified Blade,progression,"MINES_FLOOR_90,WEAPON",
-60,Space Boots,useful,"FOOTWEAR,MINES_FLOOR_110",
-61,Crystal Shoes,useful,"FOOTWEAR,MINES_FLOOR_110",
-62,Steel Falchion,progression,"MINES_FLOOR_110,WEAPON",
-63,The Slammer,progression,"MINES_FLOOR_110,WEAPON",
+25,Rusty Sword,filler,"WEAPON,DEPRECATED",
+26,Leather Boots,filler,"FOOTWEAR,DEPRECATED",
+27,Work Boots,filler,"FOOTWEAR,DEPRECATED",
+28,Wooden Blade,filler,"WEAPON,DEPRECATED",
+29,Iron Dirk,filler,"WEAPON,DEPRECATED",
+30,Wind Spire,filler,"WEAPON,DEPRECATED",
+31,Femur,filler,"WEAPON,DEPRECATED",
+32,Steel Smallsword,filler,"WEAPON,DEPRECATED",
+33,Wood Club,filler,"WEAPON,DEPRECATED",
+34,Elf Blade,filler,"WEAPON,DEPRECATED",
+37,Slingshot,filler,"WEAPON,DEPRECATED",
+38,Tundra Boots,filler,"FOOTWEAR,DEPRECATED",
+39,Thermal Boots,filler,"FOOTWEAR,DEPRECATED",
+40,Combat Boots,filler,"FOOTWEAR,DEPRECATED",
+41,Silver Saber,filler,"WEAPON,DEPRECATED",
+42,Pirate's Sword,filler,"WEAPON,DEPRECATED",
+43,Crystal Dagger,filler,"WEAPON,DEPRECATED",
+44,Cutlass,filler,"WEAPON,DEPRECATED",
+45,Iron Edge,filler,"WEAPON,DEPRECATED",
+46,Burglar's Shank,filler,"WEAPON,DEPRECATED",
+47,Wood Mallet,filler,"WEAPON,DEPRECATED",
+48,Master Slingshot,filler,"WEAPON,DEPRECATED",
+49,Firewalker Boots,filler,"FOOTWEAR,DEPRECATED",
+50,Dark Boots,filler,"FOOTWEAR,DEPRECATED",
+51,Claymore,filler,"WEAPON,DEPRECATED",
+52,Templar's Blade,filler,"WEAPON,DEPRECATED",
+53,Kudgel,filler,"WEAPON,DEPRECATED",
+54,Shadow Dagger,filler,"WEAPON,DEPRECATED",
+55,Obsidian Edge,filler,"WEAPON,DEPRECATED",
+56,Tempered Broadsword,filler,"WEAPON,DEPRECATED",
+57,Wicked Kris,filler,"WEAPON,DEPRECATED",
+58,Bone Sword,filler,"WEAPON,DEPRECATED",
+59,Ossified Blade,filler,"WEAPON,DEPRECATED",
+60,Space Boots,filler,"FOOTWEAR,DEPRECATED",
+61,Crystal Shoes,filler,"FOOTWEAR,DEPRECATED",
+62,Steel Falchion,filler,"WEAPON,DEPRECATED",
+63,The Slammer,filler,"WEAPON,DEPRECATED",
64,Skull Key,progression,,
65,Progressive Hoe,progression,PROGRESSIVE_TOOLS,
66,Progressive Pickaxe,progression,PROGRESSIVE_TOOLS,
@@ -56,47 +54,47 @@ id,name,classification,groups,mod_name
68,Progressive Watering Can,progression,PROGRESSIVE_TOOLS,
69,Progressive Trash Can,progression,PROGRESSIVE_TOOLS,
70,Progressive Fishing Rod,progression,PROGRESSIVE_TOOLS,
-71,Golden Scythe,useful,,
+71,Golden Scythe,useful,DEPRECATED,
72,Progressive Mine Elevator,progression,,
73,Farming Level,progression,SKILL_LEVEL_UP,
74,Fishing Level,progression,SKILL_LEVEL_UP,
75,Foraging Level,progression,SKILL_LEVEL_UP,
76,Mining Level,progression,SKILL_LEVEL_UP,
77,Combat Level,progression,SKILL_LEVEL_UP,
-78,Earth Obelisk,progression,,
-79,Water Obelisk,progression,,
-80,Desert Obelisk,progression,,
-81,Island Obelisk,progression,GINGER_ISLAND,
-82,Junimo Hut,useful,,
-83,Gold Clock,progression,,
-84,Progressive Coop,progression,,
-85,Progressive Barn,progression,,
-86,Well,useful,,
-87,Silo,progression,,
-88,Mill,progression,,
-89,Progressive Shed,progression,,
-90,Fish Pond,progression,,
-91,Stable,useful,,
-92,Slime Hutch,useful,,
-93,Shipping Bin,progression,,
+78,Earth Obelisk,progression,WIZARD_BUILDING,
+79,Water Obelisk,progression,WIZARD_BUILDING,
+80,Desert Obelisk,progression,WIZARD_BUILDING,
+81,Island Obelisk,progression,"WIZARD_BUILDING,GINGER_ISLAND",
+82,Junimo Hut,useful,WIZARD_BUILDING,
+83,Gold Clock,progression,WIZARD_BUILDING,
+84,Progressive Coop,progression,BUILDING,
+85,Progressive Barn,progression,BUILDING,
+86,Well,useful,BUILDING,
+87,Silo,progression,BUILDING,
+88,Mill,progression,BUILDING,
+89,Progressive Shed,progression,BUILDING,
+90,Fish Pond,progression,BUILDING,
+91,Stable,useful,BUILDING,
+92,Slime Hutch,progression,BUILDING,
+93,Shipping Bin,progression,BUILDING,
94,Beach Bridge,progression,,
-95,Adventurer's Guild,progression,,
+95,Adventurer's Guild,progression,DEPRECATED,
96,Club Card,progression,,
97,Magnifying Glass,progression,,
98,Bear's Knowledge,progression,,
-99,Iridium Snake Milk,progression,,
+99,Iridium Snake Milk,useful,,
100,JotPK: Progressive Boots,progression,ARCADE_MACHINE_BUFFS,
101,JotPK: Progressive Gun,progression,ARCADE_MACHINE_BUFFS,
102,JotPK: Progressive Ammo,progression,ARCADE_MACHINE_BUFFS,
103,JotPK: Extra Life,progression,ARCADE_MACHINE_BUFFS,
104,JotPK: Increased Drop Rate,progression,ARCADE_MACHINE_BUFFS,
105,Junimo Kart: Extra Life,progression,ARCADE_MACHINE_BUFFS,
-106,Galaxy Sword,progression,"GALAXY_WEAPONS,WEAPON",
-107,Galaxy Dagger,progression,"GALAXY_WEAPONS,WEAPON",
-108,Galaxy Hammer,progression,"GALAXY_WEAPONS,WEAPON",
-109,Movement Speed Bonus,progression,,
-110,Luck Bonus,progression,,
-111,Lava Katana,progression,"MINES_FLOOR_110,WEAPON",
+106,Galaxy Sword,filler,"WEAPON,DEPRECATED",
+107,Galaxy Dagger,filler,"WEAPON,DEPRECATED",
+108,Galaxy Hammer,filler,"WEAPON,DEPRECATED",
+109,Movement Speed Bonus,useful,,
+110,Luck Bonus,filler,PLAYER_BUFF,
+111,Lava Katana,filler,"WEAPON,DEPRECATED",
112,Progressive House,progression,,
113,Traveling Merchant: Sunday,progression,TRAVELING_MERCHANT_DAY,
114,Traveling Merchant: Monday,progression,TRAVELING_MERCHANT_DAY,
@@ -105,8 +103,8 @@ id,name,classification,groups,mod_name
117,Traveling Merchant: Thursday,progression,TRAVELING_MERCHANT_DAY,
118,Traveling Merchant: Friday,progression,TRAVELING_MERCHANT_DAY,
119,Traveling Merchant: Saturday,progression,TRAVELING_MERCHANT_DAY,
-120,Traveling Merchant Stock Size,progression,,
-121,Traveling Merchant Discount,progression,,
+120,Traveling Merchant Stock Size,useful,,
+121,Traveling Merchant Discount,useful,DEPRECATED,
122,Return Scepter,useful,,
123,Progressive Season,progression,,
124,Spring,progression,SEASON,
@@ -223,7 +221,7 @@ id,name,classification,groups,mod_name
235,Quality Bobber Recipe,progression,SPECIAL_ORDER_BOARD,
236,Mini-Obelisk Recipe,progression,SPECIAL_ORDER_BOARD,
237,Monster Musk Recipe,progression,SPECIAL_ORDER_BOARD,
-239,Sewing Machine,progression,"RESOURCE_PACK_USEFUL,SPECIAL_ORDER_BOARD",
+239,Sewing Machine,useful,"RESOURCE_PACK_USEFUL,SPECIAL_ORDER_BOARD",
240,Coffee Maker,progression,"RESOURCE_PACK_USEFUL,SPECIAL_ORDER_BOARD",
241,Mini-Fridge,useful,"RESOURCE_PACK_USEFUL,SPECIAL_ORDER_BOARD",
242,Mini-Shipping Bin,progression,"RESOURCE_PACK_USEFUL,SPECIAL_ORDER_BOARD",
@@ -245,7 +243,7 @@ id,name,classification,groups,mod_name
258,Banana Sapling,progression,"GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY",
259,Mango Sapling,progression,"GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL,CROPSANITY",
260,Boat Repair,progression,GINGER_ISLAND,
-261,Open Professor Snail Cave,progression,"GINGER_ISLAND",
+261,Open Professor Snail Cave,progression,GINGER_ISLAND,
262,Island North Turtle,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
263,Island West Turtle,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
264,Island Farmhouse,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
@@ -254,42 +252,290 @@ id,name,classification,groups,mod_name
267,Dig Site Bridge,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
268,Island Trader,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
269,Volcano Bridge,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
-270,Volcano Exit Shortcut,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
+270,Volcano Exit Shortcut,useful,"GINGER_ISLAND,WALNUT_PURCHASE",
271,Island Resort,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
272,Parrot Express,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
273,Qi Walnut Room,progression,"GINGER_ISLAND,WALNUT_PURCHASE",
274,Pineapple Seeds,progression,"GINGER_ISLAND,CROPSANITY",
275,Taro Tuber,progression,"GINGER_ISLAND,CROPSANITY",
-276,Weather Report,useful,"TV_CHANNEL",
-277,Fortune Teller,useful,"TV_CHANNEL",
-278,Livin' Off The Land,useful,"TV_CHANNEL",
-279,The Queen of Sauce,progression,"TV_CHANNEL",
-280,Fishing Information Broadcasting Service,useful,"TV_CHANNEL",
-281,Sinister Signal,useful,"TV_CHANNEL",
+276,Weather Report,useful,TV_CHANNEL,
+277,Fortune Teller,useful,TV_CHANNEL,
+278,Livin' Off The Land,useful,TV_CHANNEL,
+279,The Queen of Sauce,progression,TV_CHANNEL,
+280,Fishing Information Broadcasting Service,useful,TV_CHANNEL,
+281,Sinister Signal,filler,TV_CHANNEL,
282,Dark Talisman,progression,,
-283,Ostrich Incubator Recipe,progression,"GINGER_ISLAND",
-284,Cute Baby,progression,"BABY",
-285,Ugly Baby,progression,"BABY",
-286,Deluxe Scarecrow Recipe,progression,"FESTIVAL,RARECROW",
-287,Treehouse,progression,"GINGER_ISLAND",
+283,Ostrich Incubator Recipe,progression,GINGER_ISLAND,
+284,Cute Baby,progression,BABY,
+285,Ugly Baby,progression,BABY,
+286,Deluxe Scarecrow Recipe,progression,RARECROW,
+287,Treehouse,progression,GINGER_ISLAND,
288,Coffee Bean,progression,CROPSANITY,
-4001,Burnt,trap,TRAP,
-4002,Darkness,trap,TRAP,
-4003,Frozen,trap,TRAP,
-4004,Jinxed,trap,TRAP,
-4005,Nauseated,trap,TRAP,
-4006,Slimed,trap,TRAP,
-4007,Weakness,trap,TRAP,
-4008,Taxes,trap,TRAP,
-4009,Random Teleport,trap,TRAP,
-4010,The Crows,trap,TRAP,
-4011,Monsters,trap,TRAP,
-4012,Entrance Reshuffle,trap,"TRAP,DEPRECATED",
-4013,Debris,trap,TRAP,
-4014,Shuffle,trap,TRAP,
-4015,Temporary Winter,trap,"TRAP,DEPRECATED",
-4016,Pariah,trap,TRAP,
-4017,Drought,trap,TRAP,
+289,Progressive Weapon,progression,"WEAPON,WEAPON_GENERIC",
+290,Progressive Sword,progression,"WEAPON,WEAPON_SWORD",
+291,Progressive Club,progression,"WEAPON,WEAPON_CLUB",
+292,Progressive Dagger,progression,"WEAPON,WEAPON_DAGGER",
+293,Progressive Slingshot,progression,"WEAPON,WEAPON_SLINGSHOT",
+294,Progressive Footwear,useful,FOOTWEAR,
+295,Small Glow Ring,filler,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+296,Glow Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+297,Small Magnet Ring,filler,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+298,Magnet Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+299,Slime Charmer Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+300,Warrior Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+301,Vampire Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+302,Savage Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+303,Ring of Yoba,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+304,Sturdy Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+305,Burglar's Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+306,Iridium Band,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+307,Jukebox Ring,filler,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+308,Amethyst Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+309,Topaz Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+310,Aquamarine Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+311,Jade Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+312,Emerald Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+313,Ruby Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+314,Wedding Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+315,Crabshell Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+316,Napalm Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+317,Thorns Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+318,Lucky Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+319,Hot Java Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+320,Protection Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+321,Soul Sapper Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+322,Phoenix Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+323,Immunity Band,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+324,Glowstone Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE",
+325,Fairy Dust Recipe,progression,"GINGER_ISLAND",
+326,Heavy Tapper Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND",
+327,Hyper Speed-Gro Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND",
+328,Deluxe Fertilizer Recipe,progression,QI_CRAFTING_RECIPE,
+329,Hopper Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND",
+330,Magic Bait Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND",
+331,Jack-O-Lantern Recipe,progression,FESTIVAL,
+333,Tub o' Flowers Recipe,progression,FESTIVAL,
+335,Moonlight Jellies Banner,filler,FESTIVAL,
+336,Starport Decal,filler,FESTIVAL,
+337,Golden Egg,progression,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
+340,Algae Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+341,Artichoke Dip Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+342,Autumn's Bounty Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+343,Baked Fish Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+344,Banana Pudding Recipe,progression,"CHEFSANITY,GINGER_ISLAND,CHEFSANITY_PURCHASE",
+345,Bean Hotpot Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+346,Blackberry Cobbler Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+347,Blueberry Tart Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+348,Bread Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_FRIENDSHIP,CHEFSANITY_PURCHASE",
+349,Bruschetta Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+350,Carp Surprise Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+351,Cheese Cauliflower Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+352,Chocolate Cake Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+353,Chowder Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+354,Coleslaw Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+355,Complete Breakfast Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+356,Cookies Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+357,Crab Cakes Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+358,Cranberry Candy Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+359,Cranberry Sauce Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+360,Crispy Bass Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+361,Dish O' The Sea Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",
+362,Eggplant Parmesan Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+363,Escargot Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+364,Farmer's Lunch Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",
+365,Fiddlehead Risotto Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+366,Fish Stew Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+367,Fish Taco Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+368,Fried Calamari Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+369,Fried Eel Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+370,Fried Egg Recipe,progression,CHEFSANITY_STARTER,
+371,Fried Mushroom Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+372,Fruit Salad Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+373,Ginger Ale Recipe,progression,"CHEFSANITY,GINGER_ISLAND,CHEFSANITY_PURCHASE",
+374,Glazed Yams Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+375,Hashbrowns Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_PURCHASE",
+376,Ice Cream Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+377,Lobster Bisque Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_FRIENDSHIP",
+378,Lucky Lunch Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+379,Maki Roll Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_PURCHASE",
+380,Mango Sticky Rice Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP,GINGER_ISLAND",
+381,Maple Bar Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+382,Miner's Treat Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",
+383,Omelet Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_PURCHASE",
+384,Pale Broth Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+385,Pancakes Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_PURCHASE",
+386,Parsnip Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+387,Pepper Poppers Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+388,Pink Cake Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+389,Pizza Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_PURCHASE",
+390,Plum Pudding Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+391,Poi Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP,GINGER_ISLAND",
+392,Poppyseed Muffin Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+393,Pumpkin Pie Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+394,Pumpkin Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+395,Radish Salad Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+396,Red Plate Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+397,Rhubarb Pie Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+398,Rice Pudding Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+399,Roasted Hazelnuts Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+400,Roots Platter Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",
+401,Salad Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+402,Salmon Dinner Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+403,Sashimi Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+404,Seafoam Pudding Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",
+405,Shrimp Cocktail Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+406,Spaghetti Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+407,Spicy Eel Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+408,Squid Ink Ravioli Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",
+409,Stir Fry Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+410,Strange Bun Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+411,Stuffing Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+412,Super Meal Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+413,Survival Burger Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",
+414,Tom Kha Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+415,Tortilla Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_PURCHASE",
+416,Triple Shot Espresso Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",
+417,Tropical Curry Recipe,progression,"CHEFSANITY,GINGER_ISLAND,CHEFSANITY_PURCHASE",
+418,Trout Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS",
+419,Vegetable Medley Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+420,Moss Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",
+425,Gate Recipe,progression,CRAFTSANITY,
+426,Wood Fence Recipe,progression,CRAFTSANITY,
+427,Deluxe Retaining Soil Recipe,progression,"CRAFTSANITY,GINGER_ISLAND",
+428,Grass Starter Recipe,progression,CRAFTSANITY,
+429,Wood Floor Recipe,progression,CRAFTSANITY,
+430,Rustic Plank Floor Recipe,progression,CRAFTSANITY,
+431,Straw Floor Recipe,progression,CRAFTSANITY,
+432,Weathered Floor Recipe,progression,CRAFTSANITY,
+433,Crystal Floor Recipe,progression,CRAFTSANITY,
+434,Stone Floor Recipe,progression,CRAFTSANITY,
+435,Stone Walkway Floor Recipe,progression,CRAFTSANITY,
+436,Brick Floor Recipe,progression,CRAFTSANITY,
+437,Wood Path Recipe,progression,CRAFTSANITY,
+438,Gravel Path Recipe,progression,CRAFTSANITY,
+439,Cobblestone Path Recipe,progression,CRAFTSANITY,
+440,Stepping Stone Path Recipe,progression,CRAFTSANITY,
+441,Crystal Path Recipe,progression,CRAFTSANITY,
+442,Wedding Ring Recipe,progression,CRAFTSANITY,
+443,Warp Totem: Desert Recipe,progression,CRAFTSANITY,
+444,Warp Totem: Island Recipe,progression,"CRAFTSANITY,GINGER_ISLAND",
+445,Torch Recipe,progression,CRAFTSANITY,
+446,Campfire Recipe,progression,CRAFTSANITY,
+447,Wooden Brazier Recipe,progression,CRAFTSANITY,
+448,Stone Brazier Recipe,progression,CRAFTSANITY,
+449,Gold Brazier Recipe,progression,CRAFTSANITY,
+450,Carved Brazier Recipe,progression,CRAFTSANITY,
+451,Stump Brazier Recipe,progression,CRAFTSANITY,
+452,Barrel Brazier Recipe,progression,CRAFTSANITY,
+453,Skull Brazier Recipe,progression,CRAFTSANITY,
+454,Marble Brazier Recipe,progression,CRAFTSANITY,
+455,Wood Lamp-post Recipe,progression,CRAFTSANITY,
+456,Iron Lamp-post Recipe,progression,CRAFTSANITY,
+457,Furnace Recipe,progression,"CRAFTSANITY",
+458,Wicked Statue Recipe,progression,CRAFTSANITY,
+459,Chest Recipe,progression,CRAFTSANITY,
+460,Wood Sign Recipe,progression,CRAFTSANITY,
+461,Stone Sign Recipe,progression,CRAFTSANITY,
+469,Railroad Boulder Removed,progression,,
+470,Fruit Bats,progression,,
+471,Mushroom Boxes,progression,,
+475,The Gateway Gazette,progression,TV_CHANNEL,
+476,Carrot Seeds,progression,CROPSANITY,
+477,Summer Squash Seeds,progression,CROPSANITY,
+478,Broccoli Seeds,progression,CROPSANITY,
+479,Powdermelon Seeds,progression,CROPSANITY,
+480,Progressive Raccoon,progression,,
+481,Farming Mastery,progression,SKILL_MASTERY,
+482,Mining Mastery,progression,SKILL_MASTERY,
+483,Foraging Mastery,progression,SKILL_MASTERY,
+484,Fishing Mastery,progression,SKILL_MASTERY,
+485,Combat Mastery,progression,SKILL_MASTERY,
+486,Fish Smoker Recipe,progression,CRAFTSANITY,
+487,Dehydrator Recipe,progression,CRAFTSANITY,
+488,Big Chest Recipe,progression,CRAFTSANITY,
+489,Big Stone Chest Recipe,progression,CRAFTSANITY,
+490,Text Sign Recipe,progression,CRAFTSANITY,
+491,Blue Grass Starter Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND",
+492,Mastery Of The Five Ways,progression,SKILL_MASTERY,
+493,Progressive Scythe,useful,,
+494,Progressive Pan,progression,PROGRESSIVE_TOOLS,
+495,Calico Statue,filler,FESTIVAL,
+496,Mummy Mask,filler,FESTIVAL,
+497,Free Cactis,filler,FESTIVAL,
+498,Gil's Hat,filler,FESTIVAL,
+499,Bucket Hat,filler,FESTIVAL,
+500,Mounted Trout,filler,FESTIVAL,
+501,'Squid Kid',filler,FESTIVAL,
+502,Squid Hat,filler,FESTIVAL,
+503,Resource Pack: 200 Calico Egg,useful,"FESTIVAL",
+504,Resource Pack: 120 Calico Egg,useful,"FESTIVAL",
+505,Resource Pack: 100 Calico Egg,useful,"FESTIVAL",
+506,Resource Pack: 50 Calico Egg,useful,"FESTIVAL",
+507,Resource Pack: 40 Calico Egg,useful,"FESTIVAL",
+508,Resource Pack: 35 Calico Egg,useful,"FESTIVAL",
+509,Resource Pack: 30 Calico Egg,useful,"FESTIVAL",
+510,Book: The Art O' Crabbing,useful,"FESTIVAL",
+511,Mr Qi's Plane Ride,progression,,
+521,Power: Price Catalogue,useful,"BOOK_POWER",
+522,Power: Mapping Cave Systems,useful,"BOOK_POWER",
+523,Power: Way Of The Wind pt. 1,progression,"BOOK_POWER",
+524,Power: Way Of The Wind pt. 2,useful,"BOOK_POWER",
+525,Power: Monster Compendium,useful,"BOOK_POWER",
+526,Power: Friendship 101,useful,"BOOK_POWER",
+527,"Power: Jack Be Nimble, Jack Be Thick",useful,"BOOK_POWER",
+528,Power: Woody's Secret,useful,"BOOK_POWER",
+529,Power: Raccoon Journal,useful,"BOOK_POWER",
+530,Power: Jewels Of The Sea,useful,"BOOK_POWER",
+531,Power: Dwarvish Safety Manual,useful,"BOOK_POWER",
+532,Power: The Art O' Crabbing,useful,"BOOK_POWER",
+533,Power: The Alleyway Buffet,useful,"BOOK_POWER",
+534,Power: The Diamond Hunter,useful,"BOOK_POWER",
+535,Power: Book of Mysteries,progression,"BOOK_POWER",
+536,Power: Horse: The Book,useful,"BOOK_POWER",
+537,Power: Treasure Appraisal Guide,useful,"BOOK_POWER",
+538,Power: Ol' Slitherlegs,useful,"BOOK_POWER",
+539,Power: Animal Catalogue,useful,"BOOK_POWER",
+541,Progressive Lost Book,progression,"LOST_BOOK",
+551,Golden Walnut,progression,"RESOURCE_PACK,GINGER_ISLAND",
+552,3 Golden Walnuts,progression,"GINGER_ISLAND",
+553,5 Golden Walnuts,progression,"GINGER_ISLAND",
+554,Damage Bonus,filler,PLAYER_BUFF,
+555,Defense Bonus,filler,PLAYER_BUFF,
+556,Immunity Bonus,filler,PLAYER_BUFF,
+557,Health Bonus,filler,PLAYER_BUFF,
+558,Energy Bonus,filler,PLAYER_BUFF,
+559,Bite Rate Bonus,filler,PLAYER_BUFF,
+560,Fish Trap Bonus,filler,PLAYER_BUFF,
+561,Fishing Bar Size Bonus,filler,PLAYER_BUFF,
+562,Quality Bonus,filler,PLAYER_BUFF,
+563,Glow Bonus,filler,PLAYER_BUFF,
+4001,Burnt Trap,trap,TRAP,
+4002,Darkness Trap,trap,TRAP,
+4003,Frozen Trap,trap,TRAP,
+4004,Jinxed Trap,trap,TRAP,
+4005,Nauseated Trap,trap,TRAP,
+4006,Slimed Trap,trap,TRAP,
+4007,Weakness Trap,trap,TRAP,
+4008,Taxes Trap,trap,TRAP,
+4009,Random Teleport Trap,trap,TRAP,
+4010,The Crows Trap,trap,TRAP,
+4011,Monsters Trap,trap,TRAP,
+4012,Entrance Reshuffle Trap,trap,"TRAP,DEPRECATED",
+4013,Debris Trap,trap,TRAP,
+4014,Shuffle Trap,trap,TRAP,
+4015,Temporary Winter Trap,trap,"TRAP,DEPRECATED",
+4016,Pariah Trap,trap,TRAP,
+4017,Drought Trap,trap,TRAP,
+4018,Time Flies Trap,trap,TRAP,
+4019,Babies Trap,trap,TRAP,
+4020,Meow Trap,trap,TRAP,
+4021,Bark Trap,trap,TRAP,
+4022,Depression Trap,trap,"TRAP,DEPRECATED",
+4023,Benjamin Budton Trap,trap,TRAP,
+4024,Inflation Trap,trap,TRAP,
+4025,Bomb Trap,trap,TRAP,
+4026,Nudge Trap,trap,TRAP,
+4501,Deflation Bonus,filler,,
5000,Resource Pack: 500 Money,useful,"BASE_RESOURCE,RESOURCE_PACK",
5001,Resource Pack: 1000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK",
5002,Resource Pack: 1500 Money,useful,"BASE_RESOURCE,RESOURCE_PACK",
@@ -336,12 +582,12 @@ id,name,classification,groups,mod_name
5043,Resource Pack: 7 Warp Totem: Farm,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM",
5044,Resource Pack: 9 Warp Totem: Farm,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM",
5045,Resource Pack: 10 Warp Totem: Farm,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM",
-5046,Resource Pack: 1 Warp Totem: Island,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM",
-5047,Resource Pack: 3 Warp Totem: Island,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM",
-5048,Resource Pack: 5 Warp Totem: Island,filler,"RESOURCE_PACK,WARP_TOTEM",
-5049,Resource Pack: 7 Warp Totem: Island,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM",
-5050,Resource Pack: 9 Warp Totem: Island,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM",
-5051,Resource Pack: 10 Warp Totem: Island,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM",
+5046,Resource Pack: 1 Warp Totem: Island,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM,GINGER_ISLAND",
+5047,Resource Pack: 3 Warp Totem: Island,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM,GINGER_ISLAND",
+5048,Resource Pack: 5 Warp Totem: Island,filler,"RESOURCE_PACK,WARP_TOTEM,GINGER_ISLAND",
+5049,Resource Pack: 7 Warp Totem: Island,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM,GINGER_ISLAND",
+5050,Resource Pack: 9 Warp Totem: Island,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM,GINGER_ISLAND",
+5051,Resource Pack: 10 Warp Totem: Island,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM,GINGER_ISLAND",
5052,Resource Pack: 1 Warp Totem: Mountains,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM",
5053,Resource Pack: 3 Warp Totem: Mountains,filler,"DEPRECATED,RESOURCE_PACK,WARP_TOTEM",
5054,Resource Pack: 5 Warp Totem: Mountains,filler,"RESOURCE_PACK,WARP_TOTEM",
@@ -492,7 +738,7 @@ id,name,classification,groups,mod_name
5199,Friendship Bonus (2 <3),useful,"FRIENDSHIP_PACK,COMMUNITY_REWARD",
5200,Friendship Bonus (3 <3),useful,"DEPRECATED,FRIENDSHIP_PACK,RESOURCE_PACK",
5201,Friendship Bonus (4 <3),useful,"DEPRECATED,FRIENDSHIP_PACK,RESOURCE_PACK",
-5202,30 Qi Gems,useful,"GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
+5202,15 Qi Gems,progression,"GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5203,Solar Panel,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5204,Geode Crusher,filler,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5205,Farm Computer,filler,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
@@ -507,36 +753,29 @@ id,name,classification,groups,mod_name
5214,Quality Sprinkler,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5215,Iridium Sprinkler,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5216,Scarecrow,filler,RESOURCE_PACK,
-5217,Deluxe Scarecrow,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
+5217,Deluxe Scarecrow,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5218,Furnace,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5219,Charcoal Kiln,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5220,Lightning Rod,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5221,Resource Pack: 5000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5222,Resource Pack: 10000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5223,Junimo Chest,filler,"EXACTLY_TWO,RESOURCE_PACK",
-5224,Horse Flute,filler,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
-5225,Pierre's Missing Stocklist,filler,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
+5224,Horse Flute,useful,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
+5225,Pierre's Missing Stocklist,useful,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5226,Hopper,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5227,Enricher,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5228,Pressure Nozzle,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5229,Deconstructor,filler,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
-5230,Key To The Town,filler,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
+5230,Key To The Town,useful,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5231,Galaxy Soul,filler,"GINGER_ISLAND,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5232,Mushroom Tree Seed,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5233,Resource Pack: 20 Magic Bait,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5234,Resource Pack: 10 Qi Seasoning,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5235,Mr. Qi's Hat,filler,"MAXIMUM_ONE,RESOURCE_PACK",
5236,Aquatic Sanctuary,filler,RESOURCE_PACK,
-5237,Heavy Tapper Recipe,filler,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
-5238,Hyper Speed-Gro Recipe,filler,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
-5239,Deluxe Fertilizer Recipe,filler,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
-5240,Hopper Recipe,filler,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
-5241,Magic Bait Recipe,filler,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
+5237,Leprechaun Hat,filler,"MAXIMUM_ONE,RESOURCE_PACK",
5242,Exotic Double Bed,filler,RESOURCE_PACK,
-5243,Resource Pack: 2 Qi Gem,filler,"GINGER_ISLAND,RESOURCE_PACK",
-5244,Golden Egg,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
-5245,Golden Walnut,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL,GINGER_ISLAND",
-5246,Fairy Dust Recipe,useful,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
+5243,Resource Pack: 2 Qi Gem,filler,"GINGER_ISLAND,RESOURCE_PACK,DEPRECATED",
5247,Fairy Dust,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5248,Seed Maker,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5249,Keg,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
@@ -552,10 +791,34 @@ id,name,classification,groups,mod_name
5259,Worm Bin,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5260,Tapper,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5261,Heavy Tapper,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
-5262,Slime Incubator,useful,"RESOURCE_PACK",
-5263,Slime Egg-Press,useful,"RESOURCE_PACK",
+5262,Slime Incubator,useful,RESOURCE_PACK,
+5263,Slime Egg-Press,useful,RESOURCE_PACK,
5264,Crystalarium,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5265,Ostrich Incubator,useful,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
+5266,Resource Pack: 5 Staircase,filler,"RESOURCE_PACK",
+5267,Auto-Petter,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
+5268,Auto-Grabber,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
+5269,Resource Pack: 10 Calico Egg,filler,"RESOURCE_PACK",
+5270,Resource Pack: 20 Calico Egg,filler,"RESOURCE_PACK",
+5272,Tent Kit,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
+5273,Resource Pack: 4 Mystery Box,filler,"RESOURCE_PACK",
+5274,Prize Ticket,filler,"RESOURCE_PACK",
+5275,Resource Pack: 20 Deluxe Bait,filler,"RESOURCE_PACK",
+5276,Resource Pack: 2 Triple Shot Espresso,filler,"RESOURCE_PACK",
+5277,Dish O' The Sea,filler,"RESOURCE_PACK",
+5278,Seafoam Pudding,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
+5279,Trap Bobber,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
+5280,Treasure Chest,filler,"RESOURCE_PACK",
+5281,Resource Pack: 15 Mixed Seeds,filler,"RESOURCE_PACK",
+5282,Resource Pack: 15 Mixed Flower Seeds,filler,"RESOURCE_PACK",
+5283,Resource Pack: 5 Cherry Bomb,filler,"RESOURCE_PACK",
+5284,Resource Pack: 3 Bomb,filler,"RESOURCE_PACK",
+5285,Resource Pack: 2 Mega Bomb,filler,"RESOURCE_PACK",
+5286,Resource Pack: 2 Life Elixir,filler,"RESOURCE_PACK",
+5287,Resource Pack: 5 Coffee,filler,"RESOURCE_PACK",
+5289,Prismatic Shard,filler,"RESOURCE_PACK",
+5290,Stardrop Tea,filler,"RESOURCE_PACK",
+5291,Resource Pack: 2 Artifact Trove,filler,"RESOURCE_PACK",
10001,Luck Level,progression,SKILL_LEVEL_UP,Luck Skill
10002,Magic Level,progression,SKILL_LEVEL_UP,Magic
10003,Socializing Level,progression,SKILL_LEVEL_UP,Socializing Skill
@@ -565,26 +828,29 @@ id,name,classification,groups,mod_name
10007,Tractor Garage,useful,,Tractor Mod
10008,Woods Obelisk,progression,,DeepWoods
10009,Spell: Clear Debris,progression,MAGIC_SPELL,Magic
-10010,Spell: Till,useful,MAGIC_SPELL,Magic
+10010,Spell: Till,progression,MAGIC_SPELL,Magic
10011,Spell: Water,progression,MAGIC_SPELL,Magic
10012,Spell: Blink,progression,MAGIC_SPELL,Magic
-10013,Spell: Evac,useful,MAGIC_SPELL,Magic
-10014,Spell: Haste,filler,MAGIC_SPELL,Magic
+10013,Spell: Evac,progression,MAGIC_SPELL,Magic
+10014,Spell: Haste,progression,MAGIC_SPELL,Magic
10015,Spell: Heal,progression,MAGIC_SPELL,Magic
-10016,Spell: Buff,useful,MAGIC_SPELL,Magic
+10016,Spell: Buff,progression,MAGIC_SPELL,Magic
10017,Spell: Shockwave,progression,MAGIC_SPELL,Magic
10018,Spell: Fireball,progression,MAGIC_SPELL,Magic
-10019,Spell: Frostbite,progression,MAGIC_SPELL,Magic
+10019,Spell: Frostbolt,progression,MAGIC_SPELL,Magic
10020,Spell: Teleport,progression,MAGIC_SPELL,Magic
-10021,Spell: Lantern,filler,MAGIC_SPELL,Magic
+10021,Spell: Lantern,progression,MAGIC_SPELL,Magic
10022,Spell: Tendrils,progression,MAGIC_SPELL,Magic
-10023,Spell: Photosynthesis,useful,MAGIC_SPELL,Magic
+10023,Spell: Photosynthesis,progression,MAGIC_SPELL,Magic
10024,Spell: Descend,progression,MAGIC_SPELL,Magic
10025,Spell: Meteor,progression,MAGIC_SPELL,Magic
-10026,Spell: Bloodmana,useful,MAGIC_SPELL,Magic
-10027,Spell: Lucksteal,useful,MAGIC_SPELL,Magic
+10026,Spell: Bloodmana,progression,MAGIC_SPELL,Magic
+10027,Spell: Lucksteal,progression,MAGIC_SPELL,Magic
10028,Spell: Spirit,progression,MAGIC_SPELL,Magic
-10029,Spell: Rewind,useful,MAGIC_SPELL,Magic
+10029,Spell: Rewind,progression,MAGIC_SPELL,Magic
+10030,Pendant of Community,progression,,DeepWoods
+10031,Pendant of Elders,progression,,DeepWoods
+10032,Pendant of Depths,progression,,DeepWoods
10101,Juna <3,progression,FRIENDSANITY,Juna - Roommate NPC
10102,Jasper <3,progression,FRIENDSANITY,Professor Jasper Thomas
10103,Alec <3,progression,FRIENDSANITY,Alec Revisited
@@ -596,5 +862,101 @@ id,name,classification,groups,mod_name
10109,Delores <3,progression,FRIENDSANITY,Delores - Custom NPC
10110,Ayeisha <3,progression,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
10111,Riley <3,progression,FRIENDSANITY,Custom NPC - Riley
+10112,Claire <3,progression,FRIENDSANITY,Stardew Valley Expanded
+10113,Lance <3,progression,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+10114,Olivia <3,progression,FRIENDSANITY,Stardew Valley Expanded
+10115,Sophia <3,progression,FRIENDSANITY,Stardew Valley Expanded
+10116,Victor <3,progression,FRIENDSANITY,Stardew Valley Expanded
+10117,Andy <3,progression,FRIENDSANITY,Stardew Valley Expanded
+10118,Apples <3,progression,FRIENDSANITY,Stardew Valley Expanded
+10119,Gunther <3,progression,FRIENDSANITY,Stardew Valley Expanded
+10120,Martin <3,progression,FRIENDSANITY,Stardew Valley Expanded
+10121,Marlon <3,progression,FRIENDSANITY,Stardew Valley Expanded
+10122,Morgan <3,progression,FRIENDSANITY,Stardew Valley Expanded
+10123,Scarlett <3,progression,FRIENDSANITY,Stardew Valley Expanded
+10124,Susan <3,progression,FRIENDSANITY,Stardew Valley Expanded
+10125,Morris <3,progression,FRIENDSANITY,Stardew Valley Expanded
+10126,Alecto <3,progression,FRIENDSANITY,Alecto the Witch
+10127,Zic <3,progression,FRIENDSANITY,Distant Lands - Witch Swamp Overhaul
+10128,Lacey <3,progression,FRIENDSANITY,Hat Mouse Lacey
+10129,Gregory <3,progression,FRIENDSANITY,Boarding House and Bus Stop Extension
+10130,Sheila <3,progression,FRIENDSANITY,Boarding House and Bus Stop Extension
+10131,Joel <3,progression,FRIENDSANITY,Boarding House and Bus Stop Extension
10301,Progressive Woods Obelisk Sigils,progression,,DeepWoods
10302,Progressive Skull Cavern Elevator,progression,,Skull Cavern Elevator
+10401,Baked Berry Oatmeal Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+10402,Big Bark Burger Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+10403,Flower Cookie Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+10404,Frog Legs Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+10405,Glazed Butterfish Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+10406,Mixed Berry Pie Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+10407,Mushroom Berry Rice Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+10408,Seaweed Salad Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+10409,Void Delight Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+10410,Void Salmon Sushi Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+10411,Mushroom Kebab Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul
+10412,Crayfish Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul
+10413,Pemmican Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul
+10414,Void Mint Tea Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul
+10415,Ginger Tincture Recipe,progression,"GINGER_ISLAND,CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul
+10416,Special Pumpkin Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Boarding House and Bus Stop Extension
+10417,Rocky Root Coffee Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology
+10418,Digger's Delight Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology
+10419,Ancient Jello Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology
+10420,Grilled Cheese Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill
+10421,Fish Casserole Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill
+10450,Void Mint Seeds,progression,DEPRECATED,Distant Lands - Witch Swamp Overhaul
+10451,Vile Ancient Fruit Seeds,progression,DEPRECATED,Distant Lands - Witch Swamp Overhaul
+10501,Marlon's Boat Paddle,progression,GINGER_ISLAND,Stardew Valley Expanded
+10502,Diamond Wand,filler,"WEAPON,DEPRECATED",Stardew Valley Expanded
+10503,Iridium Bomb,progression,,Stardew Valley Expanded
+10504,Void Spirit Peace Agreement,useful,GINGER_ISLAND,Stardew Valley Expanded
+10505,Kittyfish Spell,progression,,Stardew Valley Expanded
+10506,Nexus: Adventurer's Guild Runes,progression,MOD_WARP,Stardew Valley Expanded
+10507,Nexus: Junimo Woods Runes,progression,MOD_WARP,Stardew Valley Expanded
+10508,Nexus: Aurora Vineyard Runes,progression,MOD_WARP,Stardew Valley Expanded
+10509,Nexus: Sprite Spring Runes,progression,MOD_WARP,Stardew Valley Expanded
+10510,Nexus: Outpost Runes,progression,MOD_WARP,Stardew Valley Expanded
+10511,Nexus: Farm Runes,progression,MOD_WARP,Stardew Valley Expanded
+10512,Nexus: Wizard Runes,progression,MOD_WARP,Stardew Valley Expanded
+10513,Fable Reef Portal,progression,GINGER_ISLAND,Stardew Valley Expanded
+10514,Tempered Galaxy Sword,filler,"WEAPON,DEPRECATED",Stardew Valley Expanded
+10515,Tempered Galaxy Dagger,filler,"WEAPON,DEPRECATED",Stardew Valley Expanded
+10516,Tempered Galaxy Hammer,filler,"WEAPON,DEPRECATED",Stardew Valley Expanded
+10517,Grandpa's Shed,progression,,Stardew Valley Expanded
+10518,Aurora Vineyard Tablet,progression,,Stardew Valley Expanded
+10519,Scarlett's Job Offer,progression,,Stardew Valley Expanded
+10520,Morgan's Schooling,progression,,Stardew Valley Expanded
+10601,Magic Elixir Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Magic
+10602,Travel Core Recipe,progression,CRAFTSANITY,Magic
+10603,Haste Elixir Recipe,progression,CRAFTSANITY,Stardew Valley Expanded
+10604,Hero Elixir Recipe,progression,CRAFTSANITY,Stardew Valley Expanded
+10605,Armor Elixir Recipe,progression,CRAFTSANITY,Stardew Valley Expanded
+10606,Neanderthal Skeleton Recipe,progression,CRAFTSANITY,Boarding House and Bus Stop Extension
+10607,Pterodactyl Skeleton L Recipe,progression,CRAFTSANITY,Boarding House and Bus Stop Extension
+10608,Pterodactyl Skeleton M Recipe,progression,CRAFTSANITY,Boarding House and Bus Stop Extension
+10609,Pterodactyl Skeleton R Recipe,progression,CRAFTSANITY,Boarding House and Bus Stop Extension
+10610,T-Rex Skeleton L Recipe,progression,CRAFTSANITY,Boarding House and Bus Stop Extension
+10611,T-Rex Skeleton M Recipe,progression,CRAFTSANITY,Boarding House and Bus Stop Extension
+10612,T-Rex Skeleton R Recipe,progression,CRAFTSANITY,Boarding House and Bus Stop Extension
+10701,Resource Pack: 3 Magic Elixir,filler,RESOURCE_PACK,Magic
+10702,Resource Pack: 3 Travel Core,filler,RESOURCE_PACK,Magic
+10703,Preservation Chamber,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Archaeology
+10704,Hardwood Preservation Chamber,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Archaeology
+10705,Resource Pack: 3 Water Shifter,filler,RESOURCE_PACK,Archaeology
+10706,Resource Pack: 5 Hardwood Display,filler,RESOURCE_PACK,Archaeology
+10707,Resource Pack: 5 Wooden Display,filler,RESOURCE_PACK,Archaeology
+10708,Grinder,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Archaeology
+10709,Ancient Battery Production Station,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Archaeology
+10710,Hero Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded
+10711,Aegis Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded
+10712,Haste Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded
+10713,Lightning Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded
+10714,Armor Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded
+10715,Gravity Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded
+10716,Barbarian Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded
+10717,Restoration Table,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Archaeology
+10718,Trash Bin,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill
+10719,Composter,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill
+10720,Recycling Bin,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill
+10721,Advanced Recycling Machine,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill
diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv
index ef56bf5a12ba..608b6a5f576a 100644
--- a/worlds/stardew_valley/data/locations.csv
+++ b/worlds/stardew_valley/data/locations.csv
@@ -1,1291 +1,3288 @@
-id,region,name,tags,mod_name
-1,Crafts Room,Spring Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY",
-2,Crafts Room,Summer Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY",
-3,Crafts Room,Fall Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY",
-4,Crafts Room,Winter Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY",
-5,Crafts Room,Construction Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY",
-6,Crafts Room,Exotic Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY",
-7,Pantry,Spring Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE",
-8,Pantry,Summer Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE",
-9,Pantry,Fall Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE",
-10,Pantry,Quality Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE",
-11,Pantry,Animal Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE",
-12,Pantry,Artisan Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE",
-13,Fish Tank,River Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY",
-14,Fish Tank,Lake Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY",
-15,Fish Tank,Ocean Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY",
-16,Fish Tank,Night Fishing Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY",
-17,Fish Tank,Crab Pot Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY",
-18,Fish Tank,Specialty Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY",
-19,Boiler Room,Blacksmith's Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY",
-20,Boiler Room,Geologist's Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY",
-21,Boiler Room,Adventurer's Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY",
-22,Bulletin Board,Chef's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY",
-23,Bulletin Board,Dye Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY",
-24,Bulletin Board,Field Research Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY",
-25,Bulletin Board,Fodder Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY",
-26,Bulletin Board,Enchanter's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY",
-27,Vault,"2,500g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,VAULT_BUNDLE",
-28,Vault,"5,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,VAULT_BUNDLE",
-29,Vault,"10,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,VAULT_BUNDLE",
-30,Vault,"25,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,VAULT_BUNDLE",
-31,Abandoned JojaMart,The Missing Bundle,BUNDLE,
-32,Crafts Room,Complete Crafts Room,"COMMUNITY_CENTER_ROOM,MANDATORY",
-33,Pantry,Complete Pantry,"COMMUNITY_CENTER_ROOM,MANDATORY",
-34,Fish Tank,Complete Fish Tank,"COMMUNITY_CENTER_ROOM,MANDATORY",
-35,Boiler Room,Complete Boiler Room,"COMMUNITY_CENTER_ROOM,MANDATORY",
-36,Bulletin Board,Complete Bulletin Board,"COMMUNITY_CENTER_ROOM,MANDATORY",
-37,Vault,Complete Vault,"COMMUNITY_CENTER_ROOM,MANDATORY",
-101,Pierre's General Store,Large Pack,BACKPACK,
-102,Pierre's General Store,Deluxe Pack,BACKPACK,
-103,Clint's Blacksmith,Copper Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE",
-104,Clint's Blacksmith,Iron Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE",
-105,Clint's Blacksmith,Gold Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE",
-106,Clint's Blacksmith,Iridium Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE",
-107,Clint's Blacksmith,Copper Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE",
-108,Clint's Blacksmith,Iron Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE",
-109,Clint's Blacksmith,Gold Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE",
-110,Clint's Blacksmith,Iridium Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE",
-111,Clint's Blacksmith,Copper Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE",
-112,Clint's Blacksmith,Iron Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE",
-113,Clint's Blacksmith,Gold Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE",
-114,Clint's Blacksmith,Iridium Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE",
-115,Clint's Blacksmith,Copper Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE",
-116,Clint's Blacksmith,Iron Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE",
-117,Clint's Blacksmith,Gold Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE",
-118,Clint's Blacksmith,Iridium Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE",
-119,Clint's Blacksmith,Copper Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE",
-120,Clint's Blacksmith,Iron Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE",
-121,Clint's Blacksmith,Gold Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE",
-122,Clint's Blacksmith,Iridium Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE",
-123,Willy's Fish Shop,Purchase Training Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE",
-124,Beach,Bamboo Pole Cutscene,"FISHING_ROD_UPGRADE,TOOL_UPGRADE",
-125,Willy's Fish Shop,Purchase Fiberglass Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE",
-126,Willy's Fish Shop,Purchase Iridium Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE",
-201,The Mines - Floor 10,The Mines Floor 10 Treasure,"MANDATORY,THE_MINES_TREASURE",
-202,The Mines - Floor 20,The Mines Floor 20 Treasure,"MANDATORY,THE_MINES_TREASURE",
-203,The Mines - Floor 40,The Mines Floor 40 Treasure,"MANDATORY,THE_MINES_TREASURE",
-204,The Mines - Floor 50,The Mines Floor 50 Treasure,"MANDATORY,THE_MINES_TREASURE",
-205,The Mines - Floor 60,The Mines Floor 60 Treasure,"MANDATORY,THE_MINES_TREASURE",
-206,The Mines - Floor 70,The Mines Floor 70 Treasure,"MANDATORY,THE_MINES_TREASURE",
-207,The Mines - Floor 80,The Mines Floor 80 Treasure,"MANDATORY,THE_MINES_TREASURE",
-208,The Mines - Floor 90,The Mines Floor 90 Treasure,"MANDATORY,THE_MINES_TREASURE",
-209,The Mines - Floor 100,The Mines Floor 100 Treasure,"MANDATORY,THE_MINES_TREASURE",
-210,The Mines - Floor 110,The Mines Floor 110 Treasure,"MANDATORY,THE_MINES_TREASURE",
-211,The Mines - Floor 120,The Mines Floor 120 Treasure,"MANDATORY,THE_MINES_TREASURE",
-212,Quarry Mine,Grim Reaper statue,MANDATORY,
-213,The Mines,The Mines Entrance Cutscene,MANDATORY,
-214,The Mines - Floor 5,Floor 5 Elevator,ELEVATOR,
-215,The Mines - Floor 10,Floor 10 Elevator,ELEVATOR,
-216,The Mines - Floor 15,Floor 15 Elevator,ELEVATOR,
-217,The Mines - Floor 20,Floor 20 Elevator,ELEVATOR,
-218,The Mines - Floor 25,Floor 25 Elevator,ELEVATOR,
-219,The Mines - Floor 30,Floor 30 Elevator,ELEVATOR,
-220,The Mines - Floor 35,Floor 35 Elevator,ELEVATOR,
-221,The Mines - Floor 40,Floor 40 Elevator,ELEVATOR,
-222,The Mines - Floor 45,Floor 45 Elevator,ELEVATOR,
-223,The Mines - Floor 50,Floor 50 Elevator,ELEVATOR,
-224,The Mines - Floor 55,Floor 55 Elevator,ELEVATOR,
-225,The Mines - Floor 60,Floor 60 Elevator,ELEVATOR,
-226,The Mines - Floor 65,Floor 65 Elevator,ELEVATOR,
-227,The Mines - Floor 70,Floor 70 Elevator,ELEVATOR,
-228,The Mines - Floor 75,Floor 75 Elevator,ELEVATOR,
-229,The Mines - Floor 80,Floor 80 Elevator,ELEVATOR,
-230,The Mines - Floor 85,Floor 85 Elevator,ELEVATOR,
-231,The Mines - Floor 90,Floor 90 Elevator,ELEVATOR,
-232,The Mines - Floor 95,Floor 95 Elevator,ELEVATOR,
-233,The Mines - Floor 100,Floor 100 Elevator,ELEVATOR,
-234,The Mines - Floor 105,Floor 105 Elevator,ELEVATOR,
-235,The Mines - Floor 110,Floor 110 Elevator,ELEVATOR,
-236,The Mines - Floor 115,Floor 115 Elevator,ELEVATOR,
-237,The Mines - Floor 120,Floor 120 Elevator,ELEVATOR,
-251,Volcano - Floor 10,Volcano Caldera Treasure,"MANDATORY,GINGER_ISLAND",
-301,Stardew Valley,Level 1 Farming,"FARMING_LEVEL,SKILL_LEVEL",
-302,Stardew Valley,Level 2 Farming,"FARMING_LEVEL,SKILL_LEVEL",
-303,Stardew Valley,Level 3 Farming,"FARMING_LEVEL,SKILL_LEVEL",
-304,Stardew Valley,Level 4 Farming,"FARMING_LEVEL,SKILL_LEVEL",
-305,Stardew Valley,Level 5 Farming,"FARMING_LEVEL,SKILL_LEVEL",
-306,Stardew Valley,Level 6 Farming,"FARMING_LEVEL,SKILL_LEVEL",
-307,Stardew Valley,Level 7 Farming,"FARMING_LEVEL,SKILL_LEVEL",
-308,Stardew Valley,Level 8 Farming,"FARMING_LEVEL,SKILL_LEVEL",
-309,Stardew Valley,Level 9 Farming,"FARMING_LEVEL,SKILL_LEVEL",
-310,Stardew Valley,Level 10 Farming,"FARMING_LEVEL,SKILL_LEVEL",
-311,Stardew Valley,Level 1 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
-312,Stardew Valley,Level 2 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
-313,Stardew Valley,Level 3 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
-314,Stardew Valley,Level 4 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
-315,Stardew Valley,Level 5 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
-316,Stardew Valley,Level 6 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
-317,Stardew Valley,Level 7 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
-318,Stardew Valley,Level 8 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
-319,Stardew Valley,Level 9 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
-320,Stardew Valley,Level 10 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
-321,Stardew Valley,Level 1 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
-322,Stardew Valley,Level 2 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
-323,Stardew Valley,Level 3 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
-324,Stardew Valley,Level 4 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
-325,Stardew Valley,Level 5 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
-326,Stardew Valley,Level 6 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
-327,Stardew Valley,Level 7 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
-328,Stardew Valley,Level 8 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
-329,Stardew Valley,Level 9 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
-330,Stardew Valley,Level 10 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
-331,Stardew Valley,Level 1 Mining,"MINING_LEVEL,SKILL_LEVEL",
-332,Stardew Valley,Level 2 Mining,"MINING_LEVEL,SKILL_LEVEL",
-333,Stardew Valley,Level 3 Mining,"MINING_LEVEL,SKILL_LEVEL",
-334,Stardew Valley,Level 4 Mining,"MINING_LEVEL,SKILL_LEVEL",
-335,Stardew Valley,Level 5 Mining,"MINING_LEVEL,SKILL_LEVEL",
-336,Stardew Valley,Level 6 Mining,"MINING_LEVEL,SKILL_LEVEL",
-337,Stardew Valley,Level 7 Mining,"MINING_LEVEL,SKILL_LEVEL",
-338,Stardew Valley,Level 8 Mining,"MINING_LEVEL,SKILL_LEVEL",
-339,Stardew Valley,Level 9 Mining,"MINING_LEVEL,SKILL_LEVEL",
-340,Stardew Valley,Level 10 Mining,"MINING_LEVEL,SKILL_LEVEL",
-341,Stardew Valley,Level 1 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
-342,Stardew Valley,Level 2 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
-343,Stardew Valley,Level 3 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
-344,Stardew Valley,Level 4 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
-345,Stardew Valley,Level 5 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
-346,Stardew Valley,Level 6 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
-347,Stardew Valley,Level 7 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
-348,Stardew Valley,Level 8 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
-349,Stardew Valley,Level 9 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
-350,Stardew Valley,Level 10 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
-401,Carpenter Shop,Coop Blueprint,BUILDING_BLUEPRINT,
-402,Carpenter Shop,Big Coop Blueprint,BUILDING_BLUEPRINT,
-403,Carpenter Shop,Deluxe Coop Blueprint,BUILDING_BLUEPRINT,
-404,Carpenter Shop,Barn Blueprint,BUILDING_BLUEPRINT,
-405,Carpenter Shop,Big Barn Blueprint,BUILDING_BLUEPRINT,
-406,Carpenter Shop,Deluxe Barn Blueprint,BUILDING_BLUEPRINT,
-407,Carpenter Shop,Well Blueprint,BUILDING_BLUEPRINT,
-408,Carpenter Shop,Silo Blueprint,BUILDING_BLUEPRINT,
-409,Carpenter Shop,Mill Blueprint,BUILDING_BLUEPRINT,
-410,Carpenter Shop,Shed Blueprint,BUILDING_BLUEPRINT,
-411,Carpenter Shop,Big Shed Blueprint,BUILDING_BLUEPRINT,
-412,Carpenter Shop,Fish Pond Blueprint,BUILDING_BLUEPRINT,
-413,Carpenter Shop,Stable Blueprint,BUILDING_BLUEPRINT,
-414,Carpenter Shop,Slime Hutch Blueprint,BUILDING_BLUEPRINT,
-415,Carpenter Shop,Shipping Bin Blueprint,BUILDING_BLUEPRINT,
-416,Carpenter Shop,Kitchen Blueprint,BUILDING_BLUEPRINT,
-417,Carpenter Shop,Kids Room Blueprint,BUILDING_BLUEPRINT,
-418,Carpenter Shop,Cellar Blueprint,BUILDING_BLUEPRINT,
-501,Town,Introductions,"MANDATORY,QUEST",
-502,Town,How To Win Friends,"MANDATORY,QUEST",
-503,Farm,Getting Started,"MANDATORY,QUEST",
-504,Farm,Raising Animals,"MANDATORY,QUEST",
-505,Farm,Advancement,"MANDATORY,QUEST",
-506,Museum,Archaeology,"MANDATORY,QUEST",
-507,Wizard Tower,Meet The Wizard,"MANDATORY,QUEST",
-508,Farm,Forging Ahead,"MANDATORY,QUEST",
-509,Farm,Smelting,"MANDATORY,QUEST",
-510,The Mines - Floor 5,Initiation,"MANDATORY,QUEST",
-511,Forest,Robin's Lost Axe,"MANDATORY,QUEST",
-512,Sam's House,Jodi's Request,"MANDATORY,QUEST",
-513,Marnie's Ranch,"Mayor's ""Shorts""","MANDATORY,QUEST",
-514,Tunnel Entrance,Blackberry Basket,"MANDATORY,QUEST",
-515,Marnie's Ranch,Marnie's Request,"MANDATORY,QUEST",
-516,Town,Pam Is Thirsty,"MANDATORY,QUEST",
-517,Wizard Tower,A Dark Reagent,"MANDATORY,QUEST",
-518,Marnie's Ranch,Cow's Delight,"MANDATORY,QUEST",
-519,Skull Cavern Entrance,The Skull Key,"MANDATORY,QUEST",
-520,Town,Crop Research,"MANDATORY,QUEST",
-521,Town,Knee Therapy,"MANDATORY,QUEST",
-522,Town,Robin's Request,"MANDATORY,QUEST",
-523,Skull Cavern,Qi's Challenge,"MANDATORY,QUEST",
-524,Desert,The Mysterious Qi,"MANDATORY,QUEST",
-525,Town,Carving Pumpkins,"MANDATORY,QUEST",
-526,Town,A Winter Mystery,"MANDATORY,QUEST",
-527,Secret Woods,Strange Note,"MANDATORY,QUEST",
-528,Skull Cavern,Cryptic Note,"MANDATORY,QUEST",
-529,Town,Fresh Fruit,"MANDATORY,QUEST",
-530,Town,Aquatic Research,"MANDATORY,QUEST",
-531,Town,A Soldier's Star,"MANDATORY,QUEST",
-532,Town,Mayor's Need,"MANDATORY,QUEST",
-533,Saloon,Wanted: Lobster,"MANDATORY,QUEST",
-534,Town,Pam Needs Juice,"MANDATORY,QUEST",
-535,Sam's House,Fish Casserole,"MANDATORY,QUEST",
-536,Beach,Catch A Squid,"MANDATORY,QUEST",
-537,Saloon,Fish Stew,"MANDATORY,QUEST",
-538,Town,Pierre's Notice,"MANDATORY,QUEST",
-539,Town,Clint's Attempt,"MANDATORY,QUEST",
-540,Town,A Favor For Clint,"MANDATORY,QUEST",
-541,Wizard Tower,Staff Of Power,"MANDATORY,QUEST",
-542,Town,Granny's Gift,"MANDATORY,QUEST",
-543,Saloon,Exotic Spirits,"MANDATORY,QUEST",
-544,Town,Catch a Lingcod,"MANDATORY,QUEST",
-545,Island West,The Pirate's Wife,"GINGER_ISLAND,MANDATORY,QUEST",
-546,Railroad,Dark Talisman,"MANDATORY,QUEST",
-547,Witch's Swamp,Goblin Problem,"MANDATORY,QUEST",
-548,Witch's Hut,Magic Ink,"MANDATORY,QUEST",
-601,JotPK World 1,JotPK: Boots 1,"ARCADE_MACHINE,JOTPK",
-602,JotPK World 1,JotPK: Boots 2,"ARCADE_MACHINE,JOTPK",
-603,JotPK World 1,JotPK: Gun 1,"ARCADE_MACHINE,JOTPK",
-604,JotPK World 2,JotPK: Gun 2,"ARCADE_MACHINE,JOTPK",
-605,JotPK World 2,JotPK: Gun 3,"ARCADE_MACHINE,JOTPK",
-606,JotPK World 3,JotPK: Super Gun,"ARCADE_MACHINE,JOTPK",
-607,JotPK World 1,JotPK: Ammo 1,"ARCADE_MACHINE,JOTPK",
-608,JotPK World 2,JotPK: Ammo 2,"ARCADE_MACHINE,JOTPK",
-609,JotPK World 3,JotPK: Ammo 3,"ARCADE_MACHINE,JOTPK",
-610,JotPK World 1,JotPK: Cowboy 1,"ARCADE_MACHINE,JOTPK",
-611,JotPK World 2,JotPK: Cowboy 2,"ARCADE_MACHINE,JOTPK",
-612,Junimo Kart 1,Junimo Kart: Crumble Cavern,"ARCADE_MACHINE,JUNIMO_KART",
-613,Junimo Kart 1,Junimo Kart: Slippery Slopes,"ARCADE_MACHINE,JUNIMO_KART",
-614,Junimo Kart 2,Junimo Kart: Secret Level,"ARCADE_MACHINE,JUNIMO_KART",
-615,Junimo Kart 2,Junimo Kart: The Gem Sea Giant,"ARCADE_MACHINE,JUNIMO_KART",
-616,Junimo Kart 2,Junimo Kart: Slomp's Stomp,"ARCADE_MACHINE,JUNIMO_KART",
-617,Junimo Kart 2,Junimo Kart: Ghastly Galleon,"ARCADE_MACHINE,JUNIMO_KART",
-618,Junimo Kart 3,Junimo Kart: Glowshroom Grotto,"ARCADE_MACHINE,JUNIMO_KART",
-619,Junimo Kart 3,Junimo Kart: Red Hot Rollercoaster,"ARCADE_MACHINE,JUNIMO_KART",
-620,JotPK World 3,Journey of the Prairie King Victory,"ARCADE_MACHINE_VICTORY,JOTPK",
-621,Junimo Kart 3,Junimo Kart: Sunset Speedway (Victory),"ARCADE_MACHINE_VICTORY,JUNIMO_KART",
-701,Secret Woods,Old Master Cannoli,MANDATORY,
-702,Beach,Beach Bridge Repair,MANDATORY,
-703,Desert,Galaxy Sword Shrine,MANDATORY,
-704,Farmhouse,Have a Baby,MANDATORY,
-705,Farmhouse,Have Another Baby,MANDATORY,
-801,Town,Help Wanted: Gathering 1,HELP_WANTED,
-802,Town,Help Wanted: Gathering 2,HELP_WANTED,
-803,Town,Help Wanted: Gathering 3,HELP_WANTED,
-804,Town,Help Wanted: Gathering 4,HELP_WANTED,
-805,Town,Help Wanted: Gathering 5,HELP_WANTED,
-806,Town,Help Wanted: Gathering 6,HELP_WANTED,
-807,Town,Help Wanted: Gathering 7,HELP_WANTED,
-808,Town,Help Wanted: Gathering 8,HELP_WANTED,
-811,Town,Help Wanted: Slay Monsters 1,HELP_WANTED,
-812,Town,Help Wanted: Slay Monsters 2,HELP_WANTED,
-813,Town,Help Wanted: Slay Monsters 3,HELP_WANTED,
-814,Town,Help Wanted: Slay Monsters 4,HELP_WANTED,
-815,Town,Help Wanted: Slay Monsters 5,HELP_WANTED,
-816,Town,Help Wanted: Slay Monsters 6,HELP_WANTED,
-817,Town,Help Wanted: Slay Monsters 7,HELP_WANTED,
-818,Town,Help Wanted: Slay Monsters 8,HELP_WANTED,
-821,Town,Help Wanted: Fishing 1,HELP_WANTED,
-822,Town,Help Wanted: Fishing 2,HELP_WANTED,
-823,Town,Help Wanted: Fishing 3,HELP_WANTED,
-824,Town,Help Wanted: Fishing 4,HELP_WANTED,
-825,Town,Help Wanted: Fishing 5,HELP_WANTED,
-826,Town,Help Wanted: Fishing 6,HELP_WANTED,
-827,Town,Help Wanted: Fishing 7,HELP_WANTED,
-828,Town,Help Wanted: Fishing 8,HELP_WANTED,
-841,Town,Help Wanted: Item Delivery 1,HELP_WANTED,
-842,Town,Help Wanted: Item Delivery 2,HELP_WANTED,
-843,Town,Help Wanted: Item Delivery 3,HELP_WANTED,
-844,Town,Help Wanted: Item Delivery 4,HELP_WANTED,
-845,Town,Help Wanted: Item Delivery 5,HELP_WANTED,
-846,Town,Help Wanted: Item Delivery 6,HELP_WANTED,
-847,Town,Help Wanted: Item Delivery 7,HELP_WANTED,
-848,Town,Help Wanted: Item Delivery 8,HELP_WANTED,
-849,Town,Help Wanted: Item Delivery 9,HELP_WANTED,
-850,Town,Help Wanted: Item Delivery 10,HELP_WANTED,
-851,Town,Help Wanted: Item Delivery 11,HELP_WANTED,
-852,Town,Help Wanted: Item Delivery 12,HELP_WANTED,
-853,Town,Help Wanted: Item Delivery 13,HELP_WANTED,
-854,Town,Help Wanted: Item Delivery 14,HELP_WANTED,
-855,Town,Help Wanted: Item Delivery 15,HELP_WANTED,
-856,Town,Help Wanted: Item Delivery 16,HELP_WANTED,
-857,Town,Help Wanted: Item Delivery 17,HELP_WANTED,
-858,Town,Help Wanted: Item Delivery 18,HELP_WANTED,
-859,Town,Help Wanted: Item Delivery 19,HELP_WANTED,
-860,Town,Help Wanted: Item Delivery 20,HELP_WANTED,
-861,Town,Help Wanted: Item Delivery 21,HELP_WANTED,
-862,Town,Help Wanted: Item Delivery 22,HELP_WANTED,
-863,Town,Help Wanted: Item Delivery 23,HELP_WANTED,
-864,Town,Help Wanted: Item Delivery 24,HELP_WANTED,
-865,Town,Help Wanted: Item Delivery 25,HELP_WANTED,
-866,Town,Help Wanted: Item Delivery 26,HELP_WANTED,
-867,Town,Help Wanted: Item Delivery 27,HELP_WANTED,
-868,Town,Help Wanted: Item Delivery 28,HELP_WANTED,
-869,Town,Help Wanted: Item Delivery 29,HELP_WANTED,
-870,Town,Help Wanted: Item Delivery 30,HELP_WANTED,
-871,Town,Help Wanted: Item Delivery 31,HELP_WANTED,
-872,Town,Help Wanted: Item Delivery 32,HELP_WANTED,
-901,Forest,Traveling Merchant Sunday Item 1,"MANDATORY,TRAVELING_MERCHANT",
-902,Forest,Traveling Merchant Sunday Item 2,"MANDATORY,TRAVELING_MERCHANT",
-903,Forest,Traveling Merchant Sunday Item 3,"MANDATORY,TRAVELING_MERCHANT",
-911,Forest,Traveling Merchant Monday Item 1,"MANDATORY,TRAVELING_MERCHANT",
-912,Forest,Traveling Merchant Monday Item 2,"MANDATORY,TRAVELING_MERCHANT",
-913,Forest,Traveling Merchant Monday Item 3,"MANDATORY,TRAVELING_MERCHANT",
-921,Forest,Traveling Merchant Tuesday Item 1,"MANDATORY,TRAVELING_MERCHANT",
-922,Forest,Traveling Merchant Tuesday Item 2,"MANDATORY,TRAVELING_MERCHANT",
-923,Forest,Traveling Merchant Tuesday Item 3,"MANDATORY,TRAVELING_MERCHANT",
-931,Forest,Traveling Merchant Wednesday Item 1,"MANDATORY,TRAVELING_MERCHANT",
-932,Forest,Traveling Merchant Wednesday Item 2,"MANDATORY,TRAVELING_MERCHANT",
-933,Forest,Traveling Merchant Wednesday Item 3,"MANDATORY,TRAVELING_MERCHANT",
-941,Forest,Traveling Merchant Thursday Item 1,"MANDATORY,TRAVELING_MERCHANT",
-942,Forest,Traveling Merchant Thursday Item 2,"MANDATORY,TRAVELING_MERCHANT",
-943,Forest,Traveling Merchant Thursday Item 3,"MANDATORY,TRAVELING_MERCHANT",
-951,Forest,Traveling Merchant Friday Item 1,"MANDATORY,TRAVELING_MERCHANT",
-952,Forest,Traveling Merchant Friday Item 2,"MANDATORY,TRAVELING_MERCHANT",
-953,Forest,Traveling Merchant Friday Item 3,"MANDATORY,TRAVELING_MERCHANT",
-961,Forest,Traveling Merchant Saturday Item 1,"MANDATORY,TRAVELING_MERCHANT",
-962,Forest,Traveling Merchant Saturday Item 2,"MANDATORY,TRAVELING_MERCHANT",
-963,Forest,Traveling Merchant Saturday Item 3,"MANDATORY,TRAVELING_MERCHANT",
-1001,Mountain,Fishsanity: Carp,FISHSANITY,
-1002,Beach,Fishsanity: Herring,FISHSANITY,
-1003,Forest,Fishsanity: Smallmouth Bass,FISHSANITY,
-1004,Beach,Fishsanity: Anchovy,FISHSANITY,
-1005,Beach,Fishsanity: Sardine,FISHSANITY,
-1006,Forest,Fishsanity: Sunfish,FISHSANITY,
-1007,Forest,Fishsanity: Perch,FISHSANITY,
-1008,Forest,Fishsanity: Chub,FISHSANITY,
-1009,Forest,Fishsanity: Bream,FISHSANITY,
-1010,Beach,Fishsanity: Red Snapper,FISHSANITY,
-1011,Beach,Fishsanity: Sea Cucumber,FISHSANITY,
-1012,Forest,Fishsanity: Rainbow Trout,FISHSANITY,
-1013,Forest,Fishsanity: Walleye,FISHSANITY,
-1014,Forest,Fishsanity: Shad,FISHSANITY,
-1015,Mountain,Fishsanity: Bullhead,FISHSANITY,
-1016,Mountain,Fishsanity: Largemouth Bass,FISHSANITY,
-1017,Forest,Fishsanity: Salmon,FISHSANITY,
-1018,The Mines - Floor 20,Fishsanity: Ghostfish,FISHSANITY,
-1019,Beach,Fishsanity: Tilapia,FISHSANITY,
-1020,Secret Woods,Fishsanity: Woodskip,FISHSANITY,
-1021,Beach,Fishsanity: Flounder,FISHSANITY,
-1022,Beach,Fishsanity: Halibut,FISHSANITY,
-1023,Island West,Fishsanity: Lionfish,"FISHSANITY,GINGER_ISLAND",
-1024,Mutant Bug Lair,Fishsanity: Slimejack,FISHSANITY,
-1025,Forest,Fishsanity: Midnight Carp,FISHSANITY,
-1026,Beach,Fishsanity: Red Mullet,FISHSANITY,
-1027,Forest,Fishsanity: Pike,FISHSANITY,
-1028,Forest,Fishsanity: Tiger Trout,FISHSANITY,
-1029,Island West,Fishsanity: Blue Discus,"FISHSANITY,GINGER_ISLAND",
-1030,Beach,Fishsanity: Albacore,FISHSANITY,
-1031,Desert,Fishsanity: Sandfish,FISHSANITY,
-1032,The Mines - Floor 20,Fishsanity: Stonefish,FISHSANITY,
-1033,Beach,Fishsanity: Tuna,FISHSANITY,
-1034,Beach,Fishsanity: Eel,FISHSANITY,
-1035,Forest,Fishsanity: Catfish,FISHSANITY,
-1036,Beach,Fishsanity: Squid,FISHSANITY,
-1037,Mountain,Fishsanity: Sturgeon,FISHSANITY,
-1038,Forest,Fishsanity: Dorado,FISHSANITY,
-1039,Beach,Fishsanity: Pufferfish,FISHSANITY,
-1040,Witch's Swamp,Fishsanity: Void Salmon,FISHSANITY,
-1041,Beach,Fishsanity: Super Cucumber,FISHSANITY,
-1042,Pirate Cove,Fishsanity: Stingray,"FISHSANITY,GINGER_ISLAND",
-1043,The Mines - Floor 60,Fishsanity: Ice Pip,FISHSANITY,
-1044,Forest,Fishsanity: Lingcod,FISHSANITY,
-1045,Desert,Fishsanity: Scorpion Carp,FISHSANITY,
-1046,The Mines - Floor 100,Fishsanity: Lava Eel,FISHSANITY,
-1047,Beach,Fishsanity: Octopus,FISHSANITY,
-1048,Beach,Fishsanity: Midnight Squid,FISHSANITY,
-1049,Beach,Fishsanity: Spook Fish,FISHSANITY,
-1050,Beach,Fishsanity: Blobfish,FISHSANITY,
-1051,Beach,Fishsanity: Crimsonfish,FISHSANITY,
-1052,Town,Fishsanity: Angler,FISHSANITY,
-1053,Mountain,Fishsanity: Legend,FISHSANITY,
-1054,Forest,Fishsanity: Glacierfish,FISHSANITY,
-1055,Sewer,Fishsanity: Mutant Carp,FISHSANITY,
-1056,Town,Fishsanity: Crayfish,FISHSANITY,
-1057,Town,Fishsanity: Snail,FISHSANITY,
-1058,Town,Fishsanity: Periwinkle,FISHSANITY,
-1059,Beach,Fishsanity: Lobster,FISHSANITY,
-1060,Beach,Fishsanity: Clam,FISHSANITY,
-1061,Beach,Fishsanity: Crab,FISHSANITY,
-1062,Beach,Fishsanity: Cockle,FISHSANITY,
-1063,Beach,Fishsanity: Mussel,FISHSANITY,
-1064,Beach,Fishsanity: Shrimp,FISHSANITY,
-1065,Beach,Fishsanity: Oyster,FISHSANITY,
-1100,Stardew Valley,Museumsanity: 5 Donations,MUSEUM_MILESTONES,
-1101,Stardew Valley,Museumsanity: 10 Donations,MUSEUM_MILESTONES,
-1102,Stardew Valley,Museumsanity: 15 Donations,MUSEUM_MILESTONES,
-1103,Stardew Valley,Museumsanity: 20 Donations,MUSEUM_MILESTONES,
-1104,Stardew Valley,Museumsanity: 25 Donations,MUSEUM_MILESTONES,
-1105,Stardew Valley,Museumsanity: 30 Donations,MUSEUM_MILESTONES,
-1106,Stardew Valley,Museumsanity: 35 Donations,MUSEUM_MILESTONES,
-1107,Stardew Valley,Museumsanity: 40 Donations,MUSEUM_MILESTONES,
-1108,Stardew Valley,Museumsanity: 50 Donations,MUSEUM_MILESTONES,
-1109,Stardew Valley,Museumsanity: 60 Donations,MUSEUM_MILESTONES,
-1110,Stardew Valley,Museumsanity: 70 Donations,MUSEUM_MILESTONES,
-1111,Stardew Valley,Museumsanity: 80 Donations,MUSEUM_MILESTONES,
-1112,Stardew Valley,Museumsanity: 90 Donations,MUSEUM_MILESTONES,
-1113,Stardew Valley,Museumsanity: 95 Donations,MUSEUM_MILESTONES,
-1114,Stardew Valley,Museumsanity: 11 Minerals,MUSEUM_MILESTONES,
-1115,Stardew Valley,Museumsanity: 21 Minerals,MUSEUM_MILESTONES,
-1116,Stardew Valley,Museumsanity: 31 Minerals,MUSEUM_MILESTONES,
-1117,Stardew Valley,Museumsanity: 41 Minerals,MUSEUM_MILESTONES,
-1118,Stardew Valley,Museumsanity: 50 Minerals,MUSEUM_MILESTONES,
-1119,Stardew Valley,Museumsanity: 3 Artifacts,MUSEUM_MILESTONES,
-1120,Stardew Valley,Museumsanity: 6 Artifacts,MUSEUM_MILESTONES,
-1121,Stardew Valley,Museumsanity: 9 Artifacts,MUSEUM_MILESTONES,
-1122,Stardew Valley,Museumsanity: 11 Artifacts,MUSEUM_MILESTONES,
-1123,Stardew Valley,Museumsanity: 15 Artifacts,MUSEUM_MILESTONES,
-1124,Stardew Valley,Museumsanity: 20 Artifacts,MUSEUM_MILESTONES,
-1125,Stardew Valley,Museumsanity: Dwarf Scrolls,MUSEUM_MILESTONES,
-1126,Stardew Valley,Museumsanity: Skeleton Front,MUSEUM_MILESTONES,
-1127,Stardew Valley,Museumsanity: Skeleton Middle,MUSEUM_MILESTONES,
-1128,Stardew Valley,Museumsanity: Skeleton Back,MUSEUM_MILESTONES,
-1201,The Mines - Floor 20,Museumsanity: Dwarf Scroll I,MUSEUM_DONATIONS,
-1202,The Mines - Floor 20,Museumsanity: Dwarf Scroll II,MUSEUM_DONATIONS,
-1203,The Mines - Floor 60,Museumsanity: Dwarf Scroll III,MUSEUM_DONATIONS,
-1204,The Mines - Floor 100,Museumsanity: Dwarf Scroll IV,MUSEUM_DONATIONS,
-1205,Town,Museumsanity: Chipped Amphora,MUSEUM_DONATIONS,
-1206,Forest,Museumsanity: Arrowhead,MUSEUM_DONATIONS,
-1207,Forest,Museumsanity: Ancient Doll,MUSEUM_DONATIONS,
-1208,Forest,Museumsanity: Elvish Jewelry,MUSEUM_DONATIONS,
-1209,Forest,Museumsanity: Chewing Stick,MUSEUM_DONATIONS,
-1210,Forest,Museumsanity: Ornamental Fan,MUSEUM_DONATIONS,
-1211,Mountain,Museumsanity: Dinosaur Egg,MUSEUM_DONATIONS,
-1212,Stardew Valley,Museumsanity: Rare Disc,MUSEUM_DONATIONS,
-1213,Forest,Museumsanity: Ancient Sword,MUSEUM_DONATIONS,
-1214,Town,Museumsanity: Rusty Spoon,MUSEUM_DONATIONS,
-1215,Farm,Museumsanity: Rusty Spur,MUSEUM_DONATIONS,
-1216,Mountain,Museumsanity: Rusty Cog,MUSEUM_DONATIONS,
-1217,Farm,Museumsanity: Chicken Statue,MUSEUM_DONATIONS,
-1218,Forest,Museumsanity: Ancient Seed,"MUSEUM_DONATIONS,MUSEUM_MILESTONES",
-1219,Forest,Museumsanity: Prehistoric Tool,MUSEUM_DONATIONS,
-1220,Beach,Museumsanity: Dried Starfish,MUSEUM_DONATIONS,
-1221,Beach,Museumsanity: Anchor,MUSEUM_DONATIONS,
-1222,Beach,Museumsanity: Glass Shards,MUSEUM_DONATIONS,
-1223,Forest,Museumsanity: Bone Flute,MUSEUM_DONATIONS,
-1224,Forest,Museumsanity: Prehistoric Handaxe,MUSEUM_DONATIONS,
-1225,The Mines - Floor 20,Museumsanity: Dwarvish Helm,MUSEUM_DONATIONS,
-1226,The Mines - Floor 60,Museumsanity: Dwarf Gadget,MUSEUM_DONATIONS,
-1227,Forest,Museumsanity: Ancient Drum,MUSEUM_DONATIONS,
-1228,Desert,Museumsanity: Golden Mask,MUSEUM_DONATIONS,
-1229,Desert,Museumsanity: Golden Relic,MUSEUM_DONATIONS,
-1230,Town,Museumsanity: Strange Doll (Green),MUSEUM_DONATIONS,
-1231,Desert,Museumsanity: Strange Doll,MUSEUM_DONATIONS,
-1232,Forest,Museumsanity: Prehistoric Scapula,MUSEUM_DONATIONS,
-1233,Forest,Museumsanity: Prehistoric Tibia,MUSEUM_DONATIONS,
-1234,Dig Site,Museumsanity: Prehistoric Skull,MUSEUM_DONATIONS,
-1235,Dig Site,Museumsanity: Skeletal Hand,MUSEUM_DONATIONS,
-1236,Dig Site,Museumsanity: Prehistoric Rib,MUSEUM_DONATIONS,
-1237,Dig Site,Museumsanity: Prehistoric Vertebra,MUSEUM_DONATIONS,
-1238,Dig Site,Museumsanity: Skeletal Tail,MUSEUM_DONATIONS,
-1239,Dig Site,Museumsanity: Nautilus Fossil,MUSEUM_DONATIONS,
-1240,Forest,Museumsanity: Amphibian Fossil,MUSEUM_DONATIONS,
-1241,Forest,Museumsanity: Palm Fossil,MUSEUM_DONATIONS,
-1242,Forest,Museumsanity: Trilobite,MUSEUM_DONATIONS,
-1243,The Mines - Floor 20,Museumsanity: Quartz,MUSEUM_DONATIONS,
-1244,The Mines - Floor 100,Museumsanity: Fire Quartz,MUSEUM_DONATIONS,
-1245,The Mines - Floor 60,Museumsanity: Frozen Tear,MUSEUM_DONATIONS,
-1246,The Mines - Floor 20,Museumsanity: Earth Crystal,MUSEUM_DONATIONS,
-1247,The Mines - Floor 100,Museumsanity: Emerald,MUSEUM_DONATIONS,
-1248,The Mines - Floor 60,Museumsanity: Aquamarine,MUSEUM_DONATIONS,
-1249,The Mines - Floor 100,Museumsanity: Ruby,MUSEUM_DONATIONS,
-1250,The Mines - Floor 20,Museumsanity: Amethyst,MUSEUM_DONATIONS,
-1251,The Mines - Floor 20,Museumsanity: Topaz,MUSEUM_DONATIONS,
-1252,The Mines - Floor 60,Museumsanity: Jade,MUSEUM_DONATIONS,
-1253,The Mines - Floor 60,Museumsanity: Diamond,MUSEUM_DONATIONS,
-1254,Skull Cavern Floor 100,Museumsanity: Prismatic Shard,MUSEUM_DONATIONS,
-1255,Town,Museumsanity: Alamite,MUSEUM_DONATIONS,
-1256,Town,Museumsanity: Bixite,MUSEUM_DONATIONS,
-1257,Town,Museumsanity: Baryte,MUSEUM_DONATIONS,
-1258,Town,Museumsanity: Aerinite,MUSEUM_DONATIONS,
-1259,Town,Museumsanity: Calcite,MUSEUM_DONATIONS,
-1260,Town,Museumsanity: Dolomite,MUSEUM_DONATIONS,
-1261,Town,Museumsanity: Esperite,MUSEUM_DONATIONS,
-1262,Town,Museumsanity: Fluorapatite,MUSEUM_DONATIONS,
-1263,Town,Museumsanity: Geminite,MUSEUM_DONATIONS,
-1264,Town,Museumsanity: Helvite,MUSEUM_DONATIONS,
-1265,Town,Museumsanity: Jamborite,MUSEUM_DONATIONS,
-1266,Town,Museumsanity: Jagoite,MUSEUM_DONATIONS,
-1267,Town,Museumsanity: Kyanite,MUSEUM_DONATIONS,
-1268,Town,Museumsanity: Lunarite,MUSEUM_DONATIONS,
-1269,Town,Museumsanity: Malachite,MUSEUM_DONATIONS,
-1270,Town,Museumsanity: Neptunite,MUSEUM_DONATIONS,
-1271,Town,Museumsanity: Lemon Stone,MUSEUM_DONATIONS,
-1272,Town,Museumsanity: Nekoite,MUSEUM_DONATIONS,
-1273,Town,Museumsanity: Orpiment,MUSEUM_DONATIONS,
-1274,Town,Museumsanity: Petrified Slime,MUSEUM_DONATIONS,
-1275,Town,Museumsanity: Thunder Egg,MUSEUM_DONATIONS,
-1276,Town,Museumsanity: Pyrite,MUSEUM_DONATIONS,
-1277,Town,Museumsanity: Ocean Stone,MUSEUM_DONATIONS,
-1278,Town,Museumsanity: Ghost Crystal,MUSEUM_DONATIONS,
-1279,Town,Museumsanity: Tigerseye,MUSEUM_DONATIONS,
-1280,Town,Museumsanity: Jasper,MUSEUM_DONATIONS,
-1281,Town,Museumsanity: Opal,MUSEUM_DONATIONS,
-1282,Town,Museumsanity: Fire Opal,MUSEUM_DONATIONS,
-1283,Town,Museumsanity: Celestine,MUSEUM_DONATIONS,
-1284,Town,Museumsanity: Marble,MUSEUM_DONATIONS,
-1285,Town,Museumsanity: Sandstone,MUSEUM_DONATIONS,
-1286,Town,Museumsanity: Granite,MUSEUM_DONATIONS,
-1287,Town,Museumsanity: Basalt,MUSEUM_DONATIONS,
-1288,Town,Museumsanity: Limestone,MUSEUM_DONATIONS,
-1289,Town,Museumsanity: Soapstone,MUSEUM_DONATIONS,
-1290,Town,Museumsanity: Hematite,MUSEUM_DONATIONS,
-1291,Town,Museumsanity: Mudstone,MUSEUM_DONATIONS,
-1292,Town,Museumsanity: Obsidian,MUSEUM_DONATIONS,
-1293,Town,Museumsanity: Slate,MUSEUM_DONATIONS,
-1294,Town,Museumsanity: Fairy Stone,MUSEUM_DONATIONS,
-1295,Town,Museumsanity: Star Shards,MUSEUM_DONATIONS,
-1301,Town,Friendsanity: Alex 1 <3,FRIENDSANITY,
-1302,Town,Friendsanity: Alex 2 <3,FRIENDSANITY,
-1303,Town,Friendsanity: Alex 3 <3,FRIENDSANITY,
-1304,Town,Friendsanity: Alex 4 <3,FRIENDSANITY,
-1305,Town,Friendsanity: Alex 5 <3,FRIENDSANITY,
-1306,Town,Friendsanity: Alex 6 <3,FRIENDSANITY,
-1307,Town,Friendsanity: Alex 7 <3,FRIENDSANITY,
-1308,Town,Friendsanity: Alex 8 <3,FRIENDSANITY,
-1309,Town,Friendsanity: Alex 9 <3,FRIENDSANITY,
-1310,Town,Friendsanity: Alex 10 <3,FRIENDSANITY,
-1311,Town,Friendsanity: Alex 11 <3,FRIENDSANITY,
-1312,Town,Friendsanity: Alex 12 <3,FRIENDSANITY,
-1313,Town,Friendsanity: Alex 13 <3,FRIENDSANITY,
-1314,Town,Friendsanity: Alex 14 <3,FRIENDSANITY,
-1315,Beach,Friendsanity: Elliott 1 <3,FRIENDSANITY,
-1316,Beach,Friendsanity: Elliott 2 <3,FRIENDSANITY,
-1317,Beach,Friendsanity: Elliott 3 <3,FRIENDSANITY,
-1318,Beach,Friendsanity: Elliott 4 <3,FRIENDSANITY,
-1319,Beach,Friendsanity: Elliott 5 <3,FRIENDSANITY,
-1320,Beach,Friendsanity: Elliott 6 <3,FRIENDSANITY,
-1321,Beach,Friendsanity: Elliott 7 <3,FRIENDSANITY,
-1322,Beach,Friendsanity: Elliott 8 <3,FRIENDSANITY,
-1323,Beach,Friendsanity: Elliott 9 <3,FRIENDSANITY,
-1324,Beach,Friendsanity: Elliott 10 <3,FRIENDSANITY,
-1325,Beach,Friendsanity: Elliott 11 <3,FRIENDSANITY,
-1326,Beach,Friendsanity: Elliott 12 <3,FRIENDSANITY,
-1327,Beach,Friendsanity: Elliott 13 <3,FRIENDSANITY,
-1328,Beach,Friendsanity: Elliott 14 <3,FRIENDSANITY,
-1329,Town,Friendsanity: Harvey 1 <3,FRIENDSANITY,
-1330,Town,Friendsanity: Harvey 2 <3,FRIENDSANITY,
-1331,Town,Friendsanity: Harvey 3 <3,FRIENDSANITY,
-1332,Town,Friendsanity: Harvey 4 <3,FRIENDSANITY,
-1333,Town,Friendsanity: Harvey 5 <3,FRIENDSANITY,
-1334,Town,Friendsanity: Harvey 6 <3,FRIENDSANITY,
-1335,Town,Friendsanity: Harvey 7 <3,FRIENDSANITY,
-1336,Town,Friendsanity: Harvey 8 <3,FRIENDSANITY,
-1337,Town,Friendsanity: Harvey 9 <3,FRIENDSANITY,
-1338,Town,Friendsanity: Harvey 10 <3,FRIENDSANITY,
-1339,Town,Friendsanity: Harvey 11 <3,FRIENDSANITY,
-1340,Town,Friendsanity: Harvey 12 <3,FRIENDSANITY,
-1341,Town,Friendsanity: Harvey 13 <3,FRIENDSANITY,
-1342,Town,Friendsanity: Harvey 14 <3,FRIENDSANITY,
-1343,Town,Friendsanity: Sam 1 <3,FRIENDSANITY,
-1344,Town,Friendsanity: Sam 2 <3,FRIENDSANITY,
-1345,Town,Friendsanity: Sam 3 <3,FRIENDSANITY,
-1346,Town,Friendsanity: Sam 4 <3,FRIENDSANITY,
-1347,Town,Friendsanity: Sam 5 <3,FRIENDSANITY,
-1348,Town,Friendsanity: Sam 6 <3,FRIENDSANITY,
-1349,Town,Friendsanity: Sam 7 <3,FRIENDSANITY,
-1350,Town,Friendsanity: Sam 8 <3,FRIENDSANITY,
-1351,Town,Friendsanity: Sam 9 <3,FRIENDSANITY,
-1352,Town,Friendsanity: Sam 10 <3,FRIENDSANITY,
-1353,Town,Friendsanity: Sam 11 <3,FRIENDSANITY,
-1354,Town,Friendsanity: Sam 12 <3,FRIENDSANITY,
-1355,Town,Friendsanity: Sam 13 <3,FRIENDSANITY,
-1356,Town,Friendsanity: Sam 14 <3,FRIENDSANITY,
-1357,Carpenter Shop,Friendsanity: Sebastian 1 <3,FRIENDSANITY,
-1358,Carpenter Shop,Friendsanity: Sebastian 2 <3,FRIENDSANITY,
-1359,Carpenter Shop,Friendsanity: Sebastian 3 <3,FRIENDSANITY,
-1360,Carpenter Shop,Friendsanity: Sebastian 4 <3,FRIENDSANITY,
-1361,Carpenter Shop,Friendsanity: Sebastian 5 <3,FRIENDSANITY,
-1362,Carpenter Shop,Friendsanity: Sebastian 6 <3,FRIENDSANITY,
-1363,Carpenter Shop,Friendsanity: Sebastian 7 <3,FRIENDSANITY,
-1364,Carpenter Shop,Friendsanity: Sebastian 8 <3,FRIENDSANITY,
-1365,Carpenter Shop,Friendsanity: Sebastian 9 <3,FRIENDSANITY,
-1366,Carpenter Shop,Friendsanity: Sebastian 10 <3,FRIENDSANITY,
-1367,Carpenter Shop,Friendsanity: Sebastian 11 <3,FRIENDSANITY,
-1368,Carpenter Shop,Friendsanity: Sebastian 12 <3,FRIENDSANITY,
-1369,Carpenter Shop,Friendsanity: Sebastian 13 <3,FRIENDSANITY,
-1370,Carpenter Shop,Friendsanity: Sebastian 14 <3,FRIENDSANITY,
-1371,Marnie's Ranch,Friendsanity: Shane 1 <3,FRIENDSANITY,
-1372,Marnie's Ranch,Friendsanity: Shane 2 <3,FRIENDSANITY,
-1373,Marnie's Ranch,Friendsanity: Shane 3 <3,FRIENDSANITY,
-1374,Marnie's Ranch,Friendsanity: Shane 4 <3,FRIENDSANITY,
-1375,Marnie's Ranch,Friendsanity: Shane 5 <3,FRIENDSANITY,
-1376,Marnie's Ranch,Friendsanity: Shane 6 <3,FRIENDSANITY,
-1377,Marnie's Ranch,Friendsanity: Shane 7 <3,FRIENDSANITY,
-1378,Marnie's Ranch,Friendsanity: Shane 8 <3,FRIENDSANITY,
-1379,Marnie's Ranch,Friendsanity: Shane 9 <3,FRIENDSANITY,
-1380,Marnie's Ranch,Friendsanity: Shane 10 <3,FRIENDSANITY,
-1381,Marnie's Ranch,Friendsanity: Shane 11 <3,FRIENDSANITY,
-1382,Marnie's Ranch,Friendsanity: Shane 12 <3,FRIENDSANITY,
-1383,Marnie's Ranch,Friendsanity: Shane 13 <3,FRIENDSANITY,
-1384,Marnie's Ranch,Friendsanity: Shane 14 <3,FRIENDSANITY,
-1385,Town,Friendsanity: Abigail 1 <3,FRIENDSANITY,
-1386,Town,Friendsanity: Abigail 2 <3,FRIENDSANITY,
-1387,Town,Friendsanity: Abigail 3 <3,FRIENDSANITY,
-1388,Town,Friendsanity: Abigail 4 <3,FRIENDSANITY,
-1389,Town,Friendsanity: Abigail 5 <3,FRIENDSANITY,
-1390,Town,Friendsanity: Abigail 6 <3,FRIENDSANITY,
-1391,Town,Friendsanity: Abigail 7 <3,FRIENDSANITY,
-1392,Town,Friendsanity: Abigail 8 <3,FRIENDSANITY,
-1393,Town,Friendsanity: Abigail 9 <3,FRIENDSANITY,
-1394,Town,Friendsanity: Abigail 10 <3,FRIENDSANITY,
-1395,Town,Friendsanity: Abigail 11 <3,FRIENDSANITY,
-1396,Town,Friendsanity: Abigail 12 <3,FRIENDSANITY,
-1397,Town,Friendsanity: Abigail 13 <3,FRIENDSANITY,
-1398,Town,Friendsanity: Abigail 14 <3,FRIENDSANITY,
-1399,Town,Friendsanity: Emily 1 <3,FRIENDSANITY,
-1400,Town,Friendsanity: Emily 2 <3,FRIENDSANITY,
-1401,Town,Friendsanity: Emily 3 <3,FRIENDSANITY,
-1402,Town,Friendsanity: Emily 4 <3,FRIENDSANITY,
-1403,Town,Friendsanity: Emily 5 <3,FRIENDSANITY,
-1404,Town,Friendsanity: Emily 6 <3,FRIENDSANITY,
-1405,Town,Friendsanity: Emily 7 <3,FRIENDSANITY,
-1406,Town,Friendsanity: Emily 8 <3,FRIENDSANITY,
-1407,Town,Friendsanity: Emily 9 <3,FRIENDSANITY,
-1408,Town,Friendsanity: Emily 10 <3,FRIENDSANITY,
-1409,Town,Friendsanity: Emily 11 <3,FRIENDSANITY,
-1410,Town,Friendsanity: Emily 12 <3,FRIENDSANITY,
-1411,Town,Friendsanity: Emily 13 <3,FRIENDSANITY,
-1412,Town,Friendsanity: Emily 14 <3,FRIENDSANITY,
-1413,Town,Friendsanity: Haley 1 <3,FRIENDSANITY,
-1414,Town,Friendsanity: Haley 2 <3,FRIENDSANITY,
-1415,Town,Friendsanity: Haley 3 <3,FRIENDSANITY,
-1416,Town,Friendsanity: Haley 4 <3,FRIENDSANITY,
-1417,Town,Friendsanity: Haley 5 <3,FRIENDSANITY,
-1418,Town,Friendsanity: Haley 6 <3,FRIENDSANITY,
-1419,Town,Friendsanity: Haley 7 <3,FRIENDSANITY,
-1420,Town,Friendsanity: Haley 8 <3,FRIENDSANITY,
-1421,Town,Friendsanity: Haley 9 <3,FRIENDSANITY,
-1422,Town,Friendsanity: Haley 10 <3,FRIENDSANITY,
-1423,Town,Friendsanity: Haley 11 <3,FRIENDSANITY,
-1424,Town,Friendsanity: Haley 12 <3,FRIENDSANITY,
-1425,Town,Friendsanity: Haley 13 <3,FRIENDSANITY,
-1426,Town,Friendsanity: Haley 14 <3,FRIENDSANITY,
-1427,Forest,Friendsanity: Leah 1 <3,FRIENDSANITY,
-1428,Forest,Friendsanity: Leah 2 <3,FRIENDSANITY,
-1429,Forest,Friendsanity: Leah 3 <3,FRIENDSANITY,
-1430,Forest,Friendsanity: Leah 4 <3,FRIENDSANITY,
-1431,Forest,Friendsanity: Leah 5 <3,FRIENDSANITY,
-1432,Forest,Friendsanity: Leah 6 <3,FRIENDSANITY,
-1433,Forest,Friendsanity: Leah 7 <3,FRIENDSANITY,
-1434,Forest,Friendsanity: Leah 8 <3,FRIENDSANITY,
-1435,Forest,Friendsanity: Leah 9 <3,FRIENDSANITY,
-1436,Forest,Friendsanity: Leah 10 <3,FRIENDSANITY,
-1437,Forest,Friendsanity: Leah 11 <3,FRIENDSANITY,
-1438,Forest,Friendsanity: Leah 12 <3,FRIENDSANITY,
-1439,Forest,Friendsanity: Leah 13 <3,FRIENDSANITY,
-1440,Forest,Friendsanity: Leah 14 <3,FRIENDSANITY,
-1441,Carpenter Shop,Friendsanity: Maru 1 <3,FRIENDSANITY,
-1442,Carpenter Shop,Friendsanity: Maru 2 <3,FRIENDSANITY,
-1443,Carpenter Shop,Friendsanity: Maru 3 <3,FRIENDSANITY,
-1444,Carpenter Shop,Friendsanity: Maru 4 <3,FRIENDSANITY,
-1445,Carpenter Shop,Friendsanity: Maru 5 <3,FRIENDSANITY,
-1446,Carpenter Shop,Friendsanity: Maru 6 <3,FRIENDSANITY,
-1447,Carpenter Shop,Friendsanity: Maru 7 <3,FRIENDSANITY,
-1448,Carpenter Shop,Friendsanity: Maru 8 <3,FRIENDSANITY,
-1449,Carpenter Shop,Friendsanity: Maru 9 <3,FRIENDSANITY,
-1450,Carpenter Shop,Friendsanity: Maru 10 <3,FRIENDSANITY,
-1451,Carpenter Shop,Friendsanity: Maru 11 <3,FRIENDSANITY,
-1452,Carpenter Shop,Friendsanity: Maru 12 <3,FRIENDSANITY,
-1453,Carpenter Shop,Friendsanity: Maru 13 <3,FRIENDSANITY,
-1454,Carpenter Shop,Friendsanity: Maru 14 <3,FRIENDSANITY,
-1455,Town,Friendsanity: Penny 1 <3,FRIENDSANITY,
-1456,Town,Friendsanity: Penny 2 <3,FRIENDSANITY,
-1457,Town,Friendsanity: Penny 3 <3,FRIENDSANITY,
-1458,Town,Friendsanity: Penny 4 <3,FRIENDSANITY,
-1459,Town,Friendsanity: Penny 5 <3,FRIENDSANITY,
-1460,Town,Friendsanity: Penny 6 <3,FRIENDSANITY,
-1461,Town,Friendsanity: Penny 7 <3,FRIENDSANITY,
-1462,Town,Friendsanity: Penny 8 <3,FRIENDSANITY,
-1463,Town,Friendsanity: Penny 9 <3,FRIENDSANITY,
-1464,Town,Friendsanity: Penny 10 <3,FRIENDSANITY,
-1465,Town,Friendsanity: Penny 11 <3,FRIENDSANITY,
-1466,Town,Friendsanity: Penny 12 <3,FRIENDSANITY,
-1467,Town,Friendsanity: Penny 13 <3,FRIENDSANITY,
-1468,Town,Friendsanity: Penny 14 <3,FRIENDSANITY,
-1469,Town,Friendsanity: Caroline 1 <3,FRIENDSANITY,
-1470,Town,Friendsanity: Caroline 2 <3,FRIENDSANITY,
-1471,Town,Friendsanity: Caroline 3 <3,FRIENDSANITY,
-1472,Town,Friendsanity: Caroline 4 <3,FRIENDSANITY,
-1473,Town,Friendsanity: Caroline 5 <3,FRIENDSANITY,
-1474,Town,Friendsanity: Caroline 6 <3,FRIENDSANITY,
-1475,Town,Friendsanity: Caroline 7 <3,FRIENDSANITY,
-1476,Town,Friendsanity: Caroline 8 <3,FRIENDSANITY,
-1477,Town,Friendsanity: Caroline 9 <3,FRIENDSANITY,
-1478,Town,Friendsanity: Caroline 10 <3,FRIENDSANITY,
-1480,Town,Friendsanity: Clint 1 <3,FRIENDSANITY,
-1481,Town,Friendsanity: Clint 2 <3,FRIENDSANITY,
-1482,Town,Friendsanity: Clint 3 <3,FRIENDSANITY,
-1483,Town,Friendsanity: Clint 4 <3,FRIENDSANITY,
-1484,Town,Friendsanity: Clint 5 <3,FRIENDSANITY,
-1485,Town,Friendsanity: Clint 6 <3,FRIENDSANITY,
-1486,Town,Friendsanity: Clint 7 <3,FRIENDSANITY,
-1487,Town,Friendsanity: Clint 8 <3,FRIENDSANITY,
-1488,Town,Friendsanity: Clint 9 <3,FRIENDSANITY,
-1489,Town,Friendsanity: Clint 10 <3,FRIENDSANITY,
-1491,Carpenter Shop,Friendsanity: Demetrius 1 <3,FRIENDSANITY,
-1492,Carpenter Shop,Friendsanity: Demetrius 2 <3,FRIENDSANITY,
-1493,Carpenter Shop,Friendsanity: Demetrius 3 <3,FRIENDSANITY,
-1494,Carpenter Shop,Friendsanity: Demetrius 4 <3,FRIENDSANITY,
-1495,Carpenter Shop,Friendsanity: Demetrius 5 <3,FRIENDSANITY,
-1496,Carpenter Shop,Friendsanity: Demetrius 6 <3,FRIENDSANITY,
-1497,Carpenter Shop,Friendsanity: Demetrius 7 <3,FRIENDSANITY,
-1498,Carpenter Shop,Friendsanity: Demetrius 8 <3,FRIENDSANITY,
-1499,Carpenter Shop,Friendsanity: Demetrius 9 <3,FRIENDSANITY,
-1500,Carpenter Shop,Friendsanity: Demetrius 10 <3,FRIENDSANITY,
-1502,The Mines,Friendsanity: Dwarf 1 <3,FRIENDSANITY,
-1503,The Mines,Friendsanity: Dwarf 2 <3,FRIENDSANITY,
-1504,The Mines,Friendsanity: Dwarf 3 <3,FRIENDSANITY,
-1505,The Mines,Friendsanity: Dwarf 4 <3,FRIENDSANITY,
-1506,The Mines,Friendsanity: Dwarf 5 <3,FRIENDSANITY,
-1507,The Mines,Friendsanity: Dwarf 6 <3,FRIENDSANITY,
-1508,The Mines,Friendsanity: Dwarf 7 <3,FRIENDSANITY,
-1509,The Mines,Friendsanity: Dwarf 8 <3,FRIENDSANITY,
-1510,The Mines,Friendsanity: Dwarf 9 <3,FRIENDSANITY,
-1511,The Mines,Friendsanity: Dwarf 10 <3,FRIENDSANITY,
-1513,Town,Friendsanity: Evelyn 1 <3,FRIENDSANITY,
-1514,Town,Friendsanity: Evelyn 2 <3,FRIENDSANITY,
-1515,Town,Friendsanity: Evelyn 3 <3,FRIENDSANITY,
-1516,Town,Friendsanity: Evelyn 4 <3,FRIENDSANITY,
-1517,Town,Friendsanity: Evelyn 5 <3,FRIENDSANITY,
-1518,Town,Friendsanity: Evelyn 6 <3,FRIENDSANITY,
-1519,Town,Friendsanity: Evelyn 7 <3,FRIENDSANITY,
-1520,Town,Friendsanity: Evelyn 8 <3,FRIENDSANITY,
-1521,Town,Friendsanity: Evelyn 9 <3,FRIENDSANITY,
-1522,Town,Friendsanity: Evelyn 10 <3,FRIENDSANITY,
-1524,Town,Friendsanity: George 1 <3,FRIENDSANITY,
-1525,Town,Friendsanity: George 2 <3,FRIENDSANITY,
-1526,Town,Friendsanity: George 3 <3,FRIENDSANITY,
-1527,Town,Friendsanity: George 4 <3,FRIENDSANITY,
-1528,Town,Friendsanity: George 5 <3,FRIENDSANITY,
-1529,Town,Friendsanity: George 6 <3,FRIENDSANITY,
-1530,Town,Friendsanity: George 7 <3,FRIENDSANITY,
-1531,Town,Friendsanity: George 8 <3,FRIENDSANITY,
-1532,Town,Friendsanity: George 9 <3,FRIENDSANITY,
-1533,Town,Friendsanity: George 10 <3,FRIENDSANITY,
-1535,Town,Friendsanity: Gus 1 <3,FRIENDSANITY,
-1536,Town,Friendsanity: Gus 2 <3,FRIENDSANITY,
-1537,Town,Friendsanity: Gus 3 <3,FRIENDSANITY,
-1538,Town,Friendsanity: Gus 4 <3,FRIENDSANITY,
-1539,Town,Friendsanity: Gus 5 <3,FRIENDSANITY,
-1540,Town,Friendsanity: Gus 6 <3,FRIENDSANITY,
-1541,Town,Friendsanity: Gus 7 <3,FRIENDSANITY,
-1542,Town,Friendsanity: Gus 8 <3,FRIENDSANITY,
-1543,Town,Friendsanity: Gus 9 <3,FRIENDSANITY,
-1544,Town,Friendsanity: Gus 10 <3,FRIENDSANITY,
-1546,Marnie's Ranch,Friendsanity: Jas 1 <3,FRIENDSANITY,
-1547,Marnie's Ranch,Friendsanity: Jas 2 <3,FRIENDSANITY,
-1548,Marnie's Ranch,Friendsanity: Jas 3 <3,FRIENDSANITY,
-1549,Marnie's Ranch,Friendsanity: Jas 4 <3,FRIENDSANITY,
-1550,Marnie's Ranch,Friendsanity: Jas 5 <3,FRIENDSANITY,
-1551,Marnie's Ranch,Friendsanity: Jas 6 <3,FRIENDSANITY,
-1552,Marnie's Ranch,Friendsanity: Jas 7 <3,FRIENDSANITY,
-1553,Marnie's Ranch,Friendsanity: Jas 8 <3,FRIENDSANITY,
-1554,Marnie's Ranch,Friendsanity: Jas 9 <3,FRIENDSANITY,
-1555,Marnie's Ranch,Friendsanity: Jas 10 <3,FRIENDSANITY,
-1557,Town,Friendsanity: Jodi 1 <3,FRIENDSANITY,
-1558,Town,Friendsanity: Jodi 2 <3,FRIENDSANITY,
-1559,Town,Friendsanity: Jodi 3 <3,FRIENDSANITY,
-1560,Town,Friendsanity: Jodi 4 <3,FRIENDSANITY,
-1561,Town,Friendsanity: Jodi 5 <3,FRIENDSANITY,
-1562,Town,Friendsanity: Jodi 6 <3,FRIENDSANITY,
-1563,Town,Friendsanity: Jodi 7 <3,FRIENDSANITY,
-1564,Town,Friendsanity: Jodi 8 <3,FRIENDSANITY,
-1565,Town,Friendsanity: Jodi 9 <3,FRIENDSANITY,
-1566,Town,Friendsanity: Jodi 10 <3,FRIENDSANITY,
-1568,Town,Friendsanity: Kent 1 <3,FRIENDSANITY,
-1569,Town,Friendsanity: Kent 2 <3,FRIENDSANITY,
-1570,Town,Friendsanity: Kent 3 <3,FRIENDSANITY,
-1571,Town,Friendsanity: Kent 4 <3,FRIENDSANITY,
-1572,Town,Friendsanity: Kent 5 <3,FRIENDSANITY,
-1573,Town,Friendsanity: Kent 6 <3,FRIENDSANITY,
-1574,Town,Friendsanity: Kent 7 <3,FRIENDSANITY,
-1575,Town,Friendsanity: Kent 8 <3,FRIENDSANITY,
-1576,Town,Friendsanity: Kent 9 <3,FRIENDSANITY,
-1577,Town,Friendsanity: Kent 10 <3,FRIENDSANITY,
-1579,Sewer,Friendsanity: Krobus 1 <3,FRIENDSANITY,
-1580,Sewer,Friendsanity: Krobus 2 <3,FRIENDSANITY,
-1581,Sewer,Friendsanity: Krobus 3 <3,FRIENDSANITY,
-1582,Sewer,Friendsanity: Krobus 4 <3,FRIENDSANITY,
-1583,Sewer,Friendsanity: Krobus 5 <3,FRIENDSANITY,
-1584,Sewer,Friendsanity: Krobus 6 <3,FRIENDSANITY,
-1585,Sewer,Friendsanity: Krobus 7 <3,FRIENDSANITY,
-1586,Sewer,Friendsanity: Krobus 8 <3,FRIENDSANITY,
-1587,Sewer,Friendsanity: Krobus 9 <3,FRIENDSANITY,
-1588,Sewer,Friendsanity: Krobus 10 <3,FRIENDSANITY,
-1590,Leo's Hut,Friendsanity: Leo 1 <3,"FRIENDSANITY,GINGER_ISLAND",
-1591,Leo's Hut,Friendsanity: Leo 2 <3,"FRIENDSANITY,GINGER_ISLAND",
-1592,Leo's Hut,Friendsanity: Leo 3 <3,"FRIENDSANITY,GINGER_ISLAND",
-1593,Leo's Hut,Friendsanity: Leo 4 <3,"FRIENDSANITY,GINGER_ISLAND",
-1594,Leo's Hut,Friendsanity: Leo 5 <3,"FRIENDSANITY,GINGER_ISLAND",
-1595,Leo's Hut,Friendsanity: Leo 6 <3,"FRIENDSANITY,GINGER_ISLAND",
-1596,Leo's Hut,Friendsanity: Leo 7 <3,"FRIENDSANITY,GINGER_ISLAND",
-1597,Leo's Hut,Friendsanity: Leo 8 <3,"FRIENDSANITY,GINGER_ISLAND",
-1598,Leo's Hut,Friendsanity: Leo 9 <3,"FRIENDSANITY,GINGER_ISLAND",
-1599,Leo's Hut,Friendsanity: Leo 10 <3,"FRIENDSANITY,GINGER_ISLAND",
-1601,Town,Friendsanity: Lewis 1 <3,FRIENDSANITY,
-1602,Town,Friendsanity: Lewis 2 <3,FRIENDSANITY,
-1603,Town,Friendsanity: Lewis 3 <3,FRIENDSANITY,
-1604,Town,Friendsanity: Lewis 4 <3,FRIENDSANITY,
-1605,Town,Friendsanity: Lewis 5 <3,FRIENDSANITY,
-1606,Town,Friendsanity: Lewis 6 <3,FRIENDSANITY,
-1607,Town,Friendsanity: Lewis 7 <3,FRIENDSANITY,
-1608,Town,Friendsanity: Lewis 8 <3,FRIENDSANITY,
-1609,Town,Friendsanity: Lewis 9 <3,FRIENDSANITY,
-1610,Town,Friendsanity: Lewis 10 <3,FRIENDSANITY,
-1612,Mountain,Friendsanity: Linus 1 <3,FRIENDSANITY,
-1613,Mountain,Friendsanity: Linus 2 <3,FRIENDSANITY,
-1614,Mountain,Friendsanity: Linus 3 <3,FRIENDSANITY,
-1615,Mountain,Friendsanity: Linus 4 <3,FRIENDSANITY,
-1616,Mountain,Friendsanity: Linus 5 <3,FRIENDSANITY,
-1617,Mountain,Friendsanity: Linus 6 <3,FRIENDSANITY,
-1618,Mountain,Friendsanity: Linus 7 <3,FRIENDSANITY,
-1619,Mountain,Friendsanity: Linus 8 <3,FRIENDSANITY,
-1620,Mountain,Friendsanity: Linus 9 <3,FRIENDSANITY,
-1621,Mountain,Friendsanity: Linus 10 <3,FRIENDSANITY,
-1623,Marnie's Ranch,Friendsanity: Marnie 1 <3,FRIENDSANITY,
-1624,Marnie's Ranch,Friendsanity: Marnie 2 <3,FRIENDSANITY,
-1625,Marnie's Ranch,Friendsanity: Marnie 3 <3,FRIENDSANITY,
-1626,Marnie's Ranch,Friendsanity: Marnie 4 <3,FRIENDSANITY,
-1627,Marnie's Ranch,Friendsanity: Marnie 5 <3,FRIENDSANITY,
-1628,Marnie's Ranch,Friendsanity: Marnie 6 <3,FRIENDSANITY,
-1629,Marnie's Ranch,Friendsanity: Marnie 7 <3,FRIENDSANITY,
-1630,Marnie's Ranch,Friendsanity: Marnie 8 <3,FRIENDSANITY,
-1631,Marnie's Ranch,Friendsanity: Marnie 9 <3,FRIENDSANITY,
-1632,Marnie's Ranch,Friendsanity: Marnie 10 <3,FRIENDSANITY,
-1634,Town,Friendsanity: Pam 1 <3,FRIENDSANITY,
-1635,Town,Friendsanity: Pam 2 <3,FRIENDSANITY,
-1636,Town,Friendsanity: Pam 3 <3,FRIENDSANITY,
-1637,Town,Friendsanity: Pam 4 <3,FRIENDSANITY,
-1638,Town,Friendsanity: Pam 5 <3,FRIENDSANITY,
-1639,Town,Friendsanity: Pam 6 <3,FRIENDSANITY,
-1640,Town,Friendsanity: Pam 7 <3,FRIENDSANITY,
-1641,Town,Friendsanity: Pam 8 <3,FRIENDSANITY,
-1642,Town,Friendsanity: Pam 9 <3,FRIENDSANITY,
-1643,Town,Friendsanity: Pam 10 <3,FRIENDSANITY,
-1645,Town,Friendsanity: Pierre 1 <3,FRIENDSANITY,
-1646,Town,Friendsanity: Pierre 2 <3,FRIENDSANITY,
-1647,Town,Friendsanity: Pierre 3 <3,FRIENDSANITY,
-1648,Town,Friendsanity: Pierre 4 <3,FRIENDSANITY,
-1649,Town,Friendsanity: Pierre 5 <3,FRIENDSANITY,
-1650,Town,Friendsanity: Pierre 6 <3,FRIENDSANITY,
-1651,Town,Friendsanity: Pierre 7 <3,FRIENDSANITY,
-1652,Town,Friendsanity: Pierre 8 <3,FRIENDSANITY,
-1653,Town,Friendsanity: Pierre 9 <3,FRIENDSANITY,
-1654,Town,Friendsanity: Pierre 10 <3,FRIENDSANITY,
-1656,Carpenter Shop,Friendsanity: Robin 1 <3,FRIENDSANITY,
-1657,Carpenter Shop,Friendsanity: Robin 2 <3,FRIENDSANITY,
-1658,Carpenter Shop,Friendsanity: Robin 3 <3,FRIENDSANITY,
-1659,Carpenter Shop,Friendsanity: Robin 4 <3,FRIENDSANITY,
-1660,Carpenter Shop,Friendsanity: Robin 5 <3,FRIENDSANITY,
-1661,Carpenter Shop,Friendsanity: Robin 6 <3,FRIENDSANITY,
-1662,Carpenter Shop,Friendsanity: Robin 7 <3,FRIENDSANITY,
-1663,Carpenter Shop,Friendsanity: Robin 8 <3,FRIENDSANITY,
-1664,Carpenter Shop,Friendsanity: Robin 9 <3,FRIENDSANITY,
-1665,Carpenter Shop,Friendsanity: Robin 10 <3,FRIENDSANITY,
-1667,Oasis,Friendsanity: Sandy 1 <3,FRIENDSANITY,
-1668,Oasis,Friendsanity: Sandy 2 <3,FRIENDSANITY,
-1669,Oasis,Friendsanity: Sandy 3 <3,FRIENDSANITY,
-1670,Oasis,Friendsanity: Sandy 4 <3,FRIENDSANITY,
-1671,Oasis,Friendsanity: Sandy 5 <3,FRIENDSANITY,
-1672,Oasis,Friendsanity: Sandy 6 <3,FRIENDSANITY,
-1673,Oasis,Friendsanity: Sandy 7 <3,FRIENDSANITY,
-1674,Oasis,Friendsanity: Sandy 8 <3,FRIENDSANITY,
-1675,Oasis,Friendsanity: Sandy 9 <3,FRIENDSANITY,
-1676,Oasis,Friendsanity: Sandy 10 <3,FRIENDSANITY,
-1678,Town,Friendsanity: Vincent 1 <3,FRIENDSANITY,
-1679,Town,Friendsanity: Vincent 2 <3,FRIENDSANITY,
-1680,Town,Friendsanity: Vincent 3 <3,FRIENDSANITY,
-1681,Town,Friendsanity: Vincent 4 <3,FRIENDSANITY,
-1682,Town,Friendsanity: Vincent 5 <3,FRIENDSANITY,
-1683,Town,Friendsanity: Vincent 6 <3,FRIENDSANITY,
-1684,Town,Friendsanity: Vincent 7 <3,FRIENDSANITY,
-1685,Town,Friendsanity: Vincent 8 <3,FRIENDSANITY,
-1686,Town,Friendsanity: Vincent 9 <3,FRIENDSANITY,
-1687,Town,Friendsanity: Vincent 10 <3,FRIENDSANITY,
-1689,Beach,Friendsanity: Willy 1 <3,FRIENDSANITY,
-1690,Beach,Friendsanity: Willy 2 <3,FRIENDSANITY,
-1691,Beach,Friendsanity: Willy 3 <3,FRIENDSANITY,
-1692,Beach,Friendsanity: Willy 4 <3,FRIENDSANITY,
-1693,Beach,Friendsanity: Willy 5 <3,FRIENDSANITY,
-1694,Beach,Friendsanity: Willy 6 <3,FRIENDSANITY,
-1695,Beach,Friendsanity: Willy 7 <3,FRIENDSANITY,
-1696,Beach,Friendsanity: Willy 8 <3,FRIENDSANITY,
-1697,Beach,Friendsanity: Willy 9 <3,FRIENDSANITY,
-1698,Beach,Friendsanity: Willy 10 <3,FRIENDSANITY,
-1700,Forest,Friendsanity: Wizard 1 <3,FRIENDSANITY,
-1701,Forest,Friendsanity: Wizard 2 <3,FRIENDSANITY,
-1702,Forest,Friendsanity: Wizard 3 <3,FRIENDSANITY,
-1703,Forest,Friendsanity: Wizard 4 <3,FRIENDSANITY,
-1704,Forest,Friendsanity: Wizard 5 <3,FRIENDSANITY,
-1705,Forest,Friendsanity: Wizard 6 <3,FRIENDSANITY,
-1706,Forest,Friendsanity: Wizard 7 <3,FRIENDSANITY,
-1707,Forest,Friendsanity: Wizard 8 <3,FRIENDSANITY,
-1708,Forest,Friendsanity: Wizard 9 <3,FRIENDSANITY,
-1709,Forest,Friendsanity: Wizard 10 <3,FRIENDSANITY,
-1710,Farm,Friendsanity: Pet 1 <3,FRIENDSANITY,
-1711,Farm,Friendsanity: Pet 2 <3,FRIENDSANITY,
-1712,Farm,Friendsanity: Pet 3 <3,FRIENDSANITY,
-1713,Farm,Friendsanity: Pet 4 <3,FRIENDSANITY,
-1714,Farm,Friendsanity: Pet 5 <3,FRIENDSANITY,
-1715,Farm,Friendsanity: Friend 1 <3,FRIENDSANITY,
-1716,Farm,Friendsanity: Friend 2 <3,FRIENDSANITY,
-1717,Farm,Friendsanity: Friend 3 <3,FRIENDSANITY,
-1718,Farm,Friendsanity: Friend 4 <3,FRIENDSANITY,
-1719,Farm,Friendsanity: Friend 5 <3,FRIENDSANITY,
-1720,Farm,Friendsanity: Friend 6 <3,FRIENDSANITY,
-1721,Farm,Friendsanity: Friend 7 <3,FRIENDSANITY,
-1722,Farm,Friendsanity: Friend 8 <3,FRIENDSANITY,
-1723,Farm,Friendsanity: Suitor 9 <3,FRIENDSANITY,
-1724,Farm,Friendsanity: Suitor 10 <3,FRIENDSANITY,
-1725,Farm,Friendsanity: Spouse 11 <3,FRIENDSANITY,
-1726,Farm,Friendsanity: Spouse 12 <3,FRIENDSANITY,
-1727,Farm,Friendsanity: Spouse 13 <3,FRIENDSANITY,
-1728,Farm,Friendsanity: Spouse 14 <3,FRIENDSANITY,
-2001,Town,Egg Hunt Victory,FESTIVAL,
-2002,Town,Egg Festival: Strawberry Seeds,FESTIVAL,
-2003,Forest,Dance with someone,FESTIVAL,
-2004,Forest,Rarecrow #5 (Woman),FESTIVAL,
-2005,Beach,Luau Soup,FESTIVAL,
-2006,Beach,Dance of the Moonlight Jellies,FESTIVAL,
-2007,Town,Smashing Stone,FESTIVAL,
-2008,Town,Grange Display,FESTIVAL,
-2009,Town,Rarecrow #1 (Turnip Head),FESTIVAL,
-2010,Town,Fair Stardrop,FESTIVAL,
-2011,Town,Spirit's Eve Maze,FESTIVAL,
-2012,Town,Rarecrow #2 (Witch),FESTIVAL,
-2013,Forest,Win Fishing Competition,FESTIVAL,
-2014,Forest,Rarecrow #4 (Snowman),FESTIVAL,
-2015,Beach,Mermaid Pearl,FESTIVAL,
-2016,Beach,Cone Hat,FESTIVAL_HARD,
-2017,Beach,Iridium Fireplace,FESTIVAL_HARD,
-2018,Beach,Rarecrow #7 (Tanuki),FESTIVAL,
-2019,Beach,Rarecrow #8 (Tribal Mask),FESTIVAL,
-2020,Beach,Lupini: Red Eagle,FESTIVAL,
-2021,Beach,Lupini: Portrait Of A Mermaid,FESTIVAL,
-2022,Beach,Lupini: Solar Kingdom,FESTIVAL,
-2023,Beach,Lupini: Clouds,FESTIVAL_HARD,
-2024,Beach,Lupini: 1000 Years From Now,FESTIVAL_HARD,
-2025,Beach,Lupini: Three Trees,FESTIVAL_HARD,
-2026,Beach,Lupini: The Serpent,FESTIVAL_HARD,
-2027,Beach,Lupini: 'Tropical Fish #173',FESTIVAL_HARD,
-2028,Beach,Lupini: Land Of Clay,FESTIVAL_HARD,
-2029,Town,Secret Santa,FESTIVAL,
-2030,Town,The Legend of the Winter Star,FESTIVAL,
-2031,Farm,Collect All Rarecrows,FESTIVAL,
-2101,Town,Island Ingredients,"GINGER_ISLAND,SPECIAL_ORDER_BOARD",
-2102,Town,Cave Patrol,SPECIAL_ORDER_BOARD,
-2103,Town,Aquatic Overpopulation,SPECIAL_ORDER_BOARD,
-2104,Town,Biome Balance,SPECIAL_ORDER_BOARD,
-2105,Town,Rock Rejuvenation,SPECIAL_ORDER_BOARD,
-2106,Town,Gifts for George,SPECIAL_ORDER_BOARD,
-2107,Town,Fragments of the past,"GINGER_ISLAND,SPECIAL_ORDER_BOARD",
-2108,Town,Gus' Famous Omelet,SPECIAL_ORDER_BOARD,
-2109,Town,Crop Order,SPECIAL_ORDER_BOARD,
-2110,Town,Community Cleanup,SPECIAL_ORDER_BOARD,
-2111,Town,The Strong Stuff,SPECIAL_ORDER_BOARD,
-2112,Town,Pierre's Prime Produce,SPECIAL_ORDER_BOARD,
-2113,Town,Robin's Project,SPECIAL_ORDER_BOARD,
-2114,Town,Robin's Resource Rush,SPECIAL_ORDER_BOARD,
-2115,Town,Juicy Bugs Wanted!,SPECIAL_ORDER_BOARD,
-2116,Town,Tropical Fish,"GINGER_ISLAND,SPECIAL_ORDER_BOARD",
-2117,Town,A Curious Substance,SPECIAL_ORDER_BOARD,
-2118,Town,Prismatic Jelly,SPECIAL_ORDER_BOARD,
-2151,Qi's Walnut Room,Qi's Crop,"GINGER_ISLAND,SPECIAL_ORDER_QI",
-2152,Qi's Walnut Room,Let's Play A Game,"GINGER_ISLAND,SPECIAL_ORDER_QI,JUNIMO_KART",
-2153,Qi's Walnut Room,Four Precious Stones,"GINGER_ISLAND,SPECIAL_ORDER_QI",
-2154,Qi's Walnut Room,Qi's Hungry Challenge,"GINGER_ISLAND,SPECIAL_ORDER_QI",
-2155,Qi's Walnut Room,Qi's Cuisine,"GINGER_ISLAND,SPECIAL_ORDER_QI",
-2156,Qi's Walnut Room,Qi's Kindness,"GINGER_ISLAND,SPECIAL_ORDER_QI",
-2157,Qi's Walnut Room,Extended Family,"GINGER_ISLAND,SPECIAL_ORDER_QI",
-2158,Qi's Walnut Room,Danger In The Deep,"GINGER_ISLAND,SPECIAL_ORDER_QI",
-2159,Qi's Walnut Room,Skull Cavern Invasion,"GINGER_ISLAND,SPECIAL_ORDER_QI",
-2160,Qi's Walnut Room,Qi's Prismatic Grange,"GINGER_ISLAND,SPECIAL_ORDER_QI",
-2201,Boat Tunnel,Repair Ticket Machine,GINGER_ISLAND,
-2202,Boat Tunnel,Repair Boat Hull,GINGER_ISLAND,
-2203,Boat Tunnel,Repair Boat Anchor,GINGER_ISLAND,
-2204,Leo's Hut,Leo's Parrot,"GINGER_ISLAND,WALNUT_PURCHASE",
-2205,Island South,Island West Turtle,"GINGER_ISLAND,WALNUT_PURCHASE",
-2206,Island West,Island Farmhouse,"GINGER_ISLAND,WALNUT_PURCHASE",
-2207,Island Farmhouse,Island Mailbox,"GINGER_ISLAND,WALNUT_PURCHASE",
-2208,Island Farmhouse,Farm Obelisk,"GINGER_ISLAND,WALNUT_PURCHASE",
-2209,Island North,Dig Site Bridge,"GINGER_ISLAND,WALNUT_PURCHASE",
-2210,Island North,Island Trader,"GINGER_ISLAND,WALNUT_PURCHASE",
-2211,Volcano Entrance,Volcano Bridge,"GINGER_ISLAND,WALNUT_PURCHASE",
-2212,Volcano - Floor 5,Volcano Exit Shortcut,"GINGER_ISLAND,WALNUT_PURCHASE",
-2213,Island South,Island Resort,"GINGER_ISLAND,WALNUT_PURCHASE",
-2214,Island West,Parrot Express,"GINGER_ISLAND,WALNUT_PURCHASE",
-2215,Dig Site,Open Professor Snail Cave,"GINGER_ISLAND",
-2216,Field Office,Complete Island Field Office,"GINGER_ISLAND",
-2301,Farm,Harvest Amaranth,"CROPSANITY",
-2302,Farm,Harvest Artichoke,"CROPSANITY",
-2303,Farm,Harvest Beet,"CROPSANITY",
-2304,Farm,Harvest Blue Jazz,"CROPSANITY",
-2305,Farm,Harvest Blueberry,"CROPSANITY",
-2306,Farm,Harvest Bok Choy,"CROPSANITY",
-2307,Farm,Harvest Cauliflower,"CROPSANITY",
-2308,Farm,Harvest Corn,"CROPSANITY",
-2309,Farm,Harvest Cranberries,"CROPSANITY",
-2310,Farm,Harvest Eggplant,"CROPSANITY",
-2311,Farm,Harvest Fairy Rose,"CROPSANITY",
-2312,Farm,Harvest Garlic,"CROPSANITY",
-2313,Farm,Harvest Grape,"CROPSANITY",
-2314,Farm,Harvest Green Bean,"CROPSANITY",
-2315,Farm,Harvest Hops,"CROPSANITY",
-2316,Farm,Harvest Hot Pepper,"CROPSANITY",
-2317,Farm,Harvest Kale,"CROPSANITY",
-2318,Farm,Harvest Melon,"CROPSANITY",
-2319,Farm,Harvest Parsnip,"CROPSANITY",
-2320,Farm,Harvest Poppy,"CROPSANITY",
-2321,Farm,Harvest Potato,"CROPSANITY",
-2322,Farm,Harvest Pumpkin,"CROPSANITY",
-2323,Farm,Harvest Radish,"CROPSANITY",
-2324,Farm,Harvest Red Cabbage,"CROPSANITY",
-2325,Farm,Harvest Rhubarb,"CROPSANITY",
-2326,Farm,Harvest Starfruit,"CROPSANITY",
-2327,Farm,Harvest Strawberry,"CROPSANITY",
-2328,Farm,Harvest Summer Spangle,"CROPSANITY",
-2329,Farm,Harvest Sunflower,"CROPSANITY",
-2330,Farm,Harvest Tomato,"CROPSANITY",
-2331,Farm,Harvest Tulip,"CROPSANITY",
-2332,Farm,Harvest Unmilled Rice,"CROPSANITY",
-2333,Farm,Harvest Wheat,"CROPSANITY",
-2334,Farm,Harvest Yam,"CROPSANITY",
-2335,Farm,Harvest Cactus Fruit,"CROPSANITY",
-2336,Farm,Harvest Pineapple,"CROPSANITY,GINGER_ISLAND",
-2337,Farm,Harvest Taro Root,"CROPSANITY,GINGER_ISLAND",
-2338,Farm,Harvest Sweet Gem Berry,"CROPSANITY",
-2339,Farm,Harvest Apple,"CROPSANITY",
-2340,Farm,Harvest Apricot,"CROPSANITY",
-2341,Farm,Harvest Cherry,"CROPSANITY",
-2342,Farm,Harvest Orange,"CROPSANITY",
-2343,Farm,Harvest Pomegranate,"CROPSANITY",
-2344,Farm,Harvest Peach,"CROPSANITY",
-2345,Farm,Harvest Banana,"CROPSANITY,GINGER_ISLAND",
-2346,Farm,Harvest Mango,"CROPSANITY,GINGER_ISLAND",
-2347,Farm,Harvest Coffee Bean,"CROPSANITY",
-5001,Stardew Valley,Level 1 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
-5002,Stardew Valley,Level 2 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
-5003,Stardew Valley,Level 3 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
-5004,Stardew Valley,Level 4 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
-5005,Stardew Valley,Level 5 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
-5006,Stardew Valley,Level 6 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
-5007,Stardew Valley,Level 7 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
-5008,Stardew Valley,Level 8 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
-5009,Stardew Valley,Level 9 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
-5010,Stardew Valley,Level 10 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
-5011,Stardew Valley,Level 1 Socializing,"SOCIALIZING_LEVEL,SKILL_LEVEL",Socializing Skill
-5012,Stardew Valley,Level 2 Socializing,"SOCIALIZING_LEVEL,SKILL_LEVEL",Socializing Skill
-5013,Stardew Valley,Level 3 Socializing,"SOCIALIZING_LEVEL,SKILL_LEVEL",Socializing Skill
-5014,Stardew Valley,Level 4 Socializing,"SOCIALIZING_LEVEL,SKILL_LEVEL",Socializing Skill
-5015,Stardew Valley,Level 5 Socializing,"SOCIALIZING_LEVEL,SKILL_LEVEL",Socializing Skill
-5016,Stardew Valley,Level 6 Socializing,"SOCIALIZING_LEVEL,SKILL_LEVEL",Socializing Skill
-5017,Stardew Valley,Level 7 Socializing,"SOCIALIZING_LEVEL,SKILL_LEVEL",Socializing Skill
-5018,Stardew Valley,Level 8 Socializing,"SOCIALIZING_LEVEL,SKILL_LEVEL",Socializing Skill
-5019,Stardew Valley,Level 9 Socializing,"SOCIALIZING_LEVEL,SKILL_LEVEL",Socializing Skill
-5020,Stardew Valley,Level 10 Socializing,"SOCIALIZING_LEVEL,SKILL_LEVEL",Socializing Skill
-5021,Stardew Valley,Level 1 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
-5022,Stardew Valley,Level 2 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
-5023,Stardew Valley,Level 3 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
-5024,Stardew Valley,Level 4 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
-5025,Stardew Valley,Level 5 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
-5026,Stardew Valley,Level 6 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
-5027,Stardew Valley,Level 7 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
-5028,Stardew Valley,Level 8 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
-5029,Stardew Valley,Level 9 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
-5030,Stardew Valley,Level 10 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
-5031,Stardew Valley,Level 1 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
-5032,Stardew Valley,Level 2 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
-5033,Stardew Valley,Level 3 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
-5034,Stardew Valley,Level 4 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
-5035,Stardew Valley,Level 5 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
-5036,Stardew Valley,Level 6 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
-5037,Stardew Valley,Level 7 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
-5038,Stardew Valley,Level 8 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
-5039,Stardew Valley,Level 9 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
-5040,Stardew Valley,Level 10 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
-5041,Stardew Valley,Level 1 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
-5042,Stardew Valley,Level 2 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
-5043,Stardew Valley,Level 3 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
-5044,Stardew Valley,Level 4 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
-5045,Stardew Valley,Level 5 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
-5046,Stardew Valley,Level 6 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
-5047,Stardew Valley,Level 7 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
-5048,Stardew Valley,Level 8 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
-5049,Stardew Valley,Level 9 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
-5050,Stardew Valley,Level 10 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
-5051,Stardew Valley,Level 1 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
-5052,Stardew Valley,Level 2 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
-5053,Stardew Valley,Level 3 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
-5054,Stardew Valley,Level 4 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
-5055,Stardew Valley,Level 5 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
-5056,Stardew Valley,Level 6 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
-5057,Stardew Valley,Level 7 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
-5058,Stardew Valley,Level 8 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
-5059,Stardew Valley,Level 9 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
-5060,Stardew Valley,Level 10 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
-5501,Stardew Valley,Analyze: Clear Debris,MANDATORY,Magic
-5502,Stardew Valley,Analyze: Till,MANDATORY,Magic
-5503,Stardew Valley,Analyze: Water,MANDATORY,Magic
-5504,Stardew Valley,Analyze All Toil School Locations,MANDATORY,Magic
-5505,Stardew Valley,Analyze: Evac,MANDATORY,Magic
-5506,Stardew Valley,Analyze: Haste,MANDATORY,Magic
-5507,Stardew Valley,Analyze: Heal,MANDATORY,Magic
-5508,Stardew Valley,Analyze All Life School Locations,MANDATORY,Magic
-5509,Stardew Valley,Analyze: Descend,MANDATORY,Magic
-5510,Stardew Valley,Analyze: Fireball,MANDATORY,Magic
-5511,Stardew Valley,Analyze: Frostbite,MANDATORY,Magic
-5512,Stardew Valley,Analyze All Elemental School Locations,MANDATORY,Magic
-5513,Stardew Valley,Analyze: Lantern,MANDATORY,Magic
-5514,Stardew Valley,Analyze: Tendrils,MANDATORY,Magic
-5515,Stardew Valley,Analyze: Shockwave,MANDATORY,Magic
-5516,Stardew Valley,Analyze All Nature School Locations,MANDATORY,Magic
-5517,Stardew Valley,Analyze: Meteor,MANDATORY,Magic
-5518,Stardew Valley,Analyze: Lucksteal,MANDATORY,Magic
-5519,Stardew Valley,Analyze: Bloodmana,MANDATORY,Magic
-5520,Stardew Valley,Analyze All Eldritch School Locations,MANDATORY,Magic
-5521,Stardew Valley,Analyze Every Magic School Location,MANDATORY,Magic
-6001,Town,Friendsanity: Jasper 1 <3,FRIENDSANITY,Professor Jasper Thomas
-6002,Town,Friendsanity: Jasper 2 <3,FRIENDSANITY,Professor Jasper Thomas
-6003,Town,Friendsanity: Jasper 3 <3,FRIENDSANITY,Professor Jasper Thomas
-6004,Town,Friendsanity: Jasper 4 <3,FRIENDSANITY,Professor Jasper Thomas
-6005,Town,Friendsanity: Jasper 5 <3,FRIENDSANITY,Professor Jasper Thomas
-6006,Town,Friendsanity: Jasper 6 <3,FRIENDSANITY,Professor Jasper Thomas
-6007,Town,Friendsanity: Jasper 7 <3,FRIENDSANITY,Professor Jasper Thomas
-6008,Town,Friendsanity: Jasper 8 <3,FRIENDSANITY,Professor Jasper Thomas
-6009,Town,Friendsanity: Jasper 9 <3,FRIENDSANITY,Professor Jasper Thomas
-6010,Town,Friendsanity: Jasper 10 <3,FRIENDSANITY,Professor Jasper Thomas
-6011,Town,Friendsanity: Jasper 11 <3,FRIENDSANITY,Professor Jasper Thomas
-6012,Town,Friendsanity: Jasper 12 <3,FRIENDSANITY,Professor Jasper Thomas
-6013,Town,Friendsanity: Jasper 13 <3,FRIENDSANITY,Professor Jasper Thomas
-6014,Town,Friendsanity: Jasper 14 <3,FRIENDSANITY,Professor Jasper Thomas
-6015,Secret Woods,Friendsanity: Yoba 1 <3,FRIENDSANITY,Custom NPC - Yoba
-6016,Secret Woods,Friendsanity: Yoba 2 <3,FRIENDSANITY,Custom NPC - Yoba
-6017,Secret Woods,Friendsanity: Yoba 3 <3,FRIENDSANITY,Custom NPC - Yoba
-6018,Secret Woods,Friendsanity: Yoba 4 <3,FRIENDSANITY,Custom NPC - Yoba
-6019,Secret Woods,Friendsanity: Yoba 5 <3,FRIENDSANITY,Custom NPC - Yoba
-6020,Secret Woods,Friendsanity: Yoba 6 <3,FRIENDSANITY,Custom NPC - Yoba
-6021,Secret Woods,Friendsanity: Yoba 7 <3,FRIENDSANITY,Custom NPC - Yoba
-6022,Secret Woods,Friendsanity: Yoba 8 <3,FRIENDSANITY,Custom NPC - Yoba
-6023,Secret Woods,Friendsanity: Yoba 9 <3,FRIENDSANITY,Custom NPC - Yoba
-6024,Secret Woods,Friendsanity: Yoba 10 <3,FRIENDSANITY,Custom NPC - Yoba
-6025,Forest,Friendsanity: Mr. Ginger 1 <3,FRIENDSANITY,Mister Ginger (cat npc)
-6026,Forest,Friendsanity: Mr. Ginger 2 <3,FRIENDSANITY,Mister Ginger (cat npc)
-6027,Forest,Friendsanity: Mr. Ginger 3 <3,FRIENDSANITY,Mister Ginger (cat npc)
-6028,Forest,Friendsanity: Mr. Ginger 4 <3,FRIENDSANITY,Mister Ginger (cat npc)
-6029,Forest,Friendsanity: Mr. Ginger 5 <3,FRIENDSANITY,Mister Ginger (cat npc)
-6030,Forest,Friendsanity: Mr. Ginger 6 <3,FRIENDSANITY,Mister Ginger (cat npc)
-6031,Forest,Friendsanity: Mr. Ginger 7 <3,FRIENDSANITY,Mister Ginger (cat npc)
-6032,Forest,Friendsanity: Mr. Ginger 8 <3,FRIENDSANITY,Mister Ginger (cat npc)
-6033,Forest,Friendsanity: Mr. Ginger 9 <3,FRIENDSANITY,Mister Ginger (cat npc)
-6034,Forest,Friendsanity: Mr. Ginger 10 <3,FRIENDSANITY,Mister Ginger (cat npc)
-6035,Town,Friendsanity: Ayeisha 1 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
-6036,Town,Friendsanity: Ayeisha 2 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
-6037,Town,Friendsanity: Ayeisha 3 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
-6038,Town,Friendsanity: Ayeisha 4 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
-6039,Town,Friendsanity: Ayeisha 5 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
-6040,Town,Friendsanity: Ayeisha 6 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
-6041,Town,Friendsanity: Ayeisha 7 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
-6042,Town,Friendsanity: Ayeisha 8 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
-6043,Town,Friendsanity: Ayeisha 9 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
-6044,Town,Friendsanity: Ayeisha 10 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
-6045,Town,Friendsanity: Shiko 1 <3,FRIENDSANITY,Shiko - New Custom NPC
-6046,Town,Friendsanity: Shiko 2 <3,FRIENDSANITY,Shiko - New Custom NPC
-6047,Town,Friendsanity: Shiko 3 <3,FRIENDSANITY,Shiko - New Custom NPC
-6048,Town,Friendsanity: Shiko 4 <3,FRIENDSANITY,Shiko - New Custom NPC
-6049,Town,Friendsanity: Shiko 5 <3,FRIENDSANITY,Shiko - New Custom NPC
-6050,Town,Friendsanity: Shiko 6 <3,FRIENDSANITY,Shiko - New Custom NPC
-6051,Town,Friendsanity: Shiko 7 <3,FRIENDSANITY,Shiko - New Custom NPC
-6052,Town,Friendsanity: Shiko 8 <3,FRIENDSANITY,Shiko - New Custom NPC
-6053,Town,Friendsanity: Shiko 9 <3,FRIENDSANITY,Shiko - New Custom NPC
-6054,Town,Friendsanity: Shiko 10 <3,FRIENDSANITY,Shiko - New Custom NPC
-6055,Town,Friendsanity: Shiko 11 <3,FRIENDSANITY,Shiko - New Custom NPC
-6056,Town,Friendsanity: Shiko 12 <3,FRIENDSANITY,Shiko - New Custom NPC
-6057,Town,Friendsanity: Shiko 13 <3,FRIENDSANITY,Shiko - New Custom NPC
-6058,Town,Friendsanity: Shiko 14 <3,FRIENDSANITY,Shiko - New Custom NPC
-6059,Forest,Friendsanity: Wellwick 1 <3,FRIENDSANITY,'Prophet' Wellwick
-6060,Forest,Friendsanity: Wellwick 2 <3,FRIENDSANITY,'Prophet' Wellwick
-6061,Forest,Friendsanity: Wellwick 3 <3,FRIENDSANITY,'Prophet' Wellwick
-6062,Forest,Friendsanity: Wellwick 4 <3,FRIENDSANITY,'Prophet' Wellwick
-6063,Forest,Friendsanity: Wellwick 5 <3,FRIENDSANITY,'Prophet' Wellwick
-6064,Forest,Friendsanity: Wellwick 6 <3,FRIENDSANITY,'Prophet' Wellwick
-6065,Forest,Friendsanity: Wellwick 7 <3,FRIENDSANITY,'Prophet' Wellwick
-6066,Forest,Friendsanity: Wellwick 8 <3,FRIENDSANITY,'Prophet' Wellwick
-6067,Forest,Friendsanity: Wellwick 9 <3,FRIENDSANITY,'Prophet' Wellwick
-6068,Forest,Friendsanity: Wellwick 10 <3,FRIENDSANITY,'Prophet' Wellwick
-6069,Forest,Friendsanity: Wellwick 11 <3,FRIENDSANITY,'Prophet' Wellwick
-6070,Forest,Friendsanity: Wellwick 12 <3,FRIENDSANITY,'Prophet' Wellwick
-6071,Forest,Friendsanity: Wellwick 13 <3,FRIENDSANITY,'Prophet' Wellwick
-6072,Forest,Friendsanity: Wellwick 14 <3,FRIENDSANITY,'Prophet' Wellwick
-6073,Forest,Friendsanity: Delores 1 <3,FRIENDSANITY,Delores - Custom NPC
-6074,Forest,Friendsanity: Delores 2 <3,FRIENDSANITY,Delores - Custom NPC
-6075,Forest,Friendsanity: Delores 3 <3,FRIENDSANITY,Delores - Custom NPC
-6076,Forest,Friendsanity: Delores 4 <3,FRIENDSANITY,Delores - Custom NPC
-6077,Forest,Friendsanity: Delores 5 <3,FRIENDSANITY,Delores - Custom NPC
-6078,Forest,Friendsanity: Delores 6 <3,FRIENDSANITY,Delores - Custom NPC
-6079,Forest,Friendsanity: Delores 7 <3,FRIENDSANITY,Delores - Custom NPC
-6080,Forest,Friendsanity: Delores 8 <3,FRIENDSANITY,Delores - Custom NPC
-6081,Forest,Friendsanity: Delores 9 <3,FRIENDSANITY,Delores - Custom NPC
-6082,Forest,Friendsanity: Delores 10 <3,FRIENDSANITY,Delores - Custom NPC
-6083,Forest,Friendsanity: Delores 11 <3,FRIENDSANITY,Delores - Custom NPC
-6084,Forest,Friendsanity: Delores 12 <3,FRIENDSANITY,Delores - Custom NPC
-6085,Forest,Friendsanity: Delores 13 <3,FRIENDSANITY,Delores - Custom NPC
-6086,Forest,Friendsanity: Delores 14 <3,FRIENDSANITY,Delores - Custom NPC
-6087,Forest,Friendsanity: Alec 1 <3,FRIENDSANITY,Alec Revisited
-6088,Forest,Friendsanity: Alec 2 <3,FRIENDSANITY,Alec Revisited
-6089,Forest,Friendsanity: Alec 3 <3,FRIENDSANITY,Alec Revisited
-6090,Forest,Friendsanity: Alec 4 <3,FRIENDSANITY,Alec Revisited
-6091,Forest,Friendsanity: Alec 5 <3,FRIENDSANITY,Alec Revisited
-6092,Forest,Friendsanity: Alec 6 <3,FRIENDSANITY,Alec Revisited
-6093,Forest,Friendsanity: Alec 7 <3,FRIENDSANITY,Alec Revisited
-6094,Forest,Friendsanity: Alec 8 <3,FRIENDSANITY,Alec Revisited
-6095,Forest,Friendsanity: Alec 9 <3,FRIENDSANITY,Alec Revisited
-6096,Forest,Friendsanity: Alec 10 <3,FRIENDSANITY,Alec Revisited
-6097,Forest,Friendsanity: Alec 11 <3,FRIENDSANITY,Alec Revisited
-6098,Forest,Friendsanity: Alec 12 <3,FRIENDSANITY,Alec Revisited
-6099,Forest,Friendsanity: Alec 13 <3,FRIENDSANITY,Alec Revisited
-6100,Forest,Friendsanity: Alec 14 <3,FRIENDSANITY,Alec Revisited
-6101,Forest,Friendsanity: Eugene 1 <3,FRIENDSANITY,Custom NPC Eugene
-6102,Forest,Friendsanity: Eugene 2 <3,FRIENDSANITY,Custom NPC Eugene
-6103,Forest,Friendsanity: Eugene 3 <3,FRIENDSANITY,Custom NPC Eugene
-6104,Forest,Friendsanity: Eugene 4 <3,FRIENDSANITY,Custom NPC Eugene
-6105,Forest,Friendsanity: Eugene 5 <3,FRIENDSANITY,Custom NPC Eugene
-6106,Forest,Friendsanity: Eugene 6 <3,FRIENDSANITY,Custom NPC Eugene
-6107,Forest,Friendsanity: Eugene 7 <3,FRIENDSANITY,Custom NPC Eugene
-6108,Forest,Friendsanity: Eugene 8 <3,FRIENDSANITY,Custom NPC Eugene
-6109,Forest,Friendsanity: Eugene 9 <3,FRIENDSANITY,Custom NPC Eugene
-6110,Forest,Friendsanity: Eugene 10 <3,FRIENDSANITY,Custom NPC Eugene
-6111,Forest,Friendsanity: Eugene 11 <3,FRIENDSANITY,Custom NPC Eugene
-6112,Forest,Friendsanity: Eugene 12 <3,FRIENDSANITY,Custom NPC Eugene
-6113,Forest,Friendsanity: Eugene 13 <3,FRIENDSANITY,Custom NPC Eugene
-6114,Forest,Friendsanity: Eugene 14 <3,FRIENDSANITY,Custom NPC Eugene
-6115,Forest,Friendsanity: Juna 1 <3,FRIENDSANITY,Juna - Roommate NPC
-6116,Forest,Friendsanity: Juna 2 <3,FRIENDSANITY,Juna - Roommate NPC
-6117,Forest,Friendsanity: Juna 3 <3,FRIENDSANITY,Juna - Roommate NPC
-6118,Forest,Friendsanity: Juna 4 <3,FRIENDSANITY,Juna - Roommate NPC
-6119,Forest,Friendsanity: Juna 5 <3,FRIENDSANITY,Juna - Roommate NPC
-6120,Forest,Friendsanity: Juna 6 <3,FRIENDSANITY,Juna - Roommate NPC
-6121,Forest,Friendsanity: Juna 7 <3,FRIENDSANITY,Juna - Roommate NPC
-6122,Forest,Friendsanity: Juna 8 <3,FRIENDSANITY,Juna - Roommate NPC
-6123,Forest,Friendsanity: Juna 9 <3,FRIENDSANITY,Juna - Roommate NPC
-6124,Forest,Friendsanity: Juna 10 <3,FRIENDSANITY,Juna - Roommate NPC
-6125,Town,Friendsanity: Riley 1 <3,FRIENDSANITY,Custom NPC - Riley
-6126,Town,Friendsanity: Riley 2 <3,FRIENDSANITY,Custom NPC - Riley
-6127,Town,Friendsanity: Riley 3 <3,FRIENDSANITY,Custom NPC - Riley
-6128,Town,Friendsanity: Riley 4 <3,FRIENDSANITY,Custom NPC - Riley
-6129,Town,Friendsanity: Riley 5 <3,FRIENDSANITY,Custom NPC - Riley
-6130,Town,Friendsanity: Riley 6 <3,FRIENDSANITY,Custom NPC - Riley
-6131,Town,Friendsanity: Riley 7 <3,FRIENDSANITY,Custom NPC - Riley
-6132,Town,Friendsanity: Riley 8 <3,FRIENDSANITY,Custom NPC - Riley
-6133,Town,Friendsanity: Riley 9 <3,FRIENDSANITY,Custom NPC - Riley
-6134,Town,Friendsanity: Riley 10 <3,FRIENDSANITY,Custom NPC - Riley
-6135,Town,Friendsanity: Riley 11 <3,FRIENDSANITY,Custom NPC - Riley
-6136,Town,Friendsanity: Riley 12 <3,FRIENDSANITY,Custom NPC - Riley
-6137,Town,Friendsanity: Riley 13 <3,FRIENDSANITY,Custom NPC - Riley
-6138,Town,Friendsanity: Riley 14 <3,FRIENDSANITY,Custom NPC - Riley
-7001,Pierre's General Store,Premium Pack,BACKPACK,Bigger Backpack
-7002,Carpenter Shop,Tractor Garage Blueprint,BUILDING_BLUEPRINT,Tractor Mod
-7003,The Deep Woods Depth 100,Pet the Deep Woods Unicorn,MANDATORY,DeepWoods
-7004,The Deep Woods Depth 50,Breaking Up Deep Woods Gingerbread House,MANDATORY,DeepWoods
-7005,The Deep Woods Depth 50,Drinking From Deep Woods Fountain,MANDATORY,DeepWoods
-7006,The Deep Woods Depth 100,Deep Woods Treasure Chest,MANDATORY,DeepWoods
-7007,The Deep Woods Depth 100,Deep Woods Trash Bin,MANDATORY,DeepWoods
-7008,The Deep Woods Depth 50,Chop Down a Deep Woods Iridium Tree,MANDATORY,DeepWoods
-7009,The Deep Woods Depth 10,The Deep Woods: Depth 10,ELEVATOR,DeepWoods
-7010,The Deep Woods Depth 20,The Deep Woods: Depth 20,ELEVATOR,DeepWoods
-7011,The Deep Woods Depth 30,The Deep Woods: Depth 30,ELEVATOR,DeepWoods
-7012,The Deep Woods Depth 40,The Deep Woods: Depth 40,ELEVATOR,DeepWoods
-7013,The Deep Woods Depth 50,The Deep Woods: Depth 50,ELEVATOR,DeepWoods
-7014,The Deep Woods Depth 60,The Deep Woods: Depth 60,ELEVATOR,DeepWoods
-7015,The Deep Woods Depth 70,The Deep Woods: Depth 70,ELEVATOR,DeepWoods
-7016,The Deep Woods Depth 80,The Deep Woods: Depth 80,ELEVATOR,DeepWoods
-7017,The Deep Woods Depth 90,The Deep Woods: Depth 90,ELEVATOR,DeepWoods
-7018,The Deep Woods Depth 100,The Deep Woods: Depth 100,ELEVATOR,DeepWoods
-7019,The Deep Woods Depth 50,Purify an Infested Lichtung,MANDATORY,DeepWoods
-7020,Skull Cavern Floor 25,Skull Cavern: Floor 25,ELEVATOR,Skull Cavern Elevator
-7021,Skull Cavern Floor 50,Skull Cavern: Floor 50,ELEVATOR,Skull Cavern Elevator
-7022,Skull Cavern Floor 75,Skull Cavern: Floor 75,ELEVATOR,Skull Cavern Elevator
-7023,Skull Cavern Floor 100,Skull Cavern: Floor 100,ELEVATOR,Skull Cavern Elevator
-7024,Skull Cavern Floor 125,Skull Cavern: Floor 125,ELEVATOR,Skull Cavern Elevator
-7025,Skull Cavern Floor 150,Skull Cavern: Floor 150,ELEVATOR,Skull Cavern Elevator
-7026,Skull Cavern Floor 175,Skull Cavern: Floor 175,ELEVATOR,Skull Cavern Elevator
-7027,Skull Cavern Floor 200,Skull Cavern: Floor 200,ELEVATOR,Skull Cavern Elevator
-7501,Town,Missing Envelope,"MANDATORY,QUEST",Ayeisha - The Postal Worker (Custom NPC)
-7502,Town,Lost Emerald Ring,"MANDATORY,QUEST",Ayeisha - The Postal Worker (Custom NPC)
-7503,Forest,Mr.Ginger's request,"MANDATORY,QUEST",Mister Ginger (cat npc)
-7504,Forest,Juna's Drink Request,"MANDATORY,QUEST",Juna - Roommate NPC
-7505,Forest,Juna's BFF Request,"MANDATORY,QUEST",Juna - Roommate NPC
-7506,Forest,Juna's Monster Mash,SPECIAL_ORDER_BOARD,Juna - Roommate NPC
+id,region,name,tags,mod_name
+1,Crafts Room,Spring Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+2,Crafts Room,Summer Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+3,Crafts Room,Fall Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+4,Crafts Room,Winter Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+5,Crafts Room,Construction Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+6,Crafts Room,Exotic Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+7,Pantry,Spring Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,PANTRY_BUNDLE",
+8,Pantry,Summer Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,PANTRY_BUNDLE",
+9,Pantry,Fall Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,PANTRY_BUNDLE",
+10,Pantry,Quality Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,PANTRY_BUNDLE",
+11,Pantry,Animal Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,PANTRY_BUNDLE",
+12,Pantry,Artisan Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,PANTRY_BUNDLE",
+13,Fish Tank,River Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+14,Fish Tank,Lake Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+15,Fish Tank,Ocean Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+16,Fish Tank,Night Fishing Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+17,Fish Tank,Crab Pot Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+18,Fish Tank,Specialty Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+19,Boiler Room,Blacksmith's Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+20,Boiler Room,Geologist's Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+21,Boiler Room,Adventurer's Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+22,Bulletin Board,Chef's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+23,Bulletin Board,Dye Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+24,Bulletin Board,Field Research Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+25,Bulletin Board,Fodder Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+26,Bulletin Board,Enchanter's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+27,Vault,"2,500g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+28,Vault,"5,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+29,Vault,"10,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+30,Vault,"25,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+31,Abandoned JojaMart,The Missing Bundle,BUNDLE,
+32,Crafts Room,Complete Crafts Room,COMMUNITY_CENTER_ROOM,
+33,Pantry,Complete Pantry,COMMUNITY_CENTER_ROOM,
+34,Fish Tank,Complete Fish Tank,COMMUNITY_CENTER_ROOM,
+35,Boiler Room,Complete Boiler Room,COMMUNITY_CENTER_ROOM,
+36,Bulletin Board,Complete Bulletin Board,COMMUNITY_CENTER_ROOM,
+37,Vault,Complete Vault,COMMUNITY_CENTER_ROOM,
+38,Crafts Room,Forest Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+39,Fish Tank,Deep Fishing Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+40,Crafts Room,Beach Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+41,Crafts Room,Mines Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+42,Crafts Room,Desert Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+43,Crafts Room,Island Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+44,Crafts Room,Sticky Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+45,Crafts Room,Wild Medicine Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+46,Crafts Room,Quality Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+47,Boiler Room,Paleontologist's Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,BOILER_ROOM_BUNDLE",
+48,Boiler Room,Archaeologist's Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,BOILER_ROOM_BUNDLE",
+49,Pantry,Slime Farmer Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,PANTRY_BUNDLE",
+50,Pantry,Rare Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,PANTRY_BUNDLE",
+51,Pantry,Fish Farmer's Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,PANTRY_BUNDLE",
+52,Pantry,Garden Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,PANTRY_BUNDLE",
+53,Pantry,Brewer's Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,PANTRY_BUNDLE",
+54,Pantry,Orchard Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,PANTRY_BUNDLE",
+55,Pantry,Island Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,PANTRY_BUNDLE",
+56,Pantry,Agronomist's Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+57,Fish Tank,Tackle Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+58,Fish Tank,Trash Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+59,Fish Tank,Spring Fishing Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+60,Fish Tank,Summer Fishing Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+61,Fish Tank,Fall Fishing Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+62,Fish Tank,Winter Fishing Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+63,Fish Tank,Rain Fishing Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+64,Fish Tank,Quality Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+65,Fish Tank,Master Fisher's Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+66,Fish Tank,Legendary Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+67,Fish Tank,Island Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+68,Fish Tank,Master Baiter Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE",
+69,Boiler Room,Recycling Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+70,Boiler Room,Treasure Hunter's Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+71,Boiler Room,Engineer's Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+72,Boiler Room,Demolition Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+73,Bulletin Board,Children's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+74,Bulletin Board,Forager's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+75,Bulletin Board,Home Cook's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+76,Bulletin Board,Bartender's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+77,Vault,250g Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+78,Vault,500g Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+79,Vault,"1,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+80,Vault,"2,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+82,Vault,"1,500g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+83,Vault,"3,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+84,Vault,"3,500g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+85,Vault,"4,500g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+86,Vault,"6,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+87,Vault,"7,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+88,Vault,"9,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+89,Vault,"14,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+90,Vault,"15,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+91,Vault,"18,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+92,Vault,"20,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+93,Vault,"35,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+94,Vault,"40,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+95,Vault,"45,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+96,Vault,"100,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+97,Vault,Gambler's Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+98,Vault,Carnival Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+99,Vault,Walnut Hunter Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+100,Vault,Qi's Helper Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE",
+101,Pierre's General Store,Large Pack,BACKPACK,
+102,Pierre's General Store,Deluxe Pack,BACKPACK,
+103,Blacksmith Copper Upgrades,Copper Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE",
+104,Blacksmith Iron Upgrades,Iron Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE",
+105,Blacksmith Gold Upgrades,Gold Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE",
+106,Blacksmith Iridium Upgrades,Iridium Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE",
+107,Blacksmith Copper Upgrades,Copper Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE",
+108,Blacksmith Iron Upgrades,Iron Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE",
+109,Blacksmith Gold Upgrades,Gold Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE",
+110,Blacksmith Iridium Upgrades,Iridium Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE",
+111,Blacksmith Copper Upgrades,Copper Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE",
+112,Blacksmith Iron Upgrades,Iron Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE",
+113,Blacksmith Gold Upgrades,Gold Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE",
+114,Blacksmith Iridium Upgrades,Iridium Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE",
+115,Blacksmith Copper Upgrades,Copper Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE",
+116,Blacksmith Iron Upgrades,Iron Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE",
+117,Blacksmith Gold Upgrades,Gold Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE",
+118,Blacksmith Iridium Upgrades,Iridium Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE",
+119,Blacksmith Copper Upgrades,Copper Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE",
+120,Blacksmith Iron Upgrades,Iron Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE",
+121,Blacksmith Gold Upgrades,Gold Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE",
+122,Blacksmith Iridium Upgrades,Iridium Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE",
+123,Willy's Fish Shop,Purchase Training Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE",
+124,Beach,Bamboo Pole Cutscene,"FISHING_ROD_UPGRADE,TOOL_UPGRADE",
+125,Willy's Fish Shop,Purchase Fiberglass Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE",
+126,Willy's Fish Shop,Purchase Iridium Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE",
+127,Mountain,Copper Pan Cutscene,"TOOL_UPGRADE,PAN_UPGRADE",
+128,Blacksmith Iron Upgrades,Iron Pan Upgrade,"TOOL_UPGRADE,PAN_UPGRADE",
+129,Blacksmith Gold Upgrades,Gold Pan Upgrade,"TOOL_UPGRADE,PAN_UPGRADE",
+130,Blacksmith Iridium Upgrades,Iridium Pan Upgrade,"TOOL_UPGRADE,PAN_UPGRADE",
+151,Bulletin Board,Helper's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+152,Bulletin Board,Spirit's Eve Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+153,Bulletin Board,Winter Star Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+154,Bulletin Board,Calico Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+155,Pantry,Sommelier Bundle,"PANTRY_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+156,Pantry,Dry Bundle,"PANTRY_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+157,Fish Tank,Fish Smoker Bundle,"FISH_TANK_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+158,Bulletin Board,Raccoon Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+159,Crafts Room,Green Rain Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE",
+160,Fish Tank,Specific Fishing Bundle,"FISH_TANK_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE",
+201,The Mines - Floor 10,The Mines Floor 10 Treasure,"MANDATORY,THE_MINES_TREASURE",
+202,The Mines - Floor 20,The Mines Floor 20 Treasure,"MANDATORY,THE_MINES_TREASURE",
+203,The Mines - Floor 40,The Mines Floor 40 Treasure,"MANDATORY,THE_MINES_TREASURE",
+204,The Mines - Floor 50,The Mines Floor 50 Treasure,"MANDATORY,THE_MINES_TREASURE",
+205,The Mines - Floor 60,The Mines Floor 60 Treasure,"MANDATORY,THE_MINES_TREASURE",
+206,The Mines - Floor 70,The Mines Floor 70 Treasure,"MANDATORY,THE_MINES_TREASURE",
+207,The Mines - Floor 80,The Mines Floor 80 Treasure,"MANDATORY,THE_MINES_TREASURE",
+208,The Mines - Floor 90,The Mines Floor 90 Treasure,"MANDATORY,THE_MINES_TREASURE",
+209,The Mines - Floor 100,The Mines Floor 100 Treasure,"MANDATORY,THE_MINES_TREASURE",
+210,The Mines - Floor 110,The Mines Floor 110 Treasure,"MANDATORY,THE_MINES_TREASURE",
+211,The Mines - Floor 120,The Mines Floor 120 Treasure,"MANDATORY,THE_MINES_TREASURE",
+212,Quarry Mine,Grim Reaper statue,MANDATORY,
+213,The Mines,The Mines Entrance Cutscene,MANDATORY,
+214,The Mines - Floor 5,Floor 5 Elevator,ELEVATOR,
+215,The Mines - Floor 10,Floor 10 Elevator,ELEVATOR,
+216,The Mines - Floor 15,Floor 15 Elevator,ELEVATOR,
+217,The Mines - Floor 20,Floor 20 Elevator,ELEVATOR,
+218,The Mines - Floor 25,Floor 25 Elevator,ELEVATOR,
+219,The Mines - Floor 30,Floor 30 Elevator,ELEVATOR,
+220,The Mines - Floor 35,Floor 35 Elevator,ELEVATOR,
+221,The Mines - Floor 40,Floor 40 Elevator,ELEVATOR,
+222,The Mines - Floor 45,Floor 45 Elevator,ELEVATOR,
+223,The Mines - Floor 50,Floor 50 Elevator,ELEVATOR,
+224,The Mines - Floor 55,Floor 55 Elevator,ELEVATOR,
+225,The Mines - Floor 60,Floor 60 Elevator,ELEVATOR,
+226,The Mines - Floor 65,Floor 65 Elevator,ELEVATOR,
+227,The Mines - Floor 70,Floor 70 Elevator,ELEVATOR,
+228,The Mines - Floor 75,Floor 75 Elevator,ELEVATOR,
+229,The Mines - Floor 80,Floor 80 Elevator,ELEVATOR,
+230,The Mines - Floor 85,Floor 85 Elevator,ELEVATOR,
+231,The Mines - Floor 90,Floor 90 Elevator,ELEVATOR,
+232,The Mines - Floor 95,Floor 95 Elevator,ELEVATOR,
+233,The Mines - Floor 100,Floor 100 Elevator,ELEVATOR,
+234,The Mines - Floor 105,Floor 105 Elevator,ELEVATOR,
+235,The Mines - Floor 110,Floor 110 Elevator,ELEVATOR,
+236,The Mines - Floor 115,Floor 115 Elevator,ELEVATOR,
+237,The Mines - Floor 120,Floor 120 Elevator,ELEVATOR,
+250,Shipping,Demetrius's Breakthrough,MANDATORY,
+251,Volcano - Floor 10,Volcano Caldera Treasure,"GINGER_ISLAND,MANDATORY",
+301,Farm,Level 1 Farming,"FARMING_LEVEL,SKILL_LEVEL",
+302,Farm,Level 2 Farming,"FARMING_LEVEL,SKILL_LEVEL",
+303,Farm,Level 3 Farming,"FARMING_LEVEL,SKILL_LEVEL",
+304,Farm,Level 4 Farming,"FARMING_LEVEL,SKILL_LEVEL",
+305,Farm,Level 5 Farming,"FARMING_LEVEL,SKILL_LEVEL",
+306,Farm,Level 6 Farming,"FARMING_LEVEL,SKILL_LEVEL",
+307,Farm,Level 7 Farming,"FARMING_LEVEL,SKILL_LEVEL",
+308,Farm,Level 8 Farming,"FARMING_LEVEL,SKILL_LEVEL",
+309,Farm,Level 9 Farming,"FARMING_LEVEL,SKILL_LEVEL",
+310,Farm,Level 10 Farming,"FARMING_LEVEL,SKILL_LEVEL",
+311,Fishing,Level 1 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
+312,Fishing,Level 2 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
+313,Fishing,Level 3 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
+314,Fishing,Level 4 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
+315,Fishing,Level 5 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
+316,Fishing,Level 6 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
+317,Fishing,Level 7 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
+318,Fishing,Level 8 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
+319,Fishing,Level 9 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
+320,Fishing,Level 10 Fishing,"FISHING_LEVEL,SKILL_LEVEL",
+321,Forest,Level 1 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
+322,Forest,Level 2 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
+323,Forest,Level 3 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
+324,Forest,Level 4 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
+325,Forest,Level 5 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
+326,Secret Woods,Level 6 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
+327,Secret Woods,Level 7 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
+328,Secret Woods,Level 8 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
+329,Secret Woods,Level 9 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
+330,Secret Woods,Level 10 Foraging,"FORAGING_LEVEL,SKILL_LEVEL",
+331,The Mines - Floor 20,Level 1 Mining,"MINING_LEVEL,SKILL_LEVEL",
+332,The Mines - Floor 30,Level 2 Mining,"MINING_LEVEL,SKILL_LEVEL",
+333,The Mines - Floor 40,Level 3 Mining,"MINING_LEVEL,SKILL_LEVEL",
+334,The Mines - Floor 50,Level 4 Mining,"MINING_LEVEL,SKILL_LEVEL",
+335,The Mines - Floor 60,Level 5 Mining,"MINING_LEVEL,SKILL_LEVEL",
+336,The Mines - Floor 70,Level 6 Mining,"MINING_LEVEL,SKILL_LEVEL",
+337,The Mines - Floor 80,Level 7 Mining,"MINING_LEVEL,SKILL_LEVEL",
+338,The Mines - Floor 90,Level 8 Mining,"MINING_LEVEL,SKILL_LEVEL",
+339,The Mines - Floor 100,Level 9 Mining,"MINING_LEVEL,SKILL_LEVEL",
+340,The Mines - Floor 110,Level 10 Mining,"MINING_LEVEL,SKILL_LEVEL",
+341,The Mines - Floor 20,Level 1 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
+342,The Mines - Floor 30,Level 2 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
+343,The Mines - Floor 40,Level 3 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
+344,The Mines - Floor 50,Level 4 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
+345,The Mines - Floor 60,Level 5 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
+346,The Mines - Floor 70,Level 6 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
+347,The Mines - Floor 80,Level 7 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
+348,The Mines - Floor 90,Level 8 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
+349,The Mines - Floor 100,Level 9 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
+350,The Mines - Floor 110,Level 10 Combat,"COMBAT_LEVEL,SKILL_LEVEL",
+351,Mastery Cave,Farming Mastery,"MASTERY_LEVEL,SKILL_LEVEL",
+352,Mastery Cave,Fishing Mastery,"MASTERY_LEVEL,SKILL_LEVEL",
+353,Mastery Cave,Foraging Mastery,"MASTERY_LEVEL,SKILL_LEVEL",
+354,Mastery Cave,Mining Mastery,"MASTERY_LEVEL,SKILL_LEVEL",
+355,Mastery Cave,Combat Mastery,"MASTERY_LEVEL,SKILL_LEVEL",
+401,Carpenter Shop,Coop Blueprint,BUILDING_BLUEPRINT,
+402,Carpenter Shop,Big Coop Blueprint,BUILDING_BLUEPRINT,
+403,Carpenter Shop,Deluxe Coop Blueprint,BUILDING_BLUEPRINT,
+404,Carpenter Shop,Barn Blueprint,BUILDING_BLUEPRINT,
+405,Carpenter Shop,Big Barn Blueprint,BUILDING_BLUEPRINT,
+406,Carpenter Shop,Deluxe Barn Blueprint,BUILDING_BLUEPRINT,
+407,Carpenter Shop,Well Blueprint,BUILDING_BLUEPRINT,
+408,Carpenter Shop,Silo Blueprint,BUILDING_BLUEPRINT,
+409,Carpenter Shop,Mill Blueprint,BUILDING_BLUEPRINT,
+410,Carpenter Shop,Shed Blueprint,BUILDING_BLUEPRINT,
+411,Carpenter Shop,Big Shed Blueprint,BUILDING_BLUEPRINT,
+412,Carpenter Shop,Fish Pond Blueprint,BUILDING_BLUEPRINT,
+413,Carpenter Shop,Stable Blueprint,BUILDING_BLUEPRINT,
+414,Carpenter Shop,Slime Hutch Blueprint,BUILDING_BLUEPRINT,
+415,Carpenter Shop,Shipping Bin Blueprint,BUILDING_BLUEPRINT,
+416,Carpenter Shop,Kitchen Blueprint,BUILDING_BLUEPRINT,
+417,Carpenter Shop,Kids Room Blueprint,BUILDING_BLUEPRINT,
+418,Carpenter Shop,Cellar Blueprint,BUILDING_BLUEPRINT,
+501,Town,Introductions,"STORY_QUEST",
+502,Town,How To Win Friends,"STORY_QUEST",
+503,Farm,Getting Started,"STORY_QUEST",
+504,Farm,Raising Animals,"STORY_QUEST",
+505,Farm,Advancement,"STORY_QUEST",
+506,Museum,Archaeology,"STORY_QUEST",
+507,Wizard Tower,Meet The Wizard,"STORY_QUEST",
+508,The Mines - Floor 5,Forging Ahead,"STORY_QUEST",
+509,The Mines - Floor 10,Smelting,"STORY_QUEST",
+510,The Mines - Floor 15,Initiation,"STORY_QUEST",
+511,Mountain,Robin's Lost Axe,"STORY_QUEST",
+512,Town,Jodi's Request,"STORY_QUEST",
+513,Town,"Mayor's ""Shorts""","STORY_QUEST",
+514,Mountain,Blackberry Basket,"STORY_QUEST",
+515,Marnie's Ranch,Marnie's Request,"STORY_QUEST",
+516,Town,Pam Is Thirsty,"STORY_QUEST",
+517,Wizard Tower,A Dark Reagent,"STORY_QUEST",
+518,Forest,Cow's Delight,"STORY_QUEST",
+519,Skull Cavern Entrance,The Skull Key,"STORY_QUEST",
+520,Mountain,Crop Research,"STORY_QUEST",
+521,Alex's House,Knee Therapy,"STORY_QUEST",
+522,Mountain,Robin's Request,"STORY_QUEST",
+523,Skull Cavern Floor 25,Qi's Challenge,"STORY_QUEST",
+524,Desert,The Mysterious Qi,"STORY_QUEST",
+525,Town,Carving Pumpkins,"STORY_QUEST",
+526,Town,A Winter Mystery,"STORY_QUEST",
+527,Secret Woods,Strange Note,"STORY_QUEST",
+528,Skull Cavern Floor 100,Cryptic Note,"STORY_QUEST",
+529,Town,Fresh Fruit,"STORY_QUEST",
+530,Mountain,Aquatic Research,"STORY_QUEST",
+531,Town,A Soldier's Star,"STORY_QUEST",
+532,Town,Mayor's Need,"STORY_QUEST",
+533,Saloon,Wanted: Lobster,"STORY_QUEST",
+534,Town,Pam Needs Juice,"STORY_QUEST",
+535,Sam's House,Fish Casserole,"STORY_QUEST",
+536,Beach,Catch A Squid,"STORY_QUEST",
+537,Saloon,Fish Stew,"STORY_QUEST",
+538,Pierre's General Store,Pierre's Notice,"STORY_QUEST",
+539,Clint's Blacksmith,Clint's Attempt,"STORY_QUEST",
+540,Town,A Favor For Clint,"STORY_QUEST",
+541,Wizard Tower,Staff Of Power,"STORY_QUEST",
+542,Town,Granny's Gift,"STORY_QUEST",
+543,Desert,Exotic Spirits,"STORY_QUEST",
+544,Fishing,Catch a Lingcod,"STORY_QUEST",
+545,Island West,The Pirate's Wife,"GINGER_ISLAND,STORY_QUEST",
+546,Mutant Bug Lair,Dark Talisman,"STORY_QUEST",
+547,Witch's Swamp,Goblin Problem,"STORY_QUEST",
+548,Witch's Hut,Magic Ink,"STORY_QUEST",
+549,Forest,The Giant Stump,"STORY_QUEST",
+550,Farm,Feeding Animals,"STORY_QUEST",
+601,JotPK World 1,JotPK: Boots 1,"ARCADE_MACHINE,JOTPK",
+602,JotPK World 1,JotPK: Boots 2,"ARCADE_MACHINE,JOTPK",
+603,JotPK World 1,JotPK: Gun 1,"ARCADE_MACHINE,JOTPK",
+604,JotPK World 2,JotPK: Gun 2,"ARCADE_MACHINE,JOTPK",
+605,JotPK World 2,JotPK: Gun 3,"ARCADE_MACHINE,JOTPK",
+606,JotPK World 3,JotPK: Super Gun,"ARCADE_MACHINE,JOTPK",
+607,JotPK World 1,JotPK: Ammo 1,"ARCADE_MACHINE,JOTPK",
+608,JotPK World 2,JotPK: Ammo 2,"ARCADE_MACHINE,JOTPK",
+609,JotPK World 3,JotPK: Ammo 3,"ARCADE_MACHINE,JOTPK",
+610,JotPK World 1,JotPK: Cowboy 1,"ARCADE_MACHINE,JOTPK",
+611,JotPK World 2,JotPK: Cowboy 2,"ARCADE_MACHINE,JOTPK",
+612,Junimo Kart 1,Junimo Kart: Crumble Cavern,"ARCADE_MACHINE,JUNIMO_KART",
+613,Junimo Kart 1,Junimo Kart: Slippery Slopes,"ARCADE_MACHINE,JUNIMO_KART",
+614,Junimo Kart 2,Junimo Kart: Secret Level,"ARCADE_MACHINE,JUNIMO_KART",
+615,Junimo Kart 2,Junimo Kart: The Gem Sea Giant,"ARCADE_MACHINE,JUNIMO_KART",
+616,Junimo Kart 2,Junimo Kart: Slomp's Stomp,"ARCADE_MACHINE,JUNIMO_KART",
+617,Junimo Kart 2,Junimo Kart: Ghastly Galleon,"ARCADE_MACHINE,JUNIMO_KART",
+618,Junimo Kart 3,Junimo Kart: Glowshroom Grotto,"ARCADE_MACHINE,JUNIMO_KART",
+619,Junimo Kart 3,Junimo Kart: Red Hot Rollercoaster,"ARCADE_MACHINE,JUNIMO_KART",
+620,JotPK World 3,Journey of the Prairie King Victory,"ARCADE_MACHINE_VICTORY,JOTPK",
+621,Junimo Kart 3,Junimo Kart: Sunset Speedway (Victory),"ARCADE_MACHINE_VICTORY,JUNIMO_KART",
+701,Secret Woods,Old Master Cannoli,MANDATORY,
+702,Beach,Beach Bridge Repair,MANDATORY,
+703,Desert,Galaxy Sword Shrine,MANDATORY,
+704,Farmhouse,Have a Baby,BABY,
+705,Farmhouse,Have Another Baby,BABY,
+706,Farmhouse,Spouse Stardrop,,
+707,Sewer,Krobus Stardrop,MANDATORY,
+708,Forest,Pot Of Gold,MANDATORY,
+801,Forest,Help Wanted: Gathering 1,HELP_WANTED,
+802,Forest,Help Wanted: Gathering 2,HELP_WANTED,
+803,Forest,Help Wanted: Gathering 3,HELP_WANTED,
+804,Forest,Help Wanted: Gathering 4,HELP_WANTED,
+805,Forest,Help Wanted: Gathering 5,HELP_WANTED,
+806,Forest,Help Wanted: Gathering 6,HELP_WANTED,
+807,Forest,Help Wanted: Gathering 7,HELP_WANTED,
+808,Forest,Help Wanted: Gathering 8,HELP_WANTED,
+811,The Mines - Floor 5,Help Wanted: Slay Monsters 1,HELP_WANTED,
+812,The Mines - Floor 15,Help Wanted: Slay Monsters 2,HELP_WANTED,
+813,The Mines - Floor 25,Help Wanted: Slay Monsters 3,HELP_WANTED,
+814,The Mines - Floor 35,Help Wanted: Slay Monsters 4,HELP_WANTED,
+815,The Mines - Floor 45,Help Wanted: Slay Monsters 5,HELP_WANTED,
+816,The Mines - Floor 55,Help Wanted: Slay Monsters 6,HELP_WANTED,
+817,The Mines - Floor 65,Help Wanted: Slay Monsters 7,HELP_WANTED,
+818,The Mines - Floor 75,Help Wanted: Slay Monsters 8,HELP_WANTED,
+821,Fishing,Help Wanted: Fishing 1,HELP_WANTED,
+822,Fishing,Help Wanted: Fishing 2,HELP_WANTED,
+823,Fishing,Help Wanted: Fishing 3,HELP_WANTED,
+824,Fishing,Help Wanted: Fishing 4,HELP_WANTED,
+825,Fishing,Help Wanted: Fishing 5,HELP_WANTED,
+826,Fishing,Help Wanted: Fishing 6,HELP_WANTED,
+827,Fishing,Help Wanted: Fishing 7,HELP_WANTED,
+828,Fishing,Help Wanted: Fishing 8,HELP_WANTED,
+841,Town,Help Wanted: Item Delivery 1,HELP_WANTED,
+842,Town,Help Wanted: Item Delivery 2,HELP_WANTED,
+843,Town,Help Wanted: Item Delivery 3,HELP_WANTED,
+844,Town,Help Wanted: Item Delivery 4,HELP_WANTED,
+845,Town,Help Wanted: Item Delivery 5,HELP_WANTED,
+846,Town,Help Wanted: Item Delivery 6,HELP_WANTED,
+847,Town,Help Wanted: Item Delivery 7,HELP_WANTED,
+848,Town,Help Wanted: Item Delivery 8,HELP_WANTED,
+849,Town,Help Wanted: Item Delivery 9,HELP_WANTED,
+850,Town,Help Wanted: Item Delivery 10,HELP_WANTED,
+851,Town,Help Wanted: Item Delivery 11,HELP_WANTED,
+852,Town,Help Wanted: Item Delivery 12,HELP_WANTED,
+853,Town,Help Wanted: Item Delivery 13,HELP_WANTED,
+854,Town,Help Wanted: Item Delivery 14,HELP_WANTED,
+855,Town,Help Wanted: Item Delivery 15,HELP_WANTED,
+856,Town,Help Wanted: Item Delivery 16,HELP_WANTED,
+857,Town,Help Wanted: Item Delivery 17,HELP_WANTED,
+858,Town,Help Wanted: Item Delivery 18,HELP_WANTED,
+859,Town,Help Wanted: Item Delivery 19,HELP_WANTED,
+860,Town,Help Wanted: Item Delivery 20,HELP_WANTED,
+861,Town,Help Wanted: Item Delivery 21,HELP_WANTED,
+862,Town,Help Wanted: Item Delivery 22,HELP_WANTED,
+863,Town,Help Wanted: Item Delivery 23,HELP_WANTED,
+864,Town,Help Wanted: Item Delivery 24,HELP_WANTED,
+865,Town,Help Wanted: Item Delivery 25,HELP_WANTED,
+866,Town,Help Wanted: Item Delivery 26,HELP_WANTED,
+867,Town,Help Wanted: Item Delivery 27,HELP_WANTED,
+868,Town,Help Wanted: Item Delivery 28,HELP_WANTED,
+869,Town,Help Wanted: Item Delivery 29,HELP_WANTED,
+870,Town,Help Wanted: Item Delivery 30,HELP_WANTED,
+871,Town,Help Wanted: Item Delivery 31,HELP_WANTED,
+872,Town,Help Wanted: Item Delivery 32,HELP_WANTED,
+901,Traveling Cart Sunday,Traveling Merchant Sunday Item 1,"MANDATORY,TRAVELING_MERCHANT",
+902,Traveling Cart Sunday,Traveling Merchant Sunday Item 2,"MANDATORY,TRAVELING_MERCHANT",
+903,Traveling Cart Sunday,Traveling Merchant Sunday Item 3,"MANDATORY,TRAVELING_MERCHANT",
+911,Traveling Cart Monday,Traveling Merchant Monday Item 1,"MANDATORY,TRAVELING_MERCHANT",
+912,Traveling Cart Monday,Traveling Merchant Monday Item 2,"MANDATORY,TRAVELING_MERCHANT",
+913,Traveling Cart Monday,Traveling Merchant Monday Item 3,"MANDATORY,TRAVELING_MERCHANT",
+921,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 1,"MANDATORY,TRAVELING_MERCHANT",
+922,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 2,"MANDATORY,TRAVELING_MERCHANT",
+923,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 3,"MANDATORY,TRAVELING_MERCHANT",
+931,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 1,"MANDATORY,TRAVELING_MERCHANT",
+932,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 2,"MANDATORY,TRAVELING_MERCHANT",
+933,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 3,"MANDATORY,TRAVELING_MERCHANT",
+941,Traveling Cart Thursday,Traveling Merchant Thursday Item 1,"MANDATORY,TRAVELING_MERCHANT",
+942,Traveling Cart Thursday,Traveling Merchant Thursday Item 2,"MANDATORY,TRAVELING_MERCHANT",
+943,Traveling Cart Thursday,Traveling Merchant Thursday Item 3,"MANDATORY,TRAVELING_MERCHANT",
+951,Traveling Cart Friday,Traveling Merchant Friday Item 1,"MANDATORY,TRAVELING_MERCHANT",
+952,Traveling Cart Friday,Traveling Merchant Friday Item 2,"MANDATORY,TRAVELING_MERCHANT",
+953,Traveling Cart Friday,Traveling Merchant Friday Item 3,"MANDATORY,TRAVELING_MERCHANT",
+961,Traveling Cart Saturday,Traveling Merchant Saturday Item 1,"MANDATORY,TRAVELING_MERCHANT",
+962,Traveling Cart Saturday,Traveling Merchant Saturday Item 2,"MANDATORY,TRAVELING_MERCHANT",
+963,Traveling Cart Saturday,Traveling Merchant Saturday Item 3,"MANDATORY,TRAVELING_MERCHANT",
+1001,Fishing,Fishsanity: Carp,FISHSANITY,
+1002,Fishing,Fishsanity: Herring,FISHSANITY,
+1003,Fishing,Fishsanity: Smallmouth Bass,FISHSANITY,
+1004,Fishing,Fishsanity: Anchovy,FISHSANITY,
+1005,Fishing,Fishsanity: Sardine,FISHSANITY,
+1006,Fishing,Fishsanity: Sunfish,FISHSANITY,
+1007,Fishing,Fishsanity: Perch,FISHSANITY,
+1008,Fishing,Fishsanity: Chub,FISHSANITY,
+1009,Fishing,Fishsanity: Bream,FISHSANITY,
+1010,Fishing,Fishsanity: Red Snapper,FISHSANITY,
+1011,Fishing,Fishsanity: Sea Cucumber,FISHSANITY,
+1012,Fishing,Fishsanity: Rainbow Trout,FISHSANITY,
+1013,Fishing,Fishsanity: Walleye,FISHSANITY,
+1014,Fishing,Fishsanity: Shad,FISHSANITY,
+1015,Fishing,Fishsanity: Bullhead,FISHSANITY,
+1016,Fishing,Fishsanity: Largemouth Bass,FISHSANITY,
+1017,Fishing,Fishsanity: Salmon,FISHSANITY,
+1018,Fishing,Fishsanity: Ghostfish,FISHSANITY,
+1019,Fishing,Fishsanity: Tilapia,FISHSANITY,
+1020,Fishing,Fishsanity: Woodskip,FISHSANITY,
+1021,Fishing,Fishsanity: Flounder,FISHSANITY,
+1022,Fishing,Fishsanity: Halibut,FISHSANITY,
+1023,Fishing,Fishsanity: Lionfish,"FISHSANITY,GINGER_ISLAND",
+1024,Fishing,Fishsanity: Slimejack,FISHSANITY,
+1025,Fishing,Fishsanity: Midnight Carp,FISHSANITY,
+1026,Fishing,Fishsanity: Red Mullet,FISHSANITY,
+1027,Fishing,Fishsanity: Pike,FISHSANITY,
+1028,Fishing,Fishsanity: Tiger Trout,FISHSANITY,
+1029,Fishing,Fishsanity: Blue Discus,"FISHSANITY,GINGER_ISLAND",
+1030,Fishing,Fishsanity: Albacore,FISHSANITY,
+1031,Fishing,Fishsanity: Sandfish,FISHSANITY,
+1032,Fishing,Fishsanity: Stonefish,FISHSANITY,
+1033,Fishing,Fishsanity: Tuna,FISHSANITY,
+1034,Fishing,Fishsanity: Eel,FISHSANITY,
+1035,Fishing,Fishsanity: Catfish,FISHSANITY,
+1036,Fishing,Fishsanity: Squid,FISHSANITY,
+1037,Fishing,Fishsanity: Sturgeon,FISHSANITY,
+1038,Fishing,Fishsanity: Dorado,FISHSANITY,
+1039,Fishing,Fishsanity: Pufferfish,FISHSANITY,
+1040,Fishing,Fishsanity: Void Salmon,FISHSANITY,
+1041,Fishing,Fishsanity: Super Cucumber,FISHSANITY,
+1042,Fishing,Fishsanity: Stingray,"FISHSANITY,GINGER_ISLAND",
+1043,Fishing,Fishsanity: Ice Pip,FISHSANITY,
+1044,Fishing,Fishsanity: Lingcod,FISHSANITY,
+1045,Desert,Fishsanity: Scorpion Carp,FISHSANITY,
+1046,Fishing,Fishsanity: Lava Eel,FISHSANITY,
+1047,Fishing,Fishsanity: Octopus,FISHSANITY,
+1048,Fishing,Fishsanity: Midnight Squid,FISHSANITY,
+1049,Fishing,Fishsanity: Spook Fish,FISHSANITY,
+1050,Fishing,Fishsanity: Blobfish,FISHSANITY,
+1051,Fishing,Fishsanity: Crimsonfish,FISHSANITY,
+1052,Fishing,Fishsanity: Angler,FISHSANITY,
+1053,Fishing,Fishsanity: Legend,FISHSANITY,
+1054,Fishing,Fishsanity: Glacierfish,FISHSANITY,
+1055,Fishing,Fishsanity: Mutant Carp,FISHSANITY,
+1056,Town,Fishsanity: Crayfish,FISHSANITY,
+1057,Town,Fishsanity: Snail,FISHSANITY,
+1058,Town,Fishsanity: Periwinkle,FISHSANITY,
+1059,Beach,Fishsanity: Lobster,FISHSANITY,
+1060,Beach,Fishsanity: Clam,FISHSANITY,
+1061,Beach,Fishsanity: Crab,FISHSANITY,
+1062,Beach,Fishsanity: Cockle,FISHSANITY,
+1063,Beach,Fishsanity: Mussel,FISHSANITY,
+1064,Beach,Fishsanity: Shrimp,FISHSANITY,
+1065,Beach,Fishsanity: Oyster,FISHSANITY,
+1066,Beach,Fishsanity: Son of Crimsonfish,"FISHSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS",
+1067,Beach,Fishsanity: Glacierfish Jr.,"FISHSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS",
+1068,Beach,Fishsanity: Legend II,"FISHSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS",
+1069,Beach,Fishsanity: Ms. Angler,"FISHSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS",
+1070,Beach,Fishsanity: Radioactive Carp,"FISHSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS",
+1071,Fishing,Fishsanity: Goby,FISHSANITY,
+1100,Museum,Museumsanity: 5 Donations,MUSEUM_MILESTONES,
+1101,Museum,Museumsanity: 10 Donations,MUSEUM_MILESTONES,
+1102,Museum,Museumsanity: 15 Donations,MUSEUM_MILESTONES,
+1103,Museum,Museumsanity: 20 Donations,MUSEUM_MILESTONES,
+1104,Museum,Museumsanity: 25 Donations,MUSEUM_MILESTONES,
+1105,Museum,Museumsanity: 30 Donations,MUSEUM_MILESTONES,
+1106,Museum,Museumsanity: 35 Donations,MUSEUM_MILESTONES,
+1107,Museum,Museumsanity: 40 Donations,MUSEUM_MILESTONES,
+1108,Museum,Museumsanity: 50 Donations,MUSEUM_MILESTONES,
+1109,Museum,Museumsanity: 60 Donations,MUSEUM_MILESTONES,
+1110,Museum,Museumsanity: 70 Donations,MUSEUM_MILESTONES,
+1111,Museum,Museumsanity: 80 Donations,MUSEUM_MILESTONES,
+1112,Museum,Museumsanity: 90 Donations,MUSEUM_MILESTONES,
+1113,Museum,Museumsanity: 95 Donations,MUSEUM_MILESTONES,
+1114,Museum,Museumsanity: 11 Minerals,MUSEUM_MILESTONES,
+1115,Museum,Museumsanity: 21 Minerals,MUSEUM_MILESTONES,
+1116,Museum,Museumsanity: 31 Minerals,MUSEUM_MILESTONES,
+1117,Museum,Museumsanity: 41 Minerals,MUSEUM_MILESTONES,
+1118,Museum,Museumsanity: 50 Minerals,MUSEUM_MILESTONES,
+1119,Museum,Museumsanity: 3 Artifacts,MUSEUM_MILESTONES,
+1120,Museum,Museumsanity: 6 Artifacts,MUSEUM_MILESTONES,
+1121,Museum,Museumsanity: 9 Artifacts,MUSEUM_MILESTONES,
+1122,Museum,Museumsanity: 11 Artifacts,MUSEUM_MILESTONES,
+1123,Museum,Museumsanity: 15 Artifacts,MUSEUM_MILESTONES,
+1124,Museum,Museumsanity: 20 Artifacts,MUSEUM_MILESTONES,
+1125,Museum,Museumsanity: Dwarf Scrolls,MUSEUM_MILESTONES,
+1126,Museum,Museumsanity: Skeleton Front,MUSEUM_MILESTONES,
+1127,Museum,Museumsanity: Skeleton Middle,MUSEUM_MILESTONES,
+1128,Museum,Museumsanity: Skeleton Back,MUSEUM_MILESTONES,
+1201,Museum,Museumsanity: Dwarf Scroll I,MUSEUM_DONATIONS,
+1202,Museum,Museumsanity: Dwarf Scroll II,MUSEUM_DONATIONS,
+1203,Museum,Museumsanity: Dwarf Scroll III,MUSEUM_DONATIONS,
+1204,Museum,Museumsanity: Dwarf Scroll IV,MUSEUM_DONATIONS,
+1205,Museum,Museumsanity: Chipped Amphora,MUSEUM_DONATIONS,
+1206,Museum,Museumsanity: Arrowhead,MUSEUM_DONATIONS,
+1207,Museum,Museumsanity: Ancient Doll,MUSEUM_DONATIONS,
+1208,Museum,Museumsanity: Elvish Jewelry,MUSEUM_DONATIONS,
+1209,Museum,Museumsanity: Chewing Stick,MUSEUM_DONATIONS,
+1210,Museum,Museumsanity: Ornamental Fan,MUSEUM_DONATIONS,
+1211,Museum,Museumsanity: Dinosaur Egg,MUSEUM_DONATIONS,
+1212,Museum,Museumsanity: Rare Disc,MUSEUM_DONATIONS,
+1213,Museum,Museumsanity: Ancient Sword,MUSEUM_DONATIONS,
+1214,Museum,Museumsanity: Rusty Spoon,MUSEUM_DONATIONS,
+1215,Museum,Museumsanity: Rusty Spur,MUSEUM_DONATIONS,
+1216,Museum,Museumsanity: Rusty Cog,MUSEUM_DONATIONS,
+1217,Museum,Museumsanity: Chicken Statue,MUSEUM_DONATIONS,
+1218,Museum,Museumsanity: Ancient Seed,"MUSEUM_DONATIONS,MUSEUM_MILESTONES",
+1219,Museum,Museumsanity: Prehistoric Tool,MUSEUM_DONATIONS,
+1220,Museum,Museumsanity: Dried Starfish,MUSEUM_DONATIONS,
+1221,Museum,Museumsanity: Anchor,MUSEUM_DONATIONS,
+1222,Museum,Museumsanity: Glass Shards,MUSEUM_DONATIONS,
+1223,Museum,Museumsanity: Bone Flute,MUSEUM_DONATIONS,
+1224,Museum,Museumsanity: Prehistoric Handaxe,MUSEUM_DONATIONS,
+1225,Museum,Museumsanity: Dwarvish Helm,MUSEUM_DONATIONS,
+1226,Museum,Museumsanity: Dwarf Gadget,MUSEUM_DONATIONS,
+1227,Museum,Museumsanity: Ancient Drum,MUSEUM_DONATIONS,
+1228,Museum,Museumsanity: Golden Mask,MUSEUM_DONATIONS,
+1229,Museum,Museumsanity: Golden Relic,MUSEUM_DONATIONS,
+1230,Museum,Museumsanity: Strange Doll (Green),MUSEUM_DONATIONS,
+1231,Museum,Museumsanity: Strange Doll,MUSEUM_DONATIONS,
+1232,Museum,Museumsanity: Prehistoric Scapula,MUSEUM_DONATIONS,
+1233,Museum,Museumsanity: Prehistoric Tibia,MUSEUM_DONATIONS,
+1234,Museum,Museumsanity: Prehistoric Skull,MUSEUM_DONATIONS,
+1235,Museum,Museumsanity: Skeletal Hand,MUSEUM_DONATIONS,
+1236,Museum,Museumsanity: Prehistoric Rib,MUSEUM_DONATIONS,
+1237,Museum,Museumsanity: Prehistoric Vertebra,MUSEUM_DONATIONS,
+1238,Museum,Museumsanity: Skeletal Tail,MUSEUM_DONATIONS,
+1239,Museum,Museumsanity: Nautilus Fossil,MUSEUM_DONATIONS,
+1240,Museum,Museumsanity: Amphibian Fossil,MUSEUM_DONATIONS,
+1241,Museum,Museumsanity: Palm Fossil,MUSEUM_DONATIONS,
+1242,Museum,Museumsanity: Trilobite,MUSEUM_DONATIONS,
+1243,Museum,Museumsanity: Quartz,MUSEUM_DONATIONS,
+1244,Museum,Museumsanity: Fire Quartz,MUSEUM_DONATIONS,
+1245,Museum,Museumsanity: Frozen Tear,MUSEUM_DONATIONS,
+1246,Museum,Museumsanity: Earth Crystal,MUSEUM_DONATIONS,
+1247,Museum,Museumsanity: Emerald,MUSEUM_DONATIONS,
+1248,Museum,Museumsanity: Aquamarine,MUSEUM_DONATIONS,
+1249,Museum,Museumsanity: Ruby,MUSEUM_DONATIONS,
+1250,Museum,Museumsanity: Amethyst,MUSEUM_DONATIONS,
+1251,Museum,Museumsanity: Topaz,MUSEUM_DONATIONS,
+1252,Museum,Museumsanity: Jade,MUSEUM_DONATIONS,
+1253,Museum,Museumsanity: Diamond,MUSEUM_DONATIONS,
+1254,Museum,Museumsanity: Prismatic Shard,MUSEUM_DONATIONS,
+1255,Museum,Museumsanity: Alamite,MUSEUM_DONATIONS,
+1256,Museum,Museumsanity: Bixite,MUSEUM_DONATIONS,
+1257,Museum,Museumsanity: Baryte,MUSEUM_DONATIONS,
+1258,Museum,Museumsanity: Aerinite,MUSEUM_DONATIONS,
+1259,Museum,Museumsanity: Calcite,MUSEUM_DONATIONS,
+1260,Museum,Museumsanity: Dolomite,MUSEUM_DONATIONS,
+1261,Museum,Museumsanity: Esperite,MUSEUM_DONATIONS,
+1262,Museum,Museumsanity: Fluorapatite,MUSEUM_DONATIONS,
+1263,Museum,Museumsanity: Geminite,MUSEUM_DONATIONS,
+1264,Museum,Museumsanity: Helvite,MUSEUM_DONATIONS,
+1265,Museum,Museumsanity: Jamborite,MUSEUM_DONATIONS,
+1266,Museum,Museumsanity: Jagoite,MUSEUM_DONATIONS,
+1267,Museum,Museumsanity: Kyanite,MUSEUM_DONATIONS,
+1268,Museum,Museumsanity: Lunarite,MUSEUM_DONATIONS,
+1269,Museum,Museumsanity: Malachite,MUSEUM_DONATIONS,
+1270,Museum,Museumsanity: Neptunite,MUSEUM_DONATIONS,
+1271,Museum,Museumsanity: Lemon Stone,MUSEUM_DONATIONS,
+1272,Museum,Museumsanity: Nekoite,MUSEUM_DONATIONS,
+1273,Museum,Museumsanity: Orpiment,MUSEUM_DONATIONS,
+1274,Museum,Museumsanity: Petrified Slime,MUSEUM_DONATIONS,
+1275,Museum,Museumsanity: Thunder Egg,MUSEUM_DONATIONS,
+1276,Museum,Museumsanity: Pyrite,MUSEUM_DONATIONS,
+1277,Museum,Museumsanity: Ocean Stone,MUSEUM_DONATIONS,
+1278,Museum,Museumsanity: Ghost Crystal,MUSEUM_DONATIONS,
+1279,Museum,Museumsanity: Tigerseye,MUSEUM_DONATIONS,
+1280,Museum,Museumsanity: Jasper,MUSEUM_DONATIONS,
+1281,Museum,Museumsanity: Opal,MUSEUM_DONATIONS,
+1282,Museum,Museumsanity: Fire Opal,MUSEUM_DONATIONS,
+1283,Museum,Museumsanity: Celestine,MUSEUM_DONATIONS,
+1284,Museum,Museumsanity: Marble,MUSEUM_DONATIONS,
+1285,Museum,Museumsanity: Sandstone,MUSEUM_DONATIONS,
+1286,Museum,Museumsanity: Granite,MUSEUM_DONATIONS,
+1287,Museum,Museumsanity: Basalt,MUSEUM_DONATIONS,
+1288,Museum,Museumsanity: Limestone,MUSEUM_DONATIONS,
+1289,Museum,Museumsanity: Soapstone,MUSEUM_DONATIONS,
+1290,Museum,Museumsanity: Hematite,MUSEUM_DONATIONS,
+1291,Museum,Museumsanity: Mudstone,MUSEUM_DONATIONS,
+1292,Museum,Museumsanity: Obsidian,MUSEUM_DONATIONS,
+1293,Museum,Museumsanity: Slate,MUSEUM_DONATIONS,
+1294,Museum,Museumsanity: Fairy Stone,MUSEUM_DONATIONS,
+1295,Museum,Museumsanity: Star Shards,MUSEUM_DONATIONS,
+1301,Alex's House,Friendsanity: Alex 1 <3,FRIENDSANITY,
+1302,Alex's House,Friendsanity: Alex 2 <3,FRIENDSANITY,
+1303,Alex's House,Friendsanity: Alex 3 <3,FRIENDSANITY,
+1304,Alex's House,Friendsanity: Alex 4 <3,FRIENDSANITY,
+1305,Alex's House,Friendsanity: Alex 5 <3,FRIENDSANITY,
+1306,Alex's House,Friendsanity: Alex 6 <3,FRIENDSANITY,
+1307,Alex's House,Friendsanity: Alex 7 <3,FRIENDSANITY,
+1308,Alex's House,Friendsanity: Alex 8 <3,FRIENDSANITY,
+1309,Alex's House,Friendsanity: Alex 9 <3,FRIENDSANITY,
+1310,Alex's House,Friendsanity: Alex 10 <3,FRIENDSANITY,
+1311,Alex's House,Friendsanity: Alex 11 <3,FRIENDSANITY,
+1312,Alex's House,Friendsanity: Alex 12 <3,FRIENDSANITY,
+1313,Alex's House,Friendsanity: Alex 13 <3,FRIENDSANITY,
+1314,Alex's House,Friendsanity: Alex 14 <3,FRIENDSANITY,
+1315,Elliott's House,Friendsanity: Elliott 1 <3,FRIENDSANITY,
+1316,Elliott's House,Friendsanity: Elliott 2 <3,FRIENDSANITY,
+1317,Elliott's House,Friendsanity: Elliott 3 <3,FRIENDSANITY,
+1318,Elliott's House,Friendsanity: Elliott 4 <3,FRIENDSANITY,
+1319,Elliott's House,Friendsanity: Elliott 5 <3,FRIENDSANITY,
+1320,Elliott's House,Friendsanity: Elliott 6 <3,FRIENDSANITY,
+1321,Elliott's House,Friendsanity: Elliott 7 <3,FRIENDSANITY,
+1322,Elliott's House,Friendsanity: Elliott 8 <3,FRIENDSANITY,
+1323,Elliott's House,Friendsanity: Elliott 9 <3,FRIENDSANITY,
+1324,Elliott's House,Friendsanity: Elliott 10 <3,FRIENDSANITY,
+1325,Elliott's House,Friendsanity: Elliott 11 <3,FRIENDSANITY,
+1326,Elliott's House,Friendsanity: Elliott 12 <3,FRIENDSANITY,
+1327,Elliott's House,Friendsanity: Elliott 13 <3,FRIENDSANITY,
+1328,Elliott's House,Friendsanity: Elliott 14 <3,FRIENDSANITY,
+1329,Hospital,Friendsanity: Harvey 1 <3,FRIENDSANITY,
+1330,Hospital,Friendsanity: Harvey 2 <3,FRIENDSANITY,
+1331,Hospital,Friendsanity: Harvey 3 <3,FRIENDSANITY,
+1332,Hospital,Friendsanity: Harvey 4 <3,FRIENDSANITY,
+1333,Hospital,Friendsanity: Harvey 5 <3,FRIENDSANITY,
+1334,Hospital,Friendsanity: Harvey 6 <3,FRIENDSANITY,
+1335,Hospital,Friendsanity: Harvey 7 <3,FRIENDSANITY,
+1336,Hospital,Friendsanity: Harvey 8 <3,FRIENDSANITY,
+1337,Hospital,Friendsanity: Harvey 9 <3,FRIENDSANITY,
+1338,Hospital,Friendsanity: Harvey 10 <3,FRIENDSANITY,
+1339,Hospital,Friendsanity: Harvey 11 <3,FRIENDSANITY,
+1340,Hospital,Friendsanity: Harvey 12 <3,FRIENDSANITY,
+1341,Hospital,Friendsanity: Harvey 13 <3,FRIENDSANITY,
+1342,Hospital,Friendsanity: Harvey 14 <3,FRIENDSANITY,
+1343,Sam's House,Friendsanity: Sam 1 <3,FRIENDSANITY,
+1344,Sam's House,Friendsanity: Sam 2 <3,FRIENDSANITY,
+1345,Sam's House,Friendsanity: Sam 3 <3,FRIENDSANITY,
+1346,Sam's House,Friendsanity: Sam 4 <3,FRIENDSANITY,
+1347,Sam's House,Friendsanity: Sam 5 <3,FRIENDSANITY,
+1348,Sam's House,Friendsanity: Sam 6 <3,FRIENDSANITY,
+1349,Sam's House,Friendsanity: Sam 7 <3,FRIENDSANITY,
+1350,Sam's House,Friendsanity: Sam 8 <3,FRIENDSANITY,
+1351,Sam's House,Friendsanity: Sam 9 <3,FRIENDSANITY,
+1352,Sam's House,Friendsanity: Sam 10 <3,FRIENDSANITY,
+1353,Sam's House,Friendsanity: Sam 11 <3,FRIENDSANITY,
+1354,Sam's House,Friendsanity: Sam 12 <3,FRIENDSANITY,
+1355,Sam's House,Friendsanity: Sam 13 <3,FRIENDSANITY,
+1356,Sam's House,Friendsanity: Sam 14 <3,FRIENDSANITY,
+1357,Carpenter Shop,Friendsanity: Sebastian 1 <3,FRIENDSANITY,
+1358,Carpenter Shop,Friendsanity: Sebastian 2 <3,FRIENDSANITY,
+1359,Carpenter Shop,Friendsanity: Sebastian 3 <3,FRIENDSANITY,
+1360,Carpenter Shop,Friendsanity: Sebastian 4 <3,FRIENDSANITY,
+1361,Carpenter Shop,Friendsanity: Sebastian 5 <3,FRIENDSANITY,
+1362,Carpenter Shop,Friendsanity: Sebastian 6 <3,FRIENDSANITY,
+1363,Carpenter Shop,Friendsanity: Sebastian 7 <3,FRIENDSANITY,
+1364,Carpenter Shop,Friendsanity: Sebastian 8 <3,FRIENDSANITY,
+1365,Carpenter Shop,Friendsanity: Sebastian 9 <3,FRIENDSANITY,
+1366,Carpenter Shop,Friendsanity: Sebastian 10 <3,FRIENDSANITY,
+1367,Carpenter Shop,Friendsanity: Sebastian 11 <3,FRIENDSANITY,
+1368,Carpenter Shop,Friendsanity: Sebastian 12 <3,FRIENDSANITY,
+1369,Carpenter Shop,Friendsanity: Sebastian 13 <3,FRIENDSANITY,
+1370,Carpenter Shop,Friendsanity: Sebastian 14 <3,FRIENDSANITY,
+1371,Marnie's Ranch,Friendsanity: Shane 1 <3,FRIENDSANITY,
+1372,Marnie's Ranch,Friendsanity: Shane 2 <3,FRIENDSANITY,
+1373,Marnie's Ranch,Friendsanity: Shane 3 <3,FRIENDSANITY,
+1374,Marnie's Ranch,Friendsanity: Shane 4 <3,FRIENDSANITY,
+1375,Marnie's Ranch,Friendsanity: Shane 5 <3,FRIENDSANITY,
+1376,Marnie's Ranch,Friendsanity: Shane 6 <3,FRIENDSANITY,
+1377,Marnie's Ranch,Friendsanity: Shane 7 <3,FRIENDSANITY,
+1378,Marnie's Ranch,Friendsanity: Shane 8 <3,FRIENDSANITY,
+1379,Marnie's Ranch,Friendsanity: Shane 9 <3,FRIENDSANITY,
+1380,Marnie's Ranch,Friendsanity: Shane 10 <3,FRIENDSANITY,
+1381,Marnie's Ranch,Friendsanity: Shane 11 <3,FRIENDSANITY,
+1382,Marnie's Ranch,Friendsanity: Shane 12 <3,FRIENDSANITY,
+1383,Marnie's Ranch,Friendsanity: Shane 13 <3,FRIENDSANITY,
+1384,Marnie's Ranch,Friendsanity: Shane 14 <3,FRIENDSANITY,
+1385,Pierre's General Store,Friendsanity: Abigail 1 <3,FRIENDSANITY,
+1386,Pierre's General Store,Friendsanity: Abigail 2 <3,FRIENDSANITY,
+1387,Pierre's General Store,Friendsanity: Abigail 3 <3,FRIENDSANITY,
+1388,Pierre's General Store,Friendsanity: Abigail 4 <3,FRIENDSANITY,
+1389,Pierre's General Store,Friendsanity: Abigail 5 <3,FRIENDSANITY,
+1390,Pierre's General Store,Friendsanity: Abigail 6 <3,FRIENDSANITY,
+1391,Pierre's General Store,Friendsanity: Abigail 7 <3,FRIENDSANITY,
+1392,Pierre's General Store,Friendsanity: Abigail 8 <3,FRIENDSANITY,
+1393,Pierre's General Store,Friendsanity: Abigail 9 <3,FRIENDSANITY,
+1394,Pierre's General Store,Friendsanity: Abigail 10 <3,FRIENDSANITY,
+1395,Pierre's General Store,Friendsanity: Abigail 11 <3,FRIENDSANITY,
+1396,Pierre's General Store,Friendsanity: Abigail 12 <3,FRIENDSANITY,
+1397,Pierre's General Store,Friendsanity: Abigail 13 <3,FRIENDSANITY,
+1398,Pierre's General Store,Friendsanity: Abigail 14 <3,FRIENDSANITY,
+1399,Haley's House,Friendsanity: Emily 1 <3,FRIENDSANITY,
+1400,Haley's House,Friendsanity: Emily 2 <3,FRIENDSANITY,
+1401,Haley's House,Friendsanity: Emily 3 <3,FRIENDSANITY,
+1402,Haley's House,Friendsanity: Emily 4 <3,FRIENDSANITY,
+1403,Haley's House,Friendsanity: Emily 5 <3,FRIENDSANITY,
+1404,Haley's House,Friendsanity: Emily 6 <3,FRIENDSANITY,
+1405,Haley's House,Friendsanity: Emily 7 <3,FRIENDSANITY,
+1406,Haley's House,Friendsanity: Emily 8 <3,FRIENDSANITY,
+1407,Haley's House,Friendsanity: Emily 9 <3,FRIENDSANITY,
+1408,Haley's House,Friendsanity: Emily 10 <3,FRIENDSANITY,
+1409,Haley's House,Friendsanity: Emily 11 <3,FRIENDSANITY,
+1410,Haley's House,Friendsanity: Emily 12 <3,FRIENDSANITY,
+1411,Haley's House,Friendsanity: Emily 13 <3,FRIENDSANITY,
+1412,Haley's House,Friendsanity: Emily 14 <3,FRIENDSANITY,
+1413,Haley's House,Friendsanity: Haley 1 <3,FRIENDSANITY,
+1414,Haley's House,Friendsanity: Haley 2 <3,FRIENDSANITY,
+1415,Haley's House,Friendsanity: Haley 3 <3,FRIENDSANITY,
+1416,Haley's House,Friendsanity: Haley 4 <3,FRIENDSANITY,
+1417,Haley's House,Friendsanity: Haley 5 <3,FRIENDSANITY,
+1418,Haley's House,Friendsanity: Haley 6 <3,FRIENDSANITY,
+1419,Haley's House,Friendsanity: Haley 7 <3,FRIENDSANITY,
+1420,Haley's House,Friendsanity: Haley 8 <3,FRIENDSANITY,
+1421,Haley's House,Friendsanity: Haley 9 <3,FRIENDSANITY,
+1422,Haley's House,Friendsanity: Haley 10 <3,FRIENDSANITY,
+1423,Haley's House,Friendsanity: Haley 11 <3,FRIENDSANITY,
+1424,Haley's House,Friendsanity: Haley 12 <3,FRIENDSANITY,
+1425,Haley's House,Friendsanity: Haley 13 <3,FRIENDSANITY,
+1426,Haley's House,Friendsanity: Haley 14 <3,FRIENDSANITY,
+1427,Leah's Cottage,Friendsanity: Leah 1 <3,FRIENDSANITY,
+1428,Leah's Cottage,Friendsanity: Leah 2 <3,FRIENDSANITY,
+1429,Leah's Cottage,Friendsanity: Leah 3 <3,FRIENDSANITY,
+1430,Leah's Cottage,Friendsanity: Leah 4 <3,FRIENDSANITY,
+1431,Leah's Cottage,Friendsanity: Leah 5 <3,FRIENDSANITY,
+1432,Leah's Cottage,Friendsanity: Leah 6 <3,FRIENDSANITY,
+1433,Leah's Cottage,Friendsanity: Leah 7 <3,FRIENDSANITY,
+1434,Leah's Cottage,Friendsanity: Leah 8 <3,FRIENDSANITY,
+1435,Leah's Cottage,Friendsanity: Leah 9 <3,FRIENDSANITY,
+1436,Leah's Cottage,Friendsanity: Leah 10 <3,FRIENDSANITY,
+1437,Leah's Cottage,Friendsanity: Leah 11 <3,FRIENDSANITY,
+1438,Leah's Cottage,Friendsanity: Leah 12 <3,FRIENDSANITY,
+1439,Leah's Cottage,Friendsanity: Leah 13 <3,FRIENDSANITY,
+1440,Leah's Cottage,Friendsanity: Leah 14 <3,FRIENDSANITY,
+1441,Carpenter Shop,Friendsanity: Maru 1 <3,FRIENDSANITY,
+1442,Carpenter Shop,Friendsanity: Maru 2 <3,FRIENDSANITY,
+1443,Carpenter Shop,Friendsanity: Maru 3 <3,FRIENDSANITY,
+1444,Carpenter Shop,Friendsanity: Maru 4 <3,FRIENDSANITY,
+1445,Carpenter Shop,Friendsanity: Maru 5 <3,FRIENDSANITY,
+1446,Carpenter Shop,Friendsanity: Maru 6 <3,FRIENDSANITY,
+1447,Carpenter Shop,Friendsanity: Maru 7 <3,FRIENDSANITY,
+1448,Carpenter Shop,Friendsanity: Maru 8 <3,FRIENDSANITY,
+1449,Carpenter Shop,Friendsanity: Maru 9 <3,FRIENDSANITY,
+1450,Carpenter Shop,Friendsanity: Maru 10 <3,FRIENDSANITY,
+1451,Carpenter Shop,Friendsanity: Maru 11 <3,FRIENDSANITY,
+1452,Carpenter Shop,Friendsanity: Maru 12 <3,FRIENDSANITY,
+1453,Carpenter Shop,Friendsanity: Maru 13 <3,FRIENDSANITY,
+1454,Carpenter Shop,Friendsanity: Maru 14 <3,FRIENDSANITY,
+1455,Trailer,Friendsanity: Penny 1 <3,FRIENDSANITY,
+1456,Trailer,Friendsanity: Penny 2 <3,FRIENDSANITY,
+1457,Trailer,Friendsanity: Penny 3 <3,FRIENDSANITY,
+1458,Trailer,Friendsanity: Penny 4 <3,FRIENDSANITY,
+1459,Trailer,Friendsanity: Penny 5 <3,FRIENDSANITY,
+1460,Trailer,Friendsanity: Penny 6 <3,FRIENDSANITY,
+1461,Trailer,Friendsanity: Penny 7 <3,FRIENDSANITY,
+1462,Trailer,Friendsanity: Penny 8 <3,FRIENDSANITY,
+1463,Trailer,Friendsanity: Penny 9 <3,FRIENDSANITY,
+1464,Trailer,Friendsanity: Penny 10 <3,FRIENDSANITY,
+1465,Trailer,Friendsanity: Penny 11 <3,FRIENDSANITY,
+1466,Trailer,Friendsanity: Penny 12 <3,FRIENDSANITY,
+1467,Trailer,Friendsanity: Penny 13 <3,FRIENDSANITY,
+1468,Trailer,Friendsanity: Penny 14 <3,FRIENDSANITY,
+1469,Pierre's General Store,Friendsanity: Caroline 1 <3,FRIENDSANITY,
+1470,Pierre's General Store,Friendsanity: Caroline 2 <3,FRIENDSANITY,
+1471,Pierre's General Store,Friendsanity: Caroline 3 <3,FRIENDSANITY,
+1472,Pierre's General Store,Friendsanity: Caroline 4 <3,FRIENDSANITY,
+1473,Pierre's General Store,Friendsanity: Caroline 5 <3,FRIENDSANITY,
+1474,Pierre's General Store,Friendsanity: Caroline 6 <3,FRIENDSANITY,
+1475,Pierre's General Store,Friendsanity: Caroline 7 <3,FRIENDSANITY,
+1476,Pierre's General Store,Friendsanity: Caroline 8 <3,FRIENDSANITY,
+1477,Pierre's General Store,Friendsanity: Caroline 9 <3,FRIENDSANITY,
+1478,Pierre's General Store,Friendsanity: Caroline 10 <3,FRIENDSANITY,
+1480,Clint's Blacksmith,Friendsanity: Clint 1 <3,FRIENDSANITY,
+1481,Clint's Blacksmith,Friendsanity: Clint 2 <3,FRIENDSANITY,
+1482,Clint's Blacksmith,Friendsanity: Clint 3 <3,FRIENDSANITY,
+1483,Clint's Blacksmith,Friendsanity: Clint 4 <3,FRIENDSANITY,
+1484,Clint's Blacksmith,Friendsanity: Clint 5 <3,FRIENDSANITY,
+1485,Clint's Blacksmith,Friendsanity: Clint 6 <3,FRIENDSANITY,
+1486,Clint's Blacksmith,Friendsanity: Clint 7 <3,FRIENDSANITY,
+1487,Clint's Blacksmith,Friendsanity: Clint 8 <3,FRIENDSANITY,
+1488,Clint's Blacksmith,Friendsanity: Clint 9 <3,FRIENDSANITY,
+1489,Clint's Blacksmith,Friendsanity: Clint 10 <3,FRIENDSANITY,
+1491,Carpenter Shop,Friendsanity: Demetrius 1 <3,FRIENDSANITY,
+1492,Carpenter Shop,Friendsanity: Demetrius 2 <3,FRIENDSANITY,
+1493,Carpenter Shop,Friendsanity: Demetrius 3 <3,FRIENDSANITY,
+1494,Carpenter Shop,Friendsanity: Demetrius 4 <3,FRIENDSANITY,
+1495,Carpenter Shop,Friendsanity: Demetrius 5 <3,FRIENDSANITY,
+1496,Carpenter Shop,Friendsanity: Demetrius 6 <3,FRIENDSANITY,
+1497,Carpenter Shop,Friendsanity: Demetrius 7 <3,FRIENDSANITY,
+1498,Carpenter Shop,Friendsanity: Demetrius 8 <3,FRIENDSANITY,
+1499,Carpenter Shop,Friendsanity: Demetrius 9 <3,FRIENDSANITY,
+1500,Carpenter Shop,Friendsanity: Demetrius 10 <3,FRIENDSANITY,
+1502,Mines Dwarf Shop,Friendsanity: Dwarf 1 <3,FRIENDSANITY,
+1503,Mines Dwarf Shop,Friendsanity: Dwarf 2 <3,FRIENDSANITY,
+1504,Mines Dwarf Shop,Friendsanity: Dwarf 3 <3,FRIENDSANITY,
+1505,Mines Dwarf Shop,Friendsanity: Dwarf 4 <3,FRIENDSANITY,
+1506,Mines Dwarf Shop,Friendsanity: Dwarf 5 <3,FRIENDSANITY,
+1507,Mines Dwarf Shop,Friendsanity: Dwarf 6 <3,FRIENDSANITY,
+1508,Mines Dwarf Shop,Friendsanity: Dwarf 7 <3,FRIENDSANITY,
+1509,Mines Dwarf Shop,Friendsanity: Dwarf 8 <3,FRIENDSANITY,
+1510,Mines Dwarf Shop,Friendsanity: Dwarf 9 <3,FRIENDSANITY,
+1511,Mines Dwarf Shop,Friendsanity: Dwarf 10 <3,FRIENDSANITY,
+1513,Alex's House,Friendsanity: Evelyn 1 <3,FRIENDSANITY,
+1514,Alex's House,Friendsanity: Evelyn 2 <3,FRIENDSANITY,
+1515,Alex's House,Friendsanity: Evelyn 3 <3,FRIENDSANITY,
+1516,Alex's House,Friendsanity: Evelyn 4 <3,FRIENDSANITY,
+1517,Alex's House,Friendsanity: Evelyn 5 <3,FRIENDSANITY,
+1518,Alex's House,Friendsanity: Evelyn 6 <3,FRIENDSANITY,
+1519,Alex's House,Friendsanity: Evelyn 7 <3,FRIENDSANITY,
+1520,Alex's House,Friendsanity: Evelyn 8 <3,FRIENDSANITY,
+1521,Alex's House,Friendsanity: Evelyn 9 <3,FRIENDSANITY,
+1522,Alex's House,Friendsanity: Evelyn 10 <3,FRIENDSANITY,
+1524,Alex's House,Friendsanity: George 1 <3,FRIENDSANITY,
+1525,Alex's House,Friendsanity: George 2 <3,FRIENDSANITY,
+1526,Alex's House,Friendsanity: George 3 <3,FRIENDSANITY,
+1527,Alex's House,Friendsanity: George 4 <3,FRIENDSANITY,
+1528,Alex's House,Friendsanity: George 5 <3,FRIENDSANITY,
+1529,Alex's House,Friendsanity: George 6 <3,FRIENDSANITY,
+1530,Alex's House,Friendsanity: George 7 <3,FRIENDSANITY,
+1531,Alex's House,Friendsanity: George 8 <3,FRIENDSANITY,
+1532,Alex's House,Friendsanity: George 9 <3,FRIENDSANITY,
+1533,Alex's House,Friendsanity: George 10 <3,FRIENDSANITY,
+1535,Saloon,Friendsanity: Gus 1 <3,FRIENDSANITY,
+1536,Saloon,Friendsanity: Gus 2 <3,FRIENDSANITY,
+1537,Saloon,Friendsanity: Gus 3 <3,FRIENDSANITY,
+1538,Saloon,Friendsanity: Gus 4 <3,FRIENDSANITY,
+1539,Saloon,Friendsanity: Gus 5 <3,FRIENDSANITY,
+1540,Saloon,Friendsanity: Gus 6 <3,FRIENDSANITY,
+1541,Saloon,Friendsanity: Gus 7 <3,FRIENDSANITY,
+1542,Saloon,Friendsanity: Gus 8 <3,FRIENDSANITY,
+1543,Saloon,Friendsanity: Gus 9 <3,FRIENDSANITY,
+1544,Saloon,Friendsanity: Gus 10 <3,FRIENDSANITY,
+1546,Marnie's Ranch,Friendsanity: Jas 1 <3,FRIENDSANITY,
+1547,Marnie's Ranch,Friendsanity: Jas 2 <3,FRIENDSANITY,
+1548,Marnie's Ranch,Friendsanity: Jas 3 <3,FRIENDSANITY,
+1549,Marnie's Ranch,Friendsanity: Jas 4 <3,FRIENDSANITY,
+1550,Marnie's Ranch,Friendsanity: Jas 5 <3,FRIENDSANITY,
+1551,Marnie's Ranch,Friendsanity: Jas 6 <3,FRIENDSANITY,
+1552,Marnie's Ranch,Friendsanity: Jas 7 <3,FRIENDSANITY,
+1553,Marnie's Ranch,Friendsanity: Jas 8 <3,FRIENDSANITY,
+1554,Marnie's Ranch,Friendsanity: Jas 9 <3,FRIENDSANITY,
+1555,Marnie's Ranch,Friendsanity: Jas 10 <3,FRIENDSANITY,
+1557,Sam's House,Friendsanity: Jodi 1 <3,FRIENDSANITY,
+1558,Sam's House,Friendsanity: Jodi 2 <3,FRIENDSANITY,
+1559,Sam's House,Friendsanity: Jodi 3 <3,FRIENDSANITY,
+1560,Sam's House,Friendsanity: Jodi 4 <3,FRIENDSANITY,
+1561,Sam's House,Friendsanity: Jodi 5 <3,FRIENDSANITY,
+1562,Sam's House,Friendsanity: Jodi 6 <3,FRIENDSANITY,
+1563,Sam's House,Friendsanity: Jodi 7 <3,FRIENDSANITY,
+1564,Sam's House,Friendsanity: Jodi 8 <3,FRIENDSANITY,
+1565,Sam's House,Friendsanity: Jodi 9 <3,FRIENDSANITY,
+1566,Sam's House,Friendsanity: Jodi 10 <3,FRIENDSANITY,
+1568,Sam's House,Friendsanity: Kent 1 <3,FRIENDSANITY,
+1569,Sam's House,Friendsanity: Kent 2 <3,FRIENDSANITY,
+1570,Sam's House,Friendsanity: Kent 3 <3,FRIENDSANITY,
+1571,Sam's House,Friendsanity: Kent 4 <3,FRIENDSANITY,
+1572,Sam's House,Friendsanity: Kent 5 <3,FRIENDSANITY,
+1573,Sam's House,Friendsanity: Kent 6 <3,FRIENDSANITY,
+1574,Sam's House,Friendsanity: Kent 7 <3,FRIENDSANITY,
+1575,Sam's House,Friendsanity: Kent 8 <3,FRIENDSANITY,
+1576,Sam's House,Friendsanity: Kent 9 <3,FRIENDSANITY,
+1577,Sam's House,Friendsanity: Kent 10 <3,FRIENDSANITY,
+1579,Sewer,Friendsanity: Krobus 1 <3,FRIENDSANITY,
+1580,Sewer,Friendsanity: Krobus 2 <3,FRIENDSANITY,
+1581,Sewer,Friendsanity: Krobus 3 <3,FRIENDSANITY,
+1582,Sewer,Friendsanity: Krobus 4 <3,FRIENDSANITY,
+1583,Sewer,Friendsanity: Krobus 5 <3,FRIENDSANITY,
+1584,Sewer,Friendsanity: Krobus 6 <3,FRIENDSANITY,
+1585,Sewer,Friendsanity: Krobus 7 <3,FRIENDSANITY,
+1586,Sewer,Friendsanity: Krobus 8 <3,FRIENDSANITY,
+1587,Sewer,Friendsanity: Krobus 9 <3,FRIENDSANITY,
+1588,Sewer,Friendsanity: Krobus 10 <3,FRIENDSANITY,
+1590,Leo's Hut,Friendsanity: Leo 1 <3,"FRIENDSANITY,GINGER_ISLAND",
+1591,Leo's Hut,Friendsanity: Leo 2 <3,"FRIENDSANITY,GINGER_ISLAND",
+1592,Leo's Hut,Friendsanity: Leo 3 <3,"FRIENDSANITY,GINGER_ISLAND",
+1593,Leo's Hut,Friendsanity: Leo 4 <3,"FRIENDSANITY,GINGER_ISLAND",
+1594,Leo's Hut,Friendsanity: Leo 5 <3,"FRIENDSANITY,GINGER_ISLAND",
+1595,Leo's Hut,Friendsanity: Leo 6 <3,"FRIENDSANITY,GINGER_ISLAND",
+1596,Leo's Hut,Friendsanity: Leo 7 <3,"FRIENDSANITY,GINGER_ISLAND",
+1597,Leo's Hut,Friendsanity: Leo 8 <3,"FRIENDSANITY,GINGER_ISLAND",
+1598,Leo's Hut,Friendsanity: Leo 9 <3,"FRIENDSANITY,GINGER_ISLAND",
+1599,Leo's Hut,Friendsanity: Leo 10 <3,"FRIENDSANITY,GINGER_ISLAND",
+1601,Mayor's Manor,Friendsanity: Lewis 1 <3,FRIENDSANITY,
+1602,Mayor's Manor,Friendsanity: Lewis 2 <3,FRIENDSANITY,
+1603,Mayor's Manor,Friendsanity: Lewis 3 <3,FRIENDSANITY,
+1604,Mayor's Manor,Friendsanity: Lewis 4 <3,FRIENDSANITY,
+1605,Mayor's Manor,Friendsanity: Lewis 5 <3,FRIENDSANITY,
+1606,Mayor's Manor,Friendsanity: Lewis 6 <3,FRIENDSANITY,
+1607,Mayor's Manor,Friendsanity: Lewis 7 <3,FRIENDSANITY,
+1608,Mayor's Manor,Friendsanity: Lewis 8 <3,FRIENDSANITY,
+1609,Mayor's Manor,Friendsanity: Lewis 9 <3,FRIENDSANITY,
+1610,Mayor's Manor,Friendsanity: Lewis 10 <3,FRIENDSANITY,
+1612,Tent,Friendsanity: Linus 1 <3,FRIENDSANITY,
+1613,Tent,Friendsanity: Linus 2 <3,FRIENDSANITY,
+1614,Tent,Friendsanity: Linus 3 <3,FRIENDSANITY,
+1615,Tent,Friendsanity: Linus 4 <3,FRIENDSANITY,
+1616,Tent,Friendsanity: Linus 5 <3,FRIENDSANITY,
+1617,Tent,Friendsanity: Linus 6 <3,FRIENDSANITY,
+1618,Tent,Friendsanity: Linus 7 <3,FRIENDSANITY,
+1619,Tent,Friendsanity: Linus 8 <3,FRIENDSANITY,
+1620,Tent,Friendsanity: Linus 9 <3,FRIENDSANITY,
+1621,Tent,Friendsanity: Linus 10 <3,FRIENDSANITY,
+1623,Marnie's Ranch,Friendsanity: Marnie 1 <3,FRIENDSANITY,
+1624,Marnie's Ranch,Friendsanity: Marnie 2 <3,FRIENDSANITY,
+1625,Marnie's Ranch,Friendsanity: Marnie 3 <3,FRIENDSANITY,
+1626,Marnie's Ranch,Friendsanity: Marnie 4 <3,FRIENDSANITY,
+1627,Marnie's Ranch,Friendsanity: Marnie 5 <3,FRIENDSANITY,
+1628,Marnie's Ranch,Friendsanity: Marnie 6 <3,FRIENDSANITY,
+1629,Marnie's Ranch,Friendsanity: Marnie 7 <3,FRIENDSANITY,
+1630,Marnie's Ranch,Friendsanity: Marnie 8 <3,FRIENDSANITY,
+1631,Marnie's Ranch,Friendsanity: Marnie 9 <3,FRIENDSANITY,
+1632,Marnie's Ranch,Friendsanity: Marnie 10 <3,FRIENDSANITY,
+1634,Trailer,Friendsanity: Pam 1 <3,FRIENDSANITY,
+1635,Trailer,Friendsanity: Pam 2 <3,FRIENDSANITY,
+1636,Trailer,Friendsanity: Pam 3 <3,FRIENDSANITY,
+1637,Trailer,Friendsanity: Pam 4 <3,FRIENDSANITY,
+1638,Trailer,Friendsanity: Pam 5 <3,FRIENDSANITY,
+1639,Trailer,Friendsanity: Pam 6 <3,FRIENDSANITY,
+1640,Trailer,Friendsanity: Pam 7 <3,FRIENDSANITY,
+1641,Trailer,Friendsanity: Pam 8 <3,FRIENDSANITY,
+1642,Trailer,Friendsanity: Pam 9 <3,FRIENDSANITY,
+1643,Trailer,Friendsanity: Pam 10 <3,FRIENDSANITY,
+1645,Pierre's General Store,Friendsanity: Pierre 1 <3,FRIENDSANITY,
+1646,Pierre's General Store,Friendsanity: Pierre 2 <3,FRIENDSANITY,
+1647,Pierre's General Store,Friendsanity: Pierre 3 <3,FRIENDSANITY,
+1648,Pierre's General Store,Friendsanity: Pierre 4 <3,FRIENDSANITY,
+1649,Pierre's General Store,Friendsanity: Pierre 5 <3,FRIENDSANITY,
+1650,Pierre's General Store,Friendsanity: Pierre 6 <3,FRIENDSANITY,
+1651,Pierre's General Store,Friendsanity: Pierre 7 <3,FRIENDSANITY,
+1652,Pierre's General Store,Friendsanity: Pierre 8 <3,FRIENDSANITY,
+1653,Pierre's General Store,Friendsanity: Pierre 9 <3,FRIENDSANITY,
+1654,Pierre's General Store,Friendsanity: Pierre 10 <3,FRIENDSANITY,
+1656,Carpenter Shop,Friendsanity: Robin 1 <3,FRIENDSANITY,
+1657,Carpenter Shop,Friendsanity: Robin 2 <3,FRIENDSANITY,
+1658,Carpenter Shop,Friendsanity: Robin 3 <3,FRIENDSANITY,
+1659,Carpenter Shop,Friendsanity: Robin 4 <3,FRIENDSANITY,
+1660,Carpenter Shop,Friendsanity: Robin 5 <3,FRIENDSANITY,
+1661,Carpenter Shop,Friendsanity: Robin 6 <3,FRIENDSANITY,
+1662,Carpenter Shop,Friendsanity: Robin 7 <3,FRIENDSANITY,
+1663,Carpenter Shop,Friendsanity: Robin 8 <3,FRIENDSANITY,
+1664,Carpenter Shop,Friendsanity: Robin 9 <3,FRIENDSANITY,
+1665,Carpenter Shop,Friendsanity: Robin 10 <3,FRIENDSANITY,
+1667,Oasis,Friendsanity: Sandy 1 <3,FRIENDSANITY,
+1668,Oasis,Friendsanity: Sandy 2 <3,FRIENDSANITY,
+1669,Oasis,Friendsanity: Sandy 3 <3,FRIENDSANITY,
+1670,Oasis,Friendsanity: Sandy 4 <3,FRIENDSANITY,
+1671,Oasis,Friendsanity: Sandy 5 <3,FRIENDSANITY,
+1672,Oasis,Friendsanity: Sandy 6 <3,FRIENDSANITY,
+1673,Oasis,Friendsanity: Sandy 7 <3,FRIENDSANITY,
+1674,Oasis,Friendsanity: Sandy 8 <3,FRIENDSANITY,
+1675,Oasis,Friendsanity: Sandy 9 <3,FRIENDSANITY,
+1676,Oasis,Friendsanity: Sandy 10 <3,FRIENDSANITY,
+1678,Sam's House,Friendsanity: Vincent 1 <3,FRIENDSANITY,
+1679,Sam's House,Friendsanity: Vincent 2 <3,FRIENDSANITY,
+1680,Sam's House,Friendsanity: Vincent 3 <3,FRIENDSANITY,
+1681,Sam's House,Friendsanity: Vincent 4 <3,FRIENDSANITY,
+1682,Sam's House,Friendsanity: Vincent 5 <3,FRIENDSANITY,
+1683,Sam's House,Friendsanity: Vincent 6 <3,FRIENDSANITY,
+1684,Sam's House,Friendsanity: Vincent 7 <3,FRIENDSANITY,
+1685,Sam's House,Friendsanity: Vincent 8 <3,FRIENDSANITY,
+1686,Sam's House,Friendsanity: Vincent 9 <3,FRIENDSANITY,
+1687,Sam's House,Friendsanity: Vincent 10 <3,FRIENDSANITY,
+1689,Willy's Fish Shop,Friendsanity: Willy 1 <3,FRIENDSANITY,
+1690,Willy's Fish Shop,Friendsanity: Willy 2 <3,FRIENDSANITY,
+1691,Willy's Fish Shop,Friendsanity: Willy 3 <3,FRIENDSANITY,
+1692,Willy's Fish Shop,Friendsanity: Willy 4 <3,FRIENDSANITY,
+1693,Willy's Fish Shop,Friendsanity: Willy 5 <3,FRIENDSANITY,
+1694,Willy's Fish Shop,Friendsanity: Willy 6 <3,FRIENDSANITY,
+1695,Willy's Fish Shop,Friendsanity: Willy 7 <3,FRIENDSANITY,
+1696,Willy's Fish Shop,Friendsanity: Willy 8 <3,FRIENDSANITY,
+1697,Willy's Fish Shop,Friendsanity: Willy 9 <3,FRIENDSANITY,
+1698,Willy's Fish Shop,Friendsanity: Willy 10 <3,FRIENDSANITY,
+1700,Wizard Tower,Friendsanity: Wizard 1 <3,FRIENDSANITY,
+1701,Wizard Tower,Friendsanity: Wizard 2 <3,FRIENDSANITY,
+1702,Wizard Tower,Friendsanity: Wizard 3 <3,FRIENDSANITY,
+1703,Wizard Tower,Friendsanity: Wizard 4 <3,FRIENDSANITY,
+1704,Wizard Tower,Friendsanity: Wizard 5 <3,FRIENDSANITY,
+1705,Wizard Tower,Friendsanity: Wizard 6 <3,FRIENDSANITY,
+1706,Wizard Tower,Friendsanity: Wizard 7 <3,FRIENDSANITY,
+1707,Wizard Tower,Friendsanity: Wizard 8 <3,FRIENDSANITY,
+1708,Wizard Tower,Friendsanity: Wizard 9 <3,FRIENDSANITY,
+1709,Wizard Tower,Friendsanity: Wizard 10 <3,FRIENDSANITY,
+1710,Farm,Friendsanity: Pet 1 <3,FRIENDSANITY,
+1711,Farm,Friendsanity: Pet 2 <3,FRIENDSANITY,
+1712,Farm,Friendsanity: Pet 3 <3,FRIENDSANITY,
+1713,Farm,Friendsanity: Pet 4 <3,FRIENDSANITY,
+1714,Farm,Friendsanity: Pet 5 <3,FRIENDSANITY,
+1715,Town,Friendsanity: Friend 1 <3,FRIENDSANITY,
+1716,Town,Friendsanity: Friend 2 <3,FRIENDSANITY,
+1717,Town,Friendsanity: Friend 3 <3,FRIENDSANITY,
+1718,Town,Friendsanity: Friend 4 <3,FRIENDSANITY,
+1719,Town,Friendsanity: Friend 5 <3,FRIENDSANITY,
+1720,Town,Friendsanity: Friend 6 <3,FRIENDSANITY,
+1721,Town,Friendsanity: Friend 7 <3,FRIENDSANITY,
+1722,Town,Friendsanity: Friend 8 <3,FRIENDSANITY,
+1723,Town,Friendsanity: Suitor 9 <3,FRIENDSANITY,
+1724,Town,Friendsanity: Suitor 10 <3,FRIENDSANITY,
+1725,Town,Friendsanity: Spouse 11 <3,FRIENDSANITY,
+1726,Town,Friendsanity: Spouse 12 <3,FRIENDSANITY,
+1727,Town,Friendsanity: Spouse 13 <3,FRIENDSANITY,
+1728,Town,Friendsanity: Spouse 14 <3,FRIENDSANITY,
+2001,Egg Festival,Egg Hunt Victory,FESTIVAL,
+2002,Egg Festival,Egg Festival: Strawberry Seeds,FESTIVAL,
+2003,Flower Dance,Dance with someone,FESTIVAL,
+2004,Flower Dance,Rarecrow #5 (Woman),FESTIVAL,
+2005,Luau,Luau Soup,FESTIVAL,
+2006,Dance of the Moonlight Jellies,Dance of the Moonlight Jellies,FESTIVAL,
+2007,Stardew Valley Fair,Smashing Stone,FESTIVAL,
+2008,Stardew Valley Fair,Grange Display,FESTIVAL,
+2009,Stardew Valley Fair,Rarecrow #1 (Turnip Head),FESTIVAL,
+2010,Stardew Valley Fair,Fair Stardrop,FESTIVAL,
+2011,Spirit's Eve,Spirit's Eve Maze,FESTIVAL,
+2012,Spirit's Eve,Rarecrow #2 (Witch),FESTIVAL,
+2013,Festival of Ice,Win Fishing Competition,FESTIVAL,
+2014,Festival of Ice,Rarecrow #4 (Snowman),FESTIVAL,
+2015,Night Market,Mermaid Pearl,FESTIVAL,
+2016,Night Market,Cone Hat,FESTIVAL_HARD,
+2017,Night Market,Iridium Fireplace,FESTIVAL_HARD,
+2018,Night Market,Rarecrow #7 (Tanuki),FESTIVAL,
+2019,Night Market,Rarecrow #8 (Tribal Mask),FESTIVAL,
+2020,Night Market,Lupini: Red Eagle,FESTIVAL,
+2021,Night Market,Lupini: Portrait Of A Mermaid,FESTIVAL,
+2022,Night Market,Lupini: Solar Kingdom,FESTIVAL,
+2023,Night Market,Lupini: Clouds,FESTIVAL_HARD,
+2024,Night Market,Lupini: 1000 Years From Now,FESTIVAL_HARD,
+2025,Night Market,Lupini: Three Trees,FESTIVAL_HARD,
+2026,Night Market,Lupini: The Serpent,FESTIVAL_HARD,
+2027,Night Market,Lupini: 'Tropical Fish #173',FESTIVAL_HARD,
+2028,Night Market,Lupini: Land Of Clay,FESTIVAL_HARD,
+2029,Feast of the Winter Star,Secret Santa,FESTIVAL,
+2030,Feast of the Winter Star,The Legend of the Winter Star,FESTIVAL,
+2031,Farm,Collect All Rarecrows,FESTIVAL,
+2032,Flower Dance,Tub o' Flowers Recipe,FESTIVAL,
+2033,Spirit's Eve,Jack-O-Lantern Recipe,FESTIVAL,
+2034,Dance of the Moonlight Jellies,Moonlight Jellies Banner,FESTIVAL,
+2035,Dance of the Moonlight Jellies,Starport Decal,FESTIVAL,
+2036,Casino,Rarecrow #3 (Alien),FESTIVAL,
+2041,Desert Festival,Calico Race,FESTIVAL,
+2042,Desert Festival,Mummy Mask,FESTIVAL_HARD,
+2043,Desert Festival,Calico Statue,FESTIVAL,
+2044,Desert Festival,Emily's Outfit Services,FESTIVAL,
+2045,Desert Festival,Earthy Mousse,DESERT_FESTIVAL_CHEF,
+2046,Desert Festival,Sweet Bean Cake,DESERT_FESTIVAL_CHEF,
+2047,Desert Festival,Skull Cave Casserole,DESERT_FESTIVAL_CHEF,
+2048,Desert Festival,Spicy Tacos,DESERT_FESTIVAL_CHEF,
+2049,Desert Festival,Mountain Chili,DESERT_FESTIVAL_CHEF,
+2050,Desert Festival,Crystal Cake,DESERT_FESTIVAL_CHEF,
+2051,Desert Festival,Cave Kebab,DESERT_FESTIVAL_CHEF,
+2052,Desert Festival,Hot Log,DESERT_FESTIVAL_CHEF,
+2053,Desert Festival,Sour Salad,DESERT_FESTIVAL_CHEF,
+2054,Desert Festival,Superfood Cake,DESERT_FESTIVAL_CHEF,
+2055,Desert Festival,Warrior Smoothie,DESERT_FESTIVAL_CHEF,
+2056,Desert Festival,Rumpled Fruit Skin,DESERT_FESTIVAL_CHEF,
+2057,Desert Festival,Calico Pizza,DESERT_FESTIVAL_CHEF,
+2058,Desert Festival,Stuffed Mushrooms,DESERT_FESTIVAL_CHEF,
+2059,Desert Festival,Elf Quesadilla,DESERT_FESTIVAL_CHEF,
+2060,Desert Festival,Nachos Of The Desert,DESERT_FESTIVAL_CHEF,
+2061,Desert Festival,Cioppino,DESERT_FESTIVAL_CHEF,
+2062,Desert Festival,Rainforest Shrimp,DESERT_FESTIVAL_CHEF,
+2063,Desert Festival,Shrimp Donut,DESERT_FESTIVAL_CHEF,
+2064,Desert Festival,Smell Of The Sea,DESERT_FESTIVAL_CHEF,
+2065,Desert Festival,Desert Gumbo,DESERT_FESTIVAL_CHEF,
+2066,Desert Festival,Free Cactis,FESTIVAL,
+2067,Desert Festival,Monster Hunt,FESTIVAL_HARD,
+2068,Desert Festival,Deep Dive,FESTIVAL_HARD,
+2069,Desert Festival,Treasure Hunt,FESTIVAL_HARD,
+2070,Desert Festival,Touch A Calico Statue,FESTIVAL,
+2071,Desert Festival,Real Calico Egg Hunter,FESTIVAL,
+2072,Desert Festival,Willy's Challenge,FESTIVAL_HARD,
+2073,Desert Festival,Desert Scholar,FESTIVAL,
+2074,Trout Derby,Trout Derby Reward 1,FESTIVAL,
+2075,Trout Derby,Trout Derby Reward 2,FESTIVAL,
+2076,Trout Derby,Trout Derby Reward 3,FESTIVAL,
+2077,Trout Derby,Trout Derby Reward 4,FESTIVAL_HARD,
+2078,Trout Derby,Trout Derby Reward 5,FESTIVAL_HARD,
+2079,Trout Derby,Trout Derby Reward 6,FESTIVAL_HARD,
+2080,Trout Derby,Trout Derby Reward 7,FESTIVAL_HARD,
+2081,Trout Derby,Trout Derby Reward 8,FESTIVAL_HARD,
+2082,Trout Derby,Trout Derby Reward 9,FESTIVAL_HARD,
+2083,Trout Derby,Trout Derby Reward 10,FESTIVAL_HARD,
+2084,SquidFest,SquidFest Day 1 Copper,FESTIVAL,
+2085,SquidFest,SquidFest Day 1 Iron,FESTIVAL,
+2086,SquidFest,SquidFest Day 1 Gold,FESTIVAL_HARD,
+2087,SquidFest,SquidFest Day 1 Iridium,FESTIVAL_HARD,
+2088,SquidFest,SquidFest Day 2 Copper,FESTIVAL,
+2089,SquidFest,SquidFest Day 2 Iron,FESTIVAL,
+2090,SquidFest,SquidFest Day 2 Gold,FESTIVAL_HARD,
+2091,SquidFest,SquidFest Day 2 Iridium,FESTIVAL_HARD,
+2101,Town,Island Ingredients,"GINGER_ISLAND,SPECIAL_ORDER_BOARD",
+2102,The Mines - Floor 75,Cave Patrol,SPECIAL_ORDER_BOARD,
+2103,Fishing,Aquatic Overpopulation,SPECIAL_ORDER_BOARD,
+2104,Fishing,Biome Balance,SPECIAL_ORDER_BOARD,
+2105,Haley's House,Rock Rejuvenation,SPECIAL_ORDER_BOARD,
+2106,Alex's House,Gifts for George,SPECIAL_ORDER_BOARD,
+2107,Museum,Fragments of the past,"GINGER_ISLAND,SPECIAL_ORDER_BOARD",
+2108,Saloon,Gus' Famous Omelet,SPECIAL_ORDER_BOARD,
+2109,Farm,Crop Order,SPECIAL_ORDER_BOARD,
+2110,Railroad,Community Cleanup,SPECIAL_ORDER_BOARD,
+2111,Trailer,The Strong Stuff,SPECIAL_ORDER_BOARD,
+2112,Pierre's General Store,Pierre's Prime Produce,SPECIAL_ORDER_BOARD,
+2113,Carpenter Shop,Robin's Project,SPECIAL_ORDER_BOARD,
+2114,Carpenter Shop,Robin's Resource Rush,SPECIAL_ORDER_BOARD,
+2115,Beach,Juicy Bugs Wanted!,SPECIAL_ORDER_BOARD,
+2116,Town,Tropical Fish,"GINGER_ISLAND,SPECIAL_ORDER_BOARD",
+2117,The Mines - Floor 75,A Curious Substance,SPECIAL_ORDER_BOARD,
+2118,The Mines - Floor 35,Prismatic Jelly,SPECIAL_ORDER_BOARD,
+2151,Qi's Walnut Room,Qi's Crop,"GINGER_ISLAND,SPECIAL_ORDER_QI",
+2152,Qi's Walnut Room,Let's Play A Game,"GINGER_ISLAND,JUNIMO_KART,SPECIAL_ORDER_QI",
+2153,Qi's Walnut Room,Four Precious Stones,"GINGER_ISLAND,SPECIAL_ORDER_QI",
+2154,Qi's Walnut Room,Qi's Hungry Challenge,"GINGER_ISLAND,SPECIAL_ORDER_QI",
+2155,Qi's Walnut Room,Qi's Cuisine,"GINGER_ISLAND,SPECIAL_ORDER_QI",
+2156,Qi's Walnut Room,Qi's Kindness,"GINGER_ISLAND,SPECIAL_ORDER_QI",
+2157,Qi's Walnut Room,Extended Family,"GINGER_ISLAND,SPECIAL_ORDER_QI",
+2158,Qi's Walnut Room,Danger In The Deep,"GINGER_ISLAND,SPECIAL_ORDER_QI",
+2159,Qi's Walnut Room,Skull Cavern Invasion,"GINGER_ISLAND,SPECIAL_ORDER_QI",
+2160,Qi's Walnut Room,Qi's Prismatic Grange,"GINGER_ISLAND,SPECIAL_ORDER_QI",
+2201,Boat Tunnel,Repair Ticket Machine,GINGER_ISLAND,
+2202,Boat Tunnel,Repair Boat Hull,GINGER_ISLAND,
+2203,Boat Tunnel,Repair Boat Anchor,GINGER_ISLAND,
+2204,Leo's Hut,Leo's Parrot,"GINGER_ISLAND,WALNUT_PURCHASE",
+2205,Island South,Island West Turtle,"GINGER_ISLAND,WALNUT_PURCHASE",
+2206,Island West,Island Farmhouse,"GINGER_ISLAND,WALNUT_PURCHASE",
+2207,Island Farmhouse,Island Mailbox,"GINGER_ISLAND,WALNUT_PURCHASE",
+2208,Island Farmhouse,Farm Obelisk,"GINGER_ISLAND,WALNUT_PURCHASE",
+2209,Island North,Dig Site Bridge,"GINGER_ISLAND,WALNUT_PURCHASE",
+2210,Island North,Island Trader,"GINGER_ISLAND,WALNUT_PURCHASE",
+2211,Volcano Entrance,Volcano Bridge,"GINGER_ISLAND,WALNUT_PURCHASE",
+2212,Volcano - Floor 5,Volcano Exit Shortcut,"GINGER_ISLAND,WALNUT_PURCHASE",
+2213,Island South,Island Resort,"GINGER_ISLAND,WALNUT_PURCHASE",
+2214,Island West,Parrot Express,"GINGER_ISLAND,WALNUT_PURCHASE",
+2215,Dig Site,Open Professor Snail Cave,GINGER_ISLAND,
+2216,Field Office,Complete Island Field Office,GINGER_ISLAND,
+2301,Fall Farming,Harvest Amaranth,CROPSANITY,
+2302,Fall Farming,Harvest Artichoke,CROPSANITY,
+2303,Fall Farming,Harvest Beet,CROPSANITY,
+2304,Spring Farming,Harvest Blue Jazz,CROPSANITY,
+2305,Summer Farming,Harvest Blueberry,CROPSANITY,
+2306,Fall Farming,Harvest Bok Choy,CROPSANITY,
+2307,Spring Farming,Harvest Cauliflower,CROPSANITY,
+2308,Summer or Fall Farming,Harvest Corn,CROPSANITY,
+2309,Fall Farming,Harvest Cranberries,CROPSANITY,
+2310,Fall Farming,Harvest Eggplant,CROPSANITY,
+2311,Fall Farming,Harvest Fairy Rose,CROPSANITY,
+2312,Spring Farming,Harvest Garlic,CROPSANITY,
+2313,Fall Farming,Harvest Grape,CROPSANITY,
+2314,Spring Farming,Harvest Green Bean,CROPSANITY,
+2315,Summer Farming,Harvest Hops,CROPSANITY,
+2316,Summer Farming,Harvest Hot Pepper,CROPSANITY,
+2317,Spring Farming,Harvest Kale,CROPSANITY,
+2318,Summer Farming,Harvest Melon,CROPSANITY,
+2319,Spring Farming,Harvest Parsnip,CROPSANITY,
+2320,Summer Farming,Harvest Poppy,CROPSANITY,
+2321,Spring Farming,Harvest Potato,CROPSANITY,
+2322,Fall Farming,Harvest Pumpkin,CROPSANITY,
+2323,Summer Farming,Harvest Radish,CROPSANITY,
+2324,Summer Farming,Harvest Red Cabbage,CROPSANITY,
+2325,Spring Farming,Harvest Rhubarb,CROPSANITY,
+2326,Summer Farming,Harvest Starfruit,CROPSANITY,
+2327,Spring Farming,Harvest Strawberry,CROPSANITY,
+2328,Summer Farming,Harvest Summer Spangle,CROPSANITY,
+2329,Summer or Fall Farming,Harvest Sunflower,CROPSANITY,
+2330,Summer Farming,Harvest Tomato,CROPSANITY,
+2331,Spring Farming,Harvest Tulip,CROPSANITY,
+2332,Spring Farming,Harvest Unmilled Rice,CROPSANITY,
+2333,Summer or Fall Farming,Harvest Wheat,CROPSANITY,
+2334,Fall Farming,Harvest Yam,CROPSANITY,
+2335,Indoor Farming,Harvest Cactus Fruit,CROPSANITY,
+2336,Summer Farming,Harvest Pineapple,"CROPSANITY,GINGER_ISLAND",
+2337,Summer Farming,Harvest Taro Root,"CROPSANITY,GINGER_ISLAND",
+2338,Fall Farming,Harvest Sweet Gem Berry,CROPSANITY,
+2339,Fall Farming,Harvest Apple,CROPSANITY,
+2340,Spring Farming,Harvest Apricot,CROPSANITY,
+2341,Spring Farming,Harvest Cherry,CROPSANITY,
+2342,Summer Farming,Harvest Orange,CROPSANITY,
+2343,Fall Farming,Harvest Pomegranate,CROPSANITY,
+2344,Summer Farming,Harvest Peach,CROPSANITY,
+2345,Summer Farming,Harvest Banana,"CROPSANITY,GINGER_ISLAND",
+2346,Summer Farming,Harvest Mango,"CROPSANITY,GINGER_ISLAND",
+2347,Indoor Farming,Harvest Coffee Bean,CROPSANITY,
+2348,Fall Farming,Harvest Broccoli,CROPSANITY,
+2349,Spring Farming,Harvest Carrot,CROPSANITY,
+2350,Summer Farming,Harvest Powdermelon,CROPSANITY,
+2351,Summer Farming,Harvest Summer Squash,CROPSANITY,
+2352,Indoor Farming,Harvest Ancient Fruit,CROPSANITY,
+2353,Indoor Farming,Harvest Qi Fruit,"CROPSANITY,GINGER_ISLAND",
+2401,Shipping,Shipsanity: Duck Egg,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2402,Shipping,Shipsanity: Duck Feather,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2403,Shipping,Shipsanity: Egg,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2404,Shipping,Shipsanity: Egg (Brown),"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2405,Shipping,Shipsanity: Goat Milk,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2406,Shipping,Shipsanity: Large Goat Milk,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2407,Shipping,Shipsanity: Large Egg,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2408,Shipping,Shipsanity: Large Egg (Brown),"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2409,Shipping,Shipsanity: Large Milk,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2410,Shipping,Shipsanity: Milk,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2411,Shipping,Shipsanity: Rabbit's Foot,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2412,Shipping,Shipsanity: Roe,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2413,Shipping,Shipsanity: Truffle,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2414,Shipping,Shipsanity: Void Egg,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2415,Shipping,Shipsanity: Wool,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2416,Shipping,Shipsanity: Anchor,SHIPSANITY,
+2417,Shipping,Shipsanity: Ancient Doll,SHIPSANITY,
+2418,Shipping,Shipsanity: Ancient Drum,SHIPSANITY,
+2419,Shipping,Shipsanity: Ancient Seed,SHIPSANITY,
+2420,Shipping,Shipsanity: Ancient Sword,SHIPSANITY,
+2421,Shipping,Shipsanity: Arrowhead,SHIPSANITY,
+2422,Shipping,Shipsanity: Artifact Trove,SHIPSANITY,
+2423,Shipping,Shipsanity: Bone Flute,SHIPSANITY,
+2424,Shipping,Shipsanity: Chewing Stick,SHIPSANITY,
+2425,Shipping,Shipsanity: Chicken Statue,SHIPSANITY,
+2426,Shipping,Shipsanity: Chipped Amphora,SHIPSANITY,
+2427,Shipping,Shipsanity: Dinosaur Egg,SHIPSANITY,
+2428,Shipping,Shipsanity: Dried Starfish,SHIPSANITY,
+2429,Shipping,Shipsanity: Dwarf Gadget,SHIPSANITY,
+2430,Shipping,Shipsanity: Dwarf Scroll I,SHIPSANITY,
+2431,Shipping,Shipsanity: Dwarf Scroll II,SHIPSANITY,
+2432,Shipping,Shipsanity: Dwarf Scroll III,SHIPSANITY,
+2433,Shipping,Shipsanity: Dwarf Scroll IV,SHIPSANITY,
+2434,Shipping,Shipsanity: Dwarvish Helm,SHIPSANITY,
+2435,Shipping,Shipsanity: Elvish Jewelry,SHIPSANITY,
+2436,Shipping,Shipsanity: Glass Shards,SHIPSANITY,
+2437,Shipping,Shipsanity: Golden Mask,SHIPSANITY,
+2438,Shipping,Shipsanity: Golden Relic,SHIPSANITY,
+2439,Shipping,Shipsanity: Ornamental Fan,SHIPSANITY,
+2440,Shipping,Shipsanity: Prehistoric Handaxe,SHIPSANITY,
+2441,Shipping,Shipsanity: Prehistoric Tool,SHIPSANITY,
+2442,Shipping,Shipsanity: Rare Disc,SHIPSANITY,
+2443,Shipping,Shipsanity: Rusty Cog,SHIPSANITY,
+2444,Shipping,Shipsanity: Rusty Spoon,SHIPSANITY,
+2445,Shipping,Shipsanity: Rusty Spur,SHIPSANITY,
+2446,Shipping,Shipsanity: Strange Doll,SHIPSANITY,
+2447,Shipping,Shipsanity: Strange Doll (Green),SHIPSANITY,
+2448,Shipping,Shipsanity: Treasure Chest,SHIPSANITY,
+2449,Shipping,Shipsanity: Aged Roe,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2450,Shipping,Shipsanity: Beer,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2451,Shipping,Shipsanity: Caviar,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2452,Shipping,Shipsanity: Cheese,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2453,Shipping,Shipsanity: Cloth,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2454,Shipping,Shipsanity: Coffee,SHIPSANITY,
+2455,Shipping,Shipsanity: Dinosaur Mayonnaise,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2456,Shipping,Shipsanity: Duck Mayonnaise,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2457,Shipping,Shipsanity: Goat Cheese,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2458,Shipping,Shipsanity: Green Tea,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2459,Shipping,Shipsanity: Honey,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2460,Shipping,Shipsanity: Jelly,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2461,Shipping,Shipsanity: Juice,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2462,Shipping,Shipsanity: Maple Syrup,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2463,Shipping,Shipsanity: Mayonnaise,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2464,Shipping,Shipsanity: Mead,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2465,Shipping,Shipsanity: Oak Resin,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2466,Shipping,Shipsanity: Pale Ale,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2467,Shipping,Shipsanity: Pickles,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2468,Shipping,Shipsanity: Pine Tar,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2469,Shipping,Shipsanity: Truffle Oil,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2470,Shipping,Shipsanity: Void Mayonnaise,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2471,Shipping,Shipsanity: Wine,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2472,Shipping,Shipsanity: Algae Soup,SHIPSANITY,
+2473,Shipping,Shipsanity: Artichoke Dip,SHIPSANITY,
+2474,Shipping,Shipsanity: Autumn's Bounty,SHIPSANITY,
+2475,Shipping,Shipsanity: Baked Fish,SHIPSANITY,
+2476,Shipping,Shipsanity: Bean Hotpot,SHIPSANITY,
+2477,Shipping,Shipsanity: Blackberry Cobbler,SHIPSANITY,
+2478,Shipping,Shipsanity: Blueberry Tart,SHIPSANITY,
+2479,Shipping,Shipsanity: Bread,SHIPSANITY,
+2480,Shipping,Shipsanity: Bruschetta,SHIPSANITY,
+2481,Shipping,Shipsanity: Carp Surprise,SHIPSANITY,
+2482,Shipping,Shipsanity: Cheese Cauliflower,SHIPSANITY,
+2483,Shipping,Shipsanity: Chocolate Cake,SHIPSANITY,
+2484,Shipping,Shipsanity: Chowder,SHIPSANITY,
+2485,Shipping,Shipsanity: Coleslaw,SHIPSANITY,
+2486,Shipping,Shipsanity: Complete Breakfast,SHIPSANITY,
+2487,Shipping,Shipsanity: Cookies,SHIPSANITY,
+2488,Shipping,Shipsanity: Crab Cakes,SHIPSANITY,
+2489,Shipping,Shipsanity: Cranberry Candy,SHIPSANITY,
+2490,Shipping,Shipsanity: Cranberry Sauce,SHIPSANITY,
+2491,Shipping,Shipsanity: Crispy Bass,SHIPSANITY,
+2492,Shipping,Shipsanity: Dish O' The Sea,SHIPSANITY,
+2493,Shipping,Shipsanity: Eggplant Parmesan,SHIPSANITY,
+2494,Shipping,Shipsanity: Escargot,SHIPSANITY,
+2495,Shipping,Shipsanity: Farmer's Lunch,SHIPSANITY,
+2496,Shipping,Shipsanity: Fiddlehead Risotto,SHIPSANITY,
+2497,Shipping,Shipsanity: Fish Stew,SHIPSANITY,
+2498,Shipping,Shipsanity: Fish Taco,SHIPSANITY,
+2499,Shipping,Shipsanity: Fried Calamari,SHIPSANITY,
+2500,Shipping,Shipsanity: Fried Eel,SHIPSANITY,
+2501,Shipping,Shipsanity: Fried Egg,SHIPSANITY,
+2502,Shipping,Shipsanity: Fried Mushroom,SHIPSANITY,
+2503,Shipping,Shipsanity: Fruit Salad,SHIPSANITY,
+2504,Shipping,Shipsanity: Glazed Yams,SHIPSANITY,
+2505,Shipping,Shipsanity: Hashbrowns,SHIPSANITY,
+2506,Shipping,Shipsanity: Ice Cream,SHIPSANITY,
+2507,Shipping,Shipsanity: Lobster Bisque,SHIPSANITY,
+2508,Shipping,Shipsanity: Lucky Lunch,SHIPSANITY,
+2509,Shipping,Shipsanity: Maki Roll,SHIPSANITY,
+2510,Shipping,Shipsanity: Maple Bar,SHIPSANITY,
+2511,Shipping,Shipsanity: Miner's Treat,SHIPSANITY,
+2512,Shipping,Shipsanity: Omelet,SHIPSANITY,
+2513,Shipping,Shipsanity: Pale Broth,SHIPSANITY,
+2514,Shipping,Shipsanity: Pancakes,SHIPSANITY,
+2515,Shipping,Shipsanity: Parsnip Soup,SHIPSANITY,
+2516,Shipping,Shipsanity: Pepper Poppers,SHIPSANITY,
+2517,Shipping,Shipsanity: Pink Cake,SHIPSANITY,
+2518,Shipping,Shipsanity: Pizza,SHIPSANITY,
+2519,Shipping,Shipsanity: Plum Pudding,SHIPSANITY,
+2520,Shipping,Shipsanity: Poppyseed Muffin,SHIPSANITY,
+2521,Shipping,Shipsanity: Pumpkin Pie,SHIPSANITY,
+2522,Shipping,Shipsanity: Pumpkin Soup,SHIPSANITY,
+2523,Shipping,Shipsanity: Radish Salad,SHIPSANITY,
+2524,Shipping,Shipsanity: Red Plate,SHIPSANITY,
+2525,Shipping,Shipsanity: Rhubarb Pie,SHIPSANITY,
+2526,Shipping,Shipsanity: Rice Pudding,SHIPSANITY,
+2527,Shipping,Shipsanity: Roasted Hazelnuts,SHIPSANITY,
+2528,Shipping,Shipsanity: Roots Platter,SHIPSANITY,
+2529,Shipping,Shipsanity: Salad,SHIPSANITY,
+2530,Shipping,Shipsanity: Salmon Dinner,SHIPSANITY,
+2531,Shipping,Shipsanity: Sashimi,SHIPSANITY,
+2532,Shipping,Shipsanity: Seafoam Pudding,SHIPSANITY,
+2533,Shipping,Shipsanity: Shrimp Cocktail,SHIPSANITY,
+2534,Shipping,Shipsanity: Spaghetti,SHIPSANITY,
+2535,Shipping,Shipsanity: Spicy Eel,SHIPSANITY,
+2536,Shipping,Shipsanity: Squid Ink Ravioli,SHIPSANITY,
+2537,Shipping,Shipsanity: Stir Fry,SHIPSANITY,
+2538,Shipping,Shipsanity: Strange Bun,SHIPSANITY,
+2539,Shipping,Shipsanity: Stuffing,SHIPSANITY,
+2540,Shipping,Shipsanity: Super Meal,SHIPSANITY,
+2541,Shipping,Shipsanity: Survival Burger,SHIPSANITY,
+2542,Shipping,Shipsanity: Tom Kha Soup,SHIPSANITY,
+2543,Shipping,Shipsanity: Tortilla,SHIPSANITY,
+2544,Shipping,Shipsanity: Triple Shot Espresso,SHIPSANITY,
+2545,Shipping,Shipsanity: Trout Soup,SHIPSANITY,
+2546,Shipping,Shipsanity: Vegetable Medley,SHIPSANITY,
+2547,Shipping,Shipsanity: Bait,SHIPSANITY,
+2548,Shipping,Shipsanity: Barbed Hook,SHIPSANITY,
+2549,Shipping,Shipsanity: Basic Fertilizer,SHIPSANITY,
+2550,Shipping,Shipsanity: Basic Retaining Soil,SHIPSANITY,
+2551,Shipping,Shipsanity: Blue Slime Egg,SHIPSANITY,
+2552,Shipping,Shipsanity: Bomb,SHIPSANITY,
+2553,Shipping,Shipsanity: Brick Floor,SHIPSANITY,
+2554,Shipping,Shipsanity: Bug Steak,SHIPSANITY,
+2555,Shipping,Shipsanity: Cherry Bomb,SHIPSANITY,
+2556,Shipping,Shipsanity: Cobblestone Path,SHIPSANITY,
+2557,Shipping,Shipsanity: Cookout Kit,SHIPSANITY,
+2558,Shipping,Shipsanity: Cork Bobber,SHIPSANITY,
+2559,Shipping,Shipsanity: Crab Pot,SHIPSANITY,
+2560,Shipping,Shipsanity: Crystal Floor,SHIPSANITY,
+2561,Shipping,Shipsanity: Crystal Path,SHIPSANITY,
+2562,Shipping,Shipsanity: Deluxe Speed-Gro,SHIPSANITY,
+2563,Shipping,Shipsanity: Dressed Spinner,SHIPSANITY,
+2564,Shipping,Shipsanity: Drum Block,SHIPSANITY,
+2565,Shipping,Shipsanity: Explosive Ammo,SHIPSANITY,
+2566,Shipping,Shipsanity: Fiber Seeds,SHIPSANITY,
+2567,Shipping,Shipsanity: Field Snack,SHIPSANITY,
+2568,Shipping,Shipsanity: Flute Block,SHIPSANITY,
+2569,Shipping,Shipsanity: Gate,SHIPSANITY,
+2570,Shipping,Shipsanity: Gravel Path,SHIPSANITY,
+2571,Shipping,Shipsanity: Green Slime Egg,SHIPSANITY,
+2572,Shipping,Shipsanity: Hardwood Fence,SHIPSANITY,
+2573,Shipping,Shipsanity: Iridium Sprinkler,SHIPSANITY,
+2574,Shipping,Shipsanity: Iron Fence,SHIPSANITY,
+2575,Shipping,Shipsanity: Jack-O-Lantern,SHIPSANITY,
+2576,Shipping,Shipsanity: Lead Bobber,SHIPSANITY,
+2577,Shipping,Shipsanity: Life Elixir,SHIPSANITY,
+2578,Shipping,Shipsanity: Magnet,SHIPSANITY,
+2579,Shipping,Shipsanity: Mega Bomb,SHIPSANITY,
+2580,Shipping,Shipsanity: Monster Musk,SHIPSANITY,
+2581,Shipping,Shipsanity: Oil of Garlic,SHIPSANITY,
+2582,Shipping,Shipsanity: Purple Slime Egg,SHIPSANITY,
+2583,Shipping,Shipsanity: Quality Bobber,SHIPSANITY,
+2584,Shipping,Shipsanity: Quality Fertilizer,SHIPSANITY,
+2585,Shipping,Shipsanity: Quality Retaining Soil,SHIPSANITY,
+2586,Shipping,Shipsanity: Quality Sprinkler,SHIPSANITY,
+2587,Shipping,Shipsanity: Rain Totem,SHIPSANITY,
+2588,Shipping,Shipsanity: Red Slime Egg,SHIPSANITY,
+2589,Shipping,Shipsanity: Rustic Plank Floor,SHIPSANITY,
+2590,Shipping,Shipsanity: Speed-Gro,SHIPSANITY,
+2591,Shipping,Shipsanity: Spinner,SHIPSANITY,
+2592,Shipping,Shipsanity: Sprinkler,SHIPSANITY,
+2593,Shipping,Shipsanity: Stepping Stone Path,SHIPSANITY,
+2594,Shipping,Shipsanity: Stone Fence,SHIPSANITY,
+2595,Shipping,Shipsanity: Stone Floor,SHIPSANITY,
+2596,Shipping,Shipsanity: Stone Walkway Floor,SHIPSANITY,
+2597,Shipping,Shipsanity: Straw Floor,SHIPSANITY,
+2598,Shipping,Shipsanity: Torch,SHIPSANITY,
+2599,Shipping,Shipsanity: Trap Bobber,SHIPSANITY,
+2600,Shipping,Shipsanity: Treasure Hunter,SHIPSANITY,
+2601,Shipping,Shipsanity: Tree Fertilizer,SHIPSANITY,
+2602,Shipping,Shipsanity: Warp Totem: Beach,SHIPSANITY,
+2603,Shipping,Shipsanity: Warp Totem: Desert,SHIPSANITY,
+2604,Shipping,Shipsanity: Warp Totem: Farm,SHIPSANITY,
+2605,Shipping,Shipsanity: Warp Totem: Island,"SHIPSANITY,GINGER_ISLAND",
+2606,Shipping,Shipsanity: Warp Totem: Mountains,SHIPSANITY,
+2607,Shipping,Shipsanity: Weathered Floor,SHIPSANITY,
+2608,Shipping,Shipsanity: Wild Bait,SHIPSANITY,
+2609,Shipping,Shipsanity: Wood Fence,SHIPSANITY,
+2610,Shipping,Shipsanity: Wood Floor,SHIPSANITY,
+2611,Shipping,Shipsanity: Wood Path,SHIPSANITY,
+2612,Shipping,Shipsanity: Amaranth,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2613,Shipping,Shipsanity: Ancient Fruit,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2614,Shipping,Shipsanity: Apple,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2615,Shipping,Shipsanity: Apricot,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2616,Shipping,Shipsanity: Artichoke,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2617,Shipping,Shipsanity: Beet,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2618,Shipping,Shipsanity: Blue Jazz,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2619,Shipping,Shipsanity: Blueberry,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2620,Shipping,Shipsanity: Bok Choy,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2621,Shipping,Shipsanity: Cauliflower,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2622,Shipping,Shipsanity: Cherry,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2623,Shipping,Shipsanity: Corn,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2624,Shipping,Shipsanity: Cranberries,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2625,Shipping,Shipsanity: Eggplant,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2626,Shipping,Shipsanity: Fairy Rose,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2627,Shipping,Shipsanity: Garlic,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2628,Shipping,Shipsanity: Grape,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2629,Shipping,Shipsanity: Green Bean,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2630,Shipping,Shipsanity: Hops,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2631,Shipping,Shipsanity: Hot Pepper,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2632,Shipping,Shipsanity: Kale,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2633,Shipping,Shipsanity: Melon,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2634,Shipping,Shipsanity: Orange,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2635,Shipping,Shipsanity: Parsnip,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2636,Shipping,Shipsanity: Peach,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2637,Shipping,Shipsanity: Pomegranate,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2638,Shipping,Shipsanity: Poppy,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2639,Shipping,Shipsanity: Potato,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2640,Shipping,Shipsanity: Pumpkin,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2641,Shipping,Shipsanity: Radish,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2642,Shipping,Shipsanity: Red Cabbage,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2643,Shipping,Shipsanity: Rhubarb,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2644,Shipping,Shipsanity: Starfruit,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2645,Shipping,Shipsanity: Strawberry,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2646,Shipping,Shipsanity: Summer Spangle,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2647,Shipping,Shipsanity: Sunflower,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2648,Shipping,Shipsanity: Sweet Gem Berry,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2649,Shipping,Shipsanity: Tea Leaves,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2650,Shipping,Shipsanity: Tomato,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2651,Shipping,Shipsanity: Tulip,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2652,Shipping,Shipsanity: Unmilled Rice,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2653,Shipping,Shipsanity: Wheat,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2654,Shipping,Shipsanity: Yam,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2655,Shipping,Shipsanity: Albacore,"SHIPSANITY,SHIPSANITY_FISH",
+2656,Shipping,Shipsanity: Anchovy,"SHIPSANITY,SHIPSANITY_FISH",
+2657,Shipping,Shipsanity: Angler,"SHIPSANITY,SHIPSANITY_FISH",
+2658,Shipping,Shipsanity: Blobfish,"SHIPSANITY,SHIPSANITY_FISH",
+2659,Shipping,Shipsanity: Bream,"SHIPSANITY,SHIPSANITY_FISH",
+2660,Shipping,Shipsanity: Bullhead,"SHIPSANITY,SHIPSANITY_FISH",
+2661,Shipping,Shipsanity: Carp,"SHIPSANITY,SHIPSANITY_FISH",
+2662,Shipping,Shipsanity: Catfish,"SHIPSANITY,SHIPSANITY_FISH",
+2663,Shipping,Shipsanity: Chub,"SHIPSANITY,SHIPSANITY_FISH",
+2664,Shipping,Shipsanity: Cockle,"SHIPSANITY,SHIPSANITY_FISH",
+2665,Shipping,Shipsanity: Crab,"SHIPSANITY,SHIPSANITY_FISH",
+2666,Shipping,Shipsanity: Crayfish,"SHIPSANITY,SHIPSANITY_FISH",
+2667,Shipping,Shipsanity: Crimsonfish,"SHIPSANITY,SHIPSANITY_FISH",
+2668,Shipping,Shipsanity: Dorado,"SHIPSANITY,SHIPSANITY_FISH",
+2669,Shipping,Shipsanity: Eel,"SHIPSANITY,SHIPSANITY_FISH",
+2670,Shipping,Shipsanity: Flounder,"SHIPSANITY,SHIPSANITY_FISH",
+2671,Shipping,Shipsanity: Ghostfish,"SHIPSANITY,SHIPSANITY_FISH",
+2672,Shipping,Shipsanity: Glacierfish,"SHIPSANITY,SHIPSANITY_FISH",
+2673,Shipping,Shipsanity: Halibut,"SHIPSANITY,SHIPSANITY_FISH",
+2674,Shipping,Shipsanity: Herring,"SHIPSANITY,SHIPSANITY_FISH",
+2675,Shipping,Shipsanity: Ice Pip,"SHIPSANITY,SHIPSANITY_FISH",
+2676,Shipping,Shipsanity: Largemouth Bass,"SHIPSANITY,SHIPSANITY_FISH",
+2677,Shipping,Shipsanity: Lava Eel,"SHIPSANITY,SHIPSANITY_FISH",
+2678,Shipping,Shipsanity: Legend,"SHIPSANITY,SHIPSANITY_FISH",
+2679,Shipping,Shipsanity: Lingcod,"SHIPSANITY,SHIPSANITY_FISH",
+2680,Shipping,Shipsanity: Lobster,"SHIPSANITY,SHIPSANITY_FISH",
+2681,Shipping,Shipsanity: Midnight Carp,"SHIPSANITY,SHIPSANITY_FISH",
+2682,Shipping,Shipsanity: Midnight Squid,"SHIPSANITY,SHIPSANITY_FISH",
+2683,Shipping,Shipsanity: Mussel,"SHIPSANITY,SHIPSANITY_FISH",
+2684,Shipping,Shipsanity: Mutant Carp,"SHIPSANITY,SHIPSANITY_FISH",
+2685,Shipping,Shipsanity: Octopus,"SHIPSANITY,SHIPSANITY_FISH",
+2686,Shipping,Shipsanity: Oyster,"SHIPSANITY,SHIPSANITY_FISH",
+2687,Shipping,Shipsanity: Perch,"SHIPSANITY,SHIPSANITY_FISH",
+2688,Shipping,Shipsanity: Periwinkle,"SHIPSANITY,SHIPSANITY_FISH",
+2689,Shipping,Shipsanity: Pike,"SHIPSANITY,SHIPSANITY_FISH",
+2690,Shipping,Shipsanity: Pufferfish,"SHIPSANITY,SHIPSANITY_FISH",
+2691,Shipping,Shipsanity: Rainbow Trout,"SHIPSANITY,SHIPSANITY_FISH",
+2692,Shipping,Shipsanity: Red Mullet,"SHIPSANITY,SHIPSANITY_FISH",
+2693,Shipping,Shipsanity: Red Snapper,"SHIPSANITY,SHIPSANITY_FISH",
+2694,Shipping,Shipsanity: Salmon,"SHIPSANITY,SHIPSANITY_FISH",
+2695,Shipping,Shipsanity: Sandfish,"SHIPSANITY,SHIPSANITY_FISH",
+2696,Shipping,Shipsanity: Sardine,"SHIPSANITY,SHIPSANITY_FISH",
+2697,Shipping,Shipsanity: Scorpion Carp,"SHIPSANITY,SHIPSANITY_FISH",
+2698,Shipping,Shipsanity: Sea Cucumber,"SHIPSANITY,SHIPSANITY_FISH",
+2699,Shipping,Shipsanity: Shad,"SHIPSANITY,SHIPSANITY_FISH",
+2700,Shipping,Shipsanity: Shrimp,"SHIPSANITY,SHIPSANITY_FISH",
+2701,Shipping,Shipsanity: Slimejack,"SHIPSANITY,SHIPSANITY_FISH",
+2702,Shipping,Shipsanity: Smallmouth Bass,"SHIPSANITY,SHIPSANITY_FISH",
+2703,Shipping,Shipsanity: Snail,"SHIPSANITY,SHIPSANITY_FISH",
+2704,Shipping,Shipsanity: Spook Fish,"SHIPSANITY,SHIPSANITY_FISH",
+2705,Shipping,Shipsanity: Squid,"SHIPSANITY,SHIPSANITY_FISH",
+2706,Shipping,Shipsanity: Stonefish,"SHIPSANITY,SHIPSANITY_FISH",
+2707,Shipping,Shipsanity: Sturgeon,"SHIPSANITY,SHIPSANITY_FISH",
+2708,Shipping,Shipsanity: Sunfish,"SHIPSANITY,SHIPSANITY_FISH",
+2709,Shipping,Shipsanity: Super Cucumber,"SHIPSANITY,SHIPSANITY_FISH",
+2710,Shipping,Shipsanity: Tiger Trout,"SHIPSANITY,SHIPSANITY_FISH",
+2711,Shipping,Shipsanity: Tilapia,"SHIPSANITY,SHIPSANITY_FISH",
+2712,Shipping,Shipsanity: Tuna,"SHIPSANITY,SHIPSANITY_FISH",
+2713,Shipping,Shipsanity: Void Salmon,"SHIPSANITY,SHIPSANITY_FISH",
+2714,Shipping,Shipsanity: Walleye,"SHIPSANITY,SHIPSANITY_FISH",
+2715,Shipping,Shipsanity: Woodskip,"SHIPSANITY,SHIPSANITY_FISH",
+2716,Shipping,Shipsanity: Blackberry,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2717,Shipping,Shipsanity: Cactus Fruit,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2718,Shipping,Shipsanity: Cave Carrot,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2719,Shipping,Shipsanity: Chanterelle,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2720,Shipping,Shipsanity: Clam,"SHIPSANITY,SHIPSANITY_FISH",
+2721,Shipping,Shipsanity: Coconut,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2722,Shipping,Shipsanity: Common Mushroom,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2723,Shipping,Shipsanity: Coral,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2724,Shipping,Shipsanity: Crocus,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2725,Shipping,Shipsanity: Crystal Fruit,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2726,Shipping,Shipsanity: Daffodil,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2727,Shipping,Shipsanity: Dandelion,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2728,Shipping,Shipsanity: Fiddlehead Fern,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2729,Shipping,Shipsanity: Hazelnut,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2730,Shipping,Shipsanity: Holly,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2731,Shipping,Shipsanity: Leek,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2732,Shipping,Shipsanity: Morel,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2733,Shipping,Shipsanity: Nautilus Shell,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2734,Shipping,Shipsanity: Purple Mushroom,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2735,Shipping,Shipsanity: Rainbow Shell,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2736,Shipping,Shipsanity: Red Mushroom,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2737,Shipping,Shipsanity: Salmonberry,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2738,Shipping,Shipsanity: Sea Urchin,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2739,Shipping,Shipsanity: Snow Yam,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2740,Shipping,Shipsanity: Spice Berry,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2741,Shipping,Shipsanity: Spring Onion,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2742,Shipping,Shipsanity: Sweet Pea,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2743,Shipping,Shipsanity: Wild Horseradish,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2744,Shipping,Shipsanity: Wild Plum,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2745,Shipping,Shipsanity: Winter Root,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2746,Shipping,Shipsanity: Tea Set,SHIPSANITY,
+2747,Shipping,Shipsanity: Battery Pack,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2748,Shipping,Shipsanity: Clay,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2749,Shipping,Shipsanity: Copper Bar,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2750,Shipping,Shipsanity: Fiber,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2751,Shipping,Shipsanity: Gold Bar,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2752,Shipping,Shipsanity: Hardwood,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2753,Shipping,Shipsanity: Iridium Bar,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2754,Shipping,Shipsanity: Iron Bar,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2755,Shipping,Shipsanity: Oil,SHIPSANITY,
+2756,Shipping,Shipsanity: Refined Quartz,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2757,Shipping,Shipsanity: Rice,SHIPSANITY,
+2758,Shipping,Shipsanity: Sap,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2759,Shipping,Shipsanity: Stone,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2760,Shipping,Shipsanity: Sugar,SHIPSANITY,
+2761,Shipping,Shipsanity: Vinegar,SHIPSANITY,
+2762,Shipping,Shipsanity: Wheat Flour,SHIPSANITY,
+2763,Shipping,Shipsanity: Wood,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2764,Shipping,Shipsanity: Aerinite,SHIPSANITY,
+2765,Shipping,Shipsanity: Alamite,SHIPSANITY,
+2766,Shipping,Shipsanity: Amethyst,SHIPSANITY,
+2767,Shipping,Shipsanity: Amphibian Fossil,SHIPSANITY,
+2768,Shipping,Shipsanity: Aquamarine,SHIPSANITY,
+2769,Shipping,Shipsanity: Baryte,SHIPSANITY,
+2770,Shipping,Shipsanity: Basalt,SHIPSANITY,
+2771,Shipping,Shipsanity: Bixite,SHIPSANITY,
+2772,Shipping,Shipsanity: Calcite,SHIPSANITY,
+2773,Shipping,Shipsanity: Celestine,SHIPSANITY,
+2774,Shipping,Shipsanity: Coal,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2775,Shipping,Shipsanity: Copper Ore,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2776,Shipping,Shipsanity: Diamond,SHIPSANITY,
+2777,Shipping,Shipsanity: Dolomite,SHIPSANITY,
+2778,Shipping,Shipsanity: Earth Crystal,SHIPSANITY,
+2779,Shipping,Shipsanity: Emerald,SHIPSANITY,
+2780,Shipping,Shipsanity: Esperite,SHIPSANITY,
+2781,Shipping,Shipsanity: Fairy Stone,SHIPSANITY,
+2782,Shipping,Shipsanity: Fire Opal,SHIPSANITY,
+2783,Shipping,Shipsanity: Fire Quartz,SHIPSANITY,
+2784,Shipping,Shipsanity: Fluorapatite,SHIPSANITY,
+2785,Shipping,Shipsanity: Frozen Geode,SHIPSANITY,
+2786,Shipping,Shipsanity: Frozen Tear,SHIPSANITY,
+2787,Shipping,Shipsanity: Geminite,SHIPSANITY,
+2788,Shipping,Shipsanity: Geode,SHIPSANITY,
+2789,Shipping,Shipsanity: Ghost Crystal,SHIPSANITY,
+2790,Shipping,Shipsanity: Gold Ore,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2791,Shipping,Shipsanity: Granite,SHIPSANITY,
+2792,Shipping,Shipsanity: Helvite,SHIPSANITY,
+2793,Shipping,Shipsanity: Hematite,SHIPSANITY,
+2794,Shipping,Shipsanity: Iridium Ore,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2795,Shipping,Shipsanity: Iron Ore,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2796,Shipping,Shipsanity: Jade,SHIPSANITY,
+2797,Shipping,Shipsanity: Jagoite,SHIPSANITY,
+2798,Shipping,Shipsanity: Jamborite,SHIPSANITY,
+2799,Shipping,Shipsanity: Jasper,SHIPSANITY,
+2800,Shipping,Shipsanity: Kyanite,SHIPSANITY,
+2801,Shipping,Shipsanity: Lemon Stone,SHIPSANITY,
+2802,Shipping,Shipsanity: Limestone,SHIPSANITY,
+2803,Shipping,Shipsanity: Lunarite,SHIPSANITY,
+2804,Shipping,Shipsanity: Magma Geode,SHIPSANITY,
+2805,Shipping,Shipsanity: Malachite,SHIPSANITY,
+2806,Shipping,Shipsanity: Marble,SHIPSANITY,
+2807,Shipping,Shipsanity: Mudstone,SHIPSANITY,
+2808,Shipping,Shipsanity: Nautilus Fossil,SHIPSANITY,
+2809,Shipping,Shipsanity: Nekoite,SHIPSANITY,
+2810,Shipping,Shipsanity: Neptunite,SHIPSANITY,
+2811,Shipping,Shipsanity: Obsidian,SHIPSANITY,
+2812,Shipping,Shipsanity: Ocean Stone,SHIPSANITY,
+2813,Shipping,Shipsanity: Omni Geode,SHIPSANITY,
+2814,Shipping,Shipsanity: Opal,SHIPSANITY,
+2815,Shipping,Shipsanity: Orpiment,SHIPSANITY,
+2816,Shipping,Shipsanity: Palm Fossil,SHIPSANITY,
+2817,Shipping,Shipsanity: Petrified Slime,SHIPSANITY,
+2818,Shipping,Shipsanity: Prehistoric Rib,SHIPSANITY,
+2819,Shipping,Shipsanity: Prehistoric Scapula,SHIPSANITY,
+2820,Shipping,Shipsanity: Prehistoric Skull,SHIPSANITY,
+2821,Shipping,Shipsanity: Prehistoric Tibia,SHIPSANITY,
+2822,Shipping,Shipsanity: Prehistoric Vertebra,SHIPSANITY,
+2823,Shipping,Shipsanity: Prismatic Shard,SHIPSANITY,
+2824,Shipping,Shipsanity: Pyrite,SHIPSANITY,
+2825,Shipping,Shipsanity: Quartz,SHIPSANITY,
+2826,Shipping,Shipsanity: Ruby,SHIPSANITY,
+2827,Shipping,Shipsanity: Sandstone,SHIPSANITY,
+2828,Shipping,Shipsanity: Skeletal Hand,SHIPSANITY,
+2829,Shipping,Shipsanity: Skeletal Tail,SHIPSANITY,
+2830,Shipping,Shipsanity: Slate,SHIPSANITY,
+2831,Shipping,Shipsanity: Soapstone,SHIPSANITY,
+2832,Shipping,Shipsanity: Star Shards,SHIPSANITY,
+2833,Shipping,Shipsanity: Thunder Egg,SHIPSANITY,
+2834,Shipping,Shipsanity: Tigerseye,SHIPSANITY,
+2835,Shipping,Shipsanity: Topaz,SHIPSANITY,
+2836,Shipping,Shipsanity: Trilobite,SHIPSANITY,
+2837,Shipping,Shipsanity: Bat Wing,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2838,Shipping,Shipsanity: Bone Fragment,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2839,Shipping,Shipsanity: Curiosity Lure,SHIPSANITY,
+2840,Shipping,Shipsanity: Slime,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2841,Shipping,Shipsanity: Solar Essence,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2842,Shipping,Shipsanity: Squid Ink,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2843,Shipping,Shipsanity: Void Essence,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2844,Shipping,Shipsanity: Bouquet,SHIPSANITY,
+2845,Shipping,Shipsanity: Energy Tonic,SHIPSANITY,
+2846,Shipping,Shipsanity: Golden Pumpkin,SHIPSANITY,
+2847,Shipping,Shipsanity: Green Algae,SHIPSANITY,
+2848,Shipping,Shipsanity: Hay,SHIPSANITY,
+2849,Shipping,Shipsanity: Magic Rock Candy,SHIPSANITY,
+2850,Shipping,Shipsanity: Muscle Remedy,SHIPSANITY,
+2851,Shipping,Shipsanity: Pearl,SHIPSANITY,
+2852,Shipping,Shipsanity: Rotten Plant,SHIPSANITY,
+2853,Shipping,Shipsanity: Seaweed,SHIPSANITY,
+2854,Shipping,Shipsanity: Void Ghost Pendant,SHIPSANITY,
+2855,Shipping,Shipsanity: White Algae,SHIPSANITY,
+2856,Shipping,Shipsanity: Wilted Bouquet,SHIPSANITY,
+2857,Shipping,Shipsanity: Secret Note,SHIPSANITY,
+2858,Shipping,Shipsanity: Acorn,SHIPSANITY,
+2859,Shipping,Shipsanity: Amaranth Seeds,SHIPSANITY,
+2860,Shipping,Shipsanity: Ancient Seeds,SHIPSANITY,
+2861,Shipping,Shipsanity: Apple Sapling,SHIPSANITY,
+2862,Shipping,Shipsanity: Apricot Sapling,SHIPSANITY,
+2863,Shipping,Shipsanity: Artichoke Seeds,SHIPSANITY,
+2864,Shipping,Shipsanity: Bean Starter,SHIPSANITY,
+2865,Shipping,Shipsanity: Beet Seeds,SHIPSANITY,
+2866,Shipping,Shipsanity: Blueberry Seeds,SHIPSANITY,
+2867,Shipping,Shipsanity: Bok Choy Seeds,SHIPSANITY,
+2868,Shipping,Shipsanity: Cactus Seeds,SHIPSANITY,
+2869,Shipping,Shipsanity: Cauliflower Seeds,SHIPSANITY,
+2870,Shipping,Shipsanity: Cherry Sapling,SHIPSANITY,
+2871,Shipping,Shipsanity: Coffee Bean,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2872,Shipping,Shipsanity: Corn Seeds,SHIPSANITY,
+2873,Shipping,Shipsanity: Cranberry Seeds,SHIPSANITY,
+2874,Shipping,Shipsanity: Eggplant Seeds,SHIPSANITY,
+2875,Shipping,Shipsanity: Fairy Seeds,SHIPSANITY,
+2876,Shipping,Shipsanity: Fall Seeds,SHIPSANITY,
+2877,Shipping,Shipsanity: Garlic Seeds,SHIPSANITY,
+2878,Shipping,Shipsanity: Grape Starter,SHIPSANITY,
+2879,Shipping,Shipsanity: Grass Starter,SHIPSANITY,
+2880,Shipping,Shipsanity: Hops Starter,SHIPSANITY,
+2881,Shipping,Shipsanity: Jazz Seeds,SHIPSANITY,
+2882,Shipping,Shipsanity: Kale Seeds,SHIPSANITY,
+2883,Shipping,Shipsanity: Mahogany Seed,SHIPSANITY,
+2884,Shipping,Shipsanity: Maple Seed,SHIPSANITY,
+2885,Shipping,Shipsanity: Melon Seeds,SHIPSANITY,
+2886,Shipping,Shipsanity: Mixed Seeds,SHIPSANITY,
+2887,Shipping,Shipsanity: Orange Sapling,SHIPSANITY,
+2888,Shipping,Shipsanity: Parsnip Seeds,SHIPSANITY,
+2889,Shipping,Shipsanity: Peach Sapling,SHIPSANITY,
+2890,Shipping,Shipsanity: Pepper Seeds,SHIPSANITY,
+2891,Shipping,Shipsanity: Pine Cone,SHIPSANITY,
+2892,Shipping,Shipsanity: Pomegranate Sapling,SHIPSANITY,
+2893,Shipping,Shipsanity: Poppy Seeds,SHIPSANITY,
+2894,Shipping,Shipsanity: Potato Seeds,SHIPSANITY,
+2895,Shipping,Shipsanity: Pumpkin Seeds,SHIPSANITY,
+2896,Shipping,Shipsanity: Radish Seeds,SHIPSANITY,
+2897,Shipping,Shipsanity: Rare Seed,SHIPSANITY,
+2898,Shipping,Shipsanity: Red Cabbage Seeds,SHIPSANITY,
+2899,Shipping,Shipsanity: Rhubarb Seeds,SHIPSANITY,
+2900,Shipping,Shipsanity: Rice Shoot,SHIPSANITY,
+2901,Shipping,Shipsanity: Spangle Seeds,SHIPSANITY,
+2902,Shipping,Shipsanity: Spring Seeds,SHIPSANITY,
+2903,Shipping,Shipsanity: Starfruit Seeds,SHIPSANITY,
+2904,Shipping,Shipsanity: Strawberry Seeds,SHIPSANITY,
+2905,Shipping,Shipsanity: Summer Seeds,SHIPSANITY,
+2906,Shipping,Shipsanity: Sunflower Seeds,SHIPSANITY,
+2907,Shipping,Shipsanity: Tea Sapling,SHIPSANITY,
+2908,Shipping,Shipsanity: Tomato Seeds,SHIPSANITY,
+2909,Shipping,Shipsanity: Tulip Bulb,SHIPSANITY,
+2910,Shipping,Shipsanity: Wheat Seeds,SHIPSANITY,
+2911,Shipping,Shipsanity: Winter Seeds,SHIPSANITY,
+2912,Shipping,Shipsanity: Yam Seeds,SHIPSANITY,
+2913,Shipping,Shipsanity: Broken CD,SHIPSANITY,
+2914,Shipping,Shipsanity: Broken Glasses,SHIPSANITY,
+2915,Shipping,Shipsanity: Driftwood,SHIPSANITY,
+2916,Shipping,Shipsanity: Joja Cola,SHIPSANITY,
+2917,Shipping,Shipsanity: Soggy Newspaper,SHIPSANITY,
+2918,Shipping,Shipsanity: Trash,SHIPSANITY,
+2919,Shipping,Shipsanity: Bug Meat,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2920,Shipping,Shipsanity: Golden Egg,SHIPSANITY,
+2921,Shipping,Shipsanity: Ostrich Egg,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2922,Shipping,Shipsanity: Fossilized Leg,"GINGER_ISLAND,SHIPSANITY",
+2923,Shipping,Shipsanity: Fossilized Ribs,"GINGER_ISLAND,SHIPSANITY",
+2924,Shipping,Shipsanity: Fossilized Skull,"GINGER_ISLAND,SHIPSANITY",
+2925,Shipping,Shipsanity: Fossilized Spine,"GINGER_ISLAND,SHIPSANITY",
+2926,Shipping,Shipsanity: Fossilized Tail,"GINGER_ISLAND,SHIPSANITY",
+2927,Shipping,Shipsanity: Mummified Bat,"GINGER_ISLAND,SHIPSANITY",
+2928,Shipping,Shipsanity: Mummified Frog,"GINGER_ISLAND,SHIPSANITY",
+2929,Shipping,Shipsanity: Snake Skull,"GINGER_ISLAND,SHIPSANITY",
+2930,Shipping,Shipsanity: Snake Vertebrae,"GINGER_ISLAND,SHIPSANITY",
+2931,Shipping,Shipsanity: Banana Pudding,"GINGER_ISLAND,SHIPSANITY",
+2932,Shipping,Shipsanity: Ginger Ale,"GINGER_ISLAND,SHIPSANITY",
+2933,Shipping,Shipsanity: Mango Sticky Rice,"GINGER_ISLAND,SHIPSANITY",
+2934,Shipping,Shipsanity: Pina Colada,"GINGER_ISLAND,SHIPSANITY",
+2935,Shipping,Shipsanity: Poi,"GINGER_ISLAND,SHIPSANITY",
+2936,Shipping,Shipsanity: Tropical Curry,"GINGER_ISLAND,SHIPSANITY",
+2937,Shipping,Shipsanity: Deluxe Fertilizer,"GINGER_ISLAND,SHIPSANITY",
+2938,Shipping,Shipsanity: Deluxe Retaining Soil,"GINGER_ISLAND,SHIPSANITY",
+2939,Shipping,Shipsanity: Fairy Dust,"GINGER_ISLAND,SHIPSANITY",
+2940,Shipping,Shipsanity: Hyper Speed-Gro,"GINGER_ISLAND,SHIPSANITY",
+2941,Shipping,Shipsanity: Magic Bait,"GINGER_ISLAND,SHIPSANITY",
+2942,Shipping,Shipsanity: Banana,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2943,Shipping,Shipsanity: Mango,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2944,Shipping,Shipsanity: Pineapple,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2945,Shipping,Shipsanity: Qi Fruit,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_CROP,REQUIRES_QI_ORDERS",
+2946,Shipping,Shipsanity: Taro Root,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2947,Shipping,Shipsanity: Blue Discus,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_FISH",
+2948,Shipping,Shipsanity: Glacierfish Jr.,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_FISH,REQUIRES_QI_ORDERS",
+2949,Shipping,Shipsanity: Legend II,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_FISH,REQUIRES_QI_ORDERS",
+2950,Shipping,Shipsanity: Lionfish,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_FISH",
+2951,Shipping,Shipsanity: Ms. Angler,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_FISH,REQUIRES_QI_ORDERS",
+2952,Shipping,Shipsanity: Radioactive Carp,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_FISH,REQUIRES_QI_ORDERS",
+2953,Shipping,Shipsanity: Son of Crimsonfish,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_FISH,REQUIRES_QI_ORDERS",
+2954,Shipping,Shipsanity: Stingray,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_FISH",
+2955,Shipping,Shipsanity: Ginger,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2956,Shipping,Shipsanity: Magma Cap,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+2957,Shipping,Shipsanity: Cinder Shard,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+2958,Shipping,Shipsanity: Dragon Tooth,"GINGER_ISLAND,SHIPSANITY",
+2959,Shipping,Shipsanity: Qi Seasoning,"GINGER_ISLAND,SHIPSANITY,REQUIRES_QI_ORDERS",
+2960,Shipping,Shipsanity: Radioactive Bar,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_FULL_SHIPMENT,REQUIRES_QI_ORDERS",
+2961,Shipping,Shipsanity: Radioactive Ore,"GINGER_ISLAND,SHIPSANITY,SHIPSANITY_FULL_SHIPMENT,REQUIRES_QI_ORDERS",
+2962,Shipping,Shipsanity: Enricher,"GINGER_ISLAND,SHIPSANITY,REQUIRES_QI_ORDERS",
+2963,Shipping,Shipsanity: Pressure Nozzle,"GINGER_ISLAND,SHIPSANITY,REQUIRES_QI_ORDERS",
+2964,Shipping,Shipsanity: Galaxy Soul,"GINGER_ISLAND,SHIPSANITY,REQUIRES_QI_ORDERS",
+2965,Shipping,Shipsanity: Tiger Slime Egg,"GINGER_ISLAND,SHIPSANITY",
+2966,Shipping,Shipsanity: Movie Ticket,"SHIPSANITY",
+2967,Shipping,Shipsanity: Journal Scrap,"GINGER_ISLAND,SHIPSANITY",
+2968,Shipping,Shipsanity: Banana Sapling,"GINGER_ISLAND,SHIPSANITY",
+2969,Shipping,Shipsanity: Mango Sapling,"GINGER_ISLAND,SHIPSANITY",
+2970,Shipping,Shipsanity: Mushroom Tree Seed,"GINGER_ISLAND,SHIPSANITY,REQUIRES_QI_ORDERS",
+2971,Shipping,Shipsanity: Pineapple Seeds,"GINGER_ISLAND,SHIPSANITY",
+2972,Shipping,Shipsanity: Qi Bean,"GINGER_ISLAND,SHIPSANITY,REQUIRES_QI_ORDERS",
+2973,Shipping,Shipsanity: Taro Tuber,"GINGER_ISLAND,SHIPSANITY",
+3001,Adventurer's Guild,Monster Eradication: Slimes,"MONSTERSANITY,MONSTERSANITY_GOALS",
+3002,Adventurer's Guild,Monster Eradication: Void Spirits,"MONSTERSANITY,MONSTERSANITY_GOALS",
+3003,Adventurer's Guild,Monster Eradication: Bats,"MONSTERSANITY,MONSTERSANITY_GOALS",
+3004,Adventurer's Guild,Monster Eradication: Skeletons,"MONSTERSANITY,MONSTERSANITY_GOALS",
+3005,Adventurer's Guild,Monster Eradication: Cave Insects,"MONSTERSANITY,MONSTERSANITY_GOALS",
+3006,Adventurer's Guild,Monster Eradication: Duggies,"MONSTERSANITY,MONSTERSANITY_GOALS",
+3007,Adventurer's Guild,Monster Eradication: Dust Sprites,"MONSTERSANITY,MONSTERSANITY_GOALS",
+3008,Adventurer's Guild,Monster Eradication: Rock Crabs,"MONSTERSANITY,MONSTERSANITY_GOALS",
+3009,Adventurer's Guild,Monster Eradication: Mummies,"MONSTERSANITY,MONSTERSANITY_GOALS",
+3010,Adventurer's Guild,Monster Eradication: Pepper Rex,"MONSTERSANITY,MONSTERSANITY_GOALS,MONSTERSANITY_MONSTER",
+3011,Adventurer's Guild,Monster Eradication: Serpents,"MONSTERSANITY,MONSTERSANITY_GOALS",
+3012,Adventurer's Guild,Monster Eradication: Magma Sprites,"GINGER_ISLAND,MONSTERSANITY,MONSTERSANITY_GOALS",
+3020,Adventurer's Guild,Monster Eradication: 200 Slimes,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3021,Adventurer's Guild,Monster Eradication: 400 Slimes,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3022,Adventurer's Guild,Monster Eradication: 600 Slimes,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3023,Adventurer's Guild,Monster Eradication: 800 Slimes,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3024,Adventurer's Guild,Monster Eradication: 30 Void Spirits,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3025,Adventurer's Guild,Monster Eradication: 60 Void Spirits,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3026,Adventurer's Guild,Monster Eradication: 90 Void Spirits,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3027,Adventurer's Guild,Monster Eradication: 120 Void Spirits,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3028,Adventurer's Guild,Monster Eradication: 40 Bats,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3029,Adventurer's Guild,Monster Eradication: 80 Bats,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3030,Adventurer's Guild,Monster Eradication: 120 Bats,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3031,Adventurer's Guild,Monster Eradication: 160 Bats,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3032,Adventurer's Guild,Monster Eradication: 10 Skeletons,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3033,Adventurer's Guild,Monster Eradication: 20 Skeletons,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3034,Adventurer's Guild,Monster Eradication: 30 Skeletons,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3035,Adventurer's Guild,Monster Eradication: 40 Skeletons,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3036,Adventurer's Guild,Monster Eradication: 25 Cave Insects,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3037,Adventurer's Guild,Monster Eradication: 50 Cave Insects,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3038,Adventurer's Guild,Monster Eradication: 75 Cave Insects,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3039,Adventurer's Guild,Monster Eradication: 100 Cave Insects,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3040,Adventurer's Guild,Monster Eradication: 6 Duggies,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3041,Adventurer's Guild,Monster Eradication: 12 Duggies,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3042,Adventurer's Guild,Monster Eradication: 18 Duggies,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3043,Adventurer's Guild,Monster Eradication: 24 Duggies,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3044,Adventurer's Guild,Monster Eradication: 100 Dust Sprites,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3045,Adventurer's Guild,Monster Eradication: 200 Dust Sprites,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3046,Adventurer's Guild,Monster Eradication: 300 Dust Sprites,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3047,Adventurer's Guild,Monster Eradication: 400 Dust Sprites,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3048,Adventurer's Guild,Monster Eradication: 12 Rock Crabs,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3049,Adventurer's Guild,Monster Eradication: 24 Rock Crabs,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3050,Adventurer's Guild,Monster Eradication: 36 Rock Crabs,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3051,Adventurer's Guild,Monster Eradication: 48 Rock Crabs,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3052,Adventurer's Guild,Monster Eradication: 20 Mummies,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3053,Adventurer's Guild,Monster Eradication: 40 Mummies,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3054,Adventurer's Guild,Monster Eradication: 60 Mummies,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3055,Adventurer's Guild,Monster Eradication: 80 Mummies,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3056,Adventurer's Guild,Monster Eradication: 10 Pepper Rex,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3057,Adventurer's Guild,Monster Eradication: 20 Pepper Rex,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3058,Adventurer's Guild,Monster Eradication: 30 Pepper Rex,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3059,Adventurer's Guild,Monster Eradication: 40 Pepper Rex,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3060,Adventurer's Guild,Monster Eradication: 50 Serpents,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3061,Adventurer's Guild,Monster Eradication: 100 Serpents,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3062,Adventurer's Guild,Monster Eradication: 150 Serpents,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3063,Adventurer's Guild,Monster Eradication: 200 Serpents,"MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3064,Adventurer's Guild,Monster Eradication: 30 Magma Sprites,"GINGER_ISLAND,MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3065,Adventurer's Guild,Monster Eradication: 60 Magma Sprites,"GINGER_ISLAND,MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3066,Adventurer's Guild,Monster Eradication: 90 Magma Sprites,"GINGER_ISLAND,MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3067,Adventurer's Guild,Monster Eradication: 120 Magma Sprites,"GINGER_ISLAND,MONSTERSANITY,MONSTERSANITY_PROGRESSIVE_GOALS",
+3101,Adventurer's Guild,Monster Eradication: Green Slime,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3102,Adventurer's Guild,Monster Eradication: Frost Jelly,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3103,Adventurer's Guild,Monster Eradication: Sludge,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3104,Adventurer's Guild,Monster Eradication: Tiger Slime,"GINGER_ISLAND,MONSTERSANITY,MONSTERSANITY_MONSTER",
+3105,Adventurer's Guild,Monster Eradication: Shadow Shaman,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3106,Adventurer's Guild,Monster Eradication: Shadow Brute,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3107,Adventurer's Guild,Monster Eradication: Shadow Sniper,"GINGER_ISLAND,REQUIRES_QI_ORDERS,MONSTERSANITY,MONSTERSANITY_MONSTER",
+3108,Adventurer's Guild,Monster Eradication: Bat,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3109,Adventurer's Guild,Monster Eradication: Frost Bat,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3110,Adventurer's Guild,Monster Eradication: Lava Bat,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3111,Adventurer's Guild,Monster Eradication: Iridium Bat,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3112,Adventurer's Guild,Monster Eradication: Skeleton,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3113,Adventurer's Guild,Monster Eradication: Skeleton Mage,"GINGER_ISLAND,REQUIRES_QI_ORDERS,MONSTERSANITY,MONSTERSANITY_MONSTER",
+3114,Adventurer's Guild,Monster Eradication: Bug,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3115,Adventurer's Guild,Monster Eradication: Fly,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3116,Adventurer's Guild,Monster Eradication: Grub,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3117,Adventurer's Guild,Monster Eradication: Duggy,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3118,Adventurer's Guild,Monster Eradication: Magma Duggy,"GINGER_ISLAND,REQUIRES_QI_ORDERS,MONSTERSANITY,MONSTERSANITY_MONSTER",
+3119,Adventurer's Guild,Monster Eradication: Dust Sprite,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3120,Adventurer's Guild,Monster Eradication: Rock Crab,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3121,Adventurer's Guild,Monster Eradication: Lava Crab,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3122,Adventurer's Guild,Monster Eradication: Iridium Crab,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3123,Adventurer's Guild,Monster Eradication: Mummy,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3124,Adventurer's Guild,Monster Eradication: Serpent,"MONSTERSANITY,MONSTERSANITY_MONSTER",
+3125,Adventurer's Guild,Monster Eradication: Royal Serpent,"GINGER_ISLAND,REQUIRES_QI_ORDERS,MONSTERSANITY,MONSTERSANITY_MONSTER",
+3126,Adventurer's Guild,Monster Eradication: Magma Sprite,"GINGER_ISLAND,MONSTERSANITY,MONSTERSANITY_MONSTER",
+3127,Adventurer's Guild,Monster Eradication: Magma Sparker,"GINGER_ISLAND,MONSTERSANITY,MONSTERSANITY_MONSTER",
+3201,Kitchen,Cook Algae Soup,COOKSANITY,
+3202,Kitchen,Cook Artichoke Dip,"COOKSANITY,COOKSANITY_QOS",
+3203,Kitchen,Cook Autumn's Bounty,COOKSANITY,
+3204,Kitchen,Cook Baked Fish,"COOKSANITY,COOKSANITY_QOS",
+3205,Kitchen,Cook Banana Pudding,"COOKSANITY,GINGER_ISLAND",
+3206,Kitchen,Cook Bean Hotpot,COOKSANITY,
+3207,Kitchen,Cook Blackberry Cobbler,"COOKSANITY,COOKSANITY_QOS",
+3208,Kitchen,Cook Blueberry Tart,COOKSANITY,
+3209,Kitchen,Cook Bread,"COOKSANITY,COOKSANITY_QOS",
+3210,Kitchen,Cook Bruschetta,"COOKSANITY,COOKSANITY_QOS",
+3211,Kitchen,Cook Carp Surprise,"COOKSANITY,COOKSANITY_QOS",
+3212,Kitchen,Cook Cheese Cauliflower,COOKSANITY,
+3213,Kitchen,Cook Chocolate Cake,"COOKSANITY,COOKSANITY_QOS",
+3214,Kitchen,Cook Chowder,COOKSANITY,
+3215,Kitchen,Cook Coleslaw,"COOKSANITY,COOKSANITY_QOS",
+3216,Kitchen,Cook Complete Breakfast,"COOKSANITY,COOKSANITY_QOS",
+3217,Kitchen,Cook Cookies,COOKSANITY,
+3218,Kitchen,Cook Crab Cakes,"COOKSANITY,COOKSANITY_QOS",
+3219,Kitchen,Cook Cranberry Candy,"COOKSANITY,COOKSANITY_QOS",
+3220,Kitchen,Cook Cranberry Sauce,COOKSANITY,
+3221,Kitchen,Cook Crispy Bass,COOKSANITY,
+3222,Kitchen,Cook Dish O' The Sea,COOKSANITY,
+3223,Kitchen,Cook Eggplant Parmesan,COOKSANITY,
+3224,Kitchen,Cook Escargot,COOKSANITY,
+3225,Kitchen,Cook Farmer's Lunch,COOKSANITY,
+3226,Kitchen,Cook Fiddlehead Risotto,"COOKSANITY,COOKSANITY_QOS",
+3227,Kitchen,Cook Fish Stew,COOKSANITY,
+3228,Kitchen,Cook Fish Taco,COOKSANITY,
+3229,Kitchen,Cook Fried Calamari,COOKSANITY,
+3230,Kitchen,Cook Fried Eel,COOKSANITY,
+3231,Kitchen,Cook Fried Egg,COOKSANITY,
+3232,Kitchen,Cook Fried Mushroom,COOKSANITY,
+3233,Kitchen,Cook Fruit Salad,"COOKSANITY,COOKSANITY_QOS",
+3234,Kitchen,Cook Ginger Ale,"COOKSANITY,GINGER_ISLAND",
+3235,Kitchen,Cook Glazed Yams,"COOKSANITY,COOKSANITY_QOS",
+3236,Kitchen,Cook Hashbrowns,"COOKSANITY,COOKSANITY_QOS",
+3237,Kitchen,Cook Ice Cream,COOKSANITY,
+3238,Kitchen,Cook Lobster Bisque,"COOKSANITY,COOKSANITY_QOS",
+3239,Kitchen,Cook Lucky Lunch,"COOKSANITY,COOKSANITY_QOS",
+3240,Kitchen,Cook Maki Roll,"COOKSANITY,COOKSANITY_QOS",
+3241,Kitchen,Cook Mango Sticky Rice,"COOKSANITY,GINGER_ISLAND",
+3242,Kitchen,Cook Maple Bar,"COOKSANITY,COOKSANITY_QOS",
+3243,Kitchen,Cook Miner's Treat,COOKSANITY,
+3244,Kitchen,Cook Omelet,"COOKSANITY,COOKSANITY_QOS",
+3245,Kitchen,Cook Pale Broth,COOKSANITY,
+3246,Kitchen,Cook Pancakes,"COOKSANITY,COOKSANITY_QOS",
+3247,Kitchen,Cook Parsnip Soup,COOKSANITY,
+3248,Kitchen,Cook Pepper Poppers,COOKSANITY,
+3249,Kitchen,Cook Pink Cake,"COOKSANITY,COOKSANITY_QOS",
+3250,Kitchen,Cook Pizza,"COOKSANITY,COOKSANITY_QOS",
+3251,Kitchen,Cook Plum Pudding,"COOKSANITY,COOKSANITY_QOS",
+3252,Kitchen,Cook Poi,"COOKSANITY,GINGER_ISLAND",
+3253,Kitchen,Cook Poppyseed Muffin,"COOKSANITY,COOKSANITY_QOS",
+3254,Kitchen,Cook Pumpkin Pie,"COOKSANITY,COOKSANITY_QOS",
+3255,Kitchen,Cook Pumpkin Soup,COOKSANITY,
+3256,Kitchen,Cook Radish Salad,"COOKSANITY,COOKSANITY_QOS",
+3257,Kitchen,Cook Red Plate,COOKSANITY,
+3258,Kitchen,Cook Rhubarb Pie,COOKSANITY,
+3259,Kitchen,Cook Rice Pudding,COOKSANITY,
+3260,Kitchen,Cook Roasted Hazelnuts,"COOKSANITY,COOKSANITY_QOS",
+3261,Kitchen,Cook Roots Platter,COOKSANITY,
+3262,Kitchen,Cook Salad,COOKSANITY,
+3263,Kitchen,Cook Salmon Dinner,COOKSANITY,
+3264,Kitchen,Cook Sashimi,COOKSANITY,
+3265,Kitchen,Cook Seafoam Pudding,COOKSANITY,
+3266,Kitchen,Cook Shrimp Cocktail,"COOKSANITY,COOKSANITY_QOS",
+3267,Kitchen,Cook Spaghetti,COOKSANITY,
+3268,Kitchen,Cook Spicy Eel,COOKSANITY,
+3269,Kitchen,Cook Squid Ink Ravioli,COOKSANITY,
+3270,Kitchen,Cook Stir Fry,"COOKSANITY,COOKSANITY_QOS",
+3271,Kitchen,Cook Strange Bun,COOKSANITY,
+3272,Kitchen,Cook Stuffing,COOKSANITY,
+3273,Kitchen,Cook Super Meal,COOKSANITY,
+3274,Kitchen,Cook Survival Burger,COOKSANITY,
+3275,Kitchen,Cook Tom Kha Soup,COOKSANITY,
+3276,Kitchen,Cook Tortilla,"COOKSANITY,COOKSANITY_QOS",
+3277,Kitchen,Cook Triple Shot Espresso,COOKSANITY,
+3278,Kitchen,Cook Tropical Curry,"COOKSANITY,GINGER_ISLAND",
+3279,Kitchen,Cook Trout Soup,"COOKSANITY,COOKSANITY_QOS",
+3280,Kitchen,Cook Vegetable Medley,COOKSANITY,
+3281,Kitchen,Cook Moss Soup,COOKSANITY,
+3301,Farm,Algae Soup Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3302,The Queen of Sauce,Artichoke Dip Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3303,Farm,Autumn's Bounty Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3304,The Queen of Sauce,Baked Fish Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3305,Farm,Banana Pudding Recipe,"CHEFSANITY,GINGER_ISLAND,CHEFSANITY_PURCHASE",
+3306,Farm,Bean Hotpot Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3307,The Queen of Sauce,Blackberry Cobbler Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3308,Farm,Blueberry Tart Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3309,The Queen of Sauce,Bread Recipe,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_FRIENDSHIP,CHEFSANITY_PURCHASE",
+3310,The Queen of Sauce,Bruschetta Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3311,The Queen of Sauce,Carp Surprise Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3312,Farm,Cheese Cauliflower Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3313,The Queen of Sauce,Chocolate Cake Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3314,Farm,Chowder Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3315,The Queen of Sauce,Coleslaw Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3316,The Queen of Sauce,Complete Breakfast Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3317,Farm,Cookies Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3318,The Queen of Sauce,Crab Cakes Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3319,The Queen of Sauce,Cranberry Candy Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3320,Farm,Cranberry Sauce Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3321,Farm,Crispy Bass Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3322,Farm,Dish O' The Sea Recipe,"CHEFSANITY,CHEFSANITY_SKILL",
+3323,Farm,Eggplant Parmesan Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3324,Farm,Escargot Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3325,Farm,Farmer's Lunch Recipe,"CHEFSANITY,CHEFSANITY_SKILL",
+3326,The Queen of Sauce,Fiddlehead Risotto Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3327,Farm,Fish Stew Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3328,Farm,Fish Taco Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3329,Farm,Fried Calamari Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3330,Farm,Fried Eel Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3331,Farm,Fried Egg Recipe,CHEFSANITY_STARTER,
+3332,Farm,Fried Mushroom Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3333,The Queen of Sauce,Fruit Salad Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3334,Farm,Ginger Ale Recipe,"CHEFSANITY,GINGER_ISLAND,CHEFSANITY_PURCHASE",
+3335,The Queen of Sauce,Glazed Yams Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3336,The Queen of Sauce,Hashbrowns Recipe,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_PURCHASE",
+3337,Farm,Ice Cream Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3338,The Queen of Sauce,Lobster Bisque Recipe,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_FRIENDSHIP",
+3339,The Queen of Sauce,Lucky Lunch Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3340,The Queen of Sauce,Maki Roll Recipe,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_PURCHASE",
+3341,Farm,Mango Sticky Rice Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP,GINGER_ISLAND",
+3342,The Queen of Sauce,Maple Bar Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3343,Farm,Miner's Treat Recipe,"CHEFSANITY,CHEFSANITY_SKILL",
+3344,The Queen of Sauce,Omelet Recipe,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_PURCHASE",
+3345,Farm,Pale Broth Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3346,The Queen of Sauce,Pancakes Recipe,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_PURCHASE",
+3347,Farm,Parsnip Soup Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3348,Farm,Pepper Poppers Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3349,The Queen of Sauce,Pink Cake Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3350,The Queen of Sauce,Pizza Recipe,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_PURCHASE",
+3351,The Queen of Sauce,Plum Pudding Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3352,Farm,Poi Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP,GINGER_ISLAND",
+3353,The Queen of Sauce,Poppyseed Muffin Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3354,The Queen of Sauce,Pumpkin Pie Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3355,Farm,Pumpkin Soup Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3356,The Queen of Sauce,Radish Salad Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3357,Farm,Red Plate Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3358,Farm,Rhubarb Pie Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3359,Farm,Rice Pudding Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3360,The Queen of Sauce,Roasted Hazelnuts Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3361,Farm,Roots Platter Recipe,"CHEFSANITY,CHEFSANITY_SKILL",
+3362,Farm,Salad Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3363,Farm,Salmon Dinner Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3364,Farm,Sashimi Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3365,Farm,Seafoam Pudding Recipe,"CHEFSANITY,CHEFSANITY_SKILL",
+3366,The Queen of Sauce,Shrimp Cocktail Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3367,Farm,Spaghetti Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3368,Farm,Spicy Eel Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3369,Farm,Squid Ink Ravioli Recipe,"CHEFSANITY,CHEFSANITY_SKILL",
+3370,The Queen of Sauce,Stir Fry Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3371,Farm,Strange Bun Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3372,Farm,Stuffing Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3373,Farm,Super Meal Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3374,Farm,Survival Burger Recipe,"CHEFSANITY,CHEFSANITY_SKILL",
+3375,Farm,Tom Kha Soup Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3376,The Queen of Sauce,Tortilla Recipe,"CHEFSANITY,CHEFSANITY_QOS,CHEFSANITY_PURCHASE",
+3377,Saloon,Triple Shot Espresso Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",
+3378,Island Resort,Tropical Curry Recipe,"CHEFSANITY,GINGER_ISLAND,CHEFSANITY_PURCHASE",
+3379,The Queen of Sauce,Trout Soup Recipe,"CHEFSANITY,CHEFSANITY_QOS",
+3380,Farm,Vegetable Medley Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",
+3381,Farm,Moss Soup Recipe,"CHEFSANITY,CHEFSANITY_SKILL",
+3401,Farm,Craft Cherry Bomb,CRAFTSANITY,
+3402,Farm,Craft Bomb,CRAFTSANITY,
+3403,Farm,Craft Mega Bomb,CRAFTSANITY,
+3404,Farm,Craft Gate,CRAFTSANITY,
+3405,Farm,Craft Wood Fence,CRAFTSANITY,
+3406,Farm,Craft Stone Fence,CRAFTSANITY,
+3407,Farm,Craft Iron Fence,CRAFTSANITY,
+3408,Farm,Craft Hardwood Fence,CRAFTSANITY,
+3409,Farm,Craft Sprinkler,CRAFTSANITY,
+3410,Farm,Craft Quality Sprinkler,CRAFTSANITY,
+3411,Farm,Craft Iridium Sprinkler,CRAFTSANITY,
+3412,Farm,Craft Bee House,CRAFTSANITY,
+3413,Farm,Craft Cask,CRAFTSANITY,
+3414,Farm,Craft Cheese Press,CRAFTSANITY,
+3415,Farm,Craft Keg,CRAFTSANITY,
+3416,Farm,Craft Loom,CRAFTSANITY,
+3417,Farm,Craft Mayonnaise Machine,CRAFTSANITY,
+3418,Farm,Craft Oil Maker,CRAFTSANITY,
+3419,Farm,Craft Preserves Jar,CRAFTSANITY,
+3420,Farm,Craft Basic Fertilizer,CRAFTSANITY,
+3421,Farm,Craft Quality Fertilizer,CRAFTSANITY,
+3422,Farm,Craft Deluxe Fertilizer,CRAFTSANITY,
+3423,Farm,Craft Speed-Gro,CRAFTSANITY,
+3424,Farm,Craft Deluxe Speed-Gro,CRAFTSANITY,
+3425,Farm,Craft Hyper Speed-Gro,"CRAFTSANITY,GINGER_ISLAND",
+3426,Farm,Craft Basic Retaining Soil,CRAFTSANITY,
+3427,Farm,Craft Quality Retaining Soil,CRAFTSANITY,
+3428,Farm,Craft Deluxe Retaining Soil,"CRAFTSANITY,GINGER_ISLAND",
+3429,Farm,Craft Tree Fertilizer,CRAFTSANITY,
+3430,Farm,Craft Spring Seeds,CRAFTSANITY,
+3431,Farm,Craft Summer Seeds,CRAFTSANITY,
+3432,Farm,Craft Fall Seeds,CRAFTSANITY,
+3433,Farm,Craft Winter Seeds,CRAFTSANITY,
+3434,Farm,Craft Ancient Seeds,CRAFTSANITY,
+3435,Farm,Craft Grass Starter,CRAFTSANITY,
+3436,Farm,Craft Tea Sapling,CRAFTSANITY,
+3437,Farm,Craft Fiber Seeds,CRAFTSANITY,
+3438,Farm,Craft Wood Floor,CRAFTSANITY,
+3439,Farm,Craft Rustic Plank Floor,CRAFTSANITY,
+3440,Farm,Craft Straw Floor,CRAFTSANITY,
+3441,Farm,Craft Weathered Floor,CRAFTSANITY,
+3442,Farm,Craft Crystal Floor,CRAFTSANITY,
+3443,Farm,Craft Stone Floor,CRAFTSANITY,
+3444,Farm,Craft Stone Walkway Floor,CRAFTSANITY,
+3445,Farm,Craft Brick Floor,CRAFTSANITY,
+3446,Farm,Craft Wood Path,CRAFTSANITY,
+3447,Farm,Craft Gravel Path,CRAFTSANITY,
+3448,Farm,Craft Cobblestone Path,CRAFTSANITY,
+3449,Farm,Craft Stepping Stone Path,CRAFTSANITY,
+3450,Farm,Craft Crystal Path,CRAFTSANITY,
+3451,Farm,Craft Spinner,CRAFTSANITY,
+3452,Farm,Craft Trap Bobber,CRAFTSANITY,
+3453,Farm,Craft Cork Bobber,CRAFTSANITY,
+3454,Farm,Craft Quality Bobber,CRAFTSANITY,
+3455,Farm,Craft Treasure Hunter,CRAFTSANITY,
+3456,Farm,Craft Dressed Spinner,CRAFTSANITY,
+3457,Farm,Craft Barbed Hook,CRAFTSANITY,
+3458,Farm,Craft Magnet,CRAFTSANITY,
+3459,Farm,Craft Bait,CRAFTSANITY,
+3460,Farm,Craft Wild Bait,CRAFTSANITY,
+3461,Farm,Craft Magic Bait,"CRAFTSANITY,GINGER_ISLAND",
+3462,Farm,Craft Crab Pot,CRAFTSANITY,
+3463,Farm,Craft Sturdy Ring,CRAFTSANITY,
+3464,Farm,Craft Warrior Ring,CRAFTSANITY,
+3465,Farm,Craft Ring of Yoba,CRAFTSANITY,
+3466,Farm,Craft Thorns Ring,"CRAFTSANITY,GINGER_ISLAND",
+3467,Farm,Craft Glowstone Ring,CRAFTSANITY,
+3468,Farm,Craft Iridium Band,CRAFTSANITY,
+3469,Farm,Craft Wedding Ring,CRAFTSANITY,
+3470,Farm,Craft Field Snack,CRAFTSANITY,
+3471,Farm,Craft Bug Steak,CRAFTSANITY,
+3472,Farm,Craft Life Elixir,CRAFTSANITY,
+3473,Farm,Craft Oil of Garlic,CRAFTSANITY,
+3474,Farm,Craft Monster Musk,CRAFTSANITY,
+3475,Farm,Craft Fairy Dust,"CRAFTSANITY,GINGER_ISLAND",
+3476,Farm,Craft Warp Totem: Beach,CRAFTSANITY,
+3477,Farm,Craft Warp Totem: Mountains,CRAFTSANITY,
+3478,Farm,Craft Warp Totem: Farm,CRAFTSANITY,
+3479,Farm,Craft Warp Totem: Desert,CRAFTSANITY,
+3480,Farm,Craft Warp Totem: Island,"CRAFTSANITY,GINGER_ISLAND",
+3481,Farm,Craft Rain Totem,CRAFTSANITY,
+3482,Farm,Craft Torch,CRAFTSANITY,
+3483,Farm,Craft Campfire,CRAFTSANITY,
+3484,Farm,Craft Wooden Brazier,CRAFTSANITY,
+3485,Farm,Craft Stone Brazier,CRAFTSANITY,
+3486,Farm,Craft Gold Brazier,CRAFTSANITY,
+3487,Farm,Craft Carved Brazier,CRAFTSANITY,
+3488,Farm,Craft Stump Brazier,CRAFTSANITY,
+3489,Farm,Craft Barrel Brazier,CRAFTSANITY,
+3490,Farm,Craft Skull Brazier,CRAFTSANITY,
+3491,Farm,Craft Marble Brazier,CRAFTSANITY,
+3492,Farm,Craft Wood Lamp-post,CRAFTSANITY,
+3493,Farm,Craft Iron Lamp-post,CRAFTSANITY,
+3494,Farm,Craft Jack-O-Lantern,CRAFTSANITY,
+3495,Farm,Craft Bone Mill,CRAFTSANITY,
+3496,Farm,Craft Charcoal Kiln,CRAFTSANITY,
+3497,Farm,Craft Crystalarium,CRAFTSANITY,
+3498,Farm,Craft Furnace,CRAFTSANITY,
+3499,Farm,Craft Geode Crusher,CRAFTSANITY,
+3500,Farm,Craft Heavy Tapper,"CRAFTSANITY,GINGER_ISLAND",
+3501,Farm,Craft Lightning Rod,CRAFTSANITY,
+3502,Farm,Craft Ostrich Incubator,"CRAFTSANITY,GINGER_ISLAND",
+3503,Farm,Craft Recycling Machine,CRAFTSANITY,
+3504,Farm,Craft Seed Maker,CRAFTSANITY,
+3505,Farm,Craft Slime Egg-Press,CRAFTSANITY,
+3506,Farm,Craft Slime Incubator,CRAFTSANITY,
+3507,Farm,Craft Solar Panel,"CRAFTSANITY,GINGER_ISLAND",
+3508,Farm,Craft Tapper,CRAFTSANITY,
+3509,Farm,Craft Worm Bin,CRAFTSANITY,
+3510,Farm,Craft Tub o' Flowers,CRAFTSANITY,
+3511,Farm,Craft Wicked Statue,CRAFTSANITY,
+3512,Farm,Craft Flute Block,CRAFTSANITY,
+3513,Farm,Craft Drum Block,CRAFTSANITY,
+3514,Farm,Craft Chest,CRAFTSANITY,
+3515,Farm,Craft Stone Chest,CRAFTSANITY,
+3516,Farm,Craft Wood Sign,CRAFTSANITY,
+3517,Farm,Craft Stone Sign,CRAFTSANITY,
+3518,Farm,Craft Dark Sign,CRAFTSANITY,
+3519,Farm,Craft Garden Pot,CRAFTSANITY,
+3520,Farm,Craft Scarecrow,CRAFTSANITY,
+3521,Farm,Craft Deluxe Scarecrow,CRAFTSANITY,
+3522,Farm,Craft Staircase,CRAFTSANITY,
+3523,Farm,Craft Explosive Ammo,CRAFTSANITY,
+3524,Farm,Craft Transmute (Fe),CRAFTSANITY,
+3525,Farm,Craft Transmute (Au),CRAFTSANITY,
+3526,Farm,Craft Mini-Jukebox,CRAFTSANITY,
+3527,Farm,Craft Mini-Obelisk,CRAFTSANITY,
+3528,Farm,Craft Farm Computer,CRAFTSANITY,
+3529,Farm,Craft Hopper,"CRAFTSANITY,GINGER_ISLAND",
+3530,Farm,Craft Cookout Kit,CRAFTSANITY,
+3531,Farm,Craft Fish Smoker,"CRAFTSANITY",
+3532,Farm,Craft Dehydrator,"CRAFTSANITY",
+3533,Farm,Craft Blue Grass Starter,"CRAFTSANITY,GINGER_ISLAND",
+3534,Farm,Craft Mystic Tree Seed,"CRAFTSANITY,REQUIRES_MASTERIES",
+3535,Farm,Craft Sonar Bobber,"CRAFTSANITY",
+3536,Farm,Craft Challenge Bait,"CRAFTSANITY,REQUIRES_MASTERIES",
+3537,Farm,Craft Treasure Totem,"CRAFTSANITY,REQUIRES_MASTERIES",
+3538,Farm,Craft Heavy Furnace,"CRAFTSANITY,REQUIRES_MASTERIES",
+3539,Farm,Craft Deluxe Worm Bin,"CRAFTSANITY",
+3540,Farm,Craft Mushroom Log,"CRAFTSANITY",
+3541,Farm,Craft Big Chest,"CRAFTSANITY",
+3542,Farm,Craft Big Stone Chest,"CRAFTSANITY",
+3543,Farm,Craft Text Sign,"CRAFTSANITY",
+3544,Farm,Craft Tent Kit,"CRAFTSANITY",
+3545,Farm,Craft Statue Of The Dwarf King,"CRAFTSANITY,REQUIRES_MASTERIES",
+3546,Farm,Craft Statue Of Blessings,"CRAFTSANITY,REQUIRES_MASTERIES",
+3547,Farm,Craft Anvil,"CRAFTSANITY,REQUIRES_MASTERIES",
+3548,Farm,Craft Mini-Forge,"CRAFTSANITY,GINGER_ISLAND,REQUIRES_MASTERIES",
+3549,Farm,Craft Deluxe Bait,"CRAFTSANITY",
+3550,Farm,Craft Bait Maker,"CRAFTSANITY",
+3551,Pierre's General Store,Grass Starter Recipe,CRAFTSANITY,
+3552,Carpenter Shop,Wood Floor Recipe,CRAFTSANITY,
+3553,Carpenter Shop,Rustic Plank Floor Recipe,CRAFTSANITY,
+3554,Carpenter Shop,Straw Floor Recipe,CRAFTSANITY,
+3555,Mines Dwarf Shop,Weathered Floor Recipe,CRAFTSANITY,
+3556,Sewer,Crystal Floor Recipe,CRAFTSANITY,
+3557,Carpenter Shop,Stone Floor Recipe,CRAFTSANITY,
+3558,Carpenter Shop,Stone Walkway Floor Recipe,CRAFTSANITY,
+3559,Carpenter Shop,Brick Floor Recipe,CRAFTSANITY,
+3560,Carpenter Shop,Stepping Stone Path Recipe,CRAFTSANITY,
+3561,Carpenter Shop,Crystal Path Recipe,CRAFTSANITY,
+3562,Traveling Cart,Wedding Ring Recipe,CRAFTSANITY,
+3563,Volcano Dwarf Shop,Warp Totem: Island Recipe,"CRAFTSANITY,GINGER_ISLAND",
+3564,Carpenter Shop,Wooden Brazier Recipe,CRAFTSANITY,
+3565,Carpenter Shop,Stone Brazier Recipe,CRAFTSANITY,
+3566,Carpenter Shop,Gold Brazier Recipe,CRAFTSANITY,
+3567,Carpenter Shop,Carved Brazier Recipe,CRAFTSANITY,
+3568,Carpenter Shop,Stump Brazier Recipe,CRAFTSANITY,
+3569,Carpenter Shop,Barrel Brazier Recipe,CRAFTSANITY,
+3570,Carpenter Shop,Skull Brazier Recipe,CRAFTSANITY,
+3571,Carpenter Shop,Marble Brazier Recipe,CRAFTSANITY,
+3572,Carpenter Shop,Wood Lamp-post Recipe,CRAFTSANITY,
+3573,Carpenter Shop,Iron Lamp-post Recipe,CRAFTSANITY,
+3574,Sewer,Wicked Statue Recipe,CRAFTSANITY,
+3575,Desert,Warp Totem: Desert Recipe,"CRAFTSANITY",
+3576,Island Trader,Deluxe Retaining Soil Recipe,"CRAFTSANITY,GINGER_ISLAND",
+3577,Willy's Fish Shop,Fish Smoker Recipe,CRAFTSANITY,
+3578,Pierre's General Store,Dehydrator Recipe,CRAFTSANITY,
+3579,Carpenter Shop,Big Chest Recipe,CRAFTSANITY,
+3580,Mines Dwarf Shop,Big Stone Chest Recipe,CRAFTSANITY,
+3701,Raccoon Bundles,Raccoon Request 1,"BUNDLE,RACCOON_BUNDLES",
+3702,Raccoon Bundles,Raccoon Request 2,"BUNDLE,RACCOON_BUNDLES",
+3703,Raccoon Bundles,Raccoon Request 3,"BUNDLE,RACCOON_BUNDLES",
+3704,Raccoon Bundles,Raccoon Request 4,"BUNDLE,RACCOON_BUNDLES",
+3705,Raccoon Bundles,Raccoon Request 5,"BUNDLE,RACCOON_BUNDLES",
+3706,Raccoon Bundles,Raccoon Request 6,"BUNDLE,RACCOON_BUNDLES",
+3707,Raccoon Bundles,Raccoon Request 7,"BUNDLE,RACCOON_BUNDLES",
+3708,Raccoon Bundles,Raccoon Request 8,"BUNDLE,RACCOON_BUNDLES",
+3801,Shipping,Shipsanity: Goby,"SHIPSANITY,SHIPSANITY_FISH",
+3802,Shipping,Shipsanity: Fireworks (Red),"SHIPSANITY",
+3803,Shipping,Shipsanity: Fireworks (Purple),"SHIPSANITY",
+3804,Shipping,Shipsanity: Fireworks (Green),"SHIPSANITY",
+3805,Shipping,Shipsanity: Far Away Stone,"SHIPSANITY",
+3806,Shipping,Shipsanity: Calico Egg,"SHIPSANITY",
+3807,Shipping,Shipsanity: Mixed Flower Seeds,"SHIPSANITY",
+3808,Shipping,Shipsanity: Mystery Box,"SHIPSANITY",
+3809,Shipping,Shipsanity: Golden Tag,"SHIPSANITY",
+3810,Shipping,Shipsanity: Deluxe Bait,"SHIPSANITY",
+3811,Shipping,Shipsanity: Moss,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+3812,Shipping,Shipsanity: Mossy Seed,"SHIPSANITY",
+3813,Shipping,Shipsanity: Sonar Bobber,"SHIPSANITY",
+3814,Shipping,Shipsanity: Tent Kit,"SHIPSANITY",
+3815,Shipping,Shipsanity: Mystic Tree Seed,"SHIPSANITY,REQUIRES_MASTERIES",
+3816,Shipping,Shipsanity: Mystic Syrup,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+3817,Shipping,Shipsanity: Raisins,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+3818,Shipping,Shipsanity: Dried Fruit,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+3819,Shipping,Shipsanity: Dried Mushrooms,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+3820,Shipping,Shipsanity: Stardrop Tea,"SHIPSANITY",
+3821,Shipping,Shipsanity: Prize Ticket,"SHIPSANITY",
+3822,Shipping,Shipsanity: Treasure Totem,"SHIPSANITY,REQUIRES_MASTERIES",
+3823,Shipping,Shipsanity: Challenge Bait,"SHIPSANITY,REQUIRES_MASTERIES",
+3824,Shipping,Shipsanity: Carrot Seeds,"SHIPSANITY",
+3825,Shipping,Shipsanity: Carrot,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+3826,Shipping,Shipsanity: Summer Squash Seeds,"SHIPSANITY",
+3827,Shipping,Shipsanity: Summer Squash,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+3828,Shipping,Shipsanity: Broccoli Seeds,"SHIPSANITY",
+3829,Shipping,Shipsanity: Broccoli,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+3830,Shipping,Shipsanity: Powdermelon Seeds,"SHIPSANITY",
+3831,Shipping,Shipsanity: Powdermelon,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
+3832,Shipping,Shipsanity: Smoked Fish,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
+3833,Shipping,Shipsanity: Book Of Stars,"SHIPSANITY",
+3834,Shipping,Shipsanity: Stardew Valley Almanac,"SHIPSANITY",
+3835,Shipping,Shipsanity: Woodcutter's Weekly,"SHIPSANITY",
+3836,Shipping,Shipsanity: Bait And Bobber,"SHIPSANITY",
+3837,Shipping,Shipsanity: Mining Monthly,"SHIPSANITY",
+3838,Shipping,Shipsanity: Combat Quarterly,"SHIPSANITY",
+3839,Shipping,Shipsanity: The Alleyway Buffet,"SHIPSANITY",
+3840,Shipping,Shipsanity: The Art O' Crabbing,"SHIPSANITY",
+3841,Shipping,Shipsanity: Dwarvish Safety Manual,"SHIPSANITY",
+3842,Shipping,Shipsanity: Jewels Of The Sea,"SHIPSANITY",
+3843,Shipping,Shipsanity: Raccoon Journal,"SHIPSANITY",
+3844,Shipping,Shipsanity: Woody's Secret,"SHIPSANITY",
+3845,Shipping,"Shipsanity: Jack Be Nimble, Jack Be Thick","SHIPSANITY",
+3846,Shipping,Shipsanity: Friendship 101,"SHIPSANITY",
+3847,Shipping,Shipsanity: Monster Compendium,"SHIPSANITY",
+3848,Shipping,Shipsanity: Way Of The Wind pt. 1,"SHIPSANITY",
+3849,Shipping,Shipsanity: Mapping Cave Systems,"SHIPSANITY",
+3850,Shipping,Shipsanity: Price Catalogue,"SHIPSANITY",
+3851,Shipping,Shipsanity: Queen Of Sauce Cookbook,"SHIPSANITY,GINGER_ISLAND",
+3852,Shipping,Shipsanity: The Diamond Hunter,"SHIPSANITY,GINGER_ISLAND",
+3853,Shipping,Shipsanity: Book of Mysteries,"SHIPSANITY",
+3854,Shipping,Shipsanity: Animal Catalogue,"SHIPSANITY",
+3855,Shipping,Shipsanity: Way Of The Wind pt. 2,"SHIPSANITY",
+3856,Shipping,Shipsanity: Golden Animal Cracker,"SHIPSANITY,REQUIRES_MASTERIES",
+3857,Shipping,Shipsanity: Golden Mystery Box,"SHIPSANITY,REQUIRES_MASTERIES",
+3858,Shipping,Shipsanity: Sea Jelly,"SHIPSANITY,SHIPSANITY_FISH",
+3859,Shipping,Shipsanity: Cave Jelly,"SHIPSANITY,SHIPSANITY_FISH",
+3860,Shipping,Shipsanity: River Jelly,"SHIPSANITY,SHIPSANITY_FISH",
+3861,Shipping,Shipsanity: Treasure Appraisal Guide,"SHIPSANITY",
+3862,Shipping,Shipsanity: Horse: The Book,"SHIPSANITY",
+3863,Shipping,Shipsanity: Butterfly Powder,"SHIPSANITY",
+3864,Shipping,Shipsanity: Blue Grass Starter,"SHIPSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS",
+3865,Shipping,Shipsanity: Moss Soup,"SHIPSANITY",
+3866,Shipping,Shipsanity: Ol' Slitherlegs,"SHIPSANITY",
+3867,Shipping,Shipsanity: Targeted Bait,"SHIPSANITY",
+4001,Farm,Read Price Catalogue,"BOOKSANITY,BOOKSANITY_POWER",
+4002,Farm,Read Mapping Cave Systems,"BOOKSANITY,BOOKSANITY_POWER",
+4003,Farm,Read Way Of The Wind pt. 1,"BOOKSANITY,BOOKSANITY_POWER",
+4004,Farm,Read Way Of The Wind pt. 2,"BOOKSANITY,BOOKSANITY_POWER",
+4005,Farm,Read Monster Compendium,"BOOKSANITY,BOOKSANITY_POWER",
+4006,Farm,Read Friendship 101,"BOOKSANITY,BOOKSANITY_POWER",
+4007,Farm,"Read Jack Be Nimble, Jack Be Thick","BOOKSANITY,BOOKSANITY_POWER",
+4008,Farm,Read Woody's Secret,"BOOKSANITY,BOOKSANITY_POWER",
+4009,Farm,Read Raccoon Journal,"BOOKSANITY,BOOKSANITY_POWER",
+4010,Farm,Read Jewels Of The Sea,"BOOKSANITY,BOOKSANITY_POWER",
+4011,Farm,Read Dwarvish Safety Manual,"BOOKSANITY,BOOKSANITY_POWER",
+4012,Farm,Read The Art O' Crabbing,"BOOKSANITY,BOOKSANITY_POWER",
+4013,Farm,Read The Alleyway Buffet,"BOOKSANITY,BOOKSANITY_POWER",
+4014,Farm,Read The Diamond Hunter,"BOOKSANITY,BOOKSANITY_POWER,GINGER_ISLAND",
+4015,Farm,Read Book of Mysteries,"BOOKSANITY,BOOKSANITY_POWER",
+4016,Farm,Read Horse: The Book,"BOOKSANITY,BOOKSANITY_POWER",
+4017,Farm,Read Treasure Appraisal Guide,"BOOKSANITY,BOOKSANITY_POWER",
+4018,Farm,Read Ol' Slitherlegs,"BOOKSANITY,BOOKSANITY_POWER",
+4019,Farm,Read Animal Catalogue,"BOOKSANITY,BOOKSANITY_POWER",
+4031,Farm,Read Bait And Bobber,"BOOKSANITY,BOOKSANITY_SKILL",
+4032,Farm,Read Book Of Stars,"BOOKSANITY,BOOKSANITY_SKILL",
+4033,Farm,Read Combat Quarterly,"BOOKSANITY,BOOKSANITY_SKILL",
+4034,Farm,Read Mining Monthly,"BOOKSANITY,BOOKSANITY_SKILL",
+4035,Farm,Read Queen Of Sauce Cookbook,"BOOKSANITY,BOOKSANITY_SKILL,GINGER_ISLAND",
+4036,Farm,Read Stardew Valley Almanac,"BOOKSANITY,BOOKSANITY_SKILL",
+4037,Farm,Read Woodcutter's Weekly,"BOOKSANITY,BOOKSANITY_SKILL",
+4051,Museum,Read Tips on Farming,"BOOKSANITY,BOOKSANITY_LOST",
+4052,Museum,Read This is a book by Marnie,"BOOKSANITY,BOOKSANITY_LOST",
+4053,Museum,Read On Foraging,"BOOKSANITY,BOOKSANITY_LOST",
+4054,Museum,"Read The Fisherman, Act 1","BOOKSANITY,BOOKSANITY_LOST",
+4055,Museum,Read How Deep do the mines go?,"BOOKSANITY,BOOKSANITY_LOST",
+4056,Museum,Read An Old Farmer's Journal,"BOOKSANITY,BOOKSANITY_LOST",
+4057,Museum,Read Scarecrows,"BOOKSANITY,BOOKSANITY_LOST",
+4058,Museum,Read The Secret of the Stardrop,"BOOKSANITY,BOOKSANITY_LOST",
+4059,Museum,Read Journey of the Prairie King -- The Smash Hit Video Game!,"BOOKSANITY,BOOKSANITY_LOST",
+4060,Museum,Read A Study on Diamond Yields,"BOOKSANITY,BOOKSANITY_LOST",
+4061,Museum,Read Brewmaster's Guide,"BOOKSANITY,BOOKSANITY_LOST",
+4062,Museum,Read Mysteries of the Dwarves,"BOOKSANITY,BOOKSANITY_LOST",
+4063,Museum,Read Highlights From The Book of Yoba,"BOOKSANITY,BOOKSANITY_LOST",
+4064,Museum,Read Marriage Guide for Farmers,"BOOKSANITY,BOOKSANITY_LOST",
+4065,Museum,"Read The Fisherman, Act II","BOOKSANITY,BOOKSANITY_LOST",
+4066,Museum,Read Technology Report!,"BOOKSANITY,BOOKSANITY_LOST",
+4067,Museum,Read Secrets of the Legendary Fish,"BOOKSANITY,BOOKSANITY_LOST",
+4068,Museum,Read Gunther Tunnel Notice,"BOOKSANITY,BOOKSANITY_LOST",
+4069,Museum,Read Note From Gunther,"BOOKSANITY,BOOKSANITY_LOST",
+4070,Museum,Read Goblins by M. Jasper,"BOOKSANITY,BOOKSANITY_LOST",
+4071,Museum,Read Secret Statues Acrostics,"BOOKSANITY,BOOKSANITY_LOST",
+4101,Clint's Blacksmith,Open Golden Coconut,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4102,Island West,Fishing Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4103,Island West,Fishing Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4104,Island North,Fishing Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4105,Island North,Fishing Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4106,Island Southeast,Fishing Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4107,Island East,Jungle Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4108,Island East,Banana Altar,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4109,Leo's Hut,Leo's Tree,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4110,Island Shrine,Gem Birds Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4111,Island Shrine,Gem Birds Shrine,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4112,Island West,Harvesting Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4113,Island West,Harvesting Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4114,Island West,Harvesting Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4115,Island West,Harvesting Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4116,Island West,Harvesting Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4117,Gourmand Frog Cave,Gourmand Frog Melon,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4118,Gourmand Frog Cave,Gourmand Frog Wheat,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4119,Gourmand Frog Cave,Gourmand Frog Garlic,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4120,Island West,Journal Scrap #6,"WALNUTSANITY,WALNUTSANITY_DIG",
+4121,Island West,Mussel Node Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4122,Island West,Mussel Node Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4123,Island West,Mussel Node Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4124,Island West,Mussel Node Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4125,Island West,Mussel Node Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4126,Shipwreck,Shipwreck Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4127,Island West,Whack A Mole,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4128,Island West,Starfish Triangle,"WALNUTSANITY,WALNUTSANITY_DIG",
+4129,Island West,Starfish Diamond,"WALNUTSANITY,WALNUTSANITY_DIG",
+4130,Island West,X in the sand,"WALNUTSANITY,WALNUTSANITY_DIG",
+4131,Island West,Diamond Of Indents,"WALNUTSANITY,WALNUTSANITY_DIG",
+4132,Island West,Bush Behind Coconut Tree,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4133,Island West,Journal Scrap #4,"WALNUTSANITY,WALNUTSANITY_DIG",
+4134,Island West,Walnut Room Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4135,Island West,Coast Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4136,Island West,Tiger Slime Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4137,Island West,Bush Behind Mahogany Tree,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4138,Island West,Circle Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG",
+4139,Island West,Below Colored Crystals Cave Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4140,Colored Crystals Cave,Colored Crystals,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4141,Island West,Cliff Edge Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4142,Island West,Diamond Of Pebbles,"WALNUTSANITY,WALNUTSANITY_DIG",
+4143,Island West,Farm Parrot Express Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4144,Island West,Farmhouse Cliff Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4145,Island North,Big Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
+4146,Island North,Grove Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4147,Island North,Diamond Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG",
+4148,Island North,Small Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
+4149,Island North,Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG",
+4150,Dig Site,Crooked Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
+4151,Dig Site,Above Dig Site Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4152,Dig Site,Above Field Office Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4153,Dig Site,Above Field Office Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4154,Field Office,Complete Large Animal Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4155,Field Office,Complete Snake Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4156,Field Office,Complete Mummified Frog Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4157,Field Office,Complete Mummified Bat Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4158,Field Office,Purple Flowers Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4159,Field Office,Purple Starfish Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4160,Island North,Bush Behind Volcano Tree,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4161,Island North,Arc Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
+4162,Island North,Protruding Tree Walnut,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4163,Island North,Journal Scrap #10,"WALNUTSANITY,WALNUTSANITY_DIG",
+4164,Island North,Northmost Point Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
+4165,Island North,Hidden Passage Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4166,Volcano Secret Beach,Secret Beach Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4167,Volcano Secret Beach,Secret Beach Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4168,Volcano - Floor 5,Volcano Rocks Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4169,Volcano - Floor 5,Volcano Rocks Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4170,Volcano - Floor 10,Volcano Rocks Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4171,Volcano - Floor 10,Volcano Rocks Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4172,Volcano - Floor 10,Volcano Rocks Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4173,Volcano - Floor 5,Volcano Monsters Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4174,Volcano - Floor 5,Volcano Monsters Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4175,Volcano - Floor 10,Volcano Monsters Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4176,Volcano - Floor 10,Volcano Monsters Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4177,Volcano - Floor 10,Volcano Monsters Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4178,Volcano - Floor 5,Volcano Crates Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4179,Volcano - Floor 5,Volcano Crates Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4180,Volcano - Floor 10,Volcano Crates Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4181,Volcano - Floor 10,Volcano Crates Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4182,Volcano - Floor 10,Volcano Crates Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4183,Volcano - Floor 5,Volcano Common Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4184,Volcano - Floor 10,Volcano Rare Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
+4185,Volcano - Floor 10,Forge Entrance Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4186,Volcano - Floor 10,Forge Exit Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4187,Island North,Cliff Over Island South Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
+4188,Island Southeast,Starfish Tide Pool,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4189,Island Southeast,Diamond Of Yellow Starfish,"WALNUTSANITY,WALNUTSANITY_DIG",
+4190,Island Southeast,Mermaid Song,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4191,Pirate Cove,Pirate Darts 1,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4192,Pirate Cove,Pirate Darts 2,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4193,Pirate Cove,Pirate Darts 3,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
+4194,Pirate Cove,Pirate Cove Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG",
+5001,Stardew Valley,Level 1 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
+5002,Stardew Valley,Level 2 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
+5003,Stardew Valley,Level 3 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
+5004,Stardew Valley,Level 4 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
+5005,Stardew Valley,Level 5 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
+5006,Stardew Valley,Level 6 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
+5007,Stardew Valley,Level 7 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
+5008,Stardew Valley,Level 8 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
+5009,Stardew Valley,Level 9 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
+5010,Stardew Valley,Level 10 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
+5011,Stardew Valley,Level 1 Socializing,"SKILL_LEVEL,SOCIALIZING_LEVEL",Socializing Skill
+5012,Stardew Valley,Level 2 Socializing,"SKILL_LEVEL,SOCIALIZING_LEVEL",Socializing Skill
+5013,Stardew Valley,Level 3 Socializing,"SKILL_LEVEL,SOCIALIZING_LEVEL",Socializing Skill
+5014,Stardew Valley,Level 4 Socializing,"SKILL_LEVEL,SOCIALIZING_LEVEL",Socializing Skill
+5015,Stardew Valley,Level 5 Socializing,"SKILL_LEVEL,SOCIALIZING_LEVEL",Socializing Skill
+5016,Stardew Valley,Level 6 Socializing,"SKILL_LEVEL,SOCIALIZING_LEVEL",Socializing Skill
+5017,Stardew Valley,Level 7 Socializing,"SKILL_LEVEL,SOCIALIZING_LEVEL",Socializing Skill
+5018,Stardew Valley,Level 8 Socializing,"SKILL_LEVEL,SOCIALIZING_LEVEL",Socializing Skill
+5019,Stardew Valley,Level 9 Socializing,"SKILL_LEVEL,SOCIALIZING_LEVEL",Socializing Skill
+5020,Stardew Valley,Level 10 Socializing,"SKILL_LEVEL,SOCIALIZING_LEVEL",Socializing Skill
+5021,Magic Altar,Level 1 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
+5022,Magic Altar,Level 2 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
+5023,Magic Altar,Level 3 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
+5024,Magic Altar,Level 4 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
+5025,Magic Altar,Level 5 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
+5026,Magic Altar,Level 6 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
+5027,Magic Altar,Level 7 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
+5028,Magic Altar,Level 8 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
+5029,Magic Altar,Level 9 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
+5030,Magic Altar,Level 10 Magic,"MAGIC_LEVEL,SKILL_LEVEL",Magic
+5031,Town,Level 1 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
+5032,Town,Level 2 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
+5033,Town,Level 3 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
+5034,Town,Level 4 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
+5035,Town,Level 5 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
+5036,Town,Level 6 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
+5037,Town,Level 7 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
+5038,Town,Level 8 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
+5039,Town,Level 9 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
+5040,Town,Level 10 Binning,"BINNING_LEVEL,SKILL_LEVEL",Binning Skill
+5041,Stardew Valley,Level 1 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
+5042,Stardew Valley,Level 2 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
+5043,Stardew Valley,Level 3 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
+5044,Stardew Valley,Level 4 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
+5045,Stardew Valley,Level 5 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
+5046,Stardew Valley,Level 6 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
+5047,Stardew Valley,Level 7 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
+5048,Stardew Valley,Level 8 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
+5049,Stardew Valley,Level 9 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
+5050,Stardew Valley,Level 10 Archaeology,"ARCHAEOLOGY_LEVEL,SKILL_LEVEL",Archaeology
+5051,Stardew Valley,Level 1 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
+5052,Stardew Valley,Level 2 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
+5053,Stardew Valley,Level 3 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
+5054,Stardew Valley,Level 4 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
+5055,Stardew Valley,Level 5 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
+5056,Stardew Valley,Level 6 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
+5057,Stardew Valley,Level 7 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
+5058,Stardew Valley,Level 8 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
+5059,Stardew Valley,Level 9 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
+5060,Stardew Valley,Level 10 Cooking,"COOKING_LEVEL,SKILL_LEVEL",Cooking Skill
+5501,Magic Altar,Analyze: Clear Debris,MANDATORY,Magic
+5502,Magic Altar,Analyze: Till,MANDATORY,Magic
+5503,Magic Altar,Analyze: Water,MANDATORY,Magic
+5504,Magic Altar,Analyze All Toil School Locations,MANDATORY,Magic
+5505,Magic Altar,Analyze: Evac,MANDATORY,Magic
+5506,Magic Altar,Analyze: Haste,MANDATORY,Magic
+5507,Magic Altar,Analyze: Heal,MANDATORY,Magic
+5508,Magic Altar,Analyze All Life School Locations,MANDATORY,Magic
+5509,Magic Altar,Analyze: Descend,MANDATORY,Magic
+5510,Magic Altar,Analyze: Fireball,MANDATORY,Magic
+5511,Magic Altar,Analyze: Frostbolt,MANDATORY,Magic
+5512,Magic Altar,Analyze All Elemental School Locations,MANDATORY,Magic
+5513,Magic Altar,Analyze: Lantern,MANDATORY,Magic
+5514,Magic Altar,Analyze: Tendrils,MANDATORY,Magic
+5515,Magic Altar,Analyze: Shockwave,MANDATORY,Magic
+5516,Magic Altar,Analyze All Nature School Locations,MANDATORY,Magic
+5517,Magic Altar,Analyze: Meteor,MANDATORY,Magic
+5518,Magic Altar,Analyze: Lucksteal,MANDATORY,Magic
+5519,Magic Altar,Analyze: Bloodmana,MANDATORY,Magic
+5520,Magic Altar,Analyze All Eldritch School Locations,MANDATORY,Magic
+5521,Magic Altar,Analyze Every Magic School Location,MANDATORY,Magic
+6001,Museum,Friendsanity: Jasper 1 <3,FRIENDSANITY,Professor Jasper Thomas
+6002,Museum,Friendsanity: Jasper 2 <3,FRIENDSANITY,Professor Jasper Thomas
+6003,Museum,Friendsanity: Jasper 3 <3,FRIENDSANITY,Professor Jasper Thomas
+6004,Museum,Friendsanity: Jasper 4 <3,FRIENDSANITY,Professor Jasper Thomas
+6005,Museum,Friendsanity: Jasper 5 <3,FRIENDSANITY,Professor Jasper Thomas
+6006,Museum,Friendsanity: Jasper 6 <3,FRIENDSANITY,Professor Jasper Thomas
+6007,Museum,Friendsanity: Jasper 7 <3,FRIENDSANITY,Professor Jasper Thomas
+6008,Museum,Friendsanity: Jasper 8 <3,FRIENDSANITY,Professor Jasper Thomas
+6009,Museum,Friendsanity: Jasper 9 <3,FRIENDSANITY,Professor Jasper Thomas
+6010,Museum,Friendsanity: Jasper 10 <3,FRIENDSANITY,Professor Jasper Thomas
+6011,Museum,Friendsanity: Jasper 11 <3,FRIENDSANITY,Professor Jasper Thomas
+6012,Museum,Friendsanity: Jasper 12 <3,FRIENDSANITY,Professor Jasper Thomas
+6013,Museum,Friendsanity: Jasper 13 <3,FRIENDSANITY,Professor Jasper Thomas
+6014,Museum,Friendsanity: Jasper 14 <3,FRIENDSANITY,Professor Jasper Thomas
+6015,Yoba's Clearing,Friendsanity: Yoba 1 <3,FRIENDSANITY,Custom NPC - Yoba
+6016,Yoba's Clearing,Friendsanity: Yoba 2 <3,FRIENDSANITY,Custom NPC - Yoba
+6017,Yoba's Clearing,Friendsanity: Yoba 3 <3,FRIENDSANITY,Custom NPC - Yoba
+6018,Yoba's Clearing,Friendsanity: Yoba 4 <3,FRIENDSANITY,Custom NPC - Yoba
+6019,Yoba's Clearing,Friendsanity: Yoba 5 <3,FRIENDSANITY,Custom NPC - Yoba
+6020,Yoba's Clearing,Friendsanity: Yoba 6 <3,FRIENDSANITY,Custom NPC - Yoba
+6021,Yoba's Clearing,Friendsanity: Yoba 7 <3,FRIENDSANITY,Custom NPC - Yoba
+6022,Yoba's Clearing,Friendsanity: Yoba 8 <3,FRIENDSANITY,Custom NPC - Yoba
+6023,Yoba's Clearing,Friendsanity: Yoba 9 <3,FRIENDSANITY,Custom NPC - Yoba
+6024,Yoba's Clearing,Friendsanity: Yoba 10 <3,FRIENDSANITY,Custom NPC - Yoba
+6025,Marnie's Ranch,Friendsanity: Mr. Ginger 1 <3,FRIENDSANITY,Mister Ginger (cat npc)
+6026,Marnie's Ranch,Friendsanity: Mr. Ginger 2 <3,FRIENDSANITY,Mister Ginger (cat npc)
+6027,Marnie's Ranch,Friendsanity: Mr. Ginger 3 <3,FRIENDSANITY,Mister Ginger (cat npc)
+6028,Marnie's Ranch,Friendsanity: Mr. Ginger 4 <3,FRIENDSANITY,Mister Ginger (cat npc)
+6029,Marnie's Ranch,Friendsanity: Mr. Ginger 5 <3,FRIENDSANITY,Mister Ginger (cat npc)
+6030,Marnie's Ranch,Friendsanity: Mr. Ginger 6 <3,FRIENDSANITY,Mister Ginger (cat npc)
+6031,Marnie's Ranch,Friendsanity: Mr. Ginger 7 <3,FRIENDSANITY,Mister Ginger (cat npc)
+6032,Marnie's Ranch,Friendsanity: Mr. Ginger 8 <3,FRIENDSANITY,Mister Ginger (cat npc)
+6033,Marnie's Ranch,Friendsanity: Mr. Ginger 9 <3,FRIENDSANITY,Mister Ginger (cat npc)
+6034,Marnie's Ranch,Friendsanity: Mr. Ginger 10 <3,FRIENDSANITY,Mister Ginger (cat npc)
+6035,Town,Friendsanity: Ayeisha 1 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
+6036,Town,Friendsanity: Ayeisha 2 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
+6037,Town,Friendsanity: Ayeisha 3 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
+6038,Town,Friendsanity: Ayeisha 4 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
+6039,Town,Friendsanity: Ayeisha 5 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
+6040,Town,Friendsanity: Ayeisha 6 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
+6041,Town,Friendsanity: Ayeisha 7 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
+6042,Town,Friendsanity: Ayeisha 8 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
+6043,Town,Friendsanity: Ayeisha 9 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
+6044,Town,Friendsanity: Ayeisha 10 <3,FRIENDSANITY,Ayeisha - The Postal Worker (Custom NPC)
+6045,Saloon,Friendsanity: Shiko 1 <3,FRIENDSANITY,Shiko - New Custom NPC
+6046,Saloon,Friendsanity: Shiko 2 <3,FRIENDSANITY,Shiko - New Custom NPC
+6047,Saloon,Friendsanity: Shiko 3 <3,FRIENDSANITY,Shiko - New Custom NPC
+6048,Saloon,Friendsanity: Shiko 4 <3,FRIENDSANITY,Shiko - New Custom NPC
+6049,Saloon,Friendsanity: Shiko 5 <3,FRIENDSANITY,Shiko - New Custom NPC
+6050,Saloon,Friendsanity: Shiko 6 <3,FRIENDSANITY,Shiko - New Custom NPC
+6051,Saloon,Friendsanity: Shiko 7 <3,FRIENDSANITY,Shiko - New Custom NPC
+6052,Saloon,Friendsanity: Shiko 8 <3,FRIENDSANITY,Shiko - New Custom NPC
+6053,Saloon,Friendsanity: Shiko 9 <3,FRIENDSANITY,Shiko - New Custom NPC
+6054,Saloon,Friendsanity: Shiko 10 <3,FRIENDSANITY,Shiko - New Custom NPC
+6055,Saloon,Friendsanity: Shiko 11 <3,FRIENDSANITY,Shiko - New Custom NPC
+6056,Saloon,Friendsanity: Shiko 12 <3,FRIENDSANITY,Shiko - New Custom NPC
+6057,Saloon,Friendsanity: Shiko 13 <3,FRIENDSANITY,Shiko - New Custom NPC
+6058,Saloon,Friendsanity: Shiko 14 <3,FRIENDSANITY,Shiko - New Custom NPC
+6059,Wizard Tower,Friendsanity: Wellwick 1 <3,FRIENDSANITY,'Prophet' Wellwick
+6060,Wizard Tower,Friendsanity: Wellwick 2 <3,FRIENDSANITY,'Prophet' Wellwick
+6061,Wizard Tower,Friendsanity: Wellwick 3 <3,FRIENDSANITY,'Prophet' Wellwick
+6062,Wizard Tower,Friendsanity: Wellwick 4 <3,FRIENDSANITY,'Prophet' Wellwick
+6063,Wizard Tower,Friendsanity: Wellwick 5 <3,FRIENDSANITY,'Prophet' Wellwick
+6064,Wizard Tower,Friendsanity: Wellwick 6 <3,FRIENDSANITY,'Prophet' Wellwick
+6065,Wizard Tower,Friendsanity: Wellwick 7 <3,FRIENDSANITY,'Prophet' Wellwick
+6066,Wizard Tower,Friendsanity: Wellwick 8 <3,FRIENDSANITY,'Prophet' Wellwick
+6067,Wizard Tower,Friendsanity: Wellwick 9 <3,FRIENDSANITY,'Prophet' Wellwick
+6068,Wizard Tower,Friendsanity: Wellwick 10 <3,FRIENDSANITY,'Prophet' Wellwick
+6069,Wizard Tower,Friendsanity: Wellwick 11 <3,FRIENDSANITY,'Prophet' Wellwick
+6070,Wizard Tower,Friendsanity: Wellwick 12 <3,FRIENDSANITY,'Prophet' Wellwick
+6071,Wizard Tower,Friendsanity: Wellwick 13 <3,FRIENDSANITY,'Prophet' Wellwick
+6072,Wizard Tower,Friendsanity: Wellwick 14 <3,FRIENDSANITY,'Prophet' Wellwick
+6073,Forest,Friendsanity: Delores 1 <3,FRIENDSANITY,Delores - Custom NPC
+6074,Forest,Friendsanity: Delores 2 <3,FRIENDSANITY,Delores - Custom NPC
+6075,Forest,Friendsanity: Delores 3 <3,FRIENDSANITY,Delores - Custom NPC
+6076,Forest,Friendsanity: Delores 4 <3,FRIENDSANITY,Delores - Custom NPC
+6077,Forest,Friendsanity: Delores 5 <3,FRIENDSANITY,Delores - Custom NPC
+6078,Forest,Friendsanity: Delores 6 <3,FRIENDSANITY,Delores - Custom NPC
+6079,Forest,Friendsanity: Delores 7 <3,FRIENDSANITY,Delores - Custom NPC
+6080,Forest,Friendsanity: Delores 8 <3,FRIENDSANITY,Delores - Custom NPC
+6081,Forest,Friendsanity: Delores 9 <3,FRIENDSANITY,Delores - Custom NPC
+6082,Forest,Friendsanity: Delores 10 <3,FRIENDSANITY,Delores - Custom NPC
+6083,Forest,Friendsanity: Delores 11 <3,FRIENDSANITY,Delores - Custom NPC
+6084,Forest,Friendsanity: Delores 12 <3,FRIENDSANITY,Delores - Custom NPC
+6085,Forest,Friendsanity: Delores 13 <3,FRIENDSANITY,Delores - Custom NPC
+6086,Forest,Friendsanity: Delores 14 <3,FRIENDSANITY,Delores - Custom NPC
+6087,Alec's Pet Shop,Friendsanity: Alec 1 <3,FRIENDSANITY,Alec Revisited
+6088,Alec's Pet Shop,Friendsanity: Alec 2 <3,FRIENDSANITY,Alec Revisited
+6089,Alec's Pet Shop,Friendsanity: Alec 3 <3,FRIENDSANITY,Alec Revisited
+6090,Alec's Pet Shop,Friendsanity: Alec 4 <3,FRIENDSANITY,Alec Revisited
+6091,Alec's Pet Shop,Friendsanity: Alec 5 <3,FRIENDSANITY,Alec Revisited
+6092,Alec's Pet Shop,Friendsanity: Alec 6 <3,FRIENDSANITY,Alec Revisited
+6093,Alec's Pet Shop,Friendsanity: Alec 7 <3,FRIENDSANITY,Alec Revisited
+6094,Alec's Pet Shop,Friendsanity: Alec 8 <3,FRIENDSANITY,Alec Revisited
+6095,Alec's Pet Shop,Friendsanity: Alec 9 <3,FRIENDSANITY,Alec Revisited
+6096,Alec's Pet Shop,Friendsanity: Alec 10 <3,FRIENDSANITY,Alec Revisited
+6097,Alec's Pet Shop,Friendsanity: Alec 11 <3,FRIENDSANITY,Alec Revisited
+6098,Alec's Pet Shop,Friendsanity: Alec 12 <3,FRIENDSANITY,Alec Revisited
+6099,Alec's Pet Shop,Friendsanity: Alec 13 <3,FRIENDSANITY,Alec Revisited
+6100,Alec's Pet Shop,Friendsanity: Alec 14 <3,FRIENDSANITY,Alec Revisited
+6101,Eugene's Garden,Friendsanity: Eugene 1 <3,FRIENDSANITY,Custom NPC Eugene
+6102,Eugene's Garden,Friendsanity: Eugene 2 <3,FRIENDSANITY,Custom NPC Eugene
+6103,Eugene's Garden,Friendsanity: Eugene 3 <3,FRIENDSANITY,Custom NPC Eugene
+6104,Eugene's Garden,Friendsanity: Eugene 4 <3,FRIENDSANITY,Custom NPC Eugene
+6105,Eugene's Garden,Friendsanity: Eugene 5 <3,FRIENDSANITY,Custom NPC Eugene
+6106,Eugene's Garden,Friendsanity: Eugene 6 <3,FRIENDSANITY,Custom NPC Eugene
+6107,Eugene's Garden,Friendsanity: Eugene 7 <3,FRIENDSANITY,Custom NPC Eugene
+6108,Eugene's Garden,Friendsanity: Eugene 8 <3,FRIENDSANITY,Custom NPC Eugene
+6109,Eugene's Garden,Friendsanity: Eugene 9 <3,FRIENDSANITY,Custom NPC Eugene
+6110,Eugene's Garden,Friendsanity: Eugene 10 <3,FRIENDSANITY,Custom NPC Eugene
+6111,Eugene's Garden,Friendsanity: Eugene 11 <3,FRIENDSANITY,Custom NPC Eugene
+6112,Eugene's Garden,Friendsanity: Eugene 12 <3,FRIENDSANITY,Custom NPC Eugene
+6113,Eugene's Garden,Friendsanity: Eugene 13 <3,FRIENDSANITY,Custom NPC Eugene
+6114,Eugene's Garden,Friendsanity: Eugene 14 <3,FRIENDSANITY,Custom NPC Eugene
+6115,Forest,Friendsanity: Juna 1 <3,FRIENDSANITY,Juna - Roommate NPC
+6116,Forest,Friendsanity: Juna 2 <3,FRIENDSANITY,Juna - Roommate NPC
+6117,Forest,Friendsanity: Juna 3 <3,FRIENDSANITY,Juna - Roommate NPC
+6118,Forest,Friendsanity: Juna 4 <3,FRIENDSANITY,Juna - Roommate NPC
+6119,Forest,Friendsanity: Juna 5 <3,FRIENDSANITY,Juna - Roommate NPC
+6120,Forest,Friendsanity: Juna 6 <3,FRIENDSANITY,Juna - Roommate NPC
+6121,Forest,Friendsanity: Juna 7 <3,FRIENDSANITY,Juna - Roommate NPC
+6122,Forest,Friendsanity: Juna 8 <3,FRIENDSANITY,Juna - Roommate NPC
+6123,Forest,Friendsanity: Juna 9 <3,FRIENDSANITY,Juna - Roommate NPC
+6124,Forest,Friendsanity: Juna 10 <3,FRIENDSANITY,Juna - Roommate NPC
+6125,Riley's House,Friendsanity: Riley 1 <3,FRIENDSANITY,Custom NPC - Riley
+6126,Riley's House,Friendsanity: Riley 2 <3,FRIENDSANITY,Custom NPC - Riley
+6127,Riley's House,Friendsanity: Riley 3 <3,FRIENDSANITY,Custom NPC - Riley
+6128,Riley's House,Friendsanity: Riley 4 <3,FRIENDSANITY,Custom NPC - Riley
+6129,Riley's House,Friendsanity: Riley 5 <3,FRIENDSANITY,Custom NPC - Riley
+6130,Riley's House,Friendsanity: Riley 6 <3,FRIENDSANITY,Custom NPC - Riley
+6131,Riley's House,Friendsanity: Riley 7 <3,FRIENDSANITY,Custom NPC - Riley
+6132,Riley's House,Friendsanity: Riley 8 <3,FRIENDSANITY,Custom NPC - Riley
+6133,Riley's House,Friendsanity: Riley 9 <3,FRIENDSANITY,Custom NPC - Riley
+6134,Riley's House,Friendsanity: Riley 10 <3,FRIENDSANITY,Custom NPC - Riley
+6135,Riley's House,Friendsanity: Riley 11 <3,FRIENDSANITY,Custom NPC - Riley
+6136,Riley's House,Friendsanity: Riley 12 <3,FRIENDSANITY,Custom NPC - Riley
+6137,Riley's House,Friendsanity: Riley 13 <3,FRIENDSANITY,Custom NPC - Riley
+6138,Riley's House,Friendsanity: Riley 14 <3,FRIENDSANITY,Custom NPC - Riley
+6139,JojaMart,Friendsanity: Claire 1 <3,FRIENDSANITY,Stardew Valley Expanded
+6140,JojaMart,Friendsanity: Claire 2 <3,FRIENDSANITY,Stardew Valley Expanded
+6141,JojaMart,Friendsanity: Claire 3 <3,FRIENDSANITY,Stardew Valley Expanded
+6142,JojaMart,Friendsanity: Claire 4 <3,FRIENDSANITY,Stardew Valley Expanded
+6143,JojaMart,Friendsanity: Claire 5 <3,FRIENDSANITY,Stardew Valley Expanded
+6144,JojaMart,Friendsanity: Claire 6 <3,FRIENDSANITY,Stardew Valley Expanded
+6145,JojaMart,Friendsanity: Claire 7 <3,FRIENDSANITY,Stardew Valley Expanded
+6146,JojaMart,Friendsanity: Claire 8 <3,FRIENDSANITY,Stardew Valley Expanded
+6147,JojaMart,Friendsanity: Claire 9 <3,FRIENDSANITY,Stardew Valley Expanded
+6148,JojaMart,Friendsanity: Claire 10 <3,FRIENDSANITY,Stardew Valley Expanded
+6149,JojaMart,Friendsanity: Claire 11 <3,FRIENDSANITY,Stardew Valley Expanded
+6150,JojaMart,Friendsanity: Claire 12 <3,FRIENDSANITY,Stardew Valley Expanded
+6151,JojaMart,Friendsanity: Claire 13 <3,FRIENDSANITY,Stardew Valley Expanded
+6152,JojaMart,Friendsanity: Claire 14 <3,FRIENDSANITY,Stardew Valley Expanded
+6153,Galmoran Outpost,Friendsanity: Lance 1 <3,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+6154,Galmoran Outpost,Friendsanity: Lance 2 <3,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+6155,Galmoran Outpost,Friendsanity: Lance 3 <3,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+6156,Galmoran Outpost,Friendsanity: Lance 4 <3,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+6157,Galmoran Outpost,Friendsanity: Lance 5 <3,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+6158,Galmoran Outpost,Friendsanity: Lance 6 <3,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+6159,Galmoran Outpost,Friendsanity: Lance 7 <3,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+6160,Galmoran Outpost,Friendsanity: Lance 8 <3,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+6161,Galmoran Outpost,Friendsanity: Lance 9 <3,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+6162,Galmoran Outpost,Friendsanity: Lance 10 <3,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+6163,Galmoran Outpost,Friendsanity: Lance 11 <3,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+6164,Galmoran Outpost,Friendsanity: Lance 12 <3,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+6165,Galmoran Outpost,Friendsanity: Lance 13 <3,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+6166,Galmoran Outpost,Friendsanity: Lance 14 <3,"FRIENDSANITY,GINGER_ISLAND",Stardew Valley Expanded
+6167,Jenkins' Residence,Friendsanity: Olivia 1 <3,FRIENDSANITY,Stardew Valley Expanded
+6168,Jenkins' Residence,Friendsanity: Olivia 2 <3,FRIENDSANITY,Stardew Valley Expanded
+6169,Jenkins' Residence,Friendsanity: Olivia 3 <3,FRIENDSANITY,Stardew Valley Expanded
+6170,Jenkins' Residence,Friendsanity: Olivia 4 <3,FRIENDSANITY,Stardew Valley Expanded
+6171,Jenkins' Residence,Friendsanity: Olivia 5 <3,FRIENDSANITY,Stardew Valley Expanded
+6172,Jenkins' Residence,Friendsanity: Olivia 6 <3,FRIENDSANITY,Stardew Valley Expanded
+6173,Jenkins' Residence,Friendsanity: Olivia 7 <3,FRIENDSANITY,Stardew Valley Expanded
+6174,Jenkins' Residence,Friendsanity: Olivia 8 <3,FRIENDSANITY,Stardew Valley Expanded
+6175,Jenkins' Residence,Friendsanity: Olivia 9 <3,FRIENDSANITY,Stardew Valley Expanded
+6176,Jenkins' Residence,Friendsanity: Olivia 10 <3,FRIENDSANITY,Stardew Valley Expanded
+6177,Jenkins' Residence,Friendsanity: Olivia 11 <3,FRIENDSANITY,Stardew Valley Expanded
+6178,Jenkins' Residence,Friendsanity: Olivia 12 <3,FRIENDSANITY,Stardew Valley Expanded
+6179,Jenkins' Residence,Friendsanity: Olivia 13 <3,FRIENDSANITY,Stardew Valley Expanded
+6180,Jenkins' Residence,Friendsanity: Olivia 14 <3,FRIENDSANITY,Stardew Valley Expanded
+6181,Wizard Tower,Friendsanity: Wizard 11 <3,FRIENDSANITY,Stardew Valley Expanded
+6182,Wizard Tower,Friendsanity: Wizard 12 <3,FRIENDSANITY,Stardew Valley Expanded
+6183,Wizard Tower,Friendsanity: Wizard 13 <3,FRIENDSANITY,Stardew Valley Expanded
+6184,Wizard Tower,Friendsanity: Wizard 14 <3,FRIENDSANITY,Stardew Valley Expanded
+6185,Blue Moon Vineyard,Friendsanity: Sophia 1 <3,FRIENDSANITY,Stardew Valley Expanded
+6186,Blue Moon Vineyard,Friendsanity: Sophia 2 <3,FRIENDSANITY,Stardew Valley Expanded
+6187,Blue Moon Vineyard,Friendsanity: Sophia 3 <3,FRIENDSANITY,Stardew Valley Expanded
+6188,Blue Moon Vineyard,Friendsanity: Sophia 4 <3,FRIENDSANITY,Stardew Valley Expanded
+6189,Blue Moon Vineyard,Friendsanity: Sophia 5 <3,FRIENDSANITY,Stardew Valley Expanded
+6190,Blue Moon Vineyard,Friendsanity: Sophia 6 <3,FRIENDSANITY,Stardew Valley Expanded
+6191,Blue Moon Vineyard,Friendsanity: Sophia 7 <3,FRIENDSANITY,Stardew Valley Expanded
+6192,Blue Moon Vineyard,Friendsanity: Sophia 8 <3,FRIENDSANITY,Stardew Valley Expanded
+6193,Blue Moon Vineyard,Friendsanity: Sophia 9 <3,FRIENDSANITY,Stardew Valley Expanded
+6194,Blue Moon Vineyard,Friendsanity: Sophia 10 <3,FRIENDSANITY,Stardew Valley Expanded
+6195,Blue Moon Vineyard,Friendsanity: Sophia 11 <3,FRIENDSANITY,Stardew Valley Expanded
+6196,Blue Moon Vineyard,Friendsanity: Sophia 12 <3,FRIENDSANITY,Stardew Valley Expanded
+6197,Blue Moon Vineyard,Friendsanity: Sophia 13 <3,FRIENDSANITY,Stardew Valley Expanded
+6198,Blue Moon Vineyard,Friendsanity: Sophia 14 <3,FRIENDSANITY,Stardew Valley Expanded
+6199,Jenkins' Residence,Friendsanity: Victor 1 <3,FRIENDSANITY,Stardew Valley Expanded
+6200,Jenkins' Residence,Friendsanity: Victor 2 <3,FRIENDSANITY,Stardew Valley Expanded
+6201,Jenkins' Residence,Friendsanity: Victor 3 <3,FRIENDSANITY,Stardew Valley Expanded
+6202,Jenkins' Residence,Friendsanity: Victor 4 <3,FRIENDSANITY,Stardew Valley Expanded
+6203,Jenkins' Residence,Friendsanity: Victor 5 <3,FRIENDSANITY,Stardew Valley Expanded
+6204,Jenkins' Residence,Friendsanity: Victor 6 <3,FRIENDSANITY,Stardew Valley Expanded
+6205,Jenkins' Residence,Friendsanity: Victor 7 <3,FRIENDSANITY,Stardew Valley Expanded
+6206,Jenkins' Residence,Friendsanity: Victor 8 <3,FRIENDSANITY,Stardew Valley Expanded
+6207,Jenkins' Residence,Friendsanity: Victor 9 <3,FRIENDSANITY,Stardew Valley Expanded
+6208,Jenkins' Residence,Friendsanity: Victor 10 <3,FRIENDSANITY,Stardew Valley Expanded
+6209,Jenkins' Residence,Friendsanity: Victor 11 <3,FRIENDSANITY,Stardew Valley Expanded
+6210,Jenkins' Residence,Friendsanity: Victor 12 <3,FRIENDSANITY,Stardew Valley Expanded
+6211,Jenkins' Residence,Friendsanity: Victor 13 <3,FRIENDSANITY,Stardew Valley Expanded
+6212,Jenkins' Residence,Friendsanity: Victor 14 <3,FRIENDSANITY,Stardew Valley Expanded
+6213,Fairhaven Farm,Friendsanity: Andy 1 <3,FRIENDSANITY,Stardew Valley Expanded
+6214,Fairhaven Farm,Friendsanity: Andy 2 <3,FRIENDSANITY,Stardew Valley Expanded
+6215,Fairhaven Farm,Friendsanity: Andy 3 <3,FRIENDSANITY,Stardew Valley Expanded
+6216,Fairhaven Farm,Friendsanity: Andy 4 <3,FRIENDSANITY,Stardew Valley Expanded
+6217,Fairhaven Farm,Friendsanity: Andy 5 <3,FRIENDSANITY,Stardew Valley Expanded
+6218,Fairhaven Farm,Friendsanity: Andy 6 <3,FRIENDSANITY,Stardew Valley Expanded
+6219,Fairhaven Farm,Friendsanity: Andy 7 <3,FRIENDSANITY,Stardew Valley Expanded
+6220,Fairhaven Farm,Friendsanity: Andy 8 <3,FRIENDSANITY,Stardew Valley Expanded
+6221,Fairhaven Farm,Friendsanity: Andy 9 <3,FRIENDSANITY,Stardew Valley Expanded
+6222,Fairhaven Farm,Friendsanity: Andy 10 <3,FRIENDSANITY,Stardew Valley Expanded
+6223,Aurora Vineyard,Friendsanity: Apples 1 <3,FRIENDSANITY,Stardew Valley Expanded
+6224,Aurora Vineyard,Friendsanity: Apples 2 <3,FRIENDSANITY,Stardew Valley Expanded
+6225,Aurora Vineyard,Friendsanity: Apples 3 <3,FRIENDSANITY,Stardew Valley Expanded
+6226,Aurora Vineyard,Friendsanity: Apples 4 <3,FRIENDSANITY,Stardew Valley Expanded
+6227,Aurora Vineyard,Friendsanity: Apples 5 <3,FRIENDSANITY,Stardew Valley Expanded
+6228,Aurora Vineyard,Friendsanity: Apples 6 <3,FRIENDSANITY,Stardew Valley Expanded
+6229,Aurora Vineyard,Friendsanity: Apples 7 <3,FRIENDSANITY,Stardew Valley Expanded
+6230,Aurora Vineyard,Friendsanity: Apples 8 <3,FRIENDSANITY,Stardew Valley Expanded
+6231,Aurora Vineyard,Friendsanity: Apples 9 <3,FRIENDSANITY,Stardew Valley Expanded
+6232,Aurora Vineyard,Friendsanity: Apples 10 <3,FRIENDSANITY,Stardew Valley Expanded
+6233,Museum,Friendsanity: Gunther 1 <3,FRIENDSANITY,Stardew Valley Expanded
+6234,Museum,Friendsanity: Gunther 2 <3,FRIENDSANITY,Stardew Valley Expanded
+6235,Museum,Friendsanity: Gunther 3 <3,FRIENDSANITY,Stardew Valley Expanded
+6236,Museum,Friendsanity: Gunther 4 <3,FRIENDSANITY,Stardew Valley Expanded
+6237,Museum,Friendsanity: Gunther 5 <3,FRIENDSANITY,Stardew Valley Expanded
+6238,Museum,Friendsanity: Gunther 6 <3,FRIENDSANITY,Stardew Valley Expanded
+6239,Museum,Friendsanity: Gunther 7 <3,FRIENDSANITY,Stardew Valley Expanded
+6240,Museum,Friendsanity: Gunther 8 <3,FRIENDSANITY,Stardew Valley Expanded
+6241,Museum,Friendsanity: Gunther 9 <3,FRIENDSANITY,Stardew Valley Expanded
+6242,Museum,Friendsanity: Gunther 10 <3,FRIENDSANITY,Stardew Valley Expanded
+6243,JojaMart,Friendsanity: Martin 1 <3,FRIENDSANITY,Stardew Valley Expanded
+6244,JojaMart,Friendsanity: Martin 2 <3,FRIENDSANITY,Stardew Valley Expanded
+6245,JojaMart,Friendsanity: Martin 3 <3,FRIENDSANITY,Stardew Valley Expanded
+6246,JojaMart,Friendsanity: Martin 4 <3,FRIENDSANITY,Stardew Valley Expanded
+6247,JojaMart,Friendsanity: Martin 5 <3,FRIENDSANITY,Stardew Valley Expanded
+6248,JojaMart,Friendsanity: Martin 6 <3,FRIENDSANITY,Stardew Valley Expanded
+6249,JojaMart,Friendsanity: Martin 7 <3,FRIENDSANITY,Stardew Valley Expanded
+6250,JojaMart,Friendsanity: Martin 8 <3,FRIENDSANITY,Stardew Valley Expanded
+6251,JojaMart,Friendsanity: Martin 9 <3,FRIENDSANITY,Stardew Valley Expanded
+6252,JojaMart,Friendsanity: Martin 10 <3,FRIENDSANITY,Stardew Valley Expanded
+6253,Adventurer's Guild,Friendsanity: Marlon 1 <3,FRIENDSANITY,Stardew Valley Expanded
+6254,Adventurer's Guild,Friendsanity: Marlon 2 <3,FRIENDSANITY,Stardew Valley Expanded
+6255,Adventurer's Guild,Friendsanity: Marlon 3 <3,FRIENDSANITY,Stardew Valley Expanded
+6256,Adventurer's Guild,Friendsanity: Marlon 4 <3,FRIENDSANITY,Stardew Valley Expanded
+6257,Adventurer's Guild,Friendsanity: Marlon 5 <3,FRIENDSANITY,Stardew Valley Expanded
+6258,Adventurer's Guild,Friendsanity: Marlon 6 <3,FRIENDSANITY,Stardew Valley Expanded
+6259,Adventurer's Guild,Friendsanity: Marlon 7 <3,FRIENDSANITY,Stardew Valley Expanded
+6260,Adventurer's Guild,Friendsanity: Marlon 8 <3,FRIENDSANITY,Stardew Valley Expanded
+6261,Adventurer's Guild,Friendsanity: Marlon 9 <3,FRIENDSANITY,Stardew Valley Expanded
+6262,Adventurer's Guild,Friendsanity: Marlon 10 <3,FRIENDSANITY,Stardew Valley Expanded
+6263,Wizard Tower,Friendsanity: Morgan 1 <3,FRIENDSANITY,Stardew Valley Expanded
+6264,Wizard Tower,Friendsanity: Morgan 2 <3,FRIENDSANITY,Stardew Valley Expanded
+6265,Wizard Tower,Friendsanity: Morgan 3 <3,FRIENDSANITY,Stardew Valley Expanded
+6266,Wizard Tower,Friendsanity: Morgan 4 <3,FRIENDSANITY,Stardew Valley Expanded
+6267,Wizard Tower,Friendsanity: Morgan 5 <3,FRIENDSANITY,Stardew Valley Expanded
+6268,Wizard Tower,Friendsanity: Morgan 6 <3,FRIENDSANITY,Stardew Valley Expanded
+6269,Wizard Tower,Friendsanity: Morgan 7 <3,FRIENDSANITY,Stardew Valley Expanded
+6270,Wizard Tower,Friendsanity: Morgan 8 <3,FRIENDSANITY,Stardew Valley Expanded
+6271,Wizard Tower,Friendsanity: Morgan 9 <3,FRIENDSANITY,Stardew Valley Expanded
+6272,Wizard Tower,Friendsanity: Morgan 10 <3,FRIENDSANITY,Stardew Valley Expanded
+6273,Scarlett's House,Friendsanity: Scarlett 1 <3,FRIENDSANITY,Stardew Valley Expanded
+6274,Scarlett's House,Friendsanity: Scarlett 2 <3,FRIENDSANITY,Stardew Valley Expanded
+6275,Scarlett's House,Friendsanity: Scarlett 3 <3,FRIENDSANITY,Stardew Valley Expanded
+6276,Scarlett's House,Friendsanity: Scarlett 4 <3,FRIENDSANITY,Stardew Valley Expanded
+6277,Scarlett's House,Friendsanity: Scarlett 5 <3,FRIENDSANITY,Stardew Valley Expanded
+6278,Scarlett's House,Friendsanity: Scarlett 6 <3,FRIENDSANITY,Stardew Valley Expanded
+6279,Scarlett's House,Friendsanity: Scarlett 7 <3,FRIENDSANITY,Stardew Valley Expanded
+6280,Scarlett's House,Friendsanity: Scarlett 8 <3,FRIENDSANITY,Stardew Valley Expanded
+6281,Scarlett's House,Friendsanity: Scarlett 9 <3,FRIENDSANITY,Stardew Valley Expanded
+6282,Scarlett's House,Friendsanity: Scarlett 10 <3,FRIENDSANITY,Stardew Valley Expanded
+6283,Susan's House,Friendsanity: Susan 1 <3,FRIENDSANITY,Stardew Valley Expanded
+6284,Susan's House,Friendsanity: Susan 2 <3,FRIENDSANITY,Stardew Valley Expanded
+6285,Susan's House,Friendsanity: Susan 3 <3,FRIENDSANITY,Stardew Valley Expanded
+6286,Susan's House,Friendsanity: Susan 4 <3,FRIENDSANITY,Stardew Valley Expanded
+6287,Susan's House,Friendsanity: Susan 5 <3,FRIENDSANITY,Stardew Valley Expanded
+6288,Susan's House,Friendsanity: Susan 6 <3,FRIENDSANITY,Stardew Valley Expanded
+6289,Susan's House,Friendsanity: Susan 7 <3,FRIENDSANITY,Stardew Valley Expanded
+6290,Susan's House,Friendsanity: Susan 8 <3,FRIENDSANITY,Stardew Valley Expanded
+6291,Susan's House,Friendsanity: Susan 9 <3,FRIENDSANITY,Stardew Valley Expanded
+6292,Susan's House,Friendsanity: Susan 10 <3,FRIENDSANITY,Stardew Valley Expanded
+6293,JojaMart,Friendsanity: Morris 1 <3,FRIENDSANITY,Stardew Valley Expanded
+6294,JojaMart,Friendsanity: Morris 2 <3,FRIENDSANITY,Stardew Valley Expanded
+6295,JojaMart,Friendsanity: Morris 3 <3,FRIENDSANITY,Stardew Valley Expanded
+6296,JojaMart,Friendsanity: Morris 4 <3,FRIENDSANITY,Stardew Valley Expanded
+6297,JojaMart,Friendsanity: Morris 5 <3,FRIENDSANITY,Stardew Valley Expanded
+6298,JojaMart,Friendsanity: Morris 6 <3,FRIENDSANITY,Stardew Valley Expanded
+6299,JojaMart,Friendsanity: Morris 7 <3,FRIENDSANITY,Stardew Valley Expanded
+6300,JojaMart,Friendsanity: Morris 8 <3,FRIENDSANITY,Stardew Valley Expanded
+6301,JojaMart,Friendsanity: Morris 9 <3,FRIENDSANITY,Stardew Valley Expanded
+6302,JojaMart,Friendsanity: Morris 10 <3,FRIENDSANITY,Stardew Valley Expanded
+6303,Witch's Swamp,Friendsanity: Zic 1 <3,FRIENDSANITY,Distant Lands - Witch Swamp Overhaul
+6304,Witch's Swamp,Friendsanity: Zic 2 <3,FRIENDSANITY,Distant Lands - Witch Swamp Overhaul
+6305,Witch's Swamp,Friendsanity: Zic 3 <3,FRIENDSANITY,Distant Lands - Witch Swamp Overhaul
+6306,Witch's Swamp,Friendsanity: Zic 4 <3,FRIENDSANITY,Distant Lands - Witch Swamp Overhaul
+6307,Witch's Swamp,Friendsanity: Zic 5 <3,FRIENDSANITY,Distant Lands - Witch Swamp Overhaul
+6308,Witch's Swamp,Friendsanity: Zic 6 <3,FRIENDSANITY,Distant Lands - Witch Swamp Overhaul
+6309,Witch's Swamp,Friendsanity: Zic 7 <3,FRIENDSANITY,Distant Lands - Witch Swamp Overhaul
+6310,Witch's Swamp,Friendsanity: Zic 8 <3,FRIENDSANITY,Distant Lands - Witch Swamp Overhaul
+6311,Witch's Swamp,Friendsanity: Zic 9 <3,FRIENDSANITY,Distant Lands - Witch Swamp Overhaul
+6312,Witch's Swamp,Friendsanity: Zic 10 <3,FRIENDSANITY,Distant Lands - Witch Swamp Overhaul
+6313,Witch's Attic,Friendsanity: Alecto 1 <3,FRIENDSANITY,Alecto the Witch
+6314,Witch's Attic,Friendsanity: Alecto 2 <3,FRIENDSANITY,Alecto the Witch
+6315,Witch's Attic,Friendsanity: Alecto 3 <3,FRIENDSANITY,Alecto the Witch
+6316,Witch's Attic,Friendsanity: Alecto 4 <3,FRIENDSANITY,Alecto the Witch
+6317,Witch's Attic,Friendsanity: Alecto 5 <3,FRIENDSANITY,Alecto the Witch
+6318,Witch's Attic,Friendsanity: Alecto 6 <3,FRIENDSANITY,Alecto the Witch
+6319,Witch's Attic,Friendsanity: Alecto 7 <3,FRIENDSANITY,Alecto the Witch
+6320,Witch's Attic,Friendsanity: Alecto 8 <3,FRIENDSANITY,Alecto the Witch
+6321,Witch's Attic,Friendsanity: Alecto 9 <3,FRIENDSANITY,Alecto the Witch
+6322,Witch's Attic,Friendsanity: Alecto 10 <3,FRIENDSANITY,Alecto the Witch
+6323,Mouse House,Friendsanity: Lacey 1 <3,FRIENDSANITY,Hat Mouse Lacey
+6324,Mouse House,Friendsanity: Lacey 2 <3,FRIENDSANITY,Hat Mouse Lacey
+6325,Mouse House,Friendsanity: Lacey 3 <3,FRIENDSANITY,Hat Mouse Lacey
+6326,Mouse House,Friendsanity: Lacey 4 <3,FRIENDSANITY,Hat Mouse Lacey
+6327,Mouse House,Friendsanity: Lacey 5 <3,FRIENDSANITY,Hat Mouse Lacey
+6328,Mouse House,Friendsanity: Lacey 6 <3,FRIENDSANITY,Hat Mouse Lacey
+6329,Mouse House,Friendsanity: Lacey 7 <3,FRIENDSANITY,Hat Mouse Lacey
+6330,Mouse House,Friendsanity: Lacey 8 <3,FRIENDSANITY,Hat Mouse Lacey
+6331,Mouse House,Friendsanity: Lacey 9 <3,FRIENDSANITY,Hat Mouse Lacey
+6332,Mouse House,Friendsanity: Lacey 10 <3,FRIENDSANITY,Hat Mouse Lacey
+6333,Mouse House,Friendsanity: Lacey 11 <3,FRIENDSANITY,Hat Mouse Lacey
+6334,Mouse House,Friendsanity: Lacey 12 <3,FRIENDSANITY,Hat Mouse Lacey
+6335,Mouse House,Friendsanity: Lacey 13 <3,FRIENDSANITY,Hat Mouse Lacey
+6336,Mouse House,Friendsanity: Lacey 14 <3,FRIENDSANITY,Hat Mouse Lacey
+6337,Boarding House - First Floor,Friendsanity: Joel 1 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6338,Boarding House - First Floor,Friendsanity: Joel 2 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6339,Boarding House - First Floor,Friendsanity: Joel 3 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6340,Boarding House - First Floor,Friendsanity: Joel 4 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6341,Boarding House - First Floor,Friendsanity: Joel 5 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6342,Boarding House - First Floor,Friendsanity: Joel 6 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6343,Boarding House - First Floor,Friendsanity: Joel 7 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6344,Boarding House - First Floor,Friendsanity: Joel 8 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6345,Boarding House - First Floor,Friendsanity: Joel 9 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6346,Boarding House - First Floor,Friendsanity: Joel 10 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6347,Boarding House - First Floor,Friendsanity: Sheila 1 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6348,Boarding House - First Floor,Friendsanity: Sheila 2 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6349,Boarding House - First Floor,Friendsanity: Sheila 3 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6350,Boarding House - First Floor,Friendsanity: Sheila 4 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6351,Boarding House - First Floor,Friendsanity: Sheila 5 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6352,Boarding House - First Floor,Friendsanity: Sheila 6 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6353,Boarding House - First Floor,Friendsanity: Sheila 7 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6354,Boarding House - First Floor,Friendsanity: Sheila 8 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6355,Boarding House - First Floor,Friendsanity: Sheila 9 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6356,Boarding House - First Floor,Friendsanity: Sheila 10 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6357,Boarding House - First Floor,Friendsanity: Sheila 11 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6358,Boarding House - First Floor,Friendsanity: Sheila 12 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6359,Boarding House - First Floor,Friendsanity: Sheila 13 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6360,Boarding House - First Floor,Friendsanity: Sheila 14 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6361,The Lost Valley,Friendsanity: Gregory 1 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6362,The Lost Valley,Friendsanity: Gregory 2 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6363,The Lost Valley,Friendsanity: Gregory 3 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6364,The Lost Valley,Friendsanity: Gregory 4 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6365,The Lost Valley,Friendsanity: Gregory 5 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6366,The Lost Valley,Friendsanity: Gregory 6 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6367,The Lost Valley,Friendsanity: Gregory 7 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6368,The Lost Valley,Friendsanity: Gregory 8 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6369,The Lost Valley,Friendsanity: Gregory 9 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6370,The Lost Valley,Friendsanity: Gregory 10 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6371,The Lost Valley,Friendsanity: Gregory 11 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6372,The Lost Valley,Friendsanity: Gregory 12 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6373,The Lost Valley,Friendsanity: Gregory 13 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+6374,The Lost Valley,Friendsanity: Gregory 14 <3,FRIENDSANITY,Boarding House and Bus Stop Extension
+7001,Pierre's General Store,Premium Pack,BACKPACK,Bigger Backpack
+7002,Carpenter Shop,Tractor Garage Blueprint,BUILDING_BLUEPRINT,Tractor Mod
+7003,The Deep Woods Depth 100,Pet the Deep Woods Unicorn,MANDATORY,DeepWoods
+7004,The Deep Woods Depth 50,Breaking Up Deep Woods Gingerbread House,MANDATORY,DeepWoods
+7005,The Deep Woods Depth 50,Drinking From Deep Woods Fountain,MANDATORY,DeepWoods
+7006,The Deep Woods Depth 100,Deep Woods Treasure Chest,MANDATORY,DeepWoods
+7007,The Deep Woods Depth 100,Deep Woods Trash Bin,MANDATORY,DeepWoods
+7008,The Deep Woods Depth 50,Chop Down a Deep Woods Iridium Tree,MANDATORY,DeepWoods
+7009,The Deep Woods Depth 10,The Deep Woods: Depth 10,ELEVATOR,DeepWoods
+7010,The Deep Woods Depth 20,The Deep Woods: Depth 20,ELEVATOR,DeepWoods
+7011,The Deep Woods Depth 30,The Deep Woods: Depth 30,ELEVATOR,DeepWoods
+7012,The Deep Woods Depth 40,The Deep Woods: Depth 40,ELEVATOR,DeepWoods
+7013,The Deep Woods Depth 50,The Deep Woods: Depth 50,ELEVATOR,DeepWoods
+7014,The Deep Woods Depth 60,The Deep Woods: Depth 60,ELEVATOR,DeepWoods
+7015,The Deep Woods Depth 70,The Deep Woods: Depth 70,ELEVATOR,DeepWoods
+7016,The Deep Woods Depth 80,The Deep Woods: Depth 80,ELEVATOR,DeepWoods
+7017,The Deep Woods Depth 90,The Deep Woods: Depth 90,ELEVATOR,DeepWoods
+7018,The Deep Woods Depth 100,The Deep Woods: Depth 100,ELEVATOR,DeepWoods
+7019,The Deep Woods Depth 50,Purify an Infested Lichtung,MANDATORY,DeepWoods
+7020,Skull Cavern Floor 25,Skull Cavern: Floor 25,ELEVATOR,Skull Cavern Elevator
+7021,Skull Cavern Floor 50,Skull Cavern: Floor 50,ELEVATOR,Skull Cavern Elevator
+7022,Skull Cavern Floor 75,Skull Cavern: Floor 75,ELEVATOR,Skull Cavern Elevator
+7023,Skull Cavern Floor 100,Skull Cavern: Floor 100,ELEVATOR,Skull Cavern Elevator
+7024,Skull Cavern Floor 125,Skull Cavern: Floor 125,ELEVATOR,Skull Cavern Elevator
+7025,Skull Cavern Floor 150,Skull Cavern: Floor 150,ELEVATOR,Skull Cavern Elevator
+7026,Skull Cavern Floor 175,Skull Cavern: Floor 175,ELEVATOR,Skull Cavern Elevator
+7027,Skull Cavern Floor 200,Skull Cavern: Floor 200,ELEVATOR,Skull Cavern Elevator
+7028,The Deep Woods Depth 100,The Sword in the Stone,MANDATORY,DeepWoods
+7051,Abandoned Mines - 1A,Abandoned Treasure - Floor 1A,MANDATORY,Boarding House and Bus Stop Extension
+7052,Abandoned Mines - 1B,Abandoned Treasure - Floor 1B,MANDATORY,Boarding House and Bus Stop Extension
+7053,Abandoned Mines - 2A,Abandoned Treasure - Floor 2A,MANDATORY,Boarding House and Bus Stop Extension
+7054,Abandoned Mines - 2B,Abandoned Treasure - Floor 2B,MANDATORY,Boarding House and Bus Stop Extension
+7055,Abandoned Mines - 3,Abandoned Treasure - Floor 3,MANDATORY,Boarding House and Bus Stop Extension
+7056,Abandoned Mines - 4,Abandoned Treasure - Floor 4,MANDATORY,Boarding House and Bus Stop Extension
+7057,Abandoned Mines - 5,Abandoned Treasure - Floor 5,MANDATORY,Boarding House and Bus Stop Extension
+7401,Farm,Cook Magic Elixir,COOKSANITY,Magic
+7402,Farm,Craft Travel Core,CRAFTSANITY,Magic
+7403,Farm,Craft Haste Elixir,CRAFTSANITY,Stardew Valley Expanded
+7404,Farm,Craft Hero Elixir,CRAFTSANITY,Stardew Valley Expanded
+7405,Farm,Craft Armor Elixir,CRAFTSANITY,Stardew Valley Expanded
+7406,Witch's Swamp,Craft Ginger Tincture,"CRAFTSANITY,GINGER_ISLAND",Distant Lands - Witch Swamp Overhaul
+7407,Farm,Craft Glass Path,CRAFTSANITY,Archaeology
+7408,Farm,Craft Glass Brazier,CRAFTSANITY,Archaeology
+7409,Farm,Craft Glass Fence,CRAFTSANITY,Archaeology
+7410,Farm,Craft Bone Path,CRAFTSANITY,Archaeology
+7411,Farm,Craft Water Shifter,CRAFTSANITY,Archaeology
+7412,Farm,Craft Wooden Display,CRAFTSANITY,Archaeology
+7413,Farm,Craft Hardwood Display,CRAFTSANITY,Archaeology
+7414,Farm,Craft Dwarf Gadget: Infinite Volcano Simulation,"CRAFTSANITY,GINGER_ISLAND",Archaeology
+7415,Farm,Craft Grinder,CRAFTSANITY,Archaeology
+7416,Farm,Craft Preservation Chamber,CRAFTSANITY,Archaeology
+7417,Farm,Craft Hardwood Preservation Chamber,CRAFTSANITY,Archaeology
+7418,Farm,Craft Ancient Battery Production Station,CRAFTSANITY,Archaeology
+7419,Farm,Craft Neanderthal Skeleton,CRAFTSANITY,Boarding House and Bus Stop Extension
+7420,Farm,Craft Pterodactyl Skeleton L,CRAFTSANITY,Boarding House and Bus Stop Extension
+7421,Farm,Craft Pterodactyl Skeleton M,CRAFTSANITY,Boarding House and Bus Stop Extension
+7422,Farm,Craft Pterodactyl Skeleton R,CRAFTSANITY,Boarding House and Bus Stop Extension
+7423,Farm,Craft T-Rex Skeleton L,CRAFTSANITY,Boarding House and Bus Stop Extension
+7424,Farm,Craft T-Rex Skeleton M,CRAFTSANITY,Boarding House and Bus Stop Extension
+7425,Farm,Craft T-Rex Skeleton R,CRAFTSANITY,Boarding House and Bus Stop Extension
+7426,Farm,Craft Restoration Table,CRAFTSANITY,Archaeology
+7427,Farm,Craft Rusty Path,CRAFTSANITY,Archaeology
+7428,Farm,Craft Rusty Brazier,CRAFTSANITY,Archaeology
+7429,Farm,Craft Lucky Ring,CRAFTSANITY,Archaeology
+7430,Farm,Craft Bone Fence,CRAFTSANITY,Archaeology
+7431,Farm,Craft Bouquet,CRAFTSANITY,Socializing Skill
+7432,Farm,Craft Trash Bin,CRAFTSANITY,Binning Skill
+7433,Farm,Craft Composter,CRAFTSANITY,Binning Skill
+7434,Farm,Craft Recycling Bin,CRAFTSANITY,Binning Skill
+7435,Farm,Craft Advanced Recycling Machine,CRAFTSANITY,Binning Skill
+7451,Adventurer's Guild,Magic Elixir Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Magic
+7452,Adventurer's Guild,Travel Core Recipe,CRAFTSANITY,Magic
+7453,Alesia Shop,Haste Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded
+7454,Isaac Shop,Hero Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded
+7455,Alesia Shop,Armor Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded
+7501,Mountain,Missing Envelope,"STORY_QUEST",Ayeisha - The Postal Worker (Custom NPC)
+7502,Forest,Ayeisha's Lost Ring,"STORY_QUEST",Ayeisha - The Postal Worker (Custom NPC)
+7503,Forest,Mr.Ginger's request,"STORY_QUEST",Mister Ginger (cat npc)
+7504,Forest,Juna's Drink Request,"STORY_QUEST",Juna - Roommate NPC
+7505,Forest,Juna's BFF Request,"STORY_QUEST",Juna - Roommate NPC
+7506,Forest,Juna's Monster Mash,SPECIAL_ORDER_BOARD,Juna - Roommate NPC
+7507,Adventurer's Guild,Marlon's Boat,"STORY_QUEST,GINGER_ISLAND",Stardew Valley Expanded
+7508,Railroad,The Railroad Boulder,"STORY_QUEST",Stardew Valley Expanded
+7509,Grandpa's Shed Interior,Grandpa's Shed,"STORY_QUEST",Stardew Valley Expanded
+7510,Aurora Vineyard,Aurora Vineyard,"STORY_QUEST",Stardew Valley Expanded
+7511,Lance's House Main,Monster Crops,"STORY_QUEST,GINGER_ISLAND",Stardew Valley Expanded
+7512,Sewer,Void Soul Retrieval,"STORY_QUEST,GINGER_ISLAND",Stardew Valley Expanded
+7513,Fairhaven Farm,Andy's Cellar,SPECIAL_ORDER_BOARD,Stardew Valley Expanded
+7514,Adventurer's Guild,A Mysterious Venture,SPECIAL_ORDER_BOARD,Stardew Valley Expanded
+7515,Jenkins' Residence,An Elegant Reception,SPECIAL_ORDER_BOARD,Stardew Valley Expanded
+7516,Sophia's House,Fairy Garden,"SPECIAL_ORDER_BOARD,GINGER_ISLAND",Stardew Valley Expanded
+7517,Susan's House,Homemade Fertilizer,SPECIAL_ORDER_BOARD,Stardew Valley Expanded
+7519,Witch's Swamp,Corrupted Crops Task,GINGER_ISLAND,Distant Lands - Witch Swamp Overhaul
+7520,Witch's Swamp,A New Pot,STORY_QUEST,Distant Lands - Witch Swamp Overhaul
+7521,Witch's Swamp,Fancy Blanket Task,STORY_QUEST,Distant Lands - Witch Swamp Overhaul
+7522,Witch's Swamp,Witch's order,GINGER_ISLAND,Distant Lands - Witch Swamp Overhaul
+7523,Boarding House - First Floor,Pumpkin Soup,STORY_QUEST,Boarding House and Bus Stop Extension
+7524,Museum,Geode Order,SPECIAL_ORDER_BOARD,Professor Jasper Thomas
+7525,Museum,Dwarven Scrolls,SPECIAL_ORDER_BOARD,Professor Jasper Thomas
+7526,Mouse House,Hats for the Hat Mouse,STORY_QUEST,Hat Mouse Lacey
+7551,Kitchen,Cook Baked Berry Oatmeal,COOKSANITY,Stardew Valley Expanded
+7552,Kitchen,Cook Flower Cookie,COOKSANITY,Stardew Valley Expanded
+7553,Kitchen,Cook Big Bark Burger,COOKSANITY,Stardew Valley Expanded
+7554,Kitchen,Cook Frog Legs,COOKSANITY,Stardew Valley Expanded
+7555,Kitchen,Cook Glazed Butterfish,COOKSANITY,Stardew Valley Expanded
+7556,Kitchen,Cook Mixed Berry Pie,COOKSANITY,Stardew Valley Expanded
+7557,Kitchen,Cook Mushroom Berry Rice,COOKSANITY,Stardew Valley Expanded
+7558,Kitchen,Cook Seaweed Salad,COOKSANITY,Stardew Valley Expanded
+7559,Kitchen,Cook Void Delight,COOKSANITY,Stardew Valley Expanded
+7560,Kitchen,Cook Void Salmon Sushi,COOKSANITY,Stardew Valley Expanded
+7561,Kitchen,Cook Mushroom Kebab,COOKSANITY,Distant Lands - Witch Swamp Overhaul
+7562,Kitchen,Cook Crayfish Soup,COOKSANITY,Distant Lands - Witch Swamp Overhaul
+7563,Kitchen,Cook Pemmican,COOKSANITY,Distant Lands - Witch Swamp Overhaul
+7564,Kitchen,Cook Void Mint Tea,COOKSANITY,Distant Lands - Witch Swamp Overhaul
+7565,Kitchen,Cook Special Pumpkin Soup,COOKSANITY,Boarding House and Bus Stop Extension
+7566,Kitchen,Cook Digger's Delight,COOKSANITY,Archaeology
+7567,Kitchen,Cook Rocky Root Coffee,COOKSANITY,Archaeology
+7568,Kitchen,Cook Ancient Jello,COOKSANITY,Archaeology
+7569,Kitchen,Cook Grilled Cheese,COOKSANITY,Binning Skill
+7570,Kitchen,Cook Fish Casserole,COOKSANITY,Binning Skill
+7601,Bear Shop,Baked Berry Oatmeal Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+7602,Bear Shop,Flower Cookie Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+7603,Saloon,Big Bark Burger Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Stardew Valley Expanded
+7604,Adventurer's Guild,Frog Legs Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+7605,Saloon,Glazed Butterfish Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Stardew Valley Expanded
+7606,Saloon,Mixed Berry Pie Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+7607,Adventurer's Guild,Mushroom Berry Rice Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Stardew Valley Expanded
+7608,Adventurer's Guild,Seaweed Salad Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+7609,Sewer,Void Delight Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Stardew Valley Expanded
+7610,Sewer,Void Salmon Sushi Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded
+7611,Witch's Swamp,Mushroom Kebab Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul
+7613,Witch's Swamp,Pemmican Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul
+7614,Witch's Swamp,Void Mint Tea Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul
+7616,Mines Dwarf Shop,Neanderthal Skeleton Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension
+7617,Mines Dwarf Shop,Pterodactyl Skeleton L Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension
+7618,Mines Dwarf Shop,Pterodactyl Skeleton M Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension
+7619,Mines Dwarf Shop,Pterodactyl Skeleton R Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension
+7620,Mines Dwarf Shop,T-Rex Skeleton L Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension
+7621,Mines Dwarf Shop,T-Rex Skeleton M Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension
+7622,Mines Dwarf Shop,T-Rex Skeleton R Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension
+7623,Farm,Digger's Delight Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology
+7624,Farm,Rocky Root Coffee Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology
+7625,Farm,Ancient Jello Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology
+7627,Farm,Grilled Cheese Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill
+7628,Farm,Fish Casserole Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill
+7651,Alesia Shop,Tempered Galaxy Dagger,MANDATORY,Stardew Valley Expanded
+7652,Isaac Shop,Tempered Galaxy Sword,MANDATORY,Stardew Valley Expanded
+7653,Isaac Shop,Tempered Galaxy Hammer,MANDATORY,Stardew Valley Expanded
+7701,Island South,Fishsanity: Baby Lunaloo,"FISHSANITY,GINGER_ISLAND",Stardew Valley Expanded
+7702,Crimson Badlands,Fishsanity: Bonefish,FISHSANITY,Stardew Valley Expanded
+7703,Forest,Fishsanity: Bull Trout,FISHSANITY,Stardew Valley Expanded
+7704,Forest West,Fishsanity: Butterfish,FISHSANITY,Stardew Valley Expanded
+7705,Island South,Fishsanity: Clownfish,"FISHSANITY,GINGER_ISLAND",Stardew Valley Expanded
+7706,Highlands Outside,Fishsanity: Daggerfish,"FISHSANITY,GINGER_ISLAND",Stardew Valley Expanded
+7707,Mountain,Fishsanity: Frog,FISHSANITY,Stardew Valley Expanded
+7708,Highlands Outside,Fishsanity: Gemfish,"FISHSANITY,GINGER_ISLAND",Stardew Valley Expanded
+7709,Sprite Spring,Fishsanity: Goldenfish,FISHSANITY,Stardew Valley Expanded
+7710,Secret Woods,Fishsanity: Grass Carp,FISHSANITY,Stardew Valley Expanded
+7711,Forest West,Fishsanity: King Salmon,FISHSANITY,Stardew Valley Expanded
+7712,Island West,Fishsanity: Lunaloo,"FISHSANITY,GINGER_ISLAND",Stardew Valley Expanded
+7713,Sprite Spring,Fishsanity: Meteor Carp,FISHSANITY,Stardew Valley Expanded
+7714,Town,Fishsanity: Minnow,FISHSANITY,Stardew Valley Expanded
+7715,Forest West,Fishsanity: Puppyfish,FISHSANITY,Stardew Valley Expanded
+7716,Sewer,Fishsanity: Radioactive Bass,FISHSANITY,Stardew Valley Expanded
+7717,Island West,Fishsanity: Seahorse,"FISHSANITY,GINGER_ISLAND",Stardew Valley Expanded
+7718,Island West,Fishsanity: Sea Sponge,"FISHSANITY,GINGER_ISLAND",Stardew Valley Expanded
+7719,Island South,Fishsanity: Shiny Lunaloo,"FISHSANITY,GINGER_ISLAND",Stardew Valley Expanded
+7720,Mutant Bug Lair,Fishsanity: Snatcher Worm,FISHSANITY,Stardew Valley Expanded
+7721,Beach,Fishsanity: Starfish,"FISHSANITY,GINGER_ISLAND",Stardew Valley Expanded
+7722,Fable Reef,Fishsanity: Torpedo Trout,"FISHSANITY,GINGER_ISLAND",Stardew Valley Expanded
+7723,Witch's Swamp,Fishsanity: Void Eel,FISHSANITY,Stardew Valley Expanded
+7724,Mutant Bug Lair,Fishsanity: Water Grub,FISHSANITY,Stardew Valley Expanded
+7725,Crimson Badlands,Fishsanity: Undeadfish,FISHSANITY,Stardew Valley Expanded
+7726,Shearwater Bridge,Fishsanity: Kittyfish,FISHSANITY,Stardew Valley Expanded
+7728,Witch's Swamp,Fishsanity: Void Minnow,FISHSANITY,Distant Lands - Witch Swamp Overhaul
+7729,Witch's Swamp,Fishsanity: Swamp Leech,FISHSANITY,Distant Lands - Witch Swamp Overhaul
+7730,Witch's Swamp,Fishsanity: Giant Horsehoe Crab,FISHSANITY,Distant Lands - Witch Swamp Overhaul
+7731,Witch's Swamp,Fishsanity: Purple Algae,FISHSANITY,Distant Lands - Witch Swamp Overhaul
+7901,Farm,Harvest Monster Fruit,"CROPSANITY,GINGER_ISLAND",Stardew Valley Expanded
+7902,Farm,Harvest Salal Berry,CROPSANITY,Stardew Valley Expanded
+7903,Farm,Harvest Slime Berry,"CROPSANITY,GINGER_ISLAND",Stardew Valley Expanded
+7904,Farm,Harvest Ancient Fiber,CROPSANITY,Stardew Valley Expanded
+7905,Farm,Harvest Monster Mushroom,"CROPSANITY,GINGER_ISLAND",Stardew Valley Expanded
+7906,Farm,Harvest Void Root,"CROPSANITY,GINGER_ISLAND",Stardew Valley Expanded
+7907,Farm,Harvest Void Mint Leaves,CROPSANITY,Distant Lands - Witch Swamp Overhaul
+7908,Farm,Harvest Vile Ancient Fruit,CROPSANITY,Distant Lands - Witch Swamp Overhaul
+8001,Shipping,Shipsanity: Magic Elixir,SHIPSANITY,Magic
+8002,Shipping,Shipsanity: Travel Core,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Magic
+8003,Shipping,Shipsanity: Aegis Elixir,SHIPSANITY,Stardew Valley Expanded
+8004,Shipping,Shipsanity: Aged Blue Moon Wine,SHIPSANITY,Stardew Valley Expanded
+8005,Shipping,Shipsanity: Ancient Fern Seed,SHIPSANITY,Stardew Valley Expanded
+8006,Shipping,Shipsanity: Ancient Fiber,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8007,Shipping,Shipsanity: Armor Elixir,SHIPSANITY,Stardew Valley Expanded
+8008,Shipping,Shipsanity: Baby Lunaloo,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded
+8009,Shipping,Shipsanity: Baked Berry Oatmeal,SHIPSANITY,Stardew Valley Expanded
+8010,Shipping,Shipsanity: Barbarian Elixir,SHIPSANITY,Stardew Valley Expanded
+8011,Shipping,Shipsanity: Bearberry,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8012,Shipping,Shipsanity: Big Bark Burger,SHIPSANITY,Stardew Valley Expanded
+8013,Shipping,Shipsanity: Conch,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8014,Shipping,Shipsanity: Blue Moon Wine,SHIPSANITY,Stardew Valley Expanded
+8015,Shipping,Shipsanity: Bonefish,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8016,Shipping,Shipsanity: Bull Trout,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8017,Shipping,Shipsanity: Butterfish,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8018,Shipping,Shipsanity: Clownfish,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded
+8019,Shipping,Shipsanity: Daggerfish,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded
+8020,Shipping,Shipsanity: Dewdrop Berry,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8021,Shipping,Shipsanity: Sand Dollar,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8023,Shipping,Shipsanity: Ferngill Primrose,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8024,Shipping,Shipsanity: Flower Cookie,SHIPSANITY,Stardew Valley Expanded
+8025,Shipping,Shipsanity: Frog,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8026,Shipping,Shipsanity: Frog Legs,SHIPSANITY,Stardew Valley Expanded
+8027,Shipping,Shipsanity: Fungus Seed,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded
+8029,Shipping,Shipsanity: Gemfish,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded
+8030,Shipping,Shipsanity: Glazed Butterfish,"SHIPSANITY",Stardew Valley Expanded
+8031,Shipping,Shipsanity: Golden Ocean Flower,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT,GINGER_ISLAND",Stardew Valley Expanded
+8032,Shipping,Shipsanity: Goldenfish,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8033,Shipping,Shipsanity: Goldenrod,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8034,Shipping,Shipsanity: Grampleton Orange Chicken,SHIPSANITY,Stardew Valley Expanded
+8035,Shipping,Shipsanity: Grass Carp,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8036,Shipping,Shipsanity: Gravity Elixir,SHIPSANITY,Stardew Valley Expanded
+8037,Shipping,Shipsanity: Green Mushroom,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT,GINGER_ISLAND",Stardew Valley Expanded
+8038,Shipping,Shipsanity: Haste Elixir,SHIPSANITY,Stardew Valley Expanded
+8039,Shipping,Shipsanity: Hero Elixir,SHIPSANITY,Stardew Valley Expanded
+8040,Shipping,Shipsanity: King Salmon,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8050,Shipping,Shipsanity: Kittyfish,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8051,Shipping,Shipsanity: Lightning Elixir,SHIPSANITY,Stardew Valley Expanded
+8052,Shipping,Shipsanity: Four Leaf Clover,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8053,Shipping,Shipsanity: Lunaloo,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded
+8054,Shipping,Shipsanity: Meteor Carp,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8055,Shipping,Shipsanity: Minnow,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8056,Shipping,Shipsanity: Mixed Berry Pie,SHIPSANITY,Stardew Valley Expanded
+8057,Shipping,Shipsanity: Monster Fruit,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT,GINGER_ISLAND",Stardew Valley Expanded
+8058,Shipping,Shipsanity: Monster Mushroom,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT,GINGER_ISLAND",Stardew Valley Expanded
+8059,Shipping,Shipsanity: Mushroom Berry Rice,SHIPSANITY,Stardew Valley Expanded
+8060,Shipping,Shipsanity: Mushroom Colony,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8061,Shipping,Shipsanity: Ornate Treasure Chest,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded
+8062,Shipping,Shipsanity: Poison Mushroom,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8063,Shipping,Shipsanity: Puppyfish,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8064,Shipping,Shipsanity: Radioactive Bass,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8065,Shipping,Shipsanity: Red Baneberry,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8066,Shipping,Shipsanity: Rusty Blade,SHIPSANITY,Stardew Valley Expanded
+8067,Shipping,Shipsanity: Salal Berry,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT,GINGER_ISLAND",Stardew Valley Expanded
+8068,Shipping,Shipsanity: Sea Sponge,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded
+8069,Shipping,Shipsanity: Seahorse,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded
+8070,Shipping,Shipsanity: Seaweed Salad,SHIPSANITY,Stardew Valley Expanded
+8071,Shipping,Shipsanity: Shiny Lunaloo,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded
+8072,Shipping,Shipsanity: Shrub Seed,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded
+8073,Shipping,Shipsanity: Slime Berry,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT,GINGER_ISLAND",Stardew Valley Expanded
+8074,Shipping,Shipsanity: Slime Seed,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded
+8075,Shipping,Shipsanity: Rafflesia,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8076,Shipping,Shipsanity: Sports Drink,SHIPSANITY,Stardew Valley Expanded
+8077,Shipping,Shipsanity: Stalk Seed,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded
+8078,Shipping,Shipsanity: Stamina Capsule,SHIPSANITY,Stardew Valley Expanded
+8079,Shipping,Shipsanity: Starfish,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded
+8080,Shipping,Shipsanity: Swirl Stone,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8081,Shipping,Shipsanity: Thistle,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8082,Shipping,Shipsanity: Torpedo Trout,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded
+8083,Shipping,Shipsanity: Undeadfish,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8084,Shipping,Shipsanity: Void Delight,SHIPSANITY,Stardew Valley Expanded
+8085,Shipping,Shipsanity: Void Eel,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8086,Shipping,Shipsanity: Void Pebble,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8087,Shipping,Shipsanity: Void Root,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT,GINGER_ISLAND",Stardew Valley Expanded
+8088,Shipping,Shipsanity: Void Salmon Sushi,SHIPSANITY,Stardew Valley Expanded
+8089,Shipping,Shipsanity: Void Seed,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded
+8090,Shipping,Shipsanity: Void Shard,SHIPSANITY,Stardew Valley Expanded
+8091,Shipping,Shipsanity: Void Soul,SHIPSANITY,Stardew Valley Expanded
+8092,Shipping,Shipsanity: Water Grub,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded
+8093,Shipping,Shipsanity: Winter Star Rose,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded
+8094,Shipping,Shipsanity: Wooden Display: Amphibian Fossil,SHIPSANITY,Archaeology
+8095,Shipping,Shipsanity: Hardwood Display: Amphibian Fossil,SHIPSANITY,Archaeology
+8096,Shipping,Shipsanity: Wooden Display: Anchor,SHIPSANITY,Archaeology
+8097,Shipping,Shipsanity: Hardwood Display: Anchor,SHIPSANITY,Archaeology
+8098,Shipping,Shipsanity: Wooden Display: Ancient Doll,SHIPSANITY,Archaeology
+8099,Shipping,Shipsanity: Hardwood Display: Ancient Doll,SHIPSANITY,Archaeology
+8100,Shipping,Shipsanity: Wooden Display: Ancient Drum,SHIPSANITY,Archaeology
+8101,Shipping,Shipsanity: Hardwood Display: Ancient Drum,SHIPSANITY,Archaeology
+8102,Shipping,Shipsanity: Wooden Display: Ancient Seed,SHIPSANITY,Archaeology
+8103,Shipping,Shipsanity: Hardwood Display: Ancient Seed,SHIPSANITY,Archaeology
+8104,Shipping,Shipsanity: Wooden Display: Ancient Sword,SHIPSANITY,Archaeology
+8105,Shipping,Shipsanity: Hardwood Display: Ancient Sword,SHIPSANITY,Archaeology
+8106,Shipping,Shipsanity: Wooden Display: Arrowhead,SHIPSANITY,Archaeology
+8107,Shipping,Shipsanity: Hardwood Display: Arrowhead,SHIPSANITY,Archaeology
+8108,Shipping,Shipsanity: Wooden Display: Bone Flute,SHIPSANITY,Archaeology
+8109,Shipping,Shipsanity: Hardwood Display: Bone Flute,SHIPSANITY,Archaeology
+8110,Shipping,Shipsanity: Wooden Display: Chewing Stick,SHIPSANITY,Archaeology
+8111,Shipping,Shipsanity: Hardwood Display: Chewing Stick,SHIPSANITY,Archaeology
+8112,Shipping,Shipsanity: Wooden Display: Chicken Statue,SHIPSANITY,Archaeology
+8113,Shipping,Shipsanity: Hardwood Display: Chicken Statue,SHIPSANITY,Archaeology
+8114,Shipping,Shipsanity: Wooden Display: Chipped Amphora,SHIPSANITY,Archaeology
+8115,Shipping,Shipsanity: Hardwood Display: Chipped Amphora,SHIPSANITY,Archaeology
+8116,Shipping,Shipsanity: Wooden Display: Dinosaur Egg,SHIPSANITY,Archaeology
+8117,Shipping,Shipsanity: Hardwood Display: Dinosaur Egg,SHIPSANITY,Archaeology
+8118,Shipping,Shipsanity: Wooden Display: Dried Starfish,SHIPSANITY,Archaeology
+8119,Shipping,Shipsanity: Hardwood Display: Dried Starfish,SHIPSANITY,Archaeology
+8120,Shipping,Shipsanity: Wooden Display: Dwarf Gadget,SHIPSANITY,Archaeology
+8121,Shipping,Shipsanity: Hardwood Display: Dwarf Gadget,SHIPSANITY,Archaeology
+8122,Shipping,Shipsanity: Wooden Display: Dwarf Scroll I,SHIPSANITY,Archaeology
+8123,Shipping,Shipsanity: Hardwood Display: Dwarf Scroll I,SHIPSANITY,Archaeology
+8124,Shipping,Shipsanity: Wooden Display: Dwarf Scroll II,SHIPSANITY,Archaeology
+8125,Shipping,Shipsanity: Hardwood Display: Dwarf Scroll II,SHIPSANITY,Archaeology
+8126,Shipping,Shipsanity: Wooden Display: Dwarf Scroll III,SHIPSANITY,Archaeology
+8127,Shipping,Shipsanity: Hardwood Display: Dwarf Scroll III,SHIPSANITY,Archaeology
+8128,Shipping,Shipsanity: Wooden Display: Dwarf Scroll IV,SHIPSANITY,Archaeology
+8129,Shipping,Shipsanity: Hardwood Display: Dwarf Scroll IV,SHIPSANITY,Archaeology
+8130,Shipping,Shipsanity: Wooden Display: Dwarvish Helm,SHIPSANITY,Archaeology
+8131,Shipping,Shipsanity: Hardwood Display: Dwarvish Helm,SHIPSANITY,Archaeology
+8132,Shipping,Shipsanity: Wooden Display: Elvish Jewelry,SHIPSANITY,Archaeology
+8133,Shipping,Shipsanity: Hardwood Display: Elvish Jewelry,SHIPSANITY,Archaeology
+8134,Shipping,Shipsanity: Wooden Display: Fossilized Leg,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8135,Shipping,Shipsanity: Hardwood Display: Fossilized Leg,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8136,Shipping,Shipsanity: Wooden Display: Fossilized Ribs,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8137,Shipping,Shipsanity: Hardwood Display: Fossilized Ribs,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8138,Shipping,Shipsanity: Wooden Display: Fossilized Skull,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8139,Shipping,Shipsanity: Hardwood Display: Fossilized Skull,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8140,Shipping,Shipsanity: Wooden Display: Fossilized Spine,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8141,Shipping,Shipsanity: Hardwood Display: Fossilized Spine,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8142,Shipping,Shipsanity: Wooden Display: Fossilized Tail,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8143,Shipping,Shipsanity: Hardwood Display: Fossilized Tail,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8144,Shipping,Shipsanity: Wooden Display: Glass Shards,SHIPSANITY,Archaeology
+8145,Shipping,Shipsanity: Hardwood Display: Glass Shards,SHIPSANITY,Archaeology
+8146,Shipping,Shipsanity: Wooden Display: Golden Mask,SHIPSANITY,Archaeology
+8147,Shipping,Shipsanity: Hardwood Display: Golden Mask,SHIPSANITY,Archaeology
+8148,Shipping,Shipsanity: Wooden Display: Golden Relic,SHIPSANITY,Archaeology
+8149,Shipping,Shipsanity: Hardwood Display: Golden Relic,SHIPSANITY,Archaeology
+8150,Shipping,Shipsanity: Wooden Display: Mummified Bat,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8151,Shipping,Shipsanity: Hardwood Display: Mummified Bat,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8152,Shipping,Shipsanity: Wooden Display: Mummified Frog,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8153,Shipping,Shipsanity: Hardwood Display: Mummified Frog,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8154,Shipping,Shipsanity: Wooden Display: Nautilus Fossil,SHIPSANITY,Archaeology
+8155,Shipping,Shipsanity: Hardwood Display: Nautilus Fossil,SHIPSANITY,Archaeology
+8156,Shipping,Shipsanity: Wooden Display: Ornamental Fan,SHIPSANITY,Archaeology
+8157,Shipping,Shipsanity: Hardwood Display: Ornamental Fan,SHIPSANITY,Archaeology
+8158,Shipping,Shipsanity: Wooden Display: Palm Fossil,SHIPSANITY,Archaeology
+8159,Shipping,Shipsanity: Hardwood Display: Palm Fossil,SHIPSANITY,Archaeology
+8160,Shipping,Shipsanity: Wooden Display: Prehistoric Handaxe,SHIPSANITY,Archaeology
+8161,Shipping,Shipsanity: Hardwood Display: Prehistoric Handaxe,SHIPSANITY,Archaeology
+8162,Shipping,Shipsanity: Wooden Display: Prehistoric Rib,SHIPSANITY,Archaeology
+8163,Shipping,Shipsanity: Hardwood Display: Prehistoric Rib,SHIPSANITY,Archaeology
+8164,Shipping,Shipsanity: Wooden Display: Prehistoric Scapula,SHIPSANITY,Archaeology
+8165,Shipping,Shipsanity: Hardwood Display: Prehistoric Scapula,SHIPSANITY,Archaeology
+8166,Shipping,Shipsanity: Wooden Display: Prehistoric Skull,SHIPSANITY,Archaeology
+8167,Shipping,Shipsanity: Hardwood Display: Prehistoric Skull,SHIPSANITY,Archaeology
+8168,Shipping,Shipsanity: Wooden Display: Prehistoric Tibia,SHIPSANITY,Archaeology
+8169,Shipping,Shipsanity: Hardwood Display: Prehistoric Tibia,SHIPSANITY,Archaeology
+8170,Shipping,Shipsanity: Wooden Display: Prehistoric Tool,SHIPSANITY,Archaeology
+8171,Shipping,Shipsanity: Hardwood Display: Prehistoric Tool,SHIPSANITY,Archaeology
+8172,Shipping,Shipsanity: Wooden Display: Prehistoric Vertebra,SHIPSANITY,Archaeology
+8173,Shipping,Shipsanity: Hardwood Display: Prehistoric Vertebra,SHIPSANITY,Archaeology
+8174,Shipping,Shipsanity: Wooden Display: Rare Disc,SHIPSANITY,Archaeology
+8175,Shipping,Shipsanity: Hardwood Display: Rare Disc,SHIPSANITY,Archaeology
+8176,Shipping,Shipsanity: Wooden Display: Rusty Cog,SHIPSANITY,Archaeology
+8177,Shipping,Shipsanity: Hardwood Display: Rusty Cog,SHIPSANITY,Archaeology
+8178,Shipping,Shipsanity: Wooden Display: Rusty Spoon,SHIPSANITY,Archaeology
+8179,Shipping,Shipsanity: Hardwood Display: Rusty Spoon,SHIPSANITY,Archaeology
+8180,Shipping,Shipsanity: Wooden Display: Rusty Spur,SHIPSANITY,Archaeology
+8181,Shipping,Shipsanity: Hardwood Display: Rusty Spur,SHIPSANITY,Archaeology
+8182,Shipping,Shipsanity: Wooden Display: Skeletal Hand,SHIPSANITY,Archaeology
+8183,Shipping,Shipsanity: Hardwood Display: Skeletal Hand,SHIPSANITY,Archaeology
+8184,Shipping,Shipsanity: Wooden Display: Skeletal Tail,SHIPSANITY,Archaeology
+8185,Shipping,Shipsanity: Hardwood Display: Skeletal Tail,SHIPSANITY,Archaeology
+8186,Shipping,Shipsanity: Wooden Display: Snake Skull,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8187,Shipping,Shipsanity: Hardwood Display: Snake Skull,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8188,Shipping,Shipsanity: Wooden Display: Snake Vertebrae,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8189,Shipping,Shipsanity: Hardwood Display: Snake Vertebrae,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8190,Shipping,Shipsanity: Wooden Display: Strange Doll (Green),SHIPSANITY,Archaeology
+8191,Shipping,Shipsanity: Hardwood Display: Strange Doll (Green),SHIPSANITY,Archaeology
+8192,Shipping,Shipsanity: Wooden Display: Strange Doll,SHIPSANITY,Archaeology
+8193,Shipping,Shipsanity: Hardwood Display: Strange Doll,SHIPSANITY,Archaeology
+8194,Shipping,Shipsanity: Wooden Display: Trilobite Fossil,SHIPSANITY,Archaeology
+8195,Shipping,Shipsanity: Hardwood Display: Trilobite Fossil,SHIPSANITY,Archaeology
+8196,Shipping,Shipsanity: Bone Path,SHIPSANITY,Archaeology
+8197,Shipping,Shipsanity: Glass Fence,SHIPSANITY,Archaeology
+8198,Shipping,Shipsanity: Glass Path,SHIPSANITY,Archaeology
+8199,Shipping,Shipsanity: Hardwood Display,SHIPSANITY,Archaeology
+8200,Shipping,Shipsanity: Wooden Display,SHIPSANITY,Archaeology
+8201,Shipping,Shipsanity: Dwarf Gadget: Infinite Volcano Simulation,"SHIPSANITY,GINGER_ISLAND",Archaeology
+8202,Shipping,Shipsanity: Water Shifter,SHIPSANITY,Archaeology
+8203,Shipping,Shipsanity: Brown Amanita,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Distant Lands - Witch Swamp Overhaul
+8204,Shipping,Shipsanity: Swamp Herb,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Distant Lands - Witch Swamp Overhaul
+8205,Shipping,Shipsanity: Void Mint Seeds,SHIPSANITY,Distant Lands - Witch Swamp Overhaul
+8206,Shipping,Shipsanity: Vile Ancient Fruit Seeds,SHIPSANITY,Distant Lands - Witch Swamp Overhaul
+8207,Shipping,Shipsanity: Void Mint Leaves,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",Distant Lands - Witch Swamp Overhaul
+8208,Shipping,Shipsanity: Vile Ancient Fruit,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",Distant Lands - Witch Swamp Overhaul
+8209,Shipping,Shipsanity: Void Minnow,"SHIPSANITY,SHIPSANITY_FISH",Distant Lands - Witch Swamp Overhaul
+8210,Shipping,Shipsanity: Swamp Leech,"SHIPSANITY,SHIPSANITY_FISH",Distant Lands - Witch Swamp Overhaul
+8211,Shipping,Shipsanity: Purple Algae,SHIPSANITY,Distant Lands - Witch Swamp Overhaul
+8212,Shipping,Shipsanity: Giant Horsehoe Crab,"SHIPSANITY,SHIPSANITY_FISH",Distant Lands - Witch Swamp Overhaul
+8213,Shipping,Shipsanity: Mushroom Kebab,SHIPSANITY,Distant Lands - Witch Swamp Overhaul
+8214,Shipping,Shipsanity: Crayfish Soup,SHIPSANITY,Distant Lands - Witch Swamp Overhaul
+8215,Shipping,Shipsanity: Pemmican,SHIPSANITY,Distant Lands - Witch Swamp Overhaul
+8216,Shipping,Shipsanity: Void Mint Tea,SHIPSANITY,Distant Lands - Witch Swamp Overhaul
+8217,Shipping,Shipsanity: Ginger Tincture,"SHIPSANITY,GINGER_ISLAND",Distant Lands - Witch Swamp Overhaul
+8218,Shipping,Shipsanity: Neanderthal Limb Bones,SHIPSANITY,Boarding House and Bus Stop Extension
+8219,Shipping,Shipsanity: Dinosaur Claw,SHIPSANITY,Boarding House and Bus Stop Extension
+8220,Shipping,Shipsanity: Special Pumpkin Soup,SHIPSANITY,Boarding House and Bus Stop Extension
+8221,Shipping,Shipsanity: Pterodactyl L Wing Bone,SHIPSANITY,Boarding House and Bus Stop Extension
+8222,Shipping,Shipsanity: Dinosaur Skull,SHIPSANITY,Boarding House and Bus Stop Extension
+8223,Shipping,Shipsanity: Dinosaur Tooth,SHIPSANITY,Boarding House and Bus Stop Extension
+8224,Shipping,Shipsanity: Pterodactyl Egg,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Boarding House and Bus Stop Extension
+8225,Shipping,Shipsanity: Pterodactyl Ribs,SHIPSANITY,Boarding House and Bus Stop Extension
+8226,Shipping,Shipsanity: Dinosaur Vertebra,SHIPSANITY,Boarding House and Bus Stop Extension
+8227,Shipping,Shipsanity: Neanderthal Ribs,SHIPSANITY,Boarding House and Bus Stop Extension
+8228,Shipping,Shipsanity: Dinosaur Pelvis,SHIPSANITY,Boarding House and Bus Stop Extension
+8229,Shipping,Shipsanity: Dinosaur Ribs,SHIPSANITY,Boarding House and Bus Stop Extension
+8230,Shipping,Shipsanity: Pterodactyl Phalange,SHIPSANITY,Boarding House and Bus Stop Extension
+8231,Shipping,Shipsanity: Pterodactyl Vertebra,SHIPSANITY,Boarding House and Bus Stop Extension
+8232,Shipping,Shipsanity: Neanderthal Pelvis,SHIPSANITY,Boarding House and Bus Stop Extension
+8233,Shipping,Shipsanity: Pterodactyl Skull,SHIPSANITY,Boarding House and Bus Stop Extension
+8234,Shipping,Shipsanity: Dinosaur Femur,SHIPSANITY,Boarding House and Bus Stop Extension
+8235,Shipping,Shipsanity: Pterodactyl Claw,SHIPSANITY,Boarding House and Bus Stop Extension
+8236,Shipping,Shipsanity: Neanderthal Skull,SHIPSANITY,Boarding House and Bus Stop Extension
+8237,Shipping,Shipsanity: Pterodactyl R Wing Bone,SHIPSANITY,Boarding House and Bus Stop Extension
+8238,Shipping,Shipsanity: Scrap Rust,SHIPSANITY,Archaeology
+8239,Shipping,Shipsanity: Rusty Path,SHIPSANITY,Archaeology
+8241,Shipping,Shipsanity: Digger's Delight,SHIPSANITY,Archaeology
+8242,Shipping,Shipsanity: Rocky Root Coffee,SHIPSANITY,Archaeology
+8243,Shipping,Shipsanity: Ancient Jello,SHIPSANITY,Archaeology
+8244,Shipping,Shipsanity: Bone Fence,SHIPSANITY,Archaeology
+8245,Shipping,Shipsanity: Grilled Cheese,SHIPSANITY,Binning Skill
+8246,Shipping,Shipsanity: Fish Casserole,SHIPSANITY,Binning Skill
+8247,Shipping,Shipsanity: Snatcher Worm,SHIPSANITY,Stardew Valley Expanded
diff --git a/worlds/stardew_valley/data/monster_data.py b/worlds/stardew_valley/data/monster_data.py
index 6030571f89fe..b423fce4a3db 100644
--- a/worlds/stardew_valley/data/monster_data.py
+++ b/worlds/stardew_valley/data/monster_data.py
@@ -1,8 +1,173 @@
-class Monster:
- duggy = "Duggy"
- blue_slime = "Blue Slime"
- pepper_rex = "Pepper Rex"
- stone_golem = "Stone Golem"
+from dataclasses import dataclass
+from typing import List, Tuple, Dict, Set, Callable
+from ..mods.mod_data import ModNames
+from ..mods.mod_monster_locations import modded_monsters_locations
+from ..strings.monster_names import Monster, MonsterCategory
+from ..strings.performance_names import Performance
+from ..strings.region_names import Region
-frozen_monsters = (Monster.blue_slime,)
+
+@dataclass(frozen=True)
+class StardewMonster:
+ name: str
+ category: str
+ locations: Tuple[str]
+ difficulty: str
+
+ def __repr__(self):
+ return f"{self.name} [{self.category}] (Locations: {self.locations} |" \
+ f" Difficulty: {self.difficulty}) |"
+
+
+slime_hutch = (Region.slime_hutch,)
+mines_floor_20 = (Region.mines_floor_20,)
+mines_floor_60 = (Region.mines_floor_60,)
+mines_floor_100 = (Region.mines_floor_100,)
+dangerous_mines_20 = (Region.dangerous_mines_20,)
+dangerous_mines_60 = (Region.dangerous_mines_60,)
+dangerous_mines_100 = (Region.dangerous_mines_100,)
+quarry_mine = (Region.quarry_mine,)
+mutant_bug_lair = (Region.mutant_bug_lair,)
+skull_cavern = (Region.skull_cavern_25,)
+skull_cavern_high = (Region.skull_cavern_75,)
+skull_cavern_dangerous = (Region.dangerous_skull_cavern,)
+tiger_slime_grove = (Region.island_west,)
+volcano = (Region.volcano_floor_5,)
+volcano_high = (Region.volcano_floor_10,)
+
+all_monsters: List[StardewMonster] = []
+monster_modifications_by_mod: Dict[str, Dict[str, Callable[[str, StardewMonster], StardewMonster]]] = {}
+
+
+def create_monster(name: str, category: str, locations: Tuple[str, ...], difficulty: str) -> StardewMonster:
+ monster = StardewMonster(name, category, locations, difficulty)
+ all_monsters.append(monster)
+ return monster
+
+
+def update_monster_locations(mod_name: str, monster: StardewMonster):
+ new_locations = modded_monsters_locations[mod_name][monster.name]
+ total_locations = tuple(sorted(set(monster.locations + new_locations)))
+ return StardewMonster(monster.name, monster.category, total_locations, monster.difficulty)
+
+
+def register_monster_modification(mod_name: str, monster: StardewMonster, modification_function):
+ if mod_name not in monster_modifications_by_mod:
+ monster_modifications_by_mod[mod_name] = {}
+ monster_modifications_by_mod[mod_name][monster.name] = modification_function
+
+
+green_slime = create_monster(Monster.green_slime, MonsterCategory.slime, mines_floor_20, Performance.basic)
+blue_slime = create_monster(Monster.blue_slime, MonsterCategory.slime, mines_floor_60, Performance.decent)
+red_slime = create_monster(Monster.red_slime, MonsterCategory.slime, mines_floor_100, Performance.good)
+purple_slime = create_monster(Monster.purple_slime, MonsterCategory.slime, skull_cavern, Performance.great)
+yellow_slime = create_monster(Monster.yellow_slime, MonsterCategory.slime, skull_cavern_high, Performance.galaxy)
+black_slime = create_monster(Monster.black_slime, MonsterCategory.slime, slime_hutch, Performance.decent)
+copper_slime = create_monster(Monster.copper_slime, MonsterCategory.slime, quarry_mine, Performance.decent)
+iron_slime = create_monster(Monster.iron_slime, MonsterCategory.slime, quarry_mine, Performance.good)
+tiger_slime = create_monster(Monster.tiger_slime, MonsterCategory.slime, tiger_slime_grove, Performance.galaxy)
+
+shadow_shaman = create_monster(Monster.shadow_shaman, MonsterCategory.void_spirits, mines_floor_100, Performance.good)
+shadow_shaman_dangerous = create_monster(Monster.shadow_shaman_dangerous, MonsterCategory.void_spirits, dangerous_mines_100, Performance.galaxy)
+shadow_brute = create_monster(Monster.shadow_brute, MonsterCategory.void_spirits, mines_floor_100, Performance.good)
+shadow_brute_dangerous = create_monster(Monster.shadow_brute_dangerous, MonsterCategory.void_spirits, dangerous_mines_100, Performance.galaxy)
+shadow_sniper = create_monster(Monster.shadow_sniper, MonsterCategory.void_spirits, dangerous_mines_100, Performance.galaxy)
+
+bat = create_monster(Monster.bat, MonsterCategory.bats, mines_floor_20, Performance.basic)
+bat_dangerous = create_monster(Monster.bat_dangerous, MonsterCategory.bats, dangerous_mines_20, Performance.galaxy)
+frost_bat = create_monster(Monster.frost_bat, MonsterCategory.bats, mines_floor_60, Performance.decent)
+frost_bat_dangerous = create_monster(Monster.frost_bat_dangerous, MonsterCategory.bats, dangerous_mines_60, Performance.galaxy)
+lava_bat = create_monster(Monster.lava_bat, MonsterCategory.bats, mines_floor_100, Performance.good)
+iridium_bat = create_monster(Monster.iridium_bat, MonsterCategory.bats, skull_cavern_high, Performance.great)
+
+skeleton = create_monster(Monster.skeleton, MonsterCategory.skeletons, mines_floor_100, Performance.good)
+skeleton_dangerous = create_monster(Monster.skeleton_dangerous, MonsterCategory.skeletons, dangerous_mines_100, Performance.galaxy)
+skeleton_mage = create_monster(Monster.skeleton_mage, MonsterCategory.skeletons, dangerous_mines_100, Performance.galaxy)
+
+bug = create_monster(Monster.bug, MonsterCategory.cave_insects, mines_floor_20, Performance.basic)
+bug_dangerous = create_monster(Monster.bug_dangerous, MonsterCategory.cave_insects, dangerous_mines_20, Performance.galaxy)
+cave_fly = create_monster(Monster.cave_fly, MonsterCategory.cave_insects, mines_floor_20, Performance.basic)
+cave_fly_dangerous = create_monster(Monster.cave_fly_dangerous, MonsterCategory.cave_insects, dangerous_mines_60, Performance.galaxy)
+grub = create_monster(Monster.grub, MonsterCategory.cave_insects, mines_floor_20, Performance.basic)
+grub_dangerous = create_monster(Monster.grub_dangerous, MonsterCategory.cave_insects, dangerous_mines_60, Performance.galaxy)
+mutant_fly = create_monster(Monster.mutant_fly, MonsterCategory.cave_insects, mutant_bug_lair, Performance.good)
+mutant_grub = create_monster(Monster.mutant_grub, MonsterCategory.cave_insects, mutant_bug_lair, Performance.good)
+armored_bug = create_monster(Monster.armored_bug, MonsterCategory.cave_insects, skull_cavern, Performance.basic) # Requires 'Bug Killer' enchantment
+armored_bug_dangerous = create_monster(Monster.armored_bug_dangerous, MonsterCategory.cave_insects, skull_cavern,
+ Performance.good) # Requires 'Bug Killer' enchantment
+
+duggy = create_monster(Monster.duggy, MonsterCategory.duggies, mines_floor_20, Performance.basic)
+duggy_dangerous = create_monster(Monster.duggy_dangerous, MonsterCategory.duggies, dangerous_mines_20, Performance.great)
+magma_duggy = create_monster(Monster.magma_duggy, MonsterCategory.duggies, volcano, Performance.galaxy)
+
+dust_sprite = create_monster(Monster.dust_sprite, MonsterCategory.dust_sprites, mines_floor_60, Performance.basic)
+dust_sprite_dangerous = create_monster(Monster.dust_sprite_dangerous, MonsterCategory.dust_sprites, dangerous_mines_60, Performance.great)
+
+rock_crab = create_monster(Monster.rock_crab, MonsterCategory.rock_crabs, mines_floor_20, Performance.basic)
+rock_crab_dangerous = create_monster(Monster.rock_crab_dangerous, MonsterCategory.rock_crabs, dangerous_mines_20, Performance.great)
+lava_crab = create_monster(Monster.lava_crab, MonsterCategory.rock_crabs, mines_floor_100, Performance.good)
+lava_crab_dangerous = create_monster(Monster.lava_crab_dangerous, MonsterCategory.rock_crabs, dangerous_mines_100, Performance.galaxy)
+iridium_crab = create_monster(Monster.iridium_crab, MonsterCategory.rock_crabs, skull_cavern, Performance.great)
+
+mummy = create_monster(Monster.mummy, MonsterCategory.mummies, skull_cavern, Performance.great) # Requires bombs or "Crusader" enchantment
+mummy_dangerous = create_monster(Monster.mummy_dangerous, MonsterCategory.mummies, skull_cavern_dangerous,
+ Performance.maximum) # Requires bombs or "Crusader" enchantment
+
+pepper_rex = create_monster(Monster.pepper_rex, MonsterCategory.pepper_rex, skull_cavern, Performance.great)
+
+serpent = create_monster(Monster.serpent, MonsterCategory.serpents, skull_cavern, Performance.galaxy)
+royal_serpent = create_monster(Monster.royal_serpent, MonsterCategory.serpents, skull_cavern_dangerous, Performance.maximum)
+
+magma_sprite = create_monster(Monster.magma_sprite, MonsterCategory.magma_sprites, volcano, Performance.galaxy)
+magma_sparker = create_monster(Monster.magma_sparker, MonsterCategory.magma_sprites, volcano_high, Performance.galaxy)
+
+register_monster_modification(ModNames.sve, shadow_brute_dangerous, update_monster_locations)
+register_monster_modification(ModNames.sve, shadow_sniper, update_monster_locations)
+register_monster_modification(ModNames.sve, shadow_shaman_dangerous, update_monster_locations)
+register_monster_modification(ModNames.sve, mummy_dangerous, update_monster_locations)
+register_monster_modification(ModNames.sve, royal_serpent, update_monster_locations)
+register_monster_modification(ModNames.sve, skeleton_dangerous, update_monster_locations)
+register_monster_modification(ModNames.sve, skeleton_mage, update_monster_locations)
+register_monster_modification(ModNames.sve, dust_sprite_dangerous, update_monster_locations)
+
+register_monster_modification(ModNames.deepwoods, shadow_brute, update_monster_locations)
+register_monster_modification(ModNames.deepwoods, cave_fly, update_monster_locations)
+register_monster_modification(ModNames.deepwoods, green_slime, update_monster_locations)
+
+register_monster_modification(ModNames.boarding_house, pepper_rex, update_monster_locations)
+register_monster_modification(ModNames.boarding_house, shadow_brute, update_monster_locations)
+register_monster_modification(ModNames.boarding_house, iridium_bat, update_monster_locations)
+register_monster_modification(ModNames.boarding_house, frost_bat, update_monster_locations)
+register_monster_modification(ModNames.boarding_house, cave_fly, update_monster_locations)
+register_monster_modification(ModNames.boarding_house, bat, update_monster_locations)
+register_monster_modification(ModNames.boarding_house, grub, update_monster_locations)
+register_monster_modification(ModNames.boarding_house, bug, update_monster_locations)
+
+
+def all_monsters_by_name_given_mods(mods: Set[str]) -> Dict[str, StardewMonster]:
+ monsters_by_name = {}
+ for monster in all_monsters:
+ current_monster = monster
+ for mod in monster_modifications_by_mod:
+ if mod not in mods or monster.name not in monster_modifications_by_mod[mod]:
+ continue
+ modification_function = monster_modifications_by_mod[mod][monster.name]
+ current_monster = modification_function(mod, current_monster)
+ monsters_by_name[monster.name] = current_monster
+ return monsters_by_name
+
+
+def all_monsters_by_category_given_mods(mods: Set[str]) -> Dict[str, Tuple[StardewMonster, ...]]:
+ monsters_by_category = {}
+ for monster in all_monsters:
+ current_monster = monster
+ for mod in monster_modifications_by_mod:
+ if mod not in mods or monster.name not in monster_modifications_by_mod[mod]:
+ continue
+ modification_function = monster_modifications_by_mod[mod][monster.name]
+ current_monster = modification_function(mod, current_monster)
+ if current_monster.category not in monsters_by_category:
+ monsters_by_category[monster.category] = ()
+ monsters_by_category[current_monster.category] = monsters_by_category[current_monster.category] + (current_monster,)
+ return monsters_by_category
diff --git a/worlds/stardew_valley/data/museum_data.py b/worlds/stardew_valley/data/museum_data.py
index b786f5b2d00c..b81c518a37c9 100644
--- a/worlds/stardew_valley/data/museum_data.py
+++ b/worlds/stardew_valley/data/museum_data.py
@@ -3,23 +3,24 @@
from dataclasses import dataclass
from typing import List, Tuple, Union, Optional
-from . import common_data as common
-from .game_item import GameItem
-from .monster_data import Monster
+from ..strings.monster_names import Monster
+from ..strings.fish_names import WaterChest
+from ..strings.forageable_names import Forageable
+from ..strings.metal_names import Mineral, Artifact, Fossil
from ..strings.region_names import Region
from ..strings.geode_names import Geode
@dataclass(frozen=True)
-class MuseumItem(GameItem):
+class MuseumItem:
+ item_name: str
locations: Tuple[str, ...]
geodes: Tuple[str, ...]
monsters: Tuple[str, ...]
difficulty: float
@staticmethod
- def of(name: str,
- item_id: int,
+ def of(item_name: str,
difficulty: float,
locations: Union[str, Tuple[str, ...]],
geodes: Union[str, Tuple[str, ...]],
@@ -33,10 +34,10 @@ def of(name: str,
if isinstance(monsters, str):
monsters = (monsters,)
- return MuseumItem(name, item_id, locations, geodes, monsters, difficulty)
+ return MuseumItem(item_name, locations, geodes, monsters, difficulty)
def __repr__(self):
- return f"{self.name} [{self.item_id}] (Locations: {self.locations} |" \
+ return f"{self.item_name} (Locations: {self.locations} |" \
f" Geodes: {self.geodes} |" \
f" Monsters: {self.monsters}) "
@@ -50,20 +51,18 @@ def __repr__(self):
def create_artifact(name: str,
- item_id: int,
difficulty: float,
locations: Union[str, Tuple[str, ...]] = (),
geodes: Union[str, Tuple[str, ...]] = (),
monsters: Union[str, Tuple[str, ...]] = ()) -> MuseumItem:
- artifact_item = MuseumItem.of(name, item_id, difficulty, locations, geodes, monsters)
+ artifact_item = MuseumItem.of(name, difficulty, locations, geodes, monsters)
all_museum_artifacts.append(artifact_item)
all_museum_items.append(artifact_item)
return artifact_item
def create_mineral(name: str,
- item_id: int,
- locations: Union[str, Tuple[str, ...]],
+ locations: Union[str, Tuple[str, ...]] = (),
geodes: Union[str, Tuple[str, ...]] = (),
monsters: Union[str, Tuple[str, ...]] = (),
difficulty: Optional[float] = None) -> MuseumItem:
@@ -77,213 +76,209 @@ def create_mineral(name: str,
difficulty += 1.0 / 26.0 * 100
if "Omni Geode" in geodes:
difficulty += 31.0 / 2750.0 * 100
+ if "Fishing Chest" in geodes:
+ difficulty += 4.3
- mineral_item = MuseumItem.of(name, item_id, difficulty, locations, geodes, monsters)
+ mineral_item = MuseumItem.of(name, difficulty, locations, geodes, monsters)
all_museum_minerals.append(mineral_item)
all_museum_items.append(mineral_item)
return mineral_item
class Artifact:
- dwarf_scroll_i = create_artifact("Dwarf Scroll I", 96, 5.6, Region.mines_floor_20,
+ dwarf_scroll_i = create_artifact("Dwarf Scroll I", 5.6, Region.mines_floor_20,
monsters=unlikely)
- dwarf_scroll_ii = create_artifact("Dwarf Scroll II", 97, 3, Region.mines_floor_20,
+ dwarf_scroll_ii = create_artifact("Dwarf Scroll II", 3, Region.mines_floor_20,
monsters=unlikely)
- dwarf_scroll_iii = create_artifact("Dwarf Scroll III", 98, 7.5, Region.mines_floor_60,
+ dwarf_scroll_iii = create_artifact("Dwarf Scroll III", 7.5, Region.mines_floor_60,
monsters=Monster.blue_slime)
- dwarf_scroll_iv = create_artifact("Dwarf Scroll IV", 99, 4, Region.mines_floor_100)
- chipped_amphora = create_artifact("Chipped Amphora", 100, 6.7, Region.town,
+ dwarf_scroll_iv = create_artifact("Dwarf Scroll IV", 4, Region.mines_floor_100)
+ chipped_amphora = create_artifact("Chipped Amphora", 6.7, Region.town,
geodes=Geode.artifact_trove)
- arrowhead = create_artifact("Arrowhead", 101, 8.5, (Region.mountain, Region.forest, Region.bus_stop),
+ arrowhead = create_artifact("Arrowhead", 8.5, (Region.mountain, Region.forest, Region.bus_stop),
geodes=Geode.artifact_trove)
- ancient_doll = create_artifact("Ancient Doll", 103, 13.1, (Region.mountain, Region.forest, Region.bus_stop),
- geodes=(Geode.artifact_trove, common.fishing_chest))
- elvish_jewelry = create_artifact("Elvish Jewelry", 104, 5.3, Region.forest,
- geodes=(Geode.artifact_trove, common.fishing_chest))
- chewing_stick = create_artifact("Chewing Stick", 105, 10.3, (Region.mountain, Region.forest, Region.town),
- geodes=(Geode.artifact_trove, common.fishing_chest))
- ornamental_fan = create_artifact("Ornamental Fan", 106, 7.4, (Region.beach, Region.forest, Region.town),
- geodes=(Geode.artifact_trove, common.fishing_chest))
- dinosaur_egg = create_artifact("Dinosaur Egg", 107, 11.4, (Region.mountain, Region.skull_cavern),
- geodes=common.fishing_chest,
+ ancient_doll = create_artifact(Artifact.ancient_doll, 13.1, (Region.mountain, Region.forest, Region.bus_stop),
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
+ elvish_jewelry = create_artifact("Elvish Jewelry", 5.3, Region.forest,
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
+ chewing_stick = create_artifact("Chewing Stick", 10.3, (Region.mountain, Region.forest, Region.town),
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
+ ornamental_fan = create_artifact("Ornamental Fan", 7.4, (Region.beach, Region.forest, Region.town),
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
+ dinosaur_egg = create_artifact("Dinosaur Egg", 11.4, (Region.skull_cavern),
monsters=Monster.pepper_rex)
- rare_disc = create_artifact("Rare Disc", 108, 5.6, Region.stardew_valley,
- geodes=(Geode.artifact_trove, common.fishing_chest),
+ rare_disc = create_artifact("Rare Disc", 5.6, Region.stardew_valley,
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest),
monsters=unlikely)
- ancient_sword = create_artifact("Ancient Sword", 109, 5.8, (Region.forest, Region.mountain),
- geodes=(Geode.artifact_trove, common.fishing_chest))
- rusty_spoon = create_artifact("Rusty Spoon", 110, 9.6, Region.town,
- geodes=(Geode.artifact_trove, common.fishing_chest))
- rusty_spur = create_artifact("Rusty Spur", 111, 15.6, Region.farm,
- geodes=(Geode.artifact_trove, common.fishing_chest))
- rusty_cog = create_artifact("Rusty Cog", 112, 9.6, Region.mountain,
- geodes=(Geode.artifact_trove, common.fishing_chest))
- chicken_statue = create_artifact("Chicken Statue", 113, 13.5, Region.farm,
- geodes=(Geode.artifact_trove, common.fishing_chest))
- ancient_seed = create_artifact("Ancient Seed", 114, 8.4, (Region.forest, Region.mountain),
- geodes=(Geode.artifact_trove, common.fishing_chest),
+ ancient_sword = create_artifact("Ancient Sword", 5.8, (Region.forest, Region.mountain),
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
+ rusty_spoon = create_artifact("Rusty Spoon", 9.6, Region.town,
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
+ rusty_spur = create_artifact("Rusty Spur", 15.6, Region.farm,
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
+ rusty_cog = create_artifact("Rusty Cog", 9.6, Region.mountain,
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
+ chicken_statue = create_artifact("Chicken Statue", 13.5, Region.farm,
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
+ ancient_seed = create_artifact("Ancient Seed", 8.4, (Region.forest, Region.mountain),
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest),
monsters=unlikely)
- prehistoric_tool = create_artifact("Prehistoric Tool", 115, 11.1, (Region.mountain, Region.forest, Region.bus_stop),
- geodes=(Geode.artifact_trove, common.fishing_chest))
- dried_starfish = create_artifact("Dried Starfish", 116, 12.5, Region.beach,
- geodes=(Geode.artifact_trove, common.fishing_chest))
- anchor = create_artifact("Anchor", 117, 8.5, Region.beach, geodes=(Geode.artifact_trove, common.fishing_chest))
- glass_shards = create_artifact("Glass Shards", 118, 11.5, Region.beach,
- geodes=(Geode.artifact_trove, common.fishing_chest))
- bone_flute = create_artifact("Bone Flute", 119, 6.3, (Region.mountain, Region.forest, Region.town),
- geodes=(Geode.artifact_trove, common.fishing_chest))
- prehistoric_handaxe = create_artifact("Prehistoric Handaxe", 120, 13.7,
+ prehistoric_tool = create_artifact("Prehistoric Tool", 11.1, (Region.mountain, Region.forest, Region.bus_stop),
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
+ dried_starfish = create_artifact("Dried Starfish", 12.5, Region.beach,
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
+ anchor = create_artifact("Anchor", 8.5, Region.beach, geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
+ glass_shards = create_artifact("Glass Shards", 11.5, Region.beach,
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
+ bone_flute = create_artifact("Bone Flute", 6.3, (Region.mountain, Region.forest, Region.town),
+ geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
+ prehistoric_handaxe = create_artifact(Artifact.prehistoric_handaxe, 13.7,
(Region.mountain, Region.forest, Region.bus_stop),
geodes=Geode.artifact_trove)
- dwarvish_helm = create_artifact("Dwarvish Helm", 121, 8.7, Region.mines_floor_20,
+ dwarvish_helm = create_artifact("Dwarvish Helm", 8.7, Region.mines_floor_20,
geodes=(Geode.geode, Geode.omni, Geode.artifact_trove))
- dwarf_gadget = create_artifact("Dwarf Gadget", 122, 9.7, Region.mines_floor_60,
+ dwarf_gadget = create_artifact("Dwarf Gadget", 9.7, Region.mines_floor_60,
geodes=(Geode.magma, Geode.omni, Geode.artifact_trove))
- ancient_drum = create_artifact("Ancient Drum", 123, 9.5, (Region.bus_stop, Region.forest, Region.town),
+ ancient_drum = create_artifact("Ancient Drum", 9.5, (Region.bus_stop, Region.forest, Region.town),
geodes=(Geode.frozen, Geode.omni, Geode.artifact_trove))
- golden_mask = create_artifact("Golden Mask", 124, 6.7, Region.desert,
+ golden_mask = create_artifact("Golden Mask", 6.7, Region.desert,
geodes=Geode.artifact_trove)
- golden_relic = create_artifact("Golden Relic", 125, 9.7, Region.desert,
+ golden_relic = create_artifact("Golden Relic", 9.7, Region.desert,
geodes=Geode.artifact_trove)
- strange_doll_green = create_artifact("Strange Doll (Green)", 126, 10, Region.town,
- geodes=common.secret_note)
- strange_doll = create_artifact("Strange Doll", 127, 10, Region.desert,
- geodes=common.secret_note)
- prehistoric_scapula = create_artifact("Prehistoric Scapula", 579, 6.2,
+ strange_doll_green = create_artifact("Strange Doll (Green)", 10, Region.town,
+ geodes=Forageable.secret_note)
+ strange_doll = create_artifact("Strange Doll", 10, Region.desert,
+ geodes=Forageable.secret_note)
+ prehistoric_scapula = create_artifact("Prehistoric Scapula", 6.2,
(Region.dig_site, Region.forest, Region.town))
- prehistoric_tibia = create_artifact("Prehistoric Tibia", 580, 16.6,
+ prehistoric_tibia = create_artifact("Prehistoric Tibia", 16.6,
(Region.dig_site, Region.forest, Region.railroad))
- prehistoric_skull = create_artifact("Prehistoric Skull", 581, 3.9, (Region.dig_site, Region.mountain))
- skeletal_hand = create_artifact("Skeletal Hand", 582, 7.9, (Region.dig_site, Region.backwoods, Region.beach))
- prehistoric_rib = create_artifact("Prehistoric Rib", 583, 15, (Region.dig_site, Region.farm, Region.town),
+ prehistoric_skull = create_artifact("Prehistoric Skull", 3.9, (Region.dig_site, Region.mountain))
+ skeletal_hand = create_artifact(Fossil.skeletal_hand, 7.9, (Region.dig_site, Region.backwoods, Region.beach))
+ prehistoric_rib = create_artifact("Prehistoric Rib", 15, (Region.dig_site, Region.farm, Region.town),
monsters=Monster.pepper_rex)
- prehistoric_vertebra = create_artifact("Prehistoric Vertebra", 584, 12.7, (Region.dig_site, Region.bus_stop),
+ prehistoric_vertebra = create_artifact("Prehistoric Vertebra", 12.7, (Region.dig_site, Region.bus_stop),
monsters=Monster.pepper_rex)
- skeletal_tail = create_artifact("Skeletal Tail", 585, 5.1, (Region.dig_site, Region.mines_floor_20),
- geodes=common.fishing_chest)
- nautilus_fossil = create_artifact("Nautilus Fossil", 586, 6.9, (Region.dig_site, Region.beach),
- geodes=common.fishing_chest)
- amphibian_fossil = create_artifact("Amphibian Fossil", 587, 6.3, (Region.dig_site, Region.forest, Region.mountain),
- geodes=common.fishing_chest)
- palm_fossil = create_artifact("Palm Fossil", 588, 10.2,
+ skeletal_tail = create_artifact("Skeletal Tail", 5.1, (Region.dig_site, Region.mines_floor_20),
+ geodes=WaterChest.fishing_chest)
+ nautilus_fossil = create_artifact("Nautilus Fossil", 6.9, (Region.dig_site, Region.beach),
+ geodes=WaterChest.fishing_chest)
+ amphibian_fossil = create_artifact("Amphibian Fossil", 6.3, (Region.dig_site, Region.forest, Region.mountain),
+ geodes=WaterChest.fishing_chest)
+ palm_fossil = create_artifact("Palm Fossil", 10.2,
(Region.dig_site, Region.desert, Region.forest, Region.beach))
- trilobite = create_artifact("Trilobite", 589, 7.4, (Region.dig_site, Region.desert, Region.forest, Region.beach))
+ trilobite = create_artifact("Trilobite", 7.4, (Region.dig_site, Region.desert, Region.forest, Region.beach))
class Mineral:
- quartz = create_mineral("Quartz", 80, Region.mines_floor_20,
- monsters=Monster.stone_golem)
- fire_quartz = create_mineral("Fire Quartz", 82, Region.mines_floor_100,
- geodes=(Geode.magma, Geode.omni, common.fishing_chest),
- difficulty=1.0 / 12.0)
- frozen_tear = create_mineral("Frozen Tear", 84, Region.mines_floor_60,
- geodes=(Geode.frozen, Geode.omni, common.fishing_chest),
+ quartz = create_mineral(Mineral.quartz, Region.mines_floor_20, difficulty=100.0 / 5.0)
+ fire_quartz = create_mineral("Fire Quartz", Region.mines_floor_100,
+ geodes=(Geode.magma, Geode.omni, WaterChest.fishing_chest),
+ difficulty=100.0 / 5.0)
+ frozen_tear = create_mineral("Frozen Tear", Region.mines_floor_60,
+ geodes=(Geode.frozen, Geode.omni, WaterChest.fishing_chest),
monsters=unlikely,
- difficulty=1.0 / 12.0)
- earth_crystal = create_mineral("Earth Crystal", 86, Region.mines_floor_20,
- geodes=(Geode.geode, Geode.omni, common.fishing_chest),
+ difficulty=100.0 / 5.0)
+ earth_crystal = create_mineral("Earth Crystal", Region.mines_floor_20,
+ geodes=(Geode.geode, Geode.omni, WaterChest.fishing_chest),
monsters=Monster.duggy,
- difficulty=1.0 / 12.0)
- emerald = create_mineral("Emerald", 60, Region.mines_floor_100,
- geodes=common.fishing_chest)
- aquamarine = create_mineral("Aquamarine", 62, Region.mines_floor_60,
- geodes=common.fishing_chest)
- ruby = create_mineral("Ruby", 64, Region.mines_floor_100,
- geodes=common.fishing_chest)
- amethyst = create_mineral("Amethyst", 66, Region.mines_floor_20,
- geodes=common.fishing_chest)
- topaz = create_mineral("Topaz", 68, Region.mines_floor_20,
- geodes=common.fishing_chest)
- jade = create_mineral("Jade", 70, Region.mines_floor_60,
- geodes=common.fishing_chest)
- diamond = create_mineral("Diamond", 72, Region.mines_floor_60,
- geodes=common.fishing_chest)
- prismatic_shard = create_mineral("Prismatic Shard", 74, Region.skull_cavern_100,
+ difficulty=100.0 / 5.0)
+ emerald = create_mineral("Emerald", Region.mines_floor_100,
+ geodes=WaterChest.fishing_chest)
+ aquamarine = create_mineral("Aquamarine", Region.mines_floor_60,
+ geodes=WaterChest.fishing_chest)
+ ruby = create_mineral("Ruby", Region.mines_floor_100,
+ geodes=WaterChest.fishing_chest)
+ amethyst = create_mineral("Amethyst", Region.mines_floor_20,
+ geodes=WaterChest.fishing_chest)
+ topaz = create_mineral("Topaz", Region.mines_floor_20,
+ geodes=WaterChest.fishing_chest)
+ jade = create_mineral("Jade", Region.mines_floor_60,
+ geodes=WaterChest.fishing_chest)
+ diamond = create_mineral("Diamond", Region.mines_floor_60,
+ geodes=WaterChest.fishing_chest)
+ prismatic_shard = create_mineral("Prismatic Shard", Region.skull_cavern_100,
geodes=unlikely,
monsters=unlikely)
- alamite = create_mineral("Alamite", 538, Region.town,
+ alamite = create_mineral("Alamite",
geodes=(Geode.geode, Geode.omni))
- bixite = create_mineral("Bixite", 539, Region.town,
+ bixite = create_mineral("Bixite",
geodes=(Geode.magma, Geode.omni),
monsters=unlikely)
- baryte = create_mineral("Baryte", 540, Region.town,
+ baryte = create_mineral("Baryte",
geodes=(Geode.magma, Geode.omni))
- aerinite = create_mineral("Aerinite", 541, Region.town,
+ aerinite = create_mineral("Aerinite",
geodes=(Geode.frozen, Geode.omni))
- calcite = create_mineral("Calcite", 542, Region.town,
+ calcite = create_mineral("Calcite",
geodes=(Geode.geode, Geode.omni))
- dolomite = create_mineral("Dolomite", 543, Region.town,
+ dolomite = create_mineral("Dolomite",
geodes=(Geode.magma, Geode.omni))
- esperite = create_mineral("Esperite", 544, Region.town,
+ esperite = create_mineral("Esperite",
geodes=(Geode.frozen, Geode.omni))
- fluorapatite = create_mineral("Fluorapatite", 545, Region.town,
+ fluorapatite = create_mineral("Fluorapatite",
geodes=(Geode.frozen, Geode.omni))
- geminite = create_mineral("Geminite", 546, Region.town,
+ geminite = create_mineral("Geminite",
geodes=(Geode.frozen, Geode.omni))
- helvite = create_mineral("Helvite", 547, Region.town,
+ helvite = create_mineral("Helvite",
geodes=(Geode.magma, Geode.omni))
- jamborite = create_mineral("Jamborite", 548, Region.town,
+ jamborite = create_mineral("Jamborite",
geodes=(Geode.geode, Geode.omni))
- jagoite = create_mineral("Jagoite", 549, Region.town,
+ jagoite = create_mineral("Jagoite",
geodes=(Geode.geode, Geode.omni))
- kyanite = create_mineral("Kyanite", 550, Region.town,
+ kyanite = create_mineral("Kyanite",
geodes=(Geode.frozen, Geode.omni))
- lunarite = create_mineral("Lunarite", 551, Region.town,
+ lunarite = create_mineral("Lunarite",
geodes=(Geode.frozen, Geode.omni))
- malachite = create_mineral("Malachite", 552, Region.town,
+ malachite = create_mineral("Malachite",
geodes=(Geode.geode, Geode.omni))
- neptunite = create_mineral("Neptunite", 553, Region.town,
+ neptunite = create_mineral("Neptunite",
geodes=(Geode.magma, Geode.omni))
- lemon_stone = create_mineral("Lemon Stone", 554, Region.town,
+ lemon_stone = create_mineral("Lemon Stone",
geodes=(Geode.magma, Geode.omni))
- nekoite = create_mineral("Nekoite", 555, Region.town,
+ nekoite = create_mineral("Nekoite",
geodes=(Geode.geode, Geode.omni))
- orpiment = create_mineral("Orpiment", 556, Region.town,
+ orpiment = create_mineral("Orpiment",
geodes=(Geode.geode, Geode.omni))
- petrified_slime = create_mineral("Petrified Slime", 557, Region.town,
- geodes=(Geode.geode, Geode.omni))
- thunder_egg = create_mineral("Thunder Egg", 558, Region.town,
+ petrified_slime = create_mineral(Mineral.petrified_slime, Region.slime_hutch)
+ thunder_egg = create_mineral("Thunder Egg",
geodes=(Geode.geode, Geode.omni))
- pyrite = create_mineral("Pyrite", 559, Region.town,
+ pyrite = create_mineral("Pyrite",
geodes=(Geode.frozen, Geode.omni))
- ocean_stone = create_mineral("Ocean Stone", 560, Region.town,
+ ocean_stone = create_mineral("Ocean Stone",
geodes=(Geode.frozen, Geode.omni))
- ghost_crystal = create_mineral("Ghost Crystal", 561, Region.town,
+ ghost_crystal = create_mineral("Ghost Crystal",
geodes=(Geode.frozen, Geode.omni))
- tigerseye = create_mineral("Tigerseye", 562, Region.town,
+ tigerseye = create_mineral("Tigerseye",
geodes=(Geode.magma, Geode.omni))
- jasper = create_mineral("Jasper", 563, Region.town,
+ jasper = create_mineral("Jasper",
geodes=(Geode.magma, Geode.omni))
- opal = create_mineral("Opal", 564, Region.town,
+ opal = create_mineral("Opal",
geodes=(Geode.frozen, Geode.omni))
- fire_opal = create_mineral("Fire Opal", 565, Region.town,
+ fire_opal = create_mineral("Fire Opal",
geodes=(Geode.magma, Geode.omni))
- celestine = create_mineral("Celestine", 566, Region.town,
+ celestine = create_mineral("Celestine",
geodes=(Geode.geode, Geode.omni))
- marble = create_mineral("Marble", 567, Region.town,
+ marble = create_mineral("Marble",
geodes=(Geode.frozen, Geode.omni))
- sandstone = create_mineral("Sandstone", 568, Region.town,
+ sandstone = create_mineral("Sandstone",
geodes=(Geode.geode, Geode.omni))
- granite = create_mineral("Granite", 569, Region.town,
+ granite = create_mineral("Granite",
geodes=(Geode.geode, Geode.omni))
- basalt = create_mineral("Basalt", 570, Region.town,
+ basalt = create_mineral("Basalt",
geodes=(Geode.magma, Geode.omni))
- limestone = create_mineral("Limestone", 571, Region.town,
+ limestone = create_mineral("Limestone",
geodes=(Geode.geode, Geode.omni))
- soapstone = create_mineral("Soapstone", 572, Region.town,
+ soapstone = create_mineral("Soapstone",
geodes=(Geode.frozen, Geode.omni))
- hematite = create_mineral("Hematite", 573, Region.town,
+ hematite = create_mineral("Hematite",
geodes=(Geode.frozen, Geode.omni))
- mudstone = create_mineral("Mudstone", 574, Region.town,
+ mudstone = create_mineral("Mudstone",
geodes=(Geode.geode, Geode.omni))
- obsidian = create_mineral("Obsidian", 575, Region.town,
+ obsidian = create_mineral("Obsidian",
geodes=(Geode.magma, Geode.omni))
- slate = create_mineral("Slate", 576, Region.town,
- geodes=(Geode.geode, Geode.omni))
- fairy_stone = create_mineral("Fairy Stone", 577, Region.town,
- geodes=(Geode.frozen, Geode.omni))
- star_shards = create_mineral("Star Shards", 578, Region.town,
- geodes=(Geode.magma, Geode.omni))
+ slate = create_mineral("Slate", geodes=(Geode.geode, Geode.omni))
+ fairy_stone = create_mineral("Fairy Stone", geodes=(Geode.frozen, Geode.omni))
+ star_shards = create_mineral("Star Shards", geodes=(Geode.magma, Geode.omni))
dwarf_scrolls = (Artifact.dwarf_scroll_i, Artifact.dwarf_scroll_ii, Artifact.dwarf_scroll_iii, Artifact.dwarf_scroll_iv)
@@ -291,4 +286,4 @@ class Mineral:
skeleton_middle = (Artifact.prehistoric_rib, Artifact.prehistoric_vertebra)
skeleton_back = (Artifact.prehistoric_tibia, Artifact.skeletal_tail)
-all_museum_items_by_name = {item.name: item for item in all_museum_items}
+all_museum_items_by_name = {item.item_name: item for item in all_museum_items}
diff --git a/worlds/stardew_valley/data/recipe_data.py b/worlds/stardew_valley/data/recipe_data.py
index dc1b490bf93c..3123bb924307 100644
--- a/worlds/stardew_valley/data/recipe_data.py
+++ b/worlds/stardew_valley/data/recipe_data.py
@@ -1,78 +1,36 @@
-from typing import Dict, List
-
+from typing import Dict, List, Optional
+from ..mods.mod_data import ModNames
+from .recipe_source import RecipeSource, FriendshipSource, SkillSource, QueenOfSauceSource, ShopSource, StarterSource, ShopTradeSource, ShopFriendshipSource
from ..strings.animal_product_names import AnimalProduct
from ..strings.artisan_good_names import ArtisanGood
-from ..strings.crop_names import Fruit, Vegetable
-from ..strings.fish_names import Fish, WaterItem
+from ..strings.craftable_names import ModEdible, Edible
+from ..strings.crop_names import Fruit, Vegetable, SVEFruit, DistantLandsCrop
+from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish, SVEWaterItem
from ..strings.flower_names import Flower
-from ..strings.forageable_names import Forageable
+from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom
from ..strings.ingredient_names import Ingredient
-from ..strings.food_names import Meal, Beverage
-from ..strings.region_names import Region
+from ..strings.food_names import Meal, SVEMeal, Beverage, DistantLandsMeal, BoardingHouseMeal, ArchaeologyMeal, TrashyMeal
+from ..strings.material_names import Material
+from ..strings.metal_names import Fossil, Artifact
+from ..strings.monster_drop_names import Loot
+from ..strings.region_names import Region, SVERegion
from ..strings.season_names import Season
-from ..strings.skill_names import Skill
-from ..strings.villager_names import NPC
-
-
-class RecipeSource:
- pass
-
-
-class StarterSource(RecipeSource):
- pass
-
-
-class QueenOfSauceSource(RecipeSource):
- year: int
- season: str
- day: int
-
- def __init__(self, year: int, season: str, day: int):
- self.year = year
- self.season = season
- self.day = day
-
-
-class FriendshipSource(RecipeSource):
- friend: str
- hearts: int
-
- def __init__(self, friend: str, hearts: int):
- self.friend = friend
- self.hearts = hearts
-
-
-class SkillSource(RecipeSource):
- skill: str
- level: int
-
- def __init__(self, skill: str, level: int):
- self.skill = skill
- self.level = level
-
-
-class ShopSource(RecipeSource):
- region: str
- price: int
-
- def __init__(self, region: str, price: int):
- self.region = region
- self.price = price
-
-
-class ShopTradeSource(ShopSource):
- currency: str
+from ..strings.seed_names import Seed
+from ..strings.skill_names import Skill, ModSkill
+from ..strings.villager_names import NPC, ModNPC
class CookingRecipe:
meal: str
ingredients: Dict[str, int]
source: RecipeSource
+ mod_name: Optional[str] = None
- def __init__(self, meal: str, ingredients: Dict[str, int], source: RecipeSource):
+ def __init__(self, meal: str, ingredients: Dict[str, int], source: RecipeSource, mod_name: Optional[str] = None):
self.meal = meal
self.ingredients = ingredients
self.source = source
+ self.mod_name = mod_name
def __repr__(self):
return f"{self.meal} (Source: {self.source} |" \
@@ -82,18 +40,28 @@ def __repr__(self):
all_cooking_recipes: List[CookingRecipe] = []
-def friendship_recipe(name: str, friend: str, hearts: int, ingredients: Dict[str, int]) -> CookingRecipe:
+def friendship_recipe(name: str, friend: str, hearts: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CookingRecipe:
source = FriendshipSource(friend, hearts)
- return create_recipe(name, ingredients, source)
+ return create_recipe(name, ingredients, source, mod_name)
-def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int]) -> CookingRecipe:
+def friendship_and_shop_recipe(name: str, friend: str, hearts: int, region: str, price: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CookingRecipe:
+ source = ShopFriendshipSource(friend, hearts, region, price)
+ return create_recipe(name, ingredients, source, mod_name)
+
+
+def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CookingRecipe:
source = SkillSource(skill, level)
- return create_recipe(name, ingredients, source)
+ return create_recipe(name, ingredients, source, mod_name)
-def shop_recipe(name: str, region: str, price: int, ingredients: Dict[str, int]) -> CookingRecipe:
+def shop_recipe(name: str, region: str, price: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CookingRecipe:
source = ShopSource(region, price)
+ return create_recipe(name, ingredients, source, mod_name)
+
+
+def shop_trade_recipe(name: str, region: str, currency: str, price: int, ingredients: Dict[str, int]) -> CookingRecipe:
+ source = ShopTradeSource(region, currency, price)
return create_recipe(name, ingredients, source)
@@ -107,47 +75,63 @@ def starter_recipe(name: str, ingredients: Dict[str, int]) -> CookingRecipe:
return create_recipe(name, ingredients, source)
-def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource) -> CookingRecipe:
- recipe = CookingRecipe(name, ingredients, source)
+def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, mod_name: Optional[str] = None) -> CookingRecipe:
+ recipe = CookingRecipe(name, ingredients, source, mod_name)
all_cooking_recipes.append(recipe)
return recipe
algae_soup = friendship_recipe(Meal.algae_soup, NPC.clint, 3, {WaterItem.green_algae: 4})
artichoke_dip = queen_of_sauce_recipe(Meal.artichoke_dip, 1, Season.fall, 28, {Vegetable.artichoke: 1, AnimalProduct.cow_milk: 1})
+autumn_bounty = friendship_recipe(Meal.autumn_bounty, NPC.demetrius, 7, {Vegetable.yam: 1, Vegetable.pumpkin: 1})
baked_fish = queen_of_sauce_recipe(Meal.baked_fish, 1, Season.summer, 7, {Fish.sunfish: 1, Fish.bream: 1, Ingredient.wheat_flour: 1})
+banana_pudding = shop_trade_recipe(Meal.banana_pudding, Region.island_trader, Fossil.bone_fragment, 30, {Fruit.banana: 1, AnimalProduct.cow_milk: 1, Ingredient.sugar: 1})
bean_hotpot = friendship_recipe(Meal.bean_hotpot, NPC.clint, 7, {Vegetable.green_bean: 2})
blackberry_cobbler_ingredients = {Forageable.blackberry: 2, Ingredient.sugar: 1, Ingredient.wheat_flour: 1}
blackberry_cobbler_qos = queen_of_sauce_recipe(Meal.blackberry_cobbler, 2, Season.fall, 14, blackberry_cobbler_ingredients)
-blueberry_tart = friendship_recipe(Meal.blueberry_tart, NPC.pierre, 3, {Fruit.blueberry: 1, Ingredient.wheat_flour: 1, Ingredient.sugar: 1, AnimalProduct.any_egg: 1})
+blueberry_tart_ingredients = {Fruit.blueberry: 1, Ingredient.wheat_flour: 1, Ingredient.sugar: 1, AnimalProduct.any_egg: 1}
+blueberry_tart = friendship_recipe(Meal.blueberry_tart, NPC.pierre, 3, blueberry_tart_ingredients)
bread = queen_of_sauce_recipe(Meal.bread, 1, Season.summer, 28, {Ingredient.wheat_flour: 1})
+bruschetta = queen_of_sauce_recipe(Meal.bruschetta, 2, Season.winter, 21, {Meal.bread: 1, Ingredient.oil: 1, Vegetable.tomato: 1})
+carp_surprise = queen_of_sauce_recipe(Meal.carp_surprise, 2, Season.summer, 7, {Fish.carp: 4})
cheese_cauliflower = friendship_recipe(Meal.cheese_cauliflower, NPC.pam, 3, {Vegetable.cauliflower: 1, ArtisanGood.cheese: 1})
chocolate_cake_ingredients = {Ingredient.wheat_flour: 1, Ingredient.sugar: 1, AnimalProduct.chicken_egg: 1}
chocolate_cake_qos = queen_of_sauce_recipe(Meal.chocolate_cake, 1, Season.winter, 14, chocolate_cake_ingredients)
-chowder = friendship_recipe(Meal.chowder, NPC.willy, 3, {WaterItem.clam: 1, AnimalProduct.cow_milk: 1})
-complete_breakfast = queen_of_sauce_recipe(Meal.complete_breakfast, 2, Season.spring, 21, {Meal.fried_egg: 1, AnimalProduct.milk: 1, Meal.hashbrowns: 1, Meal.pancakes: 1})
+chowder = friendship_recipe(Meal.chowder, NPC.willy, 3, {Fish.clam: 1, AnimalProduct.cow_milk: 1})
+coleslaw = queen_of_sauce_recipe(Meal.coleslaw, 14, Season.spring, 14, {Vegetable.red_cabbage: 1, Ingredient.vinegar: 1, ArtisanGood.mayonnaise: 1})
+complete_breakfast_ingredients = {Meal.fried_egg: 1, AnimalProduct.milk: 1, Meal.hashbrowns: 1, Meal.pancakes: 1}
+complete_breakfast = queen_of_sauce_recipe(Meal.complete_breakfast, 2, Season.spring, 21, complete_breakfast_ingredients)
+cookie = friendship_recipe(Meal.cookie, NPC.evelyn, 4, {Ingredient.wheat_flour: 1, Ingredient.sugar: 1, AnimalProduct.chicken_egg: 1})
crab_cakes_ingredients = {Fish.crab: 1, Ingredient.wheat_flour: 1, AnimalProduct.chicken_egg: 1, Ingredient.oil: 1}
crab_cakes_qos = queen_of_sauce_recipe(Meal.crab_cakes, 2, Season.fall, 21, crab_cakes_ingredients)
cranberry_candy = queen_of_sauce_recipe(Meal.cranberry_candy, 1, Season.winter, 28, {Fruit.cranberries: 1, Fruit.apple: 1, Ingredient.sugar: 1})
+cranberry_sauce = friendship_recipe(Meal.cranberry_sauce, NPC.gus, 7, {Fruit.cranberries: 1, Ingredient.sugar: 1})
crispy_bass = friendship_recipe(Meal.crispy_bass, NPC.kent, 3, {Fish.largemouth_bass: 1, Ingredient.wheat_flour: 1, Ingredient.oil: 1})
dish_o_the_sea = skill_recipe(Meal.dish_o_the_sea, Skill.fishing, 3, {Fish.sardine: 2, Meal.hashbrowns: 1})
eggplant_parmesan = friendship_recipe(Meal.eggplant_parmesan, NPC.lewis, 7, {Vegetable.eggplant: 1, Vegetable.tomato: 1})
escargot = friendship_recipe(Meal.escargot, NPC.willy, 5, {Fish.snail: 1, Vegetable.garlic: 1})
farmer_lunch = skill_recipe(Meal.farmer_lunch, Skill.farming, 3, {Meal.omelet: 2, Vegetable.parsnip: 1})
fiddlehead_risotto = queen_of_sauce_recipe(Meal.fiddlehead_risotto, 2, Season.fall, 28, {Ingredient.oil: 1, Forageable.fiddlehead_fern: 1, Vegetable.garlic: 1})
+fish_stew = friendship_recipe(Meal.fish_stew, NPC.willy, 7, {Fish.crayfish: 1, Fish.mussel: 1, Fish.periwinkle: 1, Vegetable.tomato: 1})
fish_taco = friendship_recipe(Meal.fish_taco, NPC.linus, 7, {Fish.tuna: 1, Meal.tortilla: 1, Vegetable.red_cabbage: 1, ArtisanGood.mayonnaise: 1})
fried_calamari = friendship_recipe(Meal.fried_calamari, NPC.jodi, 3, {Fish.squid: 1, Ingredient.wheat_flour: 1, Ingredient.oil: 1})
fried_eel = friendship_recipe(Meal.fried_eel, NPC.george, 3, {Fish.eel: 1, Ingredient.oil: 1})
fried_egg = starter_recipe(Meal.fried_egg, {AnimalProduct.chicken_egg: 1})
-fried_mushroom = friendship_recipe(Meal.fried_mushroom, NPC.demetrius, 3, {Forageable.common_mushroom: 1, Forageable.morel: 1, Ingredient.oil: 1})
+fried_mushroom = friendship_recipe(Meal.fried_mushroom, NPC.demetrius, 3, {Mushroom.common: 1, Mushroom.morel: 1, Ingredient.oil: 1})
fruit_salad = queen_of_sauce_recipe(Meal.fruit_salad, 2, Season.fall, 7, {Fruit.blueberry: 1, Fruit.melon: 1, Fruit.apricot: 1})
ginger_ale = shop_recipe(Beverage.ginger_ale, Region.volcano_dwarf_shop, 1000, {Forageable.ginger: 3, Ingredient.sugar: 1})
glazed_yams = queen_of_sauce_recipe(Meal.glazed_yams, 1, Season.fall, 21, {Vegetable.yam: 1, Ingredient.sugar: 1})
hashbrowns = queen_of_sauce_recipe(Meal.hashbrowns, 2, Season.spring, 14, {Vegetable.potato: 1, Ingredient.oil: 1})
ice_cream = friendship_recipe(Meal.ice_cream, NPC.jodi, 7, {AnimalProduct.cow_milk: 1, Ingredient.sugar: 1})
+lobster_bisque_ingredients = {Fish.lobster: 1, AnimalProduct.cow_milk: 1}
+lobster_bisque_friend = friendship_recipe(Meal.lobster_bisque, NPC.willy, 9, lobster_bisque_ingredients)
+lobster_bisque_qos = queen_of_sauce_recipe(Meal.lobster_bisque, 2, Season.winter, 14, lobster_bisque_ingredients)
+lucky_lunch = queen_of_sauce_recipe(Meal.lucky_lunch, 2, Season.spring, 28, {Fish.sea_cucumber: 1, Meal.tortilla: 1, Flower.blue_jazz: 1})
maki_roll = queen_of_sauce_recipe(Meal.maki_roll, 1, Season.summer, 21, {Fish.any: 1, WaterItem.seaweed: 1, Ingredient.rice: 1})
+mango_sticky_rice = friendship_recipe(Meal.mango_sticky_rice, NPC.leo, 7, {Fruit.mango: 1, Forageable.coconut: 1, Ingredient.rice: 1})
maple_bar = queen_of_sauce_recipe(Meal.maple_bar, 2, Season.summer, 14, {ArtisanGood.maple_syrup: 1, Ingredient.sugar: 1, Ingredient.wheat_flour: 1})
miners_treat = skill_recipe(Meal.miners_treat, Skill.mining, 3, {Forageable.cave_carrot: 2, Ingredient.sugar: 1, AnimalProduct.cow_milk: 1})
+moss_soup = skill_recipe(Meal.moss_soup, Skill.foraging, 3, {Material.moss: 20})
omelet = queen_of_sauce_recipe(Meal.omelet, 1, Season.spring, 28, {AnimalProduct.chicken_egg: 1, AnimalProduct.cow_milk: 1})
pale_broth = friendship_recipe(Meal.pale_broth, NPC.marnie, 3, {WaterItem.white_algae: 2})
pancakes = queen_of_sauce_recipe(Meal.pancakes, 1, Season.summer, 14, {Ingredient.wheat_flour: 1, AnimalProduct.chicken_egg: 1})
@@ -159,9 +143,12 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource)
pizza_qos = queen_of_sauce_recipe(Meal.pizza, 2, Season.spring, 7, pizza_ingredients)
pizza_saloon = shop_recipe(Meal.pizza, Region.saloon, 150, pizza_ingredients)
plum_pudding = queen_of_sauce_recipe(Meal.plum_pudding, 1, Season.winter, 7, {Forageable.wild_plum: 2, Ingredient.wheat_flour: 1, Ingredient.sugar: 1})
+poi = friendship_recipe(Meal.poi, NPC.leo, 3, {Vegetable.taro_root: 4})
poppyseed_muffin = queen_of_sauce_recipe(Meal.poppyseed_muffin, 2, Season.winter, 7, {Flower.poppy: 1, Ingredient.wheat_flour: 1, Ingredient.sugar: 1})
pumpkin_pie_ingredients = {Vegetable.pumpkin: 1, Ingredient.wheat_flour: 1, Ingredient.sugar: 1, AnimalProduct.cow_milk: 1}
pumpkin_pie_qos = queen_of_sauce_recipe(Meal.pumpkin_pie, 1, Season.winter, 21, pumpkin_pie_ingredients)
+pumpkin_soup = friendship_recipe(Meal.pumpkin_soup, NPC.robin, 7, {Vegetable.pumpkin: 1, AnimalProduct.cow_milk: 1})
+radish_salad = queen_of_sauce_recipe(Meal.radish_salad, 1, Season.spring, 21, {Ingredient.oil: 1, Ingredient.vinegar: 1, Vegetable.radish: 1})
red_plate = friendship_recipe(Meal.red_plate, NPC.emily, 7, {Vegetable.red_cabbage: 1, Vegetable.radish: 1})
rhubarb_pie = friendship_recipe(Meal.rhubarb_pie, NPC.marnie, 7, {Fruit.rhubarb: 1, Ingredient.wheat_flour: 1, Ingredient.sugar: 1})
rice_pudding = friendship_recipe(Meal.rice_pudding, NPC.evelyn, 7, {AnimalProduct.milk: 1, Ingredient.sugar: 1, Ingredient.rice: 1})
@@ -170,21 +157,65 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource)
salad = friendship_recipe(Meal.salad, NPC.emily, 3, {Forageable.leek: 1, Forageable.dandelion: 1, Ingredient.vinegar: 1})
salmon_dinner = friendship_recipe(Meal.salmon_dinner, NPC.gus, 3, {Fish.salmon: 1, Vegetable.amaranth: 1, Vegetable.kale: 1})
sashimi = friendship_recipe(Meal.sashimi, NPC.linus, 3, {Fish.any: 1})
+seafoam_pudding = skill_recipe(Meal.seafoam_pudding, Skill.fishing, 9, {Fish.flounder: 1, Fish.midnight_carp: 1, AnimalProduct.squid_ink: 1})
+shrimp_cocktail = queen_of_sauce_recipe(Meal.shrimp_cocktail, 2, Season.winter, 28, {Vegetable.tomato: 1, Fish.shrimp: 1, Forageable.wild_horseradish: 1})
spaghetti = friendship_recipe(Meal.spaghetti, NPC.lewis, 3, {Vegetable.tomato: 1, Ingredient.wheat_flour: 1})
-stir_fry_ingredients = {Forageable.cave_carrot: 1, Forageable.common_mushroom: 1, Vegetable.kale: 1, Ingredient.sugar: 1}
+spicy_eel = friendship_recipe(Meal.spicy_eel, NPC.george, 7, {Fish.eel: 1, Fruit.hot_pepper: 1})
+squid_ink_ravioli = skill_recipe(Meal.squid_ink_ravioli, Skill.combat, 9, {AnimalProduct.squid_ink: 1, Ingredient.wheat_flour: 1, Vegetable.tomato: 1})
+stir_fry_ingredients = {Forageable.cave_carrot: 1, Mushroom.common: 1, Vegetable.kale: 1, Ingredient.sugar: 1}
stir_fry_qos = queen_of_sauce_recipe(Meal.stir_fry, 1, Season.spring, 7, stir_fry_ingredients)
+strange_bun = friendship_recipe(Meal.strange_bun, NPC.shane, 7, {Ingredient.wheat_flour: 1, Fish.periwinkle: 1, ArtisanGood.void_mayonnaise: 1})
stuffing = friendship_recipe(Meal.stuffing, NPC.pam, 7, {Meal.bread: 1, Fruit.cranberries: 1, Forageable.hazelnut: 1})
-survival_burger = skill_recipe(Meal.survival_burger, Skill.foraging, 2, {Meal.bread: 1, Forageable.cave_carrot: 1, Vegetable.eggplant: 1})
+super_meal = friendship_recipe(Meal.super_meal, NPC.kent, 7, {Vegetable.bok_choy: 1, Fruit.cranberries: 1, Vegetable.artichoke: 1})
+
+survival_burger = skill_recipe(Meal.survival_burger, Skill.foraging, 8, {Meal.bread: 1, Forageable.cave_carrot: 1, Vegetable.eggplant: 1})
+tom_kha_soup = friendship_recipe(Meal.tom_kha_soup, NPC.sandy, 7, {Forageable.coconut: 1, Fish.shrimp: 1, Mushroom.common: 1})
tortilla_ingredients = {Vegetable.corn: 1}
tortilla_qos = queen_of_sauce_recipe(Meal.tortilla, 1, Season.fall, 7, tortilla_ingredients)
tortilla_saloon = shop_recipe(Meal.tortilla, Region.saloon, 100, tortilla_ingredients)
triple_shot_espresso = shop_recipe(Beverage.triple_shot_espresso, Region.saloon, 5000, {Beverage.coffee: 3})
tropical_curry = shop_recipe(Meal.tropical_curry, Region.island_resort, 2000, {Forageable.coconut: 1, Fruit.pineapple: 1, Fruit.hot_pepper: 1})
+trout_soup = queen_of_sauce_recipe(Meal.trout_soup, 1, Season.fall, 14, {Fish.rainbow_trout: 1, WaterItem.green_algae: 1})
vegetable_medley = friendship_recipe(Meal.vegetable_medley, NPC.caroline, 7, {Vegetable.tomato: 1, Vegetable.beet: 1})
-
-
-
-
-
-
+magic_elixir = shop_recipe(ModEdible.magic_elixir, Region.adventurer_guild, 3000, {Edible.life_elixir: 1, Mushroom.purple: 1}, ModNames.magic)
+
+baked_berry_oatmeal = shop_recipe(SVEMeal.baked_berry_oatmeal, SVERegion.bear_shop, 0, {Forageable.salmonberry: 15, Forageable.blackberry: 15,
+ Ingredient.sugar: 1, Ingredient.wheat_flour: 2}, ModNames.sve)
+big_bark_burger = friendship_and_shop_recipe(SVEMeal.big_bark_burger, NPC.gus, 5, Region.saloon, 5500,
+ {SVEFish.puppyfish: 1, Meal.bread: 1, Ingredient.oil: 1}, ModNames.sve)
+flower_cookie = shop_recipe(SVEMeal.flower_cookie, SVERegion.bear_shop, 0, {SVEForage.ferngill_primrose: 1, SVEForage.goldenrod: 1,
+ SVEForage.winter_star_rose: 1, Ingredient.wheat_flour: 1, Ingredient.sugar: 1,
+ AnimalProduct.large_egg: 1}, ModNames.sve)
+frog_legs = shop_recipe(SVEMeal.frog_legs, Region.adventurer_guild, 2000, {SVEFish.frog: 1, Ingredient.oil: 1, Ingredient.wheat_flour: 1}, ModNames.sve)
+glazed_butterfish = friendship_and_shop_recipe(SVEMeal.glazed_butterfish, NPC.gus, 10, Region.saloon, 4000,
+ {SVEFish.butterfish: 1, Ingredient.wheat_flour: 1, Ingredient.oil: 1}, ModNames.sve)
+mixed_berry_pie = shop_recipe(SVEMeal.mixed_berry_pie, Region.saloon, 3500, {Fruit.strawberry: 6, SVEFruit.salal_berry: 6, Forageable.blackberry: 6,
+ SVEForage.bearberry: 6, Ingredient.sugar: 1, Ingredient.wheat_flour: 1},
+ ModNames.sve)
+mushroom_berry_rice = friendship_and_shop_recipe(SVEMeal.mushroom_berry_rice, ModNPC.marlon, 6, Region.adventurer_guild, 1500, {SVEForage.poison_mushroom: 3, SVEForage.red_baneberry: 10,
+ Ingredient.rice: 1, Ingredient.sugar: 2}, ModNames.sve)
+seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEWaterItem.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, ModNames.sve)
+void_delight = friendship_and_shop_recipe(SVEMeal.void_delight, NPC.krobus, 10, Region.sewer, 5000,
+ {SVEFish.void_eel: 1, Loot.void_essence: 50, Loot.solar_essence: 20}, ModNames.sve)
+void_salmon_sushi = friendship_and_shop_recipe(SVEMeal.void_salmon_sushi, NPC.krobus, 10, Region.sewer, 5000,
+ {Fish.void_salmon: 1, ArtisanGood.void_mayonnaise: 1, WaterItem.seaweed: 3}, ModNames.sve)
+
+mushroom_kebab = friendship_recipe(DistantLandsMeal.mushroom_kebab, ModNPC.goblin, 2, {Mushroom.chanterelle: 1, Mushroom.common: 1,
+ Mushroom.red: 1, Material.wood: 1}, ModNames.distant_lands)
+void_mint_tea = friendship_recipe(DistantLandsMeal.void_mint_tea, ModNPC.goblin, 4, {DistantLandsCrop.void_mint: 1}, ModNames.distant_lands)
+crayfish_soup = friendship_recipe(DistantLandsMeal.crayfish_soup, ModNPC.goblin, 6, {Forageable.cave_carrot: 1, Fish.crayfish: 1,
+ DistantLandsFish.purple_algae: 1, WaterItem.white_algae: 1}, ModNames.distant_lands)
+pemmican = friendship_recipe(DistantLandsMeal.pemmican, ModNPC.goblin, 8, {Loot.bug_meat: 1, Fish.any: 1, Forageable.salmonberry: 3,
+ Material.stone: 2}, ModNames.distant_lands)
+
+special_pumpkin_soup = friendship_recipe(BoardingHouseMeal.special_pumpkin_soup, ModNPC.joel, 6, {Vegetable.pumpkin: 2, AnimalProduct.large_goat_milk: 1,
+ Vegetable.garlic: 1}, ModNames.boarding_house)
+diggers_delight = skill_recipe(ArchaeologyMeal.diggers_delight, ModSkill.archaeology, 3, {Forageable.cave_carrot: 2, Ingredient.sugar: 1, AnimalProduct.milk: 1}, ModNames.archaeology)
+rocky_root = skill_recipe(ArchaeologyMeal.rocky_root, ModSkill.archaeology, 7, {Forageable.cave_carrot: 3, Seed.coffee: 1, Material.stone: 1}, ModNames.archaeology)
+ancient_jello = skill_recipe(ArchaeologyMeal.ancient_jello, ModSkill.archaeology, 9, {WaterItem.cave_jelly: 6, Ingredient.sugar: 5, AnimalProduct.egg: 1, AnimalProduct.milk: 1, Artifact.chipped_amphora: 1}, ModNames.archaeology)
+
+grilled_cheese = skill_recipe(TrashyMeal.grilled_cheese, ModSkill.binning, 1, {Meal.bread: 1, ArtisanGood.cheese: 1}, ModNames.binning_skill)
+fish_casserole = skill_recipe(TrashyMeal.fish_casserole, ModSkill.binning, 8, {Fish.any: 1, AnimalProduct.milk: 1, Vegetable.carrot: 1}, ModNames.binning_skill)
+
+all_cooking_recipes_by_name = {recipe.meal: recipe for recipe in all_cooking_recipes}
\ No newline at end of file
diff --git a/worlds/stardew_valley/data/recipe_source.py b/worlds/stardew_valley/data/recipe_source.py
new file mode 100644
index 000000000000..24b03bf77bd4
--- /dev/null
+++ b/worlds/stardew_valley/data/recipe_source.py
@@ -0,0 +1,159 @@
+from typing import Union, List, Tuple
+
+
+class RecipeSource:
+
+ def __repr__(self):
+ return f"RecipeSource"
+
+
+class StarterSource(RecipeSource):
+
+ def __repr__(self):
+ return f"StarterSource"
+
+
+class ArchipelagoSource(RecipeSource):
+ ap_item: Tuple[str]
+
+ def __init__(self, ap_item: Union[str, List[str]]):
+ if isinstance(ap_item, str):
+ ap_item = [ap_item]
+ self.ap_item = tuple(ap_item)
+
+ def __repr__(self):
+ return f"ArchipelagoSource {self.ap_item}"
+
+
+class LogicSource(RecipeSource):
+ logic_rule: str
+
+ def __init__(self, logic_rule: str):
+ self.logic_rule = logic_rule
+
+ def __repr__(self):
+ return f"LogicSource {self.logic_rule}"
+
+
+class QueenOfSauceSource(RecipeSource):
+ year: int
+ season: str
+ day: int
+
+ def __init__(self, year: int, season: str, day: int):
+ self.year = year
+ self.season = season
+ self.day = day
+
+ def __repr__(self):
+ return f"QueenOfSauceSource at year {self.year} {self.season} {self.day}"
+
+
+class QuestSource(RecipeSource):
+ quest: str
+
+ def __init__(self, quest: str):
+ self.quest = quest
+
+ def __repr__(self):
+ return f"QuestSource at quest {self.quest}"
+
+
+class FriendshipSource(RecipeSource):
+ friend: str
+ hearts: int
+
+ def __init__(self, friend: str, hearts: int):
+ self.friend = friend
+ self.hearts = hearts
+
+ def __repr__(self):
+ return f"FriendshipSource at {self.friend} {self.hearts} <3"
+
+
+class CutsceneSource(FriendshipSource):
+ region: str
+
+ def __init__(self, region: str, friend: str, hearts: int):
+ super().__init__(friend, hearts)
+ self.region = region
+
+ def __repr__(self):
+ return f"CutsceneSource at {self.region}"
+
+
+class SkillSource(RecipeSource):
+ skill: str
+ level: int
+
+ def __init__(self, skill: str, level: int):
+ self.skill = skill
+ self.level = level
+
+ def __repr__(self):
+ return f"SkillSource at level {self.level} {self.skill}"
+
+
+class MasterySource(RecipeSource):
+ skill: str
+
+ def __init__(self, skill: str):
+ self.skill = skill
+
+ def __repr__(self):
+ return f"MasterySource at level {self.level} {self.skill}"
+
+
+class ShopSource(RecipeSource):
+ region: str
+ price: int
+
+ def __init__(self, region: str, price: int):
+ self.region = region
+ self.price = price
+
+ def __repr__(self):
+ return f"ShopSource at {self.region} costing {self.price}g"
+
+
+class ShopFriendshipSource(RecipeSource):
+ friend: str
+ hearts: int
+ region: str
+ price: int
+
+ def __init__(self, friend: str, hearts: int, region: str, price: int):
+ self.friend = friend
+ self.hearts = hearts
+ self.region = region
+ self.price = price
+
+ def __repr__(self):
+ return f"ShopFriendshipSource at {self.region} costing {self.price}g when {self.friend} has {self.hearts} hearts"
+
+
+class FestivalShopSource(ShopSource):
+
+ def __init__(self, region: str, price: int):
+ super().__init__(region, price)
+
+
+class ShopTradeSource(ShopSource):
+ currency: str
+
+ def __init__(self, region: str, currency: str, price: int):
+ super().__init__(region, price)
+ self.currency = currency
+
+ def __repr__(self):
+ return f"ShopTradeSource at {self.region} costing {self.price} {self.currency}"
+
+
+class SpecialOrderSource(RecipeSource):
+ special_order: str
+
+ def __init__(self, special_order: str):
+ self.special_order = special_order
+
+ def __repr__(self):
+ return f"SpecialOrderSource from {self.special_order}"
diff --git a/worlds/stardew_valley/data/requirement.py b/worlds/stardew_valley/data/requirement.py
new file mode 100644
index 000000000000..b2416d8d0b72
--- /dev/null
+++ b/worlds/stardew_valley/data/requirement.py
@@ -0,0 +1,57 @@
+from dataclasses import dataclass
+
+from .game_item import Requirement
+from ..strings.tool_names import ToolMaterial
+
+
+@dataclass(frozen=True)
+class BookRequirement(Requirement):
+ book: str
+
+
+@dataclass(frozen=True)
+class ToolRequirement(Requirement):
+ tool: str
+ tier: str = ToolMaterial.basic
+
+
+@dataclass(frozen=True)
+class SkillRequirement(Requirement):
+ skill: str
+ level: int
+
+
+@dataclass(frozen=True)
+class SeasonRequirement(Requirement):
+ season: str
+
+
+@dataclass(frozen=True)
+class YearRequirement(Requirement):
+ year: int
+
+
+@dataclass(frozen=True)
+class CombatRequirement(Requirement):
+ level: str
+
+
+@dataclass(frozen=True)
+class QuestRequirement(Requirement):
+ quest: str
+
+
+@dataclass(frozen=True)
+class RelationshipRequirement(Requirement):
+ npc: str
+ hearts: int
+
+
+@dataclass(frozen=True)
+class FishingRequirement(Requirement):
+ region: str
+
+
+@dataclass(frozen=True)
+class WalnutRequirement(Requirement):
+ amount: int
diff --git a/worlds/stardew_valley/data/shipsanity_unimplemented_items.csv b/worlds/stardew_valley/data/shipsanity_unimplemented_items.csv
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/stardew_valley/data/shop.py b/worlds/stardew_valley/data/shop.py
new file mode 100644
index 000000000000..f14dbac82131
--- /dev/null
+++ b/worlds/stardew_valley/data/shop.py
@@ -0,0 +1,40 @@
+from dataclasses import dataclass
+from typing import Tuple, Optional
+
+from .game_item import ItemSource, kw_only, Requirement
+from ..strings.season_names import Season
+
+ItemPrice = Tuple[int, str]
+
+
+@dataclass(frozen=True, **kw_only)
+class ShopSource(ItemSource):
+ shop_region: str
+ money_price: Optional[int] = None
+ items_price: Optional[Tuple[ItemPrice, ...]] = None
+ seasons: Tuple[str, ...] = Season.all
+ other_requirements: Tuple[Requirement, ...] = ()
+
+ def __post_init__(self):
+ assert self.money_price is not None or self.items_price is not None, "At least money price or items price need to be defined."
+ assert self.items_price is None or all(isinstance(p, tuple) for p in self.items_price), "Items price should be a tuple."
+
+
+@dataclass(frozen=True, **kw_only)
+class MysteryBoxSource(ItemSource):
+ amount: int
+
+
+@dataclass(frozen=True, **kw_only)
+class ArtifactTroveSource(ItemSource):
+ amount: int
+
+
+@dataclass(frozen=True, **kw_only)
+class PrizeMachineSource(ItemSource):
+ amount: int
+
+
+@dataclass(frozen=True, **kw_only)
+class FishingTreasureChestSource(ItemSource):
+ amount: int
diff --git a/worlds/stardew_valley/data/skill.py b/worlds/stardew_valley/data/skill.py
new file mode 100644
index 000000000000..d0674f34c0e1
--- /dev/null
+++ b/worlds/stardew_valley/data/skill.py
@@ -0,0 +1,9 @@
+from dataclasses import dataclass, field
+
+from ..data.game_item import kw_only
+
+
+@dataclass(frozen=True)
+class Skill:
+ name: str
+ has_mastery: bool = field(**kw_only)
diff --git a/worlds/stardew_valley/data/villagers_data.py b/worlds/stardew_valley/data/villagers_data.py
index 330d5eb955fb..70fb110ffbae 100644
--- a/worlds/stardew_valley/data/villagers_data.py
+++ b/worlds/stardew_valley/data/villagers_data.py
@@ -1,7 +1,10 @@
from dataclasses import dataclass
-from typing import List, Tuple, Optional, Dict
-from ..strings.region_names import Region
+from typing import Tuple, Optional
+
from ..mods.mod_data import ModNames
+from ..strings.food_names import Beverage
+from ..strings.generic_names import Generic
+from ..strings.region_names import Region, SVERegion, AlectoRegion, BoardingHouseRegion, LaceyRegion, LogicRegion
from ..strings.season_names import Season
from ..strings.villager_names import NPC, ModNPC
@@ -10,11 +13,11 @@
class Villager:
name: str
bachelor: bool
- locations: Tuple[str]
+ locations: Tuple[str, ...]
birthday: str
- gifts: Tuple[str]
+ gifts: Tuple[str, ...]
available: bool
- mod_name: Optional[str]
+ mod_name: str
def __repr__(self):
return f"{self.name} [Bachelor: {self.bachelor}] [Available from start: {self.available}]" \
@@ -33,12 +36,30 @@ def __repr__(self):
alex_house = (Region.alex_house,)
elliott_house = (Region.elliott_house,)
ranch = (Region.ranch,)
-mines_dwarf_shop = (Region.mines_dwarf_shop,)
+mines_dwarf_shop = (LogicRegion.mines_dwarf_shop,)
desert = (Region.desert,)
oasis = (Region.oasis,)
sewers = (Region.sewer,)
island = (Region.island_east,)
secret_woods = (Region.secret_woods,)
+wizard_tower = (Region.wizard_tower,)
+
+# Stardew Valley Expanded Locations
+adventurer = (Region.adventurer_guild,)
+highlands = (SVERegion.highlands_outside,)
+bluemoon = (SVERegion.blue_moon_vineyard,)
+aurora = (SVERegion.aurora_vineyard,)
+museum = (Region.museum,)
+jojamart = (Region.jojamart,)
+railroad = (Region.railroad,)
+junimo = (SVERegion.junimo_woods,)
+
+# Stray Locations
+witch_swamp = (Region.witch_swamp,)
+witch_attic = (AlectoRegion.witch_attic,)
+hat_house = (LaceyRegion.hat_house,)
+the_lost_valley = (BoardingHouseRegion.the_lost_valley,)
+boarding_house = (BoardingHouseRegion.boarding_house_first,)
golden_pumpkin = ("Golden Pumpkin",)
# magic_rock_candy = ("Magic Rock Candy",)
@@ -182,7 +203,7 @@ def __repr__(self):
pale_ale = ("Pale Ale",)
parsnip = ("Parsnip",)
# parsnip_soup = ("Parsnip Soup",)
-pina_colada = ("Piña Colada",)
+pina_colada = (Beverage.pina_colada,)
pam_loves = beer + cactus_fruit + glazed_yams + mead + pale_ale + parsnip + pina_colada # | parsnip_soup
# fried_calamari = ("Fried Calamari",)
pierre_loves = () # fried_calamari
@@ -208,7 +229,7 @@ def __repr__(self):
void_essence = ("Void Essence",)
wizard_loves = purple_mushroom + solar_essence + super_cucumber + void_essence
-#Custom NPC Items and Loves
+# Custom NPC Items and Loves
blueberry = ("Blueberry",)
chanterelle = ("Chanterelle",)
@@ -270,15 +291,74 @@ def __repr__(self):
juna_loves = ancient_doll + elvish_jewelry + dinosaur_egg + strange_doll + joja_cola + hashbrowns + pancakes + \
pink_cake + jelly + ghost_crystal + prehistoric_scapula + cherry
+glazed_butterfish = ("Glazed Butterfish",)
+aged_blue_moon_wine = ("Aged Blue Moon Wine",)
+blue_moon_wine = ("Blue Moon Wine",)
+daggerfish = ("Daggerfish",)
+gemfish = ("Gemfish",)
+green_mushroom = ("Green Mushroom",)
+monster_mushroom = ("Monster Mushroom",)
+swirl_stone = ("Swirl Stone",)
+torpedo_trout = ("Torpedo Trout",)
+void_shard = ("Void Shard",)
+ornate_treasure_chest = ("Ornate Treasure Chest",)
+frog_legs = ("Frog Legs",)
+void_delight = ("Void Delight",)
+void_pebble = ("Void Pebble",)
+void_salmon_sushi = ("Void Salmon Sushi",)
+puppyfish = ("Puppyfish",)
+butterfish = ("Butterfish",)
+king_salmon = ("King Salmon",)
+frog = ("Frog",)
+kittyfish = ("Kittyfish",)
+big_bark_burger = ("Big Bark Burger",)
+starfruit = ("Starfruit",)
+bruschetta = ("Brushetta",)
+apricot = ("Apricot",)
+ocean_stone = ("Ocean Stone",)
+fairy_stone = ("Fairy Stone",)
+lunarite = ("Lunarite",)
+bean_hotpot = ("Bean Hotpot",)
+petrified_slime = ("Petrified Slime",)
+ornamental_fan = ("Ornamental Fan",)
+ancient_sword = ("Ancient Sword",)
+star_shards = ("Star Shards",)
+life_elixir = ("Life Elixir",)
+juice = ("Juice",)
+lobster_bisque = ("Lobster Bisque",)
+chowder = ("Chowder",)
+goat_milk = ("Goat Milk",)
+maple_syrup = ("Maple Syrup",)
+cookie = ("Cookie",)
+blueberry_tart = ("Blueberry Tart",)
-all_villagers: List[Villager] = []
+claire_loves = green_tea + sunflower + energy_tonic + bruschetta + apricot + ocean_stone + glazed_butterfish
+lance_loves = aged_blue_moon_wine + daggerfish + gemfish + golden_pumpkin + \
+ green_mushroom + monster_mushroom + swirl_stone + torpedo_trout + tropical_curry + void_shard + \
+ ornate_treasure_chest
+olivia_loves = wine + chocolate_cake + pink_cake + golden_mask + golden_relic + \
+ blue_moon_wine + aged_blue_moon_wine
+sophia_loves = fairy_rose + fairy_stone + puppyfish
+victor_loves = spaghetti + battery_pack + duck_feather + lunarite + \
+ aged_blue_moon_wine + blue_moon_wine + butterfish
+andy_loves = pearl + beer + mead + pale_ale + farmers_lunch + glazed_butterfish + butterfish + \
+ king_salmon + blackberry_cobbler
+gunther_loves = bean_hotpot + petrified_slime + salmon_dinner + elvish_jewelry + ornamental_fan + \
+ dinosaur_egg + rare_disc + ancient_sword + dwarvish_helm + dwarf_gadget + golden_mask + golden_relic + \
+ star_shards
+marlon_loves = roots_platter + life_elixir + aged_blue_moon_wine + void_delight
+martin_loves = juice + ice_cream + big_bark_burger
+morgan_loves = iridium_bar + void_egg + void_mayonnaise + frog + kittyfish
+morris_loves = lobster_bisque + chowder + truffle_oil + star_shards + aged_blue_moon_wine
+scarlett_loves = goat_cheese + duck_feather + goat_milk + cherry + maple_syrup + honey + \
+ chocolate_cake + pink_cake + jade + glazed_yams # actually large milk but meh
+susan_loves = pancakes + chocolate_cake + pink_cake + ice_cream + cookie + pumpkin_pie + rhubarb_pie + \
+ blueberry_tart + blackberry_cobbler + cranberry_candy + red_plate
def villager(name: str, bachelor: bool, locations: Tuple[str, ...], birthday: str, gifts: Tuple[str, ...],
available: bool, mod_name: Optional[str] = None) -> Villager:
- npc = Villager(name, bachelor, locations, birthday, gifts, available, mod_name)
- all_villagers.append(npc)
- return npc
+ return Villager(name, bachelor, locations, birthday, gifts, available, mod_name)
josh = villager(NPC.alex, True, town + alex_house, Season.summer, universal_loves + complete_breakfast + salmon_dinner, True)
@@ -287,18 +367,18 @@ def villager(name: str, bachelor: bool, locations: Tuple[str, ...], birthday: st
sam = villager(NPC.sam, True, town, Season.summer, universal_loves + sam_loves, True)
sebastian = villager(NPC.sebastian, True, carpenter, Season.winter, universal_loves + sebastian_loves, True)
shane = villager(NPC.shane, True, ranch, Season.spring, universal_loves + shane_loves, True)
-best_girl = villager(NPC.abigail, True, town, Season.fall, universal_loves + abigail_loves, True)
+abigail = villager(NPC.abigail, True, town, Season.fall, universal_loves + abigail_loves, True)
emily = villager(NPC.emily, True, town, Season.spring, universal_loves + emily_loves, True)
-hoe = villager(NPC.haley, True, town, Season.spring, universal_loves_no_prismatic_shard + haley_loves, True)
+haley = villager(NPC.haley, True, town, Season.spring, universal_loves_no_prismatic_shard + haley_loves, True)
leah = villager(NPC.leah, True, forest, Season.winter, universal_loves + leah_loves, True)
-nerd = villager(NPC.maru, True, carpenter + hospital + town, Season.summer, universal_loves + maru_loves, True)
+maru = villager(NPC.maru, True, carpenter + hospital + town, Season.summer, universal_loves + maru_loves, True)
penny = villager(NPC.penny, True, town, Season.fall, universal_loves_no_rabbit_foot + penny_loves, True)
caroline = villager(NPC.caroline, False, town, Season.winter, universal_loves + caroline_loves, True)
clint = villager(NPC.clint, False, town, Season.winter, universal_loves + clint_loves, True)
demetrius = villager(NPC.demetrius, False, carpenter, Season.summer, universal_loves + demetrius_loves, True)
dwarf = villager(NPC.dwarf, False, mines_dwarf_shop, Season.summer, universal_loves + dwarf_loves, False)
-gilf = villager(NPC.evelyn, False, town, Season.winter, universal_loves + evelyn_loves, True)
-boomer = villager(NPC.george, False, town, Season.fall, universal_loves + george_loves, True)
+evelyn = villager(NPC.evelyn, False, town, Season.winter, universal_loves + evelyn_loves, True)
+george = villager(NPC.george, False, town, Season.fall, universal_loves + george_loves, True)
gus = villager(NPC.gus, False, town, Season.summer, universal_loves + gus_loves, True)
jas = villager(NPC.jas, False, ranch, Season.summer, universal_loves + jas_loves, True)
jodi = villager(NPC.jodi, False, town, Season.fall, universal_loves + jodi_loves, True)
@@ -310,11 +390,11 @@ def villager(name: str, bachelor: bool, locations: Tuple[str, ...], birthday: st
marnie = villager(NPC.marnie, False, ranch, Season.fall, universal_loves + marnie_loves, True)
pam = villager(NPC.pam, False, town, Season.spring, universal_loves + pam_loves, True)
pierre = villager(NPC.pierre, False, town, Season.spring, universal_loves + pierre_loves, True)
-milf = villager(NPC.robin, False, carpenter, Season.fall, universal_loves + robin_loves, True)
+robin = villager(NPC.robin, False, carpenter, Season.fall, universal_loves + robin_loves, True)
sandy = villager(NPC.sandy, False, oasis, Season.fall, universal_loves + sandy_loves, False)
vincent = villager(NPC.vincent, False, town, Season.spring, universal_loves + vincent_loves, True)
willy = villager(NPC.willy, False, beach, Season.summer, universal_loves + willy_loves, True)
-wizard = villager(NPC.wizard, False, forest, Season.winter, universal_loves + wizard_loves, True)
+wizard = villager(NPC.wizard, False, wizard_tower, Season.winter, universal_loves + wizard_loves, True)
# Custom NPCs
alec = villager(ModNPC.alec, True, forest, Season.winter, universal_loves + trilobite, True, ModNames.alec)
@@ -325,21 +405,30 @@ def villager(name: str, bachelor: bool, locations: Tuple[str, ...], birthday: st
juna = villager(ModNPC.juna, False, forest, Season.summer, universal_loves + juna_loves, True, ModNames.juna)
kitty = villager(ModNPC.mr_ginger, False, forest, Season.summer, universal_loves + mister_ginger_loves, True, ModNames.ginger)
shiko = villager(ModNPC.shiko, True, town, Season.winter, universal_loves + shiko_loves, True, ModNames.shiko)
-wellwick = villager(ModNPC.wellwick, True, forest, Season.winter, universal_loves + wellwick_loves, True, ModNames.shiko)
+wellwick = villager(ModNPC.wellwick, True, forest, Season.winter, universal_loves + wellwick_loves, True, ModNames.wellwick)
yoba = villager(ModNPC.yoba, False, secret_woods, Season.spring, universal_loves + yoba_loves, False, ModNames.yoba)
riley = villager(ModNPC.riley, True, town, Season.spring, universal_loves, True, ModNames.riley)
+zic = villager(ModNPC.goblin, False, witch_swamp, Season.fall, void_mayonnaise, False, ModNames.distant_lands)
+alecto = villager(ModNPC.alecto, False, witch_attic, Generic.any, universal_loves, False, ModNames.alecto)
+lacey = villager(ModNPC.lacey, True, forest, Season.spring, universal_loves, True, ModNames.lacey)
-all_villagers_by_name: Dict[str, Villager] = {villager.name: villager for villager in all_villagers}
-all_villagers_by_mod: Dict[str, List[Villager]] = {}
-all_villagers_by_mod_by_name: Dict[str, Dict[str, Villager]] = {}
-for npc in all_villagers:
- mod = npc.mod_name
- name = npc.name
- if mod in all_villagers_by_mod:
- all_villagers_by_mod[mod].append(npc)
- all_villagers_by_mod_by_name[mod][name] = npc
- else:
- all_villagers_by_mod[mod] = [npc]
- all_villagers_by_mod_by_name[mod] = {}
- all_villagers_by_mod_by_name[mod][name] = npc
+# Boarding House Villagers
+gregory = villager(ModNPC.gregory, True, the_lost_valley, Season.fall, universal_loves, False, ModNames.boarding_house)
+sheila = villager(ModNPC.sheila, True, boarding_house, Season.spring, universal_loves, True, ModNames.boarding_house)
+joel = villager(ModNPC.joel, False, boarding_house, Season.winter, universal_loves, True, ModNames.boarding_house)
+# SVE Villagers
+claire = villager(ModNPC.claire, True, town + jojamart, Season.fall, universal_loves + claire_loves, True, ModNames.sve)
+lance = villager(ModNPC.lance, True, adventurer + highlands + island, Season.spring, lance_loves, False, ModNames.sve)
+mommy = villager(ModNPC.olivia, True, town, Season.spring, universal_loves_no_rabbit_foot + olivia_loves, True, ModNames.sve)
+sophia = villager(ModNPC.sophia, True, bluemoon, Season.winter, universal_loves_no_rabbit_foot + sophia_loves, True, ModNames.sve)
+victor = villager(ModNPC.victor, True, town, Season.summer, universal_loves + victor_loves, True, ModNames.sve)
+andy = villager(ModNPC.andy, False, forest, Season.spring, universal_loves + andy_loves, True, ModNames.sve)
+apples = villager(ModNPC.apples, False, aurora + junimo, Generic.any, starfruit, False, ModNames.sve)
+gunther = villager(ModNPC.gunther, False, museum, Season.winter, universal_loves + gunther_loves, True, ModNames.sve)
+martin = villager(ModNPC.martin, False, town + jojamart, Season.summer, universal_loves + martin_loves, True, ModNames.sve)
+marlon = villager(ModNPC.marlon, False, adventurer, Season.winter, universal_loves + marlon_loves, False, ModNames.sve)
+morgan = villager(ModNPC.morgan, False, forest, Season.fall, universal_loves_no_rabbit_foot + morgan_loves, False, ModNames.sve)
+scarlett = villager(ModNPC.scarlett, False, bluemoon, Season.summer, universal_loves + scarlett_loves, False, ModNames.sve)
+susan = villager(ModNPC.susan, False, railroad, Season.fall, universal_loves + susan_loves, False, ModNames.sve)
+morris = villager(ModNPC.morris, False, jojamart, Season.spring, universal_loves + morris_loves, True, ModNames.sve)
diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md
index a880a40b971a..0ed693031b82 100644
--- a/worlds/stardew_valley/docs/en_Stardew Valley.md
+++ b/worlds/stardew_valley/docs/en_Stardew Valley.md
@@ -1,129 +1,173 @@
# Stardew Valley
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
-A vast number of objectives in Stardew Valley can be shuffled around the multiworld. Most of these are optional, and the player can customize their experience in their YAML file.
+A vast number of objectives in Stardew Valley can be shuffled around the multiworld. Most of these are optional, and the
+player can customize their experience in their YAML file.
-For these objectives, if they have a vanilla reward, this reward will instead be an item in the multiworld. For the remaining number of such objectives, there are a number of "Resource Pack" items, which are simply an item or a stack of items that may be useful to the player.
+For these objectives, if they have a vanilla reward, this reward will instead be an item in the multiworld. For the remaining
+number of such objectives, there are a number of "Resource Pack" items, which are simply an item or a stack of items that
+may be useful to the player.
## What is the goal of Stardew Valley?
-The player can choose from a number of goals, using their YAML settings.
+The player can choose from a number of goals, using their YAML options.
+
- Complete the [Community Center](https://stardewvalleywiki.com/Bundles)
- Succeed [Grandpa's Evaluation](https://stardewvalleywiki.com/Grandpa) with 4 lit candles
- Reach the bottom of the [Pelican Town Mineshaft](https://stardewvalleywiki.com/The_Mines)
-- Complete the [Cryptic Note](https://stardewvalleywiki.com/Secret_Notes#Secret_Note_.2310) quest, by meeting Mr Qi on floor 100 of the Skull Cavern
-- Get the achievement [Master Angler](https://stardewvalleywiki.com/Fish), which requires catching every fish in the game
-- Get the achievement [A Complete Collection](https://stardewvalleywiki.com/Museum), which requires donating all the artifacts and minerals to the museum
-- Get the achievement [Full House](https://stardewvalleywiki.com/Children), which requires getting married and having two kids.
-- Get recognized as the [Greatest Walnut Hunter](https://stardewvalleywiki.com/Golden_Walnut) by Mr Qi, which requires finding all 130 golden walnuts on ginger island
+- Complete the [Cryptic Note](https://stardewvalleywiki.com/Secret_Notes#Secret_Note_.2310) quest, by meeting Mr Qi on
+ floor 100 of the Skull Cavern
+- Become a [Master Angler](https://stardewvalleywiki.com/Fish), which requires catching every fish in your slot
+- Restore [A Complete Collection](https://stardewvalleywiki.com/Museum), which requires donating all the artifacts and
+ minerals to the museum
+- Get the achievement [Full House](https://stardewvalleywiki.com/Children), which requires getting married and having two kids
+- Get recognized as the [Greatest Walnut Hunter](https://stardewvalleywiki.com/Golden_Walnut) by Mr Qi, which requires
+ finding all 130 golden walnuts on ginger island
+- Become the [Protector of the Valley](https://stardewvalleywiki.com/Adventurer%27s_Guild#Monster_Eradication_Goals) by
+ completing all the monster slayer goals at the Adventure Guild
+- Complete a [Full Shipment](https://stardewvalleywiki.com/Shipping#Collection) by shipping every item in your slot
+- Become a [Gourmet Chef](https://stardewvalleywiki.com/Cooking) by cooking every recipe in your slot
+- Become a [Craft Master](https://stardewvalleywiki.com/Crafting) by crafting every item
+- Earn the title of [Legend](https://stardewvalleywiki.com/Gold) by earning 10 000 000g
+- Solve the [Mystery of the Stardrops](https://stardewvalleywiki.com/Stardrop) by finding every stardrop
+- Finish 100% of your randomizer slot with Allsanity: Complete every check in your slot
- Achieve [Perfection](https://stardewvalleywiki.com/Perfection) in your save file
+The following goals [Community Center, Master Angler, Protector of the Valley, Full Shipment and Gourmet Chef] will adapt
+to other options in your slots, and are therefore customizable in duration and difficulty. For example, if you set "Fishsanity"
+to "Exclude Legendaries", and pick the Master Angler goal, you will not need to catch the legendaries to complete the goal.
+
## What are location checks in Stardew Valley?
Location checks in Stardew Valley always include:
+
- [Community Center Bundles](https://stardewvalleywiki.com/Bundles)
- [Mineshaft Chest Rewards](https://stardewvalleywiki.com/The_Mines#Remixed_Rewards)
-- [Story Quests](https://stardewvalleywiki.com/Quests#List_of_Story_Quests)
- [Traveling Merchant Items](https://stardewvalleywiki.com/Traveling_Cart)
-- Isolated objectives such as the [beach bridge](https://stardewvalleywiki.com/The_Beach#Tide_Pools), [Old Master Cannoli](https://stardewvalleywiki.com/Secret_Woods#Old_Master_Cannoli), [Grim Reaper Statue](https://stardewvalleywiki.com/Golden_Scythe), etc
+- Isolated objectives such as the [beach bridge](https://stardewvalleywiki.com/The_Beach#Tide_Pools),
+ [Old Master Cannoli](https://stardewvalleywiki.com/Secret_Woods#Old_Master_Cannoli),
+ [Grim Reaper Statue](https://stardewvalleywiki.com/Golden_Scythe), etc
There also are a number of location checks that are optional, and individual players choose to include them or not in their shuffling:
-- Tools and Fishing Rod Upgrades
-- Carpenter Buildings
-- Backpack Upgrades
-- Mine Elevator Levels
-- Skill Levels
+
+- [Tools and Fishing Rod Upgrades](https://stardewvalleywiki.com/Tools)
+- [Carpenter Buildings](https://stardewvalleywiki.com/Carpenter%27s_Shop#Farm_Buildings)
+- [Backpack Upgrades](https://stardewvalleywiki.com/Tools#Other_Tools)
+- [Mine Elevator Levels](https://stardewvalleywiki.com/The_Mines#Staircases)
+- [Skill Levels](https://stardewvalleywiki.com/Skills) and [Masteries](https://stardewvalleywiki.com/Mastery_Cave#Masteries)
- Arcade Machines
-- Help Wanted Quests
-- Participating in Festivals
-- Special Orders from the town board, or from Mr Qi
-- Cropsanity: Growing and Harvesting individual crop types
-- Fishsanity: Catching individual fish
-- Museumsanity: Donating individual items, or reaching milestones for museum donations
-- Friendsanity: Reaching specific friendship levels with NPCs
+- [Story Quests](https://stardewvalleywiki.com/Quests#List_of_Story_Quests)
+- [Help Wanted Quests](https://stardewvalleywiki.com/Quests#Help_Wanted_Quests)
+- Participating in [Festivals](https://stardewvalleywiki.com/Festivals)
+- [Special Orders](https://stardewvalleywiki.com/Quests#List_of_Special_Orders) from the town board, or from
+ [Mr Qi](https://stardewvalleywiki.com/Quests#List_of_Mr._Qi.27s_Special_Orders)
+- [Cropsanity](https://stardewvalleywiki.com/Crops): Growing and Harvesting individual crop types
+- [Fishsanity](https://stardewvalleywiki.com/Fish): Catching individual fish
+- [Museumsanity](https://stardewvalleywiki.com/Museum): Donating individual items, or reaching milestones for museum donations
+- [Friendsanity](https://stardewvalleywiki.com/Friendship): Reaching specific friendship levels with NPCs
+- [Monstersanity](https://stardewvalleywiki.com/Adventurer%27s_Guild#Monster_Eradication_Goals): Completing monster slayer goals
+- [Cooksanity](https://stardewvalleywiki.com/Cooking): Cooking individual recipes
+- [Chefsanity](https://stardewvalleywiki.com/Cooking#Recipes): Learning cooking recipes
+- [Craftsanity](https://stardewvalleywiki.com/Crafting): Crafting individual items
+- [Shipsanity](https://stardewvalleywiki.com/Shipping): Shipping individual items
+- [Booksanity](https://stardewvalleywiki.com/Books): Reading individual books
+- [Walnutsanity](https://stardewvalleywiki.com/Golden_Walnut): Collecting Walnuts on Ginger Island
## Which items can be in another player's world?
Every normal reward from the above locations can be in another player's world.
For the locations which do not include a normal reward, Resource Packs and traps are instead added to the pool. Traps are optional.
-A player can enable some settings that will add some items to the pool that are relevant to progression
+A player can enable some options that will add some items to the pool that are relevant to progression
+
- Seasons Randomizer:
- - All 4 seasons will be items, and one of them will be selected randomly and be added to the player's start inventory
- - At the end of each month, the player can choose the next season, instead of following the vanilla season order. On Seasons Randomizer, they can only choose from the seasons they have received.
+ - All 4 seasons will be items, and one of them will be selected randomly and be added to the player's start inventory.
+ - At the end of each month, the player can choose the next season, instead of following the vanilla season order. On Seasons Randomizer, they can only
+ choose from the seasons they have received.
- Cropsanity:
- - Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received as multiworld items. Growing each seed and harvesting the resulting crop sends a location check
- - The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells unlimited seeds but in huge discount packs, not individually.
+ - Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received as multiworld items. Growing each
+ seed and harvesting the resulting crop sends a location check
+ - The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells unlimited seeds but in huge discount
+ packs, not individually.
- Museumsanity:
- - The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the magic rock candy, are duplicated for convenience.
- - The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness. She will sell these items as the player receives "Traveling Merchant Metal Detector" items.
+ - The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the magic rock candy, are duplicated for
+ convenience.
+ - The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness. She will sell these items as the
+ player receives "Traveling Merchant Metal Detector" items.
- TV Channels
- Babies
+ - Only if Friendsanity is enabled
There are a few extra vanilla items, which are added to the pool for convenience, but do not have a matching location. These include
+
- [Wizard Buildings](https://stardewvalleywiki.com/Wizard%27s_Tower#Buildings)
- [Return Scepter](https://stardewvalleywiki.com/Return_Scepter)
+- [Qi Walnut Room QoL items](https://stardewvalleywiki.com/Qi%27s_Walnut_Room#Stock)
And lastly, some Archipelago-exclusive items exist in the pool, which are designed around game balance and QoL. These include:
+
- Arcade Machine buffs (Only if the arcade machines are randomized)
- - Journey of the Prairie King has drop rate increases, extra lives, and equipment
- - Junimo Kart has extra lives.
+ - Journey of the Prairie King has drop rate increases, extra lives, and equipment
+ - Junimo Kart has extra lives.
- Permanent Movement Speed Bonuses (customizable)
-- Permanent Luck Bonuses (customizable)
-- Traveling Merchant buffs
+- Various Permanent Player Buffs (customizable)
+- Traveling Merchant modifiers
## When the player receives an item, what happens?
-Since Pelican Town is a remote area, it takes one business day for every item to reach the player. If an item is received while online, it will appear in the player's mailbox the next morning, with a message from the sender telling them where it was found.
-If an item is received while offline, it will be in the mailbox as soon as the player logs in.
+Since Pelican Town is a remote area, it takes one business day for every item to reach the player. If an item is received
+while online, it will appear in the player's mailbox the next morning, with a message from the sender telling them where
+it was found. If an item is received while offline, it will be in the mailbox as soon as the player logs in.
-Some items will be directly attached to the letter, while some others will instead be a world-wide unlock, and the letter only serves to tell the player about it.
+Some items will be directly attached to the letter, while some others will instead be a world-wide unlock, and the letter
+only serves to tell the player about it.
-In some cases, like receiving Carpenter and Wizard buildings, the player will still need to go ask Robin to construct the building that they have received, so they can choose its position. This construction will be completely free.
+In some cases, like receiving Carpenter and Wizard buildings, the player will still need to go ask Robin to construct the
+building that they have received, so they can choose its position. This construction will be completely free.
## Mods
-Starting in version 4.x.x, some Stardew Valley mods unrelated to Archipelago are officially "supported".
-This means that, for these specific mods, if you decide to include them in your yaml settings, the multiworld will be generated with the assumption that you will install and play with these mods.
-The multiworld will contain related items and locations for these mods, the specifics will vary from mod to mod
+Some Stardew Valley mods unrelated to Archipelago are officially "supported".
+This means that, for these specific mods, if you decide to include them in your yaml options, the multiworld will be generated
+with the assumption that you will install and play with these mods. The multiworld will contain related items and locations
+for these mods, the specifics will vary from mod to mod
-[Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/4.x.x/Documentation/Supported%20Mods.md)
+[Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md)
List of supported mods:
- General
- - [DeepWoods](https://www.nexusmods.com/stardewvalley/mods/2571)
- - [Tractor Mod](https://www.nexusmods.com/stardewvalley/mods/1401)
- - [Bigger Backpack](https://www.nexusmods.com/stardewvalley/mods/1845)
- - [Skull Cavern Elevator](https://www.nexusmods.com/stardewvalley/mods/963)
+ - [Stardew Valley Expanded](https://www.nexusmods.com/stardewvalley/mods/3753)
+ - [Skull Cavern Elevator](https://www.nexusmods.com/stardewvalley/mods/963)
+ - [Bigger Backpack](https://www.nexusmods.com/stardewvalley/mods/1845)
+ - [Tractor Mod](https://www.nexusmods.com/stardewvalley/mods/1401)
+ - [Distant Lands - Witch Swamp Overhaul](https://www.nexusmods.com/stardewvalley/mods/18109)
- Skills
- - [Luck Skill](https://www.nexusmods.com/stardewvalley/mods/521)
- - [Magic](https://www.nexusmods.com/stardewvalley/mods/2007)
- - [Socializing Skill](https://www.nexusmods.com/stardewvalley/mods/14142)
- - [Archaeology](https://www.nexusmods.com/stardewvalley/mods/15793)
- - [Cooking Skill](https://www.nexusmods.com/stardewvalley/mods/522)
- - [Binning Skill](https://www.nexusmods.com/stardewvalley/mods/14073)
+ - [Luck Skill](https://www.nexusmods.com/stardewvalley/mods/521)
+ - [Socializing Skill](https://www.nexusmods.com/stardewvalley/mods/14142)
+ - [Archaeology](https://www.nexusmods.com/stardewvalley/mods/22199)
+ - [Binning Skill](https://www.nexusmods.com/stardewvalley/mods/14073)
- NPCs
- - [Ayeisha - The Postal Worker (Custom NPC)](https://www.nexusmods.com/stardewvalley/mods/6427)
- - [Mister Ginger (cat npc)](https://www.nexusmods.com/stardewvalley/mods/5295)
- - [Juna - Roommate NPC](https://www.nexusmods.com/stardewvalley/mods/8606)
- - [Professor Jasper Thomas](https://www.nexusmods.com/stardewvalley/mods/5599)
- - [Alec Revisited](https://www.nexusmods.com/stardewvalley/mods/10697)
- - [Custom NPC - Yoba](https://www.nexusmods.com/stardewvalley/mods/14871)
- - [Custom NPC Eugene](https://www.nexusmods.com/stardewvalley/mods/9222)
- - ['Prophet' Wellwick](https://www.nexusmods.com/stardewvalley/mods/6462)
- - [Shiko - New Custom NPC](https://www.nexusmods.com/stardewvalley/mods/3732)
- - [Delores - Custom NPC](https://www.nexusmods.com/stardewvalley/mods/5510)
- - [Custom NPC - Riley](https://www.nexusmods.com/stardewvalley/mods/5811)
+ - [Ayeisha - The Postal Worker (Custom NPC)](https://www.nexusmods.com/stardewvalley/mods/6427)
+ - [Mister Ginger (cat npc)](https://www.nexusmods.com/stardewvalley/mods/5295)
+ - [Juna - Roommate NPC](https://www.nexusmods.com/stardewvalley/mods/8606)
+ - [Professor Jasper Thomas](https://www.nexusmods.com/stardewvalley/mods/5599)
+ - [Alec Revisited](https://www.nexusmods.com/stardewvalley/mods/10697)
+ - [Custom NPC Eugene](https://www.nexusmods.com/stardewvalley/mods/9222)
+ - [Alecto the Witch](https://www.nexusmods.com/stardewvalley/mods/10671)
+
+Some of these mods might need a patch mod to tie the randomizer with the mod. These can be found
+[here](https://github.com/Witchybun/SDV-Randomizer-Content-Patcher/releases)
## Multiplayer
-You cannot play an Archipelago Slot in multiplayer at the moment. There is no short-terms plans to support that feature.
+You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-term plans to support that feature.
-You can, however, send Stardew Valley objects as gifts from one Stardew Player to another Stardew player, using in-game Joja Prime delivery, for a fee. This exclusive feature can be turned off if you don't want to send and receive gifts.
+You can, however, send Stardew Valley objects as gifts from one Stardew Player to another Stardew , or a player in another game that supports gifting, using
+in-game Joja Prime delivery, for a fee. This exclusive feature can be turned off if you don't want to send and receive gifts.
diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md
index 68c7fb9af6a0..c672152543cf 100644
--- a/worlds/stardew_valley/docs/setup_en.md
+++ b/worlds/stardew_valley/docs/setup_en.md
@@ -2,19 +2,22 @@
## Required Software
-- Stardew Valley on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/))
-- SMAPI ([Mod loader for Stardew Valley](https://smapi.io/))
-- [StardewArchipelago Mod Release 4.x.x](https://github.com/agilbert1412/StardewArchipelago/releases)
- - It is important to use a mod release of version 4.x.x to play seeds that have been generated here. Later releases can only be used with later releases of the world generator, that are not hosted on archipelago.gg yet.
+- Stardew Valley 1.6 on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/))
+- SMAPI ([Mod loader for Stardew Valley](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files))
+- [StardewArchipelago Mod Release 6.x.x](https://github.com/agilbert1412/StardewArchipelago/releases)
+ - It is important to use a mod release of version 6.x.x to play seeds that have been generated here. Later releases
+ can only be used with later releases of the world generator, that are not hosted on archipelago.gg yet.
## Optional Software
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- - (Only for the TextClient)
+ * (Only for the TextClient)
- Other Stardew Valley Mods [Nexus Mods](https://www.nexusmods.com/stardewvalley)
- - There are [supported mods](https://github.com/agilbert1412/StardewArchipelago/blob/4.x.x/Documentation/Supported%20Mods.md) that you can add to your yaml to include them with the Archipelago randomization
+ * There are [supported mods](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md)
+ that you can add to your yaml to include them with the Archipelago randomization
- - It is **not** recommended to further mod Stardew Valley with unsupported mods, although it is possible to do so. Mod interactions can be unpredictable, and no support will be offered for related bugs.
- - The more unsupported mods you have, and the bigger they are, the more likely things are to break.
+ * It is **not** recommended to further mod Stardew Valley with unsupported mods, although it is possible to do so.
+ Mod interactions can be unpredictable, and no support will be offered for related bugs.
+ * The more unsupported mods you have, and the bigger they are, the more likely things are to break.
## Configuring your YAML file
@@ -25,16 +28,16 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a YAML file?
-You can customize your settings by visiting the [Stardew Valley Player Settings Page](/games/Stardew%20Valley/player-settings)
+You can customize your options by visiting the [Stardew Valley Player Options Page](/games/Stardew%20Valley/player-options)
## Joining a MultiWorld Game
### Installing the mod
-- Install [SMAPI](https://smapi.io/) by following the instructions on their website
-- Download and extract the [StardewArchipelago](https://github.com/agilbert1412/StardewArchipelago/releases) mod into your Stardew Valley "Mods" folder
-- *OPTIONAL*: If you want to launch your game through Steam, add the following to your Stardew Valley launch options:
- - "[PATH TO STARDEW VALLEY]\Stardew Valley\StardewModdingAPI.exe" %command%
+- Install [SMAPI](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files) by following the instructions on the mod page
+- Download and extract the [StardewArchipelago](https://github.com/agilbert1412/StardewArchipelago/releases) mod into
+your Stardew Valley "Mods" folder
+- *OPTIONAL*: If you want to launch your game through Steam, add the following to your Stardew Valley launch options: `"[PATH TO STARDEW VALLEY]\Stardew Valley\StardewModdingAPI.exe" %command%`
- Otherwise just launch "StardewModdingAPI.exe" in your installation folder directly
- Stardew Valley should launch itself alongside a console which allows you to read mod information and interact with some of them.
@@ -69,19 +72,25 @@ If the room's ip or port **does** change, you can follow these instructions to m
### Interacting with the MultiWorld from in-game
-When you connect, you should see a message in the chat informing you of the `!!help` command. This command will list other Stardew-exclusive chat commands you can use.
+When you connect, you should see a message in the chat informing you of the `!!help` command. This command will list other
+Stardew-exclusive chat commands you can use.
-Furthermore, you can use the in-game chat box to talk to other players in the multiworld, assuming they are using a game that supports chatting.
+Furthermore, you can use the in-game chat box to talk to other players in the multiworld, assuming they are using a game
+that supports chatting.
-Lastly, you can also run Archipelago commands `!help` from the in game chat box, allowing you to request hints on certain items, or check missing locations.
+Lastly, you can also run Archipelago commands `!help` from the in game chat box, allowing you to request hints on certain
+items, or check missing locations.
-It is important to note that the Stardew Valley chat is fairly limited in its capabilities. For example, it doesn't allow scrolling up to see history that has been pushed off screen. The SMAPI console running alonside your game will have the full history as well and may be better suited to read older messages.
-For a better chat experience, you can also use the official Archipelago Text Client, altough it will not allow you to run Stardew-exclusive commands.
+It is important to note that the Stardew Valley chat is fairly limited in its capabilities. For example, it doesn't allow
+scrolling up to see history that has been pushed off screen. The SMAPI console running alonside your game will have the
+full history as well and may be better suited to read older messages.
+For a better chat experience, you can also use the official Archipelago Text Client, altough it will not allow you to run
+Stardew-exclusive commands.
### Playing with supported mods
-See the [Supported mods documentation](https://github.com/agilbert1412/StardewArchipelago/blob/4.x.x/Documentation/Supported%20Mods.md)
+See the [Supported mods documentation](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md)
### Multiplayer
-You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-terms plans to support that feature.
\ No newline at end of file
+You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-term plans to support that feature.
\ No newline at end of file
diff --git a/worlds/stardew_valley/early_items.py b/worlds/stardew_valley/early_items.py
new file mode 100644
index 000000000000..e1ad8cebfd4a
--- /dev/null
+++ b/worlds/stardew_valley/early_items.py
@@ -0,0 +1,83 @@
+from random import Random
+
+from . import options as stardew_options
+from .strings.ap_names.ap_weapon_names import APWeapon
+from .strings.ap_names.transport_names import Transportation
+from .strings.building_names import Building
+from .strings.region_names import Region
+from .strings.season_names import Season
+from .strings.tv_channel_names import Channel
+from .strings.wallet_item_names import Wallet
+
+early_candidate_rate = 4
+always_early_candidates = [Region.greenhouse, Transportation.desert_obelisk, Wallet.rusty_key]
+seasons = [Season.spring, Season.summer, Season.fall, Season.winter]
+
+
+def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions, player: int, random: Random):
+ early_forced = []
+ early_candidates = []
+ early_candidates.extend(always_early_candidates)
+
+ add_seasonal_candidates(early_candidates, options)
+
+ if options.building_progression & stardew_options.BuildingProgression.option_progressive:
+ early_forced.append(Building.shipping_bin)
+ if options.farm_type != stardew_options.FarmType.option_meadowlands:
+ early_candidates.append("Progressive Coop")
+ early_candidates.append("Progressive Barn")
+
+ if options.backpack_progression == stardew_options.BackpackProgression.option_early_progressive:
+ early_forced.append("Progressive Backpack")
+
+ if options.tool_progression & stardew_options.ToolProgression.option_progressive:
+ if options.fishsanity != stardew_options.Fishsanity.option_none:
+ early_candidates.append("Progressive Fishing Rod")
+ early_forced.append("Progressive Pickaxe")
+
+ if options.skill_progression == stardew_options.SkillProgression.option_progressive:
+ early_forced.append("Fishing Level")
+
+ if options.quest_locations >= 0:
+ early_candidates.append(Wallet.magnifying_glass)
+
+ if options.special_order_locations & stardew_options.SpecialOrderLocations.option_board:
+ early_candidates.append("Special Order Board")
+
+ if options.cooksanity != stardew_options.Cooksanity.option_none or options.chefsanity & stardew_options.Chefsanity.option_queen_of_sauce:
+ early_candidates.append(Channel.queen_of_sauce)
+
+ if options.craftsanity != stardew_options.Craftsanity.option_none:
+ early_candidates.append("Furnace Recipe")
+
+ if options.monstersanity == stardew_options.Monstersanity.option_none:
+ early_candidates.append(APWeapon.weapon)
+ else:
+ early_candidates.append(APWeapon.sword)
+
+ if options.exclude_ginger_island == stardew_options.ExcludeGingerIsland.option_false:
+ early_candidates.append(Transportation.island_obelisk)
+
+ if options.walnutsanity.value:
+ early_candidates.append("Island North Turtle")
+ early_candidates.append("Island West Turtle")
+
+ if options.museumsanity != stardew_options.Museumsanity.option_none or options.shipsanity >= stardew_options.Shipsanity.option_full_shipment:
+ early_candidates.append(Wallet.metal_detector)
+
+ early_forced.extend(random.sample(early_candidates, len(early_candidates) // early_candidate_rate))
+
+ for item_name in early_forced:
+ if item_name in multiworld.early_items[player]:
+ continue
+ multiworld.early_items[player][item_name] = 1
+
+
+def add_seasonal_candidates(early_candidates, options):
+ if options.season_randomization == stardew_options.SeasonRandomization.option_progressive:
+ early_candidates.extend([Season.progressive] * 3)
+ return
+ if options.season_randomization == stardew_options.SeasonRandomization.option_disabled:
+ return
+
+ early_candidates.extend(seasons)
diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py
index 1035997034da..31c7da5e3ade 100644
--- a/worlds/stardew_valley/items.py
+++ b/worlds/stardew_valley/items.py
@@ -7,11 +7,22 @@
from typing import Dict, List, Protocol, Union, Set, Optional
from BaseClasses import Item, ItemClassification
-from . import options, data
-from .data.villagers_data import all_villagers
+from . import data
+from .content.feature import friendsanity
+from .content.game_content import StardewContent
+from .data.game_item import ItemTag
+from .logic.logic_event import all_events
from .mods.mod_data import ModNames
-from .options import StardewOptions
+from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \
+ BuildingProgression, SkillProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \
+ Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs
+from .strings.ap_names.ap_option_names import OptionName
+from .strings.ap_names.ap_weapon_names import APWeapon
from .strings.ap_names.buff_names import Buff
+from .strings.ap_names.community_upgrade_names import CommunityUpgrade
+from .strings.ap_names.mods.mod_items import SVEQuestItem
+from .strings.currency_names import Currency
+from .strings.wallet_item_names import Wallet
ITEM_CODE_OFFSET = 717000
@@ -24,21 +35,21 @@ class Group(enum.Enum):
FRIENDSHIP_PACK = enum.auto()
COMMUNITY_REWARD = enum.auto()
TRASH = enum.auto()
- MINES_FLOOR_10 = enum.auto()
- MINES_FLOOR_20 = enum.auto()
- MINES_FLOOR_50 = enum.auto()
- MINES_FLOOR_60 = enum.auto()
- MINES_FLOOR_80 = enum.auto()
- MINES_FLOOR_90 = enum.auto()
- MINES_FLOOR_110 = enum.auto()
FOOTWEAR = enum.auto()
HATS = enum.auto()
RING = enum.auto()
WEAPON = enum.auto()
+ WEAPON_GENERIC = enum.auto()
+ WEAPON_SWORD = enum.auto()
+ WEAPON_CLUB = enum.auto()
+ WEAPON_DAGGER = enum.auto()
+ WEAPON_SLINGSHOT = enum.auto()
PROGRESSIVE_TOOLS = enum.auto()
SKILL_LEVEL_UP = enum.auto()
+ SKILL_MASTERY = enum.auto()
+ BUILDING = enum.auto()
+ WIZARD_BUILDING = enum.auto()
ARCADE_MACHINE_BUFFS = enum.auto()
- GALAXY_WEAPONS = enum.auto()
BASE_RESOURCE = enum.auto()
WARP_TOTEM = enum.auto()
GEODE = enum.auto()
@@ -54,6 +65,7 @@ class Group(enum.Enum):
FESTIVAL = enum.auto()
RARECROW = enum.auto()
TRAP = enum.auto()
+ BONUS = enum.auto()
MAXIMUM_ONE = enum.auto()
EXACTLY_TWO = enum.auto()
DEPRECATED = enum.auto()
@@ -64,7 +76,20 @@ class Group(enum.Enum):
GINGER_ISLAND = enum.auto()
WALNUT_PURCHASE = enum.auto()
TV_CHANNEL = enum.auto()
+ QI_CRAFTING_RECIPE = enum.auto()
+ CHEFSANITY = enum.auto()
+ CHEFSANITY_STARTER = enum.auto()
+ CHEFSANITY_QOS = enum.auto()
+ CHEFSANITY_PURCHASE = enum.auto()
+ CHEFSANITY_FRIENDSHIP = enum.auto()
+ CHEFSANITY_SKILL = enum.auto()
+ CRAFTSANITY = enum.auto()
+ BOOK_POWER = enum.auto()
+ LOST_BOOK = enum.auto()
+ PLAYER_BUFF = enum.auto()
+ # Mods
MAGIC_SPELL = enum.auto()
+ MOD_WARP = enum.auto()
@dataclass(frozen=True)
@@ -89,7 +114,12 @@ def has_any_group(self, *group: Group) -> bool:
class StardewItemFactory(Protocol):
- def __call__(self, name: Union[str, ItemData]) -> Item:
+ def __call__(self, name: Union[str, ItemData], override_classification: ItemClassification = None) -> Item:
+ raise NotImplementedError
+
+
+class StardewItemDeleter(Protocol):
+ def __call__(self, item: Item):
raise NotImplementedError
@@ -112,8 +142,8 @@ def load_item_csv():
events = [
- ItemData(None, "Victory", ItemClassification.progression),
- ItemData(None, "Month End", ItemClassification.progression),
+ ItemData(None, e, ItemClassification.progression)
+ for e in sorted(all_events)
]
all_items: List[ItemData] = load_item_csv() + events
@@ -137,210 +167,305 @@ def initialize_item_table():
initialize_groups()
-def create_items(item_factory: StardewItemFactory, locations_count: int, items_to_exclude: List[Item],
- world_options: StardewOptions,
- random: Random) -> List[Item]:
+def get_too_many_items_error_message(locations_count: int, items_count: int) -> str:
+ return f"There should be at least as many locations [{locations_count}] as there are mandatory items [{items_count}]"
+
+
+def create_items(item_factory: StardewItemFactory, item_deleter: StardewItemDeleter, locations_count: int, items_to_exclude: List[Item],
+ options: StardewValleyOptions, content: StardewContent, random: Random) -> List[Item]:
items = []
- unique_items = create_unique_items(item_factory, world_options, random)
+ unique_items = create_unique_items(item_factory, options, content, random)
+
+ remove_items(item_deleter, items_to_exclude, unique_items)
- for item in items_to_exclude:
- if item in unique_items:
- unique_items.remove(item)
+ remove_items_if_no_room_for_them(item_deleter, unique_items, locations_count, random)
- assert len(unique_items) <= locations_count, f"There should be at least as many locations [{locations_count}] as there are mandatory items [{len(unique_items)}]"
items += unique_items
logger.debug(f"Created {len(unique_items)} unique items")
- unique_filler_items = create_unique_filler_items(item_factory, world_options, random, locations_count - len(items))
+ unique_filler_items = create_unique_filler_items(item_factory, options, random, locations_count - len(items))
items += unique_filler_items
logger.debug(f"Created {len(unique_filler_items)} unique filler items")
- resource_pack_items = fill_with_resource_packs_and_traps(item_factory, world_options, random, items, locations_count)
+ resource_pack_items = fill_with_resource_packs_and_traps(item_factory, options, random, items, locations_count)
items += resource_pack_items
logger.debug(f"Created {len(resource_pack_items)} resource packs")
return items
-def create_unique_items(item_factory: StardewItemFactory, world_options: StardewOptions, random: Random) -> List[Item]:
+def remove_items(item_deleter: StardewItemDeleter, items_to_remove, items):
+ for item in items_to_remove:
+ if item in items:
+ items.remove(item)
+ item_deleter(item)
+
+
+def remove_items_if_no_room_for_them(item_deleter: StardewItemDeleter, unique_items: List[Item], locations_count: int, random: Random):
+ if len(unique_items) <= locations_count:
+ return
+
+ number_of_items_to_remove = len(unique_items) - locations_count
+ removable_items = [item for item in unique_items if item.classification == ItemClassification.filler or item.classification == ItemClassification.trap]
+ if len(removable_items) < number_of_items_to_remove:
+ logger.debug(f"Player has more items than locations, trying to remove {number_of_items_to_remove} random non-progression items")
+ removable_items = [item for item in unique_items if not item.classification & ItemClassification.progression]
+ else:
+ logger.debug(f"Player has more items than locations, trying to remove {number_of_items_to_remove} random filler items")
+ assert len(removable_items) >= number_of_items_to_remove, get_too_many_items_error_message(locations_count, len(unique_items))
+ items_to_remove = random.sample(removable_items, number_of_items_to_remove)
+ remove_items(item_deleter, items_to_remove, unique_items)
+
+
+def create_unique_items(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, random: Random) -> List[Item]:
items = []
items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD])
+ items.append(item_factory(CommunityUpgrade.movie_theater)) # It is a community reward, but we need two of them
+ create_raccoons(item_factory, options, items)
+ items.append(item_factory(Wallet.metal_detector)) # Always offer at least one metal detector
- create_backpack_items(item_factory, world_options, items)
- create_mine_rewards(item_factory, items, random)
- create_elevators(item_factory, world_options, items)
- create_tools(item_factory, world_options, items)
- create_skills(item_factory, world_options, items)
- create_wizard_buildings(item_factory, world_options, items)
- create_carpenter_buildings(item_factory, world_options, items)
+ create_backpack_items(item_factory, options, items)
+ create_weapons(item_factory, options, items)
+ items.append(item_factory("Skull Key"))
+ create_elevators(item_factory, options, items)
+ create_tools(item_factory, options, items)
+ create_skills(item_factory, options, items)
+ create_wizard_buildings(item_factory, options, items)
+ create_carpenter_buildings(item_factory, options, items)
+ items.append(item_factory("Railroad Boulder Removed"))
+ items.append(item_factory(CommunityUpgrade.fruit_bats))
+ items.append(item_factory(CommunityUpgrade.mushroom_boxes))
items.append(item_factory("Beach Bridge"))
- items.append(item_factory("Dark Talisman"))
- create_tv_channels(item_factory, items)
- create_special_quest_rewards(item_factory, items)
- create_stardrops(item_factory, world_options, items)
- create_museum_items(item_factory, world_options, items)
- create_arcade_machine_items(item_factory, world_options, items)
- items.append(item_factory(random.choice(items_by_group[Group.GALAXY_WEAPONS])))
- create_player_buffs(item_factory, world_options, items)
+ create_tv_channels(item_factory, options, items)
+ create_quest_rewards(item_factory, options, items)
+ create_stardrops(item_factory, options, content, items)
+ create_museum_items(item_factory, options, items)
+ create_arcade_machine_items(item_factory, options, items)
+ create_movement_buffs(item_factory, options, items)
create_traveling_merchant_items(item_factory, items)
items.append(item_factory("Return Scepter"))
- create_seasons(item_factory, world_options, items)
- create_seeds(item_factory, world_options, items)
- create_friendsanity_items(item_factory, world_options, items)
- create_festival_rewards(item_factory, world_options, items)
- create_babies(item_factory, items, random)
- create_special_order_board_rewards(item_factory, world_options, items)
- create_special_order_qi_rewards(item_factory, world_options, items)
- create_walnut_purchase_rewards(item_factory, world_options, items)
- create_magic_mod_spells(item_factory, world_options, items)
+ create_seasons(item_factory, options, items)
+ create_seeds(item_factory, content, items)
+ create_friendsanity_items(item_factory, options, content, items, random)
+ create_festival_rewards(item_factory, options, items)
+ create_special_order_board_rewards(item_factory, options, items)
+ create_special_order_qi_rewards(item_factory, options, items)
+ create_walnuts(item_factory, options, items)
+ create_walnut_purchase_rewards(item_factory, options, items)
+ create_crafting_recipes(item_factory, options, items)
+ create_cooking_recipes(item_factory, options, items)
+ create_shipsanity_items(item_factory, options, items)
+ create_booksanity_items(item_factory, content, items)
+ create_goal_items(item_factory, options, items)
+ items.append(item_factory("Golden Egg"))
+ items.append(item_factory(CommunityUpgrade.mr_qi_plane_ride))
+
+ create_sve_special_items(item_factory, options, items)
+ create_magic_mod_spells(item_factory, options, items)
+ create_deepwoods_pendants(item_factory, options, items)
+ create_archaeology_items(item_factory, options, items)
return items
-def create_backpack_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if (world_options[options.BackpackProgression] == options.BackpackProgression.option_progressive or
- world_options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive):
+def create_raccoons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ number_progressive_raccoons = 9
+ if options.quest_locations < 0:
+ number_progressive_raccoons = number_progressive_raccoons - 1
+
+ items.extend(item_factory(item) for item in [CommunityUpgrade.raccoon] * number_progressive_raccoons)
+
+
+def create_backpack_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if (options.backpack_progression == BackpackProgression.option_progressive or
+ options.backpack_progression == BackpackProgression.option_early_progressive):
items.extend(item_factory(item) for item in ["Progressive Backpack"] * 2)
- if ModNames.big_backpack in world_options[options.Mods]:
+ if ModNames.big_backpack in options.mods:
items.append(item_factory("Progressive Backpack"))
-def create_mine_rewards(item_factory: StardewItemFactory, items: List[Item], random: Random):
- items.append(item_factory("Rusty Sword"))
- items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_10])))
- items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_20])))
- items.append(item_factory("Slingshot"))
- items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_50])))
- items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_60])))
- items.append(item_factory("Master Slingshot"))
- items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_80])))
- items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_90])))
- items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_110])))
- items.append(item_factory("Skull Key"))
+def create_weapons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ weapons = weapons_count(options)
+ items.extend(item_factory(item) for item in [APWeapon.slingshot] * 2)
+ monstersanity = options.monstersanity
+ if monstersanity == Monstersanity.option_none: # Without monstersanity, might not be enough checks to split the weapons
+ items.extend(item_factory(item) for item in [APWeapon.weapon] * weapons)
+ items.extend(item_factory(item) for item in [APWeapon.footwear] * 3) # 1-2 | 3-4 | 6-7-8
+ return
+
+ items.extend(item_factory(item) for item in [APWeapon.sword] * weapons)
+ items.extend(item_factory(item) for item in [APWeapon.club] * weapons)
+ items.extend(item_factory(item) for item in [APWeapon.dagger] * weapons)
+ items.extend(item_factory(item) for item in [APWeapon.footwear] * 4) # 1-2 | 3-4 | 6-7-8 | 11-13
+ if monstersanity == Monstersanity.option_goals or monstersanity == Monstersanity.option_one_per_category or \
+ monstersanity == Monstersanity.option_short_goals or monstersanity == Monstersanity.option_very_short_goals:
+ return
+ if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
+ rings_items = [item for item in items_by_group[Group.RING] if item.classification is not ItemClassification.filler]
+ else:
+ rings_items = [item for item in items_by_group[Group.RING]]
+ items.extend(item_factory(item) for item in rings_items)
-def create_elevators(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if world_options[options.ElevatorProgression] == options.ElevatorProgression.option_vanilla:
+def create_elevators(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if options.elevator_progression == ElevatorProgression.option_vanilla:
return
items.extend([item_factory(item) for item in ["Progressive Mine Elevator"] * 24])
- if ModNames.deepwoods in world_options[options.Mods]:
+ if ModNames.deepwoods in options.mods:
items.extend([item_factory(item) for item in ["Progressive Woods Obelisk Sigils"] * 10])
- if ModNames.skull_cavern_elevator in world_options[options.Mods]:
+ if ModNames.skull_cavern_elevator in options.mods:
items.extend([item_factory(item) for item in ["Progressive Skull Cavern Elevator"] * 8])
-def create_tools(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if world_options[options.ToolProgression] == options.ToolProgression.option_progressive:
- items.extend(item_factory(item) for item in items_by_group[Group.PROGRESSIVE_TOOLS] * 4)
- items.append(item_factory("Golden Scythe"))
+def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if options.tool_progression & ToolProgression.option_progressive:
+ for item_data in items_by_group[Group.PROGRESSIVE_TOOLS]:
+ name = item_data.name
+ if "Trash Can" in name:
+ items.extend([item_factory(item) for item in [item_data] * 3])
+ items.append(item_factory(item_data, ItemClassification.useful))
+ else:
+ items.extend([item_factory(item) for item in [item_data] * 4])
+ if options.skill_progression == SkillProgression.option_progressive_with_masteries:
+ items.append(item_factory("Progressive Scythe"))
+ items.append(item_factory("Progressive Fishing Rod"))
+ items.append(item_factory("Progressive Scythe"))
-def create_skills(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if world_options[options.SkillProgression] == options.SkillProgression.option_progressive:
- for item in items_by_group[Group.SKILL_LEVEL_UP]:
- if item.mod_name not in world_options[options.Mods] and item.mod_name is not None:
- continue
- items.extend(item_factory(item) for item in [item.name] * 10)
+def create_skills(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if options.skill_progression == SkillProgression.option_vanilla:
+ return
+
+ for item in items_by_group[Group.SKILL_LEVEL_UP]:
+ if item.mod_name not in options.mods and item.mod_name is not None:
+ continue
+ items.extend(item_factory(item) for item in [item.name] * 10)
+
+ if options.skill_progression != SkillProgression.option_progressive_with_masteries:
+ return
+
+ for item in items_by_group[Group.SKILL_MASTERY]:
+ if item.mod_name not in options.mods and item.mod_name is not None:
+ continue
+ items.append(item_factory(item))
-def create_wizard_buildings(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- items.append(item_factory("Earth Obelisk"))
- items.append(item_factory("Water Obelisk"))
+def create_wizard_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ useless_buildings_classification = ItemClassification.progression_skip_balancing if world_is_perfection(options) else ItemClassification.useful
+ items.append(item_factory("Earth Obelisk", useless_buildings_classification))
+ items.append(item_factory("Water Obelisk", useless_buildings_classification))
items.append(item_factory("Desert Obelisk"))
items.append(item_factory("Junimo Hut"))
- items.append(item_factory("Gold Clock"))
- if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_false:
+ items.append(item_factory("Gold Clock", useless_buildings_classification))
+ if options.exclude_ginger_island == ExcludeGingerIsland.option_false:
items.append(item_factory("Island Obelisk"))
- if ModNames.deepwoods in world_options[options.Mods]:
+ if ModNames.deepwoods in options.mods:
items.append(item_factory("Woods Obelisk"))
-def create_carpenter_buildings(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if world_options[options.BuildingProgression] in {options.BuildingProgression.option_progressive,
- options.BuildingProgression.option_progressive_early_shipping_bin}:
- items.append(item_factory("Progressive Coop"))
- items.append(item_factory("Progressive Coop"))
- items.append(item_factory("Progressive Coop"))
- items.append(item_factory("Progressive Barn"))
- items.append(item_factory("Progressive Barn"))
- items.append(item_factory("Progressive Barn"))
- items.append(item_factory("Well"))
- items.append(item_factory("Silo"))
- items.append(item_factory("Mill"))
- items.append(item_factory("Progressive Shed"))
- items.append(item_factory("Progressive Shed"))
- items.append(item_factory("Fish Pond"))
- items.append(item_factory("Stable"))
- items.append(item_factory("Slime Hutch"))
- items.append(item_factory("Shipping Bin"))
- items.append(item_factory("Progressive House"))
- items.append(item_factory("Progressive House"))
- items.append(item_factory("Progressive House"))
- if ModNames.tractor in world_options[options.Mods]:
- items.append(item_factory("Tractor Garage"))
-
-
-def create_special_quest_rewards(item_factory: StardewItemFactory, items: List[Item]):
- items.append(item_factory("Adventurer's Guild"))
- items.append(item_factory("Club Card"))
- items.append(item_factory("Magnifying Glass"))
- items.append(item_factory("Bear's Knowledge"))
- items.append(item_factory("Iridium Snake Milk"))
-
-
-def create_stardrops(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- items.append(item_factory("Stardrop")) # The Mines level 100
- items.append(item_factory("Stardrop")) # Old Master Cannoli
- if world_options[options.Fishsanity] != options.Fishsanity.option_none:
- items.append(item_factory("Stardrop")) #Master Angler Stardrop
- if ModNames.deepwoods in world_options[options.Mods]:
- items.append(item_factory("Stardrop")) # Petting the Unicorn
-
-
-def create_museum_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if world_options[options.Museumsanity] == options.Museumsanity.option_none:
+def create_carpenter_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ building_option = options.building_progression
+ if not building_option & BuildingProgression.option_progressive:
return
- items.extend(item_factory(item) for item in ["Magic Rock Candy"] * 5)
- items.extend(item_factory(item) for item in ["Ancient Seeds"] * 5)
- items.extend(item_factory(item) for item in ["Traveling Merchant Metal Detector"] * 4)
+ items.append(item_factory("Progressive Coop"))
+ items.append(item_factory("Progressive Coop"))
+ items.append(item_factory("Progressive Coop"))
+ items.append(item_factory("Progressive Barn"))
+ items.append(item_factory("Progressive Barn"))
+ items.append(item_factory("Progressive Barn"))
+ items.append(item_factory("Well"))
+ items.append(item_factory("Silo"))
+ items.append(item_factory("Mill"))
+ items.append(item_factory("Progressive Shed"))
+ items.append(item_factory("Progressive Shed", ItemClassification.useful))
+ items.append(item_factory("Fish Pond"))
+ items.append(item_factory("Stable"))
+ items.append(item_factory("Slime Hutch"))
+ items.append(item_factory("Shipping Bin"))
+ items.append(item_factory("Progressive House"))
+ items.append(item_factory("Progressive House"))
+ items.append(item_factory("Progressive House"))
+ if ModNames.tractor in options.mods:
+ items.append(item_factory("Tractor Garage"))
+
+
+def create_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ create_special_quest_rewards(item_factory, options, items)
+ create_help_wanted_quest_rewards(item_factory, options, items)
+
+ create_quest_rewards_sve(item_factory, options, items)
+
+
+def create_special_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if options.quest_locations < 0:
+ return
+ # items.append(item_factory("Adventurer's Guild")) # Now unlocked always!
+ items.append(item_factory(Wallet.club_card))
+ items.append(item_factory(Wallet.magnifying_glass))
+ if ModNames.sve in options.mods:
+ items.append(item_factory(Wallet.bears_knowledge))
+ else:
+ items.append(item_factory(Wallet.bears_knowledge, ItemClassification.useful)) # Not necessary outside of SVE
+ items.append(item_factory(Wallet.iridium_snake_milk))
+ items.append(item_factory("Dark Talisman"))
+ if options.exclude_ginger_island == ExcludeGingerIsland.option_false:
+ items.append(item_factory("Fairy Dust Recipe"))
+
+
+def create_help_wanted_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if options.quest_locations <= 0:
+ return
+
+ number_help_wanted = options.quest_locations.value
+ quest_per_prize_ticket = 3
+ number_prize_tickets = number_help_wanted // quest_per_prize_ticket
+ items.extend(item_factory(item) for item in [Currency.prize_ticket] * number_prize_tickets)
+
+
+def create_stardrops(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item]):
+ stardrops_classification = get_stardrop_classification(options)
+ items.append(item_factory("Stardrop", stardrops_classification)) # The Mines level 100
+ items.append(item_factory("Stardrop", stardrops_classification)) # Old Master Cannoli
+ items.append(item_factory("Stardrop", stardrops_classification)) # Krobus Stardrop
+ if content.features.fishsanity.is_enabled:
+ items.append(item_factory("Stardrop", stardrops_classification)) # Master Angler Stardrop
+ if ModNames.deepwoods in options.mods:
+ items.append(item_factory("Stardrop", stardrops_classification)) # Petting the Unicorn
+ if content.features.friendsanity.is_enabled:
+ items.append(item_factory("Stardrop", stardrops_classification)) # Spouse Stardrop
+
+
+def create_museum_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ items.append(item_factory(Wallet.rusty_key))
+ items.append(item_factory(Wallet.dwarvish_translation_guide))
items.append(item_factory("Ancient Seeds Recipe"))
- items.append(item_factory("Stardrop"))
- items.append(item_factory("Rusty Key"))
- items.append(item_factory("Dwarvish Translation Guide"))
+ items.append(item_factory("Stardrop", get_stardrop_classification(options)))
+ if options.museumsanity == Museumsanity.option_none:
+ return
+ items.extend(item_factory(item) for item in ["Magic Rock Candy"] * 10)
+ items.extend(item_factory(item) for item in ["Ancient Seeds"] * 5)
+ items.append(item_factory(Wallet.metal_detector))
-def create_friendsanity_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if world_options[options.Friendsanity] == options.Friendsanity.option_none:
+def create_friendsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item], random: Random):
+ if not content.features.friendsanity.is_enabled:
return
- exclude_non_bachelors = world_options[options.Friendsanity] == options.Friendsanity.option_bachelors
- exclude_locked_villagers = world_options[options.Friendsanity] == options.Friendsanity.option_starting_npcs or \
- world_options[options.Friendsanity] == options.Friendsanity.option_bachelors
- include_post_marriage_hearts = world_options[options.Friendsanity] == options.Friendsanity.option_all_with_marriage
- exclude_ginger_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true
- heart_size = world_options[options.FriendsanityHeartSize]
- for villager in all_villagers:
- if villager.mod_name not in world_options[options.Mods] and villager.mod_name is not None:
- continue
- if not villager.available and exclude_locked_villagers:
- continue
- if not villager.bachelor and exclude_non_bachelors:
- continue
- if villager.name == "Leo" and exclude_ginger_island:
- continue
- heart_cap = 8 if villager.bachelor else 10
- if include_post_marriage_hearts and villager.bachelor:
- heart_cap = 14
- for heart in range(1, 15):
- if heart > heart_cap:
- break
- if heart % heart_size == 0 or heart == heart_cap:
- items.append(item_factory(f"{villager.name} <3"))
- if not exclude_non_bachelors:
- for heart in range(1, 6):
- if heart % heart_size == 0 or heart == 5:
- items.append(item_factory(f"Pet <3"))
+
+ create_babies(item_factory, items, random)
+
+ for villager in content.villagers.values():
+ item_name = friendsanity.to_item_name(villager.name)
+
+ for _ in content.features.friendsanity.get_randomized_hearts(villager):
+ items.append(item_factory(item_name, ItemClassification.progression))
+
+ need_pet = options.goal == Goal.option_grandpa_evaluation
+ pet_item_classification = ItemClassification.progression_skip_balancing if need_pet else ItemClassification.useful
+
+ for _ in content.features.friendsanity.get_pet_randomized_hearts():
+ items.append(item_factory(friendsanity.pet_heart_item_name, pet_item_classification))
def create_babies(item_factory: StardewItemFactory, items: List[Item], random: Random):
@@ -350,8 +475,8 @@ def create_babies(item_factory: StardewItemFactory, items: List[Item], random: R
items.append(item_factory(chosen_baby))
-def create_arcade_machine_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling:
+def create_arcade_machine_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if options.arcade_machine_locations == ArcadeMachineLocations.option_full_shuffling:
items.append(item_factory("JotPK: Progressive Boots"))
items.append(item_factory("JotPK: Progressive Boots"))
items.append(item_factory("JotPK: Progressive Gun"))
@@ -367,49 +492,74 @@ def create_arcade_machine_items(item_factory: StardewItemFactory, world_options:
items.extend(item_factory(item) for item in ["Junimo Kart: Extra Life"] * 8)
-def create_player_buffs(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]):
- number_of_movement_buffs: int = world_options[options.NumberOfMovementBuffs]
- number_of_luck_buffs: int = world_options[options.NumberOfLuckBuffs]
- items.extend(item_factory(item) for item in [Buff.movement] * number_of_movement_buffs)
- items.extend(item_factory(item) for item in [Buff.luck] * number_of_luck_buffs)
+def create_movement_buffs(item_factory, options: StardewValleyOptions, items: List[Item]):
+ movement_buffs: int = options.movement_buff_number.value
+ items.extend(item_factory(item) for item in [Buff.movement] * movement_buffs)
def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]):
items.extend([*(item_factory(item) for item in items_by_group[Group.TRAVELING_MERCHANT_DAY]),
- *(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6),
- *(item_factory(item) for item in ["Traveling Merchant Discount"] * 8)])
+ *(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6)])
-def create_seasons(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if world_options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled:
+def create_seasons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if options.season_randomization == SeasonRandomization.option_disabled:
return
- if world_options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive:
+ if options.season_randomization == SeasonRandomization.option_progressive:
items.extend([item_factory(item) for item in ["Progressive Season"] * 3])
return
items.extend([item_factory(item) for item in items_by_group[Group.SEASON]])
-def create_seeds(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if world_options[options.Cropsanity] == options.Cropsanity.option_disabled:
+def create_seeds(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]):
+ if not content.features.cropsanity.is_enabled:
return
- include_ginger_island = world_options[options.ExcludeGingerIsland] != options.ExcludeGingerIsland.option_true
- seed_items = [item_factory(item) for item in items_by_group[Group.CROPSANITY] if include_ginger_island or Group.GINGER_ISLAND not in item.groups]
- items.extend(seed_items)
+ items.extend(item_factory(item_table[seed.name]) for seed in content.find_tagged_items(ItemTag.CROPSANITY_SEED))
-def create_festival_rewards(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if world_options[options.FestivalLocations] == options.FestivalLocations.option_disabled:
+def create_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ items.append(item_factory("Deluxe Scarecrow Recipe"))
+ if options.festival_locations == FestivalLocations.option_disabled:
return
- items.extend([*[item_factory(item) for item in items_by_group[Group.FESTIVAL] if item.classification != ItemClassification.filler],
- item_factory("Stardrop")])
+ festival_rewards = [item_factory(item) for item in items_by_group[Group.FESTIVAL] if item.classification != ItemClassification.filler]
+ items.extend([*festival_rewards, item_factory("Stardrop", get_stardrop_classification(options))])
+
+def create_walnuts(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ walnutsanity = options.walnutsanity
+ if options.exclude_ginger_island == ExcludeGingerIsland.option_true or walnutsanity == Walnutsanity.preset_none:
+ return
-def create_walnut_purchase_rewards(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
+ # Give baseline walnuts just to be nice
+ num_single_walnuts = 0
+ num_triple_walnuts = 2
+ num_penta_walnuts = 1
+ # https://stardewvalleywiki.com/Golden_Walnut
+ # Totals should be accurate, but distribution is slightly offset to make room for baseline walnuts
+ if OptionName.walnutsanity_puzzles in walnutsanity: # 61
+ num_single_walnuts += 6 # 6
+ num_triple_walnuts += 5 # 15
+ num_penta_walnuts += 8 # 40
+ if OptionName.walnutsanity_bushes in walnutsanity: # 25
+ num_single_walnuts += 16 # 16
+ num_triple_walnuts += 3 # 9
+ if OptionName.walnutsanity_dig_spots in walnutsanity: # 18
+ num_single_walnuts += 18 # 18
+ if OptionName.walnutsanity_repeatables in walnutsanity: # 33
+ num_single_walnuts += 30 # 30
+ num_triple_walnuts += 1 # 3
+
+ items.extend([item_factory(item) for item in ["Golden Walnut"] * num_single_walnuts])
+ items.extend([item_factory(item) for item in ["3 Golden Walnuts"] * num_triple_walnuts])
+ items.extend([item_factory(item) for item in ["5 Golden Walnuts"] * num_penta_walnuts])
+
+
+def create_walnut_purchase_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return
items.extend([item_factory("Boat Repair"),
@@ -419,90 +569,215 @@ def create_walnut_purchase_rewards(item_factory: StardewItemFactory, world_optio
*[item_factory(item) for item in items_by_group[Group.WALNUT_PURCHASE]]])
+def create_special_order_board_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if options.special_order_locations & SpecialOrderLocations.option_board:
+ special_order_board_items = [item for item in items_by_group[Group.SPECIAL_ORDER_BOARD]]
+ items.extend([item_factory(item) for item in special_order_board_items])
-def create_special_order_board_rewards(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled:
- return
- items.extend([item_factory(item) for item in items_by_group[Group.SPECIAL_ORDER_BOARD]])
+def special_order_board_item_classification(item: ItemData, need_all_recipes: bool) -> ItemClassification:
+ if item.classification is ItemClassification.useful:
+ return ItemClassification.useful
+ if item.name == "Special Order Board":
+ return ItemClassification.progression
+ if need_all_recipes and "Recipe" in item.name:
+ return ItemClassification.progression_skip_balancing
+ if item.name == "Monster Musk Recipe":
+ return ItemClassification.progression_skip_balancing
+ return ItemClassification.useful
-def create_special_order_qi_rewards(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if (world_options[options.SpecialOrderLocations] != options.SpecialOrderLocations.option_board_qi or
- world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true):
+def create_special_order_qi_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return
- qi_gem_rewards = ["100 Qi Gems", "10 Qi Gems", "40 Qi Gems", "25 Qi Gems", "25 Qi Gems",
- "40 Qi Gems", "20 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems"]
+ qi_gem_rewards = []
+ if options.bundle_randomization >= BundleRandomization.option_remixed:
+ qi_gem_rewards.append("15 Qi Gems")
+ qi_gem_rewards.append("15 Qi Gems")
+
+ if options.special_order_locations & SpecialOrderLocations.value_qi:
+ qi_gem_rewards.extend(["100 Qi Gems", "10 Qi Gems", "40 Qi Gems", "25 Qi Gems", "25 Qi Gems",
+ "40 Qi Gems", "20 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems"])
+
qi_gem_items = [item_factory(reward) for reward in qi_gem_rewards]
items.extend(qi_gem_items)
-def create_tv_channels(item_factory: StardewItemFactory, items: List[Item]):
- items.extend([item_factory(item) for item in items_by_group[Group.TV_CHANNEL]])
+def create_tv_channels(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ channels = [channel for channel in items_by_group[Group.TV_CHANNEL]]
+ if options.entrance_randomization == EntranceRandomization.option_disabled:
+ channels = [channel for channel in channels if channel.name != "The Gateway Gazette"]
+ items.extend([item_factory(item) for item in channels])
+
+def create_crafting_recipes(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ has_craftsanity = options.craftsanity == Craftsanity.option_all
+ crafting_recipes = []
+ crafting_recipes.extend([recipe for recipe in items_by_group[Group.QI_CRAFTING_RECIPE]])
+ if has_craftsanity:
+ crafting_recipes.extend([recipe for recipe in items_by_group[Group.CRAFTSANITY]])
+ crafting_recipes = remove_excluded_items(crafting_recipes, options)
+ items.extend([item_factory(item) for item in crafting_recipes])
-def create_filler_festival_rewards(item_factory: StardewItemFactory, world_options: StardewOptions) -> List[Item]:
- if world_options[options.FestivalLocations] == options.FestivalLocations.option_disabled:
+
+def create_cooking_recipes(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ chefsanity = options.chefsanity
+ if chefsanity == Chefsanity.option_none:
+ return
+
+ chefsanity_recipes_by_name = {recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_STARTER]} # Dictionary to not make duplicates
+
+ if chefsanity & Chefsanity.option_queen_of_sauce:
+ chefsanity_recipes_by_name.update({recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_QOS]})
+ if chefsanity & Chefsanity.option_purchases:
+ chefsanity_recipes_by_name.update({recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_PURCHASE]})
+ if chefsanity & Chefsanity.option_friendship:
+ chefsanity_recipes_by_name.update({recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_FRIENDSHIP]})
+ if chefsanity & Chefsanity.option_skills:
+ chefsanity_recipes_by_name.update({recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_SKILL]})
+
+ filtered_chefsanity_recipes = remove_excluded_items(list(chefsanity_recipes_by_name.values()), options)
+ items.extend([item_factory(item) for item in filtered_chefsanity_recipes])
+
+
+def create_shipsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ shipsanity = options.shipsanity
+ if shipsanity != Shipsanity.option_everything:
+ return
+
+ items.append(item_factory(Wallet.metal_detector))
+
+
+def create_booksanity_items(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]):
+ booksanity = content.features.booksanity
+ if not booksanity.is_enabled:
+ return
+
+ items.extend(item_factory(item_table[booksanity.to_item_name(book.name)]) for book in content.find_tagged_items(ItemTag.BOOK_POWER))
+ progressive_lost_book = item_table[booksanity.progressive_lost_book]
+ items.extend(item_factory(progressive_lost_book) for _ in content.features.booksanity.get_randomized_lost_books())
+
+
+def create_goal_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ goal = options.goal
+ if goal != Goal.option_perfection and goal != Goal.option_complete_collection:
+ return
+
+ items.append(item_factory(Wallet.metal_detector))
+
+
+def create_archaeology_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ mods = options.mods
+ if ModNames.archaeology not in mods:
+ return
+
+ items.append(item_factory(Wallet.metal_detector))
+
+
+def create_filler_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions) -> List[Item]:
+ if options.festival_locations == FestivalLocations.option_disabled:
return []
return [item_factory(item) for item in items_by_group[Group.FESTIVAL] if
item.classification == ItemClassification.filler]
-def create_magic_mod_spells(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]):
- if ModNames.magic not in world_options[options.Mods]:
- return []
+def create_magic_mod_spells(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if ModNames.magic not in options.mods:
+ return
items.extend([item_factory(item) for item in items_by_group[Group.MAGIC_SPELL]])
-def create_unique_filler_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random,
+def create_deepwoods_pendants(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if ModNames.deepwoods not in options.mods:
+ return
+ items.extend([item_factory(item) for item in ["Pendant of Elders", "Pendant of Community", "Pendant of Depths"]])
+
+
+def create_sve_special_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if ModNames.sve not in options.mods:
+ return
+
+ items.extend([item_factory(item) for item in items_by_group[Group.MOD_WARP] if item.mod_name == ModNames.sve])
+
+
+def create_quest_rewards_sve(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
+ if ModNames.sve not in options.mods:
+ return
+
+ exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true
+ items.extend([item_factory(item) for item in SVEQuestItem.sve_always_quest_items])
+ if not exclude_ginger_island:
+ items.extend([item_factory(item) for item in SVEQuestItem.sve_always_quest_items_ginger_island])
+
+ if options.quest_locations < 0:
+ return
+
+ items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items])
+ if exclude_ginger_island:
+ return
+ items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items_ginger_island])
+
+
+def create_unique_filler_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random,
available_item_slots: int) -> List[Item]:
items = []
- items.extend(create_filler_festival_rewards(item_factory, world_options))
+ items.extend(create_filler_festival_rewards(item_factory, options))
if len(items) > available_item_slots:
items = random.sample(items, available_item_slots)
return items
-def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random,
+def weapons_count(options: StardewValleyOptions):
+ weapon_count = 5
+ if ModNames.sve in options.mods:
+ weapon_count += 1
+ return weapon_count
+
+
+def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random,
items_already_added: List[Item],
number_locations: int) -> List[Item]:
- include_traps = world_options[options.TrapItems] != options.TrapItems.option_no_traps
- all_filler_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK]]
- all_filler_packs.extend(items_by_group[Group.TRASH])
- if include_traps:
- all_filler_packs.extend(items_by_group[Group.TRAP])
+ include_traps = options.trap_items != TrapItems.option_no_traps
items_already_added_names = [item.name for item in items_already_added]
useful_resource_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK_USEFUL]
if pack.name not in items_already_added_names]
- trap_items = [pack for pack in items_by_group[Group.TRAP]
- if pack.name not in items_already_added_names and
- (pack.mod_name is None or pack.mod_name in world_options[options.Mods])]
+ trap_items = [trap for trap in items_by_group[Group.TRAP]
+ if trap.name not in items_already_added_names and
+ (trap.mod_name is None or trap.mod_name in options.mods)]
+ player_buffs = get_allowed_player_buffs(options.enabled_filler_buffs)
priority_filler_items = []
priority_filler_items.extend(useful_resource_packs)
+ priority_filler_items.extend(player_buffs)
+
if include_traps:
priority_filler_items.extend(trap_items)
- all_filler_packs = remove_excluded_packs(all_filler_packs, world_options)
- priority_filler_items = remove_excluded_packs(priority_filler_items, world_options)
+ exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true
+ all_filler_packs = remove_excluded_items(get_all_filler_items(include_traps, exclude_ginger_island), options)
+ all_filler_packs.extend(player_buffs)
+ priority_filler_items = remove_excluded_items(priority_filler_items, options)
number_priority_items = len(priority_filler_items)
required_resource_pack = number_locations - len(items_already_added)
if required_resource_pack < number_priority_items:
chosen_priority_items = [item_factory(resource_pack) for resource_pack in
- random.sample(priority_filler_items, required_resource_pack)]
+ random.sample(priority_filler_items, required_resource_pack)]
return chosen_priority_items
items = []
- chosen_priority_items = [item_factory(resource_pack) for resource_pack in priority_filler_items]
+ chosen_priority_items = [item_factory(resource_pack,
+ ItemClassification.trap if resource_pack.classification == ItemClassification.trap else ItemClassification.useful)
+ for resource_pack in priority_filler_items]
items.extend(chosen_priority_items)
required_resource_pack -= number_priority_items
all_filler_packs = [filler_pack for filler_pack in all_filler_packs
if Group.MAXIMUM_ONE not in filler_pack.groups or
- filler_pack.name not in [priority_item.name for priority_item in priority_filler_items]]
+ (filler_pack.name not in [priority_item.name for priority_item in
+ priority_filler_items] and filler_pack.name not in items_already_added_names)]
while required_resource_pack > 0:
resource_pack = random.choice(all_filler_packs)
@@ -510,10 +785,11 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, world_o
while exactly_2 and required_resource_pack == 1:
resource_pack = random.choice(all_filler_packs)
exactly_2 = Group.EXACTLY_TWO in resource_pack.groups
- items.append(item_factory(resource_pack))
+ classification = ItemClassification.useful if resource_pack.classification == ItemClassification.progression else resource_pack.classification
+ items.append(item_factory(resource_pack, classification))
required_resource_pack -= 1
if exactly_2:
- items.append(item_factory(resource_pack))
+ items.append(item_factory(resource_pack, classification))
required_resource_pack -= 1
if exactly_2 or Group.MAXIMUM_ONE in resource_pack.groups:
all_filler_packs.remove(resource_pack)
@@ -521,8 +797,76 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, world_o
return items
-def remove_excluded_packs(packs, world_options):
- included_packs = [pack for pack in packs if Group.DEPRECATED not in pack.groups]
- if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
- included_packs = [pack for pack in included_packs if Group.GINGER_ISLAND not in pack.groups]
- return included_packs
+def filter_deprecated_items(items: List[ItemData]) -> List[ItemData]:
+ return [item for item in items if Group.DEPRECATED not in item.groups]
+
+
+def filter_ginger_island_items(exclude_island: bool, items: List[ItemData]) -> List[ItemData]:
+ return [item for item in items if not exclude_island or Group.GINGER_ISLAND not in item.groups]
+
+
+def filter_mod_items(mods: Set[str], items: List[ItemData]) -> List[ItemData]:
+ return [item for item in items if item.mod_name is None or item.mod_name in mods]
+
+
+def remove_excluded_items(items, options: StardewValleyOptions):
+ return remove_excluded_items_island_mods(items, options.exclude_ginger_island == ExcludeGingerIsland.option_true, options.mods.value)
+
+
+def remove_excluded_items_island_mods(items, exclude_ginger_island: bool, mods: Set[str]):
+ deprecated_filter = filter_deprecated_items(items)
+ ginger_island_filter = filter_ginger_island_items(exclude_ginger_island, deprecated_filter)
+ mod_filter = filter_mod_items(mods, ginger_island_filter)
+ return mod_filter
+
+
+def remove_limited_amount_packs(packs):
+ return [pack for pack in packs if Group.MAXIMUM_ONE not in pack.groups and Group.EXACTLY_TWO not in pack.groups]
+
+
+def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool) -> List[ItemData]:
+ all_filler_items = [pack for pack in items_by_group[Group.RESOURCE_PACK]]
+ all_filler_items.extend(items_by_group[Group.TRASH])
+ if include_traps:
+ all_filler_items.extend(items_by_group[Group.TRAP])
+ all_filler_items = remove_excluded_items_island_mods(all_filler_items, exclude_ginger_island, set())
+ return all_filler_items
+
+
+def get_allowed_player_buffs(buff_option: EnabledFillerBuffs) -> List[ItemData]:
+ allowed_buffs = []
+ if OptionName.buff_luck in buff_option:
+ allowed_buffs.append(item_table[Buff.luck])
+ if OptionName.buff_damage in buff_option:
+ allowed_buffs.append(item_table[Buff.damage])
+ if OptionName.buff_defense in buff_option:
+ allowed_buffs.append(item_table[Buff.defense])
+ if OptionName.buff_immunity in buff_option:
+ allowed_buffs.append(item_table[Buff.immunity])
+ if OptionName.buff_health in buff_option:
+ allowed_buffs.append(item_table[Buff.health])
+ if OptionName.buff_energy in buff_option:
+ allowed_buffs.append(item_table[Buff.energy])
+ if OptionName.buff_bite in buff_option:
+ allowed_buffs.append(item_table[Buff.bite_rate])
+ if OptionName.buff_fish_trap in buff_option:
+ allowed_buffs.append(item_table[Buff.fish_trap])
+ if OptionName.buff_fishing_bar in buff_option:
+ allowed_buffs.append(item_table[Buff.fishing_bar])
+ if OptionName.buff_quality in buff_option:
+ allowed_buffs.append(item_table[Buff.quality])
+ if OptionName.buff_glow in buff_option:
+ allowed_buffs.append(item_table[Buff.glow])
+ return allowed_buffs
+
+
+def get_stardrop_classification(options) -> ItemClassification:
+ return ItemClassification.progression_skip_balancing if world_is_perfection(options) or world_is_stardrops(options) else ItemClassification.useful
+
+
+def world_is_perfection(options) -> bool:
+ return options.goal == Goal.option_perfection
+
+
+def world_is_stardrops(options) -> bool:
+ return options.goal == Goal.option_mystery_of_the_stardrops
diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py
index 67bffa139632..43246a94a356 100644
--- a/worlds/stardew_valley/locations.py
+++ b/worlds/stardew_valley/locations.py
@@ -2,14 +2,21 @@
import enum
from dataclasses import dataclass
from random import Random
-from typing import Optional, Dict, Protocol, List, FrozenSet
+from typing import Optional, Dict, Protocol, List, FrozenSet, Iterable
-from . import options, data
-from .data.fish_data import legendary_fish, special_fish, all_fish
+from . import data
+from .bundles.bundle_room import BundleRoom
+from .content.game_content import StardewContent
+from .data.game_item import ItemTag
from .data.museum_data import all_museum_items
-from .data.villagers_data import all_villagers
+from .mods.mod_data import ModNames
+from .options import ExcludeGingerIsland, ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \
+ FestivalLocations, SkillProgression, BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression, FarmType
+from .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity
from .strings.goal_names import Goal
-from .strings.region_names import Region
+from .strings.quest_names import ModQuest, Quest
+from .strings.region_names import Region, LogicRegion
+from .strings.villager_names import NPC
LOCATION_CODE_OFFSET = 717000
@@ -25,6 +32,7 @@ class LocationTags(enum.Enum):
BULLETIN_BOARD_BUNDLE = enum.auto()
VAULT_BUNDLE = enum.auto()
COMMUNITY_CENTER_ROOM = enum.auto()
+ RACCOON_BUNDLES = enum.auto()
BACKPACK = enum.auto()
TOOL_UPGRADE = enum.auto()
HOE_UPGRADE = enum.auto()
@@ -33,6 +41,7 @@ class LocationTags(enum.Enum):
WATERING_CAN_UPGRADE = enum.auto()
TRASH_CAN_UPGRADE = enum.auto()
FISHING_ROD_UPGRADE = enum.auto()
+ PAN_UPGRADE = enum.auto()
THE_MINES_TREASURE = enum.auto()
CROPSANITY = enum.auto()
ELEVATOR = enum.auto()
@@ -42,8 +51,9 @@ class LocationTags(enum.Enum):
FORAGING_LEVEL = enum.auto()
COMBAT_LEVEL = enum.auto()
MINING_LEVEL = enum.auto()
+ MASTERY_LEVEL = enum.auto()
BUILDING_BLUEPRINT = enum.auto()
- QUEST = enum.auto()
+ STORY_QUEST = enum.auto()
ARCADE_MACHINE = enum.auto()
ARCADE_MACHINE_VICTORY = enum.auto()
JOTPK = enum.auto()
@@ -56,10 +66,42 @@ class LocationTags(enum.Enum):
FRIENDSANITY = enum.auto()
FESTIVAL = enum.auto()
FESTIVAL_HARD = enum.auto()
+ DESERT_FESTIVAL_CHEF = enum.auto()
SPECIAL_ORDER_BOARD = enum.auto()
SPECIAL_ORDER_QI = enum.auto()
+ REQUIRES_QI_ORDERS = enum.auto()
+ REQUIRES_MASTERIES = enum.auto()
GINGER_ISLAND = enum.auto()
WALNUT_PURCHASE = enum.auto()
+ WALNUTSANITY = enum.auto()
+ WALNUTSANITY_PUZZLE = enum.auto()
+ WALNUTSANITY_BUSH = enum.auto()
+ WALNUTSANITY_DIG = enum.auto()
+ WALNUTSANITY_REPEATABLE = enum.auto()
+
+ BABY = enum.auto()
+ MONSTERSANITY = enum.auto()
+ MONSTERSANITY_GOALS = enum.auto()
+ MONSTERSANITY_PROGRESSIVE_GOALS = enum.auto()
+ MONSTERSANITY_MONSTER = enum.auto()
+ SHIPSANITY = enum.auto()
+ SHIPSANITY_CROP = enum.auto()
+ SHIPSANITY_FISH = enum.auto()
+ SHIPSANITY_FULL_SHIPMENT = enum.auto()
+ COOKSANITY = enum.auto()
+ COOKSANITY_QOS = enum.auto()
+ CHEFSANITY = enum.auto()
+ CHEFSANITY_QOS = enum.auto()
+ CHEFSANITY_PURCHASE = enum.auto()
+ CHEFSANITY_FRIENDSHIP = enum.auto()
+ CHEFSANITY_SKILL = enum.auto()
+ CHEFSANITY_STARTER = enum.auto()
+ CRAFTSANITY = enum.auto()
+ BOOKSANITY = enum.auto()
+ BOOKSANITY_POWER = enum.auto()
+ BOOKSANITY_SKILL = enum.auto()
+ BOOKSANITY_LOST = enum.auto()
+ # Mods
# Skill Mods
LUCK_LEVEL = enum.auto()
BINNING_LEVEL = enum.auto()
@@ -110,10 +152,17 @@ def load_location_csv() -> List[LocationData]:
LocationData(None, Region.community_center, Goal.community_center),
LocationData(None, Region.mines_floor_120, Goal.bottom_of_the_mines),
LocationData(None, Region.skull_cavern_100, Goal.cryptic_note),
- LocationData(None, Region.farm, Goal.master_angler),
+ LocationData(None, Region.beach, Goal.master_angler),
LocationData(None, Region.museum, Goal.complete_museum),
LocationData(None, Region.farm_house, Goal.full_house),
LocationData(None, Region.island_west, Goal.greatest_walnut_hunter),
+ LocationData(None, Region.adventurer_guild, Goal.protector_of_the_valley),
+ LocationData(None, LogicRegion.shipping, Goal.full_shipment),
+ LocationData(None, LogicRegion.kitchen, Goal.gourmet_chef),
+ LocationData(None, Region.farm, Goal.craft_master),
+ LocationData(None, LogicRegion.shipping, Goal.legend),
+ LocationData(None, Region.farm, Goal.mystery_of_the_stardrops),
+ LocationData(None, Region.farm, Goal.allsanity),
LocationData(None, Region.qi_walnut_room, Goal.perfection),
]
@@ -133,17 +182,24 @@ def initialize_groups():
initialize_groups()
-def extend_cropsanity_locations(randomized_locations: List[LocationData], world_options):
- if world_options[options.Cropsanity] == options.Cropsanity.option_disabled:
+def extend_cropsanity_locations(randomized_locations: List[LocationData], content: StardewContent):
+ cropsanity = content.features.cropsanity
+ if not cropsanity.is_enabled:
return
- cropsanity_locations = locations_by_tag[LocationTags.CROPSANITY]
- cropsanity_locations = filter_ginger_island(world_options, cropsanity_locations)
- randomized_locations.extend(cropsanity_locations)
+ randomized_locations.extend(location_table[cropsanity.to_location_name(item.name)]
+ for item in content.find_tagged_items(ItemTag.CROPSANITY))
-def extend_help_wanted_quests(randomized_locations: List[LocationData], desired_number_of_quests: int):
- for i in range(0, desired_number_of_quests):
+def extend_quests_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
+ if options.quest_locations < 0:
+ return
+
+ story_quest_locations = locations_by_tag[LocationTags.STORY_QUEST]
+ story_quest_locations = filter_disabled_locations(options, story_quest_locations)
+ randomized_locations.extend(story_quest_locations)
+
+ for i in range(0, options.quest_locations.value):
batch = i // 7
index_this_batch = i % 7
if index_this_batch < 4:
@@ -157,109 +213,95 @@ def extend_help_wanted_quests(randomized_locations: List[LocationData], desired_
randomized_locations.append(location_table[f"Help Wanted: Gathering {batch + 1}"])
-def extend_fishsanity_locations(randomized_locations: List[LocationData], world_options, random: Random):
- prefix = "Fishsanity: "
- if world_options[options.Fishsanity] == options.Fishsanity.option_none:
+def extend_fishsanity_locations(randomized_locations: List[LocationData], content: StardewContent, random: Random):
+ fishsanity = content.features.fishsanity
+ if not fishsanity.is_enabled:
return
- elif world_options[options.Fishsanity] == options.Fishsanity.option_legendaries:
- randomized_locations.extend(location_table[f"{prefix}{legendary.name}"] for legendary in legendary_fish)
- elif world_options[options.Fishsanity] == options.Fishsanity.option_special:
- randomized_locations.extend(location_table[f"{prefix}{special.name}"] for special in special_fish)
- elif world_options[options.Fishsanity] == options.Fishsanity.option_randomized:
- fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if random.random() < 0.4]
- randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
- elif world_options[options.Fishsanity] == options.Fishsanity.option_all:
- fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish]
- randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
- elif world_options[options.Fishsanity] == options.Fishsanity.option_exclude_legendaries:
- fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if fish not in legendary_fish]
- randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
- elif world_options[options.Fishsanity] == options.Fishsanity.option_exclude_hard_fish:
- fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if fish.difficulty < 80]
- randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
- elif world_options[options.Fishsanity] == options.Fishsanity.option_only_easy_fish:
- fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if fish.difficulty < 50]
- randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
-
-
-def extend_museumsanity_locations(randomized_locations: List[LocationData], museumsanity: int, random: Random):
+
+ for fish in content.fishes.values():
+ if not fishsanity.is_included(fish):
+ continue
+
+ if fishsanity.is_randomized and random.random() >= fishsanity.randomization_ratio:
+ continue
+
+ randomized_locations.append(location_table[fishsanity.to_location_name(fish.name)])
+
+
+def extend_museumsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random):
prefix = "Museumsanity: "
- if museumsanity == options.Museumsanity.option_none:
+ if options.museumsanity == Museumsanity.option_none:
return
- elif museumsanity == options.Museumsanity.option_milestones:
+ elif options.museumsanity == Museumsanity.option_milestones:
randomized_locations.extend(locations_by_tag[LocationTags.MUSEUM_MILESTONES])
- elif museumsanity == options.Museumsanity.option_randomized:
- randomized_locations.extend(location_table[f"{prefix}{museum_item.name}"]
+ elif options.museumsanity == Museumsanity.option_randomized:
+ randomized_locations.extend(location_table[f"{prefix}{museum_item.item_name}"]
for museum_item in all_museum_items if random.random() < 0.4)
- elif museumsanity == options.Museumsanity.option_all:
- randomized_locations.extend(location_table[f"{prefix}{museum_item.name}"] for museum_item in all_museum_items)
+ elif options.museumsanity == Museumsanity.option_all:
+ randomized_locations.extend(location_table[f"{prefix}{museum_item.item_name}"] for museum_item in all_museum_items)
-def extend_friendsanity_locations(randomized_locations: List[LocationData], world_options: options.StardewOptions):
- if world_options[options.Friendsanity] == options.Friendsanity.option_none:
+def extend_friendsanity_locations(randomized_locations: List[LocationData], content: StardewContent):
+ friendsanity = content.features.friendsanity
+ if not friendsanity.is_enabled:
return
- exclude_leo = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true
- exclude_non_bachelors = world_options[options.Friendsanity] == options.Friendsanity.option_bachelors
- exclude_locked_villagers = world_options[options.Friendsanity] == options.Friendsanity.option_starting_npcs or \
- world_options[options.Friendsanity] == options.Friendsanity.option_bachelors
- include_post_marriage_hearts = world_options[options.Friendsanity] == options.Friendsanity.option_all_with_marriage
- heart_size = world_options[options.FriendsanityHeartSize]
- for villager in all_villagers:
- if villager.mod_name not in world_options[options.Mods] and villager.mod_name is not None:
- continue
- if not villager.available and exclude_locked_villagers:
- continue
- if not villager.bachelor and exclude_non_bachelors:
- continue
- if villager.name == "Leo" and exclude_leo:
- continue
- heart_cap = 8 if villager.bachelor else 10
- if include_post_marriage_hearts and villager.bachelor:
- heart_cap = 14
- for heart in range(1, 15):
- if heart > heart_cap:
- break
- if heart % heart_size == 0 or heart == heart_cap:
- randomized_locations.append(location_table[f"Friendsanity: {villager.name} {heart} <3"])
- if not exclude_non_bachelors:
- for heart in range(1, 6):
- if heart % heart_size == 0 or heart == 5:
- randomized_locations.append(location_table[f"Friendsanity: Pet {heart} <3"])
-
-
-def extend_festival_locations(randomized_locations: List[LocationData], festival_option: int):
- if festival_option == options.FestivalLocations.option_disabled:
+ randomized_locations.append(location_table[f"Spouse Stardrop"])
+ extend_baby_locations(randomized_locations)
+
+ for villager in content.villagers.values():
+ for heart in friendsanity.get_randomized_hearts(villager):
+ randomized_locations.append(location_table[friendsanity.to_location_name(villager.name, heart)])
+
+ for heart in friendsanity.get_pet_randomized_hearts():
+ randomized_locations.append(location_table[friendsanity.to_location_name(NPC.pet, heart)])
+
+
+def extend_baby_locations(randomized_locations: List[LocationData]):
+ baby_locations = [location for location in locations_by_tag[LocationTags.BABY]]
+ randomized_locations.extend(baby_locations)
+
+
+def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random):
+ if options.festival_locations == FestivalLocations.option_disabled:
return
festival_locations = locations_by_tag[LocationTags.FESTIVAL]
randomized_locations.extend(festival_locations)
- extend_hard_festival_locations(randomized_locations, festival_option)
+ extend_hard_festival_locations(randomized_locations, options)
+ extend_desert_festival_chef_locations(randomized_locations, options, random)
-def extend_hard_festival_locations(randomized_locations, festival_option: int):
- if festival_option != options.FestivalLocations.option_hard:
+def extend_hard_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
+ if options.festival_locations != FestivalLocations.option_hard:
return
hard_festival_locations = locations_by_tag[LocationTags.FESTIVAL_HARD]
randomized_locations.extend(hard_festival_locations)
-def extend_special_order_locations(randomized_locations: List[LocationData], world_options):
- if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled:
- return
+def extend_desert_festival_chef_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random):
+ festival_chef_locations = locations_by_tag[LocationTags.DESERT_FESTIVAL_CHEF]
+ number_to_add = 5 if options.festival_locations == FestivalLocations.option_easy else 10
+ locations_to_add = random.sample(festival_chef_locations, number_to_add)
+ randomized_locations.extend(locations_to_add)
+
+
+def extend_special_order_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
+ if options.special_order_locations & SpecialOrderLocations.option_board:
+ board_locations = filter_disabled_locations(options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD])
+ randomized_locations.extend(board_locations)
- include_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_false
- board_locations = filter_disabled_locations(world_options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD])
- randomized_locations.extend(board_locations)
- if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_board_qi and include_island:
- include_arcade = world_options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_disabled
- qi_orders = [location for location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI] if include_arcade or LocationTags.JUNIMO_KART not in location.tags]
+ include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false
+ if options.special_order_locations & SpecialOrderLocations.value_qi and include_island:
+ include_arcade = options.arcade_machine_locations != ArcadeMachineLocations.option_disabled
+ qi_orders = [location for location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI] if
+ include_arcade or LocationTags.JUNIMO_KART not in location.tags]
randomized_locations.extend(qi_orders)
-def extend_walnut_purchase_locations(randomized_locations: List[LocationData], world_options):
- if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
+def extend_walnut_purchase_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
+ if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return
randomized_locations.append(location_table["Repair Ticket Machine"])
randomized_locations.append(location_table["Repair Boat Hull"])
@@ -269,82 +311,249 @@ def extend_walnut_purchase_locations(randomized_locations: List[LocationData], w
randomized_locations.extend(locations_by_tag[LocationTags.WALNUT_PURCHASE])
-def extend_mandatory_locations(randomized_locations: List[LocationData], world_options):
+def extend_mandatory_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
mandatory_locations = [location for location in locations_by_tag[LocationTags.MANDATORY]]
- filtered_mandatory_locations = filter_disabled_locations(world_options, mandatory_locations)
+ filtered_mandatory_locations = filter_disabled_locations(options, mandatory_locations)
randomized_locations.extend(filtered_mandatory_locations)
-def extend_backpack_locations(randomized_locations: List[LocationData], world_options):
- if world_options[options.BackpackProgression] == options.BackpackProgression.option_vanilla:
+def extend_situational_quest_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
+ if options.quest_locations < 0:
+ return
+ if ModNames.distant_lands in options.mods:
+ if ModNames.alecto in options.mods:
+ randomized_locations.append(location_table[ModQuest.WitchOrder])
+ else:
+ randomized_locations.append(location_table[ModQuest.CorruptedCropsTask])
+
+
+def extend_bundle_locations(randomized_locations: List[LocationData], bundle_rooms: List[BundleRoom]):
+ for room in bundle_rooms:
+ room_location = f"Complete {room.name}"
+ if room_location in location_table:
+ randomized_locations.append(location_table[room_location])
+ for bundle in room.bundles:
+ randomized_locations.append(location_table[bundle.name])
+
+
+def extend_backpack_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
+ if options.backpack_progression == BackpackProgression.option_vanilla:
return
backpack_locations = [location for location in locations_by_tag[LocationTags.BACKPACK]]
- filtered_backpack_locations = filter_modded_locations(world_options, backpack_locations)
+ filtered_backpack_locations = filter_modded_locations(options, backpack_locations)
randomized_locations.extend(filtered_backpack_locations)
-def extend_elevator_locations(randomized_locations: List[LocationData], world_options):
- if world_options[options.ElevatorProgression] == options.ElevatorProgression.option_vanilla:
+def extend_elevator_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
+ if options.elevator_progression == ElevatorProgression.option_vanilla:
return
elevator_locations = [location for location in locations_by_tag[LocationTags.ELEVATOR]]
- filtered_elevator_locations = filter_modded_locations(world_options, elevator_locations)
+ filtered_elevator_locations = filter_modded_locations(options, elevator_locations)
randomized_locations.extend(filtered_elevator_locations)
+def extend_monstersanity_locations(randomized_locations: List[LocationData], options):
+ monstersanity = options.monstersanity
+ if monstersanity == Monstersanity.option_none:
+ return
+ if monstersanity == Monstersanity.option_one_per_monster or monstersanity == Monstersanity.option_split_goals:
+ monster_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_MONSTER]]
+ filtered_monster_locations = filter_disabled_locations(options, monster_locations)
+ randomized_locations.extend(filtered_monster_locations)
+ return
+ goal_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_GOALS]]
+ filtered_goal_locations = filter_disabled_locations(options, goal_locations)
+ randomized_locations.extend(filtered_goal_locations)
+ if monstersanity != Monstersanity.option_progressive_goals:
+ return
+ progressive_goal_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_PROGRESSIVE_GOALS]]
+ filtered_progressive_goal_locations = filter_disabled_locations(options, progressive_goal_locations)
+ randomized_locations.extend(filtered_progressive_goal_locations)
+
+
+def extend_shipsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
+ shipsanity = options.shipsanity
+ if shipsanity == Shipsanity.option_none:
+ return
+ if shipsanity == Shipsanity.option_everything:
+ ship_locations = [location for location in locations_by_tag[LocationTags.SHIPSANITY]]
+ filtered_ship_locations = filter_disabled_locations(options, ship_locations)
+ randomized_locations.extend(filtered_ship_locations)
+ return
+ shipsanity_locations = set()
+ if shipsanity == Shipsanity.option_fish or shipsanity == Shipsanity.option_full_shipment_with_fish:
+ shipsanity_locations = shipsanity_locations.union({location for location in locations_by_tag[LocationTags.SHIPSANITY_FISH]})
+ if shipsanity == Shipsanity.option_crops:
+ shipsanity_locations = shipsanity_locations.union({location for location in locations_by_tag[LocationTags.SHIPSANITY_CROP]})
+ if shipsanity == Shipsanity.option_full_shipment or shipsanity == Shipsanity.option_full_shipment_with_fish:
+ shipsanity_locations = shipsanity_locations.union({location for location in locations_by_tag[LocationTags.SHIPSANITY_FULL_SHIPMENT]})
+
+ filtered_shipsanity_locations = filter_disabled_locations(options, list(shipsanity_locations))
+ randomized_locations.extend(filtered_shipsanity_locations)
+
+
+def extend_cooksanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
+ cooksanity = options.cooksanity
+ if cooksanity == Cooksanity.option_none:
+ return
+ if cooksanity == Cooksanity.option_queen_of_sauce:
+ cooksanity_locations = (location for location in locations_by_tag[LocationTags.COOKSANITY_QOS])
+ else:
+ cooksanity_locations = (location for location in locations_by_tag[LocationTags.COOKSANITY])
+
+ filtered_cooksanity_locations = filter_disabled_locations(options, cooksanity_locations)
+ randomized_locations.extend(filtered_cooksanity_locations)
+
+
+def extend_chefsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
+ chefsanity = options.chefsanity
+ if chefsanity == Chefsanity.option_none:
+ return
+
+ chefsanity_locations_by_name = {} # Dictionary to not make duplicates
+
+ if chefsanity & Chefsanity.option_queen_of_sauce:
+ chefsanity_locations_by_name.update({location.name: location for location in locations_by_tag[LocationTags.CHEFSANITY_QOS]})
+ if chefsanity & Chefsanity.option_purchases:
+ chefsanity_locations_by_name.update({location.name: location for location in locations_by_tag[LocationTags.CHEFSANITY_PURCHASE]})
+ if chefsanity & Chefsanity.option_friendship:
+ chefsanity_locations_by_name.update({location.name: location for location in locations_by_tag[LocationTags.CHEFSANITY_FRIENDSHIP]})
+ if chefsanity & Chefsanity.option_skills:
+ chefsanity_locations_by_name.update({location.name: location for location in locations_by_tag[LocationTags.CHEFSANITY_SKILL]})
+
+ filtered_chefsanity_locations = filter_disabled_locations(options, list(chefsanity_locations_by_name.values()))
+ randomized_locations.extend(filtered_chefsanity_locations)
+
+
+def extend_craftsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
+ if options.craftsanity == Craftsanity.option_none:
+ return
+
+ craftsanity_locations = [craft for craft in locations_by_tag[LocationTags.CRAFTSANITY]]
+ filtered_craftsanity_locations = filter_disabled_locations(options, craftsanity_locations)
+ randomized_locations.extend(filtered_craftsanity_locations)
+
+
+def extend_book_locations(randomized_locations: List[LocationData], content: StardewContent):
+ booksanity = content.features.booksanity
+ if not booksanity.is_enabled:
+ return
+
+ book_locations = []
+ for book in content.find_tagged_items(ItemTag.BOOK):
+ if booksanity.is_included(book):
+ book_locations.append(location_table[booksanity.to_location_name(book.name)])
+
+ book_locations.extend(location_table[booksanity.to_location_name(book)] for book in booksanity.get_randomized_lost_books())
+
+ randomized_locations.extend(book_locations)
+
+
+def extend_walnutsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
+ if not options.walnutsanity:
+ return
+
+ if "Puzzles" in options.walnutsanity:
+ randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_PUZZLE])
+ if "Bushes" in options.walnutsanity:
+ randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_BUSH])
+ if "Dig Spots" in options.walnutsanity:
+ randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_DIG])
+ if "Repeatables" in options.walnutsanity:
+ randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_REPEATABLE])
+
+
def create_locations(location_collector: StardewLocationCollector,
- world_options: options.StardewOptions,
+ bundle_rooms: List[BundleRoom],
+ options: StardewValleyOptions,
+ content: StardewContent,
random: Random):
randomized_locations = []
- extend_mandatory_locations(randomized_locations, world_options)
- extend_backpack_locations(randomized_locations, world_options)
+ extend_mandatory_locations(randomized_locations, options)
+ extend_bundle_locations(randomized_locations, bundle_rooms)
+ extend_backpack_locations(randomized_locations, options)
- if not world_options[options.ToolProgression] == options.ToolProgression.option_vanilla:
+ if options.tool_progression & ToolProgression.option_progressive:
randomized_locations.extend(locations_by_tag[LocationTags.TOOL_UPGRADE])
- extend_elevator_locations(randomized_locations, world_options)
+ extend_elevator_locations(randomized_locations, options)
- if not world_options[options.SkillProgression] == options.SkillProgression.option_vanilla:
+ if not options.skill_progression == SkillProgression.option_vanilla:
for location in locations_by_tag[LocationTags.SKILL_LEVEL]:
- if location.mod_name is None or location.mod_name in world_options[options.Mods]:
- randomized_locations.append(location_table[location.name])
+ if location.mod_name is not None and location.mod_name not in options.mods:
+ continue
+ if LocationTags.MASTERY_LEVEL in location.tags and options.skill_progression != SkillProgression.option_progressive_with_masteries:
+ continue
+ randomized_locations.append(location_table[location.name])
- if not world_options[options.BuildingProgression] == options.BuildingProgression.option_vanilla:
+ if options.building_progression & BuildingProgression.option_progressive:
for location in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]:
- if location.mod_name is None or location.mod_name in world_options[options.Mods]:
+ if location.mod_name is None or location.mod_name in options.mods:
randomized_locations.append(location_table[location.name])
- if world_options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_disabled:
+ if options.arcade_machine_locations != ArcadeMachineLocations.option_disabled:
randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE_VICTORY])
- if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling:
+ if options.arcade_machine_locations == ArcadeMachineLocations.option_full_shuffling:
randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE])
- extend_cropsanity_locations(randomized_locations, world_options)
- extend_help_wanted_quests(randomized_locations, world_options[options.HelpWantedLocations])
- extend_fishsanity_locations(randomized_locations, world_options, random)
- extend_museumsanity_locations(randomized_locations, world_options[options.Museumsanity], random)
- extend_friendsanity_locations(randomized_locations, world_options)
+ extend_cropsanity_locations(randomized_locations, content)
+ extend_fishsanity_locations(randomized_locations, content, random)
+ extend_museumsanity_locations(randomized_locations, options, random)
+ extend_friendsanity_locations(randomized_locations, content)
- extend_festival_locations(randomized_locations, world_options[options.FestivalLocations])
- extend_special_order_locations(randomized_locations, world_options)
- extend_walnut_purchase_locations(randomized_locations, world_options)
+ extend_festival_locations(randomized_locations, options, random)
+ extend_special_order_locations(randomized_locations, options)
+ extend_walnut_purchase_locations(randomized_locations, options)
+
+ extend_monstersanity_locations(randomized_locations, options)
+ extend_shipsanity_locations(randomized_locations, options)
+ extend_cooksanity_locations(randomized_locations, options)
+ extend_chefsanity_locations(randomized_locations, options)
+ extend_craftsanity_locations(randomized_locations, options)
+ extend_quests_locations(randomized_locations, options)
+ extend_book_locations(randomized_locations, content)
+ extend_walnutsanity_locations(randomized_locations, options)
+
+ # Mods
+ extend_situational_quest_locations(randomized_locations, options)
for location_data in randomized_locations:
location_collector(location_data.name, location_data.code, location_data.region)
-def filter_ginger_island(world_options: options.StardewOptions, locations: List[LocationData]) -> List[LocationData]:
- include_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_false
- return [location for location in locations if include_island or LocationTags.GINGER_ISLAND not in location.tags]
+def filter_farm_type(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]:
+ # On Meadowlands, "Feeding Animals" replaces "Raising Animals"
+ if options.farm_type == FarmType.option_meadowlands:
+ return (location for location in locations if location.name != Quest.raising_animals)
+ else:
+ return (location for location in locations if location.name != Quest.feeding_animals)
+
+
+def filter_ginger_island(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]:
+ include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false
+ return (location for location in locations if include_island or LocationTags.GINGER_ISLAND not in location.tags)
+
+
+def filter_qi_order_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]:
+ include_qi_orders = options.special_order_locations & SpecialOrderLocations.value_qi
+ return (location for location in locations if include_qi_orders or LocationTags.REQUIRES_QI_ORDERS not in location.tags)
+
+
+def filter_masteries_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]:
+ include_masteries = options.skill_progression == SkillProgression.option_progressive_with_masteries
+ return (location for location in locations if include_masteries or LocationTags.REQUIRES_MASTERIES not in location.tags)
-def filter_modded_locations(world_options: options.StardewOptions, locations: List[LocationData]) -> List[LocationData]:
- current_mod_names = world_options[options.Mods]
- return [location for location in locations if location.mod_name is None or location.mod_name in current_mod_names]
+def filter_modded_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]:
+ return (location for location in locations if location.mod_name is None or location.mod_name in options.mods)
-def filter_disabled_locations(world_options: options.StardewOptions, locations: List[LocationData]) -> List[LocationData]:
- locations_first_pass = filter_ginger_island(world_options, locations)
- locations_second_pass = filter_modded_locations(world_options, locations_first_pass)
- return locations_second_pass
+def filter_disabled_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]:
+ locations_farm_filter = filter_farm_type(options, locations)
+ locations_island_filter = filter_ginger_island(options, locations_farm_filter)
+ locations_qi_filter = filter_qi_order_locations(options, locations_island_filter)
+ locations_masteries_filter = filter_masteries_locations(options, locations_qi_filter)
+ locations_mod_filter = filter_modded_locations(options, locations_masteries_filter)
+ return locations_mod_filter
diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py
deleted file mode 100644
index 00b60696a91f..000000000000
--- a/worlds/stardew_valley/logic.py
+++ /dev/null
@@ -1,1629 +0,0 @@
-from __future__ import annotations
-
-import math
-from dataclasses import dataclass, field
-from typing import Dict, Union, Optional, Iterable, Sized, List, Set
-
-from . import options
-from .data import all_fish, FishItem, all_purchasable_seeds, SeedItem, all_crops, CropItem
-from .data.bundle_data import BundleItem
-from .data.crops_data import crops_by_name
-from .data.fish_data import island_fish
-from .data.museum_data import all_museum_items, MuseumItem, all_museum_artifacts, dwarf_scrolls, all_museum_minerals
-from .data.recipe_data import all_cooking_recipes, CookingRecipe, RecipeSource, FriendshipSource, QueenOfSauceSource, \
- StarterSource, ShopSource, SkillSource
-from .data.villagers_data import all_villagers_by_name, Villager
-from .items import all_items, Group
-from .mods.logic.buildings import get_modded_building_rules
-from .mods.logic.quests import get_modded_quest_rules
-from .mods.logic.special_orders import get_modded_special_orders_rules
-from .mods.logic.skullcavernelevator import has_skull_cavern_elevator_to_floor
-from .mods.mod_data import ModNames
-from .mods.logic import magic, skills
-from .options import StardewOptions
-from .regions import vanilla_regions
-from .stardew_rule import False_, Reach, Or, True_, Received, Count, And, Has, TotalReceived, StardewRule
-from .strings.animal_names import Animal, coop_animals, barn_animals
-from .strings.animal_product_names import AnimalProduct
-from .strings.ap_names.buff_names import Buff
-from .strings.ap_names.transport_names import Transportation
-from .strings.artisan_good_names import ArtisanGood
-from .strings.building_names import Building
-from .strings.calendar_names import Weekday
-from .strings.craftable_names import Craftable
-from .strings.crop_names import Fruit, Vegetable, all_fruits, all_vegetables
-from .strings.fertilizer_names import Fertilizer
-from .strings.festival_check_names import FestivalCheck
-from .strings.fish_names import Fish, Trash, WaterItem
-from .strings.flower_names import Flower
-from .strings.forageable_names import Forageable
-from .strings.fruit_tree_names import Sapling
-from .strings.generic_names import Generic
-from .strings.geode_names import Geode
-from .strings.gift_names import Gift
-from .strings.ingredient_names import Ingredient
-from .strings.material_names import Material
-from .strings.machine_names import Machine
-from .strings.food_names import Meal, Beverage
-from .strings.metal_names import Ore, MetalBar, Mineral, Fossil
-from .strings.monster_drop_names import Loot
-from .strings.performance_names import Performance
-from .strings.quest_names import Quest
-from .strings.region_names import Region
-from .strings.season_names import Season
-from .strings.seed_names import Seed
-from .strings.skill_names import Skill, ModSkill
-from .strings.special_order_names import SpecialOrder
-from .strings.spells import MagicSpell
-from .strings.tool_names import Tool, ToolMaterial, APTool
-from .strings.tv_channel_names import Channel
-from .strings.villager_names import NPC
-from .strings.wallet_item_names import Wallet
-from .strings.weapon_names import Weapon
-
-MAX_MONTHS = 12
-MONEY_PER_MONTH = 15000
-MISSING_ITEM = "THIS ITEM IS MISSING"
-
-tool_materials = {
- ToolMaterial.copper: 1,
- ToolMaterial.iron: 2,
- ToolMaterial.gold: 3,
- ToolMaterial.iridium: 4
-}
-
-tool_upgrade_prices = {
- ToolMaterial.copper: 2000,
- ToolMaterial.iron: 5000,
- ToolMaterial.gold: 10000,
- ToolMaterial.iridium: 25000
-}
-
-fishing_regions = [Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west]
-
-@dataclass(frozen=True, repr=False)
-class StardewLogic:
- player: int
- options: StardewOptions
-
- item_rules: Dict[str, StardewRule] = field(default_factory=dict)
- sapling_rules: Dict[str, StardewRule] = field(default_factory=dict)
- tree_fruit_rules: Dict[str, StardewRule] = field(default_factory=dict)
- seed_rules: Dict[str, StardewRule] = field(default_factory=dict)
- cooking_rules: Dict[str, StardewRule] = field(default_factory=dict)
- crop_rules: Dict[str, StardewRule] = field(default_factory=dict)
- fish_rules: Dict[str, StardewRule] = field(default_factory=dict)
- museum_rules: Dict[str, StardewRule] = field(default_factory=dict)
- building_rules: Dict[str, StardewRule] = field(default_factory=dict)
- quest_rules: Dict[str, StardewRule] = field(default_factory=dict)
- festival_rules: Dict[str, StardewRule] = field(default_factory=dict)
- special_order_rules: Dict[str, StardewRule] = field(default_factory=dict)
-
- def __post_init__(self):
- self.fish_rules.update({fish.name: self.can_catch_fish(fish) for fish in all_fish})
- self.museum_rules.update({donation.name: self.can_find_museum_item(donation) for donation in all_museum_items})
-
- for recipe in all_cooking_recipes:
- can_cook_rule = self.can_cook(recipe)
- if recipe.meal in self.cooking_rules:
- can_cook_rule = can_cook_rule | self.cooking_rules[recipe.meal]
- self.cooking_rules[recipe.meal] = can_cook_rule
-
- self.sapling_rules.update({
- Sapling.apple: self.can_buy_sapling(Fruit.apple),
- Sapling.apricot: self.can_buy_sapling(Fruit.apricot),
- Sapling.cherry: self.can_buy_sapling(Fruit.cherry),
- Sapling.orange: self.can_buy_sapling(Fruit.orange),
- Sapling.peach: self.can_buy_sapling(Fruit.peach),
- Sapling.pomegranate: self.can_buy_sapling(Fruit.pomegranate),
- Sapling.banana: self.can_buy_sapling(Fruit.banana),
- Sapling.mango: self.can_buy_sapling(Fruit.mango),
- })
-
- self.tree_fruit_rules.update({
- Fruit.apple: self.can_plant_and_grow_item(Season.fall),
- Fruit.apricot: self.can_plant_and_grow_item(Season.spring),
- Fruit.cherry: self.can_plant_and_grow_item(Season.spring),
- Fruit.orange: self.can_plant_and_grow_item(Season.summer),
- Fruit.peach: self.can_plant_and_grow_item(Season.summer),
- Fruit.pomegranate: self.can_plant_and_grow_item(Season.fall),
- Fruit.banana: self.can_plant_and_grow_item(Season.summer),
- Fruit.mango: self.can_plant_and_grow_item(Season.summer),
- })
-
- for tree_fruit in self.tree_fruit_rules:
- existing_rules = self.tree_fruit_rules[tree_fruit]
- sapling = f"{tree_fruit} Sapling"
- self.tree_fruit_rules[tree_fruit] = existing_rules & self.has(sapling) & self.has_lived_months(1)
-
- self.seed_rules.update({seed.name: self.can_buy_seed(seed) for seed in all_purchasable_seeds})
- self.crop_rules.update({crop.name: self.can_grow_crop(crop) for crop in all_crops})
- self.crop_rules.update({
- Seed.coffee: (self.has_season(Season.spring) | self.has_season(
- Season.summer)) & self.can_buy_seed(crops_by_name[Seed.coffee].seed),
- Fruit.ancient_fruit: (self.received("Ancient Seeds") | self.received("Ancient Seeds Recipe")) &
- self.can_reach_region(Region.greenhouse) & self.has(Machine.seed_maker),
- })
-
- self.item_rules.update({
- ArtisanGood.aged_roe: self.can_preserves_jar(AnimalProduct.roe),
- AnimalProduct.any_egg: self.has(AnimalProduct.chicken_egg) | self.has(AnimalProduct.duck_egg),
- Fish.any: Or([self.can_catch_fish(fish) for fish in all_fish]),
- Geode.artifact_trove: self.has(Geode.omni) & self.can_reach_region(Region.desert),
- Craftable.bait: (self.has_skill_level(Skill.fishing, 2) & self.has(Loot.bug_meat)) | self.has(Machine.worm_bin),
- Fertilizer.basic: (self.has(Material.sap) & self.has_farming_level(1)) | (self.has_lived_months(1) & self.can_spend_money_at(Region.pierre_store, 100)),
- Fertilizer.quality: (self.has_farming_level(9) & self.has(Material.sap) & self.has(Fish.any)) | (self.has_year_two() & self.can_spend_money_at(Region.pierre_store, 150)),
- Fertilizer.deluxe: False_(),
- # self.received("Deluxe Fertilizer Recipe") & self.has(MetalBar.iridium) & self.has(SVItem.sap),
- Fertilizer.tree: self.has_skill_level(Skill.foraging, 7) & self.has(Material.fiber) & self.has(Material.stone),
- Loot.bat_wing: self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern(),
- ArtisanGood.battery_pack: (self.has(Machine.lightning_rod) & self.has_any_season_not_winter()) | self.has(Machine.solar_panel),
- Machine.bee_house: self.has_farming_level(3) & self.has(MetalBar.iron) & self.has(ArtisanGood.maple_syrup) & self.has(Material.coal) & self.has(Material.wood),
- Beverage.beer: self.can_keg(Vegetable.wheat) | self.can_spend_money_at(Region.saloon, 400),
- Forageable.blackberry: self.can_forage(Season.fall),
- Craftable.bomb: self.has_skill_level(Skill.mining, 6) & self.has(Material.coal) & self.has(Ore.iron),
- Fossil.bone_fragment: self.can_reach_region(Region.dig_site),
- Gift.bouquet: self.has_relationship(Generic.bachelor, 8) & self.can_spend_money_at(Region.pierre_store, 100),
- Meal.bread: self.can_spend_money_at(Region.saloon, 120),
- Trash.broken_cd: self.can_crab_pot(),
- Trash.broken_glasses: self.can_crab_pot(),
- Loot.bug_meat: self.can_mine_in_the_mines_floor_1_40(),
- Forageable.cactus_fruit: self.can_forage(Generic.any, Region.desert),
- Machine.cask: self.has_house(3) & self.can_reach_region(Region.cellar) & self.has(Material.wood) & self.has(Material.hardwood),
- Forageable.cave_carrot: self.can_forage(Generic.any, Region.mines_floor_10, True),
- ArtisanGood.caviar: self.can_preserves_jar(AnimalProduct.sturgeon_roe),
- Forageable.chanterelle: self.can_forage(Season.fall, Region.secret_woods),
- Machine.cheese_press: self.has_farming_level(6) & self.has(Material.wood) & self.has(Material.stone) & self.has(Material.hardwood) & self.has(MetalBar.copper),
- ArtisanGood.cheese: (self.has(AnimalProduct.cow_milk) & self.has(Machine.cheese_press)) | (self.can_reach_region(Region.desert) & self.has(Mineral.emerald)),
- Craftable.cherry_bomb: self.has_skill_level(Skill.mining, 1) & self.has(Material.coal) & self.has(Ore.copper),
- Animal.chicken: self.can_buy_animal(Animal.chicken),
- AnimalProduct.chicken_egg: self.has([AnimalProduct.egg, AnimalProduct.brown_egg, AnimalProduct.large_egg, AnimalProduct.large_brown_egg], 1),
- Material.cinder_shard: self.can_reach_region(Region.volcano_floor_5),
- WaterItem.clam: self.can_forage(Generic.any, Region.beach),
- Material.clay: self.can_reach_any_region([Region.farm, Region.beach, Region.quarry]) & self.has_tool(Tool.hoe),
- ArtisanGood.cloth: (self.has(AnimalProduct.wool) & self.has(Machine.loom)) | (self.can_reach_region(Region.desert) & self.has(Mineral.aquamarine)),
- Material.coal: self.can_mine_in_the_mines_floor_41_80() | self.can_do_panning(),
- WaterItem.cockle: self.can_forage(Generic.any, Region.beach),
- Forageable.coconut: self.can_forage(Generic.any, Region.desert),
- Beverage.coffee: self.can_keg(Seed.coffee) | self.has(Machine.coffee_maker) | (self.can_spend_money_at(Region.saloon, 300)) | self.has("Hot Java Ring"),
- Machine.coffee_maker: self.received(Machine.coffee_maker),
- Forageable.common_mushroom: self.can_forage(Season.fall) | (self.can_forage(Season.spring, Region.secret_woods)),
- MetalBar.copper: self.can_smelt(Ore.copper),
- Ore.copper: self.can_mine_in_the_mines_floor_1_40() | self.can_mine_in_the_skull_cavern() | self.can_do_panning(),
- WaterItem.coral: self.can_forage(Generic.any, Region.tide_pools) | self.can_forage(Season.summer, Region.beach),
- Animal.cow: self.can_buy_animal(Animal.cow),
- AnimalProduct.cow_milk: self.has(AnimalProduct.milk) | self.has(AnimalProduct.large_milk),
- Fish.crab: self.can_crab_pot(Region.beach),
- Machine.crab_pot: self.has_skill_level(Skill.fishing, 3) & (self.can_spend_money_at(Region.fish_shop, 1500) | (self.has(MetalBar.iron) & self.has(Material.wood))),
- Fish.crayfish: self.can_crab_pot(Region.town),
- Forageable.crocus: self.can_forage(Season.winter),
- Forageable.crystal_fruit: self.can_forage(Season.winter),
- Forageable.daffodil: self.can_forage(Season.spring),
- Forageable.dandelion: self.can_forage(Season.spring),
- Animal.dinosaur: self.has_building(Building.big_coop) & self.has(AnimalProduct.dinosaur_egg),
- Forageable.dragon_tooth: self.can_forage(Generic.any, Region.volcano_floor_10),
- "Dried Starfish": self.can_fish() & self.can_reach_region(Region.beach),
- Trash.driftwood: self.can_crab_pot(),
- AnimalProduct.duck_egg: self.has_animal(Animal.duck),
- AnimalProduct.duck_feather: self.has_happy_animal(Animal.duck),
- Animal.duck: self.can_buy_animal(Animal.duck),
- AnimalProduct.egg: self.has_animal(Animal.chicken),
- AnimalProduct.brown_egg: self.has_animal(Animal.chicken),
- "Energy Tonic": self.can_reach_region(Region.hospital) & self.can_spend_money(1000),
- Material.fiber: True_(),
- Forageable.fiddlehead_fern: self.can_forage(Season.summer, Region.secret_woods),
- "Magic Rock Candy": self.can_reach_region(Region.desert) & self.has("Prismatic Shard"),
- "Fishing Chest": self.can_fish_chests(),
- Craftable.flute_block: self.has_relationship(NPC.robin, 6) & self.can_reach_region(Region.carpenter) & self.has(Material.wood) & self.has(Ore.copper) & self.has(Material.fiber),
- Geode.frozen: self.can_mine_in_the_mines_floor_41_80(),
- Machine.furnace: self.has(Material.stone) & self.has(Ore.copper),
- Geode.geode: self.can_mine_in_the_mines_floor_1_40(),
- Forageable.ginger: self.can_forage(Generic.any, Region.island_west, True),
- ArtisanGood.goat_cheese: self.has(AnimalProduct.goat_milk) & self.has(Machine.cheese_press),
- AnimalProduct.goat_milk: self.has(Animal.goat),
- Animal.goat: self.can_buy_animal(Animal.goat),
- MetalBar.gold: self.can_smelt(Ore.gold),
- Ore.gold: self.can_mine_in_the_mines_floor_81_120() | self.can_mine_in_the_skull_cavern() | self.can_do_panning(),
- Geode.golden_coconut: self.can_reach_region(Region.island_north),
- Gift.golden_pumpkin: self.has_season(Season.fall) | self.can_open_geode(Geode.artifact_trove),
- WaterItem.green_algae: self.can_fish_in_freshwater(),
- ArtisanGood.green_tea: self.can_keg(Vegetable.tea_leaves),
- Material.hardwood: self.has_tool(Tool.axe, ToolMaterial.copper) & (self.can_reach_region(Region.secret_woods) | self.can_reach_region(Region.island_south)),
- Forageable.hay: self.has_building(Building.silo) & self.has_tool(Tool.scythe),
- Forageable.hazelnut: self.can_forage(Season.fall),
- Forageable.holly: self.can_forage(Season.winter),
- ArtisanGood.honey: self.can_spend_money_at(Region.oasis, 200) | (self.has(Machine.bee_house) & self.has_any_season_not_winter()),
- "Hot Java Ring": self.can_reach_region(Region.volcano_floor_10),
- Meal.ice_cream: (self.has_season(Season.summer) & self.can_spend_money_at(Region.town, 250)) | self.can_spend_money_at(Region.oasis, 240),
- # | (self.can_cook() & self.has_relationship(NPC.jodi, 7) & self.has(AnimalProduct.cow_milk) & self.has(Ingredient.sugar)),
- MetalBar.iridium: self.can_smelt(Ore.iridium),
- Ore.iridium: self.can_mine_in_the_skull_cavern(),
- MetalBar.iron: self.can_smelt(Ore.iron),
- Ore.iron: self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern() | self.can_do_panning(),
- ArtisanGood.jelly: self.has_jelly(),
- Trash.joja_cola: self.can_spend_money_at(Region.saloon, 75),
- "JotPK Small Buff": self.has_jotpk_power_level(2),
- "JotPK Medium Buff": self.has_jotpk_power_level(4),
- "JotPK Big Buff": self.has_jotpk_power_level(7),
- "JotPK Max Buff": self.has_jotpk_power_level(9),
- ArtisanGood.juice: self.has_juice(),
- "Junimo Kart Small Buff": self.has_junimo_kart_power_level(2),
- "Junimo Kart Medium Buff": self.has_junimo_kart_power_level(4),
- "Junimo Kart Big Buff": self.has_junimo_kart_power_level(6),
- "Junimo Kart Max Buff": self.has_junimo_kart_power_level(8),
- Machine.keg: self.has_farming_level(8) & self.has(Material.wood) & self.has(MetalBar.iron) & self.has(MetalBar.copper) & self.has(ArtisanGood.oak_resin),
- AnimalProduct.large_egg: self.has_happy_animal(Animal.chicken),
- AnimalProduct.large_brown_egg: self.has_happy_animal(Animal.chicken),
- AnimalProduct.large_goat_milk: self.has_happy_animal(Animal.goat),
- AnimalProduct.large_milk: self.has_happy_animal(Animal.cow),
- Forageable.leek: self.can_forage(Season.spring),
- Craftable.life_elixir: self.has_skill_level(Skill.combat, 2) & self.has(Forageable.red_mushroom) & self.has(Forageable.purple_mushroom) & self.has(Forageable.morel) & self.has(Forageable.chanterelle),
- Machine.lightning_rod: self.has_skill_level(Skill.foraging, 6) & self.has(MetalBar.iron) & self.has(MetalBar.quartz) & self.has(Loot.bat_wing),
- Fish.lobster: self.can_crab_pot(Region.beach),
- Machine.loom: self.has_farming_level(7) & self.has(Material.wood) & self.has(Material.fiber) & self.has(ArtisanGood.pine_tar),
- Forageable.magma_cap: self.can_forage(Generic.any, Region.volcano_floor_5),
- Geode.magma: self.can_mine_in_the_mines_floor_81_120() | (self.has(Fish.lava_eel) & self.has_building(Building.fish_pond)),
- ArtisanGood.maple_syrup: self.has(Machine.tapper),
- ArtisanGood.mayonnaise: self.has(Machine.mayonnaise_machine) & self.has(AnimalProduct.chicken_egg),
- Machine.mayonnaise_machine: self.has_farming_level(2) & self.has(Material.wood) & self.has(Material.stone) & self.has("Earth Crystal") & self.has(MetalBar.copper),
- ArtisanGood.mead: self.can_keg(ArtisanGood.honey),
- Craftable.mega_bomb: self.has_skill_level(Skill.mining, 8) & self.has(Ore.gold) & self.has(Loot.solar_essence) & self.has(Loot.void_essence),
- Gift.mermaid_pendant: self.can_reach_region(Region.tide_pools) & self.has_relationship(Generic.bachelor, 10) & self.has_house(1) & self.has(Craftable.rain_totem),
- AnimalProduct.milk: self.has_animal(Animal.cow),
- Craftable.monster_musk: self.has_prismatic_jelly_reward_access() & self.has(Loot.slime) & self.has(Loot.bat_wing),
- Forageable.morel: self.can_forage(Season.spring, Region.secret_woods),
- "Muscle Remedy": self.can_reach_region(Region.hospital) & self.can_spend_money(1000),
- Fish.mussel: self.can_forage(Generic.any, Region.beach) or self.has(Fish.mussel_node),
- Fish.mussel_node: self.can_reach_region(Region.island_west),
- WaterItem.nautilus_shell: self.can_forage(Season.winter, Region.beach),
- ArtisanGood.oak_resin: self.has(Machine.tapper),
- Ingredient.oil: self.can_spend_money_at(Region.pierre_store, 200) | (self.has(Machine.oil_maker) & (self.has(Vegetable.corn) | self.has(Flower.sunflower) | self.has(Seed.sunflower))),
- Machine.oil_maker: self.has_farming_level(8) & self.has(Loot.slime) & self.has(Material.hardwood) & self.has(MetalBar.gold),
- Craftable.oil_of_garlic: (self.has_skill_level(Skill.combat, 6) & self.has(Vegetable.garlic) & self.has(Ingredient.oil)) | (self.can_spend_money_at(Region.mines_dwarf_shop, 3000)),
- Geode.omni: self.can_mine_in_the_mines_floor_41_80() | self.can_reach_region(Region.desert) | self.can_do_panning() | self.received(Wallet.rusty_key) | (self.has(Fish.octopus) & self.has_building(Building.fish_pond)) | self.can_reach_region(Region.volcano_floor_10),
- Animal.ostrich: self.has_building(Building.barn) & self.has(AnimalProduct.ostrich_egg) & self.has(Machine.ostrich_incubator),
- AnimalProduct.ostrich_egg: self.can_forage(Generic.any, Region.island_north, True),
- Machine.ostrich_incubator: self.received("Ostrich Incubator Recipe") & self.has(Fossil.bone_fragment) & self.has(Material.hardwood) & self.has(Material.cinder_shard),
- Fish.oyster: self.can_forage(Generic.any, Region.beach),
- ArtisanGood.pale_ale: self.can_keg(Vegetable.hops),
- Gift.pearl: (self.has(Fish.blobfish) & self.has_building(Building.fish_pond)) | self.can_open_geode(Geode.artifact_trove),
- Fish.periwinkle: self.can_crab_pot(Region.town),
- ArtisanGood.pickles: self.has_pickle(),
- Animal.pig: self.can_buy_animal(Animal.pig),
- Beverage.pina_colada: self.can_spend_money_at(Region.island_resort, 600),
- ArtisanGood.pine_tar: self.has(Machine.tapper),
- Meal.pizza: self.can_spend_money_at(Region.saloon, 600),
- Machine.preserves_jar: self.has_farming_level(4) & self.has(Material.wood) & self.has(Material.stone) & self.has(Material.coal),
- Forageable.purple_mushroom: self.can_forage(Generic.any, Region.mines_floor_95) | self.can_forage(Generic.any, Region.skull_cavern_25),
- Animal.rabbit: self.can_buy_animal(Animal.rabbit),
- AnimalProduct.rabbit_foot: self.has_happy_animal(Animal.rabbit),
- MetalBar.radioactive: self.can_smelt(Ore.radioactive),
- Ore.radioactive: self.can_mine_perfectly() & self.can_reach_region(Region.qi_walnut_room),
- Forageable.rainbow_shell: self.can_forage(Season.summer, Region.beach),
- Craftable.rain_totem: self.has_skill_level(Skill.foraging, 9) & self.has(Material.hardwood) & self.has(ArtisanGood.truffle_oil) & self.has(ArtisanGood.pine_tar),
- Machine.recycling_machine: self.has_skill_level(Skill.fishing, 4) & self.has(Material.wood) & self.has(Material.stone) & self.has(MetalBar.iron),
- Forageable.red_mushroom: self.can_forage(Season.summer, Region.secret_woods) | self.can_forage(Season.fall, Region.secret_woods),
- MetalBar.quartz: self.can_smelt("Quartz") | self.can_smelt("Fire Quartz") |
- (self.has(Machine.recycling_machine) & (self.has(Trash.broken_cd) | self.has(Trash.broken_glasses))),
- Ingredient.rice: self.can_spend_money_at(Region.pierre_store, 200) | (
- self.has_building(Building.mill) & self.has(Vegetable.unmilled_rice)),
- AnimalProduct.roe: self.can_fish() & self.has_building(Building.fish_pond),
- Meal.salad: self.can_spend_money_at(Region.saloon, 220),
- # | (self.can_cook() & self.has_relationship(NPC.emily, 3) & self.has(Forageable.leek) & self.has(Forageable.dandelion) &
- # self.has(Ingredient.vinegar)),
- Forageable.salmonberry: self.can_forage(Season.spring),
- Material.sap: self.can_chop_trees(),
- Craftable.scarecrow: self.has_farming_level(1) & self.has(Material.wood) & self.has(Material.coal) & self.has(Material.fiber),
- WaterItem.sea_urchin: self.can_forage(Generic.any, Region.tide_pools),
- WaterItem.seaweed: (self.can_fish() & self.can_reach_region(Region.beach)) | self.can_reach_region(
- Region.tide_pools),
- Forageable.secret_note: self.received(Wallet.magnifying_glass) & (self.can_chop_trees() | self.can_mine_in_the_mines_floor_1_40()),
- Machine.seed_maker: self.has_farming_level(9) & self.has(Material.wood) & self.has(MetalBar.gold) & self.has(
- Material.coal),
- Animal.sheep: self.can_buy_animal(Animal.sheep),
- Fish.shrimp: self.can_crab_pot(Region.beach),
- Loot.slime: self.can_mine_in_the_mines_floor_1_40(),
- Weapon.any_slingshot: self.received(Weapon.slingshot) | self.received(Weapon.master_slingshot),
- Fish.snail: self.can_crab_pot(Region.town),
- Forageable.snow_yam: self.can_forage(Season.winter, Region.beach, True),
- Trash.soggy_newspaper: self.can_crab_pot(),
- Loot.solar_essence: self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern(),
- Machine.solar_panel: self.received("Solar Panel Recipe") & self.has(MetalBar.quartz) & self.has(
- MetalBar.iron) & self.has(MetalBar.gold),
- Meal.spaghetti: self.can_spend_money_at(Region.saloon, 240),
- Forageable.spice_berry: self.can_forage(Season.summer),
- Forageable.spring_onion: self.can_forage(Season.spring),
- AnimalProduct.squid_ink: self.can_mine_in_the_mines_floor_81_120() | (self.has_building(Building.fish_pond) & self.has(Fish.squid)),
- Craftable.staircase: self.has_skill_level(Skill.mining, 2) & self.has(Material.stone),
- Material.stone: self.has_tool(Tool.pickaxe),
- Meal.strange_bun: self.has_relationship(NPC.shane, 7) & self.has(Ingredient.wheat_flour) & self.has(Fish.periwinkle) & self.has(ArtisanGood.void_mayonnaise),
- AnimalProduct.sturgeon_roe: self.has(Fish.sturgeon) & self.has_building(Building.fish_pond),
- Ingredient.sugar: self.can_spend_money_at(Region.pierre_store, 100) | (
- self.has_building(Building.mill) & self.has(Vegetable.beet)),
- Forageable.sweet_pea: self.can_forage(Season.summer),
- Machine.tapper: self.has_skill_level(Skill.foraging, 3) & self.has(Material.wood) & self.has(MetalBar.copper),
- Vegetable.tea_leaves: self.has(Sapling.tea) & self.has_lived_months(2) & self.has_any_season_not_winter(),
- Sapling.tea: self.has_relationship(NPC.caroline, 2) & self.has(Material.fiber) & self.has(Material.wood),
- Trash.trash: self.can_crab_pot(),
- Beverage.triple_shot_espresso: self.has("Hot Java Ring"),
- ArtisanGood.truffle_oil: self.has(AnimalProduct.truffle) & self.has(Machine.oil_maker),
- AnimalProduct.truffle: self.has_animal(Animal.pig) & self.has_any_season_not_winter(),
- Ingredient.vinegar: self.can_spend_money_at(Region.pierre_store, 200),
- AnimalProduct.void_egg: self.can_spend_money_at(Region.sewer, 5000) | (self.has_building(Building.fish_pond) & self.has(Fish.void_salmon)),
- Loot.void_essence: self.can_mine_in_the_mines_floor_81_120() | self.can_mine_in_the_skull_cavern(),
- ArtisanGood.void_mayonnaise: (self.can_reach_region(Region.witch_swamp) & self.can_fish()) | (self.has(Machine.mayonnaise_machine) & self.has(AnimalProduct.void_egg)),
- Ingredient.wheat_flour: self.can_spend_money_at(Region.pierre_store, 100) |
- (self.has_building(Building.mill) & self.has(Vegetable.wheat)),
- WaterItem.white_algae: self.can_fish() & self.can_reach_region(Region.mines_floor_20),
- Forageable.wild_horseradish: self.can_forage(Season.spring),
- Forageable.wild_plum: self.can_forage(Season.fall),
- Gift.wilted_bouquet: self.has(Machine.furnace) & self.has(Gift.bouquet) & self.has(Material.coal),
- ArtisanGood.wine: self.has_wine(),
- Forageable.winter_root: self.can_forage(Season.winter, Region.forest, True),
- Material.wood: self.has_tool(Tool.axe),
- AnimalProduct.wool: self.has_animal(Animal.rabbit) | self.has_animal(Animal.sheep),
- Machine.worm_bin: self.has_skill_level(Skill.fishing, 8) & self.has(Material.hardwood) & self.has(MetalBar.gold) & self.has(MetalBar.iron) & self.has(Material.fiber),
- })
- self.item_rules.update(self.fish_rules)
- self.item_rules.update(self.museum_rules)
- self.item_rules.update(self.sapling_rules)
- self.item_rules.update(self.tree_fruit_rules)
- self.item_rules.update(self.seed_rules)
- self.item_rules.update(self.crop_rules)
-
- # For some recipes, the cooked item can be obtained directly, so we either cook it or get it
- for recipe in self.cooking_rules:
- cooking_rule = self.cooking_rules[recipe]
- obtention_rule = self.item_rules[recipe] if recipe in self.item_rules else False_()
- self.item_rules[recipe] = obtention_rule | cooking_rule
-
- self.building_rules.update({
- Building.barn: self.can_spend_money_at(Region.carpenter, 6000) & self.has([Material.wood, Material.stone]),
- Building.big_barn: self.can_spend_money_at(Region.carpenter, 12000) & self.has([Material.wood, Material.stone]) & self.has_building(Building.barn),
- Building.deluxe_barn: self.can_spend_money_at(Region.carpenter, 25000) & self.has([Material.wood, Material.stone]) & self.has_building(Building.big_barn),
- Building.coop: self.can_spend_money_at(Region.carpenter, 4000) & self.has([Material.wood, Material.stone]),
- Building.big_coop: self.can_spend_money_at(Region.carpenter, 10000) & self.has([Material.wood, Material.stone]) & self.has_building(Building.coop),
- Building.deluxe_coop: self.can_spend_money_at(Region.carpenter, 20000) & self.has([Material.wood, Material.stone]) & self.has_building(Building.big_coop),
- Building.fish_pond: self.can_spend_money_at(Region.carpenter, 5000) & self.has([Material.stone, WaterItem.seaweed, WaterItem.green_algae]),
- Building.mill: self.can_spend_money_at(Region.carpenter, 2500) & self.has([Material.stone, Material.wood, ArtisanGood.cloth]),
- Building.shed: self.can_spend_money_at(Region.carpenter, 15000) & self.has(Material.wood),
- Building.big_shed: self.can_spend_money_at(Region.carpenter, 20000) & self.has([Material.wood, Material.stone]) & self.has_building(Building.shed),
- Building.silo: self.can_spend_money_at(Region.carpenter, 100) & self.has([Material.stone, Material.clay, MetalBar.copper]),
- Building.slime_hutch: self.can_spend_money_at(Region.carpenter, 10000) & self.has([Material.stone, MetalBar.quartz, MetalBar.iridium]),
- Building.stable: self.can_spend_money_at(Region.carpenter, 10000) & self.has([Material.hardwood, MetalBar.iron]),
- Building.well: self.can_spend_money_at(Region.carpenter, 1000) & self.has(Material.stone),
- Building.shipping_bin: self.can_spend_money_at(Region.carpenter, 250) & self.has(Material.wood),
- Building.kitchen: self.can_spend_money_at(Region.carpenter, 10000) & self.has(Material.wood) & self.has_house(0),
- Building.kids_room: self.can_spend_money_at(Region.carpenter, 50000) & self.has(Material.hardwood) & self.has_house(1),
- Building.cellar: self.can_spend_money_at(Region.carpenter, 100000) & self.has_house(2),
- })
-
- self.building_rules.update(get_modded_building_rules(self, self.options[options.Mods]))
-
- self.quest_rules.update({
- Quest.introductions: self.can_reach_region(Region.town),
- Quest.how_to_win_friends: self.can_complete_quest(Quest.introductions),
- Quest.getting_started: self.has(Vegetable.parsnip) & self.has_tool(Tool.hoe) & self.can_water(0),
- Quest.to_the_beach: self.can_reach_region(Region.beach),
- Quest.raising_animals: self.can_complete_quest(Quest.getting_started) & self.has_building(Building.coop),
- Quest.advancement: self.can_complete_quest(Quest.getting_started) & self.has(Craftable.scarecrow),
- Quest.archaeology: (self.has_tool(Tool.hoe) | self.can_mine_in_the_mines_floor_1_40() | self.can_fish()) & self.can_reach_region(Region.museum),
- Quest.meet_the_wizard: self.can_reach_region(Region.town) & self.can_reach_region(Region.community_center) & self.can_reach_region(Region.wizard_tower),
- Quest.forging_ahead: self.has(Ore.copper) & self.has(Machine.furnace),
- Quest.smelting: self.has(MetalBar.copper),
- Quest.initiation: self.can_mine_in_the_mines_floor_1_40(),
- Quest.robins_lost_axe: self.has_season(Season.spring) & self.can_reach_region(Region.forest) & self.can_meet(NPC.robin),
- Quest.jodis_request: self.has_season(Season.spring) & self.has(Vegetable.cauliflower) & self.can_meet(NPC.jodi),
- Quest.mayors_shorts: self.has_season(Season.summer) & self.can_reach_region(Region.ranch) &
- (self.has_relationship(NPC.marnie, 2) | (magic.can_blink(self))) & self.can_meet(NPC.lewis),
- Quest.blackberry_basket: self.has_season(Season.fall) & self.can_meet(NPC.linus),
- Quest.marnies_request: self.has_relationship(NPC.marnie, 3) & self.has(Forageable.cave_carrot) & self.can_reach_region(Region.ranch),
- Quest.pam_is_thirsty: self.has_season(Season.summer) & self.has(ArtisanGood.pale_ale) & self.can_meet(NPC.pam),
- Quest.a_dark_reagent: self.has_season(Season.winter) & self.has(Loot.void_essence) & self.can_meet(NPC.wizard),
- Quest.cows_delight: self.has_season(Season.fall) & self.has(Vegetable.amaranth) & self.can_meet(NPC.marnie),
- Quest.the_skull_key: self.received(Wallet.skull_key) & self.can_reach_region(Region.skull_cavern_entrance),
- Quest.crop_research: self.has_season(Season.summer) & self.has(Fruit.melon) & self.can_meet(NPC.demetrius),
- Quest.knee_therapy: self.has_season(Season.summer) & self.has(Fruit.hot_pepper) & self.can_meet(NPC.george),
- Quest.robins_request: self.has_season(Season.winter) & self.has(Material.hardwood) & self.can_meet(NPC.robin),
- Quest.qis_challenge: self.can_mine_in_the_skull_cavern(),
- Quest.the_mysterious_qi: self.can_reach_region(Region.bus_tunnel) & self.has(ArtisanGood.battery_pack) & self.can_reach_region(Region.desert) & self.has(Forageable.rainbow_shell) & self.has(Vegetable.beet) & self.has(Loot.solar_essence),
- Quest.carving_pumpkins: self.has_season(Season.fall) & self.has(Vegetable.pumpkin) & self.can_meet(NPC.caroline),
- Quest.a_winter_mystery: self.has_season(Season.winter) & self.can_reach_region(Region.town),
- Quest.strange_note: self.has(Forageable.secret_note) & self.can_reach_region(Region.secret_woods) & self.has(ArtisanGood.maple_syrup),
- Quest.cryptic_note: self.has(Forageable.secret_note) & self.can_reach_region(Region.skull_cavern_100),
- Quest.fresh_fruit: self.has_season(Season.spring) & self.has(Fruit.apricot) & self.can_meet(NPC.emily),
- Quest.aquatic_research: self.has_season(Season.summer) & self.has(Fish.pufferfish) & self.can_meet(NPC.demetrius),
- Quest.a_soldiers_star: self.has_season(Season.summer) & self.has_year_two() & self.has(Fruit.starfruit) & self.can_meet(NPC.kent),
- Quest.mayors_need: self.has_season(Season.summer) & self.has(ArtisanGood.truffle_oil) & self.can_meet(NPC.lewis),
- Quest.wanted_lobster: self.has_season(Season.fall) & self.has_season(Season.fall) & self.has(Fish.lobster) & self.can_meet(NPC.gus),
- Quest.pam_needs_juice: self.has_season(Season.fall) & self.has(ArtisanGood.battery_pack) & self.can_meet(NPC.pam),
- Quest.fish_casserole: self.has_relationship(NPC.jodi, 4) & self.has(Fish.largemouth_bass) & self.can_reach_region(Region.sam_house),
- Quest.catch_a_squid: self.has_season(Season.winter) & self.has(Fish.squid) & self.can_meet(NPC.willy),
- Quest.fish_stew: self.has_season(Season.winter) & self.has(Fish.albacore) & self.can_meet(NPC.gus),
- Quest.pierres_notice: self.has_season(Season.spring) & self.has("Sashimi") & self.can_meet(NPC.pierre),
- Quest.clints_attempt: self.has_season(Season.winter) & self.has(Mineral.amethyst) & self.can_meet(NPC.emily),
- Quest.a_favor_for_clint: self.has_season(Season.winter) & self.has(MetalBar.iron) & self.can_meet(NPC.clint),
- Quest.staff_of_power: self.has_season(Season.winter) & self.has(MetalBar.iridium) & self.can_meet(NPC.wizard),
- Quest.grannys_gift: self.has_season(Season.spring) & self.has(Forageable.leek) & self.can_meet(NPC.evelyn),
- Quest.exotic_spirits: self.has_season(Season.winter) & self.has(Forageable.coconut) & self.can_meet(NPC.gus),
- Quest.catch_a_lingcod: self.has_season(Season.winter) & self.has("Lingcod") & self.can_meet(NPC.willy),
- Quest.dark_talisman: self.has_rusty_key() & self.can_reach_region(Region.railroad) & self.can_meet(NPC.krobus) & self.can_reach_region(Region.mutant_bug_lair),
- Quest.goblin_problem: self.can_reach_region(Region.witch_swamp) & self.has(ArtisanGood.void_mayonnaise),
- Quest.magic_ink: self.can_reach_region(Region.witch_hut) & self.can_meet(NPC.wizard),
- Quest.the_pirates_wife: self.can_reach_region(Region.island_west) & self.can_meet(NPC.kent) &
- self.can_meet(NPC.gus) & self.can_meet(NPC.sandy) & self.can_meet(NPC.george) &
- self.can_meet(NPC.wizard) & self.can_meet(NPC.willy),
- })
-
- self.quest_rules.update(get_modded_quest_rules(self, self.options[options.Mods]))
-
- self.festival_rules.update({
- FestivalCheck.egg_hunt: self.has_season(Season.spring) & self.can_reach_region(Region.town) & self.can_win_egg_hunt(),
- FestivalCheck.strawberry_seeds: self.has_season(Season.spring) & self.can_reach_region(Region.town) & self.can_spend_money(1000),
- FestivalCheck.dance: self.has_season(Season.spring) & self.can_reach_region(Region.forest) & self.has_relationship(Generic.bachelor, 4),
- FestivalCheck.rarecrow_5: self.has_season(Season.spring) & self.can_reach_region(Region.forest) & self.can_spend_money(2500),
- FestivalCheck.luau_soup: self.has_season(Season.summer) & self.can_reach_region(Region.beach) & self.can_succeed_luau_soup(),
- FestivalCheck.moonlight_jellies: self.has_season(Season.summer) & self.can_reach_region(Region.beach),
- FestivalCheck.smashing_stone: self.has_season(Season.fall) & self.can_reach_region(Region.town),
- FestivalCheck.grange_display: self.has_season(Season.fall) & self.can_reach_region(Region.town) & self.can_succeed_grange_display(),
- FestivalCheck.rarecrow_1: self.has_season(Season.fall) & self.can_reach_region(Region.town), # only cost star tokens
- FestivalCheck.fair_stardrop: self.has_season(Season.fall) & self.can_reach_region(Region.town), # only cost star tokens
- FestivalCheck.spirit_eve_maze: self.has_season(Season.fall) & self.can_reach_region(Region.town),
- FestivalCheck.rarecrow_2: self.has_season(Season.fall) & self.can_reach_region(Region.town) & self.can_spend_money(5000),
- FestivalCheck.fishing_competition: self.has_season(Season.winter) & self.can_reach_region(Region.forest) & self.can_win_fishing_competition(),
- FestivalCheck.rarecrow_4: self.has_season(Season.winter) & self.can_reach_region(Region.forest) & self.can_spend_money(5000),
- FestivalCheck.mermaid_pearl: self.has_season(Season.winter) & self.can_reach_region(Region.beach),
- FestivalCheck.cone_hat: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(2500),
- FestivalCheck.iridium_fireplace: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(15000),
- FestivalCheck.rarecrow_7: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(5000) & self.can_donate_museum_artifacts(20),
- FestivalCheck.rarecrow_8: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(5000) & self.can_donate_museum_items(40),
- FestivalCheck.lupini_red_eagle: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(1200),
- FestivalCheck.lupini_portrait_mermaid: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(1200),
- FestivalCheck.lupini_solar_kingdom: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.can_spend_money(1200),
- FestivalCheck.lupini_clouds: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.has_year_two() & self.can_spend_money(1200),
- FestivalCheck.lupini_1000_years: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.has_year_two() & self.can_spend_money(1200),
- FestivalCheck.lupini_three_trees: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.has_year_two() & self.can_spend_money(1200),
- FestivalCheck.lupini_the_serpent: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.has_year_three() & self.can_spend_money(1200),
- FestivalCheck.lupini_tropical_fish: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.has_year_three() & self.can_spend_money(1200),
- FestivalCheck.lupini_land_of_clay: self.has_season(Season.winter) & self.can_reach_region(Region.beach) & self.has_year_three() & self.can_spend_money(1200),
- FestivalCheck.secret_santa: self.has_season(Season.winter) & self.can_reach_region(Region.town) & self.has_any_universal_love(),
- FestivalCheck.legend_of_the_winter_star: self.has_season(Season.winter) & self.can_reach_region(Region.town),
- FestivalCheck.all_rarecrows: self.can_reach_region(Region.farm) & self.has_all_rarecrows(),
- })
-
- self.special_order_rules.update({
- SpecialOrder.island_ingredients: self.can_meet(NPC.caroline) & self.has_island_transport() & self.can_farm_perfectly() &
- self.can_ship(Vegetable.taro_root) & self.can_ship(Fruit.pineapple) & self.can_ship(Forageable.ginger),
- SpecialOrder.cave_patrol: self.can_meet(NPC.clint) & self.can_mine_perfectly() & self.can_mine_to_floor(120),
- SpecialOrder.aquatic_overpopulation: self.can_meet(NPC.demetrius) & self.can_fish_perfectly(),
- SpecialOrder.biome_balance: self.can_meet(NPC.demetrius) & self.can_fish_perfectly(),
- SpecialOrder.rock_rejuivenation: self.has_relationship(NPC.emily, 4) & self.has(Mineral.ruby) & self.has(Mineral.topaz) &
- self.has(Mineral.emerald) & self.has(Mineral.jade) & self.has(Mineral.amethyst) &
- self.has(ArtisanGood.cloth) & self.can_reach_region(Region.haley_house),
- SpecialOrder.gifts_for_george: self.can_reach_region(Region.alex_house) & self.has_season(Season.spring) & self.has(Forageable.leek),
- SpecialOrder.fragments_of_the_past: self.can_reach_region(Region.museum) & self.can_reach_region(Region.dig_site) & self.has_tool(Tool.pickaxe),
- SpecialOrder.gus_famous_omelet: self.can_reach_region(Region.saloon) & self.has(AnimalProduct.any_egg),
- SpecialOrder.crop_order: self.can_farm_perfectly() & self.can_ship(),
- SpecialOrder.community_cleanup: self.can_reach_region(Region.railroad) & self.can_crab_pot(),
- SpecialOrder.the_strong_stuff: self.can_reach_region(Region.trailer) & self.can_keg(Vegetable.potato),
- SpecialOrder.pierres_prime_produce: self.can_reach_region(Region.pierre_store) & self.can_farm_perfectly(),
- SpecialOrder.robins_project: self.can_meet(NPC.robin) & self.can_reach_region(Region.carpenter) & self.can_chop_perfectly() &
- self.has(Material.hardwood),
- SpecialOrder.robins_resource_rush: self.can_meet(NPC.robin) & self.can_reach_region(Region.carpenter) & self.can_chop_perfectly() &
- self.has(Fertilizer.tree) & self.can_mine_perfectly(),
- SpecialOrder.juicy_bugs_wanted_yum: self.can_reach_region(Region.beach) & self.has(Loot.bug_meat),
- SpecialOrder.tropical_fish: self.can_meet(NPC.willy) & self.received("Island Resort") & self.has_island_transport() &
- self.has(Fish.stingray) & self.has(Fish.blue_discus) & self.has(Fish.lionfish),
- SpecialOrder.a_curious_substance: self.can_reach_region(Region.wizard_tower) & self.can_mine_perfectly() & self.can_mine_to_floor(80),
- SpecialOrder.prismatic_jelly: self.can_reach_region(Region.wizard_tower) & self.can_mine_perfectly() & self.can_mine_to_floor(40),
- SpecialOrder.qis_crop: self.can_farm_perfectly() & self.can_reach_region(Region.greenhouse) &
- self.can_reach_region(Region.island_west) & self.has_total_skill_level(50) &
- self.has(Machine.seed_maker) & self.has_building(Building.shipping_bin),
- SpecialOrder.lets_play_a_game: self.has_junimo_kart_max_level(),
- SpecialOrder.four_precious_stones: self.has_lived_months(MAX_MONTHS) & self.has("Prismatic Shard") &
- self.can_mine_perfectly_in_the_skull_cavern(),
- SpecialOrder.qis_hungry_challenge: self.can_mine_perfectly_in_the_skull_cavern() & self.has_max_buffs(),
- SpecialOrder.qis_cuisine: self.can_cook() & (self.can_spend_money_at(Region.saloon, 205000) | self.can_spend_money_at(Region.pierre_store, 170000)) &
- self.can_ship(),
- SpecialOrder.qis_kindness: self.can_give_loved_gifts_to_everyone(),
- SpecialOrder.extended_family: self.can_fish_perfectly() & self.has(Fish.angler) & self.has(Fish.glacierfish) &
- self.has(Fish.crimsonfish) & self.has(Fish.mutant_carp) & self.has(Fish.legend),
- SpecialOrder.danger_in_the_deep: self.can_mine_perfectly() & self.has_mine_elevator_to_floor(120),
- SpecialOrder.skull_cavern_invasion: self.can_mine_perfectly_in_the_skull_cavern() & self.has_max_buffs(),
- SpecialOrder.qis_prismatic_grange: self.has(Loot.bug_meat) & # 100 Bug Meat
- self.can_spend_money_at(Region.saloon, 24000) & # 100 Spaghetti
- self.can_spend_money_at(Region.blacksmith, 15000) & # 100 Copper Ore
- self.can_spend_money_at(Region.ranch, 5000) & # 100 Hay
- self.can_spend_money_at(Region.saloon, 22000) & # 100 Salads
- self.can_spend_money_at(Region.saloon, 7500) & # 100 Joja Cola
- self.can_spend_money(80000), # I need this extra rule because money rules aren't additive...
- })
-
- self.special_order_rules.update(get_modded_special_orders_rules(self, self.options[options.Mods]))
-
- def has(self, items: Union[str, (Iterable[str], Sized)], count: Optional[int] = None) -> StardewRule:
- if isinstance(items, str):
- return Has(items, self.item_rules)
-
- if len(items) == 0:
- return True_()
-
- if count is None or count == len(items):
- return And(self.has(item) for item in items)
-
- if count == 1:
- return Or(self.has(item) for item in items)
-
- return Count(count, (self.has(item) for item in items))
-
- def received(self, items: Union[str, Iterable[str]], count: Optional[int] = 1) -> StardewRule:
- if count <= 0 or not items:
- return True_()
-
- if isinstance(items, str):
- return Received(items, self.player, count)
-
- if count is None:
- return And(self.received(item) for item in items)
-
- if count == 1:
- return Or(self.received(item) for item in items)
-
- return TotalReceived(count, items, self.player)
-
- def can_reach_region(self, spot: str) -> StardewRule:
- return Reach(spot, "Region", self.player)
-
- def can_reach_any_region(self, spots: Iterable[str]) -> StardewRule:
- return Or(self.can_reach_region(spot) for spot in spots)
-
- def can_reach_all_regions(self, spots: Iterable[str]) -> StardewRule:
- return And(self.can_reach_region(spot) for spot in spots)
-
- def can_reach_all_regions_except_one(self, spots: Iterable[str]) -> StardewRule:
- num_required = len(list(spots)) - 1
- if num_required <= 0:
- num_required = len(list(spots))
- return Count(num_required, [self.can_reach_region(spot) for spot in spots])
-
- def can_reach_location(self, spot: str) -> StardewRule:
- return Reach(spot, "Location", self.player)
-
- def can_reach_entrance(self, spot: str) -> StardewRule:
- return Reach(spot, "Entrance", self.player)
-
- def can_have_earned_total_money(self, amount: int) -> StardewRule:
- return self.has_lived_months(min(8, amount // MONEY_PER_MONTH))
-
- def can_spend_money(self, amount: int) -> StardewRule:
- if self.options[options.StartingMoney] == -1:
- return True_()
- return self.has_lived_months(min(8, amount // (MONEY_PER_MONTH // 5)))
-
- def can_spend_money_at(self, region: str, amount: int) -> StardewRule:
- return self.can_reach_region(region) & self.can_spend_money(amount)
-
- def has_tool(self, tool: str, material: str = ToolMaterial.basic) -> StardewRule:
- if material == ToolMaterial.basic or tool == Tool.scythe:
- return True_()
-
- if self.options[options.ToolProgression] == options.ToolProgression.option_progressive:
- return self.received(f"Progressive {tool}", count=tool_materials[material])
-
- return self.has(f"{material} Bar") & self.can_spend_money(tool_upgrade_prices[material])
-
- def can_earn_skill_level(self, skill: str, level: int) -> StardewRule:
- if level <= 0:
- return True_()
-
- tool_level = (level - 1) // 2
- tool_material = ToolMaterial.tiers[tool_level]
- months = max(1, level - 1)
- months_rule = self.has_lived_months(months)
- previous_level_rule = self.has_skill_level(skill, level - 1)
-
- if skill == Skill.fishing:
- xp_rule = self.can_get_fishing_xp() & self.has_tool(Tool.fishing_rod, ToolMaterial.tiers[max(tool_level, 3)])
- elif skill == Skill.farming:
- xp_rule = self.can_get_farming_xp() & self.has_tool(Tool.hoe, tool_material) & self.can_water(tool_level)
- elif skill == Skill.foraging:
- xp_rule = self.can_get_foraging_xp() & \
- (self.has_tool(Tool.axe, tool_material) | magic.can_use_clear_debris_instead_of_tool_level(self, tool_level))
- elif skill == Skill.mining:
- xp_rule = self.can_get_mining_xp() & \
- (self.has_tool(Tool.pickaxe, tool_material) | magic.can_use_clear_debris_instead_of_tool_level(self, tool_level))
- elif skill == Skill.combat:
- combat_tier = Performance.tiers[tool_level]
- xp_rule = self.can_get_combat_xp() & self.can_do_combat_at_level(combat_tier)
- else:
- xp_rule = skills.can_earn_mod_skill_level(self, skill, level)
-
- return previous_level_rule & months_rule & xp_rule
-
- def has_skill_level(self, skill: str, level: int) -> StardewRule:
- if level <= 0:
- return True_()
-
- if self.options[options.SkillProgression] == options.SkillProgression.option_progressive:
- return self.received(f"{skill} Level", count=level)
-
- return self.can_earn_skill_level(skill, level)
-
- def has_farming_level(self, level: int) -> StardewRule:
- return self.has_skill_level(Skill.farming, level)
-
- def has_total_skill_level(self, level: int, allow_modded_skills: bool = False) -> StardewRule:
- if level <= 0:
- return True_()
-
- if self.options[options.SkillProgression] == options.SkillProgression.option_progressive:
- skills_items = ["Farming Level", "Mining Level", "Foraging Level",
- "Fishing Level", "Combat Level"]
- if allow_modded_skills:
- skills.append_mod_skill_level(skills_items, self.options)
- return self.received(skills_items, count=level)
-
- months_with_4_skills = max(1, (level // 4) - 1)
- months_with_5_skills = max(1, (level // 5) - 1)
- rule_with_fishing = self.has_lived_months(months_with_5_skills) & self.can_get_fishing_xp()
- if level > 40:
- return rule_with_fishing
- return self.has_lived_months(months_with_4_skills) | rule_with_fishing
-
- def has_building(self, building: str) -> StardewRule:
- carpenter_rule = self.can_reach_region(Region.carpenter)
- if not self.options[options.BuildingProgression] == options.BuildingProgression.option_vanilla:
- count = 1
- if building in [Building.coop, Building.barn, Building.shed]:
- building = f"Progressive {building}"
- elif building.startswith("Big"):
- count = 2
- building = " ".join(["Progressive", *building.split(" ")[1:]])
- elif building.startswith("Deluxe"):
- count = 3
- building = " ".join(["Progressive", *building.split(" ")[1:]])
- return self.received(f"{building}", count) & carpenter_rule
-
- return Has(building, self.building_rules) & carpenter_rule
-
- def has_house(self, upgrade_level: int) -> StardewRule:
- if upgrade_level < 1:
- return True_()
-
- if upgrade_level > 3:
- return False_()
-
- if not self.options[options.BuildingProgression] == options.BuildingProgression.option_vanilla:
- return self.received(f"Progressive House", upgrade_level) & self.can_reach_region(Region.carpenter)
-
- if upgrade_level == 1:
- return Has(Building.kitchen, self.building_rules)
-
- if upgrade_level == 2:
- return Has(Building.kids_room, self.building_rules)
-
- # if upgrade_level == 3:
- return Has(Building.cellar, self.building_rules)
-
- def can_complete_quest(self, quest: str) -> StardewRule:
- return Has(quest, self.quest_rules)
-
- def can_complete_special_order(self, specialorder: str) -> StardewRule:
- return Has(specialorder, self.special_order_rules)
-
- def can_get_farming_xp(self) -> StardewRule:
- crop_rules = []
- for crop in all_crops:
- crop_rules.append(self.can_grow_crop(crop))
- return Or(crop_rules)
-
- def can_get_foraging_xp(self) -> StardewRule:
- tool_rule = self.has_tool(Tool.axe)
- tree_rule = self.can_reach_region(Region.forest) & self.has_any_season_not_winter()
- stump_rule = self.can_reach_region(Region.secret_woods) & self.has_tool(Tool.axe, ToolMaterial.copper)
- return tool_rule & (tree_rule | stump_rule)
-
- def can_get_mining_xp(self) -> StardewRule:
- tool_rule = self.has_tool(Tool.pickaxe)
- stone_rule = self.can_reach_any_region([Region.mines_floor_5, Region.quarry, Region.skull_cavern_25, Region.volcano_floor_5])
- return tool_rule & stone_rule
-
- def can_get_combat_xp(self) -> StardewRule:
- tool_rule = self.has_any_weapon()
- enemy_rule = self.can_reach_any_region([Region.mines_floor_5, Region.skull_cavern_25, Region.volcano_floor_5])
- return tool_rule & enemy_rule
-
- def can_get_fishing_xp(self) -> StardewRule:
- if self.options[options.SkillProgression] == options.SkillProgression.option_progressive:
- return self.can_fish() | self.can_crab_pot()
-
- return self.can_fish()
-
- def can_fish(self, difficulty: int = 0) -> StardewRule:
- skill_required = max(0, int((difficulty / 10) - 1))
- if difficulty <= 40:
- skill_required = 0
- skill_rule = self.has_skill_level(Skill.fishing, skill_required)
- region_rule = self.can_reach_any_region(fishing_regions)
- number_fishing_rod_required = 1 if difficulty < 50 else 2
- if self.options[options.ToolProgression] == options.ToolProgression.option_progressive:
- return self.received("Progressive Fishing Rod", number_fishing_rod_required) & skill_rule & region_rule
-
- return skill_rule & region_rule
-
- def can_fish_in_freshwater(self) -> StardewRule:
- return self.can_fish() & self.can_reach_any_region([Region.forest, Region.town, Region.mountain])
-
- def has_max_fishing(self) -> StardewRule:
- skill_rule = self.has_skill_level(Skill.fishing, 10)
- return self.has_max_fishing_rod() & skill_rule
-
- def can_fish_chests(self) -> StardewRule:
- skill_rule = self.has_skill_level(Skill.fishing, 4)
- return self.has_max_fishing_rod() & skill_rule
-
- def can_buy_seed(self, seed: SeedItem) -> StardewRule:
- if self.options[options.Cropsanity] == options.Cropsanity.option_disabled:
- item_rule = True_()
- else:
- item_rule = self.received(seed.name)
- season_rule = self.has_any_season(seed.seasons)
- region_rule = self.can_reach_all_regions(seed.regions)
- currency_rule = self.can_spend_money(1000)
- if seed.name == Seed.pineapple:
- currency_rule = self.has(Forageable.magma_cap)
- if seed.name == Seed.taro:
- currency_rule = self.has(Fossil.bone_fragment)
- return season_rule & region_rule & item_rule & currency_rule
-
- def can_buy_sapling(self, fruit: str) -> StardewRule:
- sapling_prices = {Fruit.apple: 4000, Fruit.apricot: 2000, Fruit.cherry: 3400, Fruit.orange: 4000,
- Fruit.peach: 6000,
- Fruit.pomegranate: 6000, Fruit.banana: 0, Fruit.mango: 0}
- received_sapling = self.received(f"{fruit} Sapling")
- if self.options[options.Cropsanity] == options.Cropsanity.option_disabled:
- allowed_buy_sapling = True_()
- else:
- allowed_buy_sapling = received_sapling
- can_buy_sapling = self.can_spend_money_at(Region.pierre_store, sapling_prices[fruit])
- if fruit == Fruit.banana:
- can_buy_sapling = self.has_island_trader() & self.has(Forageable.dragon_tooth)
- elif fruit == Fruit.mango:
- can_buy_sapling = self.has_island_trader() & self.has(Fish.mussel_node)
-
- return allowed_buy_sapling & can_buy_sapling
-
- def can_grow_crop(self, crop: CropItem) -> StardewRule:
- season_rule = self.has_any_season(crop.farm_growth_seasons)
- seed_rule = self.has(crop.seed.name)
- farm_rule = self.can_reach_region(Region.farm) & season_rule
- tool_rule = self.has_tool(Tool.hoe) & self.has_tool(Tool.watering_can)
- region_rule = farm_rule | self.can_reach_region(Region.greenhouse) | self.can_reach_region(Region.island_west)
- return seed_rule & region_rule & tool_rule
-
- def can_plant_and_grow_item(self, seasons: Union[str, Iterable[str]]) -> StardewRule:
- if isinstance(seasons, str):
- seasons = [seasons]
- season_rule = self.has_any_season(seasons) | self.can_reach_region(Region.greenhouse) | self.has_island_farm()
- farm_rule = self.can_reach_region(Region.farm) | self.can_reach_region(
- Region.greenhouse) | self.has_island_farm()
- return season_rule & farm_rule
-
- def has_island_farm(self) -> StardewRule:
- return self.can_reach_region(Region.island_south)
-
- def can_catch_fish(self, fish: FishItem) -> StardewRule:
- region_rule = self.can_reach_any_region(fish.locations)
- season_rule = self.has_any_season(fish.seasons)
- if fish.difficulty == -1:
- difficulty_rule = self.can_crab_pot()
- else:
- difficulty_rule = self.can_fish(fish.difficulty)
- return region_rule & season_rule & difficulty_rule
-
- def can_catch_every_fish(self) -> StardewRule:
- rules = [self.has_skill_level(Skill.fishing, 10), self.has_max_fishing_rod()]
- for fish in all_fish:
- if self.options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true and \
- fish in island_fish:
- continue
- rules.append(self.can_catch_fish(fish))
- return And(rules)
-
- def has_max_fishing_rod(self) -> StardewRule:
- if self.options[options.ToolProgression] == options.ToolProgression.option_progressive:
- return self.received(APTool.fishing_rod, 4)
- return self.can_get_fishing_xp()
-
- def can_cook(self, recipe: CookingRecipe = None) -> StardewRule:
- cook_rule = self.has_house(1) | self.has_skill_level(Skill.foraging, 9)
- if recipe is None:
- return cook_rule
-
- learn_rule = self.can_learn_recipe(recipe.source)
- ingredients_rule = And([self.has(ingredient) for ingredient in recipe.ingredients])
- number_ingredients = sum(recipe.ingredients[ingredient] for ingredient in recipe.ingredients)
- time_rule = self.has_lived_months(number_ingredients)
- return cook_rule & learn_rule & ingredients_rule & time_rule
-
- def can_learn_recipe(self, source: RecipeSource) -> StardewRule:
- if isinstance(source, StarterSource):
- return True_()
- if isinstance(source, ShopSource):
- return self.can_spend_money_at(source.region, source.price)
- if isinstance(source, SkillSource):
- return self.has_skill_level(source.skill, source.level)
- if isinstance(source, FriendshipSource):
- return self.has_relationship(source.friend, source.hearts)
- if isinstance(source, QueenOfSauceSource):
- year_rule = self.has_year_two() if source.year == 2 else self.has_year_three()
- return self.can_watch(Channel.queen_of_sauce) & self.has_season(source.season) & year_rule
-
- return False_()
-
- def can_watch(self, channel: str = None):
- tv_rule = True_()
- if channel is None:
- return tv_rule
- return self.received(channel) & tv_rule
-
- def can_smelt(self, item: str) -> StardewRule:
- return self.has(Machine.furnace) & self.has(item)
-
- def can_do_panning(self, item: str = Generic.any) -> StardewRule:
- return self.received("Glittering Boulder Removed")
-
- def can_crab_pot(self, region: str = Generic.any) -> StardewRule:
- crab_pot_rule = self.has(Craftable.bait)
- if self.options[options.SkillProgression] == options.SkillProgression.option_progressive:
- crab_pot_rule = crab_pot_rule & self.has(Machine.crab_pot)
- else:
- crab_pot_rule = crab_pot_rule & self.can_get_fishing_xp()
-
- if region != Generic.any:
- return crab_pot_rule & self.can_reach_region(region)
-
- water_region_rules = self.can_reach_any_region(fishing_regions)
- return crab_pot_rule & water_region_rules
-
- # Regions
- def can_mine_in_the_mines_floor_1_40(self) -> StardewRule:
- return self.can_reach_region(Region.mines_floor_5)
-
- def can_mine_in_the_mines_floor_41_80(self) -> StardewRule:
- return self.can_reach_region(Region.mines_floor_45)
-
- def can_mine_in_the_mines_floor_81_120(self) -> StardewRule:
- return self.can_reach_region(Region.mines_floor_85)
-
- def can_mine_in_the_skull_cavern(self) -> StardewRule:
- return (self.can_progress_in_the_mines_from_floor(120) &
- self.can_reach_region(Region.skull_cavern))
-
- def can_mine_perfectly(self) -> StardewRule:
- return self.can_progress_in_the_mines_from_floor(160)
-
- def can_mine_perfectly_in_the_skull_cavern(self) -> StardewRule:
- return (self.can_mine_perfectly() &
- self.can_reach_region(Region.skull_cavern))
-
- def can_farm_perfectly(self) -> StardewRule:
- tool_rule = self.has_tool(Tool.hoe, ToolMaterial.iridium) & self.can_water(4)
- return tool_rule & self.has_farming_level(10)
-
- def can_fish_perfectly(self) -> StardewRule:
- skill_rule = self.has_skill_level(Skill.fishing, 10)
- return skill_rule & self.has_max_fishing_rod()
-
- def can_chop_trees(self) -> StardewRule:
- return self.has_tool(Tool.axe) & self.can_reach_region(Region.forest)
-
- def can_chop_perfectly(self) -> StardewRule:
- magic_rule = (magic.can_use_clear_debris_instead_of_tool_level(self, 3)) & self.has_skill_level(ModSkill.magic, 10)
- tool_rule = self.has_tool(Tool.axe, ToolMaterial.iridium)
- foraging_rule = self.has_skill_level(Skill.foraging, 10)
- region_rule = self.can_reach_region(Region.forest)
- return region_rule & ((tool_rule & foraging_rule) | magic_rule)
-
- def has_max_buffs(self) -> StardewRule:
- number_of_movement_buffs: int = self.options[options.NumberOfMovementBuffs]
- number_of_luck_buffs: int = self.options[options.NumberOfLuckBuffs]
- return self.received(Buff.movement, number_of_movement_buffs) & self.received(Buff.luck, number_of_luck_buffs)
-
- def get_weapon_rule_for_floor_tier(self, tier: int):
- if tier >= 4:
- return self.can_do_combat_at_level(Performance.galaxy)
- if tier >= 3:
- return self.can_do_combat_at_level(Performance.great)
- if tier >= 2:
- return self.can_do_combat_at_level(Performance.good)
- if tier >= 1:
- return self.can_do_combat_at_level(Performance.decent)
- return self.can_do_combat_at_level(Performance.basic)
-
- def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule:
- tier = int(floor / 40)
- rules = []
- weapon_rule = self.get_weapon_rule_for_floor_tier(tier)
- rules.append(weapon_rule)
- if self.options[options.ToolProgression] == options.ToolProgression.option_progressive:
- rules.append(self.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier]))
- if self.options[options.SkillProgression] == options.SkillProgression.option_progressive:
- combat_tier = min(10, max(0, tier * 2))
- rules.append(self.has_skill_level(Skill.combat, combat_tier))
- return And(rules)
-
- def can_progress_easily_in_the_mines_from_floor(self, floor: int) -> StardewRule:
- tier = int(floor / 40) + 1
- rules = []
- weapon_rule = self.get_weapon_rule_for_floor_tier(tier)
- rules.append(weapon_rule)
- if self.options[options.ToolProgression] == options.ToolProgression.option_progressive:
- rules.append(self.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier]))
- if self.options[options.SkillProgression] == options.SkillProgression.option_progressive:
- combat_tier = min(10, max(0, tier * 2))
- rules.append(self.has_skill_level(Skill.combat, combat_tier))
- return And(rules)
-
- def has_mine_elevator_to_floor(self, floor: int) -> StardewRule:
- if self.options[options.ElevatorProgression] != options.ElevatorProgression.option_vanilla:
- return self.received("Progressive Mine Elevator", count=int(floor / 5))
- return True_()
-
- def can_mine_to_floor(self, floor: int) -> StardewRule:
- previous_elevator = max(floor - 5, 0)
- previous_previous_elevator = max(floor - 10, 0)
- return ((self.has_mine_elevator_to_floor(previous_elevator) &
- self.can_progress_in_the_mines_from_floor(previous_elevator)) |
- (self.has_mine_elevator_to_floor(previous_previous_elevator) &
- self.can_progress_easily_in_the_mines_from_floor(previous_previous_elevator)))
-
- def can_progress_in_the_skull_cavern_from_floor(self, floor: int) -> StardewRule:
- tier = floor // 50
- rules = []
- weapon_rule = self.has_great_weapon()
- rules.append(weapon_rule)
- rules.append(self.can_cook())
- if self.options[options.ToolProgression] == options.ToolProgression.option_progressive:
- rules.append(self.received("Progressive Pickaxe", min(4, max(0, tier + 2))))
- if self.options[options.SkillProgression] == options.SkillProgression.option_progressive:
- skill_tier = min(10, max(0, tier * 2 + 6))
- rules.extend({self.has_skill_level(Skill.combat, skill_tier),
- self.has_skill_level(Skill.mining, skill_tier)})
- return And(rules)
-
- def can_progress_easily_in_the_skull_cavern_from_floor(self, floor: int) -> StardewRule:
- return self.can_progress_in_the_skull_cavern_from_floor(floor + 50)
-
- def can_mine_to_skull_cavern_floor(self, floor: int) -> StardewRule:
- previous_elevator = max(floor - 25, 0)
- previous_previous_elevator = max(floor - 50, 0)
- has_mine_elevator = self.has_mine_elevator_to_floor(5) # Skull Cavern Elevator menu needs a normal elevator...
- return ((has_skull_cavern_elevator_to_floor(self, previous_elevator) &
- self.can_progress_in_the_skull_cavern_from_floor(previous_elevator)) |
- (has_skull_cavern_elevator_to_floor(self, previous_previous_elevator) &
- self.can_progress_easily_in_the_skull_cavern_from_floor(previous_previous_elevator))) & has_mine_elevator
-
- def has_jotpk_power_level(self, power_level: int) -> StardewRule:
- if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling:
- return True_()
- jotpk_buffs = ["JotPK: Progressive Boots", "JotPK: Progressive Gun",
- "JotPK: Progressive Ammo", "JotPK: Extra Life", "JotPK: Increased Drop Rate"]
- return self.received(jotpk_buffs, power_level)
-
- def has_junimo_kart_power_level(self, power_level: int) -> StardewRule:
- if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling:
- return True_()
- return self.received("Junimo Kart: Extra Life", power_level)
-
- def has_junimo_kart_max_level(self) -> StardewRule:
- play_rule = self.can_reach_region(Region.junimo_kart_3)
- if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling:
- return play_rule
- return self.has_junimo_kart_power_level(8)
-
- def has_traveling_merchant(self, tier: int = 1):
- traveling_merchant_days = [f"Traveling Merchant: {day}" for day in Weekday.all_days]
- return self.received(traveling_merchant_days, tier)
-
- def can_get_married(self) -> StardewRule:
- return self.has_relationship(Generic.bachelor, 10) & self.has(Gift.mermaid_pendant)
-
- def has_children(self, number_children: int) -> StardewRule:
- if number_children <= 0:
- return True_()
- possible_kids = ["Cute Baby", "Ugly Baby"]
- return self.received(possible_kids, number_children) & self.has_house(2)
-
- def can_reproduce(self, number_children: int = 1) -> StardewRule:
- if number_children <= 0:
- return True_()
- return self.can_get_married() & self.has_house(2) & self.has_relationship(Generic.bachelor, 12) & self.has_children(number_children - 1)
-
- def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule:
- if hearts <= 0:
- return True_()
- friendsanity = self.options[options.Friendsanity]
- if friendsanity == options.Friendsanity.option_none:
- return self.can_earn_relationship(npc, hearts)
- if npc not in all_villagers_by_name:
- if npc == NPC.pet:
- if friendsanity == options.Friendsanity.option_bachelors:
- return self.can_befriend_pet(hearts)
- return self.received_hearts(NPC.pet, hearts)
- if npc == Generic.any or npc == Generic.bachelor:
- possible_friends = []
- for name in all_villagers_by_name:
- if not self.npc_is_in_current_slot(name):
- continue
- if npc == Generic.any or all_villagers_by_name[name].bachelor:
- possible_friends.append(self.has_relationship(name, hearts))
- return Or(possible_friends)
- if npc == Generic.all:
- mandatory_friends = []
- for name in all_villagers_by_name:
- if not self.npc_is_in_current_slot(name):
- continue
- mandatory_friends.append(self.has_relationship(name, hearts))
- return And(mandatory_friends)
- if npc.isnumeric():
- possible_friends = []
- for name in all_villagers_by_name:
- if not self.npc_is_in_current_slot(name):
- continue
- possible_friends.append(self.has_relationship(name, hearts))
- return Count(int(npc), possible_friends)
- return self.can_earn_relationship(npc, hearts)
-
- if not self.npc_is_in_current_slot(npc):
- return True_()
- villager = all_villagers_by_name[npc]
- if friendsanity == options.Friendsanity.option_bachelors and not villager.bachelor:
- return self.can_earn_relationship(npc, hearts)
- if friendsanity == options.Friendsanity.option_starting_npcs and not villager.available:
- return self.can_earn_relationship(npc, hearts)
- is_capped_at_8 = villager.bachelor and friendsanity != options.Friendsanity.option_all_with_marriage
- if is_capped_at_8 and hearts > 8:
- return self.received_hearts(villager, 8) & self.can_earn_relationship(npc, hearts)
- return self.received_hearts(villager, hearts)
-
- def received_hearts(self, npc: Union[str, Villager], hearts: int) -> StardewRule:
- if isinstance(npc, Villager):
- return self.received_hearts(npc.name, hearts)
- heart_size: int = self.options[options.FriendsanityHeartSize]
- return self.received(self.heart(npc), math.ceil(hearts / heart_size))
-
- def can_meet(self, npc: str) -> StardewRule:
- if npc not in all_villagers_by_name or not self.npc_is_in_current_slot(npc):
- return True_()
- villager = all_villagers_by_name[npc]
- rules = [self.can_reach_any_region(villager.locations)]
- if npc == NPC.kent:
- rules.append(self.has_year_two())
- elif npc == NPC.leo:
- rules.append(self.received("Island West Turtle"))
-
- return And(rules)
-
- def can_give_loved_gifts_to_everyone(self) -> StardewRule:
- rules = []
- for npc in all_villagers_by_name:
- if not self.npc_is_in_current_slot(npc):
- continue
- villager = all_villagers_by_name[npc]
- gift_rule = self.has_any_universal_love()
- meet_rule = self.can_meet(npc)
- rules.append(meet_rule & gift_rule)
- loved_gifts_rules = And(rules)
- simplified_rules = loved_gifts_rules.simplify()
- return simplified_rules
-
- def can_earn_relationship(self, npc: str, hearts: int = 0) -> StardewRule:
- if hearts <= 0:
- return True_()
-
- heart_size: int = self.options[options.FriendsanityHeartSize]
- previous_heart = hearts - heart_size
- previous_heart_rule = self.has_relationship(npc, previous_heart)
-
- if npc == NPC.pet:
- earn_rule = self.can_befriend_pet(hearts)
- elif npc == NPC.wizard and ModNames.magic in self.options[options.Mods]:
- earn_rule = self.can_meet(npc) & self.has_lived_months(hearts)
- elif npc in all_villagers_by_name:
- if not self.npc_is_in_current_slot(npc):
- return previous_heart_rule
- villager = all_villagers_by_name[npc]
- rule_if_birthday = self.has_season(villager.birthday) & self.has_any_universal_love() & self.has_lived_months(hearts // 2)
- rule_if_not_birthday = self.has_lived_months(hearts)
- earn_rule = self.can_meet(npc) & (rule_if_birthday | rule_if_not_birthday)
- if villager.bachelor:
- if hearts > 8:
- earn_rule = earn_rule & self.can_date(npc)
- if hearts > 10:
- earn_rule = earn_rule & self.can_marry(npc)
- else:
- earn_rule = self.has_lived_months(min(hearts // 2, 8))
-
- return previous_heart_rule & earn_rule
-
- def can_date(self, npc: str) -> StardewRule:
- return self.has_relationship(npc, 8) & self.has(Gift.bouquet)
-
- def can_marry(self, npc: str) -> StardewRule:
- return self.has_relationship(npc, 10) & self.has(Gift.mermaid_pendant)
-
- def can_befriend_pet(self, hearts: int):
- if hearts <= 0:
- return True_()
- points = hearts * 200
- points_per_month = 12 * 14
- points_per_water_month = 18 * 14
- return self.can_reach_region(Region.farm) & \
- ((self.can_water(0) & self.has_lived_months(points // points_per_water_month)) |
- self.has_lived_months(points // points_per_month))
-
- def can_complete_bundle(self, bundle_requirements: List[BundleItem], number_required: int) -> StardewRule:
- item_rules = []
- highest_quality_yet = 0
- for bundle_item in bundle_requirements:
- if bundle_item.item.item_id == -1:
- return self.can_spend_money(bundle_item.amount)
- else:
- item_rules.append(bundle_item.item.name)
- if bundle_item.quality > highest_quality_yet:
- highest_quality_yet = bundle_item.quality
- return self.can_reach_region(Region.wizard_tower) & self.has(item_rules, number_required) & self.can_grow_gold_quality(highest_quality_yet)
-
- def can_grow_gold_quality(self, quality: int) -> StardewRule:
- if quality <= 0:
- return True_()
- if quality == 1:
- return self.has_farming_level(5) | (self.has_fertilizer(1) & self.has_farming_level(2)) | (
- self.has_fertilizer(2) & self.has_farming_level(1)) | self.has_fertilizer(3)
- if quality == 2:
- return self.has_farming_level(10) | (self.has_fertilizer(1) & self.has_farming_level(5)) | (
- self.has_fertilizer(2) & self.has_farming_level(3)) | (
- self.has_fertilizer(3) & self.has_farming_level(2))
- if quality >= 3:
- return self.has_fertilizer(3) & self.has_farming_level(4)
-
- def has_fertilizer(self, tier: int) -> StardewRule:
- if tier <= 0:
- return True_()
- if tier == 1:
- return self.has(Fertilizer.basic)
- if tier == 2:
- return self.has(Fertilizer.quality)
- if tier >= 3:
- return self.has(Fertilizer.deluxe)
-
- def can_complete_field_office(self) -> StardewRule:
- field_office = self.can_reach_region(Region.field_office)
- professor_snail = self.received("Open Professor Snail Cave")
- dig_site = self.can_reach_region(Region.dig_site)
- tools = self.has_tool(Tool.pickaxe) & self.has_tool(Tool.hoe) & self.has_tool(Tool.scythe)
- leg_and_snake_skull = dig_site
- ribs_and_spine = self.can_reach_region(Region.island_south)
- skull = self.can_open_geode(Geode.golden_coconut)
- tail = self.can_do_panning() & dig_site
- frog = self.can_reach_region(Region.island_east)
- bat = self.can_reach_region(Region.volcano_floor_5)
- snake_vertebrae = self.can_reach_region(Region.island_west)
- return field_office & professor_snail & tools & leg_and_snake_skull & ribs_and_spine & skull & tail & frog & bat & snake_vertebrae
-
- def can_complete_community_center(self) -> StardewRule:
- return (self.can_reach_location("Complete Crafts Room") &
- self.can_reach_location("Complete Pantry") &
- self.can_reach_location("Complete Fish Tank") &
- self.can_reach_location("Complete Bulletin Board") &
- self.can_reach_location("Complete Vault") &
- self.can_reach_location("Complete Boiler Room"))
-
- def can_finish_grandpa_evaluation(self) -> StardewRule:
- # https://stardewvalleywiki.com/Grandpa
- rules_worth_a_point = [self.can_have_earned_total_money(50000), # 50 000g
- self.can_have_earned_total_money(100000), # 100 000g
- self.can_have_earned_total_money(200000), # 200 000g
- self.can_have_earned_total_money(300000), # 300 000g
- self.can_have_earned_total_money(500000), # 500 000g
- self.can_have_earned_total_money(1000000), # 1 000 000g first point
- self.can_have_earned_total_money(1000000), # 1 000 000g second point
- self.has_total_skill_level(30), # Total Skills: 30
- self.has_total_skill_level(50), # Total Skills: 50
- self.can_complete_museum(), # Completing the museum for a point
- # Catching every fish not expected
- # Shipping every item not expected
- self.can_get_married() & self.has_house(2),
- self.has_relationship("5", 8), # 5 Friends
- self.has_relationship("10", 8), # 10 friends
- self.has_relationship(NPC.pet, 5), # Max Pet
- self.can_complete_community_center(), # Community Center Completion
- self.can_complete_community_center(), # CC Ceremony first point
- self.can_complete_community_center(), # CC Ceremony second point
- self.received(Wallet.skull_key), # Skull Key obtained
- self.has_rusty_key(), # Rusty key obtained
- ]
- return Count(12, rules_worth_a_point)
-
- def has_island_transport(self) -> StardewRule:
- return self.received(Transportation.island_obelisk) | self.received(Transportation.boat_repair)
-
- def has_any_weapon(self) -> StardewRule:
- return self.has_decent_weapon() | self.received(item.name for item in all_items if Group.WEAPON in item.groups)
-
- def has_decent_weapon(self) -> StardewRule:
- return (self.has_good_weapon() |
- self.received(item.name for item in all_items
- if Group.WEAPON in item.groups and
- (Group.MINES_FLOOR_50 in item.groups or Group.MINES_FLOOR_60 in item.groups)))
-
- def has_good_weapon(self) -> StardewRule:
- return ((self.has_great_weapon() |
- self.received(item.name for item in all_items
- if Group.WEAPON in item.groups and
- (Group.MINES_FLOOR_80 in item.groups or Group.MINES_FLOOR_90 in item.groups))) &
- self.received("Adventurer's Guild"))
-
- def has_great_weapon(self) -> StardewRule:
- return ((self.has_galaxy_weapon() |
- self.received(item.name for item in all_items
- if Group.WEAPON in item.groups and Group.MINES_FLOOR_110 in item.groups)) &
- self.received("Adventurer's Guild"))
-
- def has_galaxy_weapon(self) -> StardewRule:
- return (self.received(item.name for item in all_items
- if Group.WEAPON in item.groups and Group.GALAXY_WEAPONS in item.groups) &
- self.received("Adventurer's Guild"))
-
- def has_year_two(self) -> StardewRule:
- return self.has_lived_months(4)
-
- def has_year_three(self) -> StardewRule:
- return self.has_lived_months(8)
-
- def can_speak_dwarf(self) -> StardewRule:
- if self.options[options.Museumsanity] == options.Museumsanity.option_none:
- return And([self.can_donate_museum_item(item) for item in dwarf_scrolls])
- return self.received("Dwarvish Translation Guide")
-
- def can_donate_museum_item(self, item: MuseumItem) -> StardewRule:
- return self.can_reach_region(Region.museum) & self.can_find_museum_item(item)
-
- def can_donate_museum_items(self, number: int) -> StardewRule:
- return self.can_reach_region(Region.museum) & self.can_find_museum_items(number)
-
- def can_donate_museum_artifacts(self, number: int) -> StardewRule:
- return self.can_reach_region(Region.museum) & self.can_find_museum_artifacts(number)
-
- def can_donate_museum_minerals(self, number: int) -> StardewRule:
- return self.can_reach_region(Region.museum) & self.can_find_museum_minerals(number)
-
- def can_find_museum_item(self, item: MuseumItem) -> StardewRule:
- region_rule = self.can_reach_all_regions_except_one(item.locations)
- geodes_rule = And([self.can_open_geode(geode) for geode in item.geodes])
- # monster_rule = self.can_farm_monster(item.monsters)
- # extra_rule = True_()
- pan_rule = False_()
- if item.name == "Earth Crystal" or item.name == "Fire Quartz" or item.name == "Frozen Tear":
- pan_rule = self.can_do_panning()
- return pan_rule | (region_rule & geodes_rule) # & monster_rule & extra_rule
-
- def can_find_museum_artifacts(self, number: int) -> StardewRule:
- rules = []
- for artifact in all_museum_artifacts:
- rules.append(self.can_find_museum_item(artifact))
-
- return Count(number, rules)
-
- def can_find_museum_minerals(self, number: int) -> StardewRule:
- rules = []
- for mineral in all_museum_minerals:
- rules.append(self.can_find_museum_item(mineral))
-
- return Count(number, rules)
-
- def can_find_museum_items(self, number: int) -> StardewRule:
- rules = []
- for donation in all_museum_items:
- rules.append(self.can_find_museum_item(donation))
-
- return Count(number, rules)
-
- def can_complete_museum(self) -> StardewRule:
- rules = [self.can_reach_region(Region.museum), self.can_mine_perfectly()]
-
- if self.options[options.Museumsanity] != options.Museumsanity.option_none:
- rules.append(self.received("Traveling Merchant Metal Detector", 4))
-
- for donation in all_museum_items:
- rules.append(self.can_find_museum_item(donation))
- return And(rules)
-
- def has_season(self, season: str) -> StardewRule:
- if season == Generic.any:
- return True_()
- seasons_order = [Season.spring, Season.summer, Season.fall, Season.winter]
- if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive:
- return self.received(Season.progressive, seasons_order.index(season))
- if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled:
- if season == Season.spring:
- return True_()
- return self.has_lived_months(1)
- return self.received(season)
-
- def has_any_season(self, seasons: Iterable[str]):
- if not seasons:
- return True_()
- return Or([self.has_season(season) for season in seasons])
-
- def has_any_season_not_winter(self):
- return self.has_any_season([Season.spring, Season.summer, Season.fall])
-
- def has_all_seasons(self, seasons: Iterable[str]):
- if not seasons:
- return True_()
- return And([self.has_season(season) for season in seasons])
-
- def has_lived_months(self, number: int) -> StardewRule:
- number = max(0, min(number, MAX_MONTHS))
- return self.received("Month End", number)
-
- def has_rusty_key(self) -> StardewRule:
- if self.options[options.Museumsanity] == options.Museumsanity.option_none:
- required_donations = 80 # It's 60, but without a metal detector I'd rather overshoot so players don't get screwed by RNG
- return self.has([item.name for item in all_museum_items], required_donations) & self.can_reach_region(Region.museum)
- return self.received(Wallet.rusty_key)
-
- def can_win_egg_hunt(self) -> StardewRule:
- number_of_movement_buffs: int = self.options[options.NumberOfMovementBuffs]
- if self.options[options.FestivalLocations] == options.FestivalLocations.option_hard or number_of_movement_buffs < 2:
- return True_()
- return self.received(Buff.movement, number_of_movement_buffs // 2)
-
- def can_succeed_luau_soup(self) -> StardewRule:
- if self.options[options.FestivalLocations] != options.FestivalLocations.option_hard:
- return True_()
- eligible_fish = [Fish.blobfish, Fish.crimsonfish, "Ice Pip", Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish,
- Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, "Super Cucumber"]
- fish_rule = [self.has(fish) for fish in eligible_fish]
- eligible_kegables = [Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango,
- Fruit.melon, Fruit.orange, Fruit.peach, Fruit.pineapple, Fruit.pomegranate, Fruit.rhubarb,
- Fruit.starfruit, Fruit.strawberry, Forageable.cactus_fruit,
- Fruit.cherry, Fruit.cranberries, Fruit.grape, Forageable.spice_berry, Forageable.wild_plum, Vegetable.hops, Vegetable.wheat]
- keg_rules = [self.can_keg(kegable) for kegable in eligible_kegables]
- aged_rule = [self.can_age(rule, "Iridium") for rule in keg_rules]
- # There are a few other valid items but I don't feel like coding them all
- return Or(fish_rule) | Or(aged_rule)
-
- def can_succeed_grange_display(self) -> StardewRule:
- if self.options[options.FestivalLocations] != options.FestivalLocations.option_hard:
- return True_()
- animal_rule = self.has_animal(Generic.any)
- artisan_rule = self.can_keg(Generic.any) | self.can_preserves_jar(Generic.any)
- cooking_rule = True_() # Salads at the bar are good enough
- fish_rule = self.can_fish(50)
- forage_rule = True_() # Hazelnut always available since the grange display is in fall
- mineral_rule = self.can_open_geode(Generic.any) # More than half the minerals are good enough
- good_fruits = [Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach,
- Fruit.pomegranate,
- Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit, ]
- fruit_rule = Or([self.has(fruit) for fruit in good_fruits])
- good_vegetables = [Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale,
- Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin]
- vegetable_rule = Or([self.has(vegetable) for vegetable in good_vegetables])
-
- return animal_rule & artisan_rule & cooking_rule & fish_rule & \
- forage_rule & fruit_rule & mineral_rule & vegetable_rule
-
- def can_win_fishing_competition(self) -> StardewRule:
- return self.can_fish(60)
-
- def has_any_universal_love(self) -> StardewRule:
- return self.has(Gift.golden_pumpkin) | self.has("Magic Rock Candy") | self.has(Gift.pearl) | self.has(
- "Prismatic Shard") | self.has(AnimalProduct.rabbit_foot)
-
- def has_jelly(self) -> StardewRule:
- return self.can_preserves_jar(Fruit.any)
-
- def has_pickle(self) -> StardewRule:
- return self.can_preserves_jar(Vegetable.any)
-
- def can_preserves_jar(self, item: str) -> StardewRule:
- machine_rule = self.has(Machine.preserves_jar)
- if item == Generic.any:
- return machine_rule
- if item == Fruit.any:
- return machine_rule & self.has(all_fruits, 1)
- if item == Vegetable.any:
- return machine_rule & self.has(all_vegetables, 1)
- return machine_rule & self.has(item)
-
- def has_wine(self) -> StardewRule:
- return self.can_keg(Fruit.any)
-
- def has_juice(self) -> StardewRule:
- return self.can_keg(Vegetable.any)
-
- def can_keg(self, item: str) -> StardewRule:
- machine_rule = self.has(Machine.keg)
- if item == Generic.any:
- return machine_rule
- if item == Fruit.any:
- return machine_rule & self.has(all_fruits, 1)
- if item == Vegetable.any:
- return machine_rule & self.has(all_vegetables, 1)
- return machine_rule & self.has(item)
-
- def can_age(self, item: Union[str, StardewRule], quality: str) -> StardewRule:
- months = 1
- if quality == "Gold":
- months = 2
- elif quality == "Iridium":
- months = 3
- if isinstance(item, str):
- rule = self.has(item)
- else:
- rule: StardewRule = item
- return self.has(Machine.cask) & self.has_lived_months(months) & rule
-
- def can_buy_animal(self, animal: str) -> StardewRule:
- price = 0
- building = ""
- if animal == Animal.chicken:
- price = 800
- building = Building.coop
- elif animal == Animal.cow:
- price = 1500
- building = Building.barn
- elif animal == Animal.goat:
- price = 4000
- building = Building.big_barn
- elif animal == Animal.duck:
- price = 1200
- building = Building.big_coop
- elif animal == Animal.sheep:
- price = 8000
- building = Building.deluxe_barn
- elif animal == Animal.rabbit:
- price = 8000
- building = Building.deluxe_coop
- elif animal == Animal.pig:
- price = 16000
- building = Building.deluxe_barn
- else:
- return True_()
- return self.can_spend_money_at(Region.ranch, price) & self.has_building(building)
-
- def has_animal(self, animal: str) -> StardewRule:
- if animal == Generic.any:
- return self.has_any_animal()
- elif animal == Building.coop:
- return self.has_any_coop_animal()
- elif animal == Building.barn:
- return self.has_any_barn_animal()
- return self.has(animal)
-
- def has_happy_animal(self, animal: str) -> StardewRule:
- return self.has_animal(animal) & self.has(Forageable.hay)
-
- def has_any_animal(self) -> StardewRule:
- return self.has_any_coop_animal() | self.has_any_barn_animal()
-
- def has_any_coop_animal(self) -> StardewRule:
- coop_rule = Or([self.has_animal(coop_animal) for coop_animal in coop_animals])
- return coop_rule
-
- def has_any_barn_animal(self) -> StardewRule:
- barn_rule = Or([self.has_animal(barn_animal) for barn_animal in barn_animals])
- return barn_rule
-
- def can_open_geode(self, geode: str) -> StardewRule:
- blacksmith_access = self.can_reach_region("Clint's Blacksmith")
- geodes = [Geode.geode, Geode.frozen, Geode.magma, Geode.omni]
- if geode == Generic.any:
- return blacksmith_access & Or([self.has(geode_type) for geode_type in geodes])
- return blacksmith_access & self.has(geode)
-
- def has_island_trader(self) -> StardewRule:
- if self.options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
- return False_()
- return self.can_reach_region(Region.island_trader)
-
- def has_walnut(self, number: int) -> StardewRule:
- if self.options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
- return False_()
- if number <= 0:
- return True_()
- # https://stardewcommunitywiki.com/Golden_Walnut#Walnut_Locations
- reach_south = self.can_reach_region(Region.island_south)
- reach_north = self.can_reach_region(Region.island_north)
- reach_west = self.can_reach_region(Region.island_west)
- reach_hut = self.can_reach_region(Region.leo_hut)
- reach_southeast = self.can_reach_region(Region.island_south_east)
- reach_pirate_cove = self.can_reach_region(Region.pirate_cove)
- reach_outside_areas = And(reach_south, reach_north, reach_west, reach_hut)
- reach_volcano_regions = [self.can_reach_region(Region.volcano),
- self.can_reach_region(Region.volcano_secret_beach),
- self.can_reach_region(Region.volcano_floor_5),
- self.can_reach_region(Region.volcano_floor_10)]
- reach_volcano = Or(reach_volcano_regions)
- reach_all_volcano = And(reach_volcano_regions)
- reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano]
- reach_caves = And(self.can_reach_region(Region.qi_walnut_room), self.can_reach_region(Region.dig_site),
- self.can_reach_region(Region.gourmand_frog_cave),
- self.can_reach_region(Region.colored_crystals_cave),
- self.can_reach_region(Region.shipwreck), self.has(Weapon.any_slingshot))
- reach_entire_island = And(reach_outside_areas, reach_all_volcano,
- reach_caves, reach_southeast, reach_pirate_cove)
- if number <= 5:
- return Or(reach_south, reach_north, reach_west, reach_volcano)
- if number <= 10:
- return Count(2, reach_walnut_regions)
- if number <= 15:
- return Count(3, reach_walnut_regions)
- if number <= 20:
- return And(reach_walnut_regions)
- if number <= 50:
- return reach_entire_island
- gems = [Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz]
- return reach_entire_island & self.has(Fruit.banana) & self.has(gems) & self.can_mine_perfectly() & \
- self.can_fish_perfectly() & self.has(Craftable.flute_block) & self.has(Seed.melon) & self.has(Seed.wheat) & self.has(Seed.garlic)
-
- def has_everything(self, all_progression_items: Set[str]) -> StardewRule:
- all_regions = [region.name for region in vanilla_regions]
- rules = self.received(all_progression_items, len(all_progression_items)) & \
- self.can_reach_all_regions(all_regions)
- return rules
-
- def heart(self, npc: Union[str, Villager]) -> str:
- if isinstance(npc, str):
- return f"{npc} <3"
- return self.heart(npc.name)
-
- def can_forage(self, season: str, region: str = Region.forest, need_hoe: bool = False) -> StardewRule:
- season_rule = self.has_season(season)
- region_rule = self.can_reach_region(region)
- if need_hoe:
- return season_rule & region_rule & self.has_tool(Tool.hoe)
- return season_rule & region_rule
-
- def npc_is_in_current_slot(self, name: str) -> bool:
- npc = all_villagers_by_name[name]
- mod = npc.mod_name
- return mod is None or mod in self.options[options.Mods]
-
- def can_do_combat_at_level(self, level: str) -> StardewRule:
- if level == Performance.basic:
- return self.has_any_weapon() | magic.has_any_spell(self)
- if level == Performance.decent:
- return self.has_decent_weapon() | magic.has_decent_spells(self)
- if level == Performance.good:
- return self.has_good_weapon() | magic.has_good_spells(self)
- if level == Performance.great:
- return self.has_great_weapon() | magic.has_great_spells(self)
- if level == Performance.galaxy:
- return self.has_galaxy_weapon() | magic.has_amazing_spells(self)
-
- def can_water(self, level: int) -> StardewRule:
- tool_rule = self.has_tool(Tool.watering_can, ToolMaterial.tiers[level])
- spell_rule = (self.received(MagicSpell.water) & magic.can_use_altar(self) & self.has_skill_level(ModSkill.magic, level))
- return tool_rule | spell_rule
-
- def has_prismatic_jelly_reward_access(self) -> StardewRule:
- if self.options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled:
- return self.can_complete_special_order("Prismatic Jelly")
- return self.received("Monster Musk Recipe")
-
- def has_all_rarecrows(self) -> StardewRule:
- rules = []
- for rarecrow_number in range(1, 9):
- rules.append(self.received(f"Rarecrow #{rarecrow_number}"))
- return And(rules)
-
- def can_ship(self, item: str = "") -> StardewRule:
- shipping_bin_rule = self.has_building(Building.shipping_bin)
- if item == "":
- return shipping_bin_rule
- return shipping_bin_rule & self.has(item)
-
diff --git a/worlds/stardew_valley/logic/__init__.py b/worlds/stardew_valley/logic/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/stardew_valley/logic/ability_logic.py b/worlds/stardew_valley/logic/ability_logic.py
new file mode 100644
index 000000000000..add99a2c2e7e
--- /dev/null
+++ b/worlds/stardew_valley/logic/ability_logic.py
@@ -0,0 +1,47 @@
+from typing import Union
+
+from .base_logic import BaseLogicMixin, BaseLogic
+from .cooking_logic import CookingLogicMixin
+from .mine_logic import MineLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .skill_logic import SkillLogicMixin
+from .tool_logic import ToolLogicMixin
+from ..mods.logic.magic_logic import MagicLogicMixin
+from ..stardew_rule import StardewRule
+from ..strings.region_names import Region
+from ..strings.skill_names import Skill, ModSkill
+from ..strings.tool_names import ToolMaterial, Tool
+
+
+class AbilityLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.ability = AbilityLogic(*args, **kwargs)
+
+
+class AbilityLogic(BaseLogic[Union[AbilityLogicMixin, RegionLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, MineLogicMixin, MagicLogicMixin]]):
+ def can_mine_perfectly(self) -> StardewRule:
+ return self.logic.mine.can_progress_in_the_mines_from_floor(160)
+
+ def can_mine_perfectly_in_the_skull_cavern(self) -> StardewRule:
+ return (self.logic.ability.can_mine_perfectly() &
+ self.logic.region.can_reach(Region.skull_cavern))
+
+ def can_farm_perfectly(self) -> StardewRule:
+ tool_rule = self.logic.tool.has_tool(Tool.hoe, ToolMaterial.iridium) & self.logic.tool.can_water(4)
+ return tool_rule & self.logic.skill.has_farming_level(10)
+
+ def can_fish_perfectly(self) -> StardewRule:
+ skill_rule = self.logic.skill.has_level(Skill.fishing, 10)
+ return skill_rule & self.logic.tool.has_fishing_rod(4)
+
+ def can_chop_trees(self) -> StardewRule:
+ return self.logic.tool.has_tool(Tool.axe) & self.logic.region.can_reach(Region.forest)
+
+ def can_chop_perfectly(self) -> StardewRule:
+ magic_rule = (self.logic.magic.can_use_clear_debris_instead_of_tool_level(3)) & self.logic.mod.skill.has_mod_level(ModSkill.magic, 10)
+ tool_rule = self.logic.tool.has_tool(Tool.axe, ToolMaterial.iridium)
+ foraging_rule = self.logic.skill.has_level(Skill.foraging, 10)
+ region_rule = self.logic.region.can_reach(Region.forest)
+ return region_rule & ((tool_rule & foraging_rule) | magic_rule)
diff --git a/worlds/stardew_valley/logic/action_logic.py b/worlds/stardew_valley/logic/action_logic.py
new file mode 100644
index 000000000000..dc5deda427f3
--- /dev/null
+++ b/worlds/stardew_valley/logic/action_logic.py
@@ -0,0 +1,40 @@
+from typing import Union
+
+from Utils import cache_self1
+from .base_logic import BaseLogic, BaseLogicMixin
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .tool_logic import ToolLogicMixin
+from ..options import ToolProgression
+from ..stardew_rule import StardewRule, True_
+from ..strings.generic_names import Generic
+from ..strings.geode_names import Geode
+from ..strings.region_names import Region
+from ..strings.tool_names import Tool
+
+
+class ActionLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.action = ActionLogic(*args, **kwargs)
+
+
+class ActionLogic(BaseLogic[Union[ActionLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, ToolLogicMixin]]):
+
+ def can_watch(self, channel: str = None):
+ tv_rule = True_()
+ if channel is None:
+ return tv_rule
+ return self.logic.received(channel) & tv_rule
+
+ def can_pan_at(self, region: str, material: str) -> StardewRule:
+ return self.logic.region.can_reach(region) & self.logic.tool.has_tool(Tool.pan, material)
+
+ @cache_self1
+ def can_open_geode(self, geode: str) -> StardewRule:
+ blacksmith_access = self.logic.region.can_reach(Region.blacksmith)
+ geodes = [Geode.geode, Geode.frozen, Geode.magma, Geode.omni]
+ if geode == Generic.any:
+ return blacksmith_access & self.logic.or_(*(self.logic.has(geode_type) for geode_type in geodes))
+ return blacksmith_access & self.logic.has(geode)
diff --git a/worlds/stardew_valley/logic/animal_logic.py b/worlds/stardew_valley/logic/animal_logic.py
new file mode 100644
index 000000000000..eb1ebeeec54b
--- /dev/null
+++ b/worlds/stardew_valley/logic/animal_logic.py
@@ -0,0 +1,59 @@
+from typing import Union
+
+from .base_logic import BaseLogicMixin, BaseLogic
+from .building_logic import BuildingLogicMixin
+from .has_logic import HasLogicMixin
+from .money_logic import MoneyLogicMixin
+from ..stardew_rule import StardewRule, true_
+from ..strings.animal_names import Animal, coop_animals, barn_animals
+from ..strings.building_names import Building
+from ..strings.forageable_names import Forageable
+from ..strings.generic_names import Generic
+from ..strings.region_names import Region
+
+cost_and_building_by_animal = {
+ Animal.chicken: (800, Building.coop),
+ Animal.cow: (1500, Building.barn),
+ Animal.goat: (4000, Building.big_barn),
+ Animal.duck: (1200, Building.big_coop),
+ Animal.sheep: (8000, Building.deluxe_barn),
+ Animal.rabbit: (8000, Building.deluxe_coop),
+ Animal.pig: (16000, Building.deluxe_barn)
+}
+
+
+class AnimalLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.animal = AnimalLogic(*args, **kwargs)
+
+
+class AnimalLogic(BaseLogic[Union[HasLogicMixin, MoneyLogicMixin, BuildingLogicMixin]]):
+
+ def can_buy_animal(self, animal: str) -> StardewRule:
+ try:
+ price, building = cost_and_building_by_animal[animal]
+ except KeyError:
+ return true_
+ return self.logic.money.can_spend_at(Region.ranch, price) & self.logic.building.has_building(building)
+
+ def has_animal(self, animal: str) -> StardewRule:
+ if animal == Generic.any:
+ return self.has_any_animal()
+ elif animal == Building.coop:
+ return self.has_any_coop_animal()
+ elif animal == Building.barn:
+ return self.has_any_barn_animal()
+ return self.logic.has(animal)
+
+ def has_happy_animal(self, animal: str) -> StardewRule:
+ return self.has_animal(animal) & self.logic.has(Forageable.hay)
+
+ def has_any_animal(self) -> StardewRule:
+ return self.has_any_coop_animal() | self.has_any_barn_animal()
+
+ def has_any_coop_animal(self) -> StardewRule:
+ return self.logic.has_any(*coop_animals)
+
+ def has_any_barn_animal(self) -> StardewRule:
+ return self.logic.has_any(*barn_animals)
diff --git a/worlds/stardew_valley/logic/arcade_logic.py b/worlds/stardew_valley/logic/arcade_logic.py
new file mode 100644
index 000000000000..5e6a02a18435
--- /dev/null
+++ b/worlds/stardew_valley/logic/arcade_logic.py
@@ -0,0 +1,34 @@
+from typing import Union
+
+from .base_logic import BaseLogic, BaseLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .. import options
+from ..stardew_rule import StardewRule, True_
+from ..strings.region_names import Region
+
+
+class ArcadeLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.arcade = ArcadeLogic(*args, **kwargs)
+
+
+class ArcadeLogic(BaseLogic[Union[ArcadeLogicMixin, RegionLogicMixin, ReceivedLogicMixin]]):
+
+ def has_jotpk_power_level(self, power_level: int) -> StardewRule:
+ if self.options.arcade_machine_locations != options.ArcadeMachineLocations.option_full_shuffling:
+ return True_()
+ jotpk_buffs = ("JotPK: Progressive Boots", "JotPK: Progressive Gun", "JotPK: Progressive Ammo", "JotPK: Extra Life", "JotPK: Increased Drop Rate")
+ return self.logic.received_n(*jotpk_buffs, count=power_level)
+
+ def has_junimo_kart_power_level(self, power_level: int) -> StardewRule:
+ if self.options.arcade_machine_locations != options.ArcadeMachineLocations.option_full_shuffling:
+ return True_()
+ return self.logic.received("Junimo Kart: Extra Life", power_level)
+
+ def has_junimo_kart_max_level(self) -> StardewRule:
+ play_rule = self.logic.region.can_reach(Region.junimo_kart_3)
+ if self.options.arcade_machine_locations != options.ArcadeMachineLocations.option_full_shuffling:
+ return play_rule
+ return self.logic.arcade.has_junimo_kart_power_level(8)
diff --git a/worlds/stardew_valley/logic/artisan_logic.py b/worlds/stardew_valley/logic/artisan_logic.py
new file mode 100644
index 000000000000..23f0ae03b790
--- /dev/null
+++ b/worlds/stardew_valley/logic/artisan_logic.py
@@ -0,0 +1,93 @@
+from typing import Union
+
+from .base_logic import BaseLogic, BaseLogicMixin
+from .has_logic import HasLogicMixin
+from .time_logic import TimeLogicMixin
+from ..data.artisan import MachineSource
+from ..data.game_item import ItemTag
+from ..stardew_rule import StardewRule
+from ..strings.artisan_good_names import ArtisanGood
+from ..strings.crop_names import Vegetable, Fruit
+from ..strings.fish_names import Fish, all_fish
+from ..strings.forageable_names import Mushroom
+from ..strings.generic_names import Generic
+from ..strings.machine_names import Machine
+
+
+class ArtisanLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.artisan = ArtisanLogic(*args, **kwargs)
+
+
+class ArtisanLogic(BaseLogic[Union[ArtisanLogicMixin, TimeLogicMixin, HasLogicMixin]]):
+ def initialize_rules(self):
+ # TODO remove this one too once fish are converted to sources
+ self.registry.artisan_good_rules.update({ArtisanGood.specific_smoked_fish(fish): self.can_smoke(fish) for fish in all_fish})
+ self.registry.artisan_good_rules.update({ArtisanGood.specific_bait(fish): self.can_bait(fish) for fish in all_fish})
+
+ def has_jelly(self) -> StardewRule:
+ return self.logic.artisan.can_preserves_jar(Fruit.any)
+
+ def has_pickle(self) -> StardewRule:
+ return self.logic.artisan.can_preserves_jar(Vegetable.any)
+
+ def has_smoked_fish(self) -> StardewRule:
+ return self.logic.artisan.can_smoke(Fish.any)
+
+ def has_targeted_bait(self) -> StardewRule:
+ return self.logic.artisan.can_bait(Fish.any)
+
+ def has_dried_fruits(self) -> StardewRule:
+ return self.logic.artisan.can_dehydrate(Fruit.any)
+
+ def has_dried_mushrooms(self) -> StardewRule:
+ return self.logic.artisan.can_dehydrate(Mushroom.any_edible)
+
+ def has_raisins(self) -> StardewRule:
+ return self.logic.artisan.can_dehydrate(Fruit.grape)
+
+ def can_produce_from(self, source: MachineSource) -> StardewRule:
+ return self.logic.has(source.item) & self.logic.has(source.machine)
+
+ def can_preserves_jar(self, item: str) -> StardewRule:
+ machine_rule = self.logic.has(Machine.preserves_jar)
+ if item == Generic.any:
+ return machine_rule
+ if item == Fruit.any:
+ return machine_rule & self.logic.has_any(*(fruit.name for fruit in self.content.find_tagged_items(ItemTag.FRUIT)))
+ if item == Vegetable.any:
+ return machine_rule & self.logic.has_any(*(vege.name for vege in self.content.find_tagged_items(ItemTag.VEGETABLE)))
+ return machine_rule & self.logic.has(item)
+
+ def can_keg(self, item: str) -> StardewRule:
+ machine_rule = self.logic.has(Machine.keg)
+ if item == Generic.any:
+ return machine_rule
+ if item == Fruit.any:
+ return machine_rule & self.logic.has_any(*(fruit.name for fruit in self.content.find_tagged_items(ItemTag.FRUIT)))
+ if item == Vegetable.any:
+ return machine_rule & self.logic.has_any(*(vege.name for vege in self.content.find_tagged_items(ItemTag.VEGETABLE)))
+ return machine_rule & self.logic.has(item)
+
+ def can_mayonnaise(self, item: str) -> StardewRule:
+ return self.logic.has(Machine.mayonnaise_machine) & self.logic.has(item)
+
+ def can_smoke(self, item: str) -> StardewRule:
+ machine_rule = self.logic.has(Machine.fish_smoker)
+ return machine_rule & self.logic.has(item)
+
+ def can_bait(self, item: str) -> StardewRule:
+ machine_rule = self.logic.has(Machine.bait_maker)
+ return machine_rule & self.logic.has(item)
+
+ def can_dehydrate(self, item: str) -> StardewRule:
+ machine_rule = self.logic.has(Machine.dehydrator)
+ if item == Generic.any:
+ return machine_rule
+ if item == Fruit.any:
+ # Grapes make raisins
+ return machine_rule & self.logic.has_any(*(fruit.name for fruit in self.content.find_tagged_items(ItemTag.FRUIT) if fruit.name != Fruit.grape))
+ if item == Mushroom.any_edible:
+ return machine_rule & self.logic.has_any(*(mushroom.name for mushroom in self.content.find_tagged_items(ItemTag.EDIBLE_MUSHROOM)))
+ return machine_rule & self.logic.has(item)
diff --git a/worlds/stardew_valley/logic/base_logic.py b/worlds/stardew_valley/logic/base_logic.py
new file mode 100644
index 000000000000..7b377fce1fcc
--- /dev/null
+++ b/worlds/stardew_valley/logic/base_logic.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+from typing import TypeVar, Generic, Dict, Collection
+
+from ..content.game_content import StardewContent
+from ..options import StardewValleyOptions
+from ..stardew_rule import StardewRule
+
+
+class LogicRegistry:
+
+ def __init__(self):
+ self.item_rules: Dict[str, StardewRule] = {}
+ self.seed_rules: Dict[str, StardewRule] = {}
+ self.cooking_rules: Dict[str, StardewRule] = {}
+ self.crafting_rules: Dict[str, StardewRule] = {}
+ self.crop_rules: Dict[str, StardewRule] = {}
+ self.artisan_good_rules: Dict[str, StardewRule] = {}
+ self.fish_rules: Dict[str, StardewRule] = {}
+ self.museum_rules: Dict[str, StardewRule] = {}
+ self.festival_rules: Dict[str, StardewRule] = {}
+ self.quest_rules: Dict[str, StardewRule] = {}
+ self.building_rules: Dict[str, StardewRule] = {}
+ self.special_order_rules: Dict[str, StardewRule] = {}
+
+ self.sve_location_rules: Dict[str, StardewRule] = {}
+
+
+class BaseLogicMixin:
+ def __init__(self, *args, **kwargs):
+ pass
+
+
+T = TypeVar("T", bound=BaseLogicMixin)
+
+
+class BaseLogic(BaseLogicMixin, Generic[T]):
+ player: int
+ registry: LogicRegistry
+ options: StardewValleyOptions
+ content: StardewContent
+ regions: Collection[str]
+ logic: T
+
+ def __init__(self, player: int, registry: LogicRegistry, options: StardewValleyOptions, content: StardewContent, regions: Collection[str], logic: T):
+ super().__init__(player, registry, options, content, regions, logic)
+ self.player = player
+ self.registry = registry
+ self.options = options
+ self.content = content
+ self.regions = regions
+ self.logic = logic
diff --git a/worlds/stardew_valley/logic/book_logic.py b/worlds/stardew_valley/logic/book_logic.py
new file mode 100644
index 000000000000..464056ee06ba
--- /dev/null
+++ b/worlds/stardew_valley/logic/book_logic.py
@@ -0,0 +1,24 @@
+from typing import Union
+
+from Utils import cache_self1
+from .base_logic import BaseLogicMixin, BaseLogic
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from ..stardew_rule import StardewRule
+
+
+class BookLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.book = BookLogic(*args, **kwargs)
+
+
+class BookLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin]]):
+
+ @cache_self1
+ def has_book_power(self, book: str) -> StardewRule:
+ booksanity = self.content.features.booksanity
+ if booksanity.is_included(self.content.game_items[book]):
+ return self.logic.received(booksanity.to_item_name(book))
+ else:
+ return self.logic.has(book)
diff --git a/worlds/stardew_valley/logic/building_logic.py b/worlds/stardew_valley/logic/building_logic.py
new file mode 100644
index 000000000000..4611eba37d64
--- /dev/null
+++ b/worlds/stardew_valley/logic/building_logic.py
@@ -0,0 +1,97 @@
+from typing import Dict, Union
+
+from Utils import cache_self1
+from .base_logic import BaseLogic, BaseLogicMixin
+from .has_logic import HasLogicMixin
+from .money_logic import MoneyLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from ..options import BuildingProgression
+from ..stardew_rule import StardewRule, True_, False_, Has
+from ..strings.ap_names.event_names import Event
+from ..strings.artisan_good_names import ArtisanGood
+from ..strings.building_names import Building
+from ..strings.fish_names import WaterItem
+from ..strings.material_names import Material
+from ..strings.metal_names import MetalBar
+
+has_group = "building"
+
+
+class BuildingLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.building = BuildingLogic(*args, **kwargs)
+
+
+class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, MoneyLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]):
+ def initialize_rules(self):
+ self.registry.building_rules.update({
+ # @formatter:off
+ Building.barn: self.logic.money.can_spend(6000) & self.logic.has_all(Material.wood, Material.stone),
+ Building.big_barn: self.logic.money.can_spend(12000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.barn),
+ Building.deluxe_barn: self.logic.money.can_spend(25000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.big_barn),
+ Building.coop: self.logic.money.can_spend(4000) & self.logic.has_all(Material.wood, Material.stone),
+ Building.big_coop: self.logic.money.can_spend(10000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.coop),
+ Building.deluxe_coop: self.logic.money.can_spend(20000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.big_coop),
+ Building.fish_pond: self.logic.money.can_spend(5000) & self.logic.has_all(Material.stone, WaterItem.seaweed, WaterItem.green_algae),
+ Building.mill: self.logic.money.can_spend(2500) & self.logic.has_all(Material.stone, Material.wood, ArtisanGood.cloth),
+ Building.shed: self.logic.money.can_spend(15000) & self.logic.has(Material.wood),
+ Building.big_shed: self.logic.money.can_spend(20000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.shed),
+ Building.silo: self.logic.money.can_spend(100) & self.logic.has_all(Material.stone, Material.clay, MetalBar.copper),
+ Building.slime_hutch: self.logic.money.can_spend(10000) & self.logic.has_all(Material.stone, MetalBar.quartz, MetalBar.iridium),
+ Building.stable: self.logic.money.can_spend(10000) & self.logic.has_all(Material.hardwood, MetalBar.iron),
+ Building.well: self.logic.money.can_spend(1000) & self.logic.has(Material.stone),
+ Building.shipping_bin: self.logic.money.can_spend(250) & self.logic.has(Material.wood),
+ Building.kitchen: self.logic.money.can_spend(10000) & self.logic.has(Material.wood) & self.logic.building.has_house(0),
+ Building.kids_room: self.logic.money.can_spend(65000) & self.logic.has(Material.hardwood) & self.logic.building.has_house(1),
+ Building.cellar: self.logic.money.can_spend(100000) & self.logic.building.has_house(2),
+ # @formatter:on
+ })
+
+ def update_rules(self, new_rules: Dict[str, StardewRule]):
+ self.registry.building_rules.update(new_rules)
+
+ @cache_self1
+ def has_building(self, building: str) -> StardewRule:
+ # Shipping bin is special. The mod auto-builds it when received, no need to go to Robin.
+ if building is Building.shipping_bin:
+ if not self.options.building_progression & BuildingProgression.option_progressive:
+ return True_()
+ return self.logic.received(building)
+
+ carpenter_rule = self.logic.received(Event.can_construct_buildings)
+ if not self.options.building_progression & BuildingProgression.option_progressive:
+ return Has(building, self.registry.building_rules, has_group) & carpenter_rule
+
+ count = 1
+ if building in [Building.coop, Building.barn, Building.shed]:
+ building = f"Progressive {building}"
+ elif building.startswith("Big"):
+ count = 2
+ building = " ".join(["Progressive", *building.split(" ")[1:]])
+ elif building.startswith("Deluxe"):
+ count = 3
+ building = " ".join(["Progressive", *building.split(" ")[1:]])
+ return self.logic.received(building, count) & carpenter_rule
+
+ @cache_self1
+ def has_house(self, upgrade_level: int) -> StardewRule:
+ if upgrade_level < 1:
+ return True_()
+
+ if upgrade_level > 3:
+ return False_()
+
+ carpenter_rule = self.logic.received(Event.can_construct_buildings)
+ if self.options.building_progression & BuildingProgression.option_progressive:
+ return carpenter_rule & self.logic.received(f"Progressive House", upgrade_level)
+
+ if upgrade_level == 1:
+ return carpenter_rule & Has(Building.kitchen, self.registry.building_rules, has_group)
+
+ if upgrade_level == 2:
+ return carpenter_rule & Has(Building.kids_room, self.registry.building_rules, has_group)
+
+ # if upgrade_level == 3:
+ return carpenter_rule & Has(Building.cellar, self.registry.building_rules, has_group)
diff --git a/worlds/stardew_valley/logic/bundle_logic.py b/worlds/stardew_valley/logic/bundle_logic.py
new file mode 100644
index 000000000000..98fda1c73c7d
--- /dev/null
+++ b/worlds/stardew_valley/logic/bundle_logic.py
@@ -0,0 +1,84 @@
+from functools import cached_property
+from typing import Union, List
+
+from .base_logic import BaseLogicMixin, BaseLogic
+from .fishing_logic import FishingLogicMixin
+from .has_logic import HasLogicMixin
+from .money_logic import MoneyLogicMixin
+from .quality_logic import QualityLogicMixin
+from .quest_logic import QuestLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .skill_logic import SkillLogicMixin
+from .time_logic import TimeLogicMixin
+from ..bundles.bundle import Bundle
+from ..stardew_rule import StardewRule, True_
+from ..strings.ap_names.community_upgrade_names import CommunityUpgrade
+from ..strings.currency_names import Currency
+from ..strings.machine_names import Machine
+from ..strings.quality_names import CropQuality, ForageQuality, FishQuality, ArtisanQuality
+from ..strings.quest_names import Quest
+from ..strings.region_names import Region
+
+
+class BundleLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.bundle = BundleLogic(*args, **kwargs)
+
+
+class BundleLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, MoneyLogicMixin, QualityLogicMixin, FishingLogicMixin,
+SkillLogicMixin, QuestLogicMixin]]):
+ # Should be cached
+ def can_complete_bundle(self, bundle: Bundle) -> StardewRule:
+ item_rules = []
+ qualities = []
+ time_to_grind = 0
+ can_speak_junimo = self.logic.region.can_reach(Region.wizard_tower)
+ for bundle_item in bundle.items:
+ if Currency.is_currency(bundle_item.get_item()):
+ return can_speak_junimo & self.logic.money.can_trade(bundle_item.get_item(), bundle_item.amount)
+
+ item_rules.append(bundle_item.get_item())
+ if bundle_item.amount > 50:
+ time_to_grind = bundle_item.amount // 50
+ qualities.append(bundle_item.quality)
+ quality_rules = self.get_quality_rules(qualities)
+ item_rules = self.logic.has_n(*item_rules, count=bundle.number_required)
+ time_rule = self.logic.time.has_lived_months(time_to_grind)
+ return can_speak_junimo & item_rules & quality_rules & time_rule
+
+ def get_quality_rules(self, qualities: List[str]) -> StardewRule:
+ crop_quality = CropQuality.get_highest(qualities)
+ fish_quality = FishQuality.get_highest(qualities)
+ forage_quality = ForageQuality.get_highest(qualities)
+ artisan_quality = ArtisanQuality.get_highest(qualities)
+ quality_rules = []
+ if crop_quality != CropQuality.basic:
+ quality_rules.append(self.logic.quality.can_grow_crop_quality(crop_quality))
+ if fish_quality != FishQuality.basic:
+ quality_rules.append(self.logic.fishing.can_catch_quality_fish(fish_quality))
+ if forage_quality != ForageQuality.basic:
+ quality_rules.append(self.logic.skill.can_forage_quality(forage_quality))
+ if artisan_quality != ArtisanQuality.basic:
+ quality_rules.append(self.logic.has(Machine.cask))
+ if not quality_rules:
+ return True_()
+ return self.logic.and_(*quality_rules)
+
+ @cached_property
+ def can_complete_community_center(self) -> StardewRule:
+ return (self.logic.region.can_reach_location("Complete Crafts Room") &
+ self.logic.region.can_reach_location("Complete Pantry") &
+ self.logic.region.can_reach_location("Complete Fish Tank") &
+ self.logic.region.can_reach_location("Complete Bulletin Board") &
+ self.logic.region.can_reach_location("Complete Vault") &
+ self.logic.region.can_reach_location("Complete Boiler Room"))
+
+ def can_access_raccoon_bundles(self) -> StardewRule:
+ if self.options.quest_locations < 0:
+ return self.logic.received(CommunityUpgrade.raccoon, 1) & self.logic.quest.can_complete_quest(Quest.giant_stump)
+
+ # 1 - Break the tree
+ # 2 - Build the house, which summons the bundle racoon. This one is done manually if quests are turned off
+ return self.logic.received(CommunityUpgrade.raccoon, 2)
diff --git a/worlds/stardew_valley/logic/combat_logic.py b/worlds/stardew_valley/logic/combat_logic.py
new file mode 100644
index 000000000000..849bf14b2203
--- /dev/null
+++ b/worlds/stardew_valley/logic/combat_logic.py
@@ -0,0 +1,62 @@
+from functools import cached_property
+from typing import Union
+
+from Utils import cache_self1
+from .base_logic import BaseLogicMixin, BaseLogic
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from ..mods.logic.magic_logic import MagicLogicMixin
+from ..stardew_rule import StardewRule, False_
+from ..strings.ap_names.ap_weapon_names import APWeapon
+from ..strings.performance_names import Performance
+
+valid_weapons = (APWeapon.weapon, APWeapon.sword, APWeapon.club, APWeapon.dagger)
+
+
+class CombatLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.combat = CombatLogic(*args, **kwargs)
+
+
+class CombatLogic(BaseLogic[Union[HasLogicMixin, CombatLogicMixin, RegionLogicMixin, ReceivedLogicMixin, MagicLogicMixin]]):
+ @cache_self1
+ def can_fight_at_level(self, level: str) -> StardewRule:
+ if level == Performance.basic:
+ return self.logic.combat.has_any_weapon | self.logic.magic.has_any_spell()
+ if level == Performance.decent:
+ return self.logic.combat.has_decent_weapon | self.logic.magic.has_decent_spells()
+ if level == Performance.good:
+ return self.logic.combat.has_good_weapon | self.logic.magic.has_good_spells()
+ if level == Performance.great:
+ return self.logic.combat.has_great_weapon | self.logic.magic.has_great_spells()
+ if level == Performance.galaxy:
+ return self.logic.combat.has_galaxy_weapon | self.logic.magic.has_amazing_spells()
+ if level == Performance.maximum:
+ return self.logic.combat.has_galaxy_weapon | self.logic.magic.has_amazing_spells() # Someday we will have the ascended weapons in AP
+ return False_()
+
+ @cached_property
+ def has_any_weapon(self) -> StardewRule:
+ return self.logic.received_any(*valid_weapons)
+
+ @cached_property
+ def has_decent_weapon(self) -> StardewRule:
+ return self.logic.or_(*(self.logic.received(weapon, 2) for weapon in valid_weapons))
+
+ @cached_property
+ def has_good_weapon(self) -> StardewRule:
+ return self.logic.or_(*(self.logic.received(weapon, 3) for weapon in valid_weapons))
+
+ @cached_property
+ def has_great_weapon(self) -> StardewRule:
+ return self.logic.or_(*(self.logic.received(weapon, 4) for weapon in valid_weapons))
+
+ @cached_property
+ def has_galaxy_weapon(self) -> StardewRule:
+ return self.logic.or_(*(self.logic.received(weapon, 5) for weapon in valid_weapons))
+
+ @cached_property
+ def has_slingshot(self) -> StardewRule:
+ return self.logic.received(APWeapon.slingshot)
diff --git a/worlds/stardew_valley/logic/cooking_logic.py b/worlds/stardew_valley/logic/cooking_logic.py
new file mode 100644
index 000000000000..46f3bdc93f2f
--- /dev/null
+++ b/worlds/stardew_valley/logic/cooking_logic.py
@@ -0,0 +1,108 @@
+from functools import cached_property
+from typing import Union
+
+from Utils import cache_self1
+from .action_logic import ActionLogicMixin
+from .base_logic import BaseLogicMixin, BaseLogic
+from .building_logic import BuildingLogicMixin
+from .has_logic import HasLogicMixin
+from .money_logic import MoneyLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .relationship_logic import RelationshipLogicMixin
+from .season_logic import SeasonLogicMixin
+from .skill_logic import SkillLogicMixin
+from ..data.recipe_data import RecipeSource, StarterSource, ShopSource, SkillSource, FriendshipSource, \
+ QueenOfSauceSource, CookingRecipe, ShopFriendshipSource, \
+ all_cooking_recipes_by_name
+from ..data.recipe_source import CutsceneSource, ShopTradeSource
+from ..locations import locations_by_tag, LocationTags
+from ..options import Chefsanity
+from ..options import ExcludeGingerIsland
+from ..stardew_rule import StardewRule, True_, False_
+from ..strings.region_names import LogicRegion
+from ..strings.skill_names import Skill
+from ..strings.tv_channel_names import Channel
+
+
+class CookingLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.cooking = CookingLogic(*args, **kwargs)
+
+
+class CookingLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, MoneyLogicMixin, ActionLogicMixin,
+BuildingLogicMixin, RelationshipLogicMixin, SkillLogicMixin, CookingLogicMixin]]):
+ @cached_property
+ def can_cook_in_kitchen(self) -> StardewRule:
+ return self.logic.building.has_house(1) | self.logic.skill.has_level(Skill.foraging, 9)
+
+ # Should be cached
+ def can_cook(self, recipe: CookingRecipe = None) -> StardewRule:
+ cook_rule = self.logic.region.can_reach(LogicRegion.kitchen)
+ if recipe is None:
+ return cook_rule
+
+ recipe_rule = self.logic.cooking.knows_recipe(recipe.source, recipe.meal)
+ ingredients_rule = self.logic.has_all(*sorted(recipe.ingredients))
+ return cook_rule & recipe_rule & ingredients_rule
+
+ # Should be cached
+ def knows_recipe(self, source: RecipeSource, meal_name: str) -> StardewRule:
+ if self.options.chefsanity == Chefsanity.option_none:
+ return self.logic.cooking.can_learn_recipe(source)
+ if isinstance(source, StarterSource):
+ return self.logic.cooking.received_recipe(meal_name)
+ if isinstance(source, ShopTradeSource) and self.options.chefsanity & Chefsanity.option_purchases:
+ return self.logic.cooking.received_recipe(meal_name)
+ if isinstance(source, ShopSource) and self.options.chefsanity & Chefsanity.option_purchases:
+ return self.logic.cooking.received_recipe(meal_name)
+ if isinstance(source, SkillSource) and self.options.chefsanity & Chefsanity.option_skills:
+ return self.logic.cooking.received_recipe(meal_name)
+ if isinstance(source, CutsceneSource) and self.options.chefsanity & Chefsanity.option_friendship:
+ return self.logic.cooking.received_recipe(meal_name)
+ if isinstance(source, FriendshipSource) and self.options.chefsanity & Chefsanity.option_friendship:
+ return self.logic.cooking.received_recipe(meal_name)
+ if isinstance(source, QueenOfSauceSource) and self.options.chefsanity & Chefsanity.option_queen_of_sauce:
+ return self.logic.cooking.received_recipe(meal_name)
+ if isinstance(source, ShopFriendshipSource) and self.options.chefsanity & Chefsanity.option_purchases:
+ return self.logic.cooking.received_recipe(meal_name)
+ return self.logic.cooking.can_learn_recipe(source)
+
+ @cache_self1
+ def can_learn_recipe(self, source: RecipeSource) -> StardewRule:
+ if isinstance(source, StarterSource):
+ return True_()
+ if isinstance(source, ShopTradeSource):
+ return self.logic.money.can_trade_at(source.region, source.currency, source.price)
+ if isinstance(source, ShopSource):
+ return self.logic.money.can_spend_at(source.region, source.price)
+ if isinstance(source, SkillSource):
+ return self.logic.skill.has_level(source.skill, source.level)
+ if isinstance(source, CutsceneSource):
+ return self.logic.region.can_reach(source.region) & self.logic.relationship.has_hearts(source.friend, source.hearts)
+ if isinstance(source, FriendshipSource):
+ return self.logic.relationship.has_hearts(source.friend, source.hearts)
+ if isinstance(source, QueenOfSauceSource):
+ return self.logic.action.can_watch(Channel.queen_of_sauce) & self.logic.season.has(source.season)
+ if isinstance(source, ShopFriendshipSource):
+ return self.logic.money.can_spend_at(source.region, source.price) & self.logic.relationship.has_hearts(source.friend, source.hearts)
+ return False_()
+
+ @cache_self1
+ def received_recipe(self, meal_name: str):
+ return self.logic.received(f"{meal_name} Recipe")
+
+ @cached_property
+ def can_cook_everything(self) -> StardewRule:
+ cooksanity_prefix = "Cook "
+ all_recipes_names = []
+ exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
+ for location in locations_by_tag[LocationTags.COOKSANITY]:
+ if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
+ continue
+ if location.mod_name and location.mod_name not in self.options.mods:
+ continue
+ all_recipes_names.append(location.name[len(cooksanity_prefix):])
+ all_recipes = [all_cooking_recipes_by_name[recipe_name] for recipe_name in all_recipes_names]
+ return self.logic.and_(*(self.logic.cooking.can_cook(recipe) for recipe in all_recipes))
diff --git a/worlds/stardew_valley/logic/crafting_logic.py b/worlds/stardew_valley/logic/crafting_logic.py
new file mode 100644
index 000000000000..e346e4ba238b
--- /dev/null
+++ b/worlds/stardew_valley/logic/crafting_logic.py
@@ -0,0 +1,115 @@
+from functools import cached_property
+from typing import Union
+
+from Utils import cache_self1
+from .base_logic import BaseLogicMixin, BaseLogic
+from .has_logic import HasLogicMixin
+from .money_logic import MoneyLogicMixin
+from .quest_logic import QuestLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .relationship_logic import RelationshipLogicMixin
+from .skill_logic import SkillLogicMixin
+from .special_order_logic import SpecialOrderLogicMixin
+from .. import options
+from ..data.craftable_data import CraftingRecipe, all_crafting_recipes_by_name
+from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \
+ FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource
+from ..locations import locations_by_tag, LocationTags
+from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland, SkillProgression
+from ..stardew_rule import StardewRule, True_, False_
+from ..strings.region_names import Region
+
+
+class CraftingLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.crafting = CraftingLogic(*args, **kwargs)
+
+
+class CraftingLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, MoneyLogicMixin, RelationshipLogicMixin,
+SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]):
+ @cache_self1
+ def can_craft(self, recipe: CraftingRecipe = None) -> StardewRule:
+ if recipe is None:
+ return True_()
+
+ learn_rule = self.logic.crafting.knows_recipe(recipe)
+ ingredients_rule = self.logic.has_all(*recipe.ingredients)
+ return learn_rule & ingredients_rule
+
+ @cache_self1
+ def knows_recipe(self, recipe: CraftingRecipe) -> StardewRule:
+ if isinstance(recipe.source, ArchipelagoSource):
+ return self.logic.received_all(*recipe.source.ap_item)
+ if isinstance(recipe.source, FestivalShopSource):
+ if self.options.festival_locations == options.FestivalLocations.option_disabled:
+ return self.logic.crafting.can_learn_recipe(recipe)
+ else:
+ return self.logic.crafting.received_recipe(recipe.item)
+ if isinstance(recipe.source, QuestSource):
+ if self.options.quest_locations < 0:
+ return self.logic.crafting.can_learn_recipe(recipe)
+ else:
+ return self.logic.crafting.received_recipe(recipe.item)
+ if self.options.craftsanity == Craftsanity.option_none:
+ return self.logic.crafting.can_learn_recipe(recipe)
+ if isinstance(recipe.source, StarterSource) or isinstance(recipe.source, ShopTradeSource) or isinstance(
+ recipe.source, ShopSource):
+ return self.logic.crafting.received_recipe(recipe.item)
+ if isinstance(recipe.source, SpecialOrderSource) and self.options.special_order_locations & SpecialOrderLocations.option_board:
+ return self.logic.crafting.received_recipe(recipe.item)
+ return self.logic.crafting.can_learn_recipe(recipe)
+
+ @cache_self1
+ def can_learn_recipe(self, recipe: CraftingRecipe) -> StardewRule:
+ if isinstance(recipe.source, StarterSource):
+ return True_()
+ if isinstance(recipe.source, ArchipelagoSource):
+ return self.logic.received_all(*recipe.source.ap_item)
+ if isinstance(recipe.source, ShopTradeSource):
+ return self.logic.money.can_trade_at(recipe.source.region, recipe.source.currency, recipe.source.price)
+ if isinstance(recipe.source, ShopSource):
+ return self.logic.money.can_spend_at(recipe.source.region, recipe.source.price)
+ if isinstance(recipe.source, SkillSource):
+ return self.logic.skill.has_level(recipe.source.skill, recipe.source.level)
+ if isinstance(recipe.source, MasterySource):
+ return self.logic.skill.has_mastery(recipe.source.skill)
+ if isinstance(recipe.source, CutsceneSource):
+ return self.logic.region.can_reach(recipe.source.region) & self.logic.relationship.has_hearts(recipe.source.friend, recipe.source.hearts)
+ if isinstance(recipe.source, FriendshipSource):
+ return self.logic.relationship.has_hearts(recipe.source.friend, recipe.source.hearts)
+ if isinstance(recipe.source, QuestSource):
+ return self.logic.quest.can_complete_quest(recipe.source.quest)
+ if isinstance(recipe.source, SpecialOrderSource):
+ if self.options.special_order_locations & SpecialOrderLocations.option_board:
+ return self.logic.crafting.received_recipe(recipe.item)
+ return self.logic.special_order.can_complete_special_order(recipe.source.special_order)
+ if isinstance(recipe.source, LogicSource):
+ if recipe.source.logic_rule == "Cellar":
+ return self.logic.region.can_reach(Region.cellar)
+
+ return False_()
+
+ @cache_self1
+ def received_recipe(self, item_name: str):
+ return self.logic.received(f"{item_name} Recipe")
+
+ @cached_property
+ def can_craft_everything(self) -> StardewRule:
+ craftsanity_prefix = "Craft "
+ all_recipes_names = []
+ exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
+ exclude_masteries = self.options.skill_progression != SkillProgression.option_progressive_with_masteries
+ for location in locations_by_tag[LocationTags.CRAFTSANITY]:
+ if not location.name.startswith(craftsanity_prefix):
+ continue
+ if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
+ continue
+ if exclude_masteries and LocationTags.REQUIRES_MASTERIES in location.tags:
+ continue
+ if location.mod_name and location.mod_name not in self.options.mods:
+ continue
+ all_recipes_names.append(location.name[len(craftsanity_prefix):])
+ all_recipes = [all_crafting_recipes_by_name[recipe_name] for recipe_name in all_recipes_names]
+ return self.logic.and_(*(self.logic.crafting.can_craft(recipe) for recipe in all_recipes))
diff --git a/worlds/stardew_valley/logic/farming_logic.py b/worlds/stardew_valley/logic/farming_logic.py
new file mode 100644
index 000000000000..88523bb85d8e
--- /dev/null
+++ b/worlds/stardew_valley/logic/farming_logic.py
@@ -0,0 +1,62 @@
+from functools import cached_property
+from typing import Union, Tuple
+
+from Utils import cache_self1
+from .base_logic import BaseLogicMixin, BaseLogic
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .season_logic import SeasonLogicMixin
+from .tool_logic import ToolLogicMixin
+from .. import options
+from ..stardew_rule import StardewRule, True_, false_
+from ..strings.ap_names.event_names import Event
+from ..strings.fertilizer_names import Fertilizer
+from ..strings.region_names import Region
+from ..strings.season_names import Season
+from ..strings.tool_names import Tool
+
+farming_event_by_season = {
+ Season.spring: Event.spring_farming,
+ Season.summer: Event.summer_farming,
+ Season.fall: Event.fall_farming,
+ Season.winter: Event.winter_farming,
+}
+
+
+class FarmingLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.farming = FarmingLogic(*args, **kwargs)
+
+
+class FarmingLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, FarmingLogicMixin]]):
+
+ @cached_property
+ def has_farming_tools(self) -> StardewRule:
+ return self.logic.tool.has_tool(Tool.hoe) & self.logic.tool.can_water(0)
+
+ def has_fertilizer(self, tier: int) -> StardewRule:
+ if tier <= 0:
+ return True_()
+ if tier == 1:
+ return self.logic.has(Fertilizer.basic)
+ if tier == 2:
+ return self.logic.has(Fertilizer.quality)
+ if tier >= 3:
+ return self.logic.has(Fertilizer.deluxe)
+
+ @cache_self1
+ def can_plant_and_grow_item(self, seasons: Union[str, Tuple[str]]) -> StardewRule:
+ if seasons == (): # indoor farming
+ return (self.logic.region.can_reach(Region.greenhouse) | self.logic.farming.has_island_farm()) & self.logic.farming.has_farming_tools
+
+ if isinstance(seasons, str):
+ seasons = (seasons,)
+
+ return self.logic.or_(*(self.logic.received(farming_event_by_season[season]) for season in seasons))
+
+ def has_island_farm(self) -> StardewRule:
+ if self.options.exclude_ginger_island == options.ExcludeGingerIsland.option_false:
+ return self.logic.region.can_reach(Region.island_west)
+ return false_
diff --git a/worlds/stardew_valley/logic/fishing_logic.py b/worlds/stardew_valley/logic/fishing_logic.py
new file mode 100644
index 000000000000..539385232fd2
--- /dev/null
+++ b/worlds/stardew_valley/logic/fishing_logic.py
@@ -0,0 +1,112 @@
+from typing import Union
+
+from Utils import cache_self1
+from .base_logic import BaseLogicMixin, BaseLogic
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .season_logic import SeasonLogicMixin
+from .skill_logic import SkillLogicMixin
+from .tool_logic import ToolLogicMixin
+from ..data import fish_data
+from ..data.fish_data import FishItem
+from ..options import ExcludeGingerIsland
+from ..options import SpecialOrderLocations
+from ..stardew_rule import StardewRule, True_, False_
+from ..strings.ap_names.mods.mod_items import SVEQuestItem
+from ..strings.fish_names import SVEFish
+from ..strings.machine_names import Machine
+from ..strings.quality_names import FishQuality
+from ..strings.region_names import Region
+from ..strings.skill_names import Skill
+
+
+class FishingLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fishing = FishingLogic(*args, **kwargs)
+
+
+class FishingLogic(BaseLogic[Union[HasLogicMixin, FishingLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin,
+ SkillLogicMixin]]):
+ def can_fish_in_freshwater(self) -> StardewRule:
+ return self.logic.skill.can_fish() & self.logic.region.can_reach_any((Region.forest, Region.town, Region.mountain))
+
+ def has_max_fishing(self) -> StardewRule:
+ return self.logic.tool.has_fishing_rod(4) & self.logic.skill.has_level(Skill.fishing, 10)
+
+ def can_fish_chests(self) -> StardewRule:
+ return self.logic.tool.has_fishing_rod(4) & self.logic.skill.has_level(Skill.fishing, 6)
+
+ def can_fish_at(self, region: str) -> StardewRule:
+ return self.logic.skill.can_fish() & self.logic.region.can_reach(region)
+
+ @cache_self1
+ def can_catch_fish(self, fish: FishItem) -> StardewRule:
+ quest_rule = True_()
+ if fish.extended_family:
+ quest_rule = self.logic.fishing.can_start_extended_family_quest()
+ region_rule = self.logic.region.can_reach_any(fish.locations)
+ season_rule = self.logic.season.has_any(fish.seasons)
+ if fish.difficulty == -1:
+ difficulty_rule = self.logic.skill.can_crab_pot
+ else:
+ difficulty_rule = self.logic.skill.can_fish(difficulty=(120 if fish.legendary else fish.difficulty))
+ if fish.name == SVEFish.kittyfish:
+ item_rule = self.logic.received(SVEQuestItem.kittyfish_spell)
+ else:
+ item_rule = True_()
+ return quest_rule & region_rule & season_rule & difficulty_rule & item_rule
+
+ def can_catch_fish_for_fishsanity(self, fish: FishItem) -> StardewRule:
+ """ Rule could be different from the basic `can_catch_fish`. Imagine a fishsanity setting where you need to catch every fish with gold quality.
+ """
+ return self.logic.fishing.can_catch_fish(fish)
+
+ def can_start_extended_family_quest(self) -> StardewRule:
+ if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true:
+ return False_()
+ if not self.options.special_order_locations & SpecialOrderLocations.value_qi:
+ return False_()
+ return (self.logic.region.can_reach(Region.qi_walnut_room) &
+ self.logic.and_(*(self.logic.fishing.can_catch_fish(fish) for fish in fish_data.vanilla_legendary_fish)))
+
+ def can_catch_quality_fish(self, fish_quality: str) -> StardewRule:
+ if fish_quality == FishQuality.basic:
+ return True_()
+ rod_rule = self.logic.tool.has_fishing_rod(2)
+ if fish_quality == FishQuality.silver:
+ return rod_rule
+ if fish_quality == FishQuality.gold:
+ return rod_rule & self.logic.skill.has_level(Skill.fishing, 4)
+ if fish_quality == FishQuality.iridium:
+ return rod_rule & self.logic.skill.has_level(Skill.fishing, 10)
+
+ raise ValueError(f"Quality {fish_quality} is unknown.")
+
+ def can_catch_every_fish(self) -> StardewRule:
+ rules = [self.has_max_fishing()]
+
+ rules.extend(
+ self.logic.fishing.can_catch_fish(fish)
+ for fish in self.content.fishes.values()
+ )
+
+ return self.logic.and_(*rules)
+
+ def can_catch_every_fish_for_fishsanity(self) -> StardewRule:
+ if not self.content.features.fishsanity.is_enabled:
+ return self.can_catch_every_fish()
+
+ rules = [self.has_max_fishing()]
+
+ rules.extend(
+ self.logic.fishing.can_catch_fish_for_fishsanity(fish)
+ for fish in self.content.fishes.values()
+ if self.content.features.fishsanity.is_included(fish)
+ )
+
+ return self.logic.and_(*rules)
+
+ def has_specific_bait(self, fish: FishItem) -> StardewRule:
+ return self.can_catch_fish(fish) & self.logic.has(Machine.bait_maker)
diff --git a/worlds/stardew_valley/logic/gift_logic.py b/worlds/stardew_valley/logic/gift_logic.py
new file mode 100644
index 000000000000..527da6876411
--- /dev/null
+++ b/worlds/stardew_valley/logic/gift_logic.py
@@ -0,0 +1,20 @@
+from functools import cached_property
+
+from .base_logic import BaseLogic, BaseLogicMixin
+from .has_logic import HasLogicMixin
+from ..stardew_rule import StardewRule
+from ..strings.animal_product_names import AnimalProduct
+from ..strings.gift_names import Gift
+
+
+class GiftLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.gifts = GiftLogic(*args, **kwargs)
+
+
+class GiftLogic(BaseLogic[HasLogicMixin]):
+
+ @cached_property
+ def has_any_universal_love(self) -> StardewRule:
+ return self.logic.has_any(Gift.golden_pumpkin, Gift.pearl, "Prismatic Shard", AnimalProduct.rabbit_foot)
diff --git a/worlds/stardew_valley/logic/grind_logic.py b/worlds/stardew_valley/logic/grind_logic.py
new file mode 100644
index 000000000000..ccd8c5daccfb
--- /dev/null
+++ b/worlds/stardew_valley/logic/grind_logic.py
@@ -0,0 +1,74 @@
+from typing import Union, TYPE_CHECKING
+
+from Utils import cache_self1
+from .base_logic import BaseLogic, BaseLogicMixin
+from .book_logic import BookLogicMixin
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .time_logic import TimeLogicMixin
+from ..options import Booksanity
+from ..stardew_rule import StardewRule, HasProgressionPercent
+from ..strings.book_names import Book
+from ..strings.craftable_names import Consumable
+from ..strings.currency_names import Currency
+from ..strings.fish_names import WaterChest
+from ..strings.geode_names import Geode
+from ..strings.tool_names import Tool
+
+if TYPE_CHECKING:
+ from .tool_logic import ToolLogicMixin
+else:
+ ToolLogicMixin = object
+
+MIN_ITEMS = 10
+MAX_ITEMS = 999
+PERCENT_REQUIRED_FOR_MAX_ITEM = 24
+
+
+class GrindLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.grind = GrindLogic(*args, **kwargs)
+
+
+class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMixin, BookLogicMixin, TimeLogicMixin, ToolLogicMixin]]):
+
+ def can_grind_mystery_boxes(self, quantity: int) -> StardewRule:
+ mystery_box_rule = self.logic.has(Consumable.mystery_box)
+ book_of_mysteries_rule = self.logic.true_ \
+ if self.options.booksanity == Booksanity.option_none \
+ else self.logic.book.has_book_power(Book.book_of_mysteries)
+ # Assuming one box per day, but halved because we don't know how many months have passed before Mr. Qi's Plane Ride.
+ time_rule = self.logic.time.has_lived_months(quantity // 14)
+ return self.logic.and_(mystery_box_rule,
+ book_of_mysteries_rule,
+ time_rule)
+
+ def can_grind_artifact_troves(self, quantity: int) -> StardewRule:
+ return self.logic.and_(self.logic.has(Geode.artifact_trove),
+ # Assuming one per month if the player does not grind it.
+ self.logic.time.has_lived_months(quantity))
+
+ def can_grind_prize_tickets(self, quantity: int) -> StardewRule:
+ return self.logic.and_(self.logic.has(Currency.prize_ticket),
+ # Assuming two per month if the player does not grind it.
+ self.logic.time.has_lived_months(quantity // 2))
+
+ def can_grind_fishing_treasure_chests(self, quantity: int) -> StardewRule:
+ return self.logic.and_(self.logic.has(WaterChest.fishing_chest),
+ # Assuming one per week if the player does not grind it.
+ self.logic.time.has_lived_months(quantity // 4))
+
+ def can_grind_artifact_spots(self, quantity: int) -> StardewRule:
+ return self.logic.and_(self.logic.tool.has_tool(Tool.hoe),
+ # Assuming twelve per month if the player does not grind it.
+ self.logic.time.has_lived_months(quantity // 12))
+
+ @cache_self1
+ def can_grind_item(self, quantity: int) -> StardewRule:
+ if quantity <= MIN_ITEMS:
+ return self.logic.true_
+
+ quantity = min(quantity, MAX_ITEMS)
+ price = max(1, quantity * PERCENT_REQUIRED_FOR_MAX_ITEM // MAX_ITEMS)
+ return HasProgressionPercent(self.player, price)
diff --git a/worlds/stardew_valley/logic/harvesting_logic.py b/worlds/stardew_valley/logic/harvesting_logic.py
new file mode 100644
index 000000000000..3b4d41953ccd
--- /dev/null
+++ b/worlds/stardew_valley/logic/harvesting_logic.py
@@ -0,0 +1,56 @@
+from functools import cached_property
+from typing import Union
+
+from Utils import cache_self1
+from .base_logic import BaseLogicMixin, BaseLogic
+from .farming_logic import FarmingLogicMixin
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .season_logic import SeasonLogicMixin
+from .time_logic import TimeLogicMixin
+from .tool_logic import ToolLogicMixin
+from ..data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource
+from ..stardew_rule import StardewRule
+from ..strings.ap_names.community_upgrade_names import CommunityUpgrade
+from ..strings.region_names import Region
+
+
+class HarvestingLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.harvesting = HarvestingLogic(*args, **kwargs)
+
+
+class HarvestingLogic(BaseLogic[Union[HarvestingLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin,
+FarmingLogicMixin, TimeLogicMixin]]):
+
+ @cached_property
+ def can_harvest_from_fruit_bats(self) -> StardewRule:
+ return self.logic.region.can_reach(Region.farm_cave) & self.logic.received(CommunityUpgrade.fruit_bats)
+
+ @cached_property
+ def can_harvest_from_mushroom_cave(self) -> StardewRule:
+ return self.logic.region.can_reach(Region.farm_cave) & self.logic.received(CommunityUpgrade.mushroom_boxes)
+
+ @cache_self1
+ def can_forage_from(self, source: ForagingSource) -> StardewRule:
+ seasons_rule = self.logic.season.has_any(source.seasons)
+ regions_rule = self.logic.region.can_reach_any(source.regions)
+ return seasons_rule & regions_rule
+
+ @cache_self1
+ def can_harvest_tree_from(self, source: HarvestFruitTreeSource) -> StardewRule:
+ # FIXME tool not required for this
+ region_to_grow_rule = self.logic.farming.can_plant_and_grow_item(source.seasons)
+ sapling_rule = self.logic.has(source.sapling)
+ # Because it takes 1 month to grow the sapling
+ time_rule = self.logic.time.has_lived_months(1)
+
+ return region_to_grow_rule & sapling_rule & time_rule
+
+ @cache_self1
+ def can_harvest_crop_from(self, source: HarvestCropSource) -> StardewRule:
+ region_to_grow_rule = self.logic.farming.can_plant_and_grow_item(source.seasons)
+ seed_rule = self.logic.has(source.seed)
+ return region_to_grow_rule & seed_rule
diff --git a/worlds/stardew_valley/logic/has_logic.py b/worlds/stardew_valley/logic/has_logic.py
new file mode 100644
index 000000000000..4331780dc01d
--- /dev/null
+++ b/worlds/stardew_valley/logic/has_logic.py
@@ -0,0 +1,65 @@
+from .base_logic import BaseLogic
+from ..stardew_rule import StardewRule, And, Or, Has, Count, true_, false_
+
+
+class HasLogicMixin(BaseLogic[None]):
+ true_ = true_
+ false_ = false_
+
+ # Should be cached
+ def has(self, item: str) -> StardewRule:
+ return Has(item, self.registry.item_rules)
+
+ def has_all(self, *items: str):
+ assert items, "Can't have all of no items."
+
+ return self.logic.and_(*(self.has(item) for item in items))
+
+ def has_any(self, *items: str):
+ assert items, "Can't have any of no items."
+
+ return self.logic.or_(*(self.has(item) for item in items))
+
+ def has_n(self, *items: str, count: int):
+ return self.count(count, *(self.has(item) for item in items))
+
+ @staticmethod
+ def count(count: int, *rules: StardewRule) -> StardewRule:
+ assert rules, "Can't create a Count conditions without rules"
+ assert len(rules) >= count, "Count need at least as many rules as the count"
+ assert count > 0, "Count can't be negative"
+
+ count -= sum(r is true_ for r in rules)
+ rules = list(r for r in rules if r is not true_)
+
+ if count <= 0:
+ return true_
+
+ if len(rules) == 1:
+ return rules[0]
+
+ if count == 1:
+ return Or(*rules)
+
+ if count == len(rules):
+ return And(*rules)
+
+ return Count(rules, count)
+
+ @staticmethod
+ def and_(*rules: StardewRule) -> StardewRule:
+ assert rules, "Can't create a And conditions without rules"
+
+ if len(rules) == 1:
+ return rules[0]
+
+ return And(*rules)
+
+ @staticmethod
+ def or_(*rules: StardewRule) -> StardewRule:
+ assert rules, "Can't create a Or conditions without rules"
+
+ if len(rules) == 1:
+ return rules[0]
+
+ return Or(*rules)
diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py
new file mode 100644
index 000000000000..fb0d938fbb1e
--- /dev/null
+++ b/worlds/stardew_valley/logic/logic.py
@@ -0,0 +1,590 @@
+from __future__ import annotations
+
+import logging
+from typing import Collection, Callable
+
+from .ability_logic import AbilityLogicMixin
+from .action_logic import ActionLogicMixin
+from .animal_logic import AnimalLogicMixin
+from .arcade_logic import ArcadeLogicMixin
+from .artisan_logic import ArtisanLogicMixin
+from .base_logic import LogicRegistry
+from .book_logic import BookLogicMixin
+from .building_logic import BuildingLogicMixin
+from .bundle_logic import BundleLogicMixin
+from .combat_logic import CombatLogicMixin
+from .cooking_logic import CookingLogicMixin
+from .crafting_logic import CraftingLogicMixin
+from .farming_logic import FarmingLogicMixin
+from .fishing_logic import FishingLogicMixin
+from .gift_logic import GiftLogicMixin
+from .grind_logic import GrindLogicMixin
+from .harvesting_logic import HarvestingLogicMixin
+from .has_logic import HasLogicMixin
+from .logic_event import all_logic_events
+from .mine_logic import MineLogicMixin
+from .money_logic import MoneyLogicMixin
+from .monster_logic import MonsterLogicMixin
+from .museum_logic import MuseumLogicMixin
+from .pet_logic import PetLogicMixin
+from .quality_logic import QualityLogicMixin
+from .quest_logic import QuestLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .relationship_logic import RelationshipLogicMixin
+from .requirement_logic import RequirementLogicMixin
+from .season_logic import SeasonLogicMixin
+from .shipping_logic import ShippingLogicMixin
+from .skill_logic import SkillLogicMixin
+from .source_logic import SourceLogicMixin
+from .special_order_logic import SpecialOrderLogicMixin
+from .time_logic import TimeLogicMixin
+from .tool_logic import ToolLogicMixin
+from .traveling_merchant_logic import TravelingMerchantLogicMixin
+from .wallet_logic import WalletLogicMixin
+from .walnut_logic import WalnutLogicMixin
+from ..content.game_content import StardewContent
+from ..data.craftable_data import all_crafting_recipes
+from ..data.museum_data import all_museum_items
+from ..data.recipe_data import all_cooking_recipes
+from ..mods.logic.magic_logic import MagicLogicMixin
+from ..mods.logic.mod_logic import ModLogicMixin
+from ..mods.mod_data import ModNames
+from ..options import ExcludeGingerIsland, FestivalLocations, StardewValleyOptions
+from ..stardew_rule import False_, True_, StardewRule
+from ..strings.animal_names import Animal
+from ..strings.animal_product_names import AnimalProduct
+from ..strings.ap_names.community_upgrade_names import CommunityUpgrade
+from ..strings.artisan_good_names import ArtisanGood
+from ..strings.building_names import Building
+from ..strings.craftable_names import Consumable, Ring, Fishing, Lighting, WildSeeds
+from ..strings.crop_names import Fruit, Vegetable
+from ..strings.currency_names import Currency
+from ..strings.decoration_names import Decoration
+from ..strings.fertilizer_names import Fertilizer, SpeedGro, RetainingSoil
+from ..strings.festival_check_names import FestivalCheck
+from ..strings.fish_names import Fish, Trash, WaterItem, WaterChest
+from ..strings.flower_names import Flower
+from ..strings.food_names import Meal, Beverage
+from ..strings.forageable_names import Forageable
+from ..strings.fruit_tree_names import Sapling
+from ..strings.generic_names import Generic
+from ..strings.geode_names import Geode
+from ..strings.gift_names import Gift
+from ..strings.ingredient_names import Ingredient
+from ..strings.machine_names import Machine
+from ..strings.material_names import Material
+from ..strings.metal_names import Ore, MetalBar, Mineral, Fossil, Artifact
+from ..strings.monster_drop_names import Loot
+from ..strings.monster_names import Monster
+from ..strings.region_names import Region, LogicRegion
+from ..strings.season_names import Season
+from ..strings.seed_names import Seed, TreeSeed
+from ..strings.skill_names import Skill
+from ..strings.tool_names import Tool, ToolMaterial
+from ..strings.villager_names import NPC
+from ..strings.wallet_item_names import Wallet
+
+logger = logging.getLogger(__name__)
+
+
+class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, TravelingMerchantLogicMixin, TimeLogicMixin,
+ SeasonLogicMixin, MoneyLogicMixin, ActionLogicMixin, ArcadeLogicMixin, ArtisanLogicMixin, GiftLogicMixin,
+ BuildingLogicMixin, ShippingLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, WalletLogicMixin, AnimalLogicMixin,
+ CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin,
+ SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin,
+ SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin,
+ RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, WalnutLogicMixin):
+ player: int
+ options: StardewValleyOptions
+ content: StardewContent
+ regions: Collection[str]
+
+ def __init__(self, player: int, options: StardewValleyOptions, content: StardewContent, regions: Collection[str]):
+ self.registry = LogicRegistry()
+ super().__init__(player, self.registry, options, content, regions, self)
+
+ self.registry.fish_rules.update({fish.name: self.fishing.can_catch_fish(fish) for fish in content.fishes.values()})
+ self.registry.museum_rules.update({donation.item_name: self.museum.can_find_museum_item(donation) for donation in all_museum_items})
+
+ for recipe in all_cooking_recipes:
+ if recipe.mod_name and recipe.mod_name not in self.options.mods:
+ continue
+ can_cook_rule = self.cooking.can_cook(recipe)
+ if recipe.meal in self.registry.cooking_rules:
+ can_cook_rule = can_cook_rule | self.registry.cooking_rules[recipe.meal]
+ self.registry.cooking_rules[recipe.meal] = can_cook_rule
+
+ for recipe in all_crafting_recipes:
+ if recipe.mod_name and recipe.mod_name not in self.options.mods:
+ continue
+ can_craft_rule = self.crafting.can_craft(recipe)
+ if recipe.item in self.registry.crafting_rules:
+ can_craft_rule = can_craft_rule | self.registry.crafting_rules[recipe.item]
+ self.registry.crafting_rules[recipe.item] = can_craft_rule
+
+ self.registry.crop_rules.update({
+ Fruit.ancient_fruit: (self.received("Ancient Seeds") | self.received("Ancient Seeds Recipe")) &
+ self.region.can_reach(Region.greenhouse) & self.has(Machine.seed_maker),
+ })
+
+ # @formatter:off
+ self.registry.item_rules.update({
+ "Energy Tonic": self.money.can_spend_at(Region.hospital, 1000),
+ WaterChest.fishing_chest: self.fishing.can_fish_chests(),
+ WaterChest.golden_fishing_chest: self.fishing.can_fish_chests() & self.skill.has_mastery(Skill.fishing),
+ WaterChest.treasure: self.fishing.can_fish_chests(),
+ Ring.hot_java_ring: self.region.can_reach(Region.volcano_floor_10),
+ "Galaxy Soul": self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 40),
+ "JotPK Big Buff": self.arcade.has_jotpk_power_level(7),
+ "JotPK Max Buff": self.arcade.has_jotpk_power_level(9),
+ "JotPK Medium Buff": self.arcade.has_jotpk_power_level(4),
+ "JotPK Small Buff": self.arcade.has_jotpk_power_level(2),
+ "Junimo Kart Big Buff": self.arcade.has_junimo_kart_power_level(6),
+ "Junimo Kart Max Buff": self.arcade.has_junimo_kart_power_level(8),
+ "Junimo Kart Medium Buff": self.arcade.has_junimo_kart_power_level(4),
+ "Junimo Kart Small Buff": self.arcade.has_junimo_kart_power_level(2),
+ "Magic Rock Candy": self.region.can_reach(Region.desert) & self.has("Prismatic Shard"),
+ "Muscle Remedy": self.money.can_spend_at(Region.hospital, 1000),
+ # self.has(Ingredient.vinegar)),
+ # self.received("Deluxe Fertilizer Recipe") & self.has(MetalBar.iridium) & self.has(SVItem.sap),
+ # | (self.ability.can_cook() & self.relationship.has_hearts(NPC.emily, 3) & self.has(Forageable.leek) & self.has(Forageable.dandelion) &
+ # | (self.ability.can_cook() & self.relationship.has_hearts(NPC.jodi, 7) & self.has(AnimalProduct.cow_milk) & self.has(Ingredient.sugar)),
+ Animal.chicken: self.animal.can_buy_animal(Animal.chicken),
+ Animal.cow: self.animal.can_buy_animal(Animal.cow),
+ Animal.dinosaur: self.building.has_building(Building.big_coop) & self.has(AnimalProduct.dinosaur_egg),
+ Animal.duck: self.animal.can_buy_animal(Animal.duck),
+ Animal.goat: self.animal.can_buy_animal(Animal.goat),
+ Animal.ostrich: self.building.has_building(Building.barn) & self.has(AnimalProduct.ostrich_egg) & self.has(Machine.ostrich_incubator),
+ Animal.pig: self.animal.can_buy_animal(Animal.pig),
+ Animal.rabbit: self.animal.can_buy_animal(Animal.rabbit),
+ Animal.sheep: self.animal.can_buy_animal(Animal.sheep),
+ AnimalProduct.any_egg: self.has_any(AnimalProduct.chicken_egg, AnimalProduct.duck_egg),
+ AnimalProduct.brown_egg: self.animal.has_animal(Animal.chicken),
+ AnimalProduct.chicken_egg: self.has_any(AnimalProduct.egg, AnimalProduct.brown_egg, AnimalProduct.large_egg, AnimalProduct.large_brown_egg),
+ AnimalProduct.cow_milk: self.has_any(AnimalProduct.milk, AnimalProduct.large_milk),
+ AnimalProduct.duck_egg: self.animal.has_animal(Animal.duck),
+ AnimalProduct.duck_feather: self.animal.has_happy_animal(Animal.duck),
+ AnimalProduct.egg: self.animal.has_animal(Animal.chicken),
+ AnimalProduct.goat_milk: self.has(Animal.goat),
+ AnimalProduct.golden_egg: self.received(AnimalProduct.golden_egg) & (self.money.can_spend_at(Region.ranch, 100000) | self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 100)),
+ AnimalProduct.large_brown_egg: self.animal.has_happy_animal(Animal.chicken),
+ AnimalProduct.large_egg: self.animal.has_happy_animal(Animal.chicken),
+ AnimalProduct.large_goat_milk: self.animal.has_happy_animal(Animal.goat),
+ AnimalProduct.large_milk: self.animal.has_happy_animal(Animal.cow),
+ AnimalProduct.milk: self.animal.has_animal(Animal.cow),
+ AnimalProduct.ostrich_egg: self.tool.can_forage(Generic.any, Region.island_north, True) & self.has(Forageable.journal_scrap) & self.region.can_reach(Region.volcano_floor_5),
+ AnimalProduct.rabbit_foot: self.animal.has_happy_animal(Animal.rabbit),
+ AnimalProduct.roe: self.skill.can_fish() & self.building.has_building(Building.fish_pond),
+ AnimalProduct.squid_ink: self.mine.can_mine_in_the_mines_floor_81_120() | (self.building.has_building(Building.fish_pond) & self.has(Fish.squid)),
+ AnimalProduct.sturgeon_roe: self.has(Fish.sturgeon) & self.building.has_building(Building.fish_pond),
+ AnimalProduct.truffle: self.animal.has_animal(Animal.pig) & self.season.has_any_not_winter(),
+ AnimalProduct.void_egg: self.money.can_spend_at(Region.sewer, 5000) | (self.building.has_building(Building.fish_pond) & self.has(Fish.void_salmon)),
+ AnimalProduct.wool: self.animal.has_animal(Animal.rabbit) | self.animal.has_animal(Animal.sheep),
+ AnimalProduct.slime_egg_green: self.has(Machine.slime_egg_press) & self.has(Loot.slime),
+ AnimalProduct.slime_egg_blue: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(3),
+ AnimalProduct.slime_egg_red: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(6),
+ AnimalProduct.slime_egg_purple: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(9),
+ AnimalProduct.slime_egg_tiger: self.has(Fish.lionfish) & self.building.has_building(Building.fish_pond),
+ ArtisanGood.aged_roe: self.artisan.can_preserves_jar(AnimalProduct.roe),
+ ArtisanGood.battery_pack: (self.has(Machine.lightning_rod) & self.season.has_any_not_winter()) | self.has(Machine.solar_panel),
+ ArtisanGood.caviar: self.artisan.can_preserves_jar(AnimalProduct.sturgeon_roe),
+ ArtisanGood.cheese: (self.has(AnimalProduct.cow_milk) & self.has(Machine.cheese_press)) | (self.region.can_reach(Region.desert) & self.has(Mineral.emerald)),
+ ArtisanGood.cloth: (self.has(AnimalProduct.wool) & self.has(Machine.loom)) | (self.region.can_reach(Region.desert) & self.has(Mineral.aquamarine)),
+ ArtisanGood.dinosaur_mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.dinosaur_egg),
+ ArtisanGood.duck_mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.duck_egg),
+ ArtisanGood.goat_cheese: self.has(AnimalProduct.goat_milk) & self.has(Machine.cheese_press),
+ ArtisanGood.honey: self.money.can_spend_at(Region.oasis, 200) | (self.has(Machine.bee_house) & self.season.has_any_not_winter()),
+ ArtisanGood.maple_syrup: self.has(Machine.tapper),
+ ArtisanGood.mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.chicken_egg),
+ ArtisanGood.mystic_syrup: self.has(Machine.tapper) & self.has(TreeSeed.mystic),
+ ArtisanGood.oak_resin: self.has(Machine.tapper),
+ ArtisanGood.pine_tar: self.has(Machine.tapper),
+ ArtisanGood.smoked_fish: self.artisan.has_smoked_fish(),
+ ArtisanGood.targeted_bait: self.artisan.has_targeted_bait(),
+ ArtisanGood.stardrop_tea: self.has(WaterChest.golden_fishing_chest),
+ ArtisanGood.truffle_oil: self.has(AnimalProduct.truffle) & self.has(Machine.oil_maker),
+ ArtisanGood.void_mayonnaise: (self.skill.can_fish(Region.witch_swamp)) | (self.artisan.can_mayonnaise(AnimalProduct.void_egg)),
+ Beverage.pina_colada: self.money.can_spend_at(Region.island_resort, 600),
+ Beverage.triple_shot_espresso: self.has("Hot Java Ring"),
+ Consumable.butterfly_powder: self.money.can_spend_at(Region.sewer, 20000),
+ Consumable.far_away_stone: self.region.can_reach(Region.mines_floor_100) & self.has(Artifact.ancient_doll),
+ Consumable.fireworks_red: self.region.can_reach(Region.casino),
+ Consumable.fireworks_purple: self.region.can_reach(Region.casino),
+ Consumable.fireworks_green: self.region.can_reach(Region.casino),
+ Consumable.golden_animal_cracker: self.skill.has_mastery(Skill.farming),
+ Consumable.mystery_box: self.received(CommunityUpgrade.mr_qi_plane_ride),
+ Consumable.gold_mystery_box: self.received(CommunityUpgrade.mr_qi_plane_ride) & self.skill.has_mastery(Skill.foraging),
+ Currency.calico_egg: self.region.can_reach(LogicRegion.desert_festival),
+ Currency.golden_tag: self.region.can_reach(LogicRegion.trout_derby),
+ Currency.prize_ticket: self.time.has_lived_months(2), # Time to do a few help wanted quests
+ Decoration.rotten_plant: self.has(Lighting.jack_o_lantern) & self.season.has(Season.winter),
+ Fertilizer.basic: self.money.can_spend_at(Region.pierre_store, 100),
+ Fertilizer.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
+ Fertilizer.tree: self.skill.has_level(Skill.foraging, 7) & self.has(Material.fiber) & self.has(Material.stone),
+ Fish.any: self.logic.or_(*(self.fishing.can_catch_fish(fish) for fish in content.fishes.values())),
+ Fish.crab: self.skill.can_crab_pot_at(Region.beach),
+ Fish.crayfish: self.skill.can_crab_pot_at(Region.town),
+ Fish.lobster: self.skill.can_crab_pot_at(Region.beach),
+ Fish.mussel: self.tool.can_forage(Generic.any, Region.beach) or self.has(Fish.mussel_node),
+ Fish.mussel_node: self.region.can_reach(Region.island_west),
+ Fish.oyster: self.tool.can_forage(Generic.any, Region.beach),
+ Fish.periwinkle: self.skill.can_crab_pot_at(Region.town),
+ Fish.shrimp: self.skill.can_crab_pot_at(Region.beach),
+ Fish.snail: self.skill.can_crab_pot_at(Region.town),
+ Fishing.curiosity_lure: self.monster.can_kill(self.monster.all_monsters_by_name[Monster.mummy]),
+ Fishing.lead_bobber: self.skill.has_level(Skill.fishing, 6) & self.money.can_spend_at(Region.fish_shop, 200),
+ Forageable.hay: self.building.has_building(Building.silo) & self.tool.has_tool(Tool.scythe), #
+ Forageable.journal_scrap: self.region.can_reach_all((Region.island_west, Region.island_north, Region.island_south, Region.volcano_floor_10)) & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()),#
+ Forageable.secret_note: self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()), #
+ Fossil.bone_fragment: (self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe)) | self.monster.can_kill(Monster.skeleton),
+ Fossil.fossilized_leg: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe),
+ Fossil.fossilized_ribs: self.region.can_reach(Region.island_south) & self.tool.has_tool(Tool.hoe),
+ Fossil.fossilized_skull: self.action.can_open_geode(Geode.golden_coconut),
+ Fossil.fossilized_spine: self.skill.can_fish(Region.dig_site),
+ Fossil.fossilized_tail: self.action.can_pan_at(Region.dig_site, ToolMaterial.copper),
+ Fossil.mummified_bat: self.region.can_reach(Region.volcano_floor_10),
+ Fossil.mummified_frog: self.region.can_reach(Region.island_east) & self.tool.has_tool(Tool.scythe),
+ Fossil.snake_skull: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.hoe),
+ Fossil.snake_vertebrae: self.region.can_reach(Region.island_west) & self.tool.has_tool(Tool.hoe),
+ Geode.artifact_trove: self.has(Geode.omni) & self.region.can_reach(Region.desert),
+ Geode.frozen: self.mine.can_mine_in_the_mines_floor_41_80(),
+ Geode.geode: self.mine.can_mine_in_the_mines_floor_1_40(),
+ Geode.golden_coconut: self.region.can_reach(Region.island_north),
+ Geode.magma: self.mine.can_mine_in_the_mines_floor_81_120() | (self.has(Fish.lava_eel) & self.building.has_building(Building.fish_pond)),
+ Geode.omni: self.mine.can_mine_in_the_mines_floor_41_80() | self.region.can_reach(Region.desert) | self.tool.has_tool(Tool.pan, ToolMaterial.iron) | self.received(Wallet.rusty_key) | (self.has(Fish.octopus) & self.building.has_building(Building.fish_pond)) | self.region.can_reach(Region.volcano_floor_10),
+ Gift.bouquet: self.relationship.has_hearts_with_any_bachelor(8) & self.money.can_spend_at(Region.pierre_store, 100),
+ Gift.golden_pumpkin: self.season.has(Season.fall) | self.action.can_open_geode(Geode.artifact_trove),
+ Gift.mermaid_pendant: self.region.can_reach(Region.tide_pools) & self.relationship.has_hearts_with_any_bachelor(10) & self.building.has_house(1) & self.has(Consumable.rain_totem),
+ Gift.movie_ticket: self.money.can_spend_at(Region.movie_ticket_stand, 1000),
+ Gift.pearl: (self.has(Fish.blobfish) & self.building.has_building(Building.fish_pond)) | self.action.can_open_geode(Geode.artifact_trove),
+ Gift.tea_set: self.season.has(Season.winter) & self.time.has_lived_max_months,
+ Gift.void_ghost_pendant: self.money.can_trade_at(Region.desert, Loot.void_essence, 200) & self.relationship.has_hearts(NPC.krobus, 10),
+ Gift.wilted_bouquet: self.has(Machine.furnace) & self.has(Gift.bouquet) & self.has(Material.coal),
+ Ingredient.oil: self.money.can_spend_at(Region.pierre_store, 200) | (self.has(Machine.oil_maker) & (self.has(Vegetable.corn) | self.has(Flower.sunflower) | self.has(Seed.sunflower))),
+ Ingredient.qi_seasoning: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 10),
+ Ingredient.rice: self.money.can_spend_at(Region.pierre_store, 200) | (self.building.has_building(Building.mill) & self.has(Vegetable.unmilled_rice)),
+ Ingredient.sugar: self.money.can_spend_at(Region.pierre_store, 100) | (self.building.has_building(Building.mill) & self.has(Vegetable.beet)),
+ Ingredient.vinegar: self.money.can_spend_at(Region.pierre_store, 200) | self.artisan.can_keg(Ingredient.rice),
+ Ingredient.wheat_flour: self.money.can_spend_at(Region.pierre_store, 100) | (self.building.has_building(Building.mill) & self.has(Vegetable.wheat)),
+ Loot.bat_wing: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern(),
+ Loot.bug_meat: self.mine.can_mine_in_the_mines_floor_1_40(),
+ Loot.slime: self.mine.can_mine_in_the_mines_floor_1_40(),
+ Loot.solar_essence: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern(),
+ Loot.void_essence: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern(),
+ Machine.coffee_maker: self.received(Machine.coffee_maker),
+ Machine.crab_pot: self.skill.has_level(Skill.fishing, 3) & self.money.can_spend_at(Region.fish_shop, 1500),
+ Machine.enricher: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 20),
+ Machine.pressure_nozzle: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 20),
+ Material.cinder_shard: self.region.can_reach(Region.volcano_floor_5),
+ Material.clay: self.region.can_reach_any((Region.farm, Region.beach, Region.quarry)) & self.tool.has_tool(Tool.hoe),
+ Material.coal: self.mine.can_mine_in_the_mines_floor_41_80() | self.tool.has_tool(Tool.pan),
+ Material.fiber: True_(),
+ Material.hardwood: self.tool.has_tool(Tool.axe, ToolMaterial.copper) & (self.region.can_reach(Region.secret_woods) | self.region.can_reach(Region.island_west)),
+ Material.moss: True_(),
+ Material.sap: self.ability.can_chop_trees(),
+ Material.stone: self.tool.has_tool(Tool.pickaxe),
+ Material.wood: self.tool.has_tool(Tool.axe),
+ Meal.ice_cream: (self.season.has(Season.summer) & self.money.can_spend_at(Region.town, 250)) | self.money.can_spend_at(Region.oasis, 240),
+ Meal.strange_bun: self.relationship.has_hearts(NPC.shane, 7) & self.has(Ingredient.wheat_flour) & self.has(Fish.periwinkle) & self.has(ArtisanGood.void_mayonnaise),
+ MetalBar.copper: self.can_smelt(Ore.copper),
+ MetalBar.gold: self.can_smelt(Ore.gold),
+ MetalBar.iridium: self.can_smelt(Ore.iridium),
+ MetalBar.iron: self.can_smelt(Ore.iron),
+ MetalBar.quartz: self.can_smelt(Mineral.quartz) | self.can_smelt("Fire Quartz") | (self.has(Machine.recycling_machine) & (self.has(Trash.broken_cd) | self.has(Trash.broken_glasses))),
+ MetalBar.radioactive: self.can_smelt(Ore.radioactive),
+ Ore.copper: self.mine.can_mine_in_the_mines_floor_1_40() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper),
+ Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.iron),
+ Ore.iridium: self.mine.can_mine_in_the_skull_cavern() | self.can_fish_pond(Fish.super_cucumber) | self.tool.has_tool(Tool.pan, ToolMaterial.gold),
+ Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper),
+ Ore.radioactive: self.ability.can_mine_perfectly() & self.region.can_reach(Region.qi_walnut_room),
+ RetainingSoil.basic: self.money.can_spend_at(Region.pierre_store, 100),
+ RetainingSoil.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
+ Sapling.tea: self.relationship.has_hearts(NPC.caroline, 2) & self.has(Material.fiber) & self.has(Material.wood),
+ SpeedGro.basic: self.money.can_spend_at(Region.pierre_store, 100),
+ SpeedGro.deluxe: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
+ Trash.broken_cd: self.skill.can_crab_pot,
+ Trash.broken_glasses: self.skill.can_crab_pot,
+ Trash.driftwood: self.skill.can_crab_pot,
+ Trash.joja_cola: self.money.can_spend_at(Region.saloon, 75),
+ Trash.soggy_newspaper: self.skill.can_crab_pot,
+ Trash.trash: self.skill.can_crab_pot,
+ TreeSeed.acorn: self.skill.has_level(Skill.foraging, 1) & self.ability.can_chop_trees(),
+ TreeSeed.mahogany: self.region.can_reach(Region.secret_woods) & self.tool.has_tool(Tool.axe, ToolMaterial.iron) & self.skill.has_level(Skill.foraging, 1),
+ TreeSeed.maple: self.skill.has_level(Skill.foraging, 1) & self.ability.can_chop_trees(),
+ TreeSeed.mushroom: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 5),
+ TreeSeed.pine: self.skill.has_level(Skill.foraging, 1) & self.ability.can_chop_trees(),
+ TreeSeed.mossy: self.ability.can_chop_trees() & self.season.has(Season.summer),
+ Fish.clam: self.tool.can_forage(Generic.any, Region.beach),
+ Fish.cockle: self.tool.can_forage(Generic.any, Region.beach),
+ WaterItem.green_algae: self.fishing.can_fish_in_freshwater(),
+ WaterItem.cave_jelly: self.fishing.can_fish_at(Region.mines_floor_100) & self.tool.has_fishing_rod(2),
+ WaterItem.river_jelly: self.fishing.can_fish_at(Region.town) & self.tool.has_fishing_rod(2),
+ WaterItem.sea_jelly: self.fishing.can_fish_at(Region.beach) & self.tool.has_fishing_rod(2),
+ WaterItem.seaweed: self.skill.can_fish(Region.tide_pools),
+ WaterItem.white_algae: self.skill.can_fish(Region.mines_floor_20),
+ WildSeeds.grass_starter: self.money.can_spend_at(Region.pierre_store, 100),
+ })
+ # @formatter:on
+
+ content_rules = {
+ item_name: self.source.has_access_to_item(game_item)
+ for item_name, game_item in self.content.game_items.items()
+ }
+
+ for item in set(content_rules.keys()).intersection(self.registry.item_rules.keys()):
+ logger.warning(f"Rule for {item} already exists in the registry, overwriting it.")
+
+ self.registry.item_rules.update(content_rules)
+ self.registry.item_rules.update(self.registry.fish_rules)
+ self.registry.item_rules.update(self.registry.museum_rules)
+ self.registry.item_rules.update(self.registry.crop_rules)
+ self.artisan.initialize_rules()
+ self.registry.item_rules.update(self.registry.artisan_good_rules)
+
+ self.registry.item_rules.update(self.mod.item.get_modded_item_rules())
+ self.mod.item.modify_vanilla_item_rules_with_mod_additions(self.registry.item_rules) # New regions and content means new ways to obtain old items
+
+ # For some recipes, the cooked item can be obtained directly, so we either cook it or get it
+ for recipe in self.registry.cooking_rules:
+ cooking_rule = self.registry.cooking_rules[recipe]
+ obtention_rule = self.registry.item_rules[recipe] if recipe in self.registry.item_rules else False_()
+ self.registry.item_rules[recipe] = obtention_rule | cooking_rule
+
+ # For some recipes, the crafted item can be obtained directly, so we either craft it or get it
+ for recipe in self.registry.crafting_rules:
+ crafting_rule = self.registry.crafting_rules[recipe]
+ obtention_rule = self.registry.item_rules[recipe] if recipe in self.registry.item_rules else False_()
+ self.registry.item_rules[recipe] = obtention_rule | crafting_rule
+
+ self.building.initialize_rules()
+ self.building.update_rules(self.mod.building.get_modded_building_rules())
+
+ self.quest.initialize_rules()
+ self.quest.update_rules(self.mod.quest.get_modded_quest_rules())
+
+ self.registry.festival_rules.update({
+ FestivalCheck.egg_hunt: self.can_win_egg_hunt(),
+ FestivalCheck.strawberry_seeds: self.money.can_spend(1000),
+ FestivalCheck.dance: self.relationship.has_hearts_with_any_bachelor(4),
+ FestivalCheck.tub_o_flowers: self.money.can_spend(2000),
+ FestivalCheck.rarecrow_5: self.money.can_spend(2500),
+ FestivalCheck.luau_soup: self.can_succeed_luau_soup(),
+ FestivalCheck.moonlight_jellies: True_(),
+ FestivalCheck.moonlight_jellies_banner: self.money.can_spend(800),
+ FestivalCheck.starport_decal: self.money.can_spend(1000),
+ FestivalCheck.smashing_stone: True_(),
+ FestivalCheck.grange_display: self.can_succeed_grange_display(),
+ FestivalCheck.rarecrow_1: True_(), # only cost star tokens
+ FestivalCheck.fair_stardrop: True_(), # only cost star tokens
+ FestivalCheck.spirit_eve_maze: True_(),
+ FestivalCheck.jack_o_lantern: self.money.can_spend(2000),
+ FestivalCheck.rarecrow_2: self.money.can_spend(5000),
+ FestivalCheck.fishing_competition: self.can_win_fishing_competition(),
+ FestivalCheck.rarecrow_4: self.money.can_spend(5000),
+ FestivalCheck.mermaid_pearl: self.has(Forageable.secret_note),
+ FestivalCheck.cone_hat: self.money.can_spend(2500),
+ FestivalCheck.iridium_fireplace: self.money.can_spend(15000),
+ FestivalCheck.rarecrow_7: self.money.can_spend(5000) & self.museum.can_donate_museum_artifacts(20),
+ FestivalCheck.rarecrow_8: self.money.can_spend(5000) & self.museum.can_donate_museum_items(40),
+ FestivalCheck.lupini_red_eagle: self.money.can_spend(1200),
+ FestivalCheck.lupini_portrait_mermaid: self.money.can_spend(1200),
+ FestivalCheck.lupini_solar_kingdom: self.money.can_spend(1200),
+ FestivalCheck.lupini_clouds: self.time.has_year_two & self.money.can_spend(1200),
+ FestivalCheck.lupini_1000_years: self.time.has_year_two & self.money.can_spend(1200),
+ FestivalCheck.lupini_three_trees: self.time.has_year_two & self.money.can_spend(1200),
+ FestivalCheck.lupini_the_serpent: self.time.has_year_three & self.money.can_spend(1200),
+ FestivalCheck.lupini_tropical_fish: self.time.has_year_three & self.money.can_spend(1200),
+ FestivalCheck.lupini_land_of_clay: self.time.has_year_three & self.money.can_spend(1200),
+ FestivalCheck.secret_santa: self.gifts.has_any_universal_love,
+ FestivalCheck.legend_of_the_winter_star: True_(),
+ FestivalCheck.rarecrow_3: True_(),
+ FestivalCheck.all_rarecrows: self.region.can_reach(Region.farm) & self.has_all_rarecrows(),
+ FestivalCheck.calico_race: True_(),
+ FestivalCheck.mummy_mask: True_(),
+ FestivalCheck.calico_statue: True_(),
+ FestivalCheck.emily_outfit_service: True_(),
+ FestivalCheck.earthy_mousse: True_(),
+ FestivalCheck.sweet_bean_cake: True_(),
+ FestivalCheck.skull_cave_casserole: True_(),
+ FestivalCheck.spicy_tacos: True_(),
+ FestivalCheck.mountain_chili: True_(),
+ FestivalCheck.crystal_cake: True_(),
+ FestivalCheck.cave_kebab: True_(),
+ FestivalCheck.hot_log: True_(),
+ FestivalCheck.sour_salad: True_(),
+ FestivalCheck.superfood_cake: True_(),
+ FestivalCheck.warrior_smoothie: True_(),
+ FestivalCheck.rumpled_fruit_skin: True_(),
+ FestivalCheck.calico_pizza: True_(),
+ FestivalCheck.stuffed_mushrooms: True_(),
+ FestivalCheck.elf_quesadilla: True_(),
+ FestivalCheck.nachos_of_the_desert: True_(),
+ FestivalCheck.cloppino: True_(),
+ FestivalCheck.rainforest_shrimp: True_(),
+ FestivalCheck.shrimp_donut: True_(),
+ FestivalCheck.smell_of_the_sea: True_(),
+ FestivalCheck.desert_gumbo: True_(),
+ FestivalCheck.free_cactis: True_(),
+ FestivalCheck.monster_hunt: self.monster.can_kill(Monster.serpent),
+ FestivalCheck.deep_dive: self.region.can_reach(Region.skull_cavern_50),
+ FestivalCheck.treasure_hunt: self.region.can_reach(Region.skull_cavern_25),
+ FestivalCheck.touch_calico_statue: self.region.can_reach(Region.skull_cavern_25),
+ FestivalCheck.real_calico_egg_hunter: self.region.can_reach(Region.skull_cavern_100),
+ FestivalCheck.willy_challenge: self.fishing.can_catch_fish(content.fishes[Fish.scorpion_carp]),
+ FestivalCheck.desert_scholar: True_(),
+ FestivalCheck.squidfest_day_1_copper: self.fishing.can_catch_fish(content.fishes[Fish.squid]),
+ FestivalCheck.squidfest_day_1_iron: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.bait),
+ FestivalCheck.squidfest_day_1_gold: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.deluxe_bait),
+ FestivalCheck.squidfest_day_1_iridium: self.fishing.can_catch_fish(content.fishes[Fish.squid]) &
+ self.fishing.has_specific_bait(content.fishes[Fish.squid]),
+ FestivalCheck.squidfest_day_2_copper: self.fishing.can_catch_fish(content.fishes[Fish.squid]),
+ FestivalCheck.squidfest_day_2_iron: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.bait),
+ FestivalCheck.squidfest_day_2_gold: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.deluxe_bait),
+ FestivalCheck.squidfest_day_2_iridium: self.fishing.can_catch_fish(content.fishes[Fish.squid]) &
+ self.fishing.has_specific_bait(content.fishes[Fish.squid]),
+ })
+ for i in range(1, 11):
+ self.registry.festival_rules[f"{FestivalCheck.trout_derby_reward_pattern}{i}"] = self.fishing.can_catch_fish(content.fishes[Fish.rainbow_trout])
+
+ self.special_order.initialize_rules()
+ self.special_order.update_rules(self.mod.special_order.get_modded_special_orders_rules())
+
+ def setup_events(self, register_event: Callable[[str, str, StardewRule], None]) -> None:
+ for logic_event in all_logic_events:
+ rule = self.registry.item_rules[logic_event.item]
+ register_event(logic_event.name, logic_event.region, rule)
+ self.registry.item_rules[logic_event.item] = self.received(logic_event.name)
+
+ def can_smelt(self, item: str) -> StardewRule:
+ return self.has(Machine.furnace) & self.has(item)
+
+ def can_finish_grandpa_evaluation(self) -> StardewRule:
+ # https://stardewvalleywiki.com/Grandpa
+ rules_worth_a_point = [
+ self.money.can_have_earned_total(50000), # 50 000g
+ self.money.can_have_earned_total(100000), # 100 000g
+ self.money.can_have_earned_total(200000), # 200 000g
+ self.money.can_have_earned_total(300000), # 300 000g
+ self.money.can_have_earned_total(500000), # 500 000g
+ self.money.can_have_earned_total(1000000), # 1 000 000g first point
+ self.money.can_have_earned_total(1000000), # 1 000 000g second point
+ self.skill.has_total_level(30), # Total Skills: 30
+ self.skill.has_total_level(50), # Total Skills: 50
+ self.museum.can_complete_museum(), # Completing the museum for a point
+ # Catching every fish not expected
+ # Shipping every item not expected
+ self.relationship.can_get_married() & self.building.has_house(2),
+ self.relationship.has_hearts_with_n(5, 8), # 5 Friends
+ self.relationship.has_hearts_with_n(10, 8), # 10 friends
+ self.pet.has_pet_hearts(5), # Max Pet
+ self.bundle.can_complete_community_center, # Community Center Completion
+ self.bundle.can_complete_community_center, # CC Ceremony first point
+ self.bundle.can_complete_community_center, # CC Ceremony second point
+ self.received(Wallet.skull_key), # Skull Key obtained
+ self.wallet.has_rusty_key(), # Rusty key obtained
+ ]
+ return self.count(12, *rules_worth_a_point)
+
+ def can_win_egg_hunt(self) -> StardewRule:
+ return True_()
+
+ def can_succeed_luau_soup(self) -> StardewRule:
+ if self.options.festival_locations != FestivalLocations.option_hard:
+ return True_()
+ eligible_fish = (Fish.blobfish, Fish.crimsonfish, Fish.ice_pip, Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish,
+ Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, Fish.super_cucumber)
+ fish_rule = self.has_any(*(f for f in eligible_fish if f in self.content.fishes)) # To filter stingray
+ eligible_kegables = (Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.melon,
+ Fruit.orange, Fruit.peach, Fruit.pineapple, Fruit.pomegranate, Fruit.rhubarb, Fruit.starfruit, Fruit.strawberry,
+ Forageable.cactus_fruit, Fruit.cherry, Fruit.cranberries, Fruit.grape, Forageable.spice_berry, Forageable.wild_plum,
+ Vegetable.hops, Vegetable.wheat)
+ keg_rules = [self.artisan.can_keg(kegable) for kegable in eligible_kegables if kegable in self.content.game_items]
+ aged_rule = self.has(Machine.cask) & self.logic.or_(*keg_rules)
+ # There are a few other valid items, but I don't feel like coding them all
+ return fish_rule | aged_rule
+
+ def can_succeed_grange_display(self) -> StardewRule:
+ if self.options.festival_locations != FestivalLocations.option_hard:
+ return True_()
+
+ animal_rule = self.animal.has_animal(Generic.any)
+ artisan_rule = self.artisan.can_keg(Generic.any) | self.artisan.can_preserves_jar(Generic.any)
+ cooking_rule = self.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough
+ fish_rule = self.skill.can_fish(difficulty=50)
+ forage_rule = self.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall
+ mineral_rule = self.action.can_open_geode(Generic.any) # More than half the minerals are good enough
+ good_fruits = (fruit
+ for fruit in
+ (Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate,
+ Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit)
+ if fruit in self.content.game_items)
+ fruit_rule = self.has_any(*good_fruits)
+ good_vegetables = (vegeteable
+ for vegeteable in
+ (Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale,
+ Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin)
+ if vegeteable in self.content.game_items)
+ vegetable_rule = self.has_any(*good_vegetables)
+
+ return animal_rule & artisan_rule & cooking_rule & fish_rule & \
+ forage_rule & fruit_rule & mineral_rule & vegetable_rule
+
+ def can_win_fishing_competition(self) -> StardewRule:
+ return self.skill.can_fish(difficulty=60)
+
+ def has_island_trader(self) -> StardewRule:
+ if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true:
+ return False_()
+ return self.region.can_reach(Region.island_trader)
+
+ def has_all_stardrops(self) -> StardewRule:
+ other_rules = []
+ number_of_stardrops_to_receive = 0
+ number_of_stardrops_to_receive += 1 # The Mines level 100
+ number_of_stardrops_to_receive += 1 # Old Master Cannoli
+ number_of_stardrops_to_receive += 1 # Museum Stardrop
+ number_of_stardrops_to_receive += 1 # Krobus Stardrop
+
+ # Master Angler Stardrop
+ if self.content.features.fishsanity.is_enabled:
+ number_of_stardrops_to_receive += 1
+ else:
+ other_rules.append(self.fishing.can_catch_every_fish())
+
+ if self.options.festival_locations == FestivalLocations.option_disabled: # Fair Stardrop
+ other_rules.append(self.season.has(Season.fall))
+ else:
+ number_of_stardrops_to_receive += 1
+
+ # Spouse Stardrop
+ if self.content.features.friendsanity.is_enabled:
+ number_of_stardrops_to_receive += 1
+ else:
+ other_rules.append(self.relationship.has_hearts_with_any_bachelor(13))
+
+ if ModNames.deepwoods in self.options.mods: # Petting the Unicorn
+ number_of_stardrops_to_receive += 1
+
+ if not other_rules:
+ return self.received("Stardrop", number_of_stardrops_to_receive)
+
+ return self.received("Stardrop", number_of_stardrops_to_receive) & self.logic.and_(*other_rules)
+
+ def has_all_rarecrows(self) -> StardewRule:
+ rules = []
+ for rarecrow_number in range(1, 9):
+ rules.append(self.received(f"Rarecrow #{rarecrow_number}"))
+ return self.logic.and_(*rules)
+
+ def has_abandoned_jojamart(self) -> StardewRule:
+ return self.received(CommunityUpgrade.movie_theater, 1)
+
+ def has_movie_theater(self) -> StardewRule:
+ return self.received(CommunityUpgrade.movie_theater, 2)
+
+ def can_use_obelisk(self, obelisk: str) -> StardewRule:
+ return self.region.can_reach(Region.farm) & self.received(obelisk)
+
+ def can_fish_pond(self, fish: str) -> StardewRule:
+ return self.building.has_building(Building.fish_pond) & self.has(fish)
diff --git a/worlds/stardew_valley/logic/logic_and_mods_design.md b/worlds/stardew_valley/logic/logic_and_mods_design.md
new file mode 100644
index 000000000000..87631175b391
--- /dev/null
+++ b/worlds/stardew_valley/logic/logic_and_mods_design.md
@@ -0,0 +1,75 @@
+# Logic mixin
+
+Mixins are used to split the logic building methods in multiple classes, so it's more scoped and easier to extend specific methods.
+
+One single instance of Logic is necessary so mods can change the logics. This means that, when calling itself, a `Logic` class has to call its instance in
+the `logic`, because it might have been overriden.
+
+```python
+class TimeLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.time = TimeLogic(*args, **kwargs)
+
+
+class TimeLogic(BaseLogic[Union[TimeLogicMixin, ReceivedLogicMixin]]):
+
+ def has_lived_months(self, number: int) -> StardewRule:
+ return self.logic.received(Event.month_end, number)
+
+ def has_year_two(self) -> StardewRule:
+ return self.logic.time.has_lived_months(4)
+
+ def has_year_three(self) -> StardewRule:
+ return self.logic.time.has_lived_months(8)
+```
+
+Creating the rules for actual items has to be outside the `logic` instance. Once the vanilla logic builder is created, mods will be able to replace the logic
+implementations by their own modified version. For instance, the `combat` logic can be replaced by the magic mod to extends its methods to add spells in the
+combat logic.
+
+## Logic class created on the fly (idea)
+
+The logic class could be created dynamically, based on the `LogicMixin` provided by the content packs. This would allow replacing completely mixins, instead of
+overriding their logic afterward. Might be too complicated for no real gain tho...
+
+# Content pack (idea)
+
+Instead of using modules to hold the data, and have each mod adding their data to existing content, each mod data should be in a `ContentPack`. Vanilla, Ginger
+Island, or anything that could be disabled would be in a content pack as well.
+
+Eventually, Vanilla content could even be disabled (a split would be required for items that are necessary to all content packs) to have a Ginger Island only
+play through created without the heavy vanilla logic computation.
+
+## Unpacking
+
+Steps to unpack content follows the same steps has the world initialisation. Content pack however need to be unpacked in a specific order, based on their
+dependencies. Vanilla would always be first, then anything that depends only on Vanilla, etc.
+
+1. In `generate_early`, content packs are selected. The logic builders are created and content packs are unpacked so all their content is in the proper
+ item/npc/weapon lists.
+ - `ContentPack` instances are shared across players. However, some mods need to modify content of other packs. In that case, an instance of the content is
+ created specifically for that player (For instance, SVE changes the Wizard). This probably does not happen enough to require sharing those instances. If
+ necessary, a FlyWeight design pattern could be used.
+2. In `create_regions`, AP regions and entrances are unpacked, and randomized if needed.
+3. In `create_items`, AP items are unpacked, and randomized.
+4. In `set_rules`, the rules are applied to the AP entrances and locations. Each content pack have to apply the proper rules for their entrances and locations.
+ - (idea) To begin this step, sphere 0 could be simplified instantly as sphere 0 regions and items are already known.
+5. Nothing to do in `generate_basic`.
+
+## Item Sources
+
+Instead of containing rules directly, items would contain sources that would then be transformed into rules. Using a single dispatch mechanism, the sources will
+be associated to their actual logic.
+
+This system is extensible and easily maintainable in the ways that it decouple the rule and the actual items. Any "type" of item could be used with any "type"
+of source (Monster drop and fish can have foraging sources).
+
+- Mods requiring special rules can remove sources from vanilla content or wrap them to add their own logic (Magic add sources for some items), or change the
+ rules for monster drop sources.
+- (idea) A certain difficulty level (or maybe tags) could be added to the source, to enable or disable them given settings chosen by the player. Someone with a
+ high grinding tolerance can enable "hard" or "grindy" sources. Some source that are pushed back in further spheres can be replaced by less forgiving sources
+ if easy logic is disabled. For instance, anything that requires money could be accessible as soon as you can sell something to someone (even wood).
+
+Items are classified by their source. An item with a fishing or a crab pot source is considered a fish, an item dropping from a monster is a monster drop. An
+item with a foraging source is a forageable. Items can fit in multiple categories.
diff --git a/worlds/stardew_valley/logic/logic_event.py b/worlds/stardew_valley/logic/logic_event.py
new file mode 100644
index 000000000000..9af1d622578f
--- /dev/null
+++ b/worlds/stardew_valley/logic/logic_event.py
@@ -0,0 +1,33 @@
+from dataclasses import dataclass
+
+from ..strings.ap_names import event_names
+from ..strings.metal_names import MetalBar, Ore
+from ..strings.region_names import Region
+
+all_events = event_names.all_events.copy()
+all_logic_events = list()
+
+
+@dataclass(frozen=True)
+class LogicEvent:
+ name: str
+ region: str
+
+
+@dataclass(frozen=True)
+class LogicItemEvent(LogicEvent):
+ item: str
+
+ def __init__(self, item: str, region: str):
+ super().__init__(f"{item} (Logic event)", region)
+ super().__setattr__("item", item)
+
+
+def register_item_event(item: str, region: str = Region.farm):
+ event = LogicItemEvent(item, region)
+ all_logic_events.append(event)
+ all_events.add(event.name)
+
+
+for i in (MetalBar.copper, MetalBar.iron, MetalBar.gold, MetalBar.iridium, Ore.copper, Ore.iron, Ore.gold, Ore.iridium):
+ register_item_event(i)
diff --git a/worlds/stardew_valley/logic/mine_logic.py b/worlds/stardew_valley/logic/mine_logic.py
new file mode 100644
index 000000000000..61eba41ffe07
--- /dev/null
+++ b/worlds/stardew_valley/logic/mine_logic.py
@@ -0,0 +1,91 @@
+from typing import Union
+
+from Utils import cache_self1
+from .base_logic import BaseLogicMixin, BaseLogic
+from .combat_logic import CombatLogicMixin
+from .cooking_logic import CookingLogicMixin
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .skill_logic import SkillLogicMixin
+from .tool_logic import ToolLogicMixin
+from .. import options
+from ..options import ToolProgression
+from ..stardew_rule import StardewRule, True_
+from ..strings.performance_names import Performance
+from ..strings.region_names import Region
+from ..strings.skill_names import Skill
+from ..strings.tool_names import Tool, ToolMaterial
+
+
+class MineLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.mine = MineLogic(*args, **kwargs)
+
+
+class MineLogic(BaseLogic[Union[HasLogicMixin, MineLogicMixin, RegionLogicMixin, ReceivedLogicMixin, CombatLogicMixin, ToolLogicMixin,
+SkillLogicMixin, CookingLogicMixin]]):
+ # Regions
+ def can_mine_in_the_mines_floor_1_40(self) -> StardewRule:
+ return self.logic.region.can_reach(Region.mines_floor_5)
+
+ def can_mine_in_the_mines_floor_41_80(self) -> StardewRule:
+ return self.logic.region.can_reach(Region.mines_floor_45)
+
+ def can_mine_in_the_mines_floor_81_120(self) -> StardewRule:
+ return self.logic.region.can_reach(Region.mines_floor_85)
+
+ def can_mine_in_the_skull_cavern(self) -> StardewRule:
+ return (self.logic.mine.can_progress_in_the_mines_from_floor(120) &
+ self.logic.region.can_reach(Region.skull_cavern))
+
+ @cache_self1
+ def get_weapon_rule_for_floor_tier(self, tier: int):
+ if tier >= 4:
+ return self.logic.combat.can_fight_at_level(Performance.galaxy)
+ if tier >= 3:
+ return self.logic.combat.can_fight_at_level(Performance.great)
+ if tier >= 2:
+ return self.logic.combat.can_fight_at_level(Performance.good)
+ if tier >= 1:
+ return self.logic.combat.can_fight_at_level(Performance.decent)
+ return self.logic.combat.can_fight_at_level(Performance.basic)
+
+ @cache_self1
+ def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule:
+ tier = floor // 40
+ rules = []
+ weapon_rule = self.logic.mine.get_weapon_rule_for_floor_tier(tier)
+ rules.append(weapon_rule)
+ if self.options.tool_progression & ToolProgression.option_progressive:
+ rules.append(self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier]))
+ if self.options.skill_progression >= options.SkillProgression.option_progressive:
+ skill_tier = min(10, max(0, tier * 2))
+ rules.append(self.logic.skill.has_level(Skill.combat, skill_tier))
+ rules.append(self.logic.skill.has_level(Skill.mining, skill_tier))
+ if tier >= 4:
+ rules.append(self.logic.cooking.can_cook())
+ return self.logic.and_(*rules)
+
+ @cache_self1
+ def has_mine_elevator_to_floor(self, floor: int) -> StardewRule:
+ if floor < 0:
+ floor = 0
+ if self.options.elevator_progression != options.ElevatorProgression.option_vanilla:
+ return self.logic.received("Progressive Mine Elevator", floor // 5)
+ return True_()
+
+ @cache_self1
+ def can_progress_in_the_skull_cavern_from_floor(self, floor: int) -> StardewRule:
+ tier = floor // 50
+ rules = []
+ weapon_rule = self.logic.combat.has_great_weapon
+ rules.append(weapon_rule)
+ if self.options.tool_progression & ToolProgression.option_progressive:
+ rules.append(self.logic.received("Progressive Pickaxe", min(4, max(0, tier + 2))))
+ if self.options.skill_progression >= options.SkillProgression.option_progressive:
+ skill_tier = min(10, max(0, tier * 2 + 6))
+ rules.extend({self.logic.skill.has_level(Skill.combat, skill_tier),
+ self.logic.skill.has_level(Skill.mining, skill_tier)})
+ return self.logic.and_(*rules)
diff --git a/worlds/stardew_valley/logic/money_logic.py b/worlds/stardew_valley/logic/money_logic.py
new file mode 100644
index 000000000000..73c5291af082
--- /dev/null
+++ b/worlds/stardew_valley/logic/money_logic.py
@@ -0,0 +1,116 @@
+from typing import Union
+
+from Utils import cache_self1
+from .base_logic import BaseLogicMixin, BaseLogic
+from .grind_logic import GrindLogicMixin
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .season_logic import SeasonLogicMixin
+from .time_logic import TimeLogicMixin
+from ..data.shop import ShopSource
+from ..options import SpecialOrderLocations
+from ..stardew_rule import StardewRule, True_, HasProgressionPercent, False_, true_
+from ..strings.ap_names.event_names import Event
+from ..strings.currency_names import Currency
+from ..strings.region_names import Region, LogicRegion
+
+qi_gem_rewards = ("100 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems", "25 Qi Gems",
+ "20 Qi Gems", "15 Qi Gems", "10 Qi Gems")
+
+
+class MoneyLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.money = MoneyLogic(*args, **kwargs)
+
+
+class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SeasonLogicMixin,
+GrindLogicMixin]]):
+
+ @cache_self1
+ def can_have_earned_total(self, amount: int) -> StardewRule:
+ if amount < 1000:
+ return True_()
+
+ pierre_rule = self.logic.region.can_reach_all((Region.pierre_store, Region.forest))
+ willy_rule = self.logic.region.can_reach_all((Region.fish_shop, LogicRegion.fishing))
+ clint_rule = self.logic.region.can_reach_all((Region.blacksmith, Region.mines_floor_5))
+ robin_rule = self.logic.region.can_reach_all((Region.carpenter, Region.secret_woods))
+ shipping_rule = self.logic.received(Event.can_ship_items)
+
+ if amount < 2000:
+ selling_any_rule = pierre_rule | willy_rule | clint_rule | robin_rule | shipping_rule
+ return selling_any_rule
+
+ if amount < 5000:
+ selling_all_rule = (pierre_rule & willy_rule & clint_rule & robin_rule) | shipping_rule
+ return selling_all_rule
+
+ if amount < 10000:
+ return shipping_rule
+
+ seed_rules = self.logic.received(Event.can_shop_at_pierre)
+ if amount < 40000:
+ return shipping_rule & seed_rules
+
+ percent_progression_items_needed = min(90, amount // 20000)
+ return shipping_rule & seed_rules & HasProgressionPercent(self.player, percent_progression_items_needed)
+
+ @cache_self1
+ def can_spend(self, amount: int) -> StardewRule:
+ if self.options.starting_money == -1:
+ return True_()
+ return self.logic.money.can_have_earned_total(amount * 5)
+
+ # Should be cached
+ def can_spend_at(self, region: str, amount: int) -> StardewRule:
+ return self.logic.region.can_reach(region) & self.logic.money.can_spend(amount)
+
+ @cache_self1
+ def can_shop_from(self, source: ShopSource) -> StardewRule:
+ season_rule = self.logic.season.has_any(source.seasons)
+ money_rule = self.logic.money.can_spend(source.money_price) if source.money_price is not None else true_
+
+ item_rules = []
+ if source.items_price is not None:
+ for price, item in source.items_price:
+ item_rules.append(self.logic.has(item) & self.logic.grind.can_grind_item(price))
+
+ region_rule = self.logic.region.can_reach(source.shop_region)
+
+ return self.logic.and_(season_rule, money_rule, *item_rules, region_rule)
+
+ # Should be cached
+ def can_trade(self, currency: str, amount: int) -> StardewRule:
+ if amount == 0:
+ return True_()
+ if currency == Currency.money:
+ return self.can_spend(amount)
+ if currency == Currency.star_token:
+ return self.logic.region.can_reach(LogicRegion.fair)
+ if currency == Currency.qi_coin:
+ return self.logic.region.can_reach(Region.casino) & self.logic.time.has_lived_months(amount // 1000)
+ if currency == Currency.qi_gem:
+ if self.options.special_order_locations & SpecialOrderLocations.value_qi:
+ number_rewards = min(len(qi_gem_rewards), max(1, (amount // 10)))
+ return self.logic.received_n(*qi_gem_rewards, count=number_rewards)
+ number_rewards = 2
+ return self.logic.received_n(*qi_gem_rewards, count=number_rewards) & self.logic.region.can_reach(Region.qi_walnut_room) & \
+ self.logic.region.can_reach(Region.saloon) & self.can_have_earned_total(5000)
+ if currency == Currency.golden_walnut:
+ return self.can_spend_walnut(amount)
+
+ return self.logic.has(currency) & self.logic.grind.can_grind_item(amount)
+
+ # Should be cached
+ def can_trade_at(self, region: str, currency: str, amount: int) -> StardewRule:
+ if amount == 0:
+ return True_()
+ if currency == Currency.money:
+ return self.logic.money.can_spend_at(region, amount)
+
+ return self.logic.region.can_reach(region) & self.can_trade(currency, amount)
+
+ def can_spend_walnut(self, amount: int) -> StardewRule:
+ return False_()
diff --git a/worlds/stardew_valley/logic/monster_logic.py b/worlds/stardew_valley/logic/monster_logic.py
new file mode 100644
index 000000000000..7e6d786972ac
--- /dev/null
+++ b/worlds/stardew_valley/logic/monster_logic.py
@@ -0,0 +1,74 @@
+from functools import cached_property
+from typing import Iterable, Union, Hashable
+
+from Utils import cache_self1
+from .base_logic import BaseLogicMixin, BaseLogic
+from .combat_logic import CombatLogicMixin
+from .has_logic import HasLogicMixin
+from .region_logic import RegionLogicMixin
+from .time_logic import TimeLogicMixin, MAX_MONTHS
+from .. import options
+from ..data import monster_data
+from ..stardew_rule import StardewRule
+from ..strings.generic_names import Generic
+from ..strings.region_names import Region
+
+
+class MonsterLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.monster = MonsterLogic(*args, **kwargs)
+
+
+class MonsterLogic(BaseLogic[Union[HasLogicMixin, MonsterLogicMixin, RegionLogicMixin, CombatLogicMixin, TimeLogicMixin]]):
+
+ @cached_property
+ def all_monsters_by_name(self):
+ return monster_data.all_monsters_by_name_given_mods(self.options.mods.value)
+
+ @cached_property
+ def all_monsters_by_category(self):
+ return monster_data.all_monsters_by_category_given_mods(self.options.mods.value)
+
+ def can_kill(self, monster: Union[str, monster_data.StardewMonster], amount_tier: int = 0) -> StardewRule:
+ if amount_tier <= 0:
+ amount_tier = 0
+ time_rule = self.logic.time.has_lived_months(amount_tier)
+
+ if isinstance(monster, str):
+ if monster == Generic.any:
+ return self.logic.monster.can_kill_any(self.all_monsters_by_name.values()) & time_rule
+
+ monster = self.all_monsters_by_name[monster]
+ region_rule = self.logic.region.can_reach_any(monster.locations)
+ combat_rule = self.logic.combat.can_fight_at_level(monster.difficulty)
+
+ return region_rule & combat_rule & time_rule
+
+ @cache_self1
+ def can_kill_many(self, monster: monster_data.StardewMonster) -> StardewRule:
+ return self.logic.monster.can_kill(monster, MAX_MONTHS / 3)
+
+ @cache_self1
+ def can_kill_max(self, monster: monster_data.StardewMonster) -> StardewRule:
+ return self.logic.monster.can_kill(monster, MAX_MONTHS)
+
+ # Should be cached
+ def can_kill_any(self, monsters: (Iterable[monster_data.StardewMonster], Hashable), amount_tier: int = 0) -> StardewRule:
+ return self.logic.or_(*(self.logic.monster.can_kill(monster, amount_tier) for monster in monsters))
+
+ # Should be cached
+ def can_kill_all(self, monsters: (Iterable[monster_data.StardewMonster], Hashable), amount_tier: int = 0) -> StardewRule:
+ return self.logic.and_(*(self.logic.monster.can_kill(monster, amount_tier) for monster in monsters))
+
+ def can_complete_all_monster_slaying_goals(self) -> StardewRule:
+ rules = [self.logic.time.has_lived_max_months]
+ exclude_island = self.options.exclude_ginger_island == options.ExcludeGingerIsland.option_true
+ island_regions = [Region.volcano_floor_5, Region.volcano_floor_10, Region.island_west, Region.dangerous_skull_cavern]
+ for category in self.all_monsters_by_category:
+ if exclude_island and all(all(location in island_regions for location in monster.locations)
+ for monster in self.all_monsters_by_category[category]):
+ continue
+ rules.append(self.logic.monster.can_kill_any(self.all_monsters_by_category[category]))
+
+ return self.logic.and_(*rules)
diff --git a/worlds/stardew_valley/logic/museum_logic.py b/worlds/stardew_valley/logic/museum_logic.py
new file mode 100644
index 000000000000..36ba62b31fcb
--- /dev/null
+++ b/worlds/stardew_valley/logic/museum_logic.py
@@ -0,0 +1,85 @@
+from typing import Union
+
+from Utils import cache_self1
+from .action_logic import ActionLogicMixin
+from .base_logic import BaseLogic, BaseLogicMixin
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .time_logic import TimeLogicMixin
+from .tool_logic import ToolLogicMixin
+from .. import options
+from ..data.museum_data import MuseumItem, all_museum_items, all_museum_artifacts, all_museum_minerals
+from ..stardew_rule import StardewRule, False_
+from ..strings.metal_names import Mineral
+from ..strings.region_names import Region
+from ..strings.tool_names import Tool, ToolMaterial
+
+
+class MuseumLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.museum = MuseumLogic(*args, **kwargs)
+
+
+class MuseumLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, ActionLogicMixin, ToolLogicMixin, MuseumLogicMixin]]):
+
+ def can_donate_museum_items(self, number: int) -> StardewRule:
+ return self.logic.region.can_reach(Region.museum) & self.logic.museum.can_find_museum_items(number)
+
+ def can_donate_museum_artifacts(self, number: int) -> StardewRule:
+ return self.logic.region.can_reach(Region.museum) & self.logic.museum.can_find_museum_artifacts(number)
+
+ @cache_self1
+ def can_find_museum_item(self, item: MuseumItem) -> StardewRule:
+ if item.locations:
+ region_rule = self.logic.region.can_reach_all_except_one(item.locations)
+ else:
+ region_rule = False_()
+ if item.geodes:
+ geodes_rule = self.logic.and_(*(self.logic.action.can_open_geode(geode) for geode in item.geodes))
+ else:
+ geodes_rule = False_()
+ # monster_rule = self.can_farm_monster(item.monsters)
+ time_needed_to_grind = int((20 - item.difficulty) // 2)
+ time_rule = self.logic.time.has_lived_months(time_needed_to_grind)
+ pan_rule = False_()
+ if item.item_name == Mineral.earth_crystal or item.item_name == Mineral.fire_quartz or item.item_name == Mineral.frozen_tear:
+ pan_rule = self.logic.tool.has_tool(Tool.pan, ToolMaterial.iridium)
+ return (pan_rule | region_rule | geodes_rule) & time_rule # & monster_rule & extra_rule
+
+ def can_find_museum_artifacts(self, number: int) -> StardewRule:
+ rules = []
+ for artifact in all_museum_artifacts:
+ rules.append(self.logic.museum.can_find_museum_item(artifact))
+
+ return self.logic.count(number, *rules)
+
+ def can_find_museum_minerals(self, number: int) -> StardewRule:
+ rules = []
+ for mineral in all_museum_minerals:
+ rules.append(self.logic.museum.can_find_museum_item(mineral))
+
+ return self.logic.count(number, *rules)
+
+ def can_find_museum_items(self, number: int) -> StardewRule:
+ rules = []
+ for donation in all_museum_items:
+ rules.append(self.logic.museum.can_find_museum_item(donation))
+
+ return self.logic.count(number, *rules)
+
+ def can_complete_museum(self) -> StardewRule:
+ rules = [self.logic.region.can_reach(Region.museum)]
+
+ if self.options.museumsanity == options.Museumsanity.option_none:
+ rules.append(self.logic.received("Traveling Merchant Metal Detector", 2))
+ else:
+ rules.append(self.logic.received("Traveling Merchant Metal Detector", 3))
+
+ for donation in all_museum_items:
+ rules.append(self.logic.museum.can_find_museum_item(donation))
+ return self.logic.and_(*rules) & self.logic.region.can_reach(Region.museum)
+
+ def can_donate(self, item: str) -> StardewRule:
+ return self.logic.has(item) & self.logic.region.can_reach(Region.museum)
diff --git a/worlds/stardew_valley/logic/pet_logic.py b/worlds/stardew_valley/logic/pet_logic.py
new file mode 100644
index 000000000000..0438940a6633
--- /dev/null
+++ b/worlds/stardew_valley/logic/pet_logic.py
@@ -0,0 +1,47 @@
+import math
+from typing import Union
+
+from .base_logic import BaseLogicMixin, BaseLogic
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .time_logic import TimeLogicMixin
+from .tool_logic import ToolLogicMixin
+from ..content.feature.friendsanity import pet_heart_item_name
+from ..stardew_rule import StardewRule, True_
+from ..strings.region_names import Region
+
+
+class PetLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.pet = PetLogic(*args, **kwargs)
+
+
+class PetLogic(BaseLogic[Union[RegionLogicMixin, ReceivedLogicMixin, TimeLogicMixin, ToolLogicMixin]]):
+ def has_pet_hearts(self, hearts: int = 1) -> StardewRule:
+ assert hearts >= 0, "You can't have negative hearts with a pet."
+ if hearts == 0:
+ return True_()
+
+ if self.content.features.friendsanity.is_pet_randomized:
+ return self.received_pet_hearts(hearts)
+
+ return self.can_befriend_pet(hearts)
+
+ def received_pet_hearts(self, hearts: int) -> StardewRule:
+ return self.logic.received(pet_heart_item_name,
+ math.ceil(hearts / self.content.features.friendsanity.heart_size))
+
+ def can_befriend_pet(self, hearts: int) -> StardewRule:
+ assert hearts >= 0, "You can't have negative hearts with a pet."
+ if hearts == 0:
+ return True_()
+
+ points = hearts * 200
+ points_per_month = 12 * 14
+ points_per_water_month = 18 * 14
+ farm_rule = self.logic.region.can_reach(Region.farm)
+ time_with_water_rule = self.logic.tool.can_water(0) & self.logic.time.has_lived_months(points // points_per_water_month)
+ time_without_water_rule = self.logic.time.has_lived_months(points // points_per_month)
+ time_rule = time_with_water_rule | time_without_water_rule
+ return farm_rule & time_rule
diff --git a/worlds/stardew_valley/logic/quality_logic.py b/worlds/stardew_valley/logic/quality_logic.py
new file mode 100644
index 000000000000..54e2d242654b
--- /dev/null
+++ b/worlds/stardew_valley/logic/quality_logic.py
@@ -0,0 +1,33 @@
+from typing import Union
+
+from Utils import cache_self1
+from .base_logic import BaseLogicMixin, BaseLogic
+from .farming_logic import FarmingLogicMixin
+from .skill_logic import SkillLogicMixin
+from ..stardew_rule import StardewRule, True_, False_
+from ..strings.quality_names import CropQuality
+
+
+class QualityLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.quality = QualityLogic(*args, **kwargs)
+
+
+class QualityLogic(BaseLogic[Union[SkillLogicMixin, FarmingLogicMixin]]):
+
+ @cache_self1
+ def can_grow_crop_quality(self, quality: str) -> StardewRule:
+ if quality == CropQuality.basic:
+ return True_()
+ if quality == CropQuality.silver:
+ return self.logic.skill.has_farming_level(5) | (self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(2)) | (
+ self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(1)) | self.logic.farming.has_fertilizer(3)
+ if quality == CropQuality.gold:
+ return self.logic.skill.has_farming_level(10) | (
+ self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(5)) | (
+ self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(3)) | (
+ self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(2))
+ if quality == CropQuality.iridium:
+ return self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(4)
+ return False_()
diff --git a/worlds/stardew_valley/logic/quest_logic.py b/worlds/stardew_valley/logic/quest_logic.py
new file mode 100644
index 000000000000..42f401b96025
--- /dev/null
+++ b/worlds/stardew_valley/logic/quest_logic.py
@@ -0,0 +1,142 @@
+from typing import Dict, Union
+
+from .base_logic import BaseLogicMixin, BaseLogic
+from .building_logic import BuildingLogicMixin
+from .combat_logic import CombatLogicMixin
+from .cooking_logic import CookingLogicMixin
+from .fishing_logic import FishingLogicMixin
+from .has_logic import HasLogicMixin
+from .mine_logic import MineLogicMixin
+from .money_logic import MoneyLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .relationship_logic import RelationshipLogicMixin
+from .season_logic import SeasonLogicMixin
+from .skill_logic import SkillLogicMixin
+from .time_logic import TimeLogicMixin
+from .tool_logic import ToolLogicMixin
+from .wallet_logic import WalletLogicMixin
+from ..stardew_rule import StardewRule, Has, True_
+from ..strings.ap_names.community_upgrade_names import CommunityUpgrade
+from ..strings.artisan_good_names import ArtisanGood
+from ..strings.building_names import Building
+from ..strings.craftable_names import Craftable
+from ..strings.crop_names import Fruit, Vegetable
+from ..strings.fish_names import Fish
+from ..strings.food_names import Meal
+from ..strings.forageable_names import Forageable
+from ..strings.machine_names import Machine
+from ..strings.material_names import Material
+from ..strings.metal_names import MetalBar, Ore, Mineral
+from ..strings.monster_drop_names import Loot
+from ..strings.quest_names import Quest
+from ..strings.region_names import Region
+from ..strings.season_names import Season
+from ..strings.tool_names import Tool
+from ..strings.villager_names import NPC
+from ..strings.wallet_item_names import Wallet
+
+
+class QuestLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.quest = QuestLogic(*args, **kwargs)
+
+
+class QuestLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, MoneyLogicMixin, MineLogicMixin, RegionLogicMixin, RelationshipLogicMixin, ToolLogicMixin,
+ FishingLogicMixin, CookingLogicMixin, CombatLogicMixin, SeasonLogicMixin, SkillLogicMixin, WalletLogicMixin, QuestLogicMixin,
+ BuildingLogicMixin, TimeLogicMixin]]):
+
+ def initialize_rules(self):
+ self.update_rules({
+ Quest.introductions: True_(),
+ Quest.how_to_win_friends: self.logic.quest.can_complete_quest(Quest.introductions),
+ Quest.getting_started: self.logic.has(Vegetable.parsnip),
+ Quest.to_the_beach: self.logic.region.can_reach(Region.beach),
+ Quest.raising_animals: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.building.has_building(Building.coop),
+ Quest.feeding_animals: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.building.has_building(Building.silo),
+ Quest.advancement: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.has(Craftable.scarecrow),
+ Quest.archaeology: self.logic.tool.has_tool(Tool.hoe) | self.logic.mine.can_mine_in_the_mines_floor_1_40() | self.logic.skill.can_fish(),
+ Quest.rat_problem: self.logic.region.can_reach_all((Region.town, Region.community_center)),
+ Quest.meet_the_wizard: self.logic.quest.can_complete_quest(Quest.rat_problem),
+ Quest.forging_ahead: self.logic.has(Ore.copper) & self.logic.has(Machine.furnace),
+ Quest.smelting: self.logic.has(MetalBar.copper),
+ Quest.initiation: self.logic.mine.can_mine_in_the_mines_floor_1_40(),
+ Quest.robins_lost_axe: self.logic.season.has(Season.spring) & self.logic.relationship.can_meet(NPC.robin),
+ Quest.jodis_request: self.logic.season.has(Season.spring) & self.logic.has(Vegetable.cauliflower) & self.logic.relationship.can_meet(NPC.jodi),
+ Quest.mayors_shorts: self.logic.season.has(Season.summer) & self.logic.relationship.has_hearts(NPC.marnie, 2) &
+ self.logic.relationship.can_meet(NPC.lewis),
+ Quest.blackberry_basket: self.logic.season.has(Season.fall) & self.logic.relationship.can_meet(NPC.linus) & self.logic.region.can_reach(
+ Region.tunnel_entrance),
+ Quest.marnies_request: self.logic.relationship.has_hearts(NPC.marnie, 3) & self.logic.has(Forageable.cave_carrot),
+ Quest.pam_is_thirsty: self.logic.season.has(Season.summer) & self.logic.has(ArtisanGood.pale_ale) & self.logic.relationship.can_meet(NPC.pam),
+ Quest.a_dark_reagent: self.logic.season.has(Season.winter) & self.logic.has(Loot.void_essence) & self.logic.relationship.can_meet(NPC.wizard),
+ Quest.cows_delight: self.logic.season.has(Season.fall) & self.logic.has(Vegetable.amaranth) & self.logic.relationship.can_meet(NPC.marnie),
+ Quest.the_skull_key: self.logic.received(Wallet.skull_key),
+ Quest.crop_research: self.logic.season.has(Season.summer) & self.logic.has(Fruit.melon) & self.logic.relationship.can_meet(NPC.demetrius),
+ Quest.knee_therapy: self.logic.season.has(Season.summer) & self.logic.has(Fruit.hot_pepper) & self.logic.relationship.can_meet(NPC.george),
+ Quest.robins_request: self.logic.season.has(Season.winter) & self.logic.has(Material.hardwood) & self.logic.relationship.can_meet(NPC.robin),
+ Quest.qis_challenge: True_(), # The skull cavern floor 25 already has rules
+ Quest.the_mysterious_qi: (self.logic.region.can_reach_all((Region.bus_tunnel, Region.railroad, Region.mayor_house)) &
+ self.logic.has_all(ArtisanGood.battery_pack, Forageable.rainbow_shell, Vegetable.beet, Loot.solar_essence)),
+ Quest.carving_pumpkins: self.logic.season.has(Season.fall) & self.logic.has(Vegetable.pumpkin) & self.logic.relationship.can_meet(NPC.caroline),
+ Quest.a_winter_mystery: self.logic.season.has(Season.winter),
+ Quest.strange_note: self.logic.has(Forageable.secret_note) & self.logic.has(ArtisanGood.maple_syrup),
+ Quest.cryptic_note: self.logic.has(Forageable.secret_note),
+ Quest.fresh_fruit: self.logic.season.has(Season.spring) & self.logic.has(Fruit.apricot) & self.logic.relationship.can_meet(NPC.emily),
+ Quest.aquatic_research: self.logic.season.has(Season.summer) & self.logic.has(Fish.pufferfish) & self.logic.relationship.can_meet(NPC.demetrius),
+ Quest.a_soldiers_star: (self.logic.season.has(Season.summer) & self.logic.time.has_year_two & self.logic.has(Fruit.starfruit) &
+ self.logic.relationship.can_meet(NPC.kent)),
+ Quest.mayors_need: self.logic.season.has(Season.summer) & self.logic.has(ArtisanGood.truffle_oil) & self.logic.relationship.can_meet(NPC.lewis),
+ Quest.wanted_lobster: (self.logic.season.has(Season.fall) & self.logic.season.has(Season.fall) & self.logic.has(Fish.lobster) &
+ self.logic.relationship.can_meet(NPC.gus)),
+ Quest.pam_needs_juice: self.logic.season.has(Season.fall) & self.logic.has(ArtisanGood.battery_pack) & self.logic.relationship.can_meet(NPC.pam),
+ Quest.fish_casserole: self.logic.relationship.has_hearts(NPC.jodi, 4) & self.logic.has(Fish.largemouth_bass),
+ Quest.catch_a_squid: self.logic.season.has(Season.winter) & self.logic.has(Fish.squid) & self.logic.relationship.can_meet(NPC.willy),
+ Quest.fish_stew: self.logic.season.has(Season.winter) & self.logic.has(Fish.albacore) & self.logic.relationship.can_meet(NPC.gus),
+ Quest.pierres_notice: self.logic.season.has(Season.spring) & self.logic.has(Meal.sashimi) & self.logic.relationship.can_meet(NPC.pierre),
+ Quest.clints_attempt: self.logic.season.has(Season.winter) & self.logic.has(Mineral.amethyst) & self.logic.relationship.can_meet(NPC.emily),
+ Quest.a_favor_for_clint: self.logic.season.has(Season.winter) & self.logic.has(MetalBar.iron) & self.logic.relationship.can_meet(NPC.clint),
+ Quest.staff_of_power: self.logic.season.has(Season.winter) & self.logic.has(MetalBar.iridium) & self.logic.relationship.can_meet(NPC.wizard),
+ Quest.grannys_gift: self.logic.season.has(Season.spring) & self.logic.has(Forageable.leek) & self.logic.relationship.can_meet(NPC.evelyn),
+ Quest.exotic_spirits: self.logic.season.has(Season.winter) & self.logic.has(Forageable.coconut) & self.logic.relationship.can_meet(NPC.gus),
+ Quest.catch_a_lingcod: self.logic.season.has(Season.winter) & self.logic.has(Fish.lingcod) & self.logic.relationship.can_meet(NPC.willy),
+ Quest.dark_talisman: self.logic.region.can_reach(Region.railroad) & self.logic.wallet.has_rusty_key() & self.logic.relationship.can_meet(
+ NPC.krobus),
+ Quest.goblin_problem: self.logic.region.can_reach(Region.witch_swamp),
+ Quest.magic_ink: self.logic.relationship.can_meet(NPC.wizard),
+ Quest.the_pirates_wife: self.logic.relationship.can_meet(NPC.kent) & self.logic.relationship.can_meet(NPC.gus) &
+ self.logic.relationship.can_meet(NPC.sandy) & self.logic.relationship.can_meet(NPC.george) &
+ self.logic.relationship.can_meet(NPC.wizard) & self.logic.relationship.can_meet(NPC.willy),
+ Quest.giant_stump: self.logic.has(Material.hardwood)
+ })
+
+ def update_rules(self, new_rules: Dict[str, StardewRule]):
+ self.registry.quest_rules.update(new_rules)
+
+ def can_complete_quest(self, quest: str) -> StardewRule:
+ return Has(quest, self.registry.quest_rules, "quest")
+
+ def has_club_card(self) -> StardewRule:
+ if self.options.quest_locations < 0:
+ return self.logic.quest.can_complete_quest(Quest.the_mysterious_qi)
+ return self.logic.received(Wallet.club_card)
+
+ def has_magnifying_glass(self) -> StardewRule:
+ if self.options.quest_locations < 0:
+ return self.logic.quest.can_complete_quest(Quest.a_winter_mystery)
+ return self.logic.received(Wallet.magnifying_glass)
+
+ def has_dark_talisman(self) -> StardewRule:
+ if self.options.quest_locations < 0:
+ return self.logic.quest.can_complete_quest(Quest.dark_talisman)
+ return self.logic.received(Wallet.dark_talisman)
+
+ def has_raccoon_shop(self) -> StardewRule:
+ if self.options.quest_locations < 0:
+ return self.logic.received(CommunityUpgrade.raccoon, 2) & self.logic.quest.can_complete_quest(Quest.giant_stump)
+
+ # 1 - Break the tree
+ # 2 - Build the house, which summons the bundle racoon. This one is done manually if quests are turned off
+ # 3 - Raccoon's wife opens the shop
+ return self.logic.received(CommunityUpgrade.raccoon, 3)
diff --git a/worlds/stardew_valley/logic/received_logic.py b/worlds/stardew_valley/logic/received_logic.py
new file mode 100644
index 000000000000..f5c5c9f7a206
--- /dev/null
+++ b/worlds/stardew_valley/logic/received_logic.py
@@ -0,0 +1,44 @@
+from typing import Optional
+
+from BaseClasses import ItemClassification
+from .base_logic import BaseLogic, BaseLogicMixin
+from .has_logic import HasLogicMixin
+from .logic_event import all_events
+from ..items import item_table
+from ..stardew_rule import StardewRule, Received, TotalReceived
+
+
+class ReceivedLogicMixin(BaseLogic[HasLogicMixin], BaseLogicMixin):
+ def received(self, item: str, count: Optional[int] = 1) -> StardewRule:
+ assert count >= 0, "Can't receive a negative amount of item."
+
+ if item in all_events:
+ return Received(item, self.player, count, event=True)
+
+ assert item_table[item].classification & ItemClassification.progression, f"Item [{item_table[item].name}] has to be progression to be used in logic"
+ return Received(item, self.player, count)
+
+ def received_all(self, *items: str):
+ assert items, "Can't receive all of no items."
+
+ return self.logic.and_(*(self.received(item) for item in items))
+
+ def received_any(self, *items: str):
+ assert items, "Can't receive any of no items."
+
+ return self.logic.or_(*(self.received(item) for item in items))
+
+ def received_once(self, *items: str, count: int):
+ assert items, "Can't receive once of no items."
+ assert count >= 0, "Can't receive a negative amount of item."
+
+ return self.logic.count(count, *(self.received(item) for item in items))
+
+ def received_n(self, *items: str, count: int):
+ assert items, "Can't receive n of no items."
+ assert count >= 0, "Can't receive a negative amount of item."
+
+ for item in items:
+ assert item_table[item].classification & ItemClassification.progression, f"Item [{item_table[item].name}] has to be progression to be used in logic"
+
+ return TotalReceived(count, items, self.player)
diff --git a/worlds/stardew_valley/logic/region_logic.py b/worlds/stardew_valley/logic/region_logic.py
new file mode 100644
index 000000000000..69afa624f22c
--- /dev/null
+++ b/worlds/stardew_valley/logic/region_logic.py
@@ -0,0 +1,69 @@
+from typing import Tuple, Union
+
+from Utils import cache_self1
+from .base_logic import BaseLogic, BaseLogicMixin
+from .has_logic import HasLogicMixin
+from ..options import EntranceRandomization
+from ..stardew_rule import StardewRule, Reach, false_, true_
+from ..strings.region_names import Region
+
+main_outside_area = {Region.menu, Region.stardew_valley, Region.farm_house, Region.farm, Region.town, Region.beach, Region.mountain, Region.forest,
+ Region.bus_stop, Region.backwoods, Region.bus_tunnel, Region.tunnel_entrance}
+always_accessible_regions_without_er = {*main_outside_area, Region.community_center, Region.pantry, Region.crafts_room, Region.fish_tank, Region.boiler_room,
+ Region.vault, Region.bulletin_board, Region.mines, Region.hospital, Region.carpenter, Region.alex_house,
+ Region.elliott_house, Region.ranch, Region.farm_cave, Region.wizard_tower, Region.tent, Region.pierre_store,
+ Region.saloon, Region.blacksmith, Region.trailer, Region.museum, Region.mayor_house, Region.haley_house,
+ Region.sam_house, Region.jojamart, Region.fish_shop}
+
+always_regions_by_setting = {EntranceRandomization.option_disabled: always_accessible_regions_without_er,
+ EntranceRandomization.option_pelican_town: always_accessible_regions_without_er,
+ EntranceRandomization.option_non_progression: always_accessible_regions_without_er,
+ EntranceRandomization.option_buildings_without_house: main_outside_area,
+ EntranceRandomization.option_buildings: main_outside_area,
+ EntranceRandomization.option_chaos: always_accessible_regions_without_er}
+
+
+class RegionLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.region = RegionLogic(*args, **kwargs)
+
+
+class RegionLogic(BaseLogic[Union[RegionLogicMixin, HasLogicMixin]]):
+
+ @cache_self1
+ def can_reach(self, region_name: str) -> StardewRule:
+ if region_name in always_regions_by_setting[self.options.entrance_randomization]:
+ return true_
+
+ if region_name not in self.regions:
+ return false_
+
+ return Reach(region_name, "Region", self.player)
+
+ @cache_self1
+ def can_reach_any(self, region_names: Tuple[str, ...]) -> StardewRule:
+ if any(r in always_regions_by_setting[self.options.entrance_randomization] for r in region_names):
+ return true_
+
+ return self.logic.or_(*(self.logic.region.can_reach(spot) for spot in region_names))
+
+ @cache_self1
+ def can_reach_all(self, region_names: Tuple[str, ...]) -> StardewRule:
+ return self.logic.and_(*(self.logic.region.can_reach(spot) for spot in region_names))
+
+ @cache_self1
+ def can_reach_all_except_one(self, region_names: Tuple[str, ...]) -> StardewRule:
+ region_names = list(region_names)
+ num_required = len(region_names) - 1
+ if num_required <= 0:
+ num_required = len(region_names)
+ return self.logic.count(num_required, *(self.logic.region.can_reach(spot) for spot in region_names))
+
+ @cache_self1
+ def can_reach_location(self, location_name: str) -> StardewRule:
+ return Reach(location_name, "Location", self.player)
+
+ # @cache_self1
+ # def can_reach_entrance(self, entrance_name: str) -> StardewRule:
+ # return Reach(entrance_name, "Entrance", self.player)
diff --git a/worlds/stardew_valley/logic/relationship_logic.py b/worlds/stardew_valley/logic/relationship_logic.py
new file mode 100644
index 000000000000..61e63a90c83a
--- /dev/null
+++ b/worlds/stardew_valley/logic/relationship_logic.py
@@ -0,0 +1,206 @@
+import math
+from typing import Union
+
+from Utils import cache_self1
+from .base_logic import BaseLogic, BaseLogicMixin
+from .building_logic import BuildingLogicMixin
+from .gift_logic import GiftLogicMixin
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .season_logic import SeasonLogicMixin
+from .time_logic import TimeLogicMixin
+from ..content.feature import friendsanity
+from ..data.villagers_data import Villager
+from ..stardew_rule import StardewRule, True_, false_, true_
+from ..strings.ap_names.mods.mod_items import SVEQuestItem
+from ..strings.crop_names import Fruit
+from ..strings.generic_names import Generic
+from ..strings.gift_names import Gift
+from ..strings.region_names import Region
+from ..strings.season_names import Season
+from ..strings.villager_names import NPC, ModNPC
+
+possible_kids = ("Cute Baby", "Ugly Baby")
+
+
+def heart_item_name(npc: Union[str, Villager]) -> str:
+ if isinstance(npc, Villager):
+ npc = npc.name
+
+ return f"{npc} <3"
+
+
+class RelationshipLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.relationship = RelationshipLogic(*args, **kwargs)
+
+
+class RelationshipLogic(BaseLogic[Union[RelationshipLogicMixin, BuildingLogicMixin, SeasonLogicMixin, TimeLogicMixin, GiftLogicMixin, RegionLogicMixin,
+ReceivedLogicMixin, HasLogicMixin]]):
+
+ def can_date(self, npc: str) -> StardewRule:
+ return self.logic.relationship.has_hearts(npc, 8) & self.logic.has(Gift.bouquet)
+
+ def can_marry(self, npc: str) -> StardewRule:
+ return self.logic.relationship.has_hearts(npc, 10) & self.logic.has(Gift.mermaid_pendant)
+
+ def can_get_married(self) -> StardewRule:
+ return self.logic.relationship.has_hearts_with_any_bachelor(10) & self.logic.has(Gift.mermaid_pendant)
+
+ def has_children(self, number_children: int) -> StardewRule:
+ assert number_children >= 0, "Can't have a negative amount of children."
+ if number_children == 0:
+ return True_()
+
+ if not self.content.features.friendsanity.is_enabled:
+ return self.logic.relationship.can_reproduce(number_children)
+
+ return self.logic.received_n(*possible_kids, count=number_children) & self.logic.building.has_house(2)
+
+ def can_reproduce(self, number_children: int = 1) -> StardewRule:
+ assert number_children >= 0, "Can't have a negative amount of children."
+ if number_children == 0:
+ return True_()
+
+ baby_rules = [self.logic.relationship.can_get_married(),
+ self.logic.building.has_house(2),
+ self.logic.relationship.has_hearts_with_any_bachelor(12),
+ self.logic.relationship.has_children(number_children - 1)]
+
+ return self.logic.and_(*baby_rules)
+
+ @cache_self1
+ def has_hearts_with_any_bachelor(self, hearts: int = 1) -> StardewRule:
+ assert hearts >= 0, f"Can't have a negative hearts with any bachelor."
+ if hearts == 0:
+ return True_()
+
+ return self.logic.or_(*(self.logic.relationship.has_hearts(name, hearts)
+ for name, villager in self.content.villagers.items()
+ if villager.bachelor))
+
+ @cache_self1
+ def has_hearts_with_any(self, hearts: int = 1) -> StardewRule:
+ assert hearts >= 0, f"Can't have a negative hearts with any npc."
+ if hearts == 0:
+ return True_()
+
+ return self.logic.or_(*(self.logic.relationship.has_hearts(name, hearts)
+ for name, villager in self.content.villagers.items()))
+
+ def has_hearts_with_n(self, amount: int, hearts: int = 1) -> StardewRule:
+ assert hearts >= 0, f"Can't have a negative hearts with any npc."
+ assert amount >= 0, f"Can't have a negative amount of npc."
+ if hearts == 0 or amount == 0:
+ return True_()
+
+ return self.logic.count(amount, *(self.logic.relationship.has_hearts(name, hearts)
+ for name, villager in self.content.villagers.items()))
+
+ # Should be cached
+ def has_hearts(self, npc: str, hearts: int = 1) -> StardewRule:
+ assert hearts >= 0, f"Can't have a negative hearts with {npc}."
+
+ villager = self.content.villagers.get(npc)
+ if villager is None:
+ return false_
+
+ if hearts == 0:
+ return true_
+
+ heart_steps = self.content.features.friendsanity.get_randomized_hearts(villager)
+ if not heart_steps or hearts > heart_steps[-1]: # Hearts are sorted, bigger is the last one.
+ return self.logic.relationship.can_earn_relationship(npc, hearts)
+
+ return self.logic.relationship.received_hearts(villager, hearts)
+
+ # Should be cached
+ def received_hearts(self, villager: Villager, hearts: int) -> StardewRule:
+ heart_item = friendsanity.to_item_name(villager.name)
+
+ number_required = math.ceil(hearts / self.content.features.friendsanity.heart_size)
+ return self.logic.received(heart_item, number_required) & self.can_meet(villager.name)
+
+ @cache_self1
+ def can_meet(self, npc: str) -> StardewRule:
+ villager = self.content.villagers.get(npc)
+ if villager is None:
+ return false_
+
+ rules = [self.logic.region.can_reach_any(villager.locations)]
+
+ if npc == NPC.kent:
+ rules.append(self.logic.time.has_year_two)
+
+ elif npc == NPC.leo:
+ rules.append(self.logic.received("Island North Turtle"))
+
+ elif npc == ModNPC.lance:
+ rules.append(self.logic.region.can_reach(Region.volcano_floor_10))
+
+ elif npc == ModNPC.apples:
+ rules.append(self.logic.has(Fruit.starfruit))
+
+ elif npc == ModNPC.scarlett:
+ scarlett_job = self.logic.received(SVEQuestItem.scarlett_job_offer)
+ scarlett_spring = self.logic.season.has(Season.spring) & self.can_meet(ModNPC.andy)
+ scarlett_summer = self.logic.season.has(Season.summer) & self.can_meet(ModNPC.susan)
+ scarlett_fall = self.logic.season.has(Season.fall) & self.can_meet(ModNPC.sophia)
+ rules.append(scarlett_job & (scarlett_spring | scarlett_summer | scarlett_fall))
+
+ elif npc == ModNPC.morgan:
+ rules.append(self.logic.received(SVEQuestItem.morgan_schooling))
+
+ elif npc == ModNPC.goblin:
+ rules.append(self.logic.region.can_reach_all((Region.witch_hut, Region.wizard_tower)))
+
+ return self.logic.and_(*rules)
+
+ def can_give_loved_gifts_to_everyone(self) -> StardewRule:
+ rules = []
+
+ for npc in self.content.villagers:
+ meet_rule = self.logic.relationship.can_meet(npc)
+ rules.append(meet_rule)
+
+ rules.append(self.logic.gifts.has_any_universal_love)
+
+ return self.logic.and_(*rules)
+
+ # Should be cached
+ def can_earn_relationship(self, npc: str, hearts: int = 0) -> StardewRule:
+ assert hearts >= 0, f"Can't have a negative hearts with {npc}."
+
+ villager = self.content.villagers.get(npc)
+ if villager is None:
+ return false_
+
+ if hearts == 0:
+ return True_()
+
+ rules = [self.logic.relationship.can_meet(npc)]
+
+ heart_size = self.content.features.friendsanity.heart_size
+ max_randomized_hearts = self.content.features.friendsanity.get_randomized_hearts(villager)
+ if max_randomized_hearts:
+ if hearts > max_randomized_hearts[-1]:
+ rules.append(self.logic.relationship.has_hearts(npc, hearts - 1))
+ else:
+ previous_heart = max(hearts - heart_size, 0)
+ rules.append(self.logic.relationship.has_hearts(npc, previous_heart))
+
+ if hearts > 2 or hearts > heart_size:
+ rules.append(self.logic.season.has(villager.birthday))
+
+ if villager.birthday == Generic.any:
+ rules.append(self.logic.season.has_all() | self.logic.time.has_year_three) # push logic back for any birthday-less villager
+
+ if villager.bachelor:
+ if hearts > 10:
+ rules.append(self.logic.relationship.can_marry(npc))
+ elif hearts > 8:
+ rules.append(self.logic.relationship.can_date(npc))
+
+ return self.logic.and_(*rules)
diff --git a/worlds/stardew_valley/logic/requirement_logic.py b/worlds/stardew_valley/logic/requirement_logic.py
new file mode 100644
index 000000000000..6a5adf4890c9
--- /dev/null
+++ b/worlds/stardew_valley/logic/requirement_logic.py
@@ -0,0 +1,80 @@
+import functools
+from typing import Union, Iterable
+
+from .base_logic import BaseLogicMixin, BaseLogic
+from .book_logic import BookLogicMixin
+from .combat_logic import CombatLogicMixin
+from .fishing_logic import FishingLogicMixin
+from .has_logic import HasLogicMixin
+from .quest_logic import QuestLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .relationship_logic import RelationshipLogicMixin
+from .season_logic import SeasonLogicMixin
+from .skill_logic import SkillLogicMixin
+from .time_logic import TimeLogicMixin
+from .tool_logic import ToolLogicMixin
+from .walnut_logic import WalnutLogicMixin
+from ..data.game_item import Requirement
+from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, CombatRequirement, QuestRequirement, \
+ RelationshipRequirement, FishingRequirement, WalnutRequirement
+
+
+class RequirementLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.requirement = RequirementLogic(*args, **kwargs)
+
+
+class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin,
+SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, RelationshipLogicMixin, FishingLogicMixin, WalnutLogicMixin]]):
+
+ def meet_all_requirements(self, requirements: Iterable[Requirement]):
+ if not requirements:
+ return self.logic.true_
+ return self.logic.and_(*(self.logic.requirement.meet_requirement(requirement) for requirement in requirements))
+
+ @functools.singledispatchmethod
+ def meet_requirement(self, requirement: Requirement):
+ raise ValueError(f"Requirements of type{type(requirement)} have no rule registered.")
+
+ @meet_requirement.register
+ def _(self, requirement: ToolRequirement):
+ return self.logic.tool.has_tool(requirement.tool, requirement.tier)
+
+ @meet_requirement.register
+ def _(self, requirement: SkillRequirement):
+ return self.logic.skill.has_level(requirement.skill, requirement.level)
+
+ @meet_requirement.register
+ def _(self, requirement: BookRequirement):
+ return self.logic.book.has_book_power(requirement.book)
+
+ @meet_requirement.register
+ def _(self, requirement: SeasonRequirement):
+ return self.logic.season.has(requirement.season)
+
+ @meet_requirement.register
+ def _(self, requirement: YearRequirement):
+ return self.logic.time.has_year(requirement.year)
+
+ @meet_requirement.register
+ def _(self, requirement: WalnutRequirement):
+ return self.logic.walnut.has_walnut(requirement.amount)
+
+ @meet_requirement.register
+ def _(self, requirement: CombatRequirement):
+ return self.logic.combat.can_fight_at_level(requirement.level)
+
+ @meet_requirement.register
+ def _(self, requirement: QuestRequirement):
+ return self.logic.quest.can_complete_quest(requirement.quest)
+
+ @meet_requirement.register
+ def _(self, requirement: RelationshipRequirement):
+ return self.logic.relationship.has_hearts(requirement.npc, requirement.hearts)
+
+ @meet_requirement.register
+ def _(self, requirement: FishingRequirement):
+ return self.logic.fishing.can_fish_at(requirement.region)
+
+
diff --git a/worlds/stardew_valley/logic/season_logic.py b/worlds/stardew_valley/logic/season_logic.py
new file mode 100644
index 000000000000..6df315c0db94
--- /dev/null
+++ b/worlds/stardew_valley/logic/season_logic.py
@@ -0,0 +1,65 @@
+from functools import cached_property
+from typing import Iterable, Union
+
+from Utils import cache_self1
+from .base_logic import BaseLogic, BaseLogicMixin
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .time_logic import TimeLogicMixin
+from ..options import SeasonRandomization
+from ..stardew_rule import StardewRule, True_, true_
+from ..strings.generic_names import Generic
+from ..strings.season_names import Season
+
+
+class SeasonLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.season = SeasonLogic(*args, **kwargs)
+
+
+class SeasonLogic(BaseLogic[Union[HasLogicMixin, SeasonLogicMixin, TimeLogicMixin, ReceivedLogicMixin]]):
+
+ @cached_property
+ def has_spring(self) -> StardewRule:
+ return self.logic.season.has(Season.spring)
+
+ @cached_property
+ def has_summer(self) -> StardewRule:
+ return self.logic.season.has(Season.summer)
+
+ @cached_property
+ def has_fall(self) -> StardewRule:
+ return self.logic.season.has(Season.fall)
+
+ @cached_property
+ def has_winter(self) -> StardewRule:
+ return self.logic.season.has(Season.winter)
+
+ @cache_self1
+ def has(self, season: str) -> StardewRule:
+ if season == Generic.any:
+ return True_()
+ seasons_order = [Season.spring, Season.summer, Season.fall, Season.winter]
+ if self.options.season_randomization == SeasonRandomization.option_progressive:
+ return self.logic.received(Season.progressive, seasons_order.index(season))
+ if self.options.season_randomization == SeasonRandomization.option_disabled:
+ if season == Season.spring:
+ return True_()
+ return self.logic.time.has_lived_months(1)
+ return self.logic.received(season)
+
+ def has_any(self, seasons: Iterable[str]):
+ if seasons == Season.all:
+ return true_
+ if not seasons:
+ # That should be false, but I'm scared.
+ return True_()
+ return self.logic.or_(*(self.logic.season.has(season) for season in seasons))
+
+ def has_any_not_winter(self):
+ return self.logic.season.has_any([Season.spring, Season.summer, Season.fall])
+
+ def has_all(self):
+ seasons = [Season.spring, Season.summer, Season.fall, Season.winter]
+ return self.logic.and_(*(self.logic.season.has(season) for season in seasons))
diff --git a/worlds/stardew_valley/logic/shipping_logic.py b/worlds/stardew_valley/logic/shipping_logic.py
new file mode 100644
index 000000000000..8d545e219627
--- /dev/null
+++ b/worlds/stardew_valley/logic/shipping_logic.py
@@ -0,0 +1,60 @@
+from functools import cached_property
+from typing import Union, List
+
+from Utils import cache_self1
+from .base_logic import BaseLogic, BaseLogicMixin
+from .building_logic import BuildingLogicMixin
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from ..locations import LocationTags, locations_by_tag
+from ..options import ExcludeGingerIsland, Shipsanity
+from ..options import SpecialOrderLocations
+from ..stardew_rule import StardewRule
+from ..strings.ap_names.event_names import Event
+from ..strings.building_names import Building
+
+
+class ShippingLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.shipping = ShippingLogic(*args, **kwargs)
+
+
+class ShippingLogic(BaseLogic[Union[ReceivedLogicMixin, ShippingLogicMixin, BuildingLogicMixin, RegionLogicMixin, HasLogicMixin]]):
+
+ @cached_property
+ def can_use_shipping_bin(self) -> StardewRule:
+ return self.logic.building.has_building(Building.shipping_bin)
+
+ @cache_self1
+ def can_ship(self, item: str) -> StardewRule:
+ return self.logic.received(Event.can_ship_items) & self.logic.has(item)
+
+ def can_ship_everything(self) -> StardewRule:
+ shipsanity_prefix = "Shipsanity: "
+ all_items_to_ship = []
+ exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
+ exclude_qi = not (self.options.special_order_locations & SpecialOrderLocations.value_qi)
+ mod_list = self.options.mods.value
+ for location in locations_by_tag[LocationTags.SHIPSANITY_FULL_SHIPMENT]:
+ if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
+ continue
+ if exclude_qi and LocationTags.REQUIRES_QI_ORDERS in location.tags:
+ continue
+ if location.mod_name and location.mod_name not in mod_list:
+ continue
+ all_items_to_ship.append(location.name[len(shipsanity_prefix):])
+ return self.logic.building.has_building(Building.shipping_bin) & self.logic.has_all(*all_items_to_ship)
+
+ def can_ship_everything_in_slot(self, all_location_names_in_slot: List[str]) -> StardewRule:
+ if self.options.shipsanity == Shipsanity.option_none:
+ return self.can_ship_everything()
+
+ rules = [self.logic.building.has_building(Building.shipping_bin)]
+
+ for shipsanity_location in locations_by_tag[LocationTags.SHIPSANITY]:
+ if shipsanity_location.name not in all_location_names_in_slot:
+ continue
+ rules.append(self.logic.region.can_reach_location(shipsanity_location.name))
+ return self.logic.and_(*rules)
diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py
new file mode 100644
index 000000000000..4d5567302afe
--- /dev/null
+++ b/worlds/stardew_valley/logic/skill_logic.py
@@ -0,0 +1,209 @@
+from functools import cached_property
+from typing import Union, Tuple
+
+from Utils import cache_self1
+from .base_logic import BaseLogicMixin, BaseLogic
+from .combat_logic import CombatLogicMixin
+from .harvesting_logic import HarvestingLogicMixin
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .season_logic import SeasonLogicMixin
+from .time_logic import TimeLogicMixin
+from .tool_logic import ToolLogicMixin
+from .. import options
+from ..data.harvest import HarvestCropSource
+from ..mods.logic.magic_logic import MagicLogicMixin
+from ..mods.logic.mod_skills_levels import get_mod_skill_levels
+from ..stardew_rule import StardewRule, True_, False_, true_, And
+from ..strings.craftable_names import Fishing
+from ..strings.machine_names import Machine
+from ..strings.performance_names import Performance
+from ..strings.quality_names import ForageQuality
+from ..strings.region_names import Region
+from ..strings.skill_names import Skill, all_mod_skills
+from ..strings.tool_names import ToolMaterial, Tool
+from ..strings.wallet_item_names import Wallet
+
+fishing_regions = (Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west)
+vanilla_skill_items = ("Farming Level", "Mining Level", "Foraging Level", "Fishing Level", "Combat Level")
+
+
+class SkillLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.skill = SkillLogic(*args, **kwargs)
+
+
+class SkillLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, TimeLogicMixin, ToolLogicMixin, SkillLogicMixin,
+CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
+
+ # Should be cached
+ def can_earn_level(self, skill: str, level: int) -> StardewRule:
+ if level <= 0:
+ return True_()
+
+ tool_level = (level - 1) // 2
+ tool_material = ToolMaterial.tiers[tool_level]
+ months = max(1, level - 1)
+ months_rule = self.logic.time.has_lived_months(months)
+
+ if self.options.skill_progression != options.SkillProgression.option_vanilla:
+ previous_level_rule = self.logic.skill.has_level(skill, level - 1)
+ else:
+ previous_level_rule = true_
+
+ if skill == Skill.fishing:
+ xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 3))
+ elif skill == Skill.farming:
+ xp_rule = self.can_get_farming_xp & self.logic.tool.has_tool(Tool.hoe, tool_material) & self.logic.tool.can_water(tool_level)
+ elif skill == Skill.foraging:
+ xp_rule = (self.can_get_foraging_xp & self.logic.tool.has_tool(Tool.axe, tool_material)) |\
+ self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level)
+ elif skill == Skill.mining:
+ xp_rule = self.logic.tool.has_tool(Tool.pickaxe, tool_material) | \
+ self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level)
+ xp_rule = xp_rule & self.logic.region.can_reach(Region.mines_floor_5)
+ elif skill == Skill.combat:
+ combat_tier = Performance.tiers[tool_level]
+ xp_rule = self.logic.combat.can_fight_at_level(combat_tier)
+ xp_rule = xp_rule & self.logic.region.can_reach(Region.mines_floor_5)
+ elif skill in all_mod_skills:
+ # Ideal solution would be to add a logic registry, but I'm too lazy.
+ return previous_level_rule & months_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level)
+ else:
+ raise Exception(f"Unknown skill: {skill}")
+
+ return previous_level_rule & months_rule & xp_rule
+
+ # Should be cached
+ def has_level(self, skill: str, level: int) -> StardewRule:
+ if level <= 0:
+ return True_()
+
+ if self.options.skill_progression == options.SkillProgression.option_vanilla:
+ return self.logic.skill.can_earn_level(skill, level)
+
+ return self.logic.received(f"{skill} Level", level)
+
+ @cache_self1
+ def has_farming_level(self, level: int) -> StardewRule:
+ return self.logic.skill.has_level(Skill.farming, level)
+
+ # Should be cached
+ def has_total_level(self, level: int, allow_modded_skills: bool = False) -> StardewRule:
+ if level <= 0:
+ return True_()
+
+ if self.options.skill_progression >= options.SkillProgression.option_progressive:
+ skills_items = vanilla_skill_items
+ if allow_modded_skills:
+ skills_items += get_mod_skill_levels(self.options.mods)
+ return self.logic.received_n(*skills_items, count=level)
+
+ months_with_4_skills = max(1, (level // 4) - 1)
+ months_with_5_skills = max(1, (level // 5) - 1)
+ rule_with_fishing = self.logic.time.has_lived_months(months_with_5_skills) & self.logic.skill.can_get_fishing_xp
+ if level > 40:
+ return rule_with_fishing
+ return self.logic.time.has_lived_months(months_with_4_skills) | rule_with_fishing
+
+ def has_all_skills_maxed(self, included_modded_skills: bool = True) -> StardewRule:
+ if self.options.skill_progression == options.SkillProgression.option_vanilla:
+ return self.has_total_level(50)
+ skills_items = vanilla_skill_items
+ if included_modded_skills:
+ skills_items += get_mod_skill_levels(self.options.mods)
+ return And(*[self.logic.received(skill, 10) for skill in skills_items])
+
+ def can_enter_mastery_cave(self) -> StardewRule:
+ if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries:
+ return self.logic.received(Wallet.mastery_of_the_five_ways)
+ return self.has_all_skills_maxed()
+
+ @cached_property
+ def can_get_farming_xp(self) -> StardewRule:
+ sources = self.content.find_sources_of_type(HarvestCropSource)
+ crop_rules = []
+ for crop_source in sources:
+ crop_rules.append(self.logic.harvesting.can_harvest_crop_from(crop_source))
+ return self.logic.or_(*crop_rules)
+
+ @cached_property
+ def can_get_foraging_xp(self) -> StardewRule:
+ tool_rule = self.logic.tool.has_tool(Tool.axe)
+ tree_rule = self.logic.region.can_reach(Region.forest) & self.logic.season.has_any_not_winter()
+ stump_rule = self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.axe, ToolMaterial.copper)
+ return tool_rule & (tree_rule | stump_rule)
+
+ @cached_property
+ def can_get_mining_xp(self) -> StardewRule:
+ tool_rule = self.logic.tool.has_tool(Tool.pickaxe)
+ stone_rule = self.logic.region.can_reach_any((Region.mines_floor_5, Region.quarry, Region.skull_cavern_25, Region.volcano_floor_5))
+ return tool_rule & stone_rule
+
+ @cached_property
+ def can_get_combat_xp(self) -> StardewRule:
+ tool_rule = self.logic.combat.has_any_weapon()
+ enemy_rule = self.logic.region.can_reach_any((Region.mines_floor_5, Region.skull_cavern_25, Region.volcano_floor_5))
+ return tool_rule & enemy_rule
+
+ @cached_property
+ def can_get_fishing_xp(self) -> StardewRule:
+ if self.options.skill_progression >= options.SkillProgression.option_progressive:
+ return self.logic.skill.can_fish() | self.logic.skill.can_crab_pot
+
+ return self.logic.skill.can_fish()
+
+ # Should be cached
+ def can_fish(self, regions: Union[str, Tuple[str, ...]] = None, difficulty: int = 0) -> StardewRule:
+ if isinstance(regions, str):
+ regions = regions,
+
+ if regions is None or len(regions) == 0:
+ regions = fishing_regions
+
+ skill_required = min(10, max(0, int((difficulty / 10) - 1)))
+ if difficulty <= 40:
+ skill_required = 0
+
+ skill_rule = self.logic.skill.has_level(Skill.fishing, skill_required)
+ region_rule = self.logic.region.can_reach_any(regions)
+ # Training rod only works with fish < 50. Fiberglass does not help you to catch higher difficulty fish, so it's skipped in logic.
+ number_fishing_rod_required = 1 if difficulty < 50 else (2 if difficulty < 80 else 4)
+ return self.logic.tool.has_fishing_rod(number_fishing_rod_required) & skill_rule & region_rule
+
+ @cache_self1
+ def can_crab_pot_at(self, region: str) -> StardewRule:
+ return self.logic.skill.can_crab_pot & self.logic.region.can_reach(region)
+
+ @cached_property
+ def can_crab_pot(self) -> StardewRule:
+ crab_pot_rule = self.logic.has(Fishing.bait)
+ if self.options.skill_progression >= options.SkillProgression.option_progressive:
+ crab_pot_rule = crab_pot_rule & self.logic.has(Machine.crab_pot)
+ else:
+ crab_pot_rule = crab_pot_rule & self.logic.skill.can_get_fishing_xp
+
+ water_region_rules = self.logic.region.can_reach_any(fishing_regions)
+ return crab_pot_rule & water_region_rules
+
+ def can_forage_quality(self, quality: str) -> StardewRule:
+ if quality == ForageQuality.basic:
+ return True_()
+ if quality == ForageQuality.silver:
+ return self.has_level(Skill.foraging, 5)
+ if quality == ForageQuality.gold:
+ return self.has_level(Skill.foraging, 9)
+ return False_()
+
+ @cached_property
+ def can_earn_mastery_experience(self) -> StardewRule:
+ if self.options.skill_progression != options.SkillProgression.option_progressive_with_masteries:
+ return self.has_all_skills_maxed() & self.logic.time.has_lived_max_months
+ return self.logic.time.has_lived_max_months
+
+ def has_mastery(self, skill: str) -> StardewRule:
+ if self.options.skill_progression != options.SkillProgression.option_progressive_with_masteries:
+ return self.can_earn_mastery_experience and self.logic.region.can_reach(Region.mastery_cave)
+ return self.logic.received(f"{skill} Mastery")
diff --git a/worlds/stardew_valley/logic/source_logic.py b/worlds/stardew_valley/logic/source_logic.py
new file mode 100644
index 000000000000..0e9b8e976f5b
--- /dev/null
+++ b/worlds/stardew_valley/logic/source_logic.py
@@ -0,0 +1,106 @@
+import functools
+from typing import Union, Any, Iterable
+
+from .artisan_logic import ArtisanLogicMixin
+from .base_logic import BaseLogicMixin, BaseLogic
+from .grind_logic import GrindLogicMixin
+from .harvesting_logic import HarvestingLogicMixin
+from .has_logic import HasLogicMixin
+from .money_logic import MoneyLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .requirement_logic import RequirementLogicMixin
+from .tool_logic import ToolLogicMixin
+from ..data.artisan import MachineSource
+from ..data.game_item import GenericSource, ItemSource, GameItem, CustomRuleSource
+from ..data.harvest import ForagingSource, FruitBatsSource, MushroomCaveSource, SeasonalForagingSource, \
+ HarvestCropSource, HarvestFruitTreeSource, ArtifactSpotSource
+from ..data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
+
+
+class SourceLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.source = SourceLogic(*args, **kwargs)
+
+
+class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogicMixin, HarvestingLogicMixin, MoneyLogicMixin, RegionLogicMixin,
+ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]):
+
+ def has_access_to_item(self, item: GameItem):
+ rules = []
+
+ if self.content.features.cropsanity.is_included(item):
+ rules.append(self.logic.received(item.name))
+
+ rules.append(self.logic.source.has_access_to_any(item.sources))
+ return self.logic.and_(*rules)
+
+ def has_access_to_any(self, sources: Iterable[ItemSource]):
+ return self.logic.or_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements)
+ for source in sources))
+
+ @functools.singledispatchmethod
+ def has_access_to(self, source: Any):
+ raise ValueError(f"Sources of type{type(source)} have no rule registered.")
+
+ @has_access_to.register
+ def _(self, source: GenericSource):
+ return self.logic.region.can_reach_any(source.regions) if source.regions else self.logic.true_
+
+ @has_access_to.register
+ def _(self, source: CustomRuleSource):
+ return source.create_rule(self.logic)
+
+ @has_access_to.register
+ def _(self, source: ForagingSource):
+ return self.logic.harvesting.can_forage_from(source)
+
+ @has_access_to.register
+ def _(self, source: SeasonalForagingSource):
+ # Implementation could be different with some kind of "calendar shuffle"
+ return self.logic.harvesting.can_forage_from(source.as_foraging_source())
+
+ @has_access_to.register
+ def _(self, _: FruitBatsSource):
+ return self.logic.harvesting.can_harvest_from_fruit_bats
+
+ @has_access_to.register
+ def _(self, _: MushroomCaveSource):
+ return self.logic.harvesting.can_harvest_from_mushroom_cave
+
+ @has_access_to.register
+ def _(self, source: ShopSource):
+ return self.logic.money.can_shop_from(source)
+
+ @has_access_to.register
+ def _(self, source: HarvestFruitTreeSource):
+ return self.logic.harvesting.can_harvest_tree_from(source)
+
+ @has_access_to.register
+ def _(self, source: HarvestCropSource):
+ return self.logic.harvesting.can_harvest_crop_from(source)
+
+ @has_access_to.register
+ def _(self, source: MachineSource):
+ return self.logic.artisan.can_produce_from(source)
+
+ @has_access_to.register
+ def _(self, source: MysteryBoxSource):
+ return self.logic.grind.can_grind_mystery_boxes(source.amount)
+
+ @has_access_to.register
+ def _(self, source: ArtifactTroveSource):
+ return self.logic.grind.can_grind_artifact_troves(source.amount)
+
+ @has_access_to.register
+ def _(self, source: PrizeMachineSource):
+ return self.logic.grind.can_grind_prize_tickets(source.amount)
+
+ @has_access_to.register
+ def _(self, source: FishingTreasureChestSource):
+ return self.logic.grind.can_grind_fishing_treasure_chests(source.amount)
+
+ @has_access_to.register
+ def _(self, source: ArtifactSpotSource):
+ return self.logic.grind.can_grind_artifact_spots(source.amount)
diff --git a/worlds/stardew_valley/logic/special_order_logic.py b/worlds/stardew_valley/logic/special_order_logic.py
new file mode 100644
index 000000000000..65497df477b8
--- /dev/null
+++ b/worlds/stardew_valley/logic/special_order_logic.py
@@ -0,0 +1,125 @@
+from typing import Dict, Union
+
+from .ability_logic import AbilityLogicMixin
+from .arcade_logic import ArcadeLogicMixin
+from .artisan_logic import ArtisanLogicMixin
+from .base_logic import BaseLogicMixin, BaseLogic
+from .cooking_logic import CookingLogicMixin
+from .has_logic import HasLogicMixin
+from .mine_logic import MineLogicMixin
+from .money_logic import MoneyLogicMixin
+from .monster_logic import MonsterLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .relationship_logic import RelationshipLogicMixin
+from .season_logic import SeasonLogicMixin
+from .shipping_logic import ShippingLogicMixin
+from .skill_logic import SkillLogicMixin
+from .time_logic import TimeLogicMixin
+from .tool_logic import ToolLogicMixin
+from ..content.vanilla.ginger_island import ginger_island_content_pack
+from ..content.vanilla.qi_board import qi_board_content_pack
+from ..stardew_rule import StardewRule, Has, false_
+from ..strings.animal_product_names import AnimalProduct
+from ..strings.ap_names.event_names import Event
+from ..strings.ap_names.transport_names import Transportation
+from ..strings.artisan_good_names import ArtisanGood
+from ..strings.crop_names import Vegetable, Fruit
+from ..strings.fertilizer_names import Fertilizer
+from ..strings.fish_names import Fish
+from ..strings.forageable_names import Forageable
+from ..strings.machine_names import Machine
+from ..strings.material_names import Material
+from ..strings.metal_names import Mineral
+from ..strings.monster_drop_names import Loot
+from ..strings.monster_names import Monster
+from ..strings.region_names import Region
+from ..strings.season_names import Season
+from ..strings.special_order_names import SpecialOrder
+from ..strings.villager_names import NPC
+
+
+class SpecialOrderLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.special_order = SpecialOrderLogic(*args, **kwargs)
+
+
+class SpecialOrderLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, TimeLogicMixin, MoneyLogicMixin,
+ShippingLogicMixin, ArcadeLogicMixin, ArtisanLogicMixin, RelationshipLogicMixin, ToolLogicMixin, SkillLogicMixin,
+MineLogicMixin, CookingLogicMixin,
+AbilityLogicMixin, SpecialOrderLogicMixin, MonsterLogicMixin]]):
+
+ def initialize_rules(self):
+ self.update_rules({
+ SpecialOrder.cave_patrol: self.logic.relationship.can_meet(NPC.clint),
+ SpecialOrder.aquatic_overpopulation: self.logic.relationship.can_meet(NPC.demetrius) & self.logic.ability.can_fish_perfectly(),
+ SpecialOrder.biome_balance: self.logic.relationship.can_meet(NPC.demetrius) & self.logic.ability.can_fish_perfectly(),
+ SpecialOrder.rock_rejuivenation: (self.logic.relationship.has_hearts(NPC.emily, 4) &
+ self.logic.has_all(Mineral.ruby, Mineral.topaz, Mineral.emerald, Mineral.jade, Mineral.amethyst,
+ ArtisanGood.cloth)),
+ SpecialOrder.gifts_for_george: self.logic.season.has(Season.spring) & self.logic.has(Forageable.leek),
+ SpecialOrder.fragments_of_the_past: self.logic.monster.can_kill(Monster.skeleton),
+ SpecialOrder.gus_famous_omelet: self.logic.has(AnimalProduct.any_egg),
+ SpecialOrder.crop_order: self.logic.ability.can_farm_perfectly() & self.logic.received(Event.can_ship_items),
+ SpecialOrder.community_cleanup: self.logic.skill.can_crab_pot,
+ SpecialOrder.the_strong_stuff: self.logic.has(ArtisanGood.specific_juice(Vegetable.potato)),
+ SpecialOrder.pierres_prime_produce: self.logic.ability.can_farm_perfectly(),
+ SpecialOrder.robins_project: self.logic.relationship.can_meet(NPC.robin) & self.logic.ability.can_chop_perfectly() &
+ self.logic.has(Material.hardwood),
+ SpecialOrder.robins_resource_rush: self.logic.relationship.can_meet(NPC.robin) & self.logic.ability.can_chop_perfectly() &
+ self.logic.has(Fertilizer.tree) & self.logic.ability.can_mine_perfectly(),
+ SpecialOrder.juicy_bugs_wanted: self.logic.has(Loot.bug_meat),
+ SpecialOrder.a_curious_substance: self.logic.region.can_reach(Region.wizard_tower),
+ SpecialOrder.prismatic_jelly: self.logic.region.can_reach(Region.wizard_tower),
+
+ })
+
+ if ginger_island_content_pack.name in self.content.registered_packs:
+ self.update_rules({
+ SpecialOrder.island_ingredients: self.logic.relationship.can_meet(NPC.caroline) & self.logic.special_order.has_island_transport() &
+ self.logic.ability.can_farm_perfectly() & self.logic.shipping.can_ship(Vegetable.taro_root) &
+ self.logic.shipping.can_ship(Fruit.pineapple) & self.logic.shipping.can_ship(Forageable.ginger),
+ SpecialOrder.tropical_fish: self.logic.relationship.can_meet(NPC.willy) & self.logic.received("Island Resort") &
+ self.logic.special_order.has_island_transport() &
+ self.logic.has(Fish.stingray) & self.logic.has(Fish.blue_discus) & self.logic.has(Fish.lionfish),
+ })
+ else:
+ self.update_rules({
+ SpecialOrder.island_ingredients: false_,
+ SpecialOrder.tropical_fish: false_,
+ })
+
+ if qi_board_content_pack.name in self.content.registered_packs:
+ self.update_rules({
+ SpecialOrder.qis_crop: self.logic.ability.can_farm_perfectly() & self.logic.region.can_reach(Region.greenhouse) &
+ self.logic.region.can_reach(Region.island_west) & self.logic.skill.has_total_level(50) &
+ self.logic.has(Machine.seed_maker) & self.logic.received(Event.can_ship_items),
+ SpecialOrder.lets_play_a_game: self.logic.arcade.has_junimo_kart_max_level(),
+ SpecialOrder.four_precious_stones: self.logic.time.has_lived_max_months & self.logic.has("Prismatic Shard") &
+ self.logic.ability.can_mine_perfectly_in_the_skull_cavern(),
+ SpecialOrder.qis_hungry_challenge: self.logic.ability.can_mine_perfectly_in_the_skull_cavern(),
+ SpecialOrder.qis_cuisine: self.logic.cooking.can_cook() & self.logic.received(Event.can_ship_items) &
+ (self.logic.money.can_spend_at(Region.saloon, 205000) | self.logic.money.can_spend_at(Region.pierre_store, 170000)),
+ SpecialOrder.qis_kindness: self.logic.relationship.can_give_loved_gifts_to_everyone(),
+ SpecialOrder.extended_family: self.logic.ability.can_fish_perfectly() & self.logic.has(Fish.angler) & self.logic.has(Fish.glacierfish) &
+ self.logic.has(Fish.crimsonfish) & self.logic.has(Fish.mutant_carp) & self.logic.has(Fish.legend),
+ SpecialOrder.danger_in_the_deep: self.logic.ability.can_mine_perfectly() & self.logic.mine.has_mine_elevator_to_floor(120),
+ SpecialOrder.skull_cavern_invasion: self.logic.ability.can_mine_perfectly_in_the_skull_cavern(),
+ SpecialOrder.qis_prismatic_grange: self.logic.has(Loot.bug_meat) & # 100 Bug Meat
+ self.logic.money.can_spend_at(Region.saloon, 24000) & # 100 Spaghetti
+ self.logic.money.can_spend_at(Region.blacksmith, 15000) & # 100 Copper Ore
+ self.logic.money.can_spend_at(Region.ranch, 5000) & # 100 Hay
+ self.logic.money.can_spend_at(Region.saloon, 22000) & # 100 Salads
+ self.logic.money.can_spend_at(Region.saloon, 7500) & # 100 Joja Cola
+ self.logic.money.can_spend(80000), # I need this extra rule because money rules aren't additive...)
+ })
+
+ def update_rules(self, new_rules: Dict[str, StardewRule]):
+ self.registry.special_order_rules.update(new_rules)
+
+ def can_complete_special_order(self, special_order: str) -> StardewRule:
+ return Has(special_order, self.registry.special_order_rules, "special order")
+
+ def has_island_transport(self) -> StardewRule:
+ return self.logic.received(Transportation.island_obelisk) | self.logic.received(Transportation.boat_repair)
diff --git a/worlds/stardew_valley/logic/time_logic.py b/worlds/stardew_valley/logic/time_logic.py
new file mode 100644
index 000000000000..2ba76579ff45
--- /dev/null
+++ b/worlds/stardew_valley/logic/time_logic.py
@@ -0,0 +1,54 @@
+from functools import cached_property
+from typing import Union
+
+from Utils import cache_self1
+from .base_logic import BaseLogic, BaseLogicMixin
+from .has_logic import HasLogicMixin
+from ..stardew_rule import StardewRule, HasProgressionPercent
+
+ONE_YEAR = 4
+MAX_MONTHS = 3 * ONE_YEAR
+PERCENT_REQUIRED_FOR_MAX_MONTHS = 48
+MONTH_COEFFICIENT = PERCENT_REQUIRED_FOR_MAX_MONTHS // MAX_MONTHS
+
+MIN_ITEMS = 10
+MAX_ITEMS = 999
+PERCENT_REQUIRED_FOR_MAX_ITEM = 24
+
+
+class TimeLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.time = TimeLogic(*args, **kwargs)
+
+
+class TimeLogic(BaseLogic[Union[TimeLogicMixin, HasLogicMixin]]):
+
+ @cache_self1
+ def has_lived_months(self, number: int) -> StardewRule:
+ assert isinstance(number, int), "Can't have lived a fraction of a month. Use // instead of / when dividing."
+ if number <= 0:
+ return self.logic.true_
+
+ number = min(number, MAX_MONTHS)
+ return HasProgressionPercent(self.player, number * MONTH_COEFFICIENT)
+
+ @cached_property
+ def has_lived_max_months(self) -> StardewRule:
+ return self.logic.time.has_lived_months(MAX_MONTHS)
+
+ @cache_self1
+ def has_lived_year(self, number: int) -> StardewRule:
+ return self.logic.time.has_lived_months(number * ONE_YEAR)
+
+ @cache_self1
+ def has_year(self, number: int) -> StardewRule:
+ return self.logic.time.has_lived_year(number - 1)
+
+ @cached_property
+ def has_year_two(self) -> StardewRule:
+ return self.logic.time.has_year(2)
+
+ @cached_property
+ def has_year_three(self) -> StardewRule:
+ return self.logic.time.has_year(3)
diff --git a/worlds/stardew_valley/logic/tool_logic.py b/worlds/stardew_valley/logic/tool_logic.py
new file mode 100644
index 000000000000..ba593c085ae4
--- /dev/null
+++ b/worlds/stardew_valley/logic/tool_logic.py
@@ -0,0 +1,104 @@
+from typing import Union, Iterable, Tuple
+
+from Utils import cache_self1
+from .base_logic import BaseLogicMixin, BaseLogic
+from .has_logic import HasLogicMixin
+from .money_logic import MoneyLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .season_logic import SeasonLogicMixin
+from ..mods.logic.magic_logic import MagicLogicMixin
+from ..options import ToolProgression
+from ..stardew_rule import StardewRule, True_, False_
+from ..strings.ap_names.skill_level_names import ModSkillLevel
+from ..strings.region_names import Region
+from ..strings.spells import MagicSpell
+from ..strings.tool_names import ToolMaterial, Tool
+
+fishing_rod_prices = {
+ 3: 1800,
+ 4: 7500,
+}
+
+tool_materials = {
+ ToolMaterial.copper: 1,
+ ToolMaterial.iron: 2,
+ ToolMaterial.gold: 3,
+ ToolMaterial.iridium: 4
+}
+
+tool_upgrade_prices = {
+ ToolMaterial.copper: 2000,
+ ToolMaterial.iron: 5000,
+ ToolMaterial.gold: 10000,
+ ToolMaterial.iridium: 25000
+}
+
+
+class ToolLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.tool = ToolLogic(*args, **kwargs)
+
+
+class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, MoneyLogicMixin, MagicLogicMixin]]):
+
+ def has_all_tools(self, tools: Iterable[Tuple[str, str]]):
+ return self.logic.and_(*(self.logic.tool.has_tool(tool, material) for tool, material in tools))
+
+ # Should be cached
+ def has_tool(self, tool: str, material: str = ToolMaterial.basic) -> StardewRule:
+ if tool == Tool.fishing_rod:
+ return self.logic.tool.has_fishing_rod(tool_materials[material])
+
+ if tool == Tool.pan and material == ToolMaterial.basic:
+ material = ToolMaterial.copper # The first Pan is the copper one, so the basic one does not exist
+
+ if material == ToolMaterial.basic or tool == Tool.scythe:
+ return True_()
+
+ if self.options.tool_progression & ToolProgression.option_progressive:
+ return self.logic.received(f"Progressive {tool}", tool_materials[material])
+
+ can_upgrade_rule = self.logic.has(f"{material} Bar") & self.logic.money.can_spend_at(Region.blacksmith, tool_upgrade_prices[material])
+ if tool == Tool.pan:
+ has_base_pan = self.logic.received("Glittering Boulder Removed") & self.logic.region.can_reach(Region.mountain)
+ if material == ToolMaterial.copper:
+ return has_base_pan
+ return has_base_pan & can_upgrade_rule
+
+ return can_upgrade_rule
+
+ def can_use_tool_at(self, tool: str, material: str, region: str) -> StardewRule:
+ return self.has_tool(tool, material) & self.logic.region.can_reach(region)
+
+ @cache_self1
+ def has_fishing_rod(self, level: int) -> StardewRule:
+ assert 1 <= level <= 4, "Fishing rod 0 isn't real, it can't hurt you. Training is 1, Bamboo is 2, Fiberglass is 3 and Iridium is 4."
+
+ if self.options.tool_progression & ToolProgression.option_progressive:
+ return self.logic.received(f"Progressive {Tool.fishing_rod}", level)
+
+ if level <= 2:
+ # We assume you always have access to the Bamboo pole, because mod side there is a builtin way to get it back.
+ return self.logic.region.can_reach(Region.beach)
+
+ return self.logic.money.can_spend_at(Region.fish_shop, fishing_rod_prices[level])
+
+ # Should be cached
+ def can_forage(self, season: Union[str, Iterable[str]], region: str = Region.forest, need_hoe: bool = False) -> StardewRule:
+ season_rule = False_()
+ if isinstance(season, str):
+ season_rule = self.logic.season.has(season)
+ elif isinstance(season, Iterable):
+ season_rule = self.logic.season.has_any(season)
+ region_rule = self.logic.region.can_reach(region)
+ if need_hoe:
+ return season_rule & region_rule & self.logic.tool.has_tool(Tool.hoe)
+ return season_rule & region_rule
+
+ @cache_self1
+ def can_water(self, level: int) -> StardewRule:
+ tool_rule = self.logic.tool.has_tool(Tool.watering_can, ToolMaterial.tiers[level])
+ spell_rule = self.logic.received(MagicSpell.water) & self.logic.magic.can_use_altar() & self.logic.received(ModSkillLevel.magic_level, level)
+ return tool_rule | spell_rule
diff --git a/worlds/stardew_valley/logic/traveling_merchant_logic.py b/worlds/stardew_valley/logic/traveling_merchant_logic.py
new file mode 100644
index 000000000000..4123ded5bf24
--- /dev/null
+++ b/worlds/stardew_valley/logic/traveling_merchant_logic.py
@@ -0,0 +1,26 @@
+from typing import Union
+
+from .base_logic import BaseLogic, BaseLogicMixin
+from .received_logic import ReceivedLogicMixin
+from ..stardew_rule import True_
+from ..strings.calendar_names import Weekday
+
+
+class TravelingMerchantLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.traveling_merchant = TravelingMerchantLogic(*args, **kwargs)
+
+
+class TravelingMerchantLogic(BaseLogic[Union[TravelingMerchantLogicMixin, ReceivedLogicMixin]]):
+
+ def has_days(self, number_days: int = 1):
+ if number_days <= 0:
+ return True_()
+
+ traveling_merchant_days = tuple(f"Traveling Merchant: {day}" for day in Weekday.all_days)
+ if number_days == 1:
+ return self.logic.received_any(*traveling_merchant_days)
+
+ tier = min(7, max(1, number_days))
+ return self.logic.received_n(*traveling_merchant_days, count=tier)
diff --git a/worlds/stardew_valley/logic/wallet_logic.py b/worlds/stardew_valley/logic/wallet_logic.py
new file mode 100644
index 000000000000..3a6d12640028
--- /dev/null
+++ b/worlds/stardew_valley/logic/wallet_logic.py
@@ -0,0 +1,19 @@
+from .base_logic import BaseLogic, BaseLogicMixin
+from .received_logic import ReceivedLogicMixin
+from ..stardew_rule import StardewRule
+from ..strings.wallet_item_names import Wallet
+
+
+class WalletLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.wallet = WalletLogic(*args, **kwargs)
+
+
+class WalletLogic(BaseLogic[ReceivedLogicMixin]):
+
+ def can_speak_dwarf(self) -> StardewRule:
+ return self.logic.received(Wallet.dwarvish_translation_guide)
+
+ def has_rusty_key(self) -> StardewRule:
+ return self.logic.received(Wallet.rusty_key)
diff --git a/worlds/stardew_valley/logic/walnut_logic.py b/worlds/stardew_valley/logic/walnut_logic.py
new file mode 100644
index 000000000000..14fe1c339090
--- /dev/null
+++ b/worlds/stardew_valley/logic/walnut_logic.py
@@ -0,0 +1,135 @@
+from functools import cached_property
+from typing import Union
+
+from .ability_logic import AbilityLogicMixin
+from .base_logic import BaseLogic, BaseLogicMixin
+from .combat_logic import CombatLogicMixin
+from .has_logic import HasLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from ..strings.ap_names.event_names import Event
+from ..options import ExcludeGingerIsland, Walnutsanity
+from ..stardew_rule import StardewRule, False_, True_
+from ..strings.ap_names.ap_option_names import OptionName
+from ..strings.craftable_names import Furniture
+from ..strings.crop_names import Fruit
+from ..strings.metal_names import Mineral, Fossil
+from ..strings.region_names import Region
+from ..strings.seed_names import Seed
+
+
+class WalnutLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.walnut = WalnutLogic(*args, **kwargs)
+
+
+class WalnutLogic(BaseLogic[Union[WalnutLogicMixin, ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, CombatLogicMixin,
+ AbilityLogicMixin]]):
+
+ def has_walnut(self, number: int) -> StardewRule:
+ if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true:
+ return False_()
+ if number <= 0:
+ return True_()
+
+ if self.options.walnutsanity == Walnutsanity.preset_none:
+ return self.can_get_walnuts(number)
+ if self.options.walnutsanity == Walnutsanity.preset_all:
+ return self.has_received_walnuts(number)
+ puzzle_walnuts = 61
+ bush_walnuts = 25
+ dig_walnuts = 18
+ repeatable_walnuts = 33
+ total_walnuts = puzzle_walnuts + bush_walnuts + dig_walnuts + repeatable_walnuts
+ walnuts_to_receive = 0
+ walnuts_to_collect = number
+ if OptionName.walnutsanity_puzzles in self.options.walnutsanity:
+ puzzle_walnut_rate = puzzle_walnuts / total_walnuts
+ puzzle_walnuts_required = round(puzzle_walnut_rate * number)
+ walnuts_to_receive += puzzle_walnuts_required
+ walnuts_to_collect -= puzzle_walnuts_required
+ if OptionName.walnutsanity_bushes in self.options.walnutsanity:
+ bush_walnuts_rate = bush_walnuts / total_walnuts
+ bush_walnuts_required = round(bush_walnuts_rate * number)
+ walnuts_to_receive += bush_walnuts_required
+ walnuts_to_collect -= bush_walnuts_required
+ if OptionName.walnutsanity_dig_spots in self.options.walnutsanity:
+ dig_walnuts_rate = dig_walnuts / total_walnuts
+ dig_walnuts_required = round(dig_walnuts_rate * number)
+ walnuts_to_receive += dig_walnuts_required
+ walnuts_to_collect -= dig_walnuts_required
+ if OptionName.walnutsanity_repeatables in self.options.walnutsanity:
+ repeatable_walnuts_rate = repeatable_walnuts / total_walnuts
+ repeatable_walnuts_required = round(repeatable_walnuts_rate * number)
+ walnuts_to_receive += repeatable_walnuts_required
+ walnuts_to_collect -= repeatable_walnuts_required
+ return self.has_received_walnuts(walnuts_to_receive) & self.can_get_walnuts(walnuts_to_collect)
+
+ def has_received_walnuts(self, number: int) -> StardewRule:
+ return self.logic.received(Event.received_walnuts, number)
+
+ def can_get_walnuts(self, number: int) -> StardewRule:
+ # https://stardewcommunitywiki.com/Golden_Walnut#Walnut_Locations
+ reach_south = self.logic.region.can_reach(Region.island_south)
+ reach_north = self.logic.region.can_reach(Region.island_north)
+ reach_west = self.logic.region.can_reach(Region.island_west)
+ reach_hut = self.logic.region.can_reach(Region.leo_hut)
+ reach_southeast = self.logic.region.can_reach(Region.island_south_east)
+ reach_field_office = self.logic.region.can_reach(Region.field_office)
+ reach_pirate_cove = self.logic.region.can_reach(Region.pirate_cove)
+ reach_outside_areas = self.logic.and_(reach_south, reach_north, reach_west, reach_hut)
+ reach_volcano_regions = [self.logic.region.can_reach(Region.volcano),
+ self.logic.region.can_reach(Region.volcano_secret_beach),
+ self.logic.region.can_reach(Region.volcano_floor_5),
+ self.logic.region.can_reach(Region.volcano_floor_10)]
+ reach_volcano = self.logic.or_(*reach_volcano_regions)
+ reach_all_volcano = self.logic.and_(*reach_volcano_regions)
+ reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office]
+ reach_caves = self.logic.and_(self.logic.region.can_reach(Region.qi_walnut_room), self.logic.region.can_reach(Region.dig_site),
+ self.logic.region.can_reach(Region.gourmand_frog_cave),
+ self.logic.region.can_reach(Region.colored_crystals_cave),
+ self.logic.region.can_reach(Region.shipwreck), self.logic.combat.has_slingshot)
+ reach_entire_island = self.logic.and_(reach_outside_areas, reach_all_volcano,
+ reach_caves, reach_southeast, reach_field_office, reach_pirate_cove)
+ if number <= 5:
+ return self.logic.or_(reach_south, reach_north, reach_west, reach_volcano)
+ if number <= 10:
+ return self.logic.count(2, *reach_walnut_regions)
+ if number <= 15:
+ return self.logic.count(3, *reach_walnut_regions)
+ if number <= 20:
+ return self.logic.and_(*reach_walnut_regions)
+ if number <= 50:
+ return reach_entire_island
+ gems = (Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz)
+ return reach_entire_island & self.logic.has(Fruit.banana) & self.logic.has_all(*gems) & \
+ self.logic.ability.can_mine_perfectly() & self.logic.ability.can_fish_perfectly() & \
+ self.logic.has(Furniture.flute_block) & self.logic.has(Seed.melon) & self.logic.has(Seed.wheat) & \
+ self.logic.has(Seed.garlic) & self.can_complete_field_office()
+
+ @cached_property
+ def can_start_field_office(self) -> StardewRule:
+ field_office = self.logic.region.can_reach(Region.field_office)
+ professor_snail = self.logic.received("Open Professor Snail Cave")
+ return field_office & professor_snail
+
+ def can_complete_large_animal_collection(self) -> StardewRule:
+ fossils = self.logic.has_all(Fossil.fossilized_leg, Fossil.fossilized_ribs, Fossil.fossilized_skull, Fossil.fossilized_spine, Fossil.fossilized_tail)
+ return self.can_start_field_office & fossils
+
+ def can_complete_snake_collection(self) -> StardewRule:
+ fossils = self.logic.has_all(Fossil.snake_skull, Fossil.snake_vertebrae)
+ return self.can_start_field_office & fossils
+
+ def can_complete_frog_collection(self) -> StardewRule:
+ fossils = self.logic.has_all(Fossil.mummified_frog)
+ return self.can_start_field_office & fossils
+
+ def can_complete_bat_collection(self) -> StardewRule:
+ fossils = self.logic.has_all(Fossil.mummified_bat)
+ return self.can_start_field_office & fossils
+
+ def can_complete_field_office(self) -> StardewRule:
+ return self.can_complete_large_animal_collection() & self.can_complete_snake_collection() & \
+ self.can_complete_frog_collection() & self.can_complete_bat_collection()
diff --git a/worlds/stardew_valley/mods/logic/buildings.py b/worlds/stardew_valley/mods/logic/buildings.py
deleted file mode 100644
index 5ca4bf32d785..000000000000
--- a/worlds/stardew_valley/mods/logic/buildings.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from typing import Union
-
-from ...strings.artisan_good_names import ArtisanGood
-from ...strings.building_names import ModBuilding
-from ..mod_data import ModNames
-from ...strings.metal_names import MetalBar
-from ...strings.region_names import Region
-
-
-def get_modded_building_rules(vanilla_logic, active_mods):
- buildings = {}
- if ModNames.tractor in active_mods:
- buildings.update({
- ModBuilding.tractor_garage: vanilla_logic.can_spend_money_at(Region.carpenter, 150000) & vanilla_logic.has(MetalBar.iron) &
- vanilla_logic.has(MetalBar.iridium) & vanilla_logic.has(ArtisanGood.battery_pack)})
- return buildings
diff --git a/worlds/stardew_valley/mods/logic/buildings_logic.py b/worlds/stardew_valley/mods/logic/buildings_logic.py
new file mode 100644
index 000000000000..388204a47614
--- /dev/null
+++ b/worlds/stardew_valley/mods/logic/buildings_logic.py
@@ -0,0 +1,28 @@
+from typing import Dict, Union
+
+from ..mod_data import ModNames
+from ...logic.base_logic import BaseLogicMixin, BaseLogic
+from ...logic.has_logic import HasLogicMixin
+from ...logic.money_logic import MoneyLogicMixin
+from ...stardew_rule import StardewRule
+from ...strings.artisan_good_names import ArtisanGood
+from ...strings.building_names import ModBuilding
+from ...strings.metal_names import MetalBar
+from ...strings.region_names import Region
+
+
+class ModBuildingLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.building = ModBuildingLogic(*args, **kwargs)
+
+
+class ModBuildingLogic(BaseLogic[Union[MoneyLogicMixin, HasLogicMixin]]):
+
+ def get_modded_building_rules(self) -> Dict[str, StardewRule]:
+ buildings = dict()
+ if ModNames.tractor in self.options.mods:
+ tractor_rule = (self.logic.money.can_spend_at(Region.carpenter, 150000) &
+ self.logic.has_all(MetalBar.iron, MetalBar.iridium, ArtisanGood.battery_pack))
+ buildings.update({ModBuilding.tractor_garage: tractor_rule})
+ return buildings
diff --git a/worlds/stardew_valley/mods/logic/deepwoods.py b/worlds/stardew_valley/mods/logic/deepwoods.py
deleted file mode 100644
index f6ecd6d82806..000000000000
--- a/worlds/stardew_valley/mods/logic/deepwoods.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from ...strings.craftable_names import Craftable
-from ...strings.performance_names import Performance
-from ...strings.skill_names import Skill
-from ...strings.tool_names import Tool, ToolMaterial
-from ...strings.ap_names.transport_names import ModTransportation
-from ...stardew_rule import StardewRule, True_, And
-from ... import options
-
-
-def can_reach_woods_depth(vanilla_logic, depth: int) -> StardewRule:
- tier = int(depth / 25) + 1
- rules = []
- if depth > 10:
- rules.append(vanilla_logic.has(Craftable.bomb) | vanilla_logic.has_tool(Tool.axe, ToolMaterial.iridium))
- if depth > 30:
- rules.append(vanilla_logic.received(ModTransportation.woods_obelisk))
- if depth > 50:
- rules.append(vanilla_logic.can_do_combat_at_level(Performance.great) & vanilla_logic.can_cook() &
- vanilla_logic.received(ModTransportation.woods_obelisk))
- if vanilla_logic.options[options.SkillProgression] == options.SkillProgression.option_progressive:
- combat_tier = min(10, max(0, tier + 5))
- rules.append(vanilla_logic.has_skill_level(Skill.combat, combat_tier))
- return And(rules)
-
-
-def has_woods_rune_to_depth(vanilla_logic, floor: int) -> StardewRule:
- if vanilla_logic.options[options.ElevatorProgression] == options.ElevatorProgression.option_vanilla:
- return True_()
- return vanilla_logic.received("Progressive Woods Obelisk Sigils", count=int(floor / 10))
-
-
-def can_chop_to_depth(vanilla_logic, floor: int) -> StardewRule:
- previous_elevator = max(floor - 10, 0)
- return (has_woods_rune_to_depth(vanilla_logic, previous_elevator) &
- can_reach_woods_depth(vanilla_logic, previous_elevator))
diff --git a/worlds/stardew_valley/mods/logic/deepwoods_logic.py b/worlds/stardew_valley/mods/logic/deepwoods_logic.py
new file mode 100644
index 000000000000..26704eb7d11b
--- /dev/null
+++ b/worlds/stardew_valley/mods/logic/deepwoods_logic.py
@@ -0,0 +1,73 @@
+from typing import Union
+
+from ... import options
+from ...logic.base_logic import BaseLogicMixin, BaseLogic
+from ...logic.combat_logic import CombatLogicMixin
+from ...logic.cooking_logic import CookingLogicMixin
+from ...logic.has_logic import HasLogicMixin
+from ...logic.received_logic import ReceivedLogicMixin
+from ...logic.skill_logic import SkillLogicMixin
+from ...logic.tool_logic import ToolLogicMixin
+from ...mods.mod_data import ModNames
+from ...options import ElevatorProgression
+from ...stardew_rule import StardewRule, True_, true_
+from ...strings.ap_names.mods.mod_items import DeepWoodsItem
+from ...strings.ap_names.transport_names import ModTransportation
+from ...strings.craftable_names import Bomb
+from ...strings.food_names import Meal
+from ...strings.performance_names import Performance
+from ...strings.skill_names import Skill, ModSkill
+from ...strings.tool_names import Tool, ToolMaterial
+
+
+class DeepWoodsLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.deepwoods = DeepWoodsLogic(*args, **kwargs)
+
+
+class DeepWoodsLogic(BaseLogic[Union[SkillLogicMixin, ReceivedLogicMixin, HasLogicMixin, CombatLogicMixin, ToolLogicMixin, SkillLogicMixin,
+CookingLogicMixin]]):
+
+ def can_reach_woods_depth(self, depth: int) -> StardewRule:
+ # Assuming you can always do the 10 first floor
+ if depth <= 10:
+ return true_
+
+ rules = []
+
+ if depth > 10:
+ rules.append(self.logic.has(Bomb.bomb) | self.logic.tool.has_tool(Tool.axe, ToolMaterial.iridium))
+ if depth > 30:
+ rules.append(self.logic.received(ModTransportation.woods_obelisk))
+ if depth > 50:
+ rules.append(self.logic.combat.can_fight_at_level(Performance.great) & self.logic.cooking.can_cook() &
+ self.logic.received(ModTransportation.woods_obelisk))
+
+ tier = int(depth / 25) + 1
+ if self.options.skill_progression >= options.SkillProgression.option_progressive:
+ combat_tier = min(10, max(0, tier + 5))
+ rules.append(self.logic.skill.has_level(Skill.combat, combat_tier))
+
+ return self.logic.and_(*rules)
+
+ def has_woods_rune_to_depth(self, floor: int) -> StardewRule:
+ if self.options.elevator_progression == ElevatorProgression.option_vanilla:
+ return True_()
+ return self.logic.received(DeepWoodsItem.obelisk_sigil, int(floor / 10))
+
+ def can_chop_to_depth(self, floor: int) -> StardewRule:
+ previous_elevator = max(floor - 10, 0)
+ return (self.has_woods_rune_to_depth(previous_elevator) &
+ self.can_reach_woods_depth(previous_elevator))
+
+ def can_pull_sword(self) -> StardewRule:
+ rules = [self.logic.received(DeepWoodsItem.pendant_depths) & self.logic.received(DeepWoodsItem.pendant_community) &
+ self.logic.received(DeepWoodsItem.pendant_elder),
+ self.logic.skill.has_total_level(40)]
+ if ModNames.luck_skill in self.options.mods:
+ rules.append(self.logic.skill.has_level(ModSkill.luck, 7))
+ else:
+ rules.append(
+ self.logic.has(Meal.magic_rock_candy)) # You need more luck than this, but it'll push the logic down a ways; you can get the rest there.
+ return self.logic.and_(*rules)
diff --git a/worlds/stardew_valley/mods/logic/elevator_logic.py b/worlds/stardew_valley/mods/logic/elevator_logic.py
new file mode 100644
index 000000000000..f1d12bcb1c37
--- /dev/null
+++ b/worlds/stardew_valley/mods/logic/elevator_logic.py
@@ -0,0 +1,18 @@
+from ...logic.base_logic import BaseLogicMixin, BaseLogic
+from ...logic.received_logic import ReceivedLogicMixin
+from ...mods.mod_data import ModNames
+from ...options import ElevatorProgression
+from ...stardew_rule import StardewRule, True_
+
+
+class ModElevatorLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.elevator = ModElevatorLogic(*args, **kwargs)
+
+
+class ModElevatorLogic(BaseLogic[ReceivedLogicMixin]):
+ def has_skull_cavern_elevator_to_floor(self, floor: int) -> StardewRule:
+ if self.options.elevator_progression != ElevatorProgression.option_vanilla and ModNames.skull_cavern_elevator in self.options.mods:
+ return self.logic.received("Progressive Skull Cavern Elevator", floor // 25)
+ return True_()
diff --git a/worlds/stardew_valley/mods/logic/item_logic.py b/worlds/stardew_valley/mods/logic/item_logic.py
new file mode 100644
index 000000000000..ef5eab0134d1
--- /dev/null
+++ b/worlds/stardew_valley/mods/logic/item_logic.py
@@ -0,0 +1,153 @@
+from typing import Dict, Union
+
+from ..mod_data import ModNames
+from ... import options
+from ...data.craftable_data import all_crafting_recipes_by_name
+from ...logic.base_logic import BaseLogicMixin, BaseLogic
+from ...logic.combat_logic import CombatLogicMixin
+from ...logic.cooking_logic import CookingLogicMixin
+from ...logic.crafting_logic import CraftingLogicMixin
+from ...logic.farming_logic import FarmingLogicMixin
+from ...logic.fishing_logic import FishingLogicMixin
+from ...logic.has_logic import HasLogicMixin
+from ...logic.money_logic import MoneyLogicMixin
+from ...logic.museum_logic import MuseumLogicMixin
+from ...logic.quest_logic import QuestLogicMixin
+from ...logic.received_logic import ReceivedLogicMixin
+from ...logic.region_logic import RegionLogicMixin
+from ...logic.relationship_logic import RelationshipLogicMixin
+from ...logic.season_logic import SeasonLogicMixin
+from ...logic.skill_logic import SkillLogicMixin
+from ...logic.time_logic import TimeLogicMixin
+from ...logic.tool_logic import ToolLogicMixin
+from ...options import Cropsanity
+from ...stardew_rule import StardewRule, True_
+from ...strings.artisan_good_names import ModArtisanGood
+from ...strings.craftable_names import ModCraftable, ModMachine
+from ...strings.fish_names import ModTrash
+from ...strings.ingredient_names import Ingredient
+from ...strings.material_names import Material
+from ...strings.metal_names import all_fossils, all_artifacts, Ore, ModFossil
+from ...strings.monster_drop_names import Loot
+from ...strings.performance_names import Performance
+from ...strings.region_names import SVERegion, DeepWoodsRegion, BoardingHouseRegion
+from ...strings.tool_names import Tool, ToolMaterial
+
+display_types = [ModCraftable.wooden_display, ModCraftable.hardwood_display]
+display_items = all_artifacts + all_fossils
+
+
+class ModItemLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.item = ModItemLogic(*args, **kwargs)
+
+
+class ModItemLogic(BaseLogic[Union[CombatLogicMixin, ReceivedLogicMixin, CookingLogicMixin, FishingLogicMixin, HasLogicMixin, MoneyLogicMixin,
+RegionLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, ToolLogicMixin, CraftingLogicMixin, SkillLogicMixin, TimeLogicMixin, QuestLogicMixin,
+FarmingLogicMixin]]):
+
+ def get_modded_item_rules(self) -> Dict[str, StardewRule]:
+ items = dict()
+ if ModNames.boarding_house in self.options.mods:
+ items.update(self.get_boarding_house_item_rules())
+ return items
+
+ def modify_vanilla_item_rules_with_mod_additions(self, item_rule: Dict[str, StardewRule]):
+ if ModNames.sve in self.options.mods:
+ item_rule.update(self.get_modified_item_rules_for_sve(item_rule))
+ if ModNames.deepwoods in self.options.mods:
+ item_rule.update(self.get_modified_item_rules_for_deep_woods(item_rule))
+ return item_rule
+
+ def get_modified_item_rules_for_sve(self, items: Dict[str, StardewRule]):
+ return {
+ Loot.void_essence: items[Loot.void_essence] | self.logic.region.can_reach(SVERegion.highlands_cavern) | self.logic.region.can_reach(
+ SVERegion.crimson_badlands),
+ Loot.solar_essence: items[Loot.solar_essence] | self.logic.region.can_reach(SVERegion.crimson_badlands),
+ Ore.copper: items[Ore.copper] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.highlands_cavern) &
+ self.logic.combat.can_fight_at_level(Performance.great)),
+ Ore.iron: items[Ore.iron] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.highlands_cavern) &
+ self.logic.combat.can_fight_at_level(Performance.great)),
+ Ore.iridium: items[Ore.iridium] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.crimson_badlands) &
+ self.logic.combat.can_fight_at_level(Performance.maximum)),
+
+ }
+
+ def get_modified_item_rules_for_deep_woods(self, items: Dict[str, StardewRule]):
+ options_to_update = {
+ Material.hardwood: items[Material.hardwood] | self.logic.tool.can_use_tool_at(Tool.axe, ToolMaterial.iron, DeepWoodsRegion.floor_10),
+ Ingredient.sugar: items[Ingredient.sugar] | self.logic.tool.can_use_tool_at(Tool.axe, ToolMaterial.gold, DeepWoodsRegion.floor_50),
+ # Gingerbread House
+ Ingredient.wheat_flour: items[Ingredient.wheat_flour] | self.logic.tool.can_use_tool_at(Tool.axe, ToolMaterial.gold, DeepWoodsRegion.floor_50),
+ # Gingerbread House
+ }
+
+ if self.options.tool_progression & options.ToolProgression.option_progressive:
+ options_to_update.update({
+ Ore.iridium: items[Ore.iridium] | self.logic.tool.can_use_tool_at(Tool.axe, ToolMaterial.iridium, DeepWoodsRegion.floor_50), # Iridium Tree
+ })
+
+ return options_to_update
+
+ def get_boarding_house_item_rules(self):
+ return {
+ # Mob Drops from lost valley enemies
+ ModArtisanGood.pterodactyl_egg: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.pterodactyl_claw: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.pterodactyl_ribs: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.pterodactyl_vertebra: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.pterodactyl_skull: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.pterodactyl_phalange: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.pterodactyl_l_wing_bone: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.pterodactyl_r_wing_bone: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.dinosaur_skull: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.dinosaur_tooth: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.dinosaur_femur: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.dinosaur_pelvis: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.dinosaur_ribs: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.dinosaur_vertebra: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.dinosaur_claw: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.good),
+ ModFossil.neanderthal_skull: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.great),
+ ModFossil.neanderthal_ribs: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.great),
+ ModFossil.neanderthal_pelvis: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.great),
+ ModFossil.neanderthal_limb_bones: self.logic.region.can_reach_any((BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1,
+ BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level(
+ Performance.great),
+ }
diff --git a/worlds/stardew_valley/mods/logic/magic.py b/worlds/stardew_valley/mods/logic/magic.py
deleted file mode 100644
index a084c6aa9199..000000000000
--- a/worlds/stardew_valley/mods/logic/magic.py
+++ /dev/null
@@ -1,80 +0,0 @@
-from ...strings.region_names import MagicRegion
-from ...mods.mod_data import ModNames
-from ...strings.spells import MagicSpell
-from ...strings.ap_names.skill_level_names import ModSkillLevel
-from ...stardew_rule import Count, StardewRule, False_
-from ... import options
-
-
-def can_use_clear_debris_instead_of_tool_level(vanilla_logic, level: int) -> StardewRule:
- if ModNames.magic not in vanilla_logic.options[options.Mods]:
- return False_()
- return vanilla_logic.received(MagicSpell.clear_debris) & can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, level)
-
-
-def can_use_altar(vanilla_logic) -> StardewRule:
- if ModNames.magic not in vanilla_logic.options[options.Mods]:
- return False_()
- return vanilla_logic.can_reach_region(MagicRegion.altar)
-
-
-def has_any_spell(vanilla_logic) -> StardewRule:
- if ModNames.magic not in vanilla_logic.options[options.Mods]:
- return False_()
- return can_use_altar(vanilla_logic)
-
-
-def has_attack_spell_count(vanilla_logic, count: int) -> StardewRule:
- attack_spell_rule = [vanilla_logic.received(MagicSpell.fireball), vanilla_logic.received(
- MagicSpell.frostbite), vanilla_logic.received(MagicSpell.shockwave), vanilla_logic.received(MagicSpell.spirit),
- vanilla_logic.received(MagicSpell.meteor)
- ]
- return Count(count, attack_spell_rule)
-
-
-def has_support_spell_count(vanilla_logic, count: int) -> StardewRule:
- support_spell_rule = [can_use_altar(vanilla_logic), vanilla_logic.received(ModSkillLevel.magic_level, 2),
- vanilla_logic.received(MagicSpell.descend), vanilla_logic.received(MagicSpell.heal),
- vanilla_logic.received(MagicSpell.tendrils)]
- return Count(count, support_spell_rule)
-
-
-def has_decent_spells(vanilla_logic) -> StardewRule:
- if ModNames.magic not in vanilla_logic.options[options.Mods]:
- return False_()
- magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 2)
- magic_attack_options_rule = has_attack_spell_count(vanilla_logic, 1)
- return magic_resource_rule & magic_attack_options_rule
-
-
-def has_good_spells(vanilla_logic) -> StardewRule:
- if ModNames.magic not in vanilla_logic.options[options.Mods]:
- return False_()
- magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 4)
- magic_attack_options_rule = has_attack_spell_count(vanilla_logic, 2)
- magic_support_options_rule = has_support_spell_count(vanilla_logic, 1)
- return magic_resource_rule & magic_attack_options_rule & magic_support_options_rule
-
-
-def has_great_spells(vanilla_logic) -> StardewRule:
- if ModNames.magic not in vanilla_logic.options[options.Mods]:
- return False_()
- magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 6)
- magic_attack_options_rule = has_attack_spell_count(vanilla_logic, 3)
- magic_support_options_rule = has_support_spell_count(vanilla_logic, 1)
- return magic_resource_rule & magic_attack_options_rule & magic_support_options_rule
-
-
-def has_amazing_spells(vanilla_logic) -> StardewRule:
- if ModNames.magic not in vanilla_logic.options[options.Mods]:
- return False_()
- magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 8)
- magic_attack_options_rule = has_attack_spell_count(vanilla_logic, 4)
- magic_support_options_rule = has_support_spell_count(vanilla_logic, 2)
- return magic_resource_rule & magic_attack_options_rule & magic_support_options_rule
-
-
-def can_blink(vanilla_logic) -> StardewRule:
- if ModNames.magic not in vanilla_logic.options[options.Mods]:
- return False_()
- return vanilla_logic.received(MagicSpell.blink) & can_use_altar(vanilla_logic)
diff --git a/worlds/stardew_valley/mods/logic/magic_logic.py b/worlds/stardew_valley/mods/logic/magic_logic.py
new file mode 100644
index 000000000000..662ff3acaeb6
--- /dev/null
+++ b/worlds/stardew_valley/mods/logic/magic_logic.py
@@ -0,0 +1,83 @@
+from typing import Union
+
+from ...logic.base_logic import BaseLogicMixin, BaseLogic
+from ...logic.has_logic import HasLogicMixin
+from ...logic.received_logic import ReceivedLogicMixin
+from ...logic.region_logic import RegionLogicMixin
+from ...mods.mod_data import ModNames
+from ...stardew_rule import StardewRule, False_
+from ...strings.ap_names.skill_level_names import ModSkillLevel
+from ...strings.region_names import MagicRegion
+from ...strings.spells import MagicSpell, all_spells
+
+
+class MagicLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.magic = MagicLogic(*args, **kwargs)
+
+
+# TODO add logic.mods.magic for altar
+class MagicLogic(BaseLogic[Union[RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]):
+ def can_use_clear_debris_instead_of_tool_level(self, level: int) -> StardewRule:
+ if ModNames.magic not in self.options.mods:
+ return False_()
+ return self.logic.received(MagicSpell.clear_debris) & self.can_use_altar() & self.logic.received(ModSkillLevel.magic_level, level)
+
+ def can_use_altar(self) -> StardewRule:
+ if ModNames.magic not in self.options.mods:
+ return False_()
+ spell_rule = False_()
+ return self.logic.region.can_reach(MagicRegion.altar) & self.logic.received_any(*all_spells)
+
+ def has_any_spell(self) -> StardewRule:
+ if ModNames.magic not in self.options.mods:
+ return False_()
+ return self.can_use_altar()
+
+ def has_attack_spell_count(self, count: int) -> StardewRule:
+ attack_spell_rule = [self.logic.received(MagicSpell.fireball), self.logic.received(MagicSpell.frostbite), self.logic.received(MagicSpell.shockwave),
+ self.logic.received(MagicSpell.spirit), self.logic.received(MagicSpell.meteor)]
+ return self.logic.count(count, *attack_spell_rule)
+
+ def has_support_spell_count(self, count: int) -> StardewRule:
+ support_spell_rule = [self.can_use_altar(), self.logic.received(ModSkillLevel.magic_level, 2),
+ self.logic.received(MagicSpell.descend), self.logic.received(MagicSpell.heal),
+ self.logic.received(MagicSpell.tendrils)]
+ return self.logic.count(count, *support_spell_rule)
+
+ def has_decent_spells(self) -> StardewRule:
+ if ModNames.magic not in self.options.mods:
+ return False_()
+ magic_resource_rule = self.can_use_altar() & self.logic.received(ModSkillLevel.magic_level, 2)
+ magic_attack_options_rule = self.has_attack_spell_count(1)
+ return magic_resource_rule & magic_attack_options_rule
+
+ def has_good_spells(self) -> StardewRule:
+ if ModNames.magic not in self.options.mods:
+ return False_()
+ magic_resource_rule = self.can_use_altar() & self.logic.received(ModSkillLevel.magic_level, 4)
+ magic_attack_options_rule = self.has_attack_spell_count(2)
+ magic_support_options_rule = self.has_support_spell_count(1)
+ return magic_resource_rule & magic_attack_options_rule & magic_support_options_rule
+
+ def has_great_spells(self) -> StardewRule:
+ if ModNames.magic not in self.options.mods:
+ return False_()
+ magic_resource_rule = self.can_use_altar() & self.logic.received(ModSkillLevel.magic_level, 6)
+ magic_attack_options_rule = self.has_attack_spell_count(3)
+ magic_support_options_rule = self.has_support_spell_count(1)
+ return magic_resource_rule & magic_attack_options_rule & magic_support_options_rule
+
+ def has_amazing_spells(self) -> StardewRule:
+ if ModNames.magic not in self.options.mods:
+ return False_()
+ magic_resource_rule = self.can_use_altar() & self.logic.received(ModSkillLevel.magic_level, 8)
+ magic_attack_options_rule = self.has_attack_spell_count(4)
+ magic_support_options_rule = self.has_support_spell_count(2)
+ return magic_resource_rule & magic_attack_options_rule & magic_support_options_rule
+
+ def can_blink(self) -> StardewRule:
+ if ModNames.magic not in self.options.mods:
+ return False_()
+ return self.logic.received(MagicSpell.blink) & self.can_use_altar()
diff --git a/worlds/stardew_valley/mods/logic/mod_logic.py b/worlds/stardew_valley/mods/logic/mod_logic.py
new file mode 100644
index 000000000000..37c17183dbb0
--- /dev/null
+++ b/worlds/stardew_valley/mods/logic/mod_logic.py
@@ -0,0 +1,21 @@
+from .buildings_logic import ModBuildingLogicMixin
+from .deepwoods_logic import DeepWoodsLogicMixin
+from .elevator_logic import ModElevatorLogicMixin
+from .item_logic import ModItemLogicMixin
+from .magic_logic import MagicLogicMixin
+from .quests_logic import ModQuestLogicMixin
+from .skills_logic import ModSkillLogicMixin
+from .special_orders_logic import ModSpecialOrderLogicMixin
+from .sve_logic import SVELogicMixin
+from ...logic.base_logic import BaseLogicMixin
+
+
+class ModLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.mod = ModLogic(*args, **kwargs)
+
+
+class ModLogic(ModElevatorLogicMixin, MagicLogicMixin, ModSkillLogicMixin, ModItemLogicMixin, ModQuestLogicMixin, ModBuildingLogicMixin,
+ ModSpecialOrderLogicMixin, DeepWoodsLogicMixin, SVELogicMixin):
+ pass
diff --git a/worlds/stardew_valley/mods/logic/mod_skills_levels.py b/worlds/stardew_valley/mods/logic/mod_skills_levels.py
new file mode 100644
index 000000000000..32b3368a8c8b
--- /dev/null
+++ b/worlds/stardew_valley/mods/logic/mod_skills_levels.py
@@ -0,0 +1,22 @@
+from typing import Tuple
+
+from ...mods.mod_data import ModNames
+from ...options import Mods
+from ...strings.ap_names.mods.mod_items import SkillLevel
+
+
+def get_mod_skill_levels(mods: Mods) -> Tuple[str]:
+ skills_items = []
+ if ModNames.luck_skill in mods:
+ skills_items.append(SkillLevel.luck)
+ if ModNames.socializing_skill in mods:
+ skills_items.append(SkillLevel.socializing)
+ if ModNames.magic in mods:
+ skills_items.append(SkillLevel.magic)
+ if ModNames.archaeology in mods:
+ skills_items.append(SkillLevel.archaeology)
+ if ModNames.binning_skill in mods:
+ skills_items.append(SkillLevel.binning)
+ if ModNames.cooking_skill in mods:
+ skills_items.append(SkillLevel.cooking)
+ return tuple(skills_items)
diff --git a/worlds/stardew_valley/mods/logic/quests.py b/worlds/stardew_valley/mods/logic/quests.py
deleted file mode 100644
index bf185754b2be..000000000000
--- a/worlds/stardew_valley/mods/logic/quests.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from typing import Union
-from ...strings.quest_names import ModQuest
-from ..mod_data import ModNames
-from ...strings.food_names import Meal, Beverage
-from ...strings.monster_drop_names import Loot
-from ...strings.villager_names import ModNPC
-from ...strings.season_names import Season
-from ...strings.region_names import Region
-
-
-def get_modded_quest_rules(vanilla_logic, active_mods):
- quests = {}
- if ModNames.juna in active_mods:
- quests.update({
- ModQuest.JunaCola: vanilla_logic.has_relationship(ModNPC.juna, 3) & vanilla_logic.has(Beverage.joja_cola),
- ModQuest.JunaSpaghetti: vanilla_logic.has_relationship(ModNPC.juna, 6) & vanilla_logic.has(Meal.spaghetti)
- })
-
- if ModNames.ginger in active_mods:
- quests.update({
- ModQuest.MrGinger: vanilla_logic.has_relationship(ModNPC.mr_ginger, 6) & vanilla_logic.has(Loot.void_essence)
- })
-
- if ModNames.ayeisha in active_mods:
- quests.update({
- ModQuest.AyeishaEnvelope: (vanilla_logic.has_season(Season.spring) | vanilla_logic.has_season(Season.fall)) &
- vanilla_logic.can_reach_region(Region.mountain),
- ModQuest.AyeishaRing: vanilla_logic.has_season(Season.winter) & vanilla_logic.can_reach_region(Region.forest)
- })
-
- return quests
diff --git a/worlds/stardew_valley/mods/logic/quests_logic.py b/worlds/stardew_valley/mods/logic/quests_logic.py
new file mode 100644
index 000000000000..1aa71404ae51
--- /dev/null
+++ b/worlds/stardew_valley/mods/logic/quests_logic.py
@@ -0,0 +1,128 @@
+from typing import Dict, Union
+
+from ..mod_data import ModNames
+from ...logic.base_logic import BaseLogic, BaseLogicMixin
+from ...logic.has_logic import HasLogicMixin
+from ...logic.quest_logic import QuestLogicMixin
+from ...logic.monster_logic import MonsterLogicMixin
+from ...logic.received_logic import ReceivedLogicMixin
+from ...logic.region_logic import RegionLogicMixin
+from ...logic.relationship_logic import RelationshipLogicMixin
+from ...logic.season_logic import SeasonLogicMixin
+from ...logic.time_logic import TimeLogicMixin
+from ...stardew_rule import StardewRule
+from ...strings.animal_product_names import AnimalProduct
+from ...strings.artisan_good_names import ArtisanGood
+from ...strings.crop_names import Fruit, SVEFruit, SVEVegetable, Vegetable
+from ...strings.fertilizer_names import Fertilizer
+from ...strings.food_names import Meal, Beverage
+from ...strings.forageable_names import SVEForage
+from ...strings.material_names import Material
+from ...strings.metal_names import Ore, MetalBar
+from ...strings.monster_drop_names import Loot, ModLoot
+from ...strings.monster_names import Monster
+from ...strings.quest_names import Quest, ModQuest
+from ...strings.region_names import Region, SVERegion, BoardingHouseRegion
+from ...strings.season_names import Season
+from ...strings.villager_names import ModNPC, NPC
+from ...strings.wallet_item_names import Wallet
+
+
+class ModQuestLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.quest = ModQuestLogic(*args, **kwargs)
+
+
+class ModQuestLogic(BaseLogic[Union[HasLogicMixin, QuestLogicMixin, ReceivedLogicMixin, RegionLogicMixin,
+ TimeLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MonsterLogicMixin]]):
+ def get_modded_quest_rules(self) -> Dict[str, StardewRule]:
+ quests = dict()
+ quests.update(self._get_juna_quest_rules())
+ quests.update(self._get_mr_ginger_quest_rules())
+ quests.update(self._get_ayeisha_quest_rules())
+ quests.update(self._get_sve_quest_rules())
+ quests.update(self._get_distant_lands_quest_rules())
+ quests.update(self._get_boarding_house_quest_rules())
+ quests.update((self._get_hat_mouse_quest_rules()))
+ return quests
+
+ def _get_juna_quest_rules(self):
+ if ModNames.juna not in self.options.mods:
+ return {}
+
+ return {
+ ModQuest.JunaCola: self.logic.relationship.has_hearts(ModNPC.juna, 3) & self.logic.has(Beverage.joja_cola),
+ ModQuest.JunaSpaghetti: self.logic.relationship.has_hearts(ModNPC.juna, 6) & self.logic.has(Meal.spaghetti)
+ }
+
+ def _get_mr_ginger_quest_rules(self):
+ if ModNames.ginger not in self.options.mods:
+ return {}
+
+ return {
+ ModQuest.MrGinger: self.logic.relationship.has_hearts(ModNPC.mr_ginger, 6) & self.logic.has(Loot.void_essence)
+ }
+
+ def _get_ayeisha_quest_rules(self):
+ if ModNames.ayeisha not in self.options.mods:
+ return {}
+
+ return {
+ ModQuest.AyeishaEnvelope: (self.logic.season.has(Season.spring) | self.logic.season.has(Season.fall)),
+ ModQuest.AyeishaRing: self.logic.season.has(Season.winter)
+ }
+
+ def _get_sve_quest_rules(self):
+ if ModNames.sve not in self.options.mods:
+ return {}
+
+ return {
+ ModQuest.RailroadBoulder: self.logic.received(Wallet.skull_key) & self.logic.has_all(*(Ore.iridium, Material.coal)) &
+ self.logic.region.can_reach(Region.blacksmith) & self.logic.region.can_reach(Region.railroad),
+ ModQuest.GrandpasShed: self.logic.has_all(*(Material.hardwood, MetalBar.iron, ArtisanGood.battery_pack, Material.stone)) &
+ self.logic.region.can_reach(SVERegion.grandpas_shed),
+ ModQuest.MarlonsBoat: self.logic.has_all(*(Loot.void_essence, Loot.solar_essence, Loot.slime, Loot.bat_wing, Loot.bug_meat)) &
+ self.logic.relationship.can_meet(ModNPC.lance) & self.logic.region.can_reach(SVERegion.guild_summit),
+ ModQuest.AuroraVineyard: self.logic.has(Fruit.starfruit) & self.logic.region.can_reach(SVERegion.aurora_vineyard),
+ ModQuest.MonsterCrops: self.logic.has_all(*(SVEVegetable.monster_mushroom, SVEFruit.slime_berry, SVEFruit.monster_fruit, SVEVegetable.void_root)),
+ ModQuest.VoidSoul: self.logic.has(ModLoot.void_soul) & self.logic.region.can_reach(Region.farm) &
+ self.logic.season.has_any_not_winter() & self.logic.region.can_reach(SVERegion.badlands_entrance) &
+ self.logic.relationship.has_hearts(NPC.krobus, 10) & self.logic.quest.can_complete_quest(ModQuest.MonsterCrops) &
+ self.logic.monster.can_kill_any((Monster.shadow_brute, Monster.shadow_shaman, Monster.shadow_sniper)),
+ }
+
+ def _get_distant_lands_quest_rules(self):
+ if ModNames.distant_lands not in self.options.mods:
+ return {}
+
+ return {
+ ModQuest.CorruptedCropsTask: self.logic.region.can_reach(Region.wizard_tower) & self.logic.has(Fertilizer.deluxe) &
+ self.logic.quest.can_complete_quest(Quest.magic_ink),
+ ModQuest.WitchOrder: self.logic.region.can_reach(Region.witch_swamp) & self.logic.has(Fertilizer.deluxe) &
+ self.logic.quest.can_complete_quest(Quest.magic_ink),
+ ModQuest.ANewPot: self.logic.region.can_reach(Region.saloon) &
+ self.logic.region.can_reach(Region.sam_house) & self.logic.region.can_reach(Region.pierre_store) &
+ self.logic.region.can_reach(Region.blacksmith) & self.logic.has(MetalBar.iron) & self.logic.relationship.has_hearts(ModNPC.goblin,
+ 6),
+ ModQuest.FancyBlanketTask: self.logic.region.can_reach(Region.haley_house) & self.logic.has(AnimalProduct.wool) &
+ self.logic.has(ArtisanGood.cloth) & self.logic.relationship.has_hearts(ModNPC.goblin, 10) &
+ self.logic.relationship.has_hearts(NPC.emily, 8) & self.logic.season.has(Season.winter)
+
+ }
+
+ def _get_boarding_house_quest_rules(self):
+ if ModNames.boarding_house not in self.options.mods:
+ return {}
+
+ return {
+ ModQuest.PumpkinSoup: self.logic.region.can_reach(BoardingHouseRegion.boarding_house_first) & self.logic.has(Vegetable.pumpkin)
+ }
+
+ def _get_hat_mouse_quest_rules(self):
+ if ModNames.lacey not in self.options.mods:
+ return {}
+
+ return {
+ ModQuest.HatMouseHat: self.logic.relationship.has_hearts(ModNPC.lacey, 2) & self.logic.time.has_lived_months(4)
+ }
diff --git a/worlds/stardew_valley/mods/logic/skills.py b/worlds/stardew_valley/mods/logic/skills.py
deleted file mode 100644
index 05b4c623e157..000000000000
--- a/worlds/stardew_valley/mods/logic/skills.py
+++ /dev/null
@@ -1,94 +0,0 @@
-from typing import List, Union
-from . import magic
-from ...strings.building_names import Building
-from ...strings.geode_names import Geode
-from ...strings.region_names import Region
-from ...strings.skill_names import ModSkill
-from ...strings.spells import MagicSpell
-from ...strings.machine_names import Machine
-from ...strings.tool_names import Tool, ToolMaterial
-from ...mods.mod_data import ModNames
-from ...data.villagers_data import all_villagers
-from ...stardew_rule import Count, StardewRule, False_
-from ... import options
-
-
-def append_mod_skill_level(skills_items: List[str], active_mods):
- if ModNames.luck_skill in active_mods:
- skills_items.append("Luck Level")
- if ModNames.socializing_skill in active_mods:
- skills_items.append("Socializing Level")
- if ModNames.magic in active_mods:
- skills_items.append("Magic Level")
- if ModNames.archaeology in active_mods:
- skills_items.append("Archaeology Level")
- if ModNames.binning_skill in active_mods:
- skills_items.append("Binning Level")
- if ModNames.cooking_skill in active_mods:
- skills_items.append("Cooking Level")
-
-
-def can_earn_mod_skill_level(logic, skill: str, level: int) -> StardewRule:
- if ModNames.luck_skill in logic.options[options.Mods] and skill == ModSkill.luck:
- return can_earn_luck_skill_level(logic, level)
- if ModNames.magic in logic.options[options.Mods] and skill == ModSkill.magic:
- return can_earn_magic_skill_level(logic, level)
- if ModNames.socializing_skill in logic.options[options.Mods] and skill == ModSkill.socializing:
- return can_earn_socializing_skill_level(logic, level)
- if ModNames.archaeology in logic.options[options.Mods] and skill == ModSkill.archaeology:
- return can_earn_archaeology_skill_level(logic, level)
- if ModNames.cooking_skill in logic.options[options.Mods] and skill == ModSkill.cooking:
- return can_earn_cooking_skill_level(logic, level)
- if ModNames.binning_skill in logic.options[options.Mods] and skill == ModSkill.binning:
- return can_earn_binning_skill_level(logic, level)
- return False_()
-
-
-def can_earn_luck_skill_level(vanilla_logic, level: int) -> StardewRule:
- if level >= 6:
- return vanilla_logic.can_fish_chests() | vanilla_logic.can_open_geode(Geode.magma)
- else:
- return vanilla_logic.can_fish_chests() | vanilla_logic.can_open_geode(Geode.geode)
-
-
-def can_earn_magic_skill_level(vanilla_logic, level: int) -> StardewRule:
- spell_count = [vanilla_logic.received(MagicSpell.clear_debris), vanilla_logic.received(MagicSpell.water),
- vanilla_logic.received(MagicSpell.blink), vanilla_logic.received(MagicSpell.fireball),
- vanilla_logic.received(MagicSpell.frostbite),
- vanilla_logic.received(MagicSpell.descend), vanilla_logic.received(MagicSpell.tendrils),
- vanilla_logic.received(MagicSpell.shockwave),
- vanilla_logic.received(MagicSpell.meteor),
- vanilla_logic.received(MagicSpell.spirit)]
- return magic.can_use_altar(vanilla_logic) & Count(level, spell_count)
-
-
-def can_earn_socializing_skill_level(vanilla_logic, level: int) -> StardewRule:
- villager_count = []
- for villager in all_villagers:
- if villager.mod_name in vanilla_logic.options[options.Mods] or villager.mod_name is None:
- villager_count.append(vanilla_logic.can_earn_relationship(villager.name, level))
- return Count(level * 2, villager_count)
-
-
-def can_earn_archaeology_skill_level(vanilla_logic, level: int) -> StardewRule:
- if level >= 6:
- return vanilla_logic.can_do_panning() | vanilla_logic.has_tool(Tool.hoe, ToolMaterial.gold)
- else:
- return vanilla_logic.can_do_panning() | vanilla_logic.has_tool(Tool.hoe, ToolMaterial.basic)
-
-
-def can_earn_cooking_skill_level(vanilla_logic, level: int) -> StardewRule:
- if level >= 6:
- return vanilla_logic.can_cook() & vanilla_logic.can_fish() & vanilla_logic.can_reach_region(Region.saloon) & \
- vanilla_logic.has_building(Building.coop) & vanilla_logic.has_building(Building.barn)
- else:
- return vanilla_logic.can_cook()
-
-
-def can_earn_binning_skill_level(vanilla_logic, level: int) -> StardewRule:
- if level >= 6:
- return vanilla_logic.can_reach_region(Region.town) & vanilla_logic.has(Machine.recycling_machine) & \
- (vanilla_logic.can_fish() | vanilla_logic.can_crab_pot())
- else:
- return vanilla_logic.can_reach_region(Region.town) | (vanilla_logic.has(Machine.recycling_machine) &
- (vanilla_logic.can_fish() | vanilla_logic.can_crab_pot()))
diff --git a/worlds/stardew_valley/mods/logic/skills_logic.py b/worlds/stardew_valley/mods/logic/skills_logic.py
new file mode 100644
index 000000000000..cb12274dc651
--- /dev/null
+++ b/worlds/stardew_valley/mods/logic/skills_logic.py
@@ -0,0 +1,116 @@
+from typing import Union
+
+from .magic_logic import MagicLogicMixin
+from ...logic.action_logic import ActionLogicMixin
+from ...logic.base_logic import BaseLogicMixin, BaseLogic
+from ...logic.building_logic import BuildingLogicMixin
+from ...logic.cooking_logic import CookingLogicMixin
+from ...logic.crafting_logic import CraftingLogicMixin
+from ...logic.fishing_logic import FishingLogicMixin
+from ...logic.has_logic import HasLogicMixin
+from ...logic.received_logic import ReceivedLogicMixin
+from ...logic.region_logic import RegionLogicMixin
+from ...logic.relationship_logic import RelationshipLogicMixin
+from ...logic.tool_logic import ToolLogicMixin
+from ...mods.mod_data import ModNames
+from ...options import SkillProgression
+from ...stardew_rule import StardewRule, False_, True_, And
+from ...strings.building_names import Building
+from ...strings.craftable_names import ModCraftable, ModMachine
+from ...strings.geode_names import Geode
+from ...strings.machine_names import Machine
+from ...strings.region_names import Region
+from ...strings.skill_names import ModSkill
+from ...strings.spells import MagicSpell
+from ...strings.tool_names import Tool, ToolMaterial
+
+
+class ModSkillLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.skill = ModSkillLogic(*args, **kwargs)
+
+
+class ModSkillLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, ActionLogicMixin, RelationshipLogicMixin, BuildingLogicMixin,
+ToolLogicMixin, FishingLogicMixin, CookingLogicMixin, CraftingLogicMixin, MagicLogicMixin]]):
+ def has_mod_level(self, skill: str, level: int) -> StardewRule:
+ if level <= 0:
+ return True_()
+
+ if self.options.skill_progression == SkillProgression.option_progressive:
+ return self.logic.received(f"{skill} Level", level)
+
+ return self.can_earn_mod_skill_level(skill, level)
+
+ def can_earn_mod_skill_level(self, skill: str, level: int) -> StardewRule:
+ if ModNames.luck_skill in self.options.mods and skill == ModSkill.luck:
+ return self.can_earn_luck_skill_level(level)
+ if ModNames.magic in self.options.mods and skill == ModSkill.magic:
+ return self.can_earn_magic_skill_level(level)
+ if ModNames.socializing_skill in self.options.mods and skill == ModSkill.socializing:
+ return self.can_earn_socializing_skill_level(level)
+ if ModNames.archaeology in self.options.mods and skill == ModSkill.archaeology:
+ return self.can_earn_archaeology_skill_level(level)
+ if ModNames.cooking_skill in self.options.mods and skill == ModSkill.cooking:
+ return self.can_earn_cooking_skill_level(level)
+ if ModNames.binning_skill in self.options.mods and skill == ModSkill.binning:
+ return self.can_earn_binning_skill_level(level)
+ return False_()
+
+ def can_earn_luck_skill_level(self, level: int) -> StardewRule:
+ if level >= 6:
+ return self.logic.fishing.can_fish_chests() | self.logic.action.can_open_geode(Geode.magma)
+ if level >= 3:
+ return self.logic.fishing.can_fish_chests() | self.logic.action.can_open_geode(Geode.geode)
+ return True_() # You can literally wake up and or get them by opening starting chests.
+
+ def can_earn_magic_skill_level(self, level: int) -> StardewRule:
+ spell_count = [self.logic.received(MagicSpell.clear_debris), self.logic.received(MagicSpell.water),
+ self.logic.received(MagicSpell.blink), self.logic.received(MagicSpell.fireball),
+ self.logic.received(MagicSpell.frostbite),
+ self.logic.received(MagicSpell.descend), self.logic.received(MagicSpell.tendrils),
+ self.logic.received(MagicSpell.shockwave),
+ self.logic.received(MagicSpell.meteor),
+ self.logic.received(MagicSpell.spirit)]
+ return self.logic.count(level, *spell_count)
+
+ def can_earn_socializing_skill_level(self, level: int) -> StardewRule:
+ villager_count = []
+
+ for villager in self.content.villagers.values():
+ villager_count.append(self.logic.relationship.can_earn_relationship(villager.name, level))
+
+ return self.logic.count(level * 2, *villager_count)
+
+ def can_earn_archaeology_skill_level(self, level: int) -> StardewRule:
+ shifter_rule = True_()
+ preservation_rule = True_()
+ if self.options.skill_progression == self.options.skill_progression.option_progressive:
+ shifter_rule = self.logic.has(ModCraftable.water_shifter)
+ preservation_rule = self.logic.has(ModMachine.hardwood_preservation_chamber)
+ if level >= 8:
+ return (self.logic.tool.has_tool(Tool.pan, ToolMaterial.iridium) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.gold)) & shifter_rule & preservation_rule
+ if level >= 5:
+ return (self.logic.tool.has_tool(Tool.pan, ToolMaterial.gold) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.iron)) & shifter_rule
+ if level >= 3:
+ return self.logic.tool.has_tool(Tool.pan, ToolMaterial.iron) | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.copper)
+ return self.logic.tool.has_tool(Tool.pan, ToolMaterial.copper) | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic)
+
+ def can_earn_cooking_skill_level(self, level: int) -> StardewRule:
+ if level >= 6:
+ return self.logic.cooking.can_cook() & self.logic.region.can_reach(Region.saloon) & \
+ self.logic.building.has_building(Building.coop) & self.logic.building.has_building(Building.barn)
+ else:
+ return self.logic.cooking.can_cook()
+
+ def can_earn_binning_skill_level(self, level: int) -> StardewRule:
+ if level <= 2:
+ return True_()
+ binning_rule = [self.logic.has(ModMachine.trash_bin) & self.logic.has(Machine.recycling_machine)]
+ if level > 4:
+ binning_rule.append(self.logic.has(ModMachine.composter))
+ if level > 7:
+ binning_rule.append(self.logic.has(ModMachine.recycling_bin))
+ if level > 9:
+ binning_rule.append(self.logic.has(ModMachine.advanced_recycling_machine))
+ return And(*binning_rule)
diff --git a/worlds/stardew_valley/mods/logic/skullcavernelevator.py b/worlds/stardew_valley/mods/logic/skullcavernelevator.py
deleted file mode 100644
index 74db86d89aac..000000000000
--- a/worlds/stardew_valley/mods/logic/skullcavernelevator.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from ...stardew_rule import Count, StardewRule, True_
-from ...mods.mod_data import ModNames
-from ... import options
-
-
-def has_skull_cavern_elevator_to_floor(self, floor: int) -> StardewRule:
- if self.options[options.ElevatorProgression] != options.ElevatorProgression.option_vanilla and \
- ModNames.skull_cavern_elevator in self.options[options.Mods]:
- return self.received("Progressive Skull Cavern Elevator", floor // 25)
- return True_()
diff --git a/worlds/stardew_valley/mods/logic/special_orders.py b/worlds/stardew_valley/mods/logic/special_orders.py
deleted file mode 100644
index 45d5d572dc05..000000000000
--- a/worlds/stardew_valley/mods/logic/special_orders.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from typing import Union
-from ...strings.craftable_names import Craftable
-from ...strings.food_names import Meal
-from ...strings.material_names import Material
-from ...strings.monster_drop_names import Loot
-from ...strings.region_names import Region
-from ...strings.special_order_names import SpecialOrder, ModSpecialOrder
-from ...strings.villager_names import ModNPC
-from ..mod_data import ModNames
-
-
-def get_modded_special_orders_rules(vanilla_logic, active_mods):
- special_orders = {}
- if ModNames.juna in active_mods:
- special_orders.update({
- ModSpecialOrder.junas_monster_mash: vanilla_logic.has_relationship(ModNPC.juna, 4) &
- vanilla_logic.can_complete_special_order(SpecialOrder.a_curious_substance) &
- vanilla_logic.has_rusty_key() &
- vanilla_logic.can_reach_region(Region.forest) & vanilla_logic.has(Craftable.monster_musk) &
- vanilla_logic.has("Energy Tonic") & vanilla_logic.has(Material.sap) & vanilla_logic.has(Loot.bug_meat) &
- vanilla_logic.has(Craftable.oil_of_garlic) & vanilla_logic.has(Meal.strange_bun)
- })
-
- return special_orders
diff --git a/worlds/stardew_valley/mods/logic/special_orders_logic.py b/worlds/stardew_valley/mods/logic/special_orders_logic.py
new file mode 100644
index 000000000000..1a0934282e09
--- /dev/null
+++ b/worlds/stardew_valley/mods/logic/special_orders_logic.py
@@ -0,0 +1,75 @@
+from typing import Union
+
+from ..mod_data import ModNames
+from ...data.craftable_data import all_crafting_recipes_by_name
+from ...logic.action_logic import ActionLogicMixin
+from ...logic.artisan_logic import ArtisanLogicMixin
+from ...logic.base_logic import BaseLogicMixin, BaseLogic
+from ...logic.crafting_logic import CraftingLogicMixin
+from ...logic.has_logic import HasLogicMixin
+from ...logic.received_logic import ReceivedLogicMixin
+from ...logic.region_logic import RegionLogicMixin
+from ...logic.relationship_logic import RelationshipLogicMixin
+from ...logic.season_logic import SeasonLogicMixin
+from ...logic.wallet_logic import WalletLogicMixin
+from ...strings.ap_names.community_upgrade_names import CommunityUpgrade
+from ...strings.artisan_good_names import ArtisanGood
+from ...strings.craftable_names import Consumable, Edible, Bomb
+from ...strings.crop_names import Fruit
+from ...strings.fertilizer_names import Fertilizer
+from ...strings.food_names import Meal
+from ...strings.geode_names import Geode
+from ...strings.material_names import Material
+from ...strings.metal_names import MetalBar, Artifact
+from ...strings.monster_drop_names import Loot
+from ...strings.region_names import Region, SVERegion
+from ...strings.special_order_names import SpecialOrder, ModSpecialOrder
+from ...strings.villager_names import ModNPC
+
+
+class ModSpecialOrderLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.special_order = ModSpecialOrderLogic(*args, **kwargs)
+
+
+class ModSpecialOrderLogic(BaseLogic[Union[ActionLogicMixin, ArtisanLogicMixin, CraftingLogicMixin, HasLogicMixin, RegionLogicMixin,
+ReceivedLogicMixin, RelationshipLogicMixin, SeasonLogicMixin, WalletLogicMixin]]):
+ def get_modded_special_orders_rules(self):
+ special_orders = {}
+ if ModNames.juna in self.options.mods:
+ special_orders.update({
+ ModSpecialOrder.junas_monster_mash: self.logic.relationship.has_hearts(ModNPC.juna, 4) &
+ self.registry.special_order_rules[SpecialOrder.a_curious_substance] &
+ self.logic.wallet.has_rusty_key() &
+ self.logic.region.can_reach(Region.forest) & self.logic.has(Consumable.monster_musk) &
+ self.logic.has("Energy Tonic") & self.logic.has(Material.sap) & self.logic.has(Loot.bug_meat) &
+ self.logic.has(Edible.oil_of_garlic) & self.logic.has(Meal.strange_bun)
+ })
+ if ModNames.sve in self.options.mods:
+ special_orders.update({
+ ModSpecialOrder.andys_cellar: self.logic.has(Material.stone) & self.logic.has(Material.wood) & self.logic.has(Material.hardwood) &
+ self.logic.has(MetalBar.iron) & self.logic.received(CommunityUpgrade.movie_theater, 1) &
+ self.logic.region.can_reach(SVERegion.fairhaven_farm),
+ ModSpecialOrder.a_mysterious_venture: self.logic.has(Bomb.cherry_bomb) & self.logic.has(Bomb.bomb) & self.logic.has(Bomb.mega_bomb) &
+ self.logic.region.can_reach(Region.adventurer_guild),
+ ModSpecialOrder.an_elegant_reception: self.logic.has(ArtisanGood.specific_wine(Fruit.starfruit)) & self.logic.has(ArtisanGood.cheese) &
+ self.logic.has(ArtisanGood.goat_cheese) & self.logic.season.has_any_not_winter() &
+ self.logic.region.can_reach(SVERegion.jenkins_cellar),
+ ModSpecialOrder.fairy_garden: self.logic.has(Consumable.fairy_dust) &
+ self.logic.region.can_reach(Region.island_south) & (
+ self.logic.action.can_open_geode(Geode.frozen) | self.logic.action.can_open_geode(Geode.omni)) &
+ self.logic.region.can_reach(SVERegion.blue_moon_vineyard),
+ ModSpecialOrder.homemade_fertilizer: self.logic.crafting.can_craft(all_crafting_recipes_by_name[Fertilizer.quality]) &
+ self.logic.region.can_reach(SVERegion.susans_house) # quest requires you make the fertilizer
+ })
+
+ if ModNames.jasper in self.options.mods:
+ special_orders.update({
+ ModSpecialOrder.dwarf_scroll: self.logic.has_all(*(Artifact.dwarf_scroll_i, Artifact.dwarf_scroll_ii, Artifact.dwarf_scroll_iii,
+ Artifact.dwarf_scroll_iv,)),
+ ModSpecialOrder.geode_order: self.logic.has_all(*(Geode.geode, Geode.frozen, Geode.magma, Geode.omni,)) &
+ self.logic.relationship.has_hearts(ModNPC.jasper, 8)
+ })
+
+ return special_orders
diff --git a/worlds/stardew_valley/mods/logic/sve_logic.py b/worlds/stardew_valley/mods/logic/sve_logic.py
new file mode 100644
index 000000000000..fc093554d8e6
--- /dev/null
+++ b/worlds/stardew_valley/mods/logic/sve_logic.py
@@ -0,0 +1,68 @@
+from typing import Union
+
+from ..mod_regions import SVERegion
+from ...logic.base_logic import BaseLogicMixin, BaseLogic
+from ...logic.combat_logic import CombatLogicMixin
+from ...logic.cooking_logic import CookingLogicMixin
+from ...logic.has_logic import HasLogicMixin
+from ...logic.money_logic import MoneyLogicMixin
+from ...logic.quest_logic import QuestLogicMixin
+from ...logic.received_logic import ReceivedLogicMixin
+from ...logic.region_logic import RegionLogicMixin
+from ...logic.relationship_logic import RelationshipLogicMixin
+from ...logic.season_logic import SeasonLogicMixin
+from ...logic.time_logic import TimeLogicMixin
+from ...logic.tool_logic import ToolLogicMixin
+from ...strings.ap_names.mods.mod_items import SVELocation, SVERunes, SVEQuestItem
+from ...strings.quest_names import ModQuest
+from ...strings.quest_names import Quest
+from ...strings.region_names import Region
+from ...strings.tool_names import Tool, ToolMaterial
+from ...strings.wallet_item_names import Wallet
+
+
+class SVELogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.sve = SVELogic(*args, **kwargs)
+
+
+class SVELogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, QuestLogicMixin, RegionLogicMixin, RelationshipLogicMixin, TimeLogicMixin, ToolLogicMixin,
+ CookingLogicMixin, MoneyLogicMixin, CombatLogicMixin, SeasonLogicMixin]]):
+ def initialize_rules(self):
+ self.registry.sve_location_rules.update({
+ SVELocation.tempered_galaxy_sword: self.logic.money.can_spend_at(SVERegion.alesia_shop, 350000),
+ SVELocation.tempered_galaxy_dagger: self.logic.money.can_spend_at(SVERegion.isaac_shop, 600000),
+ SVELocation.tempered_galaxy_hammer: self.logic.money.can_spend_at(SVERegion.isaac_shop, 400000),
+ })
+
+ def has_any_rune(self):
+ rune_list = SVERunes.nexus_items
+ return self.logic.or_(*(self.logic.received(rune) for rune in rune_list))
+
+ def has_iridium_bomb(self):
+ if self.options.quest_locations < 0:
+ return self.logic.quest.can_complete_quest(ModQuest.RailroadBoulder)
+ return self.logic.received(SVEQuestItem.iridium_bomb)
+
+ def has_marlon_boat(self):
+ if self.options.quest_locations < 0:
+ return self.logic.quest.can_complete_quest(ModQuest.MarlonsBoat)
+ return self.logic.received(SVEQuestItem.marlon_boat_paddle)
+
+ def has_grandpa_shed_repaired(self):
+ if self.options.quest_locations < 0:
+ return self.logic.quest.can_complete_quest(ModQuest.GrandpasShed)
+ return self.logic.received(SVEQuestItem.grandpa_shed)
+
+ def has_bear_knowledge(self):
+ if self.options.quest_locations < 0:
+ return self.logic.quest.can_complete_quest(Quest.strange_note)
+ return self.logic.received(Wallet.bears_knowledge)
+
+ def can_buy_bear_recipe(self):
+ access_rule = (self.logic.quest.can_complete_quest(Quest.strange_note) & self.logic.tool.has_tool(Tool.axe, ToolMaterial.basic) &
+ self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.basic))
+ forage_rule = self.logic.region.can_reach_any((Region.forest, Region.backwoods, Region.mountain))
+ knowledge_rule = self.has_bear_knowledge()
+ return access_rule & forage_rule & knowledge_rule
diff --git a/worlds/stardew_valley/mods/mod_data.py b/worlds/stardew_valley/mods/mod_data.py
index 81c498941132..54408fb2c571 100644
--- a/worlds/stardew_valley/mods/mod_data.py
+++ b/worlds/stardew_valley/mods/mod_data.py
@@ -21,3 +21,8 @@ class ModNames:
ayeisha = "Ayeisha - The Postal Worker (Custom NPC)"
riley = "Custom NPC - Riley"
skull_cavern_elevator = "Skull Cavern Elevator"
+ sve = "Stardew Valley Expanded"
+ alecto = "Alecto the Witch"
+ distant_lands = "Distant Lands - Witch Swamp Overhaul"
+ lacey = "Hat Mouse Lacey"
+ boarding_house = "Boarding House and Bus Stop Extension"
diff --git a/worlds/stardew_valley/mods/mod_monster_locations.py b/worlds/stardew_valley/mods/mod_monster_locations.py
new file mode 100644
index 000000000000..96ded1b2fc39
--- /dev/null
+++ b/worlds/stardew_valley/mods/mod_monster_locations.py
@@ -0,0 +1,40 @@
+from typing import Dict, Tuple
+
+from .mod_data import ModNames
+from ..strings.monster_names import Monster
+from ..strings.region_names import SVERegion, DeepWoodsRegion, BoardingHouseRegion
+
+sve_monsters_locations: Dict[str, Tuple[str, ...]] = {
+ Monster.shadow_brute_dangerous: (SVERegion.highlands_cavern,),
+ Monster.shadow_sniper: (SVERegion.highlands_cavern,),
+ Monster.shadow_shaman_dangerous: (SVERegion.highlands_cavern,),
+ Monster.mummy_dangerous: (SVERegion.crimson_badlands,),
+ Monster.royal_serpent: (SVERegion.crimson_badlands,),
+ Monster.skeleton_dangerous: (SVERegion.crimson_badlands,),
+ Monster.skeleton_mage: (SVERegion.crimson_badlands,),
+ Monster.dust_sprite_dangerous: (SVERegion.highlands_outside,),
+}
+
+deepwoods_monsters_locations: Dict[str, Tuple[str, ...]] = {
+ Monster.shadow_brute: (DeepWoodsRegion.floor_10,),
+ Monster.cave_fly: (DeepWoodsRegion.floor_10,),
+ Monster.green_slime: (DeepWoodsRegion.floor_10,),
+}
+
+boardinghouse_monsters_locations: Dict[str, Tuple[str, ...]] = {
+ Monster.shadow_brute: (BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1, BoardingHouseRegion.lost_valley_house_2,),
+ Monster.pepper_rex: (BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_1, BoardingHouseRegion.lost_valley_house_2,),
+ Monster.iridium_bat: (BoardingHouseRegion.lost_valley_ruins, BoardingHouseRegion.lost_valley_house_2,),
+ Monster.grub: (BoardingHouseRegion.abandoned_mines_1a, BoardingHouseRegion.abandoned_mines_1b, BoardingHouseRegion.abandoned_mines_2a,
+ BoardingHouseRegion.abandoned_mines_2b,),
+ Monster.bug: (BoardingHouseRegion.abandoned_mines_1a, BoardingHouseRegion.abandoned_mines_1b,),
+ Monster.bat: (BoardingHouseRegion.abandoned_mines_2a, BoardingHouseRegion.abandoned_mines_2b,),
+ Monster.cave_fly: (BoardingHouseRegion.abandoned_mines_3, BoardingHouseRegion.abandoned_mines_4, BoardingHouseRegion.abandoned_mines_5,),
+ Monster.frost_bat: (BoardingHouseRegion.abandoned_mines_3, BoardingHouseRegion.abandoned_mines_4, BoardingHouseRegion.abandoned_mines_5,),
+}
+
+modded_monsters_locations: Dict[str, Dict[str, Tuple[str, ...]]] = {
+ ModNames.sve: sve_monsters_locations,
+ ModNames.deepwoods: deepwoods_monsters_locations,
+ ModNames.boarding_house: boardinghouse_monsters_locations
+}
diff --git a/worlds/stardew_valley/mods/mod_regions.py b/worlds/stardew_valley/mods/mod_regions.py
index b05bc9538dba..a402ba606868 100644
--- a/worlds/stardew_valley/mods/mod_regions.py
+++ b/worlds/stardew_valley/mods/mod_regions.py
@@ -1,9 +1,11 @@
-from ..strings.entrance_names import DeepWoodsEntrance, EugeneEntrance, \
- JasperEntrance, AlecEntrance, YobaEntrance, JunaEntrance, MagicEntrance, AyeishaEntrance, RileyEntrance
-from ..strings.region_names import Region, DeepWoodsRegion, EugeneRegion, JasperRegion, \
- AlecRegion, YobaRegion, JunaRegion, MagicRegion, AyeishaRegion, RileyRegion
-from ..region_classes import RegionData, ConnectionData, RandomizationFlag, ModRegionData
+from typing import Dict, List
+
from .mod_data import ModNames
+from ..region_classes import RegionData, ConnectionData, ModificationFlag, RandomizationFlag, ModRegionData
+from ..strings.entrance_names import Entrance, DeepWoodsEntrance, EugeneEntrance, LaceyEntrance, BoardingHouseEntrance, \
+ JasperEntrance, AlecEntrance, YobaEntrance, JunaEntrance, MagicEntrance, AyeishaEntrance, RileyEntrance, SVEEntrance, AlectoEntrance
+from ..strings.region_names import Region, DeepWoodsRegion, EugeneRegion, JasperRegion, BoardingHouseRegion, \
+ AlecRegion, YobaRegion, JunaRegion, MagicRegion, AyeishaRegion, RileyRegion, SVERegion, AlectoRegion, LaceyRegion
deep_woods_regions = [
RegionData(Region.farm, [DeepWoodsEntrance.use_woods_obelisk]),
@@ -131,6 +133,236 @@
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA)
]
+stardew_valley_expanded_regions = [
+ RegionData(Region.backwoods, [SVEEntrance.backwoods_to_grove]),
+ RegionData(SVERegion.enchanted_grove, [SVEEntrance.grove_to_outpost_warp, SVEEntrance.grove_to_wizard_warp,
+ SVEEntrance.grove_to_farm_warp, SVEEntrance.grove_to_guild_warp, SVEEntrance.grove_to_junimo_warp,
+ SVEEntrance.grove_to_spring_warp, SVEEntrance.grove_to_aurora_warp]),
+ RegionData(SVERegion.grove_farm_warp, [SVEEntrance.farm_warp_to_farm]),
+ RegionData(SVERegion.grove_aurora_warp, [SVEEntrance.aurora_warp_to_aurora]),
+ RegionData(SVERegion.grove_guild_warp, [SVEEntrance.guild_warp_to_guild]),
+ RegionData(SVERegion.grove_junimo_warp, [SVEEntrance.junimo_warp_to_junimo]),
+ RegionData(SVERegion.grove_spring_warp, [SVEEntrance.spring_warp_to_spring]),
+ RegionData(SVERegion.grove_outpost_warp, [SVEEntrance.outpost_warp_to_outpost]),
+ RegionData(SVERegion.grove_wizard_warp, [SVEEntrance.wizard_warp_to_wizard]),
+ RegionData(SVERegion.galmoran_outpost, [SVEEntrance.outpost_to_badlands_entrance, SVEEntrance.use_alesia_shop,
+ SVEEntrance.use_isaac_shop]),
+ RegionData(SVERegion.badlands_entrance, [SVEEntrance.badlands_entrance_to_badlands]),
+ RegionData(SVERegion.crimson_badlands, [SVEEntrance.badlands_to_cave]),
+ RegionData(SVERegion.badlands_cave),
+ RegionData(Region.bus_stop, [SVEEntrance.bus_stop_to_shed]),
+ RegionData(SVERegion.grandpas_shed, [SVEEntrance.grandpa_shed_to_interior, SVEEntrance.grandpa_shed_to_town]),
+ RegionData(SVERegion.grandpas_shed_interior, [SVEEntrance.grandpa_interior_to_upstairs]),
+ RegionData(SVERegion.grandpas_shed_upstairs),
+ RegionData(Region.forest,
+ [SVEEntrance.forest_to_fairhaven, SVEEntrance.forest_to_west, SVEEntrance.forest_to_lost_woods,
+ SVEEntrance.forest_to_bmv, SVEEntrance.forest_to_marnie_shed]),
+ RegionData(SVERegion.marnies_shed),
+ RegionData(SVERegion.fairhaven_farm),
+ RegionData(Region.town, [SVEEntrance.town_to_bmv, SVEEntrance.town_to_jenkins,
+ SVEEntrance.town_to_bridge, SVEEntrance.town_to_plot]),
+ RegionData(SVERegion.blue_moon_vineyard, [SVEEntrance.bmv_to_sophia, SVEEntrance.bmv_to_beach]),
+ RegionData(SVERegion.sophias_house),
+ RegionData(SVERegion.jenkins_residence, [SVEEntrance.jenkins_to_cellar]),
+ RegionData(SVERegion.jenkins_cellar),
+ RegionData(SVERegion.unclaimed_plot, [SVEEntrance.plot_to_bridge]),
+ RegionData(SVERegion.shearwater),
+ RegionData(Region.museum, [SVEEntrance.museum_to_gunther_bedroom]),
+ RegionData(SVERegion.gunther_bedroom),
+ RegionData(Region.fish_shop, [SVEEntrance.fish_shop_to_willy_bedroom]),
+ RegionData(SVERegion.willy_bedroom),
+ RegionData(Region.mountain, [SVEEntrance.mountain_to_guild_summit]),
+ RegionData(SVERegion.guild_summit, [SVEEntrance.guild_to_interior, SVEEntrance.guild_to_mines,
+ SVEEntrance.summit_to_highlands]),
+ RegionData(Region.railroad, [SVEEntrance.to_susan_house, SVEEntrance.enter_summit, SVEEntrance.railroad_to_grampleton_station]),
+ RegionData(SVERegion.grampleton_station, [SVEEntrance.grampleton_station_to_grampleton_suburbs]),
+ RegionData(SVERegion.grampleton_suburbs, [SVEEntrance.grampleton_suburbs_to_scarlett_house]),
+ RegionData(SVERegion.scarlett_house),
+ RegionData(Region.wizard_basement, [SVEEntrance.wizard_to_fable_reef]),
+ RegionData(SVERegion.fable_reef, [SVEEntrance.fable_reef_to_guild], is_ginger_island=True),
+ RegionData(SVERegion.first_slash_guild, [SVEEntrance.first_slash_guild_to_hallway], is_ginger_island=True),
+ RegionData(SVERegion.first_slash_hallway, [SVEEntrance.first_slash_hallway_to_room], is_ginger_island=True),
+ RegionData(SVERegion.first_slash_spare_room, is_ginger_island=True),
+ RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave, SVEEntrance.highlands_to_pond], is_ginger_island=True),
+ RegionData(SVERegion.highlands_pond, is_ginger_island=True),
+ RegionData(SVERegion.highlands_cavern, [SVEEntrance.to_dwarf_prison], is_ginger_island=True),
+ RegionData(SVERegion.dwarf_prison, is_ginger_island=True),
+ RegionData(SVERegion.lances_house, [SVEEntrance.lance_to_ladder], is_ginger_island=True),
+ RegionData(SVERegion.lances_ladder, [SVEEntrance.lance_ladder_to_highlands], is_ginger_island=True),
+ RegionData(SVERegion.forest_west, [SVEEntrance.forest_west_to_spring, SVEEntrance.west_to_aurora,
+ SVEEntrance.use_bear_shop]),
+ RegionData(SVERegion.aurora_vineyard, [SVEEntrance.to_aurora_basement]),
+ RegionData(SVERegion.aurora_vineyard_basement),
+ RegionData(Region.secret_woods, [SVEEntrance.secret_woods_to_west]),
+ RegionData(SVERegion.bear_shop),
+ RegionData(SVERegion.sprite_spring, [SVEEntrance.sprite_spring_to_cave]),
+ RegionData(SVERegion.sprite_spring_cave),
+ RegionData(SVERegion.lost_woods, [SVEEntrance.lost_woods_to_junimo_woods]),
+ RegionData(SVERegion.junimo_woods, [SVEEntrance.use_purple_junimo]),
+ RegionData(SVERegion.purple_junimo_shop),
+ RegionData(SVERegion.alesia_shop),
+ RegionData(SVERegion.isaac_shop),
+ RegionData(SVERegion.summit),
+ RegionData(SVERegion.susans_house),
+ RegionData(Region.mountain, [Entrance.mountain_to_adventurer_guild, Entrance.mountain_to_the_mines], ModificationFlag.MODIFIED)
+
+]
+
+mandatory_sve_connections = [
+ ConnectionData(SVEEntrance.town_to_jenkins, SVERegion.jenkins_residence, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
+ ConnectionData(SVEEntrance.jenkins_to_cellar, SVERegion.jenkins_cellar, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.forest_to_bmv, SVERegion.blue_moon_vineyard),
+ ConnectionData(SVEEntrance.bmv_to_beach, Region.beach),
+ ConnectionData(SVEEntrance.town_to_plot, SVERegion.unclaimed_plot),
+ ConnectionData(SVEEntrance.town_to_bmv, SVERegion.blue_moon_vineyard),
+ ConnectionData(SVEEntrance.town_to_bridge, SVERegion.shearwater),
+ ConnectionData(SVEEntrance.plot_to_bridge, SVERegion.shearwater),
+ ConnectionData(SVEEntrance.bus_stop_to_shed, SVERegion.grandpas_shed),
+ ConnectionData(SVEEntrance.grandpa_shed_to_interior, SVERegion.grandpas_shed_interior,
+ flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
+ ConnectionData(SVEEntrance.grandpa_interior_to_upstairs, SVERegion.grandpas_shed_upstairs, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.grandpa_shed_to_town, Region.town),
+ ConnectionData(SVEEntrance.bmv_to_sophia, SVERegion.sophias_house, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
+ ConnectionData(SVEEntrance.summit_to_highlands, SVERegion.highlands_outside, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(SVEEntrance.guild_to_interior, Region.adventurer_guild, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.backwoods_to_grove, SVERegion.enchanted_grove, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
+ ConnectionData(SVEEntrance.grove_to_outpost_warp, SVERegion.grove_outpost_warp),
+ ConnectionData(SVEEntrance.outpost_warp_to_outpost, SVERegion.galmoran_outpost, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.grove_to_wizard_warp, SVERegion.grove_wizard_warp),
+ ConnectionData(SVEEntrance.wizard_warp_to_wizard, Region.wizard_basement, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.grove_to_aurora_warp, SVERegion.grove_aurora_warp),
+ ConnectionData(SVEEntrance.aurora_warp_to_aurora, SVERegion.aurora_vineyard_basement, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.grove_to_farm_warp, SVERegion.grove_farm_warp),
+ ConnectionData(SVEEntrance.to_aurora_basement, SVERegion.aurora_vineyard_basement, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.farm_warp_to_farm, Region.farm),
+ ConnectionData(SVEEntrance.grove_to_guild_warp, SVERegion.grove_guild_warp),
+ ConnectionData(SVEEntrance.guild_warp_to_guild, Region.adventurer_guild, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.grove_to_junimo_warp, SVERegion.grove_junimo_warp),
+ ConnectionData(SVEEntrance.junimo_warp_to_junimo, SVERegion.junimo_woods, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.use_purple_junimo, SVERegion.purple_junimo_shop),
+ ConnectionData(SVEEntrance.grove_to_spring_warp, SVERegion.grove_spring_warp),
+ ConnectionData(SVEEntrance.spring_warp_to_spring, SVERegion.sprite_spring, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.wizard_to_fable_reef, SVERegion.fable_reef, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(SVEEntrance.fable_reef_to_guild, SVERegion.first_slash_guild, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(SVEEntrance.outpost_to_badlands_entrance, SVERegion.badlands_entrance, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.badlands_entrance_to_badlands, SVERegion.crimson_badlands, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.badlands_to_cave, SVERegion.badlands_cave, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.guild_to_mines, Region.mines, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
+ ConnectionData(SVEEntrance.mountain_to_guild_summit, SVERegion.guild_summit),
+ ConnectionData(SVEEntrance.forest_to_west, SVERegion.forest_west),
+ ConnectionData(SVEEntrance.secret_woods_to_west, SVERegion.forest_west),
+ ConnectionData(SVEEntrance.west_to_aurora, SVERegion.aurora_vineyard, flag=RandomizationFlag.NON_PROGRESSION),
+ ConnectionData(SVEEntrance.forest_to_lost_woods, SVERegion.lost_woods),
+ ConnectionData(SVEEntrance.lost_woods_to_junimo_woods, SVERegion.junimo_woods),
+ ConnectionData(SVEEntrance.forest_to_marnie_shed, SVERegion.marnies_shed, flag=RandomizationFlag.NON_PROGRESSION),
+ ConnectionData(SVEEntrance.forest_west_to_spring, SVERegion.sprite_spring, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.to_susan_house, SVERegion.susans_house, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.enter_summit, SVERegion.summit, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.forest_to_fairhaven, SVERegion.fairhaven_farm, flag=RandomizationFlag.NON_PROGRESSION),
+ ConnectionData(SVEEntrance.highlands_to_lance, SVERegion.lances_house, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(SVEEntrance.lance_to_ladder, SVERegion.lances_ladder, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(SVEEntrance.lance_ladder_to_highlands, SVERegion.highlands_outside, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(SVEEntrance.highlands_to_cave, SVERegion.highlands_cavern, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(SVEEntrance.use_bear_shop, SVERegion.bear_shop),
+ ConnectionData(SVEEntrance.use_purple_junimo, SVERegion.purple_junimo_shop),
+ ConnectionData(SVEEntrance.use_alesia_shop, SVERegion.alesia_shop),
+ ConnectionData(SVEEntrance.use_isaac_shop, SVERegion.isaac_shop),
+ ConnectionData(SVEEntrance.to_dwarf_prison, SVERegion.dwarf_prison, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(SVEEntrance.railroad_to_grampleton_station, SVERegion.grampleton_station),
+ ConnectionData(SVEEntrance.grampleton_station_to_grampleton_suburbs, SVERegion.grampleton_suburbs),
+ ConnectionData(SVEEntrance.grampleton_suburbs_to_scarlett_house, SVERegion.scarlett_house, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.first_slash_guild_to_hallway, SVERegion.first_slash_hallway, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(SVEEntrance.first_slash_hallway_to_room, SVERegion.first_slash_spare_room,
+ flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(SVEEntrance.sprite_spring_to_cave, SVERegion.sprite_spring_cave, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.fish_shop_to_willy_bedroom, SVERegion.willy_bedroom, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.museum_to_gunther_bedroom, SVERegion.gunther_bedroom, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(SVEEntrance.highlands_to_pond, SVERegion.highlands_pond),
+]
+
+alecto_regions = [
+ RegionData(Region.witch_hut, [AlectoEntrance.witch_hut_to_witch_attic]),
+ RegionData(AlectoRegion.witch_attic)
+]
+
+alecto_entrances = [
+ ConnectionData(AlectoEntrance.witch_hut_to_witch_attic, AlectoRegion.witch_attic, flag=RandomizationFlag.BUILDINGS)
+]
+
+lacey_regions = [
+ RegionData(Region.forest, [LaceyEntrance.forest_to_hat_house]),
+ RegionData(LaceyRegion.hat_house)
+]
+
+lacey_entrances = [
+ ConnectionData(LaceyEntrance.forest_to_hat_house, LaceyRegion.hat_house, flag=RandomizationFlag.BUILDINGS)
+]
+
+boarding_house_regions = [
+ RegionData(Region.bus_stop, [BoardingHouseEntrance.bus_stop_to_boarding_house_plateau]),
+ RegionData(BoardingHouseRegion.boarding_house_plateau, [BoardingHouseEntrance.boarding_house_plateau_to_boarding_house_first,
+ BoardingHouseEntrance.boarding_house_plateau_to_buffalo_ranch,
+ BoardingHouseEntrance.boarding_house_plateau_to_abandoned_mines_entrance]),
+ RegionData(BoardingHouseRegion.boarding_house_first, [BoardingHouseEntrance.boarding_house_first_to_boarding_house_second]),
+ RegionData(BoardingHouseRegion.boarding_house_second),
+ RegionData(BoardingHouseRegion.buffalo_ranch),
+ RegionData(BoardingHouseRegion.abandoned_mines_entrance, [BoardingHouseEntrance.abandoned_mines_entrance_to_abandoned_mines_1a,
+ BoardingHouseEntrance.abandoned_mines_entrance_to_the_lost_valley]),
+ RegionData(BoardingHouseRegion.abandoned_mines_1a, [BoardingHouseEntrance.abandoned_mines_1a_to_abandoned_mines_1b]),
+ RegionData(BoardingHouseRegion.abandoned_mines_1b, [BoardingHouseEntrance.abandoned_mines_1b_to_abandoned_mines_2a]),
+ RegionData(BoardingHouseRegion.abandoned_mines_2a, [BoardingHouseEntrance.abandoned_mines_2a_to_abandoned_mines_2b]),
+ RegionData(BoardingHouseRegion.abandoned_mines_2b, [BoardingHouseEntrance.abandoned_mines_2b_to_abandoned_mines_3]),
+ RegionData(BoardingHouseRegion.abandoned_mines_3, [BoardingHouseEntrance.abandoned_mines_3_to_abandoned_mines_4]),
+ RegionData(BoardingHouseRegion.abandoned_mines_4, [BoardingHouseEntrance.abandoned_mines_4_to_abandoned_mines_5]),
+ RegionData(BoardingHouseRegion.abandoned_mines_5, [BoardingHouseEntrance.abandoned_mines_5_to_the_lost_valley]),
+ RegionData(BoardingHouseRegion.the_lost_valley, [BoardingHouseEntrance.the_lost_valley_to_gregory_tent,
+ BoardingHouseEntrance.lost_valley_to_lost_valley_minecart,
+ BoardingHouseEntrance.the_lost_valley_to_lost_valley_ruins]),
+ RegionData(BoardingHouseRegion.gregory_tent),
+ RegionData(BoardingHouseRegion.lost_valley_ruins, [BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1,
+ BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2]),
+ RegionData(BoardingHouseRegion.lost_valley_minecart),
+ RegionData(BoardingHouseRegion.lost_valley_house_1),
+ RegionData(BoardingHouseRegion.lost_valley_house_2)
+]
+
+boarding_house_entrances = [
+ ConnectionData(BoardingHouseEntrance.bus_stop_to_boarding_house_plateau, BoardingHouseRegion.boarding_house_plateau),
+ ConnectionData(BoardingHouseEntrance.boarding_house_plateau_to_boarding_house_first, BoardingHouseRegion.boarding_house_first,
+ flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
+ ConnectionData(BoardingHouseEntrance.boarding_house_first_to_boarding_house_second, BoardingHouseRegion.boarding_house_second,
+ flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(BoardingHouseEntrance.boarding_house_plateau_to_buffalo_ranch, BoardingHouseRegion.buffalo_ranch,
+ flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
+ ConnectionData(BoardingHouseEntrance.boarding_house_plateau_to_abandoned_mines_entrance, BoardingHouseRegion.abandoned_mines_entrance,
+ flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
+ ConnectionData(BoardingHouseEntrance.abandoned_mines_entrance_to_the_lost_valley, BoardingHouseRegion.lost_valley_minecart,
+ flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(BoardingHouseEntrance.abandoned_mines_entrance_to_abandoned_mines_1a, BoardingHouseRegion.abandoned_mines_1a,
+ flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(BoardingHouseEntrance.abandoned_mines_1a_to_abandoned_mines_1b, BoardingHouseRegion.abandoned_mines_1b, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(BoardingHouseEntrance.abandoned_mines_1b_to_abandoned_mines_2a, BoardingHouseRegion.abandoned_mines_2a, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(BoardingHouseEntrance.abandoned_mines_2a_to_abandoned_mines_2b, BoardingHouseRegion.abandoned_mines_2b, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(BoardingHouseEntrance.abandoned_mines_2b_to_abandoned_mines_3, BoardingHouseRegion.abandoned_mines_3, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(BoardingHouseEntrance.abandoned_mines_3_to_abandoned_mines_4, BoardingHouseRegion.abandoned_mines_4, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(BoardingHouseEntrance.abandoned_mines_4_to_abandoned_mines_5, BoardingHouseRegion.abandoned_mines_5, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(BoardingHouseEntrance.abandoned_mines_5_to_the_lost_valley, BoardingHouseRegion.the_lost_valley, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(BoardingHouseEntrance.the_lost_valley_to_gregory_tent, BoardingHouseRegion.gregory_tent, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(BoardingHouseEntrance.lost_valley_to_lost_valley_minecart, BoardingHouseRegion.lost_valley_minecart),
+ ConnectionData(BoardingHouseEntrance.the_lost_valley_to_lost_valley_ruins, BoardingHouseRegion.lost_valley_ruins, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1, BoardingHouseRegion.lost_valley_house_1, flag=RandomizationFlag.BUILDINGS),
+ ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2, BoardingHouseRegion.lost_valley_house_2, flag=RandomizationFlag.BUILDINGS)
+]
+
+vanilla_connections_to_remove_by_mod: Dict[str, List[ConnectionData]] = {
+ ModNames.sve: [
+ ConnectionData(Entrance.mountain_to_the_mines, Region.mines,
+ flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
+ ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild,
+ flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
+ ]
+}
+
ModDataList = {
ModNames.deepwoods: ModRegionData(ModNames.deepwoods, deep_woods_regions, deep_woods_entrances),
ModNames.eugene: ModRegionData(ModNames.eugene, eugene_regions, eugene_entrances),
@@ -141,4 +373,8 @@
ModNames.magic: ModRegionData(ModNames.magic, magic_regions, magic_entrances),
ModNames.ayeisha: ModRegionData(ModNames.ayeisha, ayeisha_regions, ayeisha_entrances),
ModNames.riley: ModRegionData(ModNames.riley, riley_regions, riley_entrances),
+ ModNames.sve: ModRegionData(ModNames.sve, stardew_valley_expanded_regions, mandatory_sve_connections),
+ ModNames.alecto: ModRegionData(ModNames.alecto, alecto_regions, alecto_entrances),
+ ModNames.lacey: ModRegionData(ModNames.lacey, lacey_regions, lacey_entrances),
+ ModNames.boarding_house: ModRegionData(ModNames.boarding_house, boarding_house_regions, boarding_house_entrances),
}
diff --git a/worlds/stardew_valley/option_groups.py b/worlds/stardew_valley/option_groups.py
new file mode 100644
index 000000000000..d0f052348a7e
--- /dev/null
+++ b/worlds/stardew_valley/option_groups.py
@@ -0,0 +1,76 @@
+import logging
+
+from Options import DeathLink, ProgressionBalancing, Accessibility
+from .options import (Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice,
+ EntranceRandomization, SeasonRandomization, Cropsanity, BackpackProgression,
+ ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression,
+ FestivalLocations, ArcadeMachineLocations, SpecialOrderLocations,
+ QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize,
+ NumberOfMovementBuffs, EnabledFillerBuffs, ExcludeGingerIsland, TrapItems,
+ MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier,
+ FriendshipMultiplier, DebrisMultiplier, QuickStart, Gifting, FarmType,
+ Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, Mods, Booksanity, Walnutsanity, BundlePlando)
+
+sv_option_groups = []
+try:
+ from Options import OptionGroup
+except:
+ logging.warning("Old AP Version, OptionGroup not available.")
+else:
+ sv_option_groups = [
+ OptionGroup("General", [
+ Goal,
+ FarmType,
+ BundleRandomization,
+ BundlePrice,
+ EntranceRandomization,
+ ExcludeGingerIsland,
+ ]),
+ OptionGroup("Major Unlocks", [
+ SeasonRandomization,
+ Cropsanity,
+ BackpackProgression,
+ ToolProgression,
+ ElevatorProgression,
+ SkillProgression,
+ BuildingProgression,
+ ]),
+ OptionGroup("Extra Shuffling", [
+ FestivalLocations,
+ ArcadeMachineLocations,
+ SpecialOrderLocations,
+ QuestLocations,
+ Fishsanity,
+ Museumsanity,
+ Friendsanity,
+ FriendsanityHeartSize,
+ Monstersanity,
+ Shipsanity,
+ Cooksanity,
+ Chefsanity,
+ Craftsanity,
+ Booksanity,
+ Walnutsanity,
+ ]),
+ OptionGroup("Multipliers and Buffs", [
+ StartingMoney,
+ ProfitMargin,
+ ExperienceMultiplier,
+ FriendshipMultiplier,
+ DebrisMultiplier,
+ NumberOfMovementBuffs,
+ EnabledFillerBuffs,
+ TrapItems,
+ MultipleDaySleepEnabled,
+ MultipleDaySleepCost,
+ QuickStart,
+ ]),
+ OptionGroup("Advanced Options", [
+ Gifting,
+ DeathLink,
+ Mods,
+ BundlePlando,
+ ProgressionBalancing,
+ Accessibility,
+ ]),
+ ]
diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py
index 78de9e8dbbaa..5369e88a2dcb 100644
--- a/worlds/stardew_valley/options.py
+++ b/worlds/stardew_valley/options.py
@@ -1,41 +1,36 @@
+import sys
+import typing
from dataclasses import dataclass
-from typing import Dict, Union, Protocol, runtime_checkable, ClassVar
+from typing import Protocol, ClassVar
-from Options import Option, Range, DeathLink, SpecialRange, Toggle, Choice, OptionSet
+from Options import Range, NamedRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, OptionList, Visibility
from .mods.mod_data import ModNames
+from .strings.ap_names.ap_option_names import OptionName
+from .strings.bundle_names import all_cc_bundle_names
-@runtime_checkable
-class StardewOption(Protocol):
- internal_name: ClassVar[str]
-
-
-@dataclass
-class StardewOptions:
- options: Dict[str, Union[bool, int, str]]
-
- def __getitem__(self, item: Union[str, StardewOption]) -> Union[bool, int, str]:
- if isinstance(item, StardewOption):
- item = item.internal_name
- return self.options.get(item, None)
-
- def __setitem__(self, key: Union[str, StardewOption], value: Union[bool, int, str]):
- if isinstance(key, StardewOption):
- key = key.internal_name
- self.options[key] = value
+class StardewValleyOption(Protocol):
+ internal_name: ClassVar[str]
class Goal(Choice):
- """What's your goal with this play-through?
- Community Center: The world will be completed once you complete the Community Center.
- Grandpa's Evaluation: The world will be completed once 4 candles are lit at Grandpa's Shrine.
- Bottom of the Mines: The world will be completed once you reach level 120 in the mineshaft.
- Cryptic Note: The world will be completed once you complete the quest "Cryptic Note" where Mr Qi asks you to reach floor 100 in the Skull Cavern.
- Master Angler: The world will be completed once you have caught every fish in the game. Pairs well with Fishsanity.
- Complete Collection: The world will be completed once you have completed the museum by donating every possible item. Pairs well with Museumsanity.
- Full House: The world will be completed once you get married and have two kids. Pairs well with Friendsanity.
- Greatest Walnut Hunter: The world will be completed once you find all 130 Golden Walnuts
- Perfection: The world will be completed once you attain Perfection, based on the vanilla definition.
+ """Goal for this playthrough
+ Community Center: Complete the Community Center
+ Grandpa's Evaluation: 4 lit candles in Grandpa's evaluation
+ Bottom of the Mines: Reach level 120 in the mines
+ Cryptic Note: Complete the quest "Cryptic Note" (Skull Cavern Floor 100)
+ Master Angler: Catch every fish. Adapts to Fishsanity
+ Complete Collection: Complete the museum collection
+ Full House: Get married and have 2 children
+ Greatest Walnut Hunter: Find 130 Golden Walnuts. Pairs well with Walnutsanity
+ Protector of the Valley: Complete the monster slayer goals. Adapts to Monstersanity
+ Full Shipment: Ship every item. Adapts to Shipsanity
+ Gourmet Chef: Cook every recipe. Adapts to Cooksanity
+ Craft Master: Craft every item
+ Legend: Earn 10 000 000g
+ Mystery of the Stardrops: Find every stardrop
+ Allsanity: Complete every check in your slot
+ Perfection: Attain Perfection
"""
internal_name = "goal"
display_name = "Goal"
@@ -48,16 +43,18 @@ class Goal(Choice):
option_complete_collection = 5
option_full_house = 6
option_greatest_walnut_hunter = 7
+ option_protector_of_the_valley = 8
+ option_full_shipment = 9
+ option_gourmet_chef = 10
+ option_craft_master = 11
+ option_legend = 12
+ option_mystery_of_the_stardrops = 13
# option_junimo_kart =
# option_prairie_king =
# option_fector_challenge =
- # option_craft_master =
- # option_mystery_of_the_stardrops =
- # option_protector_of_the_valley =
- # option_full_shipment =
- # option_legend =
# option_beloved_farmer =
# option_master_of_the_five_ways =
+ option_allsanity = 24
option_perfection = 25
@classmethod
@@ -68,12 +65,27 @@ def get_option_name(cls, value) -> str:
return super().get_option_name(value)
-class StartingMoney(SpecialRange):
+class FarmType(Choice):
+ """What farm to play on?"""
+ internal_name = "farm_type"
+ display_name = "Farm Type"
+ default = "random"
+ option_standard = 0
+ option_riverland = 1
+ option_forest = 2
+ option_hill_top = 3
+ option_wilderness = 4
+ option_four_corners = 5
+ option_beach = 6
+ option_meadowlands = 7
+
+
+class StartingMoney(NamedRange):
"""Amount of gold when arriving at the farm.
- Set to -1 or unlimited for infinite money in this playthrough"""
+ Set to -1 or unlimited for infinite money"""
internal_name = "starting_money"
display_name = "Starting Gold"
- range_start = -1
+ range_start = 0
range_end = 50000
default = 5000
@@ -87,7 +99,7 @@ class StartingMoney(SpecialRange):
}
-class ProfitMargin(SpecialRange):
+class ProfitMargin(NamedRange):
"""Multiplier over all gold earned in-game by the player."""
internal_name = "profit_margin"
display_name = "Profit Margin"
@@ -110,37 +122,48 @@ class BundleRandomization(Choice):
"""What items are needed for the community center bundles?
Vanilla: Standard bundles from the vanilla game
Thematic: Every bundle will require random items compatible with their original theme
+ Remixed: Picks bundles at random from thematic, vanilla remixed and new custom ones
+ Remixed Anywhere: Remixed, but bundles are not locked to specific rooms.
Shuffled: Every bundle will require random items and follow no particular structure"""
internal_name = "bundle_randomization"
display_name = "Bundle Randomization"
- default = 1
option_vanilla = 0
option_thematic = 1
- option_shuffled = 2
+ option_remixed = 3
+ option_remixed_anywhere = 4
+ option_shuffled = 6
+ default = option_remixed
class BundlePrice(Choice):
"""How many items are needed for the community center bundles?
+ Minimum: Every bundle will require only one item
Very Cheap: Every bundle will require 2 items fewer than usual
Cheap: Every bundle will require 1 item fewer than usual
Normal: Every bundle will require the vanilla number of items
- Expensive: Every bundle will require 1 extra item when applicable"""
+ Expensive: Every bundle will require 1 extra item
+ Very Expensive: Every bundle will require 2 extra items
+ Maximum: Every bundle will require many extra items"""
internal_name = "bundle_price"
display_name = "Bundle Price"
- default = 2
- option_very_cheap = 0
- option_cheap = 1
- option_normal = 2
- option_expensive = 3
+ default = 0
+ option_minimum = -8
+ option_very_cheap = -2
+ option_cheap = -1
+ option_normal = 0
+ option_expensive = 1
+ option_very_expensive = 2
+ option_maximum = 8
class EntranceRandomization(Choice):
"""Should area entrances be randomized?
Disabled: No entrance randomization is done
- Pelican Town: Only buildings in the main town area are randomized among each other
- Non Progression: Only buildings that are always available are randomized with each other
- Buildings: All Entrances that Allow you to enter a building using a door are randomized with each other
- Chaos: Same as above, but the entrances get reshuffled every single day!
+ Pelican Town: Only doors in the main town area are randomized with each other
+ Non Progression: Only entrances that are always available are randomized with each other
+ Buildings: All entrances that allow you to enter a building are randomized with each other
+ Buildings Without House: Buildings, but excluding the farmhouse
+ Chaos: Same as "Buildings", but the entrances get reshuffled every single day!
"""
# Everything: All buildings and areas are randomized with each other
# Chaos, same as everything: but the buildings are shuffled again every in-game day. You can't learn it!
@@ -154,9 +177,10 @@ class EntranceRandomization(Choice):
option_disabled = 0
option_pelican_town = 1
option_non_progression = 2
- option_buildings = 3
- # option_everything = 4
- option_chaos = 5
+ option_buildings_without_house = 3
+ option_buildings = 4
+ # option_everything = 10
+ option_chaos = 12
# option_buildings_one_way = 6
# option_everything_one_way = 7
# option_chaos_one_way = 8
@@ -164,11 +188,10 @@ class EntranceRandomization(Choice):
class SeasonRandomization(Choice):
"""Should seasons be randomized?
- All settings allow you to choose which season you want to play next (from those unlocked) at the end of a season.
- Disabled: You will start in Spring with all seasons unlocked.
- Randomized: The seasons will be unlocked randomly as Archipelago items.
- Randomized Not Winter: The seasons are randomized, but you're guaranteed not to start with winter.
- Progressive: You will start in Spring and unlock the seasons in their original order.
+ Disabled: Start in Spring with all seasons unlocked.
+ Randomized: Start in a random season and the other 3 must be unlocked randomly.
+ Randomized Not Winter: Same as randomized, but the start season is guaranteed not to be winter.
+ Progressive: Start in Spring and unlock the seasons in their original order.
"""
internal_name = "season_randomization"
display_name = "Season Randomization"
@@ -183,20 +206,21 @@ class Cropsanity(Choice):
"""Formerly named "Seed Shuffle"
Pierre now sells a random amount of seasonal seeds and Joja sells them without season requirements, but only in huge packs.
Disabled: All the seeds are unlocked from the start, there are no location checks for growing and harvesting crops
- Shuffled: Seeds are unlocked as archipelago item, for each seed there is a location check for growing and harvesting that crop
+ Enabled: Seeds are unlocked as archipelago items, for each seed there is a location check for growing and harvesting that crop
"""
internal_name = "cropsanity"
display_name = "Cropsanity"
default = 1
option_disabled = 0
- option_shuffled = 1
+ option_enabled = 1
+ alias_shuffled = option_enabled
class BackpackProgression(Choice):
- """How is the backpack progression handled?
- Vanilla: You can buy them at Pierre's General Store.
+ """Shuffle the backpack?
+ Vanilla: You can buy backpacks at Pierre's General Store.
Progressive: You will randomly find Progressive Backpack upgrades.
- Early Progressive: You can expect your first Backpack in sphere 1.
+ Early Progressive: Same as progressive, but one backpack will be placed early in the multiworld.
"""
internal_name = "backpack_progression"
display_name = "Backpack Progression"
@@ -207,23 +231,28 @@ class BackpackProgression(Choice):
class ToolProgression(Choice):
- """How is the tool progression handled?
- Vanilla: Clint will upgrade your tools with ore.
- Progressive: You will randomly find Progressive Tool upgrades."""
+ """Shuffle the tool upgrades?
+ Vanilla: Clint will upgrade your tools with metal bars.
+ Progressive: You will randomly find Progressive Tool upgrades.
+ Cheap: Tool Upgrades will cost 2/5th as much
+ Very Cheap: Tool Upgrades will cost 1/5th as much"""
internal_name = "tool_progression"
display_name = "Tool Progression"
default = 1
- option_vanilla = 0
- option_progressive = 1
+ option_vanilla = 0b000 # 0
+ option_progressive = 0b001 # 1
+ option_vanilla_cheap = 0b010 # 2
+ option_vanilla_very_cheap = 0b100 # 4
+ option_progressive_cheap = 0b011 # 3
+ option_progressive_very_cheap = 0b101 # 5
class ElevatorProgression(Choice):
- """How is Elevator progression handled?
- Vanilla: You will unlock new elevator floors for yourself.
- Progressive: You will randomly find Progressive Mine Elevators to go deeper. Locations are sent for reaching
- every elevator level.
- Progressive from previous floor: Same as progressive, but you must reach elevator floors on your own,
- you cannot use the elevator to check elevator locations"""
+ """Shuffle the elevator?
+ Vanilla: Reaching a mineshaft floor unlocks the elevator for it
+ Progressive: You will randomly find Progressive Mine Elevators to go deeper.
+ Progressive from previous floor: Same as progressive, but you cannot use the elevator to check elevator locations.
+ You must reach elevator floors on your own."""
internal_name = "elevator_progression"
display_name = "Elevator Progression"
default = 2
@@ -233,37 +262,42 @@ class ElevatorProgression(Choice):
class SkillProgression(Choice):
- """How is the skill progression handled?
- Vanilla: You will level up and get the normal reward at each level.
- Progressive: The xp will be earned internally, locations will be sent when you earn a level. Your real
- levels will be scattered around the multiworld."""
+ """Shuffle skill levels?
+ Vanilla: Leveling up skills is normal
+ Progressive: Skill levels are unlocked randomly, and earning xp sends checks. Masteries are excluded
+ With Masteries: Skill levels are unlocked randomly, and earning xp sends checks. Masteries are included"""
internal_name = "skill_progression"
display_name = "Skill Progression"
- default = 1
+ default = 2
option_vanilla = 0
option_progressive = 1
+ option_progressive_with_masteries = 2
class BuildingProgression(Choice):
- """How is the building progression handled?
- Vanilla: You will buy each building normally.
+ """Shuffle Carpenter Buildings?
+ Vanilla: You can buy each building normally.
Progressive: You will receive the buildings and will be able to build the first one of each type for free,
once it is received. If you want more of the same building, it will cost the vanilla price.
- Progressive early shipping bin: You can expect your shipping bin in sphere 1.
+ Cheap: Buildings will cost half as much
+ Very Cheap: Buildings will cost 1/5th as much
"""
internal_name = "building_progression"
display_name = "Building Progression"
- default = 2
- option_vanilla = 0
- option_progressive = 1
- option_progressive_early_shipping_bin = 2
+ default = 3
+ option_vanilla = 0b000 # 0
+ option_vanilla_cheap = 0b010 # 2
+ option_vanilla_very_cheap = 0b100 # 4
+ option_progressive = 0b001 # 1
+ option_progressive_cheap = 0b011 # 3
+ option_progressive_very_cheap = 0b101 # 5
class FestivalLocations(Choice):
- """Locations for attending and participating in festivals
- With Disabled, you do not need to attend festivals
- With Easy, there are checks for participating in festivals
- With Hard, the festival checks are only granted when the player performs well in the festival
+ """Shuffle Festival Activities?
+ Disabled: You do not need to attend festivals
+ Easy: Every festival has checks, but they are easy and usually only require attendance
+ Hard: Festivals have more checks, and many require performing well, not just attending
"""
internal_name = "festival_locations"
display_name = "Festival Locations"
@@ -274,11 +308,10 @@ class FestivalLocations(Choice):
class ArcadeMachineLocations(Choice):
- """How are the Arcade Machines handled?
- Disabled: The arcade machines are not included in the Archipelago shuffling.
+ """Shuffle the arcade machines?
+ Disabled: The arcade machines are not included.
Victories: Each Arcade Machine will contain one check on victory
- Victories Easy: The arcade machines are both made considerably easier to be more accessible for the average
- player.
+ Victories Easy: Same as Victories, but both games are made considerably easier.
Full Shuffling: The arcade machines will contain multiple checks each, and different buffs that make the game
easier are in the item pool. Junimo Kart has one check at the end of each level.
Journey of the Prairie King has one check after each boss, plus one check for each vendor equipment.
@@ -293,32 +326,49 @@ class ArcadeMachineLocations(Choice):
class SpecialOrderLocations(Choice):
- """How are the Special Orders handled?
+ """Shuffle Special Orders?
Disabled: The special orders are not included in the Archipelago shuffling.
Board Only: The Special Orders on the board in town are location checks
- Board and Qi: The Special Orders from Qi's walnut room are checks, as well as the board in town
+ Board and Qi: The Special Orders from Mr Qi's walnut room are checks, in addition to the board in town
+ Short: All Special Order requirements are reduced by 40%
+ Very Short: All Special Order requirements are reduced by 80%
"""
internal_name = "special_order_locations"
display_name = "Special Order Locations"
- default = 1
- option_disabled = 0
- option_board_only = 1
- option_board_qi = 2
-
-
-class HelpWantedLocations(SpecialRange):
- """How many "Help Wanted" quests need to be completed as Archipelago Locations
- Out of every 7 quests, 4 will be item deliveries, and then 1 of each for: Fishing, Gathering and Slaying Monsters.
- Choosing a multiple of 7 is recommended."""
- internal_name = "help_wanted_locations"
+ option_vanilla = 0b0000 # 0
+ option_board = 0b0001 # 1
+ value_qi = 0b0010 # 2
+ value_short = 0b0100 # 4
+ value_very_short = 0b1000 # 8
+ option_board_qi = option_board | value_qi # 3
+ option_vanilla_short = value_short # 4
+ option_board_short = option_board | value_short # 5
+ option_board_qi_short = option_board_qi | value_short # 7
+ option_vanilla_very_short = value_very_short # 8
+ option_board_very_short = option_board | value_very_short # 9
+ option_board_qi_very_short = option_board_qi | value_very_short # 11
+ alias_disabled = option_vanilla
+ alias_board_only = option_board
+ default = option_board_short
+
+
+class QuestLocations(NamedRange):
+ """Include location checks for quests
+ None: No quests are checks
+ Story: Only story quests are checks
+ Number: Story quests and help wanted quests are checks up to the specified amount. Multiple of 7 recommended
+ Out of every 7 help wanted quests, 4 will be item deliveries, and then 1 of each for: Fishing, Gathering and Slaying Monsters.
+ Extra Help wanted quests might be added if current settings don't have enough locations"""
+ internal_name = "quest_locations"
default = 7
range_start = 0
range_end = 56
# step = 7
- display_name = "Number of Help Wanted locations"
+ display_name = "Quest Locations"
special_range_names = {
- "none": 0,
+ "none": -1,
+ "story": 0,
"minimum": 7,
"normal": 14,
"lots": 28,
@@ -327,9 +377,9 @@ class HelpWantedLocations(SpecialRange):
class Fishsanity(Choice):
- """Locations for catching fish?
+ """Locations for catching a fish the first time?
None: There are no locations for catching fish
- Legendaries: Each of the 5 legendary fish are checks
+ Legendaries: Each of the 5 legendary fish are checks, plus the extended family if qi board is turned on
Special: A curated selection of strong fish are checks
Randomized: A random selection of fish are checks
All: Every single fish in the game is a location that contains an item. Pairs well with the Master Angler Goal
@@ -356,7 +406,7 @@ class Museumsanity(Choice):
None: There are no locations for donating artifacts and minerals to the museum
Milestones: The donation milestones from the vanilla game are checks
Randomized: A random selection of minerals and artifacts are checks
- All: Every single donation will be a check
+ All: Every single donation is a check
"""
internal_name = "museumsanity"
display_name = "Museumsanity"
@@ -367,13 +417,122 @@ class Museumsanity(Choice):
option_all = 3
+class Monstersanity(Choice):
+ """Locations for slaying monsters?
+ None: There are no checks for slaying monsters
+ One per category: Every category visible at the adventure guild gives one check
+ One per Monster: Every unique monster gives one check
+ Monster Eradication Goals: The Monster Eradication Goals each contain one check
+ Short Monster Eradication Goals: The Monster Eradication Goals each contain one check, but are reduced by 60%
+ Very Short Monster Eradication Goals: The Monster Eradication Goals each contain one check, but are reduced by 90%
+ Progressive Eradication Goals: The Monster Eradication Goals each contain 5 checks, each 20% of the way
+ Split Eradication Goals: The Monster Eradication Goals are split by monsters, each monster has one check
+ """
+ internal_name = "monstersanity"
+ display_name = "Monstersanity"
+ default = 1
+ option_none = 0
+ option_one_per_category = 1
+ option_one_per_monster = 2
+ option_goals = 3
+ option_short_goals = 4
+ option_very_short_goals = 5
+ option_progressive_goals = 6
+ option_split_goals = 7
+
+
+class Shipsanity(Choice):
+ """Locations for shipping items?
+ None: There are no checks for shipping items
+ Crops: Every crop and forageable being shipped is a check
+ Fish: Every fish being shipped is a check except legendaries
+ Full Shipment: Every item in the Collections page is a check
+ Full Shipment With Fish: Every item in the Collections page and every fish is a check
+ Everything: Every item in the game that can be shipped is a check
+ """
+ internal_name = "shipsanity"
+ display_name = "Shipsanity"
+ default = 0
+ option_none = 0
+ option_crops = 1
+ # option_quality_crops = 2
+ option_fish = 3
+ # option_quality_fish = 4
+ option_full_shipment = 5
+ # option_quality_full_shipment = 6
+ option_full_shipment_with_fish = 7
+ # option_quality_full_shipment_with_fish = 8
+ option_everything = 9
+ # option_quality_everything = 10
+
+
+class Cooksanity(Choice):
+ """Locations for cooking food?
+ None: There are no checks for cooking
+ Queen of Sauce: Every Queen of Sauce Recipe can be cooked for a check
+ All: Every cooking recipe can be cooked for a check
+ """
+ internal_name = "cooksanity"
+ display_name = "Cooksanity"
+ default = 0
+ option_none = 0
+ option_queen_of_sauce = 1
+ option_all = 2
+
+
+class Chefsanity(NamedRange):
+ """Locations for learning cooking recipes?
+ Vanilla: All cooking recipes are learned normally
+ Queen of Sauce: Every Queen of sauce episode is a check, all queen of sauce recipes are items
+ Purchases: Every purchasable recipe is a check
+ Friendship: Recipes obtained from friendship are checks
+ Skills: Recipes obtained from skills are checks
+ All: Learning every cooking recipe is a check
+ """
+ internal_name = "chefsanity"
+ display_name = "Chefsanity"
+ default = 0
+ range_start = 0
+ range_end = 15
+
+ option_none = 0b0000 # 0
+ option_queen_of_sauce = 0b0001 # 1
+ option_purchases = 0b0010 # 2
+ option_qos_and_purchases = 0b0011 # 3
+ option_skills = 0b0100 # 4
+ option_friendship = 0b1000 # 8
+ option_all = 0b1111 # 15
+
+ special_range_names = {
+ "none": 0b0000, # 0
+ "queen_of_sauce": 0b0001, # 1
+ "purchases": 0b0010, # 2
+ "qos_and_purchases": 0b0011, # 3
+ "skills": 0b0100, # 4
+ "friendship": 0b1000, # 8
+ "all": 0b1111, # 15
+ }
+
+
+class Craftsanity(Choice):
+ """Checks for crafting items?
+ If enabled, all recipes purchased in shops will be checks as well.
+ Recipes obtained from other sources will depend on related archipelago settings
+ """
+ internal_name = "craftsanity"
+ display_name = "Craftsanity"
+ default = 0
+ option_none = 0
+ option_all = 1
+
+
class Friendsanity(Choice):
- """Locations for friendships?
- None: There are no checks for befriending villagers
- Bachelors: Each heart of a bachelor is a check
- Starting NPCs: Each heart for npcs that are immediately available is a check
- All: Every heart with every NPC is a check, including Leo, Kent, Sandy, etc
- All With Marriage: Marriage candidates must also be dated, married, and befriended up to 14 hearts.
+ """Shuffle Friendships?
+ None: Friendship hearts are earned normally
+ Bachelors: Hearts with bachelors are shuffled
+ Starting NPCs: Hearts for NPCs available immediately are checks
+ All: Hearts for all npcs are checks, including Leo, Kent, Sandy, etc
+ All With Marriage: Hearts for all npcs are checks, including romance hearts up to 14 when applicable
"""
internal_name = "friendsanity"
display_name = "Friendsanity"
@@ -388,7 +547,7 @@ class Friendsanity(Choice):
# Conditional Setting - Friendsanity not None
class FriendsanityHeartSize(Range):
- """If using friendsanity, how many hearts are received per item, and how many hearts must be earned to send a check
+ """If using friendsanity, how many hearts are received per heart item, and how many hearts must be earned to send a check
A higher value will lead to fewer heart items in the item pool, reducing bloat"""
internal_name = "friendsanity_heart_size"
display_name = "Friendsanity Heart Size"
@@ -398,6 +557,46 @@ class FriendsanityHeartSize(Range):
# step = 1
+class Booksanity(Choice):
+ """Shuffle Books?
+ None: All books behave like vanilla
+ Power: Power books are turned into checks
+ Power and Skill: Power and skill books are turned into checks.
+ All: Lost books are also included in the shuffling
+ """
+ internal_name = "booksanity"
+ display_name = "Booksanity"
+ default = 2
+ option_none = 0
+ option_power = 1
+ option_power_skill = 2
+ option_all = 3
+
+
+class Walnutsanity(OptionSet):
+ """Shuffle walnuts?
+ Puzzles: Walnuts obtained from solving a special puzzle or winning a minigame
+ Bushes: Walnuts that are in a bush and can be collected by clicking it
+ Dig spots: Walnuts that are underground and must be digged up. Includes Journal scrap walnuts
+ Repeatables: Random chance walnuts from normal actions (fishing, farming, combat, etc)
+ """
+ internal_name = "walnutsanity"
+ display_name = "Walnutsanity"
+ valid_keys = frozenset({OptionName.walnutsanity_puzzles, OptionName.walnutsanity_bushes, OptionName.walnutsanity_dig_spots,
+ OptionName.walnutsanity_repeatables, })
+ preset_none = frozenset()
+ preset_all = valid_keys
+ default = preset_none
+
+ def __eq__(self, other: typing.Any) -> bool:
+ if isinstance(other, OptionSet):
+ return set(self.value) == other.value
+ if isinstance(other, OptionList):
+ return set(self.value) == set(other.value)
+ else:
+ return typing.cast(bool, self.value == other)
+
+
class NumberOfMovementBuffs(Range):
"""Number of movement speed buffs to the player that exist as items in the pool.
Each movement speed buff is a +25% multiplier that stacks additively"""
@@ -409,15 +608,26 @@ class NumberOfMovementBuffs(Range):
# step = 1
-class NumberOfLuckBuffs(Range):
- """Number of luck buffs to the player that exist as items in the pool.
- Each luck buff is a bonus to daily luck of 0.025"""
- internal_name = "luck_buff_number"
- display_name = "Number of Luck Buffs"
- range_start = 0
- range_end = 12
- default = 4
- # step = 1
+class EnabledFillerBuffs(OptionSet):
+ """Enable various permanent player buffs to roll as filler items
+ Luck: Increase daily luck
+ Damage: Increased Damage %
+ Defense: Increased Defense
+ Immunity: Increased Immunity
+ Health: Increased Max Health
+ Energy: Increased Max Energy
+ Bite Rate: Shorter delay to get a bite when fishing
+ Fish Trap: Effect similar to the Trap Bobber, but weaker
+ Fishing Bar Size: Increased Fishing Bar Size
+ """
+ internal_name = "enabled_filler_buffs"
+ display_name = "Enabled Filler Buffs"
+ valid_keys = frozenset({OptionName.buff_luck, OptionName.buff_damage, OptionName.buff_defense, OptionName.buff_immunity, OptionName.buff_health,
+ OptionName.buff_energy, OptionName.buff_bite, OptionName.buff_fish_trap, OptionName.buff_fishing_bar})
+ # OptionName.buff_quality, OptionName.buff_glow}) # Disabled these two buffs because they are too hard to make on the mod side
+ preset_none = frozenset()
+ preset_all = valid_keys
+ default = frozenset({OptionName.buff_luck, OptionName.buff_defense, OptionName.buff_bite})
class ExcludeGingerIsland(Toggle):
@@ -431,6 +641,7 @@ class ExcludeGingerIsland(Toggle):
class TrapItems(Choice):
"""When rolling filler items, including resource packs, the game can also roll trap items.
+ Trap items are negative items that cause problems or annoyances for the player
This setting is for choosing if traps will be in the item pool, and if so, how punishing they will be.
"""
internal_name = "trap_items"
@@ -451,7 +662,7 @@ class MultipleDaySleepEnabled(Toggle):
default = 1
-class MultipleDaySleepCost(SpecialRange):
+class MultipleDaySleepCost(NamedRange):
"""How much gold it will cost to use MultiSleep. You will have to pay that amount for each day skipped."""
internal_name = "multiple_day_sleep_cost"
display_name = "Multiple Day Sleep Cost"
@@ -461,14 +672,16 @@ class MultipleDaySleepCost(SpecialRange):
special_range_names = {
"free": 0,
- "cheap": 25,
- "medium": 50,
- "expensive": 100,
+ "cheap": 10,
+ "medium": 25,
+ "expensive": 50,
+ "very expensive": 100,
}
-class ExperienceMultiplier(SpecialRange):
- """How fast you want to earn skill experience. A lower setting mean less experience.
+class ExperienceMultiplier(NamedRange):
+ """How fast you want to earn skill experience.
+ A lower setting mean less experience.
A higher setting means more experience."""
internal_name = "experience_multiplier"
display_name = "Experience Multiplier"
@@ -486,7 +699,7 @@ class ExperienceMultiplier(SpecialRange):
}
-class FriendshipMultiplier(SpecialRange):
+class FriendshipMultiplier(NamedRange):
"""How fast you want to earn friendship points with villagers.
A lower setting mean less friendship per action.
A higher setting means more friendship per action."""
@@ -533,76 +746,91 @@ class QuickStart(Toggle):
class Gifting(Toggle):
- """Do you want to enable gifting items to and from other Stardew Valley worlds?"""
+ """Do you want to enable gifting items to and from other Archipelago slots?
+ Items can only be sent to games that also support gifting"""
internal_name = "gifting"
display_name = "Gifting"
default = 1
+# These mods have been disabled because either they are not updated for the current supported version of Stardew Valley,
+# or we didn't find the time to validate that they work or fix compatibility issues if they do.
+# Once a mod is validated to be functional, it can simply be removed from this list
+disabled_mods = {ModNames.deepwoods, ModNames.magic,
+ ModNames.cooking_skill,
+ ModNames.yoba, ModNames.eugene,
+ ModNames.wellwick, ModNames.shiko, ModNames.delores, ModNames.riley,
+ ModNames.boarding_house}
+
+
+if 'unittest' in sys.modules.keys() or 'pytest' in sys.modules.keys():
+ disabled_mods = {}
+
+
class Mods(OptionSet):
- """List of mods that will be considered for shuffling."""
+ """List of mods that will be included in the shuffling."""
internal_name = "mods"
display_name = "Mods"
- valid_keys = {
- ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
- ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
- ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
- ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
- ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
- ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator
- }
+ valid_keys = {ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
+ ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
+ ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
+ ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
+ ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
+ ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator, ModNames.sve, ModNames.distant_lands,
+ ModNames.alecto, ModNames.lacey, ModNames.boarding_house}.difference(disabled_mods)
+
+class BundlePlando(OptionSet):
+ """If using Remixed bundles, this guarantees some of them will show up in your community center.
+ If more bundles are specified than what fits in their parent room, that room will randomly pick from only the plando ones"""
+ internal_name = "bundle_plando"
+ display_name = "Bundle Plando"
+ visibility = Visibility.template | Visibility.spoiler
+ valid_keys = set(all_cc_bundle_names)
-stardew_valley_option_classes = [
- Goal,
- StartingMoney,
- ProfitMargin,
- BundleRandomization,
- BundlePrice,
- EntranceRandomization,
- SeasonRandomization,
- Cropsanity,
- BackpackProgression,
- ToolProgression,
- SkillProgression,
- BuildingProgression,
- FestivalLocations,
- ElevatorProgression,
- ArcadeMachineLocations,
- SpecialOrderLocations,
- HelpWantedLocations,
- Fishsanity,
- Museumsanity,
- Friendsanity,
- FriendsanityHeartSize,
- NumberOfMovementBuffs,
- NumberOfLuckBuffs,
- ExcludeGingerIsland,
- TrapItems,
- MultipleDaySleepEnabled,
- MultipleDaySleepCost,
- ExperienceMultiplier,
- FriendshipMultiplier,
- DebrisMultiplier,
- QuickStart,
- Gifting,
- Mods,
-]
-stardew_valley_options: Dict[str, type(Option)] = {option.internal_name: option for option in
- stardew_valley_option_classes}
-default_options = {option.internal_name: option.default for option in stardew_valley_options.values()}
-stardew_valley_options["death_link"] = DeathLink
-
-
-def fetch_options(world, player: int) -> StardewOptions:
- return StardewOptions({option: get_option_value(world, player, option) for option in stardew_valley_options})
-
-
-def get_option_value(world, player: int, name: str) -> Union[bool, int]:
- assert name in stardew_valley_options, f"{name} is not a valid option for Stardew Valley."
-
- value = getattr(world, name)
-
- if issubclass(stardew_valley_options[name], Toggle):
- return bool(value[player].value)
- return value[player].value
+
+@dataclass
+class StardewValleyOptions(PerGameCommonOptions):
+ goal: Goal
+ farm_type: FarmType
+ bundle_randomization: BundleRandomization
+ bundle_price: BundlePrice
+ entrance_randomization: EntranceRandomization
+ season_randomization: SeasonRandomization
+ cropsanity: Cropsanity
+ backpack_progression: BackpackProgression
+ tool_progression: ToolProgression
+ skill_progression: SkillProgression
+ building_progression: BuildingProgression
+ festival_locations: FestivalLocations
+ elevator_progression: ElevatorProgression
+ arcade_machine_locations: ArcadeMachineLocations
+ special_order_locations: SpecialOrderLocations
+ quest_locations: QuestLocations
+ fishsanity: Fishsanity
+ museumsanity: Museumsanity
+ monstersanity: Monstersanity
+ shipsanity: Shipsanity
+ cooksanity: Cooksanity
+ chefsanity: Chefsanity
+ craftsanity: Craftsanity
+ friendsanity: Friendsanity
+ friendsanity_heart_size: FriendsanityHeartSize
+ booksanity: Booksanity
+ walnutsanity: Walnutsanity
+ exclude_ginger_island: ExcludeGingerIsland
+ quick_start: QuickStart
+ starting_money: StartingMoney
+ profit_margin: ProfitMargin
+ experience_multiplier: ExperienceMultiplier
+ friendship_multiplier: FriendshipMultiplier
+ debris_multiplier: DebrisMultiplier
+ movement_buff_number: NumberOfMovementBuffs
+ enabled_filler_buffs: EnabledFillerBuffs
+ trap_items: TrapItems
+ multiple_day_sleep_enabled: MultipleDaySleepEnabled
+ multiple_day_sleep_cost: MultipleDaySleepCost
+ gifting: Gifting
+ mods: Mods
+ bundle_plando: BundlePlando
+ death_link: DeathLink
diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py
new file mode 100644
index 000000000000..cf6f87a1501c
--- /dev/null
+++ b/worlds/stardew_valley/presets.py
@@ -0,0 +1,392 @@
+from typing import Any, Dict
+
+from Options import Accessibility, ProgressionBalancing, DeathLink
+from .options import Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, EntranceRandomization, SeasonRandomization, Cropsanity, \
+ BackpackProgression, ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, FestivalLocations, ArcadeMachineLocations, \
+ SpecialOrderLocations, QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, NumberOfMovementBuffs, ExcludeGingerIsland, TrapItems, \
+ MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, \
+ Gifting, FarmType, Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, Booksanity, Walnutsanity, EnabledFillerBuffs
+
+# @formatter:off
+from .strings.ap_names.ap_option_names import OptionName
+
+all_random_settings = {
+ "progression_balancing": "random",
+ "accessibility": "random",
+ Goal.internal_name: "random",
+ FarmType.internal_name: "random",
+ StartingMoney.internal_name: "random",
+ ProfitMargin.internal_name: "random",
+ BundleRandomization.internal_name: "random",
+ BundlePrice.internal_name: "random",
+ EntranceRandomization.internal_name: "random",
+ SeasonRandomization.internal_name: "random",
+ Cropsanity.internal_name: "random",
+ BackpackProgression.internal_name: "random",
+ ToolProgression.internal_name: "random",
+ ElevatorProgression.internal_name: "random",
+ SkillProgression.internal_name: "random",
+ BuildingProgression.internal_name: "random",
+ FestivalLocations.internal_name: "random",
+ ArcadeMachineLocations.internal_name: "random",
+ SpecialOrderLocations.internal_name: "random",
+ QuestLocations.internal_name: "random",
+ Fishsanity.internal_name: "random",
+ Museumsanity.internal_name: "random",
+ Monstersanity.internal_name: "random",
+ Shipsanity.internal_name: "random",
+ Cooksanity.internal_name: "random",
+ Chefsanity.internal_name: "random",
+ Craftsanity.internal_name: "random",
+ Friendsanity.internal_name: "random",
+ FriendsanityHeartSize.internal_name: "random",
+ Booksanity.internal_name: "random",
+ Walnutsanity.internal_name: "random",
+ NumberOfMovementBuffs.internal_name: "random",
+ EnabledFillerBuffs.internal_name: "random",
+ ExcludeGingerIsland.internal_name: "random",
+ TrapItems.internal_name: "random",
+ MultipleDaySleepEnabled.internal_name: "random",
+ MultipleDaySleepCost.internal_name: "random",
+ ExperienceMultiplier.internal_name: "random",
+ FriendshipMultiplier.internal_name: "random",
+ DebrisMultiplier.internal_name: "random",
+ QuickStart.internal_name: "random",
+ Gifting.internal_name: "random",
+ "death_link": "random",
+}
+
+easy_settings = {
+ "progression_balancing": ProgressionBalancing.default,
+ "accessibility": Accessibility.option_full,
+ Goal.internal_name: Goal.option_community_center,
+ FarmType.internal_name: "random",
+ StartingMoney.internal_name: "very rich",
+ ProfitMargin.internal_name: "double",
+ BundleRandomization.internal_name: BundleRandomization.option_thematic,
+ BundlePrice.internal_name: BundlePrice.option_cheap,
+ EntranceRandomization.internal_name: EntranceRandomization.option_disabled,
+ SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter,
+ Cropsanity.internal_name: Cropsanity.option_enabled,
+ BackpackProgression.internal_name: BackpackProgression.option_early_progressive,
+ ToolProgression.internal_name: ToolProgression.option_progressive_very_cheap,
+ ElevatorProgression.internal_name: ElevatorProgression.option_progressive,
+ SkillProgression.internal_name: SkillProgression.option_progressive,
+ BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap,
+ FestivalLocations.internal_name: FestivalLocations.option_easy,
+ ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla_very_short,
+ QuestLocations.internal_name: "minimum",
+ Fishsanity.internal_name: Fishsanity.option_only_easy_fish,
+ Museumsanity.internal_name: Museumsanity.option_milestones,
+ Monstersanity.internal_name: Monstersanity.option_one_per_category,
+ Shipsanity.internal_name: Shipsanity.option_none,
+ Cooksanity.internal_name: Cooksanity.option_none,
+ Chefsanity.internal_name: Chefsanity.option_none,
+ Craftsanity.internal_name: Craftsanity.option_none,
+ Friendsanity.internal_name: Friendsanity.option_none,
+ FriendsanityHeartSize.internal_name: 4,
+ Booksanity.internal_name: Booksanity.option_none,
+ Walnutsanity.internal_name: Walnutsanity.preset_none,
+ NumberOfMovementBuffs.internal_name: 8,
+ EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
+ TrapItems.internal_name: TrapItems.option_easy,
+ MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true,
+ MultipleDaySleepCost.internal_name: "free",
+ ExperienceMultiplier.internal_name: "triple",
+ FriendshipMultiplier.internal_name: "quadruple",
+ DebrisMultiplier.internal_name: DebrisMultiplier.option_quarter,
+ QuickStart.internal_name: QuickStart.option_true,
+ Gifting.internal_name: Gifting.option_true,
+ "death_link": "false",
+}
+
+medium_settings = {
+ "progression_balancing": 25,
+ "accessibility": Accessibility.option_full,
+ Goal.internal_name: Goal.option_community_center,
+ FarmType.internal_name: "random",
+ StartingMoney.internal_name: "rich",
+ ProfitMargin.internal_name: 150,
+ BundleRandomization.internal_name: BundleRandomization.option_remixed,
+ BundlePrice.internal_name: BundlePrice.option_normal,
+ EntranceRandomization.internal_name: EntranceRandomization.option_non_progression,
+ SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
+ Cropsanity.internal_name: Cropsanity.option_enabled,
+ BackpackProgression.internal_name: BackpackProgression.option_early_progressive,
+ ToolProgression.internal_name: ToolProgression.option_progressive_cheap,
+ ElevatorProgression.internal_name: ElevatorProgression.option_progressive,
+ SkillProgression.internal_name: SkillProgression.option_progressive,
+ BuildingProgression.internal_name: BuildingProgression.option_progressive_cheap,
+ FestivalLocations.internal_name: FestivalLocations.option_hard,
+ ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories_easy,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_short,
+ QuestLocations.internal_name: "normal",
+ Fishsanity.internal_name: Fishsanity.option_exclude_legendaries,
+ Museumsanity.internal_name: Museumsanity.option_milestones,
+ Monstersanity.internal_name: Monstersanity.option_one_per_monster,
+ Shipsanity.internal_name: Shipsanity.option_none,
+ Cooksanity.internal_name: Cooksanity.option_none,
+ Chefsanity.internal_name: Chefsanity.option_queen_of_sauce,
+ Craftsanity.internal_name: Craftsanity.option_none,
+ Friendsanity.internal_name: Friendsanity.option_starting_npcs,
+ FriendsanityHeartSize.internal_name: 4,
+ Booksanity.internal_name: Booksanity.option_power_skill,
+ Walnutsanity.internal_name: [OptionName.walnutsanity_puzzles],
+ NumberOfMovementBuffs.internal_name: 6,
+ EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
+ TrapItems.internal_name: TrapItems.option_medium,
+ MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true,
+ MultipleDaySleepCost.internal_name: "free",
+ ExperienceMultiplier.internal_name: "double",
+ FriendshipMultiplier.internal_name: "triple",
+ DebrisMultiplier.internal_name: DebrisMultiplier.option_half,
+ QuickStart.internal_name: QuickStart.option_true,
+ Gifting.internal_name: Gifting.option_true,
+ "death_link": "false",
+}
+
+hard_settings = {
+ "progression_balancing": 0,
+ "accessibility": Accessibility.option_full,
+ Goal.internal_name: Goal.option_grandpa_evaluation,
+ FarmType.internal_name: "random",
+ StartingMoney.internal_name: "extra",
+ ProfitMargin.internal_name: "normal",
+ BundleRandomization.internal_name: BundleRandomization.option_remixed,
+ BundlePrice.internal_name: BundlePrice.option_expensive,
+ EntranceRandomization.internal_name: EntranceRandomization.option_buildings_without_house,
+ SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
+ Cropsanity.internal_name: Cropsanity.option_enabled,
+ BackpackProgression.internal_name: BackpackProgression.option_progressive,
+ ToolProgression.internal_name: ToolProgression.option_progressive,
+ ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor,
+ SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
+ BuildingProgression.internal_name: BuildingProgression.option_progressive,
+ FestivalLocations.internal_name: FestivalLocations.option_hard,
+ ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi_short,
+ QuestLocations.internal_name: "lots",
+ Fishsanity.internal_name: Fishsanity.option_all,
+ Museumsanity.internal_name: Museumsanity.option_all,
+ Monstersanity.internal_name: Monstersanity.option_progressive_goals,
+ Shipsanity.internal_name: Shipsanity.option_crops,
+ Cooksanity.internal_name: Cooksanity.option_queen_of_sauce,
+ Chefsanity.internal_name: Chefsanity.option_qos_and_purchases,
+ Craftsanity.internal_name: Craftsanity.option_none,
+ Friendsanity.internal_name: Friendsanity.option_all,
+ FriendsanityHeartSize.internal_name: 4,
+ Booksanity.internal_name: Booksanity.option_all,
+ Walnutsanity.internal_name: Walnutsanity.preset_all,
+ NumberOfMovementBuffs.internal_name: 4,
+ EnabledFillerBuffs.internal_name: EnabledFillerBuffs.default,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
+ TrapItems.internal_name: TrapItems.option_hard,
+ MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true,
+ MultipleDaySleepCost.internal_name: "cheap",
+ ExperienceMultiplier.internal_name: "vanilla",
+ FriendshipMultiplier.internal_name: "double",
+ DebrisMultiplier.internal_name: DebrisMultiplier.option_vanilla,
+ QuickStart.internal_name: QuickStart.option_true,
+ Gifting.internal_name: Gifting.option_true,
+ "death_link": "true",
+}
+
+nightmare_settings = {
+ "progression_balancing": 0,
+ "accessibility": Accessibility.option_full,
+ Goal.internal_name: Goal.option_community_center,
+ FarmType.internal_name: "random",
+ StartingMoney.internal_name: "vanilla",
+ ProfitMargin.internal_name: "half",
+ BundleRandomization.internal_name: BundleRandomization.option_shuffled,
+ BundlePrice.internal_name: BundlePrice.option_very_expensive,
+ EntranceRandomization.internal_name: EntranceRandomization.option_buildings,
+ SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
+ Cropsanity.internal_name: Cropsanity.option_enabled,
+ BackpackProgression.internal_name: BackpackProgression.option_progressive,
+ ToolProgression.internal_name: ToolProgression.option_progressive,
+ ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor,
+ SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
+ BuildingProgression.internal_name: BuildingProgression.option_progressive,
+ FestivalLocations.internal_name: FestivalLocations.option_hard,
+ ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
+ QuestLocations.internal_name: "maximum",
+ Fishsanity.internal_name: Fishsanity.option_special,
+ Museumsanity.internal_name: Museumsanity.option_all,
+ Monstersanity.internal_name: Monstersanity.option_split_goals,
+ Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish,
+ Cooksanity.internal_name: Cooksanity.option_queen_of_sauce,
+ Chefsanity.internal_name: Chefsanity.option_qos_and_purchases,
+ Craftsanity.internal_name: Craftsanity.option_none,
+ Friendsanity.internal_name: Friendsanity.option_all_with_marriage,
+ FriendsanityHeartSize.internal_name: 4,
+ Booksanity.internal_name: Booksanity.option_all,
+ Walnutsanity.internal_name: Walnutsanity.preset_all,
+ NumberOfMovementBuffs.internal_name: 2,
+ EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_none,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
+ TrapItems.internal_name: TrapItems.option_hell,
+ MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true,
+ MultipleDaySleepCost.internal_name: "expensive",
+ ExperienceMultiplier.internal_name: "half",
+ FriendshipMultiplier.internal_name: "vanilla",
+ DebrisMultiplier.internal_name: DebrisMultiplier.option_vanilla,
+ QuickStart.internal_name: QuickStart.option_false,
+ Gifting.internal_name: Gifting.option_true,
+ "death_link": "true",
+}
+
+short_settings = {
+ "progression_balancing": ProgressionBalancing.default,
+ "accessibility": Accessibility.option_full,
+ Goal.internal_name: Goal.option_bottom_of_the_mines,
+ FarmType.internal_name: "random",
+ StartingMoney.internal_name: "filthy rich",
+ ProfitMargin.internal_name: "quadruple",
+ BundleRandomization.internal_name: BundleRandomization.option_remixed,
+ BundlePrice.internal_name: BundlePrice.option_minimum,
+ EntranceRandomization.internal_name: EntranceRandomization.option_disabled,
+ SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter,
+ Cropsanity.internal_name: Cropsanity.option_disabled,
+ BackpackProgression.internal_name: BackpackProgression.option_early_progressive,
+ ToolProgression.internal_name: ToolProgression.option_progressive_very_cheap,
+ ElevatorProgression.internal_name: ElevatorProgression.option_progressive,
+ SkillProgression.internal_name: SkillProgression.option_progressive,
+ BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap,
+ FestivalLocations.internal_name: FestivalLocations.option_disabled,
+ ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla_very_short,
+ QuestLocations.internal_name: "none",
+ Fishsanity.internal_name: Fishsanity.option_none,
+ Museumsanity.internal_name: Museumsanity.option_none,
+ Monstersanity.internal_name: Monstersanity.option_none,
+ Shipsanity.internal_name: Shipsanity.option_none,
+ Cooksanity.internal_name: Cooksanity.option_none,
+ Chefsanity.internal_name: Chefsanity.option_none,
+ Craftsanity.internal_name: Craftsanity.option_none,
+ Friendsanity.internal_name: Friendsanity.option_none,
+ FriendsanityHeartSize.internal_name: 4,
+ Booksanity.internal_name: Booksanity.option_none,
+ Walnutsanity.internal_name: Walnutsanity.preset_none,
+ NumberOfMovementBuffs.internal_name: 10,
+ EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
+ TrapItems.internal_name: TrapItems.option_easy,
+ MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true,
+ MultipleDaySleepCost.internal_name: "free",
+ ExperienceMultiplier.internal_name: "quadruple",
+ FriendshipMultiplier.internal_name: 800,
+ DebrisMultiplier.internal_name: DebrisMultiplier.option_none,
+ QuickStart.internal_name: QuickStart.option_true,
+ Gifting.internal_name: Gifting.option_true,
+ "death_link": "false",
+}
+
+minsanity_settings = {
+ "progression_balancing": ProgressionBalancing.default,
+ "accessibility": Accessibility.option_minimal,
+ Goal.internal_name: Goal.default,
+ FarmType.internal_name: "random",
+ StartingMoney.internal_name: StartingMoney.default,
+ ProfitMargin.internal_name: ProfitMargin.default,
+ BundleRandomization.internal_name: BundleRandomization.default,
+ BundlePrice.internal_name: BundlePrice.default,
+ EntranceRandomization.internal_name: EntranceRandomization.default,
+ SeasonRandomization.internal_name: SeasonRandomization.option_disabled,
+ Cropsanity.internal_name: Cropsanity.option_disabled,
+ BackpackProgression.internal_name: BackpackProgression.option_vanilla,
+ ToolProgression.internal_name: ToolProgression.option_vanilla,
+ ElevatorProgression.internal_name: ElevatorProgression.option_vanilla,
+ SkillProgression.internal_name: SkillProgression.option_vanilla,
+ BuildingProgression.internal_name: BuildingProgression.option_vanilla,
+ FestivalLocations.internal_name: FestivalLocations.option_disabled,
+ ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla_very_short,
+ QuestLocations.internal_name: "none",
+ Fishsanity.internal_name: Fishsanity.option_none,
+ Museumsanity.internal_name: Museumsanity.option_none,
+ Monstersanity.internal_name: Monstersanity.option_none,
+ Shipsanity.internal_name: Shipsanity.option_none,
+ Cooksanity.internal_name: Cooksanity.option_none,
+ Chefsanity.internal_name: Chefsanity.option_none,
+ Craftsanity.internal_name: Craftsanity.option_none,
+ Friendsanity.internal_name: Friendsanity.option_none,
+ FriendsanityHeartSize.internal_name: FriendsanityHeartSize.default,
+ Booksanity.internal_name: Booksanity.option_none,
+ Walnutsanity.internal_name: Walnutsanity.preset_none,
+ NumberOfMovementBuffs.internal_name: NumberOfMovementBuffs.default,
+ EnabledFillerBuffs.internal_name: EnabledFillerBuffs.default,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
+ TrapItems.internal_name: TrapItems.default,
+ MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.default,
+ MultipleDaySleepCost.internal_name: MultipleDaySleepCost.default,
+ ExperienceMultiplier.internal_name: ExperienceMultiplier.default,
+ FriendshipMultiplier.internal_name: FriendshipMultiplier.default,
+ DebrisMultiplier.internal_name: DebrisMultiplier.default,
+ QuickStart.internal_name: QuickStart.default,
+ Gifting.internal_name: Gifting.default,
+ "death_link": DeathLink.default,
+}
+
+allsanity_settings = {
+ "progression_balancing": ProgressionBalancing.default,
+ "accessibility": Accessibility.option_full,
+ Goal.internal_name: Goal.default,
+ FarmType.internal_name: "random",
+ StartingMoney.internal_name: StartingMoney.default,
+ ProfitMargin.internal_name: ProfitMargin.default,
+ BundleRandomization.internal_name: BundleRandomization.default,
+ BundlePrice.internal_name: BundlePrice.default,
+ EntranceRandomization.internal_name: EntranceRandomization.option_buildings,
+ SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
+ Cropsanity.internal_name: Cropsanity.option_enabled,
+ BackpackProgression.internal_name: BackpackProgression.option_early_progressive,
+ ToolProgression.internal_name: ToolProgression.option_progressive,
+ ElevatorProgression.internal_name: ElevatorProgression.option_progressive,
+ SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
+ BuildingProgression.internal_name: BuildingProgression.option_progressive,
+ FestivalLocations.internal_name: FestivalLocations.option_hard,
+ ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
+ QuestLocations.internal_name: "maximum",
+ Fishsanity.internal_name: Fishsanity.option_all,
+ Museumsanity.internal_name: Museumsanity.option_all,
+ Monstersanity.internal_name: Monstersanity.option_progressive_goals,
+ Shipsanity.internal_name: Shipsanity.option_everything,
+ Cooksanity.internal_name: Cooksanity.option_all,
+ Chefsanity.internal_name: Chefsanity.option_all,
+ Craftsanity.internal_name: Craftsanity.option_all,
+ Friendsanity.internal_name: Friendsanity.option_all,
+ FriendsanityHeartSize.internal_name: 1,
+ Booksanity.internal_name: Booksanity.option_all,
+ Walnutsanity.internal_name: Walnutsanity.preset_all,
+ NumberOfMovementBuffs.internal_name: 12,
+ EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
+ TrapItems.internal_name: TrapItems.default,
+ MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.default,
+ MultipleDaySleepCost.internal_name: MultipleDaySleepCost.default,
+ ExperienceMultiplier.internal_name: ExperienceMultiplier.default,
+ FriendshipMultiplier.internal_name: FriendshipMultiplier.default,
+ DebrisMultiplier.internal_name: DebrisMultiplier.default,
+ QuickStart.internal_name: QuickStart.default,
+ Gifting.internal_name: Gifting.default,
+ "death_link": DeathLink.default,
+}
+# @formatter:on
+
+
+sv_options_presets: Dict[str, Dict[str, Any]] = {
+ "All random": all_random_settings,
+ "Easy": easy_settings,
+ "Medium": medium_settings,
+ "Hard": hard_settings,
+ "Nightmare": nightmare_settings,
+ "Short": short_settings,
+ "Minsanity": minsanity_settings,
+ "Allsanity": allsanity_settings,
+}
diff --git a/worlds/stardew_valley/region_classes.py b/worlds/stardew_valley/region_classes.py
index 9db322416a4d..bd64518ea153 100644
--- a/worlds/stardew_valley/region_classes.py
+++ b/worlds/stardew_valley/region_classes.py
@@ -1,25 +1,33 @@
-from enum import IntFlag
-from typing import Optional, List
+from copy import deepcopy
from dataclasses import dataclass, field
+from enum import IntFlag
+from typing import Optional, List, Set
connector_keyword = " to "
+class ModificationFlag(IntFlag):
+ NOT_MODIFIED = 0
+ MODIFIED = 1
+
+
class RandomizationFlag(IntFlag):
NOT_RANDOMIZED = 0b0
- PELICAN_TOWN = 0b11111
- NON_PROGRESSION = 0b11110
- BUILDINGS = 0b11100
- EVERYTHING = 0b11000
- CHAOS = 0b10000
- GINGER_ISLAND = 0b0100000
- LEAD_TO_OPEN_AREA = 0b1000000
+ PELICAN_TOWN = 0b00011111
+ NON_PROGRESSION = 0b00011110
+ BUILDINGS = 0b00011100
+ EVERYTHING = 0b00011000
+ GINGER_ISLAND = 0b00100000
+ LEAD_TO_OPEN_AREA = 0b01000000
+ MASTERIES = 0b10000000
@dataclass(frozen=True)
class RegionData:
name: str
exits: List[str] = field(default_factory=list)
+ flag: ModificationFlag = ModificationFlag.NOT_MODIFIED
+ is_ginger_island: bool = False
def get_merged_with(self, exits: List[str]):
merged_exits = []
@@ -27,10 +35,14 @@ def get_merged_with(self, exits: List[str]):
if exits is not None:
merged_exits.extend(exits)
merged_exits = list(set(merged_exits))
- return RegionData(self.name, merged_exits)
+ return RegionData(self.name, merged_exits, is_ginger_island=self.is_ginger_island)
+
+ def get_without_exits(self, exits_to_remove: Set[str]):
+ exits = [exit_ for exit_ in self.exits if exit_ not in exits_to_remove]
+ return RegionData(self.name, exits, is_ginger_island=self.is_ginger_island)
def get_clone(self):
- return self.get_merged_with(None)
+ return deepcopy(self)
@dataclass(frozen=True)
@@ -53,6 +65,3 @@ class ModRegionData:
mod_name: str
regions: List[RegionData]
connections: List[ConnectionData]
-
-
-
diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py
index 60cad4c136fc..b0fc7fa0ea52 100644
--- a/worlds/stardew_valley/regions.py
+++ b/worlds/stardew_valley/regions.py
@@ -2,12 +2,11 @@
from typing import Iterable, Dict, Protocol, List, Tuple, Set
from BaseClasses import Region, Entrance
-from . import options
-from .strings.entrance_names import Entrance
-from .strings.region_names import Region
-from .region_classes import RegionData, ConnectionData, RandomizationFlag
-from .options import StardewOptions
-from .mods.mod_regions import ModDataList
+from .mods.mod_regions import ModDataList, vanilla_connections_to_remove_by_mod
+from .options import EntranceRandomization, ExcludeGingerIsland, StardewValleyOptions, SkillProgression
+from .region_classes import RegionData, ConnectionData, RandomizationFlag, ModificationFlag
+from .strings.entrance_names import Entrance, LogicEntrance
+from .strings.region_names import Region, LogicRegion
class RegionFactory(Protocol):
@@ -18,54 +17,57 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region:
vanilla_regions = [
RegionData(Region.menu, [Entrance.to_stardew_valley]),
RegionData(Region.stardew_valley, [Entrance.to_farmhouse]),
- RegionData(Region.farm_house, [Entrance.farmhouse_to_farm, Entrance.downstairs_to_cellar]),
+ RegionData(Region.farm_house,
+ [Entrance.farmhouse_to_farm, Entrance.downstairs_to_cellar, LogicEntrance.farmhouse_cooking, LogicEntrance.watch_queen_of_sauce]),
RegionData(Region.cellar),
RegionData(Region.farm,
- [Entrance.farm_to_backwoods, Entrance.farm_to_bus_stop, Entrance.farm_to_forest,
- Entrance.farm_to_farmcave, Entrance.enter_greenhouse,
- Entrance.use_desert_obelisk, Entrance.use_island_obelisk]),
+ [Entrance.farm_to_backwoods, Entrance.farm_to_bus_stop, Entrance.farm_to_forest, Entrance.farm_to_farmcave, Entrance.enter_greenhouse,
+ Entrance.enter_coop, Entrance.enter_barn, Entrance.enter_shed, Entrance.enter_slime_hutch, LogicEntrance.grow_spring_crops,
+ LogicEntrance.grow_summer_crops, LogicEntrance.grow_fall_crops, LogicEntrance.grow_winter_crops, LogicEntrance.shipping]),
RegionData(Region.backwoods, [Entrance.backwoods_to_mountain]),
RegionData(Region.bus_stop,
[Entrance.bus_stop_to_town, Entrance.take_bus_to_desert, Entrance.bus_stop_to_tunnel_entrance]),
RegionData(Region.forest,
- [Entrance.forest_to_town, Entrance.enter_secret_woods, Entrance.forest_to_wizard_tower,
- Entrance.forest_to_marnie_ranch,
- Entrance.forest_to_leah_cottage, Entrance.forest_to_sewer,
- Entrance.buy_from_traveling_merchant]),
- RegionData(Region.traveling_cart),
+ [Entrance.forest_to_town, Entrance.enter_secret_woods, Entrance.forest_to_wizard_tower, Entrance.forest_to_marnie_ranch,
+ Entrance.forest_to_leah_cottage, Entrance.forest_to_sewer, Entrance.forest_to_mastery_cave, LogicEntrance.buy_from_traveling_merchant,
+ LogicEntrance.complete_raccoon_requests, LogicEntrance.fish_in_waterfall, LogicEntrance.attend_flower_dance, LogicEntrance.attend_trout_derby,
+ LogicEntrance.attend_festival_of_ice]),
+ RegionData(LogicRegion.forest_waterfall),
RegionData(Region.farm_cave),
- RegionData(Region.greenhouse),
+ RegionData(Region.greenhouse,
+ [LogicEntrance.grow_spring_crops_in_greenhouse, LogicEntrance.grow_summer_crops_in_greenhouse, LogicEntrance.grow_fall_crops_in_greenhouse,
+ LogicEntrance.grow_winter_crops_in_greenhouse, LogicEntrance.grow_indoor_crops_in_greenhouse]),
RegionData(Region.mountain,
[Entrance.mountain_to_railroad, Entrance.mountain_to_tent, Entrance.mountain_to_carpenter_shop,
Entrance.mountain_to_the_mines, Entrance.enter_quarry, Entrance.mountain_to_adventurer_guild,
Entrance.mountain_to_town, Entrance.mountain_to_maru_room,
Entrance.mountain_to_leo_treehouse]),
- RegionData(Region.leo_treehouse),
+ RegionData(Region.leo_treehouse, is_ginger_island=True),
RegionData(Region.maru_room),
RegionData(Region.tunnel_entrance, [Entrance.tunnel_entrance_to_bus_tunnel]),
RegionData(Region.bus_tunnel),
RegionData(Region.town,
- [Entrance.town_to_community_center, Entrance.town_to_beach, Entrance.town_to_hospital,
- Entrance.town_to_pierre_general_store, Entrance.town_to_saloon, Entrance.town_to_alex_house,
- Entrance.town_to_trailer,
- Entrance.town_to_mayor_manor,
- Entrance.town_to_sam_house, Entrance.town_to_haley_house, Entrance.town_to_sewer,
- Entrance.town_to_clint_blacksmith,
- Entrance.town_to_museum,
- Entrance.town_to_jojamart]),
+ [Entrance.town_to_community_center, Entrance.town_to_beach, Entrance.town_to_hospital, Entrance.town_to_pierre_general_store,
+ Entrance.town_to_saloon, Entrance.town_to_alex_house, Entrance.town_to_trailer, Entrance.town_to_mayor_manor, Entrance.town_to_sam_house,
+ Entrance.town_to_haley_house, Entrance.town_to_sewer, Entrance.town_to_clint_blacksmith, Entrance.town_to_museum, Entrance.town_to_jojamart,
+ Entrance.purchase_movie_ticket, LogicEntrance.buy_experience_books, LogicEntrance.attend_egg_festival, LogicEntrance.attend_fair,
+ LogicEntrance.attend_spirit_eve, LogicEntrance.attend_winter_star]),
RegionData(Region.beach,
- [Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools]),
+ [Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools, LogicEntrance.fishing, LogicEntrance.attend_luau,
+ LogicEntrance.attend_moonlight_jellies, LogicEntrance.attend_night_market, LogicEntrance.attend_squidfest]),
RegionData(Region.railroad, [Entrance.enter_bathhouse_entrance, Entrance.enter_witch_warp_cave]),
RegionData(Region.ranch),
RegionData(Region.leah_house),
+ RegionData(Region.mastery_cave),
RegionData(Region.sewer, [Entrance.enter_mutant_bug_lair]),
RegionData(Region.mutant_bug_lair),
- RegionData(Region.wizard_tower, [Entrance.enter_wizard_basement]),
+ RegionData(Region.wizard_tower, [Entrance.enter_wizard_basement, Entrance.use_desert_obelisk, Entrance.use_island_obelisk]),
RegionData(Region.wizard_basement),
RegionData(Region.tent),
RegionData(Region.carpenter, [Entrance.enter_sebastian_room]),
RegionData(Region.sebastian_room),
- RegionData(Region.adventurer_guild),
+ RegionData(Region.adventurer_guild, [Entrance.adventurer_guild_to_bedroom]),
+ RegionData(Region.adventurer_guild_bedroom),
RegionData(Region.community_center,
[Entrance.access_crafts_room, Entrance.access_pantry, Entrance.access_fish_tank,
Entrance.access_boiler_room, Entrance.access_bulletin_board, Entrance.access_vault]),
@@ -80,16 +82,25 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region:
RegionData(Region.pierre_store, [Entrance.enter_sunroom]),
RegionData(Region.sunroom),
RegionData(Region.saloon, [Entrance.play_journey_of_the_prairie_king, Entrance.play_junimo_kart]),
+ RegionData(Region.jotpk_world_1, [Entrance.reach_jotpk_world_2]),
+ RegionData(Region.jotpk_world_2, [Entrance.reach_jotpk_world_3]),
+ RegionData(Region.jotpk_world_3),
+ RegionData(Region.junimo_kart_1, [Entrance.reach_junimo_kart_2]),
+ RegionData(Region.junimo_kart_2, [Entrance.reach_junimo_kart_3]),
+ RegionData(Region.junimo_kart_3),
RegionData(Region.alex_house),
RegionData(Region.trailer),
RegionData(Region.mayor_house),
RegionData(Region.sam_house),
RegionData(Region.haley_house),
- RegionData(Region.blacksmith),
+ RegionData(Region.blacksmith, [LogicEntrance.blacksmith_copper]),
RegionData(Region.museum),
- RegionData(Region.jojamart),
+ RegionData(Region.jojamart, [Entrance.enter_abandoned_jojamart]),
+ RegionData(Region.abandoned_jojamart, [Entrance.enter_movie_theater]),
+ RegionData(Region.movie_ticket_stand),
+ RegionData(Region.movie_theater),
RegionData(Region.fish_shop, [Entrance.fish_shop_to_boat_tunnel]),
- RegionData(Region.boat_tunnel, [Entrance.boat_to_ginger_island]),
+ RegionData(Region.boat_tunnel, [Entrance.boat_to_ginger_island], is_ginger_island=True),
RegionData(Region.elliott_house),
RegionData(Region.tide_pools),
RegionData(Region.bathhouse_entrance, [Entrance.enter_locker_room]),
@@ -102,105 +113,143 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region:
RegionData(Region.quarry_mine_entrance, [Entrance.enter_quarry_mine]),
RegionData(Region.quarry_mine),
RegionData(Region.secret_woods),
- RegionData(Region.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis]),
+ RegionData(Region.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis, LogicEntrance.attend_desert_festival]),
RegionData(Region.oasis, [Entrance.enter_casino]),
RegionData(Region.casino),
RegionData(Region.skull_cavern_entrance, [Entrance.enter_skull_cavern]),
- RegionData(Region.skull_cavern, [Entrance.mine_to_skull_cavern_floor_25, Entrance.mine_to_skull_cavern_floor_50,
- Entrance.mine_to_skull_cavern_floor_75, Entrance.mine_to_skull_cavern_floor_100,
- Entrance.mine_to_skull_cavern_floor_125, Entrance.mine_to_skull_cavern_floor_150,
- Entrance.mine_to_skull_cavern_floor_175, Entrance.mine_to_skull_cavern_floor_200]),
- RegionData(Region.skull_cavern_25),
- RegionData(Region.skull_cavern_50),
- RegionData(Region.skull_cavern_75),
- RegionData(Region.skull_cavern_100),
- RegionData(Region.skull_cavern_125),
- RegionData(Region.skull_cavern_150),
- RegionData(Region.skull_cavern_175),
- RegionData(Region.skull_cavern_200),
- RegionData(Region.island_south, [Entrance.island_south_to_west, Entrance.island_south_to_north,
- Entrance.island_south_to_east, Entrance.island_south_to_southeast,
- Entrance.use_island_resort,
- Entrance.parrot_express_docks_to_volcano,
- Entrance.parrot_express_docks_to_dig_site,
- Entrance.parrot_express_docks_to_jungle]),
- RegionData(Region.island_resort),
+ RegionData(Region.skull_cavern, [Entrance.mine_to_skull_cavern_floor_25]),
+ RegionData(Region.skull_cavern_25, [Entrance.mine_to_skull_cavern_floor_50]),
+ RegionData(Region.skull_cavern_50, [Entrance.mine_to_skull_cavern_floor_75]),
+ RegionData(Region.skull_cavern_75, [Entrance.mine_to_skull_cavern_floor_100]),
+ RegionData(Region.skull_cavern_100, [Entrance.mine_to_skull_cavern_floor_125]),
+ RegionData(Region.skull_cavern_125, [Entrance.mine_to_skull_cavern_floor_150]),
+ RegionData(Region.skull_cavern_150, [Entrance.mine_to_skull_cavern_floor_175]),
+ RegionData(Region.skull_cavern_175, [Entrance.mine_to_skull_cavern_floor_200]),
+ RegionData(Region.skull_cavern_200, [Entrance.enter_dangerous_skull_cavern]),
+ RegionData(Region.dangerous_skull_cavern, is_ginger_island=True),
+ RegionData(Region.island_south,
+ [Entrance.island_south_to_west, Entrance.island_south_to_north, Entrance.island_south_to_east, Entrance.island_south_to_southeast,
+ Entrance.use_island_resort, Entrance.parrot_express_docks_to_volcano, Entrance.parrot_express_docks_to_dig_site,
+ Entrance.parrot_express_docks_to_jungle],
+ is_ginger_island=True),
+ RegionData(Region.island_resort, is_ginger_island=True),
RegionData(Region.island_west,
- [Entrance.island_west_to_islandfarmhouse, Entrance.island_west_to_gourmand_cave,
- Entrance.island_west_to_crystals_cave, Entrance.island_west_to_shipwreck,
- Entrance.island_west_to_qi_walnut_room, Entrance.use_farm_obelisk,
- Entrance.parrot_express_jungle_to_docks, Entrance.parrot_express_jungle_to_dig_site,
- Entrance.parrot_express_jungle_to_volcano]),
- RegionData(Region.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine]),
- RegionData(Region.island_shrine),
- RegionData(Region.island_south_east, [Entrance.island_southeast_to_pirate_cove]),
- RegionData(Region.island_north, [Entrance.talk_to_island_trader, Entrance.island_north_to_field_office,
- Entrance.island_north_to_dig_site, Entrance.island_north_to_volcano,
- Entrance.parrot_express_volcano_to_dig_site,
- Entrance.parrot_express_volcano_to_jungle,
- Entrance.parrot_express_volcano_to_docks]),
- RegionData(Region.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach]),
- RegionData(Region.volcano_secret_beach),
- RegionData(Region.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10]),
- RegionData(Region.volcano_dwarf_shop),
- RegionData(Region.volcano_floor_10),
- RegionData(Region.island_trader),
- RegionData(Region.island_farmhouse),
- RegionData(Region.gourmand_frog_cave),
- RegionData(Region.colored_crystals_cave),
- RegionData(Region.shipwreck),
- RegionData(Region.qi_walnut_room),
- RegionData(Region.leo_hut),
- RegionData(Region.pirate_cove),
- RegionData(Region.field_office),
+ [Entrance.island_west_to_islandfarmhouse, Entrance.island_west_to_gourmand_cave, Entrance.island_west_to_crystals_cave,
+ Entrance.island_west_to_shipwreck, Entrance.island_west_to_qi_walnut_room, Entrance.use_farm_obelisk, Entrance.parrot_express_jungle_to_docks,
+ Entrance.parrot_express_jungle_to_dig_site, Entrance.parrot_express_jungle_to_volcano, LogicEntrance.grow_spring_crops_on_island,
+ LogicEntrance.grow_summer_crops_on_island, LogicEntrance.grow_fall_crops_on_island, LogicEntrance.grow_winter_crops_on_island,
+ LogicEntrance.grow_indoor_crops_on_island],
+ is_ginger_island=True),
+ RegionData(Region.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine], is_ginger_island=True),
+ RegionData(Region.island_shrine, is_ginger_island=True),
+ RegionData(Region.island_south_east, [Entrance.island_southeast_to_pirate_cove], is_ginger_island=True),
+ RegionData(Region.island_north,
+ [Entrance.talk_to_island_trader, Entrance.island_north_to_field_office, Entrance.island_north_to_dig_site, Entrance.island_north_to_volcano,
+ Entrance.parrot_express_volcano_to_dig_site, Entrance.parrot_express_volcano_to_jungle, Entrance.parrot_express_volcano_to_docks],
+ is_ginger_island=True),
+ RegionData(Region.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach], is_ginger_island=True),
+ RegionData(Region.volcano_secret_beach, is_ginger_island=True),
+ RegionData(Region.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10], is_ginger_island=True),
+ RegionData(Region.volcano_dwarf_shop, is_ginger_island=True),
+ RegionData(Region.volcano_floor_10, is_ginger_island=True),
+ RegionData(Region.island_trader, is_ginger_island=True),
+ RegionData(Region.island_farmhouse, [LogicEntrance.island_cooking], is_ginger_island=True),
+ RegionData(Region.gourmand_frog_cave, is_ginger_island=True),
+ RegionData(Region.colored_crystals_cave, is_ginger_island=True),
+ RegionData(Region.shipwreck, is_ginger_island=True),
+ RegionData(Region.qi_walnut_room, is_ginger_island=True),
+ RegionData(Region.leo_hut, is_ginger_island=True),
+ RegionData(Region.pirate_cove, is_ginger_island=True),
+ RegionData(Region.field_office, is_ginger_island=True),
RegionData(Region.dig_site,
[Entrance.dig_site_to_professor_snail_cave, Entrance.parrot_express_dig_site_to_volcano,
- Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_dig_site_to_jungle]),
- RegionData(Region.professor_snail_cave),
- RegionData(Region.jotpk_world_1, [Entrance.reach_jotpk_world_2]),
- RegionData(Region.jotpk_world_2, [Entrance.reach_jotpk_world_3]),
- RegionData(Region.jotpk_world_3),
- RegionData(Region.junimo_kart_1, [Entrance.reach_junimo_kart_2]),
- RegionData(Region.junimo_kart_2, [Entrance.reach_junimo_kart_3]),
- RegionData(Region.junimo_kart_3),
- RegionData(Region.mines, [Entrance.talk_to_mines_dwarf,
- Entrance.dig_to_mines_floor_5, Entrance.dig_to_mines_floor_10,
- Entrance.dig_to_mines_floor_15, Entrance.dig_to_mines_floor_20,
- Entrance.dig_to_mines_floor_25, Entrance.dig_to_mines_floor_30,
- Entrance.dig_to_mines_floor_35, Entrance.dig_to_mines_floor_40,
- Entrance.dig_to_mines_floor_45, Entrance.dig_to_mines_floor_50,
- Entrance.dig_to_mines_floor_55, Entrance.dig_to_mines_floor_60,
- Entrance.dig_to_mines_floor_65, Entrance.dig_to_mines_floor_70,
- Entrance.dig_to_mines_floor_75, Entrance.dig_to_mines_floor_80,
- Entrance.dig_to_mines_floor_85, Entrance.dig_to_mines_floor_90,
- Entrance.dig_to_mines_floor_95, Entrance.dig_to_mines_floor_100,
- Entrance.dig_to_mines_floor_105, Entrance.dig_to_mines_floor_110,
- Entrance.dig_to_mines_floor_115, Entrance.dig_to_mines_floor_120]),
- RegionData(Region.mines_dwarf_shop),
- RegionData(Region.mines_floor_5),
- RegionData(Region.mines_floor_10),
- RegionData(Region.mines_floor_15),
- RegionData(Region.mines_floor_20),
- RegionData(Region.mines_floor_25),
- RegionData(Region.mines_floor_30),
- RegionData(Region.mines_floor_35),
- RegionData(Region.mines_floor_40),
- RegionData(Region.mines_floor_45),
- RegionData(Region.mines_floor_50),
- RegionData(Region.mines_floor_55),
- RegionData(Region.mines_floor_60),
- RegionData(Region.mines_floor_65),
- RegionData(Region.mines_floor_70),
- RegionData(Region.mines_floor_75),
- RegionData(Region.mines_floor_80),
- RegionData(Region.mines_floor_85),
- RegionData(Region.mines_floor_90),
- RegionData(Region.mines_floor_95),
- RegionData(Region.mines_floor_100),
- RegionData(Region.mines_floor_105),
- RegionData(Region.mines_floor_110),
- RegionData(Region.mines_floor_115),
- RegionData(Region.mines_floor_120),
+ Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_dig_site_to_jungle],
+ is_ginger_island=True),
+ RegionData(Region.professor_snail_cave, is_ginger_island=True),
+ RegionData(Region.coop),
+ RegionData(Region.barn),
+ RegionData(Region.shed),
+ RegionData(Region.slime_hutch),
+
+ RegionData(Region.mines, [LogicEntrance.talk_to_mines_dwarf,
+ Entrance.dig_to_mines_floor_5]),
+ RegionData(Region.mines_floor_5, [Entrance.dig_to_mines_floor_10]),
+ RegionData(Region.mines_floor_10, [Entrance.dig_to_mines_floor_15]),
+ RegionData(Region.mines_floor_15, [Entrance.dig_to_mines_floor_20]),
+ RegionData(Region.mines_floor_20, [Entrance.dig_to_mines_floor_25]),
+ RegionData(Region.mines_floor_25, [Entrance.dig_to_mines_floor_30]),
+ RegionData(Region.mines_floor_30, [Entrance.dig_to_mines_floor_35]),
+ RegionData(Region.mines_floor_35, [Entrance.dig_to_mines_floor_40]),
+ RegionData(Region.mines_floor_40, [Entrance.dig_to_mines_floor_45]),
+ RegionData(Region.mines_floor_45, [Entrance.dig_to_mines_floor_50]),
+ RegionData(Region.mines_floor_50, [Entrance.dig_to_mines_floor_55]),
+ RegionData(Region.mines_floor_55, [Entrance.dig_to_mines_floor_60]),
+ RegionData(Region.mines_floor_60, [Entrance.dig_to_mines_floor_65]),
+ RegionData(Region.mines_floor_65, [Entrance.dig_to_mines_floor_70]),
+ RegionData(Region.mines_floor_70, [Entrance.dig_to_mines_floor_75]),
+ RegionData(Region.mines_floor_75, [Entrance.dig_to_mines_floor_80]),
+ RegionData(Region.mines_floor_80, [Entrance.dig_to_mines_floor_85]),
+ RegionData(Region.mines_floor_85, [Entrance.dig_to_mines_floor_90]),
+ RegionData(Region.mines_floor_90, [Entrance.dig_to_mines_floor_95]),
+ RegionData(Region.mines_floor_95, [Entrance.dig_to_mines_floor_100]),
+ RegionData(Region.mines_floor_100, [Entrance.dig_to_mines_floor_105]),
+ RegionData(Region.mines_floor_105, [Entrance.dig_to_mines_floor_110]),
+ RegionData(Region.mines_floor_110, [Entrance.dig_to_mines_floor_115]),
+ RegionData(Region.mines_floor_115, [Entrance.dig_to_mines_floor_120]),
+ RegionData(Region.mines_floor_120, [Entrance.dig_to_dangerous_mines_20, Entrance.dig_to_dangerous_mines_60, Entrance.dig_to_dangerous_mines_100]),
+ RegionData(Region.dangerous_mines_20, is_ginger_island=True),
+ RegionData(Region.dangerous_mines_60, is_ginger_island=True),
+ RegionData(Region.dangerous_mines_100, is_ginger_island=True),
+
+ RegionData(LogicRegion.mines_dwarf_shop),
+ RegionData(LogicRegion.blacksmith_copper, [LogicEntrance.blacksmith_iron]),
+ RegionData(LogicRegion.blacksmith_iron, [LogicEntrance.blacksmith_gold]),
+ RegionData(LogicRegion.blacksmith_gold, [LogicEntrance.blacksmith_iridium]),
+ RegionData(LogicRegion.blacksmith_iridium),
+ RegionData(LogicRegion.kitchen),
+ RegionData(LogicRegion.queen_of_sauce),
+ RegionData(LogicRegion.fishing),
+
+ RegionData(LogicRegion.spring_farming),
+ RegionData(LogicRegion.summer_farming, [LogicEntrance.grow_summer_fall_crops_in_summer]),
+ RegionData(LogicRegion.fall_farming, [LogicEntrance.grow_summer_fall_crops_in_fall]),
+ RegionData(LogicRegion.winter_farming),
+ RegionData(LogicRegion.summer_or_fall_farming),
+ RegionData(LogicRegion.indoor_farming),
+
+ RegionData(LogicRegion.shipping),
+ RegionData(LogicRegion.traveling_cart, [LogicEntrance.buy_from_traveling_merchant_sunday,
+ LogicEntrance.buy_from_traveling_merchant_monday,
+ LogicEntrance.buy_from_traveling_merchant_tuesday,
+ LogicEntrance.buy_from_traveling_merchant_wednesday,
+ LogicEntrance.buy_from_traveling_merchant_thursday,
+ LogicEntrance.buy_from_traveling_merchant_friday,
+ LogicEntrance.buy_from_traveling_merchant_saturday]),
+ RegionData(LogicRegion.traveling_cart_sunday),
+ RegionData(LogicRegion.traveling_cart_monday),
+ RegionData(LogicRegion.traveling_cart_tuesday),
+ RegionData(LogicRegion.traveling_cart_wednesday),
+ RegionData(LogicRegion.traveling_cart_thursday),
+ RegionData(LogicRegion.traveling_cart_friday),
+ RegionData(LogicRegion.traveling_cart_saturday),
+ RegionData(LogicRegion.raccoon_daddy, [LogicEntrance.buy_from_raccoon]),
+ RegionData(LogicRegion.raccoon_shop),
+
+ RegionData(LogicRegion.egg_festival),
+ RegionData(LogicRegion.desert_festival),
+ RegionData(LogicRegion.flower_dance),
+ RegionData(LogicRegion.luau),
+ RegionData(LogicRegion.trout_derby),
+ RegionData(LogicRegion.moonlight_jellies),
+ RegionData(LogicRegion.fair),
+ RegionData(LogicRegion.spirit_eve),
+ RegionData(LogicRegion.festival_of_ice),
+ RegionData(LogicRegion.night_market),
+ RegionData(LogicRegion.winter_star),
+ RegionData(LogicRegion.squidfest),
+ RegionData(LogicRegion.bookseller_1, [LogicEntrance.buy_year1_books]),
+ RegionData(LogicRegion.bookseller_2, [LogicEntrance.buy_year3_books]),
+ RegionData(LogicRegion.bookseller_3),
]
# Exists and where they lead
@@ -214,8 +263,12 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region:
ConnectionData(Entrance.farm_to_forest, Region.forest),
ConnectionData(Entrance.farm_to_farmcave, Region.farm_cave, flag=RandomizationFlag.NON_PROGRESSION),
ConnectionData(Entrance.enter_greenhouse, Region.greenhouse),
+ ConnectionData(Entrance.enter_coop, Region.coop),
+ ConnectionData(Entrance.enter_barn, Region.barn),
+ ConnectionData(Entrance.enter_shed, Region.shed),
+ ConnectionData(Entrance.enter_slime_hutch, Region.slime_hutch),
ConnectionData(Entrance.use_desert_obelisk, Region.desert),
- ConnectionData(Entrance.use_island_obelisk, Region.island_south),
+ ConnectionData(Entrance.use_island_obelisk, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.use_farm_obelisk, Region.farm),
ConnectionData(Entrance.backwoods_to_mountain, Region.mountain),
ConnectionData(Entrance.bus_stop_to_town, Region.town),
@@ -232,7 +285,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region:
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_secret_woods, Region.secret_woods),
ConnectionData(Entrance.forest_to_sewer, Region.sewer, flag=RandomizationFlag.BUILDINGS),
- ConnectionData(Entrance.buy_from_traveling_merchant, Region.traveling_cart),
+ ConnectionData(Entrance.forest_to_mastery_cave, Region.mastery_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.MASTERIES),
ConnectionData(Entrance.town_to_sewer, Region.sewer, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_mutant_bug_lair, Region.mutant_bug_lair, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.mountain_to_railroad, Region.railroad),
@@ -247,6 +300,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region:
ConnectionData(Entrance.enter_sebastian_room, Region.sebastian_room, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
+ ConnectionData(Entrance.adventurer_guild_to_bedroom, Region.adventurer_guild_bedroom),
ConnectionData(Entrance.enter_quarry, Region.quarry),
ConnectionData(Entrance.enter_quarry_mine_entrance, Region.quarry_mine_entrance,
flag=RandomizationFlag.BUILDINGS),
@@ -290,6 +344,9 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region:
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_jojamart, Region.jojamart,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
+ ConnectionData(Entrance.purchase_movie_ticket, Region.movie_ticket_stand),
+ ConnectionData(Entrance.enter_abandoned_jojamart, Region.abandoned_jojamart),
+ ConnectionData(Entrance.enter_movie_theater, Region.movie_theater),
ConnectionData(Entrance.town_to_beach, Region.beach),
ConnectionData(Entrance.enter_elliott_house, Region.elliott_house,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
@@ -297,11 +354,10 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region:
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.fish_shop_to_boat_tunnel, Region.boat_tunnel,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
- ConnectionData(Entrance.boat_to_ginger_island, Region.island_south),
+ ConnectionData(Entrance.boat_to_ginger_island, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.enter_tide_pools, Region.tide_pools),
ConnectionData(Entrance.mountain_to_the_mines, Region.mines,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
- ConnectionData(Entrance.talk_to_mines_dwarf, Region.mines_dwarf_shop),
ConnectionData(Entrance.dig_to_mines_floor_5, Region.mines_floor_5),
ConnectionData(Entrance.dig_to_mines_floor_10, Region.mines_floor_10),
ConnectionData(Entrance.dig_to_mines_floor_15, Region.mines_floor_15),
@@ -326,6 +382,9 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region:
ConnectionData(Entrance.dig_to_mines_floor_110, Region.mines_floor_110),
ConnectionData(Entrance.dig_to_mines_floor_115, Region.mines_floor_115),
ConnectionData(Entrance.dig_to_mines_floor_120, Region.mines_floor_120),
+ ConnectionData(Entrance.dig_to_dangerous_mines_20, Region.dangerous_mines_20, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.dig_to_dangerous_mines_60, Region.dangerous_mines_60, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.dig_to_dangerous_mines_100, Region.dangerous_mines_100, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.enter_skull_cavern_entrance, Region.skull_cavern_entrance,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_oasis, Region.oasis,
@@ -340,6 +399,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region:
ConnectionData(Entrance.mine_to_skull_cavern_floor_150, Region.skull_cavern_150),
ConnectionData(Entrance.mine_to_skull_cavern_floor_175, Region.skull_cavern_175),
ConnectionData(Entrance.mine_to_skull_cavern_floor_200, Region.skull_cavern_200),
+ ConnectionData(Entrance.enter_dangerous_skull_cavern, Region.dangerous_skull_cavern, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.enter_witch_warp_cave, Region.witch_warp_cave, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_witch_swamp, Region.witch_swamp, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_witch_hut, Region.witch_hut, flag=RandomizationFlag.BUILDINGS),
@@ -353,7 +413,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region:
ConnectionData(Entrance.island_south_to_east, Region.island_east, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_south_to_southeast, Region.island_south_east,
flag=RandomizationFlag.GINGER_ISLAND),
- ConnectionData(Entrance.use_island_resort, Region.island_resort),
+ ConnectionData(Entrance.use_island_resort, Region.island_resort, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_west_to_islandfarmhouse, Region.island_farmhouse,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_west_to_gourmand_cave, Region.gourmand_frog_cave,
@@ -362,8 +422,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region:
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_west_to_shipwreck, Region.shipwreck,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
- ConnectionData(Entrance.island_west_to_qi_walnut_room, Region.qi_walnut_room,
- flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.island_west_to_qi_walnut_room, Region.qi_walnut_room, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_east_to_leo_hut, Region.leo_hut,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_east_to_island_shrine, Region.island_shrine,
@@ -379,30 +438,86 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region:
ConnectionData(Entrance.volcano_to_secret_beach, Region.volcano_secret_beach,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.talk_to_island_trader, Region.island_trader, flag=RandomizationFlag.GINGER_ISLAND),
- ConnectionData(Entrance.climb_to_volcano_5, Region.volcano_floor_5),
- ConnectionData(Entrance.talk_to_volcano_dwarf, Region.volcano_dwarf_shop),
- ConnectionData(Entrance.climb_to_volcano_10, Region.volcano_floor_10),
- ConnectionData(Entrance.parrot_express_jungle_to_docks, Region.island_south),
- ConnectionData(Entrance.parrot_express_dig_site_to_docks, Region.island_south),
- ConnectionData(Entrance.parrot_express_volcano_to_docks, Region.island_south),
- ConnectionData(Entrance.parrot_express_volcano_to_jungle, Region.island_west),
- ConnectionData(Entrance.parrot_express_docks_to_jungle, Region.island_west),
- ConnectionData(Entrance.parrot_express_dig_site_to_jungle, Region.island_west),
- ConnectionData(Entrance.parrot_express_docks_to_dig_site, Region.dig_site),
- ConnectionData(Entrance.parrot_express_volcano_to_dig_site, Region.dig_site),
- ConnectionData(Entrance.parrot_express_jungle_to_dig_site, Region.dig_site),
- ConnectionData(Entrance.parrot_express_dig_site_to_volcano, Region.island_north),
- ConnectionData(Entrance.parrot_express_docks_to_volcano, Region.island_north),
- ConnectionData(Entrance.parrot_express_jungle_to_volcano, Region.island_north),
+ ConnectionData(Entrance.climb_to_volcano_5, Region.volcano_floor_5, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.talk_to_volcano_dwarf, Region.volcano_dwarf_shop, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.climb_to_volcano_10, Region.volcano_floor_10, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.parrot_express_jungle_to_docks, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.parrot_express_dig_site_to_docks, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.parrot_express_volcano_to_docks, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.parrot_express_volcano_to_jungle, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.parrot_express_docks_to_jungle, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.parrot_express_dig_site_to_jungle, Region.island_west, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.parrot_express_docks_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.parrot_express_volcano_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.parrot_express_jungle_to_dig_site, Region.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.parrot_express_dig_site_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.parrot_express_docks_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(Entrance.parrot_express_jungle_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND),
+
+ ConnectionData(LogicEntrance.talk_to_mines_dwarf, LogicRegion.mines_dwarf_shop),
+
+ ConnectionData(LogicEntrance.buy_from_traveling_merchant, LogicRegion.traveling_cart),
+ ConnectionData(LogicEntrance.buy_from_traveling_merchant_sunday, LogicRegion.traveling_cart_sunday),
+ ConnectionData(LogicEntrance.buy_from_traveling_merchant_monday, LogicRegion.traveling_cart_monday),
+ ConnectionData(LogicEntrance.buy_from_traveling_merchant_tuesday, LogicRegion.traveling_cart_tuesday),
+ ConnectionData(LogicEntrance.buy_from_traveling_merchant_wednesday, LogicRegion.traveling_cart_wednesday),
+ ConnectionData(LogicEntrance.buy_from_traveling_merchant_thursday, LogicRegion.traveling_cart_thursday),
+ ConnectionData(LogicEntrance.buy_from_traveling_merchant_friday, LogicRegion.traveling_cart_friday),
+ ConnectionData(LogicEntrance.buy_from_traveling_merchant_saturday, LogicRegion.traveling_cart_saturday),
+ ConnectionData(LogicEntrance.complete_raccoon_requests, LogicRegion.raccoon_daddy),
+ ConnectionData(LogicEntrance.fish_in_waterfall, LogicRegion.forest_waterfall),
+ ConnectionData(LogicEntrance.buy_from_raccoon, LogicRegion.raccoon_shop),
+ ConnectionData(LogicEntrance.farmhouse_cooking, LogicRegion.kitchen),
+ ConnectionData(LogicEntrance.watch_queen_of_sauce, LogicRegion.queen_of_sauce),
+
+ ConnectionData(LogicEntrance.grow_spring_crops, LogicRegion.spring_farming),
+ ConnectionData(LogicEntrance.grow_summer_crops, LogicRegion.summer_farming),
+ ConnectionData(LogicEntrance.grow_fall_crops, LogicRegion.fall_farming),
+ ConnectionData(LogicEntrance.grow_winter_crops, LogicRegion.winter_farming),
+ ConnectionData(LogicEntrance.grow_spring_crops_in_greenhouse, LogicRegion.spring_farming),
+ ConnectionData(LogicEntrance.grow_summer_crops_in_greenhouse, LogicRegion.summer_farming),
+ ConnectionData(LogicEntrance.grow_fall_crops_in_greenhouse, LogicRegion.fall_farming),
+ ConnectionData(LogicEntrance.grow_winter_crops_in_greenhouse, LogicRegion.winter_farming),
+ ConnectionData(LogicEntrance.grow_indoor_crops_in_greenhouse, LogicRegion.indoor_farming),
+ ConnectionData(LogicEntrance.grow_spring_crops_on_island, LogicRegion.spring_farming, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(LogicEntrance.grow_summer_crops_on_island, LogicRegion.summer_farming, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(LogicEntrance.grow_fall_crops_on_island, LogicRegion.fall_farming, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(LogicEntrance.grow_winter_crops_on_island, LogicRegion.winter_farming, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(LogicEntrance.grow_indoor_crops_on_island, LogicRegion.indoor_farming, flag=RandomizationFlag.GINGER_ISLAND),
+ ConnectionData(LogicEntrance.grow_summer_fall_crops_in_summer, LogicRegion.summer_or_fall_farming),
+ ConnectionData(LogicEntrance.grow_summer_fall_crops_in_fall, LogicRegion.summer_or_fall_farming),
+
+ ConnectionData(LogicEntrance.shipping, LogicRegion.shipping),
+ ConnectionData(LogicEntrance.blacksmith_copper, LogicRegion.blacksmith_copper),
+ ConnectionData(LogicEntrance.blacksmith_iron, LogicRegion.blacksmith_iron),
+ ConnectionData(LogicEntrance.blacksmith_gold, LogicRegion.blacksmith_gold),
+ ConnectionData(LogicEntrance.blacksmith_iridium, LogicRegion.blacksmith_iridium),
+ ConnectionData(LogicEntrance.fishing, LogicRegion.fishing),
+ ConnectionData(LogicEntrance.island_cooking, LogicRegion.kitchen),
+ ConnectionData(LogicEntrance.attend_egg_festival, LogicRegion.egg_festival),
+ ConnectionData(LogicEntrance.attend_desert_festival, LogicRegion.desert_festival),
+ ConnectionData(LogicEntrance.attend_flower_dance, LogicRegion.flower_dance),
+ ConnectionData(LogicEntrance.attend_luau, LogicRegion.luau),
+ ConnectionData(LogicEntrance.attend_trout_derby, LogicRegion.trout_derby),
+ ConnectionData(LogicEntrance.attend_moonlight_jellies, LogicRegion.moonlight_jellies),
+ ConnectionData(LogicEntrance.attend_fair, LogicRegion.fair),
+ ConnectionData(LogicEntrance.attend_spirit_eve, LogicRegion.spirit_eve),
+ ConnectionData(LogicEntrance.attend_festival_of_ice, LogicRegion.festival_of_ice),
+ ConnectionData(LogicEntrance.attend_night_market, LogicRegion.night_market),
+ ConnectionData(LogicEntrance.attend_winter_star, LogicRegion.winter_star),
+ ConnectionData(LogicEntrance.attend_squidfest, LogicRegion.squidfest),
+ ConnectionData(LogicEntrance.buy_experience_books, LogicRegion.bookseller_1),
+ ConnectionData(LogicEntrance.buy_year1_books, LogicRegion.bookseller_2),
+ ConnectionData(LogicEntrance.buy_year3_books, LogicRegion.bookseller_3),
]
-def create_final_regions(world_options: StardewOptions) -> List[RegionData]:
+def create_final_regions(world_options) -> List[RegionData]:
final_regions = []
final_regions.extend(vanilla_regions)
- if world_options[options.Mods] is None:
+ if world_options.mods is None:
return final_regions
- for mod in world_options[options.Mods]:
+ for mod in world_options.mods.value:
if mod not in ModDataList:
continue
for mod_region in ModDataList[mod].regions:
@@ -410,78 +525,116 @@ def create_final_regions(world_options: StardewOptions) -> List[RegionData]:
(region for region in final_regions if region.name == mod_region.name), None)
if existing_region:
final_regions.remove(existing_region)
+ if ModificationFlag.MODIFIED in mod_region.flag:
+ mod_region = modify_vanilla_regions(existing_region, mod_region)
final_regions.append(existing_region.get_merged_with(mod_region.exits))
continue
-
final_regions.append(mod_region.get_clone())
+
return final_regions
-def create_final_connections(world_options: StardewOptions) -> List[ConnectionData]:
- final_connections = []
- final_connections.extend(vanilla_connections)
- if world_options[options.Mods] is None:
- return final_connections
- for mod in world_options[options.Mods]:
+def create_final_connections_and_regions(world_options) -> Tuple[Dict[str, ConnectionData], Dict[str, RegionData]]:
+ regions_data: Dict[str, RegionData] = {region.name: region for region in create_final_regions(world_options)}
+ connections = {connection.name: connection for connection in vanilla_connections}
+ connections = modify_connections_for_mods(connections, sorted(world_options.mods.value))
+ include_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_false
+ return remove_ginger_island_regions_and_connections(regions_data, connections, include_island)
+
+
+def remove_ginger_island_regions_and_connections(regions_by_name: Dict[str, RegionData], connections: Dict[str, ConnectionData], include_island: bool):
+ if include_island:
+ return connections, regions_by_name
+
+ removed_connections = set()
+
+ for connection_name in tuple(connections):
+ connection = connections[connection_name]
+ if connection.flag & RandomizationFlag.GINGER_ISLAND:
+ connections.pop(connection_name)
+ removed_connections.add(connection_name)
+
+ for region_name in tuple(regions_by_name):
+ region = regions_by_name[region_name]
+ if region.is_ginger_island:
+ regions_by_name.pop(region_name)
+ else:
+ regions_by_name[region_name] = region.get_without_exits(removed_connections)
+
+ return connections, regions_by_name
+
+
+def modify_connections_for_mods(connections: Dict[str, ConnectionData], mods: Iterable) -> Dict[str, ConnectionData]:
+ for mod in mods:
if mod not in ModDataList:
continue
- final_connections.extend(ModDataList[mod].connections)
- return final_connections
+ if mod in vanilla_connections_to_remove_by_mod:
+ for connection_data in vanilla_connections_to_remove_by_mod[mod]:
+ connections.pop(connection_data.name)
+ connections.update({connection.name: connection for connection in ModDataList[mod].connections})
+ return connections
-def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewOptions) -> Tuple[
- Iterable[Region], Dict[str, str]]:
- final_regions = create_final_regions(world_options)
- regions: Dict[str: Region] = {region.name: region_factory(region.name, region.exits) for region in
- final_regions}
- entrances: Dict[str: Entrance] = {entrance.name: entrance
- for region in regions.values()
- for entrance in region.exits}
+def modify_vanilla_regions(existing_region: RegionData, modified_region: RegionData) -> RegionData:
+ updated_region = existing_region
+ region_exits = updated_region.exits
+ modified_exits = modified_region.exits
+ for exits in modified_exits:
+ region_exits.remove(exits)
- regions_by_name: Dict[str, RegionData] = {region.name: region for region in final_regions}
- connections, randomized_data = randomize_connections(random, world_options, regions_by_name)
+ return updated_region
+
+
+def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewValleyOptions) \
+ -> Tuple[Dict[str, Region], Dict[str, Entrance], Dict[str, str]]:
+ entrances_data, regions_data = create_final_connections_and_regions(world_options)
+ regions_by_name: Dict[str: Region] = {region_name: region_factory(region_name, regions_data[region_name].exits) for region_name in regions_data}
+ entrances_by_name: Dict[str: Entrance] = {
+ entrance.name: entrance
+ for region in regions_by_name.values()
+ for entrance in region.exits
+ if entrance.name in entrances_data
+ }
+
+ connections, randomized_data = randomize_connections(random, world_options, regions_data, entrances_data)
for connection in connections:
- if connection.name in entrances:
- entrances[connection.name].connect(regions[connection.destination])
-
- return regions.values(), randomized_data
-
-
-def randomize_connections(random: Random, world_options: StardewOptions, regions_by_name) -> Tuple[
- List[ConnectionData], Dict[str, str]]:
- connections_to_randomize = []
- final_connections = create_final_connections(world_options)
- connections_by_name: Dict[str, ConnectionData] = {connection.name: connection for connection in final_connections}
- if world_options[options.EntranceRandomization] == options.EntranceRandomization.option_pelican_town:
- connections_to_randomize = [connection for connection in final_connections if
- RandomizationFlag.PELICAN_TOWN in connection.flag]
- elif world_options[options.EntranceRandomization] == options.EntranceRandomization.option_non_progression:
- connections_to_randomize = [connection for connection in final_connections if
- RandomizationFlag.NON_PROGRESSION in connection.flag]
- elif world_options[options.EntranceRandomization] == options.EntranceRandomization.option_buildings:
- connections_to_randomize = [connection for connection in final_connections if
- RandomizationFlag.BUILDINGS in connection.flag]
- elif world_options[options.EntranceRandomization] == options.EntranceRandomization.option_chaos:
- connections_to_randomize = [connection for connection in final_connections if
- RandomizationFlag.BUILDINGS in connection.flag]
- connections_to_randomize = exclude_island_if_necessary(connections_to_randomize, world_options)
+ if connection.name in entrances_by_name:
+ entrances_by_name[connection.name].connect(regions_by_name[connection.destination])
+ return regions_by_name, entrances_by_name, randomized_data
+
+
+def randomize_connections(random: Random, world_options: StardewValleyOptions, regions_by_name: Dict[str, RegionData],
+ connections_by_name: Dict[str, ConnectionData]) -> Tuple[List[ConnectionData], Dict[str, str]]:
+ connections_to_randomize: List[ConnectionData] = []
+ if world_options.entrance_randomization == EntranceRandomization.option_pelican_town:
+ connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if
+ RandomizationFlag.PELICAN_TOWN in connections_by_name[connection].flag]
+ elif world_options.entrance_randomization == EntranceRandomization.option_non_progression:
+ connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if
+ RandomizationFlag.NON_PROGRESSION in connections_by_name[connection].flag]
+ elif world_options.entrance_randomization == EntranceRandomization.option_buildings or world_options.entrance_randomization == EntranceRandomization.option_buildings_without_house:
+ connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if
+ RandomizationFlag.BUILDINGS in connections_by_name[connection].flag]
+ elif world_options.entrance_randomization == EntranceRandomization.option_chaos:
+ connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if
+ RandomizationFlag.BUILDINGS in connections_by_name[connection].flag]
+ connections_to_randomize = remove_excluded_entrances(connections_to_randomize, world_options)
# On Chaos, we just add the connections to randomize, unshuffled, and the client does it every day
randomized_data_for_mod = {}
for connection in connections_to_randomize:
randomized_data_for_mod[connection.name] = connection.name
randomized_data_for_mod[connection.reverse] = connection.reverse
- return final_connections, randomized_data_for_mod
+ return list(connections_by_name.values()), randomized_data_for_mod
connections_to_randomize = remove_excluded_entrances(connections_to_randomize, world_options)
-
random.shuffle(connections_to_randomize)
destination_pool = list(connections_to_randomize)
random.shuffle(destination_pool)
randomized_connections = randomize_chosen_connections(connections_to_randomize, destination_pool)
- add_non_randomized_connections(final_connections, connections_to_randomize, randomized_connections)
+ add_non_randomized_connections(list(connections_by_name.values()), connections_to_randomize, randomized_connections)
swap_connections_until_valid(regions_by_name, connections_by_name, randomized_connections, connections_to_randomize, random)
randomized_connections_for_generation = create_connections_for_generation(randomized_connections)
@@ -490,25 +643,17 @@ def randomize_connections(random: Random, world_options: StardewOptions, regions
return randomized_connections_for_generation, randomized_data_for_mod
-def remove_excluded_entrances(connections_to_randomize, world_options):
- exclude_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true
- exclude_sewers = world_options[options.Museumsanity] == options.Museumsanity.option_none
+def remove_excluded_entrances(connections_to_randomize: List[ConnectionData], world_options: StardewValleyOptions) -> List[ConnectionData]:
+ exclude_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_true
if exclude_island:
connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.GINGER_ISLAND not in connection.flag]
- if exclude_sewers:
- connections_to_randomize = [connection for connection in connections_to_randomize if Region.sewer not in connection.name or Region.sewer not in connection.reverse]
+ exclude_masteries = world_options.skill_progression != SkillProgression.option_progressive_with_masteries
+ if exclude_masteries:
+ connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.MASTERIES not in connection.flag]
return connections_to_randomize
-def exclude_island_if_necessary(connections_to_randomize: List[ConnectionData], world_options) -> List[ConnectionData]:
- exclude_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true
- if exclude_island:
- connections_to_randomize = [connection for connection in connections_to_randomize if
- RandomizationFlag.GINGER_ISLAND not in connection.flag]
- return connections_to_randomize
-
-
def randomize_chosen_connections(connections_to_randomize: List[ConnectionData],
destination_pool: List[ConnectionData]) -> Dict[ConnectionData, ConnectionData]:
randomized_connections = {}
@@ -518,8 +663,7 @@ def randomize_chosen_connections(connections_to_randomize: List[ConnectionData],
return randomized_connections
-def create_connections_for_generation(randomized_connections: Dict[ConnectionData, ConnectionData]) -> List[
- ConnectionData]:
+def create_connections_for_generation(randomized_connections: Dict[ConnectionData, ConnectionData]) -> List[ConnectionData]:
connections = []
for connection in randomized_connections:
destination = randomized_connections[connection]
@@ -537,37 +681,50 @@ def create_data_for_mod(randomized_connections: Dict[ConnectionData, ConnectionD
add_to_mod_data(connection, destination, randomized_data_for_mod)
return randomized_data_for_mod
+
def add_to_mod_data(connection: ConnectionData, destination: ConnectionData, randomized_data_for_mod: Dict[str, str]):
randomized_data_for_mod[connection.name] = destination.name
randomized_data_for_mod[destination.reverse] = connection.reverse
-def add_non_randomized_connections(connections, connections_to_randomize: List[ConnectionData],
+def add_non_randomized_connections(all_connections: List[ConnectionData], connections_to_randomize: List[ConnectionData],
randomized_connections: Dict[ConnectionData, ConnectionData]):
- for connection in connections:
+ for connection in all_connections:
if connection in connections_to_randomize:
continue
randomized_connections[connection] = connection
-def swap_connections_until_valid(regions_by_name, connections_by_name, randomized_connections: Dict[ConnectionData, ConnectionData],
+def swap_connections_until_valid(regions_by_name, connections_by_name: Dict[str, ConnectionData], randomized_connections: Dict[ConnectionData, ConnectionData],
connections_to_randomize: List[ConnectionData], random: Random):
while True:
reachable_regions, unreachable_regions = find_reachable_regions(regions_by_name, connections_by_name, randomized_connections)
if not unreachable_regions:
return randomized_connections
- swap_one_connection(regions_by_name, connections_by_name, randomized_connections, reachable_regions,
- unreachable_regions, connections_to_randomize, random)
+ swap_one_random_connection(regions_by_name, connections_by_name, randomized_connections, reachable_regions,
+ unreachable_regions, connections_to_randomize, random)
+
+
+def region_should_be_reachable(region_name: str, connections_in_slot: Iterable[ConnectionData]) -> bool:
+ if region_name == Region.menu:
+ return True
+ for connection in connections_in_slot:
+ if region_name == connection.destination:
+ return True
+ return False
def find_reachable_regions(regions_by_name, connections_by_name,
randomized_connections: Dict[ConnectionData, ConnectionData]):
reachable_regions = {Region.menu}
unreachable_regions = {region for region in regions_by_name.keys()}
+ # unreachable_regions = {region for region in regions_by_name.keys() if region_should_be_reachable(region, connections_by_name.values())}
unreachable_regions.remove(Region.menu)
exits_to_explore = list(regions_by_name[Region.menu].exits)
while exits_to_explore:
exit_name = exits_to_explore.pop()
+ # if exit_name not in connections_by_name:
+ # continue
exit_connection = connections_by_name[exit_name]
replaced_connection = randomized_connections[exit_connection]
target_region_name = replaced_connection.destination
@@ -581,14 +738,14 @@ def find_reachable_regions(regions_by_name, connections_by_name,
return reachable_regions, unreachable_regions
-def swap_one_connection(regions_by_name, connections_by_name,randomized_connections: Dict[ConnectionData, ConnectionData],
- reachable_regions: Set[str], unreachable_regions: Set[str],
- connections_to_randomize: List[ConnectionData], random: Random):
+def swap_one_random_connection(regions_by_name, connections_by_name, randomized_connections: Dict[ConnectionData, ConnectionData],
+ reachable_regions: Set[str], unreachable_regions: Set[str],
+ connections_to_randomize: List[ConnectionData], random: Random):
randomized_connections_already_shuffled = {connection: randomized_connections[connection]
for connection in randomized_connections
if connection != randomized_connections[connection]}
unreachable_regions_names_leading_somewhere = tuple([region for region in unreachable_regions
- if len(regions_by_name[region].exits) > 0])
+ if len(regions_by_name[region].exits) > 0])
unreachable_regions_leading_somewhere = [regions_by_name[region_name] for region_name in unreachable_regions_names_leading_somewhere]
unreachable_regions_exits_names = [exit_name for region in unreachable_regions_leading_somewhere for exit_name in region.exits]
unreachable_connections = [connections_by_name[exit_name] for exit_name in unreachable_regions_exits_names]
@@ -605,7 +762,11 @@ def swap_one_connection(regions_by_name, connections_by_name,randomized_connecti
chosen_reachable_entrance_name = random.choice(chosen_reachable_region.exits)
chosen_reachable_entrance = connections_by_name[chosen_reachable_entrance_name]
- reachable_destination = randomized_connections[chosen_reachable_entrance]
- unreachable_destination = randomized_connections[chosen_unreachable_entrance]
- randomized_connections[chosen_reachable_entrance] = unreachable_destination
- randomized_connections[chosen_unreachable_entrance] = reachable_destination
+ swap_two_connections(chosen_reachable_entrance, chosen_unreachable_entrance, randomized_connections)
+
+
+def swap_two_connections(entrance_1, entrance_2, randomized_connections):
+ reachable_destination = randomized_connections[entrance_1]
+ unreachable_destination = randomized_connections[entrance_2]
+ randomized_connections[entrance_1] = unreachable_destination
+ randomized_connections[entrance_2] = reachable_destination
diff --git a/worlds/stardew_valley/requirements.txt b/worlds/stardew_valley/requirements.txt
index a7141f6aa805..65e922a64483 100644
--- a/worlds/stardew_valley/requirements.txt
+++ b/worlds/stardew_valley/requirements.txt
@@ -1 +1,2 @@
-importlib_resources; python_version <= '3.8'
\ No newline at end of file
+importlib_resources; python_version <= '3.8'
+graphlib_backport; python_version <= '3.8'
diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py
index 34ee1f807dd3..89b1cf87c3c1 100644
--- a/worlds/stardew_valley/rules.py
+++ b/worlds/stardew_valley/rules.py
@@ -1,358 +1,572 @@
import itertools
-from typing import Dict, List
+import logging
+from typing import List, Dict, Set
-from BaseClasses import MultiWorld
+from BaseClasses import MultiWorld, CollectionState
from worlds.generic import Rules as MultiWorldRules
-from . import options, locations
-from .bundles import Bundle
-from .strings.entrance_names import dig_to_mines_floor, dig_to_skull_floor, Entrance, move_to_woods_depth, \
- DeepWoodsEntrance, AlecEntrance, MagicEntrance
-from .data.museum_data import all_museum_items, all_museum_minerals, all_museum_artifacts, \
- dwarf_scrolls, skeleton_front, \
- skeleton_middle, skeleton_back, all_museum_items_by_name, Artifact
-from .strings.region_names import Region
-from .mods.mod_data import ModNames
-from .mods.logic import magic, deepwoods
+from . import locations
+from .bundles.bundle_room import BundleRoom
+from .content import StardewContent
+from .content.feature import friendsanity
+from .data.craftable_data import all_crafting_recipes_by_name
+from .data.game_item import ItemTag
+from .data.harvest import HarvestCropSource, HarvestFruitTreeSource
+from .data.museum_data import all_museum_items, dwarf_scrolls, skeleton_front, skeleton_middle, skeleton_back, all_museum_items_by_name, all_museum_minerals, \
+ all_museum_artifacts, Artifact
+from .data.recipe_data import all_cooking_recipes_by_name
from .locations import LocationTags
-from .logic import StardewLogic, And, tool_upgrade_prices
-from .options import StardewOptions
+from .logic.logic import StardewLogic
+from .logic.time_logic import MAX_MONTHS
+from .logic.tool_logic import tool_upgrade_prices
+from .mods.mod_data import ModNames
+from .options import StardewValleyOptions, Walnutsanity
+from .options import ToolProgression, BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \
+ Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity, SkillProgression
+from .stardew_rule import And, StardewRule, true_
+from .stardew_rule.indirect_connection import look_for_indirect_connection
+from .stardew_rule.rule_explain import explain
+from .strings.ap_names.ap_option_names import OptionName
+from .strings.ap_names.community_upgrade_names import CommunityUpgrade
+from .strings.ap_names.event_names import Event
+from .strings.ap_names.mods.mod_items import SVEQuestItem, SVERunes
from .strings.ap_names.transport_names import Transportation
from .strings.artisan_good_names import ArtisanGood
+from .strings.building_names import Building
+from .strings.bundle_names import CCRoom
from .strings.calendar_names import Weekday
-from .strings.craftable_names import Craftable
+from .strings.craftable_names import Bomb, Furniture
+from .strings.crop_names import Fruit, Vegetable
+from .strings.entrance_names import dig_to_mines_floor, dig_to_skull_floor, Entrance, move_to_woods_depth, DeepWoodsEntrance, AlecEntrance, \
+ SVEEntrance, LaceyEntrance, BoardingHouseEntrance, LogicEntrance
+from .strings.forageable_names import Forageable
+from .strings.geode_names import Geode
from .strings.material_names import Material
-from .strings.metal_names import MetalBar
+from .strings.metal_names import MetalBar, Mineral
+from .strings.monster_names import Monster
+from .strings.performance_names import Performance
+from .strings.quest_names import Quest
+from .strings.region_names import Region
+from .strings.season_names import Season
from .strings.skill_names import ModSkill, Skill
from .strings.tool_names import Tool, ToolMaterial
+from .strings.tv_channel_names import Channel
from .strings.villager_names import NPC, ModNPC
from .strings.wallet_item_names import Wallet
+logger = logging.getLogger(__name__)
+
+
+def set_rules(world):
+ multiworld = world.multiworld
+ world_options = world.options
+ world_content = world.content
+ player = world.player
+ logic = world.logic
+ bundle_rooms: List[BundleRoom] = world.modified_bundles
+
+ all_location_names = set(location.name for location in multiworld.get_locations(player))
+
+ set_entrance_rules(logic, multiworld, player, world_options)
+ set_ginger_island_rules(logic, multiworld, player, world_options)
+
+ set_tool_rules(logic, multiworld, player, world_options)
+ set_skills_rules(logic, multiworld, player, world_options)
+ set_bundle_rules(bundle_rooms, logic, multiworld, player, world_options)
+ set_building_rules(logic, multiworld, player, world_options)
+ set_cropsanity_rules(logic, multiworld, player, world_content)
+ set_story_quests_rules(all_location_names, logic, multiworld, player, world_options)
+ set_special_order_rules(all_location_names, logic, multiworld, player, world_options)
+ set_help_wanted_quests_rules(logic, multiworld, player, world_options)
+ set_fishsanity_rules(all_location_names, logic, multiworld, player)
+ set_museumsanity_rules(all_location_names, logic, multiworld, player, world_options)
+
+ set_friendsanity_rules(logic, multiworld, player, world_content)
+ set_backpack_rules(logic, multiworld, player, world_options)
+ set_festival_rules(all_location_names, logic, multiworld, player)
+ set_monstersanity_rules(all_location_names, logic, multiworld, player, world_options)
+ set_shipsanity_rules(all_location_names, logic, multiworld, player, world_options)
+ set_cooksanity_rules(all_location_names, logic, multiworld, player, world_options)
+ set_chefsanity_rules(all_location_names, logic, multiworld, player, world_options)
+ set_craftsanity_rules(all_location_names, logic, multiworld, player, world_options)
+ set_booksanity_rules(logic, multiworld, player, world_content)
+ set_isolated_locations_rules(logic, multiworld, player)
+ set_traveling_merchant_day_rules(logic, multiworld, player)
+ set_arcade_machine_rules(logic, multiworld, player, world_options)
+
+ set_deepwoods_rules(logic, multiworld, player, world_options)
+ set_magic_spell_rules(logic, multiworld, player, world_options)
+ set_sve_rules(logic, multiworld, player, world_options)
+
+
+def set_isolated_locations_rules(logic: StardewLogic, multiworld, player):
+ MultiWorldRules.add_rule(multiworld.get_location("Old Master Cannoli", player),
+ logic.has(Fruit.sweet_gem_berry))
+ MultiWorldRules.add_rule(multiworld.get_location("Galaxy Sword Shrine", player),
+ logic.has("Prismatic Shard"))
+ MultiWorldRules.add_rule(multiworld.get_location("Krobus Stardrop", player),
+ logic.money.can_spend(20000))
+ MultiWorldRules.add_rule(multiworld.get_location("Demetrius's Breakthrough", player),
+ logic.money.can_have_earned_total(25000))
+ MultiWorldRules.add_rule(multiworld.get_location("Pot Of Gold", player),
+ logic.season.has(Season.spring))
+
+
+def set_tool_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ if not world_options.tool_progression & ToolProgression.option_progressive:
+ return
-def set_rules(multi_world: MultiWorld, player: int, world_options: StardewOptions, logic: StardewLogic,
- current_bundles: Dict[str, Bundle]):
- all_location_names = list(location.name for location in multi_world.get_locations(player))
-
- set_entrance_rules(logic, multi_world, player, world_options)
-
- set_ginger_island_rules(logic, multi_world, player, world_options)
-
- # Those checks do not exist if ToolProgression is vanilla
- if world_options[options.ToolProgression] != options.ToolProgression.option_vanilla:
- MultiWorldRules.add_rule(multi_world.get_location("Purchase Fiberglass Rod", player),
- (logic.has_skill_level(Skill.fishing, 2) & logic.can_spend_money(1800)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Purchase Iridium Rod", player),
- (logic.has_skill_level(Skill.fishing, 6) & logic.can_spend_money(7500)).simplify())
-
- materials = [None, "Copper", "Iron", "Gold", "Iridium"]
- tool = [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.watering_can, Tool.trash_can]
- for (previous, material), tool in itertools.product(zip(materials[:4], materials[1:]), tool):
- if previous is None:
- MultiWorldRules.add_rule(multi_world.get_location(f"{material} {tool} Upgrade", player),
- (logic.has(f"{material} Ore") &
- logic.can_spend_money(tool_upgrade_prices[material])).simplify())
- else:
- MultiWorldRules.add_rule(multi_world.get_location(f"{material} {tool} Upgrade", player),
- (logic.has(f"{material} Ore") & logic.has_tool(tool, previous) &
- logic.can_spend_money(tool_upgrade_prices[material])).simplify())
-
- set_skills_rules(logic, multi_world, player, world_options)
-
- # Bundles
- for bundle in current_bundles.values():
- location = multi_world.get_location(bundle.get_name_with_bundle(), player)
- rules = logic.can_complete_bundle(bundle.requirements, bundle.number_required)
- simplified_rules = rules.simplify()
- MultiWorldRules.set_rule(location, simplified_rules)
- MultiWorldRules.add_rule(multi_world.get_location("Complete Crafts Room", player),
- And(logic.can_reach_location(bundle.name)
- for bundle in locations.locations_by_tag[LocationTags.CRAFTS_ROOM_BUNDLE]).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Complete Pantry", player),
- And(logic.can_reach_location(bundle.name)
- for bundle in locations.locations_by_tag[LocationTags.PANTRY_BUNDLE]).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Complete Fish Tank", player),
- And(logic.can_reach_location(bundle.name)
- for bundle in locations.locations_by_tag[LocationTags.FISH_TANK_BUNDLE]).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Complete Boiler Room", player),
- And(logic.can_reach_location(bundle.name)
- for bundle in locations.locations_by_tag[LocationTags.BOILER_ROOM_BUNDLE]).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Complete Bulletin Board", player),
- And(logic.can_reach_location(bundle.name)
- for bundle
- in locations.locations_by_tag[LocationTags.BULLETIN_BOARD_BUNDLE]).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Complete Vault", player),
- And(logic.can_reach_location(bundle.name)
- for bundle in locations.locations_by_tag[LocationTags.VAULT_BUNDLE]).simplify())
-
- # Buildings
- if world_options[options.BuildingProgression] != options.BuildingProgression.option_vanilla:
- for building in locations.locations_by_tag[LocationTags.BUILDING_BLUEPRINT]:
- if building.mod_name is not None and building.mod_name not in world_options[options.Mods]:
- continue
- MultiWorldRules.set_rule(multi_world.get_location(building.name, player),
- logic.building_rules[building.name.replace(" Blueprint", "")].simplify())
-
- set_cropsanity_rules(all_location_names, logic, multi_world, player, world_options)
- set_story_quests_rules(all_location_names, logic, multi_world, player, world_options)
- set_special_order_rules(all_location_names, logic, multi_world, player, world_options)
- set_help_wanted_quests_rules(logic, multi_world, player, world_options)
- set_fishsanity_rules(all_location_names, logic, multi_world, player)
- set_museumsanity_rules(all_location_names, logic, multi_world, player, world_options)
- set_friendsanity_rules(all_location_names, logic, multi_world, player)
- set_backpack_rules(logic, multi_world, player, world_options)
- set_festival_rules(all_location_names, logic, multi_world, player)
-
- MultiWorldRules.add_rule(multi_world.get_location("Old Master Cannoli", player),
- logic.has("Sweet Gem Berry").simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Galaxy Sword Shrine", player),
- logic.has("Prismatic Shard").simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Have a Baby", player),
- logic.can_reproduce(1).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Have Another Baby", player),
- logic.can_reproduce(2).simplify())
-
- set_traveling_merchant_rules(logic, multi_world, player)
- set_arcade_machine_rules(logic, multi_world, player, world_options)
- set_deepwoods_rules(logic, multi_world, player, world_options)
- set_magic_spell_rules(logic, multi_world, player, world_options)
-
-
-def set_skills_rules(logic, multi_world, player, world_options):
- # Skills
- if world_options[options.SkillProgression] != options.SkillProgression.option_vanilla:
- for i in range(1, 11):
- set_skill_rule(logic, multi_world, player, Skill.farming, i)
- set_skill_rule(logic, multi_world, player, Skill.fishing, i)
- set_skill_rule(logic, multi_world, player, Skill.foraging, i)
- set_skill_rule(logic, multi_world, player, Skill.mining, i)
- set_skill_rule(logic, multi_world, player, Skill.combat, i)
-
- # Modded Skills
- if ModNames.luck_skill in world_options[options.Mods]:
- set_skill_rule(logic, multi_world, player, ModSkill.luck, i)
- if ModNames.magic in world_options[options.Mods]:
- set_skill_rule(logic, multi_world, player, ModSkill.magic, i)
- if ModNames.binning_skill in world_options[options.Mods]:
- set_skill_rule(logic, multi_world, player, ModSkill.binning, i)
- if ModNames.cooking_skill in world_options[options.Mods]:
- set_skill_rule(logic, multi_world, player, ModSkill.cooking, i)
- if ModNames.socializing_skill in world_options[options.Mods]:
- set_skill_rule(logic, multi_world, player, ModSkill.socializing, i)
- if ModNames.archaeology in world_options[options.Mods]:
- set_skill_rule(logic, multi_world, player, ModSkill.archaeology, i)
-
-
-def set_skill_rule(logic, multi_world, player, skill: str, level: int):
- location_name = f"Level {level} {skill}"
- location = multi_world.get_location(location_name, player)
- rule = logic.can_earn_skill_level(skill, level).simplify()
- MultiWorldRules.set_rule(location, rule)
+ MultiWorldRules.add_rule(multiworld.get_location("Purchase Fiberglass Rod", player),
+ (logic.skill.has_level(Skill.fishing, 2) & logic.money.can_spend(1800)))
+ MultiWorldRules.add_rule(multiworld.get_location("Purchase Iridium Rod", player),
+ (logic.skill.has_level(Skill.fishing, 6) & logic.money.can_spend(7500)))
+
+ MultiWorldRules.add_rule(multiworld.get_location("Copper Pan Cutscene", player), logic.received("Glittering Boulder Removed"))
+
+ materials = [None, "Copper", "Iron", "Gold", "Iridium"]
+ tool = [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can, Tool.pan]
+ for (previous, material), tool in itertools.product(zip(materials[:4], materials[1:]), tool):
+ if previous is None:
+ continue
+ tool_upgrade_location = multiworld.get_location(f"{material} {tool} Upgrade", player)
+ MultiWorldRules.set_rule(tool_upgrade_location, logic.tool.has_tool(tool, previous))
+
+
+def set_building_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ if not world_options.building_progression & BuildingProgression.option_progressive:
+ return
+
+ for building in locations.locations_by_tag[LocationTags.BUILDING_BLUEPRINT]:
+ if building.mod_name is not None and building.mod_name not in world_options.mods:
+ continue
+ MultiWorldRules.set_rule(multiworld.get_location(building.name, player),
+ logic.registry.building_rules[building.name.replace(" Blueprint", "")])
+
+
+def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ for bundle_room in bundle_rooms:
+ room_rules = []
+ for bundle in bundle_room.bundles:
+ location = multiworld.get_location(bundle.name, player)
+ bundle_rules = logic.bundle.can_complete_bundle(bundle)
+ if bundle_room.name == CCRoom.raccoon_requests:
+ num = int(bundle.name[-1])
+ extra_raccoons = 1 if world_options.quest_locations >= 0 else 0
+ extra_raccoons = extra_raccoons + num
+ bundle_rules = logic.received(CommunityUpgrade.raccoon, extra_raccoons) & bundle_rules
+ if num > 1:
+ previous_bundle_name = f"Raccoon Request {num-1}"
+ bundle_rules = bundle_rules & logic.region.can_reach_location(previous_bundle_name)
+ room_rules.append(bundle_rules)
+ MultiWorldRules.set_rule(location, bundle_rules)
+ if bundle_room.name == CCRoom.abandoned_joja_mart or bundle_room.name == CCRoom.raccoon_requests:
+ continue
+ room_location = f"Complete {bundle_room.name}"
+ MultiWorldRules.add_rule(multiworld.get_location(room_location, player), And(*room_rules))
-def set_entrance_rules(logic, multi_world, player, world_options: StardewOptions):
+def set_skills_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ mods = world_options.mods
+ if world_options.skill_progression == SkillProgression.option_vanilla:
+ return
+ for i in range(1, 11):
+ set_vanilla_skill_rule_for_level(logic, multiworld, player, i)
+ set_modded_skill_rule_for_level(logic, multiworld, player, mods, i)
+ if world_options.skill_progression != SkillProgression.option_progressive_with_masteries:
+ return
+ for skill in [Skill.farming, Skill.fishing, Skill.foraging, Skill.mining, Skill.combat]:
+ MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery_experience)
+
+
+def set_vanilla_skill_rule_for_level(logic: StardewLogic, multiworld, player, level: int):
+ set_vanilla_skill_rule(logic, multiworld, player, Skill.farming, level)
+ set_vanilla_skill_rule(logic, multiworld, player, Skill.fishing, level)
+ set_vanilla_skill_rule(logic, multiworld, player, Skill.foraging, level)
+ set_vanilla_skill_rule(logic, multiworld, player, Skill.mining, level)
+ set_vanilla_skill_rule(logic, multiworld, player, Skill.combat, level)
+
+
+def set_modded_skill_rule_for_level(logic: StardewLogic, multiworld, player, mods, level: int):
+ if ModNames.luck_skill in mods:
+ set_modded_skill_rule(logic, multiworld, player, ModSkill.luck, level)
+ if ModNames.magic in mods:
+ set_modded_skill_rule(logic, multiworld, player, ModSkill.magic, level)
+ if ModNames.binning_skill in mods:
+ set_modded_skill_rule(logic, multiworld, player, ModSkill.binning, level)
+ if ModNames.cooking_skill in mods:
+ set_modded_skill_rule(logic, multiworld, player, ModSkill.cooking, level)
+ if ModNames.socializing_skill in mods:
+ set_modded_skill_rule(logic, multiworld, player, ModSkill.socializing, level)
+ if ModNames.archaeology in mods:
+ set_modded_skill_rule(logic, multiworld, player, ModSkill.archaeology, level)
+
+
+def get_skill_level_location(multiworld, player, skill: str, level: int):
+ location_name = f"Level {level} {skill}"
+ return multiworld.get_location(location_name, player)
+
+
+def set_vanilla_skill_rule(logic: StardewLogic, multiworld, player, skill: str, level: int):
+ rule = logic.skill.can_earn_level(skill, level)
+ MultiWorldRules.set_rule(get_skill_level_location(multiworld, player, skill, level), rule)
+
+
+def set_modded_skill_rule(logic: StardewLogic, multiworld, player, skill: str, level: int):
+ rule = logic.skill.can_earn_level(skill, level)
+ MultiWorldRules.set_rule(get_skill_level_location(multiworld, player, skill, level), rule)
+
+
+def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ set_mines_floor_entrance_rules(logic, multiworld, player)
+ set_skull_cavern_floor_entrance_rules(logic, multiworld, player)
+ set_blacksmith_entrance_rules(logic, multiworld, player)
+ set_skill_entrance_rules(logic, multiworld, player, world_options)
+ set_traveling_merchant_day_rules(logic, multiworld, player)
+ set_dangerous_mine_rules(logic, multiworld, player, world_options)
+
+ set_entrance_rule(multiworld, player, Entrance.enter_tide_pools, logic.received("Beach Bridge") | (logic.mod.magic.can_blink()))
+ set_entrance_rule(multiworld, player, Entrance.enter_quarry, logic.received("Bridge Repair") | (logic.mod.magic.can_blink()))
+ set_entrance_rule(multiworld, player, Entrance.enter_secret_woods, logic.tool.has_tool(Tool.axe, "Iron") | (logic.mod.magic.can_blink()))
+ set_entrance_rule(multiworld, player, Entrance.forest_to_sewer, logic.wallet.has_rusty_key())
+ set_entrance_rule(multiworld, player, Entrance.town_to_sewer, logic.wallet.has_rusty_key())
+ set_entrance_rule(multiworld, player, Entrance.enter_abandoned_jojamart, logic.has_abandoned_jojamart())
+ movie_theater_rule = logic.has_movie_theater()
+ set_entrance_rule(multiworld, player, Entrance.enter_movie_theater, movie_theater_rule)
+ set_entrance_rule(multiworld, player, Entrance.purchase_movie_ticket, movie_theater_rule)
+ set_entrance_rule(multiworld, player, Entrance.take_bus_to_desert, logic.received("Bus Repair"))
+ set_entrance_rule(multiworld, player, Entrance.enter_skull_cavern, logic.received(Wallet.skull_key))
+ set_entrance_rule(multiworld, player, LogicEntrance.talk_to_mines_dwarf,
+ logic.wallet.can_speak_dwarf() & logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron))
+ set_entrance_rule(multiworld, player, LogicEntrance.buy_from_traveling_merchant, logic.traveling_merchant.has_days())
+ set_entrance_rule(multiworld, player, LogicEntrance.buy_from_raccoon, logic.quest.has_raccoon_shop())
+ set_entrance_rule(multiworld, player, LogicEntrance.fish_in_waterfall,
+ logic.skill.has_level(Skill.fishing, 5) & logic.tool.has_fishing_rod(2))
+
+ set_farm_buildings_entrance_rules(logic, multiworld, player)
+
+ set_entrance_rule(multiworld, player, Entrance.mountain_to_railroad, logic.received("Railroad Boulder Removed"))
+ set_entrance_rule(multiworld, player, Entrance.enter_witch_warp_cave, logic.quest.has_dark_talisman() | (logic.mod.magic.can_blink()))
+ set_entrance_rule(multiworld, player, Entrance.enter_witch_hut, (logic.has(ArtisanGood.void_mayonnaise) | logic.mod.magic.can_blink()))
+ set_entrance_rule(multiworld, player, Entrance.enter_mutant_bug_lair,
+ (logic.received(Event.start_dark_talisman_quest) & logic.relationship.can_meet(NPC.krobus)) | logic.mod.magic.can_blink())
+ set_entrance_rule(multiworld, player, Entrance.enter_casino, logic.quest.has_club_card())
+
+ set_bedroom_entrance_rules(logic, multiworld, player, world_options)
+ set_festival_entrance_rules(logic, multiworld, player)
+ set_island_entrance_rule(multiworld, player, LogicEntrance.island_cooking, logic.cooking.can_cook_in_kitchen, world_options)
+ set_entrance_rule(multiworld, player, LogicEntrance.farmhouse_cooking, logic.cooking.can_cook_in_kitchen)
+ set_entrance_rule(multiworld, player, LogicEntrance.shipping, logic.shipping.can_use_shipping_bin)
+ set_entrance_rule(multiworld, player, LogicEntrance.watch_queen_of_sauce, logic.action.can_watch(Channel.queen_of_sauce))
+ set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave())
+ set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave())
+ set_entrance_rule(multiworld, player, LogicEntrance.buy_experience_books, logic.time.has_lived_months(2))
+ set_entrance_rule(multiworld, player, LogicEntrance.buy_year1_books, logic.time.has_year_two)
+ set_entrance_rule(multiworld, player, LogicEntrance.buy_year3_books, logic.time.has_year_three)
+
+
+def set_dangerous_mine_rules(logic, multiworld, player, world_options: StardewValleyOptions):
+ if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true:
+ return
+ dangerous_mine_rule = logic.mine.has_mine_elevator_to_floor(120) & logic.region.can_reach(Region.qi_walnut_room)
+ set_entrance_rule(multiworld, player, Entrance.dig_to_dangerous_mines_20, dangerous_mine_rule)
+ set_entrance_rule(multiworld, player, Entrance.dig_to_dangerous_mines_60, dangerous_mine_rule)
+ set_entrance_rule(multiworld, player, Entrance.dig_to_dangerous_mines_100, dangerous_mine_rule)
+ set_entrance_rule(multiworld, player, Entrance.enter_dangerous_skull_cavern,
+ (logic.received(Wallet.skull_key) & logic.region.can_reach(Region.qi_walnut_room)))
+
+
+def set_farm_buildings_entrance_rules(logic, multiworld, player):
+ set_entrance_rule(multiworld, player, Entrance.downstairs_to_cellar, logic.building.has_house(3))
+ set_entrance_rule(multiworld, player, Entrance.use_desert_obelisk, logic.can_use_obelisk(Transportation.desert_obelisk))
+ set_entrance_rule(multiworld, player, Entrance.enter_greenhouse, logic.received("Greenhouse"))
+ set_entrance_rule(multiworld, player, Entrance.enter_coop, logic.building.has_building(Building.coop))
+ set_entrance_rule(multiworld, player, Entrance.enter_barn, logic.building.has_building(Building.barn))
+ set_entrance_rule(multiworld, player, Entrance.enter_shed, logic.building.has_building(Building.shed))
+ set_entrance_rule(multiworld, player, Entrance.enter_slime_hutch, logic.building.has_building(Building.slime_hutch))
+
+
+def set_bedroom_entrance_rules(logic, multiworld, player, world_options: StardewValleyOptions):
+ set_entrance_rule(multiworld, player, Entrance.enter_harvey_room, logic.relationship.has_hearts(NPC.harvey, 2))
+ set_entrance_rule(multiworld, player, Entrance.mountain_to_maru_room, logic.relationship.has_hearts(NPC.maru, 2))
+ set_entrance_rule(multiworld, player, Entrance.enter_sebastian_room, (logic.relationship.has_hearts(NPC.sebastian, 2) | logic.mod.magic.can_blink()))
+ set_entrance_rule(multiworld, player, Entrance.forest_to_leah_cottage, logic.relationship.has_hearts(NPC.leah, 2))
+ set_entrance_rule(multiworld, player, Entrance.enter_elliott_house, logic.relationship.has_hearts(NPC.elliott, 2))
+ set_entrance_rule(multiworld, player, Entrance.enter_sunroom, logic.relationship.has_hearts(NPC.caroline, 2))
+ set_entrance_rule(multiworld, player, Entrance.enter_wizard_basement, logic.relationship.has_hearts(NPC.wizard, 4))
+ if ModNames.alec in world_options.mods:
+ set_entrance_rule(multiworld, player, AlecEntrance.petshop_to_bedroom, (logic.relationship.has_hearts(ModNPC.alec, 2) | logic.mod.magic.can_blink()))
+ if ModNames.lacey in world_options.mods:
+ set_entrance_rule(multiworld, player, LaceyEntrance.forest_to_hat_house, logic.relationship.has_hearts(ModNPC.lacey, 2))
+
+
+def set_mines_floor_entrance_rules(logic, multiworld, player):
for floor in range(5, 120 + 5, 5):
- MultiWorldRules.set_rule(multi_world.get_entrance(dig_to_mines_floor(floor), player),
- logic.can_mine_to_floor(floor).simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_tide_pools, player),
- logic.received("Beach Bridge") | (magic.can_blink(logic)).simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_quarry, player),
- logic.received("Bridge Repair") | (magic.can_blink(logic)).simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_secret_woods, player),
- logic.has_tool(Tool.axe, "Iron") | (magic.can_blink(logic)).simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.forest_to_sewer, player),
- logic.has_rusty_key().simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.town_to_sewer, player),
- logic.has_rusty_key().simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.take_bus_to_desert, player),
- logic.received("Bus Repair").simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_skull_cavern, player),
- logic.received(Wallet.skull_key).simplify())
+ rule = logic.mine.has_mine_elevator_to_floor(floor - 10)
+ if floor == 5 or floor == 45 or floor == 85:
+ rule = rule & logic.mine.can_progress_in_the_mines_from_floor(floor)
+ entrance = multiworld.get_entrance(dig_to_mines_floor(floor), player)
+ MultiWorldRules.set_rule(entrance, rule)
+
+
+def set_skull_cavern_floor_entrance_rules(logic, multiworld, player):
for floor in range(25, 200 + 25, 25):
- MultiWorldRules.set_rule(multi_world.get_entrance(dig_to_skull_floor(floor), player),
- logic.can_mine_to_skull_cavern_floor(floor).simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.talk_to_mines_dwarf, player),
- logic.can_speak_dwarf() & logic.has_tool(Tool.pickaxe, ToolMaterial.iron))
-
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.use_desert_obelisk, player),
- logic.received(Transportation.desert_obelisk).simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.use_island_obelisk, player),
- logic.received(Transportation.island_obelisk).simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.use_farm_obelisk, player),
- logic.received(Transportation.farm_obelisk).simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.buy_from_traveling_merchant, player),
- logic.has_traveling_merchant())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_greenhouse, player),
- logic.received("Greenhouse"))
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.mountain_to_adventurer_guild, player),
- logic.received("Adventurer's Guild"))
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.mountain_to_railroad, player),
- logic.has_lived_months(2))
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_witch_warp_cave, player),
- logic.received(Wallet.dark_talisman) | (magic.can_blink(logic)).simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_witch_hut, player),
- (logic.has(ArtisanGood.void_mayonnaise) | magic.can_blink(logic)).simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_mutant_bug_lair, player),
- ((logic.has_rusty_key() & logic.can_reach_region(Region.railroad) &
- logic.can_meet(NPC.krobus) | magic.can_blink(logic)).simplify()))
-
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_harvey_room, player),
- logic.has_relationship(NPC.harvey, 2))
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.mountain_to_maru_room, player),
- logic.has_relationship(NPC.maru, 2))
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_sebastian_room, player),
- (logic.has_relationship(NPC.sebastian, 2) | magic.can_blink(logic)).simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.forest_to_leah_cottage, player),
- logic.has_relationship(NPC.leah, 2))
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_elliott_house, player),
- logic.has_relationship(NPC.elliott, 2))
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_sunroom, player),
- logic.has_relationship(NPC.caroline, 2))
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.enter_wizard_basement, player),
- logic.has_relationship(NPC.wizard, 4))
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.mountain_to_leo_treehouse, player),
- logic.received("Treehouse"))
- if ModNames.alec in world_options[options.Mods]:
- MultiWorldRules.set_rule(multi_world.get_entrance(AlecEntrance.petshop_to_bedroom, player),
- (logic.has_relationship(ModNPC.alec, 2) | magic.can_blink(logic)).simplify())
-
-
-def set_ginger_island_rules(logic: StardewLogic, multi_world, player, world_options: StardewOptions):
- set_island_entrances_rules(logic, multi_world, player)
- if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
- return
-
- set_boat_repair_rules(logic, multi_world, player)
- set_island_parrot_rules(logic, multi_world, player)
- MultiWorldRules.add_rule(multi_world.get_location("Open Professor Snail Cave", player),
- logic.has(Craftable.cherry_bomb).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Complete Island Field Office", player),
- logic.can_complete_field_office().simplify())
-
-
-def set_boat_repair_rules(logic: StardewLogic, multi_world, player):
- MultiWorldRules.add_rule(multi_world.get_location("Repair Boat Hull", player),
- logic.has(Material.hardwood).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Repair Boat Anchor", player),
- logic.has(MetalBar.iridium).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Repair Ticket Machine", player),
- logic.has(ArtisanGood.battery_pack).simplify())
-
-
-def set_island_entrances_rules(logic: StardewLogic, multi_world, player):
- boat_repaired = logic.received(Transportation.boat_repair).simplify()
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.fish_shop_to_boat_tunnel, player),
- boat_repaired)
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.boat_to_ginger_island, player),
- boat_repaired)
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_south_to_west, player),
- logic.received("Island West Turtle").simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_south_to_north, player),
- logic.received("Island North Turtle").simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_west_to_islandfarmhouse, player),
- logic.received("Island Farmhouse").simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_west_to_gourmand_cave, player),
- logic.received("Island Farmhouse").simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_north_to_dig_site, player),
- logic.received("Dig Site Bridge").simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.dig_site_to_professor_snail_cave, player),
- logic.received("Open Professor Snail Cave").simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.talk_to_island_trader, player),
- logic.received("Island Trader").simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_south_to_southeast, player),
- logic.received("Island Resort").simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.use_island_resort, player),
- logic.received("Island Resort").simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_west_to_qi_walnut_room, player),
- logic.received("Qi Walnut Room").simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.island_north_to_volcano, player),
- (logic.can_water(0) | logic.received("Volcano Bridge") |
- magic.can_blink(logic)).simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.volcano_to_secret_beach, player),
- logic.can_water(2).simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.climb_to_volcano_5, player),
- (logic.can_mine_perfectly() & logic.can_water(1)).simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.talk_to_volcano_dwarf, player),
- logic.can_speak_dwarf())
- MultiWorldRules.set_rule(multi_world.get_entrance(Entrance.climb_to_volcano_10, player),
- (logic.can_mine_perfectly() & logic.can_water(1) & logic.received("Volcano Exit Shortcut")).simplify())
+ rule = logic.mod.elevator.has_skull_cavern_elevator_to_floor(floor - 25)
+ if floor == 25 or floor == 75 or floor == 125:
+ rule = rule & logic.mine.can_progress_in_the_skull_cavern_from_floor(floor)
+ entrance = multiworld.get_entrance(dig_to_skull_floor(floor), player)
+ MultiWorldRules.set_rule(entrance, rule)
+
+
+def set_blacksmith_entrance_rules(logic, multiworld, player):
+ set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_copper, MetalBar.copper, ToolMaterial.copper)
+ set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iron, MetalBar.iron, ToolMaterial.iron)
+ set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_gold, MetalBar.gold, ToolMaterial.gold)
+ set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iridium, MetalBar.iridium, ToolMaterial.iridium)
+
+
+def set_skill_entrance_rules(logic, multiworld, player, world_options: StardewValleyOptions):
+ set_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops, logic.farming.has_farming_tools & logic.season.has_spring)
+ set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops, logic.farming.has_farming_tools & logic.season.has_summer)
+ set_entrance_rule(multiworld, player, LogicEntrance.grow_fall_crops, logic.farming.has_farming_tools & logic.season.has_fall)
+ set_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops_in_greenhouse, logic.farming.has_farming_tools)
+ set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops_in_greenhouse, logic.farming.has_farming_tools)
+ set_entrance_rule(multiworld, player, LogicEntrance.grow_fall_crops_in_greenhouse, logic.farming.has_farming_tools)
+ set_entrance_rule(multiworld, player, LogicEntrance.grow_indoor_crops_in_greenhouse, logic.farming.has_farming_tools)
+ set_island_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops_on_island, logic.farming.has_farming_tools, world_options)
+ set_island_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops_on_island, logic.farming.has_farming_tools, world_options)
+ set_island_entrance_rule(multiworld, player, LogicEntrance.grow_fall_crops_on_island, logic.farming.has_farming_tools, world_options)
+ set_island_entrance_rule(multiworld, player, LogicEntrance.grow_indoor_crops_on_island, logic.farming.has_farming_tools, world_options)
+ set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_fall_crops_in_summer, true_)
+ set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_fall_crops_in_fall, true_)
+
+ set_entrance_rule(multiworld, player, LogicEntrance.fishing, logic.skill.can_get_fishing_xp)
+
+
+def set_blacksmith_upgrade_rule(logic, multiworld, player, entrance_name: str, item_name: str, tool_material: str):
+ material_entrance = multiworld.get_entrance(entrance_name, player)
+ upgrade_rule = logic.has(item_name) & logic.money.can_spend(tool_upgrade_prices[tool_material])
+ MultiWorldRules.set_rule(material_entrance, upgrade_rule)
+
+
+def set_festival_entrance_rules(logic, multiworld, player):
+ set_entrance_rule(multiworld, player, LogicEntrance.attend_egg_festival, logic.season.has(Season.spring))
+ set_entrance_rule(multiworld, player, LogicEntrance.attend_desert_festival, logic.season.has(Season.spring) & logic.received("Bus Repair"))
+ set_entrance_rule(multiworld, player, LogicEntrance.attend_flower_dance, logic.season.has(Season.spring))
+
+ set_entrance_rule(multiworld, player, LogicEntrance.attend_luau, logic.season.has(Season.summer))
+ set_entrance_rule(multiworld, player, LogicEntrance.attend_trout_derby, logic.season.has(Season.summer))
+ set_entrance_rule(multiworld, player, LogicEntrance.attend_moonlight_jellies, logic.season.has(Season.summer))
+
+ set_entrance_rule(multiworld, player, LogicEntrance.attend_fair, logic.season.has(Season.fall))
+ set_entrance_rule(multiworld, player, LogicEntrance.attend_spirit_eve, logic.season.has(Season.fall))
+
+ set_entrance_rule(multiworld, player, LogicEntrance.attend_festival_of_ice, logic.season.has(Season.winter))
+ set_entrance_rule(multiworld, player, LogicEntrance.attend_squidfest, logic.season.has(Season.winter))
+ set_entrance_rule(multiworld, player, LogicEntrance.attend_night_market, logic.season.has(Season.winter))
+ set_entrance_rule(multiworld, player, LogicEntrance.attend_winter_star, logic.season.has(Season.winter))
+
+
+def set_ginger_island_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ set_island_entrances_rules(logic, multiworld, player, world_options)
+ if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true:
+ return
+
+ set_boat_repair_rules(logic, multiworld, player)
+ set_island_parrot_rules(logic, multiworld, player)
+ MultiWorldRules.add_rule(multiworld.get_location("Open Professor Snail Cave", player),
+ logic.has(Bomb.cherry_bomb))
+ MultiWorldRules.add_rule(multiworld.get_location("Complete Island Field Office", player),
+ logic.walnut.can_complete_field_office())
+ set_walnut_rules(logic, multiworld, player, world_options)
+
+
+def set_boat_repair_rules(logic: StardewLogic, multiworld, player):
+ MultiWorldRules.add_rule(multiworld.get_location("Repair Boat Hull", player),
+ logic.has(Material.hardwood))
+ MultiWorldRules.add_rule(multiworld.get_location("Repair Boat Anchor", player),
+ logic.has(MetalBar.iridium))
+ MultiWorldRules.add_rule(multiworld.get_location("Repair Ticket Machine", player),
+ logic.has(ArtisanGood.battery_pack))
+
+
+def set_island_entrances_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ boat_repaired = logic.received(Transportation.boat_repair)
+ dig_site_rule = logic.received("Dig Site Bridge")
+ entrance_rules = {
+ Entrance.use_island_obelisk: logic.can_use_obelisk(Transportation.island_obelisk),
+ Entrance.use_farm_obelisk: logic.can_use_obelisk(Transportation.farm_obelisk),
+ Entrance.fish_shop_to_boat_tunnel: boat_repaired,
+ Entrance.boat_to_ginger_island: boat_repaired,
+ Entrance.island_south_to_west: logic.received("Island West Turtle"),
+ Entrance.island_south_to_north: logic.received("Island North Turtle"),
+ Entrance.island_west_to_islandfarmhouse: logic.received("Island Farmhouse"),
+ Entrance.island_west_to_gourmand_cave: logic.received("Island Farmhouse"),
+ Entrance.island_north_to_dig_site: dig_site_rule,
+ Entrance.dig_site_to_professor_snail_cave: logic.received("Open Professor Snail Cave"),
+ Entrance.talk_to_island_trader: logic.received("Island Trader"),
+ Entrance.island_south_to_southeast: logic.received("Island Resort"),
+ Entrance.use_island_resort: logic.received("Island Resort"),
+ Entrance.island_west_to_qi_walnut_room: logic.received("Qi Walnut Room"),
+ Entrance.island_north_to_volcano: logic.tool.can_water(0) | logic.received("Volcano Bridge") | logic.mod.magic.can_blink(),
+ Entrance.volcano_to_secret_beach: logic.tool.can_water(2),
+ Entrance.climb_to_volcano_5: logic.ability.can_mine_perfectly() & logic.tool.can_water(1),
+ Entrance.talk_to_volcano_dwarf: logic.wallet.can_speak_dwarf(),
+ Entrance.climb_to_volcano_10: logic.ability.can_mine_perfectly() & logic.tool.can_water(1),
+ Entrance.mountain_to_leo_treehouse: logic.received("Treehouse"),
+ }
parrots = [Entrance.parrot_express_docks_to_volcano, Entrance.parrot_express_jungle_to_volcano,
Entrance.parrot_express_dig_site_to_volcano, Entrance.parrot_express_docks_to_dig_site,
Entrance.parrot_express_jungle_to_dig_site, Entrance.parrot_express_volcano_to_dig_site,
Entrance.parrot_express_docks_to_jungle, Entrance.parrot_express_dig_site_to_jungle,
Entrance.parrot_express_volcano_to_jungle, Entrance.parrot_express_jungle_to_docks,
Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_volcano_to_docks]
+ parrot_express_rule = logic.received(Transportation.parrot_express)
+ parrot_express_to_dig_site_rule = dig_site_rule & parrot_express_rule
for parrot in parrots:
- MultiWorldRules.set_rule(multi_world.get_entrance(parrot, player), logic.received(Transportation.parrot_express).simplify())
+ if "Dig Site" in parrot:
+ entrance_rules[parrot] = parrot_express_to_dig_site_rule
+ else:
+ entrance_rules[parrot] = parrot_express_rule
+ set_many_island_entrances_rules(multiworld, player, entrance_rules, world_options)
-def set_island_parrot_rules(logic: StardewLogic, multi_world, player):
- has_walnut = logic.has_walnut(1).simplify()
- has_5_walnut = logic.has_walnut(5).simplify()
- has_10_walnut = logic.has_walnut(10).simplify()
- has_20_walnut = logic.has_walnut(20).simplify()
- MultiWorldRules.add_rule(multi_world.get_location("Leo's Parrot", player),
+
+def set_island_parrot_rules(logic: StardewLogic, multiworld, player):
+ # Logic rules require more walnuts than in reality, to allow the player to spend them "wrong"
+ has_walnut = logic.walnut.has_walnut(5)
+ has_5_walnut = logic.walnut.has_walnut(15)
+ has_10_walnut = logic.walnut.has_walnut(40)
+ has_20_walnut = logic.walnut.has_walnut(60)
+ MultiWorldRules.add_rule(multiworld.get_location("Leo's Parrot", player),
has_walnut)
- MultiWorldRules.add_rule(multi_world.get_location("Island West Turtle", player),
+ MultiWorldRules.add_rule(multiworld.get_location("Island West Turtle", player),
has_10_walnut & logic.received("Island North Turtle"))
- MultiWorldRules.add_rule(multi_world.get_location("Island Farmhouse", player),
+ MultiWorldRules.add_rule(multiworld.get_location("Island Farmhouse", player),
has_20_walnut)
- MultiWorldRules.add_rule(multi_world.get_location("Island Mailbox", player),
+ MultiWorldRules.add_rule(multiworld.get_location("Island Mailbox", player),
has_5_walnut & logic.received("Island Farmhouse"))
- MultiWorldRules.add_rule(multi_world.get_location(Transportation.farm_obelisk, player),
+ MultiWorldRules.add_rule(multiworld.get_location(Transportation.farm_obelisk, player),
has_20_walnut & logic.received("Island Mailbox"))
- MultiWorldRules.add_rule(multi_world.get_location("Dig Site Bridge", player),
+ MultiWorldRules.add_rule(multiworld.get_location("Dig Site Bridge", player),
has_10_walnut & logic.received("Island West Turtle"))
- MultiWorldRules.add_rule(multi_world.get_location("Island Trader", player),
+ MultiWorldRules.add_rule(multiworld.get_location("Island Trader", player),
has_10_walnut & logic.received("Island Farmhouse"))
- MultiWorldRules.add_rule(multi_world.get_location("Volcano Bridge", player),
+ MultiWorldRules.add_rule(multiworld.get_location("Volcano Bridge", player),
has_5_walnut & logic.received("Island West Turtle") &
- logic.can_reach_region(Region.volcano_floor_10))
- MultiWorldRules.add_rule(multi_world.get_location("Volcano Exit Shortcut", player),
+ logic.region.can_reach(Region.volcano_floor_10))
+ MultiWorldRules.add_rule(multiworld.get_location("Volcano Exit Shortcut", player),
has_5_walnut & logic.received("Island West Turtle"))
- MultiWorldRules.add_rule(multi_world.get_location("Island Resort", player),
+ MultiWorldRules.add_rule(multiworld.get_location("Island Resort", player),
has_20_walnut & logic.received("Island Farmhouse"))
- MultiWorldRules.add_rule(multi_world.get_location(Transportation.parrot_express, player),
+ MultiWorldRules.add_rule(multiworld.get_location(Transportation.parrot_express, player),
has_10_walnut)
-def set_cropsanity_rules(all_location_names: List[str], logic, multi_world, player, world_options: StardewOptions):
- if world_options[options.Cropsanity] == options.Cropsanity.option_disabled:
+def set_walnut_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ if world_options.walnutsanity == Walnutsanity.preset_none:
return
- harvest_prefix = "Harvest "
- harvest_prefix_length = len(harvest_prefix)
- for harvest_location in locations.locations_by_tag[LocationTags.CROPSANITY]:
- if harvest_location.name in all_location_names and (harvest_location.mod_name is None or harvest_location.mod_name in world_options[options.Mods]):
- crop_name = harvest_location.name[harvest_prefix_length:]
- MultiWorldRules.set_rule(multi_world.get_location(harvest_location.name, player),
- logic.has(crop_name).simplify())
+ set_walnut_puzzle_rules(logic, multiworld, player, world_options)
+ set_walnut_bushes_rules(logic, multiworld, player, world_options)
+ set_walnut_dig_spot_rules(logic, multiworld, player, world_options)
+ set_walnut_repeatable_rules(logic, multiworld, player, world_options)
+
+def set_walnut_puzzle_rules(logic: StardewLogic, multiworld, player, world_options):
+ if OptionName.walnutsanity_puzzles not in world_options.walnutsanity:
+ return
-def set_story_quests_rules(all_location_names: List[str], logic, multi_world, player, world_options: StardewOptions):
- for quest in locations.locations_by_tag[LocationTags.QUEST]:
- if quest.name in all_location_names and (quest.mod_name is None or quest.mod_name in world_options[options.Mods]):
- MultiWorldRules.set_rule(multi_world.get_location(quest.name, player),
- logic.quest_rules[quest.name].simplify())
+ MultiWorldRules.add_rule(multiworld.get_location("Open Golden Coconut", player), logic.has(Geode.golden_coconut))
+ MultiWorldRules.add_rule(multiworld.get_location("Banana Altar", player), logic.has(Fruit.banana))
+ MultiWorldRules.add_rule(multiworld.get_location("Leo's Tree", player), logic.tool.has_tool(Tool.axe))
+ MultiWorldRules.add_rule(multiworld.get_location("Gem Birds Shrine", player), logic.has(Mineral.amethyst) & logic.has(Mineral.aquamarine) &
+ logic.has(Mineral.emerald) & logic.has(Mineral.ruby) & logic.has(Mineral.topaz) &
+ logic.region.can_reach_all((Region.island_north, Region.island_west, Region.island_east, Region.island_south)))
+ MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Melon", player), logic.has(Fruit.melon) & logic.region.can_reach(Region.island_west))
+ MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) &
+ logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Melon"))
+ MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) &
+ logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Wheat"))
+ MultiWorldRules.add_rule(multiworld.get_location("Whack A Mole", player), logic.tool.has_tool(Tool.watering_can, ToolMaterial.iridium))
+ MultiWorldRules.add_rule(multiworld.get_location("Complete Large Animal Collection", player), logic.walnut.can_complete_large_animal_collection())
+ MultiWorldRules.add_rule(multiworld.get_location("Complete Snake Collection", player), logic.walnut.can_complete_snake_collection())
+ MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Frog Collection", player), logic.walnut.can_complete_frog_collection())
+ MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Bat Collection", player), logic.walnut.can_complete_bat_collection())
+ MultiWorldRules.add_rule(multiworld.get_location("Purple Flowers Island Survey", player), logic.walnut.can_start_field_office)
+ MultiWorldRules.add_rule(multiworld.get_location("Purple Starfish Island Survey", player), logic.walnut.can_start_field_office)
+ MultiWorldRules.add_rule(multiworld.get_location("Protruding Tree Walnut", player), logic.combat.has_slingshot)
+ MultiWorldRules.add_rule(multiworld.get_location("Starfish Tide Pool", player), logic.tool.has_fishing_rod(1))
+ MultiWorldRules.add_rule(multiworld.get_location("Mermaid Song", player), logic.has(Furniture.flute_block))
+
+
+def set_walnut_bushes_rules(logic, multiworld, player, world_options):
+ if OptionName.walnutsanity_bushes not in world_options.walnutsanity:
+ return
+ # I don't think any of the bushes require something special, but that might change with ER
+ return
-def set_special_order_rules(all_location_names: List[str], logic: StardewLogic, multi_world, player,
- world_options: StardewOptions):
- if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled:
+def set_walnut_dig_spot_rules(logic, multiworld, player, world_options):
+ if OptionName.walnutsanity_dig_spots not in world_options.walnutsanity:
return
- board_rule = logic.received("Special Order Board") & logic.has_lived_months(4)
- for board_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]:
- if board_order.name in all_location_names:
- order_rule = board_rule & logic.special_order_rules[board_order.name]
- MultiWorldRules.set_rule(multi_world.get_location(board_order.name, player), order_rule.simplify())
- if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
+ for dig_spot_walnut in locations.locations_by_tag[LocationTags.WALNUTSANITY_DIG]:
+ rule = logic.tool.has_tool(Tool.hoe)
+ if "Journal Scrap" in dig_spot_walnut.name:
+ rule = rule & logic.has(Forageable.journal_scrap)
+ if "Starfish Diamond" in dig_spot_walnut.name:
+ rule = rule & logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron)
+ MultiWorldRules.set_rule(multiworld.get_location(dig_spot_walnut.name, player), rule)
+
+
+def set_walnut_repeatable_rules(logic, multiworld, player, world_options):
+ if OptionName.walnutsanity_repeatables not in world_options.walnutsanity:
return
- if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_board_only:
+ for i in range(1, 6):
+ MultiWorldRules.set_rule(multiworld.get_location(f"Fishing Walnut {i}", player), logic.tool.has_fishing_rod(1))
+ MultiWorldRules.set_rule(multiworld.get_location(f"Harvesting Walnut {i}", player), logic.skill.can_get_farming_xp)
+ MultiWorldRules.set_rule(multiworld.get_location(f"Mussel Node Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe))
+ MultiWorldRules.set_rule(multiworld.get_location(f"Volcano Rocks Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe))
+ MultiWorldRules.set_rule(multiworld.get_location(f"Volcano Monsters Walnut {i}", player), logic.combat.has_galaxy_weapon)
+ MultiWorldRules.set_rule(multiworld.get_location(f"Volcano Crates Walnut {i}", player), logic.combat.has_any_weapon)
+ MultiWorldRules.set_rule(multiworld.get_location(f"Tiger Slime Walnut", player), logic.monster.can_kill(Monster.tiger_slime))
+
+
+def set_cropsanity_rules(logic: StardewLogic, multiworld, player, world_content: StardewContent):
+ if not world_content.features.cropsanity.is_enabled:
return
- qi_rule = logic.can_reach_region(Region.qi_walnut_room) & logic.has_lived_months(8)
- for qi_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_QI]:
- if qi_order.name in all_location_names:
- order_rule = qi_rule & logic.special_order_rules[qi_order.name]
- MultiWorldRules.set_rule(multi_world.get_location(qi_order.name, player), order_rule.simplify())
+
+ for item in world_content.find_tagged_items(ItemTag.CROPSANITY):
+ location = world_content.features.cropsanity.to_location_name(item.name)
+ harvest_sources = (source for source in item.sources if isinstance(source, (HarvestFruitTreeSource, HarvestCropSource)))
+ MultiWorldRules.set_rule(multiworld.get_location(location, player), logic.source.has_access_to_any(harvest_sources))
+
+
+def set_story_quests_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ if world_options.quest_locations < 0:
+ return
+ for quest in locations.locations_by_tag[LocationTags.STORY_QUEST]:
+ if quest.name in all_location_names and (quest.mod_name is None or quest.mod_name in world_options.mods):
+ MultiWorldRules.set_rule(multiworld.get_location(quest.name, player),
+ logic.registry.quest_rules[quest.name])
+
+
+def set_special_order_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player,
+ world_options: StardewValleyOptions):
+ if world_options.special_order_locations & SpecialOrderLocations.option_board:
+ board_rule = logic.received("Special Order Board") & logic.time.has_lived_months(4)
+ for board_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]:
+ if board_order.name in all_location_names:
+ order_rule = board_rule & logic.registry.special_order_rules[board_order.name]
+ MultiWorldRules.set_rule(multiworld.get_location(board_order.name, player), order_rule)
+
+ if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true:
+ return
+ if world_options.special_order_locations & SpecialOrderLocations.value_qi:
+ qi_rule = logic.region.can_reach(Region.qi_walnut_room) & logic.time.has_lived_months(8)
+ for qi_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_QI]:
+ if qi_order.name in all_location_names:
+ order_rule = qi_rule & logic.registry.special_order_rules[qi_order.name]
+ MultiWorldRules.set_rule(multiworld.get_location(qi_order.name, player), order_rule)
help_wanted_prefix = "Help Wanted:"
@@ -362,66 +576,66 @@ def set_special_order_rules(all_location_names: List[str], logic: StardewLogic,
slay_monsters = "Slay Monsters"
-def set_help_wanted_quests_rules(logic: StardewLogic, multi_world, player, world_options):
- help_wanted_number = world_options[options.HelpWantedLocations]
+def set_help_wanted_quests_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ help_wanted_number = world_options.quest_locations.value
+ if help_wanted_number < 0:
+ return
for i in range(0, help_wanted_number):
set_number = i // 7
- month_rule = logic.has_lived_months(set_number).simplify()
+ month_rule = logic.time.has_lived_months(set_number)
quest_number = set_number + 1
quest_number_in_set = i % 7
if quest_number_in_set < 4:
quest_number = set_number * 4 + quest_number_in_set + 1
- set_help_wanted_delivery_rule(multi_world, player, month_rule, quest_number)
+ set_help_wanted_delivery_rule(multiworld, player, month_rule, quest_number)
elif quest_number_in_set == 4:
- set_help_wanted_fishing_rule(logic, multi_world, player, month_rule, quest_number)
+ set_help_wanted_fishing_rule(multiworld, player, month_rule, quest_number)
elif quest_number_in_set == 5:
- set_help_wanted_slay_monsters_rule(logic, multi_world, player, month_rule, quest_number)
+ set_help_wanted_slay_monsters_rule(multiworld, player, month_rule, quest_number)
elif quest_number_in_set == 6:
- set_help_wanted_gathering_rule(multi_world, player, month_rule, quest_number)
+ set_help_wanted_gathering_rule(multiworld, player, month_rule, quest_number)
-def set_help_wanted_delivery_rule(multi_world, player, month_rule, quest_number):
+def set_help_wanted_delivery_rule(multiworld, player, month_rule, quest_number):
location_name = f"{help_wanted_prefix} {item_delivery} {quest_number}"
- MultiWorldRules.set_rule(multi_world.get_location(location_name, player), month_rule)
+ MultiWorldRules.set_rule(multiworld.get_location(location_name, player), month_rule)
-def set_help_wanted_gathering_rule(multi_world, player, month_rule, quest_number):
+def set_help_wanted_gathering_rule(multiworld, player, month_rule, quest_number):
location_name = f"{help_wanted_prefix} {gathering} {quest_number}"
- MultiWorldRules.set_rule(multi_world.get_location(location_name, player), month_rule)
+ MultiWorldRules.set_rule(multiworld.get_location(location_name, player), month_rule)
-def set_help_wanted_fishing_rule(logic: StardewLogic, multi_world, player, month_rule, quest_number):
+def set_help_wanted_fishing_rule(multiworld, player, month_rule, quest_number):
location_name = f"{help_wanted_prefix} {fishing} {quest_number}"
- fishing_rule = month_rule & logic.can_fish()
- MultiWorldRules.set_rule(multi_world.get_location(location_name, player), fishing_rule.simplify())
+ MultiWorldRules.set_rule(multiworld.get_location(location_name, player), month_rule)
-def set_help_wanted_slay_monsters_rule(logic: StardewLogic, multi_world, player, month_rule, quest_number):
+def set_help_wanted_slay_monsters_rule(multiworld, player, month_rule, quest_number):
location_name = f"{help_wanted_prefix} {slay_monsters} {quest_number}"
- slay_rule = month_rule & logic.can_do_combat_at_level("Basic")
- MultiWorldRules.set_rule(multi_world.get_location(location_name, player), slay_rule.simplify())
+ MultiWorldRules.set_rule(multiworld.get_location(location_name, player), month_rule)
-def set_fishsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int):
+def set_fishsanity_rules(all_location_names: Set[str], logic: StardewLogic, multiworld: MultiWorld, player: int):
fish_prefix = "Fishsanity: "
for fish_location in locations.locations_by_tag[LocationTags.FISHSANITY]:
if fish_location.name in all_location_names:
fish_name = fish_location.name[len(fish_prefix):]
- MultiWorldRules.set_rule(multi_world.get_location(fish_location.name, player),
- logic.has(fish_name).simplify())
+ MultiWorldRules.set_rule(multiworld.get_location(fish_location.name, player),
+ logic.has(fish_name))
-def set_museumsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int,
- world_options: StardewOptions):
+def set_museumsanity_rules(all_location_names: Set[str], logic: StardewLogic, multiworld: MultiWorld, player: int,
+ world_options: StardewValleyOptions):
museum_prefix = "Museumsanity: "
- if world_options[options.Museumsanity] == options.Museumsanity.option_milestones:
+ if world_options.museumsanity == Museumsanity.option_milestones:
for museum_milestone in locations.locations_by_tag[LocationTags.MUSEUM_MILESTONES]:
- set_museum_milestone_rule(logic, multi_world, museum_milestone, museum_prefix, player)
- elif world_options[options.Museumsanity] != options.Museumsanity.option_none:
- set_museum_individual_donations_rules(all_location_names, logic, multi_world, museum_prefix, player)
+ set_museum_milestone_rule(logic, multiworld, museum_milestone, museum_prefix, player)
+ elif world_options.museumsanity != Museumsanity.option_none:
+ set_museum_individual_donations_rules(all_location_names, logic, multiworld, museum_prefix, player)
-def set_museum_individual_donations_rules(all_location_names, logic: StardewLogic, multi_world, museum_prefix, player):
+def set_museum_individual_donations_rules(all_location_names, logic: StardewLogic, multiworld, museum_prefix, player):
all_donations = sorted(locations.locations_by_tag[LocationTags.MUSEUM_DONATIONS],
key=lambda x: all_museum_items_by_name[x.name[len(museum_prefix):]].difficulty, reverse=True)
counter = 0
@@ -429,200 +643,424 @@ def set_museum_individual_donations_rules(all_location_names, logic: StardewLogi
for museum_location in all_donations:
if museum_location.name in all_location_names:
donation_name = museum_location.name[len(museum_prefix):]
- required_detectors = counter * 5 // number_donations
- rule = logic.can_donate_museum_item(all_museum_items_by_name[donation_name]) & logic.received("Traveling Merchant Metal Detector", required_detectors)
- MultiWorldRules.set_rule(multi_world.get_location(museum_location.name, player),
- rule.simplify())
+ required_detectors = counter * 3 // number_donations
+ rule = logic.museum.can_find_museum_item(all_museum_items_by_name[donation_name]) & logic.received(Wallet.metal_detector, required_detectors)
+ MultiWorldRules.set_rule(multiworld.get_location(museum_location.name, player),
+ rule)
counter += 1
-def set_museum_milestone_rule(logic: StardewLogic, multi_world: MultiWorld, museum_milestone, museum_prefix: str,
+def set_museum_milestone_rule(logic: StardewLogic, multiworld: MultiWorld, museum_milestone, museum_prefix: str,
player: int):
milestone_name = museum_milestone.name[len(museum_prefix):]
donations_suffix = " Donations"
minerals_suffix = " Minerals"
artifacts_suffix = " Artifacts"
- metal_detector = "Traveling Merchant Metal Detector"
+ metal_detector = Wallet.metal_detector
rule = None
if milestone_name.endswith(donations_suffix):
- rule = get_museum_item_count_rule(logic, donations_suffix, milestone_name, all_museum_items, logic.can_donate_museum_items)
+ rule = get_museum_item_count_rule(logic, donations_suffix, milestone_name, all_museum_items, logic.museum.can_find_museum_items)
elif milestone_name.endswith(minerals_suffix):
- rule = get_museum_item_count_rule(logic, minerals_suffix, milestone_name, all_museum_minerals, logic.can_donate_museum_minerals)
+ rule = get_museum_item_count_rule(logic, minerals_suffix, milestone_name, all_museum_minerals, logic.museum.can_find_museum_minerals)
elif milestone_name.endswith(artifacts_suffix):
- rule = get_museum_item_count_rule(logic, artifacts_suffix, milestone_name, all_museum_artifacts, logic.can_donate_museum_artifacts)
+ rule = get_museum_item_count_rule(logic, artifacts_suffix, milestone_name, all_museum_artifacts, logic.museum.can_find_museum_artifacts)
elif milestone_name == "Dwarf Scrolls":
- rule = And([logic.can_donate_museum_item(item) for item in dwarf_scrolls]) & logic.received(metal_detector, 4)
+ rule = And(*(logic.museum.can_find_museum_item(item) for item in dwarf_scrolls)) & logic.received(metal_detector, 2)
elif milestone_name == "Skeleton Front":
- rule = And([logic.can_donate_museum_item(item) for item in skeleton_front]) & logic.received(metal_detector, 4)
+ rule = And(*(logic.museum.can_find_museum_item(item) for item in skeleton_front)) & logic.received(metal_detector, 2)
elif milestone_name == "Skeleton Middle":
- rule = And([logic.can_donate_museum_item(item) for item in skeleton_middle]) & logic.received(metal_detector, 4)
+ rule = And(*(logic.museum.can_find_museum_item(item) for item in skeleton_middle)) & logic.received(metal_detector, 2)
elif milestone_name == "Skeleton Back":
- rule = And([logic.can_donate_museum_item(item) for item in skeleton_back]) & logic.received(metal_detector, 4)
+ rule = And(*(logic.museum.can_find_museum_item(item) for item in skeleton_back)) & logic.received(metal_detector, 2)
elif milestone_name == "Ancient Seed":
- rule = logic.can_donate_museum_item(Artifact.ancient_seed) & logic.received(metal_detector, 4)
+ rule = logic.museum.can_find_museum_item(Artifact.ancient_seed) & logic.received(metal_detector, 2)
if rule is None:
return
- MultiWorldRules.set_rule(multi_world.get_location(museum_milestone.name, player), rule.simplify())
+ MultiWorldRules.set_rule(multiworld.get_location(museum_milestone.name, player), rule)
def get_museum_item_count_rule(logic: StardewLogic, suffix, milestone_name, accepted_items, donation_func):
- metal_detector = "Traveling Merchant Metal Detector"
+ metal_detector = Wallet.metal_detector
num = int(milestone_name[:milestone_name.index(suffix)])
- required_detectors = (num - 1) * 5 // len(accepted_items)
+ required_detectors = (num - 1) * 3 // len(accepted_items)
rule = donation_func(num) & logic.received(metal_detector, required_detectors)
return rule
-def set_backpack_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options):
- if world_options[options.BackpackProgression] != options.BackpackProgression.option_vanilla:
- MultiWorldRules.set_rule(multi_world.get_location("Large Pack", player),
- logic.can_spend_money(2000).simplify())
- MultiWorldRules.set_rule(multi_world.get_location("Deluxe Pack", player),
- (logic.can_spend_money(10000) & logic.received("Progressive Backpack")).simplify())
- if ModNames.big_backpack in world_options[options.Mods]:
- MultiWorldRules.set_rule(multi_world.get_location("Premium Pack", player),
- (logic.can_spend_money(150000) &
- logic.received("Progressive Backpack", 2)).simplify())
+def set_backpack_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
+ if world_options.backpack_progression != BackpackProgression.option_vanilla:
+ MultiWorldRules.set_rule(multiworld.get_location("Large Pack", player),
+ logic.money.can_spend(2000))
+ MultiWorldRules.set_rule(multiworld.get_location("Deluxe Pack", player),
+ (logic.money.can_spend(10000) & logic.received("Progressive Backpack")))
+ if ModNames.big_backpack in world_options.mods:
+ MultiWorldRules.set_rule(multiworld.get_location("Premium Pack", player),
+ (logic.money.can_spend(150000) &
+ logic.received("Progressive Backpack", 2)))
-def set_festival_rules(all_location_names: List[str], logic: StardewLogic, multi_world, player):
+def set_festival_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player):
festival_locations = []
festival_locations.extend(locations.locations_by_tag[LocationTags.FESTIVAL])
festival_locations.extend(locations.locations_by_tag[LocationTags.FESTIVAL_HARD])
for festival in festival_locations:
if festival.name in all_location_names:
- MultiWorldRules.set_rule(multi_world.get_location(festival.name, player),
- logic.festival_rules[festival.name].simplify())
+ MultiWorldRules.set_rule(multiworld.get_location(festival.name, player),
+ logic.registry.festival_rules[festival.name])
+
+
+monster_eradication_prefix = "Monster Eradication: "
+
+
+def set_monstersanity_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ monstersanity_option = world_options.monstersanity
+ if monstersanity_option == Monstersanity.option_none:
+ return
+
+ if monstersanity_option == Monstersanity.option_one_per_monster or monstersanity_option == Monstersanity.option_split_goals:
+ set_monstersanity_monster_rules(all_location_names, logic, multiworld, player, monstersanity_option)
+ return
+
+ if monstersanity_option == Monstersanity.option_progressive_goals:
+ set_monstersanity_progressive_category_rules(all_location_names, logic, multiworld, player)
+ return
+
+ set_monstersanity_category_rules(all_location_names, logic, multiworld, player, monstersanity_option)
+
+
+def set_monstersanity_monster_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, monstersanity_option):
+ for monster_name in logic.monster.all_monsters_by_name:
+ location_name = f"{monster_eradication_prefix}{monster_name}"
+ if location_name not in all_location_names:
+ continue
+ location = multiworld.get_location(location_name, player)
+ if monstersanity_option == Monstersanity.option_split_goals:
+ rule = logic.monster.can_kill_many(logic.monster.all_monsters_by_name[monster_name])
+ else:
+ rule = logic.monster.can_kill(logic.monster.all_monsters_by_name[monster_name])
+ MultiWorldRules.set_rule(location, rule)
-def set_traveling_merchant_rules(logic: StardewLogic, multi_world: MultiWorld, player: int):
+def set_monstersanity_progressive_category_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player):
+ for monster_category in logic.monster.all_monsters_by_category:
+ set_monstersanity_progressive_single_category_rules(all_location_names, logic, multiworld, player, monster_category)
+
+
+def set_monstersanity_progressive_single_category_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, monster_category: str):
+ location_names = [name for name in all_location_names if name.startswith(monster_eradication_prefix) and name.endswith(monster_category)]
+ if not location_names:
+ return
+ location_names = sorted(location_names, key=lambda name: get_monster_eradication_number(name, monster_category))
+ for i in range(5):
+ location_name = location_names[i]
+ set_monstersanity_progressive_category_rule(all_location_names, logic, multiworld, player, monster_category, location_name, i)
+
+
+def set_monstersanity_progressive_category_rule(all_location_names: Set[str], logic: StardewLogic, multiworld, player,
+ monster_category: str, location_name: str, goal_index):
+ if location_name not in all_location_names:
+ return
+ location = multiworld.get_location(location_name, player)
+ if goal_index < 3:
+ rule = logic.monster.can_kill_any(logic.monster.all_monsters_by_category[monster_category], goal_index + 1)
+ else:
+ rule = logic.monster.can_kill_any(logic.monster.all_monsters_by_category[monster_category], goal_index * 2)
+ MultiWorldRules.set_rule(location, rule)
+
+
+def get_monster_eradication_number(location_name, monster_category) -> int:
+ number = location_name[len(monster_eradication_prefix):-len(monster_category)]
+ number = number.strip()
+ if number.isdigit():
+ return int(number)
+ return 1000
+
+
+def set_monstersanity_category_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, monstersanity_option):
+ for monster_category in logic.monster.all_monsters_by_category:
+ location_name = f"{monster_eradication_prefix}{monster_category}"
+ if location_name not in all_location_names:
+ continue
+ location = multiworld.get_location(location_name, player)
+ if monstersanity_option == Monstersanity.option_one_per_category:
+ rule = logic.monster.can_kill_any(logic.monster.all_monsters_by_category[monster_category])
+ else:
+ rule = logic.monster.can_kill_any(logic.monster.all_monsters_by_category[monster_category], MAX_MONTHS)
+ MultiWorldRules.set_rule(location, rule)
+
+
+def set_shipsanity_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ shipsanity_option = world_options.shipsanity
+ if shipsanity_option == Shipsanity.option_none:
+ return
+
+ shipsanity_prefix = "Shipsanity: "
+ for location in locations.locations_by_tag[LocationTags.SHIPSANITY]:
+ if location.name not in all_location_names:
+ continue
+ item_to_ship = location.name[len(shipsanity_prefix):]
+ MultiWorldRules.set_rule(multiworld.get_location(location.name, player), logic.shipping.can_ship(item_to_ship))
+
+
+def set_cooksanity_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ cooksanity_option = world_options.cooksanity
+ if cooksanity_option == Cooksanity.option_none:
+ return
+
+ cooksanity_prefix = "Cook "
+ for location in locations.locations_by_tag[LocationTags.COOKSANITY]:
+ if location.name not in all_location_names:
+ continue
+ recipe_name = location.name[len(cooksanity_prefix):]
+ recipe = all_cooking_recipes_by_name[recipe_name]
+ cook_rule = logic.cooking.can_cook(recipe)
+ MultiWorldRules.set_rule(multiworld.get_location(location.name, player), cook_rule)
+
+
+def set_chefsanity_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ chefsanity_option = world_options.chefsanity
+ if chefsanity_option == Chefsanity.option_none:
+ return
+
+ chefsanity_suffix = " Recipe"
+ for location in locations.locations_by_tag[LocationTags.CHEFSANITY]:
+ if location.name not in all_location_names:
+ continue
+ recipe_name = location.name[:-len(chefsanity_suffix)]
+ recipe = all_cooking_recipes_by_name[recipe_name]
+ learn_rule = logic.cooking.can_learn_recipe(recipe.source)
+ MultiWorldRules.set_rule(multiworld.get_location(location.name, player), learn_rule)
+
+
+def set_craftsanity_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
+ craftsanity_option = world_options.craftsanity
+ if craftsanity_option == Craftsanity.option_none:
+ return
+
+ craft_prefix = "Craft "
+ craft_suffix = " Recipe"
+ for location in locations.locations_by_tag[LocationTags.CRAFTSANITY]:
+ if location.name not in all_location_names:
+ continue
+ if location.name.endswith(craft_suffix):
+ recipe_name = location.name[:-len(craft_suffix)]
+ recipe = all_crafting_recipes_by_name[recipe_name]
+ craft_rule = logic.crafting.can_learn_recipe(recipe)
+ else:
+ recipe_name = location.name[len(craft_prefix):]
+ recipe = all_crafting_recipes_by_name[recipe_name]
+ craft_rule = logic.crafting.can_craft(recipe)
+ MultiWorldRules.set_rule(multiworld.get_location(location.name, player), craft_rule)
+
+
+def set_booksanity_rules(logic: StardewLogic, multiworld, player, content: StardewContent):
+ booksanity = content.features.booksanity
+ if not booksanity.is_enabled:
+ return
+
+ for book in content.find_tagged_items(ItemTag.BOOK):
+ if booksanity.is_included(book):
+ MultiWorldRules.set_rule(multiworld.get_location(booksanity.to_location_name(book.name), player), logic.has(book.name))
+
+ for i, book in enumerate(booksanity.get_randomized_lost_books()):
+ if i <= 0:
+ continue
+ MultiWorldRules.set_rule(multiworld.get_location(booksanity.to_location_name(book), player), logic.received(booksanity.progressive_lost_book, i))
+
+
+def set_traveling_merchant_day_rules(logic: StardewLogic, multiworld: MultiWorld, player: int):
for day in Weekday.all_days:
item_for_day = f"Traveling Merchant: {day}"
- for i in range(1, 4):
- location_name = f"Traveling Merchant {day} Item {i}"
- MultiWorldRules.set_rule(multi_world.get_location(location_name, player),
- logic.received(item_for_day))
-
-
-def set_arcade_machine_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options):
- MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.play_junimo_kart, player),
- logic.received(Wallet.skull_key).simplify())
- if world_options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling:
- return
-
- MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.play_junimo_kart, player),
- logic.has("Junimo Kart Small Buff").simplify())
- MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.reach_junimo_kart_2, player),
- logic.has("Junimo Kart Medium Buff").simplify())
- MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.reach_junimo_kart_3, player),
- logic.has("Junimo Kart Big Buff").simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Junimo Kart: Sunset Speedway (Victory)", player),
- logic.has("Junimo Kart Max Buff").simplify())
- MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.play_journey_of_the_prairie_king, player),
- logic.has("JotPK Small Buff").simplify())
- MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.reach_jotpk_world_2, player),
- logic.has("JotPK Medium Buff").simplify())
- MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.reach_jotpk_world_3, player),
- logic.has("JotPK Big Buff").simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Journey of the Prairie King Victory", player),
- logic.has("JotPK Max Buff").simplify())
-
-
-def set_friendsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int):
- friend_prefix = "Friendsanity: "
- friend_suffix = " <3"
- for friend_location in locations.locations_by_tag[LocationTags.FRIENDSANITY]:
- if not friend_location.name in all_location_names:
- continue
- friend_location_without_prefix = friend_location.name[len(friend_prefix):]
- friend_location_trimmed = friend_location_without_prefix[:friend_location_without_prefix.index(friend_suffix)]
- split_index = friend_location_trimmed.rindex(" ")
- friend_name = friend_location_trimmed[:split_index]
- num_hearts = int(friend_location_trimmed[split_index + 1:])
- MultiWorldRules.set_rule(multi_world.get_location(friend_location.name, player),
- logic.can_earn_relationship(friend_name, num_hearts).simplify())
-
-
-def set_deepwoods_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options: StardewOptions):
- if ModNames.deepwoods in world_options[options.Mods]:
- MultiWorldRules.add_rule(multi_world.get_location("Breaking Up Deep Woods Gingerbread House", player),
- logic.has_tool(Tool.axe, "Gold") & deepwoods.can_reach_woods_depth(logic, 50).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Chop Down a Deep Woods Iridium Tree", player),
- logic.has_tool(Tool.axe, "Iridium").simplify())
- MultiWorldRules.set_rule(multi_world.get_entrance(DeepWoodsEntrance.use_woods_obelisk, player),
- logic.received("Woods Obelisk").simplify())
+ entrance_name = f"Buy from Traveling Merchant {day}"
+ set_entrance_rule(multiworld, player, entrance_name, logic.received(item_for_day))
+
+
+def set_arcade_machine_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
+ MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_junimo_kart, player),
+ logic.received(Wallet.skull_key))
+ if world_options.arcade_machine_locations != ArcadeMachineLocations.option_full_shuffling:
+ return
+
+ MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_junimo_kart, player),
+ logic.has("Junimo Kart Small Buff"))
+ MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_2, player),
+ logic.has("Junimo Kart Medium Buff"))
+ MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_3, player),
+ logic.has("Junimo Kart Big Buff"))
+ MultiWorldRules.add_rule(multiworld.get_location("Junimo Kart: Sunset Speedway (Victory)", player),
+ logic.has("Junimo Kart Max Buff"))
+ MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_journey_of_the_prairie_king, player),
+ logic.has("JotPK Small Buff"))
+ MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_jotpk_world_2, player),
+ logic.has("JotPK Medium Buff"))
+ MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_jotpk_world_3, player),
+ logic.has("JotPK Big Buff"))
+ MultiWorldRules.add_rule(multiworld.get_location("Journey of the Prairie King Victory", player),
+ logic.has("JotPK Max Buff"))
+
+
+def set_friendsanity_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, content: StardewContent):
+ if not content.features.friendsanity.is_enabled:
+ return
+ MultiWorldRules.add_rule(multiworld.get_location("Spouse Stardrop", player),
+ logic.relationship.has_hearts_with_any_bachelor(13))
+ MultiWorldRules.add_rule(multiworld.get_location("Have a Baby", player),
+ logic.relationship.can_reproduce(1))
+ MultiWorldRules.add_rule(multiworld.get_location("Have Another Baby", player),
+ logic.relationship.can_reproduce(2))
+
+ for villager in content.villagers.values():
+ for heart in content.features.friendsanity.get_randomized_hearts(villager):
+ rule = logic.relationship.can_earn_relationship(villager.name, heart)
+ location_name = friendsanity.to_location_name(villager.name, heart)
+ MultiWorldRules.set_rule(multiworld.get_location(location_name, player), rule)
+
+ for heart in content.features.friendsanity.get_pet_randomized_hearts():
+ rule = logic.pet.can_befriend_pet(heart)
+ location_name = friendsanity.to_location_name(NPC.pet, heart)
+ MultiWorldRules.set_rule(multiworld.get_location(location_name, player), rule)
+
+
+def set_deepwoods_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
+ if ModNames.deepwoods in world_options.mods:
+ MultiWorldRules.add_rule(multiworld.get_location("Breaking Up Deep Woods Gingerbread House", player),
+ logic.tool.has_tool(Tool.axe, "Gold"))
+ MultiWorldRules.add_rule(multiworld.get_location("Chop Down a Deep Woods Iridium Tree", player),
+ logic.tool.has_tool(Tool.axe, "Iridium"))
+ set_entrance_rule(multiworld, player, DeepWoodsEntrance.use_woods_obelisk, logic.received("Woods Obelisk"))
for depth in range(10, 100 + 10, 10):
- MultiWorldRules.set_rule(multi_world.get_entrance(move_to_woods_depth(depth), player),
- deepwoods.can_chop_to_depth(logic, depth).simplify())
-
-
-def set_magic_spell_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options: StardewOptions):
- if ModNames.magic not in world_options[options.Mods]:
- return
-
- MultiWorldRules.set_rule(multi_world.get_entrance(MagicEntrance.store_to_altar, player),
- (logic.has_relationship(NPC.wizard, 3) &
- logic.can_reach_region(Region.wizard_tower)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Clear Debris", player),
- ((logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic"))
- & magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Till", player),
- (logic.has_tool("Hoe", "Basic") & magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Water", player),
- (logic.has_tool("Watering Can", "Basic") & magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze All Toil School Locations", player),
- (logic.has_tool("Watering Can", "Basic") & logic.has_tool("Hoe", "Basic")
- & (logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic"))
- & magic.can_use_altar(logic)).simplify())
+ set_entrance_rule(multiworld, player, move_to_woods_depth(depth), logic.mod.deepwoods.can_chop_to_depth(depth))
+ MultiWorldRules.add_rule(multiworld.get_location("The Sword in the Stone", player),
+ logic.mod.deepwoods.can_pull_sword() & logic.mod.deepwoods.can_chop_to_depth(100))
+
+
+def set_magic_spell_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
+ if ModNames.magic not in world_options.mods:
+ return
+
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze: Clear Debris", player),
+ (logic.tool.has_tool("Axe", "Basic") | logic.tool.has_tool("Pickaxe", "Basic")))
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze: Till", player),
+ logic.tool.has_tool("Hoe", "Basic"))
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze: Water", player),
+ logic.tool.has_tool("Watering Can", "Basic"))
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze All Toil School Locations", player),
+ (logic.tool.has_tool("Watering Can", "Basic") & logic.tool.has_tool("Hoe", "Basic")
+ & (logic.tool.has_tool("Axe", "Basic") | logic.tool.has_tool("Pickaxe", "Basic"))))
# Do I *want* to add boots into logic when you get them even in vanilla without effort? idk
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Evac", player),
- (logic.can_mine_perfectly() & magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Haste", player),
- (logic.has("Coffee") & magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Heal", player),
- (logic.has("Life Elixir") & magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze All Life School Locations", player),
- (logic.has("Coffee") & logic.has("Life Elixir")
- & logic.can_mine_perfectly() & magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Descend", player),
- (logic.can_reach_region(Region.mines) & magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Fireball", player),
- (logic.has("Fire Quartz") & magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Frostbite", player),
- (logic.can_mine_to_floor(70) & logic.can_fish(85) & magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze All Elemental School Locations", player),
- (logic.can_reach_region(Region.mines) & logic.has("Fire Quartz")
- & logic.can_reach_region(Region.mines_floor_70) & logic.can_fish(85) &
- magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Lantern", player),
- magic.can_use_altar(logic).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Tendrils", player),
- (logic.can_reach_region(Region.farm) & magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Shockwave", player),
- (logic.has("Earth Crystal") & magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze All Nature School Locations", player),
- (logic.has("Earth Crystal") & logic.can_reach_region("Farm") &
- magic.can_use_altar(logic)).simplify()),
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Meteor", player),
- (logic.can_reach_region(Region.farm) & logic.has_lived_months(12)
- & magic.can_use_altar(logic)).simplify()),
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Lucksteal", player),
- (logic.can_reach_region(Region.witch_hut) & magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze: Bloodmana", player),
- (logic.can_reach_region(Region.mines_floor_100) & magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze All Eldritch School Locations", player),
- (logic.can_reach_region(Region.witch_hut) &
- logic.can_reach_region(Region.mines_floor_100) &
- logic.can_reach_region(Region.farm) & logic.has_lived_months(12) &
- magic.can_use_altar(logic)).simplify())
- MultiWorldRules.add_rule(multi_world.get_location("Analyze Every Magic School Location", player),
- (logic.has_tool("Watering Can", "Basic") & logic.has_tool("Hoe", "Basic")
- & (logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic")) &
- logic.has("Coffee") & logic.has("Life Elixir")
- & logic.can_mine_perfectly() & logic.has("Earth Crystal") &
- logic.can_reach_region(Region.mines) &
- logic.has("Fire Quartz") & logic.can_fish(85) &
- logic.can_reach_region(Region.witch_hut) &
- logic.can_reach_region(Region.mines_floor_100) &
- logic.can_reach_region(Region.farm) & logic.has_lived_months(12) &
- magic.can_use_altar(logic)).simplify())
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze: Evac", player),
+ logic.ability.can_mine_perfectly())
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze: Haste", player),
+ logic.has("Coffee"))
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze: Heal", player),
+ logic.has("Life Elixir"))
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze All Life School Locations", player),
+ (logic.has("Coffee") & logic.has("Life Elixir")
+ & logic.ability.can_mine_perfectly()))
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze: Descend", player),
+ logic.region.can_reach(Region.mines))
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze: Fireball", player),
+ logic.has("Fire Quartz"))
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze: Frostbolt", player),
+ logic.region.can_reach(Region.mines_floor_60) & logic.skill.can_fish(difficulty=85))
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze All Elemental School Locations", player),
+ logic.has("Fire Quartz") & logic.region.can_reach(Region.mines_floor_60) & logic.skill.can_fish(difficulty=85))
+ # MultiWorldRules.add_rule(multiworld.get_location("Analyze: Lantern", player),)
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze: Tendrils", player),
+ logic.region.can_reach(Region.farm))
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze: Shockwave", player),
+ logic.has("Earth Crystal"))
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze All Nature School Locations", player),
+ (logic.has("Earth Crystal") & logic.region.can_reach("Farm"))),
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze: Meteor", player),
+ (logic.region.can_reach(Region.farm) & logic.time.has_lived_months(12))),
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze: Lucksteal", player),
+ logic.region.can_reach(Region.witch_hut))
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze: Bloodmana", player),
+ logic.region.can_reach(Region.mines_floor_100))
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze All Eldritch School Locations", player),
+ (logic.region.can_reach(Region.witch_hut) &
+ logic.region.can_reach(Region.mines_floor_100) &
+ logic.region.can_reach(Region.farm) & logic.time.has_lived_months(12)))
+ MultiWorldRules.add_rule(multiworld.get_location("Analyze Every Magic School Location", player),
+ (logic.tool.has_tool("Watering Can", "Basic") & logic.tool.has_tool("Hoe", "Basic")
+ & (logic.tool.has_tool("Axe", "Basic") | logic.tool.has_tool("Pickaxe", "Basic")) &
+ logic.has("Coffee") & logic.has("Life Elixir")
+ & logic.ability.can_mine_perfectly() & logic.has("Earth Crystal") &
+ logic.has("Fire Quartz") & logic.skill.can_fish(difficulty=85) &
+ logic.region.can_reach(Region.witch_hut) &
+ logic.region.can_reach(Region.mines_floor_100) &
+ logic.region.can_reach(Region.farm) & logic.time.has_lived_months(12)))
+
+
+def set_sve_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
+ if ModNames.sve not in world_options.mods:
+ return
+ set_entrance_rule(multiworld, player, SVEEntrance.forest_to_lost_woods, logic.bundle.can_complete_community_center)
+ set_entrance_rule(multiworld, player, SVEEntrance.enter_summit, logic.mod.sve.has_iridium_bomb())
+ set_entrance_rule(multiworld, player, SVEEntrance.backwoods_to_grove, logic.mod.sve.has_any_rune())
+ set_entrance_rule(multiworld, player, SVEEntrance.badlands_to_cave, logic.has("Aegis Elixir") | logic.combat.can_fight_at_level(Performance.maximum))
+ set_entrance_rule(multiworld, player, SVEEntrance.forest_west_to_spring, logic.quest.can_complete_quest(Quest.magic_ink))
+ set_entrance_rule(multiworld, player, SVEEntrance.railroad_to_grampleton_station, logic.received(SVEQuestItem.scarlett_job_offer))
+ set_entrance_rule(multiworld, player, SVEEntrance.secret_woods_to_west, logic.tool.has_tool(Tool.axe, ToolMaterial.iron))
+ set_entrance_rule(multiworld, player, SVEEntrance.grandpa_shed_to_interior, logic.tool.has_tool(Tool.axe, ToolMaterial.iron))
+ set_entrance_rule(multiworld, player, SVEEntrance.aurora_warp_to_aurora, logic.received(SVERunes.nexus_aurora))
+ set_entrance_rule(multiworld, player, SVEEntrance.farm_warp_to_farm, logic.received(SVERunes.nexus_farm))
+ set_entrance_rule(multiworld, player, SVEEntrance.guild_warp_to_guild, logic.received(SVERunes.nexus_guild))
+ set_entrance_rule(multiworld, player, SVEEntrance.junimo_warp_to_junimo, logic.received(SVERunes.nexus_junimo))
+ set_entrance_rule(multiworld, player, SVEEntrance.spring_warp_to_spring, logic.received(SVERunes.nexus_spring))
+ set_entrance_rule(multiworld, player, SVEEntrance.outpost_warp_to_outpost, logic.received(SVERunes.nexus_outpost))
+ set_entrance_rule(multiworld, player, SVEEntrance.wizard_warp_to_wizard, logic.received(SVERunes.nexus_wizard))
+ set_entrance_rule(multiworld, player, SVEEntrance.use_purple_junimo, logic.relationship.has_hearts(ModNPC.apples, 10))
+ set_entrance_rule(multiworld, player, SVEEntrance.grandpa_interior_to_upstairs, logic.mod.sve.has_grandpa_shed_repaired())
+ set_entrance_rule(multiworld, player, SVEEntrance.use_bear_shop, (logic.mod.sve.can_buy_bear_recipe()))
+ set_entrance_rule(multiworld, player, SVEEntrance.railroad_to_grampleton_station, logic.received(SVEQuestItem.scarlett_job_offer))
+ set_entrance_rule(multiworld, player, SVEEntrance.museum_to_gunther_bedroom, logic.relationship.has_hearts(ModNPC.gunther, 2))
+ logic.mod.sve.initialize_rules()
+ for location in logic.registry.sve_location_rules:
+ MultiWorldRules.set_rule(multiworld.get_location(location, player),
+ logic.registry.sve_location_rules[location])
+ set_sve_ginger_island_rules(logic, multiworld, player, world_options)
+ set_boarding_house_rules(logic, multiworld, player, world_options)
+
+
+def set_sve_ginger_island_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
+ if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true:
+ return
+ set_entrance_rule(multiworld, player, SVEEntrance.summit_to_highlands, logic.mod.sve.has_marlon_boat())
+ set_entrance_rule(multiworld, player, SVEEntrance.wizard_to_fable_reef, logic.received(SVEQuestItem.fable_reef_portal))
+ set_entrance_rule(multiworld, player, SVEEntrance.highlands_to_cave,
+ logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron) & logic.tool.has_tool(Tool.axe, ToolMaterial.iron))
+ set_entrance_rule(multiworld, player, SVEEntrance.highlands_to_pond, logic.tool.has_tool(Tool.axe, ToolMaterial.iron))
+
+
+def set_boarding_house_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
+ if ModNames.boarding_house not in world_options.mods:
+ return
+ set_entrance_rule(multiworld, player, BoardingHouseEntrance.the_lost_valley_to_lost_valley_ruins, logic.tool.has_tool(Tool.axe, ToolMaterial.iron))
+
+
+def set_entrance_rule(multiworld, player, entrance: str, rule: StardewRule):
+ try:
+ potentially_required_regions = look_for_indirect_connection(rule)
+ if potentially_required_regions:
+ for region in potentially_required_regions:
+ multiworld.register_indirect_condition(multiworld.get_region(region, player), multiworld.get_entrance(entrance, player))
+
+ MultiWorldRules.set_rule(multiworld.get_entrance(entrance, player), rule)
+ except KeyError as ex:
+ logger.error(f"""Failed to evaluate indirect connection in: {explain(rule, CollectionState(multiworld))}""")
+ raise ex
+
+
+def set_island_entrance_rule(multiworld, player, entrance: str, rule: StardewRule, world_options: StardewValleyOptions):
+ if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true:
+ return
+ set_entrance_rule(multiworld, player, entrance, rule)
+
+
+def set_many_island_entrances_rules(multiworld, player, entrance_rules: Dict[str, StardewRule], world_options: StardewValleyOptions):
+ if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true:
+ return
+ for entrance, rule in entrance_rules.items():
+ set_entrance_rule(multiworld, player, entrance, rule)
diff --git a/worlds/stardew_valley/scripts/export_locations.py b/worlds/stardew_valley/scripts/export_locations.py
index 1dc60f79b14b..c181faec7b94 100644
--- a/worlds/stardew_valley/scripts/export_locations.py
+++ b/worlds/stardew_valley/scripts/export_locations.py
@@ -16,11 +16,17 @@
if __name__ == "__main__":
with open("output/stardew_valley_location_table.json", "w+") as f:
locations = {
+ "Cheat Console":
+ {"code": -1, "region": "Archipelago"},
+ "Server":
+ {"code": -2, "region": "Archipelago"}
+ }
+ locations.update({
location.name: {
"code": location.code,
"region": location.region,
}
for location in location_table.values()
if location.code is not None
- }
+ })
json.dump({"locations": locations}, f)
diff --git a/worlds/stardew_valley/scripts/update_data.py b/worlds/stardew_valley/scripts/update_data.py
index 7b31a3705c5c..ae8f7f8d5503 100644
--- a/worlds/stardew_valley/scripts/update_data.py
+++ b/worlds/stardew_valley/scripts/update_data.py
@@ -12,7 +12,7 @@
from worlds.stardew_valley import LocationData
from worlds.stardew_valley.items import load_item_csv, Group, ItemData
-from worlds.stardew_valley.locations import load_location_csv
+from worlds.stardew_valley.locations import load_location_csv, LocationTags
RESOURCE_PACK_CODE_OFFSET = 5000
script_folder = Path(__file__)
@@ -34,14 +34,15 @@ def write_item_csv(items: List[ItemData]):
def write_location_csv(locations: List[LocationData]):
with open((script_folder.parent.parent / "data/locations.csv").resolve(), "w", newline="") as file:
- write = csv.DictWriter(file, ["id", "region", "name", "tags"])
+ write = csv.DictWriter(file, ["id", "region", "name", "tags", "mod_name"])
write.writeheader()
for location in locations:
location_dict = {
"id": location.code_without_offset,
"name": location.name,
"region": location.region,
- "tags": ",".join(sorted(group.name for group in location.tags))
+ "tags": ",".join(sorted(group.name for group in location.tags)),
+ "mod_name": location.mod_name
}
write.writerow(location_dict)
@@ -76,12 +77,11 @@ def write_location_csv(locations: List[LocationData]):
location_counter = itertools.count(max(location.code_without_offset
for location in loaded_locations
if location.code_without_offset is not None) + 1)
-
locations_to_write = []
for location in loaded_locations:
if location.code_without_offset is None:
locations_to_write.append(
- LocationData(next(location_counter), location.region, location.name, location.tags))
+ LocationData(next(location_counter), location.region, location.name, location.mod_name, location.tags))
continue
locations_to_write.append(location)
diff --git a/worlds/stardew_valley/stardew_rule.py b/worlds/stardew_valley/stardew_rule.py
deleted file mode 100644
index d0fa9858cc0b..000000000000
--- a/worlds/stardew_valley/stardew_rule.py
+++ /dev/null
@@ -1,361 +0,0 @@
-from __future__ import annotations
-
-from dataclasses import dataclass
-from typing import Iterable, Dict, List, Union, FrozenSet
-
-from BaseClasses import CollectionState, ItemClassification
-from .items import item_table
-
-MISSING_ITEM = "THIS ITEM IS MISSING"
-
-
-class StardewRule:
- def __call__(self, state: CollectionState) -> bool:
- raise NotImplementedError
-
- def __or__(self, other) -> StardewRule:
- if isinstance(other, Or):
- return Or(self, *other.rules)
-
- return Or(self, other)
-
- def __and__(self, other) -> StardewRule:
- if isinstance(other, And):
- return And(other.rules.union({self}))
-
- return And(self, other)
-
- def get_difficulty(self):
- raise NotImplementedError
-
- def simplify(self) -> StardewRule:
- return self
-
-
-class True_(StardewRule): # noqa
-
- def __new__(cls, _cache=[]): # noqa
- # Only one single instance will be ever created.
- if not _cache:
- _cache.append(super(True_, cls).__new__(cls))
- return _cache[0]
-
- def __call__(self, state: CollectionState) -> bool:
- return True
-
- def __or__(self, other) -> StardewRule:
- return self
-
- def __and__(self, other) -> StardewRule:
- return other
-
- def __repr__(self):
- return "True"
-
- def get_difficulty(self):
- return 0
-
-
-class False_(StardewRule): # noqa
-
- def __new__(cls, _cache=[]): # noqa
- # Only one single instance will be ever created.
- if not _cache:
- _cache.append(super(False_, cls).__new__(cls))
- return _cache[0]
-
- def __call__(self, state: CollectionState) -> bool:
- return False
-
- def __or__(self, other) -> StardewRule:
- return other
-
- def __and__(self, other) -> StardewRule:
- return self
-
- def __repr__(self):
- return "False"
-
- def get_difficulty(self):
- return 999999999
-
-
-class Or(StardewRule):
- rules: FrozenSet[StardewRule]
-
- def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule):
- rules_list = set()
- if isinstance(rule, Iterable):
- rules_list.update(rule)
- else:
- rules_list.add(rule)
-
- if rules is not None:
- rules_list.update(rules)
-
- assert rules_list, "Can't create a Or conditions without rules"
-
- new_rules = set()
- for rule in rules_list:
- if isinstance(rule, Or):
- new_rules.update(rule.rules)
- else:
- new_rules.add(rule)
- rules_list = new_rules
-
- self.rules = frozenset(rules_list)
-
- def __call__(self, state: CollectionState) -> bool:
- return any(rule(state) for rule in self.rules)
-
- def __repr__(self):
- return f"({' | '.join(repr(rule) for rule in self.rules)})"
-
- def __or__(self, other):
- if isinstance(other, True_):
- return other
- if isinstance(other, False_):
- return self
- if isinstance(other, Or):
- return Or(self.rules.union(other.rules))
-
- return Or(self.rules.union({other}))
-
- def __eq__(self, other):
- return isinstance(other, self.__class__) and other.rules == self.rules
-
- def __hash__(self):
- return hash(self.rules)
-
- def get_difficulty(self):
- return min(rule.get_difficulty() for rule in self.rules)
-
- def simplify(self) -> StardewRule:
- if any(isinstance(rule, True_) for rule in self.rules):
- return True_()
-
- simplified_rules = {rule.simplify() for rule in self.rules}
- simplified_rules = {rule for rule in simplified_rules if rule is not False_()}
-
- if not simplified_rules:
- return False_()
-
- if len(simplified_rules) == 1:
- return next(iter(simplified_rules))
-
- return Or(simplified_rules)
-
-
-class And(StardewRule):
- rules: FrozenSet[StardewRule]
-
- def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule):
- rules_list = set()
- if isinstance(rule, Iterable):
- rules_list.update(rule)
- else:
- rules_list.add(rule)
-
- if rules is not None:
- rules_list.update(rules)
-
- if len(rules_list) < 1:
- rules_list.add(True_())
-
- new_rules = set()
- for rule in rules_list:
- if isinstance(rule, And):
- new_rules.update(rule.rules)
- else:
- new_rules.add(rule)
- rules_list = new_rules
-
- self.rules = frozenset(rules_list)
-
- def __call__(self, state: CollectionState) -> bool:
- return all(rule(state) for rule in self.rules)
-
- def __repr__(self):
- return f"({' & '.join(repr(rule) for rule in self.rules)})"
-
- def __and__(self, other):
- if isinstance(other, True_):
- return self
- if isinstance(other, False_):
- return other
- if isinstance(other, And):
- return And(self.rules.union(other.rules))
-
- return And(self.rules.union({other}))
-
- def __eq__(self, other):
- return isinstance(other, self.__class__) and other.rules == self.rules
-
- def __hash__(self):
- return hash(self.rules)
-
- def get_difficulty(self):
- return max(rule.get_difficulty() for rule in self.rules)
-
- def simplify(self) -> StardewRule:
- if any(isinstance(rule, False_) for rule in self.rules):
- return False_()
-
- simplified_rules = {rule.simplify() for rule in self.rules}
- simplified_rules = {rule for rule in simplified_rules if rule is not True_()}
-
- if not simplified_rules:
- return True_()
-
- if len(simplified_rules) == 1:
- return next(iter(simplified_rules))
-
- return And(simplified_rules)
-
-
-class Count(StardewRule):
- count: int
- rules: List[StardewRule]
-
- def __init__(self, count: int, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule):
- rules_list = []
- if isinstance(rule, Iterable):
- rules_list.extend(rule)
- else:
- rules_list.append(rule)
-
- if rules is not None:
- rules_list.extend(rules)
-
- assert rules_list, "Can't create a Count conditions without rules"
- assert len(rules_list) >= count, "Count need at least as many rules at the count"
-
- self.rules = rules_list
- self.count = count
-
- def __call__(self, state: CollectionState) -> bool:
- c = 0
- for r in self.rules:
- if r(state):
- c += 1
- if c >= self.count:
- return True
- return False
-
- def __repr__(self):
- return f"Received {self.count} {repr(self.rules)}"
-
- def get_difficulty(self):
- rules_sorted_by_difficulty = sorted(self.rules, key=lambda x: x.get_difficulty())
- easiest_n_rules = rules_sorted_by_difficulty[0:self.count]
- return max(rule.get_difficulty() for rule in easiest_n_rules)
-
- def simplify(self):
- return Count(self.count, [rule.simplify() for rule in self.rules])
-
-
-class TotalReceived(StardewRule):
- count: int
- items: Iterable[str]
- player: int
-
- def __init__(self, count: int, items: Union[str, Iterable[str]], player: int):
- items_list = []
- if isinstance(items, Iterable):
- items_list.extend(items)
- else:
- items_list.append(items)
-
- assert items_list, "Can't create a Total Received conditions without items"
- for item in items_list:
- assert item_table[item].classification & ItemClassification.progression, \
- "Item has to be progression to be used in logic"
-
- self.player = player
- self.items = items_list
- self.count = count
-
- def __call__(self, state: CollectionState) -> bool:
- c = 0
- for item in self.items:
- c += state.count(item, self.player)
- if c >= self.count:
- return True
- return False
-
- def __repr__(self):
- return f"Received {self.count} {self.items}"
-
- def get_difficulty(self):
- return self.count
-
-
-@dataclass(frozen=True)
-class Received(StardewRule):
- item: str
- player: int
- count: int
-
- def __post_init__(self):
- assert item_table[self.item].classification & ItemClassification.progression, \
- f"Item [{item_table[self.item].name}] has to be progression to be used in logic"
-
- def __call__(self, state: CollectionState) -> bool:
- return state.has(self.item, self.player, self.count)
-
- def __repr__(self):
- if self.count == 1:
- return f"Received {self.item}"
- return f"Received {self.count} {self.item}"
-
- def get_difficulty(self):
- if self.item == "Spring":
- return 0
- if self.item == "Summer":
- return 1
- if self.item == "Fall":
- return 2
- if self.item == "Winter":
- return 3
- return self.count
-
-
-@dataclass(frozen=True)
-class Reach(StardewRule):
- spot: str
- resolution_hint: str
- player: int
-
- def __call__(self, state: CollectionState) -> bool:
- return state.can_reach(self.spot, self.resolution_hint, self.player)
-
- def __repr__(self):
- return f"Reach {self.resolution_hint} {self.spot}"
-
- def get_difficulty(self):
- return 1
-
-
-@dataclass(frozen=True)
-class Has(StardewRule):
- item: str
- # For sure there is a better way than just passing all the rules everytime
- other_rules: Dict[str, StardewRule]
-
- def __call__(self, state: CollectionState) -> bool:
- if isinstance(self.item, str):
- return self.other_rules[self.item](state)
-
- def __repr__(self):
- if not self.item in self.other_rules:
- return f"Has {self.item} -> {MISSING_ITEM}"
- return f"Has {self.item} -> {repr(self.other_rules[self.item])}"
-
- def get_difficulty(self):
- return self.other_rules[self.item].get_difficulty() + 1
-
- def __hash__(self):
- return hash(self.item)
-
- def simplify(self) -> StardewRule:
- return self.other_rules[self.item].simplify()
diff --git a/worlds/stardew_valley/stardew_rule/__init__.py b/worlds/stardew_valley/stardew_rule/__init__.py
new file mode 100644
index 000000000000..73b2d1b66747
--- /dev/null
+++ b/worlds/stardew_valley/stardew_rule/__init__.py
@@ -0,0 +1,4 @@
+from .base import *
+from .literal import *
+from .protocol import *
+from .state import *
diff --git a/worlds/stardew_valley/stardew_rule/base.py b/worlds/stardew_valley/stardew_rule/base.py
new file mode 100644
index 000000000000..3e6eb327ea99
--- /dev/null
+++ b/worlds/stardew_valley/stardew_rule/base.py
@@ -0,0 +1,479 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from collections import deque, Counter
+from dataclasses import dataclass, field
+from functools import cached_property
+from itertools import chain
+from threading import Lock
+from typing import Iterable, Dict, List, Union, Sized, Hashable, Callable, Tuple, Set, Optional
+
+from BaseClasses import CollectionState
+from .literal import true_, false_, LiteralStardewRule
+from .protocol import StardewRule
+
+MISSING_ITEM = "THIS ITEM IS MISSING"
+
+
+class BaseStardewRule(StardewRule, ABC):
+
+ def __or__(self, other) -> StardewRule:
+ if other is true_ or other is false_ or type(other) is Or:
+ return other | self
+
+ return Or(self, other)
+
+ def __and__(self, other) -> StardewRule:
+ if other is true_ or other is false_ or type(other) is And:
+ return other & self
+
+ return And(self, other)
+
+
+class CombinableStardewRule(BaseStardewRule, ABC):
+
+ @property
+ @abstractmethod
+ def combination_key(self) -> Hashable:
+ raise NotImplementedError
+
+ @property
+ @abstractmethod
+ def value(self):
+ raise NotImplementedError
+
+ def is_same_rule(self, other: CombinableStardewRule):
+ return self.combination_key == other.combination_key
+
+ def add_into(self, rules: Dict[Hashable, CombinableStardewRule], reducer: Callable[[CombinableStardewRule, CombinableStardewRule], CombinableStardewRule]) \
+ -> Dict[Hashable, CombinableStardewRule]:
+ rules = dict(rules)
+
+ if self.combination_key in rules:
+ rules[self.combination_key] = reducer(self, rules[self.combination_key])
+ else:
+ rules[self.combination_key] = self
+
+ return rules
+
+ def __and__(self, other):
+ if isinstance(other, CombinableStardewRule) and self.is_same_rule(other):
+ return And.combine(self, other)
+ return super().__and__(other)
+
+ def __or__(self, other):
+ if isinstance(other, CombinableStardewRule) and self.is_same_rule(other):
+ return Or.combine(self, other)
+ return super().__or__(other)
+
+
+class _SimplificationState:
+ original_simplifiable_rules: Tuple[StardewRule, ...]
+
+ rules_to_simplify: deque[StardewRule]
+ simplified_rules: Set[StardewRule]
+ lock: Lock
+
+ def __init__(self, simplifiable_rules: Tuple[StardewRule, ...], rules_to_simplify: Optional[deque[StardewRule]] = None,
+ simplified_rules: Optional[Set[StardewRule]] = None):
+ if simplified_rules is None:
+ simplified_rules = set()
+
+ self.original_simplifiable_rules = simplifiable_rules
+ self.rules_to_simplify = rules_to_simplify
+ self.simplified_rules = simplified_rules
+ self.locked = False
+
+ @property
+ def is_simplified(self):
+ return self.rules_to_simplify is not None and not self.rules_to_simplify
+
+ def short_circuit(self, complement: LiteralStardewRule):
+ self.rules_to_simplify = deque()
+ self.simplified_rules = {complement}
+
+ def try_popleft(self):
+ try:
+ self.rules_to_simplify.popleft()
+ except IndexError:
+ pass
+
+ def acquire_copy(self):
+ state = _SimplificationState(self.original_simplifiable_rules, self.rules_to_simplify.copy(), self.simplified_rules.copy())
+ state.acquire()
+ return state
+
+ def merge(self, other: _SimplificationState):
+ return _SimplificationState(self.original_simplifiable_rules + other.original_simplifiable_rules)
+
+ def add(self, rule: StardewRule):
+ return _SimplificationState(self.original_simplifiable_rules + (rule,))
+
+ def acquire(self):
+ """
+ This just set a boolean to True and is absolutely not thread safe. It just works because AP is single-threaded.
+ """
+ if self.locked is True:
+ return False
+
+ self.locked = True
+ return True
+
+ def release(self):
+ assert self.locked
+ self.locked = False
+
+
+class AggregatingStardewRule(BaseStardewRule, ABC):
+ """
+ Logic for both "And" and "Or" rules.
+ """
+ identity: LiteralStardewRule
+ complement: LiteralStardewRule
+ symbol: str
+
+ combinable_rules: Dict[Hashable, CombinableStardewRule]
+ simplification_state: _SimplificationState
+ _last_short_circuiting_rule: Optional[StardewRule] = None
+
+ def __init__(self, *rules: StardewRule, _combinable_rules=None, _simplification_state=None):
+ if _combinable_rules is None:
+ assert rules, f"Can't create an aggregating condition without rules"
+ rules, _combinable_rules = self.split_rules(rules)
+ _simplification_state = _SimplificationState(rules)
+
+ self.combinable_rules = _combinable_rules
+ self.simplification_state = _simplification_state
+
+ @property
+ def original_rules(self):
+ return RepeatableChain(self.combinable_rules.values(), self.simplification_state.original_simplifiable_rules)
+
+ @property
+ def current_rules(self):
+ if self.simplification_state.rules_to_simplify is None:
+ return self.original_rules
+
+ return RepeatableChain(self.combinable_rules.values(), self.simplification_state.simplified_rules, self.simplification_state.rules_to_simplify)
+
+ @classmethod
+ def split_rules(cls, rules: Union[Iterable[StardewRule]]) -> Tuple[Tuple[StardewRule, ...], Dict[Hashable, CombinableStardewRule]]:
+ other_rules = []
+ reduced_rules = {}
+ for rule in rules:
+ if isinstance(rule, CombinableStardewRule):
+ key = rule.combination_key
+ if key not in reduced_rules:
+ reduced_rules[key] = rule
+ continue
+
+ reduced_rules[key] = cls.combine(reduced_rules[key], rule)
+ continue
+
+ if type(rule) is cls:
+ other_rules.extend(rule.simplification_state.original_simplifiable_rules) # noqa
+ reduced_rules = cls.merge(reduced_rules, rule.combinable_rules) # noqa
+ continue
+
+ other_rules.append(rule)
+
+ return tuple(other_rules), reduced_rules
+
+ @classmethod
+ def merge(cls, left: Dict[Hashable, CombinableStardewRule], right: Dict[Hashable, CombinableStardewRule]) -> Dict[Hashable, CombinableStardewRule]:
+ reduced_rules = dict(left)
+ for key, rule in right.items():
+ if key not in reduced_rules:
+ reduced_rules[key] = rule
+ continue
+
+ reduced_rules[key] = cls.combine(reduced_rules[key], rule)
+
+ return reduced_rules
+
+ @staticmethod
+ @abstractmethod
+ def combine(left: CombinableStardewRule, right: CombinableStardewRule) -> CombinableStardewRule:
+ raise NotImplementedError
+
+ def short_circuit_simplification(self):
+ self.simplification_state.short_circuit(self.complement)
+ self.combinable_rules = {}
+ return self.complement, self.complement.value
+
+ def short_circuit_evaluation(self, rule):
+ self._last_short_circuiting_rule = rule
+ return self, self.complement.value
+
+ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
+ """
+ The global idea here is the same as short-circuiting operators, applied to evaluation and rule simplification.
+ """
+
+ # Directly checking last rule that short-circuited, in case state has not changed.
+ if self._last_short_circuiting_rule:
+ if self._last_short_circuiting_rule(state) is self.complement.value:
+ return self.short_circuit_evaluation(self._last_short_circuiting_rule)
+ self._last_short_circuiting_rule = None
+
+ # Combinable rules are considered already simplified, so we evaluate them right away to go faster.
+ for rule in self.combinable_rules.values():
+ if rule(state) is self.complement.value:
+ return self.short_circuit_evaluation(rule)
+
+ if self.simplification_state.is_simplified:
+ # The rule is fully simplified, so now we can only evaluate.
+ for rule in self.simplification_state.simplified_rules:
+ if rule(state) is self.complement.value:
+ return self.short_circuit_evaluation(rule)
+ return self, self.identity.value
+
+ return self.evaluate_while_simplifying_stateful(state)
+
+ def evaluate_while_simplifying_stateful(self, state):
+ local_state = self.simplification_state
+ try:
+ # Creating a new copy, so we don't modify the rules while we're already evaluating it. This can happen if a rule is used for an entrance and a
+ # location. When evaluating a given rule what requires access to a region, the region cache can get an update. If it does, we could enter this rule
+ # again. Since the simplification is stateful, the set of simplified rules can be modified while it's being iterated on, and cause a crash.
+ #
+ # After investigation, for millions of call to this method, copy were acquired 425 times.
+ # Merging simplification state in parent call was deemed useless.
+ if not local_state.acquire():
+ local_state = local_state.acquire_copy()
+ self.simplification_state = local_state
+
+ # Evaluating what has already been simplified. First it will be faster than simplifying "new" rules, but we also assume that if we reach this point
+ # and there are already are simplified rule, one of these rules has short-circuited, and might again, so we can leave early.
+ for rule in local_state.simplified_rules:
+ if rule(state) is self.complement.value:
+ return self.short_circuit_evaluation(rule)
+
+ # If the queue is None, it means we have not start simplifying. Otherwise, we will continue simplification where we left.
+ if local_state.rules_to_simplify is None:
+ rules_to_simplify = frozenset(local_state.original_simplifiable_rules)
+ if self.complement in rules_to_simplify:
+ return self.short_circuit_simplification()
+ local_state.rules_to_simplify = deque(rules_to_simplify)
+
+ # Start simplification where we left.
+ while local_state.rules_to_simplify:
+ result = self.evaluate_rule_while_simplifying_stateful(local_state, state)
+ local_state.try_popleft()
+ if result is not None:
+ return result
+
+ # The whole rule has been simplified and evaluated without short-circuit.
+ return self, self.identity.value
+ finally:
+ local_state.release()
+
+ def evaluate_rule_while_simplifying_stateful(self, local_state, state):
+ simplified, value = local_state.rules_to_simplify[0].evaluate_while_simplifying(state)
+
+ # Identity is removed from the resulting simplification since it does not affect the result.
+ if simplified is self.identity:
+ return
+
+ # If we find a complement here, we know the rule will always short-circuit, what ever the state.
+ if simplified is self.complement:
+ return self.short_circuit_simplification()
+ # Keep the simplified rule to be reevaluated later.
+ local_state.simplified_rules.add(simplified)
+
+ # Now we use the value to short-circuit if it is the complement.
+ if value is self.complement.value:
+ return self.short_circuit_evaluation(simplified)
+
+ def __str__(self):
+ return f"({self.symbol.join(str(rule) for rule in self.original_rules)})"
+
+ def __repr__(self):
+ return f"({self.symbol.join(repr(rule) for rule in self.original_rules)})"
+
+ def __eq__(self, other):
+ return (isinstance(other, type(self)) and self.combinable_rules == other.combinable_rules and
+ self.simplification_state.original_simplifiable_rules == self.simplification_state.original_simplifiable_rules)
+
+ def __hash__(self):
+ if len(self.combinable_rules) + len(self.simplification_state.original_simplifiable_rules) > 5:
+ return id(self)
+
+ return hash((*self.combinable_rules.values(), self.simplification_state.original_simplifiable_rules))
+
+
+class Or(AggregatingStardewRule):
+ identity = false_
+ complement = true_
+ symbol = " | "
+
+ def __call__(self, state: CollectionState) -> bool:
+ return self.evaluate_while_simplifying(state)[1]
+
+ def __or__(self, other):
+ if other is true_ or other is false_:
+ return other | self
+
+ if isinstance(other, CombinableStardewRule):
+ return Or(_combinable_rules=other.add_into(self.combinable_rules, self.combine), _simplification_state=self.simplification_state)
+
+ if type(other) is Or:
+ return Or(_combinable_rules=self.merge(self.combinable_rules, other.combinable_rules),
+ _simplification_state=self.simplification_state.merge(other.simplification_state))
+
+ return Or(_combinable_rules=self.combinable_rules, _simplification_state=self.simplification_state.add(other))
+
+ @staticmethod
+ def combine(left: CombinableStardewRule, right: CombinableStardewRule) -> CombinableStardewRule:
+ return min(left, right, key=lambda x: x.value)
+
+
+class And(AggregatingStardewRule):
+ identity = true_
+ complement = false_
+ symbol = " & "
+
+ def __call__(self, state: CollectionState) -> bool:
+ return self.evaluate_while_simplifying(state)[1]
+
+ def __and__(self, other):
+ if other is true_ or other is false_:
+ return other & self
+
+ if isinstance(other, CombinableStardewRule):
+ return And(_combinable_rules=other.add_into(self.combinable_rules, self.combine), _simplification_state=self.simplification_state)
+
+ if type(other) is And:
+ return And(_combinable_rules=self.merge(self.combinable_rules, other.combinable_rules),
+ _simplification_state=self.simplification_state.merge(other.simplification_state))
+
+ return And(_combinable_rules=self.combinable_rules, _simplification_state=self.simplification_state.add(other))
+
+ @staticmethod
+ def combine(left: CombinableStardewRule, right: CombinableStardewRule) -> CombinableStardewRule:
+ return max(left, right, key=lambda x: x.value)
+
+
+class Count(BaseStardewRule):
+ count: int
+ rules: List[StardewRule]
+ counter: Counter[StardewRule]
+ evaluate: Callable[[CollectionState], bool]
+
+ total: Optional[int]
+ rule_mapping: Optional[Dict[StardewRule, StardewRule]]
+
+ def __init__(self, rules: List[StardewRule], count: int):
+ self.count = count
+ self.counter = Counter(rules)
+
+ if len(self.counter) / len(rules) < .66:
+ # Checking if it's worth using the count operation with shortcircuit or not. Value should be fine-tuned when Count has more usage.
+ self.total = sum(self.counter.values())
+ self.rules = sorted(self.counter.keys(), key=lambda x: self.counter[x], reverse=True)
+ self.rule_mapping = {}
+ self.evaluate = self.evaluate_with_shortcircuit
+ else:
+ self.rules = rules
+ self.evaluate = self.evaluate_without_shortcircuit
+
+ def __call__(self, state: CollectionState) -> bool:
+ return self.evaluate(state)
+
+ def evaluate_without_shortcircuit(self, state: CollectionState) -> bool:
+ c = 0
+ for i in range(self.rules_count):
+ self.rules[i], value = self.rules[i].evaluate_while_simplifying(state)
+ if value:
+ c += 1
+
+ if c >= self.count:
+ return True
+ if c + self.rules_count - i < self.count:
+ break
+
+ return False
+
+ def evaluate_with_shortcircuit(self, state: CollectionState) -> bool:
+ c = 0
+ t = self.total
+
+ for rule in self.rules:
+ evaluation_value = self.call_evaluate_while_simplifying_cached(rule, state)
+ rule_value = self.counter[rule]
+
+ if evaluation_value:
+ c += rule_value
+ else:
+ t -= rule_value
+
+ if c >= self.count:
+ return True
+ elif t < self.count:
+ break
+
+ return False
+
+ def call_evaluate_while_simplifying_cached(self, rule: StardewRule, state: CollectionState) -> bool:
+ try:
+ # A mapping table with the original rule is used here because two rules could resolve to the same rule.
+ # This would require to change the counter to merge both rules, and quickly become complicated.
+ return self.rule_mapping[rule](state)
+ except KeyError:
+ self.rule_mapping[rule], value = rule.evaluate_while_simplifying(state)
+ return value
+
+ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
+ return self, self(state)
+
+ @cached_property
+ def rules_count(self):
+ return len(self.rules)
+
+ def __repr__(self):
+ return f"Received {self.count} [{', '.join(f'{value}x {repr(rule)}' for rule, value in self.counter.items())}]"
+
+
+@dataclass(frozen=True)
+class Has(BaseStardewRule):
+ item: str
+ # For sure there is a better way than just passing all the rules everytime
+ other_rules: Dict[str, StardewRule] = field(repr=False, hash=False, compare=False)
+ group: str = "item"
+
+ def __call__(self, state: CollectionState) -> bool:
+ return self.evaluate_while_simplifying(state)[1]
+
+ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
+ return self.other_rules[self.item].evaluate_while_simplifying(state)
+
+ def __str__(self):
+ if self.item not in self.other_rules:
+ return f"Has {self.item} ({self.group}) -> {MISSING_ITEM}"
+ return f"Has {self.item} ({self.group})"
+
+ def __repr__(self):
+ if self.item not in self.other_rules:
+ return f"Has {self.item} ({self.group}) -> {MISSING_ITEM}"
+ return f"Has {self.item} ({self.group}) -> {repr(self.other_rules[self.item])}"
+
+
+class RepeatableChain(Iterable, Sized):
+ """
+ Essentially a copy of what's in the core, with proper type hinting
+ """
+
+ def __init__(self, *iterable: Union[Iterable, Sized]):
+ self.iterables = iterable
+
+ def __iter__(self):
+ return chain.from_iterable(self.iterables)
+
+ def __bool__(self):
+ return any(sub_iterable for sub_iterable in self.iterables)
+
+ def __len__(self):
+ return sum(len(iterable) for iterable in self.iterables)
+
+ def __contains__(self, item):
+ return any(item in it for it in self.iterables)
diff --git a/worlds/stardew_valley/stardew_rule/indirect_connection.py b/worlds/stardew_valley/stardew_rule/indirect_connection.py
new file mode 100644
index 000000000000..17433f7df4a8
--- /dev/null
+++ b/worlds/stardew_valley/stardew_rule/indirect_connection.py
@@ -0,0 +1,43 @@
+from functools import singledispatch
+from typing import Set
+
+from . import StardewRule, Reach, Count, AggregatingStardewRule, Has
+
+
+def look_for_indirect_connection(rule: StardewRule) -> Set[str]:
+ required_regions = set()
+ _find(rule, required_regions, depth=0)
+ return required_regions
+
+
+@singledispatch
+def _find(rule: StardewRule, regions: Set[str], depth: int):
+ ...
+
+
+@_find.register
+def _(rule: AggregatingStardewRule, regions: Set[str], depth: int):
+ assert depth < 50, "Recursion depth exceeded"
+ for r in rule.original_rules:
+ _find(r, regions, depth + 1)
+
+
+@_find.register
+def _(rule: Count, regions: Set[str], depth: int):
+ assert depth < 50, "Recursion depth exceeded"
+ for r in rule.rules:
+ _find(r, regions, depth + 1)
+
+
+@_find.register
+def _(rule: Has, regions: Set[str], depth: int):
+ assert depth < 50, f"Recursion depth exceeded on {rule.item}"
+ r = rule.other_rules[rule.item]
+ _find(r, regions, depth + 1)
+
+
+@_find.register
+def _(rule: Reach, regions: Set[str], depth: int):
+ assert depth < 50, "Recursion depth exceeded"
+ if rule.resolution_hint == "Region":
+ regions.add(rule.spot)
diff --git a/worlds/stardew_valley/stardew_rule/literal.py b/worlds/stardew_valley/stardew_rule/literal.py
new file mode 100644
index 000000000000..93a8503e8739
--- /dev/null
+++ b/worlds/stardew_valley/stardew_rule/literal.py
@@ -0,0 +1,56 @@
+from abc import ABC
+from typing import Tuple
+
+from BaseClasses import CollectionState
+from .protocol import StardewRule
+
+
+class LiteralStardewRule(StardewRule, ABC):
+ value: bool
+
+ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
+ return self, self.value
+
+ def __call__(self, state: CollectionState) -> bool:
+ return self.value
+
+ def __repr__(self):
+ return str(self.value)
+
+
+class True_(LiteralStardewRule): # noqa
+ value = True
+
+ def __new__(cls, _cache=[]): # noqa
+ # Only one single instance will be ever created.
+ if not _cache:
+ _cache.append(super(True_, cls).__new__(cls))
+ return _cache[0]
+
+ def __or__(self, other) -> StardewRule:
+ return self
+
+ def __and__(self, other) -> StardewRule:
+ return other
+
+
+class False_(LiteralStardewRule): # noqa
+ value = False
+
+ def __new__(cls, _cache=[]): # noqa
+ # Only one single instance will be ever created.
+ if not _cache:
+ _cache.append(super(False_, cls).__new__(cls))
+ return _cache[0]
+
+ def __or__(self, other) -> StardewRule:
+ return other
+
+ def __and__(self, other) -> StardewRule:
+ return self
+
+
+false_ = False_()
+true_ = True_()
+assert false_
+assert true_
diff --git a/worlds/stardew_valley/stardew_rule/protocol.py b/worlds/stardew_valley/stardew_rule/protocol.py
new file mode 100644
index 000000000000..f69a3663c63a
--- /dev/null
+++ b/worlds/stardew_valley/stardew_rule/protocol.py
@@ -0,0 +1,26 @@
+from __future__ import annotations
+
+from abc import abstractmethod
+from typing import Protocol, Tuple, runtime_checkable
+
+from BaseClasses import CollectionState
+
+
+@runtime_checkable
+class StardewRule(Protocol):
+
+ @abstractmethod
+ def __call__(self, state: CollectionState) -> bool:
+ ...
+
+ @abstractmethod
+ def __and__(self, other: StardewRule):
+ ...
+
+ @abstractmethod
+ def __or__(self, other: StardewRule):
+ ...
+
+ @abstractmethod
+ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
+ ...
diff --git a/worlds/stardew_valley/stardew_rule/rule_explain.py b/worlds/stardew_valley/stardew_rule/rule_explain.py
new file mode 100644
index 000000000000..a9767c7b72d5
--- /dev/null
+++ b/worlds/stardew_valley/stardew_rule/rule_explain.py
@@ -0,0 +1,191 @@
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from functools import cached_property, singledispatch
+from typing import Iterable, Set, Tuple, List, Optional
+
+from BaseClasses import CollectionState
+from worlds.generic.Rules import CollectionRule
+from . import StardewRule, AggregatingStardewRule, Count, Has, TotalReceived, Received, Reach, true_
+
+
+@dataclass
+class RuleExplanation:
+ rule: StardewRule
+ state: CollectionState
+ expected: bool
+ sub_rules: Iterable[StardewRule] = field(default_factory=list)
+ explored_rules_key: Set[Tuple[str, str]] = field(default_factory=set)
+ current_rule_explored: bool = False
+
+ def __post_init__(self):
+ checkpoint = _rule_key(self.rule)
+ if checkpoint is not None and checkpoint in self.explored_rules_key:
+ self.current_rule_explored = True
+ self.sub_rules = []
+
+ def summary(self, depth=0) -> str:
+ summary = " " * depth + f"{str(self.rule)} -> {self.result}"
+ if self.current_rule_explored:
+ summary += " [Already explained]"
+ return summary
+
+ def __str__(self, depth=0):
+ if not self.sub_rules:
+ return self.summary(depth)
+
+ return self.summary(depth) + "\n" + "\n".join(i.__str__(depth + 1)
+ if i.result is not self.expected else i.summary(depth + 1)
+ for i in sorted(self.explained_sub_rules, key=lambda x: x.result))
+
+ def __repr__(self, depth=0):
+ if not self.sub_rules:
+ return self.summary(depth)
+
+ return self.summary(depth) + "\n" + "\n".join(i.__repr__(depth + 1)
+ for i in sorted(self.explained_sub_rules, key=lambda x: x.result))
+
+ @cached_property
+ def result(self) -> bool:
+ try:
+ return self.rule(self.state)
+ except KeyError:
+ return False
+
+ @cached_property
+ def explained_sub_rules(self) -> List[RuleExplanation]:
+ rule_key = _rule_key(self.rule)
+ if rule_key is not None:
+ self.explored_rules_key.add(rule_key)
+
+ return [_explain(i, self.state, self.expected, self.explored_rules_key) for i in self.sub_rules]
+
+
+@dataclass
+class CountSubRuleExplanation(RuleExplanation):
+ count: int = 1
+
+ @staticmethod
+ def from_explanation(expl: RuleExplanation, count: int) -> CountSubRuleExplanation:
+ return CountSubRuleExplanation(expl.rule, expl.state, expl.expected, expl.sub_rules, expl.explored_rules_key, expl.current_rule_explored, count)
+
+ def summary(self, depth=0) -> str:
+ summary = " " * depth + f"{self.count}x {str(self.rule)} -> {self.result}"
+ if self.current_rule_explored:
+ summary += " [Already explained]"
+ return summary
+
+
+@dataclass
+class CountExplanation(RuleExplanation):
+ rule: Count
+
+ @cached_property
+ def explained_sub_rules(self) -> List[RuleExplanation]:
+ return [
+ CountSubRuleExplanation.from_explanation(_explain(rule, self.state, self.expected, self.explored_rules_key), count)
+ for rule, count in self.rule.counter.items()
+ ]
+
+
+def explain(rule: CollectionRule, state: CollectionState, expected: bool = True) -> RuleExplanation:
+ if isinstance(rule, StardewRule):
+ return _explain(rule, state, expected, explored_spots=set())
+ else:
+ return f"Value of rule {str(rule)} was not {str(expected)} in {str(state)}" # noqa
+
+
+@singledispatch
+def _explain(rule: StardewRule, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation:
+ return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots)
+
+
+@_explain.register
+def _(rule: AggregatingStardewRule, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation:
+ return RuleExplanation(rule, state, expected, rule.original_rules, explored_rules_key=explored_spots)
+
+
+@_explain.register
+def _(rule: Count, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation:
+ return CountExplanation(rule, state, expected, rule.rules, explored_rules_key=explored_spots)
+
+
+@_explain.register
+def _(rule: Has, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation:
+ try:
+ return RuleExplanation(rule, state, expected, [rule.other_rules[rule.item]], explored_rules_key=explored_spots)
+ except KeyError:
+ return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots)
+
+
+@_explain.register
+def _(rule: TotalReceived, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation:
+ return RuleExplanation(rule, state, expected, [Received(i, rule.player, 1) for i in rule.items], explored_rules_key=explored_spots)
+
+
+@_explain.register
+def _(rule: Reach, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation:
+ access_rules = None
+ if rule.resolution_hint == 'Location':
+ spot = state.multiworld.get_location(rule.spot, rule.player)
+
+ if isinstance(spot.access_rule, StardewRule):
+ if spot.access_rule is true_:
+ access_rules = [Reach(spot.parent_region.name, "Region", rule.player)]
+ else:
+ access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)]
+
+ elif rule.resolution_hint == 'Entrance':
+ spot = state.multiworld.get_entrance(rule.spot, rule.player)
+
+ if isinstance(spot.access_rule, StardewRule):
+ if spot.access_rule is true_:
+ access_rules = [Reach(spot.parent_region.name, "Region", rule.player)]
+ else:
+ access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)]
+
+ else:
+ spot = state.multiworld.get_region(rule.spot, rule.player)
+ access_rules = [*(Reach(e.name, "Entrance", rule.player) for e in spot.entrances)]
+
+ if not access_rules:
+ return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots)
+
+ return RuleExplanation(rule, state, expected, access_rules, explored_rules_key=explored_spots)
+
+
+@_explain.register
+def _(rule: Received, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation:
+ access_rules = None
+ if rule.event:
+ try:
+ spot = state.multiworld.get_location(rule.item, rule.player)
+ if spot.access_rule is true_:
+ access_rules = [Reach(spot.parent_region.name, "Region", rule.player)]
+ else:
+ access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)]
+ except KeyError:
+ pass
+
+ if not access_rules:
+ return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots)
+
+ return RuleExplanation(rule, state, expected, access_rules, explored_rules_key=explored_spots)
+
+
+@singledispatch
+def _rule_key(_: StardewRule) -> Optional[Tuple[str, str]]:
+ return None
+
+
+@_rule_key.register
+def _(rule: Reach) -> Tuple[str, str]:
+ return rule.spot, rule.resolution_hint
+
+
+@_rule_key.register
+def _(rule: Received) -> Optional[Tuple[str, str]]:
+ if not rule.event:
+ return None
+
+ return rule.item, "Logic Event"
diff --git a/worlds/stardew_valley/stardew_rule/state.py b/worlds/stardew_valley/stardew_rule/state.py
new file mode 100644
index 000000000000..5f5e61b3d4e5
--- /dev/null
+++ b/worlds/stardew_valley/stardew_rule/state.py
@@ -0,0 +1,125 @@
+from dataclasses import dataclass
+from typing import Iterable, Union, List, Tuple, Hashable
+
+from BaseClasses import CollectionState
+from .base import BaseStardewRule, CombinableStardewRule
+from .protocol import StardewRule
+
+
+class TotalReceived(BaseStardewRule):
+ count: int
+ items: Iterable[str]
+ player: int
+
+ def __init__(self, count: int, items: Union[str, Iterable[str]], player: int):
+ items_list: List[str]
+
+ if isinstance(items, Iterable):
+ items_list = [*items]
+ else:
+ items_list = [items]
+
+ self.player = player
+ self.items = items_list
+ self.count = count
+
+ def __call__(self, state: CollectionState) -> bool:
+ c = 0
+ for item in self.items:
+ c += state.count(item, self.player)
+ if c >= self.count:
+ return True
+ return False
+
+ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
+ return self, self(state)
+
+ def __repr__(self):
+ return f"Received {self.count} {self.items}"
+
+
+@dataclass(frozen=True)
+class Received(CombinableStardewRule):
+ item: str
+ player: int
+ count: int
+ event: bool = False
+ """Helps `explain` to know it can dig into a location with the same name."""
+
+ @property
+ def combination_key(self) -> Hashable:
+ return self.item
+
+ @property
+ def value(self):
+ return self.count
+
+ def __call__(self, state: CollectionState) -> bool:
+ return state.has(self.item, self.player, self.count)
+
+ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
+ return self, self(state)
+
+ def __repr__(self):
+ if self.count == 1:
+ return f"Received {'event ' if self.event else ''}{self.item}"
+ return f"Received {'event ' if self.event else ''}{self.count} {self.item}"
+
+
+@dataclass(frozen=True)
+class Reach(BaseStardewRule):
+ spot: str
+ resolution_hint: str
+ player: int
+
+ def __call__(self, state: CollectionState) -> bool:
+ if self.resolution_hint == 'Region' and self.spot not in state.multiworld.regions.region_cache[self.player]:
+ return False
+ return state.can_reach(self.spot, self.resolution_hint, self.player)
+
+ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
+ return self, self(state)
+
+ def __repr__(self):
+ return f"Reach {self.resolution_hint} {self.spot}"
+
+
+@dataclass(frozen=True)
+class HasProgressionPercent(CombinableStardewRule):
+ player: int
+ percent: int
+
+ def __post_init__(self):
+ assert self.percent > 0, "HasProgressionPercent rule must be above 0%"
+ assert self.percent <= 100, "HasProgressionPercent rule can't require more than 100% of items"
+
+ @property
+ def combination_key(self) -> Hashable:
+ return HasProgressionPercent.__name__
+
+ @property
+ def value(self):
+ return self.percent
+
+ def __call__(self, state: CollectionState) -> bool:
+ stardew_world = state.multiworld.worlds[self.player]
+ total_count = stardew_world.total_progression_items
+ needed_count = (total_count * self.percent) // 100
+ player_state = state.prog_items[self.player]
+
+ if needed_count <= len(player_state):
+ return True
+
+ total_count = 0
+ for item, item_count in player_state.items():
+ total_count += item_count
+ if total_count >= needed_count:
+ return True
+
+ return False
+
+ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
+ return self, self(state)
+
+ def __repr__(self):
+ return f"Received {self.percent}% progression items"
diff --git a/worlds/stardew_valley/strings/animal_product_names.py b/worlds/stardew_valley/strings/animal_product_names.py
index 6656e70e580d..f89b610ae89d 100644
--- a/worlds/stardew_valley/strings/animal_product_names.py
+++ b/worlds/stardew_valley/strings/animal_product_names.py
@@ -1,24 +1,30 @@
class AnimalProduct:
any_egg = "Any Egg"
- chicken_egg = "Chicken Egg"
- egg = "Egg"
brown_egg = "Egg (Brown)"
- large_egg = "Large Egg"
- large_brown_egg = "Large Egg (Brown)"
- milk = "Milk"
- large_milk = "Large Milk"
+ chicken_egg = "Chicken Egg"
cow_milk = "Cow Milk"
- wool = "Wool"
- goat_milk = "Goat Milk"
- large_goat_milk = "Large Goat Milk"
+ dinosaur_egg = "Dinosaur Egg"
duck_egg = "Duck Egg"
duck_feather = "Duck Feather"
- void_egg = "Void Egg"
- truffle = "Truffle"
+ egg = "Egg"
+ goat_milk = "Goat Milk"
+ golden_egg = "Golden Egg"
+ large_brown_egg = "Large Egg (Brown)"
+ large_egg = "Large Egg"
+ large_goat_milk = "Large Goat Milk"
+ large_milk = "Large Milk"
+ milk = "Milk"
+ ostrich_egg = "Ostrich Egg"
rabbit_foot = "Rabbit's Foot"
roe = "Roe"
- sturgeon_roe = "Sturgeon Roe"
- ostrich_egg = "Ostrich Egg"
- dinosaur_egg = "Dinosaur Egg"
+ slime_egg_blue = "Blue Slime Egg"
+ slime_egg_green = "Green Slime Egg"
+ slime_egg_purple = "Purple Slime Egg"
+ slime_egg_red = "Red Slime Egg"
+ slime_egg_tiger = "Tiger Slime Egg"
squid_ink = "Squid Ink"
+ sturgeon_roe = "Sturgeon Roe"
+ truffle = "Truffle"
+ void_egg = "Void Egg"
+ wool = "Wool"
diff --git a/worlds/stardew_valley/strings/ap_names/ap_option_names.py b/worlds/stardew_valley/strings/ap_names/ap_option_names.py
new file mode 100644
index 000000000000..a5cc10f7d7b8
--- /dev/null
+++ b/worlds/stardew_valley/strings/ap_names/ap_option_names.py
@@ -0,0 +1,16 @@
+class OptionName:
+ walnutsanity_puzzles = "Puzzles"
+ walnutsanity_bushes = "Bushes"
+ walnutsanity_dig_spots = "Dig Spots"
+ walnutsanity_repeatables = "Repeatables"
+ buff_luck = "Luck"
+ buff_damage = "Damage"
+ buff_defense = "Defense"
+ buff_immunity = "Immunity"
+ buff_health = "Health"
+ buff_energy = "Energy"
+ buff_bite = "Bite Rate"
+ buff_fish_trap = "Fish Trap"
+ buff_fishing_bar = "Fishing Bar Size"
+ buff_quality = "Quality"
+ buff_glow = "Glow"
diff --git a/worlds/stardew_valley/strings/ap_names/ap_weapon_names.py b/worlds/stardew_valley/strings/ap_names/ap_weapon_names.py
new file mode 100644
index 000000000000..7fcd6873761c
--- /dev/null
+++ b/worlds/stardew_valley/strings/ap_names/ap_weapon_names.py
@@ -0,0 +1,7 @@
+class APWeapon:
+ weapon = "Progressive Weapon"
+ sword = "Progressive Sword"
+ club = "Progressive Club"
+ dagger = "Progressive Dagger"
+ slingshot = "Progressive Slingshot"
+ footwear = "Progressive Footwear"
diff --git a/worlds/stardew_valley/strings/ap_names/buff_names.py b/worlds/stardew_valley/strings/ap_names/buff_names.py
index 4ddd6fb5034f..0f311869aa9a 100644
--- a/worlds/stardew_valley/strings/ap_names/buff_names.py
+++ b/worlds/stardew_valley/strings/ap_names/buff_names.py
@@ -1,3 +1,13 @@
class Buff:
movement = "Movement Speed Bonus"
- luck = "Luck Bonus"
\ No newline at end of file
+ luck = "Luck Bonus"
+ damage = "Damage Bonus"
+ defense = "Defense Bonus"
+ immunity = "Immunity Bonus"
+ health = "Health Bonus"
+ energy = "Energy Bonus"
+ bite_rate = "Bite Rate Bonus"
+ fish_trap = "Fish Trap Bonus"
+ fishing_bar = "Fishing Bar Size Bonus"
+ quality = "Quality Bonus"
+ glow = "Glow Bonus"
diff --git a/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py b/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py
new file mode 100644
index 000000000000..6826b9234a30
--- /dev/null
+++ b/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py
@@ -0,0 +1,6 @@
+class CommunityUpgrade:
+ raccoon = "Progressive Raccoon"
+ fruit_bats = "Fruit Bats"
+ mushroom_boxes = "Mushroom Boxes"
+ movie_theater = "Progressive Movie Theater"
+ mr_qi_plane_ride = "Mr Qi's Plane Ride"
diff --git a/worlds/stardew_valley/strings/ap_names/event_names.py b/worlds/stardew_valley/strings/ap_names/event_names.py
new file mode 100644
index 000000000000..88f9715abc65
--- /dev/null
+++ b/worlds/stardew_valley/strings/ap_names/event_names.py
@@ -0,0 +1,20 @@
+all_events = set()
+
+
+def event(name: str):
+ all_events.add(name)
+ return name
+
+
+class Event:
+ victory = event("Victory")
+ can_construct_buildings = event("Can Construct Buildings")
+ start_dark_talisman_quest = event("Start Dark Talisman Quest")
+ can_ship_items = event("Can Ship Items")
+ can_shop_at_pierre = event("Can Shop At Pierre's")
+ spring_farming = event("Spring Farming")
+ summer_farming = event("Summer Farming")
+ fall_farming = event("Fall Farming")
+ winter_farming = event("Winter Farming")
+
+ received_walnuts = event("Received Walnuts")
diff --git a/worlds/stardew_valley/strings/ap_names/mods/mod_items.py b/worlds/stardew_valley/strings/ap_names/mods/mod_items.py
new file mode 100644
index 000000000000..58371aebe7ed
--- /dev/null
+++ b/worlds/stardew_valley/strings/ap_names/mods/mod_items.py
@@ -0,0 +1,56 @@
+from typing import List
+
+
+class DeepWoodsItem:
+ pendant_community = "Pendant of Community"
+ pendant_elder = "Pendant of Elders"
+ pendant_depths = "Pendant of Depths"
+ obelisk_sigil = "Progressive Woods Obelisk Sigils"
+
+
+class SkillLevel:
+ cooking = "Cooking Level"
+ binning = "Binning Level"
+ magic = "Magic Level"
+ socializing = "Socializing Level"
+ luck = "Luck Level"
+ archaeology = "Archaeology Level"
+
+
+class SVEQuestItem:
+ aurora_vineyard_tablet = "Aurora Vineyard Tablet"
+ iridium_bomb = "Iridium Bomb"
+ void_soul = "Void Spirit Peace Agreement"
+ kittyfish_spell = "Kittyfish Spell"
+ scarlett_job_offer = "Scarlett's Job Offer"
+ morgan_schooling = "Morgan's Schooling"
+ diamond_wand = "Diamond Wand"
+ marlon_boat_paddle = "Marlon's Boat Paddle"
+ fable_reef_portal = "Fable Reef Portal"
+ grandpa_shed = "Grandpa's Shed"
+
+ sve_always_quest_items: List[str] = [kittyfish_spell, scarlett_job_offer, morgan_schooling]
+ sve_always_quest_items_ginger_island: List[str] = [fable_reef_portal]
+ sve_quest_items: List[str] = [aurora_vineyard_tablet, iridium_bomb, void_soul, grandpa_shed]
+ sve_quest_items_ginger_island: List[str] = [marlon_boat_paddle]
+
+
+class SVELocation:
+ tempered_galaxy_sword = "Tempered Galaxy Sword"
+ tempered_galaxy_hammer = "Tempered Galaxy Hammer"
+ tempered_galaxy_dagger = "Tempered Galaxy Dagger"
+ diamond_wand = "Lance's Diamond Wand"
+ monster_crops = "Monster Crops"
+
+
+class SVERunes:
+ nexus_guild = "Nexus: Adventurer's Guild Runes"
+ nexus_junimo = "Nexus: Junimo Woods Runes"
+ nexus_outpost = "Nexus: Outpost Runes"
+ nexus_aurora = "Nexus: Aurora Vineyard Runes"
+ nexus_spring = "Nexus: Sprite Spring Runes"
+ nexus_farm = "Nexus: Farm Runes"
+ nexus_wizard = "Nexus: Wizard Runes"
+
+ nexus_items: List[str] = [nexus_farm, nexus_wizard, nexus_spring, nexus_aurora, nexus_guild, nexus_junimo, nexus_outpost]
+
diff --git a/worlds/stardew_valley/strings/artisan_good_names.py b/worlds/stardew_valley/strings/artisan_good_names.py
index 469644d95fd3..366189568cf7 100644
--- a/worlds/stardew_valley/strings/artisan_good_names.py
+++ b/worlds/stardew_valley/strings/artisan_good_names.py
@@ -21,3 +21,46 @@ class ArtisanGood:
caviar = "Caviar"
green_tea = "Green Tea"
mead = "Mead"
+ mystic_syrup = "Mystic Syrup"
+ dried_fruit = "Dried Fruit"
+ dried_mushroom = "Dried Mushrooms"
+ raisins = "Raisins"
+ stardrop_tea = "Stardrop Tea"
+ smoked_fish = "Smoked Fish"
+ targeted_bait = "Targeted Bait"
+
+ @classmethod
+ def specific_wine(cls, fruit: str) -> str:
+ return f"{cls.wine} [{fruit}]"
+
+ @classmethod
+ def specific_juice(cls, vegetable: str) -> str:
+ return f"{cls.juice} [{vegetable}]"
+
+ @classmethod
+ def specific_jelly(cls, fruit: str) -> str:
+ return f"{cls.jelly} [{fruit}]"
+
+ @classmethod
+ def specific_pickles(cls, vegetable: str) -> str:
+ return f"{cls.pickles} [{vegetable}]"
+
+ @classmethod
+ def specific_dried_fruit(cls, food: str) -> str:
+ return f"{cls.dried_fruit} [{food}]"
+
+ @classmethod
+ def specific_dried_mushroom(cls, food: str) -> str:
+ return f"{cls.dried_mushroom} [{food}]"
+
+ @classmethod
+ def specific_smoked_fish(cls, fish: str) -> str:
+ return f"{cls.smoked_fish} [{fish}]"
+
+ @classmethod
+ def specific_bait(cls, fish: str) -> str:
+ return f"{cls.targeted_bait} [{fish}]"
+
+
+class ModArtisanGood:
+ pterodactyl_egg = "Pterodactyl Egg"
diff --git a/worlds/stardew_valley/strings/book_names.py b/worlds/stardew_valley/strings/book_names.py
new file mode 100644
index 000000000000..6c271f42ae9c
--- /dev/null
+++ b/worlds/stardew_valley/strings/book_names.py
@@ -0,0 +1,61 @@
+class Book:
+ animal_catalogue = "Animal Catalogue"
+ book_of_mysteries = "Book of Mysteries"
+ book_of_stars = "Book Of Stars"
+ stardew_valley_almanac = "Stardew Valley Almanac"
+ bait_and_bobber = "Bait And Bobber"
+ mining_monthly = "Mining Monthly"
+ combat_quarterly = "Combat Quarterly"
+ woodcutters_weekly = "Woodcutter's Weekly"
+ the_alleyway_buffet = "The Alleyway Buffet"
+ the_art_o_crabbing = "The Art O' Crabbing"
+ dwarvish_safety_manual = "Dwarvish Safety Manual"
+ jewels_of_the_sea = "Jewels Of The Sea"
+ raccoon_journal = "Raccoon Journal"
+ woodys_secret = "Woody's Secret"
+ jack_be_nimble_jack_be_thick = "Jack Be Nimble, Jack Be Thick"
+ friendship_101 = "Friendship 101"
+ monster_compendium = "Monster Compendium"
+ mapping_cave_systems = "Mapping Cave Systems"
+ treasure_appraisal_guide = "Treasure Appraisal Guide"
+ way_of_the_wind_pt_1 = "Way Of The Wind pt. 1"
+ way_of_the_wind_pt_2 = "Way Of The Wind pt. 2"
+ horse_the_book = "Horse: The Book"
+ ol_slitherlegs = "Ol' Slitherlegs"
+ queen_of_sauce_cookbook = "Queen Of Sauce Cookbook"
+ price_catalogue = "Price Catalogue"
+ the_diamond_hunter = "The Diamond Hunter"
+
+
+ordered_lost_books = []
+all_lost_books = set()
+
+
+def lost_book(book_name: str):
+ ordered_lost_books.append(book_name)
+ all_lost_books.add(book_name)
+ return book_name
+
+
+class LostBook:
+ tips_on_farming = lost_book("Tips on Farming")
+ this_is_a_book_by_marnie = lost_book("This is a book by Marnie")
+ on_foraging = lost_book("On Foraging")
+ the_fisherman_act_1 = lost_book("The Fisherman, Act 1")
+ how_deep_do_the_mines_go = lost_book("How Deep do the mines go?")
+ an_old_farmers_journal = lost_book("An Old Farmer's Journal")
+ scarecrows = lost_book("Scarecrows")
+ the_secret_of_the_stardrop = lost_book("The Secret of the Stardrop")
+ journey_of_the_prairie_king_the_smash_hit_video_game = lost_book("Journey of the Prairie King -- The Smash Hit Video Game!")
+ a_study_on_diamond_yields = lost_book("A Study on Diamond Yields")
+ brewmasters_guide = lost_book("Brewmaster's Guide")
+ mysteries_of_the_dwarves = lost_book("Mysteries of the Dwarves")
+ highlights_from_the_book_of_yoba = lost_book("Highlights From The Book of Yoba")
+ marriage_guide_for_farmers = lost_book("Marriage Guide for Farmers")
+ the_fisherman_act_ii = lost_book("The Fisherman, Act II")
+ technology_report = lost_book("Technology Report!")
+ secrets_of_the_legendary_fish = lost_book("Secrets of the Legendary Fish")
+ gunther_tunnel_notice = lost_book("Gunther Tunnel Notice")
+ note_from_gunther = lost_book("Note From Gunther")
+ goblins_by_m_jasper = lost_book("Goblins by M. Jasper")
+ secret_statues_acrostics = lost_book("Secret Statues Acrostics")
diff --git a/worlds/stardew_valley/strings/bundle_names.py b/worlds/stardew_valley/strings/bundle_names.py
new file mode 100644
index 000000000000..5f560a545434
--- /dev/null
+++ b/worlds/stardew_valley/strings/bundle_names.py
@@ -0,0 +1,108 @@
+class CCRoom:
+ pantry = "Pantry"
+ crafts_room = "Crafts Room"
+ fish_tank = "Fish Tank"
+ bulletin_board = "Bulletin Board"
+ vault = "Vault"
+ boiler_room = "Boiler Room"
+ abandoned_joja_mart = "Abandoned Joja Mart"
+ raccoon_requests = "Raccoon Requests"
+
+
+all_cc_bundle_names = []
+
+
+def cc_bundle(name: str) -> str:
+ all_cc_bundle_names.append(name)
+ return name
+
+
+class BundleName:
+ spring_foraging = cc_bundle("Spring Foraging Bundle")
+ summer_foraging = cc_bundle("Summer Foraging Bundle")
+ fall_foraging = cc_bundle("Fall Foraging Bundle")
+ winter_foraging = cc_bundle("Winter Foraging Bundle")
+ construction = cc_bundle("Construction Bundle")
+ exotic_foraging = cc_bundle("Exotic Foraging Bundle")
+ beach_foraging = cc_bundle("Beach Foraging Bundle")
+ mines_foraging = cc_bundle("Mines Foraging Bundle")
+ desert_foraging = cc_bundle("Desert Foraging Bundle")
+ island_foraging = cc_bundle("Island Foraging Bundle")
+ sticky = cc_bundle("Sticky Bundle")
+ forest = cc_bundle("Forest Bundle")
+ green_rain = cc_bundle("Green Rain Bundle")
+ wild_medicine = cc_bundle("Wild Medicine Bundle")
+ quality_foraging = cc_bundle("Quality Foraging Bundle")
+ spring_crops = cc_bundle("Spring Crops Bundle")
+ summer_crops = cc_bundle("Summer Crops Bundle")
+ fall_crops = cc_bundle("Fall Crops Bundle")
+ quality_crops = cc_bundle("Quality Crops Bundle")
+ animal = cc_bundle("Animal Bundle")
+ artisan = cc_bundle("Artisan Bundle")
+ rare_crops = cc_bundle("Rare Crops Bundle")
+ fish_farmer = cc_bundle("Fish Farmer's Bundle")
+ garden = cc_bundle("Garden Bundle")
+ brewer = cc_bundle("Brewer's Bundle")
+ orchard = cc_bundle("Orchard Bundle")
+ island_crops = cc_bundle("Island Crops Bundle")
+ agronomist = cc_bundle("Agronomist's Bundle")
+ slime_farmer = cc_bundle("Slime Farmer Bundle")
+ sommelier = cc_bundle("Sommelier Bundle")
+ dry = cc_bundle("Dry Bundle")
+ river_fish = cc_bundle("River Fish Bundle")
+ lake_fish = cc_bundle("Lake Fish Bundle")
+ ocean_fish = cc_bundle("Ocean Fish Bundle")
+ night_fish = cc_bundle("Night Fishing Bundle")
+ crab_pot = cc_bundle("Crab Pot Bundle")
+ trash = cc_bundle("Trash Bundle")
+ recycling = cc_bundle("Recycling Bundle")
+ specialty_fish = cc_bundle("Specialty Fish Bundle")
+ spring_fish = cc_bundle("Spring Fishing Bundle")
+ summer_fish = cc_bundle("Summer Fishing Bundle")
+ fall_fish = cc_bundle("Fall Fishing Bundle")
+ winter_fish = cc_bundle("Winter Fishing Bundle")
+ rain_fish = cc_bundle("Rain Fishing Bundle")
+ quality_fish = cc_bundle("Quality Fish Bundle")
+ master_fisher = cc_bundle("Master Fisher's Bundle")
+ legendary_fish = cc_bundle("Legendary Fish Bundle")
+ island_fish = cc_bundle("Island Fish Bundle")
+ deep_fishing = cc_bundle("Deep Fishing Bundle")
+ tackle = cc_bundle("Tackle Bundle")
+ bait = cc_bundle("Master Baiter Bundle")
+ specific_bait = cc_bundle("Specific Fishing Bundle")
+ fish_smoker = cc_bundle("Fish Smoker Bundle")
+ blacksmith = cc_bundle("Blacksmith's Bundle")
+ geologist = cc_bundle("Geologist's Bundle")
+ adventurer = cc_bundle("Adventurer's Bundle")
+ treasure_hunter = cc_bundle("Treasure Hunter's Bundle")
+ engineer = cc_bundle("Engineer's Bundle")
+ demolition = cc_bundle("Demolition Bundle")
+ paleontologist = cc_bundle("Paleontologist's Bundle")
+ archaeologist = cc_bundle("Archaeologist's Bundle")
+ chef = cc_bundle("Chef's Bundle")
+ dye = cc_bundle("Dye Bundle")
+ field_research = cc_bundle("Field Research Bundle")
+ fodder = cc_bundle("Fodder Bundle")
+ enchanter = cc_bundle("Enchanter's Bundle")
+ children = cc_bundle("Children's Bundle")
+ forager = cc_bundle("Forager's Bundle")
+ home_cook = cc_bundle("Home Cook's Bundle")
+ helper = cc_bundle("Helper's Bundle")
+ spirit_eve = cc_bundle("Spirit's Eve Bundle")
+ winter_star = cc_bundle("Winter Star Bundle")
+ bartender = cc_bundle("Bartender's Bundle")
+ calico = cc_bundle("Calico Bundle")
+ raccoon = cc_bundle("Raccoon Bundle")
+ money_2500 = cc_bundle("2,500g Bundle")
+ money_5000 = cc_bundle("5,000g Bundle")
+ money_10000 = cc_bundle("10,000g Bundle")
+ money_25000 = cc_bundle("25,000g Bundle")
+ gambler = cc_bundle("Gambler's Bundle")
+ carnival = cc_bundle("Carnival Bundle")
+ walnut_hunter = cc_bundle("Walnut Hunter Bundle")
+ qi_helper = cc_bundle("Qi's Helper Bundle")
+ missing_bundle = "The Missing Bundle"
+ raccoon_fish = "Raccoon Fish"
+ raccoon_artisan = "Raccoon Artisan"
+ raccoon_food = "Raccoon Food"
+ raccoon_foraging = "Raccoon Foraging"
diff --git a/worlds/stardew_valley/strings/craftable_names.py b/worlds/stardew_valley/strings/craftable_names.py
index a1ee15b12fde..83445c702c32 100644
--- a/worlds/stardew_valley/strings/craftable_names.py
+++ b/worlds/stardew_valley/strings/craftable_names.py
@@ -1,16 +1,216 @@
-class Craftable:
- bait = "Bait"
+class Bomb:
cherry_bomb = "Cherry Bomb"
bomb = "Bomb"
mega_bomb = "Mega Bomb"
- staircase = "Staircase"
- scarecrow = "Scarecrow"
- rain_totem = "Rain Totem"
- flute_block = "Flute Block"
+
+
+class Fence:
+ gate = "Gate"
+ wood = "Wood Fence"
+ stone = "Stone Fence"
+ iron = "Iron Fence"
+ hardwood = "Hardwood Fence"
+
+
+class Sprinkler:
+ basic = "Sprinkler"
+ quality = "Quality Sprinkler"
+ iridium = "Iridium Sprinkler"
+
+
+class WildSeeds:
+ spring = "Spring Seeds"
+ summer = "Summer Seeds"
+ fall = "Fall Seeds"
+ winter = "Winter Seeds"
+ ancient = "Ancient Seeds"
+ grass_starter = "Grass Starter"
+ blue_grass_starter = "Blue Grass Starter"
+ tea_sapling = "Tea Sapling"
+ fiber = "Fiber Seeds"
+
+
+class Floor:
+ wood = "Wood Floor"
+ rustic = "Rustic Plank Floor"
+ straw = "Straw Floor"
+ weathered = "Weathered Floor"
+ crystal = "Crystal Floor"
+ stone = "Stone Floor"
+ stone_walkway = "Stone Walkway Floor"
+ brick = "Brick Floor"
+ wood_path = "Wood Path"
+ gravel_path = "Gravel Path"
+ cobblestone_path = "Cobblestone Path"
+ stepping_stone_path = "Stepping Stone Path"
+ crystal_path = "Crystal Path"
+
+
+class Fishing:
+ spinner = "Spinner"
+ trap_bobber = "Trap Bobber"
+ sonar_bobber = "Sonar Bobber"
+ cork_bobber = "Cork Bobber"
+ quality_bobber = "Quality Bobber"
+ treasure_hunter = "Treasure Hunter"
+ dressed_spinner = "Dressed Spinner"
+ barbed_hook = "Barbed Hook"
+ magnet = "Magnet"
+ bait = "Bait"
+ wild_bait = "Wild Bait"
+ magic_bait = "Magic Bait"
+ lead_bobber = "Lead Bobber"
+ curiosity_lure = "Curiosity Lure"
+ deluxe_bait = "Deluxe Bait"
+ challenge_bait = "Challenge Bait"
+
+
+class Ring:
+ hot_java_ring = "Hot Java Ring"
+ sturdy_ring = "Sturdy Ring"
+ warrior_ring = "Warrior Ring"
+ ring_of_yoba = "Ring of Yoba"
+ thorns_ring = "Thorns Ring"
+ glowstone_ring = "Glowstone Ring"
+ iridium_band = "Iridium Band"
+ wedding_ring = "Wedding Ring"
+ lucky_ring = "Lucky Ring"
+
+
+class Edible:
+ field_snack = "Field Snack"
+ bug_steak = "Bug Steak"
life_elixir = "Life Elixir"
- monster_musk = "Monster Musk"
oil_of_garlic = "Oil of Garlic"
+class Consumable:
+ monster_musk = "Monster Musk"
+ fairy_dust = "Fairy Dust"
+ warp_totem_beach = "Warp Totem: Beach"
+ warp_totem_mountains = "Warp Totem: Mountains"
+ warp_totem_farm = "Warp Totem: Farm"
+ warp_totem_desert = "Warp Totem: Desert"
+ warp_totem_island = "Warp Totem: Island"
+ rain_totem = "Rain Totem"
+ mystery_box = "Mystery Box"
+ gold_mystery_box = "Golden Mystery Box"
+ treasure_totem = "Treasure Totem"
+ fireworks_red = "Fireworks (Red)"
+ fireworks_purple = "Fireworks (Purple)"
+ fireworks_green = "Fireworks (Green)"
+ far_away_stone = "Far Away Stone"
+ golden_animal_cracker = "Golden Animal Cracker"
+ butterfly_powder = "Butterfly Powder"
+
+
+class Lighting:
+ torch = "Torch"
+ campfire = "Campfire"
+ wooden_brazier = "Wooden Brazier"
+ stone_brazier = "Stone Brazier"
+ gold_brazier = "Gold Brazier"
+ carved_brazier = "Carved Brazier"
+ stump_brazier = "Stump Brazier"
+ barrel_brazier = "Barrel Brazier"
+ skull_brazier = "Skull Brazier"
+ marble_brazier = "Marble Brazier"
+ wood_lamp_post = "Wood Lamp-post"
+ iron_lamp_post = "Iron Lamp-post"
+ jack_o_lantern = "Jack-O-Lantern"
+
+
+class Furniture:
+ tub_o_flowers = "Tub o' Flowers"
+ wicked_statue = "Wicked Statue"
+ flute_block = "Flute Block"
+ drum_block = "Drum Block"
+
+
+class Storage:
+ chest = "Chest"
+ stone_chest = "Stone Chest"
+ big_chest = "Big Chest"
+ big_stone_chest = "Big Stone Chest"
+
+
+class Sign:
+ wood = "Wood Sign"
+ stone = "Stone Sign"
+ dark = "Dark Sign"
+ text = "Text Sign"
+
+
+class Statue:
+ blessings = "Statue Of Blessings"
+ dwarf_king = "Statue Of The Dwarf King"
+
+
+class Craftable:
+ garden_pot = "Garden Pot"
+ scarecrow = "Scarecrow"
+ deluxe_scarecrow = "Deluxe Scarecrow"
+ staircase = "Staircase"
+ explosive_ammo = "Explosive Ammo"
+ transmute_fe = "Transmute (Fe)"
+ transmute_au = "Transmute (Au)"
+ mini_jukebox = "Mini-Jukebox"
+ mini_obelisk = "Mini-Obelisk"
+ farm_computer = "Farm Computer"
+ hopper = "Hopper"
+ cookout_kit = "Cookout Kit"
+ tent_kit = "Tent Kit"
+
+
+class ModEdible:
+ magic_elixir = "Magic Elixir"
+ aegis_elixir = "Aegis Elixir"
+ armor_elixir = "Armor Elixir"
+ barbarian_elixir = "Barbarian Elixir"
+ lightning_elixir = "Lightning Elixir"
+ gravity_elixir = "Gravity Elixir"
+ hero_elixir = "Hero Elixir"
+ haste_elixir = "Haste Elixir"
+
+
+class ModCraftable:
+ travel_core = "Travel Core"
+ glass_brazier = "Glass Brazier"
+ water_shifter = "Water Shifter"
+ rusty_brazier = "Rusty Brazier"
+ glass_fence = "Glass Fence"
+ bone_fence = "Bone Fence"
+ wooden_display = "Wooden Display"
+ hardwood_display = "Hardwood Display"
+ neanderthal_skeleton = "Neanderthal Skeleton"
+ pterodactyl_skeleton_l = "Pterodactyl Skeleton L"
+ pterodactyl_skeleton_m = "Pterodactyl Skeleton M"
+ pterodactyl_skeleton_r = "Pterodactyl Skeleton R"
+ trex_skeleton_l = "T-Rex Skeleton L"
+ trex_skeleton_m = "T-Rex Skeleton M"
+ trex_skeleton_r = "T-Rex Skeleton R"
+
+
+class ModMachine:
+ preservation_chamber = "Preservation Chamber"
+ hardwood_preservation_chamber = "Hardwood Preservation Chamber"
+ grinder = "Grinder"
+ ancient_battery = "Ancient Battery Production Station"
+ restoration_table = "Restoration Table"
+ trash_bin = "Trash Bin"
+ composter = "Composter"
+ recycling_bin = "Recycling Bin"
+ advanced_recycling_machine = "Advanced Recycling Machine"
+
+
+class ModFloor:
+ glass_path = "Glass Path"
+ bone_path = "Bone Path"
+ rusty_path = "Rusty Path"
+
+
+class ModConsumable:
+ volcano_totem = "Dwarf Gadget: Infinite Volcano Simulation"
+ ginger_tincture = "Ginger Tincture"
diff --git a/worlds/stardew_valley/strings/crop_names.py b/worlds/stardew_valley/strings/crop_names.py
index 2b5ea4d32768..fa7a77c834fc 100644
--- a/worlds/stardew_valley/strings/crop_names.py
+++ b/worlds/stardew_valley/strings/crop_names.py
@@ -1,59 +1,69 @@
-all_fruits = []
-all_vegetables = []
-
-
-def veggie(name: str) -> str:
- all_vegetables.append(name)
- return name
-
-
-def fruity(name: str) -> str:
- all_fruits.append(name)
- return name
-
-
class Fruit:
+ sweet_gem_berry = "Sweet Gem Berry"
any = "Any Fruit"
- blueberry = fruity("Blueberry")
- melon = fruity("Melon")
- apple = fruity("Apple")
- apricot = fruity("Apricot")
- cherry = fruity("Cherry")
- orange = fruity("Orange")
- peach = fruity("Peach")
- pomegranate = fruity("Pomegranate")
- banana = fruity("Banana")
- mango = fruity("Mango")
- pineapple = fruity("Pineapple")
- ancient_fruit = fruity("Ancient Fruit")
- strawberry = fruity("Strawberry")
- starfruit = fruity("Starfruit")
- rhubarb = fruity("Rhubarb")
- grape = fruity("Grape")
- cranberries = fruity("Cranberries")
- hot_pepper = fruity("Hot Pepper")
+ blueberry = "Blueberry"
+ melon = "Melon"
+ apple = "Apple"
+ apricot = "Apricot"
+ cherry = "Cherry"
+ orange = "Orange"
+ peach = "Peach"
+ pomegranate = "Pomegranate"
+ banana = "Banana"
+ mango = "Mango"
+ pineapple = "Pineapple"
+ ancient_fruit = "Ancient Fruit"
+ strawberry = "Strawberry"
+ starfruit = "Starfruit"
+ rhubarb = "Rhubarb"
+ grape = "Grape"
+ cranberries = "Cranberries"
+ hot_pepper = "Hot Pepper"
+ powdermelon = "Powdermelon"
+ qi_fruit = "Qi Fruit"
class Vegetable:
any = "Any Vegetable"
- parsnip = veggie("Parsnip")
- garlic = veggie("Garlic")
+ parsnip = "Parsnip"
+ garlic = "Garlic"
+ bok_choy = "Bok Choy"
wheat = "Wheat"
- potato = veggie("Potato")
- corn = veggie("Corn")
- tomato = veggie("Tomato")
- pumpkin = veggie("Pumpkin")
- unmilled_rice = veggie("Unmilled Rice")
- beet = veggie("Beet")
+ potato = "Potato"
+ corn = "Corn"
+ tomato = "Tomato"
+ pumpkin = "Pumpkin"
+ unmilled_rice = "Unmilled Rice"
+ beet = "Beet"
hops = "Hops"
- cauliflower = veggie("Cauliflower")
- amaranth = veggie("Amaranth")
- kale = veggie("Kale")
- artichoke = veggie("Artichoke")
+ cauliflower = "Cauliflower"
+ amaranth = "Amaranth"
+ kale = "Kale"
+ artichoke = "Artichoke"
tea_leaves = "Tea Leaves"
- eggplant = veggie("Eggplant")
- green_bean = veggie("Green Bean")
- red_cabbage = veggie("Red Cabbage")
- yam = veggie("Yam")
- radish = veggie("Radish")
- taro_root = veggie("Taro Root")
+ eggplant = "Eggplant"
+ green_bean = "Green Bean"
+ red_cabbage = "Red Cabbage"
+ yam = "Yam"
+ radish = "Radish"
+ taro_root = "Taro Root"
+ carrot = "Carrot"
+ summer_squash = "Summer Squash"
+ broccoli = "Broccoli"
+
+
+class SVEFruit:
+ slime_berry = "Slime Berry"
+ monster_fruit = "Monster Fruit"
+ salal_berry = "Salal Berry"
+
+
+class SVEVegetable:
+ monster_mushroom = "Monster Mushroom"
+ void_root = "Void Root"
+ ancient_fiber = "Ancient Fiber"
+
+
+class DistantLandsCrop:
+ void_mint = "Void Mint Leaves"
+ vile_ancient_fruit = "Vile Ancient Fruit"
diff --git a/worlds/stardew_valley/strings/currency_names.py b/worlds/stardew_valley/strings/currency_names.py
new file mode 100644
index 000000000000..21ccb5b55c58
--- /dev/null
+++ b/worlds/stardew_valley/strings/currency_names.py
@@ -0,0 +1,16 @@
+class Currency:
+ qi_coin = "Qi Coin"
+ golden_walnut = "Golden Walnut"
+ qi_gem = "Qi Gem"
+ star_token = "Star Token"
+ money = "Money"
+ cinder_shard = "Cinder Shard"
+ prize_ticket = "Prize Ticket"
+ calico_egg = "Calico Egg"
+ golden_tag = "Golden Tag"
+
+ @staticmethod
+ def is_currency(item: str) -> bool:
+ return item in [Currency.qi_coin, Currency.golden_walnut, Currency.qi_gem, Currency.star_token, Currency.money]
+
+
diff --git a/worlds/stardew_valley/strings/decoration_names.py b/worlds/stardew_valley/strings/decoration_names.py
new file mode 100644
index 000000000000..150a106c6a20
--- /dev/null
+++ b/worlds/stardew_valley/strings/decoration_names.py
@@ -0,0 +1,2 @@
+class Decoration:
+ rotten_plant = "Rotten Plant"
diff --git a/worlds/stardew_valley/strings/entrance_names.py b/worlds/stardew_valley/strings/entrance_names.py
index e744400cfbd5..58a919f2a8a4 100644
--- a/worlds/stardew_valley/strings/entrance_names.py
+++ b/worlds/stardew_valley/strings/entrance_names.py
@@ -2,6 +2,10 @@ def dig_to_mines_floor(floor: int) -> str:
return f"Dig to The Mines - Floor {floor}"
+def dig_to_dangerous_mines_floor(floor: int) -> str:
+ return f"Dig to the Dangerous Mines - Floor {floor}"
+
+
def dig_to_skull_floor(floor: int) -> str:
return f"Mine to Skull Cavern Floor {floor}"
@@ -22,6 +26,10 @@ class Entrance:
farm_to_forest = "Farm to Forest"
farm_to_farmcave = "Farm to Farmcave"
enter_greenhouse = "Farm to Greenhouse"
+ enter_coop = "Farm to Coop"
+ enter_barn = "Farm to Barn"
+ enter_shed = "Farm to Shed"
+ enter_slime_hutch = "Farm to Slime Hutch"
use_desert_obelisk = "Use Desert Obelisk"
use_island_obelisk = "Use Island Obelisk"
use_farm_obelisk = "Use Farm Obelisk"
@@ -34,7 +42,7 @@ class Entrance:
forest_to_marnie_ranch = "Forest to Marnie's Ranch"
forest_to_leah_cottage = "Forest to Leah's Cottage"
forest_to_sewer = "Forest to Sewer"
- buy_from_traveling_merchant = "Buy from Traveling Merchant"
+ forest_to_mastery_cave = "Forest to Mastery Cave"
mountain_to_railroad = "Mountain to Railroad"
mountain_to_tent = "Mountain to Tent"
mountain_to_carpenter_shop = "Mountain to Carpenter Shop"
@@ -42,6 +50,7 @@ class Entrance:
mountain_to_the_mines = "Mountain to The Mines"
enter_quarry = "Mountain to Quarry"
mountain_to_adventurer_guild = "Mountain to Adventurer's Guild"
+ adventurer_guild_to_bedroom = "Adventurer's Guild to Marlon's Bedroom"
mountain_to_town = "Mountain to Town"
town_to_community_center = "Town to Community Center"
access_crafts_room = "Access Crafts Room"
@@ -63,6 +72,9 @@ class Entrance:
town_to_clint_blacksmith = "Town to Clint's Blacksmith"
town_to_museum = "Town to Museum"
town_to_jojamart = "Town to JojaMart"
+ purchase_movie_ticket = "Purchase Movie Ticket"
+ enter_abandoned_jojamart = "Enter Abandoned Joja Mart"
+ enter_movie_theater = "Enter Movie Theater"
beach_to_willy_fish_shop = "Beach to Willy's Fish Shop"
fish_shop_to_boat_tunnel = "Fish Shop to Boat Tunnel"
boat_to_ginger_island = "Take the Boat to Ginger Island"
@@ -101,7 +113,7 @@ class Entrance:
mine_to_skull_cavern_floor_150 = dig_to_skull_floor(150)
mine_to_skull_cavern_floor_175 = dig_to_skull_floor(175)
mine_to_skull_cavern_floor_200 = dig_to_skull_floor(200)
- talk_to_mines_dwarf = "Talk to Mines Dwarf"
+ enter_dangerous_skull_cavern = "Enter the Dangerous Skull Cavern"
dig_to_mines_floor_5 = dig_to_mines_floor(5)
dig_to_mines_floor_10 = dig_to_mines_floor(10)
dig_to_mines_floor_15 = dig_to_mines_floor(15)
@@ -126,6 +138,9 @@ class Entrance:
dig_to_mines_floor_110 = dig_to_mines_floor(110)
dig_to_mines_floor_115 = dig_to_mines_floor(115)
dig_to_mines_floor_120 = dig_to_mines_floor(120)
+ dig_to_dangerous_mines_20 = dig_to_dangerous_mines_floor(20)
+ dig_to_dangerous_mines_60 = dig_to_dangerous_mines_floor(60)
+ dig_to_dangerous_mines_100 = dig_to_dangerous_mines_floor(100)
island_south_to_west = "Island South to West"
island_south_to_north = "Island South to North"
island_south_to_east = "Island South to East"
@@ -162,6 +177,65 @@ class Entrance:
parrot_express_dig_site_to_docks = "Parrot Express Dig Site to Docks"
parrot_express_volcano_to_docks = "Parrot Express Volcano to Docks"
+
+class LogicEntrance:
+ talk_to_mines_dwarf = "Talk to Mines Dwarf"
+
+ buy_from_traveling_merchant = "Buy from Traveling Merchant"
+ buy_from_traveling_merchant_sunday = "Buy from Traveling Merchant Sunday"
+ buy_from_traveling_merchant_monday = "Buy from Traveling Merchant Monday"
+ buy_from_traveling_merchant_tuesday = "Buy from Traveling Merchant Tuesday"
+ buy_from_traveling_merchant_wednesday = "Buy from Traveling Merchant Wednesday"
+ buy_from_traveling_merchant_thursday = "Buy from Traveling Merchant Thursday"
+ buy_from_traveling_merchant_friday = "Buy from Traveling Merchant Friday"
+ buy_from_traveling_merchant_saturday = "Buy from Traveling Merchant Saturday"
+ farmhouse_cooking = "Farmhouse Cooking"
+ island_cooking = "Island Cooking"
+ shipping = "Use Shipping Bin"
+ watch_queen_of_sauce = "Watch Queen of Sauce"
+ blacksmith_copper = "Upgrade Copper Tools"
+ blacksmith_iron = "Upgrade Iron Tools"
+ blacksmith_gold = "Upgrade Gold Tools"
+ blacksmith_iridium = "Upgrade Iridium Tools"
+
+ grow_spring_crops = "Grow Spring Crops"
+ grow_summer_crops = "Grow Summer Crops"
+ grow_fall_crops = "Grow Fall Crops"
+ grow_winter_crops = "Grow Winter Crops"
+ grow_spring_crops_in_greenhouse = "Grow Spring Crops in Greenhouse"
+ grow_summer_crops_in_greenhouse = "Grow Summer Crops in Greenhouse"
+ grow_fall_crops_in_greenhouse = "Grow Fall Crops in Greenhouse"
+ grow_winter_crops_in_greenhouse = "Grow Winter Crops in Greenhouse"
+ grow_indoor_crops_in_greenhouse = "Grow Indoor Crops in Greenhouse"
+ grow_spring_crops_on_island = "Grow Spring Crops on Island"
+ grow_summer_crops_on_island = "Grow Summer Crops on Island"
+ grow_fall_crops_on_island = "Grow Fall Crops on Island"
+ grow_winter_crops_on_island = "Grow Winter Crops on Island"
+ grow_indoor_crops_on_island = "Grow Indoor Crops on Island"
+ grow_summer_fall_crops_in_summer = "Grow Summer Fall Crops in Summer"
+ grow_summer_fall_crops_in_fall = "Grow Summer Fall Crops in Fall"
+
+ fishing = "Start Fishing"
+ attend_egg_festival = "Attend Egg Festival"
+ attend_desert_festival = "Attend Desert Festival"
+ attend_flower_dance = "Attend Flower Dance"
+ attend_luau = "Attend Luau"
+ attend_trout_derby = "Attend Trout Derby"
+ attend_moonlight_jellies = "Attend Dance of the Moonlight Jellies"
+ attend_fair = "Attend Stardew Valley Fair"
+ attend_spirit_eve = "Attend Spirit's Eve"
+ attend_festival_of_ice = "Attend Festival of Ice"
+ attend_night_market = "Attend Night Market"
+ attend_winter_star = "Attend Feast of the Winter Star"
+ attend_squidfest = "Attend SquidFest"
+ buy_experience_books = "Buy Experience Books from the bookseller"
+ buy_year1_books = "Buy Year 1 Books from the Bookseller"
+ buy_year3_books = "Buy Year 3 Books from the Bookseller"
+ complete_raccoon_requests = "Complete Raccoon Requests"
+ buy_from_raccoon = "Buy From Raccoon"
+ fish_in_waterfall = "Fish In Waterfall"
+
+
# Skull Cavern Elevator
@@ -215,3 +289,103 @@ class AyeishaEntrance:
class RileyEntrance:
town_to_riley = "Town to Riley's House"
+
+class SVEEntrance:
+ backwoods_to_grove = "Backwoods to Enchanted Grove"
+ grove_to_outpost_warp = "Enchanted Grove to Grove Outpost Warp"
+ outpost_warp_to_outpost = "Grove Outpost Warp to Galmoran Outpost"
+ grove_to_wizard_warp = "Enchanted Grove to Grove Wizard Warp"
+ wizard_warp_to_wizard = "Grove Wizard Warp to Wizard Basement"
+ grove_to_aurora_warp = "Enchanted Grove to Grove Aurora Vineyard Warp"
+ aurora_warp_to_aurora = "Grove Aurora Vineyard Warp to Aurora Vineyard Basement"
+ grove_to_farm_warp = "Enchanted Grove to Grove Farm Warp"
+ farm_warp_to_farm = "Grove Farm Warp to Farm"
+ grove_to_guild_warp = "Enchanted Grove to Grove Guild Warp"
+ guild_warp_to_guild = "Grove Guild Warp to Guild Summit"
+ grove_to_junimo_warp = "Enchanted Grove to Grove Junimo Woods Warp"
+ junimo_warp_to_junimo = "Grove Junimo Woods Warp to Junimo Woods"
+ grove_to_spring_warp = "Enchanted Grove to Grove Sprite Spring Warp"
+ spring_warp_to_spring = "Grove Sprite Spring Warp to Sprite Spring"
+ wizard_to_fable_reef = "Wizard Basement to Fable Reef"
+ bus_stop_to_shed = "Bus Stop to Grandpa's Shed"
+ grandpa_shed_to_interior = "Grandpa's Shed to Grandpa's Shed Interior"
+ grandpa_shed_to_town = "Grandpa's Shed to Town"
+ grandpa_interior_to_upstairs = "Grandpa's Shed Interior to Grandpa's Shed Upstairs"
+ forest_to_fairhaven = "Forest to Fairhaven Farm"
+ forest_to_west = "Forest to Forest West"
+ forest_to_lost_woods = "Forest to Lost Woods"
+ lost_woods_to_junimo_woods = "Lost Woods to Junimo Woods"
+ use_purple_junimo = "Talk to Purple Junimo"
+ forest_to_bmv = "Forest to Blue Moon Vineyard"
+ forest_to_marnie_shed = "Forest to Marnie's Shed"
+ town_to_bmv = "Town to Blue Moon Vineyard"
+ town_to_jenkins = "Town to Jenkins' Residence"
+ town_to_bridge = "Town to Shearwater Bridge"
+ town_to_plot = "Town to Unclaimed Plot"
+ bmv_to_sophia = "Blue Moon Vineyard to Sophia's House"
+ bmv_to_beach = "Blue Moon Vineyard to Beach"
+ jenkins_to_cellar = "Jenkins' Residence to Jenkins' Cellar"
+ plot_to_bridge = "Unclaimed Plot to Shearwater Bridge"
+ mountain_to_guild_summit = "Mountain to Guild Summit"
+ guild_to_interior = "Guild Summit to Adventurer's Guild"
+ guild_to_mines = "Guild Summit to The Mines"
+ summit_to_boat = "Guild Summit to Marlon's Boat"
+ summit_to_highlands = "Guild Summit to Highlands Outside"
+ to_aurora_basement = "Aurora Vineyard to Aurora Vineyard Basement"
+ outpost_to_badlands_entrance = "Galmoran Outpost to Badlands Entrance"
+ use_alesia_shop = "Talk to Alesia"
+ use_isaac_shop = "Talk to Isaac"
+ badlands_entrance_to_badlands = "Badlands Entrance to Crimson Badlands"
+ badlands_to_cave = "Crimson Badlands to Badlands Cave"
+ to_susan_house = "Railroad to Susan's House"
+ enter_summit = "Railroad to Summit"
+ fable_reef_to_guild = "Fable Reef to First Slash Guild"
+ highlands_to_lance = "Highlands Outside to Lance's House Main"
+ lance_to_ladder = "Lance's House Main to Lance's House Ladder"
+ highlands_to_cave = "Highlands Outside to Highlands Cavern"
+ to_dwarf_prison = "Highlands Cavern to Highlands Cavern Prison"
+ lance_ladder_to_highlands = "Lance's House Ladder to Highlands Outside"
+ forest_west_to_spring = "Forest West to Sprite Spring"
+ west_to_aurora = "Forest West to Aurora Vineyard"
+ use_bear_shop = "Talk to Bear Shop"
+ secret_woods_to_west = "Secret Woods to Forest West"
+ to_outpost_roof = "Galmoran Outpost to Galmoran Outpost Roof"
+ railroad_to_grampleton_station = "Railroad to Grampleton Station"
+ grampleton_station_to_grampleton_suburbs = "Grampleton Station to Grampleton Suburbs"
+ grampleton_suburbs_to_scarlett_house = "Grampleton Suburbs to Scarlett's House"
+ first_slash_guild_to_hallway = "First Slash Guild to First Slash Hallway"
+ first_slash_hallway_to_room = "First Slash Hallway to First Slash Spare Room"
+ sprite_spring_to_cave = "Sprite Spring to Sprite Spring Cave"
+ fish_shop_to_willy_bedroom = "Willy's Fish Shop to Willy's Bedroom"
+ museum_to_gunther_bedroom = "Museum to Gunther's Bedroom"
+ highlands_to_pond = "Highlands to Highlands Pond"
+
+
+class AlectoEntrance:
+ witch_hut_to_witch_attic = "Witch's Hut to Witch's Attic"
+
+
+class LaceyEntrance:
+ forest_to_hat_house = "Forest to Mouse House"
+
+
+class BoardingHouseEntrance:
+ bus_stop_to_boarding_house_plateau = "Bus Stop to Boarding House Outside"
+ boarding_house_plateau_to_boarding_house_first = "Boarding House Outside to Boarding House - First Floor"
+ boarding_house_first_to_boarding_house_second = "Boarding House - First Floor to Boarding House - Second Floor"
+ boarding_house_plateau_to_abandoned_mines_entrance = "Boarding House Outside to Abandoned Mines Entrance"
+ abandoned_mines_entrance_to_abandoned_mines_1a = "Abandoned Mines Entrance to Abandoned Mines - 1A"
+ abandoned_mines_1a_to_abandoned_mines_1b = "Abandoned Mines - 1A to Abandoned Mines - 1B"
+ abandoned_mines_1b_to_abandoned_mines_2a = "Abandoned Mines - 1B to Abandoned Mines - 2A"
+ abandoned_mines_2a_to_abandoned_mines_2b = "Abandoned Mines - 2A to Abandoned Mines - 2B"
+ abandoned_mines_2b_to_abandoned_mines_3 = "Abandoned Mines - 2B to Abandoned Mines - 3"
+ abandoned_mines_3_to_abandoned_mines_4 = "Abandoned Mines - 3 to Abandoned Mines - 4"
+ abandoned_mines_4_to_abandoned_mines_5 = "Abandoned Mines - 4 to Abandoned Mines - 5"
+ abandoned_mines_5_to_the_lost_valley = "Abandoned Mines - 5 to The Lost Valley"
+ lost_valley_to_lost_valley_minecart = "The Lost Valley to Lost Valley Minecart"
+ abandoned_mines_entrance_to_the_lost_valley = "Abandoned Mines Entrance to The Lost Valley"
+ the_lost_valley_to_gregory_tent = "The Lost Valley to Gregory's Tent"
+ the_lost_valley_to_lost_valley_ruins = "The Lost Valley to Lost Valley Ruins"
+ lost_valley_ruins_to_lost_valley_house_1 = "Lost Valley Ruins to Lost Valley Ruins - First House"
+ lost_valley_ruins_to_lost_valley_house_2 = "Lost Valley Ruins to Lost Valley Ruins - Second House"
+ boarding_house_plateau_to_buffalo_ranch = "Boarding House Outside to Buffalo's Ranch"
diff --git a/worlds/stardew_valley/strings/festival_check_names.py b/worlds/stardew_valley/strings/festival_check_names.py
index 404878999fc7..b59b3cd03f17 100644
--- a/worlds/stardew_valley/strings/festival_check_names.py
+++ b/worlds/stardew_valley/strings/festival_check_names.py
@@ -20,6 +20,7 @@ class FestivalCheck:
moonlight_jellies = "Dance of the Moonlight Jellies"
rarecrow_1 = "Rarecrow #1 (Turnip Head)"
rarecrow_2 = "Rarecrow #2 (Witch)"
+ rarecrow_3 = "Rarecrow #3 (Alien)"
rarecrow_4 = "Rarecrow #4 (Snowman)"
rarecrow_5 = "Rarecrow #5 (Woman)"
rarecrow_7 = "Rarecrow #7 (Tanuki)"
@@ -30,3 +31,50 @@ class FestivalCheck:
spirit_eve_maze = "Spirit's Eve Maze"
strawberry_seeds = "Egg Festival: Strawberry Seeds"
all_rarecrows = "Collect All Rarecrows"
+ tub_o_flowers = "Tub o' Flowers Recipe"
+ jack_o_lantern = "Jack-O-Lantern Recipe"
+ moonlight_jellies_banner = "Moonlight Jellies Banner"
+ starport_decal = "Starport Decal"
+ calico_race = "Calico Race"
+ mummy_mask = "Mummy Mask"
+ calico_statue = "Calico Statue"
+ emily_outfit_service = "Emily's Outfit Services"
+ earthy_mousse = "Earthy Mousse"
+ sweet_bean_cake = "Sweet Bean Cake"
+ skull_cave_casserole = "Skull Cave Casserole"
+ spicy_tacos = "Spicy Tacos"
+ mountain_chili = "Mountain Chili"
+ crystal_cake = "Crystal Cake"
+ cave_kebab = "Cave Kebab"
+ hot_log = "Hot Log"
+ sour_salad = "Sour Salad"
+ superfood_cake = "Superfood Cake"
+ warrior_smoothie = "Warrior Smoothie"
+ rumpled_fruit_skin = "Rumpled Fruit Skin"
+ calico_pizza = "Calico Pizza"
+ stuffed_mushrooms = "Stuffed Mushrooms"
+ elf_quesadilla = "Elf Quesadilla"
+ nachos_of_the_desert = "Nachos Of The Desert"
+ cloppino = "Cloppino"
+ rainforest_shrimp = "Rainforest Shrimp"
+ shrimp_donut = "Shrimp Donut"
+ smell_of_the_sea = "Smell Of The Sea"
+ desert_gumbo = "Desert Gumbo"
+ free_cactis = "Free Cactis"
+ monster_hunt = "Monster Hunt"
+ deep_dive = "Deep Dive"
+ treasure_hunt = "Treasure Hunt"
+ touch_calico_statue = "Touch A Calico Statue"
+ real_calico_egg_hunter = "Real Calico Egg Hunter"
+ willy_challenge = "Willy's Challenge"
+ desert_scholar = "Desert Scholar"
+ trout_derby_reward_pattern = "Trout Derby Reward "
+ squidfest_day_1_copper = "SquidFest Day 1 Copper"
+ squidfest_day_1_iron = "SquidFest Day 1 Iron"
+ squidfest_day_1_gold = "SquidFest Day 1 Gold"
+ squidfest_day_1_iridium = "SquidFest Day 1 Iridium"
+ squidfest_day_2_copper = "SquidFest Day 2 Copper"
+ squidfest_day_2_iron = "SquidFest Day 2 Iron"
+ squidfest_day_2_gold = "SquidFest Day 2 Gold"
+ squidfest_day_2_iridium = "SquidFest Day 2 Iridium"
+
diff --git a/worlds/stardew_valley/strings/fish_names.py b/worlds/stardew_valley/strings/fish_names.py
index 8ee778103752..d4ee81430eb4 100644
--- a/worlds/stardew_valley/strings/fish_names.py
+++ b/worlds/stardew_valley/strings/fish_names.py
@@ -1,49 +1,95 @@
+all_fish = []
+
+
+def fish(fish_name: str) -> str:
+ all_fish.append(fish_name)
+ return fish_name
+
+
class Fish:
- angler = "Angler"
- any = "Any Fish"
- blobfish = "Blobfish"
- blue_discus = "Blue Discus"
- bream = "Bream"
- catfish = "Catfish"
- crab = "Crab"
- crayfish = "Crayfish"
- crimsonfish = "Crimsonfish"
- dorado = "Dorado"
- glacierfish = "Glacierfish"
- lava_eel = "Lava Eel"
- legend = "Legend"
- lionfish = "Lionfish"
- lobster = "Lobster"
- mussel = "Mussel"
- mussel_node = "Mussel Node"
- mutant_carp = "Mutant Carp"
- octopus = "Octopus"
- oyster = "Oyster"
- pufferfish = "Pufferfish"
- spookfish = "Spook Fish"
- squid = "Squid"
- stingray = "Stingray"
- sturgeon = "Sturgeon"
- sunfish = "Sunfish"
- void_salmon = "Void Salmon"
- albacore = "Albacore"
- largemouth_bass = "Largemouth Bass"
- smallmouth_bass = "Smallmouth Bass"
- sardine = "Sardine"
- periwinkle = "Periwinkle"
- shrimp = "Shrimp"
- snail = "Snail"
- tuna = "Tuna"
- eel = "Eel"
- salmon = "Salmon"
+ albacore = fish("Albacore")
+ anchovy = fish("Anchovy")
+ angler = fish("Angler")
+ any = fish("Any Fish")
+ blobfish = fish("Blobfish")
+ blue_discus = fish("Blue Discus")
+ bream = fish("Bream")
+ bullhead = fish("Bullhead")
+ carp = fish("Carp")
+ catfish = fish("Catfish")
+ chub = fish("Chub")
+ clam = fish("Clam")
+ cockle = fish("Cockle")
+ crab = fish("Crab")
+ crayfish = fish("Crayfish")
+ crimsonfish = fish("Crimsonfish")
+ dorado = fish("Dorado")
+ eel = fish("Eel")
+ flounder = fish("Flounder")
+ ghostfish = fish("Ghostfish")
+ goby = fish("Goby")
+ glacierfish = fish("Glacierfish")
+ glacierfish_jr = fish("Glacierfish Jr.")
+ halibut = fish("Halibut")
+ herring = fish("Herring")
+ ice_pip = fish("Ice Pip")
+ largemouth_bass = fish("Largemouth Bass")
+ lava_eel = fish("Lava Eel")
+ legend = fish("Legend")
+ legend_ii = fish("Legend II")
+ lingcod = fish("Lingcod")
+ lionfish = fish("Lionfish")
+ lobster = fish("Lobster")
+ midnight_carp = fish("Midnight Carp")
+ midnight_squid = fish("Midnight Squid")
+ ms_angler = fish("Ms. Angler")
+ mussel = fish("Mussel")
+ mussel_node = fish("Mussel Node")
+ mutant_carp = fish("Mutant Carp")
+ octopus = fish("Octopus")
+ oyster = fish("Oyster")
+ perch = fish("Perch")
+ periwinkle = fish("Periwinkle")
+ pike = fish("Pike")
+ pufferfish = fish("Pufferfish")
+ radioactive_carp = fish("Radioactive Carp")
+ rainbow_trout = fish("Rainbow Trout")
+ red_mullet = fish("Red Mullet")
+ red_snapper = fish("Red Snapper")
+ salmon = fish("Salmon")
+ sandfish = fish("Sandfish")
+ sardine = fish("Sardine")
+ scorpion_carp = fish("Scorpion Carp")
+ sea_cucumber = fish("Sea Cucumber")
+ shad = fish("Shad")
+ shrimp = fish("Shrimp")
+ slimejack = fish("Slimejack")
+ smallmouth_bass = fish("Smallmouth Bass")
+ snail = fish("Snail")
+ son_of_crimsonfish = fish("Son of Crimsonfish")
+ spook_fish = fish("Spook Fish")
+ spookfish = fish("Spook Fish")
+ squid = fish("Squid")
+ stingray = fish("Stingray")
+ stonefish = fish("Stonefish")
+ sturgeon = fish("Sturgeon")
+ sunfish = fish("Sunfish")
+ super_cucumber = fish("Super Cucumber")
+ tiger_trout = fish("Tiger Trout")
+ tilapia = fish("Tilapia")
+ tuna = fish("Tuna")
+ void_salmon = fish("Void Salmon")
+ walleye = fish("Walleye")
+ woodskip = fish("Woodskip")
class WaterItem:
+ sea_jelly = "Sea Jelly"
+ river_jelly = "River Jelly"
+ cave_jelly = "Cave Jelly"
seaweed = "Seaweed"
green_algae = "Green Algae"
white_algae = "White Algae"
- clam = "Clam"
- cockle = "Cockle"
coral = "Coral"
nautilus_shell = "Nautilus Shell"
sea_urchin = "Sea Urchin"
@@ -58,5 +104,54 @@ class Trash:
soggy_newspaper = "Soggy Newspaper"
+class WaterChest:
+ fishing_chest = "Fishing Chest"
+ golden_fishing_chest = "Golden Fishing Chest"
+ treasure = "Treasure Chest"
+
+
+class SVEFish:
+ baby_lunaloo = "Baby Lunaloo"
+ bonefish = "Bonefish"
+ bull_trout = "Bull Trout"
+ butterfish = "Butterfish"
+ clownfish = "Clownfish"
+ daggerfish = "Daggerfish"
+ frog = "Frog"
+ gemfish = "Gemfish"
+ goldenfish = "Goldenfish"
+ grass_carp = "Grass Carp"
+ king_salmon = "King Salmon"
+ kittyfish = "Kittyfish"
+ lunaloo = "Lunaloo"
+ meteor_carp = "Meteor Carp"
+ minnow = "Minnow"
+ puppyfish = "Puppyfish"
+ radioactive_bass = "Radioactive Bass"
+ seahorse = "Seahorse"
+ shiny_lunaloo = "Shiny Lunaloo"
+ snatcher_worm = "Snatcher Worm"
+ starfish = "Starfish"
+ torpedo_trout = "Torpedo Trout"
+ undeadfish = "Undeadfish"
+ void_eel = "Void Eel"
+ water_grub = "Water Grub"
+ sea_sponge = "Sea Sponge"
+
+
+class DistantLandsFish:
+ void_minnow = "Void Minnow"
+ swamp_leech = "Swamp Leech"
+ purple_algae = "Purple Algae"
+ giant_horsehoe_crab = "Giant Horsehoe Crab"
+
+
+class SVEWaterItem:
+ dulse_seaweed = "Dulse Seaweed"
+
+
+class ModTrash:
+ rusty_scrap = "Scrap Rust"
+all_fish = tuple(all_fish)
\ No newline at end of file
diff --git a/worlds/stardew_valley/strings/flower_names.py b/worlds/stardew_valley/strings/flower_names.py
index a804682f1b55..7e708fc3c074 100644
--- a/worlds/stardew_valley/strings/flower_names.py
+++ b/worlds/stardew_valley/strings/flower_names.py
@@ -1,3 +1,7 @@
class Flower:
- sunflower = "Sunflower"
+ blue_jazz = "Blue Jazz"
+ fairy_rose = "Fairy Rose"
poppy = "Poppy"
+ summer_spangle = "Summer Spangle"
+ sunflower = "Sunflower"
+ tulip = "Tulip"
diff --git a/worlds/stardew_valley/strings/food_names.py b/worlds/stardew_valley/strings/food_names.py
index 55e3ef0a7bd3..03784336d19c 100644
--- a/worlds/stardew_valley/strings/food_names.py
+++ b/worlds/stardew_valley/strings/food_names.py
@@ -1,67 +1,131 @@
class Meal:
- blueberry_tart = "Blueberry Tart"
- bread = "Bread"
- fiddlehead_risotto = "Fiddlehead Risotto"
- complete_breakfast = "Complete Breakfast"
- fried_egg = "Fried Egg"
- hashbrowns = "Hashbrowns"
- pancakes = "Pancakes"
- ice_cream = "Ice Cream"
- maki_roll = "Maki Roll"
- miners_treat = "Miner's Treat"
- omelet = "Omelet"
- parsnip_soup = "Parsnip Soup"
- pink_cake = "Pink Cake"
- pizza = "Pizza"
- pumpkin_pie = "Pumpkin Pie"
- roasted_hazelnuts = "Roasted Hazelnuts"
- salad = "Salad"
- spaghetti = "Spaghetti"
- tortilla = "Tortilla"
+ banana_pudding = "Banana Pudding"
+ poi = "Poi"
+ mango_sticky_rice = "Mango Sticky Rice"
algae_soup = "Algae Soup"
artichoke_dip = "Artichoke Dip"
+ autumn_bounty = "Autumn's Bounty"
baked_fish = "Baked Fish"
bean_hotpot = "Bean Hotpot"
blackberry_cobbler = "Blackberry Cobbler"
+ blueberry_tart = "Blueberry Tart"
+ bread = "Bread"
+ bruschetta = "Bruschetta"
+ carp_surprise = "Carp Surprise"
cheese_cauliflower = "Cheese Cauliflower"
chocolate_cake = "Chocolate Cake"
chowder = "Chowder"
+ coleslaw = "Coleslaw"
+ complete_breakfast = "Complete Breakfast"
+ cookie = "Cookies"
crab_cakes = "Crab Cakes"
cranberry_candy = "Cranberry Candy"
+ cranberry_sauce = "Cranberry Sauce"
crispy_bass = "Crispy Bass"
dish_o_the_sea = "Dish O' The Sea"
eggplant_parmesan = "Eggplant Parmesan"
escargot = "Escargot"
farmer_lunch = "Farmer's Lunch"
+ fiddlehead_risotto = "Fiddlehead Risotto"
+ fish_stew = "Fish Stew"
fish_taco = "Fish Taco"
fried_calamari = "Fried Calamari"
fried_eel = "Fried Eel"
+ fried_egg = "Fried Egg"
fried_mushroom = "Fried Mushroom"
fruit_salad = "Fruit Salad"
glazed_yams = "Glazed Yams"
+ hashbrowns = "Hashbrowns"
+ ice_cream = "Ice Cream"
+ lobster_bisque = "Lobster Bisque"
+ lucky_lunch = "Lucky Lunch"
+ maki_roll = "Maki Roll"
maple_bar = "Maple Bar"
+ miners_treat = "Miner's Treat"
+ moss_soup = "Moss Soup"
+ omelet = "Omelet"
pale_broth = "Pale Broth"
+ pancakes = "Pancakes"
+ parsnip_soup = "Parsnip Soup"
pepper_poppers = "Pepper Poppers"
+ pink_cake = "Pink Cake"
+ pizza = "Pizza"
plum_pudding = "Plum Pudding"
poppyseed_muffin = "Poppyseed Muffin"
+ pumpkin_pie = "Pumpkin Pie"
+ pumpkin_soup = "Pumpkin Soup"
+ radish_salad = "Radish Salad"
red_plate = "Red Plate"
rhubarb_pie = "Rhubarb Pie"
rice_pudding = "Rice Pudding"
+ roasted_hazelnuts = "Roasted Hazelnuts"
roots_platter = "Roots Platter"
+ salad = "Salad"
salmon_dinner = "Salmon Dinner"
sashimi = "Sashimi"
+ seafoam_pudding = "Seafoam Pudding"
+ shrimp_cocktail = "Shrimp Cocktail"
+ spaghetti = "Spaghetti"
+ spicy_eel = "Spicy Eel"
+ squid_ink_ravioli = "Squid Ink Ravioli"
stir_fry = "Stir Fry"
strange_bun = "Strange Bun"
stuffing = "Stuffing"
+ super_meal = "Super Meal"
survival_burger = "Survival Burger"
+ tom_kha_soup = "Tom Kha Soup"
+ tortilla = "Tortilla"
tropical_curry = "Tropical Curry"
+ trout_soup = "Trout Soup"
vegetable_medley = "Vegetable Medley"
+ magic_rock_candy = "Magic Rock Candy"
class Beverage:
- pina_colada = "Piña Colada"
+ pina_colada = "Pina Colada"
ginger_ale = "Ginger Ale"
coffee = "Coffee"
triple_shot_espresso = "Triple Shot Espresso"
beer = "Beer"
joja_cola = "Joja Cola"
+
+
+class SVEMeal:
+ baked_berry_oatmeal = "Baked Berry Oatmeal"
+ big_bark_burger = "Big Bark Burger"
+ flower_cookie = "Flower Cookie"
+ frog_legs = "Frog Legs"
+ glazed_butterfish = "Glazed Butterfish"
+ mixed_berry_pie = "Mixed Berry Pie"
+ mushroom_berry_rice = "Mushroom Berry Rice"
+ seaweed_salad = "Seaweed Salad"
+ void_delight = "Void Delight"
+ void_salmon_sushi = "Void Salmon Sushi"
+ grampleton_orange_chicken = "Grampleton Orange Chicken"
+ stamina_capsule = "Stamina Capsule"
+
+
+class TrashyMeal:
+ grilled_cheese = "Grilled Cheese"
+ fish_casserole = "Fish Casserole"
+
+
+class ArchaeologyMeal:
+ diggers_delight = "Digger's Delight"
+ rocky_root = "Rocky Root Coffee"
+ ancient_jello = "Ancient Jello"
+
+
+class SVEBeverage:
+ sports_drink = "Sports Drink"
+
+
+class DistantLandsMeal:
+ mushroom_kebab = "Mushroom Kebab"
+ crayfish_soup = "Crayfish Soup"
+ pemmican = "Pemmican"
+ void_mint_tea = "Void Mint Tea"
+
+
+class BoardingHouseMeal:
+ special_pumpkin_soup = "Special Pumpkin Soup"
diff --git a/worlds/stardew_valley/strings/forageable_names.py b/worlds/stardew_valley/strings/forageable_names.py
index b29ff317cf77..c7dae8af3ce0 100644
--- a/worlds/stardew_valley/strings/forageable_names.py
+++ b/worlds/stardew_valley/strings/forageable_names.py
@@ -1,10 +1,26 @@
+all_edible_mushrooms = []
+
+
+def mushroom(name: str) -> str:
+ all_edible_mushrooms.append(name)
+ return name
+
+
+class Mushroom:
+ any_edible = "Any Edible Mushroom"
+ chanterelle = mushroom("Chanterelle")
+ common = mushroom("Common Mushroom")
+ morel = mushroom("Morel")
+ purple = mushroom("Purple Mushroom")
+ red = "Red Mushroom" # Not in all mushrooms, as it can't be dried
+ magma_cap = mushroom("Magma Cap")
+
+
class Forageable:
blackberry = "Blackberry"
cactus_fruit = "Cactus Fruit"
cave_carrot = "Cave Carrot"
- chanterelle = "Chanterelle"
coconut = "Coconut"
- common_mushroom = "Common Mushroom"
crocus = "Crocus"
crystal_fruit = "Crystal Fruit"
daffodil = "Daffodil"
@@ -14,9 +30,8 @@ class Forageable:
hay = "Hay"
hazelnut = "Hazelnut"
holly = "Holly"
+ journal_scrap = "Journal Scrap"
leek = "Leek"
- magma_cap = "Magma Cap"
- morel = "Morel"
secret_note = "Secret Note"
spice_berry = "Spice Berry"
sweet_pea = "Sweet Pea"
@@ -24,12 +39,33 @@ class Forageable:
wild_plum = "Wild Plum"
winter_root = "Winter Root"
dragon_tooth = "Dragon Tooth"
- red_mushroom = "Red Mushroom"
- purple_mushroom = "Purple Mushroom"
rainbow_shell = "Rainbow Shell"
salmonberry = "Salmonberry"
snow_yam = "Snow Yam"
spring_onion = "Spring Onion"
+class SVEForage:
+ ferngill_primrose = "Ferngill Primrose"
+ goldenrod = "Goldenrod"
+ winter_star_rose = "Winter Star Rose"
+ bearberry = "Bearberry"
+ poison_mushroom = "Poison Mushroom"
+ red_baneberry = "Red Baneberry"
+ conch = "Conch"
+ dewdrop_berry = "Dewdrop Berry"
+ sand_dollar = "Sand Dollar"
+ golden_ocean_flower = "Golden Ocean Flower"
+ four_leaf_clover = "Four Leaf Clover"
+ mushroom_colony = "Mushroom Colony"
+ rusty_blade = "Rusty Blade"
+ rafflesia = "Rafflesia"
+ thistle = "Thistle"
+
+
+class DistantLandsForageable:
+ brown_amanita = "Brown Amanita"
+ swamp_herb = "Swamp Herb"
+
+all_edible_mushrooms = tuple(all_edible_mushrooms)
diff --git a/worlds/stardew_valley/strings/gift_names.py b/worlds/stardew_valley/strings/gift_names.py
index 0baf31d5dbfd..9362f453cfea 100644
--- a/worlds/stardew_valley/strings/gift_names.py
+++ b/worlds/stardew_valley/strings/gift_names.py
@@ -1,6 +1,14 @@
class Gift:
bouquet = "Bouquet"
- wilted_bouquet = "Wilted Bouquet"
- pearl = "Pearl"
golden_pumpkin = "Golden Pumpkin"
mermaid_pendant = "Mermaid's Pendant"
+ movie_ticket = "Movie Ticket"
+ pearl = "Pearl"
+ tea_set = "Tea Set"
+ void_ghost_pendant = "Void Ghost Pendant"
+ wilted_bouquet = "Wilted Bouquet"
+
+
+class SVEGift:
+ blue_moon_wine = "Blue Moon Wine"
+ aged_blue_moon_wine = "Aged Blue Moon Wine"
diff --git a/worlds/stardew_valley/strings/goal_names.py b/worlds/stardew_valley/strings/goal_names.py
index da8b7d847006..601b00510428 100644
--- a/worlds/stardew_valley/strings/goal_names.py
+++ b/worlds/stardew_valley/strings/goal_names.py
@@ -7,4 +7,11 @@ class Goal:
complete_museum = "Complete the Museum Collection"
full_house = "Full House"
greatest_walnut_hunter = "Greatest Walnut Hunter"
+ protector_of_the_valley = "Protector of the Valley"
+ full_shipment = "Full Shipment"
+ gourmet_chef = "Gourmet Chef"
+ craft_master = "Craft Master"
+ legend = "Legend"
+ mystery_of_the_stardrops = "Mystery of the Stardrops"
+ allsanity = "Allsanity"
perfection = "Perfection"
diff --git a/worlds/stardew_valley/strings/ingredient_names.py b/worlds/stardew_valley/strings/ingredient_names.py
index 22271d661587..5537c7353c42 100644
--- a/worlds/stardew_valley/strings/ingredient_names.py
+++ b/worlds/stardew_valley/strings/ingredient_names.py
@@ -4,3 +4,4 @@ class Ingredient:
oil = "Oil"
rice = "Rice"
vinegar = "Vinegar"
+ qi_seasoning = "Qi Seasoning"
diff --git a/worlds/stardew_valley/strings/machine_names.py b/worlds/stardew_valley/strings/machine_names.py
index 55d6cef79401..d9e249a33594 100644
--- a/worlds/stardew_valley/strings/machine_names.py
+++ b/worlds/stardew_valley/strings/machine_names.py
@@ -1,22 +1,37 @@
class Machine:
+ dehydrator = "Dehydrator"
+ fish_smoker = "Fish Smoker"
bee_house = "Bee House"
+ bone_mill = "Bone Mill"
cask = "Cask"
charcoal_kiln = "Charcoal Kiln"
cheese_press = "Cheese Press"
+ coffee_maker = "Coffee Maker"
+ crab_pot = "Crab Pot"
+ crystalarium = "Crystalarium"
+ enricher = "Enricher"
furnace = "Furnace"
geode_crusher = "Geode Crusher"
+ mushroom_log = "Mushroom Log"
+ heavy_tapper = "Heavy Tapper"
keg = "Keg"
lightning_rod = "Lightning Rod"
loom = "Loom"
mayonnaise_machine = "Mayonnaise Machine"
oil_maker = "Oil Maker"
+ ostrich_incubator = "Ostrich Incubator"
preserves_jar = "Preserves Jar"
+ pressure_nozzle = "Pressure Nozzle"
recycling_machine = "Recycling Machine"
seed_maker = "Seed Maker"
+ slime_egg_press = "Slime Egg-Press"
+ slime_incubator = "Slime Incubator"
solar_panel = "Solar Panel"
tapper = "Tapper"
worm_bin = "Worm Bin"
- coffee_maker = "Coffee Maker"
- crab_pot = "Crab Pot"
- ostrich_incubator = "Ostrich Incubator"
+ deluxe_worm_bin = "Deluxe Worm Bin"
+ heavy_furnace = "Heavy Furnace"
+ anvil = "Anvil"
+ mini_forge = "Mini-Forge"
+ bait_maker = "Bait Maker"
diff --git a/worlds/stardew_valley/strings/material_names.py b/worlds/stardew_valley/strings/material_names.py
index 16511a5bcb97..797a42b73756 100644
--- a/worlds/stardew_valley/strings/material_names.py
+++ b/worlds/stardew_valley/strings/material_names.py
@@ -1,4 +1,5 @@
class Material:
+ moss = "Moss"
coal = "Coal"
fiber = "Fiber"
hardwood = "Hardwood"
diff --git a/worlds/stardew_valley/strings/metal_names.py b/worlds/stardew_valley/strings/metal_names.py
index 67aefc692a56..7798c06defeb 100644
--- a/worlds/stardew_valley/strings/metal_names.py
+++ b/worlds/stardew_valley/strings/metal_names.py
@@ -1,3 +1,17 @@
+all_fossils = []
+all_artifacts = []
+
+
+def fossil(name: str):
+ all_fossils.append(name)
+ return name
+
+
+def artifact(name: str):
+ all_artifacts.append(name)
+ return name
+
+
class Ore:
copper = "Copper Ore"
iron = "Iron Ore"
@@ -16,20 +30,117 @@ class MetalBar:
class Mineral:
+ petrified_slime = "Petrified Slime"
+ quartz = "Quartz"
+ earth_crystal = "Earth Crystal"
+ fire_quartz = "Fire Quartz"
+ marble = "Marble"
+ prismatic_shard = "Prismatic Shard"
+ diamond = "Diamond"
+ frozen_tear = "Frozen Tear"
aquamarine = "Aquamarine"
topaz = "Topaz"
jade = "Jade"
ruby = "Ruby"
emerald = "Emerald"
amethyst = "Amethyst"
+ tigerseye = "Tigerseye"
class Artifact:
- pass # Eventually this will be the artifact names
+ prehistoric_handaxe = "Prehistoric Handaxe"
+ dwarf_gadget = artifact("Dwarf Gadget")
+ ancient_seed = artifact("Ancient Seed")
+ glass_shards = artifact("Glass Shards")
+ rusty_cog = artifact("Rusty Cog")
+ rare_disc = artifact("Rare Disc")
+ ancient_doll = artifact("Ancient Doll")
+ ancient_drum = artifact("Ancient Drum")
+ ancient_sword = artifact("Ancient Sword")
+ arrowhead = artifact("Arrowhead")
+ bone_flute = artifact("Bone Flute")
+ chewing_stick = artifact("Chewing Stick")
+ chicken_statue = artifact("Chicken Statue")
+ anchor = artifact("Anchor")
+ chipped_amphora = artifact("Chipped Amphora")
+ dwarf_scroll_i = artifact("Dwarf Scroll I")
+ dwarf_scroll_ii = artifact("Dwarf Scroll II")
+ dwarf_scroll_iii = artifact("Dwarf Scroll III")
+ dwarf_scroll_iv = artifact("Dwarf Scroll IV")
+ dwarvish_helm = artifact("Dwarvish Helm")
+ elvish_jewelry = artifact("Elvish Jewelry")
+ golden_mask = artifact("Golden Mask")
+ golden_relic = artifact("Golden Relic")
+ ornamental_fan = artifact("Ornamental Fan")
+ prehistoric_hammer = artifact("Prehistoric Handaxe")
+ prehistoric_tool = artifact("Prehistoric Tool")
+ rusty_spoon = artifact("Rusty Spoon")
+ rusty_spur = artifact("Rusty Spur")
+ strange_doll_green = artifact("Strange Doll (Green)")
+ strange_doll = artifact("Strange Doll")
class Fossil:
+ amphibian_fossil = fossil("Amphibian Fossil")
bone_fragment = "Bone Fragment"
+ dinosaur_egg = fossil("Dinosaur Egg")
+ dried_starfish = fossil("Dried Starfish")
+ fossilized_leg = fossil("Fossilized Leg")
+ fossilized_ribs = fossil("Fossilized Ribs")
+ fossilized_skull = fossil("Fossilized Skull")
+ fossilized_spine = fossil("Fossilized Spine")
+ fossilized_tail = fossil("Fossilized Tail")
+ mummified_bat = fossil("Mummified Bat")
+ mummified_frog = fossil("Mummified Frog")
+ nautilus_fossil = fossil("Nautilus Fossil")
+ palm_fossil = fossil("Palm Fossil")
+ prehistoric_hand = fossil("Skeletal Hand")
+ prehistoric_rib = fossil("Prehistoric Rib")
+ prehistoric_scapula = fossil("Prehistoric Scapula")
+ prehistoric_skull = fossil("Prehistoric Skull")
+ prehistoric_tibia = fossil("Prehistoric Tibia")
+ prehistoric_vertebra = fossil("Prehistoric Vertebra")
+ skeletal_hand = "Skeletal Hand"
+ skeletal_tail = fossil("Skeletal Tail")
+ snake_skull = fossil("Snake Skull")
+ snake_vertebrae = fossil("Snake Vertebrae")
+ trilobite = fossil("Trilobite")
+
+
+class ModArtifact:
+ ancient_hilt = "Ancient Hilt"
+ ancient_blade = "Ancient Blade"
+ mask_piece_1 = "Mask Piece 1"
+ mask_piece_2 = "Mask Piece 2"
+ mask_piece_3 = "Mask Piece 3"
+ ancient_doll_body = "Ancient Doll Body"
+ ancient_doll_legs = "Ancient Doll Legs"
+ prismatic_shard_piece_1 = "Prismatic Shard Piece 1"
+ prismatic_shard_piece_2 = "Prismatic Shard Piece 2"
+ prismatic_shard_piece_3 = "Prismatic Shard Piece 3"
+ prismatic_shard_piece_4 = "Prismatic Shard Piece 4"
+ chipped_amphora_piece_1 = "Chipped Amphora Piece 1"
+ chipped_amphora_piece_2 = "Chipped Amphora Piece 2"
+
+class ModFossil:
+ neanderthal_limb_bones = "Neanderthal Limb Bones"
+ neanderthal_ribs = "Neanderthal Ribs"
+ neanderthal_skull = "Neanderthal Skull"
+ neanderthal_pelvis = "Neanderthal Pelvis"
+ dinosaur_tooth = "Dinosaur Tooth"
+ dinosaur_skull = "Dinosaur Skull"
+ dinosaur_claw = "Dinosaur Claw"
+ dinosaur_femur = "Dinosaur Femur"
+ dinosaur_ribs = "Dinosaur Ribs"
+ dinosaur_pelvis = "Dinosaur Pelvis"
+ dinosaur_vertebra = "Dinosaur Vertebra"
+ pterodactyl_ribs = "Pterodactyl Ribs"
+ pterodactyl_skull = "Pterodactyl Skull"
+ pterodactyl_r_wing_bone = "Pterodactyl R Wing Bone"
+ pterodactyl_l_wing_bone = "Pterodactyl L Wing Bone"
+ pterodactyl_phalange = "Pterodactyl Phalange"
+ pterodactyl_vertebra = "Pterodactyl Vertebra"
+ pterodactyl_claw = "Pterodactyl Claw"
diff --git a/worlds/stardew_valley/strings/monster_drop_names.py b/worlds/stardew_valley/strings/monster_drop_names.py
index 1b9f42429d07..df2cacf0c6aa 100644
--- a/worlds/stardew_valley/strings/monster_drop_names.py
+++ b/worlds/stardew_valley/strings/monster_drop_names.py
@@ -1,6 +1,21 @@
class Loot:
+ blue_slime_egg = "Blue Slime Egg"
+ red_slime_egg = "Red Slime Egg"
+ purple_slime_egg = "Purple Slime Egg"
+ green_slime_egg = "Green Slime Egg"
+ tiger_slime_egg = "Tiger Slime Egg"
slime = "Slime"
bug_meat = "Bug Meat"
bat_wing = "Bat Wing"
solar_essence = "Solar Essence"
void_essence = "Void Essence"
+
+
+class ModLoot:
+ void_shard = "Void Shard"
+ green_mushroom = "Green Mushroom"
+ ornate_treasure_chest = "Ornate Treasure Chest"
+ swirl_stone = "Swirl Stone"
+ void_pebble = "Void Pebble"
+ void_soul = "Void Soul"
+
diff --git a/worlds/stardew_valley/strings/monster_names.py b/worlds/stardew_valley/strings/monster_names.py
new file mode 100644
index 000000000000..e995d563f059
--- /dev/null
+++ b/worlds/stardew_valley/strings/monster_names.py
@@ -0,0 +1,67 @@
+class Monster:
+ green_slime = "Green Slime"
+ blue_slime = "Frost Jelly"
+ red_slime = "Sludge" # Yeah I know this is weird that these two are the same name
+ purple_slime = "Sludge" # Blame CA
+ yellow_slime = "Yellow Slime"
+ black_slime = "Black Slime"
+ copper_slime = "Copper Slime"
+ iron_slime = "Iron Slime"
+ tiger_slime = "Tiger Slime"
+ shadow_shaman = "Shadow Shaman"
+ shadow_shaman_dangerous = "Dangerous Shadow Shaman"
+ shadow_brute = "Shadow Brute"
+ shadow_brute_dangerous = "Dangerous Shadow Brute"
+ shadow_sniper = "Shadow Sniper"
+ bat = "Bat"
+ bat_dangerous = "Dangerous Bat"
+ frost_bat = "Frost Bat"
+ frost_bat_dangerous = "Dangerous Frost Bat"
+ lava_bat = "Lava Bat"
+ iridium_bat = "Iridium Bat"
+ skeleton = "Skeleton"
+ skeleton_dangerous = "Dangerous Skeleton"
+ skeleton_mage = "Skeleton Mage"
+ bug = "Bug"
+ bug_dangerous = "Dangerous Bug"
+ cave_fly = "Fly"
+ cave_fly_dangerous = "Dangerous Cave Fly"
+ grub = "Grub"
+ grub_dangerous = "Dangerous Grub"
+ mutant_fly = "Mutant Fly"
+ mutant_grub = "Mutant Grub"
+ armored_bug = "Armored Bug"
+ armored_bug_dangerous = "Armored Bug (dangerous)"
+ duggy = "Duggy"
+ duggy_dangerous = "Dangerous Duggy"
+ magma_duggy = "Magma Duggy"
+ dust_sprite = "Dust Sprite"
+ dust_sprite_dangerous = "Dangerous Dust Sprite"
+ rock_crab = "Rock Crab"
+ rock_crab_dangerous = "Dangerous Rock Crab"
+ lava_crab = "Lava Crab"
+ lava_crab_dangerous = "Dangerous Lava Crab"
+ iridium_crab = "Iridium Crab"
+ mummy = "Mummy"
+ mummy_dangerous = "Dangerous Mummy"
+ pepper_rex = "Pepper Rex"
+ serpent = "Serpent"
+ royal_serpent = "Royal Serpent"
+ magma_sprite = "Magma Sprite"
+ magma_sparker = "Magma Sparker"
+
+
+class MonsterCategory:
+ slime = "Slimes"
+ void_spirits = "Void Spirits"
+ bats = "Bats"
+ skeletons = "Skeletons"
+ cave_insects = "Cave Insects"
+ duggies = "Duggies"
+ dust_sprites = "Dust Sprites"
+ rock_crabs = "Rock Crabs"
+ mummies = "Mummies"
+ pepper_rex = "Pepper Rex"
+ serpents = "Serpents"
+ magma_sprites = "Magma Sprites"
+
diff --git a/worlds/stardew_valley/strings/quality_names.py b/worlds/stardew_valley/strings/quality_names.py
new file mode 100644
index 000000000000..740bb5a3efc2
--- /dev/null
+++ b/worlds/stardew_valley/strings/quality_names.py
@@ -0,0 +1,63 @@
+from typing import List
+
+
+class CropQuality:
+ basic = "Basic Crop"
+ silver = "Silver Crop"
+ gold = "Gold Crop"
+ iridium = "Iridium Crop"
+
+ @staticmethod
+ def get_highest(qualities: List[str]) -> str:
+ for quality in crop_qualities_in_desc_order:
+ if quality in qualities:
+ return quality
+ return CropQuality.basic
+
+
+class FishQuality:
+ basic = "Basic Fish"
+ silver = "Silver Fish"
+ gold = "Gold Fish"
+ iridium = "Iridium Fish"
+
+ @staticmethod
+ def get_highest(qualities: List[str]) -> str:
+ for quality in fish_qualities_in_desc_order:
+ if quality in qualities:
+ return quality
+ return FishQuality.basic
+
+
+class ForageQuality:
+ basic = "Basic Forage"
+ silver = "Silver Forage"
+ gold = "Gold Forage"
+ iridium = "Iridium Forage"
+
+ @staticmethod
+ def get_highest(qualities: List[str]) -> str:
+ for quality in forage_qualities_in_desc_order:
+ if quality in qualities:
+ return quality
+ return ForageQuality.basic
+
+
+class ArtisanQuality:
+ basic = "Basic Artisan"
+ silver = "Silver Artisan"
+ gold = "Gold Artisan"
+ iridium = "Iridium Artisan"
+
+ @staticmethod
+ def get_highest(qualities: List[str]) -> str:
+ for quality in artisan_qualities_in_desc_order:
+ if quality in qualities:
+ return quality
+ return ArtisanQuality.basic
+
+
+crop_qualities_in_desc_order = [CropQuality.iridium, CropQuality.gold, CropQuality.silver, CropQuality.basic]
+fish_qualities_in_desc_order = [FishQuality.iridium, FishQuality.gold, FishQuality.silver, FishQuality.basic]
+forage_qualities_in_desc_order = [ForageQuality.iridium, ForageQuality.gold, ForageQuality.silver, ForageQuality.basic]
+artisan_qualities_in_desc_order = [ArtisanQuality.iridium, ArtisanQuality.gold, ArtisanQuality.silver, ArtisanQuality.basic]
diff --git a/worlds/stardew_valley/strings/quest_names.py b/worlds/stardew_valley/strings/quest_names.py
index 112e40a5d84d..6370b8b56875 100644
--- a/worlds/stardew_valley/strings/quest_names.py
+++ b/worlds/stardew_valley/strings/quest_names.py
@@ -4,8 +4,10 @@ class Quest:
getting_started = "Getting Started"
to_the_beach = "To The Beach"
raising_animals = "Raising Animals"
+ feeding_animals = "Feeding Animals"
advancement = "Advancement"
archaeology = "Archaeology"
+ rat_problem = "Rat Problem"
meet_the_wizard = "Meet The Wizard"
forging_ahead = "Forging Ahead"
smelting = "Smelting"
@@ -48,10 +50,25 @@ class Quest:
dark_talisman = "Dark Talisman"
goblin_problem = "Goblin Problem"
magic_ink = "Magic Ink"
+ giant_stump = "The Giant Stump"
+
class ModQuest:
MrGinger = "Mr.Ginger's request"
AyeishaEnvelope = "Missing Envelope"
- AyeishaRing = "Lost Emerald Ring"
+ AyeishaRing = "Ayeisha's Lost Ring"
JunaCola = "Juna's Drink Request"
- JunaSpaghetti = "Juna's BFF Request"
\ No newline at end of file
+ JunaSpaghetti = "Juna's BFF Request"
+ RailroadBoulder = "The Railroad Boulder"
+ GrandpasShed = "Grandpa's Shed"
+ MarlonsBoat = "Marlon's Boat"
+ AuroraVineyard = "Aurora Vineyard"
+ MonsterCrops = "Monster Crops"
+ VoidSoul = "Void Soul Retrieval"
+ WizardInvite = "Wizard's Invite"
+ CorruptedCropsTask = "Corrupted Crops Task"
+ ANewPot = "A New Pot"
+ FancyBlanketTask = "Fancy Blanket Task"
+ WitchOrder = "Witch's order"
+ PumpkinSoup = "Pumpkin Soup"
+ HatMouseHat = "Hats for the Hat Mouse"
diff --git a/worlds/stardew_valley/strings/region_names.py b/worlds/stardew_valley/strings/region_names.py
index 9fa257114eb3..58763b6fcb80 100644
--- a/worlds/stardew_valley/strings/region_names.py
+++ b/worlds/stardew_valley/strings/region_names.py
@@ -4,12 +4,17 @@ class Region:
farm_house = "Farmhouse"
cellar = "Cellar"
farm = "Farm"
+ coop = "Coop"
+ barn = "Barn"
+ shed = "Shed"
+ slime_hutch = "Slime Hutch"
town = "Town"
beach = "Beach"
mountain = "Mountain"
forest = "Forest"
bus_stop = "Bus Stop"
backwoods = "Backwoods"
+ tunnel_entrance = "Tunnel Entrance"
bus_tunnel = "Bus Tunnel"
railroad = "Railroad"
secret_woods = "Secret Woods"
@@ -24,7 +29,6 @@ class Region:
oasis = "Oasis"
casino = "Casino"
mines = "The Mines"
- mines_dwarf_shop = "Mines Dwarf Shop"
skull_cavern_entrance = "Skull Cavern Entrance"
skull_cavern = "Skull Cavern"
sewer = "Sewer"
@@ -63,15 +67,15 @@ class Region:
skull_cavern_150 = "Skull Cavern Floor 150"
skull_cavern_175 = "Skull Cavern Floor 175"
skull_cavern_200 = "Skull Cavern Floor 200"
+ dangerous_skull_cavern = "Dangerous Skull Cavern"
hospital = "Hospital"
carpenter = "Carpenter Shop"
alex_house = "Alex's House"
elliott_house = "Elliott's House"
ranch = "Marnie's Ranch"
- traveling_cart = "Traveling Cart"
+ mastery_cave = "Mastery Cave"
farm_cave = "Farmcave"
greenhouse = "Greenhouse"
- tunnel_entrance = "Tunnel Entrance"
leah_house = "Leah's Cottage"
wizard_tower = "Wizard Tower"
wizard_basement = "Wizard Basement"
@@ -79,6 +83,7 @@ class Region:
maru_room = "Maru's Room"
sebastian_room = "Sebastian's Room"
adventurer_guild = "Adventurer's Guild"
+ adventurer_guild_bedroom = "Marlon's bedroom"
quarry = "Quarry"
quarry_mine_entrance = "Quarry Mine Entrance"
quarry_mine = "Quarry Mine"
@@ -94,6 +99,9 @@ class Region:
haley_house = "Haley's House"
sam_house = "Sam's House"
jojamart = "JojaMart"
+ abandoned_jojamart = "Abandoned JojaMart"
+ movie_theater = "Movie Theater"
+ movie_ticket_stand = "Ticket Stand"
fish_shop = "Willy's Fish Shop"
boat_tunnel = "Boat Tunnel"
tide_pools = "Tide Pools"
@@ -130,6 +138,57 @@ class Region:
mines_floor_110 = "The Mines - Floor 110"
mines_floor_115 = "The Mines - Floor 115"
mines_floor_120 = "The Mines - Floor 120"
+ dangerous_mines_20 = "Dangerous Mines - Floor 20"
+ dangerous_mines_60 = "Dangerous Mines - Floor 60"
+ dangerous_mines_100 = "Dangerous Mines - Floor 100"
+
+
+class LogicRegion:
+ mines_dwarf_shop = "Mines Dwarf Shop"
+
+ traveling_cart = "Traveling Cart"
+ traveling_cart_sunday = "Traveling Cart Sunday"
+ traveling_cart_monday = "Traveling Cart Monday"
+ traveling_cart_tuesday = "Traveling Cart Tuesday"
+ traveling_cart_wednesday = "Traveling Cart Wednesday"
+ traveling_cart_thursday = "Traveling Cart Thursday"
+ traveling_cart_friday = "Traveling Cart Friday"
+ traveling_cart_saturday = "Traveling Cart Saturday"
+
+ kitchen = "Kitchen"
+ shipping = "Shipping"
+ queen_of_sauce = "The Queen of Sauce"
+ blacksmith_copper = "Blacksmith Copper Upgrades"
+ blacksmith_iron = "Blacksmith Iron Upgrades"
+ blacksmith_gold = "Blacksmith Gold Upgrades"
+ blacksmith_iridium = "Blacksmith Iridium Upgrades"
+
+ spring_farming = "Spring Farming"
+ summer_farming = "Summer Farming"
+ fall_farming = "Fall Farming"
+ winter_farming = "Winter Farming"
+ indoor_farming = "Indoor Farming"
+ summer_or_fall_farming = "Summer or Fall Farming"
+
+ fishing = "Fishing"
+ egg_festival = "Egg Festival"
+ desert_festival = "Desert Festival"
+ trout_derby = "Trout Derby"
+ flower_dance = "Flower Dance"
+ luau = "Luau"
+ moonlight_jellies = "Dance of the Moonlight Jellies"
+ fair = "Stardew Valley Fair"
+ spirit_eve = "Spirit's Eve"
+ festival_of_ice = "Festival of Ice"
+ night_market = "Night Market"
+ winter_star = "Feast of the Winter Star"
+ squidfest = "SquidFest"
+ raccoon_daddy = "Raccoon Bundles"
+ raccoon_shop = "Raccoon Shop"
+ bookseller_1 = "Bookseller Experience Books"
+ bookseller_2 = "Bookseller Year 1 Books"
+ bookseller_3 = "Bookseller Year 3 Books"
+ forest_waterfall = "Waterfall"
class DeepWoodsRegion:
@@ -180,3 +239,90 @@ class AyeishaRegion:
class RileyRegion:
riley_house = "Riley's House"
+
+
+class SVERegion:
+ grandpas_shed = "Grandpa's Shed"
+ grandpas_shed_front_door = "Grandpa's Shed Front Door"
+ grandpas_shed_interior = "Grandpa's Shed Interior"
+ grandpas_shed_upstairs = "Grandpa's Shed Upstairs"
+ grove_outpost_warp = "Grove Outpost Warp"
+ grove_wizard_warp = "Grove Wizard Warp"
+ grove_farm_warp = "Grove Farm Warp"
+ grove_aurora_warp = "Grove Aurora Vineyard Warp"
+ grove_guild_warp = "Grove Guild Warp"
+ grove_junimo_warp = "Grove Junimo Woods Warp"
+ grove_spring_warp = "Grove Sprite Spring Warp"
+ marnies_shed = "Marnie's Shed"
+ fairhaven_farm = "Fairhaven Farm"
+ blue_moon_vineyard = "Blue Moon Vineyard"
+ sophias_house = "Sophia's House"
+ jenkins_residence = "Jenkins' Residence"
+ jenkins_cellar = "Jenkins' Cellar"
+ unclaimed_plot = "Unclaimed Plot"
+ shearwater = "Shearwater Bridge"
+ guild_summit = "Guild Summit"
+ fable_reef = "Fable Reef"
+ first_slash_guild = "First Slash Guild"
+ highlands_outside = "Highlands Outside"
+ highlands_cavern = "Highlands Cavern"
+ dwarf_prison = "Highlands Cavern Prison"
+ lances_house = "Lance's House Main"
+ lances_ladder = "Lance's House Ladder"
+ forest_west = "Forest West"
+ aurora_vineyard = "Aurora Vineyard"
+ aurora_vineyard_basement = "Aurora Vineyard Basement"
+ bear_shop = "Bear Shop"
+ sprite_spring = "Sprite Spring"
+ lost_woods = "Lost Woods"
+ junimo_woods = "Junimo Woods"
+ purple_junimo_shop = "Purple Junimo Shop"
+ enchanted_grove = "Enchanted Grove"
+ galmoran_outpost = "Galmoran Outpost"
+ badlands_entrance = "Badlands Entrance"
+ crimson_badlands = "Crimson Badlands"
+ alesia_shop = "Alesia Shop"
+ isaac_shop = "Isaac Shop"
+ summit = "Summit"
+ susans_house = "Susan's House"
+ marlon_boat = "Marlon's Boat"
+ badlands_cave = "Badlands Cave"
+ outpost_roof = "Galmoran Outpost Roof"
+ grampleton_station = "Grampleton Station"
+ grampleton_suburbs = "Grampleton Suburbs"
+ scarlett_house = "Scarlett's House"
+ first_slash_hallway = "First Slash Hallway"
+ first_slash_spare_room = "First Slash Spare Room"
+ sprite_spring_cave = "Sprite Spring Cave"
+ willy_bedroom = "Willy's Bedroom"
+ gunther_bedroom = "Gunther's Bedroom"
+ highlands_pond = "Highlands Pond"
+
+
+class AlectoRegion:
+ witch_attic = "Witch's Attic"
+
+
+class LaceyRegion:
+ hat_house = "Mouse House"
+
+
+class BoardingHouseRegion:
+ boarding_house_plateau = "Boarding House Outside"
+ boarding_house_first = "Boarding House - First Floor"
+ boarding_house_second = "Boarding House - Second Floor"
+ abandoned_mines_entrance = "Abandoned Mines Entrance"
+ abandoned_mines_1a = "Abandoned Mines - 1A"
+ abandoned_mines_1b = "Abandoned Mines - 1B"
+ abandoned_mines_2a = "Abandoned Mines - 2A"
+ abandoned_mines_2b = "Abandoned Mines - 2B"
+ abandoned_mines_3 = "Abandoned Mines - 3"
+ abandoned_mines_4 = "Abandoned Mines - 4"
+ abandoned_mines_5 = "Abandoned Mines - 5"
+ the_lost_valley = "The Lost Valley"
+ gregory_tent = "Gregory's Tent"
+ lost_valley_ruins = "Lost Valley Ruins"
+ lost_valley_minecart = "Lost Valley Minecart"
+ lost_valley_house_1 = "Lost Valley Ruins - First House"
+ lost_valley_house_2 = "Lost Valley Ruins - Second House"
+ buffalo_ranch = "Buffalo's Ranch"
diff --git a/worlds/stardew_valley/strings/season_names.py b/worlds/stardew_valley/strings/season_names.py
index 93c58fceb26c..1c4971c3f802 100644
--- a/worlds/stardew_valley/strings/season_names.py
+++ b/worlds/stardew_valley/strings/season_names.py
@@ -3,4 +3,7 @@ class Season:
summer = "Summer"
fall = "Fall"
winter = "Winter"
- progressive = "Progressive Season"
\ No newline at end of file
+ progressive = "Progressive Season"
+
+ all = (spring, summer, fall, winter)
+ not_winter = (spring, summer, fall,)
diff --git a/worlds/stardew_valley/strings/seed_names.py b/worlds/stardew_valley/strings/seed_names.py
index 080bdf854006..f2799d4e449f 100644
--- a/worlds/stardew_valley/strings/seed_names.py
+++ b/worlds/stardew_valley/strings/seed_names.py
@@ -1,9 +1,74 @@
class Seed:
- sunflower = "Sunflower Seeds"
- tomato = "Tomato Seeds"
- melon = "Melon Seeds"
- wheat = "Wheat Seeds"
+ amaranth = "Amaranth Seeds"
+ artichoke = "Artichoke Seeds"
+ bean = "Bean Starter"
+ beet = "Beet Seeds"
+ blueberry = "Blueberry Seeds"
+ bok_choy = "Bok Choy Seeds"
+ broccoli = "Broccoli Seeds"
+ cactus = "Cactus Seeds"
+ carrot = "Carrot Seeds"
+ cauliflower = "Cauliflower Seeds"
+ coffee_starter = "Coffee Bean (Starter)"
+ """This item does not really exist and should never end up being displayed.
+ It's there to patch the loop in logic because "Coffee Bean" is both the seed and the crop."""
+ coffee = "Coffee Bean"
+ corn = "Corn Seeds"
+ cranberry = "Cranberry Seeds"
+ eggplant = "Eggplant Seeds"
+ fairy = "Fairy Seeds"
garlic = "Garlic Seeds"
+ grape = "Grape Starter"
+ hops = "Hops Starter"
+ jazz = "Jazz Seeds"
+ kale = "Kale Seeds"
+ melon = "Melon Seeds"
+ mixed = "Mixed Seeds"
+ mixed_flower = "Mixed Flower Seeds"
+ parsnip = "Parsnip Seeds"
+ pepper = "Pepper Seeds"
pineapple = "Pineapple Seeds"
+ poppy = "Poppy Seeds"
+ potato = "Potato Seeds"
+ powdermelon = "Powdermelon Seeds"
+ pumpkin = "Pumpkin Seeds"
+ qi_bean = "Qi Bean"
+ radish = "Radish Seeds"
+ rare_seed = "Rare Seed"
+ red_cabbage = "Red Cabbage Seeds"
+ rhubarb = "Rhubarb Seeds"
+ rice = "Rice Shoot"
+ spangle = "Spangle Seeds"
+ starfruit = "Starfruit Seeds"
+ strawberry = "Strawberry Seeds"
+ summer_squash = "Summer Squash Seeds"
+ sunflower = "Sunflower Seeds"
taro = "Taro Tuber"
- coffee = "Coffee Bean"
+ tomato = "Tomato Seeds"
+ tulip = "Tulip Bulb"
+ wheat = "Wheat Seeds"
+ yam = "Yam Seeds"
+
+
+class TreeSeed:
+ acorn = "Acorn"
+ maple = "Maple Seed"
+ mossy = "Mossy Seed"
+ mystic = "Mystic Tree Seed"
+ pine = "Pine Cone"
+ mahogany = "Mahogany Seed"
+ mushroom = "Mushroom Tree Seed"
+
+
+class SVESeed:
+ stalk = "Stalk Seed"
+ fungus = "Fungus Seed"
+ slime = "Slime Seed"
+ void = "Void Seed"
+ shrub = "Shrub Seed"
+ ancient_fern = "Ancient Fern Seed"
+
+
+class DistantLandsSeed:
+ void_mint = "Void Mint Seeds"
+ vile_ancient_fruit = "Vile Ancient Fruit Seeds"
diff --git a/worlds/stardew_valley/strings/skill_names.py b/worlds/stardew_valley/strings/skill_names.py
index 7e7fdb798122..7f3a61f2dfcd 100644
--- a/worlds/stardew_valley/strings/skill_names.py
+++ b/worlds/stardew_valley/strings/skill_names.py
@@ -13,3 +13,8 @@ class ModSkill:
cooking = "Cooking"
magic = "Magic"
socializing = "Socializing"
+
+
+all_vanilla_skills = {Skill.farming, Skill.foraging, Skill.fishing, Skill.mining, Skill.combat}
+all_mod_skills = {ModSkill.luck, ModSkill.binning, ModSkill.archaeology, ModSkill.cooking, ModSkill.magic, ModSkill.socializing}
+all_skills = {*all_vanilla_skills, *all_mod_skills}
diff --git a/worlds/stardew_valley/strings/special_order_names.py b/worlds/stardew_valley/strings/special_order_names.py
index 04eec828c0b0..9802c01532c1 100644
--- a/worlds/stardew_valley/strings/special_order_names.py
+++ b/worlds/stardew_valley/strings/special_order_names.py
@@ -13,7 +13,7 @@ class SpecialOrder:
pierres_prime_produce = "Pierre's Prime Produce"
robins_project = "Robin's Project"
robins_resource_rush = "Robin's Resource Rush"
- juicy_bugs_wanted_yum = "Juicy Bugs Wanted!"
+ juicy_bugs_wanted = "Juicy Bugs Wanted!"
tropical_fish = "Tropical Fish"
a_curious_substance = "A Curious Substance"
prismatic_jelly = "Prismatic Jelly"
@@ -31,3 +31,10 @@ class SpecialOrder:
class ModSpecialOrder:
junas_monster_mash = "Juna's Monster Mash"
+ andys_cellar = "Andy's Cellar"
+ a_mysterious_venture = "A Mysterious Venture"
+ an_elegant_reception = "An Elegant Reception"
+ fairy_garden = "Fairy Garden"
+ homemade_fertilizer = "Homemade Fertilizer"
+ geode_order = "Geode Order"
+ dwarf_scroll = "Dwarven Scrolls"
diff --git a/worlds/stardew_valley/strings/spells.py b/worlds/stardew_valley/strings/spells.py
index fd2a515db9ff..4b246c173a49 100644
--- a/worlds/stardew_valley/strings/spells.py
+++ b/worlds/stardew_valley/strings/spells.py
@@ -1,22 +1,30 @@
+all_spells = []
+
+
+def spell(name: str) -> str:
+ all_spells.append(name)
+ return name
+
+
class MagicSpell:
- clear_debris = "Spell: Clear Debris"
- till = "Spell: Till"
- water = "Spell: Water"
- blink = "Spell: Blink"
- evac = "Spell: Evac"
- haste = "Spell: Haste"
- heal = "Spell: Heal"
- buff = "Spell: Buff"
- shockwave = "Spell: Shockwave"
- fireball = "Spell: Fireball"
- frostbite = "Spell: Frostbite"
- teleport = "Spell: Teleport"
- lantern = "Spell: Lantern"
- tendrils = "Spell: Tendrils"
- photosynthesis = "Spell: Photosynthesis"
- descend = "Spell: Descend"
- meteor = "Spell: Meteor"
- bloodmana = "Spell: Bloodmana"
- lucksteal = "Spell: Lucksteal"
- spirit = "Spell: Spirit"
- rewind = "Spell: Rewind"
+ clear_debris = spell("Spell: Clear Debris")
+ till = spell("Spell: Till")
+ water = spell("Spell: Water")
+ blink = spell("Spell: Blink")
+ evac = spell("Spell: Evac")
+ haste = spell("Spell: Haste")
+ heal = spell("Spell: Heal")
+ buff = spell("Spell: Buff")
+ shockwave = spell("Spell: Shockwave")
+ fireball = spell("Spell: Fireball")
+ frostbite = spell("Spell: Frostbolt")
+ teleport = spell("Spell: Teleport")
+ lantern = spell("Spell: Lantern")
+ tendrils = spell("Spell: Tendrils")
+ photosynthesis = spell("Spell: Photosynthesis")
+ descend = spell("Spell: Descend")
+ meteor = spell("Spell: Meteor")
+ bloodmana = spell("Spell: Bloodmana")
+ lucksteal = spell("Spell: Lucksteal")
+ spirit = spell("Spell: Spirit")
+ rewind = spell("Spell: Rewind")
diff --git a/worlds/stardew_valley/strings/tool_names.py b/worlds/stardew_valley/strings/tool_names.py
index ea8c00b9bfd2..761f50e0a9bb 100644
--- a/worlds/stardew_valley/strings/tool_names.py
+++ b/worlds/stardew_valley/strings/tool_names.py
@@ -4,6 +4,7 @@ class Tool:
hoe = "Hoe"
watering_can = "Watering Can"
trash_can = "Trash Can"
+ pan = "Pan"
fishing_rod = "Fishing Rod"
scythe = "Scythe"
golden_scythe = "Golden Scythe"
diff --git a/worlds/stardew_valley/strings/villager_names.py b/worlds/stardew_valley/strings/villager_names.py
index 5bf13ea8dd7e..7e87be64f1ea 100644
--- a/worlds/stardew_valley/strings/villager_names.py
+++ b/worlds/stardew_valley/strings/villager_names.py
@@ -46,4 +46,24 @@ class ModNPC:
riley = "Riley"
shiko = "Shiko"
wellwick = "Wellwick"
- yoba = "Yoba"
\ No newline at end of file
+ yoba = "Yoba"
+ lance = "Lance"
+ apples = "Apples"
+ claire = "Claire"
+ olivia = "Olivia"
+ sophia = "Sophia"
+ victor = "Victor"
+ andy = "Andy"
+ gunther = "Gunther"
+ martin = "Martin"
+ marlon = "Marlon"
+ morgan = "Morgan"
+ morris = "Morris"
+ scarlett = "Scarlett"
+ susan = "Susan"
+ alecto = "Alecto"
+ goblin = "Zic"
+ lacey = "Lacey"
+ gregory = "Gregory"
+ sheila = "Sheila"
+ joel = "Joel"
diff --git a/worlds/stardew_valley/strings/wallet_item_names.py b/worlds/stardew_valley/strings/wallet_item_names.py
index 31026ebbaeae..32655efe88c2 100644
--- a/worlds/stardew_valley/strings/wallet_item_names.py
+++ b/worlds/stardew_valley/strings/wallet_item_names.py
@@ -1,5 +1,11 @@
class Wallet:
+ metal_detector = "Traveling Merchant Metal Detector"
+ iridium_snake_milk = "Iridium Snake Milk"
+ bears_knowledge = "Bear's Knowledge"
+ dwarvish_translation_guide = "Dwarvish Translation Guide"
magnifying_glass = "Magnifying Glass"
rusty_key = "Rusty Key"
skull_key = "Skull Key"
dark_talisman = "Dark Talisman"
+ club_card = "Club Card"
+ mastery_of_the_five_ways = "Mastery Of The Five Ways"
diff --git a/worlds/stardew_valley/strings/weapon_names.py b/worlds/stardew_valley/strings/weapon_names.py
index 009cd6df0d6f..1c3e508cfa99 100644
--- a/worlds/stardew_valley/strings/weapon_names.py
+++ b/worlds/stardew_valley/strings/weapon_names.py
@@ -1,4 +1,3 @@
class Weapon:
slingshot = "Slingshot"
master_slingshot = "Master Slingshot"
- any_slingshot = "Any Slingshot"
diff --git a/worlds/stardew_valley/test/TestBackpack.py b/worlds/stardew_valley/test/TestBackpack.py
index f26a7c1f03d4..378c90e40a7f 100644
--- a/worlds/stardew_valley/test/TestBackpack.py
+++ b/worlds/stardew_valley/test/TestBackpack.py
@@ -5,40 +5,41 @@
class TestBackpackVanilla(SVTestBase):
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla}
- def test_no_backpack_in_pool(self):
- item_names = {item.name for item in self.multiworld.get_items()}
- self.assertNotIn("Progressive Backpack", item_names)
+ def test_no_backpack(self):
+ with self.subTest("no items"):
+ item_names = {item.name for item in self.multiworld.get_items()}
+ self.assertNotIn("Progressive Backpack", item_names)
- def test_no_backpack_locations(self):
- location_names = {location.name for location in self.multiworld.get_locations()}
- self.assertNotIn("Large Pack", location_names)
- self.assertNotIn("Deluxe Pack", location_names)
+ with self.subTest("no locations"):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertNotIn("Large Pack", location_names)
+ self.assertNotIn("Deluxe Pack", location_names)
class TestBackpackProgressive(SVTestBase):
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive}
- def test_backpack_is_in_pool_2_times(self):
- item_names = [item.name for item in self.multiworld.get_items()]
- self.assertEqual(item_names.count("Progressive Backpack"), 2)
+ def test_backpack(self):
+ with self.subTest(check="has items"):
+ item_names = [item.name for item in self.multiworld.get_items()]
+ self.assertEqual(item_names.count("Progressive Backpack"), 2)
- def test_2_backpack_locations(self):
- location_names = {location.name for location in self.multiworld.get_locations()}
- self.assertIn("Large Pack", location_names)
- self.assertIn("Deluxe Pack", location_names)
+ with self.subTest(check="has locations"):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertIn("Large Pack", location_names)
+ self.assertIn("Deluxe Pack", location_names)
-class TestBackpackEarlyProgressive(SVTestBase):
+class TestBackpackEarlyProgressive(TestBackpackProgressive):
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive}
- def test_backpack_is_in_pool_2_times(self):
- item_names = [item.name for item in self.multiworld.get_items()]
- self.assertEqual(item_names.count("Progressive Backpack"), 2)
+ @property
+ def run_default_tests(self) -> bool:
+ # EarlyProgressive is default
+ return False
- def test_2_backpack_locations(self):
- location_names = {location.name for location in self.multiworld.get_locations()}
- self.assertIn("Large Pack", location_names)
- self.assertIn("Deluxe Pack", location_names)
+ def test_backpack(self):
+ super().test_backpack()
- def test_progressive_backpack_is_in_early_pool(self):
- self.assertIn("Progressive Backpack", self.multiworld.early_items[1])
+ with self.subTest(check="is early"):
+ self.assertIn("Progressive Backpack", self.multiworld.early_items[1])
diff --git a/worlds/stardew_valley/test/TestBooksanity.py b/worlds/stardew_valley/test/TestBooksanity.py
new file mode 100644
index 000000000000..3ca52f5728c1
--- /dev/null
+++ b/worlds/stardew_valley/test/TestBooksanity.py
@@ -0,0 +1,207 @@
+from . import SVTestBase
+from ..options import ExcludeGingerIsland, Booksanity, Shipsanity
+from ..strings.ap_names.ap_option_names import OptionName
+from ..strings.book_names import Book, LostBook
+
+power_books = [Book.animal_catalogue, Book.book_of_mysteries,
+ Book.the_alleyway_buffet, Book.the_art_o_crabbing, Book.dwarvish_safety_manual,
+ Book.jewels_of_the_sea, Book.raccoon_journal, Book.woodys_secret, Book.jack_be_nimble_jack_be_thick, Book.friendship_101,
+ Book.monster_compendium, Book.mapping_cave_systems, Book.treasure_appraisal_guide, Book.way_of_the_wind_pt_1, Book.way_of_the_wind_pt_2,
+ Book.horse_the_book, Book.ol_slitherlegs, Book.price_catalogue, Book.the_diamond_hunter, ]
+
+skill_books = [Book.combat_quarterly, Book.woodcutters_weekly, Book.book_of_stars, Book.stardew_valley_almanac, Book.bait_and_bobber, Book.mining_monthly,
+ Book.queen_of_sauce_cookbook, ]
+
+lost_books = [
+ LostBook.tips_on_farming, LostBook.this_is_a_book_by_marnie, LostBook.on_foraging, LostBook.the_fisherman_act_1,
+ LostBook.how_deep_do_the_mines_go, LostBook.an_old_farmers_journal, LostBook.scarecrows, LostBook.the_secret_of_the_stardrop,
+ LostBook.journey_of_the_prairie_king_the_smash_hit_video_game, LostBook.a_study_on_diamond_yields, LostBook.brewmasters_guide,
+ LostBook.mysteries_of_the_dwarves, LostBook.highlights_from_the_book_of_yoba, LostBook.marriage_guide_for_farmers, LostBook.the_fisherman_act_ii,
+ LostBook.technology_report, LostBook.secrets_of_the_legendary_fish, LostBook.gunther_tunnel_notice, LostBook.note_from_gunther,
+ LostBook.goblins_by_m_jasper, LostBook.secret_statues_acrostics, ]
+
+lost_book = "Progressive Lost Book"
+
+
+class TestBooksanityNone(SVTestBase):
+ options = {
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false,
+ Shipsanity: Shipsanity.option_everything,
+ Booksanity: Booksanity.option_none,
+ }
+
+ def test_no_power_books_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for book in power_books:
+ with self.subTest(book):
+ self.assertNotIn(f"Read {book}", location_names)
+
+ def test_no_skill_books_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for book in skill_books:
+ with self.subTest(book):
+ self.assertNotIn(f"Read {book}", location_names)
+
+ def test_no_lost_books_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for book in lost_books:
+ with self.subTest(book):
+ self.assertNotIn(f"Read {book}", location_names)
+
+ def test_no_power_items(self):
+ item_names = {location.name for location in self.multiworld.get_items()}
+ for book in power_books:
+ with self.subTest(book):
+ self.assertNotIn(f"Power: {book}", item_names)
+ with self.subTest(lost_book):
+ self.assertNotIn(lost_book, item_names)
+
+ def test_can_ship_all_books(self):
+ self.collect_everything()
+ shipsanity_prefix = "Shipsanity: "
+ for location in self.multiworld.get_locations():
+ if not location.name.startswith(shipsanity_prefix):
+ continue
+ item_to_ship = location.name[len(shipsanity_prefix):]
+ if item_to_ship not in power_books and item_to_ship not in skill_books:
+ continue
+ with self.subTest(location.name):
+ self.assert_reach_location_true(location, self.multiworld.state)
+
+
+class TestBooksanityPowers(SVTestBase):
+ options = {
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false,
+ Shipsanity: Shipsanity.option_everything,
+ Booksanity: Booksanity.option_power,
+ }
+
+ def test_all_power_books_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for book in power_books:
+ with self.subTest(book):
+ self.assertIn(f"Read {book}", location_names)
+
+ def test_no_skill_books_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for book in skill_books:
+ with self.subTest(book):
+ self.assertNotIn(f"Read {book}", location_names)
+
+ def test_no_lost_books_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for book in lost_books:
+ with self.subTest(book):
+ self.assertNotIn(f"Read {book}", location_names)
+
+ def test_all_power_items(self):
+ item_names = {location.name for location in self.multiworld.get_items()}
+ for book in power_books:
+ with self.subTest(book):
+ self.assertIn(f"Power: {book}", item_names)
+ with self.subTest(lost_book):
+ self.assertNotIn(lost_book, item_names)
+
+ def test_can_ship_all_books(self):
+ self.collect_everything()
+ shipsanity_prefix = "Shipsanity: "
+ for location in self.multiworld.get_locations():
+ if not location.name.startswith(shipsanity_prefix):
+ continue
+ item_to_ship = location.name[len(shipsanity_prefix):]
+ if item_to_ship not in power_books and item_to_ship not in skill_books:
+ continue
+ with self.subTest(location.name):
+ self.assert_reach_location_true(location, self.multiworld.state)
+
+
+class TestBooksanityPowersAndSkills(SVTestBase):
+ options = {
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false,
+ Shipsanity: Shipsanity.option_everything,
+ Booksanity: Booksanity.option_power_skill,
+ }
+
+ def test_all_power_books_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for book in power_books:
+ with self.subTest(book):
+ self.assertIn(f"Read {book}", location_names)
+
+ def test_all_skill_books_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for book in skill_books:
+ with self.subTest(book):
+ self.assertIn(f"Read {book}", location_names)
+
+ def test_no_lost_books_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for book in lost_books:
+ with self.subTest(book):
+ self.assertNotIn(f"Read {book}", location_names)
+
+ def test_all_power_items(self):
+ item_names = {location.name for location in self.multiworld.get_items()}
+ for book in power_books:
+ with self.subTest(book):
+ self.assertIn(f"Power: {book}", item_names)
+ with self.subTest(lost_book):
+ self.assertNotIn(lost_book, item_names)
+
+ def test_can_ship_all_books(self):
+ self.collect_everything()
+ shipsanity_prefix = "Shipsanity: "
+ for location in self.multiworld.get_locations():
+ if not location.name.startswith(shipsanity_prefix):
+ continue
+ item_to_ship = location.name[len(shipsanity_prefix):]
+ if item_to_ship not in power_books and item_to_ship not in skill_books:
+ continue
+ with self.subTest(location.name):
+ self.assert_reach_location_true(location, self.multiworld.state)
+
+
+class TestBooksanityAll(SVTestBase):
+ options = {
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false,
+ Shipsanity: Shipsanity.option_everything,
+ Booksanity: Booksanity.option_all,
+ }
+
+ def test_all_power_books_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for book in power_books:
+ with self.subTest(book):
+ self.assertIn(f"Read {book}", location_names)
+
+ def test_all_skill_books_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for book in skill_books:
+ with self.subTest(book):
+ self.assertIn(f"Read {book}", location_names)
+
+ def test_all_lost_books_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for book in lost_books:
+ with self.subTest(book):
+ self.assertIn(f"Read {book}", location_names)
+
+ def test_all_power_items(self):
+ item_names = {location.name for location in self.multiworld.get_items()}
+ for book in power_books:
+ with self.subTest(book):
+ self.assertIn(f"Power: {book}", item_names)
+ with self.subTest(lost_book):
+ self.assertIn(lost_book, item_names)
+
+ def test_can_ship_all_books(self):
+ self.collect_everything()
+ shipsanity_prefix = "Shipsanity: "
+ for location in self.multiworld.get_locations():
+ if not location.name.startswith(shipsanity_prefix):
+ continue
+ item_to_ship = location.name[len(shipsanity_prefix):]
+ if item_to_ship not in power_books and item_to_ship not in skill_books:
+ continue
+ with self.subTest(location.name):
+ self.assert_reach_location_true(location, self.multiworld.state)
diff --git a/worlds/stardew_valley/test/TestBundles.py b/worlds/stardew_valley/test/TestBundles.py
index a13829eb67ea..091f39b2568e 100644
--- a/worlds/stardew_valley/test/TestBundles.py
+++ b/worlds/stardew_valley/test/TestBundles.py
@@ -1,30 +1,90 @@
import unittest
-from ..data.bundle_data import all_bundle_items, quality_crops_items
+from . import SVTestBase
+from .. import BundleRandomization
+from ..data.bundle_data import all_bundle_items_except_money, quality_crops_items_thematic, quality_foraging_items, quality_fish_items
+from ..options import BundlePlando
+from ..strings.bundle_names import BundleName
+from ..strings.crop_names import Fruit
+from ..strings.quality_names import CropQuality, ForageQuality, FishQuality
class TestBundles(unittest.TestCase):
def test_all_bundle_items_have_3_parts(self):
- for bundle_item in all_bundle_items:
- with self.subTest(bundle_item.item.name):
- self.assertGreater(len(bundle_item.item.name), 0)
- id = bundle_item.item.item_id
- self.assertGreaterEqual(id, -1)
- self.assertNotEqual(id, 0)
+ for bundle_item in all_bundle_items_except_money:
+ with self.subTest(bundle_item.item_name):
+ self.assertGreater(len(bundle_item.item_name), 0)
self.assertGreater(bundle_item.amount, 0)
- self.assertGreaterEqual(bundle_item.quality, 0)
+ self.assertTrue(bundle_item.quality)
def test_quality_crops_have_correct_amounts(self):
- for bundle_item in quality_crops_items:
- with self.subTest(bundle_item.item.name):
- name = bundle_item.item.name
- if name == "Sweet Gem Berry" or name == "Ancient Fruit":
+ for bundle_item in quality_crops_items_thematic:
+ with self.subTest(bundle_item.item_name):
+ name = bundle_item.item_name
+ if name == Fruit.sweet_gem_berry or name == Fruit.ancient_fruit:
self.assertEqual(bundle_item.amount, 1)
else:
self.assertEqual(bundle_item.amount, 5)
def test_quality_crops_have_correct_quality(self):
- for bundle_item in quality_crops_items:
- with self.subTest(bundle_item.item.name):
- self.assertEqual(bundle_item.quality, 2)
+ for bundle_item in quality_crops_items_thematic:
+ with self.subTest(bundle_item.item_name):
+ self.assertEqual(bundle_item.quality, CropQuality.gold)
+
+ def test_quality_foraging_have_correct_amounts(self):
+ for bundle_item in quality_foraging_items:
+ with self.subTest(bundle_item.item_name):
+ self.assertEqual(bundle_item.amount, 3)
+
+ def test_quality_foraging_have_correct_quality(self):
+ for bundle_item in quality_foraging_items:
+ with self.subTest(bundle_item.item_name):
+ self.assertEqual(bundle_item.quality, ForageQuality.gold)
+
+ def test_quality_fish_have_correct_amounts(self):
+ for bundle_item in quality_fish_items:
+ with self.subTest(bundle_item.item_name):
+ self.assertEqual(bundle_item.amount, 2)
+
+ def test_quality_fish_have_correct_quality(self):
+ for bundle_item in quality_fish_items:
+ with self.subTest(bundle_item.item_name):
+ self.assertEqual(bundle_item.quality, FishQuality.gold)
+
+
+class TestRemixedPlandoBundles(SVTestBase):
+ plando_bundles = {BundleName.money_2500, BundleName.money_5000, BundleName.money_10000, BundleName.gambler, BundleName.ocean_fish,
+ BundleName.lake_fish, BundleName.deep_fishing, BundleName.spring_fish, BundleName.legendary_fish, BundleName.bait}
+ options = {
+ BundleRandomization: BundleRandomization.option_remixed,
+ BundlePlando: frozenset(plando_bundles)
+ }
+
+ def test_all_plando_bundles_are_there(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for bundle_name in self.plando_bundles:
+ with self.subTest(f"{bundle_name}"):
+ self.assertIn(bundle_name, location_names)
+ self.assertNotIn(BundleName.money_25000, location_names)
+ self.assertNotIn(BundleName.carnival, location_names)
+ self.assertNotIn(BundleName.night_fish, location_names)
+ self.assertNotIn(BundleName.specialty_fish, location_names)
+ self.assertNotIn(BundleName.specific_bait, location_names)
+
+
+class TestRemixedAnywhereBundles(SVTestBase):
+ fish_bundle_names = {BundleName.spring_fish, BundleName.summer_fish, BundleName.fall_fish, BundleName.winter_fish, BundleName.ocean_fish,
+ BundleName.lake_fish, BundleName.river_fish, BundleName.night_fish, BundleName.legendary_fish, BundleName.specialty_fish,
+ BundleName.bait, BundleName.specific_bait, BundleName.crab_pot, BundleName.tackle, BundleName.quality_fish,
+ BundleName.rain_fish, BundleName.master_fisher}
+ options = {
+ BundleRandomization: BundleRandomization.option_remixed_anywhere,
+ BundlePlando: frozenset(fish_bundle_names)
+ }
+
+ def test_all_plando_bundles_are_there(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for bundle_name in self.fish_bundle_names:
+ with self.subTest(f"{bundle_name}"):
+ self.assertIn(bundle_name, location_names)
diff --git a/worlds/stardew_valley/test/TestCrops.py b/worlds/stardew_valley/test/TestCrops.py
new file mode 100644
index 000000000000..38b736367b80
--- /dev/null
+++ b/worlds/stardew_valley/test/TestCrops.py
@@ -0,0 +1,20 @@
+from . import SVTestBase
+from .. import options
+
+
+class TestCropsanityRules(SVTestBase):
+ options = {
+ options.Cropsanity.internal_name: options.Cropsanity.option_enabled
+ }
+
+ def test_need_greenhouse_for_cactus(self):
+ harvest_cactus = self.world.logic.region.can_reach_location("Harvest Cactus Fruit")
+ self.assert_rule_false(harvest_cactus, self.multiworld.state)
+
+ self.multiworld.state.collect(self.world.create_item("Cactus Seeds"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Desert Obelisk"), event=False)
+ self.assert_rule_false(harvest_cactus, self.multiworld.state)
+
+ self.multiworld.state.collect(self.world.create_item("Greenhouse"), event=False)
+ self.assert_rule_true(harvest_cactus, self.multiworld.state)
diff --git a/worlds/stardew_valley/test/TestData.py b/worlds/stardew_valley/test/TestData.py
index a77dc17319b7..86550705b917 100644
--- a/worlds/stardew_valley/test/TestData.py
+++ b/worlds/stardew_valley/test/TestData.py
@@ -2,19 +2,52 @@
from ..items import load_item_csv
from ..locations import load_location_csv
+from ..options import Mods
class TestCsvIntegrity(unittest.TestCase):
def test_items_integrity(self):
items = load_item_csv()
- for item in items:
- self.assertIsNotNone(item.code_without_offset, "Some item do not have an id."
- " Run the script `update_data.py` to generate them.")
+ with self.subTest("Test all items have an id"):
+ for item in items:
+ self.assertIsNotNone(item.code_without_offset, "Some item do not have an id."
+ " Run the script `update_data.py` to generate them.")
+ with self.subTest("Test all ids are unique"):
+ all_ids = [item.code_without_offset for item in items]
+ unique_ids = set(all_ids)
+ self.assertEqual(len(all_ids), len(unique_ids))
+
+ with self.subTest("Test all names are unique"):
+ all_names = [item.name for item in items]
+ unique_names = set(all_names)
+ self.assertEqual(len(all_names), len(unique_names))
+
+ with self.subTest("Test all mod names are valid"):
+ mod_names = {item.mod_name for item in items}
+ for mod_name in mod_names:
+ if mod_name:
+ self.assertIn(mod_name, Mods.valid_keys)
def test_locations_integrity(self):
locations = load_location_csv()
- for location in locations:
- self.assertIsNotNone(location.code_without_offset, "Some location do not have an id."
- " Run the script `update_data.py` to generate them.")
+ with self.subTest("Test all locations have an id"):
+ for location in locations:
+ self.assertIsNotNone(location.code_without_offset, "Some location do not have an id."
+ " Run the script `update_data.py` to generate them.")
+ with self.subTest("Test all ids are unique"):
+ all_ids = [location.code_without_offset for location in locations]
+ unique_ids = set(all_ids)
+ self.assertEqual(len(all_ids), len(unique_ids))
+
+ with self.subTest("Test all names are unique"):
+ all_names = [location.name for location in locations]
+ unique_names = set(all_names)
+ self.assertEqual(len(all_names), len(unique_names))
+
+ with self.subTest("Test all mod names are valid"):
+ mod_names = {location.mod_name for location in locations}
+ for mod_name in mod_names:
+ if mod_name:
+ self.assertIn(mod_name, Mods.valid_keys)
diff --git a/worlds/stardew_valley/test/TestDynamicGoals.py b/worlds/stardew_valley/test/TestDynamicGoals.py
new file mode 100644
index 000000000000..fe1bfb5f3044
--- /dev/null
+++ b/worlds/stardew_valley/test/TestDynamicGoals.py
@@ -0,0 +1,108 @@
+from typing import List, Tuple
+
+from . import SVTestBase
+from .assertion import WorldAssertMixin
+from .. import options, StardewItem
+from ..strings.ap_names.ap_weapon_names import APWeapon
+from ..strings.ap_names.transport_names import Transportation
+from ..strings.fish_names import Fish
+from ..strings.tool_names import APTool
+from ..strings.wallet_item_names import Wallet
+
+
+def collect_fishing_abilities(tester: SVTestBase):
+ for i in range(4):
+ tester.multiworld.state.collect(tester.world.create_item(APTool.fishing_rod), event=False)
+ tester.multiworld.state.collect(tester.world.create_item(APTool.pickaxe), event=False)
+ tester.multiworld.state.collect(tester.world.create_item(APTool.axe), event=False)
+ tester.multiworld.state.collect(tester.world.create_item(APWeapon.weapon), event=False)
+ for i in range(10):
+ tester.multiworld.state.collect(tester.world.create_item("Fishing Level"), event=False)
+ tester.multiworld.state.collect(tester.world.create_item("Combat Level"), event=False)
+ tester.multiworld.state.collect(tester.world.create_item("Mining Level"), event=False)
+ for i in range(17):
+ tester.multiworld.state.collect(tester.world.create_item("Progressive Mine Elevator"), event=False)
+ tester.multiworld.state.collect(tester.world.create_item("Spring"), event=False)
+ tester.multiworld.state.collect(tester.world.create_item("Summer"), event=False)
+ tester.multiworld.state.collect(tester.world.create_item("Fall"), event=False)
+ tester.multiworld.state.collect(tester.world.create_item("Winter"), event=False)
+ tester.multiworld.state.collect(tester.world.create_item(Transportation.desert_obelisk), event=False)
+ tester.multiworld.state.collect(tester.world.create_item("Railroad Boulder Removed"), event=False)
+ tester.multiworld.state.collect(tester.world.create_item("Island North Turtle"), event=False)
+ tester.multiworld.state.collect(tester.world.create_item("Island West Turtle"), event=False)
+
+
+def create_and_collect(tester: SVTestBase, item_name: str) -> StardewItem:
+ item = tester.world.create_item(item_name)
+ tester.multiworld.state.collect(item, event=False)
+ return item
+
+
+def create_and_collect_fishing_access_items(tester: SVTestBase) -> List[Tuple[StardewItem, str]]:
+ items = [(create_and_collect(tester, Wallet.dark_talisman), Fish.void_salmon),
+ (create_and_collect(tester, Wallet.rusty_key), Fish.slimejack),
+ (create_and_collect(tester, "Progressive Mine Elevator"), Fish.lava_eel),
+ (create_and_collect(tester, Transportation.island_obelisk), Fish.lionfish),
+ (create_and_collect(tester, "Island Resort"), Fish.stingray)]
+ return items
+
+
+class TestMasterAnglerNoFishsanity(WorldAssertMixin, SVTestBase):
+ options = {
+ options.Goal.internal_name: options.Goal.option_master_angler,
+ options.Fishsanity.internal_name: options.Fishsanity.option_none,
+ options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false
+ }
+
+ def test_need_all_fish_to_win(self):
+ collect_fishing_abilities(self)
+ self.assert_cannot_reach_victory(self.multiworld)
+ critical_items = create_and_collect_fishing_access_items(self)
+ self.assert_can_reach_victory(self.multiworld)
+ for item, fish in critical_items:
+ with self.subTest(f"Needed: {fish}"):
+ self.assert_item_was_necessary_for_victory(item, self.multiworld)
+
+
+class TestMasterAnglerNoFishsanityNoGingerIsland(WorldAssertMixin, SVTestBase):
+ options = {
+ options.Goal.internal_name: options.Goal.option_master_angler,
+ options.Fishsanity.internal_name: options.Fishsanity.option_none,
+ options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true
+ }
+
+ def test_need_fish_to_win(self):
+ collect_fishing_abilities(self)
+ self.assert_cannot_reach_victory(self.multiworld)
+ items = create_and_collect_fishing_access_items(self)
+ self.assert_can_reach_victory(self.multiworld)
+ unecessary_items = [(item, fish) for (item, fish) in items if fish in [Fish.lionfish, Fish.stingray]]
+ necessary_items = [(item, fish) for (item, fish) in items if (item, fish) not in unecessary_items]
+ for item, fish in necessary_items:
+ with self.subTest(f"Needed: {fish}"):
+ self.assert_item_was_necessary_for_victory(item, self.multiworld)
+ for item, fish in unecessary_items:
+ with self.subTest(f"Not Needed: {fish}"):
+ self.assert_item_was_not_necessary_for_victory(item, self.multiworld)
+
+
+class TestMasterAnglerFishsanityNoHardFish(WorldAssertMixin, SVTestBase):
+ options = {
+ options.Goal.internal_name: options.Goal.option_master_angler,
+ options.Fishsanity.internal_name: options.Fishsanity.option_exclude_hard_fish,
+ options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false
+ }
+
+ def test_need_fish_to_win(self):
+ collect_fishing_abilities(self)
+ self.assert_cannot_reach_victory(self.multiworld)
+ items = create_and_collect_fishing_access_items(self)
+ self.assert_can_reach_victory(self.multiworld)
+ unecessary_items = [(item, fish) for (item, fish) in items if fish in [Fish.void_salmon, Fish.stingray, Fish.lava_eel]]
+ necessary_items = [(item, fish) for (item, fish) in items if (item, fish) not in unecessary_items]
+ for item, fish in necessary_items:
+ with self.subTest(f"Needed: {fish}"):
+ self.assert_item_was_necessary_for_victory(item, self.multiworld)
+ for item, fish in unecessary_items:
+ with self.subTest(f"Not Needed: {fish}"):
+ self.assert_item_was_not_necessary_for_victory(item, self.multiworld)
diff --git a/worlds/stardew_valley/test/TestFarmType.py b/worlds/stardew_valley/test/TestFarmType.py
new file mode 100644
index 000000000000..f78edc3eece8
--- /dev/null
+++ b/worlds/stardew_valley/test/TestFarmType.py
@@ -0,0 +1,31 @@
+from . import SVTestBase
+from .assertion import WorldAssertMixin
+from .. import options
+
+
+class TestStartInventoryStandardFarm(WorldAssertMixin, SVTestBase):
+ options = {
+ options.FarmType.internal_name: options.FarmType.option_standard,
+ }
+
+ def test_start_inventory_progressive_coops(self):
+ start_items = dict(map(lambda x: (x.name, self.multiworld.precollected_items[self.player].count(x)), self.multiworld.precollected_items[self.player]))
+ items = dict(map(lambda x: (x.name, self.multiworld.itempool.count(x)), self.multiworld.itempool))
+ self.assertIn("Progressive Coop", items)
+ self.assertEqual(items["Progressive Coop"], 3)
+ self.assertNotIn("Progressive Coop", start_items)
+
+
+class TestStartInventoryMeadowLands(WorldAssertMixin, SVTestBase):
+ options = {
+ options.FarmType.internal_name: options.FarmType.option_meadowlands,
+ options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive,
+ }
+
+ def test_start_inventory_progressive_coops(self):
+ start_items = dict(map(lambda x: (x.name, self.multiworld.precollected_items[self.player].count(x)), self.multiworld.precollected_items[self.player]))
+ items = dict(map(lambda x: (x.name, self.multiworld.itempool.count(x)), self.multiworld.itempool))
+ self.assertIn("Progressive Coop", items)
+ self.assertEqual(items["Progressive Coop"], 2)
+ self.assertIn("Progressive Coop", start_items)
+ self.assertEqual(start_items["Progressive Coop"], 1)
diff --git a/worlds/stardew_valley/test/TestFill.py b/worlds/stardew_valley/test/TestFill.py
new file mode 100644
index 000000000000..0bfacb6ef6f5
--- /dev/null
+++ b/worlds/stardew_valley/test/TestFill.py
@@ -0,0 +1,30 @@
+from . import SVTestBase, minimal_locations_maximal_items
+from .assertion import WorldAssertMixin
+from .. import options
+from ..mods.mod_data import ModNames
+
+
+class TestMinLocationsMaxItems(WorldAssertMixin, SVTestBase):
+ options = minimal_locations_maximal_items()
+
+ def run_default_tests(self) -> bool:
+ return True
+
+ def test_fill(self):
+ self.assert_basic_checks(self.multiworld)
+
+
+class TestSpecificSeedForTroubleshooting(WorldAssertMixin, SVTestBase):
+ options = {
+ options.Fishsanity: options.Fishsanity.option_all,
+ options.Goal: options.Goal.option_master_angler,
+ options.QuestLocations: -1,
+ options.Mods: (ModNames.sve,),
+ }
+ seed = 65453499742665118161
+
+ def run_default_tests(self) -> bool:
+ return True
+
+ def test_fill(self):
+ self.assert_basic_checks(self.multiworld)
diff --git a/worlds/stardew_valley/test/TestFishsanity.py b/worlds/stardew_valley/test/TestFishsanity.py
new file mode 100644
index 000000000000..c5d87c0f8dd7
--- /dev/null
+++ b/worlds/stardew_valley/test/TestFishsanity.py
@@ -0,0 +1,405 @@
+import unittest
+from typing import ClassVar, Set
+
+from . import SVTestBase
+from .assertion import WorldAssertMixin
+from ..content.feature import fishsanity
+from ..mods.mod_data import ModNames
+from ..options import Fishsanity, ExcludeGingerIsland, Mods, SpecialOrderLocations, Goal, QuestLocations
+from ..strings.fish_names import Fish, SVEFish, DistantLandsFish
+
+pelican_town_legendary_fishes = {Fish.angler, Fish.crimsonfish, Fish.glacierfish, Fish.legend, Fish.mutant_carp, }
+pelican_town_hard_special_fishes = {Fish.lava_eel, Fish.octopus, Fish.scorpion_carp, Fish.ice_pip, Fish.super_cucumber, }
+pelican_town_medium_special_fishes = {Fish.blobfish, Fish.dorado, }
+pelican_town_hard_normal_fishes = {Fish.lingcod, Fish.pufferfish, Fish.void_salmon, }
+pelican_town_medium_normal_fishes = {
+ Fish.albacore, Fish.catfish, Fish.eel, Fish.flounder, Fish.ghostfish, Fish.goby, Fish.halibut, Fish.largemouth_bass, Fish.midnight_carp,
+ Fish.midnight_squid, Fish.pike, Fish.red_mullet, Fish.salmon, Fish.sandfish, Fish.slimejack, Fish.stonefish, Fish.spook_fish, Fish.squid, Fish.sturgeon,
+ Fish.tiger_trout, Fish.tilapia, Fish.tuna, Fish.woodskip,
+}
+pelican_town_easy_normal_fishes = {
+ Fish.anchovy, Fish.bream, Fish.bullhead, Fish.carp, Fish.chub, Fish.herring, Fish.perch, Fish.rainbow_trout, Fish.red_snapper, Fish.sardine, Fish.shad,
+ Fish.sea_cucumber, Fish.shad, Fish.smallmouth_bass, Fish.sunfish, Fish.walleye,
+}
+pelican_town_crab_pot_fishes = {
+ Fish.clam, Fish.cockle, Fish.crab, Fish.crayfish, Fish.lobster, Fish.mussel, Fish.oyster, Fish.periwinkle, Fish.shrimp, Fish.snail,
+}
+
+ginger_island_hard_fishes = {Fish.pufferfish, Fish.stingray, Fish.super_cucumber, }
+ginger_island_medium_fishes = {Fish.blue_discus, Fish.lionfish, Fish.tilapia, Fish.tuna, }
+qi_board_legendary_fishes = {Fish.ms_angler, Fish.son_of_crimsonfish, Fish.glacierfish_jr, Fish.legend_ii, Fish.radioactive_carp, }
+
+sve_pelican_town_hard_fishes = {
+ SVEFish.grass_carp, SVEFish.king_salmon, SVEFish.kittyfish, SVEFish.meteor_carp, SVEFish.puppyfish, SVEFish.radioactive_bass, SVEFish.undeadfish,
+ SVEFish.void_eel,
+}
+sve_pelican_town_medium_fishes = {
+ SVEFish.bonefish, SVEFish.butterfish, SVEFish.frog, SVEFish.goldenfish, SVEFish.snatcher_worm, SVEFish.water_grub,
+}
+sve_pelican_town_easy_fishes = {SVEFish.bull_trout, SVEFish.minnow, }
+sve_ginger_island_hard_fishes = {SVEFish.gemfish, SVEFish.shiny_lunaloo, }
+sve_ginger_island_medium_fishes = {SVEFish.daggerfish, SVEFish.lunaloo, SVEFish.starfish, SVEFish.torpedo_trout, }
+sve_ginger_island_easy_fishes = {SVEFish.baby_lunaloo, SVEFish.clownfish, SVEFish.seahorse, SVEFish.sea_sponge, }
+
+distant_lands_hard_fishes = {DistantLandsFish.giant_horsehoe_crab, }
+distant_lands_easy_fishes = {DistantLandsFish.void_minnow, DistantLandsFish.purple_algae, DistantLandsFish.swamp_leech, }
+
+
+def complete_options_with_default(options):
+ return {
+ **{
+ ExcludeGingerIsland: ExcludeGingerIsland.default,
+ Mods: Mods.default,
+ SpecialOrderLocations: SpecialOrderLocations.default,
+ },
+ **options
+ }
+
+
+class SVFishsanityTestBase(SVTestBase):
+ expected_fishes: ClassVar[Set[str]] = set()
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ if cls is SVFishsanityTestBase:
+ raise unittest.SkipTest("Base tests disabled")
+
+ super().setUpClass()
+
+ def test_fishsanity(self):
+ with self.subTest("Locations are valid"):
+ self.check_all_locations_match_expected_fishes()
+
+ def check_all_locations_match_expected_fishes(self):
+ location_fishes = {
+ name
+ for location_name in self.get_real_location_names()
+ if (name := fishsanity.extract_fish_from_location_name(location_name)) is not None
+ }
+
+ self.assertEqual(location_fishes, self.expected_fishes)
+
+
+class TestFishsanityNoneVanilla(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_none,
+ })
+
+ @property
+ def run_default_tests(self) -> bool:
+ # None is default
+ return False
+
+
+class TestFishsanityLegendaries_Vanilla(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_legendaries,
+ })
+ expected_fishes = pelican_town_legendary_fishes
+
+
+class TestFishsanityLegendaries_QiBoard(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_legendaries,
+ SpecialOrderLocations: SpecialOrderLocations.option_board_qi,
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false
+ })
+ expected_fishes = pelican_town_legendary_fishes | qi_board_legendary_fishes
+
+
+class TestFishsanitySpecial(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_special,
+ })
+ expected_fishes = pelican_town_legendary_fishes | pelican_town_hard_special_fishes | pelican_town_medium_special_fishes
+
+
+class TestFishsanityAll_Vanilla(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_all,
+ })
+ expected_fishes = (
+ pelican_town_legendary_fishes |
+ pelican_town_hard_special_fishes |
+ pelican_town_medium_special_fishes |
+ pelican_town_hard_normal_fishes |
+ pelican_town_medium_normal_fishes |
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes |
+ ginger_island_hard_fishes |
+ ginger_island_medium_fishes
+ )
+
+
+class TestFishsanityAll_ExcludeGingerIsland(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_all,
+ ExcludeGingerIsland: ExcludeGingerIsland.option_true,
+ })
+ expected_fishes = (
+ pelican_town_legendary_fishes |
+ pelican_town_hard_special_fishes |
+ pelican_town_medium_special_fishes |
+ pelican_town_hard_normal_fishes |
+ pelican_town_medium_normal_fishes |
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes
+ )
+
+
+class TestFishsanityAll_SVE(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_all,
+ Mods: ModNames.sve,
+ })
+ expected_fishes = (
+ pelican_town_legendary_fishes |
+ pelican_town_hard_special_fishes |
+ pelican_town_medium_special_fishes |
+ pelican_town_hard_normal_fishes |
+ pelican_town_medium_normal_fishes |
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes |
+ ginger_island_hard_fishes |
+ ginger_island_medium_fishes |
+ sve_pelican_town_hard_fishes |
+ sve_pelican_town_medium_fishes |
+ sve_pelican_town_easy_fishes |
+ sve_ginger_island_hard_fishes |
+ sve_ginger_island_medium_fishes |
+ sve_ginger_island_easy_fishes
+ )
+
+
+class TestFishsanityAll_ExcludeGingerIsland_SVE(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_all,
+ ExcludeGingerIsland: ExcludeGingerIsland.option_true,
+ Mods: ModNames.sve,
+ })
+ expected_fishes = (
+ pelican_town_legendary_fishes |
+ pelican_town_hard_special_fishes |
+ pelican_town_medium_special_fishes |
+ pelican_town_hard_normal_fishes |
+ pelican_town_medium_normal_fishes |
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes |
+ sve_pelican_town_hard_fishes |
+ sve_pelican_town_medium_fishes |
+ sve_pelican_town_easy_fishes
+ )
+
+
+class TestFishsanityAll_DistantLands(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_all,
+ Mods: ModNames.distant_lands,
+ })
+ expected_fishes = (
+ pelican_town_legendary_fishes |
+ pelican_town_hard_special_fishes |
+ pelican_town_medium_special_fishes |
+ pelican_town_hard_normal_fishes |
+ pelican_town_medium_normal_fishes |
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes |
+ ginger_island_hard_fishes |
+ ginger_island_medium_fishes |
+ distant_lands_hard_fishes |
+ distant_lands_easy_fishes
+ )
+
+
+class TestFishsanityAll_QiBoard(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_all,
+ SpecialOrderLocations: SpecialOrderLocations.option_board_qi,
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false
+ })
+ expected_fishes = (
+ pelican_town_legendary_fishes |
+ pelican_town_hard_special_fishes |
+ pelican_town_medium_special_fishes |
+ pelican_town_hard_normal_fishes |
+ pelican_town_medium_normal_fishes |
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes |
+ ginger_island_hard_fishes |
+ ginger_island_medium_fishes |
+ qi_board_legendary_fishes
+ )
+
+
+class TestFishsanityAll_ExcludeGingerIsland_QiBoard(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_all,
+ ExcludeGingerIsland: ExcludeGingerIsland.option_true,
+ SpecialOrderLocations: SpecialOrderLocations.option_board_qi,
+ })
+ expected_fishes = (
+ pelican_town_legendary_fishes |
+ pelican_town_hard_special_fishes |
+ pelican_town_medium_special_fishes |
+ pelican_town_hard_normal_fishes |
+ pelican_town_medium_normal_fishes |
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes
+ )
+
+
+class TestFishsanityExcludeLegendaries_Vanilla(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_exclude_legendaries,
+ })
+ expected_fishes = (
+ pelican_town_hard_special_fishes |
+ pelican_town_medium_special_fishes |
+ pelican_town_hard_normal_fishes |
+ pelican_town_medium_normal_fishes |
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes |
+ ginger_island_hard_fishes |
+ ginger_island_medium_fishes
+ )
+
+
+class TestFishsanityExcludeLegendaries_QiBoard(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_exclude_legendaries,
+ SpecialOrderLocations: SpecialOrderLocations.option_board_qi,
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false
+ })
+ expected_fishes = (
+ pelican_town_hard_special_fishes |
+ pelican_town_medium_special_fishes |
+ pelican_town_hard_normal_fishes |
+ pelican_town_medium_normal_fishes |
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes |
+ ginger_island_hard_fishes |
+ ginger_island_medium_fishes
+ )
+
+
+class TestFishsanityExcludeHardFishes_Vanilla(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_exclude_hard_fish,
+ })
+ expected_fishes = (
+ pelican_town_medium_special_fishes |
+ pelican_town_medium_normal_fishes |
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes |
+ ginger_island_medium_fishes
+ )
+
+
+class TestFishsanityExcludeHardFishes_SVE(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_exclude_hard_fish,
+ Mods: ModNames.sve,
+ })
+ expected_fishes = (
+ pelican_town_medium_special_fishes |
+ pelican_town_medium_normal_fishes |
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes |
+ ginger_island_medium_fishes |
+ sve_pelican_town_medium_fishes |
+ sve_pelican_town_easy_fishes |
+ sve_ginger_island_medium_fishes |
+ sve_ginger_island_easy_fishes
+ )
+
+
+class TestFishsanityExcludeHardFishes_DistantLands(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_exclude_hard_fish,
+ Mods: ModNames.distant_lands,
+ })
+ expected_fishes = (
+ pelican_town_medium_special_fishes |
+ pelican_town_medium_normal_fishes |
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes |
+ ginger_island_medium_fishes |
+ distant_lands_easy_fishes
+ )
+
+
+class TestFishsanityExcludeHardFishes_QiBoard(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_exclude_hard_fish,
+ SpecialOrderLocations: SpecialOrderLocations.option_board_qi,
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false
+ })
+ expected_fishes = (
+ pelican_town_medium_special_fishes |
+ pelican_town_medium_normal_fishes |
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes |
+ ginger_island_medium_fishes
+ )
+
+
+class TestFishsanityOnlyEasyFishes_Vanilla(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_only_easy_fish,
+ })
+ expected_fishes = (
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes
+ )
+
+
+class TestFishsanityOnlyEasyFishes_SVE(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_only_easy_fish,
+ Mods: ModNames.sve,
+ })
+ expected_fishes = (
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes |
+ sve_pelican_town_easy_fishes |
+ sve_ginger_island_easy_fishes
+ )
+
+
+class TestFishsanityOnlyEasyFishes_DistantLands(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_only_easy_fish,
+ Mods: ModNames.distant_lands,
+ })
+ expected_fishes = (
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes |
+ distant_lands_easy_fishes
+ )
+
+
+class TestFishsanityOnlyEasyFishes_QiBoard(SVFishsanityTestBase):
+ options = complete_options_with_default({
+ Fishsanity: Fishsanity.option_only_easy_fish,
+ SpecialOrderLocations: SpecialOrderLocations.option_board_qi,
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false
+ })
+ expected_fishes = (
+ pelican_town_easy_normal_fishes |
+ pelican_town_crab_pot_fishes
+ )
+
+
+class TestFishsanityMasterAnglerSVEWithoutQuests(WorldAssertMixin, SVTestBase):
+ options = {
+ Fishsanity: Fishsanity.option_all,
+ Goal: Goal.option_master_angler,
+ QuestLocations: -1,
+ Mods: (ModNames.sve,),
+ }
+
+ def run_default_tests(self) -> bool:
+ return True
+
+ def test_fill(self):
+ self.assert_basic_checks(self.multiworld)
diff --git a/worlds/stardew_valley/test/TestFriendsanity.py b/worlds/stardew_valley/test/TestFriendsanity.py
new file mode 100644
index 000000000000..842c0edd0980
--- /dev/null
+++ b/worlds/stardew_valley/test/TestFriendsanity.py
@@ -0,0 +1,159 @@
+import unittest
+from collections import Counter
+from typing import ClassVar, Set
+
+from . import SVTestBase
+from ..content.feature import friendsanity
+from ..options import Friendsanity, FriendsanityHeartSize
+
+all_vanilla_bachelor = {
+ "Harvey", "Elliott", "Sam", "Alex", "Shane", "Sebastian", "Emily", "Haley", "Leah", "Abigail", "Penny", "Maru"
+}
+
+all_vanilla_starting_npc = {
+ "Alex", "Elliott", "Harvey", "Sam", "Sebastian", "Shane", "Abigail", "Emily", "Haley", "Leah", "Maru", "Penny", "Caroline", "Clint", "Demetrius", "Evelyn",
+ "George", "Gus", "Jas", "Jodi", "Lewis", "Linus", "Marnie", "Pam", "Pierre", "Robin", "Vincent", "Willy", "Wizard", "Pet",
+}
+
+all_vanilla_npc = {
+ "Alex", "Elliott", "Harvey", "Sam", "Sebastian", "Shane", "Abigail", "Emily", "Haley", "Leah", "Maru", "Penny", "Caroline", "Clint", "Demetrius", "Evelyn",
+ "George", "Gus", "Jas", "Jodi", "Lewis", "Linus", "Marnie", "Pam", "Pierre", "Robin", "Vincent", "Willy", "Wizard", "Pet", "Sandy", "Dwarf", "Kent", "Leo",
+ "Krobus"
+}
+
+
+class SVFriendsanityTestBase(SVTestBase):
+ expected_npcs: ClassVar[Set[str]] = set()
+ expected_pet_heart_size: ClassVar[Set[str]] = set()
+ expected_bachelor_heart_size: ClassVar[Set[str]] = set()
+ expected_other_heart_size: ClassVar[Set[str]] = set()
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ if cls is SVFriendsanityTestBase:
+ raise unittest.SkipTest("Base tests disabled")
+
+ super().setUpClass()
+
+ def test_friendsanity(self):
+ with self.subTest("Items are valid"):
+ self.check_all_items_match_expected_npcs()
+ with self.subTest("Correct number of items"):
+ self.check_correct_number_of_items()
+ with self.subTest("Locations are valid"):
+ self.check_all_locations_match_expected_npcs()
+ with self.subTest("Locations heart size are valid"):
+ self.check_all_locations_match_heart_size()
+
+ def check_all_items_match_expected_npcs(self):
+ npc_names = {
+ name
+ for item in self.multiworld.itempool
+ if (name := friendsanity.extract_npc_from_item_name(item.name)) is not None
+ }
+
+ self.assertEqual(npc_names, self.expected_npcs)
+
+ def check_correct_number_of_items(self):
+ item_by_npc = Counter()
+ for item in self.multiworld.itempool:
+ name = friendsanity.extract_npc_from_item_name(item.name)
+ if name is None:
+ continue
+
+ item_by_npc[name] += 1
+
+ for name, count in item_by_npc.items():
+
+ if name == "Pet":
+ self.assertEqual(count, len(self.expected_pet_heart_size))
+ elif self.world.content.villagers[name].bachelor:
+ self.assertEqual(count, len(self.expected_bachelor_heart_size))
+ else:
+ self.assertEqual(count, len(self.expected_other_heart_size))
+
+ def check_all_locations_match_expected_npcs(self):
+ npc_names = {
+ name_and_heart[0]
+ for location_name in self.get_real_location_names()
+ if (name_and_heart := friendsanity.extract_npc_from_location_name(location_name))[0] is not None
+ }
+
+ self.assertEqual(npc_names, self.expected_npcs)
+
+ def check_all_locations_match_heart_size(self):
+ for location_name in self.get_real_location_names():
+ name, heart_size = friendsanity.extract_npc_from_location_name(location_name)
+ if name is None:
+ continue
+
+ if name == "Pet":
+ self.assertIn(heart_size, self.expected_pet_heart_size)
+ elif self.world.content.villagers[name].bachelor:
+ self.assertIn(heart_size, self.expected_bachelor_heart_size)
+ else:
+ self.assertIn(heart_size, self.expected_other_heart_size)
+
+
+class TestFriendsanityNone(SVFriendsanityTestBase):
+ options = {
+ Friendsanity: Friendsanity.option_none,
+ }
+
+ @property
+ def run_default_tests(self) -> bool:
+ # None is default
+ return False
+
+
+class TestFriendsanityBachelors(SVFriendsanityTestBase):
+ options = {
+ Friendsanity: Friendsanity.option_bachelors,
+ FriendsanityHeartSize: 1,
+ }
+ expected_npcs = all_vanilla_bachelor
+ expected_bachelor_heart_size = {1, 2, 3, 4, 5, 6, 7, 8}
+
+
+class TestFriendsanityStartingNpcs(SVFriendsanityTestBase):
+ options = {
+ Friendsanity: Friendsanity.option_starting_npcs,
+ FriendsanityHeartSize: 1,
+ }
+ expected_npcs = all_vanilla_starting_npc
+ expected_pet_heart_size = {1, 2, 3, 4, 5}
+ expected_bachelor_heart_size = {1, 2, 3, 4, 5, 6, 7, 8}
+ expected_other_heart_size = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
+
+
+class TestFriendsanityAllNpcs(SVFriendsanityTestBase):
+ options = {
+ Friendsanity: Friendsanity.option_all,
+ FriendsanityHeartSize: 4,
+ }
+ expected_npcs = all_vanilla_npc
+ expected_pet_heart_size = {4, 5}
+ expected_bachelor_heart_size = {4, 8}
+ expected_other_heart_size = {4, 8, 10}
+
+
+class TestFriendsanityHeartSize3(SVFriendsanityTestBase):
+ options = {
+ Friendsanity: Friendsanity.option_all_with_marriage,
+ FriendsanityHeartSize: 3,
+ }
+ expected_npcs = all_vanilla_npc
+ expected_pet_heart_size = {3, 5}
+ expected_bachelor_heart_size = {3, 6, 9, 12, 14}
+ expected_other_heart_size = {3, 6, 9, 10}
+
+
+class TestFriendsanityHeartSize5(SVFriendsanityTestBase):
+ options = {
+ Friendsanity: Friendsanity.option_all_with_marriage,
+ FriendsanityHeartSize: 5,
+ }
+ expected_npcs = all_vanilla_npc
+ expected_pet_heart_size = {5}
+ expected_bachelor_heart_size = {5, 10, 14}
+ expected_other_heart_size = {5, 10}
diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py
index a80f334d45d3..8431e6857eaf 100644
--- a/worlds/stardew_valley/test/TestGeneration.py
+++ b/worlds/stardew_valley/test/TestGeneration.py
@@ -1,25 +1,27 @@
-from BaseClasses import ItemClassification, MultiWorld
-from . import setup_solo_multiworld, SVTestBase
-from .. import locations, items, location_table, options
-from ..data.villagers_data import all_villagers_by_name, all_villagers_by_mod_by_name
-from ..items import items_by_group, Group
-from ..locations import LocationTags
-from ..mods.mod_data import ModNames
-
-
-def get_real_locations(tester: SVTestBase, multiworld: MultiWorld):
- return [location for location in multiworld.get_locations(tester.player) if not location.event]
+from typing import List
-
-def get_real_location_names(tester: SVTestBase, multiworld: MultiWorld):
- return [location.name for location in multiworld.get_locations(tester.player) if not location.event]
+from BaseClasses import ItemClassification, Item
+from . import SVTestBase
+from .. import items, location_table, options
+from ..items import Group
+from ..locations import LocationTags
+from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, ToolProgression, \
+ SkillProgression, Booksanity, Walnutsanity
+from ..strings.region_names import Region
class TestBaseItemGeneration(SVTestBase):
options = {
- options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
- options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive,
- options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
+ SeasonRandomization.internal_name: SeasonRandomization.option_progressive,
+ SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
+ Friendsanity.internal_name: Friendsanity.option_all_with_marriage,
+ Shipsanity.internal_name: Shipsanity.option_everything,
+ Chefsanity.internal_name: Chefsanity.option_all,
+ Craftsanity.internal_name: Craftsanity.option_all,
+ Booksanity.internal_name: Booksanity.option_all,
+ Walnutsanity.internal_name: Walnutsanity.preset_all,
}
def test_all_progression_items_are_added_to_the_pool(self):
@@ -27,21 +29,19 @@ def test_all_progression_items_are_added_to_the_pool(self):
# Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression
items_to_ignore = [event.name for event in items.events]
items_to_ignore.extend(item.name for item in items.all_items if item.mod_name is not None)
+ items_to_ignore.extend(deprecated.name for deprecated in items.items_by_group[Group.DEPRECATED])
items_to_ignore.extend(season.name for season in items.items_by_group[Group.SEASON])
items_to_ignore.extend(weapon.name for weapon in items.items_by_group[Group.WEAPON])
- items_to_ignore.extend(footwear.name for footwear in items.items_by_group[Group.FOOTWEAR])
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK])
- progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression
- and item.name not in items_to_ignore]
+ items_to_ignore.append("The Gateway Gazette")
+ progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression and item.name not in items_to_ignore]
for progression_item in progression_items:
with self.subTest(f"{progression_item.name}"):
self.assertIn(progression_item.name, all_created_items)
def test_creates_as_many_item_as_non_event_locations(self):
- non_event_locations = [location for location in get_real_locations(self, self.multiworld) if
- not location.event]
-
+ non_event_locations = self.get_real_locations()
self.assertEqual(len(non_event_locations), len(self.multiworld.itempool))
def test_does_not_create_deprecated_items(self):
@@ -66,9 +66,14 @@ def test_does_not_create_exactly_two_items(self):
class TestNoGingerIslandItemGeneration(SVTestBase):
options = {
- options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
- options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive,
- options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true
+ SeasonRandomization.internal_name: SeasonRandomization.option_progressive,
+ SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
+ Friendsanity.internal_name: Friendsanity.option_all_with_marriage,
+ Shipsanity.internal_name: Shipsanity.option_everything,
+ Chefsanity.internal_name: Chefsanity.option_all,
+ Craftsanity.internal_name: Craftsanity.option_all,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
+ Booksanity.internal_name: Booksanity.option_all,
}
def test_all_progression_items_except_island_are_added_to_the_pool(self):
@@ -76,12 +81,12 @@ def test_all_progression_items_except_island_are_added_to_the_pool(self):
# Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression
items_to_ignore = [event.name for event in items.events]
items_to_ignore.extend(item.name for item in items.all_items if item.mod_name is not None)
+ items_to_ignore.extend(deprecated.name for deprecated in items.items_by_group[Group.DEPRECATED])
items_to_ignore.extend(season.name for season in items.items_by_group[Group.SEASON])
items_to_ignore.extend(season.name for season in items.items_by_group[Group.WEAPON])
- items_to_ignore.extend(season.name for season in items.items_by_group[Group.FOOTWEAR])
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
- progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression
- and item.name not in items_to_ignore]
+ items_to_ignore.append("The Gateway Gazette")
+ progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression and item.name not in items_to_ignore]
for progression_item in progression_items:
with self.subTest(f"{progression_item.name}"):
if Group.GINGER_ISLAND in progression_item.groups:
@@ -90,8 +95,7 @@ def test_all_progression_items_except_island_are_added_to_the_pool(self):
self.assertIn(progression_item.name, all_created_items)
def test_creates_as_many_item_as_non_event_locations(self):
- non_event_locations = [location for location in get_real_locations(self, self.multiworld) if
- not location.event]
+ non_event_locations = self.get_real_locations()
self.assertEqual(len(non_event_locations), len(self.multiworld.itempool))
@@ -115,454 +119,606 @@ def test_does_not_create_exactly_two_items(self):
self.assertTrue(count == 0 or count == 2)
-class TestGivenProgressiveBackpack(SVTestBase):
- options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive}
-
- def test_when_generate_world_then_two_progressive_backpack_are_added(self):
- self.assertEqual(self.multiworld.itempool.count(self.world.create_item("Progressive Backpack")), 2)
-
- def test_when_generate_world_then_backpack_locations_are_added(self):
- created_locations = {location.name for location in self.multiworld.get_locations(1)}
- backpacks_exist = [location.name in created_locations
- for location in locations.locations_by_tag[LocationTags.BACKPACK]
- if location.name != "Premium Pack"]
- all_exist = all(backpacks_exist)
- self.assertTrue(all_exist)
-
-
-class TestRemixedMineRewards(SVTestBase):
- def test_when_generate_world_then_one_reward_is_added_per_chest(self):
- # assert self.world.create_item("Rusty Sword") in self.multiworld.itempool
- self.assertTrue(any(self.world.create_item(item) in self.multiworld.itempool
- for item in items_by_group[Group.MINES_FLOOR_10]))
- self.assertTrue(any(self.world.create_item(item) in self.multiworld.itempool
- for item in items_by_group[Group.MINES_FLOOR_20]))
- self.assertIn(self.world.create_item("Slingshot"), self.multiworld.itempool)
- self.assertTrue(any(self.world.create_item(item) in self.multiworld.itempool
- for item in items_by_group[Group.MINES_FLOOR_50]))
- self.assertTrue(any(self.world.create_item(item) in self.multiworld.itempool
- for item in items_by_group[Group.MINES_FLOOR_60]))
- self.assertIn(self.world.create_item("Master Slingshot"), self.multiworld.itempool)
- self.assertTrue(any(self.world.create_item(item) in self.multiworld.itempool
- for item in items_by_group[Group.MINES_FLOOR_80]))
- self.assertTrue(any(self.world.create_item(item) in self.multiworld.itempool
- for item in items_by_group[Group.MINES_FLOOR_90]))
- self.assertIn(self.world.create_item("Stardrop"), self.multiworld.itempool)
- self.assertTrue(any(self.world.create_item(item) in self.multiworld.itempool
- for item in items_by_group[Group.MINES_FLOOR_110]))
- self.assertIn(self.world.create_item("Skull Key"), self.multiworld.itempool)
-
- # This test has a 1/90,000 chance to fail... Sorry in advance
- def test_when_generate_world_then_rewards_are_not_all_vanilla(self):
- self.assertFalse(all(self.world.create_item(item) in self.multiworld.itempool
- for item in
- ["Leather Boots", "Steel Smallsword", "Tundra Boots", "Crystal Dagger", "Firewalker Boots",
- "Obsidian Edge", "Space Boots"]))
-
-
-class TestProgressiveElevator(SVTestBase):
+class TestMonstersanityNone(SVTestBase):
options = {
- options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive,
- options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
- options.SkillProgression.internal_name: options.SkillProgression.option_progressive,
+ options.Monstersanity.internal_name: options.Monstersanity.option_none,
+ # Not really necessary, but it adds more locations, so we don't have to remove useful items.
+ options.Fishsanity.internal_name: options.Fishsanity.option_all
}
- def test_given_access_to_floor_115_when_find_another_elevator_then_has_access_to_floor_120(self):
- self.collect([self.get_item_by_name("Progressive Pickaxe")] * 2)
- self.collect([self.get_item_by_name("Progressive Mine Elevator")] * 22)
- self.collect(self.multiworld.create_item("Bone Sword", self.player))
- self.collect([self.get_item_by_name("Combat Level")] * 4)
- self.collect(self.get_item_by_name("Adventurer's Guild"))
+ @property
+ def run_default_tests(self) -> bool:
+ # None is default
+ return False
+
+ def test_when_generate_world_then_5_generic_weapons_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Weapon"), 5)
- self.assertFalse(self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state))
+ def test_when_generate_world_then_zero_specific_weapons_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Sword"), 0)
+ self.assertEqual(item_pool.count("Progressive Club"), 0)
+ self.assertEqual(item_pool.count("Progressive Dagger"), 0)
- self.collect(self.get_item_by_name("Progressive Mine Elevator"))
+ def test_when_generate_world_then_2_slingshots_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Slingshot"), 2)
- self.assertTrue(self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state))
+ def test_when_generate_world_then_3_shoes_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Footwear"), 3)
- def test_given_access_to_floor_115_when_find_another_pickaxe_and_sword_then_has_access_to_floor_120(self):
- self.collect([self.get_item_by_name("Progressive Pickaxe")] * 2)
- self.collect([self.get_item_by_name("Progressive Mine Elevator")] * 22)
- self.collect(self.multiworld.create_item("Bone Sword", self.player))
- self.collect([self.get_item_by_name("Combat Level")] * 4)
- self.collect(self.get_item_by_name("Adventurer's Guild"))
- self.assertFalse(self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state))
+class TestMonstersanityGoals(SVTestBase):
+ options = {options.Monstersanity.internal_name: options.Monstersanity.option_goals}
- self.collect(self.get_item_by_name("Progressive Pickaxe"))
- self.collect(self.multiworld.create_item("Steel Falchion", self.player))
- self.collect(self.get_item_by_name("Combat Level"))
- self.collect(self.get_item_by_name("Combat Level"))
+ def test_when_generate_world_then_no_generic_weapons_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Weapon"), 0)
- self.assertTrue(self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state))
+ def test_when_generate_world_then_5_specific_weapons_of_each_type_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Sword"), 5)
+ self.assertEqual(item_pool.count("Progressive Club"), 5)
+ self.assertEqual(item_pool.count("Progressive Dagger"), 5)
+ def test_when_generate_world_then_2_slingshots_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Slingshot"), 2)
-class TestLocationGeneration(SVTestBase):
+ def test_when_generate_world_then_4_shoes_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Footwear"), 4)
+
+ def test_when_generate_world_then_all_monster_checks_are_inaccessible(self):
+ for location in self.get_real_locations():
+ if LocationTags.MONSTERSANITY not in location_table[location.name].tags:
+ continue
+ with self.subTest(location.name):
+ self.assertFalse(location.can_reach(self.multiworld.state))
- def test_all_location_created_are_in_location_table(self):
- for location in get_real_locations(self, self.multiworld):
- if not location.event:
- self.assertIn(location.name, location_table)
+class TestMonstersanityOnePerCategory(SVTestBase):
+ options = {options.Monstersanity.internal_name: options.Monstersanity.option_one_per_category}
-class TestLocationAndItemCount(SVTestBase):
+ def test_when_generate_world_then_no_generic_weapons_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Weapon"), 0)
- def test_minimal_location_maximal_items_still_valid(self):
- min_max_options = self.minimal_locations_maximal_items()
- multiworld = setup_solo_multiworld(min_max_options)
- valid_locations = get_real_locations(self, multiworld)
- self.assertGreaterEqual(len(valid_locations), len(multiworld.itempool))
+ def test_when_generate_world_then_5_specific_weapons_of_each_type_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Sword"), 5)
+ self.assertEqual(item_pool.count("Progressive Club"), 5)
+ self.assertEqual(item_pool.count("Progressive Dagger"), 5)
+
+ def test_when_generate_world_then_2_slingshots_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Slingshot"), 2)
+
+ def test_when_generate_world_then_4_shoes_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Footwear"), 4)
+
+ def test_when_generate_world_then_all_monster_checks_are_inaccessible(self):
+ for location in self.get_real_locations():
+ if LocationTags.MONSTERSANITY not in location_table[location.name].tags:
+ continue
+ with self.subTest(location.name):
+ self.assertFalse(location.can_reach(self.multiworld.state))
- def test_allsanity_without_mods_has_at_least_locations(self):
- expected_locations = 993
- allsanity_options = self.allsanity_options_without_mods()
- multiworld = setup_solo_multiworld(allsanity_options)
- number_locations = len(get_real_locations(self, multiworld))
- self.assertGreaterEqual(number_locations, expected_locations)
- print(f"Stardew Valley - Allsanity Locations without mods: {number_locations}")
- if number_locations != expected_locations:
- print(f"\tNew locations detected!"
- f"\n\tPlease update test_allsanity_without_mods_has_at_least_locations"
- f"\n\t\tExpected: {expected_locations}"
- f"\n\t\tActual: {number_locations}")
- def test_allsanity_with_mods_has_at_least_locations(self):
- expected_locations = 1245
- allsanity_options = self.allsanity_options_with_mods()
- multiworld = setup_solo_multiworld(allsanity_options)
- number_locations = len(get_real_locations(self, multiworld))
- self.assertGreaterEqual(number_locations, expected_locations)
- print(f"\nStardew Valley - Allsanity Locations with all mods: {number_locations}")
- if number_locations != expected_locations:
- print(f"\tNew locations detected!"
- f"\n\tPlease update test_allsanity_with_mods_has_at_least_locations"
- f"\n\t\tExpected: {expected_locations}"
- f"\n\t\tActual: {number_locations}")
+class TestMonstersanityProgressive(SVTestBase):
+ options = {options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals}
+ def test_when_generate_world_then_no_generic_weapons_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Weapon"), 0)
-class TestFriendsanityNone(SVTestBase):
+ def test_when_generate_world_then_5_specific_weapons_of_each_type_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Sword"), 5)
+ self.assertEqual(item_pool.count("Progressive Club"), 5)
+ self.assertEqual(item_pool.count("Progressive Dagger"), 5)
+
+ def test_when_generate_world_then_2_slingshots_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Slingshot"), 2)
+
+ def test_when_generate_world_then_4_shoes_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Footwear"), 4)
+
+ def test_when_generate_world_then_many_rings_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertIn("Hot Java Ring", item_pool)
+ self.assertIn("Wedding Ring", item_pool)
+ self.assertIn("Slime Charmer Ring", item_pool)
+
+ def test_when_generate_world_then_all_monster_checks_are_inaccessible(self):
+ for location in self.get_real_locations():
+ if LocationTags.MONSTERSANITY not in location_table[location.name].tags:
+ continue
+ with self.subTest(location.name):
+ self.assertFalse(location.can_reach(self.multiworld.state))
+
+
+class TestMonstersanitySplit(SVTestBase):
+ options = {options.Monstersanity.internal_name: options.Monstersanity.option_split_goals}
+
+ def test_when_generate_world_then_no_generic_weapons_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Weapon"), 0)
+
+ def test_when_generate_world_then_5_specific_weapons_of_each_type_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Sword"), 5)
+ self.assertEqual(item_pool.count("Progressive Club"), 5)
+ self.assertEqual(item_pool.count("Progressive Dagger"), 5)
+
+ def test_when_generate_world_then_2_slingshots_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Slingshot"), 2)
+
+ def test_when_generate_world_then_4_shoes_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertEqual(item_pool.count("Progressive Footwear"), 4)
+
+ def test_when_generate_world_then_many_rings_in_the_pool(self):
+ item_pool = [item.name for item in self.multiworld.itempool]
+ self.assertIn("Hot Java Ring", item_pool)
+ self.assertIn("Wedding Ring", item_pool)
+ self.assertIn("Slime Charmer Ring", item_pool)
+
+ def test_when_generate_world_then_all_monster_checks_are_inaccessible(self):
+ for location in self.get_real_locations():
+ if LocationTags.MONSTERSANITY not in location_table[location.name].tags:
+ continue
+ with self.subTest(location.name):
+ self.assertFalse(location.can_reach(self.multiworld.state))
+
+
+class TestProgressiveElevator(SVTestBase):
options = {
- options.Friendsanity.internal_name: options.Friendsanity.option_none,
+ options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive,
+ options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
+ options.SkillProgression.internal_name: options.SkillProgression.option_progressive,
}
- def test_no_friendsanity_items(self):
- for item in self.multiworld.itempool:
- self.assertFalse(item.name.endswith(" <3"))
+ def test_given_elevator_to_floor_105_when_find_another_elevator_then_has_access_to_floor_120(self):
+ items_for_115 = self.generate_items_for_mine_115()
+ last_elevator = self.get_item_by_name("Progressive Mine Elevator")
+ self.collect(items_for_115)
+ floor_115 = self.multiworld.get_region("The Mines - Floor 115", self.player)
+ floor_120 = self.multiworld.get_region("The Mines - Floor 120", self.player)
+
+ self.assertTrue(floor_115.can_reach(self.multiworld.state))
+ self.assertFalse(floor_120.can_reach(self.multiworld.state))
+
+ self.collect(last_elevator)
+
+ self.assertTrue(floor_120.can_reach(self.multiworld.state))
- def test_no_friendsanity_locations(self):
- for location_name in get_real_location_names(self, self.multiworld):
- self.assertFalse(location_name.startswith("Friendsanity"))
+ def generate_items_for_mine_115(self) -> List[Item]:
+ pickaxes = [self.get_item_by_name("Progressive Pickaxe")] * 2
+ elevators = [self.get_item_by_name("Progressive Mine Elevator")] * 21
+ swords = [self.get_item_by_name("Progressive Sword")] * 3
+ combat_levels = [self.get_item_by_name("Combat Level")] * 4
+ mining_levels = [self.get_item_by_name("Mining Level")] * 4
+ return [*combat_levels, *mining_levels, *elevators, *pickaxes, *swords]
+ def generate_items_for_extra_mine_levels(self, weapon_name: str) -> List[Item]:
+ last_pickaxe = self.get_item_by_name("Progressive Pickaxe")
+ last_weapon = self.multiworld.create_item(weapon_name, self.player)
+ second_last_combat_level = self.get_item_by_name("Combat Level")
+ last_combat_level = self.get_item_by_name("Combat Level")
+ second_last_mining_level = self.get_item_by_name("Mining Level")
+ last_mining_level = self.get_item_by_name("Mining Level")
+ return [last_pickaxe, last_weapon, second_last_combat_level, last_combat_level, second_last_mining_level, last_mining_level]
-class TestFriendsanityBachelors(SVTestBase):
+
+class TestSkullCavernLogic(SVTestBase):
options = {
- options.Friendsanity.internal_name: options.Friendsanity.option_bachelors,
- options.FriendsanityHeartSize.internal_name: 1,
+ options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
+ ToolProgression.internal_name: ToolProgression.option_progressive,
+ options.SkillProgression.internal_name: options.SkillProgression.option_progressive,
}
- bachelors = {"Harvey", "Elliott", "Sam", "Alex", "Shane", "Sebastian", "Emily", "Haley", "Leah", "Abigail", "Penny",
- "Maru"}
-
- def test_friendsanity_only_bachelor_items(self):
- suffix = " <3"
- for item in self.multiworld.itempool:
- if item.name.endswith(suffix):
- villager_name = item.name[:item.name.index(suffix)]
- self.assertIn(villager_name, self.bachelors)
-
- def test_friendsanity_only_bachelor_locations(self):
- prefix = "Friendsanity: "
- suffix = " <3"
- for location_name in get_real_location_names(self, self.multiworld):
- if location_name.startswith(prefix):
- name_no_prefix = location_name[len(prefix):]
- name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)]
- parts = name_trimmed.split(" ")
- name = parts[0]
- hearts = parts[1]
- self.assertIn(name, self.bachelors)
- self.assertLessEqual(int(hearts), 8)
-
-
-class TestFriendsanityStartingNpcs(SVTestBase):
+
+ def test_given_access_to_floor_115_when_find_more_tools_then_has_access_to_skull_cavern_25(self):
+ items_for_115 = self.generate_items_for_mine_115()
+ items_for_skull_50 = self.generate_items_for_skull_50()
+ items_for_skull_100 = self.generate_items_for_skull_100()
+ self.collect(items_for_115)
+ floor_115 = self.multiworld.get_region(Region.mines_floor_115, self.player)
+ skull_25 = self.multiworld.get_region(Region.skull_cavern_25, self.player)
+ skull_75 = self.multiworld.get_region(Region.skull_cavern_75, self.player)
+
+ self.assertTrue(floor_115.can_reach(self.multiworld.state))
+ self.assertFalse(skull_25.can_reach(self.multiworld.state))
+ self.assertFalse(skull_75.can_reach(self.multiworld.state))
+
+ self.remove(items_for_115)
+ self.collect(items_for_skull_50)
+
+ self.assertTrue(floor_115.can_reach(self.multiworld.state))
+ self.assertTrue(skull_25.can_reach(self.multiworld.state))
+ self.assertFalse(skull_75.can_reach(self.multiworld.state))
+
+ self.remove(items_for_skull_50)
+ self.collect(items_for_skull_100)
+
+ self.assertTrue(floor_115.can_reach(self.multiworld.state))
+ self.assertTrue(skull_25.can_reach(self.multiworld.state))
+ self.assertTrue(skull_75.can_reach(self.multiworld.state))
+
+ def generate_items_for_mine_115(self) -> List[Item]:
+ pickaxes = [self.get_item_by_name("Progressive Pickaxe")] * 2
+ swords = [self.get_item_by_name("Progressive Sword")] * 3
+ combat_levels = [self.get_item_by_name("Combat Level")] * 4
+ mining_levels = [self.get_item_by_name("Mining Level")] * 4
+ bus = self.get_item_by_name("Bus Repair")
+ skull_key = self.get_item_by_name("Skull Key")
+ return [*combat_levels, *mining_levels, *pickaxes, *swords, bus, skull_key]
+
+ def generate_items_for_skull_50(self) -> List[Item]:
+ pickaxes = [self.get_item_by_name("Progressive Pickaxe")] * 3
+ swords = [self.get_item_by_name("Progressive Sword")] * 4
+ combat_levels = [self.get_item_by_name("Combat Level")] * 6
+ mining_levels = [self.get_item_by_name("Mining Level")] * 6
+ bus = self.get_item_by_name("Bus Repair")
+ skull_key = self.get_item_by_name("Skull Key")
+ return [*combat_levels, *mining_levels, *pickaxes, *swords, bus, skull_key]
+
+ def generate_items_for_skull_100(self) -> List[Item]:
+ pickaxes = [self.get_item_by_name("Progressive Pickaxe")] * 4
+ swords = [self.get_item_by_name("Progressive Sword")] * 5
+ combat_levels = [self.get_item_by_name("Combat Level")] * 8
+ mining_levels = [self.get_item_by_name("Mining Level")] * 8
+ bus = self.get_item_by_name("Bus Repair")
+ skull_key = self.get_item_by_name("Skull Key")
+ return [*combat_levels, *mining_levels, *pickaxes, *swords, bus, skull_key]
+
+
+class TestShipsanityNone(SVTestBase):
options = {
- options.Friendsanity.internal_name: options.Friendsanity.option_starting_npcs,
- options.FriendsanityHeartSize.internal_name: 1,
+ Shipsanity.internal_name: Shipsanity.option_none
}
- excluded_npcs = {"Leo", "Krobus", "Dwarf", "Sandy", "Kent"}
-
- def test_friendsanity_only_starting_npcs_items(self):
- suffix = " <3"
- for item in self.multiworld.itempool:
- if item.name.endswith(suffix):
- villager_name = item.name[:item.name.index(suffix)]
- self.assertNotIn(villager_name, self.excluded_npcs)
-
- def test_friendsanity_only_starting_npcs_locations(self):
- prefix = "Friendsanity: "
- suffix = " <3"
- for location_name in get_real_location_names(self, self.multiworld):
- if location_name.startswith(prefix):
- name_no_prefix = location_name[len(prefix):]
- name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)]
- parts = name_trimmed.split(" ")
- name = parts[0]
- hearts = parts[1]
- self.assertNotIn(name, self.excluded_npcs)
- self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet")
- if name == "Pet":
- self.assertLessEqual(int(hearts), 5)
- elif all_villagers_by_name[name].bachelor:
- self.assertLessEqual(int(hearts), 8)
- else:
- self.assertLessEqual(int(hearts), 10)
+ def run_default_tests(self) -> bool:
+ # None is default
+ return False
-class TestFriendsanityAllNpcs(SVTestBase):
+ def test_no_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ with self.subTest(location.name):
+ self.assertFalse("Shipsanity" in location.name)
+ self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags)
+
+
+class TestShipsanityCrops(SVTestBase):
options = {
- options.Friendsanity.internal_name: options.Friendsanity.option_all,
- options.FriendsanityHeartSize.internal_name: 1,
+ Shipsanity.internal_name: Shipsanity.option_crops,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi
}
- def test_friendsanity_all_items(self):
- suffix = " <3"
- for item in self.multiworld.itempool:
- if item.name.endswith(suffix):
- villager_name = item.name[:item.name.index(suffix)]
- self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet")
-
- def test_friendsanity_all_locations(self):
- prefix = "Friendsanity: "
- suffix = " <3"
- for location_name in get_real_location_names(self, self.multiworld):
- if location_name.startswith(prefix):
- name_no_prefix = location_name[len(prefix):]
- name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)]
- parts = name_trimmed.split(" ")
- name = parts[0]
- hearts = parts[1]
- self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet")
- if name == "Pet":
- self.assertLessEqual(int(hearts), 5)
- elif all_villagers_by_name[name].bachelor:
- self.assertLessEqual(int(hearts), 8)
- else:
- self.assertLessEqual(int(hearts), 10)
+ def test_only_crop_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ with self.subTest(location.name):
+ self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags)
+ def test_include_island_crop_shipsanity_locations(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+ self.assertIn("Shipsanity: Banana", location_names)
+ self.assertIn("Shipsanity: Mango", location_names)
+ self.assertIn("Shipsanity: Pineapple", location_names)
+ self.assertIn("Shipsanity: Taro Root", location_names)
+ self.assertIn("Shipsanity: Ginger", location_names)
+ self.assertIn("Shipsanity: Magma Cap", location_names)
+ self.assertIn("Shipsanity: Qi Fruit", location_names)
-class TestFriendsanityAllNpcsExcludingGingerIsland(SVTestBase):
+
+class TestShipsanityCropsExcludeIsland(SVTestBase):
options = {
- options.Friendsanity.internal_name: options.Friendsanity.option_all,
- options.FriendsanityHeartSize.internal_name: 1,
- options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true
+ Shipsanity.internal_name: Shipsanity.option_crops,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true
}
- def test_friendsanity_all_items(self):
- suffix = " <3"
- for item in self.multiworld.itempool:
- if item.name.endswith(suffix):
- villager_name = item.name[:item.name.index(suffix)]
- self.assertNotEqual(villager_name, "Leo")
- self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet")
-
- def test_friendsanity_all_locations(self):
- prefix = "Friendsanity: "
- suffix = " <3"
- for location_name in get_real_location_names(self, self.multiworld):
- if location_name.startswith(prefix):
- name_no_prefix = location_name[len(prefix):]
- name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)]
- parts = name_trimmed.split(" ")
- name = parts[0]
- hearts = parts[1]
- self.assertNotEqual(name, "Leo")
- self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet")
- if name == "Pet":
- self.assertLessEqual(int(hearts), 5)
- elif all_villagers_by_name[name].bachelor:
- self.assertLessEqual(int(hearts), 8)
- else:
- self.assertLessEqual(int(hearts), 10)
+ def test_only_crop_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ with self.subTest(location.name):
+ self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags)
+ def test_only_mainland_crop_shipsanity_locations(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+ self.assertNotIn("Shipsanity: Banana", location_names)
+ self.assertNotIn("Shipsanity: Mango", location_names)
+ self.assertNotIn("Shipsanity: Pineapple", location_names)
+ self.assertNotIn("Shipsanity: Taro Root", location_names)
+ self.assertNotIn("Shipsanity: Ginger", location_names)
+ self.assertNotIn("Shipsanity: Magma Cap", location_names)
+ self.assertNotIn("Shipsanity: Qi Fruit", location_names)
-class TestFriendsanityAllNpcsWithMarriage(SVTestBase):
+
+class TestShipsanityCropsNoQiCropWithoutSpecialOrders(SVTestBase):
options = {
- options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
- options.FriendsanityHeartSize.internal_name: 1,
+ Shipsanity.internal_name: Shipsanity.option_crops,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board
}
- def test_friendsanity_all_with_marriage_items(self):
- suffix = " <3"
- for item in self.multiworld.itempool:
- if item.name.endswith(suffix):
- villager_name = item.name[:item.name.index(suffix)]
- self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet")
-
- def test_friendsanity_all_with_marriage_locations(self):
- prefix = "Friendsanity: "
- suffix = " <3"
- for location_name in get_real_location_names(self, self.multiworld):
- if location_name.startswith(prefix):
- name_no_prefix = location_name[len(prefix):]
- name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)]
- parts = name_trimmed.split(" ")
- name = parts[0]
- hearts = parts[1]
- self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet")
- if name == "Pet":
- self.assertLessEqual(int(hearts), 5)
- elif all_villagers_by_name[name].bachelor:
- self.assertLessEqual(int(hearts), 14)
- else:
- self.assertLessEqual(int(hearts), 10)
+ def test_only_crop_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ with self.subTest(location.name):
+ self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags)
+ def test_island_crops_without_qi_fruit_shipsanity_locations(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+ self.assertIn("Shipsanity: Banana", location_names)
+ self.assertIn("Shipsanity: Mango", location_names)
+ self.assertIn("Shipsanity: Pineapple", location_names)
+ self.assertIn("Shipsanity: Taro Root", location_names)
+ self.assertIn("Shipsanity: Ginger", location_names)
+ self.assertIn("Shipsanity: Magma Cap", location_names)
+ self.assertNotIn("Shipsanity: Qi Fruit", location_names)
-class TestFriendsanityAllNpcsWithMarriageHeartSize2(SVTestBase):
+
+class TestShipsanityFish(SVTestBase):
options = {
- options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
- options.FriendsanityHeartSize.internal_name: 2,
+ Shipsanity.internal_name: Shipsanity.option_fish,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi
}
- def test_friendsanity_all_with_marriage_items(self):
- suffix = " <3"
- item_names = [item.name for item in self.multiworld.itempool]
- for villager_name in all_villagers_by_mod_by_name[ModNames.vanilla]:
- heart_item_name = f"{villager_name}{suffix}"
- number_heart_items = item_names.count(heart_item_name)
- if all_villagers_by_name[villager_name].bachelor:
- self.assertEqual(number_heart_items, 7)
- else:
- self.assertEqual(number_heart_items, 5)
- self.assertEqual(item_names.count("Pet <3"), 3)
-
- def test_friendsanity_all_with_marriage_locations(self):
- prefix = "Friendsanity: "
- suffix = " <3"
- for location_name in get_real_location_names(self, self.multiworld):
- if not location_name.startswith(prefix):
- continue
- name_no_prefix = location_name[len(prefix):]
- name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)]
- parts = name_trimmed.split(" ")
- name = parts[0]
- hearts = int(parts[1])
- self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet")
- if name == "Pet":
- self.assertTrue(hearts == 2 or hearts == 4 or hearts == 5)
- elif all_villagers_by_name[name].bachelor:
- self.assertTrue(hearts == 2 or hearts == 4 or hearts == 6 or hearts == 8 or hearts == 10 or hearts == 12 or hearts == 14)
- else:
- self.assertTrue(hearts == 2 or hearts == 4 or hearts == 6 or hearts == 8 or hearts == 10)
-
-
-class TestFriendsanityAllNpcsWithMarriageHeartSize3(SVTestBase):
+ def test_only_fish_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ with self.subTest(location.name):
+ self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
+
+ def test_include_island_fish_shipsanity_locations(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+ self.assertIn("Shipsanity: Blue Discus", location_names)
+ self.assertIn("Shipsanity: Lionfish", location_names)
+ self.assertIn("Shipsanity: Stingray", location_names)
+ self.assertIn("Shipsanity: Glacierfish Jr.", location_names)
+ self.assertIn("Shipsanity: Legend II", location_names)
+ self.assertIn("Shipsanity: Ms. Angler", location_names)
+ self.assertIn("Shipsanity: Radioactive Carp", location_names)
+ self.assertIn("Shipsanity: Son of Crimsonfish", location_names)
+
+
+class TestShipsanityFishExcludeIsland(SVTestBase):
options = {
- options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
- options.FriendsanityHeartSize.internal_name: 3,
+ Shipsanity.internal_name: Shipsanity.option_fish,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true
}
- def test_friendsanity_all_with_marriage_items(self):
- suffix = " <3"
- item_names = [item.name for item in self.multiworld.itempool]
- for villager_name in all_villagers_by_mod_by_name[ModNames.vanilla]:
- heart_item_name = f"{villager_name}{suffix}"
- number_heart_items = item_names.count(heart_item_name)
- if all_villagers_by_name[villager_name].bachelor:
- self.assertEqual(number_heart_items, 5)
- else:
- self.assertEqual(number_heart_items, 4)
- self.assertEqual(item_names.count("Pet <3"), 2)
-
- def test_friendsanity_all_with_marriage_locations(self):
- prefix = "Friendsanity: "
- suffix = " <3"
- for location_name in get_real_location_names(self, self.multiworld):
- if not location_name.startswith(prefix):
- continue
- name_no_prefix = location_name[len(prefix):]
- name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)]
- parts = name_trimmed.split(" ")
- name = parts[0]
- hearts = int(parts[1])
- self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet")
- if name == "Pet":
- self.assertTrue(hearts == 3 or hearts == 5)
- elif all_villagers_by_name[name].bachelor:
- self.assertTrue(hearts == 3 or hearts == 6 or hearts == 9 or hearts == 12 or hearts == 14)
- else:
- self.assertTrue(hearts == 3 or hearts == 6 or hearts == 9 or hearts == 10)
-
-
-class TestFriendsanityAllNpcsWithMarriageHeartSize4(SVTestBase):
+ def test_only_fish_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ with self.subTest(location.name):
+ self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
+
+ def test_exclude_island_fish_shipsanity_locations(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+ self.assertNotIn("Shipsanity: Blue Discus", location_names)
+ self.assertNotIn("Shipsanity: Lionfish", location_names)
+ self.assertNotIn("Shipsanity: Stingray", location_names)
+ self.assertNotIn("Shipsanity: Glacierfish Jr.", location_names)
+ self.assertNotIn("Shipsanity: Legend II", location_names)
+ self.assertNotIn("Shipsanity: Ms. Angler", location_names)
+ self.assertNotIn("Shipsanity: Radioactive Carp", location_names)
+ self.assertNotIn("Shipsanity: Son of Crimsonfish", location_names)
+
+
+class TestShipsanityFishExcludeQiOrders(SVTestBase):
options = {
- options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
- options.FriendsanityHeartSize.internal_name: 4,
+ Shipsanity.internal_name: Shipsanity.option_fish,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board
}
- def test_friendsanity_all_with_marriage_items(self):
- suffix = " <3"
- item_names = [item.name for item in self.multiworld.itempool]
- for villager_name in all_villagers_by_mod_by_name[ModNames.vanilla]:
- heart_item_name = f"{villager_name}{suffix}"
- number_heart_items = item_names.count(heart_item_name)
- if all_villagers_by_name[villager_name].bachelor:
- self.assertEqual(number_heart_items, 4)
- else:
- self.assertEqual(number_heart_items, 3)
- self.assertEqual(item_names.count("Pet <3"), 2)
-
- def test_friendsanity_all_with_marriage_locations(self):
- prefix = "Friendsanity: "
- suffix = " <3"
- for location_name in get_real_location_names(self, self.multiworld):
- if not location_name.startswith(prefix):
- continue
- name_no_prefix = location_name[len(prefix):]
- name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)]
- parts = name_trimmed.split(" ")
- name = parts[0]
- hearts = int(parts[1])
- self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet")
- if name == "Pet":
- self.assertTrue(hearts == 4 or hearts == 5)
- elif all_villagers_by_name[name].bachelor:
- self.assertTrue(hearts == 4 or hearts == 8 or hearts == 12 or hearts == 14)
- else:
- self.assertTrue(hearts == 4 or hearts == 8 or hearts == 10)
-
-
-class TestFriendsanityAllNpcsWithMarriageHeartSize5(SVTestBase):
+ def test_only_fish_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ with self.subTest(location.name):
+ self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
+
+ def test_include_island_fish_no_extended_family_shipsanity_locations(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+ self.assertIn("Shipsanity: Blue Discus", location_names)
+ self.assertIn("Shipsanity: Lionfish", location_names)
+ self.assertIn("Shipsanity: Stingray", location_names)
+ self.assertNotIn("Shipsanity: Glacierfish Jr.", location_names)
+ self.assertNotIn("Shipsanity: Legend II", location_names)
+ self.assertNotIn("Shipsanity: Ms. Angler", location_names)
+ self.assertNotIn("Shipsanity: Radioactive Carp", location_names)
+ self.assertNotIn("Shipsanity: Son of Crimsonfish", location_names)
+
+
+class TestShipsanityFullShipment(SVTestBase):
options = {
- options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
- options.FriendsanityHeartSize.internal_name: 5,
+ Shipsanity.internal_name: Shipsanity.option_full_shipment,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi
}
- def test_friendsanity_all_with_marriage_items(self):
- suffix = " <3"
- item_names = [item.name for item in self.multiworld.itempool]
- for villager_name in all_villagers_by_mod_by_name[ModNames.vanilla]:
- heart_item_name = f"{villager_name}{suffix}"
- number_heart_items = item_names.count(heart_item_name)
- if all_villagers_by_name[villager_name].bachelor:
- self.assertEqual(number_heart_items, 3)
- else:
- self.assertEqual(number_heart_items, 2)
- self.assertEqual(item_names.count("Pet <3"), 1)
-
- def test_friendsanity_all_with_marriage_locations(self):
- prefix = "Friendsanity: "
- suffix = " <3"
- for location_name in get_real_location_names(self, self.multiworld):
- if not location_name.startswith(prefix):
- continue
- name_no_prefix = location_name[len(prefix):]
- name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)]
- parts = name_trimmed.split(" ")
- name = parts[0]
- hearts = int(parts[1])
- self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet")
- if name == "Pet":
- self.assertTrue(hearts == 5)
- elif all_villagers_by_name[name].bachelor:
- self.assertTrue(hearts == 5 or hearts == 10 or hearts == 14)
- else:
- self.assertTrue(hearts == 5 or hearts == 10)
+ def test_only_full_shipment_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ with self.subTest(location.name):
+ self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags)
+ self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
+
+ def test_include_island_items_shipsanity_locations(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+ self.assertIn("Shipsanity: Cinder Shard", location_names)
+ self.assertIn("Shipsanity: Bone Fragment", location_names)
+ self.assertIn("Shipsanity: Radioactive Ore", location_names)
+ self.assertIn("Shipsanity: Radioactive Bar", location_names)
+ self.assertIn("Shipsanity: Banana", location_names)
+ self.assertIn("Shipsanity: Mango", location_names)
+ self.assertIn("Shipsanity: Pineapple", location_names)
+ self.assertIn("Shipsanity: Taro Root", location_names)
+ self.assertIn("Shipsanity: Ginger", location_names)
+ self.assertIn("Shipsanity: Magma Cap", location_names)
+
+
+class TestShipsanityFullShipmentExcludeIsland(SVTestBase):
+ options = {
+ Shipsanity.internal_name: Shipsanity.option_full_shipment,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true
+ }
+
+ def test_only_full_shipment_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ with self.subTest(location.name):
+ self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags)
+ self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
+
+ def test_exclude_island_items_shipsanity_locations(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+ self.assertNotIn("Shipsanity: Cinder Shard", location_names)
+ self.assertNotIn("Shipsanity: Radioactive Ore", location_names)
+ self.assertNotIn("Shipsanity: Radioactive Bar", location_names)
+ self.assertNotIn("Shipsanity: Banana", location_names)
+ self.assertNotIn("Shipsanity: Mango", location_names)
+ self.assertNotIn("Shipsanity: Pineapple", location_names)
+ self.assertNotIn("Shipsanity: Taro Root", location_names)
+ self.assertNotIn("Shipsanity: Ginger", location_names)
+ self.assertNotIn("Shipsanity: Magma Cap", location_names)
+
+
+class TestShipsanityFullShipmentExcludeQiBoard(SVTestBase):
+ options = {
+ Shipsanity.internal_name: Shipsanity.option_full_shipment,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla
+ }
+
+ def test_only_full_shipment_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ with self.subTest(location.name):
+ self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags)
+ self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
+
+ def test_exclude_qi_board_items_shipsanity_locations(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+ self.assertIn("Shipsanity: Cinder Shard", location_names)
+ self.assertIn("Shipsanity: Bone Fragment", location_names)
+ self.assertNotIn("Shipsanity: Radioactive Ore", location_names)
+ self.assertNotIn("Shipsanity: Radioactive Bar", location_names)
+ self.assertIn("Shipsanity: Banana", location_names)
+ self.assertIn("Shipsanity: Mango", location_names)
+ self.assertIn("Shipsanity: Pineapple", location_names)
+ self.assertIn("Shipsanity: Taro Root", location_names)
+ self.assertIn("Shipsanity: Ginger", location_names)
+ self.assertIn("Shipsanity: Magma Cap", location_names)
+
+
+class TestShipsanityFullShipmentWithFish(SVTestBase):
+ options = {
+ Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi
+ }
+
+ def test_only_full_shipment_and_fish_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ with self.subTest(location.name):
+ self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or
+ LocationTags.SHIPSANITY_FISH in location_table[location.name].tags)
+
+ def test_include_island_items_shipsanity_locations(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+ self.assertIn("Shipsanity: Cinder Shard", location_names)
+ self.assertIn("Shipsanity: Bone Fragment", location_names)
+ self.assertIn("Shipsanity: Radioactive Ore", location_names)
+ self.assertIn("Shipsanity: Radioactive Bar", location_names)
+ self.assertIn("Shipsanity: Banana", location_names)
+ self.assertIn("Shipsanity: Mango", location_names)
+ self.assertIn("Shipsanity: Pineapple", location_names)
+ self.assertIn("Shipsanity: Taro Root", location_names)
+ self.assertIn("Shipsanity: Ginger", location_names)
+ self.assertIn("Shipsanity: Magma Cap", location_names)
+ self.assertIn("Shipsanity: Blue Discus", location_names)
+ self.assertIn("Shipsanity: Lionfish", location_names)
+ self.assertIn("Shipsanity: Stingray", location_names)
+ self.assertIn("Shipsanity: Glacierfish Jr.", location_names)
+ self.assertIn("Shipsanity: Legend II", location_names)
+ self.assertIn("Shipsanity: Ms. Angler", location_names)
+ self.assertIn("Shipsanity: Radioactive Carp", location_names)
+ self.assertIn("Shipsanity: Son of Crimsonfish", location_names)
+
+
+class TestShipsanityFullShipmentWithFishExcludeIsland(SVTestBase):
+ options = {
+ Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true
+ }
+
+ def test_only_full_shipment_and_fish_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ with self.subTest(location.name):
+ self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or
+ LocationTags.SHIPSANITY_FISH in location_table[location.name].tags)
+
+ def test_exclude_island_items_shipsanity_locations(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+ self.assertNotIn("Shipsanity: Cinder Shard", location_names)
+ self.assertNotIn("Shipsanity: Radioactive Ore", location_names)
+ self.assertNotIn("Shipsanity: Radioactive Bar", location_names)
+ self.assertNotIn("Shipsanity: Banana", location_names)
+ self.assertNotIn("Shipsanity: Mango", location_names)
+ self.assertNotIn("Shipsanity: Pineapple", location_names)
+ self.assertNotIn("Shipsanity: Taro Root", location_names)
+ self.assertNotIn("Shipsanity: Ginger", location_names)
+ self.assertNotIn("Shipsanity: Magma Cap", location_names)
+ self.assertNotIn("Shipsanity: Blue Discus", location_names)
+ self.assertNotIn("Shipsanity: Lionfish", location_names)
+ self.assertNotIn("Shipsanity: Stingray", location_names)
+ self.assertNotIn("Shipsanity: Glacierfish Jr.", location_names)
+ self.assertNotIn("Shipsanity: Legend II", location_names)
+ self.assertNotIn("Shipsanity: Ms. Angler", location_names)
+ self.assertNotIn("Shipsanity: Radioactive Carp", location_names)
+ self.assertNotIn("Shipsanity: Son of Crimsonfish", location_names)
+
+
+class TestShipsanityFullShipmentWithFishExcludeQiBoard(SVTestBase):
+ options = {
+ Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish,
+ SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board
+ }
+
+ def test_only_full_shipment_and_fish_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ with self.subTest(location.name):
+ self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or
+ LocationTags.SHIPSANITY_FISH in location_table[location.name].tags)
+
+ def test_exclude_qi_board_items_shipsanity_locations(self):
+ location_names = [location.name for location in self.multiworld.get_locations(self.player)]
+ self.assertIn("Shipsanity: Cinder Shard", location_names)
+ self.assertIn("Shipsanity: Bone Fragment", location_names)
+ self.assertNotIn("Shipsanity: Radioactive Ore", location_names)
+ self.assertNotIn("Shipsanity: Radioactive Bar", location_names)
+ self.assertIn("Shipsanity: Banana", location_names)
+ self.assertIn("Shipsanity: Mango", location_names)
+ self.assertIn("Shipsanity: Pineapple", location_names)
+ self.assertIn("Shipsanity: Taro Root", location_names)
+ self.assertIn("Shipsanity: Ginger", location_names)
+ self.assertIn("Shipsanity: Magma Cap", location_names)
+ self.assertIn("Shipsanity: Blue Discus", location_names)
+ self.assertIn("Shipsanity: Lionfish", location_names)
+ self.assertIn("Shipsanity: Stingray", location_names)
+ self.assertNotIn("Shipsanity: Glacierfish Jr.", location_names)
+ self.assertNotIn("Shipsanity: Legend II", location_names)
+ self.assertNotIn("Shipsanity: Ms. Angler", location_names)
+ self.assertNotIn("Shipsanity: Radioactive Carp", location_names)
+ self.assertNotIn("Shipsanity: Son of Crimsonfish", location_names)
diff --git a/worlds/stardew_valley/test/TestItemLink.py b/worlds/stardew_valley/test/TestItemLink.py
new file mode 100644
index 000000000000..39bf553cab2d
--- /dev/null
+++ b/worlds/stardew_valley/test/TestItemLink.py
@@ -0,0 +1,100 @@
+from . import SVTestBase
+from .. import options, item_table, Group
+
+max_iterations = 2000
+
+
+class TestItemLinksEverythingIncluded(SVTestBase):
+ options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
+ options.TrapItems.internal_name: options.TrapItems.option_medium}
+
+ def test_filler_of_all_types_generated(self):
+ max_number_filler = 114
+ filler_generated = []
+ at_least_one_trap = False
+ at_least_one_island = False
+ for i in range(0, max_iterations):
+ filler = self.multiworld.worlds[1].get_filler_item_name()
+ if filler in filler_generated:
+ continue
+ filler_generated.append(filler)
+ self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups)
+ self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups)
+ if Group.TRAP in item_table[filler].groups:
+ at_least_one_trap = True
+ if Group.GINGER_ISLAND in item_table[filler].groups:
+ at_least_one_island = True
+ if len(filler_generated) >= max_number_filler:
+ break
+ self.assertTrue(at_least_one_trap)
+ self.assertTrue(at_least_one_island)
+ self.assertGreaterEqual(len(filler_generated), max_number_filler)
+
+
+class TestItemLinksNoIsland(SVTestBase):
+ options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
+ options.TrapItems.internal_name: options.TrapItems.option_medium}
+
+ def test_filler_has_no_island_but_has_traps(self):
+ max_number_filler = 109
+ filler_generated = []
+ at_least_one_trap = False
+ for i in range(0, max_iterations):
+ filler = self.multiworld.worlds[1].get_filler_item_name()
+ if filler in filler_generated:
+ continue
+ filler_generated.append(filler)
+ self.assertNotIn(Group.GINGER_ISLAND, item_table[filler].groups)
+ self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups)
+ self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups)
+ if Group.TRAP in item_table[filler].groups:
+ at_least_one_trap = True
+ if len(filler_generated) >= max_number_filler:
+ break
+ self.assertTrue(at_least_one_trap)
+ self.assertGreaterEqual(len(filler_generated), max_number_filler)
+
+
+class TestItemLinksNoTraps(SVTestBase):
+ options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
+ options.TrapItems.internal_name: options.TrapItems.option_no_traps}
+
+ def test_filler_has_no_traps_but_has_island(self):
+ max_number_filler = 99
+ filler_generated = []
+ at_least_one_island = False
+ for i in range(0, max_iterations):
+ filler = self.multiworld.worlds[1].get_filler_item_name()
+ if filler in filler_generated:
+ continue
+ filler_generated.append(filler)
+ self.assertNotIn(Group.TRAP, item_table[filler].groups)
+ self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups)
+ self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups)
+ if Group.GINGER_ISLAND in item_table[filler].groups:
+ at_least_one_island = True
+ if len(filler_generated) >= max_number_filler:
+ break
+ self.assertTrue(at_least_one_island)
+ self.assertGreaterEqual(len(filler_generated), max_number_filler)
+
+
+class TestItemLinksNoTrapsAndIsland(SVTestBase):
+ options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
+ options.TrapItems.internal_name: options.TrapItems.option_no_traps}
+
+ def test_filler_generated_without_island_or_traps(self):
+ max_number_filler = 94
+ filler_generated = []
+ for i in range(0, max_iterations):
+ filler = self.multiworld.worlds[1].get_filler_item_name()
+ if filler in filler_generated:
+ continue
+ filler_generated.append(filler)
+ self.assertNotIn(Group.GINGER_ISLAND, item_table[filler].groups)
+ self.assertNotIn(Group.TRAP, item_table[filler].groups)
+ self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups)
+ self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups)
+ if len(filler_generated) >= max_number_filler:
+ break
+ self.assertGreaterEqual(len(filler_generated), max_number_filler)
diff --git a/worlds/stardew_valley/test/TestItems.py b/worlds/stardew_valley/test/TestItems.py
index 7f48f9347c10..671fe6387258 100644
--- a/worlds/stardew_valley/test/TestItems.py
+++ b/worlds/stardew_valley/test/TestItems.py
@@ -1,17 +1,14 @@
-import itertools
-import math
-import sys
-import unittest
-import random
-from typing import Set
-
-from BaseClasses import ItemClassification, MultiWorld
-from . import setup_solo_multiworld, SVTestBase
-from .. import ItemData, StardewValleyWorld
+from BaseClasses import MultiWorld, get_seed
+from . import setup_solo_multiworld, SVTestCase, allsanity_no_mods_6_x_x, get_minsanity_options, solo_multiworld
+from .. import StardewValleyWorld
from ..items import Group, item_table
+from ..options import Friendsanity, SeasonRandomization, Museumsanity, Shipsanity, Goal
+from ..strings.wallet_item_names import Wallet
+all_seasons = ["Spring", "Summer", "Fall", "Winter"]
-class TestItems(SVTestBase):
+
+class TestItems(SVTestCase):
def test_can_create_item_of_resource_pack(self):
item_name = "Resource Pack: 500 Money"
@@ -33,20 +30,94 @@ def test_items_table_footprint_is_between_717000_and_737000(self):
def test_babies_come_in_all_shapes_and_sizes(self):
baby_permutations = set()
+ options = {Friendsanity.internal_name: Friendsanity.option_bachelors}
for attempt_number in range(50):
if len(baby_permutations) >= 4:
print(f"Already got all 4 baby permutations, breaking early [{attempt_number} generations]")
break
- seed = random.randrange(sys.maxsize)
- multiworld = setup_solo_multiworld(seed=seed)
+ seed = get_seed()
+ multiworld = setup_solo_multiworld(options, seed=seed, _cache={})
baby_items = [item for item in multiworld.get_items() if "Baby" in item.name]
self.assertEqual(len(baby_items), 2)
baby_permutations.add(f"{baby_items[0]} - {baby_items[1]}")
self.assertEqual(len(baby_permutations), 4)
def test_correct_number_of_stardrops(self):
- seed = random.randrange(sys.maxsize)
- allsanity_options = self.allsanity_options_without_mods()
- multiworld = setup_solo_multiworld(allsanity_options, seed=seed)
- stardrop_items = [item for item in multiworld.get_items() if "Stardrop" in item.name]
- self.assertEqual(len(stardrop_items), 5)
+ allsanity_options = allsanity_no_mods_6_x_x()
+ with solo_multiworld(allsanity_options) as (multiworld, _):
+ stardrop_items = [item for item in multiworld.get_items() if item.name == "Stardrop"]
+ self.assertEqual(len(stardrop_items), 7)
+
+ def test_no_duplicate_rings(self):
+ allsanity_options = allsanity_no_mods_6_x_x()
+ with solo_multiworld(allsanity_options) as (multiworld, _):
+ ring_items = [item.name for item in multiworld.get_items() if Group.RING in item_table[item.name].groups]
+ self.assertEqual(len(ring_items), len(set(ring_items)))
+
+ def test_can_start_in_any_season(self):
+ starting_seasons_rolled = set()
+ options = {SeasonRandomization.internal_name: SeasonRandomization.option_randomized}
+ for attempt_number in range(50):
+ if len(starting_seasons_rolled) >= 4:
+ print(f"Already got all 4 starting seasons, breaking early [{attempt_number} generations]")
+ break
+ seed = get_seed()
+ multiworld = setup_solo_multiworld(options, seed=seed, _cache={})
+ starting_season_items = [item for item in multiworld.precollected_items[1] if item.name in all_seasons]
+ season_items = [item for item in multiworld.get_items() if item.name in all_seasons]
+ self.assertEqual(len(starting_season_items), 1)
+ self.assertEqual(len(season_items), 3)
+ starting_seasons_rolled.add(f"{starting_season_items[0]}")
+ self.assertEqual(len(starting_seasons_rolled), 4)
+
+
+class TestMetalDetectors(SVTestCase):
+ def test_minsanity_1_metal_detector(self):
+ options = get_minsanity_options()
+ with solo_multiworld(options) as (multiworld, _):
+ items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector]
+ self.assertEqual(len(items), 1)
+
+ def test_museumsanity_2_metal_detector(self):
+ options = get_minsanity_options().copy()
+ options[Museumsanity.internal_name] = Museumsanity.option_all
+ with solo_multiworld(options) as (multiworld, _):
+ items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector]
+ self.assertEqual(len(items), 2)
+
+ def test_shipsanity_full_shipment_1_metal_detector(self):
+ options = get_minsanity_options().copy()
+ options[Shipsanity.internal_name] = Shipsanity.option_full_shipment
+ with solo_multiworld(options) as (multiworld, _):
+ items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector]
+ self.assertEqual(len(items), 1)
+
+ def test_shipsanity_everything_2_metal_detector(self):
+ options = get_minsanity_options().copy()
+ options[Shipsanity.internal_name] = Shipsanity.option_everything
+ with solo_multiworld(options) as (multiworld, _):
+ items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector]
+ self.assertEqual(len(items), 2)
+
+ def test_complete_collection_2_metal_detector(self):
+ options = get_minsanity_options().copy()
+ options[Goal.internal_name] = Goal.option_complete_collection
+ with solo_multiworld(options) as (multiworld, _):
+ items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector]
+ self.assertEqual(len(items), 2)
+
+ def test_perfection_2_metal_detector(self):
+ options = get_minsanity_options().copy()
+ options[Goal.internal_name] = Goal.option_perfection
+ with solo_multiworld(options) as (multiworld, _):
+ items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector]
+ self.assertEqual(len(items), 2)
+
+ def test_maxsanity_4_metal_detector(self):
+ options = get_minsanity_options().copy()
+ options[Museumsanity.internal_name] = Museumsanity.option_all
+ options[Shipsanity.internal_name] = Shipsanity.option_everything
+ options[Goal.internal_name] = Goal.option_perfection
+ with solo_multiworld(options) as (multiworld, _):
+ items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector]
+ self.assertEqual(len(items), 4)
diff --git a/worlds/stardew_valley/test/TestLogic.py b/worlds/stardew_valley/test/TestLogic.py
index 7965d05b57be..65f7352a5e36 100644
--- a/worlds/stardew_valley/test/TestLogic.py
+++ b/worlds/stardew_valley/test/TestLogic.py
@@ -1,13 +1,13 @@
+import typing
import unittest
+from unittest import TestCase, SkipTest
-from test.general import setup_solo_multiworld
-from .. import StardewValleyWorld, StardewLocation
-from ..data.bundle_data import BundleItem, all_bundle_items_except_money
-from ..stardew_rule import MISSING_ITEM, False_
-
-multi_world = setup_solo_multiworld(StardewValleyWorld)
-world = multi_world.worlds[1]
-logic = world.logic
+from BaseClasses import MultiWorld
+from . import RuleAssertMixin, setup_solo_multiworld, allsanity_mods_6_x_x, minimal_locations_maximal_items
+from .. import StardewValleyWorld
+from ..data.bundle_data import all_bundle_items_except_money
+from ..logic.logic import StardewLogic
+from ..options import BundleRandomization
def collect_all(mw):
@@ -15,88 +15,91 @@ def collect_all(mw):
mw.state.collect(item, event=True)
-collect_all(multi_world)
+class LogicTestBase(RuleAssertMixin, TestCase):
+ options: typing.Dict[str, typing.Any] = {}
+ multiworld: MultiWorld
+ logic: StardewLogic
+ world: StardewValleyWorld
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ if cls is LogicTestBase:
+ raise SkipTest("Not running test on base class.")
+ def setUp(self) -> None:
+ self.multiworld = setup_solo_multiworld(self.options, _cache={})
+ collect_all(self.multiworld)
+ self.world = typing.cast(StardewValleyWorld, self.multiworld.worlds[1])
+ self.logic = self.world.logic
-class TestLogic(unittest.TestCase):
def test_given_bundle_item_then_is_available_in_logic(self):
for bundle_item in all_bundle_items_except_money:
- with self.subTest(msg=bundle_item.item.name):
- self.assertIn(bundle_item.item.name, logic.item_rules)
+ if not bundle_item.can_appear(self.world.content, self.world.options):
+ continue
+
+ with self.subTest(msg=bundle_item.item_name):
+ self.assertIn(bundle_item.get_item(), self.logic.registry.item_rules)
def test_given_item_rule_then_can_be_resolved(self):
- for item in logic.item_rules.keys():
+ for item in self.logic.registry.item_rules.keys():
with self.subTest(msg=item):
- rule = logic.item_rules[item]
- self.assertNotIn(MISSING_ITEM, repr(rule))
- self.assertTrue(rule == False_() or rule(multi_world.state), f"Could not resolve item rule for {item} {rule}")
+ rule = self.logic.registry.item_rules[item]
+ self.assert_rule_can_be_resolved(rule, self.multiworld.state)
def test_given_building_rule_then_can_be_resolved(self):
- for building in logic.building_rules.keys():
+ for building in self.logic.registry.building_rules.keys():
with self.subTest(msg=building):
- rule = logic.building_rules[building]
- self.assertNotIn(MISSING_ITEM, repr(rule))
- self.assertTrue(rule == False_() or rule(multi_world.state), f"Could not resolve building rule for {building} {rule}")
+ rule = self.logic.registry.building_rules[building]
+ self.assert_rule_can_be_resolved(rule, self.multiworld.state)
def test_given_quest_rule_then_can_be_resolved(self):
- for quest in logic.quest_rules.keys():
+ for quest in self.logic.registry.quest_rules.keys():
with self.subTest(msg=quest):
- rule = logic.quest_rules[quest]
- self.assertNotIn(MISSING_ITEM, repr(rule))
- self.assertTrue(rule == False_() or rule(multi_world.state), f"Could not resolve quest rule for {quest} {rule}")
+ rule = self.logic.registry.quest_rules[quest]
+ self.assert_rule_can_be_resolved(rule, self.multiworld.state)
def test_given_special_order_rule_then_can_be_resolved(self):
- for special_order in logic.special_order_rules.keys():
+ for special_order in self.logic.registry.special_order_rules.keys():
with self.subTest(msg=special_order):
- rule = logic.special_order_rules[special_order]
- self.assertNotIn(MISSING_ITEM, repr(rule))
- self.assertTrue(rule == False_() or rule(multi_world.state), f"Could not resolve special order rule for {special_order} {rule}")
-
- def test_given_tree_fruit_rule_then_can_be_resolved(self):
- for tree_fruit in logic.tree_fruit_rules.keys():
- with self.subTest(msg=tree_fruit):
- rule = logic.tree_fruit_rules[tree_fruit]
- self.assertNotIn(MISSING_ITEM, repr(rule))
- self.assertTrue(rule == False_() or rule(multi_world.state), f"Could not resolve tree fruit rule for {tree_fruit} {rule}")
-
- def test_given_seed_rule_then_can_be_resolved(self):
- for seed in logic.seed_rules.keys():
- with self.subTest(msg=seed):
- rule = logic.seed_rules[seed]
- self.assertNotIn(MISSING_ITEM, repr(rule))
- self.assertTrue(rule == False_() or rule(multi_world.state), f"Could not resolve seed rule for {seed} {rule}")
+ rule = self.logic.registry.special_order_rules[special_order]
+ self.assert_rule_can_be_resolved(rule, self.multiworld.state)
def test_given_crop_rule_then_can_be_resolved(self):
- for crop in logic.crop_rules.keys():
+ for crop in self.logic.registry.crop_rules.keys():
with self.subTest(msg=crop):
- rule = logic.crop_rules[crop]
- self.assertNotIn(MISSING_ITEM, repr(rule))
- self.assertTrue(rule == False_() or rule(multi_world.state), f"Could not resolve crop rule for {crop} {rule}")
+ rule = self.logic.registry.crop_rules[crop]
+ self.assert_rule_can_be_resolved(rule, self.multiworld.state)
def test_given_fish_rule_then_can_be_resolved(self):
- for fish in logic.fish_rules.keys():
+ for fish in self.logic.registry.fish_rules.keys():
with self.subTest(msg=fish):
- rule = logic.fish_rules[fish]
- self.assertNotIn(MISSING_ITEM, repr(rule))
- self.assertTrue(rule == False_() or rule(multi_world.state), f"Could not resolve fish rule for {fish} {rule}")
+ rule = self.logic.registry.fish_rules[fish]
+ self.assert_rule_can_be_resolved(rule, self.multiworld.state)
def test_given_museum_rule_then_can_be_resolved(self):
- for donation in logic.museum_rules.keys():
+ for donation in self.logic.registry.museum_rules.keys():
with self.subTest(msg=donation):
- rule = logic.museum_rules[donation]
- self.assertNotIn(MISSING_ITEM, repr(rule))
- self.assertTrue(rule == False_() or rule(multi_world.state), f"Could not resolve museum rule for {donation} {rule}")
+ rule = self.logic.registry.museum_rules[donation]
+ self.assert_rule_can_be_resolved(rule, self.multiworld.state)
def test_given_cooking_rule_then_can_be_resolved(self):
- for cooking_rule in logic.cooking_rules.keys():
+ for cooking_rule in self.logic.registry.cooking_rules.keys():
with self.subTest(msg=cooking_rule):
- rule = logic.cooking_rules[cooking_rule]
- self.assertNotIn(MISSING_ITEM, repr(rule))
- self.assertTrue(rule == False_() or rule(multi_world.state), f"Could not resolve cooking rule for {cooking_rule} {rule}")
+ rule = self.logic.registry.cooking_rules[cooking_rule]
+ self.assert_rule_can_be_resolved(rule, self.multiworld.state)
def test_given_location_rule_then_can_be_resolved(self):
- for location in multi_world.get_locations(1):
+ for location in self.multiworld.get_locations(1):
with self.subTest(msg=location.name):
rule = location.access_rule
- self.assertNotIn(MISSING_ITEM, repr(rule))
- self.assertTrue(rule == False_() or rule(multi_world.state), f"Could not resolve location rule for {location} {rule}")
+ self.assert_rule_can_be_resolved(rule, self.multiworld.state)
+
+
+class TestAllSanityLogic(LogicTestBase):
+ options = allsanity_mods_6_x_x()
+
+
+@unittest.skip("This test does not pass because some content is still not in content packs.")
+class TestMinLocationsMaxItemsLogic(LogicTestBase):
+ options = minimal_locations_maximal_items()
+ options[BundleRandomization.internal_name] = BundleRandomization.default
diff --git a/worlds/stardew_valley/test/TestLogicSimplification.py b/worlds/stardew_valley/test/TestLogicSimplification.py
deleted file mode 100644
index 83d779ce9043..000000000000
--- a/worlds/stardew_valley/test/TestLogicSimplification.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from .. import True_
-from ..logic import Received, Has, False_, And, Or, StardewLogic
-from ..options import default_options, StardewOptions
-
-
-def test_simplify_true_in_and():
- rules = {
- "Wood": True_(),
- "Rock": True_(),
- }
- summer = Received("Summer", 0, 1)
- assert (Has("Wood", rules) & summer & Has("Rock", rules)).simplify() == summer
-
-
-def test_simplify_false_in_or():
- rules = {
- "Wood": False_(),
- "Rock": False_(),
- }
- summer = Received("Summer", 0, 1)
- assert (Has("Wood", rules) | summer | Has("Rock", rules)).simplify() == summer
-
-
-def test_simplify_and_in_and():
- rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)),
- And(Received('Winter', 0, 1), Received('Spring', 0, 1)))
- assert rule.simplify() == And(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1),
- Received('Spring', 0, 1))
-
-
-def test_simplify_duplicated_and():
- rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)),
- And(Received('Summer', 0, 1), Received('Fall', 0, 1)))
- assert rule.simplify() == And(Received('Summer', 0, 1), Received('Fall', 0, 1))
-
-
-def test_simplify_or_in_or():
- rule = Or(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)),
- Or(Received('Winter', 0, 1), Received('Spring', 0, 1)))
- assert rule.simplify() == Or(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1),
- Received('Spring', 0, 1))
-
-
-def test_simplify_duplicated_or():
- rule = And(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)),
- Or(Received('Summer', 0, 1), Received('Fall', 0, 1)))
- assert rule.simplify() == Or(Received('Summer', 0, 1), Received('Fall', 0, 1))
-
-
-def test_simplify_true_in_or():
- rule = Or(True_(), Received('Summer', 0, 1))
- assert rule.simplify() == True_()
-
-
-def test_simplify_false_in_and():
- rule = And(False_(), Received('Summer', 0, 1))
- assert rule.simplify() == False_()
diff --git a/worlds/stardew_valley/test/TestMultiplePlayers.py b/worlds/stardew_valley/test/TestMultiplePlayers.py
new file mode 100644
index 000000000000..2f2092fdf7b6
--- /dev/null
+++ b/worlds/stardew_valley/test/TestMultiplePlayers.py
@@ -0,0 +1,92 @@
+from . import SVTestCase, setup_multiworld
+from .. import True_
+from ..options import FestivalLocations, StartingMoney
+from ..strings.festival_check_names import FestivalCheck
+
+
+def get_access_rule(multiworld, player: int, location_name: str):
+ return multiworld.get_location(location_name, player).access_rule
+
+
+class TestDifferentSettings(SVTestCase):
+
+ def test_different_festival_settings(self):
+ options_no_festivals = {FestivalLocations.internal_name: FestivalLocations.option_disabled}
+ options_easy_festivals = {FestivalLocations.internal_name: FestivalLocations.option_easy}
+ options_hard_festivals = {FestivalLocations.internal_name: FestivalLocations.option_hard}
+
+ multiplayer_options = [options_no_festivals, options_easy_festivals, options_hard_festivals]
+ multiworld = setup_multiworld(multiplayer_options)
+
+ self.check_location_rule(multiworld, 1, FestivalCheck.egg_hunt, False)
+ self.check_location_rule(multiworld, 2, FestivalCheck.egg_hunt, True, True)
+ self.check_location_rule(multiworld, 3, FestivalCheck.egg_hunt, True, True)
+
+ def test_different_money_settings(self):
+ options_no_festivals_unlimited_money = {FestivalLocations.internal_name: FestivalLocations.option_disabled,
+ StartingMoney.internal_name: -1}
+ options_no_festivals_limited_money = {FestivalLocations.internal_name: FestivalLocations.option_disabled,
+ StartingMoney.internal_name: 5000}
+ options_easy_festivals_unlimited_money = {FestivalLocations.internal_name: FestivalLocations.option_easy,
+ StartingMoney.internal_name: -1}
+ options_easy_festivals_limited_money = {FestivalLocations.internal_name: FestivalLocations.option_easy,
+ StartingMoney.internal_name: 5000}
+ options_hard_festivals_unlimited_money = {FestivalLocations.internal_name: FestivalLocations.option_hard,
+ StartingMoney.internal_name: -1}
+ options_hard_festivals_limited_money = {FestivalLocations.internal_name: FestivalLocations.option_hard,
+ StartingMoney.internal_name: 5000}
+
+ multiplayer_options = [options_no_festivals_unlimited_money, options_no_festivals_limited_money,
+ options_easy_festivals_unlimited_money, options_easy_festivals_limited_money,
+ options_hard_festivals_unlimited_money, options_hard_festivals_limited_money]
+ multiworld = setup_multiworld(multiplayer_options)
+
+ self.check_location_rule(multiworld, 1, FestivalCheck.rarecrow_4, False)
+ self.check_location_rule(multiworld, 2, FestivalCheck.rarecrow_4, False)
+
+ self.check_location_rule(multiworld, 3, FestivalCheck.rarecrow_4, True, True)
+ self.check_location_rule(multiworld, 4, FestivalCheck.rarecrow_4, True, False)
+
+ self.check_location_rule(multiworld, 5, FestivalCheck.rarecrow_4, True, True)
+ self.check_location_rule(multiworld, 6, FestivalCheck.rarecrow_4, True, False)
+
+ def test_money_rule_caching(self):
+ options_festivals_limited_money = {FestivalLocations.internal_name: FestivalLocations.option_easy,
+ StartingMoney.internal_name: 5000}
+ options_festivals_limited_money = {FestivalLocations.internal_name: FestivalLocations.option_easy,
+ StartingMoney.internal_name: 5000}
+
+ multiplayer_options = [options_festivals_limited_money, options_festivals_limited_money]
+ multiworld = setup_multiworld(multiplayer_options)
+
+ player_1_rarecrow_2 = get_access_rule(multiworld, 1, FestivalCheck.rarecrow_2)
+ player_1_rarecrow_4 = get_access_rule(multiworld, 1, FestivalCheck.rarecrow_4)
+ player_2_rarecrow_2 = get_access_rule(multiworld, 2, FestivalCheck.rarecrow_2)
+ player_2_rarecrow_4 = get_access_rule(multiworld, 2, FestivalCheck.rarecrow_4)
+
+ with self.subTest("Rules are not cached between players"):
+ self.assertNotEqual(id(player_1_rarecrow_2), id(player_2_rarecrow_2))
+ self.assertNotEqual(id(player_1_rarecrow_4), id(player_2_rarecrow_4))
+
+ with self.subTest("Rules are cached for the same player"):
+ self.assertEqual(id(player_1_rarecrow_2), id(player_1_rarecrow_4))
+ self.assertEqual(id(player_2_rarecrow_2), id(player_2_rarecrow_4))
+
+ def check_location_rule(self, multiworld, player: int, location_name: str, should_exist: bool, should_be_true: bool = False):
+ has = "has" if should_exist else "doesn't have"
+ rule = "without access rule" if should_be_true else f"with access rule"
+ rule_text = f" {rule}" if should_exist else ""
+ with self.subTest(f"Player {player} {has} {location_name}{rule_text}"):
+ locations = multiworld.get_locations(player)
+ locations_names = {location.name for location in locations}
+ if not should_exist:
+ self.assertNotIn(location_name, locations_names)
+ return None
+
+ self.assertIn(location_name, locations_names)
+ access_rule = get_access_rule(multiworld, player, location_name)
+ if should_be_true:
+ self.assertEqual(access_rule, True_())
+ else:
+ self.assertNotEqual(access_rule, True_())
+ return access_rule
diff --git a/worlds/stardew_valley/test/TestNumberLocations.py b/worlds/stardew_valley/test/TestNumberLocations.py
new file mode 100644
index 000000000000..ef552c10e8d5
--- /dev/null
+++ b/worlds/stardew_valley/test/TestNumberLocations.py
@@ -0,0 +1,98 @@
+from . import SVTestBase, allsanity_no_mods_6_x_x, \
+ allsanity_mods_6_x_x, minimal_locations_maximal_items, minimal_locations_maximal_items_with_island, get_minsanity_options, default_6_x_x
+from .. import location_table
+from ..items import Group, item_table
+
+
+class TestLocationGeneration(SVTestBase):
+
+ def test_all_location_created_are_in_location_table(self):
+ for location in self.get_real_locations():
+ self.assertIn(location.name, location_table)
+
+
+class TestMinLocationAndMaxItem(SVTestBase):
+ options = minimal_locations_maximal_items()
+
+ def test_minimal_location_maximal_items_still_valid(self):
+ valid_locations = self.get_real_locations()
+ number_locations = len(valid_locations)
+ number_items = len([item for item in self.multiworld.itempool
+ if Group.RESOURCE_PACK not in item_table[item.name].groups and Group.TRAP not in item_table[item.name].groups])
+ print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND EXCLUDED]")
+ self.assertGreaterEqual(number_locations, number_items)
+
+
+class TestMinLocationAndMaxItemWithIsland(SVTestBase):
+ options = minimal_locations_maximal_items_with_island()
+
+ def test_minimal_location_maximal_items_with_island_still_valid(self):
+ valid_locations = self.get_real_locations()
+ number_locations = len(valid_locations)
+ number_items = len([item for item in self.multiworld.itempool
+ if Group.RESOURCE_PACK not in item_table[item.name].groups and Group.TRAP not in item_table[item.name].groups])
+ print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND INCLUDED]")
+ self.assertGreaterEqual(number_locations, number_items)
+
+
+class TestMinSanityHasAllExpectedLocations(SVTestBase):
+ options = get_minsanity_options()
+
+ def test_minsanity_has_fewer_than_locations(self):
+ expected_locations = 85
+ real_locations = self.get_real_locations()
+ number_locations = len(real_locations)
+ print(f"Stardew Valley - Minsanity Locations: {number_locations}")
+ self.assertLessEqual(number_locations, expected_locations)
+ if number_locations != expected_locations:
+ print(f"\tDisappeared Locations Detected!"
+ f"\n\tPlease update test_minsanity_has_fewer_than_locations"
+ f"\n\t\tExpected: {expected_locations}"
+ f"\n\t\tActual: {number_locations}")
+
+
+class TestDefaultSettingsHasAllExpectedLocations(SVTestBase):
+ options = default_6_x_x()
+
+ def test_default_settings_has_exactly_locations(self):
+ expected_locations = 491
+ real_locations = self.get_real_locations()
+ number_locations = len(real_locations)
+ print(f"Stardew Valley - Default options locations: {number_locations}")
+ if number_locations != expected_locations:
+ print(f"\tNew locations detected!"
+ f"\n\tPlease update test_default_settings_has_exactly_locations"
+ f"\n\t\tExpected: {expected_locations}"
+ f"\n\t\tActual: {number_locations}")
+
+
+class TestAllSanitySettingsHasAllExpectedLocations(SVTestBase):
+ options = allsanity_no_mods_6_x_x()
+
+ def test_allsanity_without_mods_has_at_least_locations(self):
+ expected_locations = 2238
+ real_locations = self.get_real_locations()
+ number_locations = len(real_locations)
+ print(f"Stardew Valley - Allsanity Locations without mods: {number_locations}")
+ self.assertGreaterEqual(number_locations, expected_locations)
+ if number_locations != expected_locations:
+ print(f"\tNew locations detected!"
+ f"\n\tPlease update test_allsanity_without_mods_has_at_least_locations"
+ f"\n\t\tExpected: {expected_locations}"
+ f"\n\t\tActual: {number_locations}")
+
+
+class TestAllSanityWithModsSettingsHasAllExpectedLocations(SVTestBase):
+ options = allsanity_mods_6_x_x()
+
+ def test_allsanity_with_mods_has_at_least_locations(self):
+ expected_locations = 3096
+ real_locations = self.get_real_locations()
+ number_locations = len(real_locations)
+ print(f"Stardew Valley - Allsanity Locations with all mods: {number_locations}")
+ self.assertGreaterEqual(number_locations, expected_locations)
+ if number_locations != expected_locations:
+ print(f"\tNew locations detected!"
+ f"\n\tPlease update test_allsanity_with_mods_has_at_least_locations"
+ f"\n\t\tExpected: {expected_locations}"
+ f"\n\t\tActual: {number_locations}")
diff --git a/worlds/stardew_valley/test/TestOptionFlags.py b/worlds/stardew_valley/test/TestOptionFlags.py
new file mode 100644
index 000000000000..05e52b40c4bd
--- /dev/null
+++ b/worlds/stardew_valley/test/TestOptionFlags.py
@@ -0,0 +1,105 @@
+from . import SVTestBase
+from .. import BuildingProgression
+from ..options import ToolProgression
+
+
+class TestBitFlagsVanilla(SVTestBase):
+ options = {ToolProgression.internal_name: ToolProgression.option_vanilla,
+ BuildingProgression.internal_name: BuildingProgression.option_vanilla}
+
+ def test_options_are_not_detected_as_progressive(self):
+ world_options = self.world.options
+ tool_progressive = world_options.tool_progression & ToolProgression.option_progressive
+ building_progressive = world_options.building_progression & BuildingProgression.option_progressive
+ self.assertFalse(tool_progressive)
+ self.assertFalse(building_progressive)
+
+ def test_tools_and_buildings_not_in_pool(self):
+ item_names = [item.name for item in self.multiworld.itempool]
+ self.assertNotIn("Progressive Coop", item_names)
+ self.assertNotIn("Progressive Pickaxe", item_names)
+
+
+class TestBitFlagsVanillaCheap(SVTestBase):
+ options = {ToolProgression.internal_name: ToolProgression.option_vanilla_cheap,
+ BuildingProgression.internal_name: BuildingProgression.option_vanilla_cheap}
+
+ def test_options_are_not_detected_as_progressive(self):
+ world_options = self.world.options
+ tool_progressive = world_options.tool_progression & ToolProgression.option_progressive
+ building_progressive = world_options.building_progression & BuildingProgression.option_progressive
+ self.assertFalse(tool_progressive)
+ self.assertFalse(building_progressive)
+
+ def test_tools_and_buildings_not_in_pool(self):
+ item_names = [item.name for item in self.multiworld.itempool]
+ self.assertNotIn("Progressive Coop", item_names)
+ self.assertNotIn("Progressive Pickaxe", item_names)
+
+
+class TestBitFlagsVanillaVeryCheap(SVTestBase):
+ options = {ToolProgression.internal_name: ToolProgression.option_vanilla_very_cheap,
+ BuildingProgression.internal_name: BuildingProgression.option_vanilla_very_cheap}
+
+ def test_options_are_not_detected_as_progressive(self):
+ world_options = self.world.options
+ tool_progressive = world_options.tool_progression & ToolProgression.option_progressive
+ building_progressive = world_options.building_progression & BuildingProgression.option_progressive
+ self.assertFalse(tool_progressive)
+ self.assertFalse(building_progressive)
+
+ def test_tools_and_buildings_not_in_pool(self):
+ item_names = [item.name for item in self.multiworld.itempool]
+ self.assertNotIn("Progressive Coop", item_names)
+ self.assertNotIn("Progressive Pickaxe", item_names)
+
+
+class TestBitFlagsProgressive(SVTestBase):
+ options = {ToolProgression.internal_name: ToolProgression.option_progressive,
+ BuildingProgression.internal_name: BuildingProgression.option_progressive}
+
+ def test_options_are_detected_as_progressive(self):
+ world_options = self.world.options
+ tool_progressive = world_options.tool_progression & ToolProgression.option_progressive
+ building_progressive = world_options.building_progression & BuildingProgression.option_progressive
+ self.assertTrue(tool_progressive)
+ self.assertTrue(building_progressive)
+
+ def test_tools_and_buildings_in_pool(self):
+ item_names = [item.name for item in self.multiworld.itempool]
+ self.assertIn("Progressive Coop", item_names)
+ self.assertIn("Progressive Pickaxe", item_names)
+
+
+class TestBitFlagsProgressiveCheap(SVTestBase):
+ options = {ToolProgression.internal_name: ToolProgression.option_progressive_cheap,
+ BuildingProgression.internal_name: BuildingProgression.option_progressive_cheap}
+
+ def test_options_are_detected_as_progressive(self):
+ world_options = self.world.options
+ tool_progressive = world_options.tool_progression & ToolProgression.option_progressive
+ building_progressive = world_options.building_progression & BuildingProgression.option_progressive
+ self.assertTrue(tool_progressive)
+ self.assertTrue(building_progressive)
+
+ def test_tools_and_buildings_in_pool(self):
+ item_names = [item.name for item in self.multiworld.itempool]
+ self.assertIn("Progressive Coop", item_names)
+ self.assertIn("Progressive Pickaxe", item_names)
+
+
+class TestBitFlagsProgressiveVeryCheap(SVTestBase):
+ options = {ToolProgression.internal_name: ToolProgression.option_progressive_very_cheap,
+ BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap}
+
+ def test_options_are_detected_as_progressive(self):
+ world_options = self.world.options
+ tool_progressive = world_options.tool_progression & ToolProgression.option_progressive
+ building_progressive = world_options.building_progression & BuildingProgression.option_progressive
+ self.assertTrue(tool_progressive)
+ self.assertTrue(building_progressive)
+
+ def test_tools_and_buildings_in_pool(self):
+ item_names = [item.name for item in self.multiworld.itempool]
+ self.assertIn("Progressive Coop", item_names)
+ self.assertIn("Progressive Pickaxe", item_names)
diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py
index 1cd17ada1f6a..2824a10c38af 100644
--- a/worlds/stardew_valley/test/TestOptions.py
+++ b/worlds/stardew_valley/test/TestOptions.py
@@ -1,15 +1,14 @@
import itertools
-import unittest
-from random import random
-from typing import Dict
-
-from BaseClasses import ItemClassification, MultiWorld
-from Options import SpecialRange, OptionSet
-from . import setup_solo_multiworld, SVTestBase
-from .. import StardewItem, options, items_by_group, Group
+
+from Options import NamedRange
+from . import SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld
+from .assertion import WorldAssertMixin
+from .long.option_names import all_option_choices
+from .. import items_by_group, Group, StardewValleyWorld
from ..locations import locations_by_tag, LocationTags, location_table
-from ..options import StardewOption, stardew_valley_option_classes, Mods
-from ..strings.goal_names import Goal
+from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations, \
+ SkillProgression
+from ..strings.goal_names import Goal as GoalName
from ..strings.season_names import Season
from ..strings.special_order_names import SpecialOrder
from ..strings.tool_names import ToolMaterial, Tool
@@ -18,249 +17,222 @@
TOOLS = {"Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can", "Fishing Rod"}
-def assert_can_win(tester: SVTestBase, multiworld: MultiWorld):
- for item in multiworld.get_items():
- multiworld.state.collect(item)
-
- tester.assertTrue(multiworld.find_item("Victory", 1).can_reach(multiworld.state))
-
-
-def basic_checks(tester: SVTestBase, multiworld: MultiWorld):
- tester.assertIn(StardewItem("Victory", ItemClassification.progression, None, 1), multiworld.get_items())
- assert_can_win(tester, multiworld)
- non_event_locations = [location for location in multiworld.get_locations() if not location.event]
- tester.assertEqual(len(multiworld.itempool), len(non_event_locations))
-
-
-def check_no_ginger_island(tester: SVTestBase, multiworld: MultiWorld):
- ginger_island_items = [item_data.name for item_data in items_by_group[Group.GINGER_ISLAND]]
- ginger_island_locations = [location_data.name for location_data in locations_by_tag[LocationTags.GINGER_ISLAND]]
- for item in multiworld.get_items():
- tester.assertNotIn(item.name, ginger_island_items)
- for location in multiworld.get_locations():
- tester.assertNotIn(location.name, ginger_island_locations)
-
-
-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(SVTestBase):
+class TestGenerateDynamicOptions(WorldAssertMixin, SVTestCase):
def test_given_special_range_when_generate_then_basic_checks(self):
- for option in stardew_valley_option_classes:
- if not issubclass(option, SpecialRange):
+ options = StardewValleyWorld.options_dataclass.type_hints
+ for option_name, option in options.items():
+ if not issubclass(option, NamedRange):
continue
for value in option.special_range_names:
- with self.subTest(f"{option.internal_name}: {value}"):
- choices = {option.internal_name: option.special_range_names[value]}
- multiworld = setup_solo_multiworld(choices)
- basic_checks(self, multiworld)
+ world_options = {option_name: option.special_range_names[value]}
+ with self.solo_world_sub_test(f"{option_name}: {value}", world_options) as (multiworld, _):
+ self.assert_basic_checks(multiworld)
def test_given_choice_when_generate_then_basic_checks(self):
- seed = int(random() * pow(10, 18) - 1)
- for option in stardew_valley_option_classes:
+ options = StardewValleyWorld.options_dataclass.type_hints
+ for option_name, option in options.items():
if not option.options:
continue
for value in option.options:
- with self.subTest(f"{option.internal_name}: {value} [Seed: {seed}]"):
- world_options = {option.internal_name: option.options[value]}
- multiworld = setup_solo_multiworld(world_options, seed)
- basic_checks(self, multiworld)
+ world_options = {option_name: option.options[value]}
+ with self.solo_world_sub_test(f"{option_name}: {value}", world_options) as (multiworld, _):
+ self.assert_basic_checks(multiworld)
-class TestGoal(SVTestBase):
+class TestGoal(SVTestCase):
def test_given_goal_when_generate_then_victory_is_in_correct_location(self):
- for goal, location in [("community_center", Goal.community_center),
- ("grandpa_evaluation", Goal.grandpa_evaluation),
- ("bottom_of_the_mines", Goal.bottom_of_the_mines),
- ("cryptic_note", Goal.cryptic_note),
- ("master_angler", Goal.master_angler),
- ("complete_collection", Goal.complete_museum),
- ("full_house", Goal.full_house),
- ("perfection", Goal.perfection)]:
- with self.subTest(msg=f"Goal: {goal}, Location: {location}"):
- world_options = {options.Goal.internal_name: options.Goal.options[goal]}
- multi_world = setup_solo_multiworld(world_options)
+ for goal, location in [("community_center", GoalName.community_center),
+ ("grandpa_evaluation", GoalName.grandpa_evaluation),
+ ("bottom_of_the_mines", GoalName.bottom_of_the_mines),
+ ("cryptic_note", GoalName.cryptic_note),
+ ("master_angler", GoalName.master_angler),
+ ("complete_collection", GoalName.complete_museum),
+ ("full_house", GoalName.full_house),
+ ("perfection", GoalName.perfection)]:
+ world_options = {Goal.internal_name: Goal.options[goal]}
+ with self.solo_world_sub_test(f"Goal: {goal}, Location: {location}", world_options) as (multi_world, _):
victory = multi_world.find_item("Victory", 1)
self.assertEqual(victory.name, location)
-class TestSeasonRandomization(SVTestBase):
+class TestSeasonRandomization(SVTestCase):
def test_given_disabled_when_generate_then_all_seasons_are_precollected(self):
- world_options = {options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled}
- multi_world = setup_solo_multiworld(world_options)
-
- precollected_items = {item.name for item in multi_world.precollected_items[1]}
- self.assertTrue(all([season in precollected_items for season in SEASONS]))
+ world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_disabled}
+ with solo_multiworld(world_options) as (multi_world, _):
+ precollected_items = {item.name for item in multi_world.precollected_items[1]}
+ self.assertTrue(all([season in precollected_items for season in SEASONS]))
def test_given_randomized_when_generate_then_all_seasons_are_in_the_pool_or_precollected(self):
- world_options = {options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized}
- multi_world = setup_solo_multiworld(world_options)
- precollected_items = {item.name for item in multi_world.precollected_items[1]}
- items = {item.name for item in multi_world.get_items()} | precollected_items
- self.assertTrue(all([season in items for season in SEASONS]))
- self.assertEqual(len(SEASONS.intersection(precollected_items)), 1)
+ world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_randomized}
+ with solo_multiworld(world_options) as (multi_world, _):
+ precollected_items = {item.name for item in multi_world.precollected_items[1]}
+ items = {item.name for item in multi_world.get_items()} | precollected_items
+ self.assertTrue(all([season in items for season in SEASONS]))
+ self.assertEqual(len(SEASONS.intersection(precollected_items)), 1)
def test_given_progressive_when_generate_then_3_progressive_seasons_are_in_the_pool(self):
- world_options = {options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive}
- multi_world = setup_solo_multiworld(world_options)
-
- items = [item.name for item in multi_world.get_items()]
- self.assertEqual(items.count(Season.progressive), 3)
+ world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_progressive}
+ with solo_multiworld(world_options) as (multi_world, _):
+ items = [item.name for item in multi_world.get_items()]
+ self.assertEqual(items.count(Season.progressive), 3)
-class TestToolProgression(SVTestBase):
+class TestToolProgression(SVTestCase):
def test_given_vanilla_when_generate_then_no_tool_in_pool(self):
- world_options = {options.ToolProgression.internal_name: options.ToolProgression.option_vanilla}
- multi_world = setup_solo_multiworld(world_options)
+ world_options = {ToolProgression.internal_name: ToolProgression.option_vanilla}
+ with solo_multiworld(world_options) as (multi_world, _):
+ items = {item.name for item in multi_world.get_items()}
+ for tool in TOOLS:
+ self.assertNotIn(tool, items)
+
+ def test_given_progressive_when_generate_then_each_tool_is_in_pool_4_times(self):
+ world_options = {ToolProgression.internal_name: ToolProgression.option_progressive,
+ SkillProgression.internal_name: SkillProgression.option_progressive}
+ with solo_multiworld(world_options) as (multi_world, _):
+ items = [item.name for item in multi_world.get_items()]
+ for tool in TOOLS:
+ count = items.count("Progressive " + tool)
+ self.assertEqual(count, 4, f"Progressive {tool} was there {count} times")
+ scythe_count = items.count("Progressive Scythe")
+ self.assertEqual(scythe_count, 1, f"Progressive Scythe was there {scythe_count} times")
+ self.assertEqual(items.count("Golden Scythe"), 0, f"Golden Scythe is deprecated")
+
+ def test_given_progressive_with_masteries_when_generate_then_fishing_rod_is_in_the_pool_5_times(self):
+ world_options = {ToolProgression.internal_name: ToolProgression.option_progressive,
+ SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries}
+ with solo_multiworld(world_options) as (multi_world, _):
+ items = [item.name for item in multi_world.get_items()]
+ for tool in TOOLS:
+ count = items.count("Progressive " + tool)
+ expected_count = 5 if tool == "Fishing Rod" else 4
+ self.assertEqual(count, expected_count, f"Progressive {tool} was there {count} times")
+ scythe_count = items.count("Progressive Scythe")
+ self.assertEqual(scythe_count, 2, f"Progressive Scythe was there {scythe_count} times")
+ self.assertEqual(items.count("Golden Scythe"), 0, f"Golden Scythe is deprecated")
- items = {item.name for item in multi_world.get_items()}
- for tool in TOOLS:
- self.assertNotIn(tool, items)
+ def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self):
+ world_options = {ToolProgression.internal_name: ToolProgression.option_progressive}
+ with solo_multiworld(world_options) as (multi_world, _):
+ locations = {locations.name for locations in multi_world.get_locations(1)}
+ for material, tool in itertools.product(ToolMaterial.tiers.values(),
+ [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can]):
+ if material == ToolMaterial.basic:
+ continue
+ self.assertIn(f"{material} {tool} Upgrade", locations)
+ self.assertIn("Purchase Training Rod", locations)
+ self.assertIn("Bamboo Pole Cutscene", locations)
+ self.assertIn("Purchase Fiberglass Rod", locations)
+ self.assertIn("Purchase Iridium Rod", locations)
+
+
+class TestGenerateAllOptionsWithExcludeGingerIsland(WorldAssertMixin, SVTestCase):
- def test_given_progressive_when_generate_then_progressive_tool_of_each_is_in_pool_four_times(self):
- world_options = {options.ToolProgression.internal_name: options.ToolProgression.option_progressive}
- multi_world = setup_solo_multiworld(world_options)
+ def test_given_choice_when_generate_exclude_ginger_island(self):
+ for option, option_choice in all_option_choices:
+ if option is ExcludeGingerIsland:
+ continue
- items = [item.name for item in multi_world.get_items()]
- for tool in TOOLS:
- self.assertEqual(items.count("Progressive " + tool), 4)
+ world_options = {
+ ExcludeGingerIsland: ExcludeGingerIsland.option_true,
+ option: option_choice
+ }
- def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self):
- world_options = {options.ToolProgression.internal_name: options.ToolProgression.option_progressive}
- multi_world = setup_solo_multiworld(world_options)
+ with self.solo_world_sub_test(f"{option.internal_name}: {option_choice}", world_options) as (multiworld, stardew_world):
- locations = {locations.name for locations in multi_world.get_locations(1)}
- for material, tool in itertools.product(ToolMaterial.tiers.values(),
- [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can]):
- if material == ToolMaterial.basic:
- continue
- self.assertIn(f"{material} {tool} Upgrade", locations)
- self.assertIn("Purchase Training Rod", locations)
- self.assertIn("Bamboo Pole Cutscene", locations)
- self.assertIn("Purchase Fiberglass Rod", locations)
- self.assertIn("Purchase Iridium Rod", locations)
-
-
-class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase):
- def test_given_special_range_when_generate_exclude_ginger_island(self):
- for option in stardew_valley_option_classes:
- if not issubclass(option,
- SpecialRange) or option.internal_name == options.ExcludeGingerIsland.internal_name:
- continue
- for value in option.special_range_names:
- with self.subTest(f"{option.internal_name}: {value}"):
- multiworld = setup_solo_multiworld(
- {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
- option.internal_name: option.special_range_names[value]})
- check_no_ginger_island(self, multiworld)
+ # Some options, like goals, will force Ginger island back in the game. We want to skip testing those.
+ if stardew_world.options.exclude_ginger_island != ExcludeGingerIsland.option_true:
+ continue
- def test_given_choice_when_generate_exclude_ginger_island(self):
- seed = int(random() * pow(10, 18) - 1)
- island_option = options.ExcludeGingerIsland
- for option in stardew_valley_option_classes:
- if not option.options or option.internal_name == island_option.internal_name:
- continue
- for value in option.options:
- with self.subTest(f"{option.internal_name}: {value} [Seed: {seed}]"):
- multiworld = setup_solo_multiworld(
- {island_option.internal_name: island_option.option_true,
- option.internal_name: option.options[value]}, seed)
- if multiworld.worlds[self.player].options[island_option.internal_name] != island_option.option_true:
- continue
- basic_checks(self, multiworld)
- check_no_ginger_island(self, multiworld)
+ self.assert_basic_checks(multiworld)
+ self.assert_no_ginger_island_content(multiworld)
def test_given_island_related_goal_then_override_exclude_ginger_island(self):
- island_goals = [value for value in options.Goal.options if value in ["walnut_hunter", "perfection"]]
- island_option = options.ExcludeGingerIsland
- for goal in island_goals:
- for value in island_option.options:
- with self.subTest(f"Goal: {goal}, {island_option.internal_name}: {value}"):
- multiworld = setup_solo_multiworld(
- {options.Goal.internal_name: options.Goal.options[goal],
- island_option.internal_name: island_option.options[value]})
- self.assertEqual(multiworld.worlds[self.player].options[island_option.internal_name], island_option.option_false)
- basic_checks(self, multiworld)
-
-
-class TestTraps(SVTestBase):
- def test_given_no_traps_when_generate_then_no_trap_in_pool(self):
- world_options = self.allsanity_options_without_mods()
- world_options.update({options.TrapItems.internal_name: options.TrapItems.option_no_traps})
- multi_world = setup_solo_multiworld(world_options)
+ island_goals = ["greatest_walnut_hunter", "perfection"]
+ for goal, exclude_island in itertools.product(island_goals, ExcludeGingerIsland.options):
+ world_options = {
+ Goal: goal,
+ ExcludeGingerIsland: exclude_island
+ }
+
+ with self.solo_world_sub_test(f"Goal: {goal}, {ExcludeGingerIsland.internal_name}: {exclude_island}", world_options) \
+ as (multiworld, stardew_world):
+ self.assertEqual(stardew_world.options.exclude_ginger_island, ExcludeGingerIsland.option_false)
+ self.assert_basic_checks(multiworld)
+
- trap_items = [item_data.name for item_data in items_by_group[Group.TRAP]]
- multiworld_items = [item.name for item in multi_world.get_items()]
+class TestTraps(SVTestCase):
+ def test_given_no_traps_when_generate_then_no_trap_in_pool(self):
+ world_options = allsanity_no_mods_6_x_x().copy()
+ world_options[TrapItems.internal_name] = TrapItems.option_no_traps
+ with solo_multiworld(world_options) as (multi_world, _):
+ trap_items = [item_data.name for item_data in items_by_group[Group.TRAP]]
+ multiworld_items = [item.name for item in multi_world.get_items()]
- for item in trap_items:
- with self.subTest(f"{item}"):
- self.assertNotIn(item, multiworld_items)
+ for item in trap_items:
+ with self.subTest(f"{item}"):
+ self.assertNotIn(item, multiworld_items)
def test_given_traps_when_generate_then_all_traps_in_pool(self):
- trap_option = options.TrapItems
+ trap_option = TrapItems
for value in trap_option.options:
if value == "no_traps":
continue
- world_options = self.allsanity_options_with_mods()
- world_options.update({options.TrapItems.internal_name: trap_option.options[value]})
- multi_world = setup_solo_multiworld(world_options)
- trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups and item_data.mod_name is None]
- multiworld_items = [item.name for item in multi_world.get_items()]
- for item in trap_items:
- with self.subTest(f"Option: {value}, Item: {item}"):
- self.assertIn(item, multiworld_items)
-
-
-class TestSpecialOrders(SVTestBase):
+ world_options = allsanity_mods_6_x_x()
+ world_options.update({TrapItems.internal_name: trap_option.options[value]})
+ with solo_multiworld(world_options) as (multi_world, _):
+ trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if
+ Group.DEPRECATED not in item_data.groups and item_data.mod_name is None]
+ multiworld_items = [item.name for item in multi_world.get_items()]
+ for item in trap_items:
+ with self.subTest(f"Option: {value}, Item: {item}"):
+ self.assertIn(item, multiworld_items)
+
+
+class TestSpecialOrders(SVTestCase):
def test_given_disabled_then_no_order_in_pool(self):
- world_options = {options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled}
- multi_world = setup_solo_multiworld(world_options)
-
- locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table}
- for location_name in locations_in_pool:
- location = location_table[location_name]
- self.assertNotIn(LocationTags.SPECIAL_ORDER_BOARD, location.tags)
- self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags)
+ world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla}
+ with solo_multiworld(world_options) as (multi_world, _):
+ locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table}
+ for location_name in locations_in_pool:
+ location = location_table[location_name]
+ self.assertNotIn(LocationTags.SPECIAL_ORDER_BOARD, location.tags)
+ self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags)
def test_given_board_only_then_no_qi_order_in_pool(self):
- world_options = {options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_only}
- multi_world = setup_solo_multiworld(world_options)
+ world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board}
+ with solo_multiworld(world_options) as (multi_world, _):
- locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table}
- for location_name in locations_in_pool:
- location = location_table[location_name]
- self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags)
+ locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table}
+ for location_name in locations_in_pool:
+ location = location_table[location_name]
+ self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags)
- for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]:
- if board_location.mod_name:
- continue
- self.assertIn(board_location.name, locations_in_pool)
+ for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]:
+ if board_location.mod_name:
+ continue
+ self.assertIn(board_location.name, locations_in_pool)
def test_given_board_and_qi_then_all_orders_in_pool(self):
- world_options = {options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
- options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_victories}
- multi_world = setup_solo_multiworld(world_options)
-
- locations_in_pool = {location.name for location in multi_world.get_locations()}
- for qi_location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI]:
- if qi_location.mod_name:
- continue
- self.assertIn(qi_location.name, locations_in_pool)
-
- for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]:
- if board_location.mod_name:
- continue
- self.assertIn(board_location.name, locations_in_pool)
+ world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
+ ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false}
+ with solo_multiworld(world_options) as (multi_world, _):
+
+ locations_in_pool = {location.name for location in multi_world.get_locations()}
+ for qi_location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI]:
+ if qi_location.mod_name:
+ continue
+ self.assertIn(qi_location.name, locations_in_pool)
+
+ for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]:
+ if board_location.mod_name:
+ continue
+ self.assertIn(board_location.name, locations_in_pool)
def test_given_board_and_qi_without_arcade_machines_then_lets_play_a_game_not_in_pool(self):
- world_options = {options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
- options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled}
- multi_world = setup_solo_multiworld(world_options)
-
- locations_in_pool = {location.name for location in multi_world.get_locations()}
- self.assertNotIn(SpecialOrder.lets_play_a_game, locations_in_pool)
+ world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
+ ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false}
+ with solo_multiworld(world_options) as (multi_world, _):
+ locations_in_pool = {location.name for location in multi_world.get_locations()}
+ self.assertNotIn(SpecialOrder.lets_play_a_game, locations_in_pool)
diff --git a/worlds/stardew_valley/test/TestOptionsPairs.py b/worlds/stardew_valley/test/TestOptionsPairs.py
new file mode 100644
index 000000000000..d953696e887d
--- /dev/null
+++ b/worlds/stardew_valley/test/TestOptionsPairs.py
@@ -0,0 +1,56 @@
+from . import SVTestBase
+from .assertion import WorldAssertMixin
+from .. import options
+from ..options import Goal, QuestLocations
+
+
+class TestCrypticNoteNoQuests(WorldAssertMixin, SVTestBase):
+ options = {
+ Goal.internal_name: Goal.option_cryptic_note,
+ QuestLocations.internal_name: "none"
+ }
+
+ def test_given_option_pair_then_basic_checks(self):
+ self.assert_basic_checks(self.multiworld)
+
+
+class TestCompleteCollectionNoQuests(WorldAssertMixin, SVTestBase):
+ options = {
+ Goal.internal_name: Goal.option_complete_collection,
+ QuestLocations.internal_name: "none"
+ }
+
+ def test_given_option_pair_then_basic_checks(self):
+ self.assert_basic_checks(self.multiworld)
+
+
+class TestProtectorOfTheValleyNoQuests(WorldAssertMixin, SVTestBase):
+ options = {
+ Goal.internal_name: Goal.option_protector_of_the_valley,
+ QuestLocations.internal_name: "none"
+ }
+
+ def test_given_option_pair_then_basic_checks(self):
+ self.assert_basic_checks(self.multiworld)
+
+
+class TestCraftMasterNoQuests(WorldAssertMixin, SVTestBase):
+ options = {
+ Goal.internal_name: Goal.option_craft_master,
+ QuestLocations.internal_name: "none"
+ }
+
+ def test_given_option_pair_then_basic_checks(self):
+ self.assert_basic_checks(self.multiworld)
+
+
+class TestCraftMasterNoSpecialOrder(WorldAssertMixin, SVTestBase):
+ options = {
+ options.Goal.internal_name: Goal.option_craft_master,
+ options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.alias_disabled,
+ options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
+ options.Craftsanity.internal_name: options.Craftsanity.option_none
+ }
+
+ def test_given_option_pair_then_basic_checks(self):
+ self.assert_basic_checks(self.multiworld)
diff --git a/worlds/stardew_valley/test/TestPresets.py b/worlds/stardew_valley/test/TestPresets.py
new file mode 100644
index 000000000000..2bb1c7fbaeaf
--- /dev/null
+++ b/worlds/stardew_valley/test/TestPresets.py
@@ -0,0 +1,21 @@
+import builtins
+import inspect
+
+from Options import PerGameCommonOptions, OptionSet
+from . import SVTestCase
+from .. import sv_options_presets, StardewValleyOptions
+
+
+class TestPresets(SVTestCase):
+ def test_all_presets_explicitly_set_all_options(self):
+ all_option_names = {option_key for option_key in StardewValleyOptions.type_hints}
+ omitted_option_names = {option_key for option_key in PerGameCommonOptions.type_hints}
+ mandatory_option_names = {option_key for option_key in all_option_names
+ if option_key not in omitted_option_names and
+ not issubclass(StardewValleyOptions.type_hints[option_key], OptionSet)}
+
+ for preset_name in sv_options_presets:
+ with self.subTest(f"{preset_name}"):
+ for option_name in mandatory_option_names:
+ with self.subTest(f"{preset_name} -> {option_name}"):
+ self.assertIn(option_name, sv_options_presets[preset_name])
\ No newline at end of file
diff --git a/worlds/stardew_valley/test/TestRegions.py b/worlds/stardew_valley/test/TestRegions.py
index 293ce72d07cf..a25feea22085 100644
--- a/worlds/stardew_valley/test/TestRegions.py
+++ b/worlds/stardew_valley/test/TestRegions.py
@@ -1,10 +1,13 @@
import random
-import sys
import unittest
+from typing import Set
-from . import SVTestBase, setup_solo_multiworld
-from .. import StardewOptions, options, StardewValleyWorld
-from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag
+from BaseClasses import get_seed
+from . import SVTestCase, complete_options_with_default
+from ..options import EntranceRandomization, ExcludeGingerIsland, SkillProgression
+from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag, create_final_connections_and_regions
+from ..strings.entrance_names import Entrance as EntranceName
+from ..strings.region_names import Region as RegionName
connections_by_name = {connection.name for connection in vanilla_connections}
regions_by_name = {region.name for region in vanilla_regions}
@@ -25,76 +28,123 @@ def test_connection_lead_somewhere(self):
f"{connection.name} is leading to {connection.destination} but it does not exist.")
-class TestEntranceRando(unittest.TestCase):
+def explore_connections_tree_up_to_blockers(blocked_entrances: Set[str], connections_by_name, regions_by_name):
+ explored_entrances = set()
+ explored_regions = set()
+ entrances_to_explore = set()
+ current_node_name = "Menu"
+ current_node = regions_by_name[current_node_name]
+ entrances_to_explore.update(current_node.exits)
+ while entrances_to_explore:
+ current_entrance_name = entrances_to_explore.pop()
+ current_entrance = connections_by_name[current_entrance_name]
+ current_node_name = current_entrance.destination
+
+ explored_entrances.add(current_entrance_name)
+ explored_regions.add(current_node_name)
+
+ if current_entrance_name in blocked_entrances:
+ continue
+
+ current_node = regions_by_name[current_node_name]
+ entrances_to_explore.update({entrance for entrance in current_node.exits if entrance not in explored_entrances})
+ return explored_regions
+
+
+class TestEntranceRando(SVTestCase):
def test_entrance_randomization(self):
- for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
- (options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION),
- (options.EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]:
- # option = options.EntranceRandomization.option_buildings
- # flag = RandomizationFlag.BUILDINGS
- # for i in range(0, 100000):
- seed = random.randrange(sys.maxsize)
+ for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
+ (EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION),
+ (EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS),
+ (EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]:
+ sv_options = complete_options_with_default({
+ EntranceRandomization.internal_name: option,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
+ SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
+ })
+ seed = get_seed()
+ rand = random.Random(seed)
with self.subTest(flag=flag, msg=f"Seed: {seed}"):
- rand = random.Random(seed)
- world_options = StardewOptions({options.EntranceRandomization.internal_name: option,
- options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false})
- regions_by_name = {region.name: region for region in vanilla_regions}
-
- _, randomized_connections = randomize_connections(rand, world_options, regions_by_name)
+ entrances, regions = create_final_connections_and_regions(sv_options)
+ _, randomized_connections = randomize_connections(rand, sv_options, regions, entrances)
for connection in vanilla_connections:
if flag in connection.flag:
connection_in_randomized = connection.name in randomized_connections
reverse_in_randomized = connection.reverse in randomized_connections
- self.assertTrue(connection_in_randomized,
- f"Connection {connection.name} should be randomized but it is not in the output. Seed = {seed}")
- self.assertTrue(reverse_in_randomized,
- f"Connection {connection.reverse} should be randomized but it is not in the output. Seed = {seed}")
+ self.assertTrue(connection_in_randomized, f"Connection {connection.name} should be randomized but it is not in the output.")
+ self.assertTrue(reverse_in_randomized, f"Connection {connection.reverse} should be randomized but it is not in the output.")
self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()),
- f"Connections are duplicated in randomization. Seed = {seed}")
+ f"Connections are duplicated in randomization.")
def test_entrance_randomization_without_island(self):
- for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
- (options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION),
- (options.EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]:
- with self.subTest(option=option, flag=flag):
- seed = random.randrange(sys.maxsize)
- rand = random.Random(seed)
- world_options = StardewOptions({options.EntranceRandomization.internal_name: option,
- options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true})
- regions_by_name = {region.name: region for region in vanilla_regions}
-
- _, randomized_connections = randomize_connections(rand, world_options, regions_by_name)
+ for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
+ (EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION),
+ (EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS),
+ (EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]:
+
+ sv_options = complete_options_with_default({
+ EntranceRandomization.internal_name: option,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
+ SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
+ })
+ seed = get_seed()
+ rand = random.Random(seed)
+ with self.subTest(option=option, flag=flag, seed=seed):
+ entrances, regions = create_final_connections_and_regions(sv_options)
+ _, randomized_connections = randomize_connections(rand, sv_options, regions, entrances)
for connection in vanilla_connections:
if flag in connection.flag:
if RandomizationFlag.GINGER_ISLAND in connection.flag:
self.assertNotIn(connection.name, randomized_connections,
- f"Connection {connection.name} should not be randomized but it is in the output. Seed = {seed}")
+ f"Connection {connection.name} should not be randomized but it is in the output.")
self.assertNotIn(connection.reverse, randomized_connections,
- f"Connection {connection.reverse} should not be randomized but it is in the output. Seed = {seed}")
+ f"Connection {connection.reverse} should not be randomized but it is in the output.")
else:
self.assertIn(connection.name, randomized_connections,
- f"Connection {connection.name} should be randomized but it is not in the output. Seed = {seed}")
+ f"Connection {connection.name} should be randomized but it is not in the output.")
self.assertIn(connection.reverse, randomized_connections,
- f"Connection {connection.reverse} should be randomized but it is not in the output. Seed = {seed}")
+ f"Connection {connection.reverse} should be randomized but it is not in the output.")
self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()),
- f"Connections are duplicated in randomization. Seed = {seed}")
+ f"Connections are duplicated in randomization.")
+ def test_cannot_put_island_access_on_island(self):
+ sv_options = complete_options_with_default({
+ EntranceRandomization.internal_name: EntranceRandomization.option_buildings,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
+ SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
+ })
-class TestEntranceClassifications(SVTestBase):
+ for i in range(0, 100 if self.skip_long_tests else 10000):
+ seed = get_seed()
+ rand = random.Random(seed)
+ with self.subTest(msg=f"Seed: {seed}"):
+ entrances, regions = create_final_connections_and_regions(sv_options)
+ randomized_connections, randomized_data = randomize_connections(rand, sv_options, regions, entrances)
+ connections_by_name = {connection.name: connection for connection in randomized_connections}
+
+ blocked_entrances = {EntranceName.use_island_obelisk, EntranceName.boat_to_ginger_island}
+ required_regions = {RegionName.wizard_tower, RegionName.boat_tunnel}
+ self.assert_can_reach_any_region_before_blockers(required_regions, blocked_entrances, connections_by_name, regions)
+
+ def assert_can_reach_any_region_before_blockers(self, required_regions, blocked_entrances, connections_by_name, regions_by_name):
+ explored_regions = explore_connections_tree_up_to_blockers(blocked_entrances, connections_by_name, regions_by_name)
+ self.assertTrue(any(region in explored_regions for region in required_regions))
+
+
+class TestEntranceClassifications(SVTestCase):
def test_non_progression_are_all_accessible_with_empty_inventory(self):
- for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
- (options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION)]:
- seed = random.randrange(sys.maxsize)
- with self.subTest(flag=flag, msg=f"Seed: {seed}"):
- multiworld_options = {options.EntranceRandomization.internal_name: option}
- multiworld = setup_solo_multiworld(multiworld_options, seed)
- sv_world: StardewValleyWorld = multiworld.worlds[1]
+ for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
+ (EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION)]:
+ world_options = {
+ EntranceRandomization.internal_name: option
+ }
+ with self.solo_world_sub_test(world_options=world_options, flag=flag) as (multiworld, sv_world):
ap_entrances = {entrance.name: entrance for entrance in multiworld.get_entrances()}
for randomized_entrance in sv_world.randomized_entrances:
if randomized_entrance in ap_entrances:
@@ -103,3 +153,16 @@ def test_non_progression_are_all_accessible_with_empty_inventory(self):
if sv_world.randomized_entrances[randomized_entrance] in ap_entrances:
ap_entrance_destination = multiworld.get_entrance(sv_world.randomized_entrances[randomized_entrance], 1)
self.assertTrue(ap_entrance_destination.access_rule(multiworld.state))
+
+ def test_no_ginger_island_entrances_when_excluded(self):
+ world_options = {
+ EntranceRandomization.internal_name: EntranceRandomization.option_disabled,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true
+ }
+ with self.solo_world_sub_test(world_options=world_options) as (multiworld, _):
+ ap_entrances = {entrance.name: entrance for entrance in multiworld.get_entrances()}
+ entrance_data_by_name = {entrance.name: entrance for entrance in vanilla_connections}
+ for entrance_name in ap_entrances:
+ entrance_data = entrance_data_by_name[entrance_name]
+ with self.subTest(f"{entrance_name}: {entrance_data.flag}"):
+ self.assertFalse(entrance_data.flag & RandomizationFlag.GINGER_ISLAND)
diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py
deleted file mode 100644
index 0847d8a63b95..000000000000
--- a/worlds/stardew_valley/test/TestRules.py
+++ /dev/null
@@ -1,506 +0,0 @@
-from collections import Counter
-
-from . import SVTestBase
-from .. import options
-from ..locations import locations_by_tag, LocationTags, location_table
-from ..strings.animal_names import Animal
-from ..strings.animal_product_names import AnimalProduct
-from ..strings.artisan_good_names import ArtisanGood
-from ..strings.crop_names import Vegetable
-from ..strings.entrance_names import Entrance
-from ..strings.food_names import Meal
-from ..strings.ingredient_names import Ingredient
-from ..strings.machine_names import Machine
-from ..strings.region_names import Region
-from ..strings.season_names import Season
-from ..strings.seed_names import Seed
-
-
-class TestProgressiveToolsLogic(SVTestBase):
- options = {
- options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
- options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized,
- }
-
- def setUp(self):
- super().setUp()
- self.multiworld.state.prog_items = Counter()
-
- def test_sturgeon(self):
- self.assertFalse(self.world.logic.has("Sturgeon")(self.multiworld.state))
-
- summer = self.world.create_item("Summer")
- self.multiworld.state.collect(summer, event=True)
- self.assertFalse(self.world.logic.has("Sturgeon")(self.multiworld.state))
-
- fishing_rod = self.world.create_item("Progressive Fishing Rod")
- self.multiworld.state.collect(fishing_rod, event=True)
- self.multiworld.state.collect(fishing_rod, event=True)
- self.assertFalse(self.world.logic.has("Sturgeon")(self.multiworld.state))
-
- fishing_level = self.world.create_item("Fishing Level")
- self.multiworld.state.collect(fishing_level, event=True)
- self.assertFalse(self.world.logic.has("Sturgeon")(self.multiworld.state))
-
- self.multiworld.state.collect(fishing_level, event=True)
- self.multiworld.state.collect(fishing_level, event=True)
- self.multiworld.state.collect(fishing_level, event=True)
- self.multiworld.state.collect(fishing_level, event=True)
- self.multiworld.state.collect(fishing_level, event=True)
- self.assertTrue(self.world.logic.has("Sturgeon")(self.multiworld.state))
-
- self.remove(summer)
- self.assertFalse(self.world.logic.has("Sturgeon")(self.multiworld.state))
-
- winter = self.world.create_item("Winter")
- self.multiworld.state.collect(winter, event=True)
- self.assertTrue(self.world.logic.has("Sturgeon")(self.multiworld.state))
-
- self.remove(fishing_rod)
- self.assertFalse(self.world.logic.has("Sturgeon")(self.multiworld.state))
-
- def test_old_master_cannoli(self):
- self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=True)
- self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=True)
- self.multiworld.state.collect(self.world.create_item("Summer"), event=True)
-
- self.assertFalse(self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state))
-
- fall = self.world.create_item("Fall")
- self.multiworld.state.collect(fall, event=True)
- self.assertFalse(self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state))
-
- tuesday = self.world.create_item("Traveling Merchant: Tuesday")
- self.multiworld.state.collect(tuesday, event=True)
- self.assertFalse(self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state))
-
- rare_seed = self.world.create_item("Rare Seed")
- self.multiworld.state.collect(rare_seed, event=True)
- self.assertTrue(self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state))
-
- self.remove(fall)
- self.assertFalse(self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state))
- self.remove(tuesday)
-
- green_house = self.world.create_item("Greenhouse")
- self.multiworld.state.collect(green_house, event=True)
- self.assertFalse(self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state))
-
- friday = self.world.create_item("Traveling Merchant: Friday")
- self.multiworld.state.collect(friday, event=True)
- self.assertTrue(self.multiworld.get_location("Old Master Cannoli", 1).access_rule(self.multiworld.state))
-
- self.remove(green_house)
- self.assertFalse(self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state))
- self.remove(friday)
-
-
-class TestBundlesLogic(SVTestBase):
- options = {
- }
-
- def test_vault_2500g_bundle(self):
- self.assertTrue(self.world.logic.can_reach_location("2,500g Bundle")(self.multiworld.state))
-
-
-class TestBuildingLogic(SVTestBase):
- options = {
- options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_early_shipping_bin
- }
-
- def test_coop_blueprint(self):
- self.assertFalse(self.world.logic.can_reach_location("Coop Blueprint")(self.multiworld.state))
-
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.assertTrue(self.world.logic.can_reach_location("Coop Blueprint")(self.multiworld.state))
-
- def test_big_coop_blueprint(self):
- self.assertFalse(self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state),
- f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
-
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.assertFalse(self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state),
- f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
-
- self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=True)
- self.assertTrue(self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state),
- f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
-
- def test_deluxe_coop_blueprint(self):
- self.assertFalse(self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state))
-
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.assertFalse(self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state))
-
- self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=True)
- self.assertFalse(self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state))
-
- self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=True)
- self.assertTrue(self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state))
-
- def test_big_shed_blueprint(self):
- self.assertFalse(self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state),
- f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")
-
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.multiworld.state.collect(self.world.create_item("Month End"), event=True)
- self.assertFalse(self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state),
- f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")
-
- self.multiworld.state.collect(self.world.create_item("Progressive Shed"), event=True)
- self.assertTrue(self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state),
- f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")
-
-
-class TestArcadeMachinesLogic(SVTestBase):
- options = {
- options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling,
- }
-
- def test_prairie_king(self):
- self.assertFalse(self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state))
- self.assertFalse(self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state))
- self.assertFalse(self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state))
- self.assertFalse(self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
-
- boots = self.world.create_item("JotPK: Progressive Boots")
- gun = self.world.create_item("JotPK: Progressive Gun")
- ammo = self.world.create_item("JotPK: Progressive Ammo")
- life = self.world.create_item("JotPK: Extra Life")
- drop = self.world.create_item("JotPK: Increased Drop Rate")
-
- self.multiworld.state.collect(boots, event=True)
- self.multiworld.state.collect(gun, event=True)
- self.assertTrue(self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state))
- self.assertFalse(self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state))
- self.assertFalse(self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state))
- self.assertFalse(self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
- self.remove(boots)
- self.remove(gun)
-
- self.multiworld.state.collect(boots, event=True)
- self.multiworld.state.collect(boots, event=True)
- self.assertTrue(self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state))
- self.assertFalse(self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state))
- self.assertFalse(self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state))
- self.assertFalse(self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
- self.remove(boots)
- self.remove(boots)
-
- self.multiworld.state.collect(boots, event=True)
- self.multiworld.state.collect(gun, event=True)
- self.multiworld.state.collect(ammo, event=True)
- self.multiworld.state.collect(life, event=True)
- self.assertTrue(self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state))
- self.assertTrue(self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state))
- self.assertFalse(self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state))
- self.assertFalse(self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
- self.remove(boots)
- self.remove(gun)
- self.remove(ammo)
- self.remove(life)
-
- self.multiworld.state.collect(boots, event=True)
- self.multiworld.state.collect(gun, event=True)
- self.multiworld.state.collect(gun, event=True)
- self.multiworld.state.collect(ammo, event=True)
- self.multiworld.state.collect(ammo, event=True)
- self.multiworld.state.collect(life, event=True)
- self.multiworld.state.collect(drop, event=True)
- self.assertTrue(self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state))
- self.assertTrue(self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state))
- self.assertTrue(self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state))
- self.assertFalse(self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
- self.remove(boots)
- self.remove(gun)
- self.remove(gun)
- self.remove(ammo)
- self.remove(ammo)
- self.remove(life)
- self.remove(drop)
-
- self.multiworld.state.collect(boots, event=True)
- self.multiworld.state.collect(boots, event=True)
- self.multiworld.state.collect(gun, event=True)
- self.multiworld.state.collect(gun, event=True)
- self.multiworld.state.collect(gun, event=True)
- self.multiworld.state.collect(gun, event=True)
- self.multiworld.state.collect(ammo, event=True)
- self.multiworld.state.collect(ammo, event=True)
- self.multiworld.state.collect(ammo, event=True)
- self.multiworld.state.collect(life, event=True)
- self.multiworld.state.collect(drop, event=True)
- self.assertTrue(self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state))
- self.assertTrue(self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state))
- self.assertTrue(self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state))
- self.assertTrue(self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
- self.remove(boots)
- self.remove(boots)
- self.remove(gun)
- self.remove(gun)
- self.remove(gun)
- self.remove(gun)
- self.remove(ammo)
- self.remove(ammo)
- self.remove(ammo)
- self.remove(life)
- self.remove(drop)
-
-
-class TestWeaponsLogic(SVTestBase):
- options = {
- options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
- options.SkillProgression.internal_name: options.SkillProgression.option_progressive,
- }
-
- def test_mine(self):
- self.collect(self.world.create_item("Adventurer's Guild"))
- self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True)
- self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True)
- self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True)
- self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True)
- self.collect([self.world.create_item("Combat Level")] * 10)
- self.collect([self.world.create_item("Progressive Mine Elevator")] * 24)
- self.multiworld.state.collect(self.world.create_item("Bus Repair"), event=True)
- self.multiworld.state.collect(self.world.create_item("Skull Key"), event=True)
-
- self.GiveItemAndCheckReachableMine("Rusty Sword", 1)
- self.GiveItemAndCheckReachableMine("Wooden Blade", 1)
- self.GiveItemAndCheckReachableMine("Elf Blade", 1)
-
- self.GiveItemAndCheckReachableMine("Silver Saber", 2)
- self.GiveItemAndCheckReachableMine("Crystal Dagger", 2)
-
- self.GiveItemAndCheckReachableMine("Claymore", 3)
- self.GiveItemAndCheckReachableMine("Obsidian Edge", 3)
- self.GiveItemAndCheckReachableMine("Bone Sword", 3)
-
- self.GiveItemAndCheckReachableMine("The Slammer", 4)
- self.GiveItemAndCheckReachableMine("Lava Katana", 4)
-
- self.GiveItemAndCheckReachableMine("Galaxy Sword", 5)
- self.GiveItemAndCheckReachableMine("Galaxy Hammer", 5)
- self.GiveItemAndCheckReachableMine("Galaxy Dagger", 5)
-
- def GiveItemAndCheckReachableMine(self, item_name: str, reachable_level: int):
- item = self.multiworld.create_item(item_name, self.player)
- self.multiworld.state.collect(item, event=True)
- if reachable_level > 0:
- self.assertTrue(self.world.logic.can_mine_in_the_mines_floor_1_40()(self.multiworld.state))
- else:
- self.assertFalse(self.world.logic.can_mine_in_the_mines_floor_1_40()(self.multiworld.state))
-
- if reachable_level > 1:
- self.assertTrue(self.world.logic.can_mine_in_the_mines_floor_41_80()(self.multiworld.state))
- else:
- self.assertFalse(self.world.logic.can_mine_in_the_mines_floor_41_80()(self.multiworld.state))
-
- if reachable_level > 2:
- self.assertTrue(self.world.logic.can_mine_in_the_mines_floor_81_120()(self.multiworld.state))
- else:
- self.assertFalse(self.world.logic.can_mine_in_the_mines_floor_81_120()(self.multiworld.state))
-
- if reachable_level > 3:
- self.assertTrue(self.world.logic.can_mine_in_the_skull_cavern()(self.multiworld.state))
- else:
- self.assertFalse(self.world.logic.can_mine_in_the_skull_cavern()(self.multiworld.state))
-
- if reachable_level > 4:
- self.assertTrue(self.world.logic.can_mine_perfectly_in_the_skull_cavern()(self.multiworld.state))
- else:
- self.assertFalse(self.world.logic.can_mine_perfectly_in_the_skull_cavern()(self.multiworld.state))
-
- self.remove(item)
-
-
-class TestRecipeLogic(SVTestBase):
- options = {
- options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive,
- options.SkillProgression.internal_name: options.SkillProgression.option_progressive,
- options.Cropsanity.internal_name: options.Cropsanity.option_shuffled,
- }
-
- # I wanted to make a test for different ways to obtain a pizza, but I'm stuck not knowing how to block the immediate purchase from Gus
- # def test_pizza(self):
- # world = self.world
- # logic = world.logic
- # multiworld = self.multiworld
- #
- # self.assertTrue(logic.has(Ingredient.wheat_flour)(multiworld.state))
- # self.assertTrue(logic.can_spend_money_at(Region.saloon, 150)(multiworld.state))
- # self.assertFalse(logic.has(Meal.pizza)(multiworld.state))
- #
- # self.assertFalse(logic.can_cook()(multiworld.state))
- # self.collect(world.create_item("Progressive House"))
- # self.assertTrue(logic.can_cook()(multiworld.state))
- # self.assertFalse(logic.has(Meal.pizza)(multiworld.state))
- #
- # self.assertFalse(logic.has(Seed.tomato)(multiworld.state))
- # self.collect(world.create_item(Seed.tomato))
- # self.assertTrue(logic.has(Seed.tomato)(multiworld.state))
- # self.assertFalse(logic.has(Meal.pizza)(multiworld.state))
- #
- # self.assertFalse(logic.has(Vegetable.tomato)(multiworld.state))
- # self.collect(world.create_item(Season.summer))
- # self.assertTrue(logic.has(Vegetable.tomato)(multiworld.state))
- # self.assertFalse(logic.has(Meal.pizza)(multiworld.state))
- #
- # self.assertFalse(logic.has(Animal.cow)(multiworld.state))
- # self.assertFalse(logic.has(AnimalProduct.cow_milk)(multiworld.state))
- # self.collect(world.create_item("Progressive Barn"))
- # self.assertTrue(logic.has(Animal.cow)(multiworld.state))
- # self.assertTrue(logic.has(AnimalProduct.cow_milk)(multiworld.state))
- # self.assertFalse(logic.has(Meal.pizza)(multiworld.state))
- #
- # self.assertFalse(logic.has(Machine.cheese_press)(self.multiworld.state))
- # self.assertFalse(logic.has(ArtisanGood.cheese)(self.multiworld.state))
- # self.collect(world.create_item(item) for item in ["Farming Level"] * 6)
- # self.collect(world.create_item(item) for item in ["Progressive Axe"] * 2)
- # self.assertTrue(logic.has(Machine.cheese_press)(self.multiworld.state))
- # self.assertTrue(logic.has(ArtisanGood.cheese)(self.multiworld.state))
- # self.assertTrue(logic.has(Meal.pizza)(self.multiworld.state))
-
-
-class TestDonationLogicAll(SVTestBase):
- options = {
- options.Museumsanity.internal_name: options.Museumsanity.option_all
- }
-
- def test_cannot_make_any_donation_without_museum_access(self):
- guild_item = "Adventurer's Guild"
- swap_museum_and_guild(self.multiworld, self.player)
- collect_all_except(self.multiworld, guild_item)
-
- for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]:
- self.assertFalse(self.world.logic.can_reach_location(donation.name)(self.multiworld.state))
-
- self.multiworld.state.collect(self.world.create_item(guild_item), event=True)
-
- for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]:
- self.assertTrue(self.world.logic.can_reach_location(donation.name)(self.multiworld.state))
-
-
-class TestDonationLogicRandomized(SVTestBase):
- options = {
- options.Museumsanity.internal_name: options.Museumsanity.option_randomized
- }
-
- def test_cannot_make_any_donation_without_museum_access(self):
- guild_item = "Adventurer's Guild"
- swap_museum_and_guild(self.multiworld, self.player)
- collect_all_except(self.multiworld, guild_item)
- donation_locations = [location for location in self.multiworld.get_locations() if not location.event and LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags]
-
- for donation in donation_locations:
- self.assertFalse(self.world.logic.can_reach_location(donation.name)(self.multiworld.state))
-
- self.multiworld.state.collect(self.world.create_item(guild_item), event=True)
-
- for donation in donation_locations:
- self.assertTrue(self.world.logic.can_reach_location(donation.name)(self.multiworld.state))
-
-
-class TestDonationLogicMilestones(SVTestBase):
- options = {
- options.Museumsanity.internal_name: options.Museumsanity.option_milestones
- }
-
- def test_cannot_make_any_donation_without_museum_access(self):
- guild_item = "Adventurer's Guild"
- swap_museum_and_guild(self.multiworld, self.player)
- collect_all_except(self.multiworld, guild_item)
-
- for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]:
- self.assertFalse(self.world.logic.can_reach_location(donation.name)(self.multiworld.state))
-
- self.multiworld.state.collect(self.world.create_item(guild_item), event=True)
-
- for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]:
- self.assertTrue(self.world.logic.can_reach_location(donation.name)(self.multiworld.state))
-
-
-def swap_museum_and_guild(multiworld, player):
- museum_region = multiworld.get_region(Region.museum, player)
- guild_region = multiworld.get_region(Region.adventurer_guild, player)
- museum_entrance = multiworld.get_entrance(Entrance.town_to_museum, player)
- guild_entrance = multiworld.get_entrance(Entrance.mountain_to_adventurer_guild, player)
- museum_entrance.connect(guild_region)
- guild_entrance.connect(museum_region)
-
-
-def collect_all_except(multiworld, item_to_not_collect: str):
- for item in multiworld.get_items():
- if item.name != item_to_not_collect:
- multiworld.state.collect(item)
-
-
-class TestFriendsanityDatingRules(SVTestBase):
- options = {
- options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized_not_winter,
- options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
- options.FriendsanityHeartSize.internal_name: 3
- }
-
- def test_earning_dating_heart_requires_dating(self):
- month_name = "Month End"
- for i in range(12):
- month_item = self.world.create_item(month_name)
- self.multiworld.state.collect(month_item, event=True)
- self.multiworld.state.collect(self.world.create_item("Beach Bridge"), event=False)
- self.multiworld.state.collect(self.world.create_item("Progressive House"), event=False)
- self.multiworld.state.collect(self.world.create_item("Adventurer's Guild"), event=False)
- self.multiworld.state.collect(self.world.create_item("Galaxy Hammer"), event=False)
- for i in range(3):
- self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=False)
- self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=False)
- self.multiworld.state.collect(self.world.create_item("Progressive Barn"), event=False)
- for i in range(10):
- self.multiworld.state.collect(self.world.create_item("Foraging Level"), event=False)
- self.multiworld.state.collect(self.world.create_item("Farming Level"), event=False)
- self.multiworld.state.collect(self.world.create_item("Mining Level"), event=False)
- self.multiworld.state.collect(self.world.create_item("Combat Level"), event=False)
- self.multiworld.state.collect(self.world.create_item("Progressive Mine Elevator"), event=False)
- self.multiworld.state.collect(self.world.create_item("Progressive Mine Elevator"), event=False)
-
- npc = "Abigail"
- heart_name = f"{npc} <3"
- step = 3
-
- self.assert_can_reach_heart_up_to(npc, 3, step)
- self.multiworld.state.collect(self.world.create_item(heart_name), event=False)
- self.assert_can_reach_heart_up_to(npc, 6, step)
- self.multiworld.state.collect(self.world.create_item(heart_name), event=False)
- self.assert_can_reach_heart_up_to(npc, 8, step)
- self.multiworld.state.collect(self.world.create_item(heart_name), event=False)
- self.assert_can_reach_heart_up_to(npc, 10, step)
- self.multiworld.state.collect(self.world.create_item(heart_name), event=False)
- self.assert_can_reach_heart_up_to(npc, 14, step)
-
- def assert_can_reach_heart_up_to(self, npc: str, max_reachable: int, step: int):
- prefix = "Friendsanity: "
- suffix = " <3"
- for i in range(1, max_reachable + 1):
- if i % step != 0 and i != 14:
- continue
- location = f"{prefix}{npc} {i}{suffix}"
- can_reach = self.world.logic.can_reach_location(location)(self.multiworld.state)
- self.assertTrue(can_reach, f"Should be able to earn relationship up to {i} hearts")
- for i in range(max_reachable + 1, 14 + 1):
- if i % step != 0 and i != 14:
- continue
- location = f"{prefix}{npc} {i}{suffix}"
- can_reach = self.world.logic.can_reach_location(location)(self.multiworld.state)
- self.assertFalse(can_reach, f"Should not be able to earn relationship up to {i} hearts")
-
diff --git a/worlds/stardew_valley/test/TestStardewRule.py b/worlds/stardew_valley/test/TestStardewRule.py
new file mode 100644
index 000000000000..93b32b0d8ab4
--- /dev/null
+++ b/worlds/stardew_valley/test/TestStardewRule.py
@@ -0,0 +1,305 @@
+import unittest
+from typing import cast
+from unittest.mock import MagicMock, Mock
+
+from .. import StardewRule
+from ..stardew_rule import Received, And, Or, HasProgressionPercent, false_, true_, Count
+
+
+class TestSimplification(unittest.TestCase):
+ """
+ Those feature of simplifying the rules when they are built have proven to improve the fill speed considerably.
+ """
+
+ def test_simplify_and_and_and(self):
+ rule = And(Received('Summer', 0, 1), Received('Fall', 0, 1)) & And(Received('Winter', 0, 1), Received('Spring', 0, 1))
+
+ self.assertEqual(And(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1), Received('Spring', 0, 1)), rule)
+
+ def test_simplify_and_in_and(self):
+ rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), And(Received('Winter', 0, 1), Received('Spring', 0, 1)))
+ self.assertEqual(And(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1), Received('Spring', 0, 1)), rule)
+
+ def test_simplify_duplicated_and(self):
+ # This only works because "Received"s are combinable.
+ rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), And(Received('Summer', 0, 1), Received('Fall', 0, 1)))
+ self.assertEqual(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), rule)
+
+ def test_simplify_or_or_or(self):
+ rule = Or(Received('Summer', 0, 1), Received('Fall', 0, 1)) | Or(Received('Winter', 0, 1), Received('Spring', 0, 1))
+ self.assertEqual(Or(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1), Received('Spring', 0, 1)), rule)
+
+ def test_simplify_or_in_or(self):
+ rule = Or(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), Or(Received('Winter', 0, 1), Received('Spring', 0, 1)))
+ self.assertEqual(Or(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1), Received('Spring', 0, 1)), rule)
+
+ def test_simplify_duplicated_or(self):
+ rule = Or(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), Or(Received('Summer', 0, 1), Received('Fall', 0, 1)))
+ self.assertEqual(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), rule)
+
+
+class TestHasProgressionPercentSimplification(unittest.TestCase):
+ def test_has_progression_percent_and_uses_max(self):
+ rule = HasProgressionPercent(1, 20) & HasProgressionPercent(1, 10)
+ self.assertEqual(rule, HasProgressionPercent(1, 20))
+
+ def test_has_progression_percent_or_uses_min(self):
+ rule = HasProgressionPercent(1, 20) | HasProgressionPercent(1, 10)
+ self.assertEqual(rule, HasProgressionPercent(1, 10))
+
+ def test_and_between_progression_percent_and_other_progression_percent_uses_max(self):
+ cases = [
+ And(HasProgressionPercent(1, 10)) & HasProgressionPercent(1, 20),
+ HasProgressionPercent(1, 10) & And(HasProgressionPercent(1, 20)),
+ And(HasProgressionPercent(1, 20)) & And(HasProgressionPercent(1, 10)),
+ ]
+ for i, case in enumerate(cases):
+ with self.subTest(f"{i} {repr(case)}"):
+ self.assertEqual(case, And(HasProgressionPercent(1, 20)))
+
+ def test_or_between_progression_percent_and_other_progression_percent_uses_max(self):
+ cases = [
+ Or(HasProgressionPercent(1, 10)) | HasProgressionPercent(1, 20),
+ HasProgressionPercent(1, 10) | Or(HasProgressionPercent(1, 20)),
+ Or(HasProgressionPercent(1, 20)) | Or(HasProgressionPercent(1, 10))
+ ]
+ for i, case in enumerate(cases):
+ with self.subTest(f"{i} {repr(case)}"):
+ self.assertEqual(case, Or(HasProgressionPercent(1, 10)))
+
+
+class TestEvaluateWhileSimplifying(unittest.TestCase):
+ def test_propagate_evaluate_while_simplifying(self):
+ expected_result = True
+ collection_state = MagicMock()
+ other_rule = MagicMock()
+ other_rule.evaluate_while_simplifying = Mock(return_value=(other_rule, expected_result))
+ rule = And(Or(cast(StardewRule, other_rule)))
+
+ _, actual_result = rule.evaluate_while_simplifying(collection_state)
+
+ other_rule.evaluate_while_simplifying.assert_called_with(collection_state)
+ self.assertEqual(expected_result, actual_result)
+
+ def test_return_complement_when_its_found(self):
+ expected_simplified = false_
+ expected_result = False
+ collection_state = MagicMock()
+ rule = And(expected_simplified)
+
+ actual_simplified, actual_result = rule.evaluate_while_simplifying(collection_state)
+
+ self.assertEqual(expected_result, actual_result)
+ self.assertEqual(expected_simplified, actual_simplified)
+
+ def test_short_circuit_when_complement_found(self):
+ collection_state = MagicMock()
+ other_rule = MagicMock()
+ rule = Or(true_, )
+
+ rule.evaluate_while_simplifying(collection_state)
+
+ other_rule.evaluate_while_simplifying.assert_not_called()
+
+ def test_short_circuit_when_combinable_rules_is_false(self):
+ collection_state = MagicMock()
+ collection_state.has = Mock(return_value=False)
+ other_rule = MagicMock()
+ rule = And(Received("Potato", 1, 10), cast(StardewRule, other_rule))
+
+ rule.evaluate_while_simplifying(collection_state)
+
+ other_rule.evaluate_while_simplifying.assert_not_called()
+
+ def test_identity_is_removed_from_other_rules(self):
+ collection_state = MagicMock()
+ rule = Or(false_, Received("Potato", 1, 10))
+
+ rule.evaluate_while_simplifying(collection_state)
+
+ self.assertEqual(1, len(rule.current_rules))
+ self.assertIn(Received("Potato", 1, 10), rule.current_rules)
+
+ def test_complement_replaces_combinable_rules(self):
+ collection_state = MagicMock()
+ rule = Or(Received("Potato", 1, 10), true_)
+
+ rule.evaluate_while_simplifying(collection_state)
+
+ self.assertTrue(rule.current_rules)
+
+ def test_simplifying_to_complement_propagates_complement(self):
+ expected_simplified = true_
+ expected_result = True
+ collection_state = MagicMock()
+ rule = Or(Or(expected_simplified), Received("Potato", 1, 10))
+
+ actual_simplified, actual_result = rule.evaluate_while_simplifying(collection_state)
+
+ self.assertEqual(expected_result, actual_result)
+ self.assertEqual(expected_simplified, actual_simplified)
+ self.assertTrue(rule.current_rules)
+
+ def test_already_simplified_rules_are_not_simplified_again(self):
+ collection_state = MagicMock()
+ other_rule = MagicMock()
+ other_rule.evaluate_while_simplifying = Mock(return_value=(other_rule, False))
+ rule = Or(cast(StardewRule, other_rule), Received("Potato", 1, 10))
+
+ rule.evaluate_while_simplifying(collection_state)
+ other_rule.assert_not_called()
+ other_rule.evaluate_while_simplifying.reset_mock()
+
+ rule.evaluate_while_simplifying(collection_state)
+ other_rule.assert_called_with(collection_state)
+ other_rule.evaluate_while_simplifying.assert_not_called()
+
+ def test_continue_simplification_after_short_circuited(self):
+ collection_state = MagicMock()
+ a_rule = MagicMock()
+ a_rule.evaluate_while_simplifying = Mock(return_value=(a_rule, False))
+ another_rule = MagicMock()
+ another_rule.evaluate_while_simplifying = Mock(return_value=(another_rule, False))
+ rule = And(cast(StardewRule, a_rule), cast(StardewRule, another_rule))
+
+ rule.evaluate_while_simplifying(collection_state)
+ # This test is completely messed up because sets are used internally and order of the rules cannot be ensured.
+ not_yet_simplified, already_simplified = (another_rule, a_rule) if a_rule.evaluate_while_simplifying.called else (a_rule, another_rule)
+ not_yet_simplified.evaluate_while_simplifying.assert_not_called()
+ already_simplified.return_value = True
+
+ rule.evaluate_while_simplifying(collection_state)
+ not_yet_simplified.evaluate_while_simplifying.assert_called_with(collection_state)
+
+
+class TestEvaluateWhileSimplifyingDoubleCalls(unittest.TestCase):
+ """
+ So, there is a situation where a rule kind of calls itself while it's being evaluated, because its evaluation triggers a region cache refresh.
+
+ The region cache check every entrance, so if a rule is also used in an entrances, it can be reevaluated.
+
+ For instance, but not limited to
+ Has Melon -> (Farm & Summer) | Greenhouse -> Greenhouse triggers an update of the region cache
+ -> Every entrance are evaluated, for instance "can start farming" -> Look that any crop can be grown (calls Has Melon).
+ """
+
+ def test_nested_call_in_the_internal_rule_being_evaluated_does_check_the_internal_rule(self):
+ collection_state = MagicMock()
+ internal_rule = MagicMock()
+ rule = Or(cast(StardewRule, internal_rule))
+
+ called_once = False
+ internal_call_result = None
+
+ def first_call_to_internal_rule(state):
+ nonlocal internal_call_result
+ nonlocal called_once
+ if called_once:
+ return internal_rule, True
+ called_once = True
+
+ _, internal_call_result = rule.evaluate_while_simplifying(state)
+ internal_rule.evaluate_while_simplifying = Mock(return_value=(internal_rule, True))
+ return internal_rule, True
+
+ internal_rule.evaluate_while_simplifying = first_call_to_internal_rule
+
+ rule.evaluate_while_simplifying(collection_state)
+
+ self.assertTrue(called_once)
+ self.assertTrue(internal_call_result)
+
+ def test_nested_call_to_already_simplified_rule_does_not_steal_rule_to_simplify_from_parent_call(self):
+ collection_state = MagicMock()
+ an_internal_rule = MagicMock()
+ an_internal_rule.evaluate_while_simplifying = Mock(return_value=(an_internal_rule, True))
+ another_internal_rule = MagicMock()
+ another_internal_rule.evaluate_while_simplifying = Mock(return_value=(another_internal_rule, True))
+ rule = Or(cast(StardewRule, an_internal_rule), cast(StardewRule, another_internal_rule))
+
+ rule.evaluate_while_simplifying(collection_state)
+ # This test is completely messed up because sets are used internally and order of the rules cannot be ensured.
+ if an_internal_rule.evaluate_while_simplifying.called:
+ not_yet_simplified, already_simplified = another_internal_rule, an_internal_rule
+ else:
+ not_yet_simplified, already_simplified = an_internal_rule, another_internal_rule
+
+ called_once = False
+ internal_call_result = None
+
+ def call_to_already_simplified(state):
+ nonlocal internal_call_result
+ nonlocal called_once
+ if called_once:
+ return False
+ called_once = True
+
+ _, internal_call_result = rule.evaluate_while_simplifying(state)
+ return False
+
+ already_simplified.side_effect = call_to_already_simplified
+ not_yet_simplified.return_value = True
+
+ _, actual_result = rule.evaluate_while_simplifying(collection_state)
+
+ self.assertTrue(called_once)
+ self.assertTrue(internal_call_result)
+ self.assertTrue(actual_result)
+
+
+class TestCount(unittest.TestCase):
+
+ def test_duplicate_rule_count_double(self):
+ expected_result = True
+ collection_state = MagicMock()
+ simplified_rule = Mock()
+ other_rule = Mock(spec=StardewRule)
+ other_rule.evaluate_while_simplifying = Mock(return_value=(simplified_rule, expected_result))
+ rule = Count([cast(StardewRule, other_rule), other_rule, other_rule], 2)
+
+ actual_result = rule(collection_state)
+
+ other_rule.evaluate_while_simplifying.assert_called_once_with(collection_state)
+ self.assertEqual(expected_result, actual_result)
+
+ def test_simplified_rule_is_reused(self):
+ expected_result = False
+ collection_state = MagicMock()
+ simplified_rule = Mock(return_value=expected_result)
+ other_rule = Mock(spec=StardewRule)
+ other_rule.evaluate_while_simplifying = Mock(return_value=(simplified_rule, expected_result))
+ rule = Count([cast(StardewRule, other_rule), cast(StardewRule, other_rule), cast(StardewRule, other_rule)], 2)
+
+ actual_result = rule(collection_state)
+
+ other_rule.evaluate_while_simplifying.assert_called_once_with(collection_state)
+ self.assertEqual(expected_result, actual_result)
+
+ other_rule.evaluate_while_simplifying.reset_mock()
+
+ actual_result = rule(collection_state)
+
+ other_rule.evaluate_while_simplifying.assert_not_called()
+ simplified_rule.assert_called()
+ self.assertEqual(expected_result, actual_result)
+
+ def test_break_if_not_enough_rule_to_complete(self):
+ expected_result = False
+ collection_state = MagicMock()
+ simplified_rule = Mock()
+ never_called_rule = Mock()
+ other_rule = Mock(spec=StardewRule)
+ other_rule.evaluate_while_simplifying = Mock(return_value=(simplified_rule, expected_result))
+ rule = Count([cast(StardewRule, other_rule)] * 4, 2)
+
+ actual_result = rule(collection_state)
+
+ other_rule.evaluate_while_simplifying.assert_called_once_with(collection_state)
+ never_called_rule.assert_not_called()
+ never_called_rule.evaluate_while_simplifying.assert_not_called()
+ self.assertEqual(expected_result, actual_result)
+
+ def test_evaluate_without_shortcircuit_when_rules_are_all_different(self):
+ rule = Count([cast(StardewRule, Mock()) for i in range(5)], 2)
+
+ self.assertEqual(rule.evaluate, rule.evaluate_without_shortcircuit)
diff --git a/worlds/stardew_valley/test/TestStartInventory.py b/worlds/stardew_valley/test/TestStartInventory.py
new file mode 100644
index 000000000000..dc44a1bb4598
--- /dev/null
+++ b/worlds/stardew_valley/test/TestStartInventory.py
@@ -0,0 +1,41 @@
+from . import SVTestBase
+from .assertion import WorldAssertMixin
+from .. import options
+
+
+class TestStartInventoryAllsanity(WorldAssertMixin, SVTestBase):
+ options = {
+ "accessibility": "items",
+ options.Goal.internal_name: options.Goal.option_allsanity,
+ options.BundleRandomization.internal_name: options.BundleRandomization.option_remixed,
+ options.BundlePrice.internal_name: options.BundlePrice.option_minimum,
+ options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized,
+ options.Cropsanity.internal_name: options.Cropsanity.option_enabled,
+ options.ToolProgression.internal_name: options.ToolProgression.option_progressive_very_cheap,
+ options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive_from_previous_floor,
+ options.SkillProgression.internal_name: options.SkillProgression.option_progressive,
+ options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_very_cheap,
+ options.FestivalLocations.internal_name: options.FestivalLocations.option_easy,
+ options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled,
+ options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board,
+ options.QuestLocations.internal_name: -1,
+ options.Fishsanity.internal_name: options.Fishsanity.option_only_easy_fish,
+ options.Museumsanity.internal_name: options.Museumsanity.option_randomized,
+ options.Monstersanity.internal_name: options.Monstersanity.option_one_per_category,
+ options.Shipsanity.internal_name: options.Shipsanity.option_crops,
+ options.Cooksanity.internal_name: options.Cooksanity.option_queen_of_sauce,
+ options.Chefsanity.internal_name: 0b1001,
+ options.Craftsanity.internal_name: options.Craftsanity.option_all,
+ options.Friendsanity.internal_name: options.Friendsanity.option_bachelors,
+ options.FriendsanityHeartSize.internal_name: 3,
+ options.NumberOfMovementBuffs.internal_name: 10,
+ options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
+ options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
+ options.Mods.internal_name: ["Tractor Mod", "Bigger Backpack", "Luck Skill", "Magic", "Socializing Skill", "Archaeology", "Cooking Skill",
+ "Binning Skill"],
+ "start_inventory": {"Progressive Pickaxe": 2}
+ }
+
+ def test_start_inventory_progression_items_does_not_break_progression_percent(self):
+ self.assert_basic_checks_with_subtests(self.multiworld)
+ self.assert_can_win(self.multiworld)
diff --git a/worlds/stardew_valley/test/TestWalnutsanity.py b/worlds/stardew_valley/test/TestWalnutsanity.py
new file mode 100644
index 000000000000..e1ab348def41
--- /dev/null
+++ b/worlds/stardew_valley/test/TestWalnutsanity.py
@@ -0,0 +1,209 @@
+from . import SVTestBase
+from ..options import ExcludeGingerIsland, Walnutsanity
+from ..strings.ap_names.ap_option_names import OptionName
+
+
+class TestWalnutsanityNone(SVTestBase):
+ options = {
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false,
+ Walnutsanity: Walnutsanity.preset_none,
+ }
+
+ def test_no_walnut_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertNotIn("Open Golden Coconut", location_names)
+ self.assertNotIn("Fishing Walnut 4", location_names)
+ self.assertNotIn("Journal Scrap #6", location_names)
+ self.assertNotIn("Starfish Triangle", location_names)
+ self.assertNotIn("Bush Behind Coconut Tree", location_names)
+ self.assertNotIn("Purple Starfish Island Survey", location_names)
+ self.assertNotIn("Volcano Monsters Walnut 3", location_names)
+ self.assertNotIn("Cliff Over Island South Bush", location_names)
+
+ def test_logic_received_walnuts(self):
+ # You need to receive 0, and collect 40
+ self.collect("Island Obelisk")
+ self.collect("Island West Turtle")
+ self.collect("Progressive House")
+ items = self.collect("5 Golden Walnuts", 10)
+
+ self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
+ self.collect("Island North Turtle")
+ self.collect("Island Resort")
+ self.collect("Open Professor Snail Cave")
+ self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
+ self.collect("Dig Site Bridge")
+ self.collect("Island Farmhouse")
+ self.collect("Qi Walnut Room")
+ self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
+ self.collect("Combat Level", 10)
+ self.collect("Mining Level", 10)
+ self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
+ self.collect("Progressive Slingshot")
+ self.collect("Progressive Weapon", 5)
+ self.collect("Progressive Pickaxe", 4)
+ self.collect("Progressive Watering Can", 4)
+ self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player))
+
+
+class TestWalnutsanityPuzzles(SVTestBase):
+ options = {
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false,
+ Walnutsanity: frozenset({OptionName.walnutsanity_puzzles}),
+ }
+
+ def test_only_puzzle_walnut_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertIn("Open Golden Coconut", location_names)
+ self.assertNotIn("Fishing Walnut 4", location_names)
+ self.assertNotIn("Journal Scrap #6", location_names)
+ self.assertNotIn("Starfish Triangle", location_names)
+ self.assertNotIn("Bush Behind Coconut Tree", location_names)
+ self.assertIn("Purple Starfish Island Survey", location_names)
+ self.assertNotIn("Volcano Monsters Walnut 3", location_names)
+ self.assertNotIn("Cliff Over Island South Bush", location_names)
+
+ def test_field_office_locations_require_professor_snail(self):
+ location_names = ["Complete Large Animal Collection", "Complete Snake Collection", "Complete Mummified Frog Collection",
+ "Complete Mummified Bat Collection", "Purple Flowers Island Survey", "Purple Starfish Island Survey", ]
+ locations = [location for location in self.multiworld.get_locations() if location.name in location_names]
+ self.collect("Island Obelisk")
+ self.collect("Island North Turtle")
+ self.collect("Island West Turtle")
+ self.collect("Island Resort")
+ self.collect("Dig Site Bridge")
+ self.collect("Progressive House")
+ self.collect("Progressive Pan")
+ self.collect("Progressive Fishing Rod")
+ self.collect("Progressive Watering Can")
+ self.collect("Progressive Pickaxe", 4)
+ self.collect("Progressive Sword", 5)
+ self.collect("Combat Level", 10)
+ self.collect("Mining Level", 10)
+ for location in locations:
+ self.assert_reach_location_false(location, self.multiworld.state)
+ self.collect("Open Professor Snail Cave")
+ for location in locations:
+ self.assert_reach_location_true(location, self.multiworld.state)
+
+
+class TestWalnutsanityBushes(SVTestBase):
+ options = {
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false,
+ Walnutsanity: frozenset({OptionName.walnutsanity_bushes}),
+ }
+
+ def test_only_bush_walnut_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertNotIn("Open Golden Coconut", location_names)
+ self.assertNotIn("Fishing Walnut 4", location_names)
+ self.assertNotIn("Journal Scrap #6", location_names)
+ self.assertNotIn("Starfish Triangle", location_names)
+ self.assertIn("Bush Behind Coconut Tree", location_names)
+ self.assertNotIn("Purple Starfish Island Survey", location_names)
+ self.assertNotIn("Volcano Monsters Walnut 3", location_names)
+ self.assertIn("Cliff Over Island South Bush", location_names)
+
+
+class TestWalnutsanityPuzzlesAndBushes(SVTestBase):
+ options = {
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false,
+ Walnutsanity: frozenset({OptionName.walnutsanity_puzzles, OptionName.walnutsanity_bushes}),
+ }
+
+ def test_only_bush_walnut_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertIn("Open Golden Coconut", location_names)
+ self.assertNotIn("Fishing Walnut 4", location_names)
+ self.assertNotIn("Journal Scrap #6", location_names)
+ self.assertNotIn("Starfish Triangle", location_names)
+ self.assertIn("Bush Behind Coconut Tree", location_names)
+ self.assertIn("Purple Starfish Island Survey", location_names)
+ self.assertNotIn("Volcano Monsters Walnut 3", location_names)
+ self.assertIn("Cliff Over Island South Bush", location_names)
+
+ def test_logic_received_walnuts(self):
+ # You need to receive 25, and collect 15
+ self.collect("Island Obelisk")
+ self.collect("Island West Turtle")
+ items = self.collect("5 Golden Walnuts", 5)
+
+ self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
+ items = self.collect("Island North Turtle")
+ self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player))
+
+
+class TestWalnutsanityDigSpots(SVTestBase):
+ options = {
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false,
+ Walnutsanity: frozenset({OptionName.walnutsanity_dig_spots}),
+ }
+
+ def test_only_dig_spots_walnut_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertNotIn("Open Golden Coconut", location_names)
+ self.assertNotIn("Fishing Walnut 4", location_names)
+ self.assertIn("Journal Scrap #6", location_names)
+ self.assertIn("Starfish Triangle", location_names)
+ self.assertNotIn("Bush Behind Coconut Tree", location_names)
+ self.assertNotIn("Purple Starfish Island Survey", location_names)
+ self.assertNotIn("Volcano Monsters Walnut 3", location_names)
+ self.assertNotIn("Cliff Over Island South Bush", location_names)
+
+
+class TestWalnutsanityRepeatables(SVTestBase):
+ options = {
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false,
+ Walnutsanity: frozenset({OptionName.walnutsanity_repeatables}),
+ }
+
+ def test_only_repeatable_walnut_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertNotIn("Open Golden Coconut", location_names)
+ self.assertIn("Fishing Walnut 4", location_names)
+ self.assertNotIn("Journal Scrap #6", location_names)
+ self.assertNotIn("Starfish Triangle", location_names)
+ self.assertNotIn("Bush Behind Coconut Tree", location_names)
+ self.assertNotIn("Purple Starfish Island Survey", location_names)
+ self.assertIn("Volcano Monsters Walnut 3", location_names)
+ self.assertNotIn("Cliff Over Island South Bush", location_names)
+
+
+class TestWalnutsanityAll(SVTestBase):
+ options = {
+ ExcludeGingerIsland: ExcludeGingerIsland.option_false,
+ Walnutsanity: Walnutsanity.preset_all,
+ }
+
+ def test_all_walnut_locations(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertIn("Open Golden Coconut", location_names)
+ self.assertIn("Fishing Walnut 4", location_names)
+ self.assertIn("Journal Scrap #6", location_names)
+ self.assertIn("Starfish Triangle", location_names)
+ self.assertIn("Bush Behind Coconut Tree", location_names)
+ self.assertIn("Purple Starfish Island Survey", location_names)
+ self.assertIn("Volcano Monsters Walnut 3", location_names)
+ self.assertIn("Cliff Over Island South Bush", location_names)
+
+ def test_logic_received_walnuts(self):
+ # You need to receive 40, and collect 4
+ self.collect("Island Obelisk")
+ self.collect("Island West Turtle")
+ self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
+ items = self.collect("5 Golden Walnuts", 8)
+ self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player))
+ self.remove(items)
+ self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
+ items = self.collect("3 Golden Walnuts", 14)
+ self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player))
+ self.remove(items)
+ self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
+ items = self.collect("Golden Walnut", 40)
+ self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player))
+ self.remove(items)
+ self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player))
+ items = self.collect("5 Golden Walnuts", 4)
+ items = self.collect("3 Golden Walnuts", 6)
+ items = self.collect("Golden Walnut", 2)
+ self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player))
diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py
index 59da64c1d77a..7e82ea91e434 100644
--- a/worlds/stardew_valley/test/__init__.py
+++ b/worlds/stardew_valley/test/__init__.py
@@ -1,122 +1,462 @@
+import logging
import os
+import threading
+import unittest
from argparse import Namespace
-from typing import Dict, FrozenSet, Tuple, Any, ClassVar
+from contextlib import contextmanager
+from typing import Dict, ClassVar, Iterable, Tuple, Optional, List, Union, Any
-from BaseClasses import MultiWorld
-from test.TestBase import WorldTestBase
+from BaseClasses import MultiWorld, CollectionState, PlandoOptions, get_seed, Location, Item, ItemClassification
+from Options import VerifyKeys
+from test.bases import WorldTestBase
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
-from .. import StardewValleyWorld, options
-from ..mods.mod_data import ModNames
from worlds.AutoWorld import call_all
+from .assertion import RuleAssertMixin
+from .. import StardewValleyWorld, options, StardewItem
+from ..options import StardewValleyOptions, StardewValleyOption
+logger = logging.getLogger(__name__)
-class SVTestBase(WorldTestBase):
+DEFAULT_TEST_SEED = get_seed()
+logger.info(f"Default Test Seed: {DEFAULT_TEST_SEED}")
+
+
+def default_6_x_x():
+ return {
+ options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.default,
+ options.BackpackProgression.internal_name: options.BackpackProgression.default,
+ options.Booksanity.internal_name: options.Booksanity.default,
+ options.BuildingProgression.internal_name: options.BuildingProgression.default,
+ options.BundlePrice.internal_name: options.BundlePrice.default,
+ options.BundleRandomization.internal_name: options.BundleRandomization.default,
+ options.Chefsanity.internal_name: options.Chefsanity.default,
+ options.Cooksanity.internal_name: options.Cooksanity.default,
+ options.Craftsanity.internal_name: options.Craftsanity.default,
+ options.Cropsanity.internal_name: options.Cropsanity.default,
+ options.ElevatorProgression.internal_name: options.ElevatorProgression.default,
+ options.EntranceRandomization.internal_name: options.EntranceRandomization.default,
+ options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.default,
+ options.FestivalLocations.internal_name: options.FestivalLocations.default,
+ options.Fishsanity.internal_name: options.Fishsanity.default,
+ options.Friendsanity.internal_name: options.Friendsanity.default,
+ options.FriendsanityHeartSize.internal_name: options.FriendsanityHeartSize.default,
+ options.Goal.internal_name: options.Goal.default,
+ options.Mods.internal_name: options.Mods.default,
+ options.Monstersanity.internal_name: options.Monstersanity.default,
+ options.Museumsanity.internal_name: options.Museumsanity.default,
+ options.NumberOfMovementBuffs.internal_name: options.NumberOfMovementBuffs.default,
+ options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.default,
+ options.QuestLocations.internal_name: options.QuestLocations.default,
+ options.SeasonRandomization.internal_name: options.SeasonRandomization.default,
+ options.Shipsanity.internal_name: options.Shipsanity.default,
+ options.SkillProgression.internal_name: options.SkillProgression.default,
+ options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.default,
+ options.ToolProgression.internal_name: options.ToolProgression.default,
+ options.TrapItems.internal_name: options.TrapItems.default,
+ options.Walnutsanity.internal_name: options.Walnutsanity.default
+ }
+
+
+def allsanity_no_mods_6_x_x():
+ return {
+ options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling,
+ options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive,
+ options.Booksanity.internal_name: options.Booksanity.option_all,
+ options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive,
+ options.BundlePrice.internal_name: options.BundlePrice.option_expensive,
+ options.BundleRandomization.internal_name: options.BundleRandomization.option_thematic,
+ options.Chefsanity.internal_name: options.Chefsanity.option_all,
+ options.Cooksanity.internal_name: options.Cooksanity.option_all,
+ options.Craftsanity.internal_name: options.Craftsanity.option_all,
+ options.Cropsanity.internal_name: options.Cropsanity.option_enabled,
+ options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive,
+ options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
+ options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
+ options.FestivalLocations.internal_name: options.FestivalLocations.option_hard,
+ options.Fishsanity.internal_name: options.Fishsanity.option_all,
+ options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
+ options.FriendsanityHeartSize.internal_name: 1,
+ options.Goal.internal_name: options.Goal.option_perfection,
+ options.Mods.internal_name: frozenset(),
+ options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals,
+ options.Museumsanity.internal_name: options.Museumsanity.option_all,
+ options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
+ options.NumberOfMovementBuffs.internal_name: 12,
+ options.QuestLocations.internal_name: 56,
+ options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized,
+ options.Shipsanity.internal_name: options.Shipsanity.option_everything,
+ options.SkillProgression.internal_name: options.SkillProgression.option_progressive,
+ options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
+ options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
+ options.TrapItems.internal_name: options.TrapItems.option_nightmare,
+ options.Walnutsanity.internal_name: options.Walnutsanity.preset_all
+ }
+
+
+def allsanity_mods_6_x_x():
+ allsanity = allsanity_no_mods_6_x_x()
+ allsanity.update({options.Mods.internal_name: frozenset(options.Mods.valid_keys)})
+ return allsanity
+
+
+def get_minsanity_options():
+ return {
+ options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled,
+ options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla,
+ options.Booksanity.internal_name: options.Booksanity.option_none,
+ options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla,
+ options.BundlePrice.internal_name: options.BundlePrice.option_very_cheap,
+ options.BundleRandomization.internal_name: options.BundleRandomization.option_vanilla,
+ options.Chefsanity.internal_name: options.Chefsanity.option_none,
+ options.Cooksanity.internal_name: options.Cooksanity.option_none,
+ options.Craftsanity.internal_name: options.Craftsanity.option_none,
+ options.Cropsanity.internal_name: options.Cropsanity.option_disabled,
+ options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
+ options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
+ options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
+ options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled,
+ options.Fishsanity.internal_name: options.Fishsanity.option_none,
+ options.Friendsanity.internal_name: options.Friendsanity.option_none,
+ options.FriendsanityHeartSize.internal_name: 8,
+ options.Goal.internal_name: options.Goal.option_bottom_of_the_mines,
+ options.Mods.internal_name: frozenset(),
+ options.Monstersanity.internal_name: options.Monstersanity.option_none,
+ options.Museumsanity.internal_name: options.Museumsanity.option_none,
+ options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_none,
+ options.NumberOfMovementBuffs.internal_name: 0,
+ options.QuestLocations.internal_name: -1,
+ options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled,
+ options.Shipsanity.internal_name: options.Shipsanity.option_none,
+ options.SkillProgression.internal_name: options.SkillProgression.option_vanilla,
+ options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla,
+ options.ToolProgression.internal_name: options.ToolProgression.option_vanilla,
+ options.TrapItems.internal_name: options.TrapItems.option_no_traps,
+ options.Walnutsanity.internal_name: options.Walnutsanity.preset_none
+ }
+
+
+def minimal_locations_maximal_items():
+ min_max_options = {
+ options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled,
+ options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla,
+ options.Booksanity.internal_name: options.Booksanity.option_none,
+ options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla,
+ options.BundlePrice.internal_name: options.BundlePrice.option_expensive,
+ options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled,
+ options.Chefsanity.internal_name: options.Chefsanity.option_none,
+ options.Cooksanity.internal_name: options.Cooksanity.option_none,
+ options.Craftsanity.internal_name: options.Craftsanity.option_none,
+ options.Cropsanity.internal_name: options.Cropsanity.option_disabled,
+ options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
+ options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
+ options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
+ options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled,
+ options.Fishsanity.internal_name: options.Fishsanity.option_none,
+ options.Friendsanity.internal_name: options.Friendsanity.option_none,
+ options.FriendsanityHeartSize.internal_name: 8,
+ options.Goal.internal_name: options.Goal.option_craft_master,
+ options.Mods.internal_name: frozenset(),
+ options.Monstersanity.internal_name: options.Monstersanity.option_none,
+ options.Museumsanity.internal_name: options.Museumsanity.option_none,
+ options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
+ options.NumberOfMovementBuffs.internal_name: 12,
+ options.QuestLocations.internal_name: -1,
+ options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized,
+ options.Shipsanity.internal_name: options.Shipsanity.option_none,
+ options.SkillProgression.internal_name: options.SkillProgression.option_vanilla,
+ options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla,
+ options.ToolProgression.internal_name: options.ToolProgression.option_vanilla,
+ options.TrapItems.internal_name: options.TrapItems.option_nightmare,
+ options.Walnutsanity.internal_name: options.Walnutsanity.preset_none
+ }
+ return min_max_options
+
+
+def minimal_locations_maximal_items_with_island():
+ min_max_options = minimal_locations_maximal_items()
+ min_max_options.update({options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false})
+ return min_max_options
+
+
+class SVTestCase(unittest.TestCase):
+ # Set False to not skip some 'extra' tests
+ skip_base_tests: bool = True
+ # Set False to run tests that take long
+ skip_long_tests: bool = True
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ super().setUpClass()
+ base_tests_key = "base"
+ if base_tests_key in os.environ:
+ cls.skip_base_tests = not bool(os.environ[base_tests_key])
+ long_tests_key = "long"
+ if long_tests_key in os.environ:
+ cls.skip_long_tests = not bool(os.environ[long_tests_key])
+
+ @contextmanager
+ def solo_world_sub_test(self, msg: Optional[str] = None,
+ /,
+ world_options: Optional[Dict[Union[str, StardewValleyOption], Any]] = None,
+ *,
+ seed=DEFAULT_TEST_SEED,
+ world_caching=True,
+ **kwargs) -> Tuple[MultiWorld, StardewValleyWorld]:
+ if msg is not None:
+ msg += " "
+ else:
+ msg = ""
+ msg += f"[Seed = {seed}]"
+
+ with self.subTest(msg, **kwargs):
+ with solo_multiworld(world_options, seed=seed, world_caching=world_caching) as (multiworld, world):
+ yield multiworld, world
+
+
+class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
game = "Stardew Valley"
world: StardewValleyWorld
player: ClassVar[int] = 1
- skip_long_tests: bool = True
+
+ seed = DEFAULT_TEST_SEED
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ if cls is SVTestBase:
+ raise unittest.SkipTest("No running tests on SVTestBase import.")
+
+ super().setUpClass()
def world_setup(self, *args, **kwargs):
- super().world_setup(*args, **kwargs)
- long_tests_key = "long"
- if long_tests_key in os.environ:
- self.skip_long_tests = not bool(os.environ[long_tests_key])
+ self.options = parse_class_option_keys(self.options)
+
+ self.multiworld = setup_solo_multiworld(self.options, seed=self.seed)
+ self.multiworld.lock.acquire()
+ world = self.multiworld.worlds[self.player]
+
+ self.original_state = self.multiworld.state.copy()
+ self.original_itempool = self.multiworld.itempool.copy()
+ self.original_prog_item_count = world.total_progression_items
+ self.unfilled_locations = self.multiworld.get_unfilled_locations(1)
if self.constructed:
- self.world = self.multiworld.worlds[self.player] # noqa
+ self.world = world # noqa
+
+ def tearDown(self) -> None:
+ self.multiworld.state = self.original_state
+ self.multiworld.itempool = self.original_itempool
+ for location in self.unfilled_locations:
+ location.item = None
+ self.world.total_progression_items = self.original_prog_item_count
+
+ self.multiworld.lock.release()
@property
def run_default_tests(self) -> bool:
- # world_setup is overridden, so it'd always run default tests when importing SVTestBase
- is_not_stardew_test = type(self) is not SVTestBase
- should_run_default_tests = is_not_stardew_test and super().run_default_tests
- return should_run_default_tests
-
- def minimal_locations_maximal_items(self):
- min_max_options = {
- options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized,
- options.Cropsanity.internal_name: options.Cropsanity.option_shuffled,
- options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla,
- options.ToolProgression.internal_name: options.ToolProgression.option_vanilla,
- options.SkillProgression.internal_name: options.SkillProgression.option_vanilla,
- options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla,
- options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
- options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled,
- options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled,
- options.HelpWantedLocations.internal_name: 0,
- options.Fishsanity.internal_name: options.Fishsanity.option_none,
- options.Museumsanity.internal_name: options.Museumsanity.option_none,
- options.Friendsanity.internal_name: options.Friendsanity.option_none,
- options.NumberOfMovementBuffs.internal_name: 12,
- options.NumberOfLuckBuffs.internal_name: 12,
- }
- return min_max_options
-
- def allsanity_options_without_mods(self):
- allsanity = {
- options.Goal.internal_name: options.Goal.option_perfection,
- options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled,
- options.BundlePrice.internal_name: options.BundlePrice.option_expensive,
- options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized,
- options.Cropsanity.internal_name: options.Cropsanity.option_shuffled,
- options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive,
- options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
- options.SkillProgression.internal_name: options.SkillProgression.option_progressive,
- options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive,
- options.FestivalLocations.internal_name: options.FestivalLocations.option_hard,
- options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive,
- options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling,
- options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
- options.HelpWantedLocations.internal_name: 56,
- options.Fishsanity.internal_name: options.Fishsanity.option_all,
- options.Museumsanity.internal_name: options.Museumsanity.option_all,
- options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
- options.FriendsanityHeartSize.internal_name: 1,
- options.NumberOfMovementBuffs.internal_name: 12,
- options.NumberOfLuckBuffs.internal_name: 12,
- options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
- options.TrapItems.internal_name: options.TrapItems.option_nightmare,
- }
- return allsanity
-
- def allsanity_options_with_mods(self):
- allsanity = {}
- allsanity.update(self.allsanity_options_without_mods())
- all_mods = (
- ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
- ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
- ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
- ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
- ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
- ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator
- )
- allsanity.update({options.Mods.internal_name: all_mods})
- return allsanity
+ if self.skip_base_tests:
+ return False
+ return super().run_default_tests
+
+ def collect_lots_of_money(self):
+ self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False)
+ required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.25))
+ for i in range(required_prog_items):
+ self.multiworld.state.collect(self.world.create_item("Stardrop"), event=False)
+
+ def collect_all_the_money(self):
+ self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False)
+ required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.95))
+ for i in range(required_prog_items):
+ self.multiworld.state.collect(self.world.create_item("Stardrop"), event=False)
+
+ def collect_everything(self):
+ non_event_items = [item for item in self.multiworld.get_items() if item.code]
+ for item in non_event_items:
+ self.multiworld.state.collect(item)
+
+ def collect_all_except(self, item_to_not_collect: str):
+ for item in self.multiworld.get_items():
+ if item.name != item_to_not_collect:
+ self.multiworld.state.collect(item)
+
+ def get_real_locations(self) -> List[Location]:
+ return [location for location in self.multiworld.get_locations(self.player) if location.address is not None]
+
+ def get_real_location_names(self) -> List[str]:
+ return [location.name for location in self.get_real_locations()]
+
+ def collect(self, item: Union[str, Item, Iterable[Item]], count: int = 1) -> Union[None, Item, List[Item]]:
+ assert count > 0
+ if not isinstance(item, str):
+ super().collect(item)
+ return
+ if count == 1:
+ item = self.create_item(item)
+ self.multiworld.state.collect(item)
+ return item
+ items = []
+ for i in range(count):
+ item = self.create_item(item)
+ self.multiworld.state.collect(item)
+ items.append(item)
+ return items
+
+ def create_item(self, item: str) -> StardewItem:
+ created_item = self.world.create_item(item)
+ if created_item.classification == ItemClassification.progression:
+ self.multiworld.worlds[self.player].total_progression_items -= 1
+ return created_item
+
pre_generated_worlds = {}
+@contextmanager
+def solo_multiworld(world_options: Optional[Dict[Union[str, StardewValleyOption], Any]] = None,
+ *,
+ seed=DEFAULT_TEST_SEED,
+ world_caching=True) -> Tuple[MultiWorld, StardewValleyWorld]:
+ if not world_caching:
+ multiworld = setup_solo_multiworld(world_options, seed, _cache={})
+ yield multiworld, multiworld.worlds[1]
+ else:
+ multiworld = setup_solo_multiworld(world_options, seed)
+ multiworld.lock.acquire()
+ world = multiworld.worlds[1]
+
+ original_state = multiworld.state.copy()
+ original_itempool = multiworld.itempool.copy()
+ unfilled_locations = multiworld.get_unfilled_locations(1)
+ original_prog_item_count = world.total_progression_items
+
+ yield multiworld, world
+
+ multiworld.state = original_state
+ multiworld.itempool = original_itempool
+ for location in unfilled_locations:
+ location.item = None
+ multiworld.total_progression_items = original_prog_item_count
+
+ multiworld.lock.release()
+
+
# Mostly a copy of test.general.setup_solo_multiworld, I just don't want to change the core.
-def setup_solo_multiworld(test_options=None, seed=None,
- _cache: Dict[FrozenSet[Tuple[str, Any]], MultiWorld] = {}) -> MultiWorld: # noqa
- if test_options is None:
- test_options = {}
+def setup_solo_multiworld(test_options: Optional[Dict[Union[str, StardewValleyOption], str]] = None,
+ seed=DEFAULT_TEST_SEED,
+ _cache: Dict[frozenset, MultiWorld] = {}, # noqa
+ _steps=gen_steps) -> MultiWorld:
+ test_options = parse_class_option_keys(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]
+ # If the simple dict caching ends up taking too much memory, we could replace it with some kind of lru cache.
+ should_cache = "start_inventory" not in test_options
+ if should_cache:
+ frozen_options = frozenset(test_options.items()).union({("seed", seed)})
+ cached_multi_world = search_world_cache(_cache, frozen_options)
+ if cached_multi_world:
+ print(f"Using cached solo multi world [Seed = {cached_multi_world.seed}] [Cache size = {len(_cache)}]")
+ return cached_multi_world
- multiworld = setup_base_solo_multiworld(StardewValleyWorld, ())
- multiworld.set_seed(seed)
+ multiworld = setup_base_solo_multiworld(StardewValleyWorld, (), seed=seed)
# print(f"Seed: {multiworld.seed}") # Uncomment to print the seed for every test
+
args = Namespace()
- for name, option in StardewValleyWorld.option_definitions.items():
- value = option(test_options[name]) if name in test_options else option.from_any(option.default)
+ for name, option in StardewValleyWorld.options_dataclass.type_hints.items():
+ value = option.from_any(test_options.get(name, option.default))
+
+ if issubclass(option, VerifyKeys):
+ # Values should already be verified, but just in case...
+ value.verify(StardewValleyWorld, "Tester", PlandoOptions.bosses)
+
setattr(args, name, {1: value})
multiworld.set_options(args)
- for step in gen_steps:
+
+ if "start_inventory" in test_options:
+ for item, amount in test_options["start_inventory"].items():
+ for _ in range(amount):
+ multiworld.push_precollected(multiworld.create_item(item, 1))
+
+ for step in _steps:
call_all(multiworld, step)
- _cache[frozen_options] = multiworld
+ if should_cache:
+ add_to_world_cache(_cache, frozen_options, multiworld) # noqa
+
+ # Lock is needed for multi-threading tests
+ setattr(multiworld, "lock", threading.Lock())
return multiworld
+
+
+def parse_class_option_keys(test_options: Optional[Dict]) -> dict:
+ """ Now the option class is allowed as key. """
+ if test_options is None:
+ return {}
+ parsed_options = {}
+
+ for option, value in test_options.items():
+ if hasattr(option, "internal_name"):
+ assert option.internal_name not in test_options, "Defined two times by class and internal_name"
+ parsed_options[option.internal_name] = value
+ else:
+ assert option in StardewValleyOptions.type_hints, \
+ f"All keys of world_options must be a possible Stardew Valley option, {option} is not."
+ parsed_options[option] = value
+
+ return parsed_options
+
+
+def search_world_cache(cache: Dict[frozenset, MultiWorld], frozen_options: frozenset) -> Optional[MultiWorld]:
+ try:
+ return cache[frozen_options]
+ except KeyError:
+ for cached_options, multi_world in cache.items():
+ if frozen_options.issubset(cached_options):
+ return multi_world
+ return None
+
+
+def add_to_world_cache(cache: Dict[frozenset, MultiWorld], frozen_options: frozenset, multi_world: MultiWorld) -> None:
+ # We could complete the key with all the default options, but that does not seem to improve performances.
+ cache[frozen_options] = multi_world
+
+
+def complete_options_with_default(options_to_complete=None) -> StardewValleyOptions:
+ if options_to_complete is None:
+ options_to_complete = {}
+
+ for name, option in StardewValleyOptions.type_hints.items():
+ options_to_complete[name] = option.from_any(options_to_complete.get(name, option.default))
+
+ return StardewValleyOptions(**options_to_complete)
+
+
+def setup_multiworld(test_options: Iterable[Dict[str, int]] = None, seed=None) -> MultiWorld: # noqa
+ if test_options is None:
+ test_options = []
+
+ multiworld = MultiWorld(len(test_options))
+ multiworld.player_name = {}
+ multiworld.set_seed(seed)
+ multiworld.state = CollectionState(multiworld)
+ for i in range(1, len(test_options) + 1):
+ multiworld.game[i] = StardewValleyWorld.game
+ multiworld.player_name.update({i: f"Tester{i}"})
+ args = create_args(test_options)
+ multiworld.set_options(args)
+
+ for step in gen_steps:
+ call_all(multiworld, step)
+
+ return multiworld
+
+
+def create_args(test_options):
+ args = Namespace()
+ for name, option in StardewValleyWorld.options_dataclass.type_hints.items():
+ options = {}
+ for i in range(1, len(test_options) + 1):
+ player_options = test_options[i - 1]
+ value = option(player_options[name]) if name in player_options else option.from_any(option.default)
+ options.update({i: value})
+ setattr(args, name, options)
+ return args
diff --git a/worlds/stardew_valley/test/assertion/__init__.py b/worlds/stardew_valley/test/assertion/__init__.py
new file mode 100644
index 000000000000..3a1420fe65e4
--- /dev/null
+++ b/worlds/stardew_valley/test/assertion/__init__.py
@@ -0,0 +1,5 @@
+from .goal_assert import *
+from .mod_assert import *
+from .option_assert import *
+from .rule_assert import *
+from .world_assert import *
diff --git a/worlds/stardew_valley/test/assertion/goal_assert.py b/worlds/stardew_valley/test/assertion/goal_assert.py
new file mode 100644
index 000000000000..2b2efbf2ec03
--- /dev/null
+++ b/worlds/stardew_valley/test/assertion/goal_assert.py
@@ -0,0 +1,55 @@
+from unittest import TestCase
+
+from BaseClasses import MultiWorld
+from .option_assert import get_stardew_options
+from ... import options, ExcludeGingerIsland
+
+
+def is_goal(multiworld: MultiWorld, goal: int) -> bool:
+ return get_stardew_options(multiworld).goal.value == goal
+
+
+def is_bottom_mines(multiworld: MultiWorld) -> bool:
+ return is_goal(multiworld, options.Goal.option_bottom_of_the_mines)
+
+
+def is_not_bottom_mines(multiworld: MultiWorld) -> bool:
+ return not is_bottom_mines(multiworld)
+
+
+def is_walnut_hunter(multiworld: MultiWorld) -> bool:
+ return is_goal(multiworld, options.Goal.option_greatest_walnut_hunter)
+
+
+def is_not_walnut_hunter(multiworld: MultiWorld) -> bool:
+ return not is_walnut_hunter(multiworld)
+
+
+def is_perfection(multiworld: MultiWorld) -> bool:
+ return is_goal(multiworld, options.Goal.option_perfection)
+
+
+def is_not_perfection(multiworld: MultiWorld) -> bool:
+ return not is_perfection(multiworld)
+
+
+class GoalAssertMixin(TestCase):
+
+ def assert_ginger_island_is_included(self, multiworld: MultiWorld):
+ self.assertEqual(get_stardew_options(multiworld).exclude_ginger_island, ExcludeGingerIsland.option_false)
+
+ def assert_walnut_hunter_world_is_valid(self, multiworld: MultiWorld):
+ if is_not_walnut_hunter(multiworld):
+ return
+
+ self.assert_ginger_island_is_included(multiworld)
+
+ def assert_perfection_world_is_valid(self, multiworld: MultiWorld):
+ if is_not_perfection(multiworld):
+ return
+
+ self.assert_ginger_island_is_included(multiworld)
+
+ def assert_goal_world_is_valid(self, multiworld: MultiWorld):
+ self.assert_walnut_hunter_world_is_valid(multiworld)
+ self.assert_perfection_world_is_valid(multiworld)
diff --git a/worlds/stardew_valley/test/assertion/mod_assert.py b/worlds/stardew_valley/test/assertion/mod_assert.py
new file mode 100644
index 000000000000..baba9bbaf856
--- /dev/null
+++ b/worlds/stardew_valley/test/assertion/mod_assert.py
@@ -0,0 +1,28 @@
+from typing import Union, Iterable
+from unittest import TestCase
+
+from BaseClasses import MultiWorld
+from ... import item_table, location_table
+from ...mods.mod_data import ModNames
+
+
+class ModAssertMixin(TestCase):
+ def assert_stray_mod_items(self, chosen_mods: Union[Iterable[str], str], multiworld: MultiWorld):
+ if isinstance(chosen_mods, str):
+ chosen_mods = [chosen_mods]
+ else:
+ chosen_mods = list(chosen_mods)
+
+ if ModNames.jasper in chosen_mods:
+ # Jasper is a weird case because it shares NPC w/ SVE...
+ chosen_mods.append(ModNames.sve)
+
+ for multiworld_item in multiworld.get_items():
+ item = item_table[multiworld_item.name]
+ self.assertTrue(item.mod_name is None or item.mod_name in chosen_mods,
+ f"Item {item.name} has is from mod {item.mod_name}. Allowed mods are {chosen_mods}.")
+ for multiworld_location in multiworld.get_locations():
+ if multiworld_location.address is None:
+ continue
+ location = location_table[multiworld_location.name]
+ self.assertTrue(location.mod_name is None or location.mod_name in chosen_mods)
diff --git a/worlds/stardew_valley/test/assertion/option_assert.py b/worlds/stardew_valley/test/assertion/option_assert.py
new file mode 100644
index 000000000000..a07831f73e3f
--- /dev/null
+++ b/worlds/stardew_valley/test/assertion/option_assert.py
@@ -0,0 +1,100 @@
+from unittest import TestCase
+
+from BaseClasses import MultiWorld
+from .world_assert import get_all_item_names, get_all_location_names
+from ... import StardewValleyWorld, options, item_table, Group, location_table, ExcludeGingerIsland
+from ...locations import LocationTags
+from ...strings.ap_names.transport_names import Transportation
+
+
+def get_stardew_world(multiworld: MultiWorld) -> StardewValleyWorld:
+ for world_key in multiworld.worlds:
+ world = multiworld.worlds[world_key]
+ if isinstance(world, StardewValleyWorld):
+ return world
+ raise ValueError("no stardew world in this multiworld")
+
+
+def get_stardew_options(multiworld: MultiWorld) -> options.StardewValleyOptions:
+ return get_stardew_world(multiworld).options
+
+
+class OptionAssertMixin(TestCase):
+
+ def assert_has_item(self, multiworld: MultiWorld, item: str):
+ all_item_names = set(get_all_item_names(multiworld))
+ self.assertIn(item, all_item_names)
+
+ def assert_has_not_item(self, multiworld: MultiWorld, item: str):
+ all_item_names = set(get_all_item_names(multiworld))
+ self.assertNotIn(item, all_item_names)
+
+ def assert_has_location(self, multiworld: MultiWorld, item: str):
+ all_location_names = set(get_all_location_names(multiworld))
+ self.assertIn(item, all_location_names)
+
+ def assert_has_not_location(self, multiworld: MultiWorld, item: str):
+ all_location_names = set(get_all_location_names(multiworld))
+ self.assertNotIn(item, all_location_names)
+
+ def assert_can_reach_island(self, multiworld: MultiWorld):
+ all_item_names = get_all_item_names(multiworld)
+ self.assertIn(Transportation.boat_repair, all_item_names)
+ self.assertIn(Transportation.island_obelisk, all_item_names)
+
+ def assert_cannot_reach_island(self, multiworld: MultiWorld):
+ all_item_names = get_all_item_names(multiworld)
+ self.assertNotIn(Transportation.boat_repair, all_item_names)
+ self.assertNotIn(Transportation.island_obelisk, all_item_names)
+
+ def assert_can_reach_island_if_should(self, multiworld: MultiWorld):
+ stardew_options = get_stardew_options(multiworld)
+ include_island = stardew_options.exclude_ginger_island.value == ExcludeGingerIsland.option_false
+ if include_island:
+ self.assert_can_reach_island(multiworld)
+ else:
+ self.assert_cannot_reach_island(multiworld)
+
+ def assert_cropsanity_same_number_items_and_locations(self, multiworld: MultiWorld):
+ is_cropsanity = get_stardew_options(multiworld).cropsanity.value == options.Cropsanity.option_enabled
+ if not is_cropsanity:
+ return
+
+ all_item_names = set(get_all_item_names(multiworld))
+ all_location_names = set(get_all_location_names(multiworld))
+ all_cropsanity_item_names = {item_name for item_name in all_item_names if Group.CROPSANITY in item_table[item_name].groups}
+ all_cropsanity_location_names = {location_name
+ for location_name in all_location_names
+ if LocationTags.CROPSANITY in location_table[location_name].tags
+ # Qi Beans do not have an item
+ and location_name != "Harvest Qi Fruit"}
+ self.assertEqual(len(all_cropsanity_item_names) + 1, len(all_cropsanity_location_names))
+
+ def assert_all_rarecrows_exist(self, multiworld: MultiWorld):
+ all_item_names = set(get_all_item_names(multiworld))
+ for rarecrow_number in range(1, 9):
+ self.assertIn(f"Rarecrow #{rarecrow_number}", all_item_names)
+
+ def assert_has_deluxe_scarecrow_recipe(self, multiworld: MultiWorld):
+ self.assert_has_item(multiworld, "Deluxe Scarecrow Recipe")
+
+ def assert_festivals_give_access_to_deluxe_scarecrow(self, multiworld: MultiWorld):
+ stardew_options = get_stardew_options(multiworld)
+ has_festivals = stardew_options.festival_locations.value != options.FestivalLocations.option_disabled
+ if not has_festivals:
+ return
+
+ self.assert_all_rarecrows_exist(multiworld)
+ self.assert_has_deluxe_scarecrow_recipe(multiworld)
+
+ def assert_has_festival_recipes(self, multiworld: MultiWorld):
+ stardew_options = get_stardew_options(multiworld)
+ has_festivals = stardew_options.festival_locations.value != options.FestivalLocations.option_disabled
+ festival_items = ["Tub o' Flowers Recipe", "Jack-O-Lantern Recipe"]
+ for festival_item in festival_items:
+ if has_festivals:
+ self.assert_has_item(multiworld, festival_item)
+ self.assert_has_location(multiworld, festival_item)
+ else:
+ self.assert_has_not_item(multiworld, festival_item)
+ self.assert_has_not_location(multiworld, festival_item)
diff --git a/worlds/stardew_valley/test/assertion/rule_assert.py b/worlds/stardew_valley/test/assertion/rule_assert.py
new file mode 100644
index 000000000000..5a1dad2925cf
--- /dev/null
+++ b/worlds/stardew_valley/test/assertion/rule_assert.py
@@ -0,0 +1,49 @@
+from unittest import TestCase
+
+from BaseClasses import CollectionState, Location
+from ...stardew_rule import StardewRule, false_, MISSING_ITEM, Reach
+from ...stardew_rule.rule_explain import explain
+
+
+class RuleAssertMixin(TestCase):
+ def assert_rule_true(self, rule: StardewRule, state: CollectionState):
+ expl = explain(rule, state)
+ try:
+ self.assertTrue(rule(state), expl)
+ except KeyError as e:
+ raise AssertionError(f"Error while checking rule {rule}: {e}"
+ f"\nExplanation: {expl}")
+
+ def assert_rule_false(self, rule: StardewRule, state: CollectionState):
+ expl = explain(rule, state, expected=False)
+ try:
+ self.assertFalse(rule(state), expl)
+ except KeyError as e:
+ raise AssertionError(f"Error while checking rule {rule}: {e}"
+ f"\nExplanation: {expl}")
+
+ def assert_rule_can_be_resolved(self, rule: StardewRule, complete_state: CollectionState):
+ expl = explain(rule, complete_state)
+ try:
+ self.assertNotIn(MISSING_ITEM, repr(rule))
+ self.assertTrue(rule is false_ or rule(complete_state), expl)
+ except KeyError as e:
+ raise AssertionError(f"Error while checking rule {rule}: {e}"
+ f"\nExplanation: {expl}")
+
+ def assert_reach_location_true(self, location: Location, state: CollectionState):
+ expl = explain(Reach(location.name, "Location", 1), state)
+ try:
+ can_reach = location.can_reach(state)
+ self.assertTrue(can_reach, expl)
+ except KeyError as e:
+ raise AssertionError(f"Error while checking location {location.name}: {e}"
+ f"\nExplanation: {expl}")
+
+ def assert_reach_location_false(self, location: Location, state: CollectionState):
+ expl = explain(Reach(location.name, "Location", 1), state, expected=False)
+ try:
+ self.assertFalse(location.can_reach(state), expl)
+ except KeyError as e:
+ raise AssertionError(f"Error while checking location {location.name}: {e}"
+ f"\nExplanation: {expl}")
diff --git a/worlds/stardew_valley/test/assertion/world_assert.py b/worlds/stardew_valley/test/assertion/world_assert.py
new file mode 100644
index 000000000000..c1c24bdf75b4
--- /dev/null
+++ b/worlds/stardew_valley/test/assertion/world_assert.py
@@ -0,0 +1,83 @@
+from typing import List
+from unittest import TestCase
+
+from BaseClasses import MultiWorld, ItemClassification
+from .rule_assert import RuleAssertMixin
+from ... import StardewItem
+from ...items import items_by_group, Group
+from ...locations import LocationTags, locations_by_tag
+
+
+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 location.address is not None]
+
+
+class WorldAssertMixin(RuleAssertMixin, TestCase):
+
+ def assert_victory_exists(self, multiworld: MultiWorld):
+ self.assertIn(StardewItem("Victory", ItemClassification.progression, None, 1), multiworld.get_items())
+
+ def assert_can_reach_victory(self, multiworld: MultiWorld):
+ victory = multiworld.find_item("Victory", 1)
+ self.assert_rule_true(victory.access_rule, multiworld.state)
+
+ def assert_cannot_reach_victory(self, multiworld: MultiWorld):
+ victory = multiworld.find_item("Victory", 1)
+ self.assert_rule_false(victory.access_rule, multiworld.state)
+
+ def assert_item_was_necessary_for_victory(self, item: StardewItem, multiworld: MultiWorld):
+ self.assert_can_reach_victory(multiworld)
+ multiworld.state.remove(item)
+ self.assert_cannot_reach_victory(multiworld)
+ multiworld.state.collect(item, event=False)
+ self.assert_can_reach_victory(multiworld)
+
+ def assert_item_was_not_necessary_for_victory(self, item: StardewItem, multiworld: MultiWorld):
+ self.assert_can_reach_victory(multiworld)
+ multiworld.state.remove(item)
+ self.assert_can_reach_victory(multiworld)
+ multiworld.state.collect(item, event=False)
+ self.assert_can_reach_victory(multiworld)
+
+ def assert_can_win(self, multiworld: MultiWorld):
+ self.assert_victory_exists(multiworld)
+ self.assert_can_reach_victory(multiworld)
+
+ def assert_same_number_items_locations(self, multiworld: MultiWorld):
+ non_event_locations = [location for location in multiworld.get_locations() if location.address is not None]
+ self.assertEqual(len(multiworld.itempool), len(non_event_locations))
+
+ def assert_can_reach_everything(self, multiworld: MultiWorld):
+ for location in multiworld.get_locations():
+ self.assert_reach_location_true(location, multiworld.state)
+
+ def assert_basic_checks(self, multiworld: MultiWorld):
+ self.assert_same_number_items_locations(multiworld)
+ non_event_items = [item for item in multiworld.get_items() if item.code]
+ for item in non_event_items:
+ multiworld.state.collect(item)
+ self.assert_can_win(multiworld)
+ self.assert_can_reach_everything(multiworld)
+
+ def assert_basic_checks_with_subtests(self, multiworld: MultiWorld):
+ with self.subTest("same_number_items_locations"):
+ self.assert_same_number_items_locations(multiworld)
+ non_event_items = [item for item in multiworld.get_items() if item.code]
+ for item in non_event_items:
+ multiworld.state.collect(item)
+ with self.subTest("can_win"):
+ self.assert_can_win(multiworld)
+ with self.subTest("can_reach_everything"):
+ self.assert_can_reach_everything(multiworld)
+
+ def assert_no_ginger_island_content(self, multiworld: MultiWorld):
+ ginger_island_items = [item_data.name for item_data in items_by_group[Group.GINGER_ISLAND]]
+ ginger_island_locations = [location_data.name for location_data in locations_by_tag[LocationTags.GINGER_ISLAND]]
+ for item in multiworld.get_items():
+ self.assertNotIn(item.name, ginger_island_items)
+ for location in multiworld.get_locations():
+ self.assertNotIn(location.name, ginger_island_locations)
diff --git a/worlds/stardew_valley/test/checks/goal_checks.py b/worlds/stardew_valley/test/checks/goal_checks.py
deleted file mode 100644
index e1059fe2d641..000000000000
--- a/worlds/stardew_valley/test/checks/goal_checks.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from BaseClasses import MultiWorld
-from .option_checks import is_setting, assert_is_setting
-from ... import options
-from .. import SVTestBase
-
-
-def is_goal(multiworld: MultiWorld, goal: int) -> bool:
- return is_setting(multiworld, options.Goal.internal_name, goal)
-
-
-def is_bottom_mines(multiworld: MultiWorld) -> bool:
- return is_goal(multiworld, options.Goal.option_bottom_of_the_mines)
-
-
-def is_not_bottom_mines(multiworld: MultiWorld) -> bool:
- return not is_bottom_mines(multiworld)
-
-
-def is_walnut_hunter(multiworld: MultiWorld) -> bool:
- return is_goal(multiworld, options.Goal.option_greatest_walnut_hunter)
-
-
-def is_not_walnut_hunter(multiworld: MultiWorld) -> bool:
- return not is_walnut_hunter(multiworld)
-
-
-def is_perfection(multiworld: MultiWorld) -> bool:
- return is_goal(multiworld, options.Goal.option_perfection)
-
-
-def is_not_perfection(multiworld: MultiWorld) -> bool:
- return not is_perfection(multiworld)
-
-
-def assert_ginger_island_is_included(tester: SVTestBase, multiworld: MultiWorld):
- assert_is_setting(tester, multiworld, options.ExcludeGingerIsland.internal_name, options.ExcludeGingerIsland.option_false)
-
-
-def assert_walnut_hunter_world_is_valid(tester: SVTestBase, multiworld: MultiWorld):
- if is_not_walnut_hunter(multiworld):
- return
-
- assert_ginger_island_is_included(tester, multiworld)
-
-
-def assert_perfection_world_is_valid(tester: SVTestBase, multiworld: MultiWorld):
- if is_not_perfection(multiworld):
- return
-
- assert_ginger_island_is_included(tester, multiworld)
-
-
-def assert_goal_world_is_valid(tester: SVTestBase, multiworld: MultiWorld):
- assert_walnut_hunter_world_is_valid(tester, multiworld)
- assert_perfection_world_is_valid(tester, multiworld)
diff --git a/worlds/stardew_valley/test/checks/option_checks.py b/worlds/stardew_valley/test/checks/option_checks.py
deleted file mode 100644
index e6bced5b1ce7..000000000000
--- a/worlds/stardew_valley/test/checks/option_checks.py
+++ /dev/null
@@ -1,90 +0,0 @@
-from typing import Union
-
-from BaseClasses import MultiWorld
-from .world_checks import get_all_item_names, get_all_location_names
-from .. import SVTestBase
-from ... import StardewValleyWorld, options, item_table, Group, location_table
-from ...locations import LocationTags
-from ...strings.ap_names.transport_names import Transportation
-
-
-def get_stardew_world(multiworld: MultiWorld) -> Union[StardewValleyWorld, None]:
- for world_key in multiworld.worlds:
- world = multiworld.worlds[world_key]
- if isinstance(world, StardewValleyWorld):
- return world
- return None
-
-
-def is_setting(multiworld: MultiWorld, setting_name: str, setting_value: int) -> bool:
- stardew_world = get_stardew_world(multiworld)
- if not stardew_world:
- return False
- current_value = stardew_world.options[setting_name]
- return current_value == setting_value
-
-
-def is_not_setting(multiworld: MultiWorld, setting_name: str, setting_value: int) -> bool:
- return not is_setting(multiworld, setting_name, setting_value)
-
-
-def assert_is_setting(tester: SVTestBase, multiworld: MultiWorld, setting_name: str, setting_value: int) -> bool:
- stardew_world = get_stardew_world(multiworld)
- if not stardew_world:
- return False
- current_value = stardew_world.options[setting_name]
- tester.assertEqual(current_value, setting_value)
-
-
-def assert_can_reach_island(tester: SVTestBase, multiworld: MultiWorld):
- all_item_names = get_all_item_names(multiworld)
- tester.assertIn(Transportation.boat_repair, all_item_names)
- tester.assertIn(Transportation.island_obelisk, all_item_names)
-
-
-def assert_cannot_reach_island(tester: SVTestBase, multiworld: MultiWorld):
- all_item_names = get_all_item_names(multiworld)
- tester.assertNotIn(Transportation.boat_repair, all_item_names)
- tester.assertNotIn(Transportation.island_obelisk, all_item_names)
-
-
-def assert_can_reach_island_if_should(tester: SVTestBase, multiworld: MultiWorld):
- include_island = is_setting(multiworld, options.ExcludeGingerIsland.internal_name, options.ExcludeGingerIsland.option_false)
- if include_island:
- assert_can_reach_island(tester, multiworld)
- else:
- assert_cannot_reach_island(tester, multiworld)
-
-
-def assert_cropsanity_same_number_items_and_locations(tester: SVTestBase, multiworld: MultiWorld):
- is_cropsanity = is_setting(multiworld, options.Cropsanity.internal_name, options.Cropsanity.option_shuffled)
- if not is_cropsanity:
- return
-
- all_item_names = set(get_all_item_names(multiworld))
- all_location_names = set(get_all_location_names(multiworld))
- all_cropsanity_item_names = {item_name for item_name in all_item_names if Group.CROPSANITY in item_table[item_name].groups}
- all_cropsanity_location_names = {location_name for location_name in all_location_names if LocationTags.CROPSANITY in location_table[location_name].tags}
- tester.assertEqual(len(all_cropsanity_item_names), len(all_cropsanity_location_names))
-
-
-def assert_all_rarecrows_exist(tester: SVTestBase, multiworld: MultiWorld):
- all_item_names = set(get_all_item_names(multiworld))
- for rarecrow_number in range(1, 9):
- tester.assertIn(f"Rarecrow #{rarecrow_number}", all_item_names)
-
-
-def assert_has_deluxe_scarecrow_recipe(tester: SVTestBase, multiworld: MultiWorld):
- all_item_names = set(get_all_item_names(multiworld))
- tester.assertIn(f"Deluxe Scarecrow Recipe", all_item_names)
-
-
-def assert_festivals_give_access_to_deluxe_scarecrow(tester: SVTestBase, multiworld: MultiWorld):
- has_festivals = is_not_setting(multiworld, options.FestivalLocations.internal_name, options.FestivalLocations.option_disabled)
- if not has_festivals:
- return
-
- assert_all_rarecrows_exist(tester, multiworld)
- assert_has_deluxe_scarecrow_recipe(tester, multiworld)
-
-
diff --git a/worlds/stardew_valley/test/checks/world_checks.py b/worlds/stardew_valley/test/checks/world_checks.py
deleted file mode 100644
index 2cdb0534d40a..000000000000
--- a/worlds/stardew_valley/test/checks/world_checks.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from typing import List
-
-from BaseClasses import MultiWorld, ItemClassification
-from ... import StardewItem
-from .. import SVTestBase
-
-
-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: SVTestBase, multiworld: MultiWorld):
- tester.assertIn(StardewItem("Victory", ItemClassification.progression, None, 1), multiworld.get_items())
-
-
-def collect_all_then_assert_can_win(tester: SVTestBase, multiworld: MultiWorld):
- for item in multiworld.get_items():
- multiworld.state.collect(item)
- tester.assertTrue(multiworld.find_item("Victory", 1).can_reach(multiworld.state))
-
-
-def assert_can_win(tester: SVTestBase, multiworld: MultiWorld):
- assert_victory_exists(tester, multiworld)
- collect_all_then_assert_can_win(tester, multiworld)
-
-
-def assert_same_number_items_locations(tester: SVTestBase, 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/stardew_valley/test/content/TestArtisanEquipment.py b/worlds/stardew_valley/test/content/TestArtisanEquipment.py
new file mode 100644
index 000000000000..32821511c44f
--- /dev/null
+++ b/worlds/stardew_valley/test/content/TestArtisanEquipment.py
@@ -0,0 +1,54 @@
+from . import SVContentPackTestBase
+from ...data.artisan import MachineSource
+from ...strings.artisan_good_names import ArtisanGood
+from ...strings.crop_names import Vegetable, Fruit
+from ...strings.food_names import Beverage
+from ...strings.forageable_names import Forageable
+from ...strings.machine_names import Machine
+from ...strings.seed_names import Seed
+
+wine_base_fruits = [
+ Fruit.ancient_fruit, Fruit.apple, Fruit.apricot, Forageable.blackberry, Fruit.blueberry, Forageable.cactus_fruit, Fruit.cherry,
+ Forageable.coconut, Fruit.cranberries, Forageable.crystal_fruit, Fruit.grape, Fruit.hot_pepper, Fruit.melon, Fruit.orange, Fruit.peach,
+ Fruit.pomegranate, Fruit.powdermelon, Fruit.rhubarb, Forageable.salmonberry, Forageable.spice_berry, Fruit.starfruit, Fruit.strawberry
+]
+
+juice_base_vegetables = (
+ Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.bok_choy, Vegetable.broccoli, Vegetable.carrot, Vegetable.cauliflower,
+ Vegetable.corn, Vegetable.eggplant, Forageable.fiddlehead_fern, Vegetable.garlic, Vegetable.green_bean, Vegetable.kale, Vegetable.parsnip, Vegetable.potato,
+ Vegetable.pumpkin, Vegetable.radish, Vegetable.red_cabbage, Vegetable.summer_squash, Vegetable.tomato, Vegetable.unmilled_rice, Vegetable.yam
+)
+
+non_juice_base_vegetables = (
+ Vegetable.hops, Vegetable.tea_leaves, Vegetable.wheat
+)
+
+
+class TestArtisanEquipment(SVContentPackTestBase):
+
+ def test_keg_special_recipes(self):
+ self.assertIn(MachineSource(item=Vegetable.wheat, machine=Machine.keg), self.content.game_items[Beverage.beer].sources)
+ # self.assertIn(MachineSource(item=Ingredient.rice, machine=Machine.keg), self.content.game_items[Ingredient.vinegar].sources)
+ self.assertIn(MachineSource(item=Seed.coffee, machine=Machine.keg), self.content.game_items[Beverage.coffee].sources)
+ self.assertIn(MachineSource(item=Vegetable.tea_leaves, machine=Machine.keg), self.content.game_items[ArtisanGood.green_tea].sources)
+ self.assertIn(MachineSource(item=ArtisanGood.honey, machine=Machine.keg), self.content.game_items[ArtisanGood.mead].sources)
+ self.assertIn(MachineSource(item=Vegetable.hops, machine=Machine.keg), self.content.game_items[ArtisanGood.pale_ale].sources)
+
+ def test_fruits_can_be_made_into_wines(self):
+
+ for fruit in wine_base_fruits:
+ with self.subTest(fruit):
+ self.assertIn(MachineSource(item=fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(fruit)].sources)
+ self.assertIn(MachineSource(item=fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources)
+
+ def test_vegetables_can_be_made_into_juices(self):
+ for vegetable in juice_base_vegetables:
+ with self.subTest(vegetable):
+ self.assertIn(MachineSource(item=vegetable, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_juice(vegetable)].sources)
+ self.assertIn(MachineSource(item=vegetable, machine=Machine.keg), self.content.game_items[ArtisanGood.juice].sources)
+
+ def test_non_juice_vegetables_cannot_be_made_into_juices(self):
+ for vegetable in non_juice_base_vegetables:
+ with self.subTest(vegetable):
+ self.assertNotIn(ArtisanGood.specific_juice(vegetable), self.content.game_items)
+ self.assertNotIn(MachineSource(item=vegetable, machine=Machine.keg), self.content.game_items[ArtisanGood.juice].sources)
diff --git a/worlds/stardew_valley/test/content/TestGingerIsland.py b/worlds/stardew_valley/test/content/TestGingerIsland.py
new file mode 100644
index 000000000000..7e7f866dfc8e
--- /dev/null
+++ b/worlds/stardew_valley/test/content/TestGingerIsland.py
@@ -0,0 +1,55 @@
+from . import SVContentPackTestBase
+from .. import SVTestBase
+from ... import options
+from ...content import content_packs
+from ...data.artisan import MachineSource
+from ...strings.artisan_good_names import ArtisanGood
+from ...strings.crop_names import Fruit, Vegetable
+from ...strings.fish_names import Fish
+from ...strings.machine_names import Machine
+from ...strings.villager_names import NPC
+
+
+class TestGingerIsland(SVContentPackTestBase):
+ vanilla_packs = SVContentPackTestBase.vanilla_packs + (content_packs.ginger_island_content_pack,)
+
+ def test_leo_is_included(self):
+ self.assertIn(NPC.leo, self.content.villagers)
+
+ def test_ginger_island_fishes_are_included(self):
+ fish_names = self.content.fishes.keys()
+
+ self.assertIn(Fish.blue_discus, fish_names)
+ self.assertIn(Fish.lionfish, fish_names)
+ self.assertIn(Fish.stingray, fish_names)
+
+ # 63 from pelican town + 3 ginger island exclusive
+ self.assertEqual(63 + 3, len(self.content.fishes))
+
+ def test_ginger_island_fruits_can_be_made_into_wines(self):
+ self.assertIn(MachineSource(item=Fruit.banana, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.banana)].sources)
+ self.assertIn(MachineSource(item=Fruit.banana, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources)
+
+ self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.mango)].sources)
+ self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources)
+
+ self.assertIn(MachineSource(item=Fruit.pineapple, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.pineapple)].sources)
+ self.assertIn(MachineSource(item=Fruit.pineapple, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources)
+
+ def test_ginger_island_vegetables_can_be_made_into_wines(self):
+ taro_root_juice_sources = self.content.game_items[ArtisanGood.specific_juice(Vegetable.taro_root)].sources
+ self.assertIn(MachineSource(item=Vegetable.taro_root, machine=Machine.keg), taro_root_juice_sources)
+ self.assertIn(MachineSource(item=Vegetable.taro_root, machine=Machine.keg), self.content.game_items[ArtisanGood.juice].sources)
+
+
+class TestWithoutGingerIslandE2E(SVTestBase):
+ options = {
+ options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true
+ }
+
+ def test_leo_is_not_in_the_pool(self):
+ for item in self.multiworld.itempool:
+ self.assertFalse(("Friendsanity: " + NPC.leo) in item.name)
+
+ for location in self.multiworld.get_locations(self.player):
+ self.assertFalse(("Friendsanity: " + NPC.leo) in location.name)
diff --git a/worlds/stardew_valley/test/content/TestPelicanTown.py b/worlds/stardew_valley/test/content/TestPelicanTown.py
new file mode 100644
index 000000000000..fa70916c9d33
--- /dev/null
+++ b/worlds/stardew_valley/test/content/TestPelicanTown.py
@@ -0,0 +1,112 @@
+from . import SVContentPackTestBase
+from ...strings.fish_names import Fish
+from ...strings.villager_names import NPC
+
+
+class TestPelicanTown(SVContentPackTestBase):
+
+ def test_all_pelican_town_villagers_are_included(self):
+ self.assertIn(NPC.alex, self.content.villagers)
+ self.assertIn(NPC.elliott, self.content.villagers)
+ self.assertIn(NPC.harvey, self.content.villagers)
+ self.assertIn(NPC.sam, self.content.villagers)
+ self.assertIn(NPC.sebastian, self.content.villagers)
+ self.assertIn(NPC.shane, self.content.villagers)
+ self.assertIn(NPC.abigail, self.content.villagers)
+ self.assertIn(NPC.emily, self.content.villagers)
+ self.assertIn(NPC.haley, self.content.villagers)
+ self.assertIn(NPC.leah, self.content.villagers)
+ self.assertIn(NPC.maru, self.content.villagers)
+ self.assertIn(NPC.penny, self.content.villagers)
+ self.assertIn(NPC.caroline, self.content.villagers)
+ self.assertIn(NPC.clint, self.content.villagers)
+ self.assertIn(NPC.demetrius, self.content.villagers)
+ self.assertIn(NPC.dwarf, self.content.villagers)
+ self.assertIn(NPC.evelyn, self.content.villagers)
+ self.assertIn(NPC.george, self.content.villagers)
+ self.assertIn(NPC.gus, self.content.villagers)
+ self.assertIn(NPC.jas, self.content.villagers)
+ self.assertIn(NPC.jodi, self.content.villagers)
+ self.assertIn(NPC.kent, self.content.villagers)
+ self.assertIn(NPC.krobus, self.content.villagers)
+ self.assertIn(NPC.lewis, self.content.villagers)
+ self.assertIn(NPC.linus, self.content.villagers)
+ self.assertIn(NPC.marnie, self.content.villagers)
+ self.assertIn(NPC.pam, self.content.villagers)
+ self.assertIn(NPC.pierre, self.content.villagers)
+ self.assertIn(NPC.robin, self.content.villagers)
+ self.assertIn(NPC.sandy, self.content.villagers)
+ self.assertIn(NPC.vincent, self.content.villagers)
+ self.assertIn(NPC.willy, self.content.villagers)
+ self.assertIn(NPC.wizard, self.content.villagers)
+
+ self.assertEqual(33, len(self.content.villagers))
+
+ def test_all_pelican_town_fishes_are_included(self):
+ fish_names = self.content.fishes.keys()
+
+ self.assertIn(Fish.albacore, fish_names)
+ self.assertIn(Fish.anchovy, fish_names)
+ self.assertIn(Fish.bream, fish_names)
+ self.assertIn(Fish.bullhead, fish_names)
+ self.assertIn(Fish.carp, fish_names)
+ self.assertIn(Fish.catfish, fish_names)
+ self.assertIn(Fish.chub, fish_names)
+ self.assertIn(Fish.dorado, fish_names)
+ self.assertIn(Fish.eel, fish_names)
+ self.assertIn(Fish.flounder, fish_names)
+ self.assertIn(Fish.ghostfish, fish_names)
+ self.assertIn(Fish.goby, fish_names)
+ self.assertIn(Fish.halibut, fish_names)
+ self.assertIn(Fish.herring, fish_names)
+ self.assertIn(Fish.ice_pip, fish_names)
+ self.assertIn(Fish.largemouth_bass, fish_names)
+ self.assertIn(Fish.lava_eel, fish_names)
+ self.assertIn(Fish.lingcod, fish_names)
+ self.assertIn(Fish.midnight_carp, fish_names)
+ self.assertIn(Fish.octopus, fish_names)
+ self.assertIn(Fish.perch, fish_names)
+ self.assertIn(Fish.pike, fish_names)
+ self.assertIn(Fish.pufferfish, fish_names)
+ self.assertIn(Fish.rainbow_trout, fish_names)
+ self.assertIn(Fish.red_mullet, fish_names)
+ self.assertIn(Fish.red_snapper, fish_names)
+ self.assertIn(Fish.salmon, fish_names)
+ self.assertIn(Fish.sandfish, fish_names)
+ self.assertIn(Fish.sardine, fish_names)
+ self.assertIn(Fish.scorpion_carp, fish_names)
+ self.assertIn(Fish.sea_cucumber, fish_names)
+ self.assertIn(Fish.shad, fish_names)
+ self.assertIn(Fish.slimejack, fish_names)
+ self.assertIn(Fish.smallmouth_bass, fish_names)
+ self.assertIn(Fish.squid, fish_names)
+ self.assertIn(Fish.stonefish, fish_names)
+ self.assertIn(Fish.sturgeon, fish_names)
+ self.assertIn(Fish.sunfish, fish_names)
+ self.assertIn(Fish.super_cucumber, fish_names)
+ self.assertIn(Fish.tiger_trout, fish_names)
+ self.assertIn(Fish.tilapia, fish_names)
+ self.assertIn(Fish.tuna, fish_names)
+ self.assertIn(Fish.void_salmon, fish_names)
+ self.assertIn(Fish.walleye, fish_names)
+ self.assertIn(Fish.woodskip, fish_names)
+ self.assertIn(Fish.blobfish, fish_names)
+ self.assertIn(Fish.midnight_squid, fish_names)
+ self.assertIn(Fish.spook_fish, fish_names)
+ self.assertIn(Fish.angler, fish_names)
+ self.assertIn(Fish.crimsonfish, fish_names)
+ self.assertIn(Fish.glacierfish, fish_names)
+ self.assertIn(Fish.legend, fish_names)
+ self.assertIn(Fish.mutant_carp, fish_names)
+ self.assertIn(Fish.clam, fish_names)
+ self.assertIn(Fish.cockle, fish_names)
+ self.assertIn(Fish.crab, fish_names)
+ self.assertIn(Fish.crayfish, fish_names)
+ self.assertIn(Fish.lobster, fish_names)
+ self.assertIn(Fish.mussel, fish_names)
+ self.assertIn(Fish.oyster, fish_names)
+ self.assertIn(Fish.periwinkle, fish_names)
+ self.assertIn(Fish.shrimp, fish_names)
+ self.assertIn(Fish.snail, fish_names)
+
+ self.assertEqual(63, len(self.content.fishes))
diff --git a/worlds/stardew_valley/test/content/TestQiBoard.py b/worlds/stardew_valley/test/content/TestQiBoard.py
new file mode 100644
index 000000000000..b9d940d2c887
--- /dev/null
+++ b/worlds/stardew_valley/test/content/TestQiBoard.py
@@ -0,0 +1,27 @@
+from . import SVContentPackTestBase
+from ...content import content_packs
+from ...data.artisan import MachineSource
+from ...strings.artisan_good_names import ArtisanGood
+from ...strings.crop_names import Fruit
+from ...strings.fish_names import Fish
+from ...strings.machine_names import Machine
+
+
+class TestQiBoard(SVContentPackTestBase):
+ vanilla_packs = SVContentPackTestBase.vanilla_packs + (content_packs.ginger_island_content_pack, content_packs.qi_board_content_pack)
+
+ def test_extended_family_fishes_are_included(self):
+ fish_names = self.content.fishes.keys()
+
+ self.assertIn(Fish.ms_angler, fish_names)
+ self.assertIn(Fish.son_of_crimsonfish, fish_names)
+ self.assertIn(Fish.glacierfish_jr, fish_names)
+ self.assertIn(Fish.legend_ii, fish_names)
+ self.assertIn(Fish.radioactive_carp, fish_names)
+
+ # 63 from pelican town + 3 ginger island exclusive + 5 extended family
+ self.assertEqual(63 + 3 + 5, len(self.content.fishes))
+
+ def test_wines(self):
+ self.assertIn(MachineSource(item=Fruit.qi_fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.qi_fruit)].sources)
+ self.assertIn(MachineSource(item=Fruit.qi_fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources)
diff --git a/worlds/stardew_valley/test/content/__init__.py b/worlds/stardew_valley/test/content/__init__.py
new file mode 100644
index 000000000000..4130dae90dc3
--- /dev/null
+++ b/worlds/stardew_valley/test/content/__init__.py
@@ -0,0 +1,23 @@
+import unittest
+from typing import ClassVar, Tuple
+
+from ...content import content_packs, ContentPack, StardewContent, unpack_content, StardewFeatures, feature
+
+default_features = StardewFeatures(
+ feature.booksanity.BooksanityDisabled(),
+ feature.cropsanity.CropsanityDisabled(),
+ feature.fishsanity.FishsanityNone(),
+ feature.friendsanity.FriendsanityNone()
+)
+
+
+class SVContentPackTestBase(unittest.TestCase):
+ vanilla_packs: ClassVar[Tuple[ContentPack]] = (content_packs.pelican_town, content_packs.the_desert, content_packs.the_farm, content_packs.the_mines)
+ mods: ClassVar[Tuple[str]] = ()
+
+ content: ClassVar[StardewContent]
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ packs = cls.vanilla_packs + tuple(content_packs.by_mod[mod] for mod in cls.mods)
+ cls.content = unpack_content(default_features, packs)
diff --git a/worlds/stardew_valley/test/content/feature/TestFriendsanity.py b/worlds/stardew_valley/test/content/feature/TestFriendsanity.py
new file mode 100644
index 000000000000..804ac0978bb5
--- /dev/null
+++ b/worlds/stardew_valley/test/content/feature/TestFriendsanity.py
@@ -0,0 +1,33 @@
+import unittest
+
+from ....content.feature import friendsanity
+
+
+class TestHeartSteps(unittest.TestCase):
+
+ def test_given_size_of_one_when_calculate_steps_then_advance_one_heart_at_the_time(self):
+ steps = friendsanity.get_heart_steps(4, 1)
+
+ self.assertEqual(steps, (1, 2, 3, 4))
+
+ def test_given_size_of_two_when_calculate_steps_then_advance_two_heart_at_the_time(self):
+ steps = friendsanity.get_heart_steps(6, 2)
+
+ self.assertEqual(steps, (2, 4, 6))
+
+ def test_given_size_of_three_and_max_heart_not_multiple_of_three_when_calculate_steps_then_add_max_as_last_step(self):
+ steps = friendsanity.get_heart_steps(7, 3)
+
+ self.assertEqual(steps, (3, 6, 7))
+
+
+class TestExtractNpcFromLocation(unittest.TestCase):
+
+ def test_given_npc_with_space_in_name_when_extract_then_find_name_and_heart(self):
+ npc = "Mr. Ginger"
+ location_name = friendsanity.to_location_name(npc, 34)
+
+ found_name, found_heart = friendsanity.extract_npc_from_location_name(location_name)
+
+ self.assertEqual(found_name, npc)
+ self.assertEqual(found_heart, 34)
diff --git a/worlds/stardew_valley/test/content/feature/__init__.py b/worlds/stardew_valley/test/content/feature/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/stardew_valley/test/content/mods/TestDeepwoods.py b/worlds/stardew_valley/test/content/mods/TestDeepwoods.py
new file mode 100644
index 000000000000..381502da13ba
--- /dev/null
+++ b/worlds/stardew_valley/test/content/mods/TestDeepwoods.py
@@ -0,0 +1,14 @@
+from ....data.artisan import MachineSource
+from ....mods.mod_data import ModNames
+from ....strings.artisan_good_names import ArtisanGood
+from ....strings.crop_names import Fruit
+from ....strings.machine_names import Machine
+from ....test.content import SVContentPackTestBase
+
+
+class TestArtisanEquipment(SVContentPackTestBase):
+ mods = (ModNames.deepwoods,)
+
+ def test_mango_wine_exists(self):
+ self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.mango)].sources)
+ self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources)
diff --git a/worlds/stardew_valley/test/content/mods/TestJasper.py b/worlds/stardew_valley/test/content/mods/TestJasper.py
new file mode 100644
index 000000000000..40927e67c258
--- /dev/null
+++ b/worlds/stardew_valley/test/content/mods/TestJasper.py
@@ -0,0 +1,27 @@
+from .. import SVContentPackTestBase
+from ....mods.mod_data import ModNames
+from ....strings.villager_names import ModNPC
+
+
+class TestJasperWithoutSVE(SVContentPackTestBase):
+ mods = (ModNames.jasper,)
+
+ def test_gunther_is_added(self):
+ self.assertIn(ModNPC.gunther, self.content.villagers)
+ self.assertEqual(self.content.villagers[ModNPC.gunther].mod_name, ModNames.jasper)
+
+ def test_marlon_is_added(self):
+ self.assertIn(ModNPC.marlon, self.content.villagers)
+ self.assertEqual(self.content.villagers[ModNPC.marlon].mod_name, ModNames.jasper)
+
+
+class TestJasperWithSVE(SVContentPackTestBase):
+ mods = (ModNames.jasper, ModNames.sve)
+
+ def test_gunther_is_added(self):
+ self.assertIn(ModNPC.gunther, self.content.villagers)
+ self.assertEqual(self.content.villagers[ModNPC.gunther].mod_name, ModNames.sve)
+
+ def test_marlon_is_added(self):
+ self.assertIn(ModNPC.marlon, self.content.villagers)
+ self.assertEqual(self.content.villagers[ModNPC.marlon].mod_name, ModNames.sve)
diff --git a/worlds/stardew_valley/test/content/mods/TestSVE.py b/worlds/stardew_valley/test/content/mods/TestSVE.py
new file mode 100644
index 000000000000..4065498d6be7
--- /dev/null
+++ b/worlds/stardew_valley/test/content/mods/TestSVE.py
@@ -0,0 +1,143 @@
+from .. import SVContentPackTestBase
+from ... import SVTestBase
+from .... import options
+from ....content import content_packs
+from ....mods.mod_data import ModNames
+from ....strings.fish_names import SVEFish
+from ....strings.villager_names import ModNPC, NPC
+
+vanilla_villagers = 33
+vanilla_villagers_with_leo = 34
+sve_villagers = 13
+sve_villagers_with_lance = 14
+vanilla_pelican_town_fish = 63
+vanilla_ginger_island_fish = 3
+sve_pelican_town_fish = 16
+sve_ginger_island_fish = 10
+
+
+class TestVanilla(SVContentPackTestBase):
+
+ def test_wizard_is_not_bachelor(self):
+ self.assertFalse(self.content.villagers[NPC.wizard].bachelor)
+
+
+class TestSVE(SVContentPackTestBase):
+ mods = (ModNames.sve,)
+
+ def test_lance_is_not_included(self):
+ self.assertNotIn(ModNPC.lance, self.content.villagers)
+
+ def test_wizard_is_bachelor(self):
+ self.assertTrue(self.content.villagers[NPC.wizard].bachelor)
+ self.assertEqual(self.content.villagers[NPC.wizard].mod_name, ModNames.sve)
+
+ def test_sve_npc_are_included(self):
+ self.assertIn(ModNPC.apples, self.content.villagers)
+ self.assertIn(ModNPC.claire, self.content.villagers)
+ self.assertIn(ModNPC.olivia, self.content.villagers)
+ self.assertIn(ModNPC.sophia, self.content.villagers)
+ self.assertIn(ModNPC.victor, self.content.villagers)
+ self.assertIn(ModNPC.andy, self.content.villagers)
+ self.assertIn(ModNPC.gunther, self.content.villagers)
+ self.assertIn(ModNPC.martin, self.content.villagers)
+ self.assertIn(ModNPC.marlon, self.content.villagers)
+ self.assertIn(ModNPC.morgan, self.content.villagers)
+ self.assertIn(ModNPC.morris, self.content.villagers)
+ self.assertIn(ModNPC.scarlett, self.content.villagers)
+ self.assertIn(ModNPC.susan, self.content.villagers)
+
+ self.assertEqual(vanilla_villagers + sve_villagers, len(self.content.villagers))
+
+ def test_sve_has_sve_fish(self):
+ fish_names = self.content.fishes.keys()
+
+ self.assertIn(SVEFish.bonefish, fish_names)
+ self.assertIn(SVEFish.bull_trout, fish_names)
+ self.assertIn(SVEFish.butterfish, fish_names)
+ self.assertIn(SVEFish.frog, fish_names)
+ self.assertIn(SVEFish.goldenfish, fish_names)
+ self.assertIn(SVEFish.grass_carp, fish_names)
+ self.assertIn(SVEFish.king_salmon, fish_names)
+ self.assertIn(SVEFish.kittyfish, fish_names)
+ self.assertIn(SVEFish.meteor_carp, fish_names)
+ self.assertIn(SVEFish.minnow, fish_names)
+ self.assertIn(SVEFish.puppyfish, fish_names)
+ self.assertIn(SVEFish.radioactive_bass, fish_names)
+ self.assertIn(SVEFish.snatcher_worm, fish_names)
+ self.assertIn(SVEFish.undeadfish, fish_names)
+ self.assertIn(SVEFish.void_eel, fish_names)
+ self.assertIn(SVEFish.water_grub, fish_names)
+
+ self.assertEqual(vanilla_pelican_town_fish + sve_pelican_town_fish, len(self.content.fishes))
+
+
+class TestSVEWithGingerIsland(SVContentPackTestBase):
+ vanilla_packs = SVContentPackTestBase.vanilla_packs + (content_packs.ginger_island_content_pack,)
+ mods = (ModNames.sve,)
+
+ def test_lance_is_included(self):
+ self.assertIn(ModNPC.lance, self.content.villagers)
+
+ def test_other_sve_npc_are_included(self):
+ self.assertIn(ModNPC.apples, self.content.villagers)
+ self.assertIn(ModNPC.claire, self.content.villagers)
+ self.assertIn(ModNPC.olivia, self.content.villagers)
+ self.assertIn(ModNPC.sophia, self.content.villagers)
+ self.assertIn(ModNPC.victor, self.content.villagers)
+ self.assertIn(ModNPC.andy, self.content.villagers)
+ self.assertIn(ModNPC.gunther, self.content.villagers)
+ self.assertIn(ModNPC.martin, self.content.villagers)
+ self.assertIn(ModNPC.marlon, self.content.villagers)
+ self.assertIn(ModNPC.morgan, self.content.villagers)
+ self.assertIn(ModNPC.morris, self.content.villagers)
+ self.assertIn(ModNPC.scarlett, self.content.villagers)
+ self.assertIn(ModNPC.susan, self.content.villagers)
+
+ self.assertEqual(vanilla_villagers_with_leo + sve_villagers_with_lance, len(self.content.villagers))
+
+ def test_sve_has_sve_fish(self):
+ fish_names = self.content.fishes.keys()
+
+ self.assertIn(SVEFish.baby_lunaloo, fish_names)
+ self.assertIn(SVEFish.bonefish, fish_names)
+ self.assertIn(SVEFish.bull_trout, fish_names)
+ self.assertIn(SVEFish.butterfish, fish_names)
+ self.assertIn(SVEFish.clownfish, fish_names)
+ self.assertIn(SVEFish.daggerfish, fish_names)
+ self.assertIn(SVEFish.frog, fish_names)
+ self.assertIn(SVEFish.gemfish, fish_names)
+ self.assertIn(SVEFish.goldenfish, fish_names)
+ self.assertIn(SVEFish.grass_carp, fish_names)
+ self.assertIn(SVEFish.king_salmon, fish_names)
+ self.assertIn(SVEFish.kittyfish, fish_names)
+ self.assertIn(SVEFish.lunaloo, fish_names)
+ self.assertIn(SVEFish.meteor_carp, fish_names)
+ self.assertIn(SVEFish.minnow, fish_names)
+ self.assertIn(SVEFish.puppyfish, fish_names)
+ self.assertIn(SVEFish.radioactive_bass, fish_names)
+ self.assertIn(SVEFish.seahorse, fish_names)
+ self.assertIn(SVEFish.shiny_lunaloo, fish_names)
+ self.assertIn(SVEFish.snatcher_worm, fish_names)
+ self.assertIn(SVEFish.starfish, fish_names)
+ self.assertIn(SVEFish.torpedo_trout, fish_names)
+ self.assertIn(SVEFish.undeadfish, fish_names)
+ self.assertIn(SVEFish.void_eel, fish_names)
+ self.assertIn(SVEFish.water_grub, fish_names)
+ self.assertIn(SVEFish.sea_sponge, fish_names)
+
+ self.assertEqual(vanilla_pelican_town_fish + vanilla_ginger_island_fish + sve_pelican_town_fish + sve_ginger_island_fish, len(self.content.fishes))
+
+
+class TestSVEWithoutGingerIslandE2E(SVTestBase):
+ options = {
+ options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true,
+ options.Mods: ModNames.sve
+ }
+
+ def test_lance_is_not_in_the_pool(self):
+ for item in self.multiworld.itempool:
+ self.assertFalse(("Friendsanity: " + ModNPC.lance) in item.name)
+
+ for location in self.multiworld.get_locations(self.player):
+ self.assertFalse(("Friendsanity: " + ModNPC.lance) in location.name)
diff --git a/worlds/stardew_valley/test/content/mods/__init__.py b/worlds/stardew_valley/test/content/mods/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/stardew_valley/test/long/TestModsLong.py b/worlds/stardew_valley/test/long/TestModsLong.py
new file mode 100644
index 000000000000..395c48ee698a
--- /dev/null
+++ b/worlds/stardew_valley/test/long/TestModsLong.py
@@ -0,0 +1,69 @@
+import unittest
+from itertools import combinations, product
+
+from BaseClasses import get_seed
+from .option_names import all_option_choices, get_option_choices
+from .. import SVTestCase
+from ..assertion import WorldAssertMixin, ModAssertMixin
+from ... import options
+from ...mods.mod_data import ModNames
+
+assert unittest
+
+
+class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase):
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ super().setUpClass()
+ if cls.skip_long_tests:
+ raise unittest.SkipTest("Long tests disabled")
+
+ def test_given_mod_pairs_when_generate_then_basic_checks(self):
+ for mod_pair in combinations(options.Mods.valid_keys, 2):
+ world_options = {
+ options.Mods: frozenset(mod_pair)
+ }
+
+ with self.solo_world_sub_test(f"Mods: {mod_pair}", world_options, world_caching=False) as (multiworld, _):
+ self.assert_basic_checks(multiworld)
+ self.assert_stray_mod_items(list(mod_pair), multiworld)
+
+ def test_given_mod_names_when_generate_paired_with_other_options_then_basic_checks(self):
+ for mod, (option, value) in product(options.Mods.valid_keys, all_option_choices):
+ world_options = {
+ option: value,
+ options.Mods: mod
+ }
+
+ with self.solo_world_sub_test(f"{option.internal_name}: {value}, Mod: {mod}", world_options, world_caching=False) as (multiworld, _):
+ self.assert_basic_checks(multiworld)
+ self.assert_stray_mod_items(mod, multiworld)
+
+ def test_given_no_quest_all_mods_when_generate_with_all_goals_then_basic_checks(self):
+ for goal, (option, value) in product(get_option_choices(options.Goal), all_option_choices):
+ if option is options.QuestLocations:
+ continue
+
+ world_options = {
+ options.Goal: goal,
+ option: value,
+ options.QuestLocations: -1,
+ options.Mods: frozenset(options.Mods.valid_keys),
+ }
+
+ with self.solo_world_sub_test(f"Goal: {goal}, {option.internal_name}: {value}", world_options, world_caching=False) as (multiworld, _):
+ self.assert_basic_checks(multiworld)
+
+ @unittest.skip
+ def test_troubleshoot_option(self):
+ seed = get_seed(78709133382876990000)
+
+ world_options = {
+ options.EntranceRandomization: options.EntranceRandomization.option_buildings,
+ options.Mods: ModNames.sve
+ }
+
+ with self.solo_world_sub_test(world_options=world_options, seed=seed, world_caching=False) as (multiworld, _):
+ self.assert_basic_checks(multiworld)
+ self.assert_stray_mod_items(world_options[options.Mods], multiworld)
diff --git a/worlds/stardew_valley/test/long/TestOptionsLong.py b/worlds/stardew_valley/test/long/TestOptionsLong.py
index c614ddcc36fa..0c8cfcb1e107 100644
--- a/worlds/stardew_valley/test/long/TestOptionsLong.py
+++ b/worlds/stardew_valley/test/long/TestOptionsLong.py
@@ -1,41 +1,47 @@
-from typing import Dict
+import unittest
+from itertools import combinations
-from BaseClasses import MultiWorld
-from Options import SpecialRange
-from .option_names import options_to_include
-from worlds.stardew_valley.test.checks.world_checks import assert_can_win, assert_same_number_items_locations
-from .. import setup_solo_multiworld, SVTestBase
+from BaseClasses import get_seed
+from .option_names import all_option_choices
+from .. import SVTestCase, solo_multiworld
+from ..assertion.world_assert import WorldAssertMixin
+from ... import options
+from ...mods.mod_data import ModNames
-def basic_checks(tester: SVTestBase, 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(SVTestBase):
+class TestGenerateDynamicOptions(WorldAssertMixin, SVTestCase):
def test_given_option_pair_when_generate_then_basic_checks(self):
if self.skip_long_tests:
- return
-
- 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_solo_multiworld(choices)
- basic_checks(self, multiworld)
\ No newline at end of file
+ raise unittest.SkipTest("Long tests disabled")
+
+ for (option1, option1_choice), (option2, option2_choice) in combinations(all_option_choices, 2):
+ if option1 is option2:
+ continue
+
+ world_options = {
+ option1: option1_choice,
+ option2: option2_choice
+ }
+
+ with self.solo_world_sub_test(f"{option1.internal_name}: {option1_choice}, {option2.internal_name}: {option2_choice}",
+ world_options,
+ world_caching=False) \
+ as (multiworld, _):
+ self.assert_basic_checks(multiworld)
+
+
+class TestDynamicOptionDebug(WorldAssertMixin, SVTestCase):
+
+ def test_option_pair_debug(self):
+ option_dict = {
+ options.Goal.internal_name: options.Goal.option_master_angler,
+ options.QuestLocations.internal_name: -1,
+ options.Fishsanity.internal_name: options.Fishsanity.option_all,
+ options.Mods.internal_name: frozenset({ModNames.sve}),
+ }
+ for i in range(1):
+ seed = get_seed()
+ with self.subTest(f"Seed: {seed}"):
+ print(f"Seed: {seed}")
+ with solo_multiworld(option_dict, seed=seed) as (multiworld, _):
+ self.assert_basic_checks(multiworld)
diff --git a/worlds/stardew_valley/test/long/TestPreRolledRandomness.py b/worlds/stardew_valley/test/long/TestPreRolledRandomness.py
new file mode 100644
index 000000000000..f233fc36dc84
--- /dev/null
+++ b/worlds/stardew_valley/test/long/TestPreRolledRandomness.py
@@ -0,0 +1,28 @@
+import unittest
+
+from BaseClasses import get_seed
+from .. import SVTestCase
+from ..assertion import WorldAssertMixin
+from ... import options
+
+
+class TestGeneratePreRolledRandomness(WorldAssertMixin, SVTestCase):
+ def test_given_pre_rolled_difficult_randomness_when_generate_then_basic_checks(self):
+ if self.skip_long_tests:
+ raise unittest.SkipTest("Long tests disabled")
+
+ choices = {
+ options.EntranceRandomization.internal_name: options.EntranceRandomization.option_buildings,
+ options.BundleRandomization.internal_name: options.BundleRandomization.option_remixed,
+ options.BundlePrice.internal_name: options.BundlePrice.option_maximum
+ }
+
+ num_tests = 1000
+ for i in range(num_tests):
+ seed = get_seed() # Put seed in parameter to test
+ with self.solo_world_sub_test(f"Entrance Randomizer and Remixed Bundles",
+ choices,
+ seed=seed,
+ world_caching=False) \
+ as (multiworld, _):
+ self.assert_basic_checks(multiworld)
diff --git a/worlds/stardew_valley/test/long/TestRandomWorlds.py b/worlds/stardew_valley/test/long/TestRandomWorlds.py
index 6ba814aba2c4..6d4931280a79 100644
--- a/worlds/stardew_valley/test/long/TestRandomWorlds.py
+++ b/worlds/stardew_valley/test/long/TestRandomWorlds.py
@@ -1,19 +1,16 @@
-from typing import Dict, List
import random
+import unittest
+from typing import Dict
-from BaseClasses import MultiWorld
-from Options import SpecialRange, Range
+from BaseClasses import MultiWorld, get_seed
+from Options import NamedRange, Range
from .option_names import options_to_include
-from .. import setup_solo_multiworld, SVTestBase
-from ..checks.goal_checks import assert_perfection_world_is_valid, assert_goal_world_is_valid
-from ..checks.option_checks import assert_can_reach_island_if_should, assert_cropsanity_same_number_items_and_locations, \
- assert_festivals_give_access_to_deluxe_scarecrow
-from ..checks.world_checks import assert_same_number_items_locations, assert_victory_exists
-from ... import options
+from .. import SVTestCase
+from ..assertion import GoalAssertMixin, OptionAssertMixin, WorldAssertMixin
def get_option_choices(option) -> Dict[str, int]:
- if issubclass(option, SpecialRange):
+ if issubclass(option, NamedRange):
return option.special_range_names
if issubclass(option, Range):
return {f"{val}": val for val in range(option.range_start, option.range_end + 1)}
@@ -22,16 +19,10 @@ def get_option_choices(option) -> Dict[str, int]:
return {}
-def generate_random_multiworld(world_id: int):
- world_options = generate_random_world_options(world_id)
- multiworld = setup_solo_multiworld(world_options, seed=world_id)
- return multiworld
-
-
-def generate_random_world_options(world_id: int) -> Dict[str, int]:
+def generate_random_world_options(seed: int) -> Dict[str, int]:
num_options = len(options_to_include)
world_options = dict()
- rng = random.Random(world_id)
+ rng = random.Random(seed)
for option_index in range(0, num_options):
option = options_to_include[option_index]
option_choices = get_option_choices(option)
@@ -58,42 +49,38 @@ def get_number_log_steps(number_worlds: int) -> int:
return 100
-def generate_many_worlds(number_worlds: int, start_index: int) -> Dict[int, MultiWorld]:
- num_steps = get_number_log_steps(number_worlds)
- log_step = number_worlds / num_steps
- multiworlds = dict()
- print(f"Generating {number_worlds} Solo Multiworlds [Start Seed: {start_index}] for Stardew Valley...")
- for world_number in range(0, number_worlds + 1):
- world_id = world_number + start_index
- multiworld = generate_random_multiworld(world_id)
- multiworlds[world_id] = multiworld
- if world_number > 0 and world_number % log_step == 0:
- print(f"Generated {world_number}/{number_worlds} worlds [{(world_number * 100) // number_worlds}%]")
- print(f"Finished generating {number_worlds} Solo Multiworlds for Stardew Valley")
- return multiworlds
+class TestGenerateManyWorlds(GoalAssertMixin, OptionAssertMixin, WorldAssertMixin, SVTestCase):
+ def test_generate_many_worlds_then_check_results(self):
+ if self.skip_long_tests:
+ raise unittest.SkipTest("Long tests disabled")
+
+ number_worlds = 10 if self.skip_long_tests else 1000
+ seed = get_seed()
+ self.generate_and_check_many_worlds(number_worlds, seed)
+
+ def generate_and_check_many_worlds(self, number_worlds: int, seed: int):
+ num_steps = get_number_log_steps(number_worlds)
+ log_step = number_worlds / num_steps
+ print(f"Generating {number_worlds} Solo Multiworlds [Start Seed: {seed}] for Stardew Valley...")
+ for world_number in range(0, number_worlds + 1):
-def check_every_multiworld_is_valid(tester: SVTestBase, multiworlds: Dict[int, MultiWorld]):
- for multiworld_id in multiworlds:
- multiworld = multiworlds[multiworld_id]
- with tester.subTest(f"Checking validity of world {multiworld_id}"):
- check_multiworld_is_valid(tester, multiworld_id, multiworld)
+ world_seed = world_number + seed
+ world_options = generate_random_world_options(world_seed)
+ with self.solo_world_sub_test(f"Multiworld: {world_seed}", world_options, seed=world_seed, world_caching=False) as (multiworld, _):
+ self.assert_multiworld_is_valid(multiworld)
-def check_multiworld_is_valid(tester: SVTestBase, multiworld_id: int, multiworld: MultiWorld):
- assert_victory_exists(tester, multiworld)
- assert_same_number_items_locations(tester, multiworld)
- assert_goal_world_is_valid(tester, multiworld)
- assert_can_reach_island_if_should(tester, multiworld)
- assert_cropsanity_same_number_items_and_locations(tester, multiworld)
- assert_festivals_give_access_to_deluxe_scarecrow(tester, multiworld)
+ if world_number > 0 and world_number % log_step == 0:
+ print(f"Generated and Verified {world_number}/{number_worlds} worlds [{(world_number * 100) // number_worlds}%]")
+ print(f"Finished generating and verifying {number_worlds} Solo Multiworlds for Stardew Valley")
-class TestGenerateManyWorlds(SVTestBase):
- def test_generate_many_worlds_then_check_results(self):
- if self.skip_long_tests:
- return
- number_worlds = 1000
- start_index = random.Random().randint(0, 9999999999)
- multiworlds = generate_many_worlds(number_worlds, start_index)
- check_every_multiworld_is_valid(self, multiworlds)
+ def assert_multiworld_is_valid(self, multiworld: MultiWorld):
+ self.assert_victory_exists(multiworld)
+ self.assert_same_number_items_locations(multiworld)
+ self.assert_goal_world_is_valid(multiworld)
+ self.assert_can_reach_island_if_should(multiworld)
+ self.assert_cropsanity_same_number_items_and_locations(multiworld)
+ self.assert_festivals_give_access_to_deluxe_scarecrow(multiworld)
+ self.assert_has_festival_recipes(multiworld)
diff --git a/worlds/stardew_valley/test/long/option_names.py b/worlds/stardew_valley/test/long/option_names.py
index 9bb950d3a64b..9f3cf98b872c 100644
--- a/worlds/stardew_valley/test/long/option_names.py
+++ b/worlds/stardew_valley/test/long/option_names.py
@@ -1,7 +1,30 @@
-from worlds.stardew_valley.options import stardew_valley_option_classes
+from typing import Dict
-options_to_exclude = ["profit_margin", "starting_money", "multiple_day_sleep_enabled", "multiple_day_sleep_cost",
+from Options import NamedRange
+from ... import StardewValleyWorld
+
+options_to_exclude = {"profit_margin", "starting_money", "multiple_day_sleep_enabled", "multiple_day_sleep_cost",
"experience_multiplier", "friendship_multiplier", "debris_multiplier",
- "quick_start", "gifting", "gift_tax"]
-options_to_include = [option_to_include for option_to_include in stardew_valley_option_classes
- if option_to_include.internal_name not in options_to_exclude]
+ "quick_start", "gifting", "gift_tax",
+ "progression_balancing", "accessibility", "start_inventory", "start_hints", "death_link"}
+
+options_to_include = [option
+ for option_name, option in StardewValleyWorld.options_dataclass.type_hints.items()
+ if option_name not in options_to_exclude]
+
+
+def get_option_choices(option) -> Dict[str, int]:
+ if issubclass(option, NamedRange):
+ return option.special_range_names
+ elif option.options:
+ return option.options
+ return {}
+
+
+all_option_choices = [(option, value)
+ for option in options_to_include
+ if option.options
+ for value in get_option_choices(option)
+ if option.default != get_option_choices(option)[value]]
+
+assert all_option_choices
diff --git a/worlds/stardew_valley/test/mods/TestBiggerBackpack.py b/worlds/stardew_valley/test/mods/TestBiggerBackpack.py
index 0265f61731c5..f6d312976c45 100644
--- a/worlds/stardew_valley/test/mods/TestBiggerBackpack.py
+++ b/worlds/stardew_valley/test/mods/TestBiggerBackpack.py
@@ -1,51 +1,52 @@
from .. import SVTestBase
-from ... import options
from ...mods.mod_data import ModNames
+from ...options import Mods, BackpackProgression
class TestBiggerBackpackVanilla(SVTestBase):
- options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla,
- options.Mods.internal_name: ModNames.big_backpack}
+ options = {
+ BackpackProgression.internal_name: BackpackProgression.option_vanilla,
+ Mods.internal_name: ModNames.big_backpack
+ }
- def test_no_backpack_in_pool(self):
- item_names = {item.name for item in self.multiworld.get_items()}
- self.assertNotIn("Progressive Backpack", item_names)
+ def test_no_backpack(self):
+ with self.subTest(check="no items"):
+ item_names = {item.name for item in self.multiworld.get_items()}
+ self.assertNotIn("Progressive Backpack", item_names)
- def test_no_backpack_locations(self):
- location_names = {location.name for location in self.multiworld.get_locations()}
- self.assertNotIn("Large Pack", location_names)
- self.assertNotIn("Deluxe Pack", location_names)
- self.assertNotIn("Premium Pack", location_names)
+ with self.subTest(check="no locations"):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertNotIn("Large Pack", location_names)
+ self.assertNotIn("Deluxe Pack", location_names)
+ self.assertNotIn("Premium Pack", location_names)
class TestBiggerBackpackProgressive(SVTestBase):
- options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive,
- options.Mods.internal_name: ModNames.big_backpack}
-
- def test_backpack_is_in_pool_3_times(self):
- item_names = [item.name for item in self.multiworld.get_items()]
- self.assertEqual(item_names.count("Progressive Backpack"), 3)
-
- def test_3_backpack_locations(self):
- location_names = {location.name for location in self.multiworld.get_locations()}
- self.assertIn("Large Pack", location_names)
- self.assertIn("Deluxe Pack", location_names)
- self.assertIn("Premium Pack", location_names)
-
-
-class TestBiggerBackpackEarlyProgressive(SVTestBase):
- options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive,
- options.Mods.internal_name: ModNames.big_backpack}
-
- def test_backpack_is_in_pool_3_times(self):
- item_names = [item.name for item in self.multiworld.get_items()]
- self.assertEqual(item_names.count("Progressive Backpack"), 3)
-
- def test_3_backpack_locations(self):
- location_names = {location.name for location in self.multiworld.get_locations()}
- self.assertIn("Large Pack", location_names)
- self.assertIn("Deluxe Pack", location_names)
- self.assertIn("Premium Pack", location_names)
-
- def test_progressive_backpack_is_in_early_pool(self):
- self.assertIn("Progressive Backpack", self.multiworld.early_items[1])
+ options = {
+ BackpackProgression.internal_name: BackpackProgression.option_progressive,
+ Mods.internal_name: ModNames.big_backpack
+ }
+
+ def test_backpack(self):
+ with self.subTest(check="has items"):
+ item_names = [item.name for item in self.multiworld.get_items()]
+ self.assertEqual(item_names.count("Progressive Backpack"), 3)
+
+ with self.subTest(check="has locations"):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ self.assertIn("Large Pack", location_names)
+ self.assertIn("Deluxe Pack", location_names)
+ self.assertIn("Premium Pack", location_names)
+
+
+class TestBiggerBackpackEarlyProgressive(TestBiggerBackpackProgressive):
+ options = {
+ BackpackProgression.internal_name: BackpackProgression.option_early_progressive,
+ Mods.internal_name: ModNames.big_backpack
+ }
+
+ def test_backpack(self):
+ super().test_backpack()
+
+ with self.subTest(check="is early"):
+ self.assertIn("Progressive Backpack", self.multiworld.early_items[1])
diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py
index a3198e4d2a24..97184b1338b8 100644
--- a/worlds/stardew_valley/test/mods/TestMods.py
+++ b/worlds/stardew_valley/test/mods/TestMods.py
@@ -1,101 +1,80 @@
-from typing import List, Union
-import unittest
import random
-import sys
-
-from BaseClasses import MultiWorld
-from worlds.stardew_valley.test import setup_solo_multiworld
-from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestBase
-from worlds.stardew_valley import options, locations, items, Group, ItemClassification, StardewOptions
-from worlds.stardew_valley.mods.mod_data import ModNames
-from worlds.stardew_valley.regions import RandomizationFlag, create_final_connections, randomize_connections, create_final_regions
-from worlds.stardew_valley.items import item_table, items_by_group
-from worlds.stardew_valley.locations import location_table, LocationTags
-from worlds.stardew_valley.options import stardew_valley_option_classes, Mods, EntranceRandomization
-
-mod_list = ["DeepWoods", "Tractor Mod", "Bigger Backpack",
- "Luck Skill", "Magic", "Socializing Skill", "Archaeology",
- "Cooking Skill", "Binning Skill", "Juna - Roommate NPC",
- "Professor Jasper Thomas", "Alec Revisited", "Custom NPC - Yoba", "Custom NPC Eugene",
- "'Prophet' Wellwick", "Mister Ginger (cat npc)", "Shiko - New Custom NPC", "Delores - Custom NPC",
- "Ayeisha - The Postal Worker (Custom NPC)", "Custom NPC - Riley", "Skull Cavern Elevator"]
-
-
-def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld):
- if isinstance(chosen_mods, str):
- chosen_mods = [chosen_mods]
- for multiworld_item in multiworld.get_items():
- item = item_table[multiworld_item.name]
- tester.assertTrue(item.mod_name is None or item.mod_name in chosen_mods)
- for multiworld_location in multiworld.get_locations():
- if multiworld_location.event:
- continue
- location = location_table[multiworld_location.name]
- tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods)
-
-
-class TestGenerateModsOptions(SVTestBase):
+
+from BaseClasses import get_seed
+from .. import SVTestBase, SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, complete_options_with_default, solo_multiworld
+from ..assertion import ModAssertMixin, WorldAssertMixin
+from ... import items, Group, ItemClassification
+from ... import options
+from ...items import items_by_group
+from ...options import SkillProgression, Walnutsanity
+from ...regions import RandomizationFlag, randomize_connections, create_final_connections_and_regions
+
+
+class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase):
def test_given_single_mods_when_generate_then_basic_checks(self):
- for mod in mod_list:
- with self.subTest(f"Mod: {mod}"):
- multi_world = setup_solo_multiworld({Mods: mod})
- basic_checks(self, multi_world)
- check_stray_mod_items(mod, self, multi_world)
-
- def test_given_mod_pairs_when_generate_then_basic_checks(self):
- if self.skip_long_tests:
- return
- num_mods = len(mod_list)
- for mod1_index in range(0, num_mods):
- for mod2_index in range(mod1_index + 1, num_mods):
- mod1 = mod_list[mod1_index]
- mod2 = mod_list[mod2_index]
- mods = (mod1, mod2)
- with self.subTest(f"Mods: {mods}"):
- multiworld = setup_solo_multiworld({Mods: mods})
- basic_checks(self, multiworld)
- check_stray_mod_items(list(mods), self, multiworld)
+ for mod in options.Mods.valid_keys:
+ world_options = {options.Mods: mod, options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false}
+ with self.solo_world_sub_test(f"Mod: {mod}", world_options) as (multi_world, _):
+ self.assert_basic_checks(multi_world)
+ self.assert_stray_mod_items(mod, multi_world)
def test_given_mod_names_when_generate_paired_with_entrance_randomizer_then_basic_checks(self):
- for option in EntranceRandomization.options:
- for mod in mod_list:
- with self.subTest(f"entrance_randomization: {option}, Mod: {mod}"):
- multiworld = setup_solo_multiworld({EntranceRandomization.internal_name: option, Mods: mod})
- basic_checks(self, multiworld)
- check_stray_mod_items(mod, self, multiworld)
-
- def test_given_mod_names_when_generate_paired_with_other_options_then_basic_checks(self):
- if self.skip_long_tests:
- return
- for option in stardew_valley_option_classes:
- if not option.options:
- continue
- for value in option.options:
- for mod in mod_list:
- with self.subTest(f"{option.internal_name}: {value}, Mod: {mod}"):
- multiworld = setup_solo_multiworld({option.internal_name: option.options[value], Mods: mod})
- basic_checks(self, multiworld)
- check_stray_mod_items(mod, self, multiworld)
+ for option in options.EntranceRandomization.options:
+ for mod in options.Mods.valid_keys:
+ world_options = {
+ options.EntranceRandomization: options.EntranceRandomization.options[option],
+ options.Mods: mod,
+ options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false
+ }
+ with self.solo_world_sub_test(f"entrance_randomization: {option}, Mod: {mod}", world_options) as (multi_world, _):
+ self.assert_basic_checks(multi_world)
+ self.assert_stray_mod_items(mod, multi_world)
+
+ def test_allsanity_all_mods_when_generate_then_basic_checks(self):
+ with self.solo_world_sub_test(world_options=allsanity_mods_6_x_x()) as (multi_world, _):
+ self.assert_basic_checks(multi_world)
+
+ def test_allsanity_all_mods_exclude_island_when_generate_then_basic_checks(self):
+ world_options = allsanity_mods_6_x_x()
+ world_options.update({options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true})
+ with self.solo_world_sub_test(world_options=world_options) as (multi_world, _):
+ self.assert_basic_checks(multi_world)
+
+
+class TestBaseLocationDependencies(SVTestBase):
+ options = {
+ options.Mods.internal_name: frozenset(options.Mods.valid_keys),
+ options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
+ options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized
+ }
class TestBaseItemGeneration(SVTestBase):
options = {
- options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive,
+ options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries,
+ options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
- options.Mods.internal_name: mod_list
+ options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
+ options.Shipsanity.internal_name: options.Shipsanity.option_everything,
+ options.Chefsanity.internal_name: options.Chefsanity.option_all,
+ options.Craftsanity.internal_name: options.Craftsanity.option_all,
+ options.Booksanity.internal_name: options.Booksanity.option_all,
+ Walnutsanity.internal_name: Walnutsanity.preset_all,
+ options.Mods.internal_name: frozenset(options.Mods.valid_keys)
}
def test_all_progression_items_are_added_to_the_pool(self):
all_created_items = [item.name for item in self.multiworld.itempool]
# Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression
items_to_ignore = [event.name for event in items.events]
+ items_to_ignore.extend(deprecated.name for deprecated in items.items_by_group[Group.DEPRECATED])
items_to_ignore.extend(season.name for season in items.items_by_group[Group.SEASON])
items_to_ignore.extend(weapon.name for weapon in items.items_by_group[Group.WEAPON])
- items_to_ignore.extend(footwear.name for footwear in items.items_by_group[Group.FOOTWEAR])
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK])
+ items_to_ignore.append("The Gateway Gazette")
progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression
and item.name not in items_to_ignore]
for progression_item in progression_items:
@@ -105,21 +84,27 @@ def test_all_progression_items_are_added_to_the_pool(self):
class TestNoGingerIslandModItemGeneration(SVTestBase):
options = {
- options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive,
+ options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries,
+ options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
+ options.Shipsanity.internal_name: options.Shipsanity.option_everything,
+ options.Chefsanity.internal_name: options.Chefsanity.option_all,
+ options.Craftsanity.internal_name: options.Craftsanity.option_all,
+ options.Booksanity.internal_name: options.Booksanity.option_all,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
- options.Mods.internal_name: mod_list
+ options.Mods.internal_name: frozenset(options.Mods.valid_keys)
}
def test_all_progression_items_except_island_are_added_to_the_pool(self):
all_created_items = [item.name for item in self.multiworld.itempool]
# Ignore all the stuff that the algorithm chooses one of, instead of all, to fulfill logical progression
items_to_ignore = [event.name for event in items.events]
+ items_to_ignore.extend(deprecated.name for deprecated in items.items_by_group[Group.DEPRECATED])
items_to_ignore.extend(season.name for season in items.items_by_group[Group.SEASON])
items_to_ignore.extend(weapon.name for weapon in items.items_by_group[Group.WEAPON])
- items_to_ignore.extend(footwear.name for footwear in items.items_by_group[Group.FOOTWEAR])
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK])
+ items_to_ignore.append("The Gateway Gazette")
progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression
and item.name not in items_to_ignore]
for progression_item in progression_items:
@@ -130,49 +115,49 @@ def test_all_progression_items_except_island_are_added_to_the_pool(self):
self.assertIn(progression_item.name, all_created_items)
-class TestModEntranceRando(unittest.TestCase):
+class TestModEntranceRando(SVTestCase):
def test_mod_entrance_randomization(self):
-
for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
(options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION),
+ (options.EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS),
(options.EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]:
- with self.subTest(option=option, flag=flag):
- seed = random.randrange(sys.maxsize)
- rand = random.Random(seed)
- world_options = StardewOptions({options.EntranceRandomization.internal_name: option,
- options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
- options.Mods.internal_name: mod_list})
- final_regions = create_final_regions(world_options)
- final_connections = create_final_connections(world_options)
-
- regions_by_name = {region.name: region for region in final_regions}
- _, randomized_connections = randomize_connections(rand, world_options, regions_by_name)
-
- for connection in final_connections:
+ sv_options = complete_options_with_default({
+ options.EntranceRandomization.internal_name: option,
+ options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
+ SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
+ options.Mods.internal_name: frozenset(options.Mods.valid_keys)
+ })
+ seed = get_seed()
+ rand = random.Random(seed)
+ with self.subTest(option=option, flag=flag, seed=seed):
+ final_connections, final_regions = create_final_connections_and_regions(sv_options)
+
+ _, randomized_connections = randomize_connections(rand, sv_options, final_regions, final_connections)
+
+ for connection_name in final_connections:
+ connection = final_connections[connection_name]
if flag in connection.flag:
- connection_in_randomized = connection.name in randomized_connections
+ connection_in_randomized = connection_name in randomized_connections
reverse_in_randomized = connection.reverse in randomized_connections
- self.assertTrue(connection_in_randomized,
- f"Connection {connection.name} should be randomized but it is not in the output. Seed = {seed}")
- self.assertTrue(reverse_in_randomized,
- f"Connection {connection.reverse} should be randomized but it is not in the output. Seed = {seed}")
+ self.assertTrue(connection_in_randomized, f"Connection {connection_name} should be randomized but it is not in the output")
+ self.assertTrue(reverse_in_randomized, f"Connection {connection.reverse} should be randomized but it is not in the output.")
self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()),
- f"Connections are duplicated in randomization. Seed = {seed}")
+ f"Connections are duplicated in randomization.")
-class TestModTraps(SVTestBase):
+class TestModTraps(SVTestCase):
def test_given_traps_when_generate_then_all_traps_in_pool(self):
- trap_option = options.TrapItems
- for value in trap_option.options:
+ for value in options.TrapItems.options:
if value == "no_traps":
continue
- world_options = self.allsanity_options_without_mods()
- world_options.update({options.TrapItems.internal_name: trap_option.options[value], Mods: "Magic"})
- multi_world = setup_solo_multiworld(world_options)
- trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups]
- multiworld_items = [item.name for item in multi_world.get_items()]
- for item in trap_items:
- with self.subTest(f"Option: {value}, Item: {item}"):
- self.assertIn(item, multiworld_items)
+
+ world_options = allsanity_no_mods_6_x_x()
+ world_options.update({options.TrapItems.internal_name: options.TrapItems.options[value], options.Mods.internal_name: "Magic"})
+ with solo_multiworld(world_options) as (multi_world, _):
+ trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups]
+ multiworld_items = [item.name for item in multi_world.get_items()]
+ for item in trap_items:
+ with self.subTest(f"Option: {value}, Item: {item}"):
+ self.assertIn(item, multiworld_items)
diff --git a/worlds/stardew_valley/test/performance/TestPerformance.py b/worlds/stardew_valley/test/performance/TestPerformance.py
new file mode 100644
index 000000000000..b5ad0cae66c6
--- /dev/null
+++ b/worlds/stardew_valley/test/performance/TestPerformance.py
@@ -0,0 +1,232 @@
+import os
+import time
+import unittest
+from dataclasses import dataclass
+from statistics import mean, median, variance, stdev
+from typing import List
+
+from BaseClasses import get_seed
+from Fill import distribute_items_restrictive, balance_multiworld_progression
+from worlds import AutoWorld
+from .. import SVTestCase, minimal_locations_maximal_items, setup_multiworld, default_6_x_x, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x
+
+assert default_6_x_x
+assert allsanity_no_mods_6_x_x
+
+default_number_generations = 25
+acceptable_deviation = 4
+
+
+@dataclass
+class PerformanceResults:
+ case: SVTestCase
+
+ amount_of_players: int
+ results: List[float]
+ acceptable_mean: float
+
+ def __repr__(self):
+ size = size_name(self.amount_of_players)
+
+ total_time = sum(self.results)
+ mean_time = mean(self.results)
+ median_time = median(self.results)
+ stdev_time = stdev(self.results, mean_time)
+ variance_time = variance(self.results, mean_time)
+
+ return f"""Generated {len(self.results)} {size} multiworlds in {total_time:.2f} seconds. Average {mean_time:.2f} seconds (Acceptable: {self.acceptable_mean:.2f})
+Mean: {mean_time:.2f} Median: {median_time:.2f} Stdeviation: {stdev_time:.2f} Variance: {variance_time:.4f} Deviation percent: {stdev_time / mean_time:.2%}"""
+
+
+class SVPerformanceTestCase(SVTestCase):
+ acceptable_time_per_player: float
+ results: List[PerformanceResults]
+
+ # Set False to not call the fill in the tests"""
+ skip_fill: bool = True
+ # Set True to print results as CSV"""
+ csv: bool = False
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ performance_tests_key = "performance"
+ if performance_tests_key not in os.environ or os.environ[performance_tests_key] != "True":
+ raise unittest.SkipTest("Performance tests disabled")
+
+ super().setUpClass()
+
+ fill_tests_key = "fill"
+ if fill_tests_key in os.environ:
+ cls.skip_fill = os.environ[fill_tests_key] != "True"
+
+ fixed_seed_key = "fixed_seed"
+ if fixed_seed_key in os.environ:
+ cls.fixed_seed = bool(os.environ[fixed_seed_key])
+ else:
+ cls.fixed_seed = False
+
+ number_generations_key = "number_gen"
+ if number_generations_key in os.environ:
+ cls.number_generations = int(os.environ[number_generations_key])
+ else:
+ cls.number_generations = default_number_generations
+
+ csv_key = "csv"
+ if csv_key in os.environ:
+ cls.csv = bool(os.environ[csv_key])
+
+ @classmethod
+ def tearDownClass(cls) -> None:
+ if cls.csv:
+ csved_results = (f"{type(result.case).__name__},{result.amount_of_players},{val:.6f}"
+ for result in cls.results for val in result.results)
+ for r in csved_results:
+ print(r)
+ else:
+ case = None
+ for result in cls.results:
+ if type(result.case) is not case:
+ case = type(result.case)
+ print(case.__name__)
+ print(result)
+ print()
+
+ super().tearDownClass()
+
+ def performance_test_multiworld(self, options):
+ amount_of_players = len(options)
+ acceptable_average_time = self.acceptable_time_per_player * amount_of_players
+ total_time = 0
+ all_times = []
+ seeds = [get_seed() for _ in range(self.number_generations)] if not self.fixed_seed else [85635032403287291967] * self.number_generations
+
+ for i, seed in enumerate(seeds):
+ with self.subTest(f"Seed: {seed}"):
+ time_before = time.time()
+
+ print("Starting world setup")
+ multiworld = setup_multiworld(options, seed)
+ if not self.skip_fill:
+ distribute_items_restrictive(multiworld)
+ AutoWorld.call_all(multiworld, 'post_fill')
+ if multiworld.players > 1:
+ balance_multiworld_progression(multiworld)
+
+ time_after = time.time()
+ elapsed_time = time_after - time_before
+ total_time += elapsed_time
+ all_times.append(elapsed_time)
+ print(f"Multiworld {i + 1}/{self.number_generations} [{seed}] generated in {elapsed_time:.4f} seconds")
+ # tester.assertLessEqual(elapsed_time, acceptable_average_time * acceptable_deviation)
+
+ self.results.append(PerformanceResults(self, amount_of_players, all_times, acceptable_average_time))
+ self.assertLessEqual(mean(all_times), acceptable_average_time)
+
+
+def size_name(number_players):
+ if number_players == 1:
+ return "solo"
+ elif number_players == 2:
+ return "duo"
+ elif number_players == 3:
+ return "trio"
+ return f"{number_players}-player"
+
+
+class TestDefaultOptions(SVPerformanceTestCase):
+ acceptable_time_per_player = 2
+ options = default_6_x_x()
+ results = []
+
+ def test_solo(self):
+ number_players = 1
+ multiworld_options = [self.options] * number_players
+ self.performance_test_multiworld(multiworld_options)
+
+ def test_duo(self):
+ number_players = 2
+ multiworld_options = [self.options] * number_players
+ self.performance_test_multiworld(multiworld_options)
+
+ def test_5_player(self):
+ number_players = 5
+ multiworld_options = [self.options] * number_players
+ self.performance_test_multiworld(multiworld_options)
+
+ @unittest.skip
+ def test_10_player(self):
+ number_players = 10
+ multiworld_options = [self.options] * number_players
+ self.performance_test_multiworld(multiworld_options)
+
+
+class TestMinLocationMaxItems(SVPerformanceTestCase):
+ acceptable_time_per_player = 0.3
+ options = minimal_locations_maximal_items()
+ results = []
+
+ def test_solo(self):
+ number_players = 1
+ multiworld_options = [self.options] * number_players
+ self.performance_test_multiworld(multiworld_options)
+
+ def test_duo(self):
+ number_players = 2
+ multiworld_options = [self.options] * number_players
+ self.performance_test_multiworld(multiworld_options)
+
+ def test_5_player(self):
+ number_players = 5
+ multiworld_options = [self.options] * number_players
+ self.performance_test_multiworld(multiworld_options)
+
+ def test_10_player(self):
+ number_players = 10
+ multiworld_options = [self.options] * number_players
+ self.performance_test_multiworld(multiworld_options)
+
+
+class TestAllsanityWithoutMods(SVPerformanceTestCase):
+ acceptable_time_per_player = 10
+ options = allsanity_no_mods_6_x_x()
+ results = []
+
+ def test_solo(self):
+ number_players = 1
+ multiworld_options = [self.options] * number_players
+ self.performance_test_multiworld(multiworld_options)
+
+ def test_duo(self):
+ number_players = 2
+ multiworld_options = [self.options] * number_players
+ self.performance_test_multiworld(multiworld_options)
+
+ @unittest.skip
+ def test_5_player(self):
+ number_players = 5
+ multiworld_options = [self.options] * number_players
+ self.performance_test_multiworld(multiworld_options)
+
+ @unittest.skip
+ def test_10_player(self):
+ number_players = 10
+ multiworld_options = [self.options] * number_players
+ self.performance_test_multiworld(multiworld_options)
+
+
+class TestAllsanityWithMods(SVPerformanceTestCase):
+ acceptable_time_per_player = 25
+ options = allsanity_mods_6_x_x()
+ results = []
+
+ @unittest.skip
+ def test_solo(self):
+ number_players = 1
+ multiworld_options = [self.options] * number_players
+ self.performance_test_multiworld(multiworld_options)
+
+ @unittest.skip
+ def test_duo(self):
+ number_players = 2
+ multiworld_options = [self.options] * number_players
+ self.performance_test_multiworld(multiworld_options)
diff --git a/worlds/stardew_valley/test/performance/__init__.py b/worlds/stardew_valley/test/performance/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/stardew_valley/test/rules/TestArcades.py b/worlds/stardew_valley/test/rules/TestArcades.py
new file mode 100644
index 000000000000..fb62a456378a
--- /dev/null
+++ b/worlds/stardew_valley/test/rules/TestArcades.py
@@ -0,0 +1,97 @@
+from ... import options
+from ...test import SVTestBase
+
+
+class TestArcadeMachinesLogic(SVTestBase):
+ options = {
+ options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling,
+ }
+
+ def test_prairie_king(self):
+ self.assertFalse(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state))
+ self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state))
+ self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state))
+ self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
+
+ boots = self.create_item("JotPK: Progressive Boots")
+ gun = self.create_item("JotPK: Progressive Gun")
+ ammo = self.create_item("JotPK: Progressive Ammo")
+ life = self.create_item("JotPK: Extra Life")
+ drop = self.create_item("JotPK: Increased Drop Rate")
+
+ self.multiworld.state.collect(boots, event=True)
+ self.multiworld.state.collect(gun, event=True)
+ self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state))
+ self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state))
+ self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state))
+ self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
+ self.remove(boots)
+ self.remove(gun)
+
+ self.multiworld.state.collect(boots, event=True)
+ self.multiworld.state.collect(boots, event=True)
+ self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state))
+ self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state))
+ self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state))
+ self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
+ self.remove(boots)
+ self.remove(boots)
+
+ self.multiworld.state.collect(boots, event=True)
+ self.multiworld.state.collect(gun, event=True)
+ self.multiworld.state.collect(ammo, event=True)
+ self.multiworld.state.collect(life, event=True)
+ self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state))
+ self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state))
+ self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state))
+ self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
+ self.remove(boots)
+ self.remove(gun)
+ self.remove(ammo)
+ self.remove(life)
+
+ self.multiworld.state.collect(boots, event=True)
+ self.multiworld.state.collect(gun, event=True)
+ self.multiworld.state.collect(gun, event=True)
+ self.multiworld.state.collect(ammo, event=True)
+ self.multiworld.state.collect(ammo, event=True)
+ self.multiworld.state.collect(life, event=True)
+ self.multiworld.state.collect(drop, event=True)
+ self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state))
+ self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state))
+ self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state))
+ self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
+ self.remove(boots)
+ self.remove(gun)
+ self.remove(gun)
+ self.remove(ammo)
+ self.remove(ammo)
+ self.remove(life)
+ self.remove(drop)
+
+ self.multiworld.state.collect(boots, event=True)
+ self.multiworld.state.collect(boots, event=True)
+ self.multiworld.state.collect(gun, event=True)
+ self.multiworld.state.collect(gun, event=True)
+ self.multiworld.state.collect(gun, event=True)
+ self.multiworld.state.collect(gun, event=True)
+ self.multiworld.state.collect(ammo, event=True)
+ self.multiworld.state.collect(ammo, event=True)
+ self.multiworld.state.collect(ammo, event=True)
+ self.multiworld.state.collect(life, event=True)
+ self.multiworld.state.collect(drop, event=True)
+ self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state))
+ self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state))
+ self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state))
+ self.assertTrue(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state))
+ self.remove(boots)
+ self.remove(boots)
+ self.remove(gun)
+ self.remove(gun)
+ self.remove(gun)
+ self.remove(gun)
+ self.remove(ammo)
+ self.remove(ammo)
+ self.remove(ammo)
+ self.remove(life)
+ self.remove(drop)
diff --git a/worlds/stardew_valley/test/rules/TestBuildings.py b/worlds/stardew_valley/test/rules/TestBuildings.py
new file mode 100644
index 000000000000..b00e4138a195
--- /dev/null
+++ b/worlds/stardew_valley/test/rules/TestBuildings.py
@@ -0,0 +1,62 @@
+from ...options import BuildingProgression, FarmType
+from ...test import SVTestBase
+
+
+class TestBuildingLogic(SVTestBase):
+ options = {
+ FarmType.internal_name: FarmType.option_standard,
+ BuildingProgression.internal_name: BuildingProgression.option_progressive,
+ }
+
+ def test_coop_blueprint(self):
+ self.assertFalse(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state))
+
+ self.collect_lots_of_money()
+ self.assertTrue(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state))
+
+ def test_big_coop_blueprint(self):
+ big_coop_blueprint_rule = self.world.logic.region.can_reach_location("Big Coop Blueprint")
+ self.assertFalse(big_coop_blueprint_rule(self.multiworld.state),
+ f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
+
+ self.collect_lots_of_money()
+ self.assertFalse(big_coop_blueprint_rule(self.multiworld.state),
+ f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
+
+ self.multiworld.state.collect(self.create_item("Can Construct Buildings"), event=True)
+ self.assertFalse(big_coop_blueprint_rule(self.multiworld.state),
+ f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
+
+ self.multiworld.state.collect(self.create_item("Progressive Coop"), event=False)
+ self.assertTrue(big_coop_blueprint_rule(self.multiworld.state),
+ f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
+
+ def test_deluxe_coop_blueprint(self):
+ self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state))
+
+ self.collect_lots_of_money()
+ self.multiworld.state.collect(self.create_item("Can Construct Buildings"), event=True)
+ self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state))
+
+ self.multiworld.state.collect(self.create_item("Progressive Coop"), event=True)
+ self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state))
+
+ self.multiworld.state.collect(self.create_item("Progressive Coop"), event=True)
+ self.assertTrue(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state))
+
+ def test_big_shed_blueprint(self):
+ big_shed_rule = self.world.logic.region.can_reach_location("Big Shed Blueprint")
+ self.assertFalse(big_shed_rule(self.multiworld.state),
+ f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")
+
+ self.collect_lots_of_money()
+ self.assertFalse(big_shed_rule(self.multiworld.state),
+ f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")
+
+ self.multiworld.state.collect(self.create_item("Can Construct Buildings"), event=True)
+ self.assertFalse(big_shed_rule(self.multiworld.state),
+ f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")
+
+ self.multiworld.state.collect(self.create_item("Progressive Shed"), event=True)
+ self.assertTrue(big_shed_rule(self.multiworld.state),
+ f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")
diff --git a/worlds/stardew_valley/test/rules/TestBundles.py b/worlds/stardew_valley/test/rules/TestBundles.py
new file mode 100644
index 000000000000..ab376c90d4ea
--- /dev/null
+++ b/worlds/stardew_valley/test/rules/TestBundles.py
@@ -0,0 +1,66 @@
+from ... import options
+from ...options import BundleRandomization
+from ...strings.bundle_names import BundleName
+from ...test import SVTestBase
+
+
+class TestBundlesLogic(SVTestBase):
+ options = {
+ options.BundleRandomization: BundleRandomization.option_vanilla,
+ options.BundlePrice: options.BundlePrice.default,
+ }
+
+ def test_vault_2500g_bundle(self):
+ self.assertFalse(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state))
+
+ self.collect_lots_of_money()
+ self.assertTrue(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state))
+
+
+class TestRemixedBundlesLogic(SVTestBase):
+ options = {
+ options.BundleRandomization: BundleRandomization.option_remixed,
+ options.BundlePrice: options.BundlePrice.default,
+ options.BundlePlando: frozenset({BundleName.sticky})
+ }
+
+ def test_sticky_bundle_has_grind_rules(self):
+ self.assertFalse(self.world.logic.region.can_reach_location("Sticky Bundle")(self.multiworld.state))
+
+ self.collect_all_the_money()
+ self.assertTrue(self.world.logic.region.can_reach_location("Sticky Bundle")(self.multiworld.state))
+
+
+class TestRaccoonBundlesLogic(SVTestBase):
+ options = {
+ options.BundleRandomization: BundleRandomization.option_vanilla,
+ options.BundlePrice: options.BundlePrice.option_normal,
+ options.Craftsanity: options.Craftsanity.option_all,
+ }
+ seed = 2 # Magic seed that does what I want. Might need to get changed if we change the randomness behavior of raccoon bundles
+
+ def test_raccoon_bundles_rely_on_previous_ones(self):
+ # The first raccoon bundle is a fishing one
+ raccoon_rule_1 = self.world.logic.region.can_reach_location("Raccoon Request 1")
+
+ # The 3th raccoon bundle is a foraging one
+ raccoon_rule_3 = self.world.logic.region.can_reach_location("Raccoon Request 3")
+ self.collect("Progressive Raccoon", 6)
+ self.collect("Progressive Mine Elevator", 24)
+ self.collect("Mining Level", 12)
+ self.collect("Combat Level", 12)
+ self.collect("Progressive Axe", 4)
+ self.collect("Progressive Pickaxe", 4)
+ self.collect("Progressive Weapon", 4)
+ self.collect("Dehydrator Recipe")
+ self.collect("Mushroom Boxes")
+ self.collect("Progressive Fishing Rod", 4)
+ self.collect("Fishing Level", 10)
+
+ self.assertFalse(raccoon_rule_1(self.multiworld.state))
+ self.assertFalse(raccoon_rule_3(self.multiworld.state))
+
+ self.collect("Fish Smoker Recipe")
+
+ self.assertTrue(raccoon_rule_1(self.multiworld.state))
+ self.assertTrue(raccoon_rule_3(self.multiworld.state))
diff --git a/worlds/stardew_valley/test/rules/TestCookingRecipes.py b/worlds/stardew_valley/test/rules/TestCookingRecipes.py
new file mode 100644
index 000000000000..81a91d1e7482
--- /dev/null
+++ b/worlds/stardew_valley/test/rules/TestCookingRecipes.py
@@ -0,0 +1,83 @@
+from ... import options
+from ...options import BuildingProgression, ExcludeGingerIsland, Chefsanity
+from ...test import SVTestBase
+
+
+class TestRecipeLearnLogic(SVTestBase):
+ options = {
+ BuildingProgression.internal_name: BuildingProgression.option_progressive,
+ options.Cropsanity.internal_name: options.Cropsanity.option_enabled,
+ options.Cooksanity.internal_name: options.Cooksanity.option_all,
+ Chefsanity.internal_name: Chefsanity.option_none,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
+ }
+
+ def test_can_learn_qos_recipe(self):
+ location = "Cook Radish Salad"
+ rule = self.world.logic.region.can_reach_location(location)
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ self.multiworld.state.collect(self.create_item("Progressive House"), event=False)
+ self.multiworld.state.collect(self.create_item("Radish Seeds"), event=False)
+ self.multiworld.state.collect(self.create_item("Spring"), event=False)
+ self.multiworld.state.collect(self.create_item("Summer"), event=False)
+ self.collect_lots_of_money()
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ self.multiworld.state.collect(self.create_item("The Queen of Sauce"), event=False)
+ self.assert_rule_true(rule, self.multiworld.state)
+
+
+class TestRecipeReceiveLogic(SVTestBase):
+ options = {
+ BuildingProgression.internal_name: BuildingProgression.option_progressive,
+ options.Cropsanity.internal_name: options.Cropsanity.option_enabled,
+ options.Cooksanity.internal_name: options.Cooksanity.option_all,
+ Chefsanity.internal_name: Chefsanity.option_all,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
+ }
+
+ def test_can_learn_qos_recipe(self):
+ location = "Cook Radish Salad"
+ rule = self.world.logic.region.can_reach_location(location)
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ self.multiworld.state.collect(self.create_item("Progressive House"), event=False)
+ self.multiworld.state.collect(self.create_item("Radish Seeds"), event=False)
+ self.multiworld.state.collect(self.create_item("Summer"), event=False)
+ self.collect_lots_of_money()
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ spring = self.create_item("Spring")
+ qos = self.create_item("The Queen of Sauce")
+ self.multiworld.state.collect(spring, event=False)
+ self.multiworld.state.collect(qos, event=False)
+ self.assert_rule_false(rule, self.multiworld.state)
+ self.multiworld.state.remove(spring)
+ self.multiworld.state.remove(qos)
+
+ self.multiworld.state.collect(self.create_item("Radish Salad Recipe"), event=False)
+ self.assert_rule_true(rule, self.multiworld.state)
+
+ def test_get_chefsanity_check_recipe(self):
+ location = "Radish Salad Recipe"
+ rule = self.world.logic.region.can_reach_location(location)
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ self.multiworld.state.collect(self.create_item("Spring"), event=False)
+ self.collect_lots_of_money()
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ seeds = self.create_item("Radish Seeds")
+ summer = self.create_item("Summer")
+ house = self.create_item("Progressive House")
+ self.multiworld.state.collect(seeds, event=False)
+ self.multiworld.state.collect(summer, event=False)
+ self.multiworld.state.collect(house, event=False)
+ self.assert_rule_false(rule, self.multiworld.state)
+ self.multiworld.state.remove(seeds)
+ self.multiworld.state.remove(summer)
+ self.multiworld.state.remove(house)
+
+ self.multiworld.state.collect(self.create_item("The Queen of Sauce"), event=False)
+ self.assert_rule_true(rule, self.multiworld.state)
diff --git a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py
new file mode 100644
index 000000000000..59d41f6a63d6
--- /dev/null
+++ b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py
@@ -0,0 +1,123 @@
+from ... import options
+from ...data.craftable_data import all_crafting_recipes_by_name
+from ...options import BuildingProgression, ExcludeGingerIsland, Craftsanity, SeasonRandomization
+from ...test import SVTestBase
+
+
+class TestCraftsanityLogic(SVTestBase):
+ options = {
+ BuildingProgression.internal_name: BuildingProgression.option_progressive,
+ options.Cropsanity.internal_name: options.Cropsanity.option_enabled,
+ Craftsanity.internal_name: Craftsanity.option_all,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
+ }
+
+ def test_can_craft_recipe(self):
+ location = "Craft Marble Brazier"
+ rule = self.world.logic.region.can_reach_location(location)
+ self.collect([self.create_item("Progressive Pickaxe")] * 4)
+ self.collect([self.create_item("Progressive Fishing Rod")] * 4)
+ self.collect([self.create_item("Progressive Sword")] * 4)
+ self.collect([self.create_item("Progressive Mine Elevator")] * 24)
+ self.collect([self.create_item("Mining Level")] * 10)
+ self.collect([self.create_item("Combat Level")] * 10)
+ self.collect([self.create_item("Fishing Level")] * 10)
+ self.collect_all_the_money()
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ self.multiworld.state.collect(self.create_item("Marble Brazier Recipe"), event=False)
+ self.assert_rule_true(rule, self.multiworld.state)
+
+ def test_can_learn_crafting_recipe(self):
+ location = "Marble Brazier Recipe"
+ rule = self.world.logic.region.can_reach_location(location)
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ self.collect_lots_of_money()
+ self.assert_rule_true(rule, self.multiworld.state)
+
+ def test_can_craft_festival_recipe(self):
+ recipe = all_crafting_recipes_by_name["Jack-O-Lantern"]
+ self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), event=False)
+ self.multiworld.state.collect(self.create_item("Torch Recipe"), event=False)
+ self.collect_lots_of_money()
+ rule = self.world.logic.crafting.can_craft(recipe)
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ self.multiworld.state.collect(self.create_item("Fall"), event=False)
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), event=False)
+ self.assert_rule_true(rule, self.multiworld.state)
+
+
+class TestCraftsanityWithFestivalsLogic(SVTestBase):
+ options = {
+ BuildingProgression.internal_name: BuildingProgression.option_progressive,
+ options.Cropsanity.internal_name: options.Cropsanity.option_enabled,
+ options.FestivalLocations.internal_name: options.FestivalLocations.option_easy,
+ Craftsanity.internal_name: Craftsanity.option_all,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
+ }
+
+ def test_can_craft_festival_recipe(self):
+ recipe = all_crafting_recipes_by_name["Jack-O-Lantern"]
+ self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), event=False)
+ self.multiworld.state.collect(self.create_item("Fall"), event=False)
+ self.collect_lots_of_money()
+ rule = self.world.logic.crafting.can_craft(recipe)
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), event=False)
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ self.multiworld.state.collect(self.create_item("Torch Recipe"), event=False)
+ self.assert_rule_true(rule, self.multiworld.state)
+
+
+class TestNoCraftsanityLogic(SVTestBase):
+ options = {
+ BuildingProgression.internal_name: BuildingProgression.option_progressive,
+ SeasonRandomization.internal_name: SeasonRandomization.option_progressive,
+ options.Cropsanity.internal_name: options.Cropsanity.option_enabled,
+ options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled,
+ Craftsanity.internal_name: Craftsanity.option_none,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
+ }
+
+ def test_can_craft_recipe(self):
+ recipe = all_crafting_recipes_by_name["Wood Floor"]
+ rule = self.world.logic.crafting.can_craft(recipe)
+ self.assert_rule_true(rule, self.multiworld.state)
+
+ def test_can_craft_festival_recipe(self):
+ recipe = all_crafting_recipes_by_name["Jack-O-Lantern"]
+ self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), event=False)
+ self.collect_lots_of_money()
+ rule = self.world.logic.crafting.can_craft(recipe)
+ result = rule(self.multiworld.state)
+ self.assertFalse(result)
+
+ self.collect([self.create_item("Progressive Season")] * 2)
+ self.assert_rule_true(rule, self.multiworld.state)
+
+
+class TestNoCraftsanityWithFestivalsLogic(SVTestBase):
+ options = {
+ BuildingProgression.internal_name: BuildingProgression.option_progressive,
+ options.Cropsanity.internal_name: options.Cropsanity.option_enabled,
+ options.FestivalLocations.internal_name: options.FestivalLocations.option_easy,
+ Craftsanity.internal_name: Craftsanity.option_none,
+ ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
+ }
+
+ def test_can_craft_festival_recipe(self):
+ recipe = all_crafting_recipes_by_name["Jack-O-Lantern"]
+ self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), event=False)
+ self.multiworld.state.collect(self.create_item("Fall"), event=False)
+ self.collect_lots_of_money()
+ rule = self.world.logic.crafting.can_craft(recipe)
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), event=False)
+ self.assert_rule_true(rule, self.multiworld.state)
diff --git a/worlds/stardew_valley/test/rules/TestDonations.py b/worlds/stardew_valley/test/rules/TestDonations.py
new file mode 100644
index 000000000000..84ceac50ff5a
--- /dev/null
+++ b/worlds/stardew_valley/test/rules/TestDonations.py
@@ -0,0 +1,73 @@
+from ... import options
+from ...locations import locations_by_tag, LocationTags, location_table
+from ...strings.entrance_names import Entrance
+from ...strings.region_names import Region
+from ...test import SVTestBase
+
+
+class TestDonationLogicAll(SVTestBase):
+ options = {
+ options.Museumsanity.internal_name: options.Museumsanity.option_all
+ }
+
+ def test_cannot_make_any_donation_without_museum_access(self):
+ railroad_item = "Railroad Boulder Removed"
+ swap_museum_and_bathhouse(self.multiworld, self.player)
+ self.collect_all_except(railroad_item)
+
+ for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]:
+ self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state))
+
+ self.multiworld.state.collect(self.create_item(railroad_item), event=False)
+
+ for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]:
+ self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state))
+
+
+class TestDonationLogicRandomized(SVTestBase):
+ options = {
+ options.Museumsanity.internal_name: options.Museumsanity.option_randomized
+ }
+
+ def test_cannot_make_any_donation_without_museum_access(self):
+ railroad_item = "Railroad Boulder Removed"
+ swap_museum_and_bathhouse(self.multiworld, self.player)
+ self.collect_all_except(railroad_item)
+ donation_locations = [location for location in self.get_real_locations() if
+ LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags]
+
+ for donation in donation_locations:
+ self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state))
+
+ self.multiworld.state.collect(self.create_item(railroad_item), event=False)
+
+ for donation in donation_locations:
+ self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state))
+
+
+class TestDonationLogicMilestones(SVTestBase):
+ options = {
+ options.Museumsanity.internal_name: options.Museumsanity.option_milestones
+ }
+
+ def test_cannot_make_any_donation_without_museum_access(self):
+ railroad_item = "Railroad Boulder Removed"
+ swap_museum_and_bathhouse(self.multiworld, self.player)
+ self.collect_all_except(railroad_item)
+
+ for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]:
+ self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state))
+
+ self.multiworld.state.collect(self.create_item(railroad_item), event=False)
+
+ for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]:
+ self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state))
+
+
+def swap_museum_and_bathhouse(multiworld, player):
+ museum_region = multiworld.get_region(Region.museum, player)
+ bathhouse_region = multiworld.get_region(Region.bathhouse_entrance, player)
+ museum_entrance = multiworld.get_entrance(Entrance.town_to_museum, player)
+ bathhouse_entrance = multiworld.get_entrance(Entrance.enter_bathhouse_entrance, player)
+ museum_entrance.connect(bathhouse_region)
+ bathhouse_entrance.connect(museum_region)
diff --git a/worlds/stardew_valley/test/rules/TestFriendship.py b/worlds/stardew_valley/test/rules/TestFriendship.py
new file mode 100644
index 000000000000..43c5e55c7fca
--- /dev/null
+++ b/worlds/stardew_valley/test/rules/TestFriendship.py
@@ -0,0 +1,58 @@
+from ...options import SeasonRandomization, Friendsanity, FriendsanityHeartSize
+from ...test import SVTestBase
+
+
+class TestFriendsanityDatingRules(SVTestBase):
+ options = {
+ SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter,
+ Friendsanity.internal_name: Friendsanity.option_all_with_marriage,
+ FriendsanityHeartSize.internal_name: 3
+ }
+
+ def test_earning_dating_heart_requires_dating(self):
+ self.collect_all_the_money()
+ self.multiworld.state.collect(self.create_item("Fall"), event=False)
+ self.multiworld.state.collect(self.create_item("Beach Bridge"), event=False)
+ self.multiworld.state.collect(self.create_item("Progressive House"), event=False)
+ for i in range(3):
+ self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=False)
+ self.multiworld.state.collect(self.create_item("Progressive Weapon"), event=False)
+ self.multiworld.state.collect(self.create_item("Progressive Axe"), event=False)
+ self.multiworld.state.collect(self.create_item("Progressive Barn"), event=False)
+ for i in range(10):
+ self.multiworld.state.collect(self.create_item("Foraging Level"), event=False)
+ self.multiworld.state.collect(self.create_item("Farming Level"), event=False)
+ self.multiworld.state.collect(self.create_item("Mining Level"), event=False)
+ self.multiworld.state.collect(self.create_item("Combat Level"), event=False)
+ self.multiworld.state.collect(self.create_item("Progressive Mine Elevator"), event=False)
+ self.multiworld.state.collect(self.create_item("Progressive Mine Elevator"), event=False)
+
+ npc = "Abigail"
+ heart_name = f"{npc} <3"
+ step = 3
+
+ self.assert_can_reach_heart_up_to(npc, 3, step)
+ self.multiworld.state.collect(self.create_item(heart_name), event=False)
+ self.assert_can_reach_heart_up_to(npc, 6, step)
+ self.multiworld.state.collect(self.create_item(heart_name), event=False)
+ self.assert_can_reach_heart_up_to(npc, 8, step)
+ self.multiworld.state.collect(self.create_item(heart_name), event=False)
+ self.assert_can_reach_heart_up_to(npc, 10, step)
+ self.multiworld.state.collect(self.create_item(heart_name), event=False)
+ self.assert_can_reach_heart_up_to(npc, 14, step)
+
+ def assert_can_reach_heart_up_to(self, npc: str, max_reachable: int, step: int):
+ prefix = "Friendsanity: "
+ suffix = " <3"
+ for i in range(1, max_reachable + 1):
+ if i % step != 0 and i != 14:
+ continue
+ location = f"{prefix}{npc} {i}{suffix}"
+ can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state)
+ self.assertTrue(can_reach, f"Should be able to earn relationship up to {i} hearts")
+ for i in range(max_reachable + 1, 14 + 1):
+ if i % step != 0 and i != 14:
+ continue
+ location = f"{prefix}{npc} {i}{suffix}"
+ can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state)
+ self.assertFalse(can_reach, f"Should not be able to earn relationship up to {i} hearts")
diff --git a/worlds/stardew_valley/test/rules/TestMuseum.py b/worlds/stardew_valley/test/rules/TestMuseum.py
new file mode 100644
index 000000000000..35dad8f43ebc
--- /dev/null
+++ b/worlds/stardew_valley/test/rules/TestMuseum.py
@@ -0,0 +1,16 @@
+from collections import Counter
+
+from ...options import Museumsanity
+from .. import SVTestBase
+
+
+class TestMuseumMilestones(SVTestBase):
+ options = {
+ Museumsanity.internal_name: Museumsanity.option_milestones
+ }
+
+ def test_50_milestone(self):
+ self.multiworld.state.prog_items = {1: Counter()}
+
+ milestone_rule = self.world.logic.museum.can_find_museum_items(50)
+ self.assert_rule_false(milestone_rule, self.multiworld.state)
diff --git a/worlds/stardew_valley/test/rules/TestShipping.py b/worlds/stardew_valley/test/rules/TestShipping.py
new file mode 100644
index 000000000000..378933b7d75d
--- /dev/null
+++ b/worlds/stardew_valley/test/rules/TestShipping.py
@@ -0,0 +1,82 @@
+from ...locations import LocationTags, location_table
+from ...options import BuildingProgression, Shipsanity
+from ...test import SVTestBase
+
+
+class TestShipsanityNone(SVTestBase):
+ options = {
+ Shipsanity.internal_name: Shipsanity.option_none
+ }
+
+ def test_no_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ self.assertFalse("Shipsanity" in location.name)
+ self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags)
+
+
+class TestShipsanityCrops(SVTestBase):
+ options = {
+ Shipsanity.internal_name: Shipsanity.option_crops
+ }
+
+ def test_only_crop_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags)
+
+
+class TestShipsanityFish(SVTestBase):
+ options = {
+ Shipsanity.internal_name: Shipsanity.option_fish
+ }
+
+ def test_only_fish_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
+
+
+class TestShipsanityFullShipment(SVTestBase):
+ options = {
+ Shipsanity.internal_name: Shipsanity.option_full_shipment
+ }
+
+ def test_only_full_shipment_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags)
+ self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
+
+
+class TestShipsanityFullShipmentWithFish(SVTestBase):
+ options = {
+ Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish
+ }
+
+ def test_only_full_shipment_and_fish_shipsanity_locations(self):
+ for location in self.get_real_locations():
+ if LocationTags.SHIPSANITY in location_table[location.name].tags:
+ self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or
+ LocationTags.SHIPSANITY_FISH in location_table[location.name].tags)
+
+
+class TestShipsanityEverything(SVTestBase):
+ options = {
+ Shipsanity.internal_name: Shipsanity.option_everything,
+ BuildingProgression.internal_name: BuildingProgression.option_progressive
+ }
+
+ def test_all_shipsanity_locations_require_shipping_bin(self):
+ bin_name = "Shipping Bin"
+ self.collect_all_except(bin_name)
+ shipsanity_locations = [location for location in self.get_real_locations() if
+ LocationTags.SHIPSANITY in location_table[location.name].tags]
+ bin_item = self.create_item(bin_name)
+ for location in shipsanity_locations:
+ with self.subTest(location.name):
+ self.remove(bin_item)
+ self.assertFalse(self.world.logic.region.can_reach_location(location.name)(self.multiworld.state))
+ self.multiworld.state.collect(bin_item, event=False)
+ shipsanity_rule = self.world.logic.region.can_reach_location(location.name)
+ self.assert_rule_true(shipsanity_rule, self.multiworld.state)
+ self.remove(bin_item)
diff --git a/worlds/stardew_valley/test/rules/TestSkills.py b/worlds/stardew_valley/test/rules/TestSkills.py
new file mode 100644
index 000000000000..1c6874f31529
--- /dev/null
+++ b/worlds/stardew_valley/test/rules/TestSkills.py
@@ -0,0 +1,40 @@
+from ... import HasProgressionPercent
+from ...options import ToolProgression, SkillProgression, Mods
+from ...strings.skill_names import all_skills
+from ...test import SVTestBase
+
+
+class TestVanillaSkillLogicSimplification(SVTestBase):
+ options = {
+ SkillProgression.internal_name: SkillProgression.option_vanilla,
+ ToolProgression.internal_name: ToolProgression.option_progressive,
+ }
+
+ def test_skill_logic_has_level_only_uses_one_has_progression_percent(self):
+ rule = self.multiworld.worlds[1].logic.skill.has_level("Farming", 8)
+ self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) == HasProgressionPercent))
+
+
+class TestAllSkillsRequirePrevious(SVTestBase):
+ options = {
+ SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
+ Mods.internal_name: frozenset(Mods.valid_keys),
+ }
+
+ def test_all_skill_levels_require_previous_level(self):
+ for skill in all_skills:
+ self.collect_everything()
+ self.remove_by_name(f"{skill} Level")
+ for level in range(1, 11):
+ location_name = f"Level {level} {skill}"
+ with self.subTest(location_name):
+ can_reach = self.can_reach_location(location_name)
+ if level > 1:
+ self.assertFalse(can_reach)
+ self.collect(f"{skill} Level")
+ can_reach = self.can_reach_location(location_name)
+ self.assertTrue(can_reach)
+ self.multiworld.state = self.original_state.copy()
+
+
+
diff --git a/worlds/stardew_valley/test/rules/TestStateRules.py b/worlds/stardew_valley/test/rules/TestStateRules.py
new file mode 100644
index 000000000000..4f53b9a7f536
--- /dev/null
+++ b/worlds/stardew_valley/test/rules/TestStateRules.py
@@ -0,0 +1,12 @@
+import unittest
+
+from BaseClasses import ItemClassification
+from ...test import solo_multiworld
+
+
+class TestHasProgressionPercent(unittest.TestCase):
+ def test_max_item_amount_is_full_collection(self):
+ # Not caching because it fails too often for some reason
+ with solo_multiworld(world_caching=False) as (multiworld, world):
+ progression_item_count = sum(1 for i in multiworld.get_items() if ItemClassification.progression in i.classification)
+ self.assertEqual(world.total_progression_items, progression_item_count - 1) # -1 to skip Victory
diff --git a/worlds/stardew_valley/test/rules/TestTools.py b/worlds/stardew_valley/test/rules/TestTools.py
new file mode 100644
index 000000000000..a1fb152812c8
--- /dev/null
+++ b/worlds/stardew_valley/test/rules/TestTools.py
@@ -0,0 +1,141 @@
+from collections import Counter
+
+from .. import SVTestBase
+from ... import Event, options
+from ...options import ToolProgression, SeasonRandomization
+from ...strings.entrance_names import Entrance
+from ...strings.region_names import Region
+from ...strings.tool_names import Tool, ToolMaterial
+
+
+class TestProgressiveToolsLogic(SVTestBase):
+ options = {
+ ToolProgression.internal_name: ToolProgression.option_progressive,
+ SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
+ }
+
+ def test_sturgeon(self):
+ self.multiworld.state.prog_items = {1: Counter()}
+
+ sturgeon_rule = self.world.logic.has("Sturgeon")
+ self.assert_rule_false(sturgeon_rule, self.multiworld.state)
+
+ summer = self.create_item("Summer")
+ self.multiworld.state.collect(summer, event=False)
+ self.assert_rule_false(sturgeon_rule, self.multiworld.state)
+
+ fishing_rod = self.create_item("Progressive Fishing Rod")
+ self.multiworld.state.collect(fishing_rod, event=False)
+ self.multiworld.state.collect(fishing_rod, event=False)
+ self.assert_rule_false(sturgeon_rule, self.multiworld.state)
+
+ fishing_level = self.create_item("Fishing Level")
+ self.multiworld.state.collect(fishing_level, event=False)
+ self.assert_rule_false(sturgeon_rule, self.multiworld.state)
+
+ self.multiworld.state.collect(fishing_level, event=False)
+ self.multiworld.state.collect(fishing_level, event=False)
+ self.multiworld.state.collect(fishing_level, event=False)
+ self.multiworld.state.collect(fishing_level, event=False)
+ self.multiworld.state.collect(fishing_level, event=False)
+ self.assert_rule_true(sturgeon_rule, self.multiworld.state)
+
+ self.remove(summer)
+ self.assert_rule_false(sturgeon_rule, self.multiworld.state)
+
+ winter = self.create_item("Winter")
+ self.multiworld.state.collect(winter, event=False)
+ self.assert_rule_true(sturgeon_rule, self.multiworld.state)
+
+ self.remove(fishing_rod)
+ self.assert_rule_false(sturgeon_rule, self.multiworld.state)
+
+ def test_old_master_cannoli(self):
+ self.multiworld.state.prog_items = {1: Counter()}
+
+ self.multiworld.state.collect(self.create_item("Progressive Axe"), event=False)
+ self.multiworld.state.collect(self.create_item("Progressive Axe"), event=False)
+ self.multiworld.state.collect(self.create_item("Summer"), event=False)
+ self.collect_lots_of_money()
+
+ rule = self.world.logic.region.can_reach_location("Old Master Cannoli")
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ fall = self.create_item("Fall")
+ self.multiworld.state.collect(fall, event=False)
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ tuesday = self.create_item("Traveling Merchant: Tuesday")
+ self.multiworld.state.collect(tuesday, event=False)
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ rare_seed = self.create_item("Rare Seed")
+ self.multiworld.state.collect(rare_seed, event=False)
+ self.assert_rule_true(rule, self.multiworld.state)
+
+ self.remove(fall)
+ self.remove(self.create_item(Event.fall_farming))
+ self.assert_rule_false(rule, self.multiworld.state)
+ self.remove(tuesday)
+
+ green_house = self.create_item("Greenhouse")
+ self.collect(self.create_item(Event.fall_farming))
+ self.multiworld.state.collect(green_house, event=False)
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ friday = self.create_item("Traveling Merchant: Friday")
+ self.multiworld.state.collect(friday, event=False)
+ self.assertTrue(self.multiworld.get_location("Old Master Cannoli", 1).access_rule(self.multiworld.state))
+
+ self.remove(green_house)
+ self.remove(self.create_item(Event.fall_farming))
+ self.assert_rule_false(rule, self.multiworld.state)
+ self.remove(friday)
+
+
+class TestToolVanillaRequiresBlacksmith(SVTestBase):
+ options = {
+ options.EntranceRandomization: options.EntranceRandomization.option_buildings,
+ options.ToolProgression: options.ToolProgression.option_vanilla,
+ }
+ seed = 4111845104987680262
+
+ # Seed is hardcoded to make sure the ER is a valid roll that actually lock the blacksmith behind the Railroad Boulder Removed.
+
+ def test_cannot_get_any_tool_without_blacksmith_access(self):
+ railroad_item = "Railroad Boulder Removed"
+ place_region_at_entrance(self.multiworld, self.player, Region.blacksmith, Entrance.enter_bathhouse_entrance)
+ self.collect_all_except(railroad_item)
+
+ for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]:
+ for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]:
+ self.assert_rule_false(self.world.logic.tool.has_tool(tool, material), self.multiworld.state)
+
+ self.multiworld.state.collect(self.create_item(railroad_item), event=False)
+
+ for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]:
+ for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]:
+ self.assert_rule_true(self.world.logic.tool.has_tool(tool, material), self.multiworld.state)
+
+ def test_cannot_get_fishing_rod_without_willy_access(self):
+ railroad_item = "Railroad Boulder Removed"
+ place_region_at_entrance(self.multiworld, self.player, Region.fish_shop, Entrance.enter_bathhouse_entrance)
+ self.collect_all_except(railroad_item)
+
+ for fishing_rod_level in [3, 4]:
+ self.assert_rule_false(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state)
+
+ self.multiworld.state.collect(self.create_item(railroad_item), event=False)
+
+ for fishing_rod_level in [3, 4]:
+ self.assert_rule_true(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state)
+
+
+def place_region_at_entrance(multiworld, player, region, entrance):
+ region_to_place = multiworld.get_region(region, player)
+ entrance_to_place_region = multiworld.get_entrance(entrance, player)
+
+ entrance_to_switch = region_to_place.entrances[0]
+ region_to_switch = entrance_to_place_region.connected_region
+ entrance_to_switch.connect(region_to_switch)
+ entrance_to_place_region.connect(region_to_place)
diff --git a/worlds/stardew_valley/test/rules/TestWeapons.py b/worlds/stardew_valley/test/rules/TestWeapons.py
new file mode 100644
index 000000000000..77887f8eca0c
--- /dev/null
+++ b/worlds/stardew_valley/test/rules/TestWeapons.py
@@ -0,0 +1,75 @@
+from ... import options
+from ...options import ToolProgression
+from ...test import SVTestBase
+
+
+class TestWeaponsLogic(SVTestBase):
+ options = {
+ ToolProgression.internal_name: ToolProgression.option_progressive,
+ options.SkillProgression.internal_name: options.SkillProgression.option_progressive,
+ }
+
+ def test_mine(self):
+ self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=True)
+ self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=True)
+ self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=True)
+ self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=True)
+ self.multiworld.state.collect(self.create_item("Progressive House"), event=True)
+ self.collect([self.create_item("Combat Level")] * 10)
+ self.collect([self.create_item("Mining Level")] * 10)
+ self.collect([self.create_item("Progressive Mine Elevator")] * 24)
+ self.multiworld.state.collect(self.create_item("Bus Repair"), event=True)
+ self.multiworld.state.collect(self.create_item("Skull Key"), event=True)
+
+ self.GiveItemAndCheckReachableMine("Progressive Sword", 1)
+ self.GiveItemAndCheckReachableMine("Progressive Dagger", 1)
+ self.GiveItemAndCheckReachableMine("Progressive Club", 1)
+
+ self.GiveItemAndCheckReachableMine("Progressive Sword", 2)
+ self.GiveItemAndCheckReachableMine("Progressive Dagger", 2)
+ self.GiveItemAndCheckReachableMine("Progressive Club", 2)
+
+ self.GiveItemAndCheckReachableMine("Progressive Sword", 3)
+ self.GiveItemAndCheckReachableMine("Progressive Dagger", 3)
+ self.GiveItemAndCheckReachableMine("Progressive Club", 3)
+
+ self.GiveItemAndCheckReachableMine("Progressive Sword", 4)
+ self.GiveItemAndCheckReachableMine("Progressive Dagger", 4)
+ self.GiveItemAndCheckReachableMine("Progressive Club", 4)
+
+ self.GiveItemAndCheckReachableMine("Progressive Sword", 5)
+ self.GiveItemAndCheckReachableMine("Progressive Dagger", 5)
+ self.GiveItemAndCheckReachableMine("Progressive Club", 5)
+
+ def GiveItemAndCheckReachableMine(self, item_name: str, reachable_level: int):
+ item = self.multiworld.create_item(item_name, self.player)
+ self.multiworld.state.collect(item, event=True)
+ rule = self.world.logic.mine.can_mine_in_the_mines_floor_1_40()
+ if reachable_level > 0:
+ self.assert_rule_true(rule, self.multiworld.state)
+ else:
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ rule = self.world.logic.mine.can_mine_in_the_mines_floor_41_80()
+ if reachable_level > 1:
+ self.assert_rule_true(rule, self.multiworld.state)
+ else:
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ rule = self.world.logic.mine.can_mine_in_the_mines_floor_81_120()
+ if reachable_level > 2:
+ self.assert_rule_true(rule, self.multiworld.state)
+ else:
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ rule = self.world.logic.mine.can_mine_in_the_skull_cavern()
+ if reachable_level > 3:
+ self.assert_rule_true(rule, self.multiworld.state)
+ else:
+ self.assert_rule_false(rule, self.multiworld.state)
+
+ rule = self.world.logic.ability.can_mine_perfectly_in_the_skull_cavern()
+ if reachable_level > 4:
+ self.assert_rule_true(rule, self.multiworld.state)
+ else:
+ self.assert_rule_false(rule, self.multiworld.state)
diff --git a/worlds/stardew_valley/test/rules/__init__.py b/worlds/stardew_valley/test/rules/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/stardew_valley/test/script/__init__.py b/worlds/stardew_valley/test/script/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/stardew_valley/test/script/benchmark_locations.py b/worlds/stardew_valley/test/script/benchmark_locations.py
new file mode 100644
index 000000000000..04553e39968e
--- /dev/null
+++ b/worlds/stardew_valley/test/script/benchmark_locations.py
@@ -0,0 +1,140 @@
+"""
+Copy of the script in test/benchmark, adapted to Stardew Valley.
+
+Run with `python -m worlds.stardew_valley.test.script.benchmark_locations --options minimal_locations_maximal_items`
+"""
+
+import argparse
+import collections
+import gc
+import logging
+import os
+import sys
+import time
+import typing
+
+from BaseClasses import CollectionState, Location
+from Utils import init_logging
+from worlds.stardew_valley.stardew_rule.rule_explain import explain
+from ... import test
+
+
+def run_locations_benchmark():
+ init_logging("Benchmark Runner")
+ logger = logging.getLogger("Benchmark")
+
+ class BenchmarkRunner:
+ gen_steps: typing.Tuple[str, ...] = (
+ "generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
+ rule_iterations: int = 100_000
+
+ @staticmethod
+ def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str:
+ return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
+
+ def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float:
+ with TimeIt(f"{test_location.game} {self.rule_iterations} "
+ f"runs of {test_location}.access_rule({state_name})", logger) as t:
+ for _ in range(self.rule_iterations):
+ test_location.access_rule(state)
+ # if time is taken to disentangle complex ref chains,
+ # this time should be attributed to the rule.
+ gc.collect()
+ return t.dif
+
+ def main(self):
+ game = "Stardew Valley"
+ summary_data: typing.Dict[str, collections.Counter[str]] = {
+ "empty_state": collections.Counter(),
+ "all_state": collections.Counter(),
+ }
+ try:
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--options', help="Define the option set to use, from the preset in test/__init__.py .", type=str, required=True)
+ parser.add_argument('--seed', help="Define the seed to use.", type=int, required=True)
+ parser.add_argument('--location', help="Define the specific location to benchmark.", type=str, default=None)
+ parser.add_argument('--state', help="Define the state in which the location will be benchmarked.", type=str, default=None)
+ args = parser.parse_args()
+ options_set = args.options
+ options = getattr(test, options_set)()
+ seed = args.seed
+ location = args.location
+ state = args.state
+
+ multiworld = test.setup_solo_multiworld(options, seed)
+ gc.collect()
+
+ if location:
+ locations = [multiworld.get_location(location, 1)]
+ else:
+ locations = sorted(multiworld.get_unfilled_locations())
+
+ all_state = multiworld.get_all_state(False)
+ for location in locations:
+ if state != "all_state":
+ time_taken = self.location_test(location, multiworld.state, "empty_state")
+ summary_data["empty_state"][location.name] = time_taken
+
+ if state != "empty_state":
+ time_taken = self.location_test(location, all_state, "all_state")
+ summary_data["all_state"][location.name] = time_taken
+
+ total_empty_state = sum(summary_data["empty_state"].values())
+ total_all_state = sum(summary_data["all_state"].values())
+
+ logger.info(f"{game} took {total_empty_state / len(locations):.4f} "
+ f"seconds per location in empty_state and {total_all_state / len(locations):.4f} "
+ f"in all_state. (all times summed for {self.rule_iterations} runs.)")
+ logger.info(f"Top times in empty_state:\n"
+ f"{self.format_times_from_counter(summary_data['empty_state'])}")
+ logger.info(f"Top times in all_state:\n"
+ f"{self.format_times_from_counter(summary_data['all_state'])}")
+
+ if len(locations) == 1:
+ logger.info(str(explain(locations[0].access_rule, all_state, False)))
+
+ except Exception as e:
+ logger.exception(e)
+
+ runner = BenchmarkRunner()
+ runner.main()
+
+
+class TimeIt:
+ def __init__(self, name: str, time_logger=None):
+ self.name = name
+ self.logger = time_logger
+ self.timer = None
+ self.end_timer = None
+
+ def __enter__(self):
+ self.timer = time.perf_counter()
+ return self
+
+ @property
+ def dif(self):
+ return self.end_timer - self.timer
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if not self.end_timer:
+ self.end_timer = time.perf_counter()
+ if self.logger:
+ self.logger.info(f"{self.dif:.4f} seconds in {self.name}.")
+
+
+def change_home():
+ """Allow scripts to run from "this" folder."""
+ old_home = os.path.dirname(__file__)
+ sys.path.remove(old_home)
+ new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
+ os.chdir(new_home)
+ sys.path.append(new_home)
+ # fallback to local import
+ sys.path.append(old_home)
+
+ from Utils import local_path
+ local_path.cached_path = new_home
+
+
+if __name__ == "__main__":
+ run_locations_benchmark()
diff --git a/worlds/stardew_valley/test/stability/StabilityOutputScript.py b/worlds/stardew_valley/test/stability/StabilityOutputScript.py
new file mode 100644
index 000000000000..c8918d6cf2e1
--- /dev/null
+++ b/worlds/stardew_valley/test/stability/StabilityOutputScript.py
@@ -0,0 +1,35 @@
+import argparse
+import json
+
+from ...options import FarmType, EntranceRandomization
+from ...test import setup_solo_multiworld, allsanity_mods_6_x_x
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--seed', help='Define seed number to generate.', type=int, required=True)
+
+ args = parser.parse_args()
+ seed = args.seed
+
+ options = allsanity_mods_6_x_x()
+ options[FarmType.internal_name] = FarmType.option_standard
+ options[EntranceRandomization.internal_name] = EntranceRandomization.option_buildings
+ multi_world = setup_solo_multiworld(options, seed=seed)
+
+ world = multi_world.worlds[1]
+ output = {
+ "bundles": {
+ bundle_room.name: {
+ bundle.name: str(bundle.items)
+ for bundle in bundle_room.bundles
+ }
+ for bundle_room in world.modified_bundles
+ },
+ "items": [item.name for item in multi_world.get_items()],
+ "location_rules": {location.name: repr(location.access_rule) for location in multi_world.get_locations(1)},
+ "slot_data": world.fill_slot_data()
+ }
+
+ print(json.dumps(output))
+else:
+ raise RuntimeError("Do not import this file, execute it in different python session so the PYTHONHASHSEED is different..")
diff --git a/worlds/stardew_valley/test/stability/TestStability.py b/worlds/stardew_valley/test/stability/TestStability.py
new file mode 100644
index 000000000000..8bb904a56ea2
--- /dev/null
+++ b/worlds/stardew_valley/test/stability/TestStability.py
@@ -0,0 +1,58 @@
+import json
+import re
+import subprocess
+import sys
+import unittest
+
+from BaseClasses import get_seed
+from .. import SVTestCase
+
+# There seems to be 4 bytes that appear at random at the end of the output, breaking the json... I don't know where they came from.
+BYTES_TO_REMOVE = 4
+
+# at 0x102ca98a0>
+lambda_regex = re.compile(r"^ at (.*)>$")
+# Python 3.10.2\r\n
+python_version_regex = re.compile(r"^Python (\d+)\.(\d+)\.(\d+)\s*$")
+
+
+class TestGenerationIsStable(SVTestCase):
+ """Let it be known that I hate this tests, and if someone has a better idea than starting subprocesses, please fix this.
+ """
+
+ def test_all_locations_and_items_are_the_same_between_two_generations(self):
+ if self.skip_long_tests:
+ raise unittest.SkipTest("Long tests disabled")
+
+ seed = get_seed()
+
+ output_a = subprocess.check_output([sys.executable, '-m', 'worlds.stardew_valley.test.stability.StabilityOutputScript', '--seed', str(seed)])
+ output_b = subprocess.check_output([sys.executable, '-m', 'worlds.stardew_valley.test.stability.StabilityOutputScript', '--seed', str(seed)])
+
+ result_a = json.loads(output_a[:-BYTES_TO_REMOVE])
+ result_b = json.loads(output_b[:-BYTES_TO_REMOVE])
+
+ for i, ((room_a, bundles_a), (room_b, bundles_b)) in enumerate(zip(result_a["bundles"].items(), result_b["bundles"].items())):
+ self.assertEqual(room_a, room_b, f"Bundle rooms at index {i} is different between both executions. Seed={seed}")
+ for j, ((bundle_a, items_a), (bundle_b, items_b)) in enumerate(zip(bundles_a.items(), bundles_b.items())):
+ self.assertEqual(bundle_a, bundle_b, f"Bundle in room {room_a} at index {j} is different between both executions. Seed={seed}")
+ self.assertEqual(items_a, items_b, f"Items in bundle {bundle_a} are different between both executions. Seed={seed}")
+
+ for i, (item_a, item_b) in enumerate(zip(result_a["items"], result_b["items"])):
+ self.assertEqual(item_a, item_b, f"Item at index {i} is different between both executions. Seed={seed}")
+
+ for i, ((location_a, rule_a), (location_b, rule_b)) in enumerate(zip(result_a["location_rules"].items(), result_b["location_rules"].items())):
+ self.assertEqual(location_a, location_b, f"Location at index {i} is different between both executions. Seed={seed}")
+
+ match = lambda_regex.match(rule_a)
+ if match:
+ self.assertTrue(bool(lambda_regex.match(rule_b)),
+ f"Location rule of {location_a} at index {i} is different between both executions. Seed={seed}")
+ continue
+
+ # We check that the actual rule has the same order to make sure it is evaluated in the same order,
+ # so performance tests are repeatable as much as possible.
+ self.assertEqual(rule_a, rule_b, f"Location rule of {location_a} at index {i} is different between both executions. Seed={seed}")
+
+ for key, value in result_a["slot_data"].items():
+ self.assertEqual(value, result_b["slot_data"][key], f"Slot data {key} is different between both executions. Seed={seed}")
diff --git a/worlds/stardew_valley/test/stability/TestUniversalTracker.py b/worlds/stardew_valley/test/stability/TestUniversalTracker.py
new file mode 100644
index 000000000000..3e334098341d
--- /dev/null
+++ b/worlds/stardew_valley/test/stability/TestUniversalTracker.py
@@ -0,0 +1,52 @@
+import unittest
+from unittest.mock import Mock
+
+from .. import SVTestBase, create_args, allsanity_mods_6_x_x
+from ... import STARDEW_VALLEY, FarmType, BundleRandomization, EntranceRandomization
+
+
+class TestUniversalTrackerGenerationIsStable(SVTestBase):
+ options = allsanity_mods_6_x_x()
+ options.update({
+ EntranceRandomization.internal_name: EntranceRandomization.option_buildings,
+ BundleRandomization.internal_name: BundleRandomization.option_shuffled,
+ FarmType.internal_name: FarmType.option_standard, # Need to choose one otherwise it's random
+ })
+
+ def test_all_locations_and_items_are_the_same_between_two_generations(self):
+ # This might open a kivy window temporarily, but it's the only way to test this...
+ if self.skip_long_tests:
+ raise unittest.SkipTest("Long tests disabled")
+
+ try:
+ # This test only run if UT is present, so no risk of running in the CI.
+ from worlds.tracker.TrackerClient import TrackerGameContext # noqa
+ except ImportError:
+ raise unittest.SkipTest("UT not loaded, skipping test")
+
+ slot_data = self.world.fill_slot_data()
+ ut_data = self.world.interpret_slot_data(slot_data)
+
+ fake_context = Mock()
+ fake_context.re_gen_passthrough = {STARDEW_VALLEY: ut_data}
+ args = create_args({0: self.options})
+ args.outputpath = None
+ args.outputname = None
+ args.multi = 1
+ args.race = None
+ args.plando_options = self.multiworld.plando_options
+ args.plando_items = self.multiworld.plando_items
+ args.plando_texts = self.multiworld.plando_texts
+ args.plando_connections = self.multiworld.plando_connections
+ args.game = self.multiworld.game
+ args.name = self.multiworld.player_name
+ args.sprite = {}
+ args.sprite_pool = {}
+ args.skip_output = True
+
+ generated_multi_world = TrackerGameContext.TMain(fake_context, args, self.multiworld.seed)
+ generated_slot_data = generated_multi_world.worlds[1].fill_slot_data()
+
+ # Just checking slot data should prove that UT generates the same result as AP generation.
+ self.maxDiff = None
+ self.assertEqual(slot_data, generated_slot_data)
diff --git a/worlds/stardew_valley/test/stability/__init__.py b/worlds/stardew_valley/test/stability/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/subnautica/Exports.py b/worlds/subnautica/Exports.py
deleted file mode 100644
index 7a69cffc8d04..000000000000
--- a/worlds/subnautica/Exports.py
+++ /dev/null
@@ -1,76 +0,0 @@
-"""Runnable module that exports data needed by the mod/client."""
-
-if __name__ == "__main__":
- import json
- import math
- import sys
- import os
-
- # makes this module runnable from its world folder.
- sys.path.remove(os.path.dirname(__file__))
- new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
- os.chdir(new_home)
- sys.path.append(new_home)
-
- from worlds.subnautica.Locations import Vector, location_table
- from worlds.subnautica.Items import item_table, group_items, items_by_type
- from NetUtils import encode
-
- export_folder = os.path.join(new_home, "Subnautica Export")
- os.makedirs(export_folder, exist_ok=True)
-
- def in_export_folder(path: str) -> str:
- return os.path.join(export_folder, path)
-
- payload = {location_id: location_data["position"] for location_id, location_data in location_table.items()}
- with open(in_export_folder("locations.json"), "w") as f:
- json.dump(payload, f)
-
- # copy-paste from Rules
- def is_radiated(x: float, y: float, z: float) -> bool:
- aurora_dist = math.sqrt((x - 1038.0) ** 2 + y ** 2 + (z - -163.1) ** 2)
- return aurora_dist < 950
- # end of copy-paste
-
-
- def radiated(pos: Vector):
- return is_radiated(pos["x"], pos["y"], pos["z"])
-
-
- def far_away(pos: Vector):
- return (pos["x"] ** 2 + pos["z"] ** 2) > (800 ** 2)
-
-
- payload = {
- # "LaserCutter" in Subnautica ID
- "761": [location_id for location_id, location_data
- in location_table.items() if location_data["need_laser_cutter"]],
- # PropulsionCannon in Subnautica ID
- "757": [location_id for location_id, location_data
- in location_table.items() if location_data.get("need_propulsion_cannon", False)],
- # Radiation Suit in Subnautica ID
- "519": [location_id for location_id, location_data
- in location_table.items() if radiated(location_data["position"])],
- # SeaGlide in Subnautica ID
- "751": [location_id for location_id, location_data
- in location_table.items() if far_away(location_data["position"])],
- }
- with open(in_export_folder("logic.json"), "w") as f:
- json.dump(payload, f)
-
- itemcount = sum(item_data.count for item_data in item_table.values())
- assert itemcount == len(location_table), f"{itemcount} != {len(location_table)}"
- payload = {item_id: item_data.tech_type for item_id, item_data in item_table.items()}
- import json
-
- with open(in_export_folder("items.json"), "w") as f:
- json.dump(payload, f)
-
- with open(in_export_folder("group_items.json"), "w") as f:
- # encode to convert set to list
- f.write(encode(group_items))
-
- with open(in_export_folder("item_types.json"), "w") as f:
- json.dump(items_by_type, f)
-
- print(f"Subnautica exports dumped to {in_export_folder('')}")
diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py
deleted file mode 100644
index bffc84324147..000000000000
--- a/worlds/subnautica/Items.py
+++ /dev/null
@@ -1,154 +0,0 @@
-from BaseClasses import ItemClassification as IC
-from typing import NamedTuple, Dict, Set, List
-from enum import IntEnum
-
-
-class ItemType(IntEnum):
- technology = 1
- resource = 2
- group = 3
-
-
-class ItemData(NamedTuple):
- classification: IC
- count: int
- name: str
- tech_type: str
- type: ItemType = ItemType.technology
-
-
-def make_resource_bundle_data(display_name: str, internal_name: str = "") -> ItemData:
- if not internal_name:
- internal_name = display_name
- return ItemData(IC.filler, 0, display_name, internal_name, ItemType.resource)
-
-
-item_table: Dict[int, ItemData] = {
- 35000: ItemData(IC.useful, 1, "Compass", "Compass"),
- 35001: ItemData(IC.progression, 1, "Lightweight High Capacity Tank", "PlasteelTank"),
- 35002: ItemData(IC.progression, 1, "Vehicle Upgrade Console", "BaseUpgradeConsole"),
- 35003: ItemData(IC.progression, 1, "Ultra Glide Fins", "UltraGlideFins"),
- 35004: ItemData(IC.useful, 1, "Cyclops Sonar Upgrade", "CyclopsSonarModule"),
- 35005: ItemData(IC.useful, 1, "Reinforced Dive Suit", "ReinforcedDiveSuit"),
- 35006: ItemData(IC.useful, 1, "Cyclops Thermal Reactor Module", "CyclopsThermalReactorModule"),
- 35007: ItemData(IC.filler, 1, "Water Filtration Suit", "WaterFiltrationSuit"),
- 35008: ItemData(IC.progression, 1, "Alien Containment", "BaseWaterPark"),
- 35009: ItemData(IC.useful, 1, "Creature Decoy", "CyclopsDecoy"),
- 35010: ItemData(IC.useful, 1, "Cyclops Fire Suppression System", "CyclopsFireSuppressionModule"),
- 35011: ItemData(IC.useful, 1, "Swim Charge Fins", "SwimChargeFins"),
- 35012: ItemData(IC.useful, 1, "Repulsion Cannon", "RepulsionCannon"),
- 35013: ItemData(IC.useful, 1, "Cyclops Decoy Tube Upgrade", "CyclopsDecoyModule"),
- 35014: ItemData(IC.progression, 1, "Cyclops Shield Generator", "CyclopsShieldModule"),
- 35015: ItemData(IC.progression, 1, "Cyclops Depth Module MK1", "CyclopsHullModule1"),
- 35016: ItemData(IC.useful, 1, "Cyclops Docking Bay Repair Module", "CyclopsSeamothRepairModule"),
- 35017: ItemData(IC.useful, 2, "Battery Charger fragment", "BatteryChargerFragment"),
- 35018: ItemData(IC.filler, 2, "Beacon Fragment", "BeaconFragment"),
- 35019: ItemData(IC.useful, 2, "Bioreactor Fragment", "BaseBioReactorFragment"),
- 35020: ItemData(IC.progression, 4, "Cyclops Bridge Fragment", "CyclopsBridgeFragment"),
- 35021: ItemData(IC.progression, 4, "Cyclops Engine Fragment", "CyclopsEngineFragment"),
- 35022: ItemData(IC.progression, 4, "Cyclops Hull Fragment", "CyclopsHullFragment"),
- 35023: ItemData(IC.filler, 2, "Grav Trap Fragment", "GravSphereFragment"),
- 35024: ItemData(IC.progression, 3, "Laser Cutter Fragment", "LaserCutterFragment"),
- 35025: ItemData(IC.filler, 2, "Light Stick Fragment", "TechlightFragment"),
- 35026: ItemData(IC.progression, 5, "Mobile Vehicle Bay Fragment", "ConstructorFragment"),
- 35027: ItemData(IC.progression, 3, "Modification Station Fragment", "WorkbenchFragment"),
- 35028: ItemData(IC.progression, 2, "Moonpool Fragment", "MoonpoolFragment"),
- 35029: ItemData(IC.useful, 3, "Nuclear Reactor Fragment", "BaseNuclearReactorFragment"),
- 35030: ItemData(IC.useful, 2, "Power Cell Charger Fragment", "PowerCellChargerFragment"),
- 35031: ItemData(IC.filler, 1, "Power Transmitter Fragment", "PowerTransmitterFragment"),
- 35032: ItemData(IC.progression, 6, "Prawn Suit Fragment", "ExosuitFragment"),
- 35033: ItemData(IC.useful, 2, "Prawn Suit Drill Arm Fragment", "ExosuitDrillArmFragment"),
- 35034: ItemData(IC.useful, 2, "Prawn Suit Grappling Arm Fragment", "ExosuitGrapplingArmFragment"),
- 35035: ItemData(IC.useful, 2, "Prawn Suit Propulsion Cannon Fragment", "ExosuitPropulsionArmFragment"),
- 35036: ItemData(IC.useful, 2, "Prawn Suit Torpedo Arm Fragment", "ExosuitTorpedoArmFragment"),
- 35037: ItemData(IC.useful, 3, "Scanner Room Fragment", "BaseMapRoomFragment"),
- 35038: ItemData(IC.progression, 5, "Seamoth Fragment", "SeamothFragment"),
- 35039: ItemData(IC.progression, 2, "Stasis Rifle Fragment", "StasisRifleFragment"),
- 35040: ItemData(IC.useful, 2, "Thermal Plant Fragment", "ThermalPlantFragment"),
- 35041: ItemData(IC.progression, 4, "Seaglide Fragment", "SeaglideFragment"),
- 35042: ItemData(IC.progression, 1, "Radiation Suit", "RadiationSuit"),
- 35043: ItemData(IC.progression, 2, "Propulsion Cannon Fragment", "PropulsionCannonFragment"),
- 35044: ItemData(IC.progression_skip_balancing, 1, "Neptune Launch Platform", "RocketBase"),
- 35045: ItemData(IC.progression, 1, "Ion Power Cell", "PrecursorIonPowerCell"),
- 35046: ItemData(IC.filler, 2, "Exterior Growbed", "FarmingTray"),
- 35047: ItemData(IC.filler, 1, "Picture Frame", "PictureFrameFragment"),
- 35048: ItemData(IC.filler, 1, "Bench", "Bench"),
- 35049: ItemData(IC.filler, 1, "Basic Plant Pot", "PlanterPotFragment"),
- 35050: ItemData(IC.filler, 1, "Interior Growbed", "PlanterBoxFragment"),
- 35051: ItemData(IC.filler, 1, "Plant Shelf", "PlanterShelfFragment"),
- 35052: ItemData(IC.filler, 1, "Observatory", "BaseObservatory"),
- 35053: ItemData(IC.progression, 1, "Multipurpose Room", "BaseRoom"),
- 35054: ItemData(IC.useful, 1, "Bulkhead", "BaseBulkhead"),
- 35055: ItemData(IC.filler, 1, "Spotlight", "Spotlight"),
- 35056: ItemData(IC.filler, 1, "Desk", "StarshipDesk"),
- 35057: ItemData(IC.filler, 1, "Swivel Chair", "StarshipChair"),
- 35058: ItemData(IC.filler, 1, "Office Chair", "StarshipChair2"),
- 35059: ItemData(IC.filler, 1, "Command Chair", "StarshipChair3"),
- 35060: ItemData(IC.filler, 1, "Counter", "LabCounter"),
- 35061: ItemData(IC.filler, 1, "Single Bed", "NarrowBed"),
- 35062: ItemData(IC.filler, 1, "Basic Double Bed", "Bed1"),
- 35063: ItemData(IC.filler, 1, "Quilted Double Bed", "Bed2"),
- 35064: ItemData(IC.filler, 1, "Coffee Vending Machine", "CoffeeVendingMachine"),
- 35065: ItemData(IC.filler, 1, "Trash Can", "Trashcans"),
- 35066: ItemData(IC.filler, 1, "Floodlight", "Techlight"),
- 35067: ItemData(IC.filler, 1, "Bar Table", "BarTable"),
- 35068: ItemData(IC.filler, 1, "Vending Machine", "VendingMachine"),
- 35069: ItemData(IC.filler, 1, "Single Wall Shelf", "SingleWallShelf"),
- 35070: ItemData(IC.filler, 1, "Wall Shelves", "WallShelves"),
- 35071: ItemData(IC.filler, 1, "Round Plant Pot", "PlanterPot2"),
- 35072: ItemData(IC.filler, 1, "Chic Plant Pot", "PlanterPot3"),
- 35073: ItemData(IC.filler, 1, "Nuclear Waste Disposal", "LabTrashcan"),
- 35074: ItemData(IC.filler, 1, "Wall Planter", "BasePlanter"),
- 35075: ItemData(IC.progression, 1, "Ion Battery", "PrecursorIonBattery"),
- 35076: ItemData(IC.progression_skip_balancing, 1, "Neptune Gantry", "RocketBaseLadder"),
- 35077: ItemData(IC.progression_skip_balancing, 1, "Neptune Boosters", "RocketStage1"),
- 35078: ItemData(IC.progression_skip_balancing, 1, "Neptune Fuel Reserve", "RocketStage2"),
- 35079: ItemData(IC.progression_skip_balancing, 1, "Neptune Cockpit", "RocketStage3"),
- 35080: ItemData(IC.filler, 1, "Water Filtration Machine", "BaseFiltrationMachine"),
- 35081: ItemData(IC.progression, 1, "Ultra High Capacity Tank", "HighCapacityTank"),
- 35082: ItemData(IC.progression, 1, "Large Room", "BaseLargeRoom"),
- # awarded with their rooms, keeping that as-is as they"re cosmetic
- 35083: ItemData(IC.filler, 0, "Large Room Glass Dome", "BaseLargeGlassDome"),
- 35084: ItemData(IC.filler, 0, "Multipurpose Room Glass Dome", "BaseGlassDome"),
- 35085: ItemData(IC.filler, 0, "Partition", "BasePartition"),
- 35086: ItemData(IC.filler, 0, "Partition Door", "BasePartitionDoor"),
-
- # Bundles of items
- # Awards all furniture as a bundle
- 35100: ItemData(IC.filler, 0, "Furniture", "AP_Furniture", ItemType.group),
- # Awards all farming blueprints as a bundle
- 35101: ItemData(IC.filler, 0, "Farming", "AP_Farming", ItemType.group),
-
- # Awards multiple resources as a bundle
- 35102: ItemData(IC.filler, 0, "Resources Bundle", "AP_Resources", ItemType.group),
-
- # resource bundles, as convenience/filler
-
- # ores
- 35200: make_resource_bundle_data("Titanium"),
- 35201: make_resource_bundle_data("Copper Ore", "Copper"),
- 35202: make_resource_bundle_data("Silver Ore", "Silver"),
- 35203: make_resource_bundle_data("Gold"),
- 35204: make_resource_bundle_data("Lead"),
- 35205: make_resource_bundle_data("Diamond"),
- 35206: make_resource_bundle_data("Lithium"),
- 35207: make_resource_bundle_data("Ruby", "AluminumOxide"),
- 35208: make_resource_bundle_data("Nickel Ore", "Nickel"),
- 35209: make_resource_bundle_data("Crystalline Sulfur", "Sulphur"),
- 35210: make_resource_bundle_data("Salt Deposit", "Salt"),
- 35211: make_resource_bundle_data("Kyanite"),
- 35212: make_resource_bundle_data("Magnetite"),
- 35213: make_resource_bundle_data("Reactor Rod", "ReactorRod"),
-}
-
-
-items_by_type: Dict[ItemType, List[int]] = {item_type: [] for item_type in ItemType}
-for item_id, item_data in item_table.items():
- items_by_type[item_data.type].append(item_id)
-
-group_items: Dict[int, Set[int]] = {
- 35100: {35025, 35047, 35048, 35056, 35057, 35058, 35059, 35060, 35061, 35062, 35063, 35064, 35065, 35067, 35068,
- 35069, 35070, 35073, 35074},
- 35101: {35049, 35050, 35051, 35071, 35072, 35074},
- 35102: set(items_by_type[ItemType.resource]),
-}
diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py
deleted file mode 100644
index 582e93eb0ecb..000000000000
--- a/worlds/subnautica/Options.py
+++ /dev/null
@@ -1,115 +0,0 @@
-import typing
-
-from Options import Choice, Range, DeathLink, Toggle, DefaultOnToggle, StartInventoryPool
-from .Creatures import all_creatures, Definitions
-
-
-class SwimRule(Choice):
- """What logic considers ok swimming distances.
- Easy: +200 depth from any max vehicle depth.
- Normal: +400 depth from any max vehicle depth.
- Warning: Normal can expect you to death run to a location (No viable return trip).
- Hard: +600 depth from any max vehicle depth.
- Warning: Hard may require bases, deaths, glitches, multi-tank inventory or other depth extending means.
- Items: Expected depth is extended by items like seaglide, ultra glide fins and capacity tanks.
- """
- display_name = "Swim Rule"
- option_easy = 0
- option_normal = 1
- option_hard = 2
- option_items_easy = 3
- option_items_normal = 4
- option_items_hard = 5
-
- @property
- def base_depth(self) -> int:
- return [200, 400, 600][self.value % 3]
-
- @property
- def consider_items(self) -> bool:
- return self.value > 2
-
-
-class EarlySeaglide(DefaultOnToggle):
- """Make sure 2 of the Seaglide Fragments are available in or near the Safe Shallows (Sphere 1 Locations)."""
- display_name = "Early Seaglide"
-
-
-class FreeSamples(Toggle):
- """Get free items with your blueprints.
- Items that can go into your inventory are awarded when you unlock their blueprint through Archipelago."""
- display_name = "Free Samples"
-
-
-class Goal(Choice):
- """Goal to complete.
- Launch: Leave the planet.
- Free: Disable quarantine.
- Infected: Reach maximum infection level.
- Drive: Repair the Aurora's Drive Core"""
- auto_display_name = True
- display_name = "Goal"
- option_launch = 0
- option_free = 1
- option_infected = 2
- option_drive = 3
-
- def get_event_name(self) -> str:
- return {
- self.option_launch: "Neptune Launch",
- self.option_infected: "Full Infection",
- self.option_free: "Disable Quarantine",
- self.option_drive: "Repair Aurora Drive"
- }[self.value]
-
-
-class CreatureScans(Range):
- """Place items on specific, randomly chosen, creature scans.
- Warning: Includes aggressive Leviathans."""
- display_name = "Creature Scans"
- range_end = len(all_creatures)
-
-
-class AggressiveScanLogic(Choice):
- """By default (Stasis), aggressive Creature Scans are logically expected only with a Stasis Rifle.
- Containment: Removes Stasis Rifle as expected solution and expects Alien Containment instead.
- Either: Creatures may be expected to be scanned via Stasis Rifle or Containment, whichever is found first.
- None: Aggressive Creatures are assumed to not need any tools to scan.
- Removed: No Creatures needing Stasis or Containment will be in the pool at all.
-
- Note: Containment, Either and None adds Cuddlefish as an option for scans.
- Note: Stasis, Either and None adds unhatchable aggressive species, such as Warper.
- Note: This is purely a logic expectation, and does not affect gameplay, only placement."""
- display_name = "Aggressive Creature Scan Logic"
- option_stasis = 0
- option_containment = 1
- option_either = 2
- option_none = 3
- option_removed = 4
-
- def get_pool(self) -> typing.List[str]:
- if self == self.option_removed:
- return Definitions.all_creatures_presorted_without_aggressive_and_containment
- elif self == self.option_stasis:
- return Definitions.all_creatures_presorted_without_containment
- elif self == self.option_containment:
- return Definitions.all_creatures_presorted_without_stasis
- else:
- return Definitions.all_creatures_presorted
-
-
-class SubnauticaDeathLink(DeathLink):
- """When you die, everyone dies. Of course the reverse is true too.
- Note: can be toggled via in-game console command "deathlink"."""
-
-
-options = {
- "swim_rule": SwimRule,
- "early_seaglide": EarlySeaglide,
- "free_samples": FreeSamples,
- "goal": Goal,
- "creature_scans": CreatureScans,
- "creature_scan_logic": AggressiveScanLogic,
- "death_link": SubnauticaDeathLink,
- "start_inventory_from_pool": StartInventoryPool,
-}
diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py
deleted file mode 100644
index 793c85be4164..000000000000
--- a/worlds/subnautica/Rules.py
+++ /dev/null
@@ -1,334 +0,0 @@
-from typing import TYPE_CHECKING, Dict, Callable, Optional
-
-from worlds.generic.Rules import set_rule, add_rule
-from .Locations import location_table, LocationDict
-from .Creatures import all_creatures, aggressive, suffix, hatchable, containment
-from .Options import AggressiveScanLogic, SwimRule
-import math
-
-if TYPE_CHECKING:
- from . import SubnauticaWorld
- from BaseClasses import CollectionState, Location
-
-
-def has_seaglide(state: "CollectionState", player: int) -> bool:
- return state.has("Seaglide Fragment", player, 2)
-
-
-def has_modification_station(state: "CollectionState", player: int) -> bool:
- return state.has("Modification Station Fragment", player, 3)
-
-
-def has_mobile_vehicle_bay(state: "CollectionState", player: int) -> bool:
- return state.has("Mobile Vehicle Bay Fragment", player, 3)
-
-
-def has_moonpool(state: "CollectionState", player: int) -> bool:
- return state.has("Moonpool Fragment", player, 2)
-
-
-def has_vehicle_upgrade_console(state: "CollectionState", player: int) -> bool:
- return state.has("Vehicle Upgrade Console", player) and \
- has_moonpool(state, player)
-
-
-def has_seamoth(state: "CollectionState", player: int) -> bool:
- return state.has("Seamoth Fragment", player, 3) and \
- has_mobile_vehicle_bay(state, player)
-
-
-def has_seamoth_depth_module_mk1(state: "CollectionState", player: int) -> bool:
- return has_vehicle_upgrade_console(state, player)
-
-
-def has_seamoth_depth_module_mk2(state: "CollectionState", player: int) -> bool:
- return has_seamoth_depth_module_mk1(state, player) and \
- has_modification_station(state, player)
-
-
-def has_seamoth_depth_module_mk3(state: "CollectionState", player: int) -> bool:
- return has_seamoth_depth_module_mk2(state, player) and \
- has_modification_station(state, player)
-
-
-def has_cyclops_bridge(state: "CollectionState", player: int) -> bool:
- return state.has("Cyclops Bridge Fragment", player, 3)
-
-
-def has_cyclops_engine(state: "CollectionState", player: int) -> bool:
- return state.has("Cyclops Engine Fragment", player, 3)
-
-
-def has_cyclops_hull(state: "CollectionState", player: int) -> bool:
- return state.has("Cyclops Hull Fragment", player, 3)
-
-
-def has_cyclops(state: "CollectionState", player: int) -> bool:
- return has_cyclops_bridge(state, player) and \
- has_cyclops_engine(state, player) and \
- has_cyclops_hull(state, player) and \
- has_mobile_vehicle_bay(state, player)
-
-
-def has_cyclops_depth_module_mk1(state: "CollectionState", player: int) -> bool:
- # Crafted in the Cyclops, so we don't need to check for crafting station
- return state.has("Cyclops Depth Module MK1", player)
-
-
-def has_cyclops_depth_module_mk2(state: "CollectionState", player: int) -> bool:
- return has_cyclops_depth_module_mk1(state, player) and \
- has_modification_station(state, player)
-
-
-def has_cyclops_depth_module_mk3(state: "CollectionState", player: int) -> bool:
- return has_cyclops_depth_module_mk2(state, player) and \
- has_modification_station(state, player)
-
-
-def has_prawn(state: "CollectionState", player: int) -> bool:
- return state.has("Prawn Suit Fragment", player, 4) and \
- has_mobile_vehicle_bay(state, player)
-
-
-def has_prawn_propulsion_arm(state: "CollectionState", player: int) -> bool:
- return state.has("Prawn Suit Propulsion Cannon Fragment", player, 2) and \
- has_vehicle_upgrade_console(state, player)
-
-
-def has_prawn_depth_module_mk1(state: "CollectionState", player: int) -> bool:
- return has_vehicle_upgrade_console(state, player)
-
-
-def has_prawn_depth_module_mk2(state: "CollectionState", player: int) -> bool:
- return has_prawn_depth_module_mk1(state, player) and \
- has_modification_station(state, player)
-
-
-def has_laser_cutter(state: "CollectionState", player: int) -> bool:
- return state.has("Laser Cutter Fragment", player, 3)
-
-
-def has_stasis_rifle(state: "CollectionState", player: int) -> bool:
- return state.has("Stasis Rifle Fragment", player, 2)
-
-
-def has_containment(state: "CollectionState", player: int) -> bool:
- return state.has("Alien Containment", player) and has_utility_room(state, player)
-
-
-def has_utility_room(state: "CollectionState", player: int) -> bool:
- return state.has("Large Room", player) or state.has("Multipurpose Room", player)
-
-
-# Either we have propulsion cannon, or prawn + propulsion cannon arm
-def has_propulsion_cannon(state: "CollectionState", player: int) -> bool:
- return state.has("Propulsion Cannon Fragment", player, 2)
-
-
-def has_cyclops_shield(state: "CollectionState", player: int) -> bool:
- return has_cyclops(state, player) and \
- state.has("Cyclops Shield Generator", player)
-
-
-def has_ultra_high_capacity_tank(state: "CollectionState", player: int) -> bool:
- return has_modification_station(state, player) and state.has("Ultra High Capacity Tank", player)
-
-
-def has_lightweight_high_capacity_tank(state: "CollectionState", player: int) -> bool:
- return has_modification_station(state, player) and state.has("Lightweight High Capacity Tank", player)
-
-
-def has_ultra_glide_fins(state: "CollectionState", player: int) -> bool:
- return has_modification_station(state, player) and state.has("Ultra Glide Fins", player)
-
-# Swim depth rules:
-# Rebreather, high capacity tank and fins are available from the start.
-# All tests for those were done without inventory for light weight.
-# Fins and ultra Fins are better than charge fins, so we ignore charge fins.
-
-# swim speeds: https://subnautica.fandom.com/wiki/Swimming_Speed
-
-
-def get_max_swim_depth(state: "CollectionState", player: int) -> int:
- swim_rule: SwimRule = state.multiworld.swim_rule[player]
- depth: int = swim_rule.base_depth
- if swim_rule.consider_items:
- if has_seaglide(state, player):
- if has_ultra_high_capacity_tank(state, player):
- depth += 350 # It's about 800m. Give some room
- else:
- depth += 200 # It's about 650m. Give some room
- # seaglide and fins cannot be used together
- elif has_ultra_glide_fins(state, player):
- if has_ultra_high_capacity_tank(state, player):
- depth += 150
- elif has_lightweight_high_capacity_tank(state, player):
- depth += 75
- else:
- depth += 50
- elif has_ultra_high_capacity_tank(state, player):
- depth += 100
- elif has_lightweight_high_capacity_tank(state, player):
- depth += 25
- return depth
-
-
-def get_seamoth_max_depth(state: "CollectionState", player: int):
- if has_seamoth(state, player):
- if has_seamoth_depth_module_mk3(state, player):
- return 900
- elif has_seamoth_depth_module_mk2(state, player): # Will never be the case, 3 is craftable
- return 500
- elif has_seamoth_depth_module_mk1(state, player):
- return 300
- else:
- return 200
- else:
- return 0
-
-
-def get_cyclops_max_depth(state: "CollectionState", player):
- if has_cyclops(state, player):
- if has_cyclops_depth_module_mk3(state, player):
- return 1700
- elif has_cyclops_depth_module_mk2(state, player): # Will never be the case, 3 is craftable
- return 1300
- elif has_cyclops_depth_module_mk1(state, player):
- return 900
- else:
- return 500
- else:
- return 0
-
-
-def get_prawn_max_depth(state: "CollectionState", player):
- if has_prawn(state, player):
- if has_prawn_depth_module_mk2(state, player):
- return 1700
- elif has_prawn_depth_module_mk1(state, player):
- return 1300
- else:
- return 900
- else:
- return 0
-
-
-def get_max_depth(state: "CollectionState", player: int):
- return get_max_swim_depth(state, player) + max(
- get_seamoth_max_depth(state, player),
- get_cyclops_max_depth(state, player),
- get_prawn_max_depth(state, player)
- )
-
-
-def is_radiated(x: float, y: float, z: float) -> bool:
- aurora_dist = math.sqrt((x - 1038.0) ** 2 + y ** 2 + (z - -163.1) ** 2)
- return aurora_dist < 950
-
-
-def can_access_location(state: "CollectionState", player: int, loc: LocationDict) -> bool:
- need_laser_cutter = loc.get("need_laser_cutter", False)
- if need_laser_cutter and not has_laser_cutter(state, player):
- return False
-
- need_propulsion_cannon = loc.get("need_propulsion_cannon", False)
- if need_propulsion_cannon and not has_propulsion_cannon(state, player):
- return False
-
- pos = loc["position"]
- pos_x = pos["x"]
- pos_y = pos["y"]
- pos_z = pos["z"]
-
- need_radiation_suit = is_radiated(pos_x, pos_y, pos_z)
- if need_radiation_suit and not state.has("Radiation Suit", player):
- return False
-
- # Seaglide doesn't unlock anything specific, but just allows for faster movement.
- # Otherwise the game is painfully slow.
- map_center_dist = math.sqrt(pos_x ** 2 + pos_z ** 2)
- if (map_center_dist > 800 or pos_y < -200) and not has_seaglide(state, player):
- return False
-
- depth = -pos_y # y-up
- return get_max_depth(state, player) >= depth
-
-
-def set_location_rule(world, player: int, loc: LocationDict):
- set_rule(world.get_location(loc["name"], player), lambda state: can_access_location(state, player, loc))
-
-
-def can_scan_creature(state: "CollectionState", player: int, creature: str) -> bool:
- if not has_seaglide(state, player):
- return False
- return get_max_depth(state, player) >= all_creatures[creature]
-
-
-def set_creature_rule(world, player: int, creature_name: str) -> "Location":
- location = world.get_location(creature_name + suffix, player)
- set_rule(location,
- lambda state: can_scan_creature(state, player, creature_name))
- return location
-
-
-def get_aggression_rule(option: AggressiveScanLogic, creature_name: str) -> \
- Optional[Callable[["CollectionState", int], bool]]:
- """Get logic rule for a creature scan location."""
- if creature_name not in hatchable and option != option.option_none: # can only be done via stasis
- return has_stasis_rifle
- # otherwise allow option preference
- return aggression_rules.get(option.value, None)
-
-
-aggression_rules: Dict[int, Callable[["CollectionState", int], bool]] = {
- AggressiveScanLogic.option_stasis: has_stasis_rifle,
- AggressiveScanLogic.option_containment: has_containment,
- AggressiveScanLogic.option_either: lambda state, player:
- has_stasis_rifle(state, player) or has_containment(state, player)
-}
-
-
-def set_rules(subnautica_world: "SubnauticaWorld"):
- player = subnautica_world.player
- world = subnautica_world.multiworld
-
- for loc in location_table.values():
- set_location_rule(world, player, loc)
-
- if subnautica_world.creatures_to_scan:
- option = world.creature_scan_logic[player]
-
- for creature_name in subnautica_world.creatures_to_scan:
- location = set_creature_rule(world, player, creature_name)
- if creature_name in containment: # there is no other way, hard-required containment
- add_rule(location, lambda state: has_containment(state, player))
- elif creature_name in aggressive:
- rule = get_aggression_rule(option, creature_name)
- if rule:
- add_rule(location,
- lambda state, loc_rule=get_aggression_rule(option, creature_name): loc_rule(state, player))
-
- # Victory locations
- set_rule(world.get_location("Neptune Launch", player),
- lambda state:
- get_max_depth(state, player) >= 1444 and
- has_mobile_vehicle_bay(state, player) and
- state.has("Neptune Launch Platform", player) and
- state.has("Neptune Gantry", player) and
- state.has("Neptune Boosters", player) and
- state.has("Neptune Fuel Reserve", player) and
- state.has("Neptune Cockpit", player) and
- state.has("Ion Power Cell", player) and
- state.has("Ion Battery", player) and
- has_cyclops_shield(state, player))
-
- set_rule(world.get_location("Disable Quarantine", player), lambda state:
- get_max_depth(state, player) >= 1444)
-
- set_rule(world.get_location("Full Infection", player), lambda state:
- get_max_depth(state, player) >= 900)
-
- room = world.get_location("Aurora Drive Room - Upgrade Console", player)
- set_rule(world.get_location("Repair Aurora Drive", player), lambda state: room.can_reach(state))
-
- world.completion_condition[player] = lambda state: state.has("Victory", player)
diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py
index 1684260cc1a0..58d8fa543a6d 100644
--- a/worlds/subnautica/__init__.py
+++ b/worlds/subnautica/__init__.py
@@ -4,14 +4,14 @@
import itertools
from typing import List, Dict, Any, cast
-from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
+from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld
-from . import Items
-from . import Locations
-from . import Creatures
-from . import Options
-from .Items import item_table, group_items, items_by_type, ItemType
-from .Rules import set_rules
+from . import items
+from . import locations
+from . import creatures
+from . import options
+from .items import item_table, group_items, items_by_type, ItemType
+from .rules import set_rules
logger = logging.getLogger("Subnautica")
@@ -27,8 +27,8 @@ class SubnaticaWeb(WebWorld):
)]
-all_locations = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
-all_locations.update(Creatures.creature_locations)
+all_locations = {data["name"]: loc_id for loc_id, data in locations.location_table.items()}
+all_locations.update(creatures.creature_locations)
class SubnauticaWorld(World):
@@ -40,55 +40,72 @@ class SubnauticaWorld(World):
game = "Subnautica"
web = SubnaticaWeb()
- item_name_to_id = {data.name: item_id for item_id, data in Items.item_table.items()}
+ item_name_to_id = {data.name: item_id for item_id, data in items.item_table.items()}
location_name_to_id = all_locations
- option_definitions = Options.options
-
- data_version = 10
- required_client_version = (0, 4, 1)
+ options_dataclass = options.SubnauticaOptions
+ options: options.SubnauticaOptions
+ required_client_version = (0, 5, 0)
creatures_to_scan: List[str]
def generate_early(self) -> None:
- if self.multiworld.early_seaglide[self.player]:
+ if not self.options.filler_items_distribution.weights_pair[1][-1]:
+ raise Exception("Filler Items Distribution needs at least one positive weight.")
+ if self.options.early_seaglide:
self.multiworld.local_early_items[self.player]["Seaglide Fragment"] = 2
- scan_option: Options.AggressiveScanLogic = self.multiworld.creature_scan_logic[self.player]
+ scan_option: options.AggressiveScanLogic = self.options.creature_scan_logic
creature_pool = scan_option.get_pool()
- self.multiworld.creature_scans[self.player].value = min(
+ self.options.creature_scans.value = min(
len(creature_pool),
- self.multiworld.creature_scans[self.player].value
+ self.options.creature_scans.value
)
- self.creatures_to_scan = self.multiworld.random.sample(
- creature_pool, self.multiworld.creature_scans[self.player].value)
+ self.creatures_to_scan = self.random.sample(
+ creature_pool, self.options.creature_scans.value)
def create_regions(self):
+ # Create Regions
+ menu_region = Region("Menu", self.player, self.multiworld)
+ planet_region = Region("Planet 4546B", self.player, self.multiworld)
+
+ # Link regions together
+ menu_region.connect(planet_region, "Lifepod 5")
+
+ # Create regular locations
+ location_names = itertools.chain((location["name"] for location in locations.location_table.values()),
+ (creature + creatures.suffix for creature in self.creatures_to_scan))
+ for location_name in location_names:
+ loc_id = self.location_name_to_id[location_name]
+ location = SubnauticaLocation(self.player, location_name, loc_id, planet_region)
+ planet_region.locations.append(location)
+
+ # Create events
+ goal_event_name = self.options.goal.get_event_name()
+
+ for event in locations.events:
+ location = SubnauticaLocation(self.player, event, None, planet_region)
+ planet_region.locations.append(location)
+ location.place_locked_item(
+ SubnauticaItem(event, ItemClassification.progression, None, player=self.player))
+ if event == goal_event_name:
+ # make the goal event the victory "item"
+ location.item.name = "Victory"
+
+ # Register regions to multiworld
self.multiworld.regions += [
- self.create_region("Menu", None, ["Lifepod 5"]),
- self.create_region("Planet 4546B",
- Locations.events +
- [location["name"] for location in Locations.location_table.values()] +
- [creature+Creatures.suffix for creature in self.creatures_to_scan])
+ menu_region,
+ planet_region
]
- # Link regions
- self.multiworld.get_entrance("Lifepod 5", self.player).connect(self.multiworld.get_region("Planet 4546B", self.player))
-
- for event in Locations.events:
- self.multiworld.get_location(event, self.player).place_locked_item(
- SubnauticaItem(event, ItemClassification.progression, None, player=self.player))
- # make the goal event the victory "item"
- self.multiworld.get_location(self.multiworld.goal[self.player].get_event_name(), self.player).item.name = "Victory"
-
- # refer to Rules.py
+ # refer to rules.py
set_rules = set_rules
def create_items(self):
# Generate item pool
pool: List[SubnauticaItem] = []
- extras = self.multiworld.creature_scans[self.player].value
+ extras = self.options.creature_scans.value
grouped = set(itertools.chain.from_iterable(group_items.values()))
@@ -99,7 +116,7 @@ def create_items(self):
for i in range(item.count):
subnautica_item = self.create_item(item.name)
if item.name == "Neptune Launch Platform":
- self.multiworld.get_location("Aurora - Captain Data Terminal", self.player).place_locked_item(
+ self.get_location("Aurora - Captain Data Terminal").place_locked_item(
subnautica_item)
else:
pool.append(subnautica_item)
@@ -112,19 +129,20 @@ def create_items(self):
pool.append(self.create_item(name))
extras -= group_amount
- for item_name in self.multiworld.random.sample(
- # list of high-count important fragments as priority filler
+ for item_name in self.random.sample(
+ # list of high-count important fragments as priority filler
[
"Cyclops Engine Fragment",
- "Modification Station Fragment",
- "Mobile Vehicle Bay Fragment",
- "Seamoth Fragment",
"Cyclops Hull Fragment",
"Cyclops Bridge Fragment",
+ "Seamoth Fragment",
"Prawn Suit Fragment",
+ "Mobile Vehicle Bay Fragment",
+ "Modification Station Fragment",
"Moonpool Fragment",
- ],
- k=min(extras, 8)):
+ "Laser Cutter Fragment",
+ ],
+ k=min(extras, 9)):
item = self.create_item(item_name)
pool.append(item)
extras -= 1
@@ -138,17 +156,15 @@ def create_items(self):
self.multiworld.itempool += pool
def fill_slot_data(self) -> Dict[str, Any]:
- goal: Options.Goal = self.multiworld.goal[self.player]
- swim_rule: Options.SwimRule = self.multiworld.swim_rule[self.player]
vanilla_tech: List[str] = []
slot_data: Dict[str, Any] = {
- "goal": goal.current_key,
- "swim_rule": swim_rule.current_key,
+ "goal": self.options.goal.current_key,
+ "swim_rule": self.options.swim_rule.current_key,
"vanilla_tech": vanilla_tech,
"creatures_to_scan": self.creatures_to_scan,
- "death_link": self.multiworld.death_link[self.player].value,
- "free_samples": self.multiworld.free_samples[self.player].value,
+ "death_link": self.options.death_link.value,
+ "free_samples": self.options.free_samples.value,
}
return slot_data
@@ -160,20 +176,11 @@ def create_item(self, name: str) -> SubnauticaItem:
item_table[item_id].classification,
item_id, player=self.player)
- def create_region(self, name: str, locations=None, exits=None):
- ret = Region(name, self.player, self.multiworld)
- if locations:
- for location in locations:
- loc_id = self.location_name_to_id.get(location, None)
- location = SubnauticaLocation(self.player, location, loc_id, ret)
- ret.locations.append(location)
- if exits:
- for region_exit in exits:
- ret.exits.append(Entrance(self.player, region_exit, ret))
- return ret
-
def get_filler_item_name(self) -> str:
- return item_table[self.multiworld.random.choice(items_by_type[ItemType.resource])].name
+ item_names, cum_item_weights = self.options.filler_items_distribution.weights_pair
+ return self.random.choices(item_names,
+ cum_weights=cum_item_weights,
+ k=1)[0]
class SubnauticaLocation(Location):
diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/creatures.py
similarity index 100%
rename from worlds/subnautica/Creatures.py
rename to worlds/subnautica/creatures.py
diff --git a/worlds/subnautica/docs/en_Subnautica.md b/worlds/subnautica/docs/en_Subnautica.md
index 9a112aa596ec..50004de5a003 100644
--- a/worlds/subnautica/docs/en_Subnautica.md
+++ b/worlds/subnautica/docs/en_Subnautica.md
@@ -1,8 +1,8 @@
# Subnautica
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -12,7 +12,7 @@ awarded from scanning those items have been shuffled into location checks throug
## What is the goal of Subnautica when randomized?
-The goal remains unchanged. Cure the plague, build the Neptune Escape Rocket, and escape into space.
+There are four goals currently available. The Launch goal has you leave the planet. The Free goal has you cure the plague. Infected is achieved at maximum infection level. Drive asks you to repair the Aurora Drive Core.
## What items and locations get shuffled?
@@ -34,5 +34,5 @@ player's world.
## When the player receives a technology, what happens?
-When the player receives a technology, the chat log displays a notification the technology has been received.
+When the player receives a technology, the chat log displays a notification that the technology has been received.
diff --git a/worlds/subnautica/docs/setup_en.md b/worlds/subnautica/docs/setup_en.md
index 83f4186bdfaf..7fc637df2639 100644
--- a/worlds/subnautica/docs/setup_en.md
+++ b/worlds/subnautica/docs/setup_en.md
@@ -19,7 +19,7 @@
Use the connect form in Subnautica's main menu to enter your connection information to connect to an Archipelago multiworld.
Connection information consists of:
- Host: the full url that you're trying to connect to, such as `archipelago.gg:38281`.
- - PlayerName: your name in the multiworld. Can also be called "slot name" and is the name you entered when creating your settings.
+ - PlayerName: your name in the multiworld. Can also be called "slot name" and is the name you entered when creating your options.
- Password: optional password, leave blank if no password was set.
After the connection is made, start a new game. You should start to see Archipelago chat messages to appear, such as a message announcing that you joined the multiworld.
diff --git a/worlds/subnautica/exports.py b/worlds/subnautica/exports.py
new file mode 100644
index 000000000000..911c6a6f6937
--- /dev/null
+++ b/worlds/subnautica/exports.py
@@ -0,0 +1,76 @@
+"""Runnable module that exports data needed by the mod/client."""
+
+if __name__ == "__main__":
+ import json
+ import math
+ import sys
+ import os
+
+ # makes this module runnable from its world folder.
+ sys.path.remove(os.path.dirname(__file__))
+ new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
+ os.chdir(new_home)
+ sys.path.append(new_home)
+
+ from worlds.subnautica.locations import Vector, location_table
+ from worlds.subnautica.items import item_table, group_items, items_by_type
+ from NetUtils import encode
+
+ export_folder = os.path.join(new_home, "Subnautica Export")
+ os.makedirs(export_folder, exist_ok=True)
+
+ def in_export_folder(path: str) -> str:
+ return os.path.join(export_folder, path)
+
+ payload = {location_id: location_data["position"] for location_id, location_data in location_table.items()}
+ with open(in_export_folder("locations.json"), "w") as f:
+ json.dump(payload, f)
+
+ # copy-paste from Rules
+ def is_radiated(x: float, y: float, z: float) -> bool:
+ aurora_dist = math.sqrt((x - 1038.0) ** 2 + y ** 2 + (z - -163.1) ** 2)
+ return aurora_dist < 950
+ # end of copy-paste
+
+
+ def radiated(pos: Vector):
+ return is_radiated(pos["x"], pos["y"], pos["z"])
+
+
+ def far_away(pos: Vector):
+ return (pos["x"] ** 2 + pos["z"] ** 2) > (800 ** 2)
+
+
+ payload = {
+ # "LaserCutter" in Subnautica ID
+ "761": [location_id for location_id, location_data
+ in location_table.items() if location_data["need_laser_cutter"]],
+ # PropulsionCannon in Subnautica ID
+ "757": [location_id for location_id, location_data
+ in location_table.items() if location_data.get("need_propulsion_cannon", False)],
+ # Radiation Suit in Subnautica ID
+ "519": [location_id for location_id, location_data
+ in location_table.items() if radiated(location_data["position"])],
+ # SeaGlide in Subnautica ID
+ "751": [location_id for location_id, location_data
+ in location_table.items() if far_away(location_data["position"])],
+ }
+ with open(in_export_folder("logic.json"), "w") as f:
+ json.dump(payload, f)
+
+ itemcount = sum(item_data.count for item_data in item_table.values())
+ assert itemcount == len(location_table), f"{itemcount} != {len(location_table)}"
+ payload = {item_id: item_data.tech_type for item_id, item_data in item_table.items()}
+ import json
+
+ with open(in_export_folder("items.json"), "w") as f:
+ json.dump(payload, f)
+
+ with open(in_export_folder("group_items.json"), "w") as f:
+ # encode to convert set to list
+ f.write(encode(group_items))
+
+ with open(in_export_folder("item_types.json"), "w") as f:
+ json.dump(items_by_type, f)
+
+ print(f"Subnautica exports dumped to {in_export_folder('')}")
diff --git a/worlds/subnautica/items.py b/worlds/subnautica/items.py
new file mode 100644
index 000000000000..d5dcf6a6af25
--- /dev/null
+++ b/worlds/subnautica/items.py
@@ -0,0 +1,157 @@
+from BaseClasses import ItemClassification as IC
+from typing import NamedTuple, Dict, Set, List
+from enum import IntEnum
+
+
+class ItemType(IntEnum):
+ technology = 1
+ resource = 2
+ group = 3
+
+
+class ItemData(NamedTuple):
+ classification: IC
+ count: int
+ name: str
+ tech_type: str
+ type: ItemType = ItemType.technology
+
+
+def make_resource_bundle_data(display_name: str, internal_name: str = "") -> ItemData:
+ if not internal_name:
+ internal_name = display_name
+ return ItemData(IC.filler, 0, display_name, internal_name, ItemType.resource)
+
+
+item_table: Dict[int, ItemData] = {
+ 35000: ItemData(IC.useful, 1, "Compass", "Compass"),
+ 35001: ItemData(IC.progression, 1, "Lightweight High Capacity Tank", "PlasteelTank"),
+ 35002: ItemData(IC.progression, 1, "Vehicle Upgrade Console", "BaseUpgradeConsole"),
+ 35003: ItemData(IC.progression, 1, "Ultra Glide Fins", "UltraGlideFins"),
+ 35004: ItemData(IC.useful, 1, "Cyclops Sonar Upgrade", "CyclopsSonarModule"),
+ 35005: ItemData(IC.useful, 1, "Reinforced Dive Suit", "ReinforcedDiveSuit"),
+ 35006: ItemData(IC.useful, 1, "Cyclops Thermal Reactor Module", "CyclopsThermalReactorModule"),
+ 35007: ItemData(IC.filler, 1, "Water Filtration Suit", "WaterFiltrationSuit"),
+ 35008: ItemData(IC.progression, 1, "Alien Containment", "BaseWaterPark"),
+ 35009: ItemData(IC.useful, 1, "Creature Decoy", "CyclopsDecoy"),
+ 35010: ItemData(IC.useful, 1, "Cyclops Fire Suppression System", "CyclopsFireSuppressionModule"),
+ 35011: ItemData(IC.useful, 1, "Swim Charge Fins", "SwimChargeFins"),
+ 35012: ItemData(IC.useful, 1, "Repulsion Cannon", "RepulsionCannon"),
+ 35013: ItemData(IC.useful, 1, "Cyclops Decoy Tube Upgrade", "CyclopsDecoyModule"),
+ 35014: ItemData(IC.progression, 1, "Cyclops Shield Generator", "CyclopsShieldModule"),
+ 35015: ItemData(IC.progression, 1, "Cyclops Depth Module MK1", "CyclopsHullModule1"),
+ 35016: ItemData(IC.useful, 1, "Cyclops Docking Bay Repair Module", "CyclopsSeamothRepairModule"),
+ 35017: ItemData(IC.useful, 2, "Battery Charger fragment", "BatteryChargerFragment"),
+ 35018: ItemData(IC.filler, 2, "Beacon Fragment", "BeaconFragment"),
+ 35019: ItemData(IC.useful, 2, "Bioreactor Fragment", "BaseBioReactorFragment"),
+ 35020: ItemData(IC.progression, 4, "Cyclops Bridge Fragment", "CyclopsBridgeFragment"),
+ 35021: ItemData(IC.progression, 4, "Cyclops Engine Fragment", "CyclopsEngineFragment"),
+ 35022: ItemData(IC.progression, 4, "Cyclops Hull Fragment", "CyclopsHullFragment"),
+ 35023: ItemData(IC.filler, 2, "Grav Trap Fragment", "GravSphereFragment"),
+ 35024: ItemData(IC.progression, 3, "Laser Cutter Fragment", "LaserCutterFragment"),
+ 35025: ItemData(IC.filler, 2, "Light Stick Fragment", "TechlightFragment"),
+ 35026: ItemData(IC.progression, 5, "Mobile Vehicle Bay Fragment", "ConstructorFragment"),
+ 35027: ItemData(IC.progression, 3, "Modification Station Fragment", "WorkbenchFragment"),
+ 35028: ItemData(IC.progression, 2, "Moonpool Fragment", "MoonpoolFragment"),
+ 35029: ItemData(IC.useful, 3, "Nuclear Reactor Fragment", "BaseNuclearReactorFragment"),
+ 35030: ItemData(IC.useful, 2, "Power Cell Charger Fragment", "PowerCellChargerFragment"),
+ 35031: ItemData(IC.filler, 1, "Power Transmitter Fragment", "PowerTransmitterFragment"),
+ 35032: ItemData(IC.progression, 6, "Prawn Suit Fragment", "ExosuitFragment"),
+ 35033: ItemData(IC.useful, 2, "Prawn Suit Drill Arm Fragment", "ExosuitDrillArmFragment"),
+ 35034: ItemData(IC.useful, 2, "Prawn Suit Grappling Arm Fragment", "ExosuitGrapplingArmFragment"),
+ 35035: ItemData(IC.useful, 2, "Prawn Suit Propulsion Cannon Fragment", "ExosuitPropulsionArmFragment"),
+ 35036: ItemData(IC.useful, 2, "Prawn Suit Torpedo Arm Fragment", "ExosuitTorpedoArmFragment"),
+ 35037: ItemData(IC.useful, 3, "Scanner Room Fragment", "BaseMapRoomFragment"),
+ 35038: ItemData(IC.progression, 5, "Seamoth Fragment", "SeamothFragment"),
+ 35039: ItemData(IC.progression, 2, "Stasis Rifle Fragment", "StasisRifleFragment"),
+ 35040: ItemData(IC.useful, 2, "Thermal Plant Fragment", "ThermalPlantFragment"),
+ 35041: ItemData(IC.progression, 4, "Seaglide Fragment", "SeaglideFragment"),
+ 35042: ItemData(IC.progression, 1, "Radiation Suit", "RadiationSuit"),
+ 35043: ItemData(IC.progression, 2, "Propulsion Cannon Fragment", "PropulsionCannonFragment"),
+ 35044: ItemData(IC.progression_skip_balancing, 1, "Neptune Launch Platform", "RocketBase"),
+ 35045: ItemData(IC.progression, 1, "Ion Power Cell", "PrecursorIonPowerCell"),
+ 35046: ItemData(IC.filler, 2, "Exterior Growbed", "FarmingTray"),
+ 35047: ItemData(IC.filler, 1, "Picture Frame", "PictureFrameFragment"),
+ 35048: ItemData(IC.filler, 1, "Bench", "Bench"),
+ 35049: ItemData(IC.filler, 1, "Basic Plant Pot", "PlanterPotFragment"),
+ 35050: ItemData(IC.filler, 1, "Interior Growbed", "PlanterBoxFragment"),
+ 35051: ItemData(IC.filler, 1, "Plant Shelf", "PlanterShelfFragment"),
+ 35052: ItemData(IC.filler, 1, "Observatory", "BaseObservatory"),
+ 35053: ItemData(IC.progression, 1, "Multipurpose Room", "BaseRoom"),
+ 35054: ItemData(IC.useful, 1, "Bulkhead", "BaseBulkhead"),
+ 35055: ItemData(IC.filler, 1, "Spotlight", "Spotlight"),
+ 35056: ItemData(IC.filler, 1, "Desk", "StarshipDesk"),
+ 35057: ItemData(IC.filler, 1, "Swivel Chair", "StarshipChair"),
+ 35058: ItemData(IC.filler, 1, "Office Chair", "StarshipChair2"),
+ 35059: ItemData(IC.filler, 1, "Command Chair", "StarshipChair3"),
+ 35060: ItemData(IC.filler, 1, "Counter", "LabCounter"),
+ 35061: ItemData(IC.filler, 1, "Single Bed", "NarrowBed"),
+ 35062: ItemData(IC.filler, 1, "Basic Double Bed", "Bed1"),
+ 35063: ItemData(IC.filler, 1, "Quilted Double Bed", "Bed2"),
+ 35064: ItemData(IC.filler, 1, "Coffee Vending Machine", "CoffeeVendingMachine"),
+ 35065: ItemData(IC.filler, 1, "Trash Can", "Trashcans"),
+ 35066: ItemData(IC.filler, 1, "Floodlight", "Techlight"),
+ 35067: ItemData(IC.filler, 1, "Bar Table", "BarTable"),
+ 35068: ItemData(IC.filler, 1, "Vending Machine", "VendingMachine"),
+ 35069: ItemData(IC.filler, 1, "Single Wall Shelf", "SingleWallShelf"),
+ 35070: ItemData(IC.filler, 1, "Wall Shelves", "WallShelves"),
+ 35071: ItemData(IC.filler, 1, "Round Plant Pot", "PlanterPot2"),
+ 35072: ItemData(IC.filler, 1, "Chic Plant Pot", "PlanterPot3"),
+ 35073: ItemData(IC.filler, 1, "Nuclear Waste Disposal", "LabTrashcan"),
+ 35074: ItemData(IC.filler, 1, "Wall Planter", "BasePlanter"),
+ 35075: ItemData(IC.progression, 1, "Ion Battery", "PrecursorIonBattery"),
+ 35076: ItemData(IC.progression_skip_balancing, 1, "Neptune Gantry", "RocketBaseLadder"),
+ 35077: ItemData(IC.progression_skip_balancing, 1, "Neptune Boosters", "RocketStage1"),
+ 35078: ItemData(IC.progression_skip_balancing, 1, "Neptune Fuel Reserve", "RocketStage2"),
+ 35079: ItemData(IC.progression_skip_balancing, 1, "Neptune Cockpit", "RocketStage3"),
+ 35080: ItemData(IC.filler, 1, "Water Filtration Machine", "BaseFiltrationMachine"),
+ 35081: ItemData(IC.progression, 1, "Ultra High Capacity Tank", "HighCapacityTank"),
+ 35082: ItemData(IC.progression, 1, "Large Room", "BaseLargeRoom"),
+ # awarded with their rooms, keeping that as-is as they"re cosmetic
+ 35083: ItemData(IC.filler, 0, "Large Room Glass Dome", "BaseLargeGlassDome"),
+ 35084: ItemData(IC.filler, 0, "Multipurpose Room Glass Dome", "BaseGlassDome"),
+ 35085: ItemData(IC.filler, 0, "Partition", "BasePartition"),
+ 35086: ItemData(IC.filler, 0, "Partition Door", "BasePartitionDoor"),
+
+ # Bundles of items
+ # Awards all furniture as a bundle
+ 35100: ItemData(IC.filler, 0, "Furniture", "AP_Furniture", ItemType.group),
+ # Awards all farming blueprints as a bundle
+ 35101: ItemData(IC.filler, 0, "Farming", "AP_Farming", ItemType.group),
+
+ # Awards multiple resources as a bundle
+ 35102: ItemData(IC.filler, 0, "Resources Bundle", "AP_Resources", ItemType.group),
+
+ # resource bundles, as convenience/filler
+
+ # ores
+ 35200: make_resource_bundle_data("Titanium"),
+ 35201: make_resource_bundle_data("Copper Ore", "Copper"),
+ 35202: make_resource_bundle_data("Silver Ore", "Silver"),
+ 35203: make_resource_bundle_data("Gold"),
+ 35204: make_resource_bundle_data("Lead"),
+ 35205: make_resource_bundle_data("Diamond"),
+ 35206: make_resource_bundle_data("Lithium"),
+ 35207: make_resource_bundle_data("Ruby", "AluminumOxide"),
+ 35208: make_resource_bundle_data("Nickel Ore", "Nickel"),
+ 35209: make_resource_bundle_data("Crystalline Sulfur", "Sulphur"),
+ 35210: make_resource_bundle_data("Salt Deposit", "Salt"),
+ 35211: make_resource_bundle_data("Kyanite"),
+ 35212: make_resource_bundle_data("Magnetite"),
+ 35213: make_resource_bundle_data("Reactor Rod", "ReactorRod"),
+}
+
+
+items_by_type: Dict[ItemType, List[int]] = {item_type: [] for item_type in ItemType}
+for item_id, item_data in item_table.items():
+ items_by_type[item_data.type].append(item_id)
+item_names_by_type: Dict[ItemType, List[str]] = {
+ item_type: sorted(item_table[item_id].name for item_id in item_ids) for item_type, item_ids in items_by_type.items()
+}
+
+group_items: Dict[int, Set[int]] = {
+ 35100: {35025, 35047, 35048, 35056, 35057, 35058, 35059, 35060, 35061, 35062, 35063, 35064, 35065, 35067, 35068,
+ 35069, 35070, 35073, 35074},
+ 35101: {35049, 35050, 35051, 35071, 35072, 35074},
+ 35102: set(items_by_type[ItemType.resource]),
+}
diff --git a/worlds/subnautica/Locations.py b/worlds/subnautica/locations.py
similarity index 100%
rename from worlds/subnautica/Locations.py
rename to worlds/subnautica/locations.py
diff --git a/worlds/subnautica/options.py b/worlds/subnautica/options.py
new file mode 100644
index 000000000000..4bdd9aafa53f
--- /dev/null
+++ b/worlds/subnautica/options.py
@@ -0,0 +1,143 @@
+import typing
+from dataclasses import dataclass
+from functools import cached_property
+
+from Options import (
+ Choice,
+ Range,
+ DeathLink,
+ Toggle,
+ DefaultOnToggle,
+ StartInventoryPool,
+ ItemDict,
+ PerGameCommonOptions,
+)
+
+from .creatures import all_creatures, Definitions
+from .items import ItemType, item_names_by_type
+
+
+class SwimRule(Choice):
+ """What logic considers ok swimming distances.
+ Easy: +200 depth from any max vehicle depth.
+ Normal: +400 depth from any max vehicle depth.
+ Warning: Normal can expect you to death run to a location (No viable return trip).
+ Hard: +600 depth from any max vehicle depth.
+ Warning: Hard may require bases, deaths, glitches, multi-tank inventory or other depth extending means.
+ Items: Expected depth is extended by items like seaglide, ultra glide fins and capacity tanks.
+ """
+ display_name = "Swim Rule"
+ option_easy = 0
+ option_normal = 1
+ option_hard = 2
+ option_items_easy = 3
+ option_items_normal = 4
+ option_items_hard = 5
+
+ @property
+ def base_depth(self) -> int:
+ return [200, 400, 600][self.value % 3]
+
+ @property
+ def consider_items(self) -> bool:
+ return self.value > 2
+
+
+class EarlySeaglide(DefaultOnToggle):
+ """Make sure 2 of the Seaglide Fragments are available in or near the Safe Shallows (Sphere 1 Locations)."""
+ display_name = "Early Seaglide"
+
+
+class FreeSamples(Toggle):
+ """Get free items with your blueprints.
+ Items that can go into your inventory are awarded when you unlock their blueprint through Archipelago."""
+ display_name = "Free Samples"
+
+
+class Goal(Choice):
+ """Goal to complete.
+ Launch: Leave the planet.
+ Free: Disable quarantine.
+ Infected: Reach maximum infection level.
+ Drive: Repair the Aurora's Drive Core"""
+ auto_display_name = True
+ display_name = "Goal"
+ option_launch = 0
+ option_free = 1
+ option_infected = 2
+ option_drive = 3
+
+ def get_event_name(self) -> str:
+ return {
+ self.option_launch: "Neptune Launch",
+ self.option_infected: "Full Infection",
+ self.option_free: "Disable Quarantine",
+ self.option_drive: "Repair Aurora Drive"
+ }[self.value]
+
+
+class CreatureScans(Range):
+ """Place items on specific, randomly chosen, creature scans.
+ Warning: Includes aggressive Leviathans."""
+ display_name = "Creature Scans"
+ range_end = len(all_creatures)
+
+
+class AggressiveScanLogic(Choice):
+ """By default (Stasis), aggressive Creature Scans are logically expected only with a Stasis Rifle.
+ Containment: Removes Stasis Rifle as expected solution and expects Alien Containment instead.
+ Either: Creatures may be expected to be scanned via Stasis Rifle or Containment, whichever is found first.
+ None: Aggressive Creatures are assumed to not need any tools to scan.
+ Removed: No Creatures needing Stasis or Containment will be in the pool at all.
+
+ Note: Containment, Either and None adds Cuddlefish as an option for scans.
+ Note: Stasis, Either and None adds unhatchable aggressive species, such as Warper.
+ Note: This is purely a logic expectation, and does not affect gameplay, only placement."""
+ display_name = "Aggressive Creature Scan Logic"
+ option_stasis = 0
+ option_containment = 1
+ option_either = 2
+ option_none = 3
+ option_removed = 4
+
+ def get_pool(self) -> typing.List[str]:
+ if self == self.option_removed:
+ return Definitions.all_creatures_presorted_without_aggressive_and_containment
+ elif self == self.option_stasis:
+ return Definitions.all_creatures_presorted_without_containment
+ elif self == self.option_containment:
+ return Definitions.all_creatures_presorted_without_stasis
+ else:
+ return Definitions.all_creatures_presorted
+
+
+class SubnauticaDeathLink(DeathLink):
+ """When you die, everyone dies. Of course the reverse is true too.
+ Note: can be toggled via in-game console command "deathlink"."""
+
+
+class FillerItemsDistribution(ItemDict):
+ """Random chance weights of various filler resources that can be obtained.
+ Available items: """
+ __doc__ += ", ".join(f"\"{item_name}\"" for item_name in item_names_by_type[ItemType.resource])
+ valid_keys = sorted(item_names_by_type[ItemType.resource])
+ default = {item_name: 1 for item_name in item_names_by_type[ItemType.resource]}
+ display_name = "Filler Items Distribution"
+
+ @cached_property
+ def weights_pair(self) -> typing.Tuple[typing.List[str], typing.List[int]]:
+ from itertools import accumulate
+ return list(self.value.keys()), list(accumulate(self.value.values()))
+
+
+@dataclass
+class SubnauticaOptions(PerGameCommonOptions):
+ swim_rule: SwimRule
+ early_seaglide: EarlySeaglide
+ free_samples: FreeSamples
+ goal: Goal
+ creature_scans: CreatureScans
+ creature_scan_logic: AggressiveScanLogic
+ death_link: SubnauticaDeathLink
+ start_inventory_from_pool: StartInventoryPool
+ filler_items_distribution: FillerItemsDistribution
diff --git a/worlds/subnautica/rules.py b/worlds/subnautica/rules.py
new file mode 100644
index 000000000000..ea9ec6a8058f
--- /dev/null
+++ b/worlds/subnautica/rules.py
@@ -0,0 +1,335 @@
+from typing import TYPE_CHECKING, Dict, Callable, Optional
+
+from worlds.generic.Rules import set_rule, add_rule
+from .locations import location_table, LocationDict
+from .creatures import all_creatures, aggressive, suffix, hatchable, containment
+from .options import AggressiveScanLogic, SwimRule
+import math
+
+if TYPE_CHECKING:
+ from . import SubnauticaWorld
+ from BaseClasses import CollectionState, Location
+
+
+def has_seaglide(state: "CollectionState", player: int) -> bool:
+ return state.has("Seaglide Fragment", player, 2)
+
+
+def has_modification_station(state: "CollectionState", player: int) -> bool:
+ return state.has("Modification Station Fragment", player, 3)
+
+
+def has_mobile_vehicle_bay(state: "CollectionState", player: int) -> bool:
+ return state.has("Mobile Vehicle Bay Fragment", player, 3)
+
+
+def has_moonpool(state: "CollectionState", player: int) -> bool:
+ return state.has("Moonpool Fragment", player, 2)
+
+
+def has_vehicle_upgrade_console(state: "CollectionState", player: int) -> bool:
+ return state.has("Vehicle Upgrade Console", player) and \
+ has_moonpool(state, player)
+
+
+def has_seamoth(state: "CollectionState", player: int) -> bool:
+ return state.has("Seamoth Fragment", player, 3) and \
+ has_mobile_vehicle_bay(state, player)
+
+
+def has_seamoth_depth_module_mk1(state: "CollectionState", player: int) -> bool:
+ return has_vehicle_upgrade_console(state, player)
+
+
+def has_seamoth_depth_module_mk2(state: "CollectionState", player: int) -> bool:
+ return has_seamoth_depth_module_mk1(state, player) and \
+ has_modification_station(state, player)
+
+
+def has_seamoth_depth_module_mk3(state: "CollectionState", player: int) -> bool:
+ return has_seamoth_depth_module_mk2(state, player) and \
+ has_modification_station(state, player)
+
+
+def has_cyclops_bridge(state: "CollectionState", player: int) -> bool:
+ return state.has("Cyclops Bridge Fragment", player, 3)
+
+
+def has_cyclops_engine(state: "CollectionState", player: int) -> bool:
+ return state.has("Cyclops Engine Fragment", player, 3)
+
+
+def has_cyclops_hull(state: "CollectionState", player: int) -> bool:
+ return state.has("Cyclops Hull Fragment", player, 3)
+
+
+def has_cyclops(state: "CollectionState", player: int) -> bool:
+ return has_cyclops_bridge(state, player) and \
+ has_cyclops_engine(state, player) and \
+ has_cyclops_hull(state, player) and \
+ has_mobile_vehicle_bay(state, player)
+
+
+def has_cyclops_depth_module_mk1(state: "CollectionState", player: int) -> bool:
+ # Crafted in the Cyclops, so we don't need to check for crafting station
+ return state.has("Cyclops Depth Module MK1", player)
+
+
+def has_cyclops_depth_module_mk2(state: "CollectionState", player: int) -> bool:
+ return has_cyclops_depth_module_mk1(state, player) and \
+ has_modification_station(state, player)
+
+
+def has_cyclops_depth_module_mk3(state: "CollectionState", player: int) -> bool:
+ return has_cyclops_depth_module_mk2(state, player) and \
+ has_modification_station(state, player)
+
+
+def has_prawn(state: "CollectionState", player: int) -> bool:
+ return state.has("Prawn Suit Fragment", player, 4) and \
+ has_mobile_vehicle_bay(state, player)
+
+
+def has_prawn_propulsion_arm(state: "CollectionState", player: int) -> bool:
+ return state.has("Prawn Suit Propulsion Cannon Fragment", player, 2) and \
+ has_vehicle_upgrade_console(state, player)
+
+
+def has_prawn_depth_module_mk1(state: "CollectionState", player: int) -> bool:
+ return has_vehicle_upgrade_console(state, player)
+
+
+def has_prawn_depth_module_mk2(state: "CollectionState", player: int) -> bool:
+ return has_prawn_depth_module_mk1(state, player) and \
+ has_modification_station(state, player)
+
+
+def has_laser_cutter(state: "CollectionState", player: int) -> bool:
+ return state.has("Laser Cutter Fragment", player, 3)
+
+
+def has_stasis_rifle(state: "CollectionState", player: int) -> bool:
+ return state.has("Stasis Rifle Fragment", player, 2)
+
+
+def has_containment(state: "CollectionState", player: int) -> bool:
+ return state.has("Alien Containment", player) and has_utility_room(state, player)
+
+
+def has_utility_room(state: "CollectionState", player: int) -> bool:
+ return state.has("Large Room", player) or state.has("Multipurpose Room", player)
+
+
+# Either we have propulsion cannon, or prawn + propulsion cannon arm
+def has_propulsion_cannon(state: "CollectionState", player: int) -> bool:
+ return state.has("Propulsion Cannon Fragment", player, 2)
+
+
+def has_cyclops_shield(state: "CollectionState", player: int) -> bool:
+ return has_cyclops(state, player) and \
+ state.has("Cyclops Shield Generator", player)
+
+
+def has_ultra_high_capacity_tank(state: "CollectionState", player: int) -> bool:
+ return has_modification_station(state, player) and state.has("Ultra High Capacity Tank", player)
+
+
+def has_lightweight_high_capacity_tank(state: "CollectionState", player: int) -> bool:
+ return has_modification_station(state, player) and state.has("Lightweight High Capacity Tank", player)
+
+
+def has_ultra_glide_fins(state: "CollectionState", player: int) -> bool:
+ return has_modification_station(state, player) and state.has("Ultra Glide Fins", player)
+
+# Swim depth rules:
+# Rebreather, high capacity tank and fins are available from the start.
+# All tests for those were done without inventory for light weight.
+# Fins and ultra Fins are better than charge fins, so we ignore charge fins.
+
+# swim speeds: https://subnautica.fandom.com/wiki/Swimming_Speed
+
+
+def get_max_swim_depth(state: "CollectionState", player: int) -> int:
+ swim_rule: SwimRule = state.multiworld.worlds[player].options.swim_rule
+ depth: int = swim_rule.base_depth
+ if swim_rule.consider_items:
+ if has_seaglide(state, player):
+ if has_ultra_high_capacity_tank(state, player):
+ depth += 350 # It's about 800m. Give some room
+ else:
+ depth += 200 # It's about 650m. Give some room
+ # seaglide and fins cannot be used together
+ elif has_ultra_glide_fins(state, player):
+ if has_ultra_high_capacity_tank(state, player):
+ depth += 150
+ elif has_lightweight_high_capacity_tank(state, player):
+ depth += 75
+ else:
+ depth += 50
+ elif has_ultra_high_capacity_tank(state, player):
+ depth += 100
+ elif has_lightweight_high_capacity_tank(state, player):
+ depth += 25
+ return depth
+
+
+def get_seamoth_max_depth(state: "CollectionState", player: int):
+ if has_seamoth(state, player):
+ if has_seamoth_depth_module_mk3(state, player):
+ return 900
+ elif has_seamoth_depth_module_mk2(state, player): # Will never be the case, 3 is craftable
+ return 500
+ elif has_seamoth_depth_module_mk1(state, player):
+ return 300
+ else:
+ return 200
+ else:
+ return 0
+
+
+def get_cyclops_max_depth(state: "CollectionState", player):
+ if has_cyclops(state, player):
+ if has_cyclops_depth_module_mk3(state, player):
+ return 1700
+ elif has_cyclops_depth_module_mk2(state, player): # Will never be the case, 3 is craftable
+ return 1300
+ elif has_cyclops_depth_module_mk1(state, player):
+ return 900
+ else:
+ return 500
+ else:
+ return 0
+
+
+def get_prawn_max_depth(state: "CollectionState", player):
+ if has_prawn(state, player):
+ if has_prawn_depth_module_mk2(state, player):
+ return 1700
+ elif has_prawn_depth_module_mk1(state, player):
+ return 1300
+ else:
+ return 900
+ else:
+ return 0
+
+
+def get_max_depth(state: "CollectionState", player: int):
+ return get_max_swim_depth(state, player) + max(
+ get_seamoth_max_depth(state, player),
+ get_cyclops_max_depth(state, player),
+ get_prawn_max_depth(state, player)
+ )
+
+
+def is_radiated(x: float, y: float, z: float) -> bool:
+ aurora_dist = math.sqrt((x - 1038.0) ** 2 + y ** 2 + (z - -163.1) ** 2)
+ return aurora_dist < 950
+
+
+def can_access_location(state: "CollectionState", player: int, loc: LocationDict) -> bool:
+ need_laser_cutter = loc.get("need_laser_cutter", False)
+ if need_laser_cutter and not has_laser_cutter(state, player):
+ return False
+
+ need_propulsion_cannon = loc.get("need_propulsion_cannon", False)
+ if need_propulsion_cannon and not has_propulsion_cannon(state, player):
+ return False
+
+ pos = loc["position"]
+ pos_x = pos["x"]
+ pos_y = pos["y"]
+ pos_z = pos["z"]
+
+ need_radiation_suit = is_radiated(pos_x, pos_y, pos_z)
+ if need_radiation_suit and not state.has("Radiation Suit", player):
+ return False
+
+ # Seaglide doesn't unlock anything specific, but just allows for faster movement.
+ # Otherwise the game is painfully slow.
+ map_center_dist = math.sqrt(pos_x ** 2 + pos_z ** 2)
+ if (map_center_dist > 800 or pos_y < -200) and not has_seaglide(state, player):
+ return False
+
+ depth = -pos_y # y-up
+ return get_max_depth(state, player) >= depth
+
+
+def set_location_rule(world, player: int, loc: LocationDict):
+ set_rule(world.get_location(loc["name"], player), lambda state: can_access_location(state, player, loc))
+
+
+def can_scan_creature(state: "CollectionState", player: int, creature: str) -> bool:
+ if not has_seaglide(state, player):
+ return False
+ return get_max_depth(state, player) >= all_creatures[creature]
+
+
+def set_creature_rule(world, player: int, creature_name: str) -> "Location":
+ location = world.get_location(creature_name + suffix, player)
+ set_rule(location,
+ lambda state: can_scan_creature(state, player, creature_name))
+ return location
+
+
+def get_aggression_rule(option: AggressiveScanLogic, creature_name: str) -> \
+ Optional[Callable[["CollectionState", int], bool]]:
+ """Get logic rule for a creature scan location."""
+ if creature_name not in hatchable and option != option.option_none: # can only be done via stasis
+ return has_stasis_rifle
+ # otherwise allow option preference
+ return aggression_rules.get(option.value, None)
+
+
+aggression_rules: Dict[int, Callable[["CollectionState", int], bool]] = {
+ AggressiveScanLogic.option_stasis: has_stasis_rifle,
+ AggressiveScanLogic.option_containment: has_containment,
+ AggressiveScanLogic.option_either: lambda state, player:
+ has_stasis_rifle(state, player) or has_containment(state, player)
+}
+
+
+def set_rules(subnautica_world: "SubnauticaWorld"):
+ player = subnautica_world.player
+ multiworld = subnautica_world.multiworld
+
+ for loc in location_table.values():
+ set_location_rule(multiworld, player, loc)
+
+ if subnautica_world.creatures_to_scan:
+ option = multiworld.worlds[player].options.creature_scan_logic
+
+ for creature_name in subnautica_world.creatures_to_scan:
+ location = set_creature_rule(multiworld, player, creature_name)
+ if creature_name in containment: # there is no other way, hard-required containment
+ add_rule(location, lambda state: has_containment(state, player))
+ elif creature_name in aggressive:
+ rule = get_aggression_rule(option, creature_name)
+ if rule:
+ add_rule(location,
+ lambda state, loc_rule=get_aggression_rule(option, creature_name): loc_rule(state, player))
+
+ # Victory locations
+ set_rule(multiworld.get_location("Neptune Launch", player),
+ lambda state:
+ get_max_depth(state, player) >= 1444 and
+ has_mobile_vehicle_bay(state, player) and
+ state.has("Neptune Launch Platform", player) and
+ state.has("Neptune Gantry", player) and
+ state.has("Neptune Boosters", player) and
+ state.has("Neptune Fuel Reserve", player) and
+ state.has("Neptune Cockpit", player) and
+ state.has("Ion Power Cell", player) and
+ state.has("Ion Battery", player) and
+ has_cyclops_shield(state, player))
+
+ set_rule(multiworld.get_location("Disable Quarantine", player),
+ lambda state: get_max_depth(state, player) >= 1444)
+
+ set_rule(multiworld.get_location("Full Infection", player),
+ lambda state: get_max_depth(state, player) >= 900)
+
+ room = multiworld.get_location("Aurora Drive Room - Upgrade Console", player)
+ set_rule(multiworld.get_location("Repair Aurora Drive", player),
+ lambda state: room.can_reach(state))
+
+ multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
diff --git a/worlds/subnautica/test/__init__.py b/worlds/subnautica/test/__init__.py
index d938830737dc..c0e0a593a9f5 100644
--- a/worlds/subnautica/test/__init__.py
+++ b/worlds/subnautica/test/__init__.py
@@ -15,11 +15,11 @@ def testIDRange(self):
self.assertGreater(self.scancutoff, id)
def testGroupAssociation(self):
- from worlds.subnautica import Items
- for item_id, item_data in Items.item_table.items():
- if item_data.type == Items.ItemType.group:
+ from .. import items
+ for item_id, item_data in items.item_table.items():
+ if item_data.type == items.ItemType.group:
with self.subTest(item=item_data.name):
- self.assertIn(item_id, Items.group_items)
- for item_id in Items.group_items:
+ self.assertIn(item_id, items.group_items)
+ for item_id in items.group_items:
with self.subTest(item_id=item_id):
- self.assertEqual(Items.item_table[item_id].type, Items.ItemType.group)
+ self.assertEqual(items.item_table[item_id].type, items.ItemType.group)
diff --git a/worlds/terraria/Checks.py b/worlds/terraria/Checks.py
index b6be45258c5d..0630d6290be0 100644
--- a/worlds/terraria/Checks.py
+++ b/worlds/terraria/Checks.py
@@ -177,6 +177,7 @@ def validate_conditions(
if condition not in {
"npc",
"calamity",
+ "grindy",
"pickaxe",
"hammer",
"mech_boss",
@@ -221,62 +222,60 @@ def mark_progression(
mark_progression(conditions, progression, rules, rule_indices, loc_to_item)
-def read_data() -> (
- Tuple[
- # Goal to rule index that ends that goal's range and the locations required
- List[Tuple[int, Set[str]]],
- # Rules
- List[
- Tuple[
- # Rule
- str,
- # Flag to flag arg
- Dict[str, Union[str, int, None]],
- # True = or, False = and, None = N/A
- Union[bool, None],
- # Conditions
- List[
- Tuple[
- # True = positive, False = negative
- bool,
- # Condition type
- int,
- # Condition name or list (True = or, False = and, None = N/A) (list shares type with outer)
- Union[str, Tuple[Union[bool, None], List]],
- # Condition arg
- Union[str, int, None],
- ]
- ],
- ]
- ],
- # Rule to rule index
- Dict[str, int],
- # Label to rewards
- Dict[str, List[str]],
- # Reward to flags
- Dict[str, Set[str]],
- # Item name to ID
- Dict[str, int],
- # Location name to ID
- Dict[str, int],
- # NPCs
- List[str],
- # Pickaxe to pick power
- Dict[str, int],
- # Hammer to hammer power
- Dict[str, int],
- # Mechanical bosses
- List[str],
- # Calamity final bosses
- List[str],
- # Progression rules
- Set[str],
- # Armor to minion count,
- Dict[str, int],
- # Accessory to minion count,
- Dict[str, int],
- ]
-):
+def read_data() -> Tuple[
+ # Goal to rule index that ends that goal's range and the locations required
+ List[Tuple[int, Set[str]]],
+ # Rules
+ List[
+ Tuple[
+ # Rule
+ str,
+ # Flag to flag arg
+ Dict[str, Union[str, int, None]],
+ # True = or, False = and, None = N/A
+ Union[bool, None],
+ # Conditions
+ List[
+ Tuple[
+ # True = positive, False = negative
+ bool,
+ # Condition type
+ int,
+ # Condition name or list (True = or, False = and, None = N/A) (list shares type with outer)
+ Union[str, Tuple[Union[bool, None], List]],
+ # Condition arg
+ Union[str, int, None],
+ ]
+ ],
+ ]
+ ],
+ # Rule to rule index
+ Dict[str, int],
+ # Label to rewards
+ Dict[str, List[str]],
+ # Reward to flags
+ Dict[str, Set[str]],
+ # Item name to ID
+ Dict[str, int],
+ # Location name to ID
+ Dict[str, int],
+ # NPCs
+ List[str],
+ # Pickaxe to pick power
+ Dict[str, int],
+ # Hammer to hammer power
+ Dict[str, int],
+ # Mechanical bosses
+ List[str],
+ # Calamity final bosses
+ List[str],
+ # Progression rules
+ Set[str],
+ # Armor to minion count,
+ Dict[str, int],
+ # Accessory to minion count,
+ Dict[str, int],
+]:
next_id = 0x7E0000
item_name_to_id = {}
diff --git a/worlds/terraria/Options.py b/worlds/terraria/Options.py
index 1f9ba69afeb9..4c4b96056c5f 100644
--- a/worlds/terraria/Options.py
+++ b/worlds/terraria/Options.py
@@ -1,5 +1,5 @@
-from Options import Choice, Option, Toggle, DeathLink
-import typing
+from dataclasses import dataclass
+from Options import Choice, DeathLink, PerGameCommonOptions
class Goal(Choice):
@@ -49,9 +49,9 @@ class FillExtraChecksWith(Choice):
default = 1
-options: typing.Dict[str, type(Option)] = { # type: ignore
- "goal": Goal,
- "achievements": Achievements,
- "fill_extra_checks_with": FillExtraChecksWith,
- "death_link": DeathLink,
-}
+@dataclass
+class TerrariaOptions(PerGameCommonOptions):
+ goal: Goal
+ achievements: Achievements
+ fill_extra_checks_with: FillExtraChecksWith
+ death_link: DeathLink
diff --git a/worlds/terraria/Rules.dsv b/worlds/terraria/Rules.dsv
index 5f5551e465e1..322bf9c5d3a3 100644
--- a/worlds/terraria/Rules.dsv
+++ b/worlds/terraria/Rules.dsv
@@ -207,7 +207,7 @@ Clothier; Npc;
Dungeon; ; Skeletron;
Dungeon Heist; Achievement; Dungeon;
Bone; ; Dungeon | (@calamity & #Skeletron);
-Bewitching Table; Minions(1); Dungeon;
+Bewitching Table; Minions(1); Dungeon | (Witch Doctor & Wizard);
Mechanic; ; Dungeon;
Wire; ; Mechanic;
Decryption Computer; Calamity; Mysterious Circuitry & Dubious Plating & Wire;
@@ -234,9 +234,9 @@ Spider Armor; ArmorMinions(3);
Cross Necklace; ; Wall of Flesh;
Altar; ; Wall of Flesh & @hammer(80);
Begone, Evil!; Achievement; Altar;
-Cobalt Ore; ; ((~@calamity & Altar) | (@calamity & Wall of Flesh)) & @pickaxe(100);
+Cobalt Ore; ; (((~@calamity & Altar) | (@calamity & Wall of Flesh)) & @pickaxe(100)) | Wall of Flesh;
Extra Shiny!; Achievement; Cobalt Ore | Mythril Ore | Adamantite Ore | Chlorophyte Ore;
-Cobalt Bar; ; Cobalt Ore;
+Cobalt Bar; ; Cobalt Ore | Wall of Flesh;
Cobalt Pickaxe; Pickaxe(110); Cobalt Bar;
Soul of Night; ; Wall of Flesh | (@calamity & Altar);
Hallow; ; Wall of Flesh;
@@ -249,7 +249,7 @@ Blessed Apple; ;
Rod of Discord; ; Hallow;
Gelatin World Tour; Achievement | Grindy; Dungeon & Wall of Flesh & Hallow & #King Slime;
Soul of Flight; ; Wall of Flesh;
-Head in the Clouds; Achievement; (Soul of Flight & ((Hardmode Anvil & (Soul of Light | Soul of Night | Pixie Dust | Wall of Flesh | Solar Eclipse | @mech_boss(1) | Plantera | Spectre Bar | #Golem)) | (Shroomite Bar & Autohammer) | #Mourning Wood | #Pumpking)) | Steampunker | (Wall of Flesh & Witch Doctor) | (Solar Eclipse & Plantera) | #Everscream | #Old One's Army Tier 3 | #Empress of Light | #Duke Fishron | (Fragment & Luminite Bar & Ancient Manipulator); // Leaf Wings are Post-Plantera in 1.4.4
+Head in the Clouds; Achievement; @grindy | (Soul of Flight & ((Hardmode Anvil & (Soul of Light | Soul of Night | Pixie Dust | Wall of Flesh | Solar Eclipse | @mech_boss(1) | Plantera | Spectre Bar | #Golem)) | (Shroomite Bar & Autohammer) | #Mourning Wood | #Pumpking)) | Steampunker | (Wall of Flesh & Witch Doctor) | (Solar Eclipse & Plantera) | #Everscream | #Old One's Army Tier 3 | #Empress of Light | #Duke Fishron | (Fragment & Luminite Bar & Ancient Manipulator); // Leaf Wings are Post-Plantera in 1.4.4
Bunny; Npc; Zoologist & Wall of Flesh; // Extremely simplified
Forbidden Fragment; ; Sandstorm & Wall of Flesh;
Astral Infection; Calamity; Wall of Flesh;
@@ -274,13 +274,13 @@ Pirate; Npc;
Queen Slime; Location | Item; Hallow;
// Aquatic Scourge
-Mythril Ore; ; ((~@calamity & Altar) | (@calamity & @mech_boss(1))) & @pickaxe(110);
-Mythril Bar; ; Mythril Ore;
+Mythril Ore; ; (((~@calamity & Altar) | (@calamity & @mech_boss(1))) & @pickaxe(110)) | (Wall of Flesh & (~@calamity | @mech_boss(1)));
+Mythril Bar; ; Mythril Ore | (Wall of Flesh & (~@calamity | @mech_boss(1)));
Hardmode Anvil; ; Mythril Bar;
Mythril Pickaxe; Pickaxe(150); Hardmode Anvil & Mythril Bar;
-Adamantite Ore; ; ((~@calamity & Altar) | (@calamity & @mech_boss(2))) & @pickaxe(150);
+Adamantite Ore; ; (((~@calamity & Altar) | (@calamity & @mech_boss(2))) & @pickaxe(150)) | (Wall of Flesh & (~@calamity | @mech_boss(2)));
Hardmode Forge; ; Hardmode Anvil & Adamantite Ore & Hellforge;
-Adamantite Bar; ; Hardmode Forge & Adamantite Ore;
+Adamantite Bar; ; (Hardmode Forge & Adamantite Ore) | (Wall of Flesh & (~@calamity | @mech_boss(2)));
Adamantite Pickaxe; Pickaxe(180); Hardmode Anvil & Adamantite Bar;
Forbidden Armor; ArmorMinions(2); Hardmode Anvil & Adamantite Bar & Forbidden Fragment;
Aquatic Scourge; Calamity | Location | Item;
@@ -305,7 +305,7 @@ Hydraulic Volt Crusher; Calamity;
Life Fruit; ; (@mech_boss(1) & Wall of Flesh) | (@calamity & (Living Shard | Wall of Flesh));
Get a Life; Achievement; Life Fruit;
Topped Off; Achievement; Life Fruit;
-Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & (@mech_boss(1) | #Old One's Army Tier 3);
+Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & ((Wall of Flesh & @mech_boss(1)) | #Old One's Army Tier 3);
// Brimstone Elemental
Infernal Suevite; Calamity; @pickaxe(150) | Brimstone Elemental;
@@ -385,7 +385,7 @@ Armored Digger; Calamity | Location | Item;
Temple Raider; Achievement; #Plantera;
Lihzahrd Temple; ; #Plantera | (Plantera & Actuator) | @pickaxe(210) | (@calamity & Hardmode Anvil & Soul of Light & Soul of Night);
Solar Eclipse; ; Lihzahrd Temple & Wall of Flesh;
-Broken Hero Sword; ; (Solar Eclipse & Plantera) | (@calamity & #Calamitas Clone);
+Broken Hero Sword; ; (Solar Eclipse & Plantera & @mech_boss(3)) | (@calamity & #Calamitas Clone);
Terra Blade; ; Hardmode Anvil & True Night's Edge & True Excalibur & Broken Hero Sword & (~@calamity | Living Shard);
Sword of the Hero; Achievement; Terra Blade;
Kill the Sun; Achievement; Solar Eclipse;
@@ -410,7 +410,7 @@ Scoria Bar; Calamity;
Seismic Hampick; Calamity | Pickaxe(210) | Hammer(95); Hardmode Anvil & Scoria Bar;
Life Alloy; Calamity; (Hardmode Anvil & Cryonic Bar & Perennial Bar & Scoria Bar) | Necromantic Geode;
Advanced Display; Calamity; Hardmode Anvil & Mysterious Circuitry & Dubious Plating & Life Alloy & Long Ranged Sensor Array;
-Old One's Army Tier 3; Location | Item; #Old One's Army Tier 1 & Golem;
+Old One's Army Tier 3; Location | Item; #Old One's Army Tier 1 & Wall of Flesh & Golem;
// Martian Madness
Martian Madness; Location | Item; Wall of Flesh & Golem;
diff --git a/worlds/terraria/__init__.py b/worlds/terraria/__init__.py
index a8c823bcb8ac..abc10a7bb37c 100644
--- a/worlds/terraria/__init__.py
+++ b/worlds/terraria/__init__.py
@@ -25,7 +25,7 @@
armor_minions,
accessory_minions,
)
-from .Options import options
+from .Options import TerrariaOptions
class TerrariaWeb(WebWorld):
@@ -49,12 +49,8 @@ class TerrariaWorld(World):
game = "Terraria"
web = TerrariaWeb()
- option_definitions = options
-
- # data_version is used to signal that items, locations or their names
- # changed. Set this to 0 during development so other games' clients do not
- # cache any texts, then increase by 1 for each release that makes changes.
- data_version = 2
+ options_dataclass = TerrariaOptions
+ options: TerrariaOptions
item_name_to_id = item_name_to_id
location_name_to_id = location_name_to_id
@@ -70,7 +66,7 @@ class TerrariaWorld(World):
goal_locations: Set[str]
def generate_early(self) -> None:
- goal, goal_locations = goals[self.multiworld.goal[self.player].value]
+ goal, goal_locations = goals[self.options.goal.value]
ter_goals = {}
goal_items = set()
for location in goal_locations:
@@ -79,7 +75,7 @@ def generate_early(self) -> None:
ter_goals[item] = location
goal_items.add(item)
- achievements = self.multiworld.achievements[self.player].value
+ achievements = self.options.achievements.value
location_count = 0
locations = []
for rule, flags, _, _ in rules[:goal]:
@@ -89,7 +85,7 @@ def generate_early(self) -> None:
or (achievements < 2 and "Grindy" in flags)
or (achievements < 3 and "Fishing" in flags)
or (
- rule == "Zenith" and self.multiworld.goal[self.player].value != 11
+ rule == "Zenith" and self.options.goal.value != 11
) # Bad hardcoding
):
continue
@@ -123,7 +119,7 @@ def generate_early(self) -> None:
# Event
items.append(rule)
- extra_checks = self.multiworld.fill_extra_checks_with[self.player].value
+ extra_checks = self.options.fill_extra_checks_with.value
ordered_rewards = [
reward
for reward in labels["ordered"]
@@ -240,6 +236,8 @@ def check_condition(
return not sign
elif condition == "calamity":
return sign == self.calamity
+ elif condition == "grindy":
+ return sign == (self.options.achievements.value >= 2)
elif condition == "pickaxe":
if type(arg) is not int:
raise Exception("@pickaxe requires an integer argument")
@@ -338,7 +336,6 @@ def check(state: CollectionState, location=location):
def fill_slot_data(self) -> Dict[str, object]:
return {
"goal": list(self.goal_locations),
- "achievements": self.multiworld.achievements[self.player].value,
- "fill_extra_checks_with": self.multiworld.fill_extra_checks_with[self.player].value,
- "deathlink": bool(self.multiworld.death_link[self.player]),
+ "achievements": self.options.achievements.value,
+ "deathlink": bool(self.options.death_link),
}
diff --git a/worlds/terraria/docs/en_Terraria.md b/worlds/terraria/docs/en_Terraria.md
index b0a8529ba76b..26428f75789d 100644
--- a/worlds/terraria/docs/en_Terraria.md
+++ b/worlds/terraria/docs/en_Terraria.md
@@ -1,14 +1,14 @@
# Terraria
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Boss/event flags are randomized. So, defeating Empress of Light could give you Post-Skeletron, which allows you to enter
-the Dungeon, for example. In your player settings, you may also add item rewards and achievements to the pool.
+the Dungeon, for example. In your player options, you may also add item rewards and achievements to the pool.
## What Terraria items can appear in other players' worlds?
diff --git a/worlds/terraria/docs/setup_en.md b/worlds/terraria/docs/setup_en.md
index d0833b748452..55a4df1df30d 100644
--- a/worlds/terraria/docs/setup_en.md
+++ b/worlds/terraria/docs/setup_en.md
@@ -3,11 +3,23 @@
## Required Software
Download and install [Terraria](https://store.steampowered.com/app/105600/Terraria/)
-and [TModLoader](https://store.steampowered.com/app/1281930/tModLoader/) on Steam
+and [tModLoader](https://store.steampowered.com/app/1281930/tModLoader/) on Steam
## Installing the Archipelago Mod
-Subscribe to [the mod](https://steamcommunity.com/sharedfiles/filedetails/?id=2922217554) on Steam.
+1. Subscribe to [the mod](https://steamcommunity.com/sharedfiles/filedetails/?id=2922217554) on Steam
+2. Open tModLoader
+3. Go to **Workshop -> Manage Mods** and enable the Archipelago mod
+ - If tModLoader states that you need version 1.4.3, follow the following steps
+ 1. Close tModLoader
+ 2. Right-Click tModLoader in Steam and select **Properties**
+ 3. Navigate to **Betas -> Beta Participation**
+ 4. Select **1.4.3-legacy - Legacy - Stable tModLoader for Terraria 1.4.3**
+ 5. Update tModLoader through Steam
+ 6. Open tModLoader and navigate back to the **Manage Mods** menu
+4. tModLoader will say that it needs to refresh; exit this menu, and it will do this automatically
+5. Once tModLoader finishes loading, the Archipelago mod is finished installing; you can now
+[connect to an Archipelago game](#joining-an-archipelago-game-in-terraria).
This mod might not work with mods that significantly alter progression or vanilla features. It is
highly recommended to use utility mods and features to speed up gameplay, such as:
@@ -16,35 +28,35 @@ highly recommended to use utility mods and features to speed up gameplay, such a
- Ore Excavator
- Magic Storage
- Alchemist NPC Lite
- - (May be used to break progression)
+ - (Can be used to break progression)
- Reduced Grinding
- Upgraded Research
+ - (WARNING: Do not use without Journey mode)
+ - (NOTE: If items you pick up aren't showing up in your inventory, check your research menu. This mod automatically researches certain items.)
## Configuring your YAML File
### What is a YAML and why do I need one?
-You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here
-on the Archipelago website to learn about why Archipelago uses YAML files and what they're for.
+The [basic multiworld setup guide](/tutorial/Archipelago/setup/en) can be found on Archipelago's website. Among other things, it explains what .yaml
+files are, and how they are used.
### Where do I get a YAML?
-You can use the [game settings page for Terraria](/games/Terraria/player-settings) here
+You can use the [game options page for Terraria](/games/Terraria/player-options) here
on the Archipelago website to generate a YAML using a graphical interface.
## Joining an Archipelago Game in Terraria
-1. Launch TModLoader
-2. In Workshop > Manage Mods, edit Archipelago Randomizer's settings
- - "Name" should be the player name you set when creating your YAML file
- - "Port" should be the port number associated with the Archipelago server. It will be a 4 or 5
- digit number.
- - If you're not hosting your game on the Archipelago website, change "Address" to the server's
- URL or IP address
-3. Create a new character and world as normal (or use an existing one if you prefer). Terraria is
-usually significantly more difficult with this mod, so it is recommended to choose a lower
-difficulty than you normally would.
-4. Open the world in single player or multiplayer
+1. Launch tModLoader
+2. In **Workshop > Manage Mods**, edit Archipelago Randomizer's settings
+ - **Name** should be the player name you set when creating your YAML file.
+ - **Port** should be the port number associated with the Archipelago server. It will be a 4 or 5-digit number.
+ - If you're not hosting your game on the Archipelago website, change **Address** to the server's URL or IP address
+3. Create a new character and world as normal (or use an existing one if you prefer). Terraria usually becomes
+significantly more difficult with this mod, so it is recommended to choose a lower difficulty than you normally would
+play on.
+4. Open the world in single player or multiplayer.
5. When you're ready, open chat, and enter `/apstart` to start the game.
## Commands
diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py
index 70c76b863842..86839f0f2167 100644
--- a/worlds/timespinner/Locations.py
+++ b/worlds/timespinner/Locations.py
@@ -1,6 +1,6 @@
-from typing import List, Tuple, Optional, Callable, NamedTuple
-from BaseClasses import MultiWorld, CollectionState
-from .Options import is_option_enabled
+from typing import List, Optional, Callable, NamedTuple
+from BaseClasses import CollectionState
+from .Options import TimespinnerOptions
from .PreCalculatedWeights import PreCalculatedWeights
from .LogicExtensions import TimespinnerLogic
@@ -11,14 +11,13 @@ class LocationData(NamedTuple):
region: str
name: str
code: Optional[int]
- rule: Callable[[CollectionState], bool] = lambda state: True
+ rule: Optional[Callable[[CollectionState], bool]] = None
-def get_location_datas(world: Optional[MultiWorld], player: Optional[int],
- precalculated_weights: PreCalculatedWeights) -> Tuple[LocationData, ...]:
-
- flooded: PreCalculatedWeights = precalculated_weights
- logic = TimespinnerLogic(world, player, precalculated_weights)
+def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptions],
+ precalculated_weights: Optional[PreCalculatedWeights]) -> List[LocationData]:
+ flooded: Optional[PreCalculatedWeights] = precalculated_weights
+ logic = TimespinnerLogic(player, options, precalculated_weights)
# 1337000 - 1337155 Generic locations
# 1337171 - 1337175 New Pickup checks
@@ -88,9 +87,9 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int],
LocationData('Military Fortress (hangar)', 'Military Fortress: Soldiers bridge', 1337060),
LocationData('Military Fortress (hangar)', 'Military Fortress: Giantess room', 1337061),
LocationData('Military Fortress (hangar)', 'Military Fortress: Giantess bridge', 1337062),
- LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 2', 1337063, lambda state: logic.has_doublejump(state) and logic.has_keycard_B(state)),
- LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 1', 1337064, lambda state: logic.has_doublejump(state) and logic.has_keycard_B(state)),
- LocationData('Military Fortress (hangar)', 'Military Fortress: Pedestal', 1337065, lambda state: logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state)),
+ LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 2', 1337063, lambda state: logic.has_keycard_B(state) and (state.has('Water Mask', player) if flooded.flood_lab else logic.has_doublejump(state))),
+ LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 1', 1337064, lambda state: logic.has_keycard_B(state) and (state.has('Water Mask', player) if flooded.flood_lab else logic.has_doublejump(state))),
+ LocationData('Military Fortress (hangar)', 'Military Fortress: Pedestal', 1337065, lambda state: state.has('Water Mask', player) if flooded.flood_lab else (logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state))),
LocationData('The lab', 'Lab: Coffee break', 1337066),
LocationData('The lab', 'Lab: Lower trash right', 1337067, logic.has_doublejump),
LocationData('The lab', 'Lab: Lower trash left', 1337068, logic.has_upwarddash),
@@ -139,17 +138,17 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int],
LocationData('Lower Lake Serene', 'Lake Serene (Lower): Under the eels', 1337106),
LocationData('Lower Lake Serene', 'Lake Serene (Lower): Water spikes room', 1337107),
LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater secret', 1337108, logic.can_break_walls),
- LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109, lambda state: not flooded.dry_lake_serene or logic.has_doublejump_of_npc(state)),
+ LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109, lambda state: flooded.flood_lake_serene or logic.has_doublejump_of_npc(state)),
LocationData('Lower Lake Serene', 'Lake Serene (Lower): Past the eels', 1337110),
- LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111, lambda state: not flooded.dry_lake_serene or logic.has_doublejump(state)),
- LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: not flooded.flood_maw or logic.has_doublejump(state)),
+ LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111, lambda state: flooded.flood_lake_serene or logic.has_doublejump(state)),
+ LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: flooded.flood_maw or logic.has_doublejump(state)),
LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Secret room', 1337113, lambda state: logic.can_break_walls(state) and (not flooded.flood_maw or state.has('Water Mask', player))),
LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Bottom left room', 1337114, lambda state: not flooded.flood_maw or state.has('Water Mask', player)),
LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Single shroom room', 1337115),
- LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 1', 1337116, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw),
- LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 2', 1337117, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw),
- LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 3', 1337118, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw),
- LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 4', 1337119, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw),
+ LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 1', 1337116, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)),
+ LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 2', 1337117, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)),
+ LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 3', 1337118, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)),
+ LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 4', 1337119, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)),
LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Pedestal', 1337120, lambda state: not flooded.flood_maw or state.has('Water Mask', player)),
LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Last chance before Maw', 1337121, lambda state: state.has('Water Mask', player) if flooded.flood_maw else logic.has_doublejump(state)),
LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Plasma Crystal', 1337173, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (not flooded.flood_maw or state.has('Water Mask', player))),
@@ -197,13 +196,13 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int],
LocationData('Ancient Pyramid (entrance)', 'Ancient Pyramid: Why not it\'s right there', 1337246),
LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Conviction guarded room', 1337247),
LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Pit secret room', 1337248, lambda state: logic.can_break_walls(state) and (not flooded.flood_pyramid_shaft or state.has('Water Mask', player))),
- LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Regret chest', 1337249, lambda state: logic.can_break_walls(state) and (not flooded.flood_pyramid_shaft or state.has('Water Mask', player))),
+ LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Regret chest', 1337249, lambda state: logic.can_break_walls(state) and (state.has('Water Mask', player) if flooded.flood_pyramid_shaft else logic.has_doublejump(state))),
LocationData('Ancient Pyramid (right)', 'Ancient Pyramid: Nightmare Door chest', 1337236, lambda state: not flooded.flood_pyramid_back or state.has('Water Mask', player)),
LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId, lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player) and (not flooded.flood_pyramid_back or state.has('Water Mask', player)))
]
# 1337156 - 1337170 Downloads
- if not world or is_option_enabled(world, player, "DownloadableItems"):
+ if not options or options.downloadable_items:
location_table += (
LocationData('Library', 'Library: Terminal 2 (Lachiem)', 1337156, lambda state: state.has('Tablet', player)),
LocationData('Library', 'Library: Terminal 1 (Windaria)', 1337157, lambda state: state.has('Tablet', player)),
@@ -223,13 +222,13 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int],
)
# 1337176 - 1337176 Cantoran
- if not world or is_option_enabled(world, player, "Cantoran"):
+ if not options or options.cantoran:
location_table += (
LocationData('Left Side forest Caves', 'Lake Serene: Cantoran', 1337176),
)
# 1337177 - 1337198 Lore Checks
- if not world or is_option_enabled(world, player, "LoreChecks"):
+ if not options or options.lore_checks:
location_table += (
LocationData('Lower lake desolation', 'Lake Desolation: Memory - Coyote Jump (Time Messenger)', 1337177),
LocationData('Library', 'Library: Memory - Waterway (A Message)', 1337178),
@@ -258,7 +257,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int],
# 1337199 - 1337236 Reserved for future use
# 1337237 - 1337245 GyreArchives
- if not world or is_option_enabled(world, player, "GyreArchives"):
+ if not options or options.gyre_archives:
location_table += (
LocationData('Ravenlord\'s Lair', 'Ravenlord: Post fight (pedestal)', 1337237),
LocationData('Ifrit\'s Lair', 'Ifrit: Post fight (pedestal)', 1337238),
@@ -271,4 +270,4 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int],
LocationData('Ifrit\'s Lair', 'Ifrit: Post fight (chest)', 1337245),
)
- return tuple(location_table)
+ return location_table
diff --git a/worlds/timespinner/LogicExtensions.py b/worlds/timespinner/LogicExtensions.py
index d316a936b02f..6c9cb3f684a0 100644
--- a/worlds/timespinner/LogicExtensions.py
+++ b/worlds/timespinner/LogicExtensions.py
@@ -1,6 +1,6 @@
-from typing import Union
-from BaseClasses import MultiWorld, CollectionState
-from .Options import is_option_enabled
+from typing import Union, Optional
+from BaseClasses import CollectionState
+from .Options import TimespinnerOptions
from .PreCalculatedWeights import PreCalculatedWeights
@@ -10,17 +10,18 @@ class TimespinnerLogic:
flag_unchained_keys: bool
flag_eye_spy: bool
flag_specific_keycards: bool
- pyramid_keys_unlock: Union[str, None]
- present_keys_unlock: Union[str, None]
- past_keys_unlock: Union[str, None]
- time_keys_unlock: Union[str, None]
+ pyramid_keys_unlock: Optional[str]
+ present_keys_unlock: Optional[str]
+ past_keys_unlock: Optional[str]
+ time_keys_unlock: Optional[str]
- def __init__(self, world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights):
+ def __init__(self, player: int, options: Optional[TimespinnerOptions],
+ precalculated_weights: Optional[PreCalculatedWeights]):
self.player = player
- self.flag_specific_keycards = is_option_enabled(world, player, "SpecificKeycards")
- self.flag_eye_spy = is_option_enabled(world, player, "EyeSpy")
- self.flag_unchained_keys = is_option_enabled(world, player, "UnchainedKeys")
+ self.flag_specific_keycards = bool(options and options.specific_keycards)
+ self.flag_eye_spy = bool(options and options.eye_spy)
+ self.flag_unchained_keys = bool(options and options.unchained_keys)
if precalculated_weights:
if self.flag_unchained_keys:
diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py
index 8b111849442c..20ad8132c45f 100644
--- a/worlds/timespinner/Options.py
+++ b/worlds/timespinner/Options.py
@@ -1,68 +1,66 @@
-from typing import Dict, Union, List
-from BaseClasses import MultiWorld
-from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict, OptionList
+from dataclasses import dataclass
+from typing import Type, Any
+from typing import Dict
+from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, OptionDict, OptionList, Visibility, Option
+from Options import PerGameCommonOptions, DeathLinkMixin, AssembleOptions
from schema import Schema, And, Optional, Or
-
class StartWithJewelryBox(Toggle):
"Start with Jewelry Box unlocked"
display_name = "Start with Jewelry Box"
-
class DownloadableItems(DefaultOnToggle):
"With the tablet you will be able to download items at terminals"
display_name = "Downloadable items"
-
class EyeSpy(Toggle):
"Requires Oculus Ring in inventory to be able to break hidden walls."
display_name = "Eye Spy"
-
class StartWithMeyef(Toggle):
"Start with Meyef, ideal for when you want to play multiplayer."
display_name = "Start with Meyef"
-
class QuickSeed(Toggle):
"Start with Talaria Attachment, Nyoom!"
display_name = "Quick seed"
-
class SpecificKeycards(Toggle):
"Keycards can only open corresponding doors"
display_name = "Specific Keycards"
-
class Inverted(Toggle):
"Start in the past"
display_name = "Inverted"
-
class GyreArchives(Toggle):
"Gyre locations are in logic. New warps are gated by Merchant Crow and Kobo"
display_name = "Gyre Archives"
-
class Cantoran(Toggle):
"Cantoran's fight and check are available upon revisiting his room"
display_name = "Cantoran"
-
class LoreChecks(Toggle):
"Memories and journal entries contain items."
display_name = "Lore Checks"
-
-class BossRando(Toggle):
- "Shuffles the positions of all bosses."
+class BossRando(Choice):
+ "Wheter all boss locations are shuffled, and if their damage/hp should be scaled."
display_name = "Boss Randomization"
+ option_off = 0
+ option_scaled = 1
+ option_unscaled = 2
+ alias_true = 1
-
-class BossScaling(DefaultOnToggle):
- "When Boss Rando is enabled, scales the bosses' HP, XP, and ATK to the stats of the location they replace (Recommended)"
- display_name = "Scale Random Boss Stats"
-
+class EnemyRando(Choice):
+ "Wheter enemies will be randomized, and if their damage/hp should be scaled."
+ display_name = "Enemy Randomization"
+ option_off = 0
+ option_scaled = 1
+ option_unscaled = 2
+ option_ryshia = 3
+ alias_true = 1
class DamageRando(Choice):
"Randomly nerfs and buffs some orbs and their associated spells as well as some associated rings."
@@ -76,7 +74,6 @@ class DamageRando(Choice):
option_manual = 6
alias_true = 2
-
class DamageRandoOverrides(OptionDict):
"""Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that
you don't specify will roll with 1/1/1 as odds"""
@@ -182,7 +179,6 @@ class DamageRandoOverrides(OptionDict):
"Radiant": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
}
-
class HpCap(Range):
"Sets the number that Lunais's HP maxes out at."
display_name = "HP Cap"
@@ -190,7 +186,6 @@ class HpCap(Range):
range_end = 999
default = 999
-
class LevelCap(Range):
"""Sets the max level Lunais can achieve."""
display_name = "Level Cap"
@@ -198,20 +193,17 @@ class LevelCap(Range):
range_end = 99
default = 99
-
class ExtraEarringsXP(Range):
"""Adds additional XP granted by Galaxy Earrings."""
display_name = "Extra Earrings XP"
range_start = 0
range_end = 24
default = 0
-
class BossHealing(DefaultOnToggle):
"Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled."
display_name = "Heal After Bosses"
-
class ShopFill(Choice):
"""Sets the items for sale in Merchant Crow's shops.
Default: No sunglasses or trendy jacket, but sand vials for sale.
@@ -224,12 +216,10 @@ class ShopFill(Choice):
option_vanilla = 2
option_empty = 3
-
class ShopWarpShards(DefaultOnToggle):
"Shops always sell warp shards (when keys possessed), ignoring inventory setting."
display_name = "Always Sell Warp Shards"
-
class ShopMultiplier(Range):
"Multiplier for the cost of items in the shop. Set to 0 for free shops."
display_name = "Shop Price Multiplier"
@@ -237,7 +227,6 @@ class ShopMultiplier(Range):
range_end = 10
default = 1
-
class LootPool(Choice):
"""Sets the items that drop from enemies (does not apply to boss reward checks)
Vanilla: Drops are the same as the base game
@@ -248,7 +237,6 @@ class LootPool(Choice):
option_randomized = 1
option_empty = 2
-
class DropRateCategory(Choice):
"""Sets the drop rate when 'Loot Pool' is set to 'Random'
Tiered: Based on item rarity/value
@@ -262,7 +250,6 @@ class DropRateCategory(Choice):
option_randomized = 2
option_fixed = 3
-
class FixedDropRate(Range):
"Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'"
display_name = "Fixed Drop Rate"
@@ -270,7 +257,6 @@ class FixedDropRate(Range):
range_end = 100
default = 5
-
class LootTierDistro(Choice):
"""Sets how often items of each rarity tier are placed when 'Loot Pool' is set to 'Random'
Default Weight: Rarer items will be assigned to enemy drop slots less frequently than common items
@@ -282,32 +268,26 @@ class LootTierDistro(Choice):
option_full_random = 1
option_inverted_weight = 2
-
class ShowBestiary(Toggle):
"All entries in the bestiary are visible, without needing to kill one of a given enemy first"
display_name = "Show Bestiary Entries"
-
class ShowDrops(Toggle):
"All item drops in the bestiary are visible, without needing an enemy to drop one of a given item first"
display_name = "Show Bestiary Item Drops"
-
class EnterSandman(Toggle):
"The Ancient Pyramid is unlocked by the Twin Pyramid Keys, but the final boss door opens if you have all 5 Timespinner pieces"
display_name = "Enter Sandman"
-
class DadPercent(Toggle):
"""The win condition is beating the boss of Emperor's Tower"""
display_name = "Dad Percent"
-
class RisingTides(Toggle):
"""Random areas are flooded or drained, can be further specified with RisingTidesOverrides"""
display_name = "Rising Tides"
-
def rising_tide_option(location: str, with_save_point_option: bool = False) -> Dict[Optional, Or]:
if with_save_point_option:
return {
@@ -332,10 +312,10 @@ def rising_tide_option(location: str, with_save_point_option: bool = False) -> D
"Flooded")
}
-
class RisingTidesOverrides(OptionDict):
"""Odds for specific areas to be flooded or drained, only has effect when RisingTides is on.
Areas that are not specified will roll with the default 33% chance of getting flooded or drained"""
+ display_name = "Rising Tides Overrides"
schema = Schema({
**rising_tide_option("Xarion"),
**rising_tide_option("Maw"),
@@ -345,9 +325,10 @@ class RisingTidesOverrides(OptionDict):
**rising_tide_option("CastleBasement", with_save_point_option=True),
**rising_tide_option("CastleCourtyard"),
**rising_tide_option("LakeDesolation"),
- **rising_tide_option("LakeSerene")
+ **rising_tide_option("LakeSerene"),
+ **rising_tide_option("LakeSereneBridge"),
+ **rising_tide_option("Lab"),
})
- display_name = "Rising Tides Overrides"
default = {
"Xarion": { "Dry": 67, "Flooded": 33 },
"Maw": { "Dry": 67, "Flooded": 33 },
@@ -358,15 +339,15 @@ class RisingTidesOverrides(OptionDict):
"CastleCourtyard": { "Dry": 67, "Flooded": 33 },
"LakeDesolation": { "Dry": 67, "Flooded": 33 },
"LakeSerene": { "Dry": 33, "Flooded": 67 },
+ "LakeSereneBridge": { "Dry": 67, "Flooded": 33 },
+ "Lab": { "Dry": 67, "Flooded": 33 },
}
-
class UnchainedKeys(Toggle):
"""Start with Twin Pyramid Key, which does not give free warp;
warp items for Past, Present, (and ??? with Enter Sandman) can be found."""
display_name = "Unchained Keys"
-
class TrapChance(Range):
"""Chance of traps in the item pool.
Traps will only replace filler items such as potions, vials and antidotes"""
@@ -375,61 +356,256 @@ class TrapChance(Range):
range_end = 100
default = 10
-
class Traps(OptionList):
"""List of traps that may be in the item pool to find"""
display_name = "Traps Types"
valid_keys = { "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" }
default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" ]
+class PresentAccessWithWheelAndSpindle(Toggle):
+ """When inverted, allows using the refugee camp warp when both the Timespinner Wheel and Spindle is acquired."""
+ display_name = "Back to the future"
+
+@dataclass
+class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin):
+ start_with_jewelry_box: StartWithJewelryBox
+ downloadable_items: DownloadableItems
+ eye_spy: EyeSpy
+ start_with_meyef: StartWithMeyef
+ quick_seed: QuickSeed
+ specific_keycards: SpecificKeycards
+ inverted: Inverted
+ gyre_archives: GyreArchives
+ cantoran: Cantoran
+ lore_checks: LoreChecks
+ boss_rando: BossRando
+ damage_rando: DamageRando
+ damage_rando_overrides: DamageRandoOverrides
+ hp_cap: HpCap
+ level_cap: LevelCap
+ extra_earrings_xp: ExtraEarringsXP
+ boss_healing: BossHealing
+ shop_fill: ShopFill
+ shop_warp_shards: ShopWarpShards
+ shop_multiplier: ShopMultiplier
+ loot_pool: LootPool
+ drop_rate_category: DropRateCategory
+ fixed_drop_rate: FixedDropRate
+ loot_tier_distro: LootTierDistro
+ show_bestiary: ShowBestiary
+ show_drops: ShowDrops
+ enter_sandman: EnterSandman
+ dad_percent: DadPercent
+ rising_tides: RisingTides
+ rising_tides_overrides: RisingTidesOverrides
+ unchained_keys: UnchainedKeys
+ back_to_the_future: PresentAccessWithWheelAndSpindle
+ trap_chance: TrapChance
+ traps: Traps
+
+class HiddenDamageRandoOverrides(DamageRandoOverrides):
+ """Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that
+ you don't specify will roll with 1/1/1 as odds"""
+ visibility = Visibility.none
-# Some options that are available in the timespinner randomizer arent currently implemented
-timespinner_options: Dict[str, Option] = {
- "StartWithJewelryBox": StartWithJewelryBox,
- "DownloadableItems": DownloadableItems,
- "EyeSpy": EyeSpy,
- "StartWithMeyef": StartWithMeyef,
- "QuickSeed": QuickSeed,
- "SpecificKeycards": SpecificKeycards,
- "Inverted": Inverted,
- "GyreArchives": GyreArchives,
- "Cantoran": Cantoran,
- "LoreChecks": LoreChecks,
- "BossRando": BossRando,
- "BossScaling": BossScaling,
- "DamageRando": DamageRando,
- "DamageRandoOverrides": DamageRandoOverrides,
- "HpCap": HpCap,
- "LevelCap": LevelCap,
- "ExtraEarringsXP": ExtraEarringsXP,
- "BossHealing": BossHealing,
- "ShopFill": ShopFill,
- "ShopWarpShards": ShopWarpShards,
- "ShopMultiplier": ShopMultiplier,
- "LootPool": LootPool,
- "DropRateCategory": DropRateCategory,
- "FixedDropRate": FixedDropRate,
- "LootTierDistro": LootTierDistro,
- "ShowBestiary": ShowBestiary,
- "ShowDrops": ShowDrops,
- "EnterSandman": EnterSandman,
- "DadPercent": DadPercent,
- "RisingTides": RisingTides,
- "RisingTidesOverrides": RisingTidesOverrides,
- "UnchainedKeys": UnchainedKeys,
- "TrapChance": TrapChance,
- "Traps": Traps,
- "DeathLink": DeathLink,
-}
-
-
-def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool:
- return get_option_value(world, player, name) > 0
-
-
-def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, Dict, List]:
- option = getattr(world, name, None)
- if option == None:
- return 0
-
- return option[player].value
+class HiddenRisingTidesOverrides(RisingTidesOverrides):
+ """Odds for specific areas to be flooded or drained, only has effect when RisingTides is on.
+ Areas that are not specified will roll with the default 33% chance of getting flooded or drained"""
+ visibility = Visibility.none
+
+class HiddenTraps(Traps):
+ """List of traps that may be in the item pool to find"""
+ visibility = Visibility.none
+
+class OptionsHider:
+ @classmethod
+ def hidden(cls, option: Type[Option[Any]]) -> Type[Option]:
+ new_option = AssembleOptions(f"{option}Hidden", option.__bases__, vars(option).copy())
+ new_option.visibility = Visibility.none
+ new_option.__doc__ = option.__doc__
+ return new_option
+
+class HasReplacedCamelCase(Toggle):
+ """For internal use will display a warning message if true"""
+ visibility = Visibility.none
+
+@dataclass
+class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions):
+ StartWithJewelryBox: OptionsHider.hidden(StartWithJewelryBox) # type: ignore
+ DownloadableItems: OptionsHider.hidden(DownloadableItems) # type: ignore
+ EyeSpy: OptionsHider.hidden(EyeSpy) # type: ignore
+ StartWithMeyef: OptionsHider.hidden(StartWithMeyef) # type: ignore
+ QuickSeed: OptionsHider.hidden(QuickSeed) # type: ignore
+ SpecificKeycards: OptionsHider.hidden(SpecificKeycards) # type: ignore
+ Inverted: OptionsHider.hidden(Inverted) # type: ignore
+ GyreArchives: OptionsHider.hidden(GyreArchives) # type: ignore
+ Cantoran: OptionsHider.hidden(Cantoran) # type: ignore
+ LoreChecks: OptionsHider.hidden(LoreChecks) # type: ignore
+ BossRando: OptionsHider.hidden(BossRando) # type: ignore
+ DamageRando: OptionsHider.hidden(DamageRando) # type: ignore
+ DamageRandoOverrides: HiddenDamageRandoOverrides
+ HpCap: OptionsHider.hidden(HpCap) # type: ignore
+ LevelCap: OptionsHider.hidden(LevelCap) # type: ignore
+ ExtraEarringsXP: OptionsHider.hidden(ExtraEarringsXP) # type: ignore
+ BossHealing: OptionsHider.hidden(BossHealing) # type: ignore
+ ShopFill: OptionsHider.hidden(ShopFill) # type: ignore
+ ShopWarpShards: OptionsHider.hidden(ShopWarpShards) # type: ignore
+ ShopMultiplier: OptionsHider.hidden(ShopMultiplier) # type: ignore
+ LootPool: OptionsHider.hidden(LootPool) # type: ignore
+ DropRateCategory: OptionsHider.hidden(DropRateCategory) # type: ignore
+ FixedDropRate: OptionsHider.hidden(FixedDropRate) # type: ignore
+ LootTierDistro: OptionsHider.hidden(LootTierDistro) # type: ignore
+ ShowBestiary: OptionsHider.hidden(ShowBestiary) # type: ignore
+ ShowDrops: OptionsHider.hidden(ShowDrops) # type: ignore
+ EnterSandman: OptionsHider.hidden(EnterSandman) # type: ignore
+ DadPercent: OptionsHider.hidden(DadPercent) # type: ignore
+ RisingTides: OptionsHider.hidden(RisingTides) # type: ignore
+ RisingTidesOverrides: HiddenRisingTidesOverrides
+ UnchainedKeys: OptionsHider.hidden(UnchainedKeys) # type: ignore
+ PresentAccessWithWheelAndSpindle: OptionsHider.hidden(PresentAccessWithWheelAndSpindle) # type: ignore
+ TrapChance: OptionsHider.hidden(TrapChance) # type: ignore
+ Traps: HiddenTraps # type: ignore
+ DeathLink: OptionsHider.hidden(DeathLink) # type: ignore
+ has_replaced_options: HasReplacedCamelCase
+
+ def handle_backward_compatibility(self) -> None:
+ if self.StartWithJewelryBox != StartWithJewelryBox.default and \
+ self.start_with_jewelry_box == StartWithJewelryBox.default:
+ self.start_with_jewelry_box.value = self.StartWithJewelryBox.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.DownloadableItems != DownloadableItems.default and \
+ self.downloadable_items == DownloadableItems.default:
+ self.downloadable_items.value = self.DownloadableItems.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.EyeSpy != EyeSpy.default and \
+ self.eye_spy == EyeSpy.default:
+ self.eye_spy.value = self.EyeSpy.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.StartWithMeyef != StartWithMeyef.default and \
+ self.start_with_meyef == StartWithMeyef.default:
+ self.start_with_meyef.value = self.StartWithMeyef.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.QuickSeed != QuickSeed.default and \
+ self.quick_seed == QuickSeed.default:
+ self.quick_seed.value = self.QuickSeed.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.SpecificKeycards != SpecificKeycards.default and \
+ self.specific_keycards == SpecificKeycards.default:
+ self.specific_keycards.value = self.SpecificKeycards.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.Inverted != Inverted.default and \
+ self.inverted == Inverted.default:
+ self.inverted.value = self.Inverted.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.GyreArchives != GyreArchives.default and \
+ self.gyre_archives == GyreArchives.default:
+ self.gyre_archives.value = self.GyreArchives.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.Cantoran != Cantoran.default and \
+ self.cantoran == Cantoran.default:
+ self.cantoran.value = self.Cantoran.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.LoreChecks != LoreChecks.default and \
+ self.lore_checks == LoreChecks.default:
+ self.lore_checks.value = self.LoreChecks.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.BossRando != BossRando.default and \
+ self.boss_rando == BossRando.default:
+ self.boss_rando.value = self.BossRando.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.DamageRando != DamageRando.default and \
+ self.damage_rando == DamageRando.default:
+ self.damage_rando.value = self.DamageRando.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.DamageRandoOverrides != DamageRandoOverrides.default and \
+ self.damage_rando_overrides == DamageRandoOverrides.default:
+ self.damage_rando_overrides.value = self.DamageRandoOverrides.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.HpCap != HpCap.default and \
+ self.hp_cap == HpCap.default:
+ self.hp_cap.value = self.HpCap.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.LevelCap != LevelCap.default and \
+ self.level_cap == LevelCap.default:
+ self.level_cap.value = self.LevelCap.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.ExtraEarringsXP != ExtraEarringsXP.default and \
+ self.extra_earrings_xp == ExtraEarringsXP.default:
+ self.extra_earrings_xp.value = self.ExtraEarringsXP.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.BossHealing != BossHealing.default and \
+ self.boss_healing == BossHealing.default:
+ self.boss_healing.value = self.BossHealing.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.ShopFill != ShopFill.default and \
+ self.shop_fill == ShopFill.default:
+ self.shop_fill.value = self.ShopFill.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.ShopWarpShards != ShopWarpShards.default and \
+ self.shop_warp_shards == ShopWarpShards.default:
+ self.shop_warp_shards.value = self.ShopWarpShards.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.ShopMultiplier != ShopMultiplier.default and \
+ self.shop_multiplier == ShopMultiplier.default:
+ self.shop_multiplier.value = self.ShopMultiplier.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.LootPool != LootPool.default and \
+ self.loot_pool == LootPool.default:
+ self.loot_pool.value = self.LootPool.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.DropRateCategory != DropRateCategory.default and \
+ self.drop_rate_category == DropRateCategory.default:
+ self.drop_rate_category.value = self.DropRateCategory.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.FixedDropRate != FixedDropRate.default and \
+ self.fixed_drop_rate == FixedDropRate.default:
+ self.fixed_drop_rate.value = self.FixedDropRate.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.LootTierDistro != LootTierDistro.default and \
+ self.loot_tier_distro == LootTierDistro.default:
+ self.loot_tier_distro.value = self.LootTierDistro.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.ShowBestiary != ShowBestiary.default and \
+ self.show_bestiary == ShowBestiary.default:
+ self.show_bestiary.value = self.ShowBestiary.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.ShowDrops != ShowDrops.default and \
+ self.show_drops == ShowDrops.default:
+ self.show_drops.value = self.ShowDrops.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.EnterSandman != EnterSandman.default and \
+ self.enter_sandman == EnterSandman.default:
+ self.enter_sandman.value = self.EnterSandman.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.DadPercent != DadPercent.default and \
+ self.dad_percent == DadPercent.default:
+ self.dad_percent.value = self.DadPercent.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.RisingTides != RisingTides.default and \
+ self.rising_tides == RisingTides.default:
+ self.rising_tides.value = self.RisingTides.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.RisingTidesOverrides != RisingTidesOverrides.default and \
+ self.rising_tides_overrides == RisingTidesOverrides.default:
+ self.rising_tides_overrides.value = self.RisingTidesOverrides.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.UnchainedKeys != UnchainedKeys.default and \
+ self.unchained_keys == UnchainedKeys.default:
+ self.unchained_keys.value = self.UnchainedKeys.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.PresentAccessWithWheelAndSpindle != PresentAccessWithWheelAndSpindle.default and \
+ self.back_to_the_future == PresentAccessWithWheelAndSpindle.default:
+ self.back_to_the_future.value = self.PresentAccessWithWheelAndSpindle.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.TrapChance != TrapChance.default and \
+ self.trap_chance == TrapChance.default:
+ self.trap_chance.value = self.TrapChance.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.Traps != Traps.default and \
+ self.traps == Traps.default:
+ self.traps.value = self.Traps.value
+ self.has_replaced_options.value = Toggle.option_true
+ if self.DeathLink != DeathLink.default and \
+ self.death_link == DeathLink.default:
+ self.death_link.value = self.DeathLink.value
+ self.has_replaced_options.value = Toggle.option_true
diff --git a/worlds/timespinner/PreCalculatedWeights.py b/worlds/timespinner/PreCalculatedWeights.py
index 64243e25edcc..c9d80d7a709d 100644
--- a/worlds/timespinner/PreCalculatedWeights.py
+++ b/worlds/timespinner/PreCalculatedWeights.py
@@ -1,6 +1,6 @@
-from typing import Tuple, Dict, Union
-from BaseClasses import MultiWorld
-from .Options import timespinner_options, is_option_enabled, get_option_value
+from typing import Tuple, Dict, Union, List
+from random import Random
+from .Options import TimespinnerOptions
class PreCalculatedWeights:
pyramid_keys_unlock: str
@@ -17,23 +17,26 @@ class PreCalculatedWeights:
flood_moat: bool
flood_courtyard: bool
flood_lake_desolation: bool
- dry_lake_serene: bool
+ flood_lake_serene: bool
+ flood_lake_serene_bridge: bool
+ flood_lab: bool
- def __init__(self, world: MultiWorld, player: int):
- if world and is_option_enabled(world, player, "RisingTides"):
- weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(world, player)
+ def __init__(self, options: TimespinnerOptions, random: Random):
+ if options.rising_tides:
+ weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(options)
self.flood_basement, self.flood_basement_high = \
- self.roll_flood_setting(world, player, weights_overrrides, "CastleBasement")
- self.flood_xarion, _ = self.roll_flood_setting(world, player, weights_overrrides, "Xarion")
- self.flood_maw, _ = self.roll_flood_setting(world, player, weights_overrrides, "Maw")
- self.flood_pyramid_shaft, _ = self.roll_flood_setting(world, player, weights_overrrides, "AncientPyramidShaft")
- self.flood_pyramid_back, _ = self.roll_flood_setting(world, player, weights_overrrides, "Sandman")
- self.flood_moat, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat")
- self.flood_courtyard, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard")
- self.flood_lake_desolation, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation")
- flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene")
- self.dry_lake_serene = not flood_lake_serene
+ self.roll_flood_setting(random, weights_overrrides, "CastleBasement")
+ self.flood_xarion, _ = self.roll_flood_setting(random, weights_overrrides, "Xarion")
+ self.flood_maw, _ = self.roll_flood_setting(random, weights_overrrides, "Maw")
+ self.flood_pyramid_shaft, _ = self.roll_flood_setting(random, weights_overrrides, "AncientPyramidShaft")
+ self.flood_pyramid_back, _ = self.roll_flood_setting(random, weights_overrrides, "Sandman")
+ self.flood_moat, _ = self.roll_flood_setting(random, weights_overrrides, "CastleMoat")
+ self.flood_courtyard, _ = self.roll_flood_setting(random, weights_overrrides, "CastleCourtyard")
+ self.flood_lake_desolation, _ = self.roll_flood_setting(random, weights_overrrides, "LakeDesolation")
+ self.flood_lake_serene, _ = self.roll_flood_setting(random, weights_overrrides, "LakeSerene")
+ self.flood_lake_serene_bridge, _ = self.roll_flood_setting(random, weights_overrrides, "LakeSereneBridge")
+ self.flood_lab, _ = self.roll_flood_setting(random, weights_overrrides, "Lab")
else:
self.flood_basement = False
self.flood_basement_high = False
@@ -44,30 +47,34 @@ def __init__(self, world: MultiWorld, player: int):
self.flood_moat = False
self.flood_courtyard = False
self.flood_lake_desolation = False
- self.dry_lake_serene = False
+ self.flood_lake_serene = True
+ self.flood_lake_serene_bridge = False
+ self.flood_lab = False
self.pyramid_keys_unlock, self.present_key_unlock, self.past_key_unlock, self.time_key_unlock = \
- self.get_pyramid_keys_unlocks(world, player, self.flood_maw)
+ self.get_pyramid_keys_unlocks(options, random, self.flood_maw, self.flood_xarion)
@staticmethod
- def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: bool) -> Tuple[str, str, str, str]:
- present_teleportation_gates: Tuple[str, ...] = (
+ def get_pyramid_keys_unlocks(options: TimespinnerOptions, random: Random,
+ is_maw_flooded: bool, is_xarion_flooded: bool) -> Tuple[str, str, str, str]:
+
+ present_teleportation_gates: List[str] = [
"GateKittyBoss",
"GateLeftLibrary",
"GateMilitaryGate",
"GateSealedCaves",
"GateSealedSirensCave",
"GateLakeDesolation"
- )
+ ]
- past_teleportation_gates: Tuple[str, ...] = (
+ past_teleportation_gates: List[str] = [
"GateLakeSereneRight",
"GateAccessToPast",
"GateCastleRamparts",
"GateCastleKeep",
"GateRoyalTowers",
"GateCavesOfBanishment"
- )
+ ]
ancient_pyramid_teleportation_gates: Tuple[str, ...] = (
"GateGyre",
@@ -75,35 +82,30 @@ def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: boo
"GateRightPyramid"
)
- if not world:
- return (
- present_teleportation_gates[0],
- present_teleportation_gates[0],
- past_teleportation_gates[0],
- ancient_pyramid_teleportation_gates[0]
- )
-
if not is_maw_flooded:
- past_teleportation_gates += ("GateMaw", )
+ past_teleportation_gates.append("GateMaw")
+
+ if not is_xarion_flooded:
+ present_teleportation_gates.append("GateXarion")
- if is_option_enabled(world, player, "Inverted"):
+ if options.inverted:
all_gates: Tuple[str, ...] = present_teleportation_gates
else:
all_gates: Tuple[str, ...] = past_teleportation_gates + present_teleportation_gates
return (
- world.random.choice(all_gates),
- world.random.choice(present_teleportation_gates),
- world.random.choice(past_teleportation_gates),
- world.random.choice(ancient_pyramid_teleportation_gates)
+ random.choice(all_gates),
+ random.choice(present_teleportation_gates),
+ random.choice(past_teleportation_gates),
+ random.choice(ancient_pyramid_teleportation_gates)
)
@staticmethod
- def get_flood_weights_overrides(world: MultiWorld, player: int) -> Dict[str, Union[str, Dict[str, int]]]:
+ def get_flood_weights_overrides(options: TimespinnerOptions) -> Dict[str, Union[str, Dict[str, int]]]:
weights_overrides_option: Union[int, Dict[str, Union[str, Dict[str, int]]]] = \
- get_option_value(world, player, "RisingTidesOverrides")
+ options.rising_tides_overrides.value
- default_weights: Dict[str, Dict[str, int]] = timespinner_options["RisingTidesOverrides"].default
+ default_weights: Dict[str, Dict[str, int]] = options.rising_tides_overrides.default
if not weights_overrides_option:
weights_overrides_option = default_weights
@@ -115,13 +117,13 @@ def get_flood_weights_overrides(world: MultiWorld, player: int) -> Dict[str, Uni
return weights_overrides_option
@staticmethod
- def roll_flood_setting(world: MultiWorld, player: int,
- all_weights: Dict[str, Union[Dict[str, int], str]], key: str) -> Tuple[bool, bool]:
+ def roll_flood_setting(random: Random, all_weights: Dict[str, Union[Dict[str, int], str]],
+ key: str) -> Tuple[bool, bool]:
weights: Union[Dict[str, int], str] = all_weights[key]
if isinstance(weights, dict):
- result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0]
+ result: str = random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0]
else:
result: str = weights
diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py
index 905cae867ebe..f737b461d0bc 100644
--- a/worlds/timespinner/Regions.py
+++ b/worlds/timespinner/Regions.py
@@ -1,15 +1,16 @@
-from typing import List, Set, Dict, Tuple, Optional, Callable
+from typing import List, Set, Dict, Optional, Callable
from BaseClasses import CollectionState, MultiWorld, Region, Entrance, Location
-from .Options import is_option_enabled
+from .Options import TimespinnerOptions
from .Locations import LocationData, get_location_datas
from .PreCalculatedWeights import PreCalculatedWeights
from .LogicExtensions import TimespinnerLogic
-def create_regions_and_locations(world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights):
- locationn_datas: Tuple[LocationData] = get_location_datas(world, player, precalculated_weights)
+def create_regions_and_locations(world: MultiWorld, player: int, options: TimespinnerOptions,
+ precalculated_weights: PreCalculatedWeights):
- locations_per_region: Dict[str, List[LocationData]] = split_location_datas_per_region(locationn_datas)
+ locations_per_region: Dict[str, List[LocationData]] = split_location_datas_per_region(
+ get_location_datas(player, options, precalculated_weights))
regions = [
create_region(world, player, locations_per_region, 'Menu'),
@@ -32,7 +33,6 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
create_region(world, player, locations_per_region, 'The lab (upper)'),
create_region(world, player, locations_per_region, 'Emperors tower'),
create_region(world, player, locations_per_region, 'Skeleton Shaft'),
- create_region(world, player, locations_per_region, 'Sealed Caves (upper)'),
create_region(world, player, locations_per_region, 'Sealed Caves (Xarion)'),
create_region(world, player, locations_per_region, 'Refugee Camp'),
create_region(world, player, locations_per_region, 'Forest'),
@@ -55,7 +55,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
create_region(world, player, locations_per_region, 'Space time continuum')
]
- if is_option_enabled(world, player, "GyreArchives"):
+ if options.gyre_archives:
regions.extend([
create_region(world, player, locations_per_region, 'Ravenlord\'s Lair'),
create_region(world, player, locations_per_region, 'Ifrit\'s Lair'),
@@ -63,17 +63,17 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
if __debug__:
throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys())
-
+
world.regions += regions
- connectStartingRegion(world, player)
+ connectStartingRegion(world, player, options)
flooded: PreCalculatedWeights = precalculated_weights
- logic = TimespinnerLogic(world, player, precalculated_weights)
+ logic = TimespinnerLogic(player, options, precalculated_weights)
- connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: logic.has_timestop(state) or state.has('Talaria Attachment', player) or flooded.flood_lake_desolation)
- connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player))
- connect(world, player, 'Lake desolation', 'Skeleton Shaft', lambda state: logic.has_doublejump(state) or flooded.flood_lake_desolation)
+ connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: flooded.flood_lake_desolation or logic.has_timestop(state) or state.has('Talaria Attachment', player))
+ connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player), "Upper Lake Serene")
+ connect(world, player, 'Lake desolation', 'Skeleton Shaft', lambda state: flooded.flood_lake_desolation or logic.has_doublejump(state))
connect(world, player, 'Lake desolation', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Upper lake desolation', 'Lake desolation')
connect(world, player, 'Upper lake desolation', 'Eastern lake desolation')
@@ -82,7 +82,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
connect(world, player, 'Eastern lake desolation', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Eastern lake desolation', 'Library')
connect(world, player, 'Eastern lake desolation', 'Lower lake desolation')
- connect(world, player, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player))
+ connect(world, player, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player), "Upper Lake Serene")
connect(world, player, 'Library', 'Eastern lake desolation')
connect(world, player, 'Library', 'Library top', lambda state: logic.has_doublejump(state) or state.has('Talaria Attachment', player))
connect(world, player, 'Library', 'Varndagroth tower left', logic.has_keycard_D)
@@ -109,40 +109,38 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
connect(world, player, 'Military Fortress', 'Temporal Gyre', lambda state: state.has('Timespinner Wheel', player))
connect(world, player, 'Military Fortress', 'Military Fortress (hangar)', logic.has_doublejump)
connect(world, player, 'Military Fortress (hangar)', 'Military Fortress')
- connect(world, player, 'Military Fortress (hangar)', 'The lab', lambda state: logic.has_keycard_B(state) and logic.has_doublejump(state))
+ connect(world, player, 'Military Fortress (hangar)', 'The lab', lambda state: logic.has_keycard_B(state) and (state.has('Water Mask', player) if flooded.flood_lab else logic.has_doublejump(state)))
connect(world, player, 'Temporal Gyre', 'Military Fortress')
connect(world, player, 'The lab', 'Military Fortress')
connect(world, player, 'The lab', 'The lab (power off)', logic.has_doublejump_of_npc)
- connect(world, player, 'The lab (power off)', 'The lab')
+ connect(world, player, 'The lab (power off)', 'The lab', lambda state: not flooded.flood_lab or state.has('Water Mask', player))
connect(world, player, 'The lab (power off)', 'The lab (upper)', logic.has_forwarddash_doublejump)
connect(world, player, 'The lab (upper)', 'The lab (power off)')
connect(world, player, 'The lab (upper)', 'Emperors tower', logic.has_forwarddash_doublejump)
connect(world, player, 'The lab (upper)', 'Ancient Pyramid (entrance)', lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player))
connect(world, player, 'Emperors tower', 'The lab (upper)')
connect(world, player, 'Skeleton Shaft', 'Lake desolation')
- connect(world, player, 'Skeleton Shaft', 'Sealed Caves (upper)', logic.has_keycard_A)
+ connect(world, player, 'Skeleton Shaft', 'Sealed Caves (Xarion)', logic.has_keycard_A)
connect(world, player, 'Skeleton Shaft', 'Space time continuum', logic.has_teleport)
- connect(world, player, 'Sealed Caves (upper)', 'Skeleton Shaft')
- connect(world, player, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: logic.has_teleport(state) or logic.has_doublejump(state))
- connect(world, player, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', logic.has_doublejump)
+ connect(world, player, 'Sealed Caves (Xarion)', 'Skeleton Shaft')
connect(world, player, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Refugee Camp', 'Forest')
- #connect(world, player, 'Refugee Camp', 'Library', lambda state: not is_option_enabled(world, player, "Inverted"))
+ connect(world, player, 'Refugee Camp', 'Library', lambda state: options.inverted and options.back_to_the_future and state.has_all({'Timespinner Wheel', 'Timespinner Spindle'}, player))
connect(world, player, 'Refugee Camp', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Forest', 'Refugee Camp')
- connect(world, player, 'Forest', 'Left Side forest Caves', lambda state: state.has('Talaria Attachment', player) or logic.has_timestop(state))
+ connect(world, player, 'Forest', 'Left Side forest Caves', lambda state: flooded.flood_lake_serene_bridge or state.has('Talaria Attachment', player) or logic.has_timestop(state))
connect(world, player, 'Forest', 'Caves of Banishment (Sirens)')
connect(world, player, 'Forest', 'Castle Ramparts')
connect(world, player, 'Left Side forest Caves', 'Forest')
connect(world, player, 'Left Side forest Caves', 'Upper Lake Serene', logic.has_timestop)
- connect(world, player, 'Left Side forest Caves', 'Lower Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene)
+ connect(world, player, 'Left Side forest Caves', 'Lower Lake Serene', lambda state: not flooded.flood_lake_serene or state.has('Water Mask', player))
connect(world, player, 'Left Side forest Caves', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Upper Lake Serene', 'Left Side forest Caves')
- connect(world, player, 'Upper Lake Serene', 'Lower Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene)
+ connect(world, player, 'Upper Lake Serene', 'Lower Lake Serene', lambda state: not flooded.flood_lake_serene or state.has('Water Mask', player))
connect(world, player, 'Lower Lake Serene', 'Upper Lake Serene')
connect(world, player, 'Lower Lake Serene', 'Left Side forest Caves')
- connect(world, player, 'Lower Lake Serene', 'Caves of Banishment (upper)', lambda state: not flooded.dry_lake_serene or logic.has_doublejump(state))
- connect(world, player, 'Caves of Banishment (upper)', 'Upper Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene)
+ connect(world, player, 'Lower Lake Serene', 'Caves of Banishment (upper)', lambda state: flooded.flood_lake_serene or logic.has_doublejump(state))
+ connect(world, player, 'Caves of Banishment (upper)', 'Lower Lake Serene', lambda state: not flooded.flood_lake_serene or state.has('Water Mask', player))
connect(world, player, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: logic.has_doublejump(state) or state.has_any({'Gas Mask', 'Talaria Attachment'} or logic.has_teleport(state), player))
connect(world, player, 'Caves of Banishment (upper)', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: logic.has_doublejump(state) if not flooded.flood_maw else state.has('Water Mask', player))
@@ -153,7 +151,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
connect(world, player, 'Castle Ramparts', 'Castle Keep')
connect(world, player, 'Castle Ramparts', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Castle Keep', 'Castle Ramparts')
- connect(world, player, 'Castle Keep', 'Castle Basement', lambda state: state.has('Water Mask', player) or not flooded.flood_basement)
+ connect(world, player, 'Castle Keep', 'Castle Basement', lambda state: not flooded.flood_basement or state.has('Water Mask', player))
connect(world, player, 'Castle Keep', 'Royal towers (lower)', logic.has_doublejump)
connect(world, player, 'Castle Keep', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Royal towers (lower)', 'Castle Keep')
@@ -165,14 +163,15 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
#connect(world, player, 'Ancient Pyramid (entrance)', 'The lab (upper)', lambda state: not is_option_enabled(world, player, "EnterSandman"))
connect(world, player, 'Ancient Pyramid (entrance)', 'Ancient Pyramid (left)', logic.has_doublejump)
connect(world, player, 'Ancient Pyramid (left)', 'Ancient Pyramid (entrance)')
- connect(world, player, 'Ancient Pyramid (left)', 'Ancient Pyramid (right)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft)
- connect(world, player, 'Ancient Pyramid (right)', 'Ancient Pyramid (left)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft)
+ connect(world, player, 'Ancient Pyramid (left)', 'Ancient Pyramid (right)', lambda state: flooded.flood_pyramid_shaft or logic.has_upwarddash(state))
+ connect(world, player, 'Ancient Pyramid (right)', 'Ancient Pyramid (left)', lambda state: flooded.flood_pyramid_shaft or logic.has_upwarddash(state))
connect(world, player, 'Space time continuum', 'Lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateLakeDesolation"))
connect(world, player, 'Space time continuum', 'Lower lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateKittyBoss"))
connect(world, player, 'Space time continuum', 'Library', lambda state: logic.can_teleport_to(state, "Present", "GateLeftLibrary"))
connect(world, player, 'Space time continuum', 'Varndagroth tower right (lower)', lambda state: logic.can_teleport_to(state, "Present", "GateMilitaryGate"))
connect(world, player, 'Space time continuum', 'Skeleton Shaft', lambda state: logic.can_teleport_to(state, "Present", "GateSealedCaves"))
connect(world, player, 'Space time continuum', 'Sealed Caves (Sirens)', lambda state: logic.can_teleport_to(state, "Present", "GateSealedSirensCave"))
+ connect(world, player, 'Space time continuum', 'Sealed Caves (Xarion)', lambda state: logic.can_teleport_to(state, "Present", "GateXarion"))
connect(world, player, 'Space time continuum', 'Upper Lake Serene', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneLeft"))
connect(world, player, 'Space time continuum', 'Left Side forest Caves', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneRight"))
connect(world, player, 'Space time continuum', 'Refugee Camp', lambda state: logic.can_teleport_to(state, "Past", "GateAccessToPast"))
@@ -181,14 +180,14 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
connect(world, player, 'Space time continuum', 'Royal towers (lower)', lambda state: logic.can_teleport_to(state, "Past", "GateRoyalTowers"))
connect(world, player, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: logic.can_teleport_to(state, "Past", "GateMaw"))
connect(world, player, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: logic.can_teleport_to(state, "Past", "GateCavesOfBanishment"))
- connect(world, player, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not is_option_enabled(world, player, "UnchainedKeys") and is_option_enabled(world, player, "EnterSandman")))
+ connect(world, player, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not options.unchained_keys and options.enter_sandman))
connect(world, player, 'Space time continuum', 'Ancient Pyramid (left)', lambda state: logic.can_teleport_to(state, "Time", "GateLeftPyramid"))
connect(world, player, 'Space time continuum', 'Ancient Pyramid (right)', lambda state: logic.can_teleport_to(state, "Time", "GateRightPyramid"))
- if is_option_enabled(world, player, "GyreArchives"):
+ if options.gyre_archives:
connect(world, player, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player))
connect(world, player, 'Ravenlord\'s Lair', 'The lab (upper)')
- connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player))
+ connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player), "Refugee Camp")
connect(world, player, 'Ifrit\'s Lair', 'Library top')
@@ -204,12 +203,12 @@ def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames:
def create_location(player: int, location_data: LocationData, region: Region) -> Location:
location = Location(player, location_data.name, location_data.code, region)
- location.access_rule = location_data.rule
+
+ if location_data.rule:
+ location.access_rule = location_data.rule
if id is None:
- location.event = True
location.locked = True
-
return location
@@ -220,16 +219,15 @@ def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str
for location_data in locations_per_region[name]:
location = create_location(player, location_data, region)
region.locations.append(location)
-
return region
-def connectStartingRegion(world: MultiWorld, player: int):
+def connectStartingRegion(world: MultiWorld, player: int, options: TimespinnerOptions):
menu = world.get_region('Menu', player)
tutorial = world.get_region('Tutorial', player)
space_time_continuum = world.get_region('Space time continuum', player)
- if is_option_enabled(world, player, "Inverted"):
+ if options.inverted:
starting_region = world.get_region('Refugee Camp', player)
else:
starting_region = world.get_region('Lake desolation', player)
@@ -237,35 +235,33 @@ def connectStartingRegion(world: MultiWorld, player: int):
menu_to_tutorial = Entrance(player, 'Tutorial', menu)
menu_to_tutorial.connect(tutorial)
menu.exits.append(menu_to_tutorial)
-
tutorial_to_start = Entrance(player, 'Start Game', tutorial)
tutorial_to_start.connect(starting_region)
tutorial.exits.append(tutorial_to_start)
-
teleport_back_to_start = Entrance(player, 'Teleport back to start', space_time_continuum)
teleport_back_to_start.connect(starting_region)
space_time_continuum.exits.append(teleport_back_to_start)
def connect(world: MultiWorld, player: int, source: str, target: str,
- rule: Optional[Callable[[CollectionState], bool]] = None):
-
+ rule: Optional[Callable[[CollectionState], bool]] = None,
+ indirect: str = ""):
+
sourceRegion = world.get_region(source, player)
targetRegion = world.get_region(target, player)
+ entrance = sourceRegion.connect(targetRegion, rule=rule)
- connection = Entrance(player, "", sourceRegion)
-
- if rule:
- connection.access_rule = rule
+ if indirect:
+ indirectRegion = world.get_region(indirect, player)
+ if indirectRegion in world.indirect_connections:
+ world.indirect_connections[indirectRegion].add(entrance)
+ else:
+ world.indirect_connections[indirectRegion] = {entrance}
- sourceRegion.exits.append(connection)
- connection.connect(targetRegion)
-
-def split_location_datas_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]:
+def split_location_datas_per_region(locations: List[LocationData]) -> Dict[str, List[LocationData]]:
per_region: Dict[str, List[LocationData]] = {}
for location in locations:
per_region.setdefault(location.region, []).append(location)
-
- return per_region
+ return per_region
\ No newline at end of file
diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py
index de1d58e9616b..66744cffdf85 100644
--- a/worlds/timespinner/__init__.py
+++ b/worlds/timespinner/__init__.py
@@ -1,12 +1,13 @@
-from typing import Dict, List, Set, Tuple, TextIO, Union
-from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
+from typing import Dict, List, Set, Tuple, TextIO
+from BaseClasses import Item, Tutorial, ItemClassification
from .Items import get_item_names_per_category
from .Items import item_table, starter_melee_weapons, starter_spells, filler_items, starter_progression_items
from .Locations import get_location_datas, EventId
-from .Options import is_option_enabled, get_option_value, timespinner_options
+from .Options import BackwardsCompatiableTimespinnerOptions, Toggle
from .PreCalculatedWeights import PreCalculatedWeights
from .Regions import create_regions_and_locations
from worlds.AutoWorld import World, WebWorld
+import logging
class TimespinnerWebWorld(WebWorld):
theme = "ice"
@@ -35,35 +36,34 @@ class TimespinnerWorld(World):
Timespinner is a beautiful metroidvania inspired by classic 90s action-platformers.
Travel back in time to change fate itself. Join timekeeper Lunais on her quest for revenge against the empire that killed her family.
"""
-
- option_definitions = timespinner_options
+ options_dataclass = BackwardsCompatiableTimespinnerOptions
+ options: BackwardsCompatiableTimespinnerOptions
game = "Timespinner"
topology_present = True
- data_version = 11
web = TimespinnerWebWorld()
- required_client_version = (0, 3, 7)
+ required_client_version = (0, 4, 2)
item_name_to_id = {name: data.code for name, data in item_table.items()}
- location_name_to_id = {location.name: location.code for location in get_location_datas(None, None, None)}
+ location_name_to_id = {location.name: location.code for location in get_location_datas(-1, None, None)}
item_name_groups = get_item_names_per_category()
precalculated_weights: PreCalculatedWeights
- def __init__(self, world: MultiWorld, player: int):
- super().__init__(world, player)
- self.precalculated_weights = PreCalculatedWeights(world, player)
-
def generate_early(self) -> None:
+ self.options.handle_backward_compatibility()
+
+ self.precalculated_weights = PreCalculatedWeights(self.options, self.random)
+
# in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly
- if self.multiworld.start_inventory[self.player].value.pop('Meyef', 0) > 0:
- self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true
- if self.multiworld.start_inventory[self.player].value.pop('Talaria Attachment', 0) > 0:
- self.multiworld.QuickSeed[self.player].value = self.multiworld.QuickSeed[self.player].option_true
- if self.multiworld.start_inventory[self.player].value.pop('Jewelry Box', 0) > 0:
- self.multiworld.StartWithJewelryBox[self.player].value = self.multiworld.StartWithJewelryBox[self.player].option_true
+ if self.options.start_inventory.value.pop('Meyef', 0) > 0:
+ self.options.start_with_meyef.value = Toggle.option_true
+ if self.options.start_inventory.value.pop('Talaria Attachment', 0) > 0:
+ self.options.quick_seed.value = Toggle.option_true
+ if self.options.start_inventory.value.pop('Jewelry Box', 0) > 0:
+ self.options.start_with_jewelry_box.value = Toggle.option_true
def create_regions(self) -> None:
- create_regions_and_locations(self.multiworld, self.player, self.precalculated_weights)
+ create_regions_and_locations(self.multiworld, self.player, self.options, self.precalculated_weights)
def create_items(self) -> None:
self.create_and_assign_event_items()
@@ -77,7 +77,7 @@ def create_items(self) -> None:
def set_rules(self) -> None:
final_boss: str
- if self.is_option_enabled("DadPercent"):
+ if self.options.dad_percent:
final_boss = "Killed Emperor"
else:
final_boss = "Killed Nightmare"
@@ -85,46 +85,74 @@ def set_rules(self) -> None:
self.multiworld.completion_condition[self.player] = lambda state: state.has(final_boss, self.player)
def fill_slot_data(self) -> Dict[str, object]:
- slot_data: Dict[str, object] = {}
-
- ap_specific_settings: Set[str] = {"RisingTidesOverrides", "TrapChance"}
-
- for option_name in timespinner_options:
- if (option_name not in ap_specific_settings):
- slot_data[option_name] = self.get_option_value(option_name)
-
- slot_data["StinkyMaw"] = True
- slot_data["ProgressiveVerticalMovement"] = False
- slot_data["ProgressiveKeycards"] = False
- slot_data["PersonalItems"] = self.get_personal_items()
- slot_data["PyramidKeysGate"] = self.precalculated_weights.pyramid_keys_unlock
- slot_data["PresentGate"] = self.precalculated_weights.present_key_unlock
- slot_data["PastGate"] = self.precalculated_weights.past_key_unlock
- slot_data["TimeGate"] = self.precalculated_weights.time_key_unlock
- slot_data["Basement"] = int(self.precalculated_weights.flood_basement) + \
- int(self.precalculated_weights.flood_basement_high)
- slot_data["Xarion"] = self.precalculated_weights.flood_xarion
- slot_data["Maw"] = self.precalculated_weights.flood_maw
- slot_data["PyramidShaft"] = self.precalculated_weights.flood_pyramid_shaft
- slot_data["BackPyramid"] = self.precalculated_weights.flood_pyramid_back
- slot_data["CastleMoat"] = self.precalculated_weights.flood_moat
- slot_data["CastleCourtyard"] = self.precalculated_weights.flood_courtyard
- slot_data["LakeDesolation"] = self.precalculated_weights.flood_lake_desolation
- slot_data["DryLakeSerene"] = self.precalculated_weights.dry_lake_serene
-
- return slot_data
+ return {
+ # options
+ "StartWithJewelryBox": self.options.start_with_jewelry_box.value,
+ "DownloadableItems": self.options.downloadable_items.value,
+ "EyeSpy": self.options.eye_spy.value,
+ "StartWithMeyef": self.options.start_with_meyef.value,
+ "QuickSeed": self.options.quick_seed.value,
+ "SpecificKeycards": self.options.specific_keycards.value,
+ "Inverted": self.options.inverted.value,
+ "GyreArchives": self.options.gyre_archives.value,
+ "Cantoran": self.options.cantoran.value,
+ "LoreChecks": self.options.lore_checks.value,
+ "BossRando": self.options.boss_rando.value,
+ "DamageRando": self.options.damage_rando.value,
+ "DamageRandoOverrides": self.options.damage_rando_overrides.value,
+ "HpCap": self.options.hp_cap.value,
+ "LevelCap": self.options.level_cap.value,
+ "ExtraEarringsXP": self.options.extra_earrings_xp.value,
+ "BossHealing": self.options.boss_healing.value,
+ "ShopFill": self.options.shop_fill.value,
+ "ShopWarpShards": self.options.shop_warp_shards.value,
+ "ShopMultiplier": self.options.shop_multiplier.value,
+ "LootPool": self.options.loot_pool.value,
+ "DropRateCategory": self.options.drop_rate_category.value,
+ "FixedDropRate": self.options.fixed_drop_rate.value,
+ "LootTierDistro": self.options.loot_tier_distro.value,
+ "ShowBestiary": self.options.show_bestiary.value,
+ "ShowDrops": self.options.show_drops.value,
+ "EnterSandman": self.options.enter_sandman.value,
+ "DadPercent": self.options.dad_percent.value,
+ "RisingTides": self.options.rising_tides.value,
+ "UnchainedKeys": self.options.unchained_keys.value,
+ "PresentAccessWithWheelAndSpindle": self.options.back_to_the_future.value,
+ "Traps": self.options.traps.value,
+ "DeathLink": self.options.death_link.value,
+ "StinkyMaw": True,
+ # data
+ "PersonalItems": self.get_personal_items(),
+ "PyramidKeysGate": self.precalculated_weights.pyramid_keys_unlock,
+ "PresentGate": self.precalculated_weights.present_key_unlock,
+ "PastGate": self.precalculated_weights.past_key_unlock,
+ "TimeGate": self.precalculated_weights.time_key_unlock,
+ # rising tides
+ "Basement": int(self.precalculated_weights.flood_basement) + \
+ int(self.precalculated_weights.flood_basement_high),
+ "Xarion": self.precalculated_weights.flood_xarion,
+ "Maw": self.precalculated_weights.flood_maw,
+ "PyramidShaft": self.precalculated_weights.flood_pyramid_shaft,
+ "BackPyramid": self.precalculated_weights.flood_pyramid_back,
+ "CastleMoat": self.precalculated_weights.flood_moat,
+ "CastleCourtyard": self.precalculated_weights.flood_courtyard,
+ "LakeDesolation": self.precalculated_weights.flood_lake_desolation,
+ "DryLakeSerene": not self.precalculated_weights.flood_lake_serene,
+ "LakeSereneBridge": self.precalculated_weights.flood_lake_serene_bridge,
+ "Lab": self.precalculated_weights.flood_lab
+ }
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
- if self.is_option_enabled("UnchainedKeys"):
+ if self.options.unchained_keys:
spoiler_handle.write(f'Modern Warp Beacon unlock: {self.precalculated_weights.present_key_unlock}\n')
spoiler_handle.write(f'Timeworn Warp Beacon unlock: {self.precalculated_weights.past_key_unlock}\n')
- if self.is_option_enabled("EnterSandman"):
+ if self.options.enter_sandman:
spoiler_handle.write(f'Mysterious Warp Beacon unlock: {self.precalculated_weights.time_key_unlock}\n')
else:
spoiler_handle.write(f'Twin Pyramid Keys unlock: {self.precalculated_weights.pyramid_keys_unlock}\n')
- if self.is_option_enabled("RisingTides"):
+ if self.options.rising_tides:
flooded_areas: List[str] = []
if self.precalculated_weights.flood_basement:
@@ -146,8 +174,12 @@ def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
flooded_areas.append("Castle Courtyard")
if self.precalculated_weights.flood_lake_desolation:
flooded_areas.append("Lake Desolation")
- if not self.precalculated_weights.dry_lake_serene:
+ if self.precalculated_weights.flood_lake_serene:
flooded_areas.append("Lake Serene")
+ if self.precalculated_weights.flood_lake_serene_bridge:
+ flooded_areas.append("Lake Serene Bridge")
+ if self.precalculated_weights.flood_lab:
+ flooded_areas.append("Lab")
if len(flooded_areas) == 0:
flooded_areas_string: str = "None"
@@ -156,6 +188,15 @@ def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
spoiler_handle.write(f'Flooded Areas: {flooded_areas_string}\n')
+ if self.options.has_replaced_options:
+ warning = \
+ f"NOTICE: Timespinner options for player '{self.player_name}' where renamed from PasCalCase to snake_case, " \
+ "please update your yaml"
+
+ spoiler_handle.write("\n")
+ spoiler_handle.write(warning)
+ logging.warning(warning)
+
def create_item(self, name: str) -> Item:
data = item_table[name]
@@ -173,41 +214,41 @@ def create_item(self, name: str) -> Item:
if not item.advancement:
return item
- if (name == 'Tablet' or name == 'Library Keycard V') and not self.is_option_enabled("DownloadableItems"):
+ if (name == 'Tablet' or name == 'Library Keycard V') and not self.options.downloadable_items:
item.classification = ItemClassification.filler
- elif name == 'Oculus Ring' and not self.is_option_enabled("EyeSpy"):
+ elif name == 'Oculus Ring' and not self.options.eye_spy:
item.classification = ItemClassification.filler
- elif (name == 'Kobo' or name == 'Merchant Crow') and not self.is_option_enabled("GyreArchives"):
+ elif (name == 'Kobo' or name == 'Merchant Crow') and not self.options.gyre_archives:
item.classification = ItemClassification.filler
elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \
- and not self.is_option_enabled("UnchainedKeys"):
+ and not self.options.unchained_keys:
item.classification = ItemClassification.filler
return item
def get_filler_item_name(self) -> str:
- trap_chance: int = self.get_option_value("TrapChance")
- enabled_traps: List[str] = self.get_option_value("Traps")
+ trap_chance: int = self.options.trap_chance.value
+ enabled_traps: List[str] = self.options.traps.value
- if self.multiworld.random.random() < (trap_chance / 100) and enabled_traps:
- return self.multiworld.random.choice(enabled_traps)
+ if self.random.random() < (trap_chance / 100) and enabled_traps:
+ return self.random.choice(enabled_traps)
else:
- return self.multiworld.random.choice(filler_items)
+ return self.random.choice(filler_items)
def get_excluded_items(self) -> Set[str]:
excluded_items: Set[str] = set()
- if self.is_option_enabled("StartWithJewelryBox"):
+ if self.options.start_with_jewelry_box:
excluded_items.add('Jewelry Box')
- if self.is_option_enabled("StartWithMeyef"):
+ if self.options.start_with_meyef:
excluded_items.add('Meyef')
- if self.is_option_enabled("QuickSeed"):
+ if self.options.quick_seed:
excluded_items.add('Talaria Attachment')
- if self.is_option_enabled("UnchainedKeys"):
+ if self.options.unchained_keys:
excluded_items.add('Twin Pyramid Key')
- if not self.is_option_enabled("EnterSandman"):
+ if not self.options.enter_sandman:
excluded_items.add('Mysterious Warp Beacon')
else:
excluded_items.add('Timeworn Warp Beacon')
@@ -221,16 +262,19 @@ def get_excluded_items(self) -> Set[str]:
return excluded_items
def assign_starter_items(self, excluded_items: Set[str]) -> None:
- non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value
+ non_local_items: Set[str] = self.options.non_local_items.value
+ local_items: Set[str] = self.options.local_items.value
- local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item not in non_local_items)
+ local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if
+ item in local_items or not item in non_local_items)
if not local_starter_melee_weapons:
if 'Plasma Orb' in non_local_items:
raise Exception("Atleast one melee orb must be local")
else:
local_starter_melee_weapons = ('Plasma Orb',)
- local_starter_spells = tuple(item for item in starter_spells if item not in non_local_items)
+ local_starter_spells = tuple(item for item in starter_spells if
+ item in local_items or not item in non_local_items)
if not local_starter_spells:
if 'Lightwall' in non_local_items:
raise Exception("Atleast one spell must be local")
@@ -241,27 +285,26 @@ def assign_starter_items(self, excluded_items: Set[str]) -> None:
self.assign_starter_item(excluded_items, 'Tutorial: Yo Momma 2', local_starter_spells)
def assign_starter_item(self, excluded_items: Set[str], location: str, item_list: Tuple[str, ...]) -> None:
- item_name = self.multiworld.random.choice(item_list)
+ item_name = self.random.choice(item_list)
self.place_locked_item(excluded_items, location, item_name)
def place_first_progression_item(self, excluded_items: Set[str]) -> None:
- if self.is_option_enabled("QuickSeed") or self.is_option_enabled("Inverted") \
- or self.precalculated_weights.flood_lake_desolation:
+ if self.options.quick_seed or self.options.inverted or self.precalculated_weights.flood_lake_desolation:
return
- for item in self.multiworld.precollected_items[self.player]:
- if item.name in starter_progression_items and not item.name in excluded_items:
+ for item_name in self.options.start_inventory.value.keys():
+ if item_name in starter_progression_items:
return
local_starter_progression_items = tuple(
item for item in starter_progression_items
- if item not in excluded_items and item not in self.multiworld.non_local_items[self.player].value)
+ if item not in excluded_items and item not in self.options.non_local_items.value)
if not local_starter_progression_items:
return
- progression_item = self.multiworld.random.choice(local_starter_progression_items)
+ progression_item = self.random.choice(local_starter_progression_items)
self.multiworld.local_early_items[self.player][progression_item] = 1
@@ -301,9 +344,3 @@ def get_personal_items(self) -> Dict[int, int]:
personal_items[location.address] = location.item.code
return personal_items
-
- def is_option_enabled(self, option: str) -> bool:
- return is_option_enabled(self.multiworld, self.player, option)
-
- def get_option_value(self, option: str) -> Union[int, Dict, List]:
- return get_option_value(self.multiworld, self.player, option)
diff --git a/worlds/timespinner/docs/en_Timespinner.md b/worlds/timespinner/docs/en_Timespinner.md
index 6a9e7fa4c039..a5b1419b94a7 100644
--- a/worlds/timespinner/docs/en_Timespinner.md
+++ b/worlds/timespinner/docs/en_Timespinner.md
@@ -1,8 +1,8 @@
# Timespinner
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
diff --git a/worlds/timespinner/docs/setup_de.md b/worlds/timespinner/docs/setup_de.md
index 463568ecbdb4..e86474744676 100644
--- a/worlds/timespinner/docs/setup_de.md
+++ b/worlds/timespinner/docs/setup_de.md
@@ -42,7 +42,7 @@ Weitere Informationen zum Randomizer findest du hier: [ReadMe](https://github.co
## Woher bekomme ich eine Konfigurationsdatei?
-Die [Player Settings](https://archipelago.gg/games/Timespinner/player-settings) Seite auf der Website erlaubt dir,
+Die [Player Options](https://archipelago.gg/games/Timespinner/player-options) Seite auf der Website erlaubt dir,
persönliche Einstellungen zu definieren und diese in eine Konfigurationsdatei zu exportieren
* Die Timespinner Randomizer Option "StinkyMaw" ist in Archipelago Seeds aktuell immer an
diff --git a/worlds/timespinner/docs/setup_en.md b/worlds/timespinner/docs/setup_en.md
index c47c639cd20d..7ee51f91323a 100644
--- a/worlds/timespinner/docs/setup_en.md
+++ b/worlds/timespinner/docs/setup_en.md
@@ -33,8 +33,8 @@ randomized mode. For more info see the [ReadMe](https://github.com/Jarno458/TsRa
## Where do I get a config file?
-The [Player Settings](/games/Timespinner/player-settings) page on the website allows you to
-configure your personal settings and export them into a config file
+The [Player Options](/games/Timespinner/player-options) page on the website allows you to
+configure your personal options and export them into a config file
* The Timespinner Randomizer option "StinkyMaw" is currently always enabled for Archipelago generated seeds
* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported
diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py
index 1d33336172d8..4acda4ef41fc 100644
--- a/worlds/tloz/ItemPool.py
+++ b/worlds/tloz/ItemPool.py
@@ -80,7 +80,7 @@ def generate_itempool(tlozworld):
location.item.classification = ItemClassification.progression
def get_pool_core(world):
- random = world.multiworld.random
+ random = world.random
pool = []
placed_items = {}
@@ -93,14 +93,18 @@ def get_pool_core(world):
# Starting Weapon
start_weapon_locations = starting_weapon_locations.copy()
- starting_weapon = random.choice(starting_weapons)
- if world.multiworld.StartingPosition[world.player] == StartingPosition.option_safe:
+ final_starting_weapons = [weapon for weapon in starting_weapons
+ if weapon not in world.options.non_local_items]
+ if not final_starting_weapons:
+ final_starting_weapons = starting_weapons
+ starting_weapon = random.choice(final_starting_weapons)
+ if world.options.StartingPosition == StartingPosition.option_safe:
placed_items[start_weapon_locations[0]] = starting_weapon
- elif world.multiworld.StartingPosition[world.player] in \
+ elif world.options.StartingPosition in \
[StartingPosition.option_unsafe, StartingPosition.option_dangerous]:
- if world.multiworld.StartingPosition[world.player] == StartingPosition.option_dangerous:
+ if world.options.StartingPosition == StartingPosition.option_dangerous:
for location in dangerous_weapon_locations:
- if world.multiworld.ExpandedPool[world.player] or "Drop" not in location:
+ if world.options.ExpandedPool or "Drop" not in location:
start_weapon_locations.append(location)
placed_items[random.choice(start_weapon_locations)] = starting_weapon
else:
@@ -111,31 +115,26 @@ def get_pool_core(world):
# Triforce Fragments
fragment = "Triforce Fragment"
- if world.multiworld.ExpandedPool[world.player]:
+ if world.options.ExpandedPool:
possible_level_locations = [location for location in all_level_locations
if location not in level_locations[8]]
else:
possible_level_locations = [location for location in standard_level_locations
if location not in level_locations[8]]
+ for location in placed_items.keys():
+ if location in possible_level_locations:
+ possible_level_locations.remove(location)
for level in range(1, 9):
- if world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_vanilla:
+ if world.options.TriforceLocations == TriforceLocations.option_vanilla:
placed_items[f"Level {level} Triforce"] = fragment
- elif world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_dungeons:
+ elif world.options.TriforceLocations == TriforceLocations.option_dungeons:
placed_items[possible_level_locations.pop(random.randint(0, len(possible_level_locations) - 1))] = fragment
else:
pool.append(fragment)
- # Level 9 junk fill
- if world.multiworld.ExpandedPool[world.player] > 0:
- spots = random.sample(level_locations[8], len(level_locations[8]) // 2)
- for spot in spots:
- junk = random.choice(list(minor_items.keys()))
- placed_items[spot] = junk
- minor_items[junk] -= 1
-
# Finish Pool
final_pool = basic_pool
- if world.multiworld.ExpandedPool[world.player]:
+ if world.options.ExpandedPool:
final_pool = {
item: basic_pool.get(item, 0) + minor_items.get(item, 0) + take_any_items.get(item, 0)
for item in set(basic_pool) | set(minor_items) | set(take_any_items)
diff --git a/worlds/tloz/Items.py b/worlds/tloz/Items.py
index d896d11d770b..b421b740012c 100644
--- a/worlds/tloz/Items.py
+++ b/worlds/tloz/Items.py
@@ -24,7 +24,7 @@ class ItemData(typing.NamedTuple):
"Red Candle": ItemData(107, progression),
"Book of Magic": ItemData(108, progression),
"Magical Key": ItemData(109, useful),
- "Red Ring": ItemData(110, useful),
+ "Red Ring": ItemData(110, progression),
"Silver Arrow": ItemData(111, progression),
"Sword": ItemData(112, progression),
"White Sword": ItemData(113, progression),
@@ -37,7 +37,7 @@ class ItemData(typing.NamedTuple):
"Food": ItemData(120, progression),
"Water of Life (Blue)": ItemData(121, useful),
"Water of Life (Red)": ItemData(122, useful),
- "Blue Ring": ItemData(123, useful),
+ "Blue Ring": ItemData(123, progression),
"Triforce Fragment": ItemData(124, progression),
"Power Bracelet": ItemData(125, useful),
"Small Key": ItemData(126, filler),
diff --git a/worlds/tloz/Locations.py b/worlds/tloz/Locations.py
index 3e46c4383373..9715cc684291 100644
--- a/worlds/tloz/Locations.py
+++ b/worlds/tloz/Locations.py
@@ -99,12 +99,24 @@
"Potion Shop Item Left", "Potion Shop Item Middle", "Potion Shop Item Right"
]
+take_any_locations = [
+ "Take Any Item Left", "Take Any Item Middle", "Take Any Item Right"
+]
+
+sword_cave_locations = [
+ "Starting Sword Cave", "White Sword Pond", "Magical Sword Grave"
+]
+
food_locations = [
"Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)",
"Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)",
"Level 7 Bomb Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)"
]
+gleeok_locations = [
+ "Level 4 Boss", "Level 4 Triforce", "Level 8 Boss", "Level 8 Triforce"
+]
+
floor_location_game_offsets_early = {
"Level 1 Item (Bow)": 0x7F,
"Level 1 Item (Boomerang)": 0x44,
diff --git a/worlds/tloz/Options.py b/worlds/tloz/Options.py
index 96bd3e296dca..58a50ec35929 100644
--- a/worlds/tloz/Options.py
+++ b/worlds/tloz/Options.py
@@ -1,5 +1,6 @@
import typing
-from Options import Option, DefaultOnToggle, Choice
+from dataclasses import dataclass
+from Options import Option, DefaultOnToggle, Choice, PerGameCommonOptions
class ExpandedPool(DefaultOnToggle):
@@ -32,9 +33,8 @@ class StartingPosition(Choice):
option_dangerous = 2
option_very_dangerous = 3
-
-tloz_options: typing.Dict[str, type(Option)] = {
- "ExpandedPool": ExpandedPool,
- "TriforceLocations": TriforceLocations,
- "StartingPosition": StartingPosition
-}
+@dataclass
+class TlozOptions(PerGameCommonOptions):
+ ExpandedPool: ExpandedPool
+ TriforceLocations: TriforceLocations
+ StartingPosition: StartingPosition
diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py
index 12bf466bce99..39c3b954f0d4 100644
--- a/worlds/tloz/Rules.py
+++ b/worlds/tloz/Rules.py
@@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
from worlds.generic.Rules import add_rule
-from .Locations import food_locations, shop_locations
+from .Locations import food_locations, shop_locations, gleeok_locations
from .ItemPool import dangerous_weapon_locations
from .Options import StartingPosition
@@ -11,6 +11,7 @@
def set_rules(tloz_world: "TLoZWorld"):
player = tloz_world.player
world = tloz_world.multiworld
+ options = tloz_world.options
# Boss events for a nicer spoiler log play through
for level in range(1, 9):
@@ -23,10 +24,11 @@ def set_rules(tloz_world: "TLoZWorld"):
# No dungeons without weapons except for the dangerous weapon locations if we're dangerous, no unsafe dungeons
for i, level in enumerate(tloz_world.levels[1:10]):
for location in level.locations:
- if world.StartingPosition[player] < StartingPosition.option_dangerous \
+ if options.StartingPosition < StartingPosition.option_dangerous \
or location.name not in dangerous_weapon_locations:
add_rule(world.get_location(location.name, player),
lambda state: state.has_group("weapons", player))
+ # This part of the loop sets up an expected amount of defense needed for each dungeon
if i > 0: # Don't need an extra heart for Level 1
add_rule(world.get_location(location.name, player),
lambda state, hearts=i: state.has("Heart Container", player, hearts) or
@@ -48,7 +50,7 @@ def set_rules(tloz_world: "TLoZWorld"):
for location in level.locations:
add_rule(world.get_location(location.name, player),
lambda state: state.has_group("candles", player)
- or (state.has("Magical Rod", player) and state.has("Book", player)))
+ or (state.has("Magical Rod", player) and state.has("Book of Magic", player)))
# Everything from 5 on up has gaps
for level in tloz_world.levels[5:]:
@@ -66,7 +68,7 @@ def set_rules(tloz_world: "TLoZWorld"):
lambda state: state.has("Recorder", player))
add_rule(world.get_location("Level 7 Boss", player),
lambda state: state.has("Recorder", player))
- if world.ExpandedPool[player]:
+ if options.ExpandedPool:
add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player),
lambda state: state.has("Recorder", player))
add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player),
@@ -75,13 +77,22 @@ def set_rules(tloz_world: "TLoZWorld"):
lambda state: state.has("Recorder", player))
for location in food_locations:
- if world.ExpandedPool[player] or "Drop" not in location:
+ if options.ExpandedPool or "Drop" not in location:
add_rule(world.get_location(location, player),
lambda state: state.has("Food", player))
+ for location in gleeok_locations:
+ add_rule(world.get_location(location, player),
+ lambda state: state.has_group("swords", player) or state.has("Magical Rod", player))
+
+ # Candle access for Level 8
+ for location in tloz_world.levels[8].locations:
+ add_rule(world.get_location(location.name, player),
+ lambda state: state.has_group("candles", player))
+
add_rule(world.get_location("Level 8 Item (Magical Key)", player),
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
- if world.ExpandedPool[player]:
+ if options.ExpandedPool:
add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player),
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
@@ -106,13 +117,13 @@ def set_rules(tloz_world: "TLoZWorld"):
for location in stepladder_locations:
add_rule(world.get_location(location, player),
lambda state: state.has("Stepladder", player))
- if world.ExpandedPool[player]:
+ if options.ExpandedPool:
for location in stepladder_locations_expanded:
add_rule(world.get_location(location, player),
lambda state: state.has("Stepladder", player))
# Don't allow Take Any Items until we can actually get in one
- if world.ExpandedPool[player]:
+ if options.ExpandedPool:
add_rule(world.get_location("Take Any Item Left", player),
lambda state: state.has_group("candles", player) or
state.has("Raft", player))
diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py
index 20ab003ead61..8ea5f3e18ca1 100644
--- a/worlds/tloz/__init__.py
+++ b/worlds/tloz/__init__.py
@@ -12,8 +12,9 @@
from .ItemPool import generate_itempool, starting_weapons, dangerous_weapon_locations
from .Items import item_table, item_prices, item_game_ids
from .Locations import location_table, level_locations, major_locations, shop_locations, all_level_locations, \
- standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations
-from .Options import tloz_options
+ standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations, \
+ take_any_locations, sword_cave_locations
+from .Options import TlozOptions
from .Rom import TLoZDeltaPatch, get_base_rom_path, first_quest_dungeon_items_early, first_quest_dungeon_items_late
from .Rules import set_rules
from worlds.AutoWorld import World, WebWorld
@@ -45,7 +46,7 @@ class DisplayMsgs(settings.Bool):
class TLoZWeb(WebWorld):
theme = "stone"
setup = Tutorial(
- "Multiworld Setup Tutorial",
+ "Multiworld Setup Guide",
"A guide to setting up The Legend of Zelda for Archipelago on your computer.",
"English",
"multiworld_en.md",
@@ -63,11 +64,11 @@ class TLoZWorld(World):
This randomizer shuffles all the items in the game around, leading to a new adventure
every time.
"""
- option_definitions = tloz_options
+ options_dataclass = TlozOptions
+ options: TlozOptions
settings: typing.ClassVar[TLoZSettings]
game = "The Legend of Zelda"
topology_present = False
- data_version = 1
base_id = 7000
web = TLoZWeb()
@@ -87,6 +88,21 @@ class TLoZWorld(World):
}
}
+ location_name_groups = {
+ "Shops": set(shop_locations),
+ "Take Any": set(take_any_locations),
+ "Sword Caves": set(sword_cave_locations),
+ "Level 1": set(level_locations[0]),
+ "Level 2": set(level_locations[1]),
+ "Level 3": set(level_locations[2]),
+ "Level 4": set(level_locations[3]),
+ "Level 5": set(level_locations[4]),
+ "Level 6": set(level_locations[5]),
+ "Level 7": set(level_locations[6]),
+ "Level 8": set(level_locations[7]),
+ "Level 9": set(level_locations[8])
+ }
+
for k, v in item_name_to_id.items():
item_name_to_id[k] = v + base_id
@@ -115,7 +131,6 @@ def create_event(self, event: str):
def create_location(self, name, id, parent, event=False):
return_location = TLoZLocation(self.player, name, id, parent)
- return_location.event = event
return return_location
def create_regions(self):
@@ -132,7 +147,7 @@ def create_regions(self):
for i, level in enumerate(level_locations):
for location in level:
- if self.multiworld.ExpandedPool[self.player] or "Drop" not in location:
+ if self.options.ExpandedPool or "Drop" not in location:
self.levels[i + 1].locations.append(
self.create_location(location, self.location_name_to_id[location], self.levels[i + 1]))
@@ -144,7 +159,7 @@ def create_regions(self):
self.levels[level].locations.append(boss_event)
for location in major_locations:
- if self.multiworld.ExpandedPool[self.player] or "Take Any" not in location:
+ if self.options.ExpandedPool or "Take Any" not in location:
overworld.locations.append(
self.create_location(location, self.location_name_to_id[location], overworld))
@@ -179,7 +194,7 @@ def generate_basic(self):
self.multiworld.get_location("Zelda", self.player).place_locked_item(self.create_event("Rescued Zelda!"))
add_rule(self.multiworld.get_location("Zelda", self.player),
- lambda state: ganon in state.locations_checked)
+ lambda state: state.has("Triforce of Power", self.player))
self.multiworld.completion_condition[self.player] = lambda state: state.has("Rescued Zelda!", self.player)
def apply_base_patch(self, rom):
@@ -200,15 +215,17 @@ def apply_base_patch(self, rom):
for i in range(0, 0x7F):
item = rom_data[first_quest_dungeon_items_early + i]
if item & 0b00100000:
- rom_data[first_quest_dungeon_items_early + i] = item & 0b11011111
- rom_data[first_quest_dungeon_items_early + i] = item | 0b01000000
+ item = item & 0b11011111
+ item = item | 0b01000000
+ rom_data[first_quest_dungeon_items_early + i] = item
if item & 0b00011111 == 0b00000011: # Change all Item 03s to Item 3F, the proper "nothing"
rom_data[first_quest_dungeon_items_early + i] = item | 0b00111111
item = rom_data[first_quest_dungeon_items_late + i]
if item & 0b00100000:
- rom_data[first_quest_dungeon_items_late + i] = item & 0b11011111
- rom_data[first_quest_dungeon_items_late + i] = item | 0b01000000
+ item = item & 0b11011111
+ item = item | 0b01000000
+ rom_data[first_quest_dungeon_items_late + i] = item
if item & 0b00011111 == 0b00000011:
rom_data[first_quest_dungeon_items_late + i] = item | 0b00111111
return rom_data
@@ -258,11 +275,11 @@ def apply_randomizer(self):
rom_data[location_id] = item_id
# We shuffle the tiers of rupee caves. Caves that shared a value before still will.
- secret_caves = self.multiworld.per_slot_randoms[self.player].sample(sorted(secret_money_ids), 3)
+ secret_caves = self.random.sample(sorted(secret_money_ids), 3)
secret_cave_money_amounts = [20, 50, 100]
for i, amount in enumerate(secret_cave_money_amounts):
# Giving approximately double the money to keep grinding down
- amount = amount * self.multiworld.per_slot_randoms[self.player].triangular(1.5, 2.5)
+ amount = amount * self.random.triangular(1.5, 2.5)
secret_cave_money_amounts[i] = int(amount)
for i, cave in enumerate(secret_caves):
rom_data[secret_money_ids[cave]] = secret_cave_money_amounts[i]
@@ -306,10 +323,10 @@ def modify_multidata(self, multidata: dict):
def get_filler_item_name(self) -> str:
if self.filler_items is None:
self.filler_items = [item for item in item_table if item_table[item].classification == ItemClassification.filler]
- return self.multiworld.random.choice(self.filler_items)
+ return self.random.choice(self.filler_items)
def fill_slot_data(self) -> Dict[str, Any]:
- if self.multiworld.ExpandedPool[self.player]:
+ if self.options.ExpandedPool:
take_any_left = self.multiworld.get_location("Take Any Item Left", self.player).item
take_any_middle = self.multiworld.get_location("Take Any Item Middle", self.player).item
take_any_right = self.multiworld.get_location("Take Any Item Right", self.player).item
diff --git a/worlds/tloz/docs/en_The Legend of Zelda.md b/worlds/tloz/docs/en_The Legend of Zelda.md
index e443c9b95373..96b613673f00 100644
--- a/worlds/tloz/docs/en_The Legend of Zelda.md
+++ b/worlds/tloz/docs/en_The Legend of Zelda.md
@@ -1,8 +1,8 @@
# The Legend of Zelda (NES)
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -35,9 +35,16 @@ filler and useful items will cost less, and uncategorized items will be in the m
## Are there any other changes made?
-- The map and compass for each dungeon start already acquired, and other items can be found in their place.
+- The map and compass for each dungeon start already acquired, and other items can be found in their place.
- The Recorder will warp you between all eight levels regardless of Triforce count
- - It's possible for this to be your route to level 4!
+ - It's possible for this to be your route to level 4!
- Pressing Select will cycle through your inventory.
- Shop purchases are tracked within sessions, indicated by the item being elevated from its normal position.
-- What slots from a Take Any Cave have been chosen are similarly tracked.
\ No newline at end of file
+- What slots from a Take Any Cave have been chosen are similarly tracked.
+
+## Local Unique Commands
+
+The following commands are only available when using the Zelda1Client to play with Archipelago.
+
+- `/nes` Check NES Connection State
+- `/toggle_msgs` Toggle displaying messages in EmuHawk
diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md
index 581e8cf7b24e..366531e2e43a 100644
--- a/worlds/tloz/docs/multiworld_en.md
+++ b/worlds/tloz/docs/multiworld_en.md
@@ -6,6 +6,7 @@
- Bundled with Archipelago: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- The BizHawk emulator. Versions 2.3.1 and higher are supported.
- [BizHawk at TASVideos](https://tasvideos.org/BizHawk)
+- Your legally acquired US v1.0 PRG0 ROM file, probably named `Legend of Zelda, The (U) (PRG0) [!].nes`
## Optional Software
@@ -38,18 +39,18 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a config file?
-The Player Settings page on the website allows you to configure your personal settings and export a config file from
-them. Player settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legend%20of%20Zelda/player-settings)
+The Player Options page on the website allows you to configure your personal options and export a config file from
+them. Player options page: [The Legend of Zelda Player Sptions Page](/games/The%20Legend%20of%20Zelda/player-options)
### 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
-1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button.
- - Player Settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legend%20of%20Zelda/player-settings)
+1. Navigate to the Player Options page, configure your options, and click the "Generate Game" button.
+ - Player Options page: [The Legend of Zelda Player Options Page](/games/The%20Legend%20of%20Zelda/player-options)
2. You will be presented with a "Seed Info" page.
3. Click the "Create New Room" link.
4. You will be presented with a server page, from which you can download your patch file.
diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py
new file mode 100644
index 000000000000..5253e9951437
--- /dev/null
+++ b/worlds/tunic/__init__.py
@@ -0,0 +1,416 @@
+from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union
+from logging import warning
+from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld
+from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names
+from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations
+from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon
+from .er_rules import set_er_location_rules
+from .regions import tunic_regions
+from .er_scripts import create_er_regions
+from .er_data import portal_mapping
+from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections
+from worlds.AutoWorld import WebWorld, World
+from Options import PlandoConnection
+from decimal import Decimal, ROUND_HALF_UP
+from settings import Group, Bool
+
+
+class TunicSettings(Group):
+ class DisableLocalSpoiler(Bool):
+ """Disallows the TUNIC client from creating a local spoiler log."""
+
+ disable_local_spoiler: Union[DisableLocalSpoiler, bool] = False
+
+
+class TunicWeb(WebWorld):
+ tutorials = [
+ Tutorial(
+ tutorial_name="Multiworld Setup Guide",
+ description="A guide to setting up the TUNIC Randomizer for Archipelago multiworld games.",
+ language="English",
+ file_name="setup_en.md",
+ link="setup/en",
+ authors=["SilentDestroyer"]
+ )
+ ]
+ theme = "grassFlowers"
+ game = "TUNIC"
+ option_groups = tunic_option_groups
+ options_presets = tunic_option_presets
+
+
+class TunicItem(Item):
+ game: str = "TUNIC"
+
+
+class TunicLocation(Location):
+ game: str = "TUNIC"
+
+
+class SeedGroup(TypedDict):
+ logic_rules: int # logic rules value
+ laurels_at_10_fairies: bool # laurels location value
+ fixed_shop: bool # fixed shop value
+ plando: TunicPlandoConnections # consolidated of plando connections for the seed group
+
+
+class TunicWorld(World):
+ """
+ Explore a land filled with lost legends, ancient powers, and ferocious monsters in TUNIC, an isometric action game
+ about a small fox on a big adventure. Stranded on a mysterious beach, armed with only your own curiosity, you will
+ confront colossal beasts, collect strange and powerful items, and unravel long-lost secrets. Be brave, tiny fox!
+ """
+ game = "TUNIC"
+ web = TunicWeb()
+
+ options: TunicOptions
+ options_dataclass = TunicOptions
+ settings: ClassVar[TunicSettings]
+ item_name_groups = item_name_groups
+ location_name_groups = location_name_groups
+
+ item_name_to_id = item_name_to_id
+ location_name_to_id = location_name_to_id
+
+ ability_unlocks: Dict[str, int]
+ slot_data_items: List[TunicItem]
+ tunic_portal_pairs: Dict[str, str]
+ er_portal_hints: Dict[int, str]
+ seed_groups: Dict[str, SeedGroup] = {}
+
+ def generate_early(self) -> None:
+ if self.options.plando_connections:
+ for index, cxn in enumerate(self.options.plando_connections):
+ # making shops second to simplify other things later
+ if cxn.entrance.startswith("Shop"):
+ replacement = PlandoConnection(cxn.exit, "Shop Portal", "both")
+ self.options.plando_connections.value.remove(cxn)
+ self.options.plando_connections.value.insert(index, replacement)
+ elif cxn.exit.startswith("Shop"):
+ replacement = PlandoConnection(cxn.entrance, "Shop Portal", "both")
+ self.options.plando_connections.value.remove(cxn)
+ self.options.plando_connections.value.insert(index, replacement)
+
+ # Universal tracker stuff, shouldn't do anything in standard gen
+ if hasattr(self.multiworld, "re_gen_passthrough"):
+ if "TUNIC" in self.multiworld.re_gen_passthrough:
+ passthrough = self.multiworld.re_gen_passthrough["TUNIC"]
+ self.options.start_with_sword.value = passthrough["start_with_sword"]
+ self.options.keys_behind_bosses.value = passthrough["keys_behind_bosses"]
+ self.options.sword_progression.value = passthrough["sword_progression"]
+ self.options.ability_shuffling.value = passthrough["ability_shuffling"]
+ self.options.logic_rules.value = passthrough["logic_rules"]
+ self.options.lanternless.value = passthrough["lanternless"]
+ self.options.maskless.value = passthrough["maskless"]
+ self.options.hexagon_quest.value = passthrough["hexagon_quest"]
+ self.options.entrance_rando.value = passthrough["entrance_rando"]
+ self.options.shuffle_ladders.value = passthrough["shuffle_ladders"]
+ self.options.fixed_shop.value = self.options.fixed_shop.option_false
+ self.options.laurels_location.value = self.options.laurels_location.option_anywhere
+
+ @classmethod
+ def stage_generate_early(cls, multiworld: MultiWorld) -> None:
+ tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC")
+ for tunic in tunic_worlds:
+ # if it's one of the options, then it isn't a custom seed group
+ if tunic.options.entrance_rando.value in EntranceRando.options.values():
+ continue
+ group = tunic.options.entrance_rando.value
+ # if this is the first world in the group, set the rules equal to its rules
+ if group not in cls.seed_groups:
+ cls.seed_groups[group] = SeedGroup(logic_rules=tunic.options.logic_rules.value,
+ laurels_at_10_fairies=tunic.options.laurels_location == 3,
+ fixed_shop=bool(tunic.options.fixed_shop),
+ plando=tunic.options.plando_connections)
+ continue
+
+ # lower value is more restrictive
+ if tunic.options.logic_rules.value < cls.seed_groups[group]["logic_rules"]:
+ cls.seed_groups[group]["logic_rules"] = tunic.options.logic_rules.value
+ # laurels at 10 fairies changes logic for secret gathering place placement
+ if tunic.options.laurels_location == 3:
+ cls.seed_groups[group]["laurels_at_10_fairies"] = True
+ # fewer shops, one at windmill
+ if tunic.options.fixed_shop:
+ cls.seed_groups[group]["fixed_shop"] = True
+
+ if tunic.options.plando_connections:
+ # loop through the connections in the player's yaml
+ for cxn in tunic.options.plando_connections:
+ new_cxn = True
+ for group_cxn in cls.seed_groups[group]["plando"]:
+ # if neither entrance nor exit match anything in the group, add to group
+ if ((cxn.entrance == group_cxn.entrance and cxn.exit == group_cxn.exit)
+ or (cxn.exit == group_cxn.entrance and cxn.entrance == group_cxn.exit)):
+ new_cxn = False
+ break
+
+ # check if this pair is the same as a pair in the group already
+ is_mismatched = (
+ cxn.entrance == group_cxn.entrance and cxn.exit != group_cxn.exit
+ or cxn.entrance == group_cxn.exit and cxn.exit != group_cxn.entrance
+ or cxn.exit == group_cxn.entrance and cxn.entrance != group_cxn.exit
+ or cxn.exit == group_cxn.exit and cxn.entrance != group_cxn.entrance
+ )
+ if is_mismatched:
+ raise Exception(f"TUNIC: Conflict between seed group {group}'s plando "
+ f"connection {group_cxn.entrance} <-> {group_cxn.exit} and "
+ f"{tunic.multiworld.get_player_name(tunic.player)}'s plando "
+ f"connection {cxn.entrance} <-> {cxn.exit}")
+ if new_cxn:
+ cls.seed_groups[group]["plando"].value.append(cxn)
+
+ def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem:
+ item_data = item_table[name]
+ return TunicItem(name, classification or item_data.classification, self.item_name_to_id[name], self.player)
+
+ def create_items(self) -> None:
+
+ tunic_items: List[TunicItem] = []
+ self.slot_data_items = []
+
+ items_to_create: Dict[str, int] = {item: data.quantity_in_item_pool for item, data in item_table.items()}
+
+ for money_fool in fool_tiers[self.options.fool_traps]:
+ items_to_create["Fool Trap"] += items_to_create[money_fool]
+ items_to_create[money_fool] = 0
+
+ if self.options.start_with_sword:
+ self.multiworld.push_precollected(self.create_item("Sword"))
+
+ if self.options.sword_progression:
+ items_to_create["Stick"] = 0
+ items_to_create["Sword"] = 0
+ else:
+ items_to_create["Sword Upgrade"] = 0
+
+ if self.options.laurels_location:
+ laurels = self.create_item("Hero's Laurels")
+ if self.options.laurels_location == "6_coins":
+ self.multiworld.get_location("Coins in the Well - 6 Coins", self.player).place_locked_item(laurels)
+ elif self.options.laurels_location == "10_coins":
+ self.multiworld.get_location("Coins in the Well - 10 Coins", self.player).place_locked_item(laurels)
+ elif self.options.laurels_location == "10_fairies":
+ self.multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", self.player).place_locked_item(laurels)
+ items_to_create["Hero's Laurels"] = 0
+
+ if self.options.keys_behind_bosses:
+ for rgb_hexagon, location in hexagon_locations.items():
+ hex_item = self.create_item(gold_hexagon if self.options.hexagon_quest else rgb_hexagon)
+ self.multiworld.get_location(location, self.player).place_locked_item(hex_item)
+ items_to_create[rgb_hexagon] = 0
+ items_to_create[gold_hexagon] -= 3
+
+ # Filler items in the item pool
+ available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and
+ item_table[filler].classification == ItemClassification.filler]
+
+ # Remove filler to make room for other items
+ def remove_filler(amount: int) -> None:
+ for _ in range(amount):
+ if not available_filler:
+ fill = "Fool Trap"
+ else:
+ fill = self.random.choice(available_filler)
+ if items_to_create[fill] == 0:
+ raise Exception("No filler items left to accommodate options selected. Turn down fool trap amount.")
+ items_to_create[fill] -= 1
+ if items_to_create[fill] == 0:
+ available_filler.remove(fill)
+
+ if self.options.shuffle_ladders:
+ ladder_count = 0
+ for item_name, item_data in item_table.items():
+ if item_data.item_group == "Ladders":
+ items_to_create[item_name] = 1
+ ladder_count += 1
+ remove_filler(ladder_count)
+
+ if self.options.hexagon_quest:
+ # Calculate number of hexagons in item pool
+ hexagon_goal = self.options.hexagon_goal
+ extra_hexagons = self.options.extra_hexagon_percentage
+ items_to_create[gold_hexagon] += int((Decimal(100 + extra_hexagons) / 100 * hexagon_goal).to_integral_value(rounding=ROUND_HALF_UP))
+
+ # Replace pages and normal hexagons with filler
+ for replaced_item in list(filter(lambda item: "Pages" in item or item in hexagon_locations, items_to_create)):
+ filler_name = self.get_filler_item_name()
+ items_to_create[filler_name] += items_to_create[replaced_item]
+ if items_to_create[filler_name] >= 1 and filler_name not in available_filler:
+ available_filler.append(filler_name)
+ items_to_create[replaced_item] = 0
+
+ remove_filler(items_to_create[gold_hexagon])
+
+ for hero_relic in item_name_groups["Hero Relics"]:
+ tunic_items.append(self.create_item(hero_relic, ItemClassification.useful))
+ items_to_create[hero_relic] = 0
+
+ if not self.options.ability_shuffling:
+ for page in item_name_groups["Abilities"]:
+ if items_to_create[page] > 0:
+ tunic_items.append(self.create_item(page, ItemClassification.useful))
+ items_to_create[page] = 0
+
+ if self.options.maskless:
+ tunic_items.append(self.create_item("Scavenger Mask", ItemClassification.useful))
+ items_to_create["Scavenger Mask"] = 0
+
+ if self.options.lanternless:
+ tunic_items.append(self.create_item("Lantern", ItemClassification.useful))
+ items_to_create["Lantern"] = 0
+
+ for item, quantity in items_to_create.items():
+ for _ in range(quantity):
+ tunic_items.append(self.create_item(item))
+
+ for tunic_item in tunic_items:
+ if tunic_item.name in slot_data_item_names:
+ self.slot_data_items.append(tunic_item)
+
+ self.multiworld.itempool += tunic_items
+
+ def create_regions(self) -> None:
+ self.tunic_portal_pairs = {}
+ self.er_portal_hints = {}
+ self.ability_unlocks = randomize_ability_unlocks(self.random, self.options)
+
+ # stuff for universal tracker support, can be ignored for standard gen
+ if hasattr(self.multiworld, "re_gen_passthrough"):
+ if "TUNIC" in self.multiworld.re_gen_passthrough:
+ passthrough = self.multiworld.re_gen_passthrough["TUNIC"]
+ self.ability_unlocks["Pages 24-25 (Prayer)"] = passthrough["Hexagon Quest Prayer"]
+ self.ability_unlocks["Pages 42-43 (Holy Cross)"] = passthrough["Hexagon Quest Holy Cross"]
+ self.ability_unlocks["Pages 52-53 (Icebolt)"] = passthrough["Hexagon Quest Icebolt"]
+
+ # ladder rando uses ER with vanilla connections, so that we're not managing more rules files
+ if self.options.entrance_rando or self.options.shuffle_ladders:
+ portal_pairs = create_er_regions(self)
+ if self.options.entrance_rando:
+ # these get interpreted by the game to tell it which entrances to connect
+ for portal1, portal2 in portal_pairs.items():
+ self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination()
+ else:
+ # for non-ER, non-ladders
+ for region_name in tunic_regions:
+ region = Region(region_name, self.player, self.multiworld)
+ self.multiworld.regions.append(region)
+
+ for region_name, exits in tunic_regions.items():
+ region = self.multiworld.get_region(region_name, self.player)
+ region.add_exits(exits)
+
+ for location_name, location_id in self.location_name_to_id.items():
+ region = self.multiworld.get_region(location_table[location_name].region, self.player)
+ location = TunicLocation(self.player, location_name, location_id, region)
+ region.locations.append(location)
+
+ victory_region = self.multiworld.get_region("Spirit Arena", self.player)
+ victory_location = TunicLocation(self.player, "The Heir", None, victory_region)
+ victory_location.place_locked_item(TunicItem("Victory", ItemClassification.progression, None, self.player))
+ self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
+ victory_region.locations.append(victory_location)
+
+ def set_rules(self) -> None:
+ if self.options.entrance_rando or self.options.shuffle_ladders:
+ set_er_location_rules(self)
+ else:
+ set_region_rules(self)
+ set_location_rules(self)
+
+ def get_filler_item_name(self) -> str:
+ return self.random.choice(filler_items)
+
+ def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
+ if self.options.entrance_rando:
+ hint_data.update({self.player: {}})
+ # all state seems to have efficient paths
+ all_state = self.multiworld.get_all_state(True)
+ all_state.update_reachable_regions(self.player)
+ paths = all_state.path
+ portal_names = [portal.name for portal in portal_mapping]
+ for location in self.multiworld.get_locations(self.player):
+ # skipping event locations
+ if not location.address:
+ continue
+ path_to_loc = []
+ previous_name = "placeholder"
+ try:
+ name, connection = paths[location.parent_region]
+ except KeyError:
+ # logic bug, proceed with warning since it takes a long time to update AP
+ warning(f"{location.name} is not logically accessible for "
+ f"{self.multiworld.get_file_safe_player_name(self.player)}. "
+ "Creating entrance hint Inaccessible. "
+ "Please report this to the TUNIC rando devs.")
+ hint_text = "Inaccessible"
+ else:
+ while connection != ("Menu", None):
+ name, connection = connection
+ # for LS entrances, we just want to give the portal name
+ if "(LS)" in name:
+ name = name.split(" (LS) ", 1)[0]
+ # was getting some cases like Library Grave -> Library Grave -> other place
+ if name in portal_names and name != previous_name:
+ previous_name = name
+ path_to_loc.append(name)
+ hint_text = " -> ".join(reversed(path_to_loc))
+
+ if hint_text:
+ hint_data[self.player][location.address] = hint_text
+
+ def fill_slot_data(self) -> Dict[str, Any]:
+ slot_data: Dict[str, Any] = {
+ "seed": self.random.randint(0, 2147483647),
+ "start_with_sword": self.options.start_with_sword.value,
+ "keys_behind_bosses": self.options.keys_behind_bosses.value,
+ "sword_progression": self.options.sword_progression.value,
+ "ability_shuffling": self.options.ability_shuffling.value,
+ "hexagon_quest": self.options.hexagon_quest.value,
+ "fool_traps": self.options.fool_traps.value,
+ "logic_rules": self.options.logic_rules.value,
+ "lanternless": self.options.lanternless.value,
+ "maskless": self.options.maskless.value,
+ "entrance_rando": int(bool(self.options.entrance_rando.value)),
+ "shuffle_ladders": self.options.shuffle_ladders.value,
+ "Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"],
+ "Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"],
+ "Hexagon Quest Icebolt": self.ability_unlocks["Pages 52-53 (Icebolt)"],
+ "Hexagon Quest Goal": self.options.hexagon_goal.value,
+ "Entrance Rando": self.tunic_portal_pairs,
+ "disable_local_spoiler": int(self.settings.disable_local_spoiler or self.multiworld.is_race),
+ }
+
+ for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items):
+ if tunic_item.name not in slot_data:
+ slot_data[tunic_item.name] = []
+ if tunic_item.name == gold_hexagon and len(slot_data[gold_hexagon]) >= 6:
+ continue
+ slot_data[tunic_item.name].extend([tunic_item.location.name, tunic_item.location.player])
+
+ for start_item in self.options.start_inventory_from_pool:
+ if start_item in slot_data_item_names:
+ if start_item not in slot_data:
+ slot_data[start_item] = []
+ for _ in range(self.options.start_inventory_from_pool[start_item]):
+ slot_data[start_item].extend(["Your Pocket", self.player])
+
+ for plando_item in self.multiworld.plando_items[self.player]:
+ if plando_item["from_pool"]:
+ items_to_find = set()
+ for item_type in [key for key in ["item", "items"] if key in plando_item]:
+ for item in plando_item[item_type]:
+ items_to_find.add(item)
+ for item in items_to_find:
+ if item in slot_data_item_names:
+ slot_data[item] = []
+ for item_location in self.multiworld.find_item_locations(item, self.player):
+ slot_data[item].extend([item_location.name, item_location.player])
+
+ return slot_data
+
+ # for the universal tracker, doesn't get called in standard gen
+ @staticmethod
+ def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]:
+ # returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough
+ return slot_data
diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md
new file mode 100644
index 000000000000..27df4ce38be4
--- /dev/null
+++ b/worlds/tunic/docs/en_TUNIC.md
@@ -0,0 +1,94 @@
+# TUNIC
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
+
+## I haven't played TUNIC before.
+
+**Play vanilla first.** It is **_heavily discouraged_** to play this randomizer before playing the vanilla game.
+It is recommended that you achieve both endings in the vanilla game before playing the randomizer.
+
+## What does randomization do to this game?
+
+In the TUNIC Randomizer, every item in the game is randomized. All chests, key item pickups, instruction manual pages, hero relics,
+and other unique items are shuffled.
+
+Ability shuffling is an option available from the options page to shuffle certain abilities (prayer, holy cross, and the icebolt combo),
+preventing them from being used until they are unlocked.
+
+Entrances can also be randomized, shuffling the connections between every door, teleporter, etc. in the game.
+
+Enemy randomization and other options are also available and can be turned on in the client mod.
+
+## What is the goal of TUNIC when randomized?
+The standard goal is the same as the vanilla game. Find the three hexagon keys, then Take Your
+Rightful Place or seek another path and Share Your Wisdom.
+
+Alternatively, Hexagon Quest is a mode that shuffles a certain number of Gold Questagons into the item pool, with the goal
+being to find the required amount of them and then Share Your Wisdom.
+
+## What items from TUNIC can appear in another player's world?
+Every item has a chance to appear in another player's world.
+
+## How many checks are in TUNIC?
+There are 302 checks located across the world of TUNIC. The Fairy Seeking Spell can help you locate them.
+
+## What do items from other worlds look like in TUNIC?
+Items belonging to other TUNIC players will either appear as that item directly (if in a freestanding location) or in a
+chest with the original chest texture for that item.
+
+Items belonging to non-TUNIC players will either appear as a question-mark block (if in a freestanding location) or in a chest with
+a question mark symbol on it. Additionally, non-TUNIC items are color-coded by classification, with green for filler, blue for useful, and gold for progression.
+
+## Is there a tracker pack?
+There is a [tracker pack](https://github.com/SapphireSapphic/TunicTracker/releases/latest). It is compatible with both Poptracker and Emotracker. Using Poptracker, it will automatically track checked locations and important items received. It can also automatically tab between maps as you traverse the world. This tracker was originally created by SapphireSapphic and ScoutJD, and has been extensively updated by Br00ty.
+
+There is also a [standalone item tracker](https://github.com/radicoon/tunic-rando-tracker/releases/latest), which tracks what items you have received. It is great for adding an item overlay to streaming setups. This item tracker was created by Radicoon.
+
+There is an [entrance tracker](https://scipiowright.gitlab.io/tunic-tracker/) for the entrance randomizer. This is a manual tracker that runs in your browser. This tracker was created by ScipioWright, and is a fork of the Pokémon Tracker by [Sergi "Sekii" Santana](https://gitlab.com/Sekii/pokemon-tracker).
+
+You can also use the Universal Tracker (by Faris and qwint) to find a complete list of what checks are in logic with your current items. You can find it on the Archipelago Discord, in its post in the future-game-design channel. This tracker is an extension of the regular Archipelago Text Client.
+
+## What should I know regarding logic?
+In general:
+- Nighttime is not considered in logic. Every check in the game is obtainable during the day.
+- Bushes are not considered in logic. It is assumed that the player will find a way past them, whether it is with a sword, a bomb, fire, luring an enemy, etc. There is also an option in the in-game randomizer settings menu to clear some of the early bushes.
+- The Cathedral is accessible during the day by using the Hero's Laurels to reach the Overworld fuse near the Swamp entrance.
+- The Secret Legend chest at the Cathedral can be obtained during the day by opening the Holy Cross door from the outside.
+
+For the Entrance Randomizer:
+- Activating a fuse to turn on a yellow teleporter pad also activates its counterpart in the Far Shore.
+- The West Garden fuse can be activated from below.
+- You can pray at the tree at the exterior of the Library.
+- The elevators in the Rooted Ziggurat only go down.
+- The portal in the trophy room of the Old House is active from the start.
+- The elevator in Cathedral is immediately usable without activating the fuse. Activating the fuse does nothing.
+
+## Does this game have item and location groups?
+Yes! To find what they are, open up the Archipelago Text Client while connected to a TUNIC session and type in `/item_groups` or `/location_groups`.
+
+## Is Connection Plando supported?
+Yes. The host needs to enable it in their `host.yaml`, and the player's yaml needs to contain a plando_connections block.
+Example:
+```
+plando_connections:
+ - entrance: Stick House Entrance
+ exit: Stick House Exit
+ - entrance: Special Shop Exit
+ exit: Stairs to Top of the Mountain
+```
+Notes:
+- The Entrance Randomizer option must be enabled for it to work.
+- The `direction` field is not supported. Connections are always coupled.
+- For a list of entrance names, check `er_data.py` in the TUNIC world folder or generate a game with the Entrance Randomizer option enabled and check the spoiler log.
+- There is no limit to the number of Shops you can plando.
+- If you have more than one shop in a scene, you may be wrong warped when exiting a shop.
+- If you have a shop in every scene, and you have an odd number of shops, it will error out.
+
+See the [Archipelago Plando Guide](../../../tutorial/Archipelago/plando/en) for more information on Plando and Connection Plando.
+
+## Is there anything else I should know?
+You can go to [The TUNIC Randomizer Website](https://rando.tunic.run/) for a list of randomizer features as well as some helpful tips.
+You can use the Fairy Seeking Spell (ULU RDR) to locate the nearest unchecked location.
+You can use the Entrance Seeking Spell (RDR ULU) to locate the nearest unused entrance.
diff --git a/worlds/tunic/docs/setup_en.md b/worlds/tunic/docs/setup_en.md
new file mode 100644
index 000000000000..f60506795af1
--- /dev/null
+++ b/worlds/tunic/docs/setup_en.md
@@ -0,0 +1,71 @@
+# TUNIC Setup Guide
+
+## Required Software
+
+- [TUNIC](https://tunicgame.com/) for PC (Steam Deck also supported)
+- [TUNIC Randomizer Mod](https://github.com/silent-destroyer/tunic-randomizer/releases/latest)
+- [BepInEx 6.0.0-pre.1 (Unity IL2CPP x64)](https://github.com/BepInEx/BepInEx/releases/tag/v6.0.0-pre.1)
+
+## Optional Software
+- [TUNIC Randomizer Map Tracker](https://github.com/SapphireSapphic/TunicTracker/releases/latest)
+ - Requires [PopTracker](https://github.com/black-sliver/PopTracker/releases)
+- [TUNIC Randomizer Item Auto-tracker](https://github.com/radicoon/tunic-rando-tracker/releases/latest)
+- [Archipelago Text Client](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
+
+## Installation
+
+### Find Your Relevant Game Directories
+
+Find your TUNIC game installation directory:
+
+- **Steam**: Right click TUNIC in your Steam Library, then *Manage → Browse local files*.
+ - **Steam Deck**: Hold down the power button, tap "Switch to Desktop", then launch Steam from Desktop Mode to access the above option.
+- **PC Game Pass**: In the Xbox PC app, go to the TUNIC game page from your library, click the [...] button next to "Play", then
+*Manage → Files → Browse...*
+- **Other platforms**: Follow a similar pattern of steps as above to locate your specific game directory.
+
+### Install BepInEx
+
+BepInEx is a general purpose framework for modding Unity games, and is used to run the TUNIC Randomizer.
+
+Download [BepInEx 6.0.0-pre.1 (Unity IL2CPP x64)](https://github.com/BepInEx/BepInEx/releases/tag/v6.0.0-pre.1).
+
+If playing on Steam Deck, follow this [guide to set up BepInEx via Proton](https://docs.bepinex.dev/articles/advanced/proton_wine.html).
+
+If playing on Linux, you may be able to add `WINEDLLOVERRIDES="winhttp=n,b" %command%` to your Steam launch options. If this does not work, follow the guide for Steam Deck above.
+
+Extract the contents of the BepInEx .zip file into your TUNIC game directory:
+- **Steam**: Steam\steamapps\common\TUNIC
+- **PC Game Pass**: XboxGames\Tunic\Content
+- **Other platforms**: Place into the same folder that the Tunic_Data or Secret Legend_Data folder is found.
+
+Launch the game once and close it to finish the BepInEx installation.
+
+### Install The TUNIC Randomizer Mod
+
+Download the latest release of the [TUNIC Randomizer Mod](https://github.com/silent-destroyer/tunic-randomizer/releases/latest).
+
+Extract the contents of the downloaded .zip file, and find the folder labeled `Tunic Randomizer`.
+
+Copy the `Tunic Randomizer` folder into `BepInEx/plugins` in your TUNIC game installation directory.
+
+The filepath to the mod should look like `BepInEx/plugins/Tunic Randomizer/TunicRandomizer.dll`
+
+Launch the game, and if everything was installed correctly you should see `Randomizer Mod Ver. x.y.z` in the top left corner of the title screen!
+
+## Configure Archipelago Options
+
+### Configure Your YAML File
+
+Visit the [TUNIC options page](/games/TUNIC/player-options) to generate a YAML with your selected options.
+
+### Configure Your Mod Settings
+Launch the game, and using the menu on the Title Screen select `Archipelago` under `Randomizer Mode`.
+
+Click the button labeled `Edit AP Config`, and fill in *Player*, *Hostname*, *Port*, and *Password* (if required) with the correct information for your room.
+
+Once you've input your information, click the `Close` button. If everything was configured properly, you should see `Status: Connected!` and your chosen game options will be shown under `World Settings`.
+
+An error message will display if the game fails to connect to the server.
+
+Be sure to also look at the in-game options menu for a variety of additional settings, such as enemy randomization!
diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py
new file mode 100644
index 000000000000..f49e7dff3e58
--- /dev/null
+++ b/worlds/tunic/er_data.py
@@ -0,0 +1,1538 @@
+from typing import Dict, NamedTuple, List
+from enum import IntEnum
+
+
+class Portal(NamedTuple):
+ name: str # human-readable name
+ region: str # AP region
+ destination: str # vanilla destination scene
+ tag: str # vanilla tag
+
+ def scene(self) -> str: # the actual scene name in Tunic
+ return tunic_er_regions[self.region].game_scene
+
+ def scene_destination(self) -> str: # full, nonchanging name to interpret by the mod
+ return self.scene() + ", " + self.destination + self.tag
+
+ def destination_scene(self) -> str: # the vanilla connection
+ return self.destination + ", " + self.scene() + self.tag
+
+
+portal_mapping: List[Portal] = [
+ Portal(name="Stick House Entrance", region="Overworld",
+ destination="Sword Cave", tag="_"),
+ Portal(name="Windmill Entrance", region="Overworld",
+ destination="Windmill", tag="_"),
+ Portal(name="Well Ladder Entrance", region="Overworld Well Ladder",
+ destination="Sewer", tag="_entrance"),
+ Portal(name="Entrance to Well from Well Rail", region="Overworld Well to Furnace Rail",
+ destination="Sewer", tag="_west_aqueduct"),
+ Portal(name="Old House Door Entrance", region="Overworld Old House Door",
+ destination="Overworld Interiors", tag="_house"),
+ Portal(name="Old House Waterfall Entrance", region="Overworld",
+ destination="Overworld Interiors", tag="_under_checkpoint"),
+ Portal(name="Entrance to Furnace from Well Rail", region="Overworld Well to Furnace Rail",
+ destination="Furnace", tag="_gyro_upper_north"),
+ Portal(name="Entrance to Furnace under Windmill", region="Overworld",
+ destination="Furnace", tag="_gyro_upper_east"),
+ Portal(name="Entrance to Furnace near West Garden", region="Overworld to West Garden from Furnace",
+ destination="Furnace", tag="_gyro_west"),
+ Portal(name="Entrance to Furnace from Beach", region="Overworld Tunnel Turret",
+ destination="Furnace", tag="_gyro_lower"),
+ Portal(name="Caustic Light Cave Entrance", region="Overworld Swamp Lower Entry",
+ destination="Overworld Cave", tag="_"),
+ Portal(name="Swamp Upper Entrance", region="Overworld Swamp Upper Entry",
+ destination="Swamp Redux 2", tag="_wall"),
+ Portal(name="Swamp Lower Entrance", region="Overworld Swamp Lower Entry",
+ destination="Swamp Redux 2", tag="_conduit"),
+ Portal(name="Ruined Passage Not-Door Entrance", region="After Ruined Passage",
+ destination="Ruins Passage", tag="_east"),
+ Portal(name="Ruined Passage Door Entrance", region="Overworld Ruined Passage Door",
+ destination="Ruins Passage", tag="_west"),
+ Portal(name="Atoll Upper Entrance", region="Overworld to Atoll Upper",
+ destination="Atoll Redux", tag="_upper"),
+ Portal(name="Atoll Lower Entrance", region="Overworld Beach",
+ destination="Atoll Redux", tag="_lower"),
+ Portal(name="Special Shop Entrance", region="Overworld Special Shop Entry",
+ destination="ShopSpecial", tag="_"),
+ Portal(name="Maze Cave Entrance", region="Overworld Beach",
+ destination="Maze Room", tag="_"),
+ Portal(name="West Garden Entrance near Belltower", region="Overworld to West Garden Upper",
+ destination="Archipelagos Redux", tag="_upper"),
+ Portal(name="West Garden Entrance from Furnace", region="Overworld to West Garden from Furnace",
+ destination="Archipelagos Redux", tag="_lower"),
+ Portal(name="West Garden Laurels Entrance", region="Overworld West Garden Laurels Entry",
+ destination="Archipelagos Redux", tag="_lowest"),
+ Portal(name="Temple Door Entrance", region="Overworld Temple Door",
+ destination="Temple", tag="_main"),
+ Portal(name="Temple Rafters Entrance", region="Overworld after Temple Rafters",
+ destination="Temple", tag="_rafters"),
+ Portal(name="Ruined Shop Entrance", region="Overworld",
+ destination="Ruined Shop", tag="_"),
+ Portal(name="Patrol Cave Entrance", region="Overworld at Patrol Cave",
+ destination="PatrolCave", tag="_"),
+ Portal(name="Hourglass Cave Entrance", region="Overworld Beach",
+ destination="Town Basement", tag="_beach"),
+ Portal(name="Changing Room Entrance", region="Overworld",
+ destination="Changing Room", tag="_"),
+ Portal(name="Cube Cave Entrance", region="Overworld",
+ destination="CubeRoom", tag="_"),
+ Portal(name="Stairs from Overworld to Mountain", region="Upper Overworld",
+ destination="Mountain", tag="_"),
+ Portal(name="Overworld to Fortress", region="East Overworld",
+ destination="Fortress Courtyard", tag="_"),
+ Portal(name="Fountain HC Door Entrance", region="Overworld Fountain Cross Door",
+ destination="Town_FiligreeRoom", tag="_"),
+ Portal(name="Southeast HC Door Entrance", region="Overworld Southeast Cross Door",
+ destination="EastFiligreeCache", tag="_"),
+ Portal(name="Overworld to Quarry Connector", region="Overworld Quarry Entry",
+ destination="Darkwoods Tunnel", tag="_"),
+ Portal(name="Dark Tomb Main Entrance", region="Overworld",
+ destination="Crypt Redux", tag="_"),
+ Portal(name="Overworld to Forest Belltower", region="East Overworld",
+ destination="Forest Belltower", tag="_"),
+ Portal(name="Town to Far Shore", region="Overworld Town Portal",
+ destination="Transit", tag="_teleporter_town"),
+ Portal(name="Spawn to Far Shore", region="Overworld Spawn Portal",
+ destination="Transit", tag="_teleporter_starting island"),
+ Portal(name="Secret Gathering Place Entrance", region="Overworld",
+ destination="Waterfall", tag="_"),
+
+ Portal(name="Secret Gathering Place Exit", region="Secret Gathering Place",
+ destination="Overworld Redux", tag="_"),
+
+ Portal(name="Windmill Exit", region="Windmill",
+ destination="Overworld Redux", tag="_"),
+ Portal(name="Windmill Shop", region="Windmill",
+ destination="Shop", tag="_"),
+
+ Portal(name="Old House Door Exit", region="Old House Front",
+ destination="Overworld Redux", tag="_house"),
+ Portal(name="Old House to Glyph Tower", region="Old House Front",
+ destination="g_elements", tag="_"),
+ Portal(name="Old House Waterfall Exit", region="Old House Back",
+ destination="Overworld Redux", tag="_under_checkpoint"),
+
+ Portal(name="Glyph Tower Exit", region="Relic Tower",
+ destination="Overworld Interiors", tag="_"),
+
+ Portal(name="Changing Room Exit", region="Changing Room",
+ destination="Overworld Redux", tag="_"),
+
+ Portal(name="Fountain HC Room Exit", region="Fountain Cross Room",
+ destination="Overworld Redux", tag="_"),
+
+ Portal(name="Cube Cave Exit", region="Cube Cave",
+ destination="Overworld Redux", tag="_"),
+
+ Portal(name="Guard Patrol Cave Exit", region="Patrol Cave",
+ destination="Overworld Redux", tag="_"),
+
+ Portal(name="Ruined Shop Exit", region="Ruined Shop",
+ destination="Overworld Redux", tag="_"),
+
+ Portal(name="Furnace Exit towards Well", region="Furnace Fuse",
+ destination="Overworld Redux", tag="_gyro_upper_north"),
+ Portal(name="Furnace Exit to Dark Tomb", region="Furnace Walking Path",
+ destination="Crypt Redux", tag="_"),
+ Portal(name="Furnace Exit towards West Garden", region="Furnace Walking Path",
+ destination="Overworld Redux", tag="_gyro_west"),
+ Portal(name="Furnace Exit to Beach", region="Furnace Ladder Area",
+ destination="Overworld Redux", tag="_gyro_lower"),
+ Portal(name="Furnace Exit under Windmill", region="Furnace Ladder Area",
+ destination="Overworld Redux", tag="_gyro_upper_east"),
+
+ Portal(name="Stick House Exit", region="Stick House",
+ destination="Overworld Redux", tag="_"),
+
+ Portal(name="Ruined Passage Not-Door Exit", region="Ruined Passage",
+ destination="Overworld Redux", tag="_east"),
+ Portal(name="Ruined Passage Door Exit", region="Ruined Passage",
+ destination="Overworld Redux", tag="_west"),
+
+ Portal(name="Southeast HC Room Exit", region="Southeast Cross Room",
+ destination="Overworld Redux", tag="_"),
+
+ Portal(name="Caustic Light Cave Exit", region="Caustic Light Cave",
+ destination="Overworld Redux", tag="_"),
+
+ Portal(name="Maze Cave Exit", region="Maze Cave",
+ destination="Overworld Redux", tag="_"),
+
+ Portal(name="Hourglass Cave Exit", region="Hourglass Cave",
+ destination="Overworld Redux", tag="_beach"),
+
+ Portal(name="Special Shop Exit", region="Special Shop",
+ destination="Overworld Redux", tag="_"),
+
+ Portal(name="Temple Rafters Exit", region="Sealed Temple Rafters",
+ destination="Overworld Redux", tag="_rafters"),
+ Portal(name="Temple Door Exit", region="Sealed Temple",
+ destination="Overworld Redux", tag="_main"),
+
+ Portal(name="Well Ladder Exit", region="Beneath the Well Ladder Exit",
+ destination="Overworld Redux", tag="_entrance"),
+ Portal(name="Well to Well Boss", region="Beneath the Well Back",
+ destination="Sewer_Boss", tag="_"),
+ Portal(name="Well Exit towards Furnace", region="Beneath the Well Back",
+ destination="Overworld Redux", tag="_west_aqueduct"),
+
+ Portal(name="Well Boss to Well", region="Well Boss",
+ destination="Sewer", tag="_"),
+ Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint",
+ destination="Crypt Redux", tag="_"),
+
+ Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point",
+ destination="Overworld Redux", tag="_"),
+ Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit",
+ destination="Furnace", tag="_"),
+ Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point",
+ destination="Sewer_Boss", tag="_"),
+
+ Portal(name="West Garden Exit near Hero's Grave", region="West Garden",
+ destination="Overworld Redux", tag="_lower"),
+ Portal(name="West Garden to Magic Dagger House", region="West Garden",
+ destination="archipelagos_house", tag="_"),
+ Portal(name="West Garden Exit after Boss", region="West Garden after Boss",
+ destination="Overworld Redux", tag="_upper"),
+ Portal(name="West Garden Shop", region="West Garden",
+ destination="Shop", tag="_"),
+ Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit Region",
+ destination="Overworld Redux", tag="_lowest"),
+ Portal(name="West Garden Hero's Grave", region="West Garden Hero's Grave Region",
+ destination="RelicVoid", tag="_teleporter_relic plinth"),
+ Portal(name="West Garden to Far Shore", region="West Garden Portal",
+ destination="Transit", tag="_teleporter_archipelagos_teleporter"),
+
+ Portal(name="Magic Dagger House Exit", region="Magic Dagger House",
+ destination="Archipelagos Redux", tag="_"),
+
+ Portal(name="Atoll Upper Exit", region="Ruined Atoll",
+ destination="Overworld Redux", tag="_upper"),
+ Portal(name="Atoll Lower Exit", region="Ruined Atoll Lower Entry Area",
+ destination="Overworld Redux", tag="_lower"),
+ Portal(name="Atoll Shop", region="Ruined Atoll",
+ destination="Shop", tag="_"),
+ Portal(name="Atoll to Far Shore", region="Ruined Atoll Portal",
+ destination="Transit", tag="_teleporter_atoll"),
+ Portal(name="Atoll Statue Teleporter", region="Ruined Atoll Statue",
+ destination="Library Exterior", tag="_"),
+ Portal(name="Frog Stairs Eye Entrance", region="Ruined Atoll Frog Eye",
+ destination="Frog Stairs", tag="_eye"),
+ Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth",
+ destination="Frog Stairs", tag="_mouth"),
+
+ Portal(name="Frog Stairs Eye Exit", region="Frog Stairs Eye Exit",
+ destination="Atoll Redux", tag="_eye"),
+ Portal(name="Frog Stairs Mouth Exit", region="Frog Stairs Upper",
+ destination="Atoll Redux", tag="_mouth"),
+ Portal(name="Frog Stairs to Frog's Domain's Entrance", region="Frog Stairs to Frog's Domain",
+ destination="frog cave main", tag="_Entrance"),
+ Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog Stairs Lower",
+ destination="frog cave main", tag="_Exit"),
+
+ Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain Entry",
+ destination="Frog Stairs", tag="_Entrance"),
+ Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back",
+ destination="Frog Stairs", tag="_Exit"),
+
+ Portal(name="Library Exterior Tree", region="Library Exterior Tree Region",
+ destination="Atoll Redux", tag="_"),
+ Portal(name="Library Exterior Ladder", region="Library Exterior Ladder Region",
+ destination="Library Hall", tag="_"),
+
+ Portal(name="Library Hall Bookshelf Exit", region="Library Hall Bookshelf",
+ destination="Library Exterior", tag="_"),
+ Portal(name="Library Hero's Grave", region="Library Hero's Grave Region",
+ destination="RelicVoid", tag="_teleporter_relic plinth"),
+ Portal(name="Library Hall to Rotunda", region="Library Hall to Rotunda",
+ destination="Library Rotunda", tag="_"),
+
+ Portal(name="Library Rotunda Lower Exit", region="Library Rotunda to Hall",
+ destination="Library Hall", tag="_"),
+ Portal(name="Library Rotunda Upper Exit", region="Library Rotunda to Lab",
+ destination="Library Lab", tag="_"),
+
+ Portal(name="Library Lab to Rotunda", region="Library Lab Lower",
+ destination="Library Rotunda", tag="_"),
+ Portal(name="Library to Far Shore", region="Library Portal",
+ destination="Transit", tag="_teleporter_library teleporter"),
+ Portal(name="Library Lab to Librarian Arena", region="Library Lab to Librarian",
+ destination="Library Arena", tag="_"),
+
+ Portal(name="Librarian Arena Exit", region="Library Arena",
+ destination="Library Lab", tag="_"),
+
+ Portal(name="Forest to Belltower", region="East Forest",
+ destination="Forest Belltower", tag="_"),
+ Portal(name="Forest Guard House 1 Lower Entrance", region="East Forest",
+ destination="East Forest Redux Laddercave", tag="_lower"),
+ Portal(name="Forest Guard House 1 Gate Entrance", region="East Forest",
+ destination="East Forest Redux Laddercave", tag="_gate"),
+ Portal(name="Forest Dance Fox Outside Doorway", region="East Forest Dance Fox Spot",
+ destination="East Forest Redux Laddercave", tag="_upper"),
+ Portal(name="Forest to Far Shore", region="East Forest Portal",
+ destination="Transit", tag="_teleporter_forest teleporter"),
+ Portal(name="Forest Guard House 2 Lower Entrance", region="Lower Forest",
+ destination="East Forest Redux Interior", tag="_lower"),
+ Portal(name="Forest Guard House 2 Upper Entrance", region="East Forest",
+ destination="East Forest Redux Interior", tag="_upper"),
+ Portal(name="Forest Grave Path Lower Entrance", region="East Forest",
+ destination="Sword Access", tag="_lower"),
+ Portal(name="Forest Grave Path Upper Entrance", region="East Forest",
+ destination="Sword Access", tag="_upper"),
+
+ Portal(name="Guard House 1 Dance Fox Exit", region="Guard House 1 West",
+ destination="East Forest Redux", tag="_upper"),
+ Portal(name="Guard House 1 Lower Exit", region="Guard House 1 West",
+ destination="East Forest Redux", tag="_lower"),
+ Portal(name="Guard House 1 Upper Forest Exit", region="Guard House 1 East",
+ destination="East Forest Redux", tag="_gate"),
+ Portal(name="Guard House 1 to Guard Captain Room", region="Guard House 1 East",
+ destination="Forest Boss Room", tag="_"),
+
+ Portal(name="Forest Grave Path Upper Exit", region="Forest Grave Path Upper",
+ destination="East Forest Redux", tag="_upper"),
+ Portal(name="Forest Grave Path Lower Exit", region="Forest Grave Path Main",
+ destination="East Forest Redux", tag="_lower"),
+ Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave",
+ destination="RelicVoid", tag="_teleporter_relic plinth"),
+
+ Portal(name="Guard House 2 Lower Exit", region="Guard House 2 Lower",
+ destination="East Forest Redux", tag="_lower"),
+ Portal(name="Guard House 2 Upper Exit", region="Guard House 2 Upper",
+ destination="East Forest Redux", tag="_upper"),
+
+ Portal(name="Guard Captain Room Non-Gate Exit", region="Forest Boss Room",
+ destination="East Forest Redux Laddercave", tag="_"),
+ Portal(name="Guard Captain Room Gate Exit", region="Forest Boss Room",
+ destination="Forest Belltower", tag="_"),
+
+ Portal(name="Forest Belltower to Fortress", region="Forest Belltower Main",
+ destination="Fortress Courtyard", tag="_"),
+ Portal(name="Forest Belltower to Forest", region="Forest Belltower Lower",
+ destination="East Forest Redux", tag="_"),
+ Portal(name="Forest Belltower to Overworld", region="Forest Belltower Main",
+ destination="Overworld Redux", tag="_"),
+ Portal(name="Forest Belltower to Guard Captain Room", region="Forest Belltower Upper",
+ destination="Forest Boss Room", tag="_"),
+
+ Portal(name="Fortress Courtyard to Fortress Grave Path Lower", region="Fortress Courtyard",
+ destination="Fortress Reliquary", tag="_Lower"),
+ Portal(name="Fortress Courtyard to Fortress Grave Path Upper", region="Fortress Courtyard Upper",
+ destination="Fortress Reliquary", tag="_Upper"),
+ Portal(name="Fortress Courtyard to Fortress Interior", region="Fortress Courtyard",
+ destination="Fortress Main", tag="_Big Door"),
+ Portal(name="Fortress Courtyard to East Fortress", region="Fortress Courtyard Upper",
+ destination="Fortress East", tag="_"),
+ Portal(name="Fortress Courtyard to Beneath the Vault", region="Beneath the Vault Entry",
+ destination="Fortress Basement", tag="_"),
+ Portal(name="Fortress Courtyard to Forest Belltower", region="Fortress Exterior from East Forest",
+ destination="Forest Belltower", tag="_"),
+ Portal(name="Fortress Courtyard to Overworld", region="Fortress Exterior from Overworld",
+ destination="Overworld Redux", tag="_"),
+ Portal(name="Fortress Courtyard Shop", region="Fortress Exterior near cave",
+ destination="Shop", tag="_"),
+
+ Portal(name="Beneath the Vault to Fortress Interior", region="Beneath the Vault Back",
+ destination="Fortress Main", tag="_"),
+ Portal(name="Beneath the Vault to Fortress Courtyard", region="Beneath the Vault Ladder Exit",
+ destination="Fortress Courtyard", tag="_"),
+
+ Portal(name="Fortress Interior Main Exit", region="Eastern Vault Fortress",
+ destination="Fortress Courtyard", tag="_Big Door"),
+ Portal(name="Fortress Interior to Beneath the Earth", region="Eastern Vault Fortress",
+ destination="Fortress Basement", tag="_"),
+ Portal(name="Fortress Interior to Siege Engine Arena", region="Eastern Vault Fortress Gold Door",
+ destination="Fortress Arena", tag="_"),
+ Portal(name="Fortress Interior Shop", region="Eastern Vault Fortress",
+ destination="Shop", tag="_"),
+ Portal(name="Fortress Interior to East Fortress Upper", region="Eastern Vault Fortress",
+ destination="Fortress East", tag="_upper"),
+ Portal(name="Fortress Interior to East Fortress Lower", region="Eastern Vault Fortress",
+ destination="Fortress East", tag="_lower"),
+
+ Portal(name="East Fortress to Interior Lower", region="Fortress East Shortcut Lower",
+ destination="Fortress Main", tag="_lower"),
+ Portal(name="East Fortress to Courtyard", region="Fortress East Shortcut Upper",
+ destination="Fortress Courtyard", tag="_"),
+ Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper",
+ destination="Fortress Main", tag="_upper"),
+
+ Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path",
+ destination="Fortress Courtyard", tag="_Lower"),
+ Portal(name="Fortress Hero's Grave", region="Fortress Hero's Grave Region",
+ destination="RelicVoid", tag="_teleporter_relic plinth"),
+ Portal(name="Fortress Grave Path Upper Exit", region="Fortress Grave Path Upper",
+ destination="Fortress Courtyard", tag="_Upper"),
+ Portal(name="Fortress Grave Path Dusty Entrance", region="Fortress Grave Path Dusty Entrance Region",
+ destination="Dusty", tag="_"),
+
+ Portal(name="Dusty Exit", region="Fortress Leaf Piles",
+ destination="Fortress Reliquary", tag="_"),
+
+ Portal(name="Siege Engine Arena to Fortress", region="Fortress Arena",
+ destination="Fortress Main", tag="_"),
+ Portal(name="Fortress to Far Shore", region="Fortress Arena Portal",
+ destination="Transit", tag="_teleporter_spidertank"),
+
+ Portal(name="Stairs to Top of the Mountain", region="Lower Mountain Stairs",
+ destination="Mountaintop", tag="_"),
+ Portal(name="Mountain to Quarry", region="Lower Mountain",
+ destination="Quarry Redux", tag="_"),
+ Portal(name="Mountain to Overworld", region="Lower Mountain",
+ destination="Overworld Redux", tag="_"),
+
+ Portal(name="Top of the Mountain Exit", region="Top of the Mountain",
+ destination="Mountain", tag="_"),
+
+ Portal(name="Quarry Connector to Overworld", region="Quarry Connector",
+ destination="Overworld Redux", tag="_"),
+ Portal(name="Quarry Connector to Quarry", region="Quarry Connector",
+ destination="Quarry Redux", tag="_"),
+
+ Portal(name="Quarry to Overworld Exit", region="Quarry Entry",
+ destination="Darkwoods Tunnel", tag="_"),
+ Portal(name="Quarry Shop", region="Quarry Entry",
+ destination="Shop", tag="_"),
+ Portal(name="Quarry to Monastery Front", region="Quarry Monastery Entry",
+ destination="Monastery", tag="_front"),
+ Portal(name="Quarry to Monastery Back", region="Monastery Rope",
+ destination="Monastery", tag="_back"),
+ Portal(name="Quarry to Mountain", region="Quarry Back",
+ destination="Mountain", tag="_"),
+ Portal(name="Quarry to Ziggurat", region="Lower Quarry Zig Door",
+ destination="ziggurat2020_0", tag="_"),
+ Portal(name="Quarry to Far Shore", region="Quarry Portal",
+ destination="Transit", tag="_teleporter_quarry teleporter"),
+
+ Portal(name="Monastery Rear Exit", region="Monastery Back",
+ destination="Quarry Redux", tag="_back"),
+ Portal(name="Monastery Front Exit", region="Monastery Front",
+ destination="Quarry Redux", tag="_front"),
+ Portal(name="Monastery Hero's Grave", region="Monastery Hero's Grave Region",
+ destination="RelicVoid", tag="_teleporter_relic plinth"),
+
+ Portal(name="Ziggurat Entry Hallway to Ziggurat Upper", region="Rooted Ziggurat Entry",
+ destination="ziggurat2020_1", tag="_"),
+ Portal(name="Ziggurat Entry Hallway to Quarry", region="Rooted Ziggurat Entry",
+ destination="Quarry Redux", tag="_"),
+
+ Portal(name="Ziggurat Upper to Ziggurat Entry Hallway", region="Rooted Ziggurat Upper Entry",
+ destination="ziggurat2020_0", tag="_"),
+ Portal(name="Ziggurat Upper to Ziggurat Tower", region="Rooted Ziggurat Upper Back",
+ destination="ziggurat2020_2", tag="_"),
+
+ Portal(name="Ziggurat Tower to Ziggurat Upper", region="Rooted Ziggurat Middle Top",
+ destination="ziggurat2020_1", tag="_"),
+ Portal(name="Ziggurat Tower to Ziggurat Lower", region="Rooted Ziggurat Middle Bottom",
+ destination="ziggurat2020_3", tag="_"),
+
+ Portal(name="Ziggurat Lower to Ziggurat Tower", region="Rooted Ziggurat Lower Front",
+ destination="ziggurat2020_2", tag="_"),
+ Portal(name="Ziggurat Portal Room Entrance", region="Rooted Ziggurat Portal Room Entrance",
+ destination="ziggurat2020_FTRoom", tag="_"),
+ # only if fixed shop is on, removed otherwise
+ Portal(name="Ziggurat Lower Falling Entrance", region="Zig Skip Exit",
+ destination="ziggurat2020_1", tag="_zig2_skip"),
+
+ Portal(name="Ziggurat Portal Room Exit", region="Rooted Ziggurat Portal Room Exit",
+ destination="ziggurat2020_3", tag="_"),
+ Portal(name="Ziggurat to Far Shore", region="Rooted Ziggurat Portal",
+ destination="Transit", tag="_teleporter_ziggurat teleporter"),
+
+ Portal(name="Swamp Lower Exit", region="Swamp Front",
+ destination="Overworld Redux", tag="_conduit"),
+ Portal(name="Swamp to Cathedral Main Entrance", region="Swamp to Cathedral Main Entrance Region",
+ destination="Cathedral Redux", tag="_main"),
+ Portal(name="Swamp to Cathedral Secret Legend Room Entrance", region="Swamp to Cathedral Treasure Room",
+ destination="Cathedral Redux", tag="_secret"),
+ Portal(name="Swamp to Gauntlet", region="Back of Swamp",
+ destination="Cathedral Arena", tag="_"),
+ Portal(name="Swamp Shop", region="Swamp Front",
+ destination="Shop", tag="_"),
+ Portal(name="Swamp Upper Exit", region="Back of Swamp Laurels Area",
+ destination="Overworld Redux", tag="_wall"),
+ Portal(name="Swamp Hero's Grave", region="Swamp Hero's Grave Region",
+ destination="RelicVoid", tag="_teleporter_relic plinth"),
+
+ Portal(name="Cathedral Main Exit", region="Cathedral",
+ destination="Swamp Redux 2", tag="_main"),
+ Portal(name="Cathedral Elevator", region="Cathedral",
+ destination="Cathedral Arena", tag="_"),
+ Portal(name="Cathedral Secret Legend Room Exit", region="Cathedral Secret Legend Room",
+ destination="Swamp Redux 2", tag="_secret"),
+
+ Portal(name="Gauntlet to Swamp", region="Cathedral Gauntlet Exit",
+ destination="Swamp Redux 2", tag="_"),
+ Portal(name="Gauntlet Elevator", region="Cathedral Gauntlet Checkpoint",
+ destination="Cathedral Redux", tag="_"),
+ Portal(name="Gauntlet Shop", region="Cathedral Gauntlet Checkpoint",
+ destination="Shop", tag="_"),
+
+ Portal(name="Hero's Grave to Fortress", region="Hero Relic - Fortress",
+ destination="Fortress Reliquary", tag="_teleporter_relic plinth"),
+ Portal(name="Hero's Grave to Monastery", region="Hero Relic - Quarry",
+ destination="Monastery", tag="_teleporter_relic plinth"),
+ Portal(name="Hero's Grave to West Garden", region="Hero Relic - West Garden",
+ destination="Archipelagos Redux", tag="_teleporter_relic plinth"),
+ Portal(name="Hero's Grave to East Forest", region="Hero Relic - East Forest",
+ destination="Sword Access", tag="_teleporter_relic plinth"),
+ Portal(name="Hero's Grave to Library", region="Hero Relic - Library",
+ destination="Library Hall", tag="_teleporter_relic plinth"),
+ Portal(name="Hero's Grave to Swamp", region="Hero Relic - Swamp",
+ destination="Swamp Redux 2", tag="_teleporter_relic plinth"),
+
+ Portal(name="Far Shore to West Garden", region="Far Shore to West Garden Region",
+ destination="Archipelagos Redux", tag="_teleporter_archipelagos_teleporter"),
+ Portal(name="Far Shore to Library", region="Far Shore to Library Region",
+ destination="Library Lab", tag="_teleporter_library teleporter"),
+ Portal(name="Far Shore to Quarry", region="Far Shore to Quarry Region",
+ destination="Quarry Redux", tag="_teleporter_quarry teleporter"),
+ Portal(name="Far Shore to East Forest", region="Far Shore to East Forest Region",
+ destination="East Forest Redux", tag="_teleporter_forest teleporter"),
+ Portal(name="Far Shore to Fortress", region="Far Shore to Fortress Region",
+ destination="Fortress Arena", tag="_teleporter_spidertank"),
+ Portal(name="Far Shore to Atoll", region="Far Shore",
+ destination="Atoll Redux", tag="_teleporter_atoll"),
+ Portal(name="Far Shore to Ziggurat", region="Far Shore",
+ destination="ziggurat2020_FTRoom", tag="_teleporter_ziggurat teleporter"),
+ Portal(name="Far Shore to Heir", region="Far Shore",
+ destination="Spirit Arena", tag="_teleporter_spirit arena"),
+ Portal(name="Far Shore to Town", region="Far Shore",
+ destination="Overworld Redux", tag="_teleporter_town"),
+ Portal(name="Far Shore to Spawn", region="Far Shore to Spawn Region",
+ destination="Overworld Redux", tag="_teleporter_starting island"),
+
+ Portal(name="Heir Arena Exit", region="Spirit Arena",
+ destination="Transit", tag="_teleporter_spirit arena"),
+
+ Portal(name="Purgatory Bottom Exit", region="Purgatory",
+ destination="Purgatory", tag="_bottom"),
+ Portal(name="Purgatory Top Exit", region="Purgatory",
+ destination="Purgatory", tag="_top"),
+]
+
+
+class RegionInfo(NamedTuple):
+ game_scene: str # the name of the scene in the actual game
+ dead_end: int = 0 # if a region has only one exit
+
+
+class DeadEnd(IntEnum):
+ free = 0 # not a dead end
+ all_cats = 1 # dead end in every logic category
+ restricted = 2 # dead end only in restricted
+ special = 3 # special handling for secret gathering place and zig skip exit
+ # there's no dead ends that are only in unrestricted
+
+
+# key is the AP region name. "Fake" in region info just means the mod won't receive that info at all
+tunic_er_regions: Dict[str, RegionInfo] = {
+ "Menu": RegionInfo("Fake", dead_end=DeadEnd.all_cats),
+ "Overworld": RegionInfo("Overworld Redux"), # main overworld, the central area
+ "Overworld Holy Cross": RegionInfo("Fake", dead_end=DeadEnd.all_cats), # main overworld holy cross checks
+ "Overworld Belltower": RegionInfo("Overworld Redux"), # the area with the belltower and chest
+ "Overworld Belltower at Bell": RegionInfo("Overworld Redux"), # being able to ring the belltower, basically
+ "Overworld Swamp Upper Entry": RegionInfo("Overworld Redux"), # upper swamp entry spot
+ "Overworld Swamp Lower Entry": RegionInfo("Overworld Redux"), # lower swamp entrance, rotating lights entrance
+ "After Ruined Passage": RegionInfo("Overworld Redux"), # just the door and chest
+ "Above Ruined Passage": RegionInfo("Overworld Redux"), # one ladder up from ruined passage
+ "East Overworld": RegionInfo("Overworld Redux"), # where the east forest and fortress entrances are
+ "Overworld Special Shop Entry": RegionInfo("Overworld Redux"), # special shop entry spot
+ "Upper Overworld": RegionInfo("Overworld Redux"), # where the mountain stairs are
+ "Overworld above Quarry Entrance": RegionInfo("Overworld Redux"), # top of the ladder where the chest is
+ "Overworld after Temple Rafters": RegionInfo("Overworld Redux"), # the ledge after the rafters exit, before ladder
+ "Overworld Quarry Entry": RegionInfo("Overworld Redux"), # at the top of the ladder, to darkwoods
+ "Overworld after Envoy": RegionInfo("Overworld Redux"), # after the envoy on the thin bridge to quarry
+ "Overworld at Patrol Cave": RegionInfo("Overworld Redux"), # right at the patrol cave entrance
+ "Overworld above Patrol Cave": RegionInfo("Overworld Redux"), # where the hook is, and one ladder up from patrol
+ "Overworld West Garden Laurels Entry": RegionInfo("Overworld Redux"), # west garden laurels entry
+ "Overworld to West Garden Upper": RegionInfo("Overworld Redux"), # usually leads to garden knight
+ "Overworld to West Garden from Furnace": RegionInfo("Overworld Redux"), # isolated stairway with one chest
+ "Overworld Well Ladder": RegionInfo("Overworld Redux"), # just the ladder entrance itself as a region
+ "Overworld Beach": RegionInfo("Overworld Redux"), # from the two turrets to invisble maze, and lower atoll entry
+ "Overworld Tunnel Turret": RegionInfo("Overworld Redux"), # the tunnel turret by the southwest beach ladder
+ "Overworld to Atoll Upper": RegionInfo("Overworld Redux"), # the little ledge before the ladder
+ "Overworld Well to Furnace Rail": RegionInfo("Overworld Redux"), # the rail hallway, bane of unrestricted logic
+ "Overworld Ruined Passage Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal
+ "Overworld Old House Door": RegionInfo("Overworld Redux"), # the too-small space between the door and the portal
+ "Overworld Southeast Cross Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal
+ "Overworld Fountain Cross Door": RegionInfo("Overworld Redux"), # the small space between the door and the portal
+ "Overworld Temple Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal
+ "Overworld Town Portal": RegionInfo("Overworld Redux"), # being able to go to or come from the portal
+ "Overworld Spawn Portal": RegionInfo("Overworld Redux"), # being able to go to or come from the portal
+ "Stick House": RegionInfo("Sword Cave", dead_end=DeadEnd.all_cats),
+ "Windmill": RegionInfo("Windmill"),
+ "Old House Back": RegionInfo("Overworld Interiors"), # part with the hc door
+ "Old House Front": RegionInfo("Overworld Interiors"), # part with the bedroom
+ "Relic Tower": RegionInfo("g_elements", dead_end=DeadEnd.all_cats),
+ "Furnace Fuse": RegionInfo("Furnace"), # top of the furnace
+ "Furnace Ladder Area": RegionInfo("Furnace"), # the two portals accessible by the ladder
+ "Furnace Walking Path": RegionInfo("Furnace"), # dark tomb to west garden
+ "Secret Gathering Place": RegionInfo("Waterfall", dead_end=DeadEnd.special),
+ "Changing Room": RegionInfo("Changing Room", dead_end=DeadEnd.all_cats),
+ "Patrol Cave": RegionInfo("PatrolCave", dead_end=DeadEnd.all_cats),
+ "Ruined Shop": RegionInfo("Ruined Shop", dead_end=DeadEnd.all_cats),
+ "Ruined Passage": RegionInfo("Ruins Passage"),
+ "Special Shop": RegionInfo("ShopSpecial", dead_end=DeadEnd.all_cats),
+ "Caustic Light Cave": RegionInfo("Overworld Cave", dead_end=DeadEnd.all_cats),
+ "Maze Cave": RegionInfo("Maze Room", dead_end=DeadEnd.all_cats),
+ "Cube Cave": RegionInfo("CubeRoom", dead_end=DeadEnd.all_cats),
+ "Southeast Cross Room": RegionInfo("EastFiligreeCache", dead_end=DeadEnd.all_cats),
+ "Fountain Cross Room": RegionInfo("Town_FiligreeRoom", dead_end=DeadEnd.all_cats),
+ "Hourglass Cave": RegionInfo("Town Basement", dead_end=DeadEnd.all_cats),
+ "Hourglass Cave Tower": RegionInfo("Town Basement", dead_end=DeadEnd.all_cats), # top of the tower
+ "Sealed Temple": RegionInfo("Temple"),
+ "Sealed Temple Rafters": RegionInfo("Temple"),
+ "Forest Belltower Upper": RegionInfo("Forest Belltower"),
+ "Forest Belltower Main": RegionInfo("Forest Belltower"),
+ "Forest Belltower Lower": RegionInfo("Forest Belltower"),
+ "East Forest": RegionInfo("East Forest Redux"),
+ "East Forest Dance Fox Spot": RegionInfo("East Forest Redux"),
+ "East Forest Portal": RegionInfo("East Forest Redux"),
+ "Lower Forest": RegionInfo("East Forest Redux"), # bottom of the forest
+ "Guard House 1 East": RegionInfo("East Forest Redux Laddercave"),
+ "Guard House 1 West": RegionInfo("East Forest Redux Laddercave"),
+ "Guard House 2 Upper": RegionInfo("East Forest Redux Interior"),
+ "Guard House 2 Lower": RegionInfo("East Forest Redux Interior"),
+ "Forest Boss Room": RegionInfo("Forest Boss Room"),
+ "Forest Grave Path Main": RegionInfo("Sword Access"),
+ "Forest Grave Path Upper": RegionInfo("Sword Access"),
+ "Forest Grave Path by Grave": RegionInfo("Sword Access"),
+ "Forest Hero's Grave": RegionInfo("Sword Access"),
+ "Dark Tomb Entry Point": RegionInfo("Crypt Redux"), # both upper exits
+ "Dark Tomb Upper": RegionInfo("Crypt Redux"), # the part with the casket and the top of the ladder
+ "Dark Tomb Main": RegionInfo("Crypt Redux"),
+ "Dark Tomb Dark Exit": RegionInfo("Crypt Redux"),
+ "Dark Tomb Checkpoint": RegionInfo("Sewer_Boss"),
+ "Well Boss": RegionInfo("Sewer_Boss"),
+ "Beneath the Well Ladder Exit": RegionInfo("Sewer"), # just the ladder
+ "Beneath the Well Front": RegionInfo("Sewer"), # the front, to separate it from the weapon requirement in the mid
+ "Beneath the Well Main": RegionInfo("Sewer"), # the main section of it, requires a weapon
+ "Beneath the Well Back": RegionInfo("Sewer"), # the back two portals, and all 4 upper chests
+ "West Garden": RegionInfo("Archipelagos Redux"),
+ "Magic Dagger House": RegionInfo("archipelagos_house", dead_end=DeadEnd.all_cats),
+ "West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted),
+ "West Garden Portal Item": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted),
+ "West Garden Laurels Exit Region": RegionInfo("Archipelagos Redux"),
+ "West Garden after Boss": RegionInfo("Archipelagos Redux"),
+ "West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux"),
+ "Ruined Atoll": RegionInfo("Atoll Redux"),
+ "Ruined Atoll Lower Entry Area": RegionInfo("Atoll Redux"),
+ "Ruined Atoll Ladder Tops": RegionInfo("Atoll Redux"), # at the top of the 5 ladders in south Atoll
+ "Ruined Atoll Frog Mouth": RegionInfo("Atoll Redux"),
+ "Ruined Atoll Frog Eye": RegionInfo("Atoll Redux"),
+ "Ruined Atoll Portal": RegionInfo("Atoll Redux"),
+ "Ruined Atoll Statue": RegionInfo("Atoll Redux"),
+ "Frog Stairs Eye Exit": RegionInfo("Frog Stairs"),
+ "Frog Stairs Upper": RegionInfo("Frog Stairs"),
+ "Frog Stairs Lower": RegionInfo("Frog Stairs"),
+ "Frog Stairs to Frog's Domain": RegionInfo("Frog Stairs"),
+ "Frog's Domain Entry": RegionInfo("frog cave main"),
+ "Frog's Domain": RegionInfo("frog cave main"),
+ "Frog's Domain Back": RegionInfo("frog cave main"),
+ "Library Exterior Tree Region": RegionInfo("Library Exterior"),
+ "Library Exterior Ladder Region": RegionInfo("Library Exterior"),
+ "Library Hall Bookshelf": RegionInfo("Library Hall"),
+ "Library Hall": RegionInfo("Library Hall"),
+ "Library Hero's Grave Region": RegionInfo("Library Hall"),
+ "Library Hall to Rotunda": RegionInfo("Library Hall"),
+ "Library Rotunda to Hall": RegionInfo("Library Rotunda"),
+ "Library Rotunda": RegionInfo("Library Rotunda"),
+ "Library Rotunda to Lab": RegionInfo("Library Rotunda"),
+ "Library Lab": RegionInfo("Library Lab"),
+ "Library Lab Lower": RegionInfo("Library Lab"),
+ "Library Portal": RegionInfo("Library Lab"),
+ "Library Lab to Librarian": RegionInfo("Library Lab"),
+ "Library Arena": RegionInfo("Library Arena", dead_end=DeadEnd.all_cats),
+ "Fortress Exterior from East Forest": RegionInfo("Fortress Courtyard"),
+ "Fortress Exterior from Overworld": RegionInfo("Fortress Courtyard"),
+ "Fortress Exterior near cave": RegionInfo("Fortress Courtyard"), # where the shop and beneath the earth entry are
+ "Beneath the Vault Entry": RegionInfo("Fortress Courtyard"),
+ "Fortress Courtyard": RegionInfo("Fortress Courtyard"),
+ "Fortress Courtyard Upper": RegionInfo("Fortress Courtyard"),
+ "Beneath the Vault Ladder Exit": RegionInfo("Fortress Basement"),
+ "Beneath the Vault Main": RegionInfo("Fortress Basement"), # the vanilla entry point
+ "Beneath the Vault Back": RegionInfo("Fortress Basement"), # the vanilla exit point
+ "Eastern Vault Fortress": RegionInfo("Fortress Main"),
+ "Eastern Vault Fortress Gold Door": RegionInfo("Fortress Main"),
+ "Fortress East Shortcut Upper": RegionInfo("Fortress East"),
+ "Fortress East Shortcut Lower": RegionInfo("Fortress East"),
+ "Fortress Grave Path": RegionInfo("Fortress Reliquary"),
+ "Fortress Grave Path Upper": RegionInfo("Fortress Reliquary", dead_end=DeadEnd.restricted),
+ "Fortress Grave Path Dusty Entrance Region": RegionInfo("Fortress Reliquary"),
+ "Fortress Hero's Grave Region": RegionInfo("Fortress Reliquary"),
+ "Fortress Leaf Piles": RegionInfo("Dusty", dead_end=DeadEnd.all_cats),
+ "Fortress Arena": RegionInfo("Fortress Arena"),
+ "Fortress Arena Portal": RegionInfo("Fortress Arena"),
+ "Lower Mountain": RegionInfo("Mountain"),
+ "Lower Mountain Stairs": RegionInfo("Mountain"),
+ "Top of the Mountain": RegionInfo("Mountaintop", dead_end=DeadEnd.all_cats),
+ "Quarry Connector": RegionInfo("Darkwoods Tunnel"),
+ "Quarry Entry": RegionInfo("Quarry Redux"),
+ "Quarry": RegionInfo("Quarry Redux"),
+ "Quarry Portal": RegionInfo("Quarry Redux"),
+ "Quarry Back": RegionInfo("Quarry Redux"),
+ "Quarry Monastery Entry": RegionInfo("Quarry Redux"),
+ "Monastery Front": RegionInfo("Monastery"),
+ "Monastery Back": RegionInfo("Monastery"),
+ "Monastery Hero's Grave Region": RegionInfo("Monastery"),
+ "Monastery Rope": RegionInfo("Quarry Redux"),
+ "Lower Quarry": RegionInfo("Quarry Redux"),
+ "Even Lower Quarry": RegionInfo("Quarry Redux"),
+ "Lower Quarry Zig Door": RegionInfo("Quarry Redux"),
+ "Rooted Ziggurat Entry": RegionInfo("ziggurat2020_0"),
+ "Rooted Ziggurat Upper Entry": RegionInfo("ziggurat2020_1"),
+ "Rooted Ziggurat Upper Front": RegionInfo("ziggurat2020_1"),
+ "Rooted Ziggurat Upper Back": RegionInfo("ziggurat2020_1"), # after the administrator
+ "Rooted Ziggurat Middle Top": RegionInfo("ziggurat2020_2"),
+ "Rooted Ziggurat Middle Bottom": RegionInfo("ziggurat2020_2"),
+ "Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the vanilla entry point side
+ "Rooted Ziggurat Lower Back": RegionInfo("ziggurat2020_3"), # the boss side
+ "Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special), # the exit from zig skip, for use with fixed shop on
+ "Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3"), # the door itself on the zig 3 side
+ "Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom"),
+ "Rooted Ziggurat Portal Room Exit": RegionInfo("ziggurat2020_FTRoom"),
+ "Swamp Front": RegionInfo("Swamp Redux 2"), # from the main entry to the top of the ladder after south
+ "Swamp Mid": RegionInfo("Swamp Redux 2"), # from the bottom of the ladder to the cathedral door
+ "Swamp Ledge under Cathedral Door": RegionInfo("Swamp Redux 2"), # the ledge with the chest and secret door
+ "Swamp to Cathedral Treasure Room": RegionInfo("Swamp Redux 2"), # just the door
+ "Swamp to Cathedral Main Entrance Region": RegionInfo("Swamp Redux 2"), # just the door
+ "Back of Swamp": RegionInfo("Swamp Redux 2"), # the area with hero grave and gauntlet entrance
+ "Swamp Hero's Grave Region": RegionInfo("Swamp Redux 2"),
+ "Back of Swamp Laurels Area": RegionInfo("Swamp Redux 2"), # the spots you need laurels to traverse
+ "Cathedral": RegionInfo("Cathedral Redux"),
+ "Cathedral Secret Legend Room": RegionInfo("Cathedral Redux", dead_end=DeadEnd.all_cats),
+ "Cathedral Gauntlet Checkpoint": RegionInfo("Cathedral Arena"),
+ "Cathedral Gauntlet": RegionInfo("Cathedral Arena"),
+ "Cathedral Gauntlet Exit": RegionInfo("Cathedral Arena"),
+ "Far Shore": RegionInfo("Transit"),
+ "Far Shore to Spawn Region": RegionInfo("Transit"),
+ "Far Shore to East Forest Region": RegionInfo("Transit"),
+ "Far Shore to Quarry Region": RegionInfo("Transit"),
+ "Far Shore to Fortress Region": RegionInfo("Transit"),
+ "Far Shore to Library Region": RegionInfo("Transit"),
+ "Far Shore to West Garden Region": RegionInfo("Transit"),
+ "Hero Relic - Fortress": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats),
+ "Hero Relic - Quarry": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats),
+ "Hero Relic - West Garden": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats),
+ "Hero Relic - East Forest": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats),
+ "Hero Relic - Library": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats),
+ "Hero Relic - Swamp": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats),
+ "Purgatory": RegionInfo("Purgatory"),
+ "Shop": RegionInfo("Shop", dead_end=DeadEnd.all_cats),
+ "Spirit Arena": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats),
+ "Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats)
+}
+
+
+traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
+ "Overworld": {
+ "Overworld Beach":
+ [],
+ "Overworld to Atoll Upper":
+ [["Hyperdash"]],
+ "Overworld Belltower":
+ [["Hyperdash"], ["UR"]],
+ "Overworld Swamp Upper Entry":
+ [["Hyperdash"], ["UR"]],
+ "Overworld Swamp Lower Entry":
+ [],
+ "Overworld Special Shop Entry":
+ [["Hyperdash"], ["UR"]],
+ "Overworld Well Ladder":
+ [],
+ "Overworld Ruined Passage Door":
+ [],
+ "After Ruined Passage":
+ [],
+ "Above Ruined Passage":
+ [],
+ "East Overworld":
+ [],
+ "Overworld above Patrol Cave":
+ [],
+ "Overworld above Quarry Entrance":
+ [],
+ "Overworld after Envoy":
+ [],
+ "Overworld Quarry Entry":
+ [["NMG"]],
+ "Overworld Tunnel Turret":
+ [["NMG"], ["Hyperdash"]],
+ "Overworld Temple Door":
+ [["NMG"], ["Forest Belltower Upper", "Overworld Belltower"]],
+ "Overworld Southeast Cross Door":
+ [],
+ "Overworld Fountain Cross Door":
+ [],
+ "Overworld Town Portal":
+ [],
+ "Overworld Spawn Portal":
+ [],
+ "Overworld Well to Furnace Rail":
+ [["UR"]],
+ "Overworld Old House Door":
+ [],
+ },
+ "East Overworld": {
+ "Above Ruined Passage":
+ [],
+ "After Ruined Passage":
+ [["NMG"]],
+ "Overworld":
+ [],
+ "Overworld at Patrol Cave":
+ [],
+ "Overworld above Patrol Cave":
+ [],
+ "Overworld Special Shop Entry":
+ [["Hyperdash"], ["UR"]]
+ },
+ "Overworld Special Shop Entry": {
+ "East Overworld":
+ [["Hyperdash"]]
+ },
+ "Overworld Belltower": {
+ "Overworld Belltower at Bell":
+ [],
+ "Overworld":
+ [],
+ "Overworld to West Garden Upper":
+ [],
+ },
+ "Overworld to West Garden Upper": {
+ "Overworld Belltower":
+ [],
+ },
+ "Overworld Swamp Upper Entry": {
+ "Overworld":
+ [],
+ },
+ "Overworld Swamp Lower Entry": {
+ "Overworld":
+ [],
+ },
+ "Overworld Beach": {
+ "Overworld":
+ [],
+ "Overworld West Garden Laurels Entry":
+ [["Hyperdash"]],
+ "Overworld to Atoll Upper":
+ [],
+ "Overworld Tunnel Turret":
+ [],
+ },
+ "Overworld West Garden Laurels Entry": {
+ "Overworld Beach":
+ [["Hyperdash"]],
+ },
+ "Overworld to Atoll Upper": {
+ "Overworld":
+ [],
+ "Overworld Beach":
+ [],
+ },
+ "Overworld Tunnel Turret": {
+ "Overworld":
+ [],
+ "Overworld Beach":
+ [],
+ },
+ "Overworld Well Ladder": {
+ "Overworld":
+ [],
+ },
+ "Overworld at Patrol Cave": {
+ "East Overworld":
+ [["Hyperdash"]],
+ "Overworld above Patrol Cave":
+ [],
+ },
+ "Overworld above Patrol Cave": {
+ "Overworld":
+ [],
+ "East Overworld":
+ [],
+ "Upper Overworld":
+ [],
+ "Overworld at Patrol Cave":
+ [],
+ "Overworld Belltower at Bell":
+ [["NMG"]],
+ },
+ "Upper Overworld": {
+ "Overworld above Patrol Cave":
+ [],
+ "Overworld above Quarry Entrance":
+ [],
+ "Overworld after Temple Rafters":
+ [],
+ },
+ "Overworld after Temple Rafters": {
+ "Upper Overworld":
+ [],
+ },
+ "Overworld above Quarry Entrance": {
+ "Overworld":
+ [],
+ "Upper Overworld":
+ [],
+ },
+ "Overworld Quarry Entry": {
+ "Overworld after Envoy":
+ [],
+ "Overworld":
+ [["NMG"]],
+ },
+ "Overworld after Envoy": {
+ "Overworld":
+ [],
+ "Overworld Quarry Entry":
+ [],
+ },
+ "After Ruined Passage": {
+ "Overworld":
+ [],
+ "Above Ruined Passage":
+ [],
+ "East Overworld":
+ [["NMG"]],
+ },
+ "Above Ruined Passage": {
+ "Overworld":
+ [],
+ "After Ruined Passage":
+ [],
+ "East Overworld":
+ [],
+ },
+ "Overworld Ruined Passage Door": {
+ "Overworld":
+ [["Hyperdash", "NMG"]],
+ },
+ "Overworld Town Portal": {
+ "Overworld":
+ [],
+ },
+ "Overworld Spawn Portal": {
+ "Overworld":
+ [],
+ },
+ "Old House Front": {
+ "Old House Back":
+ [],
+ },
+ "Old House Back": {
+ "Old House Front":
+ [["Hyperdash", "NMG"]],
+ },
+ "Furnace Fuse": {
+ "Furnace Ladder Area":
+ [["Hyperdash"]],
+ },
+ "Furnace Ladder Area": {
+ "Furnace Fuse":
+ [["Hyperdash"], ["UR"]],
+ "Furnace Walking Path":
+ [["Hyperdash"], ["UR"]],
+ },
+ "Furnace Walking Path": {
+ "Furnace Ladder Area":
+ [["Hyperdash"]],
+ },
+ "Sealed Temple": {
+ "Sealed Temple Rafters":
+ [],
+ },
+ "Sealed Temple Rafters": {
+ "Sealed Temple":
+ [["Hyperdash"]],
+ },
+ "Hourglass Cave": {
+ "Hourglass Cave Tower":
+ [],
+ },
+ "Forest Belltower Upper": {
+ "Forest Belltower Main":
+ [],
+ },
+ "Forest Belltower Main": {
+ "Forest Belltower Lower":
+ [],
+ },
+ "East Forest": {
+ "East Forest Dance Fox Spot":
+ [["Hyperdash"], ["NMG"]],
+ "East Forest Portal":
+ [],
+ "Lower Forest":
+ [],
+ },
+ "East Forest Dance Fox Spot": {
+ "East Forest":
+ [["Hyperdash"], ["NMG"]],
+ },
+ "East Forest Portal": {
+ "East Forest":
+ [],
+ },
+ "Lower Forest": {
+ "East Forest":
+ [],
+ },
+ "Guard House 1 East": {
+ "Guard House 1 West":
+ [],
+ },
+ "Guard House 1 West": {
+ "Guard House 1 East":
+ [["Hyperdash"], ["UR"]],
+ },
+ "Guard House 2 Upper": {
+ "Guard House 2 Lower":
+ [],
+ },
+ "Guard House 2 Lower": {
+ "Guard House 2 Upper":
+ [],
+ },
+ "Forest Grave Path Main": {
+ "Forest Grave Path Upper":
+ [["Hyperdash"], ["UR"]],
+ "Forest Grave Path by Grave":
+ [],
+ },
+ "Forest Grave Path Upper": {
+ "Forest Grave Path Main":
+ [["Hyperdash"], ["NMG"]],
+ },
+ "Forest Grave Path by Grave": {
+ "Forest Hero's Grave":
+ [],
+ "Forest Grave Path Main":
+ [["NMG"]],
+ },
+ "Forest Hero's Grave": {
+ "Forest Grave Path by Grave":
+ [],
+ },
+ "Beneath the Well Ladder Exit": {
+ "Beneath the Well Front":
+ [],
+ },
+ "Beneath the Well Front": {
+ "Beneath the Well Ladder Exit":
+ [],
+ "Beneath the Well Main":
+ [],
+ },
+ "Beneath the Well Main": {
+ "Beneath the Well Front":
+ [],
+ "Beneath the Well Back":
+ [],
+ },
+ "Beneath the Well Back": {
+ "Beneath the Well Main":
+ [],
+ },
+ "Well Boss": {
+ "Dark Tomb Checkpoint":
+ [],
+ },
+ "Dark Tomb Checkpoint": {
+ "Well Boss":
+ [["Hyperdash", "NMG"]],
+ },
+ "Dark Tomb Entry Point": {
+ "Dark Tomb Upper":
+ [],
+ },
+ "Dark Tomb Upper": {
+ "Dark Tomb Entry Point":
+ [],
+ "Dark Tomb Main":
+ [],
+ },
+ "Dark Tomb Main": {
+ "Dark Tomb Upper":
+ [],
+ "Dark Tomb Dark Exit":
+ [],
+ },
+ "Dark Tomb Dark Exit": {
+ "Dark Tomb Main":
+ [],
+ },
+ "West Garden": {
+ "West Garden Laurels Exit Region":
+ [["Hyperdash"], ["UR"]],
+ "West Garden after Boss":
+ [],
+ "West Garden Hero's Grave Region":
+ [],
+ "West Garden Portal Item":
+ [["NMG"]],
+ },
+ "West Garden Laurels Exit Region": {
+ "West Garden":
+ [["Hyperdash"]],
+ },
+ "West Garden after Boss": {
+ "West Garden":
+ [["Hyperdash"]],
+ },
+ "West Garden Portal Item": {
+ "West Garden":
+ [["NMG"]],
+ "West Garden Portal":
+ [["Hyperdash", "West Garden"]],
+ },
+ "West Garden Portal": {
+ "West Garden Portal Item":
+ [["Hyperdash"]],
+ },
+ "West Garden Hero's Grave Region": {
+ "West Garden":
+ [],
+ },
+ "Ruined Atoll": {
+ "Ruined Atoll Lower Entry Area":
+ [["Hyperdash"], ["UR"]],
+ "Ruined Atoll Ladder Tops":
+ [],
+ "Ruined Atoll Frog Mouth":
+ [],
+ "Ruined Atoll Frog Eye":
+ [],
+ "Ruined Atoll Portal":
+ [],
+ "Ruined Atoll Statue":
+ [],
+ },
+ "Ruined Atoll Lower Entry Area": {
+ "Ruined Atoll":
+ [],
+ },
+ "Ruined Atoll Ladder Tops": {
+ "Ruined Atoll":
+ [],
+ },
+ "Ruined Atoll Frog Mouth": {
+ "Ruined Atoll":
+ [],
+ },
+ "Ruined Atoll Frog Eye": {
+ "Ruined Atoll":
+ [],
+ },
+ "Ruined Atoll Portal": {
+ "Ruined Atoll":
+ [],
+ },
+ "Ruined Atoll Statue": {
+ "Ruined Atoll":
+ [],
+ },
+ "Frog Stairs Eye Exit": {
+ "Frog Stairs Upper":
+ [],
+ },
+ "Frog Stairs Upper": {
+ "Frog Stairs Eye Exit":
+ [],
+ "Frog Stairs Lower":
+ [],
+ },
+ "Frog Stairs Lower": {
+ "Frog Stairs Upper":
+ [],
+ "Frog Stairs to Frog's Domain":
+ [],
+ },
+ "Frog Stairs to Frog's Domain": {
+ "Frog Stairs Lower":
+ [],
+ },
+ "Frog's Domain Entry": {
+ "Frog's Domain":
+ [],
+ },
+ "Frog's Domain": {
+ "Frog's Domain Entry":
+ [],
+ "Frog's Domain Back":
+ [],
+ },
+ "Library Exterior Ladder Region": {
+ "Library Exterior Tree Region":
+ [],
+ },
+ "Library Exterior Tree Region": {
+ "Library Exterior Ladder Region":
+ [],
+ },
+ "Library Hall Bookshelf": {
+ "Library Hall":
+ [],
+ },
+ "Library Hall": {
+ "Library Hall Bookshelf":
+ [],
+ "Library Hero's Grave Region":
+ [],
+ },
+ "Library Hero's Grave Region": {
+ "Library Hall":
+ [],
+ },
+ "Library Hall to Rotunda": {
+ "Library Hall":
+ [],
+ },
+ "Library Rotunda to Hall": {
+ "Library Rotunda":
+ [],
+ },
+ "Library Rotunda": {
+ "Library Rotunda to Hall":
+ [],
+ "Library Rotunda to Lab":
+ [],
+ },
+ "Library Rotunda to Lab": {
+ "Library Rotunda":
+ [],
+ },
+
+ "Library Lab Lower": {
+ "Library Lab":
+ [],
+ },
+ "Library Lab": {
+ "Library Lab Lower":
+ [["Hyperdash"]],
+ "Library Portal":
+ [],
+ "Library Lab to Librarian":
+ [],
+ },
+ "Library Portal": {
+ "Library Lab":
+ [],
+ },
+ "Library Lab to Librarian": {
+ "Library Lab":
+ [],
+ },
+ "Fortress Exterior from East Forest": {
+ "Fortress Exterior from Overworld":
+ [],
+ "Fortress Courtyard Upper":
+ [["UR"]],
+ "Fortress Exterior near cave":
+ [["UR"]],
+ "Fortress Courtyard":
+ [["UR"]],
+ },
+ "Fortress Exterior from Overworld": {
+ "Fortress Exterior from East Forest":
+ [["Hyperdash"]],
+ "Fortress Exterior near cave":
+ [],
+ "Fortress Courtyard":
+ [["Hyperdash"], ["NMG"]],
+ },
+ "Fortress Exterior near cave": {
+ "Fortress Exterior from Overworld":
+ [["Hyperdash"], ["UR"]],
+ "Fortress Courtyard":
+ [["UR"]],
+ "Fortress Courtyard Upper":
+ [["UR"]],
+ "Beneath the Vault Entry":
+ [],
+ },
+ "Beneath the Vault Entry": {
+ "Fortress Exterior near cave":
+ [],
+ },
+ "Fortress Courtyard": {
+ "Fortress Courtyard Upper":
+ [["NMG"]],
+ "Fortress Exterior from Overworld":
+ [["Hyperdash"]],
+ },
+ "Fortress Courtyard Upper": {
+ "Fortress Courtyard":
+ [],
+ },
+ "Beneath the Vault Ladder Exit": {
+ "Beneath the Vault Main":
+ [],
+ },
+ "Beneath the Vault Main": {
+ "Beneath the Vault Ladder Exit":
+ [],
+ "Beneath the Vault Back":
+ [],
+ },
+ "Beneath the Vault Back": {
+ "Beneath the Vault Main":
+ [],
+ "Beneath the Vault Ladder Exit":
+ [],
+ },
+ "Fortress East Shortcut Lower": {
+ "Fortress East Shortcut Upper":
+ [["NMG"]],
+ },
+ "Fortress East Shortcut Upper": {
+ "Fortress East Shortcut Lower":
+ [],
+ },
+ "Eastern Vault Fortress": {
+ "Eastern Vault Fortress Gold Door":
+ [["NMG"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]],
+ },
+ "Eastern Vault Fortress Gold Door": {
+ "Eastern Vault Fortress":
+ [["NMG"]],
+ },
+ "Fortress Grave Path": {
+ "Fortress Hero's Grave Region":
+ [],
+ "Fortress Grave Path Dusty Entrance Region":
+ [["Hyperdash"]],
+ },
+ "Fortress Grave Path Upper": {
+ "Fortress Grave Path":
+ [["NMG"]],
+ },
+ "Fortress Grave Path Dusty Entrance Region": {
+ "Fortress Grave Path":
+ [["Hyperdash"]],
+ },
+ "Fortress Hero's Grave Region": {
+ "Fortress Grave Path":
+ [],
+ },
+ "Fortress Arena": {
+ "Fortress Arena Portal":
+ [["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]],
+ },
+ "Fortress Arena Portal": {
+ "Fortress Arena":
+ [],
+ },
+ "Lower Mountain": {
+ "Lower Mountain Stairs":
+ [],
+ },
+ "Lower Mountain Stairs": {
+ "Lower Mountain":
+ [],
+ },
+ "Monastery Back": {
+ "Monastery Front":
+ [["Hyperdash", "NMG"]],
+ "Monastery Hero's Grave Region":
+ [],
+ },
+ "Monastery Hero's Grave Region": {
+ "Monastery Back":
+ [],
+ },
+ "Monastery Front": {
+ "Monastery Back":
+ [],
+ },
+ "Quarry Entry": {
+ "Quarry Portal":
+ [["Quarry Connector"]],
+ "Quarry":
+ [],
+ },
+ "Quarry Portal": {
+ "Quarry Entry":
+ [],
+ },
+ "Quarry Monastery Entry": {
+ "Quarry":
+ [],
+ "Quarry Back":
+ [["Hyperdash"]],
+ "Monastery Rope":
+ [["UR"]],
+ },
+ "Quarry Back": {
+ "Quarry":
+ [],
+ "Quarry Monastery Entry":
+ [["Hyperdash"]],
+ },
+ "Quarry": {
+ "Lower Quarry":
+ [],
+ "Quarry Entry":
+ [],
+ "Quarry Back":
+ [],
+ "Quarry Monastery Entry":
+ [],
+ "Lower Quarry Zig Door":
+ [["NMG"]],
+ },
+ "Lower Quarry": {
+ "Even Lower Quarry":
+ [],
+ },
+ "Even Lower Quarry": {
+ "Lower Quarry":
+ [],
+ "Lower Quarry Zig Door":
+ [["Quarry", "Quarry Connector"], ["NMG"]],
+ },
+ "Monastery Rope": {
+ "Quarry Back":
+ [],
+ },
+ "Rooted Ziggurat Upper Entry": {
+ "Rooted Ziggurat Upper Front":
+ [],
+ },
+ "Rooted Ziggurat Upper Front": {
+ "Rooted Ziggurat Upper Back":
+ [],
+ },
+ "Rooted Ziggurat Upper Back": {
+ "Rooted Ziggurat Upper Front":
+ [["Hyperdash"]],
+ },
+ "Rooted Ziggurat Middle Top": {
+ "Rooted Ziggurat Middle Bottom":
+ [],
+ },
+ "Rooted Ziggurat Lower Front": {
+ "Rooted Ziggurat Lower Back":
+ [],
+ },
+ "Rooted Ziggurat Lower Back": {
+ "Rooted Ziggurat Lower Front":
+ [["Hyperdash"], ["UR"]],
+ "Rooted Ziggurat Portal Room Entrance":
+ [],
+ },
+ "Zig Skip Exit": {
+ "Rooted Ziggurat Lower Front":
+ [],
+ },
+ "Rooted Ziggurat Portal Room Entrance": {
+ "Rooted Ziggurat Lower Back":
+ [],
+ },
+ "Rooted Ziggurat Portal Room Exit": {
+ "Rooted Ziggurat Portal":
+ [],
+ },
+ "Rooted Ziggurat Portal": {
+ "Rooted Ziggurat Portal Room Exit":
+ [["Rooted Ziggurat Lower Back"]],
+ },
+ "Swamp Front": {
+ "Swamp Mid":
+ [],
+ },
+ "Swamp Mid": {
+ "Swamp Front":
+ [],
+ "Swamp to Cathedral Main Entrance Region":
+ [["Hyperdash"], ["NMG"]],
+ "Swamp Ledge under Cathedral Door":
+ [],
+ "Back of Swamp":
+ [["UR"]],
+ },
+ "Swamp Ledge under Cathedral Door": {
+ "Swamp Mid":
+ [],
+ "Swamp to Cathedral Treasure Room":
+ [],
+ },
+ "Swamp to Cathedral Treasure Room": {
+ "Swamp Ledge under Cathedral Door":
+ [],
+ },
+ "Swamp to Cathedral Main Entrance Region": {
+ "Swamp Mid":
+ [["NMG"]],
+ },
+ "Back of Swamp": {
+ "Back of Swamp Laurels Area":
+ [["Hyperdash"], ["UR"]],
+ "Swamp Hero's Grave Region":
+ [],
+ },
+ "Back of Swamp Laurels Area": {
+ "Back of Swamp":
+ [["Hyperdash"]],
+ "Swamp Mid":
+ [["NMG", "Hyperdash"]],
+ },
+ "Swamp Hero's Grave Region": {
+ "Back of Swamp":
+ [],
+ },
+ "Cathedral Gauntlet Checkpoint": {
+ "Cathedral Gauntlet":
+ [],
+ },
+ "Cathedral Gauntlet": {
+ "Cathedral Gauntlet Exit":
+ [["Hyperdash"]],
+ },
+ "Cathedral Gauntlet Exit": {
+ "Cathedral Gauntlet":
+ [["Hyperdash"]],
+ },
+ "Far Shore": {
+ "Far Shore to Spawn Region":
+ [["Hyperdash"]],
+ "Far Shore to East Forest Region":
+ [["Hyperdash"]],
+ "Far Shore to Quarry Region":
+ [["Quarry Connector", "Quarry"]],
+ "Far Shore to Library Region":
+ [["Library Lab"]],
+ "Far Shore to West Garden Region":
+ [["West Garden"]],
+ "Far Shore to Fortress Region":
+ [["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]],
+ },
+ "Far Shore to Spawn Region": {
+ "Far Shore":
+ [["Hyperdash"]],
+ },
+ "Far Shore to East Forest Region": {
+ "Far Shore":
+ [["Hyperdash"]],
+ },
+ "Far Shore to Quarry Region": {
+ "Far Shore":
+ [],
+ },
+ "Far Shore to Library Region": {
+ "Far Shore":
+ [],
+ },
+ "Far Shore to West Garden Region": {
+ "Far Shore":
+ [],
+ },
+ "Far Shore to Fortress Region": {
+ "Far Shore":
+ [],
+ },
+}
diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py
new file mode 100644
index 000000000000..81e9d48b4afc
--- /dev/null
+++ b/worlds/tunic/er_rules.py
@@ -0,0 +1,1539 @@
+from typing import Dict, Set, List, Tuple, TYPE_CHECKING
+from worlds.generic.Rules import set_rule, forbid_item
+from .rules import has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage
+from .er_data import Portal
+from BaseClasses import Region, CollectionState
+
+if TYPE_CHECKING:
+ from . import TunicWorld
+
+laurels = "Hero's Laurels"
+grapple = "Magic Orb"
+ice_dagger = "Magic Dagger"
+fire_wand = "Magic Wand"
+lantern = "Lantern"
+fairies = "Fairy"
+coins = "Golden Coin"
+prayer = "Pages 24-25 (Prayer)"
+holy_cross = "Pages 42-43 (Holy Cross)"
+icebolt = "Pages 52-53 (Icebolt)"
+key = "Key"
+house_key = "Old House Key"
+vault_key = "Fortress Vault Key"
+mask = "Scavenger Mask"
+red_hexagon = "Red Questagon"
+green_hexagon = "Green Questagon"
+blue_hexagon = "Blue Questagon"
+gold_hexagon = "Gold Questagon"
+
+
+def has_ladder(ladder: str, state: CollectionState, world: "TunicWorld") -> bool:
+ return not world.options.shuffle_ladders or state.has(ladder, world.player)
+
+
+def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_pairs: Dict[Portal, Portal]) -> None:
+ player = world.player
+ options = world.options
+
+ regions["Menu"].connect(
+ connecting_region=regions["Overworld"])
+
+ # Overworld
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Holy Cross"],
+ rule=lambda state: has_ability(holy_cross, state, world))
+
+ # grapple on the west side, down the stairs from moss wall, across from ruined shop
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Beach"],
+ rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
+ or state.has_any({laurels, grapple}, player))
+ regions["Overworld Beach"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
+ or state.has_any({laurels, grapple}, player))
+
+ regions["Overworld Beach"].connect(
+ connecting_region=regions["Overworld West Garden Laurels Entry"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Overworld West Garden Laurels Entry"].connect(
+ connecting_region=regions["Overworld Beach"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Overworld Beach"].connect(
+ connecting_region=regions["Overworld to Atoll Upper"],
+ rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, world))
+ regions["Overworld to Atoll Upper"].connect(
+ connecting_region=regions["Overworld Beach"],
+ rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, world))
+
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld to Atoll Upper"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Overworld to Atoll Upper"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: state.has_any({laurels, grapple}, player))
+
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Belltower"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Overworld Belltower"].connect(
+ connecting_region=regions["Overworld"])
+
+ regions["Overworld Belltower"].connect(
+ connecting_region=regions["Overworld to West Garden Upper"],
+ rule=lambda state: has_ladder("Ladders to West Bell", state, world))
+ regions["Overworld to West Garden Upper"].connect(
+ connecting_region=regions["Overworld Belltower"],
+ rule=lambda state: has_ladder("Ladders to West Bell", state, world))
+
+ regions["Overworld Belltower"].connect(
+ connecting_region=regions["Overworld Belltower at Bell"],
+ rule=lambda state: has_ladder("Ladders to West Bell", state, world))
+
+ # long dong, do not make a reverse connection here or to belltower
+ regions["Overworld above Patrol Cave"].connect(
+ connecting_region=regions["Overworld Belltower at Bell"],
+ rule=lambda state: options.logic_rules and state.has(fire_wand, player))
+
+ # nmg: can laurels through the ruined passage door
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Ruined Passage Door"],
+ rule=lambda state: state.has(key, player, 2)
+ or (state.has(laurels, player) and options.logic_rules))
+ regions["Overworld Ruined Passage Door"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: state.has(laurels, player) and options.logic_rules)
+
+ regions["Overworld"].connect(
+ connecting_region=regions["After Ruined Passage"],
+ rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
+ or has_ice_grapple_logic(True, state, world))
+ regions["After Ruined Passage"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: has_ladder("Ladders near Weathervane", state, world))
+
+ regions["Overworld"].connect(
+ connecting_region=regions["Above Ruined Passage"],
+ rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
+ or state.has(laurels, player))
+ regions["Above Ruined Passage"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
+ or state.has(laurels, player))
+
+ regions["After Ruined Passage"].connect(
+ connecting_region=regions["Above Ruined Passage"],
+ rule=lambda state: has_ladder("Ladders near Weathervane", state, world))
+ regions["Above Ruined Passage"].connect(
+ connecting_region=regions["After Ruined Passage"],
+ rule=lambda state: has_ladder("Ladders near Weathervane", state, world))
+
+ regions["Above Ruined Passage"].connect(
+ connecting_region=regions["East Overworld"],
+ rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
+ or has_ice_grapple_logic(True, state, world))
+ regions["East Overworld"].connect(
+ connecting_region=regions["Above Ruined Passage"],
+ rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
+ or state.has(laurels, player))
+
+ # nmg: ice grapple the slimes, works both ways consistently
+ regions["East Overworld"].connect(
+ connecting_region=regions["After Ruined Passage"],
+ rule=lambda state: has_ice_grapple_logic(True, state, world))
+ regions["After Ruined Passage"].connect(
+ connecting_region=regions["East Overworld"],
+ rule=lambda state: has_ice_grapple_logic(True, state, world))
+
+ regions["Overworld"].connect(
+ connecting_region=regions["East Overworld"],
+ rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)
+ or has_ice_grapple_logic(True, state, world))
+ regions["East Overworld"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world))
+
+ regions["East Overworld"].connect(
+ connecting_region=regions["Overworld at Patrol Cave"])
+ regions["Overworld at Patrol Cave"].connect(
+ connecting_region=regions["East Overworld"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Overworld at Patrol Cave"].connect(
+ connecting_region=regions["Overworld above Patrol Cave"],
+ rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world)
+ or has_ice_grapple_logic(True, state, world))
+ regions["Overworld above Patrol Cave"].connect(
+ connecting_region=regions["Overworld at Patrol Cave"],
+ rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world))
+
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld above Patrol Cave"],
+ rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)
+ or state.has(grapple, player))
+ regions["Overworld above Patrol Cave"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world))
+
+ regions["East Overworld"].connect(
+ connecting_region=regions["Overworld above Patrol Cave"],
+ rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)
+ or has_ice_grapple_logic(True, state, world))
+ regions["Overworld above Patrol Cave"].connect(
+ connecting_region=regions["East Overworld"],
+ rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world))
+
+ regions["Overworld above Patrol Cave"].connect(
+ connecting_region=regions["Upper Overworld"],
+ rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world)
+ or has_ice_grapple_logic(True, state, world))
+ regions["Upper Overworld"].connect(
+ connecting_region=regions["Overworld above Patrol Cave"],
+ rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world)
+ or state.has(grapple, player))
+
+ regions["Upper Overworld"].connect(
+ connecting_region=regions["Overworld above Quarry Entrance"],
+ rule=lambda state: state.has_any({grapple, laurels}, player))
+ regions["Overworld above Quarry Entrance"].connect(
+ connecting_region=regions["Upper Overworld"],
+ rule=lambda state: state.has_any({grapple, laurels}, player))
+
+ regions["Upper Overworld"].connect(
+ connecting_region=regions["Overworld after Temple Rafters"],
+ rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world))
+ regions["Overworld after Temple Rafters"].connect(
+ connecting_region=regions["Upper Overworld"],
+ rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world)
+ or has_ice_grapple_logic(True, state, world))
+
+ regions["Overworld above Quarry Entrance"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: has_ladder("Ladders near Dark Tomb", state, world))
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld above Quarry Entrance"],
+ rule=lambda state: has_ladder("Ladders near Dark Tomb", state, world))
+
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld after Envoy"],
+ rule=lambda state: state.has_any({laurels, grapple}, player)
+ or state.has("Sword Upgrade", player, 4)
+ or options.logic_rules)
+ regions["Overworld after Envoy"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: state.has_any({laurels, grapple}, player)
+ or state.has("Sword Upgrade", player, 4)
+ or options.logic_rules)
+
+ regions["Overworld after Envoy"].connect(
+ connecting_region=regions["Overworld Quarry Entry"],
+ rule=lambda state: has_ladder("Ladder to Quarry", state, world))
+ regions["Overworld Quarry Entry"].connect(
+ connecting_region=regions["Overworld after Envoy"],
+ rule=lambda state: has_ladder("Ladder to Quarry", state, world))
+
+ # ice grapple through the gate
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Quarry Entry"],
+ rule=lambda state: has_ice_grapple_logic(False, state, world))
+ regions["Overworld Quarry Entry"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: has_ice_grapple_logic(False, state, world))
+
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Swamp Upper Entry"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Overworld Swamp Upper Entry"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Swamp Lower Entry"],
+ rule=lambda state: has_ladder("Ladder to Swamp", state, world))
+ regions["Overworld Swamp Lower Entry"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: has_ladder("Ladder to Swamp", state, world))
+
+ regions["East Overworld"].connect(
+ connecting_region=regions["Overworld Special Shop Entry"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Overworld Special Shop Entry"].connect(
+ connecting_region=regions["East Overworld"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Well Ladder"],
+ rule=lambda state: has_ladder("Ladders in Well", state, world))
+ regions["Overworld Well Ladder"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: has_ladder("Ladders in Well", state, world))
+
+ # nmg: can ice grapple through the door
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Old House Door"],
+ rule=lambda state: state.has(house_key, player)
+ or has_ice_grapple_logic(False, state, world))
+
+ # not including ice grapple through this because it's very tedious to get an enemy here
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Southeast Cross Door"],
+ rule=lambda state: has_ability(holy_cross, state, world))
+ regions["Overworld Southeast Cross Door"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: has_ability(holy_cross, state, world))
+
+ # not including ice grapple through this because we're not including it on the other door
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Fountain Cross Door"],
+ rule=lambda state: has_ability(holy_cross, state, world))
+ regions["Overworld Fountain Cross Door"].connect(
+ connecting_region=regions["Overworld"])
+
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Town Portal"],
+ rule=lambda state: has_ability(prayer, state, world))
+ regions["Overworld Town Portal"].connect(
+ connecting_region=regions["Overworld"])
+
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Spawn Portal"],
+ rule=lambda state: has_ability(prayer, state, world))
+ regions["Overworld Spawn Portal"].connect(
+ connecting_region=regions["Overworld"])
+
+ # nmg: ice grapple through temple door
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Temple Door"],
+ rule=lambda state: state.has_all({"Ring Eastern Bell", "Ring Western Bell"}, player)
+ or has_ice_grapple_logic(False, state, world))
+
+ regions["Overworld Temple Door"].connect(
+ connecting_region=regions["Overworld above Patrol Cave"],
+ rule=lambda state: state.has(grapple, player))
+
+ regions["Overworld Tunnel Turret"].connect(
+ connecting_region=regions["Overworld Beach"],
+ rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
+ or state.has(grapple, player))
+ regions["Overworld Beach"].connect(
+ connecting_region=regions["Overworld Tunnel Turret"],
+ rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
+ or has_ice_grapple_logic(True, state, world))
+
+ regions["Overworld"].connect(
+ connecting_region=regions["Overworld Tunnel Turret"],
+ rule=lambda state: state.has(laurels, player)
+ or has_ice_grapple_logic(True, state, world))
+ regions["Overworld Tunnel Turret"].connect(
+ connecting_region=regions["Overworld"],
+ rule=lambda state: state.has_any({grapple, laurels}, player))
+
+ # Overworld side areas
+ regions["Old House Front"].connect(
+ connecting_region=regions["Old House Back"])
+ # nmg: laurels through the gate
+ regions["Old House Back"].connect(
+ connecting_region=regions["Old House Front"],
+ rule=lambda state: state.has(laurels, player) and options.logic_rules)
+
+ regions["Sealed Temple"].connect(
+ connecting_region=regions["Sealed Temple Rafters"])
+ regions["Sealed Temple Rafters"].connect(
+ connecting_region=regions["Sealed Temple"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Furnace Walking Path"].connect(
+ connecting_region=regions["Furnace Ladder Area"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Furnace Ladder Area"].connect(
+ connecting_region=regions["Furnace Walking Path"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Furnace Walking Path"].connect(
+ connecting_region=regions["Furnace Fuse"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Furnace Fuse"].connect(
+ connecting_region=regions["Furnace Walking Path"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Furnace Fuse"].connect(
+ connecting_region=regions["Furnace Ladder Area"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Furnace Ladder Area"].connect(
+ connecting_region=regions["Furnace Fuse"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Hourglass Cave"].connect(
+ connecting_region=regions["Hourglass Cave Tower"],
+ rule=lambda state: has_ladder("Ladders in Hourglass Cave", state, world))
+
+ # East Forest
+ regions["Forest Belltower Upper"].connect(
+ connecting_region=regions["Forest Belltower Main"])
+
+ regions["Forest Belltower Main"].connect(
+ connecting_region=regions["Forest Belltower Lower"],
+ rule=lambda state: has_ladder("Ladder to East Forest", state, world))
+
+ # nmg: ice grapple up to dance fox spot, and vice versa
+ regions["East Forest"].connect(
+ connecting_region=regions["East Forest Dance Fox Spot"],
+ rule=lambda state: state.has(laurels, player)
+ or has_ice_grapple_logic(True, state, world))
+ regions["East Forest Dance Fox Spot"].connect(
+ connecting_region=regions["East Forest"],
+ rule=lambda state: state.has(laurels, player)
+ or has_ice_grapple_logic(True, state, world))
+
+ regions["East Forest"].connect(
+ connecting_region=regions["East Forest Portal"],
+ rule=lambda state: has_ability(prayer, state, world))
+ regions["East Forest Portal"].connect(
+ connecting_region=regions["East Forest"])
+
+ regions["East Forest"].connect(
+ connecting_region=regions["Lower Forest"],
+ rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)
+ or (state.has_all({grapple, fire_wand, ice_dagger}, player) and has_ability(icebolt, state, world)))
+ regions["Lower Forest"].connect(
+ connecting_region=regions["East Forest"],
+ rule=lambda state: has_ladder("Ladders to Lower Forest", state, world))
+
+ regions["Guard House 1 East"].connect(
+ connecting_region=regions["Guard House 1 West"])
+ regions["Guard House 1 West"].connect(
+ connecting_region=regions["Guard House 1 East"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Guard House 2 Upper"].connect(
+ connecting_region=regions["Guard House 2 Lower"],
+ rule=lambda state: has_ladder("Ladders to Lower Forest", state, world))
+ regions["Guard House 2 Lower"].connect(
+ connecting_region=regions["Guard House 2 Upper"],
+ rule=lambda state: has_ladder("Ladders to Lower Forest", state, world))
+
+ # nmg: ice grapple from upper grave path exit to the rest of it
+ regions["Forest Grave Path Upper"].connect(
+ connecting_region=regions["Forest Grave Path Main"],
+ rule=lambda state: state.has(laurels, player)
+ or has_ice_grapple_logic(True, state, world))
+ regions["Forest Grave Path Main"].connect(
+ connecting_region=regions["Forest Grave Path Upper"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Forest Grave Path Main"].connect(
+ connecting_region=regions["Forest Grave Path by Grave"])
+ # nmg: ice grapple or laurels through the gate
+ regions["Forest Grave Path by Grave"].connect(
+ connecting_region=regions["Forest Grave Path Main"],
+ rule=lambda state: has_ice_grapple_logic(False, state, world)
+ or (state.has(laurels, player) and options.logic_rules))
+
+ regions["Forest Grave Path by Grave"].connect(
+ connecting_region=regions["Forest Hero's Grave"],
+ rule=lambda state: has_ability(prayer, state, world))
+ regions["Forest Hero's Grave"].connect(
+ connecting_region=regions["Forest Grave Path by Grave"])
+
+ # Beneath the Well and Dark Tomb
+ regions["Beneath the Well Ladder Exit"].connect(
+ connecting_region=regions["Beneath the Well Front"],
+ rule=lambda state: has_ladder("Ladders in Well", state, world))
+ regions["Beneath the Well Front"].connect(
+ connecting_region=regions["Beneath the Well Ladder Exit"],
+ rule=lambda state: has_ladder("Ladders in Well", state, world))
+
+ regions["Beneath the Well Front"].connect(
+ connecting_region=regions["Beneath the Well Main"],
+ rule=lambda state: has_stick(state, player) or state.has(fire_wand, player))
+ regions["Beneath the Well Main"].connect(
+ connecting_region=regions["Beneath the Well Front"],
+ rule=lambda state: has_stick(state, player) or state.has(fire_wand, player))
+
+ regions["Beneath the Well Main"].connect(
+ connecting_region=regions["Beneath the Well Back"],
+ rule=lambda state: has_ladder("Ladders in Well", state, world))
+ regions["Beneath the Well Back"].connect(
+ connecting_region=regions["Beneath the Well Main"],
+ rule=lambda state: has_ladder("Ladders in Well", state, world)
+ and (has_stick(state, player) or state.has(fire_wand, player)))
+
+ regions["Well Boss"].connect(
+ connecting_region=regions["Dark Tomb Checkpoint"])
+ # nmg: can laurels through the gate
+ regions["Dark Tomb Checkpoint"].connect(
+ connecting_region=regions["Well Boss"],
+ rule=lambda state: state.has(laurels, player) and options.logic_rules)
+
+ regions["Dark Tomb Entry Point"].connect(
+ connecting_region=regions["Dark Tomb Upper"],
+ rule=lambda state: has_lantern(state, world))
+ regions["Dark Tomb Upper"].connect(
+ connecting_region=regions["Dark Tomb Entry Point"])
+
+ regions["Dark Tomb Upper"].connect(
+ connecting_region=regions["Dark Tomb Main"],
+ rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world))
+ regions["Dark Tomb Main"].connect(
+ connecting_region=regions["Dark Tomb Upper"],
+ rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world))
+
+ regions["Dark Tomb Main"].connect(
+ connecting_region=regions["Dark Tomb Dark Exit"])
+ regions["Dark Tomb Dark Exit"].connect(
+ connecting_region=regions["Dark Tomb Main"],
+ rule=lambda state: has_lantern(state, world))
+
+ # West Garden
+ regions["West Garden Laurels Exit Region"].connect(
+ connecting_region=regions["West Garden"],
+ rule=lambda state: state.has(laurels, player))
+ regions["West Garden"].connect(
+ connecting_region=regions["West Garden Laurels Exit Region"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["West Garden after Boss"].connect(
+ connecting_region=regions["West Garden"],
+ rule=lambda state: state.has(laurels, player))
+ regions["West Garden"].connect(
+ connecting_region=regions["West Garden after Boss"],
+ rule=lambda state: state.has(laurels, player) or has_sword(state, player))
+
+ regions["West Garden"].connect(
+ connecting_region=regions["West Garden Hero's Grave Region"],
+ rule=lambda state: has_ability(prayer, state, world))
+ regions["West Garden Hero's Grave Region"].connect(
+ connecting_region=regions["West Garden"])
+
+ regions["West Garden Portal"].connect(
+ connecting_region=regions["West Garden Portal Item"],
+ rule=lambda state: state.has(laurels, player))
+ regions["West Garden Portal Item"].connect(
+ connecting_region=regions["West Garden Portal"],
+ rule=lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
+
+ # nmg: can ice grapple to and from the item behind the magic dagger house
+ regions["West Garden Portal Item"].connect(
+ connecting_region=regions["West Garden"],
+ rule=lambda state: has_ice_grapple_logic(True, state, world))
+ regions["West Garden"].connect(
+ connecting_region=regions["West Garden Portal Item"],
+ rule=lambda state: has_ice_grapple_logic(True, state, world))
+
+ # Atoll and Frog's Domain
+ # nmg: ice grapple the bird below the portal
+ regions["Ruined Atoll"].connect(
+ connecting_region=regions["Ruined Atoll Lower Entry Area"],
+ rule=lambda state: state.has(laurels, player)
+ or has_ice_grapple_logic(True, state, world))
+ regions["Ruined Atoll Lower Entry Area"].connect(
+ connecting_region=regions["Ruined Atoll"],
+ rule=lambda state: state.has(laurels, player) or state.has(grapple, player))
+
+ regions["Ruined Atoll"].connect(
+ connecting_region=regions["Ruined Atoll Ladder Tops"],
+ rule=lambda state: has_ladder("Ladders in South Atoll", state, world))
+
+ regions["Ruined Atoll"].connect(
+ connecting_region=regions["Ruined Atoll Frog Mouth"],
+ rule=lambda state: state.has(laurels, player) or state.has(grapple, player))
+ regions["Ruined Atoll Frog Mouth"].connect(
+ connecting_region=regions["Ruined Atoll"],
+ rule=lambda state: state.has(laurels, player) or state.has(grapple, player))
+
+ regions["Ruined Atoll"].connect(
+ connecting_region=regions["Ruined Atoll Frog Eye"],
+ rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
+ regions["Ruined Atoll Frog Eye"].connect(
+ connecting_region=regions["Ruined Atoll"],
+ rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
+
+ regions["Ruined Atoll"].connect(
+ connecting_region=regions["Ruined Atoll Portal"],
+ rule=lambda state: has_ability(prayer, state, world))
+ regions["Ruined Atoll Portal"].connect(
+ connecting_region=regions["Ruined Atoll"])
+
+ regions["Ruined Atoll"].connect(
+ connecting_region=regions["Ruined Atoll Statue"],
+ rule=lambda state: has_ability(prayer, state, world)
+ and has_ladder("Ladders in South Atoll", state, world))
+ regions["Ruined Atoll Statue"].connect(
+ connecting_region=regions["Ruined Atoll"])
+
+ regions["Frog Stairs Eye Exit"].connect(
+ connecting_region=regions["Frog Stairs Upper"],
+ rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
+ regions["Frog Stairs Upper"].connect(
+ connecting_region=regions["Frog Stairs Eye Exit"],
+ rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
+
+ regions["Frog Stairs Upper"].connect(
+ connecting_region=regions["Frog Stairs Lower"],
+ rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
+ regions["Frog Stairs Lower"].connect(
+ connecting_region=regions["Frog Stairs Upper"],
+ rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
+
+ regions["Frog Stairs Lower"].connect(
+ connecting_region=regions["Frog Stairs to Frog's Domain"],
+ rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
+ regions["Frog Stairs to Frog's Domain"].connect(
+ connecting_region=regions["Frog Stairs Lower"],
+ rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
+
+ regions["Frog's Domain Entry"].connect(
+ connecting_region=regions["Frog's Domain"],
+ rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
+
+ regions["Frog's Domain"].connect(
+ connecting_region=regions["Frog's Domain Back"],
+ rule=lambda state: state.has(grapple, player))
+
+ # Library
+ regions["Library Exterior Tree Region"].connect(
+ connecting_region=regions["Library Exterior Ladder Region"],
+ rule=lambda state: state.has_any({grapple, laurels}, player)
+ and has_ladder("Ladders in Library", state, world))
+ regions["Library Exterior Ladder Region"].connect(
+ connecting_region=regions["Library Exterior Tree Region"],
+ rule=lambda state: has_ability(prayer, state, world)
+ and ((state.has(laurels, player) and has_ladder("Ladders in Library", state, world))
+ or state.has(grapple, player)))
+
+ regions["Library Hall Bookshelf"].connect(
+ connecting_region=regions["Library Hall"],
+ rule=lambda state: has_ladder("Ladders in Library", state, world))
+ regions["Library Hall"].connect(
+ connecting_region=regions["Library Hall Bookshelf"],
+ rule=lambda state: has_ladder("Ladders in Library", state, world))
+
+ regions["Library Hall"].connect(
+ connecting_region=regions["Library Hero's Grave Region"],
+ rule=lambda state: has_ability(prayer, state, world))
+ regions["Library Hero's Grave Region"].connect(
+ connecting_region=regions["Library Hall"])
+
+ regions["Library Hall to Rotunda"].connect(
+ connecting_region=regions["Library Hall"],
+ rule=lambda state: has_ladder("Ladders in Library", state, world))
+ regions["Library Hall"].connect(
+ connecting_region=regions["Library Hall to Rotunda"],
+ rule=lambda state: has_ladder("Ladders in Library", state, world))
+
+ regions["Library Rotunda to Hall"].connect(
+ connecting_region=regions["Library Rotunda"],
+ rule=lambda state: has_ladder("Ladders in Library", state, world))
+ regions["Library Rotunda"].connect(
+ connecting_region=regions["Library Rotunda to Hall"],
+ rule=lambda state: has_ladder("Ladders in Library", state, world))
+
+ regions["Library Rotunda"].connect(
+ connecting_region=regions["Library Rotunda to Lab"],
+ rule=lambda state: has_ladder("Ladders in Library", state, world))
+ regions["Library Rotunda to Lab"].connect(
+ connecting_region=regions["Library Rotunda"],
+ rule=lambda state: has_ladder("Ladders in Library", state, world))
+
+ regions["Library Lab Lower"].connect(
+ connecting_region=regions["Library Lab"],
+ rule=lambda state: state.has_any({grapple, laurels}, player)
+ and has_ladder("Ladders in Library", state, world))
+ regions["Library Lab"].connect(
+ connecting_region=regions["Library Lab Lower"],
+ rule=lambda state: state.has(laurels, player)
+ and has_ladder("Ladders in Library", state, world))
+
+ regions["Library Lab"].connect(
+ connecting_region=regions["Library Portal"],
+ rule=lambda state: has_ability(prayer, state, world)
+ and has_ladder("Ladders in Library", state, world))
+ regions["Library Portal"].connect(
+ connecting_region=regions["Library Lab"],
+ rule=lambda state: has_ladder("Ladders in Library", state, world)
+ or state.has(laurels, player))
+
+ regions["Library Lab"].connect(
+ connecting_region=regions["Library Lab to Librarian"],
+ rule=lambda state: has_ladder("Ladders in Library", state, world))
+ regions["Library Lab to Librarian"].connect(
+ connecting_region=regions["Library Lab"],
+ rule=lambda state: has_ladder("Ladders in Library", state, world))
+
+ # Eastern Vault Fortress
+ regions["Fortress Exterior from East Forest"].connect(
+ connecting_region=regions["Fortress Exterior from Overworld"],
+ rule=lambda state: state.has(laurels, player) or state.has(grapple, player))
+ regions["Fortress Exterior from Overworld"].connect(
+ connecting_region=regions["Fortress Exterior from East Forest"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Fortress Exterior near cave"].connect(
+ connecting_region=regions["Fortress Exterior from Overworld"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Fortress Exterior from Overworld"].connect(
+ connecting_region=regions["Fortress Exterior near cave"],
+ rule=lambda state: state.has(laurels, player) or has_ability(prayer, state, world))
+
+ regions["Fortress Exterior near cave"].connect(
+ connecting_region=regions["Beneath the Vault Entry"],
+ rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world))
+ regions["Beneath the Vault Entry"].connect(
+ connecting_region=regions["Fortress Exterior near cave"],
+ rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world))
+
+ regions["Fortress Courtyard"].connect(
+ connecting_region=regions["Fortress Exterior from Overworld"],
+ rule=lambda state: state.has(laurels, player))
+ # nmg: can ice grapple an enemy in the courtyard
+ regions["Fortress Exterior from Overworld"].connect(
+ connecting_region=regions["Fortress Courtyard"],
+ rule=lambda state: state.has(laurels, player)
+ or has_ice_grapple_logic(True, state, world))
+
+ regions["Fortress Courtyard Upper"].connect(
+ connecting_region=regions["Fortress Courtyard"])
+ # nmg: can ice grapple to the upper ledge
+ regions["Fortress Courtyard"].connect(
+ connecting_region=regions["Fortress Courtyard Upper"],
+ rule=lambda state: has_ice_grapple_logic(True, state, world))
+
+ regions["Fortress Courtyard Upper"].connect(
+ connecting_region=regions["Fortress Exterior from Overworld"])
+
+ regions["Beneath the Vault Ladder Exit"].connect(
+ connecting_region=regions["Beneath the Vault Main"],
+ rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)
+ and has_lantern(state, world))
+ regions["Beneath the Vault Main"].connect(
+ connecting_region=regions["Beneath the Vault Ladder Exit"],
+ rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world))
+
+ regions["Beneath the Vault Main"].connect(
+ connecting_region=regions["Beneath the Vault Back"])
+ regions["Beneath the Vault Back"].connect(
+ connecting_region=regions["Beneath the Vault Main"],
+ rule=lambda state: has_lantern(state, world))
+
+ regions["Fortress East Shortcut Upper"].connect(
+ connecting_region=regions["Fortress East Shortcut Lower"])
+ # nmg: can ice grapple upwards
+ regions["Fortress East Shortcut Lower"].connect(
+ connecting_region=regions["Fortress East Shortcut Upper"],
+ rule=lambda state: has_ice_grapple_logic(True, state, world))
+
+ # nmg: ice grapple through the big gold door, can do it both ways
+ regions["Eastern Vault Fortress"].connect(
+ connecting_region=regions["Eastern Vault Fortress Gold Door"],
+ rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses",
+ "Activate Eastern Vault East Fuse"}, player)
+ or has_ice_grapple_logic(False, state, world))
+ regions["Eastern Vault Fortress Gold Door"].connect(
+ connecting_region=regions["Eastern Vault Fortress"],
+ rule=lambda state: has_ice_grapple_logic(True, state, world))
+
+ regions["Fortress Grave Path"].connect(
+ connecting_region=regions["Fortress Grave Path Dusty Entrance Region"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Fortress Grave Path Dusty Entrance Region"].connect(
+ connecting_region=regions["Fortress Grave Path"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Fortress Grave Path"].connect(
+ connecting_region=regions["Fortress Hero's Grave Region"],
+ rule=lambda state: has_ability(prayer, state, world))
+ regions["Fortress Hero's Grave Region"].connect(
+ connecting_region=regions["Fortress Grave Path"])
+
+ # nmg: ice grapple from upper grave path to lower
+ regions["Fortress Grave Path Upper"].connect(
+ connecting_region=regions["Fortress Grave Path"],
+ rule=lambda state: has_ice_grapple_logic(True, state, world))
+
+ regions["Fortress Arena"].connect(
+ connecting_region=regions["Fortress Arena Portal"],
+ rule=lambda state: state.has("Activate Eastern Vault West Fuses", player))
+ regions["Fortress Arena Portal"].connect(
+ connecting_region=regions["Fortress Arena"])
+
+ # Quarry
+ regions["Lower Mountain"].connect(
+ connecting_region=regions["Lower Mountain Stairs"],
+ rule=lambda state: has_ability(holy_cross, state, world))
+ regions["Lower Mountain Stairs"].connect(
+ connecting_region=regions["Lower Mountain"],
+ rule=lambda state: has_ability(holy_cross, state, world))
+
+ regions["Quarry Entry"].connect(
+ connecting_region=regions["Quarry Portal"],
+ rule=lambda state: state.has("Activate Quarry Fuse", player))
+ regions["Quarry Portal"].connect(
+ connecting_region=regions["Quarry Entry"])
+
+ regions["Quarry Entry"].connect(
+ connecting_region=regions["Quarry"],
+ rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
+ regions["Quarry"].connect(
+ connecting_region=regions["Quarry Entry"])
+
+ regions["Quarry Back"].connect(
+ connecting_region=regions["Quarry"],
+ rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
+ regions["Quarry"].connect(
+ connecting_region=regions["Quarry Back"])
+
+ regions["Quarry Monastery Entry"].connect(
+ connecting_region=regions["Quarry"],
+ rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
+ regions["Quarry"].connect(
+ connecting_region=regions["Quarry Monastery Entry"])
+
+ regions["Quarry Monastery Entry"].connect(
+ connecting_region=regions["Quarry Back"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Quarry Back"].connect(
+ connecting_region=regions["Quarry Monastery Entry"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Monastery Rope"].connect(
+ connecting_region=regions["Quarry Back"])
+
+ regions["Quarry"].connect(
+ connecting_region=regions["Lower Quarry"],
+ rule=lambda state: has_mask(state, world))
+
+ # need the ladder, or you can ice grapple down in nmg
+ regions["Lower Quarry"].connect(
+ connecting_region=regions["Even Lower Quarry"],
+ rule=lambda state: has_ladder("Ladders in Lower Quarry", state, world)
+ or has_ice_grapple_logic(True, state, world))
+
+ # nmg: bring a scav over, then ice grapple through the door, only with ER on to avoid soft lock
+ regions["Even Lower Quarry"].connect(
+ connecting_region=regions["Lower Quarry Zig Door"],
+ rule=lambda state: state.has("Activate Quarry Fuse", player)
+ or (has_ice_grapple_logic(False, state, world) and options.entrance_rando))
+
+ # nmg: use ice grapple to get from the beginning of Quarry to the door without really needing mask only with ER on
+ regions["Quarry"].connect(
+ connecting_region=regions["Lower Quarry Zig Door"],
+ rule=lambda state: has_ice_grapple_logic(True, state, world) and options.entrance_rando)
+
+ regions["Monastery Front"].connect(
+ connecting_region=regions["Monastery Back"])
+ # nmg: can laurels through the gate
+ regions["Monastery Back"].connect(
+ connecting_region=regions["Monastery Front"],
+ rule=lambda state: state.has(laurels, player) and options.logic_rules)
+
+ regions["Monastery Back"].connect(
+ connecting_region=regions["Monastery Hero's Grave Region"],
+ rule=lambda state: has_ability(prayer, state, world))
+ regions["Monastery Hero's Grave Region"].connect(
+ connecting_region=regions["Monastery Back"])
+
+ # Ziggurat
+ regions["Rooted Ziggurat Upper Entry"].connect(
+ connecting_region=regions["Rooted Ziggurat Upper Front"])
+
+ regions["Rooted Ziggurat Upper Front"].connect(
+ connecting_region=regions["Rooted Ziggurat Upper Back"],
+ rule=lambda state: state.has(laurels, player) or has_sword(state, player))
+ regions["Rooted Ziggurat Upper Back"].connect(
+ connecting_region=regions["Rooted Ziggurat Upper Front"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Rooted Ziggurat Middle Top"].connect(
+ connecting_region=regions["Rooted Ziggurat Middle Bottom"])
+
+ regions["Rooted Ziggurat Lower Front"].connect(
+ connecting_region=regions["Rooted Ziggurat Lower Back"],
+ rule=lambda state: state.has(laurels, player)
+ or (has_sword(state, player) and has_ability(prayer, state, world)))
+ # unrestricted: use ladder storage to get to the front, get hit by one of the many enemies
+ # nmg: can ice grapple on the voidlings to the double admin fight, still need to pray at the fuse
+ regions["Rooted Ziggurat Lower Back"].connect(
+ connecting_region=regions["Rooted Ziggurat Lower Front"],
+ rule=lambda state: ((state.has(laurels, player) or has_ice_grapple_logic(True, state, world))
+ and has_ability(prayer, state, world)
+ and has_sword(state, player))
+ or can_ladder_storage(state, world))
+
+ regions["Rooted Ziggurat Lower Back"].connect(
+ connecting_region=regions["Rooted Ziggurat Portal Room Entrance"],
+ rule=lambda state: has_ability(prayer, state, world))
+ regions["Rooted Ziggurat Portal Room Entrance"].connect(
+ connecting_region=regions["Rooted Ziggurat Lower Back"])
+
+ regions["Zig Skip Exit"].connect(
+ connecting_region=regions["Rooted Ziggurat Lower Front"])
+
+ regions["Rooted Ziggurat Portal"].connect(
+ connecting_region=regions["Rooted Ziggurat Portal Room Exit"],
+ rule=lambda state: state.has("Activate Ziggurat Fuse", player))
+ regions["Rooted Ziggurat Portal Room Exit"].connect(
+ connecting_region=regions["Rooted Ziggurat Portal"],
+ rule=lambda state: has_ability(prayer, state, world))
+
+ # Swamp and Cathedral
+ regions["Swamp Front"].connect(
+ connecting_region=regions["Swamp Mid"],
+ rule=lambda state: has_ladder("Ladders in Swamp", state, world)
+ or state.has(laurels, player)
+ or has_ice_grapple_logic(False, state, world)) # nmg: ice grapple through gate
+ regions["Swamp Mid"].connect(
+ connecting_region=regions["Swamp Front"],
+ rule=lambda state: has_ladder("Ladders in Swamp", state, world)
+ or state.has(laurels, player)
+ or has_ice_grapple_logic(False, state, world)) # nmg: ice grapple through gate
+
+ # nmg: ice grapple through cathedral door, can do it both ways
+ regions["Swamp Mid"].connect(
+ connecting_region=regions["Swamp to Cathedral Main Entrance Region"],
+ rule=lambda state: (has_ability(prayer, state, world) and state.has(laurels, player))
+ or has_ice_grapple_logic(False, state, world))
+ regions["Swamp to Cathedral Main Entrance Region"].connect(
+ connecting_region=regions["Swamp Mid"],
+ rule=lambda state: has_ice_grapple_logic(False, state, world))
+
+ regions["Swamp Mid"].connect(
+ connecting_region=regions["Swamp Ledge under Cathedral Door"],
+ rule=lambda state: has_ladder("Ladders in Swamp", state, world))
+ regions["Swamp Ledge under Cathedral Door"].connect(
+ connecting_region=regions["Swamp Mid"],
+ rule=lambda state: has_ladder("Ladders in Swamp", state, world)
+ or has_ice_grapple_logic(True, state, world)) # nmg: ice grapple the enemy at door
+
+ regions["Swamp Ledge under Cathedral Door"].connect(
+ connecting_region=regions["Swamp to Cathedral Treasure Room"],
+ rule=lambda state: has_ability(holy_cross, state, world))
+ regions["Swamp to Cathedral Treasure Room"].connect(
+ connecting_region=regions["Swamp Ledge under Cathedral Door"])
+
+ regions["Back of Swamp"].connect(
+ connecting_region=regions["Back of Swamp Laurels Area"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Back of Swamp Laurels Area"].connect(
+ connecting_region=regions["Back of Swamp"],
+ rule=lambda state: state.has(laurels, player))
+
+ # nmg: can ice grapple down while you're on the pillars
+ regions["Back of Swamp Laurels Area"].connect(
+ connecting_region=regions["Swamp Mid"],
+ rule=lambda state: state.has(laurels, player)
+ and has_ice_grapple_logic(True, state, world))
+
+ regions["Back of Swamp"].connect(
+ connecting_region=regions["Swamp Hero's Grave Region"],
+ rule=lambda state: has_ability(prayer, state, world))
+ regions["Swamp Hero's Grave Region"].connect(
+ connecting_region=regions["Back of Swamp"])
+
+ regions["Cathedral Gauntlet Checkpoint"].connect(
+ connecting_region=regions["Cathedral Gauntlet"])
+
+ regions["Cathedral Gauntlet"].connect(
+ connecting_region=regions["Cathedral Gauntlet Exit"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Cathedral Gauntlet Exit"].connect(
+ connecting_region=regions["Cathedral Gauntlet"],
+ rule=lambda state: state.has(laurels, player))
+
+ # Far Shore
+ regions["Far Shore"].connect(
+ connecting_region=regions["Far Shore to Spawn Region"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Far Shore to Spawn Region"].connect(
+ connecting_region=regions["Far Shore"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Far Shore"].connect(
+ connecting_region=regions["Far Shore to East Forest Region"],
+ rule=lambda state: state.has(laurels, player))
+ regions["Far Shore to East Forest Region"].connect(
+ connecting_region=regions["Far Shore"],
+ rule=lambda state: state.has(laurels, player))
+
+ regions["Far Shore"].connect(
+ connecting_region=regions["Far Shore to West Garden Region"],
+ rule=lambda state: state.has("Activate West Garden Fuse", player))
+ regions["Far Shore to West Garden Region"].connect(
+ connecting_region=regions["Far Shore"])
+
+ regions["Far Shore"].connect(
+ connecting_region=regions["Far Shore to Quarry Region"],
+ rule=lambda state: state.has("Activate Quarry Fuse", player))
+ regions["Far Shore to Quarry Region"].connect(
+ connecting_region=regions["Far Shore"])
+
+ regions["Far Shore"].connect(
+ connecting_region=regions["Far Shore to Fortress Region"],
+ rule=lambda state: state.has("Activate Eastern Vault West Fuses", player))
+ regions["Far Shore to Fortress Region"].connect(
+ connecting_region=regions["Far Shore"])
+
+ regions["Far Shore"].connect(
+ connecting_region=regions["Far Shore to Library Region"],
+ rule=lambda state: state.has("Activate Library Fuse", player))
+ regions["Far Shore to Library Region"].connect(
+ connecting_region=regions["Far Shore"])
+
+ # Misc
+ regions["Spirit Arena"].connect(
+ connecting_region=regions["Spirit Arena Victory"],
+ rule=lambda state: (state.has(gold_hexagon, player, world.options.hexagon_goal.value) if
+ world.options.hexagon_quest else
+ (state.has_all({red_hexagon, green_hexagon, blue_hexagon, "Unseal the Heir"}, player)
+ and state.has_group_unique("Hero Relics", player, 6)
+ and has_sword(state, player))))
+
+ # connecting the regions portals are in to other portals you can access via ladder storage
+ # using has_stick instead of can_ladder_storage since it's already checking the logic rules
+ if options.logic_rules == "unrestricted":
+ def get_portal_info(portal_sd: str) -> Tuple[str, str]:
+ for portal1, portal2 in portal_pairs.items():
+ if portal1.scene_destination() == portal_sd:
+ return portal1.name, portal2.region
+ if portal2.scene_destination() == portal_sd:
+ return portal2.name, portal1.region
+ raise Exception("no matches found in get_paired_region")
+
+ ladder_storages: List[Tuple[str, str, Set[str]]] = [
+ # LS from Overworld main
+ # The upper Swamp entrance
+ ("Overworld", "Overworld Redux, Swamp Redux 2_wall",
+ {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}),
+ # Upper atoll entrance
+ ("Overworld", "Overworld Redux, Atoll Redux_upper",
+ {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}),
+ # Furnace entrance, next to the sign that leads to West Garden
+ ("Overworld", "Overworld Redux, Furnace_gyro_west",
+ {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}),
+ # Upper West Garden entry, by the belltower
+ ("Overworld", "Overworld Redux, Archipelagos Redux_upper",
+ {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}),
+ # Ruined Passage
+ ("Overworld", "Overworld Redux, Ruins Passage_east",
+ {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}),
+ # Well rail, west side. Can ls in town, get extra height by going over the portal pad
+ ("Overworld", "Overworld Redux, Sewer_west_aqueduct",
+ {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladder to Quarry"}),
+ # Well rail, east side. Need some height from the temple stairs
+ ("Overworld", "Overworld Redux, Furnace_gyro_upper_north",
+ {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladder to Quarry"}),
+ # Quarry entry
+ ("Overworld", "Overworld Redux, Darkwoods Tunnel_",
+ {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well"}),
+ # East Forest entry
+ ("Overworld", "Overworld Redux, Forest Belltower_",
+ {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well",
+ "Ladders near Patrol Cave", "Ladder to Quarry", "Ladders near Dark Tomb"}),
+ # Fortress entry
+ ("Overworld", "Overworld Redux, Fortress Courtyard_",
+ {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well",
+ "Ladders near Patrol Cave", "Ladder to Quarry", "Ladders near Dark Tomb"}),
+ # Patrol Cave entry
+ ("Overworld", "Overworld Redux, PatrolCave_",
+ {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well",
+ "Ladders near Overworld Checkpoint", "Ladder to Quarry", "Ladders near Dark Tomb"}),
+ # Special Shop entry, excluded in non-ER due to soft lock potential
+ ("Overworld", "Overworld Redux, ShopSpecial_",
+ {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well",
+ "Ladders near Overworld Checkpoint", "Ladders near Patrol Cave", "Ladder to Quarry",
+ "Ladders near Dark Tomb"}),
+ # Temple Rafters, excluded in non-ER + ladder rando due to soft lock potential
+ ("Overworld", "Overworld Redux, Temple_rafters",
+ {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well",
+ "Ladders near Overworld Checkpoint", "Ladders near Patrol Cave", "Ladder to Quarry",
+ "Ladders near Dark Tomb"}),
+ # Spot above the Quarry entrance,
+ # only gets you to the mountain stairs
+ ("Overworld above Quarry Entrance", "Overworld Redux, Mountain_",
+ {"Ladders near Dark Tomb"}),
+
+ # LS from the Overworld Beach
+ # West Garden entry by the Furnace
+ ("Overworld Beach", "Overworld Redux, Archipelagos Redux_lower",
+ {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}),
+ # West Garden laurels entry
+ ("Overworld Beach", "Overworld Redux, Archipelagos Redux_lowest",
+ {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}),
+ # Swamp lower entrance
+ ("Overworld Beach", "Overworld Redux, Swamp Redux 2_conduit",
+ {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}),
+ # Rotating Lights entrance
+ ("Overworld Beach", "Overworld Redux, Overworld Cave_",
+ {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}),
+ # Swamp upper entrance
+ ("Overworld Beach", "Overworld Redux, Swamp Redux 2_wall",
+ {"Ladder to Ruined Atoll"}),
+ # Furnace entrance, next to the sign that leads to West Garden
+ ("Overworld Beach", "Overworld Redux, Furnace_gyro_west",
+ {"Ladder to Ruined Atoll"}),
+ # Upper West Garden entry, by the belltower
+ ("Overworld Beach", "Overworld Redux, Archipelagos Redux_upper",
+ {"Ladder to Ruined Atoll"}),
+ # Ruined Passage
+ ("Overworld Beach", "Overworld Redux, Ruins Passage_east",
+ {"Ladder to Ruined Atoll"}),
+ # Well rail, west side. Can ls in town, get extra height by going over the portal pad
+ ("Overworld Beach", "Overworld Redux, Sewer_west_aqueduct",
+ {"Ladder to Ruined Atoll"}),
+ # Well rail, east side. Need some height from the temple stairs
+ ("Overworld Beach", "Overworld Redux, Furnace_gyro_upper_north",
+ {"Ladder to Ruined Atoll"}),
+ # Quarry entry
+ ("Overworld Beach", "Overworld Redux, Darkwoods Tunnel_",
+ {"Ladder to Ruined Atoll"}),
+
+ # LS from that low spot where you normally walk to swamp
+ # Only has low ones you can't get to from main Overworld
+ # West Garden main entry from swamp ladder
+ ("Overworld Swamp Lower Entry", "Overworld Redux, Archipelagos Redux_lower",
+ {"Ladder to Swamp"}),
+ # Maze Cave entry from swamp ladder
+ ("Overworld Swamp Lower Entry", "Overworld Redux, Maze Room_",
+ {"Ladder to Swamp"}),
+ # Hourglass Cave entry from swamp ladder
+ ("Overworld Swamp Lower Entry", "Overworld Redux, Town Basement_beach",
+ {"Ladder to Swamp"}),
+ # Lower Atoll entry from swamp ladder
+ ("Overworld Swamp Lower Entry", "Overworld Redux, Atoll Redux_lower",
+ {"Ladder to Swamp"}),
+ # Lowest West Garden entry from swamp ladder
+ ("Overworld Swamp Lower Entry", "Overworld Redux, Archipelagos Redux_lowest",
+ {"Ladder to Swamp"}),
+
+ # from the ladders by the belltower
+ # Ruined Passage
+ ("Overworld to West Garden Upper", "Overworld Redux, Ruins Passage_east",
+ {"Ladders to West Bell"}),
+ # Well rail, west side. Can ls in town, get extra height by going over the portal pad
+ ("Overworld to West Garden Upper", "Overworld Redux, Sewer_west_aqueduct",
+ {"Ladders to West Bell"}),
+ # Well rail, east side. Need some height from the temple stairs
+ ("Overworld to West Garden Upper", "Overworld Redux, Furnace_gyro_upper_north",
+ {"Ladders to West Bell"}),
+ # Quarry entry
+ ("Overworld to West Garden Upper", "Overworld Redux, Darkwoods Tunnel_",
+ {"Ladders to West Bell"}),
+ # East Forest entry
+ ("Overworld to West Garden Upper", "Overworld Redux, Forest Belltower_",
+ {"Ladders to West Bell"}),
+ # Fortress entry
+ ("Overworld to West Garden Upper", "Overworld Redux, Fortress Courtyard_",
+ {"Ladders to West Bell"}),
+ # Patrol Cave entry
+ ("Overworld to West Garden Upper", "Overworld Redux, PatrolCave_",
+ {"Ladders to West Bell"}),
+ # Special Shop entry, excluded in non-ER due to soft lock potential
+ ("Overworld to West Garden Upper", "Overworld Redux, ShopSpecial_",
+ {"Ladders to West Bell"}),
+ # Temple Rafters, excluded in non-ER and ladder rando due to soft lock potential
+ ("Overworld to West Garden Upper", "Overworld Redux, Temple_rafters",
+ {"Ladders to West Bell"}),
+
+ # In the furnace
+ # Furnace ladder to the fuse entrance
+ ("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_upper_north", set()),
+ # Furnace ladder to Dark Tomb
+ ("Furnace Ladder Area", "Furnace, Crypt Redux_", set()),
+ # Furnace ladder to the West Garden connector
+ ("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_west", set()),
+
+ # West Garden
+ # exit after Garden Knight
+ ("West Garden", "Archipelagos Redux, Overworld Redux_upper", set()),
+ # West Garden laurels exit
+ ("West Garden", "Archipelagos Redux, Overworld Redux_lowest", set()),
+
+ # Atoll, use the little ladder you fix at the beginning
+ ("Ruined Atoll", "Atoll Redux, Overworld Redux_lower", set()),
+ ("Ruined Atoll", "Atoll Redux, Frog Stairs_mouth", set()),
+ ("Ruined Atoll", "Atoll Redux, Frog Stairs_eye", set()),
+
+ # East Forest
+ # Entrance by the dancing fox holy cross spot
+ ("East Forest", "East Forest Redux, East Forest Redux Laddercave_upper", set()),
+
+ # From the west side of Guard House 1 to the east side
+ ("Guard House 1 West", "East Forest Redux Laddercave, East Forest Redux_gate", set()),
+ ("Guard House 1 West", "East Forest Redux Laddercave, Forest Boss Room_", set()),
+
+ # Upper exit from the Forest Grave Path, use LS at the ladder by the gate switch
+ ("Forest Grave Path Main", "Sword Access, East Forest Redux_upper", set()),
+
+ # Fortress Exterior
+ # shop, ls at the ladder by the telescope
+ ("Fortress Exterior from Overworld", "Fortress Courtyard, Shop_", set()),
+ # Fortress main entry and grave path lower entry, ls at the ladder by the telescope
+ ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Main_Big Door", set()),
+ ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Lower", set()),
+ # Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse
+ ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Upper", set()),
+ ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress East_", set()),
+
+ # same as above, except from the east side of the area
+ ("Fortress Exterior from East Forest", "Fortress Courtyard, Overworld Redux_", set()),
+ ("Fortress Exterior from East Forest", "Fortress Courtyard, Shop_", set()),
+ ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Main_Big Door", set()),
+ ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Lower", set()),
+ ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Upper", set()),
+ ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress East_", set()),
+
+ # same as above, except from the Beneath the Vault entrance ladder
+ ("Fortress Exterior near cave", "Fortress Courtyard, Overworld Redux_",
+ {"Ladder to Beneath the Vault"}),
+ ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Main_Big Door",
+ {"Ladder to Beneath the Vault"}),
+ ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Lower",
+ {"Ladder to Beneath the Vault"}),
+ ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Upper",
+ {"Ladder to Beneath the Vault"}),
+ ("Fortress Exterior near cave", "Fortress Courtyard, Fortress East_",
+ {"Ladder to Beneath the Vault"}),
+
+ # ls at the ladder, need to gain a little height to get up the stairs
+ # excluded in non-ER due to soft lock potential
+ ("Lower Mountain", "Mountain, Mountaintop_", set()),
+
+ # Where the rope is behind Monastery. Connecting here since, if you have this region, you don't need a sword
+ ("Quarry Monastery Entry", "Quarry Redux, Monastery_back", set()),
+
+ # Swamp to Gauntlet
+ ("Swamp Mid", "Swamp Redux 2, Cathedral Arena_",
+ {"Ladders in Swamp"}),
+ # Swamp to Overworld upper
+ ("Swamp Mid", "Swamp Redux 2, Overworld Redux_wall",
+ {"Ladders in Swamp"}),
+ # Ladder by the hero grave
+ ("Back of Swamp", "Swamp Redux 2, Overworld Redux_conduit", set()),
+ ("Back of Swamp", "Swamp Redux 2, Shop_", set()),
+ # Need to put the cathedral HC code mid-flight
+ ("Back of Swamp", "Swamp Redux 2, Cathedral Redux_secret", set()),
+ ]
+
+ for region_name, scene_dest, ladders in ladder_storages:
+ portal_name, paired_region = get_portal_info(scene_dest)
+ # this is the only exception, requiring holy cross as well
+ if portal_name == "Swamp to Cathedral Secret Legend Room Entrance" and region_name == "Back of Swamp":
+ regions[region_name].connect(
+ regions[paired_region],
+ name=portal_name + " (LS) " + region_name,
+ rule=lambda state: has_stick(state, player)
+ and has_ability(holy_cross, state, world)
+ and (has_ladder("Ladders in Swamp", state, world)
+ or has_ice_grapple_logic(True, state, world)
+ or not options.entrance_rando))
+ # soft locked without this ladder
+ elif portal_name == "West Garden Exit after Boss" and not options.entrance_rando:
+ regions[region_name].connect(
+ regions[paired_region],
+ name=portal_name + " (LS) " + region_name,
+ rule=lambda state: has_stick(state, player)
+ and (state.has("Ladders to West Bell", player)))
+ # soft locked unless you have either ladder. if you have laurels, you use the other Entrance
+ elif portal_name in {"Furnace Exit towards West Garden", "Furnace Exit to Dark Tomb"} \
+ and not options.entrance_rando:
+ regions[region_name].connect(
+ regions[paired_region],
+ name=portal_name + " (LS) " + region_name,
+ rule=lambda state: has_stick(state, player)
+ and state.has_any({"Ladder in Dark Tomb", "Ladders to West Bell"}, player))
+ # soft locked for the same reasons as above
+ elif portal_name in {"Entrance to Furnace near West Garden", "West Garden Entrance from Furnace"} \
+ and not options.entrance_rando:
+ regions[region_name].connect(
+ regions[paired_region],
+ name=portal_name + " (LS) " + region_name,
+ rule=lambda state: has_stick(state, player) and state.has_any(ladders, player)
+ and state.has_any({"Ladder in Dark Tomb", "Ladders to West Bell"}, player))
+ # soft locked if you can't get past garden knight backwards or up the belltower ladders
+ elif portal_name == "West Garden Entrance near Belltower" and not options.entrance_rando:
+ regions[region_name].connect(
+ regions[paired_region],
+ name=portal_name + " (LS) " + region_name,
+ rule=lambda state: has_stick(state, player) and state.has_any(ladders, player)
+ and state.has_any({"Ladders to West Bell", laurels}, player))
+ # soft locked if you can't get back out
+ elif portal_name == "Fortress Courtyard to Beneath the Vault" and not options.entrance_rando:
+ regions[region_name].connect(
+ regions[paired_region],
+ name=portal_name + " (LS) " + region_name,
+ rule=lambda state: has_stick(state, player) and state.has("Ladder to Beneath the Vault", player)
+ and has_lantern(state, world))
+ elif portal_name == "Atoll Lower Entrance" and not options.entrance_rando:
+ regions[region_name].connect(
+ regions[paired_region],
+ name=portal_name + " (LS) " + region_name,
+ rule=lambda state: has_stick(state, player) and state.has_any(ladders, player)
+ and (state.has_any({"Ladders in Overworld Town", grapple}, player)
+ or has_ice_grapple_logic(True, state, world)))
+ elif portal_name == "Atoll Upper Entrance" and not options.entrance_rando:
+ regions[region_name].connect(
+ regions[paired_region],
+ name=portal_name + " (LS) " + region_name,
+ rule=lambda state: has_stick(state, player) and state.has_any(ladders, player)
+ and state.has(grapple, player) or has_ability(prayer, state, world))
+ # soft lock potential
+ elif portal_name in {"Special Shop Entrance", "Stairs to Top of the Mountain", "Swamp Upper Entrance",
+ "Swamp Lower Entrance", "Caustic Light Cave Entrance"} and not options.entrance_rando:
+ continue
+ # soft lock if you don't have the ladder, I regret writing unrestricted logic
+ elif portal_name == "Temple Rafters Entrance" and not options.entrance_rando:
+ regions[region_name].connect(
+ regions[paired_region],
+ name=portal_name + " (LS) " + region_name,
+ rule=lambda state: has_stick(state, player)
+ and state.has_any(ladders, player)
+ and (state.has("Ladder near Temple Rafters", player)
+ or (state.has_all({laurels, grapple}, player)
+ and ((state.has("Ladders near Patrol Cave", player)
+ and (state.has("Ladders near Dark Tomb", player)
+ or state.has("Ladder to Quarry", player)
+ and (state.has(fire_wand, player) or has_sword(state, player))))
+ or state.has("Ladders near Overworld Checkpoint", player)
+ or has_ice_grapple_logic(True, state, world)))))
+ # if no ladder items are required, just do the basic stick only lambda
+ elif not ladders or not options.shuffle_ladders:
+ regions[region_name].connect(
+ regions[paired_region],
+ name=portal_name + " (LS) " + region_name,
+ rule=lambda state: has_stick(state, player))
+ # one ladder required
+ elif len(ladders) == 1:
+ ladder = ladders.pop()
+ regions[region_name].connect(
+ regions[paired_region],
+ name=portal_name + " (LS) " + region_name,
+ rule=lambda state: has_stick(state, player) and state.has(ladder, player))
+ # if multiple ladders can be used
+ else:
+ regions[region_name].connect(
+ regions[paired_region],
+ name=portal_name + " (LS) " + region_name,
+ rule=lambda state: has_stick(state, player) and state.has_any(ladders, player))
+
+
+def set_er_location_rules(world: "TunicWorld") -> None:
+ player = world.player
+ multiworld = world.multiworld
+ options = world.options
+
+ forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), fairies, player)
+
+ # Ability Shuffle Exclusive Rules
+ set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player),
+ lambda state: state.has("Activate Furnace Fuse", player))
+ set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Overworld - [Southwest] Flowers Holy Cross", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Overworld - [East] Weathervane Holy Cross", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Overworld - [Northeast] Flowers Holy Cross", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Overworld - [Southwest] Haiku Holy Cross", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Overworld - [Northwest] Golden Obelisk Page", player),
+ lambda state: has_ability(holy_cross, state, world))
+
+ # Overworld
+ set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player),
+ lambda state: state.has_any({grapple, laurels}, player))
+ set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player),
+ lambda state: state.has_any({grapple, laurels}, player))
+ set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Old House - Holy Cross Chest", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player),
+ lambda state: state.has(grapple, player))
+ set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Caustic Light Cave - Holy Cross Chest", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Cube Cave - Holy Cross Chest", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Old House - Holy Cross Door Page", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Maze Cave - Maze Room Holy Cross", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Old House - Holy Cross Chest", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Patrol Cave - Holy Cross Chest", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Ruined Passage - Holy Cross Chest", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Hourglass Cave - Holy Cross Chest", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Secret Gathering Place - Holy Cross Chest", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player),
+ lambda state: state.has(fairies, player, 10))
+ set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player),
+ lambda state: state.has(fairies, player, 20))
+ set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player),
+ lambda state: state.has(coins, player, 3))
+ set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player),
+ lambda state: state.has(coins, player, 6))
+ set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player),
+ lambda state: state.has(coins, player, 10))
+ set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player),
+ lambda state: state.has(coins, player, 15))
+
+ # East Forest
+ set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player),
+ lambda state: state.has(grapple, player))
+ set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player),
+ lambda state: state.has_all({grapple, laurels}, player))
+ set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), lambda state: (
+ state.has_all({grapple, ice_dagger, fire_wand}, player) and has_ability(icebolt, state, world)))
+
+ # West Garden
+ set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player),
+ lambda state: state.has(laurels, player) and has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player),
+ lambda state: state.has(laurels, player))
+
+ # Ruined Atoll
+ set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player),
+ lambda state: state.has(laurels, player) or state.has(key, player, 2))
+ set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player),
+ lambda state: state.has(laurels, player) or state.has(key, player, 2))
+
+ # Frog's Domain
+ set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player),
+ lambda state: state.has_any({grapple, laurels}, player))
+ set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player),
+ lambda state: state.has_any({grapple, laurels}, player))
+ set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player),
+ lambda state: state.has_any({grapple, laurels}, player))
+
+ # Eastern Vault Fortress
+ set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player),
+ lambda state: state.has(vault_key, player))
+
+ # Beneath the Vault
+ set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player),
+ lambda state: state.has_group("Melee Weapons", player, 1) or state.has_any({laurels, fire_wand}, player))
+
+ # Quarry
+ set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player),
+ lambda state: state.has(laurels, player))
+
+ # Ziggurat
+ # if ER is off, you still need to get past the Admin or you'll get stuck in lower zig
+ set_rule(multiworld.get_location("Rooted Ziggurat Upper - Near Bridge Switch", player),
+ lambda state: has_sword(state, player) or (state.has(fire_wand, player) and (state.has(laurels, player)
+ or options.entrance_rando)))
+ set_rule(multiworld.get_location("Rooted Ziggurat Lower - After Guarded Fuse", player),
+ lambda state: has_sword(state, player) and has_ability(prayer, state, world))
+
+ # Bosses
+ set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player),
+ lambda state: has_sword(state, player))
+ # nmg - kill Librarian with a lure, or gun I guess
+ set_rule(multiworld.get_location("Librarian - Hexagon Green", player),
+ lambda state: (has_sword(state, player) or options.logic_rules)
+ and has_ladder("Ladders in Library", state, world))
+ # nmg - kill boss scav with orb + firecracker, or similar
+ set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player),
+ lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules))
+
+ # Swamp
+ set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player),
+ lambda state: state.has(fire_wand, player) and has_sword(state, player))
+ set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player),
+ lambda state: state.has(laurels, player))
+ # these two swamp checks really want you to kill the big skeleton first
+ set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player),
+ lambda state: has_sword(state, player))
+
+ # Hero's Grave and Far Shore
+ set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Far Shore - Secret Chest", player),
+ lambda state: state.has(laurels, player))
+
+ # Events
+ set_rule(multiworld.get_location("Eastern Bell", player),
+ lambda state: (has_stick(state, player) or state.has(fire_wand, player)))
+ set_rule(multiworld.get_location("Western Bell", player),
+ lambda state: (has_stick(state, player) or state.has(fire_wand, player)))
+ set_rule(multiworld.get_location("Furnace Fuse", player),
+ lambda state: has_ability(prayer, state, world))
+ set_rule(multiworld.get_location("South and West Fortress Exterior Fuses", player),
+ lambda state: has_ability(prayer, state, world))
+ set_rule(multiworld.get_location("Upper and Central Fortress Exterior Fuses", player),
+ lambda state: has_ability(prayer, state, world))
+ set_rule(multiworld.get_location("Beneath the Vault Fuse", player),
+ lambda state: state.has("Activate South and West Fortress Exterior Fuses", player))
+ set_rule(multiworld.get_location("Eastern Vault West Fuses", player),
+ lambda state: state.has("Activate Beneath the Vault Fuse", player))
+ set_rule(multiworld.get_location("Eastern Vault East Fuse", player),
+ lambda state: state.has_all({"Activate Upper and Central Fortress Exterior Fuses",
+ "Activate South and West Fortress Exterior Fuses"}, player))
+ set_rule(multiworld.get_location("Quarry Connector Fuse", player),
+ lambda state: has_ability(prayer, state, world) and state.has(grapple, player))
+ set_rule(multiworld.get_location("Quarry Fuse", player),
+ lambda state: state.has("Activate Quarry Connector Fuse", player))
+ set_rule(multiworld.get_location("Ziggurat Fuse", player),
+ lambda state: has_ability(prayer, state, world))
+ set_rule(multiworld.get_location("West Garden Fuse", player),
+ lambda state: has_ability(prayer, state, world))
+ set_rule(multiworld.get_location("Library Fuse", player),
+ lambda state: has_ability(prayer, state, world))
+
+ # Shop
+ set_rule(multiworld.get_location("Shop - Potion 1", player),
+ lambda state: has_sword(state, player))
+ set_rule(multiworld.get_location("Shop - Potion 2", player),
+ lambda state: has_sword(state, player))
+ set_rule(multiworld.get_location("Shop - Coin 1", player),
+ lambda state: has_sword(state, player))
+ set_rule(multiworld.get_location("Shop - Coin 2", player),
+ lambda state: has_sword(state, player))
diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py
new file mode 100644
index 000000000000..0bd8c5e80681
--- /dev/null
+++ b/worlds/tunic/er_scripts.py
@@ -0,0 +1,506 @@
+from typing import Dict, List, Set, TYPE_CHECKING
+from BaseClasses import Region, ItemClassification, Item, Location
+from .locations import location_table
+from .er_data import Portal, tunic_er_regions, portal_mapping, traversal_requirements, DeadEnd
+from .er_rules import set_er_region_rules
+from Options import PlandoConnection
+from .options import EntranceRando
+from random import Random
+from copy import deepcopy
+
+if TYPE_CHECKING:
+ from . import TunicWorld
+
+
+class TunicERItem(Item):
+ game: str = "TUNIC"
+
+
+class TunicERLocation(Location):
+ game: str = "TUNIC"
+
+
+def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
+ regions: Dict[str, Region] = {}
+ if world.options.entrance_rando:
+ portal_pairs = pair_portals(world)
+
+ # output the entrances to the spoiler log here for convenience
+ for portal1, portal2 in portal_pairs.items():
+ world.multiworld.spoiler.set_entrance(portal1.name, portal2.name, "both", world.player)
+ else:
+ portal_pairs = vanilla_portals()
+
+ for region_name, region_data in tunic_er_regions.items():
+ regions[region_name] = Region(region_name, world.player, world.multiworld)
+
+ set_er_region_rules(world, regions, portal_pairs)
+
+ for location_name, location_id in world.location_name_to_id.items():
+ region = regions[location_table[location_name].er_region]
+ location = TunicERLocation(world.player, location_name, location_id, region)
+ region.locations.append(location)
+
+ create_randomized_entrances(portal_pairs, regions)
+
+ for region in regions.values():
+ world.multiworld.regions.append(region)
+
+ place_event_items(world, regions)
+
+ victory_region = regions["Spirit Arena Victory"]
+ victory_location = TunicERLocation(world.player, "The Heir", None, victory_region)
+ victory_location.place_locked_item(TunicERItem("Victory", ItemClassification.progression, None, world.player))
+ world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
+ victory_region.locations.append(victory_location)
+
+ return portal_pairs
+
+
+tunic_events: Dict[str, str] = {
+ "Eastern Bell": "Forest Belltower Upper",
+ "Western Bell": "Overworld Belltower at Bell",
+ "Furnace Fuse": "Furnace Fuse",
+ "South and West Fortress Exterior Fuses": "Fortress Exterior from Overworld",
+ "Upper and Central Fortress Exterior Fuses": "Fortress Courtyard Upper",
+ "Beneath the Vault Fuse": "Beneath the Vault Back",
+ "Eastern Vault West Fuses": "Eastern Vault Fortress",
+ "Eastern Vault East Fuse": "Eastern Vault Fortress",
+ "Quarry Connector Fuse": "Quarry Connector",
+ "Quarry Fuse": "Quarry Entry",
+ "Ziggurat Fuse": "Rooted Ziggurat Lower Back",
+ "West Garden Fuse": "West Garden",
+ "Library Fuse": "Library Lab",
+ "Place Questagons": "Sealed Temple",
+}
+
+
+def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None:
+ for event_name, region_name in tunic_events.items():
+ region = regions[region_name]
+ location = TunicERLocation(world.player, event_name, None, region)
+ if event_name == "Place Questagons":
+ if world.options.hexagon_quest:
+ continue
+ location.place_locked_item(
+ TunicERItem("Unseal the Heir", ItemClassification.progression, None, world.player))
+ elif event_name.endswith("Bell"):
+ location.place_locked_item(
+ TunicERItem("Ring " + event_name, ItemClassification.progression, None, world.player))
+ else:
+ location.place_locked_item(
+ TunicERItem("Activate " + event_name, ItemClassification.progression, None, world.player))
+ region.locations.append(location)
+
+
+def vanilla_portals() -> Dict[Portal, Portal]:
+ portal_pairs: Dict[Portal, Portal] = {}
+ # we don't want the zig skip exit for vanilla portals, since it shouldn't be considered for logic here
+ portal_map = [portal for portal in portal_mapping if portal.name != "Ziggurat Lower Falling Entrance"]
+
+ while portal_map:
+ portal1 = portal_map[0]
+ portal2 = None
+ # portal2 scene destination tag is portal1's destination scene tag
+ portal2_sdt = portal1.destination_scene()
+
+ if portal2_sdt.startswith("Shop,"):
+ portal2 = Portal(name="Shop", region="Shop",
+ destination="Previous Region", tag="_")
+
+ elif portal2_sdt == "Purgatory, Purgatory_bottom":
+ portal2_sdt = "Purgatory, Purgatory_top"
+
+ for portal in portal_map:
+ if portal.scene_destination() == portal2_sdt:
+ portal2 = portal
+ break
+
+ portal_pairs[portal1] = portal2
+ portal_map.remove(portal1)
+ if not portal2_sdt.startswith("Shop,"):
+ portal_map.remove(portal2)
+
+ return portal_pairs
+
+
+# pairing off portals, starting with dead ends
+def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
+ # separate the portals into dead ends and non-dead ends
+ portal_pairs: Dict[Portal, Portal] = {}
+ dead_ends: List[Portal] = []
+ two_plus: List[Portal] = []
+ player_name = world.multiworld.get_player_name(world.player)
+ portal_map = portal_mapping.copy()
+ logic_rules = world.options.logic_rules.value
+ fixed_shop = world.options.fixed_shop
+ laurels_location = world.options.laurels_location
+ traversal_reqs = deepcopy(traversal_requirements)
+ has_laurels = True
+ waterfall_plando = False
+
+ # if it's not one of the EntranceRando options, it's a custom seed
+ if world.options.entrance_rando.value not in EntranceRando.options.values():
+ seed_group = world.seed_groups[world.options.entrance_rando.value]
+ logic_rules = seed_group["logic_rules"]
+ fixed_shop = seed_group["fixed_shop"]
+ laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False
+
+ # marking that you don't immediately have laurels
+ if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"):
+ has_laurels = False
+
+ shop_scenes: Set[str] = set()
+ shop_count = 6
+ if fixed_shop:
+ shop_count = 0
+ shop_scenes.add("Overworld Redux")
+ else:
+ # if fixed shop is off, remove this portal
+ for portal in portal_map:
+ if portal.region == "Zig Skip Exit":
+ portal_map.remove(portal)
+ break
+
+ # If using Universal Tracker, restore portal_map. Could be cleaner, but it does not matter for UT even a little bit
+ if hasattr(world.multiworld, "re_gen_passthrough"):
+ if "TUNIC" in world.multiworld.re_gen_passthrough:
+ portal_map = portal_mapping.copy()
+
+ # create separate lists for dead ends and non-dead ends
+ for portal in portal_map:
+ dead_end_status = tunic_er_regions[portal.region].dead_end
+ if dead_end_status == DeadEnd.free:
+ two_plus.append(portal)
+ elif dead_end_status == DeadEnd.all_cats:
+ dead_ends.append(portal)
+ elif dead_end_status == DeadEnd.restricted:
+ if logic_rules:
+ two_plus.append(portal)
+ else:
+ dead_ends.append(portal)
+ # these two get special handling
+ elif dead_end_status == DeadEnd.special:
+ if portal.region == "Secret Gathering Place":
+ if laurels_location == "10_fairies":
+ two_plus.append(portal)
+ else:
+ dead_ends.append(portal)
+ if portal.region == "Zig Skip Exit":
+ if fixed_shop:
+ two_plus.append(portal)
+ else:
+ dead_ends.append(portal)
+
+ connected_regions: Set[str] = set()
+ # make better start region stuff when/if implementing random start
+ start_region = "Overworld"
+ connected_regions.add(start_region)
+ connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules)
+
+ if world.options.entrance_rando.value in EntranceRando.options.values():
+ plando_connections = world.options.plando_connections.value
+ else:
+ plando_connections = world.seed_groups[world.options.entrance_rando.value]["plando"]
+
+ # universal tracker support stuff, don't need to care about region dependency
+ if hasattr(world.multiworld, "re_gen_passthrough"):
+ if "TUNIC" in world.multiworld.re_gen_passthrough:
+ plando_connections.clear()
+ # universal tracker stuff, won't do anything in normal gen
+ for portal1, portal2 in world.multiworld.re_gen_passthrough["TUNIC"]["Entrance Rando"].items():
+ portal_name1 = ""
+ portal_name2 = ""
+
+ for portal in portal_mapping:
+ if portal.scene_destination() == portal1:
+ portal_name1 = portal.name
+ # connected_regions.update(add_dependent_regions(portal.region, logic_rules))
+ if portal.scene_destination() == portal2:
+ portal_name2 = portal.name
+ # connected_regions.update(add_dependent_regions(portal.region, logic_rules))
+ # shops have special handling
+ if not portal_name2 and portal2 == "Shop, Previous Region_":
+ portal_name2 = "Shop Portal"
+ plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both"))
+
+ non_dead_end_regions = set()
+ for region_name, region_info in tunic_er_regions.items():
+ if not region_info.dead_end:
+ non_dead_end_regions.add(region_name)
+ elif region_info.dead_end == 2 and logic_rules:
+ non_dead_end_regions.add(region_name)
+ elif region_info.dead_end == 3:
+ if (region_name == "Secret Gathering Place" and laurels_location == "10_fairies") \
+ or (region_name == "Zig Skip Exit" and fixed_shop):
+ non_dead_end_regions.add(region_name)
+
+ if plando_connections:
+ for connection in plando_connections:
+ p_entrance = connection.entrance
+ p_exit = connection.exit
+ portal1_dead_end = True
+ portal2_dead_end = True
+
+ portal1 = None
+ portal2 = None
+
+ # search two_plus for both at once
+ for portal in two_plus:
+ if p_entrance == portal.name:
+ portal1 = portal
+ portal1_dead_end = False
+ if p_exit == portal.name:
+ portal2 = portal
+ portal2_dead_end = False
+
+ # search dead_ends individually since we can't really remove items from two_plus during the loop
+ if portal1:
+ two_plus.remove(portal1)
+ else:
+ # if not both, they're both dead ends
+ if not portal2:
+ if world.options.entrance_rando.value not in EntranceRando.options.values():
+ raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead "
+ "end to a dead end in their plando connections.")
+ else:
+ raise Exception(f"{player_name} paired a dead end to a dead end in their "
+ "plando connections.")
+
+ for portal in dead_ends:
+ if p_entrance == portal.name:
+ portal1 = portal
+ break
+ if not portal1:
+ raise Exception(f"Could not find entrance named {p_entrance} for "
+ f"plando connections in {player_name}'s YAML.")
+ dead_ends.remove(portal1)
+
+ if portal2:
+ two_plus.remove(portal2)
+ else:
+ for portal in dead_ends:
+ if p_exit == portal.name:
+ portal2 = portal
+ break
+ # if it's not a dead end, it might be a shop
+ if p_exit == "Shop Portal":
+ portal2 = Portal(name="Shop Portal", region="Shop",
+ destination="Previous Region", tag="_")
+ shop_count -= 1
+ # need to maintain an even number of portals total
+ if shop_count < 0:
+ shop_count += 2
+ for p in portal_mapping:
+ if p.name == p_entrance:
+ shop_scenes.add(p.scene())
+ break
+ # and if it's neither shop nor dead end, it just isn't correct
+ else:
+ if not portal2:
+ raise Exception(f"Could not find entrance named {p_exit} for "
+ f"plando connections in {player_name}'s YAML.")
+ dead_ends.remove(portal2)
+
+ # update the traversal chart to say you can get from portal1's region to portal2's and vice versa
+ if not portal1_dead_end and not portal2_dead_end:
+ traversal_reqs.setdefault(portal1.region, dict())[portal2.region] = []
+ traversal_reqs.setdefault(portal2.region, dict())[portal1.region] = []
+
+ if (portal1.region == "Zig Skip Exit" and (portal2_dead_end or portal2.region == "Secret Gathering Place")
+ or portal2.region == "Zig Skip Exit" and (portal1_dead_end or portal1.region == "Secret Gathering Place")):
+ if world.options.entrance_rando.value not in EntranceRando.options.values():
+ raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead "
+ "end to a dead end in their plando connections.")
+ else:
+ raise Exception(f"{player_name} paired a dead end to a dead end in their "
+ "plando connections.")
+
+ if (portal1.region == "Secret Gathering Place" and (portal2_dead_end or portal2.region == "Zig Skip Exit")
+ or portal2.region == "Secret Gathering Place" and (portal1_dead_end or portal1.region == "Zig Skip Exit")):
+ # need to make sure you didn't pair this to a dead end or zig skip
+ if portal1_dead_end or portal2_dead_end or \
+ portal1.region == "Zig Skip Exit" or portal2.region == "Zig Skip Exit":
+ if world.options.entrance_rando.value not in EntranceRando.options.values():
+ raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead "
+ "end to a dead end in their plando connections.")
+ else:
+ raise Exception(f"{player_name} paired a dead end to a dead end in their "
+ "plando connections.")
+ waterfall_plando = True
+ portal_pairs[portal1] = portal2
+
+ # if we have plando connections, our connected regions may change somewhat
+ connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules)
+
+ if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"):
+ portal1 = None
+ for portal in two_plus:
+ if portal.scene_destination() == "Overworld Redux, Windmill_":
+ portal1 = portal
+ break
+ if not portal1:
+ raise Exception(f"Failed to do Fixed Shop option. "
+ f"Did {player_name} plando connection the Windmill Shop entrance?")
+
+ portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region", tag="_")
+
+ portal_pairs[portal1] = portal2
+ two_plus.remove(portal1)
+
+ random_object: Random = world.random
+ # use the seed given in the options to shuffle the portals
+ if isinstance(world.options.entrance_rando.value, str):
+ random_object = Random(world.options.entrance_rando.value)
+ # we want to start by making sure every region is accessible
+ random_object.shuffle(two_plus)
+ check_success = 0
+ portal1 = None
+ portal2 = None
+ previous_conn_num = 0
+ fail_count = 0
+ while len(connected_regions) < len(non_dead_end_regions):
+ # if this is universal tracker, just break immediately and move on
+ if hasattr(world.multiworld, "re_gen_passthrough"):
+ break
+ # if the connected regions length stays unchanged for too long, it's stuck in a loop
+ # should, hopefully, only ever occur if someone plandos connections poorly
+ if previous_conn_num == len(connected_regions):
+ fail_count += 1
+ if fail_count >= 500:
+ raise Exception(f"Failed to pair regions. Check plando connections for {player_name} for errors. "
+ "Unconnected regions:", non_dead_end_regions - connected_regions)
+ else:
+ fail_count = 0
+ previous_conn_num = len(connected_regions)
+
+ # find a portal in a connected region
+ if check_success == 0:
+ for portal in two_plus:
+ if portal.region in connected_regions:
+ portal1 = portal
+ two_plus.remove(portal)
+ check_success = 1
+ break
+
+ # then we find a portal in an inaccessible region
+ if check_success == 1:
+ for portal in two_plus:
+ if portal.region not in connected_regions:
+ # if secret gathering place happens to get paired really late, you can end up running out
+ if not has_laurels and len(two_plus) < 80:
+ # if you plando'd secret gathering place with laurels at 10 fairies, you're the reason for this
+ if waterfall_plando:
+ cr = connected_regions.copy()
+ cr.add(portal.region)
+ if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_rules):
+ continue
+ elif portal.region != "Secret Gathering Place":
+ continue
+ portal2 = portal
+ connected_regions.add(portal.region)
+ two_plus.remove(portal)
+ check_success = 2
+ break
+
+ # once we have both portals, connect them and add the new region(s) to connected_regions
+ if check_success == 2:
+ connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules)
+ if "Secret Gathering Place" in connected_regions:
+ has_laurels = True
+ portal_pairs[portal1] = portal2
+ check_success = 0
+ random_object.shuffle(two_plus)
+
+ # for universal tracker, we want to skip shop gen
+ if hasattr(world.multiworld, "re_gen_passthrough"):
+ if "TUNIC" in world.multiworld.re_gen_passthrough:
+ shop_count = 0
+
+ for i in range(shop_count):
+ portal1 = None
+ for portal in two_plus:
+ if portal.scene() not in shop_scenes:
+ shop_scenes.add(portal.scene())
+ portal1 = portal
+ two_plus.remove(portal)
+ break
+ if portal1 is None:
+ raise Exception("Too many shops in the pool, or something else went wrong.")
+ portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region", tag="_")
+
+ portal_pairs[portal1] = portal2
+
+ # connect dead ends to random non-dead ends
+ # none of the key events are in dead ends, so we don't need to do gate_before_switch
+ while len(dead_ends) > 0:
+ if hasattr(world.multiworld, "re_gen_passthrough"):
+ break
+ portal1 = two_plus.pop()
+ portal2 = dead_ends.pop()
+ portal_pairs[portal1] = portal2
+ # then randomly connect the remaining portals to each other
+ # every region is accessible, so gate_before_switch is not necessary
+ while len(two_plus) > 1:
+ if hasattr(world.multiworld, "re_gen_passthrough"):
+ break
+ portal1 = two_plus.pop()
+ portal2 = two_plus.pop()
+ portal_pairs[portal1] = portal2
+
+ if len(two_plus) == 1:
+ raise Exception("two plus had an odd number of portals, investigate this. last portal is " + two_plus[0].name)
+
+ return portal_pairs
+
+
+# loop through our list of paired portals and make two-way connections
+def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dict[str, Region]) -> None:
+ for portal1, portal2 in portal_pairs.items():
+ region1 = regions[portal1.region]
+ region2 = regions[portal2.region]
+ region1.connect(connecting_region=region2, name=portal1.name)
+ # prevent the logic from thinking you can get to any shop-connected region from the shop
+ if portal2.name not in {"Shop", "Shop Portal"}:
+ region2.connect(connecting_region=region1, name=portal2.name)
+
+
+def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[str, Dict[str, List[List[str]]]],
+ has_laurels: bool, logic: int) -> Set[str]:
+ # starting count, so we can run it again if this changes
+ region_count = len(connected_regions)
+ for origin, destinations in traversal_reqs.items():
+ if origin not in connected_regions:
+ continue
+ # check if we can traverse to any of the destinations
+ for destination, req_lists in destinations.items():
+ if destination in connected_regions:
+ continue
+ met_traversal_reqs = False
+ if len(req_lists) == 0:
+ met_traversal_reqs = True
+ # loop through each set of possible requirements, with a fancy for else loop
+ for reqs in req_lists:
+ for req in reqs:
+ if req == "Hyperdash":
+ if not has_laurels:
+ break
+ elif req == "NMG":
+ if not logic:
+ break
+ elif req == "UR":
+ if logic < 2:
+ break
+ elif req not in connected_regions:
+ break
+ else:
+ met_traversal_reqs = True
+ break
+ if met_traversal_reqs:
+ connected_regions.add(destination)
+
+ # if the length of connected_regions changed, we got new regions, so we want to check those new origins
+ if region_count != len(connected_regions):
+ connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic)
+
+ return connected_regions
diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py
new file mode 100644
index 000000000000..a8aec9f74485
--- /dev/null
+++ b/worlds/tunic/items.py
@@ -0,0 +1,243 @@
+from itertools import groupby
+from typing import Dict, List, Set, NamedTuple
+from BaseClasses import ItemClassification
+
+
+class TunicItemData(NamedTuple):
+ classification: ItemClassification
+ quantity_in_item_pool: int
+ item_id_offset: int
+ item_group: str = ""
+
+
+item_base_id = 509342400
+
+item_table: Dict[str, TunicItemData] = {
+ "Firecracker x2": TunicItemData(ItemClassification.filler, 3, 0, "Bombs"),
+ "Firecracker x3": TunicItemData(ItemClassification.filler, 3, 1, "Bombs"),
+ "Firecracker x4": TunicItemData(ItemClassification.filler, 3, 2, "Bombs"),
+ "Firecracker x5": TunicItemData(ItemClassification.filler, 1, 3, "Bombs"),
+ "Firecracker x6": TunicItemData(ItemClassification.filler, 2, 4, "Bombs"),
+ "Fire Bomb x2": TunicItemData(ItemClassification.filler, 2, 5, "Bombs"),
+ "Fire Bomb x3": TunicItemData(ItemClassification.filler, 1, 6, "Bombs"),
+ "Ice Bomb x2": TunicItemData(ItemClassification.filler, 2, 7, "Bombs"),
+ "Ice Bomb x3": TunicItemData(ItemClassification.filler, 2, 8, "Bombs"),
+ "Ice Bomb x5": TunicItemData(ItemClassification.filler, 1, 9, "Bombs"),
+ "Lure": TunicItemData(ItemClassification.filler, 4, 10, "Consumables"),
+ "Lure x2": TunicItemData(ItemClassification.filler, 1, 11, "Consumables"),
+ "Pepper x2": TunicItemData(ItemClassification.filler, 4, 12, "Consumables"),
+ "Ivy x3": TunicItemData(ItemClassification.filler, 2, 13, "Consumables"),
+ "Effigy": TunicItemData(ItemClassification.useful, 12, 14, "Money"),
+ "HP Berry": TunicItemData(ItemClassification.filler, 2, 15, "Consumables"),
+ "HP Berry x2": TunicItemData(ItemClassification.filler, 4, 16, "Consumables"),
+ "HP Berry x3": TunicItemData(ItemClassification.filler, 2, 17, "Consumables"),
+ "MP Berry": TunicItemData(ItemClassification.filler, 4, 18, "Consumables"),
+ "MP Berry x2": TunicItemData(ItemClassification.filler, 2, 19, "Consumables"),
+ "MP Berry x3": TunicItemData(ItemClassification.filler, 7, 20, "Consumables"),
+ "Fairy": TunicItemData(ItemClassification.progression, 20, 21),
+ "Stick": TunicItemData(ItemClassification.progression, 1, 22, "Weapons"),
+ "Sword": TunicItemData(ItemClassification.progression, 3, 23, "Weapons"),
+ "Sword Upgrade": TunicItemData(ItemClassification.progression, 4, 24, "Weapons"),
+ "Magic Wand": TunicItemData(ItemClassification.progression, 1, 25, "Weapons"),
+ "Magic Dagger": TunicItemData(ItemClassification.progression, 1, 26),
+ "Magic Orb": TunicItemData(ItemClassification.progression, 1, 27),
+ "Hero's Laurels": TunicItemData(ItemClassification.progression, 1, 28),
+ "Lantern": TunicItemData(ItemClassification.progression, 1, 29),
+ "Gun": TunicItemData(ItemClassification.useful, 1, 30, "Weapons"),
+ "Shield": TunicItemData(ItemClassification.useful, 1, 31),
+ "Dath Stone": TunicItemData(ItemClassification.useful, 1, 32),
+ "Hourglass": TunicItemData(ItemClassification.useful, 1, 33),
+ "Old House Key": TunicItemData(ItemClassification.progression, 1, 34, "Keys"),
+ "Key": TunicItemData(ItemClassification.progression, 2, 35, "Keys"),
+ "Fortress Vault Key": TunicItemData(ItemClassification.progression, 1, 36, "Keys"),
+ "Flask Shard": TunicItemData(ItemClassification.useful, 12, 37),
+ "Potion Flask": TunicItemData(ItemClassification.useful, 5, 38, "Flask"),
+ "Golden Coin": TunicItemData(ItemClassification.progression, 17, 39),
+ "Card Slot": TunicItemData(ItemClassification.useful, 4, 40),
+ "Red Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 41, "Hexagons"),
+ "Green Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 42, "Hexagons"),
+ "Blue Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 43, "Hexagons"),
+ "Gold Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 0, 44, "Hexagons"),
+ "ATT Offering": TunicItemData(ItemClassification.useful, 4, 45, "Offerings"),
+ "DEF Offering": TunicItemData(ItemClassification.useful, 4, 46, "Offerings"),
+ "Potion Offering": TunicItemData(ItemClassification.useful, 3, 47, "Offerings"),
+ "HP Offering": TunicItemData(ItemClassification.useful, 6, 48, "Offerings"),
+ "MP Offering": TunicItemData(ItemClassification.useful, 3, 49, "Offerings"),
+ "SP Offering": TunicItemData(ItemClassification.useful, 2, 50, "Offerings"),
+ "Hero Relic - ATT": TunicItemData(ItemClassification.progression_skip_balancing, 1, 51, "Hero Relics"),
+ "Hero Relic - DEF": TunicItemData(ItemClassification.progression_skip_balancing, 1, 52, "Hero Relics"),
+ "Hero Relic - HP": TunicItemData(ItemClassification.progression_skip_balancing, 1, 53, "Hero Relics"),
+ "Hero Relic - MP": TunicItemData(ItemClassification.progression_skip_balancing, 1, 54, "Hero Relics"),
+ "Hero Relic - POTION": TunicItemData(ItemClassification.progression_skip_balancing, 1, 55, "Hero Relics"),
+ "Hero Relic - SP": TunicItemData(ItemClassification.progression_skip_balancing, 1, 56, "Hero Relics"),
+ "Orange Peril Ring": TunicItemData(ItemClassification.useful, 1, 57, "Cards"),
+ "Tincture": TunicItemData(ItemClassification.useful, 1, 58, "Cards"),
+ "Scavenger Mask": TunicItemData(ItemClassification.progression, 1, 59, "Cards"),
+ "Cyan Peril Ring": TunicItemData(ItemClassification.useful, 1, 60, "Cards"),
+ "Bracer": TunicItemData(ItemClassification.useful, 1, 61, "Cards"),
+ "Dagger Strap": TunicItemData(ItemClassification.useful, 1, 62, "Cards"),
+ "Inverted Ash": TunicItemData(ItemClassification.useful, 1, 63, "Cards"),
+ "Lucky Cup": TunicItemData(ItemClassification.useful, 1, 64, "Cards"),
+ "Magic Echo": TunicItemData(ItemClassification.useful, 1, 65, "Cards"),
+ "Anklet": TunicItemData(ItemClassification.useful, 1, 66, "Cards"),
+ "Muffling Bell": TunicItemData(ItemClassification.useful, 1, 67, "Cards"),
+ "Glass Cannon": TunicItemData(ItemClassification.useful, 1, 68, "Cards"),
+ "Perfume": TunicItemData(ItemClassification.useful, 1, 69, "Cards"),
+ "Louder Echo": TunicItemData(ItemClassification.useful, 1, 70, "Cards"),
+ "Aura's Gem": TunicItemData(ItemClassification.useful, 1, 71, "Cards"),
+ "Bone Card": TunicItemData(ItemClassification.useful, 1, 72, "Cards"),
+ "Mr Mayor": TunicItemData(ItemClassification.useful, 1, 73, "Golden Treasures"),
+ "Secret Legend": TunicItemData(ItemClassification.useful, 1, 74, "Golden Treasures"),
+ "Sacred Geometry": TunicItemData(ItemClassification.useful, 1, 75, "Golden Treasures"),
+ "Vintage": TunicItemData(ItemClassification.useful, 1, 76, "Golden Treasures"),
+ "Just Some Pals": TunicItemData(ItemClassification.useful, 1, 77, "Golden Treasures"),
+ "Regal Weasel": TunicItemData(ItemClassification.useful, 1, 78, "Golden Treasures"),
+ "Spring Falls": TunicItemData(ItemClassification.useful, 1, 79, "Golden Treasures"),
+ "Power Up": TunicItemData(ItemClassification.useful, 1, 80, "Golden Treasures"),
+ "Back To Work": TunicItemData(ItemClassification.useful, 1, 81, "Golden Treasures"),
+ "Phonomath": TunicItemData(ItemClassification.useful, 1, 82, "Golden Treasures"),
+ "Dusty": TunicItemData(ItemClassification.useful, 1, 83, "Golden Treasures"),
+ "Forever Friend": TunicItemData(ItemClassification.useful, 1, 84, "Golden Treasures"),
+ "Fool Trap": TunicItemData(ItemClassification.trap, 0, 85),
+ "Money x1": TunicItemData(ItemClassification.filler, 3, 86, "Money"),
+ "Money x10": TunicItemData(ItemClassification.filler, 1, 87, "Money"),
+ "Money x15": TunicItemData(ItemClassification.filler, 10, 88, "Money"),
+ "Money x16": TunicItemData(ItemClassification.filler, 1, 89, "Money"),
+ "Money x20": TunicItemData(ItemClassification.filler, 17, 90, "Money"),
+ "Money x25": TunicItemData(ItemClassification.filler, 14, 91, "Money"),
+ "Money x30": TunicItemData(ItemClassification.filler, 4, 92, "Money"),
+ "Money x32": TunicItemData(ItemClassification.filler, 4, 93, "Money"),
+ "Money x40": TunicItemData(ItemClassification.filler, 3, 94, "Money"),
+ "Money x48": TunicItemData(ItemClassification.filler, 1, 95, "Money"),
+ "Money x50": TunicItemData(ItemClassification.filler, 7, 96, "Money"),
+ "Money x64": TunicItemData(ItemClassification.filler, 1, 97, "Money"),
+ "Money x100": TunicItemData(ItemClassification.filler, 5, 98, "Money"),
+ "Money x128": TunicItemData(ItemClassification.useful, 3, 99, "Money"),
+ "Money x200": TunicItemData(ItemClassification.useful, 1, 100, "Money"),
+ "Money x255": TunicItemData(ItemClassification.useful, 1, 101, "Money"),
+ "Pages 0-1": TunicItemData(ItemClassification.useful, 1, 102, "Pages"),
+ "Pages 2-3": TunicItemData(ItemClassification.useful, 1, 103, "Pages"),
+ "Pages 4-5": TunicItemData(ItemClassification.useful, 1, 104, "Pages"),
+ "Pages 6-7": TunicItemData(ItemClassification.useful, 1, 105, "Pages"),
+ "Pages 8-9": TunicItemData(ItemClassification.useful, 1, 106, "Pages"),
+ "Pages 10-11": TunicItemData(ItemClassification.useful, 1, 107, "Pages"),
+ "Pages 12-13": TunicItemData(ItemClassification.useful, 1, 108, "Pages"),
+ "Pages 14-15": TunicItemData(ItemClassification.useful, 1, 109, "Pages"),
+ "Pages 16-17": TunicItemData(ItemClassification.useful, 1, 110, "Pages"),
+ "Pages 18-19": TunicItemData(ItemClassification.useful, 1, 111, "Pages"),
+ "Pages 20-21": TunicItemData(ItemClassification.useful, 1, 112, "Pages"),
+ "Pages 22-23": TunicItemData(ItemClassification.useful, 1, 113, "Pages"),
+ "Pages 24-25 (Prayer)": TunicItemData(ItemClassification.progression, 1, 114, "Pages"),
+ "Pages 26-27": TunicItemData(ItemClassification.useful, 1, 115, "Pages"),
+ "Pages 28-29": TunicItemData(ItemClassification.useful, 1, 116, "Pages"),
+ "Pages 30-31": TunicItemData(ItemClassification.useful, 1, 117, "Pages"),
+ "Pages 32-33": TunicItemData(ItemClassification.useful, 1, 118, "Pages"),
+ "Pages 34-35": TunicItemData(ItemClassification.useful, 1, 119, "Pages"),
+ "Pages 36-37": TunicItemData(ItemClassification.useful, 1, 120, "Pages"),
+ "Pages 38-39": TunicItemData(ItemClassification.useful, 1, 121, "Pages"),
+ "Pages 40-41": TunicItemData(ItemClassification.useful, 1, 122, "Pages"),
+ "Pages 42-43 (Holy Cross)": TunicItemData(ItemClassification.progression, 1, 123, "Pages"),
+ "Pages 44-45": TunicItemData(ItemClassification.useful, 1, 124, "Pages"),
+ "Pages 46-47": TunicItemData(ItemClassification.useful, 1, 125, "Pages"),
+ "Pages 48-49": TunicItemData(ItemClassification.useful, 1, 126, "Pages"),
+ "Pages 50-51": TunicItemData(ItemClassification.useful, 1, 127, "Pages"),
+ "Pages 52-53 (Icebolt)": TunicItemData(ItemClassification.progression, 1, 128, "Pages"),
+ "Pages 54-55": TunicItemData(ItemClassification.useful, 1, 129, "Pages"),
+ "Ladders near Weathervane": TunicItemData(ItemClassification.progression, 0, 130, "Ladders"),
+ "Ladders near Overworld Checkpoint": TunicItemData(ItemClassification.progression, 0, 131, "Ladders"),
+ "Ladders near Patrol Cave": TunicItemData(ItemClassification.progression, 0, 132, "Ladders"),
+ "Ladder near Temple Rafters": TunicItemData(ItemClassification.progression, 0, 133, "Ladders"),
+ "Ladders near Dark Tomb": TunicItemData(ItemClassification.progression, 0, 134, "Ladders"),
+ "Ladder to Quarry": TunicItemData(ItemClassification.progression, 0, 135, "Ladders"),
+ "Ladders to West Bell": TunicItemData(ItemClassification.progression, 0, 136, "Ladders"),
+ "Ladders in Overworld Town": TunicItemData(ItemClassification.progression, 0, 137, "Ladders"),
+ "Ladder to Ruined Atoll": TunicItemData(ItemClassification.progression, 0, 138, "Ladders"),
+ "Ladder to Swamp": TunicItemData(ItemClassification.progression, 0, 139, "Ladders"),
+ "Ladders in Well": TunicItemData(ItemClassification.progression, 0, 140, "Ladders"),
+ "Ladder in Dark Tomb": TunicItemData(ItemClassification.progression, 0, 141, "Ladders"),
+ "Ladder to East Forest": TunicItemData(ItemClassification.progression, 0, 142, "Ladders"),
+ "Ladders to Lower Forest": TunicItemData(ItemClassification.progression, 0, 143, "Ladders"),
+ "Ladder to Beneath the Vault": TunicItemData(ItemClassification.progression, 0, 144, "Ladders"),
+ "Ladders in Hourglass Cave": TunicItemData(ItemClassification.progression, 0, 145, "Ladders"),
+ "Ladders in South Atoll": TunicItemData(ItemClassification.progression, 0, 146, "Ladders"),
+ "Ladders to Frog's Domain": TunicItemData(ItemClassification.progression, 0, 147, "Ladders"),
+ "Ladders in Library": TunicItemData(ItemClassification.progression, 0, 148, "Ladders"),
+ "Ladders in Lower Quarry": TunicItemData(ItemClassification.progression, 0, 149, "Ladders"),
+ "Ladders in Swamp": TunicItemData(ItemClassification.progression, 0, 150, "Ladders"),
+}
+
+fool_tiers: List[List[str]] = [
+ [],
+ ["Money x1", "Money x10", "Money x15", "Money x16"],
+ ["Money x1", "Money x10", "Money x15", "Money x16", "Money x20"],
+ ["Money x1", "Money x10", "Money x15", "Money x16", "Money x20", "Money x25", "Money x30"],
+]
+
+slot_data_item_names = [
+ "Stick",
+ "Sword",
+ "Sword Upgrade",
+ "Magic Dagger",
+ "Magic Wand",
+ "Magic Orb",
+ "Hero's Laurels",
+ "Lantern",
+ "Gun",
+ "Scavenger Mask",
+ "Shield",
+ "Dath Stone",
+ "Hourglass",
+ "Old House Key",
+ "Fortress Vault Key",
+ "Hero Relic - ATT",
+ "Hero Relic - DEF",
+ "Hero Relic - POTION",
+ "Hero Relic - HP",
+ "Hero Relic - SP",
+ "Hero Relic - MP",
+ "Pages 24-25 (Prayer)",
+ "Pages 42-43 (Holy Cross)",
+ "Pages 52-53 (Icebolt)",
+ "Red Questagon",
+ "Green Questagon",
+ "Blue Questagon",
+ "Gold Questagon",
+]
+
+item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()}
+
+filler_items: List[str] = [name for name, data in item_table.items() if data.classification == ItemClassification.filler]
+
+
+def get_item_group(item_name: str) -> str:
+ return item_table[item_name].item_group
+
+
+item_name_groups: Dict[str, Set[str]] = {
+ group: set(item_names) for group, item_names in groupby(sorted(item_table, key=get_item_group), get_item_group) if group != ""
+}
+
+# extra groups for the purpose of aliasing items
+extra_groups: Dict[str, Set[str]] = {
+ "Laurels": {"Hero's Laurels"},
+ "Orb": {"Magic Orb"},
+ "Dagger": {"Magic Dagger"},
+ "Wand": {"Magic Wand"},
+ "Magic Rod": {"Magic Wand"},
+ "Fire Rod": {"Magic Wand"},
+ "Holy Cross": {"Pages 42-43 (Holy Cross)"},
+ "Prayer": {"Pages 24-25 (Prayer)"},
+ "Icebolt": {"Pages 52-53 (Icebolt)"},
+ "Ice Rod": {"Pages 52-53 (Icebolt)"},
+ "Melee Weapons": {"Stick", "Sword", "Sword Upgrade"},
+ "Progressive Sword": {"Sword Upgrade"},
+ "Abilities": {"Pages 24-25 (Prayer)", "Pages 42-43 (Holy Cross)", "Pages 52-53 (Icebolt)"},
+ "Questagons": {"Red Questagon", "Green Questagon", "Blue Questagon", "Gold Questagon"},
+ "Ladder to Atoll": {"Ladder to Ruined Atoll"}, # fuzzy matching made it hint Ladders in Well, now it won't
+ "Ladders to Bell": {"Ladders to West Bell"},
+ "Ladders to Well": {"Ladders in Well"}, # fuzzy matching decided ladders in well was ladders to west bell
+ "Ladders in Atoll": {"Ladders in South Atoll"},
+ "Ladders in Ruined Atoll": {"Ladders in South Atoll"},
+}
+
+item_name_groups.update(extra_groups)
diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py
new file mode 100644
index 000000000000..09916228163d
--- /dev/null
+++ b/worlds/tunic/locations.py
@@ -0,0 +1,330 @@
+from typing import Dict, NamedTuple, Set, Optional
+
+
+class TunicLocationData(NamedTuple):
+ region: str
+ er_region: str # entrance rando region
+ location_group: Optional[str] = None
+
+
+location_base_id = 509342400
+
+location_table: Dict[str, TunicLocationData] = {
+ "Beneath the Well - [Powered Secret Room] Chest": TunicLocationData("Beneath the Well", "Beneath the Well Back"),
+ "Beneath the Well - [Entryway] Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
+ "Beneath the Well - [Third Room] Beneath Platform Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
+ "Beneath the Well - [Third Room] Tentacle Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
+ "Beneath the Well - [Entryway] Obscured Behind Waterfall": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
+ "Beneath the Well - [Save Room] Upper Floor Chest 1": TunicLocationData("Beneath the Well", "Beneath the Well Back"),
+ "Beneath the Well - [Save Room] Upper Floor Chest 2": TunicLocationData("Beneath the Well", "Beneath the Well Back"),
+ "Beneath the Well - [Second Room] Underwater Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
+ "Beneath the Well - [Back Corridor] Right Secret": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
+ "Beneath the Well - [Back Corridor] Left Secret": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
+ "Beneath the Well - [Second Room] Obscured Behind Waterfall": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
+ "Beneath the Well - [Side Room] Chest By Pots": TunicLocationData("Beneath the Well", "Beneath the Well Back"),
+ "Beneath the Well - [Side Room] Chest By Phrends": TunicLocationData("Beneath the Well", "Beneath the Well Back"),
+ "Beneath the Well - [Second Room] Page": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
+ "Dark Tomb Checkpoint - [Passage To Dark Tomb] Page Pickup": TunicLocationData("Overworld", "Dark Tomb Checkpoint"),
+ "Cathedral - [1F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"),
+ "Cathedral - [1F] Near Spikes": TunicLocationData("Cathedral", "Cathedral"),
+ "Cathedral - [2F] Bird Room": TunicLocationData("Cathedral", "Cathedral"),
+ "Cathedral - [2F] Entryway Upper Walkway": TunicLocationData("Cathedral", "Cathedral"),
+ "Cathedral - [1F] Library": TunicLocationData("Cathedral", "Cathedral"),
+ "Cathedral - [2F] Library": TunicLocationData("Cathedral", "Cathedral"),
+ "Cathedral - [2F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"),
+ "Cathedral - [2F] Bird Room Secret": TunicLocationData("Cathedral", "Cathedral"),
+ "Cathedral - [1F] Library Secret": TunicLocationData("Cathedral", "Cathedral"),
+ "Dark Tomb - Spike Maze Near Exit": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
+ "Dark Tomb - 2nd Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
+ "Dark Tomb - 1st Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
+ "Dark Tomb - Spike Maze Upper Walkway": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
+ "Dark Tomb - Skulls Chest": TunicLocationData("Dark Tomb", "Dark Tomb Upper"),
+ "Dark Tomb - Spike Maze Near Stairs": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
+ "Dark Tomb - 1st Laser Room Obscured": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
+ "Guardhouse 2 - Upper Floor": TunicLocationData("East Forest", "Guard House 2 Upper"),
+ "Guardhouse 2 - Bottom Floor Secret": TunicLocationData("East Forest", "Guard House 2 Lower"),
+ "Guardhouse 1 - Upper Floor Obscured": TunicLocationData("East Forest", "Guard House 1 East"),
+ "Guardhouse 1 - Upper Floor": TunicLocationData("East Forest", "Guard House 1 East"),
+ "East Forest - Dancing Fox Spirit Holy Cross": TunicLocationData("East Forest", "East Forest Dance Fox Spot", location_group="Holy Cross"),
+ "East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "Lower Forest", location_group="Holy Cross"),
+ "East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "East Forest"),
+ "East Forest - Above Save Point": TunicLocationData("East Forest", "East Forest"),
+ "East Forest - Above Save Point Obscured": TunicLocationData("East Forest", "East Forest"),
+ "East Forest - From Guardhouse 1 Chest": TunicLocationData("East Forest", "East Forest Dance Fox Spot"),
+ "East Forest - Near Save Point": TunicLocationData("East Forest", "East Forest"),
+ "East Forest - Beneath Spider Chest": TunicLocationData("East Forest", "Lower Forest"),
+ "East Forest - Near Telescope": TunicLocationData("East Forest", "East Forest"),
+ "East Forest - Spider Chest": TunicLocationData("East Forest", "Lower Forest"),
+ "East Forest - Lower Dash Chest": TunicLocationData("East Forest", "Lower Forest"),
+ "East Forest - Lower Grapple Chest": TunicLocationData("East Forest", "Lower Forest"),
+ "East Forest - Bombable Wall": TunicLocationData("East Forest", "East Forest"),
+ "East Forest - Page On Teleporter": TunicLocationData("East Forest", "East Forest"),
+ "Forest Belltower - Near Save Point": TunicLocationData("East Forest", "Forest Belltower Lower"),
+ "Forest Belltower - After Guard Captain": TunicLocationData("East Forest", "Forest Belltower Upper"),
+ "Forest Belltower - Obscured Near Bell Top Floor": TunicLocationData("East Forest", "Forest Belltower Upper"),
+ "Forest Belltower - Obscured Beneath Bell Bottom Floor": TunicLocationData("East Forest", "Forest Belltower Main"),
+ "Forest Belltower - Page Pickup": TunicLocationData("East Forest", "Forest Belltower Main"),
+ "Forest Grave Path - Holy Cross Code by Grave": TunicLocationData("East Forest", "Forest Grave Path by Grave", location_group="Holy Cross"),
+ "Forest Grave Path - Above Gate": TunicLocationData("East Forest", "Forest Grave Path Main"),
+ "Forest Grave Path - Obscured Chest": TunicLocationData("East Forest", "Forest Grave Path Main"),
+ "Forest Grave Path - Upper Walkway": TunicLocationData("East Forest", "Forest Grave Path Upper"),
+ "Forest Grave Path - Sword Pickup": TunicLocationData("East Forest", "Forest Grave Path by Grave"),
+ "Hero's Grave - Tooth Relic": TunicLocationData("East Forest", "Hero Relic - East Forest"),
+ "Fortress Courtyard - From East Belltower": TunicLocationData("East Forest", "Fortress Exterior from East Forest"),
+ "Fortress Leaf Piles - Secret Chest": TunicLocationData("Eastern Vault Fortress", "Fortress Leaf Piles"),
+ "Fortress Arena - Hexagon Red": TunicLocationData("Eastern Vault Fortress", "Fortress Arena"),
+ "Fortress Arena - Siege Engine/Vault Key Pickup": TunicLocationData("Eastern Vault Fortress", "Fortress Arena", location_group="Bosses"),
+ "Fortress East Shortcut - Chest Near Slimes": TunicLocationData("Eastern Vault Fortress", "Fortress East Shortcut Lower"),
+ "Eastern Vault Fortress - [West Wing] Candles Holy Cross": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress", location_group="Holy Cross"),
+ "Eastern Vault Fortress - [West Wing] Dark Room Chest 1": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
+ "Eastern Vault Fortress - [West Wing] Dark Room Chest 2": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
+ "Eastern Vault Fortress - [East Wing] Bombable Wall": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
+ "Eastern Vault Fortress - [West Wing] Page Pickup": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
+ "Fortress Grave Path - Upper Walkway": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path Upper"),
+ "Fortress Grave Path - Chest Right of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"),
+ "Fortress Grave Path - Obscured Chest Left of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"),
+ "Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"),
+ "Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
+ "Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
+ "Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Main"),
+ "Beneath the Fortress - Back Room Chest": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
+ "Beneath the Fortress - Cell Chest 2": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
+ "Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain"),
+ "Frog's Domain - Slorm Room": TunicLocationData("Frog's Domain", "Frog's Domain"),
+ "Frog's Domain - Escape Chest": TunicLocationData("Frog's Domain", "Frog's Domain Back"),
+ "Frog's Domain - Grapple Above Hot Tub": TunicLocationData("Frog's Domain", "Frog's Domain"),
+ "Frog's Domain - Above Vault": TunicLocationData("Frog's Domain", "Frog's Domain"),
+ "Frog's Domain - Main Room Top Floor": TunicLocationData("Frog's Domain", "Frog's Domain"),
+ "Frog's Domain - Main Room Bottom Floor": TunicLocationData("Frog's Domain", "Frog's Domain"),
+ "Frog's Domain - Side Room Secret Passage": TunicLocationData("Frog's Domain", "Frog's Domain"),
+ "Frog's Domain - Side Room Chest": TunicLocationData("Frog's Domain", "Frog's Domain"),
+ "Frog's Domain - Side Room Grapple Secret": TunicLocationData("Frog's Domain", "Frog's Domain"),
+ "Frog's Domain - Magic Orb Pickup": TunicLocationData("Frog's Domain", "Frog's Domain"),
+ "Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena", location_group="Bosses"),
+ "Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", location_group="Holy Cross"),
+ "Library Lab - Chest By Shrine 2": TunicLocationData("Library", "Library Lab"),
+ "Library Lab - Chest By Shrine 1": TunicLocationData("Library", "Library Lab"),
+ "Library Lab - Chest By Shrine 3": TunicLocationData("Library", "Library Lab"),
+ "Library Lab - Behind Chalkboard by Fuse": TunicLocationData("Library", "Library Lab"),
+ "Library Lab - Page 3": TunicLocationData("Library", "Library Lab"),
+ "Library Lab - Page 1": TunicLocationData("Library", "Library Lab"),
+ "Library Lab - Page 2": TunicLocationData("Library", "Library Lab"),
+ "Hero's Grave - Mushroom Relic": TunicLocationData("Library", "Hero Relic - Library"),
+ "Lower Mountain - Page Before Door": TunicLocationData("Overworld", "Lower Mountain"),
+ "Changing Room - Normal Chest": TunicLocationData("Overworld", "Changing Room"),
+ "Fortress Courtyard - Chest Near Cave": TunicLocationData("Overworld", "Fortress Exterior near cave"),
+ "Fortress Courtyard - Near Fuse": TunicLocationData("Overworld", "Fortress Exterior from Overworld"),
+ "Fortress Courtyard - Below Walkway": TunicLocationData("Overworld", "Fortress Exterior from Overworld"),
+ "Fortress Courtyard - Page Near Cave": TunicLocationData("Overworld", "Fortress Exterior near cave"),
+ "West Furnace - Lantern Pickup": TunicLocationData("Overworld", "Furnace Fuse"),
+ "Maze Cave - Maze Room Chest": TunicLocationData("Overworld", "Maze Cave"),
+ "Old House - Normal Chest": TunicLocationData("Overworld", "Old House Front"),
+ "Old House - Shield Pickup": TunicLocationData("Overworld", "Old House Front"),
+ "Overworld - [West] Obscured Behind Windmill": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [South] Beach Chest": TunicLocationData("Overworld", "Overworld Beach"),
+ "Overworld - [West] Obscured Near Well": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [Central] Bombable Wall": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [Northwest] Chest Near Turret": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [East] Chest Near Pots": TunicLocationData("Overworld", "East Overworld"),
+ "Overworld - [Northwest] Chest Near Golden Obelisk": TunicLocationData("Overworld", "Overworld above Quarry Entrance"),
+ "Overworld - [Southwest] South Chest Near Guard": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [Southwest] West Beach Guarded By Turret": TunicLocationData("Overworld", "Overworld Beach"),
+ "Overworld - [Southwest] Chest Guarded By Turret": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [Northwest] Shadowy Corner Chest": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [Southwest] Obscured In Tunnel To Beach": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [Southwest] Grapple Chest Over Walkway": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [Northwest] Chest Beneath Quarry Gate": TunicLocationData("Overworld", "Overworld after Envoy"),
+ "Overworld - [Southeast] Chest Near Swamp": TunicLocationData("Overworld", "Overworld Swamp Lower Entry"),
+ "Overworld - [Southwest] From West Garden": TunicLocationData("Overworld", "Overworld Beach"),
+ "Overworld - [East] Grapple Chest": TunicLocationData("Overworld", "Overworld above Patrol Cave"),
+ "Overworld - [Southwest] West Beach Guarded By Turret 2": TunicLocationData("Overworld", "Overworld Beach"),
+ "Overworld - [Southwest] Beach Chest Near Flowers": TunicLocationData("Overworld", "Overworld Beach"),
+ "Overworld - [Southwest] Bombable Wall Near Fountain": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [West] Chest After Bell": TunicLocationData("Overworld", "Overworld Belltower"),
+ "Overworld - [Southwest] Tunnel Guarded By Turret": TunicLocationData("Overworld", "Overworld Tunnel Turret"),
+ "Overworld - [East] Between Ladders Near Ruined Passage": TunicLocationData("Overworld", "After Ruined Passage"),
+ "Overworld - [Northeast] Chest Above Patrol Cave": TunicLocationData("Overworld", "Upper Overworld"),
+ "Overworld - [Southwest] Beach Chest Beneath Guard": TunicLocationData("Overworld", "Overworld Beach"),
+ "Overworld - [Central] Chest Across From Well": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [Northwest] Chest Near Quarry Gate": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [East] Chest In Trees": TunicLocationData("Overworld", "Above Ruined Passage"),
+ "Overworld - [West] Chest Behind Moss Wall": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [South] Beach Page": TunicLocationData("Overworld", "Overworld Beach"),
+ "Overworld - [Southeast] Page on Pillar by Swamp": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [Southwest] Key Pickup": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [West] Key Pickup": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [East] Page Near Secret Shop": TunicLocationData("Overworld", "East Overworld"),
+ "Overworld - [Southwest] Fountain Page": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [Northwest] Page on Pillar by Dark Tomb": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [Northwest] Fire Wand Pickup": TunicLocationData("Overworld", "Upper Overworld"),
+ "Overworld - [West] Page On Teleporter": TunicLocationData("Overworld", "Overworld"),
+ "Overworld - [Northwest] Page By Well": TunicLocationData("Overworld", "Overworld"),
+ "Patrol Cave - Normal Chest": TunicLocationData("Overworld", "Patrol Cave"),
+ "Ruined Shop - Chest 1": TunicLocationData("Overworld", "Ruined Shop"),
+ "Ruined Shop - Chest 2": TunicLocationData("Overworld", "Ruined Shop"),
+ "Ruined Shop - Chest 3": TunicLocationData("Overworld", "Ruined Shop"),
+ "Ruined Passage - Page Pickup": TunicLocationData("Overworld", "Ruined Passage"),
+ "Shop - Potion 1": TunicLocationData("Overworld", "Shop"),
+ "Shop - Potion 2": TunicLocationData("Overworld", "Shop"),
+ "Shop - Coin 1": TunicLocationData("Overworld", "Shop"),
+ "Shop - Coin 2": TunicLocationData("Overworld", "Shop"),
+ "Special Shop - Secret Page Pickup": TunicLocationData("Overworld", "Special Shop"),
+ "Stick House - Stick Chest": TunicLocationData("Overworld", "Stick House"),
+ "Sealed Temple - Page Pickup": TunicLocationData("Overworld", "Sealed Temple"),
+ "Hourglass Cave - Hourglass Chest": TunicLocationData("Overworld", "Hourglass Cave"),
+ "Far Shore - Secret Chest": TunicLocationData("Overworld", "Far Shore"),
+ "Far Shore - Page Pickup": TunicLocationData("Overworld", "Far Shore to Spawn Region"),
+ "Coins in the Well - 10 Coins": TunicLocationData("Overworld", "Overworld", location_group="Well"),
+ "Coins in the Well - 15 Coins": TunicLocationData("Overworld", "Overworld", location_group="Well"),
+ "Coins in the Well - 3 Coins": TunicLocationData("Overworld", "Overworld", location_group="Well"),
+ "Coins in the Well - 6 Coins": TunicLocationData("Overworld", "Overworld", location_group="Well"),
+ "Secret Gathering Place - 20 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", location_group="Fairies"),
+ "Secret Gathering Place - 10 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", location_group="Fairies"),
+ "Overworld - [West] Moss Wall Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"),
+ "Overworld - [Southwest] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Beach", location_group="Holy Cross"),
+ "Overworld - [Southwest] Fountain Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"),
+ "Overworld - [Northeast] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "East Overworld", location_group="Holy Cross"),
+ "Overworld - [East] Weathervane Holy Cross": TunicLocationData("Overworld Holy Cross", "East Overworld", location_group="Holy Cross"),
+ "Overworld - [West] Windmill Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"),
+ "Overworld - [Southwest] Haiku Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Beach", location_group="Holy Cross"),
+ "Overworld - [West] Windchimes Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"),
+ "Overworld - [South] Starting Platform Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"),
+ "Overworld - [Northwest] Golden Obelisk Page": TunicLocationData("Overworld Holy Cross", "Upper Overworld", location_group="Holy Cross"),
+ "Old House - Holy Cross Door Page": TunicLocationData("Overworld Holy Cross", "Old House Back", location_group="Holy Cross"),
+ "Cube Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Cube Cave", location_group="Holy Cross"),
+ "Southeast Cross Door - Chest 3": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="Holy Cross"),
+ "Southeast Cross Door - Chest 2": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="Holy Cross"),
+ "Southeast Cross Door - Chest 1": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="Holy Cross"),
+ "Maze Cave - Maze Room Holy Cross": TunicLocationData("Overworld Holy Cross", "Maze Cave", location_group="Holy Cross"),
+ "Caustic Light Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Caustic Light Cave", location_group="Holy Cross"),
+ "Old House - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Old House Front", location_group="Holy Cross"),
+ "Patrol Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Patrol Cave", location_group="Holy Cross"),
+ "Ruined Passage - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Ruined Passage", location_group="Holy Cross"),
+ "Hourglass Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Hourglass Cave Tower", location_group="Holy Cross"),
+ "Sealed Temple - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Sealed Temple", location_group="Holy Cross"),
+ "Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", location_group="Holy Cross"),
+ "Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", location_group="Holy Cross"),
+ "Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", location_group="Holy Cross"),
+ "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"),
+ "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="Holy Cross"),
+ "Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"),
+ "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry Back", "Quarry Back"),
+ "Quarry - [East] Near Telescope": TunicLocationData("Quarry", "Quarry"),
+ "Quarry - [East] Upper Floor": TunicLocationData("Quarry", "Quarry"),
+ "Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry Back", "Quarry Back"),
+ "Quarry - [East] Obscured Near Winding Staircase": TunicLocationData("Quarry", "Quarry"),
+ "Quarry - [East] Obscured Beneath Scaffolding": TunicLocationData("Quarry", "Quarry"),
+ "Quarry - [East] Obscured Near Telescope": TunicLocationData("Quarry", "Quarry"),
+ "Quarry - [Back Entrance] Obscured Behind Wall": TunicLocationData("Quarry Back", "Quarry Back"),
+ "Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry Back", "Quarry Back"),
+ "Quarry - [Central] Top Floor Overhang": TunicLocationData("Quarry", "Quarry"),
+ "Quarry - [East] Near Bridge": TunicLocationData("Quarry", "Quarry"),
+ "Quarry - [Central] Above Ladder": TunicLocationData("Quarry", "Quarry Monastery Entry"),
+ "Quarry - [Central] Obscured Behind Staircase": TunicLocationData("Quarry", "Quarry"),
+ "Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Quarry", "Quarry Monastery Entry"),
+ "Quarry - [West] Upper Area Bombable Wall": TunicLocationData("Quarry Back", "Quarry Back"),
+ "Quarry - [East] Bombable Wall": TunicLocationData("Quarry", "Quarry"),
+ "Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry"),
+ "Quarry - [West] Shooting Range Secret Path": TunicLocationData("Lower Quarry", "Lower Quarry"),
+ "Quarry - [West] Near Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"),
+ "Quarry - [West] Below Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"),
+ "Quarry - [Lowlands] Below Broken Ladder": TunicLocationData("Lower Quarry", "Even Lower Quarry"),
+ "Quarry - [West] Upper Area Near Waterfall": TunicLocationData("Lower Quarry", "Lower Quarry"),
+ "Quarry - [Lowlands] Upper Walkway": TunicLocationData("Lower Quarry", "Even Lower Quarry"),
+ "Quarry - [West] Lower Area Below Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"),
+ "Quarry - [West] Lower Area Isolated Chest": TunicLocationData("Lower Quarry", "Lower Quarry"),
+ "Quarry - [Lowlands] Near Elevator": TunicLocationData("Lower Quarry", "Even Lower Quarry"),
+ "Quarry - [West] Lower Area After Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"),
+ "Rooted Ziggurat Upper - Near Bridge Switch": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Front"),
+ "Rooted Ziggurat Upper - Beneath Bridge To Administrator": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Back"),
+ "Rooted Ziggurat Tower - Inside Tower": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Middle Top"),
+ "Rooted Ziggurat Lower - Near Corpses": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
+ "Rooted Ziggurat Lower - Spider Ambush": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
+ "Rooted Ziggurat Lower - Left Of Checkpoint Before Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
+ "Rooted Ziggurat Lower - After Guarded Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
+ "Rooted Ziggurat Lower - Guarded By Double Turrets": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
+ "Rooted Ziggurat Lower - After 2nd Double Turret Chest": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
+ "Rooted Ziggurat Lower - Guarded By Double Turrets 2": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
+ "Rooted Ziggurat Lower - Hexagon Blue": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Back", location_group="Bosses"),
+ "Ruined Atoll - [West] Near Kevin Block": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
+ "Ruined Atoll - [South] Upper Floor On Power Line": TunicLocationData("Ruined Atoll", "Ruined Atoll Ladder Tops"),
+ "Ruined Atoll - [South] Chest Near Big Crabs": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
+ "Ruined Atoll - [North] Guarded By Bird": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
+ "Ruined Atoll - [Northeast] Chest Beneath Brick Walkway": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
+ "Ruined Atoll - [Northwest] Bombable Wall": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
+ "Ruined Atoll - [North] Obscured Beneath Bridge": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
+ "Ruined Atoll - [South] Upper Floor On Bricks": TunicLocationData("Ruined Atoll", "Ruined Atoll Ladder Tops"),
+ "Ruined Atoll - [South] Near Birds": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
+ "Ruined Atoll - [Northwest] Behind Envoy": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
+ "Ruined Atoll - [Southwest] Obscured Behind Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
+ "Ruined Atoll - [East] Locked Room Upper Chest": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
+ "Ruined Atoll - [North] From Lower Overworld Entrance": TunicLocationData("Ruined Atoll", "Ruined Atoll Lower Entry Area"),
+ "Ruined Atoll - [East] Locked Room Lower Chest": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
+ "Ruined Atoll - [Northeast] Chest On Brick Walkway": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
+ "Ruined Atoll - [Southeast] Chest Near Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll Ladder Tops"),
+ "Ruined Atoll - [Northeast] Key Pickup": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
+ "Cathedral Gauntlet - Gauntlet Reward": TunicLocationData("Swamp", "Cathedral Gauntlet"),
+ "Cathedral - Secret Legend Trophy Chest": TunicLocationData("Swamp", "Cathedral Secret Legend Room"),
+ "Swamp - [Upper Graveyard] Obscured Behind Hill": TunicLocationData("Swamp", "Swamp Mid"),
+ "Swamp - [South Graveyard] 4 Orange Skulls": TunicLocationData("Swamp", "Swamp Front"),
+ "Swamp - [Central] Near Ramps Up": TunicLocationData("Swamp", "Swamp Mid"),
+ "Swamp - [Upper Graveyard] Near Shield Fleemers": TunicLocationData("Swamp", "Swamp Mid"),
+ "Swamp - [South Graveyard] Obscured Behind Ridge": TunicLocationData("Swamp", "Swamp Mid"),
+ "Swamp - [South Graveyard] Obscured Beneath Telescope": TunicLocationData("Swamp", "Swamp Front"),
+ "Swamp - [Entrance] Above Entryway": TunicLocationData("Swamp", "Back of Swamp Laurels Area"),
+ "Swamp - [Central] South Secret Passage": TunicLocationData("Swamp", "Swamp Mid"),
+ "Swamp - [South Graveyard] Upper Walkway On Pedestal": TunicLocationData("Swamp", "Swamp Front"),
+ "Swamp - [South Graveyard] Guarded By Tentacles": TunicLocationData("Swamp", "Swamp Front"),
+ "Swamp - [Upper Graveyard] Near Telescope": TunicLocationData("Swamp", "Swamp Mid"),
+ "Swamp - [Outside Cathedral] Near Moonlight Bridge Door": TunicLocationData("Swamp", "Swamp Ledge under Cathedral Door"),
+ "Swamp - [Entrance] Obscured Inside Watchtower": TunicLocationData("Swamp", "Swamp Front"),
+ "Swamp - [Entrance] South Near Fence": TunicLocationData("Swamp", "Swamp Front"),
+ "Swamp - [South Graveyard] Guarded By Big Skeleton": TunicLocationData("Swamp", "Swamp Front"),
+ "Swamp - [South Graveyard] Chest Near Graves": TunicLocationData("Swamp", "Swamp Front"),
+ "Swamp - [Entrance] North Small Island": TunicLocationData("Swamp", "Swamp Front"),
+ "Swamp - [Outside Cathedral] Obscured Behind Memorial": TunicLocationData("Swamp", "Back of Swamp"),
+ "Swamp - [Central] Obscured Behind Northern Mountain": TunicLocationData("Swamp", "Swamp Mid"),
+ "Swamp - [South Graveyard] Upper Walkway Dash Chest": TunicLocationData("Swamp", "Swamp Mid"),
+ "Swamp - [South Graveyard] Above Big Skeleton": TunicLocationData("Swamp", "Swamp Front"),
+ "Swamp - [Central] Beneath Memorial": TunicLocationData("Swamp", "Swamp Mid"),
+ "Hero's Grave - Feathers Relic": TunicLocationData("Swamp", "Hero Relic - Swamp"),
+ "West Furnace - Chest": TunicLocationData("West Garden", "Furnace Walking Path"),
+ "Overworld - [West] Near West Garden Entrance": TunicLocationData("West Garden", "Overworld to West Garden from Furnace"),
+ "West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"),
+ "West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"),
+ "West Garden - [Southeast Lowlands] Outside Cave": TunicLocationData("West Garden", "West Garden"),
+ "West Garden - [Central Lowlands] Chest Beneath Faeries": TunicLocationData("West Garden", "West Garden"),
+ "West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"),
+ "West Garden - [Central Highlands] Top of Ladder Before Boss": TunicLocationData("West Garden", "West Garden"),
+ "West Garden - [Central Lowlands] Passage Beneath Bridge": TunicLocationData("West Garden", "West Garden"),
+ "West Garden - [North] Across From Page Pickup": TunicLocationData("West Garden", "West Garden"),
+ "West Garden - [Central Lowlands] Below Left Walkway": TunicLocationData("West Garden", "West Garden"),
+ "West Garden - [West] In Flooded Walkway": TunicLocationData("West Garden", "West Garden"),
+ "West Garden - [West] Past Flooded Walkway": TunicLocationData("West Garden", "West Garden"),
+ "West Garden - [North] Obscured Beneath Hero's Memorial": TunicLocationData("West Garden", "West Garden"),
+ "West Garden - [Central Lowlands] Chest Near Shortcut Bridge": TunicLocationData("West Garden", "West Garden"),
+ "West Garden - [West Highlands] Upper Left Walkway": TunicLocationData("West Garden", "West Garden"),
+ "West Garden - [Central Lowlands] Chest Beneath Save Point": TunicLocationData("West Garden", "West Garden"),
+ "West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden"),
+ "West Garden - [Central Highlands] After Garden Knight": TunicLocationData("Overworld", "West Garden after Boss", location_group="Bosses"),
+ "West Garden - [South Highlands] Secret Chest Beneath Fuse": TunicLocationData("West Garden", "West Garden"),
+ "West Garden - [East Lowlands] Page Behind Ice Dagger House": TunicLocationData("West Garden", "West Garden Portal Item"),
+ "West Garden - [North] Page Pickup": TunicLocationData("West Garden", "West Garden"),
+ "West Garden House - [Southeast Lowlands] Ice Dagger Pickup": TunicLocationData("West Garden", "Magic Dagger House"),
+ "Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden"),
+}
+
+hexagon_locations: Dict[str, str] = {
+ "Red Questagon": "Fortress Arena - Siege Engine/Vault Key Pickup",
+ "Green Questagon": "Librarian - Hexagon Green",
+ "Blue Questagon": "Rooted Ziggurat Lower - Hexagon Blue",
+}
+
+location_name_to_id: Dict[str, int] = {name: location_base_id + index for index, name in enumerate(location_table)}
+
+location_name_groups: Dict[str, Set[str]] = {}
+for loc_name, loc_data in location_table.items():
+ loc_group_name = loc_name.split(" - ", 1)[0]
+ location_name_groups.setdefault(loc_group_name, set()).add(loc_name)
+ if loc_data.location_group:
+ location_name_groups.setdefault(loc_data.location_group, set()).add(loc_name)
diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py
new file mode 100644
index 000000000000..bf1dd860ae79
--- /dev/null
+++ b/worlds/tunic/options.py
@@ -0,0 +1,238 @@
+from dataclasses import dataclass
+from typing import Dict, Any
+from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections,
+ PerGameCommonOptions, OptionGroup)
+from .er_data import portal_mapping
+
+
+class SwordProgression(DefaultOnToggle):
+ """
+ Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new swords with increased range and attack power.
+ """
+ internal_name = "sword_progression"
+ display_name = "Sword Progression"
+
+
+class StartWithSword(Toggle):
+ """
+ Start with a sword in the player's inventory. Does not count towards Sword Progression.
+ """
+ internal_name = "start_with_sword"
+ display_name = "Start With Sword"
+
+
+class KeysBehindBosses(Toggle):
+ """
+ Places the three hexagon keys behind their respective boss fight in your world.
+ """
+ internal_name = "keys_behind_bosses"
+ display_name = "Keys Behind Bosses"
+
+
+class AbilityShuffling(Toggle):
+ """
+ Locks the usage of Prayer, Holy Cross*, and the Icebolt combo until the relevant pages of the manual have been found.
+ If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required Hexagon goal amount.
+ * Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other player-facing codes.
+ """
+ internal_name = "ability_shuffling"
+ display_name = "Shuffle Abilities"
+
+
+class LogicRules(Choice):
+ """
+ Set which logic rules to use for your world.
+ Restricted: Standard logic, no glitches.
+ No Major Glitches: Sneaky Laurels zips, ice grapples through doors, shooting the west bell, and boss quick kills are included in logic.
+ * Ice grappling through the Ziggurat door is not in logic since you will get stuck in there without Prayer.
+ Unrestricted: Logic in No Major Glitches, as well as ladder storage to get to certain places early.
+ * Torch is given to the player at the start of the game due to the high softlock potential with various tricks. Using the torch is not required in logic.
+ * Using Ladder Storage to get to individual chests is not in logic to avoid tedium.
+ * Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on.
+ """
+ internal_name = "logic_rules"
+ display_name = "Logic Rules"
+ option_restricted = 0
+ option_no_major_glitches = 1
+ alias_nmg = 1
+ option_unrestricted = 2
+ alias_ur = 2
+ default = 0
+
+
+class Lanternless(Toggle):
+ """
+ Choose whether you require the Lantern for dark areas.
+ When enabled, the Lantern is marked as Useful instead of Progression.
+ """
+ internal_name = "lanternless"
+ display_name = "Lanternless"
+
+
+class Maskless(Toggle):
+ """
+ Choose whether you require the Scavenger's Mask for Lower Quarry.
+ When enabled, the Scavenger's Mask is marked as Useful instead of Progression.
+ """
+ internal_name = "maskless"
+ display_name = "Maskless"
+
+
+class FoolTraps(Choice):
+ """
+ Replaces low-to-medium value money rewards in the item pool with fool traps, which cause random negative effects to the player.
+ """
+ internal_name = "fool_traps"
+ display_name = "Fool Traps"
+ option_off = 0
+ option_normal = 1
+ option_double = 2
+ option_onslaught = 3
+ default = 1
+
+
+class HexagonQuest(Toggle):
+ """
+ An alternate goal that shuffles Gold "Questagon" items into the item pool and allows the game to be completed after collecting the required number of them.
+ """
+ internal_name = "hexagon_quest"
+ display_name = "Hexagon Quest"
+
+
+class HexagonGoal(Range):
+ """
+ How many Gold Questagons are required to complete the game on Hexagon Quest.
+ """
+ internal_name = "hexagon_goal"
+ display_name = "Gold Hexagons Required"
+ range_start = 15
+ range_end = 50
+ default = 20
+
+
+class ExtraHexagonPercentage(Range):
+ """
+ How many extra Gold Questagons are shuffled into the item pool, taken as a percentage of the goal amount.
+ """
+ internal_name = "extra_hexagon_percentage"
+ display_name = "Percentage of Extra Gold Hexagons"
+ range_start = 0
+ range_end = 100
+ default = 50
+
+
+class EntranceRando(TextChoice):
+ """
+ Randomize the connections between scenes.
+ A small, very lost fox on a big adventure.
+
+ If you set this option's value to a string, it will be used as a custom seed.
+ Every player who uses the same custom seed will have the same entrances, choosing the most restrictive settings among these players for the purpose of pairing entrances.
+ """
+ internal_name = "entrance_rando"
+ display_name = "Entrance Rando"
+ alias_false = 0
+ option_no = 0
+ alias_true = 1
+ option_yes = 1
+ default = 0
+
+
+class FixedShop(Toggle):
+ """
+ Forces the Windmill entrance to lead to a shop, and removes the remaining shops from the pool.
+ Adds another entrance in Rooted Ziggurat Lower to keep an even number of entrances.
+ Has no effect if Entrance Rando is not enabled.
+ """
+ internal_name = "fixed_shop"
+ display_name = "Fewer Shops in Entrance Rando"
+
+
+class LaurelsLocation(Choice):
+ """
+ Force the Hero's Laurels to be placed at a location in your world.
+ For if you want to avoid or specify early or late Laurels.
+ """
+ internal_name = "laurels_location"
+ display_name = "Laurels Location"
+ option_anywhere = 0
+ option_6_coins = 1
+ option_10_coins = 2
+ option_10_fairies = 3
+ default = 0
+
+
+class ShuffleLadders(Toggle):
+ """
+ Turns several ladders in the game into items that must be found before they can be climbed on.
+ Adds more layers of progression to the game by blocking access to many areas early on.
+ "Ladders were a mistake."
+ —Andrew Shouldice
+ """
+ internal_name = "shuffle_ladders"
+ display_name = "Shuffle Ladders"
+
+
+class TunicPlandoConnections(PlandoConnections):
+ """
+ Generic connection plando. Format is:
+ - entrance: "Entrance Name"
+ exit: "Exit Name"
+ percentage: 100
+ Percentage is an integer from 0 to 100 which determines whether that connection will be made. Defaults to 100 if omitted.
+ """
+ entrances = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"}
+ exits = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"}
+
+ duplicate_exits = True
+
+
+@dataclass
+class TunicOptions(PerGameCommonOptions):
+ start_inventory_from_pool: StartInventoryPool
+ sword_progression: SwordProgression
+ start_with_sword: StartWithSword
+ keys_behind_bosses: KeysBehindBosses
+ ability_shuffling: AbilityShuffling
+ shuffle_ladders: ShuffleLadders
+ entrance_rando: EntranceRando
+ fixed_shop: FixedShop
+ logic_rules: LogicRules
+ fool_traps: FoolTraps
+ hexagon_quest: HexagonQuest
+ hexagon_goal: HexagonGoal
+ extra_hexagon_percentage: ExtraHexagonPercentage
+ lanternless: Lanternless
+ maskless: Maskless
+ laurels_location: LaurelsLocation
+ plando_connections: TunicPlandoConnections
+
+
+tunic_option_groups = [
+ OptionGroup("Logic Options", [
+ LogicRules,
+ Lanternless,
+ Maskless,
+ ])
+]
+
+tunic_option_presets: Dict[str, Dict[str, Any]] = {
+ "Sync": {
+ "ability_shuffling": True,
+ },
+ "Async": {
+ "progression_balancing": 0,
+ "ability_shuffling": True,
+ "shuffle_ladders": True,
+ "laurels_location": "10_fairies",
+ },
+ "Glace Mode": {
+ "accessibility": "minimal",
+ "ability_shuffling": True,
+ "entrance_rando": "yes",
+ "fool_traps": "onslaught",
+ "logic_rules": "unrestricted",
+ "maskless": True,
+ "lanternless": True,
+ },
+}
diff --git a/worlds/tunic/regions.py b/worlds/tunic/regions.py
new file mode 100644
index 000000000000..c30a44bb8ff6
--- /dev/null
+++ b/worlds/tunic/regions.py
@@ -0,0 +1,25 @@
+from typing import Dict, Set
+
+tunic_regions: Dict[str, Set[str]] = {
+ "Menu": {"Overworld"},
+ "Overworld": {"Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden",
+ "Ruined Atoll", "Eastern Vault Fortress", "Beneath the Vault", "Quarry Back", "Quarry", "Swamp",
+ "Spirit Arena"},
+ "Overworld Holy Cross": set(),
+ "East Forest": set(),
+ "Dark Tomb": {"West Garden"},
+ "Beneath the Well": set(),
+ "West Garden": set(),
+ "Ruined Atoll": {"Frog's Domain", "Library"},
+ "Frog's Domain": set(),
+ "Library": set(),
+ "Eastern Vault Fortress": {"Beneath the Vault"},
+ "Beneath the Vault": {"Eastern Vault Fortress"},
+ "Quarry Back": {"Quarry"},
+ "Quarry": {"Lower Quarry"},
+ "Lower Quarry": {"Rooted Ziggurat"},
+ "Rooted Ziggurat": set(),
+ "Swamp": {"Cathedral"},
+ "Cathedral": set(),
+ "Spirit Arena": set()
+}
diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py
new file mode 100644
index 000000000000..73eb8118901b
--- /dev/null
+++ b/worlds/tunic/rules.py
@@ -0,0 +1,339 @@
+from random import Random
+from typing import Dict, TYPE_CHECKING
+
+from worlds.generic.Rules import set_rule, forbid_item
+from BaseClasses import CollectionState
+from .options import TunicOptions
+if TYPE_CHECKING:
+ from . import TunicWorld
+
+laurels = "Hero's Laurels"
+grapple = "Magic Orb"
+ice_dagger = "Magic Dagger"
+fire_wand = "Magic Wand"
+lantern = "Lantern"
+fairies = "Fairy"
+coins = "Golden Coin"
+prayer = "Pages 24-25 (Prayer)"
+holy_cross = "Pages 42-43 (Holy Cross)"
+icebolt = "Pages 52-53 (Icebolt)"
+key = "Key"
+house_key = "Old House Key"
+vault_key = "Fortress Vault Key"
+mask = "Scavenger Mask"
+red_hexagon = "Red Questagon"
+green_hexagon = "Green Questagon"
+blue_hexagon = "Blue Questagon"
+gold_hexagon = "Gold Questagon"
+
+
+def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str, int]:
+ ability_requirement = [1, 1, 1]
+ if options.hexagon_quest.value:
+ hexagon_goal = options.hexagon_goal.value
+ # Set ability unlocks to 25, 50, and 75% of goal amount
+ ability_requirement = [hexagon_goal // 4, hexagon_goal // 2, hexagon_goal * 3 // 4]
+ abilities = [prayer, holy_cross, icebolt]
+ random.shuffle(abilities)
+ return dict(zip(abilities, ability_requirement))
+
+
+def has_ability(ability: str, state: CollectionState, world: "TunicWorld") -> bool:
+ options = world.options
+ ability_unlocks = world.ability_unlocks
+ if not options.ability_shuffling:
+ return True
+ if options.hexagon_quest:
+ return state.has(gold_hexagon, world.player, ability_unlocks[ability])
+ return state.has(ability, world.player)
+
+
+# a check to see if you can whack things in melee at all
+def has_stick(state: CollectionState, player: int) -> bool:
+ return (state.has("Stick", player) or state.has("Sword Upgrade", player, 1)
+ or state.has("Sword", player))
+
+
+def has_sword(state: CollectionState, player: int) -> bool:
+ return state.has("Sword", player) or state.has("Sword Upgrade", player, 2)
+
+
+def has_ice_grapple_logic(long_range: bool, state: CollectionState, world: "TunicWorld") -> bool:
+ player = world.player
+ if not world.options.logic_rules:
+ return False
+ if not long_range:
+ return state.has_all({ice_dagger, grapple}, player)
+ else:
+ return state.has_all({ice_dagger, fire_wand, grapple}, player) and has_ability(icebolt, state, world)
+
+
+def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool:
+ return world.options.logic_rules == "unrestricted" and has_stick(state, world.player)
+
+
+def has_mask(state: CollectionState, world: "TunicWorld") -> bool:
+ if world.options.maskless:
+ return True
+ else:
+ return state.has(mask, world.player)
+
+
+def has_lantern(state: CollectionState, world: "TunicWorld") -> bool:
+ if world.options.lanternless:
+ return True
+ else:
+ return state.has(lantern, world.player)
+
+
+def set_region_rules(world: "TunicWorld") -> None:
+ multiworld = world.multiworld
+ player = world.player
+ options = world.options
+
+ multiworld.get_entrance("Overworld -> Overworld Holy Cross", player).access_rule = \
+ lambda state: has_ability(holy_cross, state, world)
+ multiworld.get_entrance("Overworld -> Beneath the Well", player).access_rule = \
+ lambda state: has_stick(state, player) or state.has(fire_wand, player)
+ multiworld.get_entrance("Overworld -> Dark Tomb", player).access_rule = \
+ lambda state: has_lantern(state, world)
+ multiworld.get_entrance("Overworld -> West Garden", player).access_rule = \
+ lambda state: state.has(laurels, player) \
+ or can_ladder_storage(state, world)
+ multiworld.get_entrance("Overworld -> Eastern Vault Fortress", player).access_rule = \
+ lambda state: state.has(laurels, player) \
+ or has_ice_grapple_logic(True, state, world) \
+ or can_ladder_storage(state, world)
+ # using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules
+ multiworld.get_entrance("Overworld -> Beneath the Vault", player).access_rule = \
+ lambda state: has_lantern(state, world) and has_ability(prayer, state, world)
+ multiworld.get_entrance("Ruined Atoll -> Library", player).access_rule = \
+ lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world)
+ multiworld.get_entrance("Overworld -> Quarry", player).access_rule = \
+ lambda state: (has_sword(state, player) or state.has(fire_wand, player)) \
+ and (state.has_any({grapple, laurels}, player) or can_ladder_storage(state, world))
+ multiworld.get_entrance("Quarry Back -> Quarry", player).access_rule = \
+ lambda state: has_sword(state, player) or state.has(fire_wand, player)
+ multiworld.get_entrance("Quarry -> Lower Quarry", player).access_rule = \
+ lambda state: has_mask(state, world)
+ multiworld.get_entrance("Lower Quarry -> Rooted Ziggurat", player).access_rule = \
+ lambda state: state.has(grapple, player) and has_ability(prayer, state, world)
+ multiworld.get_entrance("Swamp -> Cathedral", player).access_rule = \
+ lambda state: state.has(laurels, player) and has_ability(prayer, state, world) \
+ or has_ice_grapple_logic(False, state, world)
+ multiworld.get_entrance("Overworld -> Spirit Arena", player).access_rule = \
+ lambda state: ((state.has(gold_hexagon, player, options.hexagon_goal.value) if options.hexagon_quest.value
+ else state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player)
+ and state.has_group_unique("Hero Relics", player, 6))
+ and has_ability(prayer, state, world) and has_sword(state, player)
+ and state.has_any({lantern, laurels}, player))
+
+
+def set_location_rules(world: "TunicWorld") -> None:
+ multiworld = world.multiworld
+ player = world.player
+ options = world.options
+
+ forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), fairies, player)
+
+ # Ability Shuffle Exclusive Rules
+ set_rule(multiworld.get_location("Far Shore - Page Pickup", player),
+ lambda state: has_ability(prayer, state, world))
+ set_rule(multiworld.get_location("Fortress Courtyard - Chest Near Cave", player),
+ lambda state: has_ability(prayer, state, world)
+ or state.has(laurels, player)
+ or can_ladder_storage(state, world)
+ or (has_ice_grapple_logic(True, state, world) and has_lantern(state, world)))
+ set_rule(multiworld.get_location("Fortress Courtyard - Page Near Cave", player),
+ lambda state: has_ability(prayer, state, world) or state.has(laurels, player)
+ or can_ladder_storage(state, world)
+ or (has_ice_grapple_logic(True, state, world) and has_lantern(state, world)))
+ set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player),
+ lambda state: has_ability(prayer, state, world))
+ set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player),
+ lambda state: has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player),
+ lambda state: has_ability(holy_cross, state, world))
+
+ # Overworld
+ set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player),
+ lambda state: state.has_any({grapple, laurels}, player))
+ set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player),
+ lambda state: state.has_any({grapple, laurels}, player))
+ set_rule(multiworld.get_location("Far Shore - Secret Chest", player),
+ lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
+ set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Old House - Normal Chest", player),
+ lambda state: state.has(house_key, player)
+ or has_ice_grapple_logic(False, state, world)
+ or (state.has(laurels, player) and options.logic_rules))
+ set_rule(multiworld.get_location("Old House - Holy Cross Chest", player),
+ lambda state: has_ability(holy_cross, state, world) and (
+ state.has(house_key, player)
+ or has_ice_grapple_logic(False, state, world)
+ or (state.has(laurels, player) and options.logic_rules)))
+ set_rule(multiworld.get_location("Old House - Shield Pickup", player),
+ lambda state: state.has(house_key, player)
+ or has_ice_grapple_logic(False, state, world)
+ or (state.has(laurels, player) and options.logic_rules))
+ set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Overworld - [West] Chest After Bell", player),
+ lambda state: state.has(laurels, player)
+ or (has_lantern(state, world) and has_sword(state, player))
+ or can_ladder_storage(state, world))
+ set_rule(multiworld.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate", player),
+ lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules)
+ set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player),
+ lambda state: state.has(grapple, player))
+ set_rule(multiworld.get_location("Special Shop - Secret Page Pickup", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player),
+ lambda state: has_ability(holy_cross, state, world)
+ and (state.has(laurels, player) or (has_lantern(state, world) and (has_sword(state, player)
+ or state.has(fire_wand, player)))
+ or has_ice_grapple_logic(False, state, world)))
+ set_rule(multiworld.get_location("Sealed Temple - Page Pickup", player),
+ lambda state: state.has(laurels, player)
+ or (has_lantern(state, world) and (has_sword(state, player) or state.has(fire_wand, player)))
+ or has_ice_grapple_logic(False, state, world))
+ set_rule(multiworld.get_location("West Furnace - Lantern Pickup", player),
+ lambda state: has_stick(state, player) or state.has_any({fire_wand, laurels}, player))
+
+ set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player),
+ lambda state: state.has(fairies, player, 10))
+ set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player),
+ lambda state: state.has(fairies, player, 20))
+ set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player),
+ lambda state: state.has(coins, player, 3))
+ set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player),
+ lambda state: state.has(coins, player, 6))
+ set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player),
+ lambda state: state.has(coins, player, 10))
+ set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player),
+ lambda state: state.has(coins, player, 15))
+
+ # East Forest
+ set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player),
+ lambda state: state.has(grapple, player))
+ set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player),
+ lambda state: state.has_all({grapple, laurels}, player))
+ set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player),
+ lambda state: state.has_all({grapple, ice_dagger, fire_wand}, player)
+ and has_ability(icebolt, state, world))
+
+ # West Garden
+ set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player),
+ lambda state: state.has(laurels, player) and has_ability(holy_cross, state, world))
+ set_rule(multiworld.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House", player),
+ lambda state: (state.has(laurels, player) and has_ability(prayer, state, world))
+ or has_ice_grapple_logic(True, state, world))
+ set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("West Garden - [Central Highlands] After Garden Knight", player),
+ lambda state: state.has(laurels, player)
+ or (has_lantern(state, world) and has_sword(state, player))
+ or can_ladder_storage(state, world))
+
+ # Ruined Atoll
+ set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player),
+ lambda state: state.has(laurels, player) or state.has(key, player, 2))
+ set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player),
+ lambda state: state.has(laurels, player) or state.has(key, player, 2))
+ set_rule(multiworld.get_location("Librarian - Hexagon Green", player),
+ lambda state: has_sword(state, player) or options.logic_rules)
+
+ # Frog's Domain
+ set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player),
+ lambda state: state.has_any({grapple, laurels}, player))
+ set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player),
+ lambda state: state.has_any({grapple, laurels}, player))
+ set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player),
+ lambda state: state.has_any({grapple, laurels}, player))
+
+ # Eastern Vault Fortress
+ set_rule(multiworld.get_location("Fortress Leaf Piles - Secret Chest", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player),
+ lambda state: has_sword(state, player)
+ and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world)))
+ set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player),
+ lambda state: state.has(vault_key, player)
+ and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world)))
+
+ # Beneath the Vault
+ set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player),
+ lambda state: has_stick(state, player) or state.has_any({laurels, fire_wand}, player))
+ set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player),
+ lambda state: has_stick(state, player) and has_lantern(state, world))
+
+ # Quarry
+ set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Rooted Ziggurat Upper - Near Bridge Switch", player),
+ lambda state: has_sword(state, player) or state.has_all({fire_wand, laurels}, player))
+ # nmg - kill boss scav with orb + firecracker, or similar
+ set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player),
+ lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules))
+
+ # Swamp
+ set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player),
+ lambda state: (state.has(fire_wand, player) and has_sword(state, player))
+ and (state.has(laurels, player) or has_ice_grapple_logic(False, state, world)))
+ set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Swamp - [Outside Cathedral] Obscured Behind Memorial", player),
+ lambda state: state.has(laurels, player))
+ set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player),
+ lambda state: has_sword(state, player))
+
+ # Hero's Grave
+ set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player),
+ lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
+ set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player),
+ lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
+ set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player),
+ lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
+ set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player),
+ lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
+ set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player),
+ lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
+ set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player),
+ lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
+
+ # Shop
+ set_rule(multiworld.get_location("Shop - Potion 1", player),
+ lambda state: has_sword(state, player))
+ set_rule(multiworld.get_location("Shop - Potion 2", player),
+ lambda state: has_sword(state, player))
+ set_rule(multiworld.get_location("Shop - Coin 1", player),
+ lambda state: has_sword(state, player))
+ set_rule(multiworld.get_location("Shop - Coin 2", player),
+ lambda state: has_sword(state, player))
diff --git a/worlds/tunic/test/__init__.py b/worlds/tunic/test/__init__.py
new file mode 100644
index 000000000000..d0b68955c538
--- /dev/null
+++ b/worlds/tunic/test/__init__.py
@@ -0,0 +1,6 @@
+from test.bases import WorldTestBase
+
+
+class TunicTestBase(WorldTestBase):
+ game = "TUNIC"
+ player = 1
diff --git a/worlds/tunic/test/test_access.py b/worlds/tunic/test/test_access.py
new file mode 100644
index 000000000000..72d4a498d1ee
--- /dev/null
+++ b/worlds/tunic/test/test_access.py
@@ -0,0 +1,70 @@
+from . import TunicTestBase
+from .. import options
+
+
+class TestAccess(TunicTestBase):
+ # test whether you can get into the temple without laurels
+ def test_temple_access(self) -> None:
+ self.collect_all_but(["Hero's Laurels", "Lantern"])
+ self.assertFalse(self.can_reach_location("Sealed Temple - Page Pickup"))
+ self.collect_by_name(["Lantern"])
+ self.assertTrue(self.can_reach_location("Sealed Temple - Page Pickup"))
+
+ # test that the wells function properly. Since fairies is written the same way, that should succeed too
+ def test_wells(self) -> None:
+ self.collect_all_but(["Golden Coin"])
+ self.assertFalse(self.can_reach_location("Coins in the Well - 3 Coins"))
+ self.collect_by_name(["Golden Coin"])
+ self.assertTrue(self.can_reach_location("Coins in the Well - 3 Coins"))
+
+
+class TestStandardShuffle(TunicTestBase):
+ options = {options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true}
+
+ # test that you need to get holy cross to open the hc door in overworld
+ def test_hc_door(self) -> None:
+ self.assertFalse(self.can_reach_location("Fountain Cross Door - Page Pickup"))
+ self.collect_by_name("Pages 42-43 (Holy Cross)")
+ self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup"))
+
+
+class TestHexQuestShuffle(TunicTestBase):
+ options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_true,
+ options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true}
+
+ # test that you need the gold questagons to open the hc door in overworld
+ def test_hc_door_hex_shuffle(self) -> None:
+ self.assertFalse(self.can_reach_location("Fountain Cross Door - Page Pickup"))
+ self.collect_by_name("Gold Questagon")
+ self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup"))
+
+
+class TestHexQuestNoShuffle(TunicTestBase):
+ options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_true,
+ options.AbilityShuffling.internal_name: options.AbilityShuffling.option_false}
+
+ # test that you can get the item behind the overworld hc door with nothing and no ability shuffle
+ def test_hc_door_no_shuffle(self) -> None:
+ self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup"))
+
+
+class TestNormalGoal(TunicTestBase):
+ options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_false}
+
+ # test that you need the three colored hexes to reach the Heir in standard
+ def test_normal_goal(self) -> None:
+ location = ["The Heir"]
+ items = [["Red Questagon", "Blue Questagon", "Green Questagon"]]
+ self.assertAccessDependency(location, items)
+
+
+class TestER(TunicTestBase):
+ options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes,
+ options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true,
+ options.HexagonQuest.internal_name: options.HexagonQuest.option_false}
+
+ def test_overworld_hc_chest(self) -> None:
+ # test to see that static connections are working properly -- this chest requires holy cross and is in Overworld
+ self.assertFalse(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross"))
+ self.collect_by_name(["Pages 42-43 (Holy Cross)"])
+ self.assertTrue(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross"))
diff --git a/worlds/undertale/Items.py b/worlds/undertale/Items.py
index 033102664c82..9f2ce1afec9e 100644
--- a/worlds/undertale/Items.py
+++ b/worlds/undertale/Items.py
@@ -105,7 +105,6 @@ class UndertaleItem(Item):
non_key_items = {
"Butterscotch Pie": 1,
"500G": 2,
- "1000G": 2,
"Face Steak": 1,
"Snowman Piece": 1,
"Instant Noodles": 1,
@@ -147,6 +146,7 @@ class UndertaleItem(Item):
key_items = {
"Complete Skeleton": 1,
"Fish": 1,
+ "1000G": 2,
"DT Extractor": 1,
"Mettaton Plush": 1,
"Punch Card": 3,
diff --git a/worlds/undertale/Locations.py b/worlds/undertale/Locations.py
index 2f7de44512fa..5b45af63a9a2 100644
--- a/worlds/undertale/Locations.py
+++ b/worlds/undertale/Locations.py
@@ -10,10 +10,6 @@ class AdvData(typing.NamedTuple):
class UndertaleAdvancement(Location):
game: str = "Undertale"
- def __init__(self, player: int, name: str, address: typing.Optional[int], parent):
- super().__init__(player, name, address, parent)
- self.event = not address
-
advancement_table = {
"Snowman": AdvData(79100, "Snowdin Forest"),
diff --git a/worlds/undertale/Options.py b/worlds/undertale/Options.py
index 146a7838f766..b2de41a3d577 100644
--- a/worlds/undertale/Options.py
+++ b/worlds/undertale/Options.py
@@ -1,5 +1,5 @@
-import typing
-from Options import Choice, Option, Toggle, Range
+from Options import Choice, Toggle, Range, PerGameCommonOptions
+from dataclasses import dataclass
class RouteRequired(Choice):
@@ -86,17 +86,17 @@ class RandoBattleOptions(Toggle):
default = 0
-undertale_options: typing.Dict[str, type(Option)] = {
- "route_required": RouteRequired,
- "starting_area": StartingArea,
- "key_hunt": KeyHunt,
- "key_pieces": KeyPieces,
- "rando_love": RandomizeLove,
- "rando_stats": RandomizeStats,
- "temy_include": IncludeTemy,
- "no_equips": NoEquips,
- "only_flakes": OnlyFlakes,
- "prog_armor": ProgressiveArmor,
- "prog_weapons": ProgressiveWeapons,
- "rando_item_button": RandoBattleOptions,
-}
+@dataclass
+class UndertaleOptions(PerGameCommonOptions):
+ route_required: RouteRequired
+ starting_area: StartingArea
+ key_hunt: KeyHunt
+ key_pieces: KeyPieces
+ rando_love: RandomizeLove
+ rando_stats: RandomizeStats
+ temy_include: IncludeTemy
+ no_equips: NoEquips
+ only_flakes: OnlyFlakes
+ prog_armor: ProgressiveArmor
+ prog_weapons: ProgressiveWeapons
+ rando_item_button: RandoBattleOptions
diff --git a/worlds/undertale/Regions.py b/worlds/undertale/Regions.py
index ec13b249fa0e..138a6846537a 100644
--- a/worlds/undertale/Regions.py
+++ b/worlds/undertale/Regions.py
@@ -24,6 +24,7 @@ def link_undertale_areas(world: MultiWorld, player: int):
("True Lab", []),
("Core", ["Core Exit"]),
("New Home", ["New Home Exit"]),
+ ("Last Corridor", ["Last Corridor Exit"]),
("Barrier", []),
]
@@ -40,7 +41,8 @@ def link_undertale_areas(world: MultiWorld, player: int):
("News Show Entrance", "News Show"),
("Lab Elevator", "True Lab"),
("Core Exit", "New Home"),
- ("New Home Exit", "Barrier"),
+ ("New Home Exit", "Last Corridor"),
+ ("Last Corridor Exit", "Barrier"),
("Snowdin Hub", "Snowdin Forest"),
("Waterfall Hub", "Waterfall"),
("Hotland Hub", "Hotland"),
diff --git a/worlds/undertale/Rules.py b/worlds/undertale/Rules.py
index 02c21f53f73c..2de61d8eb00c 100644
--- a/worlds/undertale/Rules.py
+++ b/worlds/undertale/Rules.py
@@ -1,18 +1,22 @@
-from worlds.generic.Rules import set_rule, add_rule, add_item_rule
-from BaseClasses import MultiWorld, CollectionState
+from worlds.generic.Rules import set_rule, add_rule
+from BaseClasses import CollectionState
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+ from . import UndertaleWorld
-def _undertale_is_route(state: CollectionState, player: int, route: int):
+
+def _undertale_is_route(world: "UndertaleWorld", route: int):
if route == 3:
- return state.multiworld.route_required[player].current_key == "all_routes"
- if state.multiworld.route_required[player].current_key == "all_routes":
+ return world.options.route_required.current_key == "all_routes"
+ if world.options.route_required.current_key == "all_routes":
return True
if route == 0:
- return state.multiworld.route_required[player].current_key == "neutral"
+ return world.options.route_required.current_key == "neutral"
if route == 1:
- return state.multiworld.route_required[player].current_key == "pacifist"
+ return world.options.route_required.current_key == "pacifist"
if route == 2:
- return state.multiworld.route_required[player].current_key == "genocide"
+ return world.options.route_required.current_key == "genocide"
return False
@@ -27,7 +31,7 @@ def _undertale_has_plot(state: CollectionState, player: int, item: str):
return state.has("DT Extractor", player)
-def _undertale_can_level(state: CollectionState, exp: int, lvl: int):
+def _undertale_can_level(exp: int, lvl: int):
if exp >= 10 and lvl == 1:
return True
elif exp >= 30 and lvl == 2:
@@ -70,7 +74,9 @@ def _undertale_can_level(state: CollectionState, exp: int, lvl: int):
# Sets rules on entrances and advancements that are always applied
-def set_rules(multiworld: MultiWorld, player: int):
+def set_rules(world: "UndertaleWorld"):
+ player = world.player
+ multiworld = world.multiworld
set_rule(multiworld.get_entrance("Ruins Hub", player), lambda state: state.has("Ruins Key", player))
set_rule(multiworld.get_entrance("Snowdin Hub", player), lambda state: state.has("Snowdin Key", player))
set_rule(multiworld.get_entrance("Waterfall Hub", player), lambda state: state.has("Waterfall Key", player))
@@ -81,23 +87,30 @@ def set_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_entrance("New Home Exit", player),
lambda state: (state.has("Left Home Key", player) and
state.has("Right Home Key", player)) or
- state.has("Key Piece", player, state.multiworld.key_pieces[player]))
- if _undertale_is_route(multiworld.state, player, 1):
+ state.has("Key Piece", player, world.options.key_pieces.value))
+ if _undertale_is_route(world, 1):
set_rule(multiworld.get_entrance("Papyrus\" Home Entrance", player),
lambda state: _undertale_has_plot(state, player, "Complete Skeleton"))
set_rule(multiworld.get_entrance("Undyne\"s Home Entrance", player),
lambda state: _undertale_has_plot(state, player, "Fish") and state.has("Papyrus Date", player))
set_rule(multiworld.get_entrance("Lab Elevator", player),
- lambda state: state.has("Alphys Date", player) and _undertale_has_plot(state, player, "DT Extractor"))
+ lambda state: state.has("Alphys Date", player) and state.has("DT Extractor", player) and
+ ((state.has("Left Home Key", player) and state.has("Right Home Key", player)) or
+ state.has("Key Piece", player, world.options.key_pieces.value)))
set_rule(multiworld.get_location("Alphys Date", player),
- lambda state: state.has("Undyne Letter EX", player) and state.has("Undyne Date", player))
+ lambda state: state.can_reach("New Home", "Region", player) and state.has("Undyne Letter EX", player)
+ and state.has("Undyne Date", player))
set_rule(multiworld.get_location("Papyrus Plot", player),
lambda state: state.can_reach("Snowdin Town", "Region", player))
set_rule(multiworld.get_location("Undyne Plot", player),
lambda state: state.can_reach("Waterfall", "Region", player))
set_rule(multiworld.get_location("True Lab Plot", player),
lambda state: state.can_reach("New Home", "Region", player)
- and state.can_reach("Letter Quest", "Location", player))
+ and state.can_reach("Letter Quest", "Location", player)
+ and state.can_reach("Alphys Date", "Location", player)
+ and ((state.has("Left Home Key", player) and
+ state.has("Right Home Key", player)) or
+ state.has("Key Piece", player, world.options.key_pieces.value)))
set_rule(multiworld.get_location("Chisps Machine", player),
lambda state: state.can_reach("True Lab", "Region", player))
set_rule(multiworld.get_location("Dog Sale 1", player),
@@ -113,8 +126,8 @@ def set_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_location("Hush Trade", player),
lambda state: state.can_reach("News Show", "Region", player) and state.has("Hot Dog...?", player, 1))
set_rule(multiworld.get_location("Letter Quest", player),
- lambda state: state.can_reach("New Home Exit", "Entrance", player))
- if (not _undertale_is_route(multiworld.state, player, 2)) or _undertale_is_route(multiworld.state, player, 3):
+ lambda state: state.can_reach("Last Corridor", "Region", player) and state.has("Undyne Date", player))
+ if (not _undertale_is_route(world, 2)) or _undertale_is_route(world, 3):
set_rule(multiworld.get_location("Nicecream Punch Card", player),
lambda state: state.has("Punch Card", player, 3) and state.can_reach("Waterfall", "Region", player))
set_rule(multiworld.get_location("Nicecream Snowdin", player),
@@ -125,26 +138,26 @@ def set_rules(multiworld: MultiWorld, player: int):
lambda state: state.can_reach("Waterfall", "Region", player))
set_rule(multiworld.get_location("Apron Hidden", player),
lambda state: state.can_reach("Cooking Show", "Region", player))
- if _undertale_is_route(multiworld.state, player, 2) and \
- (multiworld.rando_love[player] or multiworld.rando_stats[player]):
+ if _undertale_is_route(world, 2) and \
+ (bool(world.options.rando_love.value) or bool(world.options.rando_stats.value)):
maxlv = 1
exp = 190
curarea = "Old Home"
while maxlv < 20:
maxlv += 1
- if multiworld.rando_love[player]:
+ if world.options.rando_love:
set_rule(multiworld.get_location(("LOVE " + str(maxlv)), player), lambda state: False)
- if multiworld.rando_stats[player]:
+ if world.options.rando_stats:
set_rule(multiworld.get_location(("ATK "+str(maxlv)), player), lambda state: False)
set_rule(multiworld.get_location(("HP "+str(maxlv)), player), lambda state: False)
if maxlv in {5, 9, 13, 17}:
set_rule(multiworld.get_location(("DEF "+str(maxlv)), player), lambda state: False)
maxlv = 1
while maxlv < 20:
- while _undertale_can_level(multiworld.state, exp, maxlv):
+ while _undertale_can_level(exp, maxlv):
maxlv += 1
- if multiworld.rando_stats[player]:
+ if world.options.rando_stats:
if curarea == "Old Home":
add_rule(multiworld.get_location(("ATK "+str(maxlv)), player),
lambda state: (state.can_reach("Old Home", "Region", player)), combine="or")
@@ -193,7 +206,7 @@ def set_rules(multiworld: MultiWorld, player: int):
if maxlv == 5 or maxlv == 9 or maxlv == 13 or maxlv == 17:
add_rule(multiworld.get_location(("DEF "+str(maxlv)), player),
lambda state: (state.can_reach("New Home Exit", "Entrance", player)), combine="or")
- if multiworld.rando_love[player]:
+ if world.options.rando_love:
if curarea == "Old Home":
add_rule(multiworld.get_location(("LOVE "+str(maxlv)), player),
lambda state: (state.can_reach("Old Home", "Region", player)), combine="or")
@@ -303,9 +316,9 @@ def set_rules(multiworld: MultiWorld, player: int):
# Sets rules on completion condition
-def set_completion_rules(multiworld: MultiWorld, player: int):
- completion_requirements = lambda state: state.can_reach("New Home Exit", "Entrance", player)
- if _undertale_is_route(multiworld.state, player, 1):
- completion_requirements = lambda state: state.can_reach("True Lab", "Region", player)
-
- multiworld.completion_condition[player] = lambda state: completion_requirements(state)
+def set_completion_rules(world: "UndertaleWorld"):
+ player = world.player
+ multiworld = world.multiworld
+ multiworld.completion_condition[player] = lambda state: state.can_reach("Barrier", "Region", player)
+ if _undertale_is_route(world, 1):
+ multiworld.completion_condition[player] = lambda state: state.can_reach("True Lab", "Region", player)
diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py
index 3a34a162c478..9084c77b0065 100644
--- a/worlds/undertale/__init__.py
+++ b/worlds/undertale/__init__.py
@@ -5,9 +5,9 @@
from .Rules import set_rules, set_completion_rules
from worlds.generic.Rules import exclusion_rules
from BaseClasses import Region, Entrance, Tutorial, Item
-from .Options import undertale_options
+from .Options import UndertaleOptions
from worlds.AutoWorld import World, WebWorld
-from worlds.LauncherComponents import Component, components, Type
+from worlds.LauncherComponents import Component, components
from multiprocessing import Process
@@ -29,12 +29,12 @@ def data_path(file_name: str):
class UndertaleWeb(WebWorld):
tutorials = [Tutorial(
- "Multiworld Setup Tutorial",
+ "Multiworld Setup Guide",
"A guide to setting up the Archipelago Undertale software on your computer. This guide covers "
"single-player, multiworld, and related software.",
"English",
- "undertale_en.md",
- "undertale/en",
+ "setup_en.md",
+ "setup/en",
["Mewlif"]
)]
@@ -46,49 +46,64 @@ class UndertaleWorld(World):
from their underground prison.
"""
game = "Undertale"
- option_definitions = undertale_options
+ options_dataclass = UndertaleOptions
+ options: UndertaleOptions
web = UndertaleWeb()
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {name: data.id for name, data in advancement_table.items()}
- data_version = 7
-
def _get_undertale_data(self):
return {
- "world_seed": self.multiworld.per_slot_randoms[self.player].getrandbits(32),
+ "world_seed": self.random.getrandbits(32),
"seed_name": self.multiworld.seed_name,
"player_name": self.multiworld.get_player_name(self.player),
"player_id": self.player,
"client_version": self.required_client_version,
"race": self.multiworld.is_race,
- "route": self.multiworld.route_required[self.player].current_key,
- "starting_area": self.multiworld.starting_area[self.player].current_key,
- "temy_armor_include": bool(self.multiworld.temy_include[self.player].value),
- "only_flakes": bool(self.multiworld.only_flakes[self.player].value),
- "no_equips": bool(self.multiworld.no_equips[self.player].value),
- "key_hunt": bool(self.multiworld.key_hunt[self.player].value),
- "key_pieces": self.multiworld.key_pieces[self.player].value,
- "rando_love": bool(self.multiworld.rando_love[self.player].value),
- "rando_stats": bool(self.multiworld.rando_stats[self.player].value),
- "prog_armor": bool(self.multiworld.prog_armor[self.player].value),
- "prog_weapons": bool(self.multiworld.prog_weapons[self.player].value),
- "rando_item_button": bool(self.multiworld.rando_item_button[self.player].value)
+ "route": self.options.route_required.current_key,
+ "starting_area": self.options.starting_area.current_key,
+ "temy_armor_include": bool(self.options.temy_include.value),
+ "only_flakes": bool(self.options.only_flakes.value),
+ "no_equips": bool(self.options.no_equips.value),
+ "key_hunt": bool(self.options.key_hunt.value),
+ "key_pieces": self.options.key_pieces.value,
+ "rando_love": bool(self.options.rando_love.value),
+ "rando_stats": bool(self.options.rando_stats.value),
+ "prog_armor": bool(self.options.prog_armor.value),
+ "prog_weapons": bool(self.options.prog_weapons.value),
+ "rando_item_button": bool(self.options.rando_item_button.value)
}
+ def get_filler_item_name(self):
+ if self.options.route_required == "all_routes":
+ junk_pool = junk_weights_all
+ elif self.options.route_required == "genocide":
+ junk_pool = junk_weights_genocide
+ elif self.options.route_required == "neutral":
+ junk_pool = junk_weights_neutral
+ elif self.options.route_required == "pacifist":
+ junk_pool = junk_weights_pacifist
+ else:
+ junk_pool = junk_weights_all
+ if not self.options.only_flakes:
+ return self.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()))[0]
+ else:
+ return "Temmie Flakes"
+
def create_items(self):
self.multiworld.get_location("Undyne Date", self.player).place_locked_item(self.create_item("Undyne Date"))
self.multiworld.get_location("Alphys Date", self.player).place_locked_item(self.create_item("Alphys Date"))
self.multiworld.get_location("Papyrus Date", self.player).place_locked_item(self.create_item("Papyrus Date"))
# Generate item pool
itempool = []
- if self.multiworld.route_required[self.player] == "all_routes":
+ if self.options.route_required == "all_routes":
junk_pool = junk_weights_all.copy()
- elif self.multiworld.route_required[self.player] == "genocide":
+ elif self.options.route_required == "genocide":
junk_pool = junk_weights_genocide.copy()
- elif self.multiworld.route_required[self.player] == "neutral":
+ elif self.options.route_required == "neutral":
junk_pool = junk_weights_neutral.copy()
- elif self.multiworld.route_required[self.player] == "pacifist":
+ elif self.options.route_required == "pacifist":
junk_pool = junk_weights_pacifist.copy()
else:
junk_pool = junk_weights_all.copy()
@@ -101,73 +116,68 @@ def create_items(self):
itempool += [name] * num
for name, num in non_key_items.items():
itempool += [name] * num
- if self.multiworld.rando_item_button[self.player]:
+ if self.options.rando_item_button:
itempool += ["ITEM"]
else:
self.multiworld.push_precollected(self.create_item("ITEM"))
self.multiworld.push_precollected(self.create_item("FIGHT"))
self.multiworld.push_precollected(self.create_item("ACT"))
self.multiworld.push_precollected(self.create_item("MERCY"))
- if self.multiworld.route_required[self.player] == "genocide":
+ if self.options.route_required == "genocide":
itempool = [item for item in itempool if item != "Popato Chisps" and item != "Stained Apron" and
item != "Nice Cream" and item != "Hot Cat" and item != "Hot Dog...?" and item != "Punch Card"]
- elif self.multiworld.route_required[self.player] == "neutral":
+ elif self.options.route_required == "neutral":
itempool = [item for item in itempool if item != "Popato Chisps" and item != "Hot Cat" and
item != "Hot Dog...?"]
- if self.multiworld.route_required[self.player] == "pacifist" or \
- self.multiworld.route_required[self.player] == "all_routes":
+ if self.options.route_required == "pacifist" or self.options.route_required == "all_routes":
itempool += ["Undyne Letter EX"]
else:
itempool.remove("Complete Skeleton")
itempool.remove("Fish")
itempool.remove("DT Extractor")
itempool.remove("Hush Puppy")
- if self.multiworld.key_hunt[self.player]:
- itempool += ["Key Piece"] * self.multiworld.key_pieces[self.player].value
+ if self.options.key_hunt:
+ itempool += ["Key Piece"] * self.options.key_pieces.value
else:
itempool += ["Left Home Key"]
itempool += ["Right Home Key"]
- if not self.multiworld.rando_love[self.player] or \
- (self.multiworld.route_required[self.player] != "genocide" and
- self.multiworld.route_required[self.player] != "all_routes"):
+ if not self.options.rando_love or \
+ (self.options.route_required != "genocide" and self.options.route_required != "all_routes"):
itempool = [item for item in itempool if not item == "LOVE"]
- if not self.multiworld.rando_stats[self.player] or \
- (self.multiworld.route_required[self.player] != "genocide" and
- self.multiworld.route_required[self.player] != "all_routes"):
+ if not self.options.rando_stats or \
+ (self.options.route_required != "genocide" and self.options.route_required != "all_routes"):
itempool = [item for item in itempool if not (item == "ATK Up" or item == "DEF Up" or item == "HP Up")]
- if self.multiworld.temy_include[self.player]:
+ if self.options.temy_include:
itempool += ["temy armor"]
- if self.multiworld.no_equips[self.player]:
+ if self.options.no_equips:
itempool = [item for item in itempool if item not in required_armor and item not in required_weapons]
else:
- if self.multiworld.prog_armor[self.player]:
+ if self.options.prog_armor:
itempool = [item if (item not in required_armor and not item == "temy armor") else
"Progressive Armor" for item in itempool]
- if self.multiworld.prog_weapons[self.player]:
+ if self.options.prog_weapons:
itempool = [item if item not in required_weapons else "Progressive Weapons" for item in itempool]
- if self.multiworld.route_required[self.player] == "genocide" or \
- self.multiworld.route_required[self.player] == "all_routes":
- if not self.multiworld.only_flakes[self.player]:
+ if self.options.route_required == "genocide" or \
+ self.options.route_required == "all_routes":
+ if not self.options.only_flakes:
itempool += ["Snowman Piece"] * 2
- if not self.multiworld.no_equips[self.player]:
+ if not self.options.no_equips:
itempool = ["Real Knife" if item == "Worn Dagger" else "The Locket"
if item == "Heart Locket" else item for item in itempool]
- if self.multiworld.only_flakes[self.player]:
+ if self.options.only_flakes:
itempool = [item for item in itempool if item not in non_key_items]
- starting_key = self.multiworld.starting_area[self.player].current_key.title() + " Key"
+ starting_key = self.options.starting_area.current_key.title() + " Key"
itempool.remove(starting_key)
self.multiworld.push_precollected(self.create_item(starting_key))
# Choose locations to automatically exclude based on settings
exclusion_pool = set()
- exclusion_pool.update(exclusion_table[self.multiworld.route_required[self.player].current_key])
- if not self.multiworld.rando_love[self.player] or \
- (self.multiworld.route_required[self.player] != "genocide" and
- self.multiworld.route_required[self.player] != "all_routes"):
+ exclusion_pool.update(exclusion_table[self.options.route_required.current_key])
+ if not self.options.rando_love or \
+ (self.options.route_required != "genocide" and self.options.route_required != "all_routes"):
exclusion_pool.update(exclusion_table["NoLove"])
- if not self.multiworld.rando_stats[self.player] or \
- (self.multiworld.route_required[self.player] != "genocide" and
- self.multiworld.route_required[self.player] != "all_routes"):
+ if not self.options.rando_stats or \
+ (self.options.route_required != "genocide" and self.options.route_required != "all_routes"):
exclusion_pool.update(exclusion_table["NoStats"])
# Choose locations to automatically exclude based on settings
@@ -175,36 +185,33 @@ def create_items(self):
exclusion_checks.update(["Nicecream Punch Card", "Hush Trade"])
exclusion_rules(self.multiworld, self.player, exclusion_checks)
- # Fill remaining items with randomly generated junk or Temmie Flakes
- if not self.multiworld.only_flakes[self.player]:
- itempool += self.multiworld.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()),
- k=len(self.location_names)-len(itempool)-len(exclusion_pool))
- else:
- itempool += ["Temmie Flakes"] * (len(self.location_names) - len(itempool) - len(exclusion_pool))
# Convert itempool into real items
itempool = [item for item in map(lambda name: self.create_item(name), itempool)]
+ # Fill remaining items with randomly generated junk or Temmie Flakes
+ while len(itempool) < len(self.multiworld.get_unfilled_locations(self.player)):
+ itempool.append(self.create_filler())
self.multiworld.itempool += itempool
def set_rules(self):
- set_rules(self.multiworld, self.player)
- set_completion_rules(self.multiworld, self.player)
+ set_rules(self)
+ set_completion_rules(self)
def create_regions(self):
def UndertaleRegion(region_name: str, exits=[]):
ret = Region(region_name, self.player, self.multiworld)
- ret.locations = [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret)
- for loc_name, loc_data in advancement_table.items()
- if loc_data.region == region_name and
- (loc_name not in exclusion_table["NoStats"] or
- (self.multiworld.rando_stats[self.player] and
- (self.multiworld.route_required[self.player] == "genocide" or
- self.multiworld.route_required[self.player] == "all_routes"))) and
- (loc_name not in exclusion_table["NoLove"] or
- (self.multiworld.rando_love[self.player] and
- (self.multiworld.route_required[self.player] == "genocide" or
- self.multiworld.route_required[self.player] == "all_routes"))) and
- loc_name not in exclusion_table[self.multiworld.route_required[self.player].current_key]]
+ ret.locations += [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret)
+ for loc_name, loc_data in advancement_table.items()
+ if loc_data.region == region_name and
+ (loc_name not in exclusion_table["NoStats"] or
+ (self.options.rando_stats and
+ (self.options.route_required == "genocide" or
+ self.options.route_required == "all_routes"))) and
+ (loc_name not in exclusion_table["NoLove"] or
+ (self.options.rando_love and
+ (self.options.route_required == "genocide" or
+ self.options.route_required == "all_routes"))) and
+ loc_name not in exclusion_table[self.options.route_required.current_key]]
for exit in exits:
ret.exits.append(Entrance(self.player, exit, ret))
return ret
@@ -214,11 +221,11 @@ def UndertaleRegion(region_name: str, exits=[]):
def fill_slot_data(self):
slot_data = self._get_undertale_data()
- for option_name in undertale_options:
+ for option_name in self.options.as_dict():
option = getattr(self.multiworld, option_name)[self.player]
if (option_name == "rando_love" or option_name == "rando_stats") and \
- self.multiworld.route_required[self.player] != "genocide" and \
- self.multiworld.route_required[self.player] != "all_routes":
+ self.options.route_required != "genocide" and \
+ self.options.route_required != "all_routes":
option.value = False
if slot_data.get(option_name, None) is None and type(option.value) in {str, int}:
slot_data[option_name] = int(option.value)
diff --git a/worlds/undertale/data/patch.bsdiff b/worlds/undertale/data/patch.bsdiff
index 8d7dcbf43a96..5d137537be83 100644
Binary files a/worlds/undertale/data/patch.bsdiff and b/worlds/undertale/data/patch.bsdiff differ
diff --git a/worlds/undertale/docs/en_Undertale.md b/worlds/undertale/docs/en_Undertale.md
index 79ca21681e58..02fc32f0abc6 100644
--- a/worlds/undertale/docs/en_Undertale.md
+++ b/worlds/undertale/docs/en_Undertale.md
@@ -1,8 +1,8 @@
# Undertale
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What is considered a location check in Undertale?
@@ -24,24 +24,40 @@ every major route in the game, those being `Pacifist`, `Neutral`, and `Genocide`
There are some major differences between vanilla and the randomizer.
-There are now doors to every major area in the underground located in the flower room (The first room of the game), those being Ruins, Snowdin, Waterfall, Hotland, and Core.
+There are now doors to every major area in the underground located in the flower room (the first room of the game.)
+These doors lead to Ruins, Snowdin, Waterfall, Hotland, and Core from left to right.
Each door needs their respective key from the pool to enter.
-You start with one key for a random door. (Core will never be given to start with.)
-The rest of the keys will be in the item pool.
+You start with one key for a random door and the rest of the keys will be in the item pool to be found by other players.
+(Core will never be given to start with, unless otherwise specified.)
-Genocide works a little differently in terms of the requirements.
-You now only need to get through Core and fight Mettaton NEO, and then beat Sans, to win.
-If you choose to fight other major bosses, you will still need to grind out the area before fighting them like normal.
-Pacifist is mostly the same, except you are not required to go to the Ruins to spare Toriel,
-you only need to spare Papyrus, Undyne, and Mettaton EX. Although you still cannot kill anyone.
-You are also still required to do the date/hangout with Papyrus, the hangout with Undyne, and the date with Alphys,
-in that order, before entering the True Lab.
+**Genocide** works a little differently in terms of the requirements.
-You now require custom items to Hangout with Papyrus, Undyne, to enter the True Lab, and to fight Mettaton EX/NEO.
-Those being `Complete Skeleton`, `Fish`, `DT Extractor`, and `Mettaton Plush`.
+In order to win with the genocide route, you only need to get through Core, fight Mettaton NEO, and beat Sans to win.
+If you choose to fight other major bosses, you will still need to progress the area like normal before fighting them.
-The Riverperson will only take you to locations you have actually seen the Riverperson at.
-Meaning they will only take you to, for example, Waterfall, if you have seen the Riverperson at Waterfall at least once.
+**Pacifist** remains mostly the same.
-If you press `W` while in the save menu, you will teleport back to the flower room, for quick access to the other areas.
\ No newline at end of file
+In the Pacifist run, you are not required to go to the Ruins to spare Toriel. The only necessary spares are Papyrus,
+Undyne, and Mettaton EX. Just as it is in the vanilla game, you cannot kill anyone. You are also required to complete
+the date/hangout with Papyrus, Undyne, and Alphys, in that order, before entering the True Lab.
+
+Additionally, custom items are required to hang out with Papyrus, Undyne, to enter the True Lab, and to fight
+Mettaton EX/NEO. The respective items for each interaction are `Complete Skeleton`, `Fish`, `DT Extractor`,
+and `Mettaton Plush`.
+
+The Riverperson will only take you to locations you have seen them at, meaning they will only take you to
+Waterfall if you have seen them at Waterfall at least once.
+
+If you press `W` while in the save menu, you will teleport back to the flower room, for quick access to the other areas.
+
+## Unique Local Commands
+
+The following commands are only available when using the UndertaleClient to play with Archipelago.
+
+- `/resync` Manually trigger a resync.
+- `/savepath` Redirect to proper save data folder. This is necessary for Linux users to use before connecting.
+- `/auto_patch` Patch the game automatically.
+- `/patch` Patch the game. Only use this command if `/auto_patch` fails.
+- `/online` Toggles seeing other Undertale players.
+- `/deathlink` Toggles deathlink
diff --git a/worlds/undertale/docs/setup_en.md b/worlds/undertale/docs/setup_en.md
new file mode 100644
index 000000000000..f1f740959127
--- /dev/null
+++ b/worlds/undertale/docs/setup_en.md
@@ -0,0 +1,64 @@
+# Undertale Randomizer Setup Guide
+
+### Required Software
+
+- Undertale from the [Steam page](https://store.steampowered.com/app/391540)
+- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
+
+### First time setup
+
+Start the Undertale client from your Archipelago folder and input `/auto_patch ` at the bottom.
+
+This directory is usually located at `C:\Program Files\Steam\steamapps\Undertale`, but it can be different depending on
+your installation. You can easily find the directory by opening the Undertale directory through Steam by right-clicking
+Undertale in your library and selecting `Manage -> Browse local files`. Then, on Windows you can see the directory that
+you need at the top of the window that opens.
+
+After using the `/auto_patch` command, **Archipelago will make an Undertale folder within the Archipelago install
+location.** That folder contains the version of Undertale you will use for Archipelago. (If you update Archipelago,
+you will need to redo this set-up.)
+
+**Linux Users**: The Linux installation is mostly similar, however, Undertale will be installed on Steam as the Linux
+variant. Since this randomizer only supports the Windows version, we must fix this, by right-click the game in Steam,
+going to `Properties -> Compatibility`, and checking `Force the use of a specific Steam Play compatibility tool`. This
+downloads the Windows version of Undertale to use instead of the Linux version. If the play button is greyed out in
+Steam, be sure to go to `Settings -> Compatibility` and toggle `Enable Steam Play for all other titles`.
+
+### Connect to the MultiServer
+
+Make sure both Undertale **from the Archipelago folder** and its client are running. (Undertale will ask for a save slot
+to play on. Archipelago Undertale does not overwrite vanilla saves, but you may want to back up your save as a precaution.)
+
+In the top text box of the client, type the `IP Address` (or `Hostname`) and `Port` separated with a `:` symbol.
+(Ex. `archipelago.gg:38281`)
+
+The client will then ask for the slot name, input your slot name chosen during YAML creation in the text box at the
+bottom of the client.
+
+**Linux Users**: When you start the client, it is likely that the save data path is incorrect, and how the game
+is played depends on where the save data folder is located.
+
+**On Steam (via Proton)**: This assumes the game is in a Steam Library folder. Right-click Undertale, go to `Manage ->
+Browse Local Files`. Go up the directories to the `steamapps` folder, open `compatdata/391540` (391540 is the "magic number" for
+Undertale in Steam). Save data from here is at `/pfx/drive_c/users/steamuser/AppData/Local/UNDERTALE`.
+
+**Through WINE directly**: This depends on the prefix used. If it is default, then the save data is located at
+`/home/USERNAME/.wine/drive_c/users/USERNAME/AppData/Local/UNDERTALE`.
+
+Once the save data folder is located, run the `/savepath` command to redirect the client to the correct save data folder
+before connecting.
+
+### Play the game
+
+When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a
+multi-world game!
+
+### PLEASE READ!
+
+Please read this page in its entirety before asking questions! Most importantly, there is a list of
+gameplay differences at the bottom.
+[Undertale Game Info Page](/games/Undertale/info/en)
+
+### Where do I get a YAML file?
+
+You can customize your options by visiting the [Undertale Player Options Page](/games/Undertale/player-options)
diff --git a/worlds/undertale/docs/undertale_en.md b/worlds/undertale/docs/undertale_en.md
deleted file mode 100644
index a2f3d2579a94..000000000000
--- a/worlds/undertale/docs/undertale_en.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# Undertale Randomizer Setup Guide
-
-### Required Software
-
-- Undertale from the [Steam page](https://store.steampowered.com/app/391540)
-- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- - (select `Undertale Client` during installation.)
-
-### First time setup
-
-Start the Undertale client, and in the bottom text box, input `/auto_patch (Input your Undertale install directory here)` (It is usually located at `C:\Program Files\Steam\steamapps\Undertale`, but it can be different, you can more easily find the directory
-by opening the Undertale directory through Steam), it will then make an Undertale folder that will be created in the
-Archipelago install location. That contains the version of Undertale you will use for Archipelago. (You will need to
-redo this step when updating Archipelago.)
-
-**Linux Users**: This guide is mostly similar; however, when Undertale is installed on Steam, it defaults to a Linux
-supported variant; this randomizer only supports the Windows version. To fix this, right-click the game in Steam, go to
-Properties -> Compatibility, and check "Force the use of a specific Steam Play compatibility tool". This
-downloads the Windows version instead. If the play button is greyed out in Steam, be sure to go to
-Settings -> Compatibility and toggle "Enable Steam Play for all other titles".
-
-### Connect to the MultiServer
-
-Make sure both Undertale and its client are running. (Undertale will ask for a saveslot, it can be 1 through 99, none
-of the slots will overwrite your vanilla save, although you may want to make a backup just in case.)
-
-In the top text box of the client, type the
-`Ip Address` (or `Hostname`) and `Port` separated with a `:` symbol. (Ex. `archipelago.gg:38281`)
-
-The client will then ask for the slot name, input that in the text box at the bottom of the client.
-
-**Linux Users**: When you start the client, it is likely that the save data path is incorrect, and how the game
-is played depends on where the save data folder is located.
-
-*On Steam (via Proton)*: This assumes the game is in a Steam Library folder. Right-click Undertale, go to Manage ->
-Browse Local Files. Move back to the steamapps folder, open compatdata/391540 (391540 is the "magic number" for
-Undertale in Steam and can be confirmed by visiting its store page and looking at the URL). Save data from here is at
-/pfx/drive_c/users/steamuser/AppData/Local/UNDERTALE.
-
-*Through WINE directly*: This depends on the prefix used. If it is default, then the save data is located at
-/home/USERNAME/.wine/drive_c/users/USERNAME/AppData/Local/UNDERTALE.
-
-Once the save data folder is located, run the /savepath command to redirect the client to the correct save data folder
-before connecting.
-
-### Play the game
-
-When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a
-multiworld game!
-
-### PLEASE READ!
-
-Please read this page in its entirety before asking questions! Most importantly, there is a list of
-gameplay differences at the bottom.
-[Undertale Game Info Page](/games/Undertale/info/en)
-
-### Where do I get a YAML file?
-
-You can customize your settings by visiting the [Undertale Player Settings Page](/games/Undertale/player-settings)
diff --git a/worlds/v6/Options.py b/worlds/v6/Options.py
index 107fbab465e1..1950d1bcbd02 100644
--- a/worlds/v6/Options.py
+++ b/worlds/v6/Options.py
@@ -1,8 +1,10 @@
import typing
-from Options import Option, DeathLink, Range, Toggle
+from dataclasses import dataclass
+from Options import Option, DeathLink, Range, Toggle, PerGameCommonOptions
class DoorCost(Range):
"""Amount of Trinkets required to enter Areas. Set to 0 to disable artificial locks."""
+ display_name = "Door Cost"
range_start = 0
range_end = 3
default = 3
@@ -13,6 +15,7 @@ class AreaCostRandomizer(Toggle):
class DeathLinkAmnesty(Range):
"""Amount of Deaths to take before sending a DeathLink signal, for balancing difficulty"""
+ display_name = "Death Link Amnesty"
range_start = 0
range_end = 30
default = 15
@@ -25,11 +28,11 @@ class MusicRandomizer(Toggle):
"""Randomize Music"""
display_name = "Music Randomizer"
-v6_options: typing.Dict[str,type(Option)] = {
- "MusicRandomizer": MusicRandomizer,
- "AreaRandomizer": AreaRandomizer,
- "DoorCost": DoorCost,
- "AreaCostRandomizer": AreaCostRandomizer,
- "death_link": DeathLink,
- "DeathLinkAmnesty": DeathLinkAmnesty
-}
\ No newline at end of file
+@dataclass
+class V6Options(PerGameCommonOptions):
+ music_rando: MusicRandomizer
+ area_rando: AreaRandomizer
+ door_cost: DoorCost
+ area_cost: AreaCostRandomizer
+ death_link: DeathLink
+ death_link_amnesty: DeathLinkAmnesty
diff --git a/worlds/v6/Regions.py b/worlds/v6/Regions.py
index 5a8f0315f44a..f6e9ee753890 100644
--- a/worlds/v6/Regions.py
+++ b/worlds/v6/Regions.py
@@ -31,14 +31,3 @@ def create_regions(world: MultiWorld, player: int):
locWrp_names = ["Edge Games"]
regWrp.locations += [V6Location(player, loc_name, location_table[loc_name], regWrp) for loc_name in locWrp_names]
world.regions.append(regWrp)
-
-
-def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule):
- sourceRegion = world.get_region(source, player)
- targetRegion = world.get_region(target, player)
-
- connection = Entrance(player,'', sourceRegion)
- connection.access_rule = rule
-
- sourceRegion.exits.append(connection)
- connection.connect(targetRegion)
\ No newline at end of file
diff --git a/worlds/v6/Rules.py b/worlds/v6/Rules.py
index ecb34f2f32ff..bf0d60499eb5 100644
--- a/worlds/v6/Rules.py
+++ b/worlds/v6/Rules.py
@@ -1,6 +1,6 @@
import typing
from ..generic.Rules import add_rule
-from .Regions import connect_regions, v6areas
+from .Regions import v6areas
def _has_trinket_range(state, player, start, end) -> bool:
@@ -10,34 +10,36 @@ def _has_trinket_range(state, player, start, end) -> bool:
return True
-def set_rules(world, player, area_connections: typing.Dict[int, int], area_cost_map: typing.Dict[int, int]):
+def set_rules(multiworld, options, player, area_connections: typing.Dict[int, int], area_cost_map: typing.Dict[int, int]):
areashuffle = list(range(len(v6areas)))
- if world.AreaRandomizer[player].value:
- world.random.shuffle(areashuffle)
+ if options.area_rando:
+ multiworld.random.shuffle(areashuffle)
area_connections.update({(index + 1): (value + 1) for index, value in enumerate(areashuffle)})
area_connections.update({0: 0})
- if world.AreaCostRandomizer[player].value:
- world.random.shuffle(areashuffle)
+ if options.area_cost:
+ multiworld.random.shuffle(areashuffle)
area_cost_map.update({(index + 1): (value + 1) for index, value in enumerate(areashuffle)})
area_cost_map.update({0: 0})
+ menu_region = multiworld.get_region("Menu", player)
for i in range(1, 5):
- connect_regions(world, player, "Menu", v6areas[area_connections[i] - 1],
- lambda state, i=i: _has_trinket_range(state, player,
- world.DoorCost[player].value * (area_cost_map[i] - 1),
- world.DoorCost[player].value * area_cost_map[i]))
+ target_region = multiworld.get_region(v6areas[area_connections[i] - 1], player)
+ menu_region.connect(connecting_region=target_region,
+ rule=lambda state, i=i: _has_trinket_range(state, player,
+ options.door_cost * (area_cost_map[i] - 1),
+ options.door_cost * area_cost_map[i]))
# Special Rule for V
- add_rule(world.get_location("V", player), lambda state: state.can_reach("Laboratory", 'Region', player) and
+ add_rule(multiworld.get_location("V", player), lambda state: state.can_reach("Laboratory", 'Region', player) and
state.can_reach("The Tower", 'Region', player) and
state.can_reach("Space Station 2", 'Region', player) and
state.can_reach("Warp Zone", 'Region', player))
# Special Rule for NPC Trinket
- add_rule(world.get_location("NPC Trinket", player),
+ add_rule(multiworld.get_location("NPC Trinket", player),
lambda state: state.can_reach("Laboratory", 'Region', player) or
(state.can_reach("The Tower", 'Region', player) and
state.can_reach("Space Station 2", 'Region', player) and
state.can_reach("Warp Zone", 'Region', player)))
- world.completion_condition[player] = lambda state: state.can_reach("V", 'Location', player)
+ multiworld.completion_condition[player] = lambda state: state.can_reach("V", 'Location', player)
diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py
index 6ff7fba60c2d..3d3ee8cf58fd 100644
--- a/worlds/v6/__init__.py
+++ b/worlds/v6/__init__.py
@@ -2,7 +2,7 @@
import os, json
from .Items import item_table, V6Item
from .Locations import location_table, V6Location
-from .Options import v6_options
+from .Options import V6Options
from .Rules import set_rules
from .Regions import create_regions
from BaseClasses import Item, ItemClassification, Tutorial
@@ -34,14 +34,12 @@ class V6World(World):
item_name_to_id = item_table
location_name_to_id = location_table
- data_version = 1
-
area_connections: typing.Dict[int, int]
area_cost_map: typing.Dict[int,int]
music_map: typing.Dict[int,int]
- option_definitions = v6_options
+ options_dataclass = V6Options
def create_regions(self):
create_regions(self.multiworld, self.player)
@@ -49,7 +47,7 @@ def create_regions(self):
def set_rules(self):
self.area_connections = {}
self.area_cost_map = {}
- set_rules(self.multiworld, self.player, self.area_connections, self.area_cost_map)
+ set_rules(self.multiworld, self.options, self.player, self.area_connections, self.area_cost_map)
def create_item(self, name: str) -> Item:
return V6Item(name, ItemClassification.progression, item_table[name], self.player)
@@ -61,7 +59,7 @@ def create_items(self):
def generate_basic(self):
musiclist_o = [1,2,3,4,9,12]
musiclist_s = musiclist_o.copy()
- if self.multiworld.MusicRandomizer[self.player].value:
+ if self.options.music_rando:
self.multiworld.random.shuffle(musiclist_s)
self.music_map = dict(zip(musiclist_o, musiclist_s))
@@ -69,10 +67,10 @@ def fill_slot_data(self):
return {
"MusicRando": self.music_map,
"AreaRando": self.area_connections,
- "DoorCost": self.multiworld.DoorCost[self.player].value,
+ "DoorCost": self.options.door_cost.value,
"AreaCostRando": self.area_cost_map,
- "DeathLink": self.multiworld.death_link[self.player].value,
- "DeathLink_Amnesty": self.multiworld.DeathLinkAmnesty[self.player].value
+ "DeathLink": self.options.death_link.value,
+ "DeathLink_Amnesty": self.options.death_link_amnesty.value
}
def generate_output(self, output_directory: str):
diff --git a/worlds/v6/docs/en_VVVVVV.md b/worlds/v6/docs/en_VVVVVV.md
index 5c2aa8fec957..c5790e01c5dd 100644
--- a/worlds/v6/docs/en_VVVVVV.md
+++ b/worlds/v6/docs/en_VVVVVV.md
@@ -1,9 +1,9 @@
# VVVVVV
-## Where is the settings page?
+## Where is the options page?
-The player settings page for this game contains all the options you need to configure and export a config file. Player
-settings page link: [VVVVVV Player Settings Page](../player-settings).
+The player options page for this game contains all the options you need to configure and export a config file. Player
+options page link: [VVVVVV Player Options Page](../player-options).
## What does randomization do to this game?
All 20 Trinkets are now Location Checks and may not actually contain Trinkets, but Items for different games.
diff --git a/worlds/v6/docs/setup_en.md b/worlds/v6/docs/setup_en.md
index 7adf5948c7e4..a23b6c5b252a 100644
--- a/worlds/v6/docs/setup_en.md
+++ b/worlds/v6/docs/setup_en.md
@@ -30,7 +30,7 @@ If everything worked out, you will see a textbox informing you the connection ha
# Playing offline
-To play offline, first generate a seed on the game's settings page.
+To play offline, first generate a seed on the game's options page.
Create a room and download the `.apv6` file, include the offline single-player launch option described above.
## Installation Troubleshooting
diff --git a/worlds/wargroove/Options.py b/worlds/wargroove/Options.py
index c8b8b37ee1f9..1af077206556 100644
--- a/worlds/wargroove/Options.py
+++ b/worlds/wargroove/Options.py
@@ -1,5 +1,6 @@
import typing
-from Options import Choice, Option, Range
+from dataclasses import dataclass
+from Options import Choice, Option, Range, PerGameCommonOptions
class IncomeBoost(Range):
@@ -30,9 +31,8 @@ class CommanderChoice(Choice):
option_unlockable_factions = 1
option_random_starting_faction = 2
-
-wargroove_options: typing.Dict[str, type(Option)] = {
- "income_boost": IncomeBoost,
- "commander_defense_boost": CommanderDefenseBoost,
- "commander_choice": CommanderChoice
-}
+@dataclass
+class WargrooveOptions(PerGameCommonOptions):
+ income_boost: IncomeBoost
+ commander_defense_boost: CommanderDefenseBoost
+ commander_choice: CommanderChoice
diff --git a/worlds/wargroove/__init__.py b/worlds/wargroove/__init__.py
index ab4a9364fac0..f204f468d1ab 100644
--- a/worlds/wargroove/__init__.py
+++ b/worlds/wargroove/__init__.py
@@ -7,8 +7,8 @@
from .Locations import location_table
from .Regions import create_regions
from .Rules import set_rules
-from ..AutoWorld import World, WebWorld
-from .Options import wargroove_options
+from worlds.AutoWorld import World, WebWorld
+from .Options import WargrooveOptions
class WargrooveSettings(settings.Group):
@@ -38,11 +38,11 @@ class WargrooveWorld(World):
Command an army, in this retro style turn based strategy game!
"""
- option_definitions = wargroove_options
+ options: WargrooveOptions
+ options_dataclass = WargrooveOptions
settings: typing.ClassVar[WargrooveSettings]
game = "Wargroove"
topology_present = True
- data_version = 1
web = WargrooveWeb()
item_name_to_id = {name: data.code for name, data in item_table.items()}
@@ -50,16 +50,17 @@ class WargrooveWorld(World):
def _get_slot_data(self):
return {
- 'seed': "".join(self.multiworld.per_slot_randoms[self.player].choice(string.ascii_letters) for i in range(16)),
- 'income_boost': self.multiworld.income_boost[self.player],
- 'commander_defense_boost': self.multiworld.commander_defense_boost[self.player],
- 'can_choose_commander': self.multiworld.commander_choice[self.player] != 0,
+ 'seed': "".join(self.random.choice(string.ascii_letters) for i in range(16)),
+ 'income_boost': self.options.income_boost.value,
+ 'commander_defense_boost': self.options.commander_defense_boost.value,
+ 'can_choose_commander': self.options.commander_choice.value != 0,
+ 'commander_choice': self.options.commander_choice.value,
'starting_groove_multiplier': 20 # Backwards compatibility in case this ever becomes an option
}
def generate_early(self):
# Selecting a random starting faction
- if self.multiworld.commander_choice[self.player] == 2:
+ if self.options.commander_choice.value == 2:
factions = [faction for faction in faction_table.keys() if faction != "Starter"]
starting_faction = WargrooveItem(self.multiworld.random.choice(factions) + ' Commanders', self.player)
self.multiworld.push_precollected(starting_faction)
@@ -68,7 +69,7 @@ def create_items(self):
# Fill out our pool with our items from the item table
pool = []
precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]}
- ignore_faction_items = self.multiworld.commander_choice[self.player] == 0
+ ignore_faction_items = self.options.commander_choice.value == 0
for name, data in item_table.items():
if data.code is not None and name not in precollected_item_names and not data.classification == ItemClassification.filler:
if name.endswith(' Commanders') and ignore_faction_items:
@@ -105,9 +106,6 @@ def create_regions(self):
def fill_slot_data(self) -> dict:
slot_data = self._get_slot_data()
- for option_name in wargroove_options:
- option = getattr(self.multiworld, option_name)[self.player]
- slot_data[option_name] = int(option.value)
return slot_data
def get_filler_item_name(self) -> str:
@@ -131,12 +129,6 @@ def create_region(world: MultiWorld, player: int, name: str, locations=None, exi
class WargrooveLocation(Location):
game: str = "Wargroove"
- def __init__(self, player: int, name: str, address=None, parent=None):
- super(WargrooveLocation, self).__init__(player, name, address, parent)
- if address is None:
- self.event = True
- self.locked = True
-
class WargrooveItem(Item):
game = "Wargroove"
diff --git a/worlds/wargroove/docs/en_Wargroove.md b/worlds/wargroove/docs/en_Wargroove.md
index 18474a426915..31fd8c81301c 100644
--- a/worlds/wargroove/docs/en_Wargroove.md
+++ b/worlds/wargroove/docs/en_Wargroove.md
@@ -1,8 +1,8 @@
# Wargroove (Steam, Windows)
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -26,9 +26,16 @@ Any of the above items can be in another player's world.
## When the player receives an item, what happens?
-When the player receives an item, a message will appear in Wargroove with the item name and sender name, once an action
+When the player receives an item, a message will appear in Wargroove with the item name and sender name, once an action
is taken in game.
## What is the goal of this game when randomized?
The goal is to beat the level titled `The End` by finding the `Final Bridges`, `Final Walls`, and `Final Sickle`.
+
+## Unique Local Commands
+
+The following commands are only available when using the WargrooveClient to play with Archipelago.
+
+- `/resync` Manually trigger a resync.
+- `/commander` Set the current commander to the given commander.
diff --git a/worlds/wargroove/docs/wargroove_en.md b/worlds/wargroove/docs/wargroove_en.md
index 121e8c089083..9c2645178aa2 100644
--- a/worlds/wargroove/docs/wargroove_en.md
+++ b/worlds/wargroove/docs/wargroove_en.md
@@ -18,7 +18,7 @@ is strongly recommended in case they become corrupted.
2. Open the `host.yaml` file in your favorite text editor (Notepad will work).
3. Put your Wargroove root directory in the `root_directory:` under the `wargroove_options:` section.
- The Wargroove root directory can be found by going to
- `Steam->Right Click Wargroove->Properties->Local Files->Browse Local Files` and copying the path in the address bar.
+ `Steam->Right Click Wargroove->Properties->Installed Files->Browse` and copying the path in the address bar.
- Paste the path in between the quotes next to `root_directory:` in the `host.yaml`.
- You may have to replace all single \\ with \\\\.
4. Start the Wargroove client.
@@ -38,7 +38,7 @@ This should install the mod and campaign for you.
## Starting a Multiworld game
1. Start the Wargroove Client and connect to the server. Enter your username from your
-[settings file.](/games/Wargroove/player-settings)
+[options file.](/games/Wargroove/player-options)
2. Start Wargroove and play the Archipelago campaign by going to `Story->Campaign->Custom->Archipelago`.
## Ending a Multiworld game
diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py
deleted file mode 100644
index b7364b5e70ea..000000000000
--- a/worlds/witness/Options.py
+++ /dev/null
@@ -1,201 +0,0 @@
-from typing import Dict, Union
-from BaseClasses import MultiWorld
-from Options import Toggle, DefaultOnToggle, Range, Choice
-
-
-# class HardMode(Toggle):
-# "Play the randomizer in hardmode"
-# display_name = "Hard Mode"
-
-class DisableNonRandomizedPuzzles(Toggle):
- """Disables puzzles that cannot be randomized.
- This includes many puzzles that heavily involve the environment, such as Shadows, Monastery or Orchard.
- The lasers for those areas will be activated as you solve optional puzzles throughout the island."""
- display_name = "Disable non randomized puzzles"
-
-
-class EarlySecretArea(Toggle):
- """Opens the Mountainside shortcut to the Caves from the start.
- (Otherwise known as "UTM", "Caves" or the "Challenge Area")"""
- display_name = "Early Caves"
-
-
-class ShuffleSymbols(DefaultOnToggle):
- """You will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols.
- If you turn this off, there will be no progression items in the game unless you turn on door shuffle."""
- display_name = "Shuffle Symbols"
-
-
-class ShuffleLasers(Toggle):
- """If on, the 11 lasers are turned into items and will activate on their own upon receiving them.
- Note: There is a visual bug that can occur with the Desert Laser. It does not affect gameplay - The Laser can still
- be redirected as normal, for both applications of redirection."""
- display_name = "Shuffle Lasers"
-
-
-class ShuffleDoors(Choice):
- """If on, opening doors will require their respective "keys".
- If set to "panels", those keys will unlock the panels on doors.
- In "doors_simple" and "doors_complex", the doors will magically open by themselves upon receiving the key.
- The last option, "max", is a combination of "doors_complex" and "panels"."""
- display_name = "Shuffle Doors"
- option_none = 0
- option_panels = 1
- option_doors_simple = 2
- option_doors_complex = 3
- option_max = 4
-
-
-class ShuffleDiscardedPanels(Toggle):
- """Add Discarded Panels into the location pool.
- Solving certain Discarded Panels may still be necessary to beat the game, even if this is off."""
-
- display_name = "Shuffle Discarded Panels"
-
-
-class ShuffleVaultBoxes(Toggle):
- """Vault Boxes will have items on them."""
- display_name = "Shuffle Vault Boxes"
-
-
-class ShuffleEnvironmentalPuzzles(Choice):
- """
- Add Environmental/Obelisk Puzzles into the location pool.
- In "individual", every Environmental Puzzle sends an item.
- In "obelisk_sides", completing every puzzle on one side of an Obelisk sends an item.
- Note: In Obelisk Sides, any EPs excluded through another setting will be counted as pre-completed on their Obelisk.
- """
- display_name = "Shuffle Environmental Puzzles"
- option_off = 0
- option_individual = 1
- option_obelisk_sides = 2
-
-
-class ShuffleDog(Toggle):
- """Add petting the Town dog into the location pool."""
-
- display_name = "Pet the Dog"
-
-
-class EnvironmentalPuzzlesDifficulty(Choice):
- """
- When "Shuffle Environmental Puzzles" is on, this setting governs which EPs are eligible for the location pool.
- On "eclipse", every EP in the game is eligible, including the 1-hour-long "Theater Eclipse EP".
- On "tedious", Theater Eclipse EP is excluded from the location pool.
- On "normal", several other difficult or long EPs are excluded as well.
- """
- display_name = "Environmental Puzzles Difficulty"
- option_normal = 0
- option_tedious = 1
- option_eclipse = 2
-
-
-class ShufflePostgame(Toggle):
- """Adds locations into the pool that are guaranteed to become accessible after or at the same time as your goal.
- Use this if you don't play with release on victory. IMPORTANT NOTE: The possibility of your second
- "Progressive Dots" showing up in the Caves is ignored, they will still be considered "postgame" in base settings."""
- display_name = "Shuffle Postgame"
-
-
-class VictoryCondition(Choice):
- """Change the victory condition from the original game's ending (elevator) to beating the Challenge
- or solving the mountaintop box, either using the short solution
- (7 lasers or whatever you've changed it to) or the long solution (11 lasers or whatever you've changed it to)."""
- display_name = "Victory Condition"
- option_elevator = 0
- option_challenge = 1
- option_mountain_box_short = 2
- option_mountain_box_long = 3
-
-
-class PuzzleRandomization(Choice):
- """Puzzles in this randomizer are randomly generated. This setting changes the difficulty/types of puzzles."""
- display_name = "Puzzle Randomization"
- option_sigma_normal = 0
- option_sigma_expert = 1
- option_none = 2
-
-
-class MountainLasers(Range):
- """Sets the amount of beams required to enter the final area."""
- display_name = "Required Lasers for Mountain Entry"
- range_start = 1
- range_end = 7
- default = 7
-
-
-class ChallengeLasers(Range):
- """Sets the amount of beams required to enter the Caves through the Mountain Bottom Floor Discard."""
- display_name = "Required Lasers for Challenge"
- range_start = 1
- range_end = 11
- default = 11
-
-
-class TrapPercentage(Range):
- """Replaces junk items with traps, at the specified rate."""
- display_name = "Trap Percentage"
- range_start = 0
- range_end = 100
- default = 20
-
-
-class PuzzleSkipAmount(Range):
- """Adds this number of Puzzle Skips into the pool, if there is room. Puzzle Skips let you skip one panel.
- Works on most panels in the game - The only big exception is The Challenge."""
- display_name = "Puzzle Skips"
- range_start = 0
- range_end = 30
- default = 10
-
-
-class HintAmount(Range):
- """Adds hints to Audio Logs. Hints will have the same number of duplicates, as many as will fit. Remaining Audio
- Logs will have junk hints."""
- display_name = "Hints on Audio Logs"
- range_start = 0
- range_end = 49
- default = 10
-
-
-class DeathLink(Toggle):
- """If on: Whenever you fail a puzzle (with some exceptions), everyone who is also on Death Link dies.
- The effect of a "death" in The Witness is a Power Surge."""
- display_name = "Death Link"
-
-
-the_witness_options: Dict[str, type] = {
- "puzzle_randomization": PuzzleRandomization,
- "shuffle_symbols": ShuffleSymbols,
- "shuffle_doors": ShuffleDoors,
- "shuffle_lasers": ShuffleLasers,
- "disable_non_randomized_puzzles": DisableNonRandomizedPuzzles,
- "shuffle_discarded_panels": ShuffleDiscardedPanels,
- "shuffle_vault_boxes": ShuffleVaultBoxes,
- "shuffle_EPs": ShuffleEnvironmentalPuzzles,
- "EP_difficulty": EnvironmentalPuzzlesDifficulty,
- "shuffle_postgame": ShufflePostgame,
- "victory_condition": VictoryCondition,
- "mountain_lasers": MountainLasers,
- "challenge_lasers": ChallengeLasers,
- "early_secret_area": EarlySecretArea,
- "trap_percentage": TrapPercentage,
- "puzzle_skip_amount": PuzzleSkipAmount,
- "hint_amount": HintAmount,
- "death_link": DeathLink,
-}
-
-
-def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool:
- return get_option_value(world, player, name) > 0
-
-
-def get_option_value(world: MultiWorld, player: int, name: str) -> Union[bool, int]:
- option = getattr(world, name, None)
-
- if option is None:
- return 0
-
- if issubclass(the_witness_options[name], Toggle) or issubclass(the_witness_options[name], DefaultOnToggle):
- return bool(option[player].value)
- return option[player].value
diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt
deleted file mode 100644
index 71ffe276a60e..000000000000
--- a/worlds/witness/WitnessItems.txt
+++ /dev/null
@@ -1,207 +0,0 @@
-Symbols:
-0 - Dots
-1 - Colored Dots
-2 - Full Dots
-3 - Invisible Dots
-5 - Sound Dots
-10 - Symmetry
-20 - Triangles
-30 - Eraser
-40 - Shapers
-41 - Rotated Shapers
-50 - Negative Shapers
-60 - Stars
-61 - Stars + Same Colored Symbol
-71 - Black/White Squares
-72 - Colored Squares
-80 - Arrows
-200 - Progressive Dots - Dots,Full Dots
-260 - Progressive Stars - Stars,Stars + Same Colored Symbol
-
-Useful:
-510 - Puzzle Skip
-#511 - Energy Capacity
-
-Filler:
-500 - Speed Boost - 1
-#501 - Energy Fill (Small) - 6
-#502 - Energy Fill - 3
-#503 - Energy Fill (Max) - 1
-
-Traps:
-600 - Slowness - 8
-610 - Power Surge - 2
-
-Jokes:
-650 - Functioning Brain
-
-Doors:
-1100 - Glass Factory Entry (Panel) - 0x01A54
-1105 - Symmetry Island Lower (Panel) - 0x000B0
-1107 - Symmetry Island Upper (Panel) - 0x1C349
-1110 - Desert Light Room Entry (Panel) - 0x0C339
-1111 - Desert Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B
-1119 - Quarry Stoneworks Entry (Panel) - 0x01E5A,0x01E59
-1120 - Quarry Stoneworks Ramp Controls (Panel) - 0x03678,0x03676
-1122 - Quarry Stoneworks Lift Controls (Panel) - 0x03679,0x03675
-1125 - Quarry Boathouse Ramp Height Control (Panel) - 0x03852
-1127 - Quarry Boathouse Ramp Horizontal Control (Panel) - 0x03858
-1131 - Shadows Door Timer (Panel) - 0x334DB,0x334DC
-1150 - Monastery Entry Left (Panel) - 0x00B10
-1151 - Monastery Entry Right (Panel) - 0x00C92
-1162 - Town Tinted Glass Door (Panel) - 0x28998
-1163 - Town Church Entry (Panel) - 0x28A0D
-1166 - Town Maze Panel (Drop-Down Staircase) (Panel) - 0x28A79
-1169 - Windmill Entry (Panel) - 0x17F5F
-1200 - Treehouse First & Second Doors (Panel) - 0x0288C,0x02886
-1202 - Treehouse Third Door (Panel) - 0x0A182
-1205 - Treehouse Laser House Door Timer (Panel) - 0x2700B,0x17CBC
-1208 - Treehouse Drawbridge (Panel) - 0x037FF
-1175 - Jungle Popup Wall (Panel) - 0x17CAB
-1180 - Bunker Entry (Panel) - 0x17C2E
-1183 - Bunker Tinted Glass Door (Panel) - 0x0A099
-1186 - Bunker Elevator Control (Panel) - 0x0A079
-1190 - Swamp Entry (Panel) - 0x0056E
-1192 - Swamp Sliding Bridge (Panel) - 0x00609,0x18488
-1195 - Swamp Rotating Bridge (Panel) - 0x181F5
-1197 - Swamp Maze Control (Panel) - 0x17C0A,0x17E07
-1310 - Boat - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054
-
-1400 - Caves Mountain Shortcut (Door) - 0x2D73F
-
-1600 - Outside Tutorial Outpost Path (Door) - 0x03BA2
-1603 - Outside Tutorial Outpost Entry (Door) - 0x0A170
-1606 - Outside Tutorial Outpost Exit (Door) - 0x04CA3
-1609 - Glass Factory Entry (Door) - 0x01A29
-1612 - Glass Factory Back Wall (Door) - 0x0D7ED
-1615 - Symmetry Island Lower (Door) - 0x17F3E
-1618 - Symmetry Island Upper (Door) - 0x18269
-1619 - Orchard First Gate (Door) - 0x03307
-1620 - Orchard Second Gate (Door) - 0x03313
-1621 - Desert Light Room Entry (Door) - 0x09FEE
-1624 - Desert Pond Room Entry (Door) - 0x0C2C3
-1627 - Desert Flood Room Entry (Door) - 0x0A24B
-1630 - Desert Elevator Room Entry (Door) - 0x0C316
-1633 - Quarry Entry 1 (Door) - 0x09D6F
-1636 - Quarry Entry 2 (Door) - 0x17C07
-1639 - Quarry Stoneworks Entry (Door) - 0x02010
-1642 - Quarry Stoneworks Side Exit (Door) - 0x275FF
-1645 - Quarry Stoneworks Roof Exit (Door) - 0x17CE8
-1648 - Quarry Stoneworks Stairs (Door) - 0x0368A
-1651 - Quarry Boathouse Dock (Door) - 0x2769B,0x27163
-1653 - Quarry Boathouse First Barrier (Door) - 0x17C50
-1654 - Quarry Boathouse Second Barrier (Door) - 0x3865F
-1656 - Shadows Timed Door - 0x19B24
-1657 - Shadows Laser Entry Right (Door) - 0x194B2
-1660 - Shadows Laser Entry Left (Door) - 0x19665
-1663 - Shadows Quarry Barrier (Door) - 0x19865,0x0A2DF
-1666 - Shadows Ledge Barrier (Door) - 0x1855B,0x19ADE
-1669 - Keep Hedge Maze 1 Exit (Door) - 0x01954
-1672 - Keep Pressure Plates 1 Exit (Door) - 0x01BEC
-1675 - Keep Hedge Maze 2 Shortcut (Door) - 0x018CE
-1678 - Keep Hedge Maze 2 Exit (Door) - 0x019D8
-1681 - Keep Hedge Maze 3 Shortcut (Door) - 0x019B5
-1684 - Keep Hedge Maze 3 Exit (Door) - 0x019E6
-1687 - Keep Hedge Maze 4 Shortcut (Door) - 0x0199A
-1690 - Keep Hedge Maze 4 Exit (Door) - 0x01A0E
-1693 - Keep Pressure Plates 2 Exit (Door) - 0x01BEA
-1696 - Keep Pressure Plates 3 Exit (Door) - 0x01CD5
-1699 - Keep Pressure Plates 4 Exit (Door) - 0x01D40
-1702 - Keep Shadows Shortcut (Door) - 0x09E3D
-1705 - Keep Tower Shortcut (Door) - 0x04F8F
-1708 - Monastery Shortcut (Door) - 0x0364E
-1711 - Monastery Entry Inner (Door) - 0x0C128
-1714 - Monastery Entry Outer (Door) - 0x0C153
-1717 - Monastery Garden Entry (Door) - 0x03750
-1718 - Town Cargo Box Entry (Door) - 0x0A0C9
-1720 - Town Wooden Roof Stairs (Door) - 0x034F5
-1723 - Town Tinted Glass Door - 0x28A61
-1726 - Town Church Entry (Door) - 0x03BB0
-1729 - Town Maze Stairs (Door) - 0x28AA2
-1732 - Town Windmill Entry (Door) - 0x1845B
-1735 - Town RGB House Stairs (Door) - 0x2897B
-1738 - Town Tower Second (Door) - 0x27798
-1741 - Town Tower First (Door) - 0x27799
-1744 - Town Tower Fourth (Door) - 0x2779A
-1747 - Town Tower Third (Door) - 0x2779C
-1750 - Theater Entry (Door) - 0x17F88
-1753 - Theater Exit Left (Door) - 0x0A16D
-1756 - Theater Exit Right (Door) - 0x3CCDF
-1759 - Jungle Bamboo Laser Shortcut (Door) - 0x3873B
-1760 - Jungle Popup Wall (Door) - 0x1475B
-1762 - River Monastery Shortcut (Door) - 0x0CF2A
-1765 - Bunker Entry (Door) - 0x0C2A4
-1768 - Bunker Tinted Glass Door - 0x17C79
-1771 - Bunker UV Room Entry (Door) - 0x0C2A3
-1774 - Bunker Elevator Room Entry (Door) - 0x0A08D
-1777 - Swamp Entry (Door) - 0x00C1C
-1780 - Swamp Between Bridges First Door - 0x184B7
-1783 - Swamp Platform Shortcut Door - 0x38AE6
-1786 - Swamp Cyan Water Pump (Door) - 0x04B7F
-1789 - Swamp Between Bridges Second Door - 0x18507
-1792 - Swamp Red Water Pump (Door) - 0x183F2
-1795 - Swamp Red Underwater Exit (Door) - 0x305D5
-1798 - Swamp Blue Water Pump (Door) - 0x18482
-1801 - Swamp Purple Water Pump (Door) - 0x0A1D6
-1804 - Swamp Laser Shortcut (Door) - 0x2D880
-1807 - Treehouse First (Door) - 0x0C309
-1810 - Treehouse Second (Door) - 0x0C310
-1813 - Treehouse Third (Door) - 0x0A181
-1816 - Treehouse Drawbridge (Door) - 0x0C32D
-1819 - Treehouse Laser House Entry (Door) - 0x0C323
-1822 - Mountain Floor 1 Exit (Door) - 0x09E54
-1825 - Mountain Floor 2 Staircase Near (Door) - 0x09FFB
-1828 - Mountain Floor 2 Exit (Door) - 0x09EDD
-1831 - Mountain Floor 2 Staircase Far (Door) - 0x09E07
-1834 - Mountain Bottom Floor Giant Puzzle Exit (Door) - 0x09F89
-1840 - Mountain Bottom Floor Final Room Entry (Door) - 0x0C141
-1843 - Mountain Bottom Floor Rock (Door) - 0x17F33
-1846 - Caves Entry (Door) - 0x2D77D
-1849 - Caves Pillar Door - 0x019A5
-1855 - Caves Swamp Shortcut (Door) - 0x2D859
-1858 - Challenge Entry (Door) - 0x0A19A
-1861 - Challenge Tunnels Entry (Door) - 0x0348A
-1864 - Tunnels Theater Shortcut (Door) - 0x27739
-1867 - Tunnels Desert Shortcut (Door) - 0x27263
-1870 - Tunnels Town Shortcut (Door) - 0x09E87
-
-1903 - Outside Tutorial Outpost Doors - 0x03BA2,0x0A170,0x04CA3
-1906 - Symmetry Island Doors - 0x17F3E,0x18269
-1909 - Orchard Gates - 0x03313,0x03307
-1912 - Desert Doors - 0x09FEE,0x0C2C3,0x0A24B,0x0C316
-1915 - Quarry Main Entry - 0x09D6F,0x17C07
-1918 - Quarry Stoneworks Shortcuts - 0x17CE8,0x0368A,0x275FF
-1921 - Quarry Boathouse Barriers - 0x17C50,0x3865F
-1924 - Shadows Laser Room Door - 0x194B2,0x19665
-1927 - Shadows Barriers - 0x19865,0x0A2DF,0x1855B,0x19ADE
-1930 - Keep Hedge Maze Doors - 0x01954,0x018CE,0x019D8,0x019B5,0x019E6,0x0199A,0x01A0E
-1933 - Keep Pressure Plates Doors - 0x01BEC,0x01BEA,0x01CD5,0x01D40
-1936 - Keep Shortcuts - 0x09E3D,0x04F8F
-1939 - Monastery Entry - 0x0C128,0x0C153
-1942 - Monastery Shortcuts - 0x0364E,0x03750
-1945 - Town Doors - 0x0A0C9,0x034F5,0x28A61,0x03BB0,0x28AA2,0x1845B,0x2897B
-1948 - Town Tower Doors - 0x27798,0x27799,0x2779A,0x2779C
-1951 - Theater Exit - 0x0A16D,0x3CCDF
-1954 - Jungle & River Shortcuts - 0x3873B,0x0CF2A
-1957 - Bunker Doors - 0x0C2A4,0x17C79,0x0C2A3,0x0A08D
-1960 - Swamp Doors - 0x00C1C,0x184B7,0x38AE6,0x18507
-1963 - Swamp Water Pumps - 0x04B7F,0x183F2,0x305D5,0x18482,0x0A1D6
-1966 - Treehouse Entry Doors - 0x0C309,0x0C310,0x0A181
-1975 - Mountain Floor 2 Stairs & Doors - 0x09FFB,0x09EDD,0x09E07
-1978 - Mountain Bottom Floor Doors to Caves - 0x17F33,0x2D77D
-1981 - Caves Doors to Challenge - 0x019A5,0x0A19A
-1984 - Caves Exits to Main Island - 0x2D859,0x2D73F
-1987 - Tunnels Doors - 0x27739,0x27263,0x09E87
-
-Lasers:
-1500 - Symmetry Laser - 0x00509
-1501 - Desert Laser - 0x012FB,0x01317
-1502 - Quarry Laser - 0x01539
-1503 - Shadows Laser - 0x181B3
-1504 - Keep Laser - 0x014BB
-1505 - Monastery Laser - 0x17C65
-1506 - Town Laser - 0x032F9
-1507 - Jungle Laser - 0x00274
-1508 - Bunker Laser - 0x0C2B2
-1509 - Swamp Laser - 0x00BF6
-1510 - Treehouse Laser - 0x028A4
\ No newline at end of file
diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py
index f05202751fdb..254064098db9 100644
--- a/worlds/witness/__init__.py
+++ b/worlds/witness/__init__.py
@@ -1,21 +1,28 @@
"""
Archipelago init file for The Witness
"""
-from typing import Dict, Optional
-
-from BaseClasses import Region, Location, MultiWorld, Item, Entrance, Tutorial
-from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \
- get_priority_hint_items, make_hints, generate_joke_hints
-from worlds.AutoWorld import World, WebWorld
+import dataclasses
+from logging import error, warning
+from typing import Any, Dict, List, Optional, cast
+
+from BaseClasses import CollectionState, Entrance, Location, Region, Tutorial
+
+from Options import OptionError, PerGameCommonOptions, Toggle
+from worlds.AutoWorld import WebWorld, World
+
+from .data import static_items as static_witness_items
+from .data import static_locations as static_witness_locations
+from .data import static_logic as static_witness_logic
+from .data.item_definition_classes import DoorItemDefinition, ItemData
+from .data.utils import get_audio_logs
+from .hints import CompactItemData, create_all_hints, make_compact_hint_data, make_laser_hints
+from .locations import WitnessPlayerLocations
+from .options import TheWitnessOptions, witness_option_groups
+from .player_items import WitnessItem, WitnessPlayerItems
from .player_logic import WitnessPlayerLogic
-from .static_logic import StaticWitnessLogic
-from .locations import WitnessPlayerLocations, StaticWitnessLocations
-from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems, ItemData
+from .presets import witness_option_presets
+from .regions import WitnessPlayerRegions
from .rules import set_rules
-from .regions import WitnessRegions
-from .Options import is_option_enabled, the_witness_options, get_option_value
-from .utils import get_audio_logs
-from logging import warning, error
class WitnessWebWorld(WebWorld):
@@ -29,6 +36,9 @@ class WitnessWebWorld(WebWorld):
["NewSoupVi", "Jarno"]
)]
+ options_presets = witness_option_presets
+ option_groups = witness_option_groups
+
class WitnessWorld(World):
"""
@@ -38,82 +48,216 @@ class WitnessWorld(World):
"""
game = "The Witness"
topology_present = False
- data_version = 13
-
- StaticWitnessLogic()
- StaticWitnessLocations()
- StaticWitnessItems()
web = WitnessWebWorld()
- option_definitions = the_witness_options
+
+ options_dataclass = TheWitnessOptions
+ options: TheWitnessOptions
item_name_to_id = {
- name: data.ap_code for name, data in StaticWitnessItems.item_data.items()
+ # ITEM_DATA doesn't have any event items in it
+ name: cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
}
- location_name_to_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID
- item_name_groups = StaticWitnessItems.item_groups
+ location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID
+ item_name_groups = static_witness_items.ITEM_GROUPS
+ location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS
- required_client_version = (0, 3, 9)
+ required_client_version = (0, 4, 5)
- def __init__(self, multiworld: "MultiWorld", player: int):
- super().__init__(multiworld, player)
+ player_logic: WitnessPlayerLogic
+ player_locations: WitnessPlayerLocations
+ player_items: WitnessPlayerItems
+ player_regions: WitnessPlayerRegions
- self.player_logic = None
- self.locat = None
- self.items = None
- self.regio = None
+ log_ids_to_hints: Dict[int, CompactItemData]
+ laser_ids_to_hints: Dict[int, CompactItemData]
- self.log_ids_to_hints = None
+ items_placed_early: List[str]
+ own_itempool: List[WitnessItem]
- def _get_slot_data(self):
+ def _get_slot_data(self) -> Dict[str, Any]:
return {
- 'seed': self.multiworld.per_slot_randoms[self.player].randint(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(),
- 'door_hexes_in_the_pool': self.items.get_door_ids_in_pool(),
- 'symbols_not_in_the_game': self.items.get_symbol_ids_not_in_pool(),
- 'disabled_panels': list(self.player_logic.COMPLETELY_DISABLED_CHECKS),
- 'log_ids_to_hints': self.log_ids_to_hints,
- 'progressive_item_lists': self.items.get_progressive_item_ids_in_pool(),
- 'obelisk_side_id_to_EPs': StaticWitnessLogic.OBELISK_SIDE_ID_TO_EP_HEXES,
- 'precompleted_puzzles': [int(h, 16) for h in
- self.player_logic.EXCLUDED_LOCATIONS | self.player_logic.PRECOMPLETED_LOCATIONS],
- 'entity_to_name': StaticWitnessLogic.ENTITY_ID_TO_NAME,
+ "seed": self.random.randrange(0, 1000000),
+ "victory_location": int(self.player_logic.VICTORY_LOCATION, 16),
+ "panelhex_to_id": self.player_locations.CHECK_PANELHEX_TO_ID,
+ "item_id_to_door_hexes": static_witness_items.get_item_to_door_mappings(),
+ "door_hexes_in_the_pool": self.player_items.get_door_ids_in_pool(),
+ "symbols_not_in_the_game": self.player_items.get_symbol_ids_not_in_pool(),
+ "disabled_entities": [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES],
+ "log_ids_to_hints": self.log_ids_to_hints,
+ "laser_ids_to_hints": self.laser_ids_to_hints,
+ "progressive_item_lists": self.player_items.get_progressive_item_ids_in_pool(),
+ "obelisk_side_id_to_EPs": static_witness_logic.OBELISK_SIDE_ID_TO_EP_HEXES,
+ "precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS],
+ "entity_to_name": static_witness_logic.ENTITY_ID_TO_NAME,
}
- def generate_early(self):
- disabled_locations = self.multiworld.exclude_locations[self.player].value
+ def determine_sufficient_progression(self) -> None:
+ """
+ Determine whether there are enough progression items in this world to consider it "interactive".
+ In the case of singleplayer, this just outputs a warning.
+ In the case of multiplayer, the requirements are a bit stricter and an Exception is raised.
+ """
+
+ # A note on Obelisk Keys:
+ # Obelisk Keys are never relevant in singleplayer, because the locations they lock are irrelevant to in-game
+ # progress and irrelevant to all victory conditions. Thus, I consider them "fake progression" for singleplayer.
+ # However, those locations could obviously contain big items needed for other players, so I consider
+ # "Obelisk Keys only" valid for multiworld.
+
+ # A note on Laser Shuffle:
+ # In singleplayer, I don't mind "Ice Rod Hunt" type gameplay, so "laser shuffle only" is valid.
+ # However, I do not want to allow "Ice Rod Hunt" style gameplay in multiworld, so "laser shuffle only" is
+ # not considered interactive enough for multiworld.
+
+ interacts_sufficiently_with_multiworld = (
+ self.options.shuffle_symbols
+ or self.options.shuffle_doors
+ or self.options.obelisk_keys and self.options.shuffle_EPs
+ )
+
+ has_locally_relevant_progression = (
+ self.options.shuffle_symbols
+ or self.options.shuffle_doors
+ or self.options.shuffle_lasers
+ or self.options.shuffle_boat
+ or self.options.early_caves == "add_to_pool" and self.options.victory_condition == "challenge"
+ )
+
+ if not has_locally_relevant_progression and self.multiworld.players == 1:
+ warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression"
+ f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't seem right.")
+ elif not interacts_sufficiently_with_multiworld and self.multiworld.players > 1:
+ raise OptionError(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough"
+ f" progression items that can be placed in other players' worlds. Please turn on Symbol"
+ f" Shuffle, Door Shuffle, or Obelisk Keys.")
+
+ def generate_early(self) -> None:
+ disabled_locations = self.options.exclude_locations.value
self.player_logic = WitnessPlayerLogic(
- self.multiworld, self.player, disabled_locations, self.multiworld.start_inventory[self.player].value
+ self, disabled_locations, self.options.start_inventory.value
+ )
+
+ self.player_locations: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic)
+ self.player_items: WitnessPlayerItems = WitnessPlayerItems(
+ self, self.player_logic, self.player_locations
)
+ self.player_regions: WitnessPlayerRegions = WitnessPlayerRegions(self.player_locations, self)
- self.locat: WitnessPlayerLocations = WitnessPlayerLocations(self.multiworld, self.player, self.player_logic)
- self.items: WitnessPlayerItems = WitnessPlayerItems(self.multiworld, self.player, self.player_logic, self.locat)
- self.regio: WitnessRegions = WitnessRegions(self.locat)
+ self.log_ids_to_hints = {}
- self.log_ids_to_hints = dict()
+ self.determine_sufficient_progression()
- if not (is_option_enabled(self.multiworld, self.player, "shuffle_symbols")
- or get_option_value(self.multiworld, self.player, "shuffle_doors")
- or is_option_enabled(self.multiworld, self.player, "shuffle_lasers")):
- if self.multiworld.players == 1:
- warning("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle, Door"
- " Shuffle or Laser Shuffle if that doesn't seem right.")
- else:
- raise Exception("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle,"
- " Door Shuffle or Laser Shuffle.")
+ if self.options.shuffle_lasers == "local":
+ self.options.local_items.value |= self.item_name_groups["Lasers"]
+
+ def create_regions(self) -> None:
+ self.player_regions.create_regions(self, self.player_logic)
+
+ # Set rules early so extra locations can be created based on the results of exploring collection states
+
+ set_rules(self)
+
+ # Start creating items
+
+ self.items_placed_early = []
+ self.own_itempool = []
+
+ # Add event items and tie them to event locations (e.g. laser activations).
+
+ event_locations = []
+
+ for event_location in self.player_locations.EVENT_LOCATION_TABLE:
+ item_obj = self.create_item(
+ self.player_logic.EVENT_ITEM_PAIRS[event_location]
+ )
+ location_obj = self.get_location(event_location)
+ location_obj.place_locked_item(item_obj)
+ self.own_itempool.append(item_obj)
+
+ event_locations.append(location_obj)
+
+ # Place other locked items
+ dog_puzzle_skip = self.create_item("Puzzle Skip")
+ self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip)
+
+ self.own_itempool.append(dog_puzzle_skip)
+
+ self.items_placed_early.append("Puzzle Skip")
+
+ if self.options.early_symbol_item:
+ # Pick an early item to place on the tutorial gate.
+ early_items = [
+ item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items()
+ ]
+ if early_items:
+ random_early_item = self.random.choice(early_items)
+ if self.options.puzzle_randomization == "sigma_expert":
+ # In Expert, only tag the item as early, rather than forcing it onto the gate.
+ self.multiworld.local_early_items[self.player][random_early_item] = 1
+ else:
+ # Force the item onto the tutorial gate check and remove it from our random pool.
+ gate_item = self.create_item(random_early_item)
+ self.get_location("Tutorial Gate Open").place_locked_item(gate_item)
+ self.own_itempool.append(gate_item)
+ self.items_placed_early.append(random_early_item)
+
+ # There are some really restrictive settings in The Witness.
+ # They are rarely played, but when they are, we add some extra sphere 1 locations.
+ # This is done both to prevent generation failures, but also to make the early game less linear.
+ # Only sweeps for events because having this behavior be random based on Tutorial Gate would be strange.
- def create_regions(self):
- self.regio.create_regions(self.multiworld, self.player, self.player_logic)
+ state = CollectionState(self.multiworld)
+ state.sweep_for_events(locations=event_locations)
- def create_items(self):
+ num_early_locs = sum(1 for loc in self.multiworld.get_reachable_locations(state, self.player) if loc.address)
- # Determine pool size. Note that the dog location is included in the location list, so this needs to be -1.
- pool_size: int = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) - 1
+ # Adjust the needed size for sphere 1 based on how restrictive the settings are in terms of items
+
+ needed_size = 3
+ needed_size += self.options.puzzle_randomization == "sigma_expert"
+ needed_size += self.options.shuffle_symbols
+ needed_size += self.options.shuffle_doors > 0
+
+ # Then, add checks in order until the required amount of sphere 1 checks is met.
+
+ extra_checks = [
+ ("Tutorial First Hallway Room", "Tutorial First Hallway Bend"),
+ ("Tutorial First Hallway", "Tutorial First Hallway Straight"),
+ ("Desert Outside", "Desert Surface 1"),
+ ("Desert Outside", "Desert Surface 2"),
+ ]
+
+ for i in range(num_early_locs, needed_size):
+ if not extra_checks:
+ break
+
+ region, loc = extra_checks.pop(0)
+ self.player_locations.add_location_late(loc)
+ self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})
+
+ player = self.multiworld.get_player_name(self.player)
+
+ warning(f"""Location "{loc}" had to be added to {player}'s world due to insufficient sphere 1 size.""")
+
+ def create_items(self) -> None:
+ # Determine pool size.
+ pool_size = len(self.player_locations.CHECK_LOCATION_TABLE) - len(self.player_locations.EVENT_LOCATION_TABLE)
# Fill mandatory items and remove precollected and/or starting items from the pool.
- item_pool: Dict[str, int] = self.items.get_mandatory_items()
+ item_pool = self.player_items.get_mandatory_items()
+
+ # Remove one copy of each item that was placed early
+ for already_placed in self.items_placed_early:
+ pool_size -= 1
+
+ if already_placed not in item_pool:
+ continue
+
+ if item_pool[already_placed] == 1:
+ item_pool.pop(already_placed)
+ else:
+ item_pool[already_placed] -= 1
for precollected_item_name in [item.name for item in self.multiworld.precollected_items[self.player]]:
if precollected_item_name in item_pool:
@@ -131,159 +275,140 @@ def create_items(self):
self.multiworld.push_precollected(self.create_item(inventory_item_name))
if len(item_pool) > pool_size:
- error_string = "The Witness world has too few locations ({num_loc}) to place its necessary items " \
- "({num_item})."
- error(error_string.format(num_loc=pool_size, num_item=len(item_pool)))
+ error(f"{self.multiworld.get_player_name(self.player)}'s Witness world has too few locations ({pool_size})"
+ f" to place its necessary items ({len(item_pool)}).")
return
remaining_item_slots = pool_size - sum(item_pool.values())
# Add puzzle skips.
- num_puzzle_skips = get_option_value(self.multiworld, self.player, "puzzle_skip_amount")
+ num_puzzle_skips = self.options.puzzle_skip_amount.value
+
if num_puzzle_skips > remaining_item_slots:
- warning(f"The Witness world has insufficient locations to place all requested puzzle skips.")
+ warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations"
+ f" to place all requested puzzle skips.")
num_puzzle_skips = remaining_item_slots
item_pool["Puzzle Skip"] = num_puzzle_skips
remaining_item_slots -= num_puzzle_skips
# Add junk items.
if remaining_item_slots > 0:
- item_pool.update(self.items.get_filler_items(remaining_item_slots))
-
- # Add event items and tie them to event locations (e.g. laser activations).
- for event_location in self.locat.EVENT_LOCATION_TABLE:
- item_obj = self.create_item(
- self.player_logic.EVENT_ITEM_PAIRS[event_location]
- )
- location_obj = self.multiworld.get_location(event_location, self.player)
- location_obj.place_locked_item(item_obj)
-
- # BAD DOG GET BACK HERE WITH THAT PUZZLE SKIP YOU'RE POLLUTING THE ITEM POOL
- self.multiworld.get_location("Town Pet the Dog", self.player)\
- .place_locked_item(self.create_item("Puzzle Skip"))
-
- # Pick an early item to place on the tutorial gate.
- early_items = [item for item in self.items.get_early_items() if item in item_pool]
- if early_items:
- random_early_item = self.multiworld.random.choice(early_items)
- if get_option_value(self.multiworld, self.player, "puzzle_randomization") == 1:
- # In Expert, only tag the item as early, rather than forcing it onto the gate.
- self.multiworld.local_early_items[self.player][random_early_item] = 1
- else:
- # Force the item onto the tutorial gate check and remove it from our random pool.
- self.multiworld.get_location("Tutorial Gate Open", self.player)\
- .place_locked_item(self.create_item(random_early_item))
- if item_pool[random_early_item] == 1:
- item_pool.pop(random_early_item)
- else:
- item_pool[random_early_item] -= 1
+ item_pool.update(self.player_items.get_filler_items(remaining_item_slots))
# Generate the actual items.
for item_name, quantity in sorted(item_pool.items()):
- self.multiworld.itempool += [self.create_item(item_name) for _ in range(0, quantity)]
- if self.items.item_data[item_name].local_only:
- self.multiworld.local_items[self.player].value.add(item_name)
+ new_items = [self.create_item(item_name) for _ in range(0, quantity)]
- def set_rules(self):
- set_rules(self.multiworld, self.player, self.player_logic, self.locat)
+ self.own_itempool += new_items
+ self.multiworld.itempool += new_items
+ if self.player_items.item_data[item_name].local_only:
+ self.options.local_items.value.add(item_name)
- def fill_slot_data(self) -> dict:
- hint_amount = get_option_value(self.multiworld, self.player, "hint_amount")
+ def fill_slot_data(self) -> Dict[str, Any]:
+ self.log_ids_to_hints: Dict[int, CompactItemData] = {}
+ self.laser_ids_to_hints: Dict[int, CompactItemData] = {}
- credits_hint = (
- "This Randomizer is brought to you by",
- "NewSoupVi, Jarno, blastron,",
- "jbzdarkid, sigma144, IHNN, oddGarrett, Exempt-Medic.", -1
- )
+ already_hinted_locations = set()
+
+ # Laser hints
+ if self.options.laser_hints:
+ laser_hints = make_laser_hints(self, sorted(static_witness_items.ITEM_GROUPS["Lasers"]))
+
+ for item_name, hint in laser_hints.items():
+ item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name])
+ self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player)
+ already_hinted_locations.add(cast(Location, hint.location))
+
+ # Audio Log Hints
+
+ hint_amount = self.options.hint_amount.value
audio_logs = get_audio_logs().copy()
- if hint_amount != 0:
- generated_hints = make_hints(self.multiworld, self.player, hint_amount)
+ if hint_amount:
+ area_hints = round(self.options.area_hint_percentage / 100 * hint_amount)
+
+ generated_hints = create_all_hints(self, hint_amount, area_hints, already_hinted_locations)
- self.multiworld.per_slot_randoms[self.player].shuffle(audio_logs)
+ self.random.shuffle(audio_logs)
duplicates = min(3, len(audio_logs) // hint_amount)
- for _ in range(0, hint_amount):
- hint = generated_hints.pop(0)
+ for hint in generated_hints:
+ compact_hint_data = make_compact_hint_data(hint, self.player)
for _ in range(0, duplicates):
audio_log = audio_logs.pop()
- self.log_ids_to_hints[int(audio_log, 16)] = hint
+ self.log_ids_to_hints[int(audio_log, 16)] = compact_hint_data
- if audio_logs:
- audio_log = audio_logs.pop()
- self.log_ids_to_hints[int(audio_log, 16)] = credits_hint
+ # Client will generate joke hints for these.
+ self.log_ids_to_hints.update({int(audio_log, 16): ("", -1, -1) for audio_log in audio_logs})
- joke_hints = generate_joke_hints(self.multiworld, self.player, len(audio_logs))
-
- while audio_logs:
- audio_log = audio_logs.pop()
- self.log_ids_to_hints[int(audio_log, 16)] = joke_hints.pop()
-
- # generate hints done
+ # Options for the client & auto-tracker
slot_data = self._get_slot_data()
- for option_name in the_witness_options:
- slot_data[option_name] = get_option_value(
- self.multiworld, self.player, option_name
- )
+ for option_name in (attr.name for attr in dataclasses.fields(TheWitnessOptions)
+ if attr not in dataclasses.fields(PerGameCommonOptions)):
+ option = getattr(self.options, option_name)
+ slot_data[option_name] = bool(option.value) if isinstance(option, Toggle) else option.value
return slot_data
- def create_item(self, item_name: str) -> Item:
+ def create_item(self, item_name: str) -> WitnessItem:
# If the player's plando options are malformed, the item_name parameter could be a dictionary containing the
# name of the item, rather than the item itself. This is a workaround to prevent a crash.
- if type(item_name) is dict:
- item_name = list(item_name.keys())[0]
+ if isinstance(item_name, dict):
+ item_name = next(iter(item_name))
# this conditional is purely for unit tests, which need to be able to create an item before generate_early
item_data: ItemData
- if hasattr(self, 'items') and self.items and item_name in self.items.item_data:
- item_data = self.items.item_data[item_name]
+ if hasattr(self, "player_items") and self.player_items and item_name in self.player_items.item_data:
+ item_data = self.player_items.item_data[item_name]
else:
- item_data = StaticWitnessItems.item_data[item_name]
+ item_data = static_witness_items.ITEM_DATA[item_name]
return WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player)
+ def get_filler_item_name(self) -> str:
+ return "Speed Boost"
+
class WitnessLocation(Location):
"""
Archipelago Location for The Witness
"""
game: str = "The Witness"
- check_hex: int = -1
+ entity_hex: int = -1
- def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1):
+ def __init__(self, player: int, name: str, address: Optional[int], parent: Region, ch_hex: int = -1) -> None:
super().__init__(player, name, address, parent)
- self.check_hex = ch_hex
+ self.entity_hex = ch_hex
-def create_region(world: MultiWorld, player: int, name: str,
- locat: WitnessPlayerLocations, region_locations=None, exits=None):
+def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlayerLocations,
+ region_locations: Optional[List[str]] = None, exits: Optional[List[str]] = None) -> Region:
"""
Create an Archipelago Region for The Witness
"""
- ret = Region(name, player, world)
+ ret = Region(name, world.player, world.multiworld)
if region_locations:
for location in region_locations:
- loc_id = locat.CHECK_LOCATION_TABLE[location]
+ loc_id = player_locations.CHECK_LOCATION_TABLE[location]
- check_hex = -1
- if location in StaticWitnessLogic.CHECKS_BY_NAME:
- check_hex = int(
- StaticWitnessLogic.CHECKS_BY_NAME[location]["checkHex"], 0
+ entity_hex = -1
+ if location in static_witness_logic.ENTITIES_BY_NAME:
+ entity_hex = int(
+ static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"], 0
)
- location = WitnessLocation(
- player, location, loc_id, ret, check_hex
+ location_obj = WitnessLocation(
+ world.player, location, loc_id, ret, entity_hex
)
- ret.locations.append(location)
+ ret.locations.append(location_obj)
if exits:
for single_exit in exits:
- ret.exits.append(Entrance(player, single_exit, ret))
+ ret.exits.append(Entrance(world.player, single_exit, ret))
return ret
diff --git a/worlds/witness/data/WitnessItems.txt b/worlds/witness/data/WitnessItems.txt
new file mode 100644
index 000000000000..782fa9c3d226
--- /dev/null
+++ b/worlds/witness/data/WitnessItems.txt
@@ -0,0 +1,286 @@
+Symbols:
+0 - Dots
+1 - Colored Dots
+2 - Full Dots
+3 - Invisible Dots
+5 - Sound Dots
+10 - Symmetry
+20 - Triangles
+30 - Eraser
+40 - Shapers
+41 - Rotated Shapers
+50 - Negative Shapers
+60 - Stars
+61 - Stars + Same Colored Symbol
+71 - Black/White Squares
+72 - Colored Squares
+80 - Arrows
+200 - Progressive Dots - Dots,Full Dots
+210 - Progressive Symmetry - Symmetry,Colored Dots
+260 - Progressive Stars - Stars,Stars + Same Colored Symbol
+
+Useful:
+510 - Puzzle Skip
+#511 - Energy Capacity
+
+Filler:
+500 - Speed Boost - 1
+#501 - Energy Fill (Small) - 6
+#502 - Energy Fill - 3
+#503 - Energy Fill (Max) - 1
+
+Traps:
+600 - Slowness - 6
+610 - Power Surge - 2
+615 - Bonk - 1
+
+Jokes:
+650 - Functioning Brain
+
+Doors:
+1100 - Glass Factory Entry (Panel) - 0x01A54
+1101 - Outside Tutorial Outpost Entry (Panel) - 0x0A171
+1102 - Outside Tutorial Outpost Exit (Panel) - 0x04CA4
+1105 - Symmetry Island Lower (Panel) - 0x000B0
+1107 - Symmetry Island Upper (Panel) - 0x1C349
+1108 - Desert Surface 3 Control (Panel) - 0x09FA0
+1109 - Desert Surface 8 Control (Panel) - 0x09F86
+1110 - Desert Light Room Entry (Panel) - 0x0C339
+1111 - Desert Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B
+1112 - Desert Light Control (Panel) - 0x09FAA
+1113 - Desert Flood Room Entry (Panel) - 0x0A249
+1114 - Desert Elevator Room Hexagonal Control (Panel) - 0x0A015
+1115 - Quarry Elevator Control (Panel) - 0x17CC4
+1117 - Quarry Entry 1 (Panel) - 0x09E57
+1118 - Quarry Entry 2 (Panel) - 0x17C09
+1119 - Quarry Stoneworks Entry (Panel) - 0x01E5A,0x01E59
+1120 - Quarry Stoneworks Ramp Controls (Panel) - 0x03678,0x03676
+1122 - Quarry Stoneworks Lift Controls (Panel) - 0x03679,0x03675
+1125 - Quarry Boathouse Ramp Height Control (Panel) - 0x03852
+1127 - Quarry Boathouse Ramp Horizontal Control (Panel) - 0x03858
+1129 - Quarry Boathouse Hook Control (Panel) - 0x275FA
+1131 - Shadows Door Timer (Panel) - 0x334DB,0x334DC
+1140 - Keep Hedge Maze 1 (Panel) - 0x00139
+1142 - Keep Hedge Maze 2 (Panel) - 0x019DC
+1144 - Keep Hedge Maze 3 (Panel) - 0x019E7
+1146 - Keep Hedge Maze 4 (Panel) - 0x01A0F
+1150 - Monastery Entry Left (Panel) - 0x00B10
+1151 - Monastery Entry Right (Panel) - 0x00C92
+1156 - Monastery Shutters Control (Panel) - 0x09D9B
+1162 - Town RGB House Entry (Panel) - 0x28998
+1163 - Town Church Entry (Panel) - 0x28A0D
+1164 - Town RGB Control (Panel) - 0x334D8
+1166 - Town Maze Stairs (Panel) - 0x28A79
+1167 - Town Maze Rooftop Bridge (Panel) - 0x2896A
+1169 - Windmill Entry (Panel) - 0x17F5F
+1172 - Town Cargo Box Entry (Panel) - 0x0A0C8
+1173 - Town Desert Laser Redirect Control (Panel) - 0x09F98
+1182 - Windmill Turn Control (Panel) - 0x17D02
+1184 - Theater Entry (Panel) - 0x17F89
+1185 - Theater Video Input (Panel) - 0x00815
+1189 - Theater Exit (Panel) - 0x0A168,0x33AB2
+1200 - Treehouse First & Second Doors (Panel) - 0x0288C,0x02886
+1202 - Treehouse Third Door (Panel) - 0x0A182
+1205 - Treehouse Laser House Door Timer (Panel) - 0x2700B,0x17CBC
+1208 - Treehouse Drawbridge (Panel) - 0x037FF
+1175 - Jungle Popup Wall (Panel) - 0x17CAB
+1180 - Bunker Entry (Panel) - 0x17C2E
+1183 - Bunker Tinted Glass Door (Panel) - 0x0A099
+1186 - Bunker Elevator Control (Panel) - 0x0A079
+1188 - Bunker Drop-Down Door Controls (Panel) - 0x34BC5,0x34BC6
+1190 - Swamp Entry (Panel) - 0x0056E
+1192 - Swamp Sliding Bridge (Panel) - 0x00609,0x18488
+1194 - Swamp Platform Shortcut (Panel) - 0x17C0D
+1195 - Swamp Rotating Bridge (Panel) - 0x181F5
+1196 - Swamp Long Bridge (Panel) - 0x17E2B
+1197 - Swamp Maze Controls (Panel) - 0x17C0A,0x17E07
+1220 - Mountain Floor 1 Light Bridge (Panel) - 0x09E39
+1225 - Mountain Floor 2 Light Bridge Near (Panel) - 0x09E86
+1230 - Mountain Floor 2 Light Bridge Far (Panel) - 0x09ED8
+1235 - Mountain Floor 2 Elevator Control (Panel) - 0x09EEB
+1240 - Caves Entry (Panel) - 0x00FF8
+1242 - Caves Elevator Controls (Panel) - 0x335AB,0x335AC,0x3369D
+1245 - Challenge Entry (Panel) - 0x0A16E
+1250 - Tunnels Entry (Panel) - 0x039B4
+1255 - Tunnels Town Shortcut (Panel) - 0x09E85
+
+
+1310 - Boat - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054
+
+1400 - Caves Mountain Shortcut (Door) - 0x2D73F
+
+1600 - Outside Tutorial Outpost Path (Door) - 0x03BA2
+1603 - Outside Tutorial Outpost Entry (Door) - 0x0A170
+1606 - Outside Tutorial Outpost Exit (Door) - 0x04CA3
+1609 - Glass Factory Entry (Door) - 0x01A29
+1612 - Glass Factory Back Wall (Door) - 0x0D7ED
+1615 - Symmetry Island Lower (Door) - 0x17F3E
+1618 - Symmetry Island Upper (Door) - 0x18269
+1619 - Orchard First Gate (Door) - 0x03307
+1620 - Orchard Second Gate (Door) - 0x03313
+1621 - Desert Light Room Entry (Door) - 0x09FEE
+1624 - Desert Pond Room Entry (Door) - 0x0C2C3
+1627 - Desert Flood Room Entry (Door) - 0x0A24B
+1630 - Desert Elevator Room Entry (Door) - 0x0C316
+1631 - Desert Elevator (Door) - 0x01317
+1633 - Quarry Entry 1 (Door) - 0x09D6F
+1636 - Quarry Entry 2 (Door) - 0x17C07
+1639 - Quarry Stoneworks Entry (Door) - 0x02010
+1642 - Quarry Stoneworks Side Exit (Door) - 0x275FF
+1645 - Quarry Stoneworks Roof Exit (Door) - 0x17CE8
+1648 - Quarry Stoneworks Stairs (Door) - 0x0368A
+1651 - Quarry Boathouse Dock (Door) - 0x2769B,0x27163
+1653 - Quarry Boathouse First Barrier (Door) - 0x17C50
+1654 - Quarry Boathouse Second Barrier (Door) - 0x3865F
+1656 - Shadows Timed Door - 0x19B24
+1657 - Shadows Laser Entry Right (Door) - 0x194B2
+1660 - Shadows Laser Entry Left (Door) - 0x19665
+1663 - Shadows Quarry Barrier (Door) - 0x19865,0x0A2DF
+1666 - Shadows Ledge Barrier (Door) - 0x1855B,0x19ADE
+1669 - Keep Hedge Maze 1 Exit (Door) - 0x01954
+1672 - Keep Pressure Plates 1 Exit (Door) - 0x01BEC
+1675 - Keep Hedge Maze 2 Shortcut (Door) - 0x018CE
+1678 - Keep Hedge Maze 2 Exit (Door) - 0x019D8
+1681 - Keep Hedge Maze 3 Shortcut (Door) - 0x019B5
+1684 - Keep Hedge Maze 3 Exit (Door) - 0x019E6
+1687 - Keep Hedge Maze 4 Shortcut (Door) - 0x0199A
+1690 - Keep Hedge Maze 4 Exit (Door) - 0x01A0E
+1693 - Keep Pressure Plates 2 Exit (Door) - 0x01BEA
+1696 - Keep Pressure Plates 3 Exit (Door) - 0x01CD5
+1699 - Keep Pressure Plates 4 Exit (Door) - 0x01D40
+1702 - Keep Shadows Shortcut (Door) - 0x09E3D
+1705 - Keep Tower Shortcut (Door) - 0x04F8F
+1708 - Monastery Laser Shortcut (Door) - 0x0364E
+1711 - Monastery Entry Inner (Door) - 0x0C128
+1714 - Monastery Entry Outer (Door) - 0x0C153
+1717 - Monastery Garden Entry (Door) - 0x03750
+1718 - Town Cargo Box Entry (Door) - 0x0A0C9
+1720 - Town Wooden Roof Stairs (Door) - 0x034F5
+1723 - Town RGB House Entry (Door) - 0x28A61
+1726 - Town Church Entry (Door) - 0x03BB0
+1729 - Town Maze Stairs (Door) - 0x28AA2
+1732 - Windmill Entry (Door) - 0x1845B
+1735 - Town RGB House Stairs (Door) - 0x2897B
+1738 - Town Tower Second (Door) - 0x27798
+1741 - Town Tower First (Door) - 0x27799
+1744 - Town Tower Fourth (Door) - 0x2779A
+1747 - Town Tower Third (Door) - 0x2779C
+1750 - Theater Entry (Door) - 0x17F88
+1753 - Theater Exit Left (Door) - 0x0A16D
+1756 - Theater Exit Right (Door) - 0x3CCDF
+1759 - Jungle Laser Shortcut (Door) - 0x3873B
+1760 - Jungle Popup Wall (Door) - 0x1475B
+1762 - Jungle Monastery Garden Shortcut (Door) - 0x0CF2A
+1765 - Bunker Entry (Door) - 0x0C2A4
+1768 - Bunker Tinted Glass Door - 0x17C79
+1771 - Bunker UV Room Entry (Door) - 0x0C2A3
+1774 - Bunker Elevator Room Entry (Door) - 0x0A08D
+1777 - Swamp Entry (Door) - 0x00C1C
+1780 - Swamp Between Bridges First Door - 0x184B7
+1783 - Swamp Platform Shortcut (Door) - 0x38AE6
+1786 - Swamp Cyan Water Pump (Door) - 0x04B7F
+1789 - Swamp Between Bridges Second Door - 0x18507
+1792 - Swamp Red Water Pump (Door) - 0x183F2
+1795 - Swamp Red Underwater Exit (Door) - 0x305D5
+1798 - Swamp Blue Water Pump (Door) - 0x18482
+1801 - Swamp Purple Water Pump (Door) - 0x0A1D6
+1804 - Swamp Laser Shortcut (Door) - 0x2D880
+1807 - Treehouse First (Door) - 0x0C309
+1810 - Treehouse Second (Door) - 0x0C310
+1813 - Treehouse Third (Door) - 0x0A181
+1816 - Treehouse Drawbridge (Door) - 0x0C32D
+1819 - Treehouse Laser House Entry (Door) - 0x0C323
+1822 - Mountain Floor 1 Exit (Door) - 0x09E54
+1825 - Mountain Floor 2 Staircase Near (Door) - 0x09FFB
+1828 - Mountain Floor 2 Exit (Door) - 0x09EDD
+1831 - Mountain Floor 2 Staircase Far (Door) - 0x09E07
+1834 - Mountain Bottom Floor Giant Puzzle Exit (Door) - 0x09F89
+1840 - Mountain Bottom Floor Pillars Room Entry (Door) - 0x0C141
+1843 - Mountain Bottom Floor Rock (Door) - 0x17F33
+1846 - Caves Entry (Door) - 0x2D77D
+1849 - Caves Pillar Door - 0x019A5
+1855 - Caves Swamp Shortcut (Door) - 0x2D859
+1858 - Challenge Entry (Door) - 0x0A19A
+1861 - Tunnels Entry (Door) - 0x0348A
+1864 - Tunnels Theater Shortcut (Door) - 0x27739
+1867 - Tunnels Desert Shortcut (Door) - 0x27263
+1870 - Tunnels Town Shortcut (Door) - 0x09E87
+
+1903 - Outside Tutorial Outpost Doors - 0x03BA2,0x0A170,0x04CA3
+1904 - Glass Factory Doors - 0x0D7ED,0x01A29
+1906 - Symmetry Island Doors - 0x17F3E,0x18269
+1909 - Orchard Gates - 0x03313,0x03307
+1912 - Desert Doors & Elevator - 0x09FEE,0x0C2C3,0x0A24B,0x0C316,0x01317
+1915 - Quarry Entry Doors - 0x09D6F,0x17C07
+1918 - Quarry Stoneworks Doors - 0x02010,0x275FF,0x17CE8,0x0368A
+1921 - Quarry Boathouse Doors - 0x17C50,0x3865F,0x2769B,0x27163
+1924 - Shadows Laser Room Doors - 0x194B2,0x19665
+1927 - Shadows Lower Doors - 0x19865,0x0A2DF,0x1855B,0x19ADE,0x19B24
+1930 - Keep Hedge Maze Doors - 0x01954,0x018CE,0x019D8,0x019B5,0x019E6,0x0199A,0x01A0E
+1933 - Keep Pressure Plates Doors - 0x01BEC,0x01BEA,0x01CD5,0x01D40
+1936 - Keep Shortcuts - 0x09E3D,0x04F8F
+1939 - Monastery Entry Doors - 0x0C128,0x0C153
+1942 - Monastery Shortcuts - 0x0364E,0x03750,0x0CF2A
+1945 - Town Doors - 0x0A0C9,0x034F5,0x28A61,0x03BB0,0x28AA2,0x2897B
+1948 - Town Tower Doors - 0x27798,0x27799,0x2779A,0x2779C
+1951 - Windmill & Theater Doors - 0x0A16D,0x3CCDF,0x1845B,0x17F88
+1954 - Jungle Doors - 0x3873B,0x1475B
+1957 - Bunker Doors - 0x0C2A4,0x17C79,0x0C2A3,0x0A08D
+1960 - Swamp Doors - 0x00C1C,0x184B7,0x18507
+1961 - Swamp Shortcuts - 0x38AE6,0x2D880
+1963 - Swamp Water Pumps - 0x04B7F,0x183F2,0x305D5,0x18482,0x0A1D6
+1966 - Treehouse Entry Doors - 0x0C309,0x0C310,0x0A181
+1969 - Treehouse Upper Doors - 0x0C323,0x0C32D
+1975 - Mountain Floor 1 & 2 Doors - 0x09E54,0x09FFB,0x09EDD,0x09E07
+1978 - Mountain Bottom Floor Doors - 0x0C141,0x17F33,0x09F89
+1981 - Caves Doors - 0x019A5,0x0A19A,0x2D77D
+1984 - Caves Shortcuts - 0x2D859,0x2D73F
+1987 - Tunnels Doors - 0x27739,0x27263,0x09E87,0x0348A
+
+2000 - Desert Control Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B,0x0A015,0x09FA0,0x09F86
+2005 - Quarry Stoneworks Control Panels - 0x03678,0x03676,0x03679,0x03675
+2010 - Quarry Boathouse Control Panels - 0x03852,0x03858,0x275FA
+2015 - Town Control Panels - 0x2896A,0x334D8,0x09F98
+2020 - Windmill & Theater Control Panels - 0x17D02,0x00815
+2025 - Bunker Control Panels - 0x34BC5,0x34BC6,0x0A079
+2030 - Swamp Control Panels - 0x00609,0x18488,0x181F5,0x17E2B,0x17C0A,0x17E07
+2035 - Mountain & Caves Control Panels - 0x09ED8,0x09E86,0x09E39,0x09EEB,0x335AB,0x335AC,0x3369D
+
+2100 - Symmetry Island Panels - 0x1C349,0x000B0
+2101 - Outside Tutorial Outpost Panels - 0x0A171,0x04CA4
+2105 - Desert Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B,0x0C339,0x0A249,0x0A015,0x09FA0,0x09F86
+2110 - Quarry Outside Panels - 0x17C09,0x09E57,0x17CC4
+2115 - Quarry Stoneworks Panels - 0x01E5A,0x01E59,0x03678,0x03676,0x03679,0x03675
+2120 - Quarry Boathouse Panels - 0x03852,0x03858,0x275FA
+2122 - Keep Hedge Maze Panels - 0x00139,0x019DC,0x019E7,0x01A0F
+2125 - Monastery Panels - 0x09D9B,0x00C92,0x00B10
+2130 - Town Church & RGB House Panels - 0x28998,0x28A0D,0x334D8
+2135 - Town Maze Panels - 0x2896A,0x28A79
+2137 - Town Dockside House Panels - 0x0A0C8,0x09F98
+2140 - Windmill & Theater Panels - 0x17D02,0x00815,0x17F5F,0x17F89,0x0A168,0x33AB2
+2145 - Treehouse Panels - 0x0A182,0x0288C,0x02886,0x2700B,0x17CBC,0x037FF
+2150 - Bunker Panels - 0x34BC5,0x34BC6,0x0A079,0x0A099,0x17C2E
+2155 - Swamp Panels - 0x00609,0x18488,0x181F5,0x17E2B,0x17C0A,0x17E07,0x17C0D,0x0056E
+2160 - Mountain Panels - 0x09ED8,0x09E86,0x09E39,0x09EEB
+2165 - Caves Panels - 0x3369D,0x00FF8,0x0A16E,0x335AB,0x335AC
+2170 - Tunnels Panels - 0x09E85,0x039B4
+
+2200 - Desert Obelisk Key - 0x0332B,0x03367,0x28B8A,0x037B6,0x037B2,0x000F7,0x3351D,0x0053C,0x00771,0x335C8,0x335C9,0x337F8,0x037BB,0x220E4,0x220E5,0x334B9,0x334BC,0x22106,0x0A14C,0x0A14D,0x00359
+2201 - Monastery Obelisk Key - 0x03ABC,0x03ABE,0x03AC0,0x03AC4,0x03AC5,0x03BE2,0x03BE3,0x0A409,0x006E5,0x006E6,0x006E7,0x034A7,0x034AD,0x034AF,0x03DAB,0x03DAC,0x03DAD,0x03E01,0x289F4,0x289F5,0x00263
+2202 - Treehouse Obelisk Key - 0x0053D,0x0053E,0x00769,0x33721,0x220A7,0x220BD,0x03B22,0x03B23,0x03B24,0x03B25,0x03A79,0x28ABD,0x28ABE,0x3388F,0x28B29,0x28B2A,0x018B6,0x033BE,0x033BF,0x033DD,0x033E5,0x28AE9,0x3348F,0x00097
+2203 - Mountainside Obelisk Key - 0x001A3,0x335AE,0x000D3,0x035F5,0x09D5D,0x09D5E,0x09D63,0x3370E,0x035DE,0x03601,0x03603,0x03D0D,0x3369A,0x336C8,0x33505,0x03A9E,0x016B2,0x3365F,0x03731,0x036CE,0x03C07,0x03A93,0x03AA6,0x3397C,0x0105D,0x0A304,0x035CB,0x035CF,0x00367
+2204 - Quarry Obelisk Key - 0x28A7B,0x005F6,0x00859,0x17CB9,0x28A4A,0x334B6,0x00614,0x0069D,0x28A4C,0x289CF,0x289D1,0x33692,0x03E77,0x03E7C,0x22073
+2205 - Town Obelisk Key - 0x035C7,0x01848,0x03D06,0x33530,0x33600,0x28A2F,0x28A37,0x334A3,0x3352F,0x33857,0x33879,0x03C19,0x28B30,0x035C9,0x03335,0x03412,0x038A6,0x038AA,0x03E3F,0x03E40,0x28B8E,0x28B91,0x03BCE,0x03BCF,0x03BD1,0x339B6,0x33A20,0x33A29,0x33A2A,0x33B06,0x0A16C
+
+Lasers:
+1500 - Symmetry Laser - 0x00509
+1501 - Desert Laser - 0x012FB
+1502 - Quarry Laser - 0x01539
+1503 - Shadows Laser - 0x181B3
+1504 - Keep Laser - 0x014BB
+1505 - Monastery Laser - 0x17C65
+1506 - Town Laser - 0x032F9
+1507 - Jungle Laser - 0x00274
+1508 - Bunker Laser - 0x0C2B2
+1509 - Swamp Laser - 0x00BF6
+1510 - Treehouse Laser - 0x028A4
\ No newline at end of file
diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/data/WitnessLogic.txt
similarity index 80%
rename from worlds/witness/WitnessLogic.txt
rename to worlds/witness/data/WitnessLogic.txt
index dffdc1a701d0..b7814626ada0 100644
--- a/worlds/witness/WitnessLogic.txt
+++ b/worlds/witness/data/WitnessLogic.txt
@@ -1,10 +1,14 @@
+==Tutorial (Inside)==
+
+Menu (Menu) - Entry - True:
+
Entry (Entry):
-First Hallway (First Hallway) - Entry - True - First Hallway Room - 0x00064:
+Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064:
158000 - 0x00064 (Straight) - True - True
159510 - 0x01848 (EP) - 0x00064 - True
-First Hallway Room (First Hallway) - Tutorial - 0x00182:
+Tutorial First Hallway Room (Tutorial First Hallway) - Tutorial - 0x00182:
158001 - 0x00182 (Bend) - True - True
Tutorial (Tutorial) - Outside Tutorial - 0x03629:
@@ -21,9 +25,11 @@ Tutorial (Tutorial) - Outside Tutorial - 0x03629:
159513 - 0x33600 (Patio Flowers EP) - 0x0C373 - True
159517 - 0x3352F (Gate EP) - 0x03505 - True
-Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2:
-158650 - 0x033D4 (Vault) - True - Dots & Black/White Squares
-158651 - 0x03481 (Vault Box) - 0x033D4 - True
+==Tutorial (Outside)==
+
+Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2 - Outside Tutorial Vault - 0x033D0:
+158650 - 0x033D4 (Vault Panel) - True - Dots & Black/White Squares
+Door - 0x033D0 (Vault Door) - 0x033D4
158013 - 0x0005D (Shed Row 1) - True - Dots
158014 - 0x0005E (Shed Row 2) - 0x0005D - Dots
158015 - 0x0005F (Shed Row 3) - 0x0005E - Dots
@@ -44,6 +50,9 @@ Door - 0x03BA2 (Outpost Path) - 0x0A3B5
159516 - 0x334A3 (Path EP) - True - True
159500 - 0x035C7 (Tractor EP) - True - True
+Outside Tutorial Vault (Outside Tutorial):
+158651 - 0x03481 (Vault Box) - True - True
+
Outside Tutorial Path To Outpost (Outside Tutorial) - Outside Tutorial Outpost - 0x0A170:
158011 - 0x0A171 (Outpost Entry Panel) - True - Dots & Full Dots
Door - 0x0A170 (Outpost Entry) - 0x0A171
@@ -53,8 +62,23 @@ Outside Tutorial Outpost (Outside Tutorial) - Outside Tutorial - 0x04CA3:
Door - 0x04CA3 (Outpost Exit) - 0x04CA4
158600 - 0x17CFB (Discard) - True - Triangles
+Orchard (Orchard) - Main Island - True - Orchard Beyond First Gate - 0x03307:
+158071 - 0x00143 (Apple Tree 1) - True - True
+158072 - 0x0003B (Apple Tree 2) - 0x00143 - True
+158073 - 0x00055 (Apple Tree 3) - 0x0003B - True
+Door - 0x03307 (First Gate) - 0x00055
+
+Orchard Beyond First Gate (Orchard) - Orchard End - 0x03313:
+158074 - 0x032F7 (Apple Tree 4) - 0x00055 - True
+158075 - 0x032FF (Apple Tree 5) - 0x032F7 - True
+Door - 0x03313 (Second Gate) - 0x032FF
+
+Orchard End (Orchard):
+
Main Island (Main Island) - Outside Tutorial - True:
-159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True
+159801 - 0xFFD00 (Reached Independently) - True - True
+
+==Glass Factory==
Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29:
158027 - 0x01A54 (Entry Panel) - True - Symmetry
@@ -76,9 +100,11 @@ Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0
158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry
Door - 0x0D7ED (Back Wall) - 0x0005C
-Inside Glass Factory Behind Back Wall (Glass Factory) - Boat - 0x17CC8:
+Inside Glass Factory Behind Back Wall (Glass Factory) - The Ocean - 0x17CC8:
158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat
+==Symmetry Island==
+
Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E:
158040 - 0x000B0 (Lower Panel) - 0x0343A - Dots
Door - 0x17F3E (Lower) - 0x000B0
@@ -112,32 +138,29 @@ Door - 0x18269 (Upper) - 0x1C349
159000 - 0x0332B (Glass Factory Black Line Reflection EP) - True - True
Symmetry Island Upper (Symmetry Island):
-158065 - 0x00A52 (Yellow 1) - True - Symmetry & Colored Dots
-158066 - 0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots
-158067 - 0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots
-158068 - 0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots
-158069 - 0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots
-158070 - 0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots
+158065 - 0x00A52 (Laser Yellow 1) - True - Symmetry & Colored Dots
+158066 - 0x00A57 (Laser Yellow 2) - 0x00A52 - Symmetry & Colored Dots
+158067 - 0x00A5B (Laser Yellow 3) - 0x00A57 - Symmetry & Colored Dots
+158068 - 0x00A61 (Laser Blue 1) - 0x00A52 - Symmetry & Colored Dots
+158069 - 0x00A64 (Laser Blue 2) - 0x00A61 & 0x00A57 - Symmetry & Colored Dots
+158070 - 0x00A68 (Laser Blue 3) - 0x00A64 & 0x00A5B - Symmetry & Colored Dots
158700 - 0x0360D (Laser Panel) - 0x00A68 - True
Laser - 0x00509 (Laser) - 0x0360D
159001 - 0x03367 (Glass Factory Black Line EP) - True - True
-Orchard (Orchard) - Main Island - True - Orchard Beyond First Gate - 0x03307:
-158071 - 0x00143 (Apple Tree 1) - True - True
-158072 - 0x0003B (Apple Tree 2) - 0x00143 - True
-158073 - 0x00055 (Apple Tree 3) - 0x0003B - True
-Door - 0x03307 (First Gate) - 0x00055
-
-Orchard Beyond First Gate (Orchard) - Orchard End - 0x03313:
-158074 - 0x032F7 (Apple Tree 4) - 0x00055 - True
-158075 - 0x032FF (Apple Tree 5) - 0x032F7 - True
-Door - 0x03313 (Second Gate) - 0x032FF
+==Desert==
-Orchard End (Orchard):
+Desert Obelisk (Desert) - Entry - True:
+159700 - 0xFFE00 (Obelisk Side 1) - 0x0332B & 0x03367 & 0x28B8A - True
+159701 - 0xFFE01 (Obelisk Side 2) - 0x037B6 & 0x037B2 & 0x000F7 - True
+159702 - 0xFFE02 (Obelisk Side 3) - 0x3351D - True
+159703 - 0xFFE03 (Obelisk Side 4) - 0x0053C & 0x00771 & 0x335C8 & 0x335C9 & 0x337F8 & 0x037BB & 0x220E4 & 0x220E5 - True
+159704 - 0xFFE04 (Obelisk Side 5) - 0x334B9 & 0x334BC & 0x22106 & 0x0A14C & 0x0A14D - True
+159709 - 0x00359 (Obelisk) - True - True
-Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE:
-158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots
-158653 - 0x0339E (Vault Box) - 0x0CC7B - True
+Desert Outside (Desert) - Main Island - True - Desert Light Room - 0x09FEE - Desert Vault - 0x03444:
+158652 - 0x0CC7B (Vault Panel) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots
+Door - 0x03444 (Vault Door) - 0x0CC7B
158602 - 0x17CE7 (Discard) - True - Triangles
158076 - 0x00698 (Surface 1) - True - True
158077 - 0x0048F (Surface 2) - 0x00698 - True
@@ -163,14 +186,17 @@ Laser - 0x012FB (Laser) - 0x03608
159040 - 0x334B9 (Shore EP) - True - True
159041 - 0x334BC (Island EP) - True - True
-Desert Floodlight Room (Desert) - Desert Pond Room - 0x0C2C3:
+Desert Vault (Desert):
+158653 - 0x0339E (Vault Box) - True - True
+
+Desert Light Room (Desert) - Desert Pond Room - 0x0C2C3:
158087 - 0x09FAA (Light Control) - True - True
158088 - 0x00422 (Light Room 1) - 0x09FAA - True
158089 - 0x006E3 (Light Room 2) - 0x09FAA - True
158090 - 0x0A02D (Light Room 3) - 0x09FAA & 0x00422 & 0x006E3 - True
Door - 0x0C2C3 (Pond Room Entry) - 0x0A02D
-Desert Pond Room (Desert) - Desert Water Levels Room - 0x0A24B:
+Desert Pond Room (Desert) - Desert Flood Room - 0x0A24B:
158091 - 0x00C72 (Pond Room 1) - True - True
158092 - 0x0129D (Pond Room 2) - 0x00C72 - True
158093 - 0x008BB (Pond Room 3) - 0x0129D - True
@@ -181,7 +207,7 @@ Door - 0x0A24B (Flood Room Entry) - 0x0A249
159043 - 0x0A14C (Pond Room Near Reflection EP) - True - True
159044 - 0x0A14D (Pond Room Far Reflection EP) - True - True
-Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316:
+Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316:
158097 - 0x1C2DF (Reduce Water Level Far Left) - True - True
158098 - 0x1831E (Reduce Water Level Far Right) - True - True
158099 - 0x1C260 (Reduce Water Level Near Left) - True - True
@@ -199,18 +225,29 @@ Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316:
Door - 0x0C316 (Elevator Room Entry) - 0x18076
159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True
-Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB:
-158111 - 0x17C31 (Final Transparent) - True - True
-158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True
-158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True
-158115 - 0x0A15C (Final Bent 1) - True - True
-158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True
-158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True
-159035 - 0x037BB (Elevator EP) - 0x012FB - True
-
-Desert Lowest Level Inbetween Shortcuts (Desert):
-
-Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - TrueOneWay:
+Desert Elevator Room (Desert) - Desert Behind Elevator - 0x01317:
+158111 - 0x17C31 (Elevator Room Transparent) - True - True
+158113 - 0x012D7 (Elevator Room Hexagonal) - 0x17C31 & 0x0A015 - True
+158114 - 0x0A015 (Elevator Room Hexagonal Control) - 0x17C31 - True
+158115 - 0x0A15C (Elevator Room Bent 1) - True - True
+158116 - 0x09FFF (Elevator Room Bent 2) - 0x0A15C - True
+158117 - 0x0A15F (Elevator Room Bent 3) - 0x09FFF - True
+159035 - 0x037BB (Elevator EP) - 0x01317 - True
+Door - 0x01317 (Elevator) - 0x03608
+
+Desert Behind Elevator (Desert):
+
+==Quarry==
+
+Quarry Obelisk (Quarry) - Entry - True:
+159740 - 0xFFE40 (Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True
+159741 - 0xFFE41 (Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True
+159742 - 0xFFE42 (Obelisk Side 3) - 0x289CF & 0x289D1 - True
+159743 - 0xFFE43 (Obelisk Side 4) - 0x33692 - True
+159744 - 0xFFE44 (Obelisk Side 5) - 0x03E77 & 0x03E7C - True
+159749 - 0x22073 (Obelisk) - True - True
+
+Outside Quarry (Quarry) - Main Island - True - Quarry Between Entry Doors - 0x09D6F - Quarry Elevator - 0xFFD00 & 0xFFD01:
158118 - 0x09E57 (Entry 1 Panel) - True - Black/White Squares
158603 - 0x17CF0 (Discard) - True - Triangles
158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Shapers
@@ -222,36 +259,39 @@ Door - 0x09D6F (Entry 1) - 0x09E57
159420 - 0x289CF (Rock Line EP) - True - True
159421 - 0x289D1 (Rock Line Reflection EP) - True - True
-Quarry Elevator (Quarry):
+Quarry Elevator (Quarry) - Outside Quarry - 0x17CC4 - Quarry - 0x17CC4:
158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser
159403 - 0x17CB9 (Railroad EP) - 0x17CC4 - True
-Quarry Between Entrys (Quarry) - Quarry - 0x17C07:
+Quarry Between Entry Doors (Quarry) - Quarry - 0x17C07:
158119 - 0x17C09 (Entry 2 Panel) - True - Shapers
Door - 0x17C07 (Entry 2) - 0x17C09
-Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010 - Quarry Elevator - 0x17CC4:
+Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010:
+159802 - 0xFFD01 (Inside Reached Independently) - True - True
158121 - 0x01E5A (Stoneworks Entry Left Panel) - True - Black/White Squares
158122 - 0x01E59 (Stoneworks Entry Right Panel) - True - Dots
Door - 0x02010 (Stoneworks Entry) - 0x01E59 & 0x01E5A
-Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8:
+Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8 - Quarry Stoneworks Lift - TrueOneWay:
158123 - 0x275ED (Side Exit Panel) - True - True
Door - 0x275FF (Side Exit) - 0x275ED
158124 - 0x03678 (Lower Ramp Control) - True - Dots & Eraser
158145 - 0x17CAC (Roof Exit Panel) - True - True
Door - 0x17CE8 (Roof Exit) - 0x17CAC
-Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Ground Floor - 0x03675 - Quarry Stoneworks Upper Floor - 0x03679:
+Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - TrueOneWay:
158125 - 0x00E0C (Lower Row 1) - True - Dots & Eraser
158126 - 0x01489 (Lower Row 2) - 0x00E0C - Dots & Eraser
158127 - 0x0148A (Lower Row 3) - 0x01489 - Dots & Eraser
158128 - 0x014D9 (Lower Row 4) - 0x0148A - Dots & Eraser
158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Dots & Eraser
158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Dots & Eraser
+
+Quarry Stoneworks Lift (Quarry Stoneworks) - Quarry Stoneworks Middle Floor - 0x03679 - Quarry Stoneworks Ground Floor - 0x03679 - Quarry Stoneworks Upper Floor - 0x03679:
158131 - 0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser
-Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Floor - 0x03676 & 0x03679 - Quarry Stoneworks Ground Floor - 0x0368A:
+Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - 0x03675 - Quarry Stoneworks Ground Floor - 0x0368A:
158132 - 0x03676 (Upper Ramp Control) - True - Dots & Eraser
158133 - 0x03675 (Upper Lift Control) - True - Dots & Eraser
158134 - 0x00557 (Upper Row 1) - True - Colored Squares & Eraser
@@ -262,7 +302,7 @@ Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Flo
158139 - 0x3C12D (Upper Row 6) - 0x0146C - Colored Squares & Eraser
158140 - 0x03686 (Upper Row 7) - 0x3C12D - Colored Squares & Eraser
158141 - 0x014E9 (Upper Row 8) - 0x03686 - Colored Squares & Eraser
-158142 - 0x03677 (Stair Control) - True - Colored Squares & Eraser
+158142 - 0x03677 (Stairs Panel) - True - Colored Squares & Eraser
Door - 0x0368A (Stairs) - 0x03677
158143 - 0x3C125 (Control Room Left) - 0x014E9 - Black/White Squares & Dots & Eraser
158144 - 0x0367C (Control Room Right) - 0x014E9 - Colored Squares & Dots & Eraser
@@ -277,7 +317,7 @@ Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Fro
Door - 0x2769B (Dock) - 0x17CA6
Door - 0x27163 (Dock Invis Barrier) - 0x17CA6
-Quarry Boathouse Behind Staircase (Quarry Boathouse) - Boat - 0x17CA6:
+Quarry Boathouse Behind Staircase (Quarry Boathouse) - The Ocean - 0x17CA6:
Quarry Boathouse Upper Front (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x17C50:
158149 - 0x021B3 (Front Row 1) - True - Shapers & Eraser
@@ -309,9 +349,11 @@ Door - 0x3865F (Second Barrier) - 0x38663
158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Stars & Eraser & Shapers
159401 - 0x005F6 (Hook EP) - 0x275FA & 0x03852 & 0x3865F - True
-Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 & 0x19665:
+==Shadows==
+
+Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 | 0x19665:
158170 - 0x334DB (Door Timer Outside) - True - True
-Door - 0x19B24 (Timed Door) - 0x334DB
+Door - 0x19B24 (Timed Door) - 0x334DB | 0x334DC
158171 - 0x0AC74 (Intro 6) - 0x0A8DC - True
158172 - 0x0AC7A (Intro 7) - 0x0AC74 - True
158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - True
@@ -336,7 +378,7 @@ Shadows Ledge (Shadows) - Shadows - 0x1855B - Quarry - 0x19865 & 0x0A2DF:
158187 - 0x334DC (Door Timer Inside) - True - True
158188 - 0x198B5 (Intro 1) - True - True
158189 - 0x198BD (Intro 2) - 0x198B5 - True
-158190 - 0x198BF (Intro 3) - 0x198BD & 0x334DC & 0x19B24 - True
+158190 - 0x198BF (Intro 3) - 0x198BD & 0x19B24 - True
Door - 0x19865 (Quarry Barrier) - 0x198BF
Door - 0x0A2DF (Quarry Barrier 2) - 0x198BF
158191 - 0x19771 (Intro 4) - 0x198BF - True
@@ -345,22 +387,21 @@ Door - 0x1855B (Ledge Barrier) - 0x0A8DC
Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC
Shadows Laser Room (Shadows):
-158703 - 0x19650 (Laser Panel) - True - True
+158703 - 0x19650 (Laser Panel) - 0x194B2 & 0x19665 - True
Laser - 0x181B3 (Laser) - 0x19650
-Treehouse Beach (Treehouse Beach) - Main Island - True:
-159200 - 0x0053D (Rock Shadow EP) - True - True
-159201 - 0x0053E (Sand Shadow EP) - True - True
-159212 - 0x220BD (Both Orange Bridges EP) - 0x17DA2 & 0x17DDB - True
+==Keep==
-Keep (Keep) - Main Island - True - Keep 2nd Maze - 0x01954 - Keep 2nd Pressure Plate - 0x01BEC:
+Outside Keep (Keep) - Main Island - True:
+159430 - 0x03E77 (Red Flowers EP) - True - True
+159431 - 0x03E7C (Purple Flowers EP) - True - True
+
+Keep (Keep) - Outside Keep - True - Keep 2nd Maze - 0x01954 - Keep 2nd Pressure Plate - 0x01BEC:
158193 - 0x00139 (Hedge Maze 1) - True - True
158197 - 0x0A3A8 (Reset Pressure Plates 1) - True - True
158198 - 0x033EA (Pressure Plates 1) - 0x0A3A8 - Dots
Door - 0x01954 (Hedge Maze 1 Exit) - 0x00139
Door - 0x01BEC (Pressure Plates 1 Exit) - 0x033EA
-159430 - 0x03E77 (Red Flowers EP) - True - True
-159431 - 0x03E7C (Purple Flowers EP) - True - True
Keep 2nd Maze (Keep) - Keep - 0x018CE - Keep 3rd Maze - 0x019D8:
Door - 0x018CE (Hedge Maze 2 Shortcut) - 0x00139
@@ -395,18 +436,6 @@ Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F
158205 - 0x09E49 (Shadows Shortcut Panel) - True - True
Door - 0x09E3D (Shadows Shortcut) - 0x09E49
-Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True:
-158654 - 0x00AFB (Vault) - True - Symmetry & Sound Dots & Colored Dots
-158655 - 0x03535 (Vault Box) - 0x00AFB - True
-158605 - 0x17D28 (Discard) - True - Triangles
-159220 - 0x03B22 (Circle Far EP) - True - True
-159221 - 0x03B23 (Circle Left EP) - True - True
-159222 - 0x03B24 (Circle Near EP) - True - True
-159224 - 0x03A79 (Stern EP) - True - True
-159225 - 0x28ABD (Rope Inner EP) - True - True
-159226 - 0x28ABE (Rope Outer EP) - True - True
-159230 - 0x3388F (Couch EP) - 0x17CDF | 0x0A054 - True
-
Keep Tower (Keep) - Keep - 0x04F8F:
158206 - 0x0361B (Tower Shortcut Panel) - True - True
Door - 0x04F8F (Tower Shortcut) - 0x0361B
@@ -421,11 +450,39 @@ Laser - 0x014BB (Laser) - 0x0360E | 0x03317
159250 - 0x28AE9 (Path EP) - True - True
159251 - 0x3348F (Hedges EP) - True - True
+==Shipwreck==
+
+Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True - Shipwreck Vault - 0x17BB4:
+158654 - 0x00AFB (Vault Panel) - True - Symmetry & Sound Dots & Colored Dots
+Door - 0x17BB4 (Vault Door) - 0x00AFB
+158605 - 0x17D28 (Discard) - True - Triangles
+159220 - 0x03B22 (Circle Far EP) - True - True
+159221 - 0x03B23 (Circle Left EP) - True - True
+159222 - 0x03B24 (Circle Near EP) - True - True
+159224 - 0x03A79 (Stern EP) - True - True
+159225 - 0x28ABD (Rope Inner EP) - True - True
+159226 - 0x28ABE (Rope Outer EP) - True - True
+159230 - 0x3388F (Couch EP) - 0x17CDF | 0x0A054 - True
+
+Shipwreck Vault (Shipwreck):
+158655 - 0x03535 (Vault Box) - True - True
+
+==Monastery==
+
+Monastery Obelisk (Monastery) - Entry - True:
+159710 - 0xFFE10 (Obelisk Side 1) - 0x03ABC & 0x03ABE & 0x03AC0 & 0x03AC4 - True
+159711 - 0xFFE11 (Obelisk Side 2) - 0x03AC5 - True
+159712 - 0xFFE12 (Obelisk Side 3) - 0x03BE2 & 0x03BE3 & 0x0A409 - True
+159713 - 0xFFE13 (Obelisk Side 4) - 0x006E5 & 0x006E6 & 0x006E7 & 0x034A7 & 0x034AD & 0x034AF & 0x03DAB & 0x03DAC & 0x03DAD - True
+159714 - 0xFFE14 (Obelisk Side 5) - 0x03E01 - True
+159715 - 0xFFE15 (Obelisk Side 6) - 0x289F4 & 0x289F5 - True
+159719 - 0x00263 (Obelisk) - True - True
+
Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750:
-158207 - 0x03713 (Shortcut Panel) - True - True
-Door - 0x0364E (Shortcut) - 0x03713
+158207 - 0x03713 (Laser Shortcut Panel) - True - True
+Door - 0x0364E (Laser Shortcut) - 0x03713
158208 - 0x00B10 (Entry Left) - True - True
-158209 - 0x00C92 (Entry Right) - True - True
+158209 - 0x00C92 (Entry Right) - 0x00B10 - True
Door - 0x0C128 (Entry Inner) - 0x00B10
Door - 0x0C153 (Entry Outer) - 0x00C92
158210 - 0x00290 (Outside 1) - 0x09D9B - True
@@ -441,6 +498,9 @@ Laser - 0x17C65 (Laser) - 0x17CA4
159137 - 0x03DAC (Facade Left Stairs EP) - True - True
159138 - 0x03DAD (Facade Right Stairs EP) - True - True
159140 - 0x03E01 (Grass Stairs EP) - True - True
+159120 - 0x03BE2 (Garden Left EP) - 0x03750 - True
+159121 - 0x03BE3 (Garden Right EP) - True - True
+159122 - 0x0A409 (Wall EP) - True - True
Inside Monastery (Monastery):
158213 - 0x09D9B (Shutters Control) - True - Dots
@@ -454,11 +514,22 @@ Inside Monastery (Monastery):
Monastery Garden (Monastery):
-Town (Town) - Main Island - True - Boat - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9:
+==Town==
+
+Town Obelisk (Town) - Entry - True:
+159750 - 0xFFE50 (Obelisk Side 1) - 0x035C7 - True
+159751 - 0xFFE51 (Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True
+159752 - 0xFFE52 (Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True
+159753 - 0xFFE53 (Obelisk Side 4) - 0x28B30 & 0x035C9 - True
+159754 - 0xFFE54 (Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True
+159755 - 0xFFE55 (Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True
+159759 - 0x0A16C (Obelisk) - True - True
+
+Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - Town RGB House - 0x28A61 - Town Inside Cargo Box - 0x0A0C9 - Outside Windmill - True:
158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat
158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Black/White Squares & Shapers
Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8
-158707 - 0x09F98 (Desert Laser Redirect) - True - True
+158707 - 0x09F98 (Desert Laser Redirect Control) - True - True
158220 - 0x18590 (Transparent) - True - Symmetry
158221 - 0x28AE3 (Vines) - 0x18590 - True
158222 - 0x28938 (Apple Tree) - 0x28AE3 - True
@@ -469,17 +540,12 @@ Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8
158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Rotated Shapers & Dots & Full Dots
158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Rotated Shapers & Dots & Full Dots
Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1
-158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers
-Door - 0x28A61 (Tinted Glass Door) - 0x28998
+158225 - 0x28998 (RGB House Entry Panel) - True - Stars & Rotated Shapers
+Door - 0x28A61 (RGB House Entry) - 0x28998
158226 - 0x28A0D (Church Entry Panel) - 0x28A61 - Stars
Door - 0x03BB0 (Church Entry) - 0x28A0D
-158228 - 0x28A79 (Maze Stair Control) - True - True
+158228 - 0x28A79 (Maze Panel) - True - True
Door - 0x28AA2 (Maze Stairs) - 0x28A79
-158241 - 0x17F5F (Windmill Entry Panel) - True - Dots
-Door - 0x1845B (Windmill Entry) - 0x17F5F
-159010 - 0x037B6 (Windmill First Blade EP) - 0x17D02 - True
-159011 - 0x037B2 (Windmill Second Blade EP) - 0x17D02 - True
-159012 - 0x000F7 (Windmill Third Blade EP) - 0x17D02 - True
159540 - 0x03335 (Tower Underside Third EP) - True - True
159541 - 0x03412 (Tower Underside Fourth EP) - True - True
159542 - 0x038A6 (Tower Underside First EP) - True - True
@@ -512,20 +578,26 @@ Town Church (Town):
158227 - 0x28A69 (Church Lattice) - 0x03BB0 - True
159553 - 0x03BD1 (Black Line Church EP) - True - True
-RGB House (Town) - RGB Room - 0x2897B:
+Town RGB House (Town RGB House) - Town RGB House Upstairs - 0x2897B:
158242 - 0x034E4 (Sound Room Left) - True - True
158243 - 0x034E3 (Sound Room Right) - True - Sound Dots
-Door - 0x2897B (RGB House Stairs) - 0x034E4 & 0x034E3
+Door - 0x2897B (Stairs) - 0x034E4 & 0x034E3
-RGB Room (Town):
+Town RGB House Upstairs (Town RGB House Upstairs):
158244 - 0x334D8 (RGB Control) - True - Rotated Shapers & Colored Squares
-158245 - 0x03C0C (RGB Room Left) - 0x334D8 - Colored Squares & Black/White Squares
-158246 - 0x03C08 (RGB Room Right) - 0x334D8 - Stars
+158245 - 0x03C0C (Left) - 0x334D8 - Colored Squares & Black/White Squares
+158246 - 0x03C08 (Right) - 0x334D8 - Stars
+
+Town Tower Bottom (Town Tower) - Town - True - Town Tower After First Door - 0x27799:
+Door - 0x27799 (First Door) - 0x28A69
-Town Tower (Town Tower) - Town - True - Town Tower Top - 0x27798 & 0x27799 & 0x2779A & 0x2779C:
+Town Tower After First Door (Town Tower) - Town Tower After Second Door - 0x27798:
Door - 0x27798 (Second Door) - 0x28ACC
+
+Town Tower After Second Door (Town Tower) - Town Tower After Third Door - 0x2779C:
Door - 0x2779C (Third Door) - 0x28AD9
-Door - 0x27799 (First Door) - 0x28A69
+
+Town Tower After Third Door (Town Tower) - Town Tower Top - 0x2779A:
Door - 0x2779A (Fourth Door) - 0x28B39
Town Tower Top (Town):
@@ -534,6 +606,15 @@ Laser - 0x032F9 (Laser) - 0x032F5
159422 - 0x33692 (Brown Bridge EP) - True - True
159551 - 0x03BCE (Black Line Tower EP) - True - True
+==Windmill & Theater==
+
+Outside Windmill (Windmill) - Windmill Interior - 0x1845B:
+159010 - 0x037B6 (First Blade EP) - 0x17D02 - True
+159011 - 0x037B2 (Second Blade EP) - 0x17D02 - True
+159012 - 0x000F7 (Third Blade EP) - 0x17D02 - True
+158241 - 0x17F5F (Entry Panel) - True - Dots
+Door - 0x1845B (Entry) - 0x17F5F
+
Windmill Interior (Windmill) - Theater - 0x17F88:
158247 - 0x17D02 (Turn Control) - True - Dots
158248 - 0x17F89 (Theater Entry Panel) - True - Black/White Squares
@@ -557,7 +638,9 @@ Door - 0x3CCDF (Exit Right) - 0x33AB2
159556 - 0x33A2A (Door EP) - 0x03553 - True
159558 - 0x33B06 (Church EP) - 0x0354E - True
-Jungle (Jungle) - Main Island - True - Outside Jungle River - 0x3873B - Boat - 0x17CDF:
+==Jungle==
+
+Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF:
158251 - 0x17CDF (Shore Boat Spawn) - True - Boat
158609 - 0x17F9B (Discard) - True - Triangles
158252 - 0x002C4 (First Row 1) - True - True
@@ -588,18 +671,20 @@ Door - 0x3873B (Laser Shortcut) - 0x337FA
159350 - 0x035CB (Bamboo CCW EP) - True - True
159351 - 0x035CF (Bamboo CW EP) - True - True
-Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A:
-158267 - 0x17CAA (Monastery Shortcut Panel) - True - True
-Door - 0x0CF2A (Monastery Shortcut) - 0x17CAA
-158663 - 0x15ADD (Vault) - True - Black/White Squares & Dots
-158664 - 0x03702 (Vault Box) - 0x15ADD - True
+Outside Jungle River (Jungle) - Main Island - True - Monastery Garden - 0x0CF2A - Jungle Vault - 0x15287:
+158267 - 0x17CAA (Monastery Garden Shortcut Panel) - True - True
+Door - 0x0CF2A (Monastery Garden Shortcut) - 0x17CAA
+158663 - 0x15ADD (Vault Panel) - True - Black/White Squares & Dots
+Door - 0x15287 (Vault Door) - 0x15ADD
159110 - 0x03AC5 (Green Leaf Moss EP) - True - True
-159120 - 0x03BE2 (Monastery Garden Left EP) - 0x03750 - True
-159121 - 0x03BE3 (Monastery Garden Right EP) - True - True
-159122 - 0x0A409 (Monastery Wall EP) - True - True
+
+Jungle Vault (Jungle):
+158664 - 0x03702 (Vault Box) - True - True
+
+==Bunker==
Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4:
-158268 - 0x17C2E (Entry Panel) - True - Black/White Squares & Colored Squares
+158268 - 0x17C2E (Entry Panel) - True - Black/White Squares
Door - 0x0C2A4 (Entry) - 0x17C2E
Bunker (Bunker) - Bunker Glass Room - 0x17C79:
@@ -616,9 +701,9 @@ Bunker (Bunker) - Bunker Glass Room - 0x17C79:
Door - 0x17C79 (Tinted Glass Door) - 0x0A099
Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3:
-158279 - 0x0A010 (Glass Room 1) - True - Colored Squares
-158280 - 0x0A01B (Glass Room 2) - 0x0A010 - Colored Squares & Black/White Squares
-158281 - 0x0A01F (Glass Room 3) - 0x0A01B - Colored Squares & Black/White Squares
+158279 - 0x0A010 (Glass Room 1) - 0x17C79 - Colored Squares
+158280 - 0x0A01B (Glass Room 2) - 0x17C79 & 0x0A010 - Colored Squares & Black/White Squares
+158281 - 0x0A01F (Glass Room 3) - 0x17C79 & 0x0A01B - Colored Squares & Black/White Squares
Door - 0x0C2A3 (UV Room Entry) - 0x0A01F
Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D:
@@ -631,9 +716,11 @@ Door - 0x0A08D (Elevator Room Entry) - 0x17E67
Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay:
159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True
-Bunker Elevator (Bunker) - Bunker Laser Platform - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079:
+Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Cyan Room - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079:
158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares
+Bunker Cyan Room (Bunker) - Bunker Elevator - TrueOneWay:
+
Bunker Green Room (Bunker) - Bunker Elevator - TrueOneWay:
159310 - 0x000D3 (Green Room Flowers EP) - True - True
@@ -641,6 +728,8 @@ Bunker Laser Platform (Bunker) - Bunker Elevator - TrueOneWay:
158710 - 0x09DE0 (Laser Panel) - True - True
Laser - 0x0C2B2 (Laser) - 0x09DE0
+==Swamp==
+
Outside Swamp (Swamp) - Swamp Entry Area - 0x00C1C - Main Island - True:
158287 - 0x0056E (Entry Panel) - True - Shapers
Door - 0x00C1C (Entry) - 0x0056E
@@ -676,8 +765,8 @@ Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat
158316 - 0x00990 (Platform Row 4) - 0x0098F - Shapers
Door - 0x184B7 (Between Bridges First Door) - 0x00990
158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Shapers
-158318 - 0x17C0E (Platform Shortcut Right Panel) - True - Shapers
-Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E
+158318 - 0x17C0E (Platform Shortcut Right Panel) - 0x17C0D - Shapers
+Door - 0x38AE6 (Platform Shortcut) - 0x17C0E
Door - 0x04B7F (Cyan Water Pump) - 0x00006
Swamp Cyan Underwater (Swamp):
@@ -715,18 +804,21 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near
159331 - 0x016B2 (Rotating Bridge CCW EP) - 0x181F5 - True
159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True
-Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482:
+Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8:
+159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True
158328 - 0x09DB8 (Boat Spawn) - True - Boat
158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers
158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers
158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Rotated Shapers
158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers
-158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers
Door - 0x18482 (Blue Water Pump) - 0x00E3A
159332 - 0x3365F (Boat EP) - 0x09DB8 - True
159333 - 0x03731 (Long Bridge Side EP) - 0x17E2B - True
-Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6:
+Swamp Long Bridge (Swamp) - Swamp Near Boat - 0x17E2B - Outside Swamp - 0x17E2B:
+158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers
+
+Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6 - Swamp Near Boat - TrueOneWay:
Door - 0x0A1D6 (Purple Water Pump) - 0x00E3A
Swamp Purple Underwater (Swamp):
@@ -752,13 +844,29 @@ Laser - 0x00BF6 (Laser) - 0x03615
158342 - 0x17C02 (Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Rotated Shapers
Door - 0x2D880 (Laser Shortcut) - 0x17C02
-Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309:
+==Treehouse==
+
+Treehouse Obelisk (Treehouse) - Entry - True:
+159720 - 0xFFE20 (Obelisk Side 1) - 0x0053D & 0x0053E & 0x00769 - True
+159721 - 0xFFE21 (Obelisk Side 2) - 0x33721 & 0x220A7 & 0x220BD - True
+159722 - 0xFFE22 (Obelisk Side 3) - 0x03B22 & 0x03B23 & 0x03B24 & 0x03B25 & 0x03A79 & 0x28ABD & 0x28ABE - True
+159723 - 0xFFE23 (Obelisk Side 4) - 0x3388F & 0x28B29 & 0x28B2A - True
+159724 - 0xFFE24 (Obelisk Side 5) - 0x018B6 & 0x033BE & 0x033BF & 0x033DD & 0x033E5 - True
+159725 - 0xFFE25 (Obelisk Side 6) - 0x28AE9 & 0x3348F - True
+159729 - 0x00097 (Obelisk) - True - True
+
+Treehouse Beach (Treehouse Beach) - Main Island - True:
+159200 - 0x0053D (Rock Shadow EP) - True - True
+159201 - 0x0053E (Sand Shadow EP) - True - True
+159212 - 0x220BD (Both Orange Bridges EP) - 0x17DA2 & 0x17DDB - True
+
+Treehouse Entry Area (Treehouse) - Treehouse Between Entry Doors - 0x0C309 - The Ocean - 0x17C95:
158343 - 0x17C95 (Boat Spawn) - True - Boat
158344 - 0x0288C (First Door Panel) - True - Stars
Door - 0x0C309 (First Door) - 0x0288C
159210 - 0x33721 (Buoy EP) - 0x17C95 - True
-Treehouse Between Doors (Treehouse) - Treehouse Yellow Bridge - 0x0C310:
+Treehouse Between Entry Doors (Treehouse) - Treehouse Yellow Bridge - 0x0C310:
158345 - 0x02886 (Second Door Panel) - True - Stars
Door - 0x0C310 (Second Door) - 0x02886
@@ -778,7 +886,7 @@ Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181:
Door - 0x0A181 (Third Door) - 0x0A182
Treehouse Junction (Treehouse) - Treehouse Right Orange Bridge - True - Treehouse First Purple Bridge - True - Treehouse Green Bridge - True:
-158356 - 0x2700B (Laser House Door Timer Outside Control) - True - True
+158356 - 0x2700B (Laser House Door Timer Outside) - True - True
Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x17D6C:
158357 - 0x17DC8 (First Purple Bridge 1) - True - Stars & Dots
@@ -787,7 +895,7 @@ Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x1
158360 - 0x17D2D (First Purple Bridge 4) - 0x17CE4 - Stars & Dots
158361 - 0x17D6C (First Purple Bridge 5) - 0x17D2D - Stars & Dots
-Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2:
+Treehouse Right Orange Bridge (Treehouse) - Treehouse Drawbridge Platform - 0x17DA2:
158391 - 0x17D88 (Right Orange Bridge 1) - True - Stars
158392 - 0x17DB4 (Right Orange Bridge 2) - 0x17D88 - Stars
158393 - 0x17D8C (Right Orange Bridge 3) - 0x17DB4 - Stars
@@ -801,8 +909,8 @@ Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2:
158401 - 0x17DB1 (Right Orange Bridge 11) - 0x17DB7 - Stars
158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars
-Treehouse Bridge Platform (Treehouse) - Main Island - 0x0C32D:
-158404 - 0x037FF (Bridge Control) - True - Stars
+Treehouse Drawbridge Platform (Treehouse) - Main Island - 0x0C32D:
+158404 - 0x037FF (Drawbridge Panel) - True - Stars
Door - 0x0C32D (Drawbridge) - 0x037FF
Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6:
@@ -847,7 +955,7 @@ Treehouse Green Bridge Left House (Treehouse):
159211 - 0x220A7 (Right Orange Bridge EP) - 0x17DA2 - True
Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323:
-Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DDB
+Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DDB | 0x17CBC
Treehouse Laser Room Back Platform (Treehouse):
158611 - 0x17FA0 (Laser Discard) - True - Triangles
@@ -860,28 +968,45 @@ Treehouse Laser Room (Treehouse):
158403 - 0x17CBC (Laser House Door Timer Inside) - True - True
Laser - 0x028A4 (Laser) - 0x03613
-Mountainside (Mountainside) - Main Island - True - Mountaintop - True:
+==Mountain (Outside)==
+
+Mountainside Obelisk (Mountainside) - Entry - True:
+159730 - 0xFFE30 (Obelisk Side 1) - 0x001A3 & 0x335AE - True
+159731 - 0xFFE31 (Obelisk Side 2) - 0x000D3 & 0x035F5 & 0x09D5D & 0x09D5E & 0x09D63 - True
+159732 - 0xFFE32 (Obelisk Side 3) - 0x3370E & 0x035DE & 0x03601 & 0x03603 & 0x03D0D & 0x3369A & 0x336C8 & 0x33505 - True
+159733 - 0xFFE33 (Obelisk Side 4) - 0x03A9E & 0x016B2 & 0x3365F & 0x03731 & 0x036CE & 0x03C07 & 0x03A93 - True
+159734 - 0xFFE34 (Obelisk Side 5) - 0x03AA6 & 0x3397C & 0x0105D & 0x0A304 - True
+159735 - 0xFFE35 (Obelisk Side 6) - 0x035CB & 0x035CF - True
+159739 - 0x00367 (Obelisk) - True - True
+
+Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
+159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True
158612 - 0x17C42 (Discard) - True - Triangles
-158665 - 0x002A6 (Vault) - True - Symmetry & Colored Dots & Black/White Squares & Dots
-158666 - 0x03542 (Vault Box) - 0x002A6 - True
+158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares & Dots
+Door - 0x00085 (Vault Door) - 0x002A6
159301 - 0x335AE (Cloud Cycle EP) - True - True
159325 - 0x33505 (Bush EP) - True - True
159335 - 0x03C07 (Apparent River EP) - True - True
-Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34:
+Mountainside Vault (Mountainside):
+158666 - 0x03542 (Vault Box) - True - True
+
+Mountaintop (Mountaintop) - Mountain Floor 1 - 0x17C34:
158405 - 0x0042D (River Shape) - True - True
-158406 - 0x09F7F (Box Short) - 7 Lasers - True
-158407 - 0x17C34 (Trap Door Triple Exit) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol
-158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True
+158406 - 0x09F7F (Box Short) - 7 Lasers + Redirect - True
+158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol
+158800 - 0xFFF00 (Box Long) - 11 Lasers + Redirect & 0x17C34 - True
159300 - 0x001A3 (River Shape EP) - True - True
159320 - 0x3370E (Arch Black EP) - True - True
159324 - 0x336C8 (Arch White Right EP) - True - True
159326 - 0x3369A (Arch White Left EP) - True - True
-Mountain Top Layer (Mountain Floor 1) - Mountain Top Layer Bridge - 0x09E39:
+==Mountain (Inside)==
+
+Mountain Floor 1 (Mountain Floor 1) - Mountain Floor 1 Bridge - 0x09E39:
158408 - 0x09E39 (Light Bridge Controller) - True - Black/White Squares & Colored Squares & Eraser
-Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54:
+Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneWay:
158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots
158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Dots
158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers & Dots
@@ -899,25 +1024,25 @@ Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54:
158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Dots
158424 - 0x09EAD (Trash Pillar 1) - True - Black/White Squares & Shapers
158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Black/White Squares & Shapers
+
+Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54:
Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B
-Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Blue Bridge - 0x09E86 - Mountain Pink Bridge EP - TrueOneWay:
+Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 Above The Abyss - True - Mountain Pink Bridge EP - TrueOneWay:
158426 - 0x09FD3 (Near Row 1) - True - Stars & Colored Squares & Stars + Same Colored Symbol
158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Stars & Colored Squares & Stars + Same Colored Symbol
158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Colored Squares & Stars + Same Colored Symbol
158429 - 0x09FD7 (Near Row 4) - 0x09FD6 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers
-158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares & Symmetry & Colored Dots
+158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Symmetry & Colored Dots
Door - 0x09FFB (Staircase Near) - 0x09FD8
-Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - 0x09ED8:
-
-Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD:
+Mountain Floor 2 Above The Abyss (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD & 0x09ED8 & 0x09E86:
Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86
Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2):
158431 - 0x09E86 (Light Bridge Controller Near) - True - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser
-Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay:
+Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay - Mountain Floor 2 - 0x09ED8:
158432 - 0x09FCC (Far Row 1) - True - Dots
158433 - 0x09FCE (Far Row 2) - 0x09FCC - Black/White Squares
158434 - 0x09FCF (Far Row 3) - 0x09FCE - Stars
@@ -932,37 +1057,56 @@ Mountain Floor 2 Light Bridge Room Far (Mountain Floor 2):
Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - TrueOneWay:
158613 - 0x17F93 (Elevator Discard) - True - Triangles
-Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Third Layer - 0x09EEB:
+Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Floor 3 - 0x09EEB:
158439 - 0x09EEB (Elevator Control Panel) - True - Dots
-Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89:
+Mountain Floor 3 (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89 - Mountain Pink Bridge EP - TrueOneWay:
158440 - 0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Eraser
158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser
158442 - 0x09F01 (Giant Puzzle Top Right) - True - Rotated Shapers
158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser
158444 - 0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry
+159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True
+159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True
Door - 0x09F89 (Exit) - 0x09FDA
-Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x17FA2 - Final Room - 0x0C141 - Mountain Pink Bridge EP - TrueOneWay:
+Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Path to Caves - 0x17F33 - Mountain Bottom Floor Pillars Room - 0x0C141:
158614 - 0x17FA2 (Discard) - 0xFFF00 - Triangles
-158445 - 0x01983 (Final Room Entry Left) - True - Shapers & Stars
-158446 - 0x01987 (Final Room Entry Right) - True - Colored Squares & Dots
-Door - 0x0C141 (Final Room Entry) - 0x01983 & 0x01987
-159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True
-159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True
+158445 - 0x01983 (Pillars Room Entry Left) - True - Shapers & Stars
+158446 - 0x01987 (Pillars Room Entry Right) - True - Colored Squares & Dots
+Door - 0x0C141 (Pillars Room Entry) - 0x01983 & 0x01987
+Door - 0x17F33 (Rock Open) - 0x17FA2 | 0x334E1
+
+Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB & 0x33961:
+158522 - 0x0383A (Right Pillar 1) - True - Stars
+158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Stars & Dots
+158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Full Dots
+158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Dots & Symmetry
+158526 - 0x0383D (Left Pillar 1) - True - Dots
+158527 - 0x0383F (Left Pillar 2) - 0x0383D - Black/White Squares
+158528 - 0x03859 (Left Pillar 3) - 0x0383F - Shapers
+158529 - 0x339BB (Left Pillar 4) - 0x03859 - Black/White Squares & Stars & Symmetry
+
+Elevator (Mountain Bottom Floor):
+158530 - 0x3D9A6 (Elevator Door Close Left) - True - True
+158531 - 0x3D9A7 (Elevator Door Close Right) - True - True
+158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True
+158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True
+158534 - 0x3D9AA (Back Wall Left) - 0x3D9A6 | 0x3D9A7 - True
+158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True
+158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True
Mountain Pink Bridge EP (Mountain Floor 2):
159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True
-Mountain Bottom Floor Rock (Mountain Bottom Floor) - Mountain Bottom Floor - 0x17F33 - Mountain Path to Caves - 0x17F33:
-Door - 0x17F33 (Rock Open) - True - True
-
-Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x334E1 - Caves - 0x2D77D:
+Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D:
158447 - 0x00FF8 (Caves Entry Panel) - True - Triangles & Black/White Squares
Door - 0x2D77D (Caves Entry) - 0x00FF8
158448 - 0x334E1 (Rock Control) - True - True
-Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Path to Challenge - 0x019A5:
+==Caves==
+
+Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Caves Path to Challenge - 0x019A5:
158451 - 0x335AB (Elevator Inside Control) - True - Dots & Black/White Squares
158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Black/White Squares
158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Black/White Squares & Dots
@@ -1017,11 +1161,13 @@ Door - 0x2D73F (Mountain Shortcut Door) - 0x021D7
Door - 0x2D859 (Swamp Shortcut Door) - 0x17CF2
159341 - 0x3397C (Skylight EP) - True - True
-Path to Challenge (Caves) - Challenge - 0x0A19A:
+Caves Path to Challenge (Caves) - Challenge - 0x0A19A:
158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Shapers & Stars + Same Colored Symbol
Door - 0x0A19A (Challenge Entry) - 0x0A16E
-Challenge (Challenge) - Tunnels - 0x0348A:
+==Challenge==
+
+Challenge (Challenge) - Tunnels - 0x0348A - Challenge Vault - 0x04D75:
158499 - 0x0A332 (Start Timer) - 11 Lasers - True
158500 - 0x0088E (Small Basic) - 0x0A332 - True
158501 - 0x00BAF (Big Basic) - 0x0088E - True
@@ -1041,12 +1187,17 @@ Challenge (Challenge) - Tunnels - 0x0348A:
158515 - 0x034EC (Maze Hidden 2) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles
158516 - 0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry
158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Black/White Squares & Symmetry
-158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True
+Door - 0x04D75 (Vault Door) - 0x1C31A & 0x1C319
158518 - 0x039B4 (Tunnels Entry Panel) - True - Triangles
Door - 0x0348A (Tunnels Entry) - 0x039B4
159530 - 0x28B30 (Water EP) - True - True
-Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Lowest Level Inbetween Shortcuts - 0x27263 - Town - 0x09E87:
+Challenge Vault (Challenge):
+158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True
+
+==Tunnels==
+
+Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Behind Elevator - 0x27263 - Town - 0x09E87:
158668 - 0x2FAF6 (Vault Box) - True - True
158519 - 0x27732 (Theater Shortcut Panel) - True - True
Door - 0x27739 (Theater Shortcut) - 0x27732
@@ -1056,26 +1207,9 @@ Door - 0x27263 (Desert Shortcut) - 0x2773D
Door - 0x09E87 (Town Shortcut) - 0x09E85
159557 - 0x33A20 (Theater Flowers EP) - 0x03553 & Theater to Tunnels - True
-Final Room (Mountain Final Room) - Elevator - 0x339BB & 0x33961:
-158522 - 0x0383A (Right Pillar 1) - True - Stars
-158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Stars & Dots
-158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Full Dots
-158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Dots & Symmetry
-158526 - 0x0383D (Left Pillar 1) - True - Dots
-158527 - 0x0383F (Left Pillar 2) - 0x0383D - Black/White Squares
-158528 - 0x03859 (Left Pillar 3) - 0x0383F - Shapers
-158529 - 0x339BB (Left Pillar 4) - 0x03859 - Black/White Squares & Stars & Symmetry
+==Boat==
-Elevator (Mountain Final Room):
-158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True
-158531 - 0x3D9A7 (Elevator Door Close Right) - True - True
-158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True
-158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True
-158534 - 0x3D9AA (Back Wall Left) - 0x3D9A6 | 0x3D9A7 - True
-158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True
-158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True
-
-Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay:
+The Ocean (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay:
159042 - 0x22106 (Desert EP) - True - True
159223 - 0x03B25 (Shipwreck CCW Underside EP) - True - True
159231 - 0x28B29 (Shipwreck Green EP) - True - True
@@ -1086,41 +1220,3 @@ Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehous
159521 - 0x33879 (Tutorial Reflection EP) - True - True
159522 - 0x03C19 (Tutorial Moss EP) - True - True
159531 - 0x035C9 (Cargo Box EP) - 0x0A0C9 - True
-
-Obelisks (EPs) - Entry - True:
-159700 - 0xFFE00 (Desert Obelisk Side 1) - 0x0332B & 0x03367 & 0x28B8A - True
-159701 - 0xFFE01 (Desert Obelisk Side 2) - 0x037B6 & 0x037B2 & 0x000F7 - True
-159702 - 0xFFE02 (Desert Obelisk Side 3) - 0x3351D - True
-159703 - 0xFFE03 (Desert Obelisk Side 4) - 0x0053C & 0x00771 & 0x335C8 & 0x335C9 & 0x337F8 & 0x037BB & 0x220E4 & 0x220E5 - True
-159704 - 0xFFE04 (Desert Obelisk Side 5) - 0x334B9 & 0x334BC & 0x22106 & 0x0A14C & 0x0A14D - True
-159710 - 0xFFE10 (Monastery Obelisk Side 1) - 0x03ABC & 0x03ABE & 0x03AC0 & 0x03AC4 - True
-159711 - 0xFFE11 (Monastery Obelisk Side 2) - 0x03AC5 - True
-159712 - 0xFFE12 (Monastery Obelisk Side 3) - 0x03BE2 & 0x03BE3 & 0x0A409 - True
-159713 - 0xFFE13 (Monastery Obelisk Side 4) - 0x006E5 & 0x006E6 & 0x006E7 & 0x034A7 & 0x034AD & 0x034AF & 0x03DAB & 0x03DAC & 0x03DAD - True
-159714 - 0xFFE14 (Monastery Obelisk Side 5) - 0x03E01 - True
-159715 - 0xFFE15 (Monastery Obelisk Side 6) - 0x289F4 & 0x289F5 - True
-159720 - 0xFFE20 (Treehouse Obelisk Side 1) - 0x0053D & 0x0053E & 0x00769 - True
-159721 - 0xFFE21 (Treehouse Obelisk Side 2) - 0x33721 & 0x220A7 & 0x220BD - True
-159722 - 0xFFE22 (Treehouse Obelisk Side 3) - 0x03B22 & 0x03B23 & 0x03B24 & 0x03B25 & 0x03A79 & 0x28ABD & 0x28ABE - True
-159723 - 0xFFE23 (Treehouse Obelisk Side 4) - 0x3388F & 0x28B29 & 0x28B2A - True
-159724 - 0xFFE24 (Treehouse Obelisk Side 5) - 0x018B6 & 0x033BE & 0x033BF & 0x033DD & 0x033E5 - True
-159725 - 0xFFE25 (Treehouse Obelisk Side 6) - 0x28AE9 & 0x3348F - True
-159730 - 0xFFE30 (River Obelisk Side 1) - 0x001A3 & 0x335AE - True
-159731 - 0xFFE31 (River Obelisk Side 2) - 0x000D3 & 0x035F5 & 0x09D5D & 0x09D5E & 0x09D63 - True
-159732 - 0xFFE32 (River Obelisk Side 3) - 0x3370E & 0x035DE & 0x03601 & 0x03603 & 0x03D0D & 0x3369A & 0x336C8 & 0x33505 - True
-159733 - 0xFFE33 (River Obelisk Side 4) - 0x03A9E & 0x016B2 & 0x3365F & 0x03731 & 0x036CE & 0x03C07 & 0x03A93 - True
-159734 - 0xFFE34 (River Obelisk Side 5) - 0x03AA6 & 0x3397C & 0x0105D & 0x0A304 - True
-159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True
-159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True
-159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True
-159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True
-159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True
-159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True
-159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True
-159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True
-159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True
-159753 - 0xFFE53 (Town Obelisk Side 4) - 0x28B30 & 0x035C9 - True
-159754 - 0xFFE54 (Town Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True
-159755 - 0xFFE55 (Town Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True
-
-Lasers (Lasers) - Entry - True:
diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/data/WitnessLogicExpert.txt
similarity index 80%
rename from worlds/witness/WitnessLogicExpert.txt
rename to worlds/witness/data/WitnessLogicExpert.txt
index b373c7417c24..1d1d010fde88 100644
--- a/worlds/witness/WitnessLogicExpert.txt
+++ b/worlds/witness/data/WitnessLogicExpert.txt
@@ -1,10 +1,14 @@
+==Tutorial (Inside)==
+
+Menu (Menu) - Entry - True:
+
Entry (Entry):
-First Hallway (First Hallway) - Entry - True - First Hallway Room - 0x00064:
+Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064:
158000 - 0x00064 (Straight) - True - True
159510 - 0x01848 (EP) - 0x00064 - True
-First Hallway Room (First Hallway) - Tutorial - 0x00182:
+Tutorial First Hallway Room (Tutorial First Hallway) - Tutorial - 0x00182:
158001 - 0x00182 (Bend) - True - True
Tutorial (Tutorial) - Outside Tutorial - True:
@@ -21,9 +25,11 @@ Tutorial (Tutorial) - Outside Tutorial - True:
159513 - 0x33600 (Patio Flowers EP) - 0x0C373 - True
159517 - 0x3352F (Gate EP) - 0x03505 - True
-Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2:
-158650 - 0x033D4 (Vault) - True - Dots & Full Dots & Squares & Black/White Squares
-158651 - 0x03481 (Vault Box) - 0x033D4 - True
+==Tutorial (Outside)==
+
+Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2 - Outside Tutorial Vault - 0x033D0:
+158650 - 0x033D4 (Vault Panel) - True - Dots & Full Dots & Squares & Black/White Squares
+Door - 0x033D0 (Vault Door) - 0x033D4
158013 - 0x0005D (Shed Row 1) - True - Dots & Full Dots
158014 - 0x0005E (Shed Row 2) - 0x0005D - Dots & Full Dots
158015 - 0x0005F (Shed Row 3) - 0x0005E - Dots & Full Dots
@@ -44,6 +50,9 @@ Door - 0x03BA2 (Outpost Path) - 0x0A3B5
159516 - 0x334A3 (Path EP) - True - True
159500 - 0x035C7 (Tractor EP) - True - True
+Outside Tutorial Vault (Outside Tutorial):
+158651 - 0x03481 (Vault Box) - True - True
+
Outside Tutorial Path To Outpost (Outside Tutorial) - Outside Tutorial Outpost - 0x0A170:
158011 - 0x0A171 (Outpost Entry Panel) - True - Dots & Full Dots & Triangles
Door - 0x0A170 (Outpost Entry) - 0x0A171
@@ -53,8 +62,23 @@ Outside Tutorial Outpost (Outside Tutorial) - Outside Tutorial - 0x04CA3:
Door - 0x04CA3 (Outpost Exit) - 0x04CA4
158600 - 0x17CFB (Discard) - True - Arrows
+Orchard (Orchard) - Main Island - True - Orchard Beyond First Gate - 0x03307:
+158071 - 0x00143 (Apple Tree 1) - True - True
+158072 - 0x0003B (Apple Tree 2) - 0x00143 - True
+158073 - 0x00055 (Apple Tree 3) - 0x0003B - True
+Door - 0x03307 (First Gate) - 0x00055
+
+Orchard Beyond First Gate (Orchard) - Orchard End - 0x03313:
+158074 - 0x032F7 (Apple Tree 4) - 0x00055 - True
+158075 - 0x032FF (Apple Tree 5) - 0x032F7 - True
+Door - 0x03313 (Second Gate) - 0x032FF
+
+Orchard End (Orchard):
+
Main Island (Main Island) - Outside Tutorial - True:
-159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True
+159801 - 0xFFD00 (Reached Independently) - True - True
+
+==Glass Factory==
Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29:
158027 - 0x01A54 (Entry Panel) - True - Symmetry
@@ -76,9 +100,11 @@ Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0
158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry & Dots
Door - 0x0D7ED (Back Wall) - 0x0005C
-Inside Glass Factory Behind Back Wall (Glass Factory) - Boat - 0x17CC8:
+Inside Glass Factory Behind Back Wall (Glass Factory) - The Ocean - 0x17CC8:
158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat
+==Symmetry Island==
+
Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E:
158040 - 0x000B0 (Lower Panel) - 0x0343A - Triangles
Door - 0x17F3E (Lower) - 0x000B0
@@ -112,32 +138,29 @@ Door - 0x18269 (Upper) - 0x1C349
159000 - 0x0332B (Glass Factory Black Line Reflection EP) - True - True
Symmetry Island Upper (Symmetry Island):
-158065 - 0x00A52 (Yellow 1) - True - Symmetry & Colored Dots
-158066 - 0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots
-158067 - 0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots
-158068 - 0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots
-158069 - 0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots
-158070 - 0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots
+158065 - 0x00A52 (Laser Yellow 1) - True - Symmetry & Colored Dots
+158066 - 0x00A57 (Laser Yellow 2) - 0x00A52 - Symmetry & Colored Dots
+158067 - 0x00A5B (Laser Yellow 3) - 0x00A57 - Symmetry & Colored Dots
+158068 - 0x00A61 (Laser Blue 1) - 0x00A52 - Symmetry & Colored Dots
+158069 - 0x00A64 (Laser Blue 2) - 0x00A61 & 0x00A57 - Symmetry & Colored Dots
+158070 - 0x00A68 (Laser Blue 3) - 0x00A64 & 0x00A5B - Symmetry & Colored Dots
158700 - 0x0360D (Laser Panel) - 0x00A68 - True
Laser - 0x00509 (Laser) - 0x0360D
159001 - 0x03367 (Glass Factory Black Line EP) - True - True
-Orchard (Orchard) - Main Island - True - Orchard Beyond First Gate - 0x03307:
-158071 - 0x00143 (Apple Tree 1) - True - True
-158072 - 0x0003B (Apple Tree 2) - 0x00143 - True
-158073 - 0x00055 (Apple Tree 3) - 0x0003B - True
-Door - 0x03307 (First Gate) - 0x00055
-
-Orchard Beyond First Gate (Orchard) - Orchard End - 0x03313:
-158074 - 0x032F7 (Apple Tree 4) - 0x00055 - True
-158075 - 0x032FF (Apple Tree 5) - 0x032F7 - True
-Door - 0x03313 (Second Gate) - 0x032FF
+==Desert==
-Orchard End (Orchard):
+Desert Obelisk (Desert) - Entry - True:
+159700 - 0xFFE00 (Obelisk Side 1) - 0x0332B & 0x03367 & 0x28B8A - True
+159701 - 0xFFE01 (Obelisk Side 2) - 0x037B6 & 0x037B2 & 0x000F7 - True
+159702 - 0xFFE02 (Obelisk Side 3) - 0x3351D - True
+159703 - 0xFFE03 (Obelisk Side 4) - 0x0053C & 0x00771 & 0x335C8 & 0x335C9 & 0x337F8 & 0x037BB & 0x220E4 & 0x220E5 - True
+159704 - 0xFFE04 (Obelisk Side 5) - 0x334B9 & 0x334BC & 0x22106 & 0x0A14C & 0x0A14D - True
+159709 - 0x00359 (Obelisk) - True - True
-Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE:
-158652 - 0x0CC7B (Vault) - True - Dots & Full Dots & Stars & Stars + Same Colored Symbol & Eraser & Triangles & Shapers & Negative Shapers & Colored Squares
-158653 - 0x0339E (Vault Box) - 0x0CC7B - True
+Desert Outside (Desert) - Main Island - True - Desert Light Room - 0x09FEE - Desert Vault - 0x03444:
+158652 - 0x0CC7B (Vault Panel) - True - Dots & Full Dots & Stars & Stars + Same Colored Symbol & Eraser & Triangles & Shapers & Negative Shapers & Colored Squares
+Door - 0x03444 (Vault Door) - 0x0CC7B
158602 - 0x17CE7 (Discard) - True - Arrows
158076 - 0x00698 (Surface 1) - True - True
158077 - 0x0048F (Surface 2) - 0x00698 - True
@@ -163,14 +186,17 @@ Laser - 0x012FB (Laser) - 0x03608
159040 - 0x334B9 (Shore EP) - True - True
159041 - 0x334BC (Island EP) - True - True
-Desert Floodlight Room (Desert) - Desert Pond Room - 0x0C2C3:
+Desert Vault (Desert):
+158653 - 0x0339E (Vault Box) - True - True
+
+Desert Light Room (Desert) - Desert Pond Room - 0x0C2C3:
158087 - 0x09FAA (Light Control) - True - True
158088 - 0x00422 (Light Room 1) - 0x09FAA - True
158089 - 0x006E3 (Light Room 2) - 0x09FAA - True
158090 - 0x0A02D (Light Room 3) - 0x09FAA & 0x00422 & 0x006E3 - True
Door - 0x0C2C3 (Pond Room Entry) - 0x0A02D
-Desert Pond Room (Desert) - Desert Water Levels Room - 0x0A24B:
+Desert Pond Room (Desert) - Desert Flood Room - 0x0A24B:
158091 - 0x00C72 (Pond Room 1) - True - True
158092 - 0x0129D (Pond Room 2) - 0x00C72 - True
158093 - 0x008BB (Pond Room 3) - 0x0129D - True
@@ -181,7 +207,7 @@ Door - 0x0A24B (Flood Room Entry) - 0x0A249
159043 - 0x0A14C (Pond Room Near Reflection EP) - True - True
159044 - 0x0A14D (Pond Room Far Reflection EP) - True - True
-Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316:
+Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316:
158097 - 0x1C2DF (Reduce Water Level Far Left) - True - True
158098 - 0x1831E (Reduce Water Level Far Right) - True - True
158099 - 0x1C260 (Reduce Water Level Near Left) - True - True
@@ -199,18 +225,29 @@ Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316:
Door - 0x0C316 (Elevator Room Entry) - 0x18076
159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True
-Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB:
-158111 - 0x17C31 (Final Transparent) - True - True
-158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True
-158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True
-158115 - 0x0A15C (Final Bent 1) - True - True
-158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True
-158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True
-159035 - 0x037BB (Elevator EP) - 0x012FB - True
-
-Desert Lowest Level Inbetween Shortcuts (Desert):
-
-Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - TrueOneWay:
+Desert Elevator Room (Desert) - Desert Behind Elevator - 0x01317:
+158111 - 0x17C31 (Elevator Room Transparent) - True - True
+158113 - 0x012D7 (Elevator Room Hexagonal) - 0x17C31 & 0x0A015 - True
+158114 - 0x0A015 (Elevator Room Hexagonal Control) - 0x17C31 - True
+158115 - 0x0A15C (Elevator Room Bent 1) - True - True
+158116 - 0x09FFF (Elevator Room Bent 2) - 0x0A15C - True
+158117 - 0x0A15F (Elevator Room Bent 3) - 0x09FFF - True
+159035 - 0x037BB (Elevator EP) - 0x01317 - True
+Door - 0x01317 (Elevator) - 0x03608
+
+Desert Behind Elevator (Desert):
+
+==Quarry==
+
+Quarry Obelisk (Quarry) - Entry - True:
+159740 - 0xFFE40 (Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True
+159741 - 0xFFE41 (Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True
+159742 - 0xFFE42 (Obelisk Side 3) - 0x289CF & 0x289D1 - True
+159743 - 0xFFE43 (Obelisk Side 4) - 0x33692 - True
+159744 - 0xFFE44 (Obelisk Side 5) - 0x03E77 & 0x03E7C - True
+159749 - 0x22073 (Obelisk) - True - True
+
+Outside Quarry (Quarry) - Main Island - True - Quarry Between Entry Doors - 0x09D6F - Quarry Elevator - 0xFFD00 & 0xFFD01:
158118 - 0x09E57 (Entry 1 Panel) - True - Squares & Black/White Squares & Triangles
158603 - 0x17CF0 (Discard) - True - Arrows
158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Triangles & Stars & Stars + Same Colored Symbol
@@ -222,36 +259,39 @@ Door - 0x09D6F (Entry 1) - 0x09E57
159420 - 0x289CF (Rock Line EP) - True - True
159421 - 0x289D1 (Rock Line Reflection EP) - True - True
-Quarry Elevator (Quarry):
+Quarry Elevator (Quarry) - Outside Quarry - 0x17CC4 - Quarry - 0x17CC4:
158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser
159403 - 0x17CB9 (Railroad EP) - 0x17CC4 - True
-Quarry Between Entrys (Quarry) - Quarry - 0x17C07:
+Quarry Between Entry Doors (Quarry) - Quarry - 0x17C07:
158119 - 0x17C09 (Entry 2 Panel) - True - Shapers & Triangles
Door - 0x17C07 (Entry 2) - 0x17C09
-Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010 - Quarry Elevator - 0x17CC4:
+Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010:
+159802 - 0xFFD01 (Inside Reached Independently) - True - True
158121 - 0x01E5A (Stoneworks Entry Left Panel) - True - Squares & Black/White Squares & Stars & Stars + Same Colored Symbol
158122 - 0x01E59 (Stoneworks Entry Right Panel) - True - Triangles
Door - 0x02010 (Stoneworks Entry) - 0x01E59 & 0x01E5A
-Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8:
+Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8 - Quarry Stoneworks Lift - TrueOneWay:
158123 - 0x275ED (Side Exit Panel) - True - True
Door - 0x275FF (Side Exit) - 0x275ED
158124 - 0x03678 (Lower Ramp Control) - True - Dots & Eraser
158145 - 0x17CAC (Roof Exit Panel) - True - True
Door - 0x17CE8 (Roof Exit) - 0x17CAC
-Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Ground Floor - 0x03675 - Quarry Stoneworks Upper Floor - 0x03679:
+Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - TrueOneWay:
158125 - 0x00E0C (Lower Row 1) - True - Triangles & Eraser
158126 - 0x01489 (Lower Row 2) - 0x00E0C - Triangles & Eraser
158127 - 0x0148A (Lower Row 3) - 0x01489 - Triangles & Eraser
158128 - 0x014D9 (Lower Row 4) - 0x0148A - Triangles & Eraser
158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Triangles & Eraser
158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Triangles & Eraser
+
+Quarry Stoneworks Lift (Quarry Stoneworks) - Quarry Stoneworks Middle Floor - 0x03679 - Quarry Stoneworks Ground Floor - 0x03679 - Quarry Stoneworks Upper Floor - 0x03679:
158131 - 0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser
-Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Floor - 0x03676 & 0x03679 - Quarry Stoneworks Ground Floor - 0x0368A:
+Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - 0x03675 - Quarry Stoneworks Ground Floor - 0x0368A:
158132 - 0x03676 (Upper Ramp Control) - True - Dots & Eraser
158133 - 0x03675 (Upper Lift Control) - True - Dots & Eraser
158134 - 0x00557 (Upper Row 1) - True - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol
@@ -262,7 +302,7 @@ Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Flo
158139 - 0x3C12D (Upper Row 6) - 0x0146C - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol
158140 - 0x03686 (Upper Row 7) - 0x3C12D - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol
158141 - 0x014E9 (Upper Row 8) - 0x03686 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol
-158142 - 0x03677 (Stair Control) - True - Squares & Colored Squares & Eraser
+158142 - 0x03677 (Stairs Panel) - True - Squares & Colored Squares & Eraser
Door - 0x0368A (Stairs) - 0x03677
158143 - 0x3C125 (Control Room Left) - 0x014E9 - Squares & Black/White Squares & Dots & Full Dots & Eraser
158144 - 0x0367C (Control Room Right) - 0x014E9 - Squares & Colored Squares & Triangles & Eraser & Stars & Stars + Same Colored Symbol
@@ -277,7 +317,7 @@ Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Fro
Door - 0x2769B (Dock) - 0x17CA6
Door - 0x27163 (Dock Invis Barrier) - 0x17CA6
-Quarry Boathouse Behind Staircase (Quarry Boathouse) - Boat - 0x17CA6:
+Quarry Boathouse Behind Staircase (Quarry Boathouse) - The Ocean - 0x17CA6:
Quarry Boathouse Upper Front (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x17C50:
158149 - 0x021B3 (Front Row 1) - True - Shapers & Eraser & Negative Shapers
@@ -309,9 +349,11 @@ Door - 0x3865F (Second Barrier) - 0x38663
158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Stars & Eraser & Shapers & Negative Shapers & Stars + Same Colored Symbol
159401 - 0x005F6 (Hook EP) - 0x275FA & 0x03852 & 0x3865F - True
-Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 & 0x19665:
+==Shadows==
+
+Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 | 0x19665:
158170 - 0x334DB (Door Timer Outside) - True - True
-Door - 0x19B24 (Timed Door) - 0x334DB
+Door - 0x19B24 (Timed Door) - 0x334DB | 0x334DC
158171 - 0x0AC74 (Intro 6) - 0x0A8DC - True
158172 - 0x0AC7A (Intro 7) - 0x0AC74 - True
158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - True
@@ -336,7 +378,7 @@ Shadows Ledge (Shadows) - Shadows - 0x1855B - Quarry - 0x19865 & 0x0A2DF:
158187 - 0x334DC (Door Timer Inside) - True - True
158188 - 0x198B5 (Intro 1) - True - True
158189 - 0x198BD (Intro 2) - 0x198B5 - True
-158190 - 0x198BF (Intro 3) - 0x198BD & 0x334DC & 0x19B24 - True
+158190 - 0x198BF (Intro 3) - 0x198BD & 0x19B24 - True
Door - 0x19865 (Quarry Barrier) - 0x198BF
Door - 0x0A2DF (Quarry Barrier 2) - 0x198BF
158191 - 0x19771 (Intro 4) - 0x198BF - True
@@ -345,22 +387,21 @@ Door - 0x1855B (Ledge Barrier) - 0x0A8DC
Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC
Shadows Laser Room (Shadows):
-158703 - 0x19650 (Laser Panel) - True - True
+158703 - 0x19650 (Laser Panel) - 0x194B2 & 0x19665 - True
Laser - 0x181B3 (Laser) - 0x19650
-Treehouse Beach (Treehouse Beach) - Main Island - True:
-159200 - 0x0053D (Rock Shadow EP) - True - True
-159201 - 0x0053E (Sand Shadow EP) - True - True
-159212 - 0x220BD (Both Orange Bridges EP) - 0x17DA2 & 0x17DDB - True
+==Keep==
-Keep (Keep) - Main Island - True - Keep 2nd Maze - 0x01954 - Keep 2nd Pressure Plate - 0x01BEC:
+Outside Keep (Keep) - Main Island - True:
+159430 - 0x03E77 (Red Flowers EP) - True - True
+159431 - 0x03E7C (Purple Flowers EP) - True - True
+
+Keep (Keep) - Outside Keep - True - Keep 2nd Maze - 0x01954 - Keep 2nd Pressure Plate - 0x01BEC:
158193 - 0x00139 (Hedge Maze 1) - True - True
158197 - 0x0A3A8 (Reset Pressure Plates 1) - True - True
158198 - 0x033EA (Pressure Plates 1) - 0x0A3A8 - Colored Squares & Triangles & Stars & Stars + Same Colored Symbol
Door - 0x01954 (Hedge Maze 1 Exit) - 0x00139
Door - 0x01BEC (Pressure Plates 1 Exit) - 0x033EA
-159430 - 0x03E77 (Red Flowers EP) - True - True
-159431 - 0x03E7C (Purple Flowers EP) - True - True
Keep 2nd Maze (Keep) - Keep - 0x018CE - Keep 3rd Maze - 0x019D8:
Door - 0x018CE (Hedge Maze 2 Shortcut) - 0x00139
@@ -395,18 +436,6 @@ Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F
158205 - 0x09E49 (Shadows Shortcut Panel) - True - True
Door - 0x09E3D (Shadows Shortcut) - 0x09E49
-Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True:
-158654 - 0x00AFB (Vault) - True - Symmetry & Sound Dots & Colored Dots
-158655 - 0x03535 (Vault Box) - 0x00AFB - True
-158605 - 0x17D28 (Discard) - True - Arrows
-159220 - 0x03B22 (Circle Far EP) - True - True
-159221 - 0x03B23 (Circle Left EP) - True - True
-159222 - 0x03B24 (Circle Near EP) - True - True
-159224 - 0x03A79 (Stern EP) - True - True
-159225 - 0x28ABD (Rope Inner EP) - True - True
-159226 - 0x28ABE (Rope Outer EP) - True - True
-159230 - 0x3388F (Couch EP) - 0x17CDF | 0x0A054 - True
-
Keep Tower (Keep) - Keep - 0x04F8F:
158206 - 0x0361B (Tower Shortcut Panel) - True - True
Door - 0x04F8F (Tower Shortcut) - 0x0361B
@@ -421,11 +450,39 @@ Laser - 0x014BB (Laser) - 0x0360E | 0x03317
159250 - 0x28AE9 (Path EP) - True - True
159251 - 0x3348F (Hedges EP) - True - True
+==Shipwreck==
+
+Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True - Shipwreck Vault - 0x17BB4:
+158654 - 0x00AFB (Vault Panel) - True - Symmetry & Sound Dots & Colored Dots
+Door - 0x17BB4 (Vault Door) - 0x00AFB
+158605 - 0x17D28 (Discard) - True - Arrows
+159220 - 0x03B22 (Circle Far EP) - True - True
+159221 - 0x03B23 (Circle Left EP) - True - True
+159222 - 0x03B24 (Circle Near EP) - True - True
+159224 - 0x03A79 (Stern EP) - True - True
+159225 - 0x28ABD (Rope Inner EP) - True - True
+159226 - 0x28ABE (Rope Outer EP) - True - True
+159230 - 0x3388F (Couch EP) - 0x17CDF | 0x0A054 - True
+
+Shipwreck Vault (Shipwreck):
+158655 - 0x03535 (Vault Box) - True - True
+
+==Monastery==
+
+Monastery Obelisk (Monastery) - Entry - True:
+159710 - 0xFFE10 (Obelisk Side 1) - 0x03ABC & 0x03ABE & 0x03AC0 & 0x03AC4 - True
+159711 - 0xFFE11 (Obelisk Side 2) - 0x03AC5 - True
+159712 - 0xFFE12 (Obelisk Side 3) - 0x03BE2 & 0x03BE3 & 0x0A409 - True
+159713 - 0xFFE13 (Obelisk Side 4) - 0x006E5 & 0x006E6 & 0x006E7 & 0x034A7 & 0x034AD & 0x034AF & 0x03DAB & 0x03DAC & 0x03DAD - True
+159714 - 0xFFE14 (Obelisk Side 5) - 0x03E01 - True
+159715 - 0xFFE15 (Obelisk Side 6) - 0x289F4 & 0x289F5 - True
+159719 - 0x00263 (Obelisk) - True - True
+
Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750:
-158207 - 0x03713 (Shortcut Panel) - True - True
-Door - 0x0364E (Shortcut) - 0x03713
+158207 - 0x03713 (Laser Shortcut Panel) - True - True
+Door - 0x0364E (Laser Shortcut) - 0x03713
158208 - 0x00B10 (Entry Left) - True - True
-158209 - 0x00C92 (Entry Right) - True - True
+158209 - 0x00C92 (Entry Right) - 0x00B10 - True
Door - 0x0C128 (Entry Inner) - 0x00B10
Door - 0x0C153 (Entry Outer) - 0x00C92
158210 - 0x00290 (Outside 1) - 0x09D9B - True
@@ -441,6 +498,9 @@ Laser - 0x17C65 (Laser) - 0x17CA4
159137 - 0x03DAC (Facade Left Stairs EP) - True - True
159138 - 0x03DAD (Facade Right Stairs EP) - True - True
159140 - 0x03E01 (Grass Stairs EP) - True - True
+159120 - 0x03BE2 (Garden Left EP) - 0x03750 - True
+159121 - 0x03BE3 (Garden Right EP) - True - True
+159122 - 0x0A409 (Wall EP) - True - True
Inside Monastery (Monastery):
158213 - 0x09D9B (Shutters Control) - True - Dots
@@ -454,11 +514,22 @@ Inside Monastery (Monastery):
Monastery Garden (Monastery):
-Town (Town) - Main Island - True - Boat - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9:
+==Town==
+
+Town Obelisk (Town) - Entry - True:
+159750 - 0xFFE50 (Obelisk Side 1) - 0x035C7 - True
+159751 - 0xFFE51 (Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True
+159752 - 0xFFE52 (Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True
+159753 - 0xFFE53 (Obelisk Side 4) - 0x28B30 & 0x035C9 - True
+159754 - 0xFFE54 (Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True
+159755 - 0xFFE55 (Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True
+159759 - 0x0A16C (Obelisk) - True - True
+
+Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - Town RGB House - 0x28A61 - Town Inside Cargo Box - 0x0A0C9 - Outside Windmill - True:
158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat
158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Squares & Black/White Squares & Shapers & Triangles
Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8
-158707 - 0x09F98 (Desert Laser Redirect) - True - True
+158707 - 0x09F98 (Desert Laser Redirect Control) - True - True
158220 - 0x18590 (Transparent) - True - Symmetry
158221 - 0x28AE3 (Vines) - 0x18590 - True
158222 - 0x28938 (Apple Tree) - 0x28AE3 - True
@@ -469,22 +540,17 @@ Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8
158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Triangles & Dots & Full Dots
158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Triangles & Dots & Full Dots
Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1
-158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers & Stars + Same Colored Symbol
-Door - 0x28A61 (Tinted Glass Door) - 0x28A0D
+158225 - 0x28998 (RGB House Entry Panel) - True - Stars & Rotated Shapers & Stars + Same Colored Symbol
+Door - 0x28A61 (RGB House Entry) - 0x28A0D
158226 - 0x28A0D (Church Entry Panel) - 0x28998 - Stars
Door - 0x03BB0 (Church Entry) - 0x03C08
-158228 - 0x28A79 (Maze Stair Control) - True - True
+158228 - 0x28A79 (Maze Panel) - True - True
Door - 0x28AA2 (Maze Stairs) - 0x28A79
-158241 - 0x17F5F (Windmill Entry Panel) - True - Dots
-Door - 0x1845B (Windmill Entry) - 0x17F5F
-159010 - 0x037B6 (Windmill First Blade EP) - 0x17D02 - True
-159011 - 0x037B2 (Windmill Second Blade EP) - 0x17D02 - True
-159012 - 0x000F7 (Windmill Third Blade EP) - 0x17D02 - True
159540 - 0x03335 (Tower Underside Third EP) - True - True
159541 - 0x03412 (Tower Underside Fourth EP) - True - True
159542 - 0x038A6 (Tower Underside First EP) - True - True
159543 - 0x038AA (Tower Underside Second EP) - True - True
-159545 - 0x03E40 (RGB House Green EP) - 0x334D8 & 0x03C0C & 0x03C08 - True
+159545 - 0x03E40 (RGB House Green EP) - 0x334D8 - True
159546 - 0x28B8E (Maze Bridge Underside EP) - 0x2896A - True
159552 - 0x03BCF (Black Line Redirect EP) - True - True
159800 - 0xFFF80 (Pet the Dog) - True - True
@@ -512,20 +578,26 @@ Town Church (Town):
158227 - 0x28A69 (Church Lattice) - 0x03BB0 - True
159553 - 0x03BD1 (Black Line Church EP) - True - True
-RGB House (Town) - RGB Room - 0x2897B:
+Town RGB House (Town RGB House) - Town RGB House Upstairs - 0x2897B:
158242 - 0x034E4 (Sound Room Left) - True - True
158243 - 0x034E3 (Sound Room Right) - True - Sound Dots
-Door - 0x2897B (RGB House Stairs) - 0x034E4 & 0x034E3
+Door - 0x2897B (Stairs) - 0x034E4 & 0x034E3
-RGB Room (Town):
+Town RGB House Upstairs (Town RGB House Upstairs):
158244 - 0x334D8 (RGB Control) - True - Rotated Shapers & Squares & Colored Squares & Triangles
-158245 - 0x03C0C (RGB Room Left) - 0x334D8 - Squares & Colored Squares & Black/White Squares & Eraser
-158246 - 0x03C08 (RGB Room Right) - 0x334D8 & 0x03C0C - Symmetry & Dots & Colored Dots & Triangles
+158245 - 0x03C0C (Left) - 0x334D8 - Squares & Colored Squares & Black/White Squares & Eraser
+158246 - 0x03C08 (Right) - 0x334D8 & 0x03C0C - Symmetry & Dots & Colored Dots & Triangles
+
+Town Tower Bottom (Town Tower) - Town - True - Town Tower After First Door - 0x27799:
+Door - 0x27799 (First Door) - 0x28A69
-Town Tower (Town Tower) - Town - True - Town Tower Top - 0x27798 & 0x27799 & 0x2779A & 0x2779C:
+Town Tower After First Door (Town Tower) - Town Tower After Second Door - 0x27798:
Door - 0x27798 (Second Door) - 0x28ACC
+
+Town Tower After Second Door (Town Tower) - Town Tower After Third Door - 0x2779C:
Door - 0x2779C (Third Door) - 0x28AD9
-Door - 0x27799 (First Door) - 0x28A69
+
+Town Tower After Third Door (Town Tower) - Town Tower Top - 0x2779A:
Door - 0x2779A (Fourth Door) - 0x28B39
Town Tower Top (Town):
@@ -534,6 +606,15 @@ Laser - 0x032F9 (Laser) - 0x032F5
159422 - 0x33692 (Brown Bridge EP) - True - True
159551 - 0x03BCE (Black Line Tower EP) - True - True
+==Windmill & Theater==
+
+Outside Windmill (Windmill) - Windmill Interior - 0x1845B:
+159010 - 0x037B6 (First Blade EP) - 0x17D02 - True
+159011 - 0x037B2 (Second Blade EP) - 0x17D02 - True
+159012 - 0x000F7 (Third Blade EP) - 0x17D02 - True
+158241 - 0x17F5F (Entry Panel) - True - Dots
+Door - 0x1845B (Entry) - 0x17F5F
+
Windmill Interior (Windmill) - Theater - 0x17F88:
158247 - 0x17D02 (Turn Control) - True - Dots
158248 - 0x17F89 (Theater Entry Panel) - True - Squares & Black/White Squares & Eraser & Triangles
@@ -557,7 +638,9 @@ Door - 0x3CCDF (Exit Right) - 0x33AB2
159556 - 0x33A2A (Door EP) - 0x03553 - True
159558 - 0x33B06 (Church EP) - 0x0354E - True
-Jungle (Jungle) - Main Island - True - Outside Jungle River - 0x3873B - Boat - 0x17CDF:
+==Jungle==
+
+Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF:
158251 - 0x17CDF (Shore Boat Spawn) - True - Boat
158609 - 0x17F9B (Discard) - True - Arrows
158252 - 0x002C4 (First Row 1) - True - True
@@ -588,18 +671,20 @@ Door - 0x3873B (Laser Shortcut) - 0x337FA
159350 - 0x035CB (Bamboo CCW EP) - True - True
159351 - 0x035CF (Bamboo CW EP) - True - True
-Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A:
-158267 - 0x17CAA (Monastery Shortcut Panel) - True - True
-Door - 0x0CF2A (Monastery Shortcut) - 0x17CAA
-158663 - 0x15ADD (Vault) - True - Black/White Squares & Dots
-158664 - 0x03702 (Vault Box) - 0x15ADD - True
+Outside Jungle River (Jungle) - Main Island - True - Monastery Garden - 0x0CF2A - Jungle Vault - 0x15287:
+158267 - 0x17CAA (Monastery Garden Shortcut Panel) - True - True
+Door - 0x0CF2A (Monastery Garden Shortcut) - 0x17CAA
+158663 - 0x15ADD (Vault Panel) - True - Black/White Squares & Dots
+Door - 0x15287 (Vault Door) - 0x15ADD
159110 - 0x03AC5 (Green Leaf Moss EP) - True - True
-159120 - 0x03BE2 (Monastery Garden Left EP) - 0x03750 - True
-159121 - 0x03BE3 (Monastery Garden Right EP) - True - True
-159122 - 0x0A409 (Monastery Wall EP) - True - True
+
+Jungle Vault (Jungle):
+158664 - 0x03702 (Vault Box) - True - True
+
+==Bunker==
Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4:
-158268 - 0x17C2E (Entry Panel) - True - Squares & Black/White Squares & Colored Squares
+158268 - 0x17C2E (Entry Panel) - True - Squares & Black/White Squares
Door - 0x0C2A4 (Entry) - 0x17C2E
Bunker (Bunker) - Bunker Glass Room - 0x17C79:
@@ -616,9 +701,9 @@ Bunker (Bunker) - Bunker Glass Room - 0x17C79:
Door - 0x17C79 (Tinted Glass Door) - 0x0A099
Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3:
-158279 - 0x0A010 (Glass Room 1) - True - Squares & Colored Squares
-158280 - 0x0A01B (Glass Room 2) - 0x0A010 - Squares & Colored Squares & Black/White Squares
-158281 - 0x0A01F (Glass Room 3) - 0x0A01B - Squares & Colored Squares & Black/White Squares
+158279 - 0x0A010 (Glass Room 1) - 0x17C79 - Squares & Colored Squares
+158280 - 0x0A01B (Glass Room 2) - 0x17C79 & 0x0A010 - Squares & Colored Squares & Black/White Squares
+158281 - 0x0A01F (Glass Room 3) - 0x17C79 & 0x0A01B - Squares & Colored Squares & Black/White Squares
Door - 0x0C2A3 (UV Room Entry) - 0x0A01F
Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D:
@@ -631,9 +716,11 @@ Door - 0x0A08D (Elevator Room Entry) - 0x17E67
Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay:
159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True
-Bunker Elevator (Bunker) - Bunker Laser Platform - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079:
+Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Cyan Room - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079:
158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares
+Bunker Cyan Room (Bunker) - Bunker Elevator - TrueOneWay:
+
Bunker Green Room (Bunker) - Bunker Elevator - TrueOneWay:
159310 - 0x000D3 (Green Room Flowers EP) - True - True
@@ -641,6 +728,8 @@ Bunker Laser Platform (Bunker) - Bunker Elevator - TrueOneWay:
158710 - 0x09DE0 (Laser Panel) - True - True
Laser - 0x0C2B2 (Laser) - 0x09DE0
+==Swamp==
+
Outside Swamp (Swamp) - Swamp Entry Area - 0x00C1C - Main Island - True:
158287 - 0x0056E (Entry Panel) - True - Rotated Shapers & Black/White Squares & Triangles
Door - 0x00C1C (Entry) - 0x0056E
@@ -676,8 +765,8 @@ Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat
158316 - 0x00990 (Platform Row 4) - 0x0098F - Rotated Shapers
Door - 0x184B7 (Between Bridges First Door) - 0x00990
158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Rotated Shapers
-158318 - 0x17C0E (Platform Shortcut Right Panel) - True - Rotated Shapers
-Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E
+158318 - 0x17C0E (Platform Shortcut Right Panel) - 0x17C0D - Rotated Shapers
+Door - 0x38AE6 (Platform Shortcut) - 0x17C0E
Door - 0x04B7F (Cyan Water Pump) - 0x00006
Swamp Cyan Underwater (Swamp):
@@ -703,7 +792,7 @@ Swamp Between Bridges Far (Swamp) - Swamp Red Underwater - 0x183F2 - Swamp Rotat
158322 - 0x0000A (Between Bridges Far Row 4) - 0x00009 - Rotated Shapers & Shapers & Dots & Full Dots
Door - 0x183F2 (Red Water Pump) - 0x00596
-Swamp Red Underwater (Swamp) - Swamp Maze - 0x014D1:
+Swamp Red Underwater (Swamp) - Swamp Maze - 0x305D5:
158323 - 0x00001 (Red Underwater 1) - True - Shapers & Negative Shapers & Dots & Full Dots
158324 - 0x014D2 (Red Underwater 2) - True - Shapers & Negative Shapers & Dots & Full Dots
158325 - 0x014D4 (Red Underwater 3) - True - Shapers & Negative Shapers & Dots & Full Dots
@@ -715,18 +804,21 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near
159331 - 0x016B2 (Rotating Bridge CCW EP) - 0x181F5 - True
159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True
-Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482:
+Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8:
+159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True
158328 - 0x09DB8 (Boat Spawn) - True - Boat
158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Shapers & Dots & Full Dots
158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers & Shapers & Dots & Full Dots
158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Shapers & Dots & Full Dots
158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Shapers & Dots & Full Dots
-158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers
Door - 0x18482 (Blue Water Pump) - 0x00E3A
159332 - 0x3365F (Boat EP) - 0x09DB8 - True
159333 - 0x03731 (Long Bridge Side EP) - 0x17E2B - True
-Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6:
+Swamp Long Bridge (Swamp) - Swamp Near Boat - 0x17E2B - Outside Swamp - 0x17E2B:
+158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers
+
+Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6 - Swamp Near Boat - TrueOneWay:
Door - 0x0A1D6 (Purple Water Pump) - 0x00E3A
Swamp Purple Underwater (Swamp):
@@ -752,13 +844,29 @@ Laser - 0x00BF6 (Laser) - 0x03615
158342 - 0x17C02 (Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Stars & Stars + Same Colored Symbol
Door - 0x2D880 (Laser Shortcut) - 0x17C02
-Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309:
+==Treehouse==
+
+Treehouse Obelisk (Treehouse) - Entry - True:
+159720 - 0xFFE20 (Obelisk Side 1) - 0x0053D & 0x0053E & 0x00769 - True
+159721 - 0xFFE21 (Obelisk Side 2) - 0x33721 & 0x220A7 & 0x220BD - True
+159722 - 0xFFE22 (Obelisk Side 3) - 0x03B22 & 0x03B23 & 0x03B24 & 0x03B25 & 0x03A79 & 0x28ABD & 0x28ABE - True
+159723 - 0xFFE23 (Obelisk Side 4) - 0x3388F & 0x28B29 & 0x28B2A - True
+159724 - 0xFFE24 (Obelisk Side 5) - 0x018B6 & 0x033BE & 0x033BF & 0x033DD & 0x033E5 - True
+159725 - 0xFFE25 (Obelisk Side 6) - 0x28AE9 & 0x3348F - True
+159729 - 0x00097 (Obelisk) - True - True
+
+Treehouse Beach (Treehouse Beach) - Main Island - True:
+159200 - 0x0053D (Rock Shadow EP) - True - True
+159201 - 0x0053E (Sand Shadow EP) - True - True
+159212 - 0x220BD (Both Orange Bridges EP) - 0x17DA2 & 0x17DDB - True
+
+Treehouse Entry Area (Treehouse) - Treehouse Between Entry Doors - 0x0C309 - The Ocean - 0x17C95:
158343 - 0x17C95 (Boat Spawn) - True - Boat
158344 - 0x0288C (First Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles
Door - 0x0C309 (First Door) - 0x0288C
159210 - 0x33721 (Buoy EP) - 0x17C95 - True
-Treehouse Between Doors (Treehouse) - Treehouse Yellow Bridge - 0x0C310:
+Treehouse Between Entry Doors (Treehouse) - Treehouse Yellow Bridge - 0x0C310:
158345 - 0x02886 (Second Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles
Door - 0x0C310 (Second Door) - 0x02886
@@ -778,7 +886,7 @@ Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181:
Door - 0x0A181 (Third Door) - 0x0A182
Treehouse Junction (Treehouse) - Treehouse Right Orange Bridge - True - Treehouse First Purple Bridge - True - Treehouse Green Bridge - True:
-158356 - 0x2700B (Laser House Door Timer Outside Control) - True - True
+158356 - 0x2700B (Laser House Door Timer Outside) - True - True
Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x17D6C:
158357 - 0x17DC8 (First Purple Bridge 1) - True - Stars & Dots & Full Dots
@@ -787,7 +895,7 @@ Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x1
158360 - 0x17D2D (First Purple Bridge 4) - 0x17CE4 - Stars & Dots & Full Dots
158361 - 0x17D6C (First Purple Bridge 5) - 0x17D2D - Stars & Dots & Full Dots
-Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2:
+Treehouse Right Orange Bridge (Treehouse) - Treehouse Drawbridge Platform - 0x17DA2:
158391 - 0x17D88 (Right Orange Bridge 1) - True - Stars & Stars + Same Colored Symbol & Triangles
158392 - 0x17DB4 (Right Orange Bridge 2) - 0x17D88 - Stars & Stars + Same Colored Symbol & Triangles
158393 - 0x17D8C (Right Orange Bridge 3) - 0x17DB4 - Stars & Stars + Same Colored Symbol & Triangles
@@ -801,8 +909,8 @@ Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2:
158401 - 0x17DB1 (Right Orange Bridge 11) - 0x17DB7 - Stars & Stars + Same Colored Symbol & Triangles
158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars & Stars + Same Colored Symbol & Triangles
-Treehouse Bridge Platform (Treehouse) - Main Island - 0x0C32D:
-158404 - 0x037FF (Bridge Control) - True - Stars
+Treehouse Drawbridge Platform (Treehouse) - Main Island - 0x0C32D:
+158404 - 0x037FF (Drawbridge Panel) - True - Stars
Door - 0x0C32D (Drawbridge) - 0x037FF
Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6:
@@ -847,7 +955,7 @@ Treehouse Green Bridge Left House (Treehouse):
159211 - 0x220A7 (Right Orange Bridge EP) - 0x17DA2 - True
Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323:
-Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DEC
+Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DEC | 0x17CBC
Treehouse Laser Room Back Platform (Treehouse):
158611 - 0x17FA0 (Laser Discard) - True - Arrows
@@ -860,28 +968,45 @@ Treehouse Laser Room (Treehouse):
158403 - 0x17CBC (Laser House Door Timer Inside) - True - True
Laser - 0x028A4 (Laser) - 0x03613
-Mountainside (Mountainside) - Main Island - True - Mountaintop - True:
+==Mountain (Outside)==
+
+Mountainside Obelisk (Mountainside) - Entry - True:
+159730 - 0xFFE30 (Obelisk Side 1) - 0x001A3 & 0x335AE - True
+159731 - 0xFFE31 (Obelisk Side 2) - 0x000D3 & 0x035F5 & 0x09D5D & 0x09D5E & 0x09D63 - True
+159732 - 0xFFE32 (Obelisk Side 3) - 0x3370E & 0x035DE & 0x03601 & 0x03603 & 0x03D0D & 0x3369A & 0x336C8 & 0x33505 - True
+159733 - 0xFFE33 (Obelisk Side 4) - 0x03A9E & 0x016B2 & 0x3365F & 0x03731 & 0x036CE & 0x03C07 & 0x03A93 - True
+159734 - 0xFFE34 (Obelisk Side 5) - 0x03AA6 & 0x3397C & 0x0105D & 0x0A304 - True
+159735 - 0xFFE35 (Obelisk Side 6) - 0x035CB & 0x035CF - True
+159739 - 0x00367 (Obelisk) - True - True
+
+Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
+159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True
158612 - 0x17C42 (Discard) - True - Arrows
-158665 - 0x002A6 (Vault) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol
-158666 - 0x03542 (Vault Box) - 0x002A6 - True
+158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol
+Door - 0x00085 (Vault Door) - 0x002A6
159301 - 0x335AE (Cloud Cycle EP) - True - True
159325 - 0x33505 (Bush EP) - True - True
159335 - 0x03C07 (Apparent River EP) - True - True
-Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34:
+Mountainside Vault (Mountainside):
+158666 - 0x03542 (Vault Box) - True - True
+
+Mountaintop (Mountaintop) - Mountain Floor 1 - 0x17C34:
158405 - 0x0042D (River Shape) - True - True
-158406 - 0x09F7F (Box Short) - 7 Lasers - True
-158407 - 0x17C34 (Trap Door Triple Exit) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol & Triangles
-158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True
+158406 - 0x09F7F (Box Short) - 7 Lasers + Redirect - True
+158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol & Triangles
+158800 - 0xFFF00 (Box Long) - 11 Lasers + Redirect & 0x17C34 - True
159300 - 0x001A3 (River Shape EP) - True - True
159320 - 0x3370E (Arch Black EP) - True - True
159324 - 0x336C8 (Arch White Right EP) - True - True
159326 - 0x3369A (Arch White Left EP) - True - True
-Mountain Top Layer (Mountain Floor 1) - Mountain Top Layer Bridge - 0x09E39:
+==Mountain (Inside)==
+
+Mountain Floor 1 (Mountain Floor 1) - Mountain Floor 1 Bridge - 0x09E39:
158408 - 0x09E39 (Light Bridge Controller) - True - Eraser & Triangles
-Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54:
+Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneWay:
158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots & Stars & Stars + Same Colored Symbol
158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Triangles
158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers & Stars & Stars + Same Colored Symbol
@@ -899,9 +1024,11 @@ Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54:
158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Stars & Shapers & Stars + Same Colored Symbol
158424 - 0x09EAD (Trash Pillar 1) - True - Rotated Shapers & Stars
158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Rotated Shapers & Triangles
+
+Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54:
Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B
-Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Blue Bridge - 0x09E86 - Mountain Pink Bridge EP - TrueOneWay:
+Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 Above The Abyss - True - Mountain Pink Bridge EP - TrueOneWay:
158426 - 0x09FD3 (Near Row 1) - True - Stars & Colored Squares & Stars + Same Colored Symbol
158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Stars & Triangles & Stars + Same Colored Symbol
158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol
@@ -909,15 +1036,13 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near -
158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser
Door - 0x09FFB (Staircase Near) - 0x09FD8
-Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - 0x09ED8:
-
-Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD:
+Mountain Floor 2 Above The Abyss (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD & 0x09ED8 & 0x09E86:
Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86
Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2):
158431 - 0x09E86 (Light Bridge Controller Near) - True - Shapers & Dots
-Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay:
+Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay - Mountain Floor 2 - 0x09ED8:
158432 - 0x09FCC (Far Row 1) - True - Triangles
158433 - 0x09FCE (Far Row 2) - 0x09FCC - Black/White Squares & Stars & Stars + Same Colored Symbol
158434 - 0x09FCF (Far Row 3) - 0x09FCE - Stars & Triangles & Stars + Same Colored Symbol
@@ -932,37 +1057,56 @@ Mountain Floor 2 Light Bridge Room Far (Mountain Floor 2):
Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - TrueOneWay:
158613 - 0x17F93 (Elevator Discard) - True - Arrows
-Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Third Layer - 0x09EEB:
+Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Floor 3 - 0x09EEB:
158439 - 0x09EEB (Elevator Control Panel) - True - Dots
-Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89:
+Mountain Floor 3 (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89 - Mountain Pink Bridge EP - TrueOneWay:
158440 - 0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Eraser & Negative Shapers
158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser & Negative Shapers
158442 - 0x09F01 (Giant Puzzle Top Right) - True - Shapers & Eraser & Negative Shapers
158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser & Negative Shapers
158444 - 0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry
+159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True
+159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True
Door - 0x09F89 (Exit) - 0x09FDA
-Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x17FA2 - Final Room - 0x0C141 - Mountain Pink Bridge EP - TrueOneWay:
+Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Path to Caves - 0x17F33 - Mountain Bottom Floor Pillars Room - 0x0C141:
158614 - 0x17FA2 (Discard) - 0xFFF00 - Arrows
-158445 - 0x01983 (Final Room Entry Left) - True - Shapers & Stars
-158446 - 0x01987 (Final Room Entry Right) - True - Squares & Colored Squares & Dots
-Door - 0x0C141 (Final Room Entry) - 0x01983 & 0x01987
-159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True
-159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True
+158445 - 0x01983 (Pillars Room Entry Left) - True - Shapers & Stars
+158446 - 0x01987 (Pillars Room Entry Right) - True - Squares & Colored Squares & Dots
+Door - 0x0C141 (Pillars Room Entry) - 0x01983 & 0x01987
+Door - 0x17F33 (Rock Open) - 0x17FA2 | 0x334E1
+
+Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB & 0x33961:
+158522 - 0x0383A (Right Pillar 1) - True - Stars & Eraser & Triangles & Stars + Same Colored Symbol
+158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Dots & Full Dots & Triangles & Symmetry
+158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Shapers & Stars & Negative Shapers & Stars + Same Colored Symbol & Symmetry
+158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Eraser & Symmetry & Stars & Stars + Same Colored Symbol & Negative Shapers & Shapers
+158526 - 0x0383D (Left Pillar 1) - True - Stars & Black/White Squares & Stars + Same Colored Symbol
+158527 - 0x0383F (Left Pillar 2) - 0x0383D - Triangles & Symmetry
+158528 - 0x03859 (Left Pillar 3) - 0x0383F - Symmetry & Shapers & Black/White Squares
+158529 - 0x339BB (Left Pillar 4) - 0x03859 - Symmetry & Black/White Squares & Stars & Stars + Same Colored Symbol & Triangles & Colored Dots
+
+Elevator (Mountain Bottom Floor):
+158530 - 0x3D9A6 (Elevator Door Close Left) - True - True
+158531 - 0x3D9A7 (Elevator Door Close Right) - True - True
+158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True
+158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True
+158534 - 0x3D9AA (Back Wall Left) - 0x3D9A6 | 0x3D9A7 - True
+158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True
+158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True
Mountain Pink Bridge EP (Mountain Floor 2):
159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True
-Mountain Bottom Floor Rock (Mountain Bottom Floor) - Mountain Bottom Floor - 0x17F33 - Mountain Path to Caves - 0x17F33:
-Door - 0x17F33 (Rock Open) - True
-
-Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x334E1 - Caves - 0x2D77D:
+Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D:
158447 - 0x00FF8 (Caves Entry Panel) - True - Arrows & Black/White Squares
Door - 0x2D77D (Caves Entry) - 0x00FF8
158448 - 0x334E1 (Rock Control) - True - True
-Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Path to Challenge - 0x019A5:
+==Caves==
+
+Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Caves Path to Challenge - 0x019A5:
158451 - 0x335AB (Elevator Inside Control) - True - Dots & Squares & Black/White Squares
158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Squares & Black/White Squares
158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Squares & Black/White Squares & Dots
@@ -1017,36 +1161,43 @@ Door - 0x2D73F (Mountain Shortcut Door) - 0x021D7
Door - 0x2D859 (Swamp Shortcut Door) - 0x17CF2
159341 - 0x3397C (Skylight EP) - True - True
-Path to Challenge (Caves) - Challenge - 0x0A19A:
+Caves Path to Challenge (Caves) - Challenge - 0x0A19A:
158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Arrows & Stars + Same Colored Symbol
Door - 0x0A19A (Challenge Entry) - 0x0A16E
-Challenge (Challenge) - Tunnels - 0x0348A:
+==Challenge==
+
+Challenge (Challenge) - Tunnels - 0x0348A - Challenge Vault - 0x04D75:
158499 - 0x0A332 (Start Timer) - 11 Lasers - True
158500 - 0x0088E (Small Basic) - 0x0A332 - True
158501 - 0x00BAF (Big Basic) - 0x0088E - True
-158502 - 0x00BF3 (Square) - 0x00BAF - Squares & Black/White Squares
+158502 - 0x00BF3 (Square) - 0x00BAF - Black/White Squares
158503 - 0x00C09 (Maze Map) - 0x00BF3 - Dots
158504 - 0x00CDB (Stars and Dots) - 0x00C09 - Stars & Dots
158505 - 0x0051F (Symmetry) - 0x00CDB - Symmetry & Colored Dots & Dots
158506 - 0x00524 (Stars and Shapers) - 0x0051F - Stars & Shapers
158507 - 0x00CD4 (Big Basic 2) - 0x00524 - True
-158508 - 0x00CB9 (Choice Squares Right) - 0x00CD4 - Squares & Black/White Squares
-158509 - 0x00CA1 (Choice Squares Middle) - 0x00CD4 - Squares & Black/White Squares
-158510 - 0x00C80 (Choice Squares Left) - 0x00CD4 - Squares & Black/White Squares
-158511 - 0x00C68 (Choice Squares 2 Right) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares
-158512 - 0x00C59 (Choice Squares 2 Middle) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares
-158513 - 0x00C22 (Choice Squares 2 Left) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares
+158508 - 0x00CB9 (Choice Squares Right) - 0x00CD4 - Black/White Squares
+158509 - 0x00CA1 (Choice Squares Middle) - 0x00CD4 - Black/White Squares
+158510 - 0x00C80 (Choice Squares Left) - 0x00CD4 - Black/White Squares
+158511 - 0x00C68 (Choice Squares 2 Right) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares
+158512 - 0x00C59 (Choice Squares 2 Middle) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares
+158513 - 0x00C22 (Choice Squares 2 Left) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares
158514 - 0x034F4 (Maze Hidden 1) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles
158515 - 0x034EC (Maze Hidden 2) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles
158516 - 0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry
-158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Squares & Black/White Squares & Symmetry
-158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True
+158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Black/White Squares & Symmetry
+Door - 0x04D75 (Vault Door) - 0x1C31A & 0x1C319
158518 - 0x039B4 (Tunnels Entry Panel) - True - Arrows
Door - 0x0348A (Tunnels Entry) - 0x039B4
159530 - 0x28B30 (Water EP) - True - True
-Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Lowest Level Inbetween Shortcuts - 0x27263 - Town - 0x09E87:
+Challenge Vault (Challenge):
+158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True
+
+==Tunnels==
+
+Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Behind Elevator - 0x27263 - Town - 0x09E87:
158668 - 0x2FAF6 (Vault Box) - True - True
158519 - 0x27732 (Theater Shortcut Panel) - True - True
Door - 0x27739 (Theater Shortcut) - 0x27732
@@ -1056,26 +1207,9 @@ Door - 0x27263 (Desert Shortcut) - 0x2773D
Door - 0x09E87 (Town Shortcut) - 0x09E85
159557 - 0x33A20 (Theater Flowers EP) - 0x03553 & Theater to Tunnels - True
-Final Room (Mountain Final Room) - Elevator - 0x339BB & 0x33961:
-158522 - 0x0383A (Right Pillar 1) - True - Stars & Eraser & Triangles & Stars + Same Colored Symbol
-158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Dots & Full Dots & Triangles & Symmetry
-158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Shapers & Stars & Negative Shapers & Stars + Same Colored Symbol & Symmetry
-158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Eraser & Symmetry & Stars & Stars + Same Colored Symbol & Negative Shapers & Shapers
-158526 - 0x0383D (Left Pillar 1) - True - Stars & Black/White Squares & Stars + Same Colored Symbol
-158527 - 0x0383F (Left Pillar 2) - 0x0383D - Triangles & Symmetry
-158528 - 0x03859 (Left Pillar 3) - 0x0383F - Symmetry & Shapers & Black/White Squares
-158529 - 0x339BB (Left Pillar 4) - 0x03859 - Symmetry & Black/White Squares & Stars & Stars + Same Colored Symbol & Triangles & Colored Dots
+==Boat==
-Elevator (Mountain Final Room):
-158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True
-158531 - 0x3D9A7 (Elevator Door Close Right) - True - True
-158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True
-158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True
-158534 - 0x3D9AA (Back Wall Left) - 0x3D9A6 | 0x3D9A7 - True
-158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True
-158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True
-
-Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay:
+The Ocean (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay:
159042 - 0x22106 (Desert EP) - True - True
159223 - 0x03B25 (Shipwreck CCW Underside EP) - True - True
159231 - 0x28B29 (Shipwreck Green EP) - True - True
@@ -1086,39 +1220,3 @@ Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehous
159521 - 0x33879 (Tutorial Reflection EP) - True - True
159522 - 0x03C19 (Tutorial Moss EP) - True - True
159531 - 0x035C9 (Cargo Box EP) - 0x0A0C9 - True
-
-Obelisks (EPs) - Entry - True:
-159700 - 0xFFE00 (Desert Obelisk Side 1) - 0x0332B & 0x03367 & 0x28B8A - True
-159701 - 0xFFE01 (Desert Obelisk Side 2) - 0x037B6 & 0x037B2 & 0x000F7 - True
-159702 - 0xFFE02 (Desert Obelisk Side 3) - 0x3351D - True
-159703 - 0xFFE03 (Desert Obelisk Side 4) - 0x0053C & 0x00771 & 0x335C8 & 0x335C9 & 0x337F8 & 0x037BB & 0x220E4 & 0x220E5 - True
-159704 - 0xFFE04 (Desert Obelisk Side 5) - 0x334B9 & 0x334BC & 0x22106 & 0x0A14C & 0x0A14D - True
-159710 - 0xFFE10 (Monastery Obelisk Side 1) - 0x03ABC & 0x03ABE & 0x03AC0 & 0x03AC4 - True
-159711 - 0xFFE11 (Monastery Obelisk Side 2) - 0x03AC5 - True
-159712 - 0xFFE12 (Monastery Obelisk Side 3) - 0x03BE2 & 0x03BE3 & 0x0A409 - True
-159713 - 0xFFE13 (Monastery Obelisk Side 4) - 0x006E5 & 0x006E6 & 0x006E7 & 0x034A7 & 0x034AD & 0x034AF & 0x03DAB & 0x03DAC & 0x03DAD - True
-159714 - 0xFFE14 (Monastery Obelisk Side 5) - 0x03E01 - True
-159715 - 0xFFE15 (Monastery Obelisk Side 6) - 0x289F4 & 0x289F5 - True
-159720 - 0xFFE20 (Treehouse Obelisk Side 1) - 0x0053D & 0x0053E & 0x00769 - True
-159721 - 0xFFE21 (Treehouse Obelisk Side 2) - 0x33721 & 0x220A7 & 0x220BD - True
-159722 - 0xFFE22 (Treehouse Obelisk Side 3) - 0x03B22 & 0x03B23 & 0x03B24 & 0x03B25 & 0x03A79 & 0x28ABD & 0x28ABE - True
-159723 - 0xFFE23 (Treehouse Obelisk Side 4) - 0x3388F & 0x28B29 & 0x28B2A - True
-159724 - 0xFFE24 (Treehouse Obelisk Side 5) - 0x018B6 & 0x033BE & 0x033BF & 0x033DD & 0x033E5 - True
-159725 - 0xFFE25 (Treehouse Obelisk Side 6) - 0x28AE9 & 0x3348F - True
-159730 - 0xFFE30 (River Obelisk Side 1) - 0x001A3 & 0x335AE - True
-159731 - 0xFFE31 (River Obelisk Side 2) - 0x000D3 & 0x035F5 & 0x09D5D & 0x09D5E & 0x09D63 - True
-159732 - 0xFFE32 (River Obelisk Side 3) - 0x3370E & 0x035DE & 0x03601 & 0x03603 & 0x03D0D & 0x3369A & 0x336C8 & 0x33505 - True
-159733 - 0xFFE33 (River Obelisk Side 4) - 0x03A9E & 0x016B2 & 0x3365F & 0x03731 & 0x036CE & 0x03C07 & 0x03A93 - True
-159734 - 0xFFE34 (River Obelisk Side 5) - 0x03AA6 & 0x3397C & 0x0105D & 0x0A304 - True
-159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True
-159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True
-159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True
-159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True
-159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True
-159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True
-159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True
-159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True
-159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True
-159753 - 0xFFE53 (Town Obelisk Side 4) - 0x28B30 & 0x035C9 - True
-159754 - 0xFFE54 (Town Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True
-159755 - 0xFFE55 (Town Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True
diff --git a/worlds/witness/WitnessLogicVanilla.txt b/worlds/witness/data/WitnessLogicVanilla.txt
similarity index 78%
rename from worlds/witness/WitnessLogicVanilla.txt
rename to worlds/witness/data/WitnessLogicVanilla.txt
index 84e73e68a53c..851031ab72f0 100644
--- a/worlds/witness/WitnessLogicVanilla.txt
+++ b/worlds/witness/data/WitnessLogicVanilla.txt
@@ -1,10 +1,14 @@
+==Tutorial (Inside)==
+
+Menu (Menu) - Entry - True:
+
Entry (Entry):
-First Hallway (First Hallway) - Entry - True - First Hallway Room - 0x00064:
+Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064:
158000 - 0x00064 (Straight) - True - True
159510 - 0x01848 (EP) - 0x00064 - True
-First Hallway Room (First Hallway) - Tutorial - 0x00182:
+Tutorial First Hallway Room (Tutorial First Hallway) - Tutorial - 0x00182:
158001 - 0x00182 (Bend) - True - True
Tutorial (Tutorial) - Outside Tutorial - 0x03629:
@@ -21,9 +25,11 @@ Tutorial (Tutorial) - Outside Tutorial - 0x03629:
159513 - 0x33600 (Patio Flowers EP) - 0x0C373 - True
159517 - 0x3352F (Gate EP) - 0x03505 - True
-Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2:
-158650 - 0x033D4 (Vault) - True - Dots & Black/White Squares
-158651 - 0x03481 (Vault Box) - 0x033D4 - True
+==Tutorial (Outside)==
+
+Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2 - Outside Tutorial Vault - 0x033D0:
+158650 - 0x033D4 (Vault Panel) - True - Dots & Black/White Squares
+Door - 0x033D0 (Vault Door) - 0x033D4
158013 - 0x0005D (Shed Row 1) - True - Dots
158014 - 0x0005E (Shed Row 2) - 0x0005D - Dots
158015 - 0x0005F (Shed Row 3) - 0x0005E - Dots
@@ -44,6 +50,9 @@ Door - 0x03BA2 (Outpost Path) - 0x0A3B5
159516 - 0x334A3 (Path EP) - True - True
159500 - 0x035C7 (Tractor EP) - True - True
+Outside Tutorial Vault (Outside Tutorial):
+158651 - 0x03481 (Vault Box) - True - True
+
Outside Tutorial Path To Outpost (Outside Tutorial) - Outside Tutorial Outpost - 0x0A170:
158011 - 0x0A171 (Outpost Entry Panel) - True - Dots & Full Dots
Door - 0x0A170 (Outpost Entry) - 0x0A171
@@ -53,8 +62,23 @@ Outside Tutorial Outpost (Outside Tutorial) - Outside Tutorial - 0x04CA3:
Door - 0x04CA3 (Outpost Exit) - 0x04CA4
158600 - 0x17CFB (Discard) - True - Triangles
+Orchard (Orchard) - Main Island - True - Orchard Beyond First Gate - 0x03307:
+158071 - 0x00143 (Apple Tree 1) - True - True
+158072 - 0x0003B (Apple Tree 2) - 0x00143 - True
+158073 - 0x00055 (Apple Tree 3) - 0x0003B - True
+Door - 0x03307 (First Gate) - 0x00055
+
+Orchard Beyond First Gate (Orchard) - Orchard End - 0x03313:
+158074 - 0x032F7 (Apple Tree 4) - 0x00055 - True
+158075 - 0x032FF (Apple Tree 5) - 0x032F7 - True
+Door - 0x03313 (Second Gate) - 0x032FF
+
+Orchard End (Orchard):
+
Main Island (Main Island) - Outside Tutorial - True:
-159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True
+159801 - 0xFFD00 (Reached Independently) - True - True
+
+==Glass Factory==
Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29:
158027 - 0x01A54 (Entry Panel) - True - Symmetry
@@ -76,9 +100,11 @@ Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0
158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry
Door - 0x0D7ED (Back Wall) - 0x0005C
-Inside Glass Factory Behind Back Wall (Glass Factory) - Boat - 0x17CC8:
+Inside Glass Factory Behind Back Wall (Glass Factory) - The Ocean - 0x17CC8:
158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat
+==Symmetry Island==
+
Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E:
158040 - 0x000B0 (Lower Panel) - 0x0343A - Dots
Door - 0x17F3E (Lower) - 0x000B0
@@ -112,32 +138,29 @@ Door - 0x18269 (Upper) - 0x1C349
159000 - 0x0332B (Glass Factory Black Line Reflection EP) - True - True
Symmetry Island Upper (Symmetry Island):
-158065 - 0x00A52 (Yellow 1) - True - Symmetry & Colored Dots
-158066 - 0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots
-158067 - 0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots
-158068 - 0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots
-158069 - 0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots
-158070 - 0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots
+158065 - 0x00A52 (Laser Yellow 1) - True - Symmetry & Colored Dots
+158066 - 0x00A57 (Laser Yellow 2) - 0x00A52 - Symmetry & Colored Dots
+158067 - 0x00A5B (Laser Yellow 3) - 0x00A57 - Symmetry & Colored Dots
+158068 - 0x00A61 (Laser Blue 1) - 0x00A52 - Symmetry & Colored Dots
+158069 - 0x00A64 (Laser Blue 2) - 0x00A61 & 0x00A57 - Symmetry & Colored Dots
+158070 - 0x00A68 (Laser Blue 3) - 0x00A64 & 0x00A5B - Symmetry & Colored Dots
158700 - 0x0360D (Laser Panel) - 0x00A68 - True
Laser - 0x00509 (Laser) - 0x0360D
159001 - 0x03367 (Glass Factory Black Line EP) - True - True
-Orchard (Orchard) - Main Island - True - Orchard Beyond First Gate - 0x03307:
-158071 - 0x00143 (Apple Tree 1) - True - True
-158072 - 0x0003B (Apple Tree 2) - 0x00143 - True
-158073 - 0x00055 (Apple Tree 3) - 0x0003B - True
-Door - 0x03307 (First Gate) - 0x00055
-
-Orchard Beyond First Gate (Orchard) - Orchard End - 0x03313:
-158074 - 0x032F7 (Apple Tree 4) - 0x00055 - True
-158075 - 0x032FF (Apple Tree 5) - 0x032F7 - True
-Door - 0x03313 (Second Gate) - 0x032FF
+==Desert==
-Orchard End (Orchard):
+Desert Obelisk (Desert) - Entry - True:
+159700 - 0xFFE00 (Obelisk Side 1) - 0x0332B & 0x03367 & 0x28B8A - True
+159701 - 0xFFE01 (Obelisk Side 2) - 0x037B6 & 0x037B2 & 0x000F7 - True
+159702 - 0xFFE02 (Obelisk Side 3) - 0x3351D - True
+159703 - 0xFFE03 (Obelisk Side 4) - 0x0053C & 0x00771 & 0x335C8 & 0x335C9 & 0x337F8 & 0x037BB & 0x220E4 & 0x220E5 - True
+159704 - 0xFFE04 (Obelisk Side 5) - 0x334B9 & 0x334BC & 0x22106 & 0x0A14C & 0x0A14D - True
+159709 - 0x00359 (Obelisk) - True - True
-Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE:
-158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots
-158653 - 0x0339E (Vault Box) - 0x0CC7B - True
+Desert Outside (Desert) - Main Island - True - Desert Light Room - 0x09FEE - Desert Vault - 0x03444:
+158652 - 0x0CC7B (Vault Panel) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots
+Door - 0x03444 (Vault Door) - 0x0CC7B
158602 - 0x17CE7 (Discard) - True - Triangles
158076 - 0x00698 (Surface 1) - True - True
158077 - 0x0048F (Surface 2) - 0x00698 - True
@@ -163,14 +186,17 @@ Laser - 0x012FB (Laser) - 0x03608
159040 - 0x334B9 (Shore EP) - True - True
159041 - 0x334BC (Island EP) - True - True
-Desert Floodlight Room (Desert) - Desert Pond Room - 0x0C2C3:
+Desert Vault (Desert):
+158653 - 0x0339E (Vault Box) - True - True
+
+Desert Light Room (Desert) - Desert Pond Room - 0x0C2C3:
158087 - 0x09FAA (Light Control) - True - True
158088 - 0x00422 (Light Room 1) - 0x09FAA - True
158089 - 0x006E3 (Light Room 2) - 0x09FAA - True
158090 - 0x0A02D (Light Room 3) - 0x09FAA & 0x00422 & 0x006E3 - True
Door - 0x0C2C3 (Pond Room Entry) - 0x0A02D
-Desert Pond Room (Desert) - Desert Water Levels Room - 0x0A24B:
+Desert Pond Room (Desert) - Desert Flood Room - 0x0A24B:
158091 - 0x00C72 (Pond Room 1) - True - True
158092 - 0x0129D (Pond Room 2) - 0x00C72 - True
158093 - 0x008BB (Pond Room 3) - 0x0129D - True
@@ -181,7 +207,7 @@ Door - 0x0A24B (Flood Room Entry) - 0x0A249
159043 - 0x0A14C (Pond Room Near Reflection EP) - True - True
159044 - 0x0A14D (Pond Room Far Reflection EP) - True - True
-Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316:
+Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316:
158097 - 0x1C2DF (Reduce Water Level Far Left) - True - True
158098 - 0x1831E (Reduce Water Level Far Right) - True - True
158099 - 0x1C260 (Reduce Water Level Near Left) - True - True
@@ -199,18 +225,29 @@ Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316:
Door - 0x0C316 (Elevator Room Entry) - 0x18076
159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True
-Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB:
-158111 - 0x17C31 (Final Transparent) - True - True
-158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True
-158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True
-158115 - 0x0A15C (Final Bent 1) - True - True
-158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True
-158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True
-159035 - 0x037BB (Elevator EP) - 0x012FB - True
-
-Desert Lowest Level Inbetween Shortcuts (Desert):
-
-Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - TrueOneWay:
+Desert Elevator Room (Desert) - Desert Behind Elevator - 0x01317:
+158111 - 0x17C31 (Elevator Room Transparent) - True - True
+158113 - 0x012D7 (Elevator Room Hexagonal) - 0x17C31 & 0x0A015 - True
+158114 - 0x0A015 (Elevator Room Hexagonal Control) - 0x17C31 - True
+158115 - 0x0A15C (Elevator Room Bent 1) - True - True
+158116 - 0x09FFF (Elevator Room Bent 2) - 0x0A15C - True
+158117 - 0x0A15F (Elevator Room Bent 3) - 0x09FFF - True
+159035 - 0x037BB (Elevator EP) - 0x01317 - True
+Door - 0x01317 (Elevator) - 0x03608
+
+Desert Behind Elevator (Desert):
+
+==Quarry==
+
+Quarry Obelisk (Quarry) - Entry - True:
+159740 - 0xFFE40 (Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True
+159741 - 0xFFE41 (Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True
+159742 - 0xFFE42 (Obelisk Side 3) - 0x289CF & 0x289D1 - True
+159743 - 0xFFE43 (Obelisk Side 4) - 0x33692 - True
+159744 - 0xFFE44 (Obelisk Side 5) - 0x03E77 & 0x03E7C - True
+159749 - 0x22073 (Obelisk) - True - True
+
+Outside Quarry (Quarry) - Main Island - True - Quarry Between Entry Doors - 0x09D6F - Quarry Elevator - 0xFFD00 & 0xFFD01:
158118 - 0x09E57 (Entry 1 Panel) - True - Black/White Squares
158603 - 0x17CF0 (Discard) - True - Triangles
158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Shapers
@@ -222,36 +259,39 @@ Door - 0x09D6F (Entry 1) - 0x09E57
159420 - 0x289CF (Rock Line EP) - True - True
159421 - 0x289D1 (Rock Line Reflection EP) - True - True
-Quarry Elevator (Quarry):
+Quarry Elevator (Quarry) - Outside Quarry - 0x17CC4 - Quarry - 0x17CC4:
158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser
159403 - 0x17CB9 (Railroad EP) - 0x17CC4 - True
-Quarry Between Entrys (Quarry) - Quarry - 0x17C07:
+Quarry Between Entry Doors (Quarry) - Quarry - 0x17C07:
158119 - 0x17C09 (Entry 2 Panel) - True - Shapers
Door - 0x17C07 (Entry 2) - 0x17C09
-Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010 - Quarry Elevator - 0x17CC4:
+Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010:
+159802 - 0xFFD01 (Inside Reached Independently) - True - True
158121 - 0x01E5A (Stoneworks Entry Left Panel) - True - Black/White Squares
158122 - 0x01E59 (Stoneworks Entry Right Panel) - True - Dots
Door - 0x02010 (Stoneworks Entry) - 0x01E59 & 0x01E5A
-Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8:
+Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8 - Quarry Stoneworks Lift - TrueOneWay:
158123 - 0x275ED (Side Exit Panel) - True - True
Door - 0x275FF (Side Exit) - 0x275ED
158124 - 0x03678 (Lower Ramp Control) - True - Dots & Eraser
158145 - 0x17CAC (Roof Exit Panel) - True - True
Door - 0x17CE8 (Roof Exit) - 0x17CAC
-Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Ground Floor - 0x03675 - Quarry Stoneworks Upper Floor - 0x03679:
+Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - TrueOneWay:
158125 - 0x00E0C (Lower Row 1) - True - Dots & Eraser
158126 - 0x01489 (Lower Row 2) - 0x00E0C - Dots & Eraser
158127 - 0x0148A (Lower Row 3) - 0x01489 - Dots & Eraser
-158128 - 0x014D9 (Lower Row 4) - 0x0148A - Dots & Eraser
+158128 - 0x014D9 (Lower Row 4) - 0x0148A - Dots & Full Dots & Eraser
158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Dots
158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Dots & Eraser
+
+Quarry Stoneworks Lift (Quarry Stoneworks) - Quarry Stoneworks Middle Floor - 0x03679 - Quarry Stoneworks Ground Floor - 0x03679 - Quarry Stoneworks Upper Floor - 0x03679:
158131 - 0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser
-Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Floor - 0x03676 & 0x03679 - Quarry Stoneworks Ground Floor - 0x0368A:
+Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - 0x03675 - Quarry Stoneworks Ground Floor - 0x0368A:
158132 - 0x03676 (Upper Ramp Control) - True - Dots & Eraser
158133 - 0x03675 (Upper Lift Control) - True - Dots & Eraser
158134 - 0x00557 (Upper Row 1) - True - Colored Squares & Eraser
@@ -262,7 +302,7 @@ Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Flo
158139 - 0x3C12D (Upper Row 6) - 0x0146C - Colored Squares & Eraser
158140 - 0x03686 (Upper Row 7) - 0x3C12D - Colored Squares & Eraser
158141 - 0x014E9 (Upper Row 8) - 0x03686 - Colored Squares & Eraser
-158142 - 0x03677 (Stair Control) - True - Colored Squares & Eraser
+158142 - 0x03677 (Stairs Panel) - True - Colored Squares & Eraser
Door - 0x0368A (Stairs) - 0x03677
158143 - 0x3C125 (Control Room Left) - 0x014E9 - Black/White Squares & Dots & Eraser
158144 - 0x0367C (Control Room Right) - 0x014E9 - Colored Squares & Dots & Eraser
@@ -277,7 +317,7 @@ Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Fro
Door - 0x2769B (Dock) - 0x17CA6
Door - 0x27163 (Dock Invis Barrier) - 0x17CA6
-Quarry Boathouse Behind Staircase (Quarry Boathouse) - Boat - 0x17CA6:
+Quarry Boathouse Behind Staircase (Quarry Boathouse) - The Ocean - 0x17CA6:
Quarry Boathouse Upper Front (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x17C50:
158149 - 0x021B3 (Front Row 1) - True - Shapers & Eraser
@@ -294,9 +334,9 @@ Quarry Boathouse Upper Middle (Quarry Boathouse) - Quarry Boathouse Upper Back -
Quarry Boathouse Upper Back (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x3865F:
158155 - 0x38663 (Second Barrier Panel) - True - True
Door - 0x3865F (Second Barrier) - 0x38663
-158156 - 0x021B5 (Back First Row 1) - True - Stars & Stars + Same Colored Symbol & Eraser
+158156 - 0x021B5 (Back First Row 1) - True - Stars & Eraser
158157 - 0x021B6 (Back First Row 2) - 0x021B5 - Stars & Stars + Same Colored Symbol & Eraser
-158158 - 0x021B7 (Back First Row 3) - 0x021B6 - Stars & Stars + Same Colored Symbol & Eraser
+158158 - 0x021B7 (Back First Row 3) - 0x021B6 - Stars & Eraser
158159 - 0x021BB (Back First Row 4) - 0x021B7 - Stars & Stars + Same Colored Symbol & Eraser
158160 - 0x09DB5 (Back First Row 5) - 0x021BB - Stars & Stars + Same Colored Symbol & Eraser
158161 - 0x09DB1 (Back First Row 6) - 0x09DB5 - Stars & Stars + Same Colored Symbol & Eraser
@@ -309,9 +349,11 @@ Door - 0x3865F (Second Barrier) - 0x38663
158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Stars & Eraser & Shapers
159401 - 0x005F6 (Hook EP) - 0x275FA & 0x03852 & 0x3865F - True
-Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 & 0x19665:
+==Shadows==
+
+Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 | 0x19665:
158170 - 0x334DB (Door Timer Outside) - True - True
-Door - 0x19B24 (Timed Door) - 0x334DB
+Door - 0x19B24 (Timed Door) - 0x334DB | 0x334DC
158171 - 0x0AC74 (Intro 6) - 0x0A8DC - True
158172 - 0x0AC7A (Intro 7) - 0x0AC74 - True
158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - True
@@ -336,7 +378,7 @@ Shadows Ledge (Shadows) - Shadows - 0x1855B - Quarry - 0x19865 & 0x0A2DF:
158187 - 0x334DC (Door Timer Inside) - True - True
158188 - 0x198B5 (Intro 1) - True - True
158189 - 0x198BD (Intro 2) - 0x198B5 - True
-158190 - 0x198BF (Intro 3) - 0x198BD & 0x334DC & 0x19B24 - True
+158190 - 0x198BF (Intro 3) - 0x198BD & 0x19B24 - True
Door - 0x19865 (Quarry Barrier) - 0x198BF
Door - 0x0A2DF (Quarry Barrier 2) - 0x198BF
158191 - 0x19771 (Intro 4) - 0x198BF - True
@@ -345,22 +387,21 @@ Door - 0x1855B (Ledge Barrier) - 0x0A8DC
Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC
Shadows Laser Room (Shadows):
-158703 - 0x19650 (Laser Panel) - True - True
+158703 - 0x19650 (Laser Panel) - 0x194B2 & 0x19665 - True
Laser - 0x181B3 (Laser) - 0x19650
-Treehouse Beach (Treehouse Beach) - Main Island - True:
-159200 - 0x0053D (Rock Shadow EP) - True - True
-159201 - 0x0053E (Sand Shadow EP) - True - True
-159212 - 0x220BD (Both Orange Bridges EP) - 0x17DA2 & 0x17DDB - True
+==Keep==
-Keep (Keep) - Main Island - True - Keep 2nd Maze - 0x01954 - Keep 2nd Pressure Plate - 0x01BEC:
+Outside Keep (Keep) - Main Island - True:
+159430 - 0x03E77 (Red Flowers EP) - True - True
+159431 - 0x03E7C (Purple Flowers EP) - True - True
+
+Keep (Keep) - Outside Keep - True - Keep 2nd Maze - 0x01954 - Keep 2nd Pressure Plate - 0x01BEC:
158193 - 0x00139 (Hedge Maze 1) - True - True
158197 - 0x0A3A8 (Reset Pressure Plates 1) - True - True
158198 - 0x033EA (Pressure Plates 1) - 0x0A3A8 - Dots
Door - 0x01954 (Hedge Maze 1 Exit) - 0x00139
Door - 0x01BEC (Pressure Plates 1 Exit) - 0x033EA
-159430 - 0x03E77 (Red Flowers EP) - True - True
-159431 - 0x03E7C (Purple Flowers EP) - True - True
Keep 2nd Maze (Keep) - Keep - 0x018CE - Keep 3rd Maze - 0x019D8:
Door - 0x018CE (Hedge Maze 2 Shortcut) - 0x00139
@@ -395,37 +436,53 @@ Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F
158205 - 0x09E49 (Shadows Shortcut Panel) - True - True
Door - 0x09E3D (Shadows Shortcut) - 0x09E49
-Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True:
-158654 - 0x00AFB (Vault) - True - Symmetry & Sound Dots & Colored Dots
-158655 - 0x03535 (Vault Box) - 0x00AFB - True
-158605 - 0x17D28 (Discard) - True - Triangles
-159220 - 0x03B22 (Circle Far EP) - True - True
-159221 - 0x03B23 (Circle Left EP) - True - True
-159222 - 0x03B24 (Circle Near EP) - True - True
-159224 - 0x03A79 (Stern EP) - True - True
-159225 - 0x28ABD (Rope Inner EP) - True - True
-159226 - 0x28ABE (Rope Outer EP) - True - True
-159230 - 0x3388F (Couch EP) - 0x17CDF | 0x0A054 - True
-
Keep Tower (Keep) - Keep - 0x04F8F:
158206 - 0x0361B (Tower Shortcut Panel) - True - True
Door - 0x04F8F (Tower Shortcut) - 0x0361B
158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - True
-158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Shapers & Black/White Squares & Rotated Shapers
+158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Dots & Shapers & Black/White Squares & Rotated Shapers
Laser - 0x014BB (Laser) - 0x0360E | 0x03317
159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True
-159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True
+159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 & 0x01BEA - True
159242 - 0x033DD (Pressure Plates 3 EP) - 0x01CD3 & 0x01CD5 - True
159243 - 0x033E5 (Pressure Plates 4 Left Exit EP) - 0x01D3F - True
159244 - 0x018B6 (Pressure Plates 4 Right Exit EP) - 0x01D3F - True
159250 - 0x28AE9 (Path EP) - True - True
159251 - 0x3348F (Hedges EP) - True - True
+==Shipwreck==
+
+Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True - Shipwreck Vault - 0x17BB4:
+158654 - 0x00AFB (Vault Panel) - True - Symmetry & Sound Dots & Colored Dots
+Door - 0x17BB4 (Vault Door) - 0x00AFB
+158605 - 0x17D28 (Discard) - True - Triangles
+159220 - 0x03B22 (Circle Far EP) - True - True
+159221 - 0x03B23 (Circle Left EP) - True - True
+159222 - 0x03B24 (Circle Near EP) - True - True
+159224 - 0x03A79 (Stern EP) - True - True
+159225 - 0x28ABD (Rope Inner EP) - True - True
+159226 - 0x28ABE (Rope Outer EP) - True - True
+159230 - 0x3388F (Couch EP) - 0x17CDF | 0x0A054 - True
+
+Shipwreck Vault (Shipwreck):
+158655 - 0x03535 (Vault Box) - True - True
+
+==Monastery==
+
+Monastery Obelisk (Monastery) - Entry - True:
+159710 - 0xFFE10 (Obelisk Side 1) - 0x03ABC & 0x03ABE & 0x03AC0 & 0x03AC4 - True
+159711 - 0xFFE11 (Obelisk Side 2) - 0x03AC5 - True
+159712 - 0xFFE12 (Obelisk Side 3) - 0x03BE2 & 0x03BE3 & 0x0A409 - True
+159713 - 0xFFE13 (Obelisk Side 4) - 0x006E5 & 0x006E6 & 0x006E7 & 0x034A7 & 0x034AD & 0x034AF & 0x03DAB & 0x03DAC & 0x03DAD - True
+159714 - 0xFFE14 (Obelisk Side 5) - 0x03E01 - True
+159715 - 0xFFE15 (Obelisk Side 6) - 0x289F4 & 0x289F5 - True
+159719 - 0x00263 (Obelisk) - True - True
+
Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750:
-158207 - 0x03713 (Shortcut Panel) - True - True
-Door - 0x0364E (Shortcut) - 0x03713
+158207 - 0x03713 (Laser Shortcut Panel) - True - True
+Door - 0x0364E (Laser Shortcut) - 0x03713
158208 - 0x00B10 (Entry Left) - True - True
-158209 - 0x00C92 (Entry Right) - True - True
+158209 - 0x00C92 (Entry Right) - 0x00B10 - True
Door - 0x0C128 (Entry Inner) - 0x00B10
Door - 0x0C153 (Entry Outer) - 0x00C92
158210 - 0x00290 (Outside 1) - 0x09D9B - True
@@ -441,6 +498,9 @@ Laser - 0x17C65 (Laser) - 0x17CA4
159137 - 0x03DAC (Facade Left Stairs EP) - True - True
159138 - 0x03DAD (Facade Right Stairs EP) - True - True
159140 - 0x03E01 (Grass Stairs EP) - True - True
+159120 - 0x03BE2 (Garden Left EP) - 0x03750 - True
+159121 - 0x03BE3 (Garden Right EP) - True - True
+159122 - 0x0A409 (Wall EP) - True - True
Inside Monastery (Monastery):
158213 - 0x09D9B (Shutters Control) - True - Dots
@@ -454,11 +514,22 @@ Inside Monastery (Monastery):
Monastery Garden (Monastery):
-Town (Town) - Main Island - True - Boat - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9:
+==Town==
+
+Town Obelisk (Town) - Entry - True:
+159750 - 0xFFE50 (Obelisk Side 1) - 0x035C7 - True
+159751 - 0xFFE51 (Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True
+159752 - 0xFFE52 (Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True
+159753 - 0xFFE53 (Obelisk Side 4) - 0x28B30 & 0x035C9 - True
+159754 - 0xFFE54 (Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True
+159755 - 0xFFE55 (Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True
+159759 - 0x0A16C (Obelisk) - True - True
+
+Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - Town RGB House - 0x28A61 - Town Inside Cargo Box - 0x0A0C9 - Outside Windmill - True:
158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat
158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Black/White Squares & Shapers
Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8
-158707 - 0x09F98 (Desert Laser Redirect) - True - True
+158707 - 0x09F98 (Desert Laser Redirect Control) - True - True
158220 - 0x18590 (Transparent) - True - Symmetry
158221 - 0x28AE3 (Vines) - 0x18590 - True
158222 - 0x28938 (Apple Tree) - 0x28AE3 - True
@@ -469,17 +540,12 @@ Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8
158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Rotated Shapers & Dots & Full Dots
158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Rotated Shapers & Dots & Full Dots
Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1
-158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers
-Door - 0x28A61 (Tinted Glass Door) - 0x28998
+158225 - 0x28998 (RGB House Entry Panel) - True - Stars & Rotated Shapers
+Door - 0x28A61 (RGB House Entry) - 0x28998
158226 - 0x28A0D (Church Entry Panel) - 0x28A61 - Stars
Door - 0x03BB0 (Church Entry) - 0x28A0D
-158228 - 0x28A79 (Maze Stair Control) - True - True
+158228 - 0x28A79 (Maze Panel) - True - True
Door - 0x28AA2 (Maze Stairs) - 0x28A79
-158241 - 0x17F5F (Windmill Entry Panel) - True - Dots
-Door - 0x1845B (Windmill Entry) - 0x17F5F
-159010 - 0x037B6 (Windmill First Blade EP) - 0x17D02 - True
-159011 - 0x037B2 (Windmill Second Blade EP) - 0x17D02 - True
-159012 - 0x000F7 (Windmill Third Blade EP) - 0x17D02 - True
159540 - 0x03335 (Tower Underside Third EP) - True - True
159541 - 0x03412 (Tower Underside Fourth EP) - True - True
159542 - 0x038A6 (Tower Underside First EP) - True - True
@@ -500,32 +566,38 @@ Town Red Rooftop (Town):
158607 - 0x17C71 (Rooftop Discard) - True - Triangles
158230 - 0x28AC7 (Red Rooftop 1) - True - Symmetry & Black/White Squares
158231 - 0x28AC8 (Red Rooftop 2) - 0x28AC7 - Symmetry & Black/White Squares
-158232 - 0x28ACA (Red Rooftop 3) - 0x28AC8 - Symmetry & Black/White Squares & Dots
+158232 - 0x28ACA (Red Rooftop 3) - 0x28AC8 - Symmetry & Black/White Squares
158233 - 0x28ACB (Red Rooftop 4) - 0x28ACA - Symmetry & Black/White Squares & Dots
158234 - 0x28ACC (Red Rooftop 5) - 0x28ACB - Symmetry & Black/White Squares & Dots
158224 - 0x28B39 (Tall Hexagonal) - 0x079DF - True
Town Wooden Rooftop (Town):
-158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Rotated Shapers & Dots & Eraser & Full Dots
+158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Shapers & Dots & Eraser & Full Dots
Town Church (Town):
158227 - 0x28A69 (Church Lattice) - 0x03BB0 - True
159553 - 0x03BD1 (Black Line Church EP) - True - True
-RGB House (Town) - RGB Room - 0x2897B:
+Town RGB House (Town RGB House) - Town RGB House Upstairs - 0x2897B:
158242 - 0x034E4 (Sound Room Left) - True - True
158243 - 0x034E3 (Sound Room Right) - True - Sound Dots
-Door - 0x2897B (RGB House Stairs) - 0x034E4 & 0x034E3
+Door - 0x2897B (Stairs) - 0x034E4 & 0x034E3
-RGB Room (Town):
+Town RGB House Upstairs (Town RGB House Upstairs):
158244 - 0x334D8 (RGB Control) - True - Rotated Shapers & Colored Squares
-158245 - 0x03C0C (RGB Room Left) - 0x334D8 - Colored Squares & Black/White Squares
-158246 - 0x03C08 (RGB Room Right) - 0x334D8 - Stars
+158245 - 0x03C0C (Left) - 0x334D8 - Colored Squares & Black/White Squares
+158246 - 0x03C08 (Right) - 0x334D8 - Stars
+
+Town Tower Bottom (Town Tower) - Town - True - Town Tower After First Door - 0x27799:
+Door - 0x27799 (First Door) - 0x28A69
-Town Tower (Town Tower) - Town - True - Town Tower Top - 0x27798 & 0x27799 & 0x2779A & 0x2779C:
+Town Tower After First Door (Town Tower) - Town Tower After Second Door - 0x27798:
Door - 0x27798 (Second Door) - 0x28ACC
+
+Town Tower After Second Door (Town Tower) - Town Tower After Third Door - 0x2779C:
Door - 0x2779C (Third Door) - 0x28AD9
-Door - 0x27799 (First Door) - 0x28A69
+
+Town Tower After Third Door (Town Tower) - Town Tower Top - 0x2779A:
Door - 0x2779A (Fourth Door) - 0x28B39
Town Tower Top (Town):
@@ -534,6 +606,15 @@ Laser - 0x032F9 (Laser) - 0x032F5
159422 - 0x33692 (Brown Bridge EP) - True - True
159551 - 0x03BCE (Black Line Tower EP) - True - True
+==Windmill & Theater==
+
+Outside Windmill (Windmill) - Windmill Interior - 0x1845B:
+159010 - 0x037B6 (First Blade EP) - 0x17D02 - True
+159011 - 0x037B2 (Second Blade EP) - 0x17D02 - True
+159012 - 0x000F7 (Third Blade EP) - 0x17D02 - True
+158241 - 0x17F5F (Entry Panel) - True - Dots
+Door - 0x1845B (Entry) - 0x17F5F
+
Windmill Interior (Windmill) - Theater - 0x17F88:
158247 - 0x17D02 (Turn Control) - True - Dots
158248 - 0x17F89 (Theater Entry Panel) - True - Black/White Squares
@@ -557,7 +638,9 @@ Door - 0x3CCDF (Exit Right) - 0x33AB2
159556 - 0x33A2A (Door EP) - 0x03553 - True
159558 - 0x33B06 (Church EP) - 0x0354E - True
-Jungle (Jungle) - Main Island - True - Outside Jungle River - 0x3873B - Boat - 0x17CDF:
+==Jungle==
+
+Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF:
158251 - 0x17CDF (Shore Boat Spawn) - True - Boat
158609 - 0x17F9B (Discard) - True - Triangles
158252 - 0x002C4 (First Row 1) - True - True
@@ -588,18 +671,20 @@ Door - 0x3873B (Laser Shortcut) - 0x337FA
159350 - 0x035CB (Bamboo CCW EP) - True - True
159351 - 0x035CF (Bamboo CW EP) - True - True
-Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A:
-158267 - 0x17CAA (Monastery Shortcut Panel) - True - True
-Door - 0x0CF2A (Monastery Shortcut) - 0x17CAA
-158663 - 0x15ADD (Vault) - True - Black/White Squares & Dots
-158664 - 0x03702 (Vault Box) - 0x15ADD - True
+Outside Jungle River (Jungle) - Main Island - True - Monastery Garden - 0x0CF2A - Jungle Vault - 0x15287:
+158267 - 0x17CAA (Monastery Garden Shortcut Panel) - True - True
+Door - 0x0CF2A (Monastery Garden Shortcut) - 0x17CAA
+158663 - 0x15ADD (Vault Panel) - True - Black/White Squares & Dots
+Door - 0x15287 (Vault Door) - 0x15ADD
159110 - 0x03AC5 (Green Leaf Moss EP) - True - True
-159120 - 0x03BE2 (Monastery Garden Left EP) - 0x03750 - True
-159121 - 0x03BE3 (Monastery Garden Right EP) - True - True
-159122 - 0x0A409 (Monastery Wall EP) - True - True
+
+Jungle Vault (Jungle):
+158664 - 0x03702 (Vault Box) - True - True
+
+==Bunker==
Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4:
-158268 - 0x17C2E (Entry Panel) - True - Black/White Squares & Colored Squares
+158268 - 0x17C2E (Entry Panel) - True - Black/White Squares
Door - 0x0C2A4 (Entry) - 0x17C2E
Bunker (Bunker) - Bunker Glass Room - 0x17C79:
@@ -616,9 +701,9 @@ Bunker (Bunker) - Bunker Glass Room - 0x17C79:
Door - 0x17C79 (Tinted Glass Door) - 0x0A099
Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3:
-158279 - 0x0A010 (Glass Room 1) - True - Colored Squares
-158280 - 0x0A01B (Glass Room 2) - 0x0A010 - Colored Squares & Black/White Squares
-158281 - 0x0A01F (Glass Room 3) - 0x0A01B - Colored Squares & Black/White Squares
+158279 - 0x0A010 (Glass Room 1) - 0x17C79 - Colored Squares
+158280 - 0x0A01B (Glass Room 2) - 0x17C79 & 0x0A010 - Colored Squares & Black/White Squares
+158281 - 0x0A01F (Glass Room 3) - 0x17C79 & 0x0A01B - Colored Squares & Black/White Squares
Door - 0x0C2A3 (UV Room Entry) - 0x0A01F
Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D:
@@ -631,9 +716,11 @@ Door - 0x0A08D (Elevator Room Entry) - 0x17E67
Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay:
159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True
-Bunker Elevator (Bunker) - Bunker Laser Platform - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079:
+Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Cyan Room - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079:
158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares
+Bunker Cyan Room (Bunker) - Bunker Elevator - TrueOneWay:
+
Bunker Green Room (Bunker) - Bunker Elevator - TrueOneWay:
159310 - 0x000D3 (Green Room Flowers EP) - True - True
@@ -641,6 +728,8 @@ Bunker Laser Platform (Bunker) - Bunker Elevator - TrueOneWay:
158710 - 0x09DE0 (Laser Panel) - True - True
Laser - 0x0C2B2 (Laser) - 0x09DE0
+==Swamp==
+
Outside Swamp (Swamp) - Swamp Entry Area - 0x00C1C - Main Island - True:
158287 - 0x0056E (Entry Panel) - True - Shapers
Door - 0x00C1C (Entry) - 0x0056E
@@ -676,8 +765,8 @@ Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat
158316 - 0x00990 (Platform Row 4) - 0x0098F - Shapers
Door - 0x184B7 (Between Bridges First Door) - 0x00990
158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Shapers
-158318 - 0x17C0E (Platform Shortcut Right Panel) - True - Shapers
-Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E
+158318 - 0x17C0E (Platform Shortcut Right Panel) - 0x17C0D - Shapers
+Door - 0x38AE6 (Platform Shortcut) - 0x17C0E
Door - 0x04B7F (Cyan Water Pump) - 0x00006
Swamp Cyan Underwater (Swamp):
@@ -715,18 +804,21 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near
159331 - 0x016B2 (Rotating Bridge CCW EP) - 0x181F5 - True
159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True
-Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482:
+Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8:
+159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True
158328 - 0x09DB8 (Boat Spawn) - True - Boat
158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers
158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers
158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Rotated Shapers & Shapers
-158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers
-158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers
+158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers & Shapers
Door - 0x18482 (Blue Water Pump) - 0x00E3A
159332 - 0x3365F (Boat EP) - 0x09DB8 - True
159333 - 0x03731 (Long Bridge Side EP) - 0x17E2B - True
-Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6:
+Swamp Long Bridge (Swamp) - Swamp Near Boat - 0x17E2B - Outside Swamp - 0x17E2B:
+158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers
+
+Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6 - Swamp Near Boat - TrueOneWay:
Door - 0x0A1D6 (Purple Water Pump) - 0x00E3A
Swamp Purple Underwater (Swamp):
@@ -752,13 +844,29 @@ Laser - 0x00BF6 (Laser) - 0x03615
158342 - 0x17C02 (Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Rotated Shapers
Door - 0x2D880 (Laser Shortcut) - 0x17C02
-Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309:
+==Treehouse==
+
+Treehouse Obelisk (Treehouse) - Entry - True:
+159720 - 0xFFE20 (Obelisk Side 1) - 0x0053D & 0x0053E & 0x00769 - True
+159721 - 0xFFE21 (Obelisk Side 2) - 0x33721 & 0x220A7 & 0x220BD - True
+159722 - 0xFFE22 (Obelisk Side 3) - 0x03B22 & 0x03B23 & 0x03B24 & 0x03B25 & 0x03A79 & 0x28ABD & 0x28ABE - True
+159723 - 0xFFE23 (Obelisk Side 4) - 0x3388F & 0x28B29 & 0x28B2A - True
+159724 - 0xFFE24 (Obelisk Side 5) - 0x018B6 & 0x033BE & 0x033BF & 0x033DD & 0x033E5 - True
+159725 - 0xFFE25 (Obelisk Side 6) - 0x28AE9 & 0x3348F - True
+159729 - 0x00097 (Obelisk) - True - True
+
+Treehouse Beach (Treehouse Beach) - Main Island - True:
+159200 - 0x0053D (Rock Shadow EP) - True - True
+159201 - 0x0053E (Sand Shadow EP) - True - True
+159212 - 0x220BD (Both Orange Bridges EP) - 0x17DA2 & 0x17DDB - True
+
+Treehouse Entry Area (Treehouse) - Treehouse Between Entry Doors - 0x0C309 - The Ocean - 0x17C95:
158343 - 0x17C95 (Boat Spawn) - True - Boat
158344 - 0x0288C (First Door Panel) - True - Stars
Door - 0x0C309 (First Door) - 0x0288C
159210 - 0x33721 (Buoy EP) - 0x17C95 - True
-Treehouse Between Doors (Treehouse) - Treehouse Yellow Bridge - 0x0C310:
+Treehouse Between Entry Doors (Treehouse) - Treehouse Yellow Bridge - 0x0C310:
158345 - 0x02886 (Second Door Panel) - True - Stars
Door - 0x0C310 (Second Door) - 0x02886
@@ -778,7 +886,7 @@ Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181:
Door - 0x0A181 (Third Door) - 0x0A182
Treehouse Junction (Treehouse) - Treehouse Right Orange Bridge - True - Treehouse First Purple Bridge - True - Treehouse Green Bridge - True:
-158356 - 0x2700B (Laser House Door Timer Outside Control) - True - True
+158356 - 0x2700B (Laser House Door Timer Outside) - True - True
Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x17D6C:
158357 - 0x17DC8 (First Purple Bridge 1) - True - Stars & Dots
@@ -787,7 +895,7 @@ Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x1
158360 - 0x17D2D (First Purple Bridge 4) - 0x17CE4 - Stars & Dots
158361 - 0x17D6C (First Purple Bridge 5) - 0x17D2D - Stars & Dots
-Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2:
+Treehouse Right Orange Bridge (Treehouse) - Treehouse Drawbridge Platform - 0x17DA2:
158391 - 0x17D88 (Right Orange Bridge 1) - True - Stars
158392 - 0x17DB4 (Right Orange Bridge 2) - 0x17D88 - Stars
158393 - 0x17D8C (Right Orange Bridge 3) - 0x17DB4 - Stars
@@ -801,8 +909,8 @@ Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2:
158401 - 0x17DB1 (Right Orange Bridge 11) - 0x17DB7 - Stars
158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars
-Treehouse Bridge Platform (Treehouse) - Main Island - 0x0C32D:
-158404 - 0x037FF (Bridge Control) - True - Stars
+Treehouse Drawbridge Platform (Treehouse) - Main Island - 0x0C32D:
+158404 - 0x037FF (Drawbridge Panel) - True - Stars
Door - 0x0C32D (Drawbridge) - 0x037FF
Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6:
@@ -837,7 +945,7 @@ Treehouse Green Bridge (Treehouse) - Treehouse Green Bridge Front House - 0x17E6
158371 - 0x17E4F (Green Bridge 3) - 0x17E4D - Stars & Shapers & Rotated Shapers
158372 - 0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Rotated Shapers
158373 - 0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Stars + Same Colored Symbol
-158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol & Rotated Shapers
+158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Negative Shapers & Rotated Shapers
158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Rotated Shapers
Treehouse Green Bridge Front House (Treehouse):
@@ -847,7 +955,7 @@ Treehouse Green Bridge Left House (Treehouse):
159211 - 0x220A7 (Right Orange Bridge EP) - 0x17DA2 - True
Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323:
-Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DDB
+Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DDB | 0x17CBC
Treehouse Laser Room Back Platform (Treehouse):
158611 - 0x17FA0 (Laser Discard) - True - Triangles
@@ -860,28 +968,45 @@ Treehouse Laser Room (Treehouse):
158403 - 0x17CBC (Laser House Door Timer Inside) - True - True
Laser - 0x028A4 (Laser) - 0x03613
-Mountainside (Mountainside) - Main Island - True - Mountaintop - True:
+==Mountain (Outside)==
+
+Mountainside Obelisk (Mountainside) - Entry - True:
+159730 - 0xFFE30 (Obelisk Side 1) - 0x001A3 & 0x335AE - True
+159731 - 0xFFE31 (Obelisk Side 2) - 0x000D3 & 0x035F5 & 0x09D5D & 0x09D5E & 0x09D63 - True
+159732 - 0xFFE32 (Obelisk Side 3) - 0x3370E & 0x035DE & 0x03601 & 0x03603 & 0x03D0D & 0x3369A & 0x336C8 & 0x33505 - True
+159733 - 0xFFE33 (Obelisk Side 4) - 0x03A9E & 0x016B2 & 0x3365F & 0x03731 & 0x036CE & 0x03C07 & 0x03A93 - True
+159734 - 0xFFE34 (Obelisk Side 5) - 0x03AA6 & 0x3397C & 0x0105D & 0x0A304 - True
+159735 - 0xFFE35 (Obelisk Side 6) - 0x035CB & 0x035CF - True
+159739 - 0x00367 (Obelisk) - True - True
+
+Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
+159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True
158612 - 0x17C42 (Discard) - True - Triangles
-158665 - 0x002A6 (Vault) - True - Symmetry & Colored Dots & Black/White Squares
-158666 - 0x03542 (Vault Box) - 0x002A6 - True
+158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares
+Door - 0x00085 (Vault Door) - 0x002A6
159301 - 0x335AE (Cloud Cycle EP) - True - True
159325 - 0x33505 (Bush EP) - True - True
159335 - 0x03C07 (Apparent River EP) - True - True
-Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34:
+Mountainside Vault (Mountainside):
+158666 - 0x03542 (Vault Box) - True - True
+
+Mountaintop (Mountaintop) - Mountain Floor 1 - 0x17C34:
158405 - 0x0042D (River Shape) - True - True
-158406 - 0x09F7F (Box Short) - 7 Lasers - True
-158407 - 0x17C34 (Trap Door Triple Exit) - 0x09F7F - Black/White Squares
-158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True
+158406 - 0x09F7F (Box Short) - 7 Lasers + Redirect - True
+158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Black/White Squares
+158800 - 0xFFF00 (Box Long) - 11 Lasers + Redirect & 0x17C34 - True
159300 - 0x001A3 (River Shape EP) - True - True
159320 - 0x3370E (Arch Black EP) - True - True
159324 - 0x336C8 (Arch White Right EP) - True - True
159326 - 0x3369A (Arch White Left EP) - True - True
-Mountain Top Layer (Mountain Floor 1) - Mountain Top Layer Bridge - 0x09E39:
+==Mountain (Inside)==
+
+Mountain Floor 1 (Mountain Floor 1) - Mountain Floor 1 Bridge - 0x09E39:
158408 - 0x09E39 (Light Bridge Controller) - True - Black/White Squares & Rotated Shapers
-Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54:
+Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneWay:
158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots
158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Dots
158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers
@@ -892,32 +1017,32 @@ Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54:
158416 - 0x09E78 (Left Row 3) - 0x09E75 - Dots & Shapers
158417 - 0x09E79 (Left Row 4) - 0x09E78 - Shapers & Rotated Shapers
158418 - 0x09E6C (Left Row 5) - 0x09E79 - Stars & Black/White Squares
-158419 - 0x09E6F (Left Row 6) - 0x09E6C - Shapers
+158419 - 0x09E6F (Left Row 6) - 0x09E6C - Shapers & Dots
158420 - 0x09E6B (Left Row 7) - 0x09E6F - Dots
158421 - 0x33AF5 (Back Row 1) - True - Black/White Squares & Symmetry
-158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Black/White Squares & Stars
+158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Black/White Squares
158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Dots
158424 - 0x09EAD (Trash Pillar 1) - True - Black/White Squares & Shapers
158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Black/White Squares & Shapers
+
+Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54:
Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B
-Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Blue Bridge - 0x09E86 - Mountain Pink Bridge EP - TrueOneWay:
+Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 Above The Abyss - True - Mountain Pink Bridge EP - TrueOneWay:
158426 - 0x09FD3 (Near Row 1) - True - Colored Squares
158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Colored Squares & Dots
158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Colored Squares & Stars + Same Colored Symbol
158429 - 0x09FD7 (Near Row 4) - 0x09FD6 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers
-158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares & Symmetry
+158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares
Door - 0x09FFB (Staircase Near) - 0x09FD8
-Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - 0x09ED8:
-
-Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD:
+Mountain Floor 2 Above The Abyss (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD & 0x09ED8 & 0x09E86:
Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86
Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2):
158431 - 0x09E86 (Light Bridge Controller Near) - True - Stars & Black/White Squares
-Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay:
+Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay - Mountain Floor 2 - 0x09ED8:
158432 - 0x09FCC (Far Row 1) - True - Dots
158433 - 0x09FCE (Far Row 2) - 0x09FCC - Black/White Squares
158434 - 0x09FCF (Far Row 3) - 0x09FCE - Shapers
@@ -932,37 +1057,56 @@ Mountain Floor 2 Light Bridge Room Far (Mountain Floor 2):
Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - TrueOneWay:
158613 - 0x17F93 (Elevator Discard) - True - Triangles
-Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Third Layer - 0x09EEB:
+Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Floor 3 - 0x09EEB:
158439 - 0x09EEB (Elevator Control Panel) - True - Dots
-Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89:
+Mountain Floor 3 (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89 - Mountain Pink Bridge EP - TrueOneWay:
158440 - 0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Eraser
158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Rotated Shapers & Eraser
158442 - 0x09F01 (Giant Puzzle Top Right) - True - Shapers & Eraser
158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser
158444 - 0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry
+159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True
+159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True
Door - 0x09F89 (Exit) - 0x09FDA
-Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x17FA2 - Final Room - 0x0C141 - Mountain Pink Bridge EP - TrueOneWay:
+Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Path to Caves - 0x17F33 - Mountain Bottom Floor Pillars Room - 0x0C141:
158614 - 0x17FA2 (Discard) - 0xFFF00 - Triangles
-158445 - 0x01983 (Final Room Entry Left) - True - Shapers & Stars
-158446 - 0x01987 (Final Room Entry Right) - True - Colored Squares & Dots
-Door - 0x0C141 (Final Room Entry) - 0x01983 & 0x01987
-159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True
-159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True
+158445 - 0x01983 (Pillars Room Entry Left) - True - Shapers & Stars
+158446 - 0x01987 (Pillars Room Entry Right) - True - Colored Squares & Dots
+Door - 0x0C141 (Pillars Room Entry) - 0x01983 & 0x01987
+Door - 0x17F33 (Rock Open) - 0x17FA2 | 0x334E1
+
+Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB & 0x33961:
+158522 - 0x0383A (Right Pillar 1) - True - Stars
+158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Stars & Dots
+158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Full Dots
+158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Dots & Symmetry
+158526 - 0x0383D (Left Pillar 1) - True - Dots & Full Dots
+158527 - 0x0383F (Left Pillar 2) - 0x0383D - Black/White Squares
+158528 - 0x03859 (Left Pillar 3) - 0x0383F - Shapers
+158529 - 0x339BB (Left Pillar 4) - 0x03859 - Black/White Squares & Stars & Symmetry
+
+Elevator (Mountain Bottom Floor):
+158530 - 0x3D9A6 (Elevator Door Close Left) - True - True
+158531 - 0x3D9A7 (Elevator Door Close Right) - True - True
+158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True
+158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True
+158534 - 0x3D9AA (Back Wall Left) - 0x3D9A6 | 0x3D9A7 - True
+158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True
+158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True
Mountain Pink Bridge EP (Mountain Floor 2):
159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True
-Mountain Bottom Floor Rock (Mountain Bottom Floor) - Mountain Bottom Floor - 0x17F33 - Mountain Path to Caves - 0x17F33:
-Door - 0x17F33 (Rock Open) - True - True
-
-Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x334E1 - Caves - 0x2D77D:
+Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D:
158447 - 0x00FF8 (Caves Entry Panel) - True - Black/White Squares
Door - 0x2D77D (Caves Entry) - 0x00FF8
158448 - 0x334E1 (Rock Control) - True - True
-Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Path to Challenge - 0x019A5:
+==Caves==
+
+Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Caves Path to Challenge - 0x019A5:
158451 - 0x335AB (Elevator Inside Control) - True - Dots & Black/White Squares
158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Black/White Squares
158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Black/White Squares & Dots
@@ -984,8 +1128,8 @@ Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Path to Challenge - 0x019A5:
158469 - 0x009A4 (Blue Tunnel Left Third 1) - True - Shapers
158470 - 0x018A0 (Blue Tunnel Right Third 1) - True - Shapers & Symmetry
158471 - 0x00A72 (Blue Tunnel Left Fourth 1) - True - Shapers & Negative Shapers
-158472 - 0x32962 (First Floor Left) - True - Rotated Shapers
-158473 - 0x32966 (First Floor Grounded) - True - Stars & Black/White Squares & Stars + Same Colored Symbol
+158472 - 0x32962 (First Floor Left) - True - Rotated Shapers & Shapers
+158473 - 0x32966 (First Floor Grounded) - True - Stars & Black/White Squares
158474 - 0x01A31 (First Floor Middle) - True - Colored Squares
158475 - 0x00B71 (First Floor Right) - True - Colored Squares & Stars & Stars + Same Colored Symbol & Eraser
158478 - 0x288EA (First Wooden Beam) - True - Rotated Shapers
@@ -1017,11 +1161,13 @@ Door - 0x2D73F (Mountain Shortcut Door) - 0x021D7
Door - 0x2D859 (Swamp Shortcut Door) - 0x17CF2
159341 - 0x3397C (Skylight EP) - True - True
-Path to Challenge (Caves) - Challenge - 0x0A19A:
+Caves Path to Challenge (Caves) - Challenge - 0x0A19A:
158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Shapers & Stars + Same Colored Symbol
Door - 0x0A19A (Challenge Entry) - 0x0A16E
-Challenge (Challenge) - Tunnels - 0x0348A:
+==Challenge==
+
+Challenge (Challenge) - Tunnels - 0x0348A - Challenge Vault - 0x04D75:
158499 - 0x0A332 (Start Timer) - 11 Lasers - True
158500 - 0x0088E (Small Basic) - 0x0A332 - True
158501 - 0x00BAF (Big Basic) - 0x0088E - True
@@ -1041,12 +1187,17 @@ Challenge (Challenge) - Tunnels - 0x0348A:
158515 - 0x034EC (Maze Hidden 2) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles
158516 - 0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry
158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Black/White Squares & Symmetry
-158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True
+Door - 0x04D75 (Vault Door) - 0x1C31A & 0x1C319
158518 - 0x039B4 (Tunnels Entry Panel) - True - Triangles
Door - 0x0348A (Tunnels Entry) - 0x039B4
159530 - 0x28B30 (Water EP) - True - True
-Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Lowest Level Inbetween Shortcuts - 0x27263 - Town - 0x09E87:
+Challenge Vault (Challenge):
+158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True
+
+==Tunnels==
+
+Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Behind Elevator - 0x27263 - Town - 0x09E87:
158668 - 0x2FAF6 (Vault Box) - True - True
158519 - 0x27732 (Theater Shortcut Panel) - True - True
Door - 0x27739 (Theater Shortcut) - 0x27732
@@ -1056,26 +1207,9 @@ Door - 0x27263 (Desert Shortcut) - 0x2773D
Door - 0x09E87 (Town Shortcut) - 0x09E85
159557 - 0x33A20 (Theater Flowers EP) - 0x03553 & Theater to Tunnels - True
-Final Room (Mountain Final Room) - Elevator - 0x339BB & 0x33961:
-158522 - 0x0383A (Right Pillar 1) - True - Stars
-158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Stars & Dots
-158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Full Dots
-158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Dots & Symmetry
-158526 - 0x0383D (Left Pillar 1) - True - Dots & Full Dots
-158527 - 0x0383F (Left Pillar 2) - 0x0383D - Black/White Squares
-158528 - 0x03859 (Left Pillar 3) - 0x0383F - Shapers
-158529 - 0x339BB (Left Pillar 4) - 0x03859 - Black/White Squares & Stars & Symmetry
+==Boat==
-Elevator (Mountain Final Room):
-158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True
-158531 - 0x3D9A7 (Elevator Door Close Right) - True - True
-158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True
-158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True
-158534 - 0x3D9AA (Back Wall Left) - 0x3D9A6 | 0x3D9A7 - True
-158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True
-158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True
-
-Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay:
+The Ocean (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay:
159042 - 0x22106 (Desert EP) - True - True
159223 - 0x03B25 (Shipwreck CCW Underside EP) - True - True
159231 - 0x28B29 (Shipwreck Green EP) - True - True
@@ -1086,39 +1220,3 @@ Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehous
159521 - 0x33879 (Tutorial Reflection EP) - True - True
159522 - 0x03C19 (Tutorial Moss EP) - True - True
159531 - 0x035C9 (Cargo Box EP) - 0x0A0C9 - True
-
-Obelisks (EPs) - Entry - True:
-159700 - 0xFFE00 (Desert Obelisk Side 1) - 0x0332B & 0x03367 & 0x28B8A - True
-159701 - 0xFFE01 (Desert Obelisk Side 2) - 0x037B6 & 0x037B2 & 0x000F7 - True
-159702 - 0xFFE02 (Desert Obelisk Side 3) - 0x3351D - True
-159703 - 0xFFE03 (Desert Obelisk Side 4) - 0x0053C & 0x00771 & 0x335C8 & 0x335C9 & 0x337F8 & 0x037BB & 0x220E4 & 0x220E5 - True
-159704 - 0xFFE04 (Desert Obelisk Side 5) - 0x334B9 & 0x334BC & 0x22106 & 0x0A14C & 0x0A14D - True
-159710 - 0xFFE10 (Monastery Obelisk Side 1) - 0x03ABC & 0x03ABE & 0x03AC0 & 0x03AC4 - True
-159711 - 0xFFE11 (Monastery Obelisk Side 2) - 0x03AC5 - True
-159712 - 0xFFE12 (Monastery Obelisk Side 3) - 0x03BE2 & 0x03BE3 & 0x0A409 - True
-159713 - 0xFFE13 (Monastery Obelisk Side 4) - 0x006E5 & 0x006E6 & 0x006E7 & 0x034A7 & 0x034AD & 0x034AF & 0x03DAB & 0x03DAC & 0x03DAD - True
-159714 - 0xFFE14 (Monastery Obelisk Side 5) - 0x03E01 - True
-159715 - 0xFFE15 (Monastery Obelisk Side 6) - 0x289F4 & 0x289F5 - True
-159720 - 0xFFE20 (Treehouse Obelisk Side 1) - 0x0053D & 0x0053E & 0x00769 - True
-159721 - 0xFFE21 (Treehouse Obelisk Side 2) - 0x33721 & 0x220A7 & 0x220BD - True
-159722 - 0xFFE22 (Treehouse Obelisk Side 3) - 0x03B22 & 0x03B23 & 0x03B24 & 0x03B25 & 0x03A79 & 0x28ABD & 0x28ABE - True
-159723 - 0xFFE23 (Treehouse Obelisk Side 4) - 0x3388F & 0x28B29 & 0x28B2A - True
-159724 - 0xFFE24 (Treehouse Obelisk Side 5) - 0x018B6 & 0x033BE & 0x033BF & 0x033DD & 0x033E5 - True
-159725 - 0xFFE25 (Treehouse Obelisk Side 6) - 0x28AE9 & 0x3348F - True
-159730 - 0xFFE30 (River Obelisk Side 1) - 0x001A3 & 0x335AE - True
-159731 - 0xFFE31 (River Obelisk Side 2) - 0x000D3 & 0x035F5 & 0x09D5D & 0x09D5E & 0x09D63 - True
-159732 - 0xFFE32 (River Obelisk Side 3) - 0x3370E & 0x035DE & 0x03601 & 0x03603 & 0x03D0D & 0x3369A & 0x336C8 & 0x33505 - True
-159733 - 0xFFE33 (River Obelisk Side 4) - 0x03A9E & 0x016B2 & 0x3365F & 0x03731 & 0x036CE & 0x03C07 & 0x03A93 - True
-159734 - 0xFFE34 (River Obelisk Side 5) - 0x03AA6 & 0x3397C & 0x0105D & 0x0A304 - True
-159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True
-159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True
-159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True
-159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True
-159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True
-159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True
-159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True
-159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True
-159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True
-159753 - 0xFFE53 (Town Obelisk Side 4) - 0x28B30 & 0x035C9 - True
-159754 - 0xFFE54 (Town Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True
-159755 - 0xFFE55 (Town Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True
diff --git a/worlds/witness/data/__init__.py b/worlds/witness/data/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/witness/data/item_definition_classes.py b/worlds/witness/data/item_definition_classes.py
new file mode 100644
index 000000000000..b095a83abe63
--- /dev/null
+++ b/worlds/witness/data/item_definition_classes.py
@@ -0,0 +1,59 @@
+from dataclasses import dataclass
+from enum import Enum
+from typing import Dict, List, Optional
+
+from BaseClasses import ItemClassification
+
+
+class ItemCategory(Enum):
+ SYMBOL = 0
+ DOOR = 1
+ LASER = 2
+ USEFUL = 3
+ FILLER = 4
+ TRAP = 5
+ JOKE = 6
+ EVENT = 7
+
+
+CATEGORY_NAME_MAPPINGS: Dict[str, ItemCategory] = {
+ "Symbols:": ItemCategory.SYMBOL,
+ "Doors:": ItemCategory.DOOR,
+ "Lasers:": ItemCategory.LASER,
+ "Useful:": ItemCategory.USEFUL,
+ "Filler:": ItemCategory.FILLER,
+ "Traps:": ItemCategory.TRAP,
+ "Jokes:": ItemCategory.JOKE
+}
+
+
+@dataclass(frozen=True)
+class ItemDefinition:
+ local_code: int
+ category: ItemCategory
+
+
+@dataclass(frozen=True)
+class ProgressiveItemDefinition(ItemDefinition):
+ child_item_names: List[str]
+
+
+@dataclass(frozen=True)
+class DoorItemDefinition(ItemDefinition):
+ panel_id_hexes: List[str]
+
+
+@dataclass(frozen=True)
+class WeightedItemDefinition(ItemDefinition):
+ weight: int
+
+
+@dataclass()
+class ItemData:
+ """
+ ItemData for an item in The Witness
+ """
+ ap_code: Optional[int]
+ definition: ItemDefinition
+ classification: ItemClassification
+ local_only: bool = False
diff --git a/worlds/witness/settings/Audio_Logs.txt b/worlds/witness/data/settings/Audio_Logs.txt
similarity index 100%
rename from worlds/witness/settings/Audio_Logs.txt
rename to worlds/witness/data/settings/Audio_Logs.txt
diff --git a/worlds/witness/data/settings/Door_Shuffle/Boat.txt b/worlds/witness/data/settings/Door_Shuffle/Boat.txt
new file mode 100644
index 000000000000..6494b455cf79
--- /dev/null
+++ b/worlds/witness/data/settings/Door_Shuffle/Boat.txt
@@ -0,0 +1,2 @@
+Items:
+Boat
\ No newline at end of file
diff --git a/worlds/witness/data/settings/Door_Shuffle/Complex_Additional_Panels.txt b/worlds/witness/data/settings/Door_Shuffle/Complex_Additional_Panels.txt
new file mode 100644
index 000000000000..b84370908524
--- /dev/null
+++ b/worlds/witness/data/settings/Door_Shuffle/Complex_Additional_Panels.txt
@@ -0,0 +1,29 @@
+Items:
+Desert Surface 3 Control (Panel)
+Desert Surface 8 Control (Panel)
+Desert Elevator Room Hexagonal Control (Panel)
+Desert Flood Controls (Panel)
+Desert Light Control (Panel)
+Quarry Elevator Control (Panel)
+Quarry Stoneworks Ramp Controls (Panel)
+Quarry Stoneworks Lift Controls (Panel)
+Quarry Boathouse Ramp Height Control (Panel)
+Quarry Boathouse Ramp Horizontal Control (Panel)
+Quarry Boathouse Hook Control (Panel)
+Monastery Shutters Control (Panel)
+Town Maze Rooftop Bridge (Panel)
+Town RGB Control (Panel)
+Town Desert Laser Redirect Control (Panel)
+Windmill Turn Control (Panel)
+Theater Video Input (Panel)
+Bunker Drop-Down Door Controls (Panel)
+Bunker Elevator Control (Panel)
+Swamp Sliding Bridge (Panel)
+Swamp Rotating Bridge (Panel)
+Swamp Long Bridge (Panel)
+Swamp Maze Controls (Panel)
+Mountain Floor 1 Light Bridge (Panel)
+Mountain Floor 2 Light Bridge Near (Panel)
+Mountain Floor 2 Light Bridge Far (Panel)
+Mountain Floor 2 Elevator Control (Panel)
+Caves Elevator Controls (Panel)
\ No newline at end of file
diff --git a/worlds/witness/data/settings/Door_Shuffle/Complex_Door_Panels.txt b/worlds/witness/data/settings/Door_Shuffle/Complex_Door_Panels.txt
new file mode 100644
index 000000000000..63d8a58d2676
--- /dev/null
+++ b/worlds/witness/data/settings/Door_Shuffle/Complex_Door_Panels.txt
@@ -0,0 +1,38 @@
+Items:
+Glass Factory Entry (Panel)
+Outside Tutorial Outpost Entry (Panel)
+Outside Tutorial Outpost Exit (Panel)
+Symmetry Island Lower (Panel)
+Symmetry Island Upper (Panel)
+Desert Light Room Entry (Panel)
+Desert Flood Room Entry (Panel)
+Quarry Entry 1 (Panel)
+Quarry Entry 2 (Panel)
+Quarry Stoneworks Entry (Panel)
+Shadows Door Timer (Panel)
+Keep Hedge Maze 1 (Panel)
+Keep Hedge Maze 2 (Panel)
+Keep Hedge Maze 3 (Panel)
+Keep Hedge Maze 4 (Panel)
+Monastery Entry Left (Panel)
+Monastery Entry Right (Panel)
+Town RGB House Entry (Panel)
+Town Church Entry (Panel)
+Town Maze Stairs (Panel)
+Windmill Entry (Panel)
+Town Cargo Box Entry (Panel)
+Theater Entry (Panel)
+Theater Exit (Panel)
+Treehouse First & Second Doors (Panel)
+Treehouse Third Door (Panel)
+Treehouse Laser House Door Timer (Panel)
+Treehouse Drawbridge (Panel)
+Jungle Popup Wall (Panel)
+Bunker Entry (Panel)
+Bunker Tinted Glass Door (Panel)
+Swamp Entry (Panel)
+Swamp Platform Shortcut (Panel)
+Caves Entry (Panel)
+Challenge Entry (Panel)
+Tunnels Entry (Panel)
+Tunnels Town Shortcut (Panel)
\ No newline at end of file
diff --git a/worlds/witness/data/settings/Door_Shuffle/Complex_Doors.txt b/worlds/witness/data/settings/Door_Shuffle/Complex_Doors.txt
new file mode 100644
index 000000000000..513f1d9a71fb
--- /dev/null
+++ b/worlds/witness/data/settings/Door_Shuffle/Complex_Doors.txt
@@ -0,0 +1,201 @@
+Items:
+Outside Tutorial Outpost Path (Door)
+Outside Tutorial Outpost Entry (Door)
+Outside Tutorial Outpost Exit (Door)
+Glass Factory Entry (Door)
+Glass Factory Back Wall (Door)
+Symmetry Island Lower (Door)
+Symmetry Island Upper (Door)
+Orchard First Gate (Door)
+Orchard Second Gate (Door)
+Desert Light Room Entry (Door)
+Desert Pond Room Entry (Door)
+Desert Flood Room Entry (Door)
+Desert Elevator Room Entry (Door)
+Desert Elevator (Door)
+Quarry Entry 1 (Door)
+Quarry Entry 2 (Door)
+Quarry Stoneworks Entry (Door)
+Quarry Stoneworks Side Exit (Door)
+Quarry Stoneworks Roof Exit (Door)
+Quarry Stoneworks Stairs (Door)
+Quarry Boathouse Dock (Door)
+Quarry Boathouse First Barrier (Door)
+Quarry Boathouse Second Barrier (Door)
+Shadows Timed Door
+Shadows Laser Entry Right (Door)
+Shadows Laser Entry Left (Door)
+Shadows Quarry Barrier (Door)
+Shadows Ledge Barrier (Door)
+Keep Hedge Maze 1 Exit (Door)
+Keep Pressure Plates 1 Exit (Door)
+Keep Hedge Maze 2 Shortcut (Door)
+Keep Hedge Maze 2 Exit (Door)
+Keep Hedge Maze 3 Shortcut (Door)
+Keep Hedge Maze 3 Exit (Door)
+Keep Hedge Maze 4 Shortcut (Door)
+Keep Hedge Maze 4 Exit (Door)
+Keep Pressure Plates 2 Exit (Door)
+Keep Pressure Plates 3 Exit (Door)
+Keep Pressure Plates 4 Exit (Door)
+Keep Shadows Shortcut (Door)
+Keep Tower Shortcut (Door)
+Monastery Laser Shortcut (Door)
+Monastery Entry Inner (Door)
+Monastery Entry Outer (Door)
+Monastery Garden Entry (Door)
+Town Cargo Box Entry (Door)
+Town Wooden Roof Stairs (Door)
+Town RGB House Entry (Door)
+Town Church Entry (Door)
+Town Maze Stairs (Door)
+Windmill Entry (Door)
+Town RGB House Stairs (Door)
+Town Tower Second (Door)
+Town Tower First (Door)
+Town Tower Fourth (Door)
+Town Tower Third (Door)
+Theater Entry (Door)
+Theater Exit Left (Door)
+Theater Exit Right (Door)
+Jungle Laser Shortcut (Door)
+Jungle Popup Wall (Door)
+Jungle Monastery Garden Shortcut (Door)
+Bunker Entry (Door)
+Bunker Tinted Glass Door
+Bunker UV Room Entry (Door)
+Bunker Elevator Room Entry (Door)
+Swamp Entry (Door)
+Swamp Between Bridges First Door
+Swamp Platform Shortcut (Door)
+Swamp Cyan Water Pump (Door)
+Swamp Between Bridges Second Door
+Swamp Red Water Pump (Door)
+Swamp Red Underwater Exit (Door)
+Swamp Blue Water Pump (Door)
+Swamp Purple Water Pump (Door)
+Swamp Laser Shortcut (Door)
+Treehouse First (Door)
+Treehouse Second (Door)
+Treehouse Third (Door)
+Treehouse Drawbridge (Door)
+Treehouse Laser House Entry (Door)
+Mountain Floor 1 Exit (Door)
+Mountain Floor 2 Staircase Near (Door)
+Mountain Floor 2 Exit (Door)
+Mountain Floor 2 Staircase Far (Door)
+Mountain Bottom Floor Giant Puzzle Exit (Door)
+Mountain Bottom Floor Pillars Room Entry (Door)
+Mountain Bottom Floor Rock (Door)
+Caves Entry (Door)
+Caves Pillar Door
+Caves Mountain Shortcut (Door)
+Caves Swamp Shortcut (Door)
+Challenge Entry (Door)
+Tunnels Entry (Door)
+Tunnels Theater Shortcut (Door)
+Tunnels Desert Shortcut (Door)
+Tunnels Town Shortcut (Door)
+
+Added Locations:
+Outside Tutorial Outpost Entry Panel
+Outside Tutorial Outpost Exit Panel
+Glass Factory Entry Panel
+Glass Factory Back Wall 5
+Symmetry Island Lower Panel
+Symmetry Island Upper Panel
+Orchard Apple Tree 3
+Orchard Apple Tree 5
+Desert Light Room Entry Panel
+Desert Light Room 3
+Desert Flood Room Entry Panel
+Desert Flood Room 6
+Quarry Entry 1 Panel
+Quarry Entry 2 Panel
+Quarry Stoneworks Entry Right Panel
+Quarry Stoneworks Entry Left Panel
+Quarry Stoneworks Side Exit Panel
+Quarry Stoneworks Roof Exit Panel
+Quarry Stoneworks Stairs Panel
+Quarry Boathouse Second Barrier Panel
+Shadows Door Timer Inside
+Shadows Door Timer Outside
+Shadows Far 8
+Shadows Near 5
+Shadows Intro 3
+Shadows Intro 5
+Keep Hedge Maze 1
+Keep Pressure Plates 1
+Keep Hedge Maze 2
+Keep Hedge Maze 3
+Keep Hedge Maze 4
+Keep Pressure Plates 2
+Keep Pressure Plates 3
+Keep Pressure Plates 4
+Keep Shadows Shortcut Panel
+Keep Tower Shortcut Panel
+Monastery Laser Shortcut Panel
+Monastery Entry Left
+Monastery Entry Right
+Monastery Outside 3
+Town Cargo Box Entry Panel
+Town Wooden Roof Lower Row 5
+Town RGB House Entry Panel
+Town Church Entry Panel
+Town Maze Panel
+Windmill Entry Panel
+Town RGB House Sound Room Right
+Town Red Rooftop 5
+Town Church Lattice
+Town Tall Hexagonal
+Town Wooden Rooftop
+Windmill Theater Entry Panel
+Theater Exit Left Panel
+Theater Exit Right Panel
+Jungle Laser Shortcut Panel
+Jungle Popup Wall Control
+Jungle Monastery Garden Shortcut Panel
+Bunker Entry Panel
+Bunker Tinted Glass Door Panel
+Bunker Glass Room 3
+Bunker UV Room 2
+Swamp Entry Panel
+Swamp Platform Row 4
+Swamp Platform Shortcut Right Panel
+Swamp Blue Underwater 5
+Swamp Between Bridges Near Row 4
+Swamp Cyan Underwater 5
+Swamp Red Underwater 4
+Swamp Beyond Rotating Bridge 4
+Swamp Beyond Rotating Bridge 4
+Swamp Laser Shortcut Right Panel
+Treehouse First Door Panel
+Treehouse Second Door Panel
+Treehouse Third Door Panel
+Treehouse Drawbridge Panel
+Treehouse Left Orange Bridge 15
+Treehouse Right Orange Bridge 12
+Treehouse Laser House Door Timer Outside
+Treehouse Laser House Door Timer Inside
+Mountain Floor 1 Left Row 7
+Mountain Floor 1 Right Row 5
+Mountain Floor 1 Back Row 3
+Mountain Floor 1 Trash Pillar 2
+Mountain Floor 2 Near Row 5
+Mountain Floor 2 Light Bridge Controller Near
+Mountain Floor 2 Light Bridge Controller Far
+Mountain Floor 2 Far Row 6
+Mountain Bottom Floor Giant Puzzle
+Mountain Bottom Floor Pillars Room Entry Left
+Mountain Bottom Floor Pillars Room Entry Right
+Mountain Bottom Floor Discard
+Mountain Bottom Floor Rock Control
+Mountain Bottom Floor Caves Entry Panel
+Caves Lone Pillar
+Caves Mountain Shortcut Panel
+Caves Swamp Shortcut Panel
+Caves Challenge Entry Panel
+Challenge Tunnels Entry Panel
+Tunnels Theater Shortcut Panel
+Tunnels Desert Shortcut Panel
+Tunnels Town Shortcut Panel
\ No newline at end of file
diff --git a/worlds/witness/data/settings/Door_Shuffle/Elevators_Come_To_You.txt b/worlds/witness/data/settings/Door_Shuffle/Elevators_Come_To_You.txt
new file mode 100644
index 000000000000..78d245f9f0b5
--- /dev/null
+++ b/worlds/witness/data/settings/Door_Shuffle/Elevators_Come_To_You.txt
@@ -0,0 +1,11 @@
+New Connections:
+Quarry - Quarry Elevator - TrueOneWay
+Outside Quarry - Quarry Elevator - TrueOneWay
+Outside Bunker - Bunker Elevator - TrueOneWay
+Outside Swamp - Swamp Long Bridge - TrueOneWay
+Swamp Near Boat - Swamp Long Bridge - TrueOneWay
+Town Red Rooftop - Town Maze Rooftop - TrueOneWay
+
+
+Requirement Changes:
+0x035DE - 0x17E2B - True
\ No newline at end of file
diff --git a/worlds/witness/data/settings/Door_Shuffle/Obelisk_Keys.txt b/worlds/witness/data/settings/Door_Shuffle/Obelisk_Keys.txt
new file mode 100644
index 000000000000..9ebcc9fa2ffa
--- /dev/null
+++ b/worlds/witness/data/settings/Door_Shuffle/Obelisk_Keys.txt
@@ -0,0 +1,7 @@
+Items:
+Desert Obelisk Key
+Monastery Obelisk Key
+Treehouse Obelisk Key
+Mountainside Obelisk Key
+Quarry Obelisk Key
+Town Obelisk Key
\ No newline at end of file
diff --git a/worlds/witness/data/settings/Door_Shuffle/Simple_Additional_Panels.txt b/worlds/witness/data/settings/Door_Shuffle/Simple_Additional_Panels.txt
new file mode 100644
index 000000000000..c16ce737629a
--- /dev/null
+++ b/worlds/witness/data/settings/Door_Shuffle/Simple_Additional_Panels.txt
@@ -0,0 +1,11 @@
+Items:
+Desert Control Panels
+Quarry Elevator Control (Panel)
+Quarry Stoneworks Control Panels
+Quarry Boathouse Control Panels
+Monastery Shutters Control (Panel)
+Town Control Panels
+Windmill & Theater Control Panels
+Bunker Control Panels
+Swamp Control Panels
+Mountain & Caves Control Panels
\ No newline at end of file
diff --git a/worlds/witness/data/settings/Door_Shuffle/Simple_Doors.txt b/worlds/witness/data/settings/Door_Shuffle/Simple_Doors.txt
new file mode 100644
index 000000000000..2059f43af62c
--- /dev/null
+++ b/worlds/witness/data/settings/Door_Shuffle/Simple_Doors.txt
@@ -0,0 +1,134 @@
+Items:
+Outside Tutorial Outpost Doors
+Glass Factory Doors
+Symmetry Island Doors
+Orchard Gates
+Desert Doors & Elevator
+Quarry Entry Doors
+Quarry Stoneworks Doors
+Quarry Boathouse Doors
+Shadows Laser Room Doors
+Shadows Lower Doors
+Keep Hedge Maze Doors
+Keep Pressure Plates Doors
+Keep Shortcuts
+Monastery Entry Doors
+Monastery Shortcuts
+Town Doors
+Town Tower Doors
+Windmill & Theater Doors
+Jungle Doors
+Bunker Doors
+Swamp Doors
+Swamp Shortcuts
+Swamp Water Pumps
+Treehouse Entry Doors
+Treehouse Upper Doors
+Mountain Floor 1 & 2 Doors
+Mountain Bottom Floor Doors
+Caves Doors
+Caves Shortcuts
+Tunnels Doors
+
+Added Locations:
+Outside Tutorial Outpost Entry Panel
+Outside Tutorial Outpost Exit Panel
+Glass Factory Entry Panel
+Glass Factory Back Wall 5
+Symmetry Island Lower Panel
+Symmetry Island Upper Panel
+Orchard Apple Tree 3
+Orchard Apple Tree 5
+Desert Light Room Entry Panel
+Desert Light Room 3
+Desert Flood Room Entry Panel
+Desert Flood Room 6
+Quarry Entry 1 Panel
+Quarry Entry 2 Panel
+Quarry Stoneworks Entry Right Panel
+Quarry Stoneworks Entry Left Panel
+Quarry Stoneworks Side Exit Panel
+Quarry Stoneworks Roof Exit Panel
+Quarry Stoneworks Stairs Panel
+Quarry Boathouse Second Barrier Panel
+Shadows Door Timer Inside
+Shadows Door Timer Outside
+Shadows Far 8
+Shadows Near 5
+Shadows Intro 3
+Shadows Intro 5
+Keep Hedge Maze 1
+Keep Pressure Plates 1
+Keep Hedge Maze 2
+Keep Hedge Maze 3
+Keep Hedge Maze 4
+Keep Pressure Plates 2
+Keep Pressure Plates 3
+Keep Pressure Plates 4
+Keep Shadows Shortcut Panel
+Keep Tower Shortcut Panel
+Monastery Laser Shortcut Panel
+Monastery Entry Left
+Monastery Entry Right
+Monastery Outside 3
+Town Cargo Box Entry Panel
+Town Wooden Roof Lower Row 5
+Town RGB House Entry Panel
+Town Church Entry Panel
+Town Maze Panel
+Windmill Entry Panel
+Town RGB House Sound Room Right
+Town Red Rooftop 5
+Town Church Lattice
+Town Tall Hexagonal
+Town Wooden Rooftop
+Windmill Theater Entry Panel
+Theater Exit Left Panel
+Theater Exit Right Panel
+Jungle Laser Shortcut Panel
+Jungle Popup Wall Control
+Jungle Monastery Garden Shortcut Panel
+Bunker Entry Panel
+Bunker Tinted Glass Door Panel
+Bunker Glass Room 3
+Bunker UV Room 2
+Swamp Entry Panel
+Swamp Platform Row 4
+Swamp Platform Shortcut Right Panel
+Swamp Blue Underwater 5
+Swamp Between Bridges Near Row 4
+Swamp Cyan Underwater 5
+Swamp Red Underwater 4
+Swamp Beyond Rotating Bridge 4
+Swamp Beyond Rotating Bridge 4
+Swamp Laser Shortcut Right Panel
+Treehouse First Door Panel
+Treehouse Second Door Panel
+Treehouse Third Door Panel
+Treehouse Drawbridge Panel
+Treehouse Left Orange Bridge 15
+Treehouse Right Orange Bridge 12
+Treehouse Laser House Door Timer Outside
+Treehouse Laser House Door Timer Inside
+Mountain Floor 1 Left Row 7
+Mountain Floor 1 Right Row 5
+Mountain Floor 1 Back Row 3
+Mountain Floor 1 Trash Pillar 2
+Mountain Floor 2 Near Row 5
+Mountain Floor 2 Light Bridge Controller Near
+Mountain Floor 2 Light Bridge Controller Far
+Mountain Floor 2 Far Row 6
+Mountain Bottom Floor Giant Puzzle
+Mountain Bottom Floor Pillars Room Entry Left
+Mountain Bottom Floor Pillars Room Entry Right
+Mountain Bottom Floor Discard
+Mountain Bottom Floor Rock Control
+Mountain Bottom Floor Caves Entry Panel
+Caves Lone Pillar
+Caves Mountain Shortcut Panel
+Caves Swamp Shortcut Panel
+Caves Challenge Entry Panel
+Challenge Tunnels Entry Panel
+Tunnels Theater Shortcut Panel
+Tunnels Desert Shortcut Panel
+Tunnels Town Shortcut Panel
\ No newline at end of file
diff --git a/worlds/witness/data/settings/Door_Shuffle/Simple_Panels.txt b/worlds/witness/data/settings/Door_Shuffle/Simple_Panels.txt
new file mode 100644
index 000000000000..23501d20d3a7
--- /dev/null
+++ b/worlds/witness/data/settings/Door_Shuffle/Simple_Panels.txt
@@ -0,0 +1,22 @@
+Items:
+Symmetry Island Panels
+Outside Tutorial Outpost Panels
+Desert Panels
+Quarry Outside Panels
+Quarry Stoneworks Panels
+Quarry Boathouse Panels
+Keep Hedge Maze Panels
+Monastery Panels
+Town Church & RGB House Panels
+Town Maze Panels
+Windmill & Theater Panels
+Town Dockside House Panels
+Treehouse Panels
+Bunker Panels
+Swamp Panels
+Mountain Panels
+Caves Panels
+Tunnels Panels
+Glass Factory Entry (Panel)
+Shadows Door Timer (Panel)
+Jungle Popup Wall (Panel)
\ No newline at end of file
diff --git a/worlds/witness/data/settings/EP_Shuffle/EP_All.txt b/worlds/witness/data/settings/EP_Shuffle/EP_All.txt
new file mode 100644
index 000000000000..939adc36e814
--- /dev/null
+++ b/worlds/witness/data/settings/EP_Shuffle/EP_All.txt
@@ -0,0 +1,136 @@
+Added Locations:
+0x0332B (Glass Factory Black Line Reflection EP)
+0x03367 (Glass Factory Black Line EP)
+0x28B8A (Vase EP)
+0x037B6 (Windmill First Blade EP)
+0x037B2 (Windmill Second Blade EP)
+0x000F7 (Windmill Third Blade EP)
+0x3351D (Sand Snake EP)
+0x0053C (Facade Right EP)
+0x00771 (Facade Left EP)
+0x335C8 (Stairs Left EP)
+0x335C9 (Stairs Right EP)
+0x337F8 (Flood Room EP)
+0x037BB (Elevator EP)
+0x220E4 (Broken Wall Straight EP)
+0x220E5 (Broken Wall Bend EP)
+0x334B9 (Shore EP)
+0x334BC (Island EP)
+0x22106 (Desert EP)
+0x0A14C (Pond Room Near Reflection EP)
+0x0A14D (Pond Room Far Reflection EP)
+0x03ABC (Long Arch Moss EP)
+0x03ABE (Straight Left Moss EP)
+0x03AC0 (Pop-up Wall Moss EP)
+0x03AC4 (Short Arch Moss EP)
+0x03AC5 (Green Leaf Moss EP)
+0x03BE2 (Monastery Garden Left EP)
+0x03BE3 (Monastery Garden Right EP)
+0x0A409 (Monastery Wall EP)
+0x006E5 (Facade Left Near EP)
+0x006E6 (Facade Left Far Short EP)
+0x006E7 (Facade Left Far Long EP)
+0x034A7 (Left Shutter EP)
+0x034AD (Middle Shutter EP)
+0x034AF (Right Shutter EP)
+0x03DAB (Facade Right Near EP)
+0x03DAC (Facade Left Stairs EP)
+0x03DAD (Facade Right Stairs EP)
+0x03E01 (Grass Stairs EP)
+0x289F4 (Entrance EP)
+0x289F5 (Tree Halo EP)
+0x0053D (Rock Shadow EP)
+0x0053E (Sand Shadow EP)
+0x00769 (Burned House Beach EP)
+0x33721 (Buoy EP)
+0x220A7 (Right Orange Bridge EP)
+0x220BD (Both Orange Bridges EP)
+0x03B22 (Circle Far EP)
+0x03B23 (Circle Left EP)
+0x03B24 (Circle Near EP)
+0x03B25 (Shipwreck CCW Underside EP)
+0x03A79 (Stern EP)
+0x28ABD (Rope Inner EP)
+0x28ABE (Rope Outer EP)
+0x3388F (Couch EP)
+0x28B29 (Shipwreck Green EP)
+0x28B2A (Shipwreck CW Underside EP)
+0x018B6 (Pressure Plates 4 Right Exit EP)
+0x033BE (Pressure Plates 1 EP)
+0x033BF (Pressure Plates 2 EP)
+0x033DD (Pressure Plates 3 EP)
+0x033E5 (Pressure Plates 4 Left Exit EP)
+0x28AE9 (Path EP)
+0x3348F (Hedges EP)
+0x001A3 (River Shape EP)
+0x335AE (Cloud Cycle EP)
+0x000D3 (Green Room Flowers EP)
+0x035F5 (Tinted Door EP)
+0x09D5D (Yellow Bridge EP)
+0x09D5E (Blue Bridge EP)
+0x09D63 (Pink Bridge EP)
+0x3370E (Arch Black EP)
+0x035DE (Purple Sand Bottom EP)
+0x03601 (Purple Sand Top EP)
+0x03603 (Purple Sand Middle EP)
+0x03D0D (Bunker Yellow Line EP)
+0x3369A (Arch White Left EP)
+0x336C8 (Arch White Right EP)
+0x33505 (Bush EP)
+0x03A9E (Purple Underwater Right EP)
+0x016B2 (Rotating Bridge CCW EP)
+0x3365F (Boat EP)
+0x03731 (Long Bridge Side EP)
+0x036CE (Rotating Bridge CW EP)
+0x03C07 (Apparent River EP)
+0x03A93 (Purple Underwater Left EP)
+0x03AA6 (Cyan Underwater Sliding Bridge EP)
+0x3397C (Skylight EP)
+0x0105D (Sliding Bridge Left EP)
+0x0A304 (Sliding Bridge Right EP)
+0x035CB (Bamboo CCW EP)
+0x035CF (Bamboo CW EP)
+0x28A7B (Quarry Stoneworks Rooftop Vent EP)
+0x005F6 (Hook EP)
+0x00859 (Moving Ramp EP)
+0x17CB9 (Railroad EP)
+0x28A4A (Shore EP)
+0x334B6 (Entrance Pipe EP)
+0x0069D (Ramp EP)
+0x00614 (Lift EP)
+0x28A4C (Sand Pile EP)
+0x289CF (Rock Line EP)
+0x289D1 (Rock Line Reflection EP)
+0x33692 (Brown Bridge EP)
+0x03E77 (Red Flowers EP)
+0x03E7C (Purple Flowers EP)
+0x035C7 (Tractor EP)
+0x01848 (EP)
+0x03D06 (Garden EP)
+0x33530 (Cloud EP)
+0x33600 (Patio Flowers EP)
+0x28A2F (Town Sewer EP)
+0x28A37 (Town Long Sewer EP)
+0x334A3 (Path EP)
+0x3352F (Gate EP)
+0x33857 (Tutorial EP)
+0x33879 (Tutorial Reflection EP)
+0x03C19 (Tutorial Moss EP)
+0x28B30 (Water EP)
+0x035C9 (Cargo Box EP)
+0x03335 (Tower Underside Third EP)
+0x03412 (Tower Underside Fourth EP)
+0x038A6 (Tower Underside First EP)
+0x038AA (Tower Underside Second EP)
+0x03E3F (RGB House Red EP)
+0x03E40 (RGB House Green EP)
+0x28B8E (Maze Bridge Underside EP)
+0x28B91 (Thundercloud EP)
+0x03BCE (Black Line Tower EP)
+0x03BCF (Black Line Redirect EP)
+0x03BD1 (Black Line Church EP)
+0x339B6 (Eclipse EP)
+0x33A20 (Theater Flowers EP)
+0x33A29 (Window EP)
+0x33A2A (Door EP)
+0x33B06 (Church EP)
diff --git a/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt b/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt
new file mode 100644
index 000000000000..95c1fc39fb7a
--- /dev/null
+++ b/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt
@@ -0,0 +1,18 @@
+Disabled Locations:
+0x339B6 (Eclipse EP)
+0x335AE (Cloud Cycle EP)
+0x3388F (Couch EP)
+0x33A20 (Theater Flowers EP)
+0x037B2 (Windmill Second Blade EP)
+0x000F7 (Windmill Third Blade EP)
+0x28B29 (Shipwreck Green EP)
+0x33857 (Tutorial EP)
+0x33879 (Tutorial Reflection EP)
+0x016B2 (Rotating Bridge CCW EP)
+0x036CE (Rotating Bridge CW EP)
+0x03B25 (Shipwreck CCW Underside EP)
+0x28B2A (Shipwreck CW Underside EP)
+0x09D63 (Mountain Pink Bridge EP)
+0x09D5E (Mountain Blue Bridge EP)
+0x09D5D (Mountain Yellow Bridge EP)
+0x220BD (Both Orange Bridges EP)
diff --git a/worlds/witness/data/settings/EP_Shuffle/EP_NoEclipse.txt b/worlds/witness/data/settings/EP_Shuffle/EP_NoEclipse.txt
new file mode 100644
index 000000000000..f241957c823a
--- /dev/null
+++ b/worlds/witness/data/settings/EP_Shuffle/EP_NoEclipse.txt
@@ -0,0 +1,2 @@
+Disabled Locations:
+0x339B6 (Eclipse EP)
diff --git a/worlds/witness/data/settings/EP_Shuffle/EP_Sides.txt b/worlds/witness/data/settings/EP_Shuffle/EP_Sides.txt
new file mode 100644
index 000000000000..d561ffdc183f
--- /dev/null
+++ b/worlds/witness/data/settings/EP_Shuffle/EP_Sides.txt
@@ -0,0 +1,35 @@
+Added Locations:
+0xFFE00 (Desert Obelisk Side 1)
+0xFFE01 (Desert Obelisk Side 2)
+0xFFE02 (Desert Obelisk Side 3)
+0xFFE03 (Desert Obelisk Side 4)
+0xFFE04 (Desert Obelisk Side 5)
+0xFFE10 (Monastery Obelisk Side 1)
+0xFFE11 (Monastery Obelisk Side 2)
+0xFFE12 (Monastery Obelisk Side 3)
+0xFFE13 (Monastery Obelisk Side 4)
+0xFFE14 (Monastery Obelisk Side 5)
+0xFFE15 (Monastery Obelisk Side 6)
+0xFFE20 (Treehouse Obelisk Side 1)
+0xFFE21 (Treehouse Obelisk Side 2)
+0xFFE22 (Treehouse Obelisk Side 3)
+0xFFE23 (Treehouse Obelisk Side 4)
+0xFFE24 (Treehouse Obelisk Side 5)
+0xFFE25 (Treehouse Obelisk Side 6)
+0xFFE30 (Mountainside Obelisk Side 1)
+0xFFE31 (Mountainside Obelisk Side 2)
+0xFFE32 (Mountainside Obelisk Side 3)
+0xFFE33 (Mountainside Obelisk Side 4)
+0xFFE34 (Mountainside Obelisk Side 5)
+0xFFE35 (Mountainside Obelisk Side 6)
+0xFFE40 (Quarry Obelisk Side 1)
+0xFFE41 (Quarry Obelisk Side 2)
+0xFFE42 (Quarry Obelisk Side 3)
+0xFFE43 (Quarry Obelisk Side 4)
+0xFFE44 (Quarry Obelisk Side 5)
+0xFFE50 (Town Obelisk Side 1)
+0xFFE51 (Town Obelisk Side 2)
+0xFFE52 (Town Obelisk Side 3)
+0xFFE53 (Town Obelisk Side 4)
+0xFFE54 (Town Obelisk Side 5)
+0xFFE55 (Town Obelisk Side 6)
diff --git a/worlds/witness/data/settings/Early_Caves.txt b/worlds/witness/data/settings/Early_Caves.txt
new file mode 100644
index 000000000000..48c8056bc7b6
--- /dev/null
+++ b/worlds/witness/data/settings/Early_Caves.txt
@@ -0,0 +1,6 @@
+Items:
+Caves Shortcuts
+
+Remove Items:
+Caves Mountain Shortcut (Door)
+Caves Swamp Shortcut (Door)
\ No newline at end of file
diff --git a/worlds/witness/data/settings/Early_Caves_Start.txt b/worlds/witness/data/settings/Early_Caves_Start.txt
new file mode 100644
index 000000000000..a16a6d02bb9f
--- /dev/null
+++ b/worlds/witness/data/settings/Early_Caves_Start.txt
@@ -0,0 +1,9 @@
+Items:
+Caves Shortcuts
+
+Starting Inventory:
+Caves Shortcuts
+
+Remove Items:
+Caves Mountain Shortcut (Door)
+Caves Swamp Shortcut (Door)
\ No newline at end of file
diff --git a/worlds/witness/data/settings/Exclusions/Caves_Except_Path_To_Challenge.txt b/worlds/witness/data/settings/Exclusions/Caves_Except_Path_To_Challenge.txt
new file mode 100644
index 000000000000..aadb4c3f96e7
--- /dev/null
+++ b/worlds/witness/data/settings/Exclusions/Caves_Except_Path_To_Challenge.txt
@@ -0,0 +1,65 @@
+Disabled Locations:
+0x335AB (Elevator Inside Control)
+0x335AC (Elevator Upper Outside Control)
+0x3369D (Elevator Lower Outside Control)
+0x00190 (Blue Tunnel Right First 1)
+0x00558 (Blue Tunnel Right First 2)
+0x00567 (Blue Tunnel Right First 3)
+0x006FE (Blue Tunnel Right First 4)
+0x01A0D (Blue Tunnel Left First 1)
+0x008B8 (Blue Tunnel Left Second 1)
+0x00973 (Blue Tunnel Left Second 2)
+0x0097B (Blue Tunnel Left Second 3)
+0x0097D (Blue Tunnel Left Second 4)
+0x0097E (Blue Tunnel Left Second 5)
+0x00994 (Blue Tunnel Right Second 1)
+0x334D5 (Blue Tunnel Right Second 2)
+0x00995 (Blue Tunnel Right Second 3)
+0x00996 (Blue Tunnel Right Second 4)
+0x00998 (Blue Tunnel Right Second 5)
+0x009A4 (Blue Tunnel Left Third 1)
+0x018A0 (Blue Tunnel Right Third 1)
+0x00A72 (Blue Tunnel Left Fourth 1)
+0x32962 (First Floor Left)
+0x32966 (First Floor Grounded)
+0x01A31 (First Floor Middle)
+0x00B71 (First Floor Right)
+0x288EA (First Wooden Beam)
+0x288FC (Second Wooden Beam)
+0x289E7 (Third Wooden Beam)
+0x288AA (Fourth Wooden Beam)
+0x17FB9 (Left Upstairs Single)
+0x0A16B (Left Upstairs Left Row 1)
+0x0A2CE (Left Upstairs Left Row 2)
+0x0A2D7 (Left Upstairs Left Row 3)
+0x0A2DD (Left Upstairs Left Row 4)
+0x0A2EA (Left Upstairs Left Row 5)
+0x0008F (Right Upstairs Left Row 1)
+0x0006B (Right Upstairs Left Row 2)
+0x0008B (Right Upstairs Left Row 3)
+0x0008C (Right Upstairs Left Row 4)
+0x0008A (Right Upstairs Left Row 5)
+0x00089 (Right Upstairs Left Row 6)
+0x0006A (Right Upstairs Left Row 7)
+0x0006C (Right Upstairs Left Row 8)
+0x00027 (Right Upstairs Right Row 1)
+0x00028 (Right Upstairs Right Row 2)
+0x00029 (Right Upstairs Right Row 3)
+0x021D7 (Mountain Shortcut Panel)
+0x2D73F (Mountain Shortcut Door)
+0x17CF2 (Swamp Shortcut Panel)
+0x2D859 (Swamp Shortcut Door)
+0x039B4 (Tunnels Entry Panel)
+0x0348A (Tunnels Entry Door)
+0x2FAF6 (Vault Box)
+0x27732 (Tunnels Theater Shortcut Panel)
+0x27739 (Tunnels Theater Shortcut Door)
+0x2773D (Tunnels Desert Shortcut Panel)
+0x27263 (Tunnels Desert Shortcut Door)
+0x09E85 (Tunnels Town Shortcut Panel)
+0x09E87 (Tunnels Town Shortcut Door)
+
+0x3397C (Skylight EP)
+0x28B30 (Water EP)
+0x33A20 (Theater Flowers EP)
+0x3352F (Gate EP)
diff --git a/worlds/witness/data/settings/Exclusions/Disable_Unrandomized.txt b/worlds/witness/data/settings/Exclusions/Disable_Unrandomized.txt
new file mode 100644
index 000000000000..3dfc34e8ad0a
--- /dev/null
+++ b/worlds/witness/data/settings/Exclusions/Disable_Unrandomized.txt
@@ -0,0 +1,136 @@
+Event Items:
+Monastery Laser Activation - 0x17C65 - 0x00A5B,0x17CE7,0x17FA9
+Bunker Laser Activation - 0x0C2B2 - 0x00061,0x17D01,0x17C42
+Shadows Laser Activation - 0x181B3 - 0x00021,0x17D28,0x17C71
+Town Tower 4th Door Opens - 0x2779A - 0x17CFB,0x3C12B,0x17CF7
+Jungle Popup Wall Lifts - 0x1475B - 0x17FA0,0x17D27,0x17F9B,0x17CAB
+
+Requirement Changes:
+0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9
+0x0C2B2 - 0x00061 | 0x17D01 | 0x17C42
+0x181B3 - 0x00021 | 0x17D28 | 0x17C71
+0x17CAB - True - True
+0x17CA4 - True - True
+0x1475B - 0x17FA0 | 0x17D27 | 0x17F9B | 0x17CAB
+0x2779A - 0x17CFB | 0x3C12B | 0x17CF7
+
+Disabled Locations:
+0x28B39 (Town Tall Hexagonal)
+0x03505 (Tutorial Gate Close)
+0x0C335 (Tutorial Pillar)
+0x0C373 (Tutorial Patio Floor)
+0x009B8 (Symmetry Island Scenery Outlines 1)
+0x003E8 (Symmetry Island Scenery Outlines 2)
+0x00A15 (Symmetry Island Scenery Outlines 3)
+0x00B53 (Symmetry Island Scenery Outlines 4)
+0x00B8D (Symmetry Island Scenery Outlines 5)
+0x00143 (Orchard Apple Tree 1)
+0x0003B (Orchard Apple Tree 2)
+0x00055 (Orchard Apple Tree 3)
+0x032F7 (Orchard Apple Tree 4)
+0x032FF (Orchard Apple Tree 5)
+0x334DB (Door Timer Outside)
+0x334DC (Door Timer Inside)
+0x19B24 (Timed Door) - 0x334DB
+0x194B2 (Laser Entry Right)
+0x19665 (Laser Entry Left)
+0x198B5 (Shadows Intro 1)
+0x198BD (Shadows Intro 2)
+0x198BF (Shadows Intro 3)
+0x19771 (Shadows Intro 4)
+0x0A8DC (Shadows Intro 5)
+0x0AC74 (Shadows Intro 6)
+0x0AC7A (Shadows Intro 7)
+0x0A8E0 (Shadows Intro 8)
+0x386FA (Shadows Far 1)
+0x1C33F (Shadows Far 2)
+0x196E2 (Shadows Far 3)
+0x1972A (Shadows Far 4)
+0x19809 (Shadows Far 5)
+0x19806 (Shadows Far 6)
+0x196F8 (Shadows Far 7)
+0x1972F (Shadows Far 8)
+0x19797 (Shadows Near 1)
+0x1979A (Shadows Near 2)
+0x197E0 (Shadows Near 3)
+0x197E8 (Shadows Near 4)
+0x197E5 (Shadows Near 5)
+0x19650 (Shadows Laser)
+0x19865 (Quarry Barrier)
+0x0A2DF (Quarry Barrier 2)
+0x1855B (Ledge Barrier)
+0x19ADE (Ledge Barrier 2)
+0x00139 (Keep Hedge Maze 1)
+0x019DC (Keep Hedge Maze 2)
+0x019E7 (Keep Hedge Maze 3)
+0x01A0F (Keep Hedge Maze 4)
+0x0360E (Laser Hedges)
+0x01954 (Hedge 1 Exit)
+0x018CE (Hedge 2 Shortcut)
+0x019D8 (Hedge 2 Exit)
+0x019B5 (Hedge 3 Shortcut)
+0x019E6 (Hedge 3 Exit)
+0x0199A (Hedge 4 Shortcut)
+0x01A0E (Hedge 4 Exit)
+0x03307 (First Gate)
+0x03313 (Second Gate)
+0x0C128 (Entry Inner)
+0x0C153 (Entry Outer)
+0x00B10 (Monastery Entry Left)
+0x00C92 (Monastery Entry Right)
+0x00290 (Monastery Outside 1)
+0x00038 (Monastery Outside 2)
+0x00037 (Monastery Outside 3)
+0x03750 (Garden Entry)
+0x09D9B (Monastery Shutters Control)
+0x193A7 (Monastery Inside 1)
+0x193AA (Monastery Inside 2)
+0x193AB (Monastery Inside 3)
+0x193A6 (Monastery Inside 4)
+0x17CA4 (Monastery Laser Panel)
+0x0364E (Monastery Laser Shortcut Door)
+0x03713 (Monastery Laser Shortcut Panel)
+0x18590 (Transparent) - True - Symmetry & Environment
+0x28AE3 (Vines) - 0x18590 - Shadows Follow & Environment
+0x28938 (Apple Tree) - 0x28AE3 - Environment
+0x079DF (Triple Exit) - 0x28938 - Shadows Avoid & Environment & Reflection
+0x00815 (Theater Video Input)
+0x03553 (Theater Tutorial Video)
+0x03552 (Theater Desert Video)
+0x0354E (Theater Jungle Video)
+0x03549 (Theater Challenge Video)
+0x0354F (Theater Shipwreck Video)
+0x03545 (Theater Mountain Video)
+0x002C4 (First Row 1)
+0x00767 (First Row 2)
+0x002C6 (First Row 3)
+0x0070E (Second Row 1)
+0x0070F (Second Row 2)
+0x0087D (Second Row 3)
+0x002C7 (Second Row 4)
+0x17CAA (Monastery Garden Shortcut Panel)
+0x0CF2A (Monastery Garden Shortcut)
+0x0C2A4 (Bunker Entry)
+0x17C79 (Tinted Glass Door)
+0x0C2A3 (UV Room Entry)
+0x0A08D (Elevator Room Entry)
+0x17C2E (Door to Bunker)
+0x09F7D (Bunker Intro Left 1)
+0x09FDC (Bunker Intro Left 2)
+0x09FF7 (Bunker Intro Left 3)
+0x09F82 (Bunker Intro Left 4)
+0x09FF8 (Bunker Intro Left 5)
+0x09D9F (Bunker Intro Back 1)
+0x09DA1 (Bunker Intro Back 2)
+0x09DA2 (Bunker Intro Back 3)
+0x09DAF (Bunker Intro Back 4)
+0x0A010 (Bunker Glass Room 1)
+0x0A01B (Bunker Glass Room 2)
+0x0A01F (Bunker Glass Room 3)
+0x0A099 (Tinted Glass Door)
+0x34BC5 (Bunker Drop-Down Door Open)
+0x34BC6 (Bunker Drop-Down Door Close)
+0x17E63 (Bunker UV Room 1)
+0x17E67 (Bunker UV Room 2)
+0x09DE0 (Bunker Laser)
+0x0A079 (Bunker Elevator Control)
diff --git a/worlds/witness/data/settings/Exclusions/Discards.txt b/worlds/witness/data/settings/Exclusions/Discards.txt
new file mode 100644
index 000000000000..e46d1dd82b1b
--- /dev/null
+++ b/worlds/witness/data/settings/Exclusions/Discards.txt
@@ -0,0 +1,15 @@
+Disabled Locations:
+0x17CFB (Outside Tutorial Discard)
+0x3C12B (Glass Factory Discard)
+0x17CE7 (Desert Discard)
+0x17CF0 (Quarry Discard)
+0x17D27 (Keep Discard)
+0x17D28 (Shipwreck Discard)
+0x17D01 (Town Cargo Box Discard)
+0x17C71 (Town Rooftop Discard)
+0x17CF7 (Theater Discard)
+0x17F9B (Jungle Discard)
+0x17FA9 (Treehouse Green Bridge Discard)
+0x17C42 (Mountainside Discard)
+0x17F93 (Mountain Floor 2 Elevator Discard)
+0x17FA0 (Treehouse Laser Discard)
diff --git a/worlds/witness/data/settings/Exclusions/Vaults.txt b/worlds/witness/data/settings/Exclusions/Vaults.txt
new file mode 100644
index 000000000000..9eade5e52855
--- /dev/null
+++ b/worlds/witness/data/settings/Exclusions/Vaults.txt
@@ -0,0 +1,8 @@
+Disabled Locations:
+0x033D4 (Outside Tutorial Vault)
+0x0CC7B (Desert Vault)
+0x00AFB (Shipwreck Vault)
+0x15ADD (Jungle Vault)
+0x002A6 (Mountainside Vault)
+0x2FAF6 (Tunnels Vault Box)
+0x00815 (Theater Video Input)
diff --git a/worlds/witness/settings/Laser_Shuffle.txt b/worlds/witness/data/settings/Laser_Shuffle.txt
similarity index 100%
rename from worlds/witness/settings/Laser_Shuffle.txt
rename to worlds/witness/data/settings/Laser_Shuffle.txt
diff --git a/worlds/witness/settings/Symbol_Shuffle.txt b/worlds/witness/data/settings/Symbol_Shuffle.txt
similarity index 87%
rename from worlds/witness/settings/Symbol_Shuffle.txt
rename to worlds/witness/data/settings/Symbol_Shuffle.txt
index 3d0342f5e2a9..253fe98bad42 100644
--- a/worlds/witness/settings/Symbol_Shuffle.txt
+++ b/worlds/witness/data/settings/Symbol_Shuffle.txt
@@ -1,9 +1,8 @@
Items:
Arrows
Progressive Dots
-Colored Dots
Sound Dots
-Symmetry
+Progressive Symmetry
Triangles
Eraser
Shapers
diff --git a/worlds/witness/data/static_items.py b/worlds/witness/data/static_items.py
new file mode 100644
index 000000000000..b0d8fc3c4f6e
--- /dev/null
+++ b/worlds/witness/data/static_items.py
@@ -0,0 +1,56 @@
+from typing import Dict, List, Set
+
+from BaseClasses import ItemClassification
+
+from . import static_logic as static_witness_logic
+from .item_definition_classes import DoorItemDefinition, ItemCategory, ItemData
+from .static_locations import ID_START
+
+ITEM_DATA: Dict[str, ItemData] = {}
+ITEM_GROUPS: Dict[str, Set[str]] = {}
+
+# Useful items that are treated specially at generation time and should not be automatically added to the player's
+# item list during get_progression_items.
+_special_usefuls: List[str] = ["Puzzle Skip"]
+
+
+def populate_items() -> None:
+ for item_name, definition in static_witness_logic.ALL_ITEMS.items():
+ ap_item_code = definition.local_code + ID_START
+ classification: ItemClassification = ItemClassification.filler
+ local_only: bool = False
+
+ if definition.category is ItemCategory.SYMBOL:
+ classification = ItemClassification.progression
+ ITEM_GROUPS.setdefault("Symbols", set()).add(item_name)
+ elif definition.category is ItemCategory.DOOR:
+ classification = ItemClassification.progression
+ ITEM_GROUPS.setdefault("Doors", set()).add(item_name)
+ elif definition.category is ItemCategory.LASER:
+ classification = ItemClassification.progression_skip_balancing
+ ITEM_GROUPS.setdefault("Lasers", set()).add(item_name)
+ elif definition.category is ItemCategory.USEFUL:
+ classification = ItemClassification.useful
+ elif definition.category is ItemCategory.FILLER:
+ if item_name in ["Energy Fill (Small)"]:
+ local_only = True
+ classification = ItemClassification.filler
+ elif definition.category is ItemCategory.TRAP:
+ classification = ItemClassification.trap
+ elif definition.category is ItemCategory.JOKE:
+ classification = ItemClassification.filler
+
+ ITEM_DATA[item_name] = ItemData(ap_item_code, definition,
+ classification, local_only)
+
+
+def get_item_to_door_mappings() -> Dict[int, List[int]]:
+ output: Dict[int, List[int]] = {}
+ for item_name, item_data in ITEM_DATA.items():
+ if not isinstance(item_data.definition, DoorItemDefinition) or item_data.ap_code is None:
+ continue
+ output[item_data.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
+ return output
+
+
+populate_items()
diff --git a/worlds/witness/data/static_locations.py b/worlds/witness/data/static_locations.py
new file mode 100644
index 000000000000..de321d20c0f9
--- /dev/null
+++ b/worlds/witness/data/static_locations.py
@@ -0,0 +1,484 @@
+from typing import Dict, Set, cast
+
+from . import static_logic as static_witness_logic
+
+ID_START = 158000
+
+GENERAL_LOCATIONS = {
+ "Tutorial Front Left",
+ "Tutorial Back Left",
+ "Tutorial Back Right",
+ "Tutorial Patio Floor",
+ "Tutorial Gate Open",
+
+ "Outside Tutorial Vault Box",
+ "Outside Tutorial Discard",
+ "Outside Tutorial Shed Row 5",
+ "Outside Tutorial Tree Row 9",
+ "Outside Tutorial Outpost Entry Panel",
+ "Outside Tutorial Outpost Exit Panel",
+
+ "Glass Factory Discard",
+ "Glass Factory Back Wall 5",
+ "Glass Factory Front 3",
+ "Glass Factory Melting 3",
+
+ "Symmetry Island Lower Panel",
+ "Symmetry Island Right 5",
+ "Symmetry Island Back 6",
+ "Symmetry Island Left 7",
+ "Symmetry Island Upper Panel",
+ "Symmetry Island Scenery Outlines 5",
+ "Symmetry Island Laser Yellow 3",
+ "Symmetry Island Laser Blue 3",
+ "Symmetry Island Laser Panel",
+
+ "Orchard Apple Tree 5",
+
+ "Desert Vault Box",
+ "Desert Discard",
+ "Desert Surface 8",
+ "Desert Light Room 3",
+ "Desert Pond Room 5",
+ "Desert Flood Room 6",
+ "Desert Elevator Room Hexagonal",
+ "Desert Elevator Room Bent 3",
+ "Desert Laser Panel",
+
+ "Quarry Entry 1 Panel",
+ "Quarry Entry 2 Panel",
+ "Quarry Stoneworks Entry Left Panel",
+ "Quarry Stoneworks Entry Right Panel",
+ "Quarry Stoneworks Lower Row 6",
+ "Quarry Stoneworks Upper Row 8",
+ "Quarry Stoneworks Control Room Left",
+ "Quarry Stoneworks Control Room Right",
+ "Quarry Stoneworks Stairs Panel",
+ "Quarry Boathouse Intro Right",
+ "Quarry Boathouse Intro Left",
+ "Quarry Boathouse Front Row 5",
+ "Quarry Boathouse Back First Row 9",
+ "Quarry Boathouse Back Second Row 3",
+ "Quarry Discard",
+ "Quarry Laser Panel",
+
+ "Shadows Intro 8",
+ "Shadows Far 8",
+ "Shadows Near 5",
+ "Shadows Laser Panel",
+
+ "Keep Hedge Maze 1",
+ "Keep Hedge Maze 2",
+ "Keep Hedge Maze 3",
+ "Keep Hedge Maze 4",
+ "Keep Pressure Plates 1",
+ "Keep Pressure Plates 2",
+ "Keep Pressure Plates 3",
+ "Keep Pressure Plates 4",
+ "Keep Discard",
+ "Keep Laser Panel Hedges",
+ "Keep Laser Panel Pressure Plates",
+
+ "Shipwreck Vault Box",
+ "Shipwreck Discard",
+
+ "Monastery Outside 3",
+ "Monastery Inside 4",
+ "Monastery Laser Panel",
+
+ "Town Cargo Box Entry Panel",
+ "Town Cargo Box Discard",
+ "Town Tall Hexagonal",
+ "Town Church Entry Panel",
+ "Town Church Lattice",
+ "Town Maze Panel",
+ "Town Rooftop Discard",
+ "Town Red Rooftop 5",
+ "Town Wooden Roof Lower Row 5",
+ "Town Wooden Rooftop",
+ "Windmill Entry Panel",
+ "Town RGB House Entry Panel",
+ "Town Laser Panel",
+
+ "Town RGB House Upstairs Left",
+ "Town RGB House Upstairs Right",
+ "Town RGB House Sound Room Right",
+
+ "Windmill Theater Entry Panel",
+ "Theater Exit Left Panel",
+ "Theater Exit Right Panel",
+ "Theater Tutorial Video",
+ "Theater Desert Video",
+ "Theater Jungle Video",
+ "Theater Shipwreck Video",
+ "Theater Mountain Video",
+ "Theater Discard",
+
+ "Jungle Discard",
+ "Jungle First Row 3",
+ "Jungle Second Row 4",
+ "Jungle Popup Wall 6",
+ "Jungle Laser Panel",
+
+ "Jungle Vault Box",
+ "Jungle Monastery Garden Shortcut Panel",
+
+ "Bunker Entry Panel",
+ "Bunker Intro Left 5",
+ "Bunker Intro Back 4",
+ "Bunker Glass Room 3",
+ "Bunker UV Room 2",
+ "Bunker Laser Panel",
+
+ "Swamp Entry Panel",
+ "Swamp Intro Front 6",
+ "Swamp Intro Back 8",
+ "Swamp Between Bridges Near Row 4",
+ "Swamp Cyan Underwater 5",
+ "Swamp Platform Row 4",
+ "Swamp Platform Shortcut Right Panel",
+ "Swamp Between Bridges Far Row 4",
+ "Swamp Red Underwater 4",
+ "Swamp Purple Underwater",
+ "Swamp Beyond Rotating Bridge 4",
+ "Swamp Blue Underwater 5",
+ "Swamp Laser Panel",
+ "Swamp Laser Shortcut Right Panel",
+
+ "Treehouse First Door Panel",
+ "Treehouse Second Door Panel",
+ "Treehouse Third Door Panel",
+ "Treehouse Yellow Bridge 9",
+ "Treehouse First Purple Bridge 5",
+ "Treehouse Second Purple Bridge 7",
+ "Treehouse Green Bridge 7",
+ "Treehouse Green Bridge Discard",
+ "Treehouse Left Orange Bridge 15",
+ "Treehouse Laser Discard",
+ "Treehouse Right Orange Bridge 12",
+ "Treehouse Laser Panel",
+ "Treehouse Drawbridge Panel",
+
+ "Mountainside Discard",
+ "Mountainside Vault Box",
+ "Mountaintop River Shape",
+
+ "Tutorial First Hallway EP",
+ "Tutorial Cloud EP",
+ "Tutorial Patio Flowers EP",
+ "Tutorial Gate EP",
+ "Outside Tutorial Garden EP",
+ "Outside Tutorial Town Sewer EP",
+ "Outside Tutorial Path EP",
+ "Outside Tutorial Tractor EP",
+ "Mountainside Thundercloud EP",
+ "Glass Factory Vase EP",
+ "Symmetry Island Glass Factory Black Line Reflection EP",
+ "Symmetry Island Glass Factory Black Line EP",
+ "Desert Sand Snake EP",
+ "Desert Facade Right EP",
+ "Desert Facade Left EP",
+ "Desert Stairs Left EP",
+ "Desert Stairs Right EP",
+ "Desert Broken Wall Straight EP",
+ "Desert Broken Wall Bend EP",
+ "Desert Shore EP",
+ "Desert Island EP",
+ "Desert Pond Room Near Reflection EP",
+ "Desert Pond Room Far Reflection EP",
+ "Desert Flood Room EP",
+ "Desert Elevator EP",
+ "Quarry Shore EP",
+ "Quarry Entrance Pipe EP",
+ "Quarry Sand Pile EP",
+ "Quarry Rock Line EP",
+ "Quarry Rock Line Reflection EP",
+ "Quarry Railroad EP",
+ "Quarry Stoneworks Ramp EP",
+ "Quarry Stoneworks Lift EP",
+ "Quarry Boathouse Moving Ramp EP",
+ "Quarry Boathouse Hook EP",
+ "Shadows Quarry Stoneworks Rooftop Vent EP",
+ "Treehouse Beach Rock Shadow EP",
+ "Treehouse Beach Sand Shadow EP",
+ "Treehouse Beach Both Orange Bridges EP",
+ "Keep Red Flowers EP",
+ "Keep Purple Flowers EP",
+ "Shipwreck Circle Near EP",
+ "Shipwreck Circle Left EP",
+ "Shipwreck Circle Far EP",
+ "Shipwreck Stern EP",
+ "Shipwreck Rope Inner EP",
+ "Shipwreck Rope Outer EP",
+ "Shipwreck Couch EP",
+ "Keep Pressure Plates 1 EP",
+ "Keep Pressure Plates 2 EP",
+ "Keep Pressure Plates 3 EP",
+ "Keep Pressure Plates 4 Left Exit EP",
+ "Keep Pressure Plates 4 Right Exit EP",
+ "Keep Path EP",
+ "Keep Hedges EP",
+ "Monastery Facade Left Near EP",
+ "Monastery Facade Left Far Short EP",
+ "Monastery Facade Left Far Long EP",
+ "Monastery Facade Right Near EP",
+ "Monastery Facade Left Stairs EP",
+ "Monastery Facade Right Stairs EP",
+ "Monastery Grass Stairs EP",
+ "Monastery Left Shutter EP",
+ "Monastery Middle Shutter EP",
+ "Monastery Right Shutter EP",
+ "Windmill First Blade EP",
+ "Windmill Second Blade EP",
+ "Windmill Third Blade EP",
+ "Town Tower Underside Third EP",
+ "Town Tower Underside Fourth EP",
+ "Town Tower Underside First EP",
+ "Town Tower Underside Second EP",
+ "Town RGB House Red EP",
+ "Town RGB House Green EP",
+ "Town Maze Bridge Underside EP",
+ "Town Black Line Redirect EP",
+ "Town Black Line Church EP",
+ "Town Brown Bridge EP",
+ "Town Black Line Tower EP",
+ "Theater Eclipse EP",
+ "Theater Window EP",
+ "Theater Door EP",
+ "Theater Church EP",
+ "Jungle Long Arch Moss EP",
+ "Jungle Straight Left Moss EP",
+ "Jungle Pop-up Wall Moss EP",
+ "Jungle Short Arch Moss EP",
+ "Jungle Entrance EP",
+ "Jungle Tree Halo EP",
+ "Jungle Bamboo CCW EP",
+ "Jungle Bamboo CW EP",
+ "Jungle Green Leaf Moss EP",
+ "Monastery Garden Left EP",
+ "Monastery Garden Right EP",
+ "Monastery Wall EP",
+ "Bunker Tinted Door EP",
+ "Bunker Green Room Flowers EP",
+ "Swamp Purple Sand Middle EP",
+ "Swamp Purple Sand Top EP",
+ "Swamp Purple Sand Bottom EP",
+ "Swamp Sliding Bridge Left EP",
+ "Swamp Sliding Bridge Right EP",
+ "Swamp Cyan Underwater Sliding Bridge EP",
+ "Swamp Rotating Bridge CCW EP",
+ "Swamp Rotating Bridge CW EP",
+ "Swamp Boat EP",
+ "Swamp Long Bridge Side EP",
+ "Swamp Purple Underwater Right EP",
+ "Swamp Purple Underwater Left EP",
+ "Treehouse Buoy EP",
+ "Treehouse Right Orange Bridge EP",
+ "Treehouse Burned House Beach EP",
+ "Mountainside Cloud Cycle EP",
+ "Mountainside Bush EP",
+ "Mountainside Apparent River EP",
+ "Mountaintop River Shape EP",
+ "Mountaintop Arch Black EP",
+ "Mountaintop Arch White Right EP",
+ "Mountaintop Arch White Left EP",
+ "Mountain Bottom Floor Yellow Bridge EP",
+ "Mountain Bottom Floor Blue Bridge EP",
+ "Mountain Floor 2 Pink Bridge EP",
+ "Caves Skylight EP",
+ "Challenge Water EP",
+ "Tunnels Theater Flowers EP",
+ "Boat Desert EP",
+ "Boat Shipwreck CCW Underside EP",
+ "Boat Shipwreck Green EP",
+ "Boat Shipwreck CW Underside EP",
+ "Boat Bunker Yellow Line EP",
+ "Boat Town Long Sewer EP",
+ "Boat Tutorial EP",
+ "Boat Tutorial Reflection EP",
+ "Boat Tutorial Moss EP",
+ "Boat Cargo Box EP",
+
+ "Desert Obelisk Side 1",
+ "Desert Obelisk Side 2",
+ "Desert Obelisk Side 3",
+ "Desert Obelisk Side 4",
+ "Desert Obelisk Side 5",
+ "Monastery Obelisk Side 1",
+ "Monastery Obelisk Side 2",
+ "Monastery Obelisk Side 3",
+ "Monastery Obelisk Side 4",
+ "Monastery Obelisk Side 5",
+ "Monastery Obelisk Side 6",
+ "Treehouse Obelisk Side 1",
+ "Treehouse Obelisk Side 2",
+ "Treehouse Obelisk Side 3",
+ "Treehouse Obelisk Side 4",
+ "Treehouse Obelisk Side 5",
+ "Treehouse Obelisk Side 6",
+ "Mountainside Obelisk Side 1",
+ "Mountainside Obelisk Side 2",
+ "Mountainside Obelisk Side 3",
+ "Mountainside Obelisk Side 4",
+ "Mountainside Obelisk Side 5",
+ "Mountainside Obelisk Side 6",
+ "Quarry Obelisk Side 1",
+ "Quarry Obelisk Side 2",
+ "Quarry Obelisk Side 3",
+ "Quarry Obelisk Side 4",
+ "Quarry Obelisk Side 5",
+ "Town Obelisk Side 1",
+ "Town Obelisk Side 2",
+ "Town Obelisk Side 3",
+ "Town Obelisk Side 4",
+ "Town Obelisk Side 5",
+ "Town Obelisk Side 6",
+
+ "Caves Mountain Shortcut Panel",
+ "Caves Swamp Shortcut Panel",
+
+ "Caves Blue Tunnel Right First 4",
+ "Caves Blue Tunnel Left First 1",
+ "Caves Blue Tunnel Left Second 5",
+ "Caves Blue Tunnel Right Second 5",
+ "Caves Blue Tunnel Right Third 1",
+ "Caves Blue Tunnel Left Fourth 1",
+ "Caves Blue Tunnel Left Third 1",
+
+ "Caves First Floor Middle",
+ "Caves First Floor Right",
+ "Caves First Floor Left",
+ "Caves First Floor Grounded",
+ "Caves Lone Pillar",
+ "Caves First Wooden Beam",
+ "Caves Second Wooden Beam",
+ "Caves Third Wooden Beam",
+ "Caves Fourth Wooden Beam",
+ "Caves Right Upstairs Left Row 8",
+ "Caves Right Upstairs Right Row 3",
+ "Caves Left Upstairs Single",
+ "Caves Left Upstairs Left Row 5",
+
+ "Caves Challenge Entry Panel",
+ "Challenge Tunnels Entry Panel",
+
+ "Tunnels Vault Box",
+ "Theater Challenge Video",
+
+ "Tunnels Town Shortcut Panel",
+
+ "Caves Skylight EP",
+ "Challenge Water EP",
+ "Tunnels Theater Flowers EP",
+ "Tutorial Gate EP",
+
+ "Mountaintop Mountain Entry Panel",
+
+ "Mountain Floor 1 Light Bridge Controller",
+
+ "Mountain Floor 1 Right Row 5",
+ "Mountain Floor 1 Left Row 7",
+ "Mountain Floor 1 Back Row 3",
+ "Mountain Floor 1 Trash Pillar 2",
+ "Mountain Floor 2 Near Row 5",
+ "Mountain Floor 2 Far Row 6",
+
+ "Mountain Floor 2 Light Bridge Controller Near",
+ "Mountain Floor 2 Light Bridge Controller Far",
+
+ "Mountain Bottom Floor Yellow Bridge EP",
+ "Mountain Bottom Floor Blue Bridge EP",
+ "Mountain Floor 2 Pink Bridge EP",
+
+ "Mountain Floor 2 Elevator Discard",
+ "Mountain Bottom Floor Giant Puzzle",
+
+ "Mountain Bottom Floor Pillars Room Entry Left",
+ "Mountain Bottom Floor Pillars Room Entry Right",
+
+ "Mountain Bottom Floor Caves Entry Panel",
+
+ "Mountain Bottom Floor Left Pillar 4",
+ "Mountain Bottom Floor Right Pillar 4",
+
+ "Challenge Vault Box",
+ "Theater Challenge Video",
+ "Mountain Bottom Floor Discard",
+}
+
+OBELISK_SIDES = {
+ "Desert Obelisk Side 1",
+ "Desert Obelisk Side 2",
+ "Desert Obelisk Side 3",
+ "Desert Obelisk Side 4",
+ "Desert Obelisk Side 5",
+ "Monastery Obelisk Side 1",
+ "Monastery Obelisk Side 2",
+ "Monastery Obelisk Side 3",
+ "Monastery Obelisk Side 4",
+ "Monastery Obelisk Side 5",
+ "Monastery Obelisk Side 6",
+ "Treehouse Obelisk Side 1",
+ "Treehouse Obelisk Side 2",
+ "Treehouse Obelisk Side 3",
+ "Treehouse Obelisk Side 4",
+ "Treehouse Obelisk Side 5",
+ "Treehouse Obelisk Side 6",
+ "Mountainside Obelisk Side 1",
+ "Mountainside Obelisk Side 2",
+ "Mountainside Obelisk Side 3",
+ "Mountainside Obelisk Side 4",
+ "Mountainside Obelisk Side 5",
+ "Mountainside Obelisk Side 6",
+ "Quarry Obelisk Side 1",
+ "Quarry Obelisk Side 2",
+ "Quarry Obelisk Side 3",
+ "Quarry Obelisk Side 4",
+ "Quarry Obelisk Side 5",
+ "Town Obelisk Side 1",
+ "Town Obelisk Side 2",
+ "Town Obelisk Side 3",
+ "Town Obelisk Side 4",
+ "Town Obelisk Side 5",
+ "Town Obelisk Side 6",
+}
+
+ALL_LOCATIONS_TO_ID: Dict[str, int] = {}
+
+AREA_LOCATION_GROUPS: Dict[str, Set[str]] = {}
+
+
+def get_id(entity_hex: str) -> int:
+ """
+ Calculates the location ID for any given location
+ """
+
+ return cast(int, static_witness_logic.ENTITIES_BY_HEX[entity_hex]["id"])
+
+
+def get_event_name(entity_hex: str) -> str:
+ """
+ Returns the event name of any given panel.
+ """
+
+ action = " Opened" if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] == "Door" else " Solved"
+
+ return cast(str, static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"]) + action
+
+
+ALL_LOCATIONS_TO_IDS = {
+ panel_obj["checkName"]: get_id(chex)
+ for chex, panel_obj in static_witness_logic.ENTITIES_BY_HEX.items()
+ if panel_obj["id"]
+}
+
+ALL_LOCATIONS_TO_IDS = dict(
+ sorted(ALL_LOCATIONS_TO_IDS.items(), key=lambda loc: loc[1])
+)
+
+for key, item in ALL_LOCATIONS_TO_IDS.items():
+ ALL_LOCATIONS_TO_ID[key] = item
+
+for loc in ALL_LOCATIONS_TO_IDS:
+ area = static_witness_logic.ENTITIES_BY_NAME[loc]["area"]["name"]
+ AREA_LOCATION_GROUPS.setdefault(area, set()).add(loc)
diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py
new file mode 100644
index 000000000000..a9175c0c30b3
--- /dev/null
+++ b/worlds/witness/data/static_logic.py
@@ -0,0 +1,303 @@
+from collections import defaultdict
+from typing import Any, Dict, List, Optional, Set, Tuple
+
+from Utils import cache_argsless
+
+from .item_definition_classes import (
+ CATEGORY_NAME_MAPPINGS,
+ DoorItemDefinition,
+ ItemCategory,
+ ItemDefinition,
+ ProgressiveItemDefinition,
+ WeightedItemDefinition,
+)
+from .utils import (
+ WitnessRule,
+ define_new_region,
+ get_items,
+ get_sigma_expert_logic,
+ get_sigma_normal_logic,
+ get_vanilla_logic,
+ logical_or_witness_rules,
+ parse_lambda,
+)
+
+
+class StaticWitnessLogicObj:
+ def __init__(self, lines: Optional[List[str]] = None) -> None:
+ if lines is None:
+ lines = get_sigma_normal_logic()
+
+ # All regions with a list of panels in them and the connections to other regions, before logic adjustments
+ self.ALL_REGIONS_BY_NAME: Dict[str, Dict[str, Any]] = {}
+ self.ALL_AREAS_BY_NAME: Dict[str, Dict[str, Any]] = {}
+ self.CONNECTIONS_WITH_DUPLICATES: Dict[str, Dict[str, Set[WitnessRule]]] = defaultdict(lambda: defaultdict(set))
+ self.STATIC_CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = {}
+
+ self.ENTITIES_BY_HEX: Dict[str, Dict[str, Any]] = {}
+ self.ENTITIES_BY_NAME: Dict[str, Dict[str, Any]] = {}
+ self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = {}
+
+ self.OBELISK_SIDE_ID_TO_EP_HEXES: Dict[int, Set[int]] = {}
+
+ self.EP_TO_OBELISK_SIDE: Dict[str, str] = {}
+
+ self.ENTITY_ID_TO_NAME: Dict[str, str] = {}
+
+ self.read_logic_file(lines)
+ self.reverse_connections()
+ self.combine_connections()
+
+ def read_logic_file(self, lines: List[str]) -> None:
+ """
+ Reads the logic file and does the initial population of data structures
+ """
+
+ current_region = {}
+ current_area: Dict[str, Any] = {
+ "name": "Misc",
+ "regions": [],
+ }
+ self.ALL_AREAS_BY_NAME["Misc"] = current_area
+
+ for line in lines:
+ if line == "" or line[0] == "#":
+ continue
+
+ if line[-1] == ":":
+ new_region_and_connections = define_new_region(line)
+ current_region = new_region_and_connections[0]
+ region_name = current_region["name"]
+ self.ALL_REGIONS_BY_NAME[region_name] = current_region
+ for connection in new_region_and_connections[1]:
+ self.CONNECTIONS_WITH_DUPLICATES[region_name][connection[0]].add(connection[1])
+ current_area["regions"].append(region_name)
+ continue
+
+ if line[0] == "=":
+ area_name = line[2:-2]
+ current_area = {
+ "name": area_name,
+ "regions": [],
+ }
+ self.ALL_AREAS_BY_NAME[area_name] = current_area
+ continue
+
+ line_split = line.split(" - ")
+
+ location_id = line_split.pop(0)
+
+ entity_name_full = line_split.pop(0)
+
+ entity_hex = entity_name_full[0:7]
+ entity_name = entity_name_full[9:-1]
+
+ required_panel_lambda = line_split.pop(0)
+
+ full_entity_name = current_region["shortName"] + " " + entity_name
+
+ if location_id == "Door" or location_id == "Laser":
+ self.ENTITIES_BY_HEX[entity_hex] = {
+ "checkName": full_entity_name,
+ "entity_hex": entity_hex,
+ "region": None,
+ "id": None,
+ "entityType": location_id,
+ "area": current_area,
+ }
+
+ self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex]
+
+ self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = {
+ "entities": parse_lambda(required_panel_lambda)
+ }
+
+ # Lasers and Doors exist in a region, but don't have a regional *requirement*
+ # If a laser is activated, you don't need to physically walk up to it for it to count
+ # As such, logically, they behave more as if they were part of the "Entry" region
+ self.ALL_REGIONS_BY_NAME["Entry"]["entities"].append(entity_hex)
+ # However, it will also be important to keep track of their physical location for postgame purposes.
+ current_region["physical_entities"].append(entity_hex)
+ continue
+
+ required_item_lambda = line_split.pop(0)
+
+ laser_names = {
+ "Laser",
+ "Laser Hedges",
+ "Laser Pressure Plates",
+ }
+ is_vault_or_video = "Vault" in entity_name or "Video" in entity_name
+
+ if "Discard" in entity_name:
+ location_type = "Discard"
+ elif is_vault_or_video or entity_name == "Tutorial Gate Close":
+ location_type = "Vault"
+ elif entity_name in laser_names:
+ location_type = "Laser"
+ elif "Obelisk Side" in entity_name:
+ location_type = "Obelisk Side"
+ elif "EP" in entity_name:
+ location_type = "EP"
+ else:
+ location_type = "General"
+
+ required_items = parse_lambda(required_item_lambda)
+ required_panels = parse_lambda(required_panel_lambda)
+
+ required_items = frozenset(required_items)
+
+ requirement = {
+ "entities": required_panels,
+ "items": required_items
+ }
+
+ if location_type == "Obelisk Side":
+ eps = set(next(iter(required_panels)))
+ eps -= {"Theater to Tunnels"}
+
+ eps_ints = {int(h, 16) for h in eps}
+
+ self.OBELISK_SIDE_ID_TO_EP_HEXES[int(entity_hex, 16)] = eps_ints
+ for ep_hex in eps:
+ self.EP_TO_OBELISK_SIDE[ep_hex] = entity_hex
+
+ self.ENTITIES_BY_HEX[entity_hex] = {
+ "checkName": full_entity_name,
+ "entity_hex": entity_hex,
+ "region": current_region,
+ "id": int(location_id),
+ "entityType": location_type,
+ "area": current_area,
+ }
+
+ self.ENTITY_ID_TO_NAME[entity_hex] = full_entity_name
+
+ self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex]
+ self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = requirement
+
+ current_region["entities"].append(entity_hex)
+ current_region["physical_entities"].append(entity_hex)
+
+ def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]) -> None:
+ target = connection[0]
+ traversal_options = connection[1]
+
+ # Reverse this connection with all its possibilities, except the ones marked as "OneWay".
+ for requirement in traversal_options:
+ remaining_options = set()
+ for option in requirement:
+ if not any(req == "TrueOneWay" for req in option):
+ remaining_options.add(option)
+
+ if remaining_options:
+ self.CONNECTIONS_WITH_DUPLICATES[target][source_region].add(frozenset(remaining_options))
+
+ def reverse_connections(self) -> None:
+ # Iterate all connections
+ for region_name, connections in list(self.CONNECTIONS_WITH_DUPLICATES.items()):
+ for connection in connections.items():
+ self.reverse_connection(region_name, connection)
+
+ def combine_connections(self) -> None:
+ # All regions need to be present, and this dict is copied later - Thus, defaultdict is not the correct choice.
+ self.STATIC_CONNECTIONS_BY_REGION_NAME = {region_name: set() for region_name in self.ALL_REGIONS_BY_NAME}
+
+ for source, connections in self.CONNECTIONS_WITH_DUPLICATES.items():
+ for target, requirement in connections.items():
+ combined_req = logical_or_witness_rules(requirement)
+ self.STATIC_CONNECTIONS_BY_REGION_NAME[source].add((target, combined_req))
+
+
+# Item data parsed from WitnessItems.txt
+ALL_ITEMS: Dict[str, ItemDefinition] = {}
+_progressive_lookup: Dict[str, str] = {}
+
+
+def parse_items() -> None:
+ """
+ Parses currently defined items from WitnessItems.txt
+ """
+
+ lines: List[str] = get_items()
+ current_category: ItemCategory = ItemCategory.SYMBOL
+
+ for line in lines:
+ # Skip empty lines and comments.
+ if line == "" or line[0] == "#":
+ continue
+
+ # If this line is a category header, update our cached category.
+ if line in CATEGORY_NAME_MAPPINGS.keys():
+ current_category = CATEGORY_NAME_MAPPINGS[line]
+ continue
+
+ line_split = line.split(" - ")
+
+ item_code = int(line_split[0])
+ item_name = line_split[1]
+ arguments: List[str] = line_split[2].split(",") if len(line_split) >= 3 else []
+
+ if current_category in [ItemCategory.DOOR, ItemCategory.LASER]:
+ # Map doors to IDs.
+ ALL_ITEMS[item_name] = DoorItemDefinition(item_code, current_category, arguments)
+ elif current_category == ItemCategory.TRAP or current_category == ItemCategory.FILLER:
+ # Read filler weights.
+ weight = int(arguments[0]) if len(arguments) >= 1 else 1
+ ALL_ITEMS[item_name] = WeightedItemDefinition(item_code, current_category, weight)
+ elif arguments:
+ # Progressive items.
+ ALL_ITEMS[item_name] = ProgressiveItemDefinition(item_code, current_category, arguments)
+ for child_item in arguments:
+ _progressive_lookup[child_item] = item_name
+ else:
+ ALL_ITEMS[item_name] = ItemDefinition(item_code, current_category)
+
+
+def get_parent_progressive_item(item_name: str) -> str:
+ """
+ Returns the name of the item's progressive parent, if there is one, or the item's name if not.
+ """
+ return _progressive_lookup.get(item_name, item_name)
+
+
+@cache_argsless
+def get_vanilla() -> StaticWitnessLogicObj:
+ return StaticWitnessLogicObj(get_vanilla_logic())
+
+
+@cache_argsless
+def get_sigma_normal() -> StaticWitnessLogicObj:
+ return StaticWitnessLogicObj(get_sigma_normal_logic())
+
+
+@cache_argsless
+def get_sigma_expert() -> StaticWitnessLogicObj:
+ return StaticWitnessLogicObj(get_sigma_expert_logic())
+
+
+def __getattr__(name: str) -> StaticWitnessLogicObj:
+ if name == "vanilla":
+ return get_vanilla()
+ if name == "sigma_normal":
+ return get_sigma_normal()
+ if name == "sigma_expert":
+ return get_sigma_expert()
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+
+
+parse_items()
+
+ALL_REGIONS_BY_NAME = get_sigma_normal().ALL_REGIONS_BY_NAME
+ALL_AREAS_BY_NAME = get_sigma_normal().ALL_AREAS_BY_NAME
+STATIC_CONNECTIONS_BY_REGION_NAME = get_sigma_normal().STATIC_CONNECTIONS_BY_REGION_NAME
+
+ENTITIES_BY_HEX = get_sigma_normal().ENTITIES_BY_HEX
+ENTITIES_BY_NAME = get_sigma_normal().ENTITIES_BY_NAME
+STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = get_sigma_normal().STATIC_DEPENDENT_REQUIREMENTS_BY_HEX
+
+OBELISK_SIDE_ID_TO_EP_HEXES = get_sigma_normal().OBELISK_SIDE_ID_TO_EP_HEXES
+
+EP_TO_OBELISK_SIDE = get_sigma_normal().EP_TO_OBELISK_SIDE
+
+ENTITY_ID_TO_NAME = get_sigma_normal().ENTITY_ID_TO_NAME
diff --git a/worlds/witness/data/utils.py b/worlds/witness/data/utils.py
new file mode 100644
index 000000000000..f89aaf7d3e18
--- /dev/null
+++ b/worlds/witness/data/utils.py
@@ -0,0 +1,257 @@
+from math import floor
+from pkgutil import get_data
+from random import Random
+from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple, TypeVar
+
+T = TypeVar("T")
+
+# A WitnessRule is just an or-chain of and-conditions.
+# It represents the set of all options that could fulfill this requirement.
+# E.g. if something requires "Dots or (Shapers and Stars)", it'd be represented as: {{"Dots"}, {"Shapers, "Stars"}}
+# {} is an unusable requirement.
+# {{}} is an always usable requirement.
+WitnessRule = FrozenSet[FrozenSet[str]]
+
+
+def weighted_sample(world_random: Random, population: List[T], weights: List[float], k: int) -> List[T]:
+ positions = range(len(population))
+ indices: List[int] = []
+ while True:
+ needed = k - len(indices)
+ if not needed:
+ break
+ for i in world_random.choices(positions, weights, k=needed):
+ if weights[i]:
+ weights[i] = 0.0
+ indices.append(i)
+ return [population[i] for i in indices]
+
+
+def build_weighted_int_list(inputs: Collection[float], total: int) -> List[int]:
+ """
+ Converts a list of floats to a list of ints of a given length, using the Largest Remainder Method.
+ """
+
+ # Scale the inputs to sum to the desired total.
+ scale_factor: float = total / sum(inputs)
+ scaled_input = [x * scale_factor for x in inputs]
+
+ # Generate whole number counts, always rounding down.
+ rounded_output: List[int] = [floor(x) for x in scaled_input]
+ rounded_sum = sum(rounded_output)
+
+ # If the output's total is insufficient, increment the value that has the largest remainder until we meet our goal.
+ remainders: List[float] = [real - rounded for real, rounded in zip(scaled_input, rounded_output)]
+ while rounded_sum < total:
+ max_remainder = max(remainders)
+ if max_remainder == 0:
+ break
+
+ # Consume the remainder and increment the total for the given target.
+ max_remainder_index = remainders.index(max_remainder)
+ remainders[max_remainder_index] = 0
+ rounded_output[max_remainder_index] += 1
+ rounded_sum += 1
+
+ return rounded_output
+
+
+def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str, WitnessRule]]]:
+ """
+ Returns a region object by parsing a line in the logic file
+ """
+
+ region_string = region_string[:-1]
+ line_split = region_string.split(" - ")
+
+ region_name_full = line_split.pop(0)
+
+ region_name_split = region_name_full.split(" (")
+
+ region_name = region_name_split[0]
+ region_name_simple = region_name_split[1][:-1]
+
+ options = set()
+
+ for _ in range(len(line_split) // 2):
+ connected_region = line_split.pop(0)
+ corresponding_lambda = line_split.pop(0)
+
+ options.add(
+ (connected_region, parse_lambda(corresponding_lambda))
+ )
+
+ region_obj = {
+ "name": region_name,
+ "shortName": region_name_simple,
+ "entities": [],
+ "physical_entities": [],
+ }
+ return region_obj, options
+
+
+def parse_lambda(lambda_string: str) -> WitnessRule:
+ """
+ Turns a lambda String literal like this: a | b & c
+ into a set of sets like this: {{a}, {b, c}}
+ The lambda has to be in DNF.
+ """
+ if lambda_string == "True":
+ return frozenset([frozenset()])
+ split_ands = set(lambda_string.split(" | "))
+ return frozenset({frozenset(a.split(" & ")) for a in split_ands})
+
+
+_adjustment_file_cache = {}
+
+
+def get_adjustment_file(adjustment_file: str) -> List[str]:
+ if adjustment_file not in _adjustment_file_cache:
+ data = get_data(__name__, adjustment_file)
+ if data is None:
+ raise FileNotFoundError(f"Could not find {adjustment_file}")
+ _adjustment_file_cache[adjustment_file] = [line.strip() for line in data.decode("utf-8").split("\n")]
+
+ return _adjustment_file_cache[adjustment_file]
+
+
+def get_disable_unrandomized_list() -> List[str]:
+ return get_adjustment_file("settings/Exclusions/Disable_Unrandomized.txt")
+
+
+def get_early_caves_list() -> List[str]:
+ return get_adjustment_file("settings/Early_Caves.txt")
+
+
+def get_early_caves_start_list() -> List[str]:
+ return get_adjustment_file("settings/Early_Caves_Start.txt")
+
+
+def get_symbol_shuffle_list() -> List[str]:
+ return get_adjustment_file("settings/Symbol_Shuffle.txt")
+
+
+def get_complex_doors() -> List[str]:
+ return get_adjustment_file("settings/Door_Shuffle/Complex_Doors.txt")
+
+
+def get_simple_doors() -> List[str]:
+ return get_adjustment_file("settings/Door_Shuffle/Simple_Doors.txt")
+
+
+def get_complex_door_panels() -> List[str]:
+ return get_adjustment_file("settings/Door_Shuffle/Complex_Door_Panels.txt")
+
+
+def get_complex_additional_panels() -> List[str]:
+ return get_adjustment_file("settings/Door_Shuffle/Complex_Additional_Panels.txt")
+
+
+def get_simple_panels() -> List[str]:
+ return get_adjustment_file("settings/Door_Shuffle/Simple_Panels.txt")
+
+
+def get_simple_additional_panels() -> List[str]:
+ return get_adjustment_file("settings/Door_Shuffle/Simple_Additional_Panels.txt")
+
+
+def get_boat() -> List[str]:
+ return get_adjustment_file("settings/Door_Shuffle/Boat.txt")
+
+
+def get_laser_shuffle() -> List[str]:
+ return get_adjustment_file("settings/Laser_Shuffle.txt")
+
+
+def get_audio_logs() -> List[str]:
+ return get_adjustment_file("settings/Audio_Logs.txt")
+
+
+def get_ep_all_individual() -> List[str]:
+ return get_adjustment_file("settings/EP_Shuffle/EP_All.txt")
+
+
+def get_ep_obelisks() -> List[str]:
+ return get_adjustment_file("settings/EP_Shuffle/EP_Sides.txt")
+
+
+def get_obelisk_keys() -> List[str]:
+ return get_adjustment_file("settings/Door_Shuffle/Obelisk_Keys.txt")
+
+
+def get_ep_easy() -> List[str]:
+ return get_adjustment_file("settings/EP_Shuffle/EP_Easy.txt")
+
+
+def get_ep_no_eclipse() -> List[str]:
+ return get_adjustment_file("settings/EP_Shuffle/EP_NoEclipse.txt")
+
+
+def get_vault_exclusion_list() -> List[str]:
+ return get_adjustment_file("settings/Exclusions/Vaults.txt")
+
+
+def get_discard_exclusion_list() -> List[str]:
+ return get_adjustment_file("settings/Exclusions/Discards.txt")
+
+
+def get_caves_except_path_to_challenge_exclusion_list() -> List[str]:
+ return get_adjustment_file("settings/Exclusions/Caves_Except_Path_To_Challenge.txt")
+
+
+def get_elevators_come_to_you() -> List[str]:
+ return get_adjustment_file("settings/Door_Shuffle/Elevators_Come_To_You.txt")
+
+
+def get_sigma_normal_logic() -> List[str]:
+ return get_adjustment_file("WitnessLogic.txt")
+
+
+def get_sigma_expert_logic() -> List[str]:
+ return get_adjustment_file("WitnessLogicExpert.txt")
+
+
+def get_vanilla_logic() -> List[str]:
+ return get_adjustment_file("WitnessLogicVanilla.txt")
+
+
+def get_items() -> List[str]:
+ return get_adjustment_file("WitnessItems.txt")
+
+
+def optimize_witness_rule(witness_rule: WitnessRule) -> WitnessRule:
+ """Removes any redundant terms from a logical formula in disjunctive normal form.
+ This means removing any terms that are a superset of any other term get removed.
+ This is possible because of the boolean absorption law: a | (a & b) = a"""
+ to_remove = set()
+
+ for option1 in witness_rule:
+ for option2 in witness_rule:
+ if option2 < option1:
+ to_remove.add(option1)
+
+ return witness_rule - to_remove
+
+
+def logical_and_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRule:
+ """
+ performs the "and" operator on a list of logical formula in disjunctive normal form, represented as a set of sets.
+ A logical formula might look like this: {{a, b}, {c, d}}, which would mean "a & b | c & d".
+ These can be easily and-ed by just using the boolean distributive law: (a | b) & c = a & c | a & b.
+ """
+ current_overall_requirement: FrozenSet[FrozenSet[str]] = frozenset({frozenset()})
+
+ for next_dnf_requirement in witness_rules:
+ new_requirement: Set[FrozenSet[str]] = set()
+
+ for option1 in current_overall_requirement:
+ for option2 in next_dnf_requirement:
+ new_requirement.add(option1 | option2)
+
+ current_overall_requirement = frozenset(new_requirement)
+
+ return optimize_witness_rule(current_overall_requirement)
+
+
+def logical_or_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRule:
+ return optimize_witness_rule(frozenset.union(*witness_rules))
diff --git a/worlds/witness/docs/en_The Witness.md b/worlds/witness/docs/en_The Witness.md
index 2ae478bed06f..6882ed3fdedf 100644
--- a/worlds/witness/docs/en_The Witness.md
+++ b/worlds/witness/docs/en_The Witness.md
@@ -1,8 +1,8 @@
# The Witness
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -10,12 +10,13 @@ config file.
Puzzles are randomly generated using the popular [Sigma Rando](https://github.com/sigma144/witness-randomizer).
They are made to be similar to the original game, but with different solutions.
-Ontop of that each puzzle symbol (Squares, Stars, Dots, etc.) is now an item.
+On top of that, each puzzle symbol (Squares, Stars, Dots, etc.) is now an item.
Panels with puzzle symbols on them are now locked initially.
## What is a "check" in The Witness?
Solving the last panel in a row of panels or an important standalone panel will count as a check, and send out an item.
+It is also possible to add Environmental Puzzles into the location pool via the "Shuffle Environmental Puzzles" option.
## What "items" can you unlock in The Witness?
@@ -24,7 +25,7 @@ This includes symbols such as "Dots", "Black/White Squares", "Colored Squares",
Alternatively (or additionally), you can play "Door shuffle", where some doors won't open until you receive their "key".
-Receiving lasers as items is also a possible setting.
+You can also set lasers to be items you can receive.
## What else can I find in the world?
@@ -32,7 +33,7 @@ By default, the audio logs scattered around the world will have 10 hints for you
Example: "Shipwreck Vault contains Triangles".
-## The Jungle, Orchard, Forest and Color House aren't randomized. What gives?
+## The Jungle, Orchard, Forest and Color Bunker aren't randomized. What gives?
There are limitations to what can currently be randomized in The Witness.
There is an option to turn these non-randomized panels off, called "disable_non_randomized" in your yaml file. This will also slightly change the activation requirement of certain panels, detailed [here](https://github.com/sigma144/witness-randomizer/wiki/Activation-Triggers).
@@ -46,4 +47,4 @@ In this case, the generator will make its best attempt to adjust logic according
One of the use cases of this could be to pre-open a specific door or pre-activate a single laser.
In "shuffle_EPs: obelisk_sides", any Environmental Puzzles in exclude_locations will be pre-completed and not considered for their Obelisk Side.
-If every Environmental Puzzle on an Obelisk Side is pre-completed, that side disappears from the location pool entirely.
\ No newline at end of file
+If every Environmental Puzzle on an Obelisk Side is pre-completed, that side disappears from the location pool entirely.
diff --git a/worlds/witness/docs/setup_en.md b/worlds/witness/docs/setup_en.md
index 94a50846f987..7b6d631198f9 100644
--- a/worlds/witness/docs/setup_en.md
+++ b/worlds/witness/docs/setup_en.md
@@ -29,8 +29,10 @@ To continue an earlier game:
## Archipelago Text Client
-It is recommended to have Archipelago's Text Client open on the side to keep track of what items you receive and send, as The Witness has no in-game messages.
- Or use the Auto-Tracker!
+It is recommended to have Archipelago's Text Client open on the side to keep track of what items you receive and send.
+ The Witness does display received and sent items in-game, but only for a short time, and the messages are easy to miss while playing.
+
+ Of course, you can also use the Auto-Tracker!
## Auto-Tracking
@@ -41,4 +43,4 @@ The Witness has a fully functional map tracker that supports auto-tracking.
3. Click on the "AP" symbol at the top.
4. Enter the AP address, slot name and password.
-The rest should take care of itself! Items and checks will be marked automatically, and it even knows your settings - It will hide checks & adjust logic accordingly. Note that the tracker may be out of date.
\ No newline at end of file
+The rest should take care of itself! Items and checks will be marked automatically, and it even knows your options - It will hide checks & adjust logic accordingly.
diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py
index 5d8bd5d3702c..a1ca1b081d3c 100644
--- a/worlds/witness/hints.py
+++ b/worlds/witness/hints.py
@@ -1,164 +1,117 @@
-from BaseClasses import MultiWorld
-from .Options import is_option_enabled, get_option_value
-
-joke_hints = [
- "Quaternions break my brain",
- "Eclipse has nothing, but you should do it anyway.",
- "Beep",
- "Putting in custom subtitles shouldn't have been as hard as it was...",
- "BK mode is right around the corner.",
- "You can do it!",
- "I believe in you!",
- "The person playing is cute. <3",
- "dash dot, dash dash dash,\ndash, dot dot dot dot, dot dot,\ndash dot, dash dash dot",
- "When you think about it, there are actually a lot of bubbles in a stream.",
- "Never gonna give you up\nNever gonna let you down\nNever gonna run around and desert you",
- "Thanks to the Archipelago developers for making this possible.",
- "Have you tried ChecksFinder?\nIf you like puzzles, you might enjoy it!",
- "Have you tried Dark Souls III?\nA tough game like this feels better when friends are helping you!",
- "Have you tried Donkey Kong Country 3?\nA legendary game from a golden age of platformers!",
- "Have you tried Factorio?\nAlone in an unknown multiworld. Sound familiar?",
- "Have you tried Final Fantasy?\nExperience a classic game improved to fit modern standards!",
- "Have you tried Hollow Knight?\nAnother independent hit revolutionising a genre!",
- "Have you tried A Link to the Past?\nThe Archipelago game that started it all!",
- "Have you tried Meritous?\nYou should know that obscure games are often groundbreaking!",
- "Have you tried Ocarina of Time?\nOne of the biggest randomizers, big inspiration for this one's features!",
- "Have you tried Raft?\nHaven't you always wanted to explore the ocean surrounding this island?",
- "Have you tried Risk of Rain 2?\nI haven't either. But I hear it's incredible!",
- "Have you tried Rogue Legacy?\nAfter solving so many puzzles it's the perfect way to rest your \"thinking\" brain.",
- "Have you tried Secret of Evermore?\nI haven't either. But I hear it's great!",
- "Have you tried Slay the Spire?\nExperience the thrill of combat without needing fast fingers!",
- "Have you tried SMZ3?\nWhy play one incredible game when you can play 2 at once?",
- "Have you tried Starcraft 2?\nUse strategy and management to crush your enemies!",
- "Have you tried Super Mario 64?\n3-dimensional games like this owe everything to that game.",
- "Have you tried Super Metroid?\nA classic game, yet still one of the best in the genre.",
- "Have you tried Timespinner?\nEveryone who plays it ends up loving it!",
- "Have you tried VVVVVV?\nExperience the essence of gaming distilled into its purest form!",
- "Have you tried The Witness?\nOh. I guess you already have. Thanks for playing!",
- "Have you tried Super Mario World?\nI don't think I need to tell you that it is beloved by many.",
- "Have you tried Overcooked 2?\nWhen you're done relaxing with puzzles, use your energy to yell at your friends.",
- "Have you tried Zillion?\nMe neither. But it looks fun. So, let's try something new together?",
- "Have you tried Hylics 2?\nStop motion might just be the epitome of unique art styles.",
- "Have you tried Pokemon Red&Blue?\nA cute pet collecting game that fascinated an entire generation.",
- "Have you tried Lufia II?\nRoguelites are not just a 2010s phenomenon, turns out.",
- "Have you tried Minecraft?\nI have recently learned this is a question that needs to be asked.",
- "Have you tried Subnautica?\nIf you like this game's lonely atmosphere, I would suggest you try it.",
-
- "Have you tried Sonic Adventure 2?\nIf the silence on this island is getting to you, "
- "there aren't many games more energetic.",
-
- "Waiting to get your items?\nTry BK Sudoku! Make progress even while stuck.",
-
- "Have you tried Adventure?\n...Holy crud, that game is 17 years older than me.",
- "Have you tried Muse Dash?\nRhythm game with cute girls!\n(Maybe skip if you don't like the Jungle panels)",
- "Have you tried Clique?\nIt's certainly a lot less complicated than this game!",
- "Have you tried Bumper Stickers?\nDecades after its inception, people are still inventing unique twists on the match-3 genre.",
- "Have you tried DLC Quest?\nI know you all like parody games.\nI got way too many requests to make a randomizer for \"The Looker\".",
- "Have you tried Doom?\nI wonder if a smart fridge can connect to Archipelago.",
- "Have you tried Kingdom Hearts II?\nI'll wait for you to name a more epic crossover.",
- "Have you tried Link's Awakening DX?\nHopefully, Link won't be obsessed with circles when he wakes up.",
- "Have you tried The Messenger?\nOld ideas made new again. It's how all art is made.",
- "Have you tried Mega Man Battle Network 3?\nIt's a Mega Man RPG. How could you not want to try that?",
- "Have you tried Noita?\nIf you like punishing yourself, you will like it.",
- "Have you tried Stardew Valley?\nThe Farming game that gave a damn. It's so easy to lose hours and days to it...",
- "Have you tried The Legend of Zelda?\nIn some sense, it was the starting point of \"adventure\" in video games.",
- "Have you tried Undertale?\nI hope I'm not the 10th person to ask you that. But it's, like, really good.",
- "Have you tried Wargroove?\nI'm glad that for every abandoned series, enough people are yearning for its return that one of them will know how to code.",
- "Have you tried Blasphemous?\nYou haven't? Blasphemy!\n...Sorry. You should try it, though!",
-
- "One day I was fascinated by the subject of generation of waves by wind.",
- "I don't like sandwiches. Why would you think I like sandwiches? Have you ever seen me with a sandwich?",
- "Where are you right now?\nI'm at soup!\nWhat do you mean you're at soup?",
- "Remember to ask in the Archipelago Discord what the Functioning Brain does.",
- "Don't use your puzzle skips, you might need them later.",
- "For an extra challenge, try playing blindfolded.",
- "Go to the top of the mountain and see if you can see your house.",
- "Yellow = Red + Green\nCyan = Green + Blue\nMagenta = Red + Blue",
- "Maybe that panel really is unsolvable.",
- "Did you make sure it was plugged in?",
- "Do not look into laser with remaining eye.",
- "Try pressing Space to jump.",
- "The Witness is a Doom clone.\nJust replace the demons with puzzles",
- "Test Hint please ignore",
- "Shapers can never be placed outside the panel boundaries, even if subtracted.",
- "The Keep laser panels use the same trick on both sides!",
- "Can't get past a door? Try going around. Can't go around? Try building a nether portal.",
- "We've been trying to reach you about your car's extended warranty.",
- "I hate this game. I hate this game. I hate this game.\n- Chess player Bobby Fischer",
- "Dear Mario,\nPlease come to the castle. I've baked a cake for you!",
- "Have you tried waking up?\nYeah, me neither.",
- "Why do they call it The Witness, when wit game the player view play of with the game.",
- "THE WIND FISH IN NAME ONLY, FOR IT IS NEITHER",
- "Like this game?\nTry The Wit.nes, Understand, INSIGHT, Taiji What the Witness?, and Tametsi.",
- "In a race, It's survival of the Witnesst.",
- "This hint has been removed. We apologize for your inconvenience.",
- "O-----------",
- "Circle is draw\nSquare is separate\nLine is win",
- "Circle is draw\nStar is pair\nLine is win",
- "Circle is draw\nCircle is copy\nLine is win",
- "Circle is draw\nDot is eat\nLine is win",
- "Circle is start\nWalk is draw\nLine is win",
- "Circle is start\nLine is win\nWitness is you",
- "Can't find any items?\nConsider a relaxing boat trip around the island!",
- "Don't forget to like, comment, and subscribe.",
- "Ah crap, gimme a second.\n[papers rustling]\nSorry, nothing.",
- "Trying to get a hint? Too bad.",
- "Here's a hint: Get good at the game.",
- "I'm still not entirely sure what we're witnessing here.",
- "Have you found a red page yet? No? Then have you found a blue page?",
- "And here we see the Witness player, seeking answers where there are none-\nDid someone turn on the loudspeaker?",
-
- "Hints suggested by:\nIHNN, Beaker, MrPokemon11, Ember, TheM8, NewSoupVi,"
- "KF, Yoshi348, Berserker, BowlinJim, oddGarrett, Pink Switch.",
-]
-
-
-def get_always_hint_items(multiworld: MultiWorld, player: int):
+import logging
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union
+
+from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
+
+from .data import static_logic as static_witness_logic
+from .data.utils import weighted_sample
+from .player_items import WitnessItem
+
+if TYPE_CHECKING:
+ from . import WitnessWorld
+
+CompactItemData = Tuple[str, Union[str, int], int]
+
+
+@dataclass
+class WitnessLocationHint:
+ location: Location
+ hint_came_from_location: bool
+
+ # If a hint gets added to a set twice, but once as an item hint and once as a location hint, those are the same
+ def __hash__(self) -> int:
+ return hash(self.location)
+
+ def __eq__(self, other: Any) -> bool:
+ if not isinstance(other, WitnessLocationHint):
+ return False
+ return self.location == other.location
+
+
+@dataclass
+class WitnessWordedHint:
+ wording: str
+ location: Optional[Location] = None
+ area: Optional[str] = None
+ area_amount: Optional[int] = None
+
+
+def get_always_hint_items(world: "WitnessWorld") -> List[str]:
always = [
"Boat",
- "Caves Exits to Main Island",
+ "Caves Shortcuts",
"Progressive Dots",
]
- difficulty = get_option_value(multiworld, player, "puzzle_randomization")
- discards = is_option_enabled(multiworld, player, "shuffle_discarded_panels")
- wincon = get_option_value(multiworld, player, "victory_condition")
+ difficulty = world.options.puzzle_randomization
+ discards = world.options.shuffle_discarded_panels
+ wincon = world.options.victory_condition
if discards:
- if difficulty == 1:
+ if difficulty == "sigma_expert":
always.append("Arrows")
else:
always.append("Triangles")
- if wincon == 0:
- always.append("Mountain Bottom Floor Final Room Entry (Door)")
+ if wincon == "elevator":
+ always += ["Mountain Bottom Floor Pillars Room Entry (Door)", "Mountain Bottom Floor Doors"]
+
+ if wincon == "challenge":
+ always += ["Challenge Entry (Panel)", "Caves Panels"]
return always
-def get_always_hint_locations(multiworld: MultiWorld, player: int):
- return {
+def get_always_hint_locations(world: "WitnessWorld") -> List[str]:
+ always = [
"Challenge Vault Box",
"Mountain Bottom Floor Discard",
"Theater Eclipse EP",
"Shipwreck Couch EP",
"Mountainside Cloud Cycle EP",
- }
+ ]
+
+ # Add Obelisk Sides that contain EPs that are meant to be hinted, if they are necessary to complete the Obelisk Side
+ if "0x339B6" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES:
+ always.append("Town Obelisk Side 6") # Eclipse EP
+ if "0x3388F" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES:
+ always.append("Treehouse Obelisk Side 4") # Couch EP
-def get_priority_hint_items(multiworld: MultiWorld, player: int):
+ if "0x335AE" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES:
+ always.append("Mountainside Obelisk Side 1") # Cloud Cycle EP.
+
+ return always
+
+
+def get_priority_hint_items(world: "WitnessWorld") -> List[str]:
priority = {
"Caves Mountain Shortcut (Door)",
"Caves Swamp Shortcut (Door)",
- "Negative Shapers",
- "Sound Dots",
- "Colored Dots",
- "Stars + Same Colored Symbol",
"Swamp Entry (Panel)",
"Swamp Laser Shortcut (Door)",
}
- if is_option_enabled(multiworld, player, "shuffle_lasers"):
+ if world.options.shuffle_symbols:
+ symbols = [
+ "Progressive Dots",
+ "Progressive Stars",
+ "Shapers",
+ "Rotated Shapers",
+ "Negative Shapers",
+ "Arrows",
+ "Triangles",
+ "Eraser",
+ "Black/White Squares",
+ "Colored Squares",
+ "Sound Dots",
+ "Progressive Symmetry"
+ ]
+
+ priority.update(world.random.sample(symbols, 5))
+
+ if world.options.shuffle_lasers:
lasers = [
"Symmetry Laser",
"Town Laser",
@@ -172,23 +125,25 @@ def get_priority_hint_items(multiworld: MultiWorld, player: int):
"Shadows Laser",
]
- if get_option_value(multiworld, player, "shuffle_doors") >= 2:
+ if world.options.shuffle_doors >= 2:
priority.add("Desert Laser")
- priority.update(multiworld.per_slot_randoms[player].sample(lasers, 5))
+ priority.update(world.random.sample(lasers, 5))
else:
lasers.append("Desert Laser")
- priority.update(multiworld.per_slot_randoms[player].sample(lasers, 6))
+ priority.update(world.random.sample(lasers, 6))
- return priority
+ return sorted(priority)
-def get_priority_hint_locations(multiworld: MultiWorld, player: int):
- return {
+def get_priority_hint_locations(world: "WitnessWorld") -> List[str]:
+ priority = [
+ "Tutorial Patio Floor",
+ "Tutorial Patio Flowers EP",
"Swamp Purple Underwater",
"Shipwreck Vault Box",
- "Town RGB Room Left",
- "Town RGB Room Right",
+ "Town RGB House Upstairs Left",
+ "Town RGB House Upstairs Right",
"Treehouse Green Bridge 7",
"Treehouse Green Bridge Discard",
"Shipwreck Discard",
@@ -198,132 +153,471 @@ def get_priority_hint_locations(multiworld: MultiWorld, player: int):
"Tunnels Theater Flowers EP",
"Boat Shipwreck Green EP",
"Quarry Stoneworks Control Room Left",
- }
+ ]
+ # Add Obelisk Sides that contain EPs that are meant to be hinted, if they are necessary to complete the Obelisk Side
+ if "0x33A20" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES:
+ priority.append("Town Obelisk Side 6") # Theater Flowers EP
-def make_hint_from_item(multiworld: MultiWorld, player: int, item: str):
- location_obj = multiworld.find_item(item, player).item.location
- location_name = location_obj.name
- if location_obj.player != player:
- location_name += " (" + multiworld.get_player_name(location_obj.player) + ")"
+ if "0x28B29" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES:
+ priority.append("Treehouse Obelisk Side 4") # Shipwreck Green EP
- return location_name, item, location_obj.address if (location_obj.player == player) else -1
+ if "0x33600" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES:
+ priority.append("Town Obelisk Side 2") # Tutorial Patio Flowers EP.
+ return priority
-def make_hint_from_location(multiworld: MultiWorld, player: int, location: str):
- location_obj = multiworld.get_location(location, player)
- item_obj = multiworld.get_location(location, player).item
- item_name = item_obj.name
- if item_obj.player != player:
- item_name += " (" + multiworld.get_player_name(item_obj.player) + ")"
- return location, item_name, location_obj.address if (location_obj.player == player) else -1
+def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint:
+ location_name = hint.location.name
+ if hint.location.player != world.player:
+ location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")"
+ item = hint.location.item
-def make_hints(multiworld: MultiWorld, player: int, hint_amount: int):
- hints = list()
+ item_name = "Nothing"
+ if item is not None:
+ item_name = item.name
+
+ if item.player != world.player:
+ item_name += " (" + world.multiworld.get_player_name(item.player) + ")"
+
+ if hint.hint_came_from_location:
+ hint_text = f"{location_name} contains {item_name}."
+ else:
+ hint_text = f"{item_name} can be found at {location_name}."
+
+ return WitnessWordedHint(hint_text, hint.location)
- prog_items_in_this_world = {
- item.name for item in multiworld.get_items()
- if item.player == player and item.code and item.advancement
- }
- loc_in_this_world = {
- location.name for location in multiworld.get_locations()
- if location.player == player and location.address
- }
+
+def hint_from_item(world: "WitnessWorld", item_name: str,
+ own_itempool: List["WitnessItem"]) -> Optional[WitnessLocationHint]:
+ def get_real_location(multiworld: MultiWorld, location: Location) -> Location:
+ """If this location is from an item_link pseudo-world, get the location that the item_link item is on.
+ Return the original location otherwise / as a fallback."""
+ if location.player not in world.multiworld.groups:
+ return location
+
+ try:
+ if not location.item:
+ return location
+ return multiworld.find_item(location.item.name, location.player)
+ except StopIteration:
+ return location
+
+ locations = [
+ get_real_location(world.multiworld, item.location)
+ for item in own_itempool if item.name == item_name and item.location
+ ]
+
+ if not locations:
+ return None
+
+ location_obj = world.random.choice(locations)
+
+ return WitnessLocationHint(location_obj, False)
+
+
+def hint_from_location(world: "WitnessWorld", location: str) -> Optional[WitnessLocationHint]:
+ return WitnessLocationHint(world.get_location(location), True)
+
+
+def get_items_and_locations_in_random_order(world: "WitnessWorld",
+ own_itempool: List["WitnessItem"]) -> Tuple[List[str], List[str]]:
+ prog_items_in_this_world = sorted(
+ item.name for item in own_itempool
+ if item.advancement and item.code and item.location
+ )
+ locations_in_this_world = sorted(
+ location.name for location in world.multiworld.get_locations(world.player)
+ if location.address and location.progress_type != LocationProgressType.EXCLUDED
+ )
+
+ world.random.shuffle(prog_items_in_this_world)
+ world.random.shuffle(locations_in_this_world)
+
+ return prog_items_in_this_world, locations_in_this_world
+
+
+def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List["WitnessItem"],
+ already_hinted_locations: Set[Location]
+ ) -> Tuple[List[WitnessLocationHint], List[WitnessLocationHint]]:
+ prog_items_in_this_world, loc_in_this_world = get_items_and_locations_in_random_order(world, own_itempool)
always_locations = [
- location for location in get_always_hint_locations(multiworld, player)
+ location for location in get_always_hint_locations(world)
if location in loc_in_this_world
]
always_items = [
- item for item in get_always_hint_items(multiworld, player)
+ item for item in get_always_hint_items(world)
if item in prog_items_in_this_world
]
priority_locations = [
- location for location in get_priority_hint_locations(multiworld, player)
+ location for location in get_priority_hint_locations(world)
if location in loc_in_this_world
]
priority_items = [
- item for item in get_priority_hint_items(multiworld, player)
+ item for item in get_priority_hint_items(world)
if item in prog_items_in_this_world
]
- always_hint_pairs = dict()
+ # Get always and priority location/item hints
+ always_location_hints = {hint_from_location(world, location) for location in always_locations}
+ always_item_hints = {hint_from_item(world, item, own_itempool) for item in always_items}
+ priority_location_hints = {hint_from_location(world, location) for location in priority_locations}
+ priority_item_hints = {hint_from_item(world, item, own_itempool) for item in priority_items}
- for item in always_items:
- hint_pair = make_hint_from_item(multiworld, player, item)
+ # Combine the sets. This will get rid of duplicates
+ always_hints_set = always_item_hints | always_location_hints
+ priority_hints_set = priority_item_hints | priority_location_hints
- if hint_pair[2] == 158007: # Tutorial Gate Open
- continue
+ # Make sure priority hints doesn't contain any hints that are already always hints.
+ priority_hints_set -= always_hints_set
- always_hint_pairs[hint_pair[0]] = (hint_pair[1], True, hint_pair[2])
+ always_generator = [hint for hint in always_hints_set if hint and hint.location not in already_hinted_locations]
+ priority_generator = [hint for hint in priority_hints_set if hint and hint.location not in already_hinted_locations]
- for location in always_locations:
- hint_pair = make_hint_from_location(multiworld, player, location)
- always_hint_pairs[hint_pair[0]] = (hint_pair[1], False, hint_pair[2])
+ # Convert both hint types to list and then shuffle. Also, get rid of None and Tutorial Gate Open.
+ always_hints = sorted(always_generator, key=lambda h: h.location)
+ priority_hints = sorted(priority_generator, key=lambda h: h.location)
+ world.random.shuffle(always_hints)
+ world.random.shuffle(priority_hints)
- priority_hint_pairs = dict()
+ return always_hints, priority_hints
- for item in priority_items:
- hint_pair = make_hint_from_item(multiworld, player, item)
- if hint_pair[2] == 158007: # Tutorial Gate Open
- continue
+def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List["WitnessItem"],
+ already_hinted_locations: Set[Location], hints_to_use_first: List[WitnessLocationHint],
+ unhinted_locations_for_hinted_areas: Dict[str, Set[Location]]) -> List[WitnessWordedHint]:
+ prog_items_in_this_world, locations_in_this_world = get_items_and_locations_in_random_order(world, own_itempool)
+
+ next_random_hint_is_location = world.random.randrange(0, 2)
- priority_hint_pairs[hint_pair[0]] = (hint_pair[1], True, hint_pair[2])
+ hints: List[WitnessWordedHint] = []
- for location in priority_locations:
- hint_pair = make_hint_from_location(multiworld, player, location)
- priority_hint_pairs[hint_pair[0]] = (hint_pair[1], False, hint_pair[2])
+ # This is a way to reverse a Dict[a,List[b]] to a Dict[b,a]
+ area_reverse_lookup = {
+ unhinted_location: hinted_area
+ for hinted_area, unhinted_locations in unhinted_locations_for_hinted_areas.items()
+ for unhinted_location in unhinted_locations
+ }
- for loc, item in always_hint_pairs.items():
- if item[1]:
- hints.append((f"{item[0]} can be found at {loc}.", item[2]))
+ while len(hints) < hint_amount:
+ if not prog_items_in_this_world and not locations_in_this_world and not hints_to_use_first:
+ player_name = world.multiworld.get_player_name(world.player)
+ logging.warning(f"Ran out of items/locations to hint for player {player_name}.")
+ break
+
+ location_hint: Optional[WitnessLocationHint]
+ if hints_to_use_first:
+ location_hint = hints_to_use_first.pop()
+ elif next_random_hint_is_location and locations_in_this_world:
+ location_hint = hint_from_location(world, locations_in_this_world.pop())
+ elif not next_random_hint_is_location and prog_items_in_this_world:
+ location_hint = hint_from_item(world, prog_items_in_this_world.pop(), own_itempool)
+ # The list that the hint was supposed to be taken from was empty.
+ # Try the other list, which has to still have something, as otherwise, all lists would be empty,
+ # which would have triggered the guard condition above.
else:
- hints.append((f"{loc} contains {item[0]}.", item[2]))
+ next_random_hint_is_location = not next_random_hint_is_location
+ continue
+
+ if location_hint is None or location_hint.location in already_hinted_locations:
+ continue
+
+ # Don't hint locations in areas that are almost fully hinted out already
+ if location_hint.location in area_reverse_lookup:
+ area = area_reverse_lookup[location_hint.location]
+ if len(unhinted_locations_for_hinted_areas[area]) == 1:
+ continue
+ del area_reverse_lookup[location_hint.location]
+ unhinted_locations_for_hinted_areas[area] -= {location_hint.location}
- multiworld.per_slot_randoms[player].shuffle(hints) # shuffle always hint order in case of low hint amount
+ hints.append(word_direct_hint(world, location_hint))
+ already_hinted_locations.add(location_hint.location)
- remaining_hints = hint_amount - len(hints)
- priority_hint_amount = int(max(0.0, min(len(priority_hint_pairs) / 2, remaining_hints / 2)))
+ next_random_hint_is_location = not next_random_hint_is_location
- prog_items_in_this_world = sorted(list(prog_items_in_this_world))
- locations_in_this_world = sorted(list(loc_in_this_world))
+ return hints
- multiworld.per_slot_randoms[player].shuffle(prog_items_in_this_world)
- multiworld.per_slot_randoms[player].shuffle(locations_in_this_world)
- priority_hint_list = list(priority_hint_pairs.items())
- multiworld.per_slot_randoms[player].shuffle(priority_hint_list)
- for _ in range(0, priority_hint_amount):
- next_priority_hint = priority_hint_list.pop()
- loc = next_priority_hint[0]
- item = next_priority_hint[1]
+def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[str, List[Location]],
+ already_hinted_locations: Set[Location]) -> Tuple[List[str], Dict[str, Set[Location]]]:
+ """
+ Choose areas to hint.
+ This takes into account that some areas may already have had items hinted in them through location hints.
+ When this happens, they are made less likely to receive an area hint.
+ """
- if item[1]:
- hints.append((f"{item[0]} can be found at {loc}.", item[2]))
- else:
- hints.append((f"{loc} contains {item[0]}.", item[2]))
+ unhinted_locations_per_area = {}
+ unhinted_location_percentage_per_area = {}
- next_random_hint_is_item = multiworld.per_slot_randoms[player].randint(0, 2)
+ for area_name, locations in locations_per_area.items():
+ not_yet_hinted_locations = sum(location not in already_hinted_locations for location in locations)
+ unhinted_locations_per_area[area_name] = {loc for loc in locations if loc not in already_hinted_locations}
+ unhinted_location_percentage_per_area[area_name] = not_yet_hinted_locations / len(locations)
- while len(hints) < hint_amount:
- if next_random_hint_is_item:
- if not prog_items_in_this_world:
- next_random_hint_is_item = not next_random_hint_is_item
- continue
+ items_per_area = {area_name: [location.item for location in locations]
+ for area_name, locations in locations_per_area.items()}
+
+ areas = sorted(area for area in items_per_area if unhinted_location_percentage_per_area[area])
+ weights = [unhinted_location_percentage_per_area[area] for area in areas]
+
+ amount = min(amount, len(weights))
+
+ hinted_areas = weighted_sample(world.random, areas, weights, amount)
+
+ return hinted_areas, unhinted_locations_per_area
+
+
+def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]], Dict[str, List[Item]]]:
+ potential_areas = list(static_witness_logic.ALL_AREAS_BY_NAME.keys())
+
+ locations_per_area = {}
+ items_per_area = {}
+
+ for area in potential_areas:
+ regions = [
+ world.get_region(region)
+ for region in static_witness_logic.ALL_AREAS_BY_NAME[area]["regions"]
+ if region in world.player_regions.created_region_names
+ ]
+ locations = [location for region in regions for location in region.get_locations() if location.address]
+
+ if locations:
+ locations_per_area[area] = locations
+ items_per_area[area] = [location.item for location in locations]
+
+ return locations_per_area, items_per_area
+
+
+def word_area_hint(world: "WitnessWorld", hinted_area: str, corresponding_items: List[Item]) -> Tuple[str, int]:
+ """
+ Word the hint for an area using natural sounding language.
+ This takes into account how much progression there is, how much of it is local/non-local, and whether there are
+ any local lasers to be found in this area.
+ """
+
+ local_progression = sum(item.player == world.player and item.advancement for item in corresponding_items)
+ non_local_progression = sum(item.player != world.player and item.advancement for item in corresponding_items)
+
+ laser_names = {"Symmetry Laser", "Desert Laser", "Quarry Laser", "Shadows Laser", "Town Laser", "Monastery Laser",
+ "Jungle Laser", "Bunker Laser", "Swamp Laser", "Treehouse Laser", "Keep Laser", }
+
+ local_lasers = sum(
+ item.player == world.player and item.name in laser_names
+ for item in corresponding_items
+ )
+
+ total_progression = non_local_progression + local_progression
+
+ player_count = world.multiworld.players
+
+ area_progression_word = "Both" if total_progression == 2 else "All"
+
+ if not total_progression:
+ hint_string = f"In the {hinted_area} area, you will find no progression items."
+
+ elif total_progression == 1:
+ hint_string = f"In the {hinted_area} area, you will find 1 progression item."
+
+ if player_count > 1:
+ if local_lasers:
+ hint_string += "\nThis item is a laser for this world."
+ elif non_local_progression:
+ other_player_str = "the other player" if player_count == 2 else "another player"
+ hint_string += f"\nThis item is for {other_player_str}."
+ else:
+ hint_string += "\nThis item is for this world."
+ else:
+ if local_lasers:
+ hint_string += "\nThis item is a laser."
+
+ else:
+ hint_string = f"In the {hinted_area} area, you will find {total_progression} progression items."
+
+ if local_lasers == total_progression:
+ sentence_end = (" for this world." if player_count > 1 else ".")
+ hint_string += "\nAll of them are lasers" + sentence_end
+
+ elif player_count > 1:
+ if local_progression and non_local_progression:
+ if non_local_progression == 1:
+ other_player_str = "the other player" if player_count == 2 else "another player"
+ hint_string += f"\nOne of them is for {other_player_str}."
+ else:
+ other_player_str = "the other player" if player_count == 2 else "other players"
+ hint_string += f"\n{non_local_progression} of them are for {other_player_str}."
+ elif non_local_progression:
+ other_players_str = "the other player" if player_count == 2 else "other players"
+ hint_string += f"\n{area_progression_word} of them are for {other_players_str}."
+ elif local_progression:
+ hint_string += f"\n{area_progression_word} of them are for this world."
+
+ if local_lasers == 1:
+ if not non_local_progression:
+ hint_string += "\nAlso, one of them is a laser."
+ else:
+ hint_string += "\nAlso, one of them is a laser for this world."
+ elif local_lasers:
+ if not non_local_progression:
+ hint_string += f"\nAlso, {local_lasers} of them are lasers."
+ else:
+ hint_string += f"\nAlso, {local_lasers} of them are lasers for this world."
- hint = make_hint_from_item(multiworld, player, prog_items_in_this_world.pop())
- hints.append((f"{hint[1]} can be found at {hint[0]}.", hint[2]))
else:
- hint = make_hint_from_location(multiworld, player, locations_in_this_world.pop())
- hints.append((f"{hint[0]} contains {hint[1]}.", hint[2]))
+ if local_lasers == 1:
+ hint_string += "\nOne of them is a laser."
+ elif local_lasers:
+ hint_string += f"\n{local_lasers} of them are lasers."
- next_random_hint_is_item = not next_random_hint_is_item
+ return hint_string, total_progression
- return hints
+def make_area_hints(world: "WitnessWorld", amount: int, already_hinted_locations: Set[Location]
+ ) -> Tuple[List[WitnessWordedHint], Dict[str, Set[Location]]]:
+ locs_per_area, items_per_area = get_hintable_areas(world)
+
+ hinted_areas, unhinted_locations_per_area = choose_areas(world, amount, locs_per_area, already_hinted_locations)
+
+ hints = []
+
+ for hinted_area in hinted_areas:
+ hint_string, prog_amount = word_area_hint(world, hinted_area, items_per_area[hinted_area])
+
+ hints.append(WitnessWordedHint(hint_string, None, f"hinted_area:{hinted_area}", prog_amount))
+
+ if len(hinted_areas) < amount:
+ player_name = world.multiworld.get_player_name(world.player)
+ logging.warning(f"Was not able to make {amount} area hints for player {player_name}. "
+ f"Made {len(hinted_areas)} instead, and filled the rest with random location hints.")
+
+ return hints, unhinted_locations_per_area
+
+
+def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int,
+ already_hinted_locations: Set[Location]) -> List[WitnessWordedHint]:
+ generated_hints: List[WitnessWordedHint] = []
+
+ state = CollectionState(world.multiworld)
+
+ # Keep track of already hinted locations. Consider early Tutorial as "already hinted"
+
+ already_hinted_locations |= {
+ loc for loc in world.multiworld.get_reachable_locations(state, world.player)
+ if loc.address and static_witness_logic.ENTITIES_BY_NAME[loc.name]["area"]["name"] == "Tutorial (Inside)"
+ }
+
+ intended_location_hints = hint_amount - area_hints
+
+ # First, make always and priority hints.
+
+ always_hints, priority_hints = make_always_and_priority_hints(
+ world, world.own_itempool, already_hinted_locations
+ )
+
+ generated_always_hints = len(always_hints)
+ possible_priority_hints = len(priority_hints)
+
+ # Make as many always hints as possible
+ always_hints_to_use = min(intended_location_hints, generated_always_hints)
+
+ # Make up to half of the rest of the location hints priority hints, using up to half of the possibly priority hints
+ remaining_location_hints = intended_location_hints - always_hints_to_use
+ priority_hints_to_use = int(max(0.0, min(possible_priority_hints / 2, remaining_location_hints / 2)))
+
+ for _ in range(always_hints_to_use):
+ location_hint = always_hints.pop()
+ generated_hints.append(word_direct_hint(world, location_hint))
+ already_hinted_locations.add(location_hint.location)
+
+ for _ in range(priority_hints_to_use):
+ location_hint = priority_hints.pop()
+ generated_hints.append(word_direct_hint(world, location_hint))
+ already_hinted_locations.add(location_hint.location)
+
+ location_hints_created_in_round_1 = len(generated_hints)
+
+ unhinted_locations_per_area: Dict[str, Set[Location]] = {}
+
+ # Then, make area hints.
+ if area_hints:
+ generated_area_hints, unhinted_locations_per_area = make_area_hints(world, area_hints, already_hinted_locations)
+ generated_hints += generated_area_hints
+
+ # If we don't have enough hints yet, recalculate always and priority hints, then fill with random hints
+ if len(generated_hints) < hint_amount:
+ remaining_needed_location_hints = hint_amount - len(generated_hints)
+
+ # Save old values for used always and priority hints for later calculations
+ amt_of_used_always_hints = always_hints_to_use
+ amt_of_used_priority_hints = priority_hints_to_use
+
+ # Recalculate how many always hints and priority hints are supposed to be used
+ intended_location_hints = remaining_needed_location_hints + location_hints_created_in_round_1
+
+ always_hints_to_use = min(intended_location_hints, generated_always_hints)
+ priority_hints_to_use = int(max(0.0, min(possible_priority_hints / 2, remaining_location_hints / 2)))
+
+ # If we now need more always hints and priority hints than we thought previously, make some more.
+ more_always_hints = always_hints_to_use - amt_of_used_always_hints
+ more_priority_hints = priority_hints_to_use - amt_of_used_priority_hints
+
+ extra_always_and_priority_hints: List[WitnessLocationHint] = []
+
+ for _ in range(more_always_hints):
+ extra_always_and_priority_hints.append(always_hints.pop())
+
+ for _ in range(more_priority_hints):
+ extra_always_and_priority_hints.append(priority_hints.pop())
+
+ generated_hints += make_extra_location_hints(
+ world, hint_amount - len(generated_hints), world.own_itempool, already_hinted_locations,
+ extra_always_and_priority_hints, unhinted_locations_per_area
+ )
+
+ # If we still don't have enough for whatever reason, throw a warning, proceed with the lower amount
+ if len(generated_hints) != hint_amount:
+ player_name = world.multiworld.get_player_name(world.player)
+ logging.warning(f"Couldn't generate {hint_amount} hints for player {player_name}. "
+ f"Generated {len(generated_hints)} instead.")
+
+ return generated_hints
+
+
+def make_compact_hint_data(hint: WitnessWordedHint, local_player_number: int) -> CompactItemData:
+ location = hint.location
+ area_amount = hint.area_amount
+
+ # -1 if junk hint, address if location hint, area string if area hint
+ arg_1: Union[str, int]
+ if location and location.address is not None:
+ arg_1 = location.address
+ elif hint.area is not None:
+ arg_1 = hint.area
+ else:
+ arg_1 = -1
+
+ # self.player if junk hint, player if location hint, progression amount if area hint
+ arg_2: int
+ if area_amount is not None:
+ arg_2 = area_amount
+ elif location is not None:
+ arg_2 = location.player
+ else:
+ arg_2 = local_player_number
+
+ return hint.wording, arg_1, arg_2
+
+
+def make_laser_hints(world: "WitnessWorld", laser_names: List[str]) -> Dict[str, WitnessWordedHint]:
+ laser_hints_by_name = {}
+
+ for item_name in laser_names:
+ location_hint = hint_from_item(world, item_name, world.own_itempool)
+ if not location_hint:
+ continue
+
+ laser_hints_by_name[item_name] = word_direct_hint(world, location_hint)
-def generate_joke_hints(multiworld: MultiWorld, player: int, amount: int):
- return [(x, -1) for x in multiworld.per_slot_randoms[player].sample(joke_hints, amount)]
+ return laser_hints_by_name
diff --git a/worlds/witness/items.py b/worlds/witness/items.py
deleted file mode 100644
index 82c79047f3fb..000000000000
--- a/worlds/witness/items.py
+++ /dev/null
@@ -1,264 +0,0 @@
-"""
-Defines progression, junk and event items for The Witness
-"""
-import copy
-from dataclasses import dataclass
-from typing import Optional, Dict, List, Set
-
-from BaseClasses import Item, MultiWorld, ItemClassification
-from .Options import get_option_value, is_option_enabled, the_witness_options
-
-from .locations import ID_START, WitnessPlayerLocations
-from .player_logic import WitnessPlayerLogic
-from .static_logic import ItemDefinition, DoorItemDefinition, ProgressiveItemDefinition, ItemCategory, \
- StaticWitnessLogic, WeightedItemDefinition
-from .utils import build_weighted_int_list
-
-NUM_ENERGY_UPGRADES = 4
-
-
-@dataclass()
-class ItemData:
- """
- ItemData for an item in The Witness
- """
- ap_code: Optional[int]
- definition: ItemDefinition
- classification: ItemClassification
- local_only: bool = False
-
-
-class WitnessItem(Item):
- """
- Item from the game The Witness
- """
- game: str = "The Witness"
-
-
-class StaticWitnessItems:
- """
- Class that handles Witness items independent of world settings
- """
- item_data: Dict[str, ItemData] = {}
- item_groups: Dict[str, List[str]] = {}
-
- # Useful items that are treated specially at generation time and should not be automatically added to the player's
- # item list during get_progression_items.
- special_usefuls: List[str] = ["Puzzle Skip"]
-
- def __init__(self):
- for item_name, definition in StaticWitnessLogic.all_items.items():
- ap_item_code = definition.local_code + ID_START
- classification: ItemClassification = ItemClassification.filler
- local_only: bool = False
-
- if definition.category is ItemCategory.SYMBOL:
- classification = ItemClassification.progression
- StaticWitnessItems.item_groups.setdefault("Symbols", []).append(item_name)
- elif definition.category is ItemCategory.DOOR:
- classification = ItemClassification.progression
- StaticWitnessItems.item_groups.setdefault("Doors", []).append(item_name)
- elif definition.category is ItemCategory.LASER:
- classification = ItemClassification.progression
- StaticWitnessItems.item_groups.setdefault("Lasers", []).append(item_name)
- elif definition.category is ItemCategory.USEFUL:
- classification = ItemClassification.useful
- elif definition.category is ItemCategory.FILLER:
- if item_name in ["Energy Fill (Small)"]:
- local_only = True
- classification = ItemClassification.filler
- elif definition.category is ItemCategory.TRAP:
- classification = ItemClassification.trap
- elif definition.category is ItemCategory.JOKE:
- classification = ItemClassification.filler
-
- StaticWitnessItems.item_data[item_name] = ItemData(ap_item_code, definition,
- classification, local_only)
-
- @staticmethod
- def get_item_to_door_mappings() -> Dict[int, List[int]]:
- output: Dict[int, List[int]] = {}
- for item_name, item_data in {name: data for name, data in StaticWitnessItems.item_data.items()
- if isinstance(data.definition, DoorItemDefinition)}.items():
- item = StaticWitnessItems.item_data[item_name]
- output[item.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
- return output
-
-
-class WitnessPlayerItems:
- """
- Class that defines Items for a single world
- """
-
- def __init__(self, multiworld: MultiWorld, player: int, logic: WitnessPlayerLogic, locat: WitnessPlayerLocations):
- """Adds event items after logic changes due to options"""
-
- self._world: MultiWorld = multiworld
- self._player_id: int = player
- self._logic: WitnessPlayerLogic = logic
- self._locations: WitnessPlayerLocations = locat
-
- # Duplicate the static item data, then make any player-specific adjustments to classification.
- self.item_data: Dict[str, ItemData] = copy.deepcopy(StaticWitnessItems.item_data)
-
- # Remove all progression items that aren't actually in the game.
- self.item_data = {name: data for (name, data) in self.item_data.items()
- if data.classification is not ItemClassification.progression or
- name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME}
-
- # Adjust item classifications based on game settings.
- eps_shuffled = get_option_value(self._world, self._player_id, "shuffle_EPs") != 0
- for item_name, item_data in self.item_data.items():
- if not eps_shuffled and item_name in ["Monastery Garden Entry (Door)", "Monastery Shortcuts"]:
- # Downgrade doors that only gate progress in EP shuffle.
- item_data.classification = ItemClassification.useful
- elif item_name in ["River Monastery Shortcut (Door)", "Jungle & River Shortcuts",
- "Monastery Shortcut (Door)",
- "Orchard Second Gate (Door)"]:
- # Downgrade doors that don't gate progress.
- item_data.classification = ItemClassification.useful
-
- # Build the mandatory item list.
- self._mandatory_items: Dict[str, int] = {}
-
- # Add progression items to the mandatory item list.
- for item_name, item_data in {name: data for (name, data) in self.item_data.items()
- if data.classification == ItemClassification.progression}.items():
- if isinstance(item_data.definition, ProgressiveItemDefinition):
- num_progression = len(self._logic.MULTI_LISTS[item_name])
- self._mandatory_items[item_name] = num_progression
- else:
- self._mandatory_items[item_name] = 1
-
- # Add setting-specific useful items to the mandatory item list.
- for item_name, item_data in {name: data for (name, data) in self.item_data.items()
- if data.classification == ItemClassification.useful}.items():
- if item_name in StaticWitnessItems.special_usefuls:
- continue
- elif item_name == "Energy Capacity":
- self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES
- elif isinstance(item_data.classification, ProgressiveItemDefinition):
- self._mandatory_items[item_name] = len(item_data.mappings)
- else:
- self._mandatory_items[item_name] = 1
-
- # Add event items to the item definition list for later lookup.
- for event_location in self._locations.EVENT_LOCATION_TABLE:
- location_name = logic.EVENT_ITEM_PAIRS[event_location]
- self.item_data[location_name] = ItemData(None, ItemDefinition(0, ItemCategory.EVENT),
- ItemClassification.progression, False)
-
- def get_mandatory_items(self) -> Dict[str, int]:
- """
- Returns the list of items that must be in the pool for the game to successfully generate.
- """
- return self._mandatory_items.copy()
-
- def get_filler_items(self, quantity: int) -> Dict[str, int]:
- """
- Generates a list of filler items of the given length.
- """
- if quantity <= 0:
- return {}
-
- output: Dict[str, int] = {}
- remaining_quantity = quantity
-
- # Add joke items.
- output.update({name: 1 for (name, data) in self.item_data.items()
- if data.definition.category is ItemCategory.JOKE})
- remaining_quantity -= len(output)
-
- # Read trap configuration data.
- trap_weight = get_option_value(self._world, self._player_id, "trap_percentage") / 100
- filler_weight = 1 - trap_weight
-
- # Add filler items to the list.
- filler_items: Dict[str, float]
- filler_items = {name: data.definition.weight if isinstance(data.definition, WeightedItemDefinition) else 1
- for (name, data) in self.item_data.items() if data.definition.category is ItemCategory.FILLER}
- filler_items = {name: base_weight * filler_weight / sum(filler_items.values())
- for name, base_weight in filler_items.items() if base_weight > 0}
-
- # Add trap items.
- if trap_weight > 0:
- trap_items = {name: data.definition.weight if isinstance(data.definition, WeightedItemDefinition) else 1
- for (name, data) in self.item_data.items() if data.definition.category is ItemCategory.TRAP}
- filler_items.update({name: base_weight * trap_weight / sum(trap_items.values())
- for name, base_weight in trap_items.items() if base_weight > 0})
-
- # Get the actual number of each item by scaling the float weight values to match the target quantity.
- int_weights: List[int] = build_weighted_int_list(filler_items.values(), remaining_quantity)
- output.update(zip(filler_items.keys(), int_weights))
-
- return output
-
- def get_early_items(self) -> List[str]:
- """
- Returns items that are ideal for placing on extremely early checks, like the tutorial gate.
- """
- output: Set[str] = set()
- if "shuffle_symbols" not in the_witness_options.keys() \
- or is_option_enabled(self._world, self._player_id, "shuffle_symbols"):
- if get_option_value(self._world, self._player_id, "shuffle_doors") > 0:
- output = {"Dots", "Black/White Squares", "Symmetry"}
- else:
- output = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
-
- if is_option_enabled(self._world, self._player_id, "shuffle_discarded_panels"):
- if get_option_value(self._world, self._player_id, "puzzle_randomization") == 1:
- output.add("Arrows")
- else:
- output.add("Triangles")
-
- # Replace progressive items with their parents.
- output = {StaticWitnessLogic.get_parent_progressive_item(item) for item in output}
-
- # Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
- # before create_items so that we'll be able to check placed items instead of just removing all items mentioned
- # regardless of whether or not they actually wind up being manually placed.
- for plando_setting in self._world.plando_items[self._player_id]:
- if plando_setting.get("from_pool", True):
- for item_setting_key in [key for key in ["item", "items"] if key in plando_setting]:
- if type(plando_setting[item_setting_key]) is str:
- output -= {plando_setting[item_setting_key]}
- elif type(plando_setting[item_setting_key]) is dict:
- output -= {item for item, weight in plando_setting[item_setting_key].items() if weight}
- else:
- # Assume this is some other kind of iterable.
- for inner_item in plando_setting[item_setting_key]:
- if type(inner_item) is str:
- output -= {inner_item}
- elif type(inner_item) is dict:
- output -= {item for item, weight in inner_item.items() if weight}
-
- # Sort the output for consistency across versions if the implementation changes but the logic does not.
- return sorted(list(output))
-
- def get_door_ids_in_pool(self) -> List[int]:
- """
- Returns the total set of all door IDs that are controlled by items in the pool.
- """
- output: List[int] = []
- for item_name, item_data in {name: data for name, data in self.item_data.items()
- if isinstance(data.definition, DoorItemDefinition)}.items():
- output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
- return output
-
- def get_symbol_ids_not_in_pool(self) -> List[int]:
- """
- Returns the item IDs of symbol items that were defined in the configuration file but are not in the pool.
- """
- return [data.ap_code for name, data in StaticWitnessItems.item_data.items()
- if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL]
-
- def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]:
- output: Dict[int, List[int]] = {}
- for item_name, quantity in {name: quantity for name, quantity in self._mandatory_items.items()}.items():
- item = self.item_data[item_name]
- if isinstance(item.definition, ProgressiveItemDefinition):
- # Note: we need to reference the static table here rather than the player-specific one because the child
- # items were removed from the pool when we pruned out all progression items not in the settings.
- output[item.ap_code] = [StaticWitnessItems.item_data[child_item].ap_code
- for child_item in item.definition.child_item_names]
- return output
diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py
index b33e276e3ad8..1796f051b896 100644
--- a/worlds/witness/locations.py
+++ b/worlds/witness/locations.py
@@ -1,457 +1,14 @@
"""
Defines constants for different types of locations in the game
"""
+from typing import TYPE_CHECKING
-from .Options import is_option_enabled, get_option_value
+from .data import static_locations as static_witness_locations
+from .data import static_logic as static_witness_logic
from .player_logic import WitnessPlayerLogic
-from .static_logic import StaticWitnessLogic
-
-ID_START = 158000
-
-
-class StaticWitnessLocations:
- """
- Witness Location Constants that stay consistent across worlds
- """
-
- GENERAL_LOCATIONS = {
- "Tutorial Front Left",
- "Tutorial Back Left",
- "Tutorial Back Right",
- "Tutorial Gate Open",
-
- "Outside Tutorial Vault Box",
- "Outside Tutorial Discard",
- "Outside Tutorial Shed Row 5",
- "Outside Tutorial Tree Row 9",
-
- "Glass Factory Discard",
- "Glass Factory Back Wall 5",
- "Glass Factory Front 3",
- "Glass Factory Melting 3",
-
- "Symmetry Island Right 5",
- "Symmetry Island Back 6",
- "Symmetry Island Left 7",
- "Symmetry Island Scenery Outlines 5",
- "Symmetry Island Laser Panel",
-
- "Orchard Apple Tree 5",
-
- "Desert Vault Box",
- "Desert Discard",
- "Desert Surface 8",
- "Desert Light Room 3",
- "Desert Pond Room 5",
- "Desert Flood Room 6",
- "Desert Final Hexagonal",
- "Desert Final Bent 3",
- "Desert Laser Panel",
-
- "Quarry Stoneworks Lower Row 6",
- "Quarry Stoneworks Upper Row 8",
- "Quarry Stoneworks Control Room Right",
- "Quarry Boathouse Intro Right",
- "Quarry Boathouse Intro Left",
- "Quarry Boathouse Front Row 5",
- "Quarry Boathouse Back First Row 9",
- "Quarry Boathouse Back Second Row 3",
- "Quarry Discard",
- "Quarry Laser Panel",
-
- "Shadows Intro 8",
- "Shadows Far 8",
- "Shadows Near 5",
- "Shadows Laser Panel",
-
- "Keep Hedge Maze 1",
- "Keep Hedge Maze 2",
- "Keep Hedge Maze 3",
- "Keep Hedge Maze 4",
- "Keep Pressure Plates 1",
- "Keep Pressure Plates 2",
- "Keep Pressure Plates 3",
- "Keep Pressure Plates 4",
- "Keep Discard",
- "Keep Laser Panel Hedges",
- "Keep Laser Panel Pressure Plates",
-
- "Shipwreck Vault Box",
- "Shipwreck Discard",
-
- "Monastery Outside 3",
- "Monastery Inside 4",
- "Monastery Laser Panel",
-
- "Town Cargo Box Discard",
- "Town Tall Hexagonal",
- "Town Church Lattice",
- "Town Rooftop Discard",
- "Town Red Rooftop 5",
- "Town Wooden Roof Lower Row 5",
- "Town Wooden Rooftop",
- "Town Laser Panel",
-
- "Theater Discard",
-
- "Jungle Discard",
- "Jungle First Row 3",
- "Jungle Second Row 4",
- "Jungle Popup Wall 6",
- "Jungle Laser Panel",
-
- "River Vault Box",
-
- "Bunker Intro Left 5",
- "Bunker Intro Back 4",
- "Bunker Glass Room 3",
- "Bunker UV Room 2",
- "Bunker Laser Panel",
-
- "Swamp Intro Front 6",
- "Swamp Intro Back 8",
- "Swamp Between Bridges Near Row 4",
- "Swamp Cyan Underwater 5",
- "Swamp Platform Row 4",
- "Swamp Between Bridges Far Row 4",
- "Swamp Red Underwater 4",
- "Swamp Beyond Rotating Bridge 4",
- "Swamp Blue Underwater 5",
- "Swamp Laser Panel",
-
- "Treehouse Yellow Bridge 9",
- "Treehouse First Purple Bridge 5",
- "Treehouse Second Purple Bridge 7",
- "Treehouse Green Bridge 7",
- "Treehouse Green Bridge Discard",
- "Treehouse Left Orange Bridge 15",
- "Treehouse Laser Discard",
- "Treehouse Right Orange Bridge 12",
- "Treehouse Laser Panel",
-
- "Mountainside Discard",
- "Mountainside Vault Box",
-
- "Mountaintop River Shape",
- "Tutorial Patio Floor",
- "Quarry Stoneworks Control Room Left",
- "Theater Tutorial Video",
- "Theater Desert Video",
- "Theater Jungle Video",
- "Theater Shipwreck Video",
- "Theater Mountain Video",
- "Town RGB Room Left",
- "Town RGB Room Right",
- "Town Sound Room Right",
- "Swamp Purple Underwater",
-
- "First Hallway EP",
- "Tutorial Cloud EP",
- "Tutorial Patio Flowers EP",
- "Tutorial Gate EP",
- "Outside Tutorial Garden EP",
- "Outside Tutorial Town Sewer EP",
- "Outside Tutorial Path EP",
- "Outside Tutorial Tractor EP",
- "Main Island Thundercloud EP",
- "Glass Factory Vase EP",
- "Symmetry Island Glass Factory Black Line Reflection EP",
- "Symmetry Island Glass Factory Black Line EP",
- "Desert Sand Snake EP",
- "Desert Facade Right EP",
- "Desert Facade Left EP",
- "Desert Stairs Left EP",
- "Desert Stairs Right EP",
- "Desert Broken Wall Straight EP",
- "Desert Broken Wall Bend EP",
- "Desert Shore EP",
- "Desert Island EP",
- "Desert Pond Room Near Reflection EP",
- "Desert Pond Room Far Reflection EP",
- "Desert Flood Room EP",
- "Desert Elevator EP",
- "Quarry Shore EP",
- "Quarry Entrance Pipe EP",
- "Quarry Sand Pile EP",
- "Quarry Rock Line EP",
- "Quarry Rock Line Reflection EP",
- "Quarry Railroad EP",
- "Quarry Stoneworks Ramp EP",
- "Quarry Stoneworks Lift EP",
- "Quarry Boathouse Moving Ramp EP",
- "Quarry Boathouse Hook EP",
- "Shadows Quarry Stoneworks Rooftop Vent EP",
- "Treehouse Beach Rock Shadow EP",
- "Treehouse Beach Sand Shadow EP",
- "Treehouse Beach Both Orange Bridges EP",
- "Keep Red Flowers EP",
- "Keep Purple Flowers EP",
- "Shipwreck Circle Near EP",
- "Shipwreck Circle Left EP",
- "Shipwreck Circle Far EP",
- "Shipwreck Stern EP",
- "Shipwreck Rope Inner EP",
- "Shipwreck Rope Outer EP",
- "Shipwreck Couch EP",
- "Keep Pressure Plates 1 EP",
- "Keep Pressure Plates 2 EP",
- "Keep Pressure Plates 3 EP",
- "Keep Pressure Plates 4 Left Exit EP",
- "Keep Pressure Plates 4 Right Exit EP",
- "Keep Path EP",
- "Keep Hedges EP",
- "Monastery Facade Left Near EP",
- "Monastery Facade Left Far Short EP",
- "Monastery Facade Left Far Long EP",
- "Monastery Facade Right Near EP",
- "Monastery Facade Left Stairs EP",
- "Monastery Facade Right Stairs EP",
- "Monastery Grass Stairs EP",
- "Monastery Left Shutter EP",
- "Monastery Middle Shutter EP",
- "Monastery Right Shutter EP",
- "Town Windmill First Blade EP",
- "Town Windmill Second Blade EP",
- "Town Windmill Third Blade EP",
- "Town Tower Underside Third EP",
- "Town Tower Underside Fourth EP",
- "Town Tower Underside First EP",
- "Town Tower Underside Second EP",
- "Town RGB House Red EP",
- "Town RGB House Green EP",
- "Town Maze Bridge Underside EP",
- "Town Black Line Redirect EP",
- "Town Black Line Church EP",
- "Town Brown Bridge EP",
- "Town Black Line Tower EP",
- "Theater Eclipse EP",
- "Theater Window EP",
- "Theater Door EP",
- "Theater Church EP",
- "Jungle Long Arch Moss EP",
- "Jungle Straight Left Moss EP",
- "Jungle Pop-up Wall Moss EP",
- "Jungle Short Arch Moss EP",
- "Jungle Entrance EP",
- "Jungle Tree Halo EP",
- "Jungle Bamboo CCW EP",
- "Jungle Bamboo CW EP",
- "River Green Leaf Moss EP",
- "River Monastery Garden Left EP",
- "River Monastery Garden Right EP",
- "River Monastery Wall EP",
- "Bunker Tinted Door EP",
- "Bunker Green Room Flowers EP",
- "Swamp Purple Sand Middle EP",
- "Swamp Purple Sand Top EP",
- "Swamp Purple Sand Bottom EP",
- "Swamp Sliding Bridge Left EP",
- "Swamp Sliding Bridge Right EP",
- "Swamp Cyan Underwater Sliding Bridge EP",
- "Swamp Rotating Bridge CCW EP",
- "Swamp Rotating Bridge CW EP",
- "Swamp Boat EP",
- "Swamp Long Bridge Side EP",
- "Swamp Purple Underwater Right EP",
- "Swamp Purple Underwater Left EP",
- "Treehouse Buoy EP",
- "Treehouse Right Orange Bridge EP",
- "Treehouse Burned House Beach EP",
- "Mountainside Cloud Cycle EP",
- "Mountainside Bush EP",
- "Mountainside Apparent River EP",
- "Mountaintop River Shape EP",
- "Mountaintop Arch Black EP",
- "Mountaintop Arch White Right EP",
- "Mountaintop Arch White Left EP",
- "Mountain Bottom Floor Yellow Bridge EP",
- "Mountain Bottom Floor Blue Bridge EP",
- "Mountain Floor 2 Pink Bridge EP",
- "Caves Skylight EP",
- "Challenge Water EP",
- "Tunnels Theater Flowers EP",
- "Boat Desert EP",
- "Boat Shipwreck CCW Underside EP",
- "Boat Shipwreck Green EP",
- "Boat Shipwreck CW Underside EP",
- "Boat Bunker Yellow Line EP",
- "Boat Town Long Sewer EP",
- "Boat Tutorial EP",
- "Boat Tutorial Reflection EP",
- "Boat Tutorial Moss EP",
- "Boat Cargo Box EP",
-
- "Desert Obelisk Side 1",
- "Desert Obelisk Side 2",
- "Desert Obelisk Side 3",
- "Desert Obelisk Side 4",
- "Desert Obelisk Side 5",
- "Monastery Obelisk Side 1",
- "Monastery Obelisk Side 2",
- "Monastery Obelisk Side 3",
- "Monastery Obelisk Side 4",
- "Monastery Obelisk Side 5",
- "Monastery Obelisk Side 6",
- "Treehouse Obelisk Side 1",
- "Treehouse Obelisk Side 2",
- "Treehouse Obelisk Side 3",
- "Treehouse Obelisk Side 4",
- "Treehouse Obelisk Side 5",
- "Treehouse Obelisk Side 6",
- "River Obelisk Side 1",
- "River Obelisk Side 2",
- "River Obelisk Side 3",
- "River Obelisk Side 4",
- "River Obelisk Side 5",
- "River Obelisk Side 6",
- "Quarry Obelisk Side 1",
- "Quarry Obelisk Side 2",
- "Quarry Obelisk Side 3",
- "Quarry Obelisk Side 4",
- "Quarry Obelisk Side 5",
- "Town Obelisk Side 1",
- "Town Obelisk Side 2",
- "Town Obelisk Side 3",
- "Town Obelisk Side 4",
- "Town Obelisk Side 5",
- "Town Obelisk Side 6",
- }
-
- OBELISK_SIDES = {
- "Desert Obelisk Side 1",
- "Desert Obelisk Side 2",
- "Desert Obelisk Side 3",
- "Desert Obelisk Side 4",
- "Desert Obelisk Side 5",
- "Monastery Obelisk Side 1",
- "Monastery Obelisk Side 2",
- "Monastery Obelisk Side 3",
- "Monastery Obelisk Side 4",
- "Monastery Obelisk Side 5",
- "Monastery Obelisk Side 6",
- "Treehouse Obelisk Side 1",
- "Treehouse Obelisk Side 2",
- "Treehouse Obelisk Side 3",
- "Treehouse Obelisk Side 4",
- "Treehouse Obelisk Side 5",
- "Treehouse Obelisk Side 6",
- "River Obelisk Side 1",
- "River Obelisk Side 2",
- "River Obelisk Side 3",
- "River Obelisk Side 4",
- "River Obelisk Side 5",
- "River Obelisk Side 6",
- "Quarry Obelisk Side 1",
- "Quarry Obelisk Side 2",
- "Quarry Obelisk Side 3",
- "Quarry Obelisk Side 4",
- "Quarry Obelisk Side 5",
- "Town Obelisk Side 1",
- "Town Obelisk Side 2",
- "Town Obelisk Side 3",
- "Town Obelisk Side 4",
- "Town Obelisk Side 5",
- "Town Obelisk Side 6",
- }
-
- CAVES_LOCATIONS = {
- "Caves Blue Tunnel Right First 4",
- "Caves Blue Tunnel Left First 1",
- "Caves Blue Tunnel Left Second 5",
- "Caves Blue Tunnel Right Second 5",
- "Caves Blue Tunnel Right Third 1",
- "Caves Blue Tunnel Left Fourth 1",
- "Caves Blue Tunnel Left Third 1",
-
- "Caves First Floor Middle",
- "Caves First Floor Right",
- "Caves First Floor Left",
- "Caves First Floor Grounded",
- "Caves Lone Pillar",
- "Caves First Wooden Beam",
- "Caves Second Wooden Beam",
- "Caves Third Wooden Beam",
- "Caves Fourth Wooden Beam",
- "Caves Right Upstairs Left Row 8",
- "Caves Right Upstairs Right Row 3",
- "Caves Left Upstairs Single",
- "Caves Left Upstairs Left Row 5",
-
- "Tunnels Vault Box",
- "Theater Challenge Video",
-
- "Caves Skylight EP",
- "Challenge Water EP",
- "Tunnels Theater Flowers EP",
- "Tutorial Gate EP",
- }
-
- MOUNTAIN_UNREACHABLE_FROM_BEHIND = {
- "Mountaintop Trap Door Triple Exit",
-
- "Mountain Floor 1 Right Row 5",
- "Mountain Floor 1 Left Row 7",
- "Mountain Floor 1 Back Row 3",
- "Mountain Floor 1 Trash Pillar 2",
- "Mountain Floor 2 Near Row 5",
- "Mountain Floor 2 Far Row 6",
-
- "Mountain Floor 2 Light Bridge Controller Near",
- "Mountain Floor 2 Light Bridge Controller Far",
-
- "Mountain Bottom Floor Yellow Bridge EP",
- "Mountain Bottom Floor Blue Bridge EP",
- "Mountain Floor 2 Pink Bridge EP",
- }
-
- MOUNTAIN_REACHABLE_FROM_BEHIND = {
- "Mountain Floor 2 Elevator Discard",
- "Mountain Bottom Floor Giant Puzzle",
-
- "Mountain Final Room Left Pillar 4",
- "Mountain Final Room Right Pillar 4",
- }
-
- MOUNTAIN_EXTRAS = {
- "Challenge Vault Box",
- "Theater Challenge Video",
- "Mountain Bottom Floor Discard"
- }
-
- ALL_LOCATIONS_TO_ID = dict()
-
- @staticmethod
- def get_id(chex):
- """
- Calculates the location ID for any given location
- """
-
- return StaticWitnessLogic.CHECKS_BY_HEX[chex]["id"]
-
- @staticmethod
- def get_event_name(panel_hex):
- """
- Returns the event name of any given panel.
- """
-
- action = " Opened" if StaticWitnessLogic.CHECKS_BY_HEX[panel_hex]["panelType"] == "Door" else " Solved"
-
- return StaticWitnessLogic.CHECKS_BY_HEX[panel_hex]["checkName"] + action
-
- def __init__(self):
- all_loc_to_id = {
- panel_obj["checkName"]: self.get_id(chex)
- for chex, panel_obj in StaticWitnessLogic.CHECKS_BY_HEX.items()
- if panel_obj["id"]
- }
-
- all_loc_to_id = dict(
- sorted(all_loc_to_id.items(), key=lambda loc: loc[1])
- )
-
- for key, item in all_loc_to_id.items():
- self.ALL_LOCATIONS_TO_ID[key] = item
+if TYPE_CHECKING:
+ from . import WitnessWorld
class WitnessPlayerLocations:
@@ -459,104 +16,67 @@ class WitnessPlayerLocations:
Class that defines locations for a single player
"""
- def __init__(self, world, player, player_logic: WitnessPlayerLogic):
+ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> None:
"""Defines locations AFTER logic changes due to options"""
self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"}
- self.CHECK_LOCATIONS = StaticWitnessLocations.GENERAL_LOCATIONS.copy()
-
- doors = get_option_value(world, player, "shuffle_doors") >= 2
- earlyutm = is_option_enabled(world, player, "early_secret_area")
- victory = get_option_value(world, player, "victory_condition")
- mount_lasers = get_option_value(world, player, "mountain_lasers")
- chal_lasers = get_option_value(world, player, "challenge_lasers")
- # laser_shuffle = get_option_value(world, player, "shuffle_lasers")
-
- postgame = set()
- postgame = postgame | StaticWitnessLocations.CAVES_LOCATIONS
- postgame = postgame | StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND
- postgame = postgame | StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND
- postgame = postgame | StaticWitnessLocations.MOUNTAIN_EXTRAS
-
- self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | postgame
-
- mountain_enterable_from_top = victory == 0 or victory == 1 or (victory == 3 and chal_lasers > mount_lasers)
-
- if earlyutm or doors: # in non-doors, there is no way to get symbol-locked by the final pillars (currently)
- postgame -= StaticWitnessLocations.CAVES_LOCATIONS
-
- if (doors or earlyutm) and (victory == 0 or (victory == 2 and mount_lasers > chal_lasers)):
- postgame -= {"Challenge Vault Box", "Theater Challenge Video"}
+ self.CHECK_LOCATIONS = static_witness_locations.GENERAL_LOCATIONS.copy()
- if doors or mountain_enterable_from_top:
- postgame -= StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND
-
- if mountain_enterable_from_top:
- postgame -= StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND
-
- if (victory == 0 and doors) or victory == 1 or (victory == 2 and mount_lasers > chal_lasers and doors):
- postgame -= {"Mountain Bottom Floor Discard"}
-
- if is_option_enabled(world, player, "shuffle_discarded_panels"):
+ if world.options.shuffle_discarded_panels:
self.PANEL_TYPES_TO_SHUFFLE.add("Discard")
- if is_option_enabled(world, player, "shuffle_vault_boxes"):
+ if world.options.shuffle_vault_boxes:
self.PANEL_TYPES_TO_SHUFFLE.add("Vault")
- if get_option_value(world, player, "shuffle_EPs") == 1:
+ if world.options.shuffle_EPs == "individual":
self.PANEL_TYPES_TO_SHUFFLE.add("EP")
- elif get_option_value(world, player, "shuffle_EPs") == 2:
+ elif world.options.shuffle_EPs == "obelisk_sides":
self.PANEL_TYPES_TO_SHUFFLE.add("Obelisk Side")
- for obelisk_loc in StaticWitnessLocations.OBELISK_SIDES:
- obelisk_loc_hex = StaticWitnessLogic.CHECKS_BY_NAME[obelisk_loc]["checkHex"]
+ for obelisk_loc in static_witness_locations.OBELISK_SIDES:
+ obelisk_loc_hex = static_witness_logic.ENTITIES_BY_NAME[obelisk_loc]["entity_hex"]
if player_logic.REQUIREMENTS_BY_HEX[obelisk_loc_hex] == frozenset({frozenset()}):
self.CHECK_LOCATIONS.discard(obelisk_loc)
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | player_logic.ADDED_CHECKS
- if not is_option_enabled(world, player, "shuffle_postgame"):
- self.CHECK_LOCATIONS -= postgame
-
- self.CHECK_LOCATIONS -= {
- StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"]
- for panel in player_logic.PRECOMPLETED_LOCATIONS
- }
-
- self.CHECK_LOCATIONS.discard(StaticWitnessLogic.CHECKS_BY_HEX[player_logic.VICTORY_LOCATION]["checkName"])
+ self.CHECK_LOCATIONS.discard(static_witness_logic.ENTITIES_BY_HEX[player_logic.VICTORY_LOCATION]["checkName"])
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS - {
- StaticWitnessLogic.CHECKS_BY_HEX[check_hex]["checkName"]
- for check_hex in player_logic.COMPLETELY_DISABLED_CHECKS
+ static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"]
+ for entity_hex in player_logic.COMPLETELY_DISABLED_ENTITIES | player_logic.PRECOMPLETED_LOCATIONS
}
self.CHECK_PANELHEX_TO_ID = {
- StaticWitnessLogic.CHECKS_BY_NAME[ch]["checkHex"]: StaticWitnessLocations.ALL_LOCATIONS_TO_ID[ch]
+ static_witness_logic.ENTITIES_BY_NAME[ch]["entity_hex"]: static_witness_locations.ALL_LOCATIONS_TO_ID[ch]
for ch in self.CHECK_LOCATIONS
- if StaticWitnessLogic.CHECKS_BY_NAME[ch]["panelType"] in self.PANEL_TYPES_TO_SHUFFLE
+ if static_witness_logic.ENTITIES_BY_NAME[ch]["entityType"] in self.PANEL_TYPES_TO_SHUFFLE
}
- dog_hex = StaticWitnessLogic.CHECKS_BY_NAME["Town Pet the Dog"]["checkHex"]
- dog_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID["Town Pet the Dog"]
+ dog_hex = static_witness_logic.ENTITIES_BY_NAME["Town Pet the Dog"]["entity_hex"]
+ dog_id = static_witness_locations.ALL_LOCATIONS_TO_ID["Town Pet the Dog"]
self.CHECK_PANELHEX_TO_ID[dog_hex] = dog_id
self.CHECK_PANELHEX_TO_ID = dict(
sorted(self.CHECK_PANELHEX_TO_ID.items(), key=lambda item: item[1])
)
- event_locations = {
- p for p in player_logic.EVENT_PANELS
- }
+ event_locations = set(player_logic.USED_EVENT_NAMES_BY_HEX)
self.EVENT_LOCATION_TABLE = {
- StaticWitnessLocations.get_event_name(panel_hex): None
- for panel_hex in event_locations
+ static_witness_locations.get_event_name(entity_hex): None
+ for entity_hex in event_locations
}
check_dict = {
- StaticWitnessLogic.CHECKS_BY_HEX[location]["checkName"]:
- StaticWitnessLocations.get_id(StaticWitnessLogic.CHECKS_BY_HEX[location]["checkHex"])
+ static_witness_logic.ENTITIES_BY_HEX[location]["checkName"]:
+ static_witness_locations.get_id(static_witness_logic.ENTITIES_BY_HEX[location]["entity_hex"])
for location in self.CHECK_PANELHEX_TO_ID
}
self.CHECK_LOCATION_TABLE = {**self.EVENT_LOCATION_TABLE, **check_dict}
+
+ def add_location_late(self, entity_name: str) -> None:
+ entity_hex = static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"]
+ self.CHECK_LOCATION_TABLE[entity_hex] = static_witness_locations.get_id(entity_hex)
+ self.CHECK_PANELHEX_TO_ID[entity_hex] = static_witness_locations.get_id(entity_hex)
diff --git a/worlds/witness/options.py b/worlds/witness/options.py
new file mode 100644
index 000000000000..4855fc715933
--- /dev/null
+++ b/worlds/witness/options.py
@@ -0,0 +1,387 @@
+from dataclasses import dataclass
+
+from schema import And, Schema
+
+from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle, Visibility
+
+from .data import static_logic as static_witness_logic
+from .data.item_definition_classes import ItemCategory, WeightedItemDefinition
+
+
+class DisableNonRandomizedPuzzles(Toggle):
+ """
+ Disables puzzles that cannot be randomized.
+ This includes many puzzles that heavily involve the environment, such as Shadows, Monastery or Orchard.
+
+ The lasers for those areas will activate as you solve optional puzzles, such as Discarded Panels.
+ Additionally, the panel activating the Jungle Popup Wall will be on from the start.
+ """
+ display_name = "Disable non randomized puzzles"
+
+
+class EarlyCaves(Choice):
+ """
+ Adds an item that opens the Caves Shortcuts to Swamp and Mountain, allowing early access to the Caves even if you are not playing a remote Door Shuffle mode.
+ You can either add this item to the pool to be found in the multiworld, or you can outright start with it and have immediate access to the Caves.
+
+ If you choose "Add To Pool" and you are already playing a remote Door Shuffle mode, this option will do nothing.
+ """
+ display_name = "Early Caves"
+ option_off = 0
+ alias_false = 0
+ option_add_to_pool = 1
+ option_starting_inventory = 2
+ alias_true = 2
+ alias_on = 2
+
+
+class EarlySymbolItem(DefaultOnToggle):
+ """
+ Put a random helpful symbol item on an early check, specifically Tutorial Gate Open if it is available early.
+ """
+
+ visibility = Visibility.none
+
+
+class ShuffleSymbols(DefaultOnToggle):
+ """
+ If on, you will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols.
+
+ Please note that there is no minimum set of progression items in this randomizer.
+ If you turn this option off and don't turn on door shuffle or obelisk keys, there will be no progression items, which will disallow you from adding your yaml to a multiworld generation.
+ """
+ display_name = "Shuffle Symbols"
+
+
+class ShuffleLasers(Choice):
+ """
+ If on, the 11 lasers are turned into items and will activate on their own upon receiving them.
+ """
+ display_name = "Shuffle Lasers"
+ option_off = 0
+ alias_false = 0
+ option_local = 1
+ option_anywhere = 2
+ alias_true = 2
+ alias_on = 2
+
+
+class ShuffleDoors(Choice):
+ """
+ If on, opening doors, moving bridges etc. will require a "key".
+ - Panels: The panel on the door will be locked until receiving its corresponding key.
+ - Doors: The door will open immediately upon receiving its key. Door panels are added as location checks.
+ - Mixed: Includes all doors from "doors", and all control panels (bridges, elevators etc.) from "panels".
+ """
+ display_name = "Shuffle Doors"
+ option_off = 0
+ option_panels = 1
+ option_doors = 2
+ option_mixed = 3
+
+
+class DoorGroupings(Choice):
+ """
+ Controls how door items are grouped.
+
+ - Off: There will be one key for each door, potentially resulting in upwards of 120 keys being added to the item pool.
+ - Regional: All doors in the same general region will open at once with a single key, reducing the amount of door items and complexity.
+ """
+ display_name = "Door Groupings"
+ option_off = 0
+ option_regional = 1
+
+
+class ShuffleBoat(DefaultOnToggle):
+ """
+ If on, adds a "Boat" item to the item pool. Before receiving this item, you will not be able to use the boat.
+ """
+ display_name = "Shuffle Boat"
+
+
+class ShuffleDiscardedPanels(Toggle):
+ """
+ Adds Discarded Panels into the location pool.
+
+ Even if this is off, solving certain Discarded Panels may still be necessary to beat the game - The main example of this being the alternate activation triggers in "Disable non randomized puzzles".
+ """
+ display_name = "Shuffle Discarded Panels"
+
+
+class ShuffleVaultBoxes(Toggle):
+ """
+ Adds Vault Boxes to the location pool.
+ """
+ display_name = "Shuffle Vault Boxes"
+
+
+class ShuffleEnvironmentalPuzzles(Choice):
+ """
+ Adds Environmental/Obelisk Puzzles into the location pool.
+ - Individual: Every Environmental Puzzle sends an item.
+ - Obelisk Sides: Completing every puzzle on one side of an Obelisk sends an item.
+
+ Note: In Obelisk Sides, any EPs excluded through another option will be pre-completed on their Obelisk.
+ """
+ display_name = "Shuffle Environmental Puzzles"
+ option_off = 0
+ option_individual = 1
+ option_obelisk_sides = 2
+
+
+class ShuffleDog(Toggle):
+ """
+ Adds petting the Town dog into the location pool.
+ """
+ display_name = "Pet the Dog"
+
+
+class EnvironmentalPuzzlesDifficulty(Choice):
+ """
+ When "Shuffle Environmental Puzzles" is on, this setting governs which EPs are eligible for the location pool.
+ - Eclipse: Every EP in the game is eligible, including the 1-hour-long "Theater Eclipse EP".
+ - Tedious Theater Eclipse EP is excluded from the location pool.
+ - Normal: several other difficult or long EPs are excluded as well.
+ """
+ display_name = "Environmental Puzzles Difficulty"
+ option_normal = 0
+ option_tedious = 1
+ option_eclipse = 2
+
+
+class ObeliskKeys(DefaultOnToggle):
+ """
+ Add one Obelisk Key item per Obelisk, locking you out of solving any of the associated Environmental Puzzles.
+
+ Does nothing if "Shuffle Environmental Puzzles" is set to "off".
+ """
+ display_name = "Obelisk Keys"
+
+
+class ShufflePostgame(Toggle):
+ """
+ Adds locations into the pool that are guaranteed to become accessible after or at the same time as your goal.
+ Use this if you don't play with release on victory.
+ """
+ display_name = "Shuffle Postgame"
+
+
+class VictoryCondition(Choice):
+ """
+ Set the victory condition for this world.
+ - Elevator: Start the elevator at the bottom of the mountain (requires Mountain Lasers).
+ - Challenge: Beat the secret Challenge (requires Challenge Lasers).
+ - Mountain Box Short: Input the short solution to the Mountaintop Box (requires Mountain Lasers).
+ - Mountain Box Long: Input the long solution to the Mountaintop Box (requires Challenge Lasers).
+
+ It is important to note that while the Mountain Box requires Desert Laser to be redirected in Town for that laser
+ to count, the laser locks on the Elevator and Challenge Timer panels do not.
+ """
+ display_name = "Victory Condition"
+ option_elevator = 0
+ option_challenge = 1
+ option_mountain_box_short = 2
+ option_mountain_box_long = 3
+
+
+class PuzzleRandomization(Choice):
+ """
+ Puzzles in this randomizer are randomly generated. This option changes the difficulty/types of puzzles.
+ """
+ display_name = "Puzzle Randomization"
+ option_sigma_normal = 0
+ option_sigma_expert = 1
+ option_none = 2
+
+
+class MountainLasers(Range):
+ """
+ Sets the number of lasers required to enter the Mountain.
+ If set to a higher number than 7, the mountaintop box will be slightly rotated to make it possible to solve without the hatch being opened.
+ This change will also be applied logically to the long solution ("Challenge Lasers" option).
+ """
+ display_name = "Required Lasers for Mountain Entry"
+ range_start = 1
+ range_end = 11
+ default = 7
+
+
+class ChallengeLasers(Range):
+ """
+ Sets the number of lasers required to enter the Caves through the Mountain Bottom Floor Discard and to unlock the Challenge Timer Panel.
+ """
+ display_name = "Required Lasers for Challenge"
+ range_start = 1
+ range_end = 11
+ default = 11
+
+
+class ElevatorsComeToYou(Toggle):
+ """
+ If on, the Quarry Elevator, Bunker Elevator and Swamp Long Bridge will "come to you" if you approach them.
+ This does actually affect logic as it allows unintended backwards / early access into these areas.
+ """
+ display_name = "All Bridges & Elevators come to you"
+
+
+class TrapPercentage(Range):
+ """
+ Replaces junk items with traps, at the specified rate.
+ """
+ display_name = "Trap Percentage"
+ range_start = 0
+ range_end = 100
+ default = 20
+
+
+class TrapWeights(OptionDict):
+ """
+ Specify the weights determining how many copies of each trap item will be in your itempool.
+ If you don't want a specific type of trap, you can set the weight for it to 0 (Do not delete the entry outright!).
+ If you set all trap weights to 0, you will get no traps, bypassing the "Trap Percentage" option.
+ """
+ display_name = "Trap Weights"
+ schema = Schema({
+ trap_name: And(int, lambda n: n >= 0)
+ for trap_name, item_definition in static_witness_logic.ALL_ITEMS.items()
+ if isinstance(item_definition, WeightedItemDefinition) and item_definition.category is ItemCategory.TRAP
+ })
+ default = {
+ trap_name: item_definition.weight
+ for trap_name, item_definition in static_witness_logic.ALL_ITEMS.items()
+ if isinstance(item_definition, WeightedItemDefinition) and item_definition.category is ItemCategory.TRAP
+ }
+
+
+class PuzzleSkipAmount(Range):
+ """
+ Adds this many Puzzle Skips into the pool, if there is room. Puzzle Skips let you skip one panel.
+ """
+ display_name = "Puzzle Skips"
+ range_start = 0
+ range_end = 30
+ default = 10
+
+
+class HintAmount(Range):
+ """
+ Adds hints to Audio Logs. If set to a low amount, up to 2 additional duplicates of each hint will be added.
+ Remaining Audio Logs will have junk hints.
+ """
+ display_name = "Hints on Audio Logs"
+ range_start = 0
+ range_end = 49
+ default = 12
+
+
+class AreaHintPercentage(Range):
+ """
+ There are two types of hints for The Witness.
+ "Location hints" hint one location in your world or one location containing an item for your world.
+ "Area hints" tell you some general info about the items you can find in one of the main geographic areas on the island.
+ Use this option to specify how many of your hints you want to be area hints. The rest will be location hints.
+ """
+ display_name = "Area Hint Percentage"
+ range_start = 0
+ range_end = 100
+ default = 33
+
+
+class LaserHints(Toggle):
+ """
+ If on, lasers will tell you where their items are if you walk close to them in-game.
+ Only applies if Laser Shuffle is enabled.
+ """
+ display_name = "Laser Hints"
+
+
+class DeathLink(Toggle):
+ """
+ If on, whenever you fail a puzzle (with some exceptions), you and everyone who is also on Death Link dies.
+ The effect of a "death" in The Witness is a Bonk Trap.
+ """
+ display_name = "Death Link"
+
+
+class DeathLinkAmnesty(Range):
+ """
+ The number of panel fails to allow before sending a death through Death Link.
+ 0 means every panel fail will send a death, 1 means every other panel fail will send a death, etc.
+ """
+ display_name = "Death Link Amnesty"
+ range_start = 0
+ range_end = 5
+ default = 1
+
+
+@dataclass
+class TheWitnessOptions(PerGameCommonOptions):
+ puzzle_randomization: PuzzleRandomization
+ shuffle_symbols: ShuffleSymbols
+ shuffle_doors: ShuffleDoors
+ door_groupings: DoorGroupings
+ shuffle_boat: ShuffleBoat
+ shuffle_lasers: ShuffleLasers
+ disable_non_randomized_puzzles: DisableNonRandomizedPuzzles
+ shuffle_discarded_panels: ShuffleDiscardedPanels
+ shuffle_vault_boxes: ShuffleVaultBoxes
+ obelisk_keys: ObeliskKeys
+ shuffle_EPs: ShuffleEnvironmentalPuzzles # noqa: N815
+ EP_difficulty: EnvironmentalPuzzlesDifficulty
+ shuffle_postgame: ShufflePostgame
+ victory_condition: VictoryCondition
+ mountain_lasers: MountainLasers
+ challenge_lasers: ChallengeLasers
+ early_caves: EarlyCaves
+ early_symbol_item: EarlySymbolItem
+ elevators_come_to_you: ElevatorsComeToYou
+ trap_percentage: TrapPercentage
+ trap_weights: TrapWeights
+ puzzle_skip_amount: PuzzleSkipAmount
+ hint_amount: HintAmount
+ area_hint_percentage: AreaHintPercentage
+ laser_hints: LaserHints
+ death_link: DeathLink
+ death_link_amnesty: DeathLinkAmnesty
+
+
+witness_option_groups = [
+ OptionGroup("Puzzles & Goal", [
+ PuzzleRandomization,
+ VictoryCondition,
+ MountainLasers,
+ ChallengeLasers,
+ ]),
+ OptionGroup("Locations", [
+ ShuffleDiscardedPanels,
+ ShuffleVaultBoxes,
+ ShuffleEnvironmentalPuzzles,
+ EnvironmentalPuzzlesDifficulty,
+ ShufflePostgame,
+ DisableNonRandomizedPuzzles,
+ ]),
+ OptionGroup("Progression Items", [
+ ShuffleSymbols,
+ ShuffleDoors,
+ DoorGroupings,
+ ShuffleLasers,
+ ShuffleBoat,
+ ObeliskKeys,
+ ]),
+ OptionGroup("Filler Items", [
+ PuzzleSkipAmount,
+ TrapPercentage,
+ TrapWeights
+ ]),
+ OptionGroup("Hints", [
+ HintAmount,
+ AreaHintPercentage,
+ LaserHints
+ ]),
+ OptionGroup("Misc", [
+ EarlyCaves,
+ ElevatorsComeToYou,
+ DeathLink,
+ DeathLinkAmnesty,
+ ])
+]
diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py
new file mode 100644
index 000000000000..718fd7d172ba
--- /dev/null
+++ b/worlds/witness/player_items.py
@@ -0,0 +1,223 @@
+"""
+Defines progression, junk and event items for The Witness
+"""
+import copy
+from typing import TYPE_CHECKING, Dict, List, Set, cast
+
+from BaseClasses import Item, ItemClassification, MultiWorld
+
+from .data import static_items as static_witness_items
+from .data import static_logic as static_witness_logic
+from .data.item_definition_classes import (
+ DoorItemDefinition,
+ ItemCategory,
+ ItemData,
+ ItemDefinition,
+ ProgressiveItemDefinition,
+ WeightedItemDefinition,
+)
+from .data.utils import build_weighted_int_list
+from .locations import WitnessPlayerLocations
+from .player_logic import WitnessPlayerLogic
+
+if TYPE_CHECKING:
+ from . import WitnessWorld
+
+NUM_ENERGY_UPGRADES = 4
+
+
+class WitnessItem(Item):
+ """
+ Item from the game The Witness
+ """
+ game: str = "The Witness"
+
+
+class WitnessPlayerItems:
+ """
+ Class that defines Items for a single world
+ """
+
+ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic,
+ player_locations: WitnessPlayerLocations) -> None:
+ """Adds event items after logic changes due to options"""
+
+ self._world: "WitnessWorld" = world
+ self._multiworld: MultiWorld = world.multiworld
+ self._player_id: int = world.player
+ self._logic: WitnessPlayerLogic = player_logic
+ self._locations: WitnessPlayerLocations = player_locations
+
+ # Duplicate the static item data, then make any player-specific adjustments to classification.
+ self.item_data: Dict[str, ItemData] = copy.deepcopy(static_witness_items.ITEM_DATA)
+
+ # Remove all progression items that aren't actually in the game.
+ self.item_data = {
+ name: data for (name, data) in self.item_data.items()
+ if data.classification not in
+ {ItemClassification.progression, ItemClassification.progression_skip_balancing}
+ or name in player_logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME
+ }
+
+ # Downgrade door items
+ for item_name, item_data in self.item_data.items():
+ if not isinstance(item_data.definition, DoorItemDefinition):
+ continue
+
+ if all(not self._logic.solvability_guaranteed(e_hex) for e_hex in item_data.definition.panel_id_hexes):
+ item_data.classification = ItemClassification.useful
+
+ # Build the mandatory item list.
+ self._mandatory_items: Dict[str, int] = {}
+
+ # Add progression items to the mandatory item list.
+ progression_dict = {
+ name: data for (name, data) in self.item_data.items()
+ if data.classification in {ItemClassification.progression, ItemClassification.progression_skip_balancing}
+ }
+ for item_name, item_data in progression_dict.items():
+ if isinstance(item_data.definition, ProgressiveItemDefinition):
+ num_progression = len(self._logic.MULTI_LISTS[item_name])
+ self._mandatory_items[item_name] = num_progression
+ else:
+ self._mandatory_items[item_name] = 1
+
+ # Add setting-specific useful items to the mandatory item list.
+ for item_name, item_data in {name: data for (name, data) in self.item_data.items()
+ if data.classification == ItemClassification.useful}.items():
+ if item_name in static_witness_items._special_usefuls:
+ continue
+
+ if item_name == "Energy Capacity":
+ self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES
+ elif isinstance(item_data.classification, ProgressiveItemDefinition):
+ self._mandatory_items[item_name] = len(item_data.mappings)
+ else:
+ self._mandatory_items[item_name] = 1
+
+ # Add event items to the item definition list for later lookup.
+ for event_location in self._locations.EVENT_LOCATION_TABLE:
+ location_name = player_logic.EVENT_ITEM_PAIRS[event_location]
+ self.item_data[location_name] = ItemData(None, ItemDefinition(0, ItemCategory.EVENT),
+ ItemClassification.progression, False)
+
+ def get_mandatory_items(self) -> Dict[str, int]:
+ """
+ Returns the list of items that must be in the pool for the game to successfully generate.
+ """
+ return self._mandatory_items.copy()
+
+ def get_filler_items(self, quantity: int) -> Dict[str, int]:
+ """
+ Generates a list of filler items of the given length.
+ """
+ if quantity <= 0:
+ return {}
+
+ output: Dict[str, int] = {}
+ remaining_quantity = quantity
+
+ # Add joke items.
+ output.update({name: 1 for (name, data) in self.item_data.items()
+ if data.definition.category is ItemCategory.JOKE})
+ remaining_quantity -= len(output)
+
+ # Read trap configuration data.
+ trap_weight = self._world.options.trap_percentage / 100
+ trap_items = self._world.options.trap_weights.value
+
+ if not sum(trap_items.values()):
+ trap_weight = 0
+
+ # Add filler items to the list.
+ filler_weight = 1 - trap_weight
+
+ filler_items: Dict[str, float]
+ filler_items = {name: data.definition.weight if isinstance(data.definition, WeightedItemDefinition) else 1
+ for (name, data) in self.item_data.items() if data.definition.category is ItemCategory.FILLER}
+ filler_items = {name: base_weight * filler_weight / sum(filler_items.values())
+ for name, base_weight in filler_items.items() if base_weight > 0}
+
+ # Add trap items.
+ if trap_weight > 0:
+ filler_items.update({name: base_weight * trap_weight / sum(trap_items.values())
+ for name, base_weight in trap_items.items() if base_weight > 0})
+
+ # Get the actual number of each item by scaling the float weight values to match the target quantity.
+ int_weights: List[int] = build_weighted_int_list(filler_items.values(), remaining_quantity)
+ output.update(zip(filler_items.keys(), int_weights))
+
+ return output
+
+ def get_early_items(self) -> List[str]:
+ """
+ Returns items that are ideal for placing on extremely early checks, like the tutorial gate.
+ """
+ output: Set[str] = set()
+ if self._world.options.shuffle_symbols:
+ output = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
+
+ if self._world.options.shuffle_discarded_panels:
+ if self._world.options.puzzle_randomization == "sigma_expert":
+ output.add("Arrows")
+ else:
+ output.add("Triangles")
+
+ # Replace progressive items with their parents.
+ output = {static_witness_logic.get_parent_progressive_item(item) for item in output}
+
+ # Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
+ # before create_items so that we'll be able to check placed items instead of just removing all items mentioned
+ # regardless of whether or not they actually wind up being manually placed.
+ for plando_setting in self._multiworld.plando_items[self._player_id]:
+ if plando_setting.get("from_pool", True):
+ for item_setting_key in [key for key in ["item", "items"] if key in plando_setting]:
+ if isinstance(plando_setting[item_setting_key], str):
+ output -= {plando_setting[item_setting_key]}
+ elif isinstance(plando_setting[item_setting_key], dict):
+ output -= {item for item, weight in plando_setting[item_setting_key].items() if weight}
+ else:
+ # Assume this is some other kind of iterable.
+ for inner_item in plando_setting[item_setting_key]:
+ if isinstance(inner_item, str):
+ output -= {inner_item}
+ elif isinstance(inner_item, dict):
+ output -= {item for item, weight in inner_item.items() if weight}
+
+ # Sort the output for consistency across versions if the implementation changes but the logic does not.
+ return sorted(output)
+
+ def get_door_ids_in_pool(self) -> List[int]:
+ """
+ Returns the total set of all door IDs that are controlled by items in the pool.
+ """
+ output: List[int] = []
+ for item_name, item_data in dict(self.item_data.items()).items():
+ if not isinstance(item_data.definition, DoorItemDefinition):
+ continue
+ output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
+
+ return output
+
+ def get_symbol_ids_not_in_pool(self) -> List[int]:
+ """
+ Returns the item IDs of symbol items that were defined in the configuration file but are not in the pool.
+ """
+ return [
+ # data.ap_code is guaranteed for a symbol definition
+ cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
+ if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL
+ ]
+
+ def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]:
+ output: Dict[int, List[int]] = {}
+ for item_name, quantity in dict(self._mandatory_items.items()).items():
+ item = self.item_data[item_name]
+ if isinstance(item.definition, ProgressiveItemDefinition):
+ # Note: we need to reference the static table here rather than the player-specific one because the child
+ # items were removed from the pool when we pruned out all progression items not in the settings.
+ output[cast(int, item.ap_code)] = [cast(int, static_witness_items.ITEM_DATA[child_item].ap_code)
+ for child_item in item.definition.child_item_names]
+ return output
+
+
diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py
index be1a34aedfcf..e8d11f43f51c 100644
--- a/worlds/witness/player_logic.py
+++ b/worlds/witness/player_logic.py
@@ -16,22 +16,139 @@
"""
import copy
-from typing import Set, Dict, cast, List
+from collections import defaultdict
from logging import warning
-
-from BaseClasses import MultiWorld
-from .static_logic import StaticWitnessLogic, DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
-from .utils import define_new_region, get_disable_unrandomized_list, parse_lambda, get_early_utm_list, \
- get_symbol_shuffle_list, get_door_panel_shuffle_list, get_doors_complex_list, get_doors_max_list, \
- get_doors_simple_list, get_laser_shuffle, get_ep_all_individual, get_ep_obelisks, get_ep_easy, get_ep_no_eclipse, \
- get_ep_no_caves, get_ep_no_mountain, get_ep_no_videos
-from .Options import is_option_enabled, get_option_value, the_witness_options
+from typing import TYPE_CHECKING, Dict, List, Set, Tuple, cast
+
+from .data import static_logic as static_witness_logic
+from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
+from .data.static_logic import StaticWitnessLogicObj
+from .data.utils import (
+ WitnessRule,
+ define_new_region,
+ get_boat,
+ get_caves_except_path_to_challenge_exclusion_list,
+ get_complex_additional_panels,
+ get_complex_door_panels,
+ get_complex_doors,
+ get_disable_unrandomized_list,
+ get_discard_exclusion_list,
+ get_early_caves_list,
+ get_early_caves_start_list,
+ get_elevators_come_to_you,
+ get_ep_all_individual,
+ get_ep_easy,
+ get_ep_no_eclipse,
+ get_ep_obelisks,
+ get_laser_shuffle,
+ get_obelisk_keys,
+ get_simple_additional_panels,
+ get_simple_doors,
+ get_simple_panels,
+ get_symbol_shuffle_list,
+ get_vault_exclusion_list,
+ logical_and_witness_rules,
+ logical_or_witness_rules,
+ parse_lambda,
+)
+
+if TYPE_CHECKING:
+ from . import WitnessWorld
class WitnessPlayerLogic:
"""WITNESS LOGIC CLASS"""
- def reduce_req_within_region(self, panel_hex):
+ VICTORY_LOCATION: str
+
+ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]) -> None:
+ self.YAML_DISABLED_LOCATIONS: Set[str] = disabled_locations
+ self.YAML_ADDED_ITEMS: Dict[str, int] = start_inv
+
+ self.EVENT_PANELS_FROM_PANELS: Set[str] = set()
+ self.EVENT_PANELS_FROM_REGIONS: Set[str] = set()
+
+ self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES: Set[str] = set()
+
+ self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY: Set[str] = set()
+
+ self.UNREACHABLE_REGIONS: Set[str] = set()
+
+ self.THEORETICAL_ITEMS: Set[str] = set()
+ self.THEORETICAL_ITEMS_NO_MULTI: Set[str] = set()
+ self.MULTI_AMOUNTS: Dict[str, int] = defaultdict(lambda: 1)
+ self.MULTI_LISTS: Dict[str, List[str]] = {}
+ self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: Set[str] = set()
+ self.PROG_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set()
+ self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {}
+ self.STARTING_INVENTORY: Set[str] = set()
+
+ self.DIFFICULTY = world.options.puzzle_randomization
+
+ self.REFERENCE_LOGIC: StaticWitnessLogicObj
+ if self.DIFFICULTY == "sigma_expert":
+ self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
+ elif self.DIFFICULTY == "none":
+ self.REFERENCE_LOGIC = static_witness_logic.vanilla
+ else:
+ self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
+
+ self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
+ self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
+ )
+ self.CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
+ self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
+ )
+ self.DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = copy.deepcopy(
+ self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX
+ )
+ self.REQUIREMENTS_BY_HEX: Dict[str, WitnessRule] = {}
+
+ self.EVENT_ITEM_PAIRS: Dict[str, str] = {}
+ self.COMPLETELY_DISABLED_ENTITIES: Set[str] = set()
+ self.DISABLE_EVERYTHING_BEHIND: Set[str] = set()
+ self.PRECOMPLETED_LOCATIONS: Set[str] = set()
+ self.EXCLUDED_LOCATIONS: Set[str] = set()
+ self.ADDED_CHECKS: Set[str] = set()
+ self.VICTORY_LOCATION = "0x0356B"
+
+ self.ALWAYS_EVENT_NAMES_BY_HEX = {
+ "0x00509": "+1 Laser (Symmetry Laser)",
+ "0x012FB": "+1 Laser (Desert Laser)",
+ "0x09F98": "Desert Laser Redirection",
+ "0x01539": "+1 Laser (Quarry Laser)",
+ "0x181B3": "+1 Laser (Shadows Laser)",
+ "0x014BB": "+1 Laser (Keep Laser)",
+ "0x17C65": "+1 Laser (Monastery Laser)",
+ "0x032F9": "+1 Laser (Town Laser)",
+ "0x00274": "+1 Laser (Jungle Laser)",
+ "0x0C2B2": "+1 Laser (Bunker Laser)",
+ "0x00BF6": "+1 Laser (Swamp Laser)",
+ "0x028A4": "+1 Laser (Treehouse Laser)",
+ "0x17C34": "Mountain Entry",
+ "0xFFF00": "Bottom Floor Discard Turns On",
+ }
+
+ self.USED_EVENT_NAMES_BY_HEX: Dict[str, str] = {}
+ self.CONDITIONAL_EVENTS: Dict[Tuple[str, str], str] = {}
+
+ # The basic requirements to solve each entity come from StaticWitnessLogic.
+ # However, for any given world, the options (e.g. which item shuffles are enabled) affect the requirements.
+ self.make_options_adjustments(world)
+ self.determine_unrequired_entities(world)
+ self.find_unsolvable_entities(world)
+
+ # After we have adjusted the raw requirements, we perform a dependency reduction for the entity requirements.
+ # This will make the access conditions way faster, instead of recursively checking dependent entities each time.
+ self.make_dependency_reduced_checklist()
+
+ # Finalize which items actually exist in the MultiWorld and which get grouped into progressive items.
+ self.finalize_items()
+
+ # Create event-item pairs for specific panels in the game.
+ self.make_event_panel_lists()
+
+ def reduce_req_within_region(self, entity_hex: str) -> WitnessRule:
"""
Panels in this game often only turn on when other panels are solved.
Those other panels may have different item requirements.
@@ -40,106 +157,147 @@ def reduce_req_within_region(self, panel_hex):
Panels outside of the same region will still be checked manually.
"""
- if panel_hex in self.COMPLETELY_DISABLED_CHECKS or panel_hex in self.PRECOMPLETED_LOCATIONS:
+ if self.is_disabled(entity_hex):
return frozenset()
- check_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel_hex]
+ entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]
- these_items = frozenset({frozenset()})
+ if entity_obj["region"] is not None and entity_obj["region"]["name"] in self.UNREACHABLE_REGIONS:
+ return frozenset()
- if check_obj["id"]:
- these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["items"]
+ # For the requirement of an entity, we consider two things:
+ # 1. Any items this entity needs (e.g. Symbols or Door Items)
+ these_items: WitnessRule = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex].get("items", frozenset({frozenset()}))
+ # 2. Any entities that this entity depends on (e.g. one panel powering on the next panel in a set)
+ these_panels: WitnessRule = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["entities"]
+ # Remove any items that don't actually exist in the settings (e.g. Symbol Shuffle turned off)
these_items = frozenset({
subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI)
for subset in these_items
})
+ # Update the list of "items that are actually being used by any entity"
for subset in these_items:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset)
- if panel_hex in self.DOOR_ITEMS_BY_ID:
- door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[panel_hex]})
-
- all_options = set()
-
- for dependentItem in door_items:
- self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependentItem)
- for items_option in these_items:
- all_options.add(items_option.union(dependentItem))
-
- if panel_hex != "0x28A0D":
- return frozenset(all_options)
- else: # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved
- these_items = all_options
-
- these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"]
-
- these_panels = frozenset({panels - self.PRECOMPLETED_LOCATIONS for panels in these_panels})
-
- if these_panels == frozenset({frozenset()}):
- return these_items
-
- all_options = set()
+ # Handle door entities (door shuffle)
+ if entity_hex in self.DOOR_ITEMS_BY_ID:
+ # If this entity is opened by a door item that exists in the itempool, add that item to its requirements.
+ door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]})
+
+ for dependent_item in door_items:
+ self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item)
+
+ these_items = logical_and_witness_rules([door_items, these_items])
+
+ # A door entity is opened by its door item instead of previous entities powering it.
+ # That means we need to ignore any dependent requirements.
+ # However, there are some entities that depend on other entities because of an environmental reason.
+ # Those requirements need to be preserved even in door shuffle.
+ entity_dependencies_need_to_be_preserved = (
+ # EPs keep all their entity dependencies
+ static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] == "EP"
+ # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved,
+ # except in Expert, where that dependency doesn't exist, but now there *is* a power dependency.
+ # In the future, it'd be wise to make a distinction between "power dependencies" and other dependencies.
+ or entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels)
+ # Another dependency that is not power-based: The Symmetry Island Upper Panel latches
+ or entity_hex == "0x1C349"
+ )
+
+ # If this is not one of those special cases, solving this door entity only needs its own item requirement.
+ # Dependent entities from these_panels are ignored, and we just return these_items directly.
+ if not entity_dependencies_need_to_be_preserved:
+ return these_items
+
+ # Now that we have item requirements and entity dependencies, it's time for the dependency reduction.
+
+ # For each entity that this entity depends on (e.g. a panel turning on another panel),
+ # Add that entities requirements to this entity.
+ # If there are multiple options, consider each, and then or-chain them.
+ all_options = []
for option in these_panels:
- dependent_items_for_option = frozenset({frozenset()})
+ dependent_items_for_option: WitnessRule = frozenset({frozenset()})
- for option_panel in option:
- dep_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX.get(option_panel)
+ # For each entity in this option, resolve it to its actual requirement.
+ for option_entity in option:
+ dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity, {})
- if option_panel in self.COMPLETELY_DISABLED_CHECKS:
+ if option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect",
+ "PP2 Weirdness", "Theater to Tunnels"}:
+ new_items = frozenset({frozenset([option_entity])})
+ elif option_entity in self.DISABLE_EVERYTHING_BEHIND:
new_items = frozenset()
- elif option_panel in {"7 Lasers", "11 Lasers", "PP2 Weirdness", "Theater to Tunnels"}:
- new_items = frozenset({frozenset([option_panel])})
- # If a panel turns on when a panel in a different region turns on,
- # the latter panel will be an "event panel", unless it ends up being
- # a location itself. This prevents generation failures.
- elif dep_obj["region"]["name"] != check_obj["region"]["name"]:
- new_items = frozenset({frozenset([option_panel])})
- self.EVENT_PANELS_FROM_PANELS.add(option_panel)
- elif option_panel in self.ALWAYS_EVENT_NAMES_BY_HEX.keys():
- new_items = frozenset({frozenset([option_panel])})
- self.EVENT_PANELS_FROM_PANELS.add(option_panel)
else:
- new_items = self.reduce_req_within_region(option_panel)
-
- updated_items = set()
-
- for items_option in dependent_items_for_option:
- for items_option2 in new_items:
- updated_items.add(items_option.union(items_option2))
+ theoretical_new_items = self.get_entity_requirement(option_entity)
- dependent_items_for_option = updated_items
+ if not theoretical_new_items:
+ # If the dependent entity is unsolvable & it is an EP, the current entity is an Obelisk Side.
+ # In this case, we actually have to skip it because it will just become pre-solved instead.
+ if dep_obj["entityType"] == "EP":
+ continue
+ # If the dependent entity is unsolvable and is NOT an EP, this requirement option is invalid.
+ new_items = frozenset()
+ elif option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX:
+ new_items = frozenset({frozenset([option_entity])})
+ elif (entity_hex, option_entity) in self.CONDITIONAL_EVENTS:
+ new_items = frozenset({frozenset([option_entity])})
+ self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[
+ (entity_hex, option_entity)
+ ]
+ else:
+ new_items = theoretical_new_items
+ if dep_obj["region"] and entity_obj["region"] != dep_obj["region"]:
+ new_items = frozenset(
+ frozenset(possibility | {dep_obj["region"]["name"]})
+ for possibility in new_items
+ )
+
+ dependent_items_for_option = logical_and_witness_rules([dependent_items_for_option, new_items])
+
+ # Combine the resolved dependent entity requirements with the item requirements of this entity.
+ all_options.append(logical_and_witness_rules([these_items, dependent_items_for_option]))
+
+ # or-chain all separate dependent entity options.
+ return logical_or_witness_rules(all_options)
+
+ def get_entity_requirement(self, entity_hex: str) -> WitnessRule:
+ """
+ Get requirement of entity by its hex code.
+ These requirements are cached, with the actual function calculating them being reduce_req_within_region.
+ """
+ requirement = self.REQUIREMENTS_BY_HEX.get(entity_hex)
- for items_option in these_items:
- for dependentItem in dependent_items_for_option:
- all_options.add(items_option.union(dependentItem))
+ if requirement is None:
+ requirement = self.reduce_req_within_region(entity_hex)
+ self.REQUIREMENTS_BY_HEX[entity_hex] = requirement
- return frozenset(all_options)
+ return requirement
- def make_single_adjustment(self, adj_type, line):
- from . import StaticWitnessItems
+ def make_single_adjustment(self, adj_type: str, line: str) -> None:
+ from .data import static_items as static_witness_items
"""Makes a single logic adjustment based on additional logic file"""
if adj_type == "Items":
line_split = line.split(" - ")
item_name = line_split[0]
- if item_name not in StaticWitnessItems.item_data:
- raise RuntimeError("Item \"" + item_name + "\" does not exist.")
+ if item_name not in static_witness_items.ITEM_DATA:
+ raise RuntimeError(f'Item "{item_name}" does not exist.')
self.THEORETICAL_ITEMS.add(item_name)
- if isinstance(StaticWitnessLogic.all_items[item_name], ProgressiveItemDefinition):
+ if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition):
self.THEORETICAL_ITEMS_NO_MULTI.update(cast(ProgressiveItemDefinition,
- StaticWitnessLogic.all_items[item_name]).child_item_names)
+ static_witness_logic.ALL_ITEMS[item_name]).child_item_names)
else:
self.THEORETICAL_ITEMS_NO_MULTI.add(item_name)
- if StaticWitnessLogic.all_items[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
- panel_hexes = cast(DoorItemDefinition, StaticWitnessLogic.all_items[item_name]).panel_id_hexes
- for panel_hex in panel_hexes:
- self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, []).append(item_name)
+ if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
+ entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes
+ for entity_hex in entity_hexes:
+ self.DOOR_ITEMS_BY_ID.setdefault(entity_hex, []).append(item_name)
return
@@ -147,43 +305,30 @@ def make_single_adjustment(self, adj_type, line):
item_name = line
self.THEORETICAL_ITEMS.discard(item_name)
- if isinstance(StaticWitnessLogic.all_items[item_name], ProgressiveItemDefinition):
- self.THEORETICAL_ITEMS_NO_MULTI\
- .difference_update(cast(ProgressiveItemDefinition,
- StaticWitnessLogic.all_items[item_name]).child_item_names)
+ if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition):
+ self.THEORETICAL_ITEMS_NO_MULTI.difference_update(
+ cast(ProgressiveItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).child_item_names
+ )
else:
self.THEORETICAL_ITEMS_NO_MULTI.discard(item_name)
- if StaticWitnessLogic.all_items[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
- panel_hexes = cast(DoorItemDefinition, StaticWitnessLogic.all_items[item_name]).panel_id_hexes
- for panel_hex in panel_hexes:
- if panel_hex in self.DOOR_ITEMS_BY_ID and item_name in self.DOOR_ITEMS_BY_ID[panel_hex]:
- self.DOOR_ITEMS_BY_ID[panel_hex].remove(item_name)
+ if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
+ entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes
+ for entity_hex in entity_hexes:
+ if entity_hex in self.DOOR_ITEMS_BY_ID and item_name in self.DOOR_ITEMS_BY_ID[entity_hex]:
+ self.DOOR_ITEMS_BY_ID[entity_hex].remove(item_name)
if adj_type == "Starting Inventory":
self.STARTING_INVENTORY.add(line)
if adj_type == "Event Items":
line_split = line.split(" - ")
- hex_set = line_split[1].split(",")
-
- for hex_code in hex_set:
- self.ALWAYS_EVENT_NAMES_BY_HEX[hex_code] = line_split[0]
-
- """
- Should probably do this differently...
- Events right now depend on a panel.
- That seems bad.
- """
-
- to_remove = set()
-
- for hex_code, event_name in self.ALWAYS_EVENT_NAMES_BY_HEX.items():
- if hex_code not in hex_set and event_name == line_split[0]:
- to_remove.add(hex_code)
+ new_event_name = line_split[0]
+ entity_hex = line_split[1]
+ dependent_hex_set = line_split[2].split(",")
- for remove in to_remove:
- del self.ALWAYS_EVENT_NAMES_BY_HEX[remove]
+ for dependent_hex in dependent_hex_set:
+ self.CONDITIONAL_EVENTS[(entity_hex, dependent_hex)] = new_event_name
return
@@ -191,14 +336,15 @@ def make_single_adjustment(self, adj_type, line):
line_split = line.split(" - ")
requirement = {
- "panels": parse_lambda(line_split[1]),
+ "entities": parse_lambda(line_split[1]),
}
if len(line_split) > 2:
required_items = parse_lambda(line_split[2])
- items_actually_in_the_game = [item_name for item_name, item_definition
- in StaticWitnessLogic.all_items.items()
- if item_definition.category is ItemCategory.SYMBOL]
+ items_actually_in_the_game = [
+ item_name for item_name, item_definition in static_witness_logic.ALL_ITEMS.items()
+ if item_definition.category is ItemCategory.SYMBOL
+ ]
required_items = frozenset(
subset.intersection(items_actually_in_the_game)
for subset in required_items
@@ -211,118 +357,254 @@ def make_single_adjustment(self, adj_type, line):
return
if adj_type == "Disabled Locations":
- panel_hex = line[:7]
+ entity_hex = line[:7]
- self.COMPLETELY_DISABLED_CHECKS.add(panel_hex)
+ self.COMPLETELY_DISABLED_ENTITIES.add(entity_hex)
+
+ return
+
+ if adj_type == "Irrelevant Locations":
+ entity_hex = line[:7]
+
+ self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES.add(entity_hex)
return
if adj_type == "Region Changes":
new_region_and_options = define_new_region(line + ":")
-
- self.CONNECTIONS_BY_REGION_NAME[new_region_and_options[0]["name"]] = new_region_and_options[1]
+
+ self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[new_region_and_options[0]["name"]] = new_region_and_options[1]
return
+ if adj_type == "New Connections":
+ line_split = line.split(" - ")
+ source_region = line_split[0]
+ target_region = line_split[1]
+ panel_set_string = line_split[2]
+
+ for connection in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region]:
+ if connection[0] == target_region:
+ self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].remove(connection)
+
+ if panel_set_string == "TrueOneWay":
+ self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add(
+ (target_region, frozenset({frozenset(["TrueOneWay"])}))
+ )
+ else:
+ new_lambda = logical_or_witness_rules([connection[1], parse_lambda(panel_set_string)])
+ self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add((target_region, new_lambda))
+ break
+ else:
+ new_conn = (target_region, parse_lambda(panel_set_string))
+ self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add(new_conn)
+
if adj_type == "Added Locations":
if "0x" in line:
- line = StaticWitnessLogic.CHECKS_BY_HEX[line]["checkName"]
+ line = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[line]["checkName"]
self.ADDED_CHECKS.add(line)
- if adj_type == "Precompleted Locations":
- self.PRECOMPLETED_LOCATIONS.add(line)
+ def handle_postgame(self, world: "WitnessWorld") -> List[List[str]]:
+ """
+ In shuffle_postgame, panels that become accessible "after or at the same time as the goal" are disabled.
+ This mostly involves the disabling of key panels (e.g. long box when the goal is short box).
+ These will then hava a cascading effect on other entities that are locked "behind" them.
+ """
+
+ postgame_adjustments = []
+
+ # Make some quick references to some options
+ remote_doors = world.options.shuffle_doors >= 2 # "Panels" mode has no region accessibility implications.
+ early_caves = world.options.early_caves
+ victory = world.options.victory_condition
+ mnt_lasers = world.options.mountain_lasers
+ chal_lasers = world.options.challenge_lasers
+
+ # Goal is "short box", and long box requires at least as many lasers as short box (as god intended)
+ proper_shortbox_goal = victory == "mountain_box_short" and chal_lasers >= mnt_lasers
+
+ # Goal is "long box", but short box requires at least as many lasers than long box.
+ reverse_longbox_goal = victory == "mountain_box_long" and mnt_lasers >= chal_lasers
+
+ # ||| Section 1: Proper postgame cases |||
+ # When something only comes into logic after the goal, e.g. "longbox is postgame if the goal is shortbox".
+
+ # Disable anything directly locked by the victory panel
+ self.DISABLE_EVERYTHING_BEHIND.add(self.VICTORY_LOCATION)
- def make_options_adjustments(self, world, player):
+ # If we have a long box goal, Challenge is behind the amount of lasers required to just win.
+ # This is technically slightly incorrect as the Challenge Vault Box could contain a *symbol* that is required
+ # to open Mountain Entry (Stars 2). However, since there is a very easy sphere 1 snipe, this is not considered.
+ if victory == "mountain_box_long":
+ postgame_adjustments.append(["Disabled Locations:", "0x0A332 (Challenge Timer Start)"])
+
+ # If we have a proper short box goal, anything based on challenge lasers will never have something required.
+ if proper_shortbox_goal:
+ postgame_adjustments.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"])
+ postgame_adjustments.append(["Disabled Locations:", "0x0A332 (Challenge Timer Start)"])
+
+ # In a case where long box can be activated before short box, short box is postgame.
+ if reverse_longbox_goal:
+ postgame_adjustments.append(["Disabled Locations:", "0x09F7F (Mountain Box Short)"])
+
+ # ||| Section 2: "Fun" considerations |||
+ # These are cases in which it was deemed "unfun" to have an "oops, all lasers" situation, especially when
+ # it's for a single possible item.
+
+ mbfd_extra_exclusions = (
+ # Progressive Dots 2 behind 11 lasers in an Elevator seed with vanilla doors = :(
+ victory == "elevator" and not remote_doors
+
+ # Caves Shortcuts / Challenge Entry (Panel) on MBFD in a Challenge seed with vanilla doors = :(
+ or victory == "challenge" and early_caves and not remote_doors
+ )
+
+ if mbfd_extra_exclusions:
+ postgame_adjustments.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"])
+
+ # Another big postgame case that is missed is "Desert Laser Redirect (Panel)".
+ # An 11 lasers longbox seed could technically have this item on Challenge Vault Box.
+ # This case is not considered and we will act like Desert Laser Redirect (Panel) is always accessible.
+ # (Which means we do no additional work, this comment just exists to document that case)
+
+ # ||| Section 3: "Post-or-equal-game" cases |||
+ # These are cases in which something comes into logic *at the same time* as your goal and thus also can't
+ # possibly have a required item. These can be a bit awkward.
+
+ # When your victory is Challenge, but you have to get to it the vanilla way, there are no required items
+ # that can show up in the Caves that aren't also needed on the descent through Mountain.
+ # So, we should disable all entities in the Caves and Tunnels *except* for those that are required to enter.
+ if not (early_caves or remote_doors) and victory == "challenge":
+ postgame_adjustments.append(get_caves_except_path_to_challenge_exclusion_list())
+
+ return postgame_adjustments
+
+ def make_options_adjustments(self, world: "WitnessWorld") -> None:
"""Makes logic adjustments based on options"""
adjustment_linesets_in_order = []
- if get_option_value(world, player, "victory_condition") == 0:
+ # Make condensed references to some options
+
+ remote_doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region access implications.
+ lasers = world.options.shuffle_lasers
+ victory = world.options.victory_condition
+ mnt_lasers = world.options.mountain_lasers
+ chal_lasers = world.options.challenge_lasers
+
+ # Victory Condition
+ if victory == "elevator":
self.VICTORY_LOCATION = "0x3D9A9"
- elif get_option_value(world, player, "victory_condition") == 1:
+ elif victory == "challenge":
self.VICTORY_LOCATION = "0x0356B"
- elif get_option_value(world, player, "victory_condition") == 2:
+ elif victory == "mountain_box_short":
self.VICTORY_LOCATION = "0x09F7F"
- elif get_option_value(world, player, "victory_condition") == 3:
+ elif victory == "mountain_box_long":
self.VICTORY_LOCATION = "0xFFF00"
- if get_option_value(world, player, "challenge_lasers") <= 7:
+ # Exclude panels from the post-game if shuffle_postgame is false.
+ if not world.options.shuffle_postgame:
+ adjustment_linesets_in_order += self.handle_postgame(world)
+
+ # Exclude Discards / Vaults
+ if not world.options.shuffle_discarded_panels:
+ # In disable_non_randomized, the discards are needed for alternate activation triggers, UNLESS both
+ # (remote) doors and lasers are shuffled.
+ if not world.options.disable_non_randomized_puzzles or (remote_doors and lasers):
+ adjustment_linesets_in_order.append(get_discard_exclusion_list())
+
+ if remote_doors:
+ adjustment_linesets_in_order.append(["Disabled Locations:", "0x17FA2"])
+
+ if not world.options.shuffle_vault_boxes:
+ adjustment_linesets_in_order.append(get_vault_exclusion_list())
+ if not victory == "challenge":
+ adjustment_linesets_in_order.append(["Disabled Locations:", "0x0A332"])
+
+ # Long box can usually only be solved by opening Mountain Entry. However, if it requires 7 lasers or less
+ # (challenge_lasers <= 7), you can now solve it without opening Mountain Entry first.
+ # Furthermore, if the user sets mountain_lasers > 7, the box is rotated to not require Mountain Entry either.
+ if chal_lasers <= 7 or mnt_lasers > 7:
adjustment_linesets_in_order.append([
"Requirement Changes:",
"0xFFF00 - 11 Lasers - True",
])
- if is_option_enabled(world, player, "disable_non_randomized_puzzles"):
+ if world.options.disable_non_randomized_puzzles:
adjustment_linesets_in_order.append(get_disable_unrandomized_list())
- if is_option_enabled(world, player, "shuffle_symbols") or "shuffle_symbols" not in the_witness_options.keys():
+ if world.options.shuffle_symbols:
adjustment_linesets_in_order.append(get_symbol_shuffle_list())
- if get_option_value(world, player, "EP_difficulty") == 0:
+ if world.options.EP_difficulty == "normal":
adjustment_linesets_in_order.append(get_ep_easy())
- elif get_option_value(world, player, "EP_difficulty") == 1:
+ elif world.options.EP_difficulty == "tedious":
adjustment_linesets_in_order.append(get_ep_no_eclipse())
- if not is_option_enabled(world, player, "shuffle_vault_boxes"):
- adjustment_linesets_in_order.append(get_ep_no_videos())
-
- doors = get_option_value(world, player, "shuffle_doors") >= 2
- earlyutm = is_option_enabled(world, player, "early_secret_area")
- victory = get_option_value(world, player, "victory_condition")
- mount_lasers = get_option_value(world, player, "mountain_lasers")
- chal_lasers = get_option_value(world, player, "challenge_lasers")
-
- excluse_postgame = not is_option_enabled(world, player, "shuffle_postgame")
-
- if excluse_postgame and not (earlyutm or doors):
- adjustment_linesets_in_order.append(get_ep_no_caves())
-
- mountain_enterable_from_top = victory == 0 or victory == 1 or (victory == 3 and chal_lasers > mount_lasers)
- if excluse_postgame and not mountain_enterable_from_top:
- adjustment_linesets_in_order.append(get_ep_no_mountain())
-
- if get_option_value(world, player, "shuffle_doors") == 1:
- adjustment_linesets_in_order.append(get_door_panel_shuffle_list())
-
- if get_option_value(world, player, "shuffle_doors") == 2:
- adjustment_linesets_in_order.append(get_doors_simple_list())
-
- if get_option_value(world, player, "shuffle_doors") == 3:
- adjustment_linesets_in_order.append(get_doors_complex_list())
-
- if get_option_value(world, player, "shuffle_doors") == 4:
- adjustment_linesets_in_order.append(get_doors_max_list())
-
- if is_option_enabled(world, player, "early_secret_area"):
- adjustment_linesets_in_order.append(get_early_utm_list())
+ if world.options.door_groupings == "regional":
+ if world.options.shuffle_doors == "panels":
+ adjustment_linesets_in_order.append(get_simple_panels())
+ elif world.options.shuffle_doors == "doors":
+ adjustment_linesets_in_order.append(get_simple_doors())
+ elif world.options.shuffle_doors == "mixed":
+ adjustment_linesets_in_order.append(get_simple_doors())
+ adjustment_linesets_in_order.append(get_simple_additional_panels())
+ else:
+ if world.options.shuffle_doors == "panels":
+ adjustment_linesets_in_order.append(get_complex_door_panels())
+ adjustment_linesets_in_order.append(get_complex_additional_panels())
+ elif world.options.shuffle_doors == "doors":
+ adjustment_linesets_in_order.append(get_complex_doors())
+ elif world.options.shuffle_doors == "mixed":
+ adjustment_linesets_in_order.append(get_complex_doors())
+ adjustment_linesets_in_order.append(get_complex_additional_panels())
+
+ if world.options.shuffle_boat:
+ adjustment_linesets_in_order.append(get_boat())
+
+ if world.options.early_caves == "starting_inventory":
+ adjustment_linesets_in_order.append(get_early_caves_start_list())
+
+ if world.options.early_caves == "add_to_pool" and not remote_doors:
+ adjustment_linesets_in_order.append(get_early_caves_list())
+
+ if world.options.elevators_come_to_you:
+ adjustment_linesets_in_order.append(get_elevators_come_to_you())
for item in self.YAML_ADDED_ITEMS:
adjustment_linesets_in_order.append(["Items:", item])
- if is_option_enabled(world, player, "shuffle_lasers"):
+ if lasers:
adjustment_linesets_in_order.append(get_laser_shuffle())
- if get_option_value(world, player, "shuffle_EPs") == 0: # No EP Shuffle
- adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_all_individual()[1:])
- adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:])
+ if world.options.shuffle_EPs and world.options.obelisk_keys:
+ adjustment_linesets_in_order.append(get_obelisk_keys())
+
+ if world.options.shuffle_EPs == "obelisk_sides":
+ ep_gen = ((ep_hex, ep_obj) for (ep_hex, ep_obj) in self.REFERENCE_LOGIC.ENTITIES_BY_HEX.items()
+ if ep_obj["entityType"] == "EP")
- elif get_option_value(world, player, "shuffle_EPs") == 1: # Individual EPs
+ for ep_hex, ep_obj in ep_gen:
+ obelisk = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[self.REFERENCE_LOGIC.EP_TO_OBELISK_SIDE[ep_hex]]
+ obelisk_name = obelisk["checkName"]
+ ep_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[ep_hex]["checkName"]
+ self.ALWAYS_EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}"
+ else:
adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:])
- yaml_disabled_eps = []
+ if not world.options.shuffle_EPs:
+ adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_all_individual()[1:])
for yaml_disabled_location in self.YAML_DISABLED_LOCATIONS:
- if yaml_disabled_location not in StaticWitnessLogic.CHECKS_BY_NAME:
+ if yaml_disabled_location not in self.REFERENCE_LOGIC.ENTITIES_BY_NAME:
continue
- loc_obj = StaticWitnessLogic.CHECKS_BY_NAME[yaml_disabled_location]
-
- if loc_obj["panelType"] == "EP" and get_option_value(world, player, "shuffle_EPs") == 2:
- yaml_disabled_eps.append(loc_obj["checkHex"])
+ loc_obj = self.REFERENCE_LOGIC.ENTITIES_BY_NAME[yaml_disabled_location]
- if loc_obj["panelType"] in {"EP", "General"}:
- self.EXCLUDED_LOCATIONS.add(loc_obj["checkHex"])
+ if loc_obj["entityType"] == "EP":
+ self.COMPLETELY_DISABLED_ENTITIES.add(loc_obj["entity_hex"])
- adjustment_linesets_in_order.append(["Precompleted Locations:"] + yaml_disabled_eps)
+ elif loc_obj["entityType"] in {"General", "Vault", "Discard"}:
+ self.EXCLUDED_LOCATIONS.add(loc_obj["entity_hex"])
for adjustment_lineset in adjustment_linesets_in_order:
current_adjustment_type = None
@@ -335,24 +617,200 @@ def make_options_adjustments(self, world, player):
current_adjustment_type = line[:-1]
continue
+ if current_adjustment_type is None:
+ raise ValueError(f"Adjustment lineset {adjustment_lineset} is malformed")
+
self.make_single_adjustment(current_adjustment_type, line)
- def make_dependency_reduced_checklist(self):
+ for entity_id in self.COMPLETELY_DISABLED_ENTITIES:
+ if entity_id in self.DOOR_ITEMS_BY_ID:
+ del self.DOOR_ITEMS_BY_ID[entity_id]
+
+ def discover_reachable_regions(self) -> Set[str]:
+ """
+ Some options disable panels or remove specific items.
+ This can make entire regions completely unreachable, because all their incoming connections are invalid.
+ This function starts from the Entry region and performs a graph search to discover all reachable regions.
+ """
+ reachable_regions = {"Entry"}
+ new_regions_found = True
+
+ # This for loop "floods" the region graph until no more new regions are discovered.
+ # Note that connections that rely on disabled entities are considered invalid.
+ # This fact may lead to unreachable regions being discovered.
+ while new_regions_found:
+ new_regions_found = False
+ regions_to_check = reachable_regions.copy()
+
+ # Find new regions through connections from currently reachable regions
+ while regions_to_check:
+ next_region = regions_to_check.pop()
+
+ for region_exit in self.CONNECTIONS_BY_REGION_NAME[next_region]:
+ target = region_exit[0]
+
+ if target in reachable_regions:
+ continue
+
+ # There may be multiple conncetions between two regions. We should check all of them to see if
+ # any of them are valid.
+ for option in region_exit[1]:
+ # If a connection requires having access to a not-yet-reached region, do not consider it.
+ # Otherwise, this connection is valid, and the target region is reachable -> break for loop
+ if not any(req in self.CONNECTIONS_BY_REGION_NAME and req not in reachable_regions
+ for req in option):
+ break
+ # If none of the connections were valid, this region is not reachable this way, for now.
+ else:
+ continue
+
+ new_regions_found = True
+ regions_to_check.add(target)
+ reachable_regions.add(target)
+
+ return reachable_regions
+
+ def find_unsolvable_entities(self, world: "WitnessWorld") -> None:
"""
- Turns dependent check set into semi-independent check set
+ Settings like "shuffle_postgame: False" may disable certain panels.
+ This may make panels or regions logically locked by those panels unreachable.
+ We will determine these automatically and disable them as well.
"""
- for check_hex in self.DEPENDENT_REQUIREMENTS_BY_HEX.keys():
- indep_requirement = self.reduce_req_within_region(check_hex)
+ all_regions = set(self.CONNECTIONS_BY_REGION_NAME_THEORETICAL)
+
+ while True:
+ # Re-make the dependency reduced entity requirements dict, which depends on currently
+ self.make_dependency_reduced_checklist()
+
+ # Check if any regions have become unreachable.
+ reachable_regions = self.discover_reachable_regions()
+ new_unreachable_regions = all_regions - reachable_regions - self.UNREACHABLE_REGIONS
+ if new_unreachable_regions:
+ self.UNREACHABLE_REGIONS.update(new_unreachable_regions)
+
+ # Then, discover unreachable entities.
+ newly_discovered_disabled_entities = set()
+
+ # First, entities in unreachable regions are obviously themselves unreachable.
+ for region in new_unreachable_regions:
+ for entity in static_witness_logic.ALL_REGIONS_BY_NAME[region]["physical_entities"]:
+ # Never disable the Victory Location.
+ if entity == self.VICTORY_LOCATION:
+ continue
+
+ # Never disable a laser (They should still function even if you can't walk up to them).
+ if static_witness_logic.ENTITIES_BY_HEX[entity]["entityType"] == "Laser":
+ continue
+
+ newly_discovered_disabled_entities.add(entity)
+
+ # Secondly, any entities that depend on disabled entities are unreachable as well.
+ for entity, req in self.REQUIREMENTS_BY_HEX.items():
+ # If the requirement is empty (unsolvable) and it isn't disabled already, add it to "newly disabled"
+ if not req and not self.is_disabled(entity):
+ # Never disable the Victory Location.
+ if entity == self.VICTORY_LOCATION:
+ continue
+
+ # If we are disabling a laser, something has gone wrong.
+ if static_witness_logic.ENTITIES_BY_HEX[entity]["entityType"] == "Laser":
+ laser_name = static_witness_logic.ENTITIES_BY_HEX[entity]["checkName"]
+ player_name = world.multiworld.get_player_name(world.player)
+ raise RuntimeError(f"Somehow, {laser_name} was disabled for player {player_name}."
+ f" This is not allowed to happen, please report to Violet.")
+
+ newly_discovered_disabled_entities.add(entity)
+
+ # Disable the newly determined unreachable entities.
+ self.COMPLETELY_DISABLED_ENTITIES.update(newly_discovered_disabled_entities)
+
+ # If we didn't find any new unreachable regions or entities this cycle, we are done.
+ # If we did, we need to do another cycle to see if even more regions or entities became unreachable.
+ if not new_unreachable_regions and not newly_discovered_disabled_entities:
+ return
+
+ def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> WitnessRule:
+ all_possibilities = []
+
+ # Check each traversal option individually
+ for option in connection[1]:
+ individual_entity_requirements: List[WitnessRule] = []
+ for entity in option:
+ # If a connection requires solving a disabled entity, it is not valid.
+ if not self.solvability_guaranteed(entity) or entity in self.DISABLE_EVERYTHING_BEHIND:
+ individual_entity_requirements.append(frozenset())
+ # If a connection requires acquiring an event, add that event to its requirements.
+ elif (entity in self.ALWAYS_EVENT_NAMES_BY_HEX
+ or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX):
+ individual_entity_requirements.append(frozenset({frozenset({entity})}))
+ # If a connection requires entities, use their newly calculated independent requirements.
+ else:
+ entity_req = self.get_entity_requirement(entity)
+
+ if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]:
+ region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]["name"]
+ entity_req = logical_and_witness_rules([entity_req, frozenset({frozenset({region_name})})])
+
+ individual_entity_requirements.append(entity_req)
+
+ # Merge all possible requirements into one DNF condition.
+ all_possibilities.append(logical_and_witness_rules(individual_entity_requirements))
+
+ return logical_or_witness_rules(all_possibilities)
+
+ def make_dependency_reduced_checklist(self) -> None:
+ """
+ Every entity has a requirement. This requirement may involve other entities.
+ Example: Solving a panel powers a cable, and that cable turns on the next panel.
+ These dependencies are specified in the logic files (e.g. "WitnessLogic.txt") and may be modified by options.
+
+ Recursively having to check the requirements of every dependent entity would be very slow, so we go through this
+ recursion once and make a single, independent requirement for each entity.
+
+ This requirement may include symbol items, door items, regions, or events.
+ A requirement is saved as a two-dimensional set that represents a disjuntive normal form.
+ """
+
+ # Requirements are cached per entity. However, we might redo the whole reduction process multiple times.
+ # So, we first clear this cache.
+ self.REQUIREMENTS_BY_HEX = {}
+
+ # We also clear any data structures that we might have filled in a previous dependency reduction
+ self.REQUIREMENTS_BY_HEX = {}
+ self.USED_EVENT_NAMES_BY_HEX = {}
+ self.CONNECTIONS_BY_REGION_NAME = {}
+ self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set()
+
+ # Make independent requirements for entities
+ for entity_hex in self.DEPENDENT_REQUIREMENTS_BY_HEX.keys():
+ indep_requirement = self.get_entity_requirement(entity_hex)
+
+ self.REQUIREMENTS_BY_HEX[entity_hex] = indep_requirement
- self.REQUIREMENTS_BY_HEX[check_hex] = indep_requirement
+ # Make independent region connection requirements based on the entities they require
+ for region, connections in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL.items():
+ new_connections = set()
+ for connection in connections:
+ overall_requirement = self.reduce_connection_requirement(connection)
+
+ # If there is a way to use this connection, add it.
+ if overall_requirement:
+ new_connections.add((connection[0], overall_requirement))
+
+ self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
+
+ def finalize_items(self) -> None:
+ """
+ Finalise which items are used in the world, and handle their progressive versions.
+ """
for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI:
if item not in self.THEORETICAL_ITEMS:
- progressive_item_name = StaticWitnessLogic.get_parent_progressive_item(item)
+ progressive_item_name = static_witness_logic.get_parent_progressive_item(item)
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name)
child_items = cast(ProgressiveItemDefinition,
- StaticWitnessLogic.all_items[progressive_item_name]).child_item_names
+ static_witness_logic.ALL_ITEMS[progressive_item_name]).child_item_names
multi_list = [child_item for child_item in child_items
if child_item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI]
self.MULTI_AMOUNTS[item] = multi_list.index(item) + 1
@@ -360,187 +818,103 @@ def make_dependency_reduced_checklist(self):
else:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item)
- def make_event_item_pair(self, panel):
+ def solvability_guaranteed(self, entity_hex: str) -> bool:
+ return not (
+ entity_hex in self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY
+ or entity_hex in self.COMPLETELY_DISABLED_ENTITIES
+ or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES
+ )
+
+ def is_disabled(self, entity_hex: str) -> bool:
+ return (
+ entity_hex in self.COMPLETELY_DISABLED_ENTITIES
+ or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES
+ )
+
+ def determine_unrequired_entities(self, world: "WitnessWorld") -> None:
+ """Figure out which major items are actually useless in this world's settings"""
+
+ # Gather quick references to relevant options
+ eps_shuffled = world.options.shuffle_EPs
+ come_to_you = world.options.elevators_come_to_you
+ difficulty = world.options.puzzle_randomization
+ discards_shuffled = world.options.shuffle_discarded_panels
+ boat_shuffled = world.options.shuffle_boat
+ symbols_shuffled = world.options.shuffle_symbols
+ disable_non_randomized = world.options.disable_non_randomized_puzzles
+ postgame_included = world.options.shuffle_postgame
+ goal = world.options.victory_condition
+ doors = world.options.shuffle_doors
+ shortbox_req = world.options.mountain_lasers
+ longbox_req = world.options.challenge_lasers
+
+ # Make some helper booleans so it is easier to follow what's going on
+ mountain_upper_is_in_postgame = (
+ goal == "mountain_box_short"
+ or goal == "mountain_box_long" and longbox_req <= shortbox_req
+ )
+ mountain_upper_included = postgame_included or not mountain_upper_is_in_postgame
+ remote_doors = doors >= 2
+ door_panels = doors == "panels" or doors == "mixed"
+
+ # It is easier to think about when these items *are* required, so we make that dict first
+ # If the entity is disabled anyway, we don't need to consider that case
+ is_item_required_dict = {
+ "0x03750": eps_shuffled, # Monastery Garden Entry Door
+ "0x275FA": eps_shuffled, # Boathouse Hook Control
+ "0x17D02": eps_shuffled, # Windmill Turn Control
+ "0x0368A": symbols_shuffled or door_panels, # Quarry Stoneworks Stairs Door
+ "0x3865F": symbols_shuffled or door_panels or eps_shuffled, # Quarry Boathouse 2nd Barrier
+ "0x17CC4": come_to_you or eps_shuffled, # Quarry Elevator Panel
+ "0x17E2B": come_to_you and boat_shuffled or eps_shuffled, # Swamp Long Bridge
+ "0x0CF2A": False, # Jungle Monastery Garden Shortcut
+ "0x17CAA": remote_doors, # Jungle Monastery Garden Shortcut Panel
+ "0x0364E": False, # Monastery Laser Shortcut Door
+ "0x03713": remote_doors, # Monastery Laser Shortcut Panel
+ "0x03313": False, # Orchard Second Gate
+ "0x337FA": remote_doors, # Jungle Bamboo Laser Shortcut Panel
+ "0x3873B": False, # Jungle Bamboo Laser Shortcut Door
+ "0x335AB": False, # Caves Elevator Controls
+ "0x335AC": False, # Caves Elevator Controls
+ "0x3369D": False, # Caves Elevator Controls
+ "0x01BEA": difficulty == "none" and eps_shuffled, # Keep PP2
+ "0x0A0C9": eps_shuffled or discards_shuffled or disable_non_randomized, # Cargo Box Entry Door
+ "0x09EEB": discards_shuffled or mountain_upper_included, # Mountain Floor 2 Elevator Control Panel
+ "0x17CAB": symbols_shuffled or not disable_non_randomized or "0x17CAB" not in self.DOOR_ITEMS_BY_ID,
+ # Jungle Popup Wall Panel
+ }
+
+ # Now, return the keys of the dict entries where the result is False to get unrequired major items
+ self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY |= {
+ item_name for item_name, is_required in is_item_required_dict.items() if not is_required
+ }
+
+ def make_event_item_pair(self, entity_hex: str) -> Tuple[str, str]:
"""
Makes a pair of an event panel and its event item
"""
- action = " Opened" if StaticWitnessLogic.CHECKS_BY_HEX[panel]["panelType"] == "Door" else " Solved"
-
- name = StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] + action
- if panel not in self.EVENT_ITEM_NAMES:
- if StaticWitnessLogic.CHECKS_BY_HEX[panel]["panelType"] == "EP":
- obelisk = StaticWitnessLogic.CHECKS_BY_HEX[StaticWitnessLogic.EP_TO_OBELISK_SIDE[panel]]["checkName"]
-
- self.EVENT_ITEM_NAMES[panel] = obelisk + " - " + StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"]
+ action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]["entityType"] == "Door" else " Solved"
- else:
- warning("Panel \"" + name + "\" does not have an associated event name.")
- self.EVENT_ITEM_NAMES[panel] = name + " Event"
- pair = (name, self.EVENT_ITEM_NAMES[panel])
- return pair
+ name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]["checkName"] + action
+ if entity_hex not in self.USED_EVENT_NAMES_BY_HEX:
+ warning(f'Entity "{name}" does not have an associated event name.')
+ self.USED_EVENT_NAMES_BY_HEX[entity_hex] = name + " Event"
+ return (name, self.USED_EVENT_NAMES_BY_HEX[entity_hex])
- def make_event_panel_lists(self):
+ def make_event_panel_lists(self) -> None:
"""
- Special event panel data structures
+ Makes event-item pairs for entities with associated events, unless these entities are disabled.
"""
self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory"
- for region_name, connections in self.CONNECTIONS_BY_REGION_NAME.items():
- for connection in connections:
- for panel_req in connection[1]:
- for panel in panel_req:
- if panel == "TrueOneWay":
- continue
-
- if self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel]["region"]["name"] != region_name:
- self.EVENT_PANELS_FROM_REGIONS.add(panel)
+ self.USED_EVENT_NAMES_BY_HEX.update(self.ALWAYS_EVENT_NAMES_BY_HEX)
- self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_PANELS)
- self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_REGIONS)
-
- for always_hex, always_item in self.ALWAYS_EVENT_NAMES_BY_HEX.items():
- self.ALWAYS_EVENT_HEX_CODES.add(always_hex)
- self.EVENT_PANELS.add(always_hex)
- self.EVENT_ITEM_NAMES[always_hex] = always_item
+ self.USED_EVENT_NAMES_BY_HEX = {
+ event_hex: event_name for event_hex, event_name in self.USED_EVENT_NAMES_BY_HEX.items()
+ if self.solvability_guaranteed(event_hex)
+ }
- for panel in self.EVENT_PANELS:
+ for panel in self.USED_EVENT_NAMES_BY_HEX:
pair = self.make_event_item_pair(panel)
self.EVENT_ITEM_PAIRS[pair[0]] = pair[1]
-
- def __init__(self, world: MultiWorld, player: int, disabled_locations: Set[str], start_inv: Dict[str, int]):
- self.YAML_DISABLED_LOCATIONS = disabled_locations
- self.YAML_ADDED_ITEMS = start_inv
-
- self.EVENT_PANELS_FROM_PANELS = set()
- self.EVENT_PANELS_FROM_REGIONS = set()
-
- self.THEORETICAL_ITEMS = set()
- self.THEORETICAL_ITEMS_NO_MULTI = set()
- self.MULTI_AMOUNTS = dict()
- self.MULTI_LISTS = dict()
- self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set()
- self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set()
- self.DOOR_ITEMS_BY_ID: Dict[str, List[int]] = {}
- self.STARTING_INVENTORY = set()
-
- self.DIFFICULTY = get_option_value(world, player, "puzzle_randomization")
-
- if self.DIFFICULTY == 0:
- self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_normal
- elif self.DIFFICULTY == 1:
- self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_expert
- elif self.DIFFICULTY == 2:
- self.REFERENCE_LOGIC = StaticWitnessLogic.vanilla
-
- self.CONNECTIONS_BY_REGION_NAME = copy.copy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME)
- self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.copy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX)
- self.REQUIREMENTS_BY_HEX = dict()
-
- # Determining which panels need to be events is a difficult process.
- # At the end, we will have EVENT_ITEM_PAIRS for all the necessary ones.
- self.EVENT_PANELS = set()
- self.EVENT_ITEM_PAIRS = dict()
- self.ALWAYS_EVENT_HEX_CODES = set()
- self.COMPLETELY_DISABLED_CHECKS = set()
- self.PRECOMPLETED_LOCATIONS = set()
- self.EXCLUDED_LOCATIONS = set()
- self.ADDED_CHECKS = set()
- self.VICTORY_LOCATION = "0x0356B"
- self.EVENT_ITEM_NAMES = {
- "0x09D9B": "Monastery Shutters Open",
- "0x193A6": "Monastery Laser Panel Activates",
- "0x00037": "Monastery Branch Panels Activate",
- "0x0A079": "Access to Bunker Laser",
- "0x0A3B5": "Door to Tutorial Discard Opens",
- "0x00139": "Keep Hedges 1 Knowledge",
- "0x019DC": "Keep Hedges 2 Knowledge",
- "0x019E7": "Keep Hedges 3 Knowledge",
- "0x01A0F": "Keep Hedges 4 Knowledge",
- "0x033EA": "Pressure Plates 1 Knowledge",
- "0x01BE9": "Pressure Plates 2 Knowledge",
- "0x01CD3": "Pressure Plates 3 Knowledge",
- "0x01D3F": "Pressure Plates 4 Knowledge",
- "0x09F7F": "Mountain Access",
- "0x0367C": "Quarry Laser Stoneworks Requirement Met",
- "0x009A1": "Swamp Between Bridges Far 1 Activates",
- "0x00006": "Swamp Cyan Water Drains",
- "0x00990": "Swamp Between Bridges Near Row 1 Activates",
- "0x0A8DC": "Intro 6 Activates",
- "0x0000A": "Swamp Beyond Rotating Bridge 1 Access",
- "0x09E86": "Mountain Floor 2 Blue Bridge Access",
- "0x09ED8": "Mountain Floor 2 Yellow Bridge Access",
- "0x0A3D0": "Quarry Laser Boathouse Requirement Met",
- "0x00596": "Swamp Red Water Drains",
- "0x00E3A": "Swamp Purple Water Drains",
- "0x0343A": "Door to Symmetry Island Powers On",
- "0xFFF00": "Mountain Bottom Floor Discard Turns On",
- "0x17CA6": "All Boat Panels Turn On",
- "0x17CDF": "All Boat Panels Turn On",
- "0x09DB8": "All Boat Panels Turn On",
- "0x17C95": "All Boat Panels Turn On",
- "0x0A054": "Couch EP solvable",
- "0x03BB0": "Town Church Lattice Vision From Outside",
- "0x28AC1": "Town Wooden Rooftop Turns On",
- "0x28A69": "Town Tower 1st Door Opens",
- "0x28ACC": "Town Tower 2nd Door Opens",
- "0x28AD9": "Town Tower 3rd Door Opens",
- "0x28B39": "Town Tower 4th Door Opens",
- "0x03675": "Quarry Stoneworks Ramp Activation From Above",
- "0x03679": "Quarry Stoneworks Lift Lowering While Standing On It",
- "0x2FAF6": "Tutorial Gate Secret Solution Knowledge",
- "0x079DF": "Town Tall Hexagonal Turns On",
- "0x17DA2": "Right Orange Bridge Fully Extended",
- "0x19B24": "Shadows Intro Patterns Visible",
- "0x2700B": "Open Door to Treehouse Laser House",
- "0x00055": "Orchard Apple Trees 4 Turns On",
- "0x17DDB": "Left Orange Bridge Fully Extended",
- "0x03535": "Shipwreck Video Pattern Knowledge",
- "0x03542": "Mountain Video Pattern Knowledge",
- "0x0339E": "Desert Video Pattern Knowledge",
- "0x03481": "Tutorial Video Pattern Knowledge",
- "0x03702": "Jungle Video Pattern Knowledge",
- "0x0356B": "Challenge Video Pattern Knowledge",
- "0x0A15F": "Desert Laser Panel Shutters Open (1)",
- "0x012D7": "Desert Laser Panel Shutters Open (2)",
- "0x03613": "Treehouse Orange Bridge 13 Turns On",
- "0x17DEC": "Treehouse Laser House Access Requirement",
- "0x03C08": "Town Church Entry Opens",
- "0x17D02": "Windmill Blades Spinning",
- "0x0A0C9": "Cargo Box EP completable",
- "0x09E39": "Pink Light Bridge Extended",
- "0x17CC4": "Rails EP available",
- "0x2896A": "Bridge Underside EP available",
- "0x00064": "First Tunnel EP visible",
- "0x03553": "Tutorial Video EPs availble",
- "0x17C79": "Bunker Door EP available",
- "0x275FF": "Stoneworks Light EPs available",
- "0x17E2B": "Remaining Purple Sand EPs available",
- "0x03852": "Ramp EPs requirement",
- "0x334D8": "RGB panels & EPs solvable",
- "0x03750": "Left Garden EP available",
- "0x03C0C": "RGB Flowers EP requirement",
- "0x01CD5": "Pressure Plates 3 EP requirement",
- "0x3865F": "Ramp EPs access requirement",
- }
-
- self.ALWAYS_EVENT_NAMES_BY_HEX = {
- "0x00509": "Symmetry Laser Activation",
- "0x012FB": "Desert Laser Activation",
- "0x09F98": "Desert Laser Redirection",
- "0x01539": "Quarry Laser Activation",
- "0x181B3": "Shadows Laser Activation",
- "0x014BB": "Keep Laser Activation",
- "0x17C65": "Monastery Laser Activation",
- "0x032F9": "Town Laser Activation",
- "0x00274": "Jungle Laser Activation",
- "0x0C2B2": "Bunker Laser Activation",
- "0x00BF6": "Swamp Laser Activation",
- "0x028A4": "Treehouse Laser Activation",
- "0x09F7F": "Mountaintop Trap Door Turns On",
- "0x17C34": "Mountain Access",
- }
-
- self.make_options_adjustments(world, player)
- self.make_dependency_reduced_checklist()
- self.make_event_panel_lists()
diff --git a/worlds/witness/presets.py b/worlds/witness/presets.py
new file mode 100644
index 000000000000..2a53484a4c77
--- /dev/null
+++ b/worlds/witness/presets.py
@@ -0,0 +1,113 @@
+from typing import Any, Dict
+
+from .options import *
+
+witness_option_presets: Dict[str, Dict[str, Any]] = {
+ # Great for short syncs & scratching that "speedrun with light routing elements" itch.
+ "Short & Dense": {
+ "progression_balancing": 30,
+
+ "puzzle_randomization": PuzzleRandomization.option_sigma_normal,
+
+ "shuffle_symbols": False,
+ "shuffle_doors": ShuffleDoors.option_panels,
+ "door_groupings": DoorGroupings.option_off,
+ "shuffle_boat": True,
+ "shuffle_lasers": ShuffleLasers.option_local,
+ "obelisk_keys": ObeliskKeys.option_false,
+
+ "disable_non_randomized_puzzles": True,
+ "shuffle_discarded_panels": False,
+ "shuffle_vault_boxes": False,
+ "shuffle_EPs": ShuffleEnvironmentalPuzzles.option_off,
+ "EP_difficulty": EnvironmentalPuzzlesDifficulty.option_normal,
+ "shuffle_postgame": False,
+
+ "victory_condition": VictoryCondition.option_mountain_box_short,
+ "mountain_lasers": 7,
+ "challenge_lasers": 11,
+
+ "early_caves": EarlyCaves.option_off,
+ "elevators_come_to_you": False,
+
+ "trap_percentage": TrapPercentage.default,
+ "puzzle_skip_amount": PuzzleSkipAmount.default,
+ "hint_amount": HintAmount.default,
+ "area_hint_percentage": AreaHintPercentage.default,
+ "laser_hints": LaserHints.default,
+ "death_link": DeathLink.default,
+ "death_link_amnesty": DeathLinkAmnesty.default,
+ },
+
+ # For relative beginners who want to move to the next step.
+ "Advanced, But Chill": {
+ "progression_balancing": 30,
+
+ "puzzle_randomization": PuzzleRandomization.option_sigma_normal,
+
+ "shuffle_symbols": True,
+ "shuffle_doors": ShuffleDoors.option_doors,
+ "door_groupings": DoorGroupings.option_regional,
+ "shuffle_boat": True,
+ "shuffle_lasers": ShuffleLasers.option_off,
+ "obelisk_keys": ObeliskKeys.option_false,
+
+ "disable_non_randomized_puzzles": False,
+ "shuffle_discarded_panels": True,
+ "shuffle_vault_boxes": True,
+ "shuffle_EPs": ShuffleEnvironmentalPuzzles.option_obelisk_sides,
+ "EP_difficulty": EnvironmentalPuzzlesDifficulty.option_normal,
+ "shuffle_postgame": False,
+
+ "victory_condition": VictoryCondition.option_mountain_box_long,
+ "mountain_lasers": 6,
+ "challenge_lasers": 9,
+
+ "early_caves": EarlyCaves.option_off,
+ "elevators_come_to_you": False,
+
+ "trap_percentage": TrapPercentage.default,
+ "puzzle_skip_amount": 15,
+ "hint_amount": HintAmount.default,
+ "area_hint_percentage": AreaHintPercentage.default,
+ "laser_hints": LaserHints.default,
+ "death_link": DeathLink.default,
+ "death_link_amnesty": DeathLinkAmnesty.default,
+ },
+
+ # Allsanity but without the BS (no expert, no tedious EPs).
+ "Nice Allsanity": {
+ "progression_balancing": 50,
+
+ "puzzle_randomization": PuzzleRandomization.option_sigma_normal,
+
+ "shuffle_symbols": True,
+ "shuffle_doors": ShuffleDoors.option_mixed,
+ "door_groupings": DoorGroupings.option_off,
+ "shuffle_boat": True,
+ "shuffle_lasers": ShuffleLasers.option_anywhere,
+ "obelisk_keys": ObeliskKeys.option_true,
+
+ "disable_non_randomized_puzzles": False,
+ "shuffle_discarded_panels": True,
+ "shuffle_vault_boxes": True,
+ "shuffle_EPs": ShuffleEnvironmentalPuzzles.option_individual,
+ "EP_difficulty": EnvironmentalPuzzlesDifficulty.option_normal,
+ "shuffle_postgame": False,
+
+ "victory_condition": VictoryCondition.option_challenge,
+ "mountain_lasers": 6,
+ "challenge_lasers": 9,
+
+ "early_caves": EarlyCaves.option_off,
+ "elevators_come_to_you": True,
+
+ "trap_percentage": TrapPercentage.default,
+ "puzzle_skip_amount": 15,
+ "hint_amount": HintAmount.default,
+ "area_hint_percentage": AreaHintPercentage.default,
+ "laser_hints": LaserHints.default,
+ "death_link": DeathLink.default,
+ "death_link_amnesty": DeathLinkAmnesty.default,
+ },
+}
diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py
index 0e15cafe10fd..2528c8abe22b 100644
--- a/worlds/witness/regions.py
+++ b/worlds/witness/regions.py
@@ -2,112 +2,136 @@
Defines Region for The Witness, assigns locations to them,
and connects them with the proper requirements
"""
+from collections import defaultdict
+from typing import TYPE_CHECKING, Dict, List, Set, Tuple
-from BaseClasses import MultiWorld, Entrance
-from .static_logic import StaticWitnessLogic
-from .Options import get_option_value
-from .locations import WitnessPlayerLocations, StaticWitnessLocations
+from BaseClasses import Entrance, Region
+
+from worlds.generic.Rules import CollectionRule
+
+from .data import static_locations as static_witness_locations
+from .data import static_logic as static_witness_logic
+from .data.static_logic import StaticWitnessLogicObj
+from .data.utils import WitnessRule, optimize_witness_rule
+from .locations import WitnessPlayerLocations
from .player_logic import WitnessPlayerLogic
+if TYPE_CHECKING:
+ from . import WitnessWorld
-class WitnessRegions:
+
+class WitnessPlayerRegions:
"""Class that defines Witness Regions"""
- locat = None
- logic = None
+ def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None:
+ difficulty = world.options.puzzle_randomization
+
+ self.reference_logic: StaticWitnessLogicObj
+ if difficulty == "sigma_normal":
+ self.reference_logic = static_witness_logic.sigma_normal
+ elif difficulty == "sigma_expert":
+ self.reference_logic = static_witness_logic.sigma_expert
+ else:
+ self.reference_logic = static_witness_logic.vanilla
+
+ self.player_locations = player_locations
+ self.two_way_entrance_register: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: [])
+ self.created_region_names: Set[str] = set()
+
+ @staticmethod
+ def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> CollectionRule:
+ from .rules import _meets_item_requirements
- def make_lambda(self, panel_hex_to_solve_set, world, player, player_logic):
"""
Lambdas are made in a for loop, so the values have to be captured
This function is for that purpose
"""
- return lambda state: state._witness_can_solve_panels(
- panel_hex_to_solve_set, world, player, player_logic, self.locat
- )
+ return _meets_item_requirements(item_requirement, world)
- def connect(self, world: MultiWorld, player: int, source: str, target: str, player_logic: WitnessPlayerLogic,
- panel_hex_to_solve_set=frozenset({frozenset()}), backwards: bool = False):
+ def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, req: WitnessRule,
+ regions_by_name: Dict[str, Region]) -> None:
"""
connect two regions and set the corresponding requirement
"""
- source_region = world.get_region(source, player)
- target_region = world.get_region(target, player)
- backwards = " Backwards" if backwards else ""
+ # Remove any possibilities where being in the target region would be required anyway.
+ real_requirement = frozenset({option for option in req if target not in option})
+
+ # Dissolve any "True" or "TrueOneWay"
+ real_requirement = frozenset({option - {"True", "TrueOneWay"} for option in real_requirement})
+
+ # If there is no way to actually use this connection, don't even bother making it.
+ if not real_requirement:
+ return
+
+ # We don't need to check for the accessibility of the source region.
+ final_requirement = frozenset({option - frozenset({source}) for option in real_requirement})
+ final_requirement = optimize_witness_rule(final_requirement)
+
+ source_region = regions_by_name[source]
+ target_region = regions_by_name[target]
+
+ connection_name = source + " to " + target
connection = Entrance(
- player,
- source + " to " + target + backwards,
+ world.player,
+ connection_name,
source_region
)
- connection.access_rule = self.make_lambda(panel_hex_to_solve_set, world, player, player_logic)
+ connection.access_rule = self.make_lambda(final_requirement, world)
source_region.exits.append(connection)
connection.connect(target_region)
- def create_regions(self, world, player: int, player_logic: WitnessPlayerLogic):
+ self.two_way_entrance_register[source, target].append(connection)
+ self.two_way_entrance_register[target, source].append(connection)
+
+ # Register any necessary indirect connections
+ mentioned_regions = {
+ single_unlock for option in final_requirement for single_unlock in option
+ if single_unlock in self.reference_logic.ALL_REGIONS_BY_NAME
+ }
+
+ for dependent_region in mentioned_regions:
+ world.multiworld.register_indirect_condition(regions_by_name[dependent_region], connection)
+
+ def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> None:
"""
Creates all the regions for The Witness
"""
from . import create_region
- world.regions += [
- create_region(world, player, 'Menu', self.locat, None, ["The Splashscreen?"]),
- ]
+ all_locations: Set[str] = set()
+ regions_by_name: Dict[str, Region] = {}
- difficulty = get_option_value(world, player, "puzzle_randomization")
+ regions_to_create = {
+ k: v for k, v in self.reference_logic.ALL_REGIONS_BY_NAME.items()
+ if k not in player_logic.UNREACHABLE_REGIONS
+ }
- if difficulty == 1:
- reference_logic = StaticWitnessLogic.sigma_expert
- elif difficulty == 0:
- reference_logic = StaticWitnessLogic.sigma_normal
- else:
- reference_logic = StaticWitnessLogic.vanilla
-
- all_locations = set()
-
- for region_name, region in reference_logic.ALL_REGIONS_BY_NAME.items():
+ for region_name, region in regions_to_create.items():
locations_for_this_region = [
- reference_logic.CHECKS_BY_HEX[panel]["checkName"] for panel in region["panels"]
- if reference_logic.CHECKS_BY_HEX[panel]["checkName"] in self.locat.CHECK_LOCATION_TABLE
+ self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] for panel in region["entities"]
+ if self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"]
+ in self.player_locations.CHECK_LOCATION_TABLE
]
locations_for_this_region += [
- StaticWitnessLocations.get_event_name(panel) for panel in region["panels"]
- if StaticWitnessLocations.get_event_name(panel) in self.locat.EVENT_LOCATION_TABLE
+ static_witness_locations.get_event_name(panel) for panel in region["entities"]
+ if static_witness_locations.get_event_name(panel) in self.player_locations.EVENT_LOCATION_TABLE
]
all_locations = all_locations | set(locations_for_this_region)
- world.regions += [
- create_region(world, player, region_name, self.locat, locations_for_this_region)
- ]
+ new_region = create_region(world, region_name, self.player_locations, locations_for_this_region)
- for region_name, region in reference_logic.ALL_REGIONS_BY_NAME.items():
- for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]:
- if connection[1] == frozenset({frozenset(["TrueOneWay"])}):
- self.connect(world, player, region_name, connection[0], player_logic, frozenset({frozenset()}))
- continue
-
- backwards_connections = set()
-
- for subset in connection[1]:
- if all({panel in player_logic.DOOR_ITEMS_BY_ID for panel in subset}):
- if all({reference_logic.CHECKS_BY_HEX[panel]["id"] is None for panel in subset}):
- backwards_connections.add(subset)
+ regions_by_name[region_name] = new_region
- if backwards_connections:
- self.connect(
- world, player, connection[0], region_name, player_logic,
- frozenset(backwards_connections), True
- )
+ self.created_region_names = set(regions_by_name)
- self.connect(world, player, region_name, connection[0], player_logic, connection[1])
+ world.multiworld.regions += regions_by_name.values()
- world.get_entrance("The Splashscreen?", player).connect(
- world.get_region('First Hallway', player)
- )
-
- def __init__(self, locat: WitnessPlayerLocations):
- self.locat = locat
+ for region_name, region in regions_to_create.items():
+ for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]:
+ self.connect_if_possible(world, region_name, connection[0], connection[1], regions_by_name)
diff --git a/worlds/witness/ruff.toml b/worlds/witness/ruff.toml
new file mode 100644
index 000000000000..a35711cce66d
--- /dev/null
+++ b/worlds/witness/ruff.toml
@@ -0,0 +1,11 @@
+line-length = 120
+
+[lint]
+select = ["C", "E", "F", "R", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"]
+ignore = ["C9", "RUF012", "RUF100"]
+
+[lint.per-file-ignores]
+# The way options definitions work right now, I am forced to break line length requirements.
+"options.py" = ["E501"]
+# The import list would just be so big if I imported every option individually in presets.py
+"presets.py" = ["F403", "F405"]
diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py
index 4cf3054af6fd..12a9a1ed4b59 100644
--- a/worlds/witness/rules.py
+++ b/worlds/witness/rules.py
@@ -2,224 +2,292 @@
Defines the rules by which locations can be accessed,
depending on the items received
"""
+from typing import TYPE_CHECKING
-# pylint: disable=E1101
+from BaseClasses import CollectionState
-from BaseClasses import MultiWorld
-from .player_logic import WitnessPlayerLogic
-from .Options import is_option_enabled, get_option_value
+from worlds.generic.Rules import CollectionRule, set_rule
+
+from .data import static_logic as static_witness_logic
+from .data.utils import WitnessRule
from .locations import WitnessPlayerLocations
-from . import StaticWitnessLogic
-from worlds.AutoWorld import LogicMixin
-from worlds.generic.Rules import set_rule
+from .player_logic import WitnessPlayerLogic
+
+if TYPE_CHECKING:
+ from . import WitnessWorld
+
+laser_hexes = [
+ "0x028A4",
+ "0x00274",
+ "0x032F9",
+ "0x01539",
+ "0x181B3",
+ "0x0C2B2",
+ "0x00509",
+ "0x00BF6",
+ "0x014BB",
+ "0x012FB",
+ "0x17C65",
+]
+
+
+def _has_laser(laser_hex: str, world: "WitnessWorld", player: int, redirect_required: bool) -> CollectionRule:
+ if laser_hex == "0x012FB" and redirect_required:
+ return lambda state: (
+ _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)(state)
+ and state.has("Desert Laser Redirection", player)
+ )
+
+ return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)
+
+
+def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> CollectionRule:
+ laser_lambdas = []
+
+ for laser_hex in laser_hexes:
+ has_laser_lambda = _has_laser(laser_hex, world, world.player, redirect_required)
+
+ laser_lambdas.append(has_laser_lambda)
+
+ return lambda state: sum(laser_lambda(state) for laser_lambda in laser_lambdas) >= amount
+
+
+def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic,
+ player_locations: WitnessPlayerLocations) -> CollectionRule:
+ """
+ Determines whether a panel can be solved
+ """
+
+ panel_obj = player_logic.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]
+ entity_name = panel_obj["checkName"]
+
+ if entity_name + " Solved" in player_locations.EVENT_LOCATION_TABLE:
+ return lambda state: state.has(player_logic.EVENT_ITEM_PAIRS[entity_name + " Solved"], player)
+ return make_lambda(panel, world)
-class WitnessLogic(LogicMixin):
+
+def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool:
"""
- Logic macros that get reused
+ For Expert PP2, you need a way to access PP2 from the front, and a separate way from the back.
+ This condition is quite complicated. We'll attempt to evaluate it as lazily as possible.
"""
- def _witness_has_lasers(self, world, player: int, amount: int) -> bool:
- regular_lasers = not is_option_enabled(world, player, "shuffle_lasers")
+ player = world.player
+ player_regions = world.player_regions
+
+ front_access = (
+ any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 2nd Pressure Plate", "Keep"])
+ and state.can_reach_region("Keep", player)
+ )
+
+ # If we don't have front access, we can't do PP2.
+ if not front_access:
+ return False
- lasers = 0
+ # Front access works. Now, we need to check for the many ways to access PP2 from the back.
+ # All of those ways lead through the PP3 exit door from PP4. So we check this first.
- place_names = [
- "Symmetry", "Desert", "Town", "Monastery", "Keep",
- "Quarry", "Treehouse", "Jungle", "Bunker", "Swamp", "Shadows"
- ]
+ fourth_to_third = any(e.can_reach(state) for e in player_regions.two_way_entrance_register[
+ "Keep 3rd Pressure Plate", "Keep 4th Pressure Plate"
+ ])
- for place in place_names:
- has_laser = self.has(place + " Laser", player)
+ # If we can't get from PP4 to PP3, we can't do PP2.
+ if not fourth_to_third:
+ return False
- has_laser = has_laser or (regular_lasers and self.has(place + " Laser Activation", player))
+ # We can go from PP4 to PP3. We now need to find a way to PP4.
+ # The shadows shortcut is the simplest way.
- if place == "Desert":
- has_laser = has_laser and self.has("Desert Laser Redirection", player)
+ shadows_shortcut = (
+ any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Pressure Plate", "Shadows"])
+ )
- lasers += int(has_laser)
+ if shadows_shortcut:
+ return True
- return lasers >= amount
+ # We don't have the Shadows shortcut. This means we need to come in through the PP4 exit door instead.
- def _witness_can_solve_panel(self, panel, world, player, player_logic: WitnessPlayerLogic, locat):
- """
- Determines whether a panel can be solved
- """
+ tower_to_pp4 = any(
+ e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Pressure Plate", "Keep Tower"]
+ )
- panel_obj = StaticWitnessLogic.CHECKS_BY_HEX[panel]
- check_name = panel_obj["checkName"]
+ # If we don't have the PP4 exit door, we've run out of options.
+ if not tower_to_pp4:
+ return False
- if (check_name + " Solved" in locat.EVENT_LOCATION_TABLE
- and not self.has(player_logic.EVENT_ITEM_PAIRS[check_name + " Solved"], player)):
- return False
- if (check_name + " Solved" not in locat.EVENT_LOCATION_TABLE
- and not self._witness_meets_item_requirements(panel, world, player, player_logic, locat)):
- return False
+ # We have the PP4 exit door. If we can get to Keep Tower from behind, we can do PP2.
+ # The simplest way would be the Tower Shortcut.
+
+ tower_shortcut = any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep", "Keep Tower"])
+
+ if tower_shortcut:
return True
- def _witness_meets_item_requirements(self, panel, world, player, player_logic: WitnessPlayerLogic, locat):
- """
- Checks whether item and panel requirements are met for
- a panel
- """
-
- panel_req = player_logic.REQUIREMENTS_BY_HEX[panel]
-
- for option in panel_req:
- if len(option) == 0:
- return True
-
- valid_option = True
-
- for item in option:
- if item == "7 Lasers":
- laser_req = get_option_value(world, player, "mountain_lasers")
-
- if not self._witness_has_lasers(world, player, laser_req):
- valid_option = False
- break
- elif item == "11 Lasers":
- laser_req = get_option_value(world, player, "challenge_lasers")
-
- if not self._witness_has_lasers(world, player, laser_req):
- valid_option = False
- break
- elif item == "PP2 Weirdness":
- hedge_2_access = (
- self.can_reach("Keep 2nd Maze to Keep", "Entrance", player)
- or self.can_reach("Keep to Keep 2nd Maze", "Entrance", player)
- )
-
- hedge_3_access = (
- self.can_reach("Keep 3rd Maze to Keep", "Entrance", player)
- or self.can_reach("Keep 2nd Maze to Keep 3rd Maze", "Entrance", player)
- and hedge_2_access
- )
-
- hedge_4_access = (
- self.can_reach("Keep 4th Maze to Keep", "Entrance", player)
- or self.can_reach("Keep 3rd Maze to Keep 4th Maze", "Entrance", player)
- and hedge_3_access
- )
-
- hedge_access = (
- self.can_reach("Keep 4th Maze to Keep Tower", "Entrance", player)
- and self.can_reach("Keep", "Region", player)
- and hedge_4_access
- )
-
- backwards_to_fourth = (
- self.can_reach("Keep", "Region", player)
- and self.can_reach("Keep 4th Pressure Plate to Keep Tower", "Entrance", player)
- and (
- self.can_reach("Keep Tower to Keep", "Entrance", player)
- or hedge_access
- )
- )
-
- shadows_shortcut = (
- self.can_reach("Main Island", "Region", player)
- and self.can_reach("Keep 4th Pressure Plate to Shadows", "Entrance", player)
- )
-
- backwards_access = (
- self.can_reach("Keep 3rd Pressure Plate to Keep 4th Pressure Plate", "Entrance", player)
- and (backwards_to_fourth or shadows_shortcut)
- )
-
- front_access = (
- self.can_reach("Keep to Keep 2nd Pressure Plate", 'Entrance', player)
- and self.can_reach("Keep", "Region", player)
- )
-
- if not (front_access and backwards_access):
- valid_option = False
- break
- elif item == "Theater to Tunnels":
- direct_access = (
- self.can_reach("Tunnels to Windmill Interior", "Entrance", player)
- and self.can_reach("Windmill Interior to Theater", "Entrance", player)
- )
-
- theater_from_town = (
- self.can_reach("Town to Windmill Interior", "Entrance", player)
- and self.can_reach("Windmill Interior to Theater", "Entrance", player)
- or self.can_reach("Theater to Town", "Entrance", player)
- )
-
- tunnels_from_town = (
- self.can_reach("Tunnels to Windmill Interior", "Entrance", player)
- and self.can_reach("Town to Windmill Interior", "Entrance", player)
- or self.can_reach("Tunnels to Town", "Entrance", player)
- )
-
- if not (direct_access or theater_from_town and tunnels_from_town):
- valid_option = False
- break
- elif item in player_logic.EVENT_PANELS:
- if not self._witness_can_solve_panel(item, world, player, player_logic, locat):
- valid_option = False
- break
- elif not self.has(item, player):
- # The player doesn't have the item. Check to see if it's part of a progressive item and, if so, the
- # player has enough of that.
- prog_item = StaticWitnessLogic.get_parent_progressive_item(item)
- if prog_item is item or not self.has(prog_item, player, player_logic.MULTI_AMOUNTS[item]):
- valid_option = False
- break
-
- if valid_option:
- return True
+ # We don't have the Tower shortcut. At this point, there is one possibility remaining:
+ # Getting to Keep Tower through the hedge mazes. This can be done in a multitude of ways.
+ # No matter what, though, we would need Hedge Maze 4 Exit to Keep Tower.
+ tower_access_from_hedges = any(
+ e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Maze", "Keep Tower"]
+ )
+
+ if not tower_access_from_hedges:
return False
- def _witness_can_solve_panels(self, panel_hex_to_solve_set, world, player, player_logic: WitnessPlayerLogic, locat):
- """
- Checks whether a set of panels can be solved.
- """
+ # We can reach Keep Tower from Hedge Maze 4. If we now have the Hedge 4 Shortcut, we are immediately good.
- for option in panel_hex_to_solve_set:
- if len(option) == 0:
- return True
+ hedge_4_shortcut = any(
+ e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Maze", "Keep"]
+ )
- valid_option = True
+ # If we have the hedge 4 shortcut, that works.
+ if hedge_4_shortcut:
+ return True
+
+ # We don't have the hedge 4 shortcut. This means we would now need to come through Hedge Maze 3.
- for panel in option:
- if not self._witness_can_solve_panel(panel, world, player, player_logic, locat):
- valid_option = False
- break
+ hedge_3_to_4 = any(
+ e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Maze", "Keep 3rd Maze"]
+ )
- if valid_option:
- return True
+ if not hedge_3_to_4:
return False
+ # We can get to Hedge 4 from Hedge 3. If we have the Hedge 3 Shortcut, we're good.
-def make_lambda(check_hex, world, player, player_logic, locat):
+ hedge_3_shortcut = any(
+ e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 3rd Maze", "Keep"]
+ )
+
+ if hedge_3_shortcut:
+ return True
+
+ # We don't have Hedge 3 Shortcut. This means we would now need to come through Hedge Maze 2.
+
+ hedge_2_to_3 = any(
+ e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 3rd Maze", "Keep 2nd Maze"]
+ )
+
+ if not hedge_2_to_3:
+ return False
+
+ # We can get to Hedge 3 from Hedge 2. If we can get from Keep to Hedge 2, we're good.
+ # This covers both Hedge 1 Exit and Hedge 2 Shortcut, because Hedge 1 is just part of the Keep region.
+
+ return any(
+ e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 2nd Maze", "Keep"]
+ )
+
+
+def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> bool:
"""
- Lambdas are created in a for loop so values need to be captured
+ To do Tunnels Theater Flowers EP, you need to quickly move from Theater to Tunnels.
+ This condition is a little tricky. We'll attempt to evaluate it as lazily as possible.
"""
- return lambda state: state._witness_meets_item_requirements(
- check_hex, world, player, player_logic, locat
+
+ # Checking for access to Theater is not necessary, as solvability of Tutorial Video is checked in the other half
+ # of the Theater Flowers EP condition.
+
+ player_regions = world.player_regions
+
+ direct_access = (
+ any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Windmill Interior"])
+ and any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Theater", "Windmill Interior"])
)
+ if direct_access:
+ return True
+
+ # We don't have direct access through the shortest path.
+ # This means we somehow need to exit Theater to the Main Island, and then enter Tunnels from the Main Island.
+ # Getting to Tunnels through Mountain -> Caves -> Tunnels is way too slow, so we only expect paths through Town.
+
+ # We need a way from Theater to Town. This is actually guaranteed, otherwise we wouldn't be in Theater.
+ # The only ways to Theater are through Town and Tunnels. We just checked the Tunnels way.
+ # This might need to be changed when warps are implemented.
-def set_rules(world: MultiWorld, player: int, player_logic: WitnessPlayerLogic, locat: WitnessPlayerLocations):
+ # We also need a way from Town to Tunnels.
+
+ return (
+ any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Windmill Interior"])
+ and any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Town", "Windmill Interior"])
+ or any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Town"])
+ )
+
+
+def _has_item(item: str, world: "WitnessWorld", player: int,
+ player_logic: WitnessPlayerLogic, player_locations: WitnessPlayerLocations) -> CollectionRule:
+ if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME:
+ region = world.get_region(item)
+ return region.can_reach
+ if item == "7 Lasers":
+ laser_req = world.options.mountain_lasers.value
+ return _has_lasers(laser_req, world, False)
+ if item == "7 Lasers + Redirect":
+ laser_req = world.options.mountain_lasers.value
+ return _has_lasers(laser_req, world, True)
+ if item == "11 Lasers":
+ laser_req = world.options.challenge_lasers.value
+ return _has_lasers(laser_req, world, False)
+ if item == "11 Lasers + Redirect":
+ laser_req = world.options.challenge_lasers.value
+ return _has_lasers(laser_req, world, True)
+ if item == "PP2 Weirdness":
+ return lambda state: _can_do_expert_pp2(state, world)
+ if item == "Theater to Tunnels":
+ return lambda state: _can_do_theater_to_tunnels(state, world)
+ if item in player_logic.USED_EVENT_NAMES_BY_HEX:
+ return _can_solve_panel(item, world, player, player_logic, player_locations)
+
+ prog_item = static_witness_logic.get_parent_progressive_item(item)
+ return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item])
+
+
+def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -> CollectionRule:
+ """
+ Checks whether item and panel requirements are met for
+ a panel
+ """
+
+ lambda_conversion = [
+ [_has_item(item, world, world.player, world.player_logic, world.player_locations) for item in subset]
+ for subset in requirements
+ ]
+
+ return lambda state: any(
+ all(condition(state) for condition in sub_requirement)
+ for sub_requirement in lambda_conversion
+ )
+
+
+def make_lambda(entity_hex: str, world: "WitnessWorld") -> CollectionRule:
+ """
+ Lambdas are created in a for loop so values need to be captured
+ """
+ entity_req = world.player_logic.REQUIREMENTS_BY_HEX[entity_hex]
+
+ return _meets_item_requirements(entity_req, world)
+
+
+def set_rules(world: "WitnessWorld") -> None:
"""
Sets all rules for all locations
"""
- for location in locat.CHECK_LOCATION_TABLE:
+ for location in world.player_locations.CHECK_LOCATION_TABLE:
real_location = location
- if location in locat.EVENT_LOCATION_TABLE:
+ if location in world.player_locations.EVENT_LOCATION_TABLE:
real_location = location[:-7]
- panel = StaticWitnessLogic.CHECKS_BY_NAME[real_location]
- check_hex = panel["checkHex"]
+ associated_entity = world.player_logic.REFERENCE_LOGIC.ENTITIES_BY_NAME[real_location]
+ entity_hex = associated_entity["entity_hex"]
+
+ rule = make_lambda(entity_hex, world)
- rule = make_lambda(check_hex, world, player, player_logic, locat)
+ location = world.get_location(location)
- set_rule(world.get_location(location, player), rule)
+ set_rule(location, rule)
- world.completion_condition[player] = \
- lambda state: state.has('Victory', player)
+ world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
diff --git a/worlds/witness/settings/Disable_Unrandomized.txt b/worlds/witness/settings/Disable_Unrandomized.txt
deleted file mode 100644
index f7a0fcb7cbd6..000000000000
--- a/worlds/witness/settings/Disable_Unrandomized.txt
+++ /dev/null
@@ -1,128 +0,0 @@
-Event Items:
-Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9
-Bunker Laser Activation - 0x00061,0x17D01,0x17C42
-Shadows Laser Activation - 0x00021,0x17D28,0x17C71
-
-Requirement Changes:
-0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9
-0x0C2B2 - 0x00061 | 0x17D01 | 0x17C42
-0x181B3 - 0x00021 | 0x17D28 | 0x17C71
-0x28B39 - True - Reflection
-0x17CAB - True - True
-0x2779A - True - 0x17CFB | 0x3C12B | 0x17CF7
-
-Disabled Locations:
-0x03505 (Tutorial Gate Close)
-0x0C335 (Tutorial Pillar)
-0x0C373 (Tutorial Patio Floor)
-0x009B8 (Symmetry Island Scenery Outlines 1)
-0x003E8 (Symmetry Island Scenery Outlines 2)
-0x00A15 (Symmetry Island Scenery Outlines 3)
-0x00B53 (Symmetry Island Scenery Outlines 4)
-0x00B8D (Symmetry Island Scenery Outlines 5)
-0x00143 (Orchard Apple Tree 1)
-0x0003B (Orchard Apple Tree 2)
-0x00055 (Orchard Apple Tree 3)
-0x032F7 (Orchard Apple Tree 4)
-0x032FF (Orchard Apple Tree 5)
-0x198B5 (Shadows Intro 1)
-0x198BD (Shadows Intro 2)
-0x198BF (Shadows Intro 3)
-0x19771 (Shadows Intro 4)
-0x0A8DC (Shadows Intro 5)
-0x0AC74 (Shadows Intro 6)
-0x0AC7A (Shadows Intro 7)
-0x0A8E0 (Shadows Intro 8)
-0x386FA (Shadows Far 1)
-0x1C33F (Shadows Far 2)
-0x196E2 (Shadows Far 3)
-0x1972A (Shadows Far 4)
-0x19809 (Shadows Far 5)
-0x19806 (Shadows Far 6)
-0x196F8 (Shadows Far 7)
-0x1972F (Shadows Far 8)
-0x19797 (Shadows Near 1)
-0x1979A (Shadows Near 2)
-0x197E0 (Shadows Near 3)
-0x197E8 (Shadows Near 4)
-0x197E5 (Shadows Near 5)
-0x19650 (Shadows Laser)
-0x00139 (Keep Hedge Maze 1)
-0x019DC (Keep Hedge Maze 2)
-0x019E7 (Keep Hedge Maze 3)
-0x01A0F (Keep Hedge Maze 4)
-0x0360E (Laser Hedges)
-0x03307 (First Gate)
-0x03313 (Second Gate)
-0x0C128 (Entry Inner)
-0x0C153 (Entry Outer)
-0x00B10 (Monastery Entry Left)
-0x00C92 (Monastery Entry Right)
-0x00290 (Monastery Outside 1)
-0x00038 (Monastery Outside 2)
-0x00037 (Monastery Outside 3)
-0x193A7 (Monastery Inside 1)
-0x193AA (Monastery Inside 2)
-0x193AB (Monastery Inside 3)
-0x193A6 (Monastery Inside 4)
-0x17CA4 (Monastery Laser)
-0x18590 (Transparent) - True - Symmetry & Environment
-0x28AE3 (Vines) - 0x18590 - Shadows Follow & Environment
-0x28938 (Apple Tree) - 0x28AE3 - Environment
-0x079DF (Triple Exit) - 0x28938 - Shadows Avoid & Environment & Reflection
-0x28B39 (Tall Hexagonal) - 0x079DF & 0x2896A - Reflection
-0x03553 (Theater Tutorial Video)
-0x03552 (Theater Desert Video)
-0x0354E (Theater Jungle Video)
-0x03549 (Theater Challenge Video)
-0x0354F (Theater Shipwreck Video)
-0x03545 (Theater Mountain Video)
-0x002C4 (First Row 1)
-0x00767 (First Row 2)
-0x002C6 (First Row 3)
-0x0070E (Second Row 1)
-0x0070F (Second Row 2)
-0x0087D (Second Row 3)
-0x002C7 (Second Row 4)
-0x17CAA (Monastery Shortcut Panel)
-0x0C2A4 (Bunker Entry)
-0x17C79 (Tinted Glass Door)
-0x0C2A3 (UV Room Entry)
-0x0A08D (Elevator Room Entry)
-0x17C2E (Door to Bunker)
-0x09F7D (Bunker Intro Left 1)
-0x09FDC (Bunker Intro Left 2)
-0x09FF7 (Bunker Intro Left 3)
-0x09F82 (Bunker Intro Left 4)
-0x09FF8 (Bunker Intro Left 5)
-0x09D9F (Bunker Intro Back 1)
-0x09DA1 (Bunker Intro Back 2)
-0x09DA2 (Bunker Intro Back 3)
-0x09DAF (Bunker Intro Back 4)
-0x0A010 (Bunker Glass Room 1)
-0x0A01B (Bunker Glass Room 2)
-0x0A01F (Bunker Glass Room 3)
-0x0A099 (Tinted Glass Door)
-0x34BC5 (Bunker Drop-Down Door Open)
-0x34BC6 (Bunker Drop-Down Door Close)
-0x17E63 (Bunker UV Room 1)
-0x17E67 (Bunker UV Room 2)
-0x09DE0 (Bunker Laser)
-0x0A079 (Bunker Elevator Control)
-
-0x17CAA (River Garden Entry Panel)
-
-Precompleted Locations:
-0x034A7
-0x034AD
-0x034AF
-0x339B6
-0x33A29
-0x33A2A
-0x33B06
-0x3352F
-0x33600
-0x035F5
-0x000D3
-0x33A20
-0x03BE2
\ No newline at end of file
diff --git a/worlds/witness/settings/Door_Panel_Shuffle.txt b/worlds/witness/settings/Door_Panel_Shuffle.txt
deleted file mode 100644
index 80195cfb9968..000000000000
--- a/worlds/witness/settings/Door_Panel_Shuffle.txt
+++ /dev/null
@@ -1,31 +0,0 @@
-Items:
-Glass Factory Entry (Panel)
-Symmetry Island Lower (Panel)
-Symmetry Island Upper (Panel)
-Desert Light Room Entry (Panel)
-Desert Flood Controls (Panel)
-Quarry Stoneworks Entry (Panel)
-Quarry Stoneworks Ramp Controls (Panel)
-Quarry Stoneworks Lift Controls (Panel)
-Quarry Boathouse Ramp Height Control (Panel)
-Quarry Boathouse Ramp Horizontal Control (Panel)
-Shadows Door Timer (Panel)
-Monastery Entry Left (Panel)
-Monastery Entry Right (Panel)
-Town Tinted Glass Door (Panel)
-Town Church Entry (Panel)
-Town Maze Panel (Drop-Down Staircase) (Panel)
-Windmill Entry (Panel)
-Treehouse First & Second Doors (Panel)
-Treehouse Third Door (Panel)
-Treehouse Laser House Door Timer (Panel)
-Treehouse Drawbridge (Panel)
-Jungle Popup Wall (Panel)
-Bunker Entry (Panel)
-Bunker Tinted Glass Door (Panel)
-Bunker Elevator Control (Panel)
-Swamp Entry (Panel)
-Swamp Sliding Bridge (Panel)
-Swamp Rotating Bridge (Panel)
-Swamp Maze Control (Panel)
-Boat
\ No newline at end of file
diff --git a/worlds/witness/settings/Doors_Complex.txt b/worlds/witness/settings/Doors_Complex.txt
deleted file mode 100644
index d8da6783b0c5..000000000000
--- a/worlds/witness/settings/Doors_Complex.txt
+++ /dev/null
@@ -1,200 +0,0 @@
-Items:
-Outside Tutorial Outpost Path (Door)
-Outside Tutorial Outpost Entry (Door)
-Outside Tutorial Outpost Exit (Door)
-Glass Factory Entry (Door)
-Glass Factory Back Wall (Door)
-Symmetry Island Lower (Door)
-Symmetry Island Upper (Door)
-Orchard First Gate (Door)
-Orchard Second Gate (Door)
-Desert Light Room Entry (Door)
-Desert Pond Room Entry (Door)
-Desert Flood Room Entry (Door)
-Desert Elevator Room Entry (Door)
-Quarry Entry 1 (Door)
-Quarry Entry 2 (Door)
-Quarry Stoneworks Entry (Door)
-Quarry Stoneworks Side Exit (Door)
-Quarry Stoneworks Roof Exit (Door)
-Quarry Stoneworks Stairs (Door)
-Quarry Boathouse Dock (Door)
-Quarry Boathouse First Barrier (Door)
-Quarry Boathouse Second Barrier (Door)
-Shadows Timed Door
-Shadows Laser Entry Right (Door)
-Shadows Laser Entry Left (Door)
-Shadows Quarry Barrier (Door)
-Shadows Ledge Barrier (Door)
-Keep Hedge Maze 1 Exit (Door)
-Keep Pressure Plates 1 Exit (Door)
-Keep Hedge Maze 2 Shortcut (Door)
-Keep Hedge Maze 2 Exit (Door)
-Keep Hedge Maze 3 Shortcut (Door)
-Keep Hedge Maze 3 Exit (Door)
-Keep Hedge Maze 4 Shortcut (Door)
-Keep Hedge Maze 4 Exit (Door)
-Keep Pressure Plates 2 Exit (Door)
-Keep Pressure Plates 3 Exit (Door)
-Keep Pressure Plates 4 Exit (Door)
-Keep Shadows Shortcut (Door)
-Keep Tower Shortcut (Door)
-Monastery Shortcut (Door)
-Monastery Entry Inner (Door)
-Monastery Entry Outer (Door)
-Monastery Garden Entry (Door)
-Town Cargo Box Entry (Door)
-Town Wooden Roof Stairs (Door)
-Town Tinted Glass Door
-Town Church Entry (Door)
-Town Maze Stairs (Door)
-Town Windmill Entry (Door)
-Town RGB House Stairs (Door)
-Town Tower Second (Door)
-Town Tower First (Door)
-Town Tower Fourth (Door)
-Town Tower Third (Door)
-Theater Entry (Door)
-Theater Exit Left (Door)
-Theater Exit Right (Door)
-Jungle Bamboo Laser Shortcut (Door)
-Jungle Popup Wall (Door)
-River Monastery Shortcut (Door)
-Bunker Entry (Door)
-Bunker Tinted Glass Door
-Bunker UV Room Entry (Door)
-Bunker Elevator Room Entry (Door)
-Swamp Entry (Door)
-Swamp Between Bridges First Door
-Swamp Platform Shortcut Door
-Swamp Cyan Water Pump (Door)
-Swamp Between Bridges Second Door
-Swamp Red Water Pump (Door)
-Swamp Red Underwater Exit (Door)
-Swamp Blue Water Pump (Door)
-Swamp Purple Water Pump (Door)
-Swamp Laser Shortcut (Door)
-Treehouse First (Door)
-Treehouse Second (Door)
-Treehouse Third (Door)
-Treehouse Drawbridge (Door)
-Treehouse Laser House Entry (Door)
-Mountain Floor 1 Exit (Door)
-Mountain Floor 2 Staircase Near (Door)
-Mountain Floor 2 Exit (Door)
-Mountain Floor 2 Staircase Far (Door)
-Mountain Bottom Floor Giant Puzzle Exit (Door)
-Mountain Bottom Floor Final Room Entry (Door)
-Mountain Bottom Floor Rock (Door)
-Caves Entry (Door)
-Caves Pillar Door
-Caves Mountain Shortcut (Door)
-Caves Swamp Shortcut (Door)
-Challenge Entry (Door)
-Challenge Tunnels Entry (Door)
-Tunnels Theater Shortcut (Door)
-Tunnels Desert Shortcut (Door)
-Tunnels Town Shortcut (Door)
-
-Added Locations:
-Outside Tutorial Outpost Entry Panel
-Outside Tutorial Outpost Exit Panel
-Glass Factory Entry Panel
-Glass Factory Back Wall 5
-Symmetry Island Lower Panel
-Symmetry Island Upper Panel
-Orchard Apple Tree 3
-Orchard Apple Tree 5
-Desert Light Room Entry Panel
-Desert Light Room 3
-Desert Flood Room Entry Panel
-Desert Flood Room 6
-Quarry Entry 1 Panel
-Quarry Entry 2 Panel
-Quarry Stoneworks Entry Right Panel
-Quarry Stoneworks Entry Left Panel
-Quarry Stoneworks Side Exit Panel
-Quarry Stoneworks Roof Exit Panel
-Quarry Stoneworks Stair Control
-Quarry Boathouse Second Barrier Panel
-Shadows Door Timer Inside
-Shadows Door Timer Outside
-Shadows Far 8
-Shadows Near 5
-Shadows Intro 3
-Shadows Intro 5
-Keep Hedge Maze 1
-Keep Pressure Plates 1
-Keep Hedge Maze 2
-Keep Hedge Maze 3
-Keep Hedge Maze 4
-Keep Pressure Plates 2
-Keep Pressure Plates 3
-Keep Pressure Plates 4
-Keep Shadows Shortcut Panel
-Keep Tower Shortcut Panel
-Monastery Shortcut Panel
-Monastery Entry Left
-Monastery Entry Right
-Monastery Outside 3
-Town Cargo Box Entry Panel
-Town Wooden Roof Lower Row 5
-Town Tinted Glass Door Panel
-Town Church Entry Panel
-Town Maze Stair Control
-Town Windmill Entry Panel
-Town Sound Room Right
-Town Red Rooftop 5
-Town Church Lattice
-Town Tall Hexagonal
-Town Wooden Rooftop
-Windmill Theater Entry Panel
-Theater Exit Left Panel
-Theater Exit Right Panel
-Jungle Laser Shortcut Panel
-Jungle Popup Wall Control
-River Monastery Shortcut Panel
-Bunker Entry Panel
-Bunker Tinted Glass Door Panel
-Bunker Glass Room 3
-Bunker UV Room 2
-Swamp Entry Panel
-Swamp Platform Row 4
-Swamp Platform Shortcut Right Panel
-Swamp Blue Underwater 5
-Swamp Between Bridges Near Row 4
-Swamp Cyan Underwater 5
-Swamp Red Underwater 4
-Swamp Beyond Rotating Bridge 4
-Swamp Beyond Rotating Bridge 4
-Swamp Laser Shortcut Right Panel
-Treehouse First Door Panel
-Treehouse Second Door Panel
-Treehouse Third Door Panel
-Treehouse Bridge Control
-Treehouse Left Orange Bridge 15
-Treehouse Right Orange Bridge 12
-Treehouse Laser House Door Timer Outside Control
-Treehouse Laser House Door Timer Inside
-Mountain Floor 1 Left Row 7
-Mountain Floor 1 Right Row 5
-Mountain Floor 1 Back Row 3
-Mountain Floor 1 Trash Pillar 2
-Mountain Floor 2 Near Row 5
-Mountain Floor 2 Light Bridge Controller Near
-Mountain Floor 2 Light Bridge Controller Far
-Mountain Floor 2 Far Row 6
-Mountain Bottom Floor Giant Puzzle
-Mountain Bottom Floor Final Room Entry Left
-Mountain Bottom Floor Final Room Entry Right
-Mountain Bottom Floor Discard
-Mountain Bottom Floor Rock Control
-Mountain Bottom Floor Caves Entry Panel
-Caves Lone Pillar
-Caves Mountain Shortcut Panel
-Caves Swamp Shortcut Panel
-Caves Challenge Entry Panel
-Challenge Tunnels Entry Panel
-Tunnels Theater Shortcut Panel
-Tunnels Desert Shortcut Panel
-Tunnels Town Shortcut Panel
\ No newline at end of file
diff --git a/worlds/witness/settings/Doors_Max.txt b/worlds/witness/settings/Doors_Max.txt
deleted file mode 100644
index e722b61ca0c0..000000000000
--- a/worlds/witness/settings/Doors_Max.txt
+++ /dev/null
@@ -1,211 +0,0 @@
-Items:
-Outside Tutorial Outpost Path (Door)
-Outside Tutorial Outpost Entry (Door)
-Outside Tutorial Outpost Exit (Door)
-Glass Factory Entry (Door)
-Glass Factory Back Wall (Door)
-Symmetry Island Lower (Door)
-Symmetry Island Upper (Door)
-Orchard First Gate (Door)
-Orchard Second Gate (Door)
-Desert Light Room Entry (Door)
-Desert Pond Room Entry (Door)
-Desert Flood Room Entry (Door)
-Desert Elevator Room Entry (Door)
-Quarry Entry 1 (Door)
-Quarry Entry 2 (Door)
-Quarry Stoneworks Entry (Door)
-Quarry Stoneworks Side Exit (Door)
-Quarry Stoneworks Roof Exit (Door)
-Quarry Stoneworks Stairs (Door)
-Quarry Boathouse Dock (Door)
-Quarry Boathouse First Barrier (Door)
-Quarry Boathouse Second Barrier (Door)
-Shadows Timed Door
-Shadows Laser Entry Right (Door)
-Shadows Laser Entry Left (Door)
-Shadows Quarry Barrier (Door)
-Shadows Ledge Barrier (Door)
-Keep Hedge Maze 1 Exit (Door)
-Keep Pressure Plates 1 Exit (Door)
-Keep Hedge Maze 2 Shortcut (Door)
-Keep Hedge Maze 2 Exit (Door)
-Keep Hedge Maze 3 Shortcut (Door)
-Keep Hedge Maze 3 Exit (Door)
-Keep Hedge Maze 4 Shortcut (Door)
-Keep Hedge Maze 4 Exit (Door)
-Keep Pressure Plates 2 Exit (Door)
-Keep Pressure Plates 3 Exit (Door)
-Keep Pressure Plates 4 Exit (Door)
-Keep Shadows Shortcut (Door)
-Keep Tower Shortcut (Door)
-Monastery Shortcut (Door)
-Monastery Entry Inner (Door)
-Monastery Entry Outer (Door)
-Monastery Garden Entry (Door)
-Town Cargo Box Entry (Door)
-Town Wooden Roof Stairs (Door)
-Town Tinted Glass Door
-Town Church Entry (Door)
-Town Maze Stairs (Door)
-Town Windmill Entry (Door)
-Town RGB House Stairs (Door)
-Town Tower Second (Door)
-Town Tower First (Door)
-Town Tower Fourth (Door)
-Town Tower Third (Door)
-Theater Entry (Door)
-Theater Exit Left (Door)
-Theater Exit Right (Door)
-Jungle Bamboo Laser Shortcut (Door)
-Jungle Popup Wall (Door)
-River Monastery Shortcut (Door)
-Bunker Entry (Door)
-Bunker Tinted Glass Door
-Bunker UV Room Entry (Door)
-Bunker Elevator Room Entry (Door)
-Swamp Entry (Door)
-Swamp Between Bridges First Door
-Swamp Platform Shortcut Door
-Swamp Cyan Water Pump (Door)
-Swamp Between Bridges Second Door
-Swamp Red Water Pump (Door)
-Swamp Red Underwater Exit (Door)
-Swamp Blue Water Pump (Door)
-Swamp Purple Water Pump (Door)
-Swamp Laser Shortcut (Door)
-Treehouse First (Door)
-Treehouse Second (Door)
-Treehouse Third (Door)
-Treehouse Drawbridge (Door)
-Treehouse Laser House Entry (Door)
-Mountain Floor 1 Exit (Door)
-Mountain Floor 2 Staircase Near (Door)
-Mountain Floor 2 Exit (Door)
-Mountain Floor 2 Staircase Far (Door)
-Mountain Bottom Floor Giant Puzzle Exit (Door)
-Mountain Bottom Floor Final Room Entry (Door)
-Mountain Bottom Floor Rock (Door)
-Caves Entry (Door)
-Caves Pillar Door
-Caves Mountain Shortcut (Door)
-Caves Swamp Shortcut (Door)
-Challenge Entry (Door)
-Challenge Tunnels Entry (Door)
-Tunnels Theater Shortcut (Door)
-Tunnels Desert Shortcut (Door)
-Tunnels Town Shortcut (Door)
-
-Desert Flood Controls (Panel)
-Quarry Stoneworks Ramp Controls (Panel)
-Quarry Stoneworks Lift Controls (Panel)
-Quarry Boathouse Ramp Height Control (Panel)
-Quarry Boathouse Ramp Horizontal Control (Panel)
-Bunker Elevator Control (Panel)
-Swamp Sliding Bridge (Panel)
-Swamp Rotating Bridge (Panel)
-Swamp Maze Control (Panel)
-Boat
-
-Added Locations:
-Outside Tutorial Outpost Entry Panel
-Outside Tutorial Outpost Exit Panel
-Glass Factory Entry Panel
-Glass Factory Back Wall 5
-Symmetry Island Lower Panel
-Symmetry Island Upper Panel
-Orchard Apple Tree 3
-Orchard Apple Tree 5
-Desert Light Room Entry Panel
-Desert Light Room 3
-Desert Flood Room Entry Panel
-Desert Flood Room 6
-Quarry Entry 1 Panel
-Quarry Entry 2 Panel
-Quarry Stoneworks Entry Right Panel
-Quarry Stoneworks Entry Left Panel
-Quarry Stoneworks Side Exit Panel
-Quarry Stoneworks Roof Exit Panel
-Quarry Stoneworks Stair Control
-Quarry Boathouse Second Barrier Panel
-Shadows Door Timer Inside
-Shadows Door Timer Outside
-Shadows Far 8
-Shadows Near 5
-Shadows Intro 3
-Shadows Intro 5
-Keep Hedge Maze 1
-Keep Pressure Plates 1
-Keep Hedge Maze 2
-Keep Hedge Maze 3
-Keep Hedge Maze 4
-Keep Pressure Plates 2
-Keep Pressure Plates 3
-Keep Pressure Plates 4
-Keep Shadows Shortcut Panel
-Keep Tower Shortcut Panel
-Monastery Shortcut Panel
-Monastery Entry Left
-Monastery Entry Right
-Monastery Outside 3
-Town Cargo Box Entry Panel
-Town Wooden Roof Lower Row 5
-Town Tinted Glass Door Panel
-Town Church Entry Panel
-Town Maze Stair Control
-Town Windmill Entry Panel
-Town Sound Room Right
-Town Red Rooftop 5
-Town Church Lattice
-Town Tall Hexagonal
-Town Wooden Rooftop
-Windmill Theater Entry Panel
-Theater Exit Left Panel
-Theater Exit Right Panel
-Jungle Laser Shortcut Panel
-Jungle Popup Wall Control
-River Monastery Shortcut Panel
-Bunker Entry Panel
-Bunker Tinted Glass Door Panel
-Bunker Glass Room 3
-Bunker UV Room 2
-Swamp Entry Panel
-Swamp Platform Row 4
-Swamp Platform Shortcut Right Panel
-Swamp Blue Underwater 5
-Swamp Between Bridges Near Row 4
-Swamp Cyan Underwater 5
-Swamp Red Underwater 4
-Swamp Beyond Rotating Bridge 4
-Swamp Beyond Rotating Bridge 4
-Swamp Laser Shortcut Right Panel
-Treehouse First Door Panel
-Treehouse Second Door Panel
-Treehouse Third Door Panel
-Treehouse Bridge Control
-Treehouse Left Orange Bridge 15
-Treehouse Right Orange Bridge 12
-Treehouse Laser House Door Timer Outside Control
-Treehouse Laser House Door Timer Inside
-Mountain Floor 1 Left Row 7
-Mountain Floor 1 Right Row 5
-Mountain Floor 1 Back Row 3
-Mountain Floor 1 Trash Pillar 2
-Mountain Floor 2 Near Row 5
-Mountain Floor 2 Light Bridge Controller Near
-Mountain Floor 2 Light Bridge Controller Far
-Mountain Floor 2 Far Row 6
-Mountain Bottom Floor Giant Puzzle
-Mountain Bottom Floor Final Room Entry Left
-Mountain Bottom Floor Final Room Entry Right
-Mountain Bottom Floor Discard
-Mountain Bottom Floor Rock Control
-Mountain Bottom Floor Caves Entry Panel
-Caves Lone Pillar
-Caves Mountain Shortcut Panel
-Caves Swamp Shortcut Panel
-Caves Challenge Entry Panel
-Challenge Tunnels Entry Panel
-Tunnels Theater Shortcut Panel
-Tunnels Desert Shortcut Panel
-Tunnels Town Shortcut Panel
\ No newline at end of file
diff --git a/worlds/witness/settings/Doors_Simple.txt b/worlds/witness/settings/Doors_Simple.txt
deleted file mode 100644
index 309874b131b9..000000000000
--- a/worlds/witness/settings/Doors_Simple.txt
+++ /dev/null
@@ -1,145 +0,0 @@
-Items:
-Glass Factory Back Wall (Door)
-Quarry Boathouse Dock (Door)
-Outside Tutorial Outpost Doors
-Glass Factory Entry (Door)
-Symmetry Island Doors
-Orchard Gates
-Desert Doors
-Quarry Main Entry
-Quarry Stoneworks Entry (Door)
-Quarry Stoneworks Shortcuts
-Quarry Boathouse Barriers
-Shadows Timed Door
-Shadows Laser Room Door
-Shadows Barriers
-Keep Hedge Maze Doors
-Keep Pressure Plates Doors
-Keep Shortcuts
-Monastery Entry
-Monastery Shortcuts
-Town Doors
-Town Tower Doors
-Theater Entry (Door)
-Theater Exit
-Jungle & River Shortcuts
-Jungle Popup Wall (Door)
-Bunker Doors
-Swamp Doors
-Swamp Laser Shortcut (Door)
-Swamp Water Pumps
-Treehouse Entry Doors
-Treehouse Drawbridge (Door)
-Treehouse Laser House Entry (Door)
-Mountain Floor 1 Exit (Door)
-Mountain Floor 2 Stairs & Doors
-Mountain Bottom Floor Giant Puzzle Exit (Door)
-Mountain Bottom Floor Final Room Entry (Door)
-Mountain Bottom Floor Doors to Caves
-Caves Doors to Challenge
-Caves Exits to Main Island
-Challenge Tunnels Entry (Door)
-Tunnels Doors
-
-Added Locations:
-Outside Tutorial Outpost Entry Panel
-Outside Tutorial Outpost Exit Panel
-Glass Factory Entry Panel
-Glass Factory Back Wall 5
-Symmetry Island Lower Panel
-Symmetry Island Upper Panel
-Orchard Apple Tree 3
-Orchard Apple Tree 5
-Desert Light Room Entry Panel
-Desert Light Room 3
-Desert Flood Room Entry Panel
-Desert Flood Room 6
-Quarry Entry 1 Panel
-Quarry Entry 2 Panel
-Quarry Stoneworks Entry Right Panel
-Quarry Stoneworks Entry Left Panel
-Quarry Stoneworks Side Exit Panel
-Quarry Stoneworks Roof Exit Panel
-Quarry Stoneworks Stair Control
-Quarry Boathouse Second Barrier Panel
-Shadows Door Timer Inside
-Shadows Door Timer Outside
-Shadows Far 8
-Shadows Near 5
-Shadows Intro 3
-Shadows Intro 5
-Keep Hedge Maze 1
-Keep Pressure Plates 1
-Keep Hedge Maze 2
-Keep Hedge Maze 3
-Keep Hedge Maze 4
-Keep Pressure Plates 2
-Keep Pressure Plates 3
-Keep Pressure Plates 4
-Keep Shadows Shortcut Panel
-Keep Tower Shortcut Panel
-Monastery Shortcut Panel
-Monastery Entry Left
-Monastery Entry Right
-Monastery Outside 3
-Town Cargo Box Entry Panel
-Town Wooden Roof Lower Row 5
-Town Tinted Glass Door Panel
-Town Church Entry Panel
-Town Maze Stair Control
-Town Windmill Entry Panel
-Town Sound Room Right
-Town Red Rooftop 5
-Town Church Lattice
-Town Tall Hexagonal
-Town Wooden Rooftop
-Windmill Theater Entry Panel
-Theater Exit Left Panel
-Theater Exit Right Panel
-Jungle Laser Shortcut Panel
-Jungle Popup Wall Control
-River Monastery Shortcut Panel
-Bunker Entry Panel
-Bunker Tinted Glass Door Panel
-Bunker Glass Room 3
-Bunker UV Room 2
-Swamp Entry Panel
-Swamp Platform Row 4
-Swamp Platform Shortcut Right Panel
-Swamp Blue Underwater 5
-Swamp Between Bridges Near Row 4
-Swamp Cyan Underwater 5
-Swamp Red Underwater 4
-Swamp Beyond Rotating Bridge 4
-Swamp Beyond Rotating Bridge 4
-Swamp Laser Shortcut Right Panel
-Treehouse First Door Panel
-Treehouse Second Door Panel
-Treehouse Third Door Panel
-Treehouse Bridge Control
-Treehouse Left Orange Bridge 15
-Treehouse Right Orange Bridge 12
-Treehouse Laser House Door Timer Outside Control
-Treehouse Laser House Door Timer Inside
-Mountain Floor 1 Left Row 7
-Mountain Floor 1 Right Row 5
-Mountain Floor 1 Back Row 3
-Mountain Floor 1 Trash Pillar 2
-Mountain Floor 2 Near Row 5
-Mountain Floor 2 Light Bridge Controller Near
-Mountain Floor 2 Light Bridge Controller Far
-Mountain Floor 2 Far Row 6
-Mountain Bottom Floor Giant Puzzle
-Mountain Bottom Floor Final Room Entry Left
-Mountain Bottom Floor Final Room Entry Right
-Mountain Bottom Floor Discard
-Mountain Bottom Floor Rock Control
-Mountain Bottom Floor Caves Entry Panel
-Caves Lone Pillar
-Caves Mountain Shortcut Panel
-Caves Swamp Shortcut Panel
-Caves Challenge Entry Panel
-Challenge Tunnels Entry Panel
-Tunnels Theater Shortcut Panel
-Tunnels Desert Shortcut Panel
-Tunnels Town Shortcut Panel
\ No newline at end of file
diff --git a/worlds/witness/settings/EP_Shuffle/EP_All.txt b/worlds/witness/settings/EP_Shuffle/EP_All.txt
deleted file mode 100644
index 51af5e38502e..000000000000
--- a/worlds/witness/settings/EP_Shuffle/EP_All.txt
+++ /dev/null
@@ -1,136 +0,0 @@
-Added Locations:
-0x0332B
-0x03367
-0x28B8A
-0x037B6
-0x037B2
-0x000F7
-0x3351D
-0x0053C
-0x00771
-0x335C8
-0x335C9
-0x337F8
-0x037BB
-0x220E4
-0x220E5
-0x334B9
-0x334BC
-0x22106
-0x0A14C
-0x0A14D
-0x03ABC
-0x03ABE
-0x03AC0
-0x03AC4
-0x03AC5
-0x03BE2
-0x03BE3
-0x0A409
-0x006E5
-0x006E6
-0x006E7
-0x034A7
-0x034AD
-0x034AF
-0x03DAB
-0x03DAC
-0x03DAD
-0x03E01
-0x289F4
-0x289F5
-0x0053D
-0x0053E
-0x00769
-0x33721
-0x220A7
-0x220BD
-0x03B22
-0x03B23
-0x03B24
-0x03B25
-0x03A79
-0x28ABD
-0x28ABE
-0x3388F
-0x28B29
-0x28B2A
-0x018B6
-0x033BE
-0x033BF
-0x033DD
-0x033E5
-0x28AE9
-0x3348F
-0x001A3
-0x335AE
-0x000D3
-0x035F5
-0x09D5D
-0x09D5E
-0x09D63
-0x3370E
-0x035DE
-0x03601
-0x03603
-0x03D0D
-0x3369A
-0x336C8
-0x33505
-0x03A9E
-0x016B2
-0x3365F
-0x03731
-0x036CE
-0x03C07
-0x03A93
-0x03AA6
-0x3397C
-0x0105D
-0x0A304
-0x035CB
-0x035CF
-0x28A7B
-0x005F6
-0x00859
-0x17CB9
-0x28A4A
-0x334B6
-0x0069D
-0x00614
-0x28A4C
-0x289CF
-0x289D1
-0x33692
-0x03E77
-0x03E7C
-0x035C7
-0x01848
-0x03D06
-0x33530
-0x33600
-0x28A2F
-0x28A37
-0x334A3
-0x3352F
-0x33857
-0x33879
-0x03C19
-0x28B30
-0x035C9
-0x03335
-0x03412
-0x038A6
-0x038AA
-0x03E3F
-0x03E40
-0x28B8E
-0x28B91
-0x03BCE
-0x03BCF
-0x03BD1
-0x339B6
-0x33A20
-0x33A29
-0x33A2A
-0x33B06
\ No newline at end of file
diff --git a/worlds/witness/settings/EP_Shuffle/EP_Easy.txt b/worlds/witness/settings/EP_Shuffle/EP_Easy.txt
deleted file mode 100644
index 939055169a75..000000000000
--- a/worlds/witness/settings/EP_Shuffle/EP_Easy.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-Precompleted Locations:
-0x339B6
-0x335AE
-0x3388F
-0x33A20
-0x037B2
-0x000F7
-0x28B29
-0x33857
-0x33879
-0x016B2
-0x036CE
-0x03B25
-0x28B2A
\ No newline at end of file
diff --git a/worlds/witness/settings/EP_Shuffle/EP_NoCavesEPs.txt b/worlds/witness/settings/EP_Shuffle/EP_NoCavesEPs.txt
deleted file mode 100644
index 3bb318a69ee9..000000000000
--- a/worlds/witness/settings/EP_Shuffle/EP_NoCavesEPs.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-Precompleted Locations:
-0x3397C
-0x33A20
-0x3352F
-0x28B30
\ No newline at end of file
diff --git a/worlds/witness/settings/EP_Shuffle/EP_NoEclipse.txt b/worlds/witness/settings/EP_Shuffle/EP_NoEclipse.txt
deleted file mode 100644
index d41badfa1802..000000000000
--- a/worlds/witness/settings/EP_Shuffle/EP_NoEclipse.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Precompleted Locations:
-0x339B6
\ No newline at end of file
diff --git a/worlds/witness/settings/EP_Shuffle/EP_NoMountainEPs.txt b/worlds/witness/settings/EP_Shuffle/EP_NoMountainEPs.txt
deleted file mode 100644
index 3558d77ad888..000000000000
--- a/worlds/witness/settings/EP_Shuffle/EP_NoMountainEPs.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Precompleted Locations:
-0x09D63
-0x09D5D
-0x09D5E
\ No newline at end of file
diff --git a/worlds/witness/settings/EP_Shuffle/EP_Sides.txt b/worlds/witness/settings/EP_Shuffle/EP_Sides.txt
deleted file mode 100644
index 1da52ffb8959..000000000000
--- a/worlds/witness/settings/EP_Shuffle/EP_Sides.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-Added Locations:
-0xFFE00
-0xFFE01
-0xFFE02
-0xFFE03
-0xFFE04
-0xFFE10
-0xFFE11
-0xFFE12
-0xFFE13
-0xFFE14
-0xFFE15
-0xFFE20
-0xFFE21
-0xFFE22
-0xFFE23
-0xFFE24
-0xFFE25
-0xFFE30
-0xFFE31
-0xFFE32
-0xFFE33
-0xFFE34
-0xFFE35
-0xFFE40
-0xFFE41
-0xFFE42
-0xFFE43
-0xFFE50
-0xFFE51
-0xFFE52
-0xFFE53
-0xFFE54
-0xFFE55
\ No newline at end of file
diff --git a/worlds/witness/settings/EP_Shuffle/EP_Videos.txt b/worlds/witness/settings/EP_Shuffle/EP_Videos.txt
deleted file mode 100644
index c4aaca13a676..000000000000
--- a/worlds/witness/settings/EP_Shuffle/EP_Videos.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-Precompleted Locations:
-0x339B6
-0x33A29
-0x33A2A
-0x33B06
-0x33A20
\ No newline at end of file
diff --git a/worlds/witness/settings/Early_UTM.txt b/worlds/witness/settings/Early_UTM.txt
deleted file mode 100644
index b04aa3d33916..000000000000
--- a/worlds/witness/settings/Early_UTM.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-Items:
-Caves Exits to Main Island
-
-Starting Inventory:
-Caves Exits to Main Island
-
-Remove Items:
-Caves Mountain Shortcut (Door)
-Caves Swamp Shortcut (Door)
\ No newline at end of file
diff --git a/worlds/witness/static_logic.py b/worlds/witness/static_logic.py
deleted file mode 100644
index 8dc2a05de56d..000000000000
--- a/worlds/witness/static_logic.py
+++ /dev/null
@@ -1,273 +0,0 @@
-from dataclasses import dataclass
-from enum import Enum
-from typing import Dict, List
-
-from .utils import define_new_region, parse_lambda, lazy, get_items, get_sigma_normal_logic, get_sigma_expert_logic,\
- get_vanilla_logic
-
-
-class ItemCategory(Enum):
- SYMBOL = 0
- DOOR = 1
- LASER = 2
- USEFUL = 3
- FILLER = 4
- TRAP = 5
- JOKE = 6
- EVENT = 7
-
-
-CATEGORY_NAME_MAPPINGS: Dict[str, ItemCategory] = {
- "Symbols:": ItemCategory.SYMBOL,
- "Doors:": ItemCategory.DOOR,
- "Lasers:": ItemCategory.LASER,
- "Useful:": ItemCategory.USEFUL,
- "Filler:": ItemCategory.FILLER,
- "Traps:": ItemCategory.TRAP,
- "Jokes:": ItemCategory.JOKE
-}
-
-
-@dataclass(frozen=True)
-class ItemDefinition:
- local_code: int
- category: ItemCategory
-
-
-@dataclass(frozen=True)
-class ProgressiveItemDefinition(ItemDefinition):
- child_item_names: List[str]
-
-
-@dataclass(frozen=True)
-class DoorItemDefinition(ItemDefinition):
- panel_id_hexes: List[str]
-
-
-@dataclass(frozen=True)
-class WeightedItemDefinition(ItemDefinition):
- weight: int
-
-
-class StaticWitnessLogicObj:
- def read_logic_file(self, lines):
- """
- Reads the logic file and does the initial population of data structures
- """
-
- current_region = dict()
-
- for line in lines:
- if line == "" or line[0] == "#":
- continue
-
- if line[-1] == ":":
- new_region_and_connections = define_new_region(line)
- current_region = new_region_and_connections[0]
- region_name = current_region["name"]
- self.ALL_REGIONS_BY_NAME[region_name] = current_region
- self.STATIC_CONNECTIONS_BY_REGION_NAME[region_name] = new_region_and_connections[1]
- continue
-
- line_split = line.split(" - ")
-
- location_id = line_split.pop(0)
-
- check_name_full = line_split.pop(0)
-
- check_hex = check_name_full[0:7]
- check_name = check_name_full[9:-1]
-
- required_panel_lambda = line_split.pop(0)
-
- full_check_name = current_region["shortName"] + " " + check_name
-
- if location_id == "Door" or location_id == "Laser":
- self.CHECKS_BY_HEX[check_hex] = {
- "checkName": full_check_name,
- "checkHex": check_hex,
- "region": current_region,
- "id": None,
- "panelType": location_id
- }
-
- self.CHECKS_BY_NAME[self.CHECKS_BY_HEX[check_hex]["checkName"]] = self.CHECKS_BY_HEX[check_hex]
-
- self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[check_hex] = {
- "panels": parse_lambda(required_panel_lambda)
- }
-
- current_region["panels"].append(check_hex)
- continue
-
- required_item_lambda = line_split.pop(0)
-
- laser_names = {
- "Laser",
- "Laser Hedges",
- "Laser Pressure Plates",
- "Desert Laser Redirect"
- }
- is_vault_or_video = "Vault" in check_name or "Video" in check_name
-
- if "Discard" in check_name:
- location_type = "Discard"
- elif is_vault_or_video or check_name == "Tutorial Gate Close":
- location_type = "Vault"
- elif check_name in laser_names:
- location_type = "Laser"
- elif "Obelisk Side" in check_name:
- location_type = "Obelisk Side"
- full_check_name = check_name
- elif "EP" in check_name:
- location_type = "EP"
- else:
- location_type = "General"
-
- required_items = parse_lambda(required_item_lambda)
- required_panels = parse_lambda(required_panel_lambda)
-
- required_items = frozenset(required_items)
-
- requirement = {
- "panels": required_panels,
- "items": required_items
- }
-
- if location_type == "Obelisk Side":
- eps = set(list(required_panels)[0])
- eps -= {"Theater to Tunnels"}
-
- eps_ints = {int(h, 16) for h in eps}
-
- self.OBELISK_SIDE_ID_TO_EP_HEXES[int(check_hex, 16)] = eps_ints
- for ep_hex in eps:
- self.EP_TO_OBELISK_SIDE[ep_hex] = check_hex
-
- self.CHECKS_BY_HEX[check_hex] = {
- "checkName": full_check_name,
- "checkHex": check_hex,
- "region": current_region,
- "id": int(location_id),
- "panelType": location_type
- }
-
- self.ENTITY_ID_TO_NAME[check_hex] = full_check_name
-
- self.CHECKS_BY_NAME[self.CHECKS_BY_HEX[check_hex]["checkName"]] = self.CHECKS_BY_HEX[check_hex]
- self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[check_hex] = requirement
-
- current_region["panels"].append(check_hex)
-
- def __init__(self, lines=get_sigma_normal_logic()):
- # All regions with a list of panels in them and the connections to other regions, before logic adjustments
- self.ALL_REGIONS_BY_NAME = dict()
- self.STATIC_CONNECTIONS_BY_REGION_NAME = dict()
-
- self.CHECKS_BY_HEX = dict()
- self.CHECKS_BY_NAME = dict()
- self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict()
-
- self.OBELISK_SIDE_ID_TO_EP_HEXES = dict()
-
- self.EP_TO_OBELISK_SIDE = dict()
-
- self.ENTITY_ID_TO_NAME = dict()
-
- self.read_logic_file(lines)
-
-
-class StaticWitnessLogic:
- # Item data parsed from WitnessItems.txt
- all_items: Dict[str, ItemDefinition] = {}
- _progressive_lookup: Dict[str, str] = {}
-
- ALL_REGIONS_BY_NAME = dict()
- STATIC_CONNECTIONS_BY_REGION_NAME = dict()
-
- OBELISK_SIDE_ID_TO_EP_HEXES = dict()
-
- CHECKS_BY_HEX = dict()
- CHECKS_BY_NAME = dict()
- STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict()
-
- EP_TO_OBELISK_SIDE = dict()
-
- ENTITY_ID_TO_NAME = dict()
-
- @staticmethod
- def parse_items():
- """
- Parses currently defined items from WitnessItems.txt
- """
-
- lines: List[str] = get_items()
- current_category: ItemCategory = ItemCategory.SYMBOL
-
- for line in lines:
- # Skip empty lines and comments.
- if line == "" or line[0] == "#":
- continue
-
- # If this line is a category header, update our cached category.
- if line in CATEGORY_NAME_MAPPINGS.keys():
- current_category = CATEGORY_NAME_MAPPINGS[line]
- continue
-
- line_split = line.split(" - ")
-
- item_code = int(line_split[0])
- item_name = line_split[1]
- arguments: List[str] = line_split[2].split(",") if len(line_split) >= 3 else []
-
- if current_category in [ItemCategory.DOOR, ItemCategory.LASER]:
- # Map doors to IDs.
- StaticWitnessLogic.all_items[item_name] = DoorItemDefinition(item_code, current_category,
- arguments)
- elif current_category == ItemCategory.TRAP or current_category == ItemCategory.FILLER:
- # Read filler weights.
- weight = int(arguments[0]) if len(arguments) >= 1 else 1
- StaticWitnessLogic.all_items[item_name] = WeightedItemDefinition(item_code, current_category, weight)
- elif arguments:
- # Progressive items.
- StaticWitnessLogic.all_items[item_name] = ProgressiveItemDefinition(item_code, current_category,
- arguments)
- for child_item in arguments:
- StaticWitnessLogic._progressive_lookup[child_item] = item_name
- else:
- StaticWitnessLogic.all_items[item_name] = ItemDefinition(item_code, current_category)
-
- @staticmethod
- def get_parent_progressive_item(item_name: str):
- """
- Returns the name of the item's progressive parent, if there is one, or the item's name if not.
- """
- return StaticWitnessLogic._progressive_lookup.get(item_name, item_name)
-
- @lazy
- def sigma_expert(self) -> StaticWitnessLogicObj:
- return StaticWitnessLogicObj(get_sigma_expert_logic())
-
- @lazy
- def sigma_normal(self) -> StaticWitnessLogicObj:
- return StaticWitnessLogicObj(get_sigma_normal_logic())
-
- @lazy
- def vanilla(self) -> StaticWitnessLogicObj:
- return StaticWitnessLogicObj(get_vanilla_logic())
-
- def __init__(self):
- self.parse_items()
-
- self.ALL_REGIONS_BY_NAME.update(self.sigma_normal.ALL_REGIONS_BY_NAME)
- self.STATIC_CONNECTIONS_BY_REGION_NAME.update(self.sigma_normal.STATIC_CONNECTIONS_BY_REGION_NAME)
-
- self.CHECKS_BY_HEX.update(self.sigma_normal.CHECKS_BY_HEX)
- self.CHECKS_BY_NAME.update(self.sigma_normal.CHECKS_BY_NAME)
- self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX.update(self.sigma_normal.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX)
-
- self.OBELISK_SIDE_ID_TO_EP_HEXES.update(self.sigma_normal.OBELISK_SIDE_ID_TO_EP_HEXES)
-
- self.EP_TO_OBELISK_SIDE.update(self.sigma_normal.EP_TO_OBELISK_SIDE)
-
- self.ENTITY_ID_TO_NAME.update(self.sigma_normal.ENTITY_ID_TO_NAME)
diff --git a/worlds/witness/test/__init__.py b/worlds/witness/test/__init__.py
new file mode 100644
index 000000000000..0a24467feab2
--- /dev/null
+++ b/worlds/witness/test/__init__.py
@@ -0,0 +1,161 @@
+from test.bases import WorldTestBase
+from test.general import gen_steps, setup_multiworld
+from test.multiworld.test_multiworlds import MultiworldTestBase
+from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union, cast
+
+from BaseClasses import CollectionState, Entrance, Item, Location, Region
+
+from .. import WitnessWorld
+
+
+class WitnessTestBase(WorldTestBase):
+ game = "The Witness"
+ player: ClassVar[int] = 1
+
+ world: WitnessWorld
+
+ def can_beat_game_with_items(self, items: Iterable[Item]) -> bool:
+ """
+ Check that the items listed are enough to beat the game.
+ """
+
+ state = CollectionState(self.multiworld)
+ for item in items:
+ state.collect(item)
+ return state.multiworld.can_beat_game(state)
+
+ def assert_dependency_on_event_item(self, spot: Union[Location, Region, Entrance], item_name: str) -> None:
+ """
+ WorldTestBase.assertAccessDependency, but modified & simplified to work with event items
+ """
+ event_items = [item for item in self.multiworld.get_items() if item.name == item_name]
+ self.assertTrue(event_items, f"Event item {item_name} does not exist.")
+
+ event_locations = [cast(Location, event_item.location) for event_item in event_items]
+
+ # Checking for an access dependency on an event item requires a bit of extra work,
+ # as state.remove forces a sweep, which will pick up the event item again right after we tried to remove it.
+ # So, we temporarily set the access rules of the event locations to be impossible.
+ original_rules = {event_location.name: event_location.access_rule for event_location in event_locations}
+ for event_location in event_locations:
+ event_location.access_rule = lambda _: False
+
+ # We can't use self.assertAccessDependency here, it doesn't work for event items. (As of 2024-06-30)
+ test_state = self.multiworld.get_all_state(False)
+
+ self.assertFalse(spot.can_reach(test_state), f"{spot.name} is reachable without {item_name}")
+
+ test_state.collect(event_items[0])
+
+ self.assertTrue(spot.can_reach(test_state), f"{spot.name} is not reachable despite having {item_name}")
+
+ # Restore original access rules.
+ for event_location in event_locations:
+ event_location.access_rule = original_rules[event_location.name]
+
+ def assert_location_exists(self, location_name: str, strict_check: bool = True) -> None:
+ """
+ Assert that a location exists in this world.
+ If strict_check, also make sure that this (non-event) location COULD exist.
+ """
+
+ if strict_check:
+ self.assertIn(location_name, self.world.location_name_to_id, f"Location {location_name} can never exist")
+
+ try:
+ self.world.get_location(location_name)
+ except KeyError:
+ self.fail(f"Location {location_name} does not exist.")
+
+ def assert_location_does_not_exist(self, location_name: str, strict_check: bool = True) -> None:
+ """
+ Assert that a location exists in this world.
+ If strict_check, be explicit about whether the location could exist in the first place.
+ """
+
+ if strict_check:
+ self.assertIn(location_name, self.world.location_name_to_id, f"Location {location_name} can never exist")
+
+ self.assertRaises(
+ KeyError,
+ lambda _: self.world.get_location(location_name),
+ f"Location {location_name} exists, but is not supposed to.",
+ )
+
+ def assert_can_beat_with_minimally(self, required_item_counts: Mapping[str, int]) -> None:
+ """
+ Assert that the specified mapping of items is enough to beat the game,
+ and that having one less of any item would result in the game being unbeatable.
+ """
+ # Find the actual items
+ found_items = [item for item in self.multiworld.get_items() if item.name in required_item_counts]
+ actual_items: Dict[str, List[Item]] = {item_name: [] for item_name in required_item_counts}
+ for item in found_items:
+ if len(actual_items[item.name]) < required_item_counts[item.name]:
+ actual_items[item.name].append(item)
+
+ # Assert that enough items exist in the item pool to satisfy the specified required counts
+ for item_name, item_objects in actual_items.items():
+ self.assertEqual(
+ len(item_objects),
+ required_item_counts[item_name],
+ f"Couldn't find {required_item_counts[item_name]} copies of item {item_name} available in the pool, "
+ f"only found {len(item_objects)}",
+ )
+
+ # assert that multiworld is beatable with the items specified
+ self.assertTrue(
+ self.can_beat_game_with_items(item for items in actual_items.values() for item in items),
+ f"Could not beat game with items: {required_item_counts}",
+ )
+
+ # assert that one less copy of any item would result in the multiworld being unbeatable
+ for item_name, item_objects in actual_items.items():
+ with self.subTest(f"Verify cannot beat game with one less copy of {item_name}"):
+ removed_item = item_objects.pop()
+ self.assertFalse(
+ self.can_beat_game_with_items(item for items in actual_items.values() for item in items),
+ f"Game was beatable despite having {len(item_objects)} copies of {item_name} "
+ f"instead of the specified {required_item_counts[item_name]}",
+ )
+ item_objects.append(removed_item)
+
+
+class WitnessMultiworldTestBase(MultiworldTestBase):
+ options_per_world: List[Dict[str, Any]]
+ common_options: Dict[str, Any] = {}
+
+ def setUp(self) -> None:
+ """
+ Set up a multiworld with multiple players, each using different options.
+ """
+
+ self.multiworld = setup_multiworld([WitnessWorld] * len(self.options_per_world), ())
+
+ for world, options in zip(self.multiworld.worlds.values(), self.options_per_world):
+ for option_name, option_value in {**self.common_options, **options}.items():
+ option = getattr(world.options, option_name)
+ self.assertIsNotNone(option)
+
+ option.value = option.from_any(option_value).value
+
+ self.assertSteps(gen_steps)
+
+ def collect_by_name(self, item_names: Union[str, Iterable[str]], player: int) -> List[Item]:
+ """
+ Collect all copies of a specified item name (or list of item names) for a player in the multiworld item pool.
+ """
+
+ items = self.get_items_by_name(item_names, player)
+ for item in items:
+ self.multiworld.state.collect(item)
+ return items
+
+ def get_items_by_name(self, item_names: Union[str, Iterable[str]], player: int) -> List[Item]:
+ """
+ Return all copies of a specified item name (or list of item names) for a player in the multiworld item pool.
+ """
+
+ if isinstance(item_names, str):
+ item_names = (item_names,)
+ return [item for item in self.multiworld.itempool if item.name in item_names and item.player == player]
diff --git a/worlds/witness/test/test_auto_elevators.py b/worlds/witness/test/test_auto_elevators.py
new file mode 100644
index 000000000000..16b1b5a56d37
--- /dev/null
+++ b/worlds/witness/test/test_auto_elevators.py
@@ -0,0 +1,66 @@
+from ..test import WitnessMultiworldTestBase, WitnessTestBase
+
+
+class TestElevatorsComeToYou(WitnessTestBase):
+ options = {
+ "elevators_come_to_you": True,
+ "shuffle_doors": "mixed",
+ "shuffle_symbols": False,
+ }
+
+ def test_bunker_laser(self) -> None:
+ """
+ In elevators_come_to_you, Bunker can be entered from the back.
+ This means that you can access the laser with just Bunker Elevator Control (Panel).
+ It also means that you can, for example, access UV Room with the Control and the Elevator Room Entry Door.
+ """
+
+ self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", self.player))
+
+ self.collect_by_name("Bunker Elevator Control (Panel)")
+
+ self.assertTrue(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", self.player))
+ self.assertFalse(self.multiworld.state.can_reach("Bunker UV Room 2", "Location", self.player))
+
+ self.collect_by_name("Bunker Elevator Room Entry (Door)")
+ self.collect_by_name("Bunker Drop-Down Door Controls (Panel)")
+
+ self.assertTrue(self.multiworld.state.can_reach("Bunker UV Room 2", "Location", self.player))
+
+
+class TestElevatorsComeToYouBleed(WitnessMultiworldTestBase):
+ options_per_world = [
+ {
+ "elevators_come_to_you": False,
+ },
+ {
+ "elevators_come_to_you": True,
+ },
+ {
+ "elevators_come_to_you": False,
+ },
+ ]
+
+ common_options = {
+ "shuffle_symbols": False,
+ "shuffle_doors": "panels",
+ }
+
+ def test_correct_access_per_player(self) -> None:
+ """
+ Test that in a multiworld with players that alternate the elevators_come_to_you option,
+ the actual behavior alternates as well and doesn't bleed over from slot to slot.
+ (This is essentially a "does connection info bleed over" test).
+ """
+
+ self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 1))
+ self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 2))
+ self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 3))
+
+ self.collect_by_name(["Bunker Elevator Control (Panel)"], 1)
+ self.collect_by_name(["Bunker Elevator Control (Panel)"], 2)
+ self.collect_by_name(["Bunker Elevator Control (Panel)"], 3)
+
+ self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 1))
+ self.assertTrue(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 2))
+ self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 3))
diff --git a/worlds/witness/test/test_disable_non_randomized.py b/worlds/witness/test/test_disable_non_randomized.py
new file mode 100644
index 000000000000..e7cb1597b2ba
--- /dev/null
+++ b/worlds/witness/test/test_disable_non_randomized.py
@@ -0,0 +1,37 @@
+from ..rules import _has_lasers
+from ..test import WitnessTestBase
+
+
+class TestDisableNonRandomized(WitnessTestBase):
+ options = {
+ "disable_non_randomized_puzzles": True,
+ "shuffle_doors": "panels",
+ "early_symbol_item": False,
+ }
+
+ def test_locations_got_disabled_and_alternate_activation_triggers_work(self) -> None:
+ """
+ Test the different behaviors of the disable_non_randomized mode:
+
+ 1. Unrandomized locations like Orchard Apple Tree 5 are disabled.
+ 2. Certain doors or lasers that would usually be activated by unrandomized panels depend on event items instead.
+ 3. These alternate activations are tied to solving Discarded Panels.
+ """
+
+ with self.subTest("Test that unrandomized locations are disabled."):
+ self.assert_location_does_not_exist("Orchard Apple Tree 5")
+
+ with self.subTest("Test that alternate activation trigger events exist."):
+ self.assert_dependency_on_event_item(
+ self.world.get_entrance("Town Tower After Third Door to Town Tower Top"),
+ "Town Tower 4th Door Opens",
+ )
+
+ with self.subTest("Test that alternate activation triggers award lasers."):
+ self.assertFalse(_has_lasers(1, self.world, False)(self.multiworld.state))
+
+ self.collect_by_name("Triangles")
+
+ # Alternate triggers yield Bunker Laser (Mountainside Discard) and Monastery Laser (Desert Discard)
+ self.assertTrue(_has_lasers(2, self.world, False)(self.multiworld.state))
+ self.assertFalse(_has_lasers(3, self.world, False)(self.multiworld.state))
diff --git a/worlds/witness/test/test_door_shuffle.py b/worlds/witness/test/test_door_shuffle.py
new file mode 100644
index 000000000000..0e38c32d69e2
--- /dev/null
+++ b/worlds/witness/test/test_door_shuffle.py
@@ -0,0 +1,24 @@
+from ..test import WitnessTestBase
+
+
+class TestIndividualDoors(WitnessTestBase):
+ options = {
+ "shuffle_doors": "doors",
+ "door_groupings": "off",
+ }
+
+ def test_swamp_laser_shortcut(self) -> None:
+ """
+ Test that Door Shuffle grants early access to Swamp Laser from the back shortcut.
+ """
+
+ self.assertTrue(self.get_items_by_name("Swamp Laser Shortcut (Door)"))
+
+ self.assertAccessDependency(
+ ["Swamp Laser Panel"],
+ [
+ ["Swamp Laser Shortcut (Door)"],
+ ["Swamp Red Underwater Exit (Door)"],
+ ],
+ only_check_listed=True,
+ )
diff --git a/worlds/witness/test/test_ep_shuffle.py b/worlds/witness/test/test_ep_shuffle.py
new file mode 100644
index 000000000000..342390916675
--- /dev/null
+++ b/worlds/witness/test/test_ep_shuffle.py
@@ -0,0 +1,54 @@
+from ..test import WitnessTestBase
+
+
+class TestIndividualEPs(WitnessTestBase):
+ options = {
+ "shuffle_EPs": "individual",
+ "EP_difficulty": "normal",
+ "obelisk_keys": True,
+ "disable_non_randomized_puzzles": True,
+ "shuffle_postgame": False,
+ "victory_condition": "mountain_box_short",
+ "early_caves": "off",
+ }
+
+ def test_correct_eps_exist_and_are_locked(self) -> None:
+ """
+ Test that EP locations exist in shuffle_EPs, but only the ones that actually should (based on options)
+ """
+
+ # Test Tutorial First Hallways EP as a proxy for "EPs exist at all"
+ # Don't wrap in a subtest - If this fails, there is no point.
+ self.assert_location_exists("Tutorial First Hallway EP")
+
+ with self.subTest("Test that disable_non_randomized disables Monastery Garden Left EP"):
+ self.assert_location_does_not_exist("Monastery Garden Left EP")
+
+ with self.subTest("Test that shuffle_postgame being off disables postgame EPs."):
+ self.assert_location_does_not_exist("Caves Skylight EP")
+
+ with self.subTest("Test that ep_difficulty being set to normal excludes tedious EPs."):
+ self.assert_location_does_not_exist("Shipwreck Couch EP")
+
+ with self.subTest("Test that EPs are being locked by Obelisk Keys."):
+ self.assertAccessDependency(["Desert Sand Snake EP"], [["Desert Obelisk Key"]], True)
+
+
+class TestObeliskSides(WitnessTestBase):
+ options = {
+ "shuffle_EPs": "obelisk_sides",
+ "EP_difficulty": "eclipse",
+ "shuffle_vault_boxes": True,
+ "shuffle_postgame": True,
+ }
+
+ def test_eclipse_required_for_town_side_6(self) -> None:
+ """
+ Test that Obelisk Sides require the appropriate event items from the individual EPs.
+ Specifically, assert that Town Obelisk Side 6 needs Theater Eclipse EP.
+ This doubles as a test for Theater Eclipse EP existing with the right options.
+ """
+
+ self.assert_dependency_on_event_item(
+ self.world.get_location("Town Obelisk Side 6"), "Town Obelisk Side 6 - Theater Eclipse EP"
+ )
diff --git a/worlds/witness/test/test_lasers.py b/worlds/witness/test/test_lasers.py
new file mode 100644
index 000000000000..f09897ce4053
--- /dev/null
+++ b/worlds/witness/test/test_lasers.py
@@ -0,0 +1,185 @@
+from ..test import WitnessTestBase
+
+
+class TestSymbolsRequiredToWinElevatorNormal(WitnessTestBase):
+ options = {
+ "shuffle_lasers": True,
+ "puzzle_randomization": "sigma_normal",
+ "mountain_lasers": 1,
+ "victory_condition": "elevator",
+ "early_symbol_item": False,
+ }
+
+ def test_symbols_to_win(self) -> None:
+ """
+ In symbol shuffle, the only way to reach the Elevator is through Mountain Entry by descending the Mountain.
+ This requires a very specific set of symbol items per puzzle randomization mode.
+ In this case, we check Sigma Normal Puzzles.
+ """
+
+ exact_requirement = {
+ "Monastery Laser": 1,
+ "Progressive Dots": 2,
+ "Progressive Stars": 2,
+ "Progressive Symmetry": 2,
+ "Black/White Squares": 1,
+ "Colored Squares": 1,
+ "Shapers": 1,
+ "Rotated Shapers": 1,
+ "Eraser": 1,
+ }
+
+ self.assert_can_beat_with_minimally(exact_requirement)
+
+
+class TestSymbolsRequiredToWinElevatorExpert(WitnessTestBase):
+ options = {
+ "shuffle_lasers": True,
+ "mountain_lasers": 1,
+ "victory_condition": "elevator",
+ "early_symbol_item": False,
+ "puzzle_randomization": "sigma_expert",
+ }
+
+ def test_symbols_to_win(self) -> None:
+ """
+ In symbol shuffle, the only way to reach the Elevator is through Mountain Entry by descending the Mountain.
+ This requires a very specific set of symbol items per puzzle randomization mode.
+ In this case, we check Sigma Expert Puzzles.
+ """
+
+ exact_requirement = {
+ "Monastery Laser": 1,
+ "Progressive Dots": 2,
+ "Progressive Stars": 2,
+ "Progressive Symmetry": 2,
+ "Black/White Squares": 1,
+ "Colored Squares": 1,
+ "Shapers": 1,
+ "Rotated Shapers": 1,
+ "Negative Shapers": 1,
+ "Eraser": 1,
+ "Triangles": 1,
+ }
+
+ self.assert_can_beat_with_minimally(exact_requirement)
+
+
+class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase):
+ options = {
+ "shuffle_lasers": True,
+ "mountain_lasers": 1,
+ "victory_condition": "elevator",
+ "early_symbol_item": False,
+ "puzzle_randomization": "none",
+ }
+
+ def test_symbols_to_win(self) -> None:
+ """
+ In symbol shuffle, the only way to reach the Elevator is through Mountain Entry by descending the Mountain.
+ This requires a very specific set of symbol items per puzzle randomization mode.
+ In this case, we check Vanilla Puzzles.
+ """
+
+ exact_requirement = {
+ "Monastery Laser": 1,
+ "Progressive Dots": 2,
+ "Progressive Stars": 2,
+ "Progressive Symmetry": 1,
+ "Black/White Squares": 1,
+ "Colored Squares": 1,
+ "Shapers": 1,
+ "Rotated Shapers": 1,
+ "Eraser": 1,
+ }
+
+ self.assert_can_beat_with_minimally(exact_requirement)
+
+
+class TestPanelsRequiredToWinElevator(WitnessTestBase):
+ options = {
+ "shuffle_lasers": True,
+ "mountain_lasers": 1,
+ "victory_condition": "elevator",
+ "early_symbol_item": False,
+ "shuffle_symbols": False,
+ "shuffle_doors": "panels",
+ "door_groupings": "off",
+ }
+
+ def test_panels_to_win(self) -> None:
+ """
+ In door panel shuffle , the only way to reach the Elevator is through Mountain Entry by descending the Mountain.
+ This requires some control panels for each of the Mountain Floors.
+ """
+
+ exact_requirement = {
+ "Desert Laser": 1,
+ "Town Desert Laser Redirect Control (Panel)": 1,
+ "Mountain Floor 1 Light Bridge (Panel)": 1,
+ "Mountain Floor 2 Light Bridge Near (Panel)": 1,
+ "Mountain Floor 2 Light Bridge Far (Panel)": 1,
+ "Mountain Floor 2 Elevator Control (Panel)": 1,
+ }
+
+ self.assert_can_beat_with_minimally(exact_requirement)
+
+
+class TestDoorsRequiredToWinElevator(WitnessTestBase):
+ options = {
+ "shuffle_lasers": True,
+ "mountain_lasers": 1,
+ "victory_condition": "elevator",
+ "early_symbol_item": False,
+ "shuffle_symbols": False,
+ "shuffle_doors": "doors",
+ "door_groupings": "off",
+ }
+
+ def test_doors_to_elevator_paths(self) -> None:
+ """
+ In remote door shuffle, there are three ways to win.
+
+ - Through the normal route (Mountain Entry -> Descend through Mountain -> Reach Bottom Floor)
+ - Through the Caves using the Caves Shortcuts (Caves -> Reach Bottom Floor)
+ - Through the Caves via Challenge (Tunnels -> Challenge -> Caves -> Reach Bottom Floor)
+ """
+
+ with self.subTest("Test Elevator victory in shuffle_doors through Mountain Entry."):
+ exact_requirement = {
+ "Monastery Laser": 1,
+ "Mountain Floor 1 Exit (Door)": 1,
+ "Mountain Floor 2 Staircase Near (Door)": 1,
+ "Mountain Floor 2 Staircase Far (Door)": 1,
+ "Mountain Floor 2 Exit (Door)": 1,
+ "Mountain Bottom Floor Giant Puzzle Exit (Door)": 1,
+ "Mountain Bottom Floor Pillars Room Entry (Door)": 1,
+ }
+
+ self.assert_can_beat_with_minimally(exact_requirement)
+
+ with self.subTest("Test Elevator victory in shuffle_doors through Caves Shortcuts."):
+ exact_requirement = {
+ "Monastery Laser": 1, # Elevator Panel itself has a laser lock
+ "Caves Mountain Shortcut (Door)": 1,
+ "Caves Entry (Door)": 1,
+ "Mountain Bottom Floor Rock (Door)": 1,
+ "Mountain Bottom Floor Pillars Room Entry (Door)": 1,
+ }
+
+ self.assert_can_beat_with_minimally(exact_requirement)
+
+ with self.subTest("Test Elevator victory in shuffle_doors through Tunnels->Challenge->Caves."):
+ exact_requirement = {
+ "Monastery Laser": 1, # Elevator Panel itself has a laser lock
+ "Windmill Entry (Door)": 1,
+ "Tunnels Theater Shortcut (Door)": 1,
+ "Tunnels Entry (Door)": 1,
+ "Challenge Entry (Door)": 1,
+ "Caves Pillar Door": 1,
+ "Caves Entry (Door)": 1,
+ "Mountain Bottom Floor Rock (Door)": 1,
+ "Mountain Bottom Floor Pillars Room Entry (Door)": 1,
+ }
+
+ self.assert_can_beat_with_minimally(exact_requirement)
diff --git a/worlds/witness/test/test_roll_other_options.py b/worlds/witness/test/test_roll_other_options.py
new file mode 100644
index 000000000000..71743c326038
--- /dev/null
+++ b/worlds/witness/test/test_roll_other_options.py
@@ -0,0 +1,58 @@
+from ..test import WitnessTestBase
+
+# These are just some random options combinations, just to catch whether I broke anything obvious
+
+
+class TestExpertNonRandomizedEPs(WitnessTestBase):
+ options = {
+ "disable_non_randomized": True,
+ "puzzle_randomization": "sigma_expert",
+ "shuffle_EPs": "individual",
+ "ep_difficulty": "eclipse",
+ "victory_condition": "challenge",
+ "shuffle_discarded_panels": False,
+ "shuffle_boat": False,
+ }
+
+
+class TestVanillaAutoElevatorsPanels(WitnessTestBase):
+ options = {
+ "puzzle_randomization": "none",
+ "elevators_come_to_you": True,
+ "shuffle_doors": "panels",
+ "victory_condition": "mountain_box_short",
+ "early_caves": True,
+ "shuffle_vault_boxes": True,
+ "mountain_lasers": 11,
+ }
+
+
+class TestMiscOptions(WitnessTestBase):
+ options = {
+ "death_link": True,
+ "death_link_amnesty": 3,
+ "laser_hints": True,
+ "hint_amount": 40,
+ "area_hint_percentage": 100,
+ }
+
+
+class TestMaxEntityShuffle(WitnessTestBase):
+ options = {
+ "shuffle_symbols": False,
+ "shuffle_doors": "mixed",
+ "shuffle_EPs": "individual",
+ "obelisk_keys": True,
+ "shuffle_lasers": "anywhere",
+ "victory_condition": "mountain_box_long",
+ }
+
+
+class TestPostgameGroupedDoors(WitnessTestBase):
+ options = {
+ "shuffle_postgame": True,
+ "shuffle_discarded_panels": True,
+ "shuffle_doors": "doors",
+ "door_groupings": "regional",
+ "victory_condition": "elevator",
+ }
diff --git a/worlds/witness/test/test_symbol_shuffle.py b/worlds/witness/test/test_symbol_shuffle.py
new file mode 100644
index 000000000000..8012480075a7
--- /dev/null
+++ b/worlds/witness/test/test_symbol_shuffle.py
@@ -0,0 +1,74 @@
+from ..test import WitnessMultiworldTestBase, WitnessTestBase
+
+
+class TestSymbols(WitnessTestBase):
+ options = {
+ "early_symbol_item": False,
+ }
+
+ def test_progressive_symbols(self) -> None:
+ """
+ Test that Dots & Full Dots are correctly replaced by 2x Progressive Dots,
+ and test that Dots puzzles and Full Dots puzzles require 1 and 2 copies of this item respectively.
+ """
+
+ progressive_dots = self.get_items_by_name("Progressive Dots")
+ self.assertEqual(len(progressive_dots), 2)
+
+ self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
+ self.assertFalse(
+ self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player)
+ )
+
+ self.collect(progressive_dots.pop())
+
+ self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
+ self.assertFalse(
+ self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player)
+ )
+
+ self.collect(progressive_dots.pop())
+
+ self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player))
+ self.assertTrue(
+ self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player)
+ )
+
+
+class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
+ options_per_world = [
+ {
+ "puzzle_randomization": "sigma_normal",
+ },
+ {
+ "puzzle_randomization": "sigma_expert",
+ },
+ {
+ "puzzle_randomization": "none",
+ },
+ ]
+
+ common_options = {
+ "shuffle_discarded_panels": True,
+ "early_symbol_item": False,
+ }
+
+ def test_arrows_exist_and_are_required_in_expert_seeds_only(self) -> None:
+ """
+ In sigma_expert, Discarded Panels require Arrows.
+ In sigma_normal, Discarded Panels require Triangles, and Arrows shouldn't exist at all as an item.
+ """
+
+ with self.subTest("Test that Arrows exist only in the expert seed."):
+ self.assertFalse(self.get_items_by_name("Arrows", 1))
+ self.assertTrue(self.get_items_by_name("Arrows", 2))
+ self.assertFalse(self.get_items_by_name("Arrows", 3))
+
+ with self.subTest("Test that Discards ask for Triangles in normal, but Arrows in expert."):
+ desert_discard = "0x17CE7"
+ triangles = frozenset({frozenset({"Triangles"})})
+ arrows = frozenset({frozenset({"Arrows"})})
+
+ self.assertEqual(self.multiworld.worlds[1].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
+ self.assertEqual(self.multiworld.worlds[2].player_logic.REQUIREMENTS_BY_HEX[desert_discard], arrows)
+ self.assertEqual(self.multiworld.worlds[3].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
diff --git a/worlds/witness/utils.py b/worlds/witness/utils.py
deleted file mode 100644
index 72dc10fd0617..000000000000
--- a/worlds/witness/utils.py
+++ /dev/null
@@ -1,182 +0,0 @@
-from functools import lru_cache
-from math import floor
-from typing import List, Collection
-from pkgutil import get_data
-
-
-def build_weighted_int_list(inputs: Collection[float], total: int) -> List[int]:
- """
- Converts a list of floats to a list of ints of a given length, using the Largest Remainder Method.
- """
-
- # Scale the inputs to sum to the desired total.
- scale_factor: float = total / sum(inputs)
- scaled_input = [x * scale_factor for x in inputs]
-
- # Generate whole number counts, always rounding down.
- rounded_output: List[int] = [floor(x) for x in scaled_input]
- rounded_sum = sum(rounded_output)
-
- # If the output's total is insufficient, increment the value that has the largest remainder until we meet our goal.
- remainders: List[float] = [real - rounded for real, rounded in zip(scaled_input, rounded_output)]
- while rounded_sum < total:
- max_remainder = max(remainders)
- if max_remainder == 0:
- break
-
- # Consume the remainder and increment the total for the given target.
- max_remainder_index = remainders.index(max_remainder)
- remainders[max_remainder_index] = 0
- rounded_output[max_remainder_index] += 1
- rounded_sum += 1
-
- return rounded_output
-
-
-def define_new_region(region_string):
- """
- Returns a region object by parsing a line in the logic file
- """
-
- region_string = region_string[:-1]
- line_split = region_string.split(" - ")
-
- region_name_full = line_split.pop(0)
-
- region_name_split = region_name_full.split(" (")
-
- region_name = region_name_split[0]
- region_name_simple = region_name_split[1][:-1]
-
- options = set()
-
- for _ in range(len(line_split) // 2):
- connected_region = line_split.pop(0)
- corresponding_lambda = line_split.pop(0)
-
- options.add(
- (connected_region, parse_lambda(corresponding_lambda))
- )
-
- region_obj = {
- "name": region_name,
- "shortName": region_name_simple,
- "panels": list()
- }
- return region_obj, options
-
-
-def parse_lambda(lambda_string):
- """
- Turns a lambda String literal like this: a | b & c
- into a set of sets like this: {{a}, {b, c}}
- The lambda has to be in DNF.
- """
- if lambda_string == "True":
- return frozenset([frozenset()])
- split_ands = set(lambda_string.split(" | "))
- lambda_set = frozenset({frozenset(a.split(" & ")) for a in split_ands})
-
- return lambda_set
-
-
-class lazy(object):
- def __init__(self, func, name=None):
- self.func = func
- self.name = name if name is not None else func.__name__
- self.__doc__ = func.__doc__
-
- def __get__(self, instance, class_):
- if instance is None:
- res = self.func(class_)
- setattr(class_, self.name, res)
- return res
- res = self.func(instance)
- setattr(instance, self.name, res)
- return res
-
-
-@lru_cache(maxsize=None)
-def get_adjustment_file(adjustment_file):
- data = get_data(__name__, adjustment_file).decode('utf-8')
- return [line.strip() for line in data.split("\n")]
-
-
-def get_disable_unrandomized_list():
- return get_adjustment_file("settings/Disable_Unrandomized.txt")
-
-
-def get_early_utm_list():
- return get_adjustment_file("settings/Early_UTM.txt")
-
-
-def get_symbol_shuffle_list():
- return get_adjustment_file("settings/Symbol_Shuffle.txt")
-
-
-def get_door_panel_shuffle_list():
- return get_adjustment_file("settings/Door_Panel_Shuffle.txt")
-
-
-def get_doors_simple_list():
- return get_adjustment_file("settings/Doors_Simple.txt")
-
-
-def get_doors_complex_list():
- return get_adjustment_file("settings/Doors_Complex.txt")
-
-
-def get_doors_max_list():
- return get_adjustment_file("settings/Doors_Max.txt")
-
-
-def get_laser_shuffle():
- return get_adjustment_file("settings/Laser_Shuffle.txt")
-
-
-def get_audio_logs():
- return get_adjustment_file("settings/Audio_Logs.txt")
-
-
-def get_ep_all_individual():
- return get_adjustment_file("settings/EP_Shuffle/EP_All.txt")
-
-
-def get_ep_obelisks():
- return get_adjustment_file("settings/EP_Shuffle/EP_Sides.txt")
-
-
-def get_ep_easy():
- return get_adjustment_file("settings/EP_Shuffle/EP_Easy.txt")
-
-
-def get_ep_no_eclipse():
- return get_adjustment_file("settings/EP_Shuffle/EP_NoEclipse.txt")
-
-
-def get_ep_no_caves():
- return get_adjustment_file("settings/EP_Shuffle/EP_NoCavesEPs.txt")
-
-
-def get_ep_no_mountain():
- return get_adjustment_file("settings/EP_Shuffle/EP_NoMountainEPs.txt")
-
-
-def get_ep_no_videos():
- return get_adjustment_file("settings/EP_Shuffle/EP_Videos.txt")
-
-
-def get_sigma_normal_logic():
- return get_adjustment_file("WitnessLogic.txt")
-
-
-def get_sigma_expert_logic():
- return get_adjustment_file("WitnessLogicExpert.txt")
-
-
-def get_vanilla_logic():
- return get_adjustment_file("WitnessLogicVanilla.txt")
-
-
-def get_items():
- return get_adjustment_file("WitnessItems.txt")
diff --git a/worlds/yoshisisland/Client.py b/worlds/yoshisisland/Client.py
new file mode 100644
index 000000000000..9b9e0ff52b87
--- /dev/null
+++ b/worlds/yoshisisland/Client.py
@@ -0,0 +1,147 @@
+import logging
+import struct
+import typing
+import time
+from struct import pack
+
+from NetUtils import ClientStatus, color
+from worlds.AutoSNIClient import SNIClient
+
+if typing.TYPE_CHECKING:
+ from SNIClient import SNIContext
+
+snes_logger = logging.getLogger("SNES")
+
+ROM_START = 0x000000
+WRAM_START = 0xF50000
+WRAM_SIZE = 0x20000
+SRAM_START = 0xE00000
+
+YOSHISISLAND_ROMHASH_START = 0x007FC0
+ROMHASH_SIZE = 0x15
+
+ITEMQUEUE_HIGH = WRAM_START + 0x1465
+ITEM_RECEIVED = WRAM_START + 0x1467
+DEATH_RECEIVED = WRAM_START + 0x7E23B0
+GAME_MODE = WRAM_START + 0x0118
+YOSHI_STATE = SRAM_START + 0x00AC
+DEATHLINK_ADDR = ROM_START + 0x06FC8C
+DEATHMUSIC_FLAG = WRAM_START + 0x004F
+DEATHFLAG = WRAM_START + 0x00DB
+DEATHLINKRECV = WRAM_START + 0x00E0
+GOALFLAG = WRAM_START + 0x14B6
+
+VALID_GAME_STATES = [0x0F, 0x10, 0x2C]
+
+
+class YoshisIslandSNIClient(SNIClient):
+ game = "Yoshi's Island"
+ patch_suffix = ".apyi"
+
+ async def deathlink_kill_player(self, ctx: "SNIContext") -> None:
+ from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
+ game_state = await snes_read(ctx, GAME_MODE, 0x1)
+ if game_state[0] != 0x0F:
+ return
+
+ yoshi_state = await snes_read(ctx, YOSHI_STATE, 0x1)
+ if yoshi_state[0] != 0x00:
+ return
+
+ snes_buffered_write(ctx, WRAM_START + 0x026A, bytes([0x01]))
+ snes_buffered_write(ctx, WRAM_START + 0x00E0, bytes([0x01]))
+ await snes_flush_writes(ctx)
+ ctx.death_state = DeathState.dead
+ ctx.last_death_link = time.time()
+
+ async def validate_rom(self, ctx: "SNIContext") -> bool:
+ from SNIClient import snes_read
+
+ rom_name = await snes_read(ctx, YOSHISISLAND_ROMHASH_START, ROMHASH_SIZE)
+ if rom_name is None or rom_name[:7] != b"YOSHIAP":
+ return False
+
+ ctx.game = self.game
+ ctx.items_handling = 0b111 # remote items
+ ctx.rom = rom_name
+
+ death_link = await snes_read(ctx, DEATHLINK_ADDR, 1)
+ if death_link:
+ await ctx.update_death_link(bool(death_link[0] & 0b1))
+ return True
+
+ async def game_watcher(self, ctx: "SNIContext") -> None:
+ from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
+
+ game_mode = await snes_read(ctx, GAME_MODE, 0x1)
+ item_received = await snes_read(ctx, ITEM_RECEIVED, 0x1)
+ game_music = await snes_read(ctx, DEATHMUSIC_FLAG, 0x1)
+ goal_flag = await snes_read(ctx, GOALFLAG, 0x1)
+
+ if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
+ death_flag = await snes_read(ctx, DEATHFLAG, 0x1)
+ deathlink_death = await snes_read(ctx, DEATHLINKRECV, 0x1)
+ currently_dead = (game_music[0] == 0x07 or game_mode[0] == 0x12 or
+ (death_flag[0] == 0x00 and game_mode[0] == 0x11)) and deathlink_death[0] == 0x00
+ await ctx.handle_deathlink_state(currently_dead)
+
+ if game_mode is None:
+ return
+
+ elif game_mode[0] not in VALID_GAME_STATES:
+ return
+ elif item_received[0] > 0x00:
+ return
+
+ from .Rom import item_values
+ rom = await snes_read(ctx, YOSHISISLAND_ROMHASH_START, ROMHASH_SIZE)
+ if rom != ctx.rom:
+ ctx.rom = None
+ return
+
+ if goal_flag[0] != 0x00:
+ await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
+ ctx.finished_game = True
+
+ new_checks = []
+ from .Rom import location_table
+
+ location_ram_data = await snes_read(ctx, WRAM_START + 0x1440, 0x80)
+ for loc_id, loc_data in location_table.items():
+ if loc_id not in ctx.locations_checked:
+ data = location_ram_data[loc_data[0] - 0x1440]
+ masked_data = data & (1 << loc_data[1])
+ bit_set = masked_data != 0
+ invert_bit = ((len(loc_data) >= 3) and loc_data[2])
+ if bit_set != invert_bit:
+ new_checks.append(loc_id)
+
+ for new_check_id in new_checks:
+ ctx.locations_checked.add(new_check_id)
+ location = ctx.location_names.lookup_in_game(new_check_id)
+ total_locations = len(ctx.missing_locations) + len(ctx.checked_locations)
+ snes_logger.info(f"New Check: {location} ({len(ctx.locations_checked)}/{total_locations})")
+ await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [new_check_id]}])
+
+ recv_count = await snes_read(ctx, ITEMQUEUE_HIGH, 2)
+ recv_index = struct.unpack("H", recv_count)[0]
+ if recv_index < len(ctx.items_received):
+ item = ctx.items_received[recv_index]
+ recv_index += 1
+ logging.info("Received %s from %s (%s) (%d/%d in list)" % (
+ color(ctx.item_names.lookup_in_game(item.item), "red", "bold"),
+ color(ctx.player_names[item.player], "yellow"),
+ ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received)))
+
+ snes_buffered_write(ctx, ITEMQUEUE_HIGH, pack("H", recv_index))
+ if item.item in item_values:
+ item_count = await snes_read(ctx, WRAM_START + item_values[item.item][0], 0x1)
+ increment = item_values[item.item][1]
+ new_item_count = item_count[0]
+ if increment > 1:
+ new_item_count = increment
+ else:
+ new_item_count += increment
+
+ snes_buffered_write(ctx, WRAM_START + item_values[item.item][0], bytes([new_item_count]))
+ await snes_flush_writes(ctx)
diff --git a/worlds/yoshisisland/Items.py b/worlds/yoshisisland/Items.py
new file mode 100644
index 000000000000..f30c7317798f
--- /dev/null
+++ b/worlds/yoshisisland/Items.py
@@ -0,0 +1,122 @@
+from typing import Dict, Set, Tuple, NamedTuple, Optional
+from BaseClasses import ItemClassification
+
+class ItemData(NamedTuple):
+ category: str
+ code: Optional[int]
+ classification: ItemClassification
+ amount: Optional[int] = 1
+
+item_table: Dict[str, ItemData] = {
+ "! Switch": ItemData("Items", 0x302050, ItemClassification.progression),
+ "Dashed Platform": ItemData("Items", 0x302051, ItemClassification.progression),
+ "Dashed Stairs": ItemData("Items", 0x302052, ItemClassification.progression),
+ "Beanstalk": ItemData("Items", 0x302053, ItemClassification.progression),
+ "Helicopter Morph": ItemData("Morphs", 0x302054, ItemClassification.progression),
+ "Spring Ball": ItemData("Items", 0x302055, ItemClassification.progression),
+ "Large Spring Ball": ItemData("Items", 0x302056, ItemClassification.progression),
+ "Arrow Wheel": ItemData("Items", 0x302057, ItemClassification.progression),
+ "Vanishing Arrow Wheel": ItemData("Items", 0x302058, ItemClassification.progression),
+ "Mole Tank Morph": ItemData("Morphs", 0x302059, ItemClassification.progression),
+ "Watermelon": ItemData("Items", 0x30205A, ItemClassification.progression),
+ "Ice Melon": ItemData("Items", 0x30205B, ItemClassification.progression),
+ "Fire Melon": ItemData("Items", 0x30205C, ItemClassification.progression),
+ "Super Star": ItemData("Items", 0x30205D, ItemClassification.progression),
+ "Car Morph": ItemData("Morphs", 0x30205E, ItemClassification.progression),
+ "Flashing Eggs": ItemData("Items", 0x30205F, ItemClassification.progression),
+ "Giant Eggs": ItemData("Items", 0x302060, ItemClassification.progression),
+ "Egg Launcher": ItemData("Items", 0x302061, ItemClassification.progression),
+ "Egg Plant": ItemData("Items", 0x302062, ItemClassification.progression),
+ "Submarine Morph": ItemData("Morphs", 0x302063, ItemClassification.progression),
+ "Chomp Rock": ItemData("Items", 0x302064, ItemClassification.progression),
+ "Poochy": ItemData("Items", 0x302065, ItemClassification.progression),
+ "Platform Ghost": ItemData("Items", 0x302066, ItemClassification.progression),
+ "Skis": ItemData("Items", 0x302067, ItemClassification.progression),
+ "Train Morph": ItemData("Morphs", 0x302068, ItemClassification.progression),
+ "Key": ItemData("Items", 0x302069, ItemClassification.progression),
+ "Middle Ring": ItemData("Items", 0x30206A, ItemClassification.progression),
+ "Bucket": ItemData("Items", 0x30206B, ItemClassification.progression),
+ "Tulip": ItemData("Items", 0x30206C, ItemClassification.progression),
+ "Egg Capacity Upgrade": ItemData("Items", 0x30206D, ItemClassification.progression, 5),
+ "Secret Lens": ItemData("Items", 0x302081, ItemClassification.progression),
+
+ "World 1 Gate": ItemData("Gates", 0x30206E, ItemClassification.progression),
+ "World 2 Gate": ItemData("Gates", 0x30206F, ItemClassification.progression),
+ "World 3 Gate": ItemData("Gates", 0x302070, ItemClassification.progression),
+ "World 4 Gate": ItemData("Gates", 0x302071, ItemClassification.progression),
+ "World 5 Gate": ItemData("Gates", 0x302072, ItemClassification.progression),
+ "World 6 Gate": ItemData("Gates", 0x302073, ItemClassification.progression),
+
+ "Extra 1": ItemData("Panels", 0x302074, ItemClassification.progression),
+ "Extra 2": ItemData("Panels", 0x302075, ItemClassification.progression),
+ "Extra 3": ItemData("Panels", 0x302076, ItemClassification.progression),
+ "Extra 4": ItemData("Panels", 0x302077, ItemClassification.progression),
+ "Extra 5": ItemData("Panels", 0x302078, ItemClassification.progression),
+ "Extra 6": ItemData("Panels", 0x302079, ItemClassification.progression),
+ "Extra Panels": ItemData("Panels", 0x30207A, ItemClassification.progression),
+
+ "Bonus 1": ItemData("Panels", 0x30207B, ItemClassification.progression),
+ "Bonus 2": ItemData("Panels", 0x30207C, ItemClassification.progression),
+ "Bonus 3": ItemData("Panels", 0x30207D, ItemClassification.progression),
+ "Bonus 4": ItemData("Panels", 0x30207E, ItemClassification.progression),
+ "Bonus 5": ItemData("Panels", 0x30207F, ItemClassification.progression),
+ "Bonus 6": ItemData("Panels", 0x302080, ItemClassification.progression),
+ "Bonus Panels": ItemData("Panels", 0x302082, ItemClassification.progression),
+
+ "Anytime Egg": ItemData("Consumable", 0x302083, ItemClassification.useful, 0),
+ "Anywhere Pow": ItemData("Consumable", 0x302084, ItemClassification.filler, 0),
+ "Winged Cloud Maker": ItemData("Consumable", 0x302085, ItemClassification.filler, 0),
+ "Pocket Melon": ItemData("Consumable", 0x302086, ItemClassification.filler, 0),
+ "Pocket Fire Melon": ItemData("Consumable", 0x302087, ItemClassification.filler, 0),
+ "Pocket Ice Melon": ItemData("Consumable", 0x302088, ItemClassification.filler, 0),
+ "Magnifying Glass": ItemData("Consumable", 0x302089, ItemClassification.filler, 0),
+ "+10 Stars": ItemData("Consumable", 0x30208A, ItemClassification.useful, 0),
+ "+20 Stars": ItemData("Consumable", 0x30208B, ItemClassification.useful, 0),
+ "1-Up": ItemData("Lives", 0x30208C, ItemClassification.filler, 0),
+ "2-Up": ItemData("Lives", 0x30208D, ItemClassification.filler, 0),
+ "3-Up": ItemData("Lives", 0x30208E, ItemClassification.filler, 0),
+ "10-Up": ItemData("Lives", 0x30208F, ItemClassification.useful, 5),
+ "Bonus Consumables": ItemData("Events", None, ItemClassification.progression, 0),
+ "Bandit Consumables": ItemData("Events", None, ItemClassification.progression, 0),
+ "Bandit Watermelons": ItemData("Events", None, ItemClassification.progression, 0),
+
+ "Fuzzy Trap": ItemData("Traps", 0x302090, ItemClassification.trap, 0),
+ "Reversal Trap": ItemData("Traps", 0x302091, ItemClassification.trap, 0),
+ "Darkness Trap": ItemData("Traps", 0x302092, ItemClassification.trap, 0),
+ "Freeze Trap": ItemData("Traps", 0x302093, ItemClassification.trap, 0),
+
+ "Boss Clear": ItemData("Events", None, ItemClassification.progression, 0),
+ "Piece of Luigi": ItemData("Items", 0x302095, ItemClassification.progression, 0),
+ "Saved Baby Luigi": ItemData("Events", None, ItemClassification.progression, 0)
+}
+
+filler_items: Tuple[str, ...] = (
+ "Anytime Egg",
+ "Anywhere Pow",
+ "Winged Cloud Maker",
+ "Pocket Melon",
+ "Pocket Fire Melon",
+ "Pocket Ice Melon",
+ "Magnifying Glass",
+ "+10 Stars",
+ "+20 Stars",
+ "1-Up",
+ "2-Up",
+ "3-Up"
+)
+
+trap_items: Tuple[str, ...] = (
+ "Fuzzy Trap",
+ "Reversal Trap",
+ "Darkness Trap",
+ "Freeze Trap"
+)
+
+def get_item_names_per_category() -> Dict[str, Set[str]]:
+ categories: Dict[str, Set[str]] = {}
+
+ for name, data in item_table.items():
+ if data.category != "Events":
+ categories.setdefault(data.category, set()).add(name)
+
+ return categories
diff --git a/worlds/yoshisisland/Locations.py b/worlds/yoshisisland/Locations.py
new file mode 100644
index 000000000000..bc0855260eb4
--- /dev/null
+++ b/worlds/yoshisisland/Locations.py
@@ -0,0 +1,355 @@
+from typing import List, Optional, NamedTuple, TYPE_CHECKING
+
+from .Options import PlayerGoal, MinigameChecks
+from worlds.generic.Rules import CollectionRule
+
+if TYPE_CHECKING:
+ from . import YoshisIslandWorld
+from .level_logic import YoshiLogic
+
+
+class LocationData(NamedTuple):
+ region: str
+ name: str
+ code: Optional[int]
+ LevelID: int
+ rule: CollectionRule = lambda state: True
+
+
+def get_locations(world: Optional["YoshisIslandWorld"]) -> List[LocationData]:
+ if world:
+ logic = YoshiLogic(world)
+
+ location_table: List[LocationData] = [
+ LocationData("1-1", "Make Eggs, Throw Eggs: Red Coins", 0x305020, 0x00),
+ LocationData("1-1", "Make Eggs, Throw Eggs: Flowers", 0x305021, 0x00),
+ LocationData("1-1", "Make Eggs, Throw Eggs: Stars", 0x305022, 0x00),
+ LocationData("1-1", "Make Eggs, Throw Eggs: Level Clear", 0x305023, 0x00),
+
+ LocationData("1-2", "Watch Out Below!: Red Coins", 0x305024, 0x01),
+ LocationData("1-2", "Watch Out Below!: Flowers", 0x305025, 0x01),
+ LocationData("1-2", "Watch Out Below!: Stars", 0x305026, 0x01),
+ LocationData("1-2", "Watch Out Below!: Level Clear", 0x305027, 0x01),
+
+ LocationData("1-3", "The Cave Of Chomp Rock: Red Coins", 0x305028, 0x02),
+ LocationData("1-3", "The Cave Of Chomp Rock: Flowers", 0x305029, 0x02),
+ LocationData("1-3", "The Cave Of Chomp Rock: Stars", 0x30502A, 0x02),
+ LocationData("1-3", "The Cave Of Chomp Rock: Level Clear", 0x30502B, 0x02),
+
+ LocationData("1-4", "Burt The Bashful's Fort: Red Coins", 0x30502C, 0x03),
+ LocationData("1-4", "Burt The Bashful's Fort: Flowers", 0x30502D, 0x03),
+ LocationData("1-4", "Burt The Bashful's Fort: Stars", 0x30502E, 0x03),
+ LocationData("1-4", "Burt The Bashful's Fort: Level Clear", 0x30502F, 0x03, lambda state: logic._14CanFightBoss(state)),
+ LocationData("Burt The Bashful's Boss Room", "Burt The Bashful's Boss Room", None, 0x03, lambda state: logic._14Boss(state)),
+
+ LocationData("1-5", "Hop! Hop! Donut Lifts: Red Coins", 0x305031, 0x04),
+ LocationData("1-5", "Hop! Hop! Donut Lifts: Flowers", 0x305032, 0x04),
+ LocationData("1-5", "Hop! Hop! Donut Lifts: Stars", 0x305033, 0x04),
+ LocationData("1-5", "Hop! Hop! Donut Lifts: Level Clear", 0x305034, 0x04),
+
+ LocationData("1-6", "Shy-Guys On Stilts: Red Coins", 0x305035, 0x05),
+ LocationData("1-6", "Shy-Guys On Stilts: Flowers", 0x305036, 0x05),
+ LocationData("1-6", "Shy-Guys On Stilts: Stars", 0x305037, 0x05),
+ LocationData("1-6", "Shy-Guys On Stilts: Level Clear", 0x305038, 0x05),
+
+ LocationData("1-7", "Touch Fuzzy Get Dizzy: Red Coins", 0x305039, 0x06),
+ LocationData("1-7", "Touch Fuzzy Get Dizzy: Flowers", 0x30503A, 0x06),
+ LocationData("1-7", "Touch Fuzzy Get Dizzy: Stars", 0x30503B, 0x06),
+ LocationData("1-7", "Touch Fuzzy Get Dizzy: Level Clear", 0x30503C, 0x06),
+ LocationData("1-7", "Touch Fuzzy Get Dizzy: Gather Coins", None, 0x06, lambda state: logic._17Game(state)),
+
+ LocationData("1-8", "Salvo The Slime's Castle: Red Coins", 0x30503D, 0x07),
+ LocationData("1-8", "Salvo The Slime's Castle: Flowers", 0x30503E, 0x07),
+ LocationData("1-8", "Salvo The Slime's Castle: Stars", 0x30503F, 0x07),
+ LocationData("1-8", "Salvo The Slime's Castle: Level Clear", 0x305040, 0x07, lambda state: logic._18CanFightBoss(state)),
+ LocationData("Salvo The Slime's Boss Room", "Salvo The Slime's Boss Room", None, 0x07, lambda state: logic._18Boss(state)),
+
+ LocationData("1-Bonus", "Flip Cards", None, 0x09),
+ ############################################################################################
+ LocationData("2-1", "Visit Koopa And Para-Koopa: Red Coins", 0x305041, 0x0C),
+ LocationData("2-1", "Visit Koopa And Para-Koopa: Flowers", 0x305042, 0x0C),
+ LocationData("2-1", "Visit Koopa And Para-Koopa: Stars", 0x305043, 0x0C),
+ LocationData("2-1", "Visit Koopa And Para-Koopa: Level Clear", 0x305044, 0x0C),
+
+ LocationData("2-2", "The Baseball Boys: Red Coins", 0x305045, 0x0D),
+ LocationData("2-2", "The Baseball Boys: Flowers", 0x305046, 0x0D),
+ LocationData("2-2", "The Baseball Boys: Stars", 0x305047, 0x0D),
+ LocationData("2-2", "The Baseball Boys: Level Clear", 0x305048, 0x0D),
+
+ LocationData("2-3", "What's Gusty Taste Like?: Red Coins", 0x305049, 0x0E),
+ LocationData("2-3", "What's Gusty Taste Like?: Flowers", 0x30504A, 0x0E),
+ LocationData("2-3", "What's Gusty Taste Like?: Stars", 0x30504B, 0x0E),
+ LocationData("2-3", "What's Gusty Taste Like?: Level Clear", 0x30504C, 0x0E),
+
+ LocationData("2-4", "Bigger Boo's Fort: Red Coins", 0x30504D, 0x0F),
+ LocationData("2-4", "Bigger Boo's Fort: Flowers", 0x30504E, 0x0F),
+ LocationData("2-4", "Bigger Boo's Fort: Stars", 0x30504F, 0x0F),
+ LocationData("2-4", "Bigger Boo's Fort: Level Clear", 0x305050, 0x0F, lambda state: logic._24CanFightBoss(state)),
+ LocationData("Bigger Boo's Boss Room", "Bigger Boo's Boss Room", None, 0x0F, lambda state: logic._24Boss(state)),
+
+ LocationData("2-5", "Watch Out For Lakitu: Red Coins", 0x305051, 0x10),
+ LocationData("2-5", "Watch Out For Lakitu: Flowers", 0x305052, 0x10),
+ LocationData("2-5", "Watch Out For Lakitu: Stars", 0x305053, 0x10),
+ LocationData("2-5", "Watch Out For Lakitu: Level Clear", 0x305054, 0x10),
+
+ LocationData("2-6", "The Cave Of The Mystery Maze: Red Coins", 0x305055, 0x11),
+ LocationData("2-6", "The Cave Of The Mystery Maze: Flowers", 0x305056, 0x11),
+ LocationData("2-6", "The Cave Of The Mystery Maze: Stars", 0x305057, 0x11),
+ LocationData("2-6", "The Cave Of The Mystery Maze: Level Clear", 0x305058, 0x11),
+ LocationData("2-6", "The Cave Of the Mystery Maze: Seed Spitting Contest", None, 0x11, lambda state: logic._26Game(state)),
+
+ LocationData("2-7", "Lakitu's Wall: Red Coins", 0x305059, 0x12),
+ LocationData("2-7", "Lakitu's Wall: Flowers", 0x30505A, 0x12),
+ LocationData("2-7", "Lakitu's Wall: Stars", 0x30505B, 0x12),
+ LocationData("2-7", "Lakitu's Wall: Level Clear", 0x30505C, 0x12),
+ LocationData("2-7", "Lakitu's Wall: Gather Coins", None, 0x12, lambda state: logic._27Game(state)),
+
+ LocationData("2-8", "The Potted Ghost's Castle: Red Coins", 0x30505D, 0x13),
+ LocationData("2-8", "The Potted Ghost's Castle: Flowers", 0x30505E, 0x13),
+ LocationData("2-8", "The Potted Ghost's Castle: Stars", 0x30505F, 0x13),
+ LocationData("2-8", "The Potted Ghost's Castle: Level Clear", 0x305060, 0x13, lambda state: logic._28CanFightBoss(state)),
+ LocationData("Roger The Ghost's Boss Room", "Roger The Ghost's Boss Room", None, 0x13, lambda state: logic._28Boss(state)),
+ ###############################################################################################
+ LocationData("3-1", "Welcome To Monkey World!: Red Coins", 0x305061, 0x18),
+ LocationData("3-1", "Welcome To Monkey World!: Flowers", 0x305062, 0x18),
+ LocationData("3-1", "Welcome To Monkey World!: Stars", 0x305063, 0x18),
+ LocationData("3-1", "Welcome To Monkey World!: Level Clear", 0x305064, 0x18),
+
+ LocationData("3-2", "Jungle Rhythm...: Red Coins", 0x305065, 0x19),
+ LocationData("3-2", "Jungle Rhythm...: Flowers", 0x305066, 0x19),
+ LocationData("3-2", "Jungle Rhythm...: Stars", 0x305067, 0x19),
+ LocationData("3-2", "Jungle Rhythm...: Level Clear", 0x305068, 0x19),
+
+ LocationData("3-3", "Nep-Enuts' Domain: Red Coins", 0x305069, 0x1A),
+ LocationData("3-3", "Nep-Enuts' Domain: Flowers", 0x30506A, 0x1A),
+ LocationData("3-3", "Nep-Enuts' Domain: Stars", 0x30506B, 0x1A),
+ LocationData("3-3", "Nep-Enuts' Domain: Level Clear", 0x30506C, 0x1A),
+
+ LocationData("3-4", "Prince Froggy's Fort: Red Coins", 0x30506D, 0x1B),
+ LocationData("3-4", "Prince Froggy's Fort: Flowers", 0x30506E, 0x1B),
+ LocationData("3-4", "Prince Froggy's Fort: Stars", 0x30506F, 0x1B),
+ LocationData("3-4", "Prince Froggy's Fort: Level Clear", 0x305070, 0x1B, lambda state: logic._34CanFightBoss(state)),
+ LocationData("Prince Froggy's Boss Room", "Prince Froggy's Boss Room", None, 0x1B, lambda state: logic._34Boss(state)),
+
+ LocationData("3-5", "Jammin' Through The Trees: Red Coins", 0x305071, 0x1C),
+ LocationData("3-5", "Jammin' Through The Trees: Flowers", 0x305072, 0x1C),
+ LocationData("3-5", "Jammin' Through The Trees: Stars", 0x305073, 0x1C),
+ LocationData("3-5", "Jammin' Through The Trees: Level Clear", 0x305074, 0x1C),
+
+ LocationData("3-6", "The Cave Of Harry Hedgehog: Red Coins", 0x305075, 0x1D),
+ LocationData("3-6", "The Cave Of Harry Hedgehog: Flowers", 0x305076, 0x1D),
+ LocationData("3-6", "The Cave Of Harry Hedgehog: Stars", 0x305077, 0x1D),
+ LocationData("3-6", "The Cave Of Harry Hedgehog: Level Clear", 0x305078, 0x1D),
+
+ LocationData("3-7", "Monkeys' Favorite Lake: Red Coins", 0x305079, 0x1E),
+ LocationData("3-7", "Monkeys' Favorite Lake: Flowers", 0x30507A, 0x1E),
+ LocationData("3-7", "Monkeys' Favorite Lake: Stars", 0x30507B, 0x1E),
+ LocationData("3-7", "Monkeys' Favorite Lake: Level Clear", 0x30507C, 0x1E),
+
+ LocationData("3-8", "Naval Piranha's Castle: Red Coins", 0x30507D, 0x1F),
+ LocationData("3-8", "Naval Piranha's Castle: Flowers", 0x30507E, 0x1F),
+ LocationData("3-8", "Naval Piranha's Castle: Stars", 0x30507F, 0x1F),
+ LocationData("3-8", "Naval Piranha's Castle: Level Clear", 0x305080, 0x1F, lambda state: logic._38CanFightBoss(state)),
+ LocationData("Naval Piranha's Boss Room", "Naval Piranha's Boss Room", None, 0x1F, lambda state: logic._38Boss(state)),
+
+ LocationData("3-Bonus", "Drawing Lots", None, 0x21),
+ ##############################################################################################
+ LocationData("4-1", "GO! GO! MARIO!!: Red Coins", 0x305081, 0x24),
+ LocationData("4-1", "GO! GO! MARIO!!: Flowers", 0x305082, 0x24),
+ LocationData("4-1", "GO! GO! MARIO!!: Stars", 0x305083, 0x24),
+ LocationData("4-1", "GO! GO! MARIO!!: Level Clear", 0x305084, 0x24),
+
+ LocationData("4-2", "The Cave Of The Lakitus: Red Coins", 0x305085, 0x25),
+ LocationData("4-2", "The Cave Of The Lakitus: Flowers", 0x305086, 0x25),
+ LocationData("4-2", "The Cave Of The Lakitus: Stars", 0x305087, 0x25),
+ LocationData("4-2", "The Cave Of The Lakitus: Level Clear", 0x305088, 0x25),
+
+ LocationData("4-3", "Don't Look Back!: Red Coins", 0x305089, 0x26),
+ LocationData("4-3", "Don't Look Back!: Flowers", 0x30508A, 0x26),
+ LocationData("4-3", "Don't Look Back!: Stars", 0x30508B, 0x26),
+ LocationData("4-3", "Don't Look Back!: Level Clear", 0x30508C, 0x26),
+
+ LocationData("4-4", "Marching Milde's Fort: Red Coins", 0x30508D, 0x27),
+ LocationData("4-4", "Marching Milde's Fort: Flowers", 0x30508E, 0x27),
+ LocationData("4-4", "Marching Milde's Fort: Stars", 0x30508F, 0x27),
+ LocationData("4-4", "Marching Milde's Fort: Level Clear", 0x305090, 0x27, lambda state: logic._44CanFightBoss(state)),
+ LocationData("Marching Milde's Boss Room", "Marching Milde's Boss Room", None, 0x27, lambda state: logic._44Boss(state)),
+
+ LocationData("4-5", "Chomp Rock Zone: Red Coins", 0x305091, 0x28),
+ LocationData("4-5", "Chomp Rock Zone: Flowers", 0x305092, 0x28),
+ LocationData("4-5", "Chomp Rock Zone: Stars", 0x305093, 0x28),
+ LocationData("4-5", "Chomp Rock Zone: Level Clear", 0x305094, 0x28),
+
+ LocationData("4-6", "Lake Shore Paradise: Red Coins", 0x305095, 0x29),
+ LocationData("4-6", "Lake Shore Paradise: Flowers", 0x305096, 0x29),
+ LocationData("4-6", "Lake Shore Paradise: Stars", 0x305097, 0x29),
+ LocationData("4-6", "Lake Shore Paradise: Level Clear", 0x305098, 0x29),
+
+ LocationData("4-7", "Ride Like The Wind: Red Coins", 0x305099, 0x2A),
+ LocationData("4-7", "Ride Like The Wind: Flowers", 0x30509A, 0x2A),
+ LocationData("4-7", "Ride Like The Wind: Stars", 0x30509B, 0x2A),
+ LocationData("4-7", "Ride Like The Wind: Level Clear", 0x30509C, 0x2A),
+ LocationData("4-7", "Ride Like The Wind: Gather Coins", None, 0x2A, lambda state: logic._47Game(state)),
+
+ LocationData("4-8", "Hookbill The Koopa's Castle: Red Coins", 0x30509D, 0x2B),
+ LocationData("4-8", "Hookbill The Koopa's Castle: Flowers", 0x30509E, 0x2B),
+ LocationData("4-8", "Hookbill The Koopa's Castle: Stars", 0x30509F, 0x2B),
+ LocationData("4-8", "Hookbill The Koopa's Castle: Level Clear", 0x3050A0, 0x2B, lambda state: logic._48CanFightBoss(state)),
+ LocationData("Hookbill The Koopa's Boss Room", "Hookbill The Koopa's Boss Room", None, 0x2B, lambda state: logic._48Boss(state)),
+
+ LocationData("4-Bonus", "Match Cards", None, 0x2D),
+ ######################################################################################################
+ LocationData("5-1", "BLIZZARD!!!: Red Coins", 0x3050A1, 0x30),
+ LocationData("5-1", "BLIZZARD!!!: Flowers", 0x3050A2, 0x30),
+ LocationData("5-1", "BLIZZARD!!!: Stars", 0x3050A3, 0x30),
+ LocationData("5-1", "BLIZZARD!!!: Level Clear", 0x3050A4, 0x30),
+
+ LocationData("5-2", "Ride The Ski Lifts: Red Coins", 0x3050A5, 0x31),
+ LocationData("5-2", "Ride The Ski Lifts: Flowers", 0x3050A6, 0x31),
+ LocationData("5-2", "Ride The Ski Lifts: Stars", 0x3050A7, 0x31),
+ LocationData("5-2", "Ride The Ski Lifts: Level Clear", 0x3050A8, 0x31),
+
+ LocationData("5-3", "Danger - Icy Conditions Ahead: Red Coins", 0x3050A9, 0x32),
+ LocationData("5-3", "Danger - Icy Conditions Ahead: Flowers", 0x3050AA, 0x32),
+ LocationData("5-3", "Danger - Icy Conditions Ahead: Stars", 0x3050AB, 0x32),
+ LocationData("5-3", "Danger - Icy Conditions Ahead: Level Clear", 0x3050AC, 0x32),
+
+ LocationData("5-4", "Sluggy The Unshaven's Fort: Red Coins", 0x3050AD, 0x33),
+ LocationData("5-4", "Sluggy The Unshaven's Fort: Flowers", 0x3050AE, 0x33),
+ LocationData("5-4", "Sluggy The Unshaven's Fort: Stars", 0x3050AF, 0x33),
+ LocationData("5-4", "Sluggy The Unshaven's Fort: Level Clear", 0x3050B0, 0x33, lambda state: logic._54CanFightBoss(state)),
+ LocationData("Sluggy The Unshaven's Boss Room", "Sluggy The Unshaven's Boss Room", None, 0x33, lambda state: logic._54Boss(state)),
+
+ LocationData("5-5", "Goonie Rides!: Red Coins", 0x3050B1, 0x34),
+ LocationData("5-5", "Goonie Rides!: Flowers", 0x3050B2, 0x34),
+ LocationData("5-5", "Goonie Rides!: Stars", 0x3050B3, 0x34),
+ LocationData("5-5", "Goonie Rides!: Level Clear", 0x3050B4, 0x34),
+
+ LocationData("5-6", "Welcome To Cloud World: Red Coins", 0x3050B5, 0x35),
+ LocationData("5-6", "Welcome To Cloud World: Flowers", 0x3050B6, 0x35),
+ LocationData("5-6", "Welcome To Cloud World: Stars", 0x3050B7, 0x35),
+ LocationData("5-6", "Welcome To Cloud World: Level Clear", 0x3050B8, 0x35),
+
+ LocationData("5-7", "Shifting Platforms Ahead: Red Coins", 0x3050B9, 0x36),
+ LocationData("5-7", "Shifting Platforms Ahead: Flowers", 0x3050BA, 0x36),
+ LocationData("5-7", "Shifting Platforms Ahead: Stars", 0x3050BB, 0x36),
+ LocationData("5-7", "Shifting Platforms Ahead: Level Clear", 0x3050BC, 0x36),
+
+ LocationData("5-8", "Raphael The Raven's Castle: Red Coins", 0x3050BD, 0x37),
+ LocationData("5-8", "Raphael The Raven's Castle: Flowers", 0x3050BE, 0x37),
+ LocationData("5-8", "Raphael The Raven's Castle: Stars", 0x3050BF, 0x37),
+ LocationData("5-8", "Raphael The Raven's Castle: Level Clear", 0x3050C0, 0x37, lambda state: logic._58CanFightBoss(state)),
+ LocationData("Raphael The Raven's Boss Room", "Raphael The Raven's Boss Room", None, 0x37, lambda state: logic._58Boss(state)),
+ ######################################################################################################
+
+ LocationData("6-1", "Scary Skeleton Goonies!: Red Coins", 0x3050C1, 0x3C),
+ LocationData("6-1", "Scary Skeleton Goonies!: Flowers", 0x3050C2, 0x3C),
+ LocationData("6-1", "Scary Skeleton Goonies!: Stars", 0x3050C3, 0x3C),
+ LocationData("6-1", "Scary Skeleton Goonies!: Level Clear", 0x3050C4, 0x3C),
+
+ LocationData("6-2", "The Cave Of The Bandits: Red Coins", 0x3050C5, 0x3D),
+ LocationData("6-2", "The Cave Of The Bandits: Flowers", 0x3050C6, 0x3D),
+ LocationData("6-2", "The Cave Of The Bandits: Stars", 0x3050C7, 0x3D),
+ LocationData("6-2", "The Cave Of The Bandits: Level Clear", 0x3050C8, 0x3D),
+
+ LocationData("6-3", "Beware The Spinning Logs: Red Coins", 0x3050C9, 0x3E),
+ LocationData("6-3", "Beware The Spinning Logs: Flowers", 0x3050CA, 0x3E),
+ LocationData("6-3", "Beware The Spinning Logs: Stars", 0x3050CB, 0x3E),
+ LocationData("6-3", "Beware The Spinning Logs: Level Clear", 0x3050CC, 0x3E),
+
+ LocationData("6-4", "Tap-Tap The Red Nose's Fort: Red Coins", 0x3050CD, 0x3F),
+ LocationData("6-4", "Tap-Tap The Red Nose's Fort: Flowers", 0x3050CE, 0x3F),
+ LocationData("6-4", "Tap-Tap The Red Nose's Fort: Stars", 0x3050CF, 0x3F),
+ LocationData("6-4", "Tap-Tap The Red Nose's Fort: Level Clear", 0x3050D0, 0x3F, lambda state: logic._64CanFightBoss(state)),
+ LocationData("Tap-Tap The Red Nose's Boss Room", "Tap-Tap The Red Nose's Boss Room", None, 0x3F, lambda state: logic._64Boss(state)),
+
+ LocationData("6-5", "The Very Loooooong Cave: Red Coins", 0x3050D1, 0x40),
+ LocationData("6-5", "The Very Loooooong Cave: Flowers", 0x3050D2, 0x40),
+ LocationData("6-5", "The Very Loooooong Cave: Stars", 0x3050D3, 0x40),
+ LocationData("6-5", "The Very Loooooong Cave: Level Clear", 0x3050D4, 0x40),
+
+ LocationData("6-6", "The Deep, Underground Maze: Red Coins", 0x3050D5, 0x41),
+ LocationData("6-6", "The Deep, Underground Maze: Flowers", 0x3050D6, 0x41),
+ LocationData("6-6", "The Deep, Underground Maze: Stars", 0x3050D7, 0x41),
+ LocationData("6-6", "The Deep, Underground Maze: Level Clear", 0x3050D8, 0x41),
+
+ LocationData("6-7", "KEEP MOVING!!!!: Red Coins", 0x3050D9, 0x42),
+ LocationData("6-7", "KEEP MOVING!!!!: Flowers", 0x3050DA, 0x42),
+ LocationData("6-7", "KEEP MOVING!!!!: Stars", 0x3050DB, 0x42),
+ LocationData("6-7", "KEEP MOVING!!!!: Level Clear", 0x3050DC, 0x42),
+
+ LocationData("6-8", "King Bowser's Castle: Red Coins", 0x3050DD, 0x43),
+ LocationData("6-8", "King Bowser's Castle: Flowers", 0x3050DE, 0x43),
+ LocationData("6-8", "King Bowser's Castle: Stars", 0x3050DF, 0x43)
+ ]
+
+ if not world or world.options.extras_enabled:
+ location_table += [
+ LocationData("1-Extra", "Poochy Ain't Stupid: Red Coins", 0x3050E0, 0x08),
+ LocationData("1-Extra", "Poochy Ain't Stupid: Flowers", 0x3050E1, 0x08),
+ LocationData("1-Extra", "Poochy Ain't Stupid: Stars", 0x3050E2, 0x08),
+ LocationData("1-Extra", "Poochy Ain't Stupid: Level Clear", 0x3050E3, 0x08),
+
+ LocationData("2-Extra", "Hit That Switch!!: Red Coins", 0x3050E4, 0x14),
+ LocationData("2-Extra", "Hit That Switch!!: Flowers", 0x3050E5, 0x14),
+ LocationData("2-Extra", "Hit That Switch!!: Stars", 0x3050E6, 0x14),
+ LocationData("2-Extra", "Hit That Switch!!: Level Clear", 0x3050E7, 0x14),
+
+ LocationData("3-Extra", "More Monkey Madness: Red Coins", 0x3050E8, 0x20),
+ LocationData("3-Extra", "More Monkey Madness: Flowers", 0x3050E9, 0x20),
+ LocationData("3-Extra", "More Monkey Madness: Stars", 0x3050EA, 0x20),
+ LocationData("3-Extra", "More Monkey Madness: Level Clear", 0x3050EB, 0x20),
+
+ LocationData("4-Extra", "The Impossible? Maze: Red Coins", 0x3050EC, 0x2C),
+ LocationData("4-Extra", "The Impossible? Maze: Flowers", 0x3050ED, 0x2C),
+ LocationData("4-Extra", "The Impossible? Maze: Stars", 0x3050EE, 0x2C),
+ LocationData("4-Extra", "The Impossible? Maze: Level Clear", 0x3050EF, 0x2C),
+
+ LocationData("5-Extra", "Kamek's Revenge: Red Coins", 0x3050F0, 0x38),
+ LocationData("5-Extra", "Kamek's Revenge: Flowers", 0x3050F1, 0x38),
+ LocationData("5-Extra", "Kamek's Revenge: Stars", 0x3050F2, 0x38),
+ LocationData("5-Extra", "Kamek's Revenge: Level Clear", 0x3050F3, 0x38),
+
+ LocationData("6-Extra", "Castles - Masterpiece Set: Red Coins", 0x3050F4, 0x44),
+ LocationData("6-Extra", "Castles - Masterpiece Set: Flowers", 0x3050F5, 0x44),
+ LocationData("6-Extra", "Castles - Masterpiece Set: Stars", 0x3050F6, 0x44),
+ LocationData("6-Extra", "Castles - Masterpiece Set: Level Clear", 0x3050F7, 0x44),
+ ]
+
+ if not world or world.options.minigame_checks in {MinigameChecks.option_bandit_games, MinigameChecks.option_both}:
+ location_table += [
+ LocationData("1-3", "The Cave Of Chomp Rock: Bandit Game", 0x3050F8, 0x02, lambda state: logic._13Game(state)),
+ LocationData("1-7", "Touch Fuzzy Get Dizzy: Bandit Game", 0x3050F9, 0x06, lambda state: logic._17Game(state)),
+ LocationData("2-1", "Visit Koopa And Para-Koopa: Bandit Game", 0x3050FA, 0x0C, lambda state: logic._21Game(state)),
+ LocationData("2-3", "What's Gusty Taste Like?: Bandit Game", 0x3050FB, 0x0E, lambda state: logic._23Game(state)),
+ LocationData("2-6", "The Cave Of The Mystery Maze: Bandit Game", 0x3050FC, 0x11, lambda state: logic._26Game(state)),
+ LocationData("2-7", "Lakitu's Wall: Bandit Game", 0x3050FD, 0x12, lambda state: logic._27Game(state)),
+ LocationData("3-2", "Jungle Rhythm...: Bandit Game", 0x3050FE, 0x19, lambda state: logic._32Game(state)),
+ LocationData("3-7", "Monkeys' Favorite Lake: Bandit Game", 0x3050FF, 0x1E, lambda state: logic._37Game(state)),
+ LocationData("4-2", "The Cave Of The Lakitus: Bandit Game", 0x305100, 0x25, lambda state: logic._42Game(state)),
+ LocationData("4-6", "Lake Shore Paradise: Bandit Game", 0x305101, 0x29, lambda state: logic._46Game(state)),
+ LocationData("4-7", "Ride Like The Wind: Bandit Game", 0x305102, 0x2A, lambda state: logic._47Game(state)),
+ LocationData("5-1", "BLIZZARD!!!: Bandit Game", 0x305103, 0x30, lambda state: logic._51Game(state)),
+ LocationData("6-1", "Scary Skeleton Goonies!: Bandit Game", 0x305104, 0x3C, lambda state: logic._61Game(state)),
+ LocationData("6-7", "KEEP MOVING!!!!: Bandit Game", 0x305105, 0x42, lambda state: logic._67Game(state)),
+ ]
+
+ if not world or world.options.minigame_checks in {MinigameChecks.option_bonus_games, MinigameChecks.option_both}:
+ location_table += [
+ LocationData("1-Bonus", "Flip Cards: Victory", 0x305106, 0x09),
+ LocationData("2-Bonus", "Scratch And Match: Victory", 0x305107, 0x15),
+ LocationData("3-Bonus", "Drawing Lots: Victory", 0x305108, 0x21),
+ LocationData("4-Bonus", "Match Cards: Victory", 0x305109, 0x2D),
+ LocationData("5-Bonus", "Roulette: Victory", 0x30510A, 0x39),
+ LocationData("6-Bonus", "Slot Machine: Victory", 0x30510B, 0x45),
+ ]
+ if not world or world.options.goal == PlayerGoal.option_luigi_hunt:
+ location_table += [
+ LocationData("Overworld", "Reconstituted Luigi", None, 0x00, lambda state: logic.reconstitute_luigi(state)),
+ ]
+ if not world or world.options.goal == PlayerGoal.option_bowser:
+ location_table += [
+ LocationData("Bowser's Room", "King Bowser's Castle: Level Clear", None, 0x43, lambda state: logic._68Clear(state)),
+ ]
+
+ return location_table
diff --git a/worlds/yoshisisland/Options.py b/worlds/yoshisisland/Options.py
new file mode 100644
index 000000000000..07d0436f6fde
--- /dev/null
+++ b/worlds/yoshisisland/Options.py
@@ -0,0 +1,296 @@
+from dataclasses import dataclass
+from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, PerGameCommonOptions
+
+
+class ExtrasEnabled(Toggle):
+ """If enabled, the more difficult Extra stages will be added into logic. Otherwise, they will be inaccessible."""
+ display_name = "Include Extra Stages"
+
+
+class SplitExtras(Toggle):
+ """If enabled, Extra stages will be unlocked individually. Otherwise, there will be a single 'Extra Panels' item that unlocks all of them."""
+ display_name = "Split Extra Stages"
+
+
+class SplitBonus(Toggle):
+ """If enabled, Bonus Games will be unlocked individually. Otherwise, there will be a single 'Bonus Panels' item that unlocks all of them."""
+ display_name = "Split Bonus Games"
+
+
+class ObjectVis(Choice):
+ """This will determine the default visibility of objects revealed by the Magnifying Glass.
+ Strict Logic will expect the Secret Lens or a Magnifying Glass to interact with hidden clouds containing stars if they are not set to visible by default."""
+ display_name = "Hidden Object Visibility"
+ option_none = 0
+ option_coins_only = 1
+ option_clouds_only = 2
+ option_full = 3
+ default = 1
+
+
+class SoftlockPrevention(DefaultOnToggle):
+ """If enabled, hold R + X to warp to the last used Middle Ring, or the start of the level if none have been activated."""
+ display_name = "Softlock Prevention Code"
+
+
+class StageLogic(Choice):
+ """This determines what logic mode the stages will use.
+ Strict: Best for casual players or those new to playing Yoshi's Island in AP. Level requirements won't expect anything too difficult of the player.
+ Loose: Recommended for veterans of the original game. Won't expect anything too difficult, but may expect unusual platforming or egg throws.
+ Expert: Logic may expect advanced knowledge or memorization of level layouts, as well as jumps the player may only have one chance to make without restarting."""
+ display_name = "Stage Logic"
+ option_strict = 0
+ option_loose = 1
+ option_expert = 2
+ # option_glitched = 3
+ default = 0
+
+
+class ShuffleMiddleRings(Toggle):
+ """If enabled, Middle Rings will be added to the item pool."""
+ display_name = "Shuffle Middle Rings"
+
+
+class ShuffleSecretLens(Toggle):
+ """If enabled, the Secret Lens will be added to the item pool.
+ The Secret Lens will act as a permanent Magnifying Glass."""
+ display_name = "Add Secret Lens"
+
+
+class DisableAutoScrollers(Toggle):
+ """If enabled, will disable autoscrolling during levels, except during levels which cannot function otherwise."""
+ display_name = "Disable Autoscrolling"
+
+
+class ItemLogic(Toggle):
+ """This will enable logic to expect consumables to be used from the inventory in place of some major items.
+ Logic will expect you to have access to an Overworld bonus game, or a bandit game to get the necessary items.
+ Logic will NOT expect grinding end-of-level bonus games, or any inventory consumables received from checks.
+ Casual logic will only expect consumables from Overworld games; Loose and Expert may expect them from bandit games."""
+ display_name = "Consumable Logic"
+
+
+class MinigameChecks(Choice):
+ """This will set minigame victories to give Archipelago checks.
+ This will not randomize minigames amongst themselves, and is compatible with item logic.
+ Bonus games will be expected to be cleared from the Overworld, not the end of levels.
+ Additionally, 1-Up bonus games will accept any profit as a victory."""
+ display_name = "Minigame Reward Checks"
+ option_none = 0
+ option_bandit_games = 1
+ option_bonus_games = 2
+ option_both = 3
+ default = 0
+
+
+class StartingWorld(Choice):
+ """This sets which world you start in. Other worlds can be accessed by receiving a Gate respective to that world."""
+ display_name = "Starting World"
+ option_world_1 = 0
+ option_world_2 = 1
+ option_world_3 = 2
+ option_world_4 = 3
+ option_world_5 = 4
+ option_world_6 = 5
+ default = 0
+
+
+class StartingLives(Range):
+ """This sets the amount of lives Yoshi will have upon loading the game."""
+ display_name = "Starting Life Count"
+ range_start = 1
+ range_end = 999
+ default = 3
+
+
+class PlayerGoal(Choice):
+ """This sets the goal. Bowser goal requires defeating Bowser at the end of 6-8, while Luigi Hunt requires collecting all required Luigi Pieces."""
+ display_name = "Goal"
+ option_bowser = 0
+ option_luigi_hunt = 1
+ default = 0
+
+
+class LuigiPiecesReq(Range):
+ """This will set how many Luigi Pieces are required to trigger a victory."""
+ display_name = "Luigi Pieces Required"
+ range_start = 1
+ range_end = 100
+ default = 25
+
+
+class LuigiPiecesAmt(Range):
+ """This will set how many Luigi Pieces are in the item pool.
+ If the number in the pool is lower than the number required,
+ the amount in the pool will be randomized, with the minimum being the amount required."""
+ display_name = "Amount of Luigi Pieces"
+ range_start = 1
+ range_end = 100
+ default = 50
+
+
+class FinalLevelBosses(Range):
+ """This sets how many bosses need to be defeated to access 6-8.
+ You can check this in-game by pressing SELECT while in any level."""
+ display_name = "Bosses Required for 6-8 Unlock"
+ range_start = 0
+ range_end = 11
+ default = 5
+
+
+class FinalBossBosses(Range):
+ """This sets how many bosses need to be defeated to access the boss of 6-8.
+ You can check this in-game by pressing SELECT while in any level."""
+ display_name = "Bosses Required for 6-8 Clear"
+ range_start = 0
+ range_end = 11
+ default = 0
+
+
+class BowserDoor(Choice):
+ """This will set which route you take through 6-8.
+ Manual: You go through the door that you hit with an egg, as normal.
+ Doors: Route will be forced to be the door chosen here, regardless of which door you hit.
+ Gauntlet: You will be forced to go through all 4 routes in order before the final hallway."""
+ display_name = "Bowser's Castle Doors"
+ option_manual = 0
+ option_door_1 = 1
+ option_door_2 = 2
+ option_door_3 = 3
+ option_door_4 = 4
+ option_gauntlet = 5
+ default = 0
+
+
+class BossShuffle(Toggle):
+ """This whill shuffle which boss each boss door will lead to. Each boss can only appear once, and Baby Bowser is left alone."""
+ display_name = "Boss Shuffle"
+
+
+class LevelShuffle(Choice):
+ """Disabled: All levels will appear in their normal location.
+ Bosses Guaranteed: All worlds will have a boss on -4 and -8.
+ Full: Worlds may have more than 2 or no bosses in them.
+ Regardless of the setting, 6-8 and Extra stages are not shuffled."""
+ display_name = "Level Shuffle"
+ option_disabled = 0
+ option_bosses_guaranteed = 1
+ option_full = 2
+ default = 0
+
+
+class YoshiColors(Choice):
+ """Sets the Yoshi color for each level.
+ Normal will use the vanilla colors.
+ Random order will generate a random order of colors that will be used in each level. The stage 1 color will be used for Extra stages, and 6-8.
+ Random color will generate a random color for each stage.
+ Singularity will use a single color defined under 'Singularity Yoshi Color' for use in all stages."""
+ display_name = "Yoshi Colors"
+ option_normal = 0
+ option_random_order = 1
+ option_random_color = 2
+ option_singularity = 3
+ default = 0
+
+
+class SinguColor(Choice):
+ """Sets which color Yoshi will be if Yoshi Colors is set to singularity."""
+ display_name = "Singularity Yoshi Color"
+ option_green = 0
+ option_pink = 1
+ option_cyan = 3
+ option_yellow = 2
+ option_purple = 4
+ option_brown = 5
+ option_red = 6
+ option_blue = 7
+ default = 0
+
+
+class BabySound(Choice):
+ """Change the sound that Baby Mario makes when not on Yoshi."""
+ display_name = "Mario Sound Effect"
+ option_normal = 0
+ option_disabled = 1
+ option_random_sound_effect = 2
+ default = 0
+
+
+class TrapsEnabled(Toggle):
+ """Will place traps into the item pool.
+ Traps have a variety of negative effects, and will only replace filler items."""
+ display_name = "Traps Enabled"
+
+
+class TrapPercent(Range):
+ """Percentage of the item pool that becomes replaced with traps."""
+ display_name = "Trap Chance"
+ range_start = 0
+ range_end = 100
+ default = 10
+
+# class EnableScrets(Range):
+ # """This sets the amount of lives Yoshi will have upon loading the game."""
+ # display_name = "Starting Life Count"
+ # range_start = 1
+ # range_end = 255
+ # default = 3
+
+# class BackgroundColors(Range):
+ # """This sets the amount of lives Yoshi will have upon loading the game."""
+ # display_name = "Starting Life Count"
+ # range_start = 1
+ # range_end = 255
+ # default = 3
+
+# class Foreground Colors(Range):
+ # """This sets the amount of lives Yoshi will have upon loading the game."""
+ # display_name = "Starting Life Count"
+ # range_start = 1
+ # range_end = 255
+ # default = 3
+
+# class Music Shuffle(Range):
+ # """This sets the amount of lives Yoshi will have upon loading the game."""
+ # display_name = "Starting Life Count"
+ # range_start = 1
+ # range_end = 255
+ # default = 3
+
+# class Star Loss Rate(Range):
+ # """This sets the amount of lives Yoshi will have upon loading the game."""
+ # display_name = "Starting Life Count"
+ # range_start = 1
+ # range_end = 255
+ # default = 3
+
+
+@dataclass
+class YoshisIslandOptions(PerGameCommonOptions):
+ starting_world: StartingWorld
+ starting_lives: StartingLives
+ goal: PlayerGoal
+ luigi_pieces_required: LuigiPiecesReq
+ luigi_pieces_in_pool: LuigiPiecesAmt
+ extras_enabled: ExtrasEnabled
+ minigame_checks: MinigameChecks
+ split_extras: SplitExtras
+ split_bonus: SplitBonus
+ hidden_object_visibility: ObjectVis
+ add_secretlens: ShuffleSecretLens
+ shuffle_midrings: ShuffleMiddleRings
+ stage_logic: StageLogic
+ item_logic: ItemLogic
+ disable_autoscroll: DisableAutoScrollers
+ softlock_prevention: SoftlockPrevention
+ castle_open_condition: FinalLevelBosses
+ castle_clear_condition: FinalBossBosses
+ bowser_door_mode: BowserDoor
+ level_shuffle: LevelShuffle
+ boss_shuffle: BossShuffle
+ yoshi_colors: YoshiColors
+ yoshi_singularity_color: SinguColor
+ baby_mario_sound: BabySound
+ traps_enabled: TrapsEnabled
+ trap_percent: TrapPercent
+ death_link: DeathLink
diff --git a/worlds/yoshisisland/Regions.py b/worlds/yoshisisland/Regions.py
new file mode 100644
index 000000000000..59e93cfe7979
--- /dev/null
+++ b/worlds/yoshisisland/Regions.py
@@ -0,0 +1,248 @@
+from typing import List, Dict, TYPE_CHECKING
+from BaseClasses import Region, Location
+from .Locations import LocationData
+from .Options import MinigameChecks
+from .level_logic import YoshiLogic
+from .setup_bosses import BossReqs
+if TYPE_CHECKING:
+ from . import YoshisIslandWorld
+
+
+class YoshisIslandLocation(Location):
+ game: str = "Yoshi's Island"
+ level_id: int
+
+ def __init__(self, player: int, name: str = " ", address: int = None, parent=None, level_id: int = None):
+ super().__init__(player, name, address, parent)
+ self.level_id = level_id
+
+
+def init_areas(world: "YoshisIslandWorld", locations: List[LocationData]) -> None:
+ multiworld = world.multiworld
+ player = world.player
+ logic = YoshiLogic(world)
+
+ locations_per_region = get_locations_per_region(locations)
+
+ regions = [
+ create_region(world, player, locations_per_region, "Menu"),
+ create_region(world, player, locations_per_region, "Overworld"),
+ create_region(world, player, locations_per_region, "World 1"),
+ create_region(world, player, locations_per_region, "World 2"),
+ create_region(world, player, locations_per_region, "World 3"),
+ create_region(world, player, locations_per_region, "World 4"),
+ create_region(world, player, locations_per_region, "World 5"),
+ create_region(world, player, locations_per_region, "World 6"),
+
+ create_region(world, player, locations_per_region, "1-1"),
+ create_region(world, player, locations_per_region, "1-2"),
+ create_region(world, player, locations_per_region, "1-3"),
+ create_region(world, player, locations_per_region, "1-4"),
+ create_region(world, player, locations_per_region, "Burt The Bashful's Boss Room"),
+ create_region(world, player, locations_per_region, "1-5"),
+ create_region(world, player, locations_per_region, "1-6"),
+ create_region(world, player, locations_per_region, "1-7"),
+ create_region(world, player, locations_per_region, "1-8"),
+ create_region(world, player, locations_per_region, "Salvo The Slime's Boss Room"),
+
+ create_region(world, player, locations_per_region, "2-1"),
+ create_region(world, player, locations_per_region, "2-2"),
+ create_region(world, player, locations_per_region, "2-3"),
+ create_region(world, player, locations_per_region, "2-4"),
+ create_region(world, player, locations_per_region, "Bigger Boo's Boss Room"),
+ create_region(world, player, locations_per_region, "2-5"),
+ create_region(world, player, locations_per_region, "2-6"),
+ create_region(world, player, locations_per_region, "2-7"),
+ create_region(world, player, locations_per_region, "2-8"),
+ create_region(world, player, locations_per_region, "Roger The Ghost's Boss Room"),
+
+ create_region(world, player, locations_per_region, "3-1"),
+ create_region(world, player, locations_per_region, "3-2"),
+ create_region(world, player, locations_per_region, "3-3"),
+ create_region(world, player, locations_per_region, "3-4"),
+ create_region(world, player, locations_per_region, "Prince Froggy's Boss Room"),
+ create_region(world, player, locations_per_region, "3-5"),
+ create_region(world, player, locations_per_region, "3-6"),
+ create_region(world, player, locations_per_region, "3-7"),
+ create_region(world, player, locations_per_region, "3-8"),
+ create_region(world, player, locations_per_region, "Naval Piranha's Boss Room"),
+
+ create_region(world, player, locations_per_region, "4-1"),
+ create_region(world, player, locations_per_region, "4-2"),
+ create_region(world, player, locations_per_region, "4-3"),
+ create_region(world, player, locations_per_region, "4-4"),
+ create_region(world, player, locations_per_region, "Marching Milde's Boss Room"),
+ create_region(world, player, locations_per_region, "4-5"),
+ create_region(world, player, locations_per_region, "4-6"),
+ create_region(world, player, locations_per_region, "4-7"),
+ create_region(world, player, locations_per_region, "4-8"),
+ create_region(world, player, locations_per_region, "Hookbill The Koopa's Boss Room"),
+
+ create_region(world, player, locations_per_region, "5-1"),
+ create_region(world, player, locations_per_region, "5-2"),
+ create_region(world, player, locations_per_region, "5-3"),
+ create_region(world, player, locations_per_region, "5-4"),
+ create_region(world, player, locations_per_region, "Sluggy The Unshaven's Boss Room"),
+ create_region(world, player, locations_per_region, "5-5"),
+ create_region(world, player, locations_per_region, "5-6"),
+ create_region(world, player, locations_per_region, "5-7"),
+ create_region(world, player, locations_per_region, "5-8"),
+ create_region(world, player, locations_per_region, "Raphael The Raven's Boss Room"),
+
+ create_region(world, player, locations_per_region, "6-1"),
+ create_region(world, player, locations_per_region, "6-2"),
+ create_region(world, player, locations_per_region, "6-3"),
+ create_region(world, player, locations_per_region, "6-4"),
+ create_region(world, player, locations_per_region, "Tap-Tap The Red Nose's Boss Room"),
+ create_region(world, player, locations_per_region, "6-5"),
+ create_region(world, player, locations_per_region, "6-6"),
+ create_region(world, player, locations_per_region, "6-7"),
+ create_region(world, player, locations_per_region, "6-8"),
+ create_region(world, player, locations_per_region, "Bowser's Room"),
+ ]
+
+ if world.options.extras_enabled:
+ regions.insert(68, create_region(world, player, locations_per_region, "6-Extra"))
+ regions.insert(58, create_region(world, player, locations_per_region, "5-Extra"))
+ regions.insert(48, create_region(world, player, locations_per_region, "4-Extra"))
+ regions.insert(38, create_region(world, player, locations_per_region, "3-Extra"))
+ regions.insert(28, create_region(world, player, locations_per_region, "2-Extra"))
+ regions.insert(18, create_region(world, player, locations_per_region, "1-Extra"))
+
+ if world.options.minigame_checks in {MinigameChecks.option_bonus_games, MinigameChecks.option_both}:
+ regions.insert(74, create_region(world, player, locations_per_region, "6-Bonus"))
+ regions.insert(63, create_region(world, player, locations_per_region, "5-Bonus"))
+ regions.insert(52, create_region(world, player, locations_per_region, "4-Bonus"))
+ regions.insert(41, create_region(world, player, locations_per_region, "3-Bonus"))
+ regions.insert(29, create_region(world, player, locations_per_region, "2-Bonus"))
+ regions.insert(19, create_region(world, player, locations_per_region, "1-Bonus"))
+
+ multiworld.regions += regions
+
+ connect_starting_region(world)
+
+ bosses = BossReqs(world)
+
+ multiworld.get_region("Overworld", player).add_exits(
+ ["World 1", "World 2", "World 3", "World 4", "World 5", "World 6"],
+ {
+ "World 1": lambda state: state.has("World 1 Gate", player),
+ "World 2": lambda state: state.has("World 2 Gate", player),
+ "World 3": lambda state: state.has("World 3 Gate", player),
+ "World 4": lambda state: state.has("World 4 Gate", player),
+ "World 5": lambda state: state.has("World 5 Gate", player),
+ "World 6": lambda state: state.has("World 6 Gate", player)
+ }
+ )
+
+ for cur_world in range(1, 7):
+ for cur_level in range(8):
+ if cur_world != 6 or cur_level != 7:
+ multiworld.get_region(f"World {cur_world}", player).add_exits(
+ [world.level_location_list[(cur_world - 1) * 8 + cur_level]]
+ )
+
+ multiworld.get_region("1-4", player).add_exits([world.boss_order[0]],{world.boss_order[0]: lambda state: logic._14Clear(state)})
+ multiworld.get_region("1-8", player).add_exits([world.boss_order[1]],{world.boss_order[1]: lambda state: logic._18Clear(state)})
+ multiworld.get_region("2-4", player).add_exits([world.boss_order[2]],{world.boss_order[2]: lambda state: logic._24Clear(state)})
+ multiworld.get_region("2-8", player).add_exits([world.boss_order[3]],{world.boss_order[3]: lambda state: logic._28Clear(state)})
+ multiworld.get_region("3-4", player).add_exits([world.boss_order[4]],{world.boss_order[4]: lambda state: logic._34Clear(state)})
+ multiworld.get_region("3-8", player).add_exits([world.boss_order[5]],{world.boss_order[5]: lambda state: logic._38Clear(state)})
+ multiworld.get_region("4-4", player).add_exits([world.boss_order[6]],{world.boss_order[6]: lambda state: logic._44Clear(state)})
+ multiworld.get_region("4-8", player).add_exits([world.boss_order[7]],{world.boss_order[7]: lambda state: logic._48Clear(state)})
+ multiworld.get_region("5-4", player).add_exits([world.boss_order[8]],{world.boss_order[8]: lambda state: logic._54Clear(state)})
+ multiworld.get_region("5-8", player).add_exits([world.boss_order[9]],{world.boss_order[9]: lambda state: logic._58Clear(state)})
+ multiworld.get_region("World 6", player).add_exits(["6-8"],{"6-8": lambda state: bosses.castle_access(state)})
+ multiworld.get_region("6-4", player).add_exits([world.boss_order[10]],{world.boss_order[10]: lambda state: logic._64Clear(state)})
+ multiworld.get_region("6-8", player).add_exits(["Bowser's Room"],{"Bowser's Room": lambda state: bosses.castle_clear(state)})
+
+ if world.options.extras_enabled:
+ multiworld.get_region("World 1", player).add_exits(
+ ["1-Extra"],
+ {"1-Extra": lambda state: state.has_any({"Extra Panels", "Extra 1"}, player)}
+ )
+ multiworld.get_region("World 2", player).add_exits(
+ ["2-Extra"],
+ {"2-Extra": lambda state: state.has_any({"Extra Panels", "Extra 2"}, player)}
+ )
+ multiworld.get_region(
+ "World 3", player).add_exits(["3-Extra"],
+ {"3-Extra": lambda state: state.has_any({"Extra Panels", "Extra 3"}, player)}
+ )
+ multiworld.get_region("World 4", player).add_exits(
+ ["4-Extra"],
+ {"4-Extra": lambda state: state.has_any({"Extra Panels", "Extra 4"}, player)}
+ )
+ multiworld.get_region("World 5", player).add_exits(
+ ["5-Extra"],
+ {"5-Extra": lambda state: state.has_any({"Extra Panels", "Extra 5"}, player)}
+ )
+ multiworld.get_region("World 6", player).add_exits(
+ ["6-Extra"],
+ {"6-Extra": lambda state: state.has_any({"Extra Panels", "Extra 6"}, player)}
+ )
+
+ if world.options.minigame_checks in {MinigameChecks.option_bonus_games, MinigameChecks.option_both}:
+ multiworld.get_region("World 1", player).add_exits(
+ ["1-Bonus"],
+ {"1-Bonus": lambda state: state.has_any({"Bonus Panels", "Bonus 1"}, player)}
+ )
+ multiworld.get_region("World 2", player).add_exits(
+ ["2-Bonus"],
+ {"2-Bonus": lambda state: state.has_any({"Bonus Panels", "Bonus 2"}, player)}
+ )
+ multiworld.get_region("World 3", player).add_exits(
+ ["3-Bonus"],
+ {"3-Bonus": lambda state: state.has_any({"Bonus Panels", "Bonus 3"}, player)}
+ )
+ multiworld.get_region("World 4", player).add_exits(
+ ["4-Bonus"],
+ {"4-Bonus": lambda state: state.has_any({"Bonus Panels", "Bonus 4"}, player)}
+ )
+ multiworld.get_region("World 5", player).add_exits(
+ ["5-Bonus"],
+ {"5-Bonus": lambda state: state.has_any({"Bonus Panels", "Bonus 5"}, player)}
+ )
+ multiworld.get_region("World 6", player).add_exits(
+ ["6-Bonus"],
+ {"6-Bonus": lambda state: state.has_any({"Bonus Panels", "Bonus 6"}, player)}
+ )
+
+
+def create_location(player: int, location_data: LocationData, region: Region) -> Location:
+ location = YoshisIslandLocation(player, location_data.name, location_data.code, region)
+ location.access_rule = location_data.rule
+ location.level_id = location_data.LevelID
+
+ return location
+
+
+def create_region(world: "YoshisIslandWorld", player: int, locations_per_region: Dict[str, List[LocationData]], name: str) -> Region:
+ region = Region(name, player, world.multiworld)
+
+ if name in locations_per_region:
+ for location_data in locations_per_region[name]:
+ location = create_location(player, location_data, region)
+ region.locations.append(location)
+
+ return region
+
+def connect_starting_region(world: "YoshisIslandWorld") -> None:
+ multiworld = world.multiworld
+ player = world.player
+ menu = multiworld.get_region("Menu", player)
+ world_main = multiworld.get_region("Overworld", player)
+
+ starting_region = multiworld.get_region(f"World {world.options.starting_world + 1}", player)
+
+ menu.connect(world_main, "Start Game")
+ world_main.connect(starting_region, "Overworld")
+
+
+def get_locations_per_region(locations: List[LocationData]) -> Dict[str, List[LocationData]]:
+ per_region: Dict[str, List[LocationData]] = {}
+
+ for location in locations:
+ per_region.setdefault(location.region, []).append(location)
+
+ return per_region
diff --git a/worlds/yoshisisland/Rom.py b/worlds/yoshisisland/Rom.py
new file mode 100644
index 000000000000..0943ba82514c
--- /dev/null
+++ b/worlds/yoshisisland/Rom.py
@@ -0,0 +1,1230 @@
+import hashlib
+import os
+import Utils
+from worlds.Files import APDeltaPatch
+from settings import get_settings
+from typing import TYPE_CHECKING, Collection, SupportsIndex
+
+from .Options import YoshiColors, BowserDoor, PlayerGoal, MinigameChecks
+
+if TYPE_CHECKING:
+ from . import YoshisIslandWorld
+USHASH = "cb472164c5a71ccd3739963390ec6a50"
+
+item_values = {
+ 0x302050: [0x1467, 0x01], # ! Switch
+ 0x302051: [0x1467, 0x02], # Dashed Platform
+ 0x302052: [0x1467, 0x03], # Dashed Stairs
+ 0x302053: [0x1467, 0x04], # Beanstalk
+ 0x302054: [0x1467, 0x05], # Helicopter
+ 0x302059: [0x1467, 0x06], # Mole Tank
+ 0x302068: [0x1467, 0x07], # Train
+ 0x30205E: [0x1467, 0x08], # Car
+ 0x302063: [0x1467, 0x09], # Submarine
+ 0x302055: [0x1467, 0x0A], # Spring Ball
+ 0x302056: [0x1467, 0x0B], # Large Spring Ball
+ 0x302057: [0x1467, 0x0C], # Arrow Wheel
+ 0x302058: [0x1467, 0x0D], # Vanishing Arrow Wheel
+ 0x30205A: [0x1467, 0x0E], # Watermelon
+ 0x30205B: [0x1467, 0x0F], # Ice Melon
+ 0x30205C: [0x1467, 0x10], # Fire Melon
+ 0x30205D: [0x1467, 0x11], # Super Star
+ 0x30205F: [0x1467, 0x12], # Flashing Eggs
+ 0x302060: [0x1467, 0x13], # Giant Eggs
+ 0x302061: [0x1467, 0x14], # Egg Launcher
+ 0x302062: [0x1467, 0x15], # Egg Plant
+ 0x302064: [0x1467, 0x16], # Chomp Rock
+ 0x302065: [0x1467, 0x17], # Poochy
+ 0x302066: [0x1467, 0x18], # Platform Ghost
+ 0x302067: [0x1467, 0x19], # Skis
+ 0x302069: [0x1467, 0x1A], # Key
+ 0x30206A: [0x1467, 0x1B], # Middle Ring
+ 0x30206B: [0x1467, 0x1C], # Bucket
+ 0x30206C: [0x1467, 0x1D], # Tulip
+ 0x302081: [0x1467, 0x1E], # Secret Lens
+
+ 0x30206D: [0x1467, 0x1F], # Egg Capacity Upgrade
+
+ 0x30206E: [0x1467, 0x20], # World 1 Gate
+ 0x30206F: [0x1467, 0x21], # World 2 Gate
+ 0x302070: [0x1467, 0x22], # World 3 Gate
+ 0x302071: [0x1467, 0x23], # World 4 Gate
+ 0x302072: [0x1467, 0x24], # World 5 Gate
+ 0x302073: [0x1467, 0x25], # World 6 Gate
+
+ 0x302074: [0x1467, 0x26], # Extra 1
+ 0x302075: [0x1467, 0x27], # Extra 2
+ 0x302076: [0x1467, 0x28], # Extra 3
+ 0x302077: [0x1467, 0x29], # Extra 4
+ 0x302078: [0x1467, 0x2A], # Extra 5
+ 0x302079: [0x1467, 0x2B], # Extra 6
+ 0x30207A: [0x1467, 0x2C], # Extra Panels
+
+ 0x30207B: [0x1467, 0x2D], # Bonus 1
+ 0x30207C: [0x1467, 0x2E], # Bonus 2
+ 0x30207D: [0x1467, 0x2F], # Bonus 3
+ 0x30207E: [0x1467, 0x30], # Bonus 4
+ 0x30207F: [0x1467, 0x31], # Bonus 5
+ 0x302080: [0x1467, 0x32], # Bonus 6
+ 0x302082: [0x1467, 0x33], # Bonus Panels
+
+ 0x302083: [0x1467, 0x34], # Anytime Egg
+ 0x302084: [0x1467, 0x35], # Anywhere Pow
+ 0x302085: [0x1467, 0x36], # Cloud
+ 0x302086: [0x1467, 0x37], # Pocket Melon
+ 0x302088: [0x1467, 0x38], # Ice Melon
+ 0x302087: [0x1467, 0x39], # Fire Melon
+ 0x302089: [0x1467, 0x3A], # Magnifying Glass
+ 0x30208A: [0x1467, 0x3B], # 10 Stars
+ 0x30208B: [0x1467, 0x3C], # 20 Stars
+
+ 0x30208C: [0x1467, 0x3D], # 1up
+ 0x30208D: [0x1467, 0x3E], # 2up
+ 0x30208E: [0x1467, 0x3F], # 3up
+ 0x30208F: [0x1467, 0x40], # 10up
+
+ 0x302090: [0x1467, 0x41], # Fuzzy Trap
+ 0x302093: [0x1467, 0x42], # Freeze Trap
+ 0x302091: [0x1467, 0x43], # Reverse Trap
+ 0x302092: [0x1467, 0x44], # Dark Trap
+ 0x302094: [0x1467, 0x00], # Boss clear, local handling
+
+ 0x302095: [0x1467, 0x45] # Luigi Piece
+
+}
+
+location_table = {
+ # 1-1
+ 0x305020: [0x146D, 0], # Red Coins
+ 0x305021: [0x146D, 1], # Flowers
+ 0x305022: [0x146D, 2], # Stars
+ 0x305023: [0x146D, 3], # Level Clear
+ # 1-2
+ 0x305024: [0x146E, 0],
+ 0x305025: [0x146E, 1],
+ 0x305026: [0x146E, 2],
+ 0x305027: [0x146E, 3],
+ # 1-3
+ 0x305028: [0x146F, 0],
+ 0x305029: [0x146F, 1],
+ 0x30502A: [0x146F, 2],
+ 0x30502B: [0x146F, 3],
+ 0x3050F8: [0x146F, 4],
+ # 1-4
+ 0x30502C: [0x1470, 0],
+ 0x30502D: [0x1470, 1],
+ 0x30502E: [0x1470, 2],
+ 0x30502F: [0x1470, 3],
+ # 1-5
+ 0x305031: [0x1471, 0],
+ 0x305032: [0x1471, 1],
+ 0x305033: [0x1471, 2],
+ 0x305034: [0x1471, 3],
+ # 1-6
+ 0x305035: [0x1472, 0],
+ 0x305036: [0x1472, 1],
+ 0x305037: [0x1472, 2],
+ 0x305038: [0x1472, 3],
+ # 1-7
+ 0x305039: [0x1473, 0],
+ 0x30503A: [0x1473, 1],
+ 0x30503B: [0x1473, 2],
+ 0x30503C: [0x1473, 3],
+ 0x3050F9: [0x1473, 4],
+ # 1-8
+ 0x30503D: [0x1474, 0],
+ 0x30503E: [0x1474, 1],
+ 0x30503F: [0x1474, 2],
+ 0x305040: [0x1474, 3],
+ # 1-E
+ 0x3050E0: [0x1475, 0],
+ 0x3050E1: [0x1475, 1],
+ 0x3050E2: [0x1475, 2],
+ 0x3050E3: [0x1475, 3],
+ # 1-B
+ 0x305106: [0x1476, 4],
+ ######################
+ # 2-1
+ 0x305041: [0x1479, 0],
+ 0x305042: [0x1479, 1],
+ 0x305043: [0x1479, 2],
+ 0x305044: [0x1479, 3],
+ 0x3050FA: [0x1479, 4],
+ # 2-2
+ 0x305045: [0x147A, 0],
+ 0x305046: [0x147A, 1],
+ 0x305047: [0x147A, 2],
+ 0x305048: [0x147A, 3],
+ # 2-3
+ 0x305049: [0x147B, 0],
+ 0x30504A: [0x147B, 1],
+ 0x30504B: [0x147B, 2],
+ 0x30504C: [0x147B, 3],
+ 0x3050FB: [0x147B, 4],
+ # 2-4
+ 0x30504D: [0x147C, 0],
+ 0x30504E: [0x147C, 1],
+ 0x30504F: [0x147C, 2],
+ 0x305050: [0x147C, 3],
+ # 2-5
+ 0x305051: [0x147D, 0],
+ 0x305052: [0x147D, 1],
+ 0x305053: [0x147D, 2],
+ 0x305054: [0x147D, 3],
+ # 2-6
+ 0x305055: [0x147E, 0],
+ 0x305056: [0x147E, 1],
+ 0x305057: [0x147E, 2],
+ 0x305058: [0x147E, 3],
+ 0x3050FC: [0x147E, 4],
+ # 2-7
+ 0x305059: [0x147F, 0],
+ 0x30505A: [0x147F, 1],
+ 0x30505B: [0x147F, 2],
+ 0x30505C: [0x147F, 3],
+ 0x3050FD: [0x147F, 4],
+ # 2-8
+ 0x30505D: [0x1480, 0],
+ 0x30505E: [0x1480, 1],
+ 0x30505F: [0x1480, 2],
+ 0x305060: [0x1480, 3],
+ # 2-E
+ 0x3050E4: [0x1481, 0],
+ 0x3050E5: [0x1481, 1],
+ 0x3050E6: [0x1481, 2],
+ 0x3050E7: [0x1481, 3],
+ # 2-B
+ 0x305107: [0x1482, 4],
+ ######################
+ # 3-1
+ 0x305061: [0x1485, 0],
+ 0x305062: [0x1485, 1],
+ 0x305063: [0x1485, 2],
+ 0x305064: [0x1485, 3],
+ # 3-2
+ 0x305065: [0x1486, 0],
+ 0x305066: [0x1486, 1],
+ 0x305067: [0x1486, 2],
+ 0x305068: [0x1486, 3],
+ 0x3050FE: [0x1486, 4],
+ # 3-3
+ 0x305069: [0x1487, 0],
+ 0x30506A: [0x1487, 1],
+ 0x30506B: [0x1487, 2],
+ 0x30506C: [0x1487, 3],
+ # 3-4
+ 0x30506D: [0x1488, 0],
+ 0x30506E: [0x1488, 1],
+ 0x30506F: [0x1488, 2],
+ 0x305070: [0x1488, 3],
+ # 3-5
+ 0x305071: [0x1489, 0],
+ 0x305072: [0x1489, 1],
+ 0x305073: [0x1489, 2],
+ 0x305074: [0x1489, 3],
+ # 3-6
+ 0x305075: [0x148A, 0],
+ 0x305076: [0x148A, 1],
+ 0x305077: [0x148A, 2],
+ 0x305078: [0x148A, 3],
+ # 3-7
+ 0x305079: [0x148B, 0],
+ 0x30507A: [0x148B, 1],
+ 0x30507B: [0x148B, 2],
+ 0x30507C: [0x148B, 3],
+ 0x3050FF: [0x148B, 4],
+ # 3-8
+ 0x30507D: [0x148C, 0],
+ 0x30507E: [0x148C, 1],
+ 0x30507F: [0x148C, 2],
+ 0x305080: [0x148C, 3],
+ # 3-E
+ 0x3050E8: [0x148D, 0],
+ 0x3050E9: [0x148D, 1],
+ 0x3050EA: [0x148D, 2],
+ 0x3050EB: [0x148D, 3],
+ # 3-B
+ 0x305108: [0x148E, 4],
+ ######################
+ # 4-1
+ 0x305081: [0x1491, 0],
+ 0x305082: [0x1491, 1],
+ 0x305083: [0x1491, 2],
+ 0x305084: [0x1491, 3],
+ # 4-2
+ 0x305085: [0x1492, 0],
+ 0x305086: [0x1492, 1],
+ 0x305087: [0x1492, 2],
+ 0x305088: [0x1492, 3],
+ 0x305100: [0x1492, 4],
+ # 4-3
+ 0x305089: [0x1493, 0],
+ 0x30508A: [0x1493, 1],
+ 0x30508B: [0x1493, 2],
+ 0x30508C: [0x1493, 3],
+ # 4-4
+ 0x30508D: [0x1494, 0],
+ 0x30508E: [0x1494, 1],
+ 0x30508F: [0x1494, 2],
+ 0x305090: [0x1494, 3],
+ # 4-5
+ 0x305091: [0x1495, 0],
+ 0x305092: [0x1495, 1],
+ 0x305093: [0x1495, 2],
+ 0x305094: [0x1495, 3],
+ # 4-6
+ 0x305095: [0x1496, 0],
+ 0x305096: [0x1496, 1],
+ 0x305097: [0x1496, 2],
+ 0x305098: [0x1496, 3],
+ 0x305101: [0x1496, 4],
+ # 4-7
+ 0x305099: [0x1497, 0],
+ 0x30509A: [0x1497, 1],
+ 0x30509B: [0x1497, 2],
+ 0x30509C: [0x1497, 3],
+ 0x305102: [0x1497, 4],
+ # 4-8
+ 0x30509D: [0x1498, 0],
+ 0x30509E: [0x1498, 1],
+ 0x30509F: [0x1498, 2],
+ 0x3050A0: [0x1498, 3],
+ # 4-E
+ 0x3050EC: [0x1499, 0],
+ 0x3050ED: [0x1499, 1],
+ 0x3050EE: [0x1499, 2],
+ 0x3050EF: [0x1499, 3],
+ # 4-B
+ 0x305109: [0x149A, 4],
+ ######################
+ # 5-1
+ 0x3050A1: [0x149D, 0],
+ 0x3050A2: [0x149D, 1],
+ 0x3050A3: [0x149D, 2],
+ 0x3050A4: [0x149D, 3],
+ 0x305103: [0x149D, 4],
+ # 5-2
+ 0x3050A5: [0x149E, 0],
+ 0x3050A6: [0x149E, 1],
+ 0x3050A7: [0x149E, 2],
+ 0x3050A8: [0x149E, 3],
+ # 5-3
+ 0x3050A9: [0x149F, 0],
+ 0x3050AA: [0x149F, 1],
+ 0x3050AB: [0x149F, 2],
+ 0x3050AC: [0x149F, 3],
+ # 5-4
+ 0x3050AD: [0x14A0, 0],
+ 0x3050AE: [0x14A0, 1],
+ 0x3050AF: [0x14A0, 2],
+ 0x3050B0: [0x14A0, 3],
+ # 5-5
+ 0x3050B1: [0x14A1, 0],
+ 0x3050B2: [0x14A1, 1],
+ 0x3050B3: [0x14A1, 2],
+ 0x3050B4: [0x14A1, 3],
+ # 5-6
+ 0x3050B5: [0x14A2, 0],
+ 0x3050B6: [0x14A2, 1],
+ 0x3050B7: [0x14A2, 2],
+ 0x3050B8: [0x14A2, 3],
+ # 5-7
+ 0x3050B9: [0x14A3, 0],
+ 0x3050BA: [0x14A3, 1],
+ 0x3050BB: [0x14A3, 2],
+ 0x3050BC: [0x14A3, 3],
+ # 5-8
+ 0x3050BD: [0x14A4, 0],
+ 0x3050BE: [0x14A4, 1],
+ 0x3050BF: [0x14A4, 2],
+ 0x3050C0: [0x14A4, 3],
+ # 5-E
+ 0x3050F0: [0x14A5, 0],
+ 0x3050F1: [0x14A5, 1],
+ 0x3050F2: [0x14A5, 2],
+ 0x3050F3: [0x14A5, 3],
+ # 5-B
+ 0x30510A: [0x14A6, 4],
+ #######################
+ # 6-1
+ 0x3050C1: [0x14A9, 0],
+ 0x3050C2: [0x14A9, 1],
+ 0x3050C3: [0x14A9, 2],
+ 0x3050C4: [0x14A9, 3],
+ 0x305104: [0x14A9, 4],
+ # 6-2
+ 0x3050C5: [0x14AA, 0],
+ 0x3050C6: [0x14AA, 1],
+ 0x3050C7: [0x14AA, 2],
+ 0x3050C8: [0x14AA, 3],
+ # 6-3
+ 0x3050C9: [0x14AB, 0],
+ 0x3050CA: [0x14AB, 1],
+ 0x3050CB: [0x14AB, 2],
+ 0x3050CC: [0x14AB, 3],
+ # 6-4
+ 0x3050CD: [0x14AC, 0],
+ 0x3050CE: [0x14AC, 1],
+ 0x3050CF: [0x14AC, 2],
+ 0x3050D0: [0x14AC, 3],
+ # 6-5
+ 0x3050D1: [0x14AD, 0],
+ 0x3050D2: [0x14AD, 1],
+ 0x3050D3: [0x14AD, 2],
+ 0x3050D4: [0x14AD, 3],
+ # 6-6
+ 0x3050D5: [0x14AE, 0],
+ 0x3050D6: [0x14AE, 1],
+ 0x3050D7: [0x14AE, 2],
+ 0x3050D8: [0x14AE, 3],
+ # 6-7
+ 0x3050D9: [0x14AF, 0],
+ 0x3050DA: [0x14AF, 1],
+ 0x3050DB: [0x14AF, 2],
+ 0x3050DC: [0x14AF, 3],
+ 0x305105: [0x14AF, 4],
+ # 6-8
+ 0x3050DD: [0x14B0, 0],
+ 0x3050DE: [0x14B0, 1],
+ 0x3050DF: [0x14B0, 2],
+ # 6-E
+ 0x3050F4: [0x14B1, 0],
+ 0x3050F5: [0x14B1, 1],
+ 0x3050F6: [0x14B1, 2],
+ 0x3050F7: [0x14B1, 3],
+ # 6-B
+ 0x30510B: [0x14B2, 4]
+}
+
+class LocalRom:
+
+ def __init__(self, file: str) -> None:
+ self.name = None
+ self.hash = hash
+ self.orig_buffer = None
+
+ with open(file, "rb") as stream:
+ self.buffer = Utils.read_snes_rom(stream)
+
+ def read_bit(self, address: int, bit_number: int) -> bool:
+ bitflag = 1 << bit_number
+ return (self.buffer[address] & bitflag) != 0
+
+ def read_byte(self, address: int) -> int:
+ return self.buffer[address]
+
+ def read_bytes(self, startaddress: int, length: int) -> bytearray:
+ return self.buffer[startaddress:startaddress + length]
+
+ def write_byte(self, address: int, value: int) -> None:
+ self.buffer[address] = value
+
+ def write_bytes(self, startaddress: int, values: Collection[SupportsIndex]) -> None:
+ self.buffer[startaddress:startaddress + len(values)] = values
+
+ def write_to_file(self, file: str) -> None:
+ with open(file, "wb") as outfile:
+ outfile.write(self.buffer)
+
+ def read_from_file(self, file: str) -> None:
+ with open(file, "rb") as stream:
+ self.buffer = bytearray(stream.read())
+
+def handle_items(rom: LocalRom) -> None:
+ rom.write_bytes(0x0077B0, bytearray([0xE2, 0x20, 0xAD, 0x40, 0x14, 0xC2, 0x20, 0xF0, 0x08, 0xBD, 0x82, 0x71, 0x18, 0x5C, 0x3B, 0xB6]))
+ rom.write_bytes(0x0077C0, bytearray([0x0E, 0x5C, 0x97, 0xB6, 0x0E, 0xA0, 0xFF, 0xAD, 0x74, 0x79, 0x29, 0x01, 0x00, 0xD0, 0x02, 0xA0]))
+ rom.write_bytes(0x0077D0, bytearray([0x05, 0x98, 0x9D, 0xA2, 0x74, 0x6B, 0xE2, 0x20, 0xBD, 0x60, 0x73, 0xDA, 0xC2, 0x20, 0xA2, 0x00]))
+ rom.write_bytes(0x0077E0, bytearray([0xDF, 0x70, 0xAF, 0x09, 0xF0, 0x08, 0xE8, 0xE8, 0xE0, 0x08, 0xF0, 0x23, 0x80, 0xF2, 0xE2, 0x20]))
+ rom.write_bytes(0x0077F0, bytearray([0x8A, 0x4A, 0xAA, 0xBF, 0x78, 0xAF, 0x09, 0xAA, 0xBD, 0x40, 0x14, 0xC2, 0x20, 0xF0, 0x08, 0xFA]))
+ rom.write_bytes(0x007800, bytearray([0xA0, 0x05, 0x98, 0x9D, 0xA2, 0x74, 0x60, 0xFA, 0x22, 0xC5, 0xF7, 0x00, 0xEA, 0xEA, 0x60, 0xFA]))
+ rom.write_bytes(0x007810, bytearray([0x60, 0x22, 0x23, 0xAF, 0x03, 0x20, 0xD6, 0xF7, 0x6B, 0x20, 0x2F, 0xF8, 0xE2, 0x20, 0xC9, 0x00]))
+ rom.write_bytes(0x007820, bytearray([0xD0, 0x03, 0xC2, 0x20, 0x6B, 0xC2, 0x20, 0xBD, 0x60, 0x73, 0x38, 0x5C, 0xB1, 0xC9, 0x03, 0xDA]))
+ rom.write_bytes(0x007830, bytearray([0xBD, 0x60, 0x73, 0xA2, 0x00, 0xDF, 0x7C, 0xAF, 0x09, 0xF0, 0x08, 0xE8, 0xE8, 0xE0, 0x0A, 0xF0]))
+ rom.write_bytes(0x007840, bytearray([0x13, 0x80, 0xF2, 0xE2, 0x20, 0x8A, 0x4A, 0xAA, 0xBF, 0x86, 0xAF, 0x09, 0xAA, 0xBD, 0x40, 0x14]))
+ rom.write_bytes(0x007850, bytearray([0xFA, 0xC2, 0x20, 0x60, 0xA9, 0x01, 0x00, 0xFA, 0x60, 0x20, 0x2F, 0xF8, 0xE2, 0x20, 0xC9, 0x00]))
+ rom.write_bytes(0x007860, bytearray([0xC2, 0x20, 0xD0, 0x06, 0x22, 0xC5, 0xF7, 0x00, 0x80, 0x04, 0x22, 0xCF, 0xF7, 0x00, 0xA5, 0x14]))
+ rom.write_bytes(0x007870, bytearray([0x29, 0x0F, 0x00, 0x5C, 0x9A, 0xC9, 0x03, 0x5A, 0xE2, 0x10, 0x20, 0x2F, 0xF8, 0xC2, 0x10, 0x7A]))
+ rom.write_bytes(0x007880, bytearray([0xE2, 0x20, 0xC9, 0x00, 0xC2, 0x20, 0xD0, 0x08, 0xAD, 0x74, 0x79, 0x29, 0x01, 0x00, 0xF0, 0x04]))
+ rom.write_bytes(0x007890, bytearray([0x22, 0x3C, 0xAA, 0x03, 0xE2, 0x10, 0x5C, 0x47, 0xC9, 0x03, 0x22, 0x23, 0xAF, 0x03, 0xBD, 0x60]))
+ rom.write_bytes(0x0078A0, bytearray([0x73, 0xC9, 0x6F, 0x00, 0xF0, 0x07, 0xE2, 0x20, 0xAD, 0x4A, 0x14, 0x80, 0x05, 0xE2, 0x20, 0xAD]))
+ rom.write_bytes(0x0078B0, bytearray([0x49, 0x14, 0xC2, 0x20, 0xD0, 0x06, 0x22, 0xC5, 0xF7, 0x00, 0x80, 0x04, 0x22, 0xCF, 0xF7, 0x00]))
+ rom.write_bytes(0x0078C0, bytearray([0x5C, 0x2D, 0x83, 0x05, 0xBD, 0x60, 0x73, 0xC9, 0x6F, 0x00, 0xF0, 0x07, 0xE2, 0x20, 0xAD, 0x4A]))
+ rom.write_bytes(0x0078D0, bytearray([0x14, 0x80, 0x05, 0xE2, 0x20, 0xAD, 0x49, 0x14, 0xC2, 0x20, 0xD0, 0x04, 0x5C, 0xA0, 0x83, 0x05]))
+ rom.write_bytes(0x0078E0, bytearray([0xAD, 0xAC, 0x60, 0x0D, 0xAE, 0x60, 0x5C, 0x84, 0x83, 0x05, 0x22, 0x52, 0xAA, 0x03, 0xBD, 0x60]))
+ rom.write_bytes(0x0078F0, bytearray([0x73, 0xC9, 0x1E, 0x01, 0xE2, 0x20, 0xF0, 0x05, 0xAD, 0x4C, 0x14, 0x80, 0x03, 0xAD, 0x4B, 0x14]))
+ rom.write_bytes(0x007900, bytearray([0xEA, 0xC2, 0x20, 0xF0, 0x08, 0x22, 0xCF, 0xF7, 0x00, 0x5C, 0xA6, 0xF0, 0x05, 0x22, 0xC5, 0xF7]))
+ rom.write_bytes(0x007910, bytearray([0x00, 0x5C, 0xA6, 0xF0, 0x05, 0xE2, 0x20, 0xAD, 0x1C, 0x01, 0xC9, 0x0E, 0xC2, 0x20, 0xF0, 0x18]))
+ rom.write_bytes(0x007920, bytearray([0x20, 0x59, 0xF9, 0xE2, 0x20, 0xC9, 0x00, 0xF0, 0x04, 0xA9, 0x10, 0x80, 0x2A, 0xA9, 0x02, 0x9D]))
+ rom.write_bytes(0x007930, bytearray([0x00, 0x6F, 0xC2, 0x20, 0x22, 0xC5, 0xF7, 0x00, 0xA2, 0x0A, 0xA9, 0x2F, 0xCE, 0x5C, 0x22, 0x80]))
+ rom.write_bytes(0x007940, bytearray([0x04, 0x20, 0x59, 0xF9, 0xE2, 0x20, 0xC9, 0x00, 0xC2, 0x20, 0xF0, 0x0A, 0xAD, 0x0E, 0x30, 0x29]))
+ rom.write_bytes(0x007950, bytearray([0x03, 0x00, 0x5C, 0x2E, 0x80, 0x04, 0x6B, 0x80, 0xD6, 0xDA, 0xBD, 0x60, 0x73, 0xA2, 0x00, 0xDF]))
+ rom.write_bytes(0x007960, bytearray([0x8B, 0xAF, 0x09, 0xF0, 0x04, 0xE8, 0xE8, 0x80, 0xF6, 0xE2, 0x20, 0x8A, 0x4A, 0xAA, 0xBF, 0x91]))
+ rom.write_bytes(0x007970, bytearray([0xAF, 0x09, 0xAA, 0xBD, 0x40, 0x14, 0xFA, 0xC2, 0x20, 0x60, 0x22, 0x2E, 0xAA, 0x03, 0xE2, 0x20]))
+ rom.write_bytes(0x007980, bytearray([0xAD, 0x50, 0x14, 0xC2, 0x20, 0xD0, 0x06, 0x22, 0xC5, 0xF7, 0x00, 0x80, 0x04, 0x22, 0xCF, 0xF7]))
+ rom.write_bytes(0x007990, bytearray([0x00, 0x5C, 0x05, 0x99, 0x02, 0x69, 0x20, 0x00, 0xC9, 0x20, 0x01, 0xB0, 0x0D, 0xE2, 0x20, 0xAD]))
+ rom.write_bytes(0x0079A0, bytearray([0x50, 0x14, 0xC2, 0x20, 0xF0, 0x04, 0x5C, 0x3E, 0x99, 0x02, 0x5C, 0x8C, 0x99, 0x02, 0x22, 0x23]))
+ rom.write_bytes(0x0079B0, bytearray([0xAF, 0x03, 0xE2, 0x20, 0xAD, 0x1C, 0x01, 0xC9, 0x02, 0xC2, 0x20, 0xD0, 0x18, 0x20, 0x59, 0xF9]))
+ rom.write_bytes(0x0079C0, bytearray([0xE2, 0x20, 0xC9, 0x00, 0xD0, 0x13, 0xC2, 0x20, 0x22, 0xC5, 0xF7, 0x00, 0xE2, 0x20, 0xA9, 0x02]))
+ rom.write_bytes(0x0079D0, bytearray([0x9D, 0x00, 0x6F, 0xC2, 0x20, 0x5C, 0x35, 0x80, 0x04, 0xC2, 0x20, 0x22, 0xCF, 0xF7, 0x00, 0x80]))
+ rom.write_bytes(0x0079E0, bytearray([0xF2, 0xE2, 0x20, 0xAD, 0x4E, 0x14, 0xC2, 0x20, 0xF0, 0x07, 0xA9, 0x14, 0x00, 0x5C, 0x9E, 0xF1]))
+ rom.write_bytes(0x0079F0, bytearray([0x07, 0xA9, 0x0E, 0x00, 0x80, 0xF7, 0xBD, 0x60, 0x73, 0xDA, 0xA2, 0x00, 0xDF, 0x94, 0xAF, 0x09]))
+ rom.write_bytes(0x007A00, bytearray([0xF0, 0x11, 0xE0, 0x08, 0xF0, 0x04, 0xE8, 0xE8, 0x80, 0xF2, 0xFA, 0x22, 0x57, 0xF9, 0x0C, 0x5C]))
+ rom.write_bytes(0x007A10, bytearray([0xBD, 0xBE, 0x03, 0x8A, 0x4A, 0xE2, 0x20, 0xAA, 0xBF, 0x9E, 0xAF, 0x09, 0xAA, 0xBD, 0x40, 0x14]))
+ rom.write_bytes(0x007A20, bytearray([0xC9, 0x00, 0xC2, 0x20, 0xF0, 0x02, 0x80, 0xE2, 0x4C, 0x61, 0xFF, 0x00, 0x9D, 0x00, 0x6F, 0x74]))
+ rom.write_bytes(0x007A30, bytearray([0x78, 0x74, 0x18, 0x74, 0x76, 0x9E, 0x36, 0x7A, 0x9E, 0x38, 0x7A, 0x9E, 0x38, 0x7D, 0xBC, 0xC2]))
+ rom.write_bytes(0x007A40, bytearray([0x77, 0xB9, 0xB5, 0xBE, 0x9D, 0x20, 0x72, 0xA9, 0x00, 0xFC, 0x9D, 0x22, 0x72, 0xA9, 0x40, 0x00]))
+ rom.write_bytes(0x007A50, bytearray([0x9D, 0x42, 0x75, 0xA9, 0x90, 0x00, 0x22, 0xD2, 0x85, 0x00, 0x6B, 0x5A, 0xE2, 0x20, 0xAD, 0x51]))
+ rom.write_bytes(0x007A60, bytearray([0x14, 0xC9, 0x00, 0xC2, 0x20, 0xF0, 0x0D, 0x22, 0xCF, 0xF7, 0x00, 0x7A, 0x9B, 0xAD, 0x30, 0x00]))
+ rom.write_bytes(0x007A70, bytearray([0x5C, 0x62, 0xB7, 0x03, 0x22, 0xC5, 0xF7, 0x00, 0x7A, 0x9B, 0xA9, 0x03, 0x00, 0x5C, 0x62, 0xB7]))
+ rom.write_bytes(0x007A80, bytearray([0x03, 0x22, 0x23, 0xAF, 0x03, 0xE2, 0x20, 0xAD, 0x53, 0x14, 0xF0, 0x07, 0xC2, 0x20, 0x22, 0xCF]))
+ rom.write_bytes(0x007A90, bytearray([0xF7, 0x00, 0x6B, 0xC2, 0x20, 0x22, 0xC5, 0xF7, 0x00, 0x6B, 0xE2, 0x20, 0xAD, 0x53, 0x14, 0xF0]))
+ rom.write_bytes(0x007AA0, bytearray([0x07, 0xC2, 0x20, 0x22, 0x78, 0xBA, 0x07, 0x6B, 0xC2, 0x20, 0x6B, 0xC9, 0x06, 0x00, 0xB0, 0x0F]))
+ rom.write_bytes(0x007AB0, bytearray([0xE2, 0x20, 0xAD, 0x54, 0x14, 0xC9, 0x00, 0xC2, 0x20, 0xF0, 0x04, 0x5C, 0x94, 0x81, 0x07, 0x5C]))
+ rom.write_bytes(0x007AC0, bytearray([0xFB, 0x81, 0x07, 0x22, 0x23, 0xAF, 0x03, 0xE2, 0x20, 0xAD, 0x54, 0x14, 0xC2, 0x20, 0xF0, 0x08]))
+ rom.write_bytes(0x007AD0, bytearray([0x22, 0xCF, 0xF7, 0x00, 0x5C, 0xF7, 0x80, 0x07, 0x22, 0xC5, 0xF7, 0x00, 0x5C, 0xF7, 0x80, 0x07]))
+ rom.write_bytes(0x007AE0, bytearray([0x5A, 0xE2, 0x20, 0xAD, 0x55, 0x14, 0xC2, 0x20, 0xF0, 0x06, 0x22, 0xCF, 0xF7, 0x00, 0x80, 0x06]))
+ rom.write_bytes(0x007AF0, bytearray([0x22, 0xC5, 0xF7, 0x00, 0x80, 0x04, 0x22, 0x65, 0xC3, 0x0E, 0x7A, 0x5C, 0xFA, 0xBE, 0x0E, 0xE2]))
+ rom.write_bytes(0x007B00, bytearray([0x20, 0xAD, 0x56, 0x14, 0xC2, 0x20, 0xF0, 0x0A, 0x22, 0xCF, 0xF7, 0x00, 0x22, 0xB7, 0xA5, 0x03]))
+ rom.write_bytes(0x007B10, bytearray([0x80, 0x04, 0x22, 0xC5, 0xF7, 0x00, 0x5C, 0x3D, 0x96, 0x07, 0xBD, 0x02, 0x79, 0x85, 0x0E, 0xE2]))
+ rom.write_bytes(0x007B20, bytearray([0x20, 0xAD, 0x57, 0x14, 0xC2, 0x20, 0xF0, 0x05, 0x22, 0xCF, 0xF7, 0x00, 0x6B, 0x22, 0xC5, 0xF7]))
+ rom.write_bytes(0x007B30, bytearray([0x00, 0x6B, 0xE2, 0x20, 0xAD, 0x57, 0x14, 0xC2, 0x20, 0xD0, 0x0C, 0xAD, 0x74, 0x79, 0x29, 0x01]))
+ rom.write_bytes(0x007B40, bytearray([0x00, 0xD0, 0x04, 0x5C, 0x4A, 0xF3, 0x06, 0xBD, 0xD6, 0x79, 0x38, 0xFD, 0xE2, 0x70, 0x5C, 0x45]))
+ rom.write_bytes(0x007B50, bytearray([0xF2, 0x06, 0xAD, 0xAA, 0x60, 0x48, 0x30, 0x0E, 0xE2, 0x20, 0xAD, 0x57, 0x14, 0xC2, 0x20, 0xF0]))
+ rom.write_bytes(0x007B60, bytearray([0x05, 0x68, 0x5C, 0xA0, 0xF3, 0x06, 0x68, 0x5C, 0xE6, 0xF3, 0x06, 0xBD, 0x02, 0x79, 0x85, 0x0E]))
+ rom.write_bytes(0x007B70, bytearray([0xE2, 0x20, 0xAD, 0x57, 0x14, 0xC2, 0x20, 0xD0, 0x08, 0x22, 0xC5, 0xF7, 0x00, 0x5C, 0x35, 0xE5]))
+ rom.write_bytes(0x007B80, bytearray([0x06, 0x22, 0xCF, 0xF7, 0x00, 0x5C, 0x35, 0xE5, 0x06, 0xE2, 0x20, 0xAD, 0x57, 0x14, 0xC2, 0x20]))
+ rom.write_bytes(0x007B90, bytearray([0xD0, 0x0C, 0xAD, 0x74, 0x79, 0x29, 0x01, 0x00, 0xD0, 0x04, 0x5C, 0x48, 0xF3, 0x06, 0xBD, 0x36]))
+ rom.write_bytes(0x007BA0, bytearray([0x7A, 0x38, 0xE9, 0x08, 0x00, 0x5C, 0x63, 0xE8, 0x06, 0xAD, 0xAA, 0x60, 0x30, 0x0D, 0xE2, 0x20]))
+ rom.write_bytes(0x007BB0, bytearray([0xAD, 0x57, 0x14, 0xC2, 0x20, 0xF0, 0x04, 0x5C, 0x99, 0xE8, 0x06, 0x5C, 0xF1, 0xE8, 0x06, 0x9C]))
+ rom.write_bytes(0x007BC0, bytearray([0xB0, 0x61, 0x9C, 0x8C, 0x0C, 0xE2, 0x20, 0xAD, 0x58, 0x14, 0xC2, 0x20, 0xF0, 0x07, 0x9C, 0x8E]))
+ rom.write_bytes(0x007BD0, bytearray([0x0C, 0x5C, 0x9D, 0xA4, 0x02, 0xA9, 0x00, 0x00, 0x8F, 0xAE, 0x00, 0x70, 0x8F, 0xAC, 0x00, 0x70]))
+ rom.write_bytes(0x007BE0, bytearray([0xE2, 0x20, 0xA9, 0xFE, 0x9D, 0x78, 0x79, 0x8F, 0x49, 0x00, 0x7E, 0xC2, 0x20, 0x5C, 0x9D, 0xA4]))
+ rom.write_bytes(0x007BF0, bytearray([0x02, 0xE2, 0x20, 0xAF, 0x49, 0x00, 0x7E, 0xC2, 0x20, 0xF0, 0x0D, 0xA9, 0x00, 0x00, 0x9D, 0xD8]))
+ rom.write_bytes(0x007C00, bytearray([0x79, 0x9D, 0x78, 0x79, 0x8F, 0x49, 0x00, 0x7E, 0xBD, 0x16, 0x7C, 0x18, 0x5C, 0x51, 0xA3, 0x02]))
+ rom.write_bytes(0x007C10, bytearray([0xE2, 0x20, 0xAD, 0x59, 0x14, 0xC2, 0x20, 0xD0, 0x0D, 0x22, 0xC5, 0xF7, 0x00, 0xBD, 0x38, 0x7D]))
+ rom.write_bytes(0x007C20, bytearray([0xF0, 0x0A, 0x5C, 0x4F, 0xA0, 0x02, 0x22, 0xCF, 0xF7, 0x00, 0x80, 0xF1, 0x5C, 0x59, 0xA0, 0x02]))
+ rom.write_bytes(0x007C30, bytearray([0xE2, 0x20, 0xAD, 0x59, 0x14, 0xC2, 0x20, 0xF0, 0x09, 0xBB, 0x22, 0x87, 0xBF, 0x03, 0x5C, 0x8D]))
+ rom.write_bytes(0x007C40, bytearray([0xA3, 0x02, 0x5C, 0x81, 0xA3, 0x02, 0xE2, 0x20, 0xAD, 0x5A, 0x14, 0xC2, 0x20, 0xF0, 0x09, 0xB5]))
+ rom.write_bytes(0x007C50, bytearray([0x76, 0x29, 0xFF, 0x00, 0x5C, 0x9D, 0x93, 0x02, 0x8D, 0x04, 0x30, 0xA9, 0x00, 0x00, 0x8D, 0x08]))
+ rom.write_bytes(0x007C60, bytearray([0x30, 0x5C, 0xA5, 0x93, 0x02, 0xE2, 0x20, 0xAD, 0x5A, 0x14, 0xC2, 0x20, 0xD0, 0x01, 0x6B, 0x22]))
+ rom.write_bytes(0x007C70, bytearray([0x23, 0xAF, 0x03, 0x5C, 0xDA, 0x93, 0x02, 0xE2, 0x20, 0xAD, 0x5B, 0x14, 0xC2, 0x20, 0xF0, 0x09]))
+ rom.write_bytes(0x007C80, bytearray([0x9B, 0xBD, 0xD6, 0x79, 0x0A, 0x5C, 0xCA, 0xC4, 0x05, 0x6B, 0xE2, 0x20, 0xAD, 0x5B, 0x14, 0xC2]))
+ rom.write_bytes(0x007C90, bytearray([0x20, 0xF0, 0x09, 0x9B, 0xBD, 0xD6, 0x79, 0x0A, 0x5C, 0xC1, 0xC8, 0x05, 0x6B, 0x22, 0x52, 0xAA]))
+ rom.write_bytes(0x007CA0, bytearray([0x03, 0xE2, 0x20, 0xAD, 0x5B, 0x14, 0xC2, 0x20, 0xF0, 0x0A, 0xA0, 0x00, 0x22, 0xD1, 0xF7, 0x00]))
+ rom.write_bytes(0x007CB0, bytearray([0x5C, 0xD9, 0xC4, 0x05, 0x22, 0xC5, 0xF7, 0x00, 0x5C, 0x70, 0xC5, 0x05, 0x22, 0x23, 0xAF, 0x03]))
+ rom.write_bytes(0x007CC0, bytearray([0xE2, 0x20, 0xAD, 0x5C, 0x14, 0xC2, 0x20, 0xF0, 0x0A, 0xA0, 0x00, 0x22, 0xD1, 0xF7, 0x00, 0x5C]))
+ rom.write_bytes(0x007CD0, bytearray([0x24, 0xC9, 0x0C, 0x22, 0xC5, 0xF7, 0x00, 0x80, 0xF6, 0xE2, 0x20, 0xAD, 0x5C, 0x14, 0xC2, 0x20]))
+ rom.write_bytes(0x007CE0, bytearray([0xF0, 0x08, 0x8A, 0x8D, 0x02, 0x30, 0x5C, 0x4D, 0xCD, 0x0C, 0xFA, 0x5C, 0x3A, 0xCD, 0x0C, 0x48]))
+ rom.write_bytes(0x007CF0, bytearray([0xDA, 0xE2, 0x20, 0xAD, 0x5D, 0x14, 0xF0, 0x33, 0xAA, 0x4C, 0x53, 0xFF, 0xFF, 0x18, 0x4C, 0x71]))
+ rom.write_bytes(0x007D00, bytearray([0xFF, 0x8D, 0x5E, 0x14, 0xC2, 0x20, 0xFA, 0x68, 0x1A, 0x1A, 0xC9, 0x0E, 0x00, 0x90, 0x06, 0x80]))
+ rom.write_bytes(0x007D10, bytearray([0x16, 0x5C, 0x15, 0xBF, 0x03, 0xE2, 0x20, 0x48, 0xBD, 0x60, 0x73, 0xC9, 0x27, 0xF0, 0x12, 0x68]))
+ rom.write_bytes(0x007D20, bytearray([0xCD, 0x5E, 0x14, 0xC2, 0x20, 0x90, 0xEA, 0x5C, 0xE5, 0xFA, 0x0B, 0x1A, 0x8D, 0x5D, 0x14, 0x80]))
+ rom.write_bytes(0x007D30, bytearray([0xC0, 0x68, 0xC2, 0x20, 0xEE, 0xCC, 0x00, 0xEE, 0xCC, 0x00, 0x80, 0xD5, 0xA8, 0x5C, 0x20, 0xBF]))
+ rom.write_bytes(0x007D40, bytearray([0x03, 0x8B, 0xA9, 0x03, 0x8D, 0x4B, 0x09, 0x8D, 0x01, 0x21, 0x22, 0x39, 0xB4, 0x00, 0x22, 0x79]))
+ rom.write_bytes(0x007D50, bytearray([0x82, 0x10, 0xDA, 0xAD, 0x0E, 0x03, 0x4A, 0xAA, 0xBF, 0xF3, 0xFE, 0x06, 0xAA, 0xAD, 0x1A, 0x02]))
+ rom.write_bytes(0x007D60, bytearray([0x9F, 0x00, 0x7C, 0x70, 0x9C, 0x22, 0x02, 0xAF, 0x83, 0xFC, 0x0D, 0xAA, 0xBF, 0xB2, 0xAF, 0x09]))
+ rom.write_bytes(0x007D70, bytearray([0x0C, 0xCE, 0x00, 0xAD, 0x60, 0x14, 0x0C, 0xCE, 0x00, 0x5A, 0xC2, 0x10, 0xA2, 0xAA, 0xAF, 0xAD]))
+ rom.write_bytes(0x007D80, bytearray([0xCE, 0x00, 0x89, 0x01, 0xF0, 0x06, 0xA0, 0x22, 0x02, 0x20, 0xCA, 0xFD, 0x89, 0x02, 0xF0, 0x06]))
+ rom.write_bytes(0x007D90, bytearray([0xA0, 0x2E, 0x02, 0x20, 0xCA, 0xFD, 0x89, 0x04, 0xF0, 0x06, 0xA0, 0x3A, 0x02, 0x20, 0xCA, 0xFD]))
+ rom.write_bytes(0x007DA0, bytearray([0x89, 0x08, 0xF0, 0x06, 0xA0, 0x46, 0x02, 0x20, 0xCA, 0xFD, 0x89, 0x10, 0xF0, 0x06, 0xA0, 0x52]))
+ rom.write_bytes(0x007DB0, bytearray([0x02, 0x20, 0xCA, 0xFD, 0x89, 0x20, 0xF0, 0x06, 0xA0, 0x5E, 0x02, 0x20, 0xCA, 0xFD, 0x9C, 0x65]))
+ rom.write_bytes(0x007DC0, bytearray([0x02, 0xE2, 0x10, 0x7A, 0xFA, 0xAB, 0x5C, 0xB6, 0xA5, 0x17, 0xC2, 0x20, 0x48, 0xA9, 0x07, 0x00]))
+ rom.write_bytes(0x007DD0, bytearray([0xDA, 0x54, 0x00, 0x09, 0xFA, 0x68, 0xE2, 0x20, 0x60, 0xDA, 0x5A, 0x8B, 0xAD, 0x0E, 0x03, 0xC2]))
+ rom.write_bytes(0x007DE0, bytearray([0x20, 0xC2, 0x10, 0xAA, 0xBF, 0x07, 0xFF, 0x06, 0xA8, 0xE2, 0x20, 0xA9, 0x00, 0xEB, 0xA9, 0x7F]))
+ rom.write_bytes(0x007DF0, bytearray([0xA2, 0xC0, 0x14, 0x54, 0x70, 0x7E, 0xA2, 0xC0, 0x14, 0xA0, 0x40, 0x14, 0xA9, 0x00, 0xEB, 0xA9]))
+ rom.write_bytes(0x007E00, bytearray([0x7F, 0x54, 0x7E, 0x7E, 0xE2, 0x10, 0xAB, 0x7A, 0xFA, 0xA9, 0x1E, 0x8D, 0x18, 0x01, 0xAF, 0x83]))
+ rom.write_bytes(0x007E10, bytearray([0xFC, 0x0D, 0xDA, 0xAA, 0xBF, 0xB8, 0xAF, 0x09, 0x8D, 0x18, 0x02, 0xAF, 0x88, 0xFC, 0x0D, 0x49]))
+ rom.write_bytes(0x007E20, bytearray([0x01, 0x8D, 0x5A, 0x14, 0xFA, 0x5C, 0x58, 0x99, 0x17, 0xAE, 0x15, 0x11, 0xAD, 0x60, 0x14, 0x89]))
+ rom.write_bytes(0x007E30, bytearray([0x01, 0xD0, 0x0D, 0xAF, 0x83, 0xFC, 0x0D, 0xF0, 0x07, 0x9E, 0x10, 0x00, 0x5C, 0xB1, 0xD8, 0x17]))
+ rom.write_bytes(0x007E40, bytearray([0xFE, 0x10, 0x00, 0x80, 0xF7, 0xA9, 0xF0, 0x85, 0x4D, 0x8D, 0x63, 0x14, 0xA9, 0x80, 0x8D, 0x20]))
+ rom.write_bytes(0x007E50, bytearray([0x02, 0x8D, 0x4A, 0x00, 0x5C, 0x59, 0xC1, 0x01, 0xE2, 0x20, 0xAD, 0x61, 0x14, 0x89, 0x01, 0xF0]))
+ rom.write_bytes(0x007E60, bytearray([0x08, 0x48, 0xA9, 0x09, 0x8F, 0x17, 0x03, 0x17, 0x68, 0x89, 0x02, 0xF0, 0x08, 0x48, 0xA9, 0x09]))
+ rom.write_bytes(0x007E70, bytearray([0x8F, 0x23, 0x03, 0x17, 0x68, 0x89, 0x04, 0xF0, 0x08, 0x48, 0xA9, 0x09, 0x8F, 0x2F, 0x03, 0x17]))
+ rom.write_bytes(0x007E80, bytearray([0x68, 0x89, 0x08, 0xF0, 0x08, 0x48, 0xA9, 0x09, 0x8F, 0x3B, 0x03, 0x17, 0x68, 0x89, 0x10, 0xF0]))
+ rom.write_bytes(0x007E90, bytearray([0x08, 0x48, 0xA9, 0x09, 0x8F, 0x47, 0x03, 0x17, 0x68, 0x89, 0x20, 0xF0, 0x08, 0x48, 0xA9, 0x09]))
+ rom.write_bytes(0x007EA0, bytearray([0x8F, 0x53, 0x03, 0x17, 0x68, 0xAD, 0x62, 0x14, 0x89, 0x01, 0xF0, 0x08, 0x48, 0xA9, 0x0A, 0x8F]))
+ rom.write_bytes(0x007EB0, bytearray([0x18, 0x03, 0x17, 0x68, 0x89, 0x02, 0xF0, 0x08, 0x48, 0xA9, 0x0A, 0x8F, 0x24, 0x03, 0x17, 0x68]))
+ rom.write_bytes(0x007EC0, bytearray([0x89, 0x04, 0xF0, 0x08, 0x48, 0xA9, 0x0A, 0x8F, 0x30, 0x03, 0x17, 0x68, 0x89, 0x08, 0xF0, 0x08]))
+ rom.write_bytes(0x007ED0, bytearray([0x48, 0xA9, 0x0A, 0x8F, 0x3C, 0x03, 0x17, 0x68, 0x89, 0x10, 0xF0, 0x08, 0x48, 0xA9, 0x0A, 0x8F]))
+ rom.write_bytes(0x007EE0, bytearray([0x48, 0x03, 0x17, 0x68, 0x89, 0x20, 0xF0, 0x08, 0x48, 0xA9, 0x0A, 0x8F, 0x54, 0x03, 0x17, 0x68]))
+ rom.write_bytes(0x007EF0, bytearray([0xC2, 0x20, 0x5C, 0x26, 0xDB, 0x17, 0xAD, 0x63, 0x14, 0xF0, 0x0E, 0xA9, 0x00, 0x8D, 0x63, 0x14]))
+ rom.write_bytes(0x007F00, bytearray([0xA9, 0x20, 0x8D, 0x18, 0x01, 0x5C, 0x04, 0xA9, 0x17, 0xA9, 0x25, 0x80, 0xF5, 0xAD, 0x06, 0x7E]))
+ rom.write_bytes(0x007F10, bytearray([0xD0, 0x15, 0xE2, 0x20, 0xAD, 0x64, 0x14, 0xD0, 0x0E, 0xAF, 0x84, 0xFC, 0x0D, 0x89, 0x01, 0xD0]))
+ rom.write_bytes(0x007F20, bytearray([0x06, 0xC2, 0x20, 0x5C, 0x49, 0xEA, 0x0C, 0xC2, 0x20, 0x5C, 0x47, 0xEA, 0x0C, 0xAD, 0x06, 0x7E]))
+ rom.write_bytes(0x007F30, bytearray([0xD0, 0x15, 0xE2, 0x20, 0xAD, 0x64, 0x14, 0xD0, 0x0E, 0xAF, 0x84, 0xFC, 0x0D, 0x89, 0x02, 0xD0]))
+ rom.write_bytes(0x007F40, bytearray([0x06, 0xC2, 0x20, 0x5C, 0x91, 0xC0, 0x03, 0xC2, 0x20, 0x5C, 0xCC, 0xC0, 0x03]))
+ rom.write_bytes(0x007F53, bytearray([0xBF, 0xA3, 0xAF, 0x09, 0xE0, 0x06, 0xF0, 0x03, 0x4C, 0xFD, 0xFC, 0x4C, 0x01, 0xFD]))
+ rom.write_bytes(0x007F61, bytearray([0xAF, 0xAE, 0x00, 0x70, 0xD0, 0x07, 0xFA, 0xA9, 0x0E, 0x00, 0x4C, 0x2C, 0xFA, 0x4C, 0x26, 0xFA]))
+ rom.write_bytes(0x007F71, bytearray([0x6D, 0xCC, 0x00, 0xC9, 0x0E, 0x90, 0x02, 0xA9, 0x0E, 0x4C, 0x01, 0xFD]))
+
+ rom.write_bytes(0x077F82, bytearray([0xE2, 0x20, 0xAD, 0x40, 0x14, 0xD0, 0x08, 0xC2, 0x20, 0x22, 0xC5, 0xF7, 0x00, 0x80]))
+ rom.write_bytes(0x077F90, bytearray([0x06, 0xA0, 0x00, 0x22, 0xD1, 0xF7, 0x00, 0xC2, 0x20, 0x20, 0x1A, 0xB6, 0x60, 0xE2, 0x20, 0xAD]))
+ rom.write_bytes(0x077FA0, bytearray([0x55, 0x14, 0xC2, 0x20, 0xD0, 0x03, 0x4C, 0xFD, 0xBE, 0x20, 0xBB, 0xBF, 0x4C, 0xFD, 0xBE]))
+
+ rom.write_bytes(0x01FEEE, bytearray([0xB9, 0x00]))
+ rom.write_bytes(0x01FEF0, bytearray([0x6F, 0x48, 0xDA, 0xBD, 0x60, 0x73, 0xA2, 0x00, 0xDF, 0x70, 0xAF, 0x09, 0xF0, 0x08, 0xE8, 0xE8]))
+ rom.write_bytes(0x01FF00, bytearray([0xE0, 0x08, 0xF0, 0x1A, 0x80, 0xF2, 0x8A, 0x4A, 0xE2, 0x20, 0xAA, 0xBF, 0x78, 0xAF, 0x09, 0xAA]))
+ rom.write_bytes(0x01FF10, bytearray([0xBD, 0x40, 0x14, 0xC2, 0x20, 0xD0, 0x07, 0xFA, 0x68, 0xC9, 0x00, 0x00, 0x80, 0x05, 0xFA, 0x68]))
+ rom.write_bytes(0x01FF20, bytearray([0xC9, 0x10, 0x00, 0x5C, 0x34, 0xC3, 0x03, 0xAE, 0x12, 0x98, 0xE2, 0x20, 0xAD, 0x5E, 0x14, 0xC9]))
+ rom.write_bytes(0x01FF30, bytearray([0x0E, 0xF0, 0x08, 0x3A, 0x3A, 0xA8, 0xC2, 0x20, 0x4C, 0x15, 0xBF, 0x98, 0x80, 0xF8]))
+
+ rom.write_bytes(0x02FFC0, bytearray([0x0C, 0xA6, 0x12, 0x6B, 0xBD, 0x60, 0x73, 0xC9, 0x1E, 0x01, 0xE2, 0x20, 0xF0, 0x05, 0xAD, 0x4C]))
+ rom.write_bytes(0x02FFD0, bytearray([0x14, 0x80, 0x03, 0xAD, 0x4B, 0x14, 0xC9, 0x00, 0xC2, 0x20, 0xF0, 0x03, 0x20, 0xF6, 0xF1, 0x4C]))
+ rom.write_bytes(0x02FFE0, bytearray([0xB0, 0xF0]))
+
+ rom.write_bytes(0x017FD7, bytearray([0xE2, 0x20, 0xAD, 0x4D, 0x14, 0xC2, 0x20, 0xF0, 0x10]))
+ rom.write_bytes(0x017FE0, bytearray([0xBD, 0x60, 0x73, 0xC9, 0xA9, 0x01, 0xF0, 0x04, 0xA9, 0x04, 0x00, 0x60, 0xA9, 0x0A, 0x00, 0x60]))
+ rom.write_bytes(0x017FF0, bytearray([0x68, 0x4C, 0x90, 0xAD]))
+
+ rom.write_bytes(0x03FF48, bytearray([0xE2, 0x20, 0xAD, 0x56, 0x14, 0xC2, 0x20, 0xD0]))
+ rom.write_bytes(0x03FF50, bytearray([0x03, 0x4C, 0x5B, 0x96, 0x20, 0x3D, 0x9D, 0x4C, 0x4F, 0x96]))
+
+
+def Item_Data(rom: LocalRom) -> None:
+ rom.write_bytes(0x04AF70, bytearray([0xBB, 0x00, 0xBA, 0x00, 0xC7, 0x00, 0xC8, 0x00, 0x01, 0x02, 0x03, 0x03, 0xB1, 0x00, 0xB0, 0x00]))
+ rom.write_bytes(0x04AF80, bytearray([0xB2, 0x00, 0xAF, 0x00, 0xB4, 0x00, 0x04, 0x05, 0x06, 0x07, 0x08, 0x07, 0x00, 0x05, 0x00, 0x09]))
+ rom.write_bytes(0x04AF90, bytearray([0x00, 0x0D, 0x0E, 0x0F, 0x22, 0x00, 0x26, 0x00, 0x29, 0x00, 0x2A, 0x00, 0x2B, 0x00, 0x11, 0x12]))
+ rom.write_bytes(0x04AFA0, bytearray([0x12, 0x12, 0x12, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01]))
+ rom.write_bytes(0x04AFB0, bytearray([0x01, 0x01, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A]))
+
+
+def Server_Data(rom: LocalRom) -> None:
+ rom.write_bytes(0x037EAA, bytearray([0x00, 0x00, 0x01, 0x02, 0x03, 0x04]))
+ rom.write_bytes(0x037EB0, bytearray([0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14]))
+ rom.write_bytes(0x037EC0, bytearray([0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x24, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x01]))
+ rom.write_bytes(0x037ED0, bytearray([0x02, 0x04, 0x08, 0x10, 0x20, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30]))
+ rom.write_bytes(0x037EE0, bytearray([0x31, 0x32, 0x33, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0xFF, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39]))
+ rom.write_bytes(0x037EF0, bytearray([0x3A, 0x3B, 0x3C, 0x02, 0x6A, 0xD2, 0x04, 0x03, 0x06, 0x07, 0x08, 0x09, 0x05, 0x01, 0x02, 0x3D]))
+ rom.write_bytes(0x037F00, bytearray([0x3E, 0x3F, 0x40, 0x01, 0x02, 0x03, 0x0A, 0x80, 0x7E, 0x00, 0x7F, 0x80, 0x7F]))
+
+
+def Menu_Data(rom: LocalRom) -> None:
+ rom.write_bytes(0x115348, bytearray([0x80, 0x80, 0x4E, 0x80, 0x80, 0x4E, 0x80, 0x80]))
+ rom.write_bytes(0x115350, bytearray([0x4E, 0x80, 0x80, 0x4E, 0x80, 0x80, 0x4E, 0x80, 0x80, 0x4E, 0x80, 0x80, 0x4E, 0x80, 0x80, 0x4E]))
+ rom.write_bytes(0x115360, bytearray([0x80, 0x80, 0x4E, 0x80, 0x80, 0x4E, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x03]))
+ rom.write_bytes(0x115370, bytearray([0x03, 0x03, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x08, 0x08]))
+ rom.write_bytes(0x115380, bytearray([0x08, 0x09, 0x09, 0x09, 0x0A, 0x0A, 0x0A, 0x24, 0x2C, 0x00, 0x06, 0x1E, 0x00, 0x06, 0x24, 0x00]))
+ rom.write_bytes(0x115390, bytearray([0x02, 0x24, 0x00, 0x0E, 0x04, 0x00, 0x18, 0x26, 0x00, 0x26, 0x1A, 0x00, 0x04, 0x22, 0x00, 0x24]))
+ rom.write_bytes(0x1153A0, bytearray([0x18, 0x00, 0x24, 0x02, 0x00, 0x16, 0x24, 0x00, 0x00, 0x2C, 0x00, 0x2A, 0x2C, 0x00, 0x2C, 0x18]))
+ rom.write_bytes(0x1153B0, bytearray([0x00, 0x10, 0x18, 0x00, 0x0A, 0x18, 0x00, 0x24, 0x24, 0x00, 0x0A, 0x08, 0x00, 0x0C, 0x08, 0x00]))
+ rom.write_bytes(0x1153C0, bytearray([0x08, 0x16, 0x00, 0x08, 0x1E, 0x00, 0x04, 0x14, 0x00, 0x1E, 0x0E, 0x00, 0x1E, 0x0C, 0x00, 0x24]))
+ rom.write_bytes(0x1153D0, bytearray([0x14, 0x00, 0x14, 0x30, 0x00, 0x18, 0x22, 0x00, 0x02, 0x04, 0x00, 0x26, 0x16, 0x00, 0x24, 0x16]))
+ rom.write_bytes(0x1153E0, bytearray([0x00, 0x5C, 0x38, 0x60, 0x4E, 0x28, 0x1A, 0x16, 0x1C, 0x04, 0x14, 0x36, 0x36, 0x36, 0x80, 0x80]))
+ rom.write_bytes(0x1153F0, bytearray([0x34, 0x81, 0x81, 0x4E, 0x4E, 0x4E, 0x5C, 0x38, 0x60, 0x4E, 0x04, 0x16, 0x08, 0x00, 0x22, 0x36]))
+ rom.write_bytes(0x115400, bytearray([0x36, 0x36, 0x36, 0x80, 0x80, 0x34, 0x81, 0x81, 0x4E, 0x4E, 0x4E, 0x50, 0x52, 0x54, 0x56, 0x58]))
+ rom.write_bytes(0x115410, bytearray([0x5A, 0x5C, 0x5E, 0x60, 0x62, 0x50, 0x52, 0x54, 0x09, 0x15, 0x21, 0x2D, 0x39, 0x45, 0x0C, 0x03]))
+ rom.write_bytes(0x115420, bytearray([0x07, 0x0F, 0x13, 0x1B, 0x1F, 0x27, 0x2B, 0x33, 0x37, 0x3F, 0x00, 0x00, 0x02, 0x02, 0x04, 0x04]))
+ rom.write_bytes(0x115430, bytearray([0x06, 0x06, 0x08, 0x08, 0x0A, 0x41, 0x00, 0x3C, 0x00, 0x33, 0x00, 0x25, 0x00, 0x1B, 0x00, 0x14]))
+ rom.write_bytes(0x115440, bytearray([0x00, 0x0B, 0x00, 0x02, 0x00, 0xF6, 0x3F, 0xEC, 0x3F, 0xDC, 0x3F]))
+
+ rom.write_bytes(0x082660, bytearray([0x07]))
+ rom.write_bytes(0x082667, bytearray([0x05]))
+ rom.write_bytes(0x082677, bytearray([0x0A, 0x03, 0x05]))
+ rom.write_bytes(0x082688, bytearray([0x00]))
+
+ rom.write_bytes(0x11548E, bytearray([0x60, 0x3d, 0x66, 0x3b, 0x60, 0x3f, 0x60, 0x39, 0x66, 0x39, 0x66, 0x3d, 0x66, 0x3f, 0x60, 0x3b]))
+ rom.write_bytes(0x11549E, bytearray([0x02, 0x06, 0x04, 0x00, 0x01, 0x03, 0x05, 0x07]))
+
+
+def CodeHandler(rom: LocalRom) -> None:
+ rom.write_bytes(0x073637, bytearray([0x5C, 0xB0, 0xF7, 0x00])) # Check ! Switch
+ rom.write_bytes(0x07360B, bytearray([0x20, 0x82, 0xFF])) # Flash ! Switch
+
+ rom.write_bytes(0x01C2F3, bytearray([0x22, 0x11, 0xF8, 0x00])) # Check visibility of winged clouds
+ rom.write_bytes(0x01C32E, bytearray([0x5C, 0xEE, 0xFE, 0x03])) # Check items in winged clouds
+
+ rom.write_bytes(0x01C9AD, bytearray([0x5C, 0x19, 0xF8, 0x00])) # Check transformations
+ rom.write_bytes(0x01C995, bytearray([0x5C, 0x59, 0xF8, 0x00])) # Flash transformations
+ rom.write_bytes(0x01C943, bytearray([0x5C, 0x77, 0xF8, 0x00])) # Fixes a bug where transformation bubbles flashing would displace the sprite
+
+ rom.write_bytes(0x028329, bytearray([0x5C, 0x9A, 0xF8, 0x00])) # Flash Spring Ball
+ rom.write_bytes(0x02837E, bytearray([0x5C, 0xC4, 0xF8, 0x00])) # Check Spring Ball
+
+ rom.write_bytes(0x02F0A2, bytearray([0x5C, 0xEA, 0xF8, 0x00])) # Flash Arrow Wheel
+ rom.write_bytes(0x02F0AD, bytearray([0x4C, 0xC4, 0xFF])) # Check Arrow Wheel
+
+ rom.write_bytes(0x02001D, bytearray([0x5C, 0x15, 0xF9, 0x00])) # Check Melon
+ rom.write_bytes(0x020028, bytearray([0x5C, 0x41, 0xF9, 0x00])) # Secondary check for melon used to overwrite visibility on the ground
+ rom.write_bytes(0x020031, bytearray([0x5C, 0xAE, 0xF9, 0x00])) # Check for melons that are spawned by objects which skips the initial check
+ rom.write_bytes(0x012DF7, bytearray([0x20, 0xD7, 0xFF])) # Check for monkeys holding melons
+ rom.write_bytes(0x012E07, bytearray([0x20, 0xD7, 0xFF])) # Check for monkeys holding melons
+ rom.write_bytes(0x03F17D, bytearray([0x5C, 0xE1, 0xF9, 0x00])) # Fixes a bug where balloons with ice melons will write to yoshi's mouth before deactivating the melon.
+
+ rom.write_bytes(0x011901, bytearray([0x5C, 0x7A, 0xF9, 0x00])) # Flash Super Star
+ rom.write_bytes(0x01192A, bytearray([0x5C, 0x95, 0xF9, 0x00])) # Check Super Star
+
+ rom.write_bytes(0x01BEB9, bytearray([0x5C, 0xF6, 0xF9, 0x00])) # Check egg-type items
+ rom.write_bytes(0x01B75E, bytearray([0x5C, 0x5B, 0xFA, 0x00])) # Flash flashing eggs and force them to purple
+
+ rom.write_bytes(0x03BA31, bytearray([0x22, 0x81, 0xFA, 0x00])) # Flash Arrow Cloud
+ rom.write_bytes(0x03BA35, bytearray([0x22, 0x9A, 0xFA, 0x00])) # Check Arrow Cloud
+ rom.write_bytes(0x03BA3D, bytearray([0x22, 0x81, 0xFA, 0x00])) # Flash Arrow Cloud, rotating
+ rom.write_bytes(0x03BA5A, bytearray([0x22, 0x9A, 0xFA, 0x00])) # Check Arrow Cloud, rotating
+
+ rom.write_bytes(0x03818F, bytearray([0x5C, 0xAB, 0xFA, 0x00])) # Check Egg Plant
+ rom.write_bytes(0x0380F3, bytearray([0x5C, 0xC3, 0xFA, 0x00])) # Flash Egg Plant
+
+ rom.write_bytes(0x073EF6, bytearray([0x5C, 0xE0, 0xFA, 0x00])) # Flash Chomp Rock
+ rom.write_bytes(0x073EFA, bytearray([0x4C, 0x9D, 0xFF])) # Check Chomp Rock
+
+ rom.write_bytes(0x039639, bytearray([0x5C, 0xFF, 0xFA, 0x00])) # Flash Poochy
+ rom.write_bytes(0x03964C, bytearray([0x4C, 0x48, 0xFF])) # Check Poochy
+
+ rom.write_bytes(0x0370C2, bytearray([0x22, 0x1A, 0xFB, 0x00, 0xEA])) # Flash Platform Ghosts
+ rom.write_bytes(0x03723F, bytearray([0x5C, 0x32, 0xFB, 0x00])) # Fixes a bug where the eyes would assign to a random sprite while flashing
+ rom.write_bytes(0x03739B, bytearray([0x5C, 0x52, 0xFB, 0x00])) # Check Vertical Platform Ghost
+ rom.write_bytes(0x036530, bytearray([0x5C, 0x6B, 0xFB, 0x00])) # Flash horizontal ghost
+ rom.write_bytes(0x03685C, bytearray([0x5C, 0x89, 0xFB, 0x00])) # Fix flashing horizontal ghost
+ rom.write_bytes(0x036894, bytearray([0x5C, 0xA9, 0xFB, 0x00])) # Check horizontal ghost
+
+ rom.write_bytes(0x012497, bytearray([0x5C, 0xBF, 0xFB, 0x00])) # Check Skis
+ rom.write_bytes(0x01234D, bytearray([0x5C, 0xF1, 0xFB, 0x00])) # Allow ski doors to be re-entered
+
+ rom.write_bytes(0x01204A, bytearray([0x5C, 0x10, 0xFC, 0x00])) # Flash Key
+ rom.write_bytes(0x012388, bytearray([0x5C, 0x30, 0xFC, 0x00])) # Check Key
+
+ rom.write_bytes(0x011398, bytearray([0x5C, 0x46, 0xFC, 0x00])) # Flash MidRing
+ rom.write_bytes(0x0113D6, bytearray([0x5C, 0x65, 0xFC, 0x00])) # Check MidRing
+
+ rom.write_bytes(0x02C4C6, bytearray([0x5C, 0x77, 0xFC, 0x00])) # Check Bucket w/ Item
+ rom.write_bytes(0x02C8BD, bytearray([0x5C, 0x8A, 0xFC, 0x00])) # Check Bucket, ridable
+ rom.write_bytes(0x02C4D5, bytearray([0x5C, 0x9D, 0xFC, 0x00])) # Flash Bucket
+
+ rom.write_bytes(0x064920, bytearray([0x5C, 0xBC, 0xFC, 0x00])) # Flash Tulip
+ rom.write_bytes(0x064D49, bytearray([0x5C, 0xD9, 0xFC, 0x00])) # Check Tulip
+
+ rom.write_bytes(0x01BEC7, bytearray([0x5C, 0xEF, 0xFC, 0x00])) # Check Egg Capacity
+ rom.write_bytes(0x01BF12, bytearray([0x4C, 0x27, 0xFF])) # Set current egg max
+ rom.write_bytes(0x01BF1A, bytearray([0x5C, 0x3C, 0xFD, 0x00])) # Cap eggs
+
+ rom.write_bytes(0x0BA5AE, bytearray([0x5C, 0x41, 0xFD, 0x00])) # Unlock Levels
+
+ rom.write_bytes(0x0B9953, bytearray([0x5C, 0xD9, 0xFD, 0x00])) # File initialization
+
+ rom.write_bytes(0x0BD8AB, bytearray([0x5C, 0x29, 0xFE, 0x00])) # Prevent the world 1 tab from being drawn without it being unlocked
+
+ rom.write_bytes(0x00C155, bytearray([0x5C, 0x45, 0xFE, 0x00])) # Save between levels
+
+ rom.write_bytes(0x0BDB20, bytearray([0x5C, 0x58, 0xFE, 0x00])) # Unlock extra and bonus stages
+
+ rom.write_bytes(0x0BA8FF, bytearray([0x5C, 0xF6, 0xFE, 0x00])) # Skip the score animation if coming from start-select, but still save
+
+ rom.write_bytes(0x0BA8A9, bytearray([0x80, 0x46])) # Prevent unlocking new levels
+
+ rom.write_bytes(0x066A42, bytearray([0x5C, 0x0D, 0xFF, 0x00])) # Coin visibility
+ rom.write_bytes(0x01C08C, bytearray([0x5C, 0x2D, 0xFF, 0x00])) # Cloud visibility
+
+ rom.write_bytes(0x00C0D9, bytearray([0x5C, 0xB8, 0xF3, 0x0B])) # Receive item from server
+
+ rom.write_bytes(0x00C153, bytearray([0xEA, 0xEA])) # Always enable Start/Select
+
+ rom.write_bytes(0x00C18B, bytearray([0x5C, 0x1B, 0xF5, 0x0B])) # Enable traps
+
+ rom.write_bytes(0x01B365, bytearray([0x5C, 0x86, 0xF5, 0x0B])) # Red Coin checks
+ rom.write_bytes(0x0734C6, bytearray([0x5C, 0xCE, 0xF5, 0x0B])) # Flower checks
+ rom.write_bytes(0x00C0DE, bytearray([0x5C, 0xF5, 0xF5, 0x0B])) # Star checks
+ rom.write_bytes(0x00B580, bytearray([0x5C, 0xB1, 0xF5, 0x0B])) # Level Clear checks
+
+ rom.write_bytes(0x0B9937, bytearray([0x5C, 0x23, 0xF6, 0x0B])) # Load AP data
+ rom.write_bytes(0x0BE14A, bytearray([0x5C, 0x58, 0xF6, 0x0B])) # Save AP data
+
+ rom.write_bytes(0x00D09F, bytearray([0x5C, 0x8C, 0xF6, 0x0B])) # Clear Menu
+ rom.write_bytes(0x00BCB5, bytearray([0x5C, 0xAD, 0xF6, 0x0B])) # Clear Score for menu
+ rom.write_bytes(0x00D072, bytearray([0x5C, 0xC3, 0xF6, 0x0B])) # Loads the data for the AP menu
+ rom.write_bytes(0x00D07A, bytearray([0x5C, 0x5A, 0xF7, 0x0B])) # Draw the AP menu over the pause menu
+ rom.write_bytes(0x00D17A, bytearray([0x5C, 0xDA, 0xF7, 0x0B])) # Skip the flower counter in the AP menu
+ rom.write_bytes(0x00D0DE, bytearray([0x5C, 0xF1, 0xF7, 0x0B])) # Skip the coin counter in the AP menu
+ rom.write_bytes(0x00CFB4, bytearray([0x5C, 0x06, 0xF8, 0x0B])) # Get the number of bosses required to unlock 6-8
+ rom.write_bytes(0x00CFD0, bytearray([0x5C, 0x2B, 0xF8, 0x0B])) # Get bosses for 6-8 clear
+ rom.write_bytes(0x00D203, bytearray([0x5C, 0xF0, 0xF8, 0x0B])) # Wipe total score line
+ rom.write_bytes(0x00D277, bytearray([0x5C, 0x04, 0xF9, 0x0B])) # Wipe high score line
+ rom.write_bytes(0x00C104, bytearray([0x5C, 0x18, 0xF9, 0x0B])) # Replace the pause menu with AP menu when SELECT is pressed
+ rom.write_bytes(0x00C137, bytearray([0x5C, 0x31, 0xF9, 0x0B])) # Prevent accidentally quitting out of a stage while opening the AP menu
+ rom.write_bytes(0x00CE48, bytearray([0x5C, 0x42, 0xF9, 0x0B])) # When closing the AP menu, reset the AP menu flag so the normal menu can be opened.
+
+ rom.write_bytes(0x0BA5B6, bytearray([0x5C, 0x4E, 0xF9, 0x0B])) # Unlock 6-8 if the current number of defeated bosses is higher than the number of bosses required. If 6-8 is marked 'cleared', skip boss checks
+ rom.write_bytes(0x01209E, bytearray([0x5C, 0x92, 0xF9, 0x0B])) # Write a flag to check bosses if setting up the final boss door
+ rom.write_bytes(0x0123AA, bytearray([0x5C, 0xA3, 0xF9, 0x0B])) # If the boss check flag is set, read the number of bosses before opening door
+
+ rom.write_bytes(0x015F7A, bytearray([0x5C, 0xCA, 0xF9, 0x0B])) # Write Boss Clears
+
+ rom.write_bytes(0x0BE16E, bytearray([0x80, 0x12])) # Disable overworld bandit code
+
+ rom.write_bytes(0x083015, bytearray([0x5C, 0x26, 0xFA, 0x0B])) # Flip Cards
+ rom.write_bytes(0x0839B6, bytearray([0x5C, 0x18, 0xFA, 0x0B])) # Scratch Cards
+ rom.write_bytes(0x085094, bytearray([0x5C, 0x31, 0xFA, 0x0B])) # Draw Lots
+ rom.write_bytes(0x0852C5, bytearray([0x5C, 0x3D, 0xFA, 0x0B])) # Match Cards
+ rom.write_bytes(0x0845EA, bytearray([0x5C, 0x48, 0xFA, 0x0B])) # Roulette
+ rom.write_bytes(0x083E0A, bytearray([0x5C, 0x53, 0xFA, 0x0B])) # Slots
+
+ rom.write_bytes(0x01D845, bytearray([0x5C, 0x76, 0xF9, 0x0B])) # Check setting for disabled autoscrolls
+
+ rom.write_bytes(0x0BDAC2, bytearray([0x80, 0x0E])) # Prevent extra and bonus stages from auto-unlocking at 100 points
+ rom.write_bytes(0x0BA720, bytearray([0xA9, 0x00, 0x00])) # Always read level scores as 0. This stops extras and bonus from trying to unlock
+
+ rom.write_bytes(0x0BA720, bytearray([0xA9, 0x00, 0x00])) # Always read level scores as 0. This stops extras and bonus from trying to unlock
+
+ rom.write_bytes(0x03FE85, bytearray([0x5C, 0x09, 0xFB, 0x0B])) # Decrement the key counter when unlocking the 6-4 cork
+
+ rom.write_bytes(0x06F1B4, bytearray([0x5C, 0x22, 0xFB, 0x0B])) # Mark the goal and bowser clear after defeating bowser
+
+ rom.write_bytes(0x005FE2, bytearray([0x5C, 0x9C, 0xFB, 0x0B])) # Flag red coins as checked if the last one came from a pole
+
+ rom.write_bytes(0x01C2E1, bytearray([0x80])) # Makes hidden clouds not flash
+ rom.write_bytes(0x0120C0, bytearray([0x80])) # Prevents bandit game doors from sealing
+
+ rom.write_bytes(0x0382A7, bytearray([0x5C, 0xC2, 0xFB, 0x0B])) # Make cactus eggplants check the eggplant item correctly
+
+ rom.write_bytes(0x025E71, bytearray([0x5C, 0xFA, 0xFB, 0x0B])) # Write the stored reverse value
+
+ rom.write_bytes(0x00B587, bytearray([0x5C, 0x24, 0xFC, 0x0B])) # Store the reverse value and zero it
+
+ rom.write_bytes(0x0B9932, bytearray([0x5C, 0x96, 0xFA, 0x0B])) # Get 16 bit life count
+
+ rom.write_bytes(0x00C288, bytearray([0x00]))
+ rom.write_bytes(0x00C28B, bytearray([0x80])) # Disable baby mario tutorial text
+
+ rom.write_bytes(0x01141F, bytearray([0x80])) # Disable Middle Ring tutorial
+
+ rom.write_bytes(0x073534, bytearray([0x80])) # Disable Flower tutorial
+
+ rom.write_bytes(0x065B24, bytearray([0x5C, 0x45, 0xFC, 0x0B])) # Fix boss cutscenes
+
+ rom.write_bytes(0x011507, bytearray([0x5C, 0x70, 0xFC, 0x0B])) # Fix Hookbill middle ring during boss shuffle
+
+ rom.write_bytes(0x019E98, bytearray([0x5C, 0xB4, 0xFC, 0x0B])) # Flag red coins as checked if the last one was eaten
+
+ rom.write_bytes(0x011AB6, bytearray([0x5C, 0xD7, 0xFC, 0x0B])) # Check egg refills for how many eggs to spawn
+
+ rom.write_bytes(0x00DCA6, bytearray([0x5C, 0x00, 0xFD, 0x0B])) # Check egg refill pause use
+
+ rom.write_bytes(0x0BE06B, bytearray([0x5C, 0x56, 0xFD, 0x0B])) # Get level from shuffled order
+
+ rom.write_bytes(0x00C14B, bytearray([0xAE, 0x7C, 0x02, 0x8E, 0x1A, 0x02])) # Return to the original list when exiting a level
+
+ rom.write_bytes(0x00BEA8, bytearray([0x5C, 0x3F, 0xFE, 0x0B])) # Save the original level when beating a shuffled one.
+
+ rom.write_bytes(0x00E702, bytearray([0xAD, 0x7C, 0x02, 0x8D, 0x1A, 0x02, 0x80, 0x05])) # Save the original level when leaving through death
+
+ rom.write_bytes(0x0BE72A, bytearray([0x7C])) # Load yoshi colors by slot number not level number
+
+ rom.write_bytes(0x003346, bytearray([0x22, 0x54, 0xFE, 0x0B, 0xEA, 0xEA])) # Fix World 6 levels using weird tilesets
+
+ rom.write_bytes(0x003A37, bytearray([0x22, 0x54, 0xFE, 0x0B, 0xEA, 0xEA])) # Fix World 6 levels using weird tilesets
+
+ rom.write_bytes(0x0B87D5, bytearray([0x5C, 0x67, 0xFE, 0x0B]))
+
+ rom.write_bytes(0x07081F, bytearray([0x80])) # Fix for weird falling chomps. Why does this even read the world number?????
+
+ rom.write_bytes(0x0BC0B2, bytearray([0x5C, 0xD0, 0xED, 0x01])) # Load randomized yoshi colors on the world map
+
+ rom.write_bytes(0x0BC6F7, bytearray([0x5C, 0x04, 0xEE, 0x01])) # Load selected yoshi color on the world map
+
+ rom.write_bytes(0x0BC0AB, bytearray([0x80])) # Skip special color check for world 6; Levels handle this anyway
+
+
+def write_lives(rom: LocalRom) -> None:
+ rom.write_bytes(0x05FA96, bytearray([0xC2, 0x20, 0xAF, 0x89, 0xFC, 0x0D, 0x8D, 0x79, 0x03, 0xE2, 0x20, 0x5C, 0x37, 0x99, 0x17]))
+ rom.write_bytes(0x05FABF, bytearray([0x48, 0xE2, 0x20, 0xAD, 0xCC, 0x00, 0xF0, 0x06, 0xCE, 0xCC, 0x00, 0xCE, 0xCC, 0x00, 0xC2, 0x20, 0x68, 0x22, 0x87, 0xBF, 0x03, 0x5C, 0x89, 0xFE, 0x07]))
+
+
+def bonus_checks(rom: LocalRom) -> None:
+ rom.write_bytes(0x082156, bytearray([0x5C, 0x5F, 0xFA, 0x0B])) # Write bonus check
+
+
+def bandit_checks(rom: LocalRom) -> None:
+ rom.write_bytes(0x08C9E4, bytearray([0x5C, 0xF3, 0xF9, 0x0B])) # Write Bandit Checks
+
+
+def Handle_Locations(rom: LocalRom) -> None:
+ rom.write_bytes(0x05F3B8, bytearray([0xAD, 0x67, 0x14, 0xF0, 0x59, 0xDA, 0xC9, 0x1F]))
+ rom.write_bytes(0x05F3C0, bytearray([0xF0, 0x16, 0xC9, 0x20, 0xB0, 0x27, 0xAA, 0xBF, 0xAA, 0xFE, 0x06, 0xAA, 0xA9, 0x01, 0x9D, 0x40]))
+ rom.write_bytes(0x05F3D0, bytearray([0x14, 0xA9, 0x43, 0x8D, 0x53, 0x00, 0x80, 0x67, 0xAD, 0x5D, 0x14, 0xD0, 0x01, 0x1A, 0xC9, 0x06]))
+ rom.write_bytes(0x05F3E0, bytearray([0xF0, 0x04, 0x1A, 0x8D, 0x5D, 0x14, 0xA9, 0x03, 0x8D, 0x53, 0x00, 0x80, 0x52, 0xC9, 0x26, 0xB0]))
+ rom.write_bytes(0x05F3F0, bytearray([0x27, 0xA2, 0x00, 0xDF, 0xC9, 0xFE, 0x06, 0xF0, 0x03, 0xE8, 0x80, 0xF7, 0xBF, 0xCF, 0xFE, 0x06]))
+ rom.write_bytes(0x05F400, bytearray([0x8D, 0x4C, 0x00, 0xAD, 0x60, 0x14, 0x0C, 0x4C, 0x00, 0xAD, 0x4C, 0x00, 0x8D, 0x60, 0x14, 0xA9]))
+ rom.write_bytes(0x05F410, bytearray([0x97, 0x8D, 0x53, 0x00, 0x80, 0x29, 0x80, 0x70, 0xC9, 0x2D, 0xB0, 0x25, 0xA2, 0x00, 0xDF, 0xD5]))
+ rom.write_bytes(0x05F420, bytearray([0xFE, 0x06, 0xF0, 0x03, 0xE8, 0x80, 0xF7, 0xBF, 0xE3, 0xFE, 0x06, 0x8D, 0xCF, 0x00, 0xAD, 0x61]))
+ rom.write_bytes(0x05F430, bytearray([0x14, 0x0C, 0xCF, 0x00, 0xAD, 0xCF, 0x00, 0x8D, 0x61, 0x14, 0xA9, 0x95, 0x8D, 0x53, 0x00, 0x80]))
+ rom.write_bytes(0x05F440, bytearray([0x78, 0xC9, 0x34, 0xB0, 0x25, 0xA2, 0x00, 0xDF, 0xDC, 0xFE, 0x06, 0xF0, 0x03, 0xE8, 0x80, 0xF7]))
+ rom.write_bytes(0x05F450, bytearray([0xBF, 0xE3, 0xFE, 0x06, 0x8D, 0xCF, 0x00, 0xAD, 0x62, 0x14, 0x0C, 0xCF, 0x00, 0xAD, 0xCF, 0x00]))
+ rom.write_bytes(0x05F460, bytearray([0x8D, 0x62, 0x14, 0xA9, 0x95, 0x8D, 0x53, 0x00, 0x80, 0x4F, 0xC9, 0x3D, 0xB0, 0x1C, 0xA2, 0x00]))
+ rom.write_bytes(0x05F470, bytearray([0xDF, 0xEA, 0xFE, 0x06, 0xF0, 0x03, 0xE8, 0x80, 0xF7, 0xBF, 0xF6, 0xFE, 0x06, 0x22, 0xA6, 0x9C]))
+ rom.write_bytes(0x05F480, bytearray([0x10, 0xA9, 0x36, 0x8D, 0x53, 0x00, 0x80, 0x31, 0x80, 0x64, 0xC9, 0x41, 0xB0, 0x2D, 0xA2, 0x00]))
+ rom.write_bytes(0x05F490, bytearray([0xDF, 0xFF, 0xFE, 0x06, 0xF0, 0x03, 0xE8, 0x80, 0xF7, 0xA9, 0x00, 0xEB, 0xBF, 0x03, 0xFF, 0x06]))
+ rom.write_bytes(0x05F4A0, bytearray([0xAA, 0x18, 0xC2, 0x20, 0x6D, 0x79, 0x03, 0x8D, 0x79, 0x03, 0xE2, 0x20, 0xA9, 0x08, 0x22, 0xD2]))
+ rom.write_bytes(0x05F4B0, bytearray([0x85, 0x00, 0xCA, 0xE0, 0x00, 0xF0, 0x02, 0x80, 0xF5, 0x80, 0x51, 0xC9, 0x41, 0xF0, 0x1E, 0xC9]))
+ rom.write_bytes(0x05F4C0, bytearray([0x42, 0xF0, 0x2D, 0xC9, 0x43, 0xF0, 0x3A, 0xC2, 0x20, 0x5C, 0xFB, 0xB3, 0x21, 0x77, 0x14, 0xE2]))
+ rom.write_bytes(0x05F4D0, bytearray([0x20, 0xA9, 0x01, 0x8D, 0x7D, 0x02, 0xA9, 0x2E, 0x8D, 0x53, 0x00, 0x80, 0x2F, 0xA9, 0x01, 0x8D]))
+ rom.write_bytes(0x05F4E0, bytearray([0x68, 0x14, 0xC2, 0x20, 0xA9, 0x00, 0x04, 0x8D, 0x69, 0x14, 0xE2, 0x20, 0x80, 0x1E, 0x80, 0x22]))
+ rom.write_bytes(0x05F4F0, bytearray([0xC2, 0x20, 0xA9, 0x2C, 0x01, 0x8D, 0xCC, 0x0C, 0xE2, 0x20, 0xA9, 0xA0, 0x8D, 0x53, 0x00, 0x80]))
+ rom.write_bytes(0x05F500, bytearray([0x0B, 0xA9, 0x15, 0x8D, 0x53, 0x00, 0xA9, 0x05, 0x8F, 0xED, 0x61, 0x04, 0xFA, 0xA9, 0x00, 0x8D]))
+ rom.write_bytes(0x05F510, bytearray([0x67, 0x14, 0xA9, 0x10, 0x8D, 0x83, 0x0B, 0x5C, 0xDE, 0xC0, 0x01, 0xE2, 0x20, 0xAD, 0x7D, 0x02]))
+ rom.write_bytes(0x05F520, bytearray([0xF0, 0x25, 0xC2, 0x20, 0xAD, 0x7E, 0x02, 0xE2, 0x20, 0xF0, 0x12, 0xA9, 0x02, 0x8D, 0x00, 0x02]))
+ rom.write_bytes(0x05F530, bytearray([0xC2, 0x20, 0xAD, 0x7E, 0x02, 0x3A, 0x8D, 0x7E, 0x02, 0xE2, 0x20, 0x80, 0x0A, 0xA9, 0x0F, 0x8D]))
+ rom.write_bytes(0x05F540, bytearray([0x00, 0x02, 0xA9, 0x00, 0x8D, 0x7D, 0x02, 0xAD, 0x68, 0x14, 0xF0, 0x32, 0xC2, 0x20, 0xAD, 0x69]))
+ rom.write_bytes(0x05F550, bytearray([0x14, 0xF0, 0x1B, 0x3A, 0x8D, 0x69, 0x14, 0xE2, 0x20, 0x4C, 0x40, 0xFD, 0xE8, 0x1F, 0x70, 0xAD]))
+ rom.write_bytes(0x05F560, bytearray([0xD0, 0x00, 0xD0, 0x08, 0xEE, 0xD0, 0x00, 0xA9, 0x21, 0x8D, 0x53, 0x00, 0x80, 0x10, 0xE2, 0x20]))
+ rom.write_bytes(0x05F570, bytearray([0xA9, 0x22, 0x8D, 0x53, 0x00, 0xA9, 0x00, 0x8D, 0x68, 0x14, 0x8F, 0xE8, 0x1F, 0x70, 0x22, 0x59]))
+ rom.write_bytes(0x05F580, bytearray([0x82, 0x00, 0x5C, 0x8F, 0xC1, 0x01, 0xAC, 0xB4, 0x03, 0xC0, 0x14, 0x30, 0x20, 0x48, 0xDA, 0xE2]))
+ rom.write_bytes(0x05F590, bytearray([0x20, 0xAE, 0x1A, 0x02, 0xBD, 0x6D, 0x14, 0x8D, 0xD1, 0x00, 0xA9, 0x01, 0x0C, 0xD1, 0x00, 0xAD]))
+ rom.write_bytes(0x05F5A0, bytearray([0xD1, 0x00, 0x9D, 0x6D, 0x14, 0xC2, 0x20, 0xFA, 0x68, 0x5C, 0x6C, 0xB3, 0x03, 0x5C, 0x6D, 0xB3]))
+ rom.write_bytes(0x05F5B0, bytearray([0x03, 0xAE, 0x1A, 0x02, 0xBD, 0x6D, 0x14, 0x8D, 0xD1, 0x00, 0xA9, 0x08, 0x0C, 0xD1, 0x00, 0xAD]))
+ rom.write_bytes(0x05F5C0, bytearray([0xD1, 0x00, 0x9D, 0x6D, 0x14, 0xAE, 0x57, 0x0B, 0xE0, 0x0D, 0x5C, 0x85, 0xB5, 0x01, 0xA0, 0x05]))
+ rom.write_bytes(0x05F5D0, bytearray([0x8C, 0xB8, 0x03, 0x08, 0xE2, 0x20, 0xDA, 0x48, 0xAE, 0x1A, 0x02, 0xBD, 0x6D, 0x14, 0x8D, 0xD1]))
+ rom.write_bytes(0x05F5E0, bytearray([0x00, 0xA9, 0x02, 0x0C, 0xD1, 0x00, 0xAD, 0xD1, 0x00, 0x9D, 0x6D, 0x14, 0x68, 0xFA, 0xC2, 0x20]))
+ rom.write_bytes(0x05F5F0, bytearray([0x28, 0x5C, 0xCB, 0xB4, 0x0E, 0xC2, 0x20, 0xAD, 0xB6, 0x03, 0xC9, 0x2C, 0x01, 0x90, 0x18, 0xE2]))
+ rom.write_bytes(0x05F600, bytearray([0x20, 0xDA, 0xAE, 0x1A, 0x02, 0xBD, 0x6D, 0x14, 0x8D, 0xD1, 0x00, 0xA9, 0x04, 0x0C, 0xD1, 0x00]))
+ rom.write_bytes(0x05F610, bytearray([0xAD, 0xD1, 0x00, 0x9D, 0x6D, 0x14, 0xFA, 0x9C, 0x84, 0x0B, 0xE2, 0x20, 0xAD, 0x0F, 0x0D, 0x5C]))
+ rom.write_bytes(0x05F620, bytearray([0xE4, 0xC0, 0x01, 0xC2, 0x20, 0x48, 0xE2, 0x20, 0xA9, 0x1F, 0x8D, 0x18, 0x01, 0xDA, 0x5A, 0x8B]))
+ rom.write_bytes(0x05F630, bytearray([0x4C, 0xB2, 0xFA, 0xC2, 0x20, 0xC2, 0x10, 0xAA, 0xBF, 0x07, 0xFF, 0x06, 0xAA, 0xE2, 0x20, 0xA9]))
+ rom.write_bytes(0x05F640, bytearray([0x00, 0xEB, 0xA9, 0x7F, 0xA0, 0x40, 0x14, 0x54, 0x7E, 0x70, 0xE2, 0x10, 0xAB, 0x7A, 0xFA, 0xC2]))
+ rom.write_bytes(0x05F650, bytearray([0x20, 0x68, 0xE2, 0x20, 0x5C, 0x3C, 0x99, 0x17, 0xC2, 0x20, 0x48, 0xC2, 0x10, 0xDA, 0x5A, 0x8B]))
+ rom.write_bytes(0x05F660, bytearray([0xAD, 0x0E, 0x03, 0x29, 0x0F, 0x00, 0xAA, 0xBF, 0x07, 0xFF, 0x06, 0xA8, 0xE2, 0x20, 0xA9, 0x00]))
+ rom.write_bytes(0x05F670, bytearray([0xEB, 0xA9, 0x7F, 0xA2, 0x40, 0x14, 0x54, 0x70, 0x7E, 0xAB, 0x7A, 0xFA, 0xE2, 0x10, 0xC2, 0x20]))
+ rom.write_bytes(0x05F680, bytearray([0x68, 0xE2, 0x20, 0xAD, 0x3D, 0x09, 0x29, 0x20, 0x5C, 0x4F, 0xE1, 0x17, 0xE2, 0x20, 0xAD, 0xD2]))
+ rom.write_bytes(0x05F690, bytearray([0x00, 0xC2, 0x20, 0xD0, 0x09, 0xA9, 0xC1, 0xB1, 0x85, 0x10, 0x5C, 0xA4, 0xD0, 0x01, 0xA9, 0x00]))
+ rom.write_bytes(0x05F6A0, bytearray([0x00, 0x85, 0x10, 0x85, 0x12, 0x85, 0x14, 0x85, 0x16, 0x5C, 0xB3, 0xD0, 0x01, 0xE2, 0x20, 0xAD]))
+ rom.write_bytes(0x05F6B0, bytearray([0xD2, 0x00, 0xC2, 0x20, 0xD0, 0x09, 0xA9, 0x6F, 0x01, 0x05, 0x02, 0x5C, 0xBA, 0xBC, 0x01, 0x5C]))
+ rom.write_bytes(0x05F6C0, bytearray([0xBC, 0xBC, 0x01, 0xE2, 0x20, 0xAD, 0xD2, 0x00, 0xC2, 0x20, 0xD0, 0x0B, 0xBF, 0xED, 0xB7, 0x01]))
+ rom.write_bytes(0x05F6D0, bytearray([0x29, 0xFF, 0x00, 0x5C, 0x79, 0xD0, 0x01, 0xBF, 0x48, 0xD3, 0x22, 0x29, 0xFF, 0x00, 0xC9, 0x80]))
+ rom.write_bytes(0x05F6E0, bytearray([0x00, 0xF0, 0x04, 0x5C, 0x79, 0xD0, 0x01, 0xBF, 0x66, 0xD3, 0x22, 0xDA, 0xAA, 0xAD, 0xD3, 0x00]))
+ rom.write_bytes(0x05F6F0, bytearray([0x29, 0xFF, 0x00, 0xC9, 0x01, 0x00, 0xF0, 0x21, 0xC9, 0x02, 0x00, 0xF0, 0x38, 0xBD, 0x40, 0x14]))
+ rom.write_bytes(0x05F700, bytearray([0x29, 0xFF, 0x00, 0xF0, 0x0C, 0xFA, 0xBF, 0x87, 0xD3, 0x22, 0x29, 0xFF, 0x00, 0x5C, 0x79, 0xD0]))
+ rom.write_bytes(0x05F710, bytearray([0x01, 0xFA, 0xA9, 0x4E, 0x00, 0x5C, 0x79, 0xD0, 0x01, 0xBD, 0x4A, 0x14, 0x29, 0xFF, 0x00, 0xF0]))
+ rom.write_bytes(0x05F720, bytearray([0x0C, 0xFA, 0xBF, 0xA5, 0xD3, 0x22, 0x29, 0xFF, 0x00, 0x5C, 0x79, 0xD0, 0x01, 0xFA, 0xA9, 0x4E]))
+ rom.write_bytes(0x05F730, bytearray([0x00, 0x5C, 0x79, 0xD0, 0x01, 0xE0, 0x09, 0xD0, 0x05, 0xAD, 0x64, 0x14, 0x80, 0x03, 0xBD, 0x54]))
+ rom.write_bytes(0x05F740, bytearray([0x14, 0x29, 0xFF, 0x00, 0xF0, 0x0C, 0xFA, 0xBF, 0xC3, 0xD3, 0x22, 0x29, 0xFF, 0x00, 0x5C, 0x79]))
+ rom.write_bytes(0x05F750, bytearray([0xD0, 0x01, 0xFA, 0xA9, 0x4E, 0x00, 0x5C, 0x79, 0xD0, 0x01, 0xE2, 0x20, 0xAD, 0xD2, 0x00, 0xC2]))
+ rom.write_bytes(0x05F760, bytearray([0x20, 0xD0, 0x08, 0xBF, 0x5F, 0xB8, 0x01, 0x5C, 0x7E, 0xD0, 0x01, 0xAD, 0xD3, 0x00, 0x29, 0xFF]))
+ rom.write_bytes(0x05F770, bytearray([0x00, 0xC9, 0x01, 0x00, 0xF0, 0x3C, 0xC9, 0x02, 0x00, 0xF0, 0x4B, 0xBF, 0x5F, 0xB8, 0x01, 0x05]))
+ rom.write_bytes(0x05F780, bytearray([0x18, 0x99, 0xA1, 0xB1, 0xBF, 0xDD, 0xB8, 0x01, 0x05, 0x18, 0x99, 0xE1, 0xB1, 0xFA, 0xC8, 0xC8]))
+ rom.write_bytes(0x05F790, bytearray([0xE8, 0xE0, 0x1D, 0x90, 0x19, 0xEE, 0xD3, 0x00, 0xA0, 0x00, 0xA2, 0x00, 0xAD, 0xD3, 0x00, 0x29]))
+ rom.write_bytes(0x05F7A0, bytearray([0xFF, 0x00, 0xC9, 0x03, 0x00, 0xD0, 0x07, 0x9C, 0xD3, 0x00, 0x5C, 0x94, 0xD0, 0x01, 0x5C, 0x71]))
+ rom.write_bytes(0x05F7B0, bytearray([0xD0, 0x01, 0xBF, 0x5F, 0xB8, 0x01, 0x05, 0x18, 0x99, 0x21, 0xB2, 0xBF, 0xDD, 0xB8, 0x01, 0x05]))
+ rom.write_bytes(0x05F7C0, bytearray([0x18, 0x99, 0x61, 0xB2, 0x80, 0xC7, 0xBF, 0x5F, 0xB8, 0x01, 0x05, 0x18, 0x99, 0xA1, 0xB2, 0xBF]))
+ rom.write_bytes(0x05F7D0, bytearray([0xDD, 0xB8, 0x01, 0x05, 0x18, 0x99, 0xE1, 0xB2, 0x80, 0xB3, 0xE2, 0x20, 0xAD, 0xD2, 0x00, 0xC2]))
+ rom.write_bytes(0x05F7E0, bytearray([0x20, 0xD0, 0x0A, 0x64, 0x18, 0xAF, 0xB8, 0x03, 0x00, 0x5C, 0x80, 0xD1, 0x01, 0x5C, 0x02, 0xD2]))
+ rom.write_bytes(0x05F7F0, bytearray([0x01, 0xE2, 0x20, 0xAD, 0xD2, 0x00, 0xC2, 0x20, 0xD0, 0x08, 0x64, 0x18, 0xA0, 0x00, 0x5C, 0xE2]))
+ rom.write_bytes(0x05F800, bytearray([0xD0, 0x01, 0x5C, 0x02, 0xD2, 0x01, 0xAD, 0xD2, 0x00, 0x29, 0xFF, 0x00, 0xD0, 0x08, 0xBF, 0x35]))
+ rom.write_bytes(0x05F810, bytearray([0xB8, 0x01, 0x5C, 0xB8, 0xCF, 0x01, 0xBF, 0xE1, 0xD3, 0x22, 0x29, 0xFF, 0x00, 0xC9, 0x80, 0x00]))
+ rom.write_bytes(0x05F820, bytearray([0xF0, 0x2E, 0xC9, 0x81, 0x00, 0xF0, 0x47, 0x5C, 0xB8, 0xCF, 0x01, 0xAD, 0xD2, 0x00, 0x29, 0xFF]))
+ rom.write_bytes(0x05F830, bytearray([0x00, 0xD0, 0x08, 0xBF, 0x4A, 0xB8, 0x01, 0x5C, 0xD4, 0xCF, 0x01, 0xBF, 0xF6, 0xD3, 0x22, 0x29]))
+ rom.write_bytes(0x05F840, bytearray([0xFF, 0x00, 0x4C, 0xB6, 0xFD, 0xF0, 0x18, 0xC9, 0x81, 0x00, 0xF0, 0x30, 0x5C, 0xD4, 0xCF, 0x01]))
+ rom.write_bytes(0x05F850, bytearray([0xDA, 0xE2, 0x20, 0xAD, 0xB3, 0x14, 0xAA, 0xC2, 0x20, 0x20, 0x8A, 0xF8, 0xFA, 0x80, 0xC8, 0xDA]))
+ rom.write_bytes(0x05F860, bytearray([0xE2, 0x20, 0xAD, 0xB3, 0x14, 0xAA, 0xC2, 0x20, 0x20, 0xBD, 0xF8, 0xFA, 0x80, 0xDE, 0xDA, 0xE2]))
+ rom.write_bytes(0x05F870, bytearray([0x20, 0xAF, 0x85, 0xFC, 0x0D, 0xAA, 0x20, 0x8A, 0xF8, 0xFA, 0x80, 0xAB, 0xDA, 0xE2, 0x20, 0xAF]))
+ rom.write_bytes(0x05F880, bytearray([0x86, 0xFC, 0x0D, 0xAA, 0x20, 0xBD, 0xF8, 0xFA, 0x80, 0xC2, 0xE2, 0x20, 0xC9, 0x0A, 0xB0, 0x1F]))
+ rom.write_bytes(0x05F890, bytearray([0xAD, 0xD5, 0x00, 0xD0, 0x0D, 0xBF, 0x0B, 0xD4, 0x22, 0xC2, 0x20, 0xA9, 0x50, 0x00, 0xEE, 0xD5]))
+ rom.write_bytes(0x05F8A0, bytearray([0x00, 0x60, 0xBF, 0x0B, 0xD4, 0x22, 0x9C, 0xD4, 0x00, 0x9C, 0xD5, 0x00, 0xC2, 0x20, 0x60, 0xAD]))
+ rom.write_bytes(0x05F8B0, bytearray([0xD4, 0x00, 0xD0, 0xEE, 0xEE, 0xD4, 0x00, 0xC2, 0x20, 0xA9, 0x52, 0x00, 0x60, 0xE2, 0x20, 0xC9]))
+ rom.write_bytes(0x05F8C0, bytearray([0x0A, 0xB0, 0x1F, 0xAD, 0xD6, 0x00, 0xD0, 0x0D, 0xBF, 0x0B, 0xD4, 0x22, 0xC2, 0x20, 0xA9, 0x50]))
+ rom.write_bytes(0x05F8D0, bytearray([0x00, 0xEE, 0xD6, 0x00, 0x60, 0xBF, 0x0B, 0xD4, 0x22, 0x9C, 0xD7, 0x00, 0x9C, 0xD6, 0x00, 0xC2]))
+ rom.write_bytes(0x05F8E0, bytearray([0x20, 0x60, 0xAD, 0xD7, 0x00, 0xD0, 0xEE, 0xEE, 0xD7, 0x00, 0xC2, 0x20, 0xA9, 0x52, 0x00, 0x60]))
+ rom.write_bytes(0x05F8F0, bytearray([0xAD, 0xD2, 0x00, 0x29, 0xFF, 0x00, 0xF0, 0x04, 0x5C, 0x74, 0xD2, 0x01, 0x64, 0x18, 0xA0, 0x00]))
+ rom.write_bytes(0x05F900, bytearray([0x5C, 0x07, 0xD2, 0x01, 0xAD, 0xD2, 0x00, 0x29, 0xFF, 0x00, 0xF0, 0x04, 0x5C, 0x74, 0xD2, 0x01]))
+ rom.write_bytes(0x05F910, bytearray([0xAF, 0x7C, 0x02, 0x00, 0x5C, 0x7B, 0xD2, 0x01, 0xA5, 0x38, 0x89, 0x20, 0xD0, 0x0A, 0x29, 0x10]))
+ rom.write_bytes(0x05F920, bytearray([0xF0, 0x02, 0xA9, 0x01, 0x5C, 0x08, 0xC1, 0x01, 0xEE, 0xD2, 0x00, 0x64, 0x38, 0x5C, 0x08, 0xC1]))
+ rom.write_bytes(0x05F930, bytearray([0x01, 0xAD, 0xD2, 0x00, 0xD0, 0x08, 0xA5, 0x38, 0x29, 0x20, 0x5C, 0x3B, 0xC1, 0x01, 0xA9, 0x00]))
+ rom.write_bytes(0x05F940, bytearray([0x80, 0xF8, 0xAD, 0x10, 0x0B, 0x49, 0x01, 0x9C, 0xD2, 0x00, 0x5C, 0x4D, 0xCE, 0x01, 0x9C, 0x01]))
+ rom.write_bytes(0x05F950, bytearray([0x02, 0xAD, 0x5E, 0x02, 0xF0, 0x16, 0xAD, 0xB0, 0x14, 0x89, 0x08, 0xD0, 0x15, 0xAD, 0xB3, 0x14]))
+ rom.write_bytes(0x05F960, bytearray([0xCF, 0x85, 0xFC, 0x0D, 0x90, 0x06, 0xA9, 0x80, 0x8F, 0x65, 0x02, 0x7E, 0xC2, 0x20, 0x5C, 0xBB]))
+ rom.write_bytes(0x05F970, bytearray([0xA5, 0x17, 0xA9, 0x01, 0x80, 0xF2, 0xE2, 0x20, 0xAF, 0x87, 0xFC, 0x0D, 0xC2, 0x20, 0xF0, 0x0D]))
+ rom.write_bytes(0x05F980, bytearray([0x4C, 0xBF, 0xFA, 0x8D, 0x1C, 0x0C, 0x8D, 0x1E, 0x0C, 0x5C, 0x4E, 0xD8, 0x03, 0xB9, 0x04, 0x0C]))
+ rom.write_bytes(0x05F990, bytearray([0x80, 0xF1, 0xE2, 0x20, 0xA9, 0x01, 0x8D, 0xD8, 0x00, 0xC2, 0x20, 0x22, 0xBE, 0xAE, 0x03, 0x5C]))
+ rom.write_bytes(0x05F9A0, bytearray([0xA2, 0xA0, 0x02, 0xE2, 0x20, 0xAD, 0xD8, 0x00, 0xD0, 0x0F, 0xC2, 0x20, 0xA9, 0x02, 0x00, 0x9D]))
+ rom.write_bytes(0x05F9B0, bytearray([0x96, 0x7A, 0xFE, 0x78, 0x79, 0x5C, 0xAF, 0xA3, 0x02, 0xAD, 0xB3, 0x14, 0xCF, 0x86, 0xFC, 0x0D]))
+ rom.write_bytes(0x05F9C0, bytearray([0xC2, 0x20, 0xB0, 0xE8, 0xC2, 0x20, 0x5C, 0x81, 0xA3, 0x02, 0xE2, 0x20, 0xDA, 0xAE, 0x1A, 0x02]))
+ rom.write_bytes(0x05F9D0, bytearray([0xBD, 0x6D, 0x14, 0x89, 0x20, 0xF0, 0x0D, 0xFA, 0xC2, 0x20, 0xAD, 0x02, 0x74, 0xC9, 0x32, 0x00]))
+ rom.write_bytes(0x05F9E0, bytearray([0x5C, 0x80, 0xDF, 0x02, 0x18, 0x69, 0x20, 0x9D, 0x6D, 0x14, 0xAD, 0xB3, 0x14, 0x1A, 0x8D, 0xB3]))
+ rom.write_bytes(0x05F9F0, bytearray([0x14, 0x80, 0xE4, 0xE2, 0x20, 0xDA, 0xAE, 0x1A, 0x02, 0xBD, 0x6D, 0x14, 0x8D, 0xD1, 0x00, 0xA9]))
+ rom.write_bytes(0x05FA00, bytearray([0x10, 0x0C, 0xD1, 0x00, 0xAD, 0xD1, 0x00, 0x9D, 0x6D, 0x14, 0xFA, 0xC2, 0x20, 0xA9, 0x36, 0x00]))
+ rom.write_bytes(0x05FA10, bytearray([0x22, 0xD2, 0x85, 0x00, 0x5C, 0xEB, 0xC9, 0x11, 0xB9, 0xE4, 0xB9, 0xC0, 0x00, 0xF0, 0x03, 0xEE]))
+ rom.write_bytes(0x05FA20, bytearray([0xD9, 0x00, 0x5C, 0xBB, 0xB9, 0x10, 0xA9, 0x06, 0x85, 0x4D, 0xEE, 0xD9, 0x00, 0x5C, 0x19, 0xB0]))
+ rom.write_bytes(0x05FA30, bytearray([0x10, 0xA9, 0x05, 0x00, 0x85, 0x4D, 0xEE, 0xD9, 0x00, 0x5C, 0x9A, 0xD0, 0x10, 0xA9, 0x06, 0x85]))
+ rom.write_bytes(0x05FA40, bytearray([0x4D, 0xEE, 0xD9, 0x00, 0x5C, 0xC9, 0xD2, 0x10, 0xA9, 0x05, 0x85, 0x4D, 0xEE, 0xD9, 0x00, 0x5C]))
+ rom.write_bytes(0x05FA50, bytearray([0xEE, 0xC5, 0x10, 0xA9, 0x05, 0x00, 0x85, 0x4D, 0xEE, 0xD9, 0x00, 0x5C, 0x0F, 0xBE, 0x10, 0xDA]))
+ rom.write_bytes(0x05FA60, bytearray([0xE2, 0x20, 0xAD, 0xD9, 0x00, 0xF0, 0x26, 0xA2, 0x00, 0xAD, 0x1A, 0x02, 0xDF, 0x18, 0xD4, 0x22]))
+ rom.write_bytes(0x05FA70, bytearray([0xF0, 0x07, 0xE8, 0xE0, 0x06, 0xF0, 0x16, 0x80, 0xF3, 0xAE, 0x1A, 0x02, 0xBD, 0x6D, 0x14, 0x8D]))
+ rom.write_bytes(0x05FA80, bytearray([0xD1, 0x00, 0xA9, 0x10, 0x0C, 0xD1, 0x00, 0xAD, 0xD1, 0x00, 0x9D, 0x6D, 0x14, 0xFA, 0x22, 0x67]))
+ rom.write_bytes(0x05FA90, bytearray([0xFA, 0x04, 0x5C, 0x5A, 0xA1, 0x10]))
+
+ rom.write_bytes(0x05FAB2, bytearray([0xA9, 0x00, 0xEB, 0xAD, 0x0E, 0x03, 0xC2, 0x20, 0xC2, 0x10, 0x4C, 0x37, 0xF6]))
+ rom.write_bytes(0x05FABF, bytearray([0xE2]))
+ rom.write_bytes(0x05FAC0, bytearray([0x20, 0xAD, 0x1A, 0x02, 0xDA, 0xA2, 0x00, 0x00, 0xDF, 0x1E, 0xD4, 0x22, 0xF0, 0x11, 0xE8, 0xE0]))
+ rom.write_bytes(0x05FAD0, bytearray([0x01, 0x00, 0xF0, 0x02, 0x80, 0xF2, 0xFA, 0xC2, 0x20, 0xA9, 0x00, 0x00, 0x4C, 0x83, 0xF9, 0xFA]))
+ rom.write_bytes(0x05FAE0, bytearray([0xC2, 0x20, 0x4C, 0x8D, 0xF9]))
+ rom.write_bytes(0x05FAE5, bytearray([0x48, 0xE2, 0x20, 0xAD, 0x5D, 0x14, 0xC9, 0x01, 0xF0, 0x07]))
+ rom.write_bytes(0x05FAEF, bytearray([0xC2, 0x20, 0x68, 0x5C, 0xCE, 0xBE, 0x03, 0xAD, 0xCC, 0x00, 0xD0, 0xF4, 0xAF, 0xFA, 0x1D, 0x70]))
+ rom.write_bytes(0x05FAFF, bytearray([0xF0, 0xEE, 0xA9, 0x00, 0x8F, 0xFA, 0x1D, 0x70, 0x80, 0xE6]))
+ rom.write_bytes(0x05FB09, bytearray([0x48, 0xE2, 0x20, 0xAD, 0xCC, 0x00, 0xF0, 0x06, 0xCE, 0xCC, 0x00, 0xCE, 0xCC, 0x00, 0xC2, 0x20, 0x68, 0x22, 0x87, 0xBF, 0x03, 0x5C, 0x89, 0xFE, 0x07]))
+ rom.write_bytes(0x05FB22, bytearray([0xA0, 0x0A, 0x8C, 0x4D, 0x00, 0xE2, 0x20, 0xA9, 0x08, 0x0C, 0xB0, 0x14, 0x8D, 0xB6, 0x14, 0xC2, 0x20, 0x5C, 0xB9, 0xF1, 0x0D, 0x0D, 0xA8, 0xE2]))
+ rom.write_bytes(0x05FB3A, bytearray([0x20, 0xA9, 0x08, 0x0C, 0xB0, 0x14, 0xA9, 0x00, 0xEB, 0xA9, 0x7F, 0xA2, 0x40, 0x14, 0x54, 0x70, 0x7E, 0xAB, 0x7A, 0xFA, 0x1A, 0xEE, 0x14, 0xC2, 0x20, 0x68, 0x5C, 0xB9, 0xF1, 0x0D]))
+ rom.write_bytes(0x05FB58, bytearray([0x4C, 0xDD, 0xFB, 0x04, 0xAF, 0xAC, 0x00, 0x70]))
+ rom.write_bytes(0x05FB60, bytearray([0xD0, 0x2C, 0xAD, 0x35, 0x00, 0xC9, 0x50, 0xD0, 0x25, 0xAD, 0xDA, 0x00, 0xC9, 0x80, 0xF0, 0x11]))
+ rom.write_bytes(0x05FB70, bytearray([0xC9, 0x00, 0xF0, 0x21, 0xC9, 0x2A, 0xF0, 0x1D, 0xC9, 0x54, 0xF0, 0x19, 0xEE, 0xDA, 0x00, 0x80]))
+ rom.write_bytes(0x05FB80, bytearray([0x10, 0xA9, 0x2F, 0x8D, 0x53, 0x00, 0xA9, 0x11, 0x8D, 0x18, 0x01, 0xEE, 0xDB, 0x00, 0x9C, 0xDA]))
+ rom.write_bytes(0x05FB90, bytearray([0x00, 0x5C, 0x93, 0xC1, 0x01, 0xA9, 0x28, 0x8D, 0x53, 0x00, 0x80, 0xE0]))
+ rom.write_bytes(0x05FB9C, bytearray([0xA9, 0x93, 0x00, 0xEE]))
+ rom.write_bytes(0x05FBA0, bytearray([0xB4, 0x03, 0xAC, 0xB4, 0x03, 0xC0, 0x14, 0x00, 0x90, 0x14, 0xE2, 0x20, 0xDA, 0xAE, 0x1A, 0x02]))
+ rom.write_bytes(0x05FBB0, bytearray([0xBD, 0x6D, 0x14, 0x09, 0x01, 0x9D, 0x6D, 0x14, 0xFA, 0xC2, 0x20, 0xA9, 0x94, 0x00, 0x5C, 0xF1, 0xDF, 0x00]))
+ rom.write_bytes(0x05FBC2, bytearray([0x48, 0xC9, 0x06, 0x00, 0xB0, 0x10, 0xE2, 0x20, 0xAD, 0x54, 0x14, 0xC9, 0x00, 0xC2, 0x20, 0xF0]))
+ rom.write_bytes(0x05FBD2, bytearray([0x05, 0x68, 0x5C, 0xAC, 0x82, 0x07, 0x68, 0x5C, 0xFB, 0x81, 0x07, 0xAD, 0x6A, 0x02, 0xF0, 0x11]))
+ rom.write_bytes(0x05FBE2, bytearray([0xC2, 0x20, 0xA9, 0x0E, 0x00, 0x22, 0xE2, 0xF6, 0x04, 0xA9, 0x00, 0x00, 0x8D, 0x6A, 0x02, 0xE2]))
+ rom.write_bytes(0x05FBF2, bytearray([0x20, 0x22, 0x28, 0xFD, 0x04, 0x4C, 0x5C, 0xFB, 0xAF, 0xB0, 0x23, 0x7E, 0xF0, 0x18, 0xAF, 0xAC]))
+ rom.write_bytes(0x05FC02, bytearray([0x00, 0x70, 0x29, 0xFF, 0x00, 0xD0, 0x0F, 0xAF, 0xB0, 0x23, 0x7E, 0x8F, 0xEC, 0x61, 0x04, 0xA9]))
+ rom.write_bytes(0x05FC12, bytearray([0x00, 0x00, 0x8F, 0xB0, 0x23, 0x7E, 0xBD, 0xD0, 0x61, 0xF0, 0x03, 0xDE, 0xD0, 0x61, 0x5C, 0x79]))
+ rom.write_bytes(0x05FC22, bytearray([0xDE, 0x04, 0x48, 0xC2, 0x20, 0xAF, 0xEC, 0x61, 0x04, 0xD0, 0x0B, 0xE2, 0x20, 0x68, 0x22, 0xCE]))
+ rom.write_bytes(0x05FC32, bytearray([0xC0, 0x01, 0x5C, 0x8B, 0xB5, 0x01, 0x8F, 0xB0, 0x23, 0x7E, 0xA9, 0x00, 0x00, 0x8F, 0xEC, 0x61]))
+ rom.write_bytes(0x05FC42, bytearray([0x04, 0x80, 0xE8, 0x48, 0xDA, 0xE2, 0x20, 0x4C, 0xA5, 0xFC, 0xA2, 0x00, 0xDF, 0x1F, 0xD4, 0x22]))
+ rom.write_bytes(0x05FC52, bytearray([0xF0, 0x03, 0xE8, 0x80, 0xF7, 0xBF, 0x8D, 0xFC, 0x0D, 0xAA, 0xBF, 0x2A, 0xD4, 0x22, 0x8D, 0xDC]))
+ rom.write_bytes(0x05FC62, bytearray([0x00, 0xC2, 0x20, 0xFA, 0x68, 0x0D, 0xDC, 0x00, 0x95, 0x76, 0x5C, 0x29, 0xDB, 0x0C, 0xE2, 0x20]))
+ rom.write_bytes(0x05FC72, bytearray([0xAD, 0x48, 0x0B, 0xF0, 0x23, 0xAF, 0xBE, 0x03, 0x02, 0xC9, 0x02, 0xD0, 0x1B, 0xAD, 0x1A, 0x02]))
+ rom.write_bytes(0x05FC82, bytearray([0xA2, 0x00, 0xDF, 0x1F, 0xD4, 0x22, 0xF0, 0x03, 0xE8, 0x80, 0xF7, 0x8A, 0x0A, 0xAA, 0xC2, 0x20]))
+ rom.write_bytes(0x05FC92, bytearray([0xBF, 0x35, 0xD4, 0x22, 0x8F, 0xBE, 0x03, 0x02, 0xC2, 0x20, 0xEE, 0xAC, 0x03, 0xC2, 0x10, 0x5C]))
+ rom.write_bytes(0x05FCA2, bytearray([0x0C, 0x95, 0x02, 0xAD, 0x1A, 0x02, 0xC9, 0x43, 0xF0, 0x03, 0x4C, 0x4C, 0xFC, 0xA9, 0x0A, 0x4C]))
+ rom.write_bytes(0x05FCB2, bytearray([0x60, 0xFC, 0xAC, 0xB4, 0x03, 0xC0, 0x14, 0x30, 0x14, 0x1A, 0xE2, 0x20, 0xDA, 0x48, 0xAE, 0x1A]))
+ rom.write_bytes(0x05FCC2, bytearray([0x02, 0xBD, 0x6D, 0x14, 0x09, 0x01, 0x9D, 0x6D, 0x14, 0x68, 0xFA, 0xC2, 0x20, 0x22, 0xD2, 0x85]))
+ rom.write_bytes(0x05FCD2, bytearray([0x00, 0x5C, 0xA4, 0x9E, 0x03, 0xE2, 0x20, 0xAD, 0xF6, 0x7D, 0xC9, 0x0C, 0xB0, 0x1A, 0xAD, 0xCC]))
+ rom.write_bytes(0x05FCE2, bytearray([0x00, 0xAD, 0x5E, 0x14, 0x38, 0xED, 0xCC, 0x00, 0x3A, 0x3A, 0x8D, 0xDE, 0x00, 0xAD, 0xF6, 0x7D]))
+ rom.write_bytes(0x05FCF2, bytearray([0x38, 0xED, 0xCC, 0x00, 0x18, 0xCD, 0xDE, 0x00, 0xC2, 0x20, 0x5C, 0xBC, 0x9A, 0x02, 0xE2, 0x20]))
+ rom.write_bytes(0x05FD02, bytearray([0xAD, 0x5D, 0x14, 0xF0, 0x33, 0xAA, 0xBF, 0xA3, 0xAF, 0x09, 0x18, 0x6D, 0xCC, 0x00, 0x8D, 0x5E]))
+ rom.write_bytes(0x05FD12, bytearray([0x14, 0xAD, 0xF6, 0x7D, 0xC9, 0x0C, 0xB0, 0x1A, 0xAD, 0xCC, 0x00, 0xAD, 0x5E, 0x14, 0x38, 0xED]))
+ rom.write_bytes(0x05FD22, bytearray([0xCC, 0x00, 0x3A, 0x3A, 0x8D, 0xDE, 0x00, 0xAD, 0xF6, 0x7D, 0x38, 0xED, 0xCC, 0x00, 0x18, 0xCD]))
+ rom.write_bytes(0x05FD32, bytearray([0xDE, 0x00, 0xC2, 0x20, 0x5C, 0xAC, 0xDC, 0x01, 0x1A, 0x8D, 0x5D, 0x14, 0x80, 0xC0, 0xA9, 0x00]))
+ rom.write_bytes(0x05FD42, bytearray([0x8F, 0xE8, 0x1F, 0x70, 0xAD, 0xAC, 0x60, 0xC9, 0x00, 0xD0, 0x06, 0xA9, 0x01, 0x8F, 0xE8, 0x1F]))
+ rom.write_bytes(0x05FD52, bytearray([0x70, 0x4C, 0x5F, 0xF5, 0xDA, 0xAD, 0x1A, 0x02, 0x8D, 0x7C, 0x02, 0xAD, 0x12, 0x11, 0xC9, 0x08]))
+ rom.write_bytes(0x05FD62, bytearray([0xB0, 0x1D, 0xAD, 0x18, 0x02, 0x4A, 0xAA, 0xA9, 0x00, 0xE0, 0x00, 0xF0, 0x06, 0x18, 0x69, 0x08]))
+ rom.write_bytes(0x05FD72, bytearray([0xCA, 0x80, 0xF6, 0x18, 0x6D, 0x12, 0x11, 0xAA, 0xBF, 0x4B, 0xD4, 0x22, 0x8D, 0x1A, 0x02, 0xFA]))
+ rom.write_bytes(0x05FD82, bytearray([0xA9, 0x02, 0x8D, 0x13, 0x11, 0x5C, 0x70, 0xE0, 0x17, 0xAC, 0x7C, 0x02, 0x8C, 0x1A, 0x02, 0xB9]))
+ rom.write_bytes(0x05FD92, bytearray([0x22, 0x02, 0x5C, 0xA7, 0x82, 0x10, 0xAD, 0x7C, 0x02, 0x8D, 0x1A, 0x02, 0xC2, 0x20, 0xE2, 0x10]))
+ rom.write_bytes(0x05FDA2, bytearray([0x5C, 0xBC, 0xB2, 0x01, 0xC9, 0x45, 0xB0, 0x03, 0x8D, 0x7C, 0x02, 0x8D, 0x1A, 0x02, 0x29, 0x07]))
+ rom.write_bytes(0x05FDB2, bytearray([0x5C, 0x3A, 0x81, 0x10, 0xC9, 0x82, 0x00, 0xF0, 0x2E, 0xC9, 0x83, 0x00, 0xF0, 0x40, 0xC9, 0x84]))
+ rom.write_bytes(0x05FDC2, bytearray([0x00, 0xF0, 0x0B, 0xC9, 0x85, 0x00, 0xF0, 0x0E, 0xC9, 0x80, 0x00, 0x4C, 0x45, 0xF8, 0xE2, 0x20]))
+ rom.write_bytes(0x05FDD2, bytearray([0xAF, 0x99, 0xFC, 0x0D, 0x80, 0x16, 0xDA, 0xE2, 0x20, 0xAD, 0xE3, 0x00, 0xD0, 0x4E, 0x9C, 0xE1]))
+ rom.write_bytes(0x05FDE2, bytearray([0x00, 0xAF, 0x99, 0xFC, 0x0D, 0x80, 0x25, 0xE2, 0x20, 0xAD, 0xB5, 0x14, 0xC9, 0x64, 0xC2, 0x20]))
+ rom.write_bytes(0x05FDF2, bytearray([0xB0, 0x06, 0xA9, 0x4E, 0x00, 0x4C, 0x4C, 0xF8, 0xA9, 0x52, 0x00, 0x4C, 0x4C, 0xF8, 0xDA, 0xE2]))
+ rom.write_bytes(0x05FE02, bytearray([0x20, 0xAD, 0xE3, 0x00, 0xD0, 0x26, 0x9C, 0xE1, 0x00, 0xAD, 0xB5, 0x14, 0xC9, 0x0A, 0x90, 0x08]))
+ rom.write_bytes(0x05FE12, bytearray([0x38, 0xE9, 0x0A, 0xEE, 0xE1, 0x00, 0x80, 0xF4, 0x8D, 0xE2, 0x00, 0xEE, 0xE3, 0x00, 0xAD, 0xE1]))
+ rom.write_bytes(0x05FE22, bytearray([0x00, 0xAA, 0xBF, 0x0B, 0xD4, 0x22, 0xC2, 0x20, 0xFA, 0x4C, 0x46, 0xF8, 0x9C, 0xE3, 0x00, 0xAD]))
+ rom.write_bytes(0x05FE32, bytearray([0xE2, 0x00, 0xAA, 0xBF, 0x0B, 0xD4, 0x22, 0xC2, 0x20, 0xFA, 0x4C, 0x4C, 0xF8, 0x22, 0xB7, 0xB2]))
+ rom.write_bytes(0x05FE42, bytearray([0x01, 0xEA, 0xEA, 0xEA, 0xAE, 0x7C, 0x02, 0x8E, 0x1A, 0x02, 0xEA, 0xEA, 0xEA, 0xEA, 0x5C, 0xAC]))
+ rom.write_bytes(0x05FE52, bytearray([0xBE, 0x01, 0xE2, 0x20, 0xAD, 0x1A, 0x02, 0xc9, 0x3C, 0xC2, 0x20, 0xB0, 0x04, 0xA9, 0x02, 0x00]))
+ rom.write_bytes(0x05FE62, bytearray([0x6B, 0xA9, 0x00, 0x00, 0x6B, 0xAD, 0x18, 0x01, 0xC9, 0x19, 0xD0, 0x3A, 0xC2, 0x20, 0x48, 0xA9]))
+ rom.write_bytes(0x05FE72, bytearray([0x00, 0x00, 0xE2, 0x20, 0xAF, 0x9A, 0xFC, 0x0D, 0xD0, 0x05, 0xA9, 0x08, 0x0C, 0xB0, 0x14, 0xC2]))
+ rom.write_bytes(0x05FE82, bytearray([0x10, 0xDA, 0x5A, 0x8B, 0xAD, 0x0E, 0x03, 0xC2, 0x20, 0xAA, 0xBF, 0x07, 0xFF, 0x06, 0xA8, 0xE2]))
+ rom.write_bytes(0x05FE92, bytearray([0x20, 0xA9, 0x00, 0xEB, 0xA9, 0x7F, 0xA2, 0x40, 0x14, 0x54, 0x70, 0x7E, 0xAB, 0x7A, 0xFA, 0xC2]))
+ rom.write_bytes(0x05FEA2, bytearray([0x20, 0x68, 0xE2, 0x20, 0xE2, 0x10, 0x22, 0x4B, 0x82, 0x00, 0x5C, 0xD9, 0x87, 0x17]))
+
+ rom.write_bytes(0x00EDD0, bytearray([0xda, 0xa2, 0x00, 0x00, 0xe2, 0x20, 0xc9, 0x00, 0xf0, 0x0b, 0x48, 0x8a, 0x18, 0x69, 0x0c, 0xaa]))
+ rom.write_bytes(0x00EDE0, bytearray([0x68, 0x3a, 0x3a, 0x80, 0xf1, 0x98, 0x4a, 0x8f, 0x80, 0x24, 0x7e, 0x18, 0x8a, 0x6f, 0x80, 0x24]))
+ rom.write_bytes(0x00EDF0, bytearray([0x7e, 0xaa, 0xbf, 0x00, 0x80, 0x02, 0x0a, 0xaa, 0xc2, 0x20, 0xbf, 0x8e, 0xd4, 0x22, 0xfa, 0x18]))
+ rom.write_bytes(0x00EE00, bytearray([0x5c, 0xb6, 0xc0, 0x17, 0xda, 0xe2, 0x20, 0xa2, 0x00, 0x00, 0xdf, 0x4b, 0xd4, 0x22, 0xf0, 0x0e]))
+ rom.write_bytes(0x00EE10, bytearray([0xe8, 0xe0, 0x30, 0x00, 0xb0, 0x02, 0x80, 0xf2, 0xa9, 0x00, 0xeb, 0xad, 0x1a, 0x02, 0xaa, 0xbf]))
+ rom.write_bytes(0x00EE20, bytearray([0x00, 0x80, 0x02, 0xaa, 0xbf, 0x9e, 0xd4, 0x22, 0xc2, 0x20, 0x29, 0xff, 0x00, 0xfa, 0x5c, 0xfd]))
+ rom.write_bytes(0x00EE30, bytearray([0xc6, 0x17]))
+
+
+def ExtendedItemHandler(rom: LocalRom) -> None:
+ rom.write_bytes(0x10B3FB, bytearray([0xE2, 0x20, 0xC9, 0x45, 0xB0]))
+ rom.write_bytes(0x10B400, bytearray([0x0C, 0xC2, 0x20, 0xA9, 0x10, 0x03, 0x8D, 0x7E, 0x02, 0x5C, 0xCF, 0xF4, 0x0B, 0xAD, 0x0F, 0x0B]))
+ rom.write_bytes(0x10B410, bytearray([0xD0, 0x38, 0xEE, 0xB5, 0x14, 0xA9, 0x18, 0x8D, 0x53, 0x00, 0xAF, 0x9A, 0xFC, 0x0D, 0xF0, 0x09]))
+ rom.write_bytes(0x10B420, bytearray([0xAD, 0xB5, 0x14, 0xCF, 0x99, 0xFC, 0x0D, 0xB0, 0x04, 0x5C, 0x0A, 0xF5, 0x0B, 0xAD, 0xB6, 0x14]))
+ rom.write_bytes(0x10B430, bytearray([0xD0, 0xF7, 0xA9, 0x01, 0x8D, 0xB6, 0x14, 0xA9, 0x0A, 0x8D, 0x18, 0x02, 0xA9, 0x16, 0x8D, 0x18]))
+ rom.write_bytes(0x10B440, bytearray([0x01, 0xA9, 0x97, 0x8D, 0x53, 0x00, 0x5C, 0x0A, 0xF5, 0x0B, 0xFA, 0x5C, 0x10, 0xF5, 0x0B]))
+
+
+def patch_rom(world: "YoshisIslandWorld", rom: LocalRom, player: int) -> None:
+ handle_items(rom) # Implement main item functionality
+ Item_Data(rom) # Pointers necessary for item functionality
+ write_lives(rom) # Writes the number of lives as set in AP
+ CodeHandler(rom) # Jumps to my code
+ Server_Data(rom) # Pointers mostly related to receiving items
+ Menu_Data(rom) # Data related to the AP menu
+ Handle_Locations(rom)
+ ExtendedItemHandler(rom)
+ rom.write_bytes(0x11544B, bytearray(world.global_level_list))
+ rom.write_bytes(0x11547A, bytearray([0x43]))
+
+ rom.write_bytes(0x06FC89, world.starting_lives)
+ rom.write_bytes(0x03464F, ([world.baby_mario_sfx]))
+ rom.write_bytes(0x06FC83, ([world.options.starting_world.value]))
+ rom.write_bytes(0x06FC84, ([world.options.hidden_object_visibility.value]))
+ rom.write_bytes(0x06FC88, ([world.options.shuffle_midrings.value]))
+ rom.write_bytes(0x06FC85, ([world.options.castle_open_condition.value]))
+ rom.write_bytes(0x06FC86, ([world.options.castle_clear_condition.value]))
+ rom.write_bytes(0x06FC87, ([world.options.disable_autoscroll.value]))
+ rom.write_bytes(0x06FC8B, ([world.options.minigame_checks.value]))
+ rom.write_byte(0x06FC8C, world.options.death_link.value)
+ rom.write_bytes(0x06FC8D, bytearray(world.boss_room_id))
+ rom.write_bytes(0x06FC99, bytearray([world.options.luigi_pieces_required.value]))
+ rom.write_bytes(0x06FC9A, bytearray([world.options.goal.value]))
+
+ if world.options.yoshi_colors != YoshiColors.option_normal:
+ rom.write_bytes(0x113A33, bytearray(world.bowser_text))
+
+ rom.write_bytes(0x0A060C, bytearray(world.boss_burt_data))
+ rom.write_bytes(0x0A8666, bytearray(world.boss_slime_data))
+ rom.write_bytes(0x0A9D90, bytearray(world.boss_boo_data))
+ rom.write_bytes(0x0074EA, bytearray(world.boss_pot_data))
+ rom.write_bytes(0x08DC0A, bytearray(world.boss_frog_data))
+ rom.write_bytes(0x0A4440, bytearray(world.boss_plant_data))
+ rom.write_bytes(0x0968A2, bytearray(world.boss_milde_data))
+ rom.write_bytes(0x0B3E10, bytearray(world.boss_koop_data))
+ rom.write_bytes(0x0B4BD0, bytearray(world.boss_slug_data))
+ rom.write_bytes(0x0B6BBA, bytearray(world.boss_raph_data))
+ rom.write_bytes(0x087BED, bytearray(world.boss_tap_data))
+
+ rom.write_bytes(0x07A00D, ([world.tap_tap_room]))
+ rom.write_bytes(0x079DF2, ([world.tap_tap_room]))
+ rom.write_bytes(0x079CCF, ([world.tap_tap_room]))
+ rom.write_bytes(0x079C4D, ([world.tap_tap_room]))
+
+ rom.write_bytes(0x045A2E, bytearray(world.Stage11StageGFX))
+ rom.write_bytes(0x045A31, bytearray(world.Stage12StageGFX))
+ rom.write_bytes(0x045A34, bytearray(world.Stage13StageGFX))
+ rom.write_bytes(0x045A37, bytearray(world.Stage14StageGFX))
+ rom.write_bytes(0x045A3A, bytearray(world.Stage15StageGFX))
+ rom.write_bytes(0x045A3D, bytearray(world.Stage16StageGFX))
+ rom.write_bytes(0x045A40, bytearray(world.Stage17StageGFX))
+ rom.write_bytes(0x045A43, bytearray(world.Stage18StageGFX))
+
+ rom.write_bytes(0x045A52, bytearray(world.Stage21StageGFX))
+ rom.write_bytes(0x045A55, bytearray(world.Stage22StageGFX))
+ rom.write_bytes(0x045A58, bytearray(world.Stage23StageGFX))
+ rom.write_bytes(0x045A5B, bytearray(world.Stage24StageGFX))
+ rom.write_bytes(0x045A5E, bytearray(world.Stage25StageGFX))
+ rom.write_bytes(0x045A61, bytearray(world.Stage26StageGFX))
+ rom.write_bytes(0x045A64, bytearray(world.Stage27StageGFX))
+ rom.write_bytes(0x045A67, bytearray(world.Stage28StageGFX))
+
+ rom.write_bytes(0x045A76, bytearray(world.Stage31StageGFX))
+ rom.write_bytes(0x045A79, bytearray(world.Stage32StageGFX))
+ rom.write_bytes(0x045A7C, bytearray(world.Stage33StageGFX))
+ rom.write_bytes(0x045A7F, bytearray(world.Stage34StageGFX))
+ rom.write_bytes(0x045A82, bytearray(world.Stage35StageGFX))
+ rom.write_bytes(0x045A85, bytearray(world.Stage36StageGFX))
+ rom.write_bytes(0x045A88, bytearray(world.Stage37StageGFX))
+ rom.write_bytes(0x045A8B, bytearray(world.Stage38StageGFX))
+
+ rom.write_bytes(0x045A9A, bytearray(world.Stage41StageGFX))
+ rom.write_bytes(0x045A9D, bytearray(world.Stage42StageGFX))
+ rom.write_bytes(0x045AA0, bytearray(world.Stage43StageGFX))
+ rom.write_bytes(0x045AA3, bytearray(world.Stage44StageGFX))
+ rom.write_bytes(0x045AA6, bytearray(world.Stage45StageGFX))
+ rom.write_bytes(0x045AA9, bytearray(world.Stage46StageGFX))
+ rom.write_bytes(0x045AAC, bytearray(world.Stage47StageGFX))
+ rom.write_bytes(0x045AAF, bytearray(world.Stage48StageGFX))
+
+ rom.write_bytes(0x045ABE, bytearray(world.Stage51StageGFX))
+ rom.write_bytes(0x045AC1, bytearray(world.Stage52StageGFX))
+ rom.write_bytes(0x045AC4, bytearray(world.Stage53StageGFX))
+ rom.write_bytes(0x045AC7, bytearray(world.Stage54StageGFX))
+ rom.write_bytes(0x045ACA, bytearray(world.Stage55StageGFX))
+ rom.write_bytes(0x045ACD, bytearray(world.Stage56StageGFX))
+ rom.write_bytes(0x045AD0, bytearray(world.Stage57StageGFX))
+ rom.write_bytes(0x045AD3, bytearray(world.Stage58StageGFX))
+
+ rom.write_bytes(0x045AE2, bytearray(world.Stage61StageGFX))
+ rom.write_bytes(0x045AE5, bytearray(world.Stage62StageGFX))
+ rom.write_bytes(0x045AE8, bytearray(world.Stage63StageGFX))
+ rom.write_bytes(0x045AEB, bytearray(world.Stage64StageGFX))
+ rom.write_bytes(0x045AEE, bytearray(world.Stage65StageGFX))
+ rom.write_bytes(0x045AF1, bytearray(world.Stage66StageGFX))
+ rom.write_bytes(0x045AF4, bytearray(world.Stage67StageGFX))
+
+ rom.write_bytes(0x0BDBAF, bytearray(world.level_gfx_table))
+ rom.write_bytes(0x0BDC4F, bytearray(world.palette_panel_list))
+
+ if world.options.yoshi_colors == YoshiColors.option_random_order:
+ rom.write_bytes(0x010000, ([world.leader_color]))
+ rom.write_bytes(0x010008, ([world.leader_color]))
+ rom.write_bytes(0x010009, ([world.leader_color]))
+ rom.write_bytes(0x010001, bytearray(world.color_order))
+ rom.write_bytes(0x01000C, ([world.leader_color]))
+ rom.write_bytes(0x010014, ([world.leader_color]))
+ rom.write_bytes(0x010015, ([world.leader_color]))
+ rom.write_bytes(0x01000D, bytearray(world.color_order))
+ rom.write_bytes(0x010018, ([world.leader_color]))
+ rom.write_bytes(0x010020, ([world.leader_color]))
+ rom.write_bytes(0x010021, ([world.leader_color]))
+ rom.write_bytes(0x01001A, bytearray(world.color_order))
+ rom.write_bytes(0x010024, ([world.leader_color]))
+ rom.write_bytes(0x01002C, ([world.leader_color]))
+ rom.write_bytes(0x01002D, ([world.leader_color]))
+ rom.write_bytes(0x010025, bytearray(world.color_order))
+ rom.write_bytes(0x010030, ([world.leader_color]))
+ rom.write_bytes(0x010038, ([world.leader_color]))
+ rom.write_bytes(0x010039, ([world.leader_color]))
+ rom.write_bytes(0x010031, bytearray(world.color_order))
+ rom.write_bytes(0x01003C, ([world.leader_color]))
+ rom.write_bytes(0x010044, ([world.leader_color]))
+ rom.write_bytes(0x010045, ([world.leader_color]))
+ rom.write_bytes(0x01003D, bytearray(world.color_order))
+ rom.write_bytes(0x010043, ([world.leader_color]))
+ elif world.options.yoshi_colors in {YoshiColors.option_random_color, YoshiColors.option_singularity}:
+ rom.write_bytes(0x010000, bytearray(world.level_colors))
+
+ if world.options.minigame_checks in {MinigameChecks.option_bonus_games, MinigameChecks.option_both}:
+ bonus_checks(rom)
+
+ if world.options.minigame_checks in {MinigameChecks.option_bandit_games, MinigameChecks.option_both}:
+ bandit_checks(rom)
+
+ rom.write_bytes(0x00BF2C, bytearray(world.world_bonus))
+
+ if world.options.softlock_prevention:
+ rom.write_bytes(0x00C18F, bytearray([0x5C, 0x58, 0xFB, 0x0B])) # R + X Code
+
+ if world.options.bowser_door_mode != BowserDoor.option_manual:
+ rom.write_bytes(0x07891F, bytearray(world.castle_door)) # 1 Entry
+ rom.write_bytes(0x078923, bytearray(world.castle_door)) # 2 Entry
+ rom.write_bytes(0x078927, bytearray(world.castle_door)) # 3 Entry
+ rom.write_bytes(0x07892B, bytearray(world.castle_door)) # 4 Entry
+
+ if world.options.bowser_door_mode == BowserDoor.option_gauntlet:
+ rom.write_bytes(0x0AF517, bytearray([0xC6, 0x07, 0x7A, 0x00])) # Door 2
+ rom.write_bytes(0x0AF6B7, bytearray([0xCD, 0x05, 0x5B, 0x00])) # Door 3
+ rom.write_bytes(0x0AF8F2, bytearray([0xD3, 0x00, 0x77, 0x06])) # Door 4
+
+ if world.options.goal == PlayerGoal.option_luigi_hunt:
+ rom.write_bytes(0x1153F6, bytearray([0x16, 0x28, 0x10, 0x0C, 0x10, 0x4E, 0x1E, 0x10, 0x08, 0x04, 0x08, 0x24, 0x36, 0x82, 0x83, 0x83, 0x34, 0x84, 0x85, 0x85])) # Luigi piece clear text
+ rom.write_bytes(0x06FC86, bytearray([0xFF])) # Boss clear goal = 255, renders bowser inaccessible
+
+ from Main import __version__
+ rom.name = bytearray(f'YOSHIAP{__version__.replace(".", "")[0:3]}_{player}_{world.multiworld.seed:11}\0', "utf8")[:21]
+ rom.name.extend([0] * (21 - len(rom.name)))
+ rom.write_bytes(0x007FC0, rom.name)
+
+
+class YoshisIslandDeltaPatch(APDeltaPatch):
+ hash = USHASH
+ game: str = "Yoshi's Island"
+ patch_file_ending = ".apyi"
+
+ @classmethod
+ def get_source_data(cls) -> bytes:
+ return get_base_rom_bytes()
+
+
+def get_base_rom_bytes(file_name: str = "") -> bytes:
+ base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
+ if not base_rom_bytes:
+ file_name = get_base_rom_path(file_name)
+ base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb")))
+
+ basemd5 = hashlib.md5()
+ basemd5.update(base_rom_bytes)
+ if USHASH != basemd5.hexdigest():
+ raise Exception("Supplied Base Rom does not match known MD5 for US(1.0) release. "
+ "Get the correct game and version, then dump it")
+ get_base_rom_bytes.base_rom_bytes = base_rom_bytes
+ return base_rom_bytes
+
+
+def get_base_rom_path(file_name: str = "") -> str:
+ if not file_name:
+ file_name = get_settings()["yoshisisland_options"]["rom_file"]
+ if not os.path.exists(file_name):
+ file_name = Utils.user_path(file_name)
+ return file_name
diff --git a/worlds/yoshisisland/Rules.py b/worlds/yoshisisland/Rules.py
new file mode 100644
index 000000000000..68d4f29a7381
--- /dev/null
+++ b/worlds/yoshisisland/Rules.py
@@ -0,0 +1,612 @@
+from .level_logic import YoshiLogic
+from worlds.generic.Rules import set_rule
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+ from . import YoshisIslandWorld
+
+
+def set_easy_rules(world: "YoshisIslandWorld") -> None:
+ logic = YoshiLogic(world)
+ player = world.player
+
+ set_rule(world.multiworld.get_location("Make Eggs, Throw Eggs: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Beanstalk"}, player))
+ set_rule(world.multiworld.get_location("Make Eggs, Throw Eggs: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Beanstalk"}, player))
+ set_rule(world.multiworld.get_location("Make Eggs, Throw Eggs: Stars", player), lambda state: state.has_all({"Tulip", "Beanstalk", "Dashed Stairs"}, player))
+ set_rule(world.multiworld.get_location("Make Eggs, Throw Eggs: Level Clear", player), lambda state: state.has("Beanstalk", player))
+
+ set_rule(world.multiworld.get_location("Watch Out Below!: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "Helicopter Morph"}, player))
+ set_rule(world.multiworld.get_location("Watch Out Below!: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "Helicopter Morph"}, player))
+ set_rule(world.multiworld.get_location("Watch Out Below!: Stars", player), lambda state: state.has("Large Spring Ball", player) and logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Watch Out Below!: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("The Cave Of Chomp Rock: Red Coins", player), lambda state: state.has("Chomp Rock", player))
+ set_rule(world.multiworld.get_location("The Cave Of Chomp Rock: Flowers", player), lambda state: state.has("Chomp Rock", player))
+
+ set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Red Coins", player), lambda state: state.has("Spring Ball", player))
+ set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Flowers", player), lambda state: state.has_all({"Spring Ball", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Stars", player), lambda state: state.has("Spring Ball", player) and (logic.has_midring(state) or state.has("Key", player)))
+
+ set_rule(world.multiworld.get_location("Hop! Hop! Donut Lifts: Stars", player), lambda state: logic.has_midring(state) or logic.cansee_clouds(state))
+
+ set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "Flashing Eggs", "Mole Tank Morph", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Flowers", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Stars", player), lambda state: (logic.has_midring(state) and state.has("Tulip", player) or logic.has_midring(state) and state.has("Beanstalk", player)) and state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "Beanstalk"}, player))
+
+ set_rule(world.multiworld.get_location("Touch Fuzzy Get Dizzy: Red Coins", player), lambda state: state.has_all({"Flashing Eggs", "Spring Ball", "Chomp Rock", "Beanstalk"}, player))
+ set_rule(world.multiworld.get_location("Touch Fuzzy Get Dizzy: Stars", player), lambda state: logic.has_midring(state) or (logic.cansee_clouds and state.has_all({"Spring Ball", "Chomp Rock", "Beanstalk"}, player)))
+
+ set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Red Coins", player), lambda state: state.has("Platform Ghost", player))
+ set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Flowers", player), lambda state: state.has("Platform Ghost", player))
+ set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Stars", player), lambda state: logic.has_midring(state) and (state.has("Platform Ghost", player) or state.has_all({"Arrow Wheel", "Key"}, player)))
+
+ set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Red Coins", player), lambda state: state.has_all({"Poochy", "Large Spring Ball", "Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Flowers", player), lambda state: state.has_all({"Super Star", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Stars", player), lambda state: state.has("Large Spring Ball", player) and logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("The Baseball Boys: Red Coins", player), lambda state: state.has_all({"Beanstalk", "Super Star", "Egg Launcher", "Large Spring Ball", "Mole Tank Morph"}, player))
+ set_rule(world.multiworld.get_location("The Baseball Boys: Flowers", player), lambda state: state.has_all({"Beanstalk", "Super Star", "Egg Launcher", "Large Spring Ball", "Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("The Baseball Boys: Stars", player), lambda state: (logic.has_midring(state) and (state.has("Tulip", player))) and state.has_all({"Beanstalk", "Super Star", "Large Spring Ball", "Egg Launcher"}, player))
+ set_rule(world.multiworld.get_location("The Baseball Boys: Level Clear", player), lambda state: state.has_all({"Beanstalk", "Super Star", "Egg Launcher", "Large Spring Ball"}, player))
+
+ set_rule(world.multiworld.get_location("What's Gusty Taste Like?: Red Coins", player), lambda state: state.has("! Switch", player))
+ set_rule(world.multiworld.get_location("What's Gusty Taste Like?: Flowers", player), lambda state: state.has_any({"Large Spring Ball", "Super Star"}, player))
+ set_rule(world.multiworld.get_location("What's Gusty Taste Like?: Level Clear", player), lambda state: state.has_any({"Large Spring Ball", "Super Star"}, player))
+
+ set_rule(world.multiworld.get_location("Bigger Boo's Fort: Red Coins", player), lambda state: state.has_all({"! Switch", "Key", "Dashed Stairs"}, player))
+ set_rule(world.multiworld.get_location("Bigger Boo's Fort: Flowers", player), lambda state: state.has_all({"! Switch", "Key", "Dashed Stairs"}, player))
+ set_rule(world.multiworld.get_location("Bigger Boo's Fort: Stars", player), lambda state: state.has_all({"! Switch", "Key", "Dashed Stairs"}, player) and logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("Watch Out For Lakitu: Red Coins", player), lambda state: state.has("Chomp Rock", player))
+ set_rule(world.multiworld.get_location("Watch Out For Lakitu: Flowers", player), lambda state: state.has_all({"Key", "Train Morph", "Chomp Rock"}, player))
+ set_rule(world.multiworld.get_location("Watch Out For Lakitu: Level Clear", player), lambda state: state.has("Chomp Rock", player))
+
+ set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Red Coins", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "Egg Launcher"}, player))
+ set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Stars", player), lambda state: state.has("Large Spring Ball", player) and logic.has_midring(state))
+ set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("Lakitu's Wall: Red Coins", player), lambda state: (state.has_any({"Dashed Platform", "Giant Eggs"}, player) or logic.combat_item(state)) and state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Lakitu's Wall: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player) and (logic.combat_item(state) or state.has("Giant Eggs", player)))
+ set_rule(world.multiworld.get_location("Lakitu's Wall: Stars", player), lambda state: state.has("Giant Eggs", player) and logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Lakitu's Wall: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "Car Morph"}, player))
+
+ set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Red Coins", player), lambda state: state.has_all({"Arrow Wheel", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 1)))
+ set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Flowers", player), lambda state: state.has_all({"Arrow Wheel", "Train Morph", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 1)))
+ set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Stars", player), lambda state: state.has_all({"Arrow Wheel", "Key"}, player) and logic.has_midring(state) and (state.has("Egg Capacity Upgrade", player, 1)))
+
+ set_rule(world.multiworld.get_location("Welcome To Monkey World!: Stars", player), lambda state: logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Jungle Rhythm...: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("Jungle Rhythm...: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("Jungle Rhythm...: Stars", player), lambda state: logic.has_midring(state) and state.has("Tulip", player))
+ set_rule(world.multiworld.get_location("Jungle Rhythm...: Level Clear", player), lambda state: state.has_all({"Dashed Stairs", "Spring Ball"}, player))
+
+ set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Red Coins", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player))
+ set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Flowers", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player))
+ set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Stars", player), lambda state: logic.has_midring(state) or state.has_all({"Submarine Morph", "Helicopter Morph"}, player))
+ set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Level Clear", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player))
+
+ set_rule(world.multiworld.get_location("Prince Froggy's Fort: Red Coins", player), lambda state: state.has("Submarine Morph", player))
+ set_rule(world.multiworld.get_location("Prince Froggy's Fort: Flowers", player), lambda state: (state.has("Egg Capacity Upgrade", player, 5) or logic.combat_item(state)) and (state.has("Dashed Platform", player)))
+ set_rule(world.multiworld.get_location("Prince Froggy's Fort: Stars", player), lambda state: logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("Jammin' Through The Trees: Flowers", player), lambda state: state.has("Watermelon", player) or logic.melon_item(state))
+ set_rule(world.multiworld.get_location("Jammin' Through The Trees: Stars", player), lambda state: ((logic.has_midring(state) or state.has("Tulip", player)) and logic.cansee_clouds(state)) or logic.has_midring(state) and state.has("Tulip", player))
+
+ set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Red Coins", player), lambda state: state.has_all({"Chomp Rock", "Beanstalk", "Mole Tank Morph", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Flowers", player), lambda state: state.has_all({"Chomp Rock", "Beanstalk", "Mole Tank Morph", "Large Spring Ball", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Stars", player), lambda state: state.has_all({"Tulip", "Large Spring Ball", "Dashed Stairs", "Chomp Rock", "Beanstalk", "Mole Tank Morph"}, player) and logic.has_midring(state))
+ set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Level Clear", player), lambda state: state.has_all({"Chomp Rock", "Large Spring Ball", "Key"}, player))
+
+ set_rule(world.multiworld.get_location("Monkeys' Favorite Lake: Red Coins", player), lambda state: state.has_all({"! Switch", "Submarine Morph", "Large Spring Ball", "Beanstalk"}, player))
+ set_rule(world.multiworld.get_location("Monkeys' Favorite Lake: Flowers", player), lambda state: state.has_all({"Beanstalk", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("Monkeys' Favorite Lake: Stars", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Monkeys' Favorite Lake: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("Naval Piranha's Castle: Red Coins", player), lambda state: (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Naval Piranha's Castle: Flowers", player), lambda state: (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Naval Piranha's Castle: Stars", player), lambda state: logic.has_midring(state) and state.has("Tulip", player))
+
+ set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Red Coins", player), lambda state: state.has("Super Star", player))
+ set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Flowers", player), lambda state: state.has("Super Star", player))
+ set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Stars", player), lambda state: logic.has_midring(state) or (state.has("Tulip", player) and logic.cansee_clouds(state)))
+ set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Level Clear", player), lambda state: state.has("Super Star", player))
+
+ set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "! Switch", "Egg Launcher"}, player))
+ set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "Egg Launcher"}, player))
+ set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Stars", player), lambda state: state.has_all({"Large Spring Ball", "Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("Don't Look Back!: Red Coins", player), lambda state: state.has_all({"Helicopter Morph", "! Switch", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("Don't Look Back!: Flowers", player), lambda state: state.has_all({"! Switch", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("Don't Look Back!: Stars", player), lambda state: (logic.has_midring(state) and state.has("Tulip", player)) and state.has("! Switch", player))
+ set_rule(world.multiworld.get_location("Don't Look Back!: Level Clear", player), lambda state: state.has("! Switch", player))
+
+ set_rule(world.multiworld.get_location("Marching Milde's Fort: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Marching Milde's Fort: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket"}, player) and (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Marching Milde's Fort: Stars", player), lambda state: state.has("Dashed Stairs", player) and (logic.has_midring(state) or state.has("Vanishing Arrow Wheel", player) or logic.cansee_clouds(state)))
+
+ set_rule(world.multiworld.get_location("Chomp Rock Zone: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "Chomp Rock"}, player))
+ set_rule(world.multiworld.get_location("Chomp Rock Zone: Flowers", player), lambda state: state.has_all({"Chomp Rock", "! Switch", "Spring Ball", "Dashed Platform"}, player))
+ set_rule(world.multiworld.get_location("Chomp Rock Zone: Stars", player), lambda state: state.has_all({"Chomp Rock", "! Switch", "Spring Ball", "Dashed Platform"}, player))
+
+ set_rule(world.multiworld.get_location("Lake Shore Paradise: Red Coins", player), lambda state: state.has_any({"Large Spring Ball", "Spring Ball"}, player) and (state.has("Egg Plant", player) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Lake Shore Paradise: Flowers", player), lambda state: state.has_any({"Large Spring Ball", "Spring Ball"}, player) and (state.has("Egg Plant", player) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Lake Shore Paradise: Stars", player), lambda state: (logic.has_midring(state) or (state.has("Tulip", player) and logic.cansee_clouds(state))) and (state.has("Egg Plant", player) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Lake Shore Paradise: Level Clear", player), lambda state: state.has("Egg Plant", player) or logic.combat_item(state))
+
+ set_rule(world.multiworld.get_location("Ride Like The Wind: Red Coins", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Ride Like The Wind: Flowers", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Ride Like The Wind: Stars", player), lambda state: (logic.has_midring(state) and state.has("Helicopter Morph", player)) and state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Ride Like The Wind: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Key"}, player))
+ set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Key"}, player))
+ set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Stars", player), lambda state: logic.has_midring(state) and (state.has_any({"Dashed Stairs", "Vanishing Arrow Wheel"}, player)))
+
+ set_rule(world.multiworld.get_location("BLIZZARD!!!: Red Coins", player), lambda state: state.has_all({"Helicopter Morph", "Dashed Stairs"}, player))
+ set_rule(world.multiworld.get_location("BLIZZARD!!!: Stars", player), lambda state: logic.cansee_clouds(state) or ((logic.has_midring(state) and state.has("Dashed Stairs", player)) or state.has("Tulip", player)))
+
+ set_rule(world.multiworld.get_location("Ride The Ski Lifts: Stars", player), lambda state: logic.has_midring(state) or state.has("Super Star", player))
+
+ set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Red Coins", player), lambda state: (state.has("Fire Melon", player) or logic.melon_item(state)) and (state.has_all({"Bucket", "Spring Ball", "Super Star", "Skis", "Dashed Platform"}, player)))
+ set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Flowers", player), lambda state: (state.has("Fire Melon", player) or logic.melon_item(state)) and state.has_all({"Spring Ball", "Skis", "Dashed Platform"}, player))
+ set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Stars", player), lambda state: (logic.has_midring(state) and (state.has("Fire Melon", player) or logic.melon_item(state))) and state.has("Spring Ball", player))
+ set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Level Clear", player), lambda state: state.has_all({"Spring Ball", "Skis", "Dashed Platform"}, player))
+
+ set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Dashed Platform", "Platform Ghost"}, player))
+ set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Platform Ghost", "Dashed Platform"}, player))
+ set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Stars", player), lambda state: ((state.has_all({"Dashed Stairs", "Platform Ghost"}, player)) and logic.has_midring(state)) or (logic.cansee_clouds(state) and state.has("Dashed Stairs", player) and state.has("Dashed Platform", player)))
+
+ set_rule(world.multiworld.get_location("Goonie Rides!: Red Coins", player), lambda state: state.has_all({"Helicopter Morph", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("Goonie Rides!: Flowers", player), lambda state: state.has_all({"Helicopter Morph", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("Goonie Rides!: Stars", player), lambda state: logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Goonie Rides!: Level Clear", player), lambda state: state.has_all({"Helicopter Morph", "! Switch"}, player))
+
+ set_rule(world.multiworld.get_location("Welcome To Cloud World: Stars", player), lambda state: logic.has_midring(state) or state.has("Tulip", player))
+
+ set_rule(world.multiworld.get_location("Shifting Platforms Ahead: Red Coins", player), lambda state: state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state))
+ set_rule(world.multiworld.get_location("Shifting Platforms Ahead: Flowers", player), lambda state: state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state))
+ set_rule(world.multiworld.get_location("Shifting Platforms Ahead: Stars", player), lambda state: logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Red Coins", player), lambda state: state.has_all({"Arrow Wheel", "Train Morph"}, player))
+ set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Flowers", player), lambda state: state.has_all({"Arrow Wheel", "Train Morph"}, player))
+ set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Stars", player), lambda state: logic.has_midring(state) and state.has("Arrow Wheel", player))
+
+ set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Red Coins", player), lambda state: state.has_all({"Dashed Platform", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Flowers", player), lambda state: state.has_all({"Dashed Platform", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Stars", player), lambda state: state.has("Dashed Platform", player) and logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Level Clear", player), lambda state: state.has_all({"Dashed Platform", "Large Spring Ball"}, player))
+
+ set_rule(world.multiworld.get_location("The Cave Of The Bandits: Red Coins", player), lambda state: state.has("Super Star", player))
+ set_rule(world.multiworld.get_location("The Cave Of The Bandits: Flowers", player), lambda state: state.has("Super Star", player))
+ set_rule(world.multiworld.get_location("The Cave Of The Bandits: Stars", player), lambda state: logic.cansee_clouds(state) or logic.has_midring(state))
+ set_rule(world.multiworld.get_location("The Cave Of The Bandits: Level Clear", player), lambda state: state.has("Super Star", player))
+
+ set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Red Coins", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Egg Plant", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Flowers", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Egg Plant", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Stars", player), lambda state: logic.has_midring(state) and state.has_all({"Spring Ball", "Large Spring Ball", "Egg Plant", "Key"}, player))
+
+ set_rule(world.multiworld.get_location("The Very Loooooong Cave: Red Coins", player), lambda state: state.has("Chomp Rock", player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("The Very Loooooong Cave: Flowers", player), lambda state: state.has("Chomp Rock", player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("The Very Loooooong Cave: Stars", player), lambda state: state.has("Chomp Rock", player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state)) and logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("The Deep, Underground Maze: Red Coins", player), lambda state: state.has_all({"Chomp Rock", "Key", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("The Deep, Underground Maze: Flowers", player), lambda state: state.has_all({"Chomp Rock", "Key", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("The Deep, Underground Maze: Stars", player), lambda state: state.has_all({"Chomp Rock", "Tulip", "Key"}, player) or (logic.has_midring(state) and state.has_all({"Key", "Chomp Rock", "Large Spring Ball"}, player)))
+ set_rule(world.multiworld.get_location("The Deep, Underground Maze: Level Clear", player), lambda state: state.has_all({"Chomp Rock", "Key", "Large Spring Ball", "Dashed Platform"}, player))
+
+ set_rule(world.multiworld.get_location("KEEP MOVING!!!!: Red Coins", player), lambda state: (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("KEEP MOVING!!!!: Flowers", player), lambda state: state.has("Egg Plant", player))
+ set_rule(world.multiworld.get_location("KEEP MOVING!!!!: Stars", player), lambda state: logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("King Bowser's Castle: Red Coins", player), lambda state: state.has_all({"Helicopter Morph", "Egg Plant"}, player) and logic._68CollectibleRoute(state))
+ set_rule(world.multiworld.get_location("King Bowser's Castle: Flowers", player), lambda state: state.has_all({"Helicopter Morph", "Egg Plant"}, player) and logic._68CollectibleRoute(state))
+ set_rule(world.multiworld.get_location("King Bowser's Castle: Stars", player), lambda state: state.has_all({"Helicopter Morph", "Egg Plant"}, player) and logic._68Route(state))
+
+ set_easy_extra_rules(world)
+
+
+def set_easy_extra_rules(world: "YoshisIslandWorld") -> None:
+ player = world.player
+ logic = YoshiLogic(world)
+ if not world.options.extras_enabled:
+ return
+ set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Red Coins", player), lambda state: state.has("Poochy", player))
+ set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Flowers", player), lambda state: state.has("Poochy", player))
+ set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Stars", player), lambda state: state.has("Poochy", player))
+ set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Level Clear", player), lambda state: state.has("Poochy", player))
+
+ set_rule(world.multiworld.get_location("Hit That Switch!!: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("Hit That Switch!!: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("Hit That Switch!!: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player))
+
+ set_rule(world.multiworld.get_location("The Impossible? Maze: Red Coins", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph", "Flashing Eggs"}, player))
+ set_rule(world.multiworld.get_location("The Impossible? Maze: Flowers", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph"}, player))
+ set_rule(world.multiworld.get_location("The Impossible? Maze: Stars", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph"}, player))
+ set_rule(world.multiworld.get_location("The Impossible? Maze: Level Clear", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph"}, player))
+
+ set_rule(world.multiworld.get_location("Kamek's Revenge: Red Coins", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph", "! Switch"}, player) and logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Kamek's Revenge: Flowers", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph", "! Switch"}, player) and logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Kamek's Revenge: Stars", player), lambda state: state.has("! Switch", player) and logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Kamek's Revenge: Level Clear", player), lambda state: state.has_all({"Key", "Skis", "! Switch", "Helicopter Morph"}, player))
+
+ set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Red Coins", player), lambda state: (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state)) and state.has(("Large Spring Ball"), player))
+ set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Flowers", player), lambda state: (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state)) and state.has(("Large Spring Ball"), player))
+ set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Stars", player), lambda state: logic.has_midring(state) and state.has(("Large Spring Ball"), player))
+ set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Level Clear", player), lambda state: (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state)) and state.has(("Large Spring Ball"), player))
+
+
+def set_normal_rules(world: "YoshisIslandWorld") -> None:
+ logic = YoshiLogic(world)
+ player = world.player
+
+ set_rule(world.multiworld.get_location("Make Eggs, Throw Eggs: Red Coins", player), lambda state: state.has("Dashed Stairs", player))
+ set_rule(world.multiworld.get_location("Make Eggs, Throw Eggs: Flowers", player), lambda state: state.has("Dashed Stairs", player))
+ set_rule(world.multiworld.get_location("Make Eggs, Throw Eggs: Stars", player), lambda state: state.has_any({"Tulip", "Dashed Stairs"}, player))
+
+ set_rule(world.multiworld.get_location("Watch Out Below!: Red Coins", player), lambda state: state.has("Helicopter Morph", player))
+ set_rule(world.multiworld.get_location("Watch Out Below!: Flowers", player), lambda state: state.has("Helicopter Morph", player))
+ set_rule(world.multiworld.get_location("Watch Out Below!: Stars", player), lambda state: logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Red Coins", player), lambda state: state.has("Spring Ball", player))
+ set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Flowers", player), lambda state: state.has_all({"Spring Ball", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Stars", player), lambda state: state.has("Spring Ball", player))
+ set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Level Clear", player), lambda state: logic._14CanFightBoss(state))
+
+
+ set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "Flashing Eggs", "Mole Tank Morph", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Flowers", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Stars", player), lambda state: (logic.has_midring(state) and state.has_any(["Tulip", "Beanstalk"], player)) or (state.has_all(["Tulip", "Beanstalk", "Large Spring Ball"], player)))
+ set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "Beanstalk"}, player))
+
+ set_rule(world.multiworld.get_location("Touch Fuzzy Get Dizzy: Red Coins", player), lambda state: state.has_all({"Flashing Eggs", "Spring Ball", "Chomp Rock", "Beanstalk"}, player))
+ set_rule(world.multiworld.get_location("Touch Fuzzy Get Dizzy: Stars", player), lambda state: logic.has_midring(state) or (logic.cansee_clouds and state.has_all({"Spring Ball", "Chomp Rock", "Beanstalk"}, player)))
+
+ set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Red Coins", player), lambda state: state.has("Platform Ghost", player))
+ set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Flowers", player), lambda state: state.has("Platform Ghost", player))
+ set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Stars", player), lambda state: logic.has_midring(state) and (state.has("Platform Ghost", player) or state.has_all({"Arrow Wheel", "Key"}, player)))
+
+ set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Red Coins", player), lambda state: state.has_all({"Poochy", "Large Spring Ball", "Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Flowers", player), lambda state: state.has_all({"Super Star", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Stars", player), lambda state: state.has("Large Spring Ball", player) and logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("The Baseball Boys: Red Coins", player), lambda state: state.has_all({"Beanstalk", "Super Star", "Large Spring Ball", "Mole Tank Morph"}, player))
+ set_rule(world.multiworld.get_location("The Baseball Boys: Flowers", player), lambda state: state.has_all({"Super Star", "Large Spring Ball", "Beanstalk", "Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("The Baseball Boys: Stars", player), lambda state: (logic.has_midring(state) or (state.has("Tulip", player))) and state.has_all({"Beanstalk", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("The Baseball Boys: Level Clear", player), lambda state: state.has_all({"Beanstalk", "Super Star", "Large Spring Ball"}, player))
+
+ set_rule(world.multiworld.get_location("What's Gusty Taste Like?: Red Coins", player), lambda state: state.has("! Switch", player))
+
+ set_rule(world.multiworld.get_location("Bigger Boo's Fort: Red Coins", player), lambda state: state.has_all({"! Switch", "Key", "Dashed Stairs"}, player))
+ set_rule(world.multiworld.get_location("Bigger Boo's Fort: Flowers", player), lambda state: state.has_all({"! Switch", "Key", "Dashed Stairs"}, player))
+ set_rule(world.multiworld.get_location("Bigger Boo's Fort: Stars", player), lambda state: state.has_all({"! Switch", "Dashed Stairs"}, player))
+
+ set_rule(world.multiworld.get_location("Watch Out For Lakitu: Flowers", player), lambda state: state.has_all({"Key", "Train Morph"}, player))
+
+ set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Red Coins", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "Egg Launcher"}, player))
+ set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Stars", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("Lakitu's Wall: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player) and (logic.combat_item(state) or state.has("Giant Eggs", player)))
+ set_rule(world.multiworld.get_location("Lakitu's Wall: Stars", player), lambda state: state.has("Giant Eggs", player) or logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Red Coins", player), lambda state: state.has_all({"Arrow Wheel", "Key"}, player))
+ set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Flowers", player), lambda state: state.has_all({"Arrow Wheel", "Key", "Train Morph"}, player))
+ set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Stars", player), lambda state: state.has("Arrow Wheel", player) and (logic.has_midring(state) or state.has("Key", player)))
+
+ set_rule(world.multiworld.get_location("Welcome To Monkey World!: Stars", player), lambda state: logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("Jungle Rhythm...: Red Coins", player), lambda state: state.has("Dashed Stairs", player))
+ set_rule(world.multiworld.get_location("Jungle Rhythm...: Flowers", player), lambda state: state.has("Dashed Stairs", player))
+ set_rule(world.multiworld.get_location("Jungle Rhythm...: Stars", player), lambda state: logic.has_midring(state) and state.has("Tulip", player))
+ set_rule(world.multiworld.get_location("Jungle Rhythm...: Level Clear", player), lambda state: state.has("Dashed Stairs", player))
+
+ set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Red Coins", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player))
+ set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Flowers", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player))
+ set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Level Clear", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player))
+
+ set_rule(world.multiworld.get_location("Prince Froggy's Fort: Red Coins", player), lambda state: state.has("Submarine Morph", player))
+ set_rule(world.multiworld.get_location("Prince Froggy's Fort: Flowers", player), lambda state: (state.has("Egg Capacity Upgrade", player, 5) or logic.combat_item(state)) and (state.has("Dashed Platform", player) or logic.has_midring(state)))
+ set_rule(world.multiworld.get_location("Prince Froggy's Fort: Stars", player), lambda state: logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("Jammin' Through The Trees: Flowers", player), lambda state: state.has("Watermelon", player) or logic.melon_item(state))
+
+ set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Red Coins", player), lambda state: state.has_any({"Dashed Stairs", "Beanstalk"}, player) and state.has_all({"Mole Tank Morph", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Flowers", player), lambda state: state.has_any({"Dashed Stairs", "Beanstalk"}, player) and state.has_all({"! Switch", "Mole Tank Morph", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Stars", player), lambda state: (state.has_any({"Dashed Stairs", "Beanstalk"}, player) and state.has_all({"Mole Tank Morph", "Large Spring Ball"}, player)) and (logic.has_midring(state) or state.has("Tulip", player)))
+ set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "Key"}, player))
+
+ set_rule(world.multiworld.get_location("Monkeys' Favorite Lake: Red Coins", player), lambda state: state.has_all({"! Switch", "Submarine Morph", "Large Spring Ball", "Beanstalk"}, player))
+ set_rule(world.multiworld.get_location("Monkeys' Favorite Lake: Flowers", player), lambda state: state.has("Beanstalk", player))
+
+ set_rule(world.multiworld.get_location("Naval Piranha's Castle: Red Coins", player), lambda state: (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Naval Piranha's Castle: Flowers", player), lambda state: (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Naval Piranha's Castle: Stars", player), lambda state: (logic.has_midring(state) and state.has("Tulip", player)) and state.has("Egg Capacity Upgrade", player, 1))
+
+ set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Red Coins", player), lambda state: state.has("Super Star", player))
+ set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Flowers", player), lambda state: state.has("Super Star", player))
+ set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Stars", player), lambda state: logic.has_midring(state) or state.has("Tulip", player))
+ set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Level Clear", player), lambda state: state.has("Super Star", player))
+
+ set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "! Switch", "Egg Launcher"}, player))
+ set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "Egg Launcher"}, player))
+ set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Stars", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("Don't Look Back!: Red Coins", player), lambda state: state.has_all({"Helicopter Morph", "! Switch", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("Don't Look Back!: Flowers", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Don't Look Back!: Stars", player), lambda state: (logic.has_midring(state) or state.has("Tulip", player)) and state.has("! Switch", player))
+ set_rule(world.multiworld.get_location("Don't Look Back!: Level Clear", player), lambda state: state.has("! Switch", player))
+
+ set_rule(world.multiworld.get_location("Marching Milde's Fort: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket", "Key"}, player))
+ set_rule(world.multiworld.get_location("Marching Milde's Fort: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket"}, player))
+ set_rule(world.multiworld.get_location("Marching Milde's Fort: Stars", player), lambda state: state.has("Dashed Stairs", player))
+
+ set_rule(world.multiworld.get_location("Chomp Rock Zone: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "Chomp Rock"}, player))
+ set_rule(world.multiworld.get_location("Chomp Rock Zone: Flowers", player), lambda state: state.has_all({"Chomp Rock", "! Switch", "Dashed Platform"}, player))
+ set_rule(world.multiworld.get_location("Chomp Rock Zone: Stars", player), lambda state: state.has_all({"Chomp Rock", "! Switch", "Dashed Platform"}, player))
+
+ set_rule(world.multiworld.get_location("Lake Shore Paradise: Red Coins", player), lambda state: state.has("Egg Plant", player) or logic.combat_item(state))
+ set_rule(world.multiworld.get_location("Lake Shore Paradise: Flowers", player), lambda state: state.has("Egg Plant", player) or logic.combat_item(state))
+ set_rule(world.multiworld.get_location("Lake Shore Paradise: Stars", player), lambda state: (logic.has_midring(state) or (state.has("Tulip", player) and logic.cansee_clouds(state))) and (state.has("Egg Plant", player) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Lake Shore Paradise: Level Clear", player), lambda state: state.has("Egg Plant", player) or logic.combat_item(state))
+
+ set_rule(world.multiworld.get_location("Ride Like The Wind: Red Coins", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Ride Like The Wind: Flowers", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Ride Like The Wind: Stars", player), lambda state: (logic.has_midring(state) or state.has("Helicopter Morph", player)) and state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Ride Like The Wind: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Key"}, player))
+ set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Key"}, player))
+ set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Stars", player), lambda state: logic.has_midring(state) or (state.has_any({"Dashed Stairs", "Vanishing Arrow Wheel"}, player)))
+
+ set_rule(world.multiworld.get_location("BLIZZARD!!!: Red Coins", player), lambda state: state.has_any({"Dashed Stairs", "Ice Melon"}, player) and (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state) or state.has("Helicopter Morph", player)))
+
+ set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Red Coins", player), lambda state: (state.has("Fire Melon", player) or logic.melon_item(state)) and (state.has_all({"Spring Ball", "Skis"}, player)) and (state.has("Super Star", player) or logic.melon_item(state)))
+ set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Flowers", player), lambda state: (state.has("Fire Melon", player) or logic.melon_item(state)) and state.has_all({"Spring Ball", "Skis"}, player))
+ set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Stars", player), lambda state: (logic.has_midring(state) and (state.has("Fire Melon", player) or logic.melon_item(state))) or (logic.has_midring(state) and (state.has_all({"Tulip", "Dashed Platform"}, player))))
+ set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Level Clear", player), lambda state: state.has_all({"Spring Ball", "Skis"}, player))
+
+ set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Dashed Platform", "Platform Ghost"}, player))
+ set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Platform Ghost", "Dashed Platform"}, player))
+ set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Stars", player), lambda state: ((state.has_all({"Dashed Stairs", "Platform Ghost"}, player))) or (logic.cansee_clouds(state) and state.has("Dashed Stairs", player)))
+
+ set_rule(world.multiworld.get_location("Goonie Rides!: Red Coins", player), lambda state: state.has("Helicopter Morph", player))
+ set_rule(world.multiworld.get_location("Goonie Rides!: Flowers", player), lambda state: state.has_all({"Helicopter Morph", "! Switch"}, player))
+
+ set_rule(world.multiworld.get_location("Shifting Platforms Ahead: Stars", player), lambda state: logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Red Coins", player), lambda state: state.has_all({"Arrow Wheel", "Train Morph"}, player))
+ set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Flowers", player), lambda state: state.has_all({"Arrow Wheel", "Train Morph"}, player))
+ set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Stars", player), lambda state: logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Red Coins", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Flowers", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Stars", player), lambda state: logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("The Cave Of The Bandits: Red Coins", player), lambda state: state.has("Super Star", player))
+ set_rule(world.multiworld.get_location("The Cave Of The Bandits: Flowers", player), lambda state: state.has("Super Star", player))
+ set_rule(world.multiworld.get_location("The Cave Of The Bandits: Level Clear", player), lambda state: state.has("Super Star", player))
+
+ set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "Egg Plant", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "Egg Plant", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Stars", player), lambda state: logic.has_midring(state) and state.has_all({"Spring Ball", "Egg Plant", "Key"}, player))
+
+ set_rule(world.multiworld.get_location("The Very Loooooong Cave: Red Coins", player), lambda state: state.has("Chomp Rock", player) and (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("The Very Loooooong Cave: Flowers", player), lambda state: state.has("Chomp Rock", player) and (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("The Very Loooooong Cave: Stars", player), lambda state: state.has("Chomp Rock", player) and (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state)) and logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("The Deep, Underground Maze: Red Coins", player), lambda state: state.has_all({"Chomp Rock", "Key", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("The Deep, Underground Maze: Flowers", player), lambda state: state.has_all({"Chomp Rock", "Key", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("The Deep, Underground Maze: Stars", player), lambda state: state.has_all({"Chomp Rock", "Tulip", "Key"}, player) or (logic.has_midring(state) and state.has_all({"Key", "Chomp Rock", "Large Spring Ball"}, player)))
+ set_rule(world.multiworld.get_location("The Deep, Underground Maze: Level Clear", player), lambda state: state.has_all({"Key", "Large Spring Ball", "Dashed Platform"}, player) and (logic.combat_item(state) or state.has("Chomp Rock", player)))
+
+ set_rule(world.multiworld.get_location("KEEP MOVING!!!!: Stars", player), lambda state: logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("King Bowser's Castle: Red Coins", player), lambda state: state.has_all({"Helicopter Morph", "Egg Plant"}, player) and logic._68CollectibleRoute(state))
+ set_rule(world.multiworld.get_location("King Bowser's Castle: Flowers", player), lambda state: state.has_all({"Helicopter Morph", "Egg Plant"}, player) and logic._68CollectibleRoute(state))
+ set_rule(world.multiworld.get_location("King Bowser's Castle: Stars", player), lambda state: state.has_all({"Helicopter Morph", "Egg Plant"}, player) and logic._68Route(state))
+
+ set_normal_extra_rules(world)
+
+
+def set_normal_extra_rules(world: "YoshisIslandWorld") -> None:
+ player = world.player
+ logic = YoshiLogic(world)
+ if not world.options.extras_enabled:
+ return
+
+ set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Red Coins", player), lambda state: state.has("Poochy", player))
+ set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Flowers", player), lambda state: state.has("Poochy", player))
+ set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Stars", player), lambda state: state.has("Poochy", player))
+ set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Level Clear", player), lambda state: state.has("Poochy", player))
+
+ set_rule(world.multiworld.get_location("Hit That Switch!!: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("Hit That Switch!!: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("Hit That Switch!!: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player))
+
+ set_rule(world.multiworld.get_location("The Impossible? Maze: Red Coins", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph", "Flashing Eggs"}, player))
+ set_rule(world.multiworld.get_location("The Impossible? Maze: Flowers", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph"}, player))
+ set_rule(world.multiworld.get_location("The Impossible? Maze: Stars", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph"}, player))
+ set_rule(world.multiworld.get_location("The Impossible? Maze: Level Clear", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph"}, player))
+
+ set_rule(world.multiworld.get_location("Kamek's Revenge: Red Coins", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph", "! Switch"}, player) and logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Kamek's Revenge: Flowers", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph", "! Switch"}, player) and logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Kamek's Revenge: Stars", player), lambda state: state.has("! Switch", player) or logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Kamek's Revenge: Level Clear", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph"}, player))
+
+ set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Red Coins", player), lambda state: (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)) and state.has(("Large Spring Ball"), player))
+ set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Flowers", player), lambda state: (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)) and state.has(("Large Spring Ball"), player))
+ set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Stars", player), lambda state: logic.has_midring(state) or state.has(("Large Spring Ball"), player))
+ set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Level Clear", player), lambda state: (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)) and state.has(("Large Spring Ball"), player))
+
+
+def set_hard_rules(world: "YoshisIslandWorld"):
+ logic = YoshiLogic(world)
+ player = world.player
+
+ set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Red Coins", player), lambda state: state.has("Spring Ball", player))
+ set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Flowers", player), lambda state: state.has_all({"Spring Ball", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Stars", player), lambda state: state.has("Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "Flashing Eggs", "Mole Tank Morph", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("Touch Fuzzy Get Dizzy: Red Coins", player), lambda state: state.has_all({"Flashing Eggs", "Spring Ball", "Chomp Rock", "Beanstalk"}, player))
+
+ set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Red Coins", player), lambda state: state.has("Platform Ghost", player))
+ set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Flowers", player), lambda state: state.has("Platform Ghost", player))
+
+ set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Red Coins", player), lambda state: state.has("Large Spring Ball", player) and (state.has("Poochy", player) or logic.melon_item(state)))
+ set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Flowers", player), lambda state: state.has_all({"Super Star", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Stars", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("The Baseball Boys: Red Coins", player), lambda state: state.has("Mole Tank Morph", player) and (state.has_any({"Ice Melon", "Large Spring Ball"}, player) or logic.melon_item(state)))
+ set_rule(world.multiworld.get_location("The Baseball Boys: Flowers", player), lambda state: (state.has_any({"Ice Melon", "Large Spring Ball"}, player) or logic.melon_item(state)))
+ set_rule(world.multiworld.get_location("The Baseball Boys: Level Clear", player), lambda state: (state.has_any({"Ice Melon", "Large Spring Ball"}, player) or logic.melon_item(state)))
+
+ set_rule(world.multiworld.get_location("What's Gusty Taste Like?: Red Coins", player), lambda state: state.has("! Switch", player))
+
+ set_rule(world.multiworld.get_location("Bigger Boo's Fort: Red Coins", player), lambda state: state.has_all({"! Switch", "Key"}, player))
+ set_rule(world.multiworld.get_location("Bigger Boo's Fort: Flowers", player), lambda state: state.has_all({"! Switch", "Key"}, player))
+ set_rule(world.multiworld.get_location("Bigger Boo's Fort: Stars", player), lambda state: state.has("! Switch", player))
+
+ set_rule(world.multiworld.get_location("Watch Out For Lakitu: Flowers", player), lambda state: state.has_all({"Key", "Train Morph"}, player))
+
+ set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Red Coins", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Flowers", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("Lakitu's Wall: Flowers", player), lambda state: state.has("! Switch", player))
+ set_rule(world.multiworld.get_location("Lakitu's Wall: Stars", player), lambda state: state.has("Giant Eggs", player) or logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Red Coins", player), lambda state: state.has_all({"Arrow Wheel", "Key"}, player))
+ set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Flowers", player), lambda state: state.has_all({"Arrow Wheel", "Key", "Train Morph"}, player))
+ set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Stars", player), lambda state: state.has("Arrow Wheel", player))
+
+ set_rule(world.multiworld.get_location("Welcome To Monkey World!: Stars", player), lambda state: logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("Jungle Rhythm...: Red Coins", player), lambda state: state.has("Dashed Stairs", player))
+ set_rule(world.multiworld.get_location("Jungle Rhythm...: Flowers", player), lambda state: state.has("Dashed Stairs", player))
+ set_rule(world.multiworld.get_location("Jungle Rhythm...: Stars", player), lambda state: logic.has_midring(state) and state.has("Tulip", player))
+ set_rule(world.multiworld.get_location("Jungle Rhythm...: Level Clear", player), lambda state: state.has("Dashed Stairs", player))
+
+ set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Red Coins", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player))
+ set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Flowers", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player))
+ set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Level Clear", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player))
+
+ set_rule(world.multiworld.get_location("Prince Froggy's Fort: Red Coins", player), lambda state: state.has("Submarine Morph", player))
+ set_rule(world.multiworld.get_location("Prince Froggy's Fort: Flowers", player), lambda state: (state.has("Egg Capacity Upgrade", player, 5) or logic.combat_item(state)))
+
+ set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Red Coins", player), lambda state: state.has("Mole Tank Morph", player))
+ set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Flowers", player), lambda state: state.has_all({"Mole Tank Morph", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Stars", player), lambda state: logic.has_midring(state) or state.has("Tulip", player))
+ set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "Key"}, player))
+
+ set_rule(world.multiworld.get_location("Monkeys' Favorite Lake: Red Coins", player), lambda state: state.has_all({"! Switch", "Submarine Morph"}, player))
+
+ set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Red Coins", player), lambda state: state.has("Super Star", player))
+ set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Flowers", player), lambda state: state.has("Super Star", player))
+ set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Level Clear", player), lambda state: state.has("Super Star", player))
+
+ set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Red Coins", player), lambda state: state.has_all({"! Switch", "Egg Launcher"}, player))
+ set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Flowers", player), lambda state: state.has("Egg Launcher", player))
+
+ set_rule(world.multiworld.get_location("Don't Look Back!: Red Coins", player), lambda state: state.has_all({"Helicopter Morph", "Large Spring Ball"}, player))
+ set_rule(world.multiworld.get_location("Don't Look Back!: Flowers", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Don't Look Back!: Stars", player), lambda state: logic.has_midring(state) or state.has("Tulip", player))
+
+ set_rule(world.multiworld.get_location("Marching Milde's Fort: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket", "Key"}, player))
+ set_rule(world.multiworld.get_location("Marching Milde's Fort: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket"}, player))
+ set_rule(world.multiworld.get_location("Marching Milde's Fort: Stars", player), lambda state: state.has("Dashed Stairs", player))
+
+ set_rule(world.multiworld.get_location("Chomp Rock Zone: Red Coins", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Chomp Rock Zone: Flowers", player), lambda state: state.has_all({"Chomp Rock", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("Chomp Rock Zone: Stars", player), lambda state: state.has_all({"Chomp Rock", "! Switch"}, player))
+
+ set_rule(world.multiworld.get_location("Lake Shore Paradise: Stars", player), lambda state: (logic.has_midring(state) or (state.has("Tulip", player) and logic.cansee_clouds(state))))
+
+ set_rule(world.multiworld.get_location("Ride Like The Wind: Red Coins", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Ride Like The Wind: Flowers", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Ride Like The Wind: Stars", player), lambda state: (logic.has_midring(state) or state.has("Helicopter Morph", player)) and state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Ride Like The Wind: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Red Coins", player), lambda state: state.has_all({"Key", "Dashed Stairs"}, player))
+ set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Key"}, player))
+
+ set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Red Coins", player), lambda state: (state.has("Fire Melon", player) or logic.melon_item(state)) and (state.has_all({"Spring Ball", "Skis"}, player)) and (state.has("Super Star", player) or logic.melon_item(state)))
+ set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Flowers", player), lambda state: (state.has("Fire Melon", player) or logic.melon_item(state)) and state.has_all({"Spring Ball", "Skis"}, player))
+ set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Stars", player), lambda state: (logic.has_midring(state) and (state.has("Fire Melon", player) or logic.melon_item(state))) or (logic.has_midring(state) and (state.has_all({"Tulip", "Dashed Platform"}, player))))
+ set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Level Clear", player), lambda state: state.has_all({"Spring Ball", "Skis"}, player))
+
+ set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Dashed Platform", "Platform Ghost"}, player))
+ set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Platform Ghost"}, player))
+
+ set_rule(world.multiworld.get_location("Shifting Platforms Ahead: Stars", player), lambda state: logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Red Coins", player), lambda state: state.has_all({"Arrow Wheel", "Train Morph"}, player))
+ set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Flowers", player), lambda state: state.has_all({"Arrow Wheel", "Train Morph"}, player))
+
+ set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Red Coins", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Flowers", player), lambda state: state.has("Large Spring Ball", player))
+ set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Stars", player), lambda state: logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Level Clear", player), lambda state: state.has("Large Spring Ball", player))
+
+ set_rule(world.multiworld.get_location("The Cave Of The Bandits: Red Coins", player), lambda state: state.has("Super Star", player))
+ set_rule(world.multiworld.get_location("The Cave Of The Bandits: Flowers", player), lambda state: state.has("Super Star", player))
+ set_rule(world.multiworld.get_location("The Cave Of The Bandits: Level Clear", player), lambda state: state.has("Super Star", player))
+
+ set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Red Coins", player), lambda state: state.has_all({"Egg Plant", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Flowers", player), lambda state: state.has_all({"Egg Plant", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)))
+ set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Stars", player), lambda state: state.has("Egg Plant", player) and state.has("Key", player))
+
+ set_rule(world.multiworld.get_location("The Very Loooooong Cave: Stars", player), lambda state: logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("The Deep, Underground Maze: Red Coins", player), lambda state: state.has_all({"Key", "Large Spring Ball"}, player) and (logic.combat_item(state) or state.has("Chomp Rock", player)))
+ set_rule(world.multiworld.get_location("The Deep, Underground Maze: Flowers", player), lambda state: state.has_all({"Key", "Large Spring Ball"}, player) and (logic.combat_item(state) or state.has("Chomp Rock", player)))
+ set_rule(world.multiworld.get_location("The Deep, Underground Maze: Stars", player), lambda state: state.has_all({"Chomp Rock", "Key"}, player))
+ set_rule(world.multiworld.get_location("The Deep, Underground Maze: Level Clear", player), lambda state: state.has_all({"Key", "Large Spring Ball", "Dashed Platform"}, player) and (logic.combat_item(state) or state.has("Chomp Rock", player)))
+
+ set_rule(world.multiworld.get_location("KEEP MOVING!!!!: Stars", player), lambda state: logic.has_midring(state))
+
+ set_rule(world.multiworld.get_location("King Bowser's Castle: Red Coins", player), lambda state: state.has("Helicopter Morph", player) and logic._68CollectibleRoute(state))
+ set_rule(world.multiworld.get_location("King Bowser's Castle: Flowers", player), lambda state: state.has("Helicopter Morph", player) and logic._68CollectibleRoute(state))
+ set_rule(world.multiworld.get_location("King Bowser's Castle: Stars", player), lambda state: state.has("Helicopter Morph", player) and logic._68Route(state))
+
+ set_hard_extra_rules(world)
+
+
+def set_hard_extra_rules(world: "YoshisIslandWorld") -> None:
+ player = world.player
+ logic = YoshiLogic(world)
+ if not world.options.extras_enabled:
+ return
+ set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Red Coins", player), lambda state: state.has("Poochy", player))
+ set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Flowers", player), lambda state: state.has("Poochy", player))
+ set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Stars", player), lambda state: state.has("Poochy", player))
+ set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Level Clear", player), lambda state: state.has("Poochy", player))
+
+ set_rule(world.multiworld.get_location("Hit That Switch!!: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("Hit That Switch!!: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("Hit That Switch!!: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player))
+
+ set_rule(world.multiworld.get_location("The Impossible? Maze: Red Coins", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph", "Flashing Eggs"}, player))
+ set_rule(world.multiworld.get_location("The Impossible? Maze: Flowers", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph"}, player))
+ set_rule(world.multiworld.get_location("The Impossible? Maze: Stars", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph"}, player))
+ set_rule(world.multiworld.get_location("The Impossible? Maze: Level Clear", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph"}, player))
+
+ set_rule(world.multiworld.get_location("Kamek's Revenge: Red Coins", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph"}, player))
+ set_rule(world.multiworld.get_location("Kamek's Revenge: Flowers", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph", "! Switch"}, player))
+ set_rule(world.multiworld.get_location("Kamek's Revenge: Stars", player), lambda state: state.has("! Switch", player) or logic.has_midring(state))
+ set_rule(world.multiworld.get_location("Kamek's Revenge: Level Clear", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph"}, player))
+
+ set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Red Coins", player), lambda state: state.has(("Large Spring Ball"), player))
+ set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Flowers", player), lambda state: state.has(("Large Spring Ball"), player))
+ set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Stars", player), lambda state: True)
+ set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Level Clear", player), lambda state: state.has(("Large Spring Ball"), player))
diff --git a/worlds/yoshisisland/__init__.py b/worlds/yoshisisland/__init__.py
new file mode 100644
index 000000000000..f1aba3018bdb
--- /dev/null
+++ b/worlds/yoshisisland/__init__.py
@@ -0,0 +1,387 @@
+import base64
+import os
+import typing
+import threading
+
+from typing import List, Set, TextIO, Dict
+from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
+from worlds.AutoWorld import World, WebWorld
+import settings
+from .Items import get_item_names_per_category, item_table, filler_items, trap_items
+from .Locations import get_locations
+from .Regions import init_areas
+from .Options import YoshisIslandOptions, PlayerGoal, ObjectVis, StageLogic, MinigameChecks
+from .setup_game import setup_gamevars
+from .Client import YoshisIslandSNIClient
+from .Rules import set_easy_rules, set_normal_rules, set_hard_rules
+from .Rom import LocalRom, patch_rom, get_base_rom_path, YoshisIslandDeltaPatch, USHASH
+
+
+class YoshisIslandSettings(settings.Group):
+ class RomFile(settings.SNESRomPath):
+ """File name of the Yoshi's Island 1.0 US rom"""
+ description = "Yoshi's Island ROM File"
+ copy_to = "Super Mario World 2 - Yoshi's Island (U).sfc"
+ md5s = [USHASH]
+
+ rom_file: RomFile = RomFile(RomFile.copy_to)
+
+
+class YoshisIslandWeb(WebWorld):
+ theme = "ocean"
+
+ setup_en = Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to setting up the Yoshi's Island randomizer and connecting to an Archipelago server.",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["Pink Switch"]
+ )
+
+ tutorials = [setup_en]
+
+
+class YoshisIslandWorld(World):
+ """
+ Yoshi's Island is a 2D platforming game.
+ During a delivery, Bowser's evil ward, Kamek, attacked the stork, kidnapping Luigi and dropping Mario onto Yoshi's Island.
+ As Yoshi, you must run, jump, and throw eggs to escort the baby Mario across the island to defeat Bowser and reunite the two brothers with their parents.
+ """
+ game = "Yoshi's Island"
+ option_definitions = YoshisIslandOptions
+ required_client_version = (0, 4, 4)
+
+ item_name_to_id = {item: item_table[item].code for item in item_table}
+ location_name_to_id = {location.name: location.code for location in get_locations(None)}
+ item_name_groups = get_item_names_per_category()
+
+ web = YoshisIslandWeb()
+ settings: typing.ClassVar[YoshisIslandSettings]
+ # topology_present = True
+
+ options_dataclass = YoshisIslandOptions
+ options: YoshisIslandOptions
+
+ locked_locations: List[str]
+ set_req_bosses: str
+ lives_high: int
+ lives_low: int
+ castle_bosses: int
+ bowser_bosses: int
+ baby_mario_sfx: int
+ leader_color: int
+ boss_order: list
+ boss_burt: int
+ luigi_count: int
+
+ rom_name: bytearray
+
+ def __init__(self, multiworld: MultiWorld, player: int):
+ self.rom_name_available_event = threading.Event()
+ super().__init__(multiworld, player)
+ self.locked_locations = []
+
+ @classmethod
+ def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
+ rom_file = get_base_rom_path()
+ if not os.path.exists(rom_file):
+ raise FileNotFoundError(rom_file)
+
+ def fill_slot_data(self) -> Dict[str, List[int]]:
+ return {
+ "world_1": self.world_1_stages,
+ "world_2": self.world_2_stages,
+ "world_3": self.world_3_stages,
+ "world_4": self.world_4_stages,
+ "world_5": self.world_5_stages,
+ "world_6": self.world_6_stages
+ }
+
+ def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
+ spoiler_handle.write(f"Burt The Bashful's Boss Door: {self.boss_order[0]}\n")
+ spoiler_handle.write(f"Salvo The Slime's Boss Door: {self.boss_order[1]}\n")
+ spoiler_handle.write(f"Bigger Boo's Boss Door: {self.boss_order[2]}\n")
+ spoiler_handle.write(f"Roger The Ghost's Boss Door: {self.boss_order[3]}\n")
+ spoiler_handle.write(f"Prince Froggy's Boss Door: {self.boss_order[4]}\n")
+ spoiler_handle.write(f"Naval Piranha's Boss Door: {self.boss_order[5]}\n")
+ spoiler_handle.write(f"Marching Milde's Boss Door: {self.boss_order[6]}\n")
+ spoiler_handle.write(f"Hookbill The Koopa's Boss Door: {self.boss_order[7]}\n")
+ spoiler_handle.write(f"Sluggy The Unshaven's Boss Door: {self.boss_order[8]}\n")
+ spoiler_handle.write(f"Raphael The Raven's Boss Door: {self.boss_order[9]}\n")
+ spoiler_handle.write(f"Tap-Tap The Red Nose's Boss Door: {self.boss_order[10]}\n")
+ spoiler_handle.write(f"\nLevels:\n1-1: {self.level_name_list[0]}\n")
+ spoiler_handle.write(f"1-2: {self.level_name_list[1]}\n")
+ spoiler_handle.write(f"1-3: {self.level_name_list[2]}\n")
+ spoiler_handle.write(f"1-4: {self.level_name_list[3]}\n")
+ spoiler_handle.write(f"1-5: {self.level_name_list[4]}\n")
+ spoiler_handle.write(f"1-6: {self.level_name_list[5]}\n")
+ spoiler_handle.write(f"1-7: {self.level_name_list[6]}\n")
+ spoiler_handle.write(f"1-8: {self.level_name_list[7]}\n")
+
+ spoiler_handle.write(f"\n2-1: {self.level_name_list[8]}\n")
+ spoiler_handle.write(f"2-2: {self.level_name_list[9]}\n")
+ spoiler_handle.write(f"2-3: {self.level_name_list[10]}\n")
+ spoiler_handle.write(f"2-4: {self.level_name_list[11]}\n")
+ spoiler_handle.write(f"2-5: {self.level_name_list[12]}\n")
+ spoiler_handle.write(f"2-6: {self.level_name_list[13]}\n")
+ spoiler_handle.write(f"2-7: {self.level_name_list[14]}\n")
+ spoiler_handle.write(f"2-8: {self.level_name_list[15]}\n")
+
+ spoiler_handle.write(f"\n3-1: {self.level_name_list[16]}\n")
+ spoiler_handle.write(f"3-2: {self.level_name_list[17]}\n")
+ spoiler_handle.write(f"3-3: {self.level_name_list[18]}\n")
+ spoiler_handle.write(f"3-4: {self.level_name_list[19]}\n")
+ spoiler_handle.write(f"3-5: {self.level_name_list[20]}\n")
+ spoiler_handle.write(f"3-6: {self.level_name_list[21]}\n")
+ spoiler_handle.write(f"3-7: {self.level_name_list[22]}\n")
+ spoiler_handle.write(f"3-8: {self.level_name_list[23]}\n")
+
+ spoiler_handle.write(f"\n4-1: {self.level_name_list[24]}\n")
+ spoiler_handle.write(f"4-2: {self.level_name_list[25]}\n")
+ spoiler_handle.write(f"4-3: {self.level_name_list[26]}\n")
+ spoiler_handle.write(f"4-4: {self.level_name_list[27]}\n")
+ spoiler_handle.write(f"4-5: {self.level_name_list[28]}\n")
+ spoiler_handle.write(f"4-6: {self.level_name_list[29]}\n")
+ spoiler_handle.write(f"4-7: {self.level_name_list[30]}\n")
+ spoiler_handle.write(f"4-8: {self.level_name_list[31]}\n")
+
+ spoiler_handle.write(f"\n5-1: {self.level_name_list[32]}\n")
+ spoiler_handle.write(f"5-2: {self.level_name_list[33]}\n")
+ spoiler_handle.write(f"5-3: {self.level_name_list[34]}\n")
+ spoiler_handle.write(f"5-4: {self.level_name_list[35]}\n")
+ spoiler_handle.write(f"5-5: {self.level_name_list[36]}\n")
+ spoiler_handle.write(f"5-6: {self.level_name_list[37]}\n")
+ spoiler_handle.write(f"5-7: {self.level_name_list[38]}\n")
+ spoiler_handle.write(f"5-8: {self.level_name_list[39]}\n")
+
+ spoiler_handle.write(f"\n6-1: {self.level_name_list[40]}\n")
+ spoiler_handle.write(f"6-2: {self.level_name_list[41]}\n")
+ spoiler_handle.write(f"6-3: {self.level_name_list[42]}\n")
+ spoiler_handle.write(f"6-4: {self.level_name_list[43]}\n")
+ spoiler_handle.write(f"6-5: {self.level_name_list[44]}\n")
+ spoiler_handle.write(f"6-6: {self.level_name_list[45]}\n")
+ spoiler_handle.write(f"6-7: {self.level_name_list[46]}\n")
+ spoiler_handle.write("6-8: King Bowser's Castle")
+
+ def create_item(self, name: str) -> Item:
+ data = item_table[name]
+ return Item(name, data.classification, data.code, self.player)
+
+ def create_regions(self) -> None:
+ init_areas(self, get_locations(self))
+
+ def get_filler_item_name(self) -> str:
+ trap_chance: int = self.options.trap_percent.value
+
+ if self.random.random() < (trap_chance / 100) and self.options.traps_enabled:
+ return self.random.choice(trap_items)
+ else:
+ return self.random.choice(filler_items)
+
+ def set_rules(self) -> None:
+ rules_per_difficulty = {
+ 0: set_easy_rules,
+ 1: set_normal_rules,
+ 2: set_hard_rules
+ }
+
+ rules_per_difficulty[self.options.stage_logic.value](self)
+ self.multiworld.completion_condition[self.player] = lambda state: state.has("Saved Baby Luigi", self.player)
+ self.get_location("Burt The Bashful's Boss Room").place_locked_item(self.create_item("Boss Clear"))
+ self.get_location("Salvo The Slime's Boss Room").place_locked_item(self.create_item("Boss Clear"))
+ self.get_location("Bigger Boo's Boss Room", ).place_locked_item(self.create_item("Boss Clear"))
+ self.get_location("Roger The Ghost's Boss Room").place_locked_item(self.create_item("Boss Clear"))
+ self.get_location("Prince Froggy's Boss Room").place_locked_item(self.create_item("Boss Clear"))
+ self.get_location("Naval Piranha's Boss Room").place_locked_item(self.create_item("Boss Clear"))
+ self.get_location("Marching Milde's Boss Room").place_locked_item(self.create_item("Boss Clear"))
+ self.get_location("Hookbill The Koopa's Boss Room").place_locked_item(self.create_item("Boss Clear"))
+ self.get_location("Sluggy The Unshaven's Boss Room").place_locked_item(self.create_item("Boss Clear"))
+ self.get_location("Raphael The Raven's Boss Room").place_locked_item(self.create_item("Boss Clear"))
+ self.get_location("Tap-Tap The Red Nose's Boss Room").place_locked_item(self.create_item("Boss Clear"))
+
+ if self.options.goal == PlayerGoal.option_luigi_hunt:
+ self.get_location("Reconstituted Luigi").place_locked_item(self.create_item("Saved Baby Luigi"))
+ else:
+ self.get_location("King Bowser's Castle: Level Clear").place_locked_item(
+ self.create_item("Saved Baby Luigi")
+ )
+
+ self.get_location("Touch Fuzzy Get Dizzy: Gather Coins").place_locked_item(
+ self.create_item("Bandit Consumables")
+ )
+ self.get_location("The Cave Of the Mystery Maze: Seed Spitting Contest").place_locked_item(
+ self.create_item("Bandit Watermelons")
+ )
+ self.get_location("Lakitu's Wall: Gather Coins").place_locked_item(self.create_item("Bandit Consumables"))
+ self.get_location("Ride Like The Wind: Gather Coins").place_locked_item(self.create_item("Bandit Consumables"))
+
+ def generate_early(self) -> None:
+ setup_gamevars(self)
+
+ def get_excluded_items(self) -> Set[str]:
+ excluded_items: Set[str] = set()
+
+ starting_gate = ["World 1 Gate", "World 2 Gate", "World 3 Gate",
+ "World 4 Gate", "World 5 Gate", "World 6 Gate"]
+
+ excluded_items.add(starting_gate[self.options.starting_world])
+
+ if not self.options.shuffle_midrings:
+ excluded_items.add("Middle Ring")
+
+ if not self.options.add_secretlens:
+ excluded_items.add("Secret Lens")
+
+ if not self.options.extras_enabled:
+ excluded_items.add("Extra Panels")
+ excluded_items.add("Extra 1")
+ excluded_items.add("Extra 2")
+ excluded_items.add("Extra 3")
+ excluded_items.add("Extra 4")
+ excluded_items.add("Extra 5")
+ excluded_items.add("Extra 6")
+
+ if self.options.split_extras:
+ excluded_items.add("Extra Panels")
+ else:
+ excluded_items.add("Extra 1")
+ excluded_items.add("Extra 2")
+ excluded_items.add("Extra 3")
+ excluded_items.add("Extra 4")
+ excluded_items.add("Extra 5")
+ excluded_items.add("Extra 6")
+
+ if self.options.split_bonus:
+ excluded_items.add("Bonus Panels")
+ else:
+ excluded_items.add("Bonus 1")
+ excluded_items.add("Bonus 2")
+ excluded_items.add("Bonus 3")
+ excluded_items.add("Bonus 4")
+ excluded_items.add("Bonus 5")
+ excluded_items.add("Bonus 6")
+
+ return excluded_items
+
+ def create_item_with_correct_settings(self, name: str) -> Item:
+ data = item_table[name]
+ item = Item(name, data.classification, data.code, self.player)
+
+ if not item.advancement:
+ return item
+
+ if name == "Car Morph" and self.options.stage_logic != StageLogic.option_strict:
+ item.classification = ItemClassification.useful
+
+ secret_lens_visibility_check = (
+ self.options.hidden_object_visibility >= ObjectVis.option_clouds_only
+ or self.options.stage_logic != StageLogic.option_strict
+ )
+ if name == "Secret Lens" and secret_lens_visibility_check:
+ item.classification = ItemClassification.useful
+
+ is_bonus_location = name in {"Bonus 1", "Bonus 2", "Bonus 3", "Bonus 4", "Bonus 5", "Bonus 6", "Bonus Panels"}
+ bonus_games_disabled = (
+ self.options.minigame_checks not in {MinigameChecks.option_bonus_games, MinigameChecks.option_both}
+ )
+ if is_bonus_location and bonus_games_disabled:
+ item.classification = ItemClassification.useful
+
+ if name in {"Bonus 1", "Bonus 3", "Bonus 4", "Bonus Panels"} and self.options.item_logic:
+ item.classification = ItemClassification.progression
+
+ if name == "Piece of Luigi" and self.options.goal == PlayerGoal.option_luigi_hunt:
+ if self.luigi_count >= self.options.luigi_pieces_required:
+ item.classification = ItemClassification.useful
+ else:
+ item.classification = ItemClassification.progression_skip_balancing
+ self.luigi_count += 1
+
+ return item
+
+ def generate_filler(self, pool: List[Item]) -> None:
+ if self.options.goal == PlayerGoal.option_luigi_hunt:
+ for _ in range(self.options.luigi_pieces_in_pool.value):
+ item = self.create_item_with_correct_settings("Piece of Luigi")
+ pool.append(item)
+
+ for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(pool) - 16):
+ item = self.create_item_with_correct_settings(self.get_filler_item_name())
+ pool.append(item)
+
+ def get_item_pool(self, excluded_items: Set[str]) -> List[Item]:
+ pool: List[Item] = []
+
+ for name, data in item_table.items():
+ if name not in excluded_items:
+ for _ in range(data.amount):
+ item = self.create_item_with_correct_settings(name)
+ pool.append(item)
+
+ return pool
+
+ def create_items(self) -> None:
+ self.luigi_count = 0
+
+ if self.options.minigame_checks in {MinigameChecks.option_bonus_games, MinigameChecks.option_both}:
+ self.multiworld.get_location("Flip Cards", self.player).place_locked_item(
+ self.create_item("Bonus Consumables"))
+ self.multiworld.get_location("Drawing Lots", self.player).place_locked_item(
+ self.create_item("Bonus Consumables"))
+ self.multiworld.get_location("Match Cards", self.player).place_locked_item(
+ self.create_item("Bonus Consumables"))
+
+ pool = self.get_item_pool(self.get_excluded_items())
+
+ self.generate_filler(pool)
+
+ self.multiworld.itempool += pool
+
+ def generate_output(self, output_directory: str) -> None:
+ rompath = "" # if variable is not declared finally clause may fail
+ try:
+ world = self.multiworld
+ player = self.player
+ rom = LocalRom(get_base_rom_path())
+ patch_rom(self, rom, self.player)
+
+ rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")
+ rom.write_to_file(rompath)
+ self.rom_name = rom.name
+
+ patch = YoshisIslandDeltaPatch(os.path.splitext(rompath)[0] + YoshisIslandDeltaPatch.patch_file_ending,
+ player=player, player_name=world.player_name[player], patched_path=rompath)
+ patch.write()
+ finally:
+ self.rom_name_available_event.set()
+ if os.path.exists(rompath):
+ os.unlink(rompath)
+
+ def modify_multidata(self, multidata: dict) -> None:
+ # wait for self.rom_name to be available.
+ self.rom_name_available_event.wait()
+ rom_name = getattr(self, "rom_name", None)
+ if rom_name:
+ new_name = base64.b64encode(bytes(self.rom_name)).decode()
+ multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]
+
+ def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]) -> None:
+ world_names = [f"World {i}" for i in range(1, 7)]
+ world_stages = [
+ self.world_1_stages, self.world_2_stages, self.world_3_stages,
+ self.world_4_stages, self.world_5_stages, self.world_6_stages
+ ]
+
+ stage_pos_data = {}
+ for loc in self.multiworld.get_locations(self.player):
+ if loc.address is None:
+ continue
+
+ level_id = getattr(loc, "level_id")
+ for level, stages in zip(world_names, world_stages):
+ if level_id in stages:
+ stage_pos_data[loc.address] = level
+ break
+
+ hint_data[self.player] = stage_pos_data
diff --git a/worlds/yoshisisland/docs/en_Yoshi's Island.md b/worlds/yoshisisland/docs/en_Yoshi's Island.md
new file mode 100644
index 000000000000..d6770c070b94
--- /dev/null
+++ b/worlds/yoshisisland/docs/en_Yoshi's Island.md
@@ -0,0 +1,71 @@
+# Yoshi's Island
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
+
+## What does randomization do to this game?
+
+Certain interactable objects within levels will be unable to be used until the corresponding item is found. If the item is not in the player's posession, the object will flash and will not function. Objects include:
+- Spring Ball
+- Large Spring Ball
+- ! Switch
+- Dashed Platform
+- Dashed Stairs
+- Beanstalk
+- Arrow Wheel
+- Vanishing Arrow Wheel
+- Ice, fire, and normal watermelons
+- Super Star
+- Flashing Eggs
+- Giant Eggs
+- Egg Launcher
+- Egg Refill Plant
+- Chomp Rock
+- Poochy
+- Transformation Morphs
+- Skis
+- Platform Ghost
+- Middle Rings
+- Buckets
+- Tulips
+
+Yoshi will start out being able to carry only one egg, and 5 capacity upgrades can be found to bring the total up to 6.
+The player will start with all levels unlocked in their starting world, and can collect 'World Gates' to unlock levels from other worlds.
+Extra and Bonus stages will also start out locked, and require respective items to access them. 6-8 is locked, and will be unlocked
+upon reaching the number of boss clears defined by the player.
+Other checks will grant the player extra lives, consumables for use in the inventory, or traps.
+
+Additionally, the player is able to randomize the bosses found at the end of boss stages, the order of stages,
+the world they start in, the starting amount of lives, route through 6-8, and the color of Yoshi for each stage.
+
+## What is the goal of Yoshi's Island when randomized?
+
+The player can choose one of two goals:
+- Bowser: Defeat a pre-defined number of bosses, and defeat Bowser at the end of 6-8.
+- Luigi Hunt: Collect a pre-defined number of 'Pieces of Luigi' within levels.
+
+## What items and locations get shuffled?
+
+Locations consist of 'level objectives', that being:
+- Beating the stage
+- Collecting 20 red coins.
+- Collecting 5 flowers.
+- Collecting 30 stars.
+
+Checks will be sent immediately upon achieving that objective, regardless of if the stage is cleared or not.
+Additional checks can be placed on Bandit mini-battles, or overworld minigames.
+
+
+## Which items can be in another player's world?
+
+Any shuffled item can be in other players' worlds.
+
+## What does another world's item look like in Yoshi's Island
+
+Items do not have an appearance in Yoshi's Island
+
+## When the player receives an item, what happens?
+
+When the player recieves an item, a fanfare or sound will be heard to reflect the item received. Most items, aside from Egg Capacity and level unlocks, can be checked on the menu by pressing SELECT.
+If an item is in the queue and has not been received, checks will not be processed.
diff --git a/worlds/yoshisisland/docs/setup_en.md b/worlds/yoshisisland/docs/setup_en.md
new file mode 100644
index 000000000000..d76144608914
--- /dev/null
+++ b/worlds/yoshisisland/docs/setup_en.md
@@ -0,0 +1,122 @@
+# Yoshi's Island Archipelago Randomizer Setup Guide
+
+## Required Software
+
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
+
+
+- Hardware or software capable of loading and playing SNES ROM files
+ - An emulator capable of connecting to SNI such as:
+ - snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases),
+ - BizHawk from: [TASVideos](https://tasvideos.org/BizHawk)
+ - snes9x-nwa from: [snes9x nwa](https://github.com/Skarsnik/snes9x-emunwa/releases)
+
+ NOTE: RetroArch and FXPakPro are not currently supported.
+- Your legally obtained Yoshi's Island English 1.0 ROM file, probably named `Super Mario World 2 - Yoshi's Island (U).sfc`
+
+
+## Installation Procedures
+
+### Windows Setup
+
+1. Download and install Archipelago from the link above, making sure to install the most recent version.
+2. During generation/patching, you will be asked to locate your base ROM file. This is your Yoshi's Island ROM file.
+3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
+ files.
+ 1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
+ 2. Right-click on a ROM file and select **Open with...**
+ 3. Check the box next to **Always use this app to open .sfc files**
+ 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
+ 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you
+ extracted in step one.
+
+## Create a Config (.yaml) File
+
+### What is a config file and why do I need one?
+
+See the guide on setting up a basic YAML at the Archipelago setup
+guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
+
+### Where do I get a config file?
+
+The Player Options page on the website allows you to configure your personal options 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 page. YAML
+validator page: [YAML Validation page](/mysterycheck)
+
+## Joining a MultiWorld Game
+
+### Obtain your patch file and create your ROM
+
+When you join a multiworld game, you will be asked to provide your config file to whomever is hosting. Once that is done,
+the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
+files. Your patch file should have a `.apyi` extension.
+
+Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the
+client, and will also create your ROM in the same place as your patch file.
+
+### Connect to the client
+
+#### With an emulator
+
+When the client launched automatically, SNI should have also automatically launched in the background. If this is its
+first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
+
+##### snes9x-rr
+
+1. Load your ROM file if it hasn't already been loaded.
+2. Click on the File menu and hover on **Lua Scripting**
+3. Click on **New Lua Script Window...**
+4. In the new window, click **Browse...**
+5. Select the connector lua file included with your client
+ - Look in the Archipelago folder for `/SNI/lua/Connector.lua`.
+6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
+the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
+
+##### BizHawk
+
+1. Ensure you have the BSNES core loaded. This is done with the main menubar, under:
+ - (≤ 2.8) `Config` 〉 `Cores` 〉 `SNES` 〉 `BSNES`
+ - (≥ 2.9) `Config` 〉 `Preferred Cores` 〉 `SNES` 〉 `BSNESv115+`
+2. Load your ROM file if it hasn't already been loaded.
+ If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R).
+3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window.
+ - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
+ emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
+ - You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua`
+ with the file picker.
+
+
+
+### Connect to the Archipelago Server
+
+The patch file which launched your client should have automatically connected you to the AP Server. There are a few
+reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the
+client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it
+into the "Server" input field then press enter.
+
+The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
+
+### Play the game
+
+When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on
+successfully joining a multiworld game!
+
+## Hosting a MultiWorld game
+
+The recommended way to host a game is to use our hosting service. The process is relatively simple:
+
+1. Collect config files from your players.
+2. Create a zip file containing your players' config files.
+3. Upload that zip file to the Generate page above.
+ - Generate page: [WebHost Seed Generation Page](/generate)
+4. Wait a moment while the seed is generated.
+5. When the seed is generated, you will be redirected to a "Seed Info" page.
+6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so
+ they may download their patch files from there.
+7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all
+ players in the game. Any observers may also be given the link to this page.
+8. Once all players have joined, you may begin playing.
diff --git a/worlds/yoshisisland/level_logic.py b/worlds/yoshisisland/level_logic.py
new file mode 100644
index 000000000000..094e5efed12d
--- /dev/null
+++ b/worlds/yoshisisland/level_logic.py
@@ -0,0 +1,482 @@
+from BaseClasses import CollectionState
+from typing import TYPE_CHECKING
+
+from .Options import StageLogic, BowserDoor, ObjectVis
+
+if TYPE_CHECKING:
+ from . import YoshisIslandWorld
+
+
+class YoshiLogic:
+ player: int
+ game_logic: str
+ midring_start: bool
+ clouds_always_visible: bool
+ consumable_logic: bool
+ luigi_pieces: int
+
+ def __init__(self, world: "YoshisIslandWorld") -> None:
+ self.player = world.player
+ self.boss_order = world.boss_order
+ self.luigi_pieces = world.options.luigi_pieces_required.value
+
+ if world.options.stage_logic == StageLogic.option_strict:
+ self.game_logic = "Easy"
+ elif world.options.stage_logic == StageLogic.option_loose:
+ self.game_logic = "Normal"
+ else:
+ self.game_logic = "Hard"
+
+ self.midring_start = not world.options.shuffle_midrings
+ self.consumable_logic = not world.options.item_logic
+
+ self.clouds_always_visible = world.options.hidden_object_visibility >= ObjectVis.option_clouds_only
+
+ self.bowser_door = world.options.bowser_door_mode.value
+ if self.bowser_door == BowserDoor.option_door_4:
+ self.bowser_door = BowserDoor.option_door_3
+
+ def has_midring(self, state: CollectionState) -> bool:
+ return self.midring_start or state.has("Middle Ring", self.player)
+
+ def reconstitute_luigi(self, state: CollectionState) -> bool:
+ return state.has("Piece of Luigi", self.player, self.luigi_pieces)
+
+ def bandit_bonus(self, state: CollectionState) -> bool:
+ return state.has("Bandit Consumables", self.player) or state.has("Bandit Watermelons", self.player)
+
+ def item_bonus(self, state: CollectionState) -> bool:
+ return state.has("Bonus Consumables", self.player)
+
+ def combat_item(self, state: CollectionState) -> bool:
+ if not self.consumable_logic:
+ return False
+ else:
+ if self.game_logic == "Easy":
+ return self.item_bonus(state)
+ else:
+ return self.bandit_bonus(state) or self.item_bonus(state)
+
+ def melon_item(self, state: CollectionState) -> bool:
+ if not self.consumable_logic:
+ return False
+ else:
+ if self.game_logic == "Easy":
+ return self.item_bonus(state)
+ else:
+ return state.has("Bandit Watermelons", self.player) or self.item_bonus(state)
+
+ def default_vis(self, state: CollectionState) -> bool:
+ if self.clouds_always_visible:
+ return True
+ else:
+ return False
+
+ def cansee_clouds(self, state: CollectionState) -> bool:
+ if self.game_logic != "Easy":
+ return True
+ else:
+ return self.default_vis(state) or state.has("Secret Lens", self.player) or self.combat_item(state)
+
+ def bowserdoor_1(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Egg Plant", "! Switch"}, self.player) and state.has("Egg Capacity Upgrade", self.player, 2)
+ elif self.game_logic == "Normal":
+ return state.has("Egg Plant", self.player) and state.has("Egg Capacity Upgrade", self.player, 1)
+ else:
+ return state.has("Egg Plant", self.player)
+
+ def bowserdoor_2(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return ((state.has("Egg Capacity Upgrade", self.player, 3) and state.has("Egg Plant", self.player)) or self.combat_item(state)) and state.has("Key", self.player)
+ elif self.game_logic == "Normal":
+ return ((state.has("Egg Capacity Upgrade", self.player, 2) and state.has("Egg Plant", self.player)) or self.combat_item(state)) and state.has("Key", self.player)
+ else:
+ return ((state.has("Egg Capacity Upgrade", self.player, 1) and state.has("Egg Plant", self.player)) or self.combat_item(state)) and state.has("Key", self.player)
+
+ def bowserdoor_3(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return True
+ elif self.game_logic == "Normal":
+ return True
+ else:
+ return True
+
+ def bowserdoor_4(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return True
+ elif self.game_logic == "Normal":
+ return True
+ else:
+ return True
+
+ def _68Route(self, state: CollectionState) -> bool:
+ if self.bowser_door == 0:
+ return True
+ elif self.bowser_door == 1:
+ return self.bowserdoor_1(state)
+ elif self.bowser_door == 2:
+ return self.bowserdoor_2(state)
+ elif self.bowser_door == 3:
+ return True
+ elif self.bowser_door == 4:
+ return True
+ elif self.bowser_door == 5:
+ return self.bowserdoor_1(state) and self.bowserdoor_2(state) and self.bowserdoor_3(state)
+
+ def _68CollectibleRoute(self, state: CollectionState) -> bool:
+ if self.bowser_door == 0:
+ return True
+ elif self.bowser_door == 1:
+ return self.bowserdoor_1(state)
+ elif self.bowser_door == 2:
+ return self.bowserdoor_2(state)
+ elif self.bowser_door == 3:
+ return True
+ elif self.bowser_door == 4:
+ return True
+ elif self.bowser_door == 5:
+ return self.bowserdoor_1(state)
+
+
+##############################################################################
+ def _13Game(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has("Key", self.player)
+ elif self.game_logic == "Normal":
+ return state.has("Key", self.player)
+ else:
+ return state.has("Key", self.player)
+##############################################################################
+ def _14Clear(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Spring Ball", "Key"}, self.player)
+ elif self.game_logic == "Normal":
+ return state.has_all({"Spring Ball", "Key"}, self.player)
+ else:
+ return state.has_all({"Spring Ball", "Key"}, self.player)
+
+ def _14Boss(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has("Egg Plant", self.player)
+ elif self.game_logic == "Normal":
+ return state.has("Egg Plant", self.player)
+ else:
+ return (state.has("Egg Capacity Upgrade", self.player, 5) or state.has("Egg Plant", self.player))
+
+ def _14CanFightBoss(self, state: CollectionState) -> bool:
+ if state.can_reach(self.boss_order[0], "Location", self.player):
+ return True
+##############################################################################
+ def _17Game(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has("Key", self.player)
+ elif self.game_logic == "Normal":
+ return state.has("Key", self.player)
+ else:
+ return state.has("Key", self.player)
+##############################################################################
+ def _18Clear(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Key", "Arrow Wheel"}, self.player)
+ elif self.game_logic == "Normal":
+ return state.has_all({"Key", "Arrow Wheel"}, self.player)
+ else:
+ return state.has_all({"Key", "Arrow Wheel"}, self.player)
+
+ def _18Boss(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return True
+ elif self.game_logic == "Normal":
+ return True
+ else:
+ return True
+
+ def _18CanFightBoss(self, state: CollectionState) -> bool:
+ if state.can_reach(self.boss_order[1], "Location", self.player):
+ return True
+##############################################################################
+ def _21Game(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Poochy", "Large Spring Ball", "Key"}, self.player)
+ elif self.game_logic == "Normal":
+ return state.has_all({"Poochy", "Large Spring Ball", "Key"}, self.player)
+ else:
+ return state.has_all({"Poochy", "Large Spring Ball", "Key"}, self.player)
+##############################################################################
+ def _23Game(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Mole Tank Morph", "Key"}, self.player)
+ elif self.game_logic == "Normal":
+ return state.has_all({"Mole Tank Morph", "Key"}, self.player)
+ else:
+ return state.has_all({"Mole Tank Morph", "Key"}, self.player)
+##############################################################################
+ def _24Clear(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"! Switch", "Key", "Dashed Stairs"}, self.player)
+ elif self.game_logic == "Normal":
+ return state.has_all({"! Switch", "Dashed Stairs"}, self.player)
+ else:
+ return state.has("! Switch", self.player)
+
+ def _24Boss(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return True
+ elif self.game_logic == "Normal":
+ return True
+ else:
+ return True
+
+ def _24CanFightBoss(self, state: CollectionState) -> bool:
+ if state.can_reach(self.boss_order[2], "Location", self.player):
+ return True
+##############################################################################
+ def _26Game(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Large Spring Ball", "Key"}, self.player)
+ elif self.game_logic == "Normal":
+ return state.has_all({"Large Spring Ball", "Key"}, self.player)
+ else:
+ return state.has_all({"Large Spring Ball", "Key"}, self.player)
+##############################################################################
+ def _27Game(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has("Key", self.player)
+ elif self.game_logic == "Normal":
+ return state.has("Key", self.player)
+ else:
+ return state.has("Key", self.player)
+##############################################################################
+ def _28Clear(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Arrow Wheel", "Key"}, self.player) and (state.has("Egg Capacity Upgrade", self.player, 1))
+ elif self.game_logic == "Normal":
+ return state.has_all({"Arrow Wheel", "Key"}, self.player)
+ else:
+ return state.has_all({"Arrow Wheel", "Key"}, self.player)
+
+ def _28Boss(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return True
+ elif self.game_logic == "Normal":
+ return True
+ else:
+ return True
+
+ def _28CanFightBoss(self, state: CollectionState) -> bool:
+ if state.can_reach(self.boss_order[3], "Location", self.player):
+ return True
+##############################################################################
+ def _32Game(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Dashed Stairs", "Spring Ball", "Key"}, self.player)
+ elif self.game_logic == "Normal":
+ return state.has_all({"Dashed Stairs", "Key"}, self.player)
+ else:
+ return state.has_all({"Dashed Stairs", "Key"}, self.player)
+##############################################################################
+ def _34Clear(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has("Dashed Platform", self.player)
+ elif self.game_logic == "Normal":
+ return (state.has("Dashed Platform", self.player) or self.has_midring(state))
+ else:
+ return True
+
+ def _34Boss(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has("Giant Eggs", self.player)
+ elif self.game_logic == "Normal":
+ return True
+ else:
+ return True
+
+ def _34CanFightBoss(self, state: CollectionState) -> bool:
+ if state.can_reach(self.boss_order[4], "Location", self.player):
+ return True
+##############################################################################
+ def _37Game(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Key", "Large Spring Ball"}, self.player)
+ elif self.game_logic == "Normal":
+ return state.has("Key", self.player)
+ else:
+ return state.has("Key", self.player)
+##############################################################################
+ def _38Clear(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return (state.has("Egg Capacity Upgrade", self.player, 3) or self.combat_item(state))
+ elif self.game_logic == "Normal":
+ return (state.has("Egg Capacity Upgrade", self.player, 1) or self.combat_item(state))
+ else:
+ return True
+
+ def _38Boss(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return True
+ elif self.game_logic == "Normal":
+ return True
+ else:
+ return True
+
+ def _38CanFightBoss(self, state: CollectionState) -> bool:
+ if state.can_reach(self.boss_order[5], "Location", self.player):
+ return True
+##############################################################################
+ def _42Game(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Large Spring Ball", "Key"}, self.player)
+ elif self.game_logic == "Normal":
+ return state.has_all({"Large Spring Ball", "Key"}, self.player)
+ else:
+ return state.has("Key", self.player)
+##############################################################################
+ def _44Clear(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket", "Key"}, self.player) and (state.has("Egg Capacity Upgrade", self.player, 1) or self.combat_item(state))
+ elif self.game_logic == "Normal":
+ return state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket", "Key"}, self.player)
+ else:
+ return state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket", "Key"}, self.player)
+
+ def _44Boss(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return True
+ elif self.game_logic == "Normal":
+ return True
+ else:
+ return True
+
+ def _44CanFightBoss(self, state: CollectionState) -> bool:
+ if state.can_reach(self.boss_order[6], "Location", self.player):
+ return True
+########################################################################################################
+
+ def _46Game(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Key", "Large Spring Ball"}, self.player)
+ elif self.game_logic == "Normal":
+ return state.has_all({"Key", "Large Spring Ball"}, self.player)
+ else:
+ return state.has_all({"Key", "Large Spring Ball"}, self.player)
+
+ def _47Game(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Key", "Large Spring Ball"}, self.player)
+ elif self.game_logic == "Normal":
+ return state.has_all({"Key", "Large Spring Ball"}, self.player)
+ else:
+ return state.has_all({"Key", "Large Spring Ball"}, self.player)
+##############################################################################
+ def _48Clear(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return (state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Key", "Large Spring Ball"}, self.player))
+ elif self.game_logic == "Normal":
+ return (state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Key", "Large Spring Ball"}, self.player))
+ else:
+ return (state.has_all({"Key", "Large Spring Ball"}, self.player))
+
+ def _48Boss(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return (state.has("Egg Capacity Upgrade", self.player, 3))
+ elif self.game_logic == "Normal":
+ return (state.has("Egg Capacity Upgrade", self.player, 2))
+ else:
+ return (state.has("Egg Capacity Upgrade", self.player, 1))
+
+ def _48CanFightBoss(self, state: CollectionState) -> bool:
+ if state.can_reach(self.boss_order[7], "Location", self.player):
+ return True
+######################################################################################################################
+ def _51Game(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has("Key", self.player)
+ elif self.game_logic == "Normal":
+ return state.has("Key", self.player)
+ else:
+ return state.has("Key", self.player)
+##############################################################################
+ def _54Clear(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return (state.has_all({"Dashed Stairs", "Platform Ghost", "Dashed Platform"}, self.player))
+ elif self.game_logic == "Normal":
+ return (state.has_all({"Dashed Stairs", "Platform Ghost", "Dashed Platform"}, self.player))
+ else:
+ return (state.has_all({"Dashed Stairs", "Platform Ghost"}, self.player))
+
+ def _54Boss(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return (state.has("Egg Capacity Upgrade", self.player, 2) and state.has("Egg Plant", self.player))
+ elif self.game_logic == "Normal":
+ return ((state.has("Egg Capacity Upgrade", self.player, 1) and state.has("Egg Plant", self.player)) or (state.has("Egg Capacity Upgrade", self.player, 5) and self.has_midring(state)))
+ else:
+ return ((state.has("Egg Plant", self.player)) or (state.has("Egg Capacity Upgrade", self.player, 3) and self.has_midring(state)))
+
+ def _54CanFightBoss(self, state: CollectionState) -> bool:
+ if state.can_reach(self.boss_order[8], "Location", self.player):
+ return True
+###################################################################################################
+ def _58Clear(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Arrow Wheel", "Large Spring Ball"}, self.player)
+ elif self.game_logic == "Normal":
+ return state.has_all({"Arrow Wheel", "Large Spring Ball"}, self.player)
+ else:
+ return state.has_all({"Arrow Wheel", "Large Spring Ball"}, self.player)
+
+ def _58Boss(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return True
+ elif self.game_logic == "Normal":
+ return True
+ else:
+ return True
+
+ def _58CanFightBoss(self, state: CollectionState) -> bool:
+ if state.can_reach(self.boss_order[9], "Location", self.player):
+ return True
+##############################################################################
+ def _61Game(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Dashed Platform", "Key", "Beanstalk"}, self.player)
+ elif self.game_logic == "Normal":
+ return state.has_all({"Dashed Platform", "Key", "Beanstalk"}, self.player)
+ else:
+ return state.has("Key", self.player)
+##############################################################################
+ def _64Clear(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Spring Ball", "Large Spring Ball", "Egg Plant", "Key"}, self.player) and (state.has("Egg Capacity Upgrade", self.player, 3) or self.combat_item(state))
+ elif self.game_logic == "Normal":
+ return state.has_all({"Large Spring Ball", "Egg Plant", "Key"}, self.player) and (state.has("Egg Capacity Upgrade", self.player, 2) or self.combat_item(state))
+ else:
+ return state.has_all({"Egg Plant", "Key"}, self.player) and (state.has("Egg Capacity Upgrade", self.player, 1) or self.combat_item(state))
+
+ def _64Boss(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has("Egg Plant", self.player)
+ elif self.game_logic == "Normal":
+ return state.has("Egg Plant", self.player)
+ else:
+ return True
+
+ def _64CanFightBoss(self, state: CollectionState) -> bool:
+ if state.can_reach(self.boss_order[10], "Location", self.player):
+ return True
+##############################################################################
+ def _67Game(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has("Key", self.player)
+ elif self.game_logic == "Normal":
+ return state.has("Key", self.player)
+ else:
+ return state.has("Key", self.player)
+##############################################################################
+ def _68Clear(self, state: CollectionState) -> bool:
+ if self.game_logic == "Easy":
+ return state.has_all({"Helicopter Morph", "Egg Plant", "Giant Eggs"}, self.player) and self._68Route(state)
+ elif self.game_logic == "Normal":
+ return state.has_all({"Helicopter Morph", "Egg Plant", "Giant Eggs"}, self.player) and self._68Route(state)
+ else:
+ return state.has_all({"Helicopter Morph", "Giant Eggs"}, self.player) and self._68Route(state)
diff --git a/worlds/yoshisisland/setup_bosses.py b/worlds/yoshisisland/setup_bosses.py
new file mode 100644
index 000000000000..bbefdd31a05c
--- /dev/null
+++ b/worlds/yoshisisland/setup_bosses.py
@@ -0,0 +1,19 @@
+from BaseClasses import CollectionState
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+ from . import YoshisIslandWorld
+
+
+class BossReqs:
+ player: int
+
+ def __init__(self, world: "YoshisIslandWorld") -> None:
+ self.player = world.player
+ self.castle_unlock = world.options.castle_open_condition.value
+ self.boss_unlock = world.options.castle_clear_condition.value
+
+ def castle_access(self, state: CollectionState) -> bool:
+ return state.has("Boss Clear", self.player, self.castle_unlock)
+
+ def castle_clear(self, state: CollectionState) -> bool:
+ return state.has("Boss Clear", self.player, self.boss_unlock)
diff --git a/worlds/yoshisisland/setup_game.py b/worlds/yoshisisland/setup_game.py
new file mode 100644
index 000000000000..04a35f7657b7
--- /dev/null
+++ b/worlds/yoshisisland/setup_game.py
@@ -0,0 +1,460 @@
+import struct
+from typing import TYPE_CHECKING
+
+from .Options import YoshiColors, BabySound, LevelShuffle
+
+if TYPE_CHECKING:
+ from . import YoshisIslandWorld
+
+
+def setup_gamevars(world: "YoshisIslandWorld") -> None:
+ if world.options.luigi_pieces_in_pool < world.options.luigi_pieces_required:
+ world.options.luigi_pieces_in_pool.value = world.random.randint(world.options.luigi_pieces_required.value, 100)
+ world.starting_lives = struct.pack("H", world.options.starting_lives)
+
+ world.level_colors = []
+ world.color_order = []
+ for i in range(72):
+ world.level_colors.append(world.random.randint(0, 7))
+ if world.options.yoshi_colors == YoshiColors.option_singularity:
+ singularity_color = world.options.yoshi_singularity_color.value
+ for i in range(len(world.level_colors)):
+ world.level_colors[i] = singularity_color
+ elif world.options.yoshi_colors == YoshiColors.option_random_order:
+ world.leader_color = world.random.randint(0, 7)
+ for i in range(7):
+ world.color_order.append(world.random.randint(0, 7))
+
+ bonus_valid = [0x00, 0x02, 0x04, 0x06, 0x08, 0x0A]
+
+ world.world_bonus = []
+ for i in range(12):
+ world.world_bonus.append(world.random.choice(bonus_valid))
+
+ safe_baby_sounds = [0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
+ 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A,
+ 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x23, 0x24, 0x25, 0x26, 0x27,
+ 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33,
+ 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
+ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B,
+ 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x52, 0x53, 0x54, 0x55, 0x56,
+ 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62,
+ 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
+ 0x73, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
+ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B,
+ 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
+ 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, 0xA0, 0xA1, 0xA2]
+
+ if world.options.baby_mario_sound == BabySound.option_random_sound_effect:
+ world.baby_mario_sfx = world.random.choice(safe_baby_sounds)
+ elif world.options.baby_mario_sound == BabySound.option_disabled:
+ world.baby_mario_sfx = 0x42
+ else:
+ world.baby_mario_sfx = 0x44
+
+ boss_list = ["Burt The Bashful's Boss Room", "Salvo The Slime's Boss Room",
+ "Bigger Boo's Boss Room", "Roger The Ghost's Boss Room",
+ "Prince Froggy's Boss Room", "Naval Piranha's Boss Room",
+ "Marching Milde's Boss Room", "Hookbill The Koopa's Boss Room",
+ "Sluggy The Unshaven's Boss Room", "Raphael The Raven's Boss Room",
+ "Tap-Tap The Red Nose's Boss Room"]
+
+ world.boss_order = []
+
+ if world.options.boss_shuffle:
+ world.random.shuffle(boss_list)
+ world.boss_order = boss_list
+
+ burt_pointers = [0x3D, 0x05, 0x63, 0x00]
+ slime_pointers = [0x70, 0x04, 0x78, 0x00]
+ boo_pointers = [0x74, 0xBB, 0x7A, 0x00]
+ pot_pointers = [0xCF, 0x04, 0x4D, 0x00]
+ frog_pointers = [0xBF, 0x12, 0x62, 0x04]
+ plant_pointers = [0x7F, 0x0D, 0x42, 0x00]
+ milde_pointers = [0x82, 0x06, 0x64, 0x00]
+ koop_pointers = [0x86, 0x0D, 0x78, 0x00]
+ slug_pointers = [0x8A, 0x09, 0x7A, 0x00]
+ raph_pointers = [0xC4, 0x03, 0x4B, 0x05]
+ tap_pointers = [0xCC, 0x49, 0x64, 0x02]
+
+ boss_data_list = [
+ burt_pointers,
+ slime_pointers,
+ boo_pointers,
+ pot_pointers,
+ frog_pointers,
+ plant_pointers,
+ milde_pointers,
+ koop_pointers,
+ slug_pointers,
+ raph_pointers,
+ tap_pointers
+ ]
+
+ boss_levels = [0x03, 0x07, 0x0F, 0x13, 0x1B, 0x1F, 0x27, 0x2B, 0x33, 0x37, 0x3F]
+
+ boss_room_idlist = {
+ "Burt The Bashful's Boss Room": 0,
+ "Salvo The Slime's Boss Room": 1,
+ "Bigger Boo's Boss Room": 2,
+ "Roger The Ghost's Boss Room": 3,
+ "Prince Froggy's Boss Room": 4,
+ "Naval Piranha's Boss Room": 5,
+ "Marching Milde's Boss Room": 6,
+ "Hookbill The Koopa's Boss Room": 7,
+ "Sluggy The Unshaven's Boss Room": 8,
+ "Raphael The Raven's Boss Room": 9,
+ "Tap-Tap The Red Nose's Boss Room": 10,
+ }
+
+ boss_check_list = {
+ "Burt The Bashful's Boss Room": "Burt The Bashful Defeated",
+ "Salvo The Slime's Boss Room": "Salvo The Slime Defeated",
+ "Bigger Boo's Boss Room": "Bigger Boo Defeated",
+ "Roger The Ghost's Boss Room": "Roger The Ghost Defeated",
+ "Prince Froggy's Boss Room": "Prince Froggy Defeated",
+ "Naval Piranha's Boss Room": "Naval Piranha Defeated",
+ "Marching Milde's Boss Room": "Marching Milde Defeated",
+ "Hookbill The Koopa's Boss Room": "Hookbill The Koopa Defeated",
+ "Sluggy The Unshaven's Boss Room": "Sluggy The Unshaven Defeated",
+ "Raphael The Raven's Boss Room": "Raphael The Raven Defeated",
+ "Tap-Tap The Red Nose's Boss Room": "Tap-Tap The Red Nose Defeated",
+ }
+
+ world.boss_room_id = [boss_room_idlist[roomnum] for roomnum in world.boss_order]
+ world.tap_tap_room = boss_levels[world.boss_room_id.index(10)]
+ world.boss_ap_loc = [boss_check_list[roomnum] for roomnum in world.boss_order]
+
+ world.boss_burt_data = boss_data_list[world.boss_room_id[0]]
+
+ world.boss_slime_data = boss_data_list[world.boss_room_id[1]]
+
+ world.boss_boo_data = boss_data_list[world.boss_room_id[2]]
+
+ world.boss_pot_data = boss_data_list[world.boss_room_id[3]]
+
+ world.boss_frog_data = boss_data_list[world.boss_room_id[4]]
+
+ world.boss_plant_data = boss_data_list[world.boss_room_id[5]]
+
+ world.boss_milde_data = boss_data_list[world.boss_room_id[6]]
+
+ world.boss_koop_data = boss_data_list[world.boss_room_id[7]]
+
+ world.boss_slug_data = boss_data_list[world.boss_room_id[8]]
+
+ world.boss_raph_data = boss_data_list[world.boss_room_id[9]]
+
+ world.boss_tap_data = boss_data_list[world.boss_room_id[10]]
+
+ world.global_level_list = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13,
+ 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
+ 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B,
+ 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
+ 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42]
+ level_id_list = {
+ 0x00: "1-1",
+ 0x01: "1-2",
+ 0x02: "1-3",
+ 0x03: "1-4",
+ 0x04: "1-5",
+ 0x05: "1-6",
+ 0x06: "1-7",
+ 0x07: "1-8",
+ 0x0C: "2-1",
+ 0x0D: "2-2",
+ 0x0E: "2-3",
+ 0x0F: "2-4",
+ 0x10: "2-5",
+ 0x11: "2-6",
+ 0x12: "2-7",
+ 0x13: "2-8",
+ 0x18: "3-1",
+ 0x19: "3-2",
+ 0x1A: "3-3",
+ 0x1B: "3-4",
+ 0x1C: "3-5",
+ 0x1D: "3-6",
+ 0x1E: "3-7",
+ 0x1F: "3-8",
+ 0x24: "4-1",
+ 0x25: "4-2",
+ 0x26: "4-3",
+ 0x27: "4-4",
+ 0x28: "4-5",
+ 0x29: "4-6",
+ 0x2A: "4-7",
+ 0x2B: "4-8",
+ 0x30: "5-1",
+ 0x31: "5-2",
+ 0x32: "5-3",
+ 0x33: "5-4",
+ 0x34: "5-5",
+ 0x35: "5-6",
+ 0x36: "5-7",
+ 0x37: "5-8",
+ 0x3C: "6-1",
+ 0x3D: "6-2",
+ 0x3E: "6-3",
+ 0x3F: "6-4",
+ 0x40: "6-5",
+ 0x41: "6-6",
+ 0x42: "6-7"
+ }
+
+ level_names = {
+ 0x00: "Make Eggs, Throw Eggs",
+ 0x01: "Watch Out Below!",
+ 0x02: "The Cave Of Chomp Rock",
+ 0x03: "Burt The Bashful's Fort",
+ 0x04: "Hop! Hop! Donut Lifts",
+ 0x05: "Shy-Guys On Stilts",
+ 0x06: "Touch Fuzzy Get Dizzy",
+ 0x07: "Salvo The Slime's Castle",
+ 0x0C: "Visit Koopa And Para-Koopa",
+ 0x0D: "The Baseball Boys",
+ 0x0E: "What's Gusty Taste Like?",
+ 0x0F: "Bigger Boo's Fort",
+ 0x10: "Watch Out For Lakitu",
+ 0x11: "The Cave Of The Mystery Maze",
+ 0x12: "Lakitu's Wall",
+ 0x13: "The Potted Ghost's Castle",
+ 0x18: "Welcome To Monkey World!",
+ 0x19: "Jungle Rhythm...",
+ 0x1A: "Nep-Enuts' Domain",
+ 0x1B: "Prince Froggy's Fort",
+ 0x1C: "Jammin' Through The Trees",
+ 0x1D: "The Cave Of Harry Hedgehog",
+ 0x1E: "Monkeys' Favorite Lake",
+ 0x1F: "Naval Piranha's Castle",
+ 0x24: "GO! GO! MARIO!!",
+ 0x25: "The Cave Of The Lakitus",
+ 0x26: "Don't Look Back!",
+ 0x27: "Marching Milde's Fort",
+ 0x28: "Chomp Rock Zone",
+ 0x29: "Lake Shore Paradise",
+ 0x2A: "Ride Like The Wind",
+ 0x2B: "Hookbill The Koopa's Castle",
+ 0x30: "BLIZZARD!!!",
+ 0x31: "Ride The Ski Lifts",
+ 0x32: "Danger - Icy Conditions Ahead",
+ 0x33: "Sluggy The Unshaven's Fort",
+ 0x34: "Goonie Rides!",
+ 0x35: "Welcome To Cloud World",
+ 0x36: "Shifting Platforms Ahead",
+ 0x37: "Raphael The Raven's Castle",
+ 0x3C: "Scary Skeleton Goonies!",
+ 0x3D: "The Cave Of The Bandits",
+ 0x3E: "Beware The Spinning Logs",
+ 0x3F: "Tap-Tap The Red Nose's Fort",
+ 0x40: "The Very Loooooong Cave",
+ 0x41: "The Deep, Underground Maze",
+ 0x42: "KEEP MOVING!!!!"
+ }
+
+ world_1_offsets = [0x01, 0x00, 0x00, 0x00, 0x00, 0x00]
+ world_2_offsets = [0x01, 0x01, 0x00, 0x00, 0x00, 0x00]
+ world_3_offsets = [0x01, 0x01, 0x01, 0x00, 0x00, 0x00]
+ world_4_offsets = [0x01, 0x01, 0x01, 0x01, 0x00, 0x00]
+ world_5_offsets = [0x01, 0x01, 0x01, 0x01, 0x01, 0x00]
+ easy_start_lv = [0x02, 0x04, 0x06, 0x0E, 0x10, 0x18, 0x1C, 0x28,
+ 0x30, 0x31, 0x35, 0x36, 0x3E, 0x40, 0x42]
+ norm_start_lv = [0x00, 0x01, 0x02, 0x04, 0x06, 0x0E, 0x10, 0x12, 0x18, 0x1A,
+ 0x1C, 0x1E, 0x28, 0x30, 0x31, 0x34, 0x35, 0x36, 0x3D, 0x3E, 0x40, 0x42]
+ hard_start_lv = [0x00, 0x01, 0x02, 0x04, 0x06, 0x0D, 0x0E, 0x10, 0x11, 0x12, 0x18, 0x1A, 0x1C,
+ 0x1E, 0x24, 0x25, 0x26, 0x28, 0x29, 0x30, 0x31, 0x34, 0x35, 0x36, 0x3D, 0x3E,
+ 0x40, 0x42]
+ diff_index = [easy_start_lv, norm_start_lv, hard_start_lv]
+ diff_level = diff_index[world.options.stage_logic.value]
+ boss_lv = [0x03, 0x07, 0x0F, 0x13, 0x1B, 0x1F, 0x27, 0x2B, 0x33, 0x37, 0x3F]
+ world.world_start_lv = [0, 8, 16, 24, 32, 40]
+ if not world.options.shuffle_midrings:
+ easy_start_lv.extend([0x1A, 0x24, 0x34])
+ norm_start_lv.extend([0x24, 0x3C])
+ hard_start_lv.extend([0x1D, 0x3C])
+
+ if world.options.level_shuffle != LevelShuffle.option_bosses_guaranteed:
+ hard_start_lv.extend([0x07, 0x1B, 0x1F, 0x2B, 0x33, 0x37])
+ if not world.options.shuffle_midrings:
+ easy_start_lv.extend([0x1B])
+ norm_start_lv.extend([0x1B, 0x2B, 0x37])
+
+ starting_level = world.random.choice(diff_level)
+
+ starting_level_entrance = world.world_start_lv[world.options.starting_world.value]
+ if world.options.level_shuffle:
+ world.global_level_list.remove(starting_level)
+ world.random.shuffle(world.global_level_list)
+ if world.options.level_shuffle == LevelShuffle.option_bosses_guaranteed:
+ for i in range(11):
+ world.global_level_list = [item for item in world.global_level_list
+ if item not in boss_lv]
+ world.random.shuffle(boss_lv)
+
+ world.global_level_list.insert(3 - world_1_offsets[world.options.starting_world.value], boss_lv[0]) # 1 if starting world is 1, 0 otherwise
+ world.global_level_list.insert(7 - world_1_offsets[world.options.starting_world.value], boss_lv[1])
+ world.global_level_list.insert(11 - world_2_offsets[world.options.starting_world.value], boss_lv[2])
+ world.global_level_list.insert(15 - world_2_offsets[world.options.starting_world.value], boss_lv[3])
+ world.global_level_list.insert(19 - world_3_offsets[world.options.starting_world.value], boss_lv[4])
+ world.global_level_list.insert(23 - world_3_offsets[world.options.starting_world.value], boss_lv[5])
+ world.global_level_list.insert(27 - world_4_offsets[world.options.starting_world.value], boss_lv[6])
+ world.global_level_list.insert(31 - world_4_offsets[world.options.starting_world.value], boss_lv[7])
+ world.global_level_list.insert(35 - world_5_offsets[world.options.starting_world.value], boss_lv[8])
+ world.global_level_list.insert(39 - world_5_offsets[world.options.starting_world.value], boss_lv[9])
+ world.global_level_list.insert(43 - 1, boss_lv[10])
+ world.global_level_list.insert(starting_level_entrance, starting_level)
+ world.level_location_list = [level_id_list[LevelID] for LevelID in world.global_level_list]
+ world.level_name_list = [level_names[LevelID] for LevelID in world.global_level_list]
+
+ level_panel_dict = {
+ 0x00: [0x04, 0x04, 0x53],
+ 0x01: [0x20, 0x04, 0x53],
+ 0x02: [0x3C, 0x04, 0x53],
+ 0x03: [0x58, 0x04, 0x53],
+ 0x04: [0x74, 0x04, 0x53],
+ 0x05: [0x90, 0x04, 0x53],
+ 0x06: [0xAC, 0x04, 0x53],
+ 0x07: [0xC8, 0x04, 0x53],
+ 0x0C: [0x04, 0x24, 0x53],
+ 0x0D: [0x20, 0x24, 0x53],
+ 0x0E: [0x3C, 0x24, 0x53],
+ 0x0F: [0x58, 0x24, 0x53],
+ 0x10: [0x74, 0x24, 0x53],
+ 0x11: [0x90, 0x24, 0x53],
+ 0x12: [0xAC, 0x24, 0x53],
+ 0x13: [0xC8, 0x24, 0x53],
+ 0x18: [0x04, 0x44, 0x53],
+ 0x19: [0x20, 0x44, 0x53],
+ 0x1A: [0x3C, 0x44, 0x53],
+ 0x1B: [0x58, 0x44, 0x53],
+ 0x1C: [0x74, 0x44, 0x53],
+ 0x1D: [0x90, 0x44, 0x53],
+ 0x1E: [0xAC, 0x44, 0x53],
+ 0x1F: [0xC8, 0x44, 0x53],
+ 0x24: [0x04, 0x64, 0x53],
+ 0x25: [0x20, 0x64, 0x53],
+ 0x26: [0x3C, 0x64, 0x53],
+ 0x27: [0x58, 0x64, 0x53],
+ 0x28: [0x74, 0x64, 0x53],
+ 0x29: [0x90, 0x64, 0x53],
+ 0x2A: [0xAC, 0x64, 0x53],
+ 0x2B: [0xC8, 0x64, 0x53],
+ 0x30: [0x04, 0x04, 0x53],
+ 0x31: [0x20, 0x04, 0x53],
+ 0x32: [0x3C, 0x04, 0x53],
+ 0x33: [0x58, 0x04, 0x53],
+ 0x34: [0x74, 0x04, 0x53],
+ 0x35: [0x90, 0x04, 0x53],
+ 0x36: [0xAC, 0x04, 0x53],
+ 0x37: [0xC8, 0x04, 0x53],
+ 0x3C: [0x04, 0x24, 0x53],
+ 0x3D: [0x20, 0x24, 0x53],
+ 0x3E: [0x3C, 0x24, 0x53],
+ 0x3F: [0x58, 0x24, 0x53],
+ 0x40: [0x74, 0x24, 0x53],
+ 0x41: [0x90, 0x24, 0x53],
+ 0x42: [0xAC, 0x24, 0x53],
+ }
+ panel_palette_1 = [0x00, 0x03, 0x04, 0x05, 0x0C, 0x10, 0x12, 0x13, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
+ 0x24, 0x26, 0x27, 0x29, 0x2A, 0x2B, 0x30, 0x32, 0x34,
+ 0x35, 0x37, 0x3C, 0x3D, 0x40, 0x41] # 000C
+ panel_palette_2 = [0x01, 0x02, 0x06, 0x07, 0x0D, 0x0E, 0x0F, 0x11, 0x18, 0x1E, 0x1F, 0x25, 0x28,
+ 0x31, 0x33, 0x36, 0x3E, 0x3F, 0x42] # 0010
+
+ stage_number = 0
+ world_number = 1
+ for i in range(47):
+ stage_number += 1
+ if stage_number >= 9:
+ world_number += 1
+ stage_number = 1
+ for _ in range(3):
+ setattr(world, f"Stage{world_number}{stage_number}StageGFX",
+ level_panel_dict[world.global_level_list[i]])
+
+ world.level_gfx_table = []
+ world.palette_panel_list = []
+
+ for i in range(47):
+ if world.global_level_list[i] >= 0x30:
+ world.level_gfx_table.append(0x15)
+ else:
+ world.level_gfx_table.append(0x11)
+
+ if world.global_level_list[i] in panel_palette_1:
+ world.palette_panel_list.extend([0x00, 0x0C])
+ elif world.global_level_list[i] in panel_palette_2:
+ world.palette_panel_list.extend([0x00, 0x10])
+
+ world.palette_panel_list[16:16] = [0x00, 0x0c, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x18]
+ world.palette_panel_list[40:40] = [0x00, 0x0c, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x18]
+ world.palette_panel_list[64:64] = [0x00, 0x0c, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x18]
+ world.palette_panel_list[88:88] = [0x00, 0x0c, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x18]
+ world.palette_panel_list[112:112] = [0x00, 0x0c, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x18]
+
+ world.level_gfx_table.insert(8, 0x15)
+ world.level_gfx_table.insert(8, 0x15)
+ world.level_gfx_table.insert(8, 0x15)
+ world.level_gfx_table.insert(8, 0x11)
+
+ world.level_gfx_table.insert(20, 0x15)
+ world.level_gfx_table.insert(20, 0x15)
+ world.level_gfx_table.insert(20, 0x15)
+ world.level_gfx_table.insert(20, 0x11)
+
+ world.level_gfx_table.insert(32, 0x15)
+ world.level_gfx_table.insert(32, 0x15)
+ world.level_gfx_table.insert(32, 0x15)
+ world.level_gfx_table.insert(32, 0x11)
+
+ world.level_gfx_table.insert(44, 0x15)
+ world.level_gfx_table.insert(44, 0x15)
+ world.level_gfx_table.insert(44, 0x15)
+ world.level_gfx_table.insert(44, 0x11)
+
+ world.level_gfx_table.insert(56, 0x15)
+ world.level_gfx_table.insert(56, 0x15)
+ world.level_gfx_table.insert(56, 0x15)
+ world.level_gfx_table.insert(56, 0x15)
+
+ castle_door_dict = {
+ 0: [0xB8, 0x05, 0x77, 0x00],
+ 1: [0xB8, 0x05, 0x77, 0x00],
+ 2: [0xC6, 0x07, 0x7A, 0x00],
+ 3: [0xCD, 0x05, 0x5B, 0x00],
+ 4: [0xD3, 0x00, 0x77, 0x06],
+ 5: [0xB8, 0x05, 0x77, 0x00],
+ }
+
+ world.castle_door = castle_door_dict[world.options.bowser_door_mode.value]
+
+ world.world_1_stages = world.global_level_list[0:8]
+ world.world_2_stages = world.global_level_list[8:16]
+ world.world_3_stages = world.global_level_list[16:24]
+ world.world_4_stages = world.global_level_list[24:32]
+ world.world_5_stages = world.global_level_list[32:40]
+ world.world_6_stages = world.global_level_list[40:47]
+
+ world.world_1_stages.extend([0x08, 0x09])
+ world.world_2_stages.extend([0x14, 0x15])
+ world.world_3_stages.extend([0x20, 0x21])
+ world.world_4_stages.extend([0x2C, 0x2D])
+ world.world_5_stages.extend([0x38, 0x39])
+ world.world_6_stages.extend([0x43, 0x44, 0x45])
+
+ bowser_text_table = {
+ 0: [0xDE, 0xEE, 0xDC, 0xDC, 0xE5], # Gween
+ 1: [0xE7, 0xE0, 0xE5, 0xE2, 0xD0], # Pink
+ 3: [0xEB, 0xDF, 0xF0, 0xD8, 0xE5], # Thyan
+ 2: [0xF0, 0xDC, 0xEE, 0xEE, 0xE6], # Yewow
+ 4: [0xE7, 0xEC, 0xDF, 0xE7, 0xE3], # puhpl
+ 5: [0xD9, 0xEE, 0xE6, 0xEE, 0xE5], # Bwown
+ 6: [0xEE, 0xDC, 0xDB, 0xD0, 0xD0], # Wed
+ 7: [0xD9, 0xEE, 0xEC, 0xDC, 0xD0], # Bwue
+ }
+
+ if world.options.yoshi_colors == YoshiColors.option_random_order:
+ world.bowser_text = bowser_text_table[world.leader_color]
+ else:
+ world.bowser_text = bowser_text_table[world.level_colors[67]]
diff --git a/worlds/yugioh06/__init__.py b/worlds/yugioh06/__init__.py
new file mode 100644
index 000000000000..1cf44f090fed
--- /dev/null
+++ b/worlds/yugioh06/__init__.py
@@ -0,0 +1,456 @@
+import os
+import pkgutil
+from typing import Any, ClassVar, Dict, List
+
+import settings
+from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial
+
+import Utils
+from worlds.AutoWorld import WebWorld, World
+
+from .boosterpacks import booster_contents as booster_contents
+from .boosterpacks import get_booster_locations
+from .items import (
+ Banlist_Items,
+ booster_packs,
+ draft_boosters,
+ draft_opponents,
+ excluded_items,
+ item_to_index,
+ tier_1_opponents,
+ useful,
+)
+from .items import (
+ challenges as challenges,
+)
+from .locations import (
+ Bonuses,
+ Campaign_Opponents,
+ Limited_Duels,
+ Required_Cards,
+ Theme_Duels,
+ collection_events,
+ get_beat_challenge_events,
+ special,
+)
+from .logic import core_booster, yugioh06_difficulty
+from .opponents import OpponentData, get_opponent_condition, get_opponent_locations, get_opponents
+from .opponents import challenge_opponents as challenge_opponents
+from .options import Yugioh06Options
+from .rom import MD5America, MD5Europe, YGO06ProcedurePatch, write_tokens
+from .rom import get_base_rom_path as get_base_rom_path
+from .rom_values import banlist_ids as banlist_ids
+from .rom_values import function_addresses as function_addresses
+from .rom_values import structure_deck_selection as structure_deck_selection
+from .rules import set_rules
+from .structure_deck import get_deck_content_locations
+from .client_bh import YuGiOh2006Client
+
+
+class Yugioh06Web(WebWorld):
+ theme = "stone"
+ setup = Tutorial(
+ "Multiworld Setup Tutorial",
+ "A guide to setting up Yu-Gi-Oh! - Ultimate Masters Edition - World Championship Tournament 2006 "
+ "for Archipelago on your computer.",
+ "English",
+ "docs/setup_en.md",
+ "setup/en",
+ ["Rensen"],
+ )
+ tutorials = [setup]
+
+
+class Yugioh2006Setting(settings.Group):
+ class Yugioh2006RomFile(settings.UserFilePath):
+ """File name of your Yu-Gi-Oh 2006 ROM"""
+
+ description = "Yu-Gi-Oh 2006 ROM File"
+ copy_to = "YuGiOh06.gba"
+ md5s = [MD5Europe, MD5America]
+
+ rom_file: Yugioh2006RomFile = Yugioh2006RomFile(Yugioh2006RomFile.copy_to)
+
+
+class Yugioh06World(World):
+ """
+ Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 is the definitive Yu-Gi-Oh
+ simulator on the GBA. Featuring over 2000 cards and over 90 Challenges.
+ """
+
+ game = "Yu-Gi-Oh! 2006"
+ web = Yugioh06Web()
+ options: Yugioh06Options
+ options_dataclass = Yugioh06Options
+ settings_key = "yugioh06_settings"
+ settings: ClassVar[Yugioh2006Setting]
+
+ item_name_to_id = {}
+ start_id = 5730000
+ for k, v in item_to_index.items():
+ item_name_to_id[k] = v + start_id
+
+ location_name_to_id = {}
+ for k, v in Bonuses.items():
+ location_name_to_id[k] = v + start_id
+
+ for k, v in Limited_Duels.items():
+ location_name_to_id[k] = v + start_id
+
+ for k, v in Theme_Duels.items():
+ location_name_to_id[k] = v + start_id
+
+ for k, v in Campaign_Opponents.items():
+ location_name_to_id[k] = v + start_id
+
+ for k, v in special.items():
+ location_name_to_id[k] = v + start_id
+
+ for k, v in Required_Cards.items():
+ location_name_to_id[k] = v + start_id
+
+ item_name_groups = {
+ "Core Booster": core_booster,
+ "Campaign Boss Beaten": ["Tier 1 Beaten", "Tier 2 Beaten", "Tier 3 Beaten", "Tier 4 Beaten", "Tier 5 Beaten"],
+ }
+
+ removed_challenges: List[str]
+ starting_booster: str
+ starting_opponent: str
+ campaign_opponents: List[OpponentData]
+ is_draft_mode: bool
+
+ def __init__(self, world: MultiWorld, player: int):
+ super().__init__(world, player)
+
+ def generate_early(self):
+ self.starting_opponent = ""
+ self.starting_booster = ""
+ self.removed_challenges = []
+ # Universal tracker stuff, shouldn't do anything in standard gen
+ if hasattr(self.multiworld, "re_gen_passthrough"):
+ if "Yu-Gi-Oh! 2006" in self.multiworld.re_gen_passthrough:
+ # bypassing random yaml settings
+ slot_data = self.multiworld.re_gen_passthrough["Yu-Gi-Oh! 2006"]
+ self.options.structure_deck.value = slot_data["structure_deck"]
+ self.options.banlist.value = slot_data["banlist"]
+ self.options.final_campaign_boss_unlock_condition.value = slot_data[
+ "final_campaign_boss_unlock_condition"
+ ]
+ self.options.fourth_tier_5_campaign_boss_unlock_condition.value = slot_data[
+ "fourth_tier_5_campaign_boss_unlock_condition"
+ ]
+ self.options.third_tier_5_campaign_boss_unlock_condition.value = slot_data[
+ "third_tier_5_campaign_boss_unlock_condition"
+ ]
+ self.options.final_campaign_boss_challenges.value = slot_data["final_campaign_boss_challenges"]
+ self.options.fourth_tier_5_campaign_boss_challenges.value = slot_data[
+ "fourth_tier_5_campaign_boss_challenges"
+ ]
+ self.options.third_tier_5_campaign_boss_challenges.value = slot_data[
+ "third_tier_5_campaign_boss_challenges"
+ ]
+ self.options.final_campaign_boss_campaign_opponents.value = slot_data[
+ "final_campaign_boss_campaign_opponents"
+ ]
+ self.options.fourth_tier_5_campaign_boss_campaign_opponents.value = slot_data[
+ "fourth_tier_5_campaign_boss_campaign_opponents"
+ ]
+ self.options.third_tier_5_campaign_boss_campaign_opponents.value = slot_data[
+ "third_tier_5_campaign_boss_campaign_opponents"
+ ]
+ self.options.number_of_challenges.value = slot_data["number_of_challenges"]
+ self.removed_challenges = slot_data["removed challenges"]
+ self.starting_booster = slot_data["starting_booster"]
+ self.starting_opponent = slot_data["starting_opponent"]
+
+ if self.options.structure_deck.current_key == "none":
+ self.is_draft_mode = True
+ boosters = draft_boosters
+ if self.options.campaign_opponents_shuffle.value:
+ opponents = tier_1_opponents
+ else:
+ opponents = draft_opponents
+ else:
+ self.is_draft_mode = False
+ boosters = booster_packs
+ opponents = tier_1_opponents
+
+ if self.options.structure_deck.current_key == "random_deck":
+ self.options.structure_deck.value = self.random.randint(0, 5)
+ for item in self.options.start_inventory:
+ if item in opponents:
+ self.starting_opponent = item
+ if item in boosters:
+ self.starting_booster = item
+ if not self.starting_opponent:
+ self.starting_opponent = self.random.choice(opponents)
+ self.multiworld.push_precollected(self.create_item(self.starting_opponent))
+ if not self.starting_booster:
+ self.starting_booster = self.random.choice(boosters)
+ self.multiworld.push_precollected(self.create_item(self.starting_booster))
+ banlist = self.options.banlist.value
+ self.multiworld.push_precollected(self.create_item(Banlist_Items[banlist]))
+
+ if not self.removed_challenges:
+ challenge = list(({**Limited_Duels, **Theme_Duels}).keys())
+ noc = len(challenge) - max(
+ self.options.third_tier_5_campaign_boss_challenges.value
+ if self.options.third_tier_5_campaign_boss_unlock_condition == "challenges"
+ else 0,
+ self.options.fourth_tier_5_campaign_boss_challenges.value
+ if self.options.fourth_tier_5_campaign_boss_unlock_condition == "challenges"
+ else 0,
+ self.options.final_campaign_boss_challenges.value
+ if self.options.final_campaign_boss_unlock_condition == "challenges"
+ else 0,
+ self.options.number_of_challenges.value,
+ )
+
+ self.random.shuffle(challenge)
+ excluded = self.options.exclude_locations.value.intersection(challenge)
+ prio = self.options.priority_locations.value.intersection(challenge)
+ normal = [e for e in challenge if e not in excluded and e not in prio]
+ total = list(excluded) + normal + list(prio)
+ self.removed_challenges = total[:noc]
+
+ self.campaign_opponents = get_opponents(
+ self.multiworld, self.player, self.options.campaign_opponents_shuffle.value
+ )
+
+ def create_region(self, name: str, locations=None, exits=None):
+ region = Region(name, self.player, self.multiworld)
+ if locations:
+ for location_name, lid in locations.items():
+ if lid is not None and isinstance(lid, int):
+ lid = self.location_name_to_id[location_name]
+ else:
+ lid = None
+ location = Yugioh2006Location(self.player, location_name, lid, region)
+ region.locations.append(location)
+
+ if exits:
+ for _exit in exits:
+ region.exits.append(Entrance(self.player, _exit, region))
+ return region
+
+ def create_regions(self):
+ structure_deck = self.options.structure_deck.current_key
+ self.multiworld.regions += [
+ self.create_region("Menu", None, ["to Deck Edit", "to Campaign", "to Challenges", "to Card Shop"]),
+ self.create_region("Campaign", {**Bonuses, **Campaign_Opponents}),
+ self.create_region("Challenges"),
+ self.create_region("Card Shop", {**Required_Cards, **collection_events}),
+ self.create_region("Structure Deck", get_deck_content_locations(structure_deck)),
+ ]
+
+ self.get_entrance("to Campaign").connect(self.get_region("Campaign"))
+ self.get_entrance("to Challenges").connect(self.get_region("Challenges"))
+ self.get_entrance("to Card Shop").connect(self.get_region("Card Shop"))
+ self.get_entrance("to Deck Edit").connect(self.get_region("Structure Deck"))
+
+ campaign = self.get_region("Campaign")
+ # Campaign Opponents
+ for opponent in self.campaign_opponents:
+ unlock_item = "Campaign Tier " + str(opponent.tier) + " Column " + str(opponent.column)
+ region = self.create_region(opponent.name, get_opponent_locations(opponent))
+ entrance = Entrance(self.player, unlock_item, campaign)
+ if opponent.tier == 5 and opponent.column > 2:
+ unlock_amount = 0
+ is_challenge = True
+ if opponent.column == 3:
+ if self.options.third_tier_5_campaign_boss_unlock_condition.value == 1:
+ unlock_item = "Challenge Beaten"
+ unlock_amount = self.options.third_tier_5_campaign_boss_challenges.value
+ is_challenge = True
+ else:
+ unlock_item = "Campaign Boss Beaten"
+ unlock_amount = self.options.third_tier_5_campaign_boss_campaign_opponents.value
+ is_challenge = False
+ if opponent.column == 4:
+ if self.options.fourth_tier_5_campaign_boss_unlock_condition.value == 1:
+ unlock_item = "Challenge Beaten"
+ unlock_amount = self.options.fourth_tier_5_campaign_boss_challenges.value
+ is_challenge = True
+ else:
+ unlock_item = "Campaign Boss Beaten"
+ unlock_amount = self.options.fourth_tier_5_campaign_boss_campaign_opponents.value
+ is_challenge = False
+ if opponent.column == 5:
+ if self.options.final_campaign_boss_unlock_condition.value == 1:
+ unlock_item = "Challenge Beaten"
+ unlock_amount = self.options.final_campaign_boss_challenges.value
+ is_challenge = True
+ else:
+ unlock_item = "Campaign Boss Beaten"
+ unlock_amount = self.options.final_campaign_boss_campaign_opponents.value
+ is_challenge = False
+ entrance.access_rule = get_opponent_condition(
+ opponent, unlock_item, unlock_amount, self.player, is_challenge
+ )
+ else:
+ entrance.access_rule = lambda state, unlock=unlock_item, opp=opponent: state.has(
+ unlock, self.player
+ ) and yugioh06_difficulty(state, self.player, opp.difficulty)
+ campaign.exits.append(entrance)
+ entrance.connect(region)
+ self.multiworld.regions.append(region)
+
+ card_shop = self.get_region("Card Shop")
+ # Booster Contents
+ for booster in booster_packs:
+ region = self.create_region(booster, get_booster_locations(booster))
+ entrance = Entrance(self.player, booster, card_shop)
+ entrance.access_rule = lambda state, unlock=booster: state.has(unlock, self.player)
+ card_shop.exits.append(entrance)
+ entrance.connect(region)
+ self.multiworld.regions.append(region)
+
+ challenge_region = self.get_region("Challenges")
+ # Challenges
+ for challenge, lid in ({**Limited_Duels, **Theme_Duels}).items():
+ if challenge in self.removed_challenges:
+ continue
+ region = self.create_region(challenge, {challenge: lid, challenge + " Complete": None})
+ entrance = Entrance(self.player, challenge, challenge_region)
+ entrance.access_rule = lambda state, unlock=challenge: state.has(unlock + " Unlock", self.player)
+ challenge_region.exits.append(entrance)
+ entrance.connect(region)
+ self.multiworld.regions.append(region)
+
+ def create_item(self, name: str) -> Item:
+ classification: ItemClassification = ItemClassification.progression
+ if name == "5000DP":
+ classification = ItemClassification.filler
+ if name in useful:
+ classification = ItemClassification.useful
+ return Item(name, classification, self.item_name_to_id[name], self.player)
+
+ def create_filler(self) -> Item:
+ return self.create_item("5000DP")
+
+ def get_filler_item_name(self) -> str:
+ return "5000DP"
+
+ def create_items(self):
+ start_inventory = self.options.start_inventory.value.copy()
+ item_pool = []
+ items = item_to_index.copy()
+ starting_list = Banlist_Items[self.options.banlist.value]
+ if not self.options.add_empty_banlist.value and starting_list != "No Banlist":
+ items.pop("No Banlist")
+ for rc in self.removed_challenges:
+ items.pop(rc + " Unlock")
+ items.pop(self.starting_opponent)
+ items.pop(self.starting_booster)
+ items.pop(starting_list)
+ for name in items:
+ if name in excluded_items or name in start_inventory:
+ continue
+ item = self.create_item(name)
+ item_pool.append(item)
+
+ needed_item_pool_size = sum(loc not in self.removed_challenges for loc in self.location_name_to_id)
+ needed_filler_amount = needed_item_pool_size - len(item_pool)
+ item_pool += [self.create_item("5000DP") for _ in range(needed_filler_amount)]
+
+ self.multiworld.itempool += item_pool
+
+ for challenge in get_beat_challenge_events(self):
+ item = Yugioh2006Item("Challenge Beaten", ItemClassification.progression, None, self.player)
+ location = self.multiworld.get_location(challenge, self.player)
+ location.place_locked_item(item)
+
+ for opponent in self.campaign_opponents:
+ for location_name, event in get_opponent_locations(opponent).items():
+ if event is not None and not isinstance(event, int):
+ item = Yugioh2006Item(event, ItemClassification.progression, None, self.player)
+ location = self.multiworld.get_location(location_name, self.player)
+ location.place_locked_item(item)
+
+ for booster in booster_packs:
+ for location_name, content in get_booster_locations(booster).items():
+ item = Yugioh2006Item(content, ItemClassification.progression, None, self.player)
+ location = self.multiworld.get_location(location_name, self.player)
+ location.place_locked_item(item)
+
+ structure_deck = self.options.structure_deck.current_key
+ for location_name, content in get_deck_content_locations(structure_deck).items():
+ item = Yugioh2006Item(content, ItemClassification.progression, None, self.player)
+ location = self.multiworld.get_location(location_name, self.player)
+ location.place_locked_item(item)
+
+ for event in collection_events:
+ item = Yugioh2006Item(event, ItemClassification.progression, None, self.player)
+ location = self.multiworld.get_location(event, self.player)
+ location.place_locked_item(item)
+
+ def set_rules(self):
+ set_rules(self)
+
+ def generate_output(self, output_directory: str):
+ outfilepname = f"_P{self.player}"
+ outfilepname += f"_{self.multiworld.get_file_safe_player_name(self.player).replace(' ', '_')}"
+ self.rom_name_text = f'YGO06{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0'
+ self.romName = bytearray(self.rom_name_text, "utf8")[:0x20]
+ self.romName.extend([0] * (0x20 - len(self.romName)))
+ self.rom_name = self.romName
+ self.playerName = bytearray(self.multiworld.player_name[self.player], "utf8")[:0x20]
+ self.playerName.extend([0] * (0x20 - len(self.playerName)))
+ patch = YGO06ProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player])
+ patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, "patch.bsdiff4"))
+ procedure = [("apply_bsdiff4", ["base_patch.bsdiff4"]), ("apply_tokens", ["token_data.bin"])]
+ if self.is_draft_mode:
+ procedure.insert(1, ("apply_bsdiff4", ["draft_patch.bsdiff4"]))
+ patch.write_file("draft_patch.bsdiff4", pkgutil.get_data(__name__, "patches/draft.bsdiff4"))
+ if self.options.ocg_arts:
+ procedure.insert(1, ("apply_bsdiff4", ["ocg_patch.bsdiff4"]))
+ patch.write_file("ocg_patch.bsdiff4", pkgutil.get_data(__name__, "patches/ocg.bsdiff4"))
+ patch.procedure = procedure
+ write_tokens(self, patch)
+
+ # Write Output
+ out_file_name = self.multiworld.get_out_file_name_base(self.player)
+ patch.write(os.path.join(output_directory, f"{out_file_name}{patch.patch_file_ending}"))
+
+ def fill_slot_data(self) -> Dict[str, Any]:
+ slot_data: Dict[str, Any] = {
+ "structure_deck": self.options.structure_deck.value,
+ "banlist": self.options.banlist.value,
+ "final_campaign_boss_unlock_condition": self.options.final_campaign_boss_unlock_condition.value,
+ "fourth_tier_5_campaign_boss_unlock_condition":
+ self.options.fourth_tier_5_campaign_boss_unlock_condition.value,
+ "third_tier_5_campaign_boss_unlock_condition":
+ self.options.third_tier_5_campaign_boss_unlock_condition.value,
+ "final_campaign_boss_challenges": self.options.final_campaign_boss_challenges.value,
+ "fourth_tier_5_campaign_boss_challenges":
+ self.options.fourth_tier_5_campaign_boss_challenges.value,
+ "third_tier_5_campaign_boss_challenges":
+ self.options.third_tier_5_campaign_boss_campaign_opponents.value,
+ "final_campaign_boss_campaign_opponents":
+ self.options.final_campaign_boss_campaign_opponents.value,
+ "fourth_tier_5_campaign_boss_campaign_opponents":
+ self.options.fourth_tier_5_campaign_boss_unlock_condition.value,
+ "third_tier_5_campaign_boss_campaign_opponents":
+ self.options.third_tier_5_campaign_boss_campaign_opponents.value,
+ "number_of_challenges": self.options.number_of_challenges.value,
+ }
+
+ slot_data["removed challenges"] = self.removed_challenges
+ slot_data["starting_booster"] = self.starting_booster
+ slot_data["starting_opponent"] = self.starting_opponent
+ return slot_data
+
+ # for the universal tracker, doesn't get called in standard gen
+ @staticmethod
+ def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]:
+ # returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough
+ return slot_data
+
+
+class Yugioh2006Item(Item):
+ game: str = "Yu-Gi-Oh! 2006"
+
+
+class Yugioh2006Location(Location):
+ game: str = "Yu-Gi-Oh! 2006"
diff --git a/worlds/yugioh06/boosterpacks.py b/worlds/yugioh06/boosterpacks.py
new file mode 100644
index 000000000000..645977d28def
--- /dev/null
+++ b/worlds/yugioh06/boosterpacks.py
@@ -0,0 +1,923 @@
+from typing import Dict, List
+
+booster_contents: Dict[str, List[str]] = {
+ "LEGEND OF B.E.W.D.": [
+ "Exodia",
+ "Dark Magician",
+ "Polymerization",
+ "Skull Servant"
+ ],
+ "METAL RAIDERS": [
+ "Petit Moth",
+ "Cocoon of Evolution",
+ "Time Wizard",
+ "Gate Guardian",
+ "Kazejin",
+ "Suijin",
+ "Sanga of the Thunder",
+ "Sangan",
+ "Castle of Dark Illusions",
+ "Soul Release",
+ "Magician of Faith",
+ "Dark Elf",
+ "Summoned Skull",
+ "Sangan",
+ "7 Colored Fish",
+ "Tribute to the Doomed",
+ "Horn of Heaven",
+ "Magic Jammer",
+ "Seven Tools of the Bandit",
+ "Solemn Judgment",
+ "Dream Clown",
+ "Heavy Storm"
+ ],
+ "PHARAOH'S SERVANT": [
+ "Beast of Talwar",
+ "Jinzo",
+ "Gearfried the Iron Knight",
+ "Harpie's Brother",
+ "Gravity Bind",
+ "Solemn Wishes",
+ "Kiseitai",
+ "Morphing Jar #2",
+ "The Shallow Grave",
+ "Nobleman of Crossout",
+ "Magic Drain"
+ ],
+ "PHARAONIC GUARDIAN": [
+ "Don Zaloog",
+ "Reasoning",
+ "Dark Snake Syndrome",
+ "Helpoemer",
+ "Newdoria",
+ "Spirit Reaper",
+ "Yomi Ship",
+ "Pyramid Turtle",
+ "Master Kyonshee",
+ "Book of Life",
+ "Call of the Mummy",
+ "Gravekeeper's Spy",
+ "Gravekeeper's Guard",
+ "A Cat of Ill Omen",
+ "Jowls of Dark Demise",
+ "Non Aggression Area",
+ "Terraforming",
+ "Des Lacooda",
+ "Swarm of Locusts",
+ "Swarm of Scarabs",
+ "Wandering Mummy",
+ "Royal Keeper",
+ "Book of Moon",
+ "Book of Taiyou",
+ "Dust Tornado",
+ "Raigeki Break"
+ ],
+ "SPELL RULER": [
+ "Ritual",
+ "Messenger of Peace",
+ "Megamorph",
+ "Shining Angel",
+ "Mystic Tomato",
+ "Giant Rat",
+ "Mother Grizzly",
+ "UFO Turtle",
+ "Flying Kamakiri 1",
+ "Giant Germ",
+ "Nimble Momonga",
+ "Cyber Jar",
+ "Spear Cretin",
+ "Toon Mermaid",
+ "Toon Summoned Skull",
+ "Toon World",
+ "Rush Recklessly",
+ "The Reliable Guardian",
+ "Senju of the Thousand Hands",
+ "Sonic Bird",
+ "Mystical Space Typhoon"
+ ],
+ "LABYRINTH OF NIGHTMARE": [
+ "Destiny Board",
+ "Spirit Message 'I'",
+ "Spirit Message 'N'",
+ "Spirit Message 'A'",
+ "Spirit Message 'L'",
+ "Fusion Gate",
+ "Jowgen the Spiritualist",
+ "Fairy Box",
+ "Aqua Spirit",
+ "Rock Spirit",
+ "Spirit of Flames",
+ "Garuda the Wind Spirit",
+ "Hysteric Fairy",
+ "Kycoo the Ghost Destroyer",
+ "Gemini Elf",
+ "Amphibian Beast",
+ "Revival Jam",
+ "Dancing Fairy",
+ "Cure Mermaid",
+ "The Last Warrior from Another Planet",
+ "United We Stand",
+ "Earthbound Spirit",
+ "The Masked Beast"
+ ],
+ "LEGACY OF DARKNESS": [
+ "Last Turn",
+ "Yata-Garasu",
+ "Opticlops",
+ "Dark Ruler Ha Des",
+ "Exiled Force",
+ "Injection Fairy Lily",
+ "Spear Dragon",
+ "Luster Dragon #2",
+ "Twin-Headed Behemoth",
+ "Airknight Parshath",
+ "Freed the Matchless General",
+ "Marauding Captain",
+ "Reinforcement of the Army",
+ "Cave Dragon",
+ "Troop Dragon",
+ "Stamping Destruction",
+ "Creature Swap",
+ "Asura Priest",
+ "Fushi No Tori",
+ "Maharaghi",
+ "Susa Soldier",
+ "Emergency Provisions",
+ ],
+ "MAGICIAN'S FORCE": [
+ "Huge Revolution",
+ "Oppressed People",
+ "United Resistance",
+ "People Running About",
+ "X-Head Cannon",
+ "Y-Dragon Head",
+ "Z-Metal Tank",
+ "XY-Dragon Cannon",
+ "XZ-Tank Cannon",
+ "YZ-Tank Dragon",
+ "XYZ-Dragon Cannon",
+ "Cliff the Trap Remover",
+ "Wave-Motion Cannon",
+ "Ritual",
+ "Magical Merchant",
+ "Poison of the Old Man",
+ "Chaos Command Magician",
+ "Skilled Dark Magician",
+ "Dark Blade",
+ "Great Angus",
+ "Luster Dragon",
+ "Breaker the magical Warrior",
+ "Old Vindictive Magician",
+ "Apprentice Magician",
+ "Burning Beast",
+ "Freezing Beast",
+ "Pitch-Dark Dragon",
+ "Giant Orc",
+ "Second Goblin",
+ "Decayed Commander",
+ "Zombie Tiger",
+ "Vampire Orchis",
+ "Des Dendle",
+ "Frontline Base",
+ "Formation Union",
+ "Pitch-Black Power Stone",
+ "Magical Marionette",
+ "Royal Magical Library",
+ "Spell Shield Type-8",
+ "Tribute Doll",
+ ],
+ "DARK CRISIS": [
+ "Final Countdown",
+ "Ojama Green",
+ "Dark Scorpion Combination",
+ "Dark Scorpion - Chick the Yellow",
+ "Dark Scorpion - Meanae the Thorn",
+ "Dark Scorpion - Gorg the Strong",
+ "Ritual",
+ "Tsukuyomi",
+ "Ojama Trio",
+ "Kaiser Glider",
+ "D.D. Warrior Lady",
+ "Archfiend Soldier",
+ "Skull Archfiend of Lightning",
+ "Blindly Loyal Goblin",
+ "Gagagigo",
+ "Nin-Ken Dog",
+ "Zolga",
+ "Kelbek",
+ "Mudora",
+ "Cestus of Dagla",
+ "Vampire Lord",
+ "Metallizing Parasite - Lunatite",
+ "D. D. Trainer",
+ "Spell Reproduction",
+ "Contract with the Abyss",
+ "Dark Master - Zorc"
+ ],
+ "INVASION OF CHAOS": [
+ "Ojama Delta Hurricane",
+ "Ojama Yellow",
+ "Ojama Black",
+ "Heart of the Underdog",
+ "Chaos Emperor Dragon - Envoy of the End",
+ "Self-Destruct Button",
+ "Manticore of Darkness",
+ "Dimension Fusion",
+ "Gigantes",
+ "Inferno",
+ "Silpheed",
+ "Mad Dog of Darkness",
+ "Ryu Kokki",
+ "Berserk Gorilla",
+ "Neo Bug",
+ "Dark Driceratops",
+ "Hyper Hammerhead",
+ "Sea Serpent Warrior of Darkness",
+ "Giga Gagagigo",
+ "Terrorking Salmon",
+ "Blazing Inpachi",
+ "Stealth Bird",
+ "Reload",
+ "Cursed Seal of the Forbidden Spell",
+ "Stray Lambs",
+ "Manju of the Ten Thousand Hands"
+ ],
+ "ANCIENT SANCTUARY": [
+ "Monster Gate",
+ "Wall of Revealing Light",
+ "Mystik Wok",
+ "The Agent of Judgment - Saturn",
+ "Zaborg the Thunder Monarch",
+ "Regenerating Mummy",
+ "The End of Anubis",
+ "Solar Flare Dragon",
+ "Level Limit - Area B",
+ "King of the Swamp",
+ "Enemy Controller",
+ "Enchanting Fitting Room"
+ ],
+ "SOUL OF THE DUELIST": [
+ "Ninja Grandmaster Sasuke",
+ "Mystic Swordsman LV2",
+ "Mystic Swordsman LV4",
+ "Enraged Muka Muka",
+ "Mobius the Frost Monarch",
+ "Horus the Black Flame Dragon LV6",
+ "Ultimate Baseball Kid",
+ "Armed Dragon LV3",
+ "Armed Dragon LV5",
+ "Masked Dragon",
+ "Element Dragon",
+ "Horus the Black Flame Dragon LV4",
+ "Level Up!",
+ "Howling Insect",
+ "Mobius the Frost Monarch"
+ ],
+ "RISE OF DESTINY": [
+ "Homunculus the Alchemic Being",
+ "Thestalos the Firestorm Monarch",
+ "Roc from the Valley of Haze",
+ "Harpie Lady 1",
+ "Silent Swordsman Lv3",
+ "Mystic Swordsman LV6",
+ "Ultimate Insect Lv3",
+ "Divine Wrath",
+ "Serial Spell"
+ ],
+ "FLAMING ETERNITY": [
+ "Insect Knight",
+ "Chiron the Mage",
+ "Granmarg the Rock Monarch",
+ "Silent Swordsman Lv5",
+ "The Dark - Hex-Sealed Fusion",
+ "The Earth - Hex-Sealed Fusion",
+ "The Light - Hex-Sealed Fusion",
+ "Ultimate Insect Lv5",
+ "Blast Magician",
+ "Golem Sentry",
+ "Rescue Cat",
+ "Blade Rabbit"
+ ],
+ "THE LOST MILLENIUM": [
+ "Ritual",
+ "Megarock Dragon",
+ "D.D. Survivor",
+ "Hieracosphinx",
+ "Elemental Hero Flame Wingman",
+ "Elemental Hero Avian",
+ "Elemental Hero Burstinatrix",
+ "Elemental Hero Clayman",
+ "Elemental Hero Sparkman",
+ "Elemental Hero Thunder Giant",
+ "Aussa the Earth Charmer",
+ "Brain Control"
+ ],
+ "CYBERNETIC REVOLUTION": [
+ "Power Bond",
+ "Cyber Dragon",
+ "Cyber Twin Dragon",
+ "Cybernetic Magician",
+ "Indomitable Fighter Lei Lei",
+ "Protective Soul Ailin",
+ "Miracle Fusion",
+ "Elemental Hero Bubbleman",
+ "Jerry Beans Man"
+ ],
+ "ELEMENTAL ENERGY": [
+ "V-Tiger Jet",
+ "W-Wing Catapult",
+ "VW-Tiger Catapult",
+ "VWXYZ-Dragon Catapult Cannon",
+ "Zure, Knight of Dark World",
+ "Brron, Mad King of Dark World",
+ "Familiar-Possessed - Aussa",
+ "Familiar-Possessed - Eria",
+ "Familiar-Possessed - Hiita",
+ "Familiar-Possessed - Wynn",
+ "Oxygeddon",
+ "Roll Out!",
+ "Dark World Lightning",
+ "Elemental Hero Rampart Blaster",
+ "Elemental Hero Shining Flare Wingman",
+ "Elemental Hero Wildedge",
+ "Elemental Hero Wildheart",
+ "Elemental Hero Bladedge",
+ "Pot of Avarice",
+ "B.E.S. Tetran"
+ ],
+ "SHADOW OF INFINITY": [
+ "Hamon, Lord of Striking Thunder",
+ "Raviel, Lord of Phantasms",
+ "Uria, Lord of Searing Flames",
+ "Ritual",
+ "Treeborn Frog",
+ "Saber Beetle",
+ "Tenkabito Shien",
+ "Princess Pikeru",
+ "Gokipon",
+ "Demise, King of Armageddon",
+ "Anteatereatingant"
+ ],
+ "GAME GIFT COLLECTION": [
+ "Ritual",
+ "Valkyrion the Magna Warrior",
+ "Alpha the Magnet Warrior",
+ "Beta the Magnet Warrior",
+ "Gamma the Magnet Warrior",
+ "Magical Blast",
+ "Dunames Dark Witch",
+ "Vorse Raider",
+ "Exarion Universe",
+ "Abyss Soldier",
+ "Slate Warrior",
+ "Cyber-Tech Alligator",
+ "D.D. Assailant",
+ "Goblin Zombie",
+ "Elemental Hero Madballman",
+ "Mind Control",
+ "Toon Dark Magician Girl",
+ "Great Spirit",
+ "Graceful Dice",
+ "Negate Attack",
+ "Foolish Burial",
+ "Card Destruction",
+ "Dark Magic Ritual",
+ "Calamity of the Wicked"
+ ],
+ "Special Gift Collection": [
+ "Gate Guardian",
+ "Scapegoat",
+ "Gil Garth",
+ "La Jinn the Mystical Genie of the Lamp",
+ "Summoned Skull",
+ "Inferno Hammer",
+ "Gemini Elf",
+ "Cyber Harpie Lady",
+ "Dandylion",
+ "Blade Knight",
+ "Curse of Vampire",
+ "Elemental Hero Flame Wingman",
+ "Magician of Black Chaos"
+ ],
+ "Fairy Collection": [
+ "Silpheed",
+ "Dunames Dark Witch",
+ "Hysteric Fairy",
+ "The Agent of Judgment - Saturn",
+ "Shining Angel",
+ "Airknight Parshath",
+ "Dancing Fairy",
+ "Zolga",
+ "Kelbek",
+ "Mudora",
+ "Protective Soul Ailin",
+ "Marshmallon",
+ "Goddess with the Third Eye",
+ "Asura Priest",
+ "Manju of the Ten Thousand Hands",
+ "Senju of the Thousand Hands"
+ ],
+ "Dragon Collection": [
+ "Victory D.",
+ "Chaos Emperor Dragon - Envoy of the End",
+ "Kaiser Glider",
+ "Horus the Black Flame Dragon LV6",
+ "Luster Dragon",
+ "Luster Dragon #2"
+ "Spear Dragon",
+ "Armed Dragon LV3",
+ "Armed Dragon LV5",
+ "Twin-Headed Behemoth",
+ "Cave Dragon",
+ "Masked Dragon",
+ "Element Dragon",
+ "Troop Dragon",
+ "Horus the Black Flame Dragon LV4",
+ "Pitch-Dark Dragon"
+ ],
+ "Warrior Collection A": [
+ "Gate Guardian",
+ "Gearfried the Iron Knight",
+ "Dimensional Warrior",
+ "Command Knight",
+ "The Last Warrior from Another Planet",
+ "Dream Clown"
+ ],
+ "Warrior Collection B": [
+ "Don Zaloog",
+ "Dark Scorpion - Chick the Yellow",
+ "Dark Scorpion - Meanae the Thorn",
+ "Dark Scorpion - Gorg the Strong",
+ "Cliff the Trap Remover",
+ "Ninja Grandmaster Sasuke",
+ "D.D. Warrior Lady",
+ "Mystic Swordsman LV2",
+ "Mystic Swordsman LV4",
+ "Mystic Swordsman LV6",
+ "Dark Blade",
+ "Blindly Loyal Goblin",
+ "Exiled Force",
+ "Ultimate Baseball Kid",
+ "Freed the Matchless General",
+ "Holy Knight Ishzark",
+ "Silent Swordsman Lv3",
+ "Silent Swordsman Lv5",
+ "Warrior Lady of the Wasteland",
+ "D.D. Assailant",
+ "Blade Knight",
+ "Marauding Captain",
+ "Toon Goblin Attack Force"
+ ],
+ "Fiend Collection A": [
+ "Sangan",
+ "Castle of Dark Illusions",
+ "Barox",
+ "La Jinn the Mystical Genie of the Lamp",
+ "Summoned Skull",
+ "Beast of Talwar",
+ "Sangan",
+ "Giant Germ",
+ "Spear Cretin",
+ "Versago the Destroyer",
+ "Toon Summoned Skull"
+ ],
+ "Fiend Collection B": [
+ "Raviel, Lord of Phantasms",
+ "Yata-Garasu",
+ "Helpoemer",
+ "Archfiend Soldier",
+ "Skull Descovery Knight",
+ "Gil Garth",
+ "Opticlops",
+ "Zure, Knight of Dark World",
+ "Brron, Mad King of Dark World",
+ "D.D. Survivor",
+ "Skull Archfiend of Lightning",
+ "The End of Anubis",
+ "Dark Ruler Ha Des",
+ "Inferno Hammer",
+ "Legendary Fiend",
+ "Newdoria",
+ "Slate Warrior",
+ "Giant Orc",
+ "Second Goblin",
+ "Kiseitai",
+ "Jowls of Dark Demise",
+ "D. D. Trainer",
+ "Earthbound Spirit"
+ ],
+ "Machine Collection A": [
+ "Cyber-Stein",
+ "Mechanicalchaser",
+ "Jinzo",
+ "UFO Turtle",
+ "Cyber-Tech Alligator"
+ ],
+ "Machine Collection B": [
+ "X-Head Cannon",
+ "Y-Dragon Head",
+ "Z-Metal Tank",
+ "XY-Dragon Cannon",
+ "XZ-Tank Cannon",
+ "YZ-Tank Dragon",
+ "XYZ-Dragon Cannon",
+ "V-Tiger Jet",
+ "W-Wing Catapult",
+ "VW-Tiger Catapult",
+ "VWXYZ-Dragon Catapult Cannon",
+ "Cyber Dragon",
+ "Cyber Twin Dragon",
+ "Green Gadget",
+ "Red Gadget",
+ "Yellow Gadget",
+ "B.E.S. Tetran"
+ ],
+ "Spellcaster Collection A": [
+ "Exodia",
+ "Dark Sage",
+ "Dark Magician",
+ "Time Wizard",
+ "Kazejin",
+ "Magician of Faith",
+ "Dark Elf",
+ "Gemini Elf",
+ "Injection Fairy Lily",
+ "Cosmo Queen",
+ "Magician of Black Chaos"
+ ],
+ "Spellcaster Collection B": [
+ "Jowgen the Spiritualist",
+ "Tsukuyomi",
+ "Manticore of Darkness",
+ "Chaos Command Magician",
+ "Cybernetic Magician",
+ "Skilled Dark Magician",
+ "Kycoo the Ghost Destroyer",
+ "Toon Gemini Elf",
+ "Toon Masked Sorcerer",
+ "Toon Dark Magician Girl",
+ "Familiar-Possessed - Aussa",
+ "Familiar-Possessed - Eria",
+ "Familiar-Possessed - Hiita",
+ "Familiar-Possessed - Wynn",
+ "Breaker the magical Warrior",
+ "The Tricky",
+ "Gravekeeper's Spy",
+ "Gravekeeper's Guard",
+ "Summon Priest",
+ "Old Vindictive Magician",
+ "Apprentice Magician",
+ "Princess Pikeru",
+ "Blast Magician",
+ "Magical Marionette",
+ "Mythical Beast Cerberus",
+ "Royal Magical Library",
+ "Aussa the Earth Charmer",
+
+ ],
+ "Zombie Collection": [
+ "Skull Servant",
+ "Regenerating Mummy",
+ "Ryu Kokki",
+ "Spirit Reaper",
+ "Pyramid Turtle",
+ "Master Kyonshee",
+ "Curse of Vampire",
+ "Vampire Lord",
+ "Goblin Zombie",
+ "Decayed Commander",
+ "Zombie Tiger",
+ "Des Lacooda",
+ "Wandering Mummy",
+ "Royal Keeper"
+ ],
+ "Special Monsters A": [
+ "X-Head Cannon",
+ "Y-Dragon Head",
+ "Z-Metal Tank",
+ "V-Tiger Jet",
+ "W-Wing Catapult",
+ "Yata-Garasu",
+ "Tsukuyomi",
+ "Dark Blade",
+ "Toon Gemini Elf",
+ "Toon Goblin Attack Force",
+ "Toon Masked Sorcerer",
+ "Toon Mermaid",
+ "Toon Dark Magician Girl",
+ "Toon Summoned Skull",
+ "Toon World",
+ "Burning Beast",
+ "Freezing Beast",
+ "Metallizing Parasite - Lunatite",
+ "Pitch-Dark Dragon",
+ "Giant Orc",
+ "Second Goblin",
+ "Decayed Commander",
+ "Zombie Tiger",
+ "Vampire Orchis",
+ "Des Dendle",
+ "Indomitable Fighter Lei Lei",
+ "Protective Soul Ailin",
+ "Frontline Base",
+ "Formation Union",
+ "Roll Out!",
+ "Asura Priest",
+ "Fushi No Tori",
+ "Maharaghi",
+ "Susa Soldier"
+ ],
+ "Special Monsters B": [
+ "Polymerization",
+ "Mystic Swordsman LV2",
+ "Mystic Swordsman LV4",
+ "Mystic Swordsman LV6",
+ "Horus the Black Flame Dragon LV6",
+ "Horus the Black Flame Dragon LV4",
+ "Armed Dragon LV3"
+ "Armed Dragon LV5",
+ "Silent Swordsman Lv3",
+ "Silent Swordsman Lv5",
+ "Elemental Hero Flame Wingman",
+ "Elemental Hero Avian",
+ "Elemental Hero Burstinatrix",
+ "Miracle Fusion",
+ "Elemental Hero Madballman",
+ "Elemental Hero Bubbleman",
+ "Elemental Hero Clayman",
+ "Elemental Hero Rampart Blaster",
+ "Elemental Hero Shining Flare Wingman",
+ "Elemental Hero Sparkman",
+ "Elemental Hero Steam Healer",
+ "Elemental Hero Thunder Giant",
+ "Elemental Hero Wildedge",
+ "Elemental Hero Wildheart",
+ "Elemental Hero Bladedge",
+ "Level Up!",
+ "Ultimate Insect Lv3",
+ "Ultimate Insect Lv5"
+ ],
+ "Reverse Collection": [
+ "Magical Merchant",
+ "Castle of Dark Illusions",
+ "Magician of Faith",
+ "Penguin Soldier",
+ "Blade Knight",
+ "Gravekeeper's Spy",
+ "Gravekeeper's Guard",
+ "Old Vindictive Magician",
+ "A Cat of Ill Omen",
+ "Jowls of Dark Demise",
+ "Cyber Jar",
+ "Morphing Jar",
+ "Morphing Jar #2",
+ "Needle Worm",
+ "Spear Cretin",
+ "Nobleman of Crossout",
+ "Aussa the Earth Charmer"
+ ],
+ "LP Recovery Collection": [
+ "Mystik Wok",
+ "Poison of the Old Man",
+ "Hysteric Fairy",
+ "Dancing Fairy",
+ "Zolga",
+ "Cestus of Dagla",
+ "Nimble Momonga",
+ "Solemn Wishes",
+ "Cure Mermaid",
+ "Princess Pikeru",
+ "Kiseitai",
+ "Elemental Hero Steam Healer",
+ "Fushi No Tori",
+ "Emergency Provisions"
+ ],
+ "Special Summon Collection A": [
+ "Perfectly Ultimate Great Moth",
+ "Dark Sage",
+ "Polymerization",
+ "Ritual",
+ "Cyber-Stein",
+ "Scapegoat",
+ "Aqua Spirit",
+ "Rock Spirit",
+ "Spirit of Flames",
+ "Garuda the Wind Spirit",
+ "Shining Angel",
+ "Mystic Tomato",
+ "Giant Rat",
+ "Mother Grizzly",
+ "UFO Turtle",
+ "Flying Kamakiri 1",
+ "Giant Germ",
+ "Revival Jam",
+ "Pyramid Turtle",
+ "Troop Dragon",
+ "Gravekeeper's Spy",
+ "Pitch-Dark Dragon",
+ "Decayed Commander",
+ "Zombie Tiger",
+ "Vampire Orchis",
+ "Des Dendle",
+ "Nimble Momonga",
+ "The Last Warrior from Another Planet",
+ "Embodiment of Apophis",
+ "Cyber Jar",
+ "Morphing Jar #2",
+ "Spear Cretin",
+ "Dark Magic Curtain"
+ ],
+ "Special Summon Collection B": [
+ "Monster Gate",
+ "Chaos Emperor Dragon - Envoy of the End",
+ "Ojama Trio",
+ "Dimension Fusion",
+ "Return from the Different Dimension",
+ "Gigantes",
+ "Inferno",
+ "Silpheed",
+ "Mystic Swordsman LV2",
+ "Mystic Swordsman LV4",
+ "Skilled Dark Magician",
+ "Horus the Black Flame Dragon LV6",
+ "Armed Dragon LV3",
+ "Armed Dragon LV5",
+ "Marauding Captain",
+ "Masked Dragon",
+ "The Tricky",
+ "Magical Dimension",
+ "Frontline Base",
+ "Formation Union",
+ "Princess Pikeru",
+ "Skull Zoma",
+ "Metal Reflect Slime"
+ "Level Up!",
+ "Howling Insect",
+ "Tribute Doll",
+ "Enchanting Fitting Room",
+ "Stray Lambs"
+ ],
+ "Special Summon Collection C": [
+ "Hamon, Lord of Striking Thunder",
+ "Raviel, Lord of Phantasms",
+ "Uria, Lord of Searing Flames",
+ "Treeborn Frog",
+ "Cyber Dragon",
+ "Familiar-Possessed - Aussa",
+ "Familiar-Possessed - Eria",
+ "Familiar-Possessed - Hiita",
+ "Familiar-Possessed - Wynn",
+ "Silent Swordsman Lv3",
+ "Silent Swordsman Lv5",
+ "Warrior Lady of the Wasteland",
+ "Dandylion",
+ "Curse of Vampire",
+ "Summon Priest",
+ "Miracle Fusion",
+ "Elemental Hero Bubbleman",
+ "The Dark - Hex-Sealed Fusion",
+ "The Earth - Hex-Sealed Fusion",
+ "The Light - Hex-Sealed Fusion",
+ "Ultimate Insect Lv3",
+ "Ultimate Insect Lv5",
+ "Rescue Cat",
+ "Anteatereatingant"
+ ],
+ "Equipment Collection": [
+ "Megamorph",
+ "Cestus of Dagla",
+ "United We Stand"
+ ],
+ "Continuous Spell/Trap A": [
+ "Destiny Board",
+ "Spirit Message 'I'",
+ "Spirit Message 'N'",
+ "Spirit Message 'A'",
+ "Spirit Message 'L'",
+ "Messenger of Peace",
+ "Fairy Box",
+ "Ultimate Offering",
+ "Gravity Bind",
+ "Solemn Wishes",
+ "Embodiment of Apophis",
+ "Toon World"
+ ],
+ "Continuous Spell/Trap B": [
+ "Hamon, Lord of Striking Thunder",
+ "Uria, Lord of Searing Flames",
+ "Wave-Motion Cannon",
+ "Heart of the Underdog",
+ "Wall of Revealing Light",
+ "Dark Snake Syndrome",
+ "Call of the Mummy",
+ "Frontline Base",
+ "Level Limit - Area B",
+ "Skull Zoma",
+ "Pitch-Black Power Stone",
+ "Metal Reflect Slime"
+ ],
+ "Quick/Counter Collection": [
+ "Mystik Wok",
+ "Poison of the Old Man",
+ "Scapegoat",
+ "Magical Dimension",
+ "Enemy Controller",
+ "Collapse",
+ "Emergency Provisions",
+ "Graceful Dice",
+ "Offerings to the Doomed",
+ "Reload",
+ "Rush Recklessly",
+ "The Reliable Guardian",
+ "Cursed Seal of the Forbidden Spell",
+ "Divine Wrath",
+ "Horn of Heaven",
+ "Magic Drain",
+ "Magic Jammer",
+ "Negate Attack",
+ "Seven Tools of the Bandit",
+ "Solemn Judgment",
+ "Spell Shield Type-8",
+ "Book of Moon",
+ "Serial Spell",
+ "Mystical Space Typhoon"
+ ],
+ "Direct Damage Collection": [
+ "Hamon, Lord of Striking Thunder",
+ "Chaos Emperor Dragon - Envoy of the End",
+ "Dark Snake Syndrome",
+ "Inferno",
+ "Exarion Universe",
+ "Kycoo the Ghost Destroyer",
+ "Giant Germ",
+ "Familiar-Possessed - Aussa",
+ "Familiar-Possessed - Eria",
+ "Familiar-Possessed - Hiita",
+ "Familiar-Possessed - Wynn",
+ "Dark Driceratops",
+ "Saber Beetle",
+ "Thestalos the Firestorm Monarch",
+ "Solar Flare Dragon",
+ "Ultimate Baseball Kid",
+ "Spear Dragon",
+ "Oxygeddon",
+ "Airknight Parshath",
+ "Vampire Lord",
+ "Stamping Destruction",
+ "Decayed Commander",
+ "Jowls of Dark Demise",
+ "Stealth Bird",
+ "Elemental Hero Bladedge",
+ ],
+ "Direct Attack Collection": [
+ "Victory D.",
+ "Dark Scorpion Combination",
+ "Spirit Reaper",
+ "Elemental Hero Rampart Blaster",
+ "Toon Gemini Elf",
+ "Toon Goblin Attack Force",
+ "Toon Masked Sorcerer",
+ "Toon Mermaid",
+ "Toon Summoned Skull",
+ "Toon Dark Magician Girl"
+ ],
+ "Monster Destroy Collection": [
+ "Hamon, Lord of Striking Thunder",
+ "Inferno",
+ "Ninja Grandmaster Sasuke",
+ "Zaborg the Thunder Monarch",
+ "Mystic Swordsman LV2",
+ "Mystic Swordsman LV4",
+ "Mystic Swordsman LV6",
+ "Skull Descovery Knight",
+ "Inferno Hammer",
+ "Ryu Kokki",
+ "Newdoria",
+ "Exiled Force",
+ "Yomi Ship",
+ "Armed Dragon LV5",
+ "Element Dragon",
+ "Old Vindictive Magician",
+ "Magical Dimension",
+ "Des Dendle",
+ "Nobleman of Crossout",
+ "Shield Crash",
+ "Tribute to the Doomed",
+ "Elemental Hero Flame Wingman",
+ "Elemental Hero Shining Flare Wingman",
+ "Elemental Hero Steam Healer",
+ "Blast Magician",
+ "Magical Marionette",
+ "Swarm of Scarabs",
+ "Offerings to the Doomed",
+ "Divine Wrath",
+ "Dream Clown"
+ ],
+}
+
+
+def get_booster_locations(booster: str) -> Dict[str, str]:
+ return {
+ f"{booster} {i}": content
+ for i, content in enumerate(booster_contents[booster], 1)
+ }
diff --git a/worlds/yugioh06/client_bh.py b/worlds/yugioh06/client_bh.py
new file mode 100644
index 000000000000..ecbe48110a6c
--- /dev/null
+++ b/worlds/yugioh06/client_bh.py
@@ -0,0 +1,139 @@
+import math
+from typing import TYPE_CHECKING, List, Optional, Set
+
+from NetUtils import ClientStatus, NetworkItem
+
+import worlds._bizhawk as bizhawk
+from worlds._bizhawk.client import BizHawkClient
+from . import item_to_index
+
+if TYPE_CHECKING:
+ from worlds._bizhawk.context import BizHawkClientContext
+
+
+class YuGiOh2006Client(BizHawkClient):
+ game = "Yu-Gi-Oh! 2006"
+ system = "GBA"
+ patch_suffix = ".apygo06"
+ local_checked_locations: Set[int]
+ goal_flag: int
+ rom_slot_name: Optional[str]
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.local_checked_locations = set()
+ self.rom_slot_name = None
+
+ async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
+ from CommonClient import logger
+
+ try:
+ # Check if ROM is some version of Yu-Gi-Oh! 2006
+ game_name = ((await bizhawk.read(ctx.bizhawk_ctx, [(0xA0, 11, "ROM")]))[0]).decode("ascii")
+ if game_name != "YUGIOHWCT06":
+ return False
+
+ # Check if we can read the slot name. Doing this here instead of set_auth as a protection against
+ # validating a ROM where there's no slot name to read.
+ try:
+ slot_name_bytes = (await bizhawk.read(ctx.bizhawk_ctx, [(0x30, 32, "ROM")]))[0]
+ self.rom_slot_name = bytes([byte for byte in slot_name_bytes if byte != 0]).decode("utf-8")
+ except UnicodeDecodeError:
+ logger.info("Could not read slot name from ROM. Are you sure this ROM matches this client version?")
+ return False
+ except UnicodeDecodeError:
+ return False
+ except bizhawk.RequestFailedError:
+ return False # Should verify on the next pass
+
+ ctx.game = self.game
+ ctx.items_handling = 0b001
+ ctx.want_slot_data = False
+ return True
+
+ async def set_auth(self, ctx: "BizHawkClientContext") -> None:
+ ctx.auth = self.rom_slot_name
+
+ async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
+ try:
+ read_state = await bizhawk.read(
+ ctx.bizhawk_ctx,
+ [
+ (0x0, 8, "EWRAM"),
+ (0x52E8, 32, "EWRAM"),
+ (0x5308, 32, "EWRAM"),
+ (0x5325, 1, "EWRAM"),
+ (0x6C38, 4, "EWRAM"),
+ ],
+ )
+ game_state = read_state[0].decode("utf-8")
+ locations = read_state[1]
+ items = read_state[2]
+ amount_items = int.from_bytes(read_state[3], "little")
+ money = int.from_bytes(read_state[4], "little")
+
+ # make sure save was created
+ if game_state != "YWCT2006":
+ return
+ local_items = bytearray(items)
+ await bizhawk.guarded_write(
+ ctx.bizhawk_ctx,
+ [(0x5308, parse_items(bytearray(items), ctx.items_received), "EWRAM")],
+ [(0x5308, local_items, "EWRAM")],
+ )
+ money_received = 0
+ for item in ctx.items_received:
+ if item.item == item_to_index["5000DP"] + 5730000:
+ money_received += 1
+ if money_received > amount_items:
+ await bizhawk.guarded_write(
+ ctx.bizhawk_ctx,
+ [
+ (0x6C38, (money + (money_received - amount_items) * 5000).to_bytes(4, "little"), "EWRAM"),
+ (0x5325, money_received.to_bytes(2, "little"), "EWRAM"),
+ ],
+ [
+ (0x6C38, money.to_bytes(4, "little"), "EWRAM"),
+ (0x5325, amount_items.to_bytes(2, "little"), "EWRAM"),
+ ],
+ )
+
+ locs_to_send = set()
+
+ # Check for set location flags.
+ for byte_i, byte in enumerate(bytearray(locations)):
+ for i in range(8):
+ and_value = 1 << i
+ if byte & and_value != 0:
+ flag_id = byte_i * 8 + i
+
+ location_id = flag_id + 5730001
+ if location_id in ctx.server_locations:
+ locs_to_send.add(location_id)
+
+ # Send locations if there are any to send.
+ if locs_to_send != self.local_checked_locations:
+ self.local_checked_locations = locs_to_send
+
+ if locs_to_send is not None:
+ await ctx.send_msgs([{"cmd": "LocationChecks", "locations": list(locs_to_send)}])
+
+ # Send game clear if we're in either any ending cutscene or the credits state.
+ if not ctx.finished_game and locations[18] & (1 << 5) != 0:
+ await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
+
+ except bizhawk.RequestFailedError:
+ # Exit handler and return to main loop to reconnect.
+ pass
+
+
+# Parses bit-map for local items and adds the received items to that bit-map
+def parse_items(local_items: bytearray, items: List[NetworkItem]) -> bytearray:
+ array = local_items
+ for item in items:
+ index = item.item - 5730001
+ if index != 254:
+ byte = math.floor(index / 8)
+ bit = index % 8
+ array[byte] = array[byte] | (1 << bit)
+ return array
diff --git a/worlds/yugioh06/docs/en_Yu-Gi-Oh! 2006.md b/worlds/yugioh06/docs/en_Yu-Gi-Oh! 2006.md
new file mode 100644
index 000000000000..ee8c95a3b193
--- /dev/null
+++ b/worlds/yugioh06/docs/en_Yu-Gi-Oh! 2006.md
@@ -0,0 +1,53 @@
+# Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains all the options you need to configure and
+export a config file.
+
+## What does randomization do to this game?
+
+Unlocking Booster Packs, Campaign, Limited and Theme Duel Opponents has been changed.
+You only need to beat each Campaign Opponent once.
+Logic expects you to have access to the Booster Packs necessary to get the locations at a reasonable pace and consistency.
+Logic remains, so the game is always able to be completed, but because of the shuffle, the player may need to defeat certain opponents before they
+would in the vanilla game.
+
+You can change how much money you receive and how much booster packs cost.
+
+## What is the goal of Yu-Gi-Oh! 2006 when randomized?
+
+Defeat a certain amount of Limited/Theme Duels to Unlock the final Campaign Opponent and beat it.
+
+## What items and locations get shuffled?
+
+Locations in which items can be found:
+- Getting a Duel Bonus for the first time
+- Beating a certain amount campaign opponents of the same level.
+- Beating a Limited/Theme Duel
+- Obtaining certain cards (same that unlock a theme duel in vanilla)
+
+Items that are shuffled:
+- Unlocking Booster Packs (the "ALL" Booster Packs are excluded)
+- Unlocking Campaign Opponents
+- Unlocking Limited/Theme Duels
+- Banlists
+
+## What items are _not_ randomized?
+Certain Key Items are kept in their original locations:
+- Duel Puzzles
+- Survival Mode
+- Booster Pack Contents
+
+## Which items can be in another player's world?
+
+Any shuffled item can be in other players' worlds.
+
+
+## What does another world's item look like in Yu-Gi-Oh! 2006?
+
+You can only tell when and what you got via the client.
+
+## When the player receives an item, what happens?
+
+The Opponent/Pack becomes available to you.
diff --git a/worlds/yugioh06/docs/setup_en.md b/worlds/yugioh06/docs/setup_en.md
new file mode 100644
index 000000000000..1beeaa6c625e
--- /dev/null
+++ b/worlds/yugioh06/docs/setup_en.md
@@ -0,0 +1,72 @@
+# Setup Guide for Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 Archipelago
+
+## Important
+
+As we are using Bizhawk, this guide is only applicable to Windows and Linux systems.
+
+## Required Software
+
+- Bizhawk: [Bizhawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
+ - Version 2.7.0 and later are supported.
+ - 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)
+- A US or European Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 Rom
+
+## Configuring Bizhawk
+
+Once Bizhawk has been installed, open Bizhawk and change the following settings:
+
+- Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
+ "Lua+LuaInterface". 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 Bizhawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load**
+ **"NLua+KopiLua" until this step is done.**
+- Under Config > Customize > Advanced, make sure the box for AutoSaveRAM is checked, and click the 5s button.
+ This reduces the possibility of losing save data in emulator crashes.
+- Under Config > Customize, check the "Run in background" and "Accept background input" boxes. This will allow you to
+ continue playing in the background, even if another window is selected, such as the Client.
+- Under Config > Hotkeys, many hotkeys are listed, with many bound to common keys on the keyboard. You will likely want
+ to disable most of these, which you can do quickly using `Esc`.
+
+It is strongly recommended to associate GBA rom extensions (\*.gba) to the Bizhawk we've just installed.
+To do so, we simply have to search any GBA rom we happened to own, right click and select "Open with...", unfold
+the list that appears and select the bottom option "Look for another application", then browse to the Bizhawk folder
+and select EmuHawk.exe.
+
+## Configuring your YAML file
+
+### What is a YAML file and why do I need one?
+
+Your YAML file contains a set of configuration options which provide the generator with information about how it should
+generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
+an experience customized for their taste, and different players in the same multiworld can all have different options.
+
+### Where do I get a YAML file?
+
+You can customize your options by visiting the
+[Yu-Gi-Oh! 2006 Player Options Page](/games/Yu-Gi-Oh!%202006/player-options)
+
+## Joining a MultiWorld Game
+
+### Obtain your GBA patch file
+
+When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
+the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
+files. Your data file should have a `.apygo06` extension.
+
+Double-click on your `.apygo06` file to start your client and start the ROM patch process. Once the process is finished
+(this can take a while), the client and the emulator will be started automatically (if you associated the extension
+to the emulator as recommended).
+
+### Connect to the Multiserver
+
+Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools"
+menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script.
+
+Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
+
+To connect the client to the multiserver simply put `:` on the textfield on top and press enter (if the
+server uses password, type in the bottom textfield `/connect : [password]`)
+
+Don't forget to start manipulating RNG early by shouting "Heart of the Cards!" during generation.
\ No newline at end of file
diff --git a/worlds/yugioh06/fusions.py b/worlds/yugioh06/fusions.py
new file mode 100644
index 000000000000..22d03b389fe8
--- /dev/null
+++ b/worlds/yugioh06/fusions.py
@@ -0,0 +1,72 @@
+from typing import List, NamedTuple
+
+
+class FusionData(NamedTuple):
+ name: str
+ materials: List[str]
+ replaceable: bool
+ additional_spells: List[str]
+
+
+fusions = {
+ "Elemental Hero Flame Wingman": FusionData(
+ "Elemental Hero Flame Wingman",
+ ["Elemental Hero Avian", "Elemental Hero Burstinatrix"],
+ True,
+ ["Miracle Fusion"]),
+ "Elemental Hero Madballman": FusionData(
+ "Elemental Hero Madballman",
+ ["Elemental Hero Bubbleman", "Elemental Hero Clayman"],
+ True,
+ ["Miracle Fusion"]),
+ "Elemental Hero Rampart Blaster": FusionData(
+ "Elemental Hero Rampart Blaster",
+ ["Elemental Hero Burstinatrix", "Elemental Hero Clayman"],
+ True,
+ ["Miracle Fusion"]),
+ "Elemental Hero Shining Flare Wingman": FusionData(
+ "Elemental Hero Shining Flare Wingman",
+ ["Elemental Hero Flame Wingman", "Elemental Hero Sparkman"],
+ True,
+ ["Miracle Fusion"]),
+ "Elemental Hero Steam Healer": FusionData(
+ "Elemental Hero Steam Healer",
+ ["Elemental Hero Burstinatrix", "Elemental Hero Bubbleman"],
+ True,
+ ["Miracle Fusion"]),
+ "Elemental Hero Wildedge": FusionData(
+ "Elemental Hero Wildedge",
+ ["Elemental Hero Wildheart", "Elemental Hero Bladedge"],
+ True,
+ ["Miracle Fusion"])
+}
+
+fusion_subs = ["The Dark - Hex-Sealed Fusion",
+ "The Earth - Hex-Sealed Fusion",
+ "The Light - Hex-Sealed Fusion",
+ "Goddess with the Third Eye",
+ "King of the Swamp",
+ "Versago the Destroyer",
+ # Only in All-packs
+ "Beastking of the Swamps",
+ "Mystical Sheep #1"]
+
+
+def has_all_materials(state, monster, player):
+ data = fusions.get(monster)
+ if not state.has(monster, player):
+ return False
+ if data is None:
+ return True
+ else:
+ materials = data.replaceable and state.has_any(fusion_subs, player)
+ for material in data.materials:
+ materials += has_all_materials(state, material, player)
+ return materials >= len(data.materials)
+
+
+def count_has_materials(state, monsters, player):
+ amount = 0
+ for monster in monsters:
+ amount += has_all_materials(state, monster, player)
+ return amount
diff --git a/worlds/yugioh06/items.py b/worlds/yugioh06/items.py
new file mode 100644
index 000000000000..f0f877fd9f7b
--- /dev/null
+++ b/worlds/yugioh06/items.py
@@ -0,0 +1,369 @@
+from typing import Dict, List
+
+item_to_index: Dict[str, int] = {
+ "LEGEND OF B.E.W.D.": 1,
+ "METAL RAIDERS": 2,
+ "PHARAOH'S SERVANT": 3,
+ "PHARAONIC GUARDIAN": 4,
+ "SPELL RULER": 5,
+ "LABYRINTH OF NIGHTMARE": 6,
+ "LEGACY OF DARKNESS": 7,
+ "MAGICIAN'S FORCE": 8,
+ "DARK CRISIS": 9,
+ "INVASION OF CHAOS": 10,
+ "ANCIENT SANCTUARY": 11,
+ "SOUL OF THE DUELIST": 12,
+ "RISE OF DESTINY": 13,
+ "FLAMING ETERNITY": 14,
+ "THE LOST MILLENIUM": 15,
+ "CYBERNETIC REVOLUTION": 16,
+ "ELEMENTAL ENERGY": 17,
+ "SHADOW OF INFINITY": 18,
+ "GAME GIFT COLLECTION": 19,
+ "Special Gift Collection": 20,
+ "Fairy Collection": 21,
+ "Dragon Collection": 22,
+ "Warrior Collection A": 23,
+ "Warrior Collection B": 24,
+ "Fiend Collection A": 25,
+ "Fiend Collection B": 26,
+ "Machine Collection A": 27,
+ "Machine Collection B": 28,
+ "Spellcaster Collection A": 29,
+ "Spellcaster Collection B": 30,
+ "Zombie Collection": 31,
+ "Special Monsters A": 32,
+ "Special Monsters B": 33,
+ "Reverse Collection": 34,
+ "LP Recovery Collection": 35,
+ "Special Summon Collection A": 36,
+ "Special Summon Collection B": 37,
+ "Special Summon Collection C": 38,
+ "Equipment Collection": 39,
+ "Continuous Spell/Trap A": 40,
+ "Continuous Spell/Trap B": 41,
+ "Quick/Counter Collection": 42,
+ "Direct Damage Collection": 43,
+ "Direct Attack Collection": 44,
+ "Monster Destroy Collection": 45,
+ "All Normal Monsters": 46,
+ "All Effect Monsters": 47,
+ "All Fusion Monsters": 48,
+ "All Traps": 49,
+ "All Spells": 50,
+ "All at Random": 51,
+ "LD01 All except Level 4 forbidden Unlock": 52,
+ "LD02 Medium/high Level forbidden Unlock": 53,
+ "LD03 ATK 1500 or more forbidden Unlock": 54,
+ "LD04 Flip Effects forbidden Unlock": 55,
+ "LD05 Tributes forbidden Unlock": 56,
+ "LD06 Traps forbidden Unlock": 57,
+ "LD07 Large Deck A Unlock": 58,
+ "LD08 Large Deck B Unlock": 59,
+ "LD09 Sets Forbidden Unlock": 60,
+ "LD10 All except LV monsters forbidden Unlock": 61,
+ "LD11 All except Fairies forbidden Unlock": 62,
+ "LD12 All except Wind forbidden Unlock": 63,
+ "LD13 All except monsters forbidden Unlock": 64,
+ "LD14 Level 3 or below forbidden Unlock": 65,
+ "LD15 DEF 1500 or less forbidden Unlock": 66,
+ "LD16 Effect Monsters forbidden Unlock": 67,
+ "LD17 Spells forbidden Unlock": 68,
+ "LD18 Attacks forbidden Unlock": 69,
+ "LD19 All except E-Hero's forbidden Unlock": 70,
+ "LD20 All except Warriors forbidden Unlock": 71,
+ "LD21 All except Dark forbidden Unlock": 72,
+ "LD22 All limited cards forbidden Unlock": 73,
+ "LD23 Refer to Mar 05 Banlist Unlock": 74,
+ "LD24 Refer to Sept 04 Banlist Unlock": 75,
+ "LD25 Low Life Points Unlock": 76,
+ "LD26 All except Toons forbidden Unlock": 77,
+ "LD27 All except Spirits forbidden Unlock": 78,
+ "LD28 All except Dragons forbidden Unlock": 79,
+ "LD29 All except Spellcasters forbidden Unlock": 80,
+ "LD30 All except Light forbidden Unlock": 81,
+ "LD31 All except Fire forbidden Unlock": 82,
+ "LD32 Decks with multiples forbidden Unlock": 83,
+ "LD33 Special Summons forbidden Unlock": 84,
+ "LD34 Normal Summons forbidden Unlock": 85,
+ "LD35 All except Zombies forbidden Unlock": 86,
+ "LD36 All except Earth forbidden Unlock": 87,
+ "LD37 All except Water forbidden Unlock": 88,
+ "LD38 Refer to Mar 04 Banlist Unlock": 89,
+ "LD39 Monsters forbidden Unlock": 90,
+ "LD40 Refer to Sept 05 Banlist Unlock": 91,
+ "LD41 Refer to Sept 03 Banlist Unlock": 92,
+ "TD01 Battle Damage Unlock": 93,
+ "TD02 Deflected Damage Unlock": 94,
+ "TD03 Normal Summon Unlock": 95,
+ "TD04 Ritual Summon Unlock": 96,
+ "TD05 Special Summon A Unlock": 97,
+ "TD06 20x Spell Unlock": 98,
+ "TD07 10x Trap Unlock": 99,
+ "TD08 Draw Unlock": 100,
+ "TD09 Hand Destruction Unlock": 101,
+ "TD10 During Opponent's Turn Unlock": 102,
+ "TD11 Recover Unlock": 103,
+ "TD12 Remove Monsters by Effect Unlock": 104,
+ "TD13 Flip Summon Unlock": 105,
+ "TD14 Special Summon B Unlock": 106,
+ "TD15 Token Unlock": 107,
+ "TD16 Union Unlock": 108,
+ "TD17 10x Quick Spell Unlock": 109,
+ "TD18 The Forbidden Unlock": 110,
+ "TD19 20 Turns Unlock": 111,
+ "TD20 Deck Destruction Unlock": 112,
+ "TD21 Victory D. Unlock": 113,
+ "TD22 The Preventers Fight Back Unlock": 114,
+ "TD23 Huge Revolution Unlock": 115,
+ "TD24 Victory in 5 Turns Unlock": 116,
+ "TD25 Moth Grows Up Unlock": 117,
+ "TD26 Magnetic Power Unlock": 118,
+ "TD27 Dark Sage Unlock": 119,
+ "TD28 Direct Damage Unlock": 120,
+ "TD29 Destroy Monsters in Battle Unlock": 121,
+ "TD30 Tribute Summon Unlock": 122,
+ "TD31 Special Summon C Unlock": 123,
+ "TD32 Toon Unlock": 124,
+ "TD33 10x Counter Unlock": 125,
+ "TD34 Destiny Board Unlock": 126,
+ "TD35 Huge Damage in a Turn Unlock": 127,
+ "TD36 V-Z In the House Unlock": 128,
+ "TD37 Uria, Lord of Searing Flames Unlock": 129,
+ "TD38 Hamon, Lord of Striking Thunder Unlock": 130,
+ "TD39 Raviel, Lord of Phantasms Unlock": 131,
+ "TD40 Make a Chain Unlock": 132,
+ "TD41 The Gatekeeper Stands Tall Unlock": 133,
+ "TD42 Serious Damage Unlock": 134,
+ "TD43 Return Monsters with Effects Unlock": 135,
+ "TD44 Fusion Summon Unlock": 136,
+ "TD45 Big Damage at once Unlock": 137,
+ "TD46 XYZ In the House Unlock": 138,
+ "TD47 Spell Counter Unlock": 139,
+ "TD48 Destroy Monsters with Effects Unlock": 140,
+ "TD49 Plunder Unlock": 141,
+ "TD50 Dark Scorpion Combination Unlock": 142,
+ "Campaign Tier 1 Column 1": 143,
+ "Campaign Tier 1 Column 2": 144,
+ "Campaign Tier 1 Column 3": 145,
+ "Campaign Tier 1 Column 4": 146,
+ "Campaign Tier 1 Column 5": 147,
+ "Campaign Tier 2 Column 1": 148,
+ "Campaign Tier 2 Column 2": 149,
+ "Campaign Tier 2 Column 3": 150,
+ "Campaign Tier 2 Column 4": 151,
+ "Campaign Tier 2 Column 5": 152,
+ "Campaign Tier 3 Column 1": 153,
+ "Campaign Tier 3 Column 2": 154,
+ "Campaign Tier 3 Column 3": 155,
+ "Campaign Tier 3 Column 4": 156,
+ "Campaign Tier 3 Column 5": 157,
+ "Campaign Tier 4 Column 1": 158,
+ "Campaign Tier 4 Column 2": 159,
+ "Campaign Tier 4 Column 3": 160,
+ "Campaign Tier 4 Column 4": 161,
+ "Campaign Tier 4 Column 5": 162,
+ "Campaign Tier 5 Column 1": 163,
+ "Campaign Tier 5 Column 2": 164,
+ "No Banlist": 167,
+ "Banlist September 2003": 168,
+ "Banlist March 2004": 169,
+ "Banlist September 2004": 170,
+ "Banlist March 2005": 171,
+ "Banlist September 2005": 172,
+ "5000DP": 254,
+ "Remote": 255,
+}
+
+tier_1_opponents: List[str] = [
+ "Campaign Tier 1 Column 1",
+ "Campaign Tier 1 Column 2",
+ "Campaign Tier 1 Column 3",
+ "Campaign Tier 1 Column 4",
+ "Campaign Tier 1 Column 5",
+]
+
+Banlist_Items: List[str] = [
+ "No Banlist",
+ "Banlist September 2003",
+ "Banlist March 2004",
+ "Banlist September 2004",
+ "Banlist March 2005",
+ "Banlist September 2005",
+]
+
+draft_boosters: List[str] = [
+ "METAL RAIDERS",
+ "PHARAOH'S SERVANT",
+ "PHARAONIC GUARDIAN",
+ "LABYRINTH OF NIGHTMARE",
+ "LEGACY OF DARKNESS",
+ "MAGICIAN'S FORCE",
+ "DARK CRISIS",
+ "INVASION OF CHAOS",
+ "RISE OF DESTINY",
+ "ELEMENTAL ENERGY",
+ "SHADOW OF INFINITY",
+]
+
+draft_opponents: List[str] = ["Campaign Tier 1 Column 1", "Campaign Tier 1 Column 5"]
+
+booster_packs: List[str] = [
+ "LEGEND OF B.E.W.D.",
+ "METAL RAIDERS",
+ "PHARAOH'S SERVANT",
+ "PHARAONIC GUARDIAN",
+ "SPELL RULER",
+ "LABYRINTH OF NIGHTMARE",
+ "LEGACY OF DARKNESS",
+ "MAGICIAN'S FORCE",
+ "DARK CRISIS",
+ "INVASION OF CHAOS",
+ "ANCIENT SANCTUARY",
+ "SOUL OF THE DUELIST",
+ "RISE OF DESTINY",
+ "FLAMING ETERNITY",
+ "THE LOST MILLENIUM",
+ "CYBERNETIC REVOLUTION",
+ "ELEMENTAL ENERGY",
+ "SHADOW OF INFINITY",
+ "GAME GIFT COLLECTION",
+ "Special Gift Collection",
+ "Fairy Collection",
+ "Dragon Collection",
+ "Warrior Collection A",
+ "Warrior Collection B",
+ "Fiend Collection A",
+ "Fiend Collection B",
+ "Machine Collection A",
+ "Machine Collection B",
+ "Spellcaster Collection A",
+ "Spellcaster Collection B",
+ "Zombie Collection",
+ "Special Monsters A",
+ "Special Monsters B",
+ "Reverse Collection",
+ "LP Recovery Collection",
+ "Special Summon Collection A",
+ "Special Summon Collection B",
+ "Special Summon Collection C",
+ "Equipment Collection",
+ "Continuous Spell/Trap A",
+ "Continuous Spell/Trap B",
+ "Quick/Counter Collection",
+ "Direct Damage Collection",
+ "Direct Attack Collection",
+ "Monster Destroy Collection",
+]
+
+challenges: List[str] = [
+ "LD01 All except Level 4 forbidden Unlock",
+ "LD02 Medium/high Level forbidden Unlock",
+ "LD03 ATK 1500 or more forbidden Unlock",
+ "LD04 Flip Effects forbidden Unlock",
+ "LD05 Tributes forbidden Unlock",
+ "LD06 Traps forbidden Unlock",
+ "LD07 Large Deck A Unlock",
+ "LD08 Large Deck B Unlock",
+ "LD09 Sets Forbidden Unlock",
+ "LD10 All except LV monsters forbidden Unlock",
+ "LD11 All except Fairies forbidden Unlock",
+ "LD12 All except Wind forbidden Unlock",
+ "LD13 All except monsters forbidden Unlock",
+ "LD14 Level 3 or below forbidden Unlock",
+ "LD15 DEF 1500 or less forbidden Unlock",
+ "LD16 Effect Monsters forbidden Unlock",
+ "LD17 Spells forbidden Unlock",
+ "LD18 Attacks forbidden Unlock",
+ "LD19 All except E-Hero's forbidden Unlock",
+ "LD20 All except Warriors forbidden Unlock",
+ "LD21 All except Dark forbidden Unlock",
+ "LD22 All limited cards forbidden Unlock",
+ "LD23 Refer to Mar 05 Banlist Unlock",
+ "LD24 Refer to Sept 04 Banlist Unlock",
+ "LD25 Low Life Points Unlock",
+ "LD26 All except Toons forbidden Unlock",
+ "LD27 All except Spirits forbidden Unlock",
+ "LD28 All except Dragons forbidden Unlock",
+ "LD29 All except Spellcasters forbidden Unlock",
+ "LD30 All except Light forbidden Unlock",
+ "LD31 All except Fire forbidden Unlock",
+ "LD32 Decks with multiples forbidden Unlock",
+ "LD33 Special Summons forbidden Unlock",
+ "LD34 Normal Summons forbidden Unlock",
+ "LD35 All except Zombies forbidden Unlock",
+ "LD36 All except Earth forbidden Unlock",
+ "LD37 All except Water forbidden Unlock",
+ "LD38 Refer to Mar 04 Banlist Unlock",
+ "LD39 Monsters forbidden Unlock",
+ "LD40 Refer to Sept 05 Banlist Unlock",
+ "LD41 Refer to Sept 03 Banlist Unlock",
+ "TD01 Battle Damage Unlock",
+ "TD02 Deflected Damage Unlock",
+ "TD03 Normal Summon Unlock",
+ "TD04 Ritual Summon Unlock",
+ "TD05 Special Summon A Unlock",
+ "TD06 20x Spell Unlock",
+ "TD07 10x Trap Unlock",
+ "TD08 Draw Unlock",
+ "TD09 Hand Destruction Unlock",
+ "TD10 During Opponent's Turn Unlock",
+ "TD11 Recover Unlock",
+ "TD12 Remove Monsters by Effect Unlock",
+ "TD13 Flip Summon Unlock",
+ "TD14 Special Summon B Unlock",
+ "TD15 Token Unlock",
+ "TD16 Union Unlock",
+ "TD17 10x Quick Spell Unlock",
+ "TD18 The Forbidden Unlock",
+ "TD19 20 Turns Unlock",
+ "TD20 Deck Destruction Unlock",
+ "TD21 Victory D. Unlock",
+ "TD22 The Preventers Fight Back Unlock",
+ "TD23 Huge Revolution Unlock",
+ "TD24 Victory in 5 Turns Unlock",
+ "TD25 Moth Grows Up Unlock",
+ "TD26 Magnetic Power Unlock",
+ "TD27 Dark Sage Unlock",
+ "TD28 Direct Damage Unlock",
+ "TD29 Destroy Monsters in Battle Unlock",
+ "TD30 Tribute Summon Unlock",
+ "TD31 Special Summon C Unlock",
+ "TD32 Toon Unlock",
+ "TD33 10x Counter Unlock",
+ "TD34 Destiny Board Unlock",
+ "TD35 Huge Damage in a Turn Unlock",
+ "TD36 V-Z In the House Unlock",
+ "TD37 Uria, Lord of Searing Flames Unlock",
+ "TD38 Hamon, Lord of Striking Thunder Unlock",
+ "TD39 Raviel, Lord of Phantasms Unlock",
+ "TD40 Make a Chain Unlock",
+ "TD41 The Gatekeeper Stands Tall Unlock",
+ "TD42 Serious Damage Unlock",
+ "TD43 Return Monsters with Effects Unlock",
+ "TD44 Fusion Summon Unlock",
+ "TD45 Big Damage at once Unlock",
+ "TD46 XYZ In the House Unlock",
+ "TD47 Spell Counter Unlock",
+ "TD48 Destroy Monsters with Effects Unlock",
+ "TD49 Plunder Unlock",
+ "TD50 Dark Scorpion Combination Unlock",
+]
+
+excluded_items: List[str] = [
+ "All Normal Monsters",
+ "All Effect Monsters",
+ "All Fusion Monsters",
+ "All Traps",
+ "All Spells",
+ "All at Random",
+ "5000DP",
+ "Remote",
+]
+
+useful: List[str] = [
+ "Banlist March 2004",
+ "Banlist September 2004",
+ "Banlist March 2005",
+ "Banlist September 2005",
+]
diff --git a/worlds/yugioh06/locations.py b/worlds/yugioh06/locations.py
new file mode 100644
index 000000000000..f495bfede22d
--- /dev/null
+++ b/worlds/yugioh06/locations.py
@@ -0,0 +1,213 @@
+Bonuses = {
+ "Duelist Bonus Level 1": 1,
+ "Duelist Bonus Level 2": 2,
+ "Duelist Bonus Level 3": 3,
+ "Duelist Bonus Level 4": 4,
+ "Duelist Bonus Level 5": 5,
+ "Battle Damage": 6,
+ "Battle Damage Only Bonus": 7,
+ "Max ATK Bonus": 8,
+ "Max Damage Bonus": 9,
+ "Destroyed in Battle Bonus": 10,
+ "Spell Card Bonus": 11,
+ "Trap Card Bonus": 12,
+ "Tribute Summon Bonus": 13,
+ "Fusion Summon Bonus": 14,
+ "Ritual Summon Bonus": 15,
+ "No Special Summon Bonus": 16,
+ "No Spell Cards Bonus": 17,
+ "No Trap Cards Bonus": 18,
+ "No Damage Bonus": 19,
+ "Over 20000 LP Bonus": 20,
+ "Low LP Bonus": 21,
+ "Extremely Low LP Bonus": 22,
+ "Low Deck Bonus": 23,
+ "Extremely Low Deck Bonus": 24,
+ "Effect Damage Only Bonus": 25,
+ "No More Cards Bonus": 26,
+ "Opponent's Turn Finish Bonus": 27,
+ "Exactly 0 LP Bonus": 28,
+ "Reversal Finish Bonus": 29,
+ "Quick Finish Bonus": 30,
+ "Exodia Finish Bonus": 31,
+ "Last Turn Finish Bonus": 32,
+ "Final Countdown Finish Bonus": 33,
+ "Destiny Board Finish Bonus": 34,
+ "Yata-Garasu Finish Bonus": 35,
+ "Skull Servant Finish Bonus": 36,
+ "Konami Bonus": 37,
+}
+
+Limited_Duels = {
+ "LD01 All except Level 4 forbidden": 38,
+ "LD02 Medium/high Level forbidden": 39,
+ "LD03 ATK 1500 or more forbidden": 40,
+ "LD04 Flip Effects forbidden": 41,
+ "LD05 Tributes forbidden": 42,
+ "LD06 Traps forbidden": 43,
+ "LD07 Large Deck A": 44,
+ "LD08 Large Deck B": 45,
+ "LD09 Sets Forbidden": 46,
+ "LD10 All except LV monsters forbidden": 47,
+ "LD11 All except Fairies forbidden": 48,
+ "LD12 All except Wind forbidden": 49,
+ "LD13 All except monsters forbidden": 50,
+ "LD14 Level 3 or below forbidden": 51,
+ "LD15 DEF 1500 or less forbidden": 52,
+ "LD16 Effect Monsters forbidden": 53,
+ "LD17 Spells forbidden": 54,
+ "LD18 Attacks forbidden": 55,
+ "LD19 All except E-Hero's forbidden": 56,
+ "LD20 All except Warriors forbidden": 57,
+ "LD21 All except Dark forbidden": 58,
+ "LD22 All limited cards forbidden": 59,
+ "LD23 Refer to Mar 05 Banlist": 60,
+ "LD24 Refer to Sept 04 Banlist": 61,
+ "LD25 Low Life Points": 62,
+ "LD26 All except Toons forbidden": 63,
+ "LD27 All except Spirits forbidden": 64,
+ "LD28 All except Dragons forbidden": 65,
+ "LD29 All except Spellcasters forbidden": 66,
+ "LD30 All except Light forbidden": 67,
+ "LD31 All except Fire forbidden": 68,
+ "LD32 Decks with multiples forbidden": 69,
+ "LD33 Special Summons forbidden": 70,
+ "LD34 Normal Summons forbidden": 71,
+ "LD35 All except Zombies forbidden": 72,
+ "LD36 All except Earth forbidden": 73,
+ "LD37 All except Water forbidden": 74,
+ "LD38 Refer to Mar 04 Banlist": 75,
+ "LD39 Monsters forbidden": 76,
+ "LD40 Refer to Sept 05 Banlist": 77,
+ "LD41 Refer to Sept 03 Banlist": 78,
+}
+
+Theme_Duels = {
+ "TD01 Battle Damage": 79,
+ "TD02 Deflected Damage": 80,
+ "TD03 Normal Summon": 81,
+ "TD04 Ritual Summon": 82,
+ "TD05 Special Summon A": 83,
+ "TD06 20x Spell": 84,
+ "TD07 10x Trap": 85,
+ "TD08 Draw": 86,
+ "TD09 Hand Destruction": 87,
+ "TD10 During Opponent's Turn": 88,
+ "TD11 Recover": 89,
+ "TD12 Remove Monsters by Effect": 90,
+ "TD13 Flip Summon": 91,
+ "TD14 Special Summon B": 92,
+ "TD15 Token": 93,
+ "TD16 Union": 94,
+ "TD17 10x Quick Spell": 95,
+ "TD18 The Forbidden": 96,
+ "TD19 20 Turns": 97,
+ "TD20 Deck Destruction": 98,
+ "TD21 Victory D.": 99,
+ "TD22 The Preventers Fight Back": 100,
+ "TD23 Huge Revolution": 101,
+ "TD24 Victory in 5 Turns": 102,
+ "TD25 Moth Grows Up": 103,
+ "TD26 Magnetic Power": 104,
+ "TD27 Dark Sage": 105,
+ "TD28 Direct Damage": 106,
+ "TD29 Destroy Monsters in Battle": 107,
+ "TD30 Tribute Summon": 108,
+ "TD31 Special Summon C": 109,
+ "TD32 Toon": 110,
+ "TD33 10x Counter": 111,
+ "TD34 Destiny Board": 112,
+ "TD35 Huge Damage in a Turn": 113,
+ "TD36 V-Z In the House": 114,
+ "TD37 Uria, Lord of Searing Flames": 115,
+ "TD38 Hamon, Lord of Striking Thunder": 116,
+ "TD39 Raviel, Lord of Phantasms": 117,
+ "TD40 Make a Chain": 118,
+ "TD41 The Gatekeeper Stands Tall": 119,
+ "TD42 Serious Damage": 120,
+ "TD43 Return Monsters with Effects": 121,
+ "TD44 Fusion Summon": 122,
+ "TD45 Big Damage at once": 123,
+ "TD46 XYZ In the House": 124,
+ "TD47 Spell Counter": 125,
+ "TD48 Destroy Monsters with Effects": 126,
+ "TD49 Plunder": 127,
+ "TD50 Dark Scorpion Combination": 128,
+}
+
+Campaign_Opponents = {
+ "Campaign Tier 1: 1 Win": 129,
+ "Campaign Tier 1: 3 Wins A": 130,
+ "Campaign Tier 1: 3 Wins B": 131,
+ "Campaign Tier 1: 5 Wins A": 132,
+ "Campaign Tier 1: 5 Wins B": 133,
+ "Campaign Tier 2: 1 Win": 134,
+ "Campaign Tier 2: 3 Wins A": 135,
+ "Campaign Tier 2: 3 Wins B": 136,
+ "Campaign Tier 2: 5 Wins A": 137,
+ "Campaign Tier 2: 5 Wins B": 138,
+ "Campaign Tier 3: 1 Win": 139,
+ "Campaign Tier 3: 3 Wins A": 140,
+ "Campaign Tier 3: 3 Wins B": 141,
+ "Campaign Tier 3: 5 Wins A": 142,
+ "Campaign Tier 3: 5 Wins B": 143,
+ "Campaign Tier 4: 5 Wins A": 144,
+ "Campaign Tier 4: 5 Wins B": 145,
+}
+
+special = {
+ "Campaign Tier 5: Column 1 Win": 146,
+ "Campaign Tier 5: Column 2 Win": 147,
+ "Campaign Tier 5: Column 3 Win": 148,
+ "Campaign Tier 5: Column 4 Win": 149,
+ # "Campaign Final Boss Win": 150,
+}
+
+Required_Cards = {
+ "Obtain all pieces of Exodia": 154,
+ "Obtain Final Countdown": 155,
+ "Obtain Victory Dragon": 156,
+ "Obtain Ojama Delta Hurricane and its required cards": 157,
+ "Obtain Huge Revolution and its required cards": 158,
+ "Obtain Perfectly Ultimate Great Moth and its required cards": 159,
+ "Obtain Valkyrion the Magna Warrior and its pieces": 160,
+ "Obtain Dark Sage and its required cards": 161,
+ "Obtain Destiny Board and its letters": 162,
+ "Obtain all XYZ-Dragon Cannon fusions and their materials": 163,
+ "Obtain VWXYZ-Dragon Catapult Cannon and the fusion materials": 164,
+ "Obtain Hamon, Lord of Striking Thunder": 165,
+ "Obtain Raviel, Lord of Phantasms": 166,
+ "Obtain Uria, Lord of Searing Flames": 167,
+ "Obtain Gate Guardian and its pieces": 168,
+ "Obtain Dark Scorpion Combination and its required cards": 169,
+}
+
+collection_events = {
+ "Ojama Delta Hurricane and required cards": None,
+ "Huge Revolution and its required cards": None,
+ "Perfectly Ultimate Great Moth and its required cards": None,
+ "Valkyrion the Magna Warrior and its pieces": None,
+ "Dark Sage and its required cards": None,
+ "Destiny Board and its letters": None,
+ "XYZ-Dragon Cannon fusions and their materials": None,
+ "VWXYZ-Dragon Catapult Cannon and the fusion materials": None,
+ "Gate Guardian and its pieces": None,
+ "Dark Scorpion Combination and its required cards": None,
+ "Can Exodia Win": None,
+ "Can Yata Lock": None,
+ "Can Stall with Monsters": None,
+ "Can Stall with ST": None,
+ "Can Last Turn Win": None,
+ "Has Back-row removal": None,
+}
+
+
+def get_beat_challenge_events(self):
+ beat_events = {}
+ for limited in Limited_Duels.keys():
+ if limited not in self.removed_challenges:
+ beat_events[limited + " Complete"] = None
+ for theme in Theme_Duels.keys():
+ if theme not in self.removed_challenges:
+ beat_events[theme + " Complete"] = None
+ return beat_events
diff --git a/worlds/yugioh06/logic.py b/worlds/yugioh06/logic.py
new file mode 100644
index 000000000000..3227cbfe67c3
--- /dev/null
+++ b/worlds/yugioh06/logic.py
@@ -0,0 +1,28 @@
+from typing import List
+
+from BaseClasses import CollectionState
+
+core_booster: List[str] = [
+ "LEGEND OF B.E.W.D.",
+ "METAL RAIDERS",
+ "PHARAOH'S SERVANT",
+ "PHARAONIC GUARDIAN",
+ "SPELL RULER",
+ "LABYRINTH OF NIGHTMARE",
+ "LEGACY OF DARKNESS",
+ "MAGICIAN'S FORCE",
+ "DARK CRISIS",
+ "INVASION OF CHAOS",
+ "ANCIENT SANCTUARY",
+ "SOUL OF THE DUELIST",
+ "RISE OF DESTINY",
+ "FLAMING ETERNITY",
+ "THE LOST MILLENIUM",
+ "CYBERNETIC REVOLUTION",
+ "ELEMENTAL ENERGY",
+ "SHADOW OF INFINITY",
+]
+
+
+def yugioh06_difficulty(state: CollectionState, player: int, amount: int):
+ return state.has_from_list(core_booster, player, amount)
diff --git a/worlds/yugioh06/opponents.py b/worlds/yugioh06/opponents.py
new file mode 100644
index 000000000000..68d7c2880f03
--- /dev/null
+++ b/worlds/yugioh06/opponents.py
@@ -0,0 +1,264 @@
+from typing import Dict, List, NamedTuple, Optional, Union
+
+from BaseClasses import MultiWorld
+from worlds.generic.Rules import CollectionRule
+
+from . import item_to_index, tier_1_opponents, yugioh06_difficulty
+from .locations import special
+
+
+class OpponentData(NamedTuple):
+ id: int
+ name: str
+ campaign_info: List[str]
+ tier: int
+ column: int
+ card_id: int = 0
+ deck_name_id: int = 0
+ deck_file: str = ""
+ difficulty: int = 1
+ additional_info: List[str] = []
+
+ def tier(self, tier):
+ self.tier = tier
+
+ def column(self, column):
+ self.column = column
+
+
+challenge_opponents = [
+ # Theme
+ OpponentData(27, "Exarion Universe", [], 1, 1, 5452, 13001, "deck/theme_001.ydc\x00\x00\x00\x00", 0),
+ OpponentData(28, "Stone Statue of the Aztecs", [], 4, 1, 4754, 13002, "deck/theme_002.ydc\x00\x00\x00\x00", 3),
+ OpponentData(29, "Raging Flame Sprite", [], 1, 1, 6189, 13003, "deck/theme_003.ydc\x00\x00\x00\x00", 0),
+ OpponentData(30, "Princess Pikeru", [], 1, 1, 6605, 13004, "deck/theme_004.ydc\x00\x00\x00\x00", 0),
+ OpponentData(31, "Princess Curran", ["Quick-Finish"], 1, 1, 6606, 13005, "deck/theme_005.ydc\x00\x00\x00\x00", 0,
+ ["Has Back-row removal"]),
+ OpponentData(32, "Gearfried the Iron Knight", ["Quick-Finish"], 2, 1, 5059, 13006,
+ "deck/theme_006.ydc\x00\x00\x00\x00", 1),
+ OpponentData(33, "Zaborg the Thunder Monarch", [], 3, 1, 5965, 13007, "deck/theme_007.ydc\x00\x00\x00\x00", 2),
+ OpponentData(34, "Kycoo the Ghost Destroyer", ["Quick-Finish"], 3, 1, 5248, 13008,
+ "deck/theme_008.ydc\x00\x00\x00\x00"),
+ OpponentData(35, "Penguin Soldier", ["Quick-Finish"], 1, 1, 4608, 13009, "deck/theme_009.ydc\x00\x00\x00\x00", 0),
+ OpponentData(36, "Green Gadget", [], 5, 1, 6151, 13010, "deck/theme_010.ydc\x00\x00\x00\x00", 5),
+ OpponentData(37, "Guardian Sphinx", ["Quick-Finish"], 3, 1, 5422, 13011, "deck/theme_011.ydc\x00\x00\x00\x00", 3),
+ OpponentData(38, "Cyber-Tech Alligator", [], 2, 1, 4790, 13012, "deck/theme_012.ydc\x00\x00\x00\x00", 1),
+ OpponentData(39, "UFOroid Fighter", [], 3, 1, 6395, 13013, "deck/theme_013.ydc\x00\x00\x00\x00", 2),
+ OpponentData(40, "Relinquished", [], 3, 1, 4737, 13014, "deck/theme_014.ydc\x00\x00\x00\x00", 2),
+ OpponentData(41, "Manticore of Darkness", [], 2, 1, 5881, 13015, "deck/theme_015.ydc\x00\x00\x00\x00", 1),
+ OpponentData(42, "Vampire Lord", [], 3, 1, 5410, 13016, "deck/theme_016.ydc\x00\x00\x00\x00", 2),
+ OpponentData(43, "Gigantes", ["Quick-Finish"], 3, 1, 5831, 13017, "deck/theme_017.ydc\x00\x00\x00\x00", 2),
+ OpponentData(44, "Insect Queen", ["Quick-Finish"], 2, 1, 4768, 13018, "deck/theme_018.ydc\x00\x00\x00\x00", 1),
+ OpponentData(45, "Second Goblin", ["Quick-Finish"], 1, 1, 5587, 13019, "deck/theme_019.ydc\x00\x00\x00\x00", 0),
+ OpponentData(46, "Toon Summoned Skull", [], 4, 1, 4735, 13020, "deck/theme_020.ydc\x00\x00\x00\x00", 3),
+ OpponentData(47, "Iron Blacksmith Kotetsu", [], 2, 1, 5769, 13021, "deck/theme_021.ydc\x00\x00\x00\x00", 1),
+ OpponentData(48, "Magician of Faith", [], 1, 1, 4434, 13022, "deck/theme_022.ydc\x00\x00\x00\x00", 0),
+ OpponentData(49, "Mask of Darkness", [], 1, 1, 4108, 13023, "deck/theme_023.ydc\x00\x00\x00\x00", 0),
+ OpponentData(50, "Dark Ruler Vandalgyon", [], 3, 1, 6410, 13024, "deck/theme_024.ydc\x00\x00\x00\x00", 2),
+ OpponentData(51, "Aussa the Earth Charmer", ["Quick-Finish"], 2, 1, 6335, 13025,
+ "deck/theme_025.ydc\x00\x00\x00\x00", 1),
+ OpponentData(52, "Exodia Necross", ["Quick-Finish"], 2, 1, 5701, 13026, "deck/theme_026.ydc\x00\x00\x00\x00", 1),
+ OpponentData(53, "Dark Necrofear", [], 3, 1, 5222, 13027, "deck/theme_027.ydc\x00\x00\x00\x00", 2),
+ OpponentData(54, "Demise, King of Armageddon", [], 4, 1, 6613, 13028, "deck/theme_028.ydc\x00\x00\x00\x00", 2),
+ OpponentData(55, "Yamata Dragon", [], 3, 1, 5377, 13029, "deck/theme_029.ydc\x00\x00\x00\x00", 2),
+ OpponentData(56, "Blue-Eyes Ultimate Dragon", [], 3, 1, 4386, 13030, "deck/theme_030.ydc\x00\x00\x00\x00", 2),
+ OpponentData(57, "Wave-Motion Cannon", [], 4, 1, 5614, 13031, "deck/theme_031.ydc\x00\x00\x00\x00", 3,
+ ["Has Back-row removal"]),
+ # Unused opponent
+ # OpponentData(58, "Yata-Garasu", [], 1, 1, 5375, 13032, "deck/theme_031.ydc\x00\x00\x00\x00"),
+ # Unused opponent
+ # OpponentData(59, "Makyura the Destructor", [], 1, 1, 5285, 13033, "deck/theme_031.ydc\x00\x00\x00\x00"),
+ OpponentData(60, "Morphing Jar", [], 5, 1, 4597, 13034, "deck/theme_034.ydc\x00\x00\x00\x00", 4),
+ OpponentData(61, "Spirit Reaper", [], 2, 1, 5526, 13035, "deck/theme_035.ydc\x00\x00\x00\x00", 1),
+ OpponentData(62, "Victory D.", [], 3, 1, 5868, 13036, "deck/theme_036.ydc\x00\x00\x00\x00", 2),
+ OpponentData(63, "VWXYZ-Dragon Catapult Cannon", ["Quick-Finish"], 3, 1, 6484, 13037,
+ "deck/theme_037.ydc\x00\x00\x00\x00", 2),
+ OpponentData(64, "XYZ-Dragon Cannon", [], 2, 1, 5556, 13038, "deck/theme_038.ydc\x00\x00\x00\x00", 1),
+ OpponentData(65, "Uria, Lord of Searing Flames", [], 4, 1, 6563, 13039, "deck/theme_039.ydc\x00\x00\x00\x00", 3),
+ OpponentData(66, "Hamon, Lord of Striking Thunder", [], 4, 1, 6564, 13040, "deck/theme_040.ydc\x00\x00\x00\x00", 3),
+ OpponentData(67, "Raviel, Lord of Phantasms TD", [], 4, 1, 6565, 13041, "deck/theme_041.ydc\x00\x00\x00\x00", 3),
+ OpponentData(68, "Ojama Trio", [], 1, 1, 5738, 13042, "deck/theme_042.ydc\x00\x00\x00\x00", 0),
+ OpponentData(69, "People Running About", ["Quick-Finish"], 1, 1, 5578, 13043, "deck/theme_043.ydc\x00\x00\x00\x00",
+ 0),
+ OpponentData(70, "Cyber-Stein", [], 5, 1, 4426, 13044, "deck/theme_044.ydc\x00\x00\x00\x00", 4),
+ OpponentData(71, "Winged Kuriboh LV10", [], 4, 1, 6406, 13045, "deck/theme_045.ydc\x00\x00\x00\x00", 3),
+ OpponentData(72, "Blue-Eyes Shining Dragon", [], 3, 1, 6082, 13046, "deck/theme_046.ydc\x00\x00\x00\x00", 2),
+ OpponentData(73, "Perfectly Ultimate Great Moth", ["Quick-Finish"], 3, 1, 4073, 13047,
+ "deck/theme_047.ydc\x00\x00\x00\x00", 2),
+ OpponentData(74, "Gate Guardian", [], 4, 1, 4380, 13048, "deck/theme_048.ydc\x00\x00\x00\x00", 2),
+ OpponentData(75, "Valkyrion the Magna Warrior", [], 3, 1, 5002, 13049, "deck/theme_049.ydc\x00\x00\x00\x00", 2),
+ OpponentData(76, "Dark Sage", [], 4, 1, 5230, 13050, "deck/theme_050.ydc\x00\x00\x00\x00", 3),
+ OpponentData(77, "Don Zaloog", [], 4, 1, 5426, 13051, "deck/theme_051.ydc\x00\x00\x00\x00", 3),
+ OpponentData(78, "Blast Magician", ["Quick-Finish"], 2, 1, 6250, 13052, "deck/theme_052.ydc\x00\x00\x00\x00", 1),
+ # Limited
+ OpponentData(79, "Zombyra the Dark", [], 5, 1, 5245, 23000, "deck/limit_000.ydc\x00\x00\x00\x00", 5),
+ OpponentData(80, "Goblin Attack Force", [], 4, 1, 5145, 23001, "deck/limit_001.ydc\x00\x00\x00\x00", 3),
+ OpponentData(81, "Giant Kozaky", [], 4, 1, 6420, 23002, "deck/limit_002.ydc\x00\x00\x00\x00", 4),
+ OpponentData(82, "Big Shield Gardna", ["Quick-Finish"], 2, 1, 4764, 23003, "deck/limit_003.ydc\x00\x00\x00\x00", 1),
+ OpponentData(83, "Panther Warrior", [], 3, 1, 4751, 23004, "deck/limit_004.ydc\x00\x00\x00\x00", 2),
+ OpponentData(84, "Silent Magician LV4", ["Quick-Finish"], 2, 1, 6167, 23005, "deck/limit_005.ydc\x00\x00\x00\x00",
+ 1),
+ OpponentData(85, "Summoned Skull", [], 4, 1, 4028, 23006, "deck/limit_006.ydc\x00\x00\x00\x00", 3),
+ OpponentData(86, "Ancient Gear Golem", [], 5, 1, 6315, 23007, "deck/limit_007.ydc\x00\x00\x00\x00", 5),
+ OpponentData(87, "Chaos Sorcerer", [], 5, 1, 5833, 23008, "deck/limit_008.ydc\x00\x00\x00\x00", 5),
+ OpponentData(88, "Breaker the Magical Warrior", [], 5, 1, 5655, 23009, "deck/limit_009.ydc\x00\x00\x00\x00", 4),
+ OpponentData(89, "Dark Magician of Chaos", [], 4, 1, 5880, 23010, "deck/limit_010.ydc\x00\x00\x00\x00", 3),
+ OpponentData(90, "Stealth Bird", ["Quick-Finish"], 2, 1, 5882, 23011, "deck/limit_011.ydc\x00\x00\x00\x00", 1),
+ OpponentData(91, "Rapid-Fire Magician", ["Quick-Finish"], 2, 1, 6500, 23012, "deck/limit_012.ydc\x00\x00\x00\x00",
+ 1),
+ OpponentData(92, "Morphing Jar #2", [], 5, 1, 4969, 23013, "deck/limit_013.ydc\x00\x00\x00\x00", 4),
+ OpponentData(93, "Cyber Jar", [], 5, 1, 4913, 23014, "deck/limit_014.ydc\x00\x00\x00\x00", 4),
+ # Unused/Broken
+ # OpponentData(94, "Exodia the Forbidden One", [], 1, 1, 4027, 23015, "deck/limit_015.ydc\x00\x00\x00\x00"),
+ OpponentData(94, "Dark Paladin", [], 4, 1, 5628, 23016, "deck/limit_016.ydc\x00\x00\x00\x00", 3),
+ OpponentData(95, "F.G.D.", [], 5, 1, 5502, 23017, "deck/limit_017.ydc\x00\x00\x00\x00", 4),
+ OpponentData(96, "Blue-Eyes Toon Dragon", ["Quick-Finish"], 2, 1, 4773, 23018, "deck/limit_018.ydc\x00\x00\x00\x00",
+ 1),
+ OpponentData(97, "Tsukuyomi", [], 3, 1, 5780, 23019, "deck/limit_019.ydc\x00\x00\x00\x00", 2),
+ OpponentData(98, "Silent Swordsman LV3", ["Quick-Finish"], 2, 1, 6162, 23020, "deck/limit_020.ydc\x00\x00\x00\x00",
+ 2),
+ OpponentData(99, "Elemental Hero Flame Wingman", ["Quick-Finish"], 2, 1, 6344, 23021,
+ "deck/limit_021.ydc\x00\x00\x00\x00", 0),
+ OpponentData(100, "Armed Dragon LV7", ["Quick-Finish"], 2, 1, 6107, 23022, "deck/limit_022.ydc\x00\x00\x00\x00", 0),
+ OpponentData(101, "Alkana Knight Joker", ["Quick-Finish"], 1, 1, 6454, 23023, "deck/limit_023.ydc\x00\x00\x00\x00",
+ 0),
+ OpponentData(102, "Sorcerer of Dark Magic", [], 4, 1, 6086, 23024, "deck/limit_024.ydc\x00\x00\x00\x00", 3),
+ OpponentData(103, "Shinato, King of a Higher Plane", [], 4, 1, 5697, 23025, "deck/limit_025.ydc\x00\x00\x00\x00",
+ 3),
+ OpponentData(104, "Ryu Kokki", [], 5, 1, 5902, 23026, "deck/limit_026.ydc\x00\x00\x00\x00", 4),
+ OpponentData(105, "Cyber Dragon", [], 5, 1, 6390, 23027, "deck/limit_027.ydc\x00\x00\x00\x00", 4),
+ OpponentData(106, "Dark Dreadroute", ["Quick-Finish"], 3, 1, 6405, 23028, "deck/limit_028.ydc\x00\x00\x00\x00", 2),
+ OpponentData(107, "Ultimate Insect LV7", ["Quick-Finish"], 3, 1, 6319, 23029, "deck/limit_029.ydc\x00\x00\x00\x00",
+ 2),
+ OpponentData(108, "Thestalos the Firestorm Monarch", ["Quick-Finish"], 3, 1, 6190, 23030,
+ "deck/limit_030.ydc\x00\x00\x00\x00"),
+ OpponentData(109, "Master of Oz", ["Quick-Finish"], 3, 1, 6127, 23031, "deck/limit_031.ydc\x00\x00\x00\x00", 2),
+ OpponentData(110, "Orca Mega-Fortress of Darkness", ["Quick-Finish"], 3, 1, 5896, 23032,
+ "deck/limit_032.ydc\x00\x00\x00\x00", 2),
+ OpponentData(111, "Airknight Parshath", ["Quick-Finish"], 4, 1, 5023, 23033, "deck/limit_033.ydc\x00\x00\x00\x00",
+ 3),
+ OpponentData(112, "Dark Scorpion Burglars", ["Quick-Finish"], 4, 1, 5425, 23034,
+ "deck/limit_034.ydc\x00\x00\x00\x00", 3),
+ OpponentData(113, "Gilford the Lightning", [], 4, 1, 5451, 23035, "deck/limit_035.ydc\x00\x00\x00\x00", 3),
+ OpponentData(114, "Embodiment of Apophis", [], 2, 1, 5234, 23036, "deck/limit_036.ydc\x00\x00\x00\x00", 1),
+ OpponentData(115, "Great Maju Garzett", [], 5, 1, 5768, 23037, "deck/limit_037.ydc\x00\x00\x00\x00", 4),
+ OpponentData(116, "Black Luster Soldier - Envoy of the Beginning", [], 5, 1, 5835, 23038,
+ "deck/limit_038.ydc\x00\x00\x00\x00", 4),
+ OpponentData(117, "Red-Eyes B. Dragon", [], 4, 1, 4088, 23039, "deck/limit_039.ydc\x00\x00\x00\x00", 3),
+ OpponentData(118, "Blue-Eyes White Dragon", [], 4, 1, 4007, 23040, "deck/limit_040.ydc\x00\x00\x00\x00", 3),
+ OpponentData(119, "Dark Magician", [], 4, 1, 4041, 23041, "deck/limit_041.ydc\x00\x00\x00\x00", 3),
+ OpponentData(0, "Starter", ["Quick-Finish"], 1, 1, 4064, 1510, "deck/SD0_STARTER.ydc\x00\x00", 0),
+ OpponentData(10, "DRAGON'S ROAR", ["Quick-Finish"], 2, 1, 6292, 1511, "deck/SD1_DRAGON.ydc\x00\x00\x00", 1),
+ OpponentData(11, "ZOMBIE MADNESS", ["Quick-Finish"], 2, 1, 6293, 1512, "deck/SD2_UNDEAD.ydc\x00\x00\x00", 1),
+ OpponentData(12, "BLAZING DESTRUCTION", ["Quick-Finish"], 2, 1, 6368, 1513, "deck/SD3_FIRE.ydc\x00\x00\x00\x00\x00",
+ 1,
+ ["Has Back-row removal"]),
+ OpponentData(13, "FURY FROM THE DEEP", [], 2, 1, 6376, 1514,
+ "deck/SD4_UMI.ydc\x00\x00\x00\x00\x00\x00", 1, ["Has Back-row removal"]),
+ OpponentData(15, "WARRIORS TRIUMPH", ["Quick-Finish"], 2, 1, 6456, 1515, "deck/SD5_SOLDIER.ydc\x00\x00", 1),
+ OpponentData(16, "SPELLCASTERS JUDGEMENT", ["Quick-Finish"], 2, 1, 6530, 1516, "deck/SD6_MAGICIAN.ydc\x00", 1),
+ OpponentData(17, "INVICIBLE FORTRESS", [], 2, 1, 6640, 1517, "deck/SD7_GANSEKI.ydc\x00\x00", 1),
+ OpponentData(7, "Goblin King 2", ["Quick-Finish"], 3, 3, 5973, 8007, "deck/LV2_kingG2.ydc\x00\x00\x00", 2),
+]
+
+
+def get_opponents(multiworld: Optional[MultiWorld], player: Optional[int], randomize: bool = False) -> List[
+ OpponentData]:
+ opponents_table: List[OpponentData] = [
+ # Tier 1
+ OpponentData(0, "Kuriboh", [], 1, 1, 4064, 8000, "deck/LV1_kuriboh.ydc\x00\x00"),
+ OpponentData(1, "Scapegoat", [], 1, 2, 4818, 8001, "deck/LV1_sukego.ydc\x00\x00\x00", 0,
+ ["Has Back-row removal"]),
+ OpponentData(2, "Skull Servant", [], 1, 3, 4030, 8002, "deck/LV1_waito.ydc\x00\x00\x00\x00", 0,
+ ["Has Back-row removal"]),
+ OpponentData(3, "Watapon", [], 1, 4, 6092, 8003, "deck/LV1_watapon.ydc\x00\x00", 0, ["Has Back-row removal"]),
+ OpponentData(4, "White Magician Pikeru", [], 1, 5, 5975, 8004, "deck/LV1_pikeru.ydc\x00\x00\x00"),
+ # Tier 2
+ OpponentData(5, "Battery Man C", ["Quick-Finish"], 2, 1, 6428, 8005, "deck/LV2_denti.ydc\x00\x00\x00\x00", 1),
+ OpponentData(6, "Ojama Yellow", [], 2, 2, 5811, 8006, "deck/LV2_ojama.ydc\x00\x00\x00\x00", 1,
+ ["Has Back-row removal"]),
+ OpponentData(7, "Goblin King", ["Quick-Finish"], 2, 3, 5973, 8007, "deck/LV2_kingG.ydc\x00\x00\x00\x00", 1),
+ OpponentData(8, "Des Frog", ["Quick-Finish"], 2, 4, 6424, 8008, "deck/LV2_kaeru.ydc\x00\x00\x00\x00", 1),
+ OpponentData(9, "Water Dragon", ["Quick-Finish"], 2, 5, 6481, 8009, "deck/LV2_waterD.ydc\x00\x00\x00", 1),
+ # Tier 3
+ OpponentData(10, "Red-Eyes Darkness Dragon", ["Quick-Finish"], 3, 1, 6292, 8010, "deck/LV3_RedEyes.ydc\x00\x00",
+ 2),
+ OpponentData(11, "Vampire Genesis", ["Quick-Finish"], 3, 2, 6293, 8011, "deck/LV3_vamp.ydc\x00\x00\x00\x00\x00",
+ 2),
+ OpponentData(12, "Infernal Flame Emperor", [], 3, 3, 6368, 8012, "deck/LV3_flame.ydc\x00\x00\x00\x00", 2,
+ ["Has Back-row removal"]),
+ OpponentData(13, "Ocean Dragon Lord - Neo-Daedalus", [], 3, 4, 6376, 8013, "deck/LV3_daidaros.ydc\x00", 2,
+ ["Has Back-row removal"]),
+ OpponentData(14, "Helios Duo Megiste", ["Quick-Finish"], 3, 5, 6647, 8014, "deck/LV3_heriosu.ydc\x00\x00", 2),
+ # Tier 4
+ OpponentData(15, "Gilford the Legend", ["Quick-Finish"], 4, 1, 6456, 8015, "deck/LV4_gilfo.ydc\x00\x00\x00\x00",
+ 3),
+ OpponentData(16, "Dark Eradicator Warlock", ["Quick-Finish"], 4, 2, 6530, 8016, "deck/LV4_kuromadou.ydc", 3),
+ OpponentData(17, "Guardian Exode", [], 4, 3, 6640, 8017, "deck/LV4_exodo.ydc\x00\x00\x00\x00", 3),
+ OpponentData(18, "Goldd, Wu-Lord of Dark World", ["Quick-Finish"], 4, 4, 6505, 8018, "deck/LV4_ankokukai.ydc",
+ 3),
+ OpponentData(19, "Elemental Hero Erikshieler", ["Quick-Finish"], 4, 5, 6639, 8019,
+ "deck/LV4_Ehero.ydc\x00\x00\x00\x00", 3),
+ # Tier 5
+ OpponentData(20, "Raviel, Lord of Phantasms", [], 5, 1, 6565, 8020, "deck/LV5_ravieru.ydc\x00\x00", 4),
+ OpponentData(21, "Horus the Black Flame Dragon LV8", [], 5, 2, 6100, 8021, "deck/LV5_horus.ydc\x00\x00\x00\x00",
+ 4),
+ OpponentData(22, "Stronghold", [], 5, 3, 6153, 8022, "deck/LV5_gadget.ydc\x00\x00\x00", 5),
+ OpponentData(23, "Sacred Phoenix of Nephthys", [], 5, 4, 6236, 8023, "deck/LV5_nephthys.ydc\x00", 6),
+ OpponentData(24, "Cyber End Dragon", ["Goal"], 5, 5, 6397, 8024, "deck/LV5_cyber.ydc\x00\x00\x00\x00", 7),
+ ]
+ world = multiworld.worlds[player]
+ if not randomize:
+ return opponents_table
+ opponents = opponents_table + challenge_opponents
+ start = world.random.choice([o for o in opponents if o.tier == 1 and len(o.additional_info) == 0])
+ opponents.remove(start)
+ goal = world.random.choice([o for o in opponents if "Goal" in o.campaign_info])
+ opponents.remove(goal)
+ world.random.shuffle(opponents)
+ chosen_ones = opponents[:23]
+ for item in (multiworld.precollected_items[player]):
+ if item.name in tier_1_opponents:
+ # convert item index to opponent index
+ chosen_ones.insert(item_to_index[item.name] - item_to_index["Campaign Tier 1 Column 1"], start)
+ break
+ chosen_ones.append(goal)
+ tier = 1
+ column = 1
+ recreation = []
+ for opp in chosen_ones:
+ recreation.append(OpponentData(opp.id, opp.name, opp.campaign_info, tier, column, opp.card_id,
+ opp.deck_name_id, opp.deck_file, opp.difficulty))
+ column += 1
+ if column > 5:
+ column = 1
+ tier += 1
+
+ return recreation
+
+
+def get_opponent_locations(opponent: OpponentData) -> Dict[str, Optional[Union[str, int]]]:
+ location = {opponent.name + " Beaten": "Tier " + str(opponent.tier) + " Beaten"}
+ if opponent.tier > 4 and opponent.column != 5:
+ name = "Campaign Tier 5: Column " + str(opponent.column) + " Win"
+ # return a int instead so a item can be placed at this location later
+ location[name] = special[name]
+ for info in opponent.campaign_info:
+ location[opponent.name + "-> " + info] = info
+ return location
+
+
+def get_opponent_condition(opponent: OpponentData, unlock_item: str, unlock_amount: int, player: int,
+ is_challenge: bool) -> CollectionRule:
+ if is_challenge:
+ return lambda state: (
+ state.has(unlock_item, player, unlock_amount)
+ and yugioh06_difficulty(state, player, opponent.difficulty)
+ and state.has_all(opponent.additional_info, player)
+ )
+ else:
+ return lambda state: (
+ state.has_group(unlock_item, player, unlock_amount)
+ and yugioh06_difficulty(state, player, opponent.difficulty)
+ and state.has_all(opponent.additional_info, player)
+ )
diff --git a/worlds/yugioh06/options.py b/worlds/yugioh06/options.py
new file mode 100644
index 000000000000..3100f5175d6f
--- /dev/null
+++ b/worlds/yugioh06/options.py
@@ -0,0 +1,195 @@
+from dataclasses import dataclass
+
+from Options import Choice, DefaultOnToggle, PerGameCommonOptions, Range, Toggle
+
+
+class StructureDeck(Choice):
+ """Which Structure Deck you start with"""
+
+ display_name = "Structure Deck"
+ option_dragons_roar = 0
+ option_zombie_madness = 1
+ option_blazing_destruction = 2
+ option_fury_from_the_deep = 3
+ option_warriors_triumph = 4
+ option_spellcasters_judgement = 5
+ option_none = 6
+ option_random_deck = 7
+ default = 7
+
+
+class Banlist(Choice):
+ """Which Banlist you start with"""
+
+ display_name = "Banlist"
+ option_no_banlist = 0
+ option_september_2003 = 1
+ option_march_2004 = 2
+ option_september_2004 = 3
+ option_march_2005 = 4
+ option_september_2005 = 5
+ default = option_september_2005
+
+
+class FinalCampaignBossUnlockCondition(Choice):
+ """How to unlock the final campaign boss and goal for the world"""
+
+ display_name = "Final Campaign Boss unlock Condition"
+ option_campaign_opponents = 0
+ option_challenges = 1
+
+
+class FourthTier5UnlockCondition(Choice):
+ """How to unlock the fourth campaign boss"""
+
+ display_name = "Fourth Tier 5 Campaign Boss unlock Condition"
+ option_campaign_opponents = 0
+ option_challenges = 1
+
+
+class ThirdTier5UnlockCondition(Choice):
+ """How to unlock the third campaign boss"""
+
+ display_name = "Third Tier 5 Campaign Boss unlock Condition"
+ option_campaign_opponents = 0
+ option_challenges = 1
+
+
+class FinalCampaignBossChallenges(Range):
+ """Number of Limited/Theme Duels completed for the Final Campaign Boss to appear"""
+
+ display_name = "Final Campaign Boss challenges unlock amount"
+ range_start = 0
+ range_end = 91
+ default = 10
+
+
+class FourthTier5CampaignBossChallenges(Range):
+ """Number of Limited/Theme Duels completed for the Fourth Level 5 Campaign Opponent to appear"""
+
+ display_name = "Fourth Tier 5 Campaign Boss unlock amount"
+ range_start = 0
+ range_end = 91
+ default = 5
+
+
+class ThirdTier5CampaignBossChallenges(Range):
+ """Number of Limited/Theme Duels completed for the Third Level 5 Campaign Opponent to appear"""
+
+ display_name = "Third Tier 5 Campaign Boss unlock amount"
+ range_start = 0
+ range_end = 91
+ default = 2
+
+
+class FinalCampaignBossCampaignOpponents(Range):
+ """Number of Campaign Opponents Duels defeated for the Final Campaign Boss to appear"""
+
+ display_name = "Final Campaign Boss campaign opponent unlock amount"
+ range_start = 0
+ range_end = 24
+ default = 12
+
+
+class FourthTier5CampaignBossCampaignOpponents(Range):
+ """Number of Campaign Opponents Duels defeated for the Fourth Level 5 Campaign Opponent to appear"""
+
+ display_name = "Fourth Tier 5 Campaign Boss campaign opponent unlock amount"
+ range_start = 0
+ range_end = 23
+ default = 7
+
+
+class ThirdTier5CampaignBossCampaignOpponents(Range):
+ """Number of Campaign Opponents Duels defeated for the Third Level 5 Campaign Opponent to appear"""
+
+ display_name = "Third Tier 5 Campaign Boss campaign opponent unlock amount"
+ range_start = 0
+ range_end = 22
+ default = 3
+
+
+class NumberOfChallenges(Range):
+ """Number of random Limited/Theme Duels that are included. The rest will be inaccessible."""
+
+ display_name = "Number of Challenges"
+ range_start = 0
+ range_end = 91
+ default = 10
+
+
+class StartingMoney(Range):
+ """The amount of money you start with"""
+
+ display_name = "Starting Money"
+ range_start = 0
+ range_end = 100000
+ default = 3000
+
+
+class MoneyRewardMultiplier(Range):
+ """By which amount the campaign reward money is multiplied"""
+
+ display_name = "Money Reward Multiplier"
+ range_start = 1
+ range_end = 255
+ default = 20
+
+
+class NormalizeBoostersPacks(DefaultOnToggle):
+ """If enabled every booster pack costs the same otherwise vanilla cost is used"""
+
+ display_name = "Normalize Booster Packs"
+
+
+class BoosterPackPrices(Range):
+ """
+ Only Works if normalize booster packs is enabled.
+ Sets the amount that what every booster pack costs.
+ """
+
+ display_name = "Booster Pack Prices"
+ range_start = 1
+ range_end = 3000
+ default = 100
+
+
+class AddEmptyBanList(Toggle):
+ """Adds a Ban List where everything is at 3 to the item pool"""
+
+ display_name = "Add Empty Ban List"
+
+
+class CampaignOpponentsShuffle(Toggle):
+ """Replaces the campaign with random opponents from the entire game"""
+
+ display_name = "Campaign Opponents Shuffle"
+
+
+class OCGArts(Toggle):
+ """Always use the OCG artworks for cards"""
+
+ display_name = "OCG Arts"
+
+
+@dataclass
+class Yugioh06Options(PerGameCommonOptions):
+ structure_deck: StructureDeck
+ banlist: Banlist
+ final_campaign_boss_unlock_condition: FinalCampaignBossUnlockCondition
+ fourth_tier_5_campaign_boss_unlock_condition: FourthTier5UnlockCondition
+ third_tier_5_campaign_boss_unlock_condition: ThirdTier5UnlockCondition
+ final_campaign_boss_challenges: FinalCampaignBossChallenges
+ fourth_tier_5_campaign_boss_challenges: FourthTier5CampaignBossChallenges
+ third_tier_5_campaign_boss_challenges: ThirdTier5CampaignBossChallenges
+ final_campaign_boss_campaign_opponents: FinalCampaignBossCampaignOpponents
+ fourth_tier_5_campaign_boss_campaign_opponents: FourthTier5CampaignBossCampaignOpponents
+ third_tier_5_campaign_boss_campaign_opponents: ThirdTier5CampaignBossCampaignOpponents
+ number_of_challenges: NumberOfChallenges
+ starting_money: StartingMoney
+ money_reward_multiplier: MoneyRewardMultiplier
+ normalize_boosters_packs: NormalizeBoostersPacks
+ booster_pack_prices: BoosterPackPrices
+ add_empty_banlist: AddEmptyBanList
+ campaign_opponents_shuffle: CampaignOpponentsShuffle
+ ocg_arts: OCGArts
diff --git a/worlds/yugioh06/patch.bsdiff4 b/worlds/yugioh06/patch.bsdiff4
new file mode 100644
index 000000000000..877884d5c946
Binary files /dev/null and b/worlds/yugioh06/patch.bsdiff4 differ
diff --git a/worlds/yugioh06/patches/draft.bsdiff4 b/worlds/yugioh06/patches/draft.bsdiff4
new file mode 100644
index 000000000000..73980b58abe0
Binary files /dev/null and b/worlds/yugioh06/patches/draft.bsdiff4 differ
diff --git a/worlds/yugioh06/patches/ocg.bsdiff4 b/worlds/yugioh06/patches/ocg.bsdiff4
new file mode 100644
index 000000000000..8b4b69796d13
Binary files /dev/null and b/worlds/yugioh06/patches/ocg.bsdiff4 differ
diff --git a/worlds/yugioh06/rom.py b/worlds/yugioh06/rom.py
new file mode 100644
index 000000000000..3ac10f9ea496
--- /dev/null
+++ b/worlds/yugioh06/rom.py
@@ -0,0 +1,161 @@
+import hashlib
+import math
+import os
+import struct
+
+from settings import get_settings
+
+import Utils
+from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
+
+from worlds.AutoWorld import World
+from .items import item_to_index
+from .rom_values import banlist_ids, function_addresses, structure_deck_selection
+
+MD5Europe = "020411d3b08f5639eb8cb878283f84bf"
+MD5America = "b8a7c976b28172995fe9e465d654297a"
+
+
+class YGO06ProcedurePatch(APProcedurePatch, APTokenMixin):
+ game = "Yu-Gi-Oh! 2006"
+ hash = MD5America
+ patch_file_ending = ".apygo06"
+ result_file_ending = ".gba"
+
+ @classmethod
+ def get_source_data(cls) -> bytes:
+ return get_base_rom_bytes()
+
+
+def write_tokens(world: World, patch: YGO06ProcedurePatch):
+ structure_deck = structure_deck_selection.get(world.options.structure_deck.value)
+ # set structure deck
+ patch.write_token(APTokenTypes.WRITE, 0x000FD0AA, struct.pack(" bytes:
+ base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
+ if not base_rom_bytes:
+ file_name = get_base_rom_path(file_name)
+ base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb")))
+
+ basemd5 = hashlib.md5()
+ basemd5.update(base_rom_bytes)
+ md5hash = basemd5.hexdigest()
+ if MD5Europe != md5hash and MD5America != md5hash:
+ raise Exception(
+ "Supplied Base Rom does not match known MD5 for"
+ "Yu-Gi-Oh! World Championship 2006 America or Europe "
+ "Get the correct game and version, then dump it"
+ )
+ get_base_rom_bytes.base_rom_bytes = base_rom_bytes
+ return base_rom_bytes
+
+
+def get_base_rom_path(file_name: str = "") -> str:
+ if not file_name:
+ file_name = get_settings().yugioh06_settings.rom_file
+ if not os.path.exists(file_name):
+ file_name = Utils.user_path(file_name)
+ return file_name
diff --git a/worlds/yugioh06/rom_values.py b/worlds/yugioh06/rom_values.py
new file mode 100644
index 000000000000..4fb310080de9
--- /dev/null
+++ b/worlds/yugioh06/rom_values.py
@@ -0,0 +1,38 @@
+structure_deck_selection = {
+ # DRAGON'S ROAR
+ 0: 0x1,
+ # ZOMBIE MADNESS
+ 1: 0x5,
+ # BLAZING DESTRUCTION
+ 2: 0x9,
+ # FURY FROM THE DEEP
+ 3: 0xD,
+ # Warrior'S TRIUMPH
+ 4: 0x11,
+ # SPELLCASTER'S JUDGEMENT
+ 5: 0x15,
+ # Draft Mode
+ 6: 0x1,
+}
+
+banlist_ids = {
+ # NoList
+ 0: 0x0,
+ # September 2003
+ 1: 0x5,
+ # March 2004
+ 2: 0x6,
+ # September 2004
+ 3: 0x7,
+ # March 2005
+ 4: 0x8,
+ # September 2005
+ 5: 0x9,
+}
+
+function_addresses = {
+ # Count Campaign Opponents
+ 0: 0xF0C8,
+ # Count Challenges
+ 1: 0xEF3A,
+}
diff --git a/worlds/yugioh06/ruff.toml b/worlds/yugioh06/ruff.toml
new file mode 100644
index 000000000000..8acb3b14702f
--- /dev/null
+++ b/worlds/yugioh06/ruff.toml
@@ -0,0 +1,12 @@
+line-length = 120
+
+[lint]
+preview = true
+select = ["E", "F", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"]
+ignore = ["RUF012", "RUF100"]
+
+[per-file-ignores]
+# The way options definitions work right now, world devs are forced to break line length requirements.
+"options.py" = ["E501"]
+# Yu Gi Oh specific: The structure of the Opponents.py file makes the line length violations acceptable.
+"Opponents.py" = ["E501"]
\ No newline at end of file
diff --git a/worlds/yugioh06/rules.py b/worlds/yugioh06/rules.py
new file mode 100644
index 000000000000..a804c7e7286a
--- /dev/null
+++ b/worlds/yugioh06/rules.py
@@ -0,0 +1,868 @@
+from worlds.generic.Rules import add_rule
+
+from . import yugioh06_difficulty
+from .fusions import count_has_materials
+
+
+def set_rules(world):
+ player = world.player
+ multiworld = world.multiworld
+
+ location_rules = {
+ # Campaign
+ "Campaign Tier 1: 1 Win": lambda state: state.has("Tier 1 Beaten", player),
+ "Campaign Tier 1: 3 Wins A": lambda state: state.has("Tier 1 Beaten", player, 3),
+ "Campaign Tier 1: 3 Wins B": lambda state: state.has("Tier 1 Beaten", player, 3),
+ "Campaign Tier 1: 5 Wins A": lambda state: state.has("Tier 1 Beaten", player, 5),
+ "Campaign Tier 1: 5 Wins B": lambda state: state.has("Tier 1 Beaten", player, 5),
+ "Campaign Tier 2: 1 Win": lambda state: state.has("Tier 2 Beaten", player),
+ "Campaign Tier 2: 3 Wins A": lambda state: state.has("Tier 2 Beaten", player, 3),
+ "Campaign Tier 2: 3 Wins B": lambda state: state.has("Tier 2 Beaten", player, 3),
+ "Campaign Tier 2: 5 Wins A": lambda state: state.has("Tier 2 Beaten", player, 5),
+ "Campaign Tier 2: 5 Wins B": lambda state: state.has("Tier 2 Beaten", player, 5),
+ "Campaign Tier 3: 1 Win": lambda state: state.has("Tier 3 Beaten", player),
+ "Campaign Tier 3: 3 Wins A": lambda state: state.has("Tier 3 Beaten", player, 3),
+ "Campaign Tier 3: 3 Wins B": lambda state: state.has("Tier 3 Beaten", player, 3),
+ "Campaign Tier 3: 5 Wins A": lambda state: state.has("Tier 3 Beaten", player, 5),
+ "Campaign Tier 3: 5 Wins B": lambda state: state.has("Tier 3 Beaten", player, 5),
+ "Campaign Tier 4: 5 Wins A": lambda state: state.has("Tier 4 Beaten", player, 5),
+ "Campaign Tier 4: 5 Wins B": lambda state: state.has("Tier 4 Beaten", player, 5),
+
+ # Bonuses
+ "Duelist Bonus Level 1": lambda state: state.has("Tier 1 Beaten", player),
+ "Duelist Bonus Level 2": lambda state: state.has("Tier 2 Beaten", player),
+ "Duelist Bonus Level 3": lambda state: state.has("Tier 3 Beaten", player),
+ "Duelist Bonus Level 4": lambda state: state.has("Tier 4 Beaten", player),
+ "Duelist Bonus Level 5": lambda state: state.has("Tier 5 Beaten", player),
+ "Max ATK Bonus": lambda state: yugioh06_difficulty(state, player, 2),
+ "No Spell Cards Bonus": lambda state: yugioh06_difficulty(state, player, 2),
+ "No Trap Cards Bonus": lambda state: yugioh06_difficulty(state, player, 2),
+ "No Damage Bonus": lambda state: state.has_group("Campaign Boss Beaten", player, 3),
+ "Low Deck Bonus": lambda state: state.has_any(["Reasoning", "Monster Gate", "Magical Merchant"], player) and
+ yugioh06_difficulty(state, player, 3),
+ "Extremely Low Deck Bonus":
+ lambda state: state.has_any(["Reasoning", "Monster Gate", "Magical Merchant"], player) and
+ yugioh06_difficulty(state, player, 2),
+ "Opponent's Turn Finish Bonus": lambda state: yugioh06_difficulty(state, player, 2),
+ "Exactly 0 LP Bonus": lambda state: yugioh06_difficulty(state, player, 2),
+ "Reversal Finish Bonus": lambda state: yugioh06_difficulty(state, player, 2),
+ "Quick Finish Bonus": lambda state: state.has("Quick-Finish", player) or yugioh06_difficulty(state, player, 6),
+ "Exodia Finish Bonus": lambda state: state.has("Can Exodia Win", player),
+ "Last Turn Finish Bonus": lambda state: state.has("Can Last Turn Win", player),
+ "Yata-Garasu Finish Bonus": lambda state: state.has("Can Yata Lock", player),
+ "Skull Servant Finish Bonus": lambda state: state.has("Skull Servant", player) and
+ yugioh06_difficulty(state, player, 3),
+ "Konami Bonus": lambda state: state.has_all(["Messenger of Peace", "Castle of Dark Illusions", "Mystik Wok"],
+ player) or (state.has_all(["Mystik Wok", "Barox", "Cyber-Stein",
+ "Poison of the Old Man"],
+ player) and yugioh06_difficulty(state,
+ player, 8)),
+ "Max Damage Bonus": lambda state: state.has_any(["Wave-Motion Cannon", "Megamorph", "United We Stand",
+ "Mage Power"], player),
+ "Fusion Summon Bonus": lambda state: state.has_any(["Polymerization", "Fusion Gate", "Power Bond"], player),
+ "Ritual Summon Bonus": lambda state: state.has("Ritual", player),
+ "Over 20000 LP Bonus": lambda state: can_gain_lp_every_turn(state, player)
+ and state.has("Can Stall with ST", player),
+ "Low LP Bonus": lambda state: state.has("Wall of Revealing Light", player) and yugioh06_difficulty(state, player,
+ 2),
+ "Extremely Low LP Bonus": lambda state: state.has_all(["Wall of Revealing Light", "Messenger of Peace"], player)
+ and yugioh06_difficulty(state, player, 4),
+ "Effect Damage Only Bonus": lambda state: state.has_all(["Solar Flare Dragon", "UFO Turtle"], player)
+ or state.has("Wave-Motion Cannon", player)
+ or state.can_reach("Final Countdown Finish Bonus", "Location", player)
+ or state.can_reach("Destiny Board Finish Bonus", "Location", player)
+ or state.has("Can Exodia Win", player)
+ or state.has("Can Last Turn Win", player),
+ "No More Cards Bonus": lambda state: state.has_any(["Cyber Jar", "Morphing Jar",
+ "Morphing Jar #2", "Needle Worm"], player)
+ and state.has_any(["The Shallow Grave", "Spear Cretin"],
+ player) and yugioh06_difficulty(state, player, 5),
+ "Final Countdown Finish Bonus": lambda state: state.has("Final Countdown", player)
+ and state.has("Can Stall with ST", player),
+ "Destiny Board Finish Bonus": lambda state: state.has("Can Stall with Monsters", player) and
+ state.has("Destiny Board and its letters", player) and
+ state.has("A Cat of Ill Omen", player),
+
+ # Cards
+ "Obtain all pieces of Exodia": lambda state: state.has("Exodia", player),
+ "Obtain Final Countdown": lambda state: state.has("Final Countdown", player),
+ "Obtain Victory Dragon": lambda state: state.has("Victory D.", player),
+ "Obtain Ojama Delta Hurricane and its required cards":
+ lambda state: state.has("Ojama Delta Hurricane and required cards", player),
+ "Obtain Huge Revolution and its required cards":
+ lambda state: state.has("Huge Revolution and its required cards", player),
+ "Obtain Perfectly Ultimate Great Moth and its required cards":
+ lambda state: state.has("Perfectly Ultimate Great Moth and its required cards", player),
+ "Obtain Valkyrion the Magna Warrior and its pieces":
+ lambda state: state.has("Valkyrion the Magna Warrior and its pieces", player),
+ "Obtain Dark Sage and its required cards": lambda state: state.has("Dark Sage and its required cards", player),
+ "Obtain Destiny Board and its letters": lambda state: state.has("Destiny Board and its letters", player),
+ "Obtain all XYZ-Dragon Cannon fusions and their materials":
+ lambda state: state.has("XYZ-Dragon Cannon fusions and their materials", player),
+ "Obtain VWXYZ-Dragon Catapult Cannon and the fusion materials":
+ lambda state: state.has("VWXYZ-Dragon Catapult Cannon and the fusion materials", player),
+ "Obtain Hamon, Lord of Striking Thunder":
+ lambda state: state.has("Hamon, Lord of Striking Thunder", player),
+ "Obtain Raviel, Lord of Phantasms":
+ lambda state: state.has("Raviel, Lord of Phantasms", player),
+ "Obtain Uria, Lord of Searing Flames":
+ lambda state: state.has("Uria, Lord of Searing Flames", player),
+ "Obtain Gate Guardian and its pieces":
+ lambda state: state.has("Gate Guardian and its pieces", player),
+ "Obtain Dark Scorpion Combination and its required cards":
+ lambda state: state.has("Dark Scorpion Combination and its required cards", player),
+ # Collection Events
+ "Ojama Delta Hurricane and required cards":
+ lambda state: state.has_all(["Ojama Delta Hurricane", "Ojama Green", "Ojama Yellow", "Ojama Black"],
+ player),
+ "Huge Revolution and its required cards":
+ lambda state: state.has_all(["Huge Revolution", "Oppressed People", "United Resistance",
+ "People Running About"], player),
+ "Perfectly Ultimate Great Moth and its required cards":
+ lambda state: state.has_all(["Perfectly Ultimate Great Moth", "Petit Moth", "Cocoon of Evolution"], player),
+ "Valkyrion the Magna Warrior and its pieces":
+ lambda state: state.has_all(["Valkyrion the Magna Warrior", "Alpha the Magnet Warrior",
+ "Beta the Magnet Warrior", "Gamma the Magnet Warrior"], player),
+ "Dark Sage and its required cards":
+ lambda state: state.has_all(["Dark Sage", "Dark Magician", "Time Wizard"], player),
+ "Destiny Board and its letters":
+ lambda state: state.has_all(["Destiny Board", "Spirit Message 'I'", "Spirit Message 'N'",
+ "Spirit Message 'A'", "Spirit Message 'L'"], player),
+ "XYZ-Dragon Cannon fusions and their materials":
+ lambda state: state.has_all(["X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank",
+ "XY-Dragon Cannon", "XZ-Tank Cannon", "YZ-Tank Dragon", "XYZ-Dragon Cannon"],
+ player),
+ "VWXYZ-Dragon Catapult Cannon and the fusion materials":
+ lambda state: state.has_all(["X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank", "XYZ-Dragon Cannon",
+ "V-Tiger Jet", "W-Wing Catapult", "VW-Tiger Catapult",
+ "VWXYZ-Dragon Catapult Cannon"],
+ player),
+ "Gate Guardian and its pieces":
+ lambda state: state.has_all(["Gate Guardian", "Kazejin", "Suijin", "Sanga of the Thunder"], player),
+ "Dark Scorpion Combination and its required cards":
+ lambda state: state.has_all(["Dark Scorpion Combination", "Don Zaloog", "Dark Scorpion - Chick the Yellow",
+ "Dark Scorpion - Meanae the Thorn", "Dark Scorpion - Gorg the Strong",
+ "Cliff the Trap Remover"], player),
+ "Can Exodia Win":
+ lambda state: state.has_all(["Exodia", "Heart of the Underdog"], player),
+ "Can Last Turn Win":
+ lambda state: state.has_all(["Last Turn", "Wall of Revealing Light"], player) and
+ (state.has_any(["Jowgen the Spiritualist", "Jowls of Dark Demise", "Non Aggression Area"],
+ player)
+ or state.has_all(["Cyber-Stein", "The Last Warrior from Another Planet"], player)),
+ "Can Yata Lock":
+ lambda state: state.has_all(["Yata-Garasu", "Chaos Emperor Dragon - Envoy of the End", "Sangan"], player)
+ and state.has_any(["No Banlist", "Banlist September 2003"], player),
+ "Can Stall with Monsters":
+ lambda state: state.count_from_list_unique(
+ ["Spirit Reaper", "Giant Germ", "Marshmallon", "Nimble Momonga"], player) >= 2,
+ "Can Stall with ST":
+ lambda state: state.count_from_list_unique(["Level Limit - Area B", "Gravity Bind", "Messenger of Peace"],
+ player) >= 2,
+ "Has Back-row removal":
+ lambda state: back_row_removal(state, player)
+
+ }
+ access_rules = {
+ # Limited
+ "LD01 All except Level 4 forbidden":
+ lambda state: yugioh06_difficulty(state, player, 2),
+ "LD02 Medium/high Level forbidden":
+ lambda state: yugioh06_difficulty(state, player, 1),
+ "LD03 ATK 1500 or more forbidden":
+ lambda state: yugioh06_difficulty(state, player, 4),
+ "LD04 Flip Effects forbidden":
+ lambda state: yugioh06_difficulty(state, player, 1),
+ "LD05 Tributes forbidden":
+ lambda state: yugioh06_difficulty(state, player, 1),
+ "LD06 Traps forbidden":
+ lambda state: yugioh06_difficulty(state, player, 1),
+ "LD07 Large Deck A":
+ lambda state: yugioh06_difficulty(state, player, 4),
+ "LD08 Large Deck B":
+ lambda state: yugioh06_difficulty(state, player, 4),
+ "LD09 Sets Forbidden":
+ lambda state: yugioh06_difficulty(state, player, 1),
+ "LD10 All except LV monsters forbidden":
+ lambda state: only_level(state, player) and yugioh06_difficulty(state, player, 2),
+ "LD11 All except Fairies forbidden":
+ lambda state: only_fairy(state, player) and yugioh06_difficulty(state, player, 2),
+ "LD12 All except Wind forbidden":
+ lambda state: only_wind(state, player) and yugioh06_difficulty(state, player, 2),
+ "LD13 All except monsters forbidden":
+ lambda state: yugioh06_difficulty(state, player, 3),
+ "LD14 Level 3 or below forbidden":
+ lambda state: yugioh06_difficulty(state, player, 1),
+ "LD15 DEF 1500 or less forbidden":
+ lambda state: yugioh06_difficulty(state, player, 3),
+ "LD16 Effect Monsters forbidden":
+ lambda state: only_normal(state, player) and yugioh06_difficulty(state, player, 4),
+ "LD17 Spells forbidden":
+ lambda state: yugioh06_difficulty(state, player, 3),
+ "LD18 Attacks forbidden":
+ lambda state: state.has_all(["Wave-Motion Cannon", "Stealth Bird"], player)
+ and state.count_from_list_unique(["Dark World Lightning", "Nobleman of Crossout",
+ "Shield Crash", "Tribute to the Doomed"], player) >= 2
+ and yugioh06_difficulty(state, player, 3),
+ "LD19 All except E-Hero's forbidden":
+ lambda state: state.has_any(["Polymerization", "Fusion Gate"], player) and
+ count_has_materials(state, ["Elemental Hero Flame Wingman",
+ "Elemental Hero Madballman",
+ "Elemental Hero Rampart Blaster",
+ "Elemental Hero Steam Healer",
+ "Elemental Hero Shining Flare Wingman",
+ "Elemental Hero Wildedge"], player) >= 3 and
+ yugioh06_difficulty(state, player, 3),
+ "LD20 All except Warriors forbidden":
+ lambda state: only_warrior(state, player) and yugioh06_difficulty(state, player, 2),
+ "LD21 All except Dark forbidden":
+ lambda state: only_dark(state, player) and yugioh06_difficulty(state, player, 2),
+ "LD22 All limited cards forbidden":
+ lambda state: yugioh06_difficulty(state, player, 3),
+ "LD23 Refer to Mar 05 Banlist":
+ lambda state: yugioh06_difficulty(state, player, 5),
+ "LD24 Refer to Sept 04 Banlist":
+ lambda state: yugioh06_difficulty(state, player, 5),
+ "LD25 Low Life Points":
+ lambda state: yugioh06_difficulty(state, player, 5),
+ "LD26 All except Toons forbidden":
+ lambda state: only_toons(state, player) and yugioh06_difficulty(state, player, 2),
+ "LD27 All except Spirits forbidden":
+ lambda state: only_spirit(state, player) and yugioh06_difficulty(state, player, 2),
+ "LD28 All except Dragons forbidden":
+ lambda state: only_dragon(state, player) and yugioh06_difficulty(state, player, 2),
+ "LD29 All except Spellcasters forbidden":
+ lambda state: only_spellcaster(state, player) and yugioh06_difficulty(state, player, 2),
+ "LD30 All except Light forbidden":
+ lambda state: only_light(state, player) and yugioh06_difficulty(state, player, 2),
+ "LD31 All except Fire forbidden":
+ lambda state: only_fire(state, player) and yugioh06_difficulty(state, player, 2),
+ "LD32 Decks with multiples forbidden":
+ lambda state: yugioh06_difficulty(state, player, 4),
+ "LD33 Special Summons forbidden":
+ lambda state: yugioh06_difficulty(state, player, 2),
+ "LD34 Normal Summons forbidden":
+ lambda state: state.has_all(["Polymerization", "King of the Swamp"], player) and
+ count_has_materials(state, ["Elemental Hero Flame Wingman",
+ "Elemental Hero Madballman",
+ "Elemental Hero Rampart Blaster",
+ "Elemental Hero Steam Healer",
+ "Elemental Hero Shining Flare Wingman",
+ "Elemental Hero Wildedge"], player) >= 3 and
+ yugioh06_difficulty(state, player, 4),
+ "LD35 All except Zombies forbidden":
+ lambda state: only_zombie(state, player) and yugioh06_difficulty(state, player, 2),
+ "LD36 All except Earth forbidden":
+ lambda state: only_earth(state, player) and yugioh06_difficulty(state, player, 2),
+ "LD37 All except Water forbidden":
+ lambda state: only_water(state, player) and yugioh06_difficulty(state, player, 2),
+ "LD38 Refer to Mar 04 Banlist":
+ lambda state: yugioh06_difficulty(state, player, 4),
+ "LD39 Monsters forbidden":
+ lambda state: state.has_all(["Skull Zoma", "Embodiment of Apophis"], player)
+ and yugioh06_difficulty(state, player, 5),
+ "LD40 Refer to Sept 05 Banlist":
+ lambda state: yugioh06_difficulty(state, player, 5),
+ "LD41 Refer to Sept 03 Banlist":
+ lambda state: yugioh06_difficulty(state, player, 5),
+ # Theme Duels
+ "TD01 Battle Damage":
+ lambda state: yugioh06_difficulty(state, player, 1),
+ "TD02 Deflected Damage":
+ lambda state: state.has("Fairy Box", player) and yugioh06_difficulty(state, player, 1),
+ "TD03 Normal Summon":
+ lambda state: yugioh06_difficulty(state, player, 3),
+ "TD04 Ritual Summon":
+ lambda state: yugioh06_difficulty(state, player, 3) and
+ state.has_all(["Contract with the Abyss",
+ "Manju of the Ten Thousand Hands",
+ "Senju of the Thousand Hands",
+ "Sonic Bird",
+ "Pot of Avarice",
+ "Dark Master - Zorc",
+ "Demise, King of Armageddon",
+ "The Masked Beast",
+ "Magician of Black Chaos",
+ "Dark Magic Ritual"], player),
+ "TD05 Special Summon A":
+ lambda state: yugioh06_difficulty(state, player, 3),
+ "TD06 20x Spell":
+ lambda state: state.has("Magical Blast", player) and yugioh06_difficulty(state, player, 3),
+ "TD07 10x Trap":
+ lambda state: yugioh06_difficulty(state, player, 3),
+ "TD08 Draw":
+ lambda state: state.has_any(["Self-Destruct Button", "Dark Snake Syndrome"], player) and
+ yugioh06_difficulty(state, player, 3),
+ "TD09 Hand Destruction":
+ lambda state: state.has_all(["Cyber Jar",
+ "Morphing Jar",
+ "Book of Moon",
+ "Book of Taiyou",
+ "Card Destruction",
+ "Serial Spell",
+ "Spell Reproduction",
+ "The Shallow Grave"], player) and yugioh06_difficulty(state, player, 3),
+ "TD10 During Opponent's Turn":
+ lambda state: yugioh06_difficulty(state, player, 3),
+ "TD11 Recover":
+ lambda state: can_gain_lp_every_turn(state, player) and yugioh06_difficulty(state, player, 3),
+ "TD12 Remove Monsters by Effect":
+ lambda state: state.has("Soul Release", player) and yugioh06_difficulty(state, player, 2),
+ "TD13 Flip Summon":
+ lambda state: pacman_deck(state, player) and yugioh06_difficulty(state, player, 2),
+ "TD14 Special Summon B":
+ lambda state: state.has_any(["Manticore of Darkness", "Treeborn Frog"], player) and
+ state.has("Foolish Burial", player) and
+ yugioh06_difficulty(state, player, 2),
+ "TD15 Token":
+ lambda state: state.has_all(["Dandylion", "Ojama Trio", "Stray Lambs"], player) and
+ yugioh06_difficulty(state, player, 3),
+ "TD16 Union":
+ lambda state: equip_unions(state, player) and
+ yugioh06_difficulty(state, player, 2),
+ "TD17 10x Quick Spell":
+ lambda state: quick_plays(state, player) and
+ yugioh06_difficulty(state, player, 3),
+ "TD18 The Forbidden":
+ lambda state: state.has("Can Exodia Win", player),
+ "TD19 20 Turns":
+ lambda state: state.has("Final Countdown", player) and state.has("Can Stall with ST", player) and
+ yugioh06_difficulty(state, player, 3),
+ "TD20 Deck Destruction":
+ lambda state: state.has_any(["Cyber Jar", "Morphing Jar", "Morphing Jar #2", "Needle Worm"], player)
+ and state.has_any(["The Shallow Grave", "Spear Cretin"],
+ player) and yugioh06_difficulty(state, player, 2),
+ "TD21 Victory D.":
+ lambda state: state.has("Victory D.", player) and only_dragon(state, player)
+ and yugioh06_difficulty(state, player, 3),
+ "TD22 The Preventers Fight Back":
+ lambda state: state.has("Ojama Delta Hurricane and required cards", player) and
+ state.has_all(["Rescue Cat", "Enchanting Fitting Room", "Jerry Beans Man"], player) and
+ yugioh06_difficulty(state, player, 3),
+ "TD23 Huge Revolution":
+ lambda state: state.has("Huge Revolution and its required cards", player) and
+ state.has_all(["Enchanting Fitting Room", "Jerry Beans Man"], player) and
+ yugioh06_difficulty(state, player, 3),
+ "TD24 Victory in 5 Turns":
+ lambda state: yugioh06_difficulty(state, player, 3),
+ "TD25 Moth Grows Up":
+ lambda state: state.has("Perfectly Ultimate Great Moth and its required cards", player) and
+ state.has_all(["Gokipon", "Howling Insect"], player) and
+ yugioh06_difficulty(state, player, 3),
+ "TD26 Magnetic Power":
+ lambda state: state.has("Valkyrion the Magna Warrior and its pieces", player) and
+ yugioh06_difficulty(state, player, 2),
+ "TD27 Dark Sage":
+ lambda state: state.has("Dark Sage and its required cards", player) and
+ state.has_any(["Skilled Dark Magician", "Dark Magic Curtain"], player) and
+ yugioh06_difficulty(state, player, 2),
+ "TD28 Direct Damage":
+ lambda state: yugioh06_difficulty(state, player, 2),
+ "TD29 Destroy Monsters in Battle":
+ lambda state: yugioh06_difficulty(state, player, 2),
+ "TD30 Tribute Summon":
+ lambda state: state.has("Treeborn Frog", player) and yugioh06_difficulty(state, player, 2),
+ "TD31 Special Summon C":
+ lambda state: state.count_from_list_unique(
+ ["Aqua Spirit", "Rock Spirit", "Spirit of Flames",
+ "Garuda the Wind Spirit", "Gigantes", "Inferno", "Megarock Dragon", "Silpheed"],
+ player) > 4 and yugioh06_difficulty(state, player, 3),
+ "TD32 Toon":
+ lambda state: only_toons(state, player) and yugioh06_difficulty(state, player, 3),
+ "TD33 10x Counter":
+ lambda state: counter_traps(state, player) and yugioh06_difficulty(state, player, 2),
+ "TD34 Destiny Board":
+ lambda state: state.has("Destiny Board and its letters", player)
+ and state.has("Can Stall with Monsters", player)
+ and state.has("A Cat of Ill Omen", player)
+ and yugioh06_difficulty(state, player, 2),
+ "TD35 Huge Damage in a Turn":
+ lambda state: state.has_all(["Cyber-Stein", "Cyber Twin Dragon", "Megamorph"], player)
+ and yugioh06_difficulty(state, player, 3),
+ "TD36 V-Z In the House":
+ lambda state: state.has("VWXYZ-Dragon Catapult Cannon and the fusion materials", player)
+ and yugioh06_difficulty(state, player, 3),
+ "TD37 Uria, Lord of Searing Flames":
+ lambda state: state.has_all(["Uria, Lord of Searing Flames",
+ "Embodiment of Apophis",
+ "Skull Zoma",
+ "Metal Reflect Slime"], player)
+ and yugioh06_difficulty(state, player, 3),
+ "TD38 Hamon, Lord of Striking Thunder":
+ lambda state: state.has("Hamon, Lord of Striking Thunder", player)
+ and yugioh06_difficulty(state, player, 3),
+ "TD39 Raviel, Lord of Phantasms":
+ lambda state: state.has_all(["Raviel, Lord of Phantasms", "Giant Germ"], player) and
+ state.count_from_list_unique(["Archfiend Soldier",
+ "Skull Descovery Knight",
+ "Slate Warrior",
+ "D. D. Trainer",
+ "Earthbound Spirit"], player) >= 3
+ and yugioh06_difficulty(state, player, 3),
+ "TD40 Make a Chain":
+ lambda state: state.has("Ultimate Offering", player)
+ and yugioh06_difficulty(state, player, 4),
+ "TD41 The Gatekeeper Stands Tall":
+ lambda state: state.has("Gate Guardian and its pieces", player) and
+ state.has_all(["Treeborn Frog", "Tribute Doll"], player)
+ and yugioh06_difficulty(state, player, 4),
+ "TD42 Serious Damage":
+ lambda state: yugioh06_difficulty(state, player, 3),
+ "TD43 Return Monsters with Effects":
+ lambda state: state.has_all(["Penguin Soldier", "Messenger of Peace"], player)
+ and yugioh06_difficulty(state, player, 4),
+ "TD44 Fusion Summon":
+ lambda state: state.has_all(["Fusion Gate", "Terraforming", "Dimension Fusion",
+ "Return from the Different Dimension"], player) and
+ count_has_materials(state, ["Elemental Hero Flame Wingman",
+ "Elemental Hero Madballman",
+ "Elemental Hero Rampart Blaster",
+ "Elemental Hero Steam Healer",
+ "Elemental Hero Shining Flare Wingman",
+ "Elemental Hero Wildedge"], player) >= 4 and
+ yugioh06_difficulty(state, player, 7),
+ "TD45 Big Damage at once":
+ lambda state: state.has("Wave-Motion Cannon", player)
+ and yugioh06_difficulty(state, player, 3),
+ "TD46 XYZ In the House":
+ lambda state: state.has("XYZ-Dragon Cannon fusions and their materials", player) and
+ state.has("Dimension Fusion", player),
+ "TD47 Spell Counter":
+ lambda state: spell_counter(state, player) and yugioh06_difficulty(state, player, 3),
+ "TD48 Destroy Monsters with Effects":
+ lambda state: state.has_all(["Blade Rabbit", "Dream Clown"], player) and
+ state.has("Can Stall with ST", player) and
+ yugioh06_difficulty(state, player, 3),
+ "TD49 Plunder":
+ lambda state: take_control(state, player) and yugioh06_difficulty(state, player, 5),
+ "TD50 Dark Scorpion Combination":
+ lambda state: state.has("Dark Scorpion Combination and its required cards", player) and
+ state.has_all(["Reinforcement of the Army", "Mystic Tomato"], player) and
+ yugioh06_difficulty(state, player, 3)
+ }
+ multiworld.completion_condition[player] = lambda state: state.has("Goal", player)
+
+ for loc in multiworld.get_locations(player):
+ if loc.name in location_rules:
+ add_rule(loc, location_rules[loc.name])
+ if loc.name in access_rules:
+ add_rule(multiworld.get_entrance(loc.name, player), access_rules[loc.name])
+
+
+def only_light(state, player):
+ return state.has_from_list_unique([
+ "Dunames Dark Witch",
+ "X-Head Cannon",
+ "Homunculus the Alchemic Being",
+ "Hysteric Fairy",
+ "Ninja Grandmaster Sasuke"], player, 2)\
+ and state.has_from_list_unique([
+ "Chaos Command Magician",
+ "Cybernetic Magician",
+ "Kaiser Glider",
+ "The Agent of Judgment - Saturn",
+ "Zaborg the Thunder Monarch",
+ "Cyber Dragon"], player, 1) \
+ and state.has_from_list_unique([
+ "D.D. Warrior Lady",
+ "Mystic Swordsman LV2",
+ "Y-Dragon Head",
+ "Z-Metal Tank",
+ ], player, 2) and state.has("Shining Angel", player)
+
+
+def only_dark(state, player):
+ return state.has_from_list_unique([
+ "Dark Elf",
+ "Archfiend Soldier",
+ "Mad Dog of Darkness",
+ "Vorse Raider",
+ "Skilled Dark Magician",
+ "Skull Descovery Knight",
+ "Mechanicalchaser",
+ "Dark Blade",
+ "Gil Garth",
+ "La Jinn the Mystical Genie of the Lamp",
+ "Opticlops",
+ "Zure, Knight of Dark World",
+ "Brron, Mad King of Dark World",
+ "D.D. Survivor",
+ "Exarion Universe",
+ "Kycoo the Ghost Destroyer",
+ "Regenerating Mummy"
+ ], player, 2) \
+ and state.has_any([
+ "Summoned Skull",
+ "Skull Archfiend of Lightning",
+ "The End of Anubis",
+ "Dark Ruler Ha Des",
+ "Beast of Talwar",
+ "Inferno Hammer",
+ "Jinzo",
+ "Ryu Kokki"
+ ], player) \
+ and state.has_from_list_unique([
+ "Legendary Fiend",
+ "Don Zaloog",
+ "Newdoria",
+ "Sangan",
+ "Spirit Reaper",
+ "Giant Germ"
+ ], player, 2) and state.has("Mystic Tomato", player)
+
+
+def only_earth(state, player):
+ return state.has_from_list_unique([
+ "Berserk Gorilla",
+ "Gemini Elf",
+ "Insect Knight",
+ "Toon Gemini Elf",
+ "Familiar-Possessed - Aussa",
+ "Neo Bug",
+ "Blindly Loyal Goblin",
+ "Chiron the Mage",
+ "Gearfried the Iron Knight"
+ ], player, 2) and state.has_any([
+ "Dark Driceratops",
+ "Granmarg the Rock Monarch",
+ "Hieracosphinx",
+ "Saber Beetle"
+ ], player) and state.has_from_list_unique([
+ "Hyper Hammerhead",
+ "Green Gadget",
+ "Red Gadget",
+ "Yellow Gadget",
+ "Dimensional Warrior",
+ "Enraged Muka Muka",
+ "Exiled Force"
+ ], player, 2) and state.has("Giant Rat", player)
+
+
+def only_water(state, player):
+ return state.has_from_list_unique([
+ "Gagagigo",
+ "Familiar-Possessed - Eria",
+ "7 Colored Fish",
+ "Sea Serpent Warrior of Darkness",
+ "Abyss Soldier"
+ ], player, 2) and state.has_any([
+ "Giga Gagagigo",
+ "Amphibian Beast",
+ "Terrorking Salmon",
+ "Mobius the Frost Monarch"
+ ], player) and state.has_from_list_unique([
+ "Revival Jam",
+ "Yomi Ship",
+ "Treeborn Frog"
+ ], player, 2) and state.has("Mother Grizzly", player)
+
+
+def only_fire(state, player):
+ return state.has_from_list_unique([
+ "Blazing Inpachi",
+ "Familiar-Possessed - Hiita",
+ "Great Angus",
+ "Fire Beaters"
+ ], player, 2) and state.has_any([
+ "Thestalos the Firestorm Monarch",
+ "Horus the Black Flame Dragon LV6"
+ ], player) and state.has_from_list_unique([
+ "Solar Flare Dragon",
+ "Tenkabito Shien",
+ "Ultimate Baseball Kid"
+ ], player, 2) and state.has("UFO Turtle", player)
+
+
+def only_wind(state, player):
+ return state.has_from_list_unique([
+ "Luster Dragon",
+ "Slate Warrior",
+ "Spear Dragon",
+ "Familiar-Possessed - Wynn",
+ "Harpie's Brother",
+ "Nin-Ken Dog",
+ "Cyber Harpie Lady",
+ "Oxygeddon"
+ ], player, 2) and state.has_any([
+ "Cyber-Tech Alligator",
+ "Luster Dragon #2",
+ "Armed Dragon LV5",
+ "Roc from the Valley of Haze"
+ ], player) and state.has_from_list_unique([
+ "Armed Dragon LV3",
+ "Twin-Headed Behemoth",
+ "Harpie Lady 1"
+ ], player, 2) and state.has("Flying Kamakiri 1", player)
+
+
+def only_fairy(state, player):
+ return state.has_any([
+ "Dunames Dark Witch",
+ "Hysteric Fairy"
+ ], player) and (state.count_from_list_unique([
+ "Dunames Dark Witch",
+ "Hysteric Fairy",
+ "Dancing Fairy",
+ "Zolga",
+ "Shining Angel",
+ "Kelbek",
+ "Mudora",
+ "Asura Priest",
+ "Cestus of Dagla"
+ ], player) + (state.has_any([
+ "The Agent of Judgment - Saturn",
+ "Airknight Parshath"
+ ], player))) >= 7
+
+
+def only_warrior(state, player):
+ return state.has_any([
+ "Dark Blade",
+ "Blindly Loyal Goblin",
+ "D.D. Survivor",
+ "Gearfried the Iron knight",
+ "Ninja Grandmaster Sasuke",
+ "Warrior Beaters"
+ ], player) and (state.count_from_list_unique([
+ "Warrior Lady of the Wasteland",
+ "Exiled Force",
+ "Mystic Swordsman LV2",
+ "Dimensional Warrior",
+ "Dandylion",
+ "D.D. Assailant",
+ "Blade Knight",
+ "D.D. Warrior Lady",
+ "Marauding Captain",
+ "Command Knight",
+ "Reinforcement of the Army"
+ ], player) + (state.has_any([
+ "Freed the Matchless General",
+ "Holy Knight Ishzark",
+ "Silent Swordsman Lv5"
+ ], player))) >= 7
+
+
+def only_zombie(state, player):
+ return state.has("Pyramid Turtle", player) \
+ and state.has_from_list_unique([
+ "Regenerating Mummy",
+ "Ryu Kokki",
+ "Spirit Reaper",
+ "Master Kyonshee",
+ "Curse of Vampire",
+ "Vampire Lord",
+ "Goblin Zombie",
+ "Curse of Vampire",
+ "Vampire Lord",
+ "Goblin Zombie",
+ "Book of Life",
+ "Call of the Mummy"
+ ], player, 6)
+
+
+def only_dragon(state, player):
+ return state.has_any([
+ "Luster Dragon",
+ "Spear Dragon",
+ "Cave Dragon"
+ ], player) and (state.count_from_list_unique([
+ "Luster Dragon",
+ "Spear Dragon",
+ "Cave Dragon"
+ "Armed Dragon LV3",
+ "Masked Dragon",
+ "Twin-Headed Behemoth",
+ "Element Dragon",
+ "Troop Dragon",
+ "Horus the Black Flame Dragon LV4",
+ "Stamping Destruction"
+ ], player) + (state.has_any([
+ "Luster Dragon #2",
+ "Armed Dragon LV5",
+ "Kaiser Glider",
+ "Horus the Black Flame Dragon LV6"
+ ], player))) >= 7
+
+
+def only_spellcaster(state, player):
+ return state.has_any([
+ "Dark Elf",
+ "Gemini Elf",
+ "Skilled Dark Magician",
+ "Toon Gemini Elf",
+ "Kycoo the Ghost Destroyer",
+ "Familiar-Possessed - Aussa"
+ ], player) and (state.count_from_list_unique([
+ "Dark Elf",
+ "Gemini Elf",
+ "Skilled Dark Magician",
+ "Toon Gemini Elf",
+ "Kycoo the Ghost Destroyer",
+ "Familiar-Possessed - Aussa",
+ "Breaker the magical Warrior",
+ "The Tricky",
+ "Injection Fairy Lily",
+ "Magician of Faith",
+ "Tsukuyomi",
+ "Gravekeeper's Spy",
+ "Gravekeeper's Guard",
+ "Summon Priest",
+ "Old Vindictive Magician",
+ "Apprentice Magician",
+ "Magical Dimension"
+ ], player) + (state.has_any([
+ "Chaos Command Magician",
+ "Cybernetic Magician"
+ ], player))) >= 7
+
+
+def equip_unions(state, player):
+ return (state.has_all(["Burning Beast", "Freezing Beast",
+ "Metallizing Parasite - Lunatite", "Mother Grizzly"], player) or
+ state.has_all(["Dark Blade", "Pitch-Dark Dragon",
+ "Giant Orc", "Second Goblin", "Mystic Tomato"], player) or
+ state.has_all(["Decayed Commander", "Zombie Tiger",
+ "Vampire Orchis", "Des Dendle", "Giant Rat"], player) or
+ state.has_all(["Indomitable Fighter Lei Lei", "Protective Soul Ailin",
+ "V-Tiger Jet", "W-Wing Catapult", "Shining Angel"], player) or
+ state.has_all(["X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank", "Shining Angel"], player)) and\
+ state.has_any(["Frontline Base", "Formation Union", "Roll Out!"], player)
+
+
+def can_gain_lp_every_turn(state, player):
+ return state.count_from_list_unique([
+ "Solemn Wishes",
+ "Cure Mermaid",
+ "Dancing Fairy",
+ "Princess Pikeru",
+ "Kiseitai"], player) >= 3
+
+
+def only_normal(state, player):
+ return (state.has_from_list_unique([
+ "Archfiend Soldier",
+ "Gemini Elf",
+ "Insect Knight",
+ "Luster Dragon",
+ "Mad Dog of Darkness",
+ "Vorse Raider",
+ "Blazing Inpachi",
+ "Gagagigo",
+ "Mechanicalchaser",
+ "7 Colored Fish",
+ "Dark Blade",
+ "Dunames Dark Witch",
+ "Giant Red Snake",
+ "Gil Garth",
+ "Great Angus",
+ "Harpie's Brother",
+ "La Jinn the Mystical Genie of the Lamp",
+ "Neo Bug",
+ "Nin-Ken Dog",
+ "Opticlops",
+ "Sea Serpent Warrior of Darkness",
+ "X-Head Cannon",
+ "Zure, Knight of Dark World"], player, 6) and
+ state.has_any([
+ "Cyber-Tech Alligator",
+ "Summoned Skull",
+ "Giga Gagagigo",
+ "Amphibian Beast",
+ "Beast of Talwar",
+ "Luster Dragon #2",
+ "Terrorking Salmon"], player))
+
+
+def only_level(state, player):
+ return (state.has("Level Up!", player) and
+ (state.has_all(["Armed Dragon LV3", "Armed Dragon LV5"], player) +
+ state.has_all(["Horus the Black Flame Dragon LV4", "Horus the Black Flame Dragon LV6"], player) +
+ state.has_all(["Mystic Swordsman LV4", "Mystic Swordsman LV6"], player) +
+ state.has_all(["Silent Swordsman Lv3", "Silent Swordsman Lv5"], player) +
+ state.has_all(["Ultimate Insect Lv3", "Ultimate Insect Lv5"], player)) >= 3)
+
+
+def spell_counter(state, player):
+ return (state.has("Pitch-Black Power Stone", player) and
+ state.has_from_list_unique(["Blast Magician",
+ "Magical Marionette",
+ "Mythical Beast Cerberus",
+ "Royal Magical Library",
+ "Spell-Counter Cards"], player, 2))
+
+
+def take_control(state, player):
+ return state.has_from_list_unique(["Aussa the Earth Charmer",
+ "Jowls of Dark Demise",
+ "Brain Control",
+ "Creature Swap",
+ "Enemy Controller",
+ "Mind Control",
+ "Magician of Faith"], player, 5)
+
+
+def only_toons(state, player):
+ return state.has_all(["Toon Gemini Elf",
+ "Toon Goblin Attack Force",
+ "Toon Masked Sorcerer",
+ "Toon Mermaid",
+ "Toon Dark Magician Girl",
+ "Toon World"], player)
+
+
+def only_spirit(state, player):
+ return state.has_all(["Asura Priest",
+ "Fushi No Tori",
+ "Maharaghi",
+ "Susa Soldier"], player)
+
+
+def pacman_deck(state, player):
+ return state.has_from_list_unique(["Des Lacooda",
+ "Swarm of Locusts",
+ "Swarm of Scarabs",
+ "Wandering Mummy",
+ "Golem Sentry",
+ "Great Spirit",
+ "Royal Keeper",
+ "Stealth Bird"], player, 4)
+
+
+def quick_plays(state, player):
+ return state.has_from_list_unique(["Collapse",
+ "Emergency Provisions",
+ "Enemy Controller",
+ "Graceful Dice",
+ "Mystik Wok",
+ "Offerings to the Doomed",
+ "Poison of the Old Man",
+ "Reload",
+ "Rush Recklessly",
+ "The Reliable Guardian"], player, 4)
+
+
+def counter_traps(state, player):
+ return state.has_from_list_unique(["Cursed Seal of the Forbidden Spell",
+ "Divine Wrath",
+ "Horn of Heaven",
+ "Magic Drain",
+ "Magic Jammer",
+ "Negate Attack",
+ "Seven Tools of the Bandit",
+ "Solemn Judgment",
+ "Spell Shield Type-8"], player, 5)
+
+
+def back_row_removal(state, player):
+ return state.has_from_list_unique(["Anteatereatingant",
+ "B.E.S. Tetran",
+ "Breaker the Magical Warrior",
+ "Calamity of the Wicked",
+ "Chiron the Mage",
+ "Dust Tornado",
+ "Heavy Storm",
+ "Mystical Space Typhoon",
+ "Mobius the Frost Monarch",
+ "Raigeki Break",
+ "Stamping Destruction",
+ "Swarm of Locusts"], player, 2)
diff --git a/worlds/yugioh06/structure_deck.py b/worlds/yugioh06/structure_deck.py
new file mode 100644
index 000000000000..3559e7c5153e
--- /dev/null
+++ b/worlds/yugioh06/structure_deck.py
@@ -0,0 +1,87 @@
+from typing import Dict, List
+
+structure_contents: Dict[str, List[str]] = {
+ "dragons_roar": [
+ "Luster Dragon",
+ "Armed Dragon LV3",
+ "Armed Dragon LV5",
+ "Masked Dragon",
+ "Twin-Headed Behemoth",
+ "Stamping Destruction",
+ "Nobleman of Crossout",
+ "Creature Swap",
+ "Reload",
+ "Stamping Destruction",
+ "Heavy Storm",
+ "Dust Tornado",
+ "Mystical Space Typhoon"
+ ],
+ "zombie_madness": [
+ "Pyramid Turtle",
+ "Regenerating Mummy",
+ "Ryu Kokki",
+ "Book of Life",
+ "Call of the Mummy",
+ "Creature Swap",
+ "Reload",
+ "Heavy Storm",
+ "Dust Tornado",
+ "Mystical Space Typhoon"
+ ],
+ "blazing_destruction": [
+ "Inferno",
+ "Solar Flare Dragon",
+ "UFO Turtle",
+ "Ultimate Baseball Kid",
+ "Fire Beaters",
+ "Tribute to The Doomed",
+ "Level Limit - Area B",
+ "Heavy Storm",
+ "Dust Tornado",
+ "Mystical Space Typhoon"
+ ],
+ "fury_from_the_deep": [
+ "Mother Grizzly",
+ "Water Beaters",
+ "Gravity Bind",
+ "Reload",
+ "Mobius the Frost Monarch",
+ "Heavy Storm",
+ "Dust Tornado",
+ "Mystical Space Typhoon"
+ ],
+ "warriors_triumph": [
+ "Gearfried the Iron Knight",
+ "D.D. Warrior Lady",
+ "Marauding Captain",
+ "Exiled Force",
+ "Reinforcement of the Army",
+ "Warrior Beaters",
+ "Reload",
+ "Heavy Storm",
+ "Dust Tornado",
+ "Mystical Space Typhoon"
+ ],
+ "spellcasters_judgement": [
+ "Dark Magician",
+ "Apprentice Magician",
+ "Breaker the Magical Warrior",
+ "Magician of Faith",
+ "Skilled Dark Magician",
+ "Tsukuyomi",
+ "Magical Dimension",
+ "Mage Power",
+ "Spell-Counter Cards",
+ "Heavy Storm",
+ "Dust Tornado",
+ "Mystical Space Typhoon"
+ ],
+ "none": [],
+}
+
+
+def get_deck_content_locations(deck: str) -> Dict[str, str]:
+ return {
+ f"{deck} {i}": content
+ for i, content in enumerate(structure_contents[deck], 1)
+ }
diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py
index 7c927c10eb92..cf61d93ca4ce 100644
--- a/worlds/zillion/__init__.py
+++ b/worlds/zillion/__init__.py
@@ -4,21 +4,22 @@
import settings
import threading
import typing
-from typing import Any, Dict, List, Literal, Set, Tuple, Optional, cast
+from typing import Any, Dict, List, Set, Tuple, Optional
import os
import logging
from BaseClasses import ItemClassification, LocationProgressType, \
MultiWorld, Item, CollectionState, Entrance, Tutorial
-from .config import detect_test
+
+from .gen_data import GenData
from .logic import cs_to_zz_locs
from .region import ZillionLocation, ZillionRegion
-from .options import ZillionStartChar, zillion_options, validate
-from .id_maps import item_name_to_id as _item_name_to_id, \
+from .options import ZillionOptions, validate, z_option_groups
+from .id_maps import ZillionSlotInfo, get_slot_info, item_name_to_id as _item_name_to_id, \
loc_name_to_id as _loc_name_to_id, make_id_to_others, \
zz_reg_name_to_reg_name, base_id
from .item import ZillionItem
-from .patch import ZillionDeltaPatch, get_base_rom_path
+from .patch import ZillionPatch
from zilliandomizer.randomizer import Randomizer as ZzRandomizer
from zilliandomizer.system import System
@@ -26,7 +27,7 @@
from zilliandomizer.logic_components.locations import Location as ZzLocation, Req
from zilliandomizer.options import Chars
-from ..AutoWorld import World, WebWorld
+from worlds.AutoWorld import World, WebWorld
class ZillionSettings(settings.Group):
@@ -34,7 +35,8 @@ class RomFile(settings.UserFilePath):
"""File name of the Zillion US rom"""
description = "Zillion US ROM File"
copy_to = "Zillion (UE) [!].sms"
- md5s = [ZillionDeltaPatch.hash]
+ assert ZillionPatch.hash
+ md5s = [ZillionPatch.hash]
class RomStart(str):
"""
@@ -60,6 +62,8 @@ class ZillionWebWorld(WebWorld):
["beauxq"]
)]
+ option_groups = z_option_groups
+
class ZillionWorld(World):
"""
@@ -70,19 +74,18 @@ class ZillionWorld(World):
game = "Zillion"
web = ZillionWebWorld()
- option_definitions = zillion_options
- settings: typing.ClassVar[ZillionSettings]
+ options_dataclass = ZillionOptions
+ options: ZillionOptions # type: ignore
+
+ settings: typing.ClassVar[ZillionSettings] # type: ignore
+ # these type: ignore are because of this issue: https://github.com/python/typing/discussions/1486
+
topology_present = True # indicate if world type has any meaningful layout/pathing
# map names to their IDs
item_name_to_id = _item_name_to_id
location_name_to_id = _loc_name_to_id
- # increment this every time something in your world's names/id mappings changes.
- # While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
- # retrieved by clients on every connection.
- data_version = 1
-
logger: logging.Logger
class LogStreamInterface:
@@ -130,29 +133,22 @@ def _make_item_maps(self, start_char: Chars) -> None:
_id_to_name, _id_to_zz_id, id_to_zz_item = make_id_to_others(start_char)
self.id_to_zz_item = id_to_zz_item
- @classmethod
- def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
- """Checks that a game is capable of generating, usually checks for some base file like a ROM.
- Not run for unittests since they don't produce output"""
- rom_file = get_base_rom_path()
- if not os.path.exists(rom_file):
- raise FileNotFoundError(rom_file)
-
def generate_early(self) -> None:
if not hasattr(self.multiworld, "zillion_logic_cache"):
setattr(self.multiworld, "zillion_logic_cache", {})
- zz_op, item_counts = validate(self.multiworld, self.player)
+ zz_op, item_counts = validate(self.options)
+
+ if zz_op.early_scope:
+ self.multiworld.early_items[self.player]["Scope"] = 1
self._item_counts = item_counts
- rom_dir_name = "" if detect_test() else os.path.dirname(get_base_rom_path())
with redirect_stdout(self.lsi): # type: ignore
- self.zz_system.make_patcher(rom_dir_name)
- self.zz_system.make_randomizer(zz_op)
-
- self.zz_system.seed(self.multiworld.seed)
+ self.zz_system.set_options(zz_op)
+ self.zz_system.seed(self.random.randrange(1999999999))
self.zz_system.make_map()
+ self.zz_system.make_randomizer()
# just in case the options changed anything (I don't think they do)
assert self.zz_system.randomizer, "init failed"
@@ -220,7 +216,7 @@ def access_rule_wrapped(zz_loc_local: ZzLocation,
loc.access_rule = access_rule
if not (limited_skill >= zz_loc.req):
loc.progress_type = LocationProgressType.EXCLUDED
- self.multiworld.exclude_locations[p].value.add(loc.name)
+ self.options.exclude_locations.value.add(loc.name)
here.locations.append(loc)
self.my_locations.append(loc)
@@ -283,15 +279,15 @@ def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None:
if group["game"] == "Zillion":
assert "item_pool" in group
item_pool = group["item_pool"]
- to_stay: Literal['Apple', 'Champ', 'JJ'] = "JJ"
+ to_stay: Chars = "JJ"
if "JJ" in item_pool:
assert "players" in group
group_players = group["players"]
- start_chars = cast(Dict[int, ZillionStartChar], getattr(multiworld, "start_char"))
- players_start_chars = [
- (player, start_chars[player].current_option_name)
- for player in group_players
- ]
+ players_start_chars: List[Tuple[int, Chars]] = []
+ for player in group_players:
+ z_world = multiworld.worlds[player]
+ assert isinstance(z_world, ZillionWorld)
+ players_start_chars.append((player, z_world.options.start_char.get_char()))
start_char_counts = Counter(sc for _, sc in players_start_chars)
# majority rules
if start_char_counts["Apple"] > start_char_counts["Champ"]:
@@ -299,13 +295,16 @@ def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None:
elif start_char_counts["Champ"] > start_char_counts["Apple"]:
to_stay = "Champ"
else: # equal
- to_stay = multiworld.random.choice(("Apple", "Champ"))
+ choices: Tuple[Chars, ...] = ("Apple", "Champ")
+ to_stay = multiworld.random.choice(choices)
for p, sc in players_start_chars:
if sc != to_stay:
group_players.remove(p)
assert "world" in group
- cast(ZillionWorld, group["world"])._make_item_maps(to_stay)
+ group_world = group["world"]
+ assert isinstance(group_world, ZillionWorld)
+ group_world._make_item_maps(to_stay)
def post_fill(self) -> None:
"""Optional Method that is called after regular fill. Can be used to do adjustments before output generation.
@@ -313,34 +312,36 @@ def post_fill(self) -> None:
self.zz_system.post_fill()
- def finalize_item_locations(self) -> None:
+ def finalize_item_locations(self) -> GenData:
"""
sync zilliandomizer item locations with AP item locations
+
+ return the data needed to generate output
"""
- assert self.zz_system.randomizer and self.zz_system.patcher, "generate_early hasn't been called"
- zz_options = self.zz_system.randomizer.options
+
+ assert self.zz_system.randomizer, "generate_early hasn't been called"
# debug_zz_loc_ids: Dict[str, int] = {}
empty = zz_items[4]
multi_item = empty # a different patcher method differentiates empty from ap multi item
multi_items: Dict[str, Tuple[str, str]] = {} # zz_loc_name to (item_name, player_name)
- for loc in self.multiworld.get_locations():
- if loc.player == self.player:
- z_loc = cast(ZillionLocation, loc)
- # debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc)
- if z_loc.item is None:
- self.logger.warn("generate_output location has no item - is that ok?")
- z_loc.zz_loc.item = empty
- elif z_loc.item.player == self.player:
- z_item = cast(ZillionItem, z_loc.item)
- z_loc.zz_loc.item = z_item.zz_item
- else: # another player's item
- # print(f"put multi item in {z_loc.zz_loc.name}")
- z_loc.zz_loc.item = multi_item
- multi_items[z_loc.zz_loc.name] = (
- z_loc.item.name,
- self.multiworld.get_player_name(z_loc.item.player)
- )
+ for z_loc in self.multiworld.get_locations(self.player):
+ assert isinstance(z_loc, ZillionLocation)
+ # debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc)
+ if z_loc.item is None:
+ self.logger.warning("generate_output location has no item - is that ok?")
+ z_loc.zz_loc.item = empty
+ elif z_loc.item.player == self.player:
+ z_item = z_loc.item
+ assert isinstance(z_item, ZillionItem)
+ z_loc.zz_loc.item = z_item.zz_item
+ else: # another player's item
+ # print(f"put multi item in {z_loc.zz_loc.name}")
+ z_loc.zz_loc.item = multi_item
+ multi_items[z_loc.zz_loc.name] = (
+ z_loc.item.name,
+ self.multiworld.get_player_name(z_loc.item.player)
+ )
# debug_zz_loc_ids.sort()
# for name, id_ in debug_zz_loc_ids.items():
# print(id_)
@@ -361,45 +362,32 @@ def finalize_item_locations(self) -> None:
f"in world {self.player} didn't get an item"
)
- zz_patcher = self.zz_system.patcher
-
- zz_patcher.write_locations(self.zz_system.randomizer.regions,
- zz_options.start_char,
- self.zz_system.randomizer.loc_name_2_pretty)
- self.slot_data_ready.set()
- zz_patcher.all_fixes_and_options(zz_options)
- zz_patcher.set_external_item_interface(zz_options.start_char, zz_options.max_level)
- zz_patcher.set_multiworld_items(multi_items)
game_id = self.multiworld.player_name[self.player].encode() + b'\x00' + self.multiworld.seed_name[-6:].encode()
- zz_patcher.set_rom_to_ram_data(game_id)
- def generate_output(self, output_directory: str) -> None:
- """This method gets called from a threadpool, do not use world.random here.
- If you need any last-second randomization, use MultiWorld.per_slot_randoms[slot] instead."""
- self.finalize_item_locations()
+ return GenData(multi_items, self.zz_system.get_game(), game_id)
- assert self.zz_system.patcher, "didn't get patcher from generate_early"
- # original_rom_bytes = self.zz_patcher.rom
- patched_rom_bytes = self.zz_system.patcher.get_patched_bytes()
+ def generate_output(self, output_directory: str) -> None:
+ """This method gets called from a threadpool, do not use multiworld.random here.
+ If you need any last-second randomization, use self.random instead."""
+ try:
+ gen_data = self.finalize_item_locations()
+ except BaseException:
+ raise
+ finally:
+ self.slot_data_ready.set()
out_file_base = self.multiworld.get_out_file_name_base(self.player)
- filename = os.path.join(
- output_directory,
- f'{out_file_base}{ZillionDeltaPatch.result_file_ending}'
- )
- with open(filename, "wb") as binary_file:
- binary_file.write(patched_rom_bytes)
- patch = ZillionDeltaPatch(
- os.path.splitext(filename)[0] + ZillionDeltaPatch.patch_file_ending,
- player=self.player,
- player_name=self.multiworld.player_name[self.player],
- patched_path=filename
- )
+ patch_file_name = os.path.join(output_directory, f"{out_file_base}{ZillionPatch.patch_file_ending}")
+ patch = ZillionPatch(patch_file_name,
+ player=self.player,
+ player_name=self.multiworld.player_name[self.player],
+ gen_data_str=gen_data.to_json())
patch.write()
- os.remove(filename)
- def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot
+ self.logger.debug(f"Zillion player {self.player} finished generate_output")
+
+ def fill_slot_data(self) -> ZillionSlotInfo: # json of WebHostLib.models.Slot
"""Fill in the `slot_data` field in the `Connected` network package.
This is a way the generator can give custom data to the client.
The client will receive this as JSON in the `Connected` response."""
@@ -409,25 +397,10 @@ def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot
# TODO: tell client which canisters are keywords
# so it can open and get those when restoring doors
- zz_patcher = self.zz_system.patcher
- assert zz_patcher, "didn't get patcher from generate_early"
- assert self.zz_system.randomizer, "didn't get randomizer from generate_early"
-
- rescues: Dict[str, Any] = {}
self.slot_data_ready.wait()
- for i in (0, 1):
- if i in zz_patcher.rescue_locations:
- ri = zz_patcher.rescue_locations[i]
- rescues[str(i)] = {
- "start_char": ri.start_char,
- "room_code": ri.room_code,
- "mask": ri.mask
- }
- return {
- "start_char": self.zz_system.randomizer.options.start_char,
- "rescues": rescues,
- "loc_mem_to_id": zz_patcher.loc_memory_to_loc_id
- }
+ assert self.zz_system.randomizer, "didn't get randomizer from generate_early"
+ game = self.zz_system.get_game()
+ return get_slot_info(game.regions, game.char_order[0], game.loc_name_2_pretty)
# def modify_multidata(self, multidata: Dict[str, Any]) -> None:
# """For deeper modification of server multidata."""
diff --git a/worlds/zillion/client.py b/worlds/zillion/client.py
new file mode 100644
index 000000000000..09d0565e1c5e
--- /dev/null
+++ b/worlds/zillion/client.py
@@ -0,0 +1,518 @@
+import asyncio
+import base64
+import io
+import pkgutil
+import platform
+from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast
+
+from CommonClient import CommonContext, server_loop, gui_enabled, \
+ ClientCommandProcessor, logger, get_base_parser
+from NetUtils import ClientStatus
+from Utils import async_start
+
+import colorama
+
+from zilliandomizer.zri.memory import Memory, RescueInfo
+from zilliandomizer.zri import events
+from zilliandomizer.utils.loc_name_maps import id_to_loc
+from zilliandomizer.options import Chars
+
+from .id_maps import loc_name_to_id, make_id_to_others
+from .config import base_id
+
+
+class ZillionCommandProcessor(ClientCommandProcessor):
+ ctx: "ZillionContext"
+
+ def _cmd_sms(self) -> None:
+ """ Tell the client that Zillion is running in RetroArch. """
+ logger.info("ready to look for game")
+ self.ctx.look_for_retroarch.set()
+
+ def _cmd_map(self) -> None:
+ """ Toggle view of the map tracker. """
+ self.ctx.ui_toggle_map()
+
+
+class ToggleCallback(Protocol):
+ def __call__(self) -> None: ...
+
+
+class SetRoomCallback(Protocol):
+ def __call__(self, rooms: List[List[int]]) -> None: ...
+
+
+class ZillionContext(CommonContext):
+ game = "Zillion"
+ command_processor = ZillionCommandProcessor
+ items_handling = 1 # receive items from other players
+
+ known_name: Optional[str]
+ """ This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """
+
+ from_game: "asyncio.Queue[events.EventFromGame]"
+ to_game: "asyncio.Queue[events.EventToGame]"
+ ap_local_count: int
+ """ local checks watched by server """
+ next_item: int
+ """ index in `items_received` """
+ ap_id_to_name: Dict[int, str]
+ ap_id_to_zz_id: Dict[int, int]
+ start_char: Chars = "JJ"
+ rescues: Dict[int, RescueInfo] = {}
+ loc_mem_to_id: Dict[int, int] = {}
+ got_room_info: asyncio.Event
+ """ flag for connected to server """
+ got_slot_data: asyncio.Event
+ """ serves as a flag for whether I am logged in to the server """
+
+ look_for_retroarch: asyncio.Event
+ """
+ There is a bug in Python in Windows
+ https://github.com/python/cpython/issues/91227
+ that makes it so if I look for RetroArch before it's ready,
+ it breaks the asyncio udp transport system.
+
+ As a workaround, we don't look for RetroArch until this event is set.
+ """
+
+ ui_toggle_map: ToggleCallback
+ ui_set_rooms: SetRoomCallback
+ """ parameter is y 16 x 8 numbers to show in each room """
+
+ def __init__(self,
+ server_address: str,
+ password: str) -> None:
+ super().__init__(server_address, password)
+ self.known_name = None
+ self.from_game = asyncio.Queue()
+ self.to_game = asyncio.Queue()
+ self.got_room_info = asyncio.Event()
+ self.got_slot_data = asyncio.Event()
+ self.ui_toggle_map = lambda: None
+ self.ui_set_rooms = lambda rooms: None
+
+ self.look_for_retroarch = asyncio.Event()
+ if platform.system() != "Windows":
+ # asyncio udp bug is only on Windows
+ self.look_for_retroarch.set()
+
+ self.reset_game_state()
+
+ def reset_game_state(self) -> None:
+ for _ in range(self.from_game.qsize()):
+ self.from_game.get_nowait()
+ for _ in range(self.to_game.qsize()):
+ self.to_game.get_nowait()
+ self.got_slot_data.clear()
+
+ self.ap_local_count = 0
+ self.next_item = 0
+ self.ap_id_to_name = {}
+ self.ap_id_to_zz_id = {}
+ self.rescues = {}
+ self.loc_mem_to_id = {}
+
+ self.locations_checked.clear()
+ self.missing_locations.clear()
+ self.checked_locations.clear()
+ self.finished_game = False
+ self.items_received.clear()
+
+ # override
+ def on_deathlink(self, data: Dict[str, Any]) -> None:
+ self.to_game.put_nowait(events.DeathEventToGame())
+ return super().on_deathlink(data)
+
+ # override
+ async def server_auth(self, password_requested: bool = False) -> None:
+ if password_requested and not self.password:
+ await super().server_auth(password_requested)
+ if not self.auth:
+ logger.info('waiting for connection to game...')
+ return
+ logger.info("logging in to server...")
+ await self.send_connect()
+
+ # override
+ def run_gui(self) -> None:
+ from kvui import GameManager
+ from kivy.core.text import Label as CoreLabel
+ from kivy.graphics import Ellipse, Color, Rectangle
+ from kivy.graphics.texture import Texture
+ from kivy.uix.layout import Layout
+ from kivy.uix.image import CoreImage
+ from kivy.uix.widget import Widget
+
+ class ZillionManager(GameManager):
+ logging_pairs = [
+ ("Client", "Archipelago")
+ ]
+ base_title = "Archipelago Zillion Client"
+
+ class MapPanel(Widget):
+ MAP_WIDTH: ClassVar[int] = 281
+
+ map_background: CoreImage
+ _number_textures: List[Texture] = []
+ rooms: List[List[int]] = []
+
+ def __init__(self, **kwargs: Any) -> None:
+ super().__init__(**kwargs)
+
+ FILE_NAME = "empty-zillion-map-row-col-labels-281.png"
+ image_file_data = pkgutil.get_data(__name__, FILE_NAME)
+ if not image_file_data:
+ raise FileNotFoundError(f"{__name__=} {FILE_NAME=}")
+ data = io.BytesIO(image_file_data)
+ self.map_background = CoreImage(data, ext="png")
+ assert self.map_background.texture.size[0] == ZillionManager.MapPanel.MAP_WIDTH
+
+ self.rooms = [[0 for _ in range(8)] for _ in range(16)]
+
+ self._make_numbers()
+ self.update_map()
+
+ self.bind(pos=self.update_map)
+ # self.bind(size=self.update_bg)
+
+ def _make_numbers(self) -> None:
+ self._number_textures = []
+ for n in range(10):
+ label = CoreLabel(text=str(n), font_size=22, color=(0.1, 0.9, 0, 1))
+ label.refresh()
+ self._number_textures.append(label.texture)
+
+ def update_map(self, *args: Any) -> None:
+ self.canvas.clear()
+
+ with self.canvas:
+ Color(1, 1, 1, 1)
+ Rectangle(texture=self.map_background.texture,
+ pos=self.pos,
+ size=self.map_background.texture.size)
+ for y in range(16):
+ for x in range(8):
+ num = self.rooms[15 - y][x]
+ if num > 0:
+ Color(0, 0, 0, 0.4)
+ pos = [self.pos[0] + 17 + x * 32, self.pos[1] + 14 + y * 24]
+ Ellipse(size=[22, 22], pos=pos)
+ Color(1, 1, 1, 1)
+ pos = [self.pos[0] + 22 + x * 32, self.pos[1] + 12 + y * 24]
+ num_texture = self._number_textures[num]
+ Rectangle(texture=num_texture, size=num_texture.size, pos=pos)
+
+ def build(self) -> Layout:
+ container = super().build()
+ self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=ZillionManager.MapPanel.MAP_WIDTH)
+ self.main_area_container.add_widget(self.map_widget)
+ return container
+
+ def toggle_map_width(self) -> None:
+ if self.map_widget.width == 0:
+ self.map_widget.width = ZillionManager.MapPanel.MAP_WIDTH
+ else:
+ self.map_widget.width = 0
+ self.container.do_layout()
+
+ def set_rooms(self, rooms: List[List[int]]) -> None:
+ self.map_widget.rooms = rooms
+ self.map_widget.update_map()
+
+ self.ui = ZillionManager(self)
+ self.ui_toggle_map = lambda: self.ui.toggle_map_width()
+ self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms)
+ run_co: Coroutine[Any, Any, None] = self.ui.async_run()
+ self.ui_task = asyncio.create_task(run_co, name="UI")
+
+ def on_package(self, cmd: str, args: Dict[str, Any]) -> None:
+ self.room_item_numbers_to_ui()
+ if cmd == "Connected":
+ logger.info("logged in to Archipelago server")
+ if "slot_data" not in args:
+ logger.warning("`Connected` packet missing `slot_data`")
+ return
+ slot_data = args["slot_data"]
+
+ if "start_char" not in slot_data:
+ logger.warning("invalid Zillion `Connected` packet, `slot_data` missing `start_char`")
+ return
+ self.start_char = slot_data['start_char']
+ if self.start_char not in {"Apple", "Champ", "JJ"}:
+ logger.warning("invalid Zillion `Connected` packet, "
+ f"`slot_data` `start_char` has invalid value: {self.start_char}")
+
+ if "rescues" not in slot_data:
+ logger.warning("invalid Zillion `Connected` packet, `slot_data` missing `rescues`")
+ return
+ rescues = slot_data["rescues"]
+ self.rescues = {}
+ for rescue_id, json_info in rescues.items():
+ assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}"
+ # TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch?
+ assert json_info["start_char"] == self.start_char, \
+ f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}'
+ ri = RescueInfo(json_info["start_char"],
+ json_info["room_code"],
+ json_info["mask"])
+ self.rescues[0 if rescue_id == "0" else 1] = ri
+
+ if "loc_mem_to_id" not in slot_data:
+ logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`")
+ return
+ loc_mem_to_id = slot_data["loc_mem_to_id"]
+ self.loc_mem_to_id = {}
+ for mem_str, id_str in loc_mem_to_id.items():
+ mem = int(mem_str)
+ id_ = int(id_str)
+ room_i = mem // 256
+ assert 0 <= room_i < 74
+ assert id_ in id_to_loc
+ self.loc_mem_to_id[mem] = id_
+
+ if len(self.loc_mem_to_id) != 394:
+ logger.warning("invalid Zillion `Connected` packet, "
+ f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}")
+
+ self.got_slot_data.set()
+
+ payload = {
+ "cmd": "Get",
+ "keys": [f"zillion-{self.auth}-doors"]
+ }
+ async_start(self.send_msgs([payload]))
+ elif cmd == "Retrieved":
+ if "keys" not in args:
+ logger.warning(f"invalid Retrieved packet to ZillionClient: {args}")
+ return
+ keys = cast(Dict[str, Optional[str]], args["keys"])
+ doors_b64 = keys.get(f"zillion-{self.auth}-doors", None)
+ if doors_b64:
+ logger.info("received door data from server")
+ doors = base64.b64decode(doors_b64)
+ self.to_game.put_nowait(events.DoorEventToGame(doors))
+ elif cmd == "RoomInfo":
+ self.seed_name = args["seed_name"]
+ self.got_room_info.set()
+
+ def room_item_numbers_to_ui(self) -> None:
+ rooms = [[0 for _ in range(8)] for _ in range(16)]
+ for loc_id in self.missing_locations:
+ loc_id_small = loc_id - base_id
+ loc_name = id_to_loc[loc_id_small]
+ y = ord(loc_name[0]) - 65
+ x = ord(loc_name[2]) - 49
+ if y == 9 and x == 5:
+ # don't show main computer in numbers
+ continue
+ assert (0 <= y < 16) and (0 <= x < 8), f"invalid index from location name {loc_name}"
+ rooms[y][x] += 1
+ # TODO: also add locations with locals lost from loading save state or reset
+ self.ui_set_rooms(rooms)
+
+ def process_from_game_queue(self) -> None:
+ if self.from_game.qsize():
+ event_from_game = self.from_game.get_nowait()
+ if isinstance(event_from_game, events.AcquireLocationEventFromGame):
+ server_id = event_from_game.id + base_id
+ loc_name = id_to_loc[event_from_game.id]
+ self.locations_checked.add(server_id)
+ if server_id in self.missing_locations:
+ self.ap_local_count += 1
+ n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win
+ logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})')
+ async_start(self.send_msgs([
+ {"cmd": 'LocationChecks', "locations": [server_id]}
+ ]))
+ else:
+ # This will happen a lot in Zillion,
+ # because all the key words are local and unwatched by the server.
+ logger.debug(f"DEBUG: {loc_name} not in missing")
+ elif isinstance(event_from_game, events.DeathEventFromGame):
+ async_start(self.send_death())
+ elif isinstance(event_from_game, events.WinEventFromGame):
+ if not self.finished_game:
+ async_start(self.send_msgs([
+ {"cmd": 'LocationChecks', "locations": [loc_name_to_id["J-6 bottom far left"]]},
+ {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}
+ ]))
+ self.finished_game = True
+ elif isinstance(event_from_game, events.DoorEventFromGame):
+ if self.auth:
+ doors_b64 = base64.b64encode(event_from_game.doors).decode()
+ payload = {
+ "cmd": "Set",
+ "key": f"zillion-{self.auth}-doors",
+ "operations": [{"operation": "replace", "value": doors_b64}]
+ }
+ async_start(self.send_msgs([payload]))
+ elif isinstance(event_from_game, events.MapEventFromGame):
+ row = event_from_game.map_index // 8
+ col = event_from_game.map_index % 8
+ room_name = f"({chr(row + 64)}-{col + 1})"
+ logger.info(f"You are at {room_name}")
+ else:
+ logger.warning(f"WARNING: unhandled event from game {event_from_game}")
+
+ def process_items_received(self) -> None:
+ if len(self.items_received) > self.next_item:
+ zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received]
+ for index in range(self.next_item, len(self.items_received)):
+ ap_id = self.items_received[index].item
+ from_name = self.player_names[self.items_received[index].player]
+ # TODO: colors in this text, like sni client?
+ logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}')
+ self.to_game.put_nowait(
+ events.ItemEventToGame(zz_item_ids)
+ )
+ self.next_item = len(self.items_received)
+
+
+def name_seed_from_ram(data: bytes) -> Tuple[str, str]:
+ """ returns player name, and end of seed string """
+ if len(data) == 0:
+ # no connection to game
+ return "", "xxx"
+ null_index = data.find(b'\x00')
+ if null_index == -1:
+ logger.warning(f"invalid game id in rom {repr(data)}")
+ null_index = len(data)
+ name = data[:null_index].decode()
+ null_index_2 = data.find(b'\x00', null_index + 1)
+ if null_index_2 == -1:
+ null_index_2 = len(data)
+ seed_name = data[null_index + 1:null_index_2].decode()
+
+ return name, seed_name
+
+
+async def zillion_sync_task(ctx: ZillionContext) -> None:
+ logger.info("started zillion sync task")
+
+ # to work around the Python bug where we can't check for RetroArch
+ if not ctx.look_for_retroarch.is_set():
+ logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.")
+ await asyncio.wait((
+ asyncio.create_task(ctx.look_for_retroarch.wait()),
+ asyncio.create_task(ctx.exit_event.wait())
+ ), return_when=asyncio.FIRST_COMPLETED)
+
+ last_log = ""
+
+ def log_no_spam(msg: str) -> None:
+ nonlocal last_log
+ if msg != last_log:
+ last_log = msg
+ logger.info(msg)
+
+ # to only show this message once per client run
+ help_message_shown = False
+
+ with Memory(ctx.from_game, ctx.to_game) as memory:
+ while not ctx.exit_event.is_set():
+ ram = await memory.read()
+ game_id = memory.get_rom_to_ram_data(ram)
+ name, seed_end = name_seed_from_ram(game_id)
+ if len(name):
+ if name == ctx.known_name:
+ ctx.auth = name
+ # this is the name we know
+ if ctx.server and ctx.server.socket: # type: ignore
+ if ctx.got_room_info.is_set():
+ if ctx.seed_name and ctx.seed_name.endswith(seed_end):
+ # correct seed
+ if memory.have_generation_info():
+ log_no_spam("everything connected")
+ await memory.process_ram(ram)
+ ctx.process_from_game_queue()
+ ctx.process_items_received()
+ else: # no generation info
+ if ctx.got_slot_data.is_set():
+ memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id)
+ ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \
+ make_id_to_others(ctx.start_char)
+ ctx.next_item = 0
+ ctx.ap_local_count = len(ctx.checked_locations)
+ else: # no slot data yet
+ async_start(ctx.send_connect())
+ log_no_spam("logging in to server...")
+ await asyncio.wait((
+ asyncio.create_task(ctx.got_slot_data.wait()),
+ asyncio.create_task(ctx.exit_event.wait()),
+ asyncio.create_task(asyncio.sleep(6))
+ ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets
+ else: # not correct seed name
+ log_no_spam("incorrect seed - did you mix up roms?")
+ else: # no room info
+ # If we get here, it looks like `RoomInfo` packet got lost
+ log_no_spam("waiting for room info from server...")
+ else: # server not connected
+ log_no_spam("waiting for server connection...")
+ else: # new game
+ log_no_spam("connected to new game")
+ await ctx.disconnect()
+ ctx.reset_server_state()
+ ctx.seed_name = None
+ ctx.got_room_info.clear()
+ ctx.reset_game_state()
+ memory.reset_game_state()
+
+ ctx.auth = name
+ ctx.known_name = name
+ async_start(ctx.connect())
+ await asyncio.wait((
+ asyncio.create_task(ctx.got_room_info.wait()),
+ asyncio.create_task(ctx.exit_event.wait()),
+ asyncio.create_task(asyncio.sleep(6))
+ ), return_when=asyncio.FIRST_COMPLETED)
+ else: # no name found in game
+ if not help_message_shown:
+ logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.')
+ help_message_shown = True
+ log_no_spam("looking for connection to game...")
+ await asyncio.sleep(0.3)
+
+ await asyncio.sleep(0.09375)
+ logger.info("zillion sync task ending")
+
+
+async def main() -> None:
+ parser = get_base_parser()
+ parser.add_argument('diff_file', default="", type=str, nargs="?",
+ help='Path to a .apzl Archipelago Binary Patch file')
+ # SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
+ args = parser.parse_args()
+ print(args)
+
+ if args.diff_file:
+ import Patch
+ logger.info("patch file was supplied - creating sms rom...")
+ meta, rom_file = Patch.create_rom_file(args.diff_file)
+ if "server" in meta:
+ args.connect = meta["server"]
+ logger.info(f"wrote rom file to {rom_file}")
+
+ ctx = ZillionContext(args.connect, args.password)
+ if ctx.server_task is None:
+ ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
+
+ if gui_enabled:
+ ctx.run_gui()
+ ctx.run_cli()
+
+ sync_task = asyncio.create_task(zillion_sync_task(ctx))
+
+ await ctx.exit_event.wait()
+
+ ctx.server_address = None
+ logger.debug("waiting for sync task to end")
+ await sync_task
+ logger.debug("sync task ended")
+ await ctx.shutdown()
+
+
+def launch() -> None:
+ colorama.init()
+ asyncio.run(main())
+ colorama.deinit()
diff --git a/worlds/zillion/config.py b/worlds/zillion/config.py
index db61d0c45347..e08c4f4278ed 100644
--- a/worlds/zillion/config.py
+++ b/worlds/zillion/config.py
@@ -1,21 +1 @@
-import os
-
base_id = 8675309
-zillion_map = os.path.join(os.path.dirname(__file__), "empty-zillion-map-row-col-labels-281.png")
-
-
-def detect_test() -> bool:
- """
- Parts of generation that are in unit tests need the rom.
- This is to detect whether we are running unit tests
- so we can work around the need for the rom.
- """
- import __main__
- try:
- if "test" in __main__.__file__:
- return True
- except AttributeError:
- # In some environments, __main__ doesn't have __file__
- # We'll assume that's not unit tests.
- pass
- return False
diff --git a/worlds/zillion/docs/en_Zillion.md b/worlds/zillion/docs/en_Zillion.md
index b5d37cc20209..697a9b7dadbe 100644
--- a/worlds/zillion/docs/en_Zillion.md
+++ b/worlds/zillion/docs/en_Zillion.md
@@ -4,9 +4,9 @@ Zillion is a metroidvania-style game released in 1987 for the 8-bit Sega Master
It's based on the anime Zillion (赤ã„光弾ジリオン, Akai Koudan Zillion).
-## Where is the settings page?
+## Where is the options page?
-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
+The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
## What changes are made to this game?
@@ -67,8 +67,16 @@ Note that in "restrictive" mode, Champ is the only one that can get Zillion powe
Canisters retain their original appearance, so you won't know if an item belongs to another player until you collect it.
-When you collect an item, you see the name of the player it goes to. You can see in the client log what item was collected.
+When you collect an item, you see the name of the player it goes to. You can see in the client log what item was
+collected.
## When the player receives an item, what happens?
The item collect sound is played. You can see in the client log what item was received.
+
+## Unique Local Commands
+
+The following commands are only available when using the ZillionClient to play with Archipelago.
+
+- `/sms` Tell the client that Zillion is running in RetroArch.
+- `/map` Toggle view of the map tracker.
diff --git a/worlds/zillion/docs/setup_en.md b/worlds/zillion/docs/setup_en.md
index 16000dbe3b7a..c8e29fc36cde 100644
--- a/worlds/zillion/docs/setup_en.md
+++ b/worlds/zillion/docs/setup_en.md
@@ -2,7 +2,7 @@
## Required Software
-- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `Zillion Client - Zillion Patch Setup`
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms).
@@ -30,9 +30,10 @@ Put your Zillion ROM file in the Archipelago directory in your home directory.
### Windows Setup
-1. During the installation of Archipelago, install the Zillion Client. If you did not do this,
- or you are on an older version, you may run the installer again to install the Zillion Client.
-2. During setup, you will be asked to locate your base ROM file. This is the Zillion ROM file mentioned above in Required Software.
+1. Download and install [Archipelago](). **The installer
+ file is located in the assets section at the bottom of the version information.**
+2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
+ This is the Zillion ROM file mentioned above in Required Software. This only needs to be done once.
---
# Play
@@ -46,16 +47,16 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a config file?
-The [player settings page](/games/Zillion/player-settings) on the website allows you to configure your personal settings and export a config file from
+The [player options page](/games/Zillion/player-options) on the website allows you to configure your personal options 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 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
-1. Navigate to the [player settings page](/games/Zillion/player-settings), configure your options, and click the "Generate Game" button.
+1. Navigate to the [player options page](/games/Zillion/player-options), configure your options, and click the "Generate Game" button.
2. A "Seed Info" page will appear.
3. Click the "Create New Room" link.
4. A server page will appear. Download your patch file from this page.
diff --git a/worlds/zillion/gen_data.py b/worlds/zillion/gen_data.py
new file mode 100644
index 000000000000..13cbee9ced20
--- /dev/null
+++ b/worlds/zillion/gen_data.py
@@ -0,0 +1,42 @@
+from dataclasses import dataclass
+import json
+from typing import Dict, Tuple
+
+from zilliandomizer.game import Game as ZzGame
+
+
+@dataclass
+class GenData:
+ """ data passed from generation to patcher """
+
+ multi_items: Dict[str, Tuple[str, str]]
+ """ zz_loc_name to (item_name, player_name) """
+ zz_game: ZzGame
+ game_id: bytes
+ """ the byte string used to detect the rom """
+
+ def to_json(self) -> str:
+ """ serialized data from generation needed to patch rom """
+ jsonable = {
+ "multi_items": self.multi_items,
+ "zz_game": self.zz_game.to_jsonable(),
+ "game_id": list(self.game_id)
+ }
+ return json.dumps(jsonable)
+
+ @staticmethod
+ def from_json(gen_data_str: str) -> "GenData":
+ """ the reverse of `to_json` """
+ from_json = json.loads(gen_data_str)
+
+ # backwards compatibility for seeds generated before new map_gen options
+ room_gen = from_json["zz_game"]["options"].get("room_gen", None)
+ if room_gen is not None:
+ from_json["zz_game"]["options"]["map_gen"] = {False: "none", True: "rooms"}.get(room_gen, "none")
+ del from_json["zz_game"]["options"]["room_gen"]
+
+ return GenData(
+ from_json["multi_items"],
+ ZzGame.from_jsonable(from_json["zz_game"]),
+ bytes(from_json["game_id"])
+ )
diff --git a/worlds/zillion/id_maps.py b/worlds/zillion/id_maps.py
index bc9caeeece2e..32d71fc79b30 100644
--- a/worlds/zillion/id_maps.py
+++ b/worlds/zillion/id_maps.py
@@ -1,10 +1,22 @@
-from typing import Dict, Tuple
-from zilliandomizer.logic_components.items import Item as ZzItem, \
- item_name_to_id as zz_item_name_to_zz_id, items as zz_items, \
- item_name_to_item as zz_item_name_to_zz_item
+from collections import defaultdict
+from typing import Dict, Iterable, Mapping, Tuple, TypedDict
+
+from zilliandomizer.logic_components.items import (
+ Item as ZzItem,
+ KEYWORD,
+ NORMAL,
+ RESCUE,
+ item_name_to_id as zz_item_name_to_zz_id,
+ items as zz_items,
+ item_name_to_item as zz_item_name_to_zz_item,
+)
+from zilliandomizer.logic_components.regions import RegionData
+from zilliandomizer.low_resources.item_rooms import item_room_codes
from zilliandomizer.options import Chars
from zilliandomizer.utils.loc_name_maps import loc_to_id as pretty_loc_name_to_id
-from zilliandomizer.utils import parse_reg_name
+from zilliandomizer.utils import parse_loc_name, parse_reg_name
+from zilliandomizer.zri.memory import RescueInfo
+
from .config import base_id as base_id
item_name_to_id = {
@@ -91,3 +103,56 @@ def zz_reg_name_to_reg_name(zz_reg_name: str) -> str:
end = zz_reg_name[5:]
return f"{make_room_name(row, col)} {end.upper()}"
return zz_reg_name
+
+
+class ClientRescue(TypedDict):
+ start_char: Chars
+ room_code: int
+ mask: int
+
+
+class ZillionSlotInfo(TypedDict):
+ start_char: Chars
+ rescues: Dict[str, ClientRescue]
+ loc_mem_to_id: Dict[int, int]
+ """ memory location of canister to Archipelago location id number """
+
+
+def get_slot_info(regions: Iterable[RegionData],
+ start_char: Chars,
+ loc_name_to_pretty: Mapping[str, str]) -> ZillionSlotInfo:
+ items_placed_in_map_index: Dict[int, int] = defaultdict(int)
+ rescue_locations: Dict[int, RescueInfo] = {}
+ loc_memory_to_loc_id: Dict[int, int] = {}
+ for region in regions:
+ for loc in region.locations:
+ assert loc.item, ("There should be an item placed in every location before "
+ f"writing slot info. {loc.name} is missing item.")
+ if loc.item.code in {KEYWORD, NORMAL, RESCUE}:
+ row, col, _y, _x = parse_loc_name(loc.name)
+ map_index = row * 8 + col
+ item_no = items_placed_in_map_index[map_index]
+ room_code = item_room_codes[map_index]
+
+ r = room_code
+ m = 1 << item_no
+ if loc.item.code == RESCUE:
+ rescue_locations[loc.item.id] = RescueInfo(start_char, r, m)
+ loc_memory = (r << 7) | m
+ loc_memory_to_loc_id[loc_memory] = pretty_loc_name_to_id[loc_name_to_pretty[loc.name]]
+ items_placed_in_map_index[map_index] += 1
+
+ rescues: Dict[str, ClientRescue] = {}
+ for i in (0, 1):
+ if i in rescue_locations:
+ ri = rescue_locations[i]
+ rescues[str(i)] = {
+ "start_char": ri.start_char,
+ "room_code": ri.room_code,
+ "mask": ri.mask
+ }
+ return {
+ "start_char": start_char,
+ "rescues": rescues,
+ "loc_mem_to_id": loc_memory_to_loc_id
+ }
diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py
index e99867c742aa..dcbc6131f1a9 100644
--- a/worlds/zillion/logic.py
+++ b/worlds/zillion/logic.py
@@ -1,9 +1,11 @@
-from typing import Dict, FrozenSet, Tuple, cast, List, Counter as _Counter
+from typing import Dict, FrozenSet, Tuple, List, Counter as _Counter
+
from BaseClasses import CollectionState
+
+from zilliandomizer.logic_components.items import Item, items
from zilliandomizer.logic_components.locations import Location
from zilliandomizer.randomizer import Randomizer
-from zilliandomizer.logic_components.items import Item, items
-from .region import ZillionLocation
+
from .item import ZillionItem
from .id_maps import item_name_to_id
@@ -18,11 +20,12 @@ def set_randomizer_locs(cs: CollectionState, p: int, zz_r: Randomizer) -> int:
returns a hash of the player and of the set locations with their items
"""
+ from . import ZillionWorld
z_world = cs.multiworld.worlds[p]
- my_locations = cast(List[ZillionLocation], getattr(z_world, "my_locations"))
+ assert isinstance(z_world, ZillionWorld)
_hash = p
- for z_loc in my_locations:
+ for z_loc in z_world.my_locations:
zz_name = z_loc.zz_loc.name
zz_item = z_loc.item.zz_item \
if isinstance(z_loc.item, ZillionItem) and z_loc.item.player == p \
@@ -38,10 +41,10 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]:
((item_name, count), (item_name, count), ...)
"""
- return tuple((item_name, cs.item_count(item_name, p)) for item_name in item_name_to_id)
+ return tuple((item_name, cs.count(item_name, p)) for item_name in item_name_to_id)
-LogicCacheType = Dict[int, Tuple[_Counter[Tuple[str, int]], FrozenSet[Location]]]
+LogicCacheType = Dict[int, Tuple[Dict[int, _Counter[str]], FrozenSet[Location]]]
""" { hash: (cs.prog_items, accessible_locations) } """
diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py
index 6aa88f5b220f..5de0b65c82f0 100644
--- a/worlds/zillion/options.py
+++ b/worlds/zillion/options.py
@@ -1,16 +1,18 @@
from collections import Counter
-# import logging
-from typing import TYPE_CHECKING, Any, Dict, Tuple, cast
-from Options import AssembleOptions, DefaultOnToggle, Range, SpecialRange, Toggle, Choice
-from zilliandomizer.options import \
- Options as ZzOptions, char_to_gun, char_to_jump, ID, \
- VBLR as ZzVBLR, chars, Chars, ItemCounts as ZzItemCounts
+from dataclasses import dataclass
+from typing import ClassVar, Dict, Literal, Tuple
+from typing_extensions import TypeGuard # remove when Python >= 3.10
+
+from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle
+
+from zilliandomizer.options import (
+ Options as ZzOptions, char_to_gun, char_to_jump, ID,
+ VBLR as ZzVBLR, Chars, ItemCounts as ZzItemCounts
+)
from zilliandomizer.options.parsing import validate as zz_validate
-if TYPE_CHECKING:
- from BaseClasses import MultiWorld
-class ZillionContinues(SpecialRange):
+class ZillionContinues(NamedRange):
"""
number of continues before game over
@@ -41,6 +43,19 @@ class VBLR(Choice):
option_restrictive = 3
default = 1
+ def to_zz_vblr(self) -> ZzVBLR:
+ def is_vblr(o: str) -> TypeGuard[ZzVBLR]:
+ """
+ This function is because mypy doesn't support narrowing with `in`,
+ https://github.com/python/mypy/issues/12535
+ so this is the only way I see to get type narrowing to `Literal`.
+ """
+ return o in ("vanilla", "balanced", "low", "restrictive")
+
+ key = self.current_key
+ assert is_vblr(key), f"{key=}"
+ return key
+
class ZillionGunLevels(VBLR):
"""
@@ -93,6 +108,15 @@ class ZillionStartChar(Choice):
display_name = "start character"
default = "random"
+ _name_capitalization: ClassVar[Dict[int, Chars]] = {
+ option_jj: "JJ",
+ option_apple: "Apple",
+ option_champ: "Champ",
+ }
+
+ def get_char(self) -> Chars:
+ return ZillionStartChar._name_capitalization[self.value]
+
class ZillionIDCardCount(Range):
"""
@@ -198,13 +222,20 @@ class ZillionEarlyScope(Toggle):
class ZillionSkill(Range):
- """ the difficulty level of the game """
+ """
+ the difficulty level of the game
+
+ higher skill:
+ - can require more precise platforming movement
+ - lowers your defense
+ - gives you less time to escape at the end
+ """
range_start = 0
range_end = 5
default = 2
-class ZillionStartingCards(SpecialRange):
+class ZillionStartingCards(NamedRange):
"""
how many ID Cards to start the game with
@@ -220,32 +251,58 @@ class ZillionStartingCards(SpecialRange):
}
-class ZillionRoomGen(Toggle):
- """ whether to generate rooms with random terrain """
- display_name = "room generation"
-
-
-zillion_options: Dict[str, AssembleOptions] = {
- "continues": ZillionContinues,
- "floppy_req": ZillionFloppyReq,
- "gun_levels": ZillionGunLevels,
- "jump_levels": ZillionJumpLevels,
- "randomize_alarms": ZillionRandomizeAlarms,
- "max_level": ZillionMaxLevel,
- "start_char": ZillionStartChar,
- "opas_per_level": ZillionOpasPerLevel,
- "id_card_count": ZillionIDCardCount,
- "bread_count": ZillionBreadCount,
- "opa_opa_count": ZillionOpaOpaCount,
- "zillion_count": ZillionZillionCount,
- "floppy_disk_count": ZillionFloppyDiskCount,
- "scope_count": ZillionScopeCount,
- "red_id_card_count": ZillionRedIDCardCount,
- "early_scope": ZillionEarlyScope,
- "skill": ZillionSkill,
- "starting_cards": ZillionStartingCards,
- "room_gen": ZillionRoomGen,
-}
+class ZillionMapGen(Choice):
+ """
+ - none: vanilla map
+ - rooms: random terrain inside rooms, but path through base is vanilla
+ - full: random path through base
+ """
+ display_name = "map generation"
+ option_none = 0
+ option_rooms = 1
+ option_full = 2
+ default = 0
+
+ def zz_value(self) -> Literal['none', 'rooms', 'full']:
+ if self.value == ZillionMapGen.option_none:
+ return "none"
+ if self.value == ZillionMapGen.option_rooms:
+ return "rooms"
+ assert self.value == ZillionMapGen.option_full
+ return "full"
+
+
+@dataclass
+class ZillionOptions(PerGameCommonOptions):
+ continues: ZillionContinues
+ floppy_req: ZillionFloppyReq
+ gun_levels: ZillionGunLevels
+ jump_levels: ZillionJumpLevels
+ randomize_alarms: ZillionRandomizeAlarms
+ max_level: ZillionMaxLevel
+ start_char: ZillionStartChar
+ opas_per_level: ZillionOpasPerLevel
+ id_card_count: ZillionIDCardCount
+ bread_count: ZillionBreadCount
+ opa_opa_count: ZillionOpaOpaCount
+ zillion_count: ZillionZillionCount
+ floppy_disk_count: ZillionFloppyDiskCount
+ scope_count: ZillionScopeCount
+ red_id_card_count: ZillionRedIDCardCount
+ early_scope: ZillionEarlyScope
+ skill: ZillionSkill
+ starting_cards: ZillionStartingCards
+ map_gen: ZillionMapGen
+
+ room_gen: Removed
+
+
+z_option_groups = [
+ OptionGroup("item counts", [
+ ZillionIDCardCount, ZillionBreadCount, ZillionOpaOpaCount, ZillionZillionCount,
+ ZillionFloppyDiskCount, ZillionScopeCount, ZillionRedIDCardCount
+ ])
+]
def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts:
@@ -262,47 +319,34 @@ def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts:
return tr
-def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]":
+def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]":
"""
adjusts options to make game completion possible
- `world` parameter is MultiWorld object that has my options on it
- `p` is my player id
+ `options` parameter is ZillionOptions object that was put on my world by the core
"""
- for option_name in zillion_options:
- assert hasattr(world, option_name), f"Zillion option {option_name} didn't get put in world object"
- wo = cast(Any, world) # so I don't need getattr on all the options
- skill = wo.skill[p].value
+ skill = options.skill.value
- jump_levels = cast(ZillionJumpLevels, wo.jump_levels[p])
- jump_option = jump_levels.current_key
- required_level = char_to_jump["Apple"][cast(ZzVBLR, jump_option)].index(3) + 1
+ jump_option = options.jump_levels.to_zz_vblr()
+ required_level = char_to_jump["Apple"][jump_option].index(3) + 1
if skill == 0:
# because of hp logic on final boss
required_level = 8
- gun_levels = cast(ZillionGunLevels, wo.gun_levels[p])
- gun_option = gun_levels.current_key
- guns_required = char_to_gun["Champ"][cast(ZzVBLR, gun_option)].index(3)
+ gun_option = options.gun_levels.to_zz_vblr()
+ guns_required = char_to_gun["Champ"][gun_option].index(3)
- floppy_req = cast(ZillionFloppyReq, wo.floppy_req[p])
+ floppy_req = options.floppy_req
- card = cast(ZillionIDCardCount, wo.id_card_count[p])
- bread = cast(ZillionBreadCount, wo.bread_count[p])
- opa = cast(ZillionOpaOpaCount, wo.opa_opa_count[p])
- gun = cast(ZillionZillionCount, wo.zillion_count[p])
- floppy = cast(ZillionFloppyDiskCount, wo.floppy_disk_count[p])
- scope = cast(ZillionScopeCount, wo.scope_count[p])
- red = cast(ZillionRedIDCardCount, wo.red_id_card_count[p])
item_counts = Counter({
- "ID Card": card,
- "Bread": bread,
- "Opa-Opa": opa,
- "Zillion": gun,
- "Floppy Disk": floppy,
- "Scope": scope,
- "Red ID Card": red
+ "ID Card": options.id_card_count,
+ "Bread": options.bread_count,
+ "Opa-Opa": options.opa_opa_count,
+ "Zillion": options.zillion_count,
+ "Floppy Disk": options.floppy_disk_count,
+ "Scope": options.scope_count,
+ "Red ID Card": options.red_id_card_count
})
minimums = Counter({
"ID Card": 0,
@@ -335,10 +379,10 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]":
item_counts["Empty"] += diff
assert sum(item_counts.values()) == 144
- max_level = cast(ZillionMaxLevel, wo.max_level[p])
+ max_level = options.max_level
max_level.value = max(required_level, max_level.value)
- opas_per_level = cast(ZillionOpasPerLevel, wo.opas_per_level[p])
+ opas_per_level = options.opas_per_level
while (opas_per_level.value > 1) and (1 + item_counts["Opa-Opa"] // opas_per_level.value < max_level.value):
# logging.warning(
# "zillion options validate: option opas_per_level incompatible with options max_level and opa_opa_count"
@@ -347,42 +391,27 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]":
# that should be all of the level requirements met
- name_capitalization = {
- "jj": "JJ",
- "apple": "Apple",
- "champ": "Champ",
- }
-
- start_char = cast(ZillionStartChar, wo.start_char[p])
- start_char_name = name_capitalization[start_char.current_key]
- assert start_char_name in chars
- start_char_name = cast(Chars, start_char_name)
-
- starting_cards = cast(ZillionStartingCards, wo.starting_cards[p])
-
- room_gen = cast(ZillionRoomGen, wo.room_gen[p])
+ starting_cards = options.starting_cards
- early_scope = cast(ZillionEarlyScope, wo.early_scope[p])
- if early_scope:
- world.early_items[p]["Scope"] = 1
+ map_gen = options.map_gen.zz_value()
zz_item_counts = convert_item_counts(item_counts)
zz_op = ZzOptions(
zz_item_counts,
- cast(ZzVBLR, jump_option),
- cast(ZzVBLR, gun_option),
+ jump_option,
+ gun_option,
opas_per_level.value,
max_level.value,
False, # tutorial
skill,
- start_char_name,
+ options.start_char.get_char(),
floppy_req.value,
- wo.continues[p].value,
- wo.randomize_alarms[p].value,
- False, # early scope is done with AP early_items API
+ options.continues.value,
+ bool(options.randomize_alarms.value),
+ bool(options.early_scope.value),
True, # balance defense
starting_cards.value,
- bool(room_gen.value)
+ map_gen
)
zz_validate(zz_op)
return zz_op, item_counts
diff --git a/worlds/zillion/patch.py b/worlds/zillion/patch.py
index 148caac9fb7b..6bc6d04dd663 100644
--- a/worlds/zillion/patch.py
+++ b/worlds/zillion/patch.py
@@ -1,22 +1,53 @@
-from typing import BinaryIO, Optional, cast
-import Utils
-from worlds.Files import APDeltaPatch
import os
+from typing import Any, BinaryIO, Optional, cast
+import zipfile
+
+from typing_extensions import override
+
+import Utils
+from worlds.Files import APAutoPatchInterface
+
+from zilliandomizer.patch import Patcher
+
+from .gen_data import GenData
USHASH = 'd4bf9e7bcf9a48da53785d2ae7bc4270'
-class ZillionDeltaPatch(APDeltaPatch):
+class ZillionPatch(APAutoPatchInterface):
hash = USHASH
game = "Zillion"
patch_file_ending = ".apzl"
result_file_ending = ".sms"
+ gen_data_str: str
+ """ JSON encoded """
+
+ def __init__(self, *args: Any, gen_data_str: str = "", **kwargs: Any) -> None:
+ super().__init__(*args, **kwargs)
+ self.gen_data_str = gen_data_str
+
@classmethod
def get_source_data(cls) -> bytes:
with open(get_base_rom_path(), "rb") as stream:
return read_rom(stream)
+ @override
+ def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
+ super().write_contents(opened_zipfile)
+ opened_zipfile.writestr("gen_data.json",
+ self.gen_data_str,
+ compress_type=zipfile.ZIP_DEFLATED)
+
+ @override
+ def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
+ super().read_contents(opened_zipfile)
+ self.gen_data_str = opened_zipfile.read("gen_data.json").decode()
+
+ def patch(self, target: str) -> None:
+ self.read()
+ write_rom_from_gen_data(self.gen_data_str, target)
+
def get_base_rom_path(file_name: Optional[str] = None) -> str:
options = Utils.get_options()
@@ -32,3 +63,21 @@ def read_rom(stream: BinaryIO) -> bytes:
data = stream.read()
# I'm not aware of any sms header.
return data
+
+
+def write_rom_from_gen_data(gen_data_str: str, output_rom_file_name: str) -> None:
+ """ take the output of `GenData.to_json`, and create rom from it """
+ gen_data = GenData.from_json(gen_data_str)
+
+ base_rom_path = get_base_rom_path()
+ zz_patcher = Patcher(base_rom_path)
+
+ zz_patcher.write_locations(gen_data.zz_game.regions, gen_data.zz_game.char_order[0])
+ zz_patcher.all_fixes_and_options(gen_data.zz_game)
+ zz_patcher.set_external_item_interface(gen_data.zz_game.char_order[0], gen_data.zz_game.options.max_level)
+ zz_patcher.set_multiworld_items(gen_data.multi_items)
+ zz_patcher.set_rom_to_ram_data(gen_data.game_id)
+
+ patched_rom_bytes = zz_patcher.get_patched_bytes()
+ with open(output_rom_file_name, "wb") as binary_file:
+ binary_file.write(patched_rom_bytes)
diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt
index 626579ab1139..d6b01ac107ae 100644
--- a/worlds/zillion/requirements.txt
+++ b/worlds/zillion/requirements.txt
@@ -1 +1,2 @@
-zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@cd6a940ad7b585c75a560b91468d6b9eee030559#0.5.2
+zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@33045067f626266850f91c8045b9d3a9f52d02b0#0.9.0
+typing-extensions>=4.7, <5
diff --git a/worlds/zillion/test/TestOptions.py b/worlds/zillion/test/TestOptions.py
index 1ec186dae50a..3820c32dd016 100644
--- a/worlds/zillion/test/TestOptions.py
+++ b/worlds/zillion/test/TestOptions.py
@@ -1,6 +1,6 @@
from . import ZillionTestBase
-from worlds.zillion.options import ZillionJumpLevels, ZillionGunLevels, validate
+from ..options import ZillionJumpLevels, ZillionGunLevels, ZillionOptions, validate
from zilliandomizer.options import VBLR_CHOICES
@@ -9,7 +9,9 @@ class OptionsTest(ZillionTestBase):
def test_validate_default(self) -> None:
self.world_setup()
- validate(self.multiworld, 1)
+ options = self.multiworld.worlds[1].options
+ assert isinstance(options, ZillionOptions)
+ validate(options)
def test_vblr_ap_to_zz(self) -> None:
""" all of the valid values for the AP options map to valid values for ZZ options """
@@ -20,7 +22,9 @@ def test_vblr_ap_to_zz(self) -> None:
for value in vblr_class.name_lookup.values():
self.options = {option_name: value}
self.world_setup()
- zz_options, _item_counts = validate(self.multiworld, 1)
+ options = self.multiworld.worlds[1].options
+ assert isinstance(options, ZillionOptions)
+ zz_options, _item_counts = validate(options)
assert getattr(zz_options, option_name) in VBLR_CHOICES
# TODO: test validate with invalid combinations of options
diff --git a/worlds/zillion/test/TestReproducibleRandom.py b/worlds/zillion/test/TestReproducibleRandom.py
index 392db657d90a..a92fae240709 100644
--- a/worlds/zillion/test/TestReproducibleRandom.py
+++ b/worlds/zillion/test/TestReproducibleRandom.py
@@ -1,7 +1,7 @@
from typing import cast
from . import ZillionTestBase
-from worlds.zillion import ZillionWorld
+from .. import ZillionWorld
class SeedTest(ZillionTestBase):
diff --git a/worlds/zillion/test/__init__.py b/worlds/zillion/test/__init__.py
index 3b7edebef804..fe62bae34c9e 100644
--- a/worlds/zillion/test/__init__.py
+++ b/worlds/zillion/test/__init__.py
@@ -1,6 +1,6 @@
from typing import cast
-from test.TestBase import WorldTestBase
-from worlds.zillion import ZillionWorld
+from test.bases import WorldTestBase
+from .. import ZillionWorld
class ZillionTestBase(WorldTestBase):
diff --git a/worlds/zork_grand_inquisitor/LICENSE b/worlds/zork_grand_inquisitor/LICENSE
new file mode 100644
index 000000000000..a94ca6bf9177
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Serpent.AI
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/worlds/zork_grand_inquisitor/__init__.py b/worlds/zork_grand_inquisitor/__init__.py
new file mode 100644
index 000000000000..4da257e47bd0
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/__init__.py
@@ -0,0 +1,17 @@
+import worlds.LauncherComponents as LauncherComponents
+
+from .world import ZorkGrandInquisitorWorld
+
+
+def launch_client() -> None:
+ from .client import main
+ LauncherComponents.launch_subprocess(main, name="ZorkGrandInquisitorClient")
+
+
+LauncherComponents.components.append(
+ LauncherComponents.Component(
+ "Zork Grand Inquisitor Client",
+ func=launch_client,
+ component_type=LauncherComponents.Type.CLIENT
+ )
+)
diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py
new file mode 100644
index 000000000000..11d6b7f8f183
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/client.py
@@ -0,0 +1,188 @@
+import asyncio
+
+import CommonClient
+import NetUtils
+import Utils
+
+from typing import Any, Dict, List, Optional, Set, Tuple
+
+from .data_funcs import item_names_to_id, location_names_to_id, id_to_items, id_to_locations, id_to_goals
+from .enums import ZorkGrandInquisitorItems, ZorkGrandInquisitorLocations
+from .game_controller import GameController
+
+
+class ZorkGrandInquisitorCommandProcessor(CommonClient.ClientCommandProcessor):
+ def _cmd_zork(self) -> None:
+ """Attach to an open Zork Grand Inquisitor process."""
+ result: bool = self.ctx.game_controller.open_process_handle()
+
+ if result:
+ self.ctx.process_attached_at_least_once = True
+ self.output("Successfully attached to Zork Grand Inquisitor process.")
+ else:
+ self.output("Failed to attach to Zork Grand Inquisitor process.")
+
+ def _cmd_brog(self) -> None:
+ """List received Brog items."""
+ self.ctx.game_controller.list_received_brog_items()
+
+ def _cmd_griff(self) -> None:
+ """List received Griff items."""
+ self.ctx.game_controller.list_received_griff_items()
+
+ def _cmd_lucy(self) -> None:
+ """List received Lucy items."""
+ self.ctx.game_controller.list_received_lucy_items()
+
+ def _cmd_hotspots(self) -> None:
+ """List received Hotspots."""
+ self.ctx.game_controller.list_received_hotspots()
+
+
+class ZorkGrandInquisitorContext(CommonClient.CommonContext):
+ tags: Set[str] = {"AP"}
+ game: str = "Zork Grand Inquisitor"
+ command_processor: CommonClient.ClientCommandProcessor = ZorkGrandInquisitorCommandProcessor
+ items_handling: int = 0b111
+ want_slot_data: bool = True
+
+ item_name_to_id: Dict[str, int] = item_names_to_id()
+ location_name_to_id: Dict[str, int] = location_names_to_id()
+
+ id_to_items: Dict[int, ZorkGrandInquisitorItems] = id_to_items()
+ id_to_locations: Dict[int, ZorkGrandInquisitorLocations] = id_to_locations()
+
+ game_controller: GameController
+
+ controller_task: Optional[asyncio.Task]
+
+ process_attached_at_least_once: bool
+ can_display_process_message: bool
+
+ def __init__(self, server_address: Optional[str], password: Optional[str]) -> None:
+ super().__init__(server_address, password)
+
+ self.game_controller = GameController(logger=CommonClient.logger)
+
+ self.controller_task = None
+
+ self.process_attached_at_least_once = False
+ self.can_display_process_message = True
+
+ def run_gui(self) -> None:
+ from kvui import GameManager
+
+ class TextManager(GameManager):
+ logging_pairs: List[Tuple[str, str]] = [("Client", "Archipelago")]
+ base_title: str = "Archipelago Zork Grand Inquisitor Client"
+
+ self.ui = TextManager(self)
+ self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
+
+ async def server_auth(self, password_requested: bool = False):
+ if password_requested and not self.password:
+ await super().server_auth(password_requested)
+
+ await self.get_username()
+ await self.send_connect()
+
+ def on_package(self, cmd: str, _args: Any) -> None:
+ if cmd == "Connected":
+ self.game = self.slot_info[self.slot].game
+
+ # Options
+ self.game_controller.option_goal = id_to_goals()[_args["slot_data"]["goal"]]
+ self.game_controller.option_deathsanity = _args["slot_data"]["deathsanity"] == 1
+
+ self.game_controller.option_grant_missable_location_checks = (
+ _args["slot_data"]["grant_missable_location_checks"] == 1
+ )
+
+ async def controller(self):
+ while not self.exit_event.is_set():
+ await asyncio.sleep(0.1)
+
+ # Enqueue Received Item Delta
+ network_item: NetUtils.NetworkItem
+ for network_item in self.items_received:
+ item: ZorkGrandInquisitorItems = self.id_to_items[network_item.item]
+
+ if item not in self.game_controller.received_items:
+ if item not in self.game_controller.received_items_queue:
+ self.game_controller.received_items_queue.append(item)
+
+ # Game Controller Update
+ if self.game_controller.is_process_running():
+ self.game_controller.update()
+ self.can_display_process_message = True
+ else:
+ process_message: str
+
+ if self.process_attached_at_least_once:
+ process_message = (
+ "Lost connection to Zork Grand Inquisitor process. Please restart the game and use the /zork "
+ "command to reattach."
+ )
+ else:
+ process_message = (
+ "Please use the /zork command to attach to a running Zork Grand Inquisitor process."
+ )
+
+ if self.can_display_process_message:
+ CommonClient.logger.info(process_message)
+ self.can_display_process_message = False
+
+ # Send Checked Locations
+ checked_location_ids: List[int] = list()
+
+ while len(self.game_controller.completed_locations_queue) > 0:
+ location: ZorkGrandInquisitorLocations = self.game_controller.completed_locations_queue.popleft()
+ location_id: int = self.location_name_to_id[location.value]
+
+ checked_location_ids.append(location_id)
+
+ await self.send_msgs([
+ {
+ "cmd": "LocationChecks",
+ "locations": checked_location_ids
+ }
+ ])
+
+ # Check for Goal Completion
+ if self.game_controller.goal_completed:
+ await self.send_msgs([
+ {
+ "cmd": "StatusUpdate",
+ "status": CommonClient.ClientStatus.CLIENT_GOAL
+ }
+ ])
+
+
+def main() -> None:
+ Utils.init_logging("ZorkGrandInquisitorClient", exception_logger="Client")
+
+ async def _main():
+ ctx: ZorkGrandInquisitorContext = ZorkGrandInquisitorContext(None, None)
+
+ ctx.server_task = asyncio.create_task(CommonClient.server_loop(ctx), name="server loop")
+ ctx.controller_task = asyncio.create_task(ctx.controller(), name="ZorkGrandInquisitorController")
+
+ if CommonClient.gui_enabled:
+ ctx.run_gui()
+
+ ctx.run_cli()
+
+ await ctx.exit_event.wait()
+ await ctx.shutdown()
+
+ import colorama
+
+ colorama.init()
+
+ asyncio.run(_main())
+
+ colorama.deinit()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/worlds/zork_grand_inquisitor/data/__init__.py b/worlds/zork_grand_inquisitor/data/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/zork_grand_inquisitor/data/entrance_rule_data.py b/worlds/zork_grand_inquisitor/data/entrance_rule_data.py
new file mode 100644
index 000000000000..f48be5eb6b6a
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/data/entrance_rule_data.py
@@ -0,0 +1,419 @@
+from typing import Dict, Tuple, Union
+
+from ..enums import ZorkGrandInquisitorEvents, ZorkGrandInquisitorItems, ZorkGrandInquisitorRegions
+
+
+entrance_rule_data: Dict[
+ Tuple[
+ ZorkGrandInquisitorRegions,
+ ZorkGrandInquisitorRegions,
+ ],
+ Union[
+ Tuple[
+ Tuple[
+ Union[
+ ZorkGrandInquisitorEvents,
+ ZorkGrandInquisitorItems,
+ ZorkGrandInquisitorRegions,
+ ],
+ ...,
+ ],
+ ...,
+ ],
+ None,
+ ],
+] = {
+ (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.DM_LAIR): (
+ (
+ ZorkGrandInquisitorItems.SWORD,
+ ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE,
+ ),
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.GUE_TECH): (
+ (
+ ZorkGrandInquisitorItems.SPELL_REZROV,
+ ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.HADES_SHORE): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.PORT_FOOZLE): None,
+ (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS): (
+ (
+ ZorkGrandInquisitorItems.SUBWAY_TOKEN,
+ ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.SUBWAY_MONASTERY): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.DM_LAIR, ZorkGrandInquisitorRegions.CROSSROADS): None,
+ (ZorkGrandInquisitorRegions.DM_LAIR, ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR): (
+ (
+ ZorkGrandInquisitorEvents.DOOR_SMOKED_CIGAR,
+ ZorkGrandInquisitorEvents.DOOR_DRANK_MEAD,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.DM_LAIR, ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.DM_LAIR, ZorkGrandInquisitorRegions.HADES_SHORE): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.DM_LAIR, ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.DM_LAIR, ZorkGrandInquisitorRegions.SUBWAY_MONASTERY): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, ZorkGrandInquisitorRegions.DM_LAIR): None,
+ (ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, ZorkGrandInquisitorRegions.WALKING_CASTLE): (
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_BLINDS,
+ ZorkGrandInquisitorEvents.KNOWS_OBIDIL,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, ZorkGrandInquisitorRegions.WHITE_HOUSE): (
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR,
+ ZorkGrandInquisitorItems.SPELL_NARWILE,
+ ZorkGrandInquisitorEvents.KNOWS_YASTARD,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO, ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON): (
+ (
+ ZorkGrandInquisitorItems.TOTEM_GRIFF,
+ ZorkGrandInquisitorItems.HOTSPOT_DRAGON_CLAW,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO, ZorkGrandInquisitorRegions.HADES_BEYOND_GATES): None,
+ (ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO): None,
+ (ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, ZorkGrandInquisitorRegions.ENDGAME): (
+ (
+ ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN,
+ ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS,
+ ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH,
+ ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4,
+ ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY,
+ ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS,
+ ZorkGrandInquisitorRegions.WHITE_HOUSE,
+ ZorkGrandInquisitorItems.TOTEM_BROG, # Needed here since White House is not broken down in 2 regions
+ ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH,
+ ZorkGrandInquisitorItems.BROGS_GRUE_EGG,
+ ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT,
+ ZorkGrandInquisitorItems.BROGS_PLANK,
+ ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.GUE_TECH, ZorkGrandInquisitorRegions.CROSSROADS): None,
+ (ZorkGrandInquisitorRegions.GUE_TECH, ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY): (
+ (
+ ZorkGrandInquisitorItems.SPELL_IGRAM,
+ ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.GUE_TECH, ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE): (
+ (ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_DOOR,),
+ ),
+ (ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, ZorkGrandInquisitorRegions.GUE_TECH): None,
+ (ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE): (
+ (
+ ZorkGrandInquisitorItems.STUDENT_ID,
+ ZorkGrandInquisitorItems.HOTSPOT_STUDENT_ID_MACHINE,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.CROSSROADS): (
+ (ZorkGrandInquisitorItems.MAP,),
+ ),
+ (ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.DM_LAIR): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.GUE_TECH): None,
+ (ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.HADES_SHORE): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.SUBWAY_MONASTERY): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.HADES, ZorkGrandInquisitorRegions.HADES_BEYOND_GATES): (
+ (
+ ZorkGrandInquisitorEvents.KNOWS_SNAVIG,
+ ZorkGrandInquisitorItems.TOTEM_BROG, # Visually hiding this totem is tied to owning it; no choice
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.HADES, ZorkGrandInquisitorRegions.HADES_SHORE): (
+ (ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,),
+ ),
+ (ZorkGrandInquisitorRegions.HADES_BEYOND_GATES, ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO): (
+ (
+ ZorkGrandInquisitorItems.SPELL_NARWILE,
+ ZorkGrandInquisitorEvents.KNOWS_YASTARD,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.HADES_BEYOND_GATES, ZorkGrandInquisitorRegions.HADES): None,
+ (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.CROSSROADS): (
+ (ZorkGrandInquisitorItems.MAP,),
+ ),
+ (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.DM_LAIR): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.HADES): (
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER,
+ ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS,
+ ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS): None,
+ (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM): (
+ (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM,),
+ ),
+ (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.SUBWAY_MONASTERY): (
+ (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY,),
+ ),
+ (ZorkGrandInquisitorRegions.MENU, ZorkGrandInquisitorRegions.PORT_FOOZLE): None,
+ (ZorkGrandInquisitorRegions.MONASTERY, ZorkGrandInquisitorRegions.HADES_SHORE): (
+ (
+ ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL,
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS,
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.MONASTERY, ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT): (
+ (
+ ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION,
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS,
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.MONASTERY, ZorkGrandInquisitorRegions.SUBWAY_MONASTERY): None,
+ (ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, ZorkGrandInquisitorRegions.MONASTERY): None,
+ (ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST): (
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER,
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT,
+ ZorkGrandInquisitorItems.LARGE_TELEGRAPH_HAMMER,
+ ZorkGrandInquisitorItems.SPELL_NARWILE,
+ ZorkGrandInquisitorEvents.KNOWS_YASTARD,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.PORT_FOOZLE, ZorkGrandInquisitorRegions.CROSSROADS): (
+ (
+ ZorkGrandInquisitorEvents.LANTERN_DALBOZ_ACCESSIBLE,
+ ZorkGrandInquisitorItems.ROPE,
+ ZorkGrandInquisitorItems.HOTSPOT_WELL,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.PORT_FOOZLE, ZorkGrandInquisitorRegions.PORT_FOOZLE_JACKS_SHOP): (
+ (
+ ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE,
+ ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.PORT_FOOZLE_JACKS_SHOP, ZorkGrandInquisitorRegions.PORT_FOOZLE): None,
+ (ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST, ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT): None,
+ (ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST, ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN): (
+ (
+ ZorkGrandInquisitorItems.TOTEM_LUCY,
+ ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, ZorkGrandInquisitorRegions.ENDGAME): (
+ (
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4,
+ ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY,
+ ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS,
+ ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON,
+ ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN,
+ ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS,
+ ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH,
+ ZorkGrandInquisitorRegions.WHITE_HOUSE,
+ ZorkGrandInquisitorItems.TOTEM_BROG, # Needed here since White House is not broken down in 2 regions
+ ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH,
+ ZorkGrandInquisitorItems.BROGS_GRUE_EGG,
+ ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT,
+ ZorkGrandInquisitorItems.BROGS_PLANK,
+ ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST): None,
+ (ZorkGrandInquisitorRegions.SPELL_LAB, ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE): None,
+ (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.CROSSROADS): (
+ (ZorkGrandInquisitorItems.MAP,),
+ ),
+ (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.DM_LAIR): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY): None,
+ (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.HADES_SHORE): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.SPELL_LAB): (
+ (
+ ZorkGrandInquisitorItems.SWORD,
+ ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE,
+ ZorkGrandInquisitorEvents.DAM_DESTROYED,
+ ZorkGrandInquisitorItems.SPELL_GOLGATEM,
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.SUBWAY_MONASTERY): (
+ (
+ ZorkGrandInquisitorItems.MAP,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS, ZorkGrandInquisitorRegions.CROSSROADS): None,
+ (ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS, ZorkGrandInquisitorRegions.HADES_SHORE): (
+ (
+ ZorkGrandInquisitorItems.SPELL_KENDALL,
+ ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS, ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM): (
+ (
+ ZorkGrandInquisitorItems.SPELL_KENDALL,
+ ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS, ZorkGrandInquisitorRegions.SUBWAY_MONASTERY): (
+ (
+ ZorkGrandInquisitorItems.SPELL_KENDALL,
+ ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, ZorkGrandInquisitorRegions.HADES_SHORE): (
+ (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES,),
+ ),
+ (ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS): None,
+ (ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, ZorkGrandInquisitorRegions.SUBWAY_MONASTERY): (
+ (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY,),
+ ),
+ (ZorkGrandInquisitorRegions.SUBWAY_MONASTERY, ZorkGrandInquisitorRegions.HADES_SHORE): (
+ (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES,),
+ ),
+ (ZorkGrandInquisitorRegions.SUBWAY_MONASTERY, ZorkGrandInquisitorRegions.MONASTERY): (
+ (
+ ZorkGrandInquisitorItems.SWORD,
+ ZorkGrandInquisitorEvents.ROPE_GLORFABLE,
+ ZorkGrandInquisitorItems.HOTSPOT_MONASTERY_VENT,
+ ),
+ ),
+ (ZorkGrandInquisitorRegions.SUBWAY_MONASTERY, ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS): None,
+ (ZorkGrandInquisitorRegions.SUBWAY_MONASTERY, ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM): (
+ (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM,),
+ ),
+ (ZorkGrandInquisitorRegions.WALKING_CASTLE, ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR): None,
+ (ZorkGrandInquisitorRegions.WHITE_HOUSE, ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR): None,
+ (ZorkGrandInquisitorRegions.WHITE_HOUSE, ZorkGrandInquisitorRegions.ENDGAME): (
+ (
+ ZorkGrandInquisitorItems.TOTEM_BROG, # Needed here since White House is not broken down in 2 regions
+ ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH,
+ ZorkGrandInquisitorItems.BROGS_GRUE_EGG,
+ ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT,
+ ZorkGrandInquisitorItems.BROGS_PLANK,
+ ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE,
+ ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON,
+ ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN,
+ ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS,
+ ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH,
+ ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4,
+ ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY,
+ ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS,
+ ),
+ ),
+}
diff --git a/worlds/zork_grand_inquisitor/data/item_data.py b/worlds/zork_grand_inquisitor/data/item_data.py
new file mode 100644
index 000000000000..c312bbce3d09
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/data/item_data.py
@@ -0,0 +1,792 @@
+from typing import Dict, NamedTuple, Optional, Tuple, Union
+
+from BaseClasses import ItemClassification
+
+from ..enums import ZorkGrandInquisitorItems, ZorkGrandInquisitorTags
+
+
+class ZorkGrandInquisitorItemData(NamedTuple):
+ statemap_keys: Optional[Tuple[int, ...]]
+ archipelago_id: Optional[int]
+ classification: ItemClassification
+ tags: Tuple[ZorkGrandInquisitorTags, ...]
+ maximum_quantity: Optional[int] = 1
+
+
+ITEM_OFFSET = 9758067000
+
+item_data: Dict[ZorkGrandInquisitorItems, ZorkGrandInquisitorItemData] = {
+ # Inventory Items
+ ZorkGrandInquisitorItems.BROGS_BICKERING_TORCH: ZorkGrandInquisitorItemData(
+ statemap_keys=(67,), # Extinguished = 103
+ archipelago_id=ITEM_OFFSET + 0,
+ classification=ItemClassification.filler,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH: ZorkGrandInquisitorItemData(
+ statemap_keys=(68,), # Extinguished = 104
+ archipelago_id=ITEM_OFFSET + 1,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.BROGS_GRUE_EGG: ZorkGrandInquisitorItemData(
+ statemap_keys=(70,), # Boiled = 71
+ archipelago_id=ITEM_OFFSET + 2,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.BROGS_PLANK: ZorkGrandInquisitorItemData(
+ statemap_keys=(69,),
+ archipelago_id=ITEM_OFFSET + 3,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.FLATHEADIA_FUDGE: ZorkGrandInquisitorItemData(
+ statemap_keys=(54,),
+ archipelago_id=ITEM_OFFSET + 4,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP: ZorkGrandInquisitorItemData(
+ statemap_keys=(86,),
+ archipelago_id=ITEM_OFFSET + 5,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH: ZorkGrandInquisitorItemData(
+ statemap_keys=(84,),
+ archipelago_id=ITEM_OFFSET + 6,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT: ZorkGrandInquisitorItemData(
+ statemap_keys=(9,),
+ archipelago_id=ITEM_OFFSET + 7,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN: ZorkGrandInquisitorItemData(
+ statemap_keys=(16,),
+ archipelago_id=ITEM_OFFSET + 8,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.HAMMER: ZorkGrandInquisitorItemData(
+ statemap_keys=(23,),
+ archipelago_id=ITEM_OFFSET + 9,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.HUNGUS_LARD: ZorkGrandInquisitorItemData(
+ statemap_keys=(55,),
+ archipelago_id=ITEM_OFFSET + 10,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.JAR_OF_HOTBUGS: ZorkGrandInquisitorItemData(
+ statemap_keys=(56,),
+ archipelago_id=ITEM_OFFSET + 11,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.LANTERN: ZorkGrandInquisitorItemData(
+ statemap_keys=(4,),
+ archipelago_id=ITEM_OFFSET + 12,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.LARGE_TELEGRAPH_HAMMER: ZorkGrandInquisitorItemData(
+ statemap_keys=(88,),
+ archipelago_id=ITEM_OFFSET + 13,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1: ZorkGrandInquisitorItemData(
+ statemap_keys=(116,), # With fly = 120
+ archipelago_id=ITEM_OFFSET + 14,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2: ZorkGrandInquisitorItemData(
+ statemap_keys=(117,), # With fly = 121
+ archipelago_id=ITEM_OFFSET + 15,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3: ZorkGrandInquisitorItemData(
+ statemap_keys=(118,), # With fly = 122
+ archipelago_id=ITEM_OFFSET + 16,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4: ZorkGrandInquisitorItemData(
+ statemap_keys=(119,), # With fly = 123
+ archipelago_id=ITEM_OFFSET + 17,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.MAP: ZorkGrandInquisitorItemData(
+ statemap_keys=(6,),
+ archipelago_id=ITEM_OFFSET + 18,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.MEAD_LIGHT: ZorkGrandInquisitorItemData(
+ statemap_keys=(2,),
+ archipelago_id=ITEM_OFFSET + 19,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.MOSS_OF_MAREILON: ZorkGrandInquisitorItemData(
+ statemap_keys=(57,),
+ archipelago_id=ITEM_OFFSET + 20,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.MUG: ZorkGrandInquisitorItemData(
+ statemap_keys=(35,),
+ archipelago_id=ITEM_OFFSET + 21,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.OLD_SCRATCH_CARD: ZorkGrandInquisitorItemData(
+ statemap_keys=(17,),
+ archipelago_id=ITEM_OFFSET + 22,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.PERMA_SUCK_MACHINE: ZorkGrandInquisitorItemData(
+ statemap_keys=(36,),
+ archipelago_id=ITEM_OFFSET + 23,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.PLASTIC_SIX_PACK_HOLDER: ZorkGrandInquisitorItemData(
+ statemap_keys=(3,),
+ archipelago_id=ITEM_OFFSET + 24,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS: ZorkGrandInquisitorItemData(
+ statemap_keys=(5827,),
+ archipelago_id=ITEM_OFFSET + 25,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.PROZORK_TABLET: ZorkGrandInquisitorItemData(
+ statemap_keys=(65,),
+ archipelago_id=ITEM_OFFSET + 26,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.QUELBEE_HONEYCOMB: ZorkGrandInquisitorItemData(
+ statemap_keys=(53,),
+ archipelago_id=ITEM_OFFSET + 27,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.ROPE: ZorkGrandInquisitorItemData(
+ statemap_keys=(83,),
+ archipelago_id=ITEM_OFFSET + 28,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.SCROLL_FRAGMENT_ANS: ZorkGrandInquisitorItemData(
+ statemap_keys=(101,), # SNA = 41
+ archipelago_id=ITEM_OFFSET + 29,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.SCROLL_FRAGMENT_GIV: ZorkGrandInquisitorItemData(
+ statemap_keys=(102,), # VIG = 48
+ archipelago_id=ITEM_OFFSET + 30,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.SHOVEL: ZorkGrandInquisitorItemData(
+ statemap_keys=(49,),
+ archipelago_id=ITEM_OFFSET + 31,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.SNAPDRAGON: ZorkGrandInquisitorItemData(
+ statemap_keys=(50,),
+ archipelago_id=ITEM_OFFSET + 32,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.STUDENT_ID: ZorkGrandInquisitorItemData(
+ statemap_keys=(39,),
+ archipelago_id=ITEM_OFFSET + 33,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.SUBWAY_TOKEN: ZorkGrandInquisitorItemData(
+ statemap_keys=(20,),
+ archipelago_id=ITEM_OFFSET + 34,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.SWORD: ZorkGrandInquisitorItemData(
+ statemap_keys=(21,),
+ archipelago_id=ITEM_OFFSET + 35,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.ZIMDOR_SCROLL: ZorkGrandInquisitorItemData(
+ statemap_keys=(25,),
+ archipelago_id=ITEM_OFFSET + 36,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ ZorkGrandInquisitorItems.ZORK_ROCKS: ZorkGrandInquisitorItemData(
+ statemap_keys=(37,),
+ archipelago_id=ITEM_OFFSET + 37,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,),
+ ),
+ # Hotspots
+ ZorkGrandInquisitorItems.HOTSPOT_666_MAILBOX: ZorkGrandInquisitorItemData(
+ statemap_keys=(9116,),
+ archipelago_id=ITEM_OFFSET + 100 + 0,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS: ZorkGrandInquisitorItemData(
+ statemap_keys=(15434, 15436, 15438, 15440),
+ archipelago_id=ITEM_OFFSET + 100 + 1,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_BLANK_SCROLL_BOX: ZorkGrandInquisitorItemData(
+ statemap_keys=(12096,),
+ archipelago_id=ITEM_OFFSET + 100 + 2,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_BLINDS: ZorkGrandInquisitorItemData(
+ statemap_keys=(4799,),
+ archipelago_id=ITEM_OFFSET + 100 + 3,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS: ZorkGrandInquisitorItemData(
+ statemap_keys=(12691, 12692, 12693, 12694, 12695, 12696, 12697, 12698, 12699, 12700, 12701),
+ archipelago_id=ITEM_OFFSET + 100 + 4,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT: ZorkGrandInquisitorItemData(
+ statemap_keys=(12702,),
+ archipelago_id=ITEM_OFFSET + 100 + 5,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_VACUUM_SLOT: ZorkGrandInquisitorItemData(
+ statemap_keys=(12909,),
+ archipelago_id=ITEM_OFFSET + 100 + 6,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_CHANGE_MACHINE_SLOT: ZorkGrandInquisitorItemData(
+ statemap_keys=(12900,),
+ archipelago_id=ITEM_OFFSET + 100 + 7,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR: ZorkGrandInquisitorItemData(
+ statemap_keys=(5010,),
+ archipelago_id=ITEM_OFFSET + 100 + 8,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT: ZorkGrandInquisitorItemData(
+ statemap_keys=(9539,),
+ archipelago_id=ITEM_OFFSET + 100 + 9,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER: ZorkGrandInquisitorItemData(
+ statemap_keys=(19712,),
+ archipelago_id=ITEM_OFFSET + 100 + 10,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT: ZorkGrandInquisitorItemData(
+ statemap_keys=(2586,),
+ archipelago_id=ITEM_OFFSET + 100 + 11,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_DENTED_LOCKER: ZorkGrandInquisitorItemData(
+ statemap_keys=(11878,),
+ archipelago_id=ITEM_OFFSET + 100 + 12,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_DIRT_MOUND: ZorkGrandInquisitorItemData(
+ statemap_keys=(11751,),
+ archipelago_id=ITEM_OFFSET + 100 + 13,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH: ZorkGrandInquisitorItemData(
+ statemap_keys=(15147, 15153),
+ archipelago_id=ITEM_OFFSET + 100 + 14,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_DRAGON_CLAW: ZorkGrandInquisitorItemData(
+ statemap_keys=(1705,),
+ archipelago_id=ITEM_OFFSET + 100 + 15,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS: ZorkGrandInquisitorItemData(
+ statemap_keys=(1425, 1426),
+ archipelago_id=ITEM_OFFSET + 100 + 16,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE: ZorkGrandInquisitorItemData(
+ statemap_keys=(13106,),
+ archipelago_id=ITEM_OFFSET + 100 + 17,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS: ZorkGrandInquisitorItemData(
+ statemap_keys=(13219, 13220, 13221, 13222),
+ archipelago_id=ITEM_OFFSET + 100 + 18,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS: ZorkGrandInquisitorItemData(
+ statemap_keys=(14327, 14332, 14337, 14342),
+ archipelago_id=ITEM_OFFSET + 100 + 19,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT: ZorkGrandInquisitorItemData(
+ statemap_keys=(12528,),
+ archipelago_id=ITEM_OFFSET + 100 + 20,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS: ZorkGrandInquisitorItemData(
+ statemap_keys=(12523, 12524, 12525),
+ archipelago_id=ITEM_OFFSET + 100 + 21,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_GLASS_CASE: ZorkGrandInquisitorItemData(
+ statemap_keys=(13002,),
+ archipelago_id=ITEM_OFFSET + 100 + 22,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL: ZorkGrandInquisitorItemData(
+ statemap_keys=(10726,),
+ archipelago_id=ITEM_OFFSET + 100 + 23,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_DOOR: ZorkGrandInquisitorItemData(
+ statemap_keys=(12280,),
+ archipelago_id=ITEM_OFFSET + 100 + 24,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_GRASS: ZorkGrandInquisitorItemData(
+ statemap_keys=(
+ 17694,
+ 17695,
+ 17696,
+ 17697,
+ 18200,
+ 17703,
+ 17704,
+ 17705,
+ 17710,
+ 17711,
+ 17712,
+ 17713,
+ 17714,
+ 17715,
+ 17716,
+ 17722,
+ 17723,
+ 17724,
+ 17725,
+ 17726,
+ 17727
+ ),
+ archipelago_id=ITEM_OFFSET + 100 + 25,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS: ZorkGrandInquisitorItemData(
+ statemap_keys=(8448, 8449, 8450, 8451, 8452, 8453, 8454, 8455, 8456, 8457, 8458, 8459),
+ archipelago_id=ITEM_OFFSET + 100 + 26,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER: ZorkGrandInquisitorItemData(
+ statemap_keys=(8446,),
+ archipelago_id=ITEM_OFFSET + 100 + 27,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_HARRY: ZorkGrandInquisitorItemData(
+ statemap_keys=(4260,),
+ archipelago_id=ITEM_OFFSET + 100 + 28,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY: ZorkGrandInquisitorItemData(
+ statemap_keys=(18026,),
+ archipelago_id=ITEM_OFFSET + 100 + 29,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH: ZorkGrandInquisitorItemData(
+ statemap_keys=(17623,),
+ archipelago_id=ITEM_OFFSET + 100 + 30,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR: ZorkGrandInquisitorItemData(
+ statemap_keys=(13140,),
+ archipelago_id=ITEM_OFFSET + 100 + 31,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR: ZorkGrandInquisitorItemData(
+ statemap_keys=(10441,),
+ archipelago_id=ITEM_OFFSET + 100 + 32,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_LOUDSPEAKER_VOLUME_BUTTONS: ZorkGrandInquisitorItemData(
+ statemap_keys=(19632, 19627),
+ archipelago_id=ITEM_OFFSET + 100 + 33,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_DOOR: ZorkGrandInquisitorItemData(
+ statemap_keys=(3025,),
+ archipelago_id=ITEM_OFFSET + 100 + 34,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG: ZorkGrandInquisitorItemData(
+ statemap_keys=(3036,),
+ archipelago_id=ITEM_OFFSET + 100 + 35,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_MIRROR: ZorkGrandInquisitorItemData(
+ statemap_keys=(5031,),
+ archipelago_id=ITEM_OFFSET + 100 + 36,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_MONASTERY_VENT: ZorkGrandInquisitorItemData(
+ statemap_keys=(13597,),
+ archipelago_id=ITEM_OFFSET + 100 + 37,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_MOSSY_GRATE: ZorkGrandInquisitorItemData(
+ statemap_keys=(13390,),
+ archipelago_id=ITEM_OFFSET + 100 + 38,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR: ZorkGrandInquisitorItemData(
+ statemap_keys=(2455, 2447),
+ archipelago_id=ITEM_OFFSET + 100 + 39,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS: ZorkGrandInquisitorItemData(
+ statemap_keys=(12389, 12390),
+ archipelago_id=ITEM_OFFSET + 100 + 40,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE: ZorkGrandInquisitorItemData(
+ statemap_keys=(4302,),
+ archipelago_id=ITEM_OFFSET + 100 + 41,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE: ZorkGrandInquisitorItemData(
+ statemap_keys=(16383, 16384),
+ archipelago_id=ITEM_OFFSET + 100 + 42,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE: ZorkGrandInquisitorItemData(
+ statemap_keys=(2769,),
+ archipelago_id=ITEM_OFFSET + 100 + 43,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON: ZorkGrandInquisitorItemData(
+ statemap_keys=(4149,),
+ archipelago_id=ITEM_OFFSET + 100 + 44,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_BUTTONS: ZorkGrandInquisitorItemData(
+ statemap_keys=(12584, 12585, 12586, 12587),
+ archipelago_id=ITEM_OFFSET + 100 + 45,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_COIN_SLOT: ZorkGrandInquisitorItemData(
+ statemap_keys=(12574,),
+ archipelago_id=ITEM_OFFSET + 100 + 46,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_SOUVENIR_COIN_SLOT: ZorkGrandInquisitorItemData(
+ statemap_keys=(13412,),
+ archipelago_id=ITEM_OFFSET + 100 + 47,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER: ZorkGrandInquisitorItemData(
+ statemap_keys=(12170,),
+ archipelago_id=ITEM_OFFSET + 100 + 48,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM: ZorkGrandInquisitorItemData(
+ statemap_keys=(16382,),
+ archipelago_id=ITEM_OFFSET + 100 + 49,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM: ZorkGrandInquisitorItemData(
+ statemap_keys=(4209,),
+ archipelago_id=ITEM_OFFSET + 100 + 50,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_STUDENT_ID_MACHINE: ZorkGrandInquisitorItemData(
+ statemap_keys=(11973,),
+ archipelago_id=ITEM_OFFSET + 100 + 51,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT: ZorkGrandInquisitorItemData(
+ statemap_keys=(13168,),
+ archipelago_id=ITEM_OFFSET + 100 + 52,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY: ZorkGrandInquisitorItemData(
+ statemap_keys=(15396,),
+ archipelago_id=ITEM_OFFSET + 100 + 53,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH: ZorkGrandInquisitorItemData(
+ statemap_keys=(9706,),
+ archipelago_id=ITEM_OFFSET + 100 + 54,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS: ZorkGrandInquisitorItemData(
+ statemap_keys=(9728, 9729, 9730),
+ archipelago_id=ITEM_OFFSET + 100 + 55,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ ZorkGrandInquisitorItems.HOTSPOT_WELL: ZorkGrandInquisitorItemData(
+ statemap_keys=(10314,),
+ archipelago_id=ITEM_OFFSET + 100 + 56,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.HOTSPOT,),
+ ),
+ # Spells
+ ZorkGrandInquisitorItems.SPELL_GLORF: ZorkGrandInquisitorItemData(
+ statemap_keys=(202,),
+ archipelago_id=ITEM_OFFSET + 200 + 0,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.SPELL,),
+ ),
+ ZorkGrandInquisitorItems.SPELL_GOLGATEM: ZorkGrandInquisitorItemData(
+ statemap_keys=(192,),
+ archipelago_id=ITEM_OFFSET + 200 + 1,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.SPELL,),
+ ),
+ ZorkGrandInquisitorItems.SPELL_IGRAM: ZorkGrandInquisitorItemData(
+ statemap_keys=(199,),
+ archipelago_id=ITEM_OFFSET + 200 + 2,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.SPELL,),
+ ),
+ ZorkGrandInquisitorItems.SPELL_KENDALL: ZorkGrandInquisitorItemData(
+ statemap_keys=(196,),
+ archipelago_id=ITEM_OFFSET + 200 + 3,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.SPELL,),
+ ),
+ ZorkGrandInquisitorItems.SPELL_NARWILE: ZorkGrandInquisitorItemData(
+ statemap_keys=(197,),
+ archipelago_id=ITEM_OFFSET + 200 + 4,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.SPELL,),
+ ),
+ ZorkGrandInquisitorItems.SPELL_REZROV: ZorkGrandInquisitorItemData(
+ statemap_keys=(195,),
+ archipelago_id=ITEM_OFFSET + 200 + 5,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.SPELL,),
+ ),
+ ZorkGrandInquisitorItems.SPELL_THROCK: ZorkGrandInquisitorItemData(
+ statemap_keys=(200,),
+ archipelago_id=ITEM_OFFSET + 200 + 6,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.SPELL,),
+ ),
+ ZorkGrandInquisitorItems.SPELL_VOXAM: ZorkGrandInquisitorItemData(
+ statemap_keys=(191,),
+ archipelago_id=ITEM_OFFSET + 200 + 7,
+ classification=ItemClassification.useful,
+ tags=(ZorkGrandInquisitorTags.SPELL,),
+ ),
+ # Subway Destinations
+ ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM: ZorkGrandInquisitorItemData(
+ statemap_keys=(13757, 13297, 13486, 13625),
+ archipelago_id=ITEM_OFFSET + 300 + 0,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.SUBWAY_DESTINATION,),
+ ),
+ ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES: ZorkGrandInquisitorItemData(
+ statemap_keys=(13758, 13309, 13498, 13637),
+ archipelago_id=ITEM_OFFSET + 300 + 1,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.SUBWAY_DESTINATION,),
+ ),
+ ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY: ZorkGrandInquisitorItemData(
+ statemap_keys=(13759, 13316, 13505, 13644),
+ archipelago_id=ITEM_OFFSET + 300 + 2,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.SUBWAY_DESTINATION,),
+ ),
+ # Teleporter Destinations
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR: ZorkGrandInquisitorItemData(
+ statemap_keys=(2203,),
+ archipelago_id=ITEM_OFFSET + 400 + 0,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.TELEPORTER_DESTINATION,),
+ ),
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH: ZorkGrandInquisitorItemData(
+ statemap_keys=(7132,),
+ archipelago_id=ITEM_OFFSET + 400 + 1,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.TELEPORTER_DESTINATION,),
+ ),
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES: ZorkGrandInquisitorItemData(
+ statemap_keys=(7119,),
+ archipelago_id=ITEM_OFFSET + 400 + 2,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.TELEPORTER_DESTINATION,),
+ ),
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY: ZorkGrandInquisitorItemData(
+ statemap_keys=(7148,),
+ archipelago_id=ITEM_OFFSET + 400 + 3,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.TELEPORTER_DESTINATION,),
+ ),
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB: ZorkGrandInquisitorItemData(
+ statemap_keys=(16545,),
+ archipelago_id=ITEM_OFFSET + 400 + 4,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.TELEPORTER_DESTINATION,),
+ ),
+ # Totemizer Destinations
+ ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION: ZorkGrandInquisitorItemData(
+ statemap_keys=(9660,),
+ archipelago_id=ITEM_OFFSET + 500 + 0,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.TOTEMIZER_DESTINATION,),
+ ),
+ ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY: ZorkGrandInquisitorItemData(
+ statemap_keys=(9666,),
+ archipelago_id=ITEM_OFFSET + 500 + 1,
+ classification=ItemClassification.filler,
+ tags=(ZorkGrandInquisitorTags.TOTEMIZER_DESTINATION,),
+ ),
+ ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL: ZorkGrandInquisitorItemData(
+ statemap_keys=(9668,),
+ archipelago_id=ITEM_OFFSET + 500 + 2,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.TOTEMIZER_DESTINATION,),
+ ),
+ ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ: ZorkGrandInquisitorItemData(
+ statemap_keys=(9662,),
+ archipelago_id=ITEM_OFFSET + 500 + 3,
+ classification=ItemClassification.filler,
+ tags=(ZorkGrandInquisitorTags.TOTEMIZER_DESTINATION,),
+ ),
+ # Totems
+ ZorkGrandInquisitorItems.TOTEM_BROG: ZorkGrandInquisitorItemData(
+ statemap_keys=(4853,),
+ archipelago_id=ITEM_OFFSET + 600 + 0,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.TOTEM,),
+ ),
+ ZorkGrandInquisitorItems.TOTEM_GRIFF: ZorkGrandInquisitorItemData(
+ statemap_keys=(4315,),
+ archipelago_id=ITEM_OFFSET + 600 + 1,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.TOTEM,),
+ ),
+ ZorkGrandInquisitorItems.TOTEM_LUCY: ZorkGrandInquisitorItemData(
+ statemap_keys=(5223,),
+ archipelago_id=ITEM_OFFSET + 600 + 2,
+ classification=ItemClassification.progression,
+ tags=(ZorkGrandInquisitorTags.TOTEM,),
+ ),
+ # Filler
+ ZorkGrandInquisitorItems.FILLER_INQUISITION_PROPAGANDA_FLYER: ZorkGrandInquisitorItemData(
+ statemap_keys=None,
+ archipelago_id=ITEM_OFFSET + 700 + 0,
+ classification=ItemClassification.filler,
+ tags=(ZorkGrandInquisitorTags.FILLER,),
+ maximum_quantity=None,
+ ),
+ ZorkGrandInquisitorItems.FILLER_UNREADABLE_SPELL_SCROLL: ZorkGrandInquisitorItemData(
+ statemap_keys=None,
+ archipelago_id=ITEM_OFFSET + 700 + 1,
+ classification=ItemClassification.filler,
+ tags=(ZorkGrandInquisitorTags.FILLER,),
+ maximum_quantity=None,
+ ),
+ ZorkGrandInquisitorItems.FILLER_MAGIC_CONTRABAND: ZorkGrandInquisitorItemData(
+ statemap_keys=None,
+ archipelago_id=ITEM_OFFSET + 700 + 2,
+ classification=ItemClassification.filler,
+ tags=(ZorkGrandInquisitorTags.FILLER,),
+ maximum_quantity=None,
+ ),
+ ZorkGrandInquisitorItems.FILLER_FROBOZZ_ELECTRIC_GADGET: ZorkGrandInquisitorItemData(
+ statemap_keys=None,
+ archipelago_id=ITEM_OFFSET + 700 + 3,
+ classification=ItemClassification.filler,
+ tags=(ZorkGrandInquisitorTags.FILLER,),
+ maximum_quantity=None,
+ ),
+ ZorkGrandInquisitorItems.FILLER_NONSENSICAL_INQUISITION_PAPERWORK: ZorkGrandInquisitorItemData(
+ statemap_keys=None,
+ archipelago_id=ITEM_OFFSET + 700 + 4,
+ classification=ItemClassification.filler,
+ tags=(ZorkGrandInquisitorTags.FILLER,),
+ maximum_quantity=None,
+ ),
+}
diff --git a/worlds/zork_grand_inquisitor/data/location_data.py b/worlds/zork_grand_inquisitor/data/location_data.py
new file mode 100644
index 000000000000..8b4e57392de8
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/data/location_data.py
@@ -0,0 +1,1535 @@
+from typing import Dict, NamedTuple, Optional, Tuple, Union
+
+from ..enums import (
+ ZorkGrandInquisitorEvents,
+ ZorkGrandInquisitorItems,
+ ZorkGrandInquisitorLocations,
+ ZorkGrandInquisitorRegions,
+ ZorkGrandInquisitorTags,
+)
+
+
+class ZorkGrandInquisitorLocationData(NamedTuple):
+ game_state_trigger: Optional[
+ Tuple[
+ Union[
+ Tuple[str, str],
+ Tuple[int, int],
+ Tuple[int, Tuple[int, ...]],
+ ],
+ ...,
+ ]
+ ]
+ archipelago_id: Optional[int]
+ region: ZorkGrandInquisitorRegions
+ tags: Optional[Tuple[ZorkGrandInquisitorTags, ...]] = None
+ requirements: Optional[
+ Tuple[
+ Union[
+ Union[
+ ZorkGrandInquisitorItems,
+ ZorkGrandInquisitorEvents,
+ ],
+ Tuple[
+ Union[
+ ZorkGrandInquisitorItems,
+ ZorkGrandInquisitorEvents,
+ ],
+ ...,
+ ],
+ ],
+ ...,
+ ]
+ ] = None
+ event_item_name: Optional[str] = None
+
+
+LOCATION_OFFSET = 9758067000
+
+location_data: Dict[
+ Union[ZorkGrandInquisitorLocations, ZorkGrandInquisitorEvents], ZorkGrandInquisitorLocationData
+] = {
+ ZorkGrandInquisitorLocations.ALARM_SYSTEM_IS_DOWN: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "tr2m"),),
+ archipelago_id=LOCATION_OFFSET + 0,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.ARREST_THE_VANDAL: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((10789, 1),),
+ archipelago_id=LOCATION_OFFSET + 1,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE,
+ ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.ARTIFACTS_EXPLAINED: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((11787, 1), (11788, 1), (11789, 1)),
+ archipelago_id=LOCATION_OFFSET + 2,
+ region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.A_BIG_FAT_SASSY_2_HEADED_MONSTER: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((8929, 1),),
+ archipelago_id=LOCATION_OFFSET + 3,
+ region=ZorkGrandInquisitorRegions.HADES,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorEvents.KNOWS_OBIDIL,),
+ ),
+ ZorkGrandInquisitorLocations.A_LETTER_FROM_THE_WHITE_HOUSE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((9124, 1),),
+ archipelago_id=LOCATION_OFFSET + 4,
+ region=ZorkGrandInquisitorRegions.HADES,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorEvents.WHITE_HOUSE_LETTER_MAILABLE,
+ ZorkGrandInquisitorItems.HOTSPOT_666_MAILBOX,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.A_SMALLWAY: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((11777, 1),),
+ archipelago_id=LOCATION_OFFSET + 5,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS,
+ ZorkGrandInquisitorItems.SPELL_IGRAM,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.BEAUTIFUL_THATS_PLENTY: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((13278, 1),),
+ archipelago_id=LOCATION_OFFSET + 6,
+ region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.HOTSPOT_MOSSY_GRATE,
+ ZorkGrandInquisitorItems.SPELL_THROCK,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.BEBURTT_DEMYSTIFIED: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((16315, 1),),
+ archipelago_id=LOCATION_OFFSET + 7,
+ region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE,
+ ZorkGrandInquisitorItems.SPELL_KENDALL,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.BETTER_SPELL_MANUFACTURING_IN_UNDER_10_MINUTES: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "th3x"),),
+ archipelago_id=LOCATION_OFFSET + 8,
+ region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE,),
+ ),
+ ZorkGrandInquisitorLocations.BOING_BOING_BOING: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4220, 1),),
+ archipelago_id=LOCATION_OFFSET + 9,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.HAMMER,
+ ZorkGrandInquisitorItems.SNAPDRAGON,
+ ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.BONK: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((19491, 1),),
+ archipelago_id=LOCATION_OFFSET + 10,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.HAMMER,
+ ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.BRAVE_SOULS_WANTED: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "us2g"),),
+ archipelago_id=LOCATION_OFFSET + 11,
+ region=ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.BROG_DO_GOOD: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((2644, 1),),
+ archipelago_id=LOCATION_OFFSET + 12,
+ region=ZorkGrandInquisitorRegions.WHITE_HOUSE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.TOTEM_BROG,
+ ZorkGrandInquisitorItems.BROGS_GRUE_EGG,
+ ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT,
+ ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH,
+ )
+ ),
+ ZorkGrandInquisitorLocations.BROG_EAT_ROCKS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((2629, 1),),
+ archipelago_id=LOCATION_OFFSET + 13,
+ region=ZorkGrandInquisitorRegions.WHITE_HOUSE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.TOTEM_BROG,
+ ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH,
+ )
+ ),
+ ZorkGrandInquisitorLocations.BROG_KNOW_DUMB_THAT_DUMB: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((2650, 1),),
+ archipelago_id=LOCATION_OFFSET + 14,
+ region=ZorkGrandInquisitorRegions.WHITE_HOUSE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.TOTEM_BROG,
+ ZorkGrandInquisitorItems.BROGS_GRUE_EGG,
+ ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH,
+ )
+ ),
+ ZorkGrandInquisitorLocations.BROG_MUCH_BETTER_AT_THIS_GAME: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((15715, 1),),
+ archipelago_id=LOCATION_OFFSET + 15,
+ region=ZorkGrandInquisitorRegions.WHITE_HOUSE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.TOTEM_BROG,
+ ZorkGrandInquisitorItems.BROGS_GRUE_EGG,
+ ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT,
+ ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH,
+ ZorkGrandInquisitorItems.BROGS_PLANK,
+ ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE,
+ )
+ ),
+ ZorkGrandInquisitorLocations.CASTLE_WATCHING_A_FIELD_GUIDE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "dv1t"),),
+ archipelago_id=LOCATION_OFFSET + 16,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.CAVES_NOTES: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "th3y"),),
+ archipelago_id=LOCATION_OFFSET + 17,
+ region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE,),
+ ),
+ ZorkGrandInquisitorLocations.CLOSING_THE_TIME_TUNNELS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((9543, 1),),
+ archipelago_id=LOCATION_OFFSET + 18,
+ region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.CRISIS_AVERTED: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((11769, 1),),
+ archipelago_id=LOCATION_OFFSET + 19,
+ region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_ACTIVATED,
+ ZorkGrandInquisitorItems.SPELL_IGRAM,
+ ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS,
+ ZorkGrandInquisitorItems.HOTSPOT_DENTED_LOCKER,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.CUT_THAT_OUT_YOU_LITTLE_CREEP: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((19350, 1),),
+ archipelago_id=LOCATION_OFFSET + 20,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.DENIED_BY_THE_LAKE_MONSTER: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((17632, 1),),
+ archipelago_id=LOCATION_OFFSET + 21,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.HOTSPOT_BLINDS,
+ ZorkGrandInquisitorItems.SPELL_GOLGATEM,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.DESPERATELY_SEEKING_TUTOR: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "tr2q"),),
+ archipelago_id=LOCATION_OFFSET + 22,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.DONT_EVEN_START_WITH_US_SPARKY: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "hp5e"), (8919, 2), (9, 100)),
+ archipelago_id=LOCATION_OFFSET + 23,
+ region=ZorkGrandInquisitorRegions.HADES,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorItems.SWORD,),
+ ),
+ ZorkGrandInquisitorLocations.DOOOOOOWN: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((3619, 3600),),
+ archipelago_id=LOCATION_OFFSET + 24,
+ region=ZorkGrandInquisitorRegions.WHITE_HOUSE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.TOTEM_GRIFF,
+ ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.DOWN: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((3619, 5300),),
+ archipelago_id=LOCATION_OFFSET + 25,
+ region=ZorkGrandInquisitorRegions.WHITE_HOUSE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.TOTEM_LUCY,
+ ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.DRAGON_ARCHIPELAGO_TIME_TUNNEL: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((9216, 1),),
+ archipelago_id=LOCATION_OFFSET + 26,
+ region=ZorkGrandInquisitorRegions.HADES_BEYOND_GATES,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorItems.SPELL_NARWILE,),
+ ),
+ ZorkGrandInquisitorLocations.DUNCE_LOCKER: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((11851, 1),),
+ archipelago_id=LOCATION_OFFSET + 27,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,
+ ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT,
+ ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.EGGPLANTS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((3816, 11000),),
+ archipelago_id=LOCATION_OFFSET + 28,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.ELSEWHERE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "pc1e"),),
+ archipelago_id=LOCATION_OFFSET + 29,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.EMERGENCY_MAGICATRONIC_MESSAGE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((11784, 1),),
+ archipelago_id=LOCATION_OFFSET + 30,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ ),
+ ZorkGrandInquisitorLocations.ENJOY_YOUR_TRIP: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((13743, 1),),
+ archipelago_id=LOCATION_OFFSET + 31,
+ region=ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorItems.SPELL_KENDALL,),
+ ),
+ ZorkGrandInquisitorLocations.FAT_LOT_OF_GOOD_THATLL_DO_YA: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((16368, 1),),
+ archipelago_id=LOCATION_OFFSET + 32,
+ region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(ZorkGrandInquisitorItems.SPELL_IGRAM,),
+ ),
+ ZorkGrandInquisitorLocations.FIRE_FIRE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((10277, 1),),
+ archipelago_id=LOCATION_OFFSET + 33,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE,
+ ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.FLOOD_CONTROL_DAM_3_THE_NOT_REMOTELY_BORING_TALE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "ue1h"),),
+ archipelago_id=LOCATION_OFFSET + 34,
+ region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.FLYING_SNAPDRAGON: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4222, 1),),
+ archipelago_id=LOCATION_OFFSET + 35,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.SPELL_THROCK,
+ ZorkGrandInquisitorItems.SNAPDRAGON,
+ ZorkGrandInquisitorItems.HAMMER,
+ ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.FROBUARY_3_UNDERGROUNDHOG_DAY: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "dw2g"),),
+ archipelago_id=LOCATION_OFFSET + 36,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.GETTING_SOME_CHANGE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((12892, 1),),
+ archipelago_id=LOCATION_OFFSET + 37,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorEvents.ZORKMID_BILL_ACCESSIBLE,
+ ZorkGrandInquisitorItems.HOTSPOT_CHANGE_MACHINE_SLOT,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.GO_AWAY: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((10654, 1),),
+ archipelago_id=LOCATION_OFFSET + 38,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.GUE_TECH_DEANS_LIST: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "tr2k"),),
+ archipelago_id=LOCATION_OFFSET + 39,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.GUE_TECH_ENTRANCE_EXAM: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((11082, 1), (11307, 1), (11536, 1)),
+ archipelago_id=LOCATION_OFFSET + 40,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.GUE_TECH_HEALTH_MEMO: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "tr2j"),),
+ archipelago_id=LOCATION_OFFSET + 41,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.GUE_TECH_MAGEMEISTERS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "tr2n"),),
+ archipelago_id=LOCATION_OFFSET + 42,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.HAVE_A_HELL_OF_A_DAY: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((8443, 1),),
+ archipelago_id=LOCATION_OFFSET + 43,
+ region=ZorkGrandInquisitorRegions.HADES_SHORE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER,
+ ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS,
+ )
+ ),
+ ZorkGrandInquisitorLocations.HELLO_THIS_IS_SHONA_FROM_GURTH_PUBLISHING: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4698, 1),),
+ archipelago_id=LOCATION_OFFSET + 44,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.HELP_ME_CANT_BREATHE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((10421, 1),),
+ archipelago_id=LOCATION_OFFSET + 45,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH,
+ ZorkGrandInquisitorItems.PLASTIC_SIX_PACK_HOLDER,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.HEY_FREE_DIRT: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((11747, 1),),
+ archipelago_id=LOCATION_OFFSET + 46,
+ region=ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.HOTSPOT_DIRT_MOUND,
+ ZorkGrandInquisitorItems.SHOVEL,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.HI_MY_NAME_IS_DOUG: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4698, 2),),
+ archipelago_id=LOCATION_OFFSET + 47,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.HMMM_INFORMATIVE_YET_DEEPLY_DISTURBING: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "mt2h"),),
+ archipelago_id=LOCATION_OFFSET + 48,
+ region=ZorkGrandInquisitorRegions.MONASTERY,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.HOLD_ON_FOR_AN_IMPORTANT_MESSAGE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4698, 5),),
+ archipelago_id=LOCATION_OFFSET + 49,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.HOW_TO_HYPNOTIZE_YOURSELF: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "uh1e"),),
+ archipelago_id=LOCATION_OFFSET + 50,
+ region=ZorkGrandInquisitorRegions.HADES_SHORE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.HOW_TO_WIN_AT_DOUBLE_FANUCCI: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "th3s"),),
+ archipelago_id=LOCATION_OFFSET + 51,
+ region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorEvents.DALBOZ_LOCKER_OPENABLE,),
+ ),
+ ZorkGrandInquisitorLocations.IMBUE_BEBURTT: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((194, 1),),
+ archipelago_id=LOCATION_OFFSET + 52,
+ region=ZorkGrandInquisitorRegions.SPELL_LAB,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.HOTSPOT_BLANK_SCROLL_BOX,
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.IM_COMPLETELY_NUDE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((19344, 1),),
+ archipelago_id=LOCATION_OFFSET + 53,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.INTO_THE_FOLIAGE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((13060, 1),),
+ archipelago_id=LOCATION_OFFSET + 54,
+ region=ZorkGrandInquisitorRegions.CROSSROADS,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.SWORD,
+ ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.INVISIBLE_FLOWERS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((12967, 1),),
+ archipelago_id=LOCATION_OFFSET + 55,
+ region=ZorkGrandInquisitorRegions.CROSSROADS,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorItems.SPELL_IGRAM,),
+ ),
+ ZorkGrandInquisitorLocations.IN_CASE_OF_ADVENTURE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((12931, 1),),
+ archipelago_id=LOCATION_OFFSET + 56,
+ region=ZorkGrandInquisitorRegions.CROSSROADS,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.HAMMER,
+ ZorkGrandInquisitorItems.HOTSPOT_GLASS_CASE,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.IN_MAGIC_WE_TRUST: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((13062, 1),),
+ archipelago_id=LOCATION_OFFSET + 57,
+ region=ZorkGrandInquisitorRegions.CROSSROADS,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.SPELL_REZROV,
+ ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.ITS_ONE_OF_THOSE_ADVENTURERS_AGAIN: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "pe3j"),),
+ archipelago_id=LOCATION_OFFSET + 58,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.I_DONT_THINK_YOU_WOULDVE_WANTED_THAT_TO_WORK_ANYWAY: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((3816, 1008),),
+ archipelago_id=LOCATION_OFFSET + 59,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.SPELL_THROCK,
+ ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.I_DONT_WANT_NO_TROUBLE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((10694, 1),),
+ archipelago_id=LOCATION_OFFSET + 60,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.I_HOPE_YOU_CAN_CLIMB_UP_THERE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((9637, 1),),
+ archipelago_id=LOCATION_OFFSET + 61,
+ region=ZorkGrandInquisitorRegions.SUBWAY_MONASTERY,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.SWORD,
+ ZorkGrandInquisitorEvents.ROPE_GLORFABLE,
+ ZorkGrandInquisitorItems.HOTSPOT_MONASTERY_VENT,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.I_LIKE_YOUR_STYLE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((16374, 1),),
+ archipelago_id=LOCATION_OFFSET + 62,
+ region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.SWORD,
+ ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE,
+ ZorkGrandInquisitorEvents.DAM_DESTROYED,
+ ZorkGrandInquisitorItems.SPELL_GOLGATEM,
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.I_SPIT_ON_YOUR_FILTHY_COINAGE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "tp1e"), (9, 87), (1011, 1)),
+ archipelago_id=LOCATION_OFFSET + 63,
+ region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,),
+ ),
+ ZorkGrandInquisitorLocations.LIT_SUNFLOWERS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4129, 1),),
+ archipelago_id=LOCATION_OFFSET + 64,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorItems.SPELL_THROCK,),
+ ),
+ ZorkGrandInquisitorLocations.MAGIC_FOREVER: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "pc1e"), (10304, 1), (5221, 1)),
+ archipelago_id=LOCATION_OFFSET + 65,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorEvents.LANTERN_DALBOZ_ACCESSIBLE,
+ ZorkGrandInquisitorItems.ROPE,
+ ZorkGrandInquisitorItems.HOTSPOT_WELL,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.MAILED_IT_TO_HELL: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((2498, (1, 2)),),
+ archipelago_id=LOCATION_OFFSET + 66,
+ region=ZorkGrandInquisitorRegions.WHITE_HOUSE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ (ZorkGrandInquisitorItems.TOTEM_GRIFF, ZorkGrandInquisitorItems.TOTEM_LUCY),
+ ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_DOOR,
+ ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.MAKE_LOVE_NOT_WAR: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((8623, 21),),
+ archipelago_id=LOCATION_OFFSET + 67,
+ region=ZorkGrandInquisitorRegions.HADES_SHORE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorEvents.CHARON_CALLED,
+ ZorkGrandInquisitorItems.SWORD,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.MEAD_LIGHT: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((10485, 1),),
+ archipelago_id=LOCATION_OFFSET + 68,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.MEAD_LIGHT,
+ ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.MIKES_PANTS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "tr2p"),),
+ archipelago_id=LOCATION_OFFSET + 69,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.MUSHROOM_HAMMERED: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4217, 1),),
+ archipelago_id=LOCATION_OFFSET + 70,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.HAMMER,
+ ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.NATIONAL_TREASURE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((14318, 1),),
+ archipelago_id=LOCATION_OFFSET + 71,
+ region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.SPELL_REZROV,
+ ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS,
+ ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.NATURAL_AND_SUPERNATURAL_CREATURES_OF_QUENDOR: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "dv1p"),),
+ archipelago_id=LOCATION_OFFSET + 72,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.NOOOOOOOOOOOOO: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((12706, 1),),
+ archipelago_id=LOCATION_OFFSET + 73,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,
+ ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT,
+ ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.NOTHIN_LIKE_A_GOOD_STOGIE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4237, 1),),
+ archipelago_id=LOCATION_OFFSET + 74,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE,
+ ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.NOW_YOU_LOOK_LIKE_US_WHICH_IS_AN_IMPROVEMENT: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((8935, 1),),
+ archipelago_id=LOCATION_OFFSET + 75,
+ region=ZorkGrandInquisitorRegions.HADES,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorEvents.KNOWS_SNAVIG,),
+ ),
+ ZorkGrandInquisitorLocations.NO_AUTOGRAPHS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((10476, 1),),
+ archipelago_id=LOCATION_OFFSET + 76,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR,),
+ ),
+ ZorkGrandInquisitorLocations.NO_BONDAGE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "pe2e"), (10262, 2), (15150, 83)),
+ archipelago_id=LOCATION_OFFSET + 77,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.ROPE,
+ ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.OBIDIL_DRIED_UP: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((12164, 1),),
+ archipelago_id=LOCATION_OFFSET + 78,
+ region=ZorkGrandInquisitorRegions.SPELL_LAB,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_OBIDIL,
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((1300, 1),),
+ archipelago_id=LOCATION_OFFSET + 79,
+ region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN,
+ ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((2448, 1),),
+ archipelago_id=LOCATION_OFFSET + 80,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.TOTEM_BROG,
+ ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4869, 1),),
+ archipelago_id=LOCATION_OFFSET + 81,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.FLATHEADIA_FUDGE,
+ ZorkGrandInquisitorItems.HUNGUS_LARD,
+ ZorkGrandInquisitorItems.JAR_OF_HOTBUGS,
+ ZorkGrandInquisitorItems.QUELBEE_HONEYCOMB,
+ ZorkGrandInquisitorItems.MOSS_OF_MAREILON,
+ ZorkGrandInquisitorItems.MUG,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.OLD_SCRATCH_WINNER: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4512, 32),),
+ archipelago_id=LOCATION_OFFSET + 82,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE, # This can be done anywhere if the item requirement is met
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorItems.OLD_SCRATCH_CARD,),
+ ),
+ ZorkGrandInquisitorLocations.ONLY_YOU_CAN_PREVENT_FOOZLE_FIRES: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "pe5n"),),
+ archipelago_id=LOCATION_OFFSET + 83,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.OPEN_THE_GATES_OF_HELL: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((8730, 1),),
+ archipelago_id=LOCATION_OFFSET + 84,
+ region=ZorkGrandInquisitorRegions.HADES,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorEvents.KNOWS_SNAVIG,),
+ ),
+ ZorkGrandInquisitorLocations.OUTSMART_THE_QUELBEES: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4241, 1),),
+ archipelago_id=LOCATION_OFFSET + 85,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.HUNGUS_LARD,
+ ZorkGrandInquisitorItems.SWORD,
+ ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.PERMASEAL: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "mt1g"),),
+ archipelago_id=LOCATION_OFFSET + 86,
+ region=ZorkGrandInquisitorRegions.MONASTERY,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.PLANETFALL: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "pp1j"),),
+ archipelago_id=LOCATION_OFFSET + 87,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE_JACKS_SHOP,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.PLEASE_DONT_THROCK_THE_GRASS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "te1g"),),
+ archipelago_id=LOCATION_OFFSET + 88,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.PORT_FOOZLE_TIME_TUNNEL: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((9404, 1),),
+ archipelago_id=LOCATION_OFFSET + 89,
+ region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER,
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT,
+ ZorkGrandInquisitorItems.LARGE_TELEGRAPH_HAMMER,
+ ZorkGrandInquisitorItems.SPELL_NARWILE,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.PROZORKED: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4115, 1),),
+ archipelago_id=LOCATION_OFFSET + 90,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.PROZORK_TABLET,
+ ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.REASSEMBLE_SNAVIG: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4512, 98),),
+ archipelago_id=LOCATION_OFFSET + 91,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.SCROLL_FRAGMENT_ANS,
+ ZorkGrandInquisitorItems.SCROLL_FRAGMENT_GIV,
+ ZorkGrandInquisitorItems.HOTSPOT_MIRROR,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.RESTOCKED_ON_GRUESDAY: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "tr2h"),),
+ archipelago_id=LOCATION_OFFSET + 92,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.RIGHT_HELLO_YES_UH_THIS_IS_SNEFFLE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4698, 3),),
+ archipelago_id=LOCATION_OFFSET + 93,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.RIGHT_UH_SORRY_ITS_ME_AGAIN_SNEFFLE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4698, 4),),
+ archipelago_id=LOCATION_OFFSET + 94,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.SNAVIG_REPAIRED: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((201, 1),),
+ archipelago_id=LOCATION_OFFSET + 95,
+ region=ZorkGrandInquisitorRegions.SPELL_LAB,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_SNAVIG,
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.SOUVENIR: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((13408, 1),),
+ archipelago_id=LOCATION_OFFSET + 96,
+ region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,
+ ZorkGrandInquisitorItems.HOTSPOT_SOUVENIR_COIN_SLOT,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.STRAIGHT_TO_HELL: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((9719, 1),),
+ archipelago_id=LOCATION_OFFSET + 97,
+ region=ZorkGrandInquisitorRegions.MONASTERY,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS,
+ ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL,
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((14511, 1), (14524, 5)),
+ archipelago_id=LOCATION_OFFSET + 98,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4,
+ ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY,
+ ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.SUCKING_ROCKS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((12859, 1),),
+ archipelago_id=LOCATION_OFFSET + 99,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_SUCKABLE,
+ ZorkGrandInquisitorItems.PERMA_SUCK_MACHINE,
+ ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_VACUUM_SLOT,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.TALK_TO_ME_GRAND_INQUISITOR: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((10299, 1),),
+ archipelago_id=LOCATION_OFFSET + 100,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL,),
+ ),
+ ZorkGrandInquisitorLocations.TAMING_YOUR_SNAPDRAGON: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "dv1h"),),
+ archipelago_id=LOCATION_OFFSET + 101,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((1311, 1), (1312, 1)),
+ archipelago_id=LOCATION_OFFSET + 102,
+ region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN,
+ ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS,
+ ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.THATS_A_ROPE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((10486, 1),),
+ archipelago_id=LOCATION_OFFSET + 103,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.ROPE,
+ ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.THATS_IT_JUST_KEEP_HITTING_THOSE_BUTTONS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((13805, 1),),
+ archipelago_id=LOCATION_OFFSET + 104,
+ region=ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ ),
+ ZorkGrandInquisitorLocations.THATS_STILL_A_ROPE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "tp1e"), (9, 83), (1011, 1)),
+ archipelago_id=LOCATION_OFFSET + 105,
+ region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(ZorkGrandInquisitorEvents.ROPE_GLORFABLE,),
+ ),
+ ZorkGrandInquisitorLocations.THATS_THE_SPIRIT: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((10341, 95),),
+ archipelago_id=LOCATION_OFFSET + 106,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorItems.HOTSPOT_LOUDSPEAKER_VOLUME_BUTTONS,),
+ ),
+ ZorkGrandInquisitorLocations.THE_ALCHEMICAL_DEBACLE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((9459, 1),),
+ archipelago_id=LOCATION_OFFSET + 107,
+ region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.THE_ENDLESS_FIRE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((9473, 1),),
+ archipelago_id=LOCATION_OFFSET + 108,
+ region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.THE_FLATHEADIAN_FUDGE_FIASCO: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((9520, 1),),
+ archipelago_id=LOCATION_OFFSET + 109,
+ region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.THE_PERILS_OF_MAGIC: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "me1j"),),
+ archipelago_id=LOCATION_OFFSET + 110,
+ region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.THE_UNDERGROUND_UNDERGROUND: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((13167, 1),),
+ archipelago_id=LOCATION_OFFSET + 111,
+ region=ZorkGrandInquisitorRegions.CROSSROADS,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.SUBWAY_TOKEN,
+ ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.THIS_DOESNT_LOOK_ANYTHING_LIKE_THE_BROCHURE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "cd60"), (1524, 1)),
+ archipelago_id=LOCATION_OFFSET + 112,
+ region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorItems.TOTEM_LUCY,),
+ ),
+ ZorkGrandInquisitorLocations.THROCKED_MUSHROOM_HAMMERED: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4219, 1),),
+ archipelago_id=LOCATION_OFFSET + 113,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.HAMMER,
+ ZorkGrandInquisitorItems.SPELL_THROCK,
+ ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.TIME_TRAVEL_FOR_DUMMIES: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "th3z"),),
+ archipelago_id=LOCATION_OFFSET + 114,
+ region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE,),
+ ),
+ ZorkGrandInquisitorLocations.TOTEMIZED_DAILY_BILLBOARD: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "px1h"),),
+ archipelago_id=LOCATION_OFFSET + 115,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.UH_OH_BROG_CANT_SWIM: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "cd60"), (1520, 1)),
+ archipelago_id=LOCATION_OFFSET + 116,
+ region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorItems.TOTEM_BROG,),
+ ),
+ ZorkGrandInquisitorLocations.UMBRELLA_FLOWERS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((12926, 1),),
+ archipelago_id=LOCATION_OFFSET + 117,
+ region=ZorkGrandInquisitorRegions.CROSSROADS,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorEvents.KNOWS_BEBURTT,),
+ ),
+ ZorkGrandInquisitorLocations.UP: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((3619, 5200),),
+ archipelago_id=LOCATION_OFFSET + 118,
+ region=ZorkGrandInquisitorRegions.WHITE_HOUSE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.TOTEM_LUCY,
+ ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.USELESS_BUT_FUN: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((14321, 1),),
+ archipelago_id=LOCATION_OFFSET + 119,
+ region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(ZorkGrandInquisitorItems.SPELL_GOLGATEM,),
+ ),
+ ZorkGrandInquisitorLocations.UUUUUP: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((3619, 3500),),
+ archipelago_id=LOCATION_OFFSET + 120,
+ region=ZorkGrandInquisitorRegions.WHITE_HOUSE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.TOTEM_GRIFF,
+ ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.VOYAGE_OF_CAPTAIN_ZAHAB: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "uh1h"),),
+ archipelago_id=LOCATION_OFFSET + 121,
+ region=ZorkGrandInquisitorRegions.HADES_SHORE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4034, 1),),
+ archipelago_id=LOCATION_OFFSET + 122,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorEvents.DOOR_SMOKED_CIGAR,
+ ZorkGrandInquisitorItems.MEAD_LIGHT,
+ ZorkGrandInquisitorItems.ZIMDOR_SCROLL,
+ ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((2461, 1),),
+ archipelago_id=LOCATION_OFFSET + 123,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.TOTEM_GRIFF,
+ ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((15472, 1),),
+ archipelago_id=LOCATION_OFFSET + 124,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4,
+ ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY,
+ ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.WHAT_ARE_YOU_STUPID: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((10484, 1),),
+ archipelago_id=LOCATION_OFFSET + 125,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.PLASTIC_SIX_PACK_HOLDER,
+ ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.WHITE_HOUSE_TIME_TUNNEL: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((4983, 1),),
+ archipelago_id=LOCATION_OFFSET + 126,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR,
+ ZorkGrandInquisitorItems.SPELL_NARWILE,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.WOW_IVE_NEVER_GONE_INSIDE_HIM_BEFORE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "dc10"), (1596, 1)),
+ archipelago_id=LOCATION_OFFSET + 127,
+ region=ZorkGrandInquisitorRegions.WALKING_CASTLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.YAD_GOHDNUORGREDNU_3_YRAUBORF: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "dm2g"),),
+ archipelago_id=LOCATION_OFFSET + 128,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(ZorkGrandInquisitorItems.HOTSPOT_MIRROR,),
+ ),
+ ZorkGrandInquisitorLocations.YOUR_PUNY_WEAPONS_DONT_PHASE_ME_BABY: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "dg4e"), (4266, 1), (9, 21), (4035, 1)),
+ archipelago_id=LOCATION_OFFSET + 129,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.SWORD,
+ ZorkGrandInquisitorItems.HOTSPOT_HARRY,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.YOU_DONT_GO_MESSING_WITH_A_MANS_ZIPPER: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((16405, 1),),
+ archipelago_id=LOCATION_OFFSET + 130,
+ region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(ZorkGrandInquisitorItems.SPELL_REZROV,),
+ ),
+ ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((16342, 1),),
+ archipelago_id=LOCATION_OFFSET + 131,
+ region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ requirements=(
+ ZorkGrandInquisitorItems.SWORD,
+ ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.YOU_ONE_OF_THEM_AGITATORS_AINT_YA: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((10586, 1),),
+ archipelago_id=LOCATION_OFFSET + 132,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE,),
+ ),
+ ZorkGrandInquisitorLocations.YOU_WANT_A_PIECE_OF_ME_DOCK_BOY: ZorkGrandInquisitorLocationData(
+ game_state_trigger=((15151, 1),),
+ archipelago_id=LOCATION_OFFSET + 133,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH,),
+ ),
+ # Deathsanity
+ ZorkGrandInquisitorLocations.DEATH_ARRESTED_WITH_JACK: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, 1)),
+ archipelago_id=LOCATION_OFFSET + 200 + 0,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE,
+ ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_ATTACKED_THE_QUELBEES: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, 20)),
+ archipelago_id=LOCATION_OFFSET + 200 + 1,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.SWORD,
+ ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_CLIMBED_OUT_OF_THE_WELL: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, 21)),
+ archipelago_id=LOCATION_OFFSET + 200 + 2,
+ region=ZorkGrandInquisitorRegions.CROSSROADS,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY,),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_EATEN_BY_A_GRUE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, 18)),
+ archipelago_id=LOCATION_OFFSET + 200 + 3,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.ROPE,
+ ZorkGrandInquisitorItems.HOTSPOT_WELL,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_JUMPED_IN_BOTTOMLESS_PIT: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, 3)),
+ archipelago_id=LOCATION_OFFSET + 200 + 4,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY,),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, 37)),
+ archipelago_id=LOCATION_OFFSET + 200 + 5,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4,
+ ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY,
+ ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_LOST_SOUL_TO_OLD_SCRATCH: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, 23)),
+ archipelago_id=LOCATION_OFFSET + 200 + 6,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(ZorkGrandInquisitorItems.OLD_SCRATCH_CARD,),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_OUTSMARTED_BY_THE_QUELBEES: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, 29)),
+ archipelago_id=LOCATION_OFFSET + 200 + 7,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.HUNGUS_LARD,
+ ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_SLICED_UP_BY_THE_INVISIBLE_GUARD: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, 30)),
+ archipelago_id=LOCATION_OFFSET + 200 + 8,
+ region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_STEPPED_INTO_THE_INFINITE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, 4)),
+ archipelago_id=LOCATION_OFFSET + 200 + 9,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.SPELL_IGRAM,
+ ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, 11)),
+ archipelago_id=LOCATION_OFFSET + 200 + 10,
+ region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(
+ ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN,
+ ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS,
+ ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_THROCKED_THE_GRASS: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, 34)),
+ archipelago_id=LOCATION_OFFSET + 200 + 11,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY,),
+ requirements=(
+ ZorkGrandInquisitorItems.SPELL_THROCK,
+ ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_GRASS,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_TOTEMIZED: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, (9, 32, 33))),
+ archipelago_id=LOCATION_OFFSET + 200 + 12,
+ region=ZorkGrandInquisitorRegions.MONASTERY,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY,),
+ requirements=(
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS,
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH,
+ ),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, (5, 6, 7, 8, 13))),
+ archipelago_id=LOCATION_OFFSET + 200 + 13,
+ region=ZorkGrandInquisitorRegions.MONASTERY,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY,),
+ requirements=(ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH,),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_YOURE_NOT_CHARON: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, 10)),
+ archipelago_id=LOCATION_OFFSET + 200 + 14,
+ region=ZorkGrandInquisitorRegions.HADES,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(ZorkGrandInquisitorEvents.KNOWS_SNAVIG,),
+ ),
+ ZorkGrandInquisitorLocations.DEATH_ZORK_ROCKS_EXPLODED: ZorkGrandInquisitorLocationData(
+ game_state_trigger=(("location", "gjde"), (2201, 19)),
+ archipelago_id=LOCATION_OFFSET + 200 + 15,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE),
+ requirements=(ZorkGrandInquisitorEvents.ZORK_ROCKS_ACTIVATED,),
+ ),
+ # Events
+ ZorkGrandInquisitorEvents.CHARON_CALLED: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.HADES_SHORE,
+ requirements=(
+ ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER,
+ ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.CHARON_CALLED.value,
+ ),
+ ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ requirements=(
+ ZorkGrandInquisitorItems.LANTERN,
+ ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE.value,
+ ),
+ ZorkGrandInquisitorEvents.DALBOZ_LOCKER_OPENABLE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ requirements=(
+ ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,
+ ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT,
+ ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.DALBOZ_LOCKER_OPENABLE.value,
+ ),
+ ZorkGrandInquisitorEvents.DAM_DESTROYED: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM,
+ requirements=(
+ ZorkGrandInquisitorItems.SPELL_REZROV,
+ ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS,
+ ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.DAM_DESTROYED.value,
+ ),
+ ZorkGrandInquisitorEvents.DOOR_DRANK_MEAD: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ requirements=(
+ ZorkGrandInquisitorEvents.DOOR_SMOKED_CIGAR,
+ ZorkGrandInquisitorItems.MEAD_LIGHT,
+ ZorkGrandInquisitorItems.ZIMDOR_SCROLL,
+ ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.DOOR_DRANK_MEAD.value,
+ ),
+ ZorkGrandInquisitorEvents.DOOR_SMOKED_CIGAR: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.DM_LAIR,
+ requirements=(
+ ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE,
+ ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.DOOR_SMOKED_CIGAR.value,
+ ),
+ ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ requirements=(
+ ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,
+ ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT,
+ ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE.value,
+ ),
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_OBIDIL: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ requirements=(
+ ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,
+ ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT,
+ ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.HAS_REPAIRABLE_OBIDIL.value,
+ ),
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_SNAVIG: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ requirements=(
+ ZorkGrandInquisitorItems.SCROLL_FRAGMENT_ANS,
+ ZorkGrandInquisitorItems.SCROLL_FRAGMENT_GIV,
+ ZorkGrandInquisitorItems.HOTSPOT_MIRROR,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.HAS_REPAIRABLE_SNAVIG.value,
+ ),
+ ZorkGrandInquisitorEvents.KNOWS_BEBURTT: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.SPELL_LAB,
+ requirements=(
+ ZorkGrandInquisitorItems.HOTSPOT_BLANK_SCROLL_BOX,
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.KNOWS_BEBURTT.value,
+ ),
+ ZorkGrandInquisitorEvents.KNOWS_OBIDIL: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.SPELL_LAB,
+ requirements=(
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_OBIDIL,
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.KNOWS_OBIDIL.value,
+ ),
+ ZorkGrandInquisitorEvents.KNOWS_SNAVIG: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.SPELL_LAB,
+ requirements=(
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_SNAVIG,
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.KNOWS_SNAVIG.value,
+ ),
+ ZorkGrandInquisitorEvents.KNOWS_YASTARD: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ requirements=(
+ ZorkGrandInquisitorItems.FLATHEADIA_FUDGE,
+ ZorkGrandInquisitorItems.HUNGUS_LARD,
+ ZorkGrandInquisitorItems.JAR_OF_HOTBUGS,
+ ZorkGrandInquisitorItems.QUELBEE_HONEYCOMB,
+ ZorkGrandInquisitorItems.MOSS_OF_MAREILON,
+ ZorkGrandInquisitorItems.MUG,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.KNOWS_YASTARD.value,
+ ),
+ ZorkGrandInquisitorEvents.LANTERN_DALBOZ_ACCESSIBLE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ requirements=(
+ ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE,
+ ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.LANTERN_DALBOZ_ACCESSIBLE.value,
+ ),
+ ZorkGrandInquisitorEvents.ROPE_GLORFABLE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.CROSSROADS,
+ requirements=(ZorkGrandInquisitorItems.SPELL_GLORF,),
+ event_item_name=ZorkGrandInquisitorEvents.ROPE_GLORFABLE.value,
+ ),
+ ZorkGrandInquisitorEvents.VICTORY: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.ENDGAME,
+ event_item_name=ZorkGrandInquisitorEvents.VICTORY.value,
+ ),
+ ZorkGrandInquisitorEvents.WHITE_HOUSE_LETTER_MAILABLE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.WHITE_HOUSE,
+ requirements=(
+ (ZorkGrandInquisitorItems.TOTEM_GRIFF, ZorkGrandInquisitorItems.TOTEM_LUCY),
+ ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG,
+ ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_DOOR,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.WHITE_HOUSE_LETTER_MAILABLE.value,
+ ),
+ ZorkGrandInquisitorEvents.ZORKMID_BILL_ACCESSIBLE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ requirements=(ZorkGrandInquisitorItems.OLD_SCRATCH_CARD,),
+ event_item_name=ZorkGrandInquisitorEvents.ZORKMID_BILL_ACCESSIBLE.value,
+ ),
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_ACTIVATED: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ requirements=(
+ ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,
+ ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_COIN_SLOT,
+ ZorkGrandInquisitorItems.ZORK_ROCKS,
+ ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_BUTTONS,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.ZORK_ROCKS_ACTIVATED.value,
+ ),
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_SUCKABLE: ZorkGrandInquisitorLocationData(
+ game_state_trigger=None,
+ archipelago_id=None,
+ region=ZorkGrandInquisitorRegions.GUE_TECH,
+ requirements=(
+ ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,
+ ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT,
+ ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS,
+ ),
+ event_item_name=ZorkGrandInquisitorEvents.ZORK_ROCKS_SUCKABLE.value,
+ ),
+}
diff --git a/worlds/zork_grand_inquisitor/data/missable_location_grant_conditions_data.py b/worlds/zork_grand_inquisitor/data/missable_location_grant_conditions_data.py
new file mode 100644
index 000000000000..ef6eacb78ceb
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/data/missable_location_grant_conditions_data.py
@@ -0,0 +1,200 @@
+from typing import Dict, NamedTuple, Optional, Tuple
+
+from ..enums import ZorkGrandInquisitorItems, ZorkGrandInquisitorLocations
+
+
+class ZorkGrandInquisitorMissableLocationGrantConditionsData(NamedTuple):
+ location_condition: ZorkGrandInquisitorLocations
+ item_conditions: Optional[Tuple[ZorkGrandInquisitorItems, ...]]
+
+
+missable_location_grant_conditions_data: Dict[
+ ZorkGrandInquisitorLocations, ZorkGrandInquisitorMissableLocationGrantConditionsData
+] = {
+ ZorkGrandInquisitorLocations.BOING_BOING_BOING:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.FLYING_SNAPDRAGON,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.BONK:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.PROZORKED,
+ item_conditions=(ZorkGrandInquisitorItems.HAMMER,),
+ )
+ ,
+ ZorkGrandInquisitorLocations.DEATH_ARRESTED_WITH_JACK:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.ARREST_THE_VANDAL,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.DEATH_ATTACKED_THE_QUELBEES:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.OUTSMART_THE_QUELBEES,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.DEATH_EATEN_BY_A_GRUE:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.MAGIC_FOREVER,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.DEATH_LOST_SOUL_TO_OLD_SCRATCH:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.OLD_SCRATCH_WINNER,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.DEATH_OUTSMARTED_BY_THE_QUELBEES:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.OUTSMART_THE_QUELBEES,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.DEATH_SLICED_UP_BY_THE_INVISIBLE_GUARD:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.DEATH_STEPPED_INTO_THE_INFINITE:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.A_SMALLWAY,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.THAR_SHE_BLOWS,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.DEATH_YOURE_NOT_CHARON:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.OPEN_THE_GATES_OF_HELL,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.DEATH_ZORK_ROCKS_EXPLODED:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.CRISIS_AVERTED,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.DENIED_BY_THE_LAKE_MONSTER:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.WOW_IVE_NEVER_GONE_INSIDE_HIM_BEFORE,
+ item_conditions=(ZorkGrandInquisitorItems.SPELL_GOLGATEM,),
+ )
+ ,
+ ZorkGrandInquisitorLocations.EMERGENCY_MAGICATRONIC_MESSAGE:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.ARTIFACTS_EXPLAINED,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.FAT_LOT_OF_GOOD_THATLL_DO_YA:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS,
+ item_conditions=(ZorkGrandInquisitorItems.SPELL_IGRAM,),
+ )
+ ,
+ ZorkGrandInquisitorLocations.I_DONT_THINK_YOU_WOULDVE_WANTED_THAT_TO_WORK_ANYWAY:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.PROZORKED,
+ item_conditions=(ZorkGrandInquisitorItems.SPELL_THROCK,),
+ )
+ ,
+ ZorkGrandInquisitorLocations.I_SPIT_ON_YOUR_FILTHY_COINAGE:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS,
+ item_conditions=(ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,),
+ )
+ ,
+ ZorkGrandInquisitorLocations.MEAD_LIGHT:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.FIRE_FIRE,
+ item_conditions=(ZorkGrandInquisitorItems.MEAD_LIGHT,),
+ )
+ ,
+ ZorkGrandInquisitorLocations.MUSHROOM_HAMMERED:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.THROCKED_MUSHROOM_HAMMERED,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.NO_AUTOGRAPHS:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.FIRE_FIRE,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.NO_BONDAGE:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.HELP_ME_CANT_BREATHE,
+ item_conditions=(ZorkGrandInquisitorItems.ROPE,),
+ )
+ ,
+ ZorkGrandInquisitorLocations.TALK_TO_ME_GRAND_INQUISITOR:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.FIRE_FIRE,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.THATS_A_ROPE:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.FIRE_FIRE,
+ item_conditions=(ZorkGrandInquisitorItems.ROPE,),
+ )
+ ,
+ ZorkGrandInquisitorLocations.THATS_IT_JUST_KEEP_HITTING_THOSE_BUTTONS:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.ENJOY_YOUR_TRIP,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.THATS_STILL_A_ROPE:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS,
+ item_conditions=(ZorkGrandInquisitorItems.SPELL_GLORF,),
+ )
+ ,
+ ZorkGrandInquisitorLocations.WHAT_ARE_YOU_STUPID:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.FIRE_FIRE,
+ item_conditions=(ZorkGrandInquisitorItems.PLASTIC_SIX_PACK_HOLDER,),
+ )
+ ,
+ ZorkGrandInquisitorLocations.YAD_GOHDNUORGREDNU_3_YRAUBORF:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.REASSEMBLE_SNAVIG,
+ item_conditions=None,
+ )
+ ,
+ ZorkGrandInquisitorLocations.YOUR_PUNY_WEAPONS_DONT_PHASE_ME_BABY:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO,
+ item_conditions=(ZorkGrandInquisitorItems.SWORD, ZorkGrandInquisitorItems.HOTSPOT_HARRY),
+ )
+ ,
+ ZorkGrandInquisitorLocations.YOU_DONT_GO_MESSING_WITH_A_MANS_ZIPPER:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS,
+ item_conditions=(ZorkGrandInquisitorItems.SPELL_REZROV,),
+ )
+ ,
+ ZorkGrandInquisitorLocations.YOU_WANT_A_PIECE_OF_ME_DOCK_BOY:
+ ZorkGrandInquisitorMissableLocationGrantConditionsData(
+ location_condition=ZorkGrandInquisitorLocations.HELP_ME_CANT_BREATHE,
+ item_conditions=None,
+ )
+ ,
+}
diff --git a/worlds/zork_grand_inquisitor/data/region_data.py b/worlds/zork_grand_inquisitor/data/region_data.py
new file mode 100644
index 000000000000..1aed160f3088
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/data/region_data.py
@@ -0,0 +1,183 @@
+from typing import Dict, NamedTuple, Optional, Tuple
+
+from ..enums import ZorkGrandInquisitorRegions
+
+
+class ZorkGrandInquisitorRegionData(NamedTuple):
+ exits: Optional[Tuple[ZorkGrandInquisitorRegions, ...]]
+
+
+region_data: Dict[ZorkGrandInquisitorRegions, ZorkGrandInquisitorRegionData] = {
+ ZorkGrandInquisitorRegions.CROSSROADS: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.DM_LAIR,
+ ZorkGrandInquisitorRegions.GUE_TECH,
+ ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE,
+ ZorkGrandInquisitorRegions.HADES_SHORE,
+ ZorkGrandInquisitorRegions.PORT_FOOZLE,
+ ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE,
+ ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS,
+ ZorkGrandInquisitorRegions.SUBWAY_MONASTERY,
+ )
+ ),
+ ZorkGrandInquisitorRegions.DM_LAIR: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.CROSSROADS,
+ ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE,
+ ZorkGrandInquisitorRegions.HADES_SHORE,
+ ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE,
+ ZorkGrandInquisitorRegions.SUBWAY_MONASTERY,
+ )
+ ),
+ ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.DM_LAIR,
+ ZorkGrandInquisitorRegions.WALKING_CASTLE,
+ ZorkGrandInquisitorRegions.WHITE_HOUSE,
+ )
+ ),
+ ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON,
+ ZorkGrandInquisitorRegions.HADES_BEYOND_GATES,
+ )
+ ),
+ ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO,
+ ZorkGrandInquisitorRegions.ENDGAME,
+ )
+ ),
+ ZorkGrandInquisitorRegions.ENDGAME: ZorkGrandInquisitorRegionData(exits=None),
+ ZorkGrandInquisitorRegions.GUE_TECH: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.CROSSROADS,
+ ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY,
+ ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE,
+ )
+ ),
+ ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.GUE_TECH,
+ ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE,
+ )
+ ),
+ ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.CROSSROADS,
+ ZorkGrandInquisitorRegions.DM_LAIR,
+ ZorkGrandInquisitorRegions.GUE_TECH,
+ ZorkGrandInquisitorRegions.HADES_SHORE,
+ ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE,
+ ZorkGrandInquisitorRegions.SUBWAY_MONASTERY,
+ )
+ ),
+ ZorkGrandInquisitorRegions.HADES: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.HADES_BEYOND_GATES,
+ ZorkGrandInquisitorRegions.HADES_SHORE,
+ )
+ ),
+ ZorkGrandInquisitorRegions.HADES_BEYOND_GATES: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO,
+ ZorkGrandInquisitorRegions.HADES,
+ )
+ ),
+ ZorkGrandInquisitorRegions.HADES_SHORE: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.CROSSROADS,
+ ZorkGrandInquisitorRegions.DM_LAIR,
+ ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE,
+ ZorkGrandInquisitorRegions.HADES,
+ ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE,
+ ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS,
+ ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM,
+ ZorkGrandInquisitorRegions.SUBWAY_MONASTERY,
+ )
+ ),
+ ZorkGrandInquisitorRegions.MENU: ZorkGrandInquisitorRegionData(
+ exits=(ZorkGrandInquisitorRegions.PORT_FOOZLE,)
+ ),
+ ZorkGrandInquisitorRegions.MONASTERY: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.HADES_SHORE,
+ ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT,
+ ZorkGrandInquisitorRegions.SUBWAY_MONASTERY,
+ )
+ ),
+ ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.MONASTERY,
+ ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST,
+ )
+ ),
+ ZorkGrandInquisitorRegions.PORT_FOOZLE: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.CROSSROADS,
+ ZorkGrandInquisitorRegions.PORT_FOOZLE_JACKS_SHOP,
+ )
+ ),
+ ZorkGrandInquisitorRegions.PORT_FOOZLE_JACKS_SHOP: ZorkGrandInquisitorRegionData(
+ exits=(ZorkGrandInquisitorRegions.PORT_FOOZLE,)
+ ),
+ ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT,
+ ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN,
+ )
+ ),
+ ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.ENDGAME,
+ ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST,
+ )
+ ),
+ ZorkGrandInquisitorRegions.SPELL_LAB: ZorkGrandInquisitorRegionData(
+ exits=(ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE,)
+ ),
+ ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.CROSSROADS,
+ ZorkGrandInquisitorRegions.DM_LAIR,
+ ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE,
+ ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY,
+ ZorkGrandInquisitorRegions.HADES_SHORE,
+ ZorkGrandInquisitorRegions.SPELL_LAB,
+ ZorkGrandInquisitorRegions.SUBWAY_MONASTERY,
+ )
+ ),
+ ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.CROSSROADS,
+ ZorkGrandInquisitorRegions.HADES_SHORE,
+ ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM,
+ ZorkGrandInquisitorRegions.SUBWAY_MONASTERY,
+ )
+ ),
+ ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.HADES_SHORE,
+ ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS,
+ ZorkGrandInquisitorRegions.SUBWAY_MONASTERY,
+ )
+ ),
+ ZorkGrandInquisitorRegions.SUBWAY_MONASTERY: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.HADES_SHORE,
+ ZorkGrandInquisitorRegions.MONASTERY,
+ ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS,
+ ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM,
+ )
+ ),
+ ZorkGrandInquisitorRegions.WALKING_CASTLE: ZorkGrandInquisitorRegionData(
+ exits=(ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,)
+ ),
+ ZorkGrandInquisitorRegions.WHITE_HOUSE: ZorkGrandInquisitorRegionData(
+ exits=(
+ ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR,
+ ZorkGrandInquisitorRegions.ENDGAME,
+ )
+ ),
+}
diff --git a/worlds/zork_grand_inquisitor/data_funcs.py b/worlds/zork_grand_inquisitor/data_funcs.py
new file mode 100644
index 000000000000..2a7bff1fbb6b
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/data_funcs.py
@@ -0,0 +1,247 @@
+from typing import Dict, List, Set, Tuple, Union
+
+from .data.entrance_rule_data import entrance_rule_data
+from .data.item_data import item_data, ZorkGrandInquisitorItemData
+from .data.location_data import location_data, ZorkGrandInquisitorLocationData
+
+from .enums import (
+ ZorkGrandInquisitorEvents,
+ ZorkGrandInquisitorGoals,
+ ZorkGrandInquisitorItems,
+ ZorkGrandInquisitorLocations,
+ ZorkGrandInquisitorRegions,
+ ZorkGrandInquisitorTags,
+)
+
+
+def item_names_to_id() -> Dict[str, int]:
+ return {item.value: data.archipelago_id for item, data in item_data.items()}
+
+
+def item_names_to_item() -> Dict[str, ZorkGrandInquisitorItems]:
+ return {item.value: item for item in item_data}
+
+
+def location_names_to_id() -> Dict[str, int]:
+ return {
+ location.value: data.archipelago_id
+ for location, data in location_data.items()
+ if data.archipelago_id is not None
+ }
+
+
+def location_names_to_location() -> Dict[str, ZorkGrandInquisitorLocations]:
+ return {
+ location.value: location
+ for location, data in location_data.items()
+ if data.archipelago_id is not None
+ }
+
+
+def id_to_goals() -> Dict[int, ZorkGrandInquisitorGoals]:
+ return {goal.value: goal for goal in ZorkGrandInquisitorGoals}
+
+
+def id_to_items() -> Dict[int, ZorkGrandInquisitorItems]:
+ return {data.archipelago_id: item for item, data in item_data.items()}
+
+
+def id_to_locations() -> Dict[int, ZorkGrandInquisitorLocations]:
+ return {
+ data.archipelago_id: location
+ for location, data in location_data.items()
+ if data.archipelago_id is not None
+ }
+
+
+def item_groups() -> Dict[str, List[str]]:
+ groups: Dict[str, List[str]] = dict()
+
+ item: ZorkGrandInquisitorItems
+ data: ZorkGrandInquisitorItemData
+ for item, data in item_data.items():
+ if data.tags is not None:
+ for tag in data.tags:
+ groups.setdefault(tag.value, list()).append(item.value)
+
+ return {k: v for k, v in groups.items() if len(v)}
+
+
+def items_with_tag(tag: ZorkGrandInquisitorTags) -> Set[ZorkGrandInquisitorItems]:
+ items: Set[ZorkGrandInquisitorItems] = set()
+
+ item: ZorkGrandInquisitorItems
+ data: ZorkGrandInquisitorItemData
+ for item, data in item_data.items():
+ if data.tags is not None and tag in data.tags:
+ items.add(item)
+
+ return items
+
+
+def game_id_to_items() -> Dict[int, ZorkGrandInquisitorItems]:
+ mapping: Dict[int, ZorkGrandInquisitorItems] = dict()
+
+ item: ZorkGrandInquisitorItems
+ data: ZorkGrandInquisitorItemData
+ for item, data in item_data.items():
+ if data.statemap_keys is not None:
+ for key in data.statemap_keys:
+ mapping[key] = item
+
+ return mapping
+
+
+def location_groups() -> Dict[str, List[str]]:
+ groups: Dict[str, List[str]] = dict()
+
+ tag: ZorkGrandInquisitorTags
+ for tag in ZorkGrandInquisitorTags:
+ groups[tag.value] = list()
+
+ location: ZorkGrandInquisitorLocations
+ data: ZorkGrandInquisitorLocationData
+ for location, data in location_data.items():
+ if data.tags is not None:
+ for tag in data.tags:
+ groups[tag.value].append(location.value)
+
+ return {k: v for k, v in groups.items() if len(v)}
+
+
+def locations_by_region(include_deathsanity: bool = False) -> Dict[
+ ZorkGrandInquisitorRegions, List[ZorkGrandInquisitorLocations]
+]:
+ mapping: Dict[ZorkGrandInquisitorRegions, List[ZorkGrandInquisitorLocations]] = dict()
+
+ region: ZorkGrandInquisitorRegions
+ for region in ZorkGrandInquisitorRegions:
+ mapping[region] = list()
+
+ location: ZorkGrandInquisitorLocations
+ data: ZorkGrandInquisitorLocationData
+ for location, data in location_data.items():
+ if not include_deathsanity and ZorkGrandInquisitorTags.DEATHSANITY in (
+ data.tags or tuple()
+ ):
+ continue
+
+ mapping[data.region].append(location)
+
+ return mapping
+
+
+def locations_with_tag(tag: ZorkGrandInquisitorTags) -> Set[ZorkGrandInquisitorLocations]:
+ location: ZorkGrandInquisitorLocations
+ data: ZorkGrandInquisitorLocationData
+
+ return {location for location, data in location_data.items() if data.tags is not None and tag in data.tags}
+
+
+def location_access_rule_for(location: ZorkGrandInquisitorLocations, player: int) -> str:
+ data: ZorkGrandInquisitorLocationData = location_data[location]
+
+ if data.requirements is None:
+ return "lambda state: True"
+
+ lambda_string: str = "lambda state: "
+
+ i: int
+ requirement: Union[
+ Tuple[
+ Union[
+ ZorkGrandInquisitorEvents,
+ ZorkGrandInquisitorItems,
+ ],
+ ...,
+ ],
+ ZorkGrandInquisitorEvents,
+ ZorkGrandInquisitorItems
+ ]
+
+ for i, requirement in enumerate(data.requirements):
+ if isinstance(requirement, tuple):
+ lambda_string += "("
+
+ ii: int
+ sub_requirement: Union[ZorkGrandInquisitorEvents, ZorkGrandInquisitorItems]
+ for ii, sub_requirement in enumerate(requirement):
+ lambda_string += f"state.has(\"{sub_requirement.value}\", {player})"
+
+ if ii < len(requirement) - 1:
+ lambda_string += " or "
+
+ lambda_string += ")"
+ else:
+ lambda_string += f"state.has(\"{requirement.value}\", {player})"
+
+ if i < len(data.requirements) - 1:
+ lambda_string += " and "
+
+ return lambda_string
+
+
+def entrance_access_rule_for(
+ region_origin: ZorkGrandInquisitorRegions,
+ region_destination: ZorkGrandInquisitorRegions,
+ player: int
+) -> str:
+ data: Union[
+ Tuple[
+ Tuple[
+ Union[
+ ZorkGrandInquisitorEvents,
+ ZorkGrandInquisitorItems,
+ ZorkGrandInquisitorRegions,
+ ],
+ ...,
+ ],
+ ...,
+ ],
+ None,
+ ] = entrance_rule_data[(region_origin, region_destination)]
+
+ if data is None:
+ return "lambda state: True"
+
+ lambda_string: str = "lambda state: "
+
+ i: int
+ requirement_group: Tuple[
+ Union[
+ ZorkGrandInquisitorEvents,
+ ZorkGrandInquisitorItems,
+ ZorkGrandInquisitorRegions,
+ ],
+ ...,
+ ]
+ for i, requirement_group in enumerate(data):
+ lambda_string += "("
+
+ ii: int
+ requirement: Union[
+ ZorkGrandInquisitorEvents,
+ ZorkGrandInquisitorItems,
+ ZorkGrandInquisitorRegions,
+ ]
+ for ii, requirement in enumerate(requirement_group):
+ requirement_type: Union[
+ ZorkGrandInquisitorEvents,
+ ZorkGrandInquisitorItems,
+ ZorkGrandInquisitorRegions,
+ ] = type(requirement)
+
+ if requirement_type in (ZorkGrandInquisitorEvents, ZorkGrandInquisitorItems):
+ lambda_string += f"state.has(\"{requirement.value}\", {player})"
+ elif requirement_type == ZorkGrandInquisitorRegions:
+ lambda_string += f"state.can_reach(\"{requirement.value}\", \"Region\", {player})"
+
+ if ii < len(requirement_group) - 1:
+ lambda_string += " and "
+
+ lambda_string += ")"
+
+ if i < len(data) - 1:
+ lambda_string += " or "
+
+ return lambda_string
diff --git a/worlds/zork_grand_inquisitor/docs/en_Zork Grand Inquisitor.md b/worlds/zork_grand_inquisitor/docs/en_Zork Grand Inquisitor.md
new file mode 100644
index 000000000000..d5821914beca
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/docs/en_Zork Grand Inquisitor.md
@@ -0,0 +1,102 @@
+# Zork Grand Inquisitor
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
+configuration file.
+
+## Is a tracker available for this game?
+
+Yes! You can download the latest PopTracker pack for Zork Grand Inquisitor [here](https://github.com/SerpentAI/ZorkGrandInquisitorAPTracker/releases/latest).
+
+## What does randomization do to this game?
+
+A majority of inventory items you can normally pick up are completely removed from the game (e.g. the lantern won't be
+in the crate, the mead won't be at the fish market, etc.). Instead, these items will be distributed in the multiworld.
+This means that you can expect to access areas and be in a position to solve certain puzzles in a completely different
+order than you normally would.
+
+Subway, teleporter and totemizer destinations are initially locked and need to be unlocked by receiving the
+corresponding item in the multiworld. This alone enables creative routing in a game that would otherwise be rather
+linear. The Crossroads destination is always unlocked for both the subway and teleporter to prevent softlocks. Until you
+receive your first totemizer destination, it will be locked to Newark, New Jersey.
+
+Important hotspots are also randomized. This means that you will be unable to interact with certain objects until you
+receive the corresponding item in the multiworld. This can be a bit confusing at first, but it adds depth to the
+randomization and makes the game more interesting to play.
+
+You can travel back to the surface without dying by looking inside the bucket. This will work as long as the rope is
+still attached to the well.
+
+Attempting to cast VOXAM will teleport you back to the Crossroads. Fast Travel!
+
+## What item types are distributed in the multiworld?
+
+- Inventory items
+- Pouch of Zorkmids
+- Spells
+- Totems
+- Subway destinations
+- Teleporter destinations
+- Totemizer destinations
+- Hotspots (with option to start with the items enabling them instead if you prefer not playing with the randomization
+ of hotspots)
+
+## When the player receives an item, what happens?
+
+- **Inventory items**: Directly added to the player's inventory.
+- **Pouch of Zorkmids**: Appears on the inventory screen. The player can then pick up Zorkmid coins from it.
+- **Spells**: Learned and directly added to the spell book.
+- **Totems**: Appears on the inventory screen.
+- **Subway destinations**: The destination button on the subway map becomes functional.
+- **Teleporter destinations**: The destination can show up on the teleporter screen.
+- **Totemizer destinations**: The destination button on the panel becomes functional.
+- **Hotspots**: The hotspot becomes interactable.
+
+## What is considered a location check in Zork Grand Inquisitor?
+
+- Solving puzzles
+- Accessing certain areas for the first time
+- Triggering certain interactions, even if they aren't puzzles per se
+- Dying in unique ways (Optional; Deathsanity option)
+
+## The location check names are fun but don't always convey well what's needed to unlock them. Is there a guide?
+
+Yes! You can find a complete guide for the location checks [here](https://gist.github.com/nbrochu/f7bed7a1fef4e2beb67ad6ddbf18b970).
+
+## What is the victory condition?
+
+Victory is achieved when the 3 artifacts of magic are retrieved and placed inside the walking castle.
+
+## Can I use the save system without a problem?
+
+Absolutely! The save system is fully supported (and its use is in fact strongly encouraged!). You can save and load your
+game as you normally would and the client will automatically sync your items and hotspots with what you should have in
+that game state.
+
+Depending on how your game progresses, there's a chance that certain location checks might become missable. This
+presents an excellent opportunity to utilize the save system. Simply make it a habit to save before undertaking
+irreversible actions, ensuring you can revert to a previous state if necessary. If you prefer not to depend on the save
+system for accessing missable location checks, there's an option to automatically unlock them as they become
+unavailable.
+
+## Unique Local Commands
+The following commands are only available when using the Zork Grand Inquisitor Client to play the game with Archipelago.
+
+- `/zork` Attempts to attach to a running instance of Zork Grand Inquisitor. If successful, the client will then be able
+ to read and control the state of the game.
+- `/brog` Lists received items for Brog.
+- `/griff` Lists received items for Griff.
+- `/lucy` Lists received items for Lucy.
+- `/hotspots` Lists received hotspots.
+
+## Known issues
+
+- You will get a second rope right after using GLORF (one in your inventory and one on your cursor). This is a harmless
+ side effect that will go away after you store it in your inventory as duplicates are actively removed.
+- After climbing up to the Monastery for the first time, a rope will forever remain in place in the vent. When you come
+ back to the Monastery, you will be able to climb up without needing to combine the sword and rope again. However, when
+ arriving at the top, you will receive a duplicate sword on a rope. This is a harmless side effect that will go away
+ after you store it in your inventory as duplicates are actively removed.
+- Since the client is reading and manipulating the game's memory, rare game crashes can happen. If you encounter one,
+ simply restart the game, load your latest save and use the `/zork` command again in the client. Nothing will be lost.
diff --git a/worlds/zork_grand_inquisitor/docs/setup_en.md b/worlds/zork_grand_inquisitor/docs/setup_en.md
new file mode 100644
index 000000000000..f9078c6d39ba
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/docs/setup_en.md
@@ -0,0 +1,42 @@
+# Zork Grand Inquisitor Randomizer Setup Guide
+
+## Requirements
+
+- Windows OS (Hard required. Client is using memory reading / writing through Win32 API)
+- A copy of Zork Grand Inquisitor. Only the GOG version is supported. The Steam version can work with some tinkering but
+ is not officially supported.
+- ScummVM 2.7.1 64-bit (Important: Will not work with any other version. [Direct Download](https://downloads.scummvm.org/frs/scummvm/2.7.1/scummvm-2.7.1-win32-x86_64.zip))
+- Archipelago 0.4.4+
+
+## Game Setup Instructions
+
+No game modding is required to play Zork Grand Inquisitor with Archipelago. The client does all the work by attaching to
+the game process and reading and manipulating the game state in real-time.
+
+This being said, the game does need to be played through ScummVM 2.7.1, so some configuration is required around that.
+
+### GOG
+
+- Open the directory where you installed Zork Grand Inquisitor. You should see a `Launch Zork Grand Inquisitor`
+ shortcut.
+- Open the `scummvm` directory. Delete the entire contents of that directory.
+- Still inside the `scummvm` directory, unzip the contents of the ScummVM 2.7.1 zip file you downloaded earlier.
+- Go back to the directory where you installed Zork Grand Inquisitor.
+- Verify that the game still launches when using the `Launch Zork Grand Inquisitor` shortcut.
+- Your game is now ready to be played with Archipelago. From now on, you can use the `Launch Zork Grand Inquisitor`
+ shortcut to launch the game.
+
+## Joining a Multiworld Game
+
+- Launch Zork Grand Inquisitor and start a new game.
+- Open the Archipelago Launcher and click `Zork Grand Inquisitor Client`.
+- Using the `Zork Grand Inquisitor Client`:
+ - Enter the room's hostname and port number (e.g. `archipelago.gg:54321`) in the top box and press `Connect`.
+ - Input your player name at the bottom when prompted and press `Enter`.
+ - You should now be connected to the Archipelago room.
+ - Next, input `/zork` at the bottom and press `Enter`. This will attach the client to the game process.
+ - If the command is successful, you are now ready to play Zork Grand Inquisitor with Archipelago.
+
+## Continuing a Multiworld Game
+
+- Perform the same steps as above, but instead of starting a new game, load your latest save file.
diff --git a/worlds/zork_grand_inquisitor/enums.py b/worlds/zork_grand_inquisitor/enums.py
new file mode 100644
index 000000000000..ecbb38a949b4
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/enums.py
@@ -0,0 +1,350 @@
+import enum
+
+
+class ZorkGrandInquisitorEvents(enum.Enum):
+ CHARON_CALLED = "Event: Charon Called"
+ CIGAR_ACCESSIBLE = "Event: Cigar Accessible"
+ DALBOZ_LOCKER_OPENABLE = "Event: Dalboz Locker Openable"
+ DAM_DESTROYED = "Event: Dam Destroyed"
+ DOOR_DRANK_MEAD = "Event: Door Drank Mead"
+ DOOR_SMOKED_CIGAR = "Event: Door Smoked Cigar"
+ DUNCE_LOCKER_OPENABLE = "Event: Dunce Locker Openable"
+ HAS_REPAIRABLE_OBIDIL = "Event: Has Repairable OBIDIL"
+ HAS_REPAIRABLE_SNAVIG = "Event: Has Repairable SNAVIG"
+ KNOWS_BEBURTT = "Event: Knows BEBURTT"
+ KNOWS_OBIDIL = "Event: Knows OBIDIL"
+ KNOWS_SNAVIG = "Event: Knows SNAVIG"
+ KNOWS_YASTARD = "Event: Knows YASTARD"
+ LANTERN_DALBOZ_ACCESSIBLE = "Event: Lantern (Dalboz) Accessible"
+ ROPE_GLORFABLE = "Event: Rope GLORFable"
+ VICTORY = "Victory"
+ WHITE_HOUSE_LETTER_MAILABLE = "Event: White House Letter Mailable"
+ ZORKMID_BILL_ACCESSIBLE = "Event: 500 Zorkmid Bill Accessible"
+ ZORK_ROCKS_ACTIVATED = "Event: Zork Rocks Activated"
+ ZORK_ROCKS_SUCKABLE = "Event: Zork Rocks Suckable"
+
+
+class ZorkGrandInquisitorGoals(enum.Enum):
+ THREE_ARTIFACTS = 0
+
+
+class ZorkGrandInquisitorItems(enum.Enum):
+ BROGS_BICKERING_TORCH = "Brog's Bickering Torch"
+ BROGS_FLICKERING_TORCH = "Brog's Flickering Torch"
+ BROGS_GRUE_EGG = "Brog's Grue Egg"
+ BROGS_PLANK = "Brog's Plank"
+ FILLER_FROBOZZ_ELECTRIC_GADGET = "Frobozz Electric Gadget"
+ FILLER_INQUISITION_PROPAGANDA_FLYER = "Inquisition Propaganda Flyer"
+ FILLER_MAGIC_CONTRABAND = "Magic Contraband"
+ FILLER_NONSENSICAL_INQUISITION_PAPERWORK = "Nonsensical Inquisition Paperwork"
+ FILLER_UNREADABLE_SPELL_SCROLL = "Unreadable Spell Scroll"
+ FLATHEADIA_FUDGE = "Flatheadia Fudge"
+ GRIFFS_AIR_PUMP = "Griff's Air Pump"
+ GRIFFS_DRAGON_TOOTH = "Griff's Dragon Tooth"
+ GRIFFS_INFLATABLE_RAFT = "Griff's Inflatable Raft"
+ GRIFFS_INFLATABLE_SEA_CAPTAIN = "Griff's Inflatable Sea Captain"
+ HAMMER = "Hammer"
+ HOTSPOT_666_MAILBOX = "Hotspot: 666 Mailbox"
+ HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS = "Hotspot: Alpine's Quandry Card Slots"
+ HOTSPOT_BLANK_SCROLL_BOX = "Hotspot: Blank Scroll Box"
+ HOTSPOT_BLINDS = "Hotspot: Blinds"
+ HOTSPOT_CANDY_MACHINE_BUTTONS = "Hotspot: Candy Machine Buttons"
+ HOTSPOT_CANDY_MACHINE_COIN_SLOT = "Hotspot: Candy Machine Coin Slot"
+ HOTSPOT_CANDY_MACHINE_VACUUM_SLOT = "Hotspot: Candy Machine Vacuum Slot"
+ HOTSPOT_CHANGE_MACHINE_SLOT = "Hotspot: Change Machine Slot"
+ HOTSPOT_CLOSET_DOOR = "Hotspot: Closet Door"
+ HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT = "Hotspot: Closing the Time Tunnels Hammer Slot"
+ HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER = "Hotspot: Closing the Time Tunnels Lever"
+ HOTSPOT_COOKING_POT = "Hotspot: Cooking Pot"
+ HOTSPOT_DENTED_LOCKER = "Hotspot: Dented Locker"
+ HOTSPOT_DIRT_MOUND = "Hotspot: Dirt Mound"
+ HOTSPOT_DOCK_WINCH = "Hotspot: Dock Winch"
+ HOTSPOT_DRAGON_CLAW = "Hotspot: Dragon Claw"
+ HOTSPOT_DRAGON_NOSTRILS = "Hotspot: Dragon Nostrils"
+ HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE = "Hotspot: Dungeon Master's Lair Entrance"
+ HOTSPOT_FLOOD_CONTROL_BUTTONS = "Hotspot: Flood Control Buttons"
+ HOTSPOT_FLOOD_CONTROL_DOORS = "Hotspot: Flood Control Doors"
+ HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT = "Hotspot: Frozen Treat Machine Coin Slot"
+ HOTSPOT_FROZEN_TREAT_MACHINE_DOORS = "Hotspot: Frozen Treat Machine Doors"
+ HOTSPOT_GLASS_CASE = "Hotspot: Glass Case"
+ HOTSPOT_GRAND_INQUISITOR_DOLL = "Hotspot: Grand Inquisitor Doll"
+ HOTSPOT_GUE_TECH_DOOR = "Hotspot: GUE Tech Door"
+ HOTSPOT_GUE_TECH_GRASS = "Hotspot: GUE Tech Grass"
+ HOTSPOT_HADES_PHONE_BUTTONS = "Hotspot: Hades Phone Buttons"
+ HOTSPOT_HADES_PHONE_RECEIVER = "Hotspot: Hades Phone Receiver"
+ HOTSPOT_HARRY = "Hotspot: Harry"
+ HOTSPOT_HARRYS_ASHTRAY = "Hotspot: Harry's Ashtray"
+ HOTSPOT_HARRYS_BIRD_BATH = "Hotspot: Harry's Bird Bath"
+ HOTSPOT_IN_MAGIC_WE_TRUST_DOOR = "Hotspot: In Magic We Trust Door"
+ HOTSPOT_JACKS_DOOR = "Hotspot: Jack's Door"
+ HOTSPOT_LOUDSPEAKER_VOLUME_BUTTONS = "Hotspot: Loudspeaker Volume Buttons"
+ HOTSPOT_MAILBOX_DOOR = "Hotspot: Mailbox Door"
+ HOTSPOT_MAILBOX_FLAG = "Hotspot: Mailbox Flag"
+ HOTSPOT_MIRROR = "Hotspot: Mirror"
+ HOTSPOT_MONASTERY_VENT = "Hotspot: Monastery Vent"
+ HOTSPOT_MOSSY_GRATE = "Hotspot: Mossy Grate"
+ HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR = "Hotspot: Port Foozle Past Tavern Door"
+ HOTSPOT_PURPLE_WORDS = "Hotspot: Purple Words"
+ HOTSPOT_QUELBEE_HIVE = "Hotspot: Quelbee Hive"
+ HOTSPOT_ROPE_BRIDGE = "Hotspot: Rope Bridge"
+ HOTSPOT_SKULL_CAGE = "Hotspot: Skull Cage"
+ HOTSPOT_SNAPDRAGON = "Hotspot: Snapdragon"
+ HOTSPOT_SODA_MACHINE_BUTTONS = "Hotspot: Soda Machine Buttons"
+ HOTSPOT_SODA_MACHINE_COIN_SLOT = "Hotspot: Soda Machine Coin Slot"
+ HOTSPOT_SOUVENIR_COIN_SLOT = "Hotspot: Souvenir Coin Slot"
+ HOTSPOT_SPELL_CHECKER = "Hotspot: Spell Checker"
+ HOTSPOT_SPELL_LAB_CHASM = "Hotspot: Spell Lab Chasm"
+ HOTSPOT_SPRING_MUSHROOM = "Hotspot: Spring Mushroom"
+ HOTSPOT_STUDENT_ID_MACHINE = "Hotspot: Student ID Machine"
+ HOTSPOT_SUBWAY_TOKEN_SLOT = "Hotspot: Subway Token Slot"
+ HOTSPOT_TAVERN_FLY = "Hotspot: Tavern Fly"
+ HOTSPOT_TOTEMIZER_SWITCH = "Hotspot: Totemizer Switch"
+ HOTSPOT_TOTEMIZER_WHEELS = "Hotspot: Totemizer Wheels"
+ HOTSPOT_WELL = "Hotspot: Well"
+ HUNGUS_LARD = "Hungus Lard"
+ JAR_OF_HOTBUGS = "Jar of Hotbugs"
+ LANTERN = "Lantern"
+ LARGE_TELEGRAPH_HAMMER = "Large Telegraph Hammer"
+ LUCYS_PLAYING_CARD_1 = "Lucy's Playing Card: 1 Pip"
+ LUCYS_PLAYING_CARD_2 = "Lucy's Playing Card: 2 Pips"
+ LUCYS_PLAYING_CARD_3 = "Lucy's Playing Card: 3 Pips"
+ LUCYS_PLAYING_CARD_4 = "Lucy's Playing Card: 4 Pips"
+ MAP = "Map"
+ MEAD_LIGHT = "Mead Light"
+ MOSS_OF_MAREILON = "Moss of Mareilon"
+ MUG = "Mug"
+ OLD_SCRATCH_CARD = "Old Scratch Card"
+ PERMA_SUCK_MACHINE = "Perma-Suck Machine"
+ PLASTIC_SIX_PACK_HOLDER = "Plastic Six-Pack Holder"
+ POUCH_OF_ZORKMIDS = "Pouch of Zorkmids"
+ PROZORK_TABLET = "Prozork Tablet"
+ QUELBEE_HONEYCOMB = "Quelbee Honeycomb"
+ ROPE = "Rope"
+ SCROLL_FRAGMENT_ANS = "Scroll Fragment: ANS"
+ SCROLL_FRAGMENT_GIV = "Scroll Fragment: GIV"
+ SHOVEL = "Shovel"
+ SNAPDRAGON = "Snapdragon"
+ SPELL_GLORF = "Spell: GLORF"
+ SPELL_GOLGATEM = "Spell: GOLGATEM"
+ SPELL_IGRAM = "Spell: IGRAM"
+ SPELL_KENDALL = "Spell: KENDALL"
+ SPELL_NARWILE = "Spell: NARWILE"
+ SPELL_REZROV = "Spell: REZROV"
+ SPELL_THROCK = "Spell: THROCK"
+ SPELL_VOXAM = "Spell: VOXAM"
+ STUDENT_ID = "Student ID"
+ SUBWAY_DESTINATION_FLOOD_CONTROL_DAM = "Subway Destination: Flood Control Dam #3"
+ SUBWAY_DESTINATION_HADES = "Subway Destination: Hades"
+ SUBWAY_DESTINATION_MONASTERY = "Subway Destination: Monastery"
+ SUBWAY_TOKEN = "Subway Token"
+ SWORD = "Sword"
+ TELEPORTER_DESTINATION_DM_LAIR = "Teleporter Destination: Dungeon Master's Lair"
+ TELEPORTER_DESTINATION_GUE_TECH = "Teleporter Destination: GUE Tech"
+ TELEPORTER_DESTINATION_HADES = "Teleporter Destination: Hades"
+ TELEPORTER_DESTINATION_MONASTERY = "Teleporter Destination: Monastery Station"
+ TELEPORTER_DESTINATION_SPELL_LAB = "Teleporter Destination: Spell Lab"
+ TOTEM_BROG = "Totem: Brog"
+ TOTEM_GRIFF = "Totem: Griff"
+ TOTEM_LUCY = "Totem: Lucy"
+ TOTEMIZER_DESTINATION_HALL_OF_INQUISITION = "Totemizer Destination: Hall of Inquisition"
+ TOTEMIZER_DESTINATION_INFINITY = "Totemizer Destination: Infinity"
+ TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL = "Totemizer Destination: Straight to Hell"
+ TOTEMIZER_DESTINATION_SURFACE_OF_MERZ = "Totemizer Destination: Surface of Merz"
+ ZIMDOR_SCROLL = "ZIMDOR Scroll"
+ ZORK_ROCKS = "Zork Rocks"
+
+
+class ZorkGrandInquisitorLocations(enum.Enum):
+ ALARM_SYSTEM_IS_DOWN = "Alarm System is Down"
+ ARREST_THE_VANDAL = "Arrest the Vandal!"
+ ARTIFACTS_EXPLAINED = "Artifacts, Explained"
+ A_BIG_FAT_SASSY_2_HEADED_MONSTER = "A Big, Fat, SASSY 2-Headed Monster"
+ A_LETTER_FROM_THE_WHITE_HOUSE = "A Letter from the White House"
+ A_SMALLWAY = "A Smallway"
+ BEAUTIFUL_THATS_PLENTY = "Beautiful, That's Plenty!"
+ BEBURTT_DEMYSTIFIED = "BEBURTT, Demystified"
+ BETTER_SPELL_MANUFACTURING_IN_UNDER_10_MINUTES = "Better Spell Manufacturing in Under 10 Minutes"
+ BOING_BOING_BOING = "Boing, Boing, Boing"
+ BONK = "Bonk!"
+ BRAVE_SOULS_WANTED = "Brave Souls Wanted"
+ BROG_DO_GOOD = "Brog Do Good!"
+ BROG_EAT_ROCKS = "Brog Eat Rocks"
+ BROG_KNOW_DUMB_THAT_DUMB = "Brog Know Dumb. That Dumb"
+ BROG_MUCH_BETTER_AT_THIS_GAME = "Brog Much Better at This Game"
+ CASTLE_WATCHING_A_FIELD_GUIDE = "Castle Watching: A Field Guide"
+ CAVES_NOTES = "Cave's Notes"
+ CLOSING_THE_TIME_TUNNELS = "Closing the Time Tunnels"
+ CRISIS_AVERTED = "Crisis Averted"
+ CUT_THAT_OUT_YOU_LITTLE_CREEP = "Cut That Out You Little Creep!"
+ DEATH_ARRESTED_WITH_JACK = "Death: Arrested With Jack"
+ DEATH_ATTACKED_THE_QUELBEES = "Death: Attacked the Quelbees"
+ DEATH_CLIMBED_OUT_OF_THE_WELL = "Death: Climbed Out of the Well"
+ DEATH_EATEN_BY_A_GRUE = "Death: Eaten by a Grue"
+ DEATH_JUMPED_IN_BOTTOMLESS_PIT = "Death: Jumped in Bottomless Pit"
+ DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER = "Death: Lost Game of Strip Grue, Fire, Water"
+ DEATH_LOST_SOUL_TO_OLD_SCRATCH = "Death: Lost Soul to Old Scratch"
+ DEATH_OUTSMARTED_BY_THE_QUELBEES = "Death: Outsmarted by the Quelbees"
+ DEATH_SLICED_UP_BY_THE_INVISIBLE_GUARD = "Death: Sliced up by the Invisible Guard"
+ DEATH_STEPPED_INTO_THE_INFINITE = "Death: Step Into the Infinite"
+ DEATH_SWALLOWED_BY_A_DRAGON = "Death: Swallowed by a Dragon"
+ DEATH_THROCKED_THE_GRASS = "Death: THROCKed the Grass"
+ DEATH_TOTEMIZED = "Death: Totemized?"
+ DEATH_TOTEMIZED_PERMANENTLY = "Death: Totemized... Permanently"
+ DEATH_YOURE_NOT_CHARON = "Death: You're Not Charon!?"
+ DEATH_ZORK_ROCKS_EXPLODED = "Death: Zork Rocks Exploded"
+ DENIED_BY_THE_LAKE_MONSTER = "Denied by the Lake Monster"
+ DESPERATELY_SEEKING_TUTOR = "Desperately Seeking Tutor"
+ DONT_EVEN_START_WITH_US_SPARKY = "Don't Even Start With Us, Sparky"
+ DOOOOOOWN = "Doooooown"
+ DOWN = "Down"
+ DRAGON_ARCHIPELAGO_TIME_TUNNEL = "Dragon Archipelago Time Tunnel"
+ DUNCE_LOCKER = "Dunce Locker"
+ EGGPLANTS = "Eggplants"
+ ELSEWHERE = "Elsewhere"
+ EMERGENCY_MAGICATRONIC_MESSAGE = "Emergency Magicatronic Message"
+ ENJOY_YOUR_TRIP = "Enjoy Your Trip!"
+ FAT_LOT_OF_GOOD_THATLL_DO_YA = "Fat Lot of Good That'll Do Ya"
+ FIRE_FIRE = "Fire! Fire!"
+ FLOOD_CONTROL_DAM_3_THE_NOT_REMOTELY_BORING_TALE = "Flood Control Dam #3: The Not Remotely Boring Tale"
+ FLYING_SNAPDRAGON = "Flying Snapdragon"
+ FROBUARY_3_UNDERGROUNDHOG_DAY = "Frobruary 3 - Undergroundhog Day"
+ GETTING_SOME_CHANGE = "Getting Some Change"
+ GO_AWAY = "GO AWAY!"
+ GUE_TECH_DEANS_LIST = "GUE Tech Dean's List"
+ GUE_TECH_ENTRANCE_EXAM = "GUE Tech Entrance Exam"
+ GUE_TECH_HEALTH_MEMO = "GUE Tech Health Memo"
+ GUE_TECH_MAGEMEISTERS = "GUE Tech Magemeisters"
+ HAVE_A_HELL_OF_A_DAY = "Have a Hell of a Day!"
+ HELLO_THIS_IS_SHONA_FROM_GURTH_PUBLISHING = "Hello, This is Shona from Gurth Publishing"
+ HELP_ME_CANT_BREATHE = "Help... Me. Can't... Breathe"
+ HEY_FREE_DIRT = "Hey, Free Dirt!"
+ HI_MY_NAME_IS_DOUG = "Hi, My Name is Doug"
+ HMMM_INFORMATIVE_YET_DEEPLY_DISTURBING = "Hmmm. Informative. Yet Deeply Disturbing"
+ HOLD_ON_FOR_AN_IMPORTANT_MESSAGE = "Hold on for an Important Message"
+ HOW_TO_HYPNOTIZE_YOURSELF = "How to Hypnotize Yourself"
+ HOW_TO_WIN_AT_DOUBLE_FANUCCI = "How to Win at Double Fanucci"
+ IMBUE_BEBURTT = "Imbue BEBURTT"
+ IM_COMPLETELY_NUDE = "I'm Completely Nude"
+ INTO_THE_FOLIAGE = "Into the Foliage"
+ INVISIBLE_FLOWERS = "Invisible Flowers"
+ IN_CASE_OF_ADVENTURE = "In Case of Adventure, Break Glass!"
+ IN_MAGIC_WE_TRUST = "In Magic We Trust"
+ ITS_ONE_OF_THOSE_ADVENTURERS_AGAIN = "It's One of Those Adventurers Again..."
+ I_DONT_THINK_YOU_WOULDVE_WANTED_THAT_TO_WORK_ANYWAY = "I Don't Think You Would've Wanted That to Work Anyway"
+ I_DONT_WANT_NO_TROUBLE = "I Don't Want No Trouble!"
+ I_HOPE_YOU_CAN_CLIMB_UP_THERE = "I Hope You Can Climb Up There With All This Junk"
+ I_LIKE_YOUR_STYLE = "I Like Your Style!"
+ I_SPIT_ON_YOUR_FILTHY_COINAGE = "I Spit on Your Filthy Coinage"
+ LIT_SUNFLOWERS = "Lit Sunflowers"
+ MAGIC_FOREVER = "Magic Forever!"
+ MAILED_IT_TO_HELL = "Mailed it to Hell"
+ MAKE_LOVE_NOT_WAR = "Make Love, Not War"
+ MEAD_LIGHT = "Mead Light?"
+ MIKES_PANTS = "Mike's Pants"
+ MUSHROOM_HAMMERED = "Mushroom, Hammered"
+ NATIONAL_TREASURE = "300 Year Old National Treasure"
+ NATURAL_AND_SUPERNATURAL_CREATURES_OF_QUENDOR = "Natural and Supernatural Creatures of Quendor"
+ NOOOOOOOOOOOOO = "NOOOOOOOOOOOOO!"
+ NOTHIN_LIKE_A_GOOD_STOGIE = "Nothin' Like a Good Stogie"
+ NOW_YOU_LOOK_LIKE_US_WHICH_IS_AN_IMPROVEMENT = "Now You Look Like Us, Which is an Improvement"
+ NO_AUTOGRAPHS = "No Autographs"
+ NO_BONDAGE = "No Bondage"
+ OBIDIL_DRIED_UP = "OBIDIL, Dried Up"
+ OH_DEAR_GOD_ITS_A_DRAGON = "Oh Dear God, It's a Dragon!"
+ OH_VERY_FUNNY_GUYS = "Oh, Very Funny Guys"
+ OH_WOW_TALK_ABOUT_DEJA_VU = "Oh, Wow! Talk About Deja Vu"
+ OLD_SCRATCH_WINNER = "Old Scratch Winner!"
+ ONLY_YOU_CAN_PREVENT_FOOZLE_FIRES = "Only You Can Prevent Foozle Fires"
+ OPEN_THE_GATES_OF_HELL = "Open the Gates of Hell"
+ OUTSMART_THE_QUELBEES = "Outsmart the Quelbees"
+ PERMASEAL = "PermaSeal"
+ PLANETFALL = "Planetfall"
+ PLEASE_DONT_THROCK_THE_GRASS = "Please Don't THROCK the Grass"
+ PORT_FOOZLE_TIME_TUNNEL = "Port Foozle Time Tunnel"
+ PROZORKED = "Prozorked"
+ REASSEMBLE_SNAVIG = "Reassemble SNAVIG"
+ RESTOCKED_ON_GRUESDAY = "Restocked on Gruesday"
+ RIGHT_HELLO_YES_UH_THIS_IS_SNEFFLE = "Right. Hello. Yes. Uh, This is Sneffle"
+ RIGHT_UH_SORRY_ITS_ME_AGAIN_SNEFFLE = "Right. Uh, Sorry. It's Me Again. Sneffle"
+ SNAVIG_REPAIRED = "SNAVIG, Repaired"
+ SOUVENIR = "Souvenir"
+ STRAIGHT_TO_HELL = "Straight to Hell"
+ STRIP_GRUE_FIRE_WATER = "Strip Grue, Fire, Water"
+ SUCKING_ROCKS = "Sucking Rocks"
+ TALK_TO_ME_GRAND_INQUISITOR = "Talk to Me Grand Inquisitor"
+ TAMING_YOUR_SNAPDRAGON = "Taming Your Snapdragon"
+ THAR_SHE_BLOWS = "Thar She Blows!"
+ THATS_A_ROPE = "That's a Rope"
+ THATS_IT_JUST_KEEP_HITTING_THOSE_BUTTONS = "That's it! Just Keep Hitting Those Buttons"
+ THATS_STILL_A_ROPE = "That's Still a Rope"
+ THATS_THE_SPIRIT = "That's the Spirit!"
+ THE_ALCHEMICAL_DEBACLE = "The Alchemical Debacle"
+ THE_ENDLESS_FIRE = "The Endless Fire"
+ THE_FLATHEADIAN_FUDGE_FIASCO = "The Flatheadian Fudge Fiasco"
+ THE_PERILS_OF_MAGIC = "The Perils of Magic"
+ THE_UNDERGROUND_UNDERGROUND = "The Underground Underground"
+ THIS_DOESNT_LOOK_ANYTHING_LIKE_THE_BROCHURE = "This Doesn't Look Anything Like the Brochure"
+ THROCKED_MUSHROOM_HAMMERED = "THROCKed Mushroom, Hammered"
+ TIME_TRAVEL_FOR_DUMMIES = "Time Travel for Dummies"
+ TOTEMIZED_DAILY_BILLBOARD = "Totemized Daily Billboard Functioning Correctly"
+ UH_OH_BROG_CANT_SWIM = "Uh-Oh. Brog Can't Swim"
+ UMBRELLA_FLOWERS = "Umbrella Flowers"
+ UP = "Up"
+ USELESS_BUT_FUN = "Useless, But Fun"
+ UUUUUP = "Uuuuup"
+ VOYAGE_OF_CAPTAIN_ZAHAB = "Voyage of Captain Zahab"
+ WANT_SOME_RYE_COURSE_YA_DO = "Want Some Rye? Course Ya Do!"
+ WE_DONT_SERVE_YOUR_KIND_HERE = "We Don't Serve Your Kind Here"
+ WE_GOT_A_HIGH_ROLLER = "We Got a High Roller!"
+ WHAT_ARE_YOU_STUPID = "What Are You, Stupid?"
+ WHITE_HOUSE_TIME_TUNNEL = "White House Time Tunnel"
+ WOW_IVE_NEVER_GONE_INSIDE_HIM_BEFORE = "Wow! I've Never Gone Inside Him Before!"
+ YAD_GOHDNUORGREDNU_3_YRAUBORF = "yaD gohdnuorgrednU - 3 yrauborF"
+ YOUR_PUNY_WEAPONS_DONT_PHASE_ME_BABY = "Your Puny Weapons Don't Phase Me, Baby!"
+ YOU_DONT_GO_MESSING_WITH_A_MANS_ZIPPER = "You Don't Go Messing With a Man's Zipper"
+ YOU_GAINED_86_EXPERIENCE_POINTS = "You Gained 86 Experience Points"
+ YOU_ONE_OF_THEM_AGITATORS_AINT_YA = "You One of Them Agitators, Ain't Ya?"
+ YOU_WANT_A_PIECE_OF_ME_DOCK_BOY = "You Want a Piece of Me, Dock Boy? or Girl"
+
+
+class ZorkGrandInquisitorRegions(enum.Enum):
+ CROSSROADS = "Crossroads"
+ DM_LAIR = "Dungeon Master's Lair"
+ DM_LAIR_INTERIOR = "Dungeon Master's Lair - Interior"
+ DRAGON_ARCHIPELAGO = "Dragon Archipelago"
+ DRAGON_ARCHIPELAGO_DRAGON = "Dragon Archipelago - Dragon"
+ ENDGAME = "Endgame"
+ GUE_TECH = "GUE Tech"
+ GUE_TECH_HALLWAY = "GUE Tech - Hallway"
+ GUE_TECH_OUTSIDE = "GUE Tech - Outside"
+ HADES = "Hades"
+ HADES_BEYOND_GATES = "Hades - Beyond Gates"
+ HADES_SHORE = "Hades - Shore"
+ MENU = "Menu"
+ MONASTERY = "Monastery"
+ MONASTERY_EXHIBIT = "Monastery - Exhibit"
+ PORT_FOOZLE = "Port Foozle"
+ PORT_FOOZLE_JACKS_SHOP = "Port Foozle - Jack's Shop"
+ PORT_FOOZLE_PAST = "Port Foozle Past"
+ PORT_FOOZLE_PAST_TAVERN = "Port Foozle Past - Tavern"
+ SPELL_LAB = "Spell Lab"
+ SPELL_LAB_BRIDGE = "Spell Lab - Bridge"
+ SUBWAY_CROSSROADS = "Subway Platform - Crossroads"
+ SUBWAY_FLOOD_CONTROL_DAM = "Subway Platform - Flood Control Dam #3"
+ SUBWAY_MONASTERY = "Subway Platform - Monastery"
+ WALKING_CASTLE = "Walking Castle"
+ WHITE_HOUSE = "White House"
+
+
+class ZorkGrandInquisitorTags(enum.Enum):
+ CORE = "Core"
+ DEATHSANITY = "Deathsanity"
+ FILLER = "Filler"
+ HOTSPOT = "Hotspot"
+ INVENTORY_ITEM = "Inventory Item"
+ MISSABLE = "Missable"
+ SPELL = "Spell"
+ SUBWAY_DESTINATION = "Subway Destination"
+ TELEPORTER_DESTINATION = "Teleporter Destination"
+ TOTEMIZER_DESTINATION = "Totemizer Destination"
+ TOTEM = "Totem"
diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py
new file mode 100644
index 000000000000..7a60a1460829
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/game_controller.py
@@ -0,0 +1,1388 @@
+import collections
+import functools
+import logging
+
+from typing import Dict, Optional, Set, Tuple, Union
+
+from .data.item_data import item_data, ZorkGrandInquisitorItemData
+from .data.location_data import location_data, ZorkGrandInquisitorLocationData
+
+from .data.missable_location_grant_conditions_data import (
+ missable_location_grant_conditions_data,
+ ZorkGrandInquisitorMissableLocationGrantConditionsData,
+)
+
+from .data_funcs import game_id_to_items, items_with_tag, locations_with_tag
+
+from .enums import (
+ ZorkGrandInquisitorGoals,
+ ZorkGrandInquisitorItems,
+ ZorkGrandInquisitorLocations,
+ ZorkGrandInquisitorTags,
+)
+
+from .game_state_manager import GameStateManager
+
+
+class GameController:
+ logger: Optional[logging.Logger]
+
+ game_state_manager: GameStateManager
+
+ received_items: Set[ZorkGrandInquisitorItems]
+ completed_locations: Set[ZorkGrandInquisitorLocations]
+
+ completed_locations_queue: collections.deque
+ received_items_queue: collections.deque
+
+ all_hotspot_items: Set[ZorkGrandInquisitorItems]
+
+ game_id_to_items: Dict[int, ZorkGrandInquisitorItems]
+
+ possible_inventory_items: Set[ZorkGrandInquisitorItems]
+
+ available_inventory_slots: Set[int]
+
+ goal_completed: bool
+
+ option_goal: Optional[ZorkGrandInquisitorGoals]
+ option_deathsanity: Optional[bool]
+ option_grant_missable_location_checks: Optional[bool]
+
+ def __init__(self, logger=None) -> None:
+ self.logger = logger
+
+ self.game_state_manager = GameStateManager()
+
+ self.received_items = set()
+ self.completed_locations = set()
+
+ self.completed_locations_queue = collections.deque()
+ self.received_items_queue = collections.deque()
+
+ self.all_hotspot_items = (
+ items_with_tag(ZorkGrandInquisitorTags.HOTSPOT)
+ | items_with_tag(ZorkGrandInquisitorTags.SUBWAY_DESTINATION)
+ | items_with_tag(ZorkGrandInquisitorTags.TOTEMIZER_DESTINATION)
+ )
+
+ self.game_id_to_items = game_id_to_items()
+
+ self.possible_inventory_items = (
+ items_with_tag(ZorkGrandInquisitorTags.INVENTORY_ITEM)
+ | items_with_tag(ZorkGrandInquisitorTags.SPELL)
+ | items_with_tag(ZorkGrandInquisitorTags.TOTEM)
+ )
+
+ self.available_inventory_slots = set()
+
+ self.goal_completed = False
+
+ self.option_goal = None
+ self.option_deathsanity = None
+ self.option_grant_missable_location_checks = None
+
+ @functools.cached_property
+ def brog_items(self) -> Set[ZorkGrandInquisitorItems]:
+ return {
+ ZorkGrandInquisitorItems.BROGS_BICKERING_TORCH,
+ ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH,
+ ZorkGrandInquisitorItems.BROGS_GRUE_EGG,
+ ZorkGrandInquisitorItems.BROGS_PLANK,
+ }
+
+ @functools.cached_property
+ def griff_items(self) -> Set[ZorkGrandInquisitorItems]:
+ return {
+ ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP,
+ ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN,
+ }
+
+ @functools.cached_property
+ def lucy_items(self) -> Set[ZorkGrandInquisitorItems]:
+ return {
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4,
+ }
+
+ @property
+ def totem_items(self) -> Set[ZorkGrandInquisitorItems]:
+ return self.brog_items | self.griff_items | self.lucy_items
+
+ @functools.cached_property
+ def missable_locations(self) -> Set[ZorkGrandInquisitorLocations]:
+ return locations_with_tag(ZorkGrandInquisitorTags.MISSABLE)
+
+ def log(self, message) -> None:
+ if self.logger:
+ self.logger.info(message)
+
+ def log_debug(self, message) -> None:
+ if self.logger:
+ self.logger.debug(message)
+
+ def open_process_handle(self) -> bool:
+ return self.game_state_manager.open_process_handle()
+
+ def close_process_handle(self) -> bool:
+ return self.game_state_manager.close_process_handle()
+
+ def is_process_running(self) -> bool:
+ return self.game_state_manager.is_process_running
+
+ def list_received_brog_items(self) -> None:
+ self.log("Received Brog Items:")
+
+ self._process_received_items()
+ received_brog_items: Set[ZorkGrandInquisitorItems] = self.received_items & self.brog_items
+
+ if not len(received_brog_items):
+ self.log(" Nothing")
+ return
+
+ for item in sorted(i.value for i in received_brog_items):
+ self.log(f" {item}")
+
+ def list_received_griff_items(self) -> None:
+ self.log("Received Griff Items:")
+
+ self._process_received_items()
+ received_griff_items: Set[ZorkGrandInquisitorItems] = self.received_items & self.griff_items
+
+ if not len(received_griff_items):
+ self.log(" Nothing")
+ return
+
+ for item in sorted(i.value for i in received_griff_items):
+ self.log(f" {item}")
+
+ def list_received_lucy_items(self) -> None:
+ self.log("Received Lucy Items:")
+
+ self._process_received_items()
+ received_lucy_items: Set[ZorkGrandInquisitorItems] = self.received_items & self.lucy_items
+
+ if not len(received_lucy_items):
+ self.log(" Nothing")
+ return
+
+ for item in sorted(i.value for i in received_lucy_items):
+ self.log(f" {item}")
+
+ def list_received_hotspots(self) -> None:
+ self.log("Received Hotspots:")
+
+ self._process_received_items()
+
+ hotspot_items: Set[ZorkGrandInquisitorItems] = items_with_tag(ZorkGrandInquisitorTags.HOTSPOT)
+ received_hotspots: Set[ZorkGrandInquisitorItems] = self.received_items & hotspot_items
+
+ if not len(received_hotspots):
+ self.log(" Nothing")
+ return
+
+ for item in sorted(i.value for i in received_hotspots):
+ self.log(f" {item}")
+
+ def update(self) -> None:
+ if self.game_state_manager.is_process_still_running():
+ try:
+ self.game_state_manager.refresh_game_location()
+
+ self._apply_permanent_game_state()
+ self._apply_conditional_game_state()
+
+ self._apply_permanent_game_flags()
+
+ self._check_for_completed_locations()
+
+ if self.option_grant_missable_location_checks:
+ self._check_for_missable_locations_to_grant()
+
+ self._process_received_items()
+
+ self._manage_hotspots()
+ self._manage_items()
+
+ self._apply_conditional_teleports()
+
+ self._check_for_victory()
+ except Exception as e:
+ self.log_debug(e)
+
+ def _apply_permanent_game_state(self) -> None:
+ self._write_game_state_value_for(10934, 1) # Rope Taken
+ self._write_game_state_value_for(10418, 1) # Mead Light Taken
+ self._write_game_state_value_for(10275, 0) # Lantern in Crate
+ self._write_game_state_value_for(13929, 1) # Great Underground Door Open
+ self._write_game_state_value_for(13968, 1) # Subway Token Taken
+ self._write_game_state_value_for(12930, 1) # Hammer Taken
+ self._write_game_state_value_for(12935, 1) # Griff Totem Taken
+ self._write_game_state_value_for(12948, 1) # ZIMDOR Scroll Taken
+ self._write_game_state_value_for(4058, 1) # Shovel Taken
+ self._write_game_state_value_for(4059, 1) # THROCK Scroll Taken
+ self._write_game_state_value_for(11758, 1) # KENDALL Scroll Taken
+ self._write_game_state_value_for(16959, 1) # Old Scratch Card Taken
+ self._write_game_state_value_for(12840, 0) # Zork Rocks in Perma-Suck Machine
+ self._write_game_state_value_for(11886, 1) # Student ID Taken
+ self._write_game_state_value_for(16279, 1) # Prozork Tablet Taken
+ self._write_game_state_value_for(13260, 1) # GOLGATEM Scroll Taken
+ self._write_game_state_value_for(4834, 1) # Flatheadia Fudge Taken
+ self._write_game_state_value_for(4746, 1) # Jar of Hotbugs Taken
+ self._write_game_state_value_for(4755, 1) # Hungus Lard Taken
+ self._write_game_state_value_for(4758, 1) # Mug Taken
+ self._write_game_state_value_for(3716, 1) # NARWILE Scroll Taken
+ self._write_game_state_value_for(17147, 1) # Lucy Totem Taken
+ self._write_game_state_value_for(9818, 1) # Middle Telegraph Hammer Taken
+ self._write_game_state_value_for(3766, 0) # ANS Scroll in Window
+ self._write_game_state_value_for(4980, 0) # ANS Scroll in Window
+ self._write_game_state_value_for(3768, 0) # GIV Scroll in Window
+ self._write_game_state_value_for(4978, 0) # GIV Scroll in Window
+ self._write_game_state_value_for(3765, 0) # SNA Scroll in Window
+ self._write_game_state_value_for(4979, 0) # SNA Scroll in Window
+ self._write_game_state_value_for(3767, 0) # VIG Scroll in Window
+ self._write_game_state_value_for(4977, 0) # VIG Scroll in Window
+ self._write_game_state_value_for(15065, 1) # Brog's Bickering Torch Taken
+ self._write_game_state_value_for(15088, 1) # Brog's Flickering Torch Taken
+ self._write_game_state_value_for(2628, 4) # Brog's Grue Eggs Taken
+ self._write_game_state_value_for(2971, 1) # Brog's Plank Taken
+ self._write_game_state_value_for(1340, 1) # Griff's Inflatable Sea Captain Taken
+ self._write_game_state_value_for(1341, 1) # Griff's Inflatable Raft Taken
+ self._write_game_state_value_for(1477, 1) # Griff's Air Pump Taken
+ self._write_game_state_value_for(1814, 1) # Griff's Dragon Tooth Taken
+ self._write_game_state_value_for(15403, 0) # Lucy's Cards Taken
+ self._write_game_state_value_for(15404, 1) # Lucy's Cards Taken
+ self._write_game_state_value_for(15405, 4) # Lucy's Cards Taken
+ self._write_game_state_value_for(5222, 1) # User Has Spell Book
+ self._write_game_state_value_for(13930, 1) # Skip Well Cutscenes
+ self._write_game_state_value_for(19057, 1) # Skip Well Cutscenes
+ self._write_game_state_value_for(13934, 1) # Skip Well Cutscenes
+ self._write_game_state_value_for(13935, 1) # Skip Well Cutscenes
+ self._write_game_state_value_for(13384, 1) # Skip Meanwhile... Cutscene
+ self._write_game_state_value_for(8620, 1) # First Coin Paid to Charon
+ self._write_game_state_value_for(8731, 1) # First Coin Paid to Charon
+
+ def _apply_conditional_game_state(self):
+ # Can teleport to Dungeon Master's Lair
+ if self._player_has(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR):
+ self._write_game_state_value_for(2203, 1)
+ else:
+ self._write_game_state_value_for(2203, 0)
+
+ # Can teleport to GUE Tech
+ if self._player_has(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH):
+ self._write_game_state_value_for(7132, 1)
+ else:
+ self._write_game_state_value_for(7132, 0)
+
+ # Can Teleport to Spell Lab
+ if self._player_has(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB):
+ self._write_game_state_value_for(16545, 1)
+ else:
+ self._write_game_state_value_for(16545, 0)
+
+ # Can Teleport to Hades
+ if self._player_has(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES):
+ self._write_game_state_value_for(7119, 1)
+ else:
+ self._write_game_state_value_for(7119, 0)
+
+ # Can Teleport to Monastery Station
+ if self._player_has(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY):
+ self._write_game_state_value_for(7148, 1)
+ else:
+ self._write_game_state_value_for(7148, 0)
+
+ # Initial Totemizer Destination
+ should_force_initial_totemizer_destination: bool = True
+
+ if self._player_has(ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION):
+ should_force_initial_totemizer_destination = False
+ elif self._player_has(ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL):
+ should_force_initial_totemizer_destination = False
+ elif self._player_has(ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY):
+ should_force_initial_totemizer_destination = False
+ elif self._player_has(ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ):
+ should_force_initial_totemizer_destination = False
+
+ if should_force_initial_totemizer_destination:
+ self._write_game_state_value_for(9617, 2)
+
+ # Pouch of Zorkmids
+ if self._player_has(ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS):
+ self._write_game_state_value_for(5827, 1)
+ else:
+ self._write_game_state_value_for(5827, 0)
+
+ # Brog Torches
+ if self._player_is_brog() and self._player_has(ZorkGrandInquisitorItems.BROGS_BICKERING_TORCH):
+ self._write_game_state_value_for(10999, 1)
+ else:
+ self._write_game_state_value_for(10999, 0)
+
+ if self._player_is_brog() and self._player_has(ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH):
+ self._write_game_state_value_for(10998, 1)
+ else:
+ self._write_game_state_value_for(10998, 0)
+
+ # Monastery Rope
+ if ZorkGrandInquisitorLocations.I_HOPE_YOU_CAN_CLIMB_UP_THERE in self.completed_locations:
+ self._write_game_state_value_for(9637, 1)
+
+ def _apply_permanent_game_flags(self) -> None:
+ self._write_game_flags_value_for(9437, 2) # Monastery Exhibit Door to Outside
+ self._write_game_flags_value_for(3074, 2) # White House Door
+ self._write_game_flags_value_for(13005, 2) # Map
+ self._write_game_flags_value_for(13006, 2) # Sword
+ self._write_game_flags_value_for(13007, 2) # Sword
+ self._write_game_flags_value_for(13389, 2) # Moss of Mareilon
+ self._write_game_flags_value_for(4301, 2) # Quelbee Honeycomb
+ self._write_game_flags_value_for(12895, 2) # Change Machine Money
+ self._write_game_flags_value_for(4150, 2) # Prozorked Snapdragon
+ self._write_game_flags_value_for(13413, 2) # Letter Opener
+ self._write_game_flags_value_for(15403, 2) # Lucy's Cards
+
+ def _check_for_completed_locations(self) -> None:
+ location: ZorkGrandInquisitorLocations
+ data: ZorkGrandInquisitorLocationData
+ for location, data in location_data.items():
+ if location in self.completed_locations or not isinstance(
+ location, ZorkGrandInquisitorLocations
+ ):
+ continue
+
+ is_location_completed: bool = True
+
+ trigger: [Union[str, int]]
+ value: Union[str, int, Tuple[int, ...]]
+ for trigger, value in data.game_state_trigger:
+ if trigger == "location":
+ if not self._player_is_at(value):
+ is_location_completed = False
+ break
+ elif isinstance(trigger, int):
+ if isinstance(value, int):
+ if self._read_game_state_value_for(trigger) != value:
+ is_location_completed = False
+ break
+ elif isinstance(value, tuple):
+ if self._read_game_state_value_for(trigger) not in value:
+ is_location_completed = False
+ break
+ else:
+ is_location_completed = False
+ break
+ else:
+ is_location_completed = False
+ break
+
+ if is_location_completed:
+ self.completed_locations.add(location)
+ self.completed_locations_queue.append(location)
+
+ def _check_for_missable_locations_to_grant(self) -> None:
+ missable_location: ZorkGrandInquisitorLocations
+ for missable_location in self.missable_locations:
+ if missable_location in self.completed_locations:
+ continue
+
+ data: ZorkGrandInquisitorLocationData = location_data[missable_location]
+
+ if ZorkGrandInquisitorTags.DEATHSANITY in data.tags and not self.option_deathsanity:
+ continue
+
+ condition_data: ZorkGrandInquisitorMissableLocationGrantConditionsData = (
+ missable_location_grant_conditions_data.get(missable_location)
+ )
+
+ if condition_data is None:
+ self.log_debug(f"Missable Location {missable_location.value} has no grant conditions")
+ continue
+
+ if condition_data.location_condition in self.completed_locations:
+ grant_location: bool = True
+
+ item: ZorkGrandInquisitorItems
+ for item in condition_data.item_conditions or tuple():
+ if self._player_doesnt_have(item):
+ grant_location = False
+ break
+
+ if grant_location:
+ self.completed_locations_queue.append(missable_location)
+
+ def _process_received_items(self) -> None:
+ while len(self.received_items_queue) > 0:
+ item: ZorkGrandInquisitorItems = self.received_items_queue.popleft()
+ data: ZorkGrandInquisitorItemData = item_data[item]
+
+ if ZorkGrandInquisitorTags.FILLER in data.tags:
+ continue
+
+ self.received_items.add(item)
+
+ def _manage_hotspots(self) -> None:
+ hotspot_item: ZorkGrandInquisitorItems
+ for hotspot_item in self.all_hotspot_items:
+ data: ZorkGrandInquisitorItemData = item_data[hotspot_item]
+
+ if hotspot_item not in self.received_items:
+ key: int
+ for key in data.statemap_keys:
+ self._write_game_flags_value_for(key, 2)
+ else:
+ if hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_666_MAILBOX:
+ if self.game_state_manager.game_location == "hp5g":
+ if self._read_game_state_value_for(9113) == 0:
+ self._write_game_flags_value_for(9116, 0)
+ else:
+ self._write_game_flags_value_for(9116, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS:
+ if self.game_state_manager.game_location == "qb2g":
+ if self._read_game_state_value_for(15433) == 0:
+ self._write_game_flags_value_for(15434, 0)
+ else:
+ self._write_game_flags_value_for(15434, 2)
+
+ if self._read_game_state_value_for(15435) == 0:
+ self._write_game_flags_value_for(15436, 0)
+ else:
+ self._write_game_flags_value_for(15436, 2)
+
+ if self._read_game_state_value_for(15437) == 0:
+ self._write_game_flags_value_for(15438, 0)
+ else:
+ self._write_game_flags_value_for(15438, 2)
+
+ if self._read_game_state_value_for(15439) == 0:
+ self._write_game_flags_value_for(15440, 0)
+ else:
+ self._write_game_flags_value_for(15440, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_BLANK_SCROLL_BOX:
+ if self.game_state_manager.game_location == "tp2g":
+ if self._read_game_state_value_for(12095) == 1:
+ self._write_game_flags_value_for(9115, 2)
+ else:
+ self._write_game_flags_value_for(9115, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_BLINDS:
+ if self.game_state_manager.game_location == "dv1e":
+ if self._read_game_state_value_for(4743) == 0:
+ self._write_game_flags_value_for(4799, 0)
+ else:
+ self._write_game_flags_value_for(4799, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS:
+ if self.game_state_manager.game_location == "tr5g":
+ key: int
+ for key in data.statemap_keys:
+ self._write_game_flags_value_for(key, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT:
+ if self.game_state_manager.game_location == "tr5g":
+ self._write_game_flags_value_for(12702, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_VACUUM_SLOT:
+ if self.game_state_manager.game_location == "tr5m":
+ self._write_game_flags_value_for(12909, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_CHANGE_MACHINE_SLOT:
+ if self.game_state_manager.game_location == "tr5j":
+ if self._read_game_state_value_for(12892) == 0:
+ self._write_game_flags_value_for(12900, 0)
+ else:
+ self._write_game_flags_value_for(12900, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR:
+ if self.game_state_manager.game_location == "dw1e":
+ if self._read_game_state_value_for(4983) == 0:
+ self._write_game_flags_value_for(5010, 0)
+ else:
+ self._write_game_flags_value_for(5010, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT:
+ if self.game_state_manager.game_location == "me2j":
+ if self._read_game_state_value_for(9491) == 2:
+ self._write_game_flags_value_for(9539, 0)
+ else:
+ self._write_game_flags_value_for(9539, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER:
+ if self.game_state_manager.game_location == "me2j":
+ if self._read_game_state_value_for(9546) == 2 or self._read_game_state_value_for(9419) == 1:
+ self._write_game_flags_value_for(19712, 2)
+ else:
+ self._write_game_flags_value_for(19712, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT:
+ if self.game_state_manager.game_location == "sg1f":
+ self._write_game_flags_value_for(2586, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_DENTED_LOCKER:
+ if self.game_state_manager.game_location == "th3j":
+ five_is_open: bool = self._read_game_state_value_for(11847) == 1
+ six_is_open: bool = self._read_game_state_value_for(11840) == 1
+ seven_is_open: bool = self._read_game_state_value_for(11841) == 1
+ eight_is_open: bool = self._read_game_state_value_for(11848) == 1
+
+ rocks_in_six: bool = self._read_game_state_value_for(11769) == 1
+ six_blasted: bool = self._read_game_state_value_for(11770) == 1
+
+ if five_is_open or six_is_open or seven_is_open or eight_is_open or rocks_in_six or six_blasted:
+ self._write_game_flags_value_for(11878, 2)
+ else:
+ self._write_game_flags_value_for(11878, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_DIRT_MOUND:
+ if self.game_state_manager.game_location == "te5e":
+ if self._read_game_state_value_for(11747) == 0:
+ self._write_game_flags_value_for(11751, 0)
+ else:
+ self._write_game_flags_value_for(11751, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH:
+ if self.game_state_manager.game_location == "pe2e":
+ self._write_game_flags_value_for(15147, 0)
+ self._write_game_flags_value_for(15153, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_DRAGON_CLAW:
+ if self.game_state_manager.game_location == "cd70":
+ self._write_game_flags_value_for(1705, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS:
+ if self.game_state_manager.game_location == "cd3h":
+ raft_in_left: bool = self._read_game_state_value_for(1301) == 1
+ raft_in_right: bool = self._read_game_state_value_for(1304) == 1
+ raft_inflated: bool = self._read_game_state_value_for(1379) == 1
+
+ captain_in_left: bool = self._read_game_state_value_for(1374) == 1
+ captain_in_right: bool = self._read_game_state_value_for(1381) == 1
+ captain_inflated: bool = self._read_game_state_value_for(1378) == 1
+
+ left_inflated: bool = (raft_in_left and raft_inflated) or (captain_in_left and captain_inflated)
+
+ right_inflated: bool = (raft_in_right and raft_inflated) or (
+ captain_in_right and captain_inflated
+ )
+
+ if left_inflated:
+ self._write_game_flags_value_for(1425, 2)
+ else:
+ self._write_game_flags_value_for(1425, 0)
+
+ if right_inflated:
+ self._write_game_flags_value_for(1426, 2)
+ else:
+ self._write_game_flags_value_for(1426, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE:
+ if self.game_state_manager.game_location == "uc3e":
+ if self._read_game_state_value_for(13060) == 0:
+ self._write_game_flags_value_for(13106, 0)
+ else:
+ self._write_game_flags_value_for(13106, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS:
+ if self.game_state_manager.game_location == "ue1e":
+ if self._read_game_state_value_for(14318) == 0:
+ self._write_game_flags_value_for(13219, 0)
+ self._write_game_flags_value_for(13220, 0)
+ self._write_game_flags_value_for(13221, 0)
+ self._write_game_flags_value_for(13222, 0)
+ else:
+ self._write_game_flags_value_for(13219, 2)
+ self._write_game_flags_value_for(13220, 2)
+ self._write_game_flags_value_for(13221, 2)
+ self._write_game_flags_value_for(13222, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS:
+ if self.game_state_manager.game_location == "ue1e":
+ if self._read_game_state_value_for(14318) == 0:
+ self._write_game_flags_value_for(14327, 0)
+ self._write_game_flags_value_for(14332, 0)
+ self._write_game_flags_value_for(14337, 0)
+ self._write_game_flags_value_for(14342, 0)
+ else:
+ self._write_game_flags_value_for(14327, 2)
+ self._write_game_flags_value_for(14332, 2)
+ self._write_game_flags_value_for(14337, 2)
+ self._write_game_flags_value_for(14342, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT:
+ if self.game_state_manager.game_location == "tr5e":
+ self._write_game_flags_value_for(12528, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS:
+ if self.game_state_manager.game_location == "tr5e":
+ if self._read_game_state_value_for(12220) == 0:
+ self._write_game_flags_value_for(12523, 2)
+ self._write_game_flags_value_for(12524, 2)
+ self._write_game_flags_value_for(12525, 2)
+ else:
+ self._write_game_flags_value_for(12523, 0)
+ self._write_game_flags_value_for(12524, 0)
+ self._write_game_flags_value_for(12525, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_GLASS_CASE:
+ if self.game_state_manager.game_location == "uc1g":
+ if self._read_game_state_value_for(12931) == 1 or self._read_game_state_value_for(12929) == 1:
+ self._write_game_flags_value_for(13002, 2)
+ else:
+ self._write_game_flags_value_for(13002, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL:
+ if self.game_state_manager.game_location == "pe5e":
+ if self._read_game_state_value_for(10277) == 0:
+ self._write_game_flags_value_for(10726, 0)
+ else:
+ self._write_game_flags_value_for(10726, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_DOOR:
+ if self.game_state_manager.game_location == "tr1k":
+ if self._read_game_state_value_for(12212) == 0:
+ self._write_game_flags_value_for(12280, 0)
+ else:
+ self._write_game_flags_value_for(12280, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_GRASS:
+ if self.game_state_manager.game_location in ("te10", "te1g", "te20", "te30", "te40"):
+ key: int
+ for key in data.statemap_keys:
+ self._write_game_flags_value_for(key, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS:
+ if self.game_state_manager.game_location == "hp1e":
+ if self._read_game_state_value_for(8431) == 1:
+ key: int
+ for key in data.statemap_keys:
+ self._write_game_flags_value_for(key, 0)
+ else:
+ key: int
+ for key in data.statemap_keys:
+ self._write_game_flags_value_for(key, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER:
+ if self.game_state_manager.game_location == "hp1e":
+ if self._read_game_state_value_for(8431) == 1:
+ self._write_game_flags_value_for(8446, 2)
+ else:
+ self._write_game_flags_value_for(8446, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_HARRY:
+ if self.game_state_manager.game_location == "dg4e":
+ if self._read_game_state_value_for(4237) == 1 and self._read_game_state_value_for(4034) == 1:
+ self._write_game_flags_value_for(4260, 2)
+ else:
+ self._write_game_flags_value_for(4260, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY:
+ if self.game_state_manager.game_location == "dg4h":
+ if self._read_game_state_value_for(4279) == 1:
+ self._write_game_flags_value_for(18026, 2)
+ else:
+ self._write_game_flags_value_for(18026, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH:
+ if self.game_state_manager.game_location == "dg4g":
+ if self._read_game_state_value_for(4034) == 1:
+ self._write_game_flags_value_for(17623, 2)
+ else:
+ self._write_game_flags_value_for(17623, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR:
+ if self.game_state_manager.game_location == "uc4e":
+ if self._read_game_state_value_for(13062) == 1:
+ self._write_game_flags_value_for(13140, 2)
+ else:
+ self._write_game_flags_value_for(13140, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR:
+ if self.game_state_manager.game_location == "pe1e":
+ if self._read_game_state_value_for(10451) == 1:
+ self._write_game_flags_value_for(10441, 2)
+ else:
+ self._write_game_flags_value_for(10441, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_LOUDSPEAKER_VOLUME_BUTTONS:
+ if self.game_state_manager.game_location == "pe2j":
+ self._write_game_flags_value_for(19632, 0)
+ self._write_game_flags_value_for(19627, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_DOOR:
+ if self.game_state_manager.game_location == "sw4e":
+ if self._read_game_state_value_for(2989) == 1:
+ self._write_game_flags_value_for(3025, 2)
+ else:
+ self._write_game_flags_value_for(3025, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG:
+ if self.game_state_manager.game_location == "sw4e":
+ self._write_game_flags_value_for(3036, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_MIRROR:
+ if self.game_state_manager.game_location == "dw1f":
+ self._write_game_flags_value_for(5031, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_MONASTERY_VENT:
+ if self.game_state_manager.game_location == "um1e":
+ if self._read_game_state_value_for(9637) == 0:
+ self._write_game_flags_value_for(13597, 0)
+ else:
+ self._write_game_flags_value_for(13597, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_MOSSY_GRATE:
+ if self.game_state_manager.game_location == "ue2g":
+ if self._read_game_state_value_for(13278) == 0:
+ self._write_game_flags_value_for(13390, 0)
+ else:
+ self._write_game_flags_value_for(13390, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR:
+ if self.game_state_manager.game_location == "qe1e":
+ if self._player_is_brog():
+ self._write_game_flags_value_for(2447, 0)
+ elif self._player_is_griff():
+ self._write_game_flags_value_for(2455, 0)
+ elif self._player_is_lucy():
+ if self._read_game_state_value_for(2457) == 0:
+ self._write_game_flags_value_for(2455, 0)
+ else:
+ self._write_game_flags_value_for(2455, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS:
+ if self.game_state_manager.game_location == "tr3h":
+ if self._read_game_state_value_for(11777) == 1:
+ self._write_game_flags_value_for(12389, 2)
+ else:
+ self._write_game_flags_value_for(12389, 0)
+
+ self._write_game_state_value_for(12390, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE:
+ if self.game_state_manager.game_location == "dg4f":
+ if self._read_game_state_value_for(4241) == 1:
+ self._write_game_flags_value_for(4302, 2)
+ else:
+ self._write_game_flags_value_for(4302, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE:
+ if self.game_state_manager.game_location == "tp1e":
+ if self._read_game_state_value_for(16342) == 1:
+ self._write_game_flags_value_for(16383, 2)
+ self._write_game_flags_value_for(16384, 2)
+ else:
+ self._write_game_flags_value_for(16383, 0)
+ self._write_game_flags_value_for(16384, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE:
+ if self.game_state_manager.game_location == "sg6e":
+ if self._read_game_state_value_for(15715) == 1:
+ self._write_game_flags_value_for(2769, 2)
+ else:
+ self._write_game_flags_value_for(2769, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON:
+ if self.game_state_manager.game_location == "dg2f":
+ if self._read_game_state_value_for(4114) == 1 or self._read_game_state_value_for(4115) == 1:
+ self._write_game_flags_value_for(4149, 2)
+ else:
+ self._write_game_flags_value_for(4149, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_BUTTONS:
+ if self.game_state_manager.game_location == "tr5f":
+ self._write_game_flags_value_for(12584, 0)
+ self._write_game_flags_value_for(12585, 0)
+ self._write_game_flags_value_for(12586, 0)
+ self._write_game_flags_value_for(12587, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_COIN_SLOT:
+ if self.game_state_manager.game_location == "tr5f":
+ self._write_game_flags_value_for(12574, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_SOUVENIR_COIN_SLOT:
+ if self.game_state_manager.game_location == "ue2j":
+ if self._read_game_state_value_for(13408) == 1:
+ self._write_game_flags_value_for(13412, 2)
+ else:
+ self._write_game_flags_value_for(13412, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER:
+ if self.game_state_manager.game_location == "tp4g":
+ self._write_game_flags_value_for(12170, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM:
+ if self.game_state_manager.game_location == "tp1e":
+ if self._read_game_state_value_for(16342) == 1 and self._read_game_state_value_for(16374) == 0:
+ self._write_game_flags_value_for(16382, 0)
+ else:
+ self._write_game_flags_value_for(16382, 2)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM:
+ if self.game_state_manager.game_location == "dg3e":
+ self._write_game_flags_value_for(4209, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_STUDENT_ID_MACHINE:
+ if self.game_state_manager.game_location == "th3r":
+ self._write_game_flags_value_for(11973, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT:
+ if self.game_state_manager.game_location == "uc6e":
+ self._write_game_flags_value_for(13168, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY:
+ if self.game_state_manager.game_location == "qb2e":
+ if self._read_game_state_value_for(15395) == 1:
+ self._write_game_flags_value_for(15396, 2)
+ else:
+ self._write_game_flags_value_for(15396, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH:
+ if self.game_state_manager.game_location == "mt2e":
+ self._write_game_flags_value_for(9706, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS:
+ if self.game_state_manager.game_location == "mt2g":
+ self._write_game_flags_value_for(9728, 0)
+ self._write_game_flags_value_for(9729, 0)
+ self._write_game_flags_value_for(9730, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_WELL:
+ if self.game_state_manager.game_location == "pc1e":
+ self._write_game_flags_value_for(10314, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM:
+ if self.game_state_manager.game_location == "us2e":
+ self._write_game_flags_value_for(13757, 0)
+ elif self.game_state_manager.game_location == "ue2e":
+ self._write_game_flags_value_for(13297, 0)
+ elif self.game_state_manager.game_location == "uh2e":
+ self._write_game_flags_value_for(13486, 0)
+ elif self.game_state_manager.game_location == "um2e":
+ self._write_game_flags_value_for(13625, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES:
+ if self.game_state_manager.game_location == "us2e":
+ self._write_game_flags_value_for(13758, 0)
+ elif self.game_state_manager.game_location == "ue2e":
+ self._write_game_flags_value_for(13309, 0)
+ elif self.game_state_manager.game_location == "uh2e":
+ self._write_game_flags_value_for(13498, 0)
+ elif self.game_state_manager.game_location == "um2e":
+ self._write_game_flags_value_for(13637, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY:
+ if self.game_state_manager.game_location == "us2e":
+ self._write_game_flags_value_for(13759, 0)
+ elif self.game_state_manager.game_location == "ue2e":
+ self._write_game_flags_value_for(13316, 0)
+ elif self.game_state_manager.game_location == "uh2e":
+ self._write_game_flags_value_for(13505, 0)
+ elif self.game_state_manager.game_location == "um2e":
+ self._write_game_flags_value_for(13644, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION:
+ if self.game_state_manager.game_location == "mt1f":
+ self._write_game_flags_value_for(9660, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY:
+ if self.game_state_manager.game_location == "mt1f":
+ self._write_game_flags_value_for(9666, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL:
+ if self.game_state_manager.game_location == "mt1f":
+ self._write_game_flags_value_for(9668, 0)
+ elif hotspot_item == ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ:
+ if self.game_state_manager.game_location == "mt1f":
+ self._write_game_flags_value_for(9662, 0)
+
+ def _manage_items(self) -> None:
+ if self._player_is_afgncaap():
+ self.available_inventory_slots = self._determine_available_inventory_slots()
+
+ received_inventory_items: Set[ZorkGrandInquisitorItems]
+ received_inventory_items = self.received_items & self.possible_inventory_items
+
+ received_inventory_items = self._filter_received_inventory_items(received_inventory_items)
+ elif self._player_is_totem():
+ self.available_inventory_slots = self._determine_available_inventory_slots(is_totem=True)
+
+ received_inventory_items: Set[ZorkGrandInquisitorItems]
+
+ if self._player_is_brog():
+ received_inventory_items = self.received_items & self.brog_items
+ received_inventory_items = self._filter_received_brog_inventory_items(received_inventory_items)
+ elif self._player_is_griff():
+ received_inventory_items = self.received_items & self.griff_items
+ received_inventory_items = self._filter_received_griff_inventory_items(received_inventory_items)
+ elif self._player_is_lucy():
+ received_inventory_items = self.received_items & self.lucy_items
+ received_inventory_items = self._filter_received_lucy_inventory_items(received_inventory_items)
+ else:
+ return None
+ else:
+ return None
+
+ game_state_inventory_items: Set[ZorkGrandInquisitorItems] = self._determine_game_state_inventory()
+
+ inventory_items_to_remove: Set[ZorkGrandInquisitorItems]
+ inventory_items_to_remove = game_state_inventory_items - received_inventory_items
+
+ inventory_items_to_add: Set[ZorkGrandInquisitorItems]
+ inventory_items_to_add = received_inventory_items - game_state_inventory_items
+
+ item: ZorkGrandInquisitorItems
+ for item in inventory_items_to_remove:
+ self._remove_from_inventory(item)
+
+ item: ZorkGrandInquisitorItems
+ for item in inventory_items_to_add:
+ self._add_to_inventory(item)
+
+ # Item Deduplication (Just in Case)
+ seen_items: Set[int] = set()
+
+ i: int
+ for i in range(151, 171):
+ item: int = self._read_game_state_value_for(i)
+
+ if item in seen_items:
+ self._write_game_state_value_for(i, 0)
+ else:
+ seen_items.add(item)
+
+ def _apply_conditional_teleports(self) -> None:
+ if self._player_is_at("uw1x"):
+ self.game_state_manager.set_game_location("uw10", 0)
+
+ if self._player_is_at("uw1k") and self._read_game_state_value_for(13938) == 0:
+ self.game_state_manager.set_game_location("pc10", 250)
+
+ if self._player_is_at("ue1q"):
+ self.game_state_manager.set_game_location("ue1e", 0)
+
+ if self._player_is_at("ej10"):
+ self.game_state_manager.set_game_location("uc10", 1200)
+
+ if self._read_game_state_value_for(9) == 224:
+ self._write_game_state_value_for(9, 0)
+ self.game_state_manager.set_game_location("uc10", 1200)
+
+ def _check_for_victory(self) -> None:
+ if self.option_goal == ZorkGrandInquisitorGoals.THREE_ARTIFACTS:
+ coconut_is_placed = self._read_game_state_value_for(2200) == 1
+ cube_is_placed = self._read_game_state_value_for(2322) == 1
+ skull_is_placed = self._read_game_state_value_for(2321) == 1
+
+ self.goal_completed = coconut_is_placed and cube_is_placed and skull_is_placed
+
+ def _determine_game_state_inventory(self) -> Set[ZorkGrandInquisitorItems]:
+ game_state_inventory: Set[ZorkGrandInquisitorItems] = set()
+
+ # Item on Cursor
+ item_on_cursor: int = self._read_game_state_value_for(9)
+
+ if item_on_cursor != 0:
+ if item_on_cursor in self.game_id_to_items:
+ game_state_inventory.add(self.game_id_to_items[item_on_cursor])
+
+ # Item in Inspector
+ item_in_inspector: int = self._read_game_state_value_for(4512)
+
+ if item_in_inspector != 0:
+ if item_in_inspector in self.game_id_to_items:
+ game_state_inventory.add(self.game_id_to_items[item_in_inspector])
+
+ # Items in Inventory Slots
+ i: int
+ for i in range(151, 171):
+ if self._read_game_state_value_for(i) != 0:
+ if self._read_game_state_value_for(i) in self.game_id_to_items:
+ game_state_inventory.add(
+ self.game_id_to_items[self._read_game_state_value_for(i)]
+ )
+
+ # Pouch of Zorkmids
+ if self._read_game_state_value_for(5827) == 1:
+ game_state_inventory.add(ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS)
+
+ # Spells
+ i: int
+ for i in range(191, 203):
+ if self._read_game_state_value_for(i) == 1:
+ if i in self.game_id_to_items:
+ game_state_inventory.add(self.game_id_to_items[i])
+
+ # Totems
+ if self._read_game_state_value_for(4853) == 1:
+ game_state_inventory.add(ZorkGrandInquisitorItems.TOTEM_BROG)
+
+ if self._read_game_state_value_for(4315) == 1:
+ game_state_inventory.add(ZorkGrandInquisitorItems.TOTEM_GRIFF)
+
+ if self._read_game_state_value_for(5223) == 1:
+ game_state_inventory.add(ZorkGrandInquisitorItems.TOTEM_LUCY)
+
+ return game_state_inventory
+
+ def _add_to_inventory(self, item: ZorkGrandInquisitorItems) -> None:
+ if item == ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS:
+ return None
+
+ data: ZorkGrandInquisitorItemData = item_data[item]
+
+ if ZorkGrandInquisitorTags.INVENTORY_ITEM in data.tags:
+ if len(self.available_inventory_slots): # Inventory slot overflow protection
+ inventory_slot: int = self.available_inventory_slots.pop()
+ self._write_game_state_value_for(inventory_slot, data.statemap_keys[0])
+ elif ZorkGrandInquisitorTags.SPELL in data.tags:
+ self._write_game_state_value_for(data.statemap_keys[0], 1)
+ elif ZorkGrandInquisitorTags.TOTEM in data.tags:
+ self._write_game_state_value_for(data.statemap_keys[0], 1)
+
+ def _remove_from_inventory(self, item: ZorkGrandInquisitorItems) -> None:
+ if item == ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS:
+ return None
+
+ data: ZorkGrandInquisitorItemData = item_data[item]
+
+ if ZorkGrandInquisitorTags.INVENTORY_ITEM in data.tags:
+ inventory_slot: Optional[int] = self._inventory_slot_for(item)
+
+ if inventory_slot is None:
+ return None
+
+ self._write_game_state_value_for(inventory_slot, 0)
+
+ if inventory_slot != 9:
+ self.available_inventory_slots.add(inventory_slot)
+ elif ZorkGrandInquisitorTags.SPELL in data.tags:
+ self._write_game_state_value_for(data.statemap_keys[0], 0)
+ elif ZorkGrandInquisitorTags.TOTEM in data.tags:
+ self._write_game_state_value_for(data.statemap_keys[0], 0)
+
+ def _determine_available_inventory_slots(self, is_totem: bool = False) -> Set[int]:
+ available_inventory_slots: Set[int] = set()
+
+ inventory_slot_range_end: int = 171
+
+ if is_totem:
+ if self._player_is_brog():
+ inventory_slot_range_end = 161
+ elif self._player_is_griff():
+ inventory_slot_range_end = 160
+ elif self._player_is_lucy():
+ inventory_slot_range_end = 157
+
+ i: int
+ for i in range(151, inventory_slot_range_end):
+ if self._read_game_state_value_for(i) == 0:
+ available_inventory_slots.add(i)
+
+ return available_inventory_slots
+
+ def _inventory_slot_for(self, item) -> Optional[int]:
+ data: ZorkGrandInquisitorItemData = item_data[item]
+
+ if ZorkGrandInquisitorTags.INVENTORY_ITEM in data.tags:
+ i: int
+ for i in range(151, 171):
+ if self._read_game_state_value_for(i) == data.statemap_keys[0]:
+ return i
+
+ if self._read_game_state_value_for(9) == data.statemap_keys[0]:
+ return 9
+
+ if self._read_game_state_value_for(4512) == data.statemap_keys[0]:
+ return 4512
+
+ return None
+
+ def _filter_received_inventory_items(
+ self, received_inventory_items: Set[ZorkGrandInquisitorItems]
+ ) -> Set[ZorkGrandInquisitorItems]:
+ to_filter_inventory_items: Set[ZorkGrandInquisitorItems] = self.totem_items
+
+ inventory_item_values: Set[int] = set()
+
+ i: int
+ for i in range(151, 171):
+ inventory_item_values.add(self._read_game_state_value_for(i))
+
+ cursor_item_value: int = self._read_game_state_value_for(9)
+ inspector_item_value: int = self._read_game_state_value_for(4512)
+
+ inventory_item_values.add(cursor_item_value)
+ inventory_item_values.add(inspector_item_value)
+
+ item: ZorkGrandInquisitorItems
+ for item in received_inventory_items:
+ if item == ZorkGrandInquisitorItems.FLATHEADIA_FUDGE:
+ if self._read_game_state_value_for(4766) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(4869) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.HUNGUS_LARD:
+ if self._read_game_state_value_for(4870) == 1:
+ to_filter_inventory_items.add(item)
+ elif (
+ self._read_game_state_value_for(4244) == 1
+ and self._read_game_state_value_for(4309) == 0
+ ):
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.JAR_OF_HOTBUGS:
+ if self._read_game_state_value_for(4750) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(4869) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.LANTERN:
+ if self._read_game_state_value_for(10477) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(5221) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.LARGE_TELEGRAPH_HAMMER:
+ if self._read_game_state_value_for(9491) == 3:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.MAP:
+ if self._read_game_state_value_for(16618) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.MEAD_LIGHT:
+ if 105 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(17620) > 0:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(4034) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.MOSS_OF_MAREILON:
+ if self._read_game_state_value_for(4763) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(4869) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.MUG:
+ if self._read_game_state_value_for(4772) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(4869) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.OLD_SCRATCH_CARD:
+ if 32 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(12892) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.PERMA_SUCK_MACHINE:
+ if self._read_game_state_value_for(12218) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.PLASTIC_SIX_PACK_HOLDER:
+ if self._read_game_state_value_for(15150) == 3:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(10421) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.PROZORK_TABLET:
+ if self._read_game_state_value_for(4115) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.QUELBEE_HONEYCOMB:
+ if self._read_game_state_value_for(4769) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(4869) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.ROPE:
+ if 22 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif 111 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif (
+ self._read_game_state_value_for(10304) == 1
+ and not self._read_game_state_value_for(13938) == 1
+ ):
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15150) == 83:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.SCROLL_FRAGMENT_ANS:
+ if 41 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif 98 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(201) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.SCROLL_FRAGMENT_GIV:
+ if 48 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif 98 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(201) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.SNAPDRAGON:
+ if self._read_game_state_value_for(4199) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.STUDENT_ID:
+ if self._read_game_state_value_for(11838) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.SUBWAY_TOKEN:
+ if self._read_game_state_value_for(13167) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.SWORD:
+ if 22 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif 100 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif 111 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.ZIMDOR_SCROLL:
+ if 105 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(17620) == 3:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(4034) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.ZORK_ROCKS:
+ if self._read_game_state_value_for(12486) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(12487) == 1:
+ to_filter_inventory_items.add(item)
+ elif 52 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(11769) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(11840) == 1:
+ to_filter_inventory_items.add(item)
+
+ return received_inventory_items - to_filter_inventory_items
+
+ def _filter_received_brog_inventory_items(
+ self, received_inventory_items: Set[ZorkGrandInquisitorItems]
+ ) -> Set[ZorkGrandInquisitorItems]:
+ to_filter_inventory_items: Set[ZorkGrandInquisitorItems] = set()
+
+ inventory_item_values: Set[int] = set()
+
+ i: int
+ for i in range(151, 161):
+ inventory_item_values.add(self._read_game_state_value_for(i))
+
+ cursor_item_value: int = self._read_game_state_value_for(9)
+ inspector_item_value: int = self._read_game_state_value_for(2194)
+
+ inventory_item_values.add(cursor_item_value)
+ inventory_item_values.add(inspector_item_value)
+
+ item: ZorkGrandInquisitorItems
+ for item in received_inventory_items:
+ if item == ZorkGrandInquisitorItems.BROGS_BICKERING_TORCH:
+ if 103 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH:
+ if 104 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.BROGS_GRUE_EGG:
+ if self._read_game_state_value_for(2577) == 1:
+ to_filter_inventory_items.add(item)
+ elif 71 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(2641) == 1:
+ to_filter_inventory_items.add(item)
+
+ return received_inventory_items - to_filter_inventory_items
+
+ def _filter_received_griff_inventory_items(
+ self, received_inventory_items: Set[ZorkGrandInquisitorItems]
+ ) -> Set[ZorkGrandInquisitorItems]:
+ to_filter_inventory_items: Set[ZorkGrandInquisitorItems] = set()
+
+ inventory_item_values: Set[int] = set()
+
+ i: int
+ for i in range(151, 160):
+ inventory_item_values.add(self._read_game_state_value_for(i))
+
+ cursor_item_value: int = self._read_game_state_value_for(9)
+ inspector_item_value: int = self._read_game_state_value_for(4512)
+
+ inventory_item_values.add(cursor_item_value)
+ inventory_item_values.add(inspector_item_value)
+
+ item: ZorkGrandInquisitorItems
+ for item in received_inventory_items:
+ if item == ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT:
+ if self._read_game_state_value_for(1301) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(1304) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(16562) == 1:
+ to_filter_inventory_items.add(item)
+ if item == ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN:
+ if self._read_game_state_value_for(1374) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(1381) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(16562) == 1:
+ to_filter_inventory_items.add(item)
+
+ return received_inventory_items - to_filter_inventory_items
+
+ def _filter_received_lucy_inventory_items(
+ self, received_inventory_items: Set[ZorkGrandInquisitorItems]
+ ) -> Set[ZorkGrandInquisitorItems]:
+ to_filter_inventory_items: Set[ZorkGrandInquisitorItems] = set()
+
+ inventory_item_values: Set[int] = set()
+
+ i: int
+ for i in range(151, 157):
+ inventory_item_values.add(self._read_game_state_value_for(i))
+
+ cursor_item_value: int = self._read_game_state_value_for(9)
+ inspector_item_value: int = self._read_game_state_value_for(2198)
+
+ inventory_item_values.add(cursor_item_value)
+ inventory_item_values.add(inspector_item_value)
+
+ item: ZorkGrandInquisitorItems
+ for item in received_inventory_items:
+ if item == ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1:
+ if 120 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15433) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15435) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15437) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15439) == 1:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15472) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2:
+ if 121 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15433) == 2:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15435) == 2:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15437) == 2:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15439) == 2:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15472) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3:
+ if 122 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15433) == 3:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15435) == 3:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15437) == 3:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15439) == 3:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15472) == 1:
+ to_filter_inventory_items.add(item)
+ elif item == ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4:
+ if 123 in inventory_item_values:
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15433) in (4, 5):
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15435) in (4, 5):
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15437) in (4, 5):
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15439) in (4, 5):
+ to_filter_inventory_items.add(item)
+ elif self._read_game_state_value_for(15472) == 1:
+ to_filter_inventory_items.add(item)
+
+ return received_inventory_items - to_filter_inventory_items
+
+ def _read_game_state_value_for(self, key: int) -> Optional[int]:
+ try:
+ return self.game_state_manager.read_game_state_value_for(key)
+ except Exception as e:
+ self.log_debug(f"Exception: {e} while trying to read game state key '{key}'")
+ raise e
+
+ def _write_game_state_value_for(self, key: int, value: int) -> Optional[bool]:
+ try:
+ return self.game_state_manager.write_game_state_value_for(key, value)
+ except Exception as e:
+ self.log_debug(f"Exception: {e} while trying to write '{key} = {value}' to game state")
+ raise e
+
+ def _read_game_flags_value_for(self, key: int) -> Optional[int]:
+ try:
+ return self.game_state_manager.read_game_flags_value_for(key)
+ except Exception as e:
+ self.log_debug(f"Exception: {e} while trying to read game flags key '{key}'")
+ raise e
+
+ def _write_game_flags_value_for(self, key: int, value: int) -> Optional[bool]:
+ try:
+ return self.game_state_manager.write_game_flags_value_for(key, value)
+ except Exception as e:
+ self.log_debug(f"Exception: {e} while trying to write '{key} = {value}' to game flags")
+ raise e
+
+ def _player_has(self, item: ZorkGrandInquisitorItems) -> bool:
+ return item in self.received_items
+
+ def _player_doesnt_have(self, item: ZorkGrandInquisitorItems) -> bool:
+ return item not in self.received_items
+
+ def _player_is_at(self, game_location: str) -> bool:
+ return self.game_state_manager.game_location == game_location
+
+ def _player_is_afgncaap(self) -> bool:
+ return self._read_game_state_value_for(1596) == 1
+
+ def _player_is_totem(self) -> bool:
+ return self._player_is_brog() or self._player_is_griff() or self._player_is_lucy()
+
+ def _player_is_brog(self) -> bool:
+ return self._read_game_state_value_for(1520) == 1
+
+ def _player_is_griff(self) -> bool:
+ return self._read_game_state_value_for(1296) == 1
+
+ def _player_is_lucy(self) -> bool:
+ return self._read_game_state_value_for(1524) == 1
diff --git a/worlds/zork_grand_inquisitor/game_state_manager.py b/worlds/zork_grand_inquisitor/game_state_manager.py
new file mode 100644
index 000000000000..25b35969bf5e
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/game_state_manager.py
@@ -0,0 +1,370 @@
+from typing import Optional, Tuple
+
+from pymem import Pymem
+from pymem.process import close_handle
+
+
+class GameStateManager:
+ process_name = "scummvm.exe"
+
+ process: Optional[Pymem]
+ is_process_running: bool
+
+ script_manager_struct_address: int
+ render_manager_struct_address: int
+
+ game_location: Optional[str]
+ game_location_offset: Optional[int]
+
+ def __init__(self) -> None:
+ self.process = None
+ self.is_process_running = False
+
+ self.script_manager_struct_address = 0x0
+ self.render_manager_struct_address = 0x0
+
+ self.game_location = None
+ self.game_location_offset = None
+
+ @property
+ def game_state_storage_pointer_address(self) -> int:
+ return self.script_manager_struct_address + 0x88
+
+ @property
+ def game_state_storage_address(self) -> int:
+ return self.process.read_longlong(self.game_state_storage_pointer_address)
+
+ @property
+ def game_state_hashmap_size_address(self) -> int:
+ return self.script_manager_struct_address + 0x90
+
+ @property
+ def game_state_key_count_address(self) -> int:
+ return self.script_manager_struct_address + 0x94
+
+ @property
+ def game_state_deleted_key_count_address(self) -> int:
+ return self.script_manager_struct_address + 0x98
+
+ @property
+ def game_flags_storage_pointer_address(self) -> int:
+ return self.script_manager_struct_address + 0x120
+
+ @property
+ def game_flags_storage_address(self) -> int:
+ return self.process.read_longlong(self.game_flags_storage_pointer_address)
+
+ @property
+ def game_flags_hashmap_size_address(self) -> int:
+ return self.script_manager_struct_address + 0x128
+
+ @property
+ def game_flags_key_count_address(self) -> int:
+ return self.script_manager_struct_address + 0x12C
+
+ @property
+ def game_flags_deleted_key_count_address(self) -> int:
+ return self.script_manager_struct_address + 0x130
+
+ @property
+ def current_location_address(self) -> int:
+ return self.script_manager_struct_address + 0x400
+
+ @property
+ def current_location_offset_address(self) -> int:
+ return self.script_manager_struct_address + 0x404
+
+ @property
+ def next_location_address(self) -> int:
+ return self.script_manager_struct_address + 0x408
+
+ @property
+ def next_location_offset_address(self) -> int:
+ return self.script_manager_struct_address + 0x40C
+
+ @property
+ def panorama_reversed_address(self) -> int:
+ return self.render_manager_struct_address + 0x1C
+
+ def open_process_handle(self) -> bool:
+ try:
+ self.process = Pymem(self.process_name)
+ self.is_process_running = True
+
+ self.script_manager_struct_address = self._resolve_address(0x5276600, (0xC8, 0x0))
+ self.render_manager_struct_address = self._resolve_address(0x5276600, (0xD0, 0x120))
+ except Exception:
+ return False
+
+ return True
+
+ def close_process_handle(self) -> bool:
+ if close_handle(self.process.process_handle):
+ self.is_process_running = False
+ self.process = None
+
+ self.script_manager_struct_address = 0x0
+ self.render_manager_struct_address = 0x0
+
+ return True
+
+ return False
+
+ def is_process_still_running(self) -> bool:
+ try:
+ self.process.read_int(self.process.base_address)
+ except Exception:
+ self.is_process_running = False
+ self.process = None
+
+ self.script_manager_struct_address = 0x0
+ self.render_manager_struct_address = 0x0
+
+ return False
+
+ return True
+
+ def read_game_state_value_for(self, key: int) -> Optional[int]:
+ return self.read_statemap_value_for(key, scope="game_state")
+
+ def read_game_flags_value_for(self, key: int) -> Optional[int]:
+ return self.read_statemap_value_for(key, scope="game_flags")
+
+ def read_statemap_value_for(self, key: int, scope: str = "game_state") -> Optional[int]:
+ if self.is_process_running:
+ offset: int
+
+ address: int
+ address_value: int
+
+ if scope == "game_state":
+ offset = self._get_game_state_address_read_offset_for(key)
+
+ address = self.game_state_storage_address + offset
+ address_value = self.process.read_longlong(address)
+ elif scope == "game_flags":
+ offset = self._get_game_flags_address_read_offset_for(key)
+
+ address = self.game_flags_storage_address + offset
+ address_value = self.process.read_longlong(address)
+ else:
+ raise ValueError(f"Invalid scope: {scope}")
+
+ if address_value == 0:
+ return 0
+
+ statemap_value: int = self.process.read_int(address_value + 0x0)
+ statemap_key: int = self.process.read_int(address_value + 0x4)
+
+ assert statemap_key == key
+
+ return statemap_value
+
+ return None
+
+ def write_game_state_value_for(self, key: int, value: int) -> Optional[bool]:
+ return self.write_statemap_value_for(key, value, scope="game_state")
+
+ def write_game_flags_value_for(self, key: int, value: int) -> Optional[bool]:
+ return self.write_statemap_value_for(key, value, scope="game_flags")
+
+ def write_statemap_value_for(self, key: int, value: int, scope: str = "game_state") -> Optional[bool]:
+ if self.is_process_running:
+ offset: int
+ is_existing_node: bool
+ is_reused_dummy_node: bool
+
+ key_count_address: int
+ deleted_key_count_address: int
+
+ storage_address: int
+
+ if scope == "game_state":
+ offset, is_existing_node, is_reused_dummy_node = self._get_game_state_address_write_offset_for(key)
+
+ key_count_address = self.game_state_key_count_address
+ deleted_key_count_address = self.game_state_deleted_key_count_address
+
+ storage_address = self.game_state_storage_address
+ elif scope == "game_flags":
+ offset, is_existing_node, is_reused_dummy_node = self._get_game_flags_address_write_offset_for(key)
+
+ key_count_address = self.game_flags_key_count_address
+ deleted_key_count_address = self.game_flags_deleted_key_count_address
+
+ storage_address = self.game_flags_storage_address
+ else:
+ raise ValueError(f"Invalid scope: {scope}")
+
+ statemap_key_count: int = self.process.read_int(key_count_address)
+ statemap_deleted_key_count: int = self.process.read_int(deleted_key_count_address)
+
+ if value == 0:
+ if not is_existing_node:
+ return False
+
+ self.process.write_longlong(storage_address + offset, 1)
+
+ self.process.write_int(key_count_address, statemap_key_count - 1)
+ self.process.write_int(deleted_key_count_address, statemap_deleted_key_count + 1)
+ else:
+ if is_existing_node:
+ address_value: int = self.process.read_longlong(storage_address + offset)
+ self.process.write_int(address_value + 0x0, value)
+ else:
+ write_address: int = self.process.allocate(0x8)
+
+ self.process.write_int(write_address + 0x0, value)
+ self.process.write_int(write_address + 0x4, key)
+
+ self.process.write_longlong(storage_address + offset, write_address)
+
+ self.process.write_int(key_count_address, statemap_key_count + 1)
+
+ if is_reused_dummy_node:
+ self.process.write_int(deleted_key_count_address, statemap_deleted_key_count - 1)
+
+ return True
+
+ return None
+
+ def refresh_game_location(self) -> Optional[bool]:
+ if self.is_process_running:
+ game_location_bytes: bytes = self.process.read_bytes(self.current_location_address, 4)
+
+ self.game_location = game_location_bytes.decode("ascii")
+ self.game_location_offset = self.process.read_int(self.current_location_offset_address)
+
+ return True
+
+ return None
+
+ def set_game_location(self, game_location: str, offset: int) -> Optional[bool]:
+ if self.is_process_running:
+ game_location_bytes: bytes = game_location.encode("ascii")
+
+ self.process.write_bytes(self.next_location_address, game_location_bytes, 4)
+ self.process.write_int(self.next_location_offset_address, offset)
+
+ return True
+
+ return None
+
+ def set_panorama_reversed(self, is_reversed: bool) -> Optional[bool]:
+ if self.is_process_running:
+ self.process.write_int(self.panorama_reversed_address, 1 if is_reversed else 0)
+
+ return True
+
+ return None
+
+ def _resolve_address(self, base_offset: int, offsets: Tuple[int, ...]):
+ address: int = self.process.read_longlong(self.process.base_address + base_offset)
+
+ for offset in offsets[:-1]:
+ address = self.process.read_longlong(address + offset)
+
+ return address + offsets[-1]
+
+ def _get_game_state_address_read_offset_for(self, key: int):
+ return self._get_statemap_address_read_offset_for(key, scope="game_state")
+
+ def _get_game_flags_address_read_offset_for(self, key: int):
+ return self._get_statemap_address_read_offset_for(key, scope="game_flags")
+
+ def _get_statemap_address_read_offset_for(self, key: int, scope: str = "game_state") -> int:
+ hashmap_size_address: int
+ storage_address: int
+
+ if scope == "game_state":
+ hashmap_size_address = self.game_state_hashmap_size_address
+ storage_address = self.game_state_storage_address
+ elif scope == "game_flags":
+ hashmap_size_address = self.game_flags_hashmap_size_address
+ storage_address = self.game_flags_storage_address
+ else:
+ raise ValueError(f"Invalid scope: {scope}")
+
+ statemap_hashmap_size: int = self.process.read_int(hashmap_size_address)
+
+ perturb: int = key
+ perturb_shift: int = 0x5
+
+ index: int = key & statemap_hashmap_size
+ offset: int = index * 0x8
+
+ while True:
+ offset_value: int = self.process.read_longlong(storage_address + offset)
+
+ if offset_value == 0: # Null Pointer
+ break
+ elif offset_value == 1: # Dummy Node
+ pass
+ elif offset_value > 1: # Existing Node
+ if self.process.read_int(offset_value + 0x4) == key:
+ break
+
+ index = ((0x5 * index) + perturb + 0x1) & statemap_hashmap_size
+ offset = index * 0x8
+
+ perturb >>= perturb_shift
+
+ return offset
+
+ def _get_game_state_address_write_offset_for(self, key: int) -> Tuple[int, bool, bool]:
+ return self._get_statemap_address_write_offset_for(key, scope="game_state")
+
+ def _get_game_flags_address_write_offset_for(self, key: int) -> Tuple[int, bool, bool]:
+ return self._get_statemap_address_write_offset_for(key, scope="game_flags")
+
+ def _get_statemap_address_write_offset_for(self, key: int, scope: str = "game_state") -> Tuple[int, bool, bool]:
+ hashmap_size_address: int
+ storage_address: int
+
+ if scope == "game_state":
+ hashmap_size_address = self.game_state_hashmap_size_address
+ storage_address = self.game_state_storage_address
+ elif scope == "game_flags":
+ hashmap_size_address = self.game_flags_hashmap_size_address
+ storage_address = self.game_flags_storage_address
+ else:
+ raise ValueError(f"Invalid scope: {scope}")
+
+ statemap_hashmap_size: int = self.process.read_int(hashmap_size_address)
+
+ perturb: int = key
+ perturb_shift: int = 0x5
+
+ index: int = key & statemap_hashmap_size
+ offset: int = index * 0x8
+
+ node_found: bool = False
+
+ dummy_node_found: bool = False
+ dummy_node_offset: Optional[int] = None
+
+ while True:
+ offset_value: int = self.process.read_longlong(storage_address + offset)
+
+ if offset_value == 0: # Null Pointer
+ break
+ elif offset_value == 1: # Dummy Node
+ if not dummy_node_found:
+ dummy_node_offset = offset
+ dummy_node_found = True
+ elif offset_value > 1: # Existing Node
+ if self.process.read_int(offset_value + 0x4) == key:
+ node_found = True
+ break
+
+ index = ((0x5 * index) + perturb + 0x1) & statemap_hashmap_size
+ offset = index * 0x8
+
+ perturb >>= perturb_shift
+
+ if not node_found and dummy_node_found: # We should reuse the dummy node
+ return dummy_node_offset, False, True
+ elif not node_found and not dummy_node_found: # We should allocate a new node
+ return offset, False, False
+
+ return offset, True, False # We should update the existing node
diff --git a/worlds/zork_grand_inquisitor/options.py b/worlds/zork_grand_inquisitor/options.py
new file mode 100644
index 000000000000..f06415199934
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/options.py
@@ -0,0 +1,61 @@
+from dataclasses import dataclass
+
+from Options import Choice, DefaultOnToggle, PerGameCommonOptions, Toggle
+
+
+class Goal(Choice):
+ """
+ Determines the victory condition
+
+ Three Artifacts: Retrieve the three artifacts of magic and place them in the walking castle
+ """
+ display_name: str = "Goal"
+
+ default: int = 0
+ option_three_artifacts: int = 0
+
+
+class QuickPortFoozle(DefaultOnToggle):
+ """If true, the items needed to go down the well will be found in early locations for a smoother early game"""
+
+ display_name: str = "Quick Port Foozle"
+
+
+class StartWithHotspotItems(DefaultOnToggle):
+ """
+ If true, the player will be given all the hotspot items at the start of the game, effectively removing the need
+ to enable the important hotspots in the game before interacting with them. Recommended for beginners
+
+ Note: The spots these hotspot items would have occupied in the item pool will instead be filled with junk items.
+ Expect a higher volume of filler items if you enable this option
+ """
+
+ display_name: str = "Start with Hotspot Items"
+
+
+class Deathsanity(Toggle):
+ """If true, adds 16 player death locations to the world"""
+
+ display_name: str = "Deathsanity"
+
+
+class GrantMissableLocationChecks(Toggle):
+ """
+ If true, performing an irreversible action will grant the locations checks that would have become unobtainable as a
+ result of that action when you meet the item requirements
+
+ Otherwise, the player is expected to potentially have to use the save system to reach those location checks. If you
+ don't like the idea of rarely having to reload an earlier save to get a location check, make sure this option is
+ enabled
+ """
+
+ display_name: str = "Grant Missable Checks"
+
+
+@dataclass
+class ZorkGrandInquisitorOptions(PerGameCommonOptions):
+ goal: Goal
+ quick_port_foozle: QuickPortFoozle
+ start_with_hotspot_items: StartWithHotspotItems
+ deathsanity: Deathsanity
+ grant_missable_location_checks: GrantMissableLocationChecks
diff --git a/worlds/zork_grand_inquisitor/requirements.txt b/worlds/zork_grand_inquisitor/requirements.txt
new file mode 100644
index 000000000000..fe25267f6705
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/requirements.txt
@@ -0,0 +1 @@
+Pymem>=1.13.0
\ No newline at end of file
diff --git a/worlds/zork_grand_inquisitor/test/__init__.py b/worlds/zork_grand_inquisitor/test/__init__.py
new file mode 100644
index 000000000000..c8ceda43a7bf
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/test/__init__.py
@@ -0,0 +1,5 @@
+from test.bases import WorldTestBase
+
+
+class ZorkGrandInquisitorTestBase(WorldTestBase):
+ game = "Zork Grand Inquisitor"
diff --git a/worlds/zork_grand_inquisitor/test/test_access.py b/worlds/zork_grand_inquisitor/test/test_access.py
new file mode 100644
index 000000000000..63a5f8c9ab1d
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/test/test_access.py
@@ -0,0 +1,2927 @@
+from typing import List
+
+from . import ZorkGrandInquisitorTestBase
+
+from ..enums import (
+ ZorkGrandInquisitorEvents,
+ ZorkGrandInquisitorItems,
+ ZorkGrandInquisitorLocations,
+ ZorkGrandInquisitorRegions,
+)
+
+
+class AccessTestRegions(ZorkGrandInquisitorTestBase):
+ options = {
+ "start_with_hotspot_items": "false",
+ }
+
+ def test_access_crossroads_to_dm_lair_sword(self) -> None:
+ self._go_to_crossroads()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SWORD.value,
+ ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR.value))
+
+ def test_access_crossroads_to_dm_lair_teleporter(self) -> None:
+ self._go_to_crossroads()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR.value))
+
+ def test_access_crossroads_to_gue_tech(self) -> None:
+ self._go_to_crossroads()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SPELL_REZROV.value,
+ ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH.value))
+
+ def test_access_crossroads_to_gue_tech_outside(self) -> None:
+ self._go_to_crossroads()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE.value))
+
+ def test_access_crossroads_to_hades_shore(self) -> None:
+ self._go_to_crossroads()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ def test_access_crossroads_to_port_foozle(self) -> None:
+ self._go_to_crossroads()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.PORT_FOOZLE.value))
+
+ def test_access_crossroads_to_spell_lab_bridge(self) -> None:
+ self._go_to_crossroads()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE.value))
+
+ def test_access_crossroads_to_subway_crossroads(self) -> None:
+ self._go_to_crossroads()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SUBWAY_TOKEN.value,
+ ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS.value))
+
+ def test_access_crossroads_to_subway_monastery(self) -> None:
+ self._go_to_crossroads()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ def test_access_dm_lair_to_crossroads(self) -> None:
+ self._go_to_dm_lair()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.CROSSROADS.value))
+
+ def test_access_dm_lair_to_dm_lair_interior(self) -> None:
+ self._go_to_dm_lair()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY.value,
+ ZorkGrandInquisitorItems.MEAD_LIGHT.value,
+ ZorkGrandInquisitorItems.ZIMDOR_SCROLL.value,
+ ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR.value))
+
+ def test_access_dm_lair_to_gue_tech_outside(self) -> None:
+ self._go_to_dm_lair()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH.value))
+
+ def test_access_dm_lair_to_hades_shore(self) -> None:
+ self._go_to_dm_lair()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ def test_access_dm_lair_to_spell_lab_bridge(self) -> None:
+ self._go_to_dm_lair()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE.value))
+
+ def test_access_dm_lair_to_subway_monastery(self) -> None:
+ self._go_to_dm_lair()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ def test_access_dm_lair_interior_to_dm_lair(self) -> None:
+ self._go_to_dm_lair_interior()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR.value))
+
+ def test_access_dm_lair_interior_to_walking_castle(self) -> None:
+ self._go_to_dm_lair_interior()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.WALKING_CASTLE.value))
+
+ self._obtain_obidil()
+
+ self.collect_by_name(ZorkGrandInquisitorItems.HOTSPOT_BLINDS.value)
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.WALKING_CASTLE.value))
+
+ def test_access_dm_lair_interior_to_white_house(self) -> None:
+ self._go_to_dm_lair_interior()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.WHITE_HOUSE.value))
+
+ self._obtain_yastard()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR.value,
+ ZorkGrandInquisitorItems.SPELL_NARWILE.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.WHITE_HOUSE.value))
+
+ def test_access_dragon_archipelago_to_dragon_archipelago_dragon(self) -> None:
+ self._go_to_dragon_archipelago()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.TOTEM_GRIFF.value,
+ ZorkGrandInquisitorItems.HOTSPOT_DRAGON_CLAW.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON.value))
+
+ def test_access_dragon_archipelago_to_hades_beyond_gates(self) -> None:
+ self._go_to_dragon_archipelago()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_BEYOND_GATES.value))
+
+ def test_access_dragon_archipelago_dragon_to_dragon_archipelago(self) -> None:
+ self._go_to_dragon_archipelago_dragon()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO.value))
+
+ def test_access_dragon_archipelago_dragon_to_endgame(self) -> None:
+ self._go_to_dragon_archipelago_dragon()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.ENDGAME.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP.value,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT.value,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN.value,
+ ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS.value,
+ ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH.value,
+ )
+ )
+
+ self._go_to_port_foozle_past_tavern()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1.value,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2.value,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3.value,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4.value,
+ ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY.value,
+ ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS.value,
+ )
+ )
+
+ self._go_to_white_house()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.TOTEM_BROG.value,
+ ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH.value,
+ ZorkGrandInquisitorItems.BROGS_GRUE_EGG.value,
+ ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT.value,
+ ZorkGrandInquisitorItems.BROGS_PLANK.value,
+ ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.ENDGAME.value))
+
+ def test_access_gue_tech_to_crossroads(self) -> None:
+ self._go_to_gue_tech()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.CROSSROADS.value))
+
+ def test_access_gue_tech_to_gue_tech_hallway(self) -> None:
+ self._go_to_gue_tech()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SPELL_IGRAM.value,
+ ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY.value))
+
+ def test_access_gue_tech_to_gue_tech_outside(self) -> None:
+ self._go_to_gue_tech()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE.value))
+
+ self.collect_by_name(ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_DOOR.value)
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE.value))
+
+ def test_access_gue_tech_hallway_to_gue_tech(self) -> None:
+ self._go_to_gue_tech_hallway()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH.value))
+
+ def test_access_gue_tech_hallway_to_spell_lab_bridge(self) -> None:
+ self._go_to_gue_tech_hallway()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.STUDENT_ID.value,
+ ZorkGrandInquisitorItems.HOTSPOT_STUDENT_ID_MACHINE.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE.value))
+
+ def test_access_gue_tech_outside_to_crossroads(self) -> None:
+ self._go_to_gue_tech_outside()
+
+ # Direct connection requires the map but indirect connection is free
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.CROSSROADS.value))
+
+ def test_access_gue_tech_outside_to_dm_lair(self) -> None:
+ self._go_to_gue_tech_outside()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR.value))
+
+ def test_access_gue_tech_outside_to_gue_tech(self) -> None:
+ self._go_to_gue_tech_outside()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH.value))
+
+ def test_access_gue_tech_outside_to_hades_shore(self) -> None:
+ self._go_to_gue_tech_outside()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ def test_access_gue_tech_outside_to_spell_lab_bridge(self) -> None:
+ self._go_to_gue_tech_outside()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE.value))
+
+ def test_access_gue_tech_outside_to_subway_monastery(self) -> None:
+ self._go_to_gue_tech_outside()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ def test_access_hades_to_hades_beyond_gates(self) -> None:
+ self._go_to_hades()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_BEYOND_GATES.value))
+
+ self._obtain_snavig()
+
+ self.collect_by_name(ZorkGrandInquisitorItems.TOTEM_BROG.value)
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_BEYOND_GATES.value))
+
+ def test_access_hades_to_hades_shore(self) -> None:
+ self._go_to_hades()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ def test_access_hades_beyond_gates_to_dragon_archipelago(self) -> None:
+ self._go_to_hades_beyond_gates()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO.value))
+
+ self._obtain_yastard()
+
+ self.collect_by_name(ZorkGrandInquisitorItems.SPELL_NARWILE.value)
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO.value))
+
+ def test_access_hades_beyond_gates_to_hades(self) -> None:
+ self._go_to_hades_beyond_gates()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.HADES.value))
+
+ def test_access_hades_shore_to_crossroads(self) -> None:
+ self._go_to_hades_shore()
+
+ # Direct connection requires the map but indirect connection is free
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.CROSSROADS.value))
+
+ def test_access_hades_shore_to_dm_lair(self) -> None:
+ self._go_to_hades_shore()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR.value))
+
+ def test_access_hades_shore_to_gue_tech_outside(self) -> None:
+ self._go_to_hades_shore()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE.value))
+
+ def test_access_hades_shore_to_hades(self) -> None:
+ self._go_to_hades_shore()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.HADES.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER.value,
+ ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS.value,
+ ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.HADES.value))
+
+ def test_access_hades_shore_to_spell_lab_bridge(self) -> None:
+ self._go_to_hades_shore()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE.value))
+
+ def test_access_hades_shore_to_subway_crossroads(self) -> None:
+ self._go_to_hades_shore()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS.value))
+
+ def test_access_hades_shore_to_subway_flood_control_dam(self) -> None:
+ self._go_to_hades_shore()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM.value))
+
+ self.collect_by_name(ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM.value)
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM.value))
+
+ def test_access_hades_shore_to_subway_monastery(self) -> None:
+ self._go_to_hades_shore()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ self.collect_by_name(ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY.value)
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ def test_access_monastery_to_hades_shore(self) -> None:
+ self._go_to_monastery()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL.value,
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS.value,
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ def test_access_monastery_to_monastery_exhibit(self) -> None:
+ self._go_to_monastery()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION.value,
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS.value,
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT.value))
+
+ def test_access_monastery_to_subway_monastery(self) -> None:
+ self._go_to_monastery()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ def test_access_monastery_exhibit_to_monastery(self) -> None:
+ self._go_to_monastery_exhibit()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.MONASTERY.value))
+
+ def test_access_monastery_exhibit_to_port_foozle_past(self) -> None:
+ self._go_to_monastery_exhibit()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST.value))
+
+ self._obtain_yastard()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER.value,
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT.value,
+ ZorkGrandInquisitorItems.LARGE_TELEGRAPH_HAMMER.value,
+ ZorkGrandInquisitorItems.SPELL_NARWILE.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST.value))
+
+ def test_access_port_foozle_to_crossroads(self) -> None:
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.CROSSROADS.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR.value,
+ ZorkGrandInquisitorItems.LANTERN.value,
+ ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL.value,
+ ZorkGrandInquisitorItems.ROPE.value,
+ ZorkGrandInquisitorItems.HOTSPOT_WELL.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.CROSSROADS.value))
+
+ def test_access_port_foozle_to_port_foozle_jacks_shop(self) -> None:
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.PORT_FOOZLE_JACKS_SHOP.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR.value,
+ ZorkGrandInquisitorItems.LANTERN.value,
+ ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.PORT_FOOZLE_JACKS_SHOP.value))
+
+ def test_access_port_foozle_jacks_shop_to_port_foozle(self) -> None:
+ self._go_to_port_foozle_jacks_shop()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.PORT_FOOZLE.value))
+
+ def test_access_port_foozle_past_to_monastery_exhibit(self) -> None:
+ self._go_to_port_foozle_past()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT.value))
+
+ def test_access_port_foozle_past_to_port_foozle_past_tavern(self) -> None:
+ self._go_to_port_foozle_past()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.TOTEM_LUCY.value,
+ ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN.value))
+
+ def test_access_port_foozle_past_tavern_to_endgame(self) -> None:
+ self._go_to_port_foozle_past_tavern()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.ENDGAME.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1.value,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2.value,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3.value,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4.value,
+ ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY.value,
+ ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS.value,
+ )
+ )
+
+ self._go_to_dragon_archipelago_dragon()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP.value,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT.value,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN.value,
+ ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS.value,
+ ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH.value,
+ )
+ )
+
+ self._go_to_white_house()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.TOTEM_BROG.value,
+ ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH.value,
+ ZorkGrandInquisitorItems.BROGS_GRUE_EGG.value,
+ ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT.value,
+ ZorkGrandInquisitorItems.BROGS_PLANK.value,
+ ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.ENDGAME.value))
+
+ def test_access_port_foozle_past_tavern_to_port_foozle_past(self) -> None:
+ self._go_to_port_foozle_past_tavern()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST.value))
+
+ def test_access_spell_lab_to_spell_lab_bridge(self) -> None:
+ self._go_to_spell_lab()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE.value))
+
+ def test_access_spell_lab_bridge_to_crossroads(self) -> None:
+ self._go_to_spell_lab_bridge()
+
+ # Direct connection requires the map but indirect connection is free
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.CROSSROADS.value))
+
+ def test_access_spell_lab_bridge_to_dm_lair(self) -> None:
+ self._go_to_spell_lab_bridge()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR.value))
+
+ def test_access_spell_lab_bridge_to_gue_tech_outside(self) -> None:
+ self._go_to_spell_lab_bridge()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE.value))
+
+ def test_access_spell_lab_bridge_to_gue_tech_hallway(self) -> None:
+ self._go_to_spell_lab_bridge()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY.value))
+
+ def test_access_spell_lab_bridge_to_hades_shore(self) -> None:
+ self._go_to_spell_lab_bridge()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ def test_access_spell_lab_bridge_to_spell_lab(self) -> None:
+ self._go_to_spell_lab_bridge()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SPELL_LAB.value))
+
+ self._go_to_subway_flood_control_dam()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SPELL_REZROV.value,
+ ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS.value,
+ ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS.value,
+ ZorkGrandInquisitorItems.SWORD.value,
+ ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE.value,
+ ZorkGrandInquisitorItems.SPELL_GOLGATEM.value,
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SPELL_LAB.value))
+
+ def test_access_spell_lab_bridge_to_subway_monastery(self) -> None:
+ self._go_to_spell_lab_bridge()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ def test_access_subway_crossroads_to_crossroads(self) -> None:
+ self._go_to_subway_crossroads()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.CROSSROADS.value))
+
+ def test_access_subway_crossroads_to_hades_shore(self) -> None:
+ self._go_to_subway_crossroads()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SPELL_KENDALL.value,
+ ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ def test_access_subway_crossroads_to_subway_flood_control_dam(self) -> None:
+ self._go_to_subway_crossroads()
+
+ self.assertFalse(
+ self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM.value)
+ )
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SPELL_KENDALL.value,
+ ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM.value,
+ )
+ )
+
+ self.assertTrue(
+ self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM.value)
+ )
+
+ def test_access_subway_crossroads_to_subway_monastery(self) -> None:
+ self._go_to_subway_crossroads()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SPELL_KENDALL.value,
+ ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ def test_access_subway_flood_control_dam_to_hades_shore(self) -> None:
+ self._go_to_subway_flood_control_dam()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ self.collect_by_name(ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES.value)
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ def test_access_subway_flood_control_dam_to_subway_crossroads(self) -> None:
+ self._go_to_subway_flood_control_dam()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS.value))
+
+ def test_access_subway_flood_control_dam_to_subway_monastery(self) -> None:
+ self._go_to_subway_flood_control_dam()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ self.collect_by_name(ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY.value)
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_MONASTERY.value))
+
+ def test_access_subway_monastery_to_hades_shore(self) -> None:
+ self._go_to_subway_monastery()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ self.collect_by_name(ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES.value)
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.HADES_SHORE.value))
+
+ def test_access_subway_monastery_to_monastery(self) -> None:
+ self._go_to_subway_monastery()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.MONASTERY.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SWORD.value,
+ ZorkGrandInquisitorItems.SPELL_GLORF.value,
+ ZorkGrandInquisitorItems.HOTSPOT_MONASTERY_VENT.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.MONASTERY.value))
+
+ def test_access_subway_monastery_to_subway_crossroads(self) -> None:
+ self._go_to_subway_monastery()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS.value))
+
+ def test_access_subway_monastery_to_subway_flood_control_dam(self) -> None:
+ self._go_to_subway_monastery()
+
+ self.assertFalse(
+ self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM.value)
+ )
+
+ self.collect_by_name(ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM.value)
+
+ self.assertTrue(
+ self.can_reach_region(ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM.value)
+ )
+
+ def test_access_walking_castle_to_dm_lair_interior(self) -> None:
+ self._go_to_walking_castle()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR.value))
+
+ def test_access_white_house_to_dm_lair_interior(self) -> None:
+ self._go_to_white_house()
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR.value))
+
+ def test_access_white_house_to_endgame(self) -> None:
+ self._go_to_white_house()
+
+ self.assertFalse(self.can_reach_region(ZorkGrandInquisitorRegions.ENDGAME.value))
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.TOTEM_BROG.value,
+ ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH.value,
+ ZorkGrandInquisitorItems.BROGS_GRUE_EGG.value,
+ ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT.value,
+ ZorkGrandInquisitorItems.BROGS_PLANK.value,
+ ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE.value,
+ )
+ )
+
+ self._go_to_dragon_archipelago_dragon()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP.value,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT.value,
+ ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN.value,
+ ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS.value,
+ ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH.value,
+ )
+ )
+
+ self._go_to_port_foozle_past_tavern()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1.value,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2.value,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3.value,
+ ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4.value,
+ ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY.value,
+ ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS.value,
+ )
+ )
+
+ self.assertTrue(self.can_reach_region(ZorkGrandInquisitorRegions.ENDGAME.value))
+
+ def _go_to_crossroads(self) -> None:
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.LANTERN.value,
+ ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR.value,
+ ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL.value,
+ ZorkGrandInquisitorItems.ROPE.value,
+ ZorkGrandInquisitorItems.HOTSPOT_WELL.value,
+ )
+ )
+
+ def _go_to_dm_lair(self) -> None:
+ self._go_to_crossroads()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SWORD.value,
+ ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE.value,
+ )
+ )
+
+ def _go_to_dm_lair_interior(self) -> None:
+ self._go_to_dm_lair()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY.value,
+ ZorkGrandInquisitorItems.MEAD_LIGHT.value,
+ ZorkGrandInquisitorItems.ZIMDOR_SCROLL.value,
+ ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH.value,
+ )
+ )
+
+ def _go_to_dragon_archipelago(self) -> None:
+ self._go_to_hades_beyond_gates()
+ self._obtain_yastard()
+
+ self.collect_by_name(ZorkGrandInquisitorItems.SPELL_NARWILE.value)
+
+ def _go_to_dragon_archipelago_dragon(self) -> None:
+ self._go_to_dragon_archipelago()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.TOTEM_GRIFF.value,
+ ZorkGrandInquisitorItems.HOTSPOT_DRAGON_CLAW.value,
+ )
+ )
+
+ def _go_to_gue_tech(self) -> None:
+ self._go_to_crossroads()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SPELL_REZROV.value,
+ ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR.value,
+ )
+ )
+
+ def _go_to_gue_tech_hallway(self) -> None:
+ self._go_to_gue_tech()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SPELL_IGRAM.value,
+ ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS.value,
+ )
+ )
+
+ def _go_to_gue_tech_outside(self) -> None:
+ self._go_to_crossroads()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH.value,
+ )
+ )
+
+ def _go_to_hades(self) -> None:
+ self._go_to_hades_shore()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER.value,
+ ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS.value,
+ ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS.value,
+ )
+ )
+
+ def _go_to_hades_beyond_gates(self) -> None:
+ self._go_to_hades()
+ self._obtain_snavig()
+
+ self.collect_by_name(ZorkGrandInquisitorItems.TOTEM_BROG.value)
+
+ def _go_to_hades_shore(self) -> None:
+ self._go_to_crossroads()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES.value,
+ )
+ )
+
+ def _go_to_monastery(self) -> None:
+ self._go_to_subway_monastery()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SWORD.value,
+ ZorkGrandInquisitorItems.SPELL_GLORF.value,
+ ZorkGrandInquisitorItems.HOTSPOT_MONASTERY_VENT.value,
+ )
+ )
+
+ def _go_to_monastery_exhibit(self) -> None:
+ self._go_to_monastery()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION.value,
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS.value,
+ ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH.value,
+ )
+ )
+
+ def _go_to_port_foozle_jacks_shop(self) -> None:
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR.value,
+ ZorkGrandInquisitorItems.LANTERN.value,
+ ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL.value,
+ )
+ )
+
+ def _go_to_port_foozle_past(self) -> None:
+ self._go_to_monastery_exhibit()
+
+ self._obtain_yastard()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER.value,
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT.value,
+ ZorkGrandInquisitorItems.LARGE_TELEGRAPH_HAMMER.value,
+ ZorkGrandInquisitorItems.SPELL_NARWILE.value,
+ )
+ )
+
+ def _go_to_port_foozle_past_tavern(self) -> None:
+ self._go_to_port_foozle_past()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.TOTEM_LUCY.value,
+ ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR.value,
+ )
+ )
+
+ def _go_to_spell_lab(self) -> None:
+ self._go_to_subway_flood_control_dam()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SPELL_REZROV.value,
+ ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS.value,
+ ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS.value,
+ )
+ )
+
+ self._go_to_spell_lab_bridge()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SWORD.value,
+ ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE.value,
+ ZorkGrandInquisitorItems.SPELL_GOLGATEM.value,
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM.value,
+ )
+ )
+
+ def _go_to_spell_lab_bridge(self) -> None:
+ self._go_to_crossroads()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB.value,
+ )
+ )
+
+ def _go_to_subway_crossroads(self) -> None:
+ self._go_to_crossroads()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SUBWAY_TOKEN.value,
+ ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT.value,
+ )
+ )
+
+ def _go_to_subway_flood_control_dam(self) -> None:
+ self._go_to_subway_crossroads()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SPELL_KENDALL.value,
+ ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM.value,
+ )
+ )
+
+ def _go_to_subway_monastery(self) -> None:
+ self._go_to_crossroads()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.MAP.value,
+ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY.value,
+ )
+ )
+
+ def _go_to_white_house(self) -> None:
+ self._go_to_dm_lair_interior()
+
+ self._obtain_yastard()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR.value,
+ ZorkGrandInquisitorItems.SPELL_NARWILE.value,
+ )
+ )
+
+ def _go_to_walking_castle(self) -> None:
+ self._go_to_dm_lair_interior()
+
+ self._obtain_obidil()
+ self.collect_by_name(ZorkGrandInquisitorItems.HOTSPOT_BLINDS.value)
+
+ def _obtain_obidil(self) -> None:
+ self._go_to_crossroads()
+ self._go_to_gue_tech()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS.value,
+ ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT.value,
+ ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS.value,
+ )
+ )
+
+ self._go_to_subway_flood_control_dam()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SPELL_REZROV.value,
+ ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS.value,
+ ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS.value,
+ )
+ )
+
+ self._go_to_spell_lab_bridge()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SWORD.value,
+ ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE.value,
+ ZorkGrandInquisitorItems.SPELL_GOLGATEM.value,
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM.value,
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER.value,
+ )
+ )
+
+ def _obtain_snavig(self) -> None:
+ self._go_to_crossroads()
+ self._go_to_dm_lair_interior()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SCROLL_FRAGMENT_ANS.value,
+ ZorkGrandInquisitorItems.SCROLL_FRAGMENT_GIV.value,
+ ZorkGrandInquisitorItems.HOTSPOT_MIRROR.value,
+ )
+ )
+
+ self._go_to_subway_flood_control_dam()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SPELL_REZROV.value,
+ ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS.value,
+ ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS.value,
+ )
+ )
+
+ self._go_to_spell_lab_bridge()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.SWORD.value,
+ ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE.value,
+ ZorkGrandInquisitorItems.SPELL_GOLGATEM.value,
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM.value,
+ ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER.value,
+ )
+ )
+
+ def _obtain_yastard(self) -> None:
+ self._go_to_crossroads()
+ self._go_to_dm_lair_interior()
+
+ self.collect_by_name(
+ (
+ ZorkGrandInquisitorItems.FLATHEADIA_FUDGE.value,
+ ZorkGrandInquisitorItems.HUNGUS_LARD.value,
+ ZorkGrandInquisitorItems.JAR_OF_HOTBUGS.value,
+ ZorkGrandInquisitorItems.QUELBEE_HONEYCOMB.value,
+ ZorkGrandInquisitorItems.MOSS_OF_MAREILON.value,
+ ZorkGrandInquisitorItems.MUG.value,
+ )
+ )
+
+
+class AccessTestLocations(ZorkGrandInquisitorTestBase):
+ options = {
+ "deathsanity": "true",
+ "start_with_hotspot_items": "false",
+ }
+
+ def test_access_locations_requiring_brogs_flickering_torch(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BROG_DO_GOOD.value,
+ ZorkGrandInquisitorLocations.BROG_EAT_ROCKS.value,
+ ZorkGrandInquisitorLocations.BROG_KNOW_DUMB_THAT_DUMB.value,
+ ZorkGrandInquisitorLocations.BROG_MUCH_BETTER_AT_THIS_GAME.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH.value,)]
+ )
+
+ def test_access_locations_requiring_brogs_grue_egg(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BROG_DO_GOOD.value,
+ ZorkGrandInquisitorLocations.BROG_KNOW_DUMB_THAT_DUMB.value,
+ ZorkGrandInquisitorLocations.BROG_MUCH_BETTER_AT_THIS_GAME.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.BROGS_GRUE_EGG.value,)]
+ )
+
+ def test_access_locations_requiring_brogs_plank(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BROG_MUCH_BETTER_AT_THIS_GAME.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.BROGS_PLANK.value,)]
+ )
+
+ def test_access_locations_requiring_flatheadia_fudge(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU.value,
+ ZorkGrandInquisitorEvents.KNOWS_YASTARD.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.FLATHEADIA_FUDGE.value,)]
+ )
+
+ def test_access_locations_requiring_griffs_air_pump(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS.value,
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP.value,)]
+ )
+
+ def test_access_locations_requiring_griffs_dragon_tooth(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS.value,
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH.value,)]
+ )
+
+ def test_access_locations_requiring_griffs_inflatable_raft(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS.value,
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT.value,)]
+ )
+
+ def test_access_locations_requiring_griffs_inflatable_sea_captain(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS.value,
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN.value,)]
+ )
+
+ def test_access_locations_requiring_hammer(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BOING_BOING_BOING.value,
+ ZorkGrandInquisitorLocations.BONK.value,
+ ZorkGrandInquisitorLocations.FLYING_SNAPDRAGON.value,
+ ZorkGrandInquisitorLocations.IN_CASE_OF_ADVENTURE.value,
+ ZorkGrandInquisitorLocations.MUSHROOM_HAMMERED.value,
+ ZorkGrandInquisitorLocations.THROCKED_MUSHROOM_HAMMERED.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HAMMER.value,)]
+ )
+
+ def test_access_locations_requiring_hungus_lard(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU.value,
+ ZorkGrandInquisitorLocations.OUTSMART_THE_QUELBEES.value,
+ ZorkGrandInquisitorLocations.DEATH_OUTSMARTED_BY_THE_QUELBEES.value,
+ ZorkGrandInquisitorEvents.KNOWS_YASTARD.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HUNGUS_LARD.value,)]
+ )
+
+ def test_access_locations_requiring_jar_of_hotbugs(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU.value,
+ ZorkGrandInquisitorEvents.KNOWS_YASTARD.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.JAR_OF_HOTBUGS.value,)]
+ )
+
+ def test_access_locations_requiring_lantern(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.LANTERN.value,)]
+ )
+
+ def test_access_locations_requiring_large_telegraph_hammer(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.PORT_FOOZLE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS.value,
+ ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.LARGE_TELEGRAPH_HAMMER.value,)]
+ )
+
+ def test_access_locations_requiring_lucys_playing_cards(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1.value,)]
+ )
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2.value,)]
+ )
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3.value,)]
+ )
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4.value,)]
+ )
+
+ def test_access_locations_requiring_map(self) -> None:
+ locations: List[str] = list()
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.MAP.value,)]
+ )
+
+ def test_access_locations_requiring_mead_light(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.MEAD_LIGHT.value,
+ ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO.value,
+ ZorkGrandInquisitorEvents.DOOR_DRANK_MEAD.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.MEAD_LIGHT.value,)]
+ )
+
+ def test_access_locations_requiring_moss_of_mareilon(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU.value,
+ ZorkGrandInquisitorEvents.KNOWS_YASTARD.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.MOSS_OF_MAREILON.value,)]
+ )
+
+ def test_access_locations_requiring_mug(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU.value,
+ ZorkGrandInquisitorEvents.KNOWS_YASTARD.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.MUG.value,)]
+ )
+
+ def test_access_locations_requiring_old_scratch_card(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DEATH_LOST_SOUL_TO_OLD_SCRATCH.value,
+ ZorkGrandInquisitorLocations.OLD_SCRATCH_WINNER.value,
+ ZorkGrandInquisitorEvents.ZORKMID_BILL_ACCESSIBLE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.OLD_SCRATCH_CARD.value,)]
+ )
+
+ def test_access_locations_requiring_perma_suck_machine(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.SUCKING_ROCKS.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.PERMA_SUCK_MACHINE.value,)]
+ )
+
+ def test_access_locations_requiring_plastic_six_pack_holder(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.HELP_ME_CANT_BREATHE.value,
+ ZorkGrandInquisitorLocations.WHAT_ARE_YOU_STUPID.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.PLASTIC_SIX_PACK_HOLDER.value,)]
+ )
+
+ def test_access_locations_requiring_pouch_of_zorkmids(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.A_BIG_FAT_SASSY_2_HEADED_MONSTER.value,
+ ZorkGrandInquisitorLocations.A_LETTER_FROM_THE_WHITE_HOUSE.value,
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.DEATH_YOURE_NOT_CHARON.value,
+ ZorkGrandInquisitorLocations.DONT_EVEN_START_WITH_US_SPARKY.value,
+ ZorkGrandInquisitorLocations.DRAGON_ARCHIPELAGO_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.DUNCE_LOCKER.value,
+ ZorkGrandInquisitorLocations.I_SPIT_ON_YOUR_FILTHY_COINAGE.value,
+ ZorkGrandInquisitorLocations.NOOOOOOOOOOOOO.value,
+ ZorkGrandInquisitorLocations.NOW_YOU_LOOK_LIKE_US_WHICH_IS_AN_IMPROVEMENT.value,
+ ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.OPEN_THE_GATES_OF_HELL.value,
+ ZorkGrandInquisitorLocations.SOUVENIR.value,
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS.value,
+ ZorkGrandInquisitorLocations.THIS_DOESNT_LOOK_ANYTHING_LIKE_THE_BROCHURE.value,
+ ZorkGrandInquisitorLocations.UH_OH_BROG_CANT_SWIM.value,
+ ZorkGrandInquisitorEvents.DALBOZ_LOCKER_OPENABLE.value,
+ ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE.value,
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_OBIDIL.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_ACTIVATED.value,
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_SUCKABLE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS.value,)]
+ )
+
+ def test_access_locations_requiring_prozork_tablet(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.PROZORKED.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.PROZORK_TABLET.value,)]
+ )
+
+ def test_access_locations_requiring_quelbee_honeycomb(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU.value,
+ ZorkGrandInquisitorEvents.KNOWS_YASTARD.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.QUELBEE_HONEYCOMB.value,)]
+ )
+
+ def test_access_locations_requiring_rope(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.ALARM_SYSTEM_IS_DOWN.value,
+ ZorkGrandInquisitorLocations.ARTIFACTS_EXPLAINED.value,
+ ZorkGrandInquisitorLocations.A_BIG_FAT_SASSY_2_HEADED_MONSTER.value,
+ ZorkGrandInquisitorLocations.A_LETTER_FROM_THE_WHITE_HOUSE.value,
+ ZorkGrandInquisitorLocations.A_SMALLWAY.value,
+ ZorkGrandInquisitorLocations.BEAUTIFUL_THATS_PLENTY.value,
+ ZorkGrandInquisitorLocations.BEBURTT_DEMYSTIFIED.value,
+ ZorkGrandInquisitorLocations.BETTER_SPELL_MANUFACTURING_IN_UNDER_10_MINUTES.value,
+ ZorkGrandInquisitorLocations.BOING_BOING_BOING.value,
+ ZorkGrandInquisitorLocations.BONK.value,
+ ZorkGrandInquisitorLocations.BRAVE_SOULS_WANTED.value,
+ ZorkGrandInquisitorLocations.BROG_DO_GOOD.value,
+ ZorkGrandInquisitorLocations.BROG_EAT_ROCKS.value,
+ ZorkGrandInquisitorLocations.BROG_KNOW_DUMB_THAT_DUMB.value,
+ ZorkGrandInquisitorLocations.BROG_MUCH_BETTER_AT_THIS_GAME.value,
+ ZorkGrandInquisitorLocations.CASTLE_WATCHING_A_FIELD_GUIDE.value,
+ ZorkGrandInquisitorLocations.CAVES_NOTES.value,
+ ZorkGrandInquisitorLocations.CLOSING_THE_TIME_TUNNELS.value,
+ ZorkGrandInquisitorLocations.CRISIS_AVERTED.value,
+ ZorkGrandInquisitorLocations.DEATH_ATTACKED_THE_QUELBEES.value,
+ ZorkGrandInquisitorLocations.DEATH_CLIMBED_OUT_OF_THE_WELL.value,
+ ZorkGrandInquisitorLocations.DEATH_EATEN_BY_A_GRUE.value,
+ ZorkGrandInquisitorLocations.DEATH_JUMPED_IN_BOTTOMLESS_PIT.value,
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.DEATH_OUTSMARTED_BY_THE_QUELBEES.value,
+ ZorkGrandInquisitorLocations.DEATH_SLICED_UP_BY_THE_INVISIBLE_GUARD.value,
+ ZorkGrandInquisitorLocations.DEATH_STEPPED_INTO_THE_INFINITE.value,
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.DEATH_THROCKED_THE_GRASS.value,
+ ZorkGrandInquisitorLocations.DEATH_TOTEMIZED.value,
+ ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY.value,
+ ZorkGrandInquisitorLocations.DEATH_YOURE_NOT_CHARON.value,
+ ZorkGrandInquisitorLocations.DEATH_ZORK_ROCKS_EXPLODED.value,
+ ZorkGrandInquisitorLocations.DENIED_BY_THE_LAKE_MONSTER.value,
+ ZorkGrandInquisitorLocations.DESPERATELY_SEEKING_TUTOR.value,
+ ZorkGrandInquisitorLocations.DONT_EVEN_START_WITH_US_SPARKY.value,
+ ZorkGrandInquisitorLocations.DOOOOOOWN.value,
+ ZorkGrandInquisitorLocations.DOWN.value,
+ ZorkGrandInquisitorLocations.DRAGON_ARCHIPELAGO_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.DUNCE_LOCKER.value,
+ ZorkGrandInquisitorLocations.EGGPLANTS.value,
+ ZorkGrandInquisitorLocations.EMERGENCY_MAGICATRONIC_MESSAGE.value,
+ ZorkGrandInquisitorLocations.ENJOY_YOUR_TRIP.value,
+ ZorkGrandInquisitorLocations.FAT_LOT_OF_GOOD_THATLL_DO_YA.value,
+ ZorkGrandInquisitorLocations.FLOOD_CONTROL_DAM_3_THE_NOT_REMOTELY_BORING_TALE.value,
+ ZorkGrandInquisitorLocations.FLYING_SNAPDRAGON.value,
+ ZorkGrandInquisitorLocations.FROBUARY_3_UNDERGROUNDHOG_DAY.value,
+ ZorkGrandInquisitorLocations.GETTING_SOME_CHANGE.value,
+ ZorkGrandInquisitorLocations.GUE_TECH_DEANS_LIST.value,
+ ZorkGrandInquisitorLocations.GUE_TECH_ENTRANCE_EXAM.value,
+ ZorkGrandInquisitorLocations.GUE_TECH_HEALTH_MEMO.value,
+ ZorkGrandInquisitorLocations.GUE_TECH_MAGEMEISTERS.value,
+ ZorkGrandInquisitorLocations.HAVE_A_HELL_OF_A_DAY.value,
+ ZorkGrandInquisitorLocations.HELLO_THIS_IS_SHONA_FROM_GURTH_PUBLISHING.value,
+ ZorkGrandInquisitorLocations.HEY_FREE_DIRT.value,
+ ZorkGrandInquisitorLocations.HI_MY_NAME_IS_DOUG.value,
+ ZorkGrandInquisitorLocations.HMMM_INFORMATIVE_YET_DEEPLY_DISTURBING.value,
+ ZorkGrandInquisitorLocations.HOLD_ON_FOR_AN_IMPORTANT_MESSAGE.value,
+ ZorkGrandInquisitorLocations.HOW_TO_HYPNOTIZE_YOURSELF.value,
+ ZorkGrandInquisitorLocations.HOW_TO_WIN_AT_DOUBLE_FANUCCI.value,
+ ZorkGrandInquisitorLocations.I_DONT_THINK_YOU_WOULDVE_WANTED_THAT_TO_WORK_ANYWAY.value,
+ ZorkGrandInquisitorLocations.I_SPIT_ON_YOUR_FILTHY_COINAGE.value,
+ ZorkGrandInquisitorLocations.IMBUE_BEBURTT.value,
+ ZorkGrandInquisitorLocations.INTO_THE_FOLIAGE.value,
+ ZorkGrandInquisitorLocations.IN_CASE_OF_ADVENTURE.value,
+ ZorkGrandInquisitorLocations.IN_MAGIC_WE_TRUST.value,
+ ZorkGrandInquisitorLocations.INVISIBLE_FLOWERS.value,
+ ZorkGrandInquisitorLocations.I_HOPE_YOU_CAN_CLIMB_UP_THERE.value,
+ ZorkGrandInquisitorLocations.I_LIKE_YOUR_STYLE.value,
+ ZorkGrandInquisitorLocations.LIT_SUNFLOWERS.value,
+ ZorkGrandInquisitorLocations.MAGIC_FOREVER.value,
+ ZorkGrandInquisitorLocations.MAILED_IT_TO_HELL.value,
+ ZorkGrandInquisitorLocations.MAKE_LOVE_NOT_WAR.value,
+ ZorkGrandInquisitorLocations.MIKES_PANTS.value,
+ ZorkGrandInquisitorLocations.MUSHROOM_HAMMERED.value,
+ ZorkGrandInquisitorLocations.NATIONAL_TREASURE.value,
+ ZorkGrandInquisitorLocations.NATURAL_AND_SUPERNATURAL_CREATURES_OF_QUENDOR.value,
+ ZorkGrandInquisitorLocations.NO_BONDAGE.value,
+ ZorkGrandInquisitorLocations.NOOOOOOOOOOOOO.value,
+ ZorkGrandInquisitorLocations.NOTHIN_LIKE_A_GOOD_STOGIE.value,
+ ZorkGrandInquisitorLocations.NOW_YOU_LOOK_LIKE_US_WHICH_IS_AN_IMPROVEMENT.value,
+ ZorkGrandInquisitorLocations.OBIDIL_DRIED_UP.value,
+ ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS.value,
+ ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU.value,
+ ZorkGrandInquisitorLocations.OPEN_THE_GATES_OF_HELL.value,
+ ZorkGrandInquisitorLocations.OUTSMART_THE_QUELBEES.value,
+ ZorkGrandInquisitorLocations.PERMASEAL.value,
+ ZorkGrandInquisitorLocations.PLEASE_DONT_THROCK_THE_GRASS.value,
+ ZorkGrandInquisitorLocations.PORT_FOOZLE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.PROZORKED.value,
+ ZorkGrandInquisitorLocations.REASSEMBLE_SNAVIG.value,
+ ZorkGrandInquisitorLocations.RESTOCKED_ON_GRUESDAY.value,
+ ZorkGrandInquisitorLocations.RIGHT_HELLO_YES_UH_THIS_IS_SNEFFLE.value,
+ ZorkGrandInquisitorLocations.RIGHT_UH_SORRY_ITS_ME_AGAIN_SNEFFLE.value,
+ ZorkGrandInquisitorLocations.SNAVIG_REPAIRED.value,
+ ZorkGrandInquisitorLocations.SOUVENIR.value,
+ ZorkGrandInquisitorLocations.STRAIGHT_TO_HELL.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.SUCKING_ROCKS.value,
+ ZorkGrandInquisitorLocations.TAMING_YOUR_SNAPDRAGON.value,
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS.value,
+ ZorkGrandInquisitorLocations.THATS_A_ROPE.value,
+ ZorkGrandInquisitorLocations.THATS_IT_JUST_KEEP_HITTING_THOSE_BUTTONS.value,
+ ZorkGrandInquisitorLocations.THATS_STILL_A_ROPE.value,
+ ZorkGrandInquisitorLocations.THE_ALCHEMICAL_DEBACLE.value,
+ ZorkGrandInquisitorLocations.THE_ENDLESS_FIRE.value,
+ ZorkGrandInquisitorLocations.THE_FLATHEADIAN_FUDGE_FIASCO.value,
+ ZorkGrandInquisitorLocations.THE_PERILS_OF_MAGIC.value,
+ ZorkGrandInquisitorLocations.THE_UNDERGROUND_UNDERGROUND.value,
+ ZorkGrandInquisitorLocations.THIS_DOESNT_LOOK_ANYTHING_LIKE_THE_BROCHURE.value,
+ ZorkGrandInquisitorLocations.THROCKED_MUSHROOM_HAMMERED.value,
+ ZorkGrandInquisitorLocations.TIME_TRAVEL_FOR_DUMMIES.value,
+ ZorkGrandInquisitorLocations.UH_OH_BROG_CANT_SWIM.value,
+ ZorkGrandInquisitorLocations.UMBRELLA_FLOWERS.value,
+ ZorkGrandInquisitorLocations.UP.value,
+ ZorkGrandInquisitorLocations.USELESS_BUT_FUN.value,
+ ZorkGrandInquisitorLocations.UUUUUP.value,
+ ZorkGrandInquisitorLocations.VOYAGE_OF_CAPTAIN_ZAHAB.value,
+ ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO.value,
+ ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorLocations.WHITE_HOUSE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.WOW_IVE_NEVER_GONE_INSIDE_HIM_BEFORE.value,
+ ZorkGrandInquisitorLocations.YAD_GOHDNUORGREDNU_3_YRAUBORF.value,
+ ZorkGrandInquisitorLocations.YOU_DONT_GO_MESSING_WITH_A_MANS_ZIPPER.value,
+ ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS.value,
+ ZorkGrandInquisitorLocations.YOUR_PUNY_WEAPONS_DONT_PHASE_ME_BABY.value,
+ ZorkGrandInquisitorEvents.CHARON_CALLED.value,
+ ZorkGrandInquisitorEvents.DAM_DESTROYED.value,
+ ZorkGrandInquisitorEvents.DOOR_DRANK_MEAD.value,
+ ZorkGrandInquisitorEvents.DOOR_SMOKED_CIGAR.value,
+ ZorkGrandInquisitorEvents.DALBOZ_LOCKER_OPENABLE.value,
+ ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE.value,
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_OBIDIL.value,
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_SNAVIG.value,
+ ZorkGrandInquisitorEvents.KNOWS_BEBURTT.value,
+ ZorkGrandInquisitorEvents.KNOWS_OBIDIL.value,
+ ZorkGrandInquisitorEvents.KNOWS_SNAVIG.value,
+ ZorkGrandInquisitorEvents.KNOWS_YASTARD.value,
+ ZorkGrandInquisitorEvents.ROPE_GLORFABLE.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ZorkGrandInquisitorEvents.WHITE_HOUSE_LETTER_MAILABLE.value,
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_ACTIVATED.value,
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_SUCKABLE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.ROPE.value,)]
+ )
+
+ def test_access_locations_requiring_scroll_fragment_ans(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.REASSEMBLE_SNAVIG.value,
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_SNAVIG.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SCROLL_FRAGMENT_ANS.value,)]
+ )
+
+ def test_access_locations_requiring_scroll_fragment_giv(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.REASSEMBLE_SNAVIG.value,
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_SNAVIG.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SCROLL_FRAGMENT_GIV.value,)]
+ )
+
+ def test_access_locations_requiring_shovel(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.HEY_FREE_DIRT.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SHOVEL.value,)]
+ )
+
+ def test_access_locations_requiring_snapdragon(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BOING_BOING_BOING.value,
+ ZorkGrandInquisitorLocations.FLYING_SNAPDRAGON.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SNAPDRAGON.value,)]
+ )
+
+ def test_access_locations_requiring_student_id(self) -> None:
+ locations: List[str] = list()
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.STUDENT_ID.value,)]
+ )
+
+ def test_access_locations_requiring_subway_token(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.THE_UNDERGROUND_UNDERGROUND.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SUBWAY_TOKEN.value,)]
+ )
+
+ def test_access_locations_requiring_sword(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.CLOSING_THE_TIME_TUNNELS.value,
+ ZorkGrandInquisitorLocations.DEATH_ATTACKED_THE_QUELBEES.value,
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.DEATH_TOTEMIZED.value,
+ ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY.value,
+ ZorkGrandInquisitorLocations.DONT_EVEN_START_WITH_US_SPARKY.value,
+ ZorkGrandInquisitorLocations.HMMM_INFORMATIVE_YET_DEEPLY_DISTURBING.value,
+ ZorkGrandInquisitorLocations.I_HOPE_YOU_CAN_CLIMB_UP_THERE.value,
+ ZorkGrandInquisitorLocations.I_LIKE_YOUR_STYLE.value,
+ ZorkGrandInquisitorLocations.IMBUE_BEBURTT.value,
+ ZorkGrandInquisitorLocations.INTO_THE_FOLIAGE.value,
+ ZorkGrandInquisitorLocations.MAKE_LOVE_NOT_WAR.value,
+ ZorkGrandInquisitorLocations.OBIDIL_DRIED_UP.value,
+ ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS.value,
+ ZorkGrandInquisitorLocations.OUTSMART_THE_QUELBEES.value,
+ ZorkGrandInquisitorLocations.PERMASEAL.value,
+ ZorkGrandInquisitorLocations.PORT_FOOZLE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.SNAVIG_REPAIRED.value,
+ ZorkGrandInquisitorLocations.STRAIGHT_TO_HELL.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.THE_ALCHEMICAL_DEBACLE.value,
+ ZorkGrandInquisitorLocations.THE_ENDLESS_FIRE.value,
+ ZorkGrandInquisitorLocations.THE_FLATHEADIAN_FUDGE_FIASCO.value,
+ ZorkGrandInquisitorLocations.THE_PERILS_OF_MAGIC.value,
+ ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS.value,
+ ZorkGrandInquisitorLocations.YOUR_PUNY_WEAPONS_DONT_PHASE_ME_BABY.value,
+ ZorkGrandInquisitorEvents.KNOWS_BEBURTT.value,
+ ZorkGrandInquisitorEvents.KNOWS_OBIDIL.value,
+ ZorkGrandInquisitorEvents.KNOWS_SNAVIG.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SWORD.value,)]
+ )
+
+ def test_access_locations_requiring_zimdor_scroll(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO.value,
+ ZorkGrandInquisitorEvents.DOOR_DRANK_MEAD.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.ZIMDOR_SCROLL.value,)]
+ )
+
+ def test_access_locations_requiring_zork_rocks(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_ACTIVATED.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.ZORK_ROCKS.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_666_mailbox(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.A_LETTER_FROM_THE_WHITE_HOUSE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_666_MAILBOX.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_alpines_quandry_card_slots(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_blank_scroll_box(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.IMBUE_BEBURTT.value,
+ ZorkGrandInquisitorEvents.KNOWS_BEBURTT.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_BLANK_SCROLL_BOX.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_blinds(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DENIED_BY_THE_LAKE_MONSTER.value,
+ ZorkGrandInquisitorLocations.WOW_IVE_NEVER_GONE_INSIDE_HIM_BEFORE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_BLINDS.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_candy_machine_buttons(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DUNCE_LOCKER.value,
+ ZorkGrandInquisitorLocations.NOOOOOOOOOOOOO.value,
+ ZorkGrandInquisitorEvents.DALBOZ_LOCKER_OPENABLE.value,
+ ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE.value,
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_SUCKABLE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_candy_machine_coin_slot(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DUNCE_LOCKER.value,
+ ZorkGrandInquisitorLocations.NOOOOOOOOOOOOO.value,
+ ZorkGrandInquisitorEvents.DALBOZ_LOCKER_OPENABLE.value,
+ ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE.value,
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_SUCKABLE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_candy_machine_vacuum_slot(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.SUCKING_ROCKS.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_VACUUM_SLOT.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_change_machine_slot(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.GETTING_SOME_CHANGE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_CHANGE_MACHINE_SLOT.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_closet_door(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BROG_DO_GOOD.value,
+ ZorkGrandInquisitorLocations.BROG_EAT_ROCKS.value,
+ ZorkGrandInquisitorLocations.BROG_KNOW_DUMB_THAT_DUMB.value,
+ ZorkGrandInquisitorLocations.BROG_MUCH_BETTER_AT_THIS_GAME.value,
+ ZorkGrandInquisitorLocations.DOOOOOOWN.value,
+ ZorkGrandInquisitorLocations.DOWN.value,
+ ZorkGrandInquisitorLocations.UP.value,
+ ZorkGrandInquisitorLocations.UUUUUP.value,
+ ZorkGrandInquisitorLocations.MAILED_IT_TO_HELL.value,
+ ZorkGrandInquisitorLocations.WHITE_HOUSE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ZorkGrandInquisitorEvents.WHITE_HOUSE_LETTER_MAILABLE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_closing_the_time_tunnels_hammer_slot(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS.value,
+ ZorkGrandInquisitorLocations.PORT_FOOZLE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_closing_the_time_tunnels_lever(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS.value,
+ ZorkGrandInquisitorLocations.PORT_FOOZLE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_cooking_pot(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BROG_DO_GOOD.value,
+ ZorkGrandInquisitorLocations.BROG_MUCH_BETTER_AT_THIS_GAME.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_dented_locker(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.CRISIS_AVERTED.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_DENTED_LOCKER.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_dirt_mound(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.HEY_FREE_DIRT.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_DIRT_MOUND.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_dock_winch(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.HELP_ME_CANT_BREATHE.value,
+ ZorkGrandInquisitorLocations.NO_BONDAGE.value,
+ ZorkGrandInquisitorLocations.YOU_WANT_A_PIECE_OF_ME_DOCK_BOY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_dragon_claw(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_DRAGON_CLAW.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_dragon_nostrils(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_dungeon_masters_lair_entrance(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.INTO_THE_FOLIAGE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_flood_control_buttons(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.NATIONAL_TREASURE.value,
+ ZorkGrandInquisitorEvents.DAM_DESTROYED.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_flood_control_doors(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.NATIONAL_TREASURE.value,
+ ZorkGrandInquisitorEvents.DAM_DESTROYED.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_frozen_treat_machine_coin_slot(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_OBIDIL.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_frozen_treat_machine_doors(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_OBIDIL.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_glass_case(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.IN_CASE_OF_ADVENTURE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_GLASS_CASE.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_grand_inquisitor_doll(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.ARREST_THE_VANDAL.value,
+ ZorkGrandInquisitorLocations.DEATH_ARRESTED_WITH_JACK.value,
+ ZorkGrandInquisitorLocations.FIRE_FIRE.value,
+ ZorkGrandInquisitorLocations.PLANETFALL.value,
+ ZorkGrandInquisitorLocations.TALK_TO_ME_GRAND_INQUISITOR.value,
+ ZorkGrandInquisitorEvents.LANTERN_DALBOZ_ACCESSIBLE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_gue_tech_door(self) -> None:
+ locations: List[str] = list()
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_DOOR.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_gue_tech_grass(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DEATH_THROCKED_THE_GRASS.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_GRASS.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_hades_phone_buttons(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.A_BIG_FAT_SASSY_2_HEADED_MONSTER.value,
+ ZorkGrandInquisitorLocations.A_LETTER_FROM_THE_WHITE_HOUSE.value,
+ ZorkGrandInquisitorLocations.DEATH_YOURE_NOT_CHARON.value,
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.DONT_EVEN_START_WITH_US_SPARKY.value,
+ ZorkGrandInquisitorLocations.DRAGON_ARCHIPELAGO_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.HAVE_A_HELL_OF_A_DAY.value,
+ ZorkGrandInquisitorLocations.NOW_YOU_LOOK_LIKE_US_WHICH_IS_AN_IMPROVEMENT.value,
+ ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.OPEN_THE_GATES_OF_HELL.value,
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS.value,
+ ZorkGrandInquisitorLocations.THIS_DOESNT_LOOK_ANYTHING_LIKE_THE_BROCHURE.value,
+ ZorkGrandInquisitorLocations.UH_OH_BROG_CANT_SWIM.value,
+ ZorkGrandInquisitorEvents.CHARON_CALLED.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_hades_phone_receiver(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.A_BIG_FAT_SASSY_2_HEADED_MONSTER.value,
+ ZorkGrandInquisitorLocations.A_LETTER_FROM_THE_WHITE_HOUSE.value,
+ ZorkGrandInquisitorLocations.DEATH_YOURE_NOT_CHARON.value,
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.DONT_EVEN_START_WITH_US_SPARKY.value,
+ ZorkGrandInquisitorLocations.DRAGON_ARCHIPELAGO_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.HAVE_A_HELL_OF_A_DAY.value,
+ ZorkGrandInquisitorLocations.NOW_YOU_LOOK_LIKE_US_WHICH_IS_AN_IMPROVEMENT.value,
+ ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.OPEN_THE_GATES_OF_HELL.value,
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS.value,
+ ZorkGrandInquisitorLocations.THIS_DOESNT_LOOK_ANYTHING_LIKE_THE_BROCHURE.value,
+ ZorkGrandInquisitorLocations.UH_OH_BROG_CANT_SWIM.value,
+ ZorkGrandInquisitorEvents.CHARON_CALLED.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_harry(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.YOUR_PUNY_WEAPONS_DONT_PHASE_ME_BABY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_HARRY.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_harrys_ashtray(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.NOTHIN_LIKE_A_GOOD_STOGIE.value,
+ ZorkGrandInquisitorEvents.DOOR_SMOKED_CIGAR.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_harrys_bird_bath(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO.value,
+ ZorkGrandInquisitorEvents.DOOR_DRANK_MEAD.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_in_magic_we_trust_door(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.IN_MAGIC_WE_TRUST.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_jacks_door(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.MEAD_LIGHT.value,
+ ZorkGrandInquisitorLocations.NO_AUTOGRAPHS.value,
+ ZorkGrandInquisitorLocations.THATS_A_ROPE.value,
+ ZorkGrandInquisitorLocations.WHAT_ARE_YOU_STUPID.value,
+ ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_loudspeaker_volume_buttons(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.THATS_THE_SPIRIT.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_LOUDSPEAKER_VOLUME_BUTTONS.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_mailbox_door(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.MAILED_IT_TO_HELL.value,
+ ZorkGrandInquisitorEvents.WHITE_HOUSE_LETTER_MAILABLE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_DOOR.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_mailbox_flag(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DOOOOOOWN.value,
+ ZorkGrandInquisitorLocations.DOWN.value,
+ ZorkGrandInquisitorLocations.MAILED_IT_TO_HELL.value,
+ ZorkGrandInquisitorLocations.UP.value,
+ ZorkGrandInquisitorLocations.UUUUUP.value,
+ ZorkGrandInquisitorEvents.WHITE_HOUSE_LETTER_MAILABLE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_mirror(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.REASSEMBLE_SNAVIG.value,
+ ZorkGrandInquisitorLocations.YAD_GOHDNUORGREDNU_3_YRAUBORF.value,
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_SNAVIG.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_MIRROR.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_monastery_vent(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.CLOSING_THE_TIME_TUNNELS.value,
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.DEATH_TOTEMIZED.value,
+ ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY.value,
+ ZorkGrandInquisitorLocations.HMMM_INFORMATIVE_YET_DEEPLY_DISTURBING.value,
+ ZorkGrandInquisitorLocations.I_HOPE_YOU_CAN_CLIMB_UP_THERE.value,
+ ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS.value,
+ ZorkGrandInquisitorLocations.PERMASEAL.value,
+ ZorkGrandInquisitorLocations.PORT_FOOZLE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.STRAIGHT_TO_HELL.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.THE_ALCHEMICAL_DEBACLE.value,
+ ZorkGrandInquisitorLocations.THE_ENDLESS_FIRE.value,
+ ZorkGrandInquisitorLocations.THE_FLATHEADIAN_FUDGE_FIASCO.value,
+ ZorkGrandInquisitorLocations.THE_PERILS_OF_MAGIC.value,
+ ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_MONASTERY_VENT.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_mossy_grate(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BEAUTIFUL_THATS_PLENTY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_MOSSY_GRATE.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_port_foozle_past_tavern_door(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_purple_words(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.A_SMALLWAY.value,
+ ZorkGrandInquisitorLocations.CRISIS_AVERTED.value,
+ ZorkGrandInquisitorLocations.DEATH_STEPPED_INTO_THE_INFINITE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_quelbee_hive(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DEATH_ATTACKED_THE_QUELBEES.value,
+ ZorkGrandInquisitorLocations.DEATH_OUTSMARTED_BY_THE_QUELBEES.value,
+ ZorkGrandInquisitorLocations.OUTSMART_THE_QUELBEES.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_rope_bridge(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.I_LIKE_YOUR_STYLE.value,
+ ZorkGrandInquisitorLocations.IMBUE_BEBURTT.value,
+ ZorkGrandInquisitorLocations.OBIDIL_DRIED_UP.value,
+ ZorkGrandInquisitorLocations.SNAVIG_REPAIRED.value,
+ ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS.value,
+ ZorkGrandInquisitorEvents.KNOWS_BEBURTT.value,
+ ZorkGrandInquisitorEvents.KNOWS_OBIDIL.value,
+ ZorkGrandInquisitorEvents.KNOWS_SNAVIG.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_skull_cage(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BROG_MUCH_BETTER_AT_THIS_GAME.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_snapdragon(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BONK.value,
+ ZorkGrandInquisitorLocations.I_DONT_THINK_YOU_WOULDVE_WANTED_THAT_TO_WORK_ANYWAY.value,
+ ZorkGrandInquisitorLocations.PROZORKED.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_soda_machine_buttons(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_ACTIVATED.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_BUTTONS.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_soda_machine_coin_slot(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_ACTIVATED.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_COIN_SLOT.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_souvenir_coin_slot(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.SOUVENIR.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_SOUVENIR_COIN_SLOT.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_spell_checker(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.IMBUE_BEBURTT.value,
+ ZorkGrandInquisitorLocations.OBIDIL_DRIED_UP.value,
+ ZorkGrandInquisitorLocations.SNAVIG_REPAIRED.value,
+ ZorkGrandInquisitorEvents.KNOWS_BEBURTT.value,
+ ZorkGrandInquisitorEvents.KNOWS_OBIDIL.value,
+ ZorkGrandInquisitorEvents.KNOWS_SNAVIG.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_spell_lab_chasm(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.I_LIKE_YOUR_STYLE.value,
+ ZorkGrandInquisitorLocations.IMBUE_BEBURTT.value,
+ ZorkGrandInquisitorLocations.OBIDIL_DRIED_UP.value,
+ ZorkGrandInquisitorLocations.SNAVIG_REPAIRED.value,
+ ZorkGrandInquisitorEvents.KNOWS_BEBURTT.value,
+ ZorkGrandInquisitorEvents.KNOWS_OBIDIL.value,
+ ZorkGrandInquisitorEvents.KNOWS_SNAVIG.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_spring_mushroom(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BOING_BOING_BOING.value,
+ ZorkGrandInquisitorLocations.FLYING_SNAPDRAGON.value,
+ ZorkGrandInquisitorLocations.MUSHROOM_HAMMERED.value,
+ ZorkGrandInquisitorLocations.THROCKED_MUSHROOM_HAMMERED.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_student_id_machine(self) -> None:
+ locations: List[str] = list()
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_STUDENT_ID_MACHINE.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_subway_token_slot(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.THE_UNDERGROUND_UNDERGROUND.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_tavern_fly(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_totemizer_switch(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.CLOSING_THE_TIME_TUNNELS.value,
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.DEATH_TOTEMIZED.value,
+ ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY.value,
+ ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS.value,
+ ZorkGrandInquisitorLocations.PORT_FOOZLE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.STRAIGHT_TO_HELL.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.THE_ALCHEMICAL_DEBACLE.value,
+ ZorkGrandInquisitorLocations.THE_ENDLESS_FIRE.value,
+ ZorkGrandInquisitorLocations.THE_FLATHEADIAN_FUDGE_FIASCO.value,
+ ZorkGrandInquisitorLocations.THE_PERILS_OF_MAGIC.value,
+ ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_totemizer_wheels(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.CLOSING_THE_TIME_TUNNELS.value,
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.DEATH_TOTEMIZED.value,
+ ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS.value,
+ ZorkGrandInquisitorLocations.PORT_FOOZLE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.STRAIGHT_TO_HELL.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.THE_ALCHEMICAL_DEBACLE.value,
+ ZorkGrandInquisitorLocations.THE_ENDLESS_FIRE.value,
+ ZorkGrandInquisitorLocations.THE_FLATHEADIAN_FUDGE_FIASCO.value,
+ ZorkGrandInquisitorLocations.THE_PERILS_OF_MAGIC.value,
+ ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS.value,)]
+ )
+
+ def test_access_locations_requiring_hotspot_well(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.ALARM_SYSTEM_IS_DOWN.value,
+ ZorkGrandInquisitorLocations.ARTIFACTS_EXPLAINED.value,
+ ZorkGrandInquisitorLocations.A_BIG_FAT_SASSY_2_HEADED_MONSTER.value,
+ ZorkGrandInquisitorLocations.A_LETTER_FROM_THE_WHITE_HOUSE.value,
+ ZorkGrandInquisitorLocations.A_SMALLWAY.value,
+ ZorkGrandInquisitorLocations.BEAUTIFUL_THATS_PLENTY.value,
+ ZorkGrandInquisitorLocations.BEBURTT_DEMYSTIFIED.value,
+ ZorkGrandInquisitorLocations.BETTER_SPELL_MANUFACTURING_IN_UNDER_10_MINUTES.value,
+ ZorkGrandInquisitorLocations.BOING_BOING_BOING.value,
+ ZorkGrandInquisitorLocations.BONK.value,
+ ZorkGrandInquisitorLocations.BRAVE_SOULS_WANTED.value,
+ ZorkGrandInquisitorLocations.BROG_DO_GOOD.value,
+ ZorkGrandInquisitorLocations.BROG_EAT_ROCKS.value,
+ ZorkGrandInquisitorLocations.BROG_KNOW_DUMB_THAT_DUMB.value,
+ ZorkGrandInquisitorLocations.BROG_MUCH_BETTER_AT_THIS_GAME.value,
+ ZorkGrandInquisitorLocations.CASTLE_WATCHING_A_FIELD_GUIDE.value,
+ ZorkGrandInquisitorLocations.CAVES_NOTES.value,
+ ZorkGrandInquisitorLocations.CLOSING_THE_TIME_TUNNELS.value,
+ ZorkGrandInquisitorLocations.CRISIS_AVERTED.value,
+ ZorkGrandInquisitorLocations.DEATH_ATTACKED_THE_QUELBEES.value,
+ ZorkGrandInquisitorLocations.DEATH_CLIMBED_OUT_OF_THE_WELL.value,
+ ZorkGrandInquisitorLocations.DEATH_EATEN_BY_A_GRUE.value,
+ ZorkGrandInquisitorLocations.DEATH_JUMPED_IN_BOTTOMLESS_PIT.value,
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.DEATH_OUTSMARTED_BY_THE_QUELBEES.value,
+ ZorkGrandInquisitorLocations.DEATH_SLICED_UP_BY_THE_INVISIBLE_GUARD.value,
+ ZorkGrandInquisitorLocations.DEATH_STEPPED_INTO_THE_INFINITE.value,
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.DEATH_THROCKED_THE_GRASS.value,
+ ZorkGrandInquisitorLocations.DEATH_TOTEMIZED.value,
+ ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY.value,
+ ZorkGrandInquisitorLocations.DEATH_YOURE_NOT_CHARON.value,
+ ZorkGrandInquisitorLocations.DEATH_ZORK_ROCKS_EXPLODED.value,
+ ZorkGrandInquisitorLocations.DENIED_BY_THE_LAKE_MONSTER.value,
+ ZorkGrandInquisitorLocations.DESPERATELY_SEEKING_TUTOR.value,
+ ZorkGrandInquisitorLocations.DONT_EVEN_START_WITH_US_SPARKY.value,
+ ZorkGrandInquisitorLocations.DOOOOOOWN.value,
+ ZorkGrandInquisitorLocations.DOWN.value,
+ ZorkGrandInquisitorLocations.DRAGON_ARCHIPELAGO_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.DUNCE_LOCKER.value,
+ ZorkGrandInquisitorLocations.EGGPLANTS.value,
+ ZorkGrandInquisitorLocations.EMERGENCY_MAGICATRONIC_MESSAGE.value,
+ ZorkGrandInquisitorLocations.ENJOY_YOUR_TRIP.value,
+ ZorkGrandInquisitorLocations.FAT_LOT_OF_GOOD_THATLL_DO_YA.value,
+ ZorkGrandInquisitorLocations.FLOOD_CONTROL_DAM_3_THE_NOT_REMOTELY_BORING_TALE.value,
+ ZorkGrandInquisitorLocations.FLYING_SNAPDRAGON.value,
+ ZorkGrandInquisitorLocations.FROBUARY_3_UNDERGROUNDHOG_DAY.value,
+ ZorkGrandInquisitorLocations.GETTING_SOME_CHANGE.value,
+ ZorkGrandInquisitorLocations.GUE_TECH_DEANS_LIST.value,
+ ZorkGrandInquisitorLocations.GUE_TECH_ENTRANCE_EXAM.value,
+ ZorkGrandInquisitorLocations.GUE_TECH_HEALTH_MEMO.value,
+ ZorkGrandInquisitorLocations.GUE_TECH_MAGEMEISTERS.value,
+ ZorkGrandInquisitorLocations.HAVE_A_HELL_OF_A_DAY.value,
+ ZorkGrandInquisitorLocations.HELLO_THIS_IS_SHONA_FROM_GURTH_PUBLISHING.value,
+ ZorkGrandInquisitorLocations.HEY_FREE_DIRT.value,
+ ZorkGrandInquisitorLocations.HI_MY_NAME_IS_DOUG.value,
+ ZorkGrandInquisitorLocations.HMMM_INFORMATIVE_YET_DEEPLY_DISTURBING.value,
+ ZorkGrandInquisitorLocations.HOLD_ON_FOR_AN_IMPORTANT_MESSAGE.value,
+ ZorkGrandInquisitorLocations.HOW_TO_HYPNOTIZE_YOURSELF.value,
+ ZorkGrandInquisitorLocations.HOW_TO_WIN_AT_DOUBLE_FANUCCI.value,
+ ZorkGrandInquisitorLocations.I_DONT_THINK_YOU_WOULDVE_WANTED_THAT_TO_WORK_ANYWAY.value,
+ ZorkGrandInquisitorLocations.I_SPIT_ON_YOUR_FILTHY_COINAGE.value,
+ ZorkGrandInquisitorLocations.IMBUE_BEBURTT.value,
+ ZorkGrandInquisitorLocations.INTO_THE_FOLIAGE.value,
+ ZorkGrandInquisitorLocations.IN_CASE_OF_ADVENTURE.value,
+ ZorkGrandInquisitorLocations.IN_MAGIC_WE_TRUST.value,
+ ZorkGrandInquisitorLocations.INVISIBLE_FLOWERS.value,
+ ZorkGrandInquisitorLocations.I_HOPE_YOU_CAN_CLIMB_UP_THERE.value,
+ ZorkGrandInquisitorLocations.I_LIKE_YOUR_STYLE.value,
+ ZorkGrandInquisitorLocations.LIT_SUNFLOWERS.value,
+ ZorkGrandInquisitorLocations.MAGIC_FOREVER.value,
+ ZorkGrandInquisitorLocations.MAILED_IT_TO_HELL.value,
+ ZorkGrandInquisitorLocations.MAKE_LOVE_NOT_WAR.value,
+ ZorkGrandInquisitorLocations.MIKES_PANTS.value,
+ ZorkGrandInquisitorLocations.MUSHROOM_HAMMERED.value,
+ ZorkGrandInquisitorLocations.NATIONAL_TREASURE.value,
+ ZorkGrandInquisitorLocations.NATURAL_AND_SUPERNATURAL_CREATURES_OF_QUENDOR.value,
+ ZorkGrandInquisitorLocations.NOOOOOOOOOOOOO.value,
+ ZorkGrandInquisitorLocations.NOTHIN_LIKE_A_GOOD_STOGIE.value,
+ ZorkGrandInquisitorLocations.NOW_YOU_LOOK_LIKE_US_WHICH_IS_AN_IMPROVEMENT.value,
+ ZorkGrandInquisitorLocations.OBIDIL_DRIED_UP.value,
+ ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS.value,
+ ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU.value,
+ ZorkGrandInquisitorLocations.OPEN_THE_GATES_OF_HELL.value,
+ ZorkGrandInquisitorLocations.OUTSMART_THE_QUELBEES.value,
+ ZorkGrandInquisitorLocations.PERMASEAL.value,
+ ZorkGrandInquisitorLocations.PLEASE_DONT_THROCK_THE_GRASS.value,
+ ZorkGrandInquisitorLocations.PORT_FOOZLE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.PROZORKED.value,
+ ZorkGrandInquisitorLocations.REASSEMBLE_SNAVIG.value,
+ ZorkGrandInquisitorLocations.RESTOCKED_ON_GRUESDAY.value,
+ ZorkGrandInquisitorLocations.RIGHT_HELLO_YES_UH_THIS_IS_SNEFFLE.value,
+ ZorkGrandInquisitorLocations.RIGHT_UH_SORRY_ITS_ME_AGAIN_SNEFFLE.value,
+ ZorkGrandInquisitorLocations.SNAVIG_REPAIRED.value,
+ ZorkGrandInquisitorLocations.SOUVENIR.value,
+ ZorkGrandInquisitorLocations.STRAIGHT_TO_HELL.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.SUCKING_ROCKS.value,
+ ZorkGrandInquisitorLocations.TAMING_YOUR_SNAPDRAGON.value,
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS.value,
+ ZorkGrandInquisitorLocations.THATS_IT_JUST_KEEP_HITTING_THOSE_BUTTONS.value,
+ ZorkGrandInquisitorLocations.THATS_STILL_A_ROPE.value,
+ ZorkGrandInquisitorLocations.THE_ALCHEMICAL_DEBACLE.value,
+ ZorkGrandInquisitorLocations.THE_ENDLESS_FIRE.value,
+ ZorkGrandInquisitorLocations.THE_FLATHEADIAN_FUDGE_FIASCO.value,
+ ZorkGrandInquisitorLocations.THE_PERILS_OF_MAGIC.value,
+ ZorkGrandInquisitorLocations.THE_UNDERGROUND_UNDERGROUND.value,
+ ZorkGrandInquisitorLocations.THIS_DOESNT_LOOK_ANYTHING_LIKE_THE_BROCHURE.value,
+ ZorkGrandInquisitorLocations.THROCKED_MUSHROOM_HAMMERED.value,
+ ZorkGrandInquisitorLocations.TIME_TRAVEL_FOR_DUMMIES.value,
+ ZorkGrandInquisitorLocations.UH_OH_BROG_CANT_SWIM.value,
+ ZorkGrandInquisitorLocations.UMBRELLA_FLOWERS.value,
+ ZorkGrandInquisitorLocations.UP.value,
+ ZorkGrandInquisitorLocations.USELESS_BUT_FUN.value,
+ ZorkGrandInquisitorLocations.UUUUUP.value,
+ ZorkGrandInquisitorLocations.VOYAGE_OF_CAPTAIN_ZAHAB.value,
+ ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO.value,
+ ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorLocations.WHITE_HOUSE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.WOW_IVE_NEVER_GONE_INSIDE_HIM_BEFORE.value,
+ ZorkGrandInquisitorLocations.YAD_GOHDNUORGREDNU_3_YRAUBORF.value,
+ ZorkGrandInquisitorLocations.YOU_DONT_GO_MESSING_WITH_A_MANS_ZIPPER.value,
+ ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS.value,
+ ZorkGrandInquisitorLocations.YOUR_PUNY_WEAPONS_DONT_PHASE_ME_BABY.value,
+ ZorkGrandInquisitorEvents.CHARON_CALLED.value,
+ ZorkGrandInquisitorEvents.DAM_DESTROYED.value,
+ ZorkGrandInquisitorEvents.DOOR_DRANK_MEAD.value,
+ ZorkGrandInquisitorEvents.DOOR_SMOKED_CIGAR.value,
+ ZorkGrandInquisitorEvents.DALBOZ_LOCKER_OPENABLE.value,
+ ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE.value,
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_OBIDIL.value,
+ ZorkGrandInquisitorEvents.HAS_REPAIRABLE_SNAVIG.value,
+ ZorkGrandInquisitorEvents.KNOWS_BEBURTT.value,
+ ZorkGrandInquisitorEvents.KNOWS_OBIDIL.value,
+ ZorkGrandInquisitorEvents.KNOWS_SNAVIG.value,
+ ZorkGrandInquisitorEvents.KNOWS_YASTARD.value,
+ ZorkGrandInquisitorEvents.ROPE_GLORFABLE.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ZorkGrandInquisitorEvents.WHITE_HOUSE_LETTER_MAILABLE.value,
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_ACTIVATED.value,
+ ZorkGrandInquisitorEvents.ZORK_ROCKS_SUCKABLE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.HOTSPOT_WELL.value,)]
+ )
+
+ def test_access_locations_requiring_spell_glorf(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorEvents.ROPE_GLORFABLE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SPELL_GLORF.value,)]
+ )
+
+ def test_access_locations_requiring_spell_golgatem(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DENIED_BY_THE_LAKE_MONSTER.value,
+ ZorkGrandInquisitorLocations.I_LIKE_YOUR_STYLE.value,
+ ZorkGrandInquisitorLocations.IMBUE_BEBURTT.value,
+ ZorkGrandInquisitorLocations.OBIDIL_DRIED_UP.value,
+ ZorkGrandInquisitorLocations.SNAVIG_REPAIRED.value,
+ ZorkGrandInquisitorLocations.USELESS_BUT_FUN.value,
+ ZorkGrandInquisitorEvents.KNOWS_BEBURTT.value,
+ ZorkGrandInquisitorEvents.KNOWS_OBIDIL.value,
+ ZorkGrandInquisitorEvents.KNOWS_SNAVIG.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SPELL_GOLGATEM.value,)]
+ )
+
+ def test_access_locations_requiring_spell_igram(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.A_SMALLWAY.value,
+ ZorkGrandInquisitorLocations.CRISIS_AVERTED.value,
+ ZorkGrandInquisitorLocations.DEATH_STEPPED_INTO_THE_INFINITE.value,
+ ZorkGrandInquisitorLocations.FAT_LOT_OF_GOOD_THATLL_DO_YA.value,
+ ZorkGrandInquisitorLocations.INVISIBLE_FLOWERS.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SPELL_IGRAM.value,)]
+ )
+
+ def test_access_locations_requiring_spell_kendall(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BEBURTT_DEMYSTIFIED.value,
+ ZorkGrandInquisitorLocations.ENJOY_YOUR_TRIP.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SPELL_KENDALL.value,)]
+ )
+
+ def test_access_locations_requiring_spell_narwile(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BROG_DO_GOOD.value,
+ ZorkGrandInquisitorLocations.BROG_EAT_ROCKS.value,
+ ZorkGrandInquisitorLocations.BROG_KNOW_DUMB_THAT_DUMB.value,
+ ZorkGrandInquisitorLocations.BROG_MUCH_BETTER_AT_THIS_GAME.value,
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.DOOOOOOWN.value,
+ ZorkGrandInquisitorLocations.DOWN.value,
+ ZorkGrandInquisitorLocations.DRAGON_ARCHIPELAGO_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.MAILED_IT_TO_HELL.value,
+ ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS.value,
+ ZorkGrandInquisitorLocations.PORT_FOOZLE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS.value,
+ ZorkGrandInquisitorLocations.THIS_DOESNT_LOOK_ANYTHING_LIKE_THE_BROCHURE.value,
+ ZorkGrandInquisitorLocations.UH_OH_BROG_CANT_SWIM.value,
+ ZorkGrandInquisitorLocations.UP.value,
+ ZorkGrandInquisitorLocations.UUUUUP.value,
+ ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorLocations.WHITE_HOUSE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ZorkGrandInquisitorEvents.WHITE_HOUSE_LETTER_MAILABLE.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SPELL_NARWILE.value,)]
+ )
+
+ def test_access_locations_requiring_spell_rezrov(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.IN_MAGIC_WE_TRUST.value,
+ ZorkGrandInquisitorLocations.NATIONAL_TREASURE.value,
+ ZorkGrandInquisitorLocations.YOU_DONT_GO_MESSING_WITH_A_MANS_ZIPPER.value,
+ ZorkGrandInquisitorEvents.DAM_DESTROYED.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SPELL_REZROV.value,)]
+ )
+
+ def test_access_locations_requiring_spell_throck(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BEAUTIFUL_THATS_PLENTY.value,
+ ZorkGrandInquisitorLocations.DEATH_THROCKED_THE_GRASS.value,
+ ZorkGrandInquisitorLocations.FLYING_SNAPDRAGON.value,
+ ZorkGrandInquisitorLocations.I_DONT_THINK_YOU_WOULDVE_WANTED_THAT_TO_WORK_ANYWAY.value,
+ ZorkGrandInquisitorLocations.LIT_SUNFLOWERS.value,
+ ZorkGrandInquisitorLocations.THROCKED_MUSHROOM_HAMMERED.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SPELL_THROCK.value,)]
+ )
+
+ def test_access_locations_requiring_subway_destination_flood_control_dam(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BEAUTIFUL_THATS_PLENTY.value,
+ ZorkGrandInquisitorLocations.FLOOD_CONTROL_DAM_3_THE_NOT_REMOTELY_BORING_TALE.value,
+ ZorkGrandInquisitorLocations.NATIONAL_TREASURE.value,
+ ZorkGrandInquisitorLocations.SOUVENIR.value,
+ ZorkGrandInquisitorLocations.USELESS_BUT_FUN.value,
+ ZorkGrandInquisitorEvents.DAM_DESTROYED.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM.value,)]
+ )
+
+ def test_access_locations_requiring_subway_destination_hades(self) -> None:
+ locations: List[str] = list()
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES.value,)]
+ )
+
+ def test_access_locations_requiring_subway_destination_monastery(self) -> None:
+ locations: List[str] = list()
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY.value,)]
+ )
+
+ def test_access_locations_requiring_teleporter_destination_dm_lair(self) -> None:
+ locations: List[str] = list()
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR.value,)]
+ )
+
+ def test_access_locations_requiring_teleporter_destination_gue_tech(self) -> None:
+ locations: List[str] = list()
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH.value,)]
+ )
+
+ def test_access_locations_requiring_teleporter_destination_hades(self) -> None:
+ locations: List[str] = list()
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES.value,)]
+ )
+
+ def test_access_locations_requiring_teleporter_destination_monastery(self) -> None:
+ locations: List[str] = list()
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY.value,)]
+ )
+
+ def test_access_locations_requiring_teleporter_destination_spell_lab(self) -> None:
+ locations: List[str] = list()
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB.value,)]
+ )
+
+ def test_access_locations_requiring_totemizer_destination_hall_of_inquisition(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.CLOSING_THE_TIME_TUNNELS.value,
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS.value,
+ ZorkGrandInquisitorLocations.PORT_FOOZLE_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.THE_ALCHEMICAL_DEBACLE.value,
+ ZorkGrandInquisitorLocations.THE_ENDLESS_FIRE.value,
+ ZorkGrandInquisitorLocations.THE_FLATHEADIAN_FUDGE_FIASCO.value,
+ ZorkGrandInquisitorLocations.THE_PERILS_OF_MAGIC.value,
+ ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION.value,)]
+ )
+
+ def test_access_locations_requiring_totemizer_destination_straight_to_hell(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.STRAIGHT_TO_HELL.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL.value,)]
+ )
+
+ def test_access_locations_requiring_totem_brog(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.BROG_DO_GOOD.value,
+ ZorkGrandInquisitorLocations.BROG_EAT_ROCKS.value,
+ ZorkGrandInquisitorLocations.BROG_KNOW_DUMB_THAT_DUMB.value,
+ ZorkGrandInquisitorLocations.BROG_MUCH_BETTER_AT_THIS_GAME.value,
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.DRAGON_ARCHIPELAGO_TIME_TUNNEL.value,
+ ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS.value,
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS.value,
+ ZorkGrandInquisitorLocations.THIS_DOESNT_LOOK_ANYTHING_LIKE_THE_BROCHURE.value,
+ ZorkGrandInquisitorLocations.UH_OH_BROG_CANT_SWIM.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.TOTEM_BROG.value,)]
+ )
+
+ def test_access_locations_requiring_totem_griff(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.DOOOOOOWN.value,
+ ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON.value,
+ ZorkGrandInquisitorLocations.THAR_SHE_BLOWS.value,
+ ZorkGrandInquisitorLocations.UUUUUP.value,
+ ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.TOTEM_GRIFF.value,)]
+ )
+
+ def test_access_locations_requiring_totem_lucy(self) -> None:
+ locations: List[str] = [
+ ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.DOWN.value,
+ ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER.value,
+ ZorkGrandInquisitorLocations.THIS_DOESNT_LOOK_ANYTHING_LIKE_THE_BROCHURE.value,
+ ZorkGrandInquisitorLocations.UP.value,
+ ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER.value,
+ ZorkGrandInquisitorEvents.VICTORY.value,
+ ]
+
+ self.assertAccessDependency(
+ locations, [(ZorkGrandInquisitorItems.TOTEM_LUCY.value,)]
+ )
diff --git a/worlds/zork_grand_inquisitor/test/test_data_funcs.py b/worlds/zork_grand_inquisitor/test/test_data_funcs.py
new file mode 100644
index 000000000000..9d8d5a4ba356
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/test/test_data_funcs.py
@@ -0,0 +1,132 @@
+import unittest
+
+from ..data_funcs import location_access_rule_for, entrance_access_rule_for
+from ..enums import ZorkGrandInquisitorLocations, ZorkGrandInquisitorRegions
+
+
+class DataFuncsTest(unittest.TestCase):
+ def test_location_access_rule_for(self) -> None:
+ # No Requirements
+ self.assertEqual(
+ "lambda state: True",
+ location_access_rule_for(ZorkGrandInquisitorLocations.ALARM_SYSTEM_IS_DOWN, 1),
+ )
+
+ # Single Item Requirement
+ self.assertEqual(
+ 'lambda state: state.has("Sword", 1)',
+ location_access_rule_for(ZorkGrandInquisitorLocations.DONT_EVEN_START_WITH_US_SPARKY, 1),
+ )
+
+ self.assertEqual(
+ 'lambda state: state.has("Spell: NARWILE", 1)',
+ location_access_rule_for(ZorkGrandInquisitorLocations.DRAGON_ARCHIPELAGO_TIME_TUNNEL, 1),
+ )
+
+ # Single Event Requirement
+ self.assertEqual(
+ 'lambda state: state.has("Event: Knows OBIDIL", 1)',
+ location_access_rule_for(ZorkGrandInquisitorLocations.A_BIG_FAT_SASSY_2_HEADED_MONSTER, 1),
+ )
+
+ self.assertEqual(
+ 'lambda state: state.has("Event: Dunce Locker Openable", 1)',
+ location_access_rule_for(ZorkGrandInquisitorLocations.BETTER_SPELL_MANUFACTURING_IN_UNDER_10_MINUTES, 1),
+ )
+
+ # Multiple Item Requirements
+ self.assertEqual(
+ 'lambda state: state.has("Hotspot: Purple Words", 1) and state.has("Spell: IGRAM", 1)',
+ location_access_rule_for(ZorkGrandInquisitorLocations.A_SMALLWAY, 1),
+ )
+
+ self.assertEqual(
+ 'lambda state: state.has("Hotspot: Mossy Grate", 1) and state.has("Spell: THROCK", 1)',
+ location_access_rule_for(ZorkGrandInquisitorLocations.BEAUTIFUL_THATS_PLENTY, 1),
+ )
+
+ # Multiple Item Requirements OR
+ self.assertEqual(
+ 'lambda state: (state.has("Totem: Griff", 1) or state.has("Totem: Lucy", 1)) and state.has("Hotspot: Mailbox Door", 1) and state.has("Hotspot: Mailbox Flag", 1)',
+ location_access_rule_for(ZorkGrandInquisitorLocations.MAILED_IT_TO_HELL, 1),
+ )
+
+ # Multiple Mixed Requirements
+ self.assertEqual(
+ 'lambda state: state.has("Event: Cigar Accessible", 1) and state.has("Hotspot: Grand Inquisitor Doll", 1)',
+ location_access_rule_for(ZorkGrandInquisitorLocations.ARREST_THE_VANDAL, 1),
+ )
+
+ self.assertEqual(
+ 'lambda state: state.has("Sword", 1) and state.has("Event: Rope GLORFable", 1) and state.has("Hotspot: Monastery Vent", 1)',
+ location_access_rule_for(ZorkGrandInquisitorLocations.I_HOPE_YOU_CAN_CLIMB_UP_THERE, 1),
+ )
+
+ def test_entrance_access_rule_for(self) -> None:
+ # No Requirements
+ self.assertEqual(
+ "lambda state: True",
+ entrance_access_rule_for(
+ ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.PORT_FOOZLE, 1
+ ),
+ )
+
+ self.assertEqual(
+ "lambda state: True",
+ entrance_access_rule_for(
+ ZorkGrandInquisitorRegions.DM_LAIR, ZorkGrandInquisitorRegions.CROSSROADS, 1
+ ),
+ )
+
+ # Single Requirement
+ self.assertEqual(
+ 'lambda state: (state.has("Map", 1))',
+ entrance_access_rule_for(
+ ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.CROSSROADS, 1
+ ),
+ )
+
+ self.assertEqual(
+ 'lambda state: (state.has("Map", 1))',
+ entrance_access_rule_for(
+ ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.CROSSROADS, 1
+ ),
+ )
+
+ # Multiple Requirements AND
+ self.assertEqual(
+ 'lambda state: (state.has("Spell: REZROV", 1) and state.has("Hotspot: In Magic We Trust Door", 1))',
+ entrance_access_rule_for(
+ ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.GUE_TECH, 1
+ ),
+ )
+
+ self.assertEqual(
+ 'lambda state: (state.has("Event: Door Smoked Cigar", 1) and state.has("Event: Door Drank Mead", 1))',
+ entrance_access_rule_for(
+ ZorkGrandInquisitorRegions.DM_LAIR, ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, 1
+ ),
+ )
+
+ self.assertEqual(
+ 'lambda state: (state.has("Hotspot: Closet Door", 1) and state.has("Spell: NARWILE", 1) and state.has("Event: Knows YASTARD", 1))',
+ entrance_access_rule_for(
+ ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, ZorkGrandInquisitorRegions.WHITE_HOUSE, 1
+ ),
+ )
+
+ # Multiple Requirements AND + OR
+ self.assertEqual(
+ 'lambda state: (state.has("Sword", 1) and state.has("Hotspot: Dungeon Master\'s Lair Entrance", 1)) or (state.has("Map", 1) and state.has("Teleporter Destination: Dungeon Master\'s Lair", 1))',
+ entrance_access_rule_for(
+ ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.DM_LAIR, 1
+ ),
+ )
+
+ # Multiple Requirements Regions
+ self.assertEqual(
+ 'lambda state: (state.has("Griff\'s Air Pump", 1) and state.has("Griff\'s Inflatable Raft", 1) and state.has("Griff\'s Inflatable Sea Captain", 1) and state.has("Hotspot: Dragon Nostrils", 1) and state.has("Griff\'s Dragon Tooth", 1) and state.can_reach("Port Foozle Past - Tavern", "Region", 1) and state.has("Lucy\'s Playing Card: 1 Pip", 1) and state.has("Lucy\'s Playing Card: 2 Pips", 1) and state.has("Lucy\'s Playing Card: 3 Pips", 1) and state.has("Lucy\'s Playing Card: 4 Pips", 1) and state.has("Hotspot: Tavern Fly", 1) and state.has("Hotspot: Alpine\'s Quandry Card Slots", 1) and state.can_reach("White House", "Region", 1) and state.has("Totem: Brog", 1) and state.has("Brog\'s Flickering Torch", 1) and state.has("Brog\'s Grue Egg", 1) and state.has("Hotspot: Cooking Pot", 1) and state.has("Brog\'s Plank", 1) and state.has("Hotspot: Skull Cage", 1))',
+ entrance_access_rule_for(
+ ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, ZorkGrandInquisitorRegions.ENDGAME, 1
+ ),
+ )
diff --git a/worlds/zork_grand_inquisitor/test/test_locations.py b/worlds/zork_grand_inquisitor/test/test_locations.py
new file mode 100644
index 000000000000..fa576dd510dc
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/test/test_locations.py
@@ -0,0 +1,49 @@
+from typing import Dict, Set
+
+from . import ZorkGrandInquisitorTestBase
+
+from ..data_funcs import location_names_to_location, locations_with_tag
+from ..enums import ZorkGrandInquisitorLocations, ZorkGrandInquisitorTags
+
+
+class LocationsTestNoDeathsanity(ZorkGrandInquisitorTestBase):
+ options = {
+ "deathsanity": "false",
+ }
+
+ def test_correct_locations_exist(self) -> None:
+ expected_locations: Set[ZorkGrandInquisitorLocations] = locations_with_tag(
+ ZorkGrandInquisitorTags.CORE
+ )
+
+ self._assert_expected_locations_exist(expected_locations)
+
+ def _assert_expected_locations_exist(self, expected_locations: Set[ZorkGrandInquisitorLocations]) -> None:
+ location_name_to_location: Dict[str, ZorkGrandInquisitorLocations] = location_names_to_location()
+
+ for location_object in self.multiworld.get_locations(1):
+ location: ZorkGrandInquisitorLocations = location_name_to_location.get(
+ location_object.name
+ )
+
+ if location is None:
+ continue
+
+ self.assertIn(location, expected_locations)
+
+ expected_locations.remove(location)
+
+ self.assertEqual(0, len(expected_locations))
+
+
+class LocationsTestDeathsanity(LocationsTestNoDeathsanity):
+ options = {
+ "deathsanity": "true",
+ }
+
+ def test_correct_locations_exist(self) -> None:
+ expected_locations: Set[ZorkGrandInquisitorLocations] = (
+ locations_with_tag(ZorkGrandInquisitorTags.CORE) | locations_with_tag(ZorkGrandInquisitorTags.DEATHSANITY)
+ )
+
+ self._assert_expected_locations_exist(expected_locations)
diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py
new file mode 100644
index 000000000000..a93f2c2134c1
--- /dev/null
+++ b/worlds/zork_grand_inquisitor/world.py
@@ -0,0 +1,205 @@
+from typing import Any, Dict, List, Tuple
+
+from BaseClasses import Item, ItemClassification, Location, Region, Tutorial
+
+from worlds.AutoWorld import WebWorld, World
+
+from .data.item_data import item_data, ZorkGrandInquisitorItemData
+from .data.location_data import location_data, ZorkGrandInquisitorLocationData
+from .data.region_data import region_data
+
+from .data_funcs import (
+ item_names_to_id,
+ item_names_to_item,
+ location_names_to_id,
+ item_groups,
+ items_with_tag,
+ location_groups,
+ locations_by_region,
+ location_access_rule_for,
+ entrance_access_rule_for,
+)
+
+from .enums import (
+ ZorkGrandInquisitorEvents,
+ ZorkGrandInquisitorItems,
+ ZorkGrandInquisitorLocations,
+ ZorkGrandInquisitorRegions,
+ ZorkGrandInquisitorTags,
+)
+
+from .options import ZorkGrandInquisitorOptions
+
+
+class ZorkGrandInquisitorItem(Item):
+ game = "Zork Grand Inquisitor"
+
+
+class ZorkGrandInquisitorLocation(Location):
+ game = "Zork Grand Inquisitor"
+
+
+class ZorkGrandInquisitorWebWorld(WebWorld):
+ theme: str = "stone"
+
+ tutorials: List[Tutorial] = [
+ Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to setting up the Zork Grand Inquisitor randomizer connected to an Archipelago Multiworld",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["Serpent.AI"],
+ )
+ ]
+
+
+class ZorkGrandInquisitorWorld(World):
+ """
+ Zork: Grand Inquisitor is a 1997 point-and-click adventure game for PC.
+ Magic has been banned from the great Underground Empire of Zork. By edict of the Grand Inquisitor Mir Yannick, the
+ Empire has been sealed off and the practice of mystic arts declared punishable by "Totemization" (a very bad thing).
+ The only way to restore magic to the kingdom is to find three hidden artifacts: The Coconut of Quendor, The Cube of
+ Foundation, and The Skull of Yoruk.
+ """
+
+ options_dataclass = ZorkGrandInquisitorOptions
+ options: ZorkGrandInquisitorOptions
+
+ game = "Zork Grand Inquisitor"
+
+ item_name_to_id = item_names_to_id()
+ location_name_to_id = location_names_to_id()
+
+ item_name_groups = item_groups()
+ location_name_groups = location_groups()
+
+ required_client_version: Tuple[int, int, int] = (0, 4, 4)
+
+ web = ZorkGrandInquisitorWebWorld()
+
+ filler_item_names: List[str] = item_groups()["Filler"]
+ item_name_to_item: Dict[str, ZorkGrandInquisitorItems] = item_names_to_item()
+
+ def create_regions(self) -> None:
+ deathsanity: bool = bool(self.options.deathsanity)
+
+ region_mapping: Dict[ZorkGrandInquisitorRegions, Region] = dict()
+
+ region_enum_item: ZorkGrandInquisitorRegions
+ for region_enum_item in region_data.keys():
+ region_mapping[region_enum_item] = Region(region_enum_item.value, self.player, self.multiworld)
+
+ region_locations_mapping: Dict[ZorkGrandInquisitorRegions, List[ZorkGrandInquisitorLocations]]
+ region_locations_mapping = locations_by_region(include_deathsanity=deathsanity)
+
+ region_enum_item: ZorkGrandInquisitorRegions
+ region: Region
+ for region_enum_item, region in region_mapping.items():
+ regions_locations: List[ZorkGrandInquisitorLocations] = region_locations_mapping[region_enum_item]
+
+ # Locations
+ location_enum_item: ZorkGrandInquisitorLocations
+ for location_enum_item in regions_locations:
+ data: ZorkGrandInquisitorLocationData = location_data[location_enum_item]
+
+ location: ZorkGrandInquisitorLocation = ZorkGrandInquisitorLocation(
+ self.player,
+ location_enum_item.value,
+ data.archipelago_id,
+ region_mapping[data.region],
+ )
+
+ if isinstance(location_enum_item, ZorkGrandInquisitorEvents):
+ location.place_locked_item(
+ ZorkGrandInquisitorItem(
+ data.event_item_name,
+ ItemClassification.progression,
+ None,
+ self.player,
+ )
+ )
+
+ location_access_rule: str = location_access_rule_for(location_enum_item, self.player)
+
+ if location_access_rule != "lambda state: True":
+ location.access_rule = eval(location_access_rule)
+
+ region.locations.append(location)
+
+ # Connections
+ region_exit: ZorkGrandInquisitorRegions
+ for region_exit in region_data[region_enum_item].exits or tuple():
+ entrance_access_rule: str = entrance_access_rule_for(region_enum_item, region_exit, self.player)
+
+ if entrance_access_rule == "lambda state: True":
+ region.connect(region_mapping[region_exit])
+ else:
+ region.connect(region_mapping[region_exit], rule=eval(entrance_access_rule))
+
+ self.multiworld.regions.append(region)
+
+ def create_items(self) -> None:
+ quick_port_foozle: bool = bool(self.options.quick_port_foozle)
+ start_with_hotspot_items: bool = bool(self.options.start_with_hotspot_items)
+
+ item_pool: List[ZorkGrandInquisitorItem] = list()
+
+ item: ZorkGrandInquisitorItems
+ data: ZorkGrandInquisitorItemData
+ for item, data in item_data.items():
+ tags: Tuple[ZorkGrandInquisitorTags, ...] = data.tags or tuple()
+
+ if ZorkGrandInquisitorTags.FILLER in tags:
+ continue
+ elif ZorkGrandInquisitorTags.HOTSPOT in tags and start_with_hotspot_items:
+ continue
+
+ item_pool.append(self.create_item(item.value))
+
+ total_locations: int = len(self.multiworld.get_unfilled_locations(self.player))
+ item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))]
+
+ self.multiworld.itempool += item_pool
+
+ if quick_port_foozle:
+ self.multiworld.early_items[self.player][ZorkGrandInquisitorItems.ROPE.value] = 1
+ self.multiworld.early_items[self.player][ZorkGrandInquisitorItems.LANTERN.value] = 1
+
+ if not start_with_hotspot_items:
+ self.multiworld.early_items[self.player][ZorkGrandInquisitorItems.HOTSPOT_WELL.value] = 1
+ self.multiworld.early_items[self.player][ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR.value] = 1
+
+ self.multiworld.early_items[self.player][
+ ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL.value
+ ] = 1
+
+ if start_with_hotspot_items:
+ item: ZorkGrandInquisitorItems
+ for item in items_with_tag(ZorkGrandInquisitorTags.HOTSPOT):
+ self.multiworld.push_precollected(self.create_item(item.value))
+
+ def create_item(self, name: str) -> ZorkGrandInquisitorItem:
+ data: ZorkGrandInquisitorItemData = item_data[self.item_name_to_item[name]]
+
+ return ZorkGrandInquisitorItem(
+ name,
+ data.classification,
+ data.archipelago_id,
+ self.player,
+ )
+
+ def generate_basic(self) -> None:
+ self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
+
+ def fill_slot_data(self) -> Dict[str, Any]:
+ return self.options.as_dict(
+ "goal",
+ "quick_port_foozle",
+ "start_with_hotspot_items",
+ "deathsanity",
+ "grant_missable_location_checks",
+ )
+
+ def get_filler_item_name(self) -> str:
+ return self.random.choice(self.filler_item_names)
diff --git a/worlds_disabled/README.md b/worlds_disabled/README.md
index b891bc71d4ba..a7bffe222b14 100644
--- a/worlds_disabled/README.md
+++ b/worlds_disabled/README.md
@@ -3,3 +3,11 @@
This folder is for already merged worlds that are unmaintained and currently broken. If you are interested in fixing and
stepping up as maintainer for any of these worlds, please review the [world maintainer](/docs/world%20maintainer.md)
documentation.
+
+## Information for Disabled Worlds
+
+For each disabled world, a README file can be found detailing when the world was disabled and the reasons that it
+was disabled. In order to be considered for reactivation, these concerns should be handled at a bare minimum. However,
+each world may have additional issues that also need to be handled, such as deprecated API calls or missing components.
+
+
diff --git a/worlds_disabled/oribf/README.md b/worlds_disabled/oribf/README.md
new file mode 100644
index 000000000000..0c78c23bea0d
--- /dev/null
+++ b/worlds_disabled/oribf/README.md
@@ -0,0 +1,7 @@
+### Ori and the Blind Forest
+
+This world was disabled for the following reasons:
+
+* Missing client
+* Unmaintained
+* Outdated, fails tests as of Jun 29, 2023